/* Royale

   Every gate measurement below is written in --u: one pixel of the design the
   gate was drawn at, so the whole cover (card, type, corner artwork, star
   lattice) scales as one piece instead of drifting apart at other sizes.

   Portrait - a phone, or any window taller than it is wide - was drawn on a
   402px-wide screen: the card is media/Wrapper_P.svg, a band wrapped across
   the two side-by-side doors, and 1u is 1/402 of the screen's width. That is
   capped at 500px, so a tablet's type stays in proportion, and held to 47.5%
   of the screen's height, so a short screen shrinks the whole cover rather
   than pushing the elephants off the doors: the doors take the height less a
   --gate-inset band above and below (84.3% of it), and the card's 419u plus
   the sprigs' 160u above it and the elephants' 126u below needs 705u of that,
   so 402u can be no more than 402/705 x 84.3% = 48% of the height.

   Landscape turns the gate on its side: the doors are stacked one above the
   other, and the card - media/Wrapper_D.svg - stands up the middle of them,
   the full height of the screen. It was drawn on a 1024px-tall screen with its
   type 1.43x the portrait card's, so there 1u is 1/717 of the height and the
   type measures the same in u either way. A short landscape phone keeps a
   floor under --u so the card stays readable; the doors' artwork follows the
   height alone (--k), so it always fits its door. */

:root {
  --font-display: "Fraunces", Georgia, serif;
  --font-names: "Cormorant Garamond", Georgia, serif;
  --font-sans: "Sora", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-devanagari: "Tiro Devanagari Sanskrit", "Nirmala UI", serif;

  --royale-maroon: #560f31;
  --royale-maroon-deep: #3e0823;
  --royale-crimson: #7f1038;
  --royale-door: #4a021e;
  --royale-flap: var(--royale-door);
  /* The crimson that edges the gate's card (the bands drawn into
     Wrapper_P/_D.svg), picked up by the doors' seam and the flaps' edges. */
  --royale-band: #800033;
  --royale-gold: #feb669;
  --royale-cream: #fdf2e1;
  /* The page's cream. The hero fades into it at its foot (see
     .royale-hero__frame::after) and the sections below carry on in it. */
  --royale-paper: #fde5c9;
  --royale-keyline: #c29a70;
  --royale-ink: #8c5a2c;
  /* The ink on the ceremony cards, on their cream panel. */
  --royale-ceremony-ink: #40080b;

  /* The screen's height - the small viewport where a browser has one (see
     the @supports below), so a phone's toolbar never hides the gate's foot. */
  --screen-h: 100vh;

  --u: calc(min(100vw, 0.475 * var(--screen-h), 500px) / 402);

  /* How far the doors stop short of the screen's ends - the top and bottom
     in portrait, the sides in landscape (see there) - so the star flaps
     behind them show in that band. 7.86% of the screen's long side. */
  --gate-inset: calc(0.0786 * var(--screen-h));

  /* The crimson strip down the doors' seam, and along the flaps' inner
     edges - one figure so the two always match. */
  --seam-w: calc(8 * var(--u));

  /* media/border_element.svg - two gold rules with a star between them -
     repeated down an edge: the gate's card and doors in landscape, and the
     scroll cards' sheets. The drawing is 20.04 x 18.04 of the file's 21 x 19;
     the #svgView crops to it exactly, or the rules would break with a
     hairline gap every repeat. Each use sets its own tile size from that
     ratio. */
  --border: url("media/border_element.svg#svgView(viewBox(0,0,20.0386,18.0385);preserveAspectRatio(none))");

  /* The gate doors' pattern: media/motif.png, faint (22%), in a half-drop
     lattice - columns 63.7u apart, every other one dropped half a row, rows
     123u apart, each motif 53u wide - measured off the design. CSS cannot
     space a repeating PNG out, so media/motif_tile.svg holds one repeat
     (two columns) with the PNG embedded in it; it is built from motif.png,
     and wants rebuilding if that file changes. */
  --motif-tile: calc(127.4 * var(--u)) calc(123 * var(--u));

  /* Opening sequence, measured from the tap. The doors start as the card is
     all but clear of the screen (~75% of its eased travel).
     app.js ends the gate on the last flap's animationend, so it needs no
     copy of these. */
  --card-dur: 1000ms;
  --card-ease: cubic-bezier(0.65, 0, 0.35, 1);
  --door-delay: 750ms;
  --door-dur: 1800ms;
  --door-ease: cubic-bezier(0.6, 0, 0.3, 1);
  /* The flaps start while the doors are only halfway open: 45deg of the
     doors' 100deg swing is 44.5% of the way through their eased time,
     750 + 0.445 x 1800 = ~1550ms. The flaps get a quicker-starting ease than
     the doors so that start actually shows (~25deg in their first 300ms,
     against ~7deg on the doors' ease), and the names begin as the first flap
     passes 50deg (~2005ms) and has lifted clear of the sky they sit in. */
  --flap-delay: 1550ms;
  --flap-dur: 1300ms;
  --flap-ease: cubic-bezier(0.3, 0, 0.3, 1);
  --names-delay: 2000ms;
  --names-stagger: 220ms;
  --names-dur: 1300ms;
}

@supports (height: 1svh) {
  :root {
    --screen-h: 100svh;
  }
}

@media (orientation: landscape) {
  :root {
    --k: calc(var(--screen-h) / 717);
    --u: max(var(--k), 0.8px);
    /* Wrapper_D's 579 x 1024 at the design's own height, in u. */
    --card-w: calc(404 * var(--u));
    /* Along the sides here, and only ever used as left/right, so a share of
       the gate's own width. */
    --gate-inset: 7.86%;
    --seam-w: calc(4 * var(--u));

    /* The gate's border (see --border): as wide as the card's crimson
       bands, 15 of Wrapper_D's 579. */
    --border-w: calc(var(--card-w) * 15 / 579);
    --border-tile: var(--border-w) calc(var(--border-w) * 18.0385 / 20.0386);
  }
}


html {
  background-color: var(--royale-paper);
}

body {
  margin: 0;
  /* The positioning box for the page's motif below. */
  position: relative;
}

/* --- the page's pattern ------------------------------------------------ */
/* media/motif.png in the same half-drop lattice the gate's doors and card
   carry (see --motif-tile), now on the page itself, so the cream between the
   sections is the same paper the invitation opens on.

   Drawn as a stencil in the page's own ink rather than as the tile's own
   artwork: the tile is a pale peach, which is what shows on the doors' maroon
   and vanishes on cream - the same reason the gate's card masks it (see
   .royale-card__motif).

   Absolute on the body rather than fixed, so it travels with the page as it
   scrolls, and behind everything: the sections that bring their own ground -
   the hero's video, the gallery's wallpaper, the ceremonies' maroon, the
   closing picture - paint over it, so it shows exactly where the cream
   does. */
body::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  background-color: var(--royale-ink);
  -webkit-mask: url("media/motif_tile.svg") repeat;
  mask: url("media/motif_tile.svg") repeat;
  -webkit-mask-size: var(--motif-tile);
  mask-size: var(--motif-tile);
  /* Centred on the page, a column down its middle, as on the doors. */
  -webkit-mask-position: calc(50% + 31.85 * var(--u)) 0;
  mask-position: calc(50% + 31.85 * var(--u)) 0;
  pointer-events: none;
}

/* --- hero ------------------------------------------------------------- */
/* Two scenes of the palace garden at dusk, one per kind of screen:
   - below 700px, media/garden_palace.mp4, moving (720x1280, near the phone's
     own shape), covered and centred, which keeps the pavilion and the water
     channel and trims only the outer trees. At full height its sky is the
     same share of the screen at every phone width, so the names can be
     placed against it with container units instead of per-screen offsets;
   - from 700px up, media/mandap_dusk.webp, a still (2322x1224), shown whole
     at full width - the hero takes its height rather than the screen's. */
.royale-hero {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  background-color: var(--royale-maroon-deep);
}

/* Below 700px, one full screen of the video, edge to edge - it covers the
   frame, cropping at the sides. From 700px up the frame is the still's own
   height instead (see "Only one of the two" below). */
.royale-hero__frame {
  position: relative;
  z-index: 1;
  width: 100%;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  container-type: inline-size;
  background-color: var(--royale-paper);
}

/* The last stretch of the picture blends into the page's cream, so there is
   no hard line where the family scrolls begin. */
.royale-hero__frame::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 6%;
  background: linear-gradient(to bottom, rgb(253 229 201 / 0), var(--royale-paper));
  pointer-events: none;
}

/* The video or the picture. pointer-events off so a long-press or
   right-click lands on the page, not on video controls or a "save" menu.

   Never transformed or animated: it used to settle back from a 1.07 zoom as
   the gate opened, and a playing video scaled by a few hundredths over
   seconds shimmers - its fine detail (the lamps, the railings, the water)
   is resampled at a slightly different size every frame, and on Windows
   Chrome hands a video to the display as an overlay whose edges snap to
   whole pixels, so the slow tail of the zoom stepped a pixel at a time. The
   footage has its own slow push-in, which is movement enough. */
.royale-hero__image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* Only one of the two is on screen: the video below 700px, the still from
   there up. The still's <picture> is otherwise invisible to layout, so its
   <img> sizes against the frame just as the video does.

   The still is never cropped to the screen: it is shown whole, full width
   at its own 2322:1224, and the hero is as tall as it comes to - running on
   below the fold on a wide screen, and stopping short of the screen's foot
   on a squarer one. */
.royale-hero__still {
  display: none;
}

@media (min-width: 700px) {
  .royale-hero {
    min-height: 0;
  }

  .royale-hero__frame {
    height: auto;
  }

  .royale-hero__video {
    display: none;
  }

  .royale-hero__still {
    display: contents;
  }

  .royale-hero__still .royale-hero__image {
    height: auto;
  }
}

/* Centred in the sky above the mandap. On a phone the video (720x1280) is
   always cropped at the sides only - a phone is narrower than its 9:16 - so
   it is the frame's full height and anything in it sits at a fixed share of
   that height. The mandap's finials stand at 29% of it (370 of 1280 on the
   first frame, and still there as the footage pushes in), so the sky's
   middle is 14.5%. translateY(-50%) centres the three lines on that point. */
.royale-couple {
  position: absolute;
  top: 14.5%;
  transform: translateY(-50%);
  left: 0;
  right: 0;
  margin: 0;
  padding-inline: 6cqw;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  font-weight: normal;
  color: var(--royale-maroon);
  text-shadow:
    0 0 1cqw rgb(253 242 225 / 0.6),
    0 0 4cqw rgb(253 242 225 / 0.5);
}

.royale-couple:focus {
  outline: none;
}

.royale-couple__name,
.royale-couple__amp {
  display: block;
  opacity: 0;
}

.royale-couple__name {
  font-family: var(--font-names);
  font-size: min(13cqw, 64px);
  font-style: italic;
  font-weight: 600;
  line-height: 1;
}

.royale-couple__amp {
  margin-block: -0.5cqw;
  font-family: var(--font-names);
  font-size: min(8.5cqw, 42px);
  font-style: italic;
  font-weight: 500;
  line-height: 1;
  color: var(--royale-crimson);
}

/* From 700px up the mandap still takes over from the video, and the names -
   sized against the frame, which is now the whole screen - take figures for
   a landscape one. */
@media (min-width: 700px) {
  .royale-couple {
    top: 6%;
    transform: none;
  }

  .royale-couple__name {
    font-size: clamp(56px, 6cqw, 96px);
  }

  .royale-couple__amp {
    font-size: clamp(34px, 3.4cqw, 54px);
  }
}

/* A landscape screen is wide and short: the names read better on one line,
   with the ampersand between them, centred in the sky.

   The sky over the names runs from the top of mandap_dusk.webp down to the
   mandap's finials at 578 of its 1224px - 47.2% of the picture's height. The
   picture is shown whole and the frame is exactly its height (see "Only one
   of the two" above), so the sky's middle is 23.6% of the frame at every
   width. translateY(-50%) centres the line on that point. */
@media (orientation: landscape) {
  .royale-couple {
    flex-direction: row;
    align-items: baseline;
    justify-content: center;
    gap: clamp(10px, 2cqw, 28px);
    top: 23.6%;
    transform: translateY(-50%);
  }

  .royale-couple__amp {
    margin-block: 0;
  }
}

/* --- scroll cards ----------------------------------------------------- */
/* The groom's and the bride's families, and at the foot of the page the
   compliments and the note, each written on a maroon scroll held between two
   rolls. A .royale-scrolls section holds a column of them. Size and spacing
   are papercut's family cards, number for number (papercut/styles.css:
   .invite, .card, .invite__inner, .card__name/__block/__rel/__elder/__amp and
   their 900px/560px steps), so the templates share one rhythm - papercut's
   compliment and note cards reuse that same card too, with their own type
   below. The face and colouring are Royale's own: Fraunces set upright in
   gold on the maroon sheet. The type is set a sixth smaller than papercut's
   and all at 500, Fraunces's lightest here - it is only loaded from 500 up
   (see _platform/vendor/fonts), and at papercut's sizes and 600 it read
   heavy on the sheet.

   --scroll-w is the card width (papercut's 50vw / 80vw) and --roll-h the
   height roll-top1.svg comes to at that width (329x31), which the unroll
   keyframes below need as a length. */
/* No background of its own: the page's cream and its motif (see body::before)
   show through, as they do between every section that is just paper. */
.royale-scrolls {
  --scroll-w: 50vw;
  --roll-h: calc(var(--scroll-w) * 31 / 329);
  --unroll-dur: 1400ms;
  --unroll-delay: 150ms;
  /* The border down each sheet's long edges: 2.6% of the card, kept between
     12px, where the star still reads, and 20px. */
  --scroll-border-w: clamp(12px, var(--scroll-w) * 0.026, 20px);

  display: grid;
  place-items: center;
  justify-content: center;
  /* papercut's gap and section padding, plus room for the rolls, which
     overhang each sheet by half their height above and below. */
  gap: calc(clamp(48px, 8vw, 88px) + var(--roll-h));
  padding: calc(clamp(48px, 9vw, 120px) + var(--roll-h) / 2) 24px;
}

