/* ══════════════════════════════════════════════════════════════════
   ნატალი - 24.09.2026
   Single stylesheet for index.html. Mobile-first: every base rule targets
   the smallest screen and scales up through min-width queries only.

   The page is a stack of flat panes, not a wash of gradients. The hero is
   oxblood - the lantern room of the mood board, chosen to sit under the
   copper photobooth - and every pane past it is a step of the swamp green,
   laid over the one before with a rounded lip. Separation is carried by the
   fold, the shadow and the tone step. Colours are flat; the only gradient
   anywhere is the whisper of shading inside a pane and the brass hairlines.

   script.js writes one custom property, --vh on :root, and toggles the
   classes .is-loading / .is-ready / .is-live / .is-printing / .is-passed /
   .is-open / .is-in.
   ══════════════════════════════════════════════════════════════════ */


/* ── SELF-HOSTED FACES ──────────────────────────────────────────────
   font-display: block, never swap - swap would paint a system fallback and
   then reflow the page into the real face. The block period is spent behind
   the loading screen, which is why the two always ship together. */
/* Geo Script is a SYMBOL-encoded face (platform 3 / encoding 0, glyphs at
   0x20-0x7E): its Georgian letterforms sit on the Latin keys and it carries
   no Georgian codepoints at all. So every string set in --f-ka-title has to
   be typed in latin transliteration - Georgian text would miss the face
   entirely and fall through to Gordeziani. */
@font-face {
  font-family: 'DM Zviadi';
  src: url('geo_script.ttf') format('truetype');
  font-weight: 400;
  font-display: block;
}
@font-face {
  font-family: 'GL Gordeziani';
  src: url('fonts/gl-gordeziani-ultralight.ttf') format('truetype');
  font-weight: 200;
  font-display: block;
}
@font-face {
  font-family: 'GL Gordeziani';
  src: url('fonts/gl-gordeziani-light.ttf') format('truetype');
  font-weight: 300;
  font-display: block;
}
@font-face {
  font-family: 'GL Gordeziani';
  src: url('fonts/gl-gordeziani-regular.ttf') format('truetype');
  font-weight: 400;
  font-display: block;
}
@font-face {
  font-family: 'GL Gordeziani';
  src: url('fonts/gl-gordeziani-medium.ttf') format('truetype');
  font-weight: 500;
  font-display: block;
}


/* ── TOKENS ─────────────────────────────────────────────────────── */
:root {
  /* the hero: the deep oxblood of the lantern room on the mood board. The
     copper machine is the only bright object standing on it, and the swamp
     green of the first sheet lands against it the way the olive drapes sit
     against that room's red walls. */
  --oxblood: #2a1214;

  /* the invitation card, sampled off the reference photograph */
  --card-red: #511810;

  /* the four panes past the hero, each a step darker than the last */
  --swamp-0: #3a442e;
  --swamp-1: #313a27;
  --swamp-2: #262e1f;
  --swamp-3: #1a2014;

  --cream: #f4ead6;
  --cream-soft: #d6caae;
  --cream-dim: #9d9179;

  --brass: #c2a05a;
  --brass-lt: #e0c68a;
  --brass-dim: #8a7038;

  --line: rgba(224, 198, 138, 0.16);
  --line-soft: rgba(224, 198, 138, 0.09);

  /* JS off, or before the inline lock runs: 1vh is the honest fallback */
  --vh: 1vh;

  /* Georgian and latin share a lot of strings here ("24 სექტემბერი"), and
     Gordeziani has to come first or the numerals render in Cormorant while the
     letters fall back to Gordeziani, and the two weights visibly disagree. */
  --f-ka: 'GL Gordeziani', 'Cormorant Garamond', Georgia, serif;
  --f-ka-title: 'DM Zviadi', 'GL Gordeziani', Georgia, serif;
  --f-serif: 'Cormorant Garamond', Georgia, serif;
  --f-script: 'Pinyon Script', 'Cormorant Garamond', cursive;

  --ease: cubic-bezier(0.22, 0.8, 0.3, 1);

  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)' opacity='0.55'/%3E%3C/svg%3E");
}


/* ── RESET / BASE ───────────────────────────────────────────────── */
*,
*::before,
*::after { box-sizing: border-box; }

html {
  /* the canvas behind the page, so an overscroll at either end shows the
     deepest pane rather than a bare white flash */
  background-color: var(--swamp-3);
}

html,
body {
  margin: 0;
  padding: 0;
  /* the in-app browsers bounce the whole document otherwise, which reads as
     the layout itself moving */
  overscroll-behavior-y: none;
}

body {
  background-color: var(--oxblood);
  color: var(--cream);
  font-family: var(--f-ka);
  font-weight: 300;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  -webkit-tap-highlight-color: transparent;
}

/* The intro lock is JS-enforced, never position:fixed / overflow:hidden -
   a non-scrollable document freezes Chrome iOS's viewport in its wrong short
   state when the page was opened from a chat app, and a band of background
   shows below the hero. See the touchmove trap in script.js. */
html.is-locked,
body.is-locked {
  overscroll-behavior: none;
  touch-action: none;
}

img { display: block; max-width: 100%; }
h1, h2, h3, p, ol, ul, figure, fieldset, legend { margin: 0; padding: 0; }
ol, ul { list-style: none; }
fieldset { border: 0; }
button { font: inherit; color: inherit; cursor: pointer; }
a { color: inherit; }

::selection { background: var(--brass); color: var(--swamp-3); }


/* ── CUSTOM SCROLLBAR ───────────────────────────────────────────── */
/* Starts invisible and fades in with the site, so it does not draw a bright
   rail down the side of the locked hero. */
html {
  scrollbar-width: thin;
  scrollbar-color: transparent transparent;
  transition: scrollbar-color 1.2s ease;
}
html.is-open { scrollbar-color: var(--brass-dim) var(--swamp-3); }

html::-webkit-scrollbar { width: 8px; }
html::-webkit-scrollbar-track { background: var(--swamp-3); }
html::-webkit-scrollbar-thumb {
  background: transparent;
  border-radius: 4px;
  transition: background 1.2s ease;
}
html.is-open::-webkit-scrollbar-thumb { background: var(--brass-dim); }
html.is-open::-webkit-scrollbar-thumb:hover { background: var(--brass); }


/* ══ LOADING SCREEN ═════════════════════════════════════════════
   Outranks every other overlay on the page. Dismissed from JS, which is
   why the <noscript> block in the markup hides it outright. */
