/* ============================================================
   Varuni & Shimon — Envelope Invitation (v28 · real-photo envelope,
   raised open flap, framed photo + playlist card, floating song
   button, golden-thread journey, full invite below: celebrations ·
   venue · kota · gratitude · faqs · countdown · contact)
   The envelope is a photograph (assets/envelope.webp — tulle lace,
   silver V&S crest), sliced into two registering layers:
     .flap-skin  — the photo's upper region incl. lace + crest; swings
                   open in 3D carrying the ruffle with it
     .env-pocket — the photo below the lace's scalloped edge; stays put
                   and shows while the envelope is closed
     .env-front  — the front's folded side flaps: the shallow velvet V
                   (sides 40%H, broad flat base 30–70%W at ~69%H) that
                   the folded-down top flap hides while closed; revealed
                   as the flap swings open, and the risen contents tuck
                   behind it
   At rest the scene reads as real stationery: .env-flap-raised (the
   opened flap from behind) stands up above the envelope, the invitation
   dominates, the ornate oval photo peeks out left, and the wedding-
   playlist card (matted from assets/music-player.png) tucks in right.
   ============================================================ */

/* Arvina — the wedding calligraphy (Creative Fabrica) */
@font-face {
  font-family: "Arvina";
  src: url("assets/fonts/Arvina.ttf") format("truetype");
  font-display: swap;
}

/* ---------- tokens ---------- */
:root {
  --velvet:      #4a1520;   /* burgundy felt base         */
  --velvet-deep: #2c0b13;   /* shadowed burgundy          */
  --ivory:       #f2ece0;   /* primary text on velvet     */
  --champagne:   #d9c3a0;   /* secondary text on velvet   */
  --stage:      #e2ddd7;   /* the quiet greige backdrop   */
  --beige:      #e7e2dc;   /* page wash (over the paper)  */
  --beige-deep: #dcd6cf;
  --paper:      #f9f6ef;   /* card / photo paper          */
  --env-paper:  #f6efdf;   /* envelope face               */
  --ink:        #4c4134;   /* text on ivory surfaces      */
  --ink-soft:   #7d7160;   /* secondary on ivory          */
  --gold:       #b3905a;   /* antique gold hairlines      */
  --gold-bright:#d3ac6e;
  --rose:       #c8848c;
  --rose-deep:  #5e1c2a;   /* burgundy script + accents   */
  --sun:        #e3af35;
  --leaf:       #7d8f5e;
  --lining:     #4a1520;   /* flap lining (burgundy)      */

  --serif:  "Cormorant Garamond", Georgia, serif;
  --script: "Great Vibes", "Arvina", "Snell Roundhand", cursive;
  --caps:   "Jost", "Avenir Next", system-ui, sans-serif;

  --ease-out:  cubic-bezier(.22, .9, .3, 1);
  --ease-soft: cubic-bezier(.33, .68, .18, 1);

  --env-w: min(640px, 90vw, 118vh);     /* fallback for browsers without dvh */
  --env-w: min(640px, 90vw, 118dvh);    /* the envelope's width, one source of truth */
}

/* ---------- base ---------- */
* { box-sizing: border-box; margin: 0; padding: 0; }

html { scroll-behavior: smooth; }

body {
  font-family: var(--serif);
  color: var(--ink);
  /* the quiet greige studio wall — a whisper of handmade paper beneath,
     so it never reads as flat digital grey */
  background:
    radial-gradient(120% 90% at 50% 6%, rgba(250, 248, 245, .5), rgba(250, 248, 245, 0) 55%),
    linear-gradient(rgba(226, 221, 215, .88), rgba(222, 216, 210, .88)),
    url("assets/paper.jpg") center / cover no-repeat fixed,
    var(--stage);
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  min-height: 100dvh;
}

/* scroll is locked until the envelope is opened */
body.locked { overflow: hidden; height: 100vh; height: 100dvh; }

::selection { background: rgba(179, 144, 90, .32); }

a { color: inherit; }

button { font: inherit; color: inherit; }

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

.script { font-family: var(--script); font-weight: 400; }

/* ============================================================
   HERO
   ============================================================ */
.hero {
  position: relative;
  min-height: 100vh;
  min-height: 100dvh;
  overflow: hidden;               /* contains the flying card */
  display: grid;
  place-items: center;
  /* a touch of extra headroom above, so the opened flap + names both breathe */
  padding: clamp(48px, 8vh, 96px) 16px 24px;
  padding: clamp(48px, 8dvh, 96px) 16px 24px;
}

/* ---------- watercolor stationery backdrop ---------- */
.bg-flora {
  position: absolute; inset: 0;
  pointer-events: none;
}
.bg-border {
  position: absolute; left: 50%; bottom: -2%;
  width: max(105%, 900px);
  transform: translateX(-50%);
  opacity: .9;
}
.bg-corner {
  position: absolute;
  opacity: .38;
}
.bg-corner-l {
  width: clamp(110px, 15vw, 190px);
  left: -1.5%; top: -2%;
  transform: rotate(165deg);
}
.bg-corner-r {
  width: clamp(90px, 12vw, 150px);
  right: 1%; top: -3%;
  transform: rotate(-8deg);
}

/* film-grain texture over the hero — makes flat areas feel like paper */
.hero::after {
  content: "";
  position: absolute; inset: 0;
  z-index: 30;
  pointer-events: none;
  opacity: .05;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/><feColorMatrix type='saturate' values='0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
}

/* ============================================================
   ENVELOPE SCENE
   ============================================================ */
.stage { position: relative; }

.stage-envelope {
  width: var(--env-w);
  margin-top: 18px;              /* air between the names and the flap */
  display: flex;
  flex-direction: column;
  align-items: center;
  animation: envIn .95s var(--ease-out) both;
}
@keyframes envIn {
  from { opacity: 0; transform: translateY(30px) scale(.965); }
}
/* after opening the envelope STAYS — main.js settles it below the card */

/* envelope + flowers move as one piece when the scene comes to rest.
   One light: soft, from the upper left — so everything's shadow leans
   gently down-right and stays quiet. */
.env-group {
  position: relative;
  width: 100%;
  filter: drop-shadow(7px 18px 26px rgba(76, 60, 40, .22));
}

/* vintage lace doily under the envelope — champagne thread, quiet now.
   It greets the closed envelope and dissolves as the flap opens, keeping
   the opened scene clean like the inspiration demo. */
.lace-ring {
  position: absolute; left: 50%;
  top: calc(.32 * var(--env-w));       /* ring centre = envelope centre */
  width: calc(1.24 * var(--env-w));
  height: auto;                        /* keep the doily perfectly round */
  transform: translate(-50%, -50%);
  z-index: 0;
  opacity: .5;
  filter: drop-shadow(0 6px 18px rgba(0, 0, 0, .22));
  pointer-events: none;
  transition: opacity 1.4s ease .2s;
}
body.opening .lace-ring,
body.opened .lace-ring { opacity: 0; }

/* ---------- the button ---------- */
.envelope {
  position: relative;
  width: 100%;
  aspect-ratio: 1280 / 832;             /* the photo's own proportions */
  background: none; border: 0; padding: 0;
  cursor: pointer;
  container-type: inline-size;          /* children can size with cqw */
  animation: floatY 5.5s ease-in-out infinite;
  z-index: 2;
}
@keyframes floatY {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-7px); }
}
body.opening .envelope { animation-play-state: paused; z-index: 7; }
/* once the card has landed, the front flowers overlap the envelope again */
body.rested .envelope { z-index: 2; }
body.opened .envelope { pointer-events: none; }

@media (hover: hover) {
  .envelope:hover { filter: drop-shadow(0 10px 22px rgba(76, 65, 52, .14)); }
}
.envelope:active { transform: translateY(-2px) scale(.995); }

/* ground shadow — softens as the card is lifted out */
.env-shadow {
  position: absolute; left: 6%; right: 6%; bottom: -7%;
  height: 7%;
  background: radial-gradient(50% 50% at 50% 50%, rgba(0, 0, 0, .5), transparent 72%);
  transition: transform .9s ease, opacity .9s ease;
}
body.card-up .env-shadow { opacity: .62; transform: scaleX(.965); }

/* ---------- the raised flap — the opened flap seen from its back ----------
   Stands up behind the envelope once the photo flap swings open, so the
   resting scene reads as a real opened envelope: flap up, card out of the
   pocket. Exactly as wide as the envelope — its hinge spans the envelope's
   full top edge, corners to corners. Burgundy velvet like the body, a
   champagne hairline + stitched echo along its edge (the SVG lives in
   index.html). Leaning back a touch; rises as the flap opens. */
.env-flap-raised {
  position: absolute;
  left: 0; right: 0;
  bottom: 98%;                 /* hinge sits on the envelope's top edge */
  height: 36%;
  z-index: 0;                  /* painted before .env-back → behind the body */
  pointer-events: none;
  transform-origin: 50% 100%;
  transform: rotateX(-11deg) scaleY(.1);
  opacity: 0;
  transition:
    transform 1.15s cubic-bezier(.3, 1.16, .36, 1) .5s,
    opacity .55s ease .5s;
  filter: drop-shadow(0 9px 14px rgba(44, 11, 19, .32));
}
.env-flap-raised svg {
  display: block; width: 100%; height: 100%;
}
/* the real-photo flap (flap-raised.webp): the bitmap's hinge is its straight
   TOP edge — flip it so the hinge meets the envelope and the lace tip points
   up; fills the same box the SVG filled (hinge corner to corner) */
.env-flap-raised img {
  display: block; width: 100%; height: 100%;
  object-fit: fill;
  transform: scaleY(-1);
}
body.opening .env-flap-raised,
body.opened .env-flap-raised {
  opacity: 1;
  transform: rotateX(-11deg) scaleY(1);
}

/* ---------- back panel — lighter now, so the burgundy reads as felt
   paper rather than a dense block; rounded like cut stationery ---------- */
.env-back {
  position: absolute; inset: 0;
  background:
    linear-gradient(rgba(88, 28, 42, .42), rgba(64, 18, 30, .6)),
    url("assets/velvet.jpg") center / cover;
  border-radius: 10px 10px 12px 12px;
  box-shadow:
    inset 0 1px 0 rgba(255, 226, 184, .12),
    inset 0 0 0 1px rgba(255, 220, 180, .05);
}
/* striped silk liner — finer and quieter than before, so the open mouth
   whispers rather than stripes. Clipped to the same deep V as the flap
   so it never peeks while the envelope is closed. */
