/* =========================================================
   1. RESET
   ========================================================= */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
html, body { height: 100%; }
/* -webkit-user-drag: sin esto, un swipe que arranca sobre una foto grande
   puede ser tomado por el navegador como "arrastrar la imagen" en vez de
   como gesto de navegación — a veces el primer intento no reaccionaba. */
img, picture, video, canvas, svg { display: block; max-width: 100%; height: auto; -webkit-user-drag: none; user-select: none; }
input, button, textarea, select { font: inherit; }
button { border: none; background: none; cursor: pointer; }
body { overflow-wrap: break-word; }
/* Sin esto, el atributo [hidden] pierde contra cualquier regla nuestra que
   le ponga display propio (ej. .rsvp-count-row { display: flex }) — el
   elemento queda "hidden" pero se sigue viendo igual. */
[hidden] { display: none !important; }

/* =========================================================
   2. DESIGN TOKENS — placeholder, pendiente de paleta y tipografía final
   ========================================================= */
:root {
  --color-bg: #0b0b0f;
  --color-fg: #f5f5f0;
  --color-muted: #a8a8a3;
  --color-accent: #C5A253;

  /* Barras laterales cuando la pantalla excede el ancho de móvil/tablet */
  --color-frame-bg: #000000;
  --frame-max-width: 820px; /* cubre desde celulares hasta tablets en vertical (iPad ~768-834px) */

  --fs-h1: clamp(2.25rem, 5vw + 1rem, 5.5rem);
  --fs-h2: clamp(1.5rem, 3vw + 0.5rem, 3rem);
  --fs-body: clamp(1rem, 0.5vw + 0.9rem, 1.15rem);

  /* Jerarquía tipográfica del contenido de cada sección (no de la Portada,
     que tiene su propio tratamiento). Un tamaño único por categoría — si
     algún texto puntual necesita otro, se overridea aparte, no acá. */
  --fs-sobretitulo: clamp(0.75rem, 2.8vw, 0.95rem); /* texto chico arriba del título, ej. "Con la bendición de" */
  --fs-titulo: clamp(2.2rem, 11vw, 3.4rem);          /* título grande de cada sección (New Icon Script) */
  --fs-subtitulo: clamp(0.94rem, 3.5vw, 1.16rem);    /* encabezados secundarios dentro del contenido */
  --fs-texto: clamp(0.86rem, 3.1vw, 1.01rem);        /* texto de párrafo / informativo */

  --space-sm: clamp(0.75rem, 2vw, 1.5rem);
  --space-md: clamp(1.5rem, 4vw, 3rem);
  --space-lg: clamp(2.5rem, 6vw, 6rem);

  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out-quart: cubic-bezier(0.76, 0, 0.24, 1);
  --ease-out-back: cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-in-quart: cubic-bezier(0.5, 0, 0.75, 0); /* acelera todo el tiempo, sin frenar — para el zoom continuo de Dress Code → No Niños */

  --shadow-card:
    0 1px 2px rgba(0,0,0,0.10),
    0 4px 10px rgba(0,0,0,0.14),
    0 16px 28px rgba(0,0,0,0.22);

  --dur-base: 500ms;
  --dur-slow: 500ms;
  --dur-cinematic: 900ms;
  --dur-envelope: 1000ms; /* apertura de la solapa */
  --dur-card-reveal: 950ms; /* salida de las tarjetas */
}

/* =========================================================
   3. TIPOGRAFÍA BASE
   ========================================================= */
/* Fuente variable autohospedada (un solo archivo cubre los 4 pesos usados). */
@font-face {
  font-family: "Montserrat";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../assets/fonts/montserrat-var-latin.woff2") format("woff2");
}
/* Serif solo para el nombre en la tarjeta de texto, como en la referencia. */
@font-face {
  font-family: "Playfair Display";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("../assets/fonts/playfair-display-600-latin.woff2") format("woff2");
}
/* Peso regular: texto del remate final (fecha, contador, etiquetas). */
@font-face {
  font-family: "Playfair Display";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/playfair-display-400-latin.woff2") format("woff2");
}
/* Script/caligráfica: nombres de la pareja en el remate final, y también
   los títulos grandes de cada sección (Itinerario, Formal, No niños,
   Regalos, Asistencia). */
@font-face {
  font-family: "New Icon Script";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/New-Icon-Script-Exfont1157.otf") format("opentype");
}
/* Script/caligráfica alternativa: solo para el texto de abajo del sobre
   en la Portada ("Gracias por ser parte..." / "Toca para continuar"). */
@font-face {
  font-family: "Alex Brush";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/alex-brush-400-latin.woff2") format("woff2");
}

body {
  font-family: "Montserrat", system-ui, -apple-system, "Segoe UI", sans-serif;
  color: var(--color-fg);
  line-height: 1.5;
}
h1, h2 { line-height: 1.05; font-weight: 700; letter-spacing: -0.02em; }
h1 { font-size: var(--fs-h1); }
h2 { font-size: var(--fs-h2); margin-bottom: var(--space-sm); }
.fs-body { font-size: var(--fs-body); color: var(--color-muted); max-width: 60ch; }

/* =========================================================
   4. LAYOUT — SHELL BLOQUEADO (núcleo del Modo App)
   ========================================================= */
html, body {
  height: 100svh;         /* small viewport height: nunca se corta ni scrollea de más */
  overflow: hidden;
  overscroll-behavior: none;
}

/* body centra el frame y expone las barras laterales en pantallas anchas */
body {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-frame-bg);
}

/* Preloader: tapa todo hasta que las fotos de Portada+Padres (las únicas
   visibles antes de cualquier gesto) terminaron de descargar del todo —
   así nunca se ve una imagen "dibujándose" de a poco. Vive fuera de
   .app-frame, por encima de absolutamente todo (ver JS al final de
   js/script.js, es la primera cosa que corre). */
.preloader {
  position: fixed;
  inset: 0;
  z-index: 300;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg);
  transition: opacity 500ms var(--ease-out-expo);
}
.preloader-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}
.preloader-heart {
  color: var(--color-accent);
  font-size: 1.6rem;
  animation: preloader-beat 1.1s ease-in-out infinite;
}
@keyframes preloader-beat {
  0%, 100% { transform: scale(1); opacity: 0.6; }
  50% { transform: scale(1.15); opacity: 1; }
}
.preloader-bar {
  width: 9rem;
  height: 2px;
  background: rgba(245, 245, 240, 0.15);
  border-radius: 999px;
  overflow: hidden;
}
.preloader-bar-fill {
  height: 100%;
  width: 0%;
  background: var(--color-accent);
  transition: width 250ms var(--ease-out-expo);
}
.preloader.is-done { opacity: 0; pointer-events: none; }

/* Pantalla de "necesitás tu invitación": tapa todo, incluso al preloader
   (z-index más alto) — se decide en el script inline de más abajo, apenas
   carga la página, según si hay o no ?inv=TOKEN en la URL. */
.sin-invitacion {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg);
  padding: var(--space-md);
  text-align: center;
}
.sin-invitacion-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  max-width: 26rem;
}
.sin-invitacion-content .ornament-heart { font-size: 1.8rem; }
.sin-invitacion-titulo {
  font-family: "Playfair Display", Georgia, serif;
  font-size: clamp(1.6rem, 6vw, 2.1rem);
  font-weight: 600;
  color: var(--color-fg);
}
.sin-invitacion-texto {
  font-family: "Playfair Display", Georgia, serif;
  font-size: var(--fs-texto);
  line-height: 1.6;
  color: var(--color-muted);
}

@media (prefers-reduced-motion: reduce) {
  .preloader-heart { animation: none; }
  .preloader { transition-duration: 200ms; }
}

/* app-frame: el "dispositivo". Ocupa el 100% en móvil/tablet real;
   en desktop se limita a --frame-max-width y quedan barras a los costados. */
.app-frame {
  position: relative;
  width: 100%;
  max-width: var(--frame-max-width);
  height: 100svh;
  overflow: hidden;
  background: var(--color-bg);
  /* Sin esto, arrastrar el dedo sobre el "dispositivo" puede disparar
     scroll/rebote/zoom nativo del navegador — se ve mal en algo pensado
     como una app de una sola pantalla fija. */
  touch-action: none;
}

.sections { position: relative; height: 100%; width: 100%; }

/* Secciones "mayores" (Portada, Galería, ...): el cambio en sí ocurre
   tapado por .transition-veil, así nunca se alcanza a ver una sección
   "sangrando" sobre la otra. Además, la sección que entra hace su propio
   "pop" de escala — se nota más viva, no es solo un corte. */
.panel {
  position: absolute;
  inset: 0;
  height: 100%;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--space-lg) var(--space-md);
  opacity: 0;
  transform: scale(1.12);
  transition: opacity 560ms var(--ease-out-expo), transform 700ms var(--ease-out-back);
  pointer-events: none;
  z-index: 1;
  /* Le avisa al navegador que este elemento va a animar transform/opacity
     seguido, para que lo suba a su propia capa de antemano en vez de
     armarla recién al primer frame de cada transición — se nota en
     celulares de gama baja. */
  will-change: transform, opacity;
}
.panel.is-active { opacity: 1; transform: scale(1); pointer-events: auto; z-index: 2; }

/* Velo de transición: un panel sólido que entra deslizándose y cubre toda
   la pantalla, cambia la sección de atrás (invisible en ese instante) y
   sale deslizándose por el lado contrario, revelándola. La dirección la
   fija JS (translateX inline) según se avance o se retroceda — así se
   siente direccional, no un simple fundido. Por encima de todo (incluso
   del remate/foto sueltos en .app-frame). */
.transition-veil {
  position: absolute;
  inset: 0;
  z-index: 80;
  background: var(--color-bg);
  transform: translateX(100%);
  pointer-events: none;
  /* Corre en TODAS las transiciones entre secciones mayores — con capa
     propia de entrada, nunca se arma tarde justo cuando más se nota. */
  will-change: transform;
}
.transition-veil.is-animating { transition: transform var(--veil-dur, 460ms) var(--ease-in-out-quart); }

.panel-inner { display: flex; flex-direction: column; gap: var(--space-sm); min-width: 0; }

.content-wrap {
  width: 100%;
  max-width: 72ch;
  margin-inline: auto;
}

/* =========================================================
   5. COMPONENTES
   ========================================================= */

/* Portada — fondo de mesa */
.panel--inicio {
  background-image: url("../assets/images/fondo-0.jpg");
  background-size: cover;
  background-position: center;
}

/* Degradado solo abajo (donde vive portada-outro) para que el texto se
   lea bien contra el fondo de mesa — z-index bajo, por debajo del sobre
   (z-index:5), así nunca lo tapa ni lo oscurece. */
.portada-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to bottom, rgba(8, 6, 4, 0) 55%, rgba(8, 6, 4, 0.55) 82%, rgba(8, 6, 4, 0.75) 100%);
}

/* Ornamento — línea / corazón / línea, arriba y abajo de la Portada */
.ornament {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  color: var(--color-fg);
}
.ornament-line { width: clamp(2rem, 8vw, 3.5rem); height: 1px; background: currentColor; opacity: 0.85; }
.ornament-heart { font-size: 0.95rem; line-height: 1; color: var(--color-fg); }