@media (max-width: 1024px) {
  .royale-scrolls {
    --scroll-w: 80vw;
  }
}

@media (max-width: 500px) {
  .royale-scrolls {
    padding-block: calc(64px + var(--roll-h) / 2);
  }
}

/* The shadow the scroll casts on the paper behind it: a close, darker one
   where it nearly touches, and a wide soft one lifting it off the page. A
   drop-shadow rather than a box-shadow, so it follows the rolls and knobs
   and the sheet's current length as it unrolls. Sized in --roll-h so it
   scales with the scroll. */
.royale-scroll {
  position: relative;
  width: var(--scroll-w);
  max-width: 100%;
  filter:
    drop-shadow(0 calc(var(--roll-h) * 0.15) calc(var(--roll-h) * 0.2) rgb(62 8 35 / 0.35))
    drop-shadow(0 calc(var(--roll-h) * 0.6) calc(var(--roll-h) * 0.9) rgb(62 8 35 / 0.3));
}

/* The sheet - papercut's .invite__inner box. Shaded dark under both rolls,
   darkened along its two long edges, a faint vertical grain, and a glow in
   the middle where the writing sits.

   Papercut's 18% padding is kept top and bottom but not at the sides: there
   it clears the thick decorative frame round its card, which a scroll does
   not have, and Fraunces sets far wider than papercut's Cormorant at the
   same sizes - at 18% nearly every name broke onto two lines. */
/* The bloom behind the sheet, centred on the card both ways. While the
   scroll is still closed the sheet is clipped to the strip between its two
   rolls, so this is what stands in the space below them; the sheet then
   unrolls over it and covers it. */
.royale-scroll__bloom {
  position: absolute;
  z-index: 0;
  top: 50%;
  left: 50%;
  height: 62%;
  width: auto;
  transform: translate(-50%, -50%);
  pointer-events: none;
  user-select: none;
}

/* The window the sheet is seen through - exactly the sheet's size, and
   clipping anything outside itself, so the unroll below can reveal the sheet
   by moving it. z-index (and the positioning it needs) so the sheet paints
   over the bloom: without it the bloom, being positioned, would sit on top
   of the sheet's background and its writing. */
.royale-scroll__window {
  position: relative;
  z-index: 1;
  overflow: hidden;
}

/* Positioned for its ::after border. */
.royale-scroll__sheet {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  width: 100%;
  min-height: min(470px, 92vw);
  padding: 18% 8%;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 1.4em;
  text-align: center;
  font-family: var(--font-display);
  color: var(--royale-gold);
  background:
    linear-gradient(to bottom, rgb(20 0 8 / 0.5), transparent calc(var(--roll-h) * 1.3)),
    linear-gradient(to top, rgb(20 0 8 / 0.5), transparent calc(var(--roll-h) * 1.3)),
    repeating-linear-gradient(90deg, rgb(0 0 0 / 0.04) 0 1px, transparent 1px 4px),
    radial-gradient(ellipse 70% 60% at 50% 50%, #6c0634, #56032a 55%, var(--royale-door));
  box-shadow:
    inset 12px 0 18px -12px rgb(20 0 8 / 0.7),
    inset -12px 0 18px -12px rgb(20 0 8 / 0.7);
}

/* The border (see --border) down both long edges of the sheet, repeated
   from its top. On a layer of its own rather than in the sheet's
   background, so the darkened edges above do not dull it - the tile is
   clear between its two rules and its star, so the shading still shows
   through there. Inside the sheet, so it is clipped with it as the scroll
   unrolls, and it runs under the rolls at either end. */
.royale-scroll__sheet::after {
  --tile: var(--scroll-border-w) calc(var(--scroll-border-w) * 18.0385 / 20.0386);

  content: "";
  position: absolute;
  inset: 0;
  background:
    var(--border) left top / var(--tile) repeat-y,
    var(--border) right top / var(--tile) repeat-y;
  pointer-events: none;
}

.royale-scroll__name {
  margin: 0 0 0.3em;
  font-size: 40px;
  font-weight: 500;
  line-height: 1.1;
}

.royale-scroll__block {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25em;
  margin: 0;
}

.royale-scroll__rel {
  font-size: 20px;
  font-weight: 500;
  opacity: 0.85;
}

.royale-scroll__elder {
  font-size: 24px;
  font-weight: 500;
  line-height: 1.2;
}

.royale-scroll__amp {
  font-size: 20px;
  font-weight: 500;
}

/* The relation lines and ampersands step down with the rest, so a line like
   "Son of" never stands larger than the names it introduces. */
@media (max-width: 900px) {
  .royale-scroll__name {
    font-size: 30px;
  }

  .royale-scroll__elder {
    font-size: 20px;
  }

  .royale-scroll__rel,
  .royale-scroll__amp {
    font-size: 18px;
  }
}

@media (max-width: 560px) {
  .royale-scroll__name {
    font-size: 26px;
  }

  .royale-scroll__elder {
    font-size: 18px;
  }

  .royale-scroll__rel,
  .royale-scroll__amp {
    font-size: 16px;
  }
}

/* The compliments and note cards - papercut's .compliment__lines /
   .compliment__line / .note__text. Their headings take .royale-scroll__name's
   ramp above, as papercut's take .card__name's. */
.royale-scroll__lines {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1em;
}

.royale-scroll__line {
  margin: 0;
  font-size: 22px;
  font-weight: 500;
  line-height: 1.4;
}

/* Prose, so lighter than its heading. */
.royale-scroll__note {
  margin: 0;
  white-space: pre-line;
  font-size: 22px;
  font-weight: 500;
  line-height: 1.7;
}

/* Papercut steps the compliment lines at 700px, not 560: a 26px line like
   "Shri Suresh Sharma & Family (Chacha-Chachi)" would wrap three times on a
   phone. The note steps with them here, though papercut's stays at 26px -
   Fraunces sets far larger than papercut's Cormorant, and at 26px the note
   towered over everything else on a phone scroll. */
@media (max-width: 700px) {

  .royale-scroll__line,
  .royale-scroll__note {
    font-size: 17px;
  }
}

/* 55% of the sheet's width, as in the design (65% of the 84% the side
   padding leaves). Set further off the writing than the sheet's own 1.4em
   gap, so it reads as the close of the card rather than one more line. */
.royale-scroll__divider {
  display: block;
  width: 65%;
  height: auto;
  margin-top: 1.2em;
}

/* The rolls: roll-top1.svg across the sheet's full width, centred on its
   top or bottom edge, with a knob capping each end outside it. The knobs
   share the roll's scale (30 of its 329 -> 9.12% of the width) and overlap
   the roll's flared ends by 15% of their own width (1.37%). */
.royale-scroll__rod {
  position: absolute;
  z-index: 1;
  left: 0;
  right: 0;
  transform: translateY(-50%);
}

.royale-scroll__rod--top {
  top: 0;
}

.royale-scroll__rod--bottom {
  top: 100%;
}

.royale-scroll__roll {
  display: block;
  width: 100%;
  height: auto;
}

.royale-scroll__knob {
  position: absolute;
  top: 50%;
  width: 9.12%;
  height: auto;
  transform: translateY(-50%);
}

.royale-scroll__knob--left {
  right: calc(100% - 1.37%);
}

.royale-scroll__knob--right {
  left: calc(100% - 1.37%);
}

.royale-scroll__roll,
.royale-scroll__knob {
  user-select: none;
  -webkit-user-drag: none;
}

/* The bottom roll hangs off a box the size of the sheet, so sliding that
   box by its own height walks the roll up and down the sheet, in step with
   the sheet's clip - both animations share one set of keyframe stops. */
.royale-scroll__roller {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}

/* Closed: the bottom roll sits directly under the top one, and only the
   strip of sheet the two rolls cover shows. Only once app.js arms the
   scroll - so with no script, or with reduced motion, the cards are simply
   open.

   The sheet is revealed by transforms alone, never a clip-path, so that the
   whole unroll runs on the compositor. It used to be a clip-path on the
   sheet while the roll below moved by transform - and the two ran on
   different threads: the compositor moves a transform by itself, but a
   clip-path is repainted by the main thread every frame, and on a phone,
   with Lenis and GSAP also on that thread, the clip fell behind and the card
   arrived after the roll that was supposed to be carrying it.

   So the window (overflow: hidden) is slid exactly as the roller is - the
   same keyframes - and the sheet inside it is slid by the opposite amount.
   The two cancel: the sheet and its writing stand still while the window's
   bottom edge, and with it the edge of what shows, travels down with the
   roll. The window's top edge travels too, but only ever by the overshoots,
   at most 0.35 of a roll, which the top roll (half a roll deep over the
   sheet's top edge) covers. */
.royale-scroll.is-armed .royale-scroll__window,
.royale-scroll.is-armed .royale-scroll__roller {
  transform: translateY(calc(-100% + var(--roll-h)));
}

.royale-scroll.is-armed .royale-scroll__sheet {
  transform: translateY(calc(100% - var(--roll-h)));
}

/* No `contain: paint` on the card to bound its repaints, tempting as it is:
   both rods hang half outside it (translateY(-50%) at top 0 and top 100%) and
   every knob sits almost wholly outside its rod, so containing the card would
   clip the rolls off the scroll entirely. */

/* Unrolling, when app.js sees the card come into view: the bottom roll
   drops, speeding up, and slams to a stop 35% of a roll past its mark
   (never more than half a roll, or the sheet's own edge would show above
   it), then jerks back and forth in shrinking bounces. The overshoots are
   in roll heights so the jerk is the same size on every screen. The window
   runs the roller's own keyframes and the sheet their mirror image, with
   the same timing functions, so all three start on the same frame and stay
   locked together. */
.royale-scroll.is-unrolled .royale-scroll__window,
.royale-scroll.is-unrolled .royale-scroll__roller {
  animation: ry-unroll-roller var(--unroll-dur) var(--unroll-delay) both;
}

.royale-scroll.is-unrolled .royale-scroll__sheet {
  animation: ry-unroll-sheet var(--unroll-dur) var(--unroll-delay) both;
}

@keyframes ry-unroll-roller {
  0% {
    transform: translateY(calc(-100% + var(--roll-h)));
    animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5);
  }

  60% {
    transform: translateY(calc(var(--roll-h) * 0.35));
    animation-timing-function: cubic-bezier(0.25, 0.6, 0.45, 1);
  }

  72% {
    transform: translateY(calc(var(--roll-h) * -0.18));
    animation-timing-function: ease-in-out;
  }

  83% {
    transform: translateY(calc(var(--roll-h) * 0.09));
    animation-timing-function: ease-in-out;
  }

  92% {
    transform: translateY(calc(var(--roll-h) * -0.035));
    animation-timing-function: ease-in-out;
  }

  100% {
    transform: translateY(0);
  }
}

/* ry-unroll-roller, negated. */
@keyframes ry-unroll-sheet {
  0% {
    transform: translateY(calc(100% - var(--roll-h)));
    animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5);
  }

  60% {
    transform: translateY(calc(var(--roll-h) * -0.35));
    animation-timing-function: cubic-bezier(0.25, 0.6, 0.45, 1);
  }

  72% {
    transform: translateY(calc(var(--roll-h) * 0.18));
    animation-timing-function: ease-in-out;
  }

  83% {
    transform: translateY(calc(var(--roll-h) * -0.09));
    animation-timing-function: ease-in-out;
  }

  92% {
    transform: translateY(calc(var(--roll-h) * 0.035));
    animation-timing-function: ease-in-out;
  }

  100% {
    transform: translateY(0);
  }
}

/* The jolt travels up the sheet: the top roll is tugged down as the bottom
   one snaps, and the bottom roll swings a little on the rebound. Both stay
   inside what their roll covers, so no sheet edge shows. */
.royale-scroll.is-unrolled .royale-scroll__rod--top {
  animation: ry-unroll-tug var(--unroll-dur) var(--unroll-delay) both;
}

.royale-scroll.is-unrolled .royale-scroll__rod--bottom {
  animation: ry-unroll-swing var(--unroll-dur) var(--unroll-delay) both;
}

@keyframes ry-unroll-tug {

  0%,
  57% {
    transform: translateY(-50%);
  }

  63% {
    transform: translateY(calc(-50% + var(--roll-h) * 0.16));
  }

  75% {
    transform: translateY(calc(-50% - var(--roll-h) * 0.05));
  }

  86%,
  100% {
    transform: translateY(-50%);
  }
}

@keyframes ry-unroll-swing {

  0%,
  58% {
    transform: translateY(-50%) rotate(0deg);
  }

  64% {
    transform: translateY(-50%) rotate(-0.8deg);
  }

  74% {
    transform: translateY(-50%) rotate(0.55deg);
  }

  84% {
    transform: translateY(-50%) rotate(-0.25deg);
  }

  100% {
    transform: translateY(-50%) rotate(0deg);
  }
}

/* --- date reveal ------------------------------------------------------ */
/* A scene laid out on a stage with the design's own 1077x671 proportions:
   the lotus crown on top, three plaques (media/date_window.svg) in a row,
   an elephant at each lower corner, and the button between the elephants
   under the plaques. Everything inside is placed in cqw - 1cqw is 1% of the
   stage's width, so 10.77 design px - which keeps the scene in proportion at
   any width. Type carries px floors so it stays readable when small.

   The whole scene stays on screen at every width, with the same 24px margin
   either side as the family scrolls above it. */
/* A band across the top and the foot of the section, as in the design: the
   gate's border element (media/border_element.svg) turned a quarter turn -
   its two rules now running across the page with its stars between them -
   repeated edge to edge. media/border_element_h.svg is that turned copy; the
   #svgView crops it to the drawn part of its box, or the rules would break
   with a hairline gap every repeat. The band's thickness sets the repeat:
   the tile keeps its own proportions, so the stars space themselves. */
.royale-date {
  --date-band: clamp(22px, 2.8vw, 34px);
  /* How far the band's shadow reaches onto the cream outside the section. */
  --date-shade: clamp(28px, 6vw, 56px);
  --date-border:
    url("media/border_element_h.svg#svgView(viewBox(0,0,18.0385,20.0386);preserveAspectRatio(none))");

  position: relative;
  padding:
    calc(clamp(24px, 5vw, 72px) + var(--date-band)) 24px
    calc(clamp(48px, 9vw, 120px) + var(--date-band));
}

