/* ============================================================
   Irene Vélez — UX/UI Designer & Digital Marketing Specialist
   Editorial Claro · neutro frío, lila y verde salvia
   1. Tokens  2. Reset  3. Base  4. Layout  5. Componentes base  6. Nav
   7. Motion  8. Hero  8b. Riel  9. Proof  10. Work  11. Toolkit  12. Proceso
   13. (retirado)  14. About  15. Contact  16. Footer
   17. Utilidades  18. Modal
   ============================================================ */

/* ---------- 1. Tokens ---------- */
:root {
  /* Neutro FRÍO, no crema: el lila y el verde salvia se ensucian sobre un
     fondo cálido — el lila tira a marrón y el verde a oliva. El blanco roto
     con sesgo violeta (#F7F7FA) los deja limpios a los dos. */
  --bg: #F7F7FA;
  --surface: #FFFFFF;
  --raised: #EFEFF4;
  --hairline: rgba(26, 24, 34, .12);
  --text: #1A1822;
  --muted: rgba(26, 24, 34, .68);
  /* .62 y no menos: `--faint` viste las etiquetas mono de 11px (métricas,
     índice del toolkit, numeración de casos, pies de formulario). A .58 el
     contraste cae a 4.2:1 y no llega al mínimo AA para texto normal; a .62
     da 4.79:1. */
  --faint: rgba(26, 24, 34, .62);

  /* Los dos acentos tienen trabajos distintos, no son intercambiables:
       --accent  (lila)  = identidad y navegación — titulares, botones,
                           enlaces, kickers, etiquetas de sección.
       --accent2 (verde) = resultado medido — contadores del hero, cifras del
                           libro de resultados, métrica de cada caso, señal de
                           disponibilidad.
     Así el color dice algo del contenido en vez de decorarlo: el verde sólo
     aparece donde hay un número que alguien midió. */
  --accent: #5B4B8A;
  --accent-ink: #FFFFFF;
  /* Los lavados y las sombras usan rgba() literal porque necesitan alfa;
     mantener sincronizado con --accent y --accent2 (y con la paleta de la
     lámina iridiscente del hero en main.js). Sobre fondo claro el acento TIÑE
     en lugar de brillar: los alfas son bajos porque un radial al 20% ya se lee
     como mancha de color plano, no como atmósfera. */
  --accent-rgb: 91, 75, 138;
  --accent2: #2F6F55;
  --accent2-rgb: 47, 111, 85;
  --ink-rgb: 26, 24, 34;
  /* Mismo verde que --accent2: en este sistema "medido" y "en verde" son la
     misma idea, y dos verdes casi iguales se leerían como un error. */
  --live: #2F6F55;
  --danger: #B3261E;

  /* Los cuatro tonos de la lámina del hero que además conviven con el lila y el
     verde de la marca. No son elegidos a ojo: salen de la MISMA paleta coseno
     que pinta el canvas (main.js, buildLut — base .73, amplitud .25, fases
     0/.31/.63), muestreada en p = .15, .30, .45 y .60. Los otros tramos de esa
     curva —coral, arena, lima— quedan fuera a propósito: son los que
     convertirían el fondo en arcoíris y los que se pelean con el verde salvia
     de las cifras. */
  --iris-violet: 167, 137, 244;
  --iris-pink:   224, 124, 198;
  --iris-sky:    125, 190, 242;
  --iris-mint:   135, 240, 194;

  /* Superficie de ficha: blanco translúcido, no blanco. Sobre la lámina un
     blanco opaco se lee como un parche pegado encima; al 76% la ficha se tiñe
     de lo que tiene debajo y sigue siendo papel. El alfa está calculado, no
     elegido: con el tono más cargado del film debajo, `--faint` sigue dando
     4.77:1 sobre la ficha — el mismo contraste para el que se afinó.
     Sin `backdrop-filter` a propósito: el film es un degradado suavísimo, no
     tiene detalle que desenfocar, así que el blur costaría GPU en las 22 fichas
     de la página sin cambiar un píxel de lo que se ve. */
  --glass: rgba(255, 255, 255, .76);

  --font-display: "Fraunces", Georgia, "Times New Roman", serif;
  --font-ui: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "DM Mono", ui-monospace, Consolas, "SF Mono", monospace;

  --fs-display: clamp(2.35rem, 1.1rem + 5vw, 5.25rem);
  --fs-h2: clamp(1.8rem, 1.15rem + 2.2vw, 3rem);
  --fs-h3: clamp(1.15rem, 1rem + .6vw, 1.5rem);
  --fs-body: clamp(.95rem, .9rem + .25vw, 1.0625rem);
  --fs-small: .875rem;
  --fs-mono: .6875rem;

  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 24px; --sp-6: 32px; --sp-7: 48px; --sp-8: 64px;
  --sp-9: 96px; --sp-10: 128px;

  /* La calle no baja de 20px (pulgar en móvil) ni pasa de 48: por encima de
     eso deja de ser margen y empieza a ser hueco. */
  --gutter: clamp(20px, 4vw, 48px);
  /* Tope del contenido. A 1180px un monitor de 1700 dejaba 260px muertos a
     cada lado; a 1480 el bloque respira sin quedar flotando en el centro.
     Subirlo no alarga ninguna línea de texto: los párrafos y titulares llevan
     su propio tope en `ch` (--measure, .hero__title, .split__title…), así que
     lo que se ensancha son las retículas y los rieles, no la lectura. */
  --container: 1480px;
  --measure: 68ch;
  --radius: 6px;
  --section-y: clamp(88px, 12vh, 168px);
  --nav-h: 68px;

  --ease: cubic-bezier(.22, .61, .36, 1);
  /* Sobrepaso corto (~11%), sólo para nodos y puntos: un elemento pequeño que
     aterriza clavado se lee como aparición; con el rebote se lee como que algo
     lo colocó ahí. No se usa en cajas grandes, donde el sobrepaso marea. */
  --ease-back: cubic-bezier(.34, 1.4, .64, 1);
}

/* ---------- 2. Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; }
body { min-height: 100vh; -webkit-font-smoothing: antialiased; }
img, picture, svg { display: block; max-width: 100%; }
button, input, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
ul, ol { list-style: none; padding: 0; }

/* ---------- 3. Base ---------- */
@media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } }

body {
  position: relative;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: var(--fs-body);
  line-height: 1.65;
  font-feature-settings: "cv11", "ss01";
}

/* La lámina del hero, continuada por toda la página. Es UN film del alto del
   documento, no un lavado por sección: al bajar, el tono va derivando de
   violeta a rosa, cielo y menta, y vuelve al violeta en el pie.
   Ahí está la diferencia entre iridiscencia y arcoíris: los cuatro tonos nunca
   se ven juntos. Cada pantalla cae dentro de uno dominante, y el cambio ocurre
   entre secciones, mientras nadie lo mira.
   No se reutiliza el canvas del hero para esto: animarlo a lo largo de toda la
   página costaría un fotograma por scroll y dejaría el texto largo sobre
   pliegues en movimiento. El hero tiene la lámina viva; el resto, su rastro.
   `position: absolute` y no `fixed`: un film clavado al viewport se lee como
   una viñeta de color que no se mueve nunca. Este acompaña al documento.
   Radios en PORCENTAJE, no en px: el alto del documento cambia mucho entre
   escritorio y móvil, y con radios fijos los tonos se separarían en islas con
   bandas neutras en medio. En porcentaje, los cinco focos siempre se solapan
   igual y el degradado es continuo mida lo que mida la página. */
body::before {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(88% 24% at 4% 2%,   rgba(var(--iris-violet), .26), transparent),
    radial-gradient(80% 22% at 98% 22%, rgba(var(--iris-pink),   .20), transparent),
    radial-gradient(85% 24% at 2% 48%,  rgba(var(--iris-sky),    .20), transparent),
    radial-gradient(80% 22% at 100% 72%, rgba(var(--iris-mint),  .22), transparent),
    radial-gradient(88% 24% at 6% 97%,  rgba(var(--iris-violet), .20), transparent);
}

/* Fraunces es variable (opsz 9..144): con `font-optical-sizing: auto` el
   titular grande afina sus remates y el texto chico los engorda solo. Sobre
   fondo claro esto importa más que sobre oscuro, porque la tinta no "sangra"
   y un serif fino a 14px se rompe. */
h1, h2, h3 {
  font-family: var(--font-display); font-weight: 400; letter-spacing: -.018em;
  font-optical-sizing: auto;
}
h1 { font-size: var(--fs-display); line-height: 1.02; }
h2 { font-size: var(--fs-h2); line-height: 1.08; }
h3 { font-size: var(--fs-h3); line-height: 1.2; }
p { color: var(--muted); max-width: var(--measure); }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}
::selection { background: var(--accent); color: var(--accent-ink); }

/* ---------- 4. Layout ---------- */
.container { width: min(var(--container), 100% - 2 * var(--gutter)); margin-inline: auto; }
.section { padding-block: var(--section-y); border-top: 1px solid var(--hairline); }

/* ---------- 5. Componentes base ---------- */
.kicker {
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--accent);
  display: block;
}
.mono {
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--faint);
}
.accent { color: var(--accent); }

.btn {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: 13px 22px; border-radius: var(--radius);
  border: 1px solid rgba(var(--ink-rgb), .22);
  font-size: var(--fs-small); font-weight: 500;
  background: var(--surface);
}
.btn:hover { border-color: var(--text); }
.btn--primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 600; }
.btn--primary:hover { background: var(--text); border-color: var(--text); color: var(--bg); }

