/* ==========================================================================
   HOME — hero, showreel, disciplinas, selected work, servicios, contacto
   ========================================================================== */

/* ---------- Hero ---------- */
/* Cuarta vuelta del hero — la que se pidió después de ver la Variante 4 en
   vivo: ya NO está pineado. Hasta acá, todo tenía que entrar en una sola
   pantalla (hero-showreel.js fijaba `.hero` con ScrollTrigger mientras
   transformaba headline/retrato/metadata) — eso obligaba a un retrato
   chico y a un ida-y-vuelta constante de cálculos de despeje (ver el
   historial de bugs que tenía esta sección). Ahora el hero es flujo NORMAL,
   sin pin: el headline gigante ocupa las dos líneas de arriba, el retrato
   es un bloque real en el documento — con su ratio 4:5 real, mucho más alto
   que la pantalla — y hace falta scrollear para verlo completo antes de
   llegar al párrafo/CTA. hero-showreel.js ya no toca nada de esto (se le
   sacó el bloque 1, el del pin) — solo le queda su propia entrada del
   showreel, sin relación con el hero. */
.hero {
  position: relative;
  padding-top: clamp(3rem, 2.4rem + 3vw, 5rem);
}

.hero__top {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--grid-gap);
  align-items: start;
  margin-bottom: var(--space-sm);
}
@media (min-width: 1080px) {
  .hero__top { grid-template-columns: repeat(12, 1fr); }
}

.hero__meta {
  grid-column: 1 / -1;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
}
@media (min-width: 1080px) {
  .hero__meta { grid-column: 1 / span 4; flex-direction: column; gap: var(--space-2xs); }
}

.hero__number {
  font-family: var(--font-mono);
  font-size: clamp(1rem, 0.8rem + 1vw, 1.5rem);
  color: var(--color-muted);
}

/* Pedido puntual: "Multimedia Designer — Zürich" en negrita + el rosa del
   sitio (--color-accent), solo acá en el hero de Home — .eyebrow en el
   resto del sitio (About, Corporate, Showreel...) queda con su estilo
   normal, sin tocar. */
.hero__eyebrow-text {
  color: var(--color-accent);
  font-weight: 700;
}

/* EXPERIMENTAL — "encuentro": el headline se queda fijo (sticky, CSS
   puro) mientras `.hero__meet` (el wrapper que lo envuelve junto con el
   retrato) sigue scrolleando por debajo; el retrato sube desde abajo del
   viewport hasta encajar detrás de la segunda línea. Cuando `.hero__meet`
   se termina, el headline se suelta solo — scroll nativo, sin pin ni
   scroll-jacking. Ver la nota larga en el HTML (index.html, arriba del
   markup del hero) para el razonamiento completo. En mobile no aplica
   nada de esto (`.hero__meet` queda como un div transparente, sin alto ni
   position propios) — headline y retrato se apilan en flujo normal, como
   ya venía siendo. */
.hero__meet {
  position: relative;
}
@media (min-width: 1080px) {
  .hero__meet {
    /* Alto total = distancia del "encuentro" (cuánto hay que scrollear
       hasta que la foto llegue al headline) + el alto real de la foto
       (128vh) + un respiro antes de `.hero__bottom`. Subir o bajar el
       primer número (70vh) es la manija principal para hacer el
       encuentro más largo/corto. */
    height: calc(70vh + 128vh + var(--space-lg));
  }
}

/* Composición: headline GIGANTE a todo el ancho (2 líneas). No hace falta
   ningún wrapper que le acote el ancho (el viejo `.hero__stage` con su
   `max-width` — eso resolvía un bug de anclaje que ya no existe, ver la
   nota en `.hero__portrait` más abajo). La clave para que la foto NUNCA
   tape el texto sigue siendo la misma de siempre: `.hero__headline` con
   z-index más alto que `.hero__portrait` — el texto pinta encima, la
   foto asoma detrás de las letras. */