.loader {
  position: fixed;
  inset: 0;
  z-index: 12000;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--oxblood);
  transition: opacity 0.6s ease;
}
.loader.is-hidden { opacity: 0; pointer-events: none; }

.loader__ring {
  width: 40px;
  height: 40px;
  border: 1px solid rgba(194, 160, 90, 0.2);
  border-top-color: var(--brass);
  border-radius: 50%;
  animation: spin 0.9s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }


/* ══ DESKTOP NOTICE ═════════════════════════════════════════════ */
.mobile-prompt {
  position: fixed;
  z-index: 9000;
  left: 50%;
  bottom: 24px;
  display: none;
  width: 262px;
  padding: 20px 18px 18px;
  margin-left: -131px;
  text-align: center;
  background-color: rgba(26, 32, 20, 0.95);
  border: 1px solid var(--line);
  border-radius: 16px;
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45);
  transition: opacity 0.4s ease, transform 0.4s ease;
}
.mobile-prompt.is-out { opacity: 0; transform: translateY(12px); }

.mobile-prompt__icon {
  width: 52px;
  height: 52px;
  margin: 0 auto 10px;
  opacity: 0.8;
}
.mobile-prompt__text {
  font-size: 13px;
  line-height: 1.55;
  color: var(--cream-soft);
}
.mobile-prompt__close {
  position: absolute;
  top: 6px;
  right: 8px;
  padding: 4px 6px;
  font-size: 13px;
  color: var(--cream-dim);
  background: none;
  border: 0;
}


/* ══ MUSIC TOGGLE ═══════════════════════════════════════════════ */
.music {
  position: fixed;
  z-index: 8000;
  top: 16px;
  right: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  background-color: rgba(20, 12, 6, 0.5);
  border: 1px solid var(--line);
  border-radius: 50%;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.6s ease, visibility 0.6s;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
.music.is-visible { opacity: 1; visibility: visible; }

.music__bars {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 14px;
}
.music__bar {
  width: 2px;
  height: 4px;
  background-color: var(--brass-lt);
  border-radius: 1px;
  /* paused, not removed: the toggle only flips play-state, so the bars never
     jump back to a start frame when the guest resumes */
  animation: bar 0.9s ease-in-out infinite alternate;
  animation-play-state: paused;
}
.music__bar:nth-child(1) { animation-duration: 0.7s; }
.music__bar:nth-child(2) { animation-duration: 1.1s; height: 8px; }
.music__bar:nth-child(3) { animation-duration: 0.85s; height: 6px; }
.music__bar:nth-child(4) { animation-duration: 1.25s; }

.music.is-playing .music__bar { animation-play-state: running; }

@keyframes bar {
  from { height: 3px; }
  to { height: 14px; }
}


/* ══ HERO ═══════════════════════════════════════════════════════
   Flat oxblood, one grain tile, nothing else. The booth is the only bright
   object on it and everything here exists to leave it room. */
.hero {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 14px 20px;
  overflow: hidden;
  /* floors first, in this order, then the JS pin last so it wins */
  min-height: 100vh;
  min-height: 100svh;
  background-color: var(--oxblood);
  cursor: pointer;
}
.vh-lock .hero { min-height: calc(var(--vh) * 100); }

/* the hero is never torn out of the flow - pulling a full viewport out from
   under a smooth scroll would yank the page. It just stops being a button. */
.hero.is-passed { cursor: default; }

.hero__grain {
  position: absolute;
  inset: 0;
  background-image: var(--grain);
  opacity: 0.05;
  mix-blend-mode: overlay;
  pointer-events: none;
}

.hero__inner {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  max-width: 420px;
  text-align: center;
}

/* Every hero element starts hidden and is walked in by .is-live, which
   script.js adds only once the loader clears. Without the hold the whole
   entry would play out behind the spinner. */
.hero__eyebrow,
.hero__name,
.booth,
.hero__date {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 1.1s ease, transform 1.1s var(--ease);
}
.hero.is-live .hero__eyebrow,
.hero.is-live .hero__name,
.hero.is-live .booth,
.hero.is-live .hero__date {
  opacity: 1;
  transform: translateY(0);
}
.hero.is-live .hero__name { transition-delay: 0.25s; }
.hero.is-live .booth { transition-delay: 0.5s; }
.hero.is-live .hero__date { transition-delay: 0.95s; }

.hero__eyebrow {
  margin-bottom: 2px;
  font-family: var(--f-serif);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 3.4px;
  /* the tracking is applied to the right of every glyph including the last,
     so without this the line sits visibly left of centre */
  text-indent: 3.4px;
  color: var(--brass);
}

/* "GOLDEN" carries the only motion in the eyebrow: one highlight crossing the
   word, then four seconds of stillness. The rest is the whole point - a
   shimmer that travels without pause reads as a loading skeleton, not as gold.
   The same treatment is reused once more, on the card inside the envelope
   (.card__gold), so the two golden lines read as the same material. */
.hero__gold,
.card__gold {
  color: var(--brass-lt);
}

/* The fill is only knocked out where the clip is actually supported; without
   the guard an engine that ignores background-clip paints an invisible word. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .hero__gold,
  .card__gold {
    /* both ends of the ramp are --brass-lt, so the word rests a shade brighter
       than the brass around it and the sweep has somewhere to return to */
    background-image: linear-gradient(100deg,
      var(--brass-lt) 0%,
      var(--brass-lt) 40%,
      #fff3d4 50%,
      var(--brass-lt) 60%,
      var(--brass-lt) 100%);
    background-size: 220% 100%;
    /* must tile, not no-repeat: the sweep parks the ramp entirely outside the
       box at both ends of the loop, and an unrepeated gradient leaves nothing
       to clip against there, so the word vanishes. Both ends of the ramp are
       the same colour, so the tiling seam is invisible. */
    background-repeat: repeat;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
    /* held until the loader clears, so the first pass is never spent behind the
       spinner; the delay then lets the eyebrow finish fading in before it runs */
    animation: gold-shimmer 6.5s ease-in-out 1.2s infinite;
    animation-play-state: paused;
  }
  /* Each one is held until the thing that carries it has arrived: the hero
     eyebrow until the loader clears, the card until it has risen out of the
     envelope. Otherwise the first sweep is spent somewhere nobody is looking. */
  .hero.is-live .hero__gold,
  .env.is-open .card__gold { animation-play-state: running; }
}

/* 0% and 100% both park the highlight outside the word, so the loop's snap
   back to the start is invisible. */
@keyframes gold-shimmer {
  0% { background-position: 160% 0; }
  38% { background-position: -60% 0; }
  100% { background-position: -60% 0; }
}

