/* ============================================================
   José & Fernanda — 02.10.2026
   Palette: warm paper, ink, aubergine plum, bougainvillea, dry gold
   ============================================================ */

/* Linestay lives in this project (fonts/), unlike the three Google fonts that
   load from the web. swap = show fallback text first, restyle when it lands. */
@font-face {
  font-family: "Linestay";
  src: url("../fonts/linestay.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Warm cream, not cool: the page has to agree with the sunset above it */
  --paper:      #FBF5EA;
  --paper-tint: #F1EADC;
  --ink:        #241F23;
  --ink-soft:   #5B5257;
  --plum:       #563A52;  /* bougainvillea, deep — kickers, buttons, the footer.
                             Held one step down from the accent it wants to be:
                             it is also the footer, and against --deep a lighter
                             plum read as a slab stuck on the end of the page.
                             At this value the footer is still a step up. */
  --gold:       #B39B6E;
  --line:       rgba(36, 31, 35, 0.14);

  /* Sampled off the hero artwork — the low sun spilling past the lawn.
     Used by .section--dusk to carry the illustration into the page.
     --grass-edge and --glow are read straight out of the images: the ground
     strip ends on #70853B and the couple plate on #6D8531 (near enough that
     one ramp serves both), and the sky's horizon burns at #E9A776.

     These four stay green and warm on purpose. The page went aubergine; the
     artwork did not, because the lawn is still a lawn. And --grass-edge is not
     a colour anyone picked — it is the literal last row of pixels of
     hero-ground.webp, so moving it opens a seam under the hero. */
  --grass-edge: #70853B;  /* the hero's literal last row of pixels */
  --glow:       #E9A776;  /* the horizon light, the warmest point of the ramp */
  --sand:       #F4D7B4;  /* the light the welcome copy sits in */
  --umber:      #7A4E22;  /* warm brown — the script words */

  /* The night end of the scale. --deep is the darkest surface the page ever
     paints — the RSVP, the gallery with a photo open, the countdown figure.
     --night-floor sits one step below it and is where the two garden clips
     bottom out, so the vignette has somewhere to hand over to instead of
     cutting straight against the RSVP. See .section--night::before. */
  --deep:        #301B2E;  /* aubergine night */
  --night-floor: #1C0F1B;  /* where the clips land, a step under --deep */

  --font-display: "Cormorant Garamond", Georgia, serif;
  --font-script:  "Ephesis", cursive;
  --font-ui:      "Jost", "Helvetica Neue", Arial, sans-serif;
  --font-amp:     "Linestay", "Ephesis", cursive;   /* only the "&" */

  --wrap:        1120px;
  --wrap-narrow: 660px;
  --pad:         clamp(1.25rem, 5vw, 3rem);
  --section-y:   clamp(4.5rem, 11vw, 9rem);
}

* { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-ui);
  font-weight: 300;
  font-size: 1rem;
  line-height: 1.7;
  overflow-x: hidden;
}

img { max-width: 100%; display: block; }

::selection { background: var(--plum); color: var(--paper); }

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

/* ---------- layout primitives ---------- */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--pad);
}
.wrap--narrow { max-width: var(--wrap-narrow); }

.section {
  padding-block: var(--section-y);
}
.section--tint { background: var(--paper-tint); }

/* ---------- the hero seam ----------
   The hero ends on saturated grass (L≈55) and the page is cream (L≈96). Dropped
   straight against each other that reads as a cut, and fading the grass into
   cream only turns it milky — the missing piece is the value in between.

   So this paints the low sun spilling past the lawn: it starts on the same
   green the hero ends on and climbs through ember, amber and sand into the
   page. No step in that ramp is more than ~10 points of lightness, which is
   what makes the join disappear.

   Texture can't carry the join — the parallax drifts the grass layer while you
   scroll, and portrait phones size the grass off the couple plate instead of
   the ground strip. Light and colour can. */
.section--dusk {
  position: relative;
  background: var(--paper);

  /* Extra room at the top so the light has space to open before any copy
     lands in it. Kept in vh so the copy always falls at the same point on the
     ramp — around the sand stop — whatever the window height. */
  padding-top: clamp(8rem, 26vh, 18rem);

  /* Warm ink, deepened: at the amber end of the ramp the stock --ink-soft
     only reaches 3.8:1. This holds ≥6.5:1 anywhere the copy can land. */
  --ink-soft: #4A4638;
}
.section--dusk::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;      /* fixed height, so it never stretches with content */
  height: 78vh;
  pointer-events: none;

  /* Lightness has to climb the whole way. An earlier version dipped through a
     brown at the top and that dip read as a band of mud under the lawn — the
     eye catches a reversal far more readily than a long ramp. No step here is
     more than ~8 points of L*. */
  background: linear-gradient(
    to bottom,
    var(--grass-edge)   0%,   /* L* 52 — matches the hero exactly */
    #7C8A40             5%,
    #94904C            12%,
    #B2985D            20%,
    #D2A26D            28%,
    var(--glow)        36%,   /* L* 74 — peak warmth */
    #EFC094            50%,
    var(--sand)        64%,
    #F8E7CE            78%,
    #FAF0DF            90%,
    var(--paper)      100%    /* L* 96 */
  );
}
/* Lifts the content over the gradient — .reveal's transform already makes a
   stacking context, so this needs the explicit z-index to win. */
.section--dusk > .wrap {
  position: relative;
  z-index: 1;
}
/* Ties the greeting to the countdown number below it, and clears the plum's
   4.83:1 against the warmest part of the ramp — --deep holds 7.76:1 there. */
.section--dusk .script-line { color: var(--deep); }

/* Small caps label that opens each section */
.section__kicker {
  margin: 0 0 1.5rem;
  font-size: 0.7rem;
  font-weight: 400;
  letter-spacing: 0.38em;
  text-transform: uppercase;
  color: var(--ink-soft);
  text-align: center;
}
.section__kicker::after {
  content: "";
  display: block;
  width: 34px;
  height: 1px;
  margin: 1.1rem auto 0;
  background: var(--gold);
}

.script-line {
  margin: 0 0 1.5rem;
  font-family: var(--font-script);
  font-size: clamp(2.75rem, 8vw, 4.5rem);
  line-height: 1;
  font-weight: 400;
  color: var(--plum);
  text-align: center;
}
.script-line--sm { font-size: clamp(2.1rem, 6vw, 3.1rem); margin-bottom: 2.5rem; }

.lede {
  max-width: 46ch;
  margin: 0 auto 3rem;
  text-align: center;
  font-family: var(--font-display);
  font-size: clamp(1.15rem, 2.4vw, 1.4rem);
  line-height: 1.65;
  color: var(--ink-soft);
}

/* ---------- buttons ---------- */

.btn {
  display: inline-block;
  padding: 0.85rem 2.1rem;
  font-family: var(--font-ui);
  font-size: 0.72rem;
  font-weight: 400;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  text-decoration: none;
  border: 1px solid var(--plum);
  border-radius: 0;
  background: transparent;
  color: var(--plum);
  cursor: pointer;
  transition: background 0.35s ease, color 0.35s ease;
}
.btn--ghost:hover,
.btn--ghost:focus-visible { background: var(--plum); color: var(--paper); }

.btn--solid {
  background: var(--plum);
  color: var(--paper);
  width: 100%;
  padding-block: 1.05rem;
}
.btn--solid:hover,
.btn--solid:focus-visible { background: var(--ink); border-color: var(--ink); }
.btn--solid[disabled] { opacity: 0.5; cursor: default; }

