:root {
  /* The brief's five, verbatim, plus four the five do not cover. Prefixed `--sk-` 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. */
  --sk-navy:      #1B2340;   /* midnight navy — the INVITE ground */
  --sk-gold:      #C9A24A;   /* warm gold, the METAL — monogram, hairlines. Never a ground:
                                at its own value a room and its gilt are one colour and
                                every rule in it disappears (Punjabi's rule). */
  --sk-champagne: #E8D9B5;   /* the FUNCTIONS ground */
  --sk-amber:     #E39A2C;   /* neon amber — the STORY ground. Kept verbatim from the brief,
                                unlike Ballroom, which had to reject this exact value: there
                                it would have read as an Indian saffron ground. Here it is
                                the amber of a lit sign over a black city and there is not
                                one other Indian cue on the page for it to collude with. */
  --sk-charcoal:  #2A2E38;   /* the brief's fifth. It is the SCRIM AND SHADOW base and is
                                NOT the route room's ground — see --sk-slate. */

  /* four derived values the five do not cover */
  --sk-slate:     #3C4356;   /* the ROUTE ground: --sk-charcoal lifted. At #2A2E38 the route
                                room and the invite room sit four points of L* apart and both
                                are dark blue-grey, so two different rooms read as one. */
  --sk-twilight:  #2A4A8C;   /* the RSVP ground: the brief's "navy" a second time, taken
                                saturated and lighter so it is a different room. */
  --sk-brass:     #6F5320;   /* the KNOW ground — the metal's colour at a ground's value,
                                still never the metal itself. */
  --sk-night:     #0C111E;   /* the navy after the last light goes out — gate, footer,
                                countdown. The countdown takes the AMBER tile at 24%
                                multiply, and a ground this dark moves under 2% under it. */

  /* THE SEVEN GROUNDS ARE A LADDER, and on this skin the ladder rule had to be EXTENDED.
     Ballroom's failure was three MUSTARDS inside .15 of each other — one hue family, so
     lightness was the only separator and it was not separating them. Five of these seven
     grounds are dark and four of those are blue, so a pure-lightness ladder is not
     reachable: slate and twilight are six points of L* apart and always will be. The guard
     that works here is that NO PAIR MAY BE CLOSE IN BOTH LIGHTNESS AND CHROMA. L* runs
        functions 87 · story 70 · know 37 · rsvp 31 · route 25 · invite 14 · countdown 6
     and the one close pair (route 25, rsvp 31) is a near-neutral against a saturated blue —
     HLS saturation .18 against .54 — which reads as a grey room beside a blue one.
     Change one of these and check the whole ladder, not the pair. */

  /* the rooms — LP slot names, rooftop values, each matched to its paper tile */
  --teal:      var(--sk-navy);        /* paper-teal tile is paper-navy */
  --teal-deep: #121831;
  --pink:      var(--sk-champagne);   /* paper-pink tile is paper-champagne */
  --pink-deep: #CDB985;
  --gold:      var(--sk-slate);       /* paper-marigold tile is paper-charcoal */
  --gold-deep: #262B38;
  --zari:      var(--sk-gold);        /* the warm metal, where LP had gold */
  --zari-deep: #9A7A2E;
  --indigo:    var(--sk-amber);       /* the story room */
  --indigo-lo: var(--sk-night);
  --cream:     var(--sk-twilight);
  --cream-deep:#1E3767;
  --rose:      var(--sk-champagne);

  /* Ink measured, not assumed — and this skin INVERTS the split every previous one shipped.
     FIVE of the seven grounds are DARK (it is a midnight card), so five rooms take
     --on-dark #F4EFE3 and only functions and story take the ink.
        invite    #1B2340 + on-dark  14.8:1      functions #E8D9B5 + ink  12.2:1
        route     #3C4356 + on-dark   8.6:1      story     #E39A2C + ink   7.2:1
        rsvp      #2A4A8C + on-dark   6.9:1
        know      #6F5320 + on-dark   6.3:1
        countdown #0C111E + on-dark  17.0:1
     --on-dark on champagne is 1.2:1 and on amber 2.1:1, so both FAIL and both take the ink.
     The dark-room button / rule / ampersand selectors below are FIVE classes here where
     Ballroom's were two: that membership is not inheritable and Punjabi and Ballroom each
     had to rewrite it too.
     The METAL is the other half of the trap, and on this skin it mostly runs the other way:
     --sk-gold is 4.6:1 on navy and 5.5:1 on night, so it carries rules and monograms
     happily on the five dark rooms — but it is 1.4:1 on champagne and 1.1:1 on amber, so a
     gilt hairline on either LIGHT room is present, measured, in the DOM and invisible.
     Those two take the ink at low opacity instead. */
  --ink:        #141A2E;
  --ink-soft:   #38405C;
  --on-dark:    #F4EFE3;
  --on-dark-soft:#DED5C4;

  /* NEITHER pairing this engine already ships. The eight Indian skins share Cormorant
     Garamond + Marcellus; Ballroom kept Cormorant and swapped in Josefin Sans. Both are
     quiet, warm, LOW-CONTRAST faces for a card printed on paper, and this card is not
     printed — it is lit.
     Playfair Display for display: a Transitional with vertical stress, flat unbracketed
     serifs and an extreme thick/thin, which is a 19th-century city masthead rather than a
     wedding script. The contrast is the functional argument and not a taste one — five of
     the seven rooms are dark grounds, and a low-contrast Garalde set in pale ink on navy
     goes soft and grey at display size where Playfair's hairlines stay crisp.
     Outfit for caps and body: a geometric grotesque with a tall x-height and a
     single-storey `a` — the lettering modern tower signage and lift plates actually use,
     and it survives the .28em tracking this skin's caps lines take. */
  --serif:  "Playfair Display", Georgia, "Times New Roman", serif;
  --body:   "Outfit", "Avenir Next", "Helvetica Neue", Arial, sans-serif;
  --caps:   "Outfit", "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 660x998 canvas at (951,165) 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 .3715 x1 .6293 y0 .1146 y1 .8076. */
  --gate-door-left:  37.15%;
  --gate-door-top:   11.46%;
  --gate-door-w:     25.78%;
  --gate-door-h:     69.30%;
  --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 .2604 x1 .7389 y0 .3457 y1 .8723. */
@media (max-width: 759px) {
  :root {
    --gate-door-left: 26.04%;
    --gate-door-top:  34.57%;
    --gate-door-w:    47.85%;
    --gate-door-h:    52.66%;
  }
}

*, *::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,17,30,.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 — here it is two olive trees and a
   fairy-light string, not a wall. Ballroom found this the expensive way: at
   `bottom: 22%` the cut ran as a plainly visible ruled line across the outer thirds of
   its hero with the mid band both wider and higher than it, and it passed every check
   that did not involve looking at the screenshot. On this skin the back band's cut is a
   WATERLINE under the city, which is if anything a more convincing false horizon.

   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 148, so the towers keep an architectural scale rather than shrinking to
   buy the fix. 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%; }
/* T-SK: both plates were TRIMMED TO THEIR INK before any of this was measured. As
   delivered palace-back was 46% empty rows and palace-mid 38% — rows that .lyr
   bottom-aligns straight off the top of the hero anyway, so they cost 40% of the file
   and made band_over_width a number about the export rather than about the painting.
   0.304 (back) and 0.347 (mid) after the trim. */

/* 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%;
  /* min(92vh, 54%) 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 54% is 778px, the percentage arm wins, and 778px 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.
     54 rather than Ballroom's 52 because this gate plate is a whole rooftop pavilion —
     the doors themselves are only 25.8% of it — so at 52% the entrance stopped being the
     subject of its own hero. */
  width: min(92vh, 54%);
  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,17,30,.55), 0 1px 3px rgba(12,17,30,.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,17,30,.34), rgba(12,17,30,.18) 54%, rgba(12,17,30,0) 88%);
}

