:root {
  /* The brief's six, verbatim, plus five grounds the six do not cover. Prefixed `--gr-`
     so they can never collide with the Lantern Palace slot aliases below.
     THIS PREFIX IS NOT COSMETIC. Marathi shipped a palette that declared `--gold` a
     second time in this same block — LP already uses that name for the ROUTE room's
     ground — and the later declaration silently won, painting two different rooms the
     same colour. CSS has no duplicate-custom-property warning of any kind. Check a new
     palette's token names against the INHERITED ones, not just against the brief. */
  --gr-whitewash: #FBF9F4;   /* the INVITE ground, and the light ink */
  --gr-aegean:    #2A9D9F;   /* the sea in the sun — an accent, never a ground: at full
                                screen it is a swimming-pool colour and the room stops
                                being a room. The route room takes the DEEP one below. */
  --gr-cobalt:    #1B4E9B;   /* the doors' own blue — the KNOW ground and every deep
                                accent. The one shared paper tile that needed no rotating. */
  --gr-bougainvillea: #D6437E; /* the flower at its own saturation. An accent. The
                                functions room takes --gr-blush, which is this at a
                                wall's worth of scale. */
  --gr-olive:     #6B7A42;   /* olive leaf — an accent and the story room's deep note */
  --gr-gold:      #E9A93B;   /* sunset gold, the METAL — monogram and the countdown.
                                Never a ground: at its own value a room and its gilt are
                                one colour and every rule in it disappears (Punjabi's
                                rule). It is also 1.96:1 or worse on all four LIGHT
                                grounds, so it is not the hairline colour either — see
                                the rules block further down. */
  /* five derived grounds the six do not cover */
  --gr-limewash:  #F0E7D6;   /* the RSVP ground: whitewash with the afternoon on it */
  --gr-blush:     #EFB9CE;   /* the FUNCTIONS ground: bougainvillea, but a wall of it */
  --gr-aegean-deep: #17707A; /* the ROUTE ground: the sea in the caldera's shadow */
  --gr-sage:      #C9CBA0;   /* the STORY ground: an olive leaf's pale side */

  /* THE SEVEN GROUNDS ARE A LUMINANCE LADDER, and that is Ballroom's repair applied
     rather than rediscovered. Read in PAGE order — invite, functions, route, story,
     rsvp, know, countdown — they run
        .97 · .83 · .28 · .71 · .89 · .36 · .12
     and no two ADJACENT rooms are within .14 of each other. Adjacency is the thing that
     matters: invite (.97) and rsvp (.89) are close and they are four rooms apart, so
     nobody ever sees them in one scroll. Change one and check the whole ladder. */
  --gr-night:     #0C1B33;   /* the caldera after the last ferry — gate, footer,
                                countdown. The countdown takes the pale OLIVE tile at 24%
                                multiply, and a near-black navy ground keeps the little
                                that lands inside the skin's own family. */

  /* the rooms — LP slot names, Santorini values, each matched to its paper tile */
  --teal:      var(--gr-whitewash);   /* paper-teal tile is paper-white */
  --teal-deep: #EFEAE0;
  --pink:      var(--gr-blush);       /* paper-pink tile is paper-bougainvillea */
  --pink-deep: #D98FAE;
  --gold:      var(--gr-aegean-deep); /* paper-marigold tile is paper-aegean */
  --gold-deep: #0E4D55;
  --zari:      var(--gr-gold);        /* the warm metal, where LP had gold */
  --zari-deep: #C1801F;
  --indigo:    var(--gr-sage);        /* the story room */
  --indigo-lo: var(--gr-night);
  --cream:     var(--gr-limewash);
  --cream-deep:#E3D6BE;
  --rose:      var(--gr-blush);

  /* Ink measured, not assumed. Near-black Aegean navy #14243D is 14.79:1 on whitewash,
     12.68:1 on limewash, 9.26:1 on blush and 9.29:1 on sage olive.
     THREE of the seven grounds here are DARK — one more than Ballroom's two and the
     first time `room--palm` is one of them, because on every skin before this one that
     class held a light gold and Ballroom's stylesheet deliberately kept it out of both
     dark selectors. Here it is cobalt: the ink on it is 1.93:1 and fails outright, so
     it takes --on-dark (7.65:1) and it is IN both selectors below.
     The METAL is the other half of the trap and it is worse here than on Ballroom:
     --gr-gold is a sunset amber rather than a pale champagne, so it measures 1.96:1 on
     whitewash, 1.68:1 on limewash, 1.23:1 on blush and 1.23:1 on sage — invisible on
     every light room — and only 2.81:1 on aegean and 3.91:1 on cobalt. It carries no
     hairline anywhere. It is kept for the gate monogram and the countdown, where it is
     8.36:1. */
  --ink:        #14243D;
  --ink-soft:   #3A4C68;
  --on-dark:    #FBF9F4;
  --on-dark-soft:#DCE8EA;

  /* Bodoni Moda for display — a Didone cut from Giambattista Bodoni's own punches, so
     the hairline serifs and the vertical stress are the letterform Mediterranean resort
     print has been set in for two centuries. Jost for caps and body: a geometric
     grotesque on Futura's skeleton, wide open counters, which is signage and timetable
     lettering and reads clean at 11px on a phone.
     Neither face is inherited. Marcellus, which the eight Indian skins share, is a
     Trajan-derived Roman for stone-carved dedications; Cormorant Garamond is a
     sixteenth-century Garalde and Ballroom's Josefin Sans is 1920s Art Deco. A Didone
     and a geometric grotesque are the one pairing on this engine that is neither
     devotional nor black-tie — it is a travel magazine, which is what a destination
     wedding invitation actually is. */
  --serif:  "Bodoni Moda", "Didot", Georgia, "Times New Roman", serif;
  --body:   "Jost", "Avenir Next", "Helvetica Neue", Arial, sans-serif;
  --caps:   "Jost", "Avenir Next", "Helvetica Neue", Arial, sans-serif;

  --wrap: 1080px;
  --edge-h: clamp(46px, 6vw, 108px);
  --pad-y: clamp(72px, 11vh, 150px);
  --pad-x: clamp(20px, 5vw, 56px);

  /* Gate geometry — the box the two door-leaf plates are drawn into, as a % of the gate
     plate. Both leaves were cut onto ONE shared 577x892 canvas at (1009,326) of the
     2560x1440 plate, so they take the same rect and cannot drift apart, and each canvas
     edge IS its hinge.
     These are defaults: main.js overrides them from ../assets/manifest.json's
     `door_rig.rect_norm` when it is present, so a repainted gate needs no edit here.
     From rect_norm x0 .3941 x1 .6195 y0 .2264 y1 .8458. The desktop leaves are ARCHED,
     so the rect is the box and the PNG's own alpha is the shape. */
  --gate-door-left:  39.41%;
  --gate-door-top:   22.64%;
  --gate-door-w:     22.54%;
  --gate-door-h:     61.94%;
  --gate-hinge-l:    0%;
  --gate-hinge-r:    100%;
}

/* The phone gate is a different painting with a different door rect, so it gets
   its own defaults. main.js overwrites all four from assets/manifest.json's
   `door_rig.mobile` once that fetch lands; these keep the first painted frame
   correct in the meantime. From rect_norm x0 .2583 x1 .7431 y0 .3879 y1 .8539. */
@media (max-width: 759px) {
  :root {
    --gate-door-left: 25.83%;
    --gate-door-top:  38.79%;
    --gate-door-w:    48.48%;
    --gate-door-h:    46.60%;
  }
}

*, *::before, *::after { box-sizing: border-box; }

html { scroll-behavior: auto; }

body {
  margin: 0;
  background: var(--indigo-lo);
  color: var(--ink);
  font-family: var(--body);
  font-size: clamp(16px, 1.05vw + 12px, 19px);
  /* Marcellus is a Roman with a modest x-height and long extenders, so it takes
     a touch more leading than Josefin Sans did */
  line-height: 1.72;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

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

a { color: inherit; }

.skip {
  position: fixed; top: -100px; left: 16px; z-index: 90;
  background: var(--cream); color: var(--ink);
  padding: 10px 18px; border-radius: 999px; text-decoration: none;
  font-family: var(--caps); font-size: 14px;
}
.skip:focus { top: 16px; }

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

/* T-GS: T-TM's bilingual block (.ta / .en / .bi and the two Tamil faces) is
   gone. This skin is monolingual English, so nothing has to cancel a
   text-transform or a tracking to stay legible. */

/* ------------------------------------------------------------------
   type
   ------------------------------------------------------------------ */
.eyebrow {
  font-family: var(--caps);
  font-size: clamp(11px, 1.1vw, 14px);
  letter-spacing: .26em;
  text-transform: uppercase;
  margin: 0 0 .9em;
  opacity: .78;
}
.room-title {
  font-family: var(--caps);
  font-weight: 400;
  font-size: clamp(30px, 5.4vw, 62px);
  letter-spacing: .1em;
  text-transform: uppercase;
  line-height: 1.12;
  margin: 0 0 .35em;
}
.room-sub {
  font-size: clamp(16px, 1.7vw, 21px);
  font-style: italic;
  opacity: .82;
  margin: 0 auto;
  max-width: 46ch;
}
.center { text-align: center; }
.wrap {
  position: relative; z-index: 3;
  width: min(var(--wrap), 100% - var(--pad-x) * 2);
  margin-inline: auto;
}
.wrap--narrow { width: min(720px, 100% - var(--pad-x) * 2); }
.room-head { margin-bottom: clamp(36px, 6vw, 72px); }

.rule {
  width: 130px; height: 1px; margin: clamp(22px,3vw,34px) auto;
  background: currentColor; opacity: .32;
}

/* ------------------------------------------------------------------
   buttons
   ------------------------------------------------------------------ */
.btn {
  display: inline-block;
  font-family: var(--caps);
  font-size: clamp(12px, 1.2vw, 15px);
  letter-spacing: .18em;
  text-transform: uppercase;
  text-decoration: none;
  padding: 14px 30px;
  border-radius: 999px;
  border: 1.5px solid currentColor;
  background: var(--ink);
  color: var(--cream);
  border-color: var(--ink);
  cursor: pointer;
  transition: transform .25s ease, background .25s ease, color .25s ease;
}
.btn:hover { transform: translateY(-2px); }
.btn--ghost { background: transparent; color: inherit; border-color: currentColor; }
.cta-row {
  display: flex; flex-wrap: wrap; gap: 14px;
  justify-content: center; align-items: center;
  margin-top: clamp(26px, 4vw, 42px);
}

.mobcta {
  position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%) translateY(140%);
  z-index: 60;
  background: var(--rose); color: var(--cream);
  font-family: var(--caps); letter-spacing: .2em; text-transform: uppercase;
  font-size: 13px; padding: 14px 40px; border-radius: 999px; text-decoration: none;
  box-shadow: 0 10px 30px rgba(12,27,51,.35);
  transition: transform .5s cubic-bezier(.2,.8,.2,1);
}
.mobcta.is-in { transform: translateX(-50%) translateY(0); }
.mobcta.is-hid { transform: translateX(-50%) translateY(140%); } /* the real buttons are on screen — main.js */
@media (min-width: 760px) { .mobcta { display: none; } }

/* ==================================================================
   1 + 2. THE JOURNEY — sky, push-through, doors
   ================================================================== */
.journey {
  position: relative;
  height: 340vh;               /* JS shortens this when motion is reduced */
  background: #1A1014;   /* near-black burgundy, behind the ballroom */
}
.stage {
  position: sticky; top: 0;
  height: 100vh; height: 100svh;
  overflow: hidden;
  perspective: 1500px;
  transform-style: preserve-3d;
}