/* Bloque inferior de la Portada: agradecimiento + hint de tap + ornamento */
.portada-outro {
  position: absolute;
  bottom: clamp(1.5rem, 6%, 3rem);
  left: 50%;
  transform: translateX(-50%);
  z-index: 2; /* por encima de .portada-scrim (z-index:1), si no el degradado lo tapaba */
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.9rem;
  padding-inline: var(--space-md);
  text-align: center;
}
.portada-thanks {
  font-family: "Alex Brush", cursive;
  font-size: clamp(1.85rem, 7vw, 2.4rem);
  letter-spacing: 0.02em;
  line-height: 1;
  color: var(--color-fg);
}
.tap-hint {
  font-family: "Times New Roman", Times, serif;
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-fg);
  animation: hint-pulse 2.4s var(--ease-in-out-quart) infinite;
  transition: opacity var(--dur-base) var(--ease-out-expo);
}
.panel--inicio.is-open .tap-hint { opacity: 0; animation: none; }
/* Reaparece una vez que el sobre y las tarjetas terminaron de salir del
   todo — es la señal de que hay un segundo tap disponible (zoom a la foto). */
.panel--inicio.is-revealed .tap-hint { opacity: 1; animation: hint-pulse 2.4s var(--ease-in-out-quart) infinite; }
@keyframes hint-pulse {
  0%, 100% { opacity: 0.5; }
  50% { opacity: 1; }
}

/* Sobre — 3 capas del gráfico + 2 tarjetas, todas alineadas al mismo footprint.
   atras y delante comparten la proporción del sobre (443:322). La solapa gira
   en 3D como una bisagra real; las tarjetas suben desde adentro del bolsillo. */
.envelope {
  position: absolute;
  left: 50%;
  top: 56%;
  transform: translate(-50%, -50%);
  width: min(62%, 380px);
  aspect-ratio: 443 / 322;
  z-index: 5;
  padding: 0;
  perspective: 1000px;
  cursor: pointer;
}

.envelope-layer { position: absolute; width: 100%; pointer-events: none; }
.envelope-atras, .envelope-delante { inset: 0; height: 100%; }
.envelope-atras { z-index: 1; }
.envelope-delante { z-index: 3; } /* por delante de las tarjetas: las oculta donde el bolsillo es opaco */
.envelope-solapa {
  top: 0;
  left: 0;
  height: auto;
  transform-origin: top center;
  transform: rotateX(0deg);
  transition: transform var(--dur-envelope) var(--ease-in-out-quart),
              z-index 0s var(--dur-envelope); /* pasa detrás recién cuando el giro terminó del todo */
  z-index: 4; /* cerrada: la capa más alta, tapa todo hasta la puntita */
  will-change: transform;
}
/* Se abre y queda abierta, apoyada hacia atrás — asoma como triángulo
   detrás del sobre (no desaparece), pero pasa detrás del bolsillo y las
   tarjetas para no taparlas mientras suben. */
.panel--inicio.is-open .envelope-solapa {
  transform: rotateX(-150deg);
  z-index: 0;
}

/* Tarjetas: en reposo quedan centradas y ocultas detrás del sobre (opacity:0
   como resguardo). Van por detrás del bolsillo delantero (envelope-delante)
   — así la parte opaca del bolsillo tapa la mitad inferior y se ve que salen
   realmente de adentro del sobre. */
.invitation-card {
  position: absolute;
  top: 50%;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  transition: transform var(--dur-card-reveal) var(--ease-out-back),
              opacity var(--dur-slow) linear;
  will-change: transform, opacity;
}
/* Ancla horizontal propia por tarjeta (en vez de traslaciones enormes desde
   un mismo centro) para que no se tapen entre sí — cada una centrada en su
   propio punto, separadas, como en la referencia. */
.card-photo {
  left: 66%;
  width: 50%;
  /* Proporción fija: para que encaje siempre igual dentro del dibujo del
     sobre, sin importar la pantalla (ver js/script.js para cómo el zoom
     a pantalla completa maneja el ajuste sin cortes ni saltos). */
  aspect-ratio: 853 / 1280;
  object-fit: cover;
  border-radius: 2px;
  box-shadow: var(--shadow-card);
  transform: translate(-50%, -50%) rotate(6deg) scale(0.92);
}
.card-text {
  top: 40%;
  left: 34%;
  width: 50%;
  aspect-ratio: 853 / 1280; /* mismo tamaño que card-photo */
  background: #f6f1e7;
  color: #000;
  border-radius: 2px;
  /* marco dorado: línea fina separada del borde, como un marquito de
     invitación (sin markup extra) */
  box-shadow: var(--shadow-card),
              inset 0 0 0 6px #f6f1e7,
              inset 0 0 0 7px var(--color-accent);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.3em;
  padding: 8% 10%;
  overflow: hidden; /* nada de contenido escapando del recuadro de la tarjeta */
  transform: translate(-50%, -50%) rotate(-7deg) scale(0.92);
}
.card-text-icon {
  width: clamp(1rem, 3.6vw, 1.3rem);
  height: auto;
  color: #000;
  flex-shrink: 0;
}
.card-text-label,
.card-text-guests {
  font-size: clamp(0.52rem, 1.7vw, 0.68rem);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #000;
}
.card-text-divider {
  width: 1.2rem;
  height: 1px;
  background: #000;
  flex-shrink: 0;
}
.card-text-name {
  font-family: "Playfair Display", Georgia, "Times New Roman", serif;
  font-size: clamp(0.84rem, 3.1vw, 1.15rem);
  font-weight: 400;
  letter-spacing: 0.01em;
  line-height: 1.25;
}

/* Mientras se trae el nombre/pases reales del invitado (link /i/TOKEN),
   estos 3 datos quedan invisibles en vez de mostrar el texto genérico un
   instante — evita el parpadeo "Familia Invitada" -> nombre real. */
#card-text-grupo.is-cargando,
.card-text-name.is-cargando,
.card-text-guests.is-cargando { opacity: 0; }
.card-text-name,
.card-text-guests,
#card-text-grupo { transition: opacity var(--dur-base) var(--ease-out-expo); }
.card-text-heart { color: #000; font-size: 0.65rem; }

/* Las tarjetas esperan a que la solapa termine de abrirse del todo
   (var(--dur-envelope)) y recién ahí empiezan a salir, una apenas
   después de la otra. */
.panel--inicio.is-open .card-photo {
  transform: translate(-50%, -100%) rotate(6deg) scale(1);
  opacity: 1;
  transition-delay: var(--dur-envelope);
}
.panel--inicio.is-open .card-text {
  transform: translate(-50%, -100%) rotate(-7deg) scale(1);
  opacity: 1;
  transition-delay: calc(var(--dur-envelope) + 120ms);
}

/* Segundo tap, paso 1 de 2: antes de hacer zoom, la foto primero se
   levanta más (por encima del sobre Y de la tarjeta de texto — z-index
   más alto que la solapa y el bolsillo) y se queda ahí un instante.
   Recién cuando termina esta subida arranca el zoom de verdad (ver
   zoomPhotoToFullscreen en js/script.js). */
.panel--inicio.is-open .card-photo.is-lifting {
  transform: translate(-50%, -205%) rotate(0deg) scale(1.08);
  /* Sin z-index propio: se queda en el natural (2, igual que card-text)
     para no pasarle nunca por delante — el orden del DOM (card-photo
     antes que card-text) ya la deja detrás en los empates. */
  /* ease-out-expo, no ease-out-back: llega fina, sin rebote, y se queda
     quieta ahí — recién de esa posición final arranca el zoom. */
  transition: transform 1500ms var(--ease-out-expo);
}

/* Segundo tap, paso 2: la foto hace zoom
   y pasa a ocupar toda la pantalla. JS la reubica como hija directa de
   .app-frame (para poder ocupar el 100% real del frame) y anima con FLIP:
   arranca "invertida" (misma posición/tamaño visual de antes) y transiciona
   a transform:none, que es cuando queda llenando el frame de verdad. */
.card-photo.is-fullscreen {
  position: absolute;
  inset: 0;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
  border-radius: 0;
  box-shadow: none;
  /* Arranca al mismo nivel que la Portada (z-index:2, ver .panel.is-active)
     — no de una al frente de todo — para que si todavía se superpone
     visualmente con el sobre en el primer instante, quede tapada por él
     como una tarjeta real. Recién sube a z-index:50 (.is-risen, puesto
     por JS con un pequeño delay) una vez que ya despegó lo suficiente. */
  z-index: 2;
  /* Al reubicarla fuera de .panel--inicio pierde ".is-open .card-photo"
     (esa regla ya no aplica: dejó de ser descendiente), así que hay que
     re-declarar opacity acá — si no, vuelve a caer en el opacity:0 base. */
  opacity: 1;
  transform: none;
  /* border-radius y box-shadow también animados: si no, desaparecen de
     golpe apenas arranca el zoom (antes solo transform estaba en la
     lista), y ese salto se notaba más que cualquier tema de proporción. */
  transition: transform 2200ms var(--ease-in-out-quart),
              border-radius 500ms var(--ease-out-expo),
              box-shadow 500ms var(--ease-out-expo);
}
.card-photo.is-risen { z-index: 50; }
/* Al avanzar a la Galería, la foto de fondo y el remate (que viven sueltos
   en .app-frame, por encima de .sections) se desvanecen — si no, tapan la
   Galería aunque ya no sea la sección activa. */
.card-photo.is-fullscreen.is-leaving {
  opacity: 0;
  transition: opacity 900ms var(--ease-in-out-quart), transform 1150ms var(--ease-in-out-quart);
}
/* El resto de la escena ya NO se desvanece — se queda como está y la
   foto, al hacer zoom, la va tapando ella sola (por delante, más grande)
   en vez de que se note un fundido aparte. Solo se desactivan los clicks
   para que no se puedan re-disparar sobre lo que quedó tapado. */
.panel--inicio.is-zoomed .envelope-atras,
.panel--inicio.is-zoomed .envelope-delante,
.panel--inicio.is-zoomed .envelope-solapa,
.panel--inicio.is-zoomed .card-text,
.panel--inicio.is-zoomed .portada-scrim,
.panel--inicio.is-zoomed .portada-outro {
  pointer-events: none;
}
/* Una vez que el zoom termina del todo (.is-covered, puesta por JS con
   delay), se ocultan de golpe — a esa altura la foto ya los tapa
   enteros, así que no se nota. Evita que se expongan de nuevo (en vez
   de verse negro tapado por .app-frame) cuando la foto se corre en la
   transición hacia la siguiente sección. */
.panel--inicio.is-covered .envelope-atras,
.panel--inicio.is-covered .envelope-delante,
.panel--inicio.is-covered .envelope-solapa,
.panel--inicio.is-covered .card-text,
.panel--inicio.is-covered .portada-scrim,
.panel--inicio.is-covered .portada-outro {
  opacity: 0;
}

/* Remate final: cortina + texto, aparece solo después del zoom a la foto.
   Vive directo en .app-frame (mismo nivel que la foto ya reubicada), por
   encima de todo. Puramente decorativo: no bloquea gestos/nav. */
.finale {
  position: absolute;
  inset: 0;
  z-index: 60;
  pointer-events: none;
  overflow: hidden;
  opacity: 1;
  transition: opacity 900ms var(--ease-in-out-quart);
}
.finale.is-leaving { opacity: 0; }
.finale-curtain {
  position: absolute;
  inset: 0;
  /* Más transparente arriba (donde está la foto/las caras), más oscuro
     abajo (donde vive el texto) para que se lea sin tapar la imagen. Ya
     no "cae" como cortina — el degradado está siempre presente y solo
     aparece con un fundido, junto con el resto del remate. */
  background: linear-gradient(
    to bottom,
    rgba(8, 8, 6, 0.1) 0%,
    rgba(8, 8, 6, 0.25) 40%,
    rgba(8, 8, 6, 0.55) 65%,
    rgba(8, 8, 6, 0.82) 100%
  );
  opacity: 0;
  transition: opacity 900ms var(--ease-in-out-quart);
}
.finale.is-active .finale-curtain { opacity: 1; }

.finale-content {
  position: relative;
  z-index: 1;
  height: 100%;
  width: 100%;
  text-align: center;
  color: var(--color-fg);
  font-family: "Playfair Display", Georgia, serif;
}

/* Regla del proyecto: el texto nunca tapa las caras de la foto. Todo el
   contenido se ancla abajo, lejos de donde suelen estar las caras en una
   foto vertical de pareja. */
.finale-lower {
  position: absolute;
  bottom: clamp(1.5rem, 5%, 3rem);
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(0.5rem, 1.8vh, 0.9rem);
  padding-inline: var(--space-md);
}

/* Ornamentos del remate: mismos componentes que la Portada (línea/corazón
   ya vienen en blanco). Quedan con el mismo flotado sutil y permanente
   que las figuritas de Padres (@keyframes padres-idle-float, más abajo),
   arrancando recién cuando cada uno ya es visible. */
.finale-content .ornament {
  opacity: 0;
  transition: opacity 700ms var(--ease-out-expo);
  animation: padres-idle-float 3.6s ease-in-out infinite;
}
.finale.is-active .ornament--finale-mid { opacity: 1; transition-delay: 1700ms; animation-delay: 1700ms; }
.finale.is-active .ornament--finale-bottom { opacity: 1; transition-delay: 2600ms; animation-delay: 2600ms; }

.finale-eyebrow {
  font-size: var(--fs-sobretitulo);
  letter-spacing: 0.35em;
  text-transform: uppercase;
  padding-left: 0.35em; /* compensa el tracking para que no quede descentrado */
}
.finale-names {
  font-family: "New Icon Script", cursive;
  font-weight: 400;
  /* Bajado de 10vw/3.4rem: a ese tamaño "Erlan & Mari" no entraba en el
     ancho real del frame (nowrap) y el sobrante lo recortaba .finale,
     que tiene overflow:hidden — se veía la E y la M cortadas. */
  font-size: clamp(1.7rem, 8.5vw, 2.9rem);
  line-height: 1.5;
  white-space: nowrap;
  max-width: 100%;
  /* Latido bien lento y sutil, una vez que ya terminó de "escribirse"
     (arranca justo cuando termina el reveal-wipe: 1900ms + 1100ms). */
  animation: finale-heartbeat 4.6s ease-in-out infinite;
  animation-delay: 1900ms;
  transform-origin: center;
}
@keyframes finale-heartbeat {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.018); }
}
.finale-names .reveal-wipe {
  display: inline-block;
  box-sizing: border-box;
  max-width: 100%;
  /* Aire real a los costados (no solo en el clip-path) para las
     florituras de la fuente, sin que el cajón se pase del ancho
     disponible — ver el comentario del clip-path un poco más abajo. */
  padding-inline: 0.3em;
}
.finale-amp { color: var(--color-accent); font-size: 0.85em; padding-inline: 0.1em; }