@media (prefers-reduced-motion: no-preference) {
  .btn { transition: border-color .2s var(--ease), background-color .2s var(--ease), color .2s var(--ease); }
}

/* ---------- 6. Nav ---------- */
.nav {
  position: fixed; inset: 0 0 auto; z-index: 50;
  height: var(--nav-h);
  background: transparent;
  border-bottom: 1px solid transparent;
}
.nav[data-scrolled="true"] {
  background: color-mix(in srgb, var(--bg) 84%, transparent);
  backdrop-filter: blur(14px);
  border-bottom-color: var(--hairline);
}
/* Con el menú móvil abierto el panel de enlaces sí lleva fondo propio, pero la
   barra necesita el suyo para que marca y botones no floten sobre el hero. */
.nav:has(.nav__links[data-menu-open="true"]) {
  background: var(--bg);
  border-bottom-color: var(--hairline);
}
@supports not (backdrop-filter: blur(1px)) { .nav[data-scrolled="true"] { background: var(--bg); } }

.nav__progress {
  position: absolute; left: 0; bottom: -1px; height: 2px; width: 0%;
  background: var(--accent);
}
.nav__inner { display: flex; align-items: center; gap: var(--sp-6); height: 100%; }

.nav__brand { font-family: var(--font-display); font-size: 1.0625rem; letter-spacing: -.01em; line-height: 1; }
.nav__brandRole { display: block; margin-top: 3px; color: var(--faint); font-size: .5625rem; }

.nav__links { display: flex; gap: var(--sp-6); margin-left: auto; }
.nav__links a {
  position: relative; font-size: var(--fs-small); color: var(--muted);
}
.nav__links a:hover { color: var(--text); }
.nav__links a[aria-current="true"] { color: var(--text); }
.nav__links a[aria-current="true"]::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -6px;
  height: 1px; background: var(--accent);
}

.nav__actions { display: flex; align-items: center; gap: var(--sp-3); }
.nav__lang {
  display: inline-flex; gap: 3px; padding: 7px 11px;
  border: 1px solid var(--hairline); border-radius: 999px; color: var(--faint);
}
.nav__lang:hover { border-color: var(--text); }
.nav__langOn { color: var(--accent); }
.nav__langSep { opacity: .35; }
.nav__lang[aria-pressed="true"] .nav__langOn { color: var(--faint); }
.nav__lang[aria-pressed="true"] .nav__langOff { color: var(--accent); }

.nav__burger { display: none; flex-direction: column; gap: 5px; padding: 10px 6px; }
.nav__burger span { display: block; width: 20px; height: 1.5px; background: var(--text); }
.nav__burger[aria-expanded="true"] span:first-child { transform: translateY(3.25px) rotate(45deg); }
.nav__burger[aria-expanded="true"] span:last-child { transform: translateY(-3.25px) rotate(-45deg); }

@media (prefers-reduced-motion: no-preference) {
  .nav { transition: background-color .25s var(--ease), border-color .25s var(--ease); }
  .nav__links a { transition: color .2s var(--ease); }
  .nav__lang { transition: border-color .2s var(--ease); }
  .nav__burger span { transition: transform .2s var(--ease); }
}

@media (max-width: 880px) {
  .nav__burger { display: flex; }
  .nav__links {
    position: fixed; inset: var(--nav-h) 0 auto; flex-direction: column; gap: 0;
    background: var(--bg); border-bottom: 1px solid var(--hairline);
    padding: var(--sp-3) var(--gutter) var(--sp-5);
    transform: translateY(-8px); opacity: 0; pointer-events: none; visibility: hidden;
  }
  .nav__links[data-menu-open="true"] { transform: none; opacity: 1; pointer-events: auto; visibility: visible; }
  .nav__links a { padding: var(--sp-4) 0; border-bottom: 1px solid var(--hairline); font-size: 1rem; }
}

@media (max-width: 880px) and (prefers-reduced-motion: no-preference) {
  .nav__links { transition: opacity .2s var(--ease), transform .2s var(--ease); }
}

#main { padding-top: var(--nav-h); }

/* ---------- 7. Motion: sistema de reveal ---------- */
/* `[data-reveal]` usa la propiedad `translate`, no `transform`, para la entrada:
   `transform` queda libre para que `.card` (tilt 3D) y `.word > span` compongan
   su propio movimiento sin que la regla de reveal los pise por especificidad. */
@media (prefers-reduced-motion: no-preference) {
  [data-js="true"] [data-reveal] {
    opacity: 0;
    translate: 0 16px;
    transition: opacity .6s var(--ease), translate .6s var(--ease);
  }
  [data-reveal][data-revealed="true"] { opacity: 1; translate: none; }

  .word { display: inline-block; overflow: hidden; vertical-align: bottom; }
  .word > span {
    display: inline-block;
    transform: translateY(105%);
    transition: transform .7s var(--ease);
    transition-delay: var(--d, 0ms);
  }
  [data-revealed="true"] .word > span { transform: none; }
}
/* Con reduce, nada se oculta: [data-reveal] no recibe estilos y todo es visible.
   Sin `data-js="true"` (JS no llegó a ejecutar), tampoco. */

/* ---------- 8. Hero ---------- */
/* `min-height` reserva el alto del bloque para que el font swap de Google Fonts
   (Georgia -> Fraunces con `display=swap`) no reduzca/aumente la caja y
   arrastre el fondo con ella (CLS). Es un piso relativo al viewport, no un
   alto fijo: nunca recorta contenido, solo evita que la caja encoja. */
.hero {
  position: relative; overflow: hidden;
  background: var(--bg);
  /* El nav es transparente arriba: el margen negativo anula el padding-top de
     #main para que el fondo del hero corra bajo el nav, y el padding-top
     devuelve el contenido a su posición original. */
  margin-top: calc(-1 * var(--nav-h));
  padding-block: calc(clamp(56px, 8vh, 104px) + var(--nav-h)) var(--sp-7);
  min-height: clamp(600px, 92vh, 900px);
  display: flex; flex-direction: column;
}

/* Iridiscencia: la película holográfica que pinta initIridescence() en main.js.
   La máscara la disuelve antes de la barra de métricas, y no es un capricho de
   composición: los contadores son verde salvia sobre un arcoíris pastel, y ahí
   el color de "medido" deja de significar nada. Abajo el hero vuelve a ser
   papel y las cifras recuperan su fondo.
   Con reduce-motion el canvas NO se oculta: el JS pinta un fotograma estático
   (ver initIridescence). Una lámina holográfica quieta sigue siendo una lámina
   holográfica; lo que molesta del movimiento es el movimiento. */
.hero__iridescence {
  position: absolute; inset: 0;
  /* canvas es replaced: con inset solo NO se estira (usa su 300×150
     intrínseco), necesita width/height explícitos. */
  width: 100%; height: 100%;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 54%, transparent 88%);
  mask-image: linear-gradient(to bottom, #000 54%, transparent 88%);
}

/* Velo sobre la iridiscencia, bajo el contenido. Los pastel son claros (L>75%),
   así que la tinta oscura del texto conserva su contraste encima; el problema
   no es el contraste sino el ruido — cinco líneas de entradilla sobre un
   arcoíris en movimiento no se leen. El velo baja la saturación justo en la
   columna de texto y deja el resto del arcoíris intacto: un fondo blanco
   uniforme se habría llevado por delante el efecto entero.
   Va en `::after` para pintarse después del canvas sin z-index de por medio;
   `.hero__inner` sí lleva z-index 1 y queda por encima de los dos. */
.hero::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(100deg,
    rgba(247, 247, 250, .9) 0%,
    rgba(247, 247, 250, .74) 32%,
    rgba(247, 247, 250, .34) 54%,
    transparent 70%);
}

/* Apilado en una columna, el texto ocupa todo el ancho y un velo lateral ya no
   protege nada: pasa a ser vertical, denso arriba (titular y entradilla) y
   abierto abajo. */
@media (max-width: 900px) {
  .hero::after {
    background: linear-gradient(to bottom,
      rgba(247, 247, 250, .86) 0%,
      rgba(247, 247, 250, .7) 46%,
      rgba(247, 247, 250, .3) 78%,
      transparent 100%);
  }
}

/* 60/40. `align-items: start` (antes `end`) es lo que ancla la ficha al tope
   de la columna en vez de dejarla flotando al pie sin alinear con nada. */
.hero__inner {
  position: relative; z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 1.55fr) minmax(272px, .95fr);
  gap: clamp(var(--sp-6), 4.5vw, var(--sp-8));
  align-items: start;
}

.hero__available { display: inline-flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-4); }
.hero__available .mono { color: var(--muted); }
.hero__dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--live); box-shadow: 0 0 0 3px rgba(var(--accent2-rgb), .18);
}
@media (prefers-reduced-motion: no-preference) {
  .hero__dot { animation: pulse 2.4s var(--ease) infinite; }
  @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
}

/* Más chico que `--fs-display` a propósito. El titular en español ("Diseño
   experiencias que convierten y marcas que se recuerdan.") es ~20 caracteres
   más largo que el inglés: a 5.25rem cae en cuatro líneas y empuja la barra de
   métricas fuera de un viewport de 900px. A 4.4rem entra en tres y el hero
   cierra completo sin scroll. */