/* ============================================================
   HERO — the signature: names split around a large script ampersand
   ============================================================ */

.hero {
  position: relative;
  height: 100svh;
  min-height: 540px;
  overflow: hidden;

  /* Couple artwork height as a share of the hero. --ground-h widens that
     artwork's grass to the full window: it covers the bottom 14.37% of the
     plate, measured from the blade tips down. Re-measure if the art changes. */
  --couple-h: 54%;
  --ground-h: 7.76%;

  /* Stands in for the sky while the image loads */
  background: linear-gradient(#6E7FB0 0%, #C99A86 55%, #F3C489 100%);
}

.hero__layers { position: absolute; inset: 0; }

/* --- layer 1: sky. Oversized so it can drift without exposing an edge --- */
.hero__sky {
  position: absolute;
  left: 0;
  top: -50%;
  width: 100%;
  height: 150%;
  object-fit: cover;
  object-position: center bottom;
}

/* --- layers 2–4: full-stage wrappers. GSAP owns their transform, so the
       artwork inside is positioned with flexbox rather than translate. --- */
.hero__stage {
  position: absolute;
  inset: 0;
  display: flex;
  justify-content: center;
}
.hero__stage img { max-width: none; }

/* Chalet sits on the horizon, offset left into the open area beside the couple.
   Positioned absolutely so each axis measures against the right side of the
   hero: a percentage margin would resolve against the width on both axes and
   drift the chalet off the grass as the window aspect changed. */
.hero__stage--chalet img {
  position: absolute;
  bottom: 5.5%;   /* of hero height — base tucked into the grass */
  left: 8%;       /* of hero width */
  height: 26%;
  width: auto;
}

/* Couple + grass, the front plate */
.hero__stage--couple { align-items: flex-end; }
.hero__stage--couple img {
  position: relative;
  height: var(--couple-h);
  width: auto;
}

/* Continues the artwork's grass strip out to the window edges. The image is
   the same strip lifted from the couple artwork, so the colours meet exactly. */
.hero__ground {
  position: absolute;
  inset: auto 0 0 0;
  height: var(--ground-h);
  /* auto width keeps the blades at the same scale as the couple artwork, so
     the strip continues its grass instead of butting against it. No background
     colour — the strip's top is transparent between the blades. */
  background: url("../images/hero-ground.webp") center bottom / auto 100% no-repeat;
}

.hero__stage--title {
  align-items: flex-start;
  padding: clamp(3.5rem, 11vh, 8rem) var(--pad) 0;
}

.hero__content {
  text-align: center;
  max-width: 900px;
}

/* The join needs a fixed colour to hand over to, and the hero can't supply one:
   the parallax drifts the couple layer down as you scroll, so its bottom edge
   slides from #7b8d39 (L*56) to #557333 (L*45) on a desktop, and on a portrait
   phone all the way to #93745b — brown, once the grass has cleared the frame
   and the sky shows through.

   So the last few pixels get settled onto --grass-edge, which is what
   .section--dusk starts on. Only the bottom third of this band carries real
   opacity, which keeps it clear of the dogs standing on the grass.

   (An earlier version of this file faded to cream here instead and only washed
   the grass out — the fix is fading to the grass, not away from it.) */
.hero__seam {
  position: absolute;
  inset: auto 0 0 0;
  height: 5%;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    rgba(112, 133, 59, 0)    0%,
    rgba(112, 133, 59, 0)   45%,
    rgba(112, 133, 59, 0.55) 78%,
    rgba(112, 133, 59, 1)  100%
  );
}

/* Portrait phones: the artwork is far wider than it is tall, so size the couple
   by width and let it bleed past both edges — no ground strip needed. There is
   no clear ground left for the chalet at this width, and a clipped one behind
   the dog reads as clutter, so it steps out and the couple gets the room. */
@media (max-aspect-ratio: 3 / 4) {
  .hero { --ground-h: 0%; }
  .hero__stage--couple img { width: 150%; height: auto; }
  .hero__stage--chalet { display: none; }
}

/* Hero type sits on the sunset, so it runs warm-white with a soft drop shadow */
.hero__content { color: #FFF7EA; text-shadow: 0 1px 18px rgba(74, 45, 24, 0.5); }

.hero__names {
  margin: 0;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: clamp(0.25rem, 1.5vw, 1.25rem);
  font-weight: 300;
  line-height: 0.92;
}
.hero__name {
  font-family: var(--font-display);
  font-size: clamp(3rem, 12vw, 7.5rem);
  letter-spacing: 0.01em;
  text-align: right;
  opacity: 0;
  animation: fadeUp 1.2s 0.45s ease forwards;
}
.hero__name--right { text-align: left; animation-delay: 0.65s; }

.hero__amp {
  font-family: var(--font-amp);
  font-size: clamp(4.5rem, 16vw, 10rem);
  color: #FFE2B0;
  line-height: 0.8;
  /* Optical centring against the names, measured on a flat background.
     In em so it tracks the font size at every breakpoint. */
  transform: translateY(0.1em);
  opacity: 0;
  animation: fadeIn 1.6s 1s ease forwards;
}

/* Narrow screens: stack the names, ampersand centred between them */
@media (max-width: 560px) {
  .hero__names {
    grid-template-columns: 1fr;
    justify-items: center;
    gap: 0;
  }
  .hero__name,
  .hero__name--right { text-align: center; }
  .hero__name { font-size: clamp(3.25rem, 17vw, 5rem); }
  .hero__amp {
    font-size: clamp(3rem, 15vw, 4.5rem);
    /* Asymmetric on purpose: Linestay's ampersand sits high in its line box,
       so an equal negative margin on both sides pulled it into "José" and
       left a gap before "Fernanda". Same total height, redistributed. */
    margin-block: 0.05em -0.25em;
    transform: none;
  }
}

.hero__date {
  margin: clamp(1.5rem, 3.5vw, 2.25rem) 0 0;
  font-size: clamp(0.72rem, 1.8vw, 0.85rem);
  letter-spacing: 0.35em;
  text-transform: uppercase;
  opacity: 0;
  animation: fadeUp 1.1s 1.15s ease forwards;
}

/* Lenis smooth scroll — required by the library, and CSS smooth-scroll must
   be off while it runs or the two fight over the same scroll position. */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-stopped { overflow: hidden; }

@keyframes fadeUp {
  from { opacity: 0; transform: translateY(1.25rem); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
/* Barely there on purpose: the day count only moves once a day, so this is what
   keeps it from reading as a printed number. Anything past ~1.5% starts to look
   like a heartbeat monitor. */
@keyframes cdBreathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.012); }
}

/* ============================================================
   COUNTDOWN — the days lead, everything else stays quiet.

   Four equal units in a ruled box is the coming-soon timer every launch page
   ships; on a wedding it borrows the wrong urgency. One number carries it
   instead, with the two script words hung off its corners.
   ============================================================ */

.countdown {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  grid-template-rows: auto auto auto;
  align-items: center;
  margin-top: clamp(2.5rem, 7vw, 4.5rem);
}

/* "faltan" off the top-left of the number, "días" off its bottom-right */
.countdown__lead { grid-area: 1 / 1 / 2 / 2; justify-self: end;   }
.countdown__days { grid-area: 1 / 2 / 3 / 3;                      }
.countdown__noun { grid-area: 2 / 3 / 3 / 4; justify-self: start; }
.countdown__rest { grid-area: 3 / 1 / 4 / 4; text-align: center;  }

