/* ==========================================================================
   ESTO NO ES UN MUSEO — Design System "Architectural Terminal"
   2026-09-18: adoptado de "Opción 02 — Architectural Terminal Resend"
   (Oscar-Knowledge-Vault/008 - Por Clasificar/analisis desing/). Dos
   desviaciones deliberadas del documento, confirmadas con Oscar: se
   mantiene --signature en #C6001F en vez de #FF0000 (ese rojo ya tiene la
   matemática de contraste WCAG resuelta esta sesión; #FF0000 es aún peor
   contra fondos oscuros) y se mantiene la escala --sp-* de 8px en vez de
   migrar a la de 4px del documento (equivalente funcional, remapear 100+
   usos no se justifica por una diferencia imperceptible).
   ========================================================================== */

:root {
  /* Paleta — ver Base de Diseño.md */
  --white:        #FFFFFF;
  --ink:          #0A0A0A;
  --signature:    #C6001F;
  --surface-gray: #F5F5F7;

  /* Neutros funcionales del documento (bordes/texto secundario, no acentos) */
  --graphite:     #292D30;
  --ash:          #A1A4A5;
  --line-light:   #CACACA;

  /* Tipografía: Inter real (carga vía Google Fonts en cada HTML) en vez del
     stack de sistema — necesario para bold 700/800 real, no negrita
     sintética. Mono nuevo para numeración/specs/estados ("voz terminal"),
     JetBrains Mono en vez de Commit Mono del doc (no está en Google Fonts,
     es el siguiente en su misma lista de fallback). */
  /* 2026-09-19: Inter → IBM Plex Sans — pedido explícito de mejorar
     legibilidad. Plex Sans tiene x-height más alto y contadores más
     abiertos que Inter (diseñada para UI/lectura en pantalla), y empareja
     de forma nativa con --font-mono (misma familia tipográfica, IBM Plex
     Mono ya vivía en su lista de fallback). Inter se queda como
     alternativa antes del stack de sistema, no se elimina de golpe. */
  --font: "IBM Plex Sans", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;

  /* Tipografía fluida — fórmula clamp(min, preferred, max).
     --fs-display y --fs-h2 usan los valores exactos del documento
     (--text-display/--text-heading). --fs-lede/--fs-body subidos ~6-7%
     (pedido explícito de "mejorar la visión" / legibilidad del cuerpo). */
  --fs-display: clamp(3.5rem, 9vw, 6rem);
  --fs-h2:      clamp(2.25rem, 5.4vw, 3.5rem);
  --fs-lede:    clamp(1.125rem, 1rem + 0.5vw, 1.5rem);
  --fs-body:    clamp(1.0625rem, 1rem + 0.2vw, 1.1875rem);
  /* 2026-09-19: subido ~8% — usado por eyebrows/labels rojos entre otras
     cosas; el pedido de "aumentar el tamaño" de los rojos se resuelve
     aquí para todos a la vez (más consistente que un override por
     selector), y de paso ayuda a la legibilidad general de texto chico. */
  --fs-small:   clamp(0.8125rem, 0.75rem + 0.15vw, 0.9375rem);

  /* Grid de espaciado — múltiplos de 8px */
  --sp-1: 0.5rem;   /* 8px */
  --sp-2: 1rem;     /* 16px */
  --sp-3: 1.5rem;   /* 24px */
  --sp-4: 2rem;     /* 32px */
  --sp-6: 3rem;     /* 48px */
  --sp-8: 4rem;     /* 64px */
  --sp-12: 6rem;    /* 96px */
  --sp-16: 8rem;    /* 128px */

  /* Radio de controles (botones/inputs) — nuevo, el sistema anterior no
     tenía radio en ningún control. Las tarjetas ya usan var(--sp-2)=16px,
     que coincide con --radius-card del documento sin cambio. */
  --radius-control: 6px;

  /* Curvas cubic-bezier a medida — ver Propuesta de Sitio Web.md.
     --ease-terminal y --duration-reveal son del documento: reveals más
     cortos y "snappy" (420ms) que los 800ms actuales. */
  --ease-snap:      cubic-bezier(0.4, 0, 0.2, 1);     /* respuesta inmediata: botones, hover */
  --ease-glide:      cubic-bezier(0.25, 1, 0.5, 1);    /* desaceleración suave: tarjetas al entrar */
  --ease-overshoot:  cubic-bezier(0.5, 0, 0, 1.5);     /* elasticidad controlada: CTAs */
  --ease-terminal:   cubic-bezier(0.22, 1, 0.36, 1);   /* reveals de scroll, estilo terminal */
  --duration-reveal: 420ms;

  --max-width: 1200px;
  --gutter: clamp(1.25rem, 4vw, 4rem);
}

/* ---------- Reset mínimo ---------- */
* { margin: 0; padding: 0; box-sizing: border-box; }

html {
  overflow-x: hidden;
  /* 2026-09-15: el centrado por CSS (margin:0 auto) usa el ancho del
     viewport SIN la scrollbar vertical, así que en navegadores donde la
     scrollbar ocupa espacio real (Windows/Linux, no overlay) todo el
     contenido centrado queda ~scrollbar-width/2 corrido a la izquierda
     del centro visual real de la ventana — reportado como "el título de
     Festival se ve descentrado" (2026-09-15), medido y confirmado: la
     sección/tarjeta/título estaban perfectamente centrados ENTRE SÍ, el
     desfase era contra el centro de la ventana. scrollbar-gutter:stable
     reserva ese espacio siempre, así que el cálculo de centrado ya lo
     contempla. overflow-y:scroll es el equivalente de siempre para
     navegadores que no soportan scrollbar-gutter todavía: fuerza el
     mismo gutter reservado sin necesitar medir el ancho real de la
     scrollbar. */
  overflow-y: scroll;
  scrollbar-gutter: stable;
}

body {
  font-family: var(--font);
  background: var(--white);
  color: var(--ink);
  font-size: var(--fs-body);
  /* 2026-09-19: 1.5 → 1.65 — más aire entre líneas, pedido explícito de
     legibilidad. Los títulos tienen su propio line-height ajustado aparte
     (no heredan este), así que esto solo afecta cuerpo/lede/párrafos. */
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }
ul, ol { list-style: none; }

/* 2026-09-18 (Architectural Terminal): foco visible sitewide — gap real de
   accesibilidad que el sistema anterior no cubría de forma explícita.
   Mismo color de acento que el resto del sitio, sin ornamento nuevo. */
:where(a, button, input, textarea, select, [tabindex]):focus-visible {
  outline: 2px solid var(--signature);
  outline-offset: 3px;
}

/* 2026-09-18: política global de reducción de movimiento (sección 9.4 del
   documento) — complementa, no reemplaza, los bloques específicos de
   layout que ya existían (lateral-scroll, [data-reveal], #modulo, más
   abajo en este archivo), que siguen resolviendo su propio fallback
   estructural. Esta regla nueva cubre cualquier animación/transición que
   no tuviera ya un caso especial. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------- Skip link (WCAG 2.4.1) ---------- */
/* Oculto fuera de pantalla por default, visible solo al recibir foco de
   teclado — mismos tokens que el resto del sitio (--ink/--white/--sp-*/
   --ease-snap), sin agregar ningún color u ornamento nuevo. */
.skip-link {
  position: absolute;
  top: -100px;
  left: var(--gutter);
  z-index: 1000;
  padding: var(--sp-2) var(--sp-3);
  background: var(--ink);
  color: var(--white);
  font-size: var(--fs-small);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  transition: top 200ms var(--ease-snap);
}

.skip-link:focus {
  top: var(--sp-3);
}

/* Textura sutil (ruido fractal SVG embebido) en vez de gradientes genéricos */
.grain {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 999;
  opacity: 0.025;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ---------- Header ---------- */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--sp-3) var(--gutter);
  background: rgba(255, 255, 255, 0.85);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid rgba(10, 10, 10, 0.06);
}

.wordmark {
  position: relative;
  font-size: var(--fs-small);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.14em;
}

