/* ═══════════════════════════════════════════════════════════════
   Estructura y componentes. Los valores salen de tokens.css.
   ═══════════════════════════════════════════════════════════════ */

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

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  background: var(--bg);
  color: var(--fg);
  font: 400 var(--text-body)/var(--lh-body) var(--font-ui);
  overflow: hidden;
  /* el scroll vive en .scroller */
  -webkit-font-smoothing: antialiased;
}

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

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

/* Todos los <video> del sitio son decorativos (autoplay o play-on-hover,
   nunca `controls`): sin esto, Safari/iOS dibuja su propio botón de play
   encima mientras el video no arrancó, aunque no haya `controls` en el
   HTML.

   DOS COSAS QUE HAY QUE RESPETAR ACÁ Y QUE NO SE RESPETABAN:

   1. Cada pseudo-elemento va en SU PROPIA regla, nunca en una lista
      separada por comas. Son pseudo-elementos del shadow DOM del user
      agent: si el navegador no reconoce UNO de los de la lista, tira la
      regla ENTERA y no se aplica ninguno. Agrupados, alcanza con que una
      versión de Safari no conozca uno para que vuelva el botón.

   2. Sin `:not([controls])` adelante. WebKit es quisquilloso con lo que
      acepta antes de un `::-webkit-media-controls-*` y con el `:not()`
      la regla se caía —que es por lo que el botón seguía apareciendo—.
      No se pierde nada: ningún <video> del sitio lleva `controls`.

   El que faltaba y es EL de la queja: `-overlay-play-button`, el botón
   grande centrado. El que estaba (`-start-playback-button`) es el de
   iOS; en Safari de escritorio el overlay es otro elemento. */
video::-webkit-media-controls {
  display: none !important;
}

video::-webkit-media-controls-enclosure {
  display: none !important;
}

video::-webkit-media-controls-panel {
  display: none !important;
}

video::-webkit-media-controls-start-playback-button {
  display: none !important;
  -webkit-appearance: none;
}

video::-webkit-media-controls-overlay-play-button {
  display: none !important;
  -webkit-appearance: none;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  /* Pedido de seguimiento: "se desalinean los elementos de ese nav" —
     faltaba acá, no en el reset de cada botón puntual. El padding
     default del UA (1px 6px en Chrome) es EL motivo por el que
     .tabbar__item (Contact es <button>, los otros cuatro son <a>) no
     repartía parejo con flex:1 1 0 pese al mismo contenido adentro:
     confirmado en vivo forzando padding:0 a mano, el ancho se acercó
     al de los <a>. Otros botones del sitio ya traen su propio
     width/height/padding explícito en su propia regla (más específica,
     sigue ganando encima de esto), así que este 0 de base no les
     cambia nada — sólo cierra el hueco para los que no lo hacían. */
  padding: 0;
  margin: 0;
  cursor: pointer;
  /* el UA de Chrome pone text-transform:none y letter-spacing:normal en los
     controles: sin esto, un <button> dentro de .micro no se ve como sus <a> */
  text-transform: inherit;
  letter-spacing: inherit;
  text-align: inherit;
}

:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 3px;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: fixed;
  top: 1rem;
  left: 1rem;
  z-index: 10000;
  padding: .75rem 1rem;
  background: var(--fg);
  color: var(--bg);
  transform: translateY(-200%);
}

.skip-link:focus {
  transform: none;
}

/* ── ESCALA TIPOGRÁFICA ─────────────────────────────────────────
   Sistema título / subtítulo / párrafo, UNA sola familia (ver el
   comentario grande en tokens.css). La jerarquía la carga el PESO
   (--weight-*), no la tipografía: título (.display/.h1) en el escalón
   más alto, subtítulo (.h2) un escalón abajo, ambos comparten
   tracking/line-height porque son la misma "voz", sólo que a distinta
   escala. Pedido de seguimiento: los titulares dejan de ir en
   mayúscula sostenida (antes text-transform:uppercase acá) — se leen
   en su caso natural, el que ya trae cada string en data.js/HTML. */
.display,
.h1,
.h2 {
  font-family: var(--font-display);
  font-weight: var(--weight-title);
  letter-spacing: var(--ls-display);
  line-height: var(--lh-display);
  color: var(--brand);
}

.display {
  font-size: var(--text-display);
}

.h1 {
  font-size: var(--text-h1);
}

.h2 {
  font-weight: var(--weight-subtitle);   /* un escalón más liviano que .display/.h1 */
  font-size: var(--text-h2);
  letter-spacing: -.02em;
  line-height: .95;
}

.lead {
  font-family: var(--font-display);
  font-weight: var(--weight-lead);
  font-size: var(--text-lead);
  line-height: 1.2;
  letter-spacing: -.01em;
  max-width: 26ch;
  color: var(--brand);
}

.micro {
  font-family: var(--font-mono);
  font-weight: var(--weight-subtitle);
  /* Antes heredaba el peso 400 de fábrica del <body> sin pedirlo: a
     DM Mono (una mono con buen contraste de trazo) le alcanzaba, pero
     Jakarta Sans en 400 uppercase+tracking se leía fina para ser una
     etiqueta — 600 le da la misma presencia de "esto es un rótulo de
     UI" que tenía antes. */
  font-size: var(--text-micro);
  letter-spacing: var(--ls-micro);
  text-transform: uppercase;
  line-height: 1.3;
}

.muted {
  color: var(--muted);
}

.prose {
  max-width: 58ch;
  color: var(--neutral);
  opacity: .82;
}

.prose p+p {
  margin-top: 1em;
}

/* ── SCROLLER ───────────────────────────────────────────────── */
.scroller {
  position: fixed;
  inset: 0;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

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

/* ── RETÍCULA ───────────────────────────────────────────────── */
.grid {
  display: grid;
  grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
  gap: var(--gutter);
  padding-inline: var(--margin);
}

.overlay {
  pointer-events: none;
}

.overlay>* {
  pointer-events: auto;
}

/* ── HEADER ─────────────────────────────────────────────────── */
/* Barra opaca de ancho completo, borde rojo de 2px arriba de todo —
   patrón pedido explícitamente por referencia. Un único <span
   class="mark"> dentro de .header__logo, mismo asset en las 4 páginas.

   EXCEPCIÓN, sólo en home: `.header__logo .mark` arranca en
   `opacity:0` puesto por JS (initHeroLogoMorph, scroll-fx.js) — es el
   punto de llegada del wordmark gigante del héroe, que se encoge hasta
   acá con el scroll y le cede el lugar en un crossfade. En las otras 3
   páginas nadie toca ese opacity y el logo es, como siempre, estático.

   El logo va CENTRADO, no a la izquierda: es un ítem `position:absolute`
   anclado al centro exacto de `.header` (que al ser `position:fixed` ya
   es el contenedor de referencia para ese absolute), independiente del
   ancho real de los dos grupos de nav a los costados — así queda
   centrado sin importar que "Work · About" (izquierda) y
   "Expertise · Toolkit · Contact" (derecha) no pesen lo mismo en texto.
   Los dos grupos se separan con `justify-content:space-between` en
   `.header`, cada uno en flex con sus propios gaps.

   data-header + .header--hidden: la barra se esconde al bajar y
   reaparece al subir — ver initHeaderAutohide() en app.js.

   data-header-theme + .header--on-light: el color del header pasa de
   marca a negro cuando la sección que queda detrás suyo es clara
   (`.home-invert`, `.case-scope`, `.case-nav`) — ver
   initHeaderTheme()/applyHeaderTheme() en app.js. */
.header {
  position: fixed;
  top: 0;
  inset-inline: 0;
  z-index: var(--z-header);
  /* env(safe-area-inset-*) sólo hace algo con viewport-fit=cover (ya
     puesto en el <meta viewport> de las 4 páginas) — sin eso, 0px
     siempre y esta línea es un no-op. Con notch, empuja el header
     debajo de la isla dinámica en vez de quedar tapado. */
  padding: max(1rem, env(safe-area-inset-top)) max(var(--margin), env(safe-area-inset-right)) 1rem max(var(--margin), env(safe-area-inset-left));
  display: flex;
  align-items: center;
  justify-content: space-between;
  color: var(--brand);
  transition: transform .45s var(--ease-out-expo), color .3s var(--ease-out-expo);
}

.header--hidden {
  transform: translateY(-100%);
}

.header--on-light,
.header--on-light a,
.header--on-light button {
  color: var(--case-fg);
}

.header--on-light .header__logo .mark {
  background: var(--case-fg);
}

.header__nav {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 3vw, 2.5rem);
}

.header__logo {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: clamp(100px, 10vw, 140px);
  aspect-ratio: 350 / 56;
  display: block;
}

.header__logo .mark {
  width: 100%;
  height: 100%;
}

@media (max-width: 700px) {

  /* Ahora TODO el nav vive en el menú de hamburguesa (.nav-sheet) —
     nada queda suelto en el header salvo el logo y el botón de
     hamburguesa.
     Especificidad de ancestro+clase (no sólo clase suelta):
     enhanceLink() le agrega `.link` a los <a> de acá y esa regla trae su
     propio `display: inline-block` más abajo en la hoja — mismo peso de
     especificidad que un `.header__hide-mobile` sencillo, así que ganaba
     por venir después en el archivo. Un primer arreglo probó
     `a.header__hide-mobile`, pero eso sólo pescaba <a> — el botón
     Contact (un <button>) se colaba igual. `.header__nav
     .header__hide-mobile` cubre cualquier tag, sin importar el orden en
     el archivo. */
  .header__nav .header__hide-mobile {
    display: none;
  }
}

/* .blend-scope ya no lleva isolation:isolate — atrapaba el z-index alto
   del header DENTRO de su propio contexto de apilado, así que perdía
   contra el canvas del héroe a nivel raíz (.blend-scope viene antes que
   .scroller en el DOM, y sin isolation ambos compiten por z-index real
   en vez de por orden de documento). Nada en el sitio usa
   mix-blend-mode hoy — la regla había quedado vestigial de un diseño
   anterior. Bug preexistente, no introducido en esta pasada: antes no
   se notaba porque el header era chico, transparente, y el canvas no
   competía por esos píxeles con nada importante. */

/* ── HOME: HÉROE "PURE FLUID OPTICS" ─────────────────────────
   Pantalla completa y sin texto encima: el shader es la pieza. El
   degradado del contenedor no es decorativo, es el fallback cuando no
   hay WebGL o hay prefers-reduced-motion — aproxima el mismo encuadre
   (foco cálido arriba a la izquierda, negro rojizo en el resto).
   La paleta es la del shader, no la de tokens.css: decisión de AD.  */
.reel-hero {
  position: relative;
  height: 100dvh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Centrado en las dos franjas — spec de Figma. Antes era flex-end
     (apoyado abajo); quedó como comentario porque este bloque documenta
     el shader, no la composición del texto (ver el bloque de .intro
     más abajo para esa historia). */
}

.reel-hero__media {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(58% 46% at 34% 30%, #ffb380 0%, #ff1a0d 18%, #a60000 42%, transparent 72%),
    radial-gradient(90% 60% at 50% 100%, #6b0000 0%, transparent 70%),
    #030101;
}

.reel-hero__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  opacity: 0;
  transition: opacity .8s var(--ease-out-expo);
}

.reel-hero__media.is-gl-on .reel-hero__canvas {
  opacity: 1;
}

/* El shader es fuerte en contraste pero variable en tono; sin este velo
   el titular pierde legibilidad según dónde quede el foco de luz en ese
   instante. Sólo abajo, donde vive el texto — arriba el shader queda
   limpio. */
.reel-hero__scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to bottom, transparent 45%, rgb(0 0 0 / .5) 75%, rgb(0 0 0 / .82) 100%);
}

/* ── HOME: INTRO ────────────────────────────────────────────────
   Wordmark gigante centrado + rol/pitch a los costados, fila de pie
   con disponibilidad/hint/ciudad — a partir de una captura de
   referencia. .intro llena el hero entero por su cuenta (position:
   absolute; inset:0) en vez de depender del justify-content:center
   de .reel-hero: así la fila central puede quedar centrada Y la fila
   de pie anclada abajo, sin flex-grow ni margin-top:auto peleando
   por el mismo eje. Mismo truco que .about-hero__content /
   .hero__bar en otras secciones del sitio. */
.intro {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: clamp(5rem, 12vh, 7rem) var(--margin) clamp(4.5rem, 11vh, 6rem);
  /* Overlay decorativo sin ningún elemento clickeable adentro (título,
     wordmark, textos de pie) — pero al cubrir el hero entero con
     inset:0, tapaba el pointermove que gl-hero.js escucha en
     .reel-hero__media para el foco de luz que sigue al cursor. Sin
     esto el shader dejaba de trackear el mouse apenas el puntero
     entraba al hero. */
  pointer-events: none;
}

.intro__row {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.25rem;
  max-width: 100%;
}

.intro__eyebrow {
  margin: 0;
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: clamp(.8rem, 1vw, .95rem);
  letter-spacing: -.01em;
  color: var(--muted);
  text-align: center;
  max-width: 22ch;
}

/* "Art Director & Brand Designer" partido en dos líneas era el bug real
   (medido en vivo: envolvía incluso en desktop, 1440px). El `max-width:
   22ch` de arriba es más angosto que el propio texto (30 caracteres) —
   por eso `.intro__eyebrow--pitch` ("Building Next-Gen Brands", 25
   caracteres) lo mantiene, pero el rol pide una sola línea sí o sí:
   `white-space: nowrap` lo garantiza estructuralmente en vez de
   depender de que el ancho disponible alcance por casualidad en cada
   viewport, y el `max-width` se saca para no pelearse con eso. */
.intro__eyebrow--role {
  max-width: none;
  white-space: nowrap;
}

/* El mismo asset que .header__logo .mark (logo.svg enmascarado en
   --brand), sólo mucho más grande: es la marca de verdad, no texto
   suelto en una fuente parecida — así el titular del hero y el
   logotipo del header quedan siendo exactamente el mismo dibujo. */
.intro__wordmark {
  display: block;
  width: clamp(220px, 78vw, 620px);
  aspect-ratio: 350 / 56;
  background: var(--brand);
  -webkit-mask: url(../assets/logo.svg) center / contain no-repeat;
  mask: url(../assets/logo.svg) center / contain no-repeat;
}

.intro__hint {
  margin: 0;
  color: var(--muted);
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: .8rem;
  letter-spacing: -.01em;
  text-align: center;
}

.intro__foot {
  position: absolute;
  left: var(--margin);
  right: var(--margin);
  bottom: clamp(1.5rem, 4vh, 2.5rem);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .5rem;
  color: var(--muted);
  text-align: center;
}

/* La tabbar fija (mobile) tapaba "Working worldwide" — mismo problema
   ya resuelto para .about-hero y .hero__bar en otras secciones, pero
   acá el bloque son 3 líneas (no un botón de una línea), así que el
   mismo margen de 5.25rem se quedaba corto contra el botón central
   elevado de la tabbar (mide más alto que el resto de la barra) —
   medido en vivo: con 5.25rem quedaban 10px de superposición. */
@media (max-width: 700px) {
  .intro__foot {
    bottom: calc(6.5rem + env(safe-area-inset-bottom));
  }
}

.intro__foot p {
  margin: 0;
}

/* ── HERO: hand tracking opcional ─────────────────────────────────
   Fuera de .intro (que es pointer-events:none) para que el botón y el
   preview sigan recibiendo clicks — ver el comentario en index.html.
   z-index 3, por encima del scrim (1) y de .intro (2). */
.hero-hand {
  position: absolute;
  top: clamp(4.5rem, 10vh, 6rem);
  right: var(--margin);
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: .6rem;
}

.hero-hand__btn {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .5rem .85rem;
  border: 1px solid rgb(255 255 255 / .3);
  border-radius: 999px;
  background: rgb(0 0 0 / .3);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: #fff;
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: .72rem;
  letter-spacing: -.01em;
  text-transform: uppercase;
  cursor: pointer;
  transition: border-color .3s var(--ease-out-quint), background .3s var(--ease-out-quint);
}

.hero-hand__btn:hover {
  border-color: rgb(255 255 255 / .6);
}

.hero-hand__btn[aria-pressed="true"] {
  background: var(--brand);
  border-color: var(--brand);
  color: #171717;
}

.hero-hand__icon {
  width: 15px;
  height: 15px;
  flex: none;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Ventanita en blanco y negro con la cámara: pedido explícito. El
   grayscale va en el <video> (así el esqueleto dibujado encima queda
   en color natural, más legible que si también se desaturara). Ambos
   —video y canvas— se espejan igual (scaleX(-1)) para que se lea como
   un espejo normal y el overlay quede pegado a la mano real. */
.hero-hand__preview {
  position: relative;
  width: clamp(150px, 20vw, 210px);
  aspect-ratio: 4 / 3;
  border-radius: .6rem;
  overflow: hidden;
  border: 1px solid rgb(255 255 255 / .3);
  background: #000;
}

.hero-hand__preview[hidden] {
  display: none;
}

.hero-hand__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.15);
  transform: scaleX(-1);
}

.hero-hand__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transform: scaleX(-1);
}

.hero-hand__close {
  position: absolute;
  top: .35rem;
  right: .35rem;
  z-index: 1;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: none;
  background: rgb(0 0 0 / .55);
  color: #fff;
  cursor: pointer;
}

/* SVG en vez del glyph "×": el caracter no quedaba centrado en el
   círculo (el ojo óptico de "×" no coincide con su caja de línea, y
   varía de fuente en fuente) — dos trazos cruzados a mano sí quedan
   exactos pase lo que pase con la tipografía del sistema. */
.hero-hand__close-icon {
  width: 11px;
  height: 11px;
  stroke: currentColor;
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
}

.hero-hand__status {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
  margin: 0;
  padding: .35rem .5rem;
  color: #fff;
  text-align: center;
  background: linear-gradient(to top, rgb(0 0 0 / .75), transparent);
}

@media (max-width: 700px) {
  .hero-hand__btn span {
    display: none;             /* sólo el ícono en mobile, no hay lugar para el label */
  }
  .hero-hand__btn {
    padding: .55rem;
  }
}

@media (min-width: 901px) {
  .intro__row {
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: clamp(2rem, 5vw, 5rem);
  }

  .intro__eyebrow {
    flex: 1 1 0;
    font-size: clamp(.85rem, .95vw, 1.05rem);
  }

  .intro__eyebrow--role {
    text-align: right;
  }

  .intro__eyebrow--pitch {
    text-align: left;
  }

  .intro__wordmark {
    flex: none;
    width: clamp(280px, 32vw, 620px);
  }

  .intro__foot {
    flex-direction: row;
    justify-content: space-between;
    gap: 1rem;
  }

  .intro__foot p {
    flex: 1 1 0;
  }

  .intro__foot p:first-child {
    text-align: left;
  }

  .intro__foot p:last-child {
    text-align: right;
  }
}

