/* ============================================================================
   styles.css — Link in bio · Jhei Trujillo
   ---------------------------------------------------------------------------
   Consume exclusivamente los tokens de tokens.css. Este archivo no contiene
   ni un hex ni un valor de espaciado literal, salvo las excepciones marcadas
   con la palabra EXCEPCION y su motivo.

   Versión sobria: la página es casi monocroma. El negro y el blanco hacen el
   trabajo; el fuego de AIVI aparece solo en el filo del retrato del hero, la
   baldosa de cada icono, el punto de cada chip, el punto de estado del hero,
   la palabra clave en degradado del reclamo del hero, el filete ornamental,
   el filo de las filas secundarias, el relleno completo de la fila
   destacada, las redes y las cards de colaboraciones, y el titular de esa
   misma sección.

   Índice
     1 · Reset y base
     2 · Utilidades
     3 · Fondo
     4 · Shell y ritmo
     5 · Hero
     6 · Fila de enlace con icono
     7 · Cintas en bucle y colaboraciones
     8 · Bio y redes
     9 · Footer
    10 · Capa de movimiento
    11 · Preferencias del usuario
   ========================================================================= */

/* ════════════════════════════════════════════════════════════════════════════
   1 · RESET Y BASE
   ══════════════════════════════════════════════════════════════════════════ */

*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-block-start: var(--sp-7);
  font-family: var(--font-sans);
  font-optical-sizing: auto;

  /* Tiene que estar en el elemento RAÍZ, no solo en body: el overflow del root
     es el único que propaga al viewport. Con `clip` únicamente en body, la
     geometría del fondo generaba 50px de scroll horizontal real en móvil
     — medido en el navegador, no supuesto. */
  overflow-x: clip;
}

body {
  /* Marco de referencia del fondo: `.backdrop` es absoluto y se dimensiona
     contra el documento entero, no contra el viewport. */
  position: relative;
  min-height: 100svh;
  background-color: var(--ink);
  color: var(--text-body);
  font-size: var(--fs-body);
  font-weight: var(--fw-light);
  line-height: var(--lh-relaxed);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  overflow-x: clip;
}

img,
svg,
picture {
  display: block;
  max-width: 100%;
}

img {
  height: auto;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
}

a {
  color: inherit;
  text-decoration: none;
}

ul {
  list-style: none;
  padding: 0;
}

:where(a, button, [tabindex]) {
  -webkit-tap-highlight-color: transparent;
}

p {
  text-wrap: pretty;
}

/* ── Foco visible: un solo tratamiento para toda la página ────────────────
   Anillo de dos tonos. El separador oscuro garantiza 3:1 contra el fondo
   negro y contra cualquier superficie cálida, sin una regla por componente. */
:where(a, button, [tabindex]:not([tabindex="-1"])):focus {
  outline: none;
}

:where(a, button, [tabindex]:not([tabindex="-1"])):focus-visible {
  outline: none;
  box-shadow: var(--focus-shadow);
  border-radius: inherit;
  transition: box-shadow var(--d-base) var(--e-glide);
}

/* ════════════════════════════════════════════════════════════════════════════
   2 · UTILIDADES
   ══════════════════════════════════════════════════════════════════════════ */

.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  inset-inline-start: var(--sp-4);
  inset-block-start: var(--sp-2);
  z-index: var(--z-skip);
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--r-full);
  background: var(--paper);
  color: var(--ink);
  font-size: var(--fs-small);
  font-weight: var(--fw-bold);
  translate: 0 -250%;
  transition: translate var(--d-fast) var(--e-glide);
}

.skip-link:focus-visible {
  translate: 0 0;
}

/* Palabra clave en el degradado de fuego. Solo en titulares ≥26px: por debajo
   de ese tamaño el degradado se lee sucio y se usa dorado plano. El texto
   sigue siendo texto real, seleccionable y copiable. */
.shine-text {
  background-image: var(--grad-text-shine);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  /* Sin esto Safari recorta los descendentes (g, j, y, q). */
  padding-block-end: 0.06em;
  margin-block-end: -0.06em;
}

@supports not ((background-clip: text) or (-webkit-background-clip: text)) {
  .shine-text {
    color: var(--steel-lit);
    background-image: none;
    -webkit-text-fill-color: currentColor;
  }
}

/* ── Chip-etiqueta ─────────────────────────────────────────────────────────
   Ocupa el lugar comunicativo que tenían las líneas en versales. Es el mismo
   recurso que AIVI usa antes de cada titular ("Tu agencia de bolsillo",
   "Planes AIVI", "FAQ"). Caja baja siempre.

   El punto dorado va como ::before y no en el marcado: es decoración pura y
   un lector de pantalla no debe anunciarlo. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--r-full);
  background-color: var(--surface-glass);
  border: 1px solid var(--border-soft);
  box-shadow: var(--sh-glass);
  font-size: var(--fs-small);
  font-weight: var(--fw-medium);
  line-height: var(--lh-normal);
  letter-spacing: var(--ls-chip);
  color: var(--text-secondary);
}

/* El punto del chip. Va como pseudo-elemento cuando encabeza la píldora, y
   como `.chip__dot` cuando separa palabras dentro de ella — mismo tamaño y
   mismo degradado, para que no convivan dos estilos de punto en una misma
   pieza.

   Separar con puntos REALES y no con el carácter "·": a 15px, un degradado
   aplicado a un glifo con `background-clip: text` se lee sucio, que es por lo
   que la página reserva ese recurso para titulares (ver `.shine-text`). */
.chip::before,
.chip__dot {
  content: "";
  flex: none;
  width: 5px;
  height: 5px;
  border-radius: var(--r-full);
  background-image: var(--grad-steel);
}

/* El chip del hero separa tres conceptos, así que sus puntos van DENTRO. Un
   punto más encabezándolo sumaría un cuarto y rompería la lectura. */
.hero__chip::before {
  display: none;
}

/* Geometría de AIVI como máscaras. `clip-path: polygon()` no puede redondear
   vértices; un trazo SVG con stroke-linejoin:round sí, y pesa ~230 bytes. */
:root {
  --mask-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 34 44'%3E%3Cpath d='M9 7 L26 22 L9 37' fill='none' stroke='%23fff' stroke-width='13' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --mask-hex: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 174'%3E%3Cpath d='M64 26H136L172 87L136 148H64L28 87Z' fill='%23fff' stroke='%23fff' stroke-width='28' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/* ── Trazo en degradado ────────────────────────────────────────────────────
   border-image queda descartado: ignora el border-radius, y todas las píldoras
   de esta página son redondas. La construcción es un pseudo-elemento con dos
   máscaras compuestas en `exclude`, que recorta el centro y deja solo el filo.

   Son DOS pseudos, no uno: el ::before es el trazo en reposo y el ::after el
   caliente, en opacity 0. Cruzar por opacidad es lo único que el compositor
   resuelve sin repintar el degradado en cada frame — cambiar background-image
   en :hover repintaría el filo completo 60 veces por segundo.

   Z-INDEX POSITIVO, no negativo: el trazo tiene que pintarse POR ENCIMA del
   contenido del host, nunca por debajo. `.collab` lleva
   una imagen a `width:100%; height:100%` que ocupa exactamente el mismo
   cuadro que el pseudo — con z-index negativo esa imagen lo tapaba entero y
   el trazo no se veía nunca, ni en reposo ni en caliente. Verificado forzando
   `img{opacity:0}`: el filo aparecía al instante, prueba de que se pintaba y
   quedaba cubierto. `isolation: isolate` en el host acota este z-index a un
   contexto de apilamiento propio: solo compite con los hijos reales de ESE
   host, nunca con el resto de la página. Si un hijo tiene que quedar POR
   ENCIMA del trazo (una insignia, un rótulo), su z-index tiene que superar
   el de aquí dentro de ese mismo contexto.

   CONTRATO — un host con `.stroke` no puede reclamar ::before ni ::after
   para nada más: colisionarían con los del trazo a igual especificidad y
   heredarían su máscara de anillo y su transición de opacidad. Cualquier
   capa adicional que necesite el host (velo, resplandor, fundido) tiene que
   ser un elemento hijo real, nunca un pseudo-elemento del mismo host. Ver
   `.collab__meta` y `.social__glow`. */
.stroke {
  position: relative;
  isolation: isolate;
}

.stroke::before,
.stroke::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  padding: var(--sw-stroke);
  pointer-events: none;
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
}