.countdown__days {
  font-family: var(--font-display);
  font-size: clamp(6rem, 22vw, 13rem);
  font-weight: 300;
  line-height: 0.8;
  /* Proportional, not tabular: Cormorant's tabular "1" carries so much side
     bearing that "61" broke into two separate figures. This only changes once
     a day, so there is no jitter to guard against. */
  font-variant-numeric: proportional-nums;
  letter-spacing: -0.02em;
  color: var(--deep);
  /* Its grid column is auto-sized, so the box already hugs the number and
     cdBreathe scales around the figure itself. The origin sits below centre so
     it breathes off its own baseline instead of drifting up into "faltan". */
  transform-origin: 50% 55%;
  will-change: transform;
  animation: cdBreathe 6s ease-in-out infinite;
}

.countdown__lead,
.countdown__noun {
  font-family: var(--font-script);
  /* Sized with .script-line--sm: Ephesis is thin, and below this it stops
     reading as handwriting and starts reading as a mistake. */
  font-size: clamp(1.9rem, 5.5vw, 3rem);
  line-height: 1;
  color: var(--umber);
}
/* Tucked in so the words touch the number's corners rather than float free */
.countdown__lead { margin-right: -0.15em; }
.countdown__noun { margin-left: -0.1em; }

/* Flex, not a run of text: the three units together run ~290px at this tracking,
   which is the whole width of a small phone. Wrapping by unit keeps a "· " from
   ever landing at the end of a line. Tabular figures so the ticking seconds
   don't shove the line around. */
.countdown__rest {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0 0.75em;
  margin: clamp(1.25rem, 3vw, 2rem) 0 0;
  font-size: 0.72rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}
.countdown__sep { opacity: 0.55; }
/* Los números que laten: inline no acepta transform, y sin esto el fundido de
   beat() se vería sin el micro-desplazamiento. */
.countdown__unit > span { display: inline-block; }

/* Narrow phones stack instead, since the day count runs to three digits for
   most of the engagement and "faltan 340 días" will not sit on one line there.
   justify-self has to be reset too — the hanging alignment from the three
   column layout would otherwise throw the words out to opposite edges. */
@media (max-width: 420px) {
  .countdown { grid-template-columns: 1fr; }
  .countdown__lead { grid-area: 1 / 1 / 2 / 2; justify-self: center; margin: 0; }
  .countdown__days { grid-area: 2 / 1 / 3 / 2; justify-self: center; }
  .countdown__noun { grid-area: 3 / 1 / 4 / 2; justify-self: center; margin: 0; }
  .countdown__rest { grid-area: 4 / 1 / 5 / 2; }
}

/* ============================================================
   SCENE — el video de fondo

   La sección se reproduce sola en cuanto la imagen tapa la pantalla entera, y
   al terminar deja la imagen ciclando con las tarjetas ya puestas. El JS solo
   pinta el canvas y las tarjetas. Ver initScrollScenes en js/main.js.

   Una pantalla y ni un píxel más. Hubo una versión con position:sticky y 160vh
   de recorrido, heredada de cuando el clip iba atado al scroll y necesitaba
   pista por donde correr. Ahora el clip corre solo y ese recorrido solo servía
   para obligar a scrollear pantalla y media por una sección que ya había
   terminado de contar lo suyo. El scroll no pilota nada: solo dispara.

   Sin la clase .is-live —sin JS, sin GSAP, o con movimiento reducido— nada de
   esto se activa: el panel vuelve a altura automática y las dos tarjetas se
   apilan en flujo normal sobre el primer frame. La información nunca depende
   del JavaScript.
   ============================================================ */

.scene {
  position: relative;
  background: #1b1d28;    /* el azul del cielo nocturno del clip */
}

/* Alto en vh, no en svh como el hero: aquí el panel es un fondo que debe tapar
   la pantalla entera, y svh —el viewport *pequeño*— dejaría una franja
   descubierta abajo en cuanto Safari esconde la barra de direcciones. Que
   sobresalga por abajo no se ve; que falte, sí.

   El track no lleva alto propio: lo toma del panel. Es lo que hace que la
   sección mida exactamente una pantalla. */
.scene.is-live .scene__viewport { height: 100vh; }

.scene__viewport {
  position: relative;
  overflow: hidden;
}

.scene__poster,
.scene__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
/* El póster es el frame 0 y es lo único que se ve hasta que el canvas pinta
   —y lo único, punto, si los frames nunca llegan. El recorte "cover" del
   canvas se calcula en el drawImage, no aquí. */
.scene__poster { object-fit: cover; z-index: 0; }
.scene__canvas { z-index: 1; opacity: 0; transition: opacity 0.4s ease; }
.scene.has-frame .scene__canvas { opacity: 1; }

/* Asienta la escena por abajo, para que el prado no termine en un corte seco
   contra el crema de la sección siguiente — y sostiene el contraste de lo que
   vive ahí abajo, que es lo que manda en los números.

   El 46% / 0.86 está medido, no elegido: en gran-día la tarjeta de la recepción
   se apoya en la franja de césped iluminado y en las luces de verbena, y con el
   30% / 0.45 de antes su hora en --sand caía a 3.1:1. En vestimenta la nota del
   jardín se apoya en lo mismo. Los dos clips terminan igual de encendidos por
   abajo, así que el degradado es uno solo. Bajarlo deja ver más jardín y menos
   texto.

   El tinte ya no es el azul frío de antes: rgb(28,15,27) es --night-floor, el
   suelo de la escala. Así el jardín aterriza exactamente en el color donde
   arranca el puente del RSVP —ver .section--night::before— y ese empalme deja
   de ser un corte. Los 6 puntos que subió el alfa son para cerrar ese mismo
   encuentro; de paso el texto de la tarjeta gana, no pierde. */
.scene__vignette {
  position: absolute;
  inset: auto 0 0 0;
  height: 46%;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(to bottom, rgba(28, 15, 27, 0), rgba(28, 15, 27, 0.86));
}

/* ---------- las tarjetas ---------- */

.scene__cards {
  position: relative;
  z-index: 3;
  display: grid;
  gap: 1.75rem;
  padding: clamp(2.5rem, 8vw, 5rem) var(--pad);
}

/* En vivo las dos ocupan la misma celda, que llena el alto del panel: la de la
   ceremonia se ancla arriba y la de la recepción abajo, para que al final del
   clip las dos estén en pantalla y se pueda llegar a cualquiera de las dos
   direcciones en cualquier momento. Ya no se cruzan ni se relevan — el cruce
   solo existía porque compartían sitio.

   env() las mantiene fuera del notch y de la barra de Safari. */
.scene.is-live .scene__cards {
  position: absolute;
  inset: 0;
  grid-template-rows: minmax(0, 1fr);   /* una fila que llena: es lo que separa arriba de abajo */
  gap: 0;
  padding: max(1.5rem, env(safe-area-inset-top)) var(--pad)
           max(1.5rem, env(safe-area-inset-bottom));
  pointer-events: none;
}
.scene.is-live .glass {
  grid-area: 1 / 1;
  opacity: 0;
  visibility: hidden;   /* con opacity 0 el navegador seguiría calculando el
                           backdrop-filter en cada frame */
}
.scene.is-live .glass--day   { align-self: start; }
.scene.is-live .glass--night { align-self: end; }
.scene.is-live .glass.is-on  { pointer-events: auto; }

.glass {
  justify-self: center;
  width: min(94%, 30rem);
  padding: clamp(1.6rem, 5.5vw, 2.2rem) clamp(1.25rem, 5vw, 2rem);
  text-align: center;
  border-radius: 26px;
  border: 1px solid;
  /* saturate compensa el lavado que produce el propio desenfoque */
  backdrop-filter: blur(20px) saturate(135%);
  -webkit-backdrop-filter: blur(20px) saturate(135%);
}