.hero__title {
  margin-top: var(--sp-4); max-width: 17ch;
  font-size: clamp(2.2rem, 1rem + 4.2vw, 4.4rem);
}
.hero__title em { font-style: italic; }
.hero__lede { margin-top: var(--sp-5); max-width: 46ch; }
.hero__ctas { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-top: var(--sp-6); }

/* Ficha de identidad. Sin retrato: lo que sostiene la columna derecha es la
   tipografía y las reglas, no una imagen. */
.hero__card {
  position: relative;
  display: grid; gap: var(--sp-5);
  margin-top: var(--sp-6);
  padding: var(--sp-5);
  /* Vidrio esmerilado sobre la lámina iridiscente: translúcido para que el
     arcoíris se adivine detrás, pero con desenfoque suficiente para que las
     tres disciplinas se lean. El valor va literal y no por token porque esta
     ficha solo existe dentro del hero.
     Sube de .62 a .74 desde que la ficha lleva maquetas encima: unas piezas
     blancas sobre un fondo casi transparente se leían como recortes flotando
     sin ficha debajo. Con .74 la ficha vuelve a ser el papel y las maquetas
     lo que está apoyado en él. */
  background: rgba(255, 255, 255, .74);
  border: 1px solid var(--hairline); border-radius: var(--radius);
  backdrop-filter: blur(10px);
}
/* El rol a la izquierda y el instrumental a la derecha, alineados por arriba:
   la etiqueta mono y el segmentado comparten línea base óptica y la ficha
   arranca con una fila de herramienta, no con un párrafo. */
.hero__cardHead {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--sp-3);
}
.hero__cardRow { display: grid; gap: var(--sp-1); }

/* ---- Instrumental de la ficha (decorativo) ---- */
.hero__cardTools { display: none; align-items: center; gap: var(--sp-2); flex: none; }
/* Cápsula segmentada. El borde exterior es la píldora; la separación entre
   segmentos la hace un `border-left` interno, así el ancho no depende de
   cuánto mida cada etiqueta. */
.hero__segment {
  display: flex; align-items: stretch;
  background: rgba(255, 255, 255, .82);
  border: 1px solid var(--hairline); border-radius: 999px;
  overflow: hidden;
}
.hero__segmentItem {
  display: flex; align-items: center; justify-content: center;
  padding: 5px var(--sp-3);
  font-size: .625rem; letter-spacing: .04em;
  color: var(--faint); white-space: nowrap;
  border-left: 1px solid var(--hairline);
}
.hero__segmentItem:first-child { border-left: 0; }
/* El segmento activo se marca con tinta, no con relleno: un chip lila macizo
   competiría con el botón primario que tiene justo a la izquierda. */
.hero__segmentItem.is-on { color: var(--accent); background: rgba(var(--accent-rgb), .09); }
.hero__segmentItem--icon { padding-inline: var(--sp-2); color: var(--faint); }
.hero__segmentItem--icon svg { width: 12px; height: 12px; }
.hero__toolBtn {
  display: grid; place-items: center; flex: none;
  width: 26px; height: 26px; border-radius: 50%;
  background: rgba(255, 255, 255, .82);
  border: 1px solid var(--hairline);
  color: var(--faint);
}
.hero__toolBtn svg { width: 13px; height: 13px; }

/* ---- Clúster de maquetas ----
   Va anclado al cuerpo (la lista) y no a la ficha: la nota de abajo cambia de
   alto entre ES y EN, y si el clúster se centrara contra la ficha entera
   bailaría al cambiar de idioma. `right` negativo saca las piezas del
   acolchado y del borde para que se apoyen SOBRE el canto derecho — es lo que
   las hace leer como capas encima de la ficha y no como contenido dentro. */
.hero__cardBody { position: relative; }
.hero__mock {
  position: absolute; top: 50%; right: calc(-1 * var(--sp-5) - 18px);
  translate: 0 -50%;
  width: 152px; height: 118px;
  pointer-events: none;
  display: none;
}
.hero__mockWindow,
.hero__mockBadge,
.hero__mockChart {
  position: absolute;
  border-radius: 8px;
  /* Sombra corta y baja: las piezas están apoyadas sobre la ficha, a un par de
     milímetros, no flotando a media altura. */
  box-shadow: 0 6px 16px rgba(var(--ink-rgb), .1), 0 1px 2px rgba(var(--ink-rgb), .06);
}
.hero__mockWindow {
  top: 0; right: 22px;
  width: 104px; padding: 9px;
  display: grid; gap: 6px;
  background: var(--surface);
  border: 1px solid var(--hairline);
}
.hero__mockBar {
  height: 26px; border-radius: 4px;
  background: linear-gradient(120deg,
    rgba(var(--accent-rgb), .16), rgba(var(--iris-sky), .28));
}
.hero__mockLine { height: 4px; border-radius: 999px; background: rgba(var(--ink-rgb), .14); }
.hero__mockLine--short { width: 58%; }
.hero__mockBadge {
  top: 54px; right: 84px;
  display: grid; place-items: center;
  width: 34px; height: 34px;
  background: var(--accent); color: var(--accent-ink);
}
.hero__mockBadge svg { width: 18px; height: 18px; }
.hero__mockChart {
  bottom: 0; right: 0;
  width: 78px; padding: 9px 10px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  color: var(--accent2);
}
.hero__mockChart svg { width: 100%; height: auto; }
/* Las barras son el dato en bruto y la línea la lectura: por eso las barras
   van en verde lavado y sólo la tendencia lleva el verde pleno. */
.hero__mockBars rect { fill: rgba(var(--accent2-rgb), .22); }

/* Deriva mínima (5px, 7s) y desfasada: tres piezas subiendo a la vez serían un
   bloque moviéndose; desfasadas se leen como capas sueltas apoyadas sobre la
   ficha. El recorrido es corto a propósito — esto acompaña al canvas
   iridiscente, no compite con él. */
@media (prefers-reduced-motion: no-preference) {
  .hero__mockWindow { animation: mockFloat 7s var(--ease) infinite; }
  .hero__mockBadge { animation: mockFloat 7s var(--ease) -2.3s infinite; }
  .hero__mockChart { animation: mockFloat 7s var(--ease) -4.6s infinite; }
  @keyframes mockFloat { 0%, 100% { translate: 0 0; } 50% { translate: 0 -5px; } }
}

.hero__cardLabel { color: var(--faint); }
/* Alto reservado y cifras de ancho fijo: el rol se re-escribe carácter por
   carácter (initScramble), y sin esto cada fotograma cambiaría la altura de la
   ficha entera. */
.hero__cardRole {
  font-family: var(--font-mono); font-size: .8125rem;
  color: var(--accent); min-height: 1.5em;
}
.hero__disciplines { display: grid; }
.hero__disciplines li {
  padding-block: var(--sp-3);
  border-top: 1px solid var(--hairline);
  font-size: var(--fs-small); color: var(--muted);
}
.hero__disciplines li:first-child { border-top: 0; padding-top: 0; }
.hero__cardNote {
  padding-top: var(--sp-4); border-top: 1px solid var(--hairline);
  color: var(--faint); letter-spacing: .06em; line-height: 1.75;
}

/* Por debajo de 560px la ficha se estrecha: el clúster taparía las filas y el
   segmentado no cabe en la misma línea que el rol. Ahí desaparecen los dos y
   la ficha vuelve a ser puramente tipográfica —el estado que tenía antes—.
   Cuando sí están, la lista reserva su carril derecho para que ninguna
   disciplina larga —"Investigación y validación"— llegue a pasar por debajo de
   las maquetas. */
@media (min-width: 560px) {
  .hero__cardTools { display: flex; }
  .hero__mock { display: block; }
  .hero__disciplines li { padding-right: 124px; }
}

/* Barra de métricas: `margin-top: auto` la empuja al pie del hero pase lo que
   pase con el alto del texto, así la barra siempre cierra la composición. */
.hero__metrics {
  position: relative; z-index: 1;
  margin-top: auto; padding-top: var(--sp-5);
  border-top: 1px solid var(--hairline);
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-5);
}
.hero__metric { display: grid; gap: var(--sp-2); align-content: start; }
.hero__metricNum {
  font-family: var(--font-display);
  font-size: clamp(2rem, 3.4vw, 3.15rem);
  line-height: .9; letter-spacing: -.04em; color: var(--accent2);
  font-variant-numeric: tabular-nums;
}
.hero__metricLabel { color: var(--faint); max-width: 20ch; line-height: 1.6; }

@media (max-width: 900px) {
  .hero__inner { grid-template-columns: 1fr; }
  .hero__card { max-width: 420px; }
  .hero__metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-5) var(--sp-4); }
}
@media (max-width: 460px) {
  .hero__metrics { grid-template-columns: 1fr 1fr; }
}

/* ---------- Encabezado partido (Work · Toolkit · Process) ---------- */
/* El titular ocupa su propia columna y el kicker + lede se van a la derecha:
   deja de haber un bloque centrado idéntico al abrir cada sección. */
.split {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  gap: clamp(var(--sp-5), 4vw, var(--sp-8));
  align-items: end;
  padding-bottom: var(--sp-5);
  margin-bottom: var(--sp-8);
  border-bottom: 1px solid var(--hairline);
}
.split__title { max-width: 20ch; }
.split__aside { display: grid; gap: var(--sp-3); }
.split__aside p { color: var(--muted); font-size: var(--fs-small); max-width: 46ch; }

@media (max-width: 820px) {
  .split { grid-template-columns: 1fr; align-items: start; gap: var(--sp-5); }
}

