/* =========================================================
   SECCIONES.CSS — Capa base COMPARTIDA de secciones aditivas + saludo.
   SPEC 0006. Transversal (como motor.js): NO se versiona.

   Qué estiliza:
     (a) Las fichas de sección DINÁMICA `.ti-sec` (las genera el motor desde el
         manifiesto), dirigidas por tokens `--sec-*` que cada plantilla define en
         su :root, CON fallbacks AA por si una plantilla aún no los define.
         Incluye el layout mínimo de los grupos del renderer `grupos`
         (`.ti-sec__grupo*`, SPEC 0011 — sección Familia), con los MISMOS --sec-*.
     (b) El `.saludo` del invitado — SOLO como FALLBACK: las plantillas con
         `.saludo` nativo (acuarela, mariposas, luz-celestial, logro, liga-heroes)
         GANAN por cascada porque su hoja se enlaza DESPUÉS de esta (ver
         class-ti-render: secciones.css va ANTES de la hoja de plantilla).
         Las que no tienen `.saludo` propio toman este estilo, legible por --sec-acento-*.
     (c) El pulso `.musica--invita` "toca para sonar" (guard prefers-reduced-motion).
     (d) La portada de apertura `.ti-portada*` (SPEC 0008), incluida la variante
         "sobre con imágenes" `.ti-portada--sobre` (imagen por background DIRECTO,
         nunca url() dentro de un token — resolución de rutas inconsistente).
     (e) La confirmación por WhatsApp `.rsvp-wa*` (SPEC 0010), tokens --rsvpwa-*
         con fallback a --sec-* y a neutros AA.
     (f) El contador del visor de galería `.lightbox__contador` (SPEC 0012),
         fallback discreto restilizable/ocultable por plantilla.
     (g) Los MODOS de galería `galeria--pantalla` / `galeria--mosaico` /
         `galeria--ajuste-completa` (SPEC 0013), dirigidos por los tokens
         `--gal-*` (lista CERRADA: --gal-pantalla-bg, --gal-ctrl-bg,
         --gal-ctrl-tinta, --gal-gap, --gal-radio; solo-consumo con fallback
         AA — los declara la plantilla). Selectores de 2 clases (0-2-0) que
         ganan a la base de plantilla (0-1-0) aunque esta hoja cargue antes;
         la capa compartida NUNCA estiliza `.carrusel__*` sin prefijo de modo
         (carrusel+cubrir = cero clases ⇒ cero reglas nuevas aplican).

   Contrato de tokens (§4.3), consumidos SIEMPRE con fallback var(--sec-x, <AA>):
     --sec-bg            fondo del recuadro de ficha
     --sec-borde         borde/hairline de la ficha
     --sec-radio         radio de esquinas
     --sec-titulo-font   tipografía del título de sección/saludo
     --sec-titulo-color  color del título (AA sobre --sec-bg)
     --sec-texto-color   color del cuerpo (AA sobre --sec-bg)
     --sec-acento        acento (enlaces, filetes)
     --sec-acento-bg     fondo del recuadro de saludo
     --sec-acento-tinta  texto sobre --sec-acento-bg (AA)
     --sec-acento-boton-bg / --sec-acento-boton-tinta  botón "Ver mi pase"

   Esta hoja NO declara --sec-* en :root: solo los consume (los declara la
   plantilla; el último :root gana por var(), independiente del orden de hojas).
   ========================================================= */

[hidden] { display: none !important; }

/* =========================================================
   Sección dinámica estándar (.ti-sec) — mobile-first, BEM
   ========================================================= */