.env-inner {
  position: absolute; left: 2%; right: 2%; top: 1%; bottom: 0;
  z-index: 1;
  clip-path: polygon(0 0, 100% 0, 50% 84%);
  background:
    linear-gradient(180deg, rgba(44, 11, 19, .5), rgba(44, 11, 19, .12) 62%, rgba(44, 11, 19, 0)),
    repeating-linear-gradient(90deg, rgba(109, 30, 44, .45) 0 5px, rgba(158, 66, 79, .22) 5px 9px);
}

/* ---------- card inside (z: back 1 < card 2 < pocket 3 < flap 4) ----------
   The invitation is the hero: wider than before, dead centre, so the eye
   lands names → card → everything else. */
.env-card {
  position: absolute;
  left: 26.8%;       /* centred on the envelope's vertical line */
  bottom: 0;         /* sunk fully out of sight: at this width the card is a
                        whisker shorter than the envelope, so the closed flap
                        + pocket cover it edge to edge — no sliver shows */
  width: 46.4%;      /* final size straight away — no resizing later */
  aspect-ratio: 5 / 7;
  z-index: 2;
  container-type: inline-size;
  will-change: transform;
}
body.card-up .env-card {
  transform: translateY(-26%);       /* rises out of the deep-V mouth — final size */
  transition: transform 1.05s var(--ease-out);
}
/* the card rises and stands perfectly straight — the still point of the
   composition; the tilt lives in the photos around it */
/* contact shadow — a soft pool where the card meets the envelope mouth,
   not a box-shadow: a blurred radial ellipse tucked behind the card */
.env-card::after {
  content: "";
  position: absolute;
  left: 5%; right: 5%; bottom: -3%; height: 6%;
  background: radial-gradient(50% 50% at 50% 50%, rgba(26, 8, 13, .38), rgba(26, 8, 13, 0) 70%);
  opacity: 0;
  transition: opacity .9s ease .35s;
  z-index: -1;               /* behind the card, above the striped liner */
}
body.card-up .env-card::after { opacity: 1; }

/* ---------- the wedding-playlist card (right of the card) ----------
   The vinyl-record tip matted out of assets/music-player.png (the sheet's
   checkerboard was baked in — see matte pipeline). Painted AFTER .env-card,
   so it overlaps the invitation's right shoulder in front — one pretty
   card leaning on the stationery. Its foot stays LOW, slipping behind
   the front fold's flat base (z 2 < .env-front 3), so its lower edge
   tucks into the felt: tucked, not stuck on. Rise maths: 18.5%-width ÷
   (660/1046) ≈ 29.3% of envelope width ≈ 45.1% of envelope height; sunk
   top sits at 54.9%H, so -72% of its own height lands its top ~22% down
   the mouth with its foot behind the fold's flat (edge ~67%H at its
   left corner, foot ~68.5%H). Same rise at every screen height. With
   CONFIG.song.url set it plays the song on tap. */
.env-playlist {
  position: absolute;
  right: 8%;         /* its left edge laps ~2.5% of the envelope onto the card */
  bottom: 0;
  width: 18.5%;
  z-index: 2;        /* above the card (painted later), below the pocket */
  opacity: 0;        /* kept invisible until the rise starts */
  will-change: transform;
  pointer-events: none;   /* taps belong to the envelope until it's open */
}
.env-playlist img {
  display: block; width: 100%; height: auto;
  filter: drop-shadow(2px 5px 9px rgba(76, 60, 40, .18))
          drop-shadow(5px 12px 24px rgba(76, 60, 40, .12));
}
/* the printed line the raster's own 'EST. 2027' left blank stays blank:
   it sits at 85.6% of the card, and the fold's right diagonal passes
   right through it — a live line there would tuck half-read behind the
   felt, so the card keeps its quiet blank hem below the pearls */
body.opening .env-playlist { opacity: 0; transition: none; }
body.card-up .env-playlist {
  opacity: 1;
  transform: translateY(-72%) rotate(-3.5deg);
  transition: transform 1.05s var(--ease-out), opacity .3s ease;
}

/* ---------- the printed play button, made touchable ----------
   A transparent disc registered over the artwork's gold play button
   (measured: centre 50.4% / 49.6% of the card, gold circle 20.15% of
   the card wide = 3.73cqw). The tap target is finger-sized (never
   under 26px), while the hover glow and the playing pulse live on a
   ::before sized to the PRINTED circle — so every golden ring hugs the
   artwork's own circle evenly, never lop-sided around it. */
.pl-play {
  position: absolute;
  left: 50.4%; top: 49.6%;
  width: 20.2%; min-width: 26px; min-height: 26px;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  pointer-events: none;
}
.pl-play::before {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  width: 3.73cqw; height: 3.73cqw;   /* the printed circle, exactly */
  transform: translate(-50%, -50%);
  border-radius: 50%;
}
body.opened .env-playlist.song-live .pl-play {
  pointer-events: auto;
  cursor: pointer;
}
.env-playlist.song-live .pl-play:hover::before {
  box-shadow: 0 0 0 2px rgba(244, 227, 187, .5), 0 0 14px 4px rgba(233, 200, 138, .45);
}
.env-playlist.song-live .pl-play:active::before { transform: translate(-50%, -50%) scale(.93); }
.env-playlist.playing .pl-play::before { animation: plPulse 1.9s ease-out infinite; }
@keyframes plPulse {
  0%   { box-shadow: 0 0 0 0 rgba(236, 209, 152, .55), 0 0 10px 2px rgba(233, 200, 138, .3); }
  70%  { box-shadow: 0 0 0 9px rgba(236, 209, 152, 0), 0 0 10px 2px rgba(233, 200, 138, .3); }
  100% { box-shadow: 0 0 0 9px rgba(236, 209, 152, 0), 0 0 10px 2px rgba(233, 200, 138, .3); }
}

/* ---------- the pearl sound-wave ----------
   The artwork's pearl-tipped line (measured: x 22.3→78.8% of the card,
   centred 36.4%H) becomes a strand of ten tiny pearls that dance along
   it while the song "plays". Each bead carries its own amplitude
   (--amp: quiet at the ends, lively in the middle — a plucked-string
   arc), its own tempo (--dur) and its own phase, so the crest keeps
   shifting shape instead of one rigid bar bobbing on a line; the path
   itself is irregular (big rise, small dip below the line, secondary
   rise), like audio, not a metronome. Idle: hidden, the printed line
   shows; playing: the beads fade in. */
.pl-wave {
  position: absolute;
  left: 22.3%; width: 56.5%;
  top: 33.9%; height: 5%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  opacity: 0;
  transition: opacity .6s ease;
  pointer-events: none;
}
.env-playlist.playing .pl-wave { opacity: 1; }
.pl-wave i {
  --amp: 1;
  --dur: 1.1s;
  width: 3.65%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #fff, #f4ecdc 45%, #d9c3a0 78%, #b39467);
  box-shadow: 0 1px 2px rgba(56, 22, 12, .45);
}
.env-playlist.playing .pl-wave i {
  animation: waveDance var(--dur) ease-in-out infinite;
  animation-delay: var(--del, 0s);
}
/* amplitude arc + free-running tempos: ends whisper, middle sings */
.pl-wave i:nth-child(1)  { --amp: .30; --dur: 1.16s; --del: -.10s; }
.pl-wave i:nth-child(2)  { --amp: .55; --dur:  .98s; --del: -.52s; }
.pl-wave i:nth-child(3)  { --amp: .85; --dur: 1.24s; --del: -.30s; }
.pl-wave i:nth-child(4)  { --amp: 1.05; --dur: 1.04s; --del: -.78s; }
.pl-wave i:nth-child(5)  { --amp: 1.20; --dur: 1.32s; --del: -.44s; }
.pl-wave i:nth-child(6)  { --amp: 1.12; --dur:  .94s; --del: -.20s; }
.pl-wave i:nth-child(7)  { --amp: .95; --dur: 1.20s; --del: -.62s; }
.pl-wave i:nth-child(8)  { --amp: .72; --dur: 1.00s; --del: -.36s; }
.pl-wave i:nth-child(9)  { --amp: .48; --dur: 1.28s; --del: -.86s; }
.pl-wave i:nth-child(10) { --amp: .26; --dur: 1.10s; --del: -.05s; }
@keyframes waveDance {
  0%, 100% { transform: translateY(0)                        scale(.8);  opacity: .8; }
  28%      { transform: translateY(calc(var(--amp) * -235%)) scale(1.2); opacity: 1; }
  52%      { transform: translateY(calc(var(--amp) * 55%))   scale(.86); }
  74%      { transform: translateY(calc(var(--amp) * -120%)) scale(1.05); }
}
/* while the local audio plays, the analyser owns the beads — its data
   replaces the CSS dance entirely */
.env-playlist.audio-live .pl-wave i { animation: none; }
/* the whole card answers a tap only once the envelope is open */
body.opened .env-playlist.no-song { pointer-events: auto; cursor: default; }
/* no song configured: a gentle "nothing here yet" wiggle on tap */
.env-playlist.shrug { animation: plShrug .55s var(--ease-soft); }
@keyframes plShrug {
  0%, 100% { transform: translateY(-72%) rotate(-3.5deg); }
  30%      { transform: translateY(-72%) rotate(-1.4deg); }
  65%      { transform: translateY(-72%) rotate(-5deg); }
}

/* ---------- the framed couple photo inside the envelope ----------
   Lives inside the button on the card's layer (z 2: above the striped
   interior, below the front pocket and the card, which is painted after
   it) — sunk fully out of sight while closed; on tap it rises with the
   card: the ornate oval frame peeks out to the LEFT of the card. */
.env-photos {
  position: absolute;
  left: 1%;
  right: 1%;
  bottom: 2%;
  width: 98%;
  aspect-ratio: 8 / 5;
  z-index: 2;
  will-change: transform;
}
body.card-up .env-photos {
  transform: translateY(-22%);
  transition: transform 1.05s var(--ease-out);
}