/* ---------- 8b. Riel — carrusel horizontal compartido ---------- */
/* Proof y Work usan el mismo riel. No es un motor de diapositivas: es scroll
   horizontal nativo con anclaje (`scroll-snap`), y las flechas sólo empujan
   `scrollLeft`. Eso deja gratis el arrastre táctil, la rueda del ratón, el
   teclado y el scroll que el navegador hace solo cuando enfocas un botón que
   está fuera de vista — y hace imposible que un "slide actual" se
   desincronice al filtrar o al repintar en otro idioma, porque no existe. */
.rail__track {
  display: flex;
  gap: var(--sp-4);
  overflow-x: auto;
  /* Explícito, no heredado: la entrada de `[data-reveal]` desplaza la ficha
     12px hacia abajo y ese desplazamiento cuenta como desbordamiento, así que
     sin esto el riel se volvería scrollable en vertical durante la animación. */
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  /* El relleno vertical es para la sombra del hover: sin él, `overflow-x`
     recorta por arriba y por abajo lo que la ficha proyecta al levantarse. */
  padding-block: 6px var(--sp-4);
  /* Barra de scroll oculta: la sustituye `.rail__progress`, que ocupa 2px en
     vez de 12 y no cambia de aspecto entre sistemas. */
  scrollbar-width: none;
}
.rail__track::-webkit-scrollbar { display: none; }
.rail__track > * { scroll-snap-align: start; }

/* Las flechas flanquean las fichas, a media altura, montadas sobre el canto:
   19px de los 38 caen fuera de la pista y 19 encima. Ese medio botón fuera es
   lo máximo que cabe sin desbordar — la calle (`--gutter`) nunca baja de 20px.
   `pointer-events` se apaga en la barra y se reenciende en los botones: la
   barra cruza las fichas de lado a lado y si no, se comería los clicks del
   centro, que es justo donde está el disparador del modal. */
.rail__viewport { position: relative; }
.rail__nav {
  position: absolute; z-index: 2;
  inset-inline: -19px;
  /* La pista lleva 6px de relleno arriba y 16 abajo (sitio para la sombra del
     hover), así que su centro geométrico cae 5px por debajo del centro óptico
     de las fichas. */
  top: 50%; translate: 0 calc(-50% - 5px);
  display: flex; justify-content: space-between;
  pointer-events: none;
}
.rail__btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px;
  pointer-events: auto;
  /* Opaco, no `--glass`: medio botón se apoya en la ficha y el texto de debajo
     no puede transparentarse a través de él. */
  color: var(--text); background: var(--surface);
  border: 1px solid var(--hairline); border-radius: 50%;
  box-shadow: 0 8px 20px -10px rgba(var(--ink-rgb), .5);
}
.rail__btn:hover:not(:disabled) {
  color: var(--accent-ink); background: var(--accent); border-color: var(--accent);
}
/* Deshabilitado y no oculto: un botón que desaparece en el extremo desplaza
   al otro y hace bailar la fila entera. */
.rail__btn:disabled { color: var(--faint); opacity: .4; cursor: default; }

.rail__progress {
  position: relative; height: 2px;
  background: var(--hairline); border-radius: 2px;
}
.rail__thumb {
  position: absolute; inset-block: 0; left: 0;
  display: block; width: 30%;
  background: var(--accent); border-radius: 2px;
}

@media (prefers-reduced-motion: no-preference) {
  .rail__btn { transition: background .25s var(--ease), color .25s var(--ease), border-color .25s var(--ease); }
}
/* En táctil el gesto es arrastrar: dos botones de 38px sólo competirían por el
   ancho de la fila. La barra de posición se queda — es la que avisa de que hay
   más fichas a la derecha. */
@media (max-width: 640px) {
  .rail__nav { display: none; }
}

/* ---------- 9. Proof — fichas de medición ---------- */
/* Una ficha por resultado, no una fila. La tabla anterior repartía cuatro
   columnas por seis filas: veinticuatro celdas de peso visual idéntico donde
   lo único que hay que retener son seis cifras. En ficha la jerarquía se
   resuelve sola y en cuatro líneas: la cifra manda, el concepto la nombra, el
   instrumento la respalda y el caso cierra el pie como acción.
   El orden es una escala de cuerpo descendente — 41px, 17px, 11px — que es
   también el orden en que alguien pregunta: cuánto, de qué, medido con qué. */
.ledger { padding-block: clamp(var(--sp-8), 8vh, var(--sp-9)); }

.ledger__head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-4);
  padding-bottom: var(--sp-4); margin-bottom: var(--sp-5);
  border-bottom: 2px solid var(--text);
}
.ledger__note { color: var(--faint); margin-left: auto; }

/* Tres fichas por vista y un asomo de la cuarta: el recorte parcial es lo que
   dice que el riel sigue. Con el ancho cerrado en `clamp`, una ficha nunca se
   estrecha por debajo de lo que necesita su cifra ni se estira hasta parecer
   un banner en pantallas grandes. */
.ledger__track > .ledger__card { flex: 0 0 clamp(15rem, 30%, 24rem); }

/* `grid-template-areas` en vez de orden del DOM: el <dl> exige dt antes de sus
   dd, pero en pantalla la cifra va primero. Las áreas separan las dos cosas.
   Una sola columna y `1fr` en la fila del instrumento: la holgura se acumula
   ahí, así el pie queda pegado al fondo y las seis fichas cierran a la misma
   altura aunque un concepto ocupe dos líneas. */
.ledger__card {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto auto 1fr auto;
  grid-template-areas: "value" "concept" "spine" "case";
  gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-5);
  background: var(--glass);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}

/* El instrumento se leía en vertical por el canto izquierdo, y era bonito pero
   caro: veinticinco caracteres girados exigen 200px de alto, así que la ficha
   no podía medir menos que su etiqueta más larga. Ahora va horizontal bajo el
   concepto, con un filete corto a la izquierda que lo marca como anotación al
   margen — sigue diciendo "medido con qué" sin decidir la altura de la ficha. */
.ledger__method {
  grid-area: spine; align-self: start;
  padding-left: var(--sp-3);
  border-left: 1px solid var(--hairline);
  color: var(--faint);
  font-size: var(--fs-mono); letter-spacing: .1em; text-transform: uppercase;
  line-height: 1.4;
}

.ledger__value {
  grid-area: value; align-self: start;
  font-family: var(--font-display);
  font-size: clamp(1.9rem, 1.2rem + 1.9vw, 2.6rem);
  line-height: 1; letter-spacing: -.03em;
  color: var(--accent2); font-variant-numeric: tabular-nums;
}
/* Línea base y unidad viven dentro de la cifra, a menor cuerpo: "51 → 84/100"
   tiene que leerse como un dato, no como cuatro cifras compitiendo. */
.ledger__from {
  font-size: .4em; color: var(--faint);
  letter-spacing: 0; margin-right: .3em;
  font-variant-numeric: tabular-nums;
}
.ledger__unit { font-size: .48em; color: rgba(var(--accent2-rgb), .72); }

.ledger__concept {
  grid-area: concept; align-self: start;
  color: var(--text); font-weight: 500; line-height: 1.35;
  text-wrap: balance;
}

/* El caso deja de ser un enlace subrayado y pasa a ser el pie accionable de la
   ficha: ocupa todo el ancho y se separa del cuerpo por filete. Es la única
   superficie táctil de la tarjeta, así que tiene que verse como tal. */
.ledger__source { grid-area: case; align-self: end; }
.ledger__case {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  width: 100%; padding-top: var(--sp-3);
  text-align: left; color: var(--accent);
  border-top: 1px solid var(--hairline);
  font-size: var(--fs-mono); letter-spacing: .1em; text-transform: uppercase;
}
.ledger__caseArrow { color: var(--faint); }

.ledger__card:hover,
.ledger__card:focus-within {
  border-color: rgba(var(--accent-rgb), .38);
  box-shadow: 0 14px 32px -20px rgba(var(--ink-rgb), .45);
}
.ledger__card:hover .ledger__method,
.ledger__card:focus-within .ledger__method { color: var(--accent); }
.ledger__card:hover .ledger__caseArrow,
.ledger__card:focus-within .ledger__caseArrow { color: var(--accent); translate: 2px -2px; }
.ledger__card:hover .ledger__case,
.ledger__card:focus-within .ledger__case { border-top-color: rgba(var(--accent-rgb), .4); }

@media (prefers-reduced-motion: no-preference) {
  .ledger__card { transition: border-color .25s var(--ease), box-shadow .25s var(--ease); }
  .ledger__method { transition: color .25s var(--ease); }
  .ledger__case { transition: border-color .25s var(--ease); }
  .ledger__caseArrow { transition: color .25s var(--ease), translate .25s var(--ease); }
}

/* En pantalla estrecha cabe una ficha por vista, con asomo de la siguiente. */
@media (max-width: 560px) {
  .ledger__track > .ledger__card { flex-basis: 82%; }
}

/* ---------- 10. Work — riel de fichas ---------- */
/* Los filtros dejan de ser botonera: línea de texto conmutable para que no
   compita con las fichas que van justo debajo. */
.work__filters {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2) var(--sp-4);
  margin-bottom: var(--sp-5);
}
.work__filter {
  font-family: var(--font-mono); font-size: var(--fs-mono);
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--faint);
  padding-bottom: 2px; border-bottom: 1px solid transparent;
}
.work__filter:hover { color: var(--text); }
.work__filter[aria-pressed="true"] { color: var(--accent); border-bottom-color: var(--accent); }