/* DM Zviadi paints far outside its em box - roughly 2.3em from the top of its
   swashes to the bottom of its loops. line-height only moves the baseline
   inside a taller box, so the overflow has to be reserved as real box height;
   the neighbours are then pulled back with negative margins rather than the
   line being squeezed, which would clip the flourishes. */
.hero__name {
  margin: -18px 0 0;
  font-family: var(--f-ka-title);
  font-size: 42px;
  font-weight: 400;
  line-height: 2.3;
  color: var(--cream);
}


/* ── THE PHOTOBOOTH ──────────────────────────────────────────────
   Height is driven off --vh so the whole machine scales with the viewport
   instead of overflowing short phones; the width follows from the source
   aspect ratio. Layer order is the entire illusion:
     1 booth__base   the machine
     2 booth__strip  the print, sliding down from above the slot
     3 booth__shadow the dark lip of the slot, over the print
     4 booth__upper  the top of the machine, hiding the print until it emerges */
.booth {
  position: relative;
  /* The machine is 2.78x as tall as it is wide, so its HEIGHT is what decides
     how big the print inside it reads - the hero has width to spare on every
     phone and none to spare vertically. 64vh is the most that clears the copy
     above and below it on a 667px screen, which is why the rule and the script
     line that used to sit here were taken out. */
  height: calc(var(--vh) * 64);
  aspect-ratio: 294 / 816;
}
.booth__base {
  position: relative;
  z-index: 1;
  width: auto;
  height: 100%;
}

/* The slot mouth, measured straight off booth-full.png: the lip is at 85/816
   of its height and the glass window ends at 690/816. Clipping the print to
   exactly that band is what sells the effect - above the lip it does not exist
   yet, below the window it has passed behind the frame. */
.booth__slot {
  position: absolute;
  z-index: 2;
  top: 11.42%;
  left: 50.5%;
  width: 49%;
  height: 85%;
  overflow: hidden;
  transform: translateX(-50%);
}
.booth__strip {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: auto;
  /* one full print-height above the lip: entirely outside the clip */
  transform: translateY(-100%);
  box-shadow: 0 6px 14px rgba(0, 0, 0, 0.5);
}
.booth.is-printing .booth__strip {
  /* The print (a 426x1182 strip) is shorter than the machine's lit glass, so
     landing its top at 44% of its own height is what centres it there, checked
     in a 390px viewport. Re-measure if the strip's aspect ever changes. */
  transform: translateY(44%);
  transition: transform 3.2s cubic-bezier(0.32, 0.02, 0.2, 1);
}

/* the print emerges at 10.42% but the machine's own slot bar runs to ~12.1%;
   this bridges that sliver so the paper reads as coming out of a dark mouth */
.booth__shadow {
  position: absolute;
  z-index: 3;
  top: 10.42%;
  left: 26%;
  width: 50%;
  height: 4.4%;
  background-image: linear-gradient(to bottom, #0a0504 0%, #150a08 40%, rgba(21, 10, 8, 0) 100%);
  pointer-events: none;
}

.booth__upper {
  position: absolute;
  z-index: 4;
  top: 0;
  left: 0;
  width: 100%;
  height: auto;
  pointer-events: none;
}

.hero__date {
  margin-top: 14px;
  font-family: var(--f-serif);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 3.2px;
  text-indent: 3.2px;
  color: var(--brass);
}


/* ── THE CUE SLOT ───────────────────────────────────────────────
   Two cues share one absolutely positioned box: "tap to open" before the
   machine has printed, "scroll down" after. Stacking them means the swap
   cannot change the slot's height and nudge the machine above it. The box
   is sized for the taller of the two. */
.hero__cue {
  position: relative;
  width: 100%;
  min-height: 56px;
  margin-top: 16px;
}
.hero__tap,
.hero__scroll {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  opacity: 0;
  transform: translateY(10px);
  pointer-events: none;
  transition: opacity 0.8s ease, transform 0.8s ease;
}
.hero__tap.is-visible,
.hero__scroll.is-visible {
  opacity: 1;
  transform: translateY(0);
}
.hero__tap.is-visible { animation: tap-pulse 2.6s ease-in-out 1s infinite; }

/* the tap cue has done its job the moment the machine starts printing */
.hero.is-passed .hero__tap {
  opacity: 0;
  animation: none;
}
/* and the scroll cue has done its when the guest actually scrolls */
.hero__scroll.is-gone { opacity: 0; }

@keyframes tap-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.42; }
}

.hero__tap-ka {
  font-family: var(--f-ka);
  font-size: 18px;
  font-weight: 400;
  letter-spacing: 0.3px;
  color: var(--cream);
  /* a touch of lift off the oxblood so the line carries at arm's length */
  text-shadow: 0 0 18px rgba(224, 198, 138, 0.28);
}
.hero__tap-en {
  font-family: var(--f-serif);
  font-size: 12px;
  letter-spacing: 2.4px;
  text-indent: 2.4px;
  color: var(--brass);
}

.hero__scroll-ka {
  font-family: var(--f-ka);
  font-size: 18px;
  font-weight: 400;
  letter-spacing: 0.3px;
  color: var(--cream);
  text-shadow: 0 0 18px rgba(224, 198, 138, 0.28);
}
/* two adjacent borders on a square rotated 45deg - a chevron with no asset */
.hero__scroll-arrow {
  display: block;
  width: 11px;
  height: 11px;
  margin-top: 6px;
  border-right: 1.5px solid var(--brass);
  border-bottom: 1.5px solid var(--brass);
  animation: scroll-bob 1.7s ease-in-out infinite;
}

@keyframes scroll-bob {
  0%       { transform: rotate(45deg) translate(0, 0);       opacity: 0.35; }
  45%      { transform: rotate(45deg) translate(4px, 4px);   opacity: 1; }
  100%     { transform: rotate(45deg) translate(8px, 8px);   opacity: 0; }
}


/* ══ THE SHEET STACK ════════════════════════════════════════════
   Every section past the hero is a pane laid over the one before it: pulled
   up 34px, rounded along its top two corners, and casting a soft shadow
   upwards onto whatever it covers. A hairline along the lip catches the light
   so the fold still reads when two panes are close in tone.

   The depth here is the fold, the shadow and the tone step - nothing is
   composited or tiled on top. The one gradient inside a pane is a whisper of
   shading, well under the threshold where it would read as a glow. */
.site {
  position: relative;
  z-index: 2;
  opacity: 0;
  visibility: hidden;
  transition: opacity 1.1s ease, visibility 1.1s;
}
.site.is-open { opacity: 1; visibility: visible; }