/* ── HOME: GALERÍA (patrón aino.agency) ───────────────────────
   Única sección de proyectos en home. Grid full-bleed de 2 columnas en
   desktop —medí las tarjetas de aino en vivo con CDP: ocupan la mitad
   del ancho, no la columna completa— y 1 en mobile. Cada foto se ve
   entera en su proporción natural (nada de object-fit:cover) y abajo una
   leyenda mono "CÓDIGO TÍTULO". El overlay ASCII decorativo que aino le
   pone encima de cada foto queda afuera a propósito — es un dispositivo
   de marca muy propio de ellos y no encaja con la tipografía/paleta de
   este sitio; ver README para la nota completa. */
.gallery {
  background: var(--bg);
  padding-bottom: clamp(2rem, 6vh, 4rem);
}

.gallery__bar {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  flex-wrap: wrap;
  padding: clamp(1.5rem, 4vh, 2.5rem) var(--margin);
}

.gallery__nav {
  display: flex;
  gap: 1.75rem;
}

/* La regla de 1px que se traza sola al entrar la sección — la firma de
   REF (sus .border-top/.border-left son divs de 1px con
   `transform .7s` de escala 0 a 1). El estado y el disparo viven en el
   bloque SCROLL-FX; acá va sólo cómo se ve. */
.gallery__rule,
.section__rule {
  display: block;
  height: 1px;
  background: var(--hairline);
  border: 0;
}

.gallery__rule {
  margin: 0 var(--margin) clamp(1rem, 3vh, 1.75rem);
}

.section__rule {
  grid-column: 1 / -1;
  margin-bottom: clamp(1.5rem, 4vh, 2.5rem);
}

.gallery__list {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 2px;
}

.gallery__item {
  display: block;
}

.gallery__media {
  display: block;
  background: var(--panel);
  overflow: hidden;
}

/* Sin object-fit:cover a propósito: la foto se ve entera, en su
   proporción natural — es lo que distingue esta galería del stack sticky
   y de la cinta, que sí recortan. Columnas parejas, filas dispares. */
.gallery__media img,
.gallery__media video {
  width: 100%;
  height: auto;
  display: block;
  transition: transform 1s var(--ease-out-expo);
}

/* El hover ya no escribe un `transform` propio: la foto lleva el del
   scroll-fx (entrada + parallax) y una segunda regla lo reemplazaría
   entero en vez de sumarse. Va por --fx-hover, que se multiplica
   adentro de ese mismo transform. */
.gallery__item:hover .gallery__media img,
.gallery__item:hover .gallery__media video {
  --fx-hover: 1.04;
}

.gallery__caption {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 4rem;
  margin: .30rem var(--margin) 0;
  color: var(--neutral);
  transition: opacity var(--dur-fast);
}

.gallery__item:hover .gallery__caption {
  opacity: .7;
}

.gallery__code {
  color: var(--neutral);
  flex: none;
}

/* En móvil la galería deja de apilarse y pasa a carrusel horizontal con
   snap (patrón H&M — ver 60fps.design/shots/h-m-header-swipe-interaction,
   la referencia que pidió el usuario): una tarjeta a la vista, la
   siguiente asoma como pista de que hay más. El label de arriba
   (.gallery__bar [data-gallery-label]) cambia con crossfade al proyecto
   centrado — ver initGallerySwipe() en home.js. */
@media (max-width: 700px) {
  .gallery__list {
    display: flex;
    grid-template-columns: none;
    gap: 0;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--margin);
    -webkit-overflow-scrolling: touch;
    padding-inline: var(--margin);
    scrollbar-width: none;
  }

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

  .gallery__item {
    flex: 0 0 82%;
    scroll-snap-align: center;
  }

  .gallery__item+.gallery__item {
    margin-left: 1rem;
  }

  .gallery__bar [data-gallery-label] {
    transition: opacity .25s var(--ease-tw);
  }

  .gallery__bar[data-swiping] [data-gallery-label] {
    opacity: 0;
  }
}

/* ── SCROLL APILADO (patrón incredibles.dev) ───────────────────────
   Las secciones grandes de la home no se van scrolleando: se quedan
   CLAVADAS arriba y la siguiente les sube por encima, como hojas de
   papel apilándose. La tapada no desaparece — sigue ahí atrás, visible
   como una franja cada vez más finita, hasta que la de arriba la cubre
   entera. Mecanismo CSS puro: `position: sticky` + `z-index` ascendente
   + esquina redondeada arriba y sombra hacia arriba en la hoja que sube.

   ── LA REGLA DEL SLOT ────────────────────────────────────────────
   Un `position:sticky` se despega cuando termina su CONTENEDOR, no
   cuando termina su propia altura. O sea que el contenedor ES la
   duración del pegado, y hay que elegirlo a propósito:

     · Sticky suelto sobre <main id="app"> (que envuelve TODA la home,
       ~10 mil px): cada sección se queda pegada casi hasta el pie.
       Ya pasó, está documentado en el README.
     · Un slot de alto arbitrario (la vuelta anterior: `min-height:
       120vh` para una sección de 100vh) es el error opuesto y más
       silencioso: deja sólo 20vh de pegado, y esos 20vh se terminan
       JUSTO cuando la sección siguiente recién asoma por abajo. O sea
       que no tapa nada: la de abajo llega cuando la de arriba ya se
       soltó, y las dos scrollean juntas. Se ve como un tirón/pausa
       muerta, no como una hoja subiendo.

   La regla correcta, y la única que hace falta recordar:

       EL SLOT ENVUELVE LA SECCIÓN QUE SE PEGA **Y** LO QUE LA TAPA.

   Así el alto extra del slot no es un número elegido a ojo: es,
   exactamente, el alto del contenido que tiene que pasarle por encima.
   El pegado dura lo que dura el tapado, ni un pixel más, y la sección
   se suelta recién cuando ya está 100% cubierta — por eso no hay
   saltos al soltarse: en ese cuadro ya no se la ve.

   Cadena de tapado, cada slot con su contenido entre paréntesis:

     .stack-slot ( HÉROE(sticky) + galería )
     .stack-slot ( ABOUT(sticky) + tools + see for yourself )     [dentro de .home-invert, beige]
     .stack-slot ( EXPERTISE(sticky) + selected clients )         [fuera de .home-invert, negro]

   Pedido de seguimiento ("desde expertise hasta el footer debería ser
   negro igual, que sea otra sección que se ponga arriba de la anterior
   al scroll"): Expertise + Selected clients salieron del slot de ABOUT
   y de `.home-invert` para armar su propio slot, en negro, con el mismo
   mecanismo pegado/tapado que el resto — Expertise es ahora la hoja
   pegada (antes no se pegaba nada ahí) y Selected clients es la que le
   sube encima, mismo rol que ya tenía sobre .s-work antes de este
   cambio (por eso no hizo falta tocarle el radio/sombra).

   No hay slots anidados: los tres son de primer nivel. Anidar hacía
   falta cuando "Our core" (eliminado — ver el comentario grande en
   index.html donde vivía) tenía que aguantar pegado mientras ABOUT,
   otra sección pegada, le subía encima.

   ── FONDO OPACO: la otra mitad del mecanismo ─────────────────────
   Una hoja sin fondo propio NO tapa nada. Las secciones de este tramo
   eran todas transparentes y el beige lo ponía `.home-invert` (el div
   padre), que al no estar posicionado pinta su fondo POR DEBAJO de
   cualquier elemento posicionado — o sea por debajo de las sticky. Con
   eso, una sección pegada se veía A TRAVÉS de la que supuestamente la
   estaba tapando. De ahí el bloque `background: var(--bg)` de abajo:
   cada hoja pinta su propio fondo. Sigue saliendo del token, así que
   `.home-invert` la sigue tiñendo de beige sin reglas duplicadas.

   El z-index asciende en TODO el recorrido (1 a 7), no sólo en las
   sticky: con dos secciones superpuestas en pantalla gana el z-index
   más alto, no el orden del DOM. */
.stack-slot {
  position: relative;
}

.stack-slot > .reel-hero,
.stack-slot > .about-hero,
.stack-slot > #expertise {
  position: sticky;
  top: 0;
}

/* Cada hoja con su propio fondo opaco — ver arriba por qué no alcanza
   con el de `.home-invert`. `var(--bg)` y no el color a mano: adentro
   de `.home-invert` ese token ya es el beige del caso. */
#expertise,
.s-work,
#clients {
  background: var(--bg);
}

.reel-hero {
  z-index: 1;
}

.gallery {
  position: relative;
  z-index: 2;
  border-radius: 24px 24px 0 0;
  box-shadow: 0 -24px 60px rgb(0 0 0 / .55);
}

/* .about-hero (z-index 3) trae su sticky/radio/sombra en su propio
   bloque, más abajo — es la hoja que tapa a la Galería directamente
   (antes tapaba a "Our core"; ver el comentario grande de arriba). */

/* Pedido de seguimiento: Expertise pasa a ser la hoja PEGADA de su
   propio .stack-slot nuevo (sticky en la regla de arriba), no la que
   sube — por eso ya no lleva radio ni sombra propios (ese tratamiento
   es de la hoja que tapa, ver .reel-hero sin radio/sombra más arriba).
   La tapa "Selected clients", que ya traía su radio/sombra de hoja. */
#expertise {
  z-index: 4;
}

.s-work {
  position: relative;
  z-index: 6;
}

/* "Selected clients" — el otro extremo nombrado explícitamente en el
   pedido. min-height:100dvh (probado antes) resolvía que la sección
   se sintiera una franja angosta, pero con sólo 5 logos centrados
   dejaba ~350px de beige vacío arriba Y otros ~350px antes del pie —
   el mismo "espacio en blanco" que se pidió eliminar, sólo que ahora
   pegado al footer. El padding-block de `.section` (mismo que usan
   Expertise y el resto) ya da aire de sobra sin inflar la sección a
   un viewport completo. */
#clients {
  /* Bug real, no sólo de color: esta regla nunca tuvo `position`, así
     que su z-index:7 nunca se aplicaba (z-index no hace nada en un
     elemento static) — no se notaba porque antes no había ningún
     hermano POSICIONADO justo antes tapándolo. Desde que Expertise
     pasó a sticky+z-index:4 (ver el stack-slot nuevo, más abajo en
     este archivo) esa capa posicionada empezó a pintarse SIEMPRE por
     encima de cualquier contenido static, sin importar el orden del
     DOM ni el z-index escrito acá — los logos quedaban tapados por el
     panel de Expertise apenas se pegaba. `position: relative` es lo
     que faltaba para que z-index:7 empiece a compararse de verdad
     contra el 4 de Expertise, mismo criterio que ya usa .gallery. */
  position: relative;
  z-index: 7;
  border-radius: 24px 24px 0 0;
  box-shadow: 0 -24px 60px rgb(0 0 0 / .12);
}

#clients .clients {
  justify-content: center;
}

/* ── PROFUNDIDAD DE LA HOJA TAPADA (GSAP ScrollTrigger, scrub) ────
   Polish sobre el mecanismo de arriba, no parte de él: el pin/tapado es
   CSS puro y funciona igual si esto no carga nunca (GSAP viene por CDN).
   Por eso el transform y el velo cuelgan de `.is-stack-depth`, una clase
   que pone `initStackDepth()` (js/modules/scroll-fx.js) recién DESPUÉS
   de que el módulo bajó: sin esa clase no hay ni un transform de más, o
   sea ni una capa de composición de más, en cuatro elementos que ocupan
   la pantalla entera.

   Lo que se scrubea son dos variables registradas con `@property`
   —mismo criterio que --fx-scale/--fx-hover más abajo: sin registrar,
   el navegador las trata como texto y saltan de golpe en vez de
   interpolar—. La hoja tapada se achica un pelo y se oscurece a medida
   que la de arriba le sube encima; el origen del transform es el borde
   de ARRIBA (50% 0), que es el que queda clavado por el sticky, así que
   lo que se encoge es justamente el lado por donde la están tapando. */
@property --stack-scale {
  syntax: '<number>';
  inherits: false;
  initial-value: 1;
}

@property --stack-dim {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}

.is-stack-depth {
  transform: scale(var(--stack-scale, 1));
  transform-origin: 50% 0;
}

/* El velo va en un ::after y no en un `filter`/`opacity` sobre la
   sección: bajarle la opacidad a la hoja la volvería translúcida y se
   vería el fondo A TRAVÉS (el negro de About dejaría pasar el beige de
   .home-invert, que se lee como un error, no como profundidad). Un velo
   opaco por encima oscurece sin transparentar nada. */
.is-stack-depth::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 30;
  pointer-events: none;
  border-radius: inherit;
  background: #0a0a0b;
  opacity: var(--stack-dim, 0);
}

/* El héroe no se pega abajo de 700px (ver abajo), así que tampoco se
   hunde: sin pin no hay hoja tapada que empujar hacia atrás. */
@media (max-width: 700px) {
  .reel-hero.is-stack-depth {
    transform: none;
  }

  .reel-hero.is-stack-depth::after {
    content: none;
  }
}

/* En móvil la galería NO puede ser una hoja: abajo de 700px deja de
   apilarse y pasa a carrusel horizontal de una tarjeta (ver el bloque
   de .gallery__list más arriba), o sea ~400px de alto contra una
   pantalla de 800 y pico. Una hoja más baja que el viewport no tapa: el
   héroe se soltaría con la galería a media pantalla y los dos pasarían
   a scrollear juntos de golpe — el mismo tirón que el slot de 120vh
   producía en desktop. Así que en ese ancho el héroe simplemente no se
   pega y scrollea normal. El resto de la cadena sigue en pie: about
   (100dvh) y expertise (~1800px) siguen midiendo más que una pantalla
   en móvil, así que su tapado no cambia. */
@media (max-width: 700px) {
  .stack-slot > .reel-hero {
    position: relative;
  }
}

@media (prefers-reduced-motion: reduce) {

  /* El tapado en sí no es una animación (es composición, no
     transition/animation), así que no dispara la regla global de
     tokens.css — pero clavar secciones a `position:sticky` durante
     todo su alto SIGUE siendo el mismo efecto de "algo se mueve
     mientras vos scrolleás" que reduced-motion pide evitar en el
     resto del sitio. Sin sticky quedan en flujo normal: mismo
     contenido, mismo orden, sin nada pegándose ni tapando nada. Los
     fondos opacos y los radios se quedan: ahí no se mueve nada, es
     sólo cómo se ve el corte entre una sección y la otra. */
  .stack-slot > .reel-hero,
  .stack-slot > .about-hero,
  .stack-slot > #clients {
    position: relative;
  }
}


/* ── SCOPE CLARO: de About al pie ────────────────────────────────
   Mismos tokens que .case-scope (project.js) — negro a beige, texto
   negro: es la misma inversión, no una nueva. Antes esta sección era
   roja; volvió a beige por pedido explícito. .display/.h1/.h2/.lead
   fuerzan rojo por regla global, así que se pisan acá para que no
   queden en rojo sobre beige (choca con el resto de textos en negro). */
.home-invert {
  --bg: var(--case-bg);
  --fg: var(--case-fg);
  --neutral: var(--case-fg);
  --muted: var(--case-muted);
  --hairline: var(--case-hairline);
  background: var(--bg);
  color: var(--fg);
}

.home-invert .display,
.home-invert .h1,
.home-invert .h2,
.home-invert .lead {
  color: var(--fg);
}

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

.section__label {
  grid-column: 1 / span 3;
  color: var(--muted);
  margin-bottom: 1rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  font-size: .78rem;
}

.section__body {
  grid-column: 5 / span 7;
}

@media (max-width: 900px) {

  .section__label,
  .section__body {
    grid-column: 1 / -1;
  }
}

/* ── ABOUT: hero full-bleed sobre video ──────────────────────────
   Calco de Figma about/mobile (1515:93), about/tablet (1515:63) y
   about/desktop (1515:36) — tres composiciones genuinamente distintas,
   no una sola escalada:
     · mobile:  columna centrada, abajo del todo — titular, línea,
                botón, en ese orden vertical.
     · tablet:  SIN el label "ABOUT" (así viene en el archivo); titular
                y botón en una fila al pie, alineados a la izquierda.
     · desktop: el label reaparece arriba a la izquierda; titular+botón
                vuelven a ir en fila al pie, más grandes.
   Reemplaza un draft anterior (.about-figma, del primer archivo de
   Figma que se evaluó — nodos 1424:4/1425:5/1425:22, layout beige de
   2 columnas) que quedó descartado a favor de este diseño.
   Los tokens de color/tipografía quedan en variables acá arriba en vez
   de sueltos en cada regla — son privados de esta sección (no pisan
   --bg/--fg del resto del sitio) porque este bloque escapa a propósito
   del scope beige de .home-invert (ver el comentario en index.html
   sobre data-header-theme). El mp4 de fondo es el reel real del sitio
   (no un placeholder de foto de comida como el que trae el archivo de
   Figma de ejemplo). */
.about-hero {
  --about-bg:      #000;
  --about-fg:      #fff;
  --about-label:   #c5bfbf;
  --about-hair:    rgb(255 255 255 / .22);

  /* sticky + z-index ascendente (ver el bloque grande "SCROLL
     APILADO" más abajo en este archivo, junto a .home-invert): esta
     es la sección que TAPA a la Galería al scrollear (antes tapaba a
     "Our core", eliminado — ABOUT quedó como la primera hoja del
     tramo beige) — el radio y la sombra son lo que hace que se lea
     como una capa nueva subiendo, no un simple cambio de fondo en el
     mismo plano. */
  position: sticky;
  top: 0;
  z-index: 3;
  border-radius: 0px 0px 0 0;
  box-shadow: 0 -24px 60px rgb(0 0 0 / .35);
  display: flex; flex-direction: column;
  min-height: 100dvh;
  overflow: hidden;
  background: var(--about-bg); color: var(--about-fg);
  padding: 2rem 1.5rem;
  isolation: isolate;
}

@media (prefers-reduced-motion: reduce) {
  .about-hero {
    position: relative;
  }
}
/* La tabbar flotante (fixed, abajo de todo) tapaba el botón — mismo
   problema ya resuelto para .hero__bar en otra sección del sitio,
   mismo valor de margen (5.25rem) para que la corrección sea
   predecible en cualquier bloque que termine pegado al pie en mobile. */
@media (max-width: 700px) {
  .about-hero { padding-bottom: calc(5.25rem + env(safe-area-inset-bottom)); }
}
.about-hero__media {
  position: absolute; inset: 0; z-index: 0;
  display: flex; align-items: center; justify-content: center;
}
.about-hero__video {
  width: 80%; height: 80%; object-fit: cover; display: block;
}
/* Velo permanente para legibilidad — mismo principio que .hero__scrim
   y .reel-hero__scrim en el resto del sitio: el titular no puede
   depender de qué frame del video le toque. El archivo de Figma sólo
   trae este degradado explícito en la versión mobile, pero el mismo
   problema (texto sobre video) existe en las tres, así que se aplica
   parejo en todos los breakpoints. */
/* Pedido explícito: más espacio negro para que About se sienta más
   inmersivo. Con "Our core" eliminado (ver el comentario grande en
   index.html) esta sección pasó a ser la primera hoja del scroll
   apilado — el negro entra apenas un tramo después de la galería, sin
   escala intermedia. El velo se profundiza (arranca el degradado antes
   —22% en vez de 32%— y el negro final es más opaco y más puro,
   #050506 en vez de #08080c) para que ese negro pese más: menos franja
   de video a media luz, más superficie realmente negra antes de que
   aparezca el titular. */