.wordmark-no {
  color: #C6001F;
}

.site-nav {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  font-size: var(--fs-small);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.site-nav a {
  position: relative;
  transition: color 180ms var(--ease-snap);
}

.site-nav a:hover { color: var(--signature); }

.nav-cta {
  padding: var(--sp-1) var(--sp-3);
  border: 1px solid var(--ink);
  border-radius: var(--radius-control);
  transition: transform 180ms var(--ease-snap), background 180ms var(--ease-snap), color 180ms var(--ease-snap);
}

.nav-cta:hover {
  background: var(--ink);
  color: var(--white);
  transform: translateY(-2px);
}

.site-nav a.active { color: var(--signature); }

/* Tap targets ampliados a ≥44px sin cambiar tamaño visual del texto — hit
   area invisible vía pseudo-elemento. DESIGN.md §1 ("ausencia de ornamento")
   descarta agrandar padding/texto visible como solución; esto no dibuja
   nada, solo extiende la zona clickeable/tocable más allá del glifo. */
.wordmark::after,
.site-nav a::after {
  content: "";
  position: absolute;
  inset: -14px;
}

/* ---------- Menú móvil: overlay de pantalla completa (Fase A) ----------
   Reemplaza el viejo `.site-nav a:not(.nav-cta) { display: none; }`, que
   solo ocultaba Empresas/Experiencias sin dar ninguna forma de llegar a
   ellas en móvil — no había menú, ni roto ni parcial. .nav-cta ("Contacto")
   se queda en el header tal cual, sin tocarlo, en todos los anchos. */
.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  flex-shrink: 0;
}

.nav-toggle-icon,
.nav-toggle-icon::before,
.nav-toggle-icon::after {
  display: block;
  width: 22px;
  height: 2px;
  background: var(--ink);
  transition: transform 220ms var(--ease-glide), opacity 220ms var(--ease-glide), background 220ms var(--ease-glide);
}

.nav-toggle-icon { position: relative; }

.nav-toggle-icon::before,
.nav-toggle-icon::after {
  content: "";
  position: absolute;
  left: 0;
}

.nav-toggle-icon::before { top: -7px; }
.nav-toggle-icon::after { top: 7px; }

.nav-toggle[aria-expanded="true"] .nav-toggle-icon { background: transparent; }
.nav-toggle[aria-expanded="true"] .nav-toggle-icon::before { transform: translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle-icon::after { transform: translateY(-7px) rotate(-45deg); }

/* .site-header sube por encima del overlay (1500) SOLO mientras está
   abierto, así el botón (ya en su estado X) se queda clickeable arriba del
   overlay para poder cerrarlo. Fuera de .menu-open sigue en 100 de
   siempre — no cambia nada del stacking actual. */
html.menu-open .site-header { z-index: 1600; }

/* Bloquea el scroll del body detrás del overlay mientras está abierto. */
html.menu-open { overflow: hidden; }

.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: 1500;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--sp-6);
  padding: calc(var(--sp-16) + env(safe-area-inset-top))
           calc(var(--gutter) + env(safe-area-inset-right))
           calc(var(--sp-8) + env(safe-area-inset-bottom))
           calc(var(--gutter) + env(safe-area-inset-left));
  background: var(--white);
  opacity: 0;
  transform: translateY(-12px);
  transition: opacity 320ms var(--ease-glide), transform 320ms var(--ease-glide);
}

.mobile-menu[hidden] { display: none; }

.mobile-menu.is-open {
  opacity: 1;
  transform: translateY(0);
}

/* Única línea roja de acento — mismas medidas que .hero-line, ningún hex
   nuevo. Pseudo-elemento, no un nodo extra: el overlay solo contiene los
   3 links en el HTML. */
.mobile-menu::before {
  content: "";
  display: block;
  width: 64px;
  height: 3px;
  background: var(--signature);
}

.mobile-menu a {
  font-size: var(--fs-display);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.1;
  color: var(--ink);
}

.mobile-menu a[aria-current="page"] { color: var(--signature); }

@media (min-width: 861px) {
  .nav-toggle,
  .mobile-menu {
    display: none !important;
  }
}

/* ---------- Layout base de sección ---------- */
.section {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: var(--sp-16) var(--gutter);
}

.section-eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--signature);
  margin-bottom: var(--sp-4);
}

.section-title {
  font-size: var(--fs-h2);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.05;
  margin-bottom: var(--sp-4);
  max-width: 20ch;
}

.section-lede {
  font-size: var(--fs-lede);
  font-weight: 400;
  max-width: 52ch;
  color: rgba(10, 10, 10, 0.72);
}

/* ---------- Botones flotantes de contacto (WhatsApp / Instagram) ----------
   Persistentes en las 3 páginas. Por defecto arriba a la derecha, debajo del
   header (top: var(--sp-16) para no chocar con .site-header, que es fixed).
   En Empresas se usa el modificador --bottom para moverlos abajo a la
   derecha (instrucción específica de esa página). */
.social-float {
  position: fixed;
  top: var(--sp-16);
  right: var(--gutter);
  z-index: 150;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.social-float--bottom {
  top: auto;
  bottom: var(--sp-6);
}

.social-float-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  box-shadow: none;
  transition: transform 300ms var(--ease-glide);
}

.social-float-btn:hover {
  transform: translateY(-2px);
}

.social-float-btn img {
  /* Artwork is a wide lockup (~1.92:1), not a square icon — size by
     height and let width follow the PNG's real aspect ratio instead of
     forcing a square box, so nothing gets padded or cropped. */
  height: 56px;
  width: auto;
  object-fit: contain;
}

/* ---------- Hero ---------- */
.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--sp-8) var(--gutter);
}

/* 2026-09-17 (debate-ui, Inicio): sin esto el primer viewport de #hero es
   blanco puro arriba/abajo del bloque centrado — nada donde anclar el
   escaneo visual (NN/g: ~57% del tiempo de página ocurre above-the-fold).
   Escenado a #hero, no a la clase base .hero, porque Empresas/Experiencias
   comparten esa clase vía .hero.hero--sub y no están en el alcance de esta
   ronda. */
#hero {
  background: var(--surface-gray);
}

/* Contenedor compartido para título+lede: antes cada uno calculaba su ancho
   por separado (.hero-title intrínseco, .hero-sub a 46ch) sin ningún token
   en común, así que sus bordes no alineaban de forma predecible. */
.hero-content {
  max-width: 60rem;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.hero-line {
  width: 64px;
  height: 3px;
  background: var(--signature);
  margin-bottom: var(--sp-6);
}

/* Paso intermedio que ya usa el resto del sitio (eyebrow → título → lede,
   ver .section-eyebrow) — el Hero era el único lugar que saltaba directo
   de --fs-display a --fs-lede sin ningún escalón entre medio. Mismos
   valores que .section-eyebrow, sin inventar un segundo lenguaje. */
.hero-eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--signature);
  margin-bottom: var(--sp-4);
}

.hero-title {
  /* 700, no 800: IBM Plex Sans no tiene peso 800 real — 800 forzaría
     negrita sintética (más borrosa) en vez de un glifo diseñado. */
  font-weight: 700;
  font-size: var(--fs-display);
  letter-spacing: -0.02em;
  line-height: 0.98;
  text-transform: uppercase;
  overflow: hidden;
}

.hero-title .line { display: block; }

.hero-sub {
  margin-top: var(--sp-6);
  font-size: var(--fs-lede);
  font-weight: 400;
  max-width: 46ch;
  color: rgba(10, 10, 10, 0.65);
}

/* Escenado a #hero: control de huérfanas en un párrafo corto y centrado
   pensado para leerse como una sola unidad — no se toca .hero-sub base
   porque también la usan Empresas/Experiencias (--hero-sub), fuera del
   alcance de esta ronda. */
#hero .hero-sub {
  text-wrap: balance;
}

/* CTA que faltaba dentro del propio Hero — antes el único elemento
   accionable en el primer viewport era el "Contacto" fijo del nav,
   desconectado del contenido. Reusa el patrón de interacción exacto de
   .nav-cta, ningún lenguaje visual nuevo. */