.sheet {
  position: relative;
  margin-top: -34px;
  border-top-left-radius: 34px;
  border-top-right-radius: 34px;
  box-shadow: 0 -18px 40px rgba(0, 0, 0, 0.42);
  overflow: hidden;
}
/* each pane sits above the one before it, so the lip is never buried */
.sheet--0 { z-index: 0; background-color: var(--swamp-0); }
.sheet--1 { z-index: 1; background-color: var(--swamp-1); }
/* the dress code breaks the green run on purpose - see THE DRESS CODE below */
.sheet--2 { z-index: 2; background-color: var(--card-red); }
.sheet--3 { z-index: 3; background-color: var(--swamp-2); }
.sheet--4 { z-index: 4; background-color: var(--swamp-3); }

/* the whisper of shading inside the pane - light off the centre, a touch of
   weight at the corners. Deliberately far too faint to read as a gradient. */
.sheet--0,
.sheet--1,
.sheet--2,
.sheet--3 {
  background-image: radial-gradient(124% 80% at 50% 32%, rgba(240, 232, 210, 0.05), rgba(0, 0, 0, 0.2) 100%);
}

/* catches the light along the fold */
.sheet::before {
  content: '';
  position: absolute;
  z-index: 3;
  top: 0;
  left: 12%;
  right: 12%;
  height: 1px;
  background-color: var(--line);
}

.wrap {
  position: relative;
  z-index: 2;
  padding: 66px 24px 70px;
  text-align: center;
}


/* ── SHARED SECTION FURNITURE ─────────────────────────────────── */
/* A hairline with a small lozenge at its centre - the section mark repeated
   down the page. Pure CSS so it costs nothing and recolours with the token. */
.orn {
  display: block;
  width: 78px;
  height: 7px;
  margin: 0 auto 24px;
  background-image:
    linear-gradient(to right, rgba(194, 160, 90, 0), var(--brass) 22%, var(--brass) 78%, rgba(194, 160, 90, 0)),
    linear-gradient(var(--brass-lt), var(--brass-lt));
  background-size: 78px 1px, 6px 6px;
  background-position: center center, center center;
  background-repeat: no-repeat;
  border-radius: 1px;
}
.orn--sm {
  width: 52px;
  margin: 24px auto 18px;
  background-size: 52px 1px, 5px 5px;
}

/* ── THE FLOURISH ─────────────────────────────────────────────────
   The hand-drawn gold swash, used once, as the separator at the top of the
   details pane.

   It is a raster, so it cannot be stroke-drawn the way the line icons are.
   The next best thing is a left-to-right clip wipe, which uncovers the left
   tail, then the heart, then the right tail - the order a hand would draw it
   in. The wipe lives on the inner <img> because the outer span's transform is
   already owned by the .reveal, and one element cannot run both. */
.flourish {
  display: block;
  width: 200px;
  margin: 0 auto 18px;
}
.flourish img {
  display: block;
  width: 100%;
  height: auto;
  clip-path: inset(0 100% 0 0);
  transition: clip-path 1.5s cubic-bezier(0.45, 0, 0.2, 1) 0.2s;
}
.flourish.is-in img {
  clip-path: inset(0 0 0 0);
  /* once drawn it keeps a slow drift, the way a pen line on paper never sits
     perfectly still. Starts after the wipe has finished. */
  animation: swash-float 7s ease-in-out 1.9s infinite;
}

@keyframes swash-float {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-4px) rotate(-0.7deg); }
}


.sheet__script {
  font-family: var(--f-script);
  font-size: 24px;
  line-height: 1.15;
  color: var(--brass-lt);
}

/* The DM Zviadi ink reserve again - see .hero__name. Note the top margin is
   zero, not negative: the 2.3 line box is exactly the room the swashes need,
   and pulling it up drives them into the script line above. Only the element
   BELOW is pulled back, where the reserve is empty. */
.sheet__title {
  margin: 0;
  font-family: var(--f-ka-title);
  font-size: 27px;
  font-weight: 400;
  line-height: 2.3;
  color: var(--cream);
}

.sheet__sub {
  /* positive, not negative: DM Zviadi's descender loops reach past the bottom
     of even a 2.3 line box, and anything pulled up here lands under them */
  margin-top: 12px;
  font-size: 12px;
  font-weight: 300;
  letter-spacing: 0.9px;
  color: var(--cream-dim);
}

.sheet__note {
  font-size: 14px;
  font-weight: 300;
  line-height: 1.7;
  color: var(--cream-soft);
}

/* ── scroll reveal (one-shot: unobserved the moment it fires) ── */
.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.9s ease, transform 0.9s var(--ease);
}
.reveal.is-in { opacity: 1; transform: translateY(0); }


/* ══ THE TRAY ═══════════════════════════════════════════════════
   The flat lay. One stage with a fixed aspect ratio holds every piece as a
   percentage of its width, so the arrangement scales as a single object and
   the overlaps hold at every screen size.

   Two elements per piece, as everywhere else on this page: the outer span
   owns the arrival (it is the one thing the reveal drives) and the <img>
   owns the idle drift afterwards. One element cannot run both. */
/* Proportioned off the reference photograph, with one correction: our print
   is a far longer object than the one in it - 430x1590 against roughly
   195x545 - so it is set narrower than the reference's, or its height alone
   would overrun the stage and bury the tray. Everything else keeps the
   reference's fractions, which is what makes the arrangement read. */
.tray-stage {
  position: relative;
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
  aspect-ratio: 74 / 63;
}
/* the tray goes edge to edge, so this pane's wrap gives up its side padding */
.sheet--0 .wrap {
  padding-left: 0;
  padding-right: 0;
}

.tray-item {
  position: absolute;
  display: block;
}
.tray-item img {
  display: block;
  width: 100%;
  height: auto;
}

/* The arrangement, and the paint order that goes with it. The envelope is
   anchored from the bottom rather than the top: its PNG carries 278px of
   transparent sky above the flap, so a top offset would mean nothing you
   could reason about, while its ink stops 2.5% short of the bottom edge. */
.tray-item--tray  { z-index: 1; left: 0;   top: 20%;    width: 100%; }
.tray-item--env   { z-index: 2; right: 12%; bottom: 38%; width: 58%; }
.tray-item--strip { z-index: 3; left: 50%; top: 2%;     width: 19%; }
.tray-item--frame { z-index: 4; left: 12%;  top: 10%;    width: 40%; }

/* ── the arrival ──
   Everything is hidden until the stage is reached, then the pieces land in
   the order they would be put down: the tray first, the envelope onto it,
   the print over the envelope, the frame last. .tray-stage is the only
   .reveal here, so one observer entry drives the whole scene. */