.stroke::before {
  background-image: var(--grad-stroke);
}

.stroke::after {
  background-image: var(--grad-stroke-hot);
  opacity: 0;
  transition: opacity var(--d-out) var(--e-fade);
}

.stroke:focus-visible::after {
  opacity: 1;
  transition-duration: var(--d-fast);
}

@media (hover: hover) and (pointer: fine) {
  .stroke:hover::after {
    opacity: 1;
    transition-duration: var(--d-fast);
  }
}

/* El guard de hover excluye al tacto: sin esto, el trazo nunca se enciende en
   un teléfono, que es el dispositivo principal de esta página. Después de
   :hover en el orden del archivo → gana a igual especificidad, la misma
   convención que sigue `.row:active` más abajo. */
.stroke:active::after {
  opacity: 1;
  transition-duration: var(--d-tap);
}

/* Reserva obligatoria. Sin composición de máscara no hay trazo en degradado:
   se cae al borde dorado plano, que es exactamente lo que la página tenía
   antes de este rediseño. Nadie ve una caja rota. */
@supports not ((mask-composite: exclude) or (-webkit-mask-composite: xor)) {
  .stroke::before,
  .stroke::after {
    display: none;
  }

  .stroke {
    border: 1px solid var(--border-edge);
  }
}

/* ════════════════════════════════════════════════════════════════════════════
   3 · FONDO
   ---------------------------------------------------------------------------
   El fondo NO está fijo al viewport: se desplaza con la página. Fijo, la luz
   te perseguía durante todo el scroll y la página entera parecía tener la
   misma atmósfera de principio a fin. En aivi.chat la luz está SECCIONADA —
   resplandor cálido arriba, columnas de luz en momentos concretos, y el resto
   casi negro plano — y eso es lo que se replica aquí.

   Reparto:
     · resplandor cálido  → solo la altura del hero, anclado arriba
     · columnas de luz    → NO viven aquí, nacen detrás del bloque de botones
                            (sección 6), que es el foco de la página
     · geometría y grano  → todo el documento, muy tenues

   Regla dura: CERO capas GPU propias. Ninguna lleva will-change ni
   translateZ(0), así el navegador las pinta en una sola textura.
   ══════════════════════════════════════════════════════════════════════════ */

.backdrop {
  position: absolute;
  inset: 0;
  z-index: var(--z-backdrop);
  overflow: hidden;
  pointer-events: none;
  background-color: var(--ink);

  /* Aísla el grupo de composición: sin esto el mix-blend-mode del grano
     arrastra al <body> a un grupo común, que es la causa habitual de "el
     texto se ve grisáceo" en fondos de este tipo. */
  isolation: isolate;
  contain: layout paint style;
}

.backdrop > * {
  position: absolute;
  inset: 0;
}

/* Resplandor cálido de la esquina superior. Acotado a una altura de viewport:
   con el fondo ya desplazándose, estirarlo a todo el documento lo diluiría
   hasta no verse. La luz se suma en `screen`, que es su comportamiento
   físico; apilada en `normal` daría un lavado sucio y apagado. */
.backdrop__glow {
  inset: 0 0 auto 0;
  height: 100svh;
  z-index: 1;
  opacity: 0.55;
  background-image:
    radial-gradient(58% 44% at 92% 0%,
      rgb(var(--rgb-paper) / 11%) 0%,
      rgb(var(--rgb-paper) / 3.5%) 46%,
      rgb(var(--rgb-paper) / 0%) 76%),
    radial-gradient(88% 62% at 100% -6%,
      rgb(var(--rgb-paper) / 7%) 0%,
      rgb(var(--rgb-paper) / 2.5%) 40%,
      rgb(var(--rgb-paper) / 0%) 74%);
  background-repeat: no-repeat;
  background-size: 100% 100%, 100% 100%;
  background-blend-mode: screen, screen;
}

/* ── Geometría a gran escala, recortada por el lienzo ──────────────────────── */
.backdrop__glyphs {
  z-index: 2;
  opacity: 0.3;
}

.backdrop__glyph {
  position: absolute;
  display: block;
  background-image: var(--grad-glass);
  mask-image: var(--glyph-mask);
  mask-repeat: no-repeat;
  mask-size: 100% 100%;
  -webkit-mask-image: var(--glyph-mask);
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
}

/* vmax y no vw: en móvil vertical el glifo escala con la altura y no queda una
   mancha diminuta arriba a la derecha. */
.backdrop__glyph--a {
  --glyph-mask: var(--mask-hex);
  width: clamp(20rem, 62vmax, 52rem);
  aspect-ratio: 200 / 174;
  top: -14vmax;
  right: -18vmax;
  rotate: -22deg;
}

.backdrop__glyph--b {
  --glyph-mask: var(--mask-hex);
  width: clamp(17rem, 52vmax, 44rem);
  aspect-ratio: 200 / 174;
  top: 60vmax;
  right: -26vmax;
  rotate: -22deg;
  opacity: 0.7;
}

.backdrop__glyph--c {
  --glyph-mask: var(--mask-chevron);
  width: clamp(11rem, 34vmax, 26rem);
  aspect-ratio: 34 / 44;
  bottom: -8vmax;
  left: -12vmax;
  rotate: 24deg;
  opacity: 0.5;
}

/* Grano teselado a 128px: el navegador rasteriza un tile pequeño una vez y lo
   repite. Generarlo a tamaño de viewport son 20–60ms de rasterizado en móvil.
   `overlay` conserva los negros y aviva los medios tonos. Por encima de 0.06
   el texto pequeño pierde nitidez. */
.backdrop__grain {
  z-index: 4;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128'%3E%3Cfilter id='n' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='2' stitchTiles='stitch' seed='7'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='128' height='128' filter='url(%23n)'/%3E%3C/svg%3E");
  background-repeat: repeat;
  background-size: 128px 128px;
  mix-blend-mode: overlay;
  opacity: 0.055;
}

/* ════════════════════════════════════════════════════════════════════════════
   4 · SHELL Y RITMO
   ══════════════════════════════════════════════════════════════════════════ */

.page,
.foot {
  position: relative;
  z-index: var(--z-content);
}

/* Eje único de la página: el mismo max-width y el mismo padding-inline en
   hero, enlaces, colaboraciones, bio, redes y footer. La cinta de tarjetas es
   la única pieza que no lleva `.shell` — se desborda al viewport a
   propósito — pero su chip, su titular y su botón de pausa sí. */
.shell {
  width: 100%;
  max-width: var(--w-col);
  margin-inline: auto;
  padding-inline: var(--sp-gutter);
}

.section {
  padding-block-start: var(--sp-section);
}

/* ════════════════════════════════════════════════════════════════════════════
   5 · HERO
   ---------------------------------------------------------------------------
   Banner a todo el ancho con el texto superpuesto. La imagen está compuesta
   para eso: el sujeto a la derecha y el tercio izquierdo en negro.

   En móvil NO se superpone nada. A 390px de ancho un 16:9 deja la cara en
   unos 90px, así que el texto va arriba y la imagen debajo, recortada en
   cuadrado sobre el sujeto.

   CENTRADO en móvil, alineado a la izquierda en escritorio. No es una
   incoherencia: son dos composiciones distintas y cada una se alinea con lo
   que tiene al lado. En móvil el bloque ocupa el ancho completo, no hay nada
   a su derecha con lo que alinearse y el eje central es el único que existe.
   En escritorio el texto se superpone al tercio izquierdo del banner, y ahí
   centrar lo empujaría encima del sujeto.
   ══════════════════════════════════════════════════════════════════════════ */

.hero {
  display: grid;
  gap: var(--sp-8);
  text-align: center;
  padding-block-start: clamp(var(--sp-9), 10vh, var(--sp-11));
}

/* Bloque ancho, como la bio: 1120px centrados. El texto de dentro lleva su
   propio `.shell`, así que su borde izquierdo cae exactamente en el eje de
   los botones aunque la tarjeta se salga por los dos lados. */
@media (min-width: 60rem) {
  .hero {
    max-width: var(--w-wide);
    margin-inline: auto;
    padding-inline: var(--sp-gutter);
  }
}