.hero__headline {
  position: relative;
  z-index: 2;
  /* --font-display ahora es Bricolage Grotesque en todo el sitio (antes
     probamos reemplazarla solo acá para el hero y no convenció aislada —
     ahora es la tipografía completa del sitio, así que el headline la usa
     igual que el resto de los títulos, sin excepción). Queda el weight más
     bold que carga (ver el <link> de Google Fonts en index.html) y el
     interlineado/tracking más cerrados, que sí se pidieron mantener. */
  font-family: var(--font-display);
  font-weight: 700;
  /* Bastante más grande que las versiones anteriores — a propósito: sin
     pin, ya no hay presupuesto de "una sola pantalla" que respetar, así
     que el headline puede ser tan grande como pide el diseño (ocupar
     TODO el ancho en 2 líneas). Un toque más grande que la primera pasada
     de esta v4 (pedido puntual). */
  font-size: clamp(3.2rem, 0.7rem + 8.3vw, 9.5rem);
  /* Interlineado bastante más cerrado que el resto del sitio (--lh-tight es
     0.92) y letras más juntas — a propósito, pedido puntual. El overflow
     de la máscara por palabra ya NO depende de este valor (ver .word más
     abajo), así que se puede apretar sin volver a cortar las letras. */
  line-height: 0.82;
  letter-spacing: -0.04em;
  text-wrap: balance;
  /* Pedido puntual: headline en mayúsculas. text-transform (no reescribir
     el texto en el HTML) para que screen readers sigan leyendo la oración
     normal, con mayúscula solo al principio. */
  text-transform: uppercase;
}
@media (min-width: 1080px) {
  .hero__headline {
    /* El "se queda fijo" del efecto de encuentro: sticky (no fixed) para
       que siga siendo un elemento normal del documento — cuando
       `.hero__meet` se termina, se suelta solo y listo, sin ningún
       cálculo ni listener de scroll. `top` lo despega del nav (que es
       `position:fixed`, altura real ~84–95px según el viewport — ver
       `.nav` en components.css); un poco más de aire que eso para que no
       quede pegado. */
    position: sticky;
    top: clamp(5rem, 4.4rem + 2vw, 6.5rem);
  }
}
/* Bug real: `.line` es un <span> (inline por default) y el único estilo
   que tenía era `overflow:hidden` — sin `display:block` ahí, "Telling
   stories," y "written in motion." son, para efectos de layout, UN SOLO
   párrafo corrido: el salto entre las dos quedaba librado a dónde el
   navegador decidiera envolver el texto según el ancho y la fuente. Con la
   fuente de reemplazo de este sandbox (más ancha que Fjalla One, la
   tipografía condensada real) el corte caía casi siempre justo entre
   "stories," y "written" — pero con la fuente real, más angosta, entraba
   TODO en una sola línea en tu Mac. `display:block` fuerza las 2 líneas
   siempre, sin importar ancho de pantalla ni fuente — deja de depender de
   una coincidencia de ancho. */
.hero__headline .line { display: block; overflow: hidden; }
.hero__headline em {
  font-style: italic;
  font-weight: 400;
  color: var(--color-emphasis);
}

.hero__portrait {
  position: relative;
  z-index: 1;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  margin-top: var(--space-md);
  width: 100%;
}
@media (min-width: 1080px) {
  .hero__portrait {
    /* El efecto pedido: una foto en su ratio real (4:5, la misma que el
       resto del sitio) pero mucho más alta que la pantalla. Ahora SÍ es
       `position: absolute` — dentro de `.hero__meet` (que le sobra alto a
       propósito, ver arriba), anclada por `bottom` y `right`. Al
       scrollear, `.hero__meet` avanza por debajo del headline sticky, y
       como el retrato está pegado al FONDO de ese wrapper, va subiendo
       desde abajo del viewport hasta encajar detrás de la segunda línea
       del headline — justo cuando `.hero__meet` se termina. object-fit
       en la imagen de adentro absorbe el recorte sin distorsionar nada. */
    position: absolute;
    right: 0;
    bottom: var(--space-lg);
    height: 128vh;
    width: auto;
  }
}
/* 14% de "colchón" arriba y abajo (128% de alto total, centrado con
   top:-14%) — no es la foto en sí, es lugar de sobra para que
   hero-showreel.js pueda desplazarla verticalmente (parallax al
   scrollear) sin descubrir nunca el fondo del contenedor. object-fit
   sigue absorbiendo el recorte del ratio 4:5 igual que antes. */
.hero__portrait img, .hero__portrait video {
  position: absolute;
  left: 0;
  top: -14%;
  width: 100%;
  height: 128%;
  object-fit: cover;
}

.hero__bottom {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding-top: var(--space-sm);
  padding-bottom: var(--space-lg);
  border-top: 1px solid var(--color-line);
}
@media (min-width: 1080px) {
  /* En mobile el texto y las acciones se apilan (columna); a partir de acá
     entran en una sola fila, texto a la izquierda y CTA/scroll a la
     derecha — mismo patrón horizontal que tenía .hero__bottom antes de
     sumarle el texto secundario. */
  .hero__bottom { flex-direction: row; justify-content: space-between; align-items: flex-end; gap: var(--space-md); }
}

.hero__sub {
  font-size: var(--fs-md);
  color: var(--color-ink-soft);
  max-width: 34ch;
}