.tray-item {
  opacity: 0;
  transition: opacity 0.9s ease, transform 1s var(--ease);
}
.tray-item--tray  { transform: translateY(26px) scale(0.94); }
.tray-item--env   { transform: translateY(46px); }
.tray-item--strip { transform: translateY(-56px) rotate(-7deg); }
.tray-item--frame { transform: scale(0.82) rotate(-13deg); }

.tray-stage.is-in .tray-item { opacity: 1; }
.tray-stage.is-in .tray-item--tray  { transform: none; transition-delay: 0s; }
.tray-stage.is-in .tray-item--env   { transform: none; transition-delay: 0.3s; }
.tray-stage.is-in .tray-item--strip { transform: rotate(3.5deg); transition-delay: 0.55s; }
.tray-stage.is-in .tray-item--frame { transform: rotate(-3deg); transition-delay: 0.8s; }

/* ── the settle ──
   Once down, the two loose pieces keep breathing, out of phase so the scene
   never rocks as one. The delays clear their own arrival first. */
.tray-stage.is-in .tray-item--strip img { animation: tray-drift 8s ease-in-out 1.8s infinite; }
.tray-stage.is-in .tray-item--frame img { animation: tray-drift 8s ease-in-out -2.2s infinite; }

@keyframes tray-drift {
  0%, 100% { transform: translateY(0) rotate(-0.9deg); }
  50%      { transform: translateY(-7px) rotate(0.9deg); }
}


/* ── THE ENVELOPE ─────────────────────────────────────────────────
   All three envelope PNGs share one 620x686 canvas, so every layer is simply
   inset:0 and they register on top of each other exactly. That is the whole
   reason the open/closed swap needs no measuring.

   The geometry that matters, read off the artwork: the front pocket's ink
   starts 41.3% down the canvas and ends at 97%, so a card parked between
   those two lines is completely hidden by it - which is what "closed" means
   here. The card is 3:2 precisely so that it fits in that band. */
.env-scene {
  margin: 6px auto 0;
  padding: 0 24px;
  max-width: 420px;
}

.env {
  position: relative;
  width: 100%;
  aspect-ratio: 620 / 686;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.env__layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: auto;
}
/* The open envelope's raised flap reaches to 6.9% of the canvas while the
   closed one only starts at 40.6%, so left on, the flap stands up behind a
   shut envelope. It is switched in with the opening instead. */
.env__back {
  z-index: 1;
  opacity: 0;
  transition: opacity 0.18s ease 0.7s;
}
.env__front { z-index: 3; }
.env__closed {
  z-index: 4;
  transition: opacity 0.18s ease 0.7s;
}

/* ── the sequencing ──
   The cover and the card must never be in motion at the same time. Fading a
   shut envelope out while the card rises behind it shows the card straight
   through the paper, which is what made the two look like they were passing
   through each other. So each direction runs in two beats, and the delays
   are asymmetric because the order reverses:

     opening   cover off (0.18s)  ->  card up   (0.78s)
     closing   card down (0.78s)  ->  cover on  (0.18s)

   The base rules above carry the closing delays; the .is-open rules below
   carry the opening ones. */
.env.is-open .env__back {
  opacity: 1;
  transition: opacity 0.18s ease;
}
.env.is-open .env__closed {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease;
}

/* ── the card ──
   Parked inside the pocket at rest and raised out of it when the flap opens.
   The two positions are the whole animation; everything on the card rides
   along with it. */
/* The card is taller than the pocket band it hides in, so no top offset can
   tuck it away - park it low enough for the pocket to cover its head and its
   foot hangs out below the envelope instead. This slot is the fix: it ends on
   the envelope's bottom edge and clips anything past it, so the card can be
   any height at all.

   4%, not the 2.5% the alpha bounds suggest: the artwork's last eight rows are
   a feathered edge, so the last SOLID row is 96.2% of the box, and a card
   clipped to the alpha bound still showed a sliver through that soft edge.

   Invisible when open, too: the front pocket already covers everything below
   41.3%, so the clip line has nothing to show either way. */
.card-slot {
  position: absolute;
  z-index: 2;
  inset: 0 0 4% 0;
  overflow: hidden;
  pointer-events: none;
}

.card {
  position: absolute;
  z-index: 2;
  left: 13%;
  width: 74%;
  height: 15rem;
  aspect-ratio: 4 / 3;
  top: 44%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 6% 8%;
  overflow: hidden;
  text-align: center;
  background-color: #58120b;
  border-radius: 2px;
  transition: top 0.78s var(--ease);
}
.env.is-open .card {
  /* just below the flap's tip at 6.9%, so the raised flap reads behind it */
  top: 8%;
  transition: top 0.78s var(--ease) 0.16s;
}

/* the drawn inner border of the reference card: softened at the top corners,
   square at the foot, a hairline inside the edge */
.card__frame {
  position: absolute;
  inset: 5.5%;
  border: 1px solid rgba(224, 198, 138, 0.42);
  border-radius: 16px 16px 3px 3px;
  pointer-events: none;
}

.card__script {
  font-family: var(--f-script);
  font-size: 19px;
  line-height: 1.1;
  color: var(--brass-lt);
}
.card__body {
  margin-top: 7px;
  font-family: var(--f-ka);
  font-size: 11px;
  font-weight: 300;
  line-height: 1.75;
  color: var(--cream-soft);
}
/* the Zviadi ink reserve again - see .hero__name */
.card__name {
  margin: -6px 0 -12px;
  font-family: var(--f-ka-title);
  font-size: 22px;
  font-weight: 400;
  line-height: 2.3;
  color: var(--cream);
}

/* The second golden line, sitting under the name as the card's kicker. It is
   deliberately the widest line on the card - the shimmer has to be read at a
   glance, and at eyebrow size it disappears against the deep red. The tracking
   is applied to the right of the last glyph too, hence the matching indent. */
.card__gold {
  margin-top: 9px;
  font-family: var(--f-serif);
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 4px;
  text-indent: 4px;
  line-height: 1.2;
}

/* the drawn cake, tucked into a corner as an accent rather than a subject */
.card__cake {
  position: absolute;
  right: 39%;
  top: 8%;
  width: 20%;
  height: auto;
  opacity: 0.5;
}

/* ── the ticket ──
   Laid over the envelope's bottom-right corner, above every envelope layer
   so it reads as sitting on top of the whole thing. */
