/* ==========================================================================
   BASE — reset, elementos, tipografía base, grid, utilidades
   ========================================================================== */

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

html {
  /* Fondo oscuro dominante desde el rebrand — quedó en "light" de la
     paleta anterior, lo que hace que el autofill nativo de Safari/Chrome
     (nombre, email) pinte un recuadro claro encima de un input oscuro. */
  color-scheme: dark;
  -webkit-text-size-adjust: 100%;
  scroll-behavior: auto; /* el smooth scroll lo controla JS con Lenis-like custom easing */
}

body {
  background: var(--color-bg);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: var(--lh-normal);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
  /* Fjalla One (--font-display) es de un solo peso — sin esto, cualquier
     font-weight:500/700/800 que siga puesto sobre ella (hay bastantes,
     ver tokens.css) le pediría al navegador una negrita FALSA (engrosado
     sintético, feo, no el diseño real de la tipografía). font-synthesis:
     none corta eso de raíz en todo el sitio; --font-body (Instrument
     Sans) sí tiene sus pesos reales cargados, así que esto no le saca
     nada — solo evita que se invente lo que no existe. */
  font-synthesis: none;
}

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

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

button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
}

ul, ol { list-style: none; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 500;
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
}

input, textarea {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  outline: none;
}

/* Visible, deliberate focus states — never remove outline without replacing it */
:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 4px;
  border-radius: var(--radius-sm);
}

::selection {
  background: var(--color-ink);
  color: var(--color-bg);
}

/* Skip link — first interactive element, visible on focus only */
.skip-link {
  position: fixed;
  top: var(--edge);
  left: var(--edge);
  z-index: 999;
  background: var(--color-ink);
  color: var(--color-bg);
  padding: 0.75em 1.25em;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  transform: translateY(-160%);
  transition: transform var(--dur-fast) var(--ease-out);
}
.skip-link:focus {
  transform: translateY(0);
}

/* Cross-document View Transitions nativas (Chrome/Edge 126+). En navegadores
   sin soporte esta regla se ignora sin error, y transitions.js aplica el
   fallback con overlay — nunca los dos a la vez. */
@view-transition {
  navigation: auto;
}
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

/* Transición real entre páginas — no el cross-fade que trae el navegador
   por default. La página vieja se queda quieta, de fondo (sin animación);
   la nueva entra encima con una cortina en clip-path que sube desde abajo
   (mismo lenguaje que el resto del sitio: las cosas entran subiendo desde
   abajo, nunca apareciendo/desapareciendo en el lugar). mix-blend-mode se
   resetea a normal — el truco de crossfade nativo del navegador usa
   plus-lighter para mezclar viejo+nuevo, y eso "quema"/aclara de más
   justo en la franja que ya se reveló si lo dejamos puesto. */
::view-transition-old(root) {
  animation: none;
  mix-blend-mode: normal;
}
::view-transition-new(root) {
  animation: page-wipe-reveal var(--dur-slow) var(--ease-emphasis) both;
  mix-blend-mode: normal;
}
@keyframes page-wipe-reveal {
  from { clip-path: inset(100% 0 0 0); }
  to   { clip-path: inset(0% 0 0 0); }
}

/* ---------- Layout shell ---------- */
.edge {
  padding-left: var(--edge);
  padding-right: var(--edge);
}

.grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--grid-gap);
}

@media (min-width: 720px) {
  .grid { grid-template-columns: repeat(8, 1fr); }
}

@media (min-width: 1080px) {
  .grid { grid-template-columns: repeat(12, 1fr); }
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}

/* ---------- Typographic utilities ---------- */
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--color-muted);
}

.italic-accent {
  font-style: italic;
  font-weight: 400;
}

/* ---------- Section number pattern (00, 01, 02...) ---------- */
.section-index {
  display: inline-block; /* un <span> inline no acepta transform — lo necesita el fade-up + el reveal.js */
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  letter-spacing: var(--tracking-wide);
  color: var(--color-muted);
  font-variant-numeric: tabular-nums; /* el contador no debe "bailar" de ancho cuadro a cuadro */
}

/* ---------- Reveal primitives (JS toggles data-state) ---------- */
[data-reveal] {
  --reveal-y: 0.6em;
}

/* El blur de entrada es más chico en mobile por default — no es "lo mismo
   escalado", es reconocer que el filter:blur() recompone caro justo en los
   dispositivos con menos GPU. Desktop (min-width:720px) recupera el blur
   completo, más notorio en pantallas grandes de todas formas. */