.hero-cta {
  display: inline-block;
  margin-top: var(--sp-6);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--ink);
  border-radius: var(--radius-control);
  font-size: var(--fs-small);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink);
  transition: transform 180ms var(--ease-snap), background 180ms var(--ease-snap), color 180ms var(--ease-snap);
}

.hero-cta:hover {
  background: var(--ink);
  color: var(--white);
  transform: translateY(-2px);
}

.hero-scroll-cue {
  position: absolute;
  bottom: var(--sp-6);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-1);
  font-size: var(--fs-small);
  letter-spacing: 0.2em;
  /* Contraste WCAG AA (texto normal, 4.5:1): el 0.4 daba ~2.7:1 sobre blanco.
     0.65 es el token de "texto secundario" que ya usa .hero-sub/.section-lede,
     no un valor nuevo — da ~6.3:1. */
  color: rgba(10, 10, 10, 0.65);
}

.hero-scroll-cue span {
  width: 1px;
  height: 32px;
  background: rgba(10, 10, 10, 0.25);
}

/* ---------- Concepto / insight ---------- */
.statement {
  font-size: var(--fs-h2);
  font-weight: 700;
  line-height: 1.2;
  max-width: 22ch;
  margin-bottom: var(--sp-8);
}

/* 2026-09-15: pedido de Oscar para 001 — bajar el tamaño del statement y
   resaltar frases clave en rojo. --fs-lede en vez de un valor nuevo: ya es
   el token que usa .section-lede/.hero-sub para texto de apoyo grande, así
   que el statement pasa de leerse como titular a leerse como lede con
   énfasis puntual. Se ensancha max-width porque a este tamaño 22ch se veía
   demasiado angosto/partido. */
.statement--sm {
  font-size: var(--fs-lede);
  max-width: 42ch;
}

/* Centra el statement cuando es el único contenido del bloque (ej. 001 tras
   quitar el recuadro de "Insight central" — ver index.html). No toca
   .statement base porque otros usos siguen alineados a la izquierda. */
.statement--center {
  margin-inline: auto;
  text-align: center;
}

/* Frases resaltadas dentro de texto corrido — mismo rojo de firma que ya
   usan eyebrows/números en todo el sitio, aplicado ahora a énfasis inline.
   Sin mix-blend-mode propio: hereda "normal" por defecto, y como vive
   dentro de .scrub-flow-halo--block (que ya aísla con isolation:isolate +
   mix-blend-mode:normal), el rojo sale íntegro en vez de invertirse a
   blanco — mismo mecanismo que ya protege el rojo del eyebrow. */
.accent-word {
  color: var(--signature);
}

.insight {
  border-top: 1px solid rgba(10, 10, 10, 0.12);
  padding-top: var(--sp-4);
  max-width: 60ch;
}

.insight-label {
  display: block;
  font-size: var(--fs-small);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--signature);
  margin-bottom: var(--sp-1);
}

.insight p {
  font-size: var(--fs-lede);
  font-weight: 400;
}

/* ---------- Módulo A ---------- */
.render-accent-line {
  position: absolute;
  bottom: var(--sp-4);
  left: var(--sp-4);
  width: 48px;
  height: 3px;
  background: var(--signature);
}

/* ---------- Fondo: Módulo A inflándose, contenido scrolleable encima ---------- */
.scrub-bg-wrap {
  position: relative;
}

.scrub-bg-sticky {
  position: sticky;
  top: 0;
  height: 100svh;
  width: 100%;
  overflow: hidden;
  background: var(--surface-gray);
  z-index: 0;
}

.scrub-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.scrub-loading {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-gray);
  font-size: var(--fs-small);
  letter-spacing: 0.14em;
  color: rgba(10, 10, 10, 0.4);
  transition: opacity 500ms var(--ease-glide);
  z-index: 2;
}

.scrub-loading.is-hidden {
  opacity: 0;
  pointer-events: none;
}

/* PreloaderLine (Architectural Terminal): línea roja progresiva 0→100%,
   generada por JS en setupScrubSequences(). El "%" en texto se conserva
   como fallback/accesibilidad, no se reemplaza. */
.scrub-loading-line {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 0%;
  height: 2px;
  background: var(--signature);
  transition: width 200ms var(--ease-snap);
}

.scrub-bg-content {
  position: relative;
  z-index: 1;
  margin-top: -100svh;
}

/* Tramos del recorrido de Inicio sin contenido en flujo (la transición
   cenital de apertura y el arranque de Proyecciones inmersivas al cierre)
   — reservan su propio scroll para que esos frames no pasen comprimidos
   en el espacio que hoy solo tiene que cubrir 001/002. El tramo de cierre
   es corto a propósito: son solo 60 frames (un estilo, no los 4), así que
   le toca proporcionalmente menos scroll que antes. */
.scrub-spacer--intro { height: 160vh; }
.scrub-spacer--outro { height: 65vh; }

/* Variante sin contenido en flujo normal encima (Público): la distancia de
   scroll la da una altura explícita en vez de secciones de texto apiladas.
   Alta a propósito — ahora carga 001, 002 (con sus 3 puntos), 003 y los 3
   artistas, todo mezclado sobre el mismo video desde el punto 1 del scroll
   (justo después del hero, igual que el home), así que necesita espacio de
   sobra para que cada tramo se lea con calma. */
.scrub-bg-wrap--captions-only {
  height: 850vh;
}

/* 2026-09-16 (Experiencias, mismo proceso que Empresas L1/M1): 004/005/006
   viven ahora en flujo dentro de #publico-scrub — un alto fijo en vh ya no
   puede convivir con contenido real, igual que ya se resolvió en
   #empresas-journey. Override por ID, no toca la clase compartida. */
#publico-scrub {
  height: auto;
}

/* Reserva el scroll de las 8 captions existentes (001-003 + los 4 artistas,
   que van de progreso 0 a ~1.01) — sin este spacer, esas captions se
   comprimirían al alto de 004+005+006 (~300vh) en vez de a los 850vh
   originales. 850vh = el alto que tenía el wrap completo antes de este
   cambio, misma lógica que .scrub-spacer--journey-tail en Empresas.
   2026-09-18: subido a 950vh — medido en vivo, 004+005+006 juntas dan
   ~26% más alto que los 300vh asumidos (padding + texto envuelto real,
   mismo fenómeno que en Empresas 001/002), así que "Miguel Ángel" y "Y
   muchos artistas más" casi no tenían ventana propia antes de que el
   contenido en flujo las alcanzara — confirmado con capturas reales de
   Oscar. Además main.js ahora nunca deja una caption activa mientras haya
   contenido de flujo en pantalla (ver flowVisible en attachScrub), así que
   esto ya no puede volver a verse roto aunque el viewport sea distinto —
   en el peor caso alguna caption se salta, no se traslapa. */
.scrub-spacer--publico-captions {
  height: 950vh;
}

/* Variante para el journey continuo de video (Empresas y Experiencias:
   locación × estilo/mapping, intercalados) — 662 frames: 1 frame blanco +
   8 clips de 60 + 9 disolvencias de 20 entre cada elemento, incluyendo las
   dos disolvencias hacia/desde blanco en las puntas (ver build_journey.py).
   Un solo canvas/ScrollTrigger de punta a punta para que el fondo nunca se
   corte ni reinicie. Reemplaza las variantes --mapping-styles y
   --style-showcase (2026-09-15): esas fragmentaban el recorrido en
   canvases separados, que es justo lo que se veía como "videos
   independientes". Misma densidad de scroll que --mapping-styles
   (500vh/240 ≈ 2.08vh/frame). */
.scrub-bg-wrap--journey {
  height: 1380vh;
}

/* 2026-09-16 (M1): #empresas-journey es el único --journey con contenido
   real en flujo dentro (.scrub-bg-content, 001/002 restaurados patrón
   Inicio) — un alto fijo en vh ya no puede convivir con eso, el contenido
   se saldría del box y el ScrollTrigger (bottom bottom) quedaría mal
   calculado. Override por ID en vez de tocar la clase compartida: no afecta
   a Experiencias (#experiencias-journey), que sigue siendo solo captions y
   sí necesita el alto fijo de arriba. */