.about-hero__scrim {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(to bottom,
    transparent 0%, transparent 22%,
    rgb(5 5 6 / .85) 60%, #050506 100%);
}
.about-hero__label {
  position: relative; z-index: 2;
  margin: 0;
  font-family: var(--font-ui); font-weight: 600;
  font-size: .6125rem;                    /* 9.8px, calco exacto de Figma mobile */
  letter-spacing: -.02em;
  text-transform: uppercase;
  color: var(--about-label);
}
.about-hero__content {
  position: relative; z-index: 2;
  margin-top: auto;                        /* empuja el bloque al pie */
  display: flex; flex-direction: column; align-items: center;
  gap: 1.5rem;
  text-align: center;
}
.about-hero__headline {
  margin: 0;
  max-width: 20ch;
  font-family: var(--font-ui); font-weight: 600;
  font-size: 1.125rem; line-height: 1.28;
  letter-spacing: -.02em;
  text-transform: lowercase;
}
.about-hero__divider {
  display: block; width: 100%; height: 1px;
  background: var(--about-hair);
}
.about-hero__cta {
  display: inline-flex; align-items: center; justify-content: center;
  width: 10rem; height: 3rem; flex: none;
  border: 1px solid var(--about-fg);
  font-family: var(--font-ui); font-weight: 700;
  font-size: .8125rem; letter-spacing: 0;
  color: var(--about-fg);
  /* resplandor violeta sutil — único acento de color de todo el bloque,
     tal cual el archivo de Figma lo trae sólo en la versión mobile */
  box-shadow: 0 8px 16px rgb(59 31 217 / .25);
  transition: background-color var(--dur-fast), color var(--dur-fast);
}
/* Figma no trae un frame de hover explícito para este botón — mismo
   patrón invertido que ya usan los demás CTA en outline del sitio
   (.case-nav__all, .work__filters), no una invención sin precedente. */
.about-hero__cta:hover,
.about-hero__cta:focus-visible {
  background: var(--about-fg); color: var(--about-bg);
}

/* 701px+: label oculto (así viene tablet en Figma — desktop lo trae de
   vuelta más abajo), titular+botón pasan de columna centrada a fila al
   pie, alineados a la izquierda. */
@media (min-width: 701px) {
  .about-hero { padding: 3rem; }
  .about-hero__label { display: none; }
  .about-hero__media {
    inset: .75rem 1.125rem;                /* 12px/18px @1024 → marco redondeado */
    border-radius: 1.85rem;                /* 29.617px */
    overflow: hidden;
  }
  .about-hero__scrim { border-radius: inherit; }
  .about-hero__content {
    flex-direction: row; align-items: flex-end; justify-content: space-between;
    gap: clamp(1.5rem, 4vw, 2.5rem);
    text-align: left;
    padding-inline: .5rem;
    padding-bottom: .5rem;
  }
  .about-hero__headline {
    max-width: 30.5rem;                     /* 487px */
    font-size: 1.75rem;                     /* 28px */
    line-height: 1.214;                     /* 34px */
    letter-spacing: -.04em;
  }
  .about-hero__divider { display: none; }
  .about-hero__cta {
    width: auto; height: 2.375rem;          /* 38px */
    padding: 0 1.5rem;
    font-size: .9375rem;                    /* 15px */
    font-weight: 600;
    letter-spacing: -.04em;
    box-shadow: none;
  }
}

/* 1100px+: el label "ABOUT" vuelve, arriba a la izquierda — la
   composición de tablet no lo trae (fiel al archivo de Figma), la de
   desktop sí. Todo escala vía clamp anclado a la referencia de 1920px
   (el frame exacto del diseño) en vez de quedar fijo: por debajo de
   1920 el desktop real sigue existiendo (1280/1440/1536…) y no puede
   depender de un viewport mínimo de 1920 para no romperse. */
@media (min-width: 1100px) {
  .about-hero {
    padding: clamp(2.5rem, 3.85vw, 3.4375rem) clamp(3rem, 5.68vw, 6.8125rem);
  }
  .about-hero__label {
    display: block;
    font-size: clamp(.9375rem, .9375vw, 1.125rem);   /* 18px @1920 */
    letter-spacing: -.04em;
  }
  .about-hero__media {
    inset: clamp(.875rem, 1.4vw, 1.6875rem) clamp(.875rem, 1.04vw, 1.25rem);
    border-radius: clamp(2.5rem, 3.125vw, 3.75rem);   /* 60px @1920 */
  }
  .about-hero__content {
    padding-inline: clamp(.5rem, 1.7vw, 1.5rem);
    padding-bottom: clamp(.5rem, 1.5vw, 1.25rem);
  }
  .about-hero__headline {
    max-width: 40rem;                                  /* 639px */
    font-size: clamp(1.75rem, 1.82vw, 2.1875rem);       /* 35px @1920 */
    line-height: 1.142;                                 /* 40px */
  }
  .about-hero__cta {
    height: clamp(2.375rem, 2.26vw, 2.6875rem);         /* 43.34px @1920 */
    padding: 0 clamp(1.25rem, 1.29vw, 1.55rem);
    font-size: clamp(.9375rem, .97vw, 1.16rem);         /* 18.57px @1920 */
    letter-spacing: -.04em;
  }
}

.clients {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1.75rem 3rem;
  margin-top: 1.5rem;
}

.clients li {
  list-style: none;
  display: flex;
  align-items: center;
}

/* Logos reales (pedido explícito de "calidad profesional"), no el
   nombre en texto grande de antes (ver ABOUT.clients en data.js).
   Pedido de seguimiento: se quedan en gris siempre —sin el
   revelado a color de antes al pasar el mouse—, sólo suben opacidad
   al acercarse, así el hover sigue leyéndose como interactivo sin
   romper el registro apagado que pedía el feedback. Altura fija y
   ancho libre: cada marca trae su propia proporción (el rectángulo de
   McArthurGlen no es el cuadrado de Apple) y forzar un ancho igual las
   deformaría o las dejaría con aire desparejo alrededor.

   Pedido de seguimiento posterior: "se pierden al hacer scroll,
   deberían estar en blanco o un gris más notorio" — cierto, quedaron
   en gris(1) puro desde que Selected Clients pasó a fondo negro (ver
   la sección de scroll apilado más arriba en este archivo): un logo
   oscuro con grayscale(1) sobre negro casi no tiene contraste, sea
   cual sea su color original.

   grayscale(1) invert(1) y NO brightness(0) invert(1): la primera
   versión probada (brightness(0) invert(1)) dejaba bien a Apple/
   Amazon/AliExpress/inDrive (un solo trazo sobre transparente), pero
   volvía un bloque sólido gris a KFC y McArthurGlen — esos dos son
   insignias con una placa de color de FONDO y texto claro ENCIMA
   (ver assets/media/clients/kfc.svg: un rectángulo #ab182f detrás de
   un "KFC" en #ffffff), y brightness(0) manda las dos capas al mismo
   negro antes de invertir, así que el texto se pierde contra su
   propio fondo. grayscale(1) primero conserva el contraste de
   luminancia entre placa y texto (la placa oscura queda gris oscuro,
   el texto claro queda gris claro) y invert(1) sólo les da vuelta la
   polaridad a los dos por igual — la insignia se sigue leyendo, sólo
   que clara sobre negro en vez de oscura sobre blanco. Para los logos
   de un solo trazo el resultado es el mismo blanco limpio que daba la
   versión anterior. */
.clients__logo {
  display: block;
  /* --logo-scale (inline, desde ABOUT.clients.scale en data.js): 1 para
     casi todos — McArthurGlen sólo, hoy — sube esto porque su marca es
     una insignia con texto chico adentro, no un ícono sólido como el
     resto, y a igual altura de caja se leía más chica de peso visual. */
  height: calc(36px * var(--logo-scale, 1));
  width: auto;
  max-width: 170px;
  object-fit: contain;
  filter: grayscale(1) invert(1);
  opacity: .65;
  transition: opacity var(--dur-fast);
}

.clients li:hover .clients__logo {
  opacity: 1;
}

@media (max-width: 700px) {
  .clients__logo {
    height: calc(28px * var(--logo-scale, 1));
  }
}

/* ── WORK: HÉROE DEL PROYECTO DESTACADO ────────────────────────
   Portada editorial arriba de todo el índice — no es 100dvh como el
   héroe de project.html (esto es una página de listado, el contenido de
   abajo tiene que asomar), pero sí lo bastante alta para leerse como
   protagonista y no como una tarjeta más grande del grid. El header fijo
   flota encima con su scrim de siempre — mismo patrón que cualquier otro
   héroe del sitio, sin necesidad de padding-top propio. */
.work-hero {
  position: relative;
  display: block;
  overflow: hidden;
  height: min(78vh, 720px);
  color: #fff;
}

.work-hero__media {
  position: absolute;
  inset: 0;
}

/* Pedido de seguimiento: el héroe ya no muestra un solo proyecto fijo
   — arranca en el destacado del mes (FEATURED_SLUG, curado a mano) y
   después rota solo entre el resto de VISIBLE_PROJECTS, al azar, sin
   repetir el mismo dos veces seguidas (ver initWork() en work.js). El
   crossfade es la señal de que cambió de proyecto y no un refresh roto
   — img/video E inner (título, categoría, link) se apagan juntos y
   work.js recién reemplaza el contenido cuando ya están en opacity:0. */
.work-hero__media,
.work-hero__inner {
  transition: opacity .5s var(--ease-tw);
}

.work-hero.is-swapping .work-hero__media,
.work-hero.is-swapping .work-hero__inner {
  opacity: 0;
}

.work-hero__media img,
.work-hero__media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .6s var(--ease-out-expo);
}

.work-hero:hover .work-hero__media img,
.work-hero:hover .work-hero__media video {
  transform: scale(1.03);
}

.work-hero__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, rgb(0 0 0 / .35) 0%, transparent 35%, transparent 55%, rgb(0 0 0 / .82) 100%);
}

.work-hero__inner {
  position: absolute;
  inset: auto var(--margin) 1.75rem;
  display: flex;
  flex-direction: column;
  gap: .4rem;
}

.work-hero__inner [data-featured-eyebrow] {
  color: rgb(255 255 255 / .75);
  text-transform: uppercase;
  letter-spacing: .08em;
}

.work-hero__title {
  font-family: var(--font-ui);
  font-size: clamp(2rem, 3vw, 3rem);
  line-height: 1.02;
}

.work-hero__inner [data-featured-meta] {
  color: rgb(255 255 255 / .65);
  margin-top: .35rem;
}

/* El título "Work" ya no es lo primero de la página (el héroe destacado
   vive arriba): el padding-top pensado para eso —hasta 12rem, vía el
   `.section` genérico— dejaba un salto enorme entre el héroe y el resto.
   Un respiro chico alcanza acá. */
.work__title {
  padding-top: clamp(4rem, 6vh, 3.5rem);
}

.work_hero.h1 {
  font-family: var(--font-ui)
}

/* ── ÍNDICE DE WORK ─────────────────────────────────────────── */
.work__bar {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  padding-bottom: .75rem;
  border-bottom: 1px solid var(--hairline);
  margin: var(--space-md) var(--margin) var(--space-md);
}

.work__toggle {
  display: flex;
  gap: .9rem;
}

.work__toggle button {
  opacity: .4;
  transition: opacity var(--dur-fast);
}

.work__toggle button[aria-pressed="true"] {
  opacity: 1;
  color: var(--brand);
}

/* Etiquetas por tipo de trabajo: chips en píldora, mismo vocabulario
   visual de "tag" que usan los demás chips del sitio. */
.work__filters {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  padding: 0 var(--margin) var(--space-md);
}

.work__filters button {
  border: 1px solid var(--hairline);
  border-radius: 999px;
  padding: .45rem .9rem .4rem;
  opacity: .55;
  transition: opacity var(--dur-fast), border-color var(--dur-fast), color var(--dur-fast);
}

.work__filters button:hover {
  opacity: .85;
}

.work__filters button[aria-pressed="true"] {
  opacity: 1;
  color: var(--brand);
  border-color: var(--brand);
}

.card.is-filtered-out,
.list-view li.is-filtered-out {
  display: none;
}

.work[data-view="grid"] .list-view,
.work[data-view="list"] .grid-view {
  display: none;
}

.grid-view {
  display: grid;
  gap: var(--space-md) var(--gutter);
  grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
  padding-inline: var(--margin);
}

.card figure {
  overflow: hidden;
  aspect-ratio: 16 / 10;
  background: var(--panel);
}

.card img,
.card video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.05);
  transition: transform 1.3s var(--ease-out-expo);
}

.card:hover img,
.card:hover video,
.card:focus-visible img,
.card:focus-visible video {
  transform: scale(1);
}

.card__meta {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin-top: .7rem;
  align-items: baseline;
}

.card__title {
  font-family: var(--font-ui);
  font-weight: 400;
  font-size: 0.90em;
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--brand);
}

.grid-view .card {
  opacity: 1;
}

.js .grid-view .card {
  transition: opacity .9s var(--ease-out-expo), transform .9s var(--ease-out-expo);
  transition-delay: calc(var(--i) * 70ms);
}

.js .grid-view.is-loading .card {
  opacity: 0;
  transform: translateY(1.5rem);
}

.list-view {
  list-style: none;
  padding: 0 var(--margin);
}

.list-view li {
  border-bottom: 1px solid var(--hairline);
}

.list-view li:first-child {
  border-top: 1px solid var(--hairline);
}

.list-view a {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 1rem clamp(1rem, 4vw, 3rem);
  align-items: baseline;
  padding: clamp(.9rem, 2vh, 1.6rem) 0;
  font-family: var(--font-display);
  font-weight: var(--weight-title);
  font-size: clamp(1.5rem, 4vw, 3.4rem);
  text-transform: uppercase;
  letter-spacing: -.01em;
  line-height: 1;
  transition: padding-left .55s var(--ease-out-expo), color var(--dur-fast);
}

.list-view a:hover,
.list-view a:focus-visible {
  padding-left: clamp(.75rem, 2vw, 2rem);
  color: var(--brand);
}

/* al pasar el cursor por la lista, todo lo demás se apaga un poco —
   la atención va al proyecto activo y a su preview grande detrás */
.list-view:hover a:not(:hover):not(:focus-visible) {
  opacity: .35;
}

.list-view a {
  transition: padding-left .55s var(--ease-out-expo), color var(--dur-fast), opacity .4s;
}

.list-view .micro {
  align-self: center;
}

@media (max-width: 700px) {
  .list-view a {
    grid-template-columns: 1fr auto;
  }

  .list-view a .cat {
    display: none;
  }
}

/* Preview de proyecto al pasar el cursor sobre la lista — patrón cipher.tv
   /talents: la imagen ocupa la mayor parte de la pantalla, centrada, y sólo
   se mueve en Y (el eje X lo fija la retícula, para que no persiga al
   cursor como un tooltip sino que se sienta parte del layout). */
.hover-poster {
  position: fixed;
  inset: 0;
  z-index: var(--z-hero-ui);
  pointer-events: none;
  opacity: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity .5s var(--ease-out-quint);
}

.hover-poster__frame {
  width: min(64vw, 900px);
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--panel);
  box-shadow: 0 40px 120px -20px rgb(0 0 0 / .8);
}

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

@media (hover: none),
(max-width: 900px) {
  .hover-poster {
    display: none;
  }
}

/* Preview que sigue al mouse en "More work" (project.js) — calco de lo
   que hace REF al pasar el cursor sobre una tarjeta: un rectángulo 16:9
   de 324px con 4 marcas de esquina tipo visor de cámara, SIN etiqueta de
   texto (el cursor nativo ya es pointer, no hace falta un pill). A
   diferencia de .hover-poster de arriba —centrado, sólo eje Y, pensado
   para filas de sólo texto— acá va pegado al cursor en X e Y. */
.case-preview {
  position: fixed;
  top: 0;
  left: 0;
  z-index: var(--z-cursor);
  pointer-events: none;
  opacity: 0;
  transition: opacity .3s var(--ease-out-quint);
  will-change: transform;
}

.case-preview__frame {
  position: relative;
  width: 324px;
  aspect-ratio: 16 / 9;
  overflow: visible;
  background: var(--panel);
}

.case-preview__frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* las 4 marcas de esquina, 8×8px, calcadas de .squares en REF (4 <span>
   reales, mismo DOM que el original, no pseudo-elementos) */
.case-preview__corner {
  position: absolute;
  width: 8px;
  height: 8px;
  background: #000;
}

.case-preview__corner--tl {
  top: -4px;
  left: -4px;
}

.case-preview__corner--tr {
  top: -4px;
  right: -4px;
}

.case-preview__corner--br {
  bottom: -4px;
  right: -4px;
}

.case-preview__corner--bl {
  bottom: -4px;
  left: -4px;
}

@media (hover: none),
(max-width: 900px) {
  .case-preview {
    display: none;
  }
}

/* ── PÁGINA DE PROYECTO ─────────────────────────────────────── */
.project {
  position: relative;
  background: var(--bg);
}

.hero {
  position: sticky;
  top: 0;
  height: 100dvh;
  overflow: hidden;
  z-index: var(--z-hero);
}

.hero__media {
  position: absolute;
  inset: 0;
}

.hero__media img,
.hero__media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.hero__scrim {
  position: absolute;
  inset: 0;
  z-index: var(--z-scrim);
  pointer-events: none;
  background:
    radial-gradient(ellipse 70% 55% at 50% 50%, rgb(0 0 0 / .45), transparent 70%),
    linear-gradient(to bottom, rgb(0 0 0 / .55), transparent 20%, transparent 72%, rgb(0 0 0 / .72)),
    var(--scrim);
  /* velo permanente + viñeta central: contraste sobre cualquier foto */
}

/* Sin texto encima a propósito: la foto de portada ya lleva el nombre del
   proyecto (packaging, señalética, lo que sea) — copy encima competía con
   esa foto en vez de dejarla respirar. Sólo queda la barra de scroll. */
.hero__bar {
  position: absolute;
  inset: auto var(--margin) 1.25rem;
  z-index: var(--z-hero-ui);
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  pointer-events: none;
  color: rgb(255 255 255 / .6);
}

.hero__bar>* {
  pointer-events: auto;
}

@media (max-width: 700px) {

  /* La tabbar flotante (fixed, abajo de todo) le tapaba el "Scroll" y
     el contador — encontrado en la revisión de QA. */
  .hero__bar {
    bottom: calc(5.25rem + env(safe-area-inset-bottom));
  }
}

/* ── SCOPE CLARO DEL CASO (patrón ref.digital/work/dime) ──────
   Todo lo que va debajo del héroe se invierte remapeando tokens, no
   duplicando reglas: los componentes de adentro ya pintan con var(--bg),
   var(--muted) y var(--hairline), así que heredan el beige solos.
   El pie queda FUERA de este scope a propósito: negro arriba, beige en
   el medio, negro abajo — el caso queda encuadrado entre dos negros.  */