/* Each band is drawn on a strip that reaches PAST the section, and carries
   two layers: the border itself, held against the section's own edge, and a
   soft shadow falling away from it onto the cream outside - the band reads
   as a raised strip, as in the design. Building the shadow into the strip
   rather than as a box-shadow keeps it out of the section: a box-shadow
   blurs inward as well, and the design has none inside. */
.royale-date::before,
.royale-date::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: calc(var(--date-band) + var(--date-shade));
  pointer-events: none;
}

.royale-date::before {
  top: calc(-1 * var(--date-shade));
  background:
    var(--date-border) 0 100% / auto var(--date-band) repeat-x,
    linear-gradient(to bottom, rgb(74 2 30 / 0), rgb(74 2 30 / 0.14)) 0 0 / 100% var(--date-shade) no-repeat;
}

.royale-date::after {
  bottom: calc(-1 * var(--date-shade));
  background:
    var(--date-border) 0 0 / auto var(--date-band) repeat-x,
    linear-gradient(to top, rgb(74 2 30 / 0), rgb(74 2 30 / 0.14)) 0 100% / 100% var(--date-shade) no-repeat;
}

.royale-date__stage {
  position: relative;
  width: min(100%, 1080px);
  aspect-ratio: 1077 / 671;
  margin-inline: auto;
  container-type: inline-size;
}

.royale-date__crown,
.royale-date__elephant {
  position: absolute;
  display: block;
  height: auto;
  user-select: none;
  -webkit-user-drag: none;
  pointer-events: none;
}

/* Centred by its own offset - (100 - 43.6) / 2 - rather than by a translate:
   scroll.js drifts the crown, and GSAP folds an element's existing translate
   into the transform it writes, in pixels, which stop matching the crown's
   width as soon as the screen is resized. */
.royale-date__crown {
  left: 28.2cqw;
  top: 2.3cqw;
  width: 43.6cqw;
}

/* The elephants' boxes include the PNG's transparent margin, so the image is
   wider (28.1cqw) than the elephant in the design (22.3cqw) and starts at the
   stage's edge. In front of the plaques, which their canopies overlap. */
.royale-date__elephant {
  z-index: 2;
  top: 33.8cqw;
  width: 28.1cqw;
}

.royale-date__elephant--left {
  left: 0;
}

.royale-date__elephant--right {
  right: 0;
  transform: scaleX(-1);
}

/* The row: three 21.3cqw plaques 2.7cqw apart, centred (15.3cqw either
   side). --cell-h is one line of the rolling strip, exactly the window's
   height - date-reveal.js steps the strip by it, so the two cannot drift. */
.royale-date__plaques {
  --cell-h: 9.14cqw;

  position: absolute;
  z-index: 1;
  left: 15.3cqw;
  right: 15.3cqw;
  top: 31.4cqw;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2.7cqw;
}

/* The plaque artwork is 84x60: the maroon window runs y 7.5-59.5 with its
   flat inside about x 11-73, y 15-52, and the cream tab runs x 16.5-67.5,
   y 0-15. The caption and the window below are placed on those. */
.royale-date__plaque {
  --cell-fs: 5.8cqw;

  position: relative;
  aspect-ratio: 84 / 60;
  background: url("media/date_window.svg") center / 100% 100% no-repeat;
  filter: drop-shadow(0 0.5cqw 1cqw rgb(74 2 30 / 0.22));
}

.royale-date__plaque[data-dial="month"],
.royale-date__plaque[data-dial="year"] {
  --cell-fs: 4.8cqw;
}

.royale-date__cap {
  position: absolute;
  left: 19.6%;
  right: 19.6%;
  top: 0;
  height: 25%;
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-size: clamp(7px, 1.35cqw, 15px);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--royale-door);
}

.royale-date__window {
  position: absolute;
  left: 14%;
  right: 14%;
  top: 26%;
  height: var(--cell-h);
  overflow: hidden;
}

.royale-date__strip {
  will-change: transform;
}

