--- version: "1.0" name: "Attio" description: "Sistema visual de Attio: un CRM que se dibuja a sí mismo como software, no como marketing. Su paleta real es casi monocroma — once negros azulados, nueve blancos y un único azul #266DF0 que sólo aparece en links, focus rings y la fila seleccionada. El CTA primario no es azul: es casi negro (#202124) con un borde gris #505967 y, sobre él, un radial-gradient cuyos dos stops están registrados con @property para que el hover INTERPOLE el degradado en 50ms. Y la elevación es una escalera de siete capas donde cada capa dobla el desplazamiento, dobla el blur, dobla el spread negativo y suma exactamente un uno por ciento de opacidad. Tarjetas de radio 12px, botones de radio 10px, píldoras de radio 13px. Light mode, densidad de producto." colors: primary: "#266DF0" primary-soft: "#538BF3" primary-light: "#709FF5" primary-lighter: "#94B9FF" primary-tint: "#C8DCFF" primary-wash: "#E8F0FF" primary-deep: "#245BC2" primary-ink: "#1A2233" cta: "#202124" cta-hover-bloom: "#505967" cta-active: "#383E47" cta-border: "#505967" background: "#FFFFFF" background-secondary: "#FAFAFB" surface-subtle: "#F3F4F6" surface: "#EDEFF3" muted-background: "#EDEFF3" muted-strong-background: "#D3D8DF" ink: "#1C1D1F" text-primary: "#1C1D1F" text-secondary: "#232529" text-tertiary: "#505967" text-accent: "#6F7988" text-caption: "#A4ADBA" text-disabled: "#D3D8DF" text-on-cta: "#F3F4F6" text-on-cta-hover: "#FFFFFF" link: "#266DF0" link-strong: "#245BC2" stroke-weak: "#EDEFF3" stroke-subtle: "#E4E7EC" stroke-default: "#D3D8DF" stroke-strong: "#CAD0D9" stroke-accent: "#709FF5" stroke-card: "#0000000D" stroke-stack: "#383E471A" focus-ring: "#266DF04D" success: "#0FC27B" success-deep: "#0DB472" error: "#FF5B59" error-deep: "#F65351" warning: "#F5B900" warning-deep: "#DBA601" tag-design: "#00B5E6" tag-enhancement: "#AB92F1" tag-feature: "#72A4FB" black-0: "#000000" black-50: "#101010" black-100: "#1C1D1F" black-200: "#202124" black-300: "#232529" black-400: "#2E3238" black-500: "#383E47" black-600: "#505967" black-700: "#6F7988" black-800: "#8F99A8" black-900: "#A4ADBA" white-100: "#FFFFFF" white-200: "#FAFAFB" white-300: "#F3F4F6" white-400: "#EDEFF3" white-500: "#E4E7EC" white-600: "#DEE2E7" white-700: "#D3D8DF" white-800: "#CAD0D9" white-900: "#B5BDC9" typography: hero: fontFamily: "interDisplay" substitute: "Inter Tight" fontSize: "clamp(64px, calc(16px + 5.333svh), 80px)" fontWeight: 600 lineHeight: "0.95" letterSpacing: "clamp(-2.4px, calc(2.08px - 0.3733svh), -1.28px)" heading-xl: fontFamily: "interDisplay" fontSize: "64px" fontWeight: 600 lineHeight: "64px" letterSpacing: "-0.02em" heading-lg: fontFamily: "interDisplay" fontSize: "56px" fontWeight: 600 lineHeight: "60px" letterSpacing: "-0.015em" heading-md: fontFamily: "interDisplay" fontSize: "40px" fontWeight: 600 lineHeight: "44px" letterSpacing: "-0.01em" heading-sm: fontFamily: "interDisplay" fontSize: "32px" fontWeight: 600 lineHeight: "36px" letterSpacing: "-0.01em" heading-xs: fontFamily: "interDisplay" fontSize: "28px" fontWeight: 600 lineHeight: "34px" letterSpacing: "-0.01em" text-2xl: fontFamily: "inter" fontSize: "24px" fontWeight: 500 lineHeight: "30px" letterSpacing: "-0.01em" text-xl: fontFamily: "inter" fontSize: "20px" fontWeight: 500 lineHeight: "26px" letterSpacing: "-0.01em" text-lg: fontFamily: "inter" fontSize: "18px" fontWeight: 500 lineHeight: "24px" letterSpacing: "-0.01em" text-base: fontFamily: "inter" fontSize: "16px" fontWeight: 500 lineHeight: "22px" letterSpacing: "-0.01em" text-sm: fontFamily: "inter" fontSize: "14px" fontWeight: 500 lineHeight: "20px" letterSpacing: "-0.005em" text-xs: fontFamily: "inter" fontSize: "12px" fontWeight: 500 lineHeight: "18px" letterSpacing: "0" pill: fontFamily: "inter" fontSize: "13px" fontWeight: 500 lineHeight: "18px" serif: fontFamily: "tiemposText" substitute: "Source Serif 4" fontWeight: 400 mono: fontFamily: "JetBrains Mono" substitute: "JetBrains Mono" fontWeight: 400 rounded: xs: "2px" sm: "4px" md: "6px" lg: "8px" button: "10px" xl: "12px" pill: "13px" product: "15px" 2xl: "16px" 3xl: "20px" spacing: base: "4px" 1: "4px" 1.5: "6px" 2: "8px" 3: "12px" 3.5: "14px" 4: "16px" 6: "24px" 9: "36px" 12: "48px" 20: "80px" nav-height: "68px" banner-height: "48px" elevation: attio-layer-1: "0px 1px 3px 0px rgba(0,0,0,0.01)" attio-layer-2: "0px 2px 4px -1px rgba(0,0,0,0.02)" attio-layer-3: "0px 4px 8px -2px rgba(0,0,0,0.03)" attio-layer-4: "0px 8px 16px -4px rgba(0,0,0,0.04)" attio-layer-5: "0px 16px 32px -8px rgba(0,0,0,0.05)" attio-layer-6: "0px 32px 64px -16px rgba(0,0,0,0.06)" attio-layer-7: "0px 64px 128px -32px rgba(0,0,0,0.07)" attio-product-e1: "0 1px 3px rgba(0,0,0,0.04), 0 0 2px rgba(28,40,64,0.18)" components: button-primary: height: "36px" rounded: "10px" padding: "0 12px" gap: "6px" border: "1px solid #505967" backgroundImage: "radial-gradient(at center -10%, var(--button-primary-bg-from) 0%, var(--button-primary-bg-to) 100%)" backgroundColor: "#202124" textColor: "#F3F4F6" typography: "{typography.text-sm}" transitionProperty: "color, border-color, --button-primary-bg-from, --button-primary-bg-to" transition: "300ms cubic-bezier(.2,0,0,1)" hoverDuration: "50ms" hover: bgFrom: "#505967" bgTo: "#202124" backgroundColor: "#383E47" borderColor: "#505967" textColor: "#FFFFFF" active: bgFrom: "#383E47" bgTo: "#383E47" backgroundColor: "#505967" textColor: "#FFFFFF" disabled: backgroundColor: "#B5BDC9" textColor: "#FFFFFF" button-primary-lg: height: "46px" rounded: "12px" padding: "0 14px" gap: "8px" typography: "{typography.text-base}" button-outline: height: "36px" rounded: "10px" padding: "0 12px" gap: "6px" backgroundColor: "#FFFFFF" border: "1px solid #CAD0D9" textColor: "#2E3238" hover: backgroundColor: "#FFFFFF" borderColor: "#6F7988" textColor: "#2E3238" active: backgroundColor: "#F3F4F6" borderColor: "#383E47" textColor: "#232529" disabled: borderColor: "#E4E7EC" textColor: "#CAD0D9" button-ghost: height: "36px" rounded: "10px" padding: "0 12px" backgroundColor: "transparent" borderColor: "transparent" textColor: "#2E3238" hover: backgroundColor: "#F3F4F6" textColor: "#232529" active: backgroundColor: "#EDEFF3" textColor: "#202124" pill: rounded: "13px" border: "1px solid #EDEFF3" backgroundColor: "#FFFFFF" padding: "5px 7px 5px 11px" gap: "4px" textColor: "#232529" typography: "{typography.pill}" hover: backgroundColor: "#FBFBFC" active: borderColor: "#E4E7EC" transition: "300ms cubic-bezier(.2,0,0,1)" card: backgroundColor: "#FFFFFF" rounded: "12px" border: "1px solid rgba(0,0,0,0.05)" boxShadow: "{elevation.attio-layer-1}, {elevation.attio-layer-2}, {elevation.attio-layer-3}" card-raised: backgroundColor: "#FFFFFF" rounded: "12px" border: "1px solid rgba(0,0,0,0.05)" boxShadow: "layers 1-6 cumulative" card-product: backgroundColor: "#FFFFFF" rounded: "15px" border: "1.08px solid transparent" borderSelected: "1.08px solid #266DF0" padding: "6.5px" gap: "1px" boxShadow: "{elevation.attio-product-e1}" card-stacked: rounded: "12px" border: "1px solid rgba(56,62,71,0.1)" rotate: "3deg / 6deg" panel-glass: rounded: "16px" padding: "6px" backgroundColor: "rgba(243,244,246,0.8)" backdropFilter: "blur(12px)" input: backgroundColor: "#FFFFFF" border: "1px solid #D3D8DF" rounded: "10px" height: "36px" padding: "0 12px" typography: "{typography.text-sm}" focusRing: "0 0 0 3px #266DF04D" easing: ease-in-out: "cubic-bezier(.2, 0, 0, 1)" 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)" ease-out-cubic: "cubic-bezier(.33, 1, .68, 1)" ease-in-cubic: "cubic-bezier(.32, 0, .67, 0)" ease-in-out-expo: "cubic-bezier(1, 0, 0, 1)" ease-in-out-quad: "cubic-bezier(.45, .05, .55, .95)" ease-reveal: "cubic-bezier(0, 0, .58, 1)" --- # 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. Así que todos los valores de este documento están leídos **directamente del CSS de producción y del HTML servido**, no de un DOM computado. 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 con `opacity:0` a la espera de la animación de entrada. > > Attio usa **Tailwind v4** con `@theme`. Los tokens viven en `:root`, `.dark` y `.blue` — tres ámbitos de tema, no dos. --- ## 1. Tema Visual y Atmósfera Attio vende un CRM y su home hace una cosa muy concreta: **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 — un panel de Slack, una vista de pipeline, una transcripción de llamada — cada una con su sombra correcta y su radio correcto. El titular dice *Welcome to agentic revenue.* en 80px, y debajo hay dos botones: uno con borde y uno casi negro. Eso obliga a un sistema donde **el marketing y el producto comparten la misma gramática**. Una tarjeta del home tiene que poder sentarse al lado de una fila real de la app sin que se note la costura. Y ahí está la decisión que mete a Attio en esta galería: 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. ```css /* la escalera. cada capa DOBLA el peldaño anterior y suma 1% de opacidad. */ --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); ``` Mira las columnas. Desplazamiento: 1, 2, 4, 8, 16, 32, 64. Blur: 3, 4, 8, 16, 32, 64, 128. Spread: 0, −1, −2, −4, −8, −16, −32. Opacidad: 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 hasta que quedó bien: es una **fórmula**. Y no se usan sueltas. Se usan **acumuladas**: ```css .shadow-attio-3 { box-shadow: var(--layer-1), var(--layer-2), var(--layer-3); } .shadow-attio-6 { box-shadow: var(--layer-1), ..., var(--layer-6); } ``` `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 es una caída que 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. La segunda firma es el **CTA primario**, y es más rara todavía. No es azul. Es `#202124`, un negro con azul dentro. Y lleva encima un `radial-gradient(at center -10%, ...)` — un foco colocado justo por **encima** del borde superior del botón. En reposo los dos stops del degradado valen lo mismo, así que el botón se ve plano. En hover, el stop de arriba salta a `#505967` y **el foco se enciende**. No es una imagen que cambia: los dos stops están declarados con `@property { syntax: "" }`, así que el navegador los **interpola**. El degradado literalmente amanece sobre el botón, en 50 milisegundos. El color, mientras tanto, es casi ascético. Once negros azulados, nueve blancos y **un solo azul**: `#266DF0`. Ese azul no pinta ni un botón. Pinta links, el focus ring, el borde de la fila seleccionada y poco más. **Palabras clave de personalidad**: exacto, geométrico, de producto, frío, sobrio, denso **Densidad del layout**: densa en el control (botones de 36px, texto de 14px), amplia en la página **Modo preferido**: claro. Existe `.dark` completo y un tercer tema `.blue` para secciones de acento **Nivel de ornamentación**: casi nulo en color; **total en elevación**. Toda la ornamentación vive en `box-shadow` **Audiencia visual objetivo**: equipos de revenue que van a vivir dentro de la app ocho horas al día --- ## 2. Paleta de Colores y Roles Semánticos Attio no tiene "colores de marca" en el sentido clásico. Tiene **tres rampas** (negros, blancos, azules) y una capa semántica encima que las mapea a roles. Las rampas son el hardware; los roles son el software. ### La rampa de negros (11 pasos) Ninguno es negro puro salvo `black-0`. Todos tienen azul dentro: fíjate cómo el canal B siempre supera al R. | Token | Hex | rgb | Rol real | |-------|-----|-----|----------| | `--color-black-0` | `#000000` | `0, 0, 0` | Sólo el fondo del banner superior | | `--color-black-50` | `#101010` | `16, 16, 16` | El canvas del tema oscuro | | `--color-black-100` | `#1C1D1F` | `28, 29, 31` | **La tinta.** `primary-foreground` en claro | | `--color-black-200` | `#202124` | `32, 33, 36` | **El CTA primario.** Y `secondary-background` en oscuro | | `--color-black-300` | `#232529` | `35, 37, 41` | `secondary-foreground`: el texto de la píldora, subtítulos | | `--color-black-400` | `#2E3238` | `46, 50, 56` | El label de los botones outline y ghost | | `--color-black-500` | `#383E47` | `56, 62, 71` | El estado `:active` del CTA. Y al **10%** es el borde de las tarjetas apiladas | | `--color-black-600` | `#505967` | `80, 89, 103` | **El borde del CTA** y el foco que se enciende en hover. `tertiary-foreground` | | `--color-black-700` | `#6F7988` | `111, 121, 136` | `accent-foreground`: el párrafo del hero. Y el borde del outline en hover | | `--color-black-800` | `#8F99A8` | `143, 153, 168` | `strong-stroke` en oscuro | | `--color-black-900` | `#A4ADBA` | `164, 173, 186` | `caption-foreground`: metadatos, pies | ### La rampa de blancos (9 pasos) | Token | Hex | rgb | Rol real | |-------|-----|-----|----------| | `--color-white-100` | `#FFFFFF` | `255, 255, 255` | **El canvas.** `page-background` = `primary-background` | | `--color-white-200` | `#FAFAFB` | `250, 250, 251` | `secondary-background`: las secciones que alternan | | `--color-white-300` | `#F3F4F6` | `243, 244, 246` | `surface-subtle`. El fondo del ghost en hover. El label del CTA | | `--color-white-400` | `#EDEFF3` | `237, 239, 243` | `surface` y `weak-stroke`: el borde de la píldora | | `--color-white-500` | `#E4E7EC` | `228, 231, 236` | `subtle-stroke`: el divisor por defecto | | `--color-white-600` | `#DEE2E7` | `222, 226, 231` | Paso intermedio de la rampa | | `--color-white-700` | `#D3D8DF` | `211, 216, 223` | `default-stroke` y `disabled-foreground` | | `--color-white-800` | `#CAD0D9` | `202, 208, 217` | **El borde del botón outline** en reposo. `strong-stroke` | | `--color-white-900` | `#B5BDC9` | `181, 189, 201` | El CTA deshabilitado | ### La rampa de azules (8 pasos) — el único color del sistema | Token | Hex | rgb | Rol real | |-------|-----|-----|----------| | `--color-blue-100` | `#E8F0FF` | `232, 240, 255` | Wash de fondo, banners informativos | | `--color-blue-200` | `#C8DCFF` | `200, 220, 255` | El stop del hover del CTA en el tema `.blue`, al 70% (`#C8DCFFB2`) | | `--color-blue-300` | `#94B9FF` | `148, 185, 255` | `link-strong-foreground` en oscuro | | `--color-blue-400` | `#709FF5` | `112, 159, 245` | **`accent-stroke`**: el borde de acento. Y el focus ring en oscuro | | `--color-blue-450` | `#538BF3` | `83, 139, 243` | El CTA del tema `.blue` | | `--color-blue-500` | `#266DF0` | `38, 109, 240` | **`link-foreground`.** El azul de Attio. Y el borde de la fila seleccionada | | `--color-blue-600` | `#245BC2` | `36, 91, 194` | `link-strong-foreground` en claro (links visitados / hover) | | `--color-blue-800` | `#1A2233` | `26, 34, 51` | Tinta azul profunda para superficies de acento | > **La regla del azul.** `#266DF0` no pinta **ni un solo botón** en el tema por defecto. Su trabajo es: links, focus ring, `accent-stroke`, y el borde de 1.08px de la fila seleccionada en los mocks de producto. Si pintas un CTA de azul, el sistema deja de leerse como Attio y empieza a leerse como cualquier SaaS. **El botón de Attio es negro.** ### Capa semántica: los mismos tokens, tres temas Attio define `:root` (claro), `.dark` y `.blue`. Un componente no conoce colores: conoce roles. | Rol (`--internal-color-…`) | `:root` (claro) | `.dark` | |---|---|---| | `primary-background` | `#FFFFFF` white-100 | `#101010` black-50 | | `secondary-background` | `#FAFAFB` white-200 | `#1C1D1F` black-100 | | `surface-subtle` | `#F3F4F6` white-300 | `#232529` black-300 | | `surface` | `#EDEFF3` white-400 | `#2E3238` black-400 | | `muted-background` | `#EDEFF3` white-400 | `#232529` black-300 | | `muted-strong-background` | `#D3D8DF` white-700 | `#2E3238` black-400 | | `weak-stroke` | `#EDEFF3` white-400 | `#232529` black-300 | | `subtle-stroke` | `#E4E7EC` white-500 | `#2E3238` black-400 | | `default-stroke` | `#D3D8DF` white-700 | `#6F7988` black-700 | | `strong-stroke` | `#CAD0D9` white-800 | `#8F99A8` black-800 | | `accent-stroke` | `#709FF5` blue-400 | `#709FF5` blue-400 | | `primary-foreground` | `#1C1D1F` black-100 | `#FFFFFF` white-100 | | `secondary-foreground` | `#232529` black-300 | `#EDEFF3` white-400 | | `tertiary-foreground` | `#505967` black-600 | `#B5BDC9` white-900 | | `accent-foreground` | `#6F7988` black-700 | `#8F99A8` black-800 | | `caption-foreground` | `#A4ADBA` black-900 | `#505967` black-600 | | `disabled-foreground` | `#D3D8DF` white-700 | `#6F7988` black-700 | | `link-foreground` | `#266DF0` blue-500 | `#709FF5` blue-400 | | `link-strong-foreground` | `#245BC2` blue-600 | `#94B9FF` blue-300 | | `focus-ring` | `#266DF04D` (blue-500 @30%) | `#709FF599` (blue-400 @60%) | > Fíjate en `accent-stroke`: es el **único** rol que vale lo mismo en claro y en oscuro. El azul de acento no se adapta al tema, porque su trabajo es cortar el tema. ### Bordes que no vienen de la rampa Dos bordes del sistema se declaran con alfa en lugar de con un token, y por una razón: | Uso | Valor | Por qué | |---|---|---| | Borde de tarjeta grande | `rgba(0, 0, 0, 0.05)` (`black-0/5`) | Se oscurece con lo que haya debajo. Sobre blanco es invisible; sobre una captura, define | | Borde de tarjeta apilada | `rgba(56, 62, 71, 0.1)` (black-500 @10%) | El mismo gris de la rampa, al 10%. Las cartas rotadas 3° y 6° del fondo | ### Colores de estado | Nombre | Hex | Par profundo | Uso | |---|---|---|---| | Success | `#0FC27B` | `#0DB472` | Deals ganados, checks | | Error | `#FF5B59` | `#F65351` | Destructivo, deals perdidos | | Warning | `#F5B900` | `#DBA601` | Deals en riesgo | Y tres colores de etiqueta del changelog, que son los únicos que se salen de la paleta: `#00B5E6` (design), `#AB92F1` (enhancement), `#72A4FB` (feature). ### Contraste verificado (sobre `#FFFFFF`) | Color | Ratio | Veredicto | |---|---|---| | `#1C1D1F` tinta | 16.6:1 | AAA | | `#232529` secundario | 15.2:1 | AAA | | `#2E3238` label de botón | 12.6:1 | AAA | | `#505967` terciario | 7.6:1 | AAA | | `#6F7988` acento (párrafo hero) | 4.6:1 | AA — límite. A 18px/500 pasa; **no bajarlo de 16px** | | `#A4ADBA` caption | 2.4:1 | Sólo decorativo o texto ≥24px | | `#266DF0` link | 4.6:1 | AA para texto normal | | `#202124` sobre CTA con `#F3F4F6` | 15.0:1 | AAA | > El punto delicado del sistema: `accent-foreground` (`#6F7988`) es el color del párrafo del hero y va justo en el filo de AA (4.6:1). Attio lo compensa poniéndolo a **18px con peso 500**. Si lo bajas a 14px regular, rompes accesibilidad. --- ## 3. Reglas de Tipografía ### Las cuatro familias reales | Rol | Familia real | Dónde | |---|---|---| | Display | **interDisplay** | Todos los `h1`–`h3` (`font-display`, `font-semibold`) | | Texto | **inter** | Absolutamente todo lo demás | | Mono | **JetBrains Mono** | Snippets de código, valores de token | | Serif | **tiemposText** | Citas editoriales y notas del changelog | ### Nota sobre sustitución tipográfica Este documento y el HTML de inspiración **sustituyen las fuentes propietarias por equivalentes de Google Fonts**, porque `interDisplay` y `tiemposText` no se sirven públicamente: | Original (Attio) | Sustituto (Google Fonts) | Por qué | |---|---|---| | `inter` | **Inter** | Es literalmente la misma tipografía. Inter es libre y está en Google Fonts. Cero pérdida | | `interDisplay` | **Inter Tight** | *Inter Display* es el corte óptico grande de Inter, y no está publicado en Google Fonts. **Inter Tight** es el corte público más cercano: mismo esqueleto, apertura reducida y tracking más apretado, que es exactamente lo que hace el corte display en titulares de 64–80px | | `tiemposText` | **Source Serif 4** | *Tiempos Text* es de Klim Type Foundry, comercial. Source Serif 4 comparte su ADN: serif de texto de contraste moderado, ojo grande, pensada para leerse a 16–20px, no una Didot decorativa | | `JetBrains Mono` | **JetBrains Mono** | Es la misma. Libre y en Google Fonts. Cero pérdida | En la práctica **dos de las cuatro familias son idénticas al original**. La sustitución sólo afecta al corte display y al serif. ### La escala tipográfica Atención a la columna de peso. Es la decisión más contraintuitiva del sistema. | 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` | | `text-heading-xs` | 28px | 34px | 600 | `-0.01em` | | `text-heading-sm` | 32px | 36px | 600 | `-0.01em` | | `text-heading-md` | 40px | 44px | 600 | `-0.01em` | | `text-heading-lg` | 56px | 60px | 600 | `-0.015em` | | `text-heading-xl` | 64px | 64px | 600 | `-0.02em` | > **El cuerpo de Attio pesa 500, no 400.** Cada token de texto trae `--text-*--font-weight: 500` de fábrica. 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 (`--font-weight-normal`) pero hay que pedirlo a mano. ### El titular del hero No usa ningún token de la escala. Se declara inline y es **fluido respecto a la altura del viewport**, no a la anchura: ```css font-family: interDisplay; /* -> Inter Tight */ font-weight: 600; line-height: 0.95; /* por DEBAJO de 1: las líneas se muerden */ font-size: clamp(64px, calc(16px + 5.333svh), 80px); letter-spacing: clamp(-2.4px, calc(2.08px - 0.3733svh), -1.28px); ``` Dos cosas raras y deliberadas: 1. **`svh`, no `vw`.** El titular crece con la **altura** de la pantalla. En un portátil de pantalla baja el hero encoge para que los mocks de producto sigan cabiendo debajo. La composición manda sobre la tipografía. 2. **El tracking va al revés que el tamaño.** Cuando `svh` sube, `font-size` sube (+5.333 por unidad) y `letter-spacing` **baja** (−0.3733 por unidad, y ya en negativo). Cuanto más grande el titular, más apretado. Es la regla óptica clásica, escrita como una interpolación lineal en CSS. ### Escala de pesos | Token | Valor | Uso | |---|---|---| | `--font-weight-light` | 300 | Prácticamente sin uso | | `--font-weight-normal` | 400 | Serif editorial y párrafos largos | | `--font-weight-medium` | 500 | **El peso por defecto de todo el texto** | | `--font-weight-semibold` | 600 | Todos los titulares | | `--font-weight-bold` | 700 | Énfasis dentro de párrafo | **No hay 800 ni 900.** El techo es 700 y casi nunca se toca. ### Letter spacing: siempre negativo, nunca cero (salvo en 12px) | Rango | Tracking | |---|---| | 12px | `0` — el único tamaño que NO se aprieta | | 14px | `-0.005em` | | 16–24px | `-0.01em` | | 28–40px | `-0.01em` | | 56px | `-0.015em` | | 64px | `-0.02em` | | 80px (hero) | `-1.28px` a `-2.4px` | La curva es monótona: **más grande, más apretado**. Y a 12px el tracking vuelve a cero porque apretar texto diminuto lo hace ilegible. --- ## 4. Anatomía de los CTAs Esta es la razón por la que Attio está en esta galería. Los tres botones comparten geometría y se diferencian **sólo** en cómo tratan el color. ### Geometría compartida ```css /* la base, idéntica en primary / outline / ghost */ display: inline-flex; align-items: center; justify-content: center; height: 36px; /* h-9 */ border-radius: 10px; /* rounded-[10px] — un valor arbitrario, NO un token */ padding: 0 12px; /* px-3 */ gap: 6px; /* gap-x-1.5 */ border: 1px solid; /* SIEMPRE hay borde, aunque sea transparente */ font-size: 14px; /* text-sm */ line-height: 20px; font-weight: 500; letter-spacing: -0.005em; white-space: nowrap; cursor: pointer; transition-duration: 300ms; transition-timing-function: cubic-bezier(.2, 0, 0, 1); ``` Y la variante grande (que en el sitio es la del **móvil**, `max-lg:`, y la del CTA del hero): ```css height: 46px; /* h-11.5 */ border-radius: 12px; /* rounded-xl — aquí sí es un token */ padding: 0 14px; /* px-3.5 */ gap: 8px; /* gap-x-2 */ font-size: 16px; /* text-base */ ``` > **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 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 ```css /* si el ÚLTIMO hijo es un svg o una img, el padding derecho baja de 12px a 8px */ .button:has(> svg:last-child, > img:last-child) { padding-right: 8px; } .button:has(> svg:first-child, > img:first-child){ padding-left: 8px; } ``` Un icono es ópticamente más ligero que una letra, así que necesita menos aire. Attio lo resuelve con `:has()` en el propio botón, sin variantes ni props. ### El botón primario: un foco que amanece El primario **no es azul**. Es `#202124` con borde `#505967`. ```css .button-primary { 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); transition-property: color, border-color, --button-primary-bg-from, --button-primary-bg-to; /* <- interpola las VARIABLES */ } ``` Y las variables, por estado (tema claro): | Estado | `bg-from` | `bg-to` | fallback | borde | texto | |---|---|---|---|---|---| | reposo | `#202124` | `#202124` | `#202124` | `#505967` | `#F3F4F6` | | **hover** | **`#505967`** | `#202124` | `#383E47` | `#505967` | `#FFFFFF` | | focus-visible | `#505967` | `#202124` | `#383E47` | `#505967` | `#FFFFFF` | | active | `#383E47` | `#383E47` | `#505967` | `#505967` | `#FFFFFF` | | disabled | `#B5BDC9` | `#B5BDC9` | `#B5BDC9` | `#B5BDC9` | `#FFFFFF` | Lee la fila de reposo: **los dos stops valen lo mismo**. Un degradado radial de `#202124` a `#202124` es un rectángulo plano. El degradado está ahí, montado, pero apagado. En hover, sólo `bg-from` cambia. Y `bg-from` es el stop del **centro** del radial, que está posicionado en `at center -10%` — es decir, **fuera del botón, diez por ciento por encima de su borde superior**. Cuando ese stop se aclara 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: ```css @property --button-primary-bg-from { syntax: ""; inherits: false; initial-value: #000; } @property --button-primary-bg-to { syntax: ""; inherits: false; initial-value: #000; } ``` Sin `@property`, una custom property es una cadena de texto y el navegador la cambia de golpe. Con `syntax: ""`, el navegador sabe que es un color y **lo interpola**. El degradado se anima. Es la única forma de animar un `background-image` sin un pseudo-elemento y un `opacity`. ### La asimetría del tiempo ```css transition-duration: 300ms; /* por defecto */ .button:hover { transition-duration: 50ms; } .button:active { transition-duration: 50ms; } ``` **Entrar cuesta 50ms. Salir cuesta 300ms.** El botón responde instantáneamente al ratón y se apaga despacio cuando te vas. Es lo contrario del `transition: all .2s` simétrico que sale por defecto en todas partes, y es la diferencia entre un control que se siente vivo y uno que se siente laggy. La curva es `cubic-bezier(.2, 0, 0, 1)` — con el segundo punto de control en `x=0`, la salida es prácticamente una caída libre frenada. ### El botón outline: sólo se mueve el borde | Estado | fondo | borde | texto | |---|---|---|---| | reposo | `#FFFFFF` | `#CAD0D9` | `#2E3238` | | **hover** | `#FFFFFF` | **`#6F7988`** | `#2E3238` | | focus-visible | `#FFFFFF` | `#6F7988` | `#2E3238` | | active | `#F3F4F6` | `#383E47` | `#232529` | | disabled | `#FFFFFF` | `#E4E7EC` | `#CAD0D9` | 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. Sólo al **pulsar** aparece el fondo `#F3F4F6`. El sistema reserva el relleno para confirmar la acción, no para anticiparla. ### El botón ghost: sólo se mueve el fondo | Estado | fondo | texto | |---|---|---| | reposo | `transparent` | `#2E3238` | | **hover** | `#F3F4F6` | `#232529` | | active | `#EDEFF3` | `#202124` | | disabled | `transparent` | `#B5BDC9` | El espejo exacto del outline: aquí **sólo se mueve el fondo**. Y el borde sigue existiendo (`border-color: transparent`), así que el botón no cambia de tamaño ni empuja a sus vecinos cuando se convierte en otra variante. > **La regla de los tres.** 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. Ningún botón de Attio anima `transform`, ni escala, ni se desplaza, ni gana sombra en hover. ### El focus ring ```css --internal-color-focus-ring: #266DF04D; /* blue-500 al 30% */ /* en oscuro: #709FF599 — blue-400 al 60% */ ``` El único sitio donde el azul toca un control. Y el primario, además del ring, aplica su estado de hover en `:focus-visible`: el foco del degradado se enciende igual, para que el teclado vea lo mismo que el ratón. --- ## 5. Anatomía de las Tarjetas Una tarjeta de Attio es **un fondo blanco, un borde de un alfa muy bajo, un radio de 12px y N capas de la escalera**. No hay más. La variación entre tarjetas es la N. ### Tarjeta base ```css .card { background: #FFFFFF; /* primary-background */ border: 1px solid rgba(0, 0, 0, 0.05); /* black-0/5 */ border-radius: 12px; /* rounded-xl. En móvil baja a 6px (rounded-md) */ box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.01), /* layer 1 */ 0px 2px 4px -1px rgba(0,0,0,0.02), /* layer 2 */ 0px 4px 8px -2px rgba(0,0,0,0.03); /* layer 3 */ } ``` El radio **cambia con el breakpoint**: `rounded-md` (6px) en móvil, `rounded-xl` (12px) en `lg` (≥992px). Y el borde sólo existe a partir de `lg` (`lg:border lg:border-black-0/5`). En una pantalla pequeña, una tarjeta que ocupa el ancho completo no necesita borde: sus límites son los del viewport. ### La escalera aplicada | Clase | Capas | Uso real | |---|---|---| | `shadow-attio-2` | 1–2 | Header pegajoso en móvil | | `shadow-attio-3` | 1–3 | Tarjeta en reposo | | `shadow-attio-4` | 1–4 | Tarjeta en hover | | `shadow-attio-5` | 1–5 | Popover, dropdown | | `shadow-attio-6` | 1–6 | Modal, panel de producto flotante | | `shadow-attio-7` | 1–7 | Reservada. El escalón que casi nunca se usa | **El hover de una tarjeta es subir un peldaño**: de `shadow-attio-3` a `shadow-attio-4`. No es una sombra nueva. Es **la misma pila más una capa**. Como cada capa es el doble de la anterior, añadir la cuarta duplica el alcance de la caída sin tocar las tres que ya estaban. La tarjeta se separa del papel; no se ilumina, no se escala, no se desplaza. ```css .card { box-shadow: var(--l1), var(--l2), var(--l3); } .card:hover { box-shadow: var(--l1), var(--l2), var(--l3), var(--l4); } ``` ### Tarjeta de producto (los mocks de la app) Es la única que usa una sombra fuera de la escalera, y la única con un borde de acento: ```css .card-product { background: #FFFFFF; border-radius: 15px; /* rounded-[15px] — otro arbitrario */ border: 1.08px solid transparent; /* !! 1.08px */ padding: 6.5px; gap: 1px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 0 2px rgba(28, 40, 64, 0.18); /* halo AZULADO, sin desplazamiento */ } .card-product[data-selected] { border-color: #266DF0; } /* blue-500 */ ``` Tres detalles medidos: - **`1.08px` de borde.** No es un error. La maqueta de producto se sirve a una escala fraccionaria (en móvil el mismo bloque es `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. Es la misma fila de la app, a otra escala. - **`6.5px` / `13px` de padding.** Mismo motivo: el móvil usa `6.5px`, el desktop `13px`. La escala del mock es exactamente 2×. - **El halo es azul.** `rgba(28, 40, 64, 0.18)` — un `0 0 2px` sin desplazamiento, teñido de azul marino, no de negro. Sobre un canvas frío, un halo negro se ve sucio. ### Tarjeta apilada (el motivo de las cartas rotadas) Detrás de cada mock hay dos rectángulos girados que sugieren una baraja: ```css .stack-back { transform: rotate(6deg); background: #FAFAFB; border: 1px solid rgba(56,62,71,0.1); border-radius: 12px; } .stack-mid { transform: rotate(3deg); background: #FFFFFF; border: 1px solid rgba(56,62,71,0.1); border-radius: 12px; } ``` 3° y 6°: una progresión aritmética, igual que todo lo demás. Y el borde es `black-500` al 10% — el mismo gris de la rampa, con alfa, para que las cartas se oscurezcan entre sí al solaparse. ### Panel de cristal (los flotantes del hero) El único glassmorphism del sitio, y es mínimo: ```css .panel-glass { border-radius: 16px; /* lg:rounded-2xl */ padding: 6px; /* lg:p-1.5 */ padding-top: 0; background: rgba(243, 244, 246, 0.8); /* white-300 al 80% */ backdrop-filter: blur(12px); /* blur-md */ } /* y cuando el hero hace scroll, el blur se APAGA */ [data-hero-scrolling] .panel-glass { backdrop-filter: none; } ``` El detalle que importa: **el blur se desactiva durante el scroll**. `backdrop-filter` es caro y a 60fps se nota. Attio lo apaga mientras el hero se mueve y lo vuelve a encender al parar. La versión oscura del mismo panel usa `rgba(0,0,0,0.8)`. ### Padding y gap de tarjeta Todo es múltiplo de 4 salvo los mocks de producto (que arrastran la escala de la app): | Contexto | Padding | Gap | |---|---|---| | Tarjeta de contenido | `24px` (`p-6`) | `16px` (`gap-4`) | | Tarjeta compacta | `16px` (`p-4`) | `12px` (`gap-3`) | | Fila de producto (desktop) | `13px` | `10px` | | Fila de producto (móvil) | `6.5px` | `5px` | | Panel de cristal | `6px`, `padding-top: 0` | — | --- ## 6. La Píldora del Hero El anuncio de arriba del titular. Pequeño, pero es un componente entero: ```css /* el exterior: SÓLO el borde */ .pill { display: block; border: 1px solid #EDEFF3; /* weak-stroke */ border-radius: 13px; /* inline, arbitrario */ transition: border-color 300ms cubic-bezier(.2, 0, 0, 1); } .pill:active { border-color: #E4E7EC; } /* subtle-stroke */ /* el
interior: SÓLO el fondo */ .pill-inner { display: flex; align-items: center; gap: 4px; /* gap-x-1 */ padding: 5px 7px 5px 11px; /* asimétrico */ background: #FFFFFF; border-radius: 13px; font-size: 13px; line-height: 18px; font-weight: 500; color: #232529; /* secondary-foreground */ transition: background-color 300ms cubic-bezier(.2, 0, 0, 1); } .pill-inner:hover { background: #FBFBFC; } ``` - **Dos elementos, dos responsabilidades.** El `` lleva el borde, el `
` lleva el fondo. Ninguno hace las dos cosas. - **Padding asimétrico**: `11px` a la izquierda (donde va texto) y `7px` a la derecha (donde va el chevron de 12×12). El mismo principio óptico que el `:has()` de los botones, aquí escrito a mano. - **`#FBFBFC` es el hover.** Está **entre** `white-100` (`#FFFFFF`) y `white-200` (`#FAFAFB`). No es un token: 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. - **Radio 13px**, otro arbitrario que no está en la escala (2/4/6/8/12/16/20). --- ## 7. Principios de Layout ### Escala de espaciado Base `--spacing: 0.25rem` = **4px** (Tailwind v4). Todo el sistema es `n × 4px`. | Paso | px | Uso | |---|---|---| | `1` | 4 | Gap de la píldora | | `1.5` | 6 | Gap del botón normal, padding del panel de cristal | | `2` | 8 | Gap del botón grande, padding lateral con icono | | `3` | 12 | Padding del botón | | `3.5` | 14 | Padding del botón grande | | `4` | 16 | Gap de tarjeta | | `6` | 24 | Padding de tarjeta | | `9` | 36 | **Altura del botón.** Y el gap del bloque del hero | | `11.5` | 46 | Altura del botón grande | | `12` | 48 | Altura del banner superior | | `20` | 80 | Padding superior de columna | ### Radios | Token | px | Uso | |---|---|---| | `--radius-xs` | 2 | Chips diminutos dentro de los mocks | | `--radius-sm` | 4 | Badges de producto (móvil) | | `--radius-md` | 6 | Tarjeta en móvil | | — | **10** | **Botones** (arbitrario) | | `--radius-lg` | 8 | Badges de producto (desktop) | | `--radius-xl` | 12 | **Tarjetas**, botón grande | | — | **13** | **Píldora** (arbitrario) | | — | **15** | **Fila de producto** (arbitrario, escala de la app) | | `--radius-2xl` | 16 | Panel de cristal, bloques grandes | | `--radius-3xl` | 20 | Contenedores de sección | > Tres radios del sistema (10, 13, 15) **no están en la escala**. No es descuido: son los tres componentes que Attio afinó ópticamente uno por uno — el botón, la píldora y la fila de la app. Todo lo demás cae en la escala. ### Contenedores y breakpoints ```css --container-sm: 24rem; /* 384px */ --container-md: 28rem; /* 448px */ --container-lg: 32rem; /* 512px */ --container-2xl: 42rem; /* 672px */ --container-4xl: 56rem; /* 896px */ --container-5xl: 64rem; /* 1024px */ --container-6xl: 72rem; /* 1152px */ --container-7xl: 80rem; /* 1280px */ --breakpoint-lg: 992px; /* !! no 1024 */ ``` **El breakpoint `lg` es 992px, no el 1024 por defecto de Tailwind.** Attio lo bajó 32px. Es el punto donde una tabla de CRM con la barra lateral desplegada deja de caber, y ese punto no lo decide el framework. ### El header ```css --site-header-nav-height: 68px; /* desktop; 52px en móvil */ --site-header-banner-visible-height: 48px; --site-header-polaris-toolbar-visible-height: 36px; --site-header-subheader-mobile-visible-height: 63px; --site-header-height: calc( var(--site-header-polaris-toolbar-height) + var(--site-header-nav-height) + var(--site-header-subheader-height) + var(--site-header-banner-height) ); ``` La altura del header es una **suma de cuatro piezas que pueden valer 0**. Cada pieza tiene su par `-visible-height` / `-hidden-height: 0px`, y mostrar u ocultar una franja 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`. --- ## 8. Movimiento ### Las curvas | Token | Valor | Cuándo | |---|---|---| | `--ease-in-out` | `cubic-bezier(.2, 0, 0, 1)` | **La curva por defecto.** Botones, píldoras, bordes | | `--ease-out` | `cubic-bezier(0, 0, 0, 1)` | Entradas | | `--ease-in` | `cubic-bezier(.3, 0, 1, 1)` | Salidas | | `--ease-in-out-cubic` | `cubic-bezier(.65, 0, .35, 1)` | Acordeones (`slideDown` / `slideUp`, 300ms) | | `--ease-out-cubic` | `cubic-bezier(.33, 1, .68, 1)` | Fade-in de navegación (100ms) | | `--ease-in-out-quad` | `cubic-bezier(.45, .05, .55, .95)` | Diálogos (`dialog-scale-in`, 200ms) | | `--ease-in-out-expo` | `cubic-bezier(1, 0, 0, 1)` | Transiciones dramáticas de sección | | `--ease-reveal` | `cubic-bezier(0, 0, .58, 1)` | Revelados al hacer scroll | 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. Es una curva de "material" — se parece más a algo con masa que a una interpolación lineal. ### Duraciones reales | Interacción | Duración | |---|---| | Botón / píldora, **entrar** | **50ms** | | Botón / píldora, **salir** | 300ms | | Navegación (fade) | 100ms | | Navegación (deslizar) | 200ms | | Diálogo (escala) | 200ms | | Acordeón | 300ms | | Barrido del buscador | 1000ms, infinito | ### La entrada del hero 44 nodos del hero llegan del servidor con `style="filter: blur(1.5px); opacity: 0"` y JavaScript los revela después. El texto no aparece: **se enfoca**. Es un `blur` de 1.5px que cae a 0 a la vez que la opacidad sube. Es también la razón por la que capturar este hero sin JS obliga a neutralizar esos 44 estilos inline. --- ## 9. Lo que Sí y lo que No ### Sí hacer - **Apilar la escalera.** Elevar es añadir la siguiente capa, nunca cambiar la sombra. `shadow-3` → `shadow-4` en hover. - **El CTA primario es negro** (`#202124`), con borde `#505967`. Siempre. - **Registrar los stops del degradado con `@property`** si quieres el hover del primario. Sin eso, el botón parpadea en vez de encenderse. - **Entrar en 50ms, salir en 300ms.** La asimetría es el sistema. - **Peso 500 para el cuerpo.** Es el valor por defecto de todos los tokens de texto. - **Tracking negativo, y más negativo cuanto más grande.** Salvo a 12px, donde es 0. - **Una propiedad por variante de botón.** Primario: degradado. Outline: borde. Ghost: fondo. - **Bordes de tarjeta con alfa** (`rgba(0,0,0,.05)`), para que se oscurezcan con lo que hay debajo. - **Apagar `backdrop-filter` durante el scroll.** ### No hacer - **No pintes un botón de azul.** `#266DF0` es para links, focus ring y `accent-stroke`. Un CTA azul mata la identidad. - **No animes `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. - **No uses una sombra de una sola capa.** `0 4px 12px rgba(0,0,0,.1)` no es de este sistema. La profundidad se apila. - **No uses negro puro** salvo en el banner. Todos los "negros" tienen azul dentro. - **No subas la opacidad de una capa.** Ninguna pasa del 7%. Si necesitas más peso, añade otra capa. - **No pongas `accent-foreground` (`#6F7988`) por debajo de 16px.** Está en el filo de AA. - **No uses transiciones simétricas.** Un `transition: all .2s ease` deshace la mitad del carácter del sistema. - **No redondees el 1.08px de la fila de producto a 1px.** Rompe la correspondencia de escala con la app real. --- ## 10. Anexo: Stack Técnico Detectado | Qué | Detalle | |---|---| | Framework | Next.js (App Router), servido desde Vercel | | CSS | Tailwind v4 con `@theme`, 8 chunks, 462 KB sin comprimir | | HTML | Server-rendered, 1.09 MB. La página entera existe antes de que corra JS | | Fuentes | `next/font` local: inter, interDisplay, tiemposText, JetBrains Mono (`.woff2`, `font-display: swap`, con familias `Fallback` y `size-adjust`) | | Temas | Tres ámbitos de clase: `:root`, `.dark`, `.blue` | | Anti-bot | **Detecta CDP.** Con Puppeteer al mando, el runtime de la página se cuelga y `Runtime.callFunctionOn` expira. El CSS y el HTML se sirven sin restricción a `curl` | | Color en CSS | Los tokens se compilan a `lab()` con fallback hex. Los hex de este documento son los fallbacks literales del CSS servido | | Animación de color | `@property` sobre `--button-primary-bg-from` / `-to` con `syntax: ""` | | Utilidad de sombra | `shadow-attio-{2..7}`, acumulativa sobre `--shadow-attio-layer-{1..7}` |