:root {
  --paper: #f1ede4;
  --ink: #191713;
  --wine: #6d1425;
  --sage: #606653;
  --line: rgba(25, 23, 19, 0.22);
  --serif: "Italiana", "Times New Roman", serif;
  --script: "Lavishly Yours", "Italiana", cursive;
  --italianno: "Italianno", "Italiana", cursive;
  --sans: "Manrope", sans-serif;
  --mono: "DM Mono", monospace;
  --night: #1b1916;
  --cream: #f8f4ea;
  --brass: #bb9a7f;
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }

/* Scroll reveal — same technique as the official invitations.anvela.ca
   welcome section: fade + rise + soften-into-focus, staggered with --d.
   Scoped to html.js so a no-JS visitor gets the fully visible page instead
   of permanently hidden content. */
html.js .reveal { opacity: 0; transform: translateY(34px); filter: blur(5px); transition: opacity .95s cubic-bezier(.19, 1, .22, 1) var(--d, 0s), transform .95s cubic-bezier(.19, 1, .22, 1) var(--d, 0s), filter .95s cubic-bezier(.19, 1, .22, 1) var(--d, 0s); }
html.js .reveal.is-in { opacity: 1; transform: translateY(0); filter: blur(0); }
body { margin: 0; background: var(--paper); color: var(--ink); font-family: var(--sans); font-weight: 300; }
body.modal-open { overflow: hidden; }
body.intro-active { overflow: hidden; }
button, input, select, textarea { font: inherit; }
button, a { -webkit-tap-highlight-color: transparent; }
a { color: inherit; }
/* ---------- The permanent botanical rails ----------
   Peonies and eucalyptus, shot on black and keyed out, delivered as transparent
   video: WebM/VP9 for Chrome and Firefox, HEVC-with-alpha in a .mov for Safari,
   which reads no other transparent format. The WebP still is the poster, so the
   opening frame is correct before a byte of footage arrives.

   FIXED, not absolute: this is the whole site's frame, not a hero decoration.
   z-index 9 puts it above every section's own background (those top out at 4)
   and below the topbar (10), the film grain (20) and the music toggle (21) —
   the grain lying over the flowers is what keeps them part of the paper rather
   than pasted on top of it.

   It cannot go BEHIND the text: the sections carry opaque backgrounds, so a
   layer under them would simply be invisible. It therefore sits on top and is
   kept out of the text's way by geometry instead — narrow plates hugging the
   extreme edges, with their inner edge feathered off (see --fade below). */
.flowers {
  position: fixed; inset: 0; z-index: 9; pointer-events: none;
  opacity: 0; transition: opacity 1.4s ease;
}
/* Held back until the opening has been read. The doors sequence gets the first
   screen to itself — the flowers frame everything AFTER the arrival. */
.flowers.is-live { opacity: 1; }

.plate { position: absolute; pointer-events: none; user-select: none; }
.plate-sway { transform-origin: top center; animation: petal-sway 9s ease-in-out infinite alternate; }
.plate img, .plate video { display: block; width: 100%; height: auto; animation: petal-breathe 13s ease-in-out infinite alternate; }
/* the still is the fallback for reduced motion and for a failed video decode */
.plate img { display: none; }

/* The inner edge dissolves before it can reach the text column. Without this the
   eucalyptus tips end in a hard vertical cut wherever the plate happens to stop. */
