@import url('tokens.css?v=1');

/* ===== FUENTES AUTOALOJADAS — cero peticiones a terceros ===== */
@font-face {
  font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 400;
  font-display: swap; src: url('../assets/fonts/PlusJakartaSans-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 500;
  font-display: swap; src: url('../assets/fonts/PlusJakartaSans-500.woff2') format('woff2');
}
@font-face {
  font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 800;
  font-display: swap; src: url('../assets/fonts/PlusJakartaSans-800.woff2') format('woff2');
}
@font-face {
  font-family: 'EB Garamond'; font-style: italic; font-weight: 400;
  font-display: swap; src: url('../assets/fonts/EBGaramond-italic.woff2') format('woff2');
}
@font-face {
  font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400;
  font-display: swap; src: url('../assets/fonts/JetBrainsMono-400.woff2') format('woff2');
}

/* ===== RESET ===== */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
img, video, canvas, svg { display: block; max-width: 100%; height: auto; }
/* Cualquier regla de autor gana al user-agent stylesheet aunque tenga menos
   especificidad — es una cuestión de origen de la cascada, no de puntos.
   Sin esto, la regla de arriba anulaba el `[hidden] { display:none }` del
   navegador: un <img hidden> o <canvas hidden> se seguía pintando, y el
   póster del hero quedaba apilado sobre el canvas del scrub. */
[hidden] { display: none !important; }
/* Sin esto un <button> hereda la fuente del sistema, no Plus Jakarta —
   hallazgo arrastrado de la revisión de la tarea 2. */
button, input, textarea, select { font: inherit; }
ul, ol { list-style: none; padding: 0; }

/* ===== SUPERFICIES POR ZONA =====
   El sitio tiene dos superficies: la clara de siempre (donde se lee y se
   contrata) y la oscura (portada, casos y cierre). Solo cambian estas
   cuatro variables entre una y otra; ninguna regla de más abajo vuelve a
   nombrar un token para fondo o texto de zona. Derivan de los 8 tokens vía
   var()/color-mix, así que el universo cerrado de color sigue intacto y
   test-css-tokens.js sigue pasando.

   Las variables se declaran en [data-zone="dark"] (0,1,0) para que las
   hereden todos los descendientes, pero el PINTADO usa
   section[data-zone="dark"] (0,1,1) a propósito: las dos secciones oscuras
   de abajo llevan también .section--alt, que pinta --off-white más
   adelante en esta misma hoja. Con igual especificidad ganaría .section--alt
   por orden y la zona se quedaría clara — el tipo de fallo que en una
   captura parece un descuido de color y en realidad es una regla perdida.

   Sin JavaScript, sin GSAP o con movimiento reducido esto es TODO lo que
   pinta las zonas oscuras, y basta: el caso degradado queda resuelto aquí,
   no como parche al final. */
:root {
  --surface: var(--white);
  --ink: var(--black);
  --ink-soft: var(--gray-dark);
  --hairline: var(--gray-light);
}
[data-zone="dark"] {
  --surface: var(--black);
  --ink: var(--white);
  --ink-soft: color-mix(in srgb, var(--white) 78%, transparent);
  --hairline: color-mix(in srgb, var(--white) 18%, transparent);
}
section[data-zone="dark"] {
  background: var(--surface);
  color: var(--ink);
}
section[data-zone="dark"] h1,
section[data-zone="dark"] h2,
section[data-zone="dark"] h3,
section[data-zone="dark"] h4 { color: var(--ink); }
section[data-zone="dark"] p { color: var(--ink-soft); }
section[data-zone="dark"] .eyebrow { color: var(--ink-soft); }

/* Dos arreglos de legibilidad que van AQUÍ, no en tareas posteriores:
   los causa esta misma regla de texto claro, y sin ellos este commit
   dejaría el sitio peor que antes. Encontrados en capturas reales; el
   color computado por sí solo no los detecta (era correcto en ambos).

   1) El panel del hero es blanco al 88% (se calculó así para que el texto
      NEGRO se leyera sobre un vídeo muy claro). Con el texto ya en blanco,
      quedaba blanco sobre blanco. Se invierte el mismo recurso, ya
      validado en el proyecto, en vez de improvisar otro. La tarea 5 lo
      sustituirá por el velo a sangre, midiendo antes contra el vídeo.
   2) .btn--ghost escribe en --black: sobre la zona oscura desaparecía y
      solo quedaba el contorno. */
section[data-zone="dark"] .hero__panel {
  background: color-mix(in srgb, var(--black) 72%, transparent);
}
section[data-zone="dark"] .btn--ghost {
  color: var(--ink);
  border-color: var(--hairline);
}
section[data-zone="dark"] .btn--ghost:hover,
section[data-zone="dark"] .btn--ghost:focus-visible { border-color: var(--ink); }

/* ===== CAPA DE FONDO ANIMADO =====
   Fija, detrás de todo, y solo visible cuando hay una zona oscura activa
   (js/zones.js pone data-dark en <html>). En zona clara no se ve NI se
   dibuja: js/motion-field.js detiene el bucle de verdad, no se limita a
   bajar la opacidad — dibujar fotogramas invisibles gasta batería.

   El contenido necesita z-index propio: sin él, el canvas taparía el
   texto en cuanto deja de ser transparente. */
.motion-bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease);
}
[data-dark] .motion-bg { opacity: 1; }
.motion-bg__canvas { width: 100%; height: 100%; display: block; }
.nav, main, .footer { position: relative; z-index: 1; }
/* Con movimiento reducido la capa ni existe: el motor tampoco se crea
   (guarda en js/motion-field.js), así que esto es solo coherencia. */
@media (prefers-reduced-motion: reduce) { .motion-bg { display: none; } }

body {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--step-0);
  line-height: 1.6;
  color: var(--black);
  background: var(--white);
  -webkit-font-smoothing: antialiased;
}

/* ===== TIPOGRAFÍA ===== */
h1, h2, h3, h4 {
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
h1 { font-size: var(--step-4); }
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-1); }
h4 { font-size: var(--step-0); }

p { max-width: var(--measure); color: var(--gray-dark); }
a { color: var(--red-deep); text-underline-offset: 0.2em; }