/* ---------- front pocket — the photo below the lace's scalloped edge ----------
   Cut along the real ruffle's outer contour, so when the flap (which owns
   the lace) swings open, the pocket keeps a scalloped felt rim with the
   dark interior shadowing behind it — exactly like a real opened envelope. */
.env-pocket {
  position: absolute; inset: 0;
  z-index: 3;
  background: url("assets/envelope.webp") center / 100% 100% no-repeat;
  clip-path: polygon(100.0% 42.01%, 98.75% 42.01%, 97.5% 43.81%, 96.25% 44.17%, 95.0% 45.01%, 93.75% 46.21%, 92.5% 48.86%, 91.25% 52.34%, 90.0% 54.39%, 88.75% 54.75%, 87.5% 55.83%, 86.25% 55.83%, 85.0% 56.67%, 83.75% 63.28%, 82.5% 64.12%, 81.25% 64.12%, 80.0% 64.96%, 78.75% 65.92%, 77.5% 70.01%, 76.25% 72.78%, 75.0% 73.74%, 73.75% 75.3%, 72.5% 75.42%, 71.25% 75.42%, 70.0% 77.7%, 68.75% 79.63%, 66.88% 83.11%, 65.62% 83.11%, 64.38% 83.71%, 63.12% 85.76%, 61.88% 89.12%, 59.38% 91.17%, 58.12% 92.25%, 56.88% 92.97%, 55.62% 94.53%, 54.38% 95.01%, 53.12% 95.37%, 51.88% 95.37%, 50.62% 95.37%, 49.38% 95.37%, 48.12% 95.37%, 46.88% 95.37%, 45.62% 95.25%, 44.38% 94.29%, 43.12% 92.85%, 41.25% 88.76%, 40.0% 88.76%, 38.75% 88.76%, 37.5% 87.8%, 36.25% 87.32%, 34.38% 86.0%, 33.12% 83.35%, 31.88% 77.46%, 30.62% 77.46%, 29.38% 77.46%, 26.25% 75.06%, 25.0% 71.81%, 23.75% 71.57%, 22.5% 66.41%, 21.25% 66.17%, 20.0% 64.72%, 17.5% 63.52%, 16.25% 61.84%, 14.38% 55.71%, 12.5% 55.71%, 11.25% 54.39%, 8.75% 52.7%, 7.5% 49.58%, 6.25% 43.93%, 5.0% 43.69%, 3.75% 43.21%, 2.5% 42.73%, 1.25% 40.8%, 0.62% 40.68%, 0 40.68%, 0 98.4%, 1.1% 100%, 98.9% 100%, 100% 98.4%, 100% 40.68%);
}
/* soft shadow the open flap casts onto the pocket */
.env-pocket::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(0, 0, 0, .30), rgba(0, 0, 0, 0) 44%);
  opacity: 0;
  transition: opacity 1s ease .55s;
}
body.opening .env-pocket::after { opacity: 1; }

/* ---------- the front's folded side flaps — the broad-based V ----------
   A real envelope's front is a fold of side flaps meeting in a shallow
   V; the top flap folded down over it hides them while closed. While
   the top flap rides open — the photo slice, lace and crest — this
   velvet fold, hidden underneath until now, is revealed: straight
   sides sweeping down to a broad flat base (|\__/|), velvet to match
   the body, a champagne hairline riding the fold's catch-light, and a
   soft shadow for the risen contents to slip behind — the oval photo
   over the left diagonal, the card resting on the flat, the playlist
   card over the flat's right shoulder, exactly like the reference
   envelope's opened front. Geometry: sides 40%H, flat 30–70%W at
   68.5/69.4%H (champagne sliver / velvet). Painted after .env-pocket
   (which it fully covers once open) and before the pearl chain, so the
   pearls drape across the velvet below the fold. */
.env-front {
  position: absolute; inset: 0;
  z-index: 3;
  filter: drop-shadow(0 -4px 7px rgba(26, 8, 13, .38));
  opacity: 0;
  transform: translateY(2.5%);
  transition: opacity .8s ease .55s, transform .95s var(--ease-soft) .55s;
  pointer-events: none;
}
/* the fold's edge: a champagne sliver showing above the velvet along
   the V only — the velvet shape sits .9%H lower and shares its bottom */
.env-front::before {
  content: "";
  position: absolute; inset: 0;
  background: rgba(233, 214, 178, .4);
  clip-path: polygon(0 40%, 30% 68.5%, 70% 68.5%, 100% 40%, 100% 98.4%, 98.9% 100%, 1.1% 100%, 0 98.4%);
}
.env-front::after {
  content: "";
  position: absolute; inset: 0;
  background:
    linear-gradient(115deg, rgba(255, 255, 255, .07), rgba(255, 255, 255, 0) 34%, rgba(0, 0, 0, .16) 88%),
    linear-gradient(rgba(88, 28, 42, .42), rgba(64, 18, 30, .6)),
    url("assets/velvet.jpg") center / cover;
  clip-path: polygon(0 40.9%, 30% 69.4%, 70% 69.4%, 100% 40.9%, 100% 98.4%, 98.9% 100%, 1.1% 100%, 0 98.4%);
}
body.opening .env-front,
body.opened .env-front { opacity: 1; transform: none; }

/* ---------- printed-on-the-envelope stationery ----------
   TEMP: the gold frame + printed sprigs are disabled for the cleaner
   lace-and-burgundy look (the markup is commented out in index.html).
   Restore this block + the markup to bring them back.
.env-print {
  position: absolute; inset: 0;
  z-index: 3;
  clip-path: polygon(0 0, 50% 55%, 100% 0, 100% 100%, 0 100%);
  pointer-events: none;
}
.env-print-sprig {
  position: absolute;
  opacity: .5;
}
.env-print-l {
  width: 27%; left: -11%; bottom: -13%;
  transform: rotate(12deg);
  opacity: .42;
}
.env-print-r {
  width: 25%; right: -5%; bottom: -6%;
  transform: rotate(-14deg) scaleX(-1);
  opacity: .48;
}
.env-frame {
  position: absolute; inset: 5.5% 3.5% 4.5% 3.5%;
  border: 1px solid rgba(179, 144, 90, .5);
}
.env-frame::before {
  content: "";
  position: absolute; inset: 3px;
  border: 1px solid rgba(179, 144, 90, .25);
}
---------- end TEMP ---------- */

/* the ruffled lace and the silver V&S crest are baked into the photo —
   no separate layers needed for them any more */

/* ---------- a single pearl chain — resting on the envelope's front ----------
   One strand isolated out of the luxury sheet's double necklace, laid flat
   for this spot (the strand's tilt is rotated into the bitmap itself —
   assets/lux-chain-flat.webp — so CSS only positions it, never transforms
   it). It rests along the opened envelope's base under the card, like a
   necklace laid across the stationery in a flat-lay: both ends visible,
   nothing severed, nothing floating. It belongs to the opened scene — the
   closed envelope keeps its pristine lace face — so it fades in with the
   open and stirs as it settles (reduced motion: still). */
.env-chain-clip {
  position: absolute; inset: 0;
  overflow: hidden;
  z-index: 3;                  /* with the pocket + front fold, above the liner */
  pointer-events: none;
}
.env-chain {
  position: absolute;
  left: 33%; bottom: 4.5%;
  width: 30%;
  transform-origin: 50% 50%;
  filter: drop-shadow(1px 2px 4px rgba(30, 8, 14, .38));
  opacity: 0;
  transition: opacity .7s ease .55s;   /* arrives with the front fold */
}
body.opening .env-chain,
body.opened .env-chain { opacity: 1; }
body.card-up .env-chain {
  animation: chainSettle 5.5s ease-in-out .85s 1 both;
}
@keyframes chainSettle {
  0%, 100% { transform: rotate(0deg); }
  30%      { transform: rotate(-1.3deg); }
  62%      { transform: rotate(.7deg); }
  84%      { transform: rotate(-.3deg); }
}

/* ---------- addressing on the envelope front ----------
   TEMP: disabled for the cleaner look (markup commented out in
   index.html). Restore to print the script + date on the pocket again.
.env-address {
  position: absolute; left: 10%; right: 10%; top: 65%;
  z-index: 3;
  text-align: center;
  pointer-events: none;
}
.addr-script {
  display: block;
  font-size: 7.4cqw;
  line-height: 1.15;
  color: var(--champagne);
  text-shadow: 0 1px 8px rgba(0, 0, 0, .45);
}
.addr-date {
  display: block;
  margin-top: 2.6cqw;
  font-family: var(--caps);
  font-weight: 400;
  font-size: 3.3cqw;
  letter-spacing: .42em;
  text-indent: .42em;
  color: rgba(242, 230, 210, .85);
}
---------- end TEMP ---------- */

/* ---------- flap — the photo's upper slice, swings back open in 3D ----------
   Its front face is the photo above the lace line (real tulle + silver
   V&S crest ride with it); its back face is an ivory paper lining. The
   clip follows the ruffle's scalloped contour and reaches ~1.2% past it,
   overlapping the pocket's cut so the closed state has no seam. */