/* Revelado tipo "escritura": arranca recortado y se va destapando de
   izquierda a derecha con pasos discretos (como si se tipeara) — solo
   en los nombres. El resto del bloque usa la entrada deslizada de abajo. */
/* El inset izquierdo queda en -15% (en vez de 0) a propósito: esta
   tipografía script tiene trazos/florituras que sobresalen a la
   izquierda del cajón del glifo (bearing negativo) — con inset 0 el
   cajón del clip-path recortaba esa parte y se veía la E de "Erlan" y
   la M de "Mari" cortadas. */
.reveal-wipe {
  display: inline-block;
  clip-path: inset(0 100% 0 -15%);
  transition: clip-path 900ms steps(14, end);
  /* clip-path animado sin esto lo repinta el CPU cuadro a cuadro (se
     traba en celulares de gama baja) — con la capa propia lo resuelve
     la GPU, igual que .panel arriba. */
  will-change: clip-path;
}
.finale.is-active .finale-names .reveal-wipe {
  clip-path: inset(0 0 0 -15%);
  transition: clip-path 850ms steps(16, end);
  transition-delay: 1050ms;
}

/* Resto del bloque (eyebrow, fecha, "Faltan", countdown): entran
   deslizando desde los costados, alternando lado, en vez de fundido
   plano — da sensación de intercalado sin repetir siempre el mismo lado. */
.finale-slide-in {
  opacity: 0;
  transition: transform 800ms var(--ease-out-back), opacity 700ms var(--ease-out-expo);
  will-change: transform, opacity;
}
.finale-slide-in--left { transform: translateX(-1.6em); }
.finale-slide-in--right { transform: translateX(1.6em); }
.finale.is-active .finale-slide-in { opacity: 1; transform: translateX(0); }

.finale-eyebrow.finale-slide-in { transition-delay: 700ms; }
.finale-date.finale-slide-in { transition-delay: 1850ms; }
.finale-faltan.finale-slide-in { transition-delay: 2100ms; }
.countdown.finale-slide-in { transition-delay: 2250ms; }

.finale-date {
  font-size: clamp(0.92rem, 3.6vw, 1.2rem);
  letter-spacing: 0.25em;
  text-transform: uppercase;
}

.finale-faltan {
  display: flex;
  align-items: center;
  gap: 0.7rem;
}
.finale-faltan-line { width: clamp(1.5rem, 6vw, 2.5rem); height: 1px; background: var(--color-accent); opacity: 0.8; }
.finale-faltan-label {
  font-size: 0.8rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--color-accent);
}

.countdown {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(0.4rem, 2vw, 1rem);
  margin-top: 0.15rem;
  width: 100%;
}
.countdown-item { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; min-width: 0; }
.countdown-value {
  font-size: clamp(1.43rem, 6.6vw, 2.4rem);
  line-height: 1;
  font-variant-numeric: tabular-nums; /* no tiembla el ancho al cambiar los segundos */
}
.countdown-label {
  font-size: clamp(0.53rem, 1.8vw, 0.68rem);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-accent);
  white-space: nowrap;
}
.countdown-sep { width: 1px; align-self: stretch; margin-block: 0.2rem; background: var(--color-accent); opacity: 0.6; }

/* Ubicaciones — reemplaza a la Galería (mismo lugar en el orden de
   navegación). Foto de fondo + degradado, dos tarjetas edge-to-edge
   (sin margen lateral) con foto + info, mismo patrón que las demás
   secciones "foto + contenido centrado". */
.panel--ubicaciones { padding: 0; overflow: hidden; }
.ubicaciones-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  z-index: 1;
}
.ubicaciones-scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: rgba(6, 5, 4, 0.78);
}
.ubicaciones-content {
  position: relative;
  z-index: 3;
  height: 100%;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(0.6rem, 2vh, 1rem);
  padding: var(--space-md) 0; /* sin margen lateral: las tarjetas llegan al borde */
  text-align: center;
  color: var(--color-fg);
  font-family: "Playfair Display", Georgia, serif;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6), 0 3px 12px rgba(0, 0, 0, 0.45);
  /* Antes tenía overflow-y:auto como escape por si el contenido no
     entraba — pero en una "app" con touch-action:none en .app-frame eso
     terminaba mostrando una barra de scroll real en algunos
     navegadores/teléfonos (Samsung Internet, Brave, Opera con doble
     barra), justo lo que nunca debe pasar acá. Con los breakpoints de
     pantalla baja de abajo el contenido entra siempre con margen de
     sobra (medido: nunca menos de ~50px libres en anchos de 320 a
     412px y altos de 560 a 780px) — si algún caso extremo no
     contemplado igual se pasara, .panel--ubicaciones ya tiene
     overflow:hidden y lo recorta en vez de mostrar scroll, que es el
     mismo comportamiento que el resto de las secciones del sitio. */
}
.ubicaciones-content > :not(.ubicaciones-list) { padding-inline: var(--space-md); }

/* Mismo tratamiento que los demás títulos de sección (New Icon Script). */
.ubicaciones-title {
  font-family: "New Icon Script", cursive;
  font-weight: 400;
  font-size: var(--fs-titulo);
  line-height: 1.30;
}

.ubicaciones-list {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  width: 100%;
  padding-inline: var(--space-sm); /* aire real, ya no pegado al borde */
  margin-top: 0.3rem;
}
.ubicaciones-card {
  display: flex;
  align-items: stretch;
  gap: 0;
  min-height: 7.5rem;
  border: 1px solid var(--color-accent);
  border-radius: 1.4rem;
  overflow: hidden;
  background: rgba(8, 6, 4, 0.35);
}
.ubicaciones-card-photo {
  width: 38%;
  flex-shrink: 0;
  object-fit: cover;
  /* Difuminado suave en los bordes de la foto (viñeta), como en la
     referencia — no un recorte duro contra el resto de la tarjeta. */
  -webkit-mask-image: radial-gradient(ellipse at center, black 58%, transparent 100%);
  mask-image: radial-gradient(ellipse at center, black 58%, transparent 100%);
}
.ubicaciones-card-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  padding: 0.7rem 0.6rem;
}
.ubicaciones-card-icon {
  width: 1.5rem;
  height: 1.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
}
.ubicaciones-card-icon img { width: 100%; height: 100%; object-fit: contain; }
.ubicaciones-card-label {
  font-size: var(--fs-subtitulo);
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.ubicaciones-card-divider {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  margin-block: 0.1rem;
}
.ubicaciones-card-divider .ornament-line { width: 1.3rem; height: 1px; background: currentColor; opacity: 0.85; }
.ubicaciones-card-divider .ornament-heart { font-size: 0.6rem; }
.ubicaciones-card-place {
  font-family: "Playfair Display", Georgia, serif;
  font-size: var(--fs-texto);
  line-height: 1.4;
}
.ubicaciones-card-place em { color: var(--color-muted); font-style: italic; }
.ubicaciones-card-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  margin-top: 0.15rem;
  padding: 0.75em 0.9em;
  border: 1px solid var(--color-accent);
  border-radius: 999px;
  font-size: clamp(0.66rem, 2.2vw, 0.75rem);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--color-fg);
  transition: background var(--dur-base) var(--ease-out-expo), color var(--dur-base) var(--ease-out-expo);
}
.ubicaciones-card-btn svg { width: 0.75rem; height: 0.75rem; flex-shrink: 0; color: var(--color-accent); }
.ubicaciones-card-btn:hover,
.ubicaciones-card-btn:focus-visible { background: var(--color-accent); color: var(--color-bg); }
.ubicaciones-card-btn:hover svg,
.ubicaciones-card-btn:focus-visible svg { color: var(--color-bg); }

/* Pantallas bajas (mismo caso que Padres: doble barra de navegador —
   Samsung Internet, Brave, Opera— comiéndose alto disponible; medido el
   desborde real a partir de ~600px de alto): esta sección es la más
   pesada del sitio en contenido (dos tarjetas completas, cada una con
   foto + 4 líneas de texto + botón), y el mínimo de alto de las
   tarjetas (7.5rem cada una) más el título grande no se achican solos
   con la altura. Al no entrar, .ubicaciones-content dependía de su
   overflow-y:auto para no recortar nada — pero en una "app" con
   touch-action:none en el frame, eso terminaba mostrando una barra de
   scroll real en algunos navegadores/teléfonos en vez de simplemente
   verse completo, que es lo que se busca siempre en este sitio (ver
   .app-frame). Se achica todo lo necesario para que jamás haga falta
   ese escape. */