.hero__actions {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

.hero__scroll {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-muted);
}
.hero__scroll-line {
  width: 1px; height: 28px;
  background: var(--color-muted);
  position: relative;
  overflow: hidden;
}
.hero__scroll-line::after {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  background: var(--color-ink);
  animation: scrollline 1.8s var(--ease-in-out) infinite;
  will-change: transform;
}
@keyframes scrollline {
  0% { transform: translateY(-100%); }
  60% { transform: translateY(100%); }
  100% { transform: translateY(100%); }
}
@media (prefers-reduced-motion: reduce) {
  .hero__scroll-line::after { animation: none; transform: translateY(0); }
}

/* ---------- Hero entrance sequence (Fase 1: coreografiada con GSAP en hero.js) ---------- */
/* Estado de reposo antes de que corra el timeline — sin transition a
   propósito. GSAP anima estas mismas propiedades cuadro a cuadro con su
   propio motor de easing; declarar una transition CSS en paralelo hace que
   el navegador le sume su propia animación encima de cada micro-cambio que
   escribe GSAP, y el resultado se siente "pastoso" en vez de preciso
   (misma lección que ya regía en scroll-story.js: nunca CSS transition +
   escritura por frame sobre la misma propiedad). */
[data-hero-eyebrow],
[data-hero-sub],
[data-hero-cta],
[data-hero-scroll] {
  opacity: 0;
  transform: translateY(14px);
}
[data-hero-portrait] {
  clip-path: inset(0% 0% 100% 0%);
  transform: scale(1.12);
  opacity: 0;
}

/* Headline por palabra: cada .word enmascara su propia .word-inner, que entra
   con un settle de rotación + desenfoque — más "editorial de motion" que un
   simple slide por línea. El timeline de GSAP la anima directamente.
   line-height PROPIO en .word, bien generoso, independiente del 0.78 tan
   ajustado del headline: era el bug reportado — con el line-height del
   padre heredado tal cual, la caja overflow:hidden de cada palabra quedaba
   más baja que el alto real de las letras y las cortaba a mitad de camino,
   como si una línea horizontal atravesara el texto.
   La primera corrección agregaba ADEMÁS un margin-block negativo para
   compensar el espacio de más y no separar las dos líneas del h1 — sin
   poder ver el resultado en un navegador, ese ajuste quedó mal calibrado
   y terminó juntando tanto las dos líneas que probablemente sea la causa
   de que se siguiera viendo cortado (o algo peor: una línea superpuesta con
   la otra). Se saca el margin por completo — las dos líneas del headline
   van a quedar con algo más de aire entre sí de lo que un 0.78 puro daría,
   pero la prioridad ahora es que nunca más se corte una letra, no la
   distancia exacta entre renglones. Si el resultado se ve con demasiado
   aire entre líneas, es un segundo ajuste fácil (achicar este line-height
   un poco, sin volver a agregar margin negativo a ciegas). */
.word { overflow: hidden; display: inline-block; line-height: 1.3; }
/* Bricolage Grotesque sintetiza el italic (no tiene un archivo itálico real)
   inclinando el glyph normal al pintar — ese ángulo puede pintar tinta más
   allá del borde derecho de esta caja con overflow:hidden, sin que el ancho
   de layout de .word lo refleje (ver mismo comentario en base.css, donde se
   detectó cortando la "l" de "feel"). "motion" acá en el hero no llegó a
   cortarse en las pruebas, pero se agrega el mismo colchón preventivo. */
.word:has(em) { padding-right: 0.14em; }
[data-hero-word] .word-inner {
  display: inline-block;
  transform-origin: 0 100%;
  transform: translateY(115%) rotate(5deg);
  filter: blur(4px); /* menos blur por default — desktop lo recupera abajo */
}
@media (min-width: 720px) {
  [data-hero-word] .word-inner { filter: blur(7px); }
}

@media (prefers-reduced-motion: reduce) {
  /* Red de seguridad si GSAP no llega a correr (CDN caído/bloqueado):
     nunca depender de una sola vía para el estado final visible. */
  [data-hero-eyebrow], [data-hero-portrait], [data-hero-sub],
  [data-hero-cta], [data-hero-scroll], [data-hero-word] .word-inner {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    clip-path: none !important;
  }
  /* hero-showreel.js (Fase 2) y manifesto.js tampoco corren con
     reduced-motion, pero por si algún estilo inline quedó escrito de una
     carga anterior, forzamos el estado de reposo igual — nunca depender
     solo del JS para esto. clip-path se suma porque la secuencia de
     entrada del manifiesto (cinco estados) usa clip-path inline como
     máscara de revelado; .manifesto (la sección, no solo las palabras) se
     suma porque el estado 5 (release) le pone un brightness() inline. */
  .showreel__stage,
  .manifesto,
  .manifesto__word {
    transform: none !important;
    filter: none !important;
    opacity: 1 !important;
    clip-path: none !important;
  }
}

