# Tokens — Faber Design System

> Gemelo en texto de https://design.wearefaber.com/tokens
> Paquete: `@pantherkit/design-system` v0.4.1
> Fuente de los valores: `src/tokens/*.css` del paquete. Los porqués de este
> documento están copiados de los comentarios de esos archivos, no reescritos.

## Cómo se usa esto

1. **Nunca escribas un hex, un radio, una duración o una curva a mano.** Todo
   sale de un `var(--token)` de esta lista.
2. Si necesitas una tinta con alfa, derívala con `color-mix()` desde un token.
   Un `rgba()` escrito a mano es deuda.
3. **Si el token que necesitas no existe, el token es lo que hay que crear**, en
   `src/tokens/` del paquete y con un comentario que diga por qué. No lo
   resuelvas con un literal en el componente.
4. Los tokens se importan antes que cualquier componente:
   ```css
   @import "@pantherkit/design-system/tokens.css";
   @import "@pantherkit/design-system/components.css";
   ```
   Los componentes no tienen ni un valor literal dentro: sin los tokens no
   pintan nada.

---

# 1 · Color

Todo el sistema vive sobre tinta (`#0b0b0c`), y no por gusto estético: la
paleta de marca está calibrada para fondo oscuro y sobre papel no cumple
accesibilidad. El naranja da 2.87:1 sobre blanco y 6.85:1 sobre tinta; el gris
de marca, 3.62:1 contra 5.43:1.

Todos los ratios de este documento están medidos sobre `#0b0b0c`. Umbrales
WCAG AA: 4.5:1 texto normal, 3:1 texto grande y elementos de interfaz.

## El lienzo: tinta

El sistema entero vive sobre tinta, y no por gusto: la paleta de marca está hecha para fondo oscuro y sobre papel no cumple accesibilidad. El naranja da 2.87:1 sobre blanco y 6.85:1 sobre tinta; el gris de marca, 3.62:1 contra 5.43:1. En oscuro la marca funciona íntegra.

### `--color-bg-primary`

```css
--color-bg-primary: #0b0b0c;
```

El lienzo. El nombre es semántico a propósito: no dice «negro», dice «el fondo». Por eso invertir el sistema se hace en un archivo y no componente por componente.

### `--color-bg-surface-1`

```css
--color-bg-surface-1: #141416;
```