/* EB Garamond cursiva — USO 1: una palabra dentro de un titular */
.accent {
  font-family: var(--font-accent);
  font-style: italic;
  font-weight: 400;
  letter-spacing: 0;
}

/* EB Garamond cursiva — USO 2: la línea manifiesto */
.manifiesto {
  font-family: var(--font-accent);
  font-style: italic;
  font-size: var(--step-2);
  line-height: 1.3;
  color: var(--black);
  max-width: 22ch;
  margin-inline: auto;
  text-align: center;
}

/* EB Garamond cursiva — USO 3: antetítulos de sección */
.eyebrow {
  font-family: var(--font-accent);
  font-style: italic;
  font-size: var(--step--1);
  color: var(--gray-dark);
  margin-bottom: 0.75rem;
  display: block;
}

.mono { font-family: var(--font-mono); font-size: var(--step--1); }

/* Oculta visualmente sin sacar el contenido del árbol de accesibilidad
   (a diferencia de display:none, que sí lo saca). Usado por los puntos
   del calendario para dar un nombre accesible que no depende de title,
   que los lectores de pantalla no anuncian de forma fiable. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ===== LAYOUT ===== */
.container { width: min(100% - var(--gutter) * 2, var(--container)); margin-inline: auto; }
.section { padding-block: var(--section-y); }
.section--alt { background: var(--off-white); }

/* ===== BOTONES — el rojo rellena, nunca escribe ===== */
.btn {
  display: inline-flex; align-items: center; gap: 0.5rem;
  padding: 0.9rem 1.75rem;
  font-weight: 800; font-size: var(--step-0);
  border-radius: 999px; border: 1px solid transparent;
  text-decoration: none; cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}
.btn--primary { background: var(--red); color: var(--white); }
.btn--primary:hover, .btn--primary:focus-visible { background: var(--red-deep); }
.btn--ghost { background: transparent; color: var(--black); border-color: var(--gray-light); }
.btn--ghost:hover, .btn--ghost:focus-visible { border-color: var(--black); }

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

/* ===== MOVIMIENTO REDUCIDO ===== */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ===== REVEALS DE SCROLL =====
   Estado previo al reveal. Se anula si no hay JS o si hay movimiento
   reducido — de lo contrario una página sin JS queda completamente en
   blanco, porque [data-reveal] arranca en opacity:0. */
[data-reveal] { opacity: 0; }
.no-js [data-reveal], [data-reveal].is-revealed { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  [data-reveal] { opacity: 1 !important; transform: none !important; }
}

/* ===== SKIP LINK ===== */
.skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--black); color: var(--white); padding: 0.75rem 1.25rem;
}
.skip:focus { left: 0; }

/* ===== HERO =====
   Base compartida por el hero de home y el de las páginas internas: solo
   lo que ambos necesitan de verdad (posicionamiento para el vídeo/scrim,
   recorte y fondo). La altura de pantalla completa NO vive aquí — vive en
   el modificador .hero--home. Así ningún otro modificador (.hero--page,
   ni el que venga después) tiene que acordarse de resetearla: la trampa
   de esta regresión fue justo que la base imponía 100svh/flex y
   .hero--page nunca lo deshacía. */
.hero {
  position: relative;
  overflow: hidden;
  background: var(--white);
}
/* v2: vídeo de fondo a pantalla completa, en bucle continuo — sustituye
   al scrub por scroll (ver spec, plan B). 100svh en vez de 100vh: en
   móvil descuenta las barras dinámicas del navegador en vez de dejar un
   hueco/scroll fantasma por debajo del viewport visual. Exclusivo de la
   home: las páginas internas no tienen vídeo y no deben ocupar el
   viewport completo. */
.hero--home {
  min-height: 100svh;
  display: flex;
  align-items: center;
}
.hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}
.hero__tag { color: var(--red-deep); }
/* .hero__sub y .hero__actions también los usan las páginas de servicio y
   el CTA final — el hueco vertical entre ellos aquí lo pone `gap` en
   .hero__content, así que sin escopar a .hero__panel estos dos `margin`
   se acumularían con el gap en TODAS las páginas. */
.hero__sub { font-size: var(--step-1); color: var(--gray-dark); max-width: 46ch; margin: 1.5rem auto 0; }
.hero__actions { display: flex; flex-wrap: wrap; gap: 1rem; justify-content: center; margin-top: 2.5rem; }

.hero__content {
  position: relative;
  z-index: 1;
  width: 100%;
  display: flex;
  justify-content: center;
  padding-block: clamp(2rem, 1rem + 3vw, 3rem);
}
/* Panel-scrim sobre el vídeo. Medido con ffmpeg contra assets/video/hero-v2-1440.mp4
   (13 instantes muestreados a lo largo del bucle de 6,13s, y dentro de
   cada uno una rejilla 6×4 para pillar el bloque MÁS OSCURO, no la media
   del fotograma — el Ferrari deja zonas casi negras bajo el chasis y en
   las llantas que una media general esconde). Con --white al 88% de
   opacidad el contraste del bloque más oscuro de todo el muestreo
   (t=0,75s) sobre --black es 15,70:1, sobre --red-deep 5,47:1 y sobre
   --gray-dark 9,02:1 — los tres pasan AA (4,5:1) con margen, no al
   límite. Ver hero-video-report.md para la tabla completa. */
.hero__panel {
  background: color-mix(in srgb, var(--white) 88%, transparent);
  border-radius: 1.5rem;
  padding: clamp(2rem, 1.25rem + 3vw, 3.5rem) clamp(1.5rem, 1rem + 2vw, 3rem);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(0.85rem, 0.5rem + 1.2vw, 1.5rem);
  max-width: 48rem;
  text-align: center;
}
/* Especificidad de contexto (0,1,1 / 0,2,0), no !important: solo pisa el
   margin de las reglas de arriba cuando .hero__sub / .hero__actions
   están dentro del panel del hero — las páginas de servicio no tienen
   .hero__panel y no se ven afectadas. */