/* Fase 7 (QA final): mismo problema que el bloque de reduced-motion de
   arriba, pero para JS caído del todo. [data-hero-eyebrow]/[data-hero-sub]/
   [data-hero-cta]/[data-hero-scroll]/[data-hero-portrait] y las palabras del
   headline arrancan ocultas/clipeadas en su CSS base — solo hero.js las
   lleva a su estado visible (por GSAP o, si GSAP no cargó, por su propio
   fallback inline). Si el JS no corre EN ABSOLUTO (no solo GSAP), ninguno
   de esos dos caminos se ejecuta y el hero entero — la primera pantalla del
   sitio — se queda invisible. .manifesto__word no arranca oculta por CSS
   (a diferencia del hero): su reposo definido en `.manifesto__word` más
   abajo en este archivo ya es la composición final, visible sin animar.
   Igual que .showreel__stage, este reset es una red de seguridad por si
   quedó un estilo inline (transform/filter/clip-path/opacity) de una
   carga anterior con JS. */
.no-js [data-hero-eyebrow],
.no-js [data-hero-portrait],
.no-js [data-hero-sub],
.no-js [data-hero-cta],
.no-js [data-hero-scroll],
.no-js [data-hero-word] .word-inner {
  opacity: 1 !important;
  transform: none !important;
  filter: none !important;
  clip-path: none !important;
}
.no-js .manifesto,
.no-js .manifesto__word {
  transform: none !important;
  filter: none !important;
  opacity: 1 !important;
  clip-path: none !important;
}

/* ---------- Showreel ---------- */
.showreel {
  position: relative;
  background: var(--color-inverse-bg);
  color: var(--color-inverse-ink);
  padding-block: var(--space-2xl);
  --color-ink: var(--color-inverse-ink);
  --color-bg-alt: #ecdba8;
  --color-ink-soft: #4a3a63;
  --color-muted: #6c5c85;
  --overlay-rgb: 23, 11, 41;
  --color-emphasis: var(--color-accent-text);
  --color-mask: var(--color-inverse-bg);
  --color-focus: var(--color-accent-text);
}
.showreel__head {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: var(--space-md);
  margin-bottom: var(--space-lg);
  flex-wrap: wrap;
}
.showreel__title {
  font-size: clamp(2.4rem, 1.5rem + 5vw, 6.5rem);
  text-transform: uppercase;
  max-width: 14ch;
}
.showreel__stage {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 8.5;
  overflow: hidden;
  cursor: none;
  will-change: transform, clip-path;
  /* El clip-path + scale de entrada (se "abre" mientras el hero se
     transforma hacia acá) ahora los maneja hero-showreel.js — Fase 2, el
     mismo pin de GSAP que anima el hero. Sin JS/reduced-motion queda tal
     cual, sin recorte: nunca depender de una sola vía para verse completo. */
}
.showreel__stage video,
.showreel__stage .media-ph { width: 100%; height: 100%; object-fit: cover; }
.showreel__caption {
  display: flex;
  justify-content: space-between;
  margin-top: var(--space-sm);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: rgba(var(--overlay-rgb),0.55);
  border-top: 1px solid rgba(var(--overlay-rgb),0.16);
  padding-top: var(--space-xs);
}

/* ---------- Manifesto — "Evoking emotion through motion" ---------- */
/* Tercera vuelta, esta vez de la COMPOSICIÓN (no solo la animación): la
   versión anterior — cuatro palabras del mismo tamaño en zigzag — se
   rechazó explícitamente ("parece demasiado vacío, arbitrario y poco
   art-directed"). Esta reemplaza esa idea de raíz, no la retoca.

   El concepto nuevo lee la frase como dos unidades desiguales, no cuatro
   palabras iguales:
     — "Evoking" / "through" son el ESQUELETO de la oración (verbo +
       preposición) — el "Evoking ___ through ___." sin las dos palabras
       de contenido. Van chicas, tracked, dorado, como una acotación en voz
       baja, arriba a la izquierda.
     — "emotion" / "motion." son las dos palabras de CONTENIDO — lo que la
       frase realmente dice. Van enormes, apiladas y superpuestas entre sí
       (motion. tira de emotion hacia arriba con un margen negativo),
       alineadas a la derecha, sangrando hacia/más allá del borde — magenta,
       cada vez más grande según baja la vista. "motion." es la más grande
       de las dos, y de toda la sección: es la palabra que manifesto.js
       hace crecer hasta dominar la pantalla en el clímax.
   El hueco entre ambas unidades es deliberado — la tensión que pedía el
   brief no viene de llenar el espacio, viene de dejarlo vacío a propósito
   entre una acotación chica y una declaración enorme.

   El orden de lectura del DOM sigue siendo la oración real (Evoking,
   emotion, through, motion. — ver index.html) para que un lector de
   pantalla la lea completa; el reagrupamiento visual en dos unidades es
   puro `order` de grid acá abajo, nunca reordena el documento. */