/* Fichas en una sola fila recorrible. El índice tabular anterior escondía la
   descripción hasta el hover para mantenerse escaneable — un compromiso que en
   táctil no existía y que dejaba seis títulos sin contexto. En ficha cabe todo
   a la vez: categoría, título, qué se hizo y con qué resultado.
   La marca de cada ficha es el filete de acento del borde superior: 34px en
   reposo, ancho completo al apuntarla. Señala cuál está activa sin mover ni
   teñir nada más. */
.cases > .case { flex: 0 0 clamp(17.5rem, 47%, 34rem); }
/* `isolation: isolate` no es decorativo: la ficha ya no es transparente como
   la fila anterior, y sin contexto de apilamiento propio el ::after de
   `.case__open` —que es el área de click de toda la ficha, con z-index
   negativo para no tapar el texto seleccionable— quedaría pintado por debajo
   del fondo blanco y dejaría de recibir el click. */
.case {
  position: relative; isolation: isolate;
  display: flex; flex-direction: column; gap: var(--sp-3);
  padding: var(--sp-6) var(--sp-5) var(--sp-5);
  background: var(--glass);
  border: 1px solid var(--hairline); border-radius: var(--radius);
  overflow: hidden;
}
.case[hidden] { display: none; }
.case::before {
  content: ""; position: absolute; top: 0; left: 0;
  width: 34px; height: 2px; background: var(--accent);
}
.case:hover::before, .case:focus-within::before { width: 100%; }

.case__meta {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: var(--sp-2) var(--sp-4);
}
.case__cat { color: var(--accent); }
.case__lock { color: var(--faint); }
.case__title { font-size: var(--fs-h3); line-height: 1.2; text-wrap: balance; }
.case__tagline {
  color: var(--muted); font-size: var(--fs-small);
  max-width: 46ch;
}

/* Pie de la ficha: el resultado a la izquierda y el disparador del modal a la
   derecha, separados del cuerpo por filete. `margin-top: auto` los ancla abajo
   para que las fichas de una misma fila cierren a la misma altura aunque los
   títulos ocupen distinto número de líneas. */
.case__end {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  margin-top: auto; padding-top: var(--sp-4);
  border-top: 1px solid var(--hairline);
}
.case__impact {
  font-family: var(--font-display); font-size: 1.2rem; line-height: 1.25;
  color: var(--accent2); font-variant-numeric: tabular-nums;
}
.case__open {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; flex: none;
  border: 1px solid var(--hairline); border-radius: 50%;
  color: var(--faint);
}
/* Área de click a toda la ficha. `z-index` negativo lo deja por DEBAJO del
   texto para que el usuario aún pueda seleccionarlo. */
.case__open::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
}
.case:hover, .case:focus-within {
  border-color: rgba(var(--accent-rgb), .38);
  box-shadow: 0 16px 36px -22px rgba(var(--ink-rgb), .5);
}
.case:hover .case__open,
.case:focus-within .case__open {
  color: var(--accent-ink); background: var(--accent); border-color: var(--accent);
}

@media (prefers-reduced-motion: no-preference) {
  .case { transition: border-color .3s var(--ease), box-shadow .3s var(--ease); }
  .case::before { transition: width .45s var(--ease); }
  .case__open { transition: background .3s var(--ease), color .3s var(--ease), border-color .3s var(--ease); }
}
@media (max-width: 640px) {
  .cases > .case { flex-basis: 86%; }
}

/* ---------- 11. Toolkit — ficha técnica ---------- */
/* Cuatro fichas de cabecera: los logos van arriba y a mayor tamaño porque son
   lo que se reconoce antes de leer, el trío de nombres hace de título y la
   frase explica para qué se usa. */
.kit__lead {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  gap: var(--sp-4);
  margin-bottom: var(--sp-8);
}
.kit__leadRow {
  display: grid; align-content: start; gap: var(--sp-3);
  padding: var(--sp-5);
  background: var(--glass);
  border: 1px solid var(--hairline); border-radius: var(--radius);
}
.kit__leadTerm {
  display: grid; gap: var(--sp-3);
  font-family: var(--font-mono); font-size: .8125rem; letter-spacing: .04em;
  color: var(--text);
}
.kit__logos { display: flex; align-items: center; gap: var(--sp-3); min-height: 22px; }
.kit__logos img { width: 22px; height: 22px; opacity: .9; }
.kit__leadRow dd { color: var(--muted); font-size: var(--fs-small); }

/* Seis fichas de inventario en riel horizontal: tres por vista y el resto a un
   empujón. Hereda todo el comportamiento de `.rail__track` (anclaje, arrastre,
   flechas, barra de posición); aquí sólo se declara el ancho de ficha.
   El 30% es el mismo reparto que el riel de resultados: tres fichas por vista
   dejando ver el canto de la cuarta, que es lo que avisa de que hay más. */
.kit__groups > .kit__group { flex: 0 0 clamp(16rem, 30%, 26rem); }
/* Todas a la misma altura: es el `stretch` por defecto del riel flex, no una
   altura fija. Un `height` en píxeles se rompería al traducir al inglés —donde
   varias etiquetas ocupan una línea más— y aquí la más alta manda siempre.
   `justify-content` no hace falta: la holgura sobrante cae al fondo de la
   ficha, que es donde no molesta. */
.kit__group {
  display: flex; flex-direction: column;
  padding: var(--sp-5);
  background: var(--glass);
  border: 1px solid var(--hairline); border-radius: var(--radius);
  scroll-margin-top: calc(var(--nav-h) + var(--sp-6));
}
/* El punto de acento es el mismo nodo que marca las fases del proceso: dos
   secciones distintas, el mismo signo para "aquí empieza un bloque". */
.kit__groupTitle {
  display: flex; align-items: baseline; gap: var(--sp-3);
  font-family: var(--font-display); font-size: 1.15rem; letter-spacing: -.01em;
  color: var(--text);
  padding-bottom: var(--sp-3); margin-bottom: var(--sp-4);
  border-bottom: 1px solid var(--hairline);
}
.kit__groupTitle::before {
  content: ""; flex: none;
  width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
}
.kit__tools { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
/* Etiquetas sin filete y con fondo: dentro de una ficha, un borde más sería
   una caja dentro de otra caja. El relleno basta para separarlas.
   El fondo es lila al 7%, no el gris `--raised`: sobre una ficha translúcida un
   gris neutro es lo único de la página que no participa del film — se lee como
   un parche apagado. Teñirlo del acento lo devuelve al sistema. */
.tool {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: 6px var(--sp-3);
  font-size: var(--fs-small); line-height: 1.3; color: var(--muted);
  background: rgba(var(--accent-rgb), .07); border-radius: var(--radius);
}
.tool__logo { width: 16px; height: 16px; flex: none; opacity: .85; }
.tool:hover { color: var(--text); background: rgba(var(--accent-rgb), .14); }

@media (prefers-reduced-motion: no-preference) {
  .tool { transition: background .2s var(--ease), color .2s var(--ease); }
}

/* ---------- Banda de embudo (sangra de borde a borde) ---------- */
.funnel {
  margin-block: var(--sp-8);
  padding-block: var(--sp-7);
  border-top: 1px solid var(--hairline); border-bottom: 1px solid var(--hairline);
  /* Translúcida, no `--raised` opaco: la banda sangra de borde a borde, y en
     gris sólido cortaría el film en dos justo por la mitad de la página. Así
     se lee como un tramo más denso del mismo material. */
  background:
    radial-gradient(900px circle at 12% 0%, rgba(var(--accent-rgb), .10), transparent 62%),
    rgba(255, 255, 255, .5);
}
.funnel__inner {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-6) var(--sp-8);
}
.funnel__head { flex: 1 1 26ch; display: grid; gap: var(--sp-3); }
.funnel__kicker { color: var(--accent); }
.funnel__claim { font-family: var(--font-display); font-size: var(--fs-h3); line-height: 1.35; max-width: 34ch; }
.funnel__flow { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); }
.funnel__step {
  display: inline-flex; align-items: baseline; gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  background: var(--bg);
  border: 1px solid var(--hairline); border-radius: var(--radius);
  font-size: var(--fs-small);
}
.funnel__num { color: var(--accent); font-variant-numeric: tabular-nums; }

/* ---------- 12. Proceso — línea de tiempo ---------- */
.timeline { margin-bottom: var(--sp-9); }
.timeline__meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-4);
  padding-bottom: var(--sp-4); border-bottom: 1px solid var(--hairline);
}
.timeline__label { color: var(--accent); }
.timeline__live { display: inline-flex; align-items: center; gap: var(--sp-2); color: var(--faint); margin-left: auto; }
.timeline__liveDot {
  width: 6px; height: 6px; border-radius: 50%; background: var(--live);
  box-shadow: 0 0 0 3px rgba(var(--accent2-rgb), .18);
}
@media (prefers-reduced-motion: no-preference) {
  .timeline__liveDot { animation: pulse 2.4s var(--ease) infinite; }
}

/* La línea de tiempo corre en horizontal, no en vertical. En vertical los siete
   títulos —de una a tres palabras cada uno— dejaban tres cuartos del ancho en
   blanco y la sección medía una pantalla entera para decir siete palabras.
   En horizontal el eje del tiempo coincide con el eje de lectura, que es lo que
   un proceso quiere decir, y la sección cabe de una sola vista.
   El conector se dibuja POR FASE y llega hasta el borde de su celda (sin hueco
   entre columnas), no como una línea del contenedor: así sobrevive al salto de
   fila cuando la retícula pasa de siete columnas a cuatro o a dos. */