#empresas-journey {
  height: auto;
}

/* 2026-09-16 (M1, hallazgo post-implementación): al pasar a height:auto el
   alto total del wrap se volvió solo el de 001+002+spacer de salida
   (~265vh) — nada reservaba scroll para las 4 captions que sobreviven
   (Producción audiovisual + Estilo 02/03/04), así que su progreso 0–1 pasaba
   en una fracción del scroll y las captions volaban. 1115vh = 1380vh
   (el alto original del wrap, calibrado a 2.085vh/frame para 662 frames)
   menos 100vh (001) − 100vh (002) − 65vh (spacer de salida) — devuelve a
   esas 4 captions el mismo ritmo que ya tenían antes de M1. */
.scrub-spacer--journey-tail {
  height: 1115vh;
}

.scrub-caption {
  position: absolute;
  left: 50%;
  bottom: var(--sp-12);
  transform: translate(-50%, 12px);
  z-index: 2;
  text-align: center;
  color: var(--white);
  mix-blend-mode: difference;
  font-size: var(--fs-h2);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0;
  transition: opacity 600ms var(--ease-glide), transform 600ms var(--ease-glide);
  pointer-events: none;
}

.scrub-caption::after {
  content: "";
  display: block;
  width: 48px;
  height: 3px;
  background: var(--signature);
  margin: var(--sp-3) auto 0;
}

.scrub-caption.is-active {
  opacity: 1;
  transform: translate(-50%, 0);
}

.scrub-caption--outro {
  font-size: var(--fs-lede);
  letter-spacing: 0.14em;
}

/* Bloques de texto (eyebrow + título + lede) mezclados sobre el video, sin
   caja/fondo detrás — contraste por blend mode invertido en vez de sombra,
   reutilizado para 001, 002 y 003. */
.scrub-caption--block {
  top: 50%;
  bottom: auto;
  left: 50%;
  transform: translate(-50%, calc(-50% + 16px));
  /* 2026-09-16 (M2): 44ch → 62ch, mismo valor que ya usa .festival
     .render-veil para el mismo tipo de bloque de texto ancho sobre
     fondo/render — no es un número nuevo. */
  max-width: 62ch;
  /* 2026-09-19: sp-8 → sp-6 — pedido explícito de "bajar el zoom" de las
     tarjetas de texto (se sentían muy grandes/dominantes sobre el render). */
  padding: var(--sp-6);
  text-transform: none;
  letter-spacing: normal;
  font-size: initial;
  /* Halo oscuro difuminado, no una caja: sin esto el blend mode se vuelve
     ilegible en tramos largos de texto que caen sobre la parte más
     detallada/colorida de la pintura (confirmado en pruebas). Con el halo,
     el texto siempre "resta" contra un fondo casi negro, así que el blend
     mode da blanco consistente sin importar qué haya en el video detrás.
     isolation:isolate es obligatorio — sin él, el blend de los hijos
     ignora este fondo y se mezcla directo contra el canvas de más atrás.
     mix-blend-mode:normal AQUÍ (no heredar "difference" de .scrub-caption)
     es igual de obligatorio — este contenedor también heredaba difference,
     y diferenciar cualquier cosa contra negro no hace nada (diff(x,0)=x),
     así que el halo se volvía invisible hasta forzar esto a "normal".
     El radio del óvalo es mucho mayor que la caja misma (160%/160% en vez
     de ~100%), así que la caja completa solo alcanza a "ver" el tramo
     inicial —más plano y más oscuro— del degradado: cobertura pareja de
     esquina a esquina en vez de un centro oscuro que se aclara antes de
     llegar a las líneas de arriba/abajo. Con bloques largos (eyebrow +
     título de 2 líneas + lede) un óvalo ajustado al tamaño de la caja deja
     esas líneas extremas sin halo, y ahí el texto choca visualmente contra
     fondos con su propio texto o detalle (confirmado con el wordmark de
     Módulo A detrás del caption de intro en Eventos). background-clip
     recorta al padding-box de la caja, así el degradado nunca se ve fuera
     de ella aunque el radio declarado sea mucho más grande. */
  background: radial-gradient(ellipse 160% 160% at center, rgba(0, 0, 0, 0.78) 0%, rgba(0, 0, 0, 0.7) 60%, rgba(0, 0, 0, 0.56) 100%);
  border-radius: var(--sp-3);
  isolation: isolate;
  mix-blend-mode: normal;
}

.scrub-caption--block.is-active {
  transform: translate(-50%, -50%);
}

.scrub-caption--block::after {
  display: none;
}

/* Halo para captions de una sola línea (nombre de estilo) sobre tramos
   claros del scrub — mismo mecanismo que --block (halo aislado + blend
   normal para que el "difference" del texto opere contra el halo, no
   contra el video) pero sin recentrar verticalmente ni perder el tamaño/
   mayúsculas de .scrub-caption. El texto va anidado en un
   <span class="scrub-caption-title"> para heredar ese blend correctamente. */
.scrub-caption--halo-bg {
  padding: var(--sp-4) var(--sp-8);
  background: radial-gradient(ellipse 70% 78% at center, rgba(0, 0, 0, 0.55) 0%, rgba(0, 0, 0, 0.22) 55%, transparent 80%);
  border-radius: var(--sp-3);
  isolation: isolate;
  mix-blend-mode: normal;
}

.scrub-caption--halo-bg .scrub-caption-title {
  margin-bottom: 0;
}

.scrub-caption-eyebrow {
  display: block;
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--fs-small);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--white);
  mix-blend-mode: difference;
  margin-bottom: var(--sp-3);
}

.scrub-caption-title {
  font-size: var(--fs-h2);
  font-weight: 700;
  color: var(--white);
  mix-blend-mode: difference;
  margin-bottom: var(--sp-4);
}

.scrub-caption-lede {
  font-size: var(--fs-lede);
  font-weight: 400;
  color: var(--white);
  mix-blend-mode: difference;
}

/* Cada sección flotando sobre el fondo ocupa al menos un viewport completo,
   así el scrubbing tiene tiempo de respirar mientras se lee el texto. */
.scrub-bg-content .section {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--sp-6);
}

/* ---------- Texto sobre el fondo animado (home, "001" y "002"): mismo halo
   + mix-blend-mode:difference que ya usan las scrub-caption de Público/
   Eventos/Empresas, en vez de las tarjetas blancas opacas de antes (se
   veían fuera de línea con el resto del sitio — feedback directo). La
   diferencia con .scrub-caption es que este texto vive en flujo normal
   (scrollea con la página) en vez de position:absolute con rango fijo,
   así que es un wrapper aparte en vez de reusar esa clase tal cual. */
.scrub-flow-halo {
  display: inline-block;
  align-self: flex-start;
  padding: var(--sp-2) var(--sp-4);
  background: radial-gradient(ellipse 160% 160% at center, rgba(0, 0, 0, 0.78) 0%, rgba(0, 0, 0, 0.7) 60%, rgba(0, 0, 0, 0.56) 100%);
  border-radius: var(--sp-3);
  isolation: isolate;
  mix-blend-mode: normal;
}

/* 2026-09-15: el bloque de lectura de 001 (statement + insight) no tenía
   contenedor definido, solo el halo difuminado detrás del texto — Oscar
   pidió un frame, pero fino (hairline), no una caja sólida como las
   tarjetas de vidrio de 003/004/005. Mismo color de borde que esas
   tarjetas para no inventar un segundo lenguaje de "borde" en la página. */
.scrub-flow-halo--block {
  display: block;
  /* 2026-09-19: sp-6/sp-8 → sp-4/sp-6 — mismo pedido de "bajar el zoom". */
  padding: var(--sp-4) var(--sp-6);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--sp-2);
}