**Contraste sobre `--color-bg-primary` (#0b0b0c): 1.07:1.**

Lo que se apoya sobre el lienzo sin despegarse: filas, celdas, campos.

### `--color-bg-surface-2`

```css
--color-bg-surface-2: #1c1c1f;
```

**Contraste sobre `--color-bg-primary` (#0b0b0c): 1.16:1.**

La tarjeta. Es la superficie que usa el botón secundario de fondo.

### `--color-bg-surface-3`

```css
--color-bg-surface-3: #26262a;
```

**Contraste sobre `--color-bg-primary` (#0b0b0c): 1.31:1.**

Lo que flota: menús, estados hover de una superficie. Sobre tinta la separación entre escalones es de centésimas de contraste — la elevación aquí la hace el borde, no el relleno.

---

## Texto: papel sobre tinta

Tres niveles y ninguno decorativo: cada uno tiene un trabajo. El contraste está medido sobre el lienzo #0b0b0c; el mínimo AA para texto normal es 4.5:1, así que los tres cumplen incluso a tamaño pequeño.

### `--color-text-primary`

```css
--color-text-primary: #f5f5f7;
```

**Contraste sobre `--color-bg-primary` (#0b0b0c): 18.07:1.**

El contenido. Blanco puro no: a 18:1 ya sobra contraste y el blanco absoluto vibra sobre negro.

### `--color-text-secondary`

```css
--color-text-secondary: #a8a8b0;
```

**Contraste sobre `--color-bg-primary` (#0b0b0c): 8.33:1.**

El párrafo de apoyo. Baja jerarquía sin bajar de AA.

### `--color-text-muted`

```css
--color-text-muted: #86868b;
```

**Contraste sobre `--color-bg-primary` (#0b0b0c): 5.43:1.**

El gris de marca. Sobre papel da 3.62:1 y no cumple; sobre tinta, 5.43:1 y sí. Es el ejemplo más limpio de por qué el sistema es oscuro.

---

## Acentos: un color, un significado

Naranja = el punto de decisión, donde interviene la persona. Azul = interactividad. No hay un tercer uso para el naranja, y esa es toda la regla.

### `--color-accent`

```css
--color-accent: #ff6a00;
```

**Contraste sobre `--color-bg-primary` (#0b0b0c): 6.85:1.**

El naranja de marca. El punto de decisión: el CTA, la cifra que importa, el paso donde entra la persona.

### `--color-accent-light`

```css
--color-accent-light: #ff8a33;
```

**Contraste sobre `--color-bg-primary` (#0b0b0c): 8.37:1.**

Para texto naranja pequeño y para hover. Más contraste que el acento, misma familia.

### `--color-accent-muted`

```css
--color-accent-muted: #3a1f0d;
```

Superficie tenue de acento, no color de texto: 1.29:1 sobre tinta, invisible como letra. Sirve para teñir un panel donde algo naranja va a pasar; el texto primario encima da 13.96:1.

---

## Interactivo

Aquí es donde el sistema toma partido: cuando la marca y la legibilidad chocan, gana la legibilidad, y el porqué se anota al lado del token.

### `--color-link`

```css
--color-link: #4da3ff;
```

**Contraste sobre `--color-bg-primary` (#0b0b0c): 7.49:1.**

El azul de marca es #0071e3 y sobre tinta da 4.19:1: por debajo del 4.5:1 que exige AA. Este cumple con margen. No es un azul «parecido»: es el azul que sí se lee.

### `--color-link-hover`

```css
--color-link-hover: #8cc4ff;
```

**Contraste sobre `--color-bg-primary` (#0b0b0c): 10.73:1.**

El hover aclara. Sobre tinta la respuesta es más luz, no más saturación.

### `--color-border`

```css
--color-border: rgba(255,255,255,0.12);
```

Sobre tinta la línea es luz, no sombra. Un borde oscuro sobre #0b0b0c se lee como un agujero, no como un contorno.

### `--color-border-hover`

```css
--color-border-hover: rgba(255,255,255,0.24);
```

El doble de luz. Es la señal de «esto responde» más barata que tiene el sistema.

---

## Portfolio: el sub-sistema frío

Siete tokens en oklch con el mismo matiz (255) para infraestructura y casos técnicos. Existen para que una sección de arquitectura no tenga que pedirle al naranja que signifique dos cosas. Es la única familia del sistema en oklch: se eligió porque una rampa de superficies necesita pasos perceptualmente iguales, y en hex hay que ajustarlos a ojo.

### `--color-portfolio-surface`

```css
--color-portfolio-surface: oklch(17% 0.012 255);
```

El lienzo frío de la sección.

### `--color-portfolio-surface-strong`

```css
--color-portfolio-surface-strong: oklch(21% 0.02 255);
```

La tarjeta sobre él.

### `--color-portfolio-surface-hover`

```css
--color-portfolio-surface-hover: oklch(25% 0.03 255);
```

Su estado hover.

### `--color-portfolio-pill`

```css
--color-portfolio-pill: oklch(24% 0.02 255);
```

El chip de tecnología.

### `--color-portfolio-border`

```css
--color-portfolio-border: oklch(31% 0.025 255);
```

El filo en reposo.

### `--color-portfolio-border-hover`

```css
--color-portfolio-border-hover: oklch(45% 0.05 255);
```

El filo cuando responde.

### `--color-portfolio-muted`

```css
--color-portfolio-muted: oklch(72% 0.03 255);
```

**Contraste sobre `--color-bg-primary` (#0b0b0c): 7.94:1.**

El único de la familia que lleva texto. Por eso es el único con ratio.

---

## Sombra

Opacidades bajas y radios largos: separan sin dibujar un borde negro. Sobre tinta la elevación casi no se ve, y forzarla es lo que produce el «agujero».

### `--color-shadow-ink`

```css
--color-shadow-ink: #000000;
```

La tinta de las sombras, y deliberadamente NO --color-bg-primary. Derivarla del lienzo funciona hoy —sobre #0b0b0c no se distingue— pero el día que el lienzo se aclare la sombra se aclara con él y deja de ser sombra. Una sombra es ausencia de luz, no el fondo.

### `--shadow-sm`

```css
--shadow-sm: 0 1px 3px rgba(0,0,0,.04);
```

Apoyado.

### `--shadow-md`

```css
--shadow-md: 0 4px 16px rgba(0,0,0,.06);
```

Levantado.

### `--shadow-lg`

```css
--shadow-lg: 0 12px 40px rgba(15,13,10,.10);
```

Flotando. Su tinta tira a cálido, no a negro puro.


---

## La regla que define al sistema: contraste > marca

`#0071e3` es el azul de `BRAND.md`. Sobre tinta da **4.19:1**, por debajo del
4.5:1 que exige AA para texto normal. El sistema **no lo usa**: usa
`--color-link: #4da3ff`, que da **7.49:1**.

**Cuando marca y legibilidad chocan, gana la legibilidad, y el porqué se anota
al lado del token.** Esto no es negociable por diseño ni por dirección de arte.
Si alguien pide "el azul de marca" para un enlace, la respuesta es este token y
esta línea.

## El presupuesto del naranja

**Un solo `--color-accent` por sección. Si aparece dos veces, sobra una.**

El naranja significa una cosa concreta: *aquí interviene la persona* — el CTA,
la cifra que decide, el paso donde alguien tiene que actuar. Un significado
sólo se sostiene mientras sea escaso: dos naranjas en una pantalla no gritan el
doble, se anulan.

- **Sí:** el CTA principal de la sección; la métrica que justifica el caso.
- **No:** un icono decorativo, un borde, un hover cualquiera, un segundo botón
  "también importante". Para interactividad está el azul.
- Para texto naranja pequeño usa `--color-accent-light` (8.37:1), no
  `--color-accent` (6.85:1): los dos cumplen, pero el claro aguanta mejor el
  peso ligero.
- `--color-accent-muted` es superficie, no texto.

## Cuándo NO usar los tokens de color

- **No uses `--color-bg-surface-*` para crear jerarquía por sí solos.** Sobre
  tinta la diferencia entre escalones es de centésimas de contraste (1.07 →
  1.16 → 1.31). La elevación real la hace `--color-border`, no el relleno.
- **No derives sombras de `--color-bg-primary`.** Existe
  `--color-shadow-ink: #000000` justamente para eso: una sombra es ausencia de
  luz, no el fondo. Si el lienzo se aclara algún día, una sombra derivada del
  fondo se aclara con él y deja de ser sombra.
- **No mezcles la familia `--color-portfolio-*` con la principal en la misma
  sección.** Es un sub-sistema frío completo para infraestructura y casos
  técnicos; usarlo a medias produce una sección que parece dos.

---

# 2 · Tipografía

Dos familias y ninguna más. La jerarquía se construye con tamaño y tracking,
no metiendo una tercera fuente.

### `--font-sans`

```css
--font-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
```

Para todo. La jerarquía se construye con tamaño y tracking, no metiendo una tercera fuente.

### `--font-mono`

```css
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
```

Una señal técnica, no una alternativa estética: marca índices, duraciones y metadata de documento. Nunca párrafos.

### `--tracking-display`

```css
--tracking-display: -0.028em;
```

De 48px para arriba. El interletraje por defecto de Jakarta está calibrado para texto corrido; a tamaño display abre demasiado y el titular se desarma en palabras sueltas.

### `--tracking-heading`

```css
--tracking-heading: -0.012em;
```

De 24 a 40px. Se cierra al crecer, y sólo ahí: en body el tracking negativo pega las letras y cuesta leer.

### `--tracking-subheading`

```css
--tracking-subheading: -0.012em;
```

Mismo valor que heading, nombre distinto: el día que un subtítulo tenga que separarse de un H2 se cambia uno sin tocar el otro.


## Cuándo NO

- **`--font-mono` nunca en párrafos.** Es una señal técnica: índices,
  duraciones, rutas, metadata de documento, microcopy de instrucción. Un texto
  corrido en mono se lee como código y ralentiza la lectura.
- **`--tracking-display` no baja de 48px.** En body el tracking negativo pega
  las letras y cuesta leer; el valor está calibrado para tamaño display.
- No existe un `--tracking-body`: el valor correcto para texto corrido es el
  de la fuente, que ya viene calibrado. No lo toques.

---

# 3 · Espacio y radios

### `--spacing`

```css
--spacing: 0.25rem  (4px);
```

Una sola raíz de la que sale toda la escala: p-4, gap-6, mt-10 son múltiplos de esto. 4px es el mínimo común divisor de los espaciados que ya usaba la landing (0.5, 0.75, 1, 1.5, 1.75, 2.5, 3.5rem). Con una raíz mayor esos ritmos no se pueden expresar y vuelven los valores sueltos; con una menor la escala deja de restringir nada.


Las utilidades de Tailwind (`p-4`, `gap-6`, `mt-10`) son múltiplos de este
valor. Si un espaciado no cae en la escala, la pregunta correcta no es "qué
número pongo", es "por qué esta pieza necesita un ritmo distinto al resto".

## Radios

El radio comunica escala: cuanto más grande la superficie, más radio necesita para que la curva se lea igual de suave desde la misma distancia. Por eso la escala crece con la pieza y no se elige a ojo.

### `--radius-sm`

```css
--radius-sm: 4px;
```

Chips y pills pequeñas: apenas matar la esquina.

### `--radius-md`

```css
--radius-md: 8px;
```

Inputs y botones.

### `--radius-lg`

```css
--radius-lg: 12px;
```

Cards.

### `--radius-xl`

```css
--radius-xl: 16px;
```

Paneles y contenedores grandes.

### `--radius-full`

```css
--radius-full: 9999px;
```

La píldora: forma, no radio. No se interpola hacia aquí — animar de 12px a 9999px no da una píldora, da una cápsula deformándose a mitad de camino.


**Cuándo NO:** no interpoles hacia `--radius-full`. Animar de 12px a 9999px no
da una píldora, da una cápsula deformándose a mitad de camino. `--radius-full`
es una forma, no un punto de una escala.

---

# 4 · Movimiento

Regla que acompaña a estos tokens: **se anima lo que está en pantalla, una
pasada, y nada en bucle sin motivo.** Si se repite, es un anuncio.

## Curvas

### `--ease-spring`

```css
--ease-spring: linear(0, 0.0895, … 0.999)  · 23 puntos;
```

La curva de la casa, y no es una bezier dibujada a ojo: es un oscilador amortiguado real (rigidez 300, amortiguación 22, masa 1) muestreado en 23 puntos. Pasa de 1 y vuelve — ese rebote es lo que hace que el movimiento se lea como materia con masa y no como una interpolación. Que el enlace y la columna compartan física es lo que hace que una sección se sienta de una pieza y no de tres piezas pegadas.

### `--ease-out-expo`

```css
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
```

Para lo que NO debe rebotar: entradas de scroll, opacidades, revelados. Sale disparada y frena larguísimo, así que el elemento ya está colocado mucho antes de que la transición termine y no se percibe espera. Sin sobrepaso: en un fade un rebote se ve como un parpadeo.

### `--ease-out-quint`

```css
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
```

easeOutQuint. Se parece a la expo pero frena antes y más suave. La ficha de proyecto la usaba literal y el patrón React la había sustituido en silencio por la expo, que no es lo mismo. Tener las dos con nombre es lo que evita que vuelvan a confundirse.


### El valor completo de `--ease-spring`

```css
--ease-spring: linear(0, 0.0895, 0.2871, 0.5121, 0.7156, 0.874, 0.9818, 1.0442, 1.0713, 1.0747, 1.0644, 1.0482, 1.0314, 1.0171, 1.0064, 0.9995, 0.9957, 0.9944, 0.9945, 0.9955, 0.9968, 0.998, 0.999);
```

23 puntos. Máximo: 1.0747 — es decir, **se pasa un
7.5% del destino y vuelve**. Ese exceso es la razón de ser del token:
es lo que hace que el movimiento se lea como materia con masa. Un
`cubic-bezier` no puede sobrepasar y volver con esta forma; por eso es un
`linear()` y no una bezier.

### `--ease-out-expo` y `--ease-out-quint` NO son la misma curva

Este error ya se cometió: la ficha de proyecto usaba la quint literal y el
patrón en React la había sustituido en silencio por la expo. Nadie lo notó
mirando.

| | expo | quint |
|---|---|---|
| valor | `cubic-bezier(0.16, 1, 0.3, 1)` | `cubic-bezier(0.22, 1, 0.36, 1)` |
| carácter | sale disparada, frena larguísimo | frena antes y más suave |
| para qué | entradas de scroll, opacidades, revelados | movimiento de una pieza concreta que no debe rebotar |

Separación máxima entre las dos: **9.5 puntos porcentuales
de recorrido**, alrededor del ms 70 de una animación de
600ms. Es poco en una tabla y perfectamente visible en pantalla. **No las
sustituyas una por otra "porque son casi iguales".**

## Duraciones

Tres principales y un hueco intermedio, no una rampa: cada una corresponde a un
tipo de suceso distinto. Si dudas entre dos, el suceso está mal definido.

### `--dur-fast`

```css
--dur-fast: 90ms;
```

La pulsación. Por debajo de ~100ms la respuesta se percibe como causada por el dedo, no como una animación que el sistema decidió tocar. Es el techo del feedback directo: hover, active, foco.

### `--dur-base`

```css
--dur-base: 250ms;
```

El cambio de estado normal: abrir, cerrar, entrar, salir. Lo bastante largo para que el ojo siga de dónde viene la cosa, lo bastante corto para no cobrarle la espera a quien ya sabe lo que va a pasar.

### `--dur-medium`

```css
--dur-medium: 450ms;
```

Entre base y slow no había nada, y una hoja que entra en 250ms se siente brusca mientras que 600ms la vuelve pesada. Lo pidieron por su cuenta los workers de varias piezas, así que no era capricho de uno.

### `--dur-slow`

```css
--dur-slow: 600ms;
```

Lo que tarda el muelle en asentarse. Es la duración que EXIGE --ease-spring: cortarla antes deja el rebote a medio camino y el elemento aterriza fuera de sitio. Sólo para movimiento con --ease-spring.


**Emparejamiento obligatorio:** `--ease-spring` va con `--dur-slow`. Cortarla
antes deja el rebote a medio camino y el elemento aterriza fuera de sitio.

## Animaciones

### `--animate-fade-up`

```css
--animate-fade-up: fade-up 0.6s ease both;
```

La entrada por defecto. Sube 24px y aparece.

### `--animate-fade-in`

```css
--animate-fade-in: fade-in 0.4s ease both;
```

Cuando no debe haber desplazamiento, sólo presencia.

### `--animate-slide-in-left`

```css
--animate-slide-in-left: slide-in-left 0.5s ease both;
```

Entrada lateral desde -30px.

### `--animate-slide-in-right`

```css
--animate-slide-in-right: slide-in-right 0.5s ease both;
```

Su espejo, desde +30px.


Todas llevan `both`: **el movimiento es el mensajero, no el mensaje** — el
estado final se queda.

## Accesibilidad del movimiento

El paquete aplica `prefers-reduced-motion` de forma global: duraciones a
**0.01ms, no a 0**. Con 0 algunos motores no disparan `transitionend` y las
secuencias encadenadas se quedan colgadas. Se anula la duración, no la
propiedad, y **el estado final sí se queda**: se va el movimiento, no la señal
de que algo respondió.

Si escribes una secuencia que depende de `transitionend` o
`animationend`, funciona igual con la preferencia activada. Ese es el motivo
del 0.01ms.

---

# Recuento

| Familia | Tokens | Archivo en el paquete |
|---|---|---|
| Color | 25 | `src/tokens/color.css` |
| Tipografía | 5 | `src/tokens/typography.css` |
| Espacio | 1 | `src/tokens/space.css` |
| Radios | 5 | `src/tokens/radius.css` |
| Movimiento | 11 | `src/tokens/motion.css` |

# Discrepancia conocida

Los comentarios de `src/tokens/color.css` anotan los ratios calculados sobre
`#111111` (naranja 6.58:1, link 7.19:1, muted 5.21:1, azul de marca 4.02:1),
no sobre el lienzo real del sistema `#0b0b0c`. **Este documento usa los ratios
recalculados sobre el lienzo real**, que son algo más altos porque la tinta es
más oscura que `#111111`. Ninguna conclusión cambia —el azul de marca sigue
sin llegar a 4.5:1 y el resto sigue cumpliendo— pero las cifras del paquete
están medidas contra un fondo que el sistema no usa y conviene corregirlas en
origen.

---

*Página HTML equivalente: https://design.wearefaber.com/tokens*