.ticket {
  position: absolute;
  z-index: 5;
  right: -5%;
  bottom: -7%;
  width: 54%;
}
.ticket img {
  display: block;
  width: 100%;
  height: auto;
}
.ticket__date {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--f-serif);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 1px;
  text-indent: 1px;
  color: #8a5a1c;
}

.env__hint {
  margin-top: 28px;
  font-family: var(--f-ka);
  font-size: 16px;
  font-weight: 300;
  letter-spacing: 0.4px;
  color: var(--cream-dim);
  text-align: center;
  transition: opacity 0.6s ease;
}
/* it has done its job the moment the envelope has been opened once */
.env-scene.is-used .env__hint { opacity: 0; }


/* ── THE SIDE PRINTS ──────────────────────────────────────────────
   Two photographs laid on the details pane, one to each side of the centred
   column of type and sitting fully on the page. They take opposite halves of
   the pane's height as well as opposite sides, so on the narrowest phone -
   where the gap to the type is tightest - they can never meet each other, and
   the drift below can never carry one into the other's band.

   One transform job per element, because a single element cannot run three:
   the outer span carries the scroll parallax (--drift-y, written by
   script.js), .tt-photo__pop takes the press, and the <img> runs the idle
   sway. Stacking them also means the sway keeps going underneath the pop
   instead of being interrupted by it.

   The reveal here is a fade and nothing more. .reveal's usual rise is
   overridden because the outer transform belongs to the parallax, and its
   transform transition is dropped with it - leaving it on would put a 0.9s
   ease behind every drift update and turn the parallax to treacle. */
/* z-index 3, ABOVE .wrap's 2, and that is load-bearing rather than cosmetic:
   .wrap is a full-width block covering the whole pane, so at any lower layer
   it swallows every tap aimed at a photograph and the press does nothing. The
   prints sit in the side margins clear of the centred column of type, so
   painting above it costs nothing visually. */
.tt-photo {
  position: absolute;
  z-index: 3;
  width: 106px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transform: translate3d(0, var(--drift-y, 0px), 0);
}
.tt-photo.reveal {
  transform: translate3d(0, var(--drift-y, 0px), 0);
  transition: opacity 0.9s ease;
}
.tt-photo.reveal.is-in { transform: translate3d(0, var(--drift-y, 0px), 0); }

.tt-photo--l { top: 34%; right: 6px; }
.tt-photo--r { top: 64%; right: 6px; }

/* The press. script.js adds .is-poked and takes it off again on animationend,
   so a second press always replays from the start rather than being swallowed
   by an animation that is still running. */
.tt-photo__pop { display: block; }
.tt-photo.is-poked .tt-photo__pop {
  animation: photo-pop 0.6s cubic-bezier(0.3, 1.4, 0.5, 1);
}

@keyframes photo-pop {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.13); }
  100% { transform: scale(1); }
}

.tt-photo img {
  display: block;
  width: 100%;
  height: auto;
}
/* the two sway out of phase, so the pair never rocks in unison */
.tt-photo--l.is-in img { animation: print-sway 9s ease-in-out 1.1s infinite; }
.tt-photo--r.is-in img { animation: print-sway 9s ease-in-out -3.4s infinite; }

@keyframes print-sway {
  0%, 100% { transform: rotate(-1.4deg) translateY(0); }
  50%      { transform: rotate(1.4deg) translateY(-6px); }
}


/* ══ THE DETAILS ════════════════════════════════════════════════
   Three facts on hairlines, no boxes. The only motion is the line icon
   drawing itself in as its row arrives. */
.facts {
  margin-top: 4px;
  border-top: 1px solid var(--line-soft);
}

/* On a phone the icon sits BESIDE the type rather than above it. Stacked, a
   44px icon plus its margin adds ~60px to every fact and the three of them
   run to roughly 550px; as rows they come to under 300px, and nothing has to
   be made smaller to get there. The icon spans every row of its own grid so
   it stays centred against however many lines the fact has. */
.fact {
  display: grid;
  grid-template-columns: 40px 1fr;
  column-gap: 15px;
  align-items: center;
  padding: 15px 2px;
  text-align: left;
  border-bottom: 1px solid var(--line-soft);
}
.fact > * { grid-column: 2; }
/* staggered so the three arrive in sequence rather than all at once */
.fact:nth-child(2) { transition-delay: 0.12s; }
.fact:nth-child(3) { transition-delay: 0.24s; }


/* pathLength="1" on every path normalises them to one unit, so a single
   dashoffset rule draws them all at the same rate whatever their real length */
.line-icon {
  display: block;
  grid-column: 1;
  grid-row: 1 / -1;
  width: 40px;
  height: 40px;
  margin: 0;
}
.line-icon svg {
  display: block;
  width: 100%;
  height: 100%;
  stroke: var(--brass);
  stroke-width: 1.1;
}
.line-icon path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  transition: stroke-dashoffset 1.1s var(--ease);
}
.reveal.is-in .line-icon path { stroke-dashoffset: 0; }
.reveal.is-in .line-icon path:nth-child(2) { transition-delay: 0.3s; }
.reveal.is-in .line-icon path:nth-child(3) { transition-delay: 0.5s; }
.reveal.is-in .line-icon path:nth-child(4) { transition-delay: 0.68s; }

.fact__label {
  font-family: var(--f-ka);
  font-size: 9px;
  font-weight: 400;
  letter-spacing: 2.6px;
  text-indent: 2.6px;
  color: var(--brass);
}
.fact__value {
  margin-top: 3px;
  font-family: var(--f-ka);
  font-size: 21px;
  font-weight: 300;
  line-height: 1.25;
  color: var(--cream);
}
.fact__note {
  margin-top: 2px;
  font-size: 13px;
  font-weight: 300;
  color: var(--cream-dim);
}
.fact__map {
  justify-self: start;
  margin-top: 8px;
  padding-bottom: 3px;
  font-size: 12px;
  color: var(--brass-lt);
  text-decoration: none;
  border-bottom: 1px solid rgba(194, 160, 90, 0.4);
  transition: color 0.3s ease, border-color 0.3s ease;
}
.fact__map:hover { color: var(--cream); border-bottom-color: var(--cream); }


/* ══ THE DRESS CODE ═════════════════════════════════════════════
   The only pane on the page that is not green, and the only instruction a
   guest has to act on before they arrive. It gets the invitation card's red
   so that the scroll stops here, and the type is set large and plain - this
   is the one place on the site that has to be readable at a glance rather
   than admired. */
.dress__plaque {
  margin: 6px auto 0;
  padding: 26px 18px 24px;
  max-width: 330px;
}

/* the two brass rules that bracket the instruction; they draw outwards from
   the centre as the plaque arrives, which is what catches the eye */