[data-reveal="lines"] .line-inner {
  display: block;
  transform-origin: 0 100%;
  transform: translateY(105%) rotate(3deg);
  filter: blur(2px);
  will-change: transform, filter;
}
[data-reveal="lines"].is-visible .line-inner {
  transform: translateY(0) rotate(0deg);
  filter: blur(0);
  transition: transform var(--dur-slow) var(--ease-emphasis), filter var(--dur-slow) var(--ease-out);
}
@media (min-width: 720px) {
  [data-reveal="lines"] .line-inner { filter: blur(3px); }
}

/* Igual técnica que "lines" arriba, pero por palabra — cada .word-inner
   tiene su propio transition-delay (inline, vía reveal.js) así que se
   revelan en ola, no todas juntas. .word necesita ser inline-block (no
   inline) para que overflow:hidden pueda enmascarar el translateY de su
   .word-inner — el espacio " " entre spans (fuera de ellos, en el HTML) es
   lo que deja que el texto siga haciendo wrap normal entre palabras. */
[data-reveal="words"] .word {
  display: inline-block;
  overflow: hidden;
  /* Buffer de altura propio, independiente del line-height (a veces muy
     ajustado, --lh-tight) del título que lo contiene — si no, el
     overflow:hidden de la máscara puede cortar ascendentes/descendentes a
     mitad de letra en vez de enmascarar solo el desplazamiento vertical.
     Mismo fix que se aplicó en el headline del hero tras reportarse el bug
     ahí (se lee cortada por una "línea" horizontal cruzando el texto) — acá
     se agrega preventivamente porque el mecanismo es idéntico en cualquier
     título grande que use data-reveal="words" (h1 de cada case study,
     declaración de about).
     El margin-block negativo compensa el alto extra que este line-height le
     suma al renglón (si no, un párrafo de varias líneas con esto se vería
     con más interlineado del pensado) — un valor moderado, pensado para
     funcionar razonablemente en cualquier contexto que use este sistema, no
     ajustado de más para un caso puntual. */
  line-height: 1.15;
  margin-block: -0.08em;
}
/* Bricolage Grotesque no tiene un italic real (0 archivos woff2 itálicos en
   ningún peso) — el navegador la sintetiza inclinando (oblique) las formas
   normales. Ese oblique es un efecto de PINTADO, no de layout: el ancho de
   caja que calcula el navegador para .word sigue siendo el del glyph SIN
   inclinar, así que la tinta inclinada de la última letra puede pintarse más
   allá del borde derecho de la caja — y overflow:hidden se lo come. Se
   detectó en "feel" (about, la "l" final cortada); getBoundingClientRect no
   lo detecta (el layout box no cambia, solo lo pintado), así que este margen
   es preventivo para cualquier palabra en cursiva dentro de este sistema de
   máscara, no solo la que se vio rota — "MOVE." (contacto) y "motion" (hero)
   no llegaron a cortarse en las pruebas, pero por las dudas quedan con el
   mismo colchón. */
[data-reveal="words"] .word:has(em) {
  padding-right: 0.14em;
}
[data-reveal="words"] .word-inner {
  display: inline-block;
  transform: translateY(105%) rotate(3deg);
  filter: blur(2px);
  will-change: transform, filter;
}
[data-reveal="words"].is-visible .word-inner {
  transform: translateY(0) rotate(0deg);
  filter: blur(0);
  transition: transform var(--dur-slow) var(--ease-emphasis), filter var(--dur-slow) var(--ease-out);
}
@media (min-width: 720px) {
  [data-reveal="words"] .word-inner { filter: blur(3px); }
}

/* Igual técnica otra vez, ahora letra por letra — pensado para firmas/
   nombres en tipografía grande (la del footer), no para párrafos. Misma
   salvedad que "words": .letter tiene que ser inline-block para que
   overflow:hidden enmascare a .letter-inner. Los espacios entre palabras
   quedan como texto literal (reveal.js no los envuelve), así que el
   wordspacing normal no se pierde.
   .letter tiene su PROPIO line-height, más generoso que el del contenedor
   — si no, con una firma grande usando un line-height muy ajustado (como
   .footer__signature, pensada para leerse casi pegada), el overflow:hidden
   de cada letra corta ascendentes/descendentes a la mitad, como si una
   línea cruzara el texto (el mismo bug que se corrigió en el headline del
   hero — la caja de la máscara nunca puede depender del line-height ajustado
   del texto que la rodea). */