/* Tarjeta, no sangre: mismas esquinas y mismo trazo en degradado que los
   botones y las portadas, para que el hero pertenezca a la misma familia
   visual que el resto de la página. `.stroke` ocupa ::before y ::after, por eso
   el velo del texto es un elemento hijo real y no un pseudo-elemento. */
.hero__media {
  position: relative;
  order: 1;                /* debajo del texto en móvil */
  /* Al mismo eje que los botones y la bio. Iba a sangre, y mientras el resto
     de la página estuvo alineado a la izquierda pasaba desapercibido; con
     todo centrado, una tarjeta redondeada cuyas esquinas tocan el filo de la
     pantalla se lee como un error de maquetación y no como una decisión.

     Va en `margin-inline` y no en un `padding` del padre a propósito: el
     texto del hero ya trae su propio `.shell`, y meter el hueco arriba se lo
     sumaría por segunda vez. */
  margin-inline: var(--sp-gutter);
  /* 6:5. Empezó en 1:1, pasó a 4:5 buscando presencia y volvió a bajar: a 4:5
     la foto medía 436px de alto en un móvil de 390 y empujaba el primer botón
     fuera de la pantalla. En un link in bio eso es el error caro — la foto es
     decoración y los botones son la página.

     El límite no es libre: por debajo de 6:5 el recorte parte los iconos
     flotantes de abajo. Es lo más corta que puede ser sin romper la
     composición de la foto. */
  aspect-ratio: 6 / 5;
  border-radius: var(--r-cta-lg);
  background-color: var(--surface-load);
  overflow: hidden;
  box-shadow: var(--sh-3);
}

.hero__media img,
.hero__media picture {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Centrado y sin más: el archivo de móvil ya viene recortado a 4:5 con el
     sujeto en su sitio, así que la caja y la foto tienen la misma proporción
     y no hay nada que reencuadrar. Si algún día se cambia esa foto por una
     de otra proporción, este es el valor que hay que revisar. */
  object-position: 50% 50%;
}

/* Velo que garantiza el contraste del texto sobre la foto. En móvil no hace
   falta —no hay texto encima—, así que está apagado y solo se enciende en
   escritorio. */
.hero__scrim {
  position: absolute;
  inset: 0;
  display: none;
  pointer-events: none;
}

.hero__text {
  display: grid;
  justify-items: center;
}

.hero__chip {
  margin-block-end: var(--sp-6);
}

/* Caja baja y peso 900. La mayúscula sostenida es un rasgo del referente, no
   de AIVI: en aivi.chat no hay un solo titular en versales. La jerarquía la
   carga el peso y el tracking negativo, no la caja. */
.hero__name {
  font-size: var(--fs-h1);
  font-weight: var(--fw-black);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-h1);
  color: var(--text-primary);
}

.hero__claim {
  margin-block-start: var(--sp-6);
  max-width: var(--w-read);
  /* `max-width` sin esto deja la caja pegada al borde izquierdo aunque el
     texto de dentro vaya centrado: lo que se centra es el texto, no el
     bloque. En escritorio se anula. */
  margin-inline: auto;
  font-size: var(--fs-lead);
  font-weight: var(--fw-regular);
  line-height: var(--lh-normal);
  color: var(--text-secondary);
}

/* La frase envuelve como una sola unidad: así el degradado nunca queda
   cortado por un salto de línea. */
.hero__claim .shine-text {
  white-space: nowrap;
}

/* ── Escritorio: banner con el texto encima ────────────────────────────────
   Imagen y texto comparten la misma celda de la rejilla; la imagen fija la
   altura y el texto se centra sobre ella. El bloque va a sangre completa: la
   imagen es un banner y recortarla a la columna la desaprovecharía.

   El texto sí vuelve al eje de la página con su propio `.shell`, así que su
   borde izquierdo sigue coincidiendo con el de los botones de abajo. */
@media (min-width: 60rem) {
  .hero {
    grid-template-columns: 1fr;
    gap: 0;
    align-items: center;
    /* Vuelve a la izquierda: aquí el texto se superpone al tercio izquierdo
       del banner —el que la foto deja en negro a propósito— y centrarlo lo
       llevaría encima del sujeto. */
    text-align: start;
  }

  .hero__media,
  .hero__text {
    grid-area: 1 / 1;
  }

  .hero__text {
    justify-items: start;
  }

  .hero__claim {
    margin-inline: 0;
  }

  .hero__media {
    order: 0;
    /* El hueco lo pone ya `.hero` con su `padding-inline`. */
    margin-inline: 0;
    /* Altura explícita, no `aspect-ratio` con `max-height`: esa combinación
       encoge la caja a lo ancho en lugar de recortar la foto, y el banner
       dejaba de llegar a los bordes. Con una altura fija manda `cover`, que
       recorta arriba y abajo y mantiene el ancho completo. */
    width: 100%;
    height: clamp(420px, 62svh, 620px);
    aspect-ratio: auto;
  }

  /* El encuadre se sube para no cortar la cabeza al recortar en vertical. */
  .hero__media img {
    object-position: 50% 18%;
  }

  /* Degradado desde la izquierda: opaco donde va el texto y transparente
     donde está el sujeto. Reproduce la caída a negro que la propia foto ya
     trae, y garantiza el contraste pase lo que pase con la imagen. */
  .hero__scrim {
    display: block;
    background-image: linear-gradient(90deg,
      var(--ink) 0%,
      rgb(var(--rgb-ink) / 96%) 42%,
      rgb(var(--rgb-ink) / 72%) 56%,
      rgb(var(--rgb-ink) / 24%) 68%,
      rgb(var(--rgb-ink) / 0%) 78%);
  }

  .hero__text {
    position: relative;
    z-index: var(--z-raised);
    align-content: center;
  }
}

/* ════════════════════════════════════════════════════════════════════════════
   6 · FILA DE ENLACE CON ICONO — el componente que paga la página
   ---------------------------------------------------------------------------
   Vidrio sobre negro: fondo apenas más claro que la página, trazo en
   degradado (clase `stroke`) en las dos filas secundarias, icono a la
   izquierda en su baldosa cálida, texto en blanco y flecha a la derecha. El
   fuego aparece en la baldosa, en el trazo de las filas secundarias, en el
   relleno completo de la fila destacada y al pasar el cursor.
   ══════════════════════════════════════════════════════════════════════════ */

/* Una sola columna. Hubo una segunda a la izquierda con el número de cada
   fila (01, 02, 03…): se retiró porque numerar cuatro enlaces sugiere un
   orden que hay que seguir, y aquí no lo hay — cada fila es una puerta
   independiente. Sin ellos la lista se centra y respira, que es la mitad de
   lo que hace que algo se lea caro. */
.links {
  display: grid;
  gap: var(--sp-5);
  margin-block-start: var(--sp-9);
}

.row {
  position: relative;
  isolation: isolate;
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--sp-5);
  padding: var(--sp-6);
  border-radius: var(--r-cta);
  background-color: var(--surface-row);
  background-image: var(--grad-row);
  box-shadow: var(--sh-glass);
  color: var(--text-primary);
  touch-action: manipulation;

  /* Asimetría de ignición: la salida (aquí, en la base) es la lenta, como una
     brasa que se apaga. La entrada corta vive en :hover. */
  transition: transform var(--d-out) var(--e-glide),
    background-color var(--d-out) var(--e-fade),
    box-shadow var(--d-out) var(--e-fade);
}

/* ── Filas rellenas ────────────────────────────────────────────────────────
   `.row--fill` es el componente: geometría, texto y comportamiento de una
   fila que se rellena de color en vez de quedarse en vidrio. NO trae color
   propio — eso lo ponen los modificadores de más abajo, cada uno reapuntando
   los mismos cuatro tokens. Un solo bloque de reglas sirve para los tres
   colores, y añadir un cuarto son cinco líneas.

   Antes esto era `.row--primary` con el fuego escrito dentro. Se generalizó
   al necesitar dos filas de taller rellenas: duplicar el bloque una vez por
   color habría triplicado los estados —hover, foco, pulsación, contraste
   forzado— y en la tercera copia alguno se habría quedado atrás.

   El texto va en NEGRO sobre los tres rellenos, y no es una elección de
   estilo: el blanco mide 1.54:1 sobre el dorado del fuego, 1.86:1 sobre el
   teal y 1.49:1 sobre el verde. Los tres son fallos graves. El negro mide
   5.50:1 en el peor punto del fuego, 5.75:1 en el del teal y 6.94:1 en el
   del verde. Lo verifica tools/check-contrast.py, pareja por pareja. */