.hero__panel h1 { font-size: clamp(2rem, 1.1rem + 3.6vw, 3.75rem); }
.hero__panel .hero__sub { margin: 0; max-width: 42ch; }
.hero__panel .hero__actions { margin-top: 0; }

.hero--page { padding-block: clamp(4rem, 3rem + 5vw, 8rem) var(--section-y); text-align: center; }

/* ===== NAV ===== */
.nav {
  position: sticky; top: 0; z-index: 50;
  background: var(--white);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-fast) var(--ease);
}
.nav.is-scrolled {
  border-bottom-color: var(--gray-light);
  background: color-mix(in srgb, var(--white) 82%, transparent);
  backdrop-filter: saturate(180%) blur(14px);
}
.nav__inner { display: flex; align-items: center; justify-content: space-between; gap: 1.5rem; min-height: 72px; }
.nav__logo { display: flex; align-items: center; text-decoration: none; }
.nav__logo img { height: 32px; width: auto; }
.nav__menu { display: flex; align-items: center; gap: 1.75rem; }
.nav__menu a { color: var(--black); text-decoration: none; font-weight: 500; font-size: var(--step--1); }
.nav__menu a.is-active { color: var(--red-deep); }
.nav__menu a:hover { color: var(--red-deep); }
/* `.nav__menu a` (0,1,1) le ganaba en especificidad a `.nav__cta` suelto
   (0,1,0) — el CTA salía negro sobre rojo, y en :hover salía rojo oscuro
   sobre rojo oscuro (texto invisible). Repetir el selector completo del
   contexto sube la especificidad por encima de `.nav__menu a` y de
   `.nav__menu a:hover` en cada estado. */
.nav__menu a.nav__cta {
  color: var(--white);
  font-weight: 800;
  font-size: var(--step-0);
}
.nav__menu a.nav__cta:hover,
.nav__menu a.nav__cta:focus-visible {
  color: var(--white);
}
.nav__toggle { display: none; background: none; border: 0; padding: 0.5rem; cursor: pointer; }
.nav__toggle span { display: block; width: 24px; height: 2px; background: var(--black); }
.nav__toggle span + span { margin-top: 6px; }

/* El spec dice "768: nav a hamburguesa", pero medido: con los 5 enlaces +
   CTA reales, el menú horizontal ya envuelve texto a dos líneas por
   debajo de ~1000px (comprobado forzando el layout horizontal con CDP a
   901–999px: "Creación de Contenido", "Formación IA" y el CTA se
   parten). Bajar el corte a 768px solo alargaría esa franja ya rota, no
   la arreglaría. Se mantiene en 900 a propósito — es una excepción
   documentada, no una inconsistencia; de las 5 franjas medidas en la
   revisión (1100/900/768/700/560) es la única que sigue viva junto a
   1100 y 768, y coincide con el 900 que ya usan footer/campañas/
   formación. Ver spec §Responsive. */
@media (max-width: 900px) {
  .nav__toggle { display: block; }
  .nav__menu {
    position: absolute; inset: 72px 0 auto; flex-direction: column; align-items: flex-start;
    gap: 1.25rem; padding: var(--gutter); background: var(--white);
    border-bottom: var(--border); display: none;
  }
  .nav__menu.is-open { display: flex; }
}

/* ===== REJILLAS ===== */
.grid { display: grid; gap: 1.5rem; margin-top: 3rem; }
.grid--3 { grid-template-columns: repeat(3, 1fr); }
.grid--4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 1100px) { .grid--3, .grid--4 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 768px) { .grid--3, .grid--4 { grid-template-columns: 1fr; } }

/* ===== TARJETAS — bordes, nunca sombras ===== */
.card {
  display: block; padding: 2rem; background: var(--white);
  border: var(--border); border-radius: 1rem;
  text-decoration: none; color: inherit;
  transition: border-color var(--dur-fast) var(--ease);
}
a.card:hover { border-color: var(--red); }
.card--plain { background: transparent; }
.card__kicker { color: var(--gray-dark); margin-bottom: 1rem; }
.card h3 { margin-bottom: 0.75rem; }
.card__link { display: inline-block; margin-top: 1.25rem; font-weight: 800; color: var(--red-deep); font-size: var(--step--1); }

/* ===== SPLIT ===== */
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-top: 3rem; }
@media (max-width: 1100px) { .split { grid-template-columns: 1fr; } }
.split__col { padding: 2rem; border: var(--border); border-radius: 1rem; }
.split__col--accent { background: var(--red-tint); border-color: var(--red-tint); }
.split__col h3 { margin-bottom: 1.25rem; }

/* ===== LISTAS ===== */
.list { list-style: none; padding: 0; }
.list li { position: relative; padding-left: 1.75rem; margin-bottom: 0.75rem; color: var(--gray-dark); max-width: var(--measure); }
.list li::before { position: absolute; left: 0; font-weight: 800; }
.list--minus li::before { content: '—'; color: var(--gray-dark); }
.list--plus li::before { content: '+'; color: var(--red-deep); }

/* ===== FOOTER ===== */
.footer { border-top: var(--border); padding-block: var(--section-y) 2rem; }
.footer__grid { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 2.5rem; }
.footer__grid a { display: block; color: var(--gray-dark); text-decoration: none; margin-bottom: 0.5rem; font-size: var(--step--1); }
.footer__grid a:hover { color: var(--red-deep); }
.footer__logo { height: 36px; width: auto; margin-bottom: 1rem; }
.footer__legal { margin-top: 3rem; padding-top: 1.5rem; border-top: var(--border); }
.footer__legal p { font-size: var(--step--1); }
@media (max-width: 900px) { .footer__grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 768px) { .footer__grid { grid-template-columns: 1fr; } }