.scrub-flow-halo .section-eyebrow,
.scrub-flow-halo .statement,
.scrub-flow-halo .section-title,
.scrub-flow-halo .section-lede,
.scrub-flow-halo .insight-label,
.scrub-flow-halo .insight p,
.scrub-flow-halo .spec dt,
.scrub-flow-halo .spec dd,
.scrub-flow-halo .price-range,
.scrub-flow-halo .price-note,
.scrub-flow-halo .cycle-list li,
.scrub-flow-halo .includes-columns h4,
.scrub-flow-halo .includes-list li {
  color: var(--white);
  mix-blend-mode: difference;
}

.scrub-flow-halo .insight {
  border-top-color: rgba(255, 255, 255, 0.3);
}

/* 2026-09-16 (Experiencias): "no incluye" necesita seguir leyéndose más
   apagado que "incluye" también sobre el render — mismo criterio que
   rgba(10,10,10,0.45) tenía sobre blanco, aquí su equivalente en blanco. */
.scrub-flow-halo .includes-list--muted li {
  color: rgba(255, 255, 255, 0.45);
  mix-blend-mode: difference;
}

/* .cycle-list/.includes-list traen su propio hairline oscuro
   (rgba(10,10,10,0.1)/0.08) — mismo problema que .insight/.price-hero de
   arriba, mismo tipo de corrección. */
.scrub-flow-halo .cycle-list li,
.scrub-flow-halo .includes-list li {
  border-bottom-color: rgba(255, 255, 255, 0.15);
}

/* 2026-09-16 (M1): .price-hero trae su propio hairline arriba/abajo
   (rgba(10,10,10,0.12), pensado para fondo blanco) — sobre el halo oscuro
   es casi invisible, mismo problema que ya resolvía el override de
   .insight de arriba. */
.scrub-flow-halo .price-hero {
  border-top-color: rgba(255, 255, 255, 0.3);
  border-bottom-color: rgba(255, 255, 255, 0.3);
}

/* 2026-09-16 (M1): feature-list de Empresas 001 vuelve a vivir sobre el
   render (ver #empresas-journey) — mismo tratamiento de "vidrio esmerilado"
   que ya usan .spec-grid--frosted/.identity-strip/.audience-card, no un
   componente nuevo, solo otra instancia de esa familia. */
.feature-list--frosted .feature {
  background: rgba(10, 10, 10, 0.55);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: var(--sp-2);
  padding: var(--sp-4);
}

.feature-list--frosted .feature h3 {
  color: var(--white);
}

.feature-list--frosted .feature p {
  color: rgba(255, 255, 255, 0.65);
}

/* Tarjetas de vidrio esmerilado sobre video: para contenido en grid
   (spec-grid) en vez de envolver todo en un solo halo grande. Cada dato
   es su propia tarjeta con fondo oscuro translúcido + backdrop-blur real,
   así el texto puede ser blanco sólido (sin depender de mix-blend-mode:
   difference) porque el fondo oscuro de la tarjeta ya está garantizado. */
/* 2026-09-19: 0.75 → 0.55 (más transparencia, pedido explícito) — el label
   vuelve a blanco 100% opaco (antes 0.75) para compensar: mismo cálculo
   que .audience-card, ~6.5:1 en el peor caso medido, con margen sobre
   4.5:1. */
.spec-grid--frosted .spec {
  background: rgba(10, 10, 10, 0.55);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
}

.spec-grid--frosted .spec dt {
  color: var(--white);
}

.spec-grid--frosted .spec dd {
  color: var(--white);
  font-weight: 400;
}

/* Franja de identidad de marca — muestra tokens reales del sistema
   (colores + curvas de easing definidas en :root arriba) dentro de 002,
   junto a las specs físicas del módulo. Mismo tratamiento de vidrio
   esmerilado que .spec-grid--frosted para mantenerse legible sobre video
   y en la misma línea visual — no es un elemento nuevo, es una extensión
   de la tarjeta de specs. */
.identity-strip {
  margin-top: var(--sp-4);
  background: rgba(10, 10, 10, 0.55);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: var(--sp-2);
  padding: var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.identity-strip-label {
  font-size: var(--fs-small);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
}

.identity-swatches {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
}

.swatch {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
}

.swatch-color {
  width: 20px;
  height: 20px;
  border-radius: 5px;
  border: 1px solid rgba(255, 255, 255, 0.25);
  flex-shrink: 0;
}

.swatch-name {
  font-size: var(--fs-small);
  color: var(--white);
}

.swatch-hex {
  font-size: var(--fs-small);
  color: rgba(255, 255, 255, 0.45);
  font-variant-numeric: tabular-nums;
}

.identity-curves {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  padding-top: var(--sp-3);
  border-top: 1px solid rgba(255, 255, 255, 0.12);
}

.curve-chip {
  font-size: var(--fs-small);
  color: rgba(255, 255, 255, 0.7);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 999px;
  padding: 2px var(--sp-2);
}

.curve-chip b { color: var(--white); font-weight: 500; }

/* ---------- Glass cards: legado, ya no se usa en index.html (ver
   .scrub-flow-halo arriba) — se deja por si scroll-formats-demo.html u
   otro experimento la sigue referenciando. ---------- */
.glass-card {
  background: rgba(255, 255, 255, 0.86);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  padding: var(--sp-6);
  box-shadow: 0 20px 60px rgba(10, 10, 10, 0.08);
}

.glass-chip {
  display: inline-block;
  align-self: flex-start;
  background: rgba(255, 255, 255, 0.86);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  padding: var(--sp-1) var(--sp-3);
}

.module-intro {
  max-width: 640px;
}

.spec-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--sp-4);
}

.spec-grid.glass-card {
  max-width: 720px;
}

.spec dt {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--fs-small);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: rgba(10, 10, 10, 0.5);
  margin-bottom: var(--sp-1);
}

.spec dd {
  font-size: var(--fs-lede);
  font-weight: 400;
}

/* ---------- Secciones sobre el render: 003→006 ----------
   SIN fondo de sección, a propósito. Pasó por dos versiones fallidas antes:
   con background:var(--ink) opaco el render quedaba tapado por completo, y
   con un velo translúcido de sección completa aparecían bandas grises de
   borde duro que cortaban la imagen en rectángulos (reportado con capturas).
   La legibilidad ahora la resuelve .render-veil — un difuminado local que
   solo oscurece detrás del texto y se desvanece antes del borde, así el
   render se ve entero. El footer se queda blanco a propósito. */
.section--dark {
  color: var(--white);
  max-width: none;
}

/* Tarjeta de vidrio detrás de bloques de texto que flotan sobre el render
   (003, 004, Festival/005, Contacto/006) — antes era un degradado radial
   que se desvanecía a transparente en el borde (0% de opacidad al 100%),
   así que el texto que caía cerca del borde del halo (eyebrows, ledes
   largos) perdía cobertura y el contraste dependía de qué tan claro fuera
   el frame del render justo detrás en ese punto del scroll — reportado
   como "transparencia inconsistente entre secciones" y "rojo poco
   legible" (2026-09-15). Ahora es la MISMA familia de vidrio esmerilado
   que .price-card/.audience-card/.identity-strip, pero MÁS opaca que esa
   familia (0.92 en vez de 0.62): medido con la firma roja real
   (#C6001F) sobre el pixel real del canvas detrás de cada eyebrow, el
   0.62 original daba contraste 1.09:1 en el peor caso (frame de video
   blanco) y 3.1-3.3:1 en los casos típicos — todos por debajo del 4.5:1
   de WCAG AA para texto normal. Con 0.92 el peor caso sube a 2.58:1.
   Nota importante: #C6001F contra NEGRO PURO (backdrop 100% opaco) da
   como máximo matemático 3.42:1 — ese rojo nunca llega a 4.5:1 sobre
   ningún fondo oscuro, sea cual sea la opacidad del velo (ver Log de
   cambios / vault). Esto ya es lo más opaco que tiene sentido sin volver
   el velo un rectángulo sólido que tape el render por completo. */