.row--fill {
  background-color: transparent;
  background-image: var(--grad-row-fill);
  box-shadow: var(--sh-row-fill);
  color: var(--text-on-fill);
}

/* El estado caliente cruza por opacidad, igual que el trazo y por el mismo
   motivo: mover los stops de un degradado es repaint. */
.row--fill::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background-image: var(--grad-row-fill-hot);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--d-out) var(--e-fade);
}

.row--fill .row__title {
  color: var(--text-on-fill);
}

.row--fill .row__note,
.row--fill .row__arrow {
  color: var(--text-on-fill-soft);
}

/* Sobre un relleno, el glifo va del mismo negro que el texto. Es lo único
   que cambia: sin baldosa detrás no hay fondo ni filo que invertir. */
.row--fill .row__icon {
  color: var(--text-on-fill);
}

/* Los tres colores. No declaran ni una propiedad: solo reapuntan tokens. */
.row--fire {
  --grad-row-fill: var(--grad-fire);
  --grad-row-fill-hot: var(--grad-fire-hot);
  --sh-row-fill: var(--sh-fire);
  --sh-row-fill-hi: var(--sh-halo-hi);
}

.row--aesthetic {
  --grad-row-fill: var(--grad-fill-aesthetic);
  --grad-row-fill-hot: var(--grad-fill-aesthetic-hot);
  --sh-row-fill: var(--sh-fill-aesthetic);
  --sh-row-fill-hi: var(--sh-fill-aesthetic-hi);
}

.row--sales {
  --grad-row-fill: var(--grad-fill-sales);
  --grad-row-fill-hot: var(--grad-fill-sales-hot);
  --sh-row-fill: var(--sh-fill-sales);
  --sh-row-fill-hi: var(--sh-fill-sales-hi);
}

@media (hover: hover) and (pointer: fine) {
  .row--fill:hover::after {
    opacity: 1;
    transition-duration: var(--d-fast);
  }
}

.row--fill:focus-visible::after {
  opacity: 1;
}

/* El glifo va SUELTO, sin baldosa detrás. Tuvo una —un cuadrado redondeado
   de 52px con relleno y filo— y era lo que más ataba la página a la estética
   de lista de apps: un icono metido en su cajita es la convención de un menú
   de aplicaciones, no la de una página de producto.

   Se conserva un ancho fijo para que los cuatro títulos arranquen en la misma
   vertical; sin él, cada glifo mediría lo suyo y la columna de texto bailaría
   de fila en fila. La altura la marca el propio SVG. */
.row__icon {
  display: grid;
  place-items: center;
  width: 30px;
  color: var(--text-accent);
  transition: color var(--d-out) var(--e-fade);
}

/* Más grande que cuando tenía baldosa: sin el marco que lo contenía, un
   glifo de 24px se queda pequeño al lado de un título de 22. */
.row__icon svg {
  width: 30px;
  height: 30px;
}

@media (min-width: 37.5rem) {
  .row {
    gap: var(--sp-6);
    padding: var(--sp-7);
  }

  .row__icon {
    width: 34px;
  }

  .row__icon svg {
    width: 34px;
    height: 34px;
  }
}

.row__body {
  display: grid;
  gap: var(--sp-1);
  min-width: 0; /* deja que el texto se rompa en vez de desbordar el grid */
  /* Centrado hasta 60rem, igual que el hero y por el mismo motivo: en una
     pantalla estrecha el eje central es el único que hay. Se centra el texto
     DENTRO de su columna, no la columna: el glifo sigue a la izquierda y la
     flecha a la derecha, y esa banda central queda tan cerca del centro real
     de la píldora que se lee como centrada. */
  text-align: center;
}

/* Vuelve a la izquierda en escritorio, donde la fila es ancha y el texto de
   apoyo llega a dos líneas: ahí el borde izquierdo recto es lo que deja al
   ojo encontrar el arranque de la segunda línea sin buscarlo. */
@media (min-width: 60rem) {
  .row__body {
    text-align: start;
  }
}

/* En caja baja y peso 800, no en mayúsculas de peso 900: a este tamaño las
   mayúsculas piden más atención que el titular de la página y la lectura de
   tres filas seguidas se vuelve pesada. */
.row__title {
  font-size: var(--fs-row);
  font-weight: var(--fw-bold);
  line-height: var(--lh-snug);
  letter-spacing: -0.012em;
  color: var(--text-primary);
}

.row__note {
  font-size: var(--fs-small);
  font-weight: var(--fw-light);
  line-height: var(--lh-normal);
  color: var(--text-muted);
}

.row__arrow {
  width: 22px;
  height: 22px;
  color: var(--text-muted);
  transition: transform var(--d-out) var(--e-glide),
    color var(--d-out) var(--e-fade);
}

/* ── Estados ───────────────────────────────────────────────────────────────
   Todo el hover va dentro del guard: sin él, Android e iOS dejan el hover
   "pegado" tras el primer tap y la fila se queda encendida para siempre. */
@media (hover: hover) and (pointer: fine) {
  .row:hover {
    transform: translate3d(0, -2px, 0);
    background-color: var(--surface-row-hi);
    box-shadow: var(--sh-glass), var(--sh-halo-hi);
    transition-duration: var(--d-fast);
  }

  /* La fila rellena ya tiene su propio glow, y del color de su relleno: se
     intensifica, no se sustituye por el dorado genérico. */
  .row--fill:hover {
    box-shadow: var(--sh-row-fill), var(--sh-row-fill-hi);
  }

  /* Sin baldosa que encender, el glifo sube de plata a blanco. En las filas
     rellenas no se toca: ya es negro pleno y no hay nada por encima. */
  .row:not(.row--fill):hover .row__icon {
    color: var(--text-primary);
    transition-duration: var(--d-fast);
  }

  .row:hover .row__arrow {
    transform: translate3d(4px, 0, 0);
    color: var(--text-accent);
    transition-duration: var(--d-fast);
  }

  .row--fill:hover .row__arrow {
    color: var(--text-on-fill);
  }
}

/* El teclado existe en cualquier dispositivo: fuera del guard de hover. El
   foco no desplaza la fila — mover el elemento enfocado desorienta. */
.row:focus-visible {
  box-shadow: var(--focus-shadow), var(--sh-glass);
}

.row:not(.row--fill):focus-visible .row__icon {
  color: var(--text-primary);
}

.row:focus-visible .row__arrow {
  color: var(--text-accent);
}

.row--fill:focus-visible .row__arrow {
  color: var(--text-on-fill);
}

/* Después de :hover en el orden del archivo → gana a igual especificidad. */
.row:active {
  transform: scale(0.985);
  transition-duration: var(--d-tap);
  transition-timing-function: var(--e-press);
}

.row:not(.row--fill):active {
  background-color: var(--surface-row-hi);
}

.row:not(.row--fill):active .row__icon {
  color: var(--text-primary);
}

.row--fill:active::after {
  opacity: 1;
  transition-duration: var(--d-tap);
}

/* ════════════════════════════════════════════════════════════════════════════
   7 · CINTAS EN BUCLE Y COLABORACIONES
   ---------------------------------------------------------------------------
   Sustituyen al carrusel con dots, que era el bloque más parecido al referente.

   NO se mueven solas. Un contenido que se desplaza más de cinco segundos
   obliga a ofrecer un mecanismo de pausa (WCAG 2.2.2); el botón que lo daba
   se retiró por decisión de diseño, así que se retiró también el movimiento.
   Sin animación el requisito desaparece, y la cinta de tarjetas se recorre a
   mano —rueda, dedo o flechas—, que es lo que cualquiera intenta primero.

   No dependen de JavaScript en absoluto: son CSS y marcado, nada más.
   ══════════════════════════════════════════════════════════════════════════ */

.ribbon {
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
}

.ribbon::-webkit-scrollbar {
  display: none;
}

.ribbon__track {
  display: flex;
  width: max-content;
}