/* ===== PASOS ===== */
.steps { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.5rem; margin-top: 3rem; }
.steps li { padding-top: 1.5rem; border-top: 2px solid var(--red); }
.steps__num { display: block; color: var(--red-deep); margin-bottom: 0.75rem; }
.steps h3 { margin-bottom: 0.5rem; }
@media (max-width: 1100px) { .steps { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 768px) { .steps { grid-template-columns: 1fr; } }

/* ===== PROCESO — scrub por scroll a pantalla completa =====
   Misma técnica que la v1 del hero (js/scroll-scrub.js, genérico) pero
   con el tratamiento visual de la v2 del hero: media de fondo a sangre
   completa + panel-scrim encima, en vez de una caja contenida. El pin
   envuelve TANTO la media como el contenido, así que texto y fotograma
   se mueven juntos durante todo el scrub — el texto está encima del
   vídeo todo el tiempo, no debajo de un bloque que se despinea antes. */
.proceso { position: relative; background: var(--white); }
.proceso__pin {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: center;
}
.proceso__media { position: absolute; inset: 0; z-index: 0; }
/* object-fit:cover ya recorta la imagen/canvas a los límites de su propia
   caja — no hace falta overflow:hidden en .proceso__pin para contenerla,
   y así el contenido (texto) puede crecer más de 100svh en móvil (4
   tarjetas apiladas) sin que nada se recorte por error. */
.proceso__media img, .proceso__media canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}
.proceso__content {
  position: relative;
  z-index: 1;
  width: 100%;
  padding-block: clamp(1.5rem, 1rem + 2vw, 3rem);
}
/* Panel-scrim — misma fórmula que .hero__panel (color-mix desde un
   token, respeta test-css-tokens.js). Medido con un muestreo EXHAUSTIVO
   de los 80 fotogramas de la tira de escritorio, no unos pocos
   instantes: para cada fotograma se tomó el bloque más oscuro dentro de
   una rejilla 14×10 sobre casi todo el encuadre (3–97% de ancho, 5–95%
   de alto — más amplio que el hueco real del panel, a propósito, para
   que el resultado valga sin importar dónde caiga el panel en cada
   breakpoint). El peor caso de los 80 fotogramas es (1,1,1) — casi negro
   puro, en el hueco de rueda de los primeros fotogramas muy cerrados
   sobre la llanta — y con el panel al 88% el contraste ahí es 5,25:1
   contra --red-deep (el texto más exigente que usa el panel), 8,66:1
   contra --gray-dark y 15,07:1 contra --black: los tres pasan AA (4,5:1)
   con margen, no al límite. Detalle completo en proceso-scrub-report.md. */
.proceso__panel {
  background: color-mix(in srgb, var(--white) 88%, transparent);
  border-radius: 1.25rem;
  padding: clamp(1rem, 0.6rem + 1.2vw, 1.5rem) clamp(1.25rem, 0.8rem + 1.5vw, 2rem);
  max-width: 34rem;
  margin-inline: auto;
  text-align: center;
}
/* El h2 de sección (--step-3, hasta 4,5rem) es demasiado alto para caber
   en un solo viewport junto con la rejilla de pasos debajo — igual que
   .hero__panel h1 reduce el tamaño del titular del hero, aquí se reduce
   el del h2 solo dentro de este panel. */
.proceso__panel h2 { font-size: clamp(1.75rem, 1.2rem + 2.4vw, 2.75rem); }
.proceso__panel .eyebrow { margin-bottom: 0.4rem; }
/* .proceso__steps reutiliza el grid de .steps (mismos breakpoints) y añade
   el mismo scrim por tarjeta, para que cada paso siga siendo legible sea
   cual sea el fotograma que quede detrás en ese punto del scroll. Este
   grid de 4 columnas (cayendo a 1 bajo 768px, igual que cualquier otro
   .steps del sitio) es el estado de base: lo que ve quien no tiene JS,
   tiene GSAP roto o movimiento reducido — ver más abajo por qué ya no
   necesita un breakpoint especial. */
.proceso__steps { margin-top: 1rem; gap: 1rem; }
.proceso__steps li {
  background: color-mix(in srgb, var(--white) 88%, transparent);
  border-radius: 0.85rem;
  padding: 1rem 1.1rem 1.25rem;
}
.proceso__steps h3 { font-size: var(--step-0); }
.proceso__steps p { font-size: var(--step--1); }

/* .is-scrubbing: solo scroll-scrub.js la añade, y solo cuando además crea
   el ScrollTrigger que muestra los pasos de uno en uno (data-scrub-steps).
   Apila los 4 <li> en la MISMA celda de grid (no position:absolute, así
   el alto del contenedor sigue al hijo visible sin salto de layout) en
   vez de en columnas, y los centra como una sola tarjeta — el "un paso
   centrado sustituye a las 4 columnas" que pidió el cliente. Selector de
   2 clases: gana por especificidad a .proceso__steps y a su @media de
   768px de arriba en cualquier breakpoint, sin depender del orden en el
   fichero. Esto es también lo que retira el problema de desbordamiento en
   phone compacto que antes obligaba a un breakpoint especial a 768px: con
   un solo paso en pantalla en vez de 4 apilados, el contenido pineado ya
   no se acerca a superar el viewport (ver proceso-scrub-report.md, tabla
   de alturas). El fallback sin JS/movimiento-reducido no pinea nada, así
   que aunque mostrara los 4 a la vez tampoco habría "trampa" de scroll —
   pero de todas formas ya no los muestra a la vez: sin la clase, el <ol>
   se queda en el grid de 4 columnas → 1 de arriba, con las 4 tarjetas en
   flujo normal, visibles y accesibles. */
.proceso__steps.is-scrubbing {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-areas: "step";
  max-width: 26rem;
  margin-inline: auto;
}
.proceso__steps.is-scrubbing > li { grid-area: step; }

/* ===== PLANES — 4 columnas, la destacada algo más ancha ===== */
.plans { display: grid; grid-template-columns: 1fr 1fr 1fr 1.15fr; gap: 1.25rem; margin-top: 3rem; align-items: start; }
@media (max-width: 1100px) { .plans { grid-template-columns: 1fr 1fr; } }
@media (max-width: 768px) { .plans { grid-template-columns: 1fr; } }

.plan { padding: 2rem 1.75rem; border: var(--border); border-radius: 1rem; }
.plan--featured { border-color: var(--red); border-width: 2px; }
/* En móvil el plan recomendado va primero. */
@media (max-width: 768px) { .plan--featured { order: -1; } }