.hero-eyebrow {
  text-shadow: 0 1px 14px rgba(12,17,30,.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,17,30,.55), 0 1px 3px rgba(12,17,30,.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,17,30,.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.820 and palace-back-9x16 is 0.751 after the ink trim, 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 -2% so its hard waterline falls below an 844px fold entirely.
     These are NOT Ballroom's 210/160 — that skin's 9:16 bands are 0.635 and 0.862, and
     at 210% the skyline here filled two thirds of the phone screen and buried the gate.
     185/145 was chosen by compositing two candidates SIDE BY SIDE at 390x844 with
     hero.py and looking at them. */
  .lyr--back { width: 185%; bottom: -2%; opacity: .85; }
  .lyr--mid  { width: 145%; bottom: -9%; }

  /* 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 entrance keeps the
     doors and loses both olive trees, both lanterns and the whole checkerboard
     wall, at the exact moment the gate is the whole screen. Its doors are also a
     DIFFERENT DESIGN (a plain brass casing rather than a lit surround), which is
     why doors.py measures it separately instead of scaling the desktop rect.
     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.
     90vw: this doorway is 49.4% of its own plate against 25.8% on desktop. */
  .gate {
    width: 90vw;
    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.3% 52.7%;
  }
}

.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. */
/* FIVE of these seven grounds are LIGHT — one more than Punjabi's four — so five of
   the seven take the ink and only two take --on-dark. Measured against #241016:
   ivory 15.1:1, champagne 12.4:1, antique gold 9.0:1, amber 6.0:1, crystal white 17.2:1.
   Against --on-dark #FBF9F4: burgundy 10.8:1, night 17.7:1. */
.room--teal     { background: var(--sk-navy);       color: var(--on-dark); --edge-fill: var(--sk-navy); }       /* The Invitation */
.room--pink     { background: var(--sk-champagne);  color: var(--ink);     --edge-fill: var(--sk-champagne); }  /* The Night, in Order */
.room--marigold { background: var(--sk-slate);      color: var(--on-dark); --edge-fill: var(--sk-slate); }      /* Getting Up There */
.room--indigo   { background: var(--sk-amber);      color: var(--ink);     --edge-fill: var(--sk-amber); }      /* How It Started */
.room--cream    { background: var(--sk-twilight);   color: var(--on-dark); --edge-fill: var(--sk-twilight); }   /* Say You'll Be There */
/* 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 CALL, re-made rather than inherited. Goa's room--palm was a DARK palm
   green; Ballroom flipped it to pale champagne and took it OUT of both dark selectors.
   Here it is brass #6F5320 and it is DARK again, so it goes back IN. That is the whole
   lesson of this block: which rooms are dark is a per-skin fact and copying the previous
   skin's membership silently ships unreadable type. */
.room--palm     { background: var(--sk-brass);      color: var(--on-dark); --edge-fill: var(--sk-brass); }      /* Before You Come Up */
.room--night    { background: var(--indigo-lo);     color: var(--on-dark); --edge-fill: var(--indigo-lo); }

/* FIVE of the seven rooms are dark grounds on this skin, against Ballroom's two, so five
   take light buttons: invite, route, rsvp, know and the countdown. Only the champagne
   functions room and the amber story room keep the dark ones. Ballroom's list was
   `.room--night, .room--marigold` and copying it here left three rooms with a near-black
   button on a near-black ground. */
.room--night .btn, .room--marigold .btn,
.room--teal .btn, .room--cream .btn, .room--palm .btn {
  background: var(--on-dark); color: var(--ink); border-color: var(--on-dark);
}
.room--night .btn--ghost, .room--marigold .btn--ghost,
.room--teal .btn--ghost, .room--cream .btn--ghost, .room--palm .btn--ghost {
  background: transparent; color: var(--on-dark); border-color: rgba(244,239,227,.6);
}
/* Hairline rules, and here the METAL is mostly the answer rather than the problem —
   --sk-gold is 4.6:1 on navy, 3.0:1 on twilight, 2.4:1 on brass and 5.5:1 on night, so it
   carries a gilt hairline on four of the five dark rooms. It CANNOT carry one on either
   light room: 1.4:1 on champagne and 1.1:1 on amber, which is a rule that is present,
   measured, in the DOM and invisible. Those two take the ink at low opacity. The slate
   route room is the one dark room the gold does not clear either (1.9:1), so it takes the
   pale ink like the countdown. */
.room--marigold .rule, .room--night .rule { background: var(--on-dark); opacity: .5; }
.room--teal .rule, .room--cream .rule, .room--palm .rule { background: var(--sk-gold); opacity: .85; }
.room--pink .rule, .room--indigo .rule { background: var(--ink); opacity: .3; }
/* The ampersand is an accent. On the two LIGHT rooms it takes the deep navy ink; on the
   five dark ones it takes the metal, which is the only place on this skin the gold gets to
   behave like gold. */
.room--pink .amp, .room--indigo .amp { color: var(--sk-navy); }
.room--teal .amp, .room--cream .amp, .room--palm .amp,
.room--marigold .amp, .room--night .amp { color: var(--sk-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,26,46,.22);
  border-radius: clamp(30px, 5vw, 64px);
  box-shadow: 0 18px 40px rgba(20,26,46,.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 WET ASPHALT UNDER SODIUM LIGHT, and it is the one place on this skin where
   the band goes DARKER than its room rather than lighter. Goa's problem was a teal band
   on a teal room and Ballroom answered it with pale marble on burgundy — but the route
   room here is slate #3C4356, and a pale band under a black car at midnight would read
   as daylight. So it is a near-black road that lifts to a warm sodium sheen at the top
   where the streetlights land, which is what actually makes it read as a road the car is
   crossing rather than as a stripe. Contrast against the room is 2.6:1 the other way. */
.drive-band {
  position: absolute; left: 0; right: 0; bottom: 0; height: 34%;
  background: linear-gradient(#4A4232, #14161F 46%, #0E1019);
}
/* the lit lip along the top of the road, so the band carries something even at scroll
   positions between car poses. Warm, because every light source on this skin is */
.drive-band::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 5px;
  background: linear-gradient(rgba(227,154,44,.9), rgba(227,154,44,0));
  opacity: .95;
}
/* THE RHYTHM IS A BROKEN CENTRE LINE, and on this skin that is finally the literal
   truth. Goa broke its line into faint strokes because a steady dash on water reads as
   tarmac; Ballroom replaced it with a continuous inlay because a dashed line down the
   middle of a ballroom floor reads as a road-marking mistake. This IS a road — a coast
   road at midnight — so the dash comes back, warm rather than white (nothing on this
   page is lit by daylight) and set below centre so the car's own reflection sits on it. */
.drive-band::after {
  content: ""; position: absolute; left: 0; right: 0; top: 46%; height: 3px;
  background:
    repeating-linear-gradient(90deg,
      rgba(227,154,44,.42) 0 62px, rgba(0,0,0,0) 62px 128px);
}
/* 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 black MERCEDES CONVERTIBLE on a night road, with
   the couple already sitting in 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 in the car twice.
   Bound by HEIGHT, like Goa's and unlike Lantern Palace's car. 24vh rather than
   Punjabi's 28: that plate was a tractor with two people riding on TOP of it and needed
   the height; this is a low, wide car and at 28vh it overran the band. The plate carries
   its own skyline backdrop, so it is also the one drive sprite on this engine that is
   not a clean silhouette — the drop-shadow below is what keeps it from floating.
   object-position is BOTTOM, so the wheels land on the road. */
.drive-car {
  width: auto; max-width: 100%;
  height: clamp(140px, 24vh, 320px);
  object-fit: contain; object-position: bottom center;
  filter: drop-shadow(0 14px 18px rgba(12,17,30,.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,26,46,.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,17,30,.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: 29.43%;
  --scr-y0: 22.17%;
  --scr-w:  41.14%;
  --scr-h:  51.04%;
  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,17,30,.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,26,46,.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,26,46,.3);
  cursor: pointer; transition: background .2s, color .2s, border-color .2s;
}
.toggle input:checked + span { background: var(--sk-slate); color: var(--on-dark); border-color: var(--sk-slate); }
.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,26,46,.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;
}
/* The pale card above is the DEFAULT here, because five of the seven rooms are dark and
   it is right on all five. Only the two LIGHT rooms need ink edges — Ballroom's list was
   the other way round (four light rooms named, two dark ones defaulting) and carrying it
   over put a translucent white card on a champagne ground and a dark card on navy. */
.room--pink .know, .room--indigo .know {
  border-color: rgba(20,26,46,.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,17,30,.74) 0%, rgba(12,17,30,.5) 60%, rgba(12,17,30,.12) 100%),
    linear-gradient(180deg, rgba(12,17,30,.5) 0%, rgba(12,17,30,.12) 38%, rgba(12,17,30,.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,17,30,.68) 0%, rgba(12,17,30,.76) 55%, rgba(12,17,30,.88) 100%),
      linear-gradient(180deg, rgba(12,17,30,.58) 0%, rgba(12,17,30,.30) 40%, rgba(12,17,30,.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 TWICE. Punjabi's rgba(16,28,58,…) cold royal indigo became Ballroom's
      rgba(26,16,20,…) near-black burgundy, and neither belongs on a midnight-navy card:
      a burgundy haze over a blue-hour skyline is a warm cast on a cold plate and reads
      as a colour-managed screenshot. Every scrim and shadow in this file is
      rgba(12,17,30,…) now — the skin's own --sk-night. It was rgba(16,28,58,…), 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,17,30,.56) 0%, rgba(12,17,30,.74) 60%, rgba(12,17,30,.90) 100%),
    linear-gradient(180deg, rgba(12,17,30,.58) 0%, rgba(12,17,30,.26) 40%, rgba(12,17,30,.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,17,30,.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,17,30,.85), 0 1px 3px rgba(12,17,30,.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,17,30,.75), 0 1px 4px rgba(12,17,30,.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,17,30,.7);
  background: rgba(12,17,30,.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); }
/* the gold ornament strip reads at .3 on the five dark grounds and needs lifting on the
   two light ones */
.room--pink .orn-edge, .room--indigo .orn-edge { opacity: .5; }

/* T-SK: THE ARCH IS A CROWN HERE, NOT A FRAME, and that is forced by the plate.
   Every earlier skin's ornament-arch is a TALL PORTRAIT frame — Punjabi's is 1024x1536,
   Ram Sita's 1400x1747 — and `.name-arch` inherited `aspect-ratio: 1024/1536` from that.
   This skin's `light-arch` is 511x342: WIDE and SHORT. Drawn `contain` inside a
   560x840 box it landed 560x375 dead centre, i.e. directly BEHIND the type — "Myra" sat
   across two lit brass lanterns and the date line was unreadable over the voile drapes.
   Caught by screenshotting the block, not by any measurement; nothing in the DOM is wrong.

   It cannot be repaired by scaling either, and that is worth writing down. The clear
   opening between this arch's two column stacks is about 36% of the plate width, so a box
   wide enough to hold an 84px name INSIDE the opening would be ~980px of arch on a 1080px
   wrap — the ornament would become the section. So the frame goes ABOVE the names and the
   type keeps its own rhythm underneath, which is what this painting actually is: an
   entrance you walk through, photographed head-on, not a cartouche. Same kind of call
   Ballroom made when its tileable strip could not serve as a standalone ornament.

   `.name-arch-frame` keeps its class and its `data-img` so the engine is untouched; only
   its box changes. The `:not(:has(...))` pair below still governs the no-plate case. */
.name-arch {
  position: relative;
  width: min(620px, 100%);
  margin: clamp(18px, 3vw, 34px) auto 0;
  display: block;
}
.name-arch-frame {
  /* The light arch is a frame, and the names sit inside it — not under it.
     Its opening, measured from the cropped plate's alpha, is the middle 41%
     of its width from very near the top down to the open foot. The plate
     hangs at the top of the box; the names block starts 28.5% down (which
     puts its centre at the opening's), and the date runs on below the foot. */
  position: absolute; top: 0; left: 0;
  width: 100%; aspect-ratio: 421 / 342; /* the cropped plate */
  background-size: 100% auto; background-repeat: no-repeat; background-position: top center;
  margin: 0;
  pointer-events: none;
}
.name-arch-in {
  position: relative;
  width: 100%;
  padding-top: 28.5%;
}
.name-arch-in .couple { width: 44%; margin: 0 auto 28%; }
/* No frame plate: the type keeps its own rhythm rather than sitting under a 415px hole.
   COLLAPSED, NOT `display: none` — and that distinction cost a round. The arch is a LAZY
   asset: paintAssets() observes it with an IntersectionObserver and sets its
   background-image when it comes near the viewport. An element hidden with
   `display: none` is never intersected, so it never gets a background, so the
   `:not([style*="url"])` test never stops matching and it stays hidden forever. The first
   version of this rule used `display: none` and the arch simply vanished from the room,
   with no error and nothing in the DOM to explain it. */
.name-arch-frame:not([style*="url"]) { aspect-ratio: 12 / 1; margin-bottom: 0; }

.name-arch .couple { font-size: clamp(30px, 7vw, 60px); } /* sized to the arch'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 this skin's frame plate, measured from its alpha (top,
     right, bottom, left, plus half a percent inside the ring). The 7%/10%
     inset this rule shipped with drew the picture larger than the ring, so
     it showed outside the frame. */
  position: absolute; inset: 21.5% 19.5% 15.5% 21.5%;
  border-radius: 50%;
  overflow: hidden;
  background: rgba(247,242,230,.14);
  background-size: cover; background-position: center;
}
.room--pink .portrait-img, .room--indigo .portrait-img { background-color: rgba(20,26,46,.06); }
.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. */
  /* a shade or two deeper than this room's own ground (--sk-slate #3C4356), never the Goa sea-teal this rule was copied with */
  background: linear-gradient(#2B303D, #1E212B);
  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: 0; right: 0; margin: 0 auto; bottom: 18%; /* centred, still — see the note in main.js */
  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); }
}
@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,17,30,.46), rgba(12,17,30,0) 72%),
    linear-gradient(rgba(12,17,30,.26), rgba(12,17,30,0) 26%, rgba(12,17,30,0) 46%, rgba(12,17,30,.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,17,30,.6), 0 1px 3px rgba(12,17,30,.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(--sk-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,17,30,.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,17,30,.62), rgba(12,17,30,0) 72%),
    linear-gradient(rgba(12,17,30,.28), rgba(12,17,30,0) 30%, rgba(12,17,30,0) 54%, rgba(12,17,30,.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; } }

/* The corner piece of the invite room (data-img="piece-peacock") sits in the
   room's bottom-right; on a phone that corner is where the RSVP row ends up,
   and the piece was drawn over "Add to calendar". A clear band under the
   buttons keeps them apart. (Measured at 430px on every skin.) */
@media (max-width: 759px) { #invite .wrap { padding-bottom: clamp(150px, 40vw, 200px); } }