.ribbon__set {
  display: flex;
  flex: none;
  align-items: center;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* ── Banda de texto ──────────────────────────────────────────────────────── */
/* La separación con el bloque anterior va en MARGIN, no en la clase `.section`.
   `.section` aporta `padding-block-start`, y aquí el padding es la distancia
   entre los filetes de la banda y su texto: si se usaran las dos cosas, la que
   se declara después gana y la banda queda pegada al último botón. */
.ribbon--text {
  border-block: 1px solid var(--border-hair);
  padding-block: var(--sp-4);
  margin-block-start: var(--sp-10);
}

/* Centrada y sin scroll. `width: auto` anula el `max-content` que `.ribbon__track`
   usa para la cinta desplazable: con max-content el track se encoge al
   contenido, no queda espacio sobrante y `justify-content: center` no centra
   nada — la banda se quedaba pegada a la izquierda. */
.ribbon--text {
  overflow-x: hidden;
}

.ribbon--text .ribbon__track {
  width: auto;
  flex: 1;
}

.ribbon--text .ribbon__track,
.ribbon--text .ribbon__set {
  flex-wrap: wrap;
  justify-content: center;
  align-items: baseline;
}

.ribbon--text .ribbon__set {
  flex: 1;
}

/* La banda ya no es un titular, así que tampoco necesita el aire de una
   sección entera a cada lado: con `--sp-section` quedaban ~129px arriba y
   112px abajo para un separador de 64px de alto. La sección que la sigue
   recorta su propio padding para que el conjunto se lea como una pausa, no
   como un corte. */
.ribbon--text + .section {
  padding-block-start: var(--sp-10);
}

/* La banda es un separador, no un titular: a tamaño de h3 competía con el
   titular de colaboraciones que viene justo debajo. */
.ribbon--text li {
  font-size: var(--fs-body);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-none);
  color: var(--text-muted);
  white-space: nowrap;
  padding-inline: var(--sp-5);
}

/* Separador ENTRE palabras: nunca colgando de la última. */
.ribbon--text li:not(:last-child)::after {
  content: "·";
  padding-inline-start: var(--sp-5);
  color: var(--text-accent);
  opacity: 0.5;
}

/* ── Cinta de tarjetas ───────────────────────────────────────────────────── */
/* Contenedor no-scrolleable, ancla del anillo de foco (`::after`, más abajo).
   Tiene que vivir FUERA de `.ribbon--cards`: ese es el elemento que se
   vuelve `overflow-x: auto` al enfocar, y cualquier hijo absoluto suyo se
   desplaza junto con el contenido cuando el usuario recorre la cinta. */
.ribbon-cards-wrap {
  position: relative;
}

/* El titular es de nivel de sección y las tarjetas son su contenido. Ojo al
   medir: la caja del titular sobresale ~18px por debajo de su contenedor, así
   que la separación que se VE es siempre menor que la declarada aquí. A 32px
   las tarjetas se leían pegadas al texto; 64px dejan unos 46px reales. */
.ribbon--cards {
  margin-block-start: var(--sp-11);
}

.ribbon--cards .ribbon__set {
  gap: var(--sp-3);
  padding-inline-end: var(--sp-3);
}

/* ── Bucle ─────────────────────────────────────────────────────────────────
   La cinta se desplaza de derecha a izquierda. Solo se anima `transform`.

   Propiedades LARGAS, nunca el shorthand `animation`: el shorthand fija
   también `animation-play-state` a su valor inicial (running), y esta regla
   tiene mayor especificidad (0,3,0) que la del estado pausado (0,2,1) — le
   ganaría siempre sin importar el orden, y el botón no pausaría nada. Medido
   en su día en el navegador: con el shorthand, animationPlayState seguía en
   "running" con aria-pressed ya en "true". */
.ribbon--cards.is-looped {
  overflow: hidden;
}

.ribbon--cards.is-looped .ribbon__track {
  animation-name: ribbon-slide;
  animation-duration: var(--d-ribbon-cards);
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

@keyframes ribbon-slide {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

/* Dos formas de detenerla: el cursor y el foco de teclado.

   AVISO: no hay control visible de pausa. WCAG 2.2.2 exige poder detener
   cualquier contenido que se mueva solo más de 5 segundos, y un botón es la
   única forma de cumplirlo para quien navega con el dedo. Se retiró por
   decisión de diseño del cliente, a sabiendas. Lo que queda cubre al ratón,
   al teclado y a quien pide movimiento reducido en su sistema; no cubre a un
   visitante de móvil que quiera pararla.

   El :hover va dentro del guard de puntero fino:
   fuera de él, un toque en Android/iOS deja `:hover` pegado y congelaría la
   cinta a mitad de bucle hasta el siguiente toque en otra parte. */
.ribbon--cards:focus-within .ribbon__track {
  animation-play-state: paused;
}

@media (hover: hover) and (pointer: fine) {
  .ribbon--cards:hover .ribbon__track {
    animation-play-state: paused;
  }
}

/* Alcanzable de verdad, no solo en apariencia: en marcha el contenedor está
   cerrado (`overflow: hidden`) para ocultar la costura del set duplicado, y
   en cuanto se detiene —por el botón, por foco o por movimiento reducido—
   vuelve a abrirse y las tarjetas se recorren con rueda, dedo o flechas.
   `tabindex="0"` en el marcado la hace tabulable en cualquier estado. Sin JS
   no hay `is-looped`, así que se queda abierta y quieta desde el principio. */
.ribbon--cards:focus-within {
  overflow-x: auto;
}

/* El tratamiento genérico de foco de toda la página (regla `:where(...)` de
   la sección 1) pone `box-shadow: var(--focus-shadow)`, que se dibuja HACIA
   AFUERA de la caja. Aquí no se ve: por el `mask-image` de más abajo, cuya
   área de pintado por defecto es la caja de borde, así que cualquier trazo
   que se dibuje FUERA de esa caja —outset box-shadow, u `outline` con offset
   positivo— queda fuera de esa área y se enmascara a transparente entero, no
   solo en los bordes horizontales. Medido: con el `outline` puesto,
   `getComputedStyle` reportaba el anillo pero cero píxeles en pantalla;
   quitando el `mask-image` a mano, el mismo anillo aparecía completo al
   instante.

   Meterlo hacia dentro con un box-shadow `inset` en el propio `.ribbon--cards`
   tampoco basta: las tarjetas son hijas opacas que llenan el alto entero del
   contenedor y se pintan POR ENCIMA de su propio fondo/sombra, así que el
   anillo solo asomaba por el hueco redondeado de cada esquina — una fila de
   marcas sueltas, no un aro. Medido: exactamente seis repeticiones, una por
   tarjeta visible, alineadas con sus esquinas.

   Un `::after` propio de `.ribbon--cards` resolvía esos dos problemas (se
   pinta encima de las tarjetas y dentro del área del mask), pero abría uno
   nuevo: en cuanto el foco activa `overflow-x: auto` en ese mismo elemento,
   el `::after` es un descendiente más de la caja que hace scroll y se
   desplaza con ella. Medido: a `scrollLeft: 0` el anillo daba
   `left: 0, width: 1470`; a `scrollLeft: 900` (con 1746px de recorrido de
   scroll disponible) daba `left: -900` — fuera de pantalla por completo con
   el foco todavía puesto ahí. Cualquier recorrido real del teclado dejaba el
   indicador sin verse mientras el elemento seguía enfocado (WCAG 2.4.7 /
   2.4.11).

   La solución es `.ribbon-cards-wrap`, de arriba: un contenedor que nunca
   hace scroll y envuelve a `.ribbon--cards`. El anillo vive en SU `::after`,
   así que ni el mask ni el scroll de `.ribbon--cards` lo tocan. El foco real
   sigue en `.ribbon--cards` (el `tabindex` vive ahí, no en el contenedor),
   así que el selector usa `:has()` para pintar el anillo del contenedor
   cuando el hijo tiene foco visible. Medido de nuevo tras el cambio: el
   anillo da la misma geometría a `scrollLeft: 0` y a `scrollLeft: 900`. */
/* Sin soporte de `:has()` el selector de abajo se descarta en el parseo por
   selector inválido, pero esta supresión no llevaba condición: apagaba el
   `box-shadow` genérico de foco de toda la página sin que nada lo
   reemplazara, y el elemento se quedaba tabulable sin ningún indicador
   visible — la misma regresión que este bloque existe para evitar. Se
   protege igual que la reserva de `mask-composite` de la sección 2: la
   supresión solo se activa donde el reemplazo por `:has()` existe de
   verdad. */
@supports selector(:has(*)) {
  .ribbon--cards:focus-visible {
    box-shadow: none;
  }
}

.ribbon-cards-wrap:has(.ribbon--cards:focus-visible)::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  box-shadow: inset 0 0 0 2px var(--focus-halo),
    inset 0 0 0 7px var(--focus-ring);
}

/* El fundido en los bordes solo tiene sentido cuando se ven varias tarjetas
   a la vez. En móvil, con una sola tarjeta centrada, un fundido de 40px por
   lado (--sp-8) se come más de una quinta parte de una pantalla de 360px. */
@media (min-width: 37.5rem) {
  .ribbon--cards {
    mask-image: linear-gradient(90deg,
      transparent 0,
      #000 var(--sp-8),
      #000 calc(100% - var(--sp-8)),
      transparent 100%);
  }
}

/* Cada tarjeta: portada arriba y, debajo, la fila de datos. La fila va FUERA
   de la imagen, sobre el fondo de la página, no superpuesta: la portada ya
   trae texto quemado del propio video y encimarle un rótulo la ensuciaba. */
/* La única sección centrada de la página. Es coherente: también es la única
   que se desborda a todo el ancho, así que ya era la excepción al eje único.
   Chip, titular y control comparten un mismo eje simétrico. */
.collabs .shell {
  text-align: center;
}

.collabs .chip {
  /* El chip es inline-flex: sin esto, `text-align` no lo movería. */
  display: inline-flex;
}

.collab {
  flex: 0 0 auto;
  width: min(62vw, 240px);
}

@media (min-width: 37.5rem) {
  .collab {
    width: 232px;
  }
}

@media (min-width: 64rem) {
  .collab {
    width: 256px;
  }
}

/* Sin trazo, a diferencia del hero y de las filas de vidrio. Llevaba uno y
   se retiró: nueve cards con filo seguidas eran nueve líneas más en pantalla,
   y una foto ya tiene su propio límite — no necesita que se lo dibujen. El
   límite lo dan el radio, la sombra y el hueco entre cards.

   No vale para todo: en `.social` el trazo SÍ se queda, porque allí es la
   única señal de que el círculo es un control (el vidrio de fondo da 1.10:1
   contra --ink y no llega al mínimo de 1.4.11). Quitar filos es una decisión
   de estilo solo donde no está sosteniendo una affordance. */
.collab__cover {
  position: relative;
  aspect-ratio: 3 / 4;
  border-radius: var(--r-lg);
  background-color: var(--surface-load);
  overflow: hidden;
  box-shadow: var(--sh-3);
}

/* En color, no en monocromo: estas ya no son fotos de relleno sino portadas
   reales de video, y el color es parte de lo que se está enseñando. */
.collab__photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 30%;
}