.env-flap {
  position: absolute; inset: 0;
  z-index: 4;
  transform-origin: top center;
  transform-style: preserve-3d;
  transition:
    transform 1.05s cubic-bezier(.3, 1.22, .38, 1) .35s,  /* crack first, then swing with overshoot */
    z-index 0s linear .85s;     /* swap behind the pocket at the edge-on moment */
  pointer-events: none;
}
.flap-skin, .flap-lining {
  position: absolute; inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.flap-skin {
  background: url("assets/envelope.webp") center / 100% 100% no-repeat;
  clip-path: polygon(0% 1.1%, 1.1% 0%, 98.9% 0%, 100% 1.1%, 100% 42.91%, 98.75% 42.91%, 97.50% 44.71%, 96.25% 45.07%, 95.00% 45.91%, 93.75% 47.11%, 92.50% 49.76%, 91.25% 53.24%, 90.00% 55.29%, 88.75% 55.65%, 87.50% 56.73%, 86.25% 56.73%, 85.00% 57.57%, 83.75% 64.18%, 82.50% 65.02%, 81.25% 65.02%, 80.00% 65.86%, 78.75% 66.82%, 77.50% 70.91%, 76.25% 73.68%, 75.00% 74.64%, 73.75% 76.20%, 72.50% 76.32%, 71.25% 76.32%, 70.00% 78.60%, 68.75% 80.53%, 66.88% 84.01%, 65.62% 84.01%, 64.38% 84.61%, 63.12% 86.66%, 61.88% 90.02%, 59.38% 92.07%, 58.12% 93.15%, 56.88% 93.87%, 55.62% 95.43%, 54.38% 95.91%, 53.12% 96.27%, 51.88% 96.27%, 50.62% 96.27%, 49.38% 96.27%, 48.12% 96.27%, 46.88% 96.27%, 45.62% 96.15%, 44.38% 95.19%, 43.12% 93.75%, 41.25% 89.66%, 40.00% 89.66%, 38.75% 89.66%, 37.50% 88.70%, 36.25% 88.22%, 34.38% 86.90%, 33.12% 84.25%, 31.88% 78.36%, 30.62% 78.36%, 29.38% 78.36%, 26.25% 75.96%, 25.00% 72.71%, 23.75% 72.47%, 22.50% 67.31%, 21.25% 67.07%, 20.00% 65.62%, 17.50% 64.42%, 16.25% 62.74%, 14.38% 56.61%, 12.50% 56.61%, 11.25% 55.29%, 8.75% 53.60%, 7.50% 50.48%, 6.25% 44.83%, 5.00% 44.59%, 3.75% 44.11%, 2.50% 43.63%, 1.25% 41.70%, 0.62% 41.58%, 0% 41.58%);
}
/* the lining lives flipped 180° inside the flap, so its triangle must be
   mirrored too — otherwise it shows upside-down (wide edge up) when open */
.flap-lining {
  transform: rotateX(180deg);
  clip-path: polygon(0 100%, 100% 100%, 50% 3%);
  /* plain ivory paper liner — the striped interior lives on .env-inner,
     where the demo shows it (inside the open mouth) */
  background:
    linear-gradient(rgba(244, 238, 226, .55), rgba(233, 224, 207, .7)),
    url("assets/paper.jpg") center / cover;
  /* past ~90° the lining's full-width plane pokes out either side of the
     narrower raised flap, reading as pale gaps between flap and envelope —
     so once the swing is underway it bows out and the raised flap
     (.env-flap-raised) alone plays the opened flap */
  opacity: 1;
  transition: opacity .45s ease .75s;
}
body.opening .flap-lining { opacity: 0; }
/* negative angle = the flap falls OPEN AWAY from the viewer, receding
   behind the envelope (a small ivory plane leaning back, like the demo) */
body.opening .env-flap { transform: rotateX(-110deg); z-index: 1; }

/* perspective wrapper: use the button as the 3D stage */
.envelope { perspective: 900px; }

/* ---------- tap hint (plain letter-spaced caps, like the demo) ---------- */
.tap-hint {
  position: absolute;                 /* hangs below the envelope — keeps the
                                        envelope + ring exactly centred */
  top: calc(100% + 26px);
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  font-family: var(--caps);
  font-weight: 400;
  font-size: 12px;
  letter-spacing: .42em;
  text-indent: .42em;
  text-transform: uppercase;
  color: #6b6259;
  animation: hintPulse 2.6s ease-in-out infinite;
  transition: opacity .45s ease;
  z-index: 2;
}
@keyframes hintPulse {
  0%, 100% { opacity: .45; }
  50%      { opacity: 1; }
}
body.opening .tap-hint,
body.opened .tap-hint { opacity: 0; animation: none; }

/* ============================================================
   FLORA around the envelope
   ============================================================ */
.flora {
  position: absolute; inset: -7% -5%;
  pointer-events: none;
}
.flora-back  { z-index: 1; }
.flora-front { z-index: 5; }

/* The botanicals whisper: pulled ~25% smaller and tucked closer than
   before, so a few blooms and leaves peek around the envelope rather than
   announcing a full arrangement. */
.flw, .rose, .sprig, .bud, .flora-img {
  position: absolute;
  transform-origin: 50% 96%;
  filter: saturate(.82) drop-shadow(2px 4px 5px rgba(76, 60, 40, .15));
}
.flw-a { width: 22%; right: -1%; top: -11%; transform: rotate(4deg);  animation: swayA 6.5s ease-in-out infinite alternate; }
.flw-b { width: 17%; right: 13%; top:  -4%; transform: rotate(-7deg); animation: swayB 7.5s ease-in-out infinite alternate; }
.rose  { width: 25%; left: -3%;  bottom: -8%;                         animation: swayB 7s   ease-in-out infinite alternate; }
.sprig { width: 22%; right: -2%; bottom: -3%;  transform: rotate(6deg); animation: swayA 6s ease-in-out infinite alternate; }
.bud   { width: 9%;  right: 21%; bottom: -6%;  transform: rotate(10deg); animation: swayB 8s ease-in-out infinite alternate; }

/* optional real-photo flowers — drop transparent PNGs in assets/ and the
   SVG art steps aside automatically (see main.js). Tucked at the envelope's
   sides like the demo's dried bouquet + greenery. */
.flower-left  {
  width: 26%; left: -12%; bottom: 13.5%;
  transform: rotate(-8deg);
  animation: swaySoftB 9.5s ease-in-out infinite alternate;
}
.flower-right {
  width: 33%; right: -11.5%; bottom: 12%;
  transform: rotate(-14deg);
  animation: swaySoftA 9s ease-in-out infinite alternate;
}

/* the photo botanicals barely breathe: a slow ~3° drift, biased leftward,
   so the scene settles instead of swinging */
@keyframes swaySoftA { from { rotate: -1deg;  } to { rotate: 1deg;   } }
@keyframes swaySoftB { from { rotate: 1.2deg; } to { rotate: -1.8deg; } }
.flora-img { filter: saturate(.78) drop-shadow(2px 4px 5px rgba(76, 60, 40, .15)); }

@keyframes swayA { from { rotate: -2.5deg; } to { rotate: 2.5deg; } }
@keyframes swayB { from { rotate: 2deg;   } to { rotate: -2.5deg; } }

/* ============================================================
   REVEAL (after opening)
   ============================================================ */
.stage-reveal {
  position: absolute; inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;          /* the cue rests below the lace circle, near the page edge */
  gap: clamp(12px, 2vh, 22px);
  padding: clamp(14px, 3vh, 36px) 18px clamp(20px, 4vh, 48px);
  padding: clamp(14px, 3dvh, 36px) 18px clamp(20px, 4dvh, 48px);
  visibility: hidden;                 /* laid out (measurable) but unseen */
  pointer-events: none;
}
body.revealed .stage-reveal { visibility: visible; }
/* the overlay hosts only the cue — everything else must click through
   to the scene beneath (the playlist card's play disc lives under it) */
body.opened .stage-reveal { pointer-events: none; }
body.opened .stage-reveal .scroll-cue { pointer-events: auto; }

/* entrance choreography — the names greet on load; the scroll cue waits
   for the envelope to open (card + photos rise via body.card-up) */
.reveal-kicker, .reveal-names, .scroll-cue {
  opacity: 0;
}
.reveal-kicker {
  animation: rise .9s var(--ease-out) .35s forwards;
}
.reveal-names {
  animation: namesIn 1.15s var(--ease-out) .55s forwards;
}
body.revealed .scroll-cue { animation: rise .9s var(--ease-out) .4s forwards; }

@keyframes rise {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes namesIn {
  from { opacity: 0; transform: translateY(12px) scale(.985); filter: blur(7px); }
  to   { opacity: 1; transform: translateY(0)   scale(1);     filter: blur(0);  }
}

/* ---------- headings (greet above the envelope from the start) ----------
   A quarter-step quieter than before: the greeting leads the eye, then
   hands it to the invitation — it no longer competes with the card. */
.reveal-head {
  position: absolute; left: 0; right: 0; top: clamp(16px, 4vh, 52px);
  text-align: center;                /* centred on the envelope's vertical line */
  z-index: 3;
}
.reveal-kicker {
  font-family: var(--caps);
  font-weight: 300;
  font-size: clamp(10px, 1.15vw, 13px);
  letter-spacing: .56em;
  text-indent: .56em;
  text-transform: uppercase;
  color: #6b6259;
  text-shadow: 0 1px 8px rgba(250, 248, 245, .8);
}
.reveal-names {
  margin-top: clamp(8px, 1.6vh, 18px);
  font-size: clamp(36px, 5.6vw, 72px);
  line-height: 1.04;
  color: var(--rose-deep);
  text-shadow: 0 1px 0 rgba(255, 255, 255, .5);
}
.reveal-names .amp {
  font-size: .8em;
  opacity: .9;
  padding: 0 .06em;
}

/* ---------- the tucked oval: the single couple photo ----------
   One photo, framed ornately so it belongs to the stationery. Sized to
   fill the card's left flank without competing. Shadows follow the
   shared upper-left light. */
.env-photos .photo {
  position: absolute;
  margin: 0;
  box-shadow:
    2px 5px 10px rgba(76, 60, 40, .16),
    6px 13px 26px rgba(76, 60, 40, .12);
}
/* the tucked oval: sized so its foot sinks behind the front fold's left
   diagonal + flat base (photo bottom ≈ 72%H vs the fold edge's 41–69%H
   across its width) — tucked like the card and the playlist card, never
   floating */
.photo-oval {
  left: 1.6%; top: 33%;
  width: 29%;
  transform: rotate(-5deg);
}

/* ---------- photos ----------
   No width here on purpose: the tucked pair is sized proportionally by
   .photo-oval (20% of the contents box) / .env-photo-side (19% of the
   envelope), and the noscript stack sets its own width — a width below
   would be later in the cascade than those rules and would clobber both. */
.photo {
  aspect-ratio: 4 / 5;
  background:
    linear-gradient(rgba(252, 247, 238, .55), rgba(246, 238, 226, .55)),
    var(--paper);
  padding: 4.5%;                       /* thicker cream border, like a print */
  box-shadow:
    2px 4px 9px rgba(76, 60, 40, .13),
    5px 11px 22px rgba(76, 60, 40, .10);
  position: relative;
  z-index: 1;
}
.photo img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  filter: saturate(.92) contrast(.97);
}
/* the oval frame photo: no paper mat — the ornate frame supplies the edge.
   The img fills the frame's transparent window exactly. The crop is biased
   upward so faces at the top of a portrait aren't cut. (The selector carries
   .env-photos so it out-specifies .env-photos .photo's box-shadow — that
   shadow on the transparent rectangle painted a ghost square behind the
   oval frame.) */
.env-photos .photo-oval { background: none; padding: 0; box-shadow: none; }
.photo-oval > img:not(.oval-frame) {
  position: absolute;
  left: 14%; top: 11.2%;
  width: 72%; height: 77.6%;
  border-radius: 50%;
  object-position: 50% 22%;
}
.oval-frame {
  position: absolute;
  inset: -2.5%;
  width: 105%; height: 105%;
  pointer-events: none;
}
/* graceful placeholder until real photos are dropped in */
.photo.empty img { visibility: hidden; }   /* keep broken-image alt text out of sight */
.ph-label {
  display: none;
  position: absolute; inset: 4.5%;
  place-items: center;
  font-family: var(--caps);
  font-weight: 300;
  font-size: 11px;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--ink-soft);
  text-align: center;
  padding: 0 8px;
  background:
    linear-gradient(rgba(244, 237, 222, .65), rgba(244, 237, 222, .65)),
    repeating-linear-gradient(45deg, rgba(179,144,90,.14) 0 10px, transparent 10px 20px);
}
.photo.empty .ph-label { display: grid; }
.photo-oval .ph-label { inset: 15%; border-radius: 50%; }
/* the label arrives on its own, after its photo settles */
body.opened .photo.empty .ph-label {
  animation: phIn .9s var(--ease-out) .5s both;
}
@keyframes phIn {
  from { opacity: 0; letter-spacing: .5em; transform: translateY(7px); }
  to   { opacity: 1; letter-spacing: .22em; transform: none; }
}