.plan__badge { color: var(--red-deep); margin-bottom: 0.5rem; }
.plan h3 { font-size: var(--step-1); margin-bottom: 0.5rem; }
.plan__price { font-size: var(--step-2); font-weight: 800; color: var(--black); line-height: 1; margin-bottom: 0.25rem; }
.plan__vat { font-size: var(--step--1); font-weight: 500; color: var(--gray-dark); }
.plan__period { font-size: var(--step--1); color: var(--gray-dark); margin-bottom: 1.5rem; }
.plan .list { margin-bottom: 2rem; }
.plan .btn { width: 100%; justify-content: center; }
.plans__note { margin: 2rem auto 0; text-align: center; font-size: var(--step--1); }

/* ===== CAMPAÑAS ===== */
.campaigns { display: grid; grid-template-columns: 1.2fr 1fr; gap: 3rem; align-items: start; }
@media (max-width: 900px) { .campaigns { grid-template-columns: 1fr; } }
.campaigns__price { color: var(--red-deep); font-size: var(--step-1); margin: 1.5rem 0; }
.campaigns__list { margin-top: 0.5rem; }
.campaigns__disclaimer {
  grid-column: 1 / -1; margin-top: 1rem; padding-top: 1.5rem;
  border-top: var(--border); font-size: var(--step--1); max-width: none;
}

/* ===== CALENDARIO ===== */
/* Estado previo a la animación de los puntos del calendario. Se anula si
   no hay JS o si hay movimiento reducido, igual que en los reveals de
   sección — de lo contrario una página sin JS deja el calendario vacío. */
.dot[data-piece] { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .dot[data-piece] { opacity: 1 !important; }
}

.calendar { margin-top: 3rem; border: var(--border); border-radius: 1rem; overflow: hidden; }
.calendar__head, .calendar__week { display: grid; grid-template-columns: 8rem repeat(5, 1fr); align-items: center; }
.calendar__head { background: var(--off-white); border-bottom: var(--border); }
.calendar__head span { padding: 0.75rem 1rem; font-size: var(--step--1); font-weight: 800; color: var(--gray-dark); }
.calendar__week { border-bottom: var(--border); min-height: 4.5rem; }
.calendar__week:last-child { border-bottom: 0; }
.calendar__week > span { padding: 1rem; height: 100%; display: flex; align-items: center; border-left: var(--border); }
.calendar__label { border-left: 0 !important; font-size: var(--step--1); font-weight: 800; color: var(--gray-dark); }

.dot { display: inline-block; width: 12px; height: 12px; border-radius: 999px; background: var(--red); }
.dot--video { width: 28px; border-radius: 999px; }
.calendar__legend { margin-top: 1.25rem; color: var(--gray-dark); display: flex; align-items: center; gap: 0.4rem; }

@media (max-width: 768px) {
  .calendar__head, .calendar__week { grid-template-columns: 4.5rem repeat(5, 1fr); }
  .calendar__head span, .calendar__week > span { padding: 0.6rem 0.4rem; }
  .calendar__label { font-size: 0.7rem; }
}

/* ===== CASOS =====
   Tarjetas 9:16 completas (antes 4:5): el gesto de la llave y la
   aparición del bolso viven en los extremos verticales del plano, y
   recortarlos a 4:5 los habría destruido (decisión de cliente). En el
   contenedor de 1320px con gap 1.5rem, 3 columnas caen en ~424×753px
   (objetivo: ~420×747). El .grid--3 heredado ya colapsa a 2 columnas
   <1100px y a 1 columna <768px — de ahí sale el colapso sensato en
   móvil, sin reglas nuevas: tres vídeos 9:16 en fila en un teléfono
   habría sido inutilizable. */
.case__media {
  position: relative;
  border-radius: 1rem;
  aspect-ratio: 9 / 16;
  overflow: hidden;
  background: var(--off-white);
}
.case__media img, .case__media video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}
/* En reposo lo que se ve SIEMPRE es el <img> de póster (fotograma real
   extraído, no un <video> en pausa) — el vídeo arranca en opacity:0.
   js/case-videos.js añade/quita .is-playing en el <figure> al
   reproducir/parar; la transición hace de crossfade entre póster y
   vídeo sin que el DOM tenga que intercambiar nodos. */
.case__video { opacity: 0; transition: opacity var(--dur-fast) var(--ease); }
.case.is-playing .case__video { opacity: 1; }
.case figcaption { margin-top: 0.75rem; font-size: var(--step--1); color: var(--gray-dark); }

/* Bloque SARAUTO: antes era el pie de las tres tarjetas (.case__note);
   con tres clientes distintos eso ya no es honesto (una tarjeta es un
   bolso). Separado con su propia regla superior + eyebrow para que se
   lea como un cliente destacado, no como la leyenda de la rejilla. */
.case__sarauto { margin-top: 3rem; padding-top: 2rem; border-top: var(--border); }
.case__sarauto p { margin-top: 0.5rem; font-size: var(--step--1); }

/* Alcance geográfico: coda tras el cliente destacado, no un bloque nuevo.
   Sin regla superior propia — otro border-top aquí competiría con el del
   bloque SARAUTO de arriba; un margen menor (1.5rem vs. los 3rem de
   separación de sección) basta para leerse como cierre discreto. */
.case__reach { margin-top: 1.5rem; font-size: var(--step--1); }

/* ===== FORMACIÓN ===== */
.formacion { display: grid; grid-template-columns: 1.4fr 1fr; gap: 3rem; align-items: start; }
@media (max-width: 900px) { .formacion { grid-template-columns: 1fr; } }
.modules { list-style: none; padding: 0; margin-top: 2rem; }
.modules li { display: flex; gap: 1rem; padding: 0.9rem 0; border-bottom: var(--border); color: var(--gray-dark); }
.modules .mono { color: var(--red-deep); }
.formacion__price { padding: 2rem; border: 2px solid var(--red); border-radius: 1rem; text-align: center; }
.formacion__price .btn { width: 100%; justify-content: center; }
.formacion__meta { margin-top: 1rem; font-size: 0.75rem; color: var(--gray-dark); }

/* ===== CTA FINAL ===== */
.cta { text-align: center; }
.cta p { margin-inline: auto; }
.cta__trust { margin-top: 2rem; color: var(--gray-dark); }