/* Las opacidades no son estéticas, están medidas: se muestrea el brillo real
   del video bajo cada tarjeta —en el sitio donde vive y en los frames en que se
   ve—, en rejilla de 8x8, y estos son los valores mínimos que mantienen todo el
   texto por encima de 4.5:1 incluso en la peor celda. Bajarlas rompe la
   accesibilidad.

   La de día ya no se releva con la de abajo, así que aguanta el clip entero:
   su 0.64 se sostiene en 4.68:1 de punta a punta, medido, y no hizo falta
   tocarlo. La de noche sí cambió, porque cambió de sitio: pegada abajo se apoya
   en el césped iluminado en vez de en el cielo, y su 0.42 de cuando iba arriba
   caía a 2.7:1. Lo que la rescata es sobre todo el vignette (ver
   .scene__vignette); este 0.46 es el empujón mínimo que hacía falta encima.

   Este 0.46 aguanta del frame 25 en adelante (4.7:1) pero no la apertura de
   día (3.3:1). Como el bucle se lleva el clip entero, para esos 25 frames
   claros el cristal se cierra a 0.58 —el mínimo medido que los sostiene— y en
   cuanto pasan vuelve a abrirse. Lo enciende y lo apaga .is-bright, que pone
   el JS mirando qué frame se está viendo (data-dim-until en el HTML). */
.glass--day {
  background: rgba(255, 252, 246, 0.64);
  border-color: rgba(255, 255, 255, 0.6);
  color: var(--ink);
  box-shadow:
    0 20px 50px rgba(46, 34, 20, 0.28),
    inset 0 1px 0 rgba(255, 255, 255, 0.8);   /* el filo de luz: lo que lo hace leer como cristal */
}
.glass--night {
  background: rgba(24, 14, 22, 0.46);
  border-color: rgba(179, 155, 110, 0.45);
  color: var(--paper);
  box-shadow:
    0 24px 60px rgba(0, 0, 0, 0.5),
    inset 0 1px 0 rgba(255, 255, 255, 0.2);
}

/* El tramo claro del bucle. Medio segundo de transición: sobre un ciclo de
   siete se lee como que la tarjeta respira con la luz, no como un parpadeo.
   Va antes de que llegue el prado —SCENE_DIM_LEAD en el JS— porque una
   transición que empieza tarde deja el texto corto justo donde hacía falta. */
.scene.is-bright .glass--night { background: rgba(24, 14, 22, 0.58); }
.glass--night { transition: background 0.5s ease; }

@media (prefers-reduced-motion: reduce) {
  .glass--night { transition: none; }
}

/* En la versión estática las dos tarjetas caen sobre el primer frame, que es de
   día: la de noche pierde contraste sobre una nube clara (3.5:1), porque está
   calibrada para el cielo nocturno. Solo ahí se cierra un poco más. */
.scene:not(.is-live) .glass--night { background: rgba(24, 14, 22, 0.62); }

/* Sin desenfoque el fondo se ve entero a través del cristal y el texto se
   pierde: se cierra casi del todo. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .glass--day   { background: rgba(250, 246, 238, 0.93); }
  .glass--night { background: rgba(26, 16, 24, 0.88); }
}

/* ---------- el contenido de la tarjeta ---------- */

.venue__icon {
  width: 42px;
  height: 42px;
  margin: 0 auto 1.5rem;
  color: var(--gold);
}
.venue__title {
  margin: 0 0 1rem;
  font-family: var(--font-ui);
  font-size: 0.7rem;
  font-weight: 400;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.venue__time {
  margin: 0 0 1rem;
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 6vw, 3.4rem);
  font-weight: 300;
  line-height: 1;
  color: var(--plum);
}
.venue__time small {
  font-size: 0.32em;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  vertical-align: 0.9em;
  margin-left: 0.2em;
  color: var(--ink-soft);
}
.venue__name {
  margin: 0 0 0.6rem;
  font-family: var(--font-display);
  font-size: 1.35rem;
  color: var(--ink);
}
.venue__address {
  margin: 0 0 2rem;
  font-size: 0.9rem;
  color: var(--ink-soft);
}

/* Mismas familias y misma escala que arriba; solo cambia el color, porque
   --ink-soft y --plum no aguantan el contraste sobre cristal (2.6:1 en el
   peor frame). La jerarquía la llevan el tamaño y el peso. */