/* ---------- card inner (shared by env-card + hero-card) ----------
   ivory paper card, slipped inside the burgundy velvet envelope.
   Letterpress treatment: warm paper, a faintly irregular die-cut arch,
   embossed double frame, and one soft shadow cast down-right (the shared
   upper-left light). */
.card-inner {
  position: absolute; inset: 0;
  /* warm cotton paper: a soft cream wash, a whisper of grain, then the
     fiber texture — thick stock you can feel */
  background:
    linear-gradient(rgba(252, 247, 234, .86), rgba(247, 238, 218, .88)),
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/><feColorMatrix type='saturate' values='0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='.06'/></svg>"),
    url("assets/paper.jpg") center / cover;
  border-radius: 46% 44% 8px 9px / 30% 28% 8px 9px;   /* arch-top card, gently imperfect */
  box-shadow:
    inset 0 0 0 1px rgba(76, 65, 52, .06),
    inset 0 1px 0 rgba(255, 255, 255, .65),
    1px 3px 7px rgba(76, 60, 40, .15),
    3px 8px 18px rgba(76, 60, 40, .19),
    8px 20px 38px rgba(76, 60, 40, .13);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5.2cqw;
  text-align: center;
  padding: 8.4cqw 7cqw;
}
/* double frame with the same arch — the light hairline under each gold
   line presses it into the paper, like an embossed border */
.card-inner::before {
  content: "";
  position: absolute; inset: 4.2%;
  border: 1px solid rgba(179, 144, 90, .55);
  border-radius: 46% 44% 7px 8px / 30% 28% 7px 8px;
  box-shadow: 0 1px 0 rgba(255, 253, 245, .7);
  pointer-events: none;
}
.card-inner::after {
  content: "";
  position: absolute; inset: 6.8%;
  border: 1px solid rgba(179, 144, 90, .22);
  border-radius: 46% 44% 6px 7px / 30% 28% 6px 7px;
  box-shadow: 0 1px 0 rgba(255, 253, 245, .55);
  pointer-events: none;
}
/* the botanical flourish — fine line art flanking a gold diamond,
   printed at the top of the arch */
.c-mono { width: 42cqw; height: auto; flex: none; }
.c-ornament { width: 38cqw; height: auto; opacity: .9; }
.c-to {
  font-size: 6.4cqw;
  line-height: 1;
  color: var(--ink-soft);
  margin-top: -2cqw;
}
.c-to[data-empty="1"] { display: none; }
/* the demo's card treatment: letter-spaced serif caps over a big script date */
.c-kicker {
  font-family: var(--serif);
  font-size: 4.7cqw;
  font-weight: 600;
  letter-spacing: .32em;
  text-indent: .32em;
  text-transform: uppercase;
  color: var(--rose-deep);
}
.c-date {
  font-size: 13.5cqw;
  line-height: 1;
  color: var(--rose-deep);
}
.c-rule {
  width: 34cqw; height: 1px;
  background: linear-gradient(90deg, transparent, var(--gold), transparent);
  position: relative;
}
.c-rule::after {
  content: "";
  position: absolute; left: 50%; top: 50%;
  width: 5px; height: 5px;
  transform: translate(-50%, -50%) rotate(45deg);
  background: var(--gold);
}
.c-hashtag {
  font-family: var(--caps);
  font-weight: 400;
  font-size: 4.2cqw;
  letter-spacing: .3em;
  text-indent: .3em;
  color: var(--gold);
  margin-top: -1cqw;
}

/* ---------- follow note + scroll cue ----------
   A fine gold hairline with a drifting diamond — quieter and more
   stationery-like than a plain grey tick. */
.scroll-cue {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 9px;
  text-decoration: none;
}
.cue-line {
  width: 1px; height: 44px;
  background: linear-gradient(180deg, rgba(179, 144, 90, 0), rgba(179, 144, 90, .65) 30%, rgba(179, 144, 90, .65) 78%, rgba(179, 144, 90, 0));
  position: relative;
  overflow: hidden;
}
.cue-dot {
  position: absolute; left: -1.75px; top: -9px;
  width: 4.5px; height: 4.5px;
  transform: rotate(45deg);
  background: var(--gold-bright);
  animation: cueDrop 2.2s ease-in-out infinite;
}
@keyframes cueDrop {
  0%   { transform: translateY(0);    opacity: 0; }
  18%  { opacity: 1; }
  78%  { transform: translateY(40px); opacity: 1; }
  100% { transform: translateY(44px); opacity: 0; }
}
.cue-word {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--caps);
  font-weight: 400;
  font-size: 9.5px;
  letter-spacing: .52em;
  text-indent: .52em;
  text-transform: uppercase;
  color: #6b5f50;
  text-shadow:
    0 0 8px rgba(251, 246, 234, .95),
    0 0 12px rgba(251, 246, 234, .85);
}
/* two faint dashes flanking the word — a whisper of gold, nothing more */
.cue-word::before,
.cue-word::after {
  content: "";
  width: 16px; height: 1px;
  background: linear-gradient(90deg, rgba(179, 144, 90, 0), rgba(179, 144, 90, .55));
}
.cue-word::after { transform: scaleX(-1); }

/* ============================================================
   THE SONG — a small floating ivory button, off the stationery
   ============================================================ */
.song-fab {
  position: fixed;
  right: clamp(16px, 2.6vw, 30px);
  bottom: clamp(16px, 3vh, 30px);
  width: 48px; height: 48px;
  border-radius: 50%;
  border: 1px solid rgba(179, 144, 90, .5);
  background:
    linear-gradient(rgba(252, 247, 238, .85), rgba(246, 238, 226, .85)),
    url("assets/paper.jpg") center / cover;
  color: var(--gold);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .7),
    2px 5px 12px rgba(76, 60, 40, .18);
  display: grid;
  place-items: center;
  cursor: pointer;
  z-index: 40;
  opacity: 0;
  transform: translateY(10px);
  pointer-events: none;
  transition: opacity .7s var(--ease-out) .6s, transform .7s var(--ease-out) .6s,
              box-shadow .25s ease;
}
.song-fab svg { width: 21px; height: 21px; display: block; }
body.revealed .song-fab {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
.song-fab:hover {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .7),
    3px 8px 18px rgba(76, 60, 40, .24);
}
/* while the song plays, the button answers quietly */
.song-fab.playing {
  color: var(--rose-deep);
  border-color: rgba(179, 144, 90, .85);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .7),
    0 0 0 3px rgba(233, 200, 138, .35),
    3px 8px 18px rgba(76, 60, 40, .24);
}

/* ============================================================
   PANELS (the invite sections + countdown + contact)
   ============================================================ */
.panel {
  max-width: 720px;
  margin: 0 auto;
  padding: clamp(72px, 13vh, 130px) 26px;
  text-align: center;
  position: relative;
  z-index: 1;
}
.panel-alt {
  max-width: none;
  background: linear-gradient(180deg, rgba(222, 216, 210, 0), rgba(222, 216, 210, .85) 18%, rgba(222, 216, 210, .85) 82%, rgba(222, 216, 210, 0));
  padding-left: 26px; padding-right: 26px;
}

/* scroll-in reveal */
.panel > * { opacity: 0; transform: translateY(22px); transition: opacity .8s var(--ease-out), transform .8s var(--ease-out); }
.panel.in > * { opacity: 1; transform: none; }
.panel.in > *:nth-child(2) { transition-delay: .08s; }
.panel.in > *:nth-child(3) { transition-delay: .16s; }
.panel.in > *:nth-child(4) { transition-delay: .24s; }
.panel.in > *:nth-child(5) { transition-delay: .32s; }
.panel.in > *:nth-child(6) { transition-delay: .4s; }

.ornament { width: clamp(130px, 17vw, 175px); margin: 0 auto 18px; }
.ornament img { width: 100%; height: auto; display: block; }

.panel-kicker {
  font-family: var(--caps);
  font-weight: 300;
  font-size: 12px;
  letter-spacing: .5em;
  text-indent: .5em;
  text-transform: uppercase;
  color: var(--gold);
}
.panel-title {
  margin-top: 14px;
  font-size: clamp(30px, 4.6vw, 44px);
  font-weight: 500;
  letter-spacing: .02em;
  color: var(--ink);
}
.panel-sub {
  margin-top: 12px;
  font-size: clamp(17px, 2.2vw, 20px);
  font-style: italic;
  color: var(--ink-soft);
}

/* ---------- the REPLACE marker — every unfilled slot wears this ---------- */
.rep {
  font-family: var(--caps);
  font-size: .74em;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: #ad6a4d;
  border-bottom: 1px dashed rgba(173, 106, 77, .55);
  padding-bottom: 1px;
}