/* ===== PORTAFOLIO =====
   Cinco piezas, mosaico controlado a mano — NO masonry algorítmico. Solo
   pósters en la rejilla (nunca vídeo en la carga de página); el clic abre
   el lightbox (ver lightbox más abajo y js/portfolio-lightbox.js). */
.portfolio-tile { display: block; }
.portfolio-tile__media {
  position: relative;
  border-radius: 1rem;
  overflow: hidden;
  background: var(--off-white);
  height: 100%;
}
.portfolio-tile__media img { width: 100%; height: 100%; object-fit: cover; }
/* Proporción base — única fuente de verdad en mobile/tablet, donde no hay
   grid-row:span (ver rejilla de escritorio más abajo). En escritorio la
   celda de grid ya fuerza la altura (stretch), así que aspect-ratio queda
   ahí sin efecto: no hace falta una regla de breakpoint aparte. */
.portfolio-tile--h .portfolio-tile__media { aspect-ratio: 16 / 9; }
.portfolio-tile--v .portfolio-tile__media { aspect-ratio: 9 / 16; }

/* Botón-disparador: cubre TODA la miniatura (no solo el icono), así que
   el "click a un tile" del spec es toda la tarjeta, un objetivo grande.
   El icono se desplaza hacia arriba con padding-bottom (en vez de vivir
   centrado en toda la caja) para que nunca quede debajo de la cinta de
   texto — ver .portfolio-tile__caption más abajo. Es un margen genérico,
   no calculado por tile: la caption vive DENTRO de la miniatura como
   overlay (no debajo, en flujo normal), así que nunca puede desbordar la
   celda de grid ni empujar el alto de la figura más allá de lo que su
   grid-row:span reserva — el motivo real por el que esto es robusto. */
.portfolio-tile__trigger {
  position: absolute; inset: 0; width: 100%; height: 100%;
  background: transparent; border: 0; padding: 0; margin: 0; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  padding-bottom: 3rem; box-sizing: border-box;
}
.portfolio-tile__play {
  display: flex; align-items: center; justify-content: center;
  width: 3.5rem; height: 3.5rem; border-radius: 999px;
  background: var(--red);
  transition: background-color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.portfolio-tile__play svg { width: 1.1rem; height: 1.1rem; fill: var(--white); }
.portfolio-tile__trigger:hover .portfolio-tile__play,
.portfolio-tile__trigger:focus-visible .portfolio-tile__play {
  background: var(--red-deep);
  transform: scale(1.08);
}
/* El botón ya tiene su propio outline en :focus-visible (regla global);
   sin este reset heredaría también el halo por defecto del navegador. */
.portfolio-tile__trigger:focus { outline: none; }
.portfolio-tile__trigger:focus-visible { outline: 2px solid var(--red-deep); outline-offset: 3px; }

/* Cinta de texto ANCLADA dentro de la miniatura (no un figcaption debajo,
   en flujo normal) — así la altura de la figura es SIEMPRE exactamente
   la de .portfolio-tile__media (aspect-ratio o grid-row:span, según
   breakpoint) y nunca depende de cuánto ocupe el título/descripción.
   Con figcaption en flujo normal el texto se salía por debajo de la
   celda de grid reservada y quedaba tapado por el tile siguiente de la
   misma columna — bug real, encontrado al revisar la captura de
   pantalla del mosaico. El scrim usa color-mix desde --black (mismo
   patrón que .hero__panel/.proceso__panel con --white) para que el
   texto blanco sea legible sobre CUALQUIER fotograma, claro u oscuro. */
.portfolio-tile__caption {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 1.25rem 1.25rem 1.1rem;
  background: color-mix(in srgb, var(--black) 74%, transparent);
  /* La cinta pinta ENCIMA del botón (va después en el DOM): sin esto,
     un clic/toque sobre el texto no abría el vídeo. */
  pointer-events: none;
}
.portfolio-tile__kicker { color: var(--white); margin-bottom: 0.3rem; }
.portfolio-tile__caption h3 {
  color: var(--white);
  font-size: var(--step-0);
  margin-bottom: 0.3rem;
}
.portfolio-tile__caption p {
  color: color-mix(in srgb, var(--white) 82%, transparent);
  font-size: var(--step--1);
  max-width: none;
}

/* Pieza destacada: fuera de la rejilla, a todo el ancho, por encima del
   mosaico — "es la declaración de apertura de la página" (spec). Cinta
   de texto algo más grande, para que la prominencia visual coincida con
   la prominencia narrativa (es la apertura de la página). */
.portfolio-featured { margin-top: 3rem; }
.portfolio-featured .portfolio-tile__media { border-radius: 1.25rem; }
.portfolio-featured .portfolio-tile__caption { padding: 2rem 2rem 1.75rem; }
.portfolio-featured .portfolio-tile__caption h3 { font-size: var(--step-2); }

/* Rejilla de las otras diez piezas — mosaico fijo, no masonry.

   Geometría real (contenedor 1320px, gap 1,5rem/24px, 3 columnas):
     1 col  = 424px  → 9:16 mide 753,8px
     2 cols = 872px  → 16:9 mide 490,5px
     3 cols = 1320px → 16:9 mide 742,5px
   Unidad de fila R=8px (0,5rem) + gap 24px: un ítem que abarca N tracks
   mide 32N-24 (técnica de "masonry sin JS" vía grid-row):
     16:9 a 2 cols → N=16 → 488px (-0,5%)
     9:16 a 1 col  → N=24 → 744px (-1,3%)
     16:9 a 3 cols → N=24 → 744px (+0,2%) — mismo alto que un vertical

   Empaquetado (5 horizontales + 5 verticales): un 16:9 a todo el ancho
   solo cabe cuando las tres columnas están a la misma altura, así que el
   mosaico va por tramos que cierran a ras — cero huecos y cero columna
   corta al final (el reparto anterior de 4 piezas dejaba 512px de
   diferencia):
     Tramo 1 (tracks 1-48)   Leapmotor + Lambo apilados en col.1 (24+24);
                             Yacht + Kia + Real Estate en cols 2-3 (16×3).
     Tramo 2 (tracks 49-72)  Ducati a todo el ancho.
     Tramo 3 (tracks 73-96)  Los tres UGC verticales, uno por columna.
     Tramo 4 (tracks 97-120) Matrix RYBL a todo el ancho.
   El orden del DOM sigue este mismo orden visual (tabulación coherente). */
.portfolio-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: 0.5rem;
  gap: 1.5rem;
  margin-top: 3rem;
}
.pt-leapmotor  { grid-column: 1 / 2; grid-row: 1 / 25; }
.pt-yacht      { grid-column: 2 / 4; grid-row: 1 / 17; }
.pt-kia        { grid-column: 2 / 4; grid-row: 17 / 33; }
.pt-lambo      { grid-column: 1 / 2; grid-row: 25 / 49; }
.pt-realestate { grid-column: 2 / 4; grid-row: 33 / 49; }
.pt-ducati     { grid-column: 1 / 4; grid-row: 49 / 73; }
.pt-ugc        { grid-column: 1 / 2; grid-row: 73 / 97; }
.pt-ugc-rybl   { grid-column: 2 / 3; grid-row: 73 / 97; }
.pt-cafe       { grid-column: 3 / 4; grid-row: 73 / 97; }
.pt-matrix     { grid-column: 1 / 4; grid-row: 97 / 121; }