.manifesto {
  position: relative;
  /* "Iris glow", sin cambios: el dorado y el magenta de esta sección son
     colores extraídos literalmente de la imagen de referencia del usuario
     (un frame de One Piece — el ojo violeta/dorado/magenta) — #f2d356 es un
     match exacto de un píxel del brillo del iris, y este violeta (#340e64
     en el centro) es el color dominante real de esa misma imagen. La
     composición cambió de raíz (dos veces ya); la paleta de fondo que la
     sostiene, no — sigue siendo el mismo lenguaje de color del resto del
     sitio. */
  background:
    radial-gradient(circle at 50% 45%, #4a1584 0%, #2a0a52 42%, #170a2c 78%),
    radial-gradient(ellipse at 10% 100%, rgba(120, 20, 30, 0.35) 0%, transparent 55%),
    radial-gradient(ellipse at 95% 0%, rgba(120, 20, 30, 0.28) 0%, transparent 50%);
  color: var(--color-ink);
  overflow: hidden;
  padding-block: var(--space-lg);
  --accent-rgb: 224, 21, 216;
  /* Tamaño de la oración YA ENSAMBLADA — una sola línea completa,
     "EVOKING EMOTION THROUGH MOTION." ocupando ~94% del ancho de
     .manifesto__field. Única fuente de verdad, leída también desde
     manifesto.js (mismo coeficiente 6.54vw, ver comentario ahí) para
     que el target del tween de GSAP y este fallback de CSS puro nunca
     queden desincronizados. Se calculó así: medimos la frase completa
     en una sola línea a 100px sobre un frame de referencia de 1528px
     de ancho (measure-singleline.py) — 100/1528 = 6.54vw, así el
     tamaño escala PROPORCIONAL al ancho real del viewport en vez de
     quedar fijo en 100px (que desbordaría en cualquier desktop más
     angosto que 1528px — la mayoría, y acá el margen es más chico que
     en la vuelta anterior porque la línea ya ocupa casi todo el ancho
     a propósito). El piso/techo son un colchón de seguridad, no un
     valor medido. */
  --assembled-fs: clamp(2.2rem, 6.54vw, 8.5rem);
}

/* ============================================================
   Quinta vuelta de Manifiesto — "Dice / Assembly". La vuelta anterior
   (líneas radiando + rail) ya se leía clara, pero el usuario pidió
   sacar el diagrama por completo (sin líneas grises ni puntos
   amarillos — el fondo/dotgrid se queda, "que se ve bien") y cambiar
   la ENTRADA: cada palabra aparece sola, grande, en el centro, y pasa
   a la siguiente con un giro tipo "dado"/rolodex — eso vive en
   manifesto.js (rotationX + backface-visibility:hidden), acá solo
   queda la geometría base:

     1) CICLO (dado) — las 4 palabras comparten el mismo punto central
        (manifesto.js las centra con xPercent/yPercent:-50, no hay CSS
        para esto). No hay "estado fragmentado" en este archivo.

     2) ENSAMBLADO (reposo / accesible / fallback) — la oración
        completa en UNA sola línea, "EVOKING EMOTION THROUGH MOTION.",
        ocupando casi todo el ancho de .manifesto__field. Esta vez el
        estado de reposo del CSS directamente ES el estado final — no
        hace falta un bloque de reduced-motion/no-js aparte (antes sí,
        porque el reposo era el diagrama fragmentado): sin JS o con
        prefers-reduced-motion, initManifesto() nunca corre y la
        oración ya se ve completa, en su lugar, de una. manifesto.js es
        el que la saca de acá con gsap.set() para armar el ciclo.
        Coordenadas medidas una sola vez con getBoundingClientRect()
        sobre la oración en una sola línea (measure-singleline.py),
        ancladas top-left — mismo principio de siempre. Color por
        palabra, permanente (no solo al ensamblar): "emotion" en rosa
        (--color-accent), "motion." en dorado (--color-gold), Evoking/
        through en el ink por defecto. */

.manifesto__dotgrid {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: radial-gradient(rgba(var(--overlay-rgb), 0.16) 1px, transparent 1px);
  background-size: 26px 26px;
  -webkit-mask-image: radial-gradient(circle at 50% 46%, black 0%, black 22%, transparent 62%);
          mask-image: radial-gradient(circle at 50% 46%, black 0%, black 22%, transparent 62%);
  opacity: 0;
}

.manifesto__field {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  padding-block: clamp(4rem, 3rem + 4vw, 6rem);
  /* Perspectiva para el giro tipo dado (rotationX) de manifesto.js —
     se fija acá y no vía transformPerspective por elemento porque los
     4 giran en el MISMO punto central y tienen que leerse como una
     sola "cara" rotando, no cuatro perspectivas independientes. */
  perspective: 1400px;
}
@media (max-width: 719px) {
  .manifesto__field { min-height: 82vh; }
}

.manifesto__word {
  font-family: var(--font-display);
  font-style: normal;
  text-transform: uppercase;
  position: absolute;
  white-space: nowrap;
  will-change: transform, left, top, font-size, opacity;
  color: var(--color-ink);
  z-index: 1;
  letter-spacing: -0.01em;
  line-height: 1;
  /* Reposo = oración YA ENSAMBLADA (ver bloque de arriba). manifesto.js
     la reposiciona con gsap.set() al centro para el ciclo; sin JS esto
     ya es el estado final, accesible, de una. backface-visibility
     evita ver el texto espejado a mitad del giro rotationX. */
  backface-visibility: hidden;
  top: 44.444%;
  font-size: var(--assembled-fs);
}
.manifesto__word--1 { left: 2.927%; }
.manifesto__word--2 { left: 26.058%; }
.manifesto__word--3 { left: 50.034%; }
.manifesto__word--4 { left: 75.636%; }
.manifesto__word--accent { color: var(--color-accent); }
.manifesto__word--gold { color: var(--color-gold); }

@media (max-width: 719px) {
  .manifesto__word {
    top: 48.637%;
    font-size: 1.4375rem; /* 23px — medido, ver measure-singleline.py */
  }
  .manifesto__word--1 { left: 4.959%; }
  .manifesto__word--2 { left: 27.093%; }
  .manifesto__word--3 { left: 50.032%; }
  .manifesto__word--4 { left: 74.527%; }
}
.manifesto__caption {
  /* Hermana de .manifesto__field, no un item suyo (ver index.html) — el
     inset horizontal ya le llega heredado de .edge en la sección. */
  position: relative;
  z-index: 1;
  margin-top: clamp(1.5rem, 1rem + 2vw, 3rem);
  padding-top: var(--space-sm);
  border-top: 1px solid rgba(var(--overlay-rgb),0.16);
  max-width: 44ch;
  font-style: italic;
  color: rgba(var(--overlay-rgb),0.6);
  text-transform: none;
  letter-spacing: 0;
  font-family: var(--font-body);
  font-size: var(--fs-md);
}

/* ---------- Services (what I make) ---------- */
/* Migrado de la sección Disciplines ("What I move between") que se sacó a
   pedido — mismo patrón de fila/hover/preview de imagen, renombrado
   .discipline* -> .service-item*. Único cambio real: el borde usa
   rgba(var(--overlay-rgb),0.16) en vez de var(--color-line), porque esto
   ahora vive dentro de .services (fondo oscuro invertido) y --overlay-rgb
   ya está redefinido ahí para ese contraste — antes lo resolvía el propio
   .service-group con el mismo valor, así que no es un color nuevo. */
.services__list {
  border-top: 1px solid rgba(var(--overlay-rgb),0.16);
  border-bottom: 1px solid rgba(var(--overlay-rgb),0.16);
}
.service-item {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-md) 0;
  border-top: 1px solid rgba(var(--overlay-rgb),0.16);
  gap: var(--space-md);
}
.service-item:first-child { border-top: none; }