.glass--day .venue__title,
.glass--day .venue__name,
.glass--day .venue__address,
.glass--day .venue__time small { color: var(--ink); }
.glass--day .venue__time       { color: var(--deep); }
.glass--day .venue__icon       { color: #8A7343; }

.glass--night .venue__title,
.glass--night .venue__address,
.glass--night .venue__time small { color: rgba(251, 245, 234, 0.85); }
.glass--night .venue__name       { color: var(--paper); }
.glass--night .venue__time       { color: var(--sand); }
.glass--night .venue__icon       { color: var(--gold); }

.glass .venue__icon    { width: 34px; height: 34px; margin-bottom: 1.1rem; }
.glass .venue__time    { font-size: clamp(2.3rem, 8vw, 3rem); }
.glass .venue__address { margin-bottom: 1.6rem; }

/* ---------- las dos a la vez ----------

   Una tarjeta completa mide ~345px y dos no caben en un iPhone SE (667px). En
   cuanto entra la segunda —.is-closed lo pone el JS con data-compact-at, no al
   final del clip: si esperara, las dos completas se saldrían de pantalla
   durante todo el tramo en que conviven— las dos se recogen a ~280px.

   Se va el icono y se aprietan dos márgenes. No se toca ni una palabra: hora,
   lugar, dirección y botón de mapa siguen enteros, que es a lo que se viene.

   El icono colapsa con max-height en vez de display:none porque así transiciona
   en lugar de dar un salto. overflow:hidden es lo que lo recorta mientras se
   cierra. */
.scene.is-closed .glass .venue__icon {
  max-height: 0;
  margin-bottom: 0;
  opacity: 0;
  overflow: hidden;
}
.scene.is-closed .glass .venue__title   { margin-bottom: 0.7rem; }
.scene.is-closed .glass .venue__address { margin-bottom: 1rem; }

/* Con eso la tarjeta baja de 345px a 323px, que basta de un iPhone 14 para
   arriba: las dos quedan con 150px de aire. En una pantalla corta —SE, 8, o
   cualquiera con la barra de Safari a la vista— aún se solapan 27px, así que
   ahí se aprieta un punto más: paddings y márgenes al mínimo y la hora un poco
   menor. Quedan en ~288px y se separan 43px.

   Solo aquí y no siempre porque en pantalla larga apretarlas no gana nada y la
   tarjeta se lee peor. La información sigue sin tocarse en ninguno de los dos
   casos. */
@media (max-height: 740px) {
  .scene.is-closed .glass {
    padding-block: 1.05rem;
  }
  .scene.is-closed .glass .venue__time {
    font-size: clamp(2rem, 7vw, 2.6rem);
    margin-bottom: 0.5rem;
  }
  .scene.is-closed .glass .venue__name { margin-bottom: 0.35rem; }
}

/* Que se acomoden en vez de saltar. Solo estas tres: animar el alto de algo con
   backdrop-filter detrás sale caro, y con esto basta para que se lea como que
   la tarjeta se recoge. */
.glass .venue__icon {
  transition: max-height 0.45s ease, margin-bottom 0.45s ease, opacity 0.3s ease;
}
.glass .venue__title,
.glass .venue__name,
.glass .venue__address { transition: margin-bottom 0.45s ease; }
.glass { transition: padding-block 0.45s ease; }
.glass .venue__time { transition: font-size 0.45s ease, margin-bottom 0.45s ease; }

@media (prefers-reduced-motion: reduce) {
  .glass,
  .glass .venue__icon,
  .glass .venue__title,
  .glass .venue__name,
  .glass .venue__time,
  .glass .venue__address { transition: none; }
}

/* El botón hereda .btn pero el plum desaparece sobre cristal, en los dos casos */
.glass--day .btn {
  border-color: var(--deep);
  color: var(--deep);
}
.glass--day .btn:hover,
.glass--day .btn:focus-visible { background: var(--deep); color: var(--paper); }

.glass--night .btn {
  border-color: rgba(244, 215, 180, 0.75);
  color: var(--sand);
}
.glass--night .btn:hover,
.glass--night .btn:focus-visible {
  background: var(--sand);
  color: var(--deep);
  border-color: var(--sand);
}

/* ---------- escena con el texto en los bordes ----------

   La variante de vestimenta: sin cristal, el texto va desnudo sobre el clip,
   uno arriba y otro abajo. Va después del bloque de arriba a propósito, porque
   .scene--edges.is-live tiene que ganarle a .scene.is-live.
   ============================================================ */

/* El recorrido es el de siempre: ya no necesita más que gran-día. Antes lo
   necesitaba porque el scroll no solo pasaba frames, también escribía, y con
   320vh los trazos salían atropellados; ahora el ritmo del trazo lo pone
   data-duration en el HTML y el scroll no tiene nada que ver. */

/* En pantalla ancha el recorte "cover" tiene que sacrificar altura, y centrado
   se comía a los perros con los tenis — que son justo de lo que habla la
   sección. El encuadre baja al 72%: se pierden las caras y se ganan los zapatos.
   El canvas hace lo mismo con data-focus en el HTML; esto es para el póster, que
   es lo que se ve hasta que pinta el primer frame. */
.scene--edges .scene__poster { object-position: 50% 72%; }

/* La rejilla que llena el alto ya viene de .scene.is-live; aquí solo cambia el
   respiro contra los bordes, que sin cristal detrás tiene que ser mayor. */
.scene--edges.is-live .scene__cards {
  padding: max(1.75rem, env(safe-area-inset-top)) var(--pad)
           max(2rem, env(safe-area-inset-bottom));
}
.scene--edges.is-live .scene__text {
  grid-area: 1 / 1;
  opacity: 0;
  visibility: hidden;
}
.scene__text {
  justify-self: center;
  width: min(94%, 32rem);
  text-align: center;
}
.scene__text--top    { align-self: start; }
.scene__text--bottom { align-self: end; }

/* Sin cristal detrás, el contraste lo sostienen los dos degradados y la sombra.
   El de arriba muerde el cielo del anochecer, que ya es oscuro; el de abajo
   tiene que tapar el césped iluminado, así que cierra más. */
.scene__scrim {
  position: absolute;
  inset: 0 0 auto 0;
  height: 42%;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(to bottom, rgba(28, 15, 27, 0.58), rgba(28, 15, 27, 0));
}
/* El 46% / 0.80 que esta escena necesitaba —la nota se apoya en la franja de
   césped iluminado y en los tenis blancos, y con 0.66 el peor pixel la dejaba
   en 4.29:1— es ahora el degradado de base, porque gran-día acabó necesitando
   exactamente el mismo. Ver .scene__vignette. */

/* Mismas familias y misma escala que la sección de siempre; solo cambia el
   color, porque ahora el texto vive sobre el vídeo. */
.scene__text .section__kicker    { color: rgba(251, 245, 234, 0.82); }
.scene__text .dresscode          { color: var(--paper); }
.scene__text .dresscode__note    { color: rgba(251, 245, 234, 0.94); }
.scene__text .dresscode__note em { color: var(--sand); }   /* --plum se hunde sobre el césped */
.scene__text p { text-shadow: 0 2px 18px rgba(28, 15, 27, 0.6); }

.scene__text .dresscode__rule { margin: 1.6rem auto 0; }
/* Las dos rayitas doradas —la del kicker y la del dress code— se dibujan al
   ritmo del bloque: --wp es su progreso de escritura y lo escribe paintStroke().
   Si se quedaran puestas desde el principio delatarían el texto antes de que
   llegue la tinta. Sin JS no hay --wp y salen enteras, que es lo correcto. */
.scene--edges.is-live .scene__text .section__kicker::after,
.scene--edges.is-live .scene__text .dresscode__rule {
  transform: scaleX(var(--wp, 1));
  transform-origin: center;
}

/* ---------- el trazo ----------

   Cada palabra lleva su propia máscara, no una sola sobre el bloque: así el
   barrido cruza una palabra, salta a la siguiente y baja de renglón con el
   texto, en vez de recorrer el párrafo en diagonal. El troceado lo hace
   splitWords() en js/main.js. */
.ink {
  --p: 0;        /* 0 = sin trazar, 1 = trazada. Lo escribe paintStroke() */
  --soft: 22%;   /* difuminado de la punta: es lo que la hace tinta y no un corte */
  --a: calc(var(--p) * (100% + var(--soft)) - var(--soft));
  --b: calc(var(--p) * (100% + var(--soft)));

  display: inline-block;
  /* Caja de máscara más ancha que la palabra, sin tocar el layout: los remates
     de la script se salen del cuadro y si no, se quedan decapitados. Los
     márgenes negativos devuelven exactamente lo que añade el padding. */
  padding: 0.34em 0.14em;
  margin:  -0.34em -0.14em;

  -webkit-mask-image: linear-gradient(to right, #000 var(--a), transparent var(--b));
          mask-image: linear-gradient(to right, #000 var(--a), transparent var(--b));
}

/* Sin máscaras el texto se vería entero desde el primer momento, que es justo lo
   que la escena no quiere: se degrada a un fundido palabra por palabra. */
@supports not ((mask-image: linear-gradient(#000, #000)) or
               (-webkit-mask-image: linear-gradient(#000, #000))) {
  .ink {
    -webkit-mask-image: none;
            mask-image: none;
    opacity: var(--p);
  }
}

/* ---------- sin JS, o con movimiento reducido ----------
   initScrollScenes() sale antes: no hay .is-live, no hay troceado y no hay
   .ink. Los dos bloques caen en flujo normal, uno detrás de otro, sobre el
   frame 0.

   Aquí el texto NO vuelve a tinta oscura como en la escena de la ceremonia: el
   frame 0 de este clip ya es de anochecer y la tinta se perdía sobre el traje y
   el vestido. Lo que cambia es el fondo — el degradado de arriba pasa a cubrir
   todo el bloque, porque ahora el texto no vive en los bordes sino encima de la
   imagen entera. La información nunca depende del JavaScript. */
.scene:not(.is-live) .scene__cards { padding-block: clamp(2.5rem, 9vw, 5rem); }
.scene--edges:not(.is-live) .scene__scrim {
  height: 100%;
  background: rgba(28, 15, 27, 0.58);
}

/* ============================================================
   DRESS CODE

   La tipografía de la sección de vestimenta. Vive aquí y no en el bloque de la
   escena porque no depende de ella: son las familias y la escala del sitio, y
   siguen puestas cuando el texto cae en flujo normal sin JS. Lo que la escena
   cambia arriba es solo el color, que sobre vídeo tiene que ser claro.
   ============================================================ */

.dresscode {
  margin: 0;
  text-align: center;
  font-family: var(--font-display);
  font-size: clamp(3rem, 11vw, 5.5rem);
  font-weight: 300;
  line-height: 1;
  letter-spacing: 0.02em;
  color: var(--ink);
}
.dresscode__rule {
  width: 60px;
  height: 1px;
  margin: 2rem auto;
  background: var(--gold);
}
.dresscode__note {
  margin: 0 auto;
  max-width: 34ch;
  text-align: center;
  font-family: var(--font-display);
  font-size: clamp(1.15rem, 2.6vw, 1.35rem);
  color: var(--ink-soft);
}
.dresscode__note em {
  font-family: var(--font-script);
  font-style: normal;
  font-size: 1.6em;
  color: var(--plum);
  line-height: 1;
}

/* ============================================================
   GALLERY — the reel

   A strip of photos that drifts on its own. Three transforms, three layers,
   one job each, so they never fight:

     .reel__stage   translateX(--recenter)  CSS transition — centres the open photo
     .reel__track   translate3d(x)          rAF, no transition — the drift
     .reel__card    push / tilt / scale     CSS transition — straighten, grow, shove aside

   The tilt lives in --rot/--dy per card and is switched off by --calm rather
   than overridden, so the per-card scatter table below and the state classes
   never collide on specificity.
   ============================================================ */

/* One screenful, laid out as a scene: title up top, strip filling whatever is
   left. Anything taller and the open photo drops below the fold, away from its
   own title — which is the whole picture the section is trying to make. */
.section--reel {
  position: relative;
  overflow: hidden;          /* the strip runs off both edges of the page */
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  padding-block: clamp(2.5rem, 7vh, 5rem);
  transition: background-color 0.85s ease;
}
/* La noche cerrada. Mismo truco que .section--dusk: se redefinen los tokens
   localmente y todo lo de dentro los sigue, sin repintar nada a mano.

   El bloque lo comparten los dos sitios que se van a noche —la galería mientras
   hay una foto abierta y el RSVP, que se queda ahí siempre— para que la paleta
   nocturna viva en un solo lugar. Ver .section--night más abajo.

   Sobre berenjena el plum de la página (#6A4062) desaparecería: es casi tan
   oscuro como el fondo. Así que aquí sube a bugambilia clara, que es el mismo
   color visto a plena luz — 6.91:1 sobre --deep. Es el acento de toda la noche:
   fondo del .btn--solid, foco de los campos del RSVP y los anillos de las
   opciones. */
.section--reel.is-open,
.section--night {
  background: var(--deep);
  --ink: var(--paper);
  --ink-soft: rgba(251, 245, 234, 0.66);
  --plum: #DE95BC;   /* bugambilia clara */
  --gold: #CBB287;
  --line: rgba(251, 245, 234, 0.2);
}
.section--reel .section__kicker,
.section--reel .script-line { transition: color 0.85s ease; }

/* ---------- heading: "Nuestra historia" ---------- */

/* Las fotos no llevan título, así que aquí sólo vive la línea de la sección;
   se apaga mientras hay una foto abierta para dejarla sola en pantalla. */
.reel__heads {
  display: grid;
  margin-bottom: 0;
}
.reel__heads > * {
  grid-area: 1 / 1;
  align-self: center;
  margin: 0;
}
.section--reel.is-open [data-reel-idle] { opacity: 0; }

/* ---------- the strip ---------- */

.reel {
  --card-h: clamp(180px, 34vh, 340px);
  --gap: clamp(0.5rem, 1.6vw, 1.1rem);
  --bar-h: clamp(3.5rem, 7vh, 5rem);

  position: relative;
  flex: 1 1 auto;                            /* takes whatever the heading leaves */
  min-height: clamp(300px, 46vh, 520px);
  margin-top: clamp(1.25rem, 3vw, 2rem);
  touch-action: pan-y;                       /* vertical scroll passes through; we own the x axis */
  user-select: none;
  -webkit-user-select: none;
  cursor: grab;
}
.reel.is-dragging { cursor: grabbing; }

/* Taken out of flow on purpose: the track is far wider than the window, and in
   flow that width would leak into .reel and drag the bottom bar off centre.
   Pinned at inset 0, so a card's offsetLeft still reads from the reel's left
   edge — which is the geometry the JS centres against. */
.reel__stage {
  position: absolute;
  inset: 0 0 var(--bar-h) 0;   /* la barra tiene su propio hueco: la foto abierta nunca se le monta */
  display: flex;
  align-items: center;
  transform: translate3d(var(--recenter, 0px), 0, 0);
  transition: transform 0.9s cubic-bezier(0.2, 0.7, 0.3, 1);
  will-change: transform;
}
.reel__track {
  display: flex;
  align-items: center;
  gap: var(--gap);
  width: max-content;
  will-change: transform;    /* transform itself is written per frame from JS */
}

.reel__card {
  --rot: 0deg;
  --dy: 0px;
  --calm: 1;                 /* 1 = scattered, 0 = squared up */
  --sc: 1;
  --push: 0px;

  flex: 0 0 auto;
  position: relative;
  height: var(--card-h);
  aspect-ratio: var(--ar, 3 / 4);
  margin: 0;
  padding: 0;
  border: 0;
  background: var(--paper-tint);
  overflow: hidden;
  cursor: inherit;
  touch-action: pan-y;       /* same deal as the reel: the x axis is ours */
  -webkit-tap-highlight-color: transparent;

  transform:
    translate3d(var(--push), calc(var(--dy) * var(--calm)), 0)
    rotate(calc(var(--rot) * var(--calm)))
    scale(var(--sc));
  box-shadow: 0 10px 28px rgba(38, 38, 31, 0.16);
  transition:
    transform 0.75s cubic-bezier(0.2, 0.7, 0.3, 1),
    opacity 0.6s ease,
    box-shadow 0.6s ease;
}
.reel__card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;      /* every pointer event belongs to the button */
  -webkit-user-drag: none;   /* or the browser starts an image drag mid-swipe */
}

/* Pre-selected: everything halts and this one squares up */
.reel__card.is-near {
  --calm: 0;
  --sc: 1.05;
  z-index: 3;
  box-shadow: 0 22px 55px rgba(38, 38, 31, 0.28);
}
/* Opened: centred and grown (--open-sc comes from JS, sized to the viewport) */
.reel__card.is-open {
  --calm: 0;
  --sc: var(--open-sc, 1.5);
  z-index: 5;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45);
}
/* …and the rest are shoved off to their side (--push comes from JS) */
.reel.is-open .reel__card:not(.is-open) { opacity: 0.4; }

/* ---------- the scatter ----------
   Twelve rules for twelve photos, so a clone at position 13 lands on 12n+1 and
   matches original 1 exactly — the loop seam stays invisible. Hand-tuned, not
   random, so the strip reads the same on every load. */
.reel__card:nth-child(12n + 1)  { --ar: 3 / 4; --rot: -6deg; --dy: -14px; }
.reel__card:nth-child(12n + 2)  { --ar: 1 / 1; --rot:  4deg; --dy:  10px; margin-inline-start: -1.1vw; }
.reel__card:nth-child(12n + 3)  { --ar: 2 / 3; --rot: -3deg; --dy:  -4px; }
.reel__card:nth-child(12n + 4)  { --ar: 4 / 3; --rot:  7deg; --dy:  16px; }
.reel__card:nth-child(12n + 5)  { --ar: 3 / 4; --rot: -8deg; --dy:   2px; margin-inline-start: -1.4vw; }
.reel__card:nth-child(12n + 6)  { --ar: 1 / 1; --rot:  3deg; --dy: -18px; }
.reel__card:nth-child(12n + 7)  { --ar: 4 / 5; --rot: -5deg; --dy:   8px; }
.reel__card:nth-child(12n + 8)  { --ar: 3 / 2; --rot:  6deg; --dy:  -8px; margin-inline-start: -1.1vw; }
.reel__card:nth-child(12n + 9)  { --ar: 2 / 3; --rot: -2deg; --dy:  14px; }
.reel__card:nth-child(12n + 10) { --ar: 1 / 1; --rot:  8deg; --dy: -12px; }
.reel__card:nth-child(12n + 11) { --ar: 4 / 3; --rot: -7deg; --dy:   4px; margin-inline-start: -1.4vw; }
.reel__card:nth-child(12n + 12) { --ar: 3 / 4; --rot:  5deg; --dy:  -6px; }

/* Placeholder texture shown while no photo exists */
.reel__card:not(:has(img)) {
  background: linear-gradient(135deg, #EFEADC 0%, #E3DCCB 50%, #EDE7D9 100%);
}
.reel__card:not(:has(img))::after {
  content: "J & F";
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--font-script);
  font-size: 2rem;
  color: rgba(38, 38, 31, 0.18);
}

/* ---------- bottom bar ---------- */

.reel__bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--bar-h);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(0.75rem, 5vw, 3.5rem);
  padding-inline: var(--pad);
  pointer-events: none;      /* the strip behind stays draggable */
  z-index: 6;
}
.reel__bar > * { pointer-events: auto; }

/* El hueco central de la barra: sólo el botón de cerrar */
.reel__slot {
  display: grid;
  min-width: 0;
  pointer-events: none;
}
.reel__slot > * {
  grid-area: 1 / 1;
  align-self: center;
  justify-self: center;
}


.reel__nav {
  flex: 0 0 auto;
  width: 2.75rem;
  height: 2.75rem;
  border: 0;
  background: none;
  color: var(--ink-soft);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.45s ease, color 0.3s ease, transform 0.3s ease;
}
.reel__nav[hidden] { display: none; }
.reel.is-open .reel__nav { opacity: 1; }
.reel__nav:hover,
.reel__nav:focus-visible { color: var(--ink); }
.reel__nav:hover:first-child { transform: translateX(-3px); }
.reel__nav:hover:last-child { transform: translateX(3px); }

.reel__close {
  border: 0;
  background: none;
  padding: 0.6rem 1rem;
  pointer-events: auto;
  font-family: var(--font-ui);
  font-size: 0.66rem;
  font-weight: 400;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--ink-soft);
  cursor: pointer;
  transition: color 0.3s ease;
}
.reel__close[hidden] { display: none; }
.reel__close:hover,
.reel__close:focus-visible { color: var(--ink); }

@media (max-width: 700px) {
  /* Big enough to fill the band, small enough to leave the open photo somewhere
     to grow into — a phone has height to spare but almost no width. */
  .reel { --card-h: clamp(170px, 32vh, 300px); }
  .reel__bar { gap: 0.5rem; }
}

/* ============================================================
   RSVP — la sección de noche

   La única de la página que se queda en berenjena cerrada: hereda los tokens de
   .section--night (arriba, compartidos con la galería abierta) y con eso el
   kicker, la frase, el lede y los filetes ya salen bien sin tocarlos.

   Lo que no sale solo son los botones sólidos: ver el bloque de contraste.
   ============================================================ */

/* ---------- el empalme con el jardín ----------
   Encima de esta sección termina el clip de vestimenta, y su viñeta cierra en
   --night-floor. Hasta ahora ahí había un corte seco: casi negro contra
   berenjena, sin nada en medio. Era la única unión de la página sin capa
   propia — el hero tiene dos (.hero__seam y .section--dusk::before) justo para
   no hacer esto.

   Así que el RSVP arranca en el mismo color donde el jardín aterriza y desde
   ahí se abre. No es un fundido para esconder la unión: es la sección
   encendiéndose. La página termina subiendo —jardín, --deep, el plum del
   pie— en vez de cerrarse de golpe.

   Va en su propia regla y no en el bloque de tokens de arriba porque aquel lo
   comparte con .section--reel.is-open, y la galería no quiere este degradado:
   ella se abre desde el crema, no desde la noche.

   Alto en vh y no en %: la sección crece con el formulario —los campos se
   despliegan al elegir "sí"— y en % el degradado se estiraría con ella. En vh
   el empalme mide siempre lo mismo, que es lo único que tiene que hacer. */
.section--night { position: relative; }
.section--night::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 26vh;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    var(--night-floor)  0%,   /* donde cierra la viñeta del jardín */
    #241429            45%,   /* el paso intermedio: sin él la rampa se ve */
    var(--deep)       100%
  );
}
/* Levanta el contenido sobre el degradado. .reveal ya crea contexto de
   apilamiento con su transform, así que el z-index tiene que ser explícito
   —mismo motivo que en .section--dusk > .wrap. */