.dress__rule {
  display: block;
  width: 100%;
  height: 1px;
  background-image: linear-gradient(to right, rgba(224, 198, 138, 0), var(--brass-lt) 24%, var(--brass-lt) 76%, rgba(224, 198, 138, 0));
  transform: scaleX(0);
  transition: transform 1s var(--ease) 0.25s;
}
.dress__plaque.is-in .dress__rule { transform: scaleX(1); }

.dress__title {
  margin: 18px 0 0;
  font-family: var(--f-serif);
  font-size: 40px;
  font-weight: 500;
  line-height: 1.05;
  letter-spacing: 6px;
  /* the tracking is applied to the right of the last glyph too, so without
     this the line sits visibly left of centre */
  text-indent: 6px;
  color: var(--cream);
}

/* A slow sweep of light across the letters. It is guarded because the fallback
   for background-clip: text is transparent text - an unsupported browser would
   be handed an invisible headline, which is the one thing this pane cannot
   afford. Everything outside the guard keeps the plain cream. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .dress__plaque.is-in .dress__title {
    background-image: linear-gradient(100deg, var(--cream) 34%, #fff6e2 46%, var(--brass-lt) 52%, var(--cream) 64%);
    background-size: 260% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    animation: dress-sheen 4.5s ease-in-out 1.1s infinite;
  }
}

@keyframes dress-sheen {
  0%, 100% { background-position: 130% 0; }
  50%      { background-position: -30% 0; }
}

.dress__extra {
  margin: 10px 0 18px;
  font-family: var(--f-serif);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 3.6px;
  text-indent: 3.6px;
  color: var(--brass-lt);
}

.dress__body {
  margin-top: 24px;
  font-family: var(--f-serif);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.75;
  color: var(--cream-soft);
}

.dress__kicker {
  margin-top: 14px;
  font-family: var(--f-serif);
  font-size: 30px;
  line-height: 1.1;
  color: var(--brass-lt);
}


/* ══ RSVP ═══════════════════════════════════════════════════════ */
/* The two polaroids lead the pane in place of a hairline mark. The sway lives
   on the inner <img> for the same reason the flourish's wipe does: the outer
   span's transform belongs to the .reveal. */
.dec-photos {
  display: block;
  width: 212px;
  margin: 0 auto 14px;
}
.dec-photos img {
  display: block;
  width: 100%;
  height: auto;
  transform-origin: 50% 0;
}
.dec-photos.is-in img { animation: photos-sway 8s ease-in-out 0.9s infinite; }

@keyframes photos-sway {
  0%, 100% { transform: rotate(-1.1deg) translateY(0); }
  50%      { transform: rotate(1.1deg) translateY(-5px); }
}

.form {
  max-width: 360px;
  margin: 26px auto 0;
  text-align: left;
}

.field { margin-bottom: 20px; }

.field__label {
  display: block;
  margin-bottom: 8px;
  font-family: var(--f-ka);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 2.4px;
  text-indent: 2.4px;
  color: var(--brass);
}

/* underlined rather than boxed: one hairline per field keeps the form as
   quiet as the rest of the pane */