.service-item__index {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--color-muted);
  flex: 0 0 auto;
  width: 3.5ch;
}
.service-item__name {
  font-family: var(--font-display);
  font-size: clamp(1.8rem, 1.2rem + 3.5vw, 4.5rem);
  text-transform: uppercase;
  letter-spacing: var(--tracking-tight);
  transition: transform var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
  flex: 1 1 auto;
}
.service-item__tags {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-muted);
  text-align: right;
  flex: 0 0 auto;
  max-width: 22ch;
  opacity: 0;
  transform: translateX(8px);
  transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
.service-item:hover .service-item__name,
.service-item:focus-within .service-item__name {
  transform: translateX(clamp(0.5rem, 1vw, 1.5rem));
  color: var(--color-emphasis);
}
.service-item:hover .service-item__tags,
.service-item:focus-within .service-item__tags {
  opacity: 1;
  transform: translateX(0);
}

/* Preview de imagen que sigue al cursor al hacer hover en una disciplina/proyecto.
   La posición/escala las escribe hover-preview.js cuadro a cuadro (transform,
   con su propio lerp) — clip-path nunca lo toca ese loop, así que puede tener
   su propia CSS transition sin pelearse con el lag por scroll (misma regla
   que evita el "pastoso" en otras piezas del proyecto: nunca una transition
   de CSS y un write de JS por frame sobre la MISMA propiedad). El iris
   cerrado→abierto es el "clip-path" que pide el brief para esta interacción. */