/* ---------- 1 · celebrations — one golden thread through the days ---------- */
.journey {
  position: relative;
  margin: 56px auto 0;
  max-width: 800px;
  text-align: center;
}
/* the stops run their own entrances — the journey skips the panel cascade */
.panel > .journey { opacity: 1; transform: none; }

/* the golden thread — main.js stitches it through the beads; the dash
   length is driven by scroll, so the line draws as you go */
.j-thread {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  overflow: visible;
  pointer-events: none;
  z-index: 0;
}
.j-thread-line {
  fill: none;
  stroke: var(--gold);
  stroke-width: 1.4;
  stroke-linecap: round;
  opacity: .55;
}

.j-node {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 1fr 76px 1fr;
  align-items: center;
  padding: 24px 0;
}

/* the pearls the thread runs through — pop when their stop arrives */
.j-bead {
  grid-column: 2; grid-row: 1;
  justify-self: center;
  display: block;
  width: 15px; height: 15px;
  border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, #fff, #f7f0e8 45%, #d8c7b8 74%, #fff);
  border: 1px solid #b9a393;
  box-shadow: 0 2px 5px rgba(76, 60, 40, .28);
  opacity: 0;
  transform: scale(0);
}
.j-node.in .j-bead { animation: jBead .7s var(--ease-soft) .1s forwards; }
@keyframes jBead {
  0%   { opacity: 0; transform: scale(0); }
  62%  { opacity: 1; transform: scale(1.28); }
  100% { opacity: 1; transform: scale(1);
         box-shadow: 0 2px 5px rgba(76, 60, 40, .28), 0 0 0 5px rgba(211, 172, 110, .14); }
}

/* the stop cards — near-opaque, so the thread passes behind the
   stationery instead of ghosting through the printed names */
.j-card {
  position: relative;
  max-width: 350px;
  padding: 32px 26px 28px;
  background: rgba(255, 253, 246, .88);
  border: 1px solid rgba(179, 144, 90, .4);
  opacity: 0;
  transition: opacity .8s var(--ease-out) .16s, transform .8s var(--ease-out) .16s;
}
.j-card::before {
  content: "";
  position: absolute; inset: 6px;
  border: 1px solid rgba(179, 144, 90, .22);
  pointer-events: none;
}
.j-left  .j-card { grid-column: 1; grid-row: 1; justify-self: end;   transform: translate(-26px, 10px); }
.j-right .j-card { grid-column: 3; grid-row: 1; justify-self: start; transform: translate(26px, 10px); }
.j-node.in .j-card { opacity: 1; transform: none; }

/* each icon draws itself once its stop arrives (pathLength=1 strokes) */
.j-icon {
  display: block;
  width: 42px; height: 42px;
  margin: 0 auto;
  color: var(--gold);
}
.j-icon path, .j-icon circle { stroke-dasharray: 1; stroke-dashoffset: 1; }
.j-icon .j-fill { opacity: 0; transition: opacity .6s ease 1.05s; }
.j-node.in .j-icon path,
.j-node.in .j-icon circle { stroke-dashoffset: 0; transition: stroke-dashoffset 1.1s var(--ease-soft) .3s; }
.j-node.in .j-icon .j-fill { opacity: 1; }

.j-when {
  margin-top: 14px;
  font-family: var(--caps);
  font-weight: 300;
  font-size: 11px;
  letter-spacing: .3em;
  text-indent: .3em;
  text-transform: uppercase;
  color: var(--gold);
}
.j-what { margin-top: 8px; font-size: 36px; line-height: 1.1; color: var(--rose-deep); }
.j-note { margin-top: 12px; font-size: 15.5px; font-style: italic; color: var(--ink-soft); }

/* the grand stop — the wedding day itself */
.j-grand { display: block; padding: 34px 0 30px; }
.j-grand .j-bead {
  width: 19px; height: 19px;
  margin: 0 auto;
  border-color: var(--gold);
}
.j-card-grand {
  max-width: 680px;
  margin: 22px auto 0;
  padding: 36px 30px 30px;
  transform: translateY(16px);
}
.j-events {
  margin-top: 24px;
  display: flex;
  justify-content: center;
  gap: 22px;
  flex-wrap: wrap;
}
.j-events .event-card { flex: 1 1 240px; max-width: 300px; padding: 28px 22px 24px; }

/* how the day flows — the hour rhythm, a hairline through gold diamonds */
.j-rhythm {
  position: relative;
  margin: 28px auto 0;
  max-width: 560px;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 6px;
}
.j-rhythm::before {
  content: "";
  position: absolute;
  top: 29px; left: 12%; right: 12%;
  height: 1px;
  background: linear-gradient(90deg, rgba(179, 144, 90, 0), rgba(179, 144, 90, .5) 12%, rgba(179, 144, 90, .5) 88%, rgba(179, 144, 90, 0));
}
.j-hour {
  display: grid;
  justify-items: center;
  align-content: start;
  opacity: 0;
  transform: translateY(10px);
  transition: opacity .7s var(--ease-out), transform .7s var(--ease-out);
}
.j-hour-time {
  height: 17px;
  font-family: var(--caps);
  font-weight: 300;
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-soft);
  white-space: nowrap;
}
.j-hour-dot {
  width: 7px; height: 7px;
  margin: 9px 0 8px;
  transform: rotate(45deg);
  background: var(--paper);
  border: 1px solid var(--gold);
}
.j-hour-what { font-size: 22px; line-height: 1.2; color: var(--ink); }
.j-hour-sub { margin-top: 3px; font-size: 13.5px; font-style: italic; color: var(--ink-soft); }
.j-node.in .j-hour { opacity: 1; transform: none; }
.j-node.in .j-hour:nth-child(1) { transition-delay: .45s; }
.j-node.in .j-hour:nth-child(2) { transition-delay: .6s; }
.j-node.in .j-hour:nth-child(3) { transition-delay: .75s; }
.j-node.in .j-hour:nth-child(4) { transition-delay: .9s; }

/* the pastels, told per event — a little silk tag pinned to each stop.
   sage on the mehendi leaf, butter in the haldi bowl, lilac for the
   sangeet night, blush at the sundowner varmala, powder by lamplight */