@media (max-width: 1100px) {
  /* grid-auto-flow:dense vuelve a hacer falta: Leapmotor y Lambo (9:16)
     están separados en el DOM por dos 16:9 que ocupan fila entera; sin
     dense, la celda junto a Leapmotor se quedaría vacía. */
  .portfolio-grid { grid-template-columns: repeat(2, 1fr); grid-auto-rows: auto; grid-auto-flow: dense; }
  /* Especificidad (0,2,0) > la de cada .pt-* (0,1,0): en esta franja se
     abandona el mosaico fijo y cada tile vuelve al flujo normal, con su
     propia aspect-ratio (arriba) marcando la altura. */
  .portfolio-grid > .portfolio-tile { grid-column: auto; grid-row: auto; }
  /* Los 16:9 ocupan todas las columnas disponibles. 1/-1 (no 1/3) para
     que tampoco desborde en el breakpoint de 1 columna. */
  .portfolio-grid > .portfolio-tile--h { grid-column: 1 / -1; }
  /* 5 verticales en 2 columnas → uno queda suelto. Los tres UGC pasan
     detrás de Matrix para que el suelto sea el ÚLTIMO de la rejilla (fin
     de masonry normal) y no un hueco en mitad, antes de Matrix. */
  .pt-ugc, .pt-ugc-rybl, .pt-cafe { order: 1; }
}
@media (max-width: 768px) {
  .portfolio-grid { grid-template-columns: 1fr; }

  /* FIX móvil (defecto de cliente): a ~340px de ancho la cinta de texto
     medía 163px sobre un póster 16:9 de 193px — tapaba la imagen entera
     y el círculo de play quedaba DEBAJO de la cinta, así que tocarlo no
     abría nada (medido por CDP con elementFromPoint). En móvil la cinta
     sale de encima del póster y pasa debajo, en flujo normal: aquí la
     rejilla es de 1 columna con filas auto, así que el motivo original
     del overlay (no desbordar la celda de grid del mosaico) no aplica. */
  .portfolio-tile--h .portfolio-tile__media,
  .portfolio-tile--v .portfolio-tile__media { aspect-ratio: auto; height: auto; background: var(--black); }
  .portfolio-tile__media img { display: block; height: auto; }
  .portfolio-tile--h .portfolio-tile__media img { aspect-ratio: 16 / 9; }
  .portfolio-tile--v .portfolio-tile__media img { aspect-ratio: 9 / 16; }
  .portfolio-tile__caption,
  .portfolio-featured .portfolio-tile__caption { position: static; background: none; padding: 0.85rem 1rem 1rem; }
  .portfolio-featured .portfolio-tile__caption h3 { font-size: var(--step-1); }
  /* El botón sigue cubriendo póster + cinta (toda la tarjeta es tocable:
     un absolute pinta por encima de la cinta estática), pero el círculo
     se centra sobre el PÓSTER, no sobre la tarjeta entera. Un margin-top
     en % se resuelve contra el ANCHO del contenedor, así que la mitad del
     alto del póster es 50%×9/16 = 28,125% (16:9) o 50%×16/9 = 88,889%
     (9:16), menos la mitad del círculo (3,5rem/2). */
  .portfolio-tile__trigger { align-items: flex-start; padding-bottom: 0; }
  .portfolio-tile--h .portfolio-tile__play { margin-top: calc(28.125% - 1.75rem); }
  .portfolio-tile--v .portfolio-tile__play { margin-top: calc(88.889% - 1.75rem); }
}

/* ===== LIGHTBOX DEL PORTAFOLIO =====
   <dialog nativo con showModal(): el navegador ya resuelve gratis el
   trampado de foco dentro del diálogo, el cierre con Escape y volver
   inert el resto del documento mientras está abierto (los tres requisitos
   más delicados del spec) — ver js/portfolio-lightbox.js para el resto
   (recordar/devolver el foco al tile que lo abrió, pausar+resetear el
   vídeo al cerrar, no autoplay con movimiento reducido). El sonido usa
   los controles NATIVOS de <video controls> — ahí ya hay un botón de
   silencio accesible por teclado, sin reinventar uno a mano. */
/* Deliberadamente NO se toca `display` aquí (el navegador centra un
   <dialog> modal con su propio mecanismo interno — no es flexbox/auto-
   margin normal — y forzar display:flex en el propio <dialog> rompía ese
   centrado: el diálogo aterrizaba en (0,0) en vez de en el centro del
   viewport, encontrado al medir su getBoundingClientRect por CDP). El
   apilado vertical de vídeo+caption lo da el flujo normal de bloque de
   .portfolio-lightbox__inner, que no necesita flex para eso. */