/* Superpuesta sobre la portada, en un rectángulo de vidrio oscuro. Cifra a la
   izquierda y nicho a la derecha: aquí SÍ pueden ir a extremos opuestos porque
   el rectángulo delimita cada tarjeta — sobre el fondo de la página, sin caja,
   el nicho de una quedaba pegado a la cifra de la siguiente.

   z-index 1: por encima de la foto pero por DEBAJO del trazo en degradado
   (z-index 2, sección 2), así el anillo sigue recorriendo el contorno entero
   sin que la barra le corte el canto inferior. */
.collab__meta {
  position: absolute;
  inset: auto 0 0 0;
  z-index: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-1) var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background-color: var(--surface-glass-dark);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  font-size: var(--fs-small);
}

.collab__views {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}

.collab__play {
  width: 12px;
  height: 12px;
  flex: none;
  color: var(--text-accent);
}

.collab__niche {
  font-weight: var(--fw-light);
  color: var(--text-muted);
}

/* ── Titular ─────────────────────────────────────────────────────────────── */
.collabs__title {
  margin-block-start: var(--sp-4);
  font-size: var(--fs-h2);
  font-weight: var(--fw-bold);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-h2);
  color: var(--text-primary);
  text-wrap: balance;
  max-width: 22ch;
  margin-inline: auto;
}

/* ════════════════════════════════════════════════════════════════════════════
   8 · BIO Y REDES
   ══════════════════════════════════════════════════════════════════════════ */

/* La bio es más ancha que el resto de secciones: es la única con contenido a
   dos lados. Su borde izquierdo no cae en el eje de las demás — es deliberado,
   el mismo tratamiento de bloque ancho que ya tiene la cinta de
   colaboraciones. */
.bio {
  /* Centrada en móvil, alineada a la izquierda en cuanto hay dos columnas.
     Un párrafo largo centrado se lee peor —el ojo pierde el arranque de cada
     línea— pero en una columna estrecha esa pérdida es pequeña y la
     alternativa era que este bloque fuese lo único descolgado a la izquierda
     en toda la pantalla. */
  text-align: center;
  max-width: var(--w-wide);
}

/* Nombre en blanco y rol en dorado, el mismo reparto que usa el resto de la
   página para destacar sin gritar. En la referencia del cliente esta línea iba
   en mayúscula sostenida; se compone en caja baja porque la página no usa
   versales en ningún sitio y tools/check-rules.py lo vigila. */
.bio__title {
  margin-block-start: var(--sp-5);
  font-size: var(--fs-h3);
  font-weight: var(--fw-bold);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-h3);
  color: var(--text-primary);
  text-wrap: balance;
}

/* El rol no se parte: si la línea rompe, que rompa después del nombre y no en
   medio de "IA Growth Strategist". Cabe de sobra — mide ~210px en una columna
   de 416px en escritorio y ~200px en los ~320px de móvil. */
.bio__title span {
  color: var(--text-accent);
  white-space: nowrap;
}

.bio__handle {
  font-size: var(--fs-small);
  font-weight: var(--fw-semi);
  letter-spacing: var(--ls-wide);
  /* Dorado a 14–15px con peso 600: 11.86:1. */
  color: var(--text-accent);
  margin-block-start: var(--sp-3);
}

/* Dos columnas: la foto sola a la izquierda y todo lo demás a la derecha —al
   revés que el hero, donde el retrato va a la derecha—, para que los dos
   bloques no se lean como la misma plantilla repetida.

   Van CENTRADOS entre sí, no estirados. Estirar la foto al alto exacto del
   texto la convertía en una tira de 200x780: la altura la marca el texto, y
   una foto que la persigue solo puede hacerse más estrecha. Con la foto en su
   proporción natural (3:4) y `align-items: center`, los dos bloques se
   equilibran solos y la sección respira.

   En móvil se apilan, foto primero. */
.bio__body {
  display: grid;
  gap: var(--sp-6);
  justify-items: center;
}

.bio__photo {
  width: min(70%, 300px);
  aspect-ratio: 3 / 4;
  border-radius: var(--r-lg);
  overflow: hidden;
  background-color: var(--surface-load);
  box-shadow: var(--sh-3);
}

.bio__photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.bio__main {
  min-width: 0;
}

@media (min-width: 56rem) {
  .bio {
    text-align: start;
  }

  .bio__body {
    grid-template-columns: 0.8fr 1fr;
    align-items: center;
    gap: var(--sp-9);
    justify-items: stretch;
  }

  .bio__photo {
    width: 100%;
  }

  /* En dos columnas la propia columna ya es la medida de lectura (~62
     caracteres); el tope de 46ch dejaba el texto flotando a media columna.
     Va con especificidad de dos clases a propósito: la regla base de
     `.bio__text` se declara más abajo en el archivo y, a igual especificidad,
     ganaría por orden. */
  .bio__body .bio__text {
    max-width: none;
  }
}

.bio__title {
  margin-block-start: var(--sp-5);
  font-size: var(--fs-h3);
  font-weight: var(--fw-bold);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-h3);
  color: var(--text-primary);
  text-wrap: balance;
}

/* El rol no se parte: si la línea rompe, que rompa después del nombre y no en
   medio de "IA Growth Strategist". Cabe de sobra — mide ~210px en una columna
   de 416px en escritorio y ~200px en los ~320px de móvil. */