.plate--l { top: -6%; left: -9%; width: min(21vw, 320px);
  -webkit-mask-image: linear-gradient(100deg, #000 30%, transparent 74%);
  mask-image: linear-gradient(100deg, #000 30%, transparent 74%); }
.plate--r { top: -4%; right: -8%; width: min(18vw, 280px);
  -webkit-mask-image: linear-gradient(260deg, #000 30%, transparent 74%);
  mask-image: linear-gradient(260deg, #000 30%, transparent 74%); }

/* Two periods per plate, offset and never shared. One duration across both and
   the movement starts reading as a metronome instead of air. */
.plate--l .plate-sway { transform-origin: top left;  animation-duration: 10.6s; animation-delay: -5.1s; }
.plate--r .plate-sway { transform-origin: top right; animation-duration: 8.2s;  animation-delay: -2.4s; }
.plate--l img, .plate--l video { animation-duration: 15s; }
.plate--r img, .plate--r video { animation-duration: 11s; animation-delay: -4s; }

@keyframes petal-sway    { from { transform: rotate(-1deg); } to { transform: rotate(1.25deg); } }
@keyframes petal-breathe { from { transform: scale(1) translateY(0); } to { transform: scale(1.018) translateY(5px); } }

/* Below 1200px the 6vw gutter stops being wide enough to hold a plate clear of
   the text, so the rails narrow, then go entirely. Squeezing them onto a phone
   would put eucalyptus over 11px body copy — the effect is not worth the read. */
@media (max-width: 1200px) {
  .plate--l { width: min(19vw, 230px); }
  .plate--r { width: min(16vw, 200px); }
}
/* Phones keep the flowers, but as a different object: two small corner accents
   on OPPOSITE corners rather than two full rails. Diagonal, so no single screen
   of text is clamped between them, and pushed well past the edge so only the
   outer mass shows.

   They are STILLS here, not video (see the .plate video rule below). A phone
   gets the frame without 3.3 MB of alpha footage and without two clips decoding
   over a scroll — and on a 390px screen, drifting eucalyptus at 150px wide is
   motion nobody registers while scrolling anyway. */
@media (max-width: 900px) {
  .plate--l { top: -3%; left: -17%; width: 46vw;
    -webkit-mask-image: linear-gradient(112deg, #000 22%, transparent 72%);
    mask-image: linear-gradient(112deg, #000 22%, transparent 72%); }
  .plate--r { top: auto; bottom: -5%; right: -19%; width: 42vw;
    -webkit-mask-image: linear-gradient(248deg, #000 20%, transparent 70%);
    mask-image: linear-gradient(248deg, #000 20%, transparent 70%); }
  /* a touch back, so 11px body copy passing underneath still reads cleanly */
  .flowers.is-live { opacity: .72; }
  .plate video { display: none; }
  .plate img { display: block; }
}

/* A still plant is the correct reduced-motion answer, and the still must replace
   the video rather than pause it: a paused <video> still costs the full download. */
@media (prefers-reduced-motion: reduce) {
  .plate video { display: none; }
  .plate img { display: block; }
  .plate img, .plate-sway { animation: none; }
}

.noise { position: fixed; inset: 0; z-index: 20; pointer-events: none; opacity: .04; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.7'/%3E%3C/svg%3E"); }

/* Invitation preloader: the envelope-reveal video plays on load, then phases out into the cover */
.invitation-intro { position: fixed; inset: 0; z-index: 100; display: grid; place-items: center; overflow: hidden; background: #2a0a10; isolation: isolate; transition: opacity .75s ease, visibility .75s ease; }
.invitation-intro::before { content: ""; position: absolute; inset: 0; opacity: .2; background: radial-gradient(circle at 50% 34%, rgba(255,255,255,.9), transparent 30%), repeating-linear-gradient(115deg, rgba(83,66,49,.035) 0 1px, transparent 1px 7px); mix-blend-mode: multiply; pointer-events: none; }
.invitation-intro.is-finished { opacity: 0; visibility: hidden; pointer-events: none; }
.intro-skip { position: absolute; z-index: 20; top: 25px; right: 30px; border: 0; border-bottom: 1px solid rgba(33,28,22,.42); padding: 5px 0; color: var(--ink); background: transparent; font: 400 8px var(--mono); text-transform: uppercase; letter-spacing: .14em; cursor: pointer; }
.intro-open { position: absolute; z-index: 10; left: 50%; bottom: max(9vh, calc(env(safe-area-inset-bottom) + 42px)); display: grid; justify-items: center; gap: 13px; border: 0; padding: 12px 18px; color: #fbf6ec; background: transparent; font: 400 9px var(--mono); letter-spacing: .22em; text-transform: uppercase; cursor: pointer; transform: translateX(-50%); transition: opacity .35s ease, transform .35s ease; }
.intro-open:hover { transform: translateX(-50%) translateY(-2px); }
.intro-open:focus-visible { outline: 1px solid #fbf6ec; outline-offset: 5px; }
.intro-open i { width: 1px; height: 28px; background: currentColor; animation: intro-pulse 1.8s ease-in-out infinite; transform-origin: top; }

.intro-video-stage { position: absolute; z-index: 4; inset: 0; overflow: hidden; }
/* The still is the video's CSS background rather than its poster attribute:
   Safari does not reliably repaint a poster swapped after first render. */
.intro-video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity .6s ease; background: #2a0a10 url("assets/envelope-wine-poster.webp") center / cover no-repeat; }
/* The film is portrait. On a wide screen it sits centred on a blurred, darkened
   copy of its own first frame instead of being centre-cropped down to the seal. */
.intro-video-ground { position: absolute; inset: -6%; z-index: 0; display: none; background: #2a0a10 url("assets/envelope-wine-poster.webp") center / cover no-repeat; filter: blur(38px) brightness(.55); transform: scale(1.08); }
@media (min-width: 901px) and (min-aspect-ratio: 4/5) {
  .intro-video-ground { display: block; }
  .intro-video { object-fit: contain; background-size: contain; background-color: transparent; }
}
.intro-video-stage::after { content:""; position:absolute; inset:0; z-index:1; background:linear-gradient(0deg,rgba(20,19,16,.55),transparent 52%); opacity: 0; transition: opacity .9s ease .3s; }
.invitation-intro.is-video-in .intro-video { opacity: 1; }
.invitation-intro.is-video-in .intro-video-stage::after { opacity: 1; }
.intro-video-copy { position: absolute; z-index: 2; left: 0; right: 0; bottom: 9vh; color: #fbf6ec; text-align: center; opacity: 0; transform: translateY(18px); transition: opacity .7s ease 1.15s, transform .7s ease 1.15s; }
.intro-video-copy p { margin:0 0 13px; font:400 9px var(--mono); text-transform:uppercase; letter-spacing:.22em; }.intro-video-copy h2 { margin:0; font:400 clamp(62px,10vw,132px)/.98 var(--italianno); letter-spacing:normal; }.intro-video-copy h2 i { color:#d4ac8c; font-size:.9em; font-style:normal; margin:0 .02em; }.intro-video-copy span { display:block; margin-top:20px; font:300 13px var(--mono); letter-spacing:.2em; }
.invitation-intro.is-video-in .intro-video-copy { opacity: 1; transform: translateY(0); }

@keyframes intro-pulse { 0%,100% { transform:scaleY(.35); opacity:.4 } 50% { transform:scaleY(1); opacity:1 } }

/* Cover: the rainy-street film sits in a cream mat beside the names on desktop,
   where the 720px-wide source is DOWNSCALED and therefore stays sharp. On phones
   it goes full-bleed, the one case where a 9:16 source needs no upscaling at all. */
/* One hero, two layouts — never two heroes. The plate is 9:16, so a landscape
   screen and a portrait screen crop it very differently, and only the position
   of the copy and the scrim changes between them. The film, the type and the
   moment are identical on both. */
/* ═══ THE COVER — two different films, one per device class ═══
   Desktop gets the watercolour garden; phones get the two-part doors sequence.
   This is a deliberate split, not a responsive reflow: the two are different
   worlds, and each was chosen because it is the stronger of the two on that
   screen. app.js loads ONLY the one for the current device — see the note on
   COVER_MOBILE_Q there. The 900px breakpoint below and that query in app.js
   must always be changed together. */

/* ── desktop: the garden ── */
/* Sticky, same as invitations.anvela.ca's hero: it pins to the top while
   #story (the next sheet) climbs over it in normal flow. main#top carries the
   z-index and opaque background that let that climb actually cover it — see
   the comment there. */
.cover { position: sticky; top: 0; height: 100svh; display: grid; place-items: center; background: #ece4d4; color: var(--ink); overflow: hidden; }
.cover > * { min-height: 0; }
/* The footage dissolves into the paper rather than being covered by a hard
   edge: --wipe is written every frame by the heroDissolve() scroll handler in
   app.js as #story's top edge approaches, and the mask erodes the frame
   bottom-up so the seam is a soft gradient instead of a cut. 128% is the
   untouched fallback for no-JS and reduced-motion visitors. */
.cover-frame {
  position: absolute; inset: 0; z-index: 0; display: block; margin: 0;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(var(--wipe, 128%) - 30%), transparent var(--wipe, 128%));
  mask-image: linear-gradient(to bottom, #000 calc(var(--wipe, 128%) - 30%), transparent var(--wipe, 128%));
}
.cover-video { position: absolute; inset: 0; display: block; width: 100%; height: 100%; object-fit: cover; object-position: center; transform-origin: 58% 50%; transition: transform 26s linear; }
.cover-doors, .cover-bloom { display: none; } /* phones only */
/* The watercolour is pale, so the copy carries its own soft wash instead of a
   scrim over the whole frame — that keeps the painting readable behind it. */
.cover-copy { position: relative; z-index: 4; text-align: center; padding: 52px clamp(34px,5vw,64px); background: radial-gradient(closest-side, rgba(242,235,222,.9), rgba(242,235,222,.55) 62%, rgba(242,235,222,0)); }
.cover-eyebrow { margin: 0 0 18px; font: 400 9px var(--mono); text-transform: uppercase; letter-spacing: .26em; color: var(--wine); }
.cover-copy h2 { margin: 0; font: 400 clamp(58px,7.4vw,124px)/.92 var(--italianno); letter-spacing: normal; color: var(--ink); }
.cover-copy h2 i { color: var(--wine); font-style: normal; font-size: .86em; margin: 0 .04em; }
.cover-rule { width: 64px; height: 1px; background: rgba(26,23,19,.3); margin: 30px auto; }
.cover-date { margin: 0; font: 300 12px var(--mono); letter-spacing: .24em; color: var(--ink); }
.cover-frame figcaption { display: none; } /* the garden is not a doorway */
.cover-frame::after { content: none; }

/* The cue lives inside the cover, and the cover is a different world on each
   device: a pale watercolour garden on desktop, a dark doorway film on phones.
   So it flips with them — ink here, cream in the phone block. */
.scroll-cue { position: absolute; z-index: 4; left: 50%; bottom: max(30px, env(safe-area-inset-bottom)); transform: translateX(-50%); display: flex; flex-direction: column; align-items: center; gap: 10px; color: var(--ink); font: 400 8px var(--mono); text-transform: uppercase; letter-spacing: .18em; text-decoration: none; text-shadow: 0 1px 10px rgba(242,235,222,.9); }
.scroll-cue i { width: 1px; height: 34px; background: var(--wine); animation: intro-pulse 1.8s ease-in-out infinite; transform-origin: top; }

/* ── phones: the doors sequence ── */
@media (max-width: 900px) {
  .cover { place-items: stretch; background: var(--night); color: var(--cream); }
  .cover-garden { display: none; }
  .cover-doors { display: block; }
  .cover-bloom { display: block; }

  /* Second half sits on top of the first and is revealed at the cut, so there
     is never a frame of empty background between the two plates. */
  .cover-video--two { z-index: 1; opacity: 0; transition: opacity .42s ease, transform 26s linear; }
  .cover-frame.is-through .cover-video--two { opacity: 1; }
  /* The seam. The two plates are unrelated places, so the cut is hidden under
     a flash rather than blended — a crossfade would show both at once and read
     as a mistake. */
  .cover-bloom { position: absolute; inset: 0; z-index: 2; pointer-events: none; opacity: 0; transition: opacity .5s ease; background: radial-gradient(ellipse at 50% 46%, rgba(255,246,228,.96), rgba(255,241,214,.72) 42%, rgba(255,238,208,0) 78%); }
  .cover-bloom.is-lit { opacity: 1; transition-duration: .26s; }
  /* Once the film lands the held frame keeps drifting, so it reads as an
     arrival rather than a stall. */
  .cover-frame.is-rested .cover-video { transform: scale(1.07); }

  /* Portrait shows nearly the whole 9:16 plate, so the copy sits along the
     bottom and the scrim runs bottom-up. Holds a real floor under the whole
     copy block while leaving the lit doorway above it untouched. */
  .cover-frame::after { content: ""; position: absolute; z-index: 3; inset: 0; background: linear-gradient(0deg, rgba(15,13,11,.90) 0%, rgba(15,13,11,.74) 30%, rgba(15,13,11,.32) 56%, rgba(15,13,11,.18) 74%, rgba(15,13,11,.48) 100%); }
  .cover-copy { align-self: end; text-align: center; padding: 0 7vw 19vh; background: none; }
  .cover-eyebrow { color: #e3cdb0; }
  .cover-copy h2 { color: var(--cream); }
  .cover-copy h2 i { color: var(--brass); }
  .cover-rule { background: rgba(248,244,234,.34); margin: 26px auto; }
  .cover-date { color: var(--cream); }

  /* The names arrive with the view, not before it — that beat is the payoff
     the sequence is built around. Gated on .cover--film, which only JS sets,
     so a guest without JS or on reduced motion always has the copy from the
     start rather than a permanently blank hero. */
  .cover--film .cover-copy > * { opacity: 0; transform: translateY(16px); filter: blur(7px); transition: opacity 1.15s ease, transform 1.15s ease, filter 1.15s ease; }
  .cover--film.is-written .cover-copy > * { opacity: 1; transform: none; filter: blur(0); }
  .cover--film.is-written .cover-eyebrow { transition-delay: .05s; }
  .cover--film.is-written .cover-copy h2 { transition-delay: .42s; }
  .cover--film.is-written .cover-rule { transition-delay: .95s; }
  .cover--film.is-written .cover-date { transition-delay: 1.15s; }

  /* Cream on the dark film. The bottom-up scrim leaves the centre bright and
     the sequence lands on a lit sea, where the cue measured 3.9:1 — its own
     soft halo is cheaper than darkening the whole bottom of the frame. */
  .scroll-cue { color: rgba(248,244,234,.72); text-shadow: 0 1px 10px rgba(15,13,11,.7); }
  .scroll-cue i { background: rgba(248,244,234,.55); }
  .scroll-cue::before { content: ""; position: absolute; z-index: -1; left: 50%; top: -18px; transform: translateX(-50%); width: 210px; height: 104px; border-radius: 50%; pointer-events: none; background: radial-gradient(ellipse, rgba(15,13,11,.66), rgba(15,13,11,.28) 54%, rgba(15,13,11,0) 76%); }
}

/* FIXED since 2026-09-12: the menu, language toggle and RSVP stay reachable on
   every screen. Over the cover it is exactly the old transparent bar; once main
   climbs past the scrim, app.js (stickyTopbar) adds .is-solid and it condenses
   into a frosted strip, plus .is-dark over the dark sections (data-topbar="dark"
   in index.html). The safe-area padding matters only in standalone/landscape,
   where viewport-fit=cover lets the page run under the notch. */
.topbar { position: fixed; z-index: 10; top: 0; left: 0; right: 0; height: calc(100px + env(safe-area-inset-top)); display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; padding: env(safe-area-inset-top) max(4vw, env(safe-area-inset-right)) 0 max(4vw, env(safe-area-inset-left)); color: #f8f4ea; border-bottom: 1px solid transparent; transition: height .5s cubic-bezier(.2,.7,.15,1), background-color .45s ease, color .45s ease, border-color .45s ease; }
.topbar.is-solid { height: calc(68px + env(safe-area-inset-top)); color: var(--ink); background: rgba(241,237,228,.9); -webkit-backdrop-filter: saturate(1.2) blur(14px); backdrop-filter: saturate(1.2) blur(14px); border-bottom-color: rgba(25,23,19,.09); }
.topbar.is-solid::before { opacity: 0; }
.topbar.is-solid .monogram span { color: var(--wine); }
.topbar.is-solid .rsvp-button--small { color: var(--wine); border-color: rgba(109,20,37,.5); }
.topbar.is-solid .rsvp-button--small:hover { color: #fffaf0; background: var(--wine); border-color: var(--wine); }
.topbar.is-solid.is-dark { color: var(--cream); background: rgba(27,18,16,.66); border-bottom-color: rgba(248,244,234,.09); }
.topbar.is-solid.is-dark .monogram span { color: #ccae92; }
.topbar.is-solid.is-dark .rsvp-button--small { color: #fff; border-color: rgba(255,255,255,.65); }
.topbar.is-solid.is-dark .rsvp-button--small:hover { color: var(--ink); background: #fff; border-color: #fff; }
.topbar .rsvp-button--small { transition: color .45s ease, background .3s ease, border-color .45s ease, transform .3s ease; }
/* Anchor jumps (native smooth scroll) land sections below the condensed bar. */
html { scroll-padding-top: calc(68px + env(safe-area-inset-top)); }
/* The nav used to sit on a night-dark cover and needed nothing. Full-bleed
   doors put it straight over the lit doorway, where "Détails" measured
   1.8:1 — a scrim is the only thing that makes cream type survive there.
   The bar is absolute, not fixed, so this never touches the pale sections. */
.topbar::before { content: ""; position: absolute; z-index: -1; inset: 0 0 auto 0; height: 190px; pointer-events: none; transition: opacity .45s ease; background: linear-gradient(180deg, rgba(15,13,11,.80) 0%, rgba(15,13,11,.56) 38%, rgba(15,13,11,.22) 72%, transparent 100%); }
.monogram { font-family: var(--serif); font-size: 25px; text-decoration: none; letter-spacing: -.06em; }
.monogram span { color: #ccae92; font-size: .75em; margin: 0 2px; }
.topbar nav { display: flex; gap: 36px; }
.topbar nav a { color: inherit; text-decoration: none; font-size: 11px; text-transform: uppercase; letter-spacing: .13em; position: relative; }
.topbar nav a::after { content: ""; position: absolute; left: 0; right: 100%; bottom: -7px; height: 1px; background: currentColor; transition: right .35s ease; }
.topbar nav a:hover::after { right: 0; }
.header-actions { justify-self: end; display: flex; align-items: center; gap: 24px; }
.lang-switch { border: 0; color: inherit; background: none; cursor: pointer; display: flex; align-items: center; gap: 7px; font: 400 10px var(--mono); padding: 10px 0; }
.lang-switch span { opacity: .48; }
.lang-switch span.active { opacity: 1; }
.lang-switch i { display: block; width: 18px; height: 1px; background: currentColor; opacity: .5; }
.rsvp-button { border: 1px solid var(--wine); background: var(--wine); color: #fffaf0; min-width: 230px; padding: 18px 28px; text-transform: uppercase; letter-spacing: .15em; font-size: 10px; cursor: pointer; transition: color .3s ease, background .3s ease, transform .3s ease; }
.rsvp-button:hover { background: transparent; color: var(--wine); transform: translateY(-2px); }
.rsvp-button--small { min-width: 92px; padding: 12px 17px; color: #fff; background: transparent; border-color: rgba(255,255,255,.65); }
.rsvp-button--small:hover { background: #fff; color: var(--ink); }
.eyebrow { margin: 0 0 30px; font: 400 10px var(--mono); letter-spacing: .24em; text-transform: uppercase; }
/* Classy staggered reveal: each line fades up and un-blurs, like ink settling */
.date-lockup { display: flex; align-items: flex-end; gap: 20px; border-top: 1px solid rgba(255,255,255,.35); padding-top: 15px; }
.date-lockup p { font: 300 18px var(--mono); margin: 0; letter-spacing: .1em; }
.date-lockup span { font-size: 10px; text-transform: uppercase; letter-spacing: .17em; opacity: .65; padding-bottom: 2px; }
.text-link { display: inline-flex; gap: 20px; align-items: center; border: 0; background: none; color: inherit; padding: 0 0 6px; border-bottom: 1px solid currentColor; margin-top: 46px; font-size: 11px; letter-spacing: .12em; text-transform: uppercase; cursor: pointer; }
.text-link svg { width: 50px; height: 12px; fill: none; stroke: currentColor; transition: transform .3s ease; }
.text-link:hover svg { transform: translateX(6px); }
.vertical-note { position: absolute; z-index: 3; right: 20px; top: 48%; margin: 0; transform: rotate(90deg) translateX(50%); transform-origin: right center; font: 300 9px var(--mono); text-transform: uppercase; letter-spacing: .2em; }
/* --- Countdown: the toast. Footage sits under a heavy wine grade so the band
   keeps its burgundy identity instead of turning into a photo strip. The
   asymmetric label-left / numerals-right grid is unchanged. --- */
.countdown { position: relative; isolation: isolate; overflow: hidden; background: var(--wine); color: #f6eee1; padding: clamp(76px, 9vw, 118px) 8vw clamp(84px, 10vw, 126px); display: grid; grid-template-columns: 1fr 2fr; gap: 8vw; align-items: end; min-height: min(62svh, 520px); }
.countdown-media { position: absolute; inset: 0; z-index: -3; width: 100%; height: 100%; object-fit: cover; filter: saturate(.92) contrast(1.04) brightness(.86); transform: scale(1.02); }
.countdown::before { content: ""; position: absolute; inset: 0; z-index: -2; background: linear-gradient(100deg, rgba(109,20,37,.86) 0%, rgba(109,20,37,.60) 32%, rgba(109,20,37,.30) 58%, rgba(88,16,30,.46) 100%); mix-blend-mode: multiply; }
.countdown::after { content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none; background: radial-gradient(95% 130% at 74% 52%, transparent 0%, rgba(48,8,17,.20) 66%, rgba(40,6,14,.42) 100%); }
.countdown > :not(.countdown-media) { position: relative; }
.countdown > p { margin: 0; max-width: 160px; font: 400 11px/1.5 var(--mono); text-transform: uppercase; letter-spacing: .15em; text-shadow: 0 1px 12px rgba(28,4,10,.75); }
.countdown-grid { display: grid; grid-template-columns: repeat(4, 1fr); }
.countdown-grid div { border-left: 1px solid rgba(255,255,255,.42); padding-left: 22px; display: flex; flex-direction: column; }
.countdown-grid strong { font: 400 clamp(40px, 5vw, 78px)/1 var(--serif); text-shadow: 0 2px 22px rgba(28,4,10,.72), 0 0 58px rgba(28,4,10,.45); }
.countdown-grid span { margin-top: 8px; font: 300 9px var(--mono); text-transform: uppercase; letter-spacing: .15em; opacity: .8; text-shadow: 0 1px 10px rgba(28,4,10,.7); }
.story, .details { padding: 110px 8vw; }
/* Rule + name only: the 01/02/03 counters were removed on 2026-09-12 (read as
   templated). The rule is shortened to match, since it no longer bridges a
   number to the name. */
.section-index { display: grid; grid-template-columns: 40px auto; align-items: center; width: max-content; gap: 14px; font: 400 9px var(--mono); text-transform: uppercase; letter-spacing: .15em; }
.section-index i { height: 1px; background: currentColor; opacity: .45; }
/* The chapter marker is the only text pinned to the extreme left edge, so it is
   the only thing the left plate can actually cover. Nudged clear above 900px,
   which is exactly where the rails exist; below that they are removed entirely
   and this must not apply. */
@media (min-width: 901px) { .section-index { margin-left: clamp(0px, 9vw, 150px); } }
.section-index--light { color: #f3ede3; }

/* main is everything after the (now sticky) hero. It needs its own stacking
   context with a z-index above .cover's — a positioned element at z-index:auto
   (the sticky hero) otherwise stacks ABOVE plain in-flow content regardless of
   DOM order, which would leave the hero pinned on top forever instead of
   getting climbed over. The opaque background is what makes it a solid sheet:
   without it, .story's own transparent gaps would let the hero show through
   until the mask finished eroding it. Stays comfortably under .flowers (9)
   and .topbar (10), which must always render above every section. */
main {
  position: relative;
  z-index: 1;
  background: var(--paper);
}
/* The café band. Sits between the lead and the prose because it IS the lead —
   the coffee that never ended. Wider than the prose measure so it reads as a
   held cinematic beat rather than an inline illustration, and the plate is
   already cropped to 16:9 so object-fit has nothing to throw away. */
/* The coffee and the paragraph it belongs to, side by side. Full width, the
   video was a 1180px letterbox competing with the lead above it for the eye;
   beside its own text it becomes an illustration of that text, which is what it
   always was. The column is the narrower of the two so the words lead. */
.story-duo { margin: 58px auto 0; max-width: min(100%, 1010px); display: grid;
  grid-template-columns: minmax(0, .92fr) minmax(0, 1fr); gap: clamp(34px, 4vw, 64px);
  align-items: center; }
.story-cafe { margin: 0; max-width: none; }
/* 4:5, matching the re-crop. A 16:9 plate in a column this narrow reads as a
   slot, not a window. */
.story-cafe-video { display: block; width: 100%; aspect-ratio: 4/5; object-fit: cover; background: #17140f; }
/* --muted is not a token in this sheet, so the obvious rgba(ink,.55) fallback
   silently landed at 3.7:1 on paper. Wine at 9px is what the gallery captions
   just below already use — consistent, and 9.6:1. */
.story-cafe figcaption { margin-top: 14px; font: 500 9px var(--mono); text-transform: uppercase; letter-spacing: .16em; color: var(--wine); text-align: left; }
/* Ranged left, not centred: it now sits beside a hard vertical edge, and centred
   text against a straight edge reads as a mistake. */
.story-body { margin: 0; max-width: 430px; text-align: left; }
.story-lead { margin: 62px auto 0; max-width: min(100%, 1020px); text-align: center; font: 400 clamp(50px, 6vw, 94px)/1.06 var(--italianno); letter-spacing: normal; }
/* Bigger than the site's 13px body copy, and only here. Beside a 550px-tall
   plate the default size left three thin lines adrift in a tall empty column;
   at 15/2 the paragraph has enough mass to hold its own half of the spread. */
.story-duo .story-body p { font-size: 15px; line-height: 2.05; }
.story-body p { margin: 0; font-size: 13px; line-height: 1.9; }
.story blockquote { margin: 88px auto 0; max-width: 650px; padding: 22px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); font: 400 clamp(34px, 3.9vw, 60px)/1.1 var(--italianno); text-align: center; color: var(--wine); }
.journey { position: relative; padding: 96px 8vw 150px; background: var(--paper); color: var(--ink); overflow: hidden; }
.journey > *:not(.sky) { position: relative; z-index: 2; }
/* Living sky behind; two cloud banks in front that scroll drives apart. The
   still's top edge samples #F5EEE2 against the page's #F1EDE4, so the horizon
   dissolves into the paper with no seam to hide. */
.sky { position: absolute; z-index: 1; left: 0; right: 0; bottom: 0; height: 66%; pointer-events: none; }
.sky-video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: .9; }
.sky::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, var(--paper) 2%, rgba(241,237,228,0) 46%); }
.sky-bank { position: absolute; bottom: -4%; width: 82%; height: 96%; background-image: url("assets/sky-clouds.jpg"); background-size: cover; background-repeat: no-repeat; will-change: transform; }
.sky-bank--l { left: -16%; background-position: left bottom; -webkit-mask-image: linear-gradient(90deg, #000 46%, transparent 95%); mask-image: linear-gradient(90deg, #000 46%, transparent 95%); }
.sky-bank--r { right: -16%; background-position: right bottom; -webkit-mask-image: linear-gradient(270deg, #000 46%, transparent 95%); mask-image: linear-gradient(270deg, #000 46%, transparent 95%); }
.journey-heading { margin: 58px 0 70px; display: flex; align-items: baseline; justify-content: space-between; border-bottom: 1px solid var(--line); }
.journey-heading > p { font: 300 11px var(--mono); text-transform: uppercase; letter-spacing: .18em; }
.journey-heading h2 { margin: 0; font: 400 clamp(68px, 10vw, 160px)/.85 var(--serif); letter-spacing: -.04em; }
.journey-heading h2 span { font-size: .26em; letter-spacing: .05em; color: var(--wine); }
/* Schedule: centered dotted line, rose medallion travels down with scroll */
.timeline { position: relative; padding: 0; margin: 0; list-style: none; }
.timeline::before { content: ""; position: absolute; left: 50%; top: 26px; bottom: 26px; border-left: 1px dashed rgba(25,23,19,.3); transform: translateX(-.5px); }
.timeline li:not(.timeline-marker) { display: grid; grid-template-columns: 1fr 96px 1fr; align-items: center; column-gap: 0; padding: 44px 0; opacity: .32; transform: translateY(8px); transition: opacity .65s ease, transform .65s ease; }
.timeline li.is-lit { opacity: 1; transform: none; }
.timeline time { text-align: right; font: 300 26px var(--mono); text-shadow: 0 1px 14px rgba(248,244,234,.95), 0 0 34px rgba(248,244,234,.75); }
.timeline-node { justify-self: center; width: 8px; height: 8px; border-radius: 50%; background: rgba(25,23,19,.4); }
.timeline h3 { margin: 0; font: 400 clamp(24px, 3vw, 44px) var(--serif); text-shadow: 0 1px 14px rgba(248,244,234,.95), 0 0 34px rgba(248,244,234,.75); }
.timeline p { margin: 8px 0 0; max-width: 300px; font-size: 11px; line-height: 1.7; opacity: .78; text-shadow: 0 1px 10px rgba(248,244,234,.95); }
/* A real rose rides the dotted line (2026-09-12, replaces the painted
   medallion). assets/rose-3d-sheet.webp is a 6x6 sprite sheet of 36 frames
   rendered from a 3D rose (photo-to-3d, see scratch notes): a gentle -22°..+22°
   turn with the light moving across the petals. app.js picks the frame from
   scroll progress, so the flower turns as it travels down. One image, no 3D
   engine on the guest's phone. Background-position steps are 100/(6-1) = 20%. */
.timeline-marker { position: absolute; z-index: 3; left: 50%; top: 0; width: 96px; height: 96px; transform: translate(-50%,-50%); background: url("assets/rose-3d-sheet.webp") 0 0 / 600% 600% no-repeat; filter: drop-shadow(0 10px 12px rgba(43,30,22,.32)); pointer-events: none; }
.venue { margin-top: 100px; min-height: 500px; background: #ded3c2; position: relative; padding: 70px 6vw; overflow: hidden; }
.venue-kicker { margin: 0 0 26px; font: 400 9px var(--mono); text-transform: uppercase; letter-spacing: .18em; color: var(--wine); }
.venue h2 { position: relative; z-index: 2; margin: 0 0 42px; font: 400 clamp(56px, 7.2vw, 110px)/.82 var(--serif); letter-spacing: -.045em; }
.venue > p:not(.venue-kicker) { font-size: 12px; line-height: 1.8; }
.venue a, .detail-pair a { display: inline-block; margin-top: 18px; font: 500 10px var(--mono); text-transform: uppercase; letter-spacing: .08em; text-underline-offset: 5px; }
.venue-mark { position: absolute; right: -2vw; bottom: -6vw; font: 400 31vw/.7 var(--serif); color: rgba(109,20,37,.075); letter-spacing: -.15em; }
.detail-pair { display: grid; grid-template-columns: 1fr 1fr; border-bottom: 1px solid var(--line); }
.detail-pair article { padding: 70px 6vw; min-height: 410px; }
.detail-pair article:first-child { border-right: 1px solid var(--line); padding-left: 0; }
.detail-pair h3 { margin: 0 0 28px; font: 400 clamp(34px, 4vw, 58px)/1 var(--serif); }
.detail-pair article > p:not(.venue-kicker) { max-width: 460px; font-size: 12px; line-height: 1.8; }
.swatches { display: flex; margin-top: 38px; }
.swatches i { width: 42px; height: 42px; border-radius: 50%; margin-left: -6px; border: 2px solid var(--paper); background: #4b413b; }
.swatches i:nth-child(2) { background: #7b1d2f; }.swatches i:nth-child(3) { background: #8c8a74; }.swatches i:nth-child(4) { background: #d6c5a6; }
/* ═══ THE ENVELOPE CLOSES (2026-09-12) ═══
   .dance-hero pins (sticky) and this section rises over it. The section box
   itself is transparent: the wine paper is .closing-paper, clipped to the
   envelope's bottom-flap point, so the party stays visible above the flap
   until the flap has climbed past it. --top is the clear band above the tip
   (room for the seal to sit ON the point rather than be cut by the section
   edge); --flap is the flap's rise from edge to tip. The seal presses on with
   .is-sealed from app.js. min-height 100svh so the pinned party never peeks
   out below this while the footer arrives. */
.closing { --top: 64px; --flap: clamp(90px, 20vw, 240px); position: relative; z-index: 3; min-height: 100svh; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: calc(var(--top) + var(--flap) + 48px) 6vw 150px; text-align: center; color: var(--cream); overflow: hidden; }
.closing-paper { position: absolute; inset: 0; z-index: 0; background: #2a0a10 url("assets/envelope-wine-paper.webp") center / cover no-repeat; clip-path: polygon(0 calc(var(--top) + var(--flap)), 50% var(--top), 100% calc(var(--top) + var(--flap)), 100% 100%, 0 100%); }
/* The still is bright at the seal; dim it to a paper the copy can sit on, and
   keep the embossing readable rather than flat black. */
.closing-paper::after { content: ""; position: absolute; inset: 0; background: radial-gradient(ellipse at 50% 30%, rgba(22,14,9,.42), rgba(22,14,9,.86) 78%); }
.closing-seam { position: absolute; z-index: 1; left: 0; right: 0; top: var(--top); width: 100%; height: var(--flap); pointer-events: none; opacity: .85; }
.closing-seal { position: absolute; z-index: 2; left: 50%; top: var(--top); width: clamp(76px, 12vw, 120px); height: auto; transform: translate(-50%, -50%) scale(1.28); opacity: 0; filter: drop-shadow(0 8px 14px rgba(20,8,10,.55)); transition: transform 1s cubic-bezier(.2, .7, .15, 1), opacity .55s ease; }
.closing.is-sealed .closing-seal { transform: translate(-50%, -50%) scale(1); opacity: 1; }
.closing h2 { margin: 0 0 24px; font: 400 clamp(58px, 8vw, 125px)/.9 var(--serif); letter-spacing: -.045em; }
.closing > p:not(.eyebrow) { font-size: 12px; margin: 0 0 40px; }
.closing-signature { position: absolute; z-index: 1; left: 2vw; bottom: -8vw; font: 400 24vw/.7 var(--script); color: rgba(248,244,234,.13); letter-spacing: normal; white-space: nowrap; }
.closing-signature span { font-size: .75em; }
.closing > *:not(.closing-signature):not(.closing-paper):not(.closing-seam):not(.closing-seal) { position: relative; z-index: 2; }
/* .dance-hero is pinned (sticky) in its own block further down, so the flap
   climbs over the party. It has to live there: a rule here loses the cascade
   to that later block, which is exactly how it shipped unpinned on 2026-09-12. */
@media (prefers-reduced-motion: reduce) {
  .closing-seal { transition: none; transform: translate(-50%, -50%); opacity: 1; }
}
footer { background: #191a17; color: rgba(255,255,255,.7); padding: 28px 4vw; display: flex; justify-content: space-between; font: 300 9px var(--mono); text-transform: uppercase; letter-spacing: .12em; }
footer p { margin: 0; } footer a { text-underline-offset: 4px; }
/* overflow-x: hidden is the guard, not the fix (see .choice-grid input): a
   modal must never pan sideways, whatever lands inside it later. */
.rsvp-modal { width: min(1040px, calc(100% - 32px)); max-height: calc(100svh - 32px); padding: 0; border: 0; background: var(--paper); color: var(--ink); box-shadow: 0 25px 90px rgba(0,0,0,.35); overflow-x: hidden; }
/* iOS Safari zooms the page into any field under 16px when it is tapped, and
   the zoomed page can then be dragged sideways. 16px on phones/touch only;
   desktop keeps the lighter 13px. */
@media (max-width: 900px), (pointer: coarse) {
  .rsvp-modal input, .rsvp-modal select, .rsvp-modal textarea { font-size: 16px; }
}
.rsvp-modal[open] { display: grid; grid-template-columns: .9fr 1.1fr; animation: modal-in .45s ease both; }
.rsvp-modal::backdrop { background: rgba(19,17,14,.73); backdrop-filter: blur(6px); }
@keyframes modal-in { from { opacity: 0; transform: translateY(24px) scale(.98); } }
.modal-close { position: absolute; right: 18px; top: 13px; z-index: 3; border: 0; background: none; font: 300 30px var(--sans); cursor: pointer; }
.modal-intro { background: var(--wine); color: #fff8ec; padding: 70px 5vw; display: flex; flex-direction: column; justify-content: center; }
.modal-intro h2, .success-message h2 { margin: 0 0 25px; font: 400 clamp(43px, 5vw, 72px)/.95 var(--serif); }
.modal-intro > p:last-child { max-width: 340px; font-size: 12px; line-height: 1.8; opacity: .75; }
#rsvp-form { padding: 65px 5vw 50px; overflow-y: auto; }
#rsvp-form > label, .attending-fields label { display: block; margin-bottom: 25px; }
#rsvp-form label > span, legend { display: block; margin-bottom: 10px; font: 400 9px var(--mono); text-transform: uppercase; letter-spacing: .12em; }
input, select, textarea { width: 100%; border: 0; border-bottom: 1px solid var(--line); padding: 12px 2px; background: transparent; border-radius: 0; outline: none; font-size: 13px; color: var(--ink); }
input:focus, select:focus, textarea:focus { border-color: var(--wine); }
fieldset { border: 0; padding: 0; margin: 0 0 25px; }
/* Party: one row per person the guest brings (name + their own meal). */
.companions-hint { margin: -2px 0 14px; font-size: 12px; line-height: 1.6; opacity: .7; }
.companion-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
/* Each person is a light card, so a name never reads as belonging to the
   meal of the person below it once the rows stack on a phone. */
.companion { display: grid; grid-template-columns: 1.3fr 1fr auto; gap: 14px; align-items: end; padding: 12px 12px 0; background: rgba(27, 26, 23, .04); }
.companion-list:not(:empty) { margin-bottom: 8px; }
.companion > * { min-width: 0; }
.attending-fields .companion label { margin-bottom: 14px; }
.remove-companion { border: 0; background: none; padding: 0 4px 22px; font: 300 24px/1 var(--sans); color: var(--ink); opacity: .55; cursor: pointer; }
.remove-companion:hover, .remove-companion:focus-visible { opacity: 1; color: var(--wine); }
.add-companion { display: block; width: 100%; margin-top: 4px; padding: 14px; border: 1px dashed var(--line); background: transparent; color: var(--ink); font: 400 9px var(--mono); text-transform: uppercase; letter-spacing: .12em; cursor: pointer; }
.add-companion:hover, .add-companion:focus-visible { border-color: var(--wine); color: var(--wine); }
.add-companion[hidden] { display: none; }
/* Packages without meal choices: each person is just a name and a remove button. */
.no-meals .companion { grid-template-columns: 1fr auto; }
.no-meals .remove-companion { grid-column: 2; grid-row: 1; }
@media (max-width: 520px) {
  .companion { grid-template-columns: 1fr auto; column-gap: 8px; }
  .companion > label:first-child { grid-column: 1; grid-row: 1; }
  .companion > label:nth-child(2) { grid-column: 1; grid-row: 2; }
  .remove-companion { grid-column: 2; grid-row: 1 / span 2; align-self: center; padding: 0 2px; }
}
.choice-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
/* The real radios stay in the form (they carry required + the value) but are
   shrunk to 1px inside their own label. Before 2026-09-12 they inherited
   `input { width: 100% }` while absolutely positioned against the fixed
   <dialog>, so each was as wide as the whole modal and poked out of its right
   edge (32px on a phone, 763px on desktop): the modal scrolled sideways. */
.choice-grid label { position: relative; cursor: pointer; }
.choice-grid input { position: absolute; top: 0; left: 0; width: 1px; height: 1px; margin: 0; padding: 0; opacity: 0; pointer-events: none; }
.choice-grid span { border: 1px solid var(--line); padding: 15px; text-align: center; margin: 0 !important; transition: .2s ease; }
.choice-grid input:checked + span { background: var(--wine); border-color: var(--wine); color: #fff; }
#rsvp-form .rsvp-button { width: 100%; margin-top: 10px; }
.success-message { grid-column: 2; padding: 80px 6vw; align-self: center; text-align: center; }
.success-message > span { display: grid; place-items: center; margin: 0 auto 28px; width: 52px; height: 52px; border: 1px solid var(--wine); border-radius: 50%; color: var(--wine); }
.success-message .text-link { color: var(--wine); }

@media (max-width: 900px) {
  .topbar { height: calc(76px + env(safe-area-inset-top)); grid-template-columns: 1fr auto; }
  .topbar.is-solid { height: calc(58px + env(safe-area-inset-top)); }
  html { scroll-padding-top: calc(58px + env(safe-area-inset-top)); }
  .topbar nav { display: none; }
  .header-actions { gap: 14px; }
  .vertical-note { display: none; }
  .countdown { grid-template-columns: 1fr; }
  .countdown > p { max-width: none; }

  /* Stacked: the video on top, its paragraph directly under. The grid gap is
     the only spacing wanted here — the old 70px margin was measuring from a
     full-width plate that no longer exists. */
  .story-duo { grid-template-columns: 1fr; gap: 30px; margin-top: 44px; }
  .story-body { margin: 0; max-width: none; text-align: center; }
  .story-cafe figcaption { text-align: center; }
  /* Back to the site's own body size. The 15px above exists only to balance a
     550px-tall plate sitting beside it; stacked, there is no column to balance
     and the larger type just reads as inconsistent with every other paragraph. */
  .story-duo .story-body p { font-size: 13px; line-height: 1.9; }
  .journey-heading { display: block; }
  .journey-heading h2 { margin-top: 30px; }
  .timeline li:not(.timeline-marker) { grid-template-columns: 1fr 84px 1fr; }
  .detail-pair { grid-template-columns: 1fr; }
  .detail-pair article:first-child { border-right: 0; border-bottom: 1px solid var(--line); padding-left: 6vw; }
  .rsvp-modal[open] { grid-template-columns: 1fr; overflow-y: auto; }
  .modal-intro { padding: 55px 7vw 40px; }
  #rsvp-form { padding: 42px 7vw; overflow: visible; }
  .success-message { grid-column: 1; padding: 70px 7vw; }
}

@media (max-width: 560px) {
  .intro-skip { top: max(18px, env(safe-area-inset-top)); right: max(18px, env(safe-area-inset-right)); z-index: 11; }
  .intro-video-copy { bottom: max(12vh, calc(env(safe-area-inset-bottom) + 10vh)); padding: 0 5vw; }
  .rsvp-button--small { min-width: 73px; padding: 10px; }
  .date-lockup { align-items: flex-start; flex-direction: column; gap: 8px; }
  .countdown { padding: 38px 6vw; }
  .countdown-grid div { padding-left: 9px; }
  .countdown-grid strong { font-size: 35px; }
  .countdown-grid span { font-size: 7px; letter-spacing: .04em; }
  .story, .details, .journey { padding: 80px 6vw; }
  .story-lead { font-size: 50px; }
  .cover-copy h2 { font-size: clamp(54px, 16vw, 92px); }
  .journey { padding-bottom: 110px; }
  .story blockquote { margin-top: 75px; }
  .journey-heading { margin: 70px 0 45px; }
  .journey-heading h2 { font-size: 64px; }
  .timeline li:not(.timeline-marker) { grid-template-columns: 1fr 64px 1.35fr; padding: 36px 0; }
  .timeline::before { left: calc((100% - 64px) / 2.35 + 32px); }
  .timeline-node { width: 6px; height: 6px; }
  .timeline time { font-size: 17px; }
  .timeline h3 { font-size: 21px; }
  .timeline p { font-size: 10px; }
  /* the dotted line is off-centre at this width (see .timeline::before above),
     so the medallion has to follow it rather than sit at 50% */
  .timeline-marker { left: calc((100% - 64px) / 2.35 + 32px); width: 72px; height: 72px; }
  .venue { margin-top: 70px; min-height: 470px; padding: 50px 7vw; }
  .detail-pair article { min-height: 0; padding: 55px 6vw !important; }
  .closing { --top: 48px; padding: calc(var(--top) + var(--flap) + 36px) 6vw 100px; }
  footer { flex-direction: column; gap: 16px; }
  .choice-grid { grid-template-columns: 1fr; }
}

/* Floating music toggle — appears once the intro finishes. */
.music-toggle { position: fixed; z-index: 21; left: max(22px, env(safe-area-inset-left)); bottom: max(22px, env(safe-area-inset-bottom)); width: 46px; height: 46px; display: flex; align-items: center; justify-content: center; padding: 0; border: 1px solid rgba(109,20,37,.28); border-radius: 50%; background: rgba(241,237,228,.72); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); color: var(--wine); cursor: pointer; opacity: 0; transform: translateY(10px); transition: opacity .6s ease, transform .6s ease, background .3s ease, border-color .3s ease; }
.music-toggle.is-visible { opacity: 1; transform: translateY(0); }
/* lifted clear of the footer instead of sitting on the credit line */
.music-toggle.is-visible.is-raised { transform: translateY(calc(-1 * var(--footer-h, 84px) - 16px)); }
.music-toggle:hover { background: var(--wine); border-color: var(--wine); }
.music-toggle:hover .music-bars i { background: #fffaf0; }
.music-bars { display: flex; align-items: center; gap: 3px; height: 16px; }
.music-bars i { width: 2.5px; height: 100%; border-radius: 2px; background: var(--wine); transform-origin: center bottom; transform: scaleY(.32); transition: background .3s ease; }
.music-toggle.is-playing .music-bars i { animation: music-eq 1.1s ease-in-out infinite; }
.music-toggle.is-playing .music-bars i:nth-child(2) { animation-delay: .18s; }
.music-toggle.is-playing .music-bars i:nth-child(3) { animation-delay: .36s; }
.music-toggle.is-playing .music-bars i:nth-child(4) { animation-delay: .54s; }
@keyframes music-eq { 0%, 100% { transform: scaleY(.32); } 50% { transform: scaleY(1); } }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; transition-duration: .01ms !important; }
  .music-toggle.is-playing .music-bars i { animation: none; transform: scaleY(.7); }
  html.js .reveal, html.js .reveal.is-in { opacity: 1; transform: none; filter: none; transition: none; }
}


/* ═══ DANCE HERO — mobile-first garden-party immersion ═══
   Full-bleed looping film between details and closing RSVP.
   Tuned first for ~390×844; desktop inherits the same immersion. */
/* Sticky so .closing (the envelope's bottom flap) climbs over the party; it sits
   in normal flow until it reaches the top, so nothing above it changes. Pinned
   for reduced motion too, like .cover: pinning is layout, not animation, and
   unpinned the flap's clear band shows bare paper instead of the party. */
.dance-hero {
  position: sticky;
  top: 0;
  isolation: isolate;
  overflow: hidden;
  min-height: 85svh;
  display: grid;
  place-items: center;
  color: var(--cream);
  background: #1a1612;
}
.dance-hero-media {
  position: absolute;
  inset: 0;
  z-index: 0;
}
.dance-hero-video {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  background: #1a1612;
}
.dance-hero-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(180deg, rgba(15,13,11,.55) 0%, rgba(15,13,11,.22) 28%, rgba(15,13,11,.28) 55%, rgba(15,13,11,.78) 100%),
    radial-gradient(ellipse at 50% 42%, rgba(15,13,11,.08), rgba(15,13,11,.45) 72%);
}
.dance-hero-copy {
  position: relative;
  z-index: 2;
  text-align: center;
  padding:
    max(48px, env(safe-area-inset-top))
    max(24px, env(safe-area-inset-right))
    max(56px, calc(env(safe-area-inset-bottom) + 28px))
    max(24px, env(safe-area-inset-left));
  max-width: 920px;
  width: 100%;
}
.dance-hero-kicker {
  margin: 0 0 18px;
  font: 400 9px var(--mono);
  text-transform: uppercase;
  letter-spacing: .26em;
  color: #e3cdb0;
}
.dance-hero-names {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: .08em .18em;
  font: 400 clamp(54px, 14vw, 118px)/.92 var(--italianno);
  letter-spacing: normal;
  color: var(--cream);
  text-shadow: 0 2px 28px rgba(10,8,6,.55), 0 0 48px rgba(10,8,6,.35);
}
.dance-hero-amp {
  color: var(--brass);
  font-style: normal;
  font-size: .86em;
  margin: 0 .02em;
}
.dance-hero-sub {
  margin: 22px auto 0;
  max-width: 28em;
  font: 300 clamp(13px, 3.4vw, 16px)/1.55 var(--sans);
  letter-spacing: .02em;
  color: rgba(248,244,234,.88);
  text-shadow: 0 1px 14px rgba(10,8,6,.55);
}
.dance-hero-date {
  margin: 26px 0 0;
  font: 300 13px var(--mono);
  letter-spacing: .24em;
  color: var(--cream);
  text-shadow: 0 1px 12px rgba(10,8,6,.5);
}

/* Ink-settling reveal for the names — mirrors the cover's staggered write */
html.js .dance-hero-name,
html.js .dance-hero-amp {
  opacity: 0;
  transform: translateY(14px);
  filter: blur(6px);
  transition:
    opacity 1.1s cubic-bezier(.19, 1, .22, 1),
    transform 1.1s cubic-bezier(.19, 1, .22, 1),
    filter 1.1s cubic-bezier(.19, 1, .22, 1);
}
html.js .dance-hero.is-written .dance-hero-name--a {
  opacity: 1; transform: none; filter: blur(0); transition-delay: .08s;
}
html.js .dance-hero.is-written .dance-hero-amp {
  opacity: 1; transform: none; filter: blur(0); transition-delay: .32s;
}
html.js .dance-hero.is-written .dance-hero-name--b {
  opacity: 1; transform: none; filter: blur(0); transition-delay: .52s;
}

@media (min-width: 901px) {
  .dance-hero { min-height: 100svh; }
  .dance-hero-names { font-size: clamp(72px, 8.5vw, 128px); }
  .dance-hero-copy { padding: 72px 6vw; }
}

@media (max-width: 560px) {
  .dance-hero { min-height: 100svh; }
  .dance-hero-names { font-size: clamp(52px, 15vw, 88px); }
  .dance-hero-kicker { margin-bottom: 14px; }
  .dance-hero-sub { margin-top: 18px; }
  .dance-hero-date { margin-top: 22px; font-size: 12px; }
}

@media (prefers-reduced-motion: reduce) {
  html.js .dance-hero-name,
  html.js .dance-hero-amp,
  html.js .dance-hero.is-written .dance-hero-name,
  html.js .dance-hero.is-written .dance-hero-amp {
    opacity: 1;
    transform: none;
    filter: none;
    transition: none;
  }
}

/* ═══ STORY PRINTS (2026-09-12, replaced the three tilted cards) ═══
   .story-memories is tall; .tirages-stage pins inside it and storyPrints() in
   app.js drops the prints by scroll progress (transform/opacity only).
   Full-bleed on purpose: the negative margins cancel .story's padding (same vw
   units, so they cancel exactly), and the stage's overflow:hidden then clips a
   falling print at the screen edge rather than at an invisible line inside
   the column. The table glow fades to --paper at its edges so the pinned
   stage never shows a seam. */
.story-memories { position: relative; height: 270svh; margin: 72px -8vw 0; }
.tirages-stage { position: sticky; top: 0; height: 100svh; display: grid; grid-template-rows: 1fr auto 1fr; justify-items: center; overflow: hidden; padding: calc(68px + env(safe-area-inset-top)) 20px 0; background: radial-gradient(ellipse 72% 58% at 50% 54%, #f8f5ee 0%, var(--paper) 100%); }
.story-memories-label { align-self: end; margin: 0 0 clamp(22px, 5svh, 48px); font: 400 9.5px var(--mono); letter-spacing: .24em; text-transform: uppercase; color: var(--wine); text-align: center; }
.pile { position: relative; width: min(62vw, 400px, calc((100svh - 330px) * .686)); aspect-ratio: .686; cursor: pointer; -webkit-tap-highlight-color: transparent; }
.tirage { position: absolute; inset: 0; margin: 0; opacity: 0; will-change: transform, opacity; }
.tirage-lift { position: absolute; inset: 0; }
.tirage-shadow { position: absolute; inset: 7% 5% -1%; border-radius: 6px; background: rgba(46, 30, 20, .5); filter: blur(16px); }
.tirage-paper { position: absolute; inset: 0; display: flex; flex-direction: column; padding: 4.5% 4.5% 0; background: #fbf8f3; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .85), 0 1px 2px rgba(25, 23, 19, .14); }
.tirage-photo { aspect-ratio: 3 / 4; overflow: hidden; background: #e9e0d4; }
.tirage-photo img { display: block; width: 100%; height: 100%; object-fit: cover; }
.tirage-note { flex: 1; display: grid; place-items: center; margin: 0; padding: 0 5%; text-align: center; font: 400 clamp(17px, 4.6vw, 25px)/1 var(--italianno); color: #3a3028; }
.tirage-caption { align-self: start; display: flex; flex-direction: column; align-items: center; gap: 12px; margin-top: clamp(28px, 5svh, 52px); text-align: center; }
.tirage-loc { font: 500 10px var(--mono); letter-spacing: .2em; text-transform: uppercase; color: var(--wine); transition: opacity .25s ease; }
.tirage-loc.is-swapping { opacity: 0; }
.tirage-actions { display: flex; gap: 20px; align-items: center; opacity: 0; transition: opacity .6s ease; }
.tirage-caption.is-ready .tirage-actions { opacity: 1; }
.tirage-next, .tirage-zoom { padding: 6px 0 3px; border: 0; border-bottom: 1px solid transparent; background: none; font: 400 9px var(--mono); letter-spacing: .16em; text-transform: uppercase; cursor: pointer; }
.tirage-next { color: rgba(25, 23, 19, .6); }
.tirage-zoom { color: var(--wine); border-bottom-color: rgba(109, 20, 37, .45); }
.tirage-next:disabled, .tirage-zoom:disabled { cursor: default; }
@media (max-width: 900px) {
  .tirages-stage { padding-top: calc(58px + env(safe-area-inset-top)); }
  .pile { width: min(62vw, 400px, calc((100svh - 310px) * .686)); }
}
@media (max-width: 560px) {
  .story-memories { margin: 60px -6vw 0; }
}
/* No pinning or falling for reduced motion: the finished pile, in flow. */
@media (prefers-reduced-motion: reduce) {
  .story-memories { height: auto; }
  .tirages-stage { position: relative; height: auto; grid-template-rows: auto auto auto; padding: 24px 20px 8px; }
  .story-memories-label { margin-bottom: 38px; }
  .pile { width: min(62vw, 360px); }
}

/* Lightbox Modal for Story Photos */
.photo-modal {
  border: 0;
  border-radius: 8px;
  padding: 0;
  background: transparent;
  max-width: 92vw;
  max-height: 92vh;
  outline: none;
  overflow: visible;
  margin: auto;
}
.photo-modal::backdrop {
  background: rgba(16, 14, 12, .88);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
.photo-modal-wrap {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.photo-modal img {
  max-width: 90vw;
  max-height: 78vh;
  object-fit: contain;
  border-radius: 4px;
  box-shadow: 0 30px 60px rgba(0,0,0,.65);
  display: block;
}
.photo-modal p {
  color: #f5efe6;
  margin-top: 14px;
  font: 400 clamp(18px, 2.2vw, 26px) var(--italianno);
  text-align: center;
  letter-spacing: .02em;
}
.photo-modal-close {
  position: absolute;
  top: -44px;
  right: -8px;
  border: 0;
  background: transparent;
  color: #f5efe6;
  font-size: 34px;
  cursor: pointer;
  line-height: 1;
  opacity: .8;
  transition: opacity .2s, transform .2s;
}
.photo-modal-close:hover {
  opacity: 1;
  transform: scale(1.1);
}