.case-scope {
  --bg: var(--case-bg);
  --fg: var(--case-fg);
  --neutral: var(--case-fg);
  --muted: var(--case-muted);
  --hairline: var(--case-hairline);

  position: relative;
  z-index: var(--z-body);
  background: var(--case-bg);
  color: var(--case-fg);
  transition: background-color var(--dur-case-lg) var(--ease-tw);
}

/* Titulares en negro como REF. El rojo no se va del caso: queda para los
   índices numerados y los hovers, que es lo que mantiene esta página
   dentro de este sitio y no dentro de ref.digital. */
.case-scope .display,
.case-scope .h1,
.case-scope .h2,
.case-scope .lead {
  color: var(--case-fg);
}

/* Índice numerado 01…NN (las secciones 01–05 de REF). Se arma con los
   bloques de tipo 'text' del proyecto, así que numera contenido real. */
.case-index {
  padding-top: clamp(3rem, 8vh, 5rem);
  padding-bottom: clamp(1rem, 3vh, 2rem);
}

.case-index__list {
  grid-column: 1 / span 6;
  list-style: none;
  padding: 0;
}

.case-index__list li {
  border-top: 1px solid var(--hairline);
}

.case-index__list li:last-child {
  border-bottom: 1px solid var(--hairline);
}

.case-index__list a {
  display: flex;
  align-items: baseline;
  gap: 1.75rem;
  padding: .7rem 0;
  transition: padding-left .5s var(--ease-out-expo), color var(--dur-fast);
}

.case-index__list a:hover,
.case-index__list a:focus-visible {
  padding-left: .6rem;
  color: var(--brand);
}

.case-index__num {
  color: var(--scrim);
  width: 2.5em;
  flex: none;
}

/* Resultados en cifras grandes (el bloque RESULTS de REF). Es opcional:
   sólo se dibuja si el proyecto declara `results` en data.js. No se
   inventan métricas de cliente. */
.case-results {
  padding-block: clamp(2rem, 6vh, 4rem);
}

.case-results__label {
  grid-column: 1 / span 3;
  color: var(--muted);
}

.case-results__list {
  grid-column: 5 / -1;
  list-style: none;
  padding: 0;
}

.case-results__list li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 2rem;
  border-bottom: 1px solid var(--hairline);
  padding-bottom: .35rem;
  margin-top: 1.1rem;
}

.case-results__value {
  font-family: var(--font-display);
  font-weight: var(--weight-title);
  font-size: clamp(2.5rem, 7vw, 5.5rem);
  line-height: .9;
  letter-spacing: -.02em;
}

.case-results__name {
  color: var(--muted);
  text-align: right;
}

/* "Our role on the project": las disciplinas del proyecto en la voz micro
   de REF, alineadas a la derecha contra un titular grande a la izquierda. */
.case-role {
  padding-block: clamp(3rem, 9vh, 6rem);
}

.case-role__title {
  grid-column: 1 / span 5;
}

.case-role__list {
  grid-column: 7 / -1;
  list-style: none;
  padding: 0;
}

.case-role__list li {
  border-top: 1px solid var(--hairline);
  padding: .6rem 0;
}

/* ── FILA DE METADATOS (patrón aino/nudie-jeans) ──────────────
   Justo debajo del héroe: tres columnas al ras, todo en voz micro —
   código+título · categoría+disciplinas · año+link. Nada de esto es
   "display": aino lo resuelve entero en mayúsculas de 12px, y esa
   contención es lo que hace que el héroe de arriba se sienta enorme
   en comparación. */
.meta-row {
  position: relative;
  z-index: var(--z-body);
  background: var(--bg);
  /* el header queda fijo y transparente por encima: este padding-top
     es lo que evita que su texto se superponga al primer contenido
     que aparece apenas el héroe termina de scrollear */
  padding-top: clamp(6rem, 14vh, 8.5rem);
  padding-bottom: clamp(1.5rem, 4vh, 2.5rem);
  border-bottom: 1px solid var(--hairline);
}

/* REF pone etiqueta y valor EN LÍNEA, no apilados: "CLIENT Dime MTL", con
   la etiqueta atenuada y el valor al lado. Cuatro pares repartidos a lo
   ancho, y el código de referencia pegado al margen derecho. */
.meta-row__col {
  grid-column: span 3;
  display: flex;
  gap: .5rem;
  align-items: baseline;
}

.meta-row__col--right {
  grid-column: span 3 / -1;
  justify-content: flex-end;
}

.meta-row__code {
  color: var(--muted);
  flex: none;
}

.meta-row__tags {
  color: var(--muted);
}

@media (max-width: 860px) {
  .meta-row__col {
    grid-column: span 6;
  }

  .meta-row__col--right {
    grid-column: span 6;
    justify-content: flex-start;
  }
}

@media (max-width: 520px) {

  .meta-row__col,
  .meta-row__col--right {
    grid-column: 1 / -1;
  }
}

/* Dentro del proyecto el scroll se calma: retícula editorial, sin sticky. */
.case {
  position: relative;
  z-index: var(--z-body);
  background: var(--bg);
  padding-block: var(--space-section);
}

.block {
  margin-block: var(--space-section);
}

/* El primer bloque ya viene separado por el padding de .case y por la fila
   de metadatos: sin esto el margen se suma al padding y abre un hueco muerto. */
.case>.block:first-child {
  margin-top: 0;
}

.case-scope .case {
  padding-top: clamp(2rem, 6vh, 4rem);
}

.block--single figure {
  grid-column: 2 / span 10;
}

/* Full-width single media (used by some cases) */
.block--single.block--full figure {
  grid-column: 1 / -1;
}

/* Pedido de seguimiento: gutter más ajustado, tipo pentagram.com/work/
   dataland, sin tocar el tamaño/recorte de las fotos (eso se revirtió
   — ver el historial). Ahí el espacio ENTRE columnas es más chico que
   el espacio entre filas (12px vs 24px, medido en vivo) — acá se copia
   esa misma relación asimétrica: el column-gap de las dos fotos de un
   par se achica a la mitad de --gutter, el row-gap entre pares queda
   como estaba (ya coincidía). El resto del sitio (.grid en general)
   no se toca — esto es sólo para .block--duo. */
.block--duo {
  column-gap: clamp(.5rem, .7vw, .75rem);
  row-gap: var(--gutter);
}

.block--duo figure {
  grid-column: span 6;
}

/* ── GALERÍA DE CASO (set grande de fotos) ──────────────────────────
   Pedido de seguimiento: 31 fotos apiladas en pares `duo` se leían
   como filas repetidas, no como una galería. CSS multi-column en vez
   de una grilla de alto fijo: cada foto conserva su proporción real y
   "cae" a la columna más corta (masonry sin una libraría ni JS de
   layout) — con fotos de retrato y paisaje mezcladas, como acá, una
   grilla de filas iguales las recortaba o dejaba huecos; columnas no. */
.case-gallery {
  grid-column: 1 / -1;
  column-count: 2;
  column-gap: var(--gutter);
}

.case-gallery__item {
  break-inside: avoid;
  margin-bottom: var(--gutter);
  overflow: hidden;
  background: var(--panel);
}

.case-gallery__item img,
.case-gallery__item video {
  width: 100%;
  height: auto;
  display: block;
}

@media (min-width: 700px) {
  .case-gallery {
    column-count: 3;
  }
}

.block__label {
  grid-column: 1 / span 3;
  color: var(--muted);
}

/* El número de sección va en rojo contra la etiqueta atenuada: es el
   ancla visual que enlaza cada bloque con el índice de arriba. */
.block__num {
  color: var(--brand);
  margin-right: .5rem;
}

.case-scope .block--text {
  scroll-margin-top: 6rem;
}

/* Párrafo de cuerpo grande y en negrita (patrón aino): el copy de un caso
   de estudio es contenido, no un pie de foto — se lee como una segunda
   voz de titular, no como prosa chica. */
.block__body {
  grid-column: 6 / span 6;
  /* hueco central deliberado */
  font-size: clamp(1.35rem, 2.6vw, 2.1rem);
  font-weight: 700;
  letter-spacing: -.015em;
  line-height: 1.25;
  color: var(--neutral);
}

.block figure {
  overflow: hidden;
  background: var(--panel);
}

.block figure img,
.block figure video {
  width: 100%;
  height: auto;
  display: block;
}

.block figcaption {
  margin-top: .5rem;
  color: var(--muted);
}

/* Hover de imagen al modo REF: la foto crece apenas dentro de su recorte
   y el pie aparece. El zoom va en la imagen y el recorte en la figura, así
   no hay reflow — sólo compositing. Mismo gesto para los bloques que son
   video en vez de foto — ver mediaTag() en project.js. */
.case-scope .block figure img,
.case-scope .block figure video {
  transition: transform .9s var(--ease-out-expo);
  will-change: transform;
}

.case-scope .block figure:hover img,
.case-scope .block figure:hover video {
  transform: scale(1.04);
}

.case-scope .block figcaption {
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-tw);
}

.case-scope .block figure:hover figcaption,
.case-scope .block figure:focus-within figcaption {
  opacity: 1;
}

@media (hover: none) {
  .case-scope .block figcaption {
    opacity: 1;
  }

  /* en táctil no hay hover que revele */
}

/* ── VIDEO-CASO ────────────────────────────────────────────────────
   Pedido de seguimiento: "debería mostrarse con audio e aislarse como
   un video importante, es el videocaso... que tenga un reproductor
   diseñado con el UI ya de la web" — el botón grande y el de mute
   reusan EXACTO el mismo lenguaje visual que .tools-hero__camera (más
   abajo en este archivo): círculo de vidrio esmerilado oscuro, blur,
   mismo grosor de borde y sombra — no una identidad nueva sólo para
   esta celda. La diferencia es de tamaño y de gesto: acá el botón
   grande VIVE centrado hasta el primer play y se retira del todo (no
   sólo se atenúa) mientras se está mirando, para que nada le compita
   al video real; el de mute es el mismo tratamiento chico que ya usa
   la cámara de Tools. */
.case-video {
  position: relative;
  cursor: pointer;
  background: #000;
}

.case-video video {
  width: 100%;
  height: auto;
  display: block;
}

/* Anula el zoom de hover genérico (.case-scope .block figure:hover
   video, ver arriba): ahí es un gesto pasivo sobre una foto que se
   mueve sola; acá el cuadro es un control, no algo que "reaccione" al
   pasar el mouse. */
.case-scope .block figure.case-video:hover video {
  transform: none;
}

/* Velo apenas visible antes de arrancar: sin esto el primer cuadro del
   video (lo que haya decodificado `preload="metadata"`) se ve como un
   fondo cualquiera en vez de leerse como "esto todavía no arrancó". */
.case-video::before {
  content: '';
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / .22);
  transition: opacity .5s var(--ease-case);
  pointer-events: none;
}

.case-video.is-playing::before {
  opacity: 0;
}

.case-video__toggle {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 78px;
  height: 78px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  color: #fff;
  background: rgb(20 12 12 / .55);
  border: 1px solid rgb(255 255 255 / .14);
  -webkit-backdrop-filter: blur(12px) saturate(1.3);
  backdrop-filter: blur(12px) saturate(1.3);
  box-shadow: 0 .5rem 1.5rem rgb(0 0 0 / .35);
  transition: transform .25s var(--ease-out-quint), opacity .3s var(--ease-case), background .25s var(--ease-tw);
}

.case-video__toggle:hover {
  transform: scale(1.06);
  background: rgb(20 12 12 / .75);
}

.case-video__toggle:active {
  transform: scale(.94);
}

.case-video__toggle-icon {
  width: 28px;
  height: 28px;
  fill: currentColor;
  /* el triángulo de play se ve corrido a la izquierda si no se
     recentra a mano: su propio bounding box no es simétrico */
  margin-left: 3px;
}

/* Reproduciendo: el botón grande se va del todo (no estorba) y sólo
   vuelve si el mouse pasa por encima del cuadro — mismo criterio que
   figcaption en .block figure más arriba, "aparece con la intención
   de tocarlo", no siempre puesto encima del video real. */
.case-video.is-playing .case-video__toggle {
  opacity: 0;
  pointer-events: none;
}

.case-video.is-playing:hover .case-video__toggle {
  opacity: 1;
  pointer-events: auto;
}

.case-video__mute {
  position: absolute;
  right: .85rem;
  bottom: .85rem;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  color: #fff;
  background: rgb(20 12 12 / .55);
  border: 1px solid rgb(255 255 255 / .14);
  -webkit-backdrop-filter: blur(12px) saturate(1.3);
  backdrop-filter: blur(12px) saturate(1.3);
  box-shadow: 0 .4rem 1rem rgb(0 0 0 / .3);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity .3s var(--ease-case), transform .3s var(--ease-case);
}

.case-video.is-playing .case-video__mute {
  opacity: 1;
  transform: none;
}

.case-video__mute:hover {
  transform: scale(1.06);
}

.case-video__mute-icon {
  width: 18px;
  height: 18px;
  fill: currentColor;
}

.case-video__badge {
  position: absolute;
  left: .85rem;
  top: .85rem;
  padding: .4rem .8rem;
  border-radius: 999px;
  color: #fff;
  background: rgb(20 12 12 / .55);
  border: 1px solid rgb(255 255 255 / .14);
  -webkit-backdrop-filter: blur(12px) saturate(1.3);
  backdrop-filter: blur(12px) saturate(1.3);
  text-transform: uppercase;
  letter-spacing: .06em;
  transition: opacity .3s var(--ease-case);
}

.case-video.is-playing .case-video__badge {
  opacity: 0;
}

.block--quote {
  grid-column: 2 / span 9;
}

.quote {
  font-weight: 700;
  font-size: var(--text-h2);
  letter-spacing: -.02em;
  line-height: 1.1;
}

.quote::before {
  content: '“';
  color: var(--scrim);
}

.quote::after {
  content: '”';
  color: var(--scrim);
}

/* Sección asimétrica (patrón "between" de aino): una figura grande y una
   chica, nunca mitad y mitad — es lo que distingue esto de .block--duo. */
.block--split {
  align-items: start;
}

.block--split .block__big {
  grid-column: 1 / span 7;
}

.block--split .block__small {
  grid-column: 9 / span 4;
  margin-top: clamp(2rem, 8vh, 5rem);
}

@media (max-width: 900px) {

  .block--single figure,
  .block--duo figure,
  .block__label,
  .block__body,
  .block--quote,
  .block--split .block__big,
  .block--split .block__small {
    grid-column: 1 / -1;
    margin-top: 0;
  }

  /* Las listas del caso van a caja completa: a media caja quedan flotando
     con medio ancho muerto al lado. */
  .case-index__list,
  .case-results__label,
  .case-results__list,
  .case-role__title,
  .case-role__list {
    grid-column: 1 / -1;
  }

  .case-role__list {
    margin-top: 1.5rem;
  }

  .case-index {
    padding-bottom: 0;
  }
}

/* Por debajo de 700px las imágenes pareadas del caso (.block--duo,
   .block--split) dejan de apilarse verticalmente —ya venían a columna
   completa por la regla de arriba— y pasan a carrusel con snap, mismo
   gesto que .gallery__list: se swipea entre las dos en vez de scrollear
   una debajo de la otra. */
@media (max-width: 700px) {

  .block--duo,
  .block--split.grid {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    gap: 1rem;
    scrollbar-width: none;
  }

  .block--duo::-webkit-scrollbar,
  .block--split.grid::-webkit-scrollbar {
    display: none;
  }

  .block--duo>figure,
  .block--split .block__big,
  .block--split .block__small {
    flex: 0 0 86%;
    scroll-snap-align: center;
    margin-top: 0;
  }
}

/* ── MORE WORK (calco 1:1 de ref.digital/work/dime "More work") ─
   Medido en vivo con Playwright/CDP a 1600px, no a ojo: fondo blanco,
   texto negro — un TERCER tono además del negro (héroe) y beige (caso).
   Título "More work" en minúscula real (no mayúscula, a pesar de que
   .h2 en el resto del sitio fuerza uppercase), botón de esquinas rectas
   con borde de 1px, grid de EXACTO 2 columnas (794px c/u a 1600px de
   viewport — confirmado con getBoundingClientRect, no supuesto), imagen
   1:1 con borde negro de 1px superpuesto, y la leyenda en un sub-grid de
   2 columnas: título a la izquierda, descripción+código apilados a la
   derecha — los tres en 12px mayúscula, sin tracking extra (Aeonik no
   está disponible en este sitio).

   Nota histórica, ya no aplica: el título "More work" fue Plus Jakarta
   Sans desde el principio (Habitus en el original se tradujo a Jakarta,
   no a la serif de display que usaba el resto de los .h2 del sitio en
   ese momento) — era la ÚNICA excepción a la regla vieja de "titulares
   en serif". Con el sistema tipográfico consolidado a una sola familia
   (ver tokens.css) esa excepción desapareció sola: ahora TODO el sitio
   usa Jakarta, así que este título ya no es distinto de nada. */
.case-nav {
  position: relative;
  z-index: var(--z-body);
  background: #ffffff;
  color: #000000;
  padding: clamp(2rem, 6vh, 3.5rem) var(--margin) clamp(3rem, 8vh, 5rem);
}

.case-nav__head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: clamp(1.5rem, 4vh, 2.5rem);
}

.case-nav__head .h2 {
  font-family: var(--font-ui);
  text-transform: none;
  font-size: clamp(1.5rem, 3vw, 2rem);
  font-weight: 300;
  letter-spacing: -.02em;
  color: #000;
}

.case-nav__all {
  font-family: var(--font-mono);
  font-weight: var(--weight-subtitle);
  font-size: .625rem;
  text-transform: none;
  letter-spacing: normal;
  color: #000;
  border: 1px solid #000;
  padding: .56rem .75rem;
  transition: background-color var(--dur-fast), color var(--dur-fast);
}

.case-nav__all:hover,
.case-nav__all:focus-visible {
  background: #000;
  color: #fff;
}

.case-nav__grid {
  list-style: none;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 2rem 1px;
}

.case-nav__card figure {
  position: relative;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  background: #f2f2f2;
}

/* el borde va en un ::after superpuesto, no en la figure: así no le come
   1px de alto/ancho a la imagen ni desplaza el recorte del object-fit */
.case-nav__card figure::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid #000;
  pointer-events: none;
}

.case-nav__card img,
.case-nav__card video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.case-nav__meta {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 1rem;
  padding-top: .5rem;
}

.case-nav__title,
.case-nav__blurb,
.case-nav__code {
  font-family: var(--font-mono);
  font-size: .75rem;
  font-weight: var(--weight-subtitle);
  text-transform: uppercase;
  letter-spacing: normal;
  line-height: 1.2;
  color: #000;
}

.case-nav__blurb {
  margin-bottom: .75rem;
}

@media (max-width: 700px) {
  .case-nav__grid {
    grid-template-columns: 1fr;
    gap: 2.5rem;
  }

  .case-nav__meta {
    grid-template-columns: 1fr;
    gap: .5rem;
  }
}

/* Preview que sigue al mouse — es lo que en REF aparece al pasar el
   cursor sobre la tarjeta: un rectángulo 16:9 con 4 marcas de esquina
   tipo visor de cámara. Ver .case-preview más arriba para el mecanismo
   de seguimiento; acá sólo el vestido visual que hace juego con REF. */