@media (max-height: 700px) {
  .ubicaciones-content { padding-block: var(--space-sm); gap: clamp(0.4rem, 1.6vh, 0.8rem); }
  .ubicaciones-title { font-size: clamp(1.9rem, 9vw, 2.8rem); }
  .ubicaciones-card { min-height: 6.4rem; }
  .ubicaciones-card-info { padding: 0.5rem 0.55rem; gap: 0.2rem; }
  .ubicaciones-card-btn { padding: 0.6em 0.85em; margin-top: 0.05rem; }
}
@media (max-height: 600px) {
  .ubicaciones-content { padding-block: clamp(0.6rem, 3vh, 1rem); gap: clamp(0.25rem, 1.2vh, 0.55rem); }
  .ubicaciones-title { font-size: clamp(1.6rem, 8vw, 2.3rem); }
  .dresscode-intro { display: none; } /* la frase de apoyo es lo primero que se puede sacrificar sin perder información */
  .ubicaciones-card { min-height: 5.6rem; }
  .ubicaciones-card-icon { width: 1.2rem; height: 1.2rem; }
  .ubicaciones-card-label { letter-spacing: 0.12em; }
  .ubicaciones-card-divider { margin-block: 0.02rem; }
  .ubicaciones-card-place { line-height: 1.25; }
  .ubicaciones-card-btn { padding: 0.5em 0.75em; }
}

/* Entrada de Ubicaciones: scoped a .ubicaciones-anim. Técnicas nuevas —
   cortina vertical (clip-path de arriba abajo, no de lado como los demás
   "wipe") para el título, y tarjetas con inclinación 3D (rotateY) que
   entran desde afuera por lados opuestos. */
.ubic-in { opacity: 0; }
.ubicaciones-anim.is-active .ubic-in { opacity: 1; }

.ubic-in--fade { transition: opacity 750ms var(--ease-out-expo); }

.ubic-in--curtain {
  clip-path: inset(0 0 100% 0);
  transition: opacity 300ms var(--ease-out-expo), clip-path 900ms var(--ease-in-out-quart);
  will-change: clip-path, opacity;
}
.ubicaciones-anim.is-active .ubic-in--curtain { clip-path: inset(0 0 0% 0); }

.ubic-in--rise {
  transform: translateY(1em);
  transition: opacity 700ms var(--ease-out-expo), transform 800ms var(--ease-out-expo);
  will-change: transform, opacity;
}
.ubicaciones-anim.is-active .ubic-in--rise { transform: translateY(0); }

.ubic-in--tilt-left,
.ubic-in--tilt-right {
  transition: opacity 700ms var(--ease-out-expo), transform 900ms var(--ease-out-back);
  /* Son tarjetas con foto adentro girando en 3D: sin esto el navegador
     las re-dibuja por software en cada cuadro y el giro entra a los
     saltos. Con la capa propia (y sin pintar la cara de atrás) el giro
     lo resuelve la GPU. Son solo 2 elementos, así que el costo de
     memoria es despreciable. */
  will-change: transform, opacity;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.ubic-in--tilt-left { transform: perspective(900px) rotateY(-35deg) translateX(-2.5em); transform-origin: left center; }
.ubic-in--tilt-right { transform: perspective(900px) rotateY(35deg) translateX(2.5em); transform-origin: right center; }
.ubicaciones-anim.is-active .ubic-in--tilt-left,
.ubicaciones-anim.is-active .ubic-in--tilt-right { transform: perspective(900px) rotateY(0) translateX(0); }

.ornament--itin-top.ubic-in--fade { transition-delay: 200ms; }
.ubicaciones-title.ubic-in--curtain { transition-delay: 400ms; }
.dresscode-intro.ubic-in--rise { transition-delay: 950ms; }
.ubic-in--tilt-left { transition-delay: 1400ms; }
.ubic-in--tilt-right { transition-delay: 1550ms; }
.ornament--finale-mid.ubic-in--fade { transition-delay: 1900ms; }


/* Itinerario — foto a la derecha, degradado hacia la izquierda, línea de
   tiempo con scroll propio (la sección en sí sigue sin scrollear). */
.panel--itinerario { padding: 0; align-items: stretch; justify-content: flex-start; overflow: hidden; }
.itinerario-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 25% center;
  z-index: 1;
}
.itinerario-scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  /* Franja oscura pegada al borde donde vive el texto — deja el resto de
     la foto (donde está la pareja) despejado. Llega a 0 recién en 70%.
     Pasos parejos (misma distancia entre cada uno) en vez de la caída
     brusca que tenía antes entre el 20% y el 55%. */
  background: linear-gradient(
    to right,
    rgba(8, 6, 4, 0.96) 0%,
    rgba(8, 6, 4, 0.8) 14%,
    rgba(8, 6, 4, 0.62) 28%,
    rgba(8, 6, 4, 0.44) 42%,
    rgba(8, 6, 4, 0.24) 56%,
    rgba(8, 6, 4, 0) 70%
  );
}

.itinerario-content {
  position: relative;
  z-index: 3;
  height: 100%;
  width: 100%;
  max-width: 58%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--space-md) var(--space-sm) var(--space-sm) var(--space-md);
  color: var(--color-fg);
  font-family: "Playfair Display", Georgia, serif;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6), 0 3px 12px rgba(0, 0, 0, 0.45);
}

.ornament--itin-top,
.ornament--itin-mid { flex-shrink: 0; }
.itinerario-title {
  flex-shrink: 0;
  font-family: "New Icon Script", cursive;
  font-weight: 400;
  /* Sin mayúsculas: una tipografía script en uppercase pierde sus trazos
     unidos y se vuelve casi ilegible. */
  letter-spacing: 0.01em;
  font-size: var(--fs-titulo);
  text-align: center;
  white-space: nowrap;
  margin-block: 0.3rem;
}

/* Compacta a propósito para que las 6 entren sin scroll en la mayoría de
   celulares; overflow-y queda como resguardo para pantallas muy bajas. */
.itinerario-timeline {
  flex: 0 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-y; /* el appFrame bloquea touch-action, esta lista necesita su propio scroll */
  margin-top: 0.4rem;
  /* Oculta la barra de scroll (el gesto sigue funcionando igual). */
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* Edge/IE viejos */
}
.itinerario-timeline::-webkit-scrollbar { display: none; } /* Chrome/Safari */

.itinerario-item { display: flex; align-items: stretch; gap: 0.6rem; }
.itinerario-icon-col { display: flex; flex-direction: column; align-items: center; width: 2.9rem; flex-shrink: 0; }
.itinerario-icon {
  width: 2.9rem;
  height: 2.9rem;
  border-radius: 50%;
  border: 1.3px solid var(--color-accent);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  background: rgba(8, 6, 4, 0.55);
}
.itinerario-icon img { width: 70%; height: 70%; object-fit: contain; }
.itinerario-connector {
  flex: 1;
  width: 0;
  min-height: 0.9rem;
  border-left: 2px dotted var(--color-accent);
  opacity: 0.5;
  margin-block: 0.15rem;
}

.itinerario-text { flex: 1; min-width: 0; padding-bottom: 0.65rem; }
.itinerario-label {
  font-size: clamp(0.78rem, 2.9vw, 0.96rem);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 600;
  margin-bottom: 0.1rem;
}
.itinerario-time {
  font-family: "Playfair Display", Georgia, serif;
  font-size: clamp(1.28rem, 5.6vw, 1.64rem);
  line-height: 1;
  margin-bottom: 0.2rem;
}

/* Dress Code — foto de fondo completa con degradado parejo (el texto corre
   por todo el largo, a diferencia del Itinerario que solo lo tiene a un
   costado) y contenido centrado encima. */
.panel--dresscode { padding: 0; overflow: hidden; }
.dresscode-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  z-index: 1;
}
.dresscode-scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: linear-gradient(
    to bottom,
    rgba(8, 6, 4, 0.8) 0%,
    rgba(8, 6, 4, 0.62) 22%,
    rgba(8, 6, 4, 0.64) 78%,
    rgba(8, 6, 4, 0.82) 100%
  );
}

.dresscode-content {
  position: relative;
  z-index: 3;
  height: 100%;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(0.9rem, 3vh, 1.5rem);
  padding: var(--space-md) var(--space-md) var(--space-lg);
  text-align: center;
  color: var(--color-fg);
  font-family: "Playfair Display", Georgia, serif;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6), 0 3px 12px rgba(0, 0, 0, 0.45);
}

/* Ornamentos e íconos vienen en un tono pálido en el PNG original, así que
   los recoloreamos a dorado EXACTO vía mask-image (más preciso que
   aproximar con filter): el PNG solo aporta la forma/máscara, el color
   real lo pinta background-color. */
.dresscode-ornament-img {
  display: block;
  width: clamp(11rem, 55vw, 15rem);
  background-color: var(--color-accent);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-position: center;
  mask-position: center;
}
.dresscode-ornament-img--divider {
  aspect-ratio: 276 / 29;
  width: clamp(7.5rem, 38vw, 10rem);
  -webkit-mask-image: url("../assets/images/dresscode/ornamento-arriba.png");
  mask-image: url("../assets/images/dresscode/ornamento-arriba.png");
}
.dresscode-ornament-img--mid {
  aspect-ratio: 252 / 30;
  -webkit-mask-image: url("../assets/images/dresscode/corazon-ramas-horizontal.png");
  mask-image: url("../assets/images/dresscode/corazon-ramas-horizontal.png");
}
/* Más chica que el resto de los ornamentos, sobre "Gracias por ser parte...". */
.dresscode-ornament-img--mid-small { width: clamp(6rem, 26vw, 7.5rem); }
.dresscode-ornament-img--bottom {
  aspect-ratio: 370 / 32;
  -webkit-mask-image: url("../assets/images/dresscode/ornamento-abajo.png");
  mask-image: url("../assets/images/dresscode/ornamento-abajo.png");
}

.dresscode-eyebrow {
  font-size: var(--fs-sobretitulo);
  letter-spacing: 0.5em;
  text-transform: uppercase;
  padding-left: 0.5em; /* compensa el tracking */
}
.dresscode-title {
  font-family: "New Icon Script", cursive;
  font-weight: 400;
  font-size: var(--fs-titulo);
  line-height: 1.1;
}

.dresscode-intro {
  font-size: var(--fs-texto);
  line-height: 1.6;
  max-width: 32ch;
}

.dresscode-row {
  display: flex;
  align-items: center;
  gap: clamp(0.6rem, 3vw, 1.1rem);
  width: 100%;
  text-align: left;
}
.dresscode-icon-img {
  flex-shrink: 0;
  width: clamp(3.4rem, 13vw, 4.2rem);
  aspect-ratio: 1 / 1;
  background-color: var(--color-accent);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-position: center;
  mask-position: center;
}
.dresscode-icon-img--vestido {
  -webkit-mask-image: url("../assets/images/dresscode/vestido.png");
  mask-image: url("../assets/images/dresscode/vestido.png");
}
.dresscode-icon-img--traje {
  -webkit-mask-image: url("../assets/images/dresscode/traje.png");
  mask-image: url("../assets/images/dresscode/traje.png");
}

.dresscode-divider-v-img {
  flex-shrink: 0;
  align-self: stretch;
  width: 0.65rem;
  background-color: var(--color-accent);
  opacity: 0.85;
  -webkit-mask-image: url("../assets/images/dresscode/corazon-lineas-vertical.png");
  mask-image: url("../assets/images/dresscode/corazon-lineas-vertical.png");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-position: center;
  mask-position: center;
}

.dresscode-text { flex: 1; min-width: 0; }
.dresscode-heading {
  font-size: var(--fs-subtitulo);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 700;
  margin-bottom: 0.3rem;
}
.dresscode-desc {
  font-size: var(--fs-texto);
  line-height: 1.5;
  color: var(--color-fg);
  opacity: 0.9;
}

/* Figuritas del Dress Code (íconos, divisores, ornamentos) con el mismo
   flotado sutil y permanente que Padres/el remate. */
/* Corre infinito y a la vez en varios elementos por sección — sin
   will-change el navegador puede repintarlo por CPU en vez de dejarlo
   en su propia capa GPU, y eso se nota como un tironeo de fondo
   constante (más notorio junto con otras animaciones grandes, como en
   Padres o Recordatorio). */
