Faber Design System / Tokens

Cuatro familias, y el porqué de cada una.

Un token no es un valor con nombre bonito: es una decisión que ya se tomó, escrita donde no hay que volver a tomarla. Lo que sigue son los valores que hay hoy en @pantherkit/design-system, y al lado la razón por la que son esos y no otros.

Todas las muestras de esta página se pintan con var(--token). Ninguna es una imagen ni un valor copiado: si el paquete cambia, cambian solas. ¿Vienes a leer y no a mirar? /tokens.md.

1 · Color

Cada ratio de esta sección está medido sobre el lienzo real del sistema, #0b0b0c. El mínimo que exige WCAG AA para texto normal es 4.5:1; para texto grande, 3:1.

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 #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 #141416 1.07:1 sobre tinta

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

  • --color-bg-surface-2 #1c1c1f 1.16:1 sobre tinta

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

  • --color-bg-surface-3 #26262a 1.31:1 sobre tinta

    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 #f5f5f7 18.07:1 sobre tinta

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

  • --color-text-secondary #a8a8b0 8.33:1 sobre tinta

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

  • --color-text-muted #86868b 5.43:1 sobre tinta

    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 #ff6a00 6.85:1 sobre tinta

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

  • --color-accent-light #ff8a33 8.37:1 sobre tinta

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

  • --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 #4da3ff 7.49:1 sobre tinta

    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 #8cc4ff 10.73:1 sobre tinta

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

  • --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 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 oklch(17% 0.012 255)

    El lienzo frío de la sección.

  • --color-portfolio-surface-strong oklch(21% 0.02 255)

    La tarjeta sobre él.

  • --color-portfolio-surface-hover oklch(25% 0.03 255)

    Su estado hover.

  • --color-portfolio-pill oklch(24% 0.02 255)

    El chip de tecnología.

  • --color-portfolio-border oklch(31% 0.025 255)

    El filo en reposo.

  • --color-portfolio-border-hover oklch(45% 0.05 255)

    El filo cuando responde.

  • --color-portfolio-muted oklch(72% 0.03 255) 7.94:1 sobre tinta

    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 #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 0 1px 3px rgba(0,0,0,.04)

    Apoyado.

  • --shadow-md 0 4px 16px rgba(0,0,0,.06)

    Levantado.

  • --shadow-lg 0 12px 40px rgba(15,13,10,.10)

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

Cuando marca y legibilidad chocan

El azul del sistema no es el azul de marca.

Agenda 30 minutos

#0071e3 · el azul de BRAND.md

4.19:1 — no cumple AA

Agenda 30 minutos

--color-link · #4da3ff

7.49:1 — cumple con margen

Es el único hex suelto de toda esta página, y está aquí porque es el que no se usa. Sobre tinta, el azul de marca se queda en 4.19:1 y el mínimo son 4.5:1 — se lee «casi bien», que en accesibilidad significa que alguien no lo lee. El sistema no negoció un azul parecido: eligió el que sí se lee y anotó el porqué junto al token, para que dentro de un año nadie lo «corrija» de vuelta.

El presupuesto del naranja

Uno por sección. Si aparece dos veces, sobra una.

El naranja significa aquí interviene la persona: el CTA, la cifra que decide, el paso donde alguien tiene que hacer algo. Un significado sólo se sostiene mientras sea escaso — dos naranjas en una pantalla no gritan el doble, se anulan, y el ojo deja de saber cuál de los dos era el importante. No es una guía de estilo: es un presupuesto, y se gasta una vez.

Este panel se tiñe con --color-accent-muted y gasta su naranja en un solo sitio: el rótulo. El botón de abajo no es naranja, y es a propósito.

2 · Tipografía

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

Una decisión, escrita una vez.

--font-sans

RUN 04 · 250ms

--font-mono

Sin tracking display

letter-spacing: normal — a este tamaño el titular se desarma en palabras sueltas

Con tracking display

--tracking-display: -0.028em — la línea vuelve a leerse como una frase

3 · Espacio y radios

--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.

  • ×1 4px
  • ×2 8px
  • ×3 12px
  • ×4 16px
  • ×6 24px
  • ×8 32px
  • ×10 40px
  • ×12 48px
  • ×16 64px
  • ×24 96px

Cada barra mide calc(var(--spacing) * n). No hay un solo píxel escrito a mano.

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.

4 · Movimiento

La física del sistema. Una curva no se documenta con su texto: un linear() de 23 números no le dice nada a nadie. Aquí se ven corriendo.

--ease-spring, corriendo

Un oscilador amortiguado real —rigidez 300, amortiguación 22, masa 1— muestreado en 23 puntos. Se pasa un 7.5% de su destino y vuelve. Ese exceso es todo: es lo que hace que el movimiento se lea como materia con masa y no como una interpolación.

La línea es el destino. El cuadro la cruza y retrocede: eso es el 7.5%.

--dur-slow · 600ms. Cortarla antes deja el rebote a medio camino.

--ease-out-quint y --ease-out-expo no son la misma curva

Se parecen lo bastante como para que ya haya pasado: 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ó en una captura. Puestas a correr a la vez, la diferencia máxima entre las dos es de 9.5 puntos de recorrido, alrededor del milisegundo 70 de una animación de 600ms — la expo ya está prácticamente colocada cuando la quint todavía viene frenando.

--ease-spring

--ease-out-expo

--ease-out-quint

--ease-spring --ease-out-expo --ease-out-quint · la línea de puntos es el 100% del recorrido

prefers-reduced-motion no es opcional

El paquete lo aplica global: si el sistema operativo pide menos movimiento, todas las duraciones bajan a 0.01ms. A cero no —con 0 algunos motores no disparan transitionend y las secuencias encadenadas se quedan colgadas. Y el estado final sí se queda: se va el movimiento, no la señal de que algo respondió. Los botones de esta página siguen funcionando con la preferencia activada; simplemente el cuadro aparece ya colocado.