.case-nav__card {
  position: relative;
}

/* ── SEE FOR YOURSELF: bento de 4 celdas ──────────────────────────
   Calco de la ESTRUCTURA de incredibles.dev (celda con pila física +
   celda de cita en oscuro + showreel chico + lista de entregas), no de
   su contenido: fotos, citas y copy son propios.

   Flex-wrap y no grid, igual que la referencia: las celdas tienen
   proporciones muy distintas (una alta, una chica, una ancha) y con
   wrap se reacomodan solas por ancho sin declarar áreas para cada
   breakpoint. */
.s-work {
  padding: clamp(3rem, 8vh, 6rem) var(--margin) clamp(4rem, 10vh, 7rem);
}

/* Pedido de seguimiento: esta sección medía sus celdas con flex-basis
   a mano (26rem/22rem) en vez de la retícula de 12 columnas que usa el
   resto del sitio (.grid, var(--cols)/var(--gutter)/var(--margin)) —
   por eso el ancho de "See for yourself" contra la cita no se leía
   como una proporción limpia comparada con cualquier otra sección de
   la página. Ahora reparte 7/5 sobre las mismas 12 columnas. */
.s-work__grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--gutter);
  max-width: 90rem;
  margin-inline: auto;
}

.s-work__cell {
  border-radius: 1.25rem;
  overflow: hidden;
  position: relative;
}

/* Celda 1 — pila interactiva. El alto es fijo (no aspect-ratio) porque
   las paredes de la simulación se construyen con el alto en px: un alto
   que cambia con el ancho obligaría a reconstruir el mundo en cada
   resize horizontal. */
.s-work__cell--execution {
  grid-column: span 7;
  min-height: 30rem;
  background: #fafafa;
  padding: clamp(1.5rem, 3vw, 2.5rem);
  display: flex;
  flex-direction: column;
}

.s-work__title {
  margin: 0;
  position: relative;
  z-index: 1;
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: clamp(2rem, 3.4vw, 3.25rem);
  line-height: 1.02;
  letter-spacing: -.045em;
  text-transform: none;
  color: #111;
  pointer-events: none;
}

/* El canvas ocupa lo que sobra bajo el título y se estira al ancho
   completo de la celda, ignorando su padding: la pila tiene que poder
   apilarse contra los bordes reales, no contra un margen interior. */
/* Ya no se agarra nada (el bloom de js/modules/bloom.js no es
   arrastrable, sólo reacciona a que el cursor pase) — sin cursor:grab
   ni touch-action restringido: en táctil el scroll de la página pasa
   normal por encima, no hay gesto propio que proteger. */
.s-work__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* Celda 2 — cita. Trama de puntos hecha con un radial-gradient
   repetido, no una imagen: son 2 líneas de CSS y escala sola. */
.s-work__cell--quote {
  grid-column: span 5;
  min-height: 30rem;
  background: #1c1c1c;
  color: #f2efef;
  padding: clamp(1.75rem, 3vw, 2.75rem);
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.s-work__cell--quote::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: radial-gradient(rgb(255 255 255 / .07) 1px, transparent 1px);
  background-size: 5px 5px;
  pointer-events: none;
}

/* Capa de tinta reactiva al cursor (js/modules/fluid.js). Calcada en
   posición de incredibles.dev: `absolute; inset:0` DENTRO de la
   tarjeta, debajo del texto. `pointer-events:none` porque quien
   escucha el mouse es la celda entera, no el canvas: si el canvas se
   quedara los eventos, la tinta se cortaría al pasar por encima de la
   propia cita. */
.s-work__fluid {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
  /* Es una capa de fondo detrás de una cita: tiene que notarse que la
     tarjeta responde, no competir con lo que dice. */
  opacity: .4;
}

/* La trama de puntos va por ENCIMA de la tinta (no debajo): así el
   fluido se ve tramado, como si pasara por detrás de una malla, en vez
   de taparla. */
.s-work__cell--quote::before {
  z-index: 1;
}

.s-work__quote-mark,
.s-work__quote {
  z-index: 2;
}

.s-work__quote-mark {
  position: relative;
  font-family: var(--font-display);
  font-weight: var(--weight-title);
  font-size: 3rem;
  line-height: 0;
  color: rgb(242 239 239 / .55);
}

.s-work__quote {
  position: relative;
  margin: 0;
}

.s-work__quote p {
  position: relative;
  margin: 0;
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: clamp(1.6rem, 2.9vw, 2.75rem);
  line-height: 1.06;
  letter-spacing: -.045em;
}

/* ── BARRIDO GLITCH / RGB SPLIT ────────────────────────────────────
   Pedido a partir de un video de referencia: un halo recorre la cita
   en loop y por un instante la "duplica" en cian y magenta con offset,
   como una aberración cromática que pasa de largo. `content:
   attr(data-text)` es lo que permite que el ::before/::after repitan
   el mismo texto sin escribirlo dos veces en el HTML — swork.js copia
   el textContent real a data-text una sola vez al montar, así el copy
   sigue viviendo en un solo lugar.

   La franja que "revela" el color es una máscara (no un recorte fijo):
   un gradiente angosto, transparente en los bordes y opaco en el
   medio, que se desliza de punta a punta del texto vía mask-position.
   Donde la máscara es opaca se ve la copia de color; en el resto, no
   — así el efecto se lee como una luz que pasa, no como un texto
   coloreado todo el tiempo. mix-blend-mode:screen es lo que hace que
   el cian/magenta se sumen a la letra en vez de taparla. */
.s-work__quote p::before,
.s-work__quote p::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  -webkit-mask-image: linear-gradient(100deg, transparent 42%, #fff 50%, transparent 58%);
  mask-image: linear-gradient(100deg, transparent 42%, #fff 50%, transparent 58%);
  -webkit-mask-size: 260% 100%;
  mask-size: 260% 100%;
  -webkit-mask-position: -160% 0;
  mask-position: -160% 0;
  animation: quote-glitch-sweep 2.8s linear infinite;
}

.s-work__quote p::before {
  color: #35e2ff;
  transform: translate(3px, 0);
}

.s-work__quote p::after {
  color: #ff3fc0;
  transform: translate(-3px, 0);
  /* Medio ciclo desfasada de la capa cian: sin este delay las dos
     franjas viajan pegadas y el "split" de color no se distingue del
     texto base, se ve blanco en vez de fringing cian/magenta. */
  animation-delay: -1.4s;
}

@keyframes quote-glitch-sweep {
  from { -webkit-mask-position: -160% 0; mask-position: -160% 0; }
  to   { -webkit-mask-position: 160% 0; mask-position: 160% 0; }
}

@media (prefers-reduced-motion: reduce) {

  .s-work__quote p::before,
  .s-work__quote p::after {
    content: none;
  }
}

.s-work__quote-by {
  margin-top: 1.25rem;
  color: rgb(242 239 239 / .6);
  text-transform: none;
  letter-spacing: .02em;
}

/* Celda 3 — unificada con .about-hero (pedido explícito: "deberían
   ser lo mismo"). A ancho completo y sola en su fila: era la compañera
   de la celda de "recent delivery" (eliminada), que le daba con quién
   compartir el resto del ancho. */
.s-work__cell--showreel {
  grid-column: 1 / -1;
  min-height: 16rem;
  background: #111;
  display: block;
}

.s-work__showreel-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: .55;
  transition: opacity var(--dur-fast), transform var(--dur-base) var(--ease-out-expo);
}

.s-work__cell--showreel:hover .s-work__showreel-video {
  opacity: .7;
  transform: scale(1.04);
}

/* Velo permanente — ya no es sólo un video con un pill flotando, ahora
   carga texto real (el mismo headline de .about-hero) que tiene que
   leerse SIEMPRE, no sólo cuando el hover baja la opacidad del video.
   Mismo criterio que .about-hero__scrim: el texto no puede depender de
   qué cuadro del video le toque atrás.

   Pedido de seguimiento: "ABOUT" arriba a la izquierda, el headline
   abajo a la izquierda — pero el botón "See the history" no viaja con
   el headline, se queda en el medio de la celda (pedido explícito de
   seguimiento sobre esto mismo). Por eso el botón salió de este flujo
   por completo: vive en .s-work__showreel-ctawrap, un hermano aparte
   centrado con position:absolute, y acá sólo quedan el label (arriba)
   y el headline (abajo) — dos hijos, separados a los extremos por
   `justify-content: space-between`. */
.s-work__showreel-content {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
  padding: 2rem;
  text-align: left;
  background: linear-gradient(to bottom,
    rgb(8 8 12 / .55) 0%, rgb(8 8 12 / .35) 40%, rgb(8 8 12 / .55) 100%);
}

.s-work__showreel-label {
  margin: 0;
  color: rgb(255 255 255 / .7);
}

.s-work__showreel-bottom {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

/* Pedido de seguimiento: "esta muy pequeño, deberia ser mas grande" —
   tenía razón: el clamp() estaba invertido (mínimo 1.625rem, máximo
   1.5rem más chico que el mínimo), así que el tamaño quedaba
   CONSTANTE en el piso de siempre, sin crecer nunca con el viewport
   —clamp(MIN, VAL, MAX) con MIN > MAX se resuelve siempre a MIN, ver
   la spec de CSS Values—. Reordenado y subido de piso/techo, mismo
   criterio de escala que --text-h2 (tokens.css) pero recortado porque
   esta celda es más chica que un titular de sección entero. */
.s-work__showreel-headline {
  margin: 0;
  max-width: 38ch;
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: clamp(1.5rem, 2.6vw, 2.25rem);
  line-height: 1.25;
  letter-spacing: -.015em;
  color: #fff;
}

/* Centrado independiente del label/headline (ver el comentario de
   arriba) — el translate(-50%,-50%) hace el centrado real; el magnet
   de abajo se suma encima en el hijo, no acá, así los dos transforms
   no se pisan entre sí. */
.s-work__showreel-ctawrap {
  position: absolute;
  z-index: 2;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

/* Botón magnético: --magnet-x/y las escribe swork.js cuadro a cuadro,
   sumadas al centrado de .s-work__showreel-ctawrap. Sin transición a
   propósito — la suavidad ya la pone el propio loop del JS, y una
   transición encima agregaría un segundo retardo que se siente como
   lag. */
.s-work__showreel-cta {
  position: relative;
  display: block;
  transform: translate(var(--magnet-x, 0), var(--magnet-y, 0));
  padding: .5rem 1.1rem;
  border: 1px solid rgb(255 255 255 / .7);
  border-radius: 999px;
  color: #fff;
  background: rgb(0 0 0 / .25);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  white-space: nowrap;
}

/* Por debajo de 760px las dos celdas altas dejan de compartir fila —
   a media caja la pila queda tan angosta que las miniaturas no tienen
   dónde apilarse y se traban contra las paredes. */
@media (max-width: 760px) {
  .s-work__cell--execution,
  .s-work__cell--quote {
    grid-column: 1 / -1;
    min-height: 24rem;
  }

  .s-work__cell--showreel {
    min-height: 15rem;
  }

  .s-work__showreel-content {
    padding: 1.5rem;
  }
}


/* ── TOOLS ──────────────────────────────────────────────────────
   1:1 de Figma (archivo "CV", nodo 1554:617, "tools-hero"): headline +
   franja de dos imágenes a pantalla completa. `.tools-hero__head` vive
   en la misma retícula de 12 columnas que el resto del sitio (headline
   a la izquierda, lista de herramientas + "More" a la derecha, calco
   del layout original) — la franja de imágenes de abajo, en cambio,
   sale de la retícula Y del contenedor de 90rem que usa .s-work__grid:
   en Figma llega borde a borde del viewport, sin gap ni esquina
   redondeada entre las dos mitades — corregido tras feedback: la
   primera pasada la había metido adentro del mismo max-width que
   "See for yourself", que no es lo que pedía el diseño acá. */
.tools-hero__head {
  row-gap: clamp(1.5rem, 4vh, 2.5rem);
}

.tools-hero__title {
  grid-column: 1 / span 8;
  margin: 0;
}

/* Pedido de seguimiento: la lista de herramientas y "More" vivían en
   la misma fila (space-between) — con las seis herramientas reales de
   ABOUT.tools (mucho más largo que el placeholder corto del Figma) esa
   fila se quedaba sin aire: el texto envolvía y terminaba pegado
   contra "More". Pasan a apilarse con un gap real entre sí en vez de
   forzar una sola línea. `align-self: end` alinea el bloque entero
   contra el piso de la retícula — a la altura de la última línea del
   titular, que es donde vive en Figma — en vez de quedar pegado
   arriba, contra la primera línea. */
.tools-hero__meta {
  grid-column: 9 / -1;
  align-self: end;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .85rem;
}

.tools-hero__tools {
  color: var(--muted);
}

.tools-hero__more {
  flex: none;
  color: var(--fg);
  transition: opacity var(--dur-fast);
}

.tools-hero__more:hover {
  opacity: .6;
}

/* Pedido de seguimiento: sin margen propio, la franja de imágenes
   arrancaba pegada contra el piso de .tools-hero__head — a un
   viewport ancho, donde el titular de dos líneas es más alto en px
   absolutos, terminaba tocando/tapando la segunda línea. Mismo
   clamp() que usaba antes de que esta franja pasara a full-bleed (ver
   el comentario grande más arriba), sólo que ahora como margin-top en
   vez de vivir adentro del padding-inline que ya no tiene. */
.tools-hero__media {
  display: flex;
  margin-top: clamp(2.5rem, 6vh, 4.5rem);
}

.tools-hero__cell {
  flex: 1 1 0;
  /* Alto real del diseño: el frame de Figma es 1920×1080 y la franja
     de imagen arranca ~40% abajo y sigue más allá del borde del frame
     — no es una banda chica, es prácticamente la pantalla completa.
     90vh en vez de un clamp() chico como el resto de las celdas del
     sitio (esas son tarjetas; esta es un héroe de imagen). */
  height: 90vh;
  min-height: 32rem;
  margin: 0;
  overflow: hidden;
  position: relative;
  background: #0a0a0a;
}

.tools-hero__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* La celda del patrón apila DOS elementos .tools-hero__img — el
   <canvas> del shader de verdad (ver js/modules/stitch.js) arriba del
   <video> de respaldo (el loop de archivo, tools-source.mp4) — así
   que acá sí necesitan salir del flujo normal y superponerse. El
   canvas nace en opacity:0 y stitch.js le agrega `is-live` recién
   cuando subió su primer cuadro real: sin esto se veía un flash del
   celeste de 1×1px de arranque. Si WebGL no está disponible, `is-live`
   nunca llega y el <video> de abajo queda como el resultado final —no
   como un fallback que se nota— porque ya trae su propio autoplay. */
.tools-hero__cell--pattern .tools-hero__img {
  position: absolute;
  inset: 0;
}

.tools-hero__stitch {
  z-index: 1;
  opacity: 0;
  transition: opacity .6s var(--ease-tw);
}

.tools-hero__stitch.is-live {
  opacity: 1;
}

/* Botón de cámara: chico y en la esquina a propósito (pedido de
   seguimiento: "un pequeño botón") — mismo lenguaje visual que la
   tabbar (vidrio esmerilado oscuro, blur) en vez de un botón nuevo
   con su propia identidad. Tres estados además del default:
   is-loading (pidiendo permiso), is-active (cámara prendida — anillo
   de marca), is-denied (el navegador lo negó, un shake y vuelve solo
   al estado default a los 2.2s — ver stitch.js). */
.tools-hero__camera {
  position: absolute;
  z-index: 2;
  right: .85rem;
  bottom: .85rem;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  color: #fff;
  background: rgb(20 12 12 / .55);
  border: 1px solid rgb(255 255 255 / .14);
  -webkit-backdrop-filter: blur(12px) saturate(1.3);
  backdrop-filter: blur(12px) saturate(1.3);
  box-shadow: 0 .4rem 1rem rgb(0 0 0 / .3);
  transition: transform .2s var(--ease-out-quint), box-shadow .25s var(--ease-tw), background .25s var(--ease-tw);
}

.tools-hero__camera:hover {
  transform: scale(1.06);
}

.tools-hero__camera:active {
  transform: scale(.94);
}

.tools-hero__camera-icon {
  width: 18px;
  height: 18px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.tools-hero__camera.is-loading .tools-hero__camera-icon {
  animation: tools-camera-pulse 1s ease-in-out infinite;
}

@keyframes tools-camera-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .35; }
}

.tools-hero__camera.is-active {
  background: rgb(20 12 12 / .75);
  box-shadow: 0 0 0 2px var(--brand), 0 .4rem 1rem rgb(0 0 0 / .3);
}

.tools-hero__camera.is-denied {
  animation: tools-camera-denied .4s var(--ease-tw);
}

@keyframes tools-camera-denied {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-4px); }
  75% { transform: translateX(4px); }
}

@media (max-width: 900px) {
  .tools-hero__title {
    grid-column: 1 / -1;
  }

  .tools-hero__meta {
    grid-column: 1 / -1;
    align-self: auto;
    align-items: flex-start;
  }
}

@media (max-width: 700px) {
  .tools-hero__media {
    flex-direction: column;
  }

  .tools-hero__cell {
    height: 60vh;
    min-height: 18rem;
  }
}


/* ── FOOTER ─────────────────────────────────────────────────── */
.footer {
  position: relative;
  z-index: var(--z-body);
  background: var(--bg);
  padding: var(--space-lg) var(--margin) max(1.25rem, env(safe-area-inset-bottom));
  display: grid;
  gap: var(--space-lg);
  border-top: 1px solid var(--hairline);
}

/* La tabbar flota fija sobre el contenido a propósito —igual que en la
   referencia— salvo acá abajo del todo: sin este aire extra el pie
   quedaba atrapado detrás de la barra sin forma de scrollear más allá. */
@media (max-width: 700px) {
  .footer {
    padding-bottom: calc(4.5rem + max(1.25rem, env(safe-area-inset-bottom)));
  }
}

.footer__cols {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-lg);
  justify-content: space-between;
}

.footer__col {
  display: grid;
  gap: .45rem;
  align-content: start;
}

.inline-links {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem .9rem;
}

/* ── LOGOTIPO DEL PIE: full-bleed, siempre ASCII ──────────────
   El margen negativo rompe el padding horizontal del footer para que el
   logotipo llegue de borde a borde ("big type footer"). Son dos capas
   apiladas y complementarias: abajo el logo nítido, arriba el canvas con
   el ASCII, y una máscara radial en el mismo centro y radio le hace un
   agujero al de abajo — así nunca se superponen y el logotipo no se
   deforma, sólo cambia de materia adentro del círculo.               */
.footer__big {
  margin-inline: calc(var(--margin) * -1);
}

.footer__big .display {
  color: var(--brand);
}

.bigmark {
  position: relative;
  width: 100%;
  aspect-ratio: 350 / 56;
  /* el viewBox de assets/logo.svg */
  /* Ojo: acá la ruta es relativa a la PÁGINA (la lee el fetch de
     ascii-mark.js), no a esta hoja como en la máscara de .mark. */
  --mark-src: url("assets/logo.svg");
  --mx: 50%;
  --my: 50%;
  --r: 0px;
}