.dresscode-idle-float { animation: padres-idle-float 3.6s ease-in-out infinite; will-change: transform; }
/* Son MUCHOS elementos (íconos, ornamentos, divisores) repetidos en cada
   sección, y el panel nunca se saca del DOM (solo se le baja la
   opacidad) — sin esto, los de TODAS las secciones ya visitadas siguen
   animando y con su capa GPU viva de fondo todo el tiempo, aunque no se
   vean. Eso compite por GPU con la transición que sí se está viendo en
   ese momento, y se nota como lentitud que empeora cuanto más se avanzó
   en el sitio. Pausado, el navegador libera esa capa sola. */
.panel:not(.is-active) .dresscode-idle-float { animation-play-state: paused; }
/* Cada una arranca en un punto distinto del ciclo para que no floten
   todas exactamente sincronizadas. */
.dresscode-icon-img--vestido.dresscode-idle-float { animation-delay: -0.4s; }
.dresscode-icon-img--traje.dresscode-idle-float { animation-delay: -2s; }
.dresscode-ornament-img--bottom.dresscode-idle-float { animation-delay: -1.2s; }

/* Palpitación bien lenta y sutil, como la de "Erlan & Mari" en el remate. */
.dresscode-heartbeat {
  animation: finale-heartbeat 4.6s ease-in-out infinite;
  transform-origin: center;
}

.dresscode-closing {
  font-size: var(--fs-texto);
  line-height: 1.6;
  max-width: 30ch;
}

/* No Niños — mismo patrón que Dress Code (foto + degradado + ornamentos),
   solo con su propia foto y un corazón suelto en vez de fila de íconos. */
.panel--noninos { padding: 0; overflow: hidden; }
.noninos-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  z-index: 1;
}
.noninos-scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: linear-gradient(
    to bottom,
    rgba(8, 6, 4, 0.68) 0%,
    rgba(8, 6, 4, 0.48) 22%,
    rgba(8, 6, 4, 0.5) 78%,
    rgba(8, 6, 4, 0.7) 100%
  );
}
.noninos-content {
  position: relative;
  z-index: 3;
  height: 100%;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-lg) var(--space-md);
  text-align: center;
  color: var(--color-fg);
  font-family: "Playfair Display", Georgia, serif;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6), 0 3px 12px rgba(0, 0, 0, 0.45);
}
.noninos-top,
.noninos-bottom {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(0.7rem, 2.4vh, 1.15rem);
}

.noninos-ornament-mid { width: clamp(7.5rem, 38vw, 10rem); }

/* Mesa de Regalos tiene el doble de contenido que las demás secciones
   (dos bloques completos en vez de uno) — mismo tamaño de letra que el
   resto del sitio, pero con menos aire entre bloques para que siga
   entrando entero en pantalla sin scroll. */
.regalos-anim .noninos-content {
  justify-content: center;
  gap: clamp(0.8rem, 3vh, 1.5rem);
}
.regalos-anim .noninos-top { gap: clamp(0.3rem, 1.1vh, 0.55rem); }
.regalos-anim .noninos-ornament-mid { width: clamp(5rem, 26vw, 6.5rem); }
/* Cada forma de aportar (Mesa de Regalos / Lluvia de Sobres) agrupada en
   su propio bloque: gap más chico adentro que el de .noninos-top (se lee
   como una unidad), y de paso todo el contenido ocupa menos alto. */
.regalo-metodo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(0.2rem, 0.7vh, 0.32rem);
}

/* Íconos de Mesa de Regalos / Lluvia de Sobres (regalo.png, sobre_regalo.png) */
.regalo-icon {
  width: clamp(2rem, 7.5vw, 2.5rem);
  height: auto;
}

/* Subtítulo de cada forma de aportar ("Mesa de Regalos", "Lluvia de Sobres") */
.regalo-subheading {
  font-family: "Playfair Display", Georgia, serif;
  font-weight: 600;
  font-size: var(--fs-subtitulo);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.regalo-btn-flecha { width: 0.7rem; height: 0.7rem; margin-left: 0.15em; }

/* Modal del QR de aporte / catálogo de Mesa de Regalos */
.qr-modal-overlay {
  position: absolute;
  inset: 0;
  z-index: 60; /* por encima de las secciones, igual que .finale */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-md);
  background: rgba(8, 6, 4, 0.78);
}
.qr-modal-overlay[hidden] { display: none; }

.qr-modal {
  position: relative;
  width: 100%;
  max-width: 22rem;
  background: var(--color-bg);
  border: 1px solid var(--color-accent);
  border-radius: 4px;
  padding: var(--space-md);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  text-align: center;
  color: var(--color-fg);
}

.qr-modal-titulo {
  font-family: "Playfair Display", Georgia, serif;
  font-size: var(--fs-subtitulo);
  letter-spacing: 0.05em;
}

.qr-modal-img {
  width: 100%;
  max-width: 15rem;
  border-radius: 2px;
}

.qr-modal-nota {
  font-family: "Playfair Display", Georgia, serif;
  font-style: italic;
  font-size: var(--fs-texto);
  color: var(--color-muted);
}

.qr-modal-texto {
  font-family: "Playfair Display", Georgia, serif;
  font-size: var(--fs-texto);
  line-height: 1.5;
}

.qr-modal-cerrar {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  width: 2rem;
  height: 2rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: var(--color-fg);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
}

/* Entrada de Recordatorio: scoped a .recordatorio-anim para no afectar a
   Mesa de Regalos, que comparte las clases base .noninos-photo/scrim/
   content/top/bottom. Dispara con .is-active, así llega igual de bien
   por cualquier camino (el slide desde abajo de Dress Code, ver
   slideCoverToSixth en js/script.js, o el velo genérico). */
.recordatorio-anim {
  /* Reservado por si algún camino futuro necesita correr estos delays
     (hoy ninguno lo pisa, queda en 0 siempre). */
  --recordatorio-extra-delay: 0ms;
}
.recordatorio-anim .ornament--itin-top {
  opacity: 0;
  transition: opacity 800ms var(--ease-out-expo);
}
.recordatorio-anim.is-active .ornament--itin-top { opacity: 1; transition-delay: calc(900ms + var(--recordatorio-extra-delay)); }

.recordatorio-title-wipe { display: inline-block; }
.recordatorio-anim.is-active .recordatorio-title-wipe {
  clip-path: inset(0 0 0 -15%);
  transition: clip-path 1000ms steps(8, end);
  transition-delay: calc(1050ms + var(--recordatorio-extra-delay));
}

/* Las dos tarjetas (Solo Adultos / Número de Pases) entran alternando de
   lado a lado, en vez de aparecer las dos juntas. */
.recordatorio-cards { display: flex; flex-direction: column; gap: clamp(0.9rem, 3vh, 1.4rem); width: 100%; }
.recordatorio-row {
  opacity: 0;
  transition: opacity 800ms var(--ease-out-expo), transform 900ms var(--ease-out-expo);
  /* Ahora son tarjetas con borde/fondo (más pesadas de pintar que el
     texto suelto de antes) — con capa propia el slide entra fino. */
  will-change: transform, opacity;
}
.recordatorio-row--left { transform: translateX(-38vw); }
.recordatorio-row--right { transform: translateX(38vw); }
.recordatorio-anim.is-active .recordatorio-row { opacity: 1; transform: translateX(0); }
.recordatorio-anim.is-active .recordatorio-row:nth-of-type(1) { transition-delay: calc(1450ms + var(--recordatorio-extra-delay)); }
.recordatorio-anim.is-active .recordatorio-row:nth-of-type(2) { transition-delay: calc(1650ms + var(--recordatorio-extra-delay)); }

/* Ornamento de abajo: aparece bien lento, simple, sin entrar desde
   ningún lado — solo un fundido largo con leve subida. */
.recordatorio-bottom-in {
  opacity: 0;
  transform: translateY(1.4em);
  transition: opacity 1600ms var(--ease-out-expo), transform 1700ms var(--ease-out-expo);
  transition-delay: calc(2150ms + var(--recordatorio-extra-delay));
}
.recordatorio-anim.is-active .recordatorio-bottom-in { opacity: 1; transform: translateY(0); }

/* Tarjetas con marco fino, como el "marquito" de la referencia — mismo
   tratamiento de borde que ya usa el resto del sitio (color dorado). */
.recordatorio-card {
  width: 100%;
  border: 1px solid var(--color-accent);
  border-radius: 1.1rem;
  padding: clamp(1rem, 4vw, 1.4rem) clamp(1.1rem, 4.5vw, 1.6rem);
  background: rgba(8, 6, 4, 0.35);
}
.recordatorio-card-heading {
  font-family: "Playfair Display", Georgia, serif;
  font-weight: 700;
  font-size: var(--fs-subtitulo);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-align: center;
}
.recordatorio-card-divider {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  margin: 0.35rem auto 0.6rem;
}
.recordatorio-card-divider .ornament-line { width: clamp(1.4rem, 6vw, 2.2rem); }
.recordatorio-card-divider .ornament-heart { font-size: 0.7rem; }
.recordatorio-card-text {
  font-size: var(--fs-texto);
  line-height: 1.55;
  text-align: center;
}
/* .recordatorio-title hereda el tamaño de .dresscode-title (mismo por
   pedido explícito, "igual que No Niños"); solo se overridea el
   line-height. */
.recordatorio-title { line-height: 1.5; }
/* Más chico y pegado arriba del todo: que quede lejos de las caras de la
   foto sin importar el tamaño. */
/* Layout propio (no space-between como el resto): todo el bloque
   (título + tarjetas + ornamento) centrado como grupo, con el título
   pegado a las tarjetas en vez de repartirse con espacio parejo. */
.recordatorio-anim .noninos-content {
  justify-content: center;
  gap: clamp(0.6rem, 2vh, 1rem);
}
.recordatorio-anim .recordatorio-title { font-size: clamp(1.7rem, 8vw, 2.5rem); }

/* Entrada de Mesa de Regalos: scoped a .regalos-anim (no toca a No
   Niños, que comparte las mismas clases base). A propósito con técnicas
   distintas a las de No Niños (nada de escritura ni filas alternadas):
   giro, caída con rebote, zoom y wipe. Arranca después de que termina
   de girar el mosaico que trae esta sección (ver mosaicFlipTransition
   en js/script.js — el panel se activa desde el primer tile, así que
   estos delays cuentan desde ahí, no desde que se termina de ver). */
.regalos-in { opacity: 0; }
.regalos-anim.is-active .regalos-in { opacity: 1; }

.regalos-in--spin {
  transform: rotate(-70deg) scale(0.4);
  transition: opacity 700ms var(--ease-out-expo), transform 900ms var(--ease-out-back);
  will-change: transform, opacity;
}
.regalos-anim.is-active .regalos-in--spin { transform: rotate(0) scale(1); }

.regalos-in--drop {
  transform: translateY(-2.2em);
  transition: opacity 650ms var(--ease-out-expo), transform 750ms var(--ease-out-back);
  will-change: transform, opacity;
}
.regalos-anim.is-active .regalos-in--drop { transform: translateY(0); }

.regalos-in--pop {
  transform: scale(0.55);
  transition: opacity 700ms var(--ease-out-expo), transform 800ms var(--ease-out-back);
  will-change: transform, opacity;
}
.regalos-anim.is-active .regalos-in--pop { transform: scale(1); }

.regalos-in--wipe {
  clip-path: inset(0 100% 0 0);
  transition: opacity 400ms var(--ease-out-expo), clip-path 950ms var(--ease-in-out-quart);
  will-change: clip-path, opacity;
}
.regalos-anim.is-active .regalos-in--wipe { clip-path: inset(0 0 0 0); }

.regalos-in--rise {
  transform: translateY(2.2em) scale(0.92);
  transition: opacity 1100ms var(--ease-out-expo), transform 1200ms var(--ease-out-expo);
  will-change: transform, opacity;
}
.regalos-anim.is-active .regalos-in--rise { transform: translateY(0) scale(1); }