.royale-date__cell {
  display: grid;
  place-items: center;
  height: var(--cell-h);
  font-family: var(--font-display);
  font-size: var(--cell-fs);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.02em;
  color: var(--royale-gold);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The hop the plaques give when the gold goes up (date-reveal.js fires it
   just behind the burst, once all three dials have landed). */
.royale-date__plaque.is-set {
  animation: ry-date-knock 420ms ease-out 1;
}

@keyframes ry-date-knock {
  0% {
    transform: translateY(0);
  }

  38% {
    transform: translateY(-3px);
  }

  100% {
    transform: translateY(0);
  }
}

/* Holds the button and the line that replaces it, centred between the
   elephants just under the plaques. */
.royale-date__action {
  position: absolute;
  z-index: 3;
  left: 50%;
  top: 50cqw;
  translate: -50% 0;
  display: grid;
  place-items: center;
}

/* The same plaque as the cover's tap cue (.ry-plaque), so the template's
   two buttons match. Sized in em of its own type: 11em holds "Reveal the
   Date" inside the plaque's straight middle, and the padding grows the
   plaque - keeping its shape - if a host writes a longer label. The floors
   are what a phone gets, and still clear both elephants on a 360px screen,
   where the gap between them is ~170px. The shadow is a drop-shadow so it
   follows the scallops. */
.royale-date__btn {
  min-width: 11em;
  margin: 0;
  padding: 0 2.4em;
  border: 0;
  background: none;
  font-size: clamp(13px, 2.1cqw, 24px);
  filter: drop-shadow(0 0.8cqw 1cqw rgb(74 2 30 / 0.3));
  cursor: pointer;
  transition: transform 140ms ease;
}

@keyframes ry-cue-bob {

  0%,
  100% {
    transform: translateY(0);
  }

  50% {
    transform: translateY(-6px);
  }
}

/* animation:none so the press shows at all - a running animation outranks
   a plain transform. */
.royale-date__btn.is-pressed {
  animation: none;
  transform: translateY(3px);
}

.royale-date__btn:focus-visible {
  outline: 2px solid var(--royale-crimson);
  outline-offset: 4px;
}

.royale-date__btn[disabled] {
  cursor: default;
}

/* Spent the moment it is pressed; visibility rather than display so the
   line that replaces it takes the same spot without a jump. The delay lets
   the press itself be seen first. */
.is-revealing .royale-date__btn,
.is-revealed .royale-date__btn {
  opacity: 0;
  visibility: hidden;
  transition: opacity 380ms ease 170ms, visibility 0s linear 550ms;
}

.royale-date__after {
  position: absolute;
  margin: 0;
  white-space: nowrap;
  font-family: var(--font-names);
  font-size: clamp(22px, 3.4cqw, 40px);
  font-style: italic;
  font-weight: 600;
  line-height: 1;
  color: var(--royale-crimson);
  animation: ry-appear 620ms ease 200ms both;
}

/* The gold thrown up as the date lands (date-reveal.js builds the flakes and
   writes each one's arc into its own properties). Above the elephants and
   the button; clipped to the stage, so what falls past it simply goes. */
.royale-date__confetti {
  position: absolute;
  z-index: 4;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

/* One bead is three nested layers, and that is the whole trick: the drift,
   the arc and the tumble each get their own element, so each can keep its
   own timing instead of three motions sharing one transform and cornering
   wherever their keyframes happen to meet. */
.ryd-flake {
  position: absolute;
  width: var(--w);
  height: var(--w);
  margin-left: calc(var(--w) / -2);
  margin-top: calc(var(--w) / -2);
  /* Sideways: all of the throw at the start, slowing as the air takes it. */
  animation: ry-flake-drift var(--dur) cubic-bezier(0.1, 0.62, 0.3, 1) var(--delay) both;
}

.ryd-flake__arc,
.ryd-flake__bead {
  display: block;
  width: 100%;
  height: 100%;
}

/* Up, over and down. The rise ends at a standstill and the fall begins at
   one, so the top of the arc is a turn rather than a corner. */
.ryd-flake__arc {
  animation: ry-flake-arc var(--dur) linear var(--delay) both;
}

.ryd-flake__bead {
  animation: ry-flake-bead var(--dur) linear var(--delay) both;
}

@keyframes ry-flake-drift {
  from {
    transform: translateX(0);
  }

  to {
    transform: translateX(var(--dx));
  }
}

@keyframes ry-flake-arc {
  0% {
    transform: translateY(0);
    animation-timing-function: cubic-bezier(0.22, 0.58, 0.38, 1);
  }

  42% {
    transform: translateY(var(--rise));
    animation-timing-function: cubic-bezier(0.55, 0, 0.85, 0.72);
  }

  100% {
    transform: translateY(var(--fall));
  }
}

/* An even tumble - the turn is the same fraction of --rot as the time
   elapsed at every stop, so the bead never speeds up or catches. It turns in
   the plane rather than flipping, which keeps a bead a bead: tipped over in
   3D these would pass edge-on and flicker away to nothing mid-flight. */
@keyframes ry-flake-bead {
  0% {
    transform: rotate(0deg) scale(0.6);
    opacity: 0;
  }

  14% {
    transform: rotate(calc(var(--rot) * 0.14)) scale(1);
    opacity: 1;
  }

  72% {
    transform: rotate(calc(var(--rot) * 0.72)) scale(1);
    opacity: 1;
  }

  100% {
    transform: rotate(var(--rot)) scale(0.92);
    opacity: 0;
  }
}

.royale-date__said {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Reduced motion: nothing rolls, knocks or bobs; the three values fade in
   in the same order (date-reveal.js takes this path and skips the roll). */
.royale-date__plaque.is-fading .royale-date__window {
  opacity: 0;
}

.royale-date__plaque.is-fading.is-shown .royale-date__window {
  opacity: 1;
  transition: opacity 420ms ease;
}

/* Phones: the elephants stand at the screen's own edges rather than inside
   the stage, big enough that their outer ends pass behind it, and level with
   the foot of the plaque row - so the scene reads as it does on a desktop,
   the row between the two of them, instead of the row above them.

   The row gives up the width for it: 24.9cqw a plaque against the 30.33 it
   took when the elephants stood below, with every size tied to it (the
   window's height, both type sizes, the caption) coming down by the same
   0.82. The stage is then shorter than it was (66 of 100 against 80.5),
   since nothing stands under the row any more but the button, which sits
   between the elephants' trunks - some 62cqw of clear paper.

   The offsets are measured from the screen, not the stage: the section holds
   the stage 24px in from each edge, so -24px puts a box's edge exactly on
   the screen's, and the rest is how far the elephant reaches past it (its
   own picture starts a tenth of the box in). The section clips what goes
   past, so nothing can drag a scrollbar across the page.

   date-reveal.js reads the elephants' places when it throws the confetti, so
   the gold now comes up at the edges with them. */
@media (max-width: 699px) {
  .royale-date {
    overflow-x: clip;
  }

  .royale-date__stage {
    aspect-ratio: 100 / 66;
  }

  .royale-date__plaques {
    --cell-h: 10.7cqw;

    left: 10.5cqw;
    right: 10.5cqw;
    gap: 2.2cqw;
  }

  .royale-date__plaque {
    --cell-fs: 6.8cqw;
  }

  .royale-date__plaque[data-dial="month"],
  .royale-date__plaque[data-dial="year"] {
    --cell-fs: 5.6cqw;
  }

  .royale-date__cap {
    font-size: clamp(8px, 1.6cqw, 15px);
  }

  /* Beside the row and a little below it, as on a desktop - their canopies
     overlapping the outer plaques, which they stand in front of. */
  .royale-date__elephant {
    top: 34.8cqw;
    width: 32.7cqw;
  }

  .royale-date__elephant--left {
    left: calc(-24px - 5.5cqw);
  }

  .royale-date__elephant--right {
    right: calc(-24px - 5.5cqw);
  }

  /* Between the two of them, just under the plaques. */
  .royale-date__action {
    top: 52cqw;
  }
}

@media (prefers-reduced-motion: reduce) {
  .royale-date__btn {
    animation: none;
  }

  .royale-date__plaque.is-set {
    animation: none;
  }

  .royale-date__after {
    animation-duration: 1ms;
    animation-delay: 0s;
  }
}

/* --- RSVP ------------------------------------------------------------- */
/* The shared platform RSVP mounts into #balcony-rsvp and brings its own card,
   but nothing that holds it in from the page's edges or off the closing
   picture above it - there is no max-width, inline padding or block margin
   anywhere in _platform/rsvp.css. This gives it the same breathing room the
   rest of the page has, at both ends: it is the last thing on the page, so
   it clears the picture above and does not sit on the page's own edge. */
#balcony-rsvp {
  /* Padding, not margin, at the foot: the page's last block margin collapses
     out through the body and leaves the card sitting on the page's edge. */
  padding: clamp(48px, 8vw, 110px) 24px;
}

/* _platform/rsvp.css sizes the card as width: min(94vw, 560px) - a
   VIEWPORT width, which takes no notice of the padding above. On a phone that
   is a card wider than its padded box: over-constrained, so its auto margins
   have nothing to split, and the card runs off the right edge and drags the
   page's width with it. Pointing the width at the CONTAINER instead lets the
   padding do its job; the platform's own 560px cap is kept. */
@media (max-width: 1024px) {
  #balcony-rsvp .brsvp {
    width: min(560px, 100%);
  }
}

/* --- pavilion -------------------------------------------------------- */
/* media/pavilion.webp - the marble pavilion in its garden - between the date
   and the gallery, on the page's cream. The picture fades out to nothing all
   round, glow and all, so it needs no frame. Its foot keeps clear of the
   gallery's lotus divider, which reaches up out of the gallery across this
   block's bottom edge: 69% of its height, the image being 128/807 of the
   screen's width tall - 10.95vw - plus a little room. */
/* The picture starts at this block's own top edge (its empty sky is trimmed
   off by the negative margin below), which put it right up against the date
   section's border band. This is the air between the two - more than the
   band's shadow reaches, so the picture starts on clean cream. */
.royale-pavilion {
  padding:
    clamp(44px, 8vw, 96px) 24px
    max(clamp(24px, 4vw, 48px), 10.95vw + 16px);
}

/* The picture's top 117 of its 1374px (10.22% of its width) is empty above
   the finial, and its last 53 below the floor's fade; the negative margins
   take that back out - all of the top, so the date section's own bottom
   padding is the gap above the pavilion, and the bottom down to the last
   21px of glow (32px, 2.79%). */
.royale-pavilion__image {
  --w: min(95vw, 1080px);

  display: block;
  width: var(--w);
  height: auto;
  /* Centred by hand rather than with auto: at 95vw on a phone the picture
     is wider than the space inside the block's 24px padding, and auto would
     then pin it to the left and push it off the right edge of the screen.
     This splits the overhang evenly across the padding instead.

     The block margin takes back the picture's own empty sky, so the garden
     itself starts at this block's top edge: the file is 1500 x 927 and its
     painting runs from y 7.2% of that, which is 4.47% of the WIDTH - the
     unit a vertical margin in percent would not give us here, hence the
     calc. Its foot is flush with the file's own bottom edge, so there is
     nothing to take back there. Re-measure if the picture changes. */
  margin: calc(var(--w) * -0.0447) calc((100% - var(--w)) / 2) 0;
  user-select: none;
  -webkit-user-drag: none;
}

/* --- gallery ---------------------------------------------------------- */
/* rose-invitation's hanging gallery (its "Photo gallery" styles), with the
   polaroids replaced by 3D picture frames. The scroller, track height, rail
   height and position, card widths, caption and story type, hint and phone
   carousel follow rose's figures; what hangs is new, and so is the spacing -
   rose's fixed positions left its cards 30px apart or overlapping, which
   thick frames cannot share. Unlike rose, the frames run clean off both
   sides of the screen, with no fade at the edges. */
/* The wall carries some paper above and below the frames rather than running
   the rail's own box edge to edge: the lotus divider lies across the top
   edge and the ceremonies' maroon starts at the foot, and the frames want
   room off both. */
.royale-gallery {
  --gallery-h: 900px;
  --frame-depth: 14px;

  position: relative;
  padding-block: clamp(36px, 6vw, 88px);
  /* Sideways only: the band on its top edge has to bleed out vertically. */
  overflow-x: clip;
  /* The papered wall the frames hang on, with the page's cream behind it
     until it loads. Covered rather than tiled, so its floral corners stay in
     the section's corners; on a narrow screen that crops the sides and
     leaves mostly the paper itself. */
  background:
    url("media/gallery_bg.webp") center / cover no-repeat,
    var(--royale-paper);
}

/* media/divider3.png laid across the join with the date section above, the
   full width of the screen, where the other sections' joins have a torn
   band. Its gold rule lies on the seam itself - the rule is 69% of the way
   down the image. Above the rule the PNG is solid #fde5c9, the date
   section's own cream, so that half simply carries the section on; below it
   the image is clear, and its swags hang over the wallpaper. (.royale-pavilion,
   above, leaves room over the seam for it.) */
.royale-gallery__divider {
  position: absolute;
  z-index: 2;
  top: 0;
  left: 0;
  width: 100%;
  height: auto;
  transform: translateY(-69%);
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* Landscape: half the screen, centred on the seam. A wide screen would
   otherwise stretch the ornament right across it; at half the width it sits
   as a piece laid over the join. Its own height follows (height: auto), so
   the gold rule stays 69% down the picture and still lands on the seam. */
@media (orientation: landscape) {
  .royale-gallery__divider {
    left: 50%;
    width: 50vw;
    transform: translate(-50%, -69%);
  }
}

/* Drag or swipe to move the rail. */
.royale-gallery__scroll {
  position: relative;
  display: flex;
  align-items: center;
  width: 100%;
  height: var(--gallery-h);
  overflow-x: auto;
  overflow-y: hidden;
  cursor: grab;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}

.royale-gallery__scroll::-webkit-scrollbar {
  display: none;
}

.royale-gallery__scroll.is-dragging {
  cursor: grabbing;
}

/* The frames stand in a row with an even gap between every pair, whatever
   their widths, and the track is as long as the row - rose's 160px margin at
   each end. */
.royale-gallery__track {
  --rail-y: 90px;

  position: relative;
  flex: 0 0 auto;
  display: flex;
  align-items: flex-start;
  gap: 100px;
  width: max-content;
  /* Never narrower than the screen, so the rail (which spans the track)
     runs edge to edge even with one or two frames, or none. */
  min-width: 100%;
  box-sizing: border-box;
  height: 750px;
  padding-inline: 160px;
}

/* A turned maroon rail: a beige highlight along its top, red where the light
   rolls over it, down to deep maroon underneath. */
.royale-gallery__rail {
  position: absolute;
  z-index: 1;
  top: var(--rail-y);
  left: 0;
  width: 100%;
  height: 10px;
  border-radius: 5px;
  background: linear-gradient(to bottom, #f2d9bf 0%, #c0284c 24%, #800033 55%, var(--royale-door) 100%);
  box-shadow: 0 14px 22px rgb(40 0 15 / 0.22);
  pointer-events: none;
}

/* ---- a hanging frame ---- */
/* The unit that swings: ring, cords and frame, pivoting on the rail
   (gallery.js writes its rotation). The perspective is its own rather than
   the track's, so every frame twists about its own centre instead of about
   a vanishing point in the middle of a 4000px track. */
.gframe {
  position: relative;
  z-index: 2;
  flex: none;
  margin-top: var(--rail-y);
  display: flex;
  flex-direction: column;
  align-items: center;
  transform-origin: top center;
  perspective: 1400px;
  perspective-origin: 50% 40%;
  will-change: transform;
}

/* A beige ring threaded on the rail. */
.gframe__hook {
  position: relative;
  z-index: 2;
  box-sizing: border-box;
  width: 16px;
  height: 16px;
  margin-top: -3px;
  border: 3px solid #f2d9bf;
  border-radius: 50%;
  box-shadow:
    0 2px 3px rgb(40 0 15 / 0.4),
    inset 0 1px 2px rgb(40 0 15 / 0.35);
}

/* Two red cords from the ring to screw-eyes behind the frame, 18% in from
   each side - the SVG is 64% of the frame's width, so its corners are those
   two points, and it runs 18px down behind the frame's top edge. Stretched
   to fit, with non-scaling strokes so the cords keep their thickness. A thin
   beige dash over each gives the twist of a silk cord. */
.gframe__cord {
  display: block;
  /* Of the FRAME, not of the unit - on a phone the unit is a full-screen
     slide, and a share of that would hang the cords wide of the frame. */
  width: calc(var(--frame-w) * 0.64);
  height: calc(var(--hang) - 8px);
  margin-top: -5px;
  margin-bottom: -18px;
  overflow: visible;
}

.gframe__cord path {
  fill: none;
  stroke: #9e1b32;
  stroke-width: 3;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

.gframe__cord .gframe__twist {
  stroke: #f2d9bf;
  stroke-width: 1.2;
  stroke-dasharray: 2 4;
}

/* The frame as a real box: the face is its front, and four side walls
   --frame-depth deep fold back from its edges. gallery.js turns the box
   about its vertical axis as it swings, which is what brings the side walls
   into view. Widths are rose's card widths. */
.gframe__body {
  position: relative;
  z-index: 1;
  width: var(--frame-w);
  transform-style: preserve-3d;
}

/* One width per kind of frame, carried on the hanging unit: the frame, its
   cords and a text frame's height are all measured from it. */
.gframe--portrait,
.gframe--story {
  --frame-w: 350px;
}

.gframe--landscape {
  --frame-w: 490px;
}

.gframe__side {
  position: absolute;
}

.gframe__side--left {
  top: 0;
  bottom: 0;
  left: 0;
  width: var(--frame-depth);
  transform-origin: left center;
  transform: rotateY(90deg);
  background: linear-gradient(to right, #5a0a28, #2e0012);
}

.gframe__side--right {
  top: 0;
  bottom: 0;
  right: 0;
  width: var(--frame-depth);
  transform-origin: right center;
  transform: rotateY(-90deg);
  background: linear-gradient(to left, #4a0620, #240010);
}

.gframe__side--top {
  top: 0;
  left: 0;
  right: 0;
  height: var(--frame-depth);
  transform-origin: center top;
  transform: rotateX(-90deg);
  background: linear-gradient(to bottom, #6e1233, #3a0117);
}

.gframe__side--bottom {
  bottom: 0;
  left: 0;
  right: 0;
  height: var(--frame-depth);
  transform-origin: center bottom;
  transform: rotateX(90deg);
  background: #2a0010;
}

/* The moulding, lit from the top left. Borders meet on true mitres at the
   corners, so four border colours give the four faces of a bevelled frame:
   the top faces the light, the left half-faces it, the right turns away and
   the underside is darkest. Casts its shadow down and to the right onto the
   wall. */
.gframe__face {
  position: relative;
  border: 18px solid;
  border-color: #8c1a40 #4f0421 #380116 #6b0e30;
  box-shadow:
    0 0 0 1px #2a0010,
    14px 22px 28px rgb(40 0 15 / 0.3),
    4px 6px 8px rgb(40 0 15 / 0.2);
}

/* The red lip inside the moulding, bevelled the other way - it slopes DOWN
   into the frame, so its top edge is the shaded one and its bottom edge
   catches the light. That reversal is what reads as a step down to the mat.
   Its ::after is a faint sheen of glass across everything inside. */
.gframe__lip {
  position: relative;
  border: 6px solid;
  border-color: #7d0c2a #c8284d #d8395c #8f1233;
}

.gframe__lip::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(115deg, rgb(255 255 255 / 0.16) 0%, rgb(255 255 255 / 0.05) 22%, transparent 40%);
  pointer-events: none;
}

/* The beige mat, sunk under the lip - shadowed along its top and left edges -
   with the caption on its deeper bottom margin, as a gallery mat carries it. */
.gframe__mat {
  margin: 0;
  padding: 20px 20px 0;
  background: linear-gradient(#f5e2cc, #eed3b5);
  box-shadow:
    inset 0 3px 7px rgb(60 10 20 / 0.35),
    inset 3px 0 5px rgb(60 10 20 / 0.2);
}

/* The window cut in the mat: a pale bevelled edge round the photo, which is
   itself sunk a little behind it. */
.gframe__photo {
  display: grid;
  place-items: center;
  /* The picture inside is placed against this corner - see .gframe__img. */
  position: relative;
  /* The zoomed picture is bigger than its frame; the frame is what crops it. */
  overflow: hidden;
  aspect-ratio: 3 / 4;
  background: linear-gradient(135deg, #e6d3bd, #d4bb9c) center / cover no-repeat;
  box-shadow:
    0 0 0 3px #fbf1e3,
    0 0 0 4px #d9bf9f,
    inset 0 2px 8px rgb(40 0 15 / 0.35);
}

.gframe--landscape .gframe__photo {
  aspect-ratio: 3 / 2;
}

/* The picture itself. These are the starting point only: the moment the image
   reports its proportions, frameImage() in render.js replaces the size with
   the picture's own - covering the frame, times the host's zoom - and offsets
   it with a transform. Until then, and if scripting never runs, `cover` gives
   the same centred crop that zoom 1 resolves to anyway.

   max-width/max-height matter: a zoomed picture is deliberately larger than
   its frame, and a stray img { max-width: 100% } would shrink it back. */
.gframe__img {
  /* Pinned to the frame's top-left rather than centred by the grid above,
     because a grid refuses to centre an item bigger than its track - it pins
     it to the start instead, which left the bottom of a zoomed picture
     permanently out of reach. Anchored here, the transform frameImage()
     writes carries the whole offset and every edge is reachable. */
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  object-fit: cover;
  display: block;
  user-select: none;
}

.gframe__placeholder {
  padding: 0 16px;
  font-family: var(--font-names);
  font-size: 21.5px;
  font-style: italic;
  text-align: center;
  color: var(--royale-crimson);
  opacity: 0.6;
}

/* Rose's caption: Cormorant bold italic, 27.5px. */
.gframe__caption {
  margin: 0;
  padding: 12px 8px 14px;
  font-family: var(--font-names);
  font-size: 27.5px;
  font-style: italic;
  font-weight: 600;
  line-height: 1.1;
  text-align: center;
  color: var(--royale-door);
}

/* A text frame, the same height as a portrait photo frame: the photo is the
   width less 88px of moulding, lip and mat (x 4/3 for its height), plus the
   20px above it and the 56px caption below. Rose's story type. */
.gframe__story {
  box-sizing: border-box;
  display: grid;
  place-items: center;
  min-height: calc((var(--frame-w) - 88px) * 4 / 3 + 76px);
  padding: 36px 30px;
  text-align: center;
}

.gframe__story p {
  margin: 0;
  font-family: var(--font-names);
  font-size: 21.5px;
  font-style: italic;
  font-weight: 600;
  line-height: 1.7;
  color: var(--royale-door);
}

/* Phones: one frame at a time, as rose does - a full-width snap carousel. */
@media (max-width: 700px) {
  .royale-gallery {
    --gallery-h: 760px;
  }

  .royale-gallery__scroll {
    scroll-snap-type: x mandatory;
  }

  /* Each slide is a full screen wide, so the frames need no gap of their
     own here - the slide's width is the spacing. */
  .royale-gallery__track {
    --rail-y: 64px;

    gap: 0;
    height: 100%;
    padding-inline: 0;
  }

  .gframe {
    position: static;
    flex: 0 0 100vw;
    height: 100%;
    margin-top: 0;
    padding-top: var(--rail-y);
    scroll-snap-align: center;
    transform-origin: center var(--rail-y);
  }

  .gframe--portrait,
  .gframe--story {
    --frame-w: min(80vw, 340px);
  }

  .gframe--landscape {
    --frame-w: min(90vw, 420px);
  }

  .gframe__story {
    padding: 30px 24px;
  }

  .gframe__caption {
    font-size: 26px;
  }
}

/* The gallery's own placing of the shared swipe hint (see "swipe hint"),
   pinned across the foot of the section. */
.royale-gallery__hint {
  position: absolute;
  z-index: 6;
  left: 50%;
  bottom: 75px;
  transform: translateX(-50%);
}

/* --- ceremonies ------------------------------------------------------- */
/* The six ceremonies on the arched card of media/ceremony_card.svg: one
   under the other on a phone, three across from 900px up. The artwork is
   447x598 with its shadow bleed included - the card itself runs x 39-408,
   y 35-554, and its cream panel x 47-400, y 42.8-546, which is what the
   panel insets below are measured from.

   Every size inside a card is in cqw (1% of that card's own width), taken
   from the design: against its cream panel (78.97cqw of the card's box) the
   title is 9.7cqw, the venue 5cqw and the date 4.6cqw, the time 3cqw, the
   pin 9cqw, and the illustration 74% of the panel wide by 54% of its height. */
.royale-ceremonies {
  /* An ornament in each top corner, the right one the left one mirrored. */
  --corner-w: min(35%, 240px);

  position: relative;
  display: grid;
  /* One full-width column: left to size itself, it would take the width of
     the title instead, and the cards below would follow it in. */
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  /* Enough room above the title for the corners to sit clear of it. */
  padding: calc(var(--corner-w) * 0.948 + clamp(12px, 3vw, 28px)) 24px clamp(48px, 9vw, 120px);
  background-color: #45021c;
}

.royale-ceremonies::before,
.royale-ceremonies::after {
  content: "";
  position: absolute;
  top: 0;
  width: var(--corner-w);
  /* corner.png is 310x294. */
  aspect-ratio: 310 / 294;
  background: url("media/corner.png") center / 100% 100% no-repeat;
  pointer-events: none;
}

.royale-ceremonies::before {
  left: 0;
}

.royale-ceremonies::after {
  right: 0;
  transform: scaleX(-1);
}

.ryc-title {
  margin: 0 0 clamp(28px, 5vw, 56px);
  font-family: var(--font-display);
  font-size: clamp(30px, 7vw, 52px);
  font-weight: 600;
  line-height: 1.1;
  text-align: center;
  color: var(--royale-gold);
}

.ryc-grid {
  display: grid;
  grid-template-columns: 1fr;
  justify-items: center;
  gap: clamp(28px, 5vw, 52px);
  width: 100%;
}

/* The torn band that marks where one section ends and the next begins:
   media/section_divider.svg is 2405x198, so at full width it comes to 8.2%
   of that - solid between 29% and 62% of its own height, torn above and
   below. Shifted by ~half of itself, that solid middle lands on the
   section's own edge and the tear reaches into both sides of it. */
.ry-tear {
  position: absolute;
  z-index: 2;
  left: 0;
  right: 0;
  width: 100%;
  height: auto;
  pointer-events: none;
  user-select: none;
}

.ry-tear--bottom {
  bottom: 0;
  transform: translateY(54%);
}

/* Flipped, so the deeper tear bites upward out of the section it belongs to. */
.ry-tear--top {
  top: 0;
  transform: translateY(-46%) scaleY(-1);
}

/* On a landscape screen the hero's picture meets the page with just its own
   fade (.royale-hero__frame::after), no torn band. */
@media (orientation: landscape) {
  .royale-hero .ry-tear--bottom {
    display: none;
  }
}

/* A divider under each card, the grid's own gap standing above and below it. */
.ryc-divider {
  display: block;
  width: min(88%, 360px);
  height: auto;
  user-select: none;
  -webkit-user-drag: none;
}

@media (min-width: 900px) {
  .royale-ceremonies {
    padding-inline: clamp(24px, 4vw, 64px);
  }

  .ryc-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    align-items: start;
    max-width: 1120px;
  }

  /* Three across, so a divider under every card would cut the rows apart.
     Only the one after the third card stays, drawn right across between the
     two rows. */
  .ryc-divider {
    display: none;
  }

  .ryc-divider--row {
    display: block;
    grid-column: 1 / -1;
    justify-self: center;
    width: min(70%, 760px);
  }
}

.ryc-card {
  position: relative;
  width: 100%;
  max-width: 360px;
  margin: 0;
  aspect-ratio: 447 / 598;
  background: url("media/ceremony_card.svg") center / 100% 100% no-repeat;
  container-type: inline-size;
}

/* Below the three-across layout: one card at a time, sized off the screen
   as in the design - the card you see (x 39-408 of the artwork's 447, the
   rest its shadow) takes 90% of the width, a 5% gap either side, up to a
   480px card on anything wider than a phone. The box is sized from that,
   so its shadow hangs out over the gaps - clipped at the screen's edges by
   the section - and the margins take that shadow back out of the layout,
   so the grid's gap is measured from the card itself, as in the design:
   9.5% of the width down to the divider, and the same again to the next
   card. The divider runs 81% of the width. */
@media (max-width: 899px) {
  .royale-ceremonies {
    padding-inline: 0;
    overflow-x: clip;
  }

  .ryc-title {
    padding-inline: 24px;
  }

  .ryc-grid {
    gap: min(9.5vw, 52px);
  }

  .ryc-card {
    --card: min(90%, 480px);

    width: calc(var(--card) * 447 / 369);
    max-width: none;
    margin:
      calc(var(--card) * -35 / 369) calc((100% - var(--card) * 447 / 369) / 2) calc(var(--card) * -44 / 369);
  }

  .ryc-divider {
    width: min(81%, 432px);
  }
}

/* Landscape: the corner ornaments larger, and the heading lifted up between
   them rather than set below them - the top padding no longer has to clear
   the corners' height. 30% a corner leaves the middle 40% of the width for
   the heading, which "Ceremonies" fits at any landscape width. */
@media (orientation: landscape) {
  .royale-ceremonies {
    --corner-w: min(30%, 380px);

    padding-top: clamp(28px, 4vw, 64px);
  }
}

/* Landscape, three across: larger cards. Each column is the card you see,
   its shadow hanging out into the gaps (the same sizing as the phone layout
   above, from a column rather than the screen), across a wider grid.

   The columns are equal shares of what the gaps leave, so the gap is the one
   number that sets both: widening it is what stands the cards apart, and the
   cards give up the width to it. */
@media (orientation: landscape) and (min-width: 900px) {
  .ryc-grid {
    max-width: 1500px;
    column-gap: clamp(40px, 6.5vw, 120px);
  }

  .ryc-card {
    width: calc(100% * 447 / 369);
    max-width: none;
    margin:
      calc(100% * -35 / 369) calc((100% - 100% * 447 / 369) / 2) calc(100% * -44 / 369);
  }
}

/* The cream panel inside the card's border, holding the name and the details.
   The illustration is not in here - it sits in its own well below, over the
   whole card - so this box starts where the writing does. */
.ryc-card__inner {
  position: absolute;
  inset: 6% 10.5% 8.7%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  /* The design's gap between the name and the details below it. */
  gap: 8cqw;
  padding: 0 4% 4%;
  text-align: center;
  color: var(--royale-ceremony-ink);
}

/* The illustrations differ: some taper at the top and nest under the arch,
   others (the vivaah and reception scenes) are wide rectangles that never
   could. So the well spans the whole card and is clipped to the card's own
   panel shape (the clipPath in index.html), which keeps every one of them
   inside the arch however it is drawn - wide ones simply meet the curve. */
.ryc-card__artwell {
  position: absolute;
  inset: 0;
  clip-path: url(#ryCeremonyPanel);
  pointer-events: none;
}

/* Each illustration fitted, whole, into one region under the arch - 74% of
   the panel wide, from 8% to 62% of its height - rather than all set to one
   width: they are drawn to different shapes, and at the size the design
   gives the mehendi scene a width alone would drop the taller ones (the
   reception) onto the name. All six are taller than the region is, so each
   fills its height and centres across it. */
.ryc-card__art {
  position: absolute;
  top: 8%;
  left: 50%;
  width: 74%;
  height: 54%;
  object-fit: contain;
  transform: translateX(-50%);
  user-select: none;
  -webkit-user-drag: none;
}

/* A ceremony the couple added, with their own photo in place of artwork.
   The photo fills the arch: this box is the whole card, cut to the cream
   panel inside its border by the same #ryCeremonyPanel the illustrations'
   well uses, so the photo meets the card's own inner edge with no frame of
   its own. Above the writing panel, which covers the whole card, so the
   photo can take the editor's tap - but only the photo: the rest of this box
   lets taps through to the fields below it. */
.ryc-card__photo {
  /* Down to 54% of the card, a straight edge just above the name (which
     starts at 57.6%). */
  --photo-h: 46.8%;

  position: absolute;
  z-index: 1;
  inset: 0;
  clip-path: url(#ryCeremonyPanel);
  pointer-events: none;
}

/* The photo's own area: from the top of the panel (x 10.5%-89.5%, from 7.2%
   down, in #ryCeremonyPanel's terms) to --photo-h below it. The frame its
   picture is zoomed and placed in (frameImage in render.js), and what crops
   it. */
.ryc-card__photo-box {
  position: absolute;
  top: 7.16%;
  left: 10.52%;
  width: 78.97%;
  height: var(--photo-h);
  overflow: hidden;
}

/* As .gframe__img: the starting point only, until frameImage() sizes the
   picture from its own proportions and the host's zoom and offsets it. Pinned
   to the top-left for the same reason, and max-* undone because a zoomed
   picture is deliberately larger than its area. */
.ryc-card__photo-img {
  position: absolute;
  top: 0;
  left: 0;
  display: block;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  object-fit: cover;
  user-select: none;
  -webkit-user-drag: none;
}

.ryc-defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.ryc-card__name {
  margin: 0;
  font-family: var(--font-display);
  font-size: 8cqw;
  font-weight: 600;
  line-height: 1.1;
}

/* The venue, date and time are set in Sora, against the name's Fraunces. */
.ryc-card__foot {
  width: 100%;
  text-align: left;
  font-family: var(--font-sans);
}

.ryc-card__where {
  display: flex;
  align-items: center;
  gap: 4%;
}

.ryc-card__venue {
  flex: 1;
  margin: 0;
  font-size: 5cqw;
  font-weight: 500;
  line-height: 1.25;
}

/* media/location.svg is the whole button - its outlined square and the pin
   inside it - drawn in a 59px box whose square is 39px of it, so 13.5cqw
   here puts that square at the 9cqw the design has it. */
.ryc-card__map {
  flex: none;
  display: block;
  width: 13.5cqw;
  border-radius: 12%;
}

.ryc-card__map img {
  display: block;
  width: 100%;
  height: auto;
}

/* The pin hops very slightly, now and then, to say it can be tapped: a 3%
   rise and settle in the first fifth of a 2.4s cycle, still for the rest.
   On the picture, not the link, so the tap target and its focus ring keep
   still. Off with reduced motion. */
.ryc-card__map img {
  animation: ry-pin-bob 2.4s ease-in-out infinite;
}

@keyframes ry-pin-bob {
  0%,
  20%,
  100% {
    transform: translateY(0);
  }

  10% {
    transform: translateY(-3%);
  }
}

@media (prefers-reduced-motion: reduce) {
  .ryc-card__map img {
    animation: none;
  }
}

.ryc-card__map:focus-visible {
  outline: 2px solid var(--royale-ceremony-ink);
  outline-offset: 2px;
}

.ryc-card__when {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 4%;
  margin: 4% 0 0;
  padding-top: 4%;
  border-top: max(1px, 0.25cqw) solid rgb(64 8 11 / 0.35);
  font-size: 4.6cqw;
  font-weight: 500;
}

.ryc-card__time {
  font-size: 3cqw;
  opacity: 0.8;
}

/* Each card rises into place the first time it is reached, the way the
   scrolls above unroll as they are met. app.js arms the cards (nothing here
   hides them on its own, so with no script or with reduced motion they
   simply stand there) and takes the arming off again card by card. The
   card's own shadow is drawn into its artwork, so there is nothing to give
   the movement away as it settles. */
/* The rise is a plain length, not cqw: a card is its own query container, so
   cq units written ON it would measure something else entirely. */
.ryc-card.is-armed {
  opacity: 0;
  transform: translateY(clamp(18px, 2.4vw, 34px)) scale(0.97);
}

.ryc-card.is-armed.is-shown {
  opacity: 1;
  transform: none;
  transition:
    opacity 700ms ease,
    transform 900ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* Three across: the row arrives left to right rather than all at once. Only
   here - a phone meets its cards one at a time, so each one goes as it is
   reached (see --i in app.js). */
@media (min-width: 900px) {
  .ryc-card.is-armed.is-shown {
    transition-delay: calc(var(--i, 0) * 120ms);
  }
}

/* --- closing picture -------------------------------------------------- */

/* The last thing on the page: the garden the invitation opened on, seen
   whole this time. Full width and its own shape, with the torn band biting
   down into it out of the cream above. */
.royale-outro {
  position: relative;
  width: 100%;
  /* As the gallery: the band has to bleed up past the top edge, so only the
     sideways overflow is clipped. */
  overflow-x: clip;
  background-color: var(--royale-paper);
}

/* <picture> is an inline box by default, which would leave a baseline gap
   under the block image inside it. */
/* The frame the closing picture pans inside (outro.js moves the picture
   itself with the mouse, drawn a little larger than this box): clipped here
   rather than on the section, which has to let the torn band bleed up past
   its top edge. */
.royale-outro picture {
  display: block;
  overflow: clip;
}

.royale-outro__image {
  display: block;
  width: 100%;
  height: auto;
}

/* The board stands with its foot on the picture's bottom edge. Each drawing
   was made on its own cut's canvas - 940x1672 against the tall picture's
   941x1672, 1671x941 against the wide one's 1672x941 - so at full width,
   bottom aligned, it lands exactly where it was drawn to stand. That is why
   there is no placing to do here beyond that, and why the two swap at the
   same 700px the picture behind them does. */
.royale-outro__easel {
  position: absolute;
  z-index: 1;
  left: 0;
  bottom: 0;
  width: 100%;
}

@media (max-width: 699px) {
  .royale-outro__easel {
    aspect-ratio: 940 / 1672;
    background: url("media/easel.webp") center bottom / 100% auto no-repeat;
  }
}

@media (min-width: 700px) {
  .royale-outro__easel {
    aspect-ratio: 1671 / 941;
    background: url("media/easel_wide.webp") center bottom / 100% auto no-repeat;
  }
}

/* The writing on the board: rose's closing easel - the guest's name, then the
   thank-you line - in rose's type, Cormorant italic, bold over regular. Sized
   in cqw of the easel, so it scales with the painted board and stays on it at
   every width. Both drawings put the cream panel dead centre across: the
   phone's runs x 254-685 of 940, the wide one x 521-1153 of 1671 - 37.8% of
   its canvas, against the phone board's 45.9% - and the text box keeps four
   fifths of whichever it stands on, which leaves room either side. The wide
   board is a plain rectangle, so its writing sits at the panel's own middle
   (y 203-540, so 39.5%); the phone's has a scalloped crown above the panel,
   which is why that one sits lower than its box's centre. */
.royale-outro__easel {
  container-type: inline-size;
}

.royale-outro__board {
  position: absolute;
  left: 50%;
  width: 38%;
  transform: translate(-50%, -50%);
  text-align: center;
  color: var(--royale-door);
}

.royale-outro__names,
.royale-outro__line {
  margin: 0;
  font-family: var(--font-names);
  font-style: italic;
  /* The phone board is narrow enough to wrap both lines; even them out
     rather than leave "to Us" alone on the last one. */
  text-wrap: balance;
}

.royale-outro__names {
  margin-bottom: 0.7em;
  font-weight: 600;
  line-height: 1.2;
}

.royale-outro__line {
  font-weight: 400;
  line-height: 1.4;
}

@media (max-width: 699px) {
  .royale-outro__board {
    top: 45%;
  }

  .royale-outro__names {
    font-size: 8cqw;
  }

  .royale-outro__line {
    font-size: 5.8cqw;
  }
}

@media (min-width: 700px) {
  .royale-outro__board {
    top: 39.5%;
    width: 31%;
  }

  /* Down by the same 31/38 the board itself came in, so the writing keeps
     its measure on the narrower panel. */
  .royale-outro__names {
    font-size: 2.8cqw;
  }

  .royale-outro__line {
    font-size: 2cqw;
  }
}

/* --- swipe hint (the gallery's and the ceremonies') -------------------- */
.ry-hint {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 18px;
  border-radius: 999px;
  background: rgb(242 217 191 / 0.7);
  pointer-events: none;
  transition: opacity 0.5s ease;
}

.ry-hint.is-hidden {
  opacity: 0;
}

.ry-hint__arrows {
  display: inline-flex;
  gap: 3px;
}

.ry-hint__arrows:first-child {
  transform: scaleX(-1);
}

.ry-hint__arrows i {
  width: 7px;
  height: 7px;
  border-top: 2px solid var(--royale-door);
  border-right: 2px solid var(--royale-door);
  transform: rotate(45deg);
  opacity: 0.25;
  animation: ry-hint-chevron 1.4s infinite;
}

.ry-hint__arrows i:nth-child(2) {
  animation-delay: 0.18s;
}

.ry-hint__arrows i:nth-child(3) {
  animation-delay: 0.36s;
}

@keyframes ry-hint-chevron {

  0%,
  100% {
    opacity: 0.2;
  }

  50% {
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ry-hint__arrows i {
    animation: none;
    opacity: 0.6;
  }
}

/* --- gate ------------------------------------------------------------- */
/* Fixed over the hero until app.js sets [hidden] at the end of the opening.
   The gate has no background of its own - the two doors are the backdrop -
   so nothing is left behind once they have swung away.

   Scrolling is not locked with overflow:hidden on the page: that removes a
   desktop scrollbar, which then pops back in and shifts the hero sideways
   just as it is revealed (and a reserved scrollbar-gutter is left unpainted
   beside the gate instead). touch-action here stops a swipe on the gate from
   scrolling what is behind it; app.js does the same for the mouse wheel and
   returns the page to the top as the gate opens.

   The card is the gate's one in-flow child. In portrait it is laid out down
   the doors, between the sprigs' 160u and the elephants' 126u, and centred in
   what is left; in landscape it stands in the middle, the full height. */
.royale-gate {
  position: fixed;
  inset: 0;
  z-index: 10;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  padding-block:
    calc(var(--gate-inset) + 160 * var(--u)) calc(var(--gate-inset) + 126 * var(--u));
  overflow: hidden;
  /* The depth the doors swing open in: 2.4 door-widths (each door is half
     the screen). Deep enough for a door's free edge to visibly loom as it
     lifts, and in vw so the swing has the same shape on a phone as on a
     desktop. A free edge comes at most half a screen forward, so it never
     reaches the viewer. */
  perspective: 120vw;
  touch-action: none;
  overscroll-behavior: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

@media (orientation: landscape) {
  .royale-gate {
    flex-direction: row;
    justify-content: center;
    padding: 0;
    /* The doors turn about the top and bottom of the screen here, so the
       same 2.4 door-heights. */
    perspective: calc(1.2 * var(--screen-h));
  }
}

.royale-gate[hidden] {
  display: none;
}

.royale-gate:focus-visible {
  outline: 2px solid var(--royale-cream);
  outline-offset: -6px;
}

.is-open .royale-gate {
  cursor: default;
  pointer-events: none;
}

/* Two flat maroon doors meeting at a seam: side by side in portrait, one
   above the other in landscape. The first (--start: the left one, or the top
   one) laps over the second: its edge at the seam is a crimson strip, and it
   casts a soft shadow across onto the second door. They overlap by a pixel
   so a fractional 50% never opens a hairline of hero between them.

   They stop --gate-inset short of the screen's ends - top and bottom in
   portrait, the sides in landscape - with a soft shadow cast off those two
   edges onto the flaps (the negative spread keeps it from spilling far
   along them). */
.royale-gate__door {
  --edge-shadow:
    0 calc(-10 * var(--u)) calc(30 * var(--u)) calc(-10 * var(--u)) rgb(20 0 8 / 0.5),
    0 calc(10 * var(--u)) calc(30 * var(--u)) calc(-10 * var(--u)) rgb(20 0 8 / 0.5);

  position: absolute;
  top: var(--gate-inset);
  bottom: var(--gate-inset);
  width: calc(50% + 1px);
  background-color: var(--royale-door);
  /* media/motif.png, faint, in a half-drop lattice - see --motif-tile. */
  background-image: url("media/motif_tile.svg");
  background-size: var(--motif-tile);
  background-repeat: repeat;
  box-shadow: var(--edge-shadow);
}

/* The lattice is centred on the seam, one column straddling it, so the two
   doors meet in step: each door's tile is placed from its seam edge, the
   1px allowing for the doors' overlap there. */
.royale-gate__door--start {
  left: 0;
  z-index: 1;
  background-position: right calc(1px - 95.55 * var(--u)) top calc(6 * var(--u));
  box-shadow:
    var(--edge-shadow),
    calc(6 * var(--u)) 0 calc(18 * var(--u)) calc(-2 * var(--u)) rgb(20 0 8 / 0.45);
}

.royale-gate__door--end {
  right: 0;
  background-position: left calc(1px - 31.85 * var(--u)) top calc(6 * var(--u));
}

/* The seam: a crimson strip down the first door's free edge, darkening a
   little toward the edge, as in the design. */
.royale-gate__door--start::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: var(--seam-w);
  background: linear-gradient(90deg, var(--royale-band) 35%, #650029);
}

@media (orientation: landscape) {
  .royale-gate__door {
    --edge-shadow:
      calc(-10 * var(--u)) 0 calc(30 * var(--u)) calc(-10 * var(--u)) rgb(20 0 8 / 0.5),
      calc(10 * var(--u)) 0 calc(30 * var(--u)) calc(-10 * var(--u)) rgb(20 0 8 / 0.5);

    left: var(--gate-inset);
    right: var(--gate-inset);
    width: auto;
    height: calc(50% + 1px);
  }

  .royale-gate__door--start {
    top: 0;
    bottom: auto;
    box-shadow:
      var(--edge-shadow),
      0 calc(6 * var(--u)) calc(18 * var(--u)) calc(-2 * var(--u)) rgb(20 0 8 / 0.45);
  }

  .royale-gate__door--end {
    top: auto;
    bottom: 0;
  }

  /* Both doors span the same width here, so the lattice centres on their
     middle - the screen's - with a column standing under the card, as in
     the design, and starts 6u under each door's top. */
  .royale-gate__door--start,
  .royale-gate__door--end {
    background-position: calc(50% + 31.85 * var(--u)) calc(6 * var(--u));
  }

  .royale-gate__door--start::after {
    top: auto;
    left: 0;
    width: auto;
    height: var(--seam-w);
    background: linear-gradient(180deg, var(--royale-band) 35%, #650029);
  }

  /* The border (see --border) down each door's two outer edges. Both doors'
     repeats start from the seam - the top door's from its foot, the bottom
     door's from its head - so the stars run on evenly across it. */
  .royale-gate__door::before {
    content: "";
    position: absolute;
    inset: 0;
    background:
      var(--border) left bottom / var(--border-tile) repeat-y,
      var(--border) right bottom / var(--border-tile) repeat-y;
  }

  .royale-gate__door--end::before {
    background-position: left top, right top;
  }
}

/* Two curved flaps behind the doors, each a half-ellipse hanging from its
   own screen edge: over the top and bottom of the hero in portrait, over its
   sides in landscape - 15% of the screen deep at the middle. The ellipse
   runs 60.35% of the screen either side of the middle - further than the
   screen does - so the curve is still ~8.4% deep where it meets the screen's
   corners instead of closing to a point. That is deeper than the doors'
   --gate-inset (7.86%), so the band the doors leave is solid flap, and the
   curve shows as the doors swing away. First in the gate so both doors
   paint over them.

   Each flap is two clipped layers: ::before is the full half-ellipse in the
   card's crimson, ::after the maroon fill, the same ellipse with both radii
   taken in by --seam-w - which leaves a crimson border of that width along
   the curve, matching the doors' seam. The clips live on the pseudo-elements
   rather than the flap itself so the flap's drop-shadow (a filter on the
   unclipped parent) is not clipped off with them. */
.royale-gate__flap {
  --curve: ellipse(60.35% 100% at 50% 0);
  --curve-in: ellipse(calc(60.35% - var(--seam-w)) calc(100% - var(--seam-w)) at 50% 0);

  position: absolute;
  left: 0;
  right: 0;
  height: 15%;
  filter: drop-shadow(0 0 calc(10 * var(--u)) rgb(20 0 8 / 0.35));
}

.royale-gate__flap::before,
.royale-gate__flap::after {
  content: "";
  position: absolute;
  inset: 0;
}

.royale-gate__flap::before {
  background-color: var(--royale-band);
  clip-path: var(--curve);
}

.royale-gate__flap::after {
  background-color: var(--royale-flap);
  clip-path: var(--curve-in);
}

.royale-gate__flap--start {
  top: 0;
}

.royale-gate__flap--end {
  --curve: ellipse(60.35% 100% at 50% 100%);
  --curve-in: ellipse(calc(60.35% - var(--seam-w)) calc(100% - var(--seam-w)) at 50% 100%);

  bottom: 0;
}

@media (orientation: landscape) {
  .royale-gate__flap {
    top: 0;
    bottom: 0;
    width: 15%;
    height: auto;
  }

  .royale-gate__flap--start {
    --curve: ellipse(100% 60.35% at 0 50%);
    --curve-in: ellipse(calc(100% - var(--seam-w)) calc(60.35% - var(--seam-w)) at 0 50%);

    right: auto;
  }

  .royale-gate__flap--end {
    --curve: ellipse(100% 60.35% at 100% 50%);
    --curve-in: ellipse(calc(100% - var(--seam-w)) calc(60.35% - var(--seam-w)) at 100% 50%);

    left: auto;
    right: 0;
  }
}

/* An all-over lattice of media/star.svg, as in the design: rows of stars
   36.5u apart, a second set halfway between them, offset half a column, so
   the marks stagger. Each set is one layer of the same file; its
   #svgView(viewBox) widens the star's own 11 x 11 box into a 36.5u x 18u
   tile with the star, drawn 7u across, at its centre - so the file is used
   as drawn, in its own colour, with no copy of it here.

   Centred across the screen, so the lattice is symmetrical, with the first
   row 2.5u in from the flap's outer edge - inside the band the doors leave
   uncovered - mirrored on the far flap. Clipped to the same curve as the
   flap's fill, so it stops inside the crimson border, and lifted over that
   fill with z-index. */
.royale-gate__flap-pattern {
  --star: url("media/star.svg#svgView(viewBox(-23.66,-9.12,57.36,28.29))");
  --tile: calc(36.5 * var(--u)) calc(18 * var(--u));

  position: absolute;
  z-index: 1;
  inset: 0;
  clip-path: var(--curve-in);
  background-image: var(--star), var(--star);
  background-size: var(--tile), var(--tile);
  background-repeat: repeat;
}

.royale-gate__flap--start .royale-gate__flap-pattern {
  background-position:
    50% calc(2.5 * var(--u)),
    calc(50% + 18.25 * var(--u)) calc(-6.5 * var(--u));
}

.royale-gate__flap--end .royale-gate__flap-pattern {
  background-position:
    50% calc(100% - 2.5 * var(--u)),
    calc(50% + 18.25 * var(--u)) calc(100% + 6.5 * var(--u));
}

@media (orientation: landscape) {
  .royale-gate__flap--start .royale-gate__flap-pattern {
    background-position:
      calc(-15.75 * var(--u)) 50%,
      calc(2.5 * var(--u)) calc(50% + 9 * var(--u));
  }

  .royale-gate__flap--end .royale-gate__flap-pattern {
    background-position:
      calc(100% + 15.75 * var(--u)) 50%,
      calc(100% - 2.5 * var(--u)) calc(50% + 9 * var(--u));
  }
}

/* The corner artwork: a sprig at the top of each side and an elephant at the
   foot, one layer per piece, above the doors and below the card. Each layer
   is exactly the size of the door it is painted on, so it turns on that
   door's hinge along with it (see the opening sequence) - and which door that
   is changes with the orientation: in portrait each side's door carries its
   own sprig and elephant, in landscape the top door carries both sprigs and
   the bottom door both elephants. */
.royale-gate__decor {
  position: absolute;
  z-index: 2;
  top: var(--gate-inset);
  bottom: var(--gate-inset);
  width: calc(50% + 1px);
  pointer-events: none;
}

.royale-gate__decor--sprig-left,
.royale-gate__decor--elephant-left {
  left: 0;
}

.royale-gate__decor--sprig-right,
.royale-gate__decor--elephant-right {
  right: 0;
}

.royale-gate__floral,
.royale-gate__elephant {
  position: absolute;
  display: block;
  height: auto;
  user-select: none;
  -webkit-user-drag: none;
}

/* Portrait: placed from each door's inner edge (the centre of the screen),
   so a tablet keeps them tucked in toward the card rather than flung out to
   its corners. The offsets allow for the transparent margin baked into each
   PNG - measured against the design, the sprig shows 80u wide from 19u under
   the door's top, the elephant 97u wide standing 18u above its foot. */
.royale-gate__floral {
  top: calc(7 * var(--u));
  width: calc(92 * var(--u));
}

.royale-gate__elephant {
  bottom: calc(14 * var(--u));
  width: calc(122 * var(--u));
}

.royale-gate__decor--sprig-left .royale-gate__floral {
  right: calc(92 * var(--u));
}

.royale-gate__decor--elephant-left .royale-gate__elephant {
  right: calc(76 * var(--u));
}

.royale-gate__decor--sprig-right .royale-gate__floral {
  left: calc(92 * var(--u));
  transform: scaleX(-1);
}

.royale-gate__decor--elephant-right .royale-gate__elephant {
  left: calc(76 * var(--u));
  transform: scaleX(-1);
}

/* Landscape: each piece in the strip of door beside the card (--side), the
   elephant centred in it and the sprig a little toward the screen's edge, as
   in the design. Sized off the height (--k), but never wider than the strip,
   so a squarer window shrinks them rather than tucking them under the card. */
@media (orientation: landscape) {
  .royale-gate__decor {
    --side: calc((100% - var(--card-w)) / 2);

    width: auto;
    left: var(--gate-inset);
    right: var(--gate-inset);
    height: calc(50% + 1px);
  }

  .royale-gate__decor--sprig-left,
  .royale-gate__decor--sprig-right {
    top: 0;
    bottom: auto;
  }

  .royale-gate__decor--elephant-left,
  .royale-gate__decor--elephant-right {
    top: auto;
    bottom: 0;
  }

  .royale-gate__floral {
    --w: min(calc(143 * var(--k)), 0.6 * var(--side));

    top: calc(30 * var(--k));
    width: var(--w);
  }

  .royale-gate__elephant {
    --w: min(calc(276 * var(--k)), var(--side));

    bottom: calc(50 * var(--k));
    width: var(--w);
  }

  .royale-gate__decor--sprig-left .royale-gate__floral {
    right: auto;
    left: max(0px, (var(--side) - var(--w)) / 2 - 35 * var(--k));
  }

  .royale-gate__decor--sprig-right .royale-gate__floral {
    left: auto;
    right: max(0px, (var(--side) - var(--w)) / 2 - 35 * var(--k));
  }

  .royale-gate__decor--elephant-left .royale-gate__elephant {
    right: auto;
    left: calc((var(--side) - var(--w)) / 2);
  }

  .royale-gate__decor--elephant-right .royale-gate__elephant {
    left: auto;
    right: calc((var(--side) - var(--w)) / 2);
  }
}

/* --- card ------------------------------------------------------------- */
/* media/Wrapper_P.svg in portrait, a band across the full width of the
   screen; media/Wrapper_D.svg in landscape, a band up its full height. The
   drawing is the card's background, stretched to fit it (the
   #svgView(preserveAspectRatio(none))), and pushed out past the card's box on
   every side by exactly the room each SVG leaves around its card for the
   shadows it casts - so the card's own box is the drawn card, and nothing
   here needs to know how deep those shadows run. isolation keeps the
   background's z-index:-1 inside the card, behind its text. */
.royale-card {
  position: relative;
  z-index: 3;
  isolation: isolate;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: calc(419 * var(--u));
  margin-block: auto;
  padding: calc(30 * var(--u)) calc(24 * var(--u)) calc(41 * var(--u));
  text-align: center;
  color: var(--royale-ink);
}

.royale-card::before {
  content: "";
  position: absolute;
  z-index: -1;
  /* Wrapper_P.svg is 514 x 606, its card x 56-458, y 75-494. */
  top: calc(-75 / 419 * 100%);
  bottom: calc(-112 / 419 * 100%);
  left: calc(-56 / 402 * 100%);
  right: calc(-56 / 402 * 100%);
  background: url("media/Wrapper_P.svg#svgView(preserveAspectRatio(none))") center / 100% 100% no-repeat;
  pointer-events: none;
}

@media (orientation: landscape) {
  .royale-card {
    flex: none;
    width: var(--card-w);
    min-height: 0;
    margin-block: 0;
    padding: calc(40 * var(--u)) calc(36 * var(--u));
  }

  .royale-card::before {
    /* Wrapper_D.svg is 788 x 1177, its card x 114-693, y 41-1065. */
    top: calc(-41 / 1024 * 100%);
    bottom: calc(-112 / 1024 * 100%);
    left: calc(-114 / 579 * 100%);
    right: calc(-95 / 579 * 100%);
    background-image: url("media/Wrapper_D.svg#svgView(preserveAspectRatio(none))");
  }

  /* The border down both of the card's crimson bands (see --border). Over
     the card's drawing, still behind its writing. */
  .royale-card::after {
    content: "";
    position: absolute;
    z-index: -1;
    inset: 0;
    background:
      var(--border) left top / var(--border-tile) repeat-y,
      var(--border) right top / var(--border-tile) repeat-y;
    pointer-events: none;
  }
}

/* The doors' motif lattice (see --motif-tile) carried onto the card. The
   tile as drawn is pale peach, which vanishes on cream, so here it is a
   stencil instead: the card's ink, showing through the tile's own
   transparency - the same faint strength as on the doors, dark on light
   rather than light on dark. Only over the cream, stopping at the crimson
   bands (15 of the card's 419 top and bottom in portrait, 15 of 579 either
   side in landscape), and centred on the card - a column down its middle,
   a row across it. Behind the writing; over the card's drawing, since it
   comes after ::before, and under the landscape border, drawn by ::after. */
.royale-card__motif {
  position: absolute;
  z-index: -1;
  inset: calc(15 / 419 * 100%) 0;
  background-color: var(--royale-ink);
  -webkit-mask: url("media/motif_tile.svg") repeat;
  mask: url("media/motif_tile.svg") repeat;
  -webkit-mask-size: var(--motif-tile);
  mask-size: var(--motif-tile);
  -webkit-mask-position: calc(50% + 31.85 * var(--u)) calc(50% + 10.1 * var(--u));
  mask-position: calc(50% + 31.85 * var(--u)) calc(50% + 10.1 * var(--u));
  pointer-events: none;
}

@media (orientation: landscape) {
  .royale-card__motif {
    inset: 0 calc(15 / 579 * 100%);
  }
}

.royale-card__symbol {
  display: block;
  flex: none;
  /* The symbol SVGs carry blank margin around the figure; 98u draws the
     Ganesha itself ~80u tall, as in the design. */
  width: calc(98 * var(--u));
  aspect-ratio: 1 / 1;
  background-color: currentColor;
  -webkit-mask: url("../symbols/_ganesha.svg") center / contain no-repeat;
  mask: url("../symbols/_ganesha.svg") center / contain no-repeat;
}

.royale-card__mantra,
.royale-card__guest,
.royale-card__note {
  margin: 0;
}

/* Tiro ships in one weight; 700 is the same synthesised bold papercut sets
   its invocation in. The loose line-height leaves room for श्री's repha. */
.royale-card__mantra {
  margin-top: calc(8 * var(--u));
  font-family: var(--font-devanagari);
  font-size: calc(25 * var(--u));
  font-weight: 700;
  line-height: 1.45;
}

.royale-card__guest {
  max-width: calc(280 * var(--u));
  margin-top: calc(10 * var(--u));
  font-family: var(--font-display);
  font-size: calc(31.5 * var(--u));
  font-weight: 600;
  line-height: 1.27;
}

/* Narrow enough to break where the design does: four lines, the last just
   "of...". */
.royale-card__note {
  max-width: calc(222 * var(--u));
  margin-top: calc(20 * var(--u));
  font-family: var(--font-display);
  font-size: calc(17.5 * var(--u));
  font-weight: 500;
  line-height: 1.32;
}

/* The landscape card has the room to space its lines a little wider. */
@media (orientation: landscape) {
  .royale-card__guest {
    margin-top: calc(30 * var(--u));
  }

  .royale-card__note {
    margin-top: calc(30 * var(--u));
  }
}

/* The plaque both of the template's buttons are drawn on - the cover's tap
   cue and the date reveal: tap_to_open.svg, a scalloped plaque with an inner
   shadow, edged in a gold gradient. Drawn inline in index.html (each copy
   with its own ids) and painted here from the theme's tokens, with gold text
   over it and ry-cue-bob saying it can be pressed. The label sits in the
   drawing's straight middle (37..263 of its 300), clear of the scallops;
   each button sets its own width and type size. */
.ry-plaque {
  --royale-cue-fill: var(--royale-band);
  --royale-cue-edge: #8b5f30;
  --royale-cue-shine: #e1c6a8;
  position: relative;
  display: inline-grid;
  place-items: center;
  box-sizing: border-box;
  aspect-ratio: 300 / 78;
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.02em;
  white-space: nowrap;
  color: var(--royale-gold);
  animation: ry-cue-bob 1.8s ease-in-out infinite;
}

.ry-plaque__frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

.ry-plaque__fill {
  fill: var(--royale-cue-fill);
}

.ry-plaque__edge {
  stop-color: var(--royale-cue-edge);
}

.ry-plaque__shine {
  stop-color: var(--royale-cue-shine);
}

/* Over the frame, centred - the plaque is symmetric about its middle. */
.ry-plaque__label {
  position: relative;
}

/* The cover's tap cue, sized in --u like the rest of the card. */
.royale-card__cue {
  width: calc(170 * var(--u));
  margin-top: calc(24 * var(--u));
  font-size: calc(15 * var(--u));
}

@media (prefers-reduced-motion: reduce) {
  .royale-card__cue {
    animation: none;
  }
}

/* --- opening sequence ------------------------------------------------- */
/* 1. The card slides off the screen.
   Portrait: straight up. It sits centred 17u below the middle of the screen
   (the difference between the gate's top and bottom padding, halved), so
   clearing the top takes half the screen, half its own height, and 130u for
   the shadow it casts below itself.
   Landscape: out to the right, from the middle - half the screen, plus its
   own width and the 20% of it its shadow reaches to the left. */
.is-open .royale-card {
  animation: ry-card-up var(--card-dur) var(--card-ease) forwards;
}

@keyframes ry-card-up {
  to {
    transform: translateY(calc(-50% - var(--screen-h) / 2 - 130 * var(--u)));
  }
}

@keyframes ry-card-right {
  to {
    transform: translateX(calc(71% + 50vw));
  }
}

/* 2. The doors swing open like the covers of a book: each is hinged on its
   outer edge and turns toward the viewer, so the gate's perspective makes
   its free edge loom larger as it lifts - the left and right doors about
   their outer sides, the top and bottom ones about the top and bottom of the
   screen. The artwork on a door turns on the same hinge, so the sprigs and
   elephants stay painted on it. Past 90deg a door has turned its back to the
   screen and backface-visibility drops it, so 100deg is the end of the
   swing; it dims a touch as it turns away from the light. */
.royale-gate__door,
.royale-gate__decor {
  backface-visibility: hidden;
}

.royale-gate__door--start,
.royale-gate__decor--sprig-left,
.royale-gate__decor--elephant-left {
  transform-origin: 0 50%;
}

.royale-gate__door--end,
.royale-gate__decor--sprig-right,
.royale-gate__decor--elephant-right {
  transform-origin: 100% 50%;
}

.is-open .royale-gate__door--start,
.is-open .royale-gate__decor--sprig-left,
.is-open .royale-gate__decor--elephant-left {
  animation: ry-door-left var(--door-dur) var(--door-ease) var(--door-delay) forwards;
}

.is-open .royale-gate__door--end,
.is-open .royale-gate__decor--sprig-right,
.is-open .royale-gate__decor--elephant-right {
  animation: ry-door-right var(--door-dur) var(--door-ease) var(--door-delay) forwards;
}

@media (orientation: landscape) {
  .is-open .royale-card {
    animation-name: ry-card-right;
  }

  .royale-gate__door--start,
  .royale-gate__decor--sprig-left,
  .royale-gate__decor--sprig-right {
    transform-origin: 50% 0;
  }

  .royale-gate__door--end,
  .royale-gate__decor--elephant-left,
  .royale-gate__decor--elephant-right {
    transform-origin: 50% 100%;
  }

  .is-open .royale-gate__door--start,
  .is-open .royale-gate__decor--sprig-left,
  .is-open .royale-gate__decor--sprig-right {
    animation-name: ry-door-top;
  }

  .is-open .royale-gate__door--end,
  .is-open .royale-gate__decor--elephant-left,
  .is-open .royale-gate__decor--elephant-right {
    animation-name: ry-door-bottom;
  }
}

/* Light falling off toward a door's free edge as it turns, which is most of
   what makes the swing read as depth rather than a squash. One artwork layer
   per door carries it - the later of its two, so it lies over the other and
   the whole door darkens, artwork and all. */
.royale-gate__decor::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0;
}

.royale-gate__decor--elephant-left::after {
  background: linear-gradient(90deg, rgb(20 0 8 / 0) 10%, rgb(20 0 8 / 0.6));
}

.royale-gate__decor--elephant-right::after {
  background: linear-gradient(270deg, rgb(20 0 8 / 0) 10%, rgb(20 0 8 / 0.6));
}

@media (orientation: landscape) {
  .royale-gate__decor--elephant-left::after {
    background: none;
  }

  .royale-gate__decor--sprig-right::after {
    background: linear-gradient(180deg, rgb(20 0 8 / 0) 10%, rgb(20 0 8 / 0.6));
  }

  .royale-gate__decor--elephant-right::after {
    background: linear-gradient(0deg, rgb(20 0 8 / 0) 10%, rgb(20 0 8 / 0.6));
  }
}

.is-open .royale-gate__decor::after {
  animation: ry-appear var(--door-dur) var(--door-ease) var(--door-delay) forwards;
}

@keyframes ry-door-left {
  to {
    transform: rotateY(-100deg);
    filter: brightness(0.65);
  }
}

@keyframes ry-door-right {
  to {
    transform: rotateY(100deg);
    filter: brightness(0.65);
  }
}

@keyframes ry-door-top {
  to {
    transform: rotateX(100deg);
    filter: brightness(0.65);
  }
}

@keyframes ry-door-bottom {
  to {
    transform: rotateX(-100deg);
    filter: brightness(0.65);
  }
}

/* 3. Once the doors are out of the way, the flaps open the same way: each
   hinged on its own screen edge and turning toward the viewer - in portrait
   the top one up and the bottom one down, in landscape the left and right
   ones outward - until it has turned its back and drops out. */
.royale-gate__flap {
  backface-visibility: hidden;
}

.royale-gate__flap--start {
  transform-origin: 50% 0;
}

.royale-gate__flap--end {
  transform-origin: 50% 100%;
}

.is-open .royale-gate__flap--start {
  animation: ry-flap-top var(--flap-dur) var(--flap-ease) var(--flap-delay) forwards;
}

.is-open .royale-gate__flap--end {
  animation: ry-flap-bottom var(--flap-dur) var(--flap-ease) var(--flap-delay) forwards;
}

@media (orientation: landscape) {
  .royale-gate__flap--start {
    transform-origin: 0 50%;
  }

  .royale-gate__flap--end {
    transform-origin: 100% 50%;
  }

  .is-open .royale-gate__flap--start {
    animation-name: ry-flap-left;
  }

  .is-open .royale-gate__flap--end {
    animation-name: ry-flap-right;
  }
}

@keyframes ry-flap-top {
  to {
    transform: rotateX(100deg);
    filter: drop-shadow(0 0 calc(10 * var(--u)) rgb(20 0 8 / 0.35)) brightness(0.65);
  }
}

@keyframes ry-flap-bottom {
  to {
    transform: rotateX(-100deg);
    filter: drop-shadow(0 0 calc(10 * var(--u)) rgb(20 0 8 / 0.35)) brightness(0.65);
  }
}

@keyframes ry-flap-left {
  to {
    transform: rotateY(-100deg);
    filter: drop-shadow(0 0 calc(10 * var(--u)) rgb(20 0 8 / 0.35)) brightness(0.65);
  }
}

@keyframes ry-flap-right {
  to {
    transform: rotateY(100deg);
    filter: drop-shadow(0 0 calc(10 * var(--u)) rgb(20 0 8 / 0.35)) brightness(0.65);
  }
}


/* 4. The names fade up out of a soft blur, one line after another. */
.is-open .royale-couple__name,
.is-open .royale-couple__amp {
  animation: ry-name-in var(--names-dur) cubic-bezier(0.22, 1, 0.36, 1) var(--names-delay) forwards;
}

.is-open .royale-couple__amp {
  animation-delay: calc(var(--names-delay) + var(--names-stagger));
}

.is-open .royale-couple__name:last-child {
  animation-delay: calc(var(--names-delay) + 2 * var(--names-stagger));
}

@keyframes ry-name-in {
  from {
    opacity: 0;
    transform: translateY(0.3em);
    filter: blur(6px);
  }

  to {
    opacity: 1;
    transform: none;
    filter: none;
  }
}

/* Reduced motion: no lifting, sliding or swinging - the gate simply fades
   away and the names fade in. app.js ends the gate on ry-gate-fade here. */
@media (prefers-reduced-motion: reduce) {

  .is-open .royale-card,
  .is-open .royale-gate__door--start,
  .is-open .royale-gate__door--end,
  .is-open .royale-gate__decor,
  .is-open .royale-gate__decor::after,
  .is-open .royale-gate__flap--start,
  .is-open .royale-gate__flap--end {
    animation: none;
  }

  .is-open .royale-gate {
    animation: ry-gate-fade 500ms ease forwards;
  }

  /* :last-child listed too, or its stagger delay above (a more specific
     selector) would still hold the groom's name back by two seconds. */
  .is-open .royale-couple__name,
  .is-open .royale-couple__name:last-child,
  .is-open .royale-couple__amp {
    animation: ry-appear 800ms ease 300ms forwards;
  }
}

@keyframes ry-gate-fade {
  to {
    opacity: 0;
  }
}

@keyframes ry-appear {
  to {
    opacity: 1;
  }
}

/* --- media row: Instagram, locations, calendar ---------------------------- */
/* The same three icons, and the same artwork, the lotus templates carry.
   That artwork is white line-drawing on transparency, drawn for the deep teal
   those templates set it on, so here it is used as a mask over the
   template's maroon (.social__icon): the same drawing, inked for the cream
   paper the row sits on. No ground of its own - the page's cream and its
   motif carry straight on from the note card above. */
.royale-social {
  padding: clamp(30px, 6vw, 52px) 20px;
}

.socials {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  /* 12px floor: the icons are a flat 90px, so three of them need 270px and a
     360px phone leaves about 320px here. 12px of gap fits inside that;
     flex-wrap is the backstop for anything narrower still. */
  gap: clamp(12px, 5vw, 52px);
}

.social {
  display: inline-flex;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

/* [hidden] { display: none } lives in the user-agent origin, so the author
   `display` above beats it and a removed icon would still be drawn. Most of
   the other templates carry a blanket [hidden] { display: none !important },
   but royale guards case by case instead - see .royale-gate[hidden] - so
   every display this file sets on something render.js hides needs its own. */
.social[hidden] {
  display: none;
}

/* The shadow is on the button, not the icon: a mask clips away anything
   painted outside the drawing, a drop-shadow included. The icons are
   cutouts, so the lift still traces the shape rather than a box. */
.social {
  filter: drop-shadow(0 6px 12px rgb(86 15 49 / 0.22));
}

.social__icon {
  display: block;
  /* The flat 90px the lotus templates settled on, so the row is the same size
     in both. The artwork is square. */
  width: 90px;
  height: 90px;
  background-color: var(--royale-maroon);
  -webkit-mask: var(--icon) center / contain no-repeat;
  mask: var(--icon) center / contain no-repeat;
  transition: transform 300ms cubic-bezier(0.22, 1, 0.36, 1);
}

.social__icon--instagram {
  --icon: url("media/instagram.webp");
}

.social__icon--location {
  --icon: url("media/location.webp");
}

.social__icon--calendar {
  --icon: url("media/calendar.webp");
}

.social:hover .social__icon,
.social:focus-visible .social__icon {
  transform: scale(1.12);
}

.social:focus-visible {
  outline: 3px solid var(--royale-maroon);
  outline-offset: 6px;
  border-radius: 8px;
}

/* --- media-row popup ------------------------------------------------------ */
/* One box, refilled per button by social.js. */
.ry-modal {
  position: fixed;
  inset: 0;
  z-index: 120;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}

/* The display above beats the user-agent's [hidden] rule, so the closed popup
   would sit over the whole invitation as an empty cream box. See .social
   [hidden] for why royale needs these one at a time. */
.ry-modal[hidden] {
  display: none;
}

.ry-modal__overlay {
  position: absolute;
  inset: 0;
  background: rgba(30, 2, 14, 0.58);
  backdrop-filter: blur(2px);
}

/* The box must NOT scroll: its gold keyline is drawn on the border box, and
   scrolling here would drag that edge up through the content. The box is a
   fixed frame and .ry-modal__content scrolls inside it instead. */
.ry-modal__box {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  width: min(92vw, 430px);
  max-height: 86vh;
  overflow: hidden;
  padding: clamp(24px, 5vw, 34px) clamp(20px, 4vw, 30px);
  border: 2px solid var(--royale-gold);
  border-radius: 12px;
  background: var(--royale-cream);
  box-shadow: 0 30px 60px rgba(30, 2, 14, 0.45);
  animation: ry-modal-pop 300ms cubic-bezier(0.22, 1, 0.36, 1);
}

@keyframes ry-modal-pop {
  from {
    opacity: 0;
    transform: translateY(12px) scale(0.97);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

.ry-modal__content {
  min-height: 0;
  overflow-y: auto;
}

.ry-modal__close {
  position: absolute;
  top: 8px;
  right: 12px;
  z-index: 2;
  border: 0;
  padding: 4px 8px;
  background: none;
  color: var(--royale-crimson);
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
}

/* Sora, not the template's serif: a popup is UI rather than invitation, and
   the serif belongs to the card. The lotus templates set their popups the
   same way. */
.ry-modal__title {
  margin: 0;
  font-family: var(--font-sans);
  font-size: clamp(21px, 4.4vw, 27px);
  font-weight: 600;
  text-align: center;
  color: var(--royale-crimson);
}

/* A gold rule under the title, fading out at both ends - the band colour the
   rest of the template trims with, rather than a fourth ornament. */
.ry-modal__divider {
  display: block;
  width: 120px;
  height: 2px;
  margin: 13px auto 18px;
  background: linear-gradient(90deg, transparent, var(--royale-gold), transparent);
}

.ry-modal__text {
  margin: 0 0 18px;
  font-family: var(--font-sans);
  font-size: clamp(13px, 3vw, 15px);
  line-height: 1.6;
  text-align: center;
  color: var(--royale-ink);
}

/* Plain text, not a pill like .ry-modal__btn - "Back to dates" is a step back,
   not a commitment on par with the three calendars above it, and a fourth
   pill would read as one more choice rather than an exit from the three. */
.ry-modal__link {
  border: 0;
  background: none;
  padding: 0;
  font-family: inherit;
  font-size: inherit;
  text-decoration: underline;
  color: var(--royale-ink);
  cursor: pointer;
}

.ry-modal__link:hover,
.ry-modal__link:focus-visible {
  color: var(--royale-crimson);
}

.ry-modal__empty {
  margin: 0;
  padding: 14px 0 6px;
  font-family: var(--font-sans);
  font-size: clamp(13px, 3vw, 15px);
  text-align: center;
  color: var(--royale-ink);
}

.ry-modal__input {
  display: block;
  width: 100%;
  margin: 0 0 18px;
  padding: 11px 13px;
  border: 1px solid rgba(127, 16, 56, 0.35);
  border-radius: 8px;
  background: #ffffff;
  font-family: var(--font-sans);
  font-size: 15px;
  color: var(--royale-crimson);
}

.ry-modal__input:focus-visible {
  outline: 2px solid var(--royale-crimson);
  outline-offset: 1px;
}

.ry-modal__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  /* The calendar's date list butts straight into this row otherwise: the
     list's own gap spaces its rows against each other, not against what
     follows it. */
  margin-top: 16px;
}

/* Below about 350px two buttons sharing a line come out near 119px each, and
   "Open Instagram" needs roughly 126px even at the 13px floor - so it would
   wrap inside its own pill. Stacked, they stay equal AND full width. */
@media (max-width: 350px) {
  .ry-modal__actions {
    flex-direction: column;
  }
}

.ry-modal__btn {
  /* Equal widths that together fill the box. The basis is 0, not auto, which
     is the point: from auto the two would be sized by their labels first and
     only share the leftover, so "Open Instagram" would come out much wider
     than "Not now". From 0 they are sized by growth alone. */
  flex: 1 1 0;

  /* An <a> and a <button> both wear this class; inline-flex centring makes
     the two render identically once they are being stretched. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-align: center;

  border: 1px solid rgba(127, 16, 56, 0.4);
  border-radius: 999px;
  padding: 0.6em 1em;
  background: transparent;
  font-family: var(--font-sans);
  font-size: clamp(13px, 3vw, 15px);
  color: var(--royale-crimson);
  text-decoration: none;
  cursor: pointer;
  transition: background-color 200ms ease;
}

.ry-modal__btn:hover,
.ry-modal__btn:focus-visible {
  background-color: rgba(127, 16, 56, 0.08);
}

.ry-modal__btn--primary {
  border-color: var(--royale-crimson);
  background: var(--royale-crimson);
  color: var(--royale-cream);
}

.ry-modal__btn--primary:hover,
.ry-modal__btn--primary:focus-visible {
  background: var(--royale-maroon-deep);
}

/* One row per venue (locations) or per date (calendar) - the same row drawn
   twice, so the two popups read as one family. */
.ry-loc-list {
  display: grid;
  gap: 10px;
}

.ry-loc {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 12px 14px;
  border: 1px solid rgba(127, 16, 56, 0.22);
  border-radius: 10px;
  background: rgba(127, 16, 56, 0.04);
  font-family: var(--font-sans);
  text-align: left;
  text-decoration: none;
  color: var(--royale-crimson);
  cursor: pointer;
  transition: background-color 200ms ease, border-color 200ms ease;
}

.ry-loc:hover,
.ry-loc:focus-visible {
  background: rgba(127, 16, 56, 0.09);
  border-color: rgba(127, 16, 56, 0.45);
}

.ry-loc__pin {
  flex: none;
  width: 26px;
  height: 26px;
  color: var(--royale-crimson);
}

.ry-loc__pin svg {
  width: 100%;
  height: 100%;
}

.ry-loc__text {
  display: grid;
  gap: 2px;
  min-width: 0;
}

/* The quiet line: which ceremonies this row covers. */
.ry-loc__ceremonies {
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--royale-ink);
}

.ry-loc__venue {
  font-family: var(--font-sans);
  font-size: clamp(15px, 3.4vw, 17px);
  line-height: 1.3;
}

.ry-loc__addr {
  font-size: 12.5px;
  color: var(--royale-ink);
}

.ry-loc__go {
  margin-left: auto;
  flex: none;
  font-size: 20px;
  color: var(--royale-ink);
}

@media (prefers-reduced-motion: reduce) {
  .ry-modal__box {
    animation: none;
  }

  .social__icon {
    transition: none;
  }

  .social:hover .social__icon,
  .social:focus-visible .social__icon {
    transform: none;
  }
}