.timeline__phases {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  margin-top: var(--sp-5);
}
.phase {
  position: relative;
  padding: 42px var(--sp-4) var(--sp-2) 0;
}
/* El nodo va sobre el conector y es opaco: la línea entra por un lado y sale
   por el otro sin cruzarlo. */
.phase::before {
  content: ""; position: absolute; left: 0; top: 24px; z-index: 1;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--surface); border: 1px solid rgba(var(--ink-rgb), .38);
}
.phase::after {
  content: ""; position: absolute; left: 0; right: 0; top: 28px;
  height: 1px; background: var(--hairline);
}
/* Fin de fila: el conector no puede apuntar a un vacío. Una regla por
   disposición, porque CSS no sabe qué elemento cierra cada fila. */
.phase:last-child::after { display: none; }
.phase--core::before { background: var(--accent); border-color: var(--accent); }
.phase__num {
  position: absolute; left: 0; top: 0;
  color: var(--faint); font-variant-numeric: tabular-nums;
}
.phase__body { display: grid; gap: var(--sp-2); }
.phase__title { font-size: 1.0625rem; line-height: 1.25; text-wrap: balance; }
.phase--core .phase__title { color: var(--accent); }
.phase__sub { color: var(--faint); letter-spacing: .06em; }
/* El bucle de iteración es el argumento de la sección: vuelve a la fase 04 en
   vez de seguir de largo, así que se marca en vez de dibujarse como un paso
   más de la secuencia. */
.phase__loop {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  justify-self: start; margin-top: var(--sp-1);
  padding: var(--sp-1) var(--sp-2);
  color: var(--accent); letter-spacing: .06em;
  border: 1px dashed rgba(var(--accent-rgb), .5); border-radius: var(--radius);
}

/* ---- La arista de retorno 06 → 04 ----
   Una etiqueta que dice "iterar 04" pide al lector que reconstruya el salto de
   memoria; la arista lo enseña. Se dibuja con bordes y no con SVG porque su
   ancho es exactamente DOS COLUMNAS de la retícula: con siete columnas de 1fr,
   `left: -200%` sobre la celda 06 cae clavado en el nodo de la 04 y sigue
   cuadrando aunque el contenedor cambie de ancho — un SVG habría que
   re-escalarlo a mano en cada breakpoint.
   Cuelga POR DEBAJO de la fila (no cruza el eje) por una razón de lectura: los
   dos montantes tendrían que atravesar el título de su propia fase para tocar
   los nodos, y una línea encima del texto se lee como tachado, no como
   diagrama. Baja, cruza por el hueco libre y apunta hacia arriba: la punta de
   flecha es la que dice a qué columna vuelve. */
.phase__retro {
  position: absolute;
  /* El arranque va en % del alto de la fila y no en px: entre 1081 y ~1300px
     las columnas son estrechas y los títulos pasan a dos y tres líneas. Un
     `top` fijo funcionaba a 1920 y cruzaba el título de la 04 a 1200; el 78%
     queda siempre por debajo del texto porque la fila la marca la 06, que es
     la fase más alta del esquema. */
  top: 78%; bottom: -26px;
  left: -200%; width: calc(200% - 12px);
  border-left: 1px dashed rgba(var(--accent-rgb), .45);
  border-bottom: 1px dashed rgba(var(--accent-rgb), .45);
  border-bottom-left-radius: 12px;
  pointer-events: none;
}
/* Codo derecho: los últimos 12px del recorrido, donde la línea gira hacia
   arriba para subir a la 06. Va aparte porque su montante mide 34px y no toda
   la altura del elemento — si el borde derecho fuese del mismo cuadro,
   subiría hasta el título de la 06 y lo cruzaría. */
.phase__retro::before {
  content: ""; position: absolute;
  left: 100%; bottom: -1px;
  width: 12px; height: 34px;
  border-right: 1px dashed rgba(var(--accent-rgb), .45);
  border-bottom: 1px dashed rgba(var(--accent-rgb), .45);
  border-bottom-right-radius: 12px;
}
/* Punta de flecha maciza, no una segunda raya: es el único trazo del diagrama
   que indica DIRECCIÓN, y a 8px una punta calada no se distingue de un guion
   más de la línea discontinua. */
.phase__retro::after {
  content: ""; position: absolute; top: -6px; left: -4px;
  width: 0; height: 0;
  border-left: 4px solid transparent; border-right: 4px solid transparent;
  border-bottom: 6px solid var(--accent);
}
/* Con la retícula de siete columnas la arista mide dos columnas exactas. En
   cuanto la retícula se parte (4+3 y 2+2+2+1) la 04 y la 06 dejan de estar en
   la misma fila y "dos columnas a la izquierda" apunta a otra fase: ahí la
   arista se retira y la etiqueta "↺ iterar 04" vuelve a ser la única marca del
   bucle, que es como funcionaba el esquema antes de existir esta línea. */
@media (max-width: 1080px) {
  .phase__retro { display: none; }
}
@media (min-width: 1081px) {
  /* Lo que la arista ocupa por debajo de la fila, devuelto como margen: sin
     esto la curva aterriza sobre el zócalo de medición. */
  .timeline__phases { margin-bottom: 26px; }
}

/* La medición no es la fase 08: corre por debajo de las siete, así que se
   dibuja como zócalo del esquema. */
.timeline__base {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3) var(--sp-5);
  margin-top: var(--sp-6); padding-top: var(--sp-4);
  border-top: 1px dashed var(--hairline);
}
.timeline__baseLabel { color: var(--accent); }
.timeline__baseItems { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); }
.timeline__baseItems li { color: var(--faint); }

/* Siete columnas no se reparten en partes iguales, así que los cortes están
   elegidos para que la última fila no quede casi vacía: 4+3 y 2+2+2+1. */