.bigmark__solid {
  position: absolute;
  inset: 0;
  -webkit-mask: radial-gradient(circle var(--r) at var(--mx) var(--my), transparent 55%, #000 100%);
  mask: radial-gradient(circle var(--r) at var(--mx) var(--my), transparent 55%, #000 100%);
}

.bigmark__solid .mark {
  width: 100%;
  height: 100%;
}

.bigmark__ascii {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  /* el hover lo escucha .bigmark */
}

.footer__bottom {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  justify-content: space-between;
  align-items: flex-end;
  border-top: 1px solid var(--hairline);
  padding-top: 1rem;
}

/* ── ENLACE CON ROLL + SUBRAYADO ────────────────────────────── */
.link {
  position: relative;
  display: inline-block;
  isolation: isolate;
}

.link__clip {
  position: relative;
  display: inline-block;
  overflow: hidden;
  padding-inline: .12em;
  margin-inline: -.12em;
  /* sólo horizontal: ver README */
}

/* gap explícito porque flex + flex-wrap descarta los nodos de texto de
   solo-espacio entre .word: sin esto, un link de dos palabras ("All
   projects") se pega en una sola ("Allprojects"). En em para escalar con
   el tamaño del link donde se use (nav grande, micro, etc). */
.link__text {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  line-height: 1.35;
  gap: 0 .28em;
}

.link__text .char {
  position: relative;
  display: inline-block;
  transition: transform .5s var(--ease-out-quint);
  transition-delay: calc(var(--i) * 20ms);
}

.link__text .char::after {
  content: attr(data-c);
  position: absolute;
  left: 0;
  top: 100%;
}

.link:hover .char,
.link:focus-visible .char {
  transform: translateY(-100%);
}

.link__underline {
  position: absolute;
  bottom: .14em;
  left: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform .5s var(--ease-out-quint);
}

.link:hover .link__underline,
.link:focus-visible .link__underline {
  transform: scaleX(1);
  transform-origin: left;
}

/* ── REVEALS ────────────────────────────────────────────────── */
/* El aire para que --lh-display (0.82) no corte los glifos va en el span
   INTERNO, no en el recorte: así el span sigue siendo exactamente la altura
   que translateY(110%) tiene que salvar, y en reposo no asoma nada. Ponerlo
   en .line agranda la caja de recorte y deja ver la línea a medio revelar. */
.line {
  display: block;
  overflow: hidden;
}

.line>span {
  display: block;
  padding-block: .16em;
  margin-block: -.16em;
}

.js .line>span {
  transform: translateY(110%);
  transition: transform var(--dur-slow) var(--ease-out-expo);
  transition-delay: calc(var(--i, 0) * 85ms);
}

.js .is-in .line>span {
  transform: none;
}

/* Pedido de seguimiento: "el scroll podría ser más fluido, con un poco
   más de efectos al ir apareciendo las cosas" — [data-reveal] es el
   caballito de batalla de todo el sitio (home, work, project lo usan
   16 veces entre los tres, no sólo acá), así que es el punto de mayor
   impacto para subir la sensación de fluidez sin inventar un sistema
   nuevo. Sumaba sólo opacity+translateY; ahora entra también con un
   leve scale (.96→1, antes sólo en móvil) y un desenfoque que se
   enfoca (blur(6px)→0) — el "focus pull" es lo que hace que un
   reveal se sienta premium y no sólo un fade, y el scale le da algo
   de profundidad al asentarse. Duración un poco más larga (antes
   --dur-base=.6s) para que el gesto se lea entero y no como un
   parpadeo. blur() es liviano acá: sólo corre UNA vez por elemento
   (is-in se pone y se desengancha, ver initScrollFx) y nunca en
   scroll continuo, así que no hay costo de repintado por cuadro. */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(1.75rem) scale(.96);
  filter: blur(6px);
  transition: opacity var(--dur-reveal) var(--ease-out-expo), transform var(--dur-reveal) var(--ease-out-expo), filter var(--dur-reveal) var(--ease-out-expo);
  transition-delay: calc(var(--i, 0) * 70ms);
}

.js [data-reveal].is-in {
  opacity: 1;
  transform: none;
  filter: blur(0);
}

/* En móvil el mismo reveal viaja todavía más —de pie chico a tamaño
   real—, para que el scroll se sienta más vivo que en desktop, donde
   el gesto ya funciona con el cursor cerca. */
@media (max-width: 700px) {
  .js [data-reveal] {
    transform: translateY(2.25rem) scale(.94);
  }

  .js [data-reveal].is-in {
    transform: none;
  }
}


/* ── SCROLL-FX: el vocabulario de ref.digital/work/dime ───────────
   Los estados de reposo y de llegada; quién los dispara y por qué
   estos números y no otros está en js/modules/scroll-fx.js. La curva
   es --ease-case (cubic-bezier(.32,.94,.6,1)), la misma que ya se
   había medido en REF para el interior de proyecto: es la ÚNICA de
   toda la referencia, desde el borde de 1px hasta la foto grande. */

/* 1 — HAIRLINE QUE SE DIBUJA.
   El retardo de .3s no es decorativo: es lo que hace que el marco
   termine de trazarse justo cuando el contenido ya está entrando, en
   vez de que las dos cosas arranquen juntas y se pisen. */
.js [data-fx-line] {
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .7s var(--ease-case) .3s;
}

.js [data-fx-line="v"] {
  transform: scaleY(0);
  transform-origin: top center;
}

.js [data-fx-line].is-in {
  transform: scale(1);
}

/* 2 — MEDIA EN DOS CAPAS.
   El recorte descansa en .95 y crece a 1; la foto de adentro descansa
   sobremedida y se acerca a su tamaño real mientras el parallax la
   corre. Son dos transforms en dos elementos distintos a propósito:
   si vivieran en el mismo, la transición de entrada y el scrub por
   frame se pelearían por la misma propiedad y la foto quedaría
   siempre un cuadro atrás del scroll.

   --fx-scale va registrada para que el navegador la interpole como
   número (una var común saltaría de golpe). Donde @property no exista
   el salto es instantáneo y el parallax sigue funcionando igual. */
@property --fx-scale {
  syntax: '<number>';
  inherits: false;
  initial-value: 1.24;
}

/* El hover viaja por su propia variable en vez de por un segundo
   `transform`: el elemento ya tiene uno (parallax + entrada) y la
   segunda regla lo pisaría entero. Multiplicando escalas, hover y
   scroll conviven sin saber uno del otro. */
@property --fx-hover {
  syntax: '<number>';
  inherits: false;
  initial-value: 1;
}

.js [data-fx-media] {
  overflow: hidden;
  transform: scale(.95);
  transition: transform .9s var(--ease-case);
}

.js [data-fx-media].is-in {
  transform: scale(1);
}

.js [data-fx-media] [data-fx-inner] {
  --fx-scale: 1.24;
  --fx-hover: 1;
  --fx-shift: 0%;
  transform: translate3d(0, var(--fx-shift), 0) scale(calc(var(--fx-scale) * var(--fx-hover)));
  transition: --fx-scale 1.2s var(--ease-case), --fx-hover 1s var(--ease-out-expo);
  will-change: transform;
}

.js [data-fx-media].is-in [data-fx-inner] {
  --fx-scale: 1.12;
}

/* Variante "flat" — para la galería. Ahí la foto se ve ENTERA en su
   proporción natural (decisión vieja y deliberada: ver el bloque
   .gallery__media más arriba), y la sobremedida de REF la recortaría
   un 12% arriba y abajo de forma permanente. Así que esta variante
   conserva la entrada —el recorte crece, la foto se asienta— pero
   aterriza en scale(1) exacto y no lleva parallax: en reposo no falta
   ni un pixel de la imagen. */
.js [data-fx-media="flat"] [data-fx-inner] {
  --fx-scale: 1.08;
}

.js [data-fx-media="flat"].is-in [data-fx-inner] {
  --fx-scale: 1;
}

/* Variante "bleed" — el nombre es de REF, que marca así sus fotos a
   sangre. El recorte NO se achica: estos son fondos `inset:0` que
   cubren la sección entera, y un scale(.95) dejaría 5% de hueco
   alrededor mostrando lo que hay debajo. Se mueve sólo la capa de
   adentro, que es justamente para lo que existe la sobremedida. */
.js [data-fx-media="bleed"] {
  transform: none;
}

/* 3 — TEXTO POR PALABRAS.
   inline-block porque un <span> en flujo no acepta transform. El
   escalonado corto (40ms) es deliberado: con 8-10 palabras, un paso
   más largo hace que la última llegue cuando el ojo ya se fue. */
.js .fx-word {
  display: inline-block;
  opacity: 0;
  transform: translateY(.42em);
  transition: opacity .6s var(--ease-case), transform .6s var(--ease-case);
  transition-delay: calc(var(--fx-i, 0) * 40ms);
}

.js [data-fx-words].is-in .fx-word {
  opacity: 1;
  transform: none;
}

/* 4 — BLOQUE QUE SUBE.
   Más contenido y más lento que el [data-reveal] de siempre (que se
   queda para lo que ya lo usaba): REF mueve poco y tarda, no al
   revés. El índice lo reparte [data-fx-stagger] desde el JS. */
.js [data-fx-rise] {
  opacity: 0;
  transform: translateY(2rem);
  transition: opacity .8s var(--ease-case), transform .8s var(--ease-case);
  transition-delay: calc(var(--fx-i, 0) * 90ms);
}

.js [data-fx-rise].is-in {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {

  .js [data-fx-line],
  .js [data-fx-media],
  .js [data-fx-rise],
  .js .fx-word {
    transform: none;
    opacity: 1;
  }

  .js [data-fx-media] [data-fx-inner] {
    --fx-scale: 1;
    transform: none;
  }
}

/* ── OVERLAY DE CONTACTO ────────────────────────────────────── */
.sheet {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  background: var(--brand);
  color: var(--bg);
  clip-path: inset(0 0 100% 0);
  visibility: hidden;
  transition: clip-path var(--dur-curtain) var(--ease-in-out-expo), visibility 0s var(--dur-curtain);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: .5rem;
  padding: var(--space-lg) var(--margin);
  overflow-y: auto;
}

.sheet.is-open {
  clip-path: inset(0 0 0 0);
  visibility: visible;
  transition-delay: 0s;
}

.sheet__item {
  transform: translateY(110%);
  transition: transform .7s var(--ease-out-expo);
}

.sheet.is-open .sheet__item {
  transform: none;
  transition-delay: calc(.2s + var(--i, 0) * .06s);
}

.sheet__close {
  position: absolute;
  top: 1.15rem;
  right: var(--margin);
}

/* `.h2` trae `color: var(--brand)` a nivel global — mientras --brand fue
   rojo, un h2 rojo sobre este panel crema leía bien. Desde que --brand
   pasó a crema (misma variable que usa el FONDO del panel), el link
   quedaba exactamente del mismo color que lo que tiene atrás: invisible
   de verdad, no sólo bajo contraste. Encontrado en la revisión QA de
   /design-critique — era la única vía de contacto por mail del sitio. */
.sheet a.h2 {
  display: inline-block;
  color: var(--bg);
}

/* ── TABBAR FLUID (sólo móvil) ───────────────────────────────────
   Reemplaza el menú de hamburguesa: patrón de nav de app nativa, barra
   flotante fija abajo. 4 ítems reales (Work/About/Toolkit/Contact —
   Expertise queda accesible scrolleando desde Home) + un botón central
   ELEVADO con el monograma "g" que hace de Home, estilo FAB — spec del
   usuario a partir de una referencia de Figma (mobile). El botón
   central es un tap target aparte: no entra en el indicador deslizante
   ni en el drag (ver initTabbar() en app.js).
   Vidrio oscuro (no blanco — otro pedido del mismo Figma) con
   indicador claro que SIGUE AL DEDO en tiempo real durante el drag: no
   es un highlight por ítem, es un solo div que se desliza. El
   ancho/posición del indicador se calculan en JS en px (25% del ancho
   útil, medido con getBoundingClientRect — un % fijo en CSS no
   alcanza porque el JS necesita esa misma medida para el drag), CSS
   sólo anima la transición cuando NO se está arrastrando
   (.is-dragging la corta).
   Nace oculta y aparece recién cuando la cortina del preloader se
   enrolla (mismo momento, ver runPreloader() en preloader.js): la idea
   es que el logo del loader "se convierta" en la barra — ver
   60fps.design/shots/recollect-logo-morph-to-features-animation. */
.tabbar {
  display: none;
}

@media (max-width: 700px) {
  .tabbar {
    display: flex;
    position: fixed;
    left: var(--margin);
    right: var(--margin);
    bottom: max(1rem, env(safe-area-inset-bottom));
    /* Arriba de --z-transition (900) a propósito: es chrome fijo tipo
       app nativa, tiene que sobrevivir intacta a la cortina de
       transición entre páginas — antes compartía --z-header (150) con
       el header y la cortina (900) la tapaba, así que desaparecía y
       reaparecía de golpe en cada navegación. */
    z-index: var(--z-shell-fixed);
    padding: .4rem;
    border-radius: 999px;
    background: rgb(20 12 12 / .55);
    color: #fff;
    -webkit-backdrop-filter: blur(16px) saturate(1.3);
    backdrop-filter: blur(16px) saturate(1.3);
    border: 1px solid rgb(255 255 255 / .08);
    box-shadow: 0 1rem 2.5rem rgb(0 0 0 / .35);
    /* La barra en sí no scrollea — el drag horizontal lo maneja JS con
       Pointer Events, así que cortar los gestos táctiles nativos acá
       adentro no le saca el scroll vertical al resto de la página
       (eso vive en .scroller, un elemento completamente distinto). */
    touch-action: none;
  }

  .js .tabbar {
    opacity: 0;
    transform: translateY(.65rem) scale(.9);
    transition: opacity .5s var(--ease-out-expo), transform .6s var(--ease-out-expo);
  }

  .js .tabbar.is-in {
    opacity: 1;
    transform: none;
  }
}

/* Pedido de seguimiento: el FAB central (círculo rojo, el favicon
   reusado como botón) leía como una alarma, no como Home — se sacó, y
   Home pasó a ser un quinto ítem igual a los otros cuatro (ver el
   comentario grande junto al <nav class="tabbar"> en el HTML). Esto
   simplifica la barra entera: ya no hay un elemento fuera del flujo
   flex con su propio z-index/sombra/borde — son 5 tabs parejos, el
   indicador se desliza entre los 5 sin casos especiales, y el "más
   premium" que pedía el feedback sale de ESO (una barra consistente y
   contenida) en vez de un botón grande gritando encima. */
.tabbar__indicator {
  position: absolute;
  top: .4rem;
  bottom: .4rem;
  left: 0;
  border-radius: 16px;
  background: linear-gradient(180deg, rgb(234 223 223 / .16), rgb(234 223 223 / .07));
  box-shadow: inset 0 0 0 1px rgb(234 223 223 / .14), inset 0 1px 0 rgb(255 255 255 / .12);
  transform: translateX(0);
  will-change: transform;
  transition: transform .5s cubic-bezier(.25, 1, .5, 1), width .5s cubic-bezier(.25, 1, .5, 1);
  pointer-events: none;
}

.tabbar.is-dragging .tabbar__indicator {
  transition: none;
}

.tabbar__indicator.is-hidden {
  opacity: 0;
}

/* Pedido de seguimiento: "se desalinean los elementos de ese nav" — el
   ítem de Contact es un <button>, no un <a> como los otros cuatro, y un
   <button> trae `min-width: auto` (el piso por defecto de todo ítem
   flex, ver la nota de la spec sobre "automatic minimum size") atado a
   su propio contenido mínimo, que en un <button> nativo no es igual al
   de un <a> con el mismo contenido adentro — con `flex: 1 1 0` de por
   sí eso ya debería repartir el ancho parejo, pero ese piso lo pisa: en
   vivo, Contact medía 11px más ancho que el resto (66.9px vs 56.1px),
   corriendo la matemática de `w = rect.width / items.length` que usa
   el indicador en initTabbar() (app.js) — el indicador terminaba mal
   alineado con el ítem que en teoría estaba marcando. `min-width: 0`
   saca ese piso y los 5 quedan realmente parejos. */
.tabbar__item {
  position: relative;
  z-index: 1;
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: .2rem;
  align-items: center;
  justify-content: center;
  height: 46px;
  color: inherit;
  opacity: .55;
  transition: opacity .25s var(--ease-tw), color .25s var(--ease-tw);
}

.tabbar__item.is-live {
  color: var(--brand);
  opacity: 1;
}

.tabbar__icon {
  width: 19px;
  height: 19px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.tabbar__label {
  font-family: var(--font-ui);
  font-size: .58rem;
  font-weight: 600;
  letter-spacing: .01em;
  line-height: 1;
}

/* ── CURSOR ─────────────────────────────────────────────────── */
.cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: var(--z-cursor);
  pointer-events: none;
  transform: translate3d(-100px, -100px, 0);
  will-change: transform;
}

.no-js .cursor {
  display: none;
}

@media (hover: none) {
  .cursor {
    display: none;
  }
}

.cursor__dot {
  width: 9px;
  height: 9px;
  margin: -4.5px 0 0 -4.5px;
  border-radius: 50%;
  background: var(--brand);
  transition: transform var(--dur-fast) var(--ease-out-quint), opacity .2s;
}

.cursor__label {
  position: absolute;
  top: -11px;
  left: 0;
  height: 22px;
  width: 0;
  overflow: hidden;
  border-radius: 11px;
  background: var(--brand);
  color: var(--bg);
  display: grid;
  place-items: center;
  transition: width .4s var(--ease-out-quint);
}

.cursor__label span {
  padding-inline: 12px;
  white-space: nowrap;
  font-family: var(--font-mono);
  font-weight: var(--weight-subtitle);
  font-size: 10px;
  letter-spacing: .1em;
  text-transform: uppercase;
}

.cursor[data-state="link"] .cursor__dot {
  transform: scale(2.6);
  opacity: .4;
}

.cursor[data-state="label"] .cursor__dot {
  opacity: 0;
}

.cursor[data-state="label"] .cursor__label {
  width: 108px;
}

/* ── TRANSICIÓN ─────────────────────────────────────────────── */
.transition {
  position: fixed;
  inset: 0;
  z-index: var(--z-transition);
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  background: rgb(0 0 0 / .3);
  will-change: opacity;
}

.transition__panel {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: var(--transition-color, var(--bg));
  transform: translateY(100%);
  will-change: transform;
}

[data-preventclick] {
  pointer-events: none;
}

/* ── PRELOADER ──────────────────────────────────────────────
   Como ilcapoproduction: el panel de carga NO es negro, es el rojo
   de marca a pantalla completa con el logo en negro encima. Al salir,
   el mismo logo queda en rojo sobre negro en el header: lo único que
   cambia entre las dos pantallas son los colores, no la forma.       */
.loader {
  position: fixed;
  inset: 0;
  z-index: var(--z-loader);
  background: var(--brand);
  color: #000;
  display: grid;
  place-content: center;
  gap: 1.75rem;
  justify-items: center;
  overflow: hidden;          /* recorta la cortina de salida (::after) */
}

/* ── SALIDA DEL LOADER — cortina de dos tiempos ────────────────
   Referencia: asiaxp.co (medido en vivo). Allá el loader es negro y
   al salir un panel ROJO barre la pantalla de abajo hacia arriba
   mientras el logo se va hacia arriba detrás de su propia máscara.
   Acá los colores ya están al revés por diseño (el loader es el rojo
   de marca, ver el comentario de arriba), así que el barrido se
   invierte igual: el panel que sube es NEGRO, el fondo del sitio.

   Eso es lo que hace que el pase no tenga costura: cuando el panel
   termina de subir, la pantalla ya ES la pantalla del sitio (mismo
   negro), y lo único que falta es que el loader se saque del DOM.
   Antes la salida era un scaleY(0) enrollando desde arriba — se veía
   como "se va el loader"; esto se lee como "entra el sitio".

   Todo en CSS colgado de una sola clase (.is-out), no en estilos
   inline desde JS: el preloader ahora sólo decide CUÁNDO, igual que
   el resto de los estados de este sitio. */
.loader__logo {
  overflow: hidden;          /* ventana de máscara: los <i> suben por detrás */
}

.loader__logo i,
.loader__count,
.loader__bar {
  transition: transform .7s var(--ease-in-out-expo), opacity .3s linear;
}

.loader.is-out .loader__logo i {
  transform: translateY(-125%);
}

.loader.is-out .loader__count,
.loader.is-out .loader__bar {
  opacity: 0;
}

.loader::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;                /* por encima del logo: lo tapa al subir */
  background: var(--bg);
  transform: translateY(100%);
}

.loader.is-out::after {
  transform: translateY(0);
  transition: transform .85s var(--ease-in-out-expo) .25s;
}

@media (prefers-reduced-motion: reduce) {
  .loader__logo i,
  .loader__count,
  .loader__bar,
  .loader::after {
    transition: none;
  }
}

.no-js .loader {
  display: none;
}

/* El logo se RELLENA con el progreso: una capa al 22% siempre visible y
   otra sólida recortada de izquierda a derecha por --p (0 → 1). */
.loader__logo {
  position: relative;
  width: min(62vw, 420px);
  aspect-ratio: 350 / 56;
}

.loader__logo i {
  position: absolute;
  inset: 0;
  display: block;
  background: #000;
  -webkit-mask: url(../assets/logo.svg) center / contain no-repeat;
  mask: url(../assets/logo.svg) center / contain no-repeat;
}

.loader__logo i:first-child {
  opacity: .22;
}

.loader__logo i:last-child {
  clip-path: inset(0 calc(100% - var(--p, 0) * 100%) 0 0);
}

.loader__count {
  display: flex;
  font-variant-numeric: tabular-nums;
  color: #000;
  opacity: .8;
  letter-spacing: .2em;
}

.loader__bar {
  position: absolute;
  inset: auto 0 0 0;
  height: 2px;
  background: rgb(0 0 0 / .18);
}

.loader__bar i {
  display: block;
  height: 100%;
  background: #000;
  transform: scaleX(0);
  transform-origin: left;
}

/* ── MARCA COMPARTIDA ───────────────────────────────────────
   Mismo asset que el loader, ahora en rojo sobre negro. */
.mark {
  display: block;
  width: clamp(120px, 13vw, 190px);
  aspect-ratio: 350 / 56;
  background: var(--brand);
  -webkit-mask: url(../assets/logo.svg) center / contain no-repeat;
  mask: url(../assets/logo.svg) center / contain no-repeat;
}

/* (el tamaño del logotipo del pie ahora lo fija .bigmark, full-bleed) */

[data-intro-locked] .scroller {
  overflow: hidden;
}

[data-intro-locked] main {
  pointer-events: none;
}

.about-readmore {
  display: inline-block;
  margin-top: 1.5rem;
  color: var(--muted);
}

/* ── PÁGINA ABOUT: ESCRITORIO DE CARPETAS ────────────────────
   Calco de ref.digital/careers, medido en vivo. Lo importante: NO es
   una página que scrollea, es un ESCRITORIO de un solo viewport sobre
   beige #c4bab0 (el mismo --case-bg del interior de proyecto), con el
   logotipo grande arriba a la izquierda, una constelación de ítems de
   160px unidos por líneas finas dibujadas en un <canvas> 2D, y un
   toggle [NODE]/GRID abajo a la izquierda. Al hacer clic, cada ítem
   abre una VENTANA-CARPETA arrastrable con pestaña de solapa (borde
   derecho sesgado 24°, como una carpeta colgante) y cuerpo blanco.

   El contenido de ellos son vacantes y fotos de oficina; el nuestro es
   el material que ya vive en data.js — bio, expertise, toolkit,
   clientes, proyectos y contacto. La estructura es de ellos, el
   contenido es nuestro: no hay nada inventado para llenar huecos.

   NO lleva pie, igual que la referencia: con overflow:hidden no habría
   forma de llegar a él. El contacto vive en su propio ítem. */
.desk {
  position: relative;
  height: 100dvh;
  overflow: hidden;
  background: var(--case-bg);
  color: var(--case-fg);
}

/* El header y el isotipo compartido viven FUERA de #app, así que el
   remapeo de tokens de un scope no los alcanza: sobre beige, el crema
   de --brand queda ilegible (~1.2:1). Se corrigen desde el body. */
body:has(.desk) .header,
body:has(.desk) .header a,
body:has(.desk) .header button {
  color: var(--case-fg);
}

body:has(.desk) .header__logo .mark {
  background: var(--case-fg);
}

/* ÍTEMS ─────────────────────────────────────────────────────
   Posicionados por --x/--y en px desde JS (about.js) y centrados sobre
   ese punto. La transición es lo que hace la coreografía Node→Grid. */
/* width/height explícitos NO son redundantes con inset:0: <canvas> es un
   elemento reemplazado, así que con width:auto usa su tamaño INTRÍNSECO
   (300×150) en vez de estirarse, y las líneas se dibujaban fuera de
   cuadro. */
.desk__links {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  pointer-events: none;
}

.desk__field {
  position: absolute;
  inset: 0;
  z-index: 2;
}

/* Carga: cada ítem arranca apilado sobre el hub (JS → placeAtHub()) a
   escala chica y sin opacidad, y "estalla" a su lugar Node real y a
   escala 1 cuando el campo recibe `.is-in` — mismo `--i` de siempre para
   el escalonado, pero ahora es un viaje, no un fade en el sitio. */
/* El parallax de mouse (--px/--py, initAbout()→parallaxLoop en about.js)
   va en un span INTERNO (.desk-item__parallax), no acá: este elemento ya
   anima `transform` con transición de .9s para la carga y el toggle
   Node/Grid, y si el offset de parallax —que se actualiza a 60fps—
   compartiera esa misma propiedad, cada frame heredaría la transición de
   .9s y el parallax se sentiría con medio segundo de rezago en vez de
   responder al cursor. Doble capa = las dos animaciones conviven sin
   pisarse. */
.desk-item {
  position: absolute;
  top: 0;
  left: 0;
  width: 160px;
  text-align: center;
  color: inherit;
  transform: translate3d(var(--x, 0), var(--y, 0), 0) translate(-50%, -50%) scale(var(--s, .3));
  opacity: 0;
  transition: transform .9s var(--ease-out-expo), opacity .5s ease;
  transition-delay: calc(var(--i, 0) * 45ms);
}

.desk__field.is-in .desk-item {
  opacity: 1;
  --s: 1;
}

.desk-item__parallax {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: .55rem;
  transform: translate3d(var(--px, 0px), var(--py, 0px), 0);
}

.desk-item__label {
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--case-fg);
  transition: opacity var(--dur-fast);
}