/* --lb-vmax = alto máximo del vídeo, compartido por __inner y __video.
   svh (no vh) donde exista: en Safari de iPhone 1vh es el viewport SIN
   barras del navegador, así que 78vh + la caption se salían por abajo de
   la zona visible y tapaban los controles nativos del vídeo. svh es el
   viewport CON barras — lo que el visitante ve de verdad. En móvil se
   reserva además más sitio para caption + botones (66). */
.portfolio-lightbox { --lb-vmax: 78vh; }
@supports (height: 1svh) { .portfolio-lightbox { --lb-vmax: 78svh; } }
@media (max-width: 768px) {
  .portfolio-lightbox { --lb-vmax: 66vh; }
  @supports (height: 1svh) { .portfolio-lightbox { --lb-vmax: 66svh; } }
  .portfolio-lightbox__caption { padding: 0.9rem 1.1rem 1rem; }
}
.portfolio-lightbox {
  border: 0; padding: 0; background: transparent;
  max-width: min(94vw, 1200px); max-height: 92vh; max-height: 92svh;
  /* fit-content (el valor del propio UA para <dialog>), NO auto: con
     auto + inset:0 el diálogo se estiraba a todo el hueco disponible y
     la caja negra quedaba pegada arriba a la izquierda dentro de él —
     con un vídeo 9:16 en escritorio, a x=120px en vez de centrada. */
  width: fit-content; height: fit-content;
  /* El reset global `* { margin: 0; }` anula el margin:auto con el que el
     navegador centra un <dialog> modal (position:fixed + inset:0 +
     margin:auto) — sin esto el diálogo aterriza en la esquina (0,0) en
     vez de centrado. Encontrado midiendo getComputedStyle por CDP. */
  margin: auto;
}
/* El scrim del fondo se deriva de --black vía color-mix (no rgba) — la
   misma regla que aplica al resto de la hoja. */
.portfolio-lightbox::backdrop {
  background: color-mix(in srgb, var(--black) 78%, transparent);
}
/* El ancho de esta caja seguía el ancho del contenido (shrink-to-fit):
   con un vídeo 9:16 el propio <video> se queda estrecho (lo limita
   78vh de alto), pero el .portfolio-lightbox__caption de debajo es más
   ancho que eso y estiraba la caja negra entera — dejaba una franja
   negra a la derecha del vídeo. Confirmado midiendo por CDP: con un
   vídeo 1080×1920 la caja medía 1200px mientras el vídeo medía solo
   ~395px.
   Arreglo: fijar el ANCHO explícitamente a partir de la proporción
   real del vídeo (--lb-w/--lb-h, que js/portfolio-lightbox.js rellena
   en 'loadedmetadata' con video.videoWidth/videoHeight). La fórmula
   replica exactamente las restricciones de .portfolio-lightbox__video
   (min(94vw,1200px) de ancho tope, 78vh de alto tope) para que ambas
   cajas midan siempre lo mismo, sea cual sea la proporción del vídeo
   — así el caption se envuelve DENTRO del ancho del vídeo en vez de
   dictarlo. El valor de respaldo 16/9 solo se usa mientras la caja
   está oculta (ver [data-ready] más abajo), nunca se pinta. */
.portfolio-lightbox__inner {
  position: relative;
  background: var(--black);
  border-radius: 1rem;
  overflow: hidden;
  width: min(94vw, 1200px, calc(var(--lb-vmax) * var(--lb-w, 16) / var(--lb-h, 9)));
}
/* Mientras no se conocen las proporciones reales del vídeo recién
   cargado (justo tras abrir el diálogo, antes de 'loadedmetadata') la
   caja se oculta con opacity — NO visibility:hidden, que además de
   ocultar sacaría al botón de cerrar (autofocus, descendiente de esta
   caja) del foco justo cuando showModal() intenta enfocarlo — para no
   pintar nunca un tamaño equivocado que luego "salte" al correcto: un
   flash de tamaño incorrecto en cada apertura sería peor que el bug
   que se arregla. js/portfolio-lightbox.js añade data-ready en cuanto
   fija --lb-w/--lb-h, en el mismo instante, así que el primer pintado
   visible ya sale con el tamaño definitivo. */
.portfolio-lightbox__inner {
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}
.portfolio-lightbox__inner[data-ready] { opacity: 1; }
.portfolio-lightbox__video {
  display: block;
  max-width: min(94vw, 1200px);
  max-height: var(--lb-vmax);
  width: auto; height: auto;
}
.portfolio-lightbox__close {
  position: absolute; top: 0.75rem; right: 0.75rem; z-index: 2;
  width: 2.75rem; height: 2.75rem; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--black) 55%, transparent);
  border: 0; cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease);
}
.portfolio-lightbox__close svg { width: 1.15rem; height: 1.15rem; fill: none; stroke: var(--white); stroke-width: 2.4; stroke-linecap: round; }
.portfolio-lightbox__close:hover,
.portfolio-lightbox__close:focus-visible { background: var(--red-deep); }
.portfolio-lightbox__caption { padding: 1.25rem 1.5rem 1.5rem; }
.portfolio-lightbox__kicker { color: color-mix(in srgb, var(--white) 82%, transparent); margin-bottom: 0.3rem; }
.portfolio-lightbox__caption h3 { color: var(--white); margin-bottom: 0; }

/* ===== LEGAL — páginas de prosa (aviso legal, privacidad, cookies) =====
   Sin hero de vídeo, sin scrub, sin vídeos de casos: son páginas ligeras
   de texto. `p`, `.list li` y `.legal-info` ya heredan --measure (65ch)
   de las reglas generales de arriba, así que el bloque de prosa no
   necesita una regla de ancho propia — solo estos dos elementos nuevos:
   la fecha de actualización bajo el hero y el bloque de datos
   identificativos/contacto. */
.legal__meta { margin-top: 1.5rem; }
.legal-info {
  border: var(--border);
  border-radius: 1rem;
  padding: 2rem;
  margin-top: 2rem;
  max-width: var(--measure);
}
.legal-info dt { font-weight: 800; font-size: var(--step--1); color: var(--gray-dark); margin-top: 1rem; }
.legal-info dt:first-child { margin-top: 0; }
.legal-info dd { margin: 0.15rem 0 0; color: var(--black); }
.legal-info dd + dt { margin-top: 1rem; }
