Radio 12px, padding 24px, gap 16px, borde rgba(0,0,0,.05). Tres capas en reposo, cuatro en hover. El borde es un alfa, no un token: se oscurece con lo que haya debajo.
Attio es un CRM y su home se dibuja como software: ventanas de producto flotando sobre blanco, sin una sola ilustración.
Su paleta real son once negros azulados, nueve blancos y un único azul, #266DF0,
que no pinta ni un botón: sólo links, focus rings y el borde de la fila seleccionada.
El CTA primario es #202124 y lleva encima un degradado radial cuyo foco está
fuera del botón, diez por ciento por encima de su borde superior. En hover, ese foco
se enciende. Y se enciende de verdad: los dos stops están registrados con
@property, así que el navegador los interpola.
#FFFFFF. No hay color de fondo de marca: el color entra sólo por los mocks de producto y por un wash lavanda muy tenue abajo.Talk to sales es un outline de borde #CAD0D9. Start for free es negro #202124 con borde #505967. Ni rastro de azul.opacity:0 y hubo que neutralizarlos para que el titular se viera.
Mira los canales. En todos, el azul supera al rojo: #1C1D1F es
rgb(28, 29, 31), #505967 es rgb(80, 89, 103),
#6F7988 es rgb(111, 121, 136). La diferencia crece según bajas por
la rampa. Sólo black-0 es negro puro, y su único trabajo es el fondo del
banner superior. Meter un gris neutro (#71717A) en este sistema desafina
inmediatamente contra el azul.
El canvas es #FFFFFF, no un gris casi blanco. Attio no usa un fondo
"roto": usa blanco puro y deja que las secciones alternas bajen a #FAFAFB.
El paso entre 100 y 200 es de un 2% de luminosidad, y aun así se ve, porque no hay
ruido de color compitiendo.
#266DF0 no pinta ni un solo botón en el tema
por defecto. Su trabajo entero es: link-foreground, el focus-ring
al 30% (#266DF04D), el accent-stroke y el borde de 1.08px de la fila
seleccionada en los mocks. Si pintas un CTA de azul, el sistema deja de leerse como Attio
y empieza a leerse como cualquier otro SaaS. El botón de Attio es negro.
Rol (--internal-color-…) | :root (claro) | .dark | Para qué |
|---|---|---|---|
| primary-background | #FFFFFF | #101010 | El canvas |
| secondary-background | #FAFAFB | #1C1D1F | Secciones que alternan |
| surface-subtle | #F3F4F6 | #232529 | Fondo del ghost en hover |
| surface | #EDEFF3 | #2E3238 | Fondo del ghost al pulsar |
| weak-stroke | #EDEFF3 | #232529 | El borde de la píldora |
| subtle-stroke | #E4E7EC | #2E3238 | El divisor por defecto |
| default-stroke | #D3D8DF | #6F7988 | El borde del input |
| strong-stroke | #CAD0D9 | #8F99A8 | El borde del outline |
| accent-stroke | #709FF5 | #709FF5 | El único rol que no cambia con el tema |
| primary-foreground | #1C1D1F | #FFFFFF | La tinta |
| secondary-foreground | #232529 | #EDEFF3 | El texto de la píldora |
| tertiary-foreground | #505967 | #B5BDC9 | Texto de apoyo |
| accent-foreground | #6F7988 | #8F99A8 | El párrafo del hero |
| caption-foreground | #A4ADBA | #505967 | Metadatos. 2.4:1 — decorativo |
| link-foreground | #266DF0 | #709FF5 | Links |
| focus-ring | #266DF04D (30%) | #709FF599 (60%) | El azul, tocando un control |
El cuerpo de Attio pesa 500, no 400. Cada token de texto del sistema trae
--text-*--font-weight: 500 de fábrica: no hay que pedirlo. No es un descuido,
es lo que hace que un CRM lleno de tablas se lea denso y firme en lugar de acuoso. El peso
400 existe, pero hay que invocarlo a mano y prácticamente sólo aparece en el serif.
El serif es la única familia del sistema que va a peso 400 y con tracking cero. Aparece en citas y en las notas del changelog, nunca en la interfaz.
| Token | Tamaño | Line-height | Peso | Tracking |
|---|---|---|---|---|
| text-xs | 12px | 18px | 500 | 0 |
| text-sm | 14px | 20px | 500 | −0.005em |
| text-base | 16px | 22px | 500 | −0.01em |
| text-lg | 18px | 24px | 500 | −0.01em |
| text-xl | 20px | 26px | 500 | −0.01em |
| text-2xl | 24px | 30px | 500 | −0.01em |
| heading-xs | 28px | 34px | 600 | −0.01em |
| heading-sm | 32px | 36px | 600 | −0.01em |
| heading-md | 40px | 44px | 600 | −0.01em |
| heading-lg | 56px | 60px | 600 | −0.015em |
| heading-xl | 64px | 64px | 600 | −0.02em |
La fila resaltada es la única con tracking 0. A 12px, apretar el texto lo hace ilegible, así que la curva se detiene. En todo lo demás, la regla es monótona: más grande, más apretado.
font-size: clamp(64px, calc(16px + 5.333svh), 80px);
letter-spacing: clamp(-2.4px, calc(2.08px - 0.3733svh), -1.28px);
line-height: 0.95;
svh, no vw. El
titular crece con la altura de la pantalla, para que en un portátil de pantalla baja el hero
encoja y los mocks de producto sigan cabiendo debajo. La composición manda sobre la
tipografía. Segunda: el tracking va al revés que el tamaño. Cuando svh
sube, el tamaño sube (+5.333/unidad) y el tracking baja (−0.3733/unidad, ya en
negativo). Es la regla óptica clásica, escrita como una interpolación lineal en CSS.
Attio no tiene una sombra. Tiene siete, y cada una es una función exacta de la anterior. Lee las columnas: el desplazamiento va 1, 2, 4, 8, 16, 32, 64. El blur va 3, 4, 8, 16, 32, 64, 128. El spread va 0, −1, −2, −4, −8, −16, −32. Y la opacidad va 1%, 2%, 3%, 4%, 5%, 6%, 7%. Cuatro progresiones geométricas en paralelo, y una aritmética para la opacidad. No es una sombra que alguien afinó a ojo: es una fórmula.
Y no se usan sueltas. Se usan acumuladas. shadow-attio-6 son
seis sombras a la vez, y ninguna pasa del 6% de negro:
Attio construye profundidad apilando capas casi invisibles, cada una el doble de
grande y un uno por ciento más oscura que la anterior. El resultado se comporta como la luz
real: nítida y densa cerca del objeto, enorme y difusa lejos. Una sola sombra de
0 20px 40px rgba(0,0,0,.15) nunca hace eso — se ve como una mancha.
Y elevar es subir un peldaño, no cambiar la sombra. El hover de una tarjeta pasa de
shadow-3 a shadow-4: las tres capas que ya estaban no se tocan,
sólo se añade la cuarta. Como la cuarta es el doble de la tercera, la caída duplica su alcance
sin que la base se mueva. La tarjeta se despega del papel.
Geometría compartida (medida): alto 36px (h-9), radio
10px, padding 0 12px, gap 6px, borde de 1px siempre
presente (aunque sea transparente), label de 14px / 500 / lh 20px / tracking
−0.005em. La variante grande — la del hero y la de móvil — sube a 46px
(h-11.5), radio 12px, padding 0 14px, label de 16px.
El radio 10px no es un token. Los radios del sistema son 2/4/6/8/12/16/20. El botón usa
rounded-[10px], un valor arbitrario que cae justo entre lg (8) y
xl (12). Alguien lo miró a 36px de alto y decidió que 8 era demasiado cuadrado
y 12 demasiado blando. Ese medio punto es el sitio.
El icono cambia el padding, no el layout. Un icono es ópticamente más ligero que una
letra, así que necesita menos aire de su lado. Attio lo resuelve en el propio botón, sin
variantes ni props:
.button:has(> svg:last-child) { padding-right: 8px; }. Compara arriba
"Start for free" con "Crear deal →": el padding derecho baja de 12px a 8px.
El primario no es azul. Es #202124 con borde #505967. Y lleva
encima un radial-gradient(at center -10%, …): el centro del degradado está
fuera del botón, un diez por ciento por encima de su borde superior. En reposo, los dos
stops valen lo mismo — un radial de #202124 a #202124 es un
rectángulo plano. El degradado está montado, pero apagado.
En hover sólo cambia --cta-from, que es el stop del centro del radial. Al
aclararse a #505967, aparece un foco que entra desde arriba y se desvanece hacia
los bordes inferiores. El botón no cambia de color: se ilumina.
Y funciona porque los dos stops están registrados:
@property --cta-from { syntax: "<color>"; inherits: false; }. Sin
@property, una custom property es una cadena de texto y el navegador la
cambia de golpe — el botón parpadearía. Con syntax: "<color>", el navegador
sabe que es un color y lo interpola. El degradado se anima. Es la única forma de animar
un background-image sin recurrir a un pseudo-elemento y un opacity.
cubic-bezier(.2, 0, 0, 1).
Entrar cuesta 50ms. Salir cuesta 300ms. Es lo contrario del transition: all .2s
simétrico que sale por defecto en todas partes, y es exactamente la diferencia entre un control
que se siente vivo y uno que se siente laggy. La curva por defecto,
cubic-bezier(.2, 0, 0, 1), tiene los dos puntos de control pegados a los extremos
verticales: arranca casi de golpe y frena larguísimo. Se parece más a algo con masa que a una
interpolación.
Outline: en hover no pasa nada más que el borde. Ni el fondo, ni el texto, ni una
sombra, ni un transform. El borde salta de #CAD0D9 (white-800) a
#6F7988 (black-700): cruza de la rampa de blancos a la de negros de un salto.
Es un cambio de contraste enorme — de 1.4:1 a 4.6:1 contra el blanco — hecho con un solo valor.
El relleno #F3F4F6 se reserva para confirmar la acción al pulsar, no para
anticiparla.
Ghost: el espejo exacto. Aquí sólo se mueve el fondo (transparente →
#F3F4F6 → #EDEFF3). Y el borde sigue existiendo, transparente, para
que el botón no cambie de tamaño ni empuje a sus vecinos al convertirse en otra variante.
Primario: se mueve el degradado. Outline: se mueve el borde.
Ghost: se mueve el fondo. Cada variante tiene exactamente una propiedad que
reacciona, y ninguna se repite. Y ningún botón de Attio anima transform: no escalan,
no se desplazan, no ganan sombra en hover.
Una tarjeta de Attio es fondo blanco + borde de un alfa muy bajo + radio 12px + N capas de la escalera. No hay más. La variación entre tarjetas es la N. Pasa el ratón: cada una sube un peldaño.
Radio 12px, padding 24px, gap 16px, borde rgba(0,0,0,.05). Tres capas en reposo, cuatro en hover. El borde es un alfa, no un token: se oscurece con lo que haya debajo.
Cinco capas. La quinta es 0 16px 32px −8px al 5%: el primer peldaño donde la caída empieza a leerse como distancia y no como contacto.
Seis capas, y ninguna pasa del 6%. La sexta mide 0 32px 64px −16px. El spread negativo de 16px impide que la sombra desborde por los lados: sólo asoma por debajo.
El radio cambia con el breakpoint, y el borde también. En móvil la tarjeta es
rounded-md (6px) y no tiene borde; a partir de lg (992px) sube a
rounded-xl (12px) y aparece el lg:border lg:border-black-0/5. Una tarjeta
que ocupa el ancho completo de un móvil no necesita borde: sus límites son los del viewport.
Tres detalles medidos, y ninguno es un accidente:
1 · El borde mide 1.08px. No es un error de redondeo. La maqueta de producto
se sirve a una escala fraccionaria: en móvil el mismo bloque lleva 0.54px,
exactamente la mitad. Attio escaló el diseño de la app real y arrastró el borde con
él en lugar de redondearlo a 1px. Lo mismo con el padding: 6.5px en móvil,
13px en desktop. Es la misma fila de la app, a otra escala.
2 · El halo es azul. 0 0 2px rgba(28, 40, 64, 0.18) — un difuso sin
desplazamiento, teñido de azul marino, no de negro. Sobre un canvas frío, un halo negro
se vería sucio. Es la única sombra del sitio que no sale de la escalera.
3 · El borde de selección es el azul de marca, #266DF0. Es el único sitio,
junto al focus ring y a los links, donde el azul toca algo. Pasa el ratón por la tabla: el borde
se aclara a #709FF5 (accent-stroke) y cada fila coge
#F3F4F6 de fondo.
El borde de las cartas del fondo es rgba(56, 62, 71, 0.1) — el
black-500 al 10%. Con alfa, no opaco, para que las cartas se oscurezcan entre sí
al solaparse. Un color sólido habría hecho que las tres se vieran igual de fuertes, y la baraja
habría perdido la profundidad.
rgba(243, 244, 246, 0.8) — white-300 al 80%. Radio 16px. Padding 6px, y padding-top: 0.
El blur se desactiva durante el scroll.
[data-hero-scrolling] .panel { backdrop-filter: none; }.
backdrop-filter es caro y a 60fps se nota. Attio lo apaga mientras el hero se mueve
y lo vuelve a encender al parar. Es la clase de decisión que sólo tomas si has medido el
frame budget.
La píldora del hero son dos elementos. El <a> exterior lleva
sólo el borde (1px solid #EDEFF3, radio 13px, y en
:active pasa a #E4E7EC). El <div> interior lleva
sólo el fondo (blanco, y en hover #FBFBFC). Ninguno de los dos hace las dos
cosas. Es la misma disciplina que separa el outline del ghost, aplicada dentro de un único
componente.
#FBFBFC no es un token. Está entre white-100
(#FFFFFF) y white-200 (#FAFAFB) — es un valor arbitrario
todavía más sutil que el paso más pequeño de la rampa. Un cambio de 1.5% de luminosidad.
Y el radio, 13px, tampoco está en la escala.
El padding es asimétrico: 5px 7px 5px 11px. Once píxeles a la izquierda,
donde va el texto; siete a la derecha, donde va el chevron de 12×12. El mismo principio óptico
que el :has() de los botones, aquí escrito a mano.
El input comparte la geometría del botón: 36px de alto, radio 10px, padding 0 12px, label de
14px. En reposo el borde es default-stroke (#D3D8DF); en hover
sube a #6F7988, igual que el outline. Y al enfocar aparece el único sitio del
sistema donde el azul toca un control: box-shadow: 0 0 0 3px #266DF04D — el
blue-500 al 30%. Dale al tabulador y míralo.
El primario, además del ring, aplica su estado de hover en :focus-visible:
el foco del degradado se enciende igual. El teclado ve exactamente lo mismo que el ratón.
Cada franja tiene su par -visible-height / -hidden-height: 0px, y
mostrar u ocultar una es reasignar una variable. Nada más abajo en la página necesita
enterarse: el scroll-margin global de las anclas ya se calcula contra
--site-header-height.
Los tres marcados en azul — 10, 13 y 15 — no están en la escala (2/4/6/8/12/16/20). No es descuido: son los tres componentes que Attio afinó ópticamente uno por uno. El botón (10px, entre lg y xl), la píldora (13px) y la fila de la app (15px, que arrastra la escala del producto real). Todo lo demás cae limpiamente en la escala.
lg es 992px, no el 1024 por defecto de Tailwind. Attio lo
bajó 32px. Es el punto exacto donde una tabla de CRM con la barra lateral desplegada deja de
caber, y ese punto no lo decide el framework: lo decide el producto.
shadow-3 → shadow-4 en hover.#202124), con borde #505967. Siempre.@property si quieres el hover del primario. Sin eso, el botón parpadea en lugar de encenderse.rgba(0,0,0,.05)), para que se oscurezcan con lo que hay debajo.backdrop-filter durante el scroll. Es caro y a 60fps se nota.rgba(28,40,64,.18)). Sobre un canvas frío, un halo negro se ve sucio.#266DF0 es para links, focus ring y accent-stroke. Un CTA azul mata la identidad.transform en un control. Attio no escala ni desplaza ningún botón ni ninguna tarjeta. La retroalimentación es cromática y de elevación.0 4px 12px rgba(0,0,0,.1) no es de este sistema: se ve como una mancha.#71717A desafina contra esta rampa al instante.accent-foreground (#6F7988) por debajo de 16px. Está en el filo de AA (4.6:1).transition: all .2s ease deshace la mitad del carácter del sistema.1.08px de la fila de producto a 1px. Rompe la correspondencia de escala con la app real.#A4ADBA para texto pequeño. 2.4:1 — es un caption, no un párrafo./* Los dos stops del CTA, registrados como COLOR.
Sin esto el degradado no se puede animar: una custom property
sin @property es una CADENA DE TEXTO y el navegador la cambia de golpe. */
@property --cta-from { syntax: "<color>"; inherits: false; initial-value: #202124; }
@property --cta-to { syntax: "<color>"; inherits: false; initial-value: #202124; }
:root {
/* --- rampa de negros: once pasos, TODOS con azul dentro --- */
--color-black-0: #000000; /* sólo el banner superior */
--color-black-50: #101010; /* el canvas del tema oscuro */
--color-black-100: #1C1D1F; /* LA TINTA */
--color-black-200: #202124; /* EL CTA */
--color-black-300: #232529; /* secondary-foreground */
--color-black-400: #2E3238; /* el label del outline y del ghost */
--color-black-500: #383E47; /* el :active del CTA. Al 10%, el borde de la baraja */
--color-black-600: #505967; /* EL BORDE DEL CTA. Y el foco que se enciende */
--color-black-700: #6F7988; /* accent-foreground. Y el outline en hover */
--color-black-800: #8F99A8;
--color-black-900: #A4ADBA; /* caption. 2.4:1 — nunca texto pequeño */
/* --- rampa de blancos --- */
--color-white-100: #FFFFFF; /* EL CANVAS. Blanco puro, no un gris roto */
--color-white-200: #FAFAFB; /* secciones que alternan */
--color-white-300: #F3F4F6; /* el ghost en hover. El label del CTA */
--color-white-400: #EDEFF3; /* surface. Y el borde de la píldora */
--color-white-500: #E4E7EC; /* el divisor */
--color-white-600: #DEE2E7;
--color-white-700: #D3D8DF; /* el borde del input */
--color-white-800: #CAD0D9; /* EL BORDE DEL OUTLINE */
--color-white-900: #B5BDC9; /* el CTA deshabilitado */
/* --- rampa de azules: el ÚNICO color. No pinta ni un botón --- */
--color-blue-100: #E8F0FF;
--color-blue-200: #C8DCFF;
--color-blue-300: #94B9FF;
--color-blue-400: #709FF5; /* accent-stroke: el único rol que NO cambia con el tema */
--color-blue-450: #538BF3; /* el CTA del tema .blue */
--color-blue-500: #266DF0; /* EL AZUL: links, focus ring, fila seleccionada */
--color-blue-600: #245BC2;
--color-blue-800: #1A2233;
/* --- estado --- */
--color-green-500: #0FC27B; --color-green-600: #0DB472;
--color-red-500: #FF5B59; --color-red-600: #F65351;
--color-yellow-500:#F5B900; --color-yellow-600:#DBA601;
/* --- LA ESCALERA. cada peldaño DOBLA el anterior y suma 1% --- */
--shadow-attio-layer-1: 0px 1px 3px 0px rgba(0,0,0,0.01);
--shadow-attio-layer-2: 0px 2px 4px -1px rgba(0,0,0,0.02);
--shadow-attio-layer-3: 0px 4px 8px -2px rgba(0,0,0,0.03);
--shadow-attio-layer-4: 0px 8px 16px -4px rgba(0,0,0,0.04);
--shadow-attio-layer-5: 0px 16px 32px -8px rgba(0,0,0,0.05);
--shadow-attio-layer-6: 0px 32px 64px -16px rgba(0,0,0,0.06);
--shadow-attio-layer-7: 0px 64px 128px -32px rgba(0,0,0,0.07);
/* la única sombra fuera de la escalera: el halo AZULADO de la fila de producto */
--shadow-attio-product-e1: 0 1px 3px rgba(0,0,0,0.04), 0 0 2px rgba(28,40,64,0.18);
/* --- radios. 10 (botón), 13 (píldora) y 15 (fila) NO están en la escala --- */
--radius-xs: 2px; --radius-sm: 4px; --radius-md: 6px; --radius-lg: 8px;
--radius-xl: 12px; --radius-2xl: 16px; --radius-3xl: 20px;
/* --- tipografía. OJO: el peso por defecto del CUERPO es 500 --- */
--font-inter: "inter"; /* -> Inter (Google, idéntica) */
--font-inter-display: "interDisplay"; /* -> Inter Tight (Google, sustituto) */
--font-tiempos-text: "tiemposText"; /* -> Source Serif 4 (Google, sustituto) */
--font-jetbrains-mono:"JetBrains Mono"; /* -> JetBrains Mono (Google, idéntica) */
--text-xs: 0.75rem; --text-xs--line-height: 1.125rem; --text-xs--font-weight: 500; --text-xs--letter-spacing: 0;
--text-sm: 0.875rem; --text-sm--line-height: 1.25rem; --text-sm--font-weight: 500; --text-sm--letter-spacing: -.005em;
--text-base: 1rem; --text-base--line-height:1.375rem; --text-base--font-weight: 500;--text-base--letter-spacing: -.01em;
--text-lg: 1.125rem; --text-lg--line-height: 1.5rem; --text-lg--font-weight: 500; --text-lg--letter-spacing: -.01em;
--text-xl: 1.25rem; --text-xl--line-height: 1.625rem; --text-xl--font-weight: 500; --text-xl--letter-spacing: -.01em;
--text-2xl: 1.5rem; --text-2xl--line-height: 1.875rem; --text-2xl--font-weight: 500; --text-2xl--letter-spacing: -.01em;
--text-heading-xs: 1.75rem; --text-heading-xs--line-height: 2.125rem; --text-heading-xs--font-weight: 600; --text-heading-xs--letter-spacing: -.01em;
--text-heading-sm: 2rem; --text-heading-sm--line-height: 2.25rem; --text-heading-sm--font-weight: 600; --text-heading-sm--letter-spacing: -.01em;
--text-heading-md: 2.5rem; --text-heading-md--line-height: 2.75rem; --text-heading-md--font-weight: 600; --text-heading-md--letter-spacing: -.01em;
--text-heading-lg: 3.5rem; --text-heading-lg--line-height: 3.75rem; --text-heading-lg--font-weight: 600; --text-heading-lg--letter-spacing: -.015em;
--text-heading-xl: 4rem; --text-heading-xl--line-height: 4rem; --text-heading-xl--font-weight: 600; --text-heading-xl--letter-spacing: -.02em;
/* --- curvas. la de por defecto arranca de golpe y frena larguísimo --- */
--ease-in-out: cubic-bezier(.2, 0, 0, 1); /* LA de por defecto */
--ease-out: cubic-bezier(0, 0, 0, 1);
--ease-in: cubic-bezier(.3, 0, 1, 1);
--ease-in-out-cubic: cubic-bezier(.65, 0, .35, 1); /* acordeones, 300ms */
--ease-out-cubic: cubic-bezier(.33, 1, .68, 1); /* fade de navegación, 100ms */
--ease-in-out-quad: cubic-bezier(.45, .05, .55, .95); /* diálogos, 200ms */
--ease-in-out-expo: cubic-bezier(1, 0, 0, 1);
--ease-reveal: cubic-bezier(0, 0, .58, 1);
/* --- espaciado. base 4px. Y el breakpoint lg es 992, no 1024 --- */
--spacing: 0.25rem;
--breakpoint-lg: 992px;
--site-header-nav-height: 68px;
--site-header-banner-visible-height: 48px;
}
/* --- capa semántica: los componentes no conocen colores, conocen ROLES --- */
:root {
--internal-color-primary-background: var(--color-white-100);
--internal-color-secondary-background: var(--color-white-200);
--internal-color-surface-subtle: var(--color-white-300);
--internal-color-surface: var(--color-white-400);
--internal-color-muted-background: var(--color-white-400);
--internal-color-muted-strong-background: var(--color-white-700);
--internal-color-weak-stroke: var(--color-white-400);
--internal-color-subtle-stroke: var(--color-white-500);
--internal-color-default-stroke: var(--color-white-700);
--internal-color-strong-stroke: var(--color-white-800);
--internal-color-accent-stroke: var(--color-blue-400);
--internal-color-primary-foreground: var(--color-black-100);
--internal-color-secondary-foreground: var(--color-black-300);
--internal-color-tertiary-foreground: var(--color-black-600);
--internal-color-accent-foreground: var(--color-black-700);
--internal-color-caption-foreground: var(--color-black-900);
--internal-color-disabled-foreground: var(--color-white-700);
--internal-color-link-foreground: var(--color-blue-500);
--internal-color-link-strong-foreground:var(--color-blue-600);
--internal-color-focus-ring: #266DF04D; /* blue-500 al 30% */
}
.dark {
--internal-color-primary-background: var(--color-black-50);
--internal-color-secondary-background: var(--color-black-100);
--internal-color-surface-subtle: var(--color-black-300);
--internal-color-surface: var(--color-black-400);
--internal-color-weak-stroke: var(--color-black-300);
--internal-color-subtle-stroke: var(--color-black-400);
--internal-color-default-stroke: var(--color-black-700);
--internal-color-strong-stroke: var(--color-black-800);
--internal-color-accent-stroke: var(--color-blue-400); /* NO cambia */
--internal-color-primary-foreground: var(--color-white-100);
--internal-color-secondary-foreground: var(--color-white-400);
--internal-color-tertiary-foreground: var(--color-white-900);
--internal-color-accent-foreground: var(--color-black-800);
--internal-color-caption-foreground: var(--color-black-600);
--internal-color-link-foreground: var(--color-blue-400);
--internal-color-link-strong-foreground:var(--color-blue-300);
--internal-color-focus-ring: #709FF599; /* blue-400 al 60% */
}
/* Attio usa Tailwind v4 con @theme. Los tokens viven en :root, .dark y .blue —
TRES ámbitos de tema, no dos. */
@import "tailwindcss";
@theme {
--color-black-0: #000000;
--color-black-50: #101010;
--color-black-100: #1C1D1F;
--color-black-200: #202124;
--color-black-300: #232529;
--color-black-400: #2E3238;
--color-black-500: #383E47;
--color-black-600: #505967;
--color-black-700: #6F7988;
--color-black-800: #8F99A8;
--color-black-900: #A4ADBA;
--color-white-100: #FFFFFF;
--color-white-200: #FAFAFB;
--color-white-300: #F3F4F6;
--color-white-400: #EDEFF3;
--color-white-500: #E4E7EC;
--color-white-600: #DEE2E7;
--color-white-700: #D3D8DF;
--color-white-800: #CAD0D9;
--color-white-900: #B5BDC9;
--color-blue-400: #709FF5;
--color-blue-450: #538BF3;
--color-blue-500: #266DF0;
--color-blue-600: #245BC2;
--shadow-attio-layer-1: 0px 1px 3px 0px --alpha(var(--color-black-0) / 1%);
--shadow-attio-layer-2: 0px 2px 4px -1px --alpha(var(--color-black-0) / 2%);
--shadow-attio-layer-3: 0px 4px 8px -2px --alpha(var(--color-black-0) / 3%);
--shadow-attio-layer-4: 0px 8px 16px -4px --alpha(var(--color-black-0) / 4%);
--shadow-attio-layer-5: 0px 16px 32px -8px --alpha(var(--color-black-0) / 5%);
--shadow-attio-layer-6: 0px 32px 64px -16px --alpha(var(--color-black-0) / 6%);
--shadow-attio-layer-7: 0px 64px 128px -32px --alpha(var(--color-black-0) / 7%);
--breakpoint-lg: 992px; /* NO 1024 */
--ease-in-out: cubic-bezier(.2, 0, 0, 1);
}
/* Las utilidades de sombra son ACUMULATIVAS: shadow-attio-N = capas 1..N */
@utility shadow-attio-2 { box-shadow: var(--shadow-attio-layer-1), var(--shadow-attio-layer-2); }
@utility shadow-attio-3 { box-shadow: var(--shadow-attio-layer-1), var(--shadow-attio-layer-2),
var(--shadow-attio-layer-3); }
@utility shadow-attio-4 { box-shadow: var(--shadow-attio-layer-1), var(--shadow-attio-layer-2),
var(--shadow-attio-layer-3), var(--shadow-attio-layer-4); }
@utility shadow-attio-5 { box-shadow: var(--shadow-attio-layer-1), var(--shadow-attio-layer-2),
var(--shadow-attio-layer-3), var(--shadow-attio-layer-4),
var(--shadow-attio-layer-5); }
@utility shadow-attio-6 { box-shadow: var(--shadow-attio-layer-1), var(--shadow-attio-layer-2),
var(--shadow-attio-layer-3), var(--shadow-attio-layer-4),
var(--shadow-attio-layer-5), var(--shadow-attio-layer-6); }
/* --- LAS CLASES REALES DEL BOTÓN, tal cual salen del HTML servido --- */
/* base (desktop) */
class="relative inline-flex cursor-pointer items-center justify-center text-nowrap
border transition-colors duration-300 ease-in-out
hover:duration-50 active:duration-50
disabled:pointer-events-none disabled:cursor-default
h-9 gap-x-1.5 rounded-[10px] px-3 text-sm
has-[>svg:last-child,>img:last-child]:pr-2
has-[>svg:first-child,>img:first-child]:pl-2
max-lg:h-11.5 max-lg:gap-x-2 max-lg:rounded-xl max-lg:px-3.5 max-lg:text-base
button-primary"
/* la tarjeta */
class="overflow-hidden rounded-md bg-primary-background
lg:rounded-xl lg:border lg:border-black-0/5"
/* la fila de producto */
class="relative z-10 flex h-full w-full flex-col gap-px
rounded-[7px] border-[0.54px] bg-white-100 p-[3px]
lg:rounded-[15px] lg:border-[1.08px] lg:p-[6.5px]
border-[#266df0] shadow-attio-product-e1"
/* el panel de cristal, y el blur que se APAGA al hacer scroll */
class="absolute touch-none select-none overflow-hidden
rounded-lg p-[3px] pt-0 backdrop-blur-md
lg:rounded-2xl lg:p-1.5 lg:pt-0
group-data-hero-scrolling/hero:backdrop-blur-none
bg-white-300/80"
/* ===========================================================================
EL CTA PRIMARIO — el foco que amanece
=========================================================================== */
@property --button-primary-bg-from { syntax: "<color>"; inherits: false; initial-value: #000; }
@property --button-primary-bg-to { syntax: "<color>"; inherits: false; initial-value: #000; }
.button-primary {
/* el foco del radial está en `at center -10%`:
FUERA del botón, un 10% por encima de su borde superior */
background-image: radial-gradient(at center -10%,
var(--button-primary-bg-from) 0%,
var(--button-primary-bg-to) 100%);
background-color: var(--button-primary-bg-fallback);
border-color: var(--button-primary-border);
color: var(--button-primary-text);
/* interpola las VARIABLES, no una imagen */
transition-property: color, border-color,
--button-primary-bg-from,
--button-primary-bg-to;
}
:root .button-primary {
/* reposo: los dos stops valen LO MISMO -> el degradado está apagado */
--button-primary-bg-from: #202124;
--button-primary-bg-to: #202124;
--button-primary-bg-fallback: #202124;
--button-primary-border: #505967;
--button-primary-text: #F3F4F6;
/* hover: sólo cambia el stop DE ARRIBA -> el foco se enciende */
--button-primary-bg-hover-from: #505967;
--button-primary-bg-hover-to: #202124;
--button-primary-bg-hover-fallback: #383E47;
--button-primary-border-hover: #505967;
--button-primary-text-hover: #FFFFFF;
/* focus-visible: EXACTAMENTE lo mismo que el hover. El teclado ve lo que ve el ratón */
--button-primary-bg-focus-from: #505967;
--button-primary-bg-focus-to: #202124;
--button-primary-bg-focus-fallback: #383E47;
/* active: plano otra vez, un peldaño más claro */
--button-primary-bg-active-from: #383E47;
--button-primary-bg-active-to: #383E47;
--button-primary-bg-active-fallback: #505967;
--button-primary-border-active: #505967;
--button-primary-text-active: #FFFFFF;
--button-primary-bg-disabled-fallback: #B5BDC9;
}
/* ===========================================================================
OUTLINE — en hover SÓLO se mueve el borde
=========================================================================== */
:root {
--button-outline-bg: #FFFFFF;
--button-outline-border: #CAD0D9; /* white-800 */
--button-outline-text: #2E3238;
--button-outline-bg-hover: #FFFFFF; /* el fondo NO cambia */
--button-outline-border-hover: #6F7988; /* black-700: SALTA de rampa */
--button-outline-text-hover: #2E3238; /* el texto NO cambia */
--button-outline-bg-active: #F3F4F6; /* el relleno se reserva para CONFIRMAR */
--button-outline-border-active: #383E47;
--button-outline-text-active: #232529;
--button-outline-border-disabled: #E4E7EC;
--button-outline-text-disabled-foreground: #CAD0D9;
}
/* ===========================================================================
GHOST — el espejo: SÓLO se mueve el fondo
=========================================================================== */
:root {
--button-ghost-bg: transparent;
--button-ghost-text: #2E3238;
--button-ghost-bg-hover: #F3F4F6;
--button-ghost-text-hover: #232529;
--button-ghost-bg-active: #EDEFF3;
--button-ghost-text-active:#202124;
--button-ghost-text-disabled-foreground: #B5BDC9;
}
/* ===========================================================================
GEOMETRÍA COMPARTIDA — y la asimetría del tiempo
=========================================================================== */
.button {
display: inline-flex; align-items: center; justify-content: center;
height: 36px; /* h-9 */
border-radius: 10px; /* ARBITRARIO: entre lg (8) y xl (12) */
padding: 0 12px;
gap: 6px;
border: 1px solid; /* SIEMPRE, aunque sea transparente */
font-size: 14px; line-height: 20px; font-weight: 500; letter-spacing: -0.005em;
transition-duration: 300ms; /* SALIR: 300ms */
transition-timing-function: cubic-bezier(.2, 0, 0, 1);
}
.button:hover, .button:active { transition-duration: 50ms; } /* ENTRAR: 50ms */
/* el icono pesa menos que una letra: menos aire de ese lado */
.button:has(> svg:last-child, > img:last-child) { padding-right: 8px; }
.button:has(> svg:first-child, > img:first-child) { padding-left: 8px; }
/* variante grande (el CTA del hero, y TODO el móvil) */
.button-lg { height: 46px; border-radius: 12px; padding: 0 14px; gap: 8px; font-size: 16px; }
/* ===========================================================================
LA TARJETA — y el hover, que es SUBIR UN PELDAÑO
=========================================================================== */
.card {
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.05); /* alfa: se oscurece con el fondo */
border-radius: 12px; /* 6px en móvil, y SIN borde */
padding: 24px;
gap: 16px;
box-shadow: var(--shadow-attio-layer-1),
var(--shadow-attio-layer-2),
var(--shadow-attio-layer-3); /* 3 capas */
transition: box-shadow 300ms cubic-bezier(.2, 0, 0, 1);
}
.card:hover {
box-shadow: var(--shadow-attio-layer-1),
var(--shadow-attio-layer-2),
var(--shadow-attio-layer-3),
var(--shadow-attio-layer-4); /* 4: las tres de antes + UNA */
transition-duration: 50ms;
}
/* ===========================================================================
LA FILA DE PRODUCTO — la única sombra fuera de la escalera
=========================================================================== */
.card-product {
background: #FFFFFF;
border-radius: 15px; /* arbitrario: la escala de la app real */
border: 1.08px solid transparent; /* !! 1.08px. En móvil: 0.54px, la mitad */
padding: 6.5px; /* en desktop: 13px. Escala 2x */
gap: 1px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04),
0 0 2px rgba(28, 40, 64, 0.18); /* halo AZULADO, no negro */
}
.card-product[data-selected] { border-color: #266DF0; }
/* ===========================================================================
LA PÍLDORA — dos elementos, dos responsabilidades
=========================================================================== */
.pill { /* el <a>: SÓLO el borde */
border: 1px solid #EDEFF3;
border-radius: 13px; /* arbitrario */
transition: border-color 300ms cubic-bezier(.2, 0, 0, 1);
}
.pill:active { border-color: #E4E7EC; }
.pill > span { /* el <div>: SÓLO el fondo */
display: flex; align-items: center; gap: 4px;
padding: 5px 7px 5px 11px; /* asimétrico: 11 al texto, 7 al chevron */
background: #FFFFFF;
border-radius: 13px;
font-size: 13px; line-height: 18px; font-weight: 500;
color: #232529;
transition: background-color 300ms cubic-bezier(.2, 0, 0, 1);
}
.pill:hover > span { background: #FBFBFC; } /* ENTRE white-100 y white-200 */
/* ===========================================================================
EL INPUT — el único sitio donde el azul toca un control
=========================================================================== */
.input {
height: 36px; border-radius: 10px; padding: 0 12px;
background: #FFFFFF;
border: 1px solid #D3D8DF;
font-size: 14px; font-weight: 500; letter-spacing: -0.005em;
transition: border-color 300ms cubic-bezier(.2, 0, 0, 1),
box-shadow 300ms cubic-bezier(.2, 0, 0, 1);
}
.input:hover { border-color: #6F7988; transition-duration: 50ms; }
.input:focus-visible{ border-color: #266DF0; outline: none;
box-shadow: 0 0 0 3px #266DF04D; } /* blue-500 al 30% */
# DESIGN.md - Attio
Sistema de diseño extraído de https://attio.com (home, julio 2026).
## NOTA DE MÉTODO
Attio DETECTA el Chrome DevTools Protocol: cuando Puppeteer controla la pestaña, el
runtime de la página se cuelga y Runtime.callFunctionOn expira. No hay estilos
computados que leer.
Pero el sitio es Next.js SERVER-RENDERED: el HTML llega completo (1.09 MB) y las ocho
hojas de estilo (462 KB) se sirven sin ninguna protección. Todos los valores de este
documento están leídos DIRECTAMENTE del CSS de producción y del HTML servido. Es una
fuente más fiable, no menos: son los tokens tal cual los escribió Attio, antes de que
el navegador los resuelva.
La captura del hero se tomó con JavaScript deshabilitado, neutralizando los 44 nodos
que llegan del servidor con `style="filter:blur(1.5px);opacity:0"` a la espera de la
animación de entrada.
## 1. TEMA VISUAL
Attio vende un CRM y su home ENSEÑA LA APLICACIÓN. No hay ilustración, no hay 3D, no hay
blob de degradado. Hay ventanas de producto flotando sobre un canvas blanco.
Eso obliga a que el marketing y el producto compartan la misma gramática. Y ahí está la
decisión que define el sistema: en lugar de una paleta de marca, Attio construyó una
ESCALERA DE ELEVACIÓN DE SIETE PELDAÑOS donde cada peldaño es una función exacta del
anterior.
desplazamiento: 1, 2, 4, 8, 16, 32, 64 (x2)
blur: 3, 4, 8, 16, 32, 64, 128 (x2)
spread: 0, -1, -2, -4, -8,-16, -32 (x2, negativo)
opacidad: 1%, 2%, 3%, 4%, 5%, 6%, 7% (+1)
Cuatro progresiones geométricas en paralelo, y una aritmética. Y se usan ACUMULADAS:
shadow-attio-6 son SEIS sombras a la vez, y ninguna pasa del 6%.
Elevar = AÑADIR LA SIGUIENTE CAPA, no cambiar la sombra.
La segunda firma es el CTA. No es azul: es #202124, un negro con azul dentro, con un
radial-gradient cuyo foco está en `at center -10%` — fuera del botón, por encima. En
reposo los dos stops valen lo mismo (plano). En hover el de arriba salta a #505967 y el
foco SE ENCIENDE. Y se anima de verdad porque los stops están declarados con
@property { syntax: "" }: el navegador los interpola.
Personalidad: exacto, geométrico, de producto, frío, sobrio, denso
Densidad: densa en el control (botones de 36px), amplia en la página
Modo: claro. Existe .dark completo y un tercer tema .blue
Ornamentación: casi nula en color; TOTAL en elevación
## 2. COLOR
Once negros (TODOS con azul dentro: el canal B siempre supera al R), nueve blancos y un
único azul.
negros: #000000 #101010 #1C1D1F #202124 #232529 #2E3238 #383E47 #505967 #6F7988
#8F99A8 #A4ADBA
blancos: #FFFFFF #FAFAFB #F3F4F6 #EDEFF3 #E4E7EC #DEE2E7 #D3D8DF #CAD0D9 #B5BDC9
azules: #E8F0FF #C8DCFF #94B9FF #709FF5 #538BF3 #266DF0 #245BC2 #1A2233
estado: #0FC27B (success) #FF5B59 (error) #F5B900 (warning)
LA REGLA DEL AZUL: #266DF0 no pinta NI UN SOLO BOTÓN. Su trabajo entero es:
link-foreground, focus-ring al 30% (#266DF04D), accent-stroke y el borde de 1.08px de la
fila seleccionada. El botón de Attio es NEGRO.
Bordes con alfa, no con token:
tarjeta: rgba(0,0,0,0.05) -> se oscurece con lo que haya debajo
baraja: rgba(56,62,71,0.1) -> black-500 al 10%, para que se oscurezcan entre sí
CONTRASTE: #6F7988 (el párrafo del hero) da 4.6:1 — AA EN EL FILO. Attio lo compensa
poniéndolo a 18px/500. NUNCA bajarlo de 16px.
## 3. TIPOGRAFÍA
Reales: interDisplay / inter / tiemposText / JetBrains Mono
Sustitutas: Inter Tight / Inter / Source Serif 4 / JetBrains Mono
DOS DE LAS CUATRO SON IDÉNTICAS: Inter y JetBrains Mono son libres y están en Google
Fonts. Sólo se sustituyen el corte display (interDisplay -> Inter Tight, el corte público
más cercano) y el serif (tiemposText, de Klim Type, comercial -> Source Serif 4).
EL CUERPO PESA 500, NO 400. Cada token trae --text-*--font-weight: 500 de fábrica.
Titulares: 600. Techo: 700. NO hay 800 ni 900.
Tracking: siempre negativo, y MÁS negativo cuanto más grande (-0.005em a 14px,
-0.02em a 64px). Salvo a 12px, donde vuelve a 0: apretar texto diminuto lo hace ilegible.
El titular del hero NO usa la escala. Es fluido respecto a la ALTURA del viewport:
font-size: clamp(64px, calc(16px + 5.333svh), 80px)
letter-spacing: clamp(-2.4px, calc(2.08px - 0.3733svh), -1.28px)
line-height: 0.95
Cuando svh sube, el tamaño sube y el tracking BAJA. La regla óptica clásica, escrita como
una interpolación lineal en CSS. Y usa svh, no vw: el hero encoge en pantallas bajas para
que los mocks de producto sigan cabiendo debajo.
## 4. CTAs
Geometría compartida: 36px de alto, radio 10px, padding 0 12px, gap 6px, borde de 1px
SIEMPRE presente, label de 14px/500/lh 20px/tracking -0.005em.
Variante grande (hero y móvil): 46px, radio 12px, padding 0 14px, label de 16px.
RADIO 10px NO ES UN TOKEN. La escala es 2/4/6/8/12/16/20. El botón usa un arbitrario que
cae entre lg (8) y xl (12).
LA REGLA DE LOS TRES — una propiedad por variante, y ninguna se repite:
PRIMARIO: se mueve el DEGRADADO (#202124 plano -> foco #505967 encendido)
OUTLINE: se mueve el BORDE (#CAD0D9 -> #6F7988; salta de la rampa de blancos
a la de negros. El fondo NO cambia)
GHOST: se mueve el FONDO (transparent -> #F3F4F6 -> #EDEFF3)
NINGÚN botón anima transform. No escalan, no se desplazan, no ganan sombra.
LA ASIMETRÍA DEL TIEMPO: entrar 50ms, salir 300ms. Curva cubic-bezier(.2, 0, 0, 1).
El icono cambia el padding, no el layout:
.button:has(> svg:last-child) { padding-right: 8px; } /* de 12 a 8 */
focus-visible del primario = su estado de hover + el ring. El teclado ve lo mismo que el
ratón.
## 5. TARJETAS
Una tarjeta es: fondo blanco + borde rgba(0,0,0,.05) + radio 12px + N capas. La variación
entre tarjetas ES LA N.
shadow-attio-3 -> tarjeta en reposo
shadow-attio-4 -> tarjeta en hover (las 3 de antes + UNA)
shadow-attio-5 -> popover, dropdown
shadow-attio-6 -> modal, panel flotante
shadow-attio-7 -> reservada
El radio Y el borde cambian con el breakpoint: en móvil es rounded-md (6px) y SIN borde;
a partir de lg (992px) sube a rounded-xl (12px) y aparece el lg:border.
FILA DE PRODUCTO (la única sombra fuera de la escalera):
radio 15px, borde 1.08px (en móvil 0.54px, la mitad exacta), padding 6.5px/13px
box-shadow: 0 1px 3px rgba(0,0,0,.04), 0 0 2px rgba(28,40,64,.18) <- halo AZULADO
seleccionada: border-color #266DF0
El 1.08px no es un error: Attio escaló la app real y ARRASTRÓ EL BORDE con ella en vez de
redondearlo. NO lo redondees a 1px.
BARAJA: dos rectángulos a 3° y 6°, borde rgba(56,62,71,.1).
PANEL DE CRISTAL: radio 16px, padding 6px con padding-top 0, bg rgba(243,244,246,.8),
backdrop-filter blur(12px). Y EL BLUR SE APAGA DURANTE EL SCROLL
(group-data-hero-scrolling:backdrop-blur-none) porque a 60fps se nota.
## 6. PÍLDORA
DOS elementos, DOS responsabilidades:
el -> SÓLO el borde (1px #EDEFF3, radio 13px, :active -> #E4E7EC)
el -> SÓLO el fondo (#FFFFFF, hover -> #FBFBFC)
#FBFBFC está ENTRE white-100 y white-200: más sutil que el paso más pequeño de la rampa.
Padding asimétrico 5px 7px 5px 11px: 11 al texto, 7 al chevron de 12x12.
## 7. ESPACIADO
Base 4px (Tailwind v4). Radios: 2/4/6/8/12/16/20, MÁS tres arbitrarios que se salen a
propósito: 10 (botón), 13 (píldora), 15 (fila de la app).
BREAKPOINT lg = 992px, NO 1024. Attio lo bajó 32px: es donde una tabla de CRM con la
barra lateral deja de caber. Ese punto no lo decide el framework.
El header es una SUMA de cuatro piezas que pueden valer 0 (toolbar 36 + nav 68 +
subheader 63 + banner 48). Mostrar u ocultar una franja es reasignar una variable.
## 8. NO HACER
- No pintes un botón de azul. El botón es NEGRO.
- No animes transform en un control.
- No uses una sombra de UNA capa. La profundidad se APILA.
- No subas la opacidad de una capa. Ninguna pasa del 7%. Añade otra capa.
- No uses negro puro (salvo el banner) ni grises neutros: todo lleva azul dentro.
- No pongas #6F7988 por debajo de 16px. Está en el filo de AA.
- No uses transiciones simétricas. 50ms entrando, 300ms saliendo.
- No redondees el 1.08px a 1px.
## ANEXO: STACK
Next.js (App Router) en Vercel. Tailwind v4 con @theme, 8 chunks, 462 KB.
HTML server-rendered de 1.09 MB. next/font local (woff2, font-display: swap, con
familias Fallback y size-adjust). Tres ámbitos de tema: :root, .dark, .blue.
Los tokens se compilan a lab() con fallback hex; los hex de este documento son los
fallbacks literales del CSS servido.
ANTI-BOT: detecta CDP. Con Puppeteer al mando el runtime se cuelga. El CSS y el HTML se
sirven sin restricción a curl.