.desk-item:hover .desk-item__label {
  opacity: .55;
}

/* La etiqueta del ítem-carpeta va en chip invertido (patrón REFFY) */
.desk-item__label--chip {
  background: var(--case-fg);
  color: var(--case-bg);
  padding: .15rem .4rem;
}

/* Tarjeta destacada — el equivalente de su "01 Jobs Opening". Invierte
   a negro+crema, que es exactamente la paleta base del sitio, así que
   sobre el beige lee como "esta es la pieza principal". */
.desk-card {
  width: 160px;
  height: 160px;
  border-radius: .8rem;
  padding: 1rem;
  background: var(--bg);
  color: var(--brand);
  display: flex;
  flex-direction: column;
  box-shadow: 0 4px 10px rgb(0 0 0 / .1);
}

.desk-card__num {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  flex: none;
  background: var(--brand);
  color: var(--bg);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.desk-card__foot {
  margin-top: auto;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: .5rem;
}

.desk-card__title {
  font-family: var(--font-ui);
  font-weight: 400;
  text-align: left;
  font-size: 1.2rem;
  line-height: 1.1;
}

.desk-item[aria-expanded="true"] .desk-card {
  background: var(--brand);
  color: var(--bg);
}

.desk-item[aria-expanded="true"] .desk-card__num {
  background: var(--bg);
  color: var(--brand);
}

/* Ícono de línea (historia, toolkit, clientes, contacto) */
.desk-icon {
  display: block;
  width: 50px;
  height: auto;
  overflow: visible;
}

.desk-icon .fill {
  fill: var(--case-bg);
}

.desk-icon .solid {
  fill: var(--case-fg);
}

.desk-icon .line {
  stroke: var(--case-fg);
  fill: none;
}

/* ── Gestos de la cara del folder "Selected_Work" ─────────────────
   Pedido de seguimiento: varios estados de gesto, uno por carpeta que
   se abre (ver initFolderFace()/openFolder() en about.js, que decide
   CUÁNDO cambia cada uno — esto es sólo el CÓMO se ve cada estado).

   La boca ya no vive acá: es un solo <path data-mouth-active> cuyo
   `d` anima directamente MorphSVGPlugin (ver setFace() en about.js),
   no hay opacity ni transform que cross-fadear entre tres formas como
   antes. Lo que queda en CSS es sólo el reflejo del gesto en los
   ojos. */

/* Los ojos también reaccionan un poco a la sorpresa —se agrandan—, no
   sólo la boca: una sorpresa real abre los ojos, no sólo la boca. */
[data-mouth-state="surprised"] .desk-folder-eye {
  transform: scale(1.25);
  transform-box: fill-box;
  transform-origin: center;
}

/* La sonrisa entorna un poco los ojos (se achican en Y) — mismo
   principio: una sonrisa de verdad también se nota en los ojos. */
[data-mouth-state="smile"] .desk-folder-eye {
  transform: scaleY(.6);
  transform-box: fill-box;
  transform-origin: center;
}

.desk-folder-eye {
  transition: transform .25s var(--ease-tw);
}

/* Pila de fotos que se abre en abanico al hover — su careers-gallery-block */
.desk-stack {
  position: relative;
  display: block;
  width: 76px;
  height: 88px;
}

.desk-stack img {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 60px;
  height: 75px;
  object-fit: cover;
  background: var(--panel);
  box-shadow: 0 4px 10px rgb(0 0 0 / .1);
  transition: transform .3s var(--ease-out-expo);
}

.desk-stack img:nth-child(1) {
  z-index: 3;
  transform: rotate(-6deg);
}

.desk-stack img:nth-child(2) {
  z-index: 2;
}

.desk-stack img:nth-child(3) {
  z-index: 1;
  transform: rotate(5deg);
}

.desk-item:hover .desk-stack img:nth-child(1) {
  transform: rotate(0deg);
}

.desk-item:hover .desk-stack img:nth-child(2) {
  transform: translate(-15px, -5px) rotate(-5deg);
}

.desk-item:hover .desk-stack img:nth-child(3) {
  transform: translate(15px, 5px) rotate(8deg);
}

/* VENTANAS-CARPETA ──────────────────────────────────────────
   Arrastrables por la solapa; el z-index sube al enfocar (about.js).
   La solapa cuelga por ENCIMA del cuerpo (bottom:100%) y su borde
   derecho se sesga 24° con un ::after, que es lo que le da la silueta
   de carpeta colgante en vez de una ventana rectangular. */
.desk__windows {
  position: absolute;
  inset: 0;
  z-index: 10;
  pointer-events: none;
}

.folder {
  --folder-bg: #ffffff;
  position: absolute;
  top: 0;
  left: 0;
  width: min(600px, calc(100vw - 2rem));
  transform: translate3d(var(--fx, 0), var(--fy, 0), 0);
  pointer-events: auto;
  color: var(--case-fg);
  box-shadow: 0 4px 10px rgb(0 0 0 / .1);
}

.folder__tab {
  position: absolute;
  bottom: 100%;
  left: 0;
  margin-bottom: -1px;
  display: flex;
  background: var(--folder-bg);
}

.folder__tab::after {
  content: '';
  position: absolute;
  top: 0;
  left: 100%;
  bottom: -1px;
  width: 16px;
  background: var(--folder-bg);
  transform: skewX(24deg);
  transform-origin: bottom left;
}

.folder__close {
  width: 32px;
  height: 32px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: rgb(0 0 0 / .1);
}

.folder__close svg {
  width: 16px;
  height: 16px;
  transition: transform .3s var(--ease-out-expo);
}

.folder__close:hover svg {
  transform: rotate(90deg);
}

.folder__handle {
  cursor: move;
  touch-action: none;
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  height: 32px;
  padding-inline: 1rem;
}

.folder__handle svg {
  width: 16px;
  height: auto;
}

.folder__body {
  background: var(--folder-bg);
  max-height: min(500px, 55dvh);
  overflow-y: auto;
  padding: .5rem 1rem 1rem;
}

.folder__list {
  list-style: none;
  padding: 0;
}

.folder__list>li+li {
  border-top: 1px solid rgb(0 0 0 / .12);
}

.folder__row {
  display: flex;
  gap: 1rem;
  align-items: flex-start;
  padding: .55rem 0;
}

.folder__thumb {
  width: 96px;
  aspect-ratio: 1;
  object-fit: cover;
  flex: none;
  background: var(--panel);
}

.folder__rowtitle {
  flex: 0 0 8rem;
}

.folder__rowtext {
  flex: 1;
  color: rgb(0 0 0 / .62);
}

/* ── Filas de "Awards & recognition" y "Experience" ──────────────
   Reusan `.folder__row` (la misma fila flex que ya usaba la lista de
   proyectos) y le suman lo que estas dos listas necesitan y esa no:
   un chip de origen adelante, una columna de título más ancha —"Apple
   Marcom Interactive" o "Juan Valdez — Ecuadorian Sunrise Yasuní" no
   entran en los 8rem de `.folder__rowtitle`—, y un dato alineado a la
   derecha (el año del premio, el dominio del medio). */
.folder__rowtitle--wide {
  flex: 0 0 11rem;
}

.folder__row--media {
  gap: .75rem;
  align-items: baseline;
}

.folder__tag {
  flex: none;
  align-self: flex-start;
  padding: .15rem .4rem;
  border: 1px solid rgb(0 0 0 / .25);
  color: rgb(0 0 0 / .62);
  white-space: nowrap;
}

.folder__scope {
  color: rgb(0 0 0 / .45);
}

/* ── Tarjetas de premio (Awards & recognition) ────────────────────
   Rediseño pedido explícito: antes un premio era una fila más, del
   mismo peso que una mención de prensa (ver folder__row--media, acá
   abajo, que SÍ sigue siendo una fila simple a propósito). Ahora cada
   premio es su propia tarjeta con el año como ancla grande a la
   izquierda — lo primero que un premio necesita comunicar es CUÁNDO,
   no enterrarlo al final de una fila. */
.award-list {
  list-style: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .75rem;
}

.award-card {
  display: flex;
  gap: 1.1rem;
  align-items: flex-start;
  padding: 1rem 1.1rem;
  background: rgb(0 0 0 / .03);
  /* Barra de acento a la izquierda en --brand: el mismo crema que el
     sitio usa para "esto importa" sobre negro, acá al revés — el único
     trazo de color en una tarjeta blanca/gris clara. border-left en vez
     de box-shadow: no se recorta si la tarjeta hereda un radio. */
  border-left: 3px solid var(--brand);
}

.award-card__year {
  flex: none;
  font-family: var(--font-display);
  font-weight: var(--weight-title);
  font-size: 1.75rem;
  line-height: 1;
  letter-spacing: -.02em;
  color: rgb(0 0 0 / .82);
}

.award-card__body {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  min-width: 0;
}

.award-card__edition {
  margin: 0;
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: -.01em;
}

.award-card__detail {
  margin: 0;
  color: rgb(0 0 0 / .62);
  text-transform: none;
  letter-spacing: normal;
}

/* El dominio hace de "ir al link": alineado a la derecha, subrayado
   SÓLO cuando la fila es de verdad un <a> — la de "Nuggets Sound Test"
   no tiene url (ver MEDIA en data.js) y subrayarla prometería un click
   que no existe. */
.folder__rowtext--domain {
  flex: none;
  margin-left: auto;
  text-align: right;
  color: rgb(0 0 0 / .45);
}

a.folder__row--media {
  color: inherit;
  text-decoration: none;
  transition: background-color .15s var(--ease-tw);
}

a.folder__row--media .folder__rowtext--domain {
  text-decoration: underline;
  text-underline-offset: 3px;
}

a.folder__row--media:hover {
  background: rgb(0 0 0 / .05);
}

a.folder__row--media:hover .folder__rowtext--domain {
  color: var(--brand);
}

/* La frase de RECOGNITION es prosa, no una etiqueta: se le sacan la
   caja alta y el tracking que trae `.micro`. */
.folder__note {
  margin: .9rem 0 0;
  color: rgb(0 0 0 / .62);
  line-height: 1.55;
  text-transform: none;
  letter-spacing: normal;
}

.folder__sub {
  margin: 1.35rem 0 0;
  padding-top: .9rem;
  border-top: 1px solid rgb(0 0 0 / .12);
  color: rgb(0 0 0 / .45);
}

/* En pantallas angostas la ventana mide casi el viewport: las columnas
   fijas dejan de entrar y las filas pasan a apilarse. */
@media (max-width: 620px) {

  .folder__rowtitle--wide {
    flex: 1 1 100%;
  }

  .folder__row--media {
    flex-wrap: wrap;
    gap: .35rem .75rem;
  }

  .folder__rowtext--domain {
    margin-left: 0;
  }

  .award-card {
    gap: .75rem;
    padding: .85rem .9rem;
  }

  .award-card__year {
    font-size: 1.4rem;
  }
}

.folder__prose>p+p {
  margin-top: .75rem;
}

.folder__prose {
  color: rgb(0 0 0 / .72);
  line-height: 1.55;
}

/* Encabezado con el nombre real, arriba de la bio (carpeta "The
   story" — ver about.js). No es `.micro`: rompe la caja alta y el
   tracking angosto del resto de la prosa a propósito, para que se lea
   como un nombre y no como una etiqueta más. */
.folder__prose-name {
  margin: 0 0 .6rem;
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: 1.15rem;
  letter-spacing: -.02em;
  text-transform: none;
  color: var(--case-fg);
}

.folder__chips {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  list-style: none;
}

.folder__chips li {
  border: 1px solid rgb(0 0 0 / .25);
  padding: .3rem .55rem;
}

/* Variante con el logo real adentro (carpeta "Clients" — ver
   about.js). Mismo chip, mismo borde; sólo cambia qué hay dentro y
   cómo se centra: un texto en mono se alinea por baseline solo, una
   imagen necesita flex para no quedar pegada al borde inferior del
   chip. */
.folder__chip--logo {
  display: flex;
  align-items: center;
}

.folder__chip--logo img {
  display: block;
  height: 14px;
  width: auto;
  max-width: 88px;
  object-fit: contain;
}

/* Enlaces de la carpeta: mismos corchetes [ ] que usa la referencia */
.folder a.folder__link {
  position: relative;
  display: inline-block;
}

.folder a.folder__link::before {
  content: '[';
  position: absolute;
  left: -.55em;
}

.folder a.folder__link::after {
  content: ']';
  position: absolute;
  right: -.55em;
}

/* Toggle [NODE]/GRID abajo a la izquierda, píldora negra */
.desk__view {
  position: absolute;
  left: var(--margin);
  bottom: var(--margin);
  z-index: 20;
  display: inline-flex;
  align-items: center;
  gap: 1.25rem;
  background: var(--bg);
  color: #ffffff;
  padding: .875rem 1.125rem;
}

.desk__view button {
  position: relative;
  opacity: .55;
  transition: opacity var(--dur-fast);
}

.desk__view button[aria-pressed="true"] {
  opacity: 1;
}

.desk__view button[aria-pressed="true"]::before {
  content: '[';
  position: absolute;
  left: -.55em;
}

.desk__view button[aria-pressed="true"]::after {
  content: ']';
  position: absolute;
  right: -.55em;
}

.desk__hint {
  position: absolute;
  right: var(--margin);
  bottom: var(--margin);
  z-index: 20;
  color: rgb(0 0 0 / .45);
}

/* En mobile el escritorio disperso no funciona (no hay lugar ni hover):
   se cae a una lista vertical scrolleable, que es lo que hace la
   referencia también a este ancho. */
@media (max-width: 700px) {
  .desk {
    height: auto;
    min-height: 100dvh;
    overflow: visible;
    padding: 7rem var(--margin) 6rem;
  }

  .desk__links,
  .desk__hint {
    display: none;
  }

  .desk__field {
    position: static;
    display: grid;
    gap: 2.5rem 1rem;
    grid-template-columns: repeat(2, 1fr);
    justify-items: center;
  }

  .desk-item {
    position: static;
    transform: none;
    width: auto;
  }

  .desk__windows {
    position: static;
  }

  .folder {
    position: static;
    transform: none;
    width: 100%;
    margin-top: 2.5rem;
  }

  /* relative, NO static: el ::after sesgado de la solapa se posiciona
     contra el ancestro posicionado más cercano, y en estático ese pasa a
     ser .desk — el borde de 16px se estiraba a toda la altura de la
     página como una franja blanca en diagonal. */
  .folder__tab {
    position: relative;
    margin-bottom: 0;
  }

  .desk__view {
    position: static;
    margin-top: 2.5rem;
    display: inline-flex;
  }
}

/* ═══ ACCORDION DE EXPERTISE ═════════════════════════════════
   Mecánica del widget "our expertise": los paneles se expanden por
   flex, el título va vertical cerrado y horizontal abierto, y hay
   barra de progreso con contador. Paleta y tipografía del sitio.  */
.xp {
  --xp-h: min(460px, 64dvh);
  --xp-collapsed: .55;
  --xp-active: 4;
  /* Pedido de seguimiento: el título abierto (.h2, hasta 2.75rem) y el
     título vertical cerrado (.micro, .6875rem fijo) se leían como dos
     tipografías distintas — ni el tamaño ni el caso (.h2 ya no va en
     mayúscula sostenida desde el pedido de tipografía de antes; .micro
     sigue en mayúscula) coincidían. --xp-title-size es el punto medio
     que pidió el feedback — ni tan chico como la etiqueta de antes ni
     tan grande como el titular de sección — y de ahí toman tamaño los
     dos estados por igual, ver .xp-vtitle y .xp-title-wrap .h2 más
     abajo. */
  --xp-title-size: clamp(1rem, 1.6vw, 1.375rem);
  display: flex;
  width: 100%;
  height: var(--xp-h);
  overflow: hidden;
  border: 1px solid var(--hairline);
}

/* Sobre el beige de .home-invert, las fichas ya no necesitan tanto aire
   alrededor ni negro puro: un poco de sección más compacta (.section--tight)
   y un panel cálido derivado del beige, no --panel puro, para que se
   sientan parte de la misma superficie en vez de un bloque negro suelto. */
.section--tight {
  padding-block: clamp(3rem, 8vh, 6rem);
}

.section--tight__head {
  margin-bottom: 1rem;
}

.section--tight__body {
  padding-inline: var(--margin);
}

/* Pedido de seguimiento: "la parte del expertise se ve rara, ajusta el
   diseño a que coincida con el resto de la web" — Expertise pasó a
   vivir en su propia sección negra (fuera de .home-invert, ver el
   comentario grande de SCROLL APILADO más arriba), pero las fichas del
   acordeón se habían quedado con el beige `#eadfdf` de cuando convivían
   con el resto del contenido claro de la home: un bloque beige flotando
   en medio de una sección que ahora es negra a propósito. Encima, el
   título del panel abierto (`.h2`) hereda `color: var(--brand)` —el
   mismo crema del fondo beige— así que quedaba literalmente invisible,
   texto crema sobre fondo crema.

   `#120503` es el mismo negro-rojizo que ya usaba el panel ABIERTO
   antes de este ajuste (no un color inventado), ahora en la regla base
   para los dos estados: encaja con la paleta oscura/roja del resto del
   sitio (gl-hero.js, fluid.js) en vez de negro plano. `color: var(--brand)`
   en la base es lo que hace que el título se vea sobre el panel nuevo
   —ya no hace falta el override de `.home-invert .xp-panel.is-active .h2`
   que había antes: ese selector dejó de matchear apenas Expertise salió
   de `.home-invert`, era código muerto. */
.xp-panel {
  position: relative;
  flex: var(--xp-collapsed) 1 0%;
  min-width: 0;
  overflow: hidden;
  cursor: pointer;
  background: #120503;
  color: var(--brand);
  border-right: 1px solid var(--hairline);
  transition: flex .7s var(--ease-out-quint);
}

/* Pedido de seguimiento: "imágenes de fondo cuando el acordion esté
   abierto, relacionado a cada área que muestra" — una foto real por
   disciplina (ver EXPERTISE en data.js), oculta en el panel colapsado
   y que aparece recién al abrirse. Primer hijo del panel a propósito:
   sin z-index propio, cae DEBAJO del resto (`.xp-content`, `.xp-plus`,
   etc.) por puro orden de DOM — no hace falta apilar nada a mano.
   grayscale+contrast la desatura un poco: a color pleno competía con
   el crema del texto encima; así queda como acento, no como foto de
   portada. El degradado en ::after es el mismo negro-rojizo del panel
   (#120503) para que la imagen se funda con el fondo en los bordes en
   vez de leerse como un recorte pegado encima. */
.xp-bg {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  opacity: 0;
  filter: grayscale(.3) contrast(1.05);
  transform: scale(1.08);
  transition: opacity .8s var(--ease-out-quint), transform 1.8s var(--ease-out-quint);
}

/* Paneles con video de fondo (e.video en data.js): el <video> llena
   .xp-bg igual que el background-image de los paneles con foto — el
   filter/scale/opacity de arriba ya le pega a esto también porque
   filter en CSS alcanza a los hijos, no hace falta repetirlo acá. */
.xp-bg video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.xp-bg::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgb(18 5 3 / .35) 0%, rgb(18 5 3 / .55) 55%, rgb(18 5 3 / .88) 100%);
}