@media (max-width: 1080px) {
  .timeline__phases { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .phase:nth-child(4n)::after { display: none; }
}
@media (max-width: 620px) {
  .timeline__phases { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .phase:nth-child(4n)::after { display: block; }
  .phase:nth-child(2n)::after { display: none; }
}

/* Entrada del esquema: el proceso se DIBUJA en orden, no aparece de golpe.
   Una sección que afirma "estas siete fases se ejecutan en secuencia" y las
   revela las siete a la vez está contradiciéndose con el movimiento.
   El índice de cada fase vive en CSS y no como `style="--i"` en el HTML porque
   el orden ya lo da el DOM: `nth-child` no puede desincronizarse del marcado. */
.phase:nth-child(1) { --i: 0; }
.phase:nth-child(2) { --i: 1; }
.phase:nth-child(3) { --i: 2; }
.phase:nth-child(4) { --i: 3; }
.phase:nth-child(5) { --i: 4; }
.phase:nth-child(6) { --i: 5; }
.phase:nth-child(7) { --i: 6; }

@media (prefers-reduced-motion: no-preference) {
  /* El observado es el <ol>, no cada <li>: las siete fases comparten fila y
     entran en el viewport en el mismo fotograma, así que observarlas por
     separado daría siete revelados simultáneos —justo lo contrario de una
     secuencia—. Con un solo disparo, el escalonado lo pone el retardo.
     Esto anula el fundido genérico de `[data-reveal]` sobre el <ol> (que
     movería el bloque entero y taparía el trazado de la línea); gana por
     especificidad, no por orden. */
  [data-js="true"] .timeline__phases[data-reveal] {
    opacity: 1; translate: none; transition: none;
  }

  /* Estado de partida. Va bajo `[data-js="true"]` como el resto del sistema de
     reveal: si el JS no llega a marcar el <html>, nada queda oculto. */
  [data-js="true"] .timeline__phases[data-reveal] .phase::after {
    transform: scaleX(0); transform-origin: left center;
  }
  [data-js="true"] .timeline__phases[data-reveal] .phase::before { scale: 0; opacity: 0; }
  [data-js="true"] .timeline__phases[data-reveal] .phase__num,
  [data-js="true"] .timeline__phases[data-reveal] .phase__body { opacity: 0; translate: 0 10px; }
  /* La arista de retorno entra la última, cuando las siete fases ya están
     dibujadas: es una lectura SOBRE la secuencia —"y de aquí se vuelve
     allá"— y sólo se entiende cuando existe la secuencia sobre la que
     volver. */
  [data-js="true"] .timeline__phases[data-reveal] .phase__retro { opacity: 0; }

  /* 150ms por fase: por debajo de ~110 las siete se solapan y el escalonado
     deja de leerse como orden; por encima de ~180 la última tarda más de un
     segundo y medio y el usuario ya ha seguido leyendo.
     Longhands y no el atajo: el retardo escalonado sólo puede afectar a las
     propiedades de la ENTRADA. Con un `transition-delay` único, pasar el ratón
     por la fase 07 tardaría 900ms en responder — el retardo de su entrada. */
  .phase::before {
    transition-property: scale, opacity, background-color, border-color;
    transition-duration: .45s, .3s, .25s, .25s;
    transition-timing-function: var(--ease-back), var(--ease), var(--ease), var(--ease);
    transition-delay: calc(var(--i, 0) * 150ms), calc(var(--i, 0) * 150ms), 0s, 0s;
  }
  .phase__num, .phase__body {
    transition: opacity .5s var(--ease), translate .5s var(--ease);
    transition-delay: calc(var(--i, 0) * 150ms + 80ms);
  }
  /* El conector sale DESPUÉS de su nodo: primero se planta la fase, después la
     línea viaja hacia la siguiente. Al revés la línea llegaría a un vacío.
     Longhands por el mismo motivo que en `::before`: el trazado de entrada va
     escalonado, pero el encendido del cursor tiene que ser inmediato. */
  .phase::after {
    transition-property: transform, background-color;
    transition-duration: .5s, .45s;
    transition-timing-function: var(--ease), var(--ease);
    transition-delay: calc(var(--i, 0) * 150ms + 150ms), 0s;
  }

  /* El prefijo `[data-js="true"]` no es decorativo: sin él estas reglas pesan
     menos que las de partida y el esquema no llegaría a revelarse nunca. */
  [data-js="true"] .timeline__phases[data-revealed="true"] .phase::before { scale: 1; opacity: 1; }
  [data-js="true"] .timeline__phases[data-revealed="true"] .phase::after { transform: scaleX(1); }
  [data-js="true"] .timeline__phases[data-revealed="true"] .phase__num,
  [data-js="true"] .timeline__phases[data-revealed="true"] .phase__body { opacity: 1; translate: none; }
  /* 1.2s = las siete fases (6 × 150ms) + el trazado del último conector. El
     `border-color` va en la misma lista SIN retardo para que el encendido del
     bucle siga respondiendo al instante una vez revelada. */
  [data-js="true"] .timeline__phases[data-revealed="true"] .phase__retro {
    opacity: 1;
    transition-property: opacity, border-color;
    transition-duration: .5s, .3s;
    transition-timing-function: var(--ease), var(--ease);
    transition-delay: 1.2s, 0s;
  }

  .phase__title { transition: color .25s var(--ease); }
}

/* ---------- Cursor automático del proceso ---------- */
/* Una fase se marca como "ahora" y el cursor avanza solo, con una vuelta atrás
   de 06 a 04 a mitad del recorrido. El orden lo lleva main.js
   (`initProcessCycle`); aquí sólo se dice cómo se ve la fase activa.
   Con `reduce` o sin JS no se activa `data-cycle` y todo esto queda inerte:
   la 04 sigue marcada en fijo por `.phase--core`, que es el estado que el
   esquema tenía antes de existir el cursor. */
.timeline__phases[data-cycle="on"] .phase--core::before {
  background: var(--surface); border-color: rgba(var(--ink-rgb), .38);
}
.timeline__phases[data-cycle="on"] .phase--core .phase__title { color: var(--text); }

.timeline__phases[data-cycle="on"] .phase[data-active="true"]::before {
  background: var(--accent); border-color: var(--accent);
}
.timeline__phases[data-cycle="on"] .phase[data-active="true"] .phase__title { color: var(--accent); }
/* El tramo que SALE de la fase activa se enciende: el cursor no sólo está en
   un punto, va hacia el siguiente. Es la diferencia entre marcar una casilla y
   leer un proceso en marcha. */
.timeline__phases[data-cycle="on"] .phase[data-active="true"]::after {
  background-color: var(--accent);
}

@media (prefers-reduced-motion: no-preference) {
  /* La aureola late alrededor del nodo activo: el mismo signo de "esto está
     vivo ahora" que el punto de "proyecto en curso" de la cabecera. */
  [data-js="true"] .timeline__phases[data-cycle="on"] .phase[data-active="true"]::before {
    animation: nodePulse 1.5s var(--ease) infinite;
  }
  @keyframes nodePulse {
    0% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), .45); }
    70%, 100% { box-shadow: 0 0 0 8px rgba(var(--accent-rgb), 0); }
  }

  /* El glifo ↺ gira exactamente cuando el cursor retrocede de 06 a 04, no en
     un temporizador propio: si girase por su cuenta ilustraría un salto que no
     está ocurriendo. */
  .phase__loop > span:first-child { display: inline-block; }
  .timeline__phases[data-loop="true"] .phase__loop > span:first-child {
    animation: loopSpin .8s var(--ease);
  }
  @keyframes loopSpin { from { transform: rotate(0); } to { transform: rotate(-360deg); } }
}

/* La marca del bucle se enciende en el salto. Fuera del bloque de motion: el
   cambio de color avisa igual con `reduce`, sólo que sin giro. */
.timeline__phases[data-loop="true"] .phase__loop {
  color: var(--accent-ink); background: var(--accent);
  border-style: solid; border-color: var(--accent);
}
/* La arista hace el mismo recorrido que el cursor, así que se enciende con él:
   de discontinua y lavada pasa a continua y lila justo cuando el cursor
   retrocede. Discontinua = "este retorno puede ocurrir"; continua = "está
   ocurriendo ahora". */
.timeline__phases[data-loop="true"] .phase__retro,
.timeline__phases[data-loop="true"] .phase__retro::before {
  border-style: solid; border-color: var(--accent);
}
@media (prefers-reduced-motion: no-preference) {
  .phase__loop { transition: background .3s var(--ease), color .3s var(--ease), border-color .3s var(--ease); }
  .phase__retro, .phase__retro::before { transition: border-color .3s var(--ease); }
}

/* El hover no es decorativo: con siete columnas estrechas, señalar cuál se
   está mirando ayuda a seguir la secuencia. Fuera del bloque de motion porque
   el cambio de color debe funcionar también con `reduce`. Se marcan el nodo y
   el título —los dos extremos de la fase—, no un fondo: la sección no tiene
   cajas y meter una sólo al pasar el ratón inventaría un componente. */
.phase:hover .phase__title { color: var(--accent); }
.phase:hover::before { background: var(--accent); border-color: var(--accent); }

/* ---------- Principios ---------- */
.principles {
  display: grid; gap: var(--sp-6);
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  padding-top: var(--sp-7); border-top: 1px solid var(--hairline);
}
.principle { display: grid; gap: var(--sp-3); align-content: start; }
.principle__glyph {
  width: 34px; height: 34px; overflow: visible;
}
.principle__glyph path, .principle__glyph rect, .principle__glyph circle {
  fill: none; stroke: var(--text); stroke-width: 1; stroke-linecap: square;
}
.principle__glyph .pg-dim { stroke: rgba(var(--ink-rgb), .3); }
.principle__glyph .pg-dash { stroke-dasharray: 3 3; }
.principle__glyph .pg-fill { fill: rgba(var(--accent-rgb), .16); stroke: none; }
.principle__glyph .pg-dot { fill: var(--accent); stroke: none; }
.principle__title { font-size: var(--fs-h3); text-wrap: balance; }
.principle p { color: var(--muted); font-size: var(--fs-small); }

/* ---------- 14. About — texto ancho, retrato estrecho ---------- */
.bio__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 21rem);
  gap: clamp(var(--sp-6), 5vw, var(--sp-9));
  align-items: start;
}
.bio__text { max-width: 54ch; }
.bio__body { margin: var(--sp-5) 0 var(--sp-6); }

/* El retrato arranca a la altura del texto —no montado sobre el borde de la
   sección—: con el desplazamiento negativo la cara quedaba un bloque entero
   por encima del titular y las dos columnas no se leían como una sola unidad.
   Sin filtro: la foto va en su color. Un blanco y negro sobre una página que
   se define por el lila y el verde deja el retrato fuera del sistema, no
   dentro. Lo que da unidad ahora es la sombra y el radio, no el desaturado. */
.bio__portrait img {
  width: 100%; height: auto; border-radius: var(--radius);
  box-shadow: 0 18px 44px rgba(var(--ink-rgb), .14);
}
.bio__caption {
  display: flex; align-items: center; gap: var(--sp-2);
  margin-top: var(--sp-4); color: var(--faint);
}
/* Filete de acento antes del pie: ata el retrato al mismo lenguaje de reglas
   de 1px que ordena el resto de la página. */
.bio__caption::before {
  content: ""; flex: 0 0 18px; height: 1px; background: var(--accent);
}

/* Los tres datos son la ficha técnica de la sección, así que se dibujan como
   una tabla de una fila: el hueco de 1px sobre el fondo de filete es lo que
   traza las divisiones internas, sin bordes por celda que se dupliquen. */
.bio__facts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1px;
  margin-top: var(--sp-6);
  background: var(--hairline);
  border: 1px solid var(--hairline); border-radius: var(--radius);
  overflow: hidden;
}
.bio__fact {
  display: grid; gap: var(--sp-3); align-content: start;
  padding: var(--sp-5);
  background: var(--glass);
}
.bio__fact dt { color: var(--accent); }
/* El dato en la display y a cuerpo mayor: es una cifra o una afirmación corta,
   no un pie de foto — antes competía hacia abajo con su propia etiqueta. */
.bio__fact dd {
  color: var(--text);
  font-family: var(--font-display); font-size: 1.0625rem; line-height: 1.35;
  text-wrap: balance;
}
.bio__fact:hover { background: var(--surface); }

@media (prefers-reduced-motion: no-preference) {
  .bio__fact { transition: background .25s var(--ease); }
}
@media (max-width: 820px) {
  .bio__inner { grid-template-columns: 1fr; }
  .bio__portrait { max-width: 320px; }
}

/* ---------- 15. Contact — dos columnas ---------- */
/* Mitades iguales y no 1fr/auto: el formulario tiene que ocupar exactamente la
   mitad derecha, no lo que le sobre al texto. `align-items: start` para que la
   ficha del formulario no se estire al alto de la columna izquierda. */