.render-veil {
  /* 2026-09-19: 0.92 → 0.88, el máximo que se puede ceder aquí. El
     eyebrow rojo de este componente ya vive en el límite matemático de
     contraste (ver nota arriba, techo real 3.42:1 contra negro puro) —
     medido contra el brillo pico real (~196), bajar más de 0.88 tira el
     contraste del rojo por debajo de lo ya aceptado como piso (~2.6:1 en
     vez de ~2.9:1). El resto de las tarjetas (texto blanco, sin ese techo)
     sí bajó bastante más — ver .audience-card/.spec-grid--frosted/etc. */
  background: rgba(10, 10, 10, 0.88);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--sp-2);
  /* 2026-09-19: sp-8/sp-6 → sp-6/sp-4 — mismo pedido de "bajar el zoom". */
  padding: var(--sp-6) var(--sp-6) var(--sp-4);
}

/* Festival va centrado: sin acotarlo, el velo se estiraría a todo el ancho
   del viewport (la sección tiene max-width:none) y el degradado quedaría
   demasiado abierto para oscurecer algo. */
.festival .render-veil {
  max-width: 62ch;
  margin: 0 auto;
}

/* 2026-09-15: el rojo de firma (#C6001F) matemáticamente nunca llega a
   4.5:1 (AA texto normal) contra un fondo oscuro — el techo es 3.42:1
   incluso contra negro puro (ver comentario de .render-veil arriba).
   Ruta elegida por Oscar: usar el umbral de "texto grande" de WCAG
   (≥24px = 1.5rem, solo pide 3:1) en vez de un chip blanco detrás del
   texto. Escondido a .render-veil .section-eyebrow porque SOLO ahí el
   eyebrow es rojo sobre fondo oscuro — en 001/002 (.scrub-flow-halo) el
   eyebrow ya es blanco por mix-blend-mode:difference, y en Empresas/
   Experiencias vive sobre blanco sólido (contraste ~6:1 sin tocar nada);
   tocar la regla base .section-eyebrow habría cambiado el tamaño en todo
   el sitio sin necesidad. .festival-date no necesita este ajuste: ya usa
   var(--fs-h2), cuyo mínimo (28px) es "texto grande" en cualquier ancho. */
.render-veil .section-eyebrow {
  /* 2026-09-19: 1.5rem → 1.625rem (pedido de subir un poco el tamaño del
     rojo). Ya era "texto grande" WCAG a 24px; con negrita (ver
     .section-eyebrow) el mínimo real baja a 18.66px, así que este tamaño
     sigue calificando con más margen todavía. */
  font-size: 1.625rem;
}

.section--dark > * {
  max-width: var(--max-width);
  margin-left: auto;
  margin-right: auto;
}

/* Un poco más opaco que el 0.65 de fondo blanco: aquí compite con el
   render que corre detrás del velo. */
.section--dark .section-lede { color: rgba(255, 255, 255, 0.82); }

/* ---------- Audiencias: scroll lateral ----------
   003 vive entero dentro de .lateral-scroll-wrap — título y tarjetas en el
   mismo viewport pineado, no separados. Antes el título ("003 — Tres
   Públicos") era su propia sección de 100svh ANTES del wrap de tarjetas:
   con scroll rápido eso se sentía como una pantalla negra vacía/rota antes
   de llegar al contenido real (reportado y confirmado viendo una
   grabación). Mismo mecanismo de sticky que ya usa el scrub de video
   (position:sticky + progreso de scroll vía GSAP), aplicado a translateX
   del track en vez de a un índice de frame. */
.lateral-scroll-wrap {
  position: relative;
  /* Sin fondo: el render corre detrás y se ve entero. El color blanco no se
     hereda solo (esto no vive dentro de .section--dark, que impondría
     max-width a sus hijos y rompería el ancho completo de la franja) — sin
     él, h3/p de las tarjetas salían en --ink, casi invisible (confirmado
     con getComputedStyle). */
  color: var(--white);
}

.lateral-scroll-heading {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: var(--sp-8) var(--gutter) var(--sp-4);
}

.lateral-scroll-heading .section-title { margin-bottom: 0; }

/* Base: título arriba en flujo normal + franja con scroll horizontal
   normal (touch/rueda) debajo — el fallback real si JS no corre o hay
   prefers-reduced-motion, no solo una nota. Sin esto, .no-js dejaría el
   track recortado por overflow:hidden con las tarjetas 2 y 3 inalcanzables. */
.lateral-track {
  display: flex;
  gap: var(--sp-6);
  padding: 0 var(--gutter) var(--sp-8);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x mandatory;
  will-change: transform;
}

.audience-card { scroll-snap-align: start; }

/* Con JS activo (quita .no-js en el primer script del body) y sin pedir
   menos movimiento: pin real vía sticky, título y track apilados en
   columna dentro del mismo viewport fijo — GSAP mueve el track en X
   (setupLateralScroll en main.js). */
html:not(.no-js) .lateral-scroll-wrap {
  /* Altura real la fija setupLateralScroll() en main.js, calculada del
     ancho real del track — un valor fijo en vh no puede saber cuánto debe
     durar el pan (confirmado: con 320vh fijo el recorrido llegaba a su
     máximo en el primer 25% del tramo y el resto quedaba fijo sin avanzar,
     sin importar qué tan ancho fuera el travel real). Este valor es solo
     el piso antes de que JS calcule el real, para evitar un salto de layout. */
  height: 180vh;
}

html:not(.no-js) .lateral-scroll-sticky {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--sp-3);
}

html:not(.no-js) .lateral-scroll-heading {
  flex-shrink: 0;
  padding: 0 var(--gutter);
}

html:not(.no-js) .lateral-track {
  overflow: visible;
  scroll-snap-type: none;
  padding: 0 var(--gutter);
}

@media (prefers-reduced-motion: reduce) {
  html:not(.no-js) .lateral-scroll-wrap { height: auto; }
  html:not(.no-js) .lateral-scroll-sticky {
    position: static;
    height: auto;
    overflow: visible;
    display: block;
  }
  html:not(.no-js) .lateral-track {
    overflow-x: auto;
    scroll-snap-type: x mandatory;
  }
}

.audience-card {
  /* Relativo al viewport con techo generoso — con un tope fijo más chico
     (680px probado primero) 3 tarjetas cabían casi enteras en pantallas
     anchas y el pan horizontal recorría apenas ~186px antes de quedarse
     quieto el resto del tramo (confirmado midiendo scrollWidth real). */
  flex: 0 0 min(88vw, 900px);
  /* Vidrio OSCURO, no claro: flotan sobre el render (que en este tramo va
     del módulo blanco brillante al negro final), así que un tinte claro las
     volvía ilegibles sobre las tomas blancas. Mismo criterio que
     .spec-grid--frosted en 002. */
  /* 2026-09-19: 0.75 → 0.55 (pedido explícito de más transparencia) — el
     texto vuelve a blanco 100% opaco (antes .audience-number a 0.7) para
     compensar: recalculado contra el brillo real medido en esta sección
     (pico ~196), blanco puro a 0.55 da ~6.5:1, con margen de sobra sobre
     el 4.5:1 de WCAG AA. */
  background: rgba(10, 10, 10, 0.55);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--sp-2);
  padding: var(--sp-6);
  /* Más bajo que antes (era 64svh/560px) para dejarle aire de sobra al
     título arriba dentro del mismo viewport de 100svh pineado. */
  min-height: min(50svh, 440px);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  transition: background 300ms var(--ease-glide), border-color 300ms var(--ease-glide);
}

.audience-card:hover {
  background: rgba(10, 10, 10, 0.75);
  border-color: rgba(255, 255, 255, 0.3);
}

.audience-number {
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  /* 2026-09-19: 0.7 → blanco 100% opaco — compensa la tarjeta ahora a 0.55
     de opacidad (ver .audience-card). */
  color: var(--white);
  letter-spacing: 0.1em;
}

.audience-card h3 {
  font-size: var(--fs-h2);
  font-weight: 700;
  line-height: 1.1;
}