.sky, .courtyard {
  position: absolute; inset: -6% -6% -6% -6%;
  background-size: cover; background-position: center bottom;
  will-change: opacity, transform;
}
.sky--dusk  { background-color: #EFD9A8; }   /* champagne bokeh, chandelier warmth */
.sky--night { background-color: #1A1014; opacity: 0; }
/* the painted night stars are large; holding the plate under full opacity keeps
   the dusk warmth showing through and stops them reading as confetti */
.courtyard  { opacity: 0; transform: scale(1.25); background-position: center; }

.lyr {
  position: absolute; left: 50%; bottom: -2%;
  width: 128%; transform: translateX(-50%);
  transform-origin: 50% 100%;
  will-change: transform;
}
.lyr img { width: 100%; }

/* the cloud plane: the slowest thing in the hero, slower than palace-back */
.lyr--cloud {
  bottom: auto; top: 0; height: 74%;
  width: 118%;
  transform-origin: 50% 60%;
}
/* T-GS: half T-TM's size and two thirds its opacity, and both pushed off the
   centre line. The Goan cloud plate is a single big cumulus rather than a thin
   wisp, so at 30% width it sat directly behind the names as a pale blob that
   read as fog on the lens — it is the thing that looked like a broken scrim in
   the first pass, not the scrim. */
.cloud { position: absolute; width: auto; will-change: transform; }
.cloud--a { top: 3%;  left: -11%; width: 20%; opacity: .22; }
.cloud--b { top: 15%; right: -9%; left: auto; width: 15%; opacity: .18; }
/* These four numbers are the whole desktop hero, and they are not free.
   `.lyr` sets a percentage WIDTH and bottom-aligns; `.lyr img` is width:100%.
   So a band's drawn height is (lyr width in px) x band_over_width, and the
   plate's own HEIGHT never enters it (assets/manifest.json -> hero_layers).
   T-BL: palace-back is 0.324 and palace-mid is 0.317. BOTH are true bands here —
   feathered at the top, painted to their own bottom edge — so unlike Punjabi's
   full-height proscenium neither needs a hazetop.py ramp, and hazetop.py is
   shipped in this template but never run. The edge that bites is the BOTTOM one;
   see the block immediately below. Both stay far past 100% so their own left and
   right cut edges fall off-screen too. Composited at 1440x900. */
/* Settled by compositing with hero.py at 1440x900, not inherited. THE ONE CONSTRAINT
   that is not obvious: all four band plates are painted to their own BOTTOM edge, so
   each ends in a hard horizontal cut, and the BACK band's cut is the one that shows.

   Two attempts tried to COVER it with the mid band and both failed, because a near band
   is mostly transparent in its upper half — it is rose urns and candles along the
   bottom, not a wall. At `bottom: 22%` the cut landed at y 702 and ran as a plainly
   visible ruled line across the outer thirds of the hero, through the champagne tower
   and the candelabra, with the mid band both wider and higher than it. It passed every
   check that did not involve looking at the screenshot.

   The fix is arithmetic and not layering. A band's bottom edge sits at
   (viewport height) - (bottom% x viewport height) NO MATTER WHAT ITS WIDTH IS, so the
   only way to get the cut off the screen is a bottom at or below zero — and the height
   that is then lost is bought back with WIDTH. 178% puts back's cut at y 927 with its
   top still at y 97. Neither top edge needs a hazetop.py ramp: all four plates arrive
   feathered, unlike Punjabi's proscenium. */
.lyr--back { bottom: -3%; width: 178%; opacity: .9; }
.lyr--mid  { bottom: -14%; width: 114%; }

/* the gate sits nearest the camera and is what you fly through. It is
   deliberately bottom-heavy: the names live in the open sky above it. */
.gate {
  position: absolute; left: 50%; bottom: -4%;
  /* 56%: palace-mid is a full-height proscenium here, and its pillars and blue
     drapes are big near-camera masses — at Ram Sita's 50% the gateway was no
     longer the subject of its own hero.
     92vh and not Ram Sita's 82vh, for the reason that skin found by shooting
     the real page rather than the composite: at 1440x900 `min(82vh, 56%)`
     resolves to the 82vh arm (738px) and NOT to 56% (806px), so the live gate
     would come out 68px smaller than the frame hero.py signed off — hero.py
     models the percentage arm only. 92vh moves the clamp onto the 56% arm at
     this viewport, which is what was actually composited. */
  /* min(92vh, 52%) and 92 is not decoration. hero.py models the PERCENTAGE arm of this
     clamp and not the vh arm, so the two have to be chosen together: at 1440x900, 92vh
     is 828px and 52% is 749px, the percentage arm wins, and 749px is what was actually
     composited and signed off. Ram Sita shipped a gateway 90px smaller than the frame
     it had approved because min(70vh, 50%) resolved to the vh arm at that viewport. */
  width: min(92vh, 52%);
  aspect-ratio: 2560 / 1440;
  transform: translateX(-50%);
  /* The camera flies into the DOORWAY, not the middle of the plate, so the
     opening stays put on screen while everything else expands past the edges.
     This is the centre of the door rect measured by doors.py and published in
     assets/manifest.json — 50.0% / 63.4%. The leaves are RECTANGULAR here, so
     unlike Punjabi the rect IS the mask: no component labelling, no fill, no
     dilation, and closed reproduces the plate to the pixel. y1 is .8938 because
     the burgundy carpet step crosses their foot and stays on the frame plate.
     Scale is 10.1: the doorway is 22.8% of this plate's width, so the camera has
     less far to travel than Punjabi's 16.8% doorway did and copying that skin's
     13.7 overshoots. main.js reads the number from the manifest. */
  transform-origin: var(--gate-origin, 50.0% 63.4%);
  will-change: transform, opacity;
}
.gate-plate { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; object-position: bottom; }
.gate-plate--open, .gate-plate--frame { opacity: 0; }

/* Lantern Palace's gate plate was keyed — transparent outside the gateway
   silhouette — so it sat on the sky with no edge. Both Tamil plates are OPAQUE
   and full-bleed (manifest: alpha false), so at the small end of the fly-through
   they read as a painted rectangle pasted onto the sky. Feather the edges: the
   gateway genuinely continues past frame, so a soft fall-off reads as aerial
   distance where a hard cut reads as a mistake. The mask scales with the plate,
   so by the time the camera is through the arch the feather is off-screen.
   T-PJ keeps Goa's DEEP top fade: this plate's own sky is painted the same
   golden morning as sky-dusk, so the step is gentler than Goa's orange-on-teal
   was, but the plate is a hard rectangle either way. 22% desktop / 18% phone. */
.gate-plate {
  /* Every plate, not just --shut: main.js shows `.gate-plate--frame` whenever
     both door leaves exist (which they do), and although that plate has the
     arch punched transparent, its OUTER boundary is still the opaque full
     rectangle — so the cut shows on whichever plate is on screen.
     Both axes. The top edge could stay crisp while the gate sat against open
     sky, but palace-mid is a full-width band now, so the gate is an opaque
     painting laid over another opaque painting of the same building — a hard
     cut there reads as a panel pasted into the facade. Feathered on all three
     free edges it reads as depth instead. Bottom stays hard; it is the ground. */
  -webkit-mask-image:
    linear-gradient(90deg, transparent 0, #000 11%, #000 89%, transparent 100%),
    linear-gradient(180deg, transparent 0, #000 22%, #000 100%);
  -webkit-mask-composite: source-in;
          mask-image:
    linear-gradient(90deg, transparent 0, #000 11%, #000 89%, transparent 100%),
    linear-gradient(180deg, transparent 0, #000 22%, #000 100%);
          mask-composite: intersect;
}
@media (max-width: 759px) {
  .gate-plate {
    -webkit-mask-image:
      linear-gradient(90deg, transparent 0, #000 7%, #000 93%, transparent 100%),
      linear-gradient(180deg, transparent 0, #000 18%, #000 100%);
    -webkit-mask-composite: source-in;
            mask-image:
      linear-gradient(90deg, transparent 0, #000 7%, #000 93%, transparent 100%),
      linear-gradient(180deg, transparent 0, #000 18%, #000 100%);
            mask-composite: intersect;
  }
}

.arch-view, .doors {
  position: absolute;
  top: var(--gate-door-top);
  left: var(--gate-door-left);
  width: var(--gate-door-w);
  height: var(--gate-door-h);
  transform-style: preserve-3d;
  /* the .gate ancestor is scaled, which cuts off .stage's perspective — so the
     doors carry their own. Keep it generous or the swing reads as a shear. */
  perspective: 1100px;
}
.door {
  position: absolute; inset: 0;      /* both leaves share the box; each paints its half */
  backface-visibility: hidden;
  will-change: transform;
}
.arch-view {
  background-size: cover;
  background-position: center bottom;
  background-color: #14213F;
  opacity: 0;
}
.door img { width: 100%; height: 100%; object-fit: fill; }
.door--l { transform-origin: var(--gate-hinge-l) center; }
.door--r { transform-origin: var(--gate-hinge-r) center; }

.drift {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  z-index: 4;
}

/* hero type */
.hero {
  position: absolute; inset: 0; z-index: 6;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center;
  /* sits above the skyline, not on it */
  padding: 0 var(--pad-x) clamp(140px, 42vh, 420px);
  color: var(--on-dark);
  text-shadow: 0 2px 26px rgba(12,27,51,.55), 0 1px 3px rgba(12,27,51,.4);
  will-change: opacity, transform;
}
.hero-plate {
  position: relative;
  padding: clamp(26px, 5vh, 60px) clamp(24px, 5vw, 80px);
}
/* T-GS: an ELLIPSE, not `closest-side`. The inherited circle was sized off the
   box's short side, so on a 1440-wide hero it read as a distinct grey disc
   floating in a bright sunset sky — very visible, where over T-TM's dark
   gateway it was invisible. Widened to the shape of the type block, dropped
   in peak alpha, and tinted to the night sky rather than to the ink; the names
   pick the rest of the contrast up from their own shadow below. */
.hero-plate::before {
  content: ""; position: absolute; inset: -8% -4%; z-index: -1;
  background: radial-gradient(ellipse 62% 74% at 50% 48%,
    rgba(12,27,51,.34), rgba(12,27,51,.18) 54%, rgba(12,27,51,0) 88%);
}

.hero-eyebrow {
  text-shadow: 0 1px 14px rgba(12,27,51,.6);
  font-family: var(--caps);
  font-size: clamp(10px, 1.1vw, 13px);
  letter-spacing: .4em; text-transform: uppercase;
  margin: 0 0 clamp(14px, 2.4vw, 26px); opacity: .85;
}
.hero-names {
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(46px, 10.5vw, 132px);
  line-height: .96;
  letter-spacing: .01em;
  margin: 0;
  /* carries most of the legibility now that the scrim behind is light — a
     tight, dark halo reads as evening air rather than as a panel */
  text-shadow: 0 2px 26px rgba(12,27,51,.55), 0 1px 3px rgba(12,27,51,.35);
  display: flex; flex-direction: column; align-items: center; gap: .04em;
}
.hero-weds {
  font-family: var(--caps);
  font-style: normal;
  font-size: clamp(13px, 1.9vw, 22px);
  letter-spacing: .5em; text-transform: lowercase;
  opacity: .82;
  padding-left: .5em;
}
.hero-date {
  font-family: var(--caps);
  font-size: clamp(13px, 1.6vw, 19px);
  letter-spacing: .22em; text-transform: uppercase;
  margin: clamp(20px, 3vw, 34px) 0 6px;
}
.hero-venue { margin: 0; font-style: italic; opacity: .86; font-size: clamp(15px,1.5vw,19px); }
/* "Gurdwara Sri Guru Singh Sabha · Amritsar" is 40 characters and can still
   wrap to two lines at 390px. The eyebrow directly above already carries
   மயிலாப்பூர் · CHENNAI, so the phone drops the duplicate rather than shrinking
   the type to fit. */
@media (max-width: 759px) { .hero-city { display: none; } }
.hero-hint {
  position: absolute; bottom: clamp(24px, 5vh, 54px); left: 0; right: 0;
  margin: 0;
  font-family: var(--caps);
  font-size: clamp(10px, 1vw, 12px);
  letter-spacing: .3em; text-transform: uppercase;
  opacity: .95;
  display: flex; flex-direction: column; align-items: center; gap: 12px;
}
/* the hint lands over the gate, so it carries its own scrim */
.hint-pill {
  background: rgba(12,27,51,.42);
  border: 1px solid rgba(247,242,230,.22);
  border-radius: 999px;
  padding: 9px 20px;
  backdrop-filter: blur(3px);
}
/* On a 390px screen "Scroll to open the doors" at .3em tracking measures about 330px,
   so the pill ran the full width of the viewport, its radius disappeared and it read as
   a grey bar laid across the doors rather than as a button. Josefin Sans is a wide
   geometric face and this skin's hint is four words where Punjabi's was five short ones,
   which is why the inherited numbers held there and not here. The tracking comes in and
   the pill keeps its shape; the copy is unchanged, because config.scrollHint is
   record-driven and a phone-only abbreviation would have to live in CSS anyway. */
@media (max-width: 480px) {
  .hero-hint { font-size: 9.5px; letter-spacing: .18em; }
  .hint-pill { padding: 8px 16px; }
}
/* ------------------------------------------------------------------
   9:16 — the phone hero
   ------------------------------------------------------------------
   `.lyr` sizes these bands by WIDTH and bottom-aligns them, so the only ratio
   that matters is band-height over plate-WIDTH; plate height never enters it.
   At 9:16 the same "bottom 40% of the frame" is 1.8x taller relative to width
   than at 16:9, which is how the inherited `.lyr--mid { width: 160% }` ended up
   filling the whole phone viewport with facade and burying the dawn sky.
   The 9:16 bands here are both painted low (0.638 and 0.654 band/width), and
   these two pairs are the asset side's A/B at 390x844
   (manifest.json → hero_layers.recommended_css_mobile).
   ------------------------------------------------------------------ */
@media (max-width: 759px) {
  /* palace-mid-9x16 is 0.862 and palace-back-9x16 is 0.635, and BOTH arrive
     feathered at the top and painted to their own bottom edge, so neither needs
     a haze ramp. The bottom-cut rule from the desktop block applies here and is
     solved the same way: back sits at -3% so its hard bottom edge falls below an
     844px fold entirely, and 210% buys back the height that costs. The mid band
     is pushed out to 160% so only its rose urns and candles show at the two
     bottom corners, where they read as foreground. Composited at 390x844 with
     hero.py. */
  .lyr--back { width: 210%; bottom: -3%; opacity: .85; }
  .lyr--mid  { width: 160%; bottom: -10%; }

  /* The phone gate is a SEPARATE PORTRAIT PAINTING (palace-gate-9x16, 941x1672),
     not a crop of the 16:9 one — a 9:16 cover-crop of the wide gateway keeps the
     doors and loses both wall sconces, both rose urns, the burgundy
     dado and the whole marble stair, i.e. every Punjabi recogniser except the
     door, at the exact moment the gate is the whole screen.
     Because the plate is portrait, the BOX has to be portrait too: the door
     rect is a percentage of this box, and a 16:9 box would letterbox the
     portrait plate inside it and slide the doors off the opening.
     86vw, and it is not Punjabi's 84 by inheritance: this doorway is 47.8% of its
     own plate against 35.1% there, so the gate needs the extra width to fill the
     phone the way the desktop one fills 1440. */
  .gate {
    width: 86vw;
    aspect-ratio: 1440 / 2560;
    /* centre of door_rig.mobile's rect, not the desktop rect. main.js overwrites
       --gate-origin from the manifest once it loads; this is the default. */
    --gate-origin: 50.0% 60.9%;
  }
}

.hint-arrow {
  width: 1px; height: 34px; background: currentColor;
  transform-origin: top;
  animation: hint 2.1s ease-in-out infinite;
}
@keyframes hint {
  0%, 100% { transform: scaleY(.35); opacity: .35; }
  50%      { transform: scaleY(1);   opacity: .95; }
}

/* ==================================================================
   ROOMS — painted paper, curved edges between them
   ================================================================== */
.room {
  position: relative;
  padding: calc(var(--pad-y) + var(--edge-h) * .55) 0 var(--pad-y);
  isolation: isolate;
}
.room::before {                       /* the paper grain */
  content: ""; position: absolute; inset: 0; z-index: 0;
  background-image: var(--paper-tex, none);
  background-size: 300px 300px;
  opacity: .24;
  mix-blend-mode: multiply;
  pointer-events: none;
}
.room > .wrap, .room > .drive { position: relative; z-index: 3; }

/* the curved wipe: the room's own paper bulges up over the section above it */
.edge {
  position: absolute; top: calc(-1 * var(--edge-h) + 1px); left: 0;
  width: 100%; height: var(--edge-h);
  z-index: 2; display: block;
  fill: var(--edge-fill, currentColor);
}

/* The four rooms of the BRIEF, plus RSVP and the countdown. Each ground is the
   colour of the paper tile main.js will multiply over it — see the map at the
   top of this file. Swapping a ground without swapping the tile makes mud. */
/* FOUR of these seven grounds are LIGHT and THREE are dark, which is one more dark room
   than any skin before this one. Measured against the ink #14243D:
   whitewash 14.79:1, blush 9.26:1, sage olive 9.29:1, limewash 12.68:1.
   Against --on-dark #FBF9F4: aegean deep 5.49:1, cobalt 7.65:1, night 16.36:1. */
.room--teal     { background: var(--gr-whitewash);   color: var(--ink);     --edge-fill: var(--gr-whitewash); }   /* The Invitation */
.room--pink     { background: var(--gr-blush);       color: var(--ink);     --edge-fill: var(--gr-blush); }       /* The Days, in Order */
.room--marigold { background: var(--gr-aegean-deep); color: var(--on-dark); --edge-fill: var(--gr-aegean-deep); } /* Getting to the Island */
.room--indigo   { background: var(--gr-sage);        color: var(--ink);     --edge-fill: var(--gr-sage); }        /* How It Began */
.room--cream    { background: var(--gr-limewash);    color: var(--ink);     --edge-fill: var(--gr-limewash); }
/* the one class this skin ADDS. main.js gains `room--palm → paper-palm` beside it;
   without that line the ground paints but the paper grain does not.
   THE CONTRAST REPAIR GOES THE OTHER WAY HERE. Goa's room--palm was palm green, a dark
   ground; Ballroom made it pale champagne, a light one, took the ink and wrote that the
   class is deliberately OUT of both dark selectors. Here it is COBALT #1B4E9B: the ink
   on it measures 1.93:1 and fails outright, so it takes --on-dark (7.65:1) and it has to
   be put BACK INTO both selectors. Inheriting Ballroom's line unchanged would have put
   near-black navy type on a cobalt wall. */
.room--palm     { background: var(--gr-cobalt);      color: var(--on-dark); --edge-fill: var(--gr-cobalt); }      /* Before You Come */
.room--night    { background: var(--indigo-lo);      color: var(--on-dark); --edge-fill: var(--indigo-lo); }

/* THREE of the seven rooms are dark grounds, so they take light buttons — the aegean
   route room, the cobalt `know` room and the night countdown. */
.room--night .btn, .room--marigold .btn, .room--palm .btn {
  background: var(--gr-whitewash); color: var(--ink); border-color: var(--gr-whitewash);
}
.room--night .btn--ghost, .room--marigold .btn--ghost, .room--palm .btn--ghost {
  background: transparent; color: var(--on-dark); border-color: rgba(251,249,244,.6);
}
/* Hairline rules. THE METAL CANNOT CARRY THESE ANYWHERE, which is a step further than
   Ballroom, where it still worked on two rooms. --gr-gold is a sunset amber: 1.96:1 on
   whitewash, 1.68:1 on limewash, 1.23:1 on blush, 1.23:1 on sage, 2.81:1 on aegean and
   3.91:1 on cobalt. A gilt hairline on any of those is a rule that is present, measured,
   in the DOM and invisible. Light rooms take the ink at low opacity, dark rooms the
   whitewash. */
.room--marigold .rule, .room--night .rule, .room--palm .rule { background: var(--gr-whitewash); opacity: .5; }
.room--teal .rule, .room--cream .rule,
.room--pink .rule, .room--indigo .rule { background: var(--ink); opacity: .3; }
/* The ampersand is an accent, so on the light grounds it takes the flower rather than
   the metal — bougainvillea is 4.4:1 on whitewash and the gold is 1.96:1. */
.room--teal .amp, .room--pink .amp, .room--indigo .amp,
.room--cream .amp { color: var(--gr-bougainvillea); }
.room--marigold .amp, .room--palm .amp, .room--night .amp { color: var(--gr-gold); }

/* petals drift BEHIND the copy inside a paper room — in the sky stage they are
   in front of the palace, but type is never something you read through. */
.room > .drift { z-index: 1; }

/* painted stars for the night rooms */
.stars {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background-repeat: repeat;
  background-size: 520px 520px;
  opacity: .85;
  animation: twinkle 7s ease-in-out infinite;
}
@keyframes twinkle {
  0%, 100% { opacity: .58; }
  50%      { opacity: .95; }
}

/* ------------------------------------------------------------------
   3. INVITE
   ------------------------------------------------------------------ */
.greet {
  font-style: italic; font-size: clamp(17px, 2vw, 23px);
  margin: 0 0 clamp(28px, 4vw, 46px); opacity: .9;
}
.hosts { margin: 0 0 clamp(30px, 4.5vw, 54px); display: grid; gap: 18px; }
.host-l { font-family: var(--caps); font-size: clamp(17px, 2.2vw, 25px); letter-spacing: .05em; margin: 0; }
.host-s { margin: 4px 0 0; font-style: italic; font-size: clamp(14px,1.4vw,16px); opacity: .72; }
.lede { font-style: italic; font-size: clamp(16px,1.7vw,20px); opacity: .85; margin: 0 0 .5em; }
.couple {
  font-weight: 300;
  font-size: clamp(48px, 11vw, 122px);
  line-height: 1; margin: .1em 0 0;
  display: flex; flex-direction: column; align-items: center;
}
.couple .amp {
  font-family: var(--caps); font-style: normal;
  font-size: clamp(20px, 3vw, 36px); opacity: .7; margin: .12em 0;
}
.couple-date {
  font-family: var(--caps); font-size: clamp(20px, 3.4vw, 40px);
  letter-spacing: .22em; margin: 0;
}
.couple-meta { margin: 10px 0 0; font-style: italic; opacity: .82; }
/* T-GP addition: the tagline under the names. */
.tagline {
  margin: clamp(12px,1.8vw,20px) 0 0;
  font-family: var(--caps); letter-spacing: .16em; text-transform: uppercase;
  font-size: clamp(11px,1.2vw,14px); opacity: .72;
}

/* ------------------------------------------------------------------
   4. FUNCTIONS
   ------------------------------------------------------------------ */
/* The entrance animation translates each card ±70px (reveal() in main.js), and
   on a 390px phone that is 50px of horizontal DOCUMENT overflow for as long as
   the card is waiting for its ScrollTrigger — enough that Chrome's mobile
   emulation widens the visual viewport from 390 to 441 and the fixed Sound
   button lands half off-screen. Measured with getBoundingClientRect, not
   guessed: the first card sits at [-50, 300] with transform matrix -70.
   `overflow-x: clip` and not `hidden`: clip does not make a scroll container,
   so nothing inside is turned scrollable and no `position: sticky` downstream
   is affected. overflow-clip-margin lets the cards' own 40px shadow out. */
.funcs { display: grid; gap: clamp(28px, 4.5vw, 56px); overflow-x: clip; overflow-clip-margin: 46px; }
.func {
  display: grid;
  grid-template-columns: minmax(0, 270px) minmax(0, 1fr);
  gap: clamp(20px, 3.5vw, 48px);
  align-items: center;
  background: rgba(255,252,246,.92);
  border: 1px solid rgba(20,36,61,.22);
  border-radius: clamp(30px, 5vw, 64px);
  box-shadow: 0 18px 40px rgba(20,36,61,.12);
  padding: clamp(24px, 3.2vw, 42px) clamp(26px, 3.6vw, 48px);
  will-change: transform, opacity;
}
.func:nth-child(even) { direction: rtl; }
.func:nth-child(even) > * { direction: ltr; }
.func-art { margin: 0; }
/* the pieces behind these cards are not one family — a harmonium, a phulkari
   dupatta, a dhol, a chooda set, a khanda, a motorcycle — so cap the height and
   let each one sit inside the box at its own aspect */
.func-art img { width: 100%; max-height: clamp(180px, 26vh, 300px); object-fit: contain; }
/* T-GP addition: a real photograph, when the record has one (see
   renderEvents() in main.js) — a photo needs cover + a fixed box, not the
   illustrated piece's contain-and-shrink. */
.func-art img.func-photo {
  /* Capped like the illustrated piece it stands in for. `max-height: none`
     let a photograph fill the whole column at 4:3, so a card that had been a
     300px lantern became a 450px photograph and the page went from a set of
     cards to a stack of posters — the "giant image" a couple sees the moment
     they upload one. The shape stays 4:3; only the size is disciplined, and
     `cover` means a portrait photo from a phone crops rather than stretches. */
  max-height: clamp(200px, 30vh, 340px); aspect-ratio: 4 / 3; object-fit: cover;
  width: 100%; margin-inline: auto; border-radius: 22px;
}
.func-title {
  font-family: var(--caps); font-weight: 400;
  font-size: clamp(26px, 4vw, 44px); letter-spacing: .06em;
  margin: 0 0 .18em;
}
.func-meta { font-family: var(--caps); letter-spacing: .12em; text-transform: uppercase;
  font-size: clamp(11px,1.2vw,13px); margin: 0 0 .55em; opacity: .8; }
.func-window { margin: 0 0 .5em; font-style: italic; opacity: .9; }
.func-venue { margin: 0 0 .35em; }
.func-venue a { text-decoration-thickness: 1px; text-underline-offset: 3px; }
.func-body { margin: 0 0 .5em; }
.func-dress, .func-avoid { margin: 0; font-size: .92em; opacity: .78; }
.func-acts { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; }
.mini {
  font-family: var(--caps); font-size: 12px; letter-spacing: .14em; text-transform: uppercase;
  padding: 9px 18px; border-radius: 999px;
  border: 1px solid currentColor; background: none; color: inherit;
  cursor: pointer; text-decoration: none; opacity: .85;
}
.mini:hover { opacity: 1; }

@media (max-width: 720px) {
  .func { grid-template-columns: 1fr; text-align: center; border-radius: 34px; }
  .func:nth-child(even) { direction: ltr; }
  .func-art { max-width: 190px; margin-inline: auto; }
  .func-acts { justify-content: center; }
}

/* ------------------------------------------------------------------
   5. ROUTE — the drive
   ------------------------------------------------------------------ */
/* The ScrollTrigger runs `start: top top` to `end: bottom bottom`, so the whole
   crossing happens in (height - 100vh) of scroll — 20vh at T-TM's 120vh, which
   is 180px on a 900px screen. The boat crossed in about a fifth of one flick of
   the wheel and every screenshot stop missed it. 168vh gives it ~610px, which
   is roughly one scroll gesture. It cannot leave a blank field the way T-TM
   warned: `end: bottom bottom` keeps the stage pinned for the whole range. */
.drive { height: 168vh; margin-bottom: clamp(30px, 6vw, 80px); }
.drive-stage {
  position: sticky; top: 0;
  height: 100vh; height: 100svh;
  overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
/* The band is a WHITEWASHED COBBLED PATH, and it is the one bright mass in the room.
   Goa's problem was a teal band on a teal room; here the room ground is deep aegean, so
   the band goes the other way — bleached limestone in full sun, going warm sand where it
   meets the wall. It is what makes the section read as a lane the Vespa is riding down
   rather than as a stripe. */
.drive-band {
  position: absolute; left: 0; right: 0; bottom: 0; height: 34%;
  background: linear-gradient(#FBF7EF, #E3D3B8);
}
/* the lip along the top of the lane, so the band carries something even at
   scroll positions between poses */
.drive-band::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 5px;
  background: linear-gradient(rgba(255,252,244,.9), rgba(255,252,244,0));
  opacity: .9;
}
/* THE RHYTHM IS A PAINTED KERB LINE, not a centre line and not Ballroom's gilt inlay.
   Goa broke its line into faint strokes because a steady dash on water reads as tarmac;
   Punjabi brought a proper broken centre line back because that really was a two-lane
   highway; Ballroom used a burgundy-and-gold inlay because a ballroom floor has one. A
   Santorini lane has neither — what it has is the cobalt line the villagers paint along
   the edge of every path and up the sides of every step, and the grout between the
   stones. So: one continuous cobalt kerb line with a whitewash highlight over it. */
.drive-band::after {
  content: ""; position: absolute; left: 0; right: 0; top: 15%; height: 3px;
  background:
    linear-gradient(rgba(255,253,248,.6) 0 1px, rgba(0,0,0,0) 1px 2px, rgba(27,78,155,.38) 2px 3px);
}
/* the car and (optionally) the couple sitting on it travel as one rig */
.drive-rig {
  position: absolute;
  /* the Rolls's wheels sit low inside the band rather than on the lip of it. The band
     is 34% of the stage and this plate draws about 250px on a 900px one; at anything
     above ~10% the floor's top edge cuts across the doors and it reads as half-buried.
     Checked at both viewports. */
  bottom: 7%;
  left: 0;
  width: clamp(280px, 44vw, 680px);
  will-change: transform;
  z-index: 2;
}
/* CONTENT SWAP: the `car` key holds the white VESPA, bougainvillea in the basket, with
   the couple already sitting on it. `couple.onCar` stays null for GOA's reason and not
   Ram Sita's: the couple are already painted into all four vehicle plates, so pairing
   one with a second couple would put them on the Vespa twice.
   Bound by HEIGHT, like Goa's and unlike Lantern Palace's car. 27vh, back up towards
   Punjabi's 28 and above Ballroom's 24: Ballroom came down to 24 because a Rolls-Royce
   is a low wide car that overran the band at 28. A Vespa is the opposite shape — two
   people sitting UPRIGHT on a narrow scooter — so at 24 it drew as a smudge in the
   middle of the lane. Checked at both viewports.
   object-position is BOTTOM, so the wheels land on the path. */
.drive-car {
  width: auto; max-width: 100%;
  height: clamp(150px, 27vh, 340px);
  object-fit: contain; object-position: bottom center;
  filter: drop-shadow(0 14px 18px rgba(12,27,51,.34));
}
.drive-couple {
  position: absolute; left: 0; bottom: 34%;
  width: 100%;
}
/* pinned to the top of the stage rather than centred: the car crosses the
   middle band, and a centred heading sits right under its wheels */
.drive-head { position: absolute; top: 11%; left: 0; right: 0; z-index: 3; margin: 0; padding: 0 var(--pad-x); }

/* ------------------------------------------------------------------
   5b. venue + travel
   ------------------------------------------------------------------ */
.venue-card {
  background: rgba(247,242,230,.10);
  border: 1px solid rgba(201,162,74,.55);
  border-radius: 999px / 140px;
  padding: clamp(34px,5vw,64px) clamp(24px,4vw,56px);
  margin-bottom: clamp(34px, 5vw, 66px);
}
.venue-name { font-family: var(--caps); font-size: clamp(24px,3.6vw,42px); margin: 0 0 .2em; letter-spacing: .04em; }
.venue-city { font-family: var(--caps); letter-spacing: .28em; font-size: clamp(11px,1.2vw,13px); margin: 0; opacity: .75; }

.rolls { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: clamp(18px,2.6vw,32px); }
.roll {
  background: rgba(247,242,230,.09);
  border: 1px solid rgba(201,162,74,.42);
  border-radius: 26px; padding: clamp(20px,2.6vw,30px);
  will-change: transform, opacity;
}
.roll-h { font-family: var(--caps); letter-spacing: .14em; text-transform: uppercase;
  font-size: clamp(12px,1.3vw,14px); margin: 0 0 .6em; }
.roll-b { margin: 0; font-size: .95em; }

.hotels { display: grid; gap: 16px; margin-top: clamp(22px,3vw,36px); }
.hotel {
  background: rgba(247,242,230,.09);
  border: 1px solid rgba(201,162,74,.42);
  border-radius: 26px; padding: clamp(20px,2.6vw,30px);
  display: grid; grid-template-columns: minmax(0,1.2fr) minmax(0,1fr); gap: 10px 26px;
  will-change: transform, opacity;
}
.hotel-n { font-family: var(--caps); font-size: clamp(17px,2vw,22px); margin: 0 0 .25em; letter-spacing: .04em; }
.hotel-m, .hotel-p { margin: 0; font-size: .93em; opacity: .85; }
.hotel-acts { display: flex; flex-wrap: wrap; gap: 10px; align-self: end; }
@media (max-width: 640px) { .hotel { grid-template-columns: 1fr; } }

.baraat {
  margin-top: clamp(34px,5vw,60px);
  display: grid; grid-template-columns: minmax(0, 240px) minmax(0,1fr);
  gap: clamp(18px,3vw,40px); align-items: center;
}
.baraat-art { width: 100%; }
.baraat-line { margin: 0; font-size: clamp(16px,1.7vw,20px); }
@media (max-width: 640px) { .baraat { grid-template-columns: 1fr; text-align: center; }
  .baraat-art { max-width: 200px; margin-inline: auto; } }

/* ------------------------------------------------------------------
   6. STORY
   ------------------------------------------------------------------ */
.beats { display: grid; gap: clamp(40px, 7vw, 96px); }
.beat {
  display: grid; grid-template-columns: minmax(0, 0.95fr) minmax(0, 1fr);
  gap: clamp(22px, 4vw, 54px); align-items: center;
  will-change: transform, opacity;
}
.beat:nth-child(even) .beat-fig { order: 2; }
.beat-fig {
  margin: 0; position: relative;
  border-radius: 50% 50% 14px 14px / 34% 34% 14px 14px;
  overflow: hidden;
  border: 1px solid rgba(247,242,230,.28);
  background: rgba(247,242,230,.08);
  aspect-ratio: 4 / 5;
  max-width: 380px;
  width: 100%;
  justify-self: center;
}
.beat-fig img { width: 100%; height: 100%; object-fit: cover; }
.beat-fig .miss {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-family: var(--caps); letter-spacing: .2em; text-transform: uppercase;
  font-size: 12px; opacity: .6;
}
.beat-no { font-family: var(--caps); font-size: clamp(32px,5vw,58px); opacity: .35; margin: 0; line-height: 1; }
.beat-kicker { font-family: var(--caps); letter-spacing: .2em; text-transform: uppercase;
  font-size: clamp(11px,1.2vw,13px); margin: .8em 0 .5em; opacity: .8; }
.beat-title { font-weight: 300; font-size: clamp(24px,3.4vw,40px); line-height: 1.16; margin: 0 0 .4em; }
.beat-body { margin: 0; opacity: .86; }
@media (max-width: 720px) {
  .beat { grid-template-columns: 1fr; }
  .beat:nth-child(even) .beat-fig { order: 0; }
  .beat-fig { aspect-ratio: 3 / 2; border-radius: 44% 44% 12px 12px / 22% 22% 12px 12px; }
}

/* ------------------------------------------------------------------
   6b. GALLERY — T-GP addition. A mosaic of the record's own photographs
   (C.gallery), portrait-friendly by default (a landscape shot still reads
   fine at 3:4, cropped by its own `focal`). See renderGallery() in main.js.
   ------------------------------------------------------------------ */
/* THREE columns above 760px, not auto-fit. `repeat(auto-fit, minmax(190px, 1fr))` in a
   1080px wrap resolves to FIVE columns, so a six-panel gallery renders as a row of five
   and one orphan hanging under the left edge. The panel list is six by convention across
   every skin on this engine, so the grid should be the shape that divides it. */
.gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: clamp(10px, 1.6vw, 18px); }
@media (min-width: 760px) { .gallery { grid-template-columns: repeat(3, 1fr); } }
.gal-shot {
  position: relative; margin: 0; overflow: hidden;
  border-radius: 22px; aspect-ratio: 3 / 4;
  border: 1px solid rgba(20,36,61,.18);
  background: rgba(255,253,249,.4);
  will-change: transform, opacity;
}
.gal-shot img { display: block; width: 100%; height: 100%; object-fit: cover; }
.gal-cap {
  position: absolute; left: 0; right: 0; bottom: 0; margin: 0;
  padding: .9em .9em .6em; font-size: .78em; color: var(--on-dark);
  background: linear-gradient(to top, rgba(12,27,51,.80), transparent);
}

/* ------------------------------------------------------------------
   6c. THE SCRATCH CARD — REBUILT around the client's painted jharokha.
   Every skin before this one painted the whole card in code. Here the PNG is
   the card and the canvas is only the flat gold panel inside its arch, whose
   rectangle is MEASURED by ../_system/goldpanel.py, published in
   assets/manifest.json under `scratch_card.bbox_norm`, and set on #scratchCard
   by main.js as the four properties below. The defaults keep the first painted
   frame correct before that fetch lands; a repainted card needs no edit here.
   bbox_norm [.2943 .2217 .7057 .7321] -> x0 29.43% y0 22.17% w 41.14% h 51.04%.
   ------------------------------------------------------------------ */
.scratch-card {
  --scr-x0: 30.22%;
  --scr-y0: 24.40%;
  --scr-w:  39.48%;
  --scr-h:  55.50%;
  position: relative; margin: 0 auto; width: 100%;
  /* 600px and not Ram Sita's 460. The readable area is the OVAL, which is 41% of the
     card's width — wider than Punjabi's 34% arch, so the date has room, but 41% of 460
     is still only 189px and the venue line broke to four rows there. */
  max-width: min(600px, 100%);
  aspect-ratio: 1 / 1;
  /* no ground and no radius — the plate is a cut-out with its own silhouette,
     and a rounded rectangle behind it would show as a card under a card */
  filter: drop-shadow(0 18px 34px rgba(12,27,51,.32));
}
.scr-plate { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
/* both of these are the GOLD PANEL, not the card */
.scratch-card .under, #scratchFoil {
  position: absolute;
  left: var(--scr-x0); top: var(--scr-y0);
  width: var(--scr-w); height: var(--scr-h);
}
.scratch-card .under {
  display: grid; align-content: center; justify-items: center;
  gap: .34em; padding: 6% 5%; color: var(--ink); text-align: center;
}
.scr-lbl {
  font-family: var(--caps); font-size: clamp(8px, 1.5vw, 10px); letter-spacing: .24em;
  /* NOT --zari-deep, which is what every earlier skin used for this label. That
     is a deep gold, and here it would be gold type on a gold panel — measured
     1.6:1, invisible. The panel is the ground now, so the label takes the ink. */
  text-transform: uppercase; color: var(--ink); opacity: .66; margin: 0;
}
.scr-date {
  font-family: var(--serif); font-size: clamp(1.3rem, 5.2vw, 2rem);
  line-height: 1.02; margin: .18em 0 0; white-space: nowrap;
}
/* NOT --zari. The panel this type sits on measures #C88F33 and --zari is #C9A24A —
   the same colour, so the separator dots between the date parts were gold on gold at
   1.1:1. They take the ink at reduced opacity, like the label above them. */
.scr-date i { font-style: normal; font-size: .44em; color: var(--ink); opacity: .5; margin: 0 .14em; }
/* .86rem and not .96: the oval is 41% of a 600px card, so the line box is about 222px,
   and "Saturday · Vows at 6:00 PM" wrapped there with "PM" orphaned on its own row. */
.scr-sub { margin: .34em 0 0; font-size: clamp(.72rem, 1.9vw, .86rem); line-height: 1.25; }
.scr-place { margin: .1em 0 0; font-size: clamp(.66rem, 1.8vw, .8rem); opacity: .88; line-height: 1.3; }
#scratchFoil {
  display: block; touch-action: none; cursor: grab; transition: opacity .8s ease;
  /* THE FOIL IS THE SHAPE OF THE OVAL, not of its bounding box. goldpanel.py publishes
     a rectangle, which is all the canvas needs to be POSITIONED by; laid over the bbox
     as a rectangle it paints a gold RECTANGLE over the crown, the candelabra arms and
     the roses, and the card reads as a sticker pasted onto the painting.

     TWO mechanisms, and they are not redundant:
     - `mask-image` is the truth about what is PAINTED. panelmask.py thresholds the flat
       gold inside the published bbox, keeps the largest region, fills it and ships the
       panel's own silhouette (73.8% of the bbox) as an alpha mask at exactly that box,
       so it lines up here by construction. RGBA and not greyscale: prep-images.mjs
       decides jpg-vs-webp from ffprobe's pix_fmt, so an `L` PNG would ship as a JPEG
       and lose the channel, and a CSS mask is an alpha mask by default.
     - `clip-path` is what clips POINTER EVENTS, which a CSS mask does not do at all.
       Without it the corners of the bbox still take pointerdown and the guest can rub at
       the crown and watch nothing happen. It is a centred ellipse rather than the
       measured path because it only has to bound the input: fitted against the mask it
       scores IoU 0.930, and objectBoundingBox units mean it scales with the element
       where a `clip-path: path()` would be frozen at one pixel size. */
  -webkit-mask-image: url("media/scratch-mask.webp");
          mask-image: url("media/scratch-mask.webp");
  -webkit-mask-size: 100% 100%;         mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;       mask-repeat: no-repeat;
  clip-path: ellipse(50% 50% at 50% 50%);
}
#scratchFoil.is-gone { opacity: 0; pointer-events: none; }
/* the prompt sits BELOW THE WHOLE CARD, not inside it. The panel is only 192px
   wide at the largest card size, so a letter-spaced caps line does not fit in
   it — and the first pass, which put it on the card just under the arch, laid
   dark indigo 11px type straight over the painted red drape and the roses,
   where it was rendered, measured, present in the DOM and completely
   invisible. On the room's own gold ground it is 8.4:1. */
.scr-prompt {
  position: absolute; left: 0; right: 0; top: calc(100% + 8px);
  pointer-events: none;
  font-family: var(--caps); font-size: clamp(9px, 1.6vw, 11px); letter-spacing: .24em;
  text-transform: uppercase; color: var(--ink); text-align: center;
  transition: opacity .4s ease;
}
.scr-prompt .shell { color: var(--zari); }
.scratch-card.is-scratched .scr-prompt { opacity: 0; }
.scr-reveal { margin-top: 2.6rem; }   /* clears .scr-prompt, which now sits under the card */
.scr-reveal.is-gone { display: none; }
@media (prefers-reduced-motion: reduce) {
  #scratchFoil, .scr-prompt { transition: none; }
}

/* ------------------------------------------------------------------
   7. RSVP
   ------------------------------------------------------------------ */
.ring-art { width: clamp(96px, 13vw, 168px); margin: 0 auto clamp(10px,2vw,22px); }
.form { display: grid; gap: clamp(18px, 2.6vw, 28px); }
.grp-h {
  display: block; font-family: var(--caps); letter-spacing: .2em; text-transform: uppercase;
  font-size: clamp(11px,1.2vw,13px); margin-bottom: 14px; opacity: .8;
}
.field { display: grid; gap: 7px; }
.field label { font-size: .88em; opacity: .8; }
input, select, textarea {
  font: inherit; font-size: 1em;
  color: var(--ink);
  background: rgba(255,255,255,.72);
  border: 1px solid rgba(20,36,61,.24);
  border-radius: 14px;
  padding: 13px 15px;
  width: 100%;
}
textarea { resize: vertical; }
.toggles { display: flex; flex-wrap: wrap; gap: 10px; }
.toggle { position: relative; }
.toggle input { position: absolute; opacity: 0; width: 1px; height: 1px; }
/* direct child only: the labels carry bilingual <span class="ta">/<span class="en">
   runs inside them, and a descendant selector drew a pill around each one */
.toggle > span {
  display: inline-block;
  font-family: var(--caps); font-size: clamp(12px,1.2vw,14px);
  letter-spacing: .1em;
  padding: 11px 22px; border-radius: 999px;
  border: 1px solid rgba(20,36,61,.3);
  cursor: pointer; transition: background .2s, color .2s, border-color .2s;
}
.toggle input:checked + span { background: var(--gr-burgundy); color: var(--on-dark); border-color: var(--gr-burgundy); }
.toggle input:focus-visible + span { outline: 3px solid var(--zari); outline-offset: 2px; }
.guests { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 14px; }
.guest {
  border: 1px solid rgba(20,36,61,.16);
  border-radius: 22px; padding: clamp(14px,1.8vw,20px);
  background: rgba(255,255,255,.34);
}
.guest-h { font-family: var(--caps); letter-spacing: .16em; text-transform: uppercase;
  font-size: 11px; margin: 0 0 12px; opacity: .75; }
.guest-row { display: grid; grid-template-columns: minmax(0,1.3fr) minmax(0,1fr); gap: 14px; }
@media (max-width: 560px) { .guest-row { grid-template-columns: 1fr; } }
.rsvp-note { font-size: .88em; opacity: .72; margin: 0; }
.rsvp-err { color: #8E1F19; opacity: 1; }
.rsvp-submit { justify-self: start; }
.rsvp-done { padding: clamp(30px,5vw,60px) 0; }
.rsvp-done-h { font-family: var(--caps); font-size: clamp(24px,3.4vw,38px); letter-spacing: .12em;
  text-transform: uppercase; margin: 0 0 .4em; }

/* ------------------------------------------------------------------
   8. THINGS TO KNOW
   ------------------------------------------------------------------ */
.knows { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: clamp(18px,2.6vw,30px); }
.know {
  border: 1px solid rgba(247,242,230,.3);
  border-radius: 28px; padding: clamp(22px,2.8vw,32px);
  background: rgba(247,242,230,.08);
  will-change: transform, opacity;
}
/* Things-to-Know sits on palm green, which is dark, so it keeps the pale card
   the countdown wants. The light rooms are the ones that need ink edges. */
.room--pink .know, .room--teal .know, .room--cream .know, .room--indigo .know {
  border-color: rgba(20,36,61,.24); background: rgba(255,253,249,.52);
}
.know-h { font-family: var(--caps); letter-spacing: .16em; text-transform: uppercase;
  font-size: clamp(12px,1.3vw,14px); margin: 0 0 .6em; }
.know-b { margin: 0; font-size: .95em; opacity: .9; }
/* T-GP addition: the wishes wall reuses this same card (see renderWishes()
   in main.js) — "How you know them", inline after the guest's name. */
.wish-rel { font-style: italic; text-transform: none; letter-spacing: 0; opacity: .75; font-size: .92em; }
.wish-empty { text-align: center; opacity: .78; font-style: italic; margin: 0 0 clamp(24px,3.5vw,40px); }

/* Reused by #evening (the timeline) and #gifts (shagun + registry) — same
   card, new content, so nothing else needed to add those two rooms. */
.know code { font-size: .9em; opacity: .85; }
.gift-qr { display: block; width: 140px; height: 140px; margin: 0 auto .8em; border-radius: 12px; }
.copy {
  font: inherit; font-size: .85em; letter-spacing: .04em; text-transform: uppercase;
  border: 1px solid currentColor; border-radius: 999px; padding: .2em .8em;
  background: transparent; color: inherit; cursor: pointer;
}

/* ------------------------------------------------------------------
   8b. COUNTDOWN
   ------------------------------------------------------------------ */
.count {
  display: grid; grid-template-columns: repeat(4, 1fr);
  gap: clamp(8px, 1.6vw, 20px);
  margin: clamp(26px,4vw,46px) 0 clamp(20px,3vw,30px);
}
.count-cell {
  border: 1px solid rgba(247,242,230,.28);
  border-radius: 20px; padding: clamp(12px,2vw,22px) 6px;
  background: rgba(247,242,230,.06);
}
.count-n { font-family: var(--caps); font-size: clamp(26px, 5.6vw, 54px); line-height: 1; }
.count-l { font-family: var(--caps); letter-spacing: .2em; text-transform: uppercase;
  font-size: clamp(9px,1vw,11px); opacity: .7; margin-top: 8px; }

/* ------------------------------------------------------------------
   footer
   ------------------------------------------------------------------ */
.site-footer {
  position: relative;
  color: var(--on-dark);
  background: var(--indigo-lo);
  padding: clamp(90px, 16vh, 200px) 0 clamp(50px, 8vh, 90px);
  overflow: hidden;
}
.footer-sky {
  position: absolute; inset: 0;
  background-size: cover; background-position: center bottom;
  opacity: .85;
}
/* The Goan night plate has a lit villa and its reflection sitting exactly where
   the hashtag and the footer line go, and white italic over lamplight is
   unreadable. One scrim, centre-weighted so the palms at the edges stay
   visible. */
.footer-sky::after {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(ellipse 74% 56% at 50% 46%, rgba(12,27,51,.74) 0%, rgba(12,27,51,.5) 60%, rgba(12,27,51,.12) 100%),
    linear-gradient(180deg, rgba(12,27,51,.5) 0%, rgba(12,27,51,.12) 38%, rgba(12,27,51,.55) 100%);
}
.site-footer .wrap { position: relative; z-index: 2; }
.hashtag {
  font-family: var(--caps); font-size: clamp(22px,4vw,46px);
  letter-spacing: .16em; margin: 0 0 .3em;
}
.footer-line { font-style: italic; opacity: .82; margin: 0 0 clamp(30px,5vw,54px); }
.fams { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px,1fr)); gap: clamp(18px,3vw,40px); }
.fam-s { font-family: var(--caps); letter-spacing: .2em; text-transform: uppercase;
  font-size: 11px; opacity: .68; margin: 0 0 .5em; }
.fam-n { font-family: var(--caps); font-size: clamp(16px,1.9vw,21px); margin: 0 0 .3em; }
.fam-l { margin: 0 0 .5em; font-style: italic; font-size: .9em; opacity: .72; }
.fam-t { margin: 0; }
.footer-fine { margin: clamp(34px,5vw,60px) 0 0; font-size: .84em; opacity: .6; }

/* ------------------------------------------------------------------
   no-JS twin
   ------------------------------------------------------------------ */
.fallback { display: none; background: var(--cream); color: var(--ink); padding: 40px 24px; }
.fallback h1 { font-weight: 300; font-size: 44px; margin: 0 0 10px; }
.fallback h2 { font-family: var(--caps); font-size: 20px; letter-spacing: .1em; text-transform: uppercase; }

/* ------------------------------------------------------------------
   reduced motion — a composed still, not a broken scroller
   ------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .journey { height: auto; }
  .stage { position: relative; height: 92vh; }
  /* the courtyard only makes sense as the thing you arrive at — with no
     journey to travel, it would just black out the dusk sky. */
  .courtyard { display: none; }
  .gate { width: min(64vh, 50%); }
  .drive { height: auto; margin-bottom: 0; }
  .drive-stage { position: relative; height: 56vh; }
  .drive-car { transform: translateX(0); left: 8%; }
  .hint-arrow { animation: none; opacity: .6; }
  .stars { animation: none; opacity: .8; }
  .drift { display: none; }
  .mobcta { transition: none; }
  * { scroll-behavior: auto !important; }
}


/* ==================================================================
   0. THE OPEN MOMENT — the envelope
   ==================================================================
   Geometry is the manifest's `envelope_rig`, in canvas fractions:
     canvas 1200x1681 · pocket V at .229/.437/.224 of canvas height
     flap art in the top .350, hinge center top, opens rotateX(-170deg)
     card 0.80 of canvas width, closed top .30, travels up ~.70 to withdraw
   Everything below is those numbers and nothing else, so a re-cut rig only
   needs these constants changed.
   ================================================================== */
:root {
  /* unitless percentages of CANVAS height, so they can be composed in calc().
     A plain `top: calc(75% + 30%)` on the card resolves both against the clip
     box, not the canvas, which put the card below the clip and made it
     invisible — hence the explicit re-basing in .env-card below. */
  --env-clip-up: 75;        /* headroom above the canvas for the card to rise into */
  --env-card-top: 30;       /* manifest envelope_rig.card.closed_top */
  --env-card-travel: -87%;  /* of the CARD's own height, = .70 canvas height */
}

.env {
  position: fixed; inset: 0; z-index: 100;
  /* .gate-bg is inset -4% so its slow drift never shows an edge, and on a 390px
     phone that alone is 16px of horizontal DOCUMENT overflow — enough for
     Chrome's mobile emulation to widen the visual viewport and push the fixed
     Sound button off the right edge. Clip it here rather than shrinking the
     bleed, which is what makes the drift work. */
  overflow: hidden;
  display: grid; place-items: center;
  background: radial-gradient(circle at 50% 44%, #FBF3E4, #E7D6B8 70%, #D8C29C);
  cursor: pointer;
  perspective: 1800px;
  transition: opacity .7s ease, transform .7s cubic-bezier(.4,0,.2,1);
}
body.env-out .env { opacity: 0; transform: translateY(6vh) scale(1.06); pointer-events: none; }
body.env-gone .env { display: none; }
/* the page does not scroll until the envelope is open */
body.pre-open { overflow: hidden; height: 100%; }

.env-stage {
  position: relative;
  width: min(430px, 74vw);
  aspect-ratio: 1200 / 1681;
  transform-style: preserve-3d;
}

/* every plate is the full canvas, so one rule places all of them */
.env-l {
  position: absolute; inset: 0;
  background-size: 100% 100%; background-repeat: no-repeat;
}
/* fallback tint if a plate is missing — a plain cream envelope, never a blank */
.env-l--back  { z-index: 1; background-color: #EFE0C2; border-radius: 8px; }
.env-l--front { z-index: 3; }

.env-cardclip {
  position: absolute; left: 0; right: 0;
  top: calc(-1% * var(--env-clip-up)); bottom: 0;
  overflow: hidden;
  z-index: 2;
}
.env-card {
  position: absolute; left: 10%; width: 80%;
  aspect-ratio: 1200 / 1694;
  /* the clip box is (100 + clip-up)% of canvas height, so a canvas-relative
     position has to be re-based onto it */
  top: calc((var(--env-clip-up) + var(--env-card-top)) / (100 + var(--env-clip-up)) * 100%);
  background: var(--cream);
  transition: transform .95s cubic-bezier(.3,0,.2,1) .2s;
}
body.is-opening .env-card { transform: translateY(var(--env-card-travel)); }
.env-card-art { border-radius: 4px; }
.env-card-txt {
  /* clear of the card's printed paisley border */
  position: absolute; left: 20%; right: 20%; top: 26%;
  text-align: center; color: var(--ink);
}
.env-note {
  font-family: var(--caps); letter-spacing: .26em; text-transform: uppercase;
  font-size: clamp(8px, 1.5vw, 12px); margin: 0 0 .8em; opacity: .7;
}
.env-names {
  font-size: clamp(19px, 4.4vw, 34px); font-weight: 300; margin: 0;
  display: flex; align-items: center; justify-content: center; gap: .28em;
  flex-wrap: wrap;
}
.env-names em { font-family: var(--caps); font-style: normal; font-size: .55em; opacity: .65; }
.env-date {
  font-family: var(--caps); letter-spacing: .18em;
  font-size: clamp(10px, 1.9vw, 14px); margin: .8em 0 0; opacity: .8;
}

/* the greeting is printed on the pocket panel, below the V */
.env-to {
  position: absolute; left: 8%; right: 8%; top: 74%; z-index: 4;
  margin: 0; text-align: center; font-style: italic;
  font-size: clamp(13px, 2.6vw, 19px);
  color: var(--ink); opacity: .85;
}

.env-flap {
  position: absolute; inset: 0; z-index: 5;
  transform-origin: center top;
  transform-style: preserve-3d;
  transition: transform .9s cubic-bezier(.5,0,.2,1);
}
/* the seal is composited into the flap plate, so it lifts with the flap —
   no separate seal element to keep in sync */
body.is-opening .env-flap { transform: rotateX(-170deg); }

.env-cta {
  position: absolute; bottom: clamp(24px, 7vh, 70px); left: 0; right: 0;
  margin: 0; text-align: center;
  font-family: var(--caps); letter-spacing: .3em; text-transform: uppercase;
  font-size: clamp(10px, 1.2vw, 13px); color: var(--ink); opacity: .62;
  animation: hint 2.4s ease-in-out infinite;
}

/* ==================================================================
   0. THE OPEN MOMENT — the gate (replaces the envelope, 2026-09-02)
   ==================================================================
   Full-bleed painted night palace, dimmed to indigo, gold monogram, two
   hairlines, the greeting, one outlined button. Lanterns drift behind the
   type. Reuses .env's fixed/transition/open-state rules above.
   ================================================================== */
:root { --gate-gold: #D9B45F; }   /* zari, lifted so it holds on the night plate */

.env.opengate {
  background: var(--indigo-lo);
  display: grid; place-items: center;
  overflow: hidden;
  cursor: pointer;
}
.gate-bg {
  position: absolute; inset: -4%;
  background: url("media/palace-night.jpg") center 62% / cover no-repeat;
  animation: gate-drift 26s ease-in-out infinite alternate;
  will-change: transform;
}
@media (max-width: 760px) {
  .gate-bg { background-image: url("media/palace-night-9x16.jpg"); background-position: center 58%; }
  /* the ballroom sits right behind the type on a phone — hold it darker there */
  .gate-shade {
    background:
      radial-gradient(ellipse 70% 46% at 50% 50%, rgba(12,27,51,.68) 0%, rgba(12,27,51,.76) 55%, rgba(12,27,51,.88) 100%),
      linear-gradient(180deg, rgba(12,27,51,.58) 0%, rgba(12,27,51,.30) 40%, rgba(12,27,51,.76) 100%);
  }
}
@keyframes gate-drift {
  from { transform: scale(1) translateY(0); }
  to   { transform: scale(1.07) translateY(-1.5%); }
}
/* TWO changes from the inherited scrim, and the first of them never failed a number.
   1. RETINTED. It was rgba(16,28,58,…) — Punjabi's cold royal indigo, which over that
      skin's blue-hour plate was invisible and over this warm champagne-and-gold ballroom
      read as a grey-blue haze laid on a gold room. It is the skin's own near-black
      burgundy now. Marathi made the same repair for the same reason.
   2. DEEPENED AT THE CENTRE, from .18 to .46 — and this one did fail a number, badly.
      The gate plate's crystal chandelier sits directly behind the small caps line, and
      at .18 the measured contrast of `.gate-note` against what was actually rendered
      behind it was 1.89:1: gold type, correctly coloured, fully present in the DOM, and
      unreadable. Nothing but sampling the screenshot finds this — the token is #D9B45F
      and looks fine in the stylesheet. */
.gate-shade {
  position: absolute; inset: 0;
  background:
    radial-gradient(ellipse at 50% 42%, rgba(12,27,51,.56) 0%, rgba(12,27,51,.74) 60%, rgba(12,27,51,.90) 100%),
    linear-gradient(180deg, rgba(12,27,51,.58) 0%, rgba(12,27,51,.26) 40%, rgba(12,27,51,.74) 100%);
}

.gate-lanterns { position: absolute; inset: 0; pointer-events: none; }
.gl {
  position: absolute; top: -20vh; width: clamp(34px, 4.2vw, 64px);
  opacity: 0; filter: drop-shadow(0 0 14px rgba(217,178,94,.5));
  animation: gl-rise linear infinite;
}
.gl-1 { left: 8%;  animation-duration: 21s; animation-delay: -3s;  }
.gl-2 { left: 22%; animation-duration: 26s; animation-delay: -11s; width: clamp(26px, 3vw, 46px); }
.gl-3 { left: 36%; animation-duration: 23s; animation-delay: -17s; }
.gl-4 { left: 64%; animation-duration: 25s; animation-delay: -7s;  width: clamp(28px, 3.4vw, 50px); }
.gl-5 { left: 78%; animation-duration: 20s; animation-delay: -14s; }
.gl-6 { left: 90%; animation-duration: 28s; animation-delay: -1s;  width: clamp(24px, 2.8vw, 42px); }
/* FALLS on the gate screen, matching the ambient drift inside the page: these
   are marigold heads and phulkari blooms, and a flower that floats upward reads
   as a bug rather than as weather. */
@keyframes gl-rise {
  0%   { transform: translate3d(0, 24vh, 0) rotate(-2deg); opacity: 0; }
  8%   { opacity: .9; }
  50%  { transform: translate3d(1.6vw, -34vh, 0) rotate(2deg); }
  92%  { opacity: .8; }
  100% { transform: translate3d(-1vw, -96vh, 0) rotate(-1deg); opacity: 0; }
}

.gate-body {
  position: relative; z-index: 2;
  display: flex; flex-direction: column; align-items: center;
  text-align: center; color: var(--on-dark);
  padding: 0 var(--pad-x);
  max-width: 34rem;
  animation: gate-in 1.4s cubic-bezier(.2,.7,.2,1) both;
}
@keyframes gate-in { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }

/* The monogram over the gate: R & A cut in white, the way the villa's own
   name is painted on its gatepost. T-TM set ஸ்ரீ here in the Tamil face; this
   skin has no equivalent opening word, so the initials carry the slot. */
.gate-sri {
  margin: 0 0 .55rem;
  font-family: var(--serif);
  font-size: clamp(34px, 4.6vw, 58px);
  font-weight: 300;
  line-height: 1;
  color: var(--on-dark);
  letter-spacing: .04em;
  text-shadow: 0 0 30px rgba(243,234,211,.28);
}
.gate-sri em { font-style: italic; font-size: .62em; opacity: .74; padding: 0 .12em; }
.gate-mono {
  margin: 0 0 .9rem;
  font-family: var(--caps); letter-spacing: .32em; color: var(--gate-gold);
  text-shadow: 0 2px 16px rgba(12,27,51,.8);
  font-size: clamp(15px, 1.7vw, 20px);
  display: flex; align-items: center; gap: .55em;
}
.gate-mono i { font-style: normal; font-size: .5em; opacity: .85; transform: translateY(-.05em); }
.gate-rule {
  display: block; width: 3.4rem; height: 1px;
  background: linear-gradient(90deg, transparent, var(--gate-gold), transparent);
  margin: .2rem 0 1.3rem; opacity: .85;
}
.gate-to {
  margin: 0 0 1.6rem; font-family: var(--serif); font-style: italic;
  font-size: clamp(16px, 2vw, 21px); color: var(--on-dark-soft);
}
/* The smallest type on the gate, on the busiest part of the plate. It carries the same
   shadow .gate-names does and for the same reason — a 10px letter-spaced caps line in a
   thin gold has almost no stroke area to carry contrast with, so it needs the ground
   pushed away from it as well as the scrim above. Full opacity, not .9: at this size the
   extra 10% was costing more than it bought. */
.gate-note {
  margin: 0 0 .5rem; font-family: var(--caps); letter-spacing: .3em; text-transform: uppercase;
  /* NOT --gate-gold. That token is #D9B45F and it is right for the monogram, the rule
     and the button, which all sit on the darker outer ring of the scrim. This line sits
     dead centre, directly over the plate's crystal chandelier, and is the smallest type
     on the screen — it takes a pale champagne so the stroke itself carries some of the
     contrast rather than asking the scrim to carry all of it. */
  font-size: clamp(9px, 1vw, 11px); color: #F2E2B4;
  text-shadow: 0 1px 10px rgba(12,27,51,.85), 0 1px 3px rgba(12,27,51,.7);
}
.gate-names {
  margin: 0; font-family: var(--serif); font-weight: 300;
  /* 1.14, not T-TM's 1.02. Cormorant's descenders are long and the two names
     stack, so at 1.02 the y of "Ananya" ran into the date line under it on a
     390px screen. The shadow is for the same reason the hero has one: the lit
     facade of the night villa sits directly behind these two words. */
  line-height: 1.14;
  font-size: clamp(40px, 7.2vw, 92px); letter-spacing: .01em;
  text-shadow: 0 2px 24px rgba(12,27,51,.75), 0 1px 4px rgba(12,27,51,.55);
  display: flex; flex-direction: column; align-items: center;
}
.gate-names em {
  font-style: italic; font-weight: 300; color: var(--gate-gold);
  font-size: .42em; line-height: 1.2; margin: .1em 0;
}
.gate-date {
  margin: 1.5rem 0 0; font-family: var(--caps); letter-spacing: .34em;
  font-size: clamp(11px, 1.3vw, 15px); color: var(--on-dark);
}
.gate-date + .gate-rule { margin-top: 1.4rem; }
.gate-cta { margin: 0; }
.gate-cta span {
  display: inline-block; padding: .95em 2.4em;
  border: 1px solid var(--gate-gold); border-radius: 999px;
  font-family: var(--caps); letter-spacing: .28em; text-transform: uppercase;
  font-size: clamp(10px, 1.05vw, 12px); color: var(--gate-gold);
  text-shadow: 0 1px 8px rgba(12,27,51,.7);
  background: rgba(12,27,51,.25); backdrop-filter: blur(2px);
  transition: background .35s ease, color .35s ease, transform .35s ease;
  animation: gate-pulse 3.2s ease-in-out infinite;
}
.env.opengate:hover .gate-cta span, .env.opengate:focus-visible .gate-cta span {
  background: var(--gate-gold); color: var(--indigo-lo); transform: translateY(-1px);
}
@keyframes gate-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(217,178,94,.0); }
  50%      { box-shadow: 0 0 0 9px rgba(217,178,94,.12); }
}
.gate-place {
  margin: 2rem 0 0; font-family: var(--caps); letter-spacing: .36em;
  font-size: clamp(9px, .95vw, 11px); color: var(--on-dark-soft); opacity: .7;
}
.env.opengate:focus-visible { outline: none; }

@media (prefers-reduced-motion: reduce) {
  .gate-bg, .gl, .gate-body, .gate-cta span { animation: none; }
  .gl { opacity: .8; top: 20%; }
}

/* ==================================================================
   THE MUSIC TOGGLE
   ================================================================== */
.audio-btn {
  position: fixed; top: 16px; right: 16px; z-index: 70;
  display: inline-flex; align-items: center; gap: 9px;
  padding: 9px 15px;
  border-radius: 999px;
  border: 1px solid rgba(247,242,230,.5);
  background: rgba(33,16,22,.4);
  backdrop-filter: blur(6px);
  color: var(--on-dark);
  font-family: var(--caps); letter-spacing: .16em; text-transform: uppercase;
  font-size: 11px; cursor: pointer;
  opacity: 0; transform: translateY(-10px);
  transition: opacity .5s ease .4s, transform .5s ease .4s, background .25s ease;
}
body.is-open .audio-btn { opacity: 1; transform: none; }
/* `display: inline-flex` above beats the UA rule for [hidden], so the toggle
   has to be told again — otherwise a site with no track shows a dead control */
.audio-btn[hidden] { display: none !important; }
.audio-btn:hover { background: rgba(33,16,22,.6); }
.audio-ico { display: inline-flex; align-items: flex-end; gap: 2px; height: 13px; }
.audio-ico i {
  width: 2px; background: currentColor; border-radius: 1px;
  height: 5px; transform-origin: bottom;
}
.audio-btn[aria-pressed="true"] .audio-ico i { animation: eq .9s ease-in-out infinite; }
.audio-btn[aria-pressed="true"] .audio-ico i:nth-child(2) { animation-delay: .18s; }
.audio-btn[aria-pressed="true"] .audio-ico i:nth-child(3) { animation-delay: .36s; }
.audio-btn[aria-pressed="false"] .audio-ico i { height: 4px; opacity: .55; }
@keyframes eq { 0%,100% { height: 4px; } 50% { height: 13px; } }

/* ==================================================================
   ORNAMENT LAYER — it frames, it does not shout
   ================================================================== */
.orn-edge {
  position: absolute; left: 0; right: 0; height: clamp(28px, 4vw, 56px);
  z-index: 2; pointer-events: none;
  background-repeat: repeat-x;
  background-size: auto 100%;
  opacity: .3;
}
.orn-edge--top { top: calc(var(--edge-h) * .5); }
.orn-edge--bot { bottom: clamp(10px, 2vw, 26px); transform: scaleY(-1); }
.room--cream .orn-edge, .room--pink .orn-edge,
.room--teal .orn-edge, .room--indigo .orn-edge { opacity: .5; }

/* T-PJ: the arch plate is 1024x1536 (not Ram Sita's 1400x1747), and it is a
   frame that is OPEN at the top — its transparent interior runs into the plate's
   own top edge rather than being a closed ring, so there is no "opening bbox"
   to measure. What is measurable is the gap between the two painted pillars at
   any given height, and that holds at about x 13-87% from a third of the way
   down. The type block is inset inside that. */
.name-arch {
  position: relative;
  width: min(560px, 100%);
  margin: clamp(18px, 3vw, 34px) auto 0;
  /* THIS SKIN'S PLATE IS A WREATH, 369x342, not the 1024x1536 portrait arch
     this rule was copied with. In a 2:3 box `contain` drew the ring small in
     the middle and the names, placed for the tall plate's opening, sat on its
     bougainvillea. The ring now hangs at the top of the box at full width,
     the names sit in its centre, and the date and the rest run on below it. */
  aspect-ratio: auto;
  display: block;
}
.name-arch-frame {
  position: absolute; top: 0; left: 0; width: 100%; aspect-ratio: 369 / 342;
  background-size: 100% auto; background-repeat: no-repeat; background-position: top center;
  pointer-events: none;
}
.name-arch-in {
  position: relative;
  width: 100%;
  /* the ring's centre is half its height down; the names block is ~a quarter
     of the box wide-height tall, so it starts at 35% and ends 35% short of
     the ring's bottom edge, where the date takes over on plain paper */
  padding-top: 35%;
}
.name-arch-in .couple { width: 58%; margin: 0 auto 35%; }
/* no frame plate: the type keeps its own rhythm rather than a squashed box */
.name-arch:not(:has(.name-arch-frame[style*="url"])) { aspect-ratio: auto; }
.name-arch:not(:has(.name-arch-frame[style*="url"])) .name-arch-in { width: 100%; margin-top: 0; }

.name-arch .couple { font-size: clamp(30px, 7vw, 60px); } /* sized to the ring's opening */
.name-arch .couple-date { font-size: clamp(15px, 2.4vw, 28px); letter-spacing: .16em; }
.name-arch .couple-meta { font-size: clamp(12px, 1.4vw, 16px); }
.name-arch .tagline { font-size: clamp(10px, 1.1vw, 13px); }
.name-arch .rule { margin: clamp(12px,2vw,22px) auto; width: 84px; }

.orn-mandala {
  position: absolute; left: 50%; top: 46%;
  /* 100% and not the inherited 108%: at 390px the extra 8% is 16px of
     horizontal document overflow, and .room cannot simply clip it — the route
     room holds a `position: sticky` drive stage. The medallion sits at 7%
     opacity behind the names; nobody can tell it is 16px narrower. */
  width: min(760px, 100%); aspect-ratio: 1;
  transform: translate(-50%, -50%);
  background-size: contain; background-repeat: no-repeat; background-position: center;
  opacity: .07; z-index: 1; pointer-events: none;
}

/* The garland spans the full width; only its lower swag is shown.
   Two earlier attempts were both wrong: sizing by width alone dropped the
   catenary through the heading, and capping the height shrank it to a small
   centred necklace. The fix is to draw it full width and clip the top off —
   the band is the window, --garland-hide is how much of the plate rides above
   it. Clipping happens on the band, NOT on the room: the room has to stay
   overflow-visible or its curved top edge gets cut off. */
.orn-garland-band {
  position: absolute; top: calc(var(--edge-h) * .3); left: 0; right: 0;
  height: clamp(88px, 12vh, 168px);
  overflow: hidden;
  z-index: 2; pointer-events: none;
}
/* T-TM ran its swag full-bleed and cropped it from the bottom, because its
   2048x796 thoranam was a dense curtain whose hanging depth varied between
   regenerations. This plate is the opposite object: one 488x189 light string
   with five bulbs and a knotted rope end at each corner. Stretched to 1440 and
   then cropped vertically, the only thing left inside the band was the two
   knots — two large brown posts floating over the coral with no string between
   them. So it is CENTRED and CONTAINED at its own aspect instead, which is how
   one string is actually hung over a doorway. */
.orn-garland {
  position: absolute; left: 50%;
  width: min(760px, 74%); height: auto;
  max-height: clamp(88px, 12vh, 168px);
  object-fit: contain; object-position: top center;
  transform: translateX(-50%);
  opacity: .95;
}
/* the swag hangs into the room, so its room needs headroom for the heading */
#functions { padding-top: calc(var(--pad-y) + var(--edge-h) * .55 + clamp(84px, 12vh, 168px)); }
#functions .orn-edge--top { display: none; }   /* the garland IS the top edge here */

/* orn-garland is the fairy-light swag (488x189): its rope leaves the plate at
   both TOP corners, so it takes a HEIGHT cap and hangs from the top rather than
   a width that would stretch it into the room.
   orn-toran here is the gold FILIGREE strip — a flat band, not a hang — sitting as a rule
   above the RSVP form. It takes its NATURAL ASPECT with no crop at all, and that is the
   second attempt at this rule.

   Punjabi height-capped this key and `object-fit: cover`-cropped it, which was right
   because its bead strip really was a flat band on disk. The strip delivered here is not:
   the seamx cut is 426x301 and its ink is rows 183-260, the top 55% being faint crystal
   drops that hold the alpha bbox open. Under a 46px cap `cover` rendered a small gold
   shape sliced off flat, which looks exactly like a broken image and passed every check
   there is. Moving `object-position` to the band's own centre only changed WHICH 46px
   slice of a 396px-tall scaled image was shown: a cover crop under a height cap can never
   show a whole ornament, whatever it is pointed at.

   So the FILE is the band now — cropped to its ink rows, rows only, so seamx's X seam is
   untouched — and the CSS lets it keep its own aspect at a modest width. Trimming a
   sprite to its ALPHA BBOX and trimming it to its INK are different operations, and
   `object-fit` cares about the second. */
.orn-toran {
  display: block; width: min(420px, 82%); height: auto;
  margin: 0 auto clamp(14px, 2.4vw, 28px);
  opacity: .9;
  /* Both ENDS are hard vertical cuts, and they have to be — seamx chose that cut so the
     strip tiles onto itself, which is exactly what orn-border-h needs. Drawn ONCE and
     centred, as it is here, those two cuts read as a torn-off piece of something. The
     fade is the cost of using a tileable asset as a standalone ornament. */
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 15%, #000 85%, transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0, #000 15%, #000 85%, transparent 100%);
}

/* four mirrored corners on a card, from one file */
.orn-corner {
  /* SMALLER AND FAINTER than Goa's. Goa left `orn-corner` unpainted, so these
     numbers were only ever tuned against Lantern Palace's flat paisley. This
     skin's corner is a phulkari flourish with magenta blooms and dark green
     leaves in it, and at 68px / .34 on the gold `know` cards it sat ON the body
     copy —
     four of them per card, one at each corner. Caught by looking at the room,
     not by a contrast number: it never failed a ratio, it just made the text
     hard to read. */
  position: absolute; width: clamp(26px, 3.2vw, 44px); aspect-ratio: 1;
  background-size: contain; background-repeat: no-repeat;
  opacity: .20; pointer-events: none;
}
.orn-corner--tl { top: 10px; left: 10px; }
.orn-corner--tr { top: 10px; right: 10px; transform: scaleX(-1); }
.orn-corner--bl { bottom: 10px; left: 10px; transform: scaleY(-1); }
.orn-corner--br { bottom: 10px; right: 10px; transform: scale(-1); }
.func, .know, .roll, .venue-card { position: relative; }

/* ==================================================================
   COUPLE SLOTS — a real photo, a commissioned illustration, or a
   deliberately abstract paper silhouette. Never a generated face.
   ================================================================== */
.portrait {
  position: relative;
  width: min(340px, 76%);
  margin: clamp(30px, 5vw, 56px) auto 0;
  aspect-ratio: 620 / 780;
}
.portrait--wide { width: min(380px, 82%); margin-top: clamp(40px, 6vw, 76px); }
.portrait-img {
  /* the opening of oval-frame.webp, measured: 18% down, 24% up, 18% and 17%
     in from the sides. The old 7%/10% inset drew the picture as an ellipse
     larger than the ring, so it showed outside the frame. */
  position: absolute; inset: 18.5% 17.5% 24.5% 18.5%;
  border-radius: 50%;
  overflow: hidden;
  background: rgba(247,242,230,.14);
  background-size: cover; background-position: center;
}
.room--cream .portrait-img { background-color: rgba(20,36,61,.05); }
.portrait-frame { position: absolute; inset: 0; width: 100%; height: 100%; }
.portrait[data-empty="1"] .portrait-img {
  background-size: contain; background-repeat: no-repeat; background-position: bottom center;
  opacity: .9;
}
/* the caption lives here, not baked into the SVG, so it takes the room's ink
   and stays legible on teal, indigo or cream alike */
.portrait[data-empty="1"]::after {
  content: "A photo of the two of you goes here";
  position: absolute; left: 0; right: 0; bottom: -1.9em;
  text-align: center; font-style: italic;
  font-size: clamp(12px, 1.2vw, 14px); opacity: .5;
}

@media (prefers-reduced-motion: reduce) {
  .env, .env-flap, .env-card { transition: none; }
  .env-cta { animation: none; }
  .audio-btn { transition: none; }
  .audio-btn[aria-pressed="true"] .audio-ico i { animation: none; height: 9px; }
}

/* ==================================================================
   ROOM ATMOSPHERE — the round-4 pieces
   ==================================================================
   Each one is dressing: low in the stack, never over live text, and every
   key is `optional` so a room ends up plainer rather than wrong.
   ================================================================== */
.piece { position: absolute; z-index: 1; pointer-events: none; }

/* THE ONLY SOURCE OF HORIZONTAL OVERFLOW ON THE PAGE, and it was `right: -4%`.
   That inset is inherited from a skin where this key held a flat silhouette — a coconut
   palm, a saffron flag — which can run off the frame and still read as a crop. Here it
   holds `couple-landing`: the pair standing on a gilded staircase landing, and a couple
   with a third of the staircase hanging off the edge reads as a mistake rather than as a
   composition. It is also 58px of real document overflow on a 1440 viewport (the page
   genuinely scrolled sideways) and 31px on a 390 one, where Chrome's mobile emulation
   answers by widening innerWidth to 421 and every `position: fixed` element placed from
   the right edge lands off the screenshot. Punjabi found exactly this on `piece-jharokha`
   and fixed that key; the same inset was still on this one.
   Found by bisecting the body's children with `display: none` and watching
   documentElement.scrollWidth, not by reading the stylesheet — every element in the
   journey LOOKS like it overflows because getBoundingClientRect ignores clipping. */
.piece--peacock {
  right: 1%; bottom: clamp(12px, 3vw, 40px);
  width: clamp(140px, 19vw, 300px);
  opacity: .85;
}

/* T-TM hung an oonjal swing from the story room's top edge. There is nothing
   in this skin that hangs, so that element is removed from index.html and its
   rules with it — a palm frond stands in the same room instead. */

/* T-PJ: `left: 2%`, not the inherited -6%. Every earlier skin put a flat
   silhouette here — a saffron flag, a palm frond — and a silhouette can run off
   the edge and still read. This key holds the JEEP, with the couple painted in
   it, and half a jeep hanging off the left edge reads as a mistake rather than
   as a crop. Opacity up from .5 too: at half strength on a saturated saffron
   ground it disappeared into the paper. */
.piece--jharokha {
  left: 2%; top: clamp(40px, 8vw, 140px);
  width: clamp(130px, 17vw, 270px);
  opacity: .72;
}

/* Sits on the floor of the night room. Capped by HEIGHT, not width: the plate
   is 2048x260 but the diyas are large within it, so a full-width draw made them
   a quarter of the viewport. Height-capped and centred, it reads as a row of
   lamps on the floor. */
.piece--diyas {
  left: 50%; bottom: 0;
  transform: translateX(-50%);
  height: clamp(44px, 7vh, 92px); width: auto; max-width: 118%;
  opacity: .95;
  animation: diya-flicker 4.5s ease-in-out infinite;
}
@keyframes diya-flicker {
  0%, 100% { opacity: .82; }
  38%      { opacity: 1; }
  61%      { opacity: .88; }
}

/* The lake strip. The boat was first put on the car's band travelling the other
   way, and the two crossed in the middle of the screen with the boat behind the
   car — so it gets its own field instead. */
.lake {
  position: relative;
  height: clamp(70px, 11vw, 130px);
  margin: clamp(26px, 4vw, 52px) 0 clamp(6px, 1vw, 14px);
  overflow: hidden;
  border-radius: 999px;
  /* the water past the end of the beach road. It sits ON the sea-teal ground,
     so it is only a shade or two off it — a strip of deeper water read against
     the shallows behind it, not a separate blue object. */
  background: linear-gradient(#1E7C7E, #0E4750);
  opacity: .6;
}
/* piece-boat is the lateen-sail fishing boat, and unlike T-TM's banana plant it
   FLOATS — so it goes back to Lantern Palace's reading and sits in the middle
   of the strip rather than standing on the bank. */
.piece--boat {
  left: 34%; bottom: 18%;
  width: clamp(90px, 12vw, 170px);
  opacity: .95;
}

@media (max-width: 759px) {
  .piece--jharokha { opacity: .34; width: clamp(110px, 30vw, 160px); }
  .piece--peacock  { right: 1%; width: clamp(110px, 30vw, 180px); }
  /* the windmill sits in the room's bottom-right corner; on a phone that is
     where the RSVP row ends up, so the room keeps a clear band under the
     buttons for it — it was drawn over "Add to calendar" */
  #invite .wrap { padding-bottom: clamp(150px, 40vw, 200px); }
}
@media (prefers-reduced-motion: reduce) {
  .piece--diyas { animation: none; opacity: .92; }
}

/* ==================================================================
   ?plain=1 — the painted scene with none of the site's furniture,
   so the video pipeline can shoot the live hero cleanly.
   ================================================================== */
body.is-plain .hero,
body.is-plain .hero-hint,
body.is-plain .audio-btn,
body.is-plain .mobcta,
body.is-plain .skip,
body.is-plain .env { display: none !important; }
body.is-plain { overflow: hidden; }

/* ==================================================================
   0b. THE OPENING FILM
   ==================================================================
   Full-bleed, plays once, and hands to the interactive hero on `ended`.
   The type is DOM rather than burnt into the clip — same reason the RSVP is
   a form and not a picture of one.
   ================================================================== */
.intro {
  position: fixed; inset: 0; z-index: 95;
  background: #0D1030;
  opacity: 0;
  transition: opacity .6s ease;
}
.intro[hidden] { display: none !important; }
.intro.is-in { opacity: 1; }
/* on handoff the film fades and the live hero is already behind it, seeked to
   the progress the last frame was rendered at */
.intro.is-out { opacity: 0; transition: opacity .65s ease; pointer-events: none; }

.intro-vid {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  background: #0D1030;
}
/* keeps cream type legible over whatever the clip is doing underneath */
.intro-scrim {
  position: absolute; inset: 0;
  background:
    /* T-GS: the pool is pushed DOWN to 62% and the bottom stop is deepened from
       .46 to .62. This clip is a bright daylit beach, and the couple stand full
       height in the 16:9 cut, so the title lands on a white dress in white
       serif — at T-TM's values "Rohan" was barely there. Tinted to the night
       sky, not to the ink. */
    radial-gradient(ellipse 66% 40% at 50% 62%, rgba(12,27,51,.46), rgba(12,27,51,0) 72%),
    linear-gradient(rgba(12,27,51,.26), rgba(12,27,51,0) 26%, rgba(12,27,51,0) 46%, rgba(12,27,51,.62));
}

.intro-type {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  text-align: center;
  padding: 0 var(--pad-x);
  color: var(--on-dark);
  text-shadow: 0 2px 26px rgba(12,27,51,.6), 0 1px 3px rgba(12,27,51,.45);
}
.intro-line {
  grid-area: 1 / 1;              /* every line stacks on the same cell */
  /* A grid item centred with place-items sizes to max-content, so a long line
     runs straight off the viewport instead of wrapping — the bilingual date +
     venue did exactly that on a 390px phone. */
  max-width: 100%;
  opacity: 0;
  will-change: transform, opacity;
}
.intro-line--greeting { font-style: italic; font-size: clamp(17px, 2.6vw, 30px); }
.intro-line--title {
  font-weight: 300; font-size: clamp(40px, 8.4vw, 118px); line-height: 1.02;
  display: flex; flex-direction: column; align-items: center; gap: .04em;
}
/* the joiner is a plain ampersand — set in the display italic, small, so the
   two names stay the loud things on the line */
.intro-line--title em {
  font-family: var(--serif); font-style: italic;
  font-size: clamp(15px, 2vw, 30px); letter-spacing: normal; text-transform: none;
  opacity: .8; padding-left: 0;
}
/* Josefin Sans caps want the tracking back that T-TM had to cut for Tamil. */
.intro-line--detail {
  font-family: var(--caps); letter-spacing: .17em; text-transform: uppercase;
  font-size: clamp(16px, 2.1vw, 28px); line-height: 1.9;
}
.intro-line--tag {
  font-family: var(--caps); letter-spacing: .18em;
  font-size: clamp(22px, 4.4vw, 52px);
}

/* ==================================================================
   0c. THE INVITE SCENE — config.intro.mode "scene"
   ==================================================================
   Ganesha, then the couple, then the words. Everything here is positioned in
   percentages of the scene box and sized in vmin, so the same choreography
   plays at 1440x900 and at 390x844 without a second set of timings — only the
   backdrop swaps to its 9:16 twin. GSAP owns opacity/transform; this file owns
   placement and the two ambient loops (flame flicker, heart pulse).
   ================================================================== */
/* CENTRING NOTE for the four elements GSAP animates (.isc-kolam, .isc-ganesha,
   .isc-diyas, .isc-heart): they carry NO transform here. GSAP writes the whole
   transform property when it animates scale or y, which silently drops a
   translate(-50%,-50%) written in CSS and shunts the element half its own size
   down and right. Their centring is set once in playIntroScene() with GSAP's
   own xPercent/yPercent so it survives every tween. .isc-couple is not animated
   directly, so it keeps its CSS transform. */
.iscene { position: absolute; inset: 0; overflow: hidden; }
.iscene[hidden] { display: none !important; }

.isc-backdrop {
  position: absolute; inset: 0;
  background: var(--gr-night) center / cover no-repeat;
  opacity: 0;
}
/* The gold kolam grid is baked into the plate, so it cannot be dimmed on its
   own — a flat wash of the plate's own ground colour pulls the pattern ~30%
   back toward it without shifting the hue or darkening the figures, which sit
   above this layer. */
.isc-backdrop::after {
  content: ""; position: absolute; inset: 0;
  background: rgba(110,27,43,.30);
}

/* The kolam draws itself left-to-right: a clip-path wipe, which works on a PNG
   as well as on an SVG and needs no path length to be known.
   The delivered plate is a square MEDALLION, not the wide strip this was first
   built against, so it sits BEHIND Ganesha as the floor he arrives onto rather
   than as a band under him — at strip proportions it overflowed the viewport
   bottom by 70-odd pixels. */
/* SIZING NOTE. These three are sized by HEIGHT, not width, because the brief
   specifies them in vh and a width in vmin only yields the intended vh on a
   landscape viewport — on a phone vmin is the WIDTH, so the same rule shrank
   Ganesha to 28vh where 44 was asked for. Heights are set directly; widths
   follow the plate's own aspect. */
.isc-kolam {
  position: absolute; left: 50%; top: 42%;
  height: 65vh; width: auto;       /* 1.25x Ganesha, behind him */
  z-index: 0; opacity: .5;
  clip-path: inset(0 100% 0 0);
}
.isc-kolam img { height: 100%; width: auto; display: block; }

.isc-diyas {
  /* bottom-anchored, not centred on a percentage: at 90% the pair hung 5px
     past the viewport floor */
  position: absolute; left: 50%; bottom: 3%; top: auto;
  width: min(30vmin, 300px);       /* centred by GSAP — see CENTRING NOTE */
  opacity: 0;
}

.isc-ganesha {
  /* Sized from the delivered sprites, not guessed: murti 1204x1604 drawn at
     78% of this width, lotus 1200x758 overlapped by 14%, so the stack is about
     1.53x whatever width is set here. 300px therefore composes ~460px tall and
     leaves the blessing a clear band beneath it. */
  position: absolute; left: 50%; top: 42%;
  height: 52vh; width: auto;       /* centred by GSAP — see CENTRING NOTE */
  z-index: 2;
  opacity: 0;
}
.isc-murti { height: 100%; width: auto; display: block; position: relative; z-index: 2; }

.isc-sparks { position: absolute; inset: 0; pointer-events: none; }
.isc-sparks img {
  position: absolute; width: min(5.5vmin, 54px);
  opacity: 0; will-change: transform, opacity;
}
/* a ring around where Ganesha lands */
/* a ring around Ganesha's landing point (50%, 38%) — scattered across the whole
   frame they read as background sparkle rather than as his arrival */
.isc-sparks img:nth-child(1) { left: 33%; top: 26%; }
.isc-sparks img:nth-child(2) { left: 65%; top: 25%; }
.isc-sparks img:nth-child(3) { left: 28%; top: 44%; }
.isc-sparks img:nth-child(4) { left: 70%; top: 43%; }
.isc-sparks img:nth-child(5) { left: 42%; top: 17%; }
.isc-sparks img:nth-child(6) { left: 58%; top: 18%; }
.isc-sparks img:nth-child(7) { left: 36%; top: 56%; }
.isc-sparks img:nth-child(8) { left: 64%; top: 55%; }

.isc-couple {
  /* Sits in the TOP two thirds — the words own the bottom third. Width is set
     from the delivered sprite (1427x1688, so 1.18x as tall as it is wide): at
     380px the pair composes ~450px and clears the title's cap height. */
  position: absolute; left: 50%; top: 42%;
  height: 58vh; width: auto;
  transform: translate(-50%, -50%);
}
/* the cream pool the bride stands against — see the note in index.html */
.isc-lift {
  position: absolute; left: 50%; top: 52%;
  height: 74vh; aspect-ratio: 1 / 1; width: auto;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(closest-side,
    rgba(247,242,230,.38), rgba(247,242,230,.17) 46%, rgba(247,242,230,0) 74%);
  pointer-events: none; opacity: 0;
}
.isc-heart {
  position: absolute; left: 50%; top: 50%;
  height: 48vh; width: auto;       /* centred by GSAP — see CENTRING NOTE */
  opacity: 0;
}
.isc-pair { position: relative; height: 100%; width: auto; display: block; opacity: 0; }

.isc-petals { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }

.isc-blessing {
  position: absolute; left: 0; right: 0; top: 74%;
  z-index: 4;                      /* over the lotus, never behind it */
  margin: 0; padding: 0 var(--pad-x);
  text-align: center; color: var(--zari);
  font-family: var(--serif);
  font-size: clamp(17px, 2.6vw, 30px);
  opacity: 0;
  text-shadow: 0 2px 20px rgba(12,27,51,.7);
}

/* the scene lays the type lower than the video path did — the couple occupies
   the middle of the frame where the film had sky */
/* The words sit in a band above the bottom edge, not against it. On a phone the
   layout viewport is taller than the captured/visible frame, and a line resting
   on the last few percent of the height falls off the bottom — the bilingual
   date line, which wraps to two rows, did exactly that. */
/* Anchored to a fixed line rather than to the bottom edge: every line's TOP
   sits at 74vh, ~4vh under the figures centred at 42%, so the text reads as
   belonging to them instead of stranded at the foot of the frame. */
.intro.is-scene .intro-type {
  align-items: start; justify-items: center;
  padding-top: 74vh; padding-bottom: 0;
}
/* Video mode too: the film composes its figures in the upper ~65% and leaves
   the lower third clear, so the live lines sit there instead of dead centre
   (2026-09-02: figures were being kept tiny to dodge centred text). */
.intro:not(.is-scene):not(.is-still) .intro-type {
  align-items: start; justify-items: center;
  padding-top: 68vh; padding-bottom: 0;
}
.intro:not(.is-scene):not(.is-still) .intro-line--title { font-size: min(9vh, 8.4vw, 110px); }
@media (max-width: 760px) {
  .intro:not(.is-scene):not(.is-still) .intro-type { padding-top: 66vh; }
}
/* The title is the only line tall enough to fall off the bottom from 74vh: two
   118px rows plus the joiner is 30vh, which needs 104. Capped against the
   HEIGHT as well as the width so it always fits the 26vh the band leaves it. */
.intro.is-scene .intro-line--title { font-size: min(9.4vh, 8.4vw, 118px); }
.intro.is-scene .intro-line--tag   { font-size: min(5.2vh, 4.4vw, 52px); }
.intro.is-scene .intro-scrim {
  background:
    radial-gradient(ellipse 70% 40% at 50% 78%, rgba(12,27,51,.62), rgba(12,27,51,0) 72%),
    linear-gradient(rgba(12,27,51,.28), rgba(12,27,51,0) 30%, rgba(12,27,51,0) 54%, rgba(12,27,51,.5));
}

@media (max-width: 759px) {
  /* Heights, not widths — see the SIZING NOTE above. The numbers are ~13%
     larger than the vh targets they hit: on a phone `vh` resolves against the
     DEVICE height while the layout viewport is taller, so a literal 44vh
     rendered as 39% of the frame the guest actually sees. These are calibrated
     against the measured result, not the nominal unit. */
  .isc-ganesha { height: 50vh; width: auto; top: 42%; }   /* ≈ 44% of frame */
  .isc-couple  { height: 54vh; width: auto; top: 42%; }   /* ≈ 48% of frame */
  .isc-kolam   { height: 62vh; width: auto; top: 42%; }   /* 1.25x Ganesha   */
  .isc-heart   { height: 45vh; width: auto; }
  .isc-lift    { height: 68vh; }
  .isc-diyas   { width: min(44vmin, 175px); bottom: 4%; }
  .isc-blessing { top: 74%; }
  /* calibrated the same way as the heights above: a literal 74vh lands at 66%
     of the visible frame, which is exactly where the couple's feet end */
  .intro.is-scene .intro-type { padding-top: 79vh; }
}

/* prefers-reduced-motion: the scene is a held still, not a sequence */
@media (prefers-reduced-motion: reduce) {
  .iscene .isc-diyas, .iscene .isc-sparks { display: none; }
  .isc-kolam { clip-path: none; }
}

.intro-skip {
  position: absolute; right: clamp(14px, 2.4vw, 30px); bottom: clamp(14px, 2.4vw, 30px);
  z-index: 3;
  padding: 9px 20px; border-radius: 999px;
  border: 1px solid rgba(247,242,230,.45);
  background: rgba(33,16,22,.4);
  backdrop-filter: blur(6px);
  color: var(--on-dark);
  font-family: var(--caps); letter-spacing: .2em; text-transform: uppercase;
  font-size: 11px; cursor: pointer;
}
.intro-skip:hover { background: rgba(33,16,22,.62); }

/* reduced motion gets the last frame and the lines stacked, no video */
.intro.is-still {
  background-size: cover; background-position: center; background-repeat: no-repeat;
}
.intro.is-still .intro-vid,
.intro.is-still .intro-skip { display: none; }
.intro.is-still .intro-type {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: clamp(12px, 2.6vh, 26px);
}
.intro.is-still .intro-line { grid-area: auto; opacity: 1; transform: none; }

/* Words the record supplies in Hindi or Marathi. This skin's own faces have
   no Devanagari, so those glyphs would fall back to whatever the phone has.
   main.js marks any leaf whose text is Devanagari with .dv after render. */
.dv{font-family:"Tiro Devanagari Hindi",Georgia,serif!important;letter-spacing:0!important;text-transform:none!important}
html[lang="mr"] .dv{font-family:"Tiro Devanagari Marathi",Georgia,serif!important}

/* A form told to hide must hide: `display` here outranks the browser's own
   [hidden] rule, so the submitted form used to sit beside its own thank-you. */
.form[hidden],form[hidden]{display:none!important}


/* Couple-uploaded photographs — the venue card and the hotel list. Both are
   optional and both are hidden until a file exists, so a wedding with no
   photographs looks exactly as this design did before they were possible. */
.venue-photo{display:block;width:100%;max-width:520px;aspect-ratio:16/9;object-fit:cover;
  margin:0 auto 18px;border-radius:6px}
/* `display:block` above beats the UA rule for [hidden] (same trap as .audio-btn),
   so an empty slot painted a blank 16:9 box above "The venue". */
.venue-photo[hidden]{display:none!important}
.hotel-photo{width:84px;height:84px;object-fit:cover;border-radius:6px;flex:none;margin-right:14px}
@media(max-width:560px){.hotel-photo{width:62px;height:62px;margin-right:10px}}

/* Scroll reveals — a class and a transition (see the reveals in main.js).
   Elements get .rv from JS only, so without JS nothing is ever hidden. */
.rv { opacity: 0; transform: translateY(46px); transition: opacity .9s cubic-bezier(.2,.7,.2,1), transform .9s cubic-bezier(.2,.7,.2,1); }
.rv-l { transform: translateX(-70px); }
.rv-r { transform: translateX(70px); }
.rv.is-shown { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) { .rv { opacity: 1; transform: none; transition: none; } }