.bio__title span {
  color: var(--text-accent);
  white-space: nowrap;
}

.bio__handle {
  font-size: var(--fs-small);
  font-weight: var(--fw-semi);
  letter-spacing: var(--ls-wide);
  /* Dorado a 14–15px con peso 600: 11.86:1. */
  color: var(--text-accent);
  margin-block-start: var(--sp-3);
}

.bio__text {
  display: grid;
  gap: var(--sp-5);
  max-width: min(var(--w-read), var(--measure));
  margin-block-start: var(--sp-5);
  /* Centra la CAJA. Sin esto el tope de medida la deja pegada a la izquierda
     aunque su texto vaya centrado, y el bloque queda descolgado del resto.
     En dos columnas el tope desaparece y esto deja de tener efecto. */
  margin-inline: auto;
}

.bio__text p {
  font-size: var(--fs-body);
  line-height: var(--lh-relaxed);
  color: var(--text-body);
}

/* El primer párrafo destaca por color y peso, no por tamaño: junto a la foto
   la columna mide ~336px, y a tamaño `--fs-lead` salían líneas de unos 32
   caracteres que se leen a trompicones. */
.bio__text p:first-child {
  font-weight: var(--fw-regular);
  color: var(--text-secondary);
}

/* Las palabras clave se resaltan en blanco con peso, nunca en naranja o
   dorado: dentro de un párrafo el color cálido cansa y rompe la jerarquía. */
.bio__text strong {
  color: var(--text-primary);
  font-weight: var(--fw-bold);
}

/* ── Cifras ────────────────────────────────────────────────────────────────
   Tres tarjetas en fila bajo el texto. Contenido centrado DENTRO de cada una:
   una tarjeta es una unidad cerrada, así que centrarla no rompe el eje
   izquierdo de la sección. `auto-fit` las apila solas en móvil, donde tres en
   línea partirían "estudiantes formados" en tres renglones. */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--sp-3);
  margin-block-start: var(--sp-7);
  list-style: none;
  padding: 0;
}

/* El resplandor cálido sube desde la base, el mismo recurso de las tarjetas de
   precio de aivi.chat. Capa propia con z-index -1 para no repintar el fondo. */
.stat {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: var(--sp-5) var(--sp-4);
  border-radius: var(--r-md);
  background-color: var(--surface-glass);
  border: 1px solid var(--border-soft);
  box-shadow: var(--sh-glass);
  text-align: center;
}

.stat::before {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 72%;
  z-index: -1;
  pointer-events: none;
  background-image: radial-gradient(130% 100% at 50% 100%,
    rgb(var(--rgb-paper) / 10%) 0%,
    rgb(var(--rgb-paper) / 3%) 44%,
    rgb(var(--rgb-paper) / 0%) 100%);
}

/* Cifras tabulares: los tres números alinean su ancho aunque tengan distinto
   número de dígitos. */
.stat__num {
  display: block;
  font-size: var(--fs-h3);
  font-weight: var(--fw-black);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-h3);
  color: var(--text-accent);
  font-variant-numeric: tabular-nums;
}

.stat__label {
  display: block;
  margin-block-start: var(--sp-2);
  font-size: var(--fs-small);
  font-weight: var(--fw-light);
  line-height: var(--lh-normal);
  color: var(--text-muted);
  text-wrap: balance;
}

/* Marcador de dato pendiente. Visible a propósito: es más honesto que una
   cifra inventada, y el cliente lo localiza de un vistazo. */
.tbd {
  padding: 0 var(--sp-1);
  border-radius: var(--r-xs);
  border: 1px dashed var(--border-marker);
  color: var(--text-accent);
  font-weight: var(--fw-semi);
  font-style: normal;
}

/* ── Iconos de redes ─────────────────────────────────────────────────────── */
/* Centradas, a diferencia del resto de la sección: cierran el bloque y actúan
   como remate, no como una línea más de contenido alineada al eje. */
.socials {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-3);
  margin-block-start: var(--sp-9);
}

/* CONTRATO `.stroke`: `.social` lleva la clase `stroke` (trazo en degradado
   que se enciende al pasar el cursor o al enfocar), que ya ocupa ::before y
   ::after para su propio trazo (ver el contrato de `.stroke` en la sección
   2). El resplandor cálido que llevaba `.social::before` no puede seguir
   siendo un pseudo-elemento propio: colisionaría con el del trazo a igual
   especificidad y heredaría su máscara de anillo y su transición de
   opacidad — el mismo motivo por el que el velo de `.collab` tampoco es un
   pseudo-elemento. Por eso el resplandor es un
   elemento hijo real, `.social__glow`, con aria-hidden en el marcado. El
   borde propio de `.social` también se retira: el filo en reposo y en
   caliente ya los gestiona `.stroke`. */
.social {
  position: relative;
  isolation: isolate;
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  border-radius: var(--r-full);
  background-color: var(--surface-glass);
  box-shadow: var(--sh-glass);
  color: var(--text-secondary);
  touch-action: manipulation;
  transition: transform var(--d-out) var(--e-glide),
    color var(--d-out) var(--e-fade);
}

@media (min-width: 37.5rem) {
  .social {
    width: 52px;
    height: 52px;
  }
}

.social svg {
  width: 20px;
  height: 20px;
}

/* Se anima la opacidad de una capa pre-pintada, no el background: así no se
   repinta el círculo. */
.social__glow {
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background-image: var(--grad-sheen);
  opacity: 0;
  transition: opacity var(--d-out) var(--e-fade);
}

@media (hover: hover) and (pointer: fine) {
  .social:hover {
    transform: translate3d(0, -2px, 0);
    color: var(--text-accent);
    transition-duration: var(--d-fast);
  }

  .social:hover .social__glow {
    opacity: 1;
    transition-duration: var(--d-fast);
  }
}

/* Teclado y ratón ven lo mismo. */
.social:focus-visible {
  color: var(--text-accent);
}

.social:focus-visible .social__glow {
  opacity: 1;
}

.social:active {
  transform: scale(0.93);
  color: var(--text-accent);
  transition-duration: var(--d-tap);
  transition-timing-function: var(--e-press);
}

.social:active .social__glow {
  opacity: 1;
  transition-duration: var(--d-tap);
}

/* ════════════════════════════════════════════════════════════════════════════
   9 · FOOTER
   ══════════════════════════════════════════════════════════════════════════ */

/* Fondo OPACO, no vidrio: es lo único de la página que tapa el fondo fijo. El
   resplandor y las columnas de luz pasaban por detrás del texto legal y se lo
   comían. Al ser opaco también actúa de cierre visual de la página. */
.foot {
  position: relative;
  margin-block-start: var(--sp-section);
  padding-block: var(--sp-7) calc(var(--sp-8) + env(safe-area-inset-bottom));
  background-color: var(--ink);
  text-align: center;
}

/* Filo de 2px a todo el ancho que separa el pie del fondo. En degradado de
   fuego y no en un gris plano: el corte entre el fondo con luz y el negro
   opaco del pie necesitaba una costura, y una costura del color de la marca
   la convierte en remate en lugar de en un borde de tabla.

   Se apaga hacia los dos extremos con una máscara, para que el filo no termine
   en dos esquinas duras contra el borde de la pantalla. */