.audience-card p {
  font-size: var(--fs-body);
  color: rgba(255, 255, 255, 0.65);
  flex-grow: 1;
}

.audience-link {
  font-size: var(--fs-small);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--signature);
  transition: transform 220ms var(--ease-overshoot);
  display: inline-block;
}

.audience-card:hover .audience-link {
  transform: translateX(6px);
}

/* ---------- Scroll lateral apilado en táctil (Fase C, fix 2) ----------
   setupLateralScroll() en main.js solo crea el pin + ScrollTrigger cuando
   hay hover fino real (mouse/trackpad) — en táctil agrega esta clase en vez
   de intentar el mismo scroll-hijack con gestos de dedo, que competía con
   el scroll vertical normal (auditoría: reportado como "se traba con el
   scroll"). El alto de 180vh de arriba (html:not(.no-js) .lateral-scroll-
   wrap) es la única fuente real de ese alto en este camino — sizeWrap()
   nunca corre aquí, así que sin este override el wrap se queda pineado con
   ese alto fijo aunque no haya JS pineando nada encima. */
.lateral-scroll-wrap--stacked,
html:not(.no-js) .lateral-scroll-wrap.lateral-scroll-wrap--stacked {
  height: auto;
}

.lateral-scroll-wrap--stacked .lateral-scroll-sticky {
  position: static;
  height: auto;
  overflow: visible;
}

.lateral-scroll-wrap--stacked .lateral-track {
  flex-direction: column;
  transform: none;
}

.lateral-scroll-wrap--stacked .audience-card {
  flex: none;
  width: 100%;
}

/* ---------- Pricing ---------- */
.pricing-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--sp-3);
  margin-top: var(--sp-8);
}

/* Catálogo de Inicio (2026-09-15): una sola caja de precio en vez de 3 —
   se centra y se acota en vez de estirarse a las 3 columnas del grid. */
.pricing-grid--single {
  grid-template-columns: minmax(0, 480px);
  justify-content: center;
}

.price-desc a {
  text-decoration: underline;
  text-underline-offset: 2px;
}

.price-desc a:hover { color: var(--signature); }

.price-card {
  border: 1px solid rgba(255, 255, 255, 0.15);
  /* 2026-09-19: 0.62 → 0.5 — más transparencia, texto ya en blanco. */
  background: rgba(10, 10, 10, 0.5);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  /* Alinea con el resto de la familia "vidrio esmerilado" (DESIGN.md §4:
     .audience-card, .spec-grid--frosted .spec, .identity-strip ya usan
     var(--sp-2)) — esta tarjeta se había quedado en esquina recta. */
  border-radius: var(--sp-2);
  padding: var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  transition: transform 300ms var(--ease-glide), box-shadow 300ms var(--ease-glide), background 300ms var(--ease-glide);
}

.price-card:hover {
  transform: translateY(-6px);
  background: rgba(10, 10, 10, 0.75);
}

/* 2026-09-18: era fondo blanco sólido — única tarjeta de toda la familia
   "vidrio oscuro" (audience-card/contact-card/price-card base) que rompía
   ese lenguaje visual flotando sobre el render. Ahora usa el mismo nivel
   que .render-veil (0.92), la sección donde vive esta tarjeta. Con esto
   .price-desc ya no necesita override: su regla base (rgba(255,255,255,.6))
   ya asume fondo oscuro. */
.price-card--featured {
  /* 2026-09-19: 0.92 → 0.7 — texto blanco, sin restricción de contraste
     del rojo, hay margen real para bajar más que .render-veil. */
  background: rgba(10, 10, 10, 0.7);
  color: var(--white);
  border-color: rgba(255, 255, 255, 0.14);
}

.price-tier {
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--signature);
}

.price-card h3 { font-size: var(--fs-lede); font-weight: 400; }

.price-range {
  font-size: var(--fs-h2);
  font-weight: 700;
}

.price-range small { font-size: var(--fs-small); font-weight: 400; }

.price-desc {
  font-size: var(--fs-body);
  color: rgba(255, 255, 255, 0.6);
}

/* ---------- Festival ---------- */
/* Sin fondo propio, igual que .section--dark: vive sobre el render que
   sigue corriendo. Su texto va dentro de un .render-veil. */
.festival {
  text-align: center;
  color: var(--white);
  max-width: none;
}

/* 2026-09-15: "Guadalajara" (palabra suelta, no se puede partir en dos)
   medía 597.8px de ancho real con --fs-display (clamp hasta 7rem en
   pantallas anchas) contra solo 489.7px de caja disponible dentro del
   panel — se desbordaba 108px a la derecha, y como no puede centrarse
   algo más ancho que su propio contenedor, el navegador la anclaba al
   borde izquierdo y dejaba que se saliera del recuadro por la derecha
   (confirmado midiendo el rect real del texto, no solo la caja). Eso se
   leía como "el título está corrido a la derecha". Dos cambios: (1)
   --fs-h2 en vez de --fs-display — mismo token que ya usan otros títulos
   de sección, con techo de 3.25rem en vez de 7rem, calculado para que
   "Guadalajara" quepa con margen de sobra en cualquier ancho. (2)
   overflow-wrap:break-word como límite estricto de verdad: si algún día
   el copy cambia a algo aún más largo, o el usuario fuerza un zoom de
   texto grande, el texto rompe DENTRO del recuadro en vez de salirse —
   nunca vuelve a desbordar el panel, pase lo que pase. */
.festival-title {
  font-size: var(--fs-h2);
  max-width: none;
  margin: 0 auto var(--sp-4);
  overflow-wrap: break-word;
}

.festival-date {
  font-size: var(--fs-h2);
  font-weight: 700;
  color: var(--signature);
  margin-bottom: var(--sp-4);
}

.festival .section-lede {
  margin: 0 auto;
  color: rgba(255, 255, 255, 0.82);
}

.festival .section-eyebrow { color: var(--signature); }

/* ---------- Contacto ---------- */
.contact-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--sp-3);
  margin-top: var(--sp-8);
}

.contact-card {
  border: 1px solid rgba(10, 10, 10, 0.12);
  padding: var(--sp-6) var(--sp-4);
  transition: transform 300ms var(--ease-glide), border-color 300ms var(--ease-snap);
}

.contact-card:hover {
  transform: translateY(-4px);
  border-color: var(--signature);
}

.contact-card--whatsapp { background: var(--surface-gray); }

.contact-card h3 { font-size: var(--fs-h2); font-weight: 700; margin-bottom: var(--sp-2); }
.contact-card p { color: rgba(10, 10, 10, 0.65); margin-bottom: var(--sp-3); }
.contact-cta {
  font-size: var(--fs-small);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--signature);
}

/* Variante oscura, solo dentro de #contacto en Inicio (.section--dark) —
   eventos.html y publico.html siguen usando .contact-card tal cual, sobre
   fondo blanco, así que esto va con selector descendiente y no toca la
   regla base de arriba. */
.section--dark .contact-card {
  border-color: rgba(255, 255, 255, 0.15);
  /* 2026-09-19: 0.62 → 0.5 — más transparencia; .contact-card p sube de
     0.65 a 0.85 para compensar (recalculado, 0.65 ya no llegaba a 4.5:1). */
  background: rgba(10, 10, 10, 0.5);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  /* En este contexto oscuro pasa a ser una tarjeta de vidrio esmerilado más
     (misma familia que .price-card/.audience-card) — la variante clara
     base se queda en esquina recta, esta no. */
  border-radius: var(--sp-2);
}

.section--dark .contact-card:hover { border-color: var(--signature); }
.section--dark .contact-card--whatsapp { background: rgba(10, 10, 10, 0.55); }
.section--dark .contact-card p { color: rgba(255, 255, 255, 0.85); }