.section--night > .wrap {
  position: relative;
  z-index: 1;
}


/* Sobre noche --plum vale #DE95BC, así que el .btn--solid de fábrica queda
   crema sobre bugambilia (1.97:1) y su hover, crema sobre crema (1:1). Estos
   dos pares están medidos: 6.91:1 en reposo y 7.76:1 en hover. */
.section--night .btn--solid {
  background: var(--plum);
  border-color: var(--plum);
  color: var(--deep);
}
.section--night .btn--solid:hover,
.section--night .btn--solid:focus-visible {
  background: var(--gold);
  border-color: var(--gold);
  color: var(--deep);
}

/* La tarjeta. Toma prestado el lenguaje de .glass —el radio y el filete
   dorado— pero no la clase: aquellas opacidades están medidas contra los
   frames de los vídeos, y un backdrop-filter sobre color plano no hace nada. */
.rsvp__card {
  padding: clamp(1.75rem, 6vw, 2.75rem);
  border: 1px solid rgba(179, 155, 110, 0.45);
  border-radius: 26px;
  background: rgba(251, 245, 234, 0.04);
}

.rsvp { display: grid; gap: 1.75rem; }

.rsvp__fields { display: grid; gap: 1.75rem; }

.rsvp__field { display: grid; gap: 0.55rem; }