.hover-preview {
  position: fixed;
  top: 0; left: 0;
  width: clamp(220px, 20vw, 340px);
  aspect-ratio: 4/5;
  pointer-events: none;
  z-index: calc(var(--z-cursor) - 1);
  overflow: hidden;
  opacity: 0;
  transform: translate3d(-50%, -50%, 0) scale(0.9);
  clip-path: inset(8% 8% 8% 8% round 999px);
  transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out), clip-path var(--dur-base) var(--ease-emphasis);
  will-change: transform, opacity, clip-path;
}
.hover-preview.is-active {
  opacity: 1;
  transform: translate3d(-50%, -50%, 0) scale(1);
  clip-path: inset(0% 0% 0% 0% round 14px);
}
@media (prefers-reduced-motion: reduce) {
  .hover-preview { transition-duration: 1ms; }
}
.preview-media {
  width: 100%;
  height: 100%;
  transition: opacity 120ms var(--ease-out);
}
.hover-preview img, .hover-preview .media-ph { width: 100%; height: 100%; object-fit: cover; }
@media (hover: none), (pointer: coarse) { .hover-preview { display: none; } }

/* ---------- Selected work ---------- */
/* Portada pinneada de la sección (cabecera + el proyecto feature) — ver
   work-story.js. min-height:100svh + flex/justify-center para que, mientras
   está fijada en pantalla, el contenido se sienta centrado y compuesto como
   un "frame" propio, no como el tope de una lista larga cortada a la mitad.
   Sin JS/GSAP (pin:false) sigue siendo perfectamente legible en flujo
   normal — el min-height solo le da aire, nunca esconde contenido. */
.work-intro {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-lg);
  padding-block: var(--space-lg);
}
/* Fase 7 (QA final): en mobile work-story.js ya no pinea esta sección (ver
   ese archivo) — sin el pin, forzar un frame de 100svh completo antes de
   que aparezca el resto de la lista es solo scroll vacío de más en una
   pantalla chica. Se achica a un alto pensado para que el contenido
   respire, no para llenar la pantalla. */
@media (max-width: 719px) {
  .work-intro { min-height: auto; padding-block: var(--space-xl) var(--space-lg); }
}
.work-list { display: grid; gap: var(--space-md); padding-top: var(--space-lg); }

/* ---------- Frames — mosaico de "Hall of Frames" ---------- */
/* 23 slots para frames sueltos de proyectos distintos — sección propia (ver
   index.html), no ligada a Selected Work ni al Showreel.
   Ajuste de layout (referencia: el panel de mosaico de casos de Inertia
   Studios que mandó el usuario — tiles de tamaño mixto, no una grilla
   pareja de miniaturas idénticas): antes era un `auto-fill` de casillas de
   1:1 todas iguales, que con 23 piezas mínimas se leía más a "hoja de
   contactos" que a sección editorial. Ahora es la retícula real de 12
   columnas del sitio (no un ancho de columna inventado en px como antes) y
   unos pocos frames "destacados" (`--feature`) rompen el ritmo: el doble de
   ancho y en 16:9 en vez de 1:1 — de video real, no ícono cuadrado — como
   si fueran el still que de verdad se eligió mostrar más grande. `dense`
   deja que el resto de los cuadrados chicos rellenen los huecos alrededor
   sin dejar espacios vacíos. */
.frames__label {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-muted);
  margin-bottom: var(--space-sm);
}
.frames__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-flow: dense;
  gap: 3px;
}
@media (min-width: 720px) {
  .frames__grid { grid-template-columns: repeat(12, 1fr); gap: 5px; }
}
.frames__slot {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--color-line);
  aspect-ratio: var(--ph-ratio, 1/1);
  grid-column: span 1;
  padding: 4px;
  /* mismo lenguaje de hover que .work-item/.discipline: el borde (ya
     presente en .media-ph) se enciende al rosa de marca — un placeholder
     también puede sentirse vivo/interactivo, no solo la imagen final.
     border/overflow/aspect-ratio están acá (no solo en .media-ph) porque
     una vez que entra el video real, .frames__slot deja de llevar la
     clase .media-ph (ver home-frame-NN.mp4 en index.html) — este slot es
     su propio wrapper, no uno separado. */
  transition: border-color var(--dur-fast) var(--ease-out);
}
.frames__slot video { width: 100%; height: 100%; object-fit: cover; display: block; }
@media (min-width: 720px) {
  .frames__slot { grid-column: span 2; }
}
.frames__slot:hover,
.frames__slot:focus-visible {
  border-color: var(--color-accent);
}
.frames__slot:hover .media-ph__label,
.frames__slot:focus-visible .media-ph__label {
  color: var(--color-accent);
}
.frames__slot .media-ph__label {
  font-size: 9px;
  letter-spacing: 0;
}