.ornament--itin-top.regalos-in--spin { transition-delay: 1500ms; }
.dresscode-eyebrow.regalos-in--drop { transition-delay: 1700ms; }
.dresscode-title.regalos-in--pop { transition-delay: 1900ms; }
.dresscode-intro.regalos-in--wipe { transition-delay: 2100ms; }
.regalo-icon-mesa.regalos-in--spin { transition-delay: 2300ms; }
.regalo-subheading-mesa.regalos-in--wipe { transition-delay: 2450ms; }
.regalo-intro-mesa.regalos-in--wipe { transition-delay: 2600ms; }
#regalo-catalogo-btn.regalos-in--wipe { transition-delay: 2800ms; }
.regalo-icon-sobres.regalos-in--spin { transition-delay: 3000ms; }
.regalo-subheading-sobres.regalos-in--wipe { transition-delay: 3150ms; }
.regalos-intro-2.regalos-in--wipe { transition-delay: 3300ms; }
#regalo-qr-btn.regalos-in--wipe { transition-delay: 3550ms; }
.noninos-bottom.regalos-in--rise { transition-delay: 3750ms; }

/* Padres — mismo fondo/degradado que No Niños, foto propia. */
.panel--padres { padding: 0; overflow: hidden; }
.padres-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 38% 30%;
  z-index: 1;
}
/* Fondo más oscuro y parejo, sin degradado (a diferencia de No Niños /
   Mesa de Regalos, que comparten .noninos-scrim). */
.padres-scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: rgba(6, 5, 4, 0.72);
}

/* Marco de esquineros: solo arriba-izquierda y abajo-derecha (dos trazos
   cada una), ancladas a las esquinas del panel, que crecen desde el
   vértice hacia afuera — no es un borde completo, solo las puntas. El
   delay arranca recién cuando termina el "push" que trae la sección
   (950ms, ver cornerSlideToSecond en JS): si empezara antes, el trazo se
   dibuja mientras el panel todavía está entrando fuera de pantalla y se
   ve ya terminado en vez de dibujándose. */
.padres-frame {
  position: absolute;
  inset: clamp(1rem, 4vw, 1.6rem);
  z-index: 3;
  pointer-events: none;
}
.padres-corner { position: absolute; width: clamp(5.5rem, 26vw, 9.5rem); height: clamp(5.5rem, 26vw, 9.5rem); }
.padres-corner::before,
.padres-corner::after {
  content: "";
  position: absolute;
  background: var(--color-accent);
  opacity: 0.85;
  transition: transform 1500ms var(--ease-in-out-quart);
  will-change: transform;
}
/* Trazo horizontal y vertical de cada esquina, con su propio origen de
   escala (el vértice) para que "se dibujen" creciendo hacia afuera. */
.padres-corner::before { height: 1.5px; width: 100%; transform: scaleX(0); }
.padres-corner::after { width: 1.5px; height: 100%; transform: scaleY(0); }

.padres-corner--tl { top: 0; left: 0; }
.padres-corner--tl::before { top: 0; left: 0; transform-origin: left; }
.padres-corner--tl::after { top: 0; left: 0; transform-origin: top; }

.padres-corner--br { bottom: 0; right: 0; }
.padres-corner--br::before { bottom: 0; right: 0; transform-origin: right; }
.padres-corner--br::after { bottom: 0; right: 0; transform-origin: bottom; }

.panel--padres.is-active .padres-corner::before,
.panel--padres.is-active .padres-corner::after {
  transform: scale(1);
  transition-delay: 600ms;
}

/* Layout general: título + ornamento arriba, columnas + placa abajo,
   dejando el medio libre (misma idea que No Niños / Mesa de Regalos). */
.padres-content { justify-content: space-between; }
.padres-top,
.padres-bottom {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(0.9rem, 3vh, 1.4rem);
}
.padres-bottom { gap: clamp(1.4rem, 4.5vh, 2rem); }

.padres-title {
  font-family: "New Icon Script", cursive;
  font-weight: 400;
  font-size: var(--fs-titulo);
  line-height: 1.2;
}

.padres-divider {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  color: var(--color-fg);
}

.padres-columns {
  display: flex;
  align-items: stretch;
  justify-content: center;
  gap: clamp(0.9rem, 4vw, 1.6rem);
  width: 100%;
}
.padres-columns-divider {
  align-self: stretch;
  width: 1px;
  background: var(--color-accent);
  opacity: 0.5;
  margin-block: 0.15rem;
}

.padres-group {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.55rem;
}

/* Pantallas muy angostas (≲360px): las dos columnas ya no entran cómodas
   una al lado de la otra — se apilan, y la línea vertical (que ahí no
   pinta nada) se esconde. */
@media (max-width: 360px) {
  .padres-columns { flex-direction: column; align-items: center; gap: 1.1rem; }
  .padres-columns-divider { display: none; }
}

/* Pantallas bajas (celulares angostos con doble barra de navegador —
   Samsung Internet, Brave, Opera con la barra de pestañas fija abajo
   además de la del navegador arriba — se midió el corte a partir de
   ~640px de alto disponible): casi todo el espaciado de esta sección se
   calcula en base al ANCHO (vw), así que en una pantalla angosta pero
   baja no se achica nada solo. El bloque de la cita (placa + comilla +
   versículo) es el que más pesa —la comilla decorativa ni tenía clamp,
   quedaba fija en 2.4rem siempre— y al ser el último elemento del panel,
   con overflow:hidden es justo lo que se recortaba. */
@media (max-height: 660px) {
  .panel--padres .noninos-content { padding-block: var(--space-md); }
  .padres-top { gap: clamp(0.5rem, 1.8vh, 0.9rem); }
  .padres-bottom { gap: clamp(0.9rem, 3vh, 1.3rem); }
  .padres-plaque {
    padding-block: clamp(1.1rem, 4vh, 1.7rem);
    padding-inline: clamp(1.2rem, 5vw, 1.8rem);
  }
  .padres-plaque-quote { font-size: 1.6rem; margin-bottom: 0.1rem; }
  .padres-verse-cite { margin-top: 0.35rem; }
}
/* Un escalón más para el caso extremo (doble barra + pantalla ya angosta
   de por sí, tipo S22 normal): a esta altura hasta el título y el
   versículo mismo tienen que ceder un poco. */
@media (max-height: 600px) {
  .panel--padres .noninos-content { padding-block: var(--space-sm); }
  .padres-title { font-size: clamp(1.8rem, 9vw, 2.6rem); }
  .padres-plaque { padding-block: clamp(0.85rem, 3vh, 1.2rem); }
  .padres-plaque-quote { font-size: 1.3rem; }
  .padres-verse { line-height: 1.35; }
}
.padres-col-heading {
  font-size: var(--fs-subtitulo);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 700;
}
.padres-col-underline { display: flex; align-items: center; gap: 0.3rem; }
.padres-col-underline i { display: block; width: clamp(1.1rem, 5vw, 1.6rem); height: 1px; background: var(--color-accent); opacity: 0.85; }
.padres-col-underline b { display: block; width: 3px; height: 3px; border-radius: 50%; background: var(--color-accent); }

.padres-name {
  font-size: var(--fs-texto);
  line-height: 1.55;
  overflow-wrap: break-word;
}

/* Placa de la cita: el contorno exacto viene del PNG que subiste
   (cuadro_seccion2.png — caja con el corazón asomando arriba, sin nada
   abajo), recoloreado a dorado vía mask-image igual que los ornamentos de
   Dress Code, en vez del intento a mano con CSS. */
.padres-plaque {
  position: relative;
  width: 100%;
  max-width: 30ch;
  padding: clamp(2.2rem, 9vw, 3rem) clamp(1.6rem, 6vw, 2.2rem) clamp(1.7rem, 6.5vw, 2.3rem);
  text-align: center;
}
.padres-plaque::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background-color: var(--color-accent);
  -webkit-mask-image: url("../assets/images/cuadro_seccion2.png");
  mask-image: url("../assets/images/cuadro_seccion2.png");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-position: center;
  mask-position: center;
}
.padres-plaque > * { position: relative; z-index: 1; }
.padres-plaque-quote {
  display: block;
  font-family: "Playfair Display", Georgia, serif;
  font-size: 2.4rem;
  line-height: 1;
  color: var(--color-accent);
  margin-bottom: 0.2rem;
}
.padres-verse {
  font-style: italic;
  font-size: var(--fs-texto);
  line-height: 1.55;
}
.padres-verse-cite {
  display: block;
  margin-top: 0.6rem;
  font-style: normal;
  font-size: var(--fs-texto);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-accent);
}

/* Entrada del contenido de Padres: cada bloque aparece con su propia
   animación (fundido, subida, deslizado lateral, escala) y en cadena,
   pausada — recién dispara cuando el panel se activa (coincide con el
   "push" que lo trae desde la Portada, ver cornerSlideToSecond en JS). */
.padres-in {
  opacity: 0;
  transition: opacity 850ms var(--ease-out-expo), transform 850ms var(--ease-out-back);
  will-change: transform, opacity;
}
.padres-in--fade { transform: none; }
.padres-in--up { transform: translateY(1.3em); }
.padres-in--left { transform: translateX(-2.4em); }
.padres-in--right { transform: translateX(2.4em); }
.padres-in--scale { transform: scale(0.82); }

/* Botón flotante de sonido — esquina superior derecha, sutil y
   semitransparente (vidrio esmerilado). Barritas tipo ecualizador de
   radio en movimiento mientras suena; ícono estático de silenciado
   cuando está pausada. */
.sound-toggle {
  position: absolute;
  top: clamp(0.9rem, 3.5%, 1.6rem);
  right: clamp(0.9rem, 3.5%, 1.6rem);
  z-index: 90;
  width: 2.5rem;
  height: 2.5rem;
  min-width: 2.5rem;
  min-height: 2.5rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Sin backdrop-filter: blur(). Este botón está fijo por encima de TODAS
     las secciones, y el blur del fondo obliga al navegador a recalcular y
     desenfocar esa zona en cada cuadro de cualquier animación que corra
     por debajo — era la causa principal de los tirones intermitentes.
     Un fondo semitransparente un poco más denso da un aspecto casi igual
     y no cuesta nada. */
  background: rgba(8, 6, 4, 0.42);
  border: 1px solid rgba(245, 245, 240, 0.28);
  color: var(--color-fg);
  opacity: 0.6;
  transition: opacity var(--dur-base) var(--ease-out-expo), background var(--dur-base) var(--ease-out-expo);
}
.sound-toggle:hover,
.sound-toggle:focus-visible { opacity: 0.95; }

.sound-toggle-bars {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.2rem;
  height: 1rem;
}
.sound-toggle-bars span {
  width: 0.16rem;
  height: 0.4rem;
  border-radius: 1px;
  background: currentColor;
  transform-origin: center;
  animation: sound-bar 900ms ease-in-out infinite;
  animation-play-state: paused; /* solo se mueven mientras suena, ver .is-playing */
}
.sound-toggle-bars span:nth-child(1) { animation-delay: 0ms; }
.sound-toggle-bars span:nth-child(2) { animation-delay: 180ms; }
.sound-toggle-bars span:nth-child(3) { animation-delay: 90ms; }
@keyframes sound-bar {
  0%, 100% { transform: scaleY(0.4); }
  50% { transform: scaleY(1.6); }
}
.sound-toggle.is-playing .sound-toggle-bars span { animation-play-state: running; }

.sound-toggle-muted-icon { display: none; width: 1.15rem; height: 1.15rem; }
.sound-toggle.is-muted .sound-toggle-bars { display: none; }
.sound-toggle.is-muted .sound-toggle-muted-icon { display: block; }