.j-wear {
  margin-top: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity .7s var(--ease-out) .75s, transform .7s var(--ease-out) .75s;
}
.j-node.in .j-wear { opacity: 1; transform: none; }
.j-wear span {
  font-family: var(--caps);
  font-weight: 300;
  font-size: 10px;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.j-wear i {
  flex: none;
  width: 15px; height: 22px;
  border-radius: 1.5px;
  clip-path: polygon(0 0, 100% 0, 100% 100%, 50% 82%, 0 100%);
  filter: drop-shadow(0 3px 4px rgba(76, 65, 52, .25));
  box-shadow: inset 0 4px 6px rgba(255, 255, 255, .45), inset 0 -5px 7px rgba(76, 65, 52, .14);
  transform-origin: 50% 0;
  transform: rotate(var(--tilt, -3deg));
  opacity: 0;
  transition: opacity .5s var(--ease-out) 1s, transform .6s var(--ease-soft) 1s;
}
.j-node.in .j-wear i {
  opacity: 1;
  animation: jSway 4.6s ease-in-out 1.7s infinite alternate;
}
.j-left .j-wear i, .j-events .event-card:first-child .j-wear i { --tilt: -3deg; }
.j-right .j-wear i, .j-events .event-card:last-child .j-wear i { --tilt: 3deg; }
@keyframes jSway {
  from { transform: rotate(calc(var(--tilt, 0deg) - 1.6deg)) translateY(0); }
  to   { transform: rotate(calc(var(--tilt, 0deg) + 1.6deg)) translateY(1.5px); }
}

/* the postscript — the general invitation, once the thread rests */
.journey-close {
  margin-top: 4px;
  opacity: 0;
  transform: translateY(10px);
  transition: opacity .8s var(--ease-out) .2s, transform .8s var(--ease-out) .2s;
}
.journey-close.in { opacity: 1; transform: none; }
.jc-line {
  font-size: clamp(24px, 3vw, 30px);
  line-height: 1.4;
  color: var(--ink);
}
.jc-sub {
  margin-top: 10px;
  font-family: var(--caps);
  font-weight: 300;
  font-size: 10.5px;
  letter-spacing: .26em;
  text-indent: .26em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* event cards — the two headline events, riding inside the big-day stop */
.event-card {
  flex: 1 1 250px;
  max-width: 330px;
  padding: 36px 26px 30px;
  background: rgba(255, 253, 246, .65);
  border: 1px solid rgba(179, 144, 90, .4);
  position: relative;
}
.event-card::before {
  content: "";
  position: absolute; inset: 6px;
  border: 1px solid rgba(179, 144, 90, .22);
  pointer-events: none;
}
.ev-icon { display: block; width: 44px; height: 44px; margin: 0 auto; color: var(--gold); }
.ev-name {
  margin-top: 14px;
  font-size: 34px;
  line-height: 1.1;
  color: var(--rose-deep);
}
.ev-time {
  margin-top: 10px;
  font-family: var(--caps);
  font-weight: 400;
  font-size: 12.5px;
  letter-spacing: .34em;
  text-indent: .34em;
  text-transform: uppercase;
  color: var(--ink);
}
.ev-note {
  margin-top: 12px;
  font-size: 15.5px;
  font-style: italic;
  color: var(--ink-soft);
}

/* ---------- 2 · venue & stay — the map + accommodation ---------- */
.map-frame {
  margin: 38px auto 0;
  max-width: 640px;
  padding: 10px;
  background: var(--paper);
  box-shadow: 0 14px 30px rgba(76, 65, 52, .16);
}
.map-frame iframe {
  display: block;
  width: 100%;
  height: clamp(240px, 40vh, 360px);
  border: 0;
  filter: sepia(.18) saturate(.88);   /* warm the map toward the palette */
}
.map-cap {
  margin-top: 14px;
  font-size: 15px;
  font-style: italic;
  color: var(--ink-soft);
}
.stay-grid {
  margin: 36px auto 0;
  display: flex;
  justify-content: center;
  gap: 22px;
  flex-wrap: wrap;
}
.stay-card {
  flex: 1 1 240px;
  max-width: 300px;
  padding: 26px 22px 24px;
  background: rgba(255, 253, 246, .65);
  border: 1px solid rgba(179, 144, 90, .4);
  position: relative;
}
.stay-card::before {
  content: "";
  position: absolute; inset: 6px;
  border: 1px solid rgba(179, 144, 90, .22);
  pointer-events: none;
}
.stay-card h3 { font-size: 28px; font-weight: 400; color: var(--ink); }
.stay-card p { margin-top: 8px; font-size: 15px; color: var(--ink-soft); }
.stay-note {
  margin-top: 24px;
  font-size: 15.5px;
  font-style: italic;
  color: var(--ink-soft);
}

/* ---------- 3 · kota — see & get ready ---------- */
.kota-grid {
  margin: 38px auto 0;
  display: flex;
  justify-content: center;
  gap: 26px;
  flex-wrap: wrap;
  max-width: 760px;
}
.kota-card {
  flex: 1 1 300px;
  max-width: 360px;
  padding: 30px 28px 26px;
  background: rgba(255, 253, 246, .65);
  border: 1px solid rgba(179, 144, 90, .4);
  position: relative;
}
.kota-card::before {
  content: "";
  position: absolute; inset: 6px;
  border: 1px solid rgba(179, 144, 90, .22);
  pointer-events: none;
}
.kota-card h3 { font-size: 30px; color: var(--rose-deep); }
.kota-list {
  list-style: none;
  margin-top: 10px;
  text-align: left;
}
.kota-list li {
  position: relative;
  padding: 9px 0 9px 20px;
  font-size: 15.5px;
  line-height: 1.5;
  color: var(--ink-soft);
  border-bottom: 1px solid rgba(179, 144, 90, .16);
}
.kota-list li:last-child { border-bottom: 0; }
.kota-list li::before {
  content: "";
  position: absolute;
  left: 2px; top: 1.05em;
  width: 5px; height: 5px;
  transform: rotate(45deg);
  background: var(--gold);
}
.kota-list strong { font-weight: 500; color: var(--ink); }
.kota-tip {
  margin-top: 14px;
  font-size: 14px;
  font-style: italic;
  color: var(--ink-soft);
}

/* ---------- 4 · gratitude — the note ---------- */
.grat {
  max-width: 640px;
  margin: 14px auto 0;
  font-size: clamp(24px, 3.2vw, 30px);
  line-height: 1.65;
  color: var(--ink);
}
.grat-sig { margin-top: 24px; font-size: 38px; line-height: 1.2; color: var(--rose-deep); }
.grat-fam {
  margin-top: 8px;
  font-family: var(--caps);
  font-weight: 300;
  font-size: 11px;
  letter-spacing: .32em;
  text-indent: .32em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* ---------- 5 · faqs — quiet accordions ---------- */
.faq-list {
  margin: 34px auto 0;
  max-width: 620px;
  text-align: left;
}
.faq-list details { border-bottom: 1px solid rgba(179, 144, 90, .28); }
.faq-list details:first-child { border-top: 1px solid rgba(179, 144, 90, .28); }
.faq-list summary {
  list-style: none;
  cursor: pointer;
  position: relative;
  padding: 17px 34px 17px 2px;
  font-size: 19.5px;
  color: var(--ink);
  transition: color .25s ease;
}
.faq-list summary::-webkit-details-marker { display: none; }
.faq-list summary:hover { color: var(--rose-deep); }
.faq-list summary::after {
  content: "+";
  position: absolute;
  right: 6px; top: 50%;
  font-family: var(--caps);
  font-weight: 300;
  font-size: 21px;
  line-height: 1;
  color: var(--gold);
  transform: translateY(-50%);
  transition: transform .35s var(--ease-out);
}
.faq-list details[open] summary::after { transform: translateY(-50%) rotate(45deg); }
.faq-list details p {
  padding: 0 30px 18px 2px;
  font-size: 16px;
  line-height: 1.65;
  color: var(--ink-soft);
}

/* ---------- countdown ---------- */
.timer {
  margin: 40px auto 0;
  display: flex;
  justify-content: center;
  gap: clamp(10px, 2.6vw, 22px);
}
.tile {
  min-width: clamp(66px, 12vw, 96px);
  padding: clamp(14px, 2vw, 22px) 8px;
  background: rgba(255, 253, 246, .65);
  border: 1px solid rgba(179, 144, 90, .4);
  box-shadow: 0 8px 20px rgba(76, 65, 52, .07);
}
.num {
  display: block;
  font-size: clamp(30px, 5.4vw, 52px);
  font-weight: 500;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.lbl {
  display: block;
  margin-top: 9px;
  font-family: var(--caps);
  font-weight: 300;
  font-size: clamp(9px, 1.1vw, 11px);
  letter-spacing: .3em;
  text-indent: .3em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.until {
  margin-top: 34px;
  font-size: clamp(30px, 4.4vw, 42px);
  color: var(--rose-deep);
}
.until-date {
  margin-top: 8px;
  font-family: var(--caps);
  font-weight: 300;
  font-size: 12.5px;
  letter-spacing: .34em;
  text-indent: .34em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* ---------- contact ---------- */
.contact-grid {
  margin: 38px auto 0;
  display: flex;
  justify-content: center;
  gap: 22px;
  flex-wrap: wrap;
}
.contact-card {
  flex: 1 1 240px;
  max-width: 300px;
  padding: 30px 22px 26px;
  background: rgba(255, 253, 246, .65);
  border: 1px solid rgba(179, 144, 90, .4);
  position: relative;
}
.contact-card::before {
  content: "";
  position: absolute; inset: 6px;
  border: 1px solid rgba(179, 144, 90, .22);
  pointer-events: none;
}
.contact-card h3 {
  font-size: 34px;
  font-weight: 400;
  color: var(--ink);
}
.contact-card a {
  display: block;
  margin-top: 10px;
  font-size: 16.5px;
  color: var(--ink-soft);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  width: fit-content;
  margin-left: auto; margin-right: auto;
  transition: color .25s ease, border-color .25s ease;
}
.contact-card a:hover { color: var(--ink); border-color: var(--gold); }

/* ---------- footer ---------- */
.foot {
  padding: 46px 20px 54px;
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
/* one quiet gold stitch where the banner used to sit */
.foot-orn {
  width: 8px; height: 8px;
  margin: 0 auto 4px;
  transform: rotate(45deg);
  border: 1px solid var(--gold);
  opacity: .8;
}
.foot-names { font-size: 30px; color: var(--ink); }
.foot-date {
  font-family: var(--caps);
  font-weight: 300;
  font-size: 11px;
  letter-spacing: .4em;
  text-indent: .4em;
  color: var(--ink-soft);
}
.foot-hash {
  margin-top: 4px;
  font-family: var(--caps);
  font-weight: 400;
  font-size: 12px;
  letter-spacing: .3em;
  text-indent: .3em;
  color: var(--gold);
}

/* ============================================================
   SHEET COMPONENTS — pieces of assets/wedding-components.svg,
   inlined in index.html (so the page's own fonts style them).
   Script text inherits this family; serif text carries its own
   font-family attribute and opts out.
   ============================================================ */
.wcomp-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

.wcomp {
  display: block;
  height: auto;
  font-family: var(--script);          /* the sheet's cursive texts inherit */
}

/* the sheet's drop-shadow filters, as CSS so they never clip */
.wcomp-paper { filter: drop-shadow(0 5px 9px rgba(44, 7, 21, .16)); }

.wcomp-garland {
  width: min(430px, 76vw);
  margin: 4px auto 2px;
  opacity: .95;
}
.wcomp-seal {
  filter: drop-shadow(0 6px 10px rgba(44, 7, 21, .22));
}

/* the gold sprig tucked into a card's top-right corner */
.wcomp-corner {
  position: absolute;
  top: 10px;
  right: 10px;
  width: clamp(44px, 6vw, 56px);
  opacity: .8;
  pointer-events: none;
}

/* the ornament slot now hosts svgs (wax seal) as well as img dividers */
.ornament svg { width: 100%; height: auto; display: block; }
.ornament-wide { width: min(430px, 76vw); }

/* ============================================================
   STATIONERY FLOURISHES — the wax seal, ink-bleed headings,
   ceremony stains, the site thread, the diya and deckle edges
   ============================================================ */

/* ---------- 1 · the wax seal, alive ----------
   Stamps itself onto the map mat's corner the first time the venue
   scrolls into view (#venue.in is added once, then unobserved — so it
   presses exactly once). It arrives high and large, presses down
   through a squidge, and its shadow tightens as it settles into the
   paper. One-time; reduced motion just shows it placed. */
.venue-seal {
  position: absolute;
  top: -30px; right: -18px;
  width: clamp(84px, 11vw, 110px);
  height: auto;
  transform: rotate(-8deg);
  opacity: 0;
  z-index: 2;
  pointer-events: none;
}
#venue.in .venue-seal { animation: sealStamp .8s cubic-bezier(.24, 1.3, .38, 1) .55s forwards; }
@keyframes sealStamp {
  0%   { opacity: 0; transform: rotate(4deg) scale(1.22);
         filter: drop-shadow(0 20px 24px rgba(44, 7, 21, 0)); }
  46%  { opacity: 1; }
  70%  { transform: rotate(-9.6deg) scale(.965);
         filter: drop-shadow(0 7px 9px rgba(44, 7, 21, .32)); }
  100% { opacity: 1; transform: rotate(-8deg) scale(1);
         filter: drop-shadow(0 6px 8px rgba(44, 7, 21, .24)); }
}

/* ---------- 2 · ink-bleed heading reveals ----------
   The stop names write themselves: a clip wipe sweeps left to right
   across the script while a soft blur resolves — ink soaking into
   paper, settling sharp. Runs once, with its stop's arrival. */
.j-what {
  position: relative;
  isolation: isolate;              /* keeps the stain behind the name */
  clip-path: inset(-34% 102% -34% -8%);
  filter: blur(3px);
  transition:
    clip-path .85s var(--ease-soft) .22s,
    filter .85s ease .22s;
}
.j-node.in .j-what {
  clip-path: inset(-34% -8% -34% -8%);
  filter: blur(0);
}

/* ---------- 4 · turmeric & henna stains ----------
   A quiet watercolor blot behind each stop's name — henna rust for
   the mehendi, turmeric for the haldi, a lilac wash for the sangeet,
   blush for the wedding day. Two offset radial washes in an irregular
   blob; blooms as the word is written. Low opacity — a stain, not a
   sticker. */
.j-what::before {
  content: "";
  position: absolute;
  left: 50%; top: 54%;
  width: 2.7em; height: 1.55em;
  transform: translate(-50%, -50%) scale(.55) rotate(-10deg);
  background:
    radial-gradient(58% 54% at 36% 44%, var(--stain), transparent 72%),
    radial-gradient(52% 62% at 64% 58%, var(--stain), transparent 76%);
  border-radius: 47% 53% 58% 42% / 60% 44% 56% 40%;
  opacity: 0;
  transition: opacity 1.1s ease .5s, transform 1.1s var(--ease-soft) .5s;
  pointer-events: none;
}
.j-node.in .j-what::before { opacity: 1; transform: translate(-50%, -50%) scale(1) rotate(-3deg); }
.j-stain-henna    .j-what { --stain: rgba(156, 74, 47, .30); }   /* henna rust  */
.j-stain-turmeric .j-what { --stain: rgba(217, 160, 47, .34); }  /* turmeric    */
.j-stain-lilac    .j-what { --stain: rgba(169, 139, 192, .32); } /* sangeet     */
.j-stain-blush    .j-what { --stain: rgba(201, 138, 146, .32); } /* the big day */

/* ---------- 6 · the diya ----------
   A small gold lamp standing in for the map's pin, perched on the
   venue spot. Settles in with its section; the flame breathes. */
.map-diya {
  position: absolute;
  left: 50%; top: 50%;
  width: clamp(38px, 5vw, 46px);
  transform: translate(-50%, -94%);
  opacity: 0;
  z-index: 2;
  pointer-events: none;
  filter: drop-shadow(0 3px 5px rgba(60, 30, 10, .35));
}
#venue.in .map-diya { animation: diyaSettle .7s var(--ease-soft) .9s forwards; }
@keyframes diyaSettle {
  from { opacity: 0; transform: translate(-50%, -112%) scale(1.14); }
  to   { opacity: 1; transform: translate(-50%, -94%) scale(1); }
}
.map-diya .diya-flame {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: diyaFlicker 2.7s ease-in-out infinite alternate;
}
@keyframes diyaFlicker {
  from { transform: scaleY(.96) scaleX(1.02) rotate(-.6deg); }
  to   { transform: scaleY(1.06) scaleX(.97) rotate(.7deg); }
}

/* ---------- 7 · deckle edges ----------
   Every card is a cut piece of stationery: the card's paper, hairline
   and shadow move onto a backing pseudo-element and run through the
   deckle filter (#deckle-edge in index.html), so the edges come out
   softly nibbled, never ruler-straight. The content stays crisp. */
.deckle { isolation: isolate; }
.j-card.deckle,
.stay-card.deckle,
.kota-card.deckle,
.event-card.deckle,
.contact-card.deckle { background: none; border-color: transparent; }
.map-frame.deckle { background: none; box-shadow: none; position: relative; }
.deckle::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--deckle-bg, var(--paper));
  border: 1px solid rgba(179, 144, 90, .4);
  border-radius: inherit;
  filter: url(#deckle-edge);
}
.j-card.deckle                    { --deckle-bg: rgba(255, 253, 246, .88); }
.stay-card.deckle, .kota-card.deckle,
.event-card.deckle, .contact-card.deckle { --deckle-bg: rgba(255, 253, 246, .65); }
.map-frame.deckle::after          { box-shadow: 0 14px 30px rgba(76, 65, 52, .16); }

/* ============================================================
   RESPONSIVE
   ============================================================ */
/* the journey folds to a left-hand rail: beads down the left edge,
   every card to their right */
@media (max-width: 700px) {
  .journey { max-width: none; }
  /* the seal and diya tighten to their corners */
  .venue-seal { top: -20px; right: -8px; width: 74px; }
  .j-node { grid-template-columns: 52px 1fr; padding: 16px 0; }
  .j-bead { grid-column: 1; }
  .j-left .j-card, .j-right .j-card {
    grid-column: 2; grid-row: 1;
    justify-self: stretch;
    max-width: none;
    transform: translateY(16px);
  }
  .j-card { padding: 26px 20px 24px; }
  .j-what { font-size: 31px; }
  .j-grand { padding: 26px 0 22px; }
  .j-card-grand { transform: translateY(16px); }
  .j-events { flex-direction: column; align-items: center; }
  .j-events .event-card { flex-basis: auto; width: 100%; max-width: 340px; }
  /* two stops per row — the hairline only makes sense in a single row */
  .j-rhythm { grid-template-columns: repeat(2, 1fr); row-gap: 18px; }
  .j-rhythm::before { display: none; }
}

@media (max-width: 720px) {
  .stage-envelope { justify-self: center; margin-left: 0; }   /* mobile: envelope centred */
  .bg-border { width: 170%; opacity: .8; }
  .bg-corner-l { width: 96px; }
  .bg-corner-r { width: 82px; }
}

/* portrait phones: the tall viewport leaves dead bands above and below a
   90vw envelope, so let the scene run wider and pull everything a touch
   closer together — the opened trio fills the middle instead of floating */
@media (max-width: 560px) {
  :root {
    --env-w: min(94vw, 118vh);     /* fallback for browsers without dvh */
    --env-w: min(94vw, 118dvh);
  }
  .stage-envelope { margin-top: 8px; }
  .hero { padding-top: clamp(36px, 5.5vh, 60px); padding-top: clamp(36px, 5.5dvh, 60px); }
}

/* laptops (1024–1680 wide, up to ~1000 tall): the scene reads a touch too
   grand at the 560px cap, so pull it in. --env-w drives the envelope AND the
   lace ring (1.24 × env-w) plus every flower/hint inside, so one change
   shrinks the whole composition proportionally — envelope and lace equally.
   Sits before the max-height tiers so very short screens can still win. */
@media (min-width: 1024px) and (max-width: 1680px) and (max-height: 1000px) {
  :root {
    --env-w: min(560px, 90vw, 112dvh);
    --env-w: min(560px, 90vw, 112dvh);
  }
}

/* shorter screens: the tucked contents peek out less, so the card's top
   never runs up under the names. The polaroid keeps one rise at every
   height — its foot tucks into the pocket regardless. Tiers run
   tallest-first; later ones win. */
@media (max-height: 820px) {
  body.card-up .env-card   { transform: translateY(-20%); }
  body.card-up .env-photos { transform: translateY(-17%); }
}

/* short laptops (1280×720 et al): the names sit close to the envelope,
   so both the card and the photo pair stay lower */
@media (max-height: 760px) {
  body.card-up .env-card   { transform: translateY(-15%); }
  body.card-up .env-photos { transform: translateY(-12%); }
}

@media (max-height: 700px) {
  body.card-up .env-card   { transform: translateY(-20%); }
  body.card-up .env-photos { transform: translateY(-16%); }
  .reveal-names { font-size: clamp(32px, 5vw, 56px); }
}

/* short landscape screens: tuck the flora closer, hint nearer the envelope */
@media (max-height: 560px) {
  .flora { inset: -4% -5%; }
  .tap-hint { top: calc(100% + 10px); }
}

/* very short screens (landscape phones): shrink envelope + text so the
   tucked contents, the names and the scroll cue all fit */
@media (max-height: 480px) {
  :root { --env-w: min(560px, 90vw, 76vh); --env-w: min(560px, 90vw, 76dvh); }
  body.card-up .env-card   { transform: translateY(-14%); }  /* keeps the hashtag above the fold's vertex */
  body.card-up .env-photos { transform: translateY(-8%); }
  .reveal-head { top: 2%; }
  .reveal-names { font-size: clamp(28px, 4.6vw, 42px); }
}

/* ============================================================
   REDUCED MOTION
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .envelope, .tap-hint, .flw, .rose, .sprig, .bud, .cue-dot, .env-chain { animation: none !important; }
  .stage-envelope, .reveal-kicker, .reveal-names,
  body.revealed .scroll-cue {
    animation-duration: .01s !important;
    animation-delay: 0s !important;
  }
  body.card-up .env-card,
  body.card-up .card-inner,
  body.card-up .env-playlist,
  body.card-up .env-photos { transition-duration: .2s !important; transition-delay: 0s !important; }
  .env-playlist, .env-playlist .pl-wave i, .env-playlist .pl-play { animation: none !important; }
  .env-playlist.playing .pl-wave { opacity: 1; }
  body.opening .env-flap-raised,
  body.opened .env-flap-raised,
  body.opening .flap-lining { transition-duration: .2s !important; transition-delay: 0s !important; }
  .env-chain { transition: none !important; }
  body.opening .env-front,
  body.opened .env-front { transition-duration: .2s !important; transition-delay: 0s !important; }
  body.revealed .song-fab { transition-delay: 0s !important; }
  .panel > *, .panel.in > * { transition-duration: .01s !important; transition-delay: 0s !important; }
  .wcomp { animation: none !important; }
  /* the journey: everything at rest — thread fully drawn, no pops or sways */
  .j-thread-line { stroke-dasharray: none !important; stroke-dashoffset: 0 !important; }
  .j-bead { opacity: 1 !important; transform: none !important; animation: none !important; }
  .j-node .j-card, .j-hour, .j-wear, .j-wear i { transition-duration: .01s !important; transition-delay: 0s !important; }
  .j-wear { opacity: 1 !important; transform: none !important; }
  .j-wear i { opacity: 1 !important; transform: rotate(var(--tilt, 0deg)) !important; animation: none !important; }
  .journey-close { opacity: 1 !important; transform: none !important; transition: none !important; }
  .j-icon path, .j-icon circle { stroke-dasharray: none !important; stroke-dashoffset: 0 !important; transition: none !important; }
  .j-icon .j-fill { opacity: 1 !important; transition: none !important; }
  body.opening .lace-ring,
  body.opened .lace-ring { transition: none !important; opacity: 0; }
  /* the flourishes at rest — everything simply there, nothing moving */
  .j-what { clip-path: none !important; filter: none !important; transition: none !important; }
  .j-what::before { opacity: 1 !important; transform: translate(-50%, -50%) !important; transition: none !important; }
  .venue-seal, .map-diya { animation: none !important; opacity: 1 !important; }
  .map-diya .diya-flame { animation: none !important; }
}