/* ---------- Footer ---------- */
.site-footer {
  display: flex;
  justify-content: space-between;
  padding: var(--sp-4) var(--gutter) var(--sp-6);
  font-size: var(--fs-small);
  /* Contraste WCAG AA: el 0.4 daba ~2.7:1 sobre blanco. 0.65 es el mismo
     token de "texto secundario" documentado en DESIGN.md §2, no un valor
     nuevo — da ~6.3:1. */
  color: rgba(10, 10, 10, 0.65);
  max-width: var(--max-width);
  margin: 0 auto;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

.footer-social {
  display: flex;
  gap: var(--sp-3);
}

.footer-social a {
  color: inherit;
}

.footer-social a:hover {
  color: var(--signature);
}

/* ---------- Reveal state (animado por GSAP, ver main.js) ---------- */
/* 2026-09-15: antes entraba con opacity+translateY(24px) — Oscar reportó
   que el movimiento vertical se sentía confuso en pantallas de lectura.
   Ahora es opacity pura, sin traducir posición — aplica a las 3 páginas
   porque main.js es compartido.
   [data-reading-panel] es el piloto de "panel de lectura discreto" (solo
   001 por ahora) — misma mecánica de opacity, pero la sección completa es
   una sola unidad (sin hijos [data-reveal] individuales dentro, así el
   loop genérico de reveal la salta) y se desvanece de nuevo al salir de
   rango en cualquier dirección, no solo al volver hacia arriba (ver
   main.js). */
[data-reveal],
[data-reading-panel] {
  opacity: 0;
}

/* Si JS no carga o el usuario pide menos movimiento, el contenido igual es visible */
.no-js [data-reveal],
.no-js [data-reading-panel] { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  [data-reveal],
  [data-reading-panel] { opacity: 1; }
}

/* ⚠️ K2 PROTOTIPO TEMPORAL — BORRAR/AJUSTAR EN K3 ⚠️
   002 arranca oculta por CSS para que no haya un parpadeo visible antes de
   que setupPagerAndCrossfade() en main.js tome control de su opacidad real
   (001 no necesita esto porque ya empieza visible). Mismo criterio de
   fallback que [data-reveal]: sin JS o con reduced-motion, se ve completa. */
#modulo { opacity: 0; }
.no-js #modulo { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  #modulo { opacity: 1; }
}

/* ---------- Hero secundario (subpáginas) ---------- */
.hero--sub {
  min-height: 60svh;
  align-items: flex-start;
  text-align: left;
  padding-top: calc(var(--sp-16) + var(--sp-8));
}

.hero-title--sub {
  font-size: clamp(2.25rem, 1.3rem + 4.5vw, 5rem);
}

.hero--sub .hero-sub { margin-left: 0; }

/* ---------- Feature list (empresas / público) ---------- */
.feature-list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--sp-6);
  margin-top: var(--sp-8);
}

.feature-mark {
  display: block;
  width: 32px;
  height: 3px;
  background: var(--signature);
  margin-bottom: var(--sp-3);
}

.feature h3 {
  font-size: var(--fs-lede);
  font-weight: 400;
  margin-bottom: var(--sp-2);
}

.feature p {
  color: rgba(10, 10, 10, 0.65);
}

/* ---------- Pricing detail (subpáginas empresas / eventos) ---------- */
.pricing-detail--alt { background: var(--surface-gray); max-width: none; }
.pricing-detail--alt > * { max-width: var(--max-width); margin-left: auto; margin-right: auto; }

.price-hero {
  margin: var(--sp-6) 0;
  padding: var(--sp-6) 0;
  border-top: 1px solid rgba(10, 10, 10, 0.12);
  border-bottom: 1px solid rgba(10, 10, 10, 0.12);
}

.price-range--lg {
  display: block;
  font-size: var(--fs-display);
  font-weight: 700;
}

.price-range--lg small {
  font-size: var(--fs-small);
  font-weight: 400;
  display: block;
  margin-top: var(--sp-1);
}

.price-note {
  margin-top: var(--sp-3);
  font-size: var(--fs-body);
  color: rgba(10, 10, 10, 0.6);
  max-width: 56ch;
}

.includes-list li {
  padding: var(--sp-1) 0;
  font-size: var(--fs-body);
  border-bottom: 1px solid rgba(10, 10, 10, 0.08);
}

.includes-list--muted li { color: rgba(10, 10, 10, 0.45); }

.includes-columns {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--sp-8);
}

.includes-columns h4 {
  font-size: var(--fs-small);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(10, 10, 10, 0.5);
  margin-bottom: var(--sp-2);
}

.spec-grid--wide { grid-template-columns: repeat(2, 1fr); }
.spec-grid--wide dd { color: rgba(10, 10, 10, 0.65); font-weight: 400; }

/* ---------- Formulario de cotización ---------- */
.quote-form {
  margin-top: var(--sp-8);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  max-width: 720px;
}

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-4);
}

.form-row--full { display: block; }

.quote-form label {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  font-size: var(--fs-small);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: rgba(10, 10, 10, 0.55);
}

.quote-form input,
.quote-form select,
.quote-form textarea {
  font-family: var(--font);
  font-size: var(--fs-body);
  color: var(--ink);
  background: var(--white);
  border: none;
  border-bottom: 1px solid rgba(10, 10, 10, 0.25);
  padding: var(--sp-2) 0;
  transition: border-color 200ms var(--ease-snap);
}

.quote-form input:focus,
.quote-form select:focus,
.quote-form textarea:focus {
  outline: none;
  border-bottom-color: var(--signature);
}

/* 2026-09-18 (Architectural Terminal): era fondo sólido — el documento pide
   ghost/contorno para TODO CTA, incluso el primario ("la diferenciación se
   hace con peso, contorno y espacio; no con un color de relleno"). Mismo
   patrón que .nav-cta/.hero-cta. */
.submit-btn {
  align-self: flex-start;
  margin-top: var(--sp-2);
  padding: var(--sp-2) var(--sp-6);
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--ink);
  border-radius: var(--radius-control);
  font-family: var(--font);
  font-weight: 700;
  font-size: var(--fs-small);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: transform 180ms var(--ease-snap), background 180ms var(--ease-snap), color 180ms var(--ease-snap);
}

.submit-btn:hover {
  background: var(--ink);
  color: var(--white);
  transform: translateY(-2px);
}

.form-confirm {
  font-size: var(--fs-lede);
  font-weight: 400;
  padding: var(--sp-6) 0;
  border-top: 1px solid rgba(10, 10, 10, 0.12);
}

/* ---------- Galería: mapping por artista (público) ---------- */
.gallery-strip {
  display: flex;
  gap: var(--sp-3);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  padding-bottom: var(--sp-3);
  margin-top: var(--sp-8);
}

.gallery-item {
  flex: 0 0 auto;
  width: min(85vw, 640px);
  scroll-snap-align: start;
}

.gallery-item img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  display: block;
  background: var(--surface-gray);
}

.gallery-item figcaption {
  margin-top: var(--sp-2);
  font-size: var(--fs-small);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(10, 10, 10, 0.6);
}

/* ---------- Ciclo (público) ---------- */
.cycle-list {
  margin-top: var(--sp-8);
  display: flex;
  flex-direction: column;
}

.cycle-list li {
  display: flex;
  align-items: baseline;
  gap: var(--sp-4);
  padding: var(--sp-4) 0;
  border-bottom: 1px solid rgba(10, 10, 10, 0.1);
  font-size: var(--fs-lede);
  font-weight: 400;
}

.cycle-num {
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  font-weight: 700;
  color: var(--signature);
  letter-spacing: 0.08em;
  flex-shrink: 0;
}

/* ---------- Contacto (variante de una sola tarjeta) ---------- */
.contact-grid--single {
  grid-template-columns: 1fr;
  max-width: 560px;
}

/* ---------- Responsive ---------- */
@media (max-width: 860px) {
  .pricing-grid,
  .contact-grid,
  .feature-list,
  .includes-columns,
  .spec-grid--wide,
  .form-row {
    grid-template-columns: 1fr;
  }

  .site-nav { gap: var(--sp-2); }
  .site-nav a:not(.nav-cta) { display: none; }
  .nav-toggle { display: flex; }

  .lateral-scroll-wrap { height: 220vh; }
  .audience-card { flex-basis: 86vw; min-height: 56svh; }
}