@media (prefers-reduced-motion: reduce) {
  .sound-toggle-bars span { animation: none; }
}

/* Navegación entre secciones: dos flechas sueltas, sin fondo ni círculo,
   a los costados y centradas verticalmente — lo más discretas posible,
   para no competir nunca con el contenido de la sección. */
.nav-btn {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 90;
  width: 2.5rem;
  height: 2.5rem;
  min-width: 2.5rem;
  min-height: 2.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: #fff;
  opacity: 0.55;
  transition: opacity var(--dur-base) var(--ease-out-expo);
  /* Halo oscuro sutil siguiendo el trazo del ícono (no una caja/fondo):
     en fotos con zonas claras (cielo, paredes blancas del Itinerario,
     etc.) la flecha blanca sin nada detrás se perdía. Dos capas —una
     ceñida, otra más difusa— dan contraste incluso sobre blanco puro sin
     verse como un botón "con fondo". Sobre las secciones oscuras del
     sitio esto es casi imperceptible, ahí no hace falta. */
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6)) drop-shadow(0 0 5px rgba(0, 0, 0, 0.4));
}
.nav-btn svg { width: 2rem; height: 2rem; }
.nav-btn--prev { left: clamp(0.1rem, 0.5%, 0.3rem); }
.nav-btn--next { right: clamp(0.1rem, 0.5%, 0.3rem); }
.nav-btn:hover,
.nav-btn:focus-visible { opacity: 0.9; }
.nav-btn:disabled { opacity: 0.2; pointer-events: none; }

/* En las primeras 3 secciones, la flecha de "siguiente" se mueve sola
   hacia la derecha en bucle — un empujoncito para que el invitado la
   note, ver updateNavButtons en js/script.js (agrega/saca .is-hint). */
@keyframes nav-btn-hint {
  0%, 100% { transform: translateY(-50%) translateX(0); }
  50% { transform: translateY(-50%) translateX(0.5rem); }
}
.nav-btn--next.is-hint { animation: nav-btn-hint 1.3s ease-in-out infinite; }
/* Ocultos del todo mientras dura el sobre + zoom — recién aparecen
   apenas cae el remate (nombres/fecha/contador), ver updateNavButtons
   en js/script.js. */
.nav-btn.is-hidden { opacity: 0; pointer-events: none; }

/* Señal instantánea de "ya te escuché" apenas se toca cualquiera de los
   dos — algunas transiciones tardan varios segundos, y sin esto parecía
   que el toque no había hecho nada. Se atenúa YA (80ms, más rápido que
   la transición normal) y vuelve a su opacidad de siempre recién cuando
   la transición (la que se vio o la que quedó encolada) termina de
   verdad — ver marcarNavOcupado()/updateNavButtons() en js/script.js. */
.nav-btn.is-ocupado { opacity: 0.2; transition: opacity 80ms ease-out; }

.panel--padres.is-active .padres-in { opacity: 1; transform: none; }

/* Arrancan recién cuando termina de dibujarse el marco de esquineros
   (1050ms de delay + 2600ms de trazo ≈ 3650ms). */
.padres-in-d1 { transition-delay: 2100ms; }
.padres-in-d2 { transition-delay: 2300ms; }
.padres-in-d3 { transition-delay: 2500ms; }
.padres-in-d4 { transition-delay: 2650ms; }
.padres-in-d5 { transition-delay: 2850ms; }

/* Las "figuritas" (ornamentos) quedan con un movimiento sutil y
   permanente una vez que aparecen, para dar sensación de vida. */
@keyframes padres-idle-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-0.25em); }
}
/* El flotado permanente arranca recién DESPUÉS de la entrada, y solo con
   la sección activa. Antes corría siempre (incluso con la sección oculta)
   y, como las animaciones CSS le ganan a las transiciones sobre la misma
   propiedad, le pisaba el transform a la animación de entrada — de ahí
   que los bloques de Padres entraran raro o con tirones. */
.panel--padres.is-active .padres-in--fade {
  animation: padres-idle-float 3.4s ease-in-out 3.2s infinite;
}

/* Confirmar Asistencia — foto 2.jpg con degradado oscuro solo arriba y
   abajo (zona clara al medio para que las manos de la foto nunca queden
   tapadas por texto); contenido en dos bloques (arriba/abajo) igual que
   No Niños y Mesa de Regalos, con hueco en el medio para la foto. */
.panel--rsvp { padding: 0; overflow: hidden; }
.rsvp-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 55%;
  z-index: 1;
}
.rsvp-scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: linear-gradient(
    to bottom,
    rgba(8, 6, 4, 0.75) 0%,
    rgba(8, 6, 4, 0.55) 22%,
    rgba(8, 6, 4, 0.1) 38%,
    rgba(8, 6, 4, 0.1) 55%,
    rgba(8, 6, 4, 0.55) 70%,
    rgba(8, 6, 4, 0.85) 100%
  );
}
.rsvp-content {
  position: relative;
  z-index: 3;
  height: 100%;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-lg) var(--space-md) clamp(0.4rem, 1.5%, 0.8rem);
  text-align: center;
  color: var(--color-fg);
  font-family: "Playfair Display", Georgia, serif;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6), 0 3px 12px rgba(0, 0, 0, 0.45);
}
.rsvp-top {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(0.6rem, 2vh, 1rem);
}
/* Más apretado que .rsvp-top: con los textos nuevos (fecha límite,
   feedback) este bloque creció y empezaba a comerse la zona clara del
   medio (manos de la foto) — ver nota en .rsvp-scrim. */
.rsvp-bottom {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(0.4rem, 1.3vh, 0.65rem);
}
/* .rsvp-title ya no necesita override: hereda el tamaño de .dresscode-title,
   igual que el resto de los títulos (mismo tamaño que No Niños). */

.rsvp-question {
  font-size: var(--fs-subtitulo);
  font-weight: 600;
}
.rsvp-label {
  font-family: "Montserrat", sans-serif;
  font-size: var(--fs-texto);
  opacity: 0.9;
}

.rsvp-count-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(0.6rem, 3vw, 1rem);
}
/* rsvp-count-btn ahora es el <label> de un radio nativo oculto
   (rsvp-count-radio) — la selección la maneja el navegador (:checked),
   no una clase que el JS tenga que agregar/quitar a mano. */