.ti-sec {
  max-width: var(--ancho, 620px);
  margin: 40px auto;
  padding: 40px 24px;
  background: var(--sec-bg, #f7f5f2);
  color: var(--sec-texto-color, #2a2a2a);
  border: 1px solid var(--sec-borde, rgba(0, 0, 0, .14));
  border-radius: var(--sec-radio, 14px);
  text-align: center;
}

.ti-sec__titulo {
  margin: 0 0 14px;
  font-family: var(--sec-titulo-font, inherit);
  font-size: clamp(1.4rem, 5vw, 2rem);
  line-height: 1.15;
  color: var(--sec-titulo-color, #1a1a1a);
}

.ti-sec__cuerpo { color: var(--sec-texto-color, #2a2a2a); }

.ti-sec__texto {
  margin: 0 auto;
  max-width: 46ch;
  font-size: 1.02rem;
  line-height: 1.7;
}

/* Sub-elementos para renderers futuros (lista/enlaces/colores — roadmap 0006 §6) */
.ti-sec__lista {
  list-style: none;
  margin: 0;
  padding: 0;
}
.ti-sec__lista li { padding: 6px 0; }

.ti-sec__enlaces {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
}
.ti-sec__enlaces a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 10px 18px;
  border-radius: 999px;
  font-weight: 600;
  text-decoration: none;
  background: var(--sec-acento, #333);
  color: var(--sec-acento-boton-tinta, #fff);
}

.ti-sec__colores {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
}
.ti-sec__color {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, .18);
}

/* ---------------------------------------------------------
   Grupos del renderer `grupos` (SPEC 0011 — sección Familia)
   Layout compartido MÍNIMO con los tokens --sec-* EXISTENTES (sin tokens
   nuevos): apilado mobile-first, etiqueta en la fuente de títulos a escala
   menor, nombres sobre .ti-sec__lista. El modificador --destacado queda
   NEUTRO aquí: su tratamiento fino lo viste cada plantilla por cascada.
   --------------------------------------------------------- */
.ti-sec__grupo { margin: 22px auto 0; }
.ti-sec__grupo:first-child { margin-top: 0; }

.ti-sec__grupo-etiqueta {
  margin: 0 0 6px;
  font-family: var(--sec-titulo-font, inherit);
  font-size: clamp(1.05rem, 4vw, 1.25rem); /* escala menor al título de sección */
  line-height: 1.2;
  color: var(--sec-titulo-color, #1a1a1a);
}

.ti-sec__grupo .ti-sec__lista li { padding: 3px 0; }

/* =========================================================
   Saludo del invitado — SOLO FALLBACK
   Las 7 plantillas con .saludo nativo prevalecen (cargan después).
   ========================================================= */
.saludo {
  text-align: center;
  padding: 40px 24px;
  background: var(--sec-acento-bg, #2b2b2b);
  color: var(--sec-acento-tinta, #ffffff);
}
.saludo__hola {
  font-family: var(--sec-titulo-font, inherit);
  font-size: clamp(1.6rem, 6vw, 2.3rem);
  line-height: 1.15;
  color: var(--sec-acento-tinta, #ffffff);
}
.saludo__pases {
  margin-top: 6px;
  letter-spacing: .04em;
  opacity: .92;
}

/* =========================================================
   CONFIRMACIÓN POR WHATSAPP DIRECTO (SPEC 0010) — fallback AA
   ---------------------------------------------------------
   El motor crea `.rsvp-wa` dentro de #rsvp cuando rsvp.modo === 'whatsapp'
   (el formulario queda [hidden]). Mobile-first: botones apilados a ancho
   completo; lado a lado desde 480px con jerarquía IDÉNTICA entre ambos.
   Tokens opcionales (definir = fidelidad; omitir = fallback AA):
     --rsvpwa-bg      fondo del botón   (fallback: par --sec-acento-boton-*)
     --rsvpwa-tinta   texto/ícono       (AA sobre --rsvpwa-bg)
     --rsvpwa-borde   borde/realce opcional
     --rsvpwa-radio   radio del botón
     --rsvpwa-intro   color del texto introductorio
   Una plantilla puede además sobrescribir `.rsvp-wa__boton` en su hoja
   (carga DESPUÉS de esta → gana por cascada, sin !important).
   ========================================================= */
.rsvp-wa {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  margin: 18px auto 0;
  max-width: 480px;
}

.rsvp-wa__intro {
  margin: 0;
  font-size: 1.02rem;
  line-height: 1.5;
  color: var(--rsvpwa-intro, inherit);
}

.rsvp-wa__botones {
  display: flex;
  flex-direction: column; /* móvil: apilados a ancho completo */
  gap: 12px;
  width: 100%;
}

.rsvp-wa__boton {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 48px;           /* área táctil ≥ 44px */
  padding: 12px 22px;
  border: 1px solid var(--rsvpwa-borde, transparent);
  border-radius: var(--rsvpwa-radio, 999px);
  /* Fallback en PARES AA del MISMO par de tokens (regla de tema.md): fondo y
     tinta caen JUNTOS al par garantizado --sec-acento-boton-* (AA por contrato)
     y, en último recurso, a los neutros AA propios #2b2b2b/#ffffff. NUNCA
     background de un par con tinta de otro (p. ej. --sec-acento suelto). */
  background: var(--rsvpwa-bg, var(--sec-acento-boton-bg, #2b2b2b));
  color: var(--rsvpwa-tinta, var(--sec-acento-boton-tinta, #ffffff));
  font-weight: 600;
  font-size: 1.02rem;
  line-height: 1.3;
  text-decoration: none;
}
.rsvp-wa__boton:focus-visible {
  outline: 3px solid currentColor;
  outline-offset: 3px;
}

.rsvp-wa__ico { display: inline-flex; flex: none; }
.rsvp-wa__ico svg {
  display: block;
  width: 20px;
  height: 20px;
}

@media (min-width: 480px) {
  .rsvp-wa__botones {
    flex-direction: row;
    justify-content: center;
  }
  .rsvp-wa__boton { flex: 1 1 0; } /* con 2 contactos, jerarquía idéntica */
}

/* =========================================================
   Foto del festejado DENTRO del sello/medallón del hero (capa compartida)
   ---------------------------------------------------------
   El motor inyecta <img class="hero__sello-foto"> dentro de cualquier sello
   ([data-rol="hero-sello"]) cuando hay CFG.fotos.hero. La foto se RECORTA a la
   forma del sello (círculo/cuadro) heredando su radio, sin importar la plantilla.
   Cada plantilla ya da tamaño y forma a su sello; el bloque :has() de abajo es
   solo una red de seguridad por si alguno no trajera dimensiones.
   ========================================================= */
.hero__sello-foto {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
  display: block;
}
/* Salvaguarda: si algún sello no define tamaño propio, la foto no colapsa. */
[data-rol="hero-sello"]:has(.hero__sello-foto) {
  min-width: 64px;
  min-height: 64px;
}

/* =========================================================
   Sello de cera de la invitación DENTRO del medallón del hero (SPEC 0008 §2.F.6)
   ---------------------------------------------------------
   El motor inyecta <img class="hero__sello-cera"> dentro de cualquier sello
   ([data-rol="hero-sello"]) SIEMPRE que la invitación tenga portada.selloId.
   Como el hook existe en TODOS los partials, el <img> también aparece en
   plantillas cuya hoja aún no lo estiliza: esto es la red de seguridad para
   que ahí no desborde el medallón.

   CONTENCIÓN PURA, no identidad: sin color, sombra, radio ni tamaño opinado —
   el look (tamaño real, sombra, tratamiento) lo pone CADA PLANTILLA en su hoja,
   que carga DESPUÉS y gana por cascada sin !important. (La segunda regla de
   abajo SÍ apaga decoración, pero del CONTENEDOR y como reset defensivo: su
   porqué está documentado ahí mismo.)

   Espeja el bloque de .hero__sello-foto de arriba con dos diferencias DURAS:
     · object-fit: contain (NO cover): un sello se contiene ENTERO; recortarlo
       lo mutilaría. Una foto sí se recorta a la forma del medallón.
     · SIN border-radius: inherit: cliparía el sello contra la forma del sello.
   Se acotan ancho Y alto: acotar una sola dimensión no contiene (un sello
   vertical desbordaría igual y object-fit no haría nada).

   ⚠️ min-width/min-height: 0 son OBLIGATORIOS, no cosméticos (bug medido en 9
   plantillas con un sello 1200×1500: desbordes de +11 a +25px; en lacre el sello
   tapaba el antetítulo). Los medallones son display:grid/flex, y un ítem de
   grid/flex tiene min-height:auto ⇒ la fila CRECE hasta caber la imagen y el
   max-height:100% se resuelve contra la fila ya crecida, no contra el medallón:
   no acota nada. Con min-*: 0 el ítem puede encogerse por debajo de su tamaño
   intrínseco y la contención vuelve a aplicar.
   Se corrige sobre el <img> (nuestro nodo) y NO sobre el contenedor: arreglarlo
   con grid-template-rows: minmax(0,1fr) también funciona, pero impondría layout
   al medallón, que es de la PLANTILLA. La capa compartida no dicta el layout
   ajeno; solo contiene lo que ella misma inyecta.
   ========================================================= */
.hero__sello-cera {
  max-width: 100%;
  max-height: 100%;
  min-width: 0;   /* ítem de grid/flex: sin esto no encoge y max-* no contiene. */
  min-height: 0;
  object-fit: contain;
  display: block;
}
/* Salvaguarda: max-height se resuelve contra la ALTURA DEL PADRE, así que en un
   medallón sin altura propia la contención no aplicaría.

   Y RESET DEFENSIVO de la decoración del contenedor (NO borrar por "innecesaria"):
   un sello PNG con alfa es una pieza gráfica COMPLETA — ningún diseño la quiere
   incrustada dentro de OTRO medallón (el círculo con gradiente de acuarela, el
   anillo de neón con glow…). Escenario real que lo dispara: el constructor solo
   ofrece subir sello donde soportaSobre (hoy solo lacre), pero el selloId
   SOBREVIVE al cambio de plantilla (grandfathering de attachment_admitido()),
   así que un cliente que sube su sello en lacre y luego prueba xv-neon vería la
   cera encima del anillo. Como el gate del sello es "siempre que haya selloId" y
   el hook existe en TODOS los partials, el reset tiene que vivir aquí, no en una
   hoja. Es defensa, no identidad: una plantilla que quisiera lo contrario
   re-enciende su decoración por cascada (esta hoja carga ANTES que la suya). */
[data-rol="hero-sello"]:has(.hero__sello-cera) {
  min-width: 64px;
  min-height: 64px;
  background: none;
  box-shadow: none;
  border-radius: 0;
}

/* =========================================================
   Pulso "toca para sonar" del botón de música (SPEC 0007 pendiente)
   El motor añade .musica--invita a #musica cuando autoGesto está activo.
   Especificidad .musica.musica--invita para ganar sin depender del orden.
   ========================================================= */
@media (prefers-reduced-motion: no-preference) {
  .musica.musica--invita {
    animation: ti-musica-pulso 2.4s ease-in-out infinite;
  }
  @keyframes ti-musica-pulso {
    0%, 100% { box-shadow: 0 0 0 0 rgba(0, 0, 0, .30); }
    50%      { box-shadow: 0 0 0 12px rgba(0, 0, 0, 0); }
  }
}

/* =========================================================
   PORTADA DE APERTURA "toca para abrir" (SPEC 0008)
   ---------------------------------------------------------
   Capa a pantalla completa que CUBRE la invitación al cargar y se ABRE al tocar
   (arranca la música y revela). La CREA motor.js (initPortada); aquí va el
   ANDAMIAJE compartido (estructura + animación por defecto + fallbacks AA),
   dirigido por tokens --portada-* que cada plantilla define en su :root. Cada
   plantilla puede SOBRESCRIBIR .ti-portada/.ti-portada__lienzo en su hoja (que
   carga DESPUÉS de esta → gana por cascada, sin !important). Transversal, NO
   versionada (como .ti-sec* y el pulso .musica--invita).

   Contrato de tokens (§4.3), consumidos con fallback var(--portada-x, <AA>):
     --portada-bg          fondo de la capa de apertura
     --portada-tinta       color de nombres/fecha/mensaje (AA sobre --portada-bg)
     --portada-acento      acento (filetes, sello)
     --portada-titulo-font tipografía de nombres/título
     --portada-radio       radio del panel/sobre
     --portada-cta-bg      fondo del botón "abrir"
     --portada-cta-tinta   texto del botón (AA sobre --portada-cta-bg)
   ========================================================= */
.ti-portada {
  position: fixed;
  inset: 0;
  z-index: 9999;                 /* por encima de música (40) y lightbox (50). */
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100svh;            /* svh: sin saltos por las barras del móvil. */
  padding: 32px 22px;
  text-align: center;
  background: var(--portada-bg, #1a1a2e);
  color: var(--portada-tinta, #ffffff);
  overflow: hidden;
  transition: opacity .5s ease;
  -webkit-tap-highlight-color: transparent;
  cursor: pointer;
  /* Un arrastre de UN dedo iniciado sobre la capa no se convierte en pan del
     documento (la capa es fixed/inset:0 ⇒ todo toque empieza aquí). Se usa
     `pinch-zoom` y NO `none` para conservar el zoom de dos dedos (accesibilidad
     para baja visión). Complementa el overflow:hidden de .ti-portada-sin-scroll y
     el preventDefault de touchmove en motor.js — ver bloquearScrollFondo(). */
  touch-action: pinch-zoom;
  /* INERTE hoy y a propósito: overscroll-behavior solo actúa sobre un contenedor
     de scroll, y esta capa es `overflow: hidden` ⇒ nunca scrollea, así que no hay
     encadenamiento que cortar (de eso se ocupan las 3 piezas de
     bloquearScrollFondo() en motor.js). Se conserva como red por si una plantilla
     le diera scroll propio a la capa; NO cuenta como parte del bloqueo. */
  overscroll-behavior: contain;
}
/* [hidden] ya lo cubre la regla global de arriba (display:none !important). */

/* Documento sin scroll mientras la portada cubre. La clase la pone motor.js en
   <html> (bloquearScrollFondo) y la quita descubrirFondo() + el propio gesto de
   apertura. Se declara en <html> Y <body> porque el overflow de <body> solo se
   propaga al viewport cuando el de <html> es `visible`.
   NO interfiere con el lightbox: aquel usa su propia regla `body:has(.lightbox.is-open)`
   en la hoja de la plantilla, que se libera sola al cerrarse el modal. */
.ti-portada-sin-scroll,
.ti-portada-sin-scroll body {
  overflow: hidden;
}

.ti-portada__lienzo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  max-width: 560px;
  width: 100%;
  min-width: 0;                  /* flex item: permite encoger bajo su contenido para
                                    que el título envuelva en vez de expandir la capa. */
  box-sizing: border-box;
  will-change: transform, opacity;
  transition: transform .55s ease, opacity .55s ease;
}

/* Decorativos (masthead "edición especial", emblema): solo los muestra/coloca la
   plantilla (p. ej. con ::before). En el fallback base no se ven. */
.ti-portada__masthead,
.ti-portada__emblema {
  display: none;
}

/* Título de ESTADO ("¡Estás invitado!" / "¡Tienes una invitación!"): secundario. */
.ti-portada__titulo {
  margin: 0;
  /* Límite ABSOLUTO al viewport (no depende del flex, que no encogía en móvil):
     garantiza que el texto largo ENVUELVA en vez de cortarse por el borde. */
  width: 100%;
  /* El tope en ch fuerza el salto de línea del texto largo de forma robusta,
     independiente del viewport/flex. Subido de 18ch a 30ch: con 18ch el tope
     valía 171px SIEMPRE —también en escritorio a 1440px de ancho— y partía en dos
     el título de estado por defecto ("¡Tienes una invitación!", 23 caracteres) aun
     sobrando espacio de sobra. 30ch sigue obligando a envolver a un título
     realmente largo, pero deja que el de fábrica quepa en un renglón; en móviles
     estrechos manda el otro término (a 320px, `100vw - 2.75rem` = 276px < 30ch). */
  max-width: min(calc(100vw - 2.75rem), 30ch);
  overflow-wrap: break-word;
  font-family: var(--portada-titulo-font, inherit);
  font-size: clamp(1.4rem, 6vw, 2.1rem);
  line-height: 1.1;
  text-wrap: balance;
  color: var(--portada-tinta, #ffffff);
}

/* Destinatario "PARA: <nombre>" — el nombre es el PROTAGONISTA de la portada.
   Llega async (initInvitado): el <p> arranca [hidden]; al resolver, motor.js lo
   muestra y añade .ti-portada--personalizada para el fade-in. */
.ti-portada__invitado {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  opacity: 0;
  transition: opacity .45s ease;
}
.ti-portada--personalizada .ti-portada__invitado { opacity: 1; }
.ti-portada__para {
  font-size: clamp(.78rem, 3.2vw, .98rem);
  letter-spacing: .18em;
  text-transform: uppercase;
  opacity: .85;
}
.ti-portada__nombre {
  font-family: var(--portada-titulo-font, inherit);
  font-size: clamp(2.3rem, 12vw, 4.2rem);
  line-height: 1.02;
  color: var(--portada-tinta, #ffffff);
}

.ti-portada__mensaje {
  margin: 0;
  max-width: 40ch;
  font-size: 1.02rem;
  line-height: 1.55;
  color: var(--portada-tinta, #ffffff);
  opacity: .9;
}

.ti-portada__cta {
  margin-top: 10px;
  min-height: 44px;
  min-width: 44px;
  padding: 12px 30px;
  font: inherit;
  font-weight: 700;
  font-size: 1.1rem;
  letter-spacing: .02em;
  color: var(--portada-cta-tinta, #1a1a2e);
  background: var(--portada-cta-bg, #ffffff);
  border: 0;
  border-radius: var(--portada-radio, 999px);
  cursor: pointer;
}
.ti-portada__cta:focus-visible {
  outline: 3px solid var(--portada-cta-tinta, #1a1a2e);
  outline-offset: 3px;
}

/* Estado de apertura (lo añade motor.js): la capa se desvanece y el lienzo sube
   ligeramente. SOLO opacity/transform en la PROPIA capa (nunca sobre el hero). */
.ti-portada--abriendo { opacity: 0; }
.ti-portada--abriendo .ti-portada__lienzo {
  transform: translateY(-18px) scale(1.03);
  opacity: 0;
}

/* Pulso invitador del CTA — mismo lenguaje que .musica--invita. El anillo usa
   --portada-acento (on-brand y visible sobre fondos claros U oscuros); fallback a
   un gris translúcido. Las plantillas con box-shadow de marca en el CTA (pop/glow)
   sustituyen este pulso por uno de solo `transform` en su hoja (para no pisar la
   sombra). */
@media (prefers-reduced-motion: no-preference) {
  .ti-portada:not(.ti-portada--abriendo) .ti-portada__cta {
    animation: ti-portada-pulso 2.2s ease-in-out infinite;
  }
  @keyframes ti-portada-pulso {
    0%, 100% { box-shadow: 0 0 0 0 var(--portada-acento, rgba(0, 0, 0, .28)); }
    50%      { box-shadow: 0 0 0 14px rgba(0, 0, 0, 0); }
  }
}

/* =========================================================
   PORTADA — variante "SOBRE con imágenes" (SPEC 0008 §2.E)
   ---------------------------------------------------------
   motor.js añade `.ti-portada--sobre` + la capa `.ti-portada__sobre` (con el
   sello) SOLO si portada.variante === 'sobre', la plantilla lo soporta
   (flag `soportaSobre` del manifiesto) y la PRE-CARGA del sobre confirmó
   onload — si la imagen no llega, queda la portada CSS pura de arriba.
   ⚠️ La imagen va por background-image DIRECTO en esta regla (relativa a esta
   hoja), NUNCA por url() dentro de una custom property (la resolución de rutas
   de url() en var() es inconsistente entre motores). Una plantilla puede
   sustituir el sobre (background-image) y la geometría en su hoja, que carga
   DESPUÉS y gana por cascada sin !important.
   La apertura reusa el MISMO estado --abriendo (sin clases nuevas): solo
   transform/opacity en capas propias dentro del lienzo (el hero, intacto), y
   sus duraciones (≤ .5s) caben en el timeout de respaldo de cerrarPortada (1.4s).
   ========================================================= */
.ti-portada--sobre .ti-portada__sobre {
  position: relative;
  /* El tercer término del min() es el CUPO DE ALTURA: `.ti-portada` no tiene
     scroll (overflow:hidden) y el lienzo va centrado por flex, así que si el
     contenido supera el viewport se recorta por ARRIBA Y POR ABAJO a partes
     iguales — y abajo es donde vive el CTA. El sobre es con diferencia el bloque
     más alto, así que se le da solo la altura que sobra:
       140px = gap 14 + título ~33 + gap 14 + margin-top del CTA 2 + CTA ~54
               (= ~117) + ~22px de aire, para que el CTA no quede pegado al borde.
       × 1.5 = el aspect-ratio 3/2 de abajo, para pasar de altura a ancho.
     Con `min()` el cupo SOLO manda cuando de verdad aprieta: con 3/2 y 360px de
     tope, deja de morder por encima de 140 + 360/1.5 = 380px de alto, o sea que
     en retrato y en landscape holgado el ancho es idéntico al de antes.
     Se usa svh (no vh) por coherencia con el min-height:100svh de la capa: es la
     altura CON la barra del navegador desplegada, que es el caso que recorta. */
  width: min(78vw, 360px, calc((100svh - 140px) * 1.5));
  aspect-ratio: 3 / 2;                    /* proporción sobre de papelería */
  background-image: url(portada/sobre-beige.webp);
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  filter: drop-shadow(0 14px 28px rgba(0, 0, 0, .25));
  will-change: transform, opacity;
  transition: transform .5s ease, opacity .5s ease;
}

/* Sello (imagen por invitación) y sello genérico (blob CSS con monograma):
   centrados sobre el sobre. Solo transform/opacity en la apertura. */
.ti-portada--sobre .ti-portada__sello,
.ti-portada--sobre .ti-portada__sello-generico {
  position: absolute;
  left: 50%;
  top: var(--portada-sello-y, 50%);
  transform: translate(-50%, -50%);
  will-change: transform, opacity;
  transition: transform .45s ease, opacity .45s ease;
}
.ti-portada--sobre .ti-portada__sello {
  width: var(--portada-sello-w, 34%);
  height: auto;
}
.ti-portada--sobre .ti-portada__sello-generico {
  display: flex;
  align-items: center;
  justify-content: center;
  /* aspect-ratio (no `height` en px) para que el blob siga siendo CIRCULAR cuando
     una plantilla fija --portada-sello-w en % del sobre. Con el default el
     resultado es idéntico al histórico: el mismo clamp en ancho y alto. */
  width: var(--portada-sello-w, clamp(72px, 24vw, 96px));
  height: auto;
  aspect-ratio: 1;
  border-radius: 47% 53% 50% 50% / 52% 48% 55% 45%; /* blob de cera */
  background: var(--portada-acento, #8a3b3c);       /* fallback cera AA con #fff */
  box-shadow:
    inset 0 2px 6px rgba(255, 255, 255, .22),
    inset 0 -3px 8px rgba(0, 0, 0, .28),
    0 4px 10px rgba(0, 0, 0, .30);
  font-family: var(--portada-titulo-font, inherit);
  font-size: 1.35rem;
  letter-spacing: .04em;
  color: #ffffff;
}

/* Apertura: el sello se LEVANTA y desvanece; el sobre se aparta hacia abajo.
   Mismo gesto/estado que la portada base (--abriendo), capas propias. */
.ti-portada--abriendo.ti-portada--sobre .ti-portada__sello,
.ti-portada--abriendo.ti-portada--sobre .ti-portada__sello-generico {
  transform: translate(-50%, -130%) rotate(-10deg) scale(1.06);
  opacity: 0;
}
.ti-portada--abriendo.ti-portada--sobre .ti-portada__sobre {
  transform: translateY(16px) scale(.94);
  opacity: 0;
}

/* ---------------------------------------------------------
   AFFORDANCE DEL SOBRE — que el invitado sepa QUÉ HACER
   ---------------------------------------------------------
   Un sobre cerrado bonito comunica "soy un objeto", no "soy un botón": en uso
   real hubo invitados que se quedaron mirándolo sin tocarlo. Tres señales, de
   permanente a insistente:
     (1) TEXTO visible bajo el sobre — es el propio .ti-portada__cta, que aquí
         deja de parecer botón de app y se lee como pie de papelería. Se usa el
         CTA y NO un texto nuevo para que el lector de pantalla siga anunciando
         UN solo control (un <p> extra + el botón dirían lo mismo dos veces).
         El copy lo pone motor.js (PORTADA_CTA_SOBRE): nombra el gesto.
     (2) LATIDO del sello — el movimiento es la señal más fuerte de
         interactividad en táctil.
     (3) REFUERZO a los ~4 s sin interacción (.ti-portada--insiste, la pone
         motor.js): el texto gana intensidad y se enciende el halo.
   LÍMITE CONOCIDO del refuerzo (no es bug, es alcance): motor.js añade
   `.ti-portada--insiste` a la capa SIEMPRE, pero TODAS las reglas que la
   consumen están scopeadas a `.ti-portada--sobre` (las 4 de este bloque). En el
   fallback CSS puro —cuando la pre-carga del .webp falla y la variante sobre no
   llega a montarse— la clase se pone y no pinta NADA: allí el CTA ya es un botón
   evidente y no necesita rescate. Si alguna vez hiciera falta, hay que escribir
   reglas `.ti-portada--insiste` sin el prefijo de variante.
   Geometría del sello y del halo por tokens (--portada-sello-y/-w) para que el
   halo siga al sello cuando una plantilla lo recoloca sobre su propio sobre:
     --portada-sello-y        centro vertical del sello en el sobre (default 50%)
     --portada-sello-w        ancho del sello (default 34% / clamp del genérico)
     --portada-cta-suave      tinta del CTA en reposo (default: --portada-tinta)
     --portada-cta-transform  text-transform del CTA (default: uppercase)
     --portada-cta-tracking   letter-spacing del CTA en reposo (default: .22em);
                              el estado --insiste lo abre +.04em por calc()
     --portada-halo           color/alfa del anillo del refuerzo (default:
                              --portada-acento). Tiene token propio porque el
                              acento de una plantilla puede no tener contraste
                              suficiente contra SU papel: en lacre el oro del
                              acento medía delta 0 sobre el crema.
   --------------------------------------------------------- */

/* (1) El CTA como pie discreto. La discreción sale de la TIPOGRAFÍA (versalitas,
   tracking, cuerpo pequeño) y NUNCA de `opacity`: medido sobre el papel de lacre,
   bajar la tinta a .8 hunde el contraste de 4.55:1 a 3.18:1 (falla AA). Sigue
   siendo un <button> real: enfocable, Enter/Espacio, área táctil ≥44px.

   `text-transform` y `letter-spacing` salen por TOKEN y no como literales: son
   voz tipográfica (versalitas espaciadas = papelería fina), no geometría, y esta
   hoja es compartida por TODAS las plantillas que adopten el sobre. Con el
   literal, una plantilla de tono neón/cómic heredaba versalitas de papelería sin
   poder evitarlo. Los defaults reproducen exactamente los valores anteriores, así
   que ninguna plantilla actual cambia de aspecto por este cambio.
   Lo demás del bloque (caja, diana ≥44px, background transparente) SÍ es
   estructura compartida y se queda como literal.

   CUERPO: 1rem = 16px, el piso que fija `compartido/estandares.md` §4. Antes iba
   a .82rem (13.1px, y 12.5px con el override de lacre): el elemento cuyo ÚNICO
   trabajo es resolver "el invitado no sabía qué tocar" era el texto más pequeño
   de la pantalla, por debajo del título. La discreción la tienen que dar las
   versalitas, el tracking, el peso 500 y el fondo transparente — no el tamaño.
   Cabe de sobra con el copy corto ("Toca para abrir"): ver la nota de
   PORTADA_CTA_SOBRE_TOQUE en motor.js. */
.ti-portada--sobre .ti-portada__cta {
  margin-top: 2px;
  min-height: 44px;
  padding: 10px 18px;
  background: transparent;
  border: 0;
  border-radius: 0;
  font-size: 1rem;
  font-weight: 500;
  text-transform: var(--portada-cta-transform, uppercase);
  letter-spacing: var(--portada-cta-tracking, .22em);
  color: var(--portada-cta-suave, var(--portada-tinta, #ffffff));
  transition: color .6s ease, letter-spacing .6s ease;
}
/* El pulso de anillo del CTA base (box-shadow) no pega en un texto sin caja.
   Especificidad 0,3,0 = la de la regla del pulso, y esta va después ⇒ gana. */
.ti-portada--sobre:not(.ti-portada--abriendo) .ti-portada__cta {
  animation: none;
}
/* (3) Refuerzo del texto: sube a la tinta plena y abre el tracking.
   El tracking se expresa como DELTA sobre el de reposo (+.04em por calc), no como
   literal: así una plantilla que baje --portada-cta-tracking conserva el mismo
   salto relativo sin tener que redeclarar también este estado. Con el default
   (.22em) da .26em — el valor que tenía escrito antes. */
.ti-portada--sobre.ti-portada--insiste .ti-portada__cta {
  color: var(--portada-tinta, #ffffff);
  letter-spacing: calc(var(--portada-cta-tracking, .22em) + .04em);
}

/* (3) Halo del sello. Nodo propio creado por motor.js (un <img> no admite
   pseudo-elementos), hermano de AMBOS sellos y primero en el DOM ⇒ pinta debajo.
   Apagado por defecto; solo lo enciende --insiste. Al abrir se va con su capa
   (.ti-portada__sobre pasa a opacity 0), sin regla propia. */
.ti-portada--sobre .ti-portada__halo {
  position: absolute;
  left: 50%;
  top: var(--portada-sello-y, 50%);
  width: calc(var(--portada-sello-w, 34%) * 2.2);
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  /* ANILLO, no resplandor. El degradado anterior arrancaba opaco en el CENTRO
     (`... 0%, transparent 68%`) y ahí no se veía NADA: el halo mide 2.2× el ancho
     del sello, así que el propio sello tapa el disco hasta 1/2.2 = 45.5% del radio
     del halo, justo donde estaba todo el alfa. Lo único que escapaba era la cola
     del degradado (alfa ya decaído a ~.17 del declarado) y se extinguía a 68%.
     Medido A/B contra el mismo frame con el halo en display:none: delta 0/255 en
     todos los canales — no pintaba un solo nivel.
     Ahora la parada opaca empieza JUSTO FUERA del borde del sello (46%), llega a
     su máximo en 56% y se apaga en 82%, así que todo el alfa cae en la corona que
     de verdad se ve. Los porcentajes están atados a ese 45.5%: si alguien cambia
     el multiplicador 2.2 del `width`, hay que recalcular las tres paradas.
     ⚠️ `closest-side` NO es decorativo: sin él un radial-gradient usa
     `farthest-corner`, o sea que el 100% del degradado cae en la ESQUINA de la
     caja, a radio × 1.414. Los porcentajes de arriba se estirarían un 41% (el 56%
     pasaría a 79% del radio real) y el anillo saldría desplazado hacia fuera y
     recortado por el border-radius. Medido: con farthest-corner la corona pintaba
     entre el 68% y el 97% del radio en vez de entre el 46% y el 82%. */
  background: radial-gradient(circle closest-side,
    transparent 46%,
    var(--portada-halo, var(--portada-acento, rgba(0, 0, 0, .28))) 56%,
    transparent 82%);
  opacity: 0;
  pointer-events: none;
  transition: opacity .6s ease;
}
.ti-portada--sobre.ti-portada--insiste .ti-portada__halo { opacity: .6; }

/* (2) LATIDO de TODA LA PIEZA (sobre + sello + halo) + respiración del halo.
   Ambos dentro de no-preference: bajo `reduce` la regla ni existe ⇒ cero
   movimiento (el halo del refuerzo sí se enciende, estático, porque encender no
   es animar).

   La animación va en `.ti-portada__sobre` y NO en los sellos. Motivo medido: el
   sello ocupa --portada-sello-w del ancho del sobre (29% en lacre ⇒ 116px sobre
   un sobre de 400px), así que la escala 1.035 que llevaba antes movía su borde
   ~2px — por debajo del umbral en el que el movimiento se lee como affordance.
   Animando el contenedor, el borde del sobre se desplaza ~4px por lado y arrastra
   el sello, el halo y el drop-shadow como una sola pieza.
   La AMPLITUD baja de 1.035 a 1.02 justamente porque el área animada creció: a
   mayor superficie, menor escala para el mismo carácter (con 1.035 sobre los
   400px del sobre el papel se deforma y lee como gelatina, no como papelería).

   ⚠️ Aquí los keyframes NO repiten ningún translate: `.ti-portada__sobre` no
   lleva transform propio en reposo (su base es `none`), a diferencia de los
   sellos, que sí van centrados con translate(-50%,-50%). El transform de un hijo
   es independiente del de su padre, así que el centrado del sello y del halo
   sigue intacto mientras el padre escala.
   ⚠️ Los sellos NO deben llevar animación propia: sus transforms se compondrían
   con el del padre y el sello latiría el doble que el sobre.
   :not(--abriendo) para que al abrir el `transform` de la apertura no quede
   bloqueado por una animación infinita — el mismo mecanismo que ya usaba el
   sello, y NO choca con el bloque `reduce` de más abajo (allí la animación ni
   se declara, porque este bloque entero vive en `no-preference`). */
@media (prefers-reduced-motion: no-preference) {
  .ti-portada--sobre:not(.ti-portada--abriendo) .ti-portada__sobre {
    animation: ti-portada-latido 2.4s ease-in-out infinite;
  }
  @keyframes ti-portada-latido {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(1.02); }
  }
  /* El halo respira ADEMÁS del latido del padre: las dos escalas se multiplican.
     Su pico baja de 1.06 a 1.04 para que la escala compuesta en el peor caso
     (ambas animaciones en fase) quede en 1.02 × 1.04 = 1.0608 ≈ el 1.06 que
     tenía el halo solo, y el refuerzo no se dispare al crecer la pieza. */
  .ti-portada--sobre.ti-portada--insiste:not(.ti-portada--abriendo) .ti-portada__halo {
    animation: ti-portada-halo 2.4s ease-in-out infinite;
  }
  @keyframes ti-portada-halo {
    0%, 100% { opacity: .38; transform: translate(-50%, -50%) scale(.92); }
    50%      { opacity: .72; transform: translate(-50%, -50%) scale(1.04); }
  }
}

/* Reduced-motion: sin desplazamientos/escala. Fundido corto (para que
   transitionend dispare la revelación rápido) en vez de un cambio brusco.
   `!important` con especificidad baja pero > `*` del reset global de cada
   plantilla (que usa `* { transition:none !important }`): así la revelación NO
   cae al timeout de respaldo (1.4 s) en ninguna plantilla — vale para todas
   sin repetir por hoja. Cubre también las capas de la variante sobre. */
@media (prefers-reduced-motion: reduce) {
  .ti-portada { transition: opacity .2s linear !important; }
  .ti-portada__lienzo { transition: opacity .2s linear !important; will-change: opacity; }
  .ti-portada--abriendo .ti-portada__lienzo { transform: none !important; opacity: 0; }
  .ti-portada--sobre .ti-portada__sobre,
  .ti-portada--sobre .ti-portada__sello,
  .ti-portada--sobre .ti-portada__sello-generico {
    transition: opacity .2s linear !important;
    will-change: opacity;
  }
  .ti-portada--abriendo.ti-portada--sobre .ti-portada__sobre {
    transform: none !important;
    opacity: 0;
  }
  .ti-portada--abriendo.ti-portada--sobre .ti-portada__sello,
  .ti-portada--abriendo.ti-portada--sobre .ti-portada__sello-generico {
    transform: translate(-50%, -50%) !important; /* quieto: solo fundido */
    opacity: 0;
  }
}

/* =========================================================
   VISOR DE GALERÍA — contador "n / total" (SPEC 0012) — fallback discreto
   ---------------------------------------------------------
   El motor lo inserta en el lightbox (aria-hidden: la posición la anuncia el
   alt de la foto). Discreto y AA sobre el fondo oscuro de los visores; una
   plantilla puede restilizarlo u OCULTARLO en su hoja (carga después → gana
   por cascada, sin !important). Con una sola foto el motor lo deja [hidden].
   ========================================================= */
.lightbox__contador {
  position: absolute;
  left: 50%;
  bottom: max(18px, env(safe-area-inset-bottom));
  transform: translateX(-50%);
  font-size: .85rem;
  letter-spacing: .08em;
  color: rgba(255, 255, 255, .88);
  text-shadow: 0 1px 3px rgba(0, 0, 0, .65);
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}

/* =========================================================
   MODOS DE GALERÍA (SPEC 0013) — inciso (g) del inventario
   ---------------------------------------------------------
   "La misma galería, tres maneras de mirarla": el motor traduce
   CONFIG.galeria a UNA clase de modo en la sección #galeria; la base de cada
   modo vive AQUÍ una sola vez y las 13 plantillas la heredan tematizada por
   los tokens --gal-* (lista cerrada, solo-consumo con fallback AA). Una
   plantilla refina un modo repitiendo el selector de 2 clases en su hoja
   (carga después ⇒ gana por cascada, sin !important). Con carrusel+cubrir
   (default/ausencia) NINGUNA regla de este bloque aplica. Todo ADITIVO.

   Markup motor-created del modo pantalla (verificado contra motor.js):
     .carrusel__slide > img.carrusel__fondo (PRIMER hijo, aria-hidden, mismo
     src que la foto) + img (la foto nítida, role=button) ; clase `is-active`
     en el slide centrado (la togglea el listener de scroll del track).
   ========================================================= */

/* ---------------------------------------------------------
   Ajuste "Verse completa" (SOLO carrusel): la foto ENTERA (contain) dentro
   del marco que la plantilla ya pinta en el slide (lacre: crema ⇒ paspartú
   gratuito). SIN token: el letterbox es el fondo del propio slide.
   --------------------------------------------------------- */
.galeria--ajuste-completa .carrusel__slide img {
  object-fit: contain;
  cursor: zoom-in; /* M5: la foto abre el visor; que se note. */
}

/* M6 — mat REAL: el paspartú solo se PERCIBE si hay margen suficiente. Las
   plantillas dejan ~6px alrededor de la foto (lacre: padding 6px), así que la
   perilla "Verse completa" no producía diferencia visible con fotos del mismo
   aspecto que el marco. Este padding pinta sobre el fondo que la plantilla ya
   da al slide (crema en lacre) ⇒ papelería intencional en todas las plantillas.
   box-sizing: border-box (base de las plantillas) ⇒ no altera el ancho del
   slide ni el snap: solo encoge la foto dentro del marco. */
.galeria--ajuste-completa .carrusel__slide { padding: clamp(12px, 4vw, 26px); }

/* ---------------------------------------------------------
   Modo PANTALLA (galeria--pantalla): cada foto entera (contain) a lo ancho
   del viewport con alto de cine, sobre un letterbox premium en 2 niveles:
   base --gal-pantalla-bg + la PROPIA foto desenfocada (blur ESTÁTICO, nunca
   animado; escape por plantilla: display:none al fondo) + velo del color de
   la plantilla. Mismo track horizontal con snap (no stack vertical).
   --------------------------------------------------------- */
.galeria.galeria--pantalla {
  /* Full-bleed: se sale del ancho de columna de la plantilla. */
  width: 100vw;
  max-width: 100vw;
  margin-inline: calc(50% - 50vw);
  /* m7 — el bloque oscuro necesita respiro contra la sección vecina (antes
     padding:0 ⇒ chocaba con "Mesa de regalos"). */
  padding-block: clamp(24px, 7vw, 56px);
  padding-inline: 0;
}

/* Las flechas flotan SOBRE la foto en desktop: ancla de posicionamiento. */
.galeria--pantalla .carrusel {
  position: relative;
  width: auto;
  max-width: none;
  margin-inline: 0;
  padding: 0;
}

/* m8 — full-bleed REAL en móvil: `padding: 0` NO neutraliza el
   `scroll-padding-inline` que las plantillas ponen al track (lacre: 7vw), y ese
   scroll-padding desplaza el punto de reposo del snap ⇒ el slide no arranca
   pegado al borde. Se resetean AMBOS (y el margen) explícitamente. */
.galeria--pantalla .carrusel__track {
  gap: 0;
  margin-inline: 0;
  padding: 0;
  scroll-padding: 0;
}

.galeria--pantalla .carrusel__slide {
  position: relative;
  overflow: hidden;
  contain: paint;
  flex: 0 0 100%;
  width: 100%;
  height: min(82svh, 720px);
  max-height: none;
  aspect-ratio: auto;      /* reset del 4/5 típico del carrusel de plantilla */
  padding: 0;
  border-radius: 0;
  box-shadow: none;
  scroll-snap-align: center;
  scroll-snap-stop: always;
  background: var(--gal-pantalla-bg, #141414); /* nivel 1: base del letterbox */
}

/* Nivel 2 (wow): la misma foto desenfocada cubriendo el slide. Blur estático. */
.galeria--pantalla .carrusel__fondo {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 0;
  filter: blur(28px) saturate(1.06);
  /* C1 — scale 1.4 (antes 1.18): con blur(28px) el desenfoque "come" ~28px de
     cada borde y a 1.18 el propio borde del fondo asomaba como COSTURA vertical
     en los extremos del letterbox. 1.4 lo empuja fuera del slide en todo ancho. */
  transform: scale(1.4);
  pointer-events: none; /* decorativo: el click del visor vive SOLO en la foto nítida */
}

/* Velo del color de plantilla sobre el fondo desenfocado. z-index EXPLÍCITO:
   el ::before se genera ANTES del fondo en el árbol y sin él pintaría debajo. */
.galeria--pantalla .carrusel__slide::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--gal-pantalla-bg, #141414);
  /* M2 — velo a .52 (antes .38): con el cacao de lacre a ~.5 la banda vira a
     chocolate cálido emparentado con vino/crema; a .38 quedaba en tierra de
     nadie (ni foto ni marco). Sigue dejando leer la textura del blur. */
  opacity: .52;
}

/* La foto nítida, entera y sin radio, por encima del letterbox. */
.galeria--pantalla .carrusel__slide img:not(.carrusel__fondo) {
  position: relative;
  z-index: 2;
  width: 100%;
  height: 100%;
  max-height: none;
  object-fit: contain;
  border-radius: 0;
  box-shadow: none;
  cursor: zoom-in; /* M5: coherencia — en los 3 modos la foto abre el visor. */
}

/* Flechas: ocultas en móvil (el gesto es el swipe); en desktop flotan sobre la
   foto (48px, par AA --gal-ctrl-*). Los dots quedan DEBAJO del track con el
   estilo de la plantilla (nunca superpuestos a la foto): sin reglas aquí. */
.galeria--pantalla .carrusel__nav { display: none; }

@media (min-width: 1024px) {
  /* C1 — ACOTAR el escenario, no estirarlo. A 1440 el slide medía 1440×720 con
     la foto pintada a 576px ⇒ 432px de banda borrosa POR LADO (60% de la
     superficie era letterbox). En desktop el slide se limita a min(92vw,1000px)
     centrado: el letterbox se lee como MARCO, no como protagonista. El alto y
     su cap no cambian. El centrado NO se hace con margin-inline:auto (en una
     línea flex con overflow no hay espacio libre que absorber): se hace con
     padding-inline en el track, que además deja centrar el PRIMER y ÚLTIMO
     slide con scroll-snap-align: center. */
  .galeria--pantalla .carrusel__track {
    padding-inline: max(0px, calc((100% - min(92vw, 1000px)) / 2));
  }

  .galeria--pantalla .carrusel__slide {
    flex-basis: min(92vw, 1000px);
    width: auto;
    max-width: min(92vw, 1000px);
    margin-inline: auto;
    max-height: 84vh;
  }

  .galeria--pantalla .carrusel__nav {
    display: flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: 50%;
    z-index: 3;
    width: 48px;
    height: 48px;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--gal-ctrl-bg, rgba(17, 17, 17, .62));
    color: var(--gal-ctrl-tinta, #ffffff);
    font-size: 1.6rem;
    line-height: 1;
    cursor: pointer;
    transform: translateY(-50%);
  }
  /* Las flechas siguen al escenario acotado (si quedaran en el borde del
     viewport flotarían sobre el fondo de la sección, lejos de la foto). */
  .galeria--pantalla .carrusel__nav--prev {
    left: calc(max(0px, (100% - min(92vw, 1000px)) / 2) + 14px);
  }
  .galeria--pantalla .carrusel__nav--next {
    right: calc(max(0px, (100% - min(92vw, 1000px)) / 2) + 14px);
  }
  /* Hover: el fondo sube ~.62→.78 SIN tocar el token (capa oscura interna). */
  .galeria--pantalla .carrusel__nav:hover {
    box-shadow: inset 0 0 0 48px rgba(17, 17, 17, .16);
  }
  .galeria--pantalla .carrusel__nav:active { transform: translateY(-50%) scale(.94); }
  .galeria--pantalla .carrusel__nav:focus-visible {
    outline: 3px solid var(--gal-ctrl-tinta, #ffffff);
    outline-offset: 2px;
  }
}

/* ---------------------------------------------------------
   Modo MOSAICO (galeria--mosaico): cuadrícula de miniaturas 1:1 uniformes
   (sin destacados en la capa base), 2/3/4 columnas fijas por breakpoint con
   cap 1040px; tap/Enter ⇒ visor (rol principal del lightbox). Navs y dots
   display:none (y los dots NI se construyen: guard en motor.js).
   --------------------------------------------------------- */
.galeria.galeria--mosaico {
  width: min(92vw, 1040px);
  max-width: none;
  margin-inline: auto;
}

/* M4 — FLEX-WRAP centrado, no grid. Con grid, una última fila incompleta
   (6 fotos en 4 columnas ⇒ 4+2) queda pegada a la izquierda con media fila
   vacía: se lee como "carga incompleta", y 5–7 fotos es el caso más común.
   flex-wrap + justify-content:center centra la fila huérfana de forma natural
   conservando columnas fijas por breakpoint (las fija el flex-basis del slide). */
.galeria--mosaico .carrusel__track {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--gal-gap, 10px);
  overflow: visible;
  margin-inline: 0;
  padding: 0;
  scroll-snap-type: none;
}

/* ⚠️ Resets ENUMERADOS de lo que el slide hereda del carrusel de plantilla
   (flex-basis, aspect-ratio 4/5, max-height, padding, box-shadow, transform):
   no confiar en "no aplican" al reusar el track del carrusel. */
.galeria--mosaico .carrusel__slide {
  /* 2 columnas (base móvil). El epsilon de .02px absorbe el redondeo
     sub-pixel del ancho del track: sin él, un navegador puede calcular la
     suma de la fila 0.01px por encima del 100% y tirar la última celda a la
     fila siguiente (fila de UNA celda con hueco). */
  flex: 0 0 calc((100% - var(--gal-gap, 10px) - .02px) / 2);
  max-width: calc((100% - var(--gal-gap, 10px) - .02px) / 2);
  width: auto;
  height: auto;
  aspect-ratio: 1 / 1;
  max-height: none;
  padding: 0;
  box-shadow: none;
  transform: none;
  scroll-snap-align: none;
  border-radius: var(--gal-radio, var(--sec-radio, 12px));
  overflow: hidden;
}

.galeria--mosaico .carrusel__slide img {
  display: block;
  width: 100%;
  height: 100%;
  max-height: none;
  object-fit: cover;
  border-radius: 0;
  cursor: zoom-in; /* M5: la miniatura AMPLÍA; sin esto no había ninguna pista. */
}

.galeria--mosaico .carrusel__nav,
.galeria--mosaico .carrusel__dots { display: none; }

/* 3 columnas */
@media (min-width: 768px) {
  .galeria--mosaico .carrusel__slide {
    flex-basis: calc((100% - 2 * var(--gal-gap, 10px) - .02px) / 3);
    max-width: calc((100% - 2 * var(--gal-gap, 10px) - .02px) / 3);
  }
}
/* 4 columnas */
@media (min-width: 1024px) {
  .galeria--mosaico .carrusel__slide {
    flex-basis: calc((100% - 3 * var(--gal-gap, 10px) - .02px) / 4);
    max-width: calc((100% - 3 * var(--gal-gap, 10px) - .02px) / 4);
  }
}

/* ---------------------------------------------------------
   Foco visible sobre la foto (guía §4): doble anillo — blanco interior
   (outline con offset negativo, visible sobre fotos oscuras) + oscuro
   (visible sobre fotos claras). Las fotos son controles reales
   (role="button" tabindex="0", verificado en motor.js §slides) y DEBEN tener
   indicador de foco en los 3 modos.

   M3 — por qué no se veía el anillo oscuro en MOSAICO: la celda lleva
   `overflow: hidden` (necesario para el radio), y eso RECORTA cualquier
   box-shadow EXTERIOR de la foto ⇒ el segundo anillo no podía pintar. Un
   box-shadow `inset` tampoco sirve sobre un <img> (el contenido reemplazado
   lo tapa). Solución: en mosaico el anillo oscuro se dibuja como borde de un
   ::after de la CELDA (dentro del recorte), vía :has(); donde `:has()` no
   exista queda el anillo blanco, que sigue siendo indicador válido.
   --------------------------------------------------------- */
.galeria--pantalla .carrusel__slide img:not(.carrusel__fondo):focus-visible,
.galeria--ajuste-completa .carrusel__slide img:focus-visible,
.galeria--mosaico .carrusel__slide img:focus-visible {
  outline: 3px solid #ffffff;
  outline-offset: -3px;
  box-shadow: 0 0 0 2px rgba(0, 0, 0, .6);
  position: relative;
  z-index: 4; /* el anillo por encima de velos/fondos del propio modo. */
}

/* Anillo oscuro DENTRO del recorte de la celda (mosaico). */
.galeria--mosaico .carrusel__slide { position: relative; }
.galeria--mosaico .carrusel__slide:has(img:focus-visible)::after {
  content: "";
  position: absolute;
  inset: 3px; /* justo por DENTRO del anillo blanco (outline-offset: -3px). */
  z-index: 5;
  border: 3px solid rgba(0, 0, 0, .6);
  border-radius: inherit;
  pointer-events: none;
}

/* ---------------------------------------------------------
   Microinteracciones de los modos — SOLO bajo no-preference (M3). El blur
   del letterbox es ESTÁTICO (no es movimiento: se conserva siempre). Jamás
   `opacity` con scroll-timeline (gotcha liga-heroes).
   --------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  /* Pantalla: la foto nítida "asienta" al centrarse (is-active del motor). */
  .galeria--pantalla .carrusel__slide img:not(.carrusel__fondo) {
    transform: scale(.97);
    transition: transform .45s ease;
  }
  .galeria--pantalla .carrusel__slide.is-active img:not(.carrusel__fondo) {
    transform: scale(1);
  }

  /* Mosaico: zoom sutil de la miniatura al hover. NO en :focus-within (M3): la
     celda recorta (overflow:hidden), así que al escalar la foto su anillo de
     foco se saldría del recorte y volvería a quedar invisible. El foco tiene su
     propio indicador (doble anillo); el zoom es puramente de puntero. */
  .galeria--mosaico .carrusel__slide img { transition: transform .35s ease; }
  .galeria--mosaico .carrusel__slide:hover img { transform: scale(1.05); }
}