.xp-panel.is-active .xp-bg {
  opacity: 1;
  transform: scale(1);
}

.xp-panel:last-child {
  border-right: 0;
}

.xp-panel.is-active {
  flex: var(--xp-active) 1 0%;
}

/* "+" de esquina, sólo con el panel cerrado */
.xp-plus {
  position: absolute;
  top: 22px;
  right: 22px;
  font-family: var(--font-ui);
  font-size: 30px;
  font-weight: 400;
  line-height: 1;
  transition: opacity .5s .35s, transform .5s var(--ease-out-quint) .3s;
}

.xp-panel.is-active .xp-plus {
  opacity: 0;
  transform: rotate(45deg) scale(.8);
  transition: opacity .25s, transform .35s var(--ease-out-quint);
}

/* título vertical, sólo cerrado — pedido de seguimiento: comparte
   tipografía con el título abierto (.xp-title-wrap .h2, más abajo) en
   vez de la voz .micro de siempre (mayúscula sostenida, .6875rem
   fijo): mismo --xp-title-size para los dos estados, sin
   text-transform. Sigue siendo .micro en el HTML (misma familia/peso
   de base, ver expertise.js) — esto sólo pisa lo que cambia. */
.xp-vtitle {
  position: absolute;
  bottom: 28px;
  left: 50%;
  writing-mode: vertical-rl;
  transform: translateX(-50%) rotate(180deg);
  white-space: nowrap;
  font-size: var(--xp-title-size);
  letter-spacing: -.01em;
  text-transform: none;
  transition: opacity .45s .4s, transform .5s var(--ease-out-quint) .35s;
}

.xp-panel.is-active .xp-vtitle {
  opacity: 0;
  transform: translateX(-50%) rotate(180deg) translateY(-8px);
  transition: opacity .25s, transform .3s var(--ease-out-quint);
}

/* contenido horizontal, sólo abierto */
.xp-content {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: clamp(1.5rem, 3vw, 2.5rem);
  pointer-events: none;
}

.xp-panel.is-active .xp-content {
  pointer-events: auto;
}

.xp-title-wrap {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity .2s, transform .2s;
}

/* Pedido de seguimiento: bajado desde el tamaño de sección (.h2, hasta
   2.75rem) a --xp-title-size — el mismo valor que .xp-vtitle de
   arriba, así el título no cambia de tipografía al abrirse, sólo de
   orientación. */
.xp-title-wrap .h2 {
  font-size: var(--xp-title-size);
  letter-spacing: -.01em;
}

.xp-panel.is-active .xp-title-wrap {
  opacity: 1;
  transform: none;
  transition: opacity .55s var(--ease-out-quint) .18s, transform .6s var(--ease-out-expo) .18s;
}

.xp-blurb {
  max-width: 46ch;
  color: inherit;
  opacity: 0;
  transform: translateY(12px);
  transition: opacity .15s, transform .15s;
}

.xp-panel.is-active .xp-blurb {
  opacity: .8;
  transform: none;
  transition: opacity .55s var(--ease-out-quint) .42s, transform .6s var(--ease-out-expo) .4s;
}

.xp-counter {
  position: absolute;
  top: 22px;
  right: 22px;
  font-variant-numeric: tabular-nums;
  /* que no baile al contar 0→100 */
  opacity: 0;
  pointer-events: none;
  transition: opacity .5s .3s;
}

.xp-panel.is-active .xp-counter {
  opacity: .45;
}

.xp-progress {
  position: absolute;
  inset: auto 0 0 0;
  height: 2px;
  overflow: hidden;
  background: var(--hairline);
  opacity: 0;
  transition: opacity .4s;
}

.xp-panel.is-active .xp-progress {
  opacity: 1;
}

.xp-progress i {
  display: block;
  height: 100%;
  background: var(--brand);
  transform: scaleX(0);
  transform-origin: left center;
}

/* móvil: se apila y deja de ser acordeón */
@media (max-width: 991px) {
  .xp {
    flex-direction: column;
    height: auto;
  }

  .xp-panel,
  .xp-panel.is-active {
    flex: none;
    min-height: 260px;
    padding: 1.75rem 1.75rem 3rem;
    border-right: 0;
    border-bottom: 1px solid var(--hairline);
    background: var(--panel);
  }

  .xp-plus,
  .xp-vtitle,
  .xp-counter,
  .xp-progress {
    display: none;
  }

  .xp-content {
    position: static;
    padding: 0;
    pointer-events: auto;
    gap: 1.25rem;
    justify-content: flex-start;
  }

  .xp-title-wrap,
  .xp-blurb {
    opacity: 1 !important;
    transform: none !important;
  }

  .xp-blurb {
    opacity: .8 !important;
  }

  /* En mobile los 6 paneles se apilan ya "abiertos" (arriba, .xp-panel
     e .is-active comparten estilos) — no hay estado colapsado visual
     que distinguir, así que la imagen de fondo tampoco debería
     depender de cuál sea el "activo" del autoplay del acordion de
     escritorio: los 6 la muestran siempre. */
  .xp-bg {
    opacity: 1;
    transform: scale(1);
  }

  /* El achique de --xp-title-size (pedido de seguimiento) es para
     resolver el choque entre el título cerrado y el abierto — acá
     .xp-vtitle está oculto (arriba) y sólo queda el título abierto, así
     que no hay nada con qué igualarlo: vuelve a su tamaño de sección
     de siempre. */
  .xp-title-wrap .h2 {
    font-size: var(--text-h2);
    letter-spacing: -.02em;
  }
}

/* ── NDA: acceso privado ────────────────────────────────────────
   Pedido de seguimiento: subpágina con contraseña para trabajo bajo
   NDA. `data-nda-content` es lo que nda.js le saca `hidden` cuando el
   código de acceso coincide — el fade+subida de acá es compartido
   entre el caso y el pie, así los dos entran juntos en vez de que uno
   aparezca de golpe. */
[data-nda-content] {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity .6s var(--ease-out-quint), transform .6s var(--ease-out-quint);
}

[data-nda-content].is-in {
  opacity: 1;
  transform: none;
}

.nda-gate {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: var(--margin);
  text-align: center;
  transition: opacity .5s var(--ease-tw), transform .5s var(--ease-out-quint);
}

/* Misma especificidad que `.nda-gate` de arriba (una clase), así que
   sin esto la cascada de autor le gana al `[hidden]{display:none}` del
   navegador — nda.js pone `gate.hidden = true` al desbloquear, pero
   visualmente seguía ocupando los 100dvh de siempre (confirmado en
   vivo: display:grid pese a hidden=true), empujando el caso real un
   viewport entero hacia abajo. */
.nda-gate[hidden] {
  display: none;
}

.nda-gate.is-out {
  opacity: 0;
  transform: scale(.98);
  pointer-events: none;
}

.nda-gate__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  max-width: 26rem;
}

.nda-gate__eyebrow {
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .08em;
}

.nda-gate__title {
  margin: 0;
}

.nda-gate__copy {
  color: var(--muted);
  font-size: .9375rem;
  line-height: 1.5;
}

.nda-gate__copy a {
  color: var(--fg);
  text-decoration: underline;
  text-underline-offset: .15em;
}

.nda-gate__form {
  display: flex;
  gap: .5rem;
  width: 100%;
  max-width: 22rem;
  margin-top: .5rem;
}

.nda-gate__input {
  flex: 1;
  min-width: 0;
  height: 48px;
  padding: 0 1.1rem;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: transparent;
  color: var(--fg);
  font: inherit;
  font-size: .9375rem;
}

.nda-gate__input::placeholder {
  color: var(--muted);
}

.nda-gate__input:focus-visible {
  outline: none;
  border-color: var(--brand);
}

.nda-gate__submit {
  height: 48px;
  padding: 0 1.5rem;
  border-radius: 999px;
  background: var(--brand);
  color: #000;
  font-weight: 600;
  font-size: .9375rem;
  transition: opacity .2s var(--ease-tw);
}

.nda-gate__submit:hover {
  opacity: .85;
}

/* Mismo rojo que ya usa el sitio en gl-hero.js/fluid.js (colVividRed) —
   no un color de error inventado aparte. */
.nda-gate__error {
  color: rgb(255 90 60);
  opacity: 0;
  height: 0;
  overflow: hidden;
  transition: opacity .2s var(--ease-tw);
}

.nda-gate__error.is-shown {
  opacity: 1;
  height: auto;
}

@keyframes nda-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-8px); }
  40% { transform: translateX(8px); }
  60% { transform: translateX(-6px); }
  80% { transform: translateX(6px); }
}

.nda-gate.is-shake .nda-gate__form {
  animation: nda-shake .4s var(--ease-tw);
}

/* ── Caso NDA (1:1 del nodo de Figma, adaptado a los tokens del sitio) ── */
.nda-case {
  padding-block: clamp(2rem, 6vh, 4rem) 0;
}

.nda-case__head {
  padding-bottom: clamp(2rem, 6vh, 4rem);
  border-bottom: 1px solid var(--hairline);
  margin-bottom: clamp(2rem, 6vh, 4rem);
}

.nda-case__crumbs {
  display: flex;
  gap: .5rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .06em;
  margin-bottom: 1.5rem;
}

.nda-case__crumbs-current {
  color: var(--fg);
}

.nda-case__headline-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 2rem;
  flex-wrap: wrap;
}

.nda-case__display {
  margin: 0;
  font-weight: 700;
  font-size: var(--text-h1);
  letter-spacing: -.03em;
  line-height: .92;
  color: var(--fg);
}

.nda-case__intro {
  max-width: 22rem;
  color: var(--muted);
  font-weight: 300;
  font-size: 1.0625rem;
  line-height: 1.5;
}

.nda-case__sidebar {
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

.nda-case__block {
  display: flex;
  flex-direction: column;
  gap: .5rem;
}

.nda-case__label {
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .08em;
}

.nda-case__value {
  font-weight: 500;
  color: var(--fg);
}

.nda-case__value--lg {
  font-size: 1.25rem;
  line-height: 1.3;
}

.nda-case__meta {
  color: var(--muted);
  font-size: .9375rem;
  line-height: 1.4;
}

.nda-case__meta strong {
  color: var(--fg);
  font-weight: 500;
}

.nda-case__row--head {
  display: flex;
  justify-content: space-between;
  padding-bottom: .75rem;
  border-bottom: 1px solid var(--hairline);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .06em;
}

.nda-case__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.nda-case__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding-block: 1.5rem;
  border-bottom: 1px solid var(--hairline);
}

.nda-case__row-left {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.nda-case__arrow {
  color: var(--brand);
  font-size: .75rem;
}

.nda-case__link {
  color: var(--fg);
  font-size: 1.125rem;
  text-decoration: underline;
  text-underline-offset: .2em;
  text-decoration-color: var(--hairline);
  transition: text-decoration-color .2s var(--ease-tw);
}

.nda-case__link:hover {
  text-decoration-color: var(--fg);
}

.nda-case__role {
  color: var(--muted);
  white-space: nowrap;
  flex: none;
}

@media (max-width: 700px) {
  .nda-case__sidebar {
    grid-column: 1 / -1 !important;
    margin-bottom: 2rem;
  }

  .nda-case__projects {
    grid-column: 1 / -1 !important;
  }

  .nda-case__headline-row {
    align-items: flex-start;
    flex-direction: column;
    gap: 1rem;
  }

  .nda-case__row {
    flex-direction: column;
    align-items: flex-start;
    gap: .5rem;
  }
}