.field__input {
  width: 100%;
  padding: 9px 2px;
  font-family: var(--f-ka);
  font-size: 16px;
  font-weight: 300;
  color: var(--cream);
  background-color: transparent;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  outline: none;
  transition: border-color 0.35s ease;
  -webkit-appearance: none;
  appearance: none;
}
.field__input:focus { border-bottom-color: var(--brass); }
.field__input.is-error { border-bottom-color: #c4795a; }

.choices { display: grid; gap: 9px; }
.choices--two { grid-template-columns: 1fr 1fr; }

.choice { display: block; cursor: pointer; }
/* visually hidden, still focusable and still the accessible control */
.choice input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.choice__box {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 8px 6px;
  font-size: 13px;
  font-weight: 300;
  line-height: 1.3;
  text-align: center;
  color: var(--cream-soft);
  background-color: transparent;
  border: 1px solid var(--line);
  border-radius: 3px;
  transition: color 0.35s ease, background-color 0.35s ease, border-color 0.35s ease;
}
.choice input:checked + .choice__box {
  color: var(--swamp-3);
  background-color: var(--brass-lt);
  border-color: var(--brass-lt);
}
.choice input:focus-visible + .choice__box { border-color: var(--brass); }

.btn {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 14px 18px;
  font-family: var(--f-ka);
  font-size: 14px;
  font-weight: 400;
  letter-spacing: 2px;
  text-indent: 2px;
  color: var(--cream);
  background-color: transparent;
  border: 1px solid var(--brass);
  border-radius: 3px;
  transition: color 0.35s ease, background-color 0.35s ease;
}
.btn:hover { color: var(--swamp-3); background-color: var(--brass); }
.btn:disabled { opacity: 0.5; cursor: default; }

.form__status {
  min-height: 20px;
  margin-top: 14px;
  font-size: 13px;
  color: var(--cream-dim);
  text-align: center;
}
.form__status.is-error { color: #d99a78; }

.thanks { padding-top: 30px; }
.thanks__script {
  font-family: var(--f-script);
  font-size: 32px;
  line-height: 1.1;
  color: var(--brass-lt);
}
.thanks__text {
  margin-top: 12px;
  font-size: 14px;
  font-weight: 300;
  line-height: 1.7;
  color: var(--cream-soft);
}
/* the deadline is stale the moment someone has answered */
.is-done #rsvp-deadline { display: none; }


/* ══ CONFETTI ═══════════════════════════════════════════════════
   Built by script.js on a successful RSVP and torn straight back down -
   the container is inert and empty the rest of the time. Thin brass and
   cream slivers, not party plastic. */
.confetti {
  position: fixed;
  inset: 0;
  z-index: 9500;
  overflow: hidden;
  pointer-events: none;
}
.confetti__bit {
  position: absolute;
  top: -20px;
  width: 3px;
  height: 14px;
  border-radius: 2px;
  animation: fall 3s ease-in forwards;
}

/* the fall distance is pinned like every other vh on the page: a keyframe
   cannot be overridden by a class, so it has to read --vh directly or an
   in-app browser's inflated vh would drop the bits far past the fold */
@keyframes fall {
  0%   { opacity: 0; transform: translateY(0) rotate(0deg); }
  8%   { opacity: 1; }
  100% { opacity: 0; transform: translateY(calc(var(--vh) * 110)) rotate(540deg); }
}


/* ══ FOOTER ═════════════════════════════════════════════════════ */
.footer .wrap { padding: 54px 24px 40px; }

.footer__name {
  margin: -18px 0 -16px;
  font-family: var(--f-ka-title);
  font-size: 26px;
  font-weight: 400;
  line-height: 2.3;
  color: var(--cream);
}
.footer__date {
  font-size: 12px;
  font-weight: 300;
  letter-spacing: 0.8px;
  color: var(--cream-dim);
}

.credit {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-top: 30px;
  font-family: var(--f-serif);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-decoration: none;
  opacity: 0.3;
  transition: opacity 0.3s ease;
}
.credit:hover { opacity: 0.7; }
.credit__logo {
  width: 18px;
  height: 18px;
  /* the svg ships dark; this recolours it to the footer's brass */
  filter: brightness(0) saturate(100%) invert(72%) sepia(19%) saturate(882%) hue-rotate(2deg) brightness(90%) contrast(87%);
}


/* ══════════════════════════════════════════════════════════════
   481px +
   ══════════════════════════════════════════════════════════════ */
@media (min-width: 481px) {
  .hero__name { font-size: 52px; }
  .booth { height: calc(var(--vh) * 62); }
  .hero__tap-ka,
  .hero__scroll-ka { font-size: 19px; }

  .wrap { padding: 78px 30px 82px; }
  .sheet__script { font-size: 28px; }
  .sheet__title { font-size: 31px; }

  .fact__value { font-size: 27px; }
  .dress__plaque { max-width: 380px; }
  .dress__title { font-size: 48px; letter-spacing: 7px; text-indent: 7px; }
  .dress__body { font-size: 17px; }
  .dress__kicker { font-size: 34px; }
  .flourish { width: 224px; }
  .dec-photos { width: 240px; }
  .tray-stage { max-width: 620px; }
  .env-scene { max-width: 470px; margin-top: 36px; }
  .card__script { font-size: 22px; }
  .card__body { font-size: 11px; }
  .card__name { font-size: 25px; }
  .card__gold { font-size: 17px; letter-spacing: 4.6px; text-indent: 4.6px; }
  .ticket__date { font-size: 17px; }
  .tt-photo { width: 138px; }
  .tt-photo--l { right: 14px; }
  .tt-photo--r { right: 14px; }
  .footer__name { font-size: 29px; }
}


/* ══════════════════════════════════════════════════════════════
   769px +  - the three facts come off the stack and stand side by side
   ══════════════════════════════════════════════════════════════ */
@media (min-width: 769px) {
  .music { top: 22px; right: 22px; width: 44px; height: 44px; }

  .hero__inner { max-width: 520px; }
  .hero__eyebrow { font-size: 11px; letter-spacing: 4px; text-indent: 4px; }
  .hero__name { font-size: 64px; }
  .booth { height: calc(var(--vh) * 60); }
  .hero__date { font-size: 12px; }
  .hero__tap-ka,
  .hero__scroll-ka { font-size: 20px; }
  .hero__tap-en { font-size: 13px; }
  .hero__cue { min-height: 62px; }

  .sheet {
    margin-top: -44px;
    border-top-left-radius: 44px;
    border-top-right-radius: 44px;
  }
  .wrap { max-width: 940px; margin: 0 auto; padding: 106px 40px 112px; }

  .sheet__script { font-size: 33px; }
  .sheet__title { font-size: 36px; }
  .sheet__sub { font-size: 13px; }
  .sheet__note { font-size: 15px; }

  .facts {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    margin-top: 48px;
    border-top: 0;
  }
  /* side by side there is width to spare, so the rows unpack back into
     centred columns with the icon over the type */
  .fact {
    display: block;
    padding: 8px 18px 4px;
    text-align: center;
    border-bottom: 0;
  }
  .line-icon { width: 44px; height: 44px; margin: 0 auto 14px; }
  .fact__label { font-size: 10px; }
  .fact__value { margin-top: 7px; }
  .fact__map { display: inline-block; margin-top: 12px; }
  /* the hairlines turn with the layout: on one row the separators are the
     two gaps between the columns, never a rule after the last */
  .fact + .fact { border-left: 1px solid var(--line-soft); }

  .flourish { width: 250px; margin-bottom: 22px; }
  .dec-photos { width: 264px; margin-bottom: 18px; }
  .tray-stage { max-width: 820px; }
  .env-scene { max-width: 540px; margin-top: 48px; }
  .card__script { font-size: 26px; }
  .card__body { font-size: 12.5px; }
  .card__name { font-size: 29px; }
  .card__gold { margin-top: 12px; font-size: 19px; letter-spacing: 5.2px; text-indent: 5.2px; }
  .ticket__date { font-size: 20px; letter-spacing: 1.6px; text-indent: 1.6px; }
  .env__hint { font-size: 14px; }

  /* the pane is far wider than its 940px column here, so the prints get both
     more size and more clearance from the type */
  .tt-photo { width: 210px; }
  .tt-photo--l { top: 26%; right: 38px; }
  .tt-photo--r { top: 58%; right: 38px; }

  .fact__value { font-size: 28px; }

  .dress__plaque { max-width: 520px; padding: 34px 26px 30px; }
  .dress__title { font-size: 64px; letter-spacing: 9px; text-indent: 9px; }
  .dress__extra { font-size: 15px; }
  .dress__body { font-size: 19px; }
  .dress__kicker { font-size: 40px; }

  .form { max-width: 400px; margin-top: 42px; }
  .choice__box:hover { border-color: var(--brass); color: var(--cream); }

  .footer .wrap { padding: 76px 40px 52px; }
  .footer__name { font-size: 32px; }

  .mobile-prompt { display: block; }
}


/* ══════════════════════════════════════════════════════════════
   1025px +
   ══════════════════════════════════════════════════════════════ */
@media (min-width: 1025px) {
  .hero__name { font-size: 74px; }
  .booth { height: calc(var(--vh) * 58); }
  .wrap { padding: 124px 40px 130px; }
}


/* ══ REDUCED MOTION ═════════════════════════════════════════════
   Everything decorative stops; nothing that carries meaning disappears.
   The reveals and the icon strokes are flattened to their finished state
   rather than left at zero, which would hide half the page. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .reveal { opacity: 1; transform: none; }
  .line-icon path { stroke-dashoffset: 0; }
  .flourish img { clip-path: inset(0 0 0 0); }
  .dress__rule { transform: scaleX(1); }
  .tray-item { opacity: 1; }
  .tray-item--tray, .tray-item--env { transform: none; }
  .tray-item--strip { transform: rotate(3.5deg); }
  .tray-item--frame { transform: rotate(-3deg); }
}