.rsvp label,
.rsvp legend {
  font-size: 0.68rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.rsvp .opt { text-transform: none; letter-spacing: 0.04em; opacity: 0.7; }

.rsvp input[type="text"],
.rsvp textarea {
  width: 100%;
  padding: 0.75rem 0.25rem;
  font-family: var(--font-display);
  font-size: 1.15rem;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  transition: border-color 0.3s ease;
}
.rsvp input:focus,
.rsvp textarea:focus {
  outline: none;
  border-bottom-color: var(--plum);
}
.rsvp textarea { resize: vertical; }

/* ---------- paso 1: el sí/no ----------
   Radios de verdad, para conservar el grupo y las flechas del teclado. El input
   se saca de la vista pero no del foco: el anillo lo pinta la caja. */

.rsvp__choice {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  margin: 0;
  padding: 0;
  border: 0;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* El radio se saca de la vista pero no del foco: opacity 0 lo deja tabulable,
   y position:relative en la etiqueta lo mantiene anclado a su propia caja — si
   se fuera al ancestro posicionado, enfocarlo con el teclado daría un salto de
   scroll hasta donde hubiera aterrizado. */
.choice,
.pill { position: relative; }

.choice input,
.pill input {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
}

.choice__box {
  display: grid;
  place-items: center;
  min-height: 5.5rem;
  padding: 1.2rem 1rem;
  text-align: center;
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 3vw, 1.3rem);
  line-height: 1.25;
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 20px;
  cursor: pointer;
  transition: border-color 0.35s ease, background 0.35s ease;
}
.choice input:checked + .choice__box {
  border-color: var(--gold);
  background: rgba(203, 178, 135, 0.12);
}
.choice input:focus-visible + .choice__box {
  outline: 2px solid var(--plum);
  outline-offset: 3px;
}

/* ---------- paso 2 ----------
   Plegado con 0fr → 1fr, que sí transiciona, en vez de una max-height
   inventada. Abierto por defecto: lo pliega initRsvp() con .is-stepped, así un
   JS que no llega deja los campos a la vista en lugar de esconderlos. */

.rsvp__rest {
  display: grid;
  grid-template-rows: 1fr;
}
/* visibility, no solo opacidad: con la caja a cero pero visible, el tabulador
   seguía entrando en unos campos que no se ven. Va sin duración y con retardo
   solo al plegarse, para que se apague cuando el plegado ya terminó y se
   encienda de inmediato al abrir. */
.rsvp.is-stepped .rsvp__rest {
  grid-template-rows: 0fr;
  opacity: 0;
  visibility: hidden;
  transition: grid-template-rows 0.6s cubic-bezier(0.2, 0.7, 0.3, 1),
              opacity 0.4s ease,
              visibility 0s linear 0.6s;
}
.rsvp.is-stepped .rsvp__rest > * {
  overflow: hidden;      /* el hijo es quien recorta mientras la fila vale 0fr */
  min-height: 0;
  /* Ese overflow recortaría el anillo de foco de lo que toca los bordes —la
     primera pastilla, el botón—, así que se abre un margen de 6px y se devuelve
     con margen negativo: el contenido no se mueve y el anillo cabe. */
  margin: 0 -6px -6px;
  padding: 0 6px 6px;
}
.rsvp.is-stepped[data-answer] .rsvp__rest {
  grid-template-rows: 1fr;
  opacity: 1;
  visibility: visible;
  transition: grid-template-rows 0.6s cubic-bezier(0.2, 0.7, 0.3, 1),
              opacity 0.5s ease 0.15s,
              visibility 0s;
}

/* Plegado, el hueco de .rsvp seguiría poniendo su separación arriba y abajo de
   una caja de altura cero: el doble de aire donde no hay nada. Se encoge, y se
   abre con el resto. */
.rsvp.is-stepped { transition: gap 0.6s cubic-bezier(0.2, 0.7, 0.3, 1); }
.rsvp.is-stepped:not([data-answer]) { gap: 0.9rem; }

/* Los lugares son cosa de quien viene */
.rsvp[data-answer="no"] .rsvp__seats { display: none; }

.rsvp__seats {
  margin: 0;
  padding: 0;
  border: 0;
}
.rsvp__pills { display: flex; gap: 0.75rem; }

.pill__box {
  display: grid;
  place-items: center;
  width: 3.4rem;
  height: 3.4rem;
  font-family: var(--font-display);
  font-size: 1.35rem;
  /* Cormorant trae cifras antiguas de serie y su "1" es prácticamente una "I"
     versalita: junto al "2" no se leía como un número. */
  font-variant-numeric: lining-nums;
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 50%;
  cursor: pointer;
  transition: border-color 0.35s ease, background 0.35s ease;
}
.pill input:checked + .pill__box {
  border-color: var(--gold);
  background: rgba(203, 178, 135, 0.12);
}
.pill input:focus-visible + .pill__box {
  outline: 2px solid var(--plum);
  outline-offset: 3px;
}

/* ---------- el pie ---------- */

/* El hueco se reserva vacío para que un aviso no empuje el botón al aparecer.
   El margen negativo se come parte de la separación de .rsvp: con la reserva
   entera, el pie de la tarjeta pesaba el doble que su cabecera. */
.rsvp__status {
  margin: -0.85rem 0 0;
  min-height: 1.4em;
  text-align: center;
  font-size: 0.9rem;
  color: var(--ink-soft);
}
/* Terracota clara: el #9B4A3A de la versión en crema se hunde sobre la
   berenjena. El aviso vive dentro de la tarjeta, así que se mide contra el
   fondo de la tarjeta (#382436), no contra --deep: ahí llega a 6.86:1. Sigue
   leyéndose como aviso y no como el dorado de la página. */
.rsvp__status.is-error { color: #E7A491; }

/* ---------- confirmado ----------
   El formulario se funde y en su sitio entra el mensaje, y ahí acaba: no hay
   vuelta al formulario. La tarjeta se queda donde está, sin el salto de altura
   que dejaba el vaciado a pelo. */

.rsvp.is-sent {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.45s ease;
}
/* `hidden` por sí solo no puede con el `display: grid` de .rsvp */
.rsvp[hidden] { display: none; }

.rsvp__sent {
  text-align: center;
  animation: rsvpSent 0.7s cubic-bezier(0.2, 0.7, 0.3, 1) both;
}
.rsvp__sent[hidden] { display: none; }

.rsvp__sent-message {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.2rem, 3.5vw, 1.5rem);
  line-height: 1.5;
  color: var(--ink);
}

@keyframes rsvpSent {
  from { opacity: 0; transform: translateY(0.75rem); }
  to   { opacity: 1; transform: none; }
}

/* Los dos botones del paso 1 no caben en fila en un teléfono estrecho: antes de
   dejar que "Sí, ahí estaré" se parta en tres líneas, se apilan. */
@media (max-width: 380px) {
  .rsvp__choice { grid-template-columns: 1fr; }
  .choice__box { min-height: 0; padding: 1.1rem 1rem; }
}

/* ============================================================
   FOOTER
   ============================================================ */

.footer {
  padding: clamp(3.5rem, 8vw, 6rem) var(--pad);
  text-align: center;
  background: var(--plum);
  color: var(--paper);
}
/* El escudo viene ya pintado en crema (--paper) sobre transparente, porque el
   original es tinta negra y aquí el fondo es el plum. */
.footer__logo {
  display: block;
  width: clamp(180px, 46vw, 240px);
  height: auto;
  margin: 0 auto 1.5rem;
}
.footer__names {
  margin: 0 0 0.4rem;
  font-family: var(--font-display);
  font-size: 1.15rem;
}
.footer__date {
  margin: 0;
  font-size: 0.66rem;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  opacity: 0.75;
}

/* ============================================================
   SCROLL REVEAL
   ============================================================ */

.reveal {
  opacity: 0;
  transform: translateY(1.5rem);
  transition: opacity 0.9s ease, transform 0.9s cubic-bezier(0.2, 0.7, 0.3, 1);
}
.reveal.is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .reveal { opacity: 1; transform: none; }
  .hero__name, .hero__amp, .hero__date { opacity: 1; }

  /* La escena no se activa (initScrollScenes ni siquiera descarga los 90
     frames), pero por si acaso: las dos tarjetas visibles y en flujo. */
  .scene .glass { opacity: 1; visibility: visible; }

  /* La cinta deja de moverse sola y las fotos se enderezan. Arrastrar y abrir
     siguen funcionando — son gestos del usuario, no movimiento involuntario —
     pero sin transiciones, que la regla de arriba ya anuló. */
  .reel__card { --calm: 0; }
}