.rsvp-count-radio {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.rsvp-count-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: clamp(2.4rem, 9.5vw, 2.9rem);
  height: clamp(2.4rem, 9.5vw, 2.9rem);
  min-width: 2.75rem;
  min-height: 2.75rem;
  border-radius: 50%;
  border: 1.3px solid var(--color-accent);
  background: transparent;
  color: var(--color-fg);
  font-family: "Playfair Display", Georgia, serif;
  font-size: clamp(0.99rem, 4vw, 1.16rem);
  cursor: pointer;
  user-select: none;
  transition: background var(--dur-base) var(--ease-out-expo), color var(--dur-base) var(--ease-out-expo);
}
.rsvp-count-btn:hover,
.rsvp-count-radio:checked + .rsvp-count-btn { background: var(--color-accent); color: var(--color-bg); }
.rsvp-count-radio:focus-visible + .rsvp-count-btn { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* Botón que abre el modal para escribir los nombres de los acompañantes
   — mismo trazo que "No asistiré" (contorno, secundario), para no
   competir con "Confirmar". El modal en sí reusa .qr-modal-overlay. */
.rsvp-acompanantes-btn {
  margin-top: 0.4em;
  padding: 0.5em 1.4em;
  border-radius: 999px;
  border: 1.3px solid var(--color-accent);
  background: transparent;
  color: var(--color-fg);
  font-family: "Playfair Display", Georgia, serif;
  font-size: clamp(0.68rem, 2.5vw, 0.8rem);
  letter-spacing: 0.08em;
  cursor: pointer;
  transition: opacity var(--dur-base) var(--ease-out-expo), background var(--dur-base) var(--ease-out-expo);
}
.rsvp-acompanantes-btn:hover,
.rsvp-acompanantes-btn:focus-visible { background: rgba(245, 245, 240, 0.08); }

/* Casillas de nombre de cada acompañante (pases 2 en adelante) — aparecen
   recién al elegir una cantidad mayor a 1, ver renderAcompanantes en
   js/rsvp.js. Mismo lenguaje visual liviano que el resto del formulario:
   sin caja completa, solo una línea inferior dorada. */
.rsvp-acompanantes {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  width: 100%;
  max-width: 20rem;
  margin-top: 0.2rem;
}
.rsvp-acompanante-input {
  width: 100%;
  padding: 0.4em 0.2em;
  border: none;
  border-bottom: 1.3px solid var(--color-accent);
  background: transparent;
  color: var(--color-fg);
  font-family: "Playfair Display", Georgia, serif;
  font-size: var(--fs-texto);
  text-align: center;
  transition: border-color var(--dur-base) var(--ease-out-expo);
}
.rsvp-acompanante-input::placeholder { color: var(--color-muted); opacity: 0.75; }
.rsvp-acompanante-input:focus { outline: none; border-color: var(--color-fg); }

.rsvp-confirm-btn:disabled,
.rsvp-no-asiste:disabled { opacity: 0.5; cursor: default; }

.rsvp-no-asiste {
  margin-top: 0.6em;
  padding: 0.4em 1.2em;
  border-radius: 999px;
  border: 1.3px solid var(--color-muted);
  background: transparent;
  color: var(--color-fg);
  opacity: 0.75;
  font-family: "Playfair Display", Georgia, serif;
  font-size: clamp(0.62rem, 2.2vw, 0.72rem);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
  transition: opacity var(--dur-base) var(--ease-out-expo), background var(--dur-base) var(--ease-out-expo);
}
.rsvp-no-asiste:hover,
.rsvp-no-asiste:focus-visible { opacity: 1; background: rgba(245, 245, 240, 0.08); }

.rsvp-deadline {
  margin-top: 0.1em;
  font-family: "Montserrat", sans-serif;
  font-size: var(--fs-texto);
  opacity: 0.65;
  text-align: center;
}

.rsvp-feedback {
  margin-top: 0.3em;
  font-family: "Playfair Display", Georgia, serif;
  font-size: var(--fs-texto);
  color: var(--color-accent);
  text-align: center;
}

.rsvp-confirm-btn {
  padding: 0.7em 2.4em;
  min-height: 2.75rem;
  border-radius: 999px;
  border: 1.3px solid var(--color-accent);
  background: transparent;
  color: var(--color-fg);
  font-family: "Playfair Display", Georgia, serif;
  font-size: clamp(0.79rem, 3vw, 0.94rem);
  letter-spacing: 0.28em;
  text-transform: uppercase;
  padding-left: calc(2.4em + 0.28em);
  cursor: pointer;
  transition: background var(--dur-base) var(--ease-out-expo), color var(--dur-base) var(--ease-out-expo);
}
.rsvp-confirm-btn:hover,
.rsvp-confirm-btn:focus-visible { background: var(--color-accent); color: var(--color-bg); }

.rsvp-ticket-btn {
  margin-top: 0.6em;
  padding: 0.6em 1.8em;
  border-radius: 999px;
  border: 1.3px solid var(--color-accent);
  background: var(--color-accent);
  color: var(--color-bg);
  font-family: "Playfair Display", Georgia, serif;
  font-size: clamp(0.75rem, 2.8vw, 0.88rem);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
  transition: opacity var(--dur-base) var(--ease-out-expo);
}
.rsvp-ticket-btn:hover,
.rsvp-ticket-btn:focus-visible { opacity: 0.85; }
.rsvp-ticket-btn:disabled { opacity: 0.5; cursor: default; }

.rsvp-ornament-bottom { width: clamp(5rem, 26vw, 7rem); }

/* Entrada de Confirmar Asistencia: scoped a .rsvp-anim. Técnicas que
   ningún otro bloque del sitio usó todavía — nada de escritura, filas
   alternadas, giro simple ni caída con rebote (eso ya lo tienen otras
   secciones): acá hay flip 3D, letter-spacing colapsando, blur a foco,
   inclinación (skew), rebote con torsión, y botones que "pop" en
   cadena rápida como fichas de dominó.
   OJO con el disparador: es .is-iris-done, NO .is-active. El panel se
   activa al ARRANCAR el iris circular, así que colgar esto de .is-active
   hacía que el contenido entrara mientras el recorte todavía estaba
   creciendo. Y no es contenido barato: letter-spacing recalcula el
   layout en cada cuadro y filter: blur() repinta la capa entera, todo
   encima de un clip-path a pantalla completa — el resultado era un iris
   a los tirones dejando ver Regalos por detrás en los cuadros perdidos.
   Ahora la clase la pone JS recién cuando el círculo terminó de tapar la
   pantalla, y los delays de abajo van rebajados en esos 1650ms, así el
   contenido entra exactamente en el mismo momento que antes — lo único
   que cambió es que ya no compite con el recorte (ver irisToEighth en
   js/script.js). */
.rsvp-in { opacity: 0; }
.rsvp-anim.is-iris-done .rsvp-in { opacity: 1; }

/* Ornamentos: se abren en 3D como una tapa, arriba hacia abajo y abajo
   hacia arriba (espejado). */
.rsvp-in--flip {
  transform: perspective(500px) rotateX(90deg);
  transform-origin: top center;
  transition: opacity 550ms var(--ease-out-expo), transform 800ms var(--ease-out-back);
  will-change: transform, opacity;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.rsvp-anim.is-iris-done .rsvp-in--flip { transform: perspective(500px) rotateX(0deg); }
.rsvp-in--flip-bottom {
  transform: perspective(500px) rotateX(-90deg);
  transform-origin: bottom center;
  transition: opacity 550ms var(--ease-out-expo), transform 800ms var(--ease-out-back);
  will-change: transform, opacity;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.rsvp-anim.is-iris-done .rsvp-in--flip-bottom { transform: perspective(500px) rotateX(0deg); }

/* "Confirmar": el tracking (separación entre letras) se va cerrando
   hasta su valor normal, en vez de aparecer ya armado. */
.rsvp-in--tracking {
  letter-spacing: 1.6em;
  transition: opacity 650ms var(--ease-out-expo), letter-spacing 950ms var(--ease-out-expo);
}
.rsvp-anim.is-iris-done .rsvp-in--tracking { letter-spacing: 0.5em; }

/* "Asistencia": entra desenfocada y se va enfocando, como si la cámara
   hiciera foco. */
.rsvp-in--blur {
  filter: blur(16px);
  transform: scale(1.12);
  transition: opacity 600ms var(--ease-out-expo), filter 900ms var(--ease-out-expo), transform 900ms var(--ease-out-expo);
  will-change: transform, filter, opacity;
}
.rsvp-anim.is-iris-done .rsvp-in--blur { filter: blur(0); transform: scale(1); }

/* El texto de la intro entra inclinado (skew), como si se enderezara. */
.rsvp-in--skew {
  transform: skewX(-14deg) translateX(-2.2em);
  transition: opacity 600ms var(--ease-out-expo), transform 800ms var(--ease-out-expo);
  will-change: transform, opacity;
}
.rsvp-anim.is-iris-done .rsvp-in--skew { transform: skewX(0) translateX(0); }

/* "¿Nos acompañarás?" rebota desde abajo con una leve torsión. */
.rsvp-in--bounce {
  transform: translateY(1.6em) rotate(-5deg);
  transition: opacity 550ms var(--ease-out-expo), transform 750ms var(--ease-out-back);
  will-change: transform, opacity;
}
.rsvp-anim.is-iris-done .rsvp-in--bounce { transform: translateY(0) rotate(0); }

.rsvp-in--fade { transition: opacity 700ms var(--ease-out-expo); }

/* Los botones de cantidad "pop" uno por uno, rápido, como fichas de
   dominó cayendo en cadena. */
.rsvp-in--pop {
  transform: scale(0);
  transition: opacity 350ms var(--ease-out-expo), transform 550ms var(--ease-out-back);
  will-change: transform, opacity;
}
.rsvp-anim.is-iris-done .rsvp-in--pop { transform: scale(1); }

/* El botón "Confirmar" se despliega como una cinta desde el centro. */
.rsvp-in--unfurl {
  transform: scaleX(0);
  transform-origin: center;
  transition: opacity 350ms var(--ease-out-expo), transform 750ms var(--ease-out-expo);
  will-change: transform, opacity;
}
.rsvp-anim.is-iris-done .rsvp-in--unfurl { transform: scaleX(1); }
/* Una vez desplegado, late suave para invitar a tocarlo. */
.rsvp-confirm-pulse { animation: finale-heartbeat 3.8s ease-in-out infinite; animation-delay: 7700ms; }

/* Delays contados desde .is-iris-done (fin del iris, ~1650ms), no desde
   el arranque de la transición — son los de antes menos esos 1650ms, así
   cada elemento entra en el mismo instante de siempre. */
.ornament--itin-top.rsvp-in--flip { transition-delay: 0ms; }
.dresscode-eyebrow.rsvp-in--tracking { transition-delay: 200ms; }
.rsvp-title.rsvp-in--blur { transition-delay: 400ms; }
.dresscode-intro.rsvp-in--skew { transition-delay: 650ms; }
.rsvp-question.rsvp-in--bounce { transition-delay: 900ms; }
.rsvp-label.rsvp-in--fade { transition-delay: 1100ms; }
/* nth-of-type, no nth-child: cada label ahora va precedido por su input
   (radio) hermano, así que la posición entre <label> del mismo tipo es lo
   que hay que contar, no la posición entre todos los hijos. */
.rsvp-count-btn.rsvp-in--pop:nth-of-type(1) { transition-delay: 1250ms; }
.rsvp-count-btn.rsvp-in--pop:nth-of-type(2) { transition-delay: 1325ms; }
.rsvp-count-btn.rsvp-in--pop:nth-of-type(3) { transition-delay: 1400ms; }
.rsvp-count-btn.rsvp-in--pop:nth-of-type(4) { transition-delay: 1475ms; }
.rsvp-confirm-btn.rsvp-in--unfurl { transition-delay: 1700ms; }
.rsvp-deadline.rsvp-in--fade { transition-delay: 1800ms; }
.rsvp-no-asiste.rsvp-in--fade { transition-delay: 1900ms; }
.rsvp-ornament-bottom.rsvp-in--flip-bottom { transition-delay: 1950ms; }
.dresscode-closing.rsvp-in--fade { transition-delay: 2150ms; }

@media (prefers-reduced-motion: reduce) {
  .finale-curtain { transition-duration: 200ms; }
  .finale-content .ornament,
  .finale-slide-in,
  .countdown { transition-duration: 200ms; }
  .padres-in { transition-duration: 200ms; }
  .padres-in--fade { animation: none; }
  .finale-content .ornament,
  .finale-names { animation: none; }
  .dresscode-idle-float,
  .dresscode-heartbeat { animation: none; }
  .recordatorio-anim .ornament--itin-top,
  .recordatorio-row,
  .recordatorio-bottom-in { transition-duration: 200ms; transition-delay: 0ms; }
  .recordatorio-title-wipe { transition-duration: 200ms; transition-delay: 0ms; transition-timing-function: linear; }
  .regalos-in { transition-duration: 200ms; transition-delay: 0ms; }
  .rsvp-in { transition-duration: 200ms; transition-delay: 0ms; }
  .ubic-in { transition-duration: 200ms; transition-delay: 0ms; }
  .rsvp-confirm-pulse { animation: none; }
  .padres-corner::before,
  .padres-corner::after { transition-duration: 200ms; }
  .reveal-wipe { transition-duration: 200ms; transition-timing-function: linear; }
  /* La duración del velo la fija JS vía --veil-dur (más corta si hay reduced-motion) */
  .panel { transition-duration: 200ms; }
}

/* .content-seen: la sección ya mostró su contenido alguna vez (ver
   contentSeen en js/script.js) — el panel puede volver a hacer toda su
   transición de entrada (zoom, mosaico, empuje...) las veces que haga
   falta, pero el contenido de ADENTRO ya no vuelve a esperar sus delays
   largos ni a "escribirse"/entrar de nuevo: aparece de una. !important
   a propósito, es una utilidad que tiene que ganarle a cualquier delay
   puesto más específico (o inline, vía JS) en cada sección. */
.content-seen .padres-in,
.content-seen .padres-corner::before,
.content-seen .padres-corner::after,
.content-seen .ubic-in,
.content-seen .ornament--itin-top,
.content-seen .ornament--itin-mid,
.content-seen .itinerario-title,
.content-seen .itinerario-icon,
.content-seen .itinerario-connector,
.content-seen .itinerario-text,
.content-seen .recordatorio-title-wipe,
.content-seen .recordatorio-row,
.content-seen .recordatorio-bottom-in,
.content-seen .regalos-in,
.content-seen .rsvp-in {
  transition-delay: 0ms !important;
  transition-duration: 1ms !important;
}

/* =========================================================
   6. TEXTO — REVEAL CINEMATOGRÁFICO
   ========================================================= */
.reveal-text span { display: block; overflow: hidden; }
.reveal-text span span {
  display: block;
  transform: translateY(100%);
  transition: transform var(--dur-cinematic) var(--ease-out-expo);
  will-change: transform;
}
.panel.is-active .reveal-text span span { transform: translateY(0); }
.panel.is-active .reveal-text span:nth-child(1) span { transition-delay: 100ms; }
.panel.is-active .reveal-text span:nth-child(2) span { transition-delay: 220ms; }

/* =========================================================
   7. RESPONSIVE — extremos
   ========================================================= */
@media (max-width: 26.25em) { /* ~420px */
  .panel { padding: var(--space-md) var(--space-sm); }
}

@media (orientation: landscape) and (max-height: 30em) { /* landscape de celular */
  .panel { justify-content: flex-start; padding-top: var(--space-sm); }
}

/* Pantallas bajas: el sobre + tarjetas achican para dejar siempre un
   margen libre antes del texto de abajo (.portada-outro), sin tocar
   ninguna animación (son los mismos top%/transform, solo más chicos). */
@media (max-height: 700px) {
  .envelope { top: 48%; width: min(56%, 320px); }
}
@media (max-height: 580px) {
  .envelope { top: 44%; width: min(50%, 280px); }
  /* iPhone 5 y similares (320x568): sin este margen extra no entra todo
     junto — el texto vuelve a quedar pegado al borde como antes. */
  .portada-outro { bottom: clamp(0.4rem, 1.5%, 0.8rem); }
  /* Y la tarjeta ahí queda tan angosta que la letra grande (arriba) se ve
     gigante encima de ella — solo en esta pantalla chica se achica. */
  .card-text-icon { width: clamp(0.75rem, 3.6vw, 1rem); }
  .card-text-label,
  .card-text-guests { font-size: clamp(0.4rem, 1.7vw, 0.5rem); }
  .card-text-name { font-size: clamp(0.62rem, 3.1vw, 0.84rem); }
}

/* =========================================================
   8. ACCESIBILIDAD DE MOVIMIENTO
   ========================================================= */
@media (prefers-reduced-motion: reduce) {
  .reveal-text span span { transition-duration: 200ms; }
  .tap-hint { animation: none; }
  .envelope-solapa,
  .invitation-card { transition-duration: 200ms; } /* se reduce, no se elimina: revela contenido funcional */
  .card-photo.is-fullscreen { transition-duration: 200ms; }
  .nav-btn--next.is-hint { animation: none; }
}