.foot::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background-image: var(--grad-steel);
  mask-image: linear-gradient(90deg,
    transparent 0%, #000 18%, #000 82%, transparent 100%);
  -webkit-mask-image: linear-gradient(90deg,
    transparent 0%, #000 18%, #000 82%, transparent 100%);
}

/* Una sola línea: claim y aviso legal separados por un punto que pone el CSS.
   `flex-wrap` los parte en dos líneas solo si de verdad no caben, y entonces
   el separador desaparece para no quedar huérfano al final de la primera. */
.foot__line {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: baseline;
  gap: var(--sp-2) var(--sp-3);
  font-size: var(--fs-small);
  font-weight: var(--fw-semi);
  letter-spacing: var(--ls-none);
  /* Los dos al mismo contraste (6.19:1, el mínimo del sistema): la jerarquía
     se hace con peso y espacio, nunca bajando el contraste. */
  color: var(--text-muted);
}

.foot__claim::after {
  content: "·";
  padding-inline-start: var(--sp-3);
  color: var(--text-accent);
  opacity: 0.55;
}

/* ════════════════════════════════════════════════════════════════════════════
  10 · CAPA DE MOVIMIENTO
   ---------------------------------------------------------------------------
   Todo depende de `html.motion`, que añade un script inline en <head>. Sin JS
   la clase no existe, nada se oculta y la página es estática y completa.
   ══════════════════════════════════════════════════════════════════════════ */

html.motion [data-enter] {
  opacity: 0;
}

html.motion.is-ready [data-enter] {
  animation: enter-light var(--dur, var(--d-enter)) var(--e-light) var(--delay, 0ms) both;
}

@keyframes enter-light {
  from {
    opacity: 0;
    transform: translate3d(0, var(--from-y, 14px), 0) scale(var(--from-s, 1));
  }
  to {
    opacity: 1;
    transform: translate3d(0, 0, 0) scale(1);
  }
}

.hero__name {
  --delay: 140ms;
  --dur: 400ms;
}

.hero__chip {
  --delay: 210ms;
  --dur: 380ms;
  --from-y: 12px;
}


.hero__claim {
  --delay: 280ms;
  --dur: 380ms;
  --from-y: 10px;
}

/* Stagger de 70ms, no de 120: a 120 se percibe como una lista cargando, a 70
   como una corriente recorriéndola.

   `nth-of-type`, NO `nth-child`: dentro de `.links` cada fila va precedida de
   su `<span>` de número, así que en el orden de hijos las filas eran la 2, la
   4 y la 6. Con `nth-child(1..3)` solo acertaba una —la primera fila, por el
   `(2)`— y las demás entraban sin retardo, todas a la vez.

   Los números ya no están y hoy `nth-child` volvería a funcionar, pero se
   deja `nth-of-type`: cuenta solo los `<a>`, así que el escalonado sobrevive
   a que alguien vuelva a meter cualquier otro elemento entre las filas. */
.row:nth-of-type(1) {
  --delay: 340ms;
  --from-y: 18px;
  --from-s: 0.988;
}

.row:nth-of-type(2) {
  --delay: 410ms;
  --from-y: 18px;
  --from-s: 0.988;
}

.row:nth-of-type(3) {
  --delay: 480ms;
  --from-y: 18px;
  --from-s: 0.988;
}

.row:nth-of-type(4) {
  --delay: 550ms;
  --from-y: 18px;
  --from-s: 0.988;
}

.hero__media {
  --delay: 60ms;
  --dur: 520ms;
  --from-y: 16px;
}

/* Al terminar la entrada, main.js marca <html> con `is-entered` y las
   animaciones se apagan. Esto no es cosmético: `animation-fill-mode: both`
   deja fijado el `transform` del último keyframe y, en la cascada, una
   animación siempre gana a una declaración — sin este apagado los :hover y
   :active de las filas no volverían a mover nada nunca. Se apaga la animación
   y se repone la opacidad, pero NO se declara `transform: none`: eso ganaría
   por especificidad a `.row:hover` y reintroduciría el bug. */
html.motion.is-entered [data-enter] {
  animation: none;
  opacity: 1;
}

/* ── Revelado al scroll ──────────────────────────────────────────────────── */
html.motion [data-reveal] {
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  transition: opacity var(--d-enter) var(--e-light),
    transform var(--d-enter) var(--e-light);
}

html.motion [data-reveal].is-revealed {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

html.motion [data-reveal-stagger] > * {
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  transition: opacity var(--d-enter) var(--e-light) calc(var(--i, 0) * 70ms),
    transform var(--d-enter) var(--e-light) calc(var(--i, 0) * 70ms);
}

html.motion [data-reveal-stagger].is-revealed > * {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

/* ════════════════════════════════════════════════════════════════════════════
  11 · PREFERENCIAS DEL USUARIO
   ══════════════════════════════════════════════════════════════════════════ */

/* No es una versión degradada: es la misma página sin movimiento. Las filas
   siguen reaccionando (color, trazo y resplandor), las cintas quedan
   detenidas y accesibles con la rueda, el dedo o el teclado, y el anillo de
   foco no se toca nunca. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  html.motion [data-enter],
  html.motion [data-reveal],
  html.motion [data-reveal-stagger] > * {
    opacity: 1;
    transform: none;
    animation: none;
    transition: none;
  }

  /* Sin control visible de pausa, esta es la única vía que le queda a quien
     tiene sensibilidad al movimiento: la cinta no se mueve en absoluto y
     queda abierta para recorrerla a mano. Funciona con y sin JavaScript. */
  .ribbon--cards.is-looped .ribbon__track {
    animation: none;
    transform: none;
  }

  .ribbon--cards {
    overflow-x: auto;
  }

  /* Se apagan traslaciones, escalas y rotaciones; se conservan las
     transiciones de color y opacidad, que son feedback y no movimiento. */
  .row,
  .row__arrow,
  .social {
    transition-property: opacity, color, background-color, background-image,
      border-color, box-shadow, filter;
  }

  .row:hover,
  .row:active,
  .social:hover,
  .social:active {
    transform: none;
  }

  .row:hover .row__arrow {
    transform: none;
  }

  .skip-link {
    transition: none;
  }
}

@media (prefers-contrast: more) {
  :root {
    --text-body: rgb(var(--rgb-paper) / 94%);
    --text-muted: rgb(var(--rgb-paper) / 78%);
    --border-soft: rgb(var(--rgb-paper) / 40%);
    --border-hair: rgb(var(--rgb-paper) / 28%);
    --border-edge: rgb(var(--rgb-paper) / 46%);
    --surface-row: rgb(var(--rgb-paper) / 9%);
    --sh-halo: none;
    --sh-halo-hi: none;
  }

  .backdrop__grain {
    display: none;
  }

  .shine-text {
    color: var(--steel-lit);
    background-image: none;
    -webkit-text-fill-color: currentColor;
  }

  /* El degradado del trazo pierde en alto contraste: filo sólido. */
  .stroke::before {
    background-image: none;
    background-color: var(--steel-lit);
  }

  .stroke::after {
    display: none;
  }
}

@media (prefers-reduced-transparency: reduce) {
  /* La barra de datos pasa a opaca y sin desenfoque: quien pide menos
     transparencia normalmente lo hace porque el texto sobre fondos translúcidos
     le cuesta, y aquí debajo hay una foto arbitraria. */
  .collab__meta {
    background-color: var(--ink);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  .backdrop__glow {
    opacity: 0.3;
  }

  .backdrop__glyphs {
    opacity: 0.25;
  }

  .backdrop__grain {
    display: none;
  }
}

/* Windows HCM: los fondos decorativos desaparecen solos, hay que devolver
   bordes reales. */
@media (forced-colors: active) {
  .backdrop {
    display: none;
  }

  /* `.row__icon` ya no está en la lista: dejó de ser una caja y pasó a ser un
     glifo suelto. Ponerle borde le dibujaría un marco que no existe. */
  .row,
  .collab__cover,
  .social {
    border: 1px solid ButtonText;
  }

  /* En colores forzados no se pintan fondos translúcidos: sin esto la barra
     quedaría como texto suelto sobre la foto. */
  .collab__meta {
    background-color: Canvas;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border-block-start: 1px solid ButtonText;
  }

  .row__arrow,
  .row__icon {
    color: ButtonText;
  }

  .shine-text {
    color: LinkText;
    background-image: none;
    -webkit-text-fill-color: currentColor;
  }

  .stroke::before,
  .stroke::after,
  .row--fill::after {
    display: none;
  }

  /* En colores forzados no hay relleno de color que valga: las tres filas
     rellenas vuelven a la paleta del sistema, igual que las de vidrio. Sin
     esto el texto negro se quedaría escrito sobre ButtonFace. */
  .row--fill {
    background-image: none;
    forced-color-adjust: none;
    background-color: ButtonFace;
    color: ButtonText;
  }

  .row--fill .row__title,
  .row--fill .row__note,
  .row--fill .row__arrow,
  .row--fill .row__icon {
    color: ButtonText;
  }

  :where(a, button, [tabindex]):focus-visible {
    outline: 3px solid Highlight;
    outline-offset: 2px;
    box-shadow: none;
  }
}

@media print {
  .backdrop {
    display: none;
  }

  body {
    background: #fff;
    color: #000;
  }
}