/* Destacados: 4 de los 23 (uno cada tanto, no agrupados) — el doble de
   ancho y en proporción de video real (16:9) para que se lean como el
   still que se eligió resaltar, no como un cuadrado más grande. */
.frames__slot--feature {
  grid-column: span 2;
}
@media (min-width: 720px) {
  .frames__slot--feature { grid-column: span 4; }
}
.frames__slot--feature .media-ph__label {
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
}

.work-item {
  position: relative;
  display: block;
  border-top: 1px solid var(--color-line);
  padding-top: var(--space-md);
  overflow: hidden;
}

.work-item__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
}
.work-item__title {
  font-size: clamp(2rem, 1.2rem + 4vw, 5.5rem);
  text-transform: uppercase;
  letter-spacing: var(--tracking-tight);
  transition: transform var(--dur-base) var(--ease-out);
}
.work-item:hover .work-item__title,
.work-item:focus-visible .work-item__title { transform: translateX(clamp(0.5rem,1vw,1.25rem)); }

.work-item__meta {
  display: flex;
  gap: var(--space-sm);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-muted);
  white-space: nowrap;
}

.work-item__media {
  position: relative;
  overflow: hidden;
}
.work-item__media img, .work-item__media .media-ph {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform 900ms var(--ease-emphasis);
}
.work-item:hover .work-item__media img,
.work-item:hover .work-item__media .media-ph { transform: scale(1.04); }

/* Ritmo deliberado: tamaños distintos por prioridad editorial */
.work-item[data-size="feature"] .work-item__media { aspect-ratio: 16/8; }
.work-item[data-size="large"] .work-item__media   { aspect-ratio: 16/9; }
.work-item[data-size="medium"] .work-item__media  { aspect-ratio: 16/10; max-width: 72%; }
.work-item[data-size="small"] .work-item__media   { aspect-ratio: 4/3; max-width: 46%; }

/* ---------- Services ---------- */
.services {
  background: var(--color-inverse-bg);
  color: var(--color-inverse-ink);
  --color-ink: var(--color-inverse-ink);
  --color-bg-alt: #ecdba8;
  --color-ink-soft: #4a3a63;
  --color-muted: #6c5c85;
  --overlay-rgb: 23, 11, 41;
  --color-emphasis: var(--color-accent-text);
  --color-mask: var(--color-inverse-bg);
  --color-focus: var(--color-accent-text);
}
.services__intro {
  font-size: var(--fs-xl);
  max-width: 22ch;
  margin-bottom: var(--space-lg);
  line-height: var(--lh-snug);
}
.services__intro em { color: var(--color-emphasis); font-style: italic; }

/* La lista de servicios en sí (filas .service-item) vive en la sección
   "Services (what I make)" más arriba en este archivo — migrada del
   patrón de Disciplines, ver el comentario ahí. */

/* ---------- Contact ---------- */
.contact {
  padding-block: var(--space-2xl);
  text-align: left;
}
.contact__headline {
  font-family: var(--font-display);
  font-size: clamp(2.6rem, 1.4rem + 6.5vw, 8rem);
  line-height: var(--lh-tight);
  text-transform: uppercase;
  letter-spacing: var(--tracking-tight);
  max-width: 18ch;
}
.contact__headline em { color: var(--color-emphasis); font-style: italic; }
.contact__cta { margin-top: var(--space-lg); }

.form {
  margin-top: var(--space-xl);
  display: grid;
  gap: var(--space-md);
  max-width: 640px;
}
.field {
  position: relative;
  border-bottom: 1px solid var(--color-line);
  padding-bottom: var(--space-2xs);
}
.field input, .field textarea {
  width: 100%;
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  padding-block: var(--space-2xs);
  resize: none;
}
.field label {
  position: absolute;
  top: var(--space-2xs);
  left: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--color-muted);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  pointer-events: none;
  transition: transform var(--dur-fast) var(--ease-out), opacity var(--dur-fast) var(--ease-out);
  transform-origin: left;
}
.field input:focus + label,
.field input:not(:placeholder-shown) + label,
.field textarea:focus + label,
.field textarea:not(:placeholder-shown) + label {
  transform: translateY(-1.4em) scale(0.85);
  opacity: 0.7;
}
.form__row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-md);
  margin-top: var(--space-xs);
}
.form__submit { justify-self: start; }
.form__status {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-muted);
  min-height: 1.2em;
}
.form__status[data-state="pending"] { color: var(--color-muted); }
.form__status[data-state="success"] { color: var(--color-ink); }
.form__status[data-state="error"] { color: var(--color-accent); }
.form__submit:disabled { opacity: 0.6; cursor: progress; pointer-events: none; }