.reach__inner {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(var(--sp-6), 5vw, var(--sp-9));
  align-items: start;
}
.reach__aside { display: grid; gap: var(--sp-7); align-content: start; }
.reach__intro { display: grid; gap: var(--sp-4); }
.reach__intro h2 { max-width: 15ch; }
.reach__lede { color: var(--muted); max-width: 46ch; }

/* La ficha convierte el formulario en un objeto acotado dentro de la sección.
   Sin ella, cuatro líneas subrayadas sueltas en media página no se leen como
   un bloque: se leen como cuatro rayas. */
.form {
  display: grid; gap: var(--sp-5);
  padding: clamp(var(--sp-5), 2.5vw, var(--sp-7));
  background: var(--glass);
  border: 1px solid var(--hairline); border-radius: var(--radius);
  box-shadow: 0 18px 44px rgba(var(--ink-rgb), .08);
}
.form__pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-5); }
.form__row { display: grid; gap: var(--sp-2); }
.form__label { color: var(--faint); }
/* Campos de línea, no cajas: el formulario se lee como el pie de una carta.
   El borde inferior es el único que queda, así que el foco tiene que
   engrosarlo — un `outline` alrededor de una caja invisible no se entiende. */
.form__input {
  background: transparent; color: var(--text);
  border: 0; border-bottom: 1px solid rgba(var(--ink-rgb), .28);
  border-radius: 0; padding: var(--sp-3) 0;
}
.form__input:hover { border-bottom-color: rgba(var(--ink-rgb), .5); }
.form__input:focus { outline: none; }
.form__input:focus-visible { border-bottom-color: var(--accent); box-shadow: 0 1px 0 0 var(--accent); }
.form__textarea { resize: vertical; min-height: 104px; }
.form__submit { justify-self: start; }
.form__submit[disabled] { opacity: .55; cursor: progress; }
.form__status { font-size: var(--fs-small); min-height: 1.5em; }
.form__status[data-state="ok"] { color: var(--live); }
.form__status[data-state="error"] { color: var(--danger); }

/* Cada canal es una fila con etiqueta, valor y salida, separada por filete:
   se lee como una tabla de contacto y no como tres enlaces sueltos. Además
   dice a dónde lleva cada uno antes de pulsarlo, que es lo que una lista de
   nombres a secas —"GitHub", "Ver portafolio"— no dice. */
.reach__direct { display: grid; gap: var(--sp-4); }
.reach__directLabel { color: var(--faint); }
.reach__links { display: grid; border-top: 1px solid var(--hairline); }
.reach__links a {
  display: grid;
  grid-template-columns: minmax(0, 7.5rem) minmax(0, 1fr) auto;
  align-items: baseline; gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-2);
  border-bottom: 1px solid var(--hairline);
}
.reach__linkTag { color: var(--accent); }
.reach__linkValue {
  font-size: var(--fs-small); color: var(--muted);
  overflow-wrap: anywhere;
}
.reach__linkArrow { color: var(--faint); font-size: var(--fs-small); }
.reach__links a:hover { background: var(--glass); }
.reach__links a:hover .reach__linkValue { color: var(--accent); }
.reach__links a:hover .reach__linkArrow { color: var(--accent); translate: 3px 0; }

.reach__status { display: inline-flex; align-items: center; gap: var(--sp-2); color: var(--faint); }
.reach__statusDot {
  width: 6px; height: 6px; border-radius: 50%; background: var(--live);
  box-shadow: 0 0 0 3px rgba(var(--accent2-rgb), .18);
}

@media (prefers-reduced-motion: no-preference) {
  .reach__links a { transition: background .25s var(--ease); }
  .reach__linkValue { transition: color .2s var(--ease); }
  .reach__linkArrow { transition: color .2s var(--ease), translate .2s var(--ease); }
  .reach__statusDot { animation: pulse 2.4s var(--ease) infinite; }
}

/* Por debajo de ~900px la mitad derecha ya no cabe como formulario legible:
   dos columnas de 380px dejan los campos más estrechos que sus etiquetas. */
@media (max-width: 900px) {
  .reach__inner { grid-template-columns: 1fr; }
  .reach__intro h2 { max-width: 20ch; }
}
@media (max-width: 560px) {
  .form__pair { grid-template-columns: 1fr; }
  .reach__links a { grid-template-columns: 1fr auto; gap: var(--sp-1) var(--sp-3); }
  .reach__linkTag { grid-column: 1 / -1; }
}

/* ---------- Toasts ---------- */
/* Debajo del nav (top: --nav-h) para no taparlo; z-index 90 queda por encima
   del resto del contenido pero bajo cualquier overlay futuro a 100+. */
.toast-stack {
  position: fixed; top: calc(var(--nav-h) + var(--sp-4)); right: var(--sp-4);
  z-index: 90; display: grid; gap: var(--sp-2);
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  font-size: var(--fs-small);
  background: var(--surface); color: var(--text);
  border: 1px solid var(--hairline); border-left: 2px solid var(--accent);
  border-radius: var(--radius);
  padding: var(--sp-3) var(--sp-4);
  box-shadow: 0 10px 30px rgba(var(--ink-rgb), .16);
  max-width: min(340px, calc(100vw - 2 * var(--sp-4)));
}
.toast[data-state="ok"] { border-left-color: var(--live); }
.toast[data-state="error"] { border-left-color: var(--danger); }
.toast[data-leaving="true"] { opacity: 0; }

@media (prefers-reduced-motion: no-preference) {
  .toast {
    animation: toastIn .3s var(--ease);
    transition: opacity .25s var(--ease);
  }
  .form__input { transition: border-color .2s var(--ease), box-shadow .2s var(--ease); }
}
@keyframes toastIn {
  from { opacity: 0; transform: translateX(16px); }
}

/* ---------- 16. Footer ---------- */
.footer { border-top: 1px solid var(--hairline); padding-block: var(--sp-5); }
.footer__inner { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-3) var(--sp-5); }
.footer__name { font-family: var(--font-display); font-size: var(--fs-h3); }
.footer__legal { margin-left: auto; }
.footer .mono { color: var(--faint); }

@media (max-width: 520px) {
  .footer__legal { margin-left: 0; }
}
/* ---------- 17. Utilidades ---------- */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0;
  margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.skip-link {
  position: absolute; left: var(--sp-4); top: -100px; z-index: 100;
  background: var(--accent); color: var(--accent-ink);
  padding: var(--sp-3) var(--sp-4); border-radius: var(--radius); font-weight: 600;
}
.skip-link:focus { top: var(--sp-4); }

@media (prefers-reduced-motion: no-preference) {
  .skip-link { transition: top .2s var(--ease); }
}

/* ---------- 18. Modal ---------- */
.modal { position: fixed; inset: 0; z-index: 80; display: grid; place-items: center; padding: var(--sp-4); }
.modal[hidden] { display: none; }
.modal__backdrop { position: absolute; inset: 0; background: rgba(var(--ink-rgb), .42); backdrop-filter: blur(6px); }

.modal__panel {
  position: relative; z-index: 1;
  width: min(760px, 100%); max-height: 88vh; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--hairline); border-radius: var(--radius);
  box-shadow: 0 24px 70px rgba(var(--ink-rgb), .22);
  padding: clamp(var(--sp-6), 4vw, var(--sp-8));
}
.modal__close {
  position: sticky; top: 0; float: right; margin: calc(var(--sp-4) * -1) calc(var(--sp-4) * -1) 0 0;
  width: 38px; height: 38px; border: 1px solid var(--hairline); border-radius: 50%;
  color: var(--muted); display: grid; place-items: center;
  background: var(--surface);
}
.modal__close:hover { color: var(--text); border-color: var(--text); }

.modal__cat { color: var(--accent); }
.modal__title { margin: var(--sp-3) 0 var(--sp-2); font-size: var(--fs-h2); }
.modal__tagline { color: var(--muted); }

.modal__block { margin-top: var(--sp-6); padding-top: var(--sp-5); border-top: 1px solid var(--hairline); }
.modal__label { display: block; color: var(--faint); margin-bottom: var(--sp-3); }
.modal__block p { color: var(--muted); }

.modal__arch { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.modal__archStep {
  font-family: var(--font-mono); font-size: var(--fs-mono); letter-spacing: .1em;
  padding: 7px 11px; border: 1px solid var(--hairline); border-radius: 4px; color: var(--text);
}
.modal__archArrow { color: var(--accent); font-size: .75rem; }

.modal__chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
/* Antes reutilizaba `.card__chip`, de la tarjeta de caso que el índice tabular
   sustituyó. Ahora es del modal, que es su único consumidor. */
.modal__chip {
  font-family: var(--font-mono); font-size: var(--fs-mono); letter-spacing: .08em;
  padding: 5px 10px; border: 1px solid var(--hairline); border-radius: 999px;
  color: var(--muted);
}
.modal__impact { display: grid; gap: var(--sp-3); }
.modal__impact li { display: flex; gap: var(--sp-3); align-items: baseline; color: var(--text); }
.modal__impact li::before { content: "→"; color: var(--accent2); flex: none; }

@media (prefers-reduced-motion: no-preference) {
  .modal__close { transition: color .2s var(--ease), border-color .2s var(--ease); }
  .modal__backdrop { animation: fadeIn .24s var(--ease); }
  .modal__panel { animation: panelIn .24s var(--ease); }
  @keyframes fadeIn { from { opacity: 0; } }
  @keyframes panelIn { from { opacity: 0; transform: scale(.98); } }
}

body[data-modal-open="true"] { overflow: hidden; }
