/* ============================================================
   UDIVERSE — A Journey Into Our Universe
   Cinematic wedding experience · Udit & Divya · Bali · 29.12.2026
   Prototype stylesheet (CSS-crafted scenes stand in for AI heroes)
   ============================================================ */

/* ---------- Fonts ---------- */
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600&family=Playfair+Display:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Cormorant+Garamond:ital,wght@1,500;1,600&family=Pinyon+Script&family=Montserrat:wght@300;400;500;600&display=swap');

/* ---------- Design tokens ---------- */
:root{
  --peach:#F7A5BA;
  --coral:#F07A59;
  --gold:#F6C177;
  --gold-soft:#e9c48a;
  --teal:#4DB6AC;
  --ocean:#0D3B4E;
  --sand:#F2E6DB;
  --ivory:#FFFAF6;

  /* Fresh-morning atmosphere. The old plum/coral dusk family read as a
     sundowner; the sea-and-sunrise family below is the same luxury, earlier
     in the day. Gold stays exactly as it was — it is the brand's metal, not
     part of the weather. */
  --azure:#1C86B0;      /* mid morning sky */
  --aqua:#54C4C0;       /* turquoise shallows */
  --sun:#FFD86B;        /* clean morning yellow (not amber, not coral) */
  --deep:#0A3049;       /* deep ocean — the base dark everything sits on */
  --ink:#08202F;

  --font-engrave:'Cinzel',serif;
  --font-display:'Playfair Display',serif;
  /* Formal engraved script — high stroke contrast, fine hairlines. Replaced
     Allura, which was rounder and lower-contrast than the reference the
     couple's names are meant to match. */
  --font-script:'Pinyon Script',cursive;
  --font-body:'Montserrat',sans-serif;

  /* Ambient sky per chapter — every mood lives in the SAME morning family
     (deep azure zenith → turquoise → fresh yellow horizon) so chapters feel
     like one bright morning opening up, never a cut to a different day.
     Consecutive chapters sit close together in hue on purpose: that is what
     makes the scroll read as one continuous sky instead of six backdrops. */
  --sky:linear-gradient(180deg,#1E7BA8 0%,#35A3C4 28%,#63C6C8 56%,#A8E0CE 78%,#FFEFB4 100%);
  --sky-tint:rgba(80,190,200,.20);
  --grain:.05;
}

*{margin:0;padding:0;box-sizing:border-box}
/* NO global scroll-behavior:smooth. It sounds like a free upgrade, but it puts
   a second smooth-scroll engine on the same axis as the JS tween that actually
   drives the journey; when the two disagree the page stalls partway through a
   move. All deliberate scrolling on this site is owned by tweenScroll() in
   script.js — one engine, always interruptible. */
body{
  font-family:var(--font-body);
  background:var(--deep);
  color:var(--ivory);
  overflow-x:hidden;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}
/* svh, not vh: on phones the URL bar makes 100vh TALLER than what's actually
   visible, so a locked 100vh body pushes the boarding pass partly off-screen
   on load. svh is the small (toolbar-visible) viewport, which always fits.
   The vh line stays first as the fallback for older engines. */
body.locked{overflow:hidden;height:100vh;height:100svh}
img{max-width:100%;display:block}
::selection{background:var(--gold);color:var(--deep)}

/* ---------- Mobile-first touch polish ----------
   This site was already tuned hard for phones (see the perf notes further
   down), but nothing had ever set how a TAP itself feels. Chrome/Safari's
   default is a grey/blue flash box that ignores border-radius — on round
   chrome like .menu-btn or .wa-fab that reads as a square flickering over a
   circle, the same "wrong shape" failure the backdrop-filter bug caused for
   a different reason. Killed globally; every real control below gets its
   own deliberate :active state instead, so a tap still gives feedback, just
   the right shape.
   touch-action:manipulation removes the ~300ms tap delay some in-app
   WebViews (WhatsApp, Instagram — this invite's actual audience) still run
   while they wait to see if a tap becomes a double-tap-to-zoom. */
*{-webkit-tap-highlight-color:transparent}
a,button,.menu-btn,.music-toggle,.scroll-cue,.wa-fab,.page-back,.ghost-btn,.pill,.nav-panel a{
  touch-action:manipulation}

/* Shared type helpers */
.overline{
  font-family:var(--font-body);
  text-transform:uppercase;
  letter-spacing:.42em;
  font-weight:500;
  font-size:.66rem;
  color:#f2d29a;
  text-shadow:0 1px 12px rgba(0,0,0,.6);
}
.script{font-family:var(--font-script);line-height:1;color:var(--gold-soft)}
.display{font-family:var(--font-display);font-weight:400;letter-spacing:.01em}
.rule{
  position:relative;width:60px;height:1px;margin:1.4rem auto;
  background:linear-gradient(90deg,transparent,var(--gold-soft),transparent);
}
/* every divider carries the same gold diamond — one recurring jewel motif */
.rule::after{content:"";position:absolute;left:50%;top:50%;width:6px;height:6px;
  transform:translate(-50%,-50%) rotate(45deg);
  background:linear-gradient(135deg,#fdf0c0,#e0b25c);
  box-shadow:0 0 10px rgba(246,193,119,.85)}
.rule.small{width:34px;margin:1rem auto}
.rule.small::after{width:5px;height:5px}

/* ============================================================
   AMBIENT SKY (persistent, colour-shifts per chapter)
   ============================================================ */
.ambient{
  position:fixed;inset:0;z-index:0;
  background:var(--sky);
  transition:background 1400ms ease, filter 1400ms ease;
}
.ambient::after{ /* film grain + vignette */
  content:"";position:absolute;inset:0;pointer-events:none;
  background:
    radial-gradient(120% 90% at 50% 15%,transparent 40%,rgba(0,0,0,.28) 100%),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.05'/%3E%3C/svg%3E");
  mix-blend-mode:soft-light;opacity:.6;
}

/* Chapter-driven sky moods (set via body[data-chapter]) */
body[data-chapter="welcome"] .ambient{--x:0}
body[data-chapter="reveal"]  .ambient{background:linear-gradient(180deg,#2183AF 0%,#3AA9C8 28%,#6ACBCB 56%,#AFE3D1 78%,#FFF2BA 100%)}
body[data-chapter="story"]   .ambient{background:linear-gradient(180deg,#1B76A2 0%,#319EC0 28%,#5EC2C5 56%,#A3DDCB 78%,#FBECAF 100%)}
/* Day One is golden hour — the one sky in the journey that has actually set.
   The gold climbs far higher up the gradient than anywhere else, but the zenith
   stays in the same blue family so the seam from Our Story still dissolves. */
body[data-chapter="day1"]    .ambient{background:linear-gradient(180deg,#2A7FA8 0%,#4FA6BE 26%,#8FC9BE 52%,#E2CFA4 76%,#FFE1A0 100%)}
body[data-chapter="day2"]    .ambient{background:linear-gradient(180deg,#2489B4 0%,#3FAFCC 28%,#70D0CE 56%,#B4E6D4 78%,#FFF4C0 100%)}
body[data-chapter="day3"]    .ambient{background:linear-gradient(180deg,#165F8A 0%,#2A8BAF 28%,#55B8BE 56%,#97D5C6 78%,#F6E6AC 100%)}
body[data-chapter="rsvp"]    .ambient{background:linear-gradient(180deg,#1F80AC 0%,#38A6C6 28%,#67C9CA 56%,#ABE1CF 78%,#FFF0B6 100%)}

/* ============================================================
   PARTICLES — fairy lights / bokeh / stars
   ============================================================ */
.particles{position:fixed;inset:0;z-index:1;pointer-events:none;overflow:hidden}
.spark{
  position:absolute;border-radius:50%;
  background:radial-gradient(circle,rgba(255,238,200,.95),rgba(255,210,140,.15) 60%,transparent 70%);
  filter:blur(.3px);
  animation:drift linear infinite, twinkle ease-in-out infinite;
  opacity:0;
}
@keyframes drift{
  from{transform:translateY(0) translateX(0)}
  to{transform:translateY(-18vh) translateX(8px)}
}
@keyframes twinkle{
  0%,100%{opacity:.15}
  50%{opacity:.9}
}

/* ============================================================
   00 · BOARDING PASS INTRO
   ============================================================ */
.intro{
  position:fixed;inset:0;z-index:1000;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:clamp(16px,4vh,40px);padding:clamp(14px,3vh,30px) 0;
  perspective:1600px;
  /* The exact same --sky the journey opens into — vivid azure through
     turquoise into morning gold, no dark navy holding it back. The boarding
     pass now sits against the same bright sky it's about to open onto. */
  background:
    radial-gradient(120% 120% at 50% 120%,rgba(246,193,119,.35),transparent 55%),
    var(--sky);
  transition:opacity 1400ms ease, transform 1400ms ease;
}
.intro.launched{pointer-events:none}
/* Boarding pass page is held still — no floating card, comets, twinkling
   stars, or glow pulses. The one motion left is the tap-to-begin CTA, which
   is navigation rather than decoration: with everything else on the ticket
   at rest, the single moving element is unmistakably the thing to press. */
.intro .boarding-pass{animation:none}
.intro .cosmos .comet,.intro .constellation .star-mote{animation:none}
.intro .bp-rule::after{animation:none}
.intro .glow{
  position:absolute;left:50%;bottom:-30%;width:90vw;height:70vh;transform:translateX(-50%);
  background:radial-gradient(closest-side,rgba(246,193,119,.5),transparent 70%);
  filter:blur(30px);opacity:.8;
}

/* Ganesh invocation — page chrome, not chapter content. It sits quietly at
   the top when the guest arrives and retires as soon as they scroll, so the
   blessing opens the journey without ever competing with it.
   Deliberately understated: flat quiet gold at half opacity, no foil gradient
   and no travelling sheen. Those drew the eye, which is the opposite of what
   this should do. A dark shadow carries it over bright photography.
   Devanagari uses a system Indic font (not our embedded Latin faces); the
   per-glyph fallback ends in serif so it renders everywhere.
   Sits below the star-track (0-34px) and clear of the nav buttons (18-64px). */
/* ABSOLUTE against #ch-welcome, not fixed against the viewport — see the long
   note in index.html by the markup for why this is a deliberately different
   fix from the "position:fixed;top:74px" approach that was tried and
   reverted before. Sibling of .content now, so it scrolls away with the
   chapter on its own, no .show state and no JS wiring needed — same as when
   it lived in the flow, just no longer sharing a box with the greeting. */
.shloka-top{
  position:absolute;left:0;right:0;text-align:center;z-index:4;
  /* Matches #ch-welcome's own top clearance logic (.site-nav uses the same
     max() against env(safe-area-inset-top)) — 74px was the number this used
     to sit at as fixed chrome; padded further on a notched/Home-Screen phone
     so it can never end up under the status bar or crowding the nav row. */
  top:max(74px,calc(env(safe-area-inset-top) + 56px));
  white-space:nowrap;
  font-family:'Nirmala UI','Noto Serif Devanagari','Tiro Devanagari Sanskrit','Mangal','Kohinoor Devanagari',serif;
  font-size:clamp(.64rem,2.2vw,.78rem);letter-spacing:.08em;font-weight:600;
  color:#f0d9a8;opacity:.62;
  text-shadow:0 1px 8px rgba(0,0,0,.8),0 0 20px rgba(0,0,0,.55);
}

/* Shooting stars — brief gold streaks entering from each side of the intro
   sky. Each comet travels along its own rotated axis, glows for a moment,
   and rests dark for most of its cycle so they stay rare and precious. */
.cosmos{position:absolute;inset:0;z-index:1;pointer-events:none;overflow:hidden}
.comet{position:absolute;width:120px;height:2px;border-radius:2px;opacity:0;
  background:linear-gradient(90deg,rgba(255,246,220,0),rgba(255,240,200,.85) 62%,#fff);
  box-shadow:0 0 10px rgba(255,240,200,.55);
  animation:comet var(--cdur,11s) ease-in infinite;animation-delay:var(--cdelay,0s)}
@keyframes comet{
  0%,84%{opacity:0;transform:rotate(var(--crot,16deg)) translateX(0)}
  87%{opacity:.95}
  96%,100%{opacity:0;transform:rotate(var(--crot,16deg)) translateX(var(--cdist,55vw))}
}

@media(max-width:820px){.cosmos{display:none}}

.boarding-pass{
  position:relative;width:min(88vw,380px);
  /* the card's own side padding, as a token: the tear line has to bleed back
     out through it to reach both edges, and the two responsive branches below
     retune it in one place rather than three. */
  --pad-x:26px;
  background:linear-gradient(155deg,#fffaf4 0%,#fbf1e4 55%,#f4e6d4 100%);
  color:var(--ink);
  border-radius:20px;
  padding:26px var(--pad-x) 30px;
  box-shadow:0 40px 90px -30px rgba(0,0,0,.7),0 2px 0 rgba(255,255,255,.6) inset;
  transform-style:preserve-3d;
  animation:passFloat 7s ease-in-out infinite;
  cursor:pointer;
  transition:transform 1200ms cubic-bezier(.7,0,.3,1),opacity 1000ms ease;
}
.intro.launched .boarding-pass{
  transform:rotateY(-88deg) translateZ(60px) scale(.7);
  opacity:0;
}
@keyframes passFloat{
  0%,100%{transform:translateY(0) rotateX(4deg) rotateY(-6deg)}
  50%{transform:translateY(-10px) rotateX(-3deg) rotateY(6deg)}
}
.boarding-pass::before{ /* gold hairline frame */
  content:"";position:absolute;inset:10px;border:1px solid rgba(190,150,90,.45);border-radius:12px;pointer-events:none;
}
.boarding-pass .perf{ /* torn stub divider — sits in the gap between the
  barcode and the tagline, so the stub holds the tagline + the button.
  In the FLOW, not absolutely positioned: its own margins are the gap, so the
  tagline that follows can never ride up onto the dashes. Bleeds back out
  through the card's side padding to reach both edges. */
  position:relative;height:1px;margin:24px calc(var(--pad-x) * -1) 20px;
  background:repeating-linear-gradient(90deg,rgba(120,90,50,.4) 0 6px,transparent 6px 12px);
}
/* The torn-stub notches are holes punched through the ticket, so they must
   show whatever is behind the card. They used to be a flat #2a1c33, which
   worked only because that was the exact top stop of the old plum intro
   background; against the sky it reads as a dark dot instead of a hole.
   `background-attachment:fixed` paints --sky relative to the VIEWPORT rather
   than the element, so each notch shows precisely the slice of sky sitting
   behind it, at any viewport size. Solid turquoise is the fallback for
   engines that don't honour fixed attachment. */
.boarding-pass .notch{position:absolute;top:50%;margin-top:-10px;width:20px;height:20px;border-radius:50%;
  background:#7FD1CA;
  background-image:var(--sky);background-attachment:fixed;
  background-size:100vw 100vh;background-position:0 0}
.boarding-pass .notch.l{left:-10px}
.boarding-pass .notch.r{right:-10px}
/* iOS Safari does not honour background-attachment:fixed. It paints --sky
   relative to the 20px notch instead of the viewport, so instead of a hole
   showing the sky behind it each notch rendered the gradient's top-left
   corner — a near-black dot on a cream ticket. `-webkit-touch-callout` is
   iOS-only, so this hands those devices the flat turquoise the solid
   `background` above was always there to provide. */
@supports (-webkit-touch-callout:none){
  .boarding-pass .notch{background-image:none}
}

.bp-airline{display:flex;justify-content:space-between;align-items:center}
.bp-airline .name{font-family:var(--font-engrave);font-size:.9rem;letter-spacing:.24em;color:#7a5a34}
/* the airline's promise, written by hand rather than stamped */
.bp-airline .cls{font-family:var(--font-display);font-style:italic;font-size:.76rem;
  letter-spacing:.03em;text-transform:none;color:#a4844f;margin-top:3px}
.bp-airline .plane{font-size:1.05rem;color:#b08b50}

/* the couple's own gold monogram, pressed into the ticket paper. Sized wider
   than the old mark: this one is near-square (485x511) where the previous
   botanical version was tall (560x702), so the same width read smaller. */
/* The monogram is the crest, not the headline — the couple's names carry
   that. Pulled in from 110px so it sits as a mark above the names, and the
   gold pushed richer: the source art photographs slightly pale against warm
   ticket stock, so saturation and a touch of contrast bring it back to leaf
   gold without repainting the asset. */
.bp-logo{display:block;width:88px;height:auto;margin:14px auto 0;
  filter:saturate(1.45) contrast(1.08) brightness(1.03)
         drop-shadow(0 4px 8px rgba(120,90,50,.26))}
/* Pinyon Script sets optically smaller than Allura did at the same px (finer
   hairlines, narrower forms), so the size is up from 2.15rem to hold the same
   presence on the ticket. Gold-brown ink rather than the old terracotta, to
   sit with the monogram above it. */
.bp-names{text-align:center;font-family:var(--font-script);font-size:2.7rem;line-height:1.15;
  color:#8a6234;margin:2px 0 0;text-shadow:0 1px 0 rgba(255,255,255,.6)}
/* engraved flourish — the site's jewel motif, pressed into the paper */
.bp-rule{position:relative;width:74px;height:1px;margin:13px auto 11px;
  background:linear-gradient(90deg,transparent,rgba(170,130,70,.65),transparent)}
.bp-rule::after{content:"";position:absolute;left:50%;top:50%;width:6px;height:6px;
  transform:translate(-50%,-50%) rotate(45deg);border-radius:1px;
  background:linear-gradient(135deg,#f2d59a,#a8783a);
  animation:jewelGlow 4.5s ease-in-out infinite}
@keyframes jewelGlow{0%,100%{box-shadow:0 0 4px rgba(200,150,70,.45)}
  50%{box-shadow:0 0 9px rgba(214,166,86,.9)}}
.bp-tag{text-align:center;font-family:var(--font-engrave);font-size:.58rem;letter-spacing:.4em;color:#a07f52;text-transform:uppercase}

/* The detail block is PRINTED, not typeset: one family, one ink, one size,
   no bold — the way a real ticket comes out of the machine. Labels stay
   legible as labels through tracked-out caps alone, never through colour,
   size or weight. */
.bp-grid{display:grid;grid-template-columns:1fr 1fr;gap:13px 10px;margin:20px 4px 8px}
.bp-grid .lab,.bp-grid .val{font-family:var(--font-body);font-weight:400;font-size:.74rem;
  color:#4a3728;line-height:1.3}
/* Only the ANSWERS are printed in full-strength ink. The field names are
   scaffolding — a guest reads "Denpasar (DPS)", not "Destination" — so they
   drop to a lighter wash of the same brown and the values stay dark. */
.bp-grid .lab{letter-spacing:.22em;text-transform:uppercase;color:rgba(74,55,40,.55)}
.bp-grid .val{letter-spacing:.01em;margin-top:3px;color:#3d2b1c}
/* second line of a value (e.g. the country under the airport code) — same
   family, ink and size as the line above it, so the block stays "printed" */
.bp-grid .val .sub{display:block}
/* Printed in the same brown ink as the rest of the pass rather than near-black,
   and lightened well below it. The barcode is texture that says "this is a
   real ticket"; it should never out-weigh the flight details above it. */
.bp-barcode{height:34px;margin:14px 4px 6px;
  background:repeating-linear-gradient(90deg,#4a3728 0 2px,transparent 2px 3px,#4a3728 3px 4px,transparent 4px 7px);
  opacity:.38;border-radius:2px}

.bp-cta{
  /* no margin-top: the tear line above owns the gap, which is what keeps the
     tagline clear of the dashes at every size. */
  text-align:center;
  font-family:var(--font-display);font-style:italic;font-size:.88rem;
  letter-spacing:.015em;text-transform:none;color:#8a6a3c;
}
/* the ONE thing to tap — solid gold, glowing pulse ring, travelling sheen.
   It has to POP off the paper: everything else on the ticket is flat print
   ink, so this is the only element with lift, a halo and motion. It arrives a
   beat after the pass has settled (ctaArrive), then breathes on a slow loop
   that pushes an expanding ring outward — the "press me" gesture read from
   across a room, and the first animation a guest ever sees on this site. */
.bp-cta .pulse{display:inline-block;position:relative;overflow:hidden;margin-top:12px;padding:15px 34px;
  border:1px solid rgba(150,100,30,.55);border-radius:34px;cursor:pointer;
  font-family:var(--font-engrave);font-weight:600;font-size:.78rem;letter-spacing:.2em;color:#2a1c33;
  background:linear-gradient(120deg,#f0b45e 0%,#fbe3b2 45%,#e9a94f 100%);
  transform-origin:50% 50%;
  animation:ctaArrive 900ms cubic-bezier(.2,.9,.25,1) both 700ms,
            ctaGlow 2.6s ease-in-out 1600ms infinite}
.bp-cta .pulse::after{content:"";position:absolute;top:0;bottom:0;left:-70%;width:46%;
  background:linear-gradient(100deg,transparent,rgba(255,255,255,.7),transparent);
  transform:skewX(-18deg);animation:btnSheen 3.4s ease-in-out infinite;pointer-events:none}
.bp-cta .pulse:hover{animation-play-state:paused;transform:translateY(-3px) scale(1.03)}
/* Entrance: the button swells up out of the ticket rather than being there
   all along, so the eye is pulled to it the moment the pass finishes drawing. */
@keyframes ctaArrive{
  0%{opacity:0;transform:scale(.72) translateY(6px)}
  60%{opacity:1;transform:scale(1.08) translateY(-3px)}
  100%{opacity:1;transform:scale(1) translateY(0)}}
@keyframes ctaGlow{
  0%,100%{transform:translateY(0) scale(1);
    box-shadow:0 12px 26px -8px rgba(190,130,50,.6),0 0 0 0 rgba(220,160,70,.6)}
  50%{transform:translateY(-4px) scale(1.035);
    box-shadow:0 20px 38px -8px rgba(190,130,50,.8),0 0 0 18px rgba(220,160,70,0)}}
/* Stamped over the barcode on its own disc of paper, so it reads as a seal
   pressed onto the ticket rather than line-art tangled in the bars. */
.bp-seal{position:absolute;right:16px;bottom:3px;width:42px;height:42px;color:#8a6a3c;
  display:flex;align-items:center;justify-content:center;border-radius:50%;
  background:radial-gradient(circle,#fffaf2 62%,#f7ecda 100%);
  box-shadow:0 0 0 1px rgba(190,150,90,.4),0 2px 7px rgba(120,90,50,.2)}
.bp-seal svg{width:82%;height:82%}

/* ============================================================
   JOURNEY / CHAPTERS
   ============================================================ */
.journey{position:relative;z-index:2}
.chapter{
  position:relative;min-height:100vh;min-height:100svh;
  display:flex;align-items:center;justify-content:center;
  padding:8vh 6vw;overflow:hidden;text-align:center;
}
/* SEAMLESS CHAPTERS — each scene used to butt its photo hard against the
   next one, so the journey read as six stacked boxes. Every scene now
   dissolves at its top and bottom edge into the shared fixed sky behind it,
   and because consecutive skies sit close together in hue, the seam has
   nothing to catch on: one continuous morning that the photos surface out of
   and sink back into. The middle 70% is untouched, so nothing is lost from
   the imagery or the text scrim. */
.scene{position:absolute;inset:0;z-index:0;overflow:hidden;
  -webkit-mask-image:linear-gradient(180deg,transparent 0,rgba(0,0,0,.6) 4%,#000 11%,#000 89%,rgba(0,0,0,.6) 96%,transparent 100%);
  mask-image:linear-gradient(180deg,transparent 0,rgba(0,0,0,.6) 4%,#000 11%,#000 89%,rgba(0,0,0,.6) 96%,transparent 100%)}
/* the first scene has no chapter above it — let it run clean to the top edge
   so the journey opens full-bleed rather than fading in from a band */
#ch-welcome .scene{
  -webkit-mask-image:linear-gradient(180deg,#000 0,#000 89%,rgba(0,0,0,.6) 96%,transparent 100%);
  mask-image:linear-gradient(180deg,#000 0,#000 89%,rgba(0,0,0,.6) 96%,transparent 100%)}

/* SEAM VEIL — the join between two chapters is the one place the eye can still
   catch an edge, because one photo ends on sand and the next begins on sky.

   This band used to be `backdrop-filter: blur(3px)`, which genuinely defocused
   the join — and cost more than everything else on the page put together.
   backdrop-filter forces the browser to re-sample and re-blur everything
   composited behind the element; there are two of these per chapter, full
   width, and they sit exactly where the scroll is always passing. Fourteen
   live backdrop blurs is the difference between a scroll that glides and one
   that stutters, and on mobile GPUs it is not close.

   It also turns out not to be load-bearing. `.scene` above already masks its
   own top and bottom edges to transparent, so the two chapters are ALREADY
   cross-fading into the shared ambient sky at the seam — the blur was a second
   pass over a join that had mostly dissolved. What is left here is a soft
   darkening veil in the sky's own key, which lands the last of the edge for
   the price of a gradient. */
.chapter::before,.chapter::after{
  content:"";position:absolute;left:-2%;right:-2%;height:10vh;z-index:2;pointer-events:none}
.chapter::before{top:0;
  background:linear-gradient(180deg,rgba(10,40,62,.22),transparent 100%)}
.chapter::after{bottom:0;
  background:linear-gradient(0deg,rgba(10,40,62,.22),transparent 100%)}
/* nothing above the opening chapter to blend with */
#ch-welcome::before{display:none}
.content{position:relative;z-index:3;width:100%;max-width:760px}
/* Legibility now lives HERE rather than in the grade. The scenes are bright
   morning, so instead of dimming the whole frame we pool a soft deep-water
   shadow directly behind the words — the picture stays fresh, the type stays
   readable. */
/* TUNED DOWN after the client flagged it: at .80 over a 62% ellipse this pool
   read as a dark smudge sitting ON the picture rather than as shade behind the
   words — most visible on the opening chapter, where it lands across a white
   hull and a bright horizon. The fix is not simply "less dark": a weaker pool
   of the same shape just becomes a weaker smudge. It is spread wider, faded
   further out, and blurred harder, so there is no edge anywhere for the eye to
   catch, and the contrast the type actually needs is handed to the per-glyph
   shadows below — those track the letterforms, so they can never read as a
   shape of their own. */
.content::before{content:"";position:absolute;inset:-30% -24%;z-index:-1;pointer-events:none;
  background:radial-gradient(ellipse 74% 78% at 50% 50%,rgba(3,26,44,.46),rgba(3,26,44,.24) 46%,transparent 76%);
  filter:blur(34px)}
/* OFF THE WATERLINE.
   The rsvp plate — used by Day One and by the RSVP chapter — puts its horizon
   at 48% of the frame on the portrait cut and 44% on the landscape one, i.e.
   just ABOVE the centre line that `.chapter{align-items:center}` aligns the
   copy to. The result was that the last line of Day One's lede finished
   exactly on the sea: "…meditation and mindful connection." with the horizon
   through the middle of it (client note, 3 Aug 2026 — "this mindful
   connection sentence is interjecting the ocean line. Move the sentence up").

   The block is lifted so the sentence ends in the sky. svh, not px, because
   the plate is `cover` on the viewport — the horizon sits at a fixed FRACTION
   of the screen, so the correction has to scale with it or it only holds on
   one phone. The info-card is unaffected in kind: it still lands on the
   water, just higher up it.

   TWO SETS OF NUMBERS, because the two cuts frame the sea differently. The
   landscape plate is letterboxed `contain` inside the oversized .plx, which
   lands its horizon roughly 39px higher again on a 800px-tall window, so the
   default lift is the larger one. The portrait branch below then dials it
   back: there the plate is `cover` at exactly the chapter box with parallax
   switched off, so the horizon sits at a known 48.1% of the screen and the
   correction can be smaller and exact.

   RSVP is the same plate and has the same fault — its lede runs three or four
   lines and the horizon crosses one of them — but it is only corrected on the
   PORTRAIT branch, below. It carries more copy below the centre line than Day
   One, and on a 800px-tall desktop window the lift needed to clear the water
   (~118px) drags the "Celebration · RSVP" overline to 34px from the top of
   the screen, level with the nav. There is no lift that both clears the sea
   and keeps the block inside the chapter's padding at that height, so desktop
   is left as it was; closing it properly means shortening that lede by a
   line, which is the couple's copy to change, not ours. */
#ch-day1 .content{transform:translateY(-8.4svh)}

/* OFF THE SUBJECT — the opening two chapters.
   Client note, 3 Aug 2026, 4:35pm: "The text of Welcome to Udiverse on first
   slide is getting lost as its on the subject/ on the boat. Either make the
   photo as a background zoomed out a bit or any way to make the text and image
   in two layers. The same for second slide. The wedding announcement date."

   Both plates put their subject across the middle of the frame — the yacht's
   superstructure runs 40–72% of the picture height, the arch's floral crown
   starts at 43% — and `.chapter{align-items:center}` was dropping the type
   straight onto them. A white hull under white display caps is the worst case
   there is, and no amount of shadow fixes type sitting on a busy object.

   Shrinking the photograph was the other option the note offered, and it is
   the wrong one: these heroes are art-directed frames already shown whole at
   `contain`, and scaling them down is exactly the "small floating image" a
   previous round asked us to get rid of. So the picture is left alone and the
   COPY moves instead, up into the clean sky band both plates open with. The
   result reads as two layers because it genuinely is two: type on sky, subject
   beneath it, nothing overlapping.

   svh for the same reason as the waterline fix above — the subject sits at a
   fixed FRACTION of the screen, so the correction has to scale with it.

   THE TWO CUTS NEED DIFFERENT ANSWERS, because they are not the same picture.
   On the PORTRAIT cuts both subjects start at ~43% of the frame and there is
   half a screen of clean sky above them, so both blocks simply move up into it
   — see the portrait branch further down. On the LANDSCAPE cuts only the yacht
   plate has that sky (the boat starts at 40%); the arch is shot tight and its
   crown is already at 17% of the frame, so there is no band anywhere on it big
   enough to hold a three-part announcement. Moving the type there just trades
   one collision for another.

   So the reveal takes the OTHER option the client offered on landscape — "make
   the photo as a background" — and gets a veil instead of a move. See below. */
/* Welcome HANGS FROM a line just above the yacht instead of being centred and
   then hauled upwards. The fixed lift this replaces had to be re-guessed at
   every viewport — the block's height and the subject's position scale by
   different rules, so a single svh number was too little on one screen (type
   back on the hull) and too much on the next (type climbing into the star
   track and the blessing). Anchoring the BOTTOM of the lockup to a fraction of
   the screen removes the guesswork entirely: the superstructure starts at ~40%
   of this plate, the copy ends above that, and it grows upward from there
   however tall it happens to be. */
#ch-welcome{align-items:flex-start;padding-top:0}
#ch-welcome .content{height:38svh;display:flex;flex-direction:column;justify-content:flex-end}
#ch-reveal  .content{transform:translateY(-4svh)}

/* THE VEIL, landscape only. The arch fills this frame, so the announcement has
   to sit on it and the picture has to drop back behind the words instead.
   Shape is the whole game here: the last attempt at this was an ellipse at .80
   and the client saw "a dark smudge sitting ON the picture". A smudge is
   something with an EDGE. This has none — it runs far wider than the viewport
   so it has no sides, and it fades to fully transparent top and bottom so it
   has no top or bottom either. What is left reads as a deeper patch of sky
   behind the type, which is exactly the two-layer separation being asked for.
   Peak opacity is half what was rejected; the per-glyph shadows still do the
   close-in contrast work. */
#ch-reveal .content::before{inset:-40% -80%;filter:blur(30px);
  background:linear-gradient(180deg,
    transparent 0,rgba(3,26,44,.26) 20%,rgba(3,26,44,.42) 50%,
    rgba(3,26,44,.26) 80%,transparent 100%)}

/* Reveal-on-scroll.
   PERFORMANCE: this used to also transition `filter:blur(4px)` → none. It
   looked lovely and it was the single most expensive thing on the site —
   ~80 elements per page, and animating a filter on TEXT forces the browser to
   re-rasterise every glyph on every frame for the whole 1.2s. That is the
   "scratchy" quality in the copy reveals. Opacity and transform both run on
   the compositor and cost effectively nothing, and at these durations the
   blur was never the part you could actually see. */
.fx{opacity:0;transform:translateY(26px);
  transition:opacity 1200ms ease,transform 1200ms cubic-bezier(.2,.7,.2,1)}
.in .fx{opacity:1;transform:none}
/* .shloka-top now carries .fx directly (it used to inherit its fade from a
   wrapping parent instead), and its own rule sets a deliberately dimmed
   opacity:.62 so it doesn't compete with the headline below it. Same
   specificity as the generic .in .fx above (one class each), source order
   would let THIS rule's opacity:1 win and the dimming would vanish the
   moment the chapter reveals. Three classes beats two — kept the dim. */
.in .fx.shloka-top{opacity:.62}
.in .fx.d1{transition-delay:.15s}
.in .fx.d2{transition-delay:.45s}
.in .fx.d3{transition-delay:.8s}
.in .fx.d4{transition-delay:1.15s}
.in .fx.d5{transition-delay:1.5s}

/* Ocean band shared by sea scenes */
.ocean{
  position:absolute;left:-5%;right:-5%;bottom:0;height:46%;
  background:linear-gradient(180deg,rgba(255,255,255,.12),transparent 12%),
    radial-gradient(120% 60% at 50% 0%,rgba(255,220,170,.35),transparent 60%),
    linear-gradient(180deg,#12586a 0%,#0d3b4e 60%,#08222e 100%);
  box-shadow:0 -20px 60px rgba(0,0,0,.25) inset;
}
.ocean.teal{background:linear-gradient(180deg,rgba(255,255,255,.18),transparent 14%),
    radial-gradient(120% 60% at 50% 0%,rgba(180,240,235,.4),transparent 60%),
    linear-gradient(180deg,#2ba7a6 0%,#127e86 55%,#0c5866 100%)}
.ocean .shimmer{position:absolute;inset:0;
  background:repeating-linear-gradient(180deg,rgba(255,255,255,.06) 0 2px,transparent 2px 9px);
  animation:sea 6s linear infinite;opacity:.5}
@keyframes sea{from{background-position-y:0}to{background-position-y:9px}}

/* Sun / moon glow */
.sun{position:absolute;left:50%;top:34%;width:190px;height:190px;
  transform:translate(-50%,-50%) translateY(var(--pary,0));
  border-radius:50%;background:radial-gradient(circle,rgba(255,241,214,.98),rgba(246,180,110,.55) 45%,transparent 68%);
  filter:blur(2px)}
.sun.low{top:52%}
.sun.moon{background:radial-gradient(circle,rgba(240,240,255,.9),rgba(180,190,230,.3) 45%,transparent 68%);width:120px;height:120px}

/* Reflection column on water */
.reflect{position:absolute;left:50%;top:40%;width:60px;height:40%;transform:translateX(-50%);
  background:linear-gradient(180deg,rgba(255,225,170,.5),transparent);filter:blur(3px);opacity:.7}

/* Palm silhouettes */
.palm{position:absolute;bottom:38%;width:150px;color:#1c1220;opacity:.9}
.palm.l{left:2%;transform:translateY(var(--pary,0)) scaleX(-1) rotate(-4deg)}
.palm.r{right:2%;transform:translateY(var(--pary,0)) rotate(-3deg)}
.palm svg{width:100%;height:auto;display:block;filter:drop-shadow(0 6px 8px rgba(0,0,0,.3))}

/* Yacht */
.yacht{position:absolute;left:50%;bottom:34%;width:230px;
  transform:translateX(-50%) translateY(var(--pary,0)) scale(var(--ys,1));
  color:#f7f3ec;filter:drop-shadow(0 14px 18px rgba(0,0,0,.45));will-change:transform}
.yacht svg{width:100%;height:auto}
.yacht .wake{position:absolute;left:50%;bottom:-14px;width:120%;height:26px;transform:translateX(-50%);
  background:radial-gradient(60% 100% at 50% 0,rgba(255,255,255,.5),transparent 70%);filter:blur(3px)}

/* Mandap */
.mandap{position:absolute;left:50%;bottom:32%;width:340px;
  transform:translateX(-50%) translateY(var(--pary,0));
  color:#fbf6ee;filter:drop-shadow(0 18px 26px rgba(0,0,0,.4));will-change:transform}
.mandap svg{width:100%;height:auto}

/* Fairy-light string */
.string{position:absolute;top:0;left:0;right:0;height:120px;pointer-events:none;transform:translateY(var(--pary,0))}
.string i{position:absolute;top:0;width:5px;height:5px;border-radius:50%;
  background:radial-gradient(circle,#fff2cf,#f6c177);box-shadow:0 0 8px 2px rgba(246,193,119,.7);
  animation:twinkle 3s ease-in-out infinite}

/* Marigold garland (day1) */
.garland{position:absolute;top:6%;left:-4%;right:-4%;height:60px;display:flex;justify-content:space-around;transform:translateY(var(--pary,0))}
.garland b{width:16px;height:16px;border-radius:50%;
  background:radial-gradient(circle at 35% 30%,#ffd76a,#f39a1f 70%);
  box-shadow:0 3px 6px rgba(0,0,0,.25);transform-origin:top;animation:sway 5s ease-in-out infinite}

@keyframes sway{0%,100%{transform:translateY(0) rotate(-3deg)}50%{transform:translateY(6px) rotate(3deg)}}

/* Scrim to seat typography over scenes */
.scrim{position:absolute;inset:0;z-index:2;
  background:radial-gradient(90% 70% at 50% 55%,rgba(20,12,25,.28),transparent 70%)}

/* ============================================================
   CHAPTER TYPOGRAPHY
   ============================================================ */
/* ------------------------------------------------------------------
   THE WORD MORPH — Universe → Udiverse
   This used to change the single letter the two words differ by, holding the
   other seven provably still. The client asked for the opposite on 5 Aug 2026
   ("replace whole Universe with Udiverse, she want the full word animation"),
   so both words are now set in full and cross over as wholes.

   The two are stacked in ONE grid cell rather than one being taken out of
   flow. That matters: the cell sizes to the wider word, so the heading's box
   never changes and neither word is ever off-centre — which is what would
   happen with position:absolute, where the h1 would size to whichever word
   stayed in flow and the other would hang off-axis.
   ------------------------------------------------------------------ */
.welcome-morph{position:relative;width:100%;padding:0 1rem;text-align:center}
.wm-lead{
  font-family:'Cormorant Garamond',var(--font-display);font-style:italic;font-weight:500;
  /* svh term added 6 Aug 2026 — this line had no height cap at all, only
     .wm-word did (see its note below). New collision on a wide-but-short
     desktop window (client screenshot, ~1512x720): the shloka moved out to
     its own top-pinned spot the same day, and .wm-lead — sitting directly
     below it, bottom-anchored inside #ch-welcome .content along with
     .wm-word and .rule — rode its 1.9rem width-driven ceiling regardless of
     how little vertical room was actually available, and rose straight
     into it. */
  font-size:clamp(1.05rem,min(3.4vw,3.4svh),1.9rem);letter-spacing:.14em;line-height:1.2;
  /* tight shadow for the contrast, wide one for the bed — the tight one is
     what replaced the depth the scrim used to supply */
  color:#fdf2dc;text-shadow:0 1px 2px rgba(0,0,0,.45),0 3px 24px rgba(0,0,0,.42);
  opacity:0;transform:translateY(10px);
  transition:opacity 1500ms ease,transform 1500ms cubic-bezier(.2,.7,.2,1);
}
.welcome-morph.lit .wm-lead{opacity:.92;transform:none}

.wm-word{
  /* one cell, both words in it — see the note above */
  display:grid;position:relative;justify-items:center;
  margin-top:clamp(.3rem,1.4vw,.7rem);
  font-family:var(--font-engrave);font-weight:500;
  /* The svh term is what keeps a short, wide window working: at 1280x620 the
     width-driven size alone made the lockup taller than the sky above the
     yacht, so there was no position that cleared both the boat and the top of
     the screen. Height now caps it too. Tightened 12svh→10.5svh 6 Aug 2026
     for the same reason .wm-lead gained its own svh term the same day — the
     shloka moving to a fixed spot above this block made the TOP boundary
     tighter than the yacht-clearance tuning this cap was originally set
     against, on wide-short desktop windows specifically (~1512x720). */
  font-size:clamp(2.4rem,min(9.4vw,10.5svh),6rem);line-height:1.08;
  letter-spacing:.14em;color:var(--ivory);
  text-shadow:0 1px 3px rgba(0,0,0,.42),0 4px 34px rgba(0,0,0,.38);
  white-space:nowrap;                 /* the word must never wrap mid-morph */
  opacity:0;transform:translateY(14px);
  transition:opacity 1600ms ease,transform 1600ms cubic-bezier(.2,.7,.2,1),
             letter-spacing 2400ms cubic-bezier(.16,.7,.2,1);
}
.welcome-morph.lit .wm-word{opacity:1;transform:none;transition-delay:.5s}
/* once the turn has happened the word draws itself in slightly, the way a
   title settles after a reveal — the last beat of the sequence */
.welcome-morph.settled .wm-word{letter-spacing:.1em}

/* The rule directly under .welcome-morph (`.fx.d3.rule` in the markup) is the
   third and last thing eating into the same bottom-anchored box as .wm-lead
   and .wm-word above — and unlike them it had NO responsive sizing at all:
   the shared `.rule{margin:1.4rem auto}` is a flat 22.4px top AND bottom
   regardless of viewport, fine everywhere .rule is used as a section
   divider, too expensive here where every pixel of a 38svh box is budgeted.
   Scoped to this chapter only — every other .rule on the site keeps its
   normal margin, this is purely about the wide-short-desktop squeeze
   documented on .wm-lead and .wm-word above. */
#ch-welcome .content > .rule{margin:clamp(.5rem,3svh,1.4rem) auto}

/* Both words occupy the same grid cell. 2200ms is the pace the client asked
   for ("on repeat and slowed"); script.js spaces the surrounding beats to it. */
.wm-alt{
  grid-area:1/1;
  transition:opacity 2200ms ease,transform 2200ms cubic-bezier(.3,.7,.2,1),
             filter 2200ms ease,text-shadow 1100ms ease,color 1100ms ease;
}
/* Universe is what the guest reads first */
.wm-alt.from{opacity:1;transform:none;filter:none}
/* Udiverse waits just below, blurred out of existence */
.wm-alt.to{opacity:0;transform:translateY(.16em) scale(.95);filter:blur(10px)}

/* beat 1 — the word about to change quietly announces itself */
.welcome-morph.arm .wm-alt.from{
  text-shadow:0 0 26px rgba(246,193,119,.95),0 0 60px rgba(246,193,119,.5);
  color:#ffeec4}
/* beat 2 — Universe lifts away and dissolves upward; Udiverse rises into it */
.welcome-morph.turn .wm-alt.from{opacity:0;transform:translateY(-.14em) scale(1.05);filter:blur(12px)}
.welcome-morph.turn .wm-alt.to{opacity:1;transform:none;filter:none;
  text-shadow:0 0 30px rgba(246,193,119,.8),0 4px 40px rgba(0,0,0,.45)}
/* beat 3 — the gold cools back to ivory and the word is just the word again */
.welcome-morph.settled .wm-alt.to{text-shadow:0 4px 40px rgba(0,0,0,.45);
  transition-duration:1800ms}

/* Glitter / gold-petal burst, radiating out from the dissolve point */
.burst-layer{position:absolute;inset:0;pointer-events:none;overflow:visible;z-index:5}
.burst-bit{position:absolute;left:50%;top:50%;opacity:0;
  animation:burstOut var(--bdur,2.4s) cubic-bezier(.15,.6,.3,1) forwards;
  animation-delay:var(--bdelay,0s)}
.burst-bit.glitter{width:5px;height:5px;border-radius:50%;
  background:radial-gradient(circle,#fff8e0,#f6c177 60%,transparent 75%);
  box-shadow:0 0 7px 1px rgba(246,193,119,.75)}
.burst-bit.petal{width:12px;height:12px;border-radius:60% 0 60% 0;
  background:radial-gradient(circle at 35% 30%,#fce3ae,#e9b95f);
  box-shadow:0 0 6px 1px rgba(246,193,119,.3)}
.burst-bit.star{width:8px;height:8px;
  background:radial-gradient(circle,#fff6df,#f6c177 55%,transparent 70%);
  clip-path:polygon(50% 0%,61% 39%,100% 50%,61% 61%,50% 100%,39% 61%,0% 50%,39% 39%)}
@keyframes burstOut{
  0%{transform:translate(-50%,-50%) scale(.2) rotate(0deg);opacity:0}
  10%{opacity:1}
  70%{opacity:.85}
  100%{transform:translate(calc(-50% + var(--bx,0px)),calc(-50% + var(--by,0px))) scale(1) rotate(var(--brot,220deg));opacity:0}
}

/* text-wrap:balance evens the two lines out instead of running the first one
   full and dropping a word or two onto the second — combined with the nbsp in
   the markup, this line can no longer strand "for." on its own. */
.reveal-pre{font-family:var(--font-display);font-style:italic;font-size:clamp(1rem,2.6vw,1.35rem);color:var(--sand);opacity:.95;
  text-wrap:balance;max-width:22em;margin-inline:auto}
.reveal-names{font-family:var(--font-display);font-size:clamp(3rem,11vw,7rem);line-height:1.02;color:var(--ivory);margin:.2em 0}
.reveal-names .amp{font-family:var(--font-script);color:var(--gold);font-size:.8em;vertical-align:.08em;margin:0 .12em}
/* THE DATE — the actual announcement.
   This used to be a dark rounded "glass chip": a filled plaque with a border
   and a backdrop blur, holding one long line that wrapped mid-place-name on a
   phone ("29 DECEMBER 2026 · BALI," / "INDONESIA"). Two problems, both fair.
   It was a coloured rectangle behind text — the exact thing the brief asks us
   to remove — and it made the single most important line on the site look like
   a caption sitting on top of the picture rather than part of it.

   Rebuilt as a proper lockup and set free of any box. The numerals carry it at
   display size, the weekday and the place sit either side in small engraved
   caps, and legibility comes from a deep soft shadow rather than a panel — the
   same way the names above it already work. Nothing between the guest and the
   photograph. */
.reveal-when{display:flex;flex-direction:column;align-items:center;gap:.55rem;
  margin-top:.4rem;text-shadow:0 2px 22px rgba(0,0,0,.75),0 1px 4px rgba(0,0,0,.5)}
.rw-day,.rw-place{font-family:var(--font-engrave);text-transform:uppercase;
  font-size:clamp(.6rem,1.7vw,.74rem);letter-spacing:.42em;
  /* letter-spacing pushes the last glyph off-centre — pull it back */
  text-indent:.42em;color:var(--gold-soft)}
/* Client, 6 Aug 2026, screen recording of the live site: "Bali dikh nahi
   raha" (Bali isn't visible). It's not missing, it's camouflaged — this line
   sits lowest in the lockup, right where the yacht plate's sky gradient
   turns to the same warm cream/gold family as the pale #f4e3c4 it was set
   in. .rw-day sits higher, over plain blue, so nobody had flagged it before.
   Swapped to --ivory (near-white, what the date itself uses) plus a denser
   shadow of its own — the day/date above keep the softer shared shadow, this
   one needed more because it's the one line guaranteed to land on the
   brightest part of the photograph.

   SAME LINE, SECOND PHOTO. Client sent a landscape crop (the arch plate,
   `hero-mandap.webp`) the same day: now it's merging into the pale sand/foam
   at the BOTTOM of the frame instead of the sky. That's the tell that ivory
   alone was never going to hold — this line sits at the tail of
   #ch-reveal .content::before (the veil a few rules down), which is deliberately
   faded to fully transparent by the time it reaches the bottom of the block
   so it never reads as a hard-edged box. Whatever photo is behind it, the
   fade means .rw-place is the one line with no scrim help at all, so any
   near-white color will wash out against any near-white patch of photo —
   sky highlight, sand, sea-foam, doesn't matter which.

   REVISED, client screenshot, 7 Aug 2026: "the bali indonesia... looks very
   odd". The first fix worked for contrast — four 1px offset shadows with NO
   blur, a hard geometric hairline — but at this letter-spaced caps size that
   hairline doesn't anti-alias cleanly along a curve; it reads as a rough,
   slightly doubled outline rather than a clean stroke, which is exactly
   what stood out as "odd" against the rest of this lockup's soft-shadow-only
   type. Rebuilt as a soft halo instead of a hard outline: several BLURRED
   shadows at increasing radius (0 0 Xpx, no offset) hug the actual glyph
   shape the way a glow does rather than stamping four hard copies behind
   it, so it still reads as a distinct dark edge on a pale background but
   looks like the same family of shadow as .rw-day/.rw-date above it, not a
   sticker outline bolted onto one line. */
.rw-place{color:var(--ivory);text-shadow:
  0 0 2px rgba(0,0,0,.9),0 0 4px rgba(0,0,0,.7),
  0 1px 3px rgba(0,0,0,.8),0 2px 10px rgba(0,0,0,.6)}
.rw-date{display:flex;align-items:baseline;justify-content:center;
  gap:clamp(.5rem,1.8vw,.9rem);
  font-family:var(--font-display);color:var(--ivory);
  font-size:clamp(1.5rem,5.2vw,2.6rem);line-height:1;white-space:nowrap}
.rw-date .m{font-style:italic;font-size:.82em;color:#f8ecd6}
/* hairline flanks, so the date reads as an engraved line on the invitation */
.rw-rule{display:block;width:min(210px,52vw);height:1px;position:relative;
  background:linear-gradient(90deg,transparent,rgba(246,193,119,.7),transparent)}
.rw-rule::after{content:"";position:absolute;left:50%;top:50%;width:5px;height:5px;
  transform:translate(-50%,-50%) rotate(45deg);border-radius:1px;
  background:linear-gradient(135deg,#f7e2b0,#d9a355)}

.section-title{font-family:var(--font-display);font-size:clamp(2rem,6vw,3.4rem);color:var(--ivory);line-height:1.1;letter-spacing:.015em}
/* Titles "settle" into place. The settle used to be a 2600ms letter-spacing
   transition, which is a LAYOUT property: every frame re-measured and
   re-wrapped the largest text on the page, 60 times a second, on every
   chapter. Same gesture, done on the compositor: the title arrives very
   slightly wide and scales back to true, which reads as tracking easing
   closed but costs one GPU transform. */
.section-title.fx{transform:translateY(26px) scaleX(1.035);transform-origin:50% 50%;
  transition:opacity 1200ms ease,
             transform 2600ms cubic-bezier(.16,.7,.2,1)}
.in .section-title.fx{transform:none}
.poem{font-family:var(--font-display);font-style:italic;font-size:clamp(1.05rem,2.5vw,1.5rem);line-height:2;color:var(--ivory)}
.poem p{margin:1.1em 0}
.lede{font-family:var(--font-body);font-weight:300;font-size:clamp(1rem,2.2vw,1.15rem);line-height:1.9;color:var(--sand);max-width:60ch;margin:1.4rem auto 0}

.story-strip{display:flex;gap:.9rem;justify-content:center;margin-top:2rem;flex-wrap:wrap}
.story-frame{
  width:120px;aspect-ratio:3/4;border-radius:10px;overflow:hidden;flex:0 0 auto;
  border:1px solid rgba(246,193,119,.35);box-shadow:0 18px 34px -14px rgba(0,0,0,.6);
  transition:transform .3s ease,box-shadow .3s ease;
}
.story-frame:hover{transform:translateY(-4px);box-shadow:0 24px 42px -14px rgba(0,0,0,.7)}
.story-frame img{width:100%;height:100%;object-fit:cover;display:block}

/* Three of these sit on the scroll path, and the largest is ~130k px². Over a
   dark, already-graded photograph a frosted pane and a simply darker pane are
   near-indistinguishable — so the translucency stays and the live backdrop
   blur goes, traded for a little more opacity to hold the same contrast. */
.info-card{
  position:relative;display:inline-block;margin-top:2rem;padding:1.6rem 2.2rem;border-radius:16px;
  background:rgba(18,10,24,.62);
  border:1px solid rgba(246,193,119,.28);
}
/* the boarding pass's inner gold hairline, echoed on every card so all the
   site "collateral" reads as one printed set */
.info-card::before,.detail-card::before,.rsvp-card::before{content:"";position:absolute;inset:7px;
  border:1px solid rgba(246,193,119,.14);border-radius:11px;pointer-events:none}
.info-card .k{font-family:var(--font-body);text-transform:uppercase;letter-spacing:.28em;font-size:.6rem;color:var(--gold-soft)}
/* Client note, 6 Aug 2026: "keep the font same for time everywhere... it
   should be same as venue and dress code." The time value was set in
   --font-engrave (the italic display face) while Venue/Dress Code below it
   are plain --font-body — same card, two typefaces. Matched to .dress's
   family/weight/size/tracking so From/Departure/Baraat read as one line of
   type with everything under them. */
.info-card .v{font-family:var(--font-body);font-weight:300;font-size:.9rem;color:var(--ivory);letter-spacing:.02em;margin-top:.3rem}
.dress{margin-top:1.4rem;font-family:var(--font-body);font-weight:300;font-size:.9rem;color:var(--sand);letter-spacing:.02em}
.dress b{font-family:var(--font-engrave);font-weight:500;color:var(--gold-soft);letter-spacing:.14em;display:block;margin-bottom:.3rem;text-transform:uppercase;font-size:.62rem}
.pill{display:inline-block;margin:.28rem;padding:.32rem .8rem;border:1px solid rgba(246,193,119,.4);border-radius:20px;font-size:.72rem;color:var(--ivory)}

/* Chapter number badge */
/* Kicker above a chapter title. The circled chapter number that used to sit
   here is gone at the client's request — it was the last thing on the page
   still numbering the journey like a table of contents. The overline text
   stays; it names the moment without counting it.

   It is a COLUMN, not a row: "Celebration Begins · Day One" set as one line
   wrapped wherever the phone happened to run out of room, which on a 375px
   screen broke it as "CELEBRATION BEGINS · DAY / ONE". The day now always sits
   on its own second line, on every screen, by construction rather than by
   luck. */
.chap-no{display:flex;flex-direction:column;align-items:center;gap:.42rem;margin-bottom:1.4rem}
.chap-no .day{font-family:var(--font-engrave);text-transform:uppercase;
  font-size:clamp(.72rem,2.1vw,.9rem);letter-spacing:.34em;text-indent:.34em;
  color:var(--ivory);opacity:.95}

/* ============================================================
   07 · RSVP
   ============================================================ */
#ch-rsvp .content{max-width:560px;width:100%}
.rsvp-card{
  position:relative;margin-top:2rem;padding:2.2rem;border-radius:20px;text-align:left;
  background:rgba(16,9,22,.5);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
  border:1px solid rgba(246,193,119,.3);
  box-shadow:0 40px 80px -30px rgba(0,0,0,.7);
}
.rsvp-card .row{display:flex;gap:1rem;flex-wrap:wrap}
.rsvp-card .field{flex:1 1 200px;margin-bottom:1.1rem}
.rsvp-card label{display:block;font-size:.6rem;letter-spacing:.24em;text-transform:uppercase;color:var(--gold-soft);margin-bottom:.5rem}
.rsvp-card input,.rsvp-card select,.rsvp-card textarea{
  width:100%;padding:.85rem 1rem;border-radius:10px;font-family:var(--font-body);font-size:.95rem;
  background:rgba(255,250,246,.06);border:1px solid rgba(246,193,119,.28);color:var(--ivory);
  transition:border-color .25s,background .25s;
}
.rsvp-card textarea{resize:vertical;min-height:80px;line-height:1.5}
.rsvp-card input:focus,.rsvp-card select:focus,.rsvp-card textarea:focus{outline:none;border-color:var(--gold);background:rgba(255,250,246,.1)}
/* UX audit, 8 Aug 2026: the rule above removes the native outline for every
   focus (including a plain mouse click) and replaces it with only a
   border-colour/background shift — no ring, no offset. On this dark card
   that's subtle enough it may not read as "focused" at all for someone
   tabbing through with reduced vision. Kept the click-focus treatment as is
   (a visible ring on every mouse click would feel wrong here) and added a
   real ring gated to :focus-visible specifically, so keyboard navigation
   gets an unambiguous indicator without changing how the form looks on a
   tap or click. */
.rsvp-card input:focus-visible,.rsvp-card select:focus-visible,.rsvp-card textarea:focus-visible{
  box-shadow:0 0 0 2px var(--deep),0 0 0 4px var(--gold)}
.rsvp-card input::placeholder,.rsvp-card textarea::placeholder{color:rgba(242,230,219,.4)}
.rsvp-card select option{color:#2a1c33}
/* display:block + text-align/decoration reset: this was written for the one
   <button type="submit"> (block by default via width:100%'s effect on an
   inline-block UA default), then reused on <a> tags for the RSVP's hotel
   redirect and travel.html's booking buttons — width:100% does nothing on
   inline's default display, and a bare <a> also needs its underline and
   left-aligned inline text stripped explicitly. Set here once so every
   .board-btn usage is anchor-safe by default, not patched per instance. */
.board-btn{
  position:relative;overflow:hidden;display:block;text-align:center;text-decoration:none;
  width:100%;margin-top:.6rem;padding:1.05rem;border:none;border-radius:12px;cursor:pointer;
  font-family:var(--font-engrave);letter-spacing:.24em;text-transform:uppercase;font-size:.82rem;color:#2a1c33;
  background:linear-gradient(120deg,var(--gold),#f4d9a0);
  box-shadow:0 14px 30px -10px rgba(246,193,119,.6);
  transition:transform .2s ease,box-shadow .2s ease;
}
.board-btn:hover{transform:translateY(-2px);box-shadow:0 20px 40px -12px rgba(246,193,119,.75)}
.board-btn:active{transform:translateY(0) scale(.99)}
/* travelling foil sheen — same treatment as the shloka's gold */
.board-btn::after{content:"";position:absolute;top:0;bottom:0;left:-70%;width:44%;
  background:linear-gradient(100deg,transparent,rgba(255,255,255,.45),transparent);
  transform:skewX(-18deg);animation:btnSheen 4.6s ease-in-out infinite;pointer-events:none}
@keyframes btnSheen{0%,55%{left:-70%}90%,100%{left:140%}}
.confirm-msg{text-align:center;padding:1.4rem 0}
.confirm-mark{width:56px;height:56px;margin:0 auto 1.2rem;color:var(--gold-soft)}
.confirm-mark svg{width:100%;height:100%}
/* the couple's full monogram lockup — the emotional payoff on "Boarding Confirmed" */
.confirm-logo{display:block;width:132px;height:auto;margin:.2rem auto 1.4rem;
  filter:drop-shadow(0 6px 16px rgba(0,0,0,.45))}
.confirm-msg .big{font-family:var(--font-display);font-size:clamp(1.6rem,5vw,2.4rem);color:var(--gold-soft)}
.confirm-msg .sub{font-family:var(--font-body);font-weight:300;color:var(--sand);margin-top:.8rem}
.confirm-stub{
  margin-top:1.6rem;padding-top:1.1rem;border-top:1px dashed rgba(246,193,119,.3);
  display:flex;justify-content:center;gap:.6rem;flex-wrap:wrap;
  font-family:var(--font-engrave);font-size:.62rem;letter-spacing:.22em;text-transform:uppercase;color:var(--gold-soft);opacity:.75;
}
/* THE HOTEL PUSH. Client note, 6 Aug 2026: rooms should feel genuinely
   limited, not like one more optional link buried in a thank-you screen —
   so this gets its own warm-tinted box instead of just another paragraph,
   the one visual break from the confirmation card's otherwise quiet gold-
   on-dark palette. Still no blur/shadow additions beyond what .board-btn
   already carries (see animation-perf-rules) — the coral tint alone is
   what reads as "urgent" here, not new expensive effects. */
.hotel-urgency{
  margin-top:1.8rem;padding:1.4rem 1.3rem;border-radius:14px;text-align:center;
  background:linear-gradient(160deg,rgba(240,122,89,.14),rgba(246,193,119,.08));
  border:1px solid rgba(240,122,89,.4);
}
.hu-tag{
  display:inline-block;font-family:var(--font-body);font-weight:600;font-size:.62rem;
  letter-spacing:.22em;text-transform:uppercase;color:var(--coral);
  padding:.3rem .8rem;border-radius:20px;border:1px solid rgba(240,122,89,.5);
  margin-bottom:.9rem;
}
.hu-copy{font-family:var(--font-body);font-weight:300;font-size:.88rem;color:var(--sand);line-height:1.5}
.hu-copy b{color:var(--gold-soft);font-weight:600}
.hu-btn{margin-top:1.1rem}

/* travel.html's own two-button choice — see the note in the markup for why
   this replaced a single auto-picked link. Stacked full-width by default
   (.board-btn is already width:100%), which is also just the right call for
   the mostly-phone audience this whole site is built for. */
.hotel-book-lede{margin-top:1.4rem;font-family:var(--font-body);font-weight:300;font-size:.9rem;color:var(--gold-soft)}
.hotel-book-ctas{margin-top:1rem;display:flex;flex-direction:column;gap:.9rem}
.hotel-book-ctas .board-btn{margin-top:0}
/* The nights each button actually covers (client note, 8 Aug 2026). Its own
   line inside the button, and deliberately NOT the parent's engraved caps:
   .board-btn is uppercase at .24em tracking, which is right for a short
   label and unreadable for a date range. Body face, normal case, no
   tracking, and dimmed so it reads as a subtitle to the label above rather
   than competing with it. */
.btn-dates{display:block;margin-top:.35rem;
  font-family:var(--font-body);font-weight:400;font-size:.66rem;
  letter-spacing:.04em;text-transform:none;opacity:.72}
/* !important so it beats higher-specificity display rules (.rsvp-card .row) —
   entitlement sections (plus-one, children, travel) must NEVER leak visible */
.hidden{display:none!important}

/* -- guest search (Step 1) -- */
.rsvp-card .field{position:relative}
.optional{text-transform:none;letter-spacing:0;color:var(--sand);opacity:.6;font-size:.7rem}
.guest-suggestions{
  margin-top:.5rem;border-radius:10px;overflow:hidden;
  border:1px solid rgba(246,193,119,.28);background:rgba(28,17,34,.96);
}
.guest-option{
  display:block;width:100%;text-align:left;padding:.7rem 1rem;border:none;background:transparent;
  color:var(--ivory);font-family:var(--font-body);font-size:.92rem;cursor:pointer;
  border-bottom:1px solid rgba(246,193,119,.12);transition:background .18s ease;
}
.guest-option:last-child{border-bottom:none}
.guest-option:hover{background:rgba(246,193,119,.14)}
.not-found{
  margin-top:1rem;font-family:var(--font-body);font-weight:300;font-size:.85rem;
  color:var(--sand);line-height:1.6;
}
.not-found a{color:var(--gold-soft);text-decoration:underline}

/* -- section dividers within the long-form RSVP -- */
.section-label{
  margin:1.6rem 0 .9rem;padding-top:1.2rem;border-top:1px solid rgba(246,193,119,.16);
  font-family:var(--font-engrave);font-size:.64rem;letter-spacing:.28em;text-transform:uppercase;
  color:var(--gold-soft);
}
.rsvp-card > .section-label:first-of-type{margin-top:0;padding-top:0;border-top:none}

/* -- the RSVP deadline note --
   Rendered as a lit gold chip so the date is unmissable.
   (Was the party-entitlement note, "This invitation admits you and a
   plus-one", until that was cut on 8 Aug 2026; the box was kept and given
   the deadline instead.)

   The top margin is load-bearing, not taste. This box used to sit inside the
   form directly under a `.section-label`, which supplied its own spacing, so
   `margin-top:0` was fine there. In its new home it follows `.lede` — which
   has `margin-bottom:0` — so zero-on-zero put the chip's top border flush
   against the last line of the paragraph and the border ran straight through
   the words. Client, 9 Aug 2026: "isko thoda neeche kar sakte… ke visible
   nahi hai aisa na bole vo" (move it down a bit, so she doesn't say it isn't
   visible). Measured 0px before, ~26px after. If this chip is ever moved
   again, check what precedes it: several blocks on this site deliberately
   carry no bottom margin. */
.party-note{display:block;margin:1.6rem 0 1.1rem;padding:.75rem 1.1rem;border-radius:12px;
  font-family:var(--font-body);font-weight:300;font-size:.86rem;line-height:1.6;color:var(--ivory);
  background:linear-gradient(120deg,rgba(246,193,119,.16),rgba(246,193,119,.06));
  border:1px solid rgba(246,193,119,.45);
  box-shadow:0 0 0 1px rgba(246,193,119,.08),0 10px 22px -14px rgba(246,193,119,.5)}
.party-note::before{content:"✦";color:var(--gold);margin-right:.55rem}
.party-note b{color:var(--gold);font-weight:600}

/* -- guest confirm bar (Step 2 header) -- */
.guest-confirm{
  display:flex;align-items:center;justify-content:space-between;gap:1rem;
  margin-bottom:1.4rem;padding-bottom:1.1rem;border-bottom:1px solid rgba(246,193,119,.2);
  font-family:var(--font-body);font-size:.92rem;color:var(--sand);
}
.guest-confirm strong{font-family:var(--font-engrave);color:var(--ivory);font-weight:500}
.change-guest{
  background:none;border:none;color:var(--gold-soft);font-family:var(--font-body);font-size:.76rem;
  text-decoration:underline;cursor:pointer;flex:0 0 auto;
}

/* -- day-one / day-two attendance pills -- */
.pill-toggle{display:flex;gap:.7rem;border-radius:10px;transition:box-shadow .2s ease}
.pill-opt{
  flex:1;padding:.75rem 1rem;border-radius:10px;cursor:pointer;
  font-family:var(--font-body);font-size:.85rem;color:var(--ivory);text-align:center;
  background:rgba(255,250,246,.06);border:1px solid rgba(246,193,119,.28);
  transition:background .2s ease,border-color .2s ease,transform .15s ease;
}
.pill-opt:hover{border-color:var(--gold-soft)}
.pill-opt.active{background:linear-gradient(120deg,var(--gold),#f4d9a0);color:#2a1c33;border-color:transparent;font-weight:500}
.pill-toggle.needs-answer{box-shadow:0 0 0 2px rgba(240,122,89,.7);border-radius:12px}

/* -- meal preference chips -- */
.meal-checks{display:flex;gap:.8rem;flex-wrap:wrap}
.meal-chip{
  display:flex;align-items:center;gap:.55rem;padding:.65rem 1.1rem;border-radius:30px;cursor:pointer;
  font-family:var(--font-body);font-size:.85rem;color:var(--ivory);text-transform:none;letter-spacing:0;
  background:rgba(255,250,246,.06);border:1px solid rgba(246,193,119,.28);transition:border-color .2s ease;
}
.meal-chip:has(input:checked){border-color:var(--gold);background:rgba(246,193,119,.14)}
.meal-chip input{width:auto;accent-color:var(--gold)}

/* ============================================================
   CHROME — music toggle, scroll cue, progress
   ============================================================ */
.music-toggle{
  width:44px;height:44px;border-radius:50%;cursor:pointer;flex:0 0 auto;
  display:flex;align-items:center;justify-content:center;
  /* NO backdrop-filter. On the client's Android phone (Brave, 5 Aug 2026) this
     button repeatedly rendered as an opaque grey SQUARE: the blurred backdrop
     was being composited into an unclipped rectangle that ignored the 50%
     border-radius. It is a known Chromium/GPU failure and it is not worth
     risking on the most visible chrome on the site for a blur nobody can see
     behind a 44px disc. The background is simply more opaque instead. */
  background:rgba(20,12,25,.62);
  border:1px solid rgba(246,193,119,.4);color:var(--gold-soft);font-size:.9rem;
  transition:transform .25s,opacity .8s,border-color .3s;opacity:0;pointer-events:none;
}
.music-toggle.show{opacity:1;pointer-events:auto}
.music-toggle:hover{transform:scale(1.08)}
.music-toggle:active{transform:scale(.92)}
.music-toggle .bars{display:flex;gap:2px;align-items:flex-end;height:14px}
.music-toggle .bars i{width:2px;background:var(--gold-soft);height:6px;animation:eq 1s ease-in-out infinite}
.music-toggle .bars i:nth-child(2){animation-delay:.2s}
.music-toggle .bars i:nth-child(3){animation-delay:.4s}
.music-toggle.muted .bars i{animation-play-state:paused;height:4px}
@keyframes eq{0%,100%{height:4px}50%{height:14px}}

/* A BUTTON, and it has to LOOK like one. With the auto-advance gone this is the
   only thing telling a guest there is more below, and as a hairline of gold on
   a bright beach it was invisible — client note, 5 Aug 2026: "the scroll is not
   very prominent on the first page". It is now the same smoked-glass pill as
   the rest of the site's chrome (nav, back button), so it reads as a control
   rather than as decoration, at a size a thumb can find. */
.scroll-cue{
  position:fixed;left:50%;bottom:max(24px,env(safe-area-inset-bottom));transform:translateX(-50%);z-index:1100;
  -webkit-appearance:none;appearance:none;cursor:pointer;
  display:flex;flex-direction:column;align-items:center;gap:2px;
  padding:10px 22px 8px;border-radius:26px;
  border:1px solid rgba(246,193,119,.5);
  /* deliberately NOT backdrop-filter — see the .music-toggle note; this pill is
     rounded chrome over photography, exactly the case that failed */
  background:rgba(12,7,16,.66);
  box-shadow:0 6px 22px -10px rgba(0,0,0,.8);
  font-family:var(--font-body);font-size:.62rem;letter-spacing:.3em;text-transform:uppercase;
  color:#f7dfae;
  opacity:0;transition:opacity .8s ease,transform .3s ease,background .3s ease;
  /* Invisible for most of the journey and fixed over the whole page, so it must
     not swallow taps meant for what is behind it — it only becomes hittable
     once it is actually on screen. */
  pointer-events:none;
}
.scroll-cue.show{opacity:1;pointer-events:auto}
.scroll-cue.show:hover{background:rgba(12,7,16,.68);transform:translateX(-50%) translateY(-2px)}
.scroll-cue:active{transform:translateX(-50%) translateY(0) scale(.97)}
.scroll-cue:focus-visible{outline:2px solid var(--gold-soft);outline-offset:4px}
.scroll-cue .lbl{line-height:1;padding-left:.3em}   /* .3em offsets the tracking on the last letter */
/* the chevron is the part that actually says "downwards", so it is the part
   that moves — transform only, and it stops for reduced motion below */
.scroll-cue .chev{width:18px;height:18px;margin-bottom:-4px;animation:cueNudge 1.9s ease-in-out infinite}
@keyframes cueNudge{0%,100%{transform:translateY(-2px);opacity:.75}50%{transform:translateY(2px);opacity:1}}

/* ============================================================
   STAR JOURNEY — the scroll progress marker
   Two stars leave opposite edges of the sky and travel toward each other as
   the guest moves through the story, meeting dead centre when the RSVP
   arrives. Their trails are the progress bar.
   ============================================================ */
.star-track{position:fixed;top:0;left:0;right:0;height:34px;z-index:1200;pointer-events:none;
  opacity:0;transition:opacity 1s ease;
  /* a faint scrim under the band so the stars keep their contrast no matter
     what photo is behind them — the journey should never be hunted for */
  background:linear-gradient(180deg,rgba(12,7,16,.42),rgba(12,7,16,0))}
.star-track.show{opacity:1}
.st-line{position:absolute;top:10px;left:0;right:0;height:3px;border-radius:3px;
  background:linear-gradient(90deg,transparent,rgba(246,193,119,.34) 10%,rgba(246,193,119,.34) 90%,transparent);
  transition:background 1.2s ease}
/* The trails are drawn at full length and scaled, rather than grown by
   setting `width` on every scroll frame. Width is a layout property: the old
   version relaid out and repainted the fixed header band continuously the
   entire way down the page, which is a cost paid on the one element that is
   on screen 100% of the time. scaleX is a compositor transform.
   The gradient and its stops are relative to the element box, so squashing it
   keeps exactly the same colour ramp it had when it was drawn short. */
.st-trail{position:absolute;top:10px;height:3px;width:50%;border-radius:3px;
  transform:scaleX(0);
  box-shadow:0 0 14px rgba(246,193,119,.85),0 0 4px rgba(255,240,205,.9)}
.st-trail.l{left:0;transform-origin:left center;
  background:linear-gradient(90deg,rgba(240,122,89,0),var(--coral) 45%,var(--gold))}
.st-trail.r{right:0;transform-origin:right center;
  background:linear-gradient(270deg,rgba(240,122,89,0),var(--coral) 45%,var(--gold))}

/* Likewise the stars ride on a transform instead of `left`/`right`. The
   -50%/+50% that used to centre them is folded into the transform JS writes. */
.star{position:absolute;top:1.5px;width:20px;height:20px}
.star.u{left:0;transform:translateX(-50%)}
.star.d{right:0;transform:translateX(50%)}
/* soft aura so the travelling star catches the eye in peripheral vision */
.star::before{content:"";position:absolute;left:50%;top:50%;width:34px;height:34px;
  margin:-17px 0 0 -17px;border-radius:50%;
  background:radial-gradient(circle,rgba(255,240,205,.5),rgba(246,193,119,.22) 42%,transparent 70%);
  animation:starAura 3.4s ease-in-out infinite}
.star.d::before{animation-delay:-1.7s}
@keyframes starAura{0%,100%{transform:scale(.8);opacity:.55}50%{transform:scale(1.15);opacity:1}}
.star i{position:relative;display:block;width:100%;height:100%;
  background:radial-gradient(circle,#fffdf6,#ffe6b0 40%,#f6c177 62%,transparent 76%);
  clip-path:polygon(50% 0%,61% 39%,100% 50%,61% 61%,50% 100%,39% 61%,0% 50%,39% 39%);
  filter:drop-shadow(0 0 8px rgba(255,231,178,1)) drop-shadow(0 0 16px rgba(246,193,119,.8));
  animation:starPulse 3.4s ease-in-out infinite}
.star.d i{animation-delay:-1.7s}
@keyframes starPulse{0%,100%{transform:scale(.9) rotate(0deg);opacity:.95}
  50%{transform:scale(1.15) rotate(45deg);opacity:1}}

/* the meeting */
.st-halo{position:absolute;left:50%;top:11.5px;width:10px;height:10px;margin:-5px 0 0 -5px;
  border-radius:50%;opacity:0;
  background:radial-gradient(circle,rgba(255,246,220,.95),rgba(246,193,119,.45) 45%,transparent 70%)}
.star-track.met .st-halo{animation:haloOut 1.6s ease-out forwards}
@keyframes haloOut{0%{opacity:.95;transform:scale(1)}70%{opacity:.5}100%{opacity:0;transform:scale(9)}}
.star-track.met .st-line{background:linear-gradient(90deg,transparent,rgba(246,193,119,.5) 12%,rgba(246,193,119,.5) 88%,transparent)}
.star-track.met .star i{animation:starMerge 1.3s cubic-bezier(.2,.8,.2,1) forwards}
@keyframes starMerge{0%{transform:scale(1) rotate(0deg)}
  42%{transform:scale(2.6) rotate(120deg);filter:drop-shadow(0 0 16px rgba(255,240,200,1))}
  100%{transform:scale(1.5) rotate(180deg);filter:drop-shadow(0 0 9px rgba(246,193,119,.95))}}
.st-burst{position:absolute;left:50%;top:12px;width:0;height:0;overflow:visible}

/* arrival chip — brief, engraved, then gone */
.arrival-note{position:fixed;top:52px;left:50%;transform:translateX(-50%) translateY(-8px);
  z-index:1250;pointer-events:none;opacity:0;
  padding:.5rem 1.2rem;border-radius:30px;white-space:nowrap;
  font-family:var(--font-engrave);font-size:.62rem;letter-spacing:.26em;text-transform:uppercase;
  color:#f8dfae;background:rgba(12,7,16,.62);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
  border:1px solid rgba(246,193,119,.45);box-shadow:0 10px 26px -12px rgba(0,0,0,.7);
  transition:opacity .9s ease,transform .9s cubic-bezier(.2,.8,.2,1)}
.arrival-note.show{opacity:1;transform:translateX(-50%) translateY(0)}
.arrival-note::before,.arrival-note::after{content:"✦";color:var(--gold);margin:0 .5em;font-size:.9em}

/* ============================================================
   RESPONSIVE + MOTION
   ============================================================ */
@media(max-width:640px){
  .boarding-pass{--pad-x:22px;padding:22px}
  .bp-logo{width:80px}
  .yacht{width:170px}
  .mandap{width:250px}
  .palm{width:100px}
  .rsvp-card{padding:1.5rem}
}


/* short screens: shrink the pass's tall elements so it never clips */
@media(max-height:700px){
  .bp-logo{width:72px;margin-top:8px}
  .bp-names{font-size:2.15rem}
  .bp-rule{margin:9px auto 8px}
  .bp-cta{font-size:.8rem}
  .bp-grid{gap:9px 10px;margin:13px 4px 6px}
  /* label and value shrink together — they must stay the same size */
  .bp-grid .lab,.bp-grid .val{font-size:.68rem}
  /* the stub tightens too: only the gaps around the tear line change, and the
     notches and seal follow it because they hang off it. */
  .boarding-pass .perf{margin-top:17px;margin-bottom:14px}
  .bp-seal{width:38px;height:38px}
  .bp-barcode{height:26px;margin-top:10px}
  .boarding-pass{--pad-x:22px;padding:20px var(--pad-x) 24px}
  .bp-cta .pulse{padding:11px 24px}
}
/* ------------------------------------------------------------------
   OFFSCREEN CHAPTERS DON'T ANIMATE.
   Paired with the `.live` observer in script.js §4b. Every decorative
   animation in a chapter that is more than half a viewport away is parked,
   which takes the site from ~330 permanently-running animations down to only
   the ones on screen. `animation-play-state` freezes a keyframe animation in
   place with no reflow and no restart — it resumes exactly where it left off,
   so scrolling back finds the sea and the embers mid-stride rather than
   snapping to a fresh cycle.

   Scoped under body.io-live so the rules cannot apply unless JS is alive to
   turn them back off again. TRANSITIONS are untouched: the .fx reveals and the
   mist clearing are transitions, not animations, so they still fire normally
   the moment a chapter arrives.
   ------------------------------------------------------------------ */
.io-live .chapter:not(.live) .photo::before,
.io-live .chapter:not(.live) .photo::after,
.io-live .chapter:not(.live) .surf i,
.io-live .chapter:not(.live) .surf i::before,
.io-live .chapter:not(.live) .goldfoil .foil,
.io-live .chapter:not(.live) .goldfoil .foil::before,
/* the glint MUST park alongside .photo::after above — they run the same
   `breathe` to stay in register, so pausing one without the other would slide
   the ink off its mask */
.io-live .chapter:not(.live) .goldglint,
.io-live .chapter:not(.live) .goldglint::before,
.io-live .chapter:not(.live) .mist i,
.io-live .chapter:not(.live) .glow,
.io-live .chapter:not(.live) .fire,
.io-live .chapter:not(.live) .ember,
.io-live .chapter:not(.live) .petal,
.io-live .chapter:not(.live) .star-mote,
.io-live .chapter:not(.live) .kite,
.io-live .chapter:not(.live) .string i,
.io-live .chapter:not(.live) .waterglint::before,
.io-live .chapter:not(.live) .rays,
.io-live .chapter:not(.live) .seaglint .spec,
.io-live .chapter:not(.live) .orb,
.io-live .chapter:not(.live) .sand-grain{animation-play-state:paused}

/* ============================================================
   LIVING PHOTO SCENES  (parallax · ken-burns · rock · overlays)
   ============================================================ */
.par{position:absolute;inset:0;overflow:hidden;z-index:0}
.plx{position:absolute;inset:-12%;transform:translateY(var(--pary,0))}
.dolly{position:absolute;inset:0;transform:scale(var(--ys,1))}
/* PROMOTE ONLY WHAT IS ON SCREEN.
   These two used to carry `will-change:transform` unconditionally, which is the
   very mistake the note under .photo below describes — except worse, because
   .plx is inset:-12%, i.e. 124% of the viewport, and it is the PARENT, so
   pinning it pins the blurred bed, the contained photograph, the gold foil and
   the masked ink underneath it as one surface. Seven of those held in GPU
   memory permanently, offscreen ones included, is more texture than a phone or
   an integrated laptop GPU has to give; when it runs out it evicts tiles, and
   evicted tiles that do not get repainted in time are exactly the hard-edged
   bands of blank sky the client photographed mid-scroll on 5 Aug 2026.

   Scoping the hint to `.live` keeps the parallax on the compositor for the one
   to three chapters actually in view and lets every other chapter's layer be
   reclaimed. `.live` is set half a viewport early, so promotion always happens
   well before the chapter can be seen. Gated on `io-live` like the rest of the
   parking rules: if the observer never runs, nothing is promoted and the
   parallax simply goes unhinted rather than the page freezing. */
.io-live .live .plx,
.io-live .live .dolly{will-change:transform}
/* Each hero is a full, art-directed frame — gold corner motifs, sand-line
   border details, nothing meant to be cropped. A plain background-size:cover
   was zooming past those edges, so the whole picture never actually showed.
   Fix: split into two layers. ::before is a soft blurred duplicate that
   floats to every edge (it can crop/zoom freely — it's atmosphere, not the
   photo). ::after is the real photograph at background-size:contain, always
   whole, centered.

   PERFORMANCE: ::after used to also carry
   `drop-shadow(0 0 46px …) drop-shadow(0 26px 54px …)` for a gold-lit frame.
   drop-shadow has to read the alpha of the whole painted surface and run a
   gaussian over it — here that is a viewport-sized photograph, twice, every
   time the layer rasterises, which is every time a chapter comes into view.
   Measured over a full-page scroll: p95 frame 1150ms and stalls up to 1650ms,
   versus 83ms with the shadows gone. It is not the ken-burns animation —
   freezing that and keeping the shadows was still 2267ms. Nor can a
   box-shadow stand in: ::after's box is exactly .par's box, so any outer
   shadow is clipped away entirely — it costs and draws nothing.
   The blurred ::before behind it already carries the edge. */
.photo{position:absolute;inset:0;transform-origin:center}
/* No `will-change:transform` here. A running transform animation already gets
   this promoted to its own layer, so the hint bought nothing — but it pinned
   all seven of these (each a 42px-blurred, oversized copy of a hero
   photograph) in GPU memory permanently, including the ones parked offscreen,
   which is exactly the memory pressure that makes mid-range phones stutter.
   Dropping it lets a paused chapter's layer be reclaimed. */
.photo::before{content:"";position:absolute;inset:-6%;
  background-image:var(--photo);background-size:cover;background-position:center;
  filter:blur(42px) saturate(1.25) brightness(.62);
  transform:scale(1.18);
  animation:kb 19s ease-in-out infinite alternate}
/* .photo is always the child of a .plx wrapper (inset:-12%, i.e. 124% of the
   viewport box) — that oversize exists so the blurred ::before has room to
   pan without ever showing an edge. The real photo must NOT inherit that
   oversize (it would get clipped by .par's overflow:hidden), so ::after
   insets back in by 12/124 = 9.677%, landing it exactly on the true viewport
   box the same size .par itself is. Page-banners have no .plx ancestor, so
   they cancel this back to inset:0 below. */
.photo::after{content:"";position:absolute;inset:9.677%;
  background-image:var(--photo);background-size:contain;background-repeat:no-repeat;background-position:center;
  filter:saturate(1.03);
  animation:breathe 19s ease-in-out infinite alternate}
.photo.pan::before{animation:kbpan 22s ease-in-out infinite alternate}
@keyframes kb{0%{transform:scale(1.06) translate3d(1.2%,.8%,0)}100%{transform:scale(1.24) translate3d(-2.2%,-1.8%,0)}}
@keyframes kbpan{0%{transform:scale(1.1) translate3d(-2.6%,.6%,0)}100%{transform:scale(1.17) translate3d(2.6%,-1.3%,0)}}
@keyframes breathe{0%{transform:scale(1)}100%{transform:scale(1.012)}}

/* ------------------------------------------------------------------
   GOLD FOIL — the painted motifs, lifted off the artwork.
   The gold clouds used to be part of the photograph, so nothing about them
   could move. They have been separated into their own transparent layers and
   the sky rebuilt underneath, which means they can now arrive, drift and
   catch light independently of the picture they came from.

   REGISTRATION is the whole trick. Each sprite is a FULL-SIZE canvas with one
   cloud painted at its exact original coordinates and everything else
   transparent — so the layer only has to repeat the photograph's own sizing
   rules (inset, background-size, background-position) to land pixel-perfect.
   Percent-positioning a cropped sprite would drift the moment the photo
   switched between `contain` on desktop and `cover` on a phone.
   ------------------------------------------------------------------ */
.goldfoil{position:absolute;inset:0;z-index:2;pointer-events:none}
/* FAIL-SAFE FIRST. The gold is no longer painted into the photograph, so if
   this layer never showed, the picture would simply be missing its clouds.
   Default is therefore fully visible — identical to the old flat artwork. Only
   once `body.io-live` proves the observer is alive does the layer hand itself
   over to the animation and start from nothing. */
.foil{position:absolute;inset:9.677%;                 /* matches .photo::after */
  background-size:contain;background-repeat:no-repeat;background-position:center;
  opacity:1}
.io-live .foil{opacity:0}
/* `--self` is the element's own artwork, and it lives on the .foil rather than
   on one pseudo — both the glow and the sheen mask themselves to the shape of
   the ink, so they need to inherit the same value. */
.foil.l{background-image:var(--foil-l);--self:var(--foil-l)}
.foil.r{background-image:var(--foil-r);--self:var(--foil-r)}

/* ARRIVAL — the clouds fade up where they belong and then STAY THERE.
   Client note, 3 Aug 2026: "those clouds doesn't need to move". They used to
   drift in from outside their resting position and then breathe on a 13s/16s
   alternate forever; both the entrance travel and the endless drift are gone,
   and only the opacity handover remains. That handover still has to be an
   animation rather than a plain rule, because `.io-live .foil{opacity:0}`
   above is the fail-safe — without something to bring it back up, a live
   observer would leave the picture with no clouds at all.
   Still keyed off `.live`, so it replays each time the guest returns. */
.io-live .live .foil.l{animation:foilFade 1500ms ease 300ms both}
.io-live .live .foil.r{animation:foilFade 1500ms ease 800ms both}
@keyframes foilFade{from{opacity:0}to{opacity:1}}
/* The foilDriftL/R keyframes that used to live here — a 40px to-and-fro sweep
   with a degree of tilt — are gone with the drift itself. */

/* Both light layers mask themselves to the artwork, so they can only ever lift
   the gold and never the sky behind it. */
.foil::before,.foil::after{content:"";position:absolute;inset:0;opacity:0;
  mix-blend-mode:screen;
  -webkit-mask-image:var(--self);mask-image:var(--self);
  -webkit-mask-size:contain;mask-size:contain;
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-position:center;mask-position:center}

/* GLOW — the bed the gold sits on. Real gold leaf is never uniformly lit; it
   holds warm and then swells as the light moves over it. Pushed hard from the
   original .05–.32: the brief came back as "bling, show it off", and a pulse
   you have to look for is not what anyone is buying. It now breathes between
   a visible warm hold and a genuine flare, on a six-second cycle so the metal
   is always doing something. */
.foil::before{background:radial-gradient(66% 66% at 50% 42%,rgba(255,232,166,.95),
    rgba(255,206,116,.4) 62%,transparent 78%)}
.io-live .live .foil.l::before{animation:foilGlow 6.2s ease-in-out 1.1s infinite}
.io-live .live .foil.r::before{animation:foilGlow 7.6s ease-in-out 2.4s infinite}
@keyframes foilGlow{0%,100%{opacity:.16}50%{opacity:.62}}

/* SHEEN — REMOVED on the same note. It was a hot band raking across the foil
   every 4.2s; of everything on the clouds it was the part that read as an
   animation rather than as metal. The glow above is what carries the "bling"
   the earlier brief asked for, and it pulses in place rather than travelling.
   `.foil::after` keeps its `opacity:0` from the shared rule above, so the
   layer is declared but never painted. */

/* On a phone the photograph switches to the portrait cut at cover, so the foil
   has to switch with it — same art, same crop, same rules. */
@media(max-width:900px) and (orientation:portrait){
  .foil{inset:0;background-size:cover}
  .foil.l{background-image:var(--foil-ml,var(--foil-l));--self:var(--foil-ml,var(--foil-l))}
  .foil.r{background-image:var(--foil-mr,var(--foil-r));--self:var(--foil-mr,var(--foil-r))}
  .foil::before,.foil::after{-webkit-mask-size:cover;mask-size:cover}
}

/* ------------------------------------------------------------------
   GOLD GLINT — the motifs that could NOT be lifted.
   Only the opening chapter got the full treatment above: its sky was
   regenerated clean so the clouds could be pulled off it and moved. Every
   other hero still has its gold PAINTED IN — the cloud scrolls and birds, the
   shells, starfish, coconuts and flower line-work on the sand, the corner
   flourishes. Erasing those would leave holes with nothing behind, so they
   cannot drift.

   They can still catch light. build_gold_masks.py writes one alpha mask per
   hero holding just the ink (see that file for how it separates gold
   line-work from cream flowers and from the gold band on the horizon), and
   this layer runs the same two lights through it as the foil: a slow glow
   that keeps the ornament warm, and a sheen that travels across every few
   seconds. The photograph underneath is untouched — all that changes is where
   the light is.

   REGISTRATION works exactly as the foil's does: each mask is a full-size
   canvas, so repeating the photograph's own inset and sizing lands it
   pixel-perfect. The one addition is `breathe`. `.photo::after` is very
   slightly zooming at all times, and at 1672px wide even a 1.2% zoom slides
   the ink some 20px away from a static mask. Running the identical animation
   here keeps the two locked together — which is also why this element has to
   park and unpark with the photo (see the pause list), or the two would drift
   apart the moment a chapter went offscreen mid-cycle.
   ------------------------------------------------------------------ */
/* The mask and the blend both live on the PARENT, not on each light. Putting
   them on the two pseudos instead — which is how the foil above does it,
   because there each cloud is a separate sprite — meant two mask surfaces and
   two blend surfaces per chapter, twelve of each across the page, all of them
   compositing offscreen buffers at whatever the device pixel ratio is. Hoisted
   here it is one of each, the two lights composite together first and screen
   onto the photograph as a group, and the mask still rides `breathe` because
   it is on the element the animation is on. */
.goldglint{position:absolute;inset:9.677%;       /* matches .photo::after */
  z-index:2;pointer-events:none;mix-blend-mode:screen;
  -webkit-mask-image:var(--ink);mask-image:var(--ink);
  -webkit-mask-size:contain;mask-size:contain;
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-position:center;mask-position:center;
  animation:breathe 19s ease-in-out infinite alternate}
.goldglint::before,.goldglint::after{content:"";position:absolute;inset:0;opacity:0}
/* the warm hold — same lift as the foil's, so ornament that is painted in and
   ornament that floats free read as the same material */
.goldglint::before{background:radial-gradient(84% 84% at 50% 45%,
    rgba(255,232,166,.95),rgba(255,208,120,.42) 66%,transparent 100%)}
.io-live .live .goldglint::before{animation:foilGlow 7.4s ease-in-out 600ms infinite}
/* The travelling light is REMOVED, exactly as the foil's sheen was. This ink
   is the same cloud scrolls and birds as the foil, just painted into the plate
   instead of lifted off it, so leaving it shimmering here would have made the
   yacht's clouds hold still while every other hero's kept moving. The glow
   above stays. `.goldglint::after` keeps `opacity:0` and is never painted. */

/* THE YACHT IS A SPECIAL CASE. Its landscape plate was repainted completely
   clean — every motif lifted into the sprites — so there is no ink there to
   light and no mask is generated for it. Its PORTRAIT cut was not: the bird,
   the flowers in the shallows and the shell line along the sand are still
   painted in. So the opening chapter and the Travel banner need a glint on a
   phone and none at all on a desktop.
   This must be display:none rather than an empty mask, and that is not
   fussiness: `mask-image:var(--ink)` with no --ink defined is an invalid
   value, which resolves to NO mask — and an unmasked glow layer would flare
   across the entire photograph. */
.goldglint.portrait-only{display:none}

/* Phones take the portrait cut at cover — same switch the photograph makes. */
@media(max-width:900px) and (orientation:portrait){
  .goldglint{inset:0;
    -webkit-mask-image:var(--ink-m,var(--ink));mask-image:var(--ink-m,var(--ink));
    -webkit-mask-size:cover;mask-size:cover}
  .goldglint.portrait-only{display:block}
}

/* ------------------------------------------------------------------
   OCEAN SWELL — the sea moves, the photograph does not.
   What used to live here was `.rock`: a rotate + translate applied to the
   WHOLE hero, so every scene with water in it swung the horizon, the mandap,
   the sky and the type together. At any amplitude big enough to notice it
   reads as camera shake, which is precisely the note we got back.

   The fix is to put the motion where the motion actually is. Three soft
   bands of light sit on the waterline and travel at different speeds and
   opacities, so the surface crawls and glitters while the frame stays
   locked. Masked to fade out well before the horizon, screen-blended so it
   only ever adds light, never a visible edge.
   ------------------------------------------------------------------ */
.surf{position:absolute;left:0;right:0;bottom:0;height:52%;z-index:1;
  pointer-events:none;overflow:hidden;mix-blend-mode:screen;
  -webkit-mask-image:linear-gradient(180deg,transparent 0,#000 34%,#000 100%);
  mask-image:linear-gradient(180deg,transparent 0,#000 34%,#000 100%)}

/* WHY THE BANDS ARE BUILT THIS WAY.
   The first version ran the crest lines at 97deg, which is four degrees off
   VERTICAL — so the sea was covered in upright columns sliding sideways. On a
   locked frame that reads as a curtain drawn across the picture, not as water.
   Two things fix it:

   1. The lines run near-HORIZONTAL (a few degrees off 180) and travel DOWN the
      frame, because that is the direction a swell actually approaches a
      stationary camera.
   2. Each band is one DEPTH SLICE of the water rather than a full-height
      sheet. The far slice gets fine lines moving slowly, the near slice coarse
      lines moving quickly. That difference in scale and speed between slices
      is the entire illusion of perspective — evenly-spaced lines at one speed
      are what read as scrolling wallpaper no matter which way they point.

   The stripes sit on ::before so the slice itself can hold a soft-edged mask
   (no band ever starts or stops on a visible line) while the thing inside it
   is what moves. Each ::before is oversized well past its slice and travels
   exactly one stripe period, so the loop has no seam. */
.surf i{position:absolute;left:0;right:0;overflow:hidden;
  -webkit-mask-image:linear-gradient(180deg,transparent 0,#000 24%,#000 76%,transparent 100%);
  mask-image:linear-gradient(180deg,transparent 0,#000 24%,#000 76%,transparent 100%)}
.surf i::before{content:"";position:absolute;left:-8%;right:-8%;top:-140%;bottom:-140%;
  animation:swell linear infinite}
/* far water — fine, slow, sitting just under the horizon */
/* CREST STRENGTH. These alphas were .085/.08/.16 — arithmetically present and
   visually nothing, which is the "the water doesn't look like it's moving"
   note. The geometry below was always right; it was simply being drawn far too
   faint to read as water at all.

   The weighting is deliberately NOT uniform. `mix-blend-mode:screen` on the
   parent only ever lightens, so a stripe over the bright sky near the horizon
   adds almost nothing while the same stripe over darker near water reads
   strongly — which is exactly the behaviour we want, and why the near slices
   are pushed hardest and the horizon slice is actually pulled back. The
   alternative tried here was a crest+trough gradient composited normally: it
   defines the water better, but the dark half also lands on the sunset above
   the horizon and stripes the sky. Screen is what keeps the effect confined
   to the water without needing a second mask.

   All of this is free — alpha changes the colour of a gradient that is
   already being painted; it adds no layer, no blur and no frame of work. */
.surf i:nth-child(1){top:0;height:40%;opacity:.9}
.surf i:nth-child(1)::before{--p:9px;animation-duration:13s;
  background:repeating-linear-gradient(184deg,
    transparent 0 3px,rgba(255,255,255,.17) 5px,transparent 7px 9px)}
/* mid water */
.surf i:nth-child(2){top:28%;height:44%;opacity:.85}
.surf i:nth-child(2)::before{--p:16px;animation-duration:9s;
  background:repeating-linear-gradient(177deg,
    transparent 0 5px,rgba(255,240,205,.36) 9px,transparent 13px 16px)}
/* the near slice carries a broad crest highlight that breathes rather than
   travels — it's what stops the other two reading as a machine */
.surf i:nth-child(3){top:58%;height:52%;opacity:.8;
  animation:crest 11s ease-in-out infinite alternate;
  background:radial-gradient(120% 46% at 50% 68%,rgba(255,246,225,.4),transparent 70%)}
.surf i:nth-child(3)::before{content:none}
@keyframes swell{from{transform:translate3d(0,0,0)}
  to{transform:translate3d(0,var(--p,12px),0)}}
@keyframes crest{0%{opacity:.28;transform:translate3d(-1.5%,.8%,0)}
  100%{opacity:.62;transform:translate3d(1.5%,-.8%,0)}}

/* ------------------------------------------------------------------
   BIG SWELL — the opening chapter only.
   The brief on this frame was that it should feel like the yacht is ON water
   and the water is MOVING. The three-band swell used elsewhere is a whisper;
   here it is opened up: the band reaches higher up the frame, the stripes are
   brighter and travel faster, and two more layers go in — a fine quick
   shimmer riding the surface and a broad slow one underneath it, so the sea
   has near, middle and far movement instead of one uniform crawl.
   Still only transforms and opacity, and still parked when offscreen. */
.surf.big{height:60%;
  -webkit-mask-image:linear-gradient(180deg,transparent 0,#000 26%,#000 100%);
  mask-image:linear-gradient(180deg,transparent 0,#000 26%,#000 100%)}
/* Same depth-slice idea, opened up: five slices instead of three, so the sea
   has a genuine far / mid / near read. Note how the period grows and the
   duration shrinks as the slices come forward — that gradient of scale and
   speed is what makes the swell look like it is coming AT the viewer rather
   than a texture panning behind the boat. */
.surf.big i:nth-child(1){top:0;height:30%;opacity:.9}
.surf.big i:nth-child(1)::before{--p:7px;animation-duration:16s;
  background:repeating-linear-gradient(185deg,
    transparent 0 2px,rgba(190,235,255,.17) 3.5px,transparent 5px 7px)}
.surf.big i:nth-child(2){top:20%;height:34%;opacity:.9}
.surf.big i:nth-child(2)::before{--p:13px;animation-duration:11s;
  background:repeating-linear-gradient(178deg,
    transparent 0 4px,rgba(255,255,255,.34) 7px,transparent 10px 13px)}
/* the crest highlight — breathes in place, never travels */
.surf.big i:nth-child(3){top:34%;height:60%;opacity:.85;
  background:radial-gradient(118% 44% at 50% 64%,rgba(255,246,225,.36),transparent 72%)}
/* mid-near — the working body of the swell */
.surf.big i:nth-child(4){top:44%;height:40%;opacity:.9;animation:none}
.surf.big i:nth-child(4)::before{--p:24px;animation:swell 7.5s linear infinite;
  background:repeating-linear-gradient(176deg,
    transparent 0 7px,rgba(255,240,205,.4) 12px,transparent 18px 24px)}
/* the near water — coarse, quick chop right under the hull, which is what
   actually sells "this boat is floating on something that moves" */
.surf.big i:nth-child(5){top:74%;height:40%;opacity:.9;animation:none}
.surf.big i:nth-child(5)::before{--p:42px;animation:swell 5.2s linear infinite;
  background:repeating-linear-gradient(174deg,
    transparent 0 12px,rgba(255,255,255,.46) 21px,transparent 31px 42px)}

/* And the vessel rides it. A slow vertical heave on the photograph itself —
   half a percent, over seven seconds — which is far too small to read as the
   camera moving but is exactly what stops the yacht looking pasted onto a
   still sea. Written on `translate` rather than `transform` so it composes
   with the ken-burns `breathe` already running on this layer instead of
   overwriting it. */
#ch-welcome .photo::after{animation:breathe 19s ease-in-out infinite alternate,
                                    heave 7.4s ease-in-out infinite}
@keyframes heave{0%,100%{translate:0 0}50%{translate:0 .55%}}

/* ------------------------------------------------------------------
   MIST — the arch has to ARRIVE, not appear.
   Three fog banks sit over the reveal scene at full strength and clear
   upward as the chapter comes in, while the photograph itself settles down
   from a 1.06 scale. The whole move runs 2.6s against the site's usual
   1.2s: the brief asked for "even an extra 0.5-1 second", and on a hero
   this size slower genuinely does read as more expensive.
   ------------------------------------------------------------------ */
.mist{position:absolute;inset:0;z-index:2;pointer-events:none;overflow:hidden}
/* PERFORMANCE: each fog bank is roughly a quarter of a megapixel carrying a
   26px blur, and the drift used to animate `margin-left` — a layout property.
   Every frame relaid out the element AND forced the blurred layer to be
   re-rasterised from scratch, three times over, for the life of the page.
   That is why the arch reveal stuttered on exactly the beat it needed to be
   smoothest.

   Two moves are wanted on the same box — a slow sideways drift (animation) and
   the upward clearing (transition) — and one element can only hold one
   transform. So the drift lives on the <i> and the fog itself on its ::before,
   which owns the clearing. Both are pure transforms on the compositor, and no
   markup or custom-property animation is needed.

   Blur is also down to 14px: the source is already a soft radial gradient with
   a transparent falloff, so beyond ~14px the extra radius isn't visible — it
   was only ever costing fill rate. */
.mist i{position:absolute;left:-30%;right:-30%;
  animation:driftMist 23s ease-in-out infinite alternate}
.mist i::before{content:"";position:absolute;inset:0;border-radius:50%;
  background:radial-gradient(ellipse 50% 50% at 50% 50%,rgba(226,240,244,.5),transparent 72%);
  filter:blur(14px);
  opacity:1;transform:translate3d(0,0,0);
  transition:opacity 3400ms cubic-bezier(.3,.6,.2,1),
             transform 3400ms cubic-bezier(.25,.7,.2,1)}
.mist i:nth-child(1){top:34%;height:46%}
.mist i:nth-child(2){top:8%;height:52%;animation-duration:31s;animation-direction:alternate-reverse}
.mist i:nth-child(2)::before{opacity:.8}
.mist i:nth-child(3){top:58%;height:50%;animation-duration:27s}
@keyframes driftMist{0%{transform:translate3d(-3%,0,0)}100%{transform:translate3d(3%,0,0)}}
/* the clearing — staggered so the fog lifts in layers rather than as a sheet */
.in .mist i::before{opacity:0;transform:translate3d(0,-22%,0)}
.in .mist i:nth-child(2)::before{transition-delay:.35s}
.in .mist i:nth-child(3)::before{transition-delay:.7s}
/* And the photograph beneath it settles on the same clock. The scale goes on
   .par, NOT on .photo::after — ::after already runs the `breathe` keyframe,
   and a running animation beats a transitioned transform on the same element,
   so a settle declared there would simply never play. .par owns no transform
   of its own, which makes it the one free layer in the stack.

   The arch is also asked to come UP through the fog rather than simply
   sharpen in place, so the scene starts fractionally low and oversized and
   RISES as it settles — the literal "gently faded upward through the mist".
   Whole move now 3.6s against the brief's "even an extra 0.5-1 second". */
.mist-reveal .par{transform:scale(1.075) translateY(2.4%);transform-origin:50% 62%;
  transition:transform 3600ms cubic-bezier(.22,.68,.15,1)}
.mist-reveal.in .par{transform:none}
/* the copy waits for the arch instead of racing it */
#ch-reveal .in .fx,.mist-reveal.in .fx{transition-duration:1600ms}
.mist-reveal.in .fx.d1{transition-delay:1.5s}
.mist-reveal.in .fx.d2{transition-delay:1.95s}
.mist-reveal.in .fx.d3{transition-delay:2.45s}

/* ------------------------------------------------------------------
   KITES — layang-layang over the RSVP sky.
   ------------------------------------------------------------------ */
/* ------------------------------------------------------------------
   GOLD FLOCK — ornament that actually travels.
   The brief was for the gold to move to and fro, twenty pixels or so, "like
   the kite". The gold already IN the photographs cannot do that: it is
   painted into the plate, so moving it would mean cutting it out and filling
   the hole behind it, and two passes at deriving that automatically both
   damaged the artwork — ghosts where the cloud scrolls had been, and smearing
   through the sunlit palm fronds the extractor could not tell from gold ink.
   Lifting those properly needs clean plates, which is artwork, not code.

   What CAN move is ornament that was never part of the photograph. So this is
   a flock of drawn gold motifs in the same house language as the painted ones
   — cloud scrolls, gulls, four-point sparkles — seeded across every chapter
   and swinging on their own individual arcs. Same construction as the kites
   below: built in JS so each gets its own amplitude, tilt, period and phase,
   because a dozen ornaments sharing one keyframe reads as a sheet of
   wallpaper sliding, not as a sky with things in it.

   Everything is transform + opacity, so the whole flock composites and costs
   nothing to animate; it parks with its chapter like every other decoration.
   ------------------------------------------------------------------ */
.goldflock{position:absolute;inset:0;z-index:2;pointer-events:none;overflow:hidden}
/* The flock is drawn gold — cloud scrolls, gulls, sparkles — and it used to
   swing on its own alternate cycle. Same client note: the clouds hold still
   now. --go2 was the brighter end of that swing, so each motif keeps the
   presence it had at the top of its arc; nav.js still randomises it so the
   flock isn't a flat wash. */
.gold-drift{position:absolute;opacity:var(--go2,.9);
  filter:drop-shadow(0 2px 10px rgba(212,160,72,.4))}
.gold-drift svg{display:block;width:100%;height:100%;overflow:visible}
/* Two golds: the line work is the deeper leaf tone, the fills catch light. */
.gold-drift .ink{fill:none;stroke:rgba(226,178,96,.85);stroke-width:1.5;
  stroke-linecap:round;stroke-linejoin:round}
.gold-drift .lit{fill:rgba(255,225,150,.5)}
.gold-drift .spark{fill:rgba(255,240,196,.9)}

.kites{position:absolute;inset:0;z-index:2;pointer-events:none;overflow:hidden}
.kite{position:absolute;width:44px;height:108px;opacity:0;
  transform:scale(var(--kscale,.7));
  animation:kiteDrift ease-in-out infinite alternate}
.kite svg{width:100%;height:100%;overflow:visible}
.kite .sail{fill:rgba(255,246,224,.5);stroke:rgba(246,193,119,.55);stroke-width:1.1}
.kite .spar{stroke:rgba(160,120,60,.3);stroke-width:.7;fill:none}
.kite .tail{stroke:rgba(255,240,205,.42);stroke-width:1.1;fill:none}
@keyframes kiteDrift{
  0%{opacity:.5;transform:translate3d(0,0,0) rotate(calc(var(--ktilt,8deg) * -1)) scale(var(--kscale,.7))}
  100%{opacity:.78;transform:translate3d(var(--kx,18px),var(--ky,-12px),0) rotate(var(--ktilt,8deg)) scale(var(--kscale,.7))}
}

/* colour grade + legibility — ONE grading system for every scene: deep-sea
   blue shadows everywhere, and a shared morning soft-light wash (::after)
   that casts identical fresh light over all six heroes: azure at the zenith,
   turquoise through the middle, clean yellow at the waterline. Variants tune
   depth only, never hue family. This wash is the single biggest lever on the
   site's time of day — it used to run plum→gold→coral, which is what made
   every scene read as a sundowner. */
.grade{position:absolute;inset:0;z-index:1;pointer-events:none;
  background:radial-gradient(135% 105% at 50% 46%,transparent 56%,rgba(6,44,70,.20) 100%),
    linear-gradient(180deg,rgba(10,52,80,.10),transparent 22%,transparent 76%,rgba(6,40,66,.13))}
/* The heavy cyan cast that used to live here is gone — it existed only to cool
   sunset source photography, and over morning-graded heroes it just muted the
   gold. What remains is a whisper of soft-light to tie the six together, weak
   enough that the gold motifs and turquoise water read at full strength. */
/* NO mix-blend-mode HERE — it was `soft-light`, and that is the single most
   expensive thing this page can do to a scrolling scene. A blended element
   pulls its backdrop into the same compositing group, so the layer underneath
   cannot be promoted and cached: the whole viewport-sized photograph has to be
   re-rasterised and re-blended on every scrolled frame, on all seven chapters.
   That is the mobile tearing the client recorded on 5 Aug 2026 — bands of the
   scene half-updated behind the copy — and it is the same rule already written
   down for `.orb` further down this file: do not blend anything that moves.
   The tint is now a plain gradient at roughly half the alpha, because normal
   compositing reads stronger than soft-light at the same value. */
.grade::after{content:"";position:absolute;inset:0;
  background:linear-gradient(180deg,rgba(40,150,190,.10) 0%,rgba(70,185,205,.04) 40%,rgba(255,228,140,.07) 100%)}
/* The variants now differ only by a few points of depth. With all six heroes
   graded to the same morning, heavy per-chapter grading would break the very
   continuity the new photography creates. Bottom darkening is deliberately
   light everywhere so the gold motifs sitting on the sand stay bright. */
.grade.warm{background:radial-gradient(135% 105% at 50% 46%,transparent 56%,rgba(6,46,72,.19) 100%),
    linear-gradient(180deg,rgba(10,54,82,.09),transparent 22%,transparent 76%,rgba(6,42,68,.13))}
.grade.strong{background:radial-gradient(132% 102% at 50% 44%,transparent 50%,rgba(5,40,66,.25) 100%),
    linear-gradient(180deg,rgba(8,48,76,.13),transparent 22%,transparent 74%,rgba(5,36,60,.17))}
.grade.teal{background:radial-gradient(135% 105% at 50% 46%,transparent 56%,rgba(6,48,74,.20) 100%),
    linear-gradient(180deg,rgba(9,52,80,.10),transparent 22%,transparent 76%,rgba(5,40,66,.14))}
.grade.bright{background:radial-gradient(138% 108% at 50% 42%,transparent 60%,rgba(7,50,78,.16) 100%),
    linear-gradient(180deg,rgba(10,56,86,.07),transparent 22%,transparent 78%,rgba(6,44,72,.11))}
.grade.night{background:radial-gradient(132% 102% at 50% 42%,transparent 52%,rgba(4,36,62,.24) 100%),
    linear-gradient(180deg,rgba(6,44,72,.12),transparent 22%,transparent 74%,rgba(4,32,56,.17))}
.grade.dusk{background:radial-gradient(132% 104% at 50% 40%,transparent 54%,rgba(5,40,66,.22) 100%),
    linear-gradient(180deg,rgba(8,48,76,.11),transparent 24%,transparent 74%,rgba(5,36,60,.15))}

/* light rays / water shimmer.
   The wedge used to have hard shoulders (transparent → .13 → transparent over
   4% of a turn) and it rotated an element pinned at inset:0, so the corners
   swung into view as pale triangles. Now the shoulders are long ramps, the
   layer is scaled up and its origin sits on the cone's own apex, so the
   rotation happens about the light source and never exposes an edge. */
.rays{position:absolute;inset:0;z-index:1;pointer-events:none;mix-blend-mode:screen;
  transform-origin:50% 20%;
  background:conic-gradient(from 206deg at 50% 20%,
    transparent 0 4%,rgba(255,248,214,.045) 9%,rgba(255,248,214,.09) 13%,
    rgba(255,248,214,.035) 18%,transparent 24% 100%);
  animation:rays 38s ease-in-out infinite alternate}
@keyframes rays{0%{opacity:.3;transform:scale(1.2) rotate(-2deg)}
  100%{opacity:.6;transform:scale(1.2) rotate(2deg)}}
/* Surface glitter. Same correction as the swell: the lines were 4deg off
   vertical, so this was a second set of upright bars sliding across the sea.
   They now lie along the water and creep toward the viewer, and the sheet is
   masked to fade at both ends so it never terminates on a visible line. */
.waterglint{position:absolute;left:0;right:0;bottom:0;height:42%;z-index:1;pointer-events:none;
  overflow:hidden;mix-blend-mode:screen;opacity:.5;
  -webkit-mask-image:linear-gradient(180deg,transparent 0,#000 30%,#000 82%,transparent 100%);
  mask-image:linear-gradient(180deg,transparent 0,#000 30%,#000 82%,transparent 100%)}
.waterglint::before{content:"";position:absolute;left:-6%;right:-6%;top:-60%;bottom:-60%;
  background:repeating-linear-gradient(179deg,
    transparent 0 6px,rgba(255,255,255,.055) 9px,transparent 12px 18px);
  animation:glint 12s linear infinite}
.waterglint.teal{opacity:.4}
@keyframes glint{from{transform:translate3d(0,0,0)}to{transform:translate3d(0,18px,0)}}

/* candle glow / agni fire / embers / petals */
.glow{position:absolute;z-index:2;width:150px;height:150px;transform:translate(-50%,-50%);pointer-events:none;
  background:radial-gradient(circle,rgba(255,196,128,.72),rgba(255,150,80,.2) 45%,transparent 66%);
  filter:blur(5px);mix-blend-mode:screen;animation:flick 3.4s ease-in-out infinite}
@keyframes flick{0%,100%{opacity:.6;transform:translate(-50%,-50%) scale(1)}
  30%{opacity:1;transform:translate(-50%,-50%) scale(1.08)}
  55%{opacity:.5;transform:translate(-50%,-51%) scale(.97)}
  80%{opacity:.88;transform:translate(-50%,-50%) scale(1.03)}}
.fire{position:absolute;z-index:2;width:150px;height:230px;transform:translate(-50%,-62%);pointer-events:none;
  background:radial-gradient(ellipse 55% 60% at 50% 72%,rgba(255,214,140,.95),rgba(255,140,50,.5) 38%,rgba(255,90,30,.12) 60%,transparent 72%);
  filter:blur(6px);mix-blend-mode:screen;animation:fire .45s ease-in-out infinite alternate}
@keyframes fire{0%{transform:translate(-50%,-62%) scale(1,1);opacity:.9}
  100%{transform:translate(-51%,-63%) scale(1.06,1.13);opacity:1}}
.embers,.petals{position:absolute;inset:0;z-index:2;pointer-events:none;overflow:hidden}
.ember{position:absolute;bottom:16%;width:4px;height:4px;border-radius:50%;
  background:radial-gradient(circle,#ffd89a,#ff9a3c);box-shadow:0 0 6px 2px rgba(255,160,70,.6);
  animation:rise linear infinite;opacity:0}
@keyframes rise{0%{transform:translateY(0) translateX(0) scale(1);opacity:0}
  12%{opacity:1}70%{opacity:.9}100%{transform:translateY(-46vh) translateX(var(--ex,20px)) scale(.4);opacity:0}}
.petal{position:absolute;top:-6%;width:12px;height:12px;border-radius:60% 0 60% 0;opacity:0;animation:fall linear infinite}
.petal.white{background:radial-gradient(circle at 35% 30%,#fff,rgba(255,240,225,.7))}
.petal.marigold{background:radial-gradient(circle at 35% 30%,#ffd24d,#f39a1f)}
.petal.gold{background:radial-gradient(circle at 35% 30%,#fce3ae,#e9b95f);box-shadow:0 0 6px 1px rgba(246,193,119,.35)}

/* Cosmic motif: scattered gold star-motes drifting inward, "stars coming together" */
.constellation{position:absolute;inset:0;z-index:2;pointer-events:none;overflow:hidden}
.star-mote{position:absolute;left:50%;top:50%;width:7px;height:7px;margin:-3.5px 0 0 -3.5px;opacity:0;
  background:radial-gradient(circle,#fff6df,#f6c177 55%,transparent 70%);
  clip-path:polygon(50% 0%,61% 39%,100% 50%,61% 61%,50% 100%,39% 61%,0% 50%,39% 39%);
  animation:converge linear infinite}
@keyframes converge{
  0%{transform:translate(var(--sx,0),var(--sy,0)) scale(.35) rotate(0deg);opacity:0}
  16%{opacity:.85}
  82%{opacity:.55}
  100%{transform:translate(0,0) scale(1.15) rotate(160deg);opacity:0}
}

/* ==================================================================
   PER-CHAPTER HEADLINE MOTION
   Every chapter now owns one animation you actually notice, at roughly
   the weight of the petal shower (which was the only one carrying that
   job before). All three below are built the same way the petals and
   embers are — a handful of absolutely-positioned spans animating
   nothing but `transform` and `opacity` — because that pattern is
   already proven to run on the compositor for free. No filters, no
   layout properties, no new blurred surfaces.

   All three MUST also appear in the `.chapter:not(.live)` park list
   above, or they run forever on all seven chapters at once.
   ================================================================== */

/* SUN GLITTER — the sea, everywhere it appears.
   Raising the swell alphas made the water move; this is what makes it
   look WET. Real sunlit water is not a smooth sheet, it is thousands of
   individual specular points blinking on and off as the surface tilts.
   A few dozen twinkling specks along the waterline read as that instantly,
   and they carry the eye far better than any gradient can. */
.seaglint{position:absolute;left:0;right:0;bottom:0;height:48%;z-index:2;
  pointer-events:none;overflow:hidden}
/* SCOPED TO .seaglint, and it must stay that way. `.spec` is also the class on
   the <dl> that sets out times, venues and dress codes on every chapter page,
   and unscoped these declarations won that collision: those lists were being
   rendered as a 3x3px absolutely-positioned transparent dot running `twinkle`.
   Every schedule time, venue and dress code on The Celebration, Travel & Stay
   and Things to Do was invisible. script.js only ever creates these specular
   points inside a .seaglint box, so the descendant selector costs nothing. */
.seaglint .spec{position:absolute;width:var(--ss,3px);height:var(--ss,3px);border-radius:50%;opacity:0;
  background:radial-gradient(circle,#fff,rgba(255,244,214,.75) 45%,transparent 72%);
  animation:twinkle ease-in-out infinite}
@keyframes twinkle{
  0%,100%{opacity:0;transform:scale(.4)}
  38%{opacity:1;transform:scale(1.35)}
  62%{opacity:.45;transform:scale(.95)}}

/* Our Story carried literal shooting stars here for a while — the poem asks
   for one by name. They were removed on request: too on-the-nose, and a
   streak crossing the frame pulls the eye off the copy, which is the whole
   point of that chapter. Its motion is the sun glitter on the water instead. */

/* GOLDEN-HOUR BOKEH — Day One.
   The brief for this chapter was stillness (yoga, an empty shore, no
   petals and no fire), and that is why it ended up with nothing moving
   but the swell. Warm light lifting off the sand keeps the restfulness —
   it is slow and it drifts rather than falls — while still giving the
   chapter something of its own to watch. */
.bokeh{position:absolute;inset:0;z-index:2;pointer-events:none;overflow:hidden}
/* Reads as LIGHT, not as an object, and does it without a blend mode.
   The first version put an offset white core on an opaque disc, which renders
   as a lit sphere — a pearl floating up the frame. `mix-blend-mode:screen`
   fixed the look but is the wrong tool on something that moves: it pulls the
   element into the backdrop's compositing group, so it cannot be promoted to
   its own layer and the region beneath it re-rasterises as it travels.
   `.ember` already solves this on this site with no blend mode at all — a
   warm core plus a box-shadow halo — so the same trick is used here, scaled
   up and softened. Centred gradient (no offset highlight), warm, and the
   halo does the work the blend mode was doing. */
.orb{position:absolute;bottom:-10%;border-radius:50%;opacity:0;
  background:radial-gradient(circle,rgba(255,248,228,.9),rgba(255,206,132,.42) 38%,rgba(255,184,100,.12) 62%,transparent 78%);
  box-shadow:0 0 18px 6px rgba(255,206,140,.28);
  animation:lift linear infinite}
@keyframes lift{
  0%  {transform:translate3d(0,0,0) scale(.65);opacity:0}
  16% {opacity:.9}
  74% {opacity:.55}
  100%{transform:translate3d(var(--ox,30px),-92vh,0) scale(1.3);opacity:0}}

/* Fine sand-grain texture for beach scenes */
.sand-grain{position:absolute;left:0;right:0;bottom:0;height:30%;z-index:1;pointer-events:none;opacity:.35;
  background-image:radial-gradient(rgba(255,240,215,.5) .5px,transparent .5px),
    radial-gradient(rgba(255,240,215,.35) .5px,transparent .5px);
  background-size:14px 14px,9px 9px;background-position:0 0,5px 6px}
@keyframes fall{0%{transform:translateY(0) translateX(0) rotate(0);opacity:0}
  10%{opacity:.9}90%{opacity:.7}100%{transform:translateY(96vh) translateX(var(--sx,40px)) rotate(320deg);opacity:0}}

/* extra legibility over photography */
.reveal-names,.section-title{text-shadow:0 1px 3px rgba(0,0,0,.42),0 2px 26px rgba(0,0,0,.45)}
.lede,.poem,.reveal-pre{text-shadow:0 1px 2px rgba(0,0,0,.4),0 1px 16px rgba(0,0,0,.42)}
/* .reveal-when sets its own, deeper shadow — it has no panel to sit on */

/* Reduce Motion: ease the busy/particle motion but keep the heroes gently alive */
@media(prefers-reduced-motion:reduce){
  .petal,.ember,.rays,.waterglint,.spark,.orb{display:none!important}
  /* the sea keeps its specular points, but they hold steady instead of
     blinking — the water still reads as water without the flicker */
  .seaglint .spec{animation:none!important;opacity:.5!important}
  /* the swell stays — it's the thing standing in for the removed camera
     shake, and a still ocean reads as a photograph of an ocean — but it
     slows right down, and the travelling bands give way to the crest alone */
  .surf i::before{animation-duration:48s!important}
  .surf i:nth-child(1),.surf i:nth-child(2),
  .surf i:nth-child(4),.surf i:nth-child(5){opacity:.3}
  .surf i:nth-child(3){animation-duration:22s!important}
  /* the clouds and the flock no longer move at all, so there is nothing left
     here to slow down — only the glow, which eases off */
  .foil::before{animation-duration:24s!important}
  .goldglint::before{animation-duration:26s!important}
  .kite{animation-duration:40s!important}
  .mist i{animation:none;transition-duration:1400ms}
  .mist-reveal .par{transition-duration:1400ms}
  .photo::before,.photo::after{animation-duration:26s!important}
  .glow,.fire{animation-duration:4s!important}
  .fx{transition-duration:.5s!important;filter:none}
  .board-btn::after{animation:none;display:none}
  .bp-cta .pulse::after{animation:none;display:none}
  .bp-cta .pulse{animation:none}
  .bp-rule::after{animation:none}
  .comet{display:none}
  /* the stars still travel (they're a progress marker) — just quietly */
  .star i,.star-track.met .star i,.star::before{animation:none}
  .star-track.met .st-halo{animation-duration:.6s}
  .ch-num,.ch-hero h1,.ch-standfirst{animation-duration:.5s}
  .rise{transition-duration:.4s;filter:none}
  .ch-cue span{animation:none}
  /* the button stays fully visible — it is a control, not decoration; only its
     nudge stops */
  .scroll-cue .chev{animation:none}
  .palm-sky .frond{animation:none}
  .section-title,.in .section-title{letter-spacing:.015em}
  .page-veil{transition-duration:.15s}
}

/* ============================================================
   PAGE TRANSITIONS
   Between chapters the site used to hard-cut: a white flash, then a new
   page. On a phone that single frame undoes a lot of the atmosphere. Now
   each chapter fades out into the deep and the next one fades up out of it,
   so moving through the site feels like the journey continuing rather than
   documents being loaded.
   ============================================================ */
/* The arrival fade is a CSS ANIMATION, not a JS class that gets removed on a
   later frame. That distinction matters more than it looks: a full-screen
   opaque overlay which only JavaScript can lift is a blank page the moment
   that JavaScript doesn't run on schedule — and requestAnimationFrame does
   not fire in a background tab, which is precisely how an invite link opened
   from WhatsApp or Gmail arrives. As an animation it always completes on its
   own, and since the element is only created by JS in the first place, no JS
   simply means no veil. Neither path can strand a guest on a coloured
   rectangle. */
.page-veil{position:fixed;inset:0;z-index:2000;pointer-events:none;
  background:var(--m-deep,var(--deep));opacity:0;
  animation:veilIn 620ms ease both}
@keyframes veilIn{from{opacity:1}to{opacity:0}}
/* Leaving is JS-driven, but safely so: it only ever adds cover for the ~440ms
   before navigation, and nav.js clears it on a timer if navigation never
   happens. */
body.leaving .page-veil{animation:none;opacity:1;pointer-events:auto;
  transition:opacity 420ms ease}

/* ============================================================
   INTEGRATIONS — WhatsApp button + Details & Travel
   ============================================================ */
.wa-fab{position:fixed;left:max(20px,env(safe-area-inset-left));bottom:max(22px,env(safe-area-inset-bottom));z-index:1100;width:52px;height:52px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;color:#fff;background:#1fab5a;text-decoration:none;
  box-shadow:0 10px 26px -6px rgba(0,0,0,.5);opacity:0;transform:translateY(10px);pointer-events:none;
  transition:opacity .8s ease,transform .3s ease,box-shadow .3s ease}
.wa-fab.show{opacity:1;transform:none;pointer-events:auto}
.wa-fab:hover{transform:translateY(-2px) scale(1.05);box-shadow:0 16px 34px -8px rgba(31,171,90,.6)}
.wa-fab:active{transform:scale(.92)}
.wa-fab svg{width:28px;height:28px}

/* .detail-grid / .detail-card lived only in the homepage's Details & Travel
   chapter, which the client cut. That content now sits on Travel & Stay, which
   uses the shared .ch-block / .spec editorial pattern like every other chapter
   page — so the card styles are gone rather than kept as a second, competing
   layout system for the same information. */
.ghost-btn{display:inline-flex;align-items:center;gap:.5rem;margin-top:1.1rem;padding:.62rem 1.1rem;border-radius:30px;
  font-family:var(--font-body);font-size:.78rem;letter-spacing:.03em;color:var(--ivory);text-decoration:none;cursor:pointer;
  background:rgba(246,193,119,.12);border:1px solid rgba(246,193,119,.42);
  transition:background .25s ease,transform .2s ease,border-color .25s ease}
.ghost-btn:hover{background:rgba(246,193,119,.24);border-color:var(--gold);transform:translateY(-1px)}
.ghost-btn svg{width:16px;height:16px;color:var(--gold-soft);flex:0 0 auto}

/* ============================================================
   SITE NAV — persistent hamburger, shared across every page
   ============================================================ */
/* No wordmark — the music + menu controls sit alone on the right, so the
   corner stays quiet and the artwork reads uninterrupted. */
/* max() rather than a flat add: on a browser tab (the normal way this invite
   opens) env(safe-area-inset-*) is 0, so this is identical to the old flat
   18/22px. It only grows on a notch/Dynamic-Island phone or once "added to
   Home Screen", where the fixed chrome would otherwise sit under the status
   bar (viewport-fit=cover in the <meta viewport> is what makes env() report
   anything other than 0 here). */
.site-nav{position:fixed;top:0;left:0;right:0;z-index:1300;display:flex;align-items:center;justify-content:flex-end;
  padding:max(18px,env(safe-area-inset-top)) max(22px,env(safe-area-inset-right)) 18px 22px;pointer-events:none}
.nav-right{display:flex;align-items:center;gap:12px}
/* see the .music-toggle note — same square-instead-of-circle failure */
.menu-btn{width:46px;height:46px;border-radius:50%;border:1px solid rgba(246,193,119,.4);background:rgba(20,12,25,.62);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;
  cursor:pointer;opacity:0;transform:translateY(-6px);transition:opacity .8s ease,transform .3s ease,border-color .3s ease;
  pointer-events:none;flex:0 0 auto}
.menu-btn.show{opacity:1;transform:none;pointer-events:auto}
.menu-btn:hover{border-color:var(--gold)}
.menu-btn:active{transform:scale(.92);border-color:var(--gold)}
.menu-btn i{width:18px;height:1px;background:var(--gold-soft);transition:transform .3s ease,opacity .3s ease}
.menu-btn.open i:nth-child(1){transform:translateY(6px) rotate(45deg)}
.menu-btn.open i:nth-child(2){opacity:0}
.menu-btn.open i:nth-child(3){transform:translateY(-6px) rotate(-45deg)}

.nav-panel{position:fixed;inset:0;z-index:1290;visibility:hidden}
/* Explicit z-index on both children — never rely on DOM order alone to keep
   the sheet above the scrim. No backdrop-filter: blurring "whatever sits
   behind" this layer has been unreliable on some mobile browsers and can end
   up smearing the sheet's own text instead of the page behind it. Opening is
   instant (no transition) so it can never be caught mid-animation. */
/* Scrim and sheet both sit in the deep-ocean family now. They used to run
   plum -> brown (#241a2c -> #3a2420), the last of the retired sunset palette,
   which read as a different site sliding in over the morning artwork. */
.nav-panel .scrim{position:absolute;inset:0;z-index:1;background:rgba(4,20,32,.72);opacity:0}
/* UX audit, 8 Aug 2026: overflow-y:auto with no overscroll-behavior means
   scrolling this drawer to either end can chain into scrolling the PAGE
   behind it (the classic iOS rubber-band-through-a-modal bug) — the menu is
   short enough on most phones that this rarely triggers, but on a small
   screen with the "reveal" note field etc. it's a real, if occasional, bug.
   contain: same family of scroll-jank this codebase already fixed elsewhere
   (see animation-perf-rules), just never applied to this one drawer. */
.nav-panel .sheet{position:absolute;top:0;right:0;bottom:0;z-index:2;width:min(88vw,380px);overflow-y:auto;
  overscroll-behavior:contain;
  background:linear-gradient(160deg,#07202F 0%,#0A3049 55%,#0F4763 100%);
  border-left:1px solid rgba(246,193,119,.25);box-shadow:-30px 0 60px rgba(0,0,0,.5);
  padding:100px 40px max(40px,env(safe-area-inset-bottom));transform:translateX(100%);
  display:flex;flex-direction:column}
.nav-panel.open{visibility:visible}
.nav-panel.open .scrim{opacity:1}
.nav-panel.open .sheet{transform:translateX(0)}
/* The menu is a table of contents, not a list of pages — every entry carries
   its chapter number, so the shape of the whole journey is visible from
   anywhere in it. */
.nav-panel .nav-head{font-family:var(--font-engrave);font-size:.58rem;letter-spacing:.34em;
  text-transform:uppercase;color:var(--gold-soft);opacity:.75;margin-bottom:1.1rem}
.nav-panel a{display:flex;align-items:baseline;gap:.9rem;
  font-family:var(--font-display);font-size:1.4rem;color:var(--ivory);text-decoration:none;
  padding:.6rem 0;border-bottom:1px solid rgba(246,193,119,.12);opacity:.88;
  transition:opacity .2s ease,padding-left .2s ease;position:relative;z-index:1}
.nav-panel a b{flex:0 0 auto;font-family:var(--font-body);font-weight:500;font-size:.62rem;
  letter-spacing:.18em;color:var(--gold-soft);opacity:.7}
.nav-panel a:hover,.nav-panel a.active{opacity:1;padding-left:8px;color:var(--gold-soft)}
.nav-panel a.active b{opacity:1}
.nav-panel .nav-foot{margin-top:auto;padding-top:1.6rem;font-family:var(--font-body);font-size:.72rem;
  letter-spacing:.08em;color:var(--sand);opacity:.6}

/* ============================================================
   CHAPTER PAGES
   ------------------------------------------------------------
   These pages used to run a "banner + text + cards" template, and because
   every page ran the SAME one, the site went cinematic on the homepage and
   then turned into a wedding-website template for the other six. The note
   we got was precise about it: think of every page as a homepage, not a
   layout; each one a chapter with its own mood, in the same world.

   So the template is gone. What replaces it is the homepage's own grammar:

     · a full-bleed hero that fills the viewport, edge to edge, cover — not
       a 46vh band with a letterboxed picture floating inside it
     · the title over the image, nothing beside it
     · the image dissolving into the page rather than ending at a line
     · body copy in a single narrow measure with a lot of air
     · NO cards, NO dashed boxes, NO coloured rectangles behind text, NO
       framed thumbnails. Information is set as typography and separated by
       hairlines. Where a picture interrupts the text it goes full-bleed and
       masked, so it reads as a held shot, not an inserted rectangle.
     · a mood per chapter — ocean, sunset, voyage, dusk, shade — carried in
       CSS variables so each page feels like a different hour of the same
       trip while sharing one palette, one type system, one grain.
   ============================================================ */

/* ---- the moods. Gold is untouched by all of them: it's the brand's metal,
        not the weather. Only the atmosphere changes. ---- */
/* ONE GROUND FOR EVERY CHAPTER PAGE.
   --m-deep is the flat colour the page sits on below its hero, and it used to
   change with the mood: deep teal on Our Story, navy on Travel, and plum on
   The Celebration. Moving between pages therefore stepped through three
   unrelated solid colours for no reason a guest could see — client note,
   3 Aug 2026: "suddenly the color has become purple here" / "navy blue here" /
   "solid colors and backgrounds thats not making sense".

   The ground is now the same deep sea on all of them, so the pages read as
   one place. The per-mood tokens that remain are the ones tied to the
   PHOTOGRAPH — the hero's glow and veil — so each chapter still has its own
   light without the page underneath it changing colour. */
.chapter-page{
  --m-deep:#07303F;
  --m-glow:rgba(60,170,190,.20);--m-veil:rgba(4,24,40,.55)}
.chapter-page[data-mood="ocean"] {--m-glow:rgba(70,195,190,.22);--m-veil:rgba(4,30,42,.55)}
.chapter-page[data-mood="sunset"]{--m-glow:rgba(255,150,80,.26);--m-veil:rgba(38,18,22,.55)}
.chapter-page[data-mood="voyage"]{--m-glow:rgba(90,180,220,.20);--m-veil:rgba(4,22,38,.55)}
.chapter-page[data-mood="dusk"]  {--m-glow:rgba(150,140,220,.18);--m-veil:rgba(8,18,34,.58)}
.chapter-page[data-mood="shade"] {--m-glow:rgba(90,200,180,.16);--m-veil:rgba(4,22,30,.60)}

body.chapter-page{background:var(--m-deep)}
.chapter-page .page{position:relative;min-height:100svh;background:transparent}

/* ---------- the hero: full viewport, full bleed ---------- */
.ch-hero{position:relative;height:100svh;min-height:520px;overflow:hidden;
  display:flex;align-items:flex-end;justify-content:center}
.ch-hero-media{position:absolute;inset:0;z-index:0}
/* The site-wide .photo renders `contain` inside an oversized .plx wrapper —
   correct on Home, where each hero is an art-directed frame meant to be seen
   whole. Here we want the opposite and it must be said explicitly: no .plx
   ancestor, no blurred bed, cover to the edges. THIS is the "small floating
   image" the note was describing. */
.ch-hero .photo::before{display:none}
.ch-hero .photo::after{inset:0;background-size:cover;
  background-position:var(--focal,center);
  filter:saturate(1.04);animation-duration:34s}
/* The banner's gold glints too, but the geometry it has to match is this
   `cover` + `--focal` framing rather than Home's `contain`, and the breathe
   here runs at 34s. These pages carry exactly one hero and have no .chapter
   ancestor, so there is no .live gate to wait on — the layer simply runs. */
/* The Travel banner is the one chapter page showing the yacht, and that
   plate is CLEAN — its clouds were lifted off into images/gold/yacht-*.png so
   they could move on Home. Without a foil here the banner is simply a sky
   with no clouds in it, which is what it had been since the split.
   Two things differ from Home. The framing is cover, so the sprites and their
   masks have to be cover too. And there is no .chapter here to ever go
   .live, so this layer states its own opacity and runs its own animation —
   the opacity explicitly, because in the single-file artifact every page is a
   route inside index.html where body.io-live IS set, and `.io-live .foil
   {opacity:0}` would otherwise leave the clouds on the deployed site and
   missing from the shareable link. */
/* Static on the inner pages too — "cut down unnecessary animations from other
   pages … those clouds doesn't need to move". The drift and both travelling
   sheens are gone; the gold keeps its slow glow so it still reads as metal. */
.ch-hero .foil{inset:0;background-size:cover;opacity:1}
.ch-hero .foil::before,.ch-hero .foil::after{
  -webkit-mask-size:cover;mask-size:cover}
.ch-hero .foil.l::before{animation:foilGlow 8.5s ease-in-out 1.6s infinite}
.ch-hero .foil.r::before{animation:foilGlow 10.9s ease-in-out 3.1s infinite}

.ch-hero .goldglint{inset:0;animation-duration:34s;
  -webkit-mask-size:cover;mask-size:cover;
  -webkit-mask-position:var(--focal,center);mask-position:var(--focal,center)}
.ch-hero .goldglint::before{animation:foilGlow 11s ease-in-out 900ms infinite}
/* the image doesn't stop, it dissolves — so the copy below flows out of the
   picture instead of starting under a hard horizontal edge */
.ch-hero-wash{position:absolute;inset:0;z-index:1;pointer-events:none;
  background:
    linear-gradient(180deg,rgba(4,20,32,.42) 0%,rgba(4,20,32,.06) 26%,
      var(--m-veil) 62%,var(--m-deep) 97%),
    radial-gradient(120% 70% at 50% 108%,var(--m-glow),transparent 62%)}
.chapter-page[data-mood="sunset"] .ch-hero-wash{
  background:
    linear-gradient(180deg,rgba(52,22,26,.30) 0%,rgba(255,170,90,.10) 30%,
      rgba(196,86,44,.34) 58%,var(--m-deep) 97%),
    radial-gradient(130% 62% at 50% 96%,rgba(255,176,92,.42),transparent 66%)}

.ch-hero-copy{position:relative;z-index:2;width:100%;max-width:940px;
  padding:0 7vw clamp(4.5rem,13vh,9rem);text-align:center}
.ch-num{font-family:var(--font-engrave);text-transform:uppercase;letter-spacing:.42em;
  font-size:.62rem;color:#f2d29a;text-shadow:0 1px 12px rgba(0,0,0,.7)}
.ch-hero h1{font-family:var(--font-display);font-weight:400;
  font-size:clamp(2.5rem,8vw,5.4rem);line-height:1.06;color:var(--ivory);
  margin:.6rem 0 0;letter-spacing:.06em;
  text-shadow:0 3px 34px rgba(0,0,0,.55)}
.ch-standfirst{margin:1.4rem auto 0;max-width:44ch;
  font-family:'Cormorant Garamond',var(--font-display);font-style:italic;font-weight:500;
  font-size:clamp(1.02rem,2.5vw,1.34rem);line-height:1.7;color:#f6e7d4;
  text-shadow:0 2px 20px rgba(0,0,0,.6)}

/* Hero entrance — the same "settle" the homepage titles use.
   Written as from-only keyframes with `backwards` fill, NOT as
   `opacity:0` + animate-to-1. The difference is what happens when the
   animation never runs: here the element's resting style is the visible one,
   so a browser with animations switched off shows the title plainly instead
   of showing nothing at all. The animation can only ever take something away
   temporarily, never be the sole reason a word exists. */
.ch-num,.ch-standfirst{animation:chIn 1500ms cubic-bezier(.2,.7,.2,1) backwards}
.ch-hero h1{letter-spacing:.03em;
  animation:chInTitle 1900ms cubic-bezier(.2,.7,.2,1) backwards;animation-delay:.55s}
.ch-num{animation-delay:.35s}
.ch-standfirst{animation-delay:.85s}
/* Both of these used to open from `filter:blur(5px)`, and the title also from
   `letter-spacing:.16em`. That put a blur AND a layout property on the single
   largest piece of type on the page, animating for 1.9s starting the instant
   the page loads — competing directly with hero-image decode for the same
   frames. It is the first thing a guest sees on every inner page and it was
   the jankiest moment on the site.
   Same arrival, composited: the title still opens fractionally wide and
   settles, but as a scaleX the GPU can do for free. */
@keyframes chIn{from{opacity:0;transform:translateY(20px)}}
@keyframes chInTitle{from{opacity:0;transform:translateY(20px) scaleX(1.055)}}

.ch-cue{position:absolute;left:50%;bottom:20px;z-index:2;transform:translateX(-50%);
  width:1px;height:44px;overflow:hidden;opacity:.6}
.ch-cue span{display:block;width:1px;height:100%;
  background:linear-gradient(var(--gold-soft),transparent);
  animation:cue 2.4s ease-in-out infinite}

/* ---------- the body: type and air, nothing else ---------- */
.ch-body{position:relative;z-index:2;padding-bottom:clamp(4rem,9vh,7rem)}
/* UX audit, 8 Aug 2026: any .ch-block that's also an anchor target (currently
   just #hotel-booking on travel.html, reached by the RSVP redirect) would
   otherwise land with its heading tucked directly under the fixed .site-nav/
   .page-back — both are position:fixed;top:0, so on a chapter page (unlike
   index.html's full-height chapters, where the same fixed nav is an overlay
   that never depends on scroll position) a normal in-flow scroll-to puts the
   section's own top exactly where that chrome sits. Scoped to .ch-block[id]
   rather than a blanket [id] rule, so this only affects real anchor targets
   on chapter pages and never touches index.html's #ch-* deep-link scrolling. */
.ch-block[id]{scroll-margin-top:96px}
.ch-block{max-width:640px;margin:0 auto;padding:clamp(3.4rem,8vh,6.5rem) 7vw 0}
.ch-block.tight{padding-top:clamp(1.6rem,4vh,3rem)}
/* A PHOTOGRAPH OWNS THE AIR AROUND IT.
   The plate already carries its own margin top and bottom. A text block that
   follows one was then ADDING its full top padding to that margin, so every
   picture had roughly twice as much space after it as before it — 114px vs
   49px on a phone, 216px vs 112px on a laptop. That lopsided void under each
   photograph is the "unnecessary spaces" note of 3 Aug 2026.
   Zeroing the padding here leaves the plate's margin as the single gap on
   both sides, so the rhythm is even. Specificity is raised past
   `.ch-block.tight` deliberately: this has to win wherever a block follows a
   plate, whether or not that block was also marked tight. */
.ch-body .ch-plate + .ch-block{padding-top:0}
.ch-body p{font-family:var(--font-body);font-weight:300;font-size:1.03rem;
  line-height:2;color:var(--sand);margin:1.3em 0}
.ch-body .lead{font-family:'Cormorant Garamond',var(--font-display);font-style:italic;font-weight:500;
  font-size:clamp(1.2rem,3vw,1.6rem);line-height:1.75;color:var(--ivory);margin:0 0 .4em}
.ch-body h2{font-family:var(--font-engrave);font-weight:500;font-size:clamp(1rem,2.4vw,1.22rem);
  letter-spacing:.24em;text-transform:uppercase;color:var(--gold-soft);
  text-align:center;margin:0 0 .2rem}
.ch-body h2 + .rule{margin-bottom:2.2rem}
.ch-body em{font-style:italic;color:var(--gold-soft)}
/* Client screenshot, 7 Aug 2026: the two hotel-booking buttons' text was
   invisible — gold-on-gold. This rule (specificity .ch-body + a + two :not()s,
   comfortably beating a single .board-btn class) was already excluding
   .ghost-btn for the exact same reason and simply predates .board-btn ever
   living inside .ch-body; it needed the same exclusion, not a new override
   bolted on somewhere else. */
.ch-body a:not(.ghost-btn):not(.ch-pager a):not(.board-btn){color:var(--gold-soft)}

/* Information as TYPOGRAPHY. This is what replaced the boxed .info-row: a
   definition list separated by hairlines. Same content, no rectangles. */
.spec{margin:1.6rem 0 0;border-top:1px solid rgba(246,193,119,.16)}
.spec > div{display:flex;gap:1.6rem;align-items:baseline;justify-content:space-between;
  padding:1.15rem .2rem;border-bottom:1px solid rgba(246,193,119,.16)}
.spec dt{flex:0 0 auto;font-family:var(--font-body);text-transform:uppercase;
  letter-spacing:.24em;font-size:.6rem;color:var(--gold-soft);padding-top:.25em}
.spec dd{flex:1 1 auto;text-align:right;font-family:var(--font-body);font-weight:300;
  font-size:.98rem;line-height:1.7;color:var(--ivory)}
@media(max-width:560px){
  .spec > div{flex-direction:column;gap:.45rem}
  .spec dd{text-align:left}
}

/* A note still to be written. Was a dashed box; now it's a marginal aside —
   one hairline, no container. */
.ch-note{margin:1.6rem 0;padding:.2rem 0 .2rem 1.5rem;
  border-left:1px solid rgba(246,193,119,.3);
  font-family:var(--font-display);font-style:italic;font-size:.98rem;line-height:1.85;
  color:var(--sand);opacity:.78}
.ch-note b{display:block;font-family:var(--font-engrave);font-style:normal;font-weight:500;
  font-size:.58rem;letter-spacing:.26em;text-transform:uppercase;
  color:var(--gold-soft);opacity:.9;margin-bottom:.5rem}

/* A photograph inside the reading flow: edge to edge, masked top and bottom
   so it surfaces out of the page and sinks back in. No frame, no radius, no
   shadow — a held shot, not an inserted rectangle. */
.ch-plate{position:relative;width:100%;height:clamp(300px,62svh,660px);overflow:hidden;
  margin:clamp(3.5rem,9vh,7rem) 0;
  -webkit-mask-image:linear-gradient(180deg,transparent 0,#000 15%,#000 85%,transparent 100%);
  mask-image:linear-gradient(180deg,transparent 0,#000 15%,#000 85%,transparent 100%)}
.ch-plate img{width:100%;height:100%;object-fit:cover;object-position:var(--focal,center)}

/* ------------------------------------------------------------------
   PORTRAIT PHOTOGRAPHS GET A PORTRAIT.
   Five of the six pictures of the couple are phone portraits (3:4, and one
   at 0.46). The plate above is a landscape band — on a laptop it is close to
   2:1 — so `object-fit:cover` was keeping barely a quarter of each picture's
   height and throwing the rest away. In several of them that quarter did not
   contain the faces: in the pavilion shot the couple sit at 63% of frame
   height, so a centred crop cut straight through them. That is the "half,
   faces not seen, not proportionate" note exactly.

   No focal point can fix that on its own — a 3:4 picture stretched edge to
   edge simply has nowhere to put the other three quarters. So a portrait is
   given the shape it was taken in: centred, whole, at its own proportions,
   with air around it.

   This is not the "framed image / boxed layout" the brief rules out. There is
   no border, no panel, no rectangle behind anything, no overlap — just the
   photograph sitting on the page's own ground, which is how a magazine sets a
   portrait. The soft drop is depth, not a frame.
   ------------------------------------------------------------------ */
/* --plate-w lets an unusually tall crop come in narrower so that showing it
   whole doesn't turn it into a 1100px column. 520px suits a 3:4; the 0.46
   crop on the story page asks for less. */
.ch-plate.tall{
  width:min(var(--plate-w,520px),86vw);height:auto;margin-inline:auto;
  border-radius:2px;
  box-shadow:0 34px 70px -34px rgba(0,0,0,.75);
  -webkit-mask-image:none;mask-image:none}
.ch-plate.tall img{width:100%;height:auto;object-fit:contain}

/* A pair reads as a spread: two portraits side by side, each in a portrait
   cell rather than a letterbox one. The duo used to be a single 2:1 band split
   down the middle, which cropped both halves as hard as the single plates. */
.ch-plate.duo{display:grid;grid-template-columns:1fr 1fr;
  gap:clamp(.5rem,1.6vw,1.3rem);
  width:min(940px,92vw);height:auto;margin-inline:auto;
  -webkit-mask-image:none;mask-image:none}
.ch-plate.duo img{width:100%;height:auto;aspect-ratio:3/4;object-fit:cover;
  border-radius:2px;box-shadow:0 26px 56px -30px rgba(0,0,0,.7)}
.ch-plate figcaption{position:absolute;left:0;right:0;bottom:clamp(1.2rem,4vh,2.4rem);
  text-align:center;font-family:var(--font-engrave);font-size:.6rem;letter-spacing:.3em;
  text-transform:uppercase;color:#f4e0b8;text-shadow:0 2px 14px rgba(0,0,0,.85)}

/* the poem keeps one voice the whole way down */
/* The stanza gap is margin PLUS the leading either side of it, and at
   line-height:2 that leading is already ~1em on each edge. 1.6em of margin on
   top of it opened a gap nearly twice the height of a blank line — the hole the
   client circled between "to the heavens above," and "the Universe was writing
   a story of its own" (5 Aug 2026, "space here"). 0.8em still reads clearly as
   a stanza break without the chasm. */
.poem-full p,.poem-full .lead{font-family:var(--font-display);font-style:italic;font-weight:400;
  font-size:clamp(1.08rem,2.5vw,1.36rem);line-height:2;color:var(--ivory);margin:.8em 0;
  text-align:center}
.poem-full em{font-style:italic;color:var(--gold-soft)}
.poem-full .stanza-break{height:.8em}
.poem-full .signature{font-family:var(--font-script);font-style:normal;font-size:2.2rem;
  color:var(--gold-soft);margin-top:1.6em;text-align:center}

/* ---------- scroll reveal for body blocks ----------
   Deliberately NOT `.rise{opacity:0}` by default. Hiding real copy in CSS and
   relying on JavaScript to bring it back means any failure of that JS — a
   thrown error, a browser without IntersectionObserver, an embedded webview
   that never composites and so never reports an intersection — leaves the
   guest on a chapter of blank page. That is the worst failure this site could
   have, and it is entirely avoidable.

   So the copy is visible by default. Only once nav.js has confirmed the
   observer is genuinely alive does it arm `reveal-armed` on <html>, and it
   disarms again if the observer ever goes quiet. Worst case the animation is
   skipped; the words are always there. */
/* The blur that used to ride along with this reveal is gone, for the same
   reason as .fx on the homepage: animating a filter over text re-rasterises
   every glyph on every frame, and these pages are nothing but blocks of text.
   Opacity and transform give the identical rise for no measurable cost. */
.rise{opacity:1;transform:none;
  transition:opacity 1300ms ease,transform 1300ms cubic-bezier(.2,.7,.2,1)}
html.reveal-armed .rise{opacity:0;transform:translateY(28px)}
html.reveal-armed .rise.seen{opacity:1;transform:none}

/* ---------- chapter pager: always a way onward, always a way back ---------- */
.ch-pager{position:relative;z-index:2;display:flex;gap:1rem;justify-content:space-between;
  align-items:stretch;max-width:940px;margin:0 auto;padding:clamp(3rem,8vh,6rem) 7vw clamp(4rem,10vh,7rem);
  border-top:1px solid rgba(246,193,119,.14)}
.ch-pager a{flex:1 1 0;display:flex;flex-direction:column;gap:.4rem;text-decoration:none;
  padding:1.4rem .2rem;transition:opacity .25s ease,transform .25s ease;opacity:.85}
.ch-pager a:hover{opacity:1;transform:translateY(-2px)}
.ch-pager a.next{text-align:right;align-items:flex-end}
.ch-pager .dir{font-family:var(--font-body);text-transform:uppercase;letter-spacing:.3em;
  font-size:.56rem;color:var(--gold-soft)}
.ch-pager .name{font-family:var(--font-display);font-size:clamp(1.15rem,3.4vw,1.6rem);color:var(--ivory)}
.ch-pager a.prev .dir::before{content:"← "}
.ch-pager a.next .dir::after{content:" →"}
@media(max-width:560px){
  .ch-pager{flex-direction:column;gap:0}
  .ch-pager a{padding:1.25rem .2rem}
  .ch-pager a + a{border-top:1px solid rgba(246,193,119,.14)}
  .ch-pager a.next{text-align:left;align-items:flex-start}
}

/* ---------- persistent back control ----------
   The note asked for a way back from one page to another. The menu was the
   only route and it costs two taps; this is one, it's in the thumb's reach on
   a phone, and it's in the same place on every chapter. */
.page-back{position:fixed;top:max(18px,env(safe-area-inset-top));left:max(18px,env(safe-area-inset-left));z-index:1300;
  display:inline-flex;align-items:center;gap:.5rem;height:46px;padding:0 1.05rem 0 .85rem;
  border-radius:30px;text-decoration:none;
  font-family:var(--font-body);font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;
  /* see the .music-toggle note — same square-instead-of-circle failure */
  color:var(--gold-soft);background:rgba(20,12,25,.66);
  border:1px solid rgba(246,193,119,.4);
  transition:border-color .3s ease,background .3s ease,transform .25s ease}
.page-back:hover{border-color:var(--gold);background:rgba(20,12,25,.6);transform:translateX(-2px)}
.page-back:active{transform:scale(.94)}
.page-back svg{width:15px;height:15px;flex:0 0 auto}
@media(max-width:560px){
  .page-back{padding:0;width:46px;justify-content:center}
  .page-back .label{display:none}    /* the arrow alone on a phone */
}

/* ---------- FAQ: atmosphere, not another photograph ----------
   The note was explicit that this page should NOT use a sixth photo — palm
   shadows, stone, linen, cloud. So the hero here has no image at all: it's a
   graded sky with palm fronds thrown across it as shadow, drifting the way
   they do on a wall in the afternoon. Fronds are generated in nav.js. */
/* Client, 7 Aug 2026, screenshot of this exact hero: "Empty space lag Rahi"
   (feels like empty space). It reads that way because the shadow logic never
   actually had contrast to work with: a shadow this dark (rgba(3,22,30,…),
   near-black) only registers against a SUNLIT surface — "the way they drift
   across a wall in the afternoon" per the note below — but the wall itself
   was a dark night-teal gradient, so the fronds were close to invisible
   against it, especially the top ~40% of the hero where the top-left
   highlight barely reached. Two changes, same shadow-on-linen concept,
   actually working this time: the highlight radial is brighter and wider so
   there is a real lit surface up there, and the fronds themselves are darker
   (more contrast, not just more paint) against it. */
.palm-sky{position:absolute;inset:0;z-index:0;overflow:hidden;
  background:
    radial-gradient(135% 100% at 26% 4%,rgba(140,225,208,.34),transparent 66%),
    radial-gradient(110% 80% at 82% 88%,rgba(20,90,110,.55),transparent 62%),
    linear-gradient(168deg,#12506A 0%,#0A3049 42%,#07202C 100%)}
.palm-sky .frond{position:absolute;transform-origin:0% 50%;
  color:rgba(2,16,22,.62);
  filter:blur(2.5px);
  animation:frondSway ease-in-out infinite alternate}
.palm-sky .frond svg{display:block;width:100%;height:auto;overflow:visible}
.palm-sky .frond path{fill:currentColor}
/* a second, softer cast further from the "light" reads as the penumbra */
.palm-sky .frond.soft{color:rgba(2,16,22,.36);filter:blur(9px)}
@keyframes frondSway{
  0%{transform:rotate(var(--fr0,-3deg)) translate3d(0,0,0)}
  100%{transform:rotate(var(--fr1,3deg)) translate3d(1.2%,.8%,0)}
}
/* woven linen, very faint — the "paper" under the shadows */
.palm-sky::after{content:"";position:absolute;inset:0;pointer-events:none;opacity:.5;
  mix-blend-mode:soft-light;
  background-image:
    repeating-linear-gradient(90deg,rgba(255,245,225,.05) 0 1px,transparent 1px 3px),
    repeating-linear-gradient(0deg,rgba(255,245,225,.04) 0 1px,transparent 1px 4px)}


/* ============================================================
   TOUCH DEVICES — input zoom + tap targets
   Both orientations: a phone held sideways has the same fingers.
   ============================================================ */
@media(max-width:900px){
  /* 16px is a hard threshold, not a preference: iOS Safari zooms the whole
     page in when a focused field is smaller, and the guest then has to pinch
     back out to keep filling in the form. This was the RSVP form's worst
     mobile behaviour. */
  .rsvp-card input,.rsvp-card select,.rsvp-card textarea{font-size:16px}
  /* ~44px minimum touch targets — these all sat at 38-43px */
  .pill-opt{padding:.85rem 1rem}
  .meal-chip{padding:.87rem 1.15rem}
  /* Keeps the 44px touch target while holding the label on ONE line: at 375px
     the old 30px flanks pushed "Journey" onto a second row, which read as the
     same orphaned-word fault as the reveal line. */
  .bp-cta .pulse{padding:15px 22px;letter-spacing:.15em}
  .change-guest{padding:.4rem 0}
  /* stop iOS inflating text in the rotated layout */
  html{-webkit-text-size-adjust:100%;text-size-adjust:100%}
}
/* 320px phones: the card is only 282px wide, so the label needs one more step
   down to stay on a single line. Vertical padding is untouched — the 44px
   touch target is not negotiable. */
@media(max-width:400px){
  .bp-cta .pulse{font-size:.7rem;letter-spacing:.11em;padding:15px 16px}
}


/* ============================================================
   PHONES & TABLETS, PORTRAIT — full-bleed portrait art
   ------------------------------------------------------------
   There are now 1:2 portrait cuts of all six heroes, shaped for this screen,
   so the phone gets the SAME treatment as desktop rather than a reduced one:
   the picture fills the viewport edge to edge, copy sits over it, and
   chapters dissolve into each other exactly as they do on a laptop.

   `cover`, not `contain`: a 1:2 image in a 0.45-0.56 viewport would letterbox
   by up to 11% under contain — the gaps. Cover fills completely and trims
   that same ~1-11% instead, which the art's safe margins are cut to absorb.

   Everything else — parallax, ken-burns, grade, seam blending, the copy
   scrim — is inherited unchanged. That's the point: no separate mobile
   design to keep in sync, just different artwork in the same machine.
   ============================================================ */
@media(max-width:900px) and (orientation:portrait){
  /* .plx's oversize + drift exist to give a letterboxed plate room to travel.
     Filling the frame there is no slack, so any drift would pull the photo
     off an edge — the scene holds still and sits flush instead. */
  .plx{inset:0;transform:none}
  .photo::before{display:none}       /* the bed can't show through a filled frame */
  .chapter .photo::after{
    inset:0;
    background-image:var(--photo-m);
    background-size:cover;
    filter:saturate(1.03)}           /* the glow only ever fell outside the viewport */

  /* THE GLINT COMES OFF ON PHONES.
     Client screen recording, 5 Aug 2026: scrolling the live site on an Android
     phone tore into horizontal streaks and stale rectangular blocks, with
     whole hero photographs failing to paint. That is a compositor running out
     of room, and the cheapest room to give it back is layer COUNT.

     Each .goldglint is a viewport-sized layer carrying a mask plus an animated
     glow, and Home stacks one per chapter — seven of them, on top of the seven
     masked .scene layers that create the chapter seams. Dropping them here
     halves that.

     Nothing is lost from the picture: this gold is PAINTED INTO the plate, so
     the cloud scrolls, gulls and shell line are all still there. The only
     thing that goes is the slow glow laid over them, which is the least
     visible effect on the site on the device with the least to spend on it.
     Scoped to .chapter, so the inner pages — one hero each, no stacking —
     keep theirs. */
  .chapter .goldglint{display:none}

  /* THE FOG DROPS A LAYER ON PHONES TOO.
     Client screen recording, 5 Aug 2026 (this time on the Welcome → Reveal
     seam): the same compositor tearing as the goldglint bug above — a hard
     rectangular seam of stale, mismatched sky tiles mid-scroll, textbook tile
     eviction. This transition is the single heaviest moment on the whole
     site: Welcome and Reveal are BOTH `.live` while the guest is mid-scroll
     between them (the crossfade needs that), so the yacht dolly, its
     parallax, and the Udiverse word's glitter burst are all still painting
     at the exact same frame that Reveal's three-layer `.mist` fog bank comes
     alive — each `.mist i::before` is a ~0.25MP layer carrying a 14px blur,
     three of them stacked, arriving on top of everything Welcome was already
     spending. No other chapter boundary stacks this many blurred layers at
     once.

     Dropping the third fog band on phones removes one whole blurred layer
     right at the worst moment, the same lever the goldglint fix above pulls.
     Two banks still clear in a stagger (the delay on the second is what
     reads as "lifting", not a single flat sheet), so the arch still emerges
     through mist — just not through quite as much of it. */
  .mist i:nth-child(3){display:none}

  /* Waterline correction, portrait cut — see the note by #ch-day1 .content.
     Cover at exactly the chapter box with no parallax puts this plate's
     horizon at a known 48.1% of the screen, so these are smaller and exact. */
  #ch-day1 .content{transform:translateY(-3.5svh)}
  #ch-rsvp .content{transform:translateY(-10svh)}

  /* Off the subject, portrait cut — see the note by #ch-welcome .content.
     Here BOTH plates open with half a screen of clean sky (yacht from 43%,
     arch crown from 43%), so both blocks move up into it and no veil is needed
     on either. The reveal is lifted further only because its block is twice as
     tall and it is the BOTTOM edge that has to clear the subject. */
  /* Welcome needs no portrait override — it is bottom-anchored, so it tracks
     the plate on its own. Only the reveal, which is still centred-and-lifted,
     has a number here. */
  /* -23svh was tuned to just clear the arch; client note 6 Aug 2026 ("move
     Bali Indo up so it's visible") is about that exact bottom edge — on a
     taller/narrower phone than this was tuned against, .rw-place still lands
     close enough to the arch's crown to feel crowded. A few more svh of lift
     buys it clean sky underneath with no other line at risk: reveal-pre sits
     highest and still has the star-track (ends ~34px) well below it. */
  #ch-reveal  .content{transform:translateY(-27svh)}
  /* On flat sky the type has nothing to fight, so the landscape veil comes off
     and the site's default soft pool is all that is left behind the words. */
  #ch-reveal .content::before{inset:-30% -24%;filter:blur(34px);
    background:radial-gradient(ellipse 74% 78% at 50% 50%,rgba(3,26,44,.46),rgba(3,26,44,.24) 46%,transparent 76%)}

  /* Chapter-page heroes get the same treatment. They're already cover and
     already inset:0, so all that's needed is the portrait cut — but it has to
     be said explicitly, because the rule above is scoped to .chapter and a
     .ch-hero is not one. Without this the phone would stretch the landscape
     plate across a 1:2 frame, which is the single most visible way to make a
     hero look cheap. The fallback keeps FAQ (no photo) and any future hero
     without a portrait cut working. */
  .ch-hero .photo::after{background-image:var(--photo-m,var(--photo))}
  /* and the banner's ink mask switches cut with it */
  .ch-hero .goldglint{
    -webkit-mask-image:var(--ink-m,var(--ink));mask-image:var(--ink-m,var(--ink))}

  /* a full-height hero plus a full-height plate is a lot of scrolling before
     any words on a phone — the landscape plates come down to something readable */
  .ch-plate{height:clamp(240px,42svh,420px);margin:clamp(2.5rem,6vh,4rem) 0}
  /* Portrait plates keep their own proportions: a phone screen IS a portrait,
     so these need no shortening — that is the one place the pictures were
     always going to sit correctly, and squashing them back into a band here
     would reintroduce the crop on the very device most guests will use. */
  .ch-plate.tall{height:auto;width:min(520px,90vw)}
  /* A pair side by side on a 375px screen is two thumbnails — stack them. */
  .ch-plate.duo{grid-template-columns:1fr;grid-auto-rows:auto;height:auto;
    width:min(520px,90vw);gap:clamp(1rem,4vw,1.6rem)}
}