[data-reveal="letters"] .letter {
  display: inline-block;
  overflow: hidden;
  line-height: 1.15;
  margin-block: -0.08em;
}
[data-reveal="letters"] .letter-inner {
  display: inline-block;
  transform: translateY(105%) rotate(4deg);
  filter: blur(2px);
  will-change: transform, filter;
}
[data-reveal="letters"].is-visible .letter-inner {
  transform: translateY(0) rotate(0deg);
  filter: blur(0);
  transition: transform var(--dur-slow) var(--ease-emphasis), filter var(--dur-slow) var(--ease-out);
}
@media (min-width: 720px) {
  [data-reveal="letters"] .letter-inner { filter: blur(3px); }
}

[data-reveal="fade-up"] {
  opacity: 0;
  transform: translateY(var(--reveal-y)) scale(0.985);
  filter: blur(3px);
  transition: opacity var(--dur-slow) var(--ease-emphasis), transform var(--dur-slow) var(--ease-emphasis), filter var(--dur-slow) var(--ease-out);
}
@media (min-width: 720px) {
  [data-reveal="fade-up"] { filter: blur(5px); }
}
[data-reveal="fade-up"].is-visible {
  opacity: 1;
  transform: translateY(0) scale(1);
  filter: blur(0);
}

[data-reveal="mask"] {
  overflow: hidden;
}
[data-reveal="mask"] img,
[data-reveal="mask"] video {
  transform: scale(1.12);
  transition: transform 1.4s var(--ease-emphasis);
}
[data-reveal="mask"]::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--color-mask, var(--color-bg));
  transform-origin: bottom;
  transition: transform 900ms var(--ease-emphasis);
}
[data-reveal="mask"] { position: relative; }
[data-reveal="mask"].is-visible::after { transform: scaleY(0); }
[data-reveal="mask"].is-visible img,
[data-reveal="mask"].is-visible video { transform: scale(1); }

@media (prefers-reduced-motion: reduce) {
  [data-reveal="lines"] .line-inner,
  [data-reveal="words"] .word-inner,
  [data-reveal="letters"] .letter-inner,
  [data-reveal="fade-up"],
  [data-reveal="mask"] img,
  [data-reveal="mask"] video {
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
    transform: none !important;
    filter: none !important;
  }
  /* [data-reveal="mask"]::after es la cortina que TAPA el contenido en su
     estado de reposo (sin transform propio = cobertura completa) y lo
     revela con `.is-visible::after { transform:scaleY(0) }`. Iba en la
     lista de arriba con `transform:none !important` — pero eso es
     exactamente el estado CERRADO de la cortina, no un "sin animación,
     mostrado ya". Con !important de por medio la especificidad de
     `.is-visible` no importa: `none` le ganaba siempre a `scaleY(0)`, así
     que cualquier persona con reduced-motion se quedaba con TODO el
     contenido detrás de máscara (case-hero__cover, media-grid, retrato del
     hero, stage del showreel, retrato de about) permanentemente oculto.
     Se saca de la lista de arriba y se fuerza acá el estado REVELADO, no
     el neutro. */
  [data-reveal="mask"]::after {
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
    transform: scaleY(0) !important;
  }
}

/**
 * Fase 7 (QA final): resiliencia sin JS. `<html class="no-js">` está en el
 * markup de las 7 páginas y main.js lo saca apenas corre — pero eso significa
 * que si el JS no carga (CDN caído, script bloqueado, error de red), la
 * clase se queda puesta y CADA [data-reveal] del sitio, que arranca oculto
 * por CSS (opacity:0, líneas/palabras tapadas, máscara cerrada) nunca recibe
 * `.is-visible` — nada más que reveal.js se la agrega. Sin este bloque, todo
 * el contenido con data-reveal queda invisible para siempre en ese
 * escenario. Mismo criterio que el bloque de reduced-motion de arriba:
 * fuerza el estado ya-revelado, no un estado neutro nuevo.
 */
.no-js [data-reveal="lines"] .line-inner,
.no-js [data-reveal="words"] .word-inner,
.no-js [data-reveal="letters"] .letter-inner,
.no-js [data-reveal="fade-up"],
.no-js [data-reveal="mask"] img,
.no-js [data-reveal="mask"] video {
  transition-duration: 1ms !important;
  transition-delay: 0ms !important;
  transform: none !important;
  filter: none !important;
  opacity: 1 !important;
}
.no-js [data-reveal="mask"]::after {
  transition-duration: 1ms !important;
  transform: scaleY(0) !important;
}
