/* Visual themes for the invitation.
 *
 * Every theme overrides the whole palette explicitly on :root[data-theme=...],
 * which outranks base.css's :root — including inside its prefers-color-scheme
 * block. A theme is a committed visual world, like a printed card: it does not
 * follow the reader's dark mode, so it must not half-inherit it either.
 */

/* ========================================================================== */
/* Shared garden scaffolding (rose, lavender)                                 */
/* ========================================================================== */

#garden { position: relative; margin: 0 0 1rem; }

/* In the flow, so it scrolls away for good. A band fixed to the top of a small
   phone costs nearly a fifth of the viewport at all times. */
#garden .garland { display: block; width: 100%; height: clamp(110px, 26vw, 175px); }

/* The falling, on the other hand, is fixed: it belongs to the window, not to
   the document, and it sits BEHIND the words so nothing ever drifts across
   something someone is trying to read. */
#garden .fall {
  position: fixed; inset: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 0;
}

#gardenPool { margin-top: 2.5rem; }
#gardenPool .pool { display: block; width: 100%; height: clamp(70px, 16vw, 110px); }

/* Everything readable rides above the petals. */
:is([data-theme="rose"],[data-theme="lavender"]) main > *:not(#garden):not(#gardenPool),
[data-theme="lavender"] main > *:not(#garden):not(#gardenPool) {
  position: relative; z-index: 1;
}

/* ==========================================================================
 * Rose and Lavender
 *
 * One design, two colourways. Every rule below is shared — type, spacing,
 * rhythm, the lot — and ONLY the token block differs. That is deliberate: if
 * the layouts were allowed to diverge the couple would be choosing between two
 * designs while believing they were choosing a colour.
 * ========================================================================== */

:root[data-theme="rose"] {
  --ground: #fdfaf7;          /* warm ivory, not white */
  --ink:    #3d3330;
  --muted:  #8d7b73;
  --line:   #ebdfd8;
  --bad:    #a8474a;
  --accent: #b5827c;          /* dusty rose */
  --soft:   #f7eeea;
}

:is([data-theme="rose"],[data-theme="lavender"]) body {
  background: var(--ground);
  color: var(--ink);
  /* EB Garamond rather than Cormorant: the same classical shapes with real
     weight behind them. Cormorant is a display face and goes wispy at phone
     sizes, which is exactly where this will be read. */
  font-family: 'EB Garamond', ui-serif, Georgia, serif;
  font-weight: 500;
  font-size: 1.08rem;
  padding: 0 0 1rem;
}
:is([data-theme="rose"],[data-theme="lavender"]) main { max-width: 34rem; padding: 0 1.25rem; }

/* Arabic is set in two faces, not one. Naskh carries the reading — it is what
   Arabic prose is set in and it stays legible small. The names get Aref Ruqaa,
   a formal calligraphic hand of the kind an engraved invitation would use, and
   which would be unreadable as body text. Both selector forms are needed: the
   invitation puts dir on <html>, the holding page puts it on each block. */
:is([data-theme="rose"],[data-theme="lavender"])[dir="rtl"],
:is([data-theme="rose"],[data-theme="lavender"]) [dir="rtl"],
:is([data-theme="rose"],[data-theme="lavender"]) [lang="ar"] {
  font-family: 'Noto Naskh Arabic', 'Geeza Pro', serif;
  font-weight: 400;
}
:is([data-theme="rose"],[data-theme="lavender"])[dir="rtl"] .names,
:is([data-theme="rose"],[data-theme="lavender"]) [dir="rtl"] .names {
  font-family: 'Aref Ruqaa', 'Amiri', serif;
  line-height: 1.7;
}

:is([data-theme="rose"],[data-theme="lavender"]) .hero { padding: 0.5rem 0 1.25rem; }

:is([data-theme="rose"],[data-theme="lavender"]) .families p {
  font-size: 0.95rem; color: var(--ink); line-height: 1.5;
}
/* The name leads; "and his family" follows it, quieter. */
:is([data-theme="rose"],[data-theme="lavender"]) .families span {
  font-size: 0.82rem; color: var(--muted); margin-top: 0.1rem;
}
:is([data-theme="rose"],[data-theme="lavender"]) .honour {
  color: var(--muted); font-style: italic; font-size: 1rem;
  margin: 1.4rem 0 1.2rem;
}
/* Arabic has no italic; letterspacing is the equivalent gesture. */
:is([data-theme="rose"],[data-theme="lavender"]) [dir="rtl"] .honour,
:is([data-theme="rose"],[data-theme="lavender"])[dir="rtl"] .honour { font-style: normal; letter-spacing: 0.02em; }

:is([data-theme="rose"],[data-theme="lavender"]) .rel {
  font-size: 0.78rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--accent);
}
:is([data-theme="rose"],[data-theme="lavender"]) [dir="rtl"] .rel { text-transform: none; letter-spacing: 0; }

:is([data-theme="rose"],[data-theme="lavender"]) .couple .names {
  font-size: clamp(1.7rem, 8vw, 2.5rem); font-weight: 600; color: var(--ink);
  line-height: 1.25;
}

/* A hairline rule between the couple and the practical half of the page. */
:is([data-theme="rose"],[data-theme="lavender"]) .when { color: var(--muted); font-size: 1rem; margin-top: 0.45rem; }
:is([data-theme="rose"],[data-theme="lavender"]) .when:first-of-type { margin-top: 1.4rem; }

:is([data-theme="rose"],[data-theme="lavender"]) .count { gap: 1.4rem; margin: 1.75rem 0; }
:is([data-theme="rose"],[data-theme="lavender"]) .count b {
  font-size: 1.45rem; font-weight: 400; color: var(--accent);
  font-variant-numeric: tabular-nums;
}
:is([data-theme="rose"],[data-theme="lavender"]) .count span {
  font-size: 0.7rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--muted);
}
:is([data-theme="rose"],[data-theme="lavender"]) [dir="rtl"] .count span { text-transform: none; letter-spacing: 0; }

:is([data-theme="rose"],[data-theme="lavender"]) .alloc {
  border: 1px solid var(--line); background: var(--soft);
  border-radius: 2px; padding: 1rem 1.15rem; margin: 1.75rem 0;
  text-align: center;
}
:is([data-theme="rose"],[data-theme="lavender"]) .nochildren {
  color: var(--accent); font-size: 0.92rem; margin-top: 0.6rem;
}

:is([data-theme="rose"],[data-theme="lavender"]) h2 {
  font-size: 0.8rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--muted); font-weight: 600; margin: 2rem 0 0.6rem;
}
:is([data-theme="rose"],[data-theme="lavender"]) [dir="rtl"] h2 {
  text-transform: none; letter-spacing: 0; font-size: 1rem;
}

:is([data-theme="rose"],[data-theme="lavender"]) select {
  border: 1px solid var(--line); background: #fff; border-radius: 2px;
  padding: 0.6rem; font-family: inherit; font-size: 1rem;
}
:is([data-theme="rose"],[data-theme="lavender"]) button {
  font-family: inherit; font-size: 0.95rem; border-radius: 2px;
  border: 1px solid var(--accent); background: var(--accent); color: #fff;
  padding: 0.65rem 1.4rem; margin-top: 0.75rem;
}
:is([data-theme="rose"],[data-theme="lavender"]) button[value="declined"], :is([data-theme="rose"],[data-theme="lavender"]) #again {
  background: transparent; color: var(--accent);
}
:is([data-theme="rose"],[data-theme="lavender"]) .toggle {
  background: transparent; color: var(--muted); border-color: var(--line);
  padding: 0.3rem 0.7rem; font-size: 0.8rem;
}
:is([data-theme="rose"],[data-theme="lavender"]) a { color: var(--accent); }

/* --- Lavender: palette only ----------------------------------------------- */

:root[data-theme="lavender"] {
  --ground: #faf9fc;          /* cool, where rose is warm */
  --ink:    #37313f;
  --muted:  #7f7689;
  --line:   #e7e2ee;
  --bad:    #a8474a;
  --accent: #8f76b5;
  --soft:   #f2eef8;
}

/* The switcher is preview furniture, never part of a theme. */
:is([data-theme="rose"],[data-theme="lavender"]) #themePicker { background: var(--ground); }

/* ==========================================================================
 * Letter
 *
 * A sheet of paper on a desk, out of its envelope. No photograph, no motion
 * beyond the one moment on arrival — the register of a printed card, which is
 * the thing this whole site replaces.
 *
 * Its palette is deliberately nothing like Rose's: putty and ecru against
 * antique gold. Another warm pink here and the couple would be shown the garden
 * three times and asked to pick a direction.
 * ========================================================================== */

:root[data-theme="letter"] {
  --ground: #ded5c6;          /* the desk */
  --paper:  #fbf7ef;
  --flap:   #efe8da;
  --ink:    #2c2721;
  --muted:  #7d7365;
  --line:   #e3dbc9;
  --bad:    #8f3b36;
  --accent: #9a7b3f;          /* antique gold, used sparingly and never filled */

  /* The sheet's width, and the ONLY place it is decided. The height is derived
     from it, so both must resolve against the same axis — hence 100vw and not
     100%: a percentage inside a height calc would resolve against the
     container's HEIGHT and silently give a different number. */
  --sheet-w: min(24rem, calc(100vw - 2.75rem), 54vh);
}

[data-theme="letter"] body {
  background: var(--ground);
  color: var(--ink);
  font-family: 'EB Garamond', ui-serif, Georgia, serif;
  font-weight: 500;
  font-size: 1.08rem;
  padding: 0 0 2.5rem;
}

/* Amiri throughout: the classical naskh an actual printed Arabic invitation is
   set in. Garden uses a calligraphic hand for the names; this one does not,
   because a printed card and a hand-written one are different registers. */
[data-theme="letter"][dir="rtl"],
[data-theme="letter"] [dir="rtl"],
[data-theme="letter"] [lang="ar"] { font-family: 'Amiri', serif; }

/* --- the scene -----------------------------------------------------------

   The desk does not move. The envelope and the sheet are fixed to the viewport
   and only the invitation scrolls, inside the sheet.

   COST, accepted deliberately: a phone's URL bar collapses on PAGE scroll, and
   there is no page scroll here, so that strip stays visible the whole time.
   ------------------------------------------------------------------------- */

[data-theme="letter"] body { overflow: hidden; height: 100dvh; }

[data-theme="letter"] #letterhead {
  position: fixed; inset: 0;
  display: grid; place-items: center;
  pointer-events: none; z-index: 0;
}

/* Exactly the sheet's box. Everything on the desk is positioned against this,
   so nothing drifts when the sheet resizes. */
[data-theme="letter"] .scene {
  position: relative;
  width: var(--sheet-w);
  height: calc(var(--sheet-w) * 1.414);
}

[data-theme="letter"] .scene .envelope {
  position: absolute; left: 50%; top: 50%;
  width: 112%; height: auto;
  /* Lying underneath and askew, rather than a border drawn around the paper. */
  transform: translate(-50%, -48%) rotate(-6.5deg);
  /* drop-shadow follows the SHAPE — a box-shadow here would draw a rectangle
     around the flap's diagonal and give the whole thing away. */
  filter: drop-shadow(0 8px 16px rgba(60, 48, 28, 0.22));
}
#letterhead .body { fill: var(--flap); }
#letterhead .flap { fill: var(--paper); }
#letterhead .fold { fill: none; stroke: var(--accent); stroke-opacity: 0.4; stroke-width: 2.5; }
#letterhead .seam { fill: none; stroke: var(--accent); stroke-opacity: 0.16; stroke-width: 2; }

/* Below the sheet, never under it. */
[data-theme="letter"] .scene .pen {
  position: absolute; left: 4%; bottom: -15%;
  width: 66%; height: auto;
  transform: rotate(-7deg);
  filter: drop-shadow(0 4px 7px rgba(60, 48, 28, 0.28));
}
#letterhead .nib    { fill: var(--accent); }
#letterhead .slit   { stroke: var(--paper); stroke-width: 2.5; stroke-opacity: 0.55; }
#letterhead .grip   { fill: #2f2a24; }
#letterhead .band   { fill: var(--accent); }
#letterhead .barrel { fill: #1f1b17; }
#letterhead .clip   { fill: var(--accent); opacity: 0.85; }

/* Moved out of the letter and onto the desk, just above the paper's top edge.
   #letterhead ignores pointer events so the scene never blocks the sheet; the
   toggle has to opt back in. */
[data-theme="letter"] .scene .toggle {
  position: absolute;
  /* The shorthand, not just `bottom` and `left`. The base rule already sets
     `top` and `inset-inline-end`, and leaving those in place over-constrains
     the box on both axes — the same failure that made the sheet endless. All
     four offsets have to be restated here. */
  inset: auto auto calc(100% + 0.55rem) 50%;
  transform: translateX(-50%);
  pointer-events: auto;
  margin: 0; white-space: nowrap;
}

/* --- the sheet ------------------------------------------------------------ */

[data-theme="letter"] main {
  /* inset + margin:auto centres without a transform — a transformed scroll
     container renders blurry text in some browsers. */
  position: fixed; inset: 0; margin: auto;
  /* A4 proportions, stated EXPLICITLY rather than via aspect-ratio.
     `inset: 0` sets top AND bottom, which over-constrains an absolutely
     positioned box: the height is then solved from those offsets or from
     content, and aspect-ratio never gets a say. The symptom was an endless
     sheet that did not scroll. With a definite height, `margin: auto` centres
     it on both axes, which is what was wanted all along.

     The 54vh cap on the width keeps the derived height on screen: 54 x 1.414
     is about 76vh, so it fits in landscape too. */
  width: var(--sheet-w);
  height: calc(var(--sheet-w) * 1.414);
  max-width: none;

  overflow-y: auto;
  overscroll-behavior: contain;          /* no rubber-banding the page behind */
  -webkit-overflow-scrolling: touch;

  background: var(--paper);
  /* No bottom padding: the scroll hint is the last block and provides it, so
     that `bottom: 0` can reach the panel's real bottom edge. */
  padding: 1.7rem 1.4rem 0;
  z-index: 1;
  border: 1px solid rgba(120,100,60,0.16);
  /* Lifted off the desk: a tight contact shadow plus a soft cast one. */
  box-shadow: 0 1px 2px rgba(60,48,28,0.18), 0 16px 34px rgba(60,48,28,0.20);
}

/* The switcher would otherwise sit on top of the scene. */
[data-theme="letter"] body:has(#themePicker) main,
[data-theme="letter"] body:has(#themePicker) #letterhead { inset: 0 0 3.4rem 0; }

/* The hint that there is more on the sheet. Sticky, so it rides the bottom
   edge of the paper while anything is still below it; letter.js hides it once
   the end is reached, because a sticky arrow at the end of a scroll just sits
   there pointing at nothing. */
[data-theme="letter"] .more {
  position: sticky; bottom: 0; z-index: 3;
  margin: 0 -1.4rem; padding: 2rem 0 1.3rem;
  transition: opacity 0.25s ease;
  text-align: center; pointer-events: none;
  color: var(--accent);
  background: linear-gradient(to bottom, rgba(251,247,239,0) 0%, var(--paper) 62%);
}
[data-theme="letter"] .more svg { width: 1.15rem; height: auto; display: inline-block; }
@media (prefers-reduced-motion: no-preference) {
  [data-theme="letter"] .more svg { animation: moreNudge 2.4s ease-in-out infinite; }
}
@keyframes moreNudge {
  0%, 62%, 100% { transform: translateY(0); opacity: 0.75; }
  80%           { transform: translateY(3px); opacity: 1; }
}

/* --- the invitation itself ------------------------------------------------ */

[data-theme="letter"] .hero { padding: 0 0 0.25rem; }

[data-theme="letter"] .families p {
  font-size: 0.95rem; color: var(--ink); line-height: 1.5;
}
[data-theme="letter"] .families span {
  font-size: 0.8rem; color: var(--muted); margin-top: 0.1rem;
}

[data-theme="letter"] .honour {
  color: var(--muted); font-size: 0.95rem; margin: 1.2rem 0 1.1rem;
  position: relative; padding: 1.1rem 0;
}
/* Two hairlines rather than a box: engraved cards rule, they do not frame. */
[data-theme="letter"] .honour::before,
[data-theme="letter"] .honour::after {
  content: ''; position: absolute; inset-inline: 22%; height: 1px;
  background: var(--accent); opacity: 0.35;
}
[data-theme="letter"] .honour::before { top: 0; }
[data-theme="letter"] .honour::after { bottom: 0; }

[data-theme="letter"] .rel {
  font-size: 0.72rem; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--muted);
}
[data-theme="letter"] [dir="rtl"] .rel { text-transform: none; letter-spacing: 0; font-size: 0.85rem; }

/* Cormorant only at display size. It is a display face — the same delicacy that
   made it too thin for body text is exactly what it is for at 2.5rem. */
[data-theme="letter"] .couple .names {
  font-family: 'Cormorant', 'EB Garamond', ui-serif, Georgia, serif;
  font-size: clamp(1.9rem, 9vw, 2.8rem); font-weight: 600;
  color: var(--ink); line-height: 1.2; margin-top: 0.2rem;
}
[data-theme="letter"][dir="rtl"] .couple .names,
[data-theme="letter"] [dir="rtl"] .couple .names {
  font-family: 'Amiri', serif; font-weight: 700; line-height: 1.5;
}

[data-theme="letter"] .when { color: var(--ink); font-size: 1rem; margin-top: 0.4rem; }
[data-theme="letter"] .when:first-of-type { margin-top: 1.5rem; }

[data-theme="letter"] .count { gap: 1.2rem; margin: 1.4rem 0; }
[data-theme="letter"] .count b {
  font-size: 1.4rem; font-weight: 400; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
[data-theme="letter"] .count span {
  font-size: 0.66rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--muted);
}
[data-theme="letter"] [dir="rtl"] .count span { text-transform: none; letter-spacing: 0; font-size: 0.8rem; }

/* Ruled top and bottom, not boxed — the same gesture as the honour line. */
[data-theme="letter"] .alloc {
  border: 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  background: none; padding: 0.95rem 0; margin: 1.4rem 0; text-align: center;
}
[data-theme="letter"] .nochildren {
  color: var(--muted); font-size: 0.9rem; margin-top: 0.55rem; font-style: italic;
}
[data-theme="letter"] [dir="rtl"] .nochildren { font-style: normal; }

[data-theme="letter"] h2 {
  font-size: 0.74rem; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--accent); font-weight: 600; margin: 2rem 0 0.6rem;
}
[data-theme="letter"] [dir="rtl"] h2 {
  text-transform: none; letter-spacing: 0; font-size: 1.05rem;
}

[data-theme="letter"] select {
  border: 1px solid var(--line); background: #fff; border-radius: 0;
  padding: 0.6rem; font-family: inherit; font-size: 1rem;
}
/* Outlined, never filled. A solid gold button would look like a coupon. */
[data-theme="letter"] button {
  font-family: inherit; font-size: 0.9rem; border-radius: 0;
  border: 1px solid var(--accent); background: transparent; color: var(--ink);
  padding: 0.65rem 1.5rem; margin-top: 0.75rem;
  letter-spacing: 0.06em;
}
[data-theme="letter"] button[value="coming"] { background: var(--accent); color: var(--paper); }
[data-theme="letter"] .toggle {
  border-color: var(--line); color: var(--muted);
  padding: 0.3rem 0.7rem; font-size: 0.78rem; letter-spacing: 0;
}
[data-theme="letter"] a { color: var(--accent); }

/* ==========================================================================
 * Frame
 *
 * The photograph fixed behind everything, the invitation on a translucent
 * panel inside the gold frame. Like Letter, the scene does not move and only
 * the invitation scrolls.
 *
 * The whole geometry comes from ONE number — the displayed width of the
 * photograph — so the panel and the frame it sits in can never drift apart.
 * The interior was measured off the 900x1600 original: left 17.778%, top
 * 29.375%, 58.889% wide, 54.375% tall.
 * ========================================================================== */

:root[data-theme="frame"] {
  --ground: #34240f;          /* for screens wider than the photograph */

  /* A DARK scrim, not a light one.
   *
   * Mid-tone text fails on either: gold sits in the middle of the range
   * whichever direction the panel goes. The difference is the escape route. On
   * a light panel the accent has to become a dark bronze to be readable; on a
   * dark one it can be pale champagne, which still reads as gold and is the
   * register a sunset-and-candles photograph actually wants.
   *
   * Every value below is measured against the panel over the LIGHTEST part of
   * the photograph — the sky — because that is where light text comes closest
   * to its ground. Over the greenery everything is far clearer.
   */
  /* Dark amber rather than near-black. Warming the base moves it closer to
     the light text, so the scrim has to be denser to keep the same margin:
     this hue needs 0.76 where near-black passed at 0.64. Anything warmer
     again — bronze, orange-brown — needs 0.82+ and stops showing the
     photograph at all, which defeats the point of a scrim. */
  --panel:  rgba(61, 42, 18, 0.76);
  --ink:    #f6efe2;          /* 6.7:1 at worst */
  --muted:  #dbceb8;          /* 4.9:1 at worst */
  --line:   rgba(240, 226, 200, 0.24);
  --bad:    #f0a8a0;
  --accent:     #e2c489;      /* rules, borders, the frame's own furniture */
  --accent-ink: #e8cf9c;      /* 5.0:1 at worst — champagne, and readable */
  --accent-glow: none;        /* light on dark needs no halo */

  /* THE WHOLE PHOTOGRAPH, never a crop. Sized to fit inside the viewport on
     both axes, so every screen sees the same picture — zooming and cropping is
     what made the phone and the laptop show different scenes. 0.5626 is the
     image's 724:1287 aspect. */
  --img-w: min(100vw, calc(var(--fit-h) * 0.5626));
  --img-h: calc(var(--img-w) / 0.5626);
  --fit-h: 100dvh;

  /* CALIBRATION — the gold frame's interior, as fractions of the photograph.
     Measured off the 724x1287 original: left 92, top 168, right 638, bottom
     1065. These four numbers are the only thing to touch if the panel sits
     wrong; everything else is derived from them.

     PLAIN NUMBERS, not percentages. calc() only allows multiplication when one
     side is a <number>, so `12.7% * 393px` is a type error — and an invalid
     calc is dropped silently, which left the panel with no position and no
     size at all. */
  --fx-left: 0.127;
  /* Calibrated by eye against the real photograph, in steps: down 0.02, down
     0.03 (each time reducing --fx-h by the same amount so the bottom edge held
     still), then the whole panel SHIFTED up 0.03 with --fx-h left alone.
     Move the top alone to slide the panel; move top and height together to
     hold one edge. */
  --fx-top:  0.1505;
  --fx-w:    0.754;
  --fx-h:    0.647;
  --fx-pad:  0.55rem;
  /* The toggle's own Y, deliberately NOT --fx-top: it belongs to the frame's
     top bar, not to the panel, so calibrating the panel's edge does not drag
     the button with it. Moved up 0.03 alongside the panel this round. */
  --fx-toggle-y: 0.1205;
}

[data-theme="frame"] body {
  background: var(--ground);
  color: var(--ink);
  font-family: 'EB Garamond', ui-serif, Georgia, serif;
  font-weight: 500;
  overflow: hidden; height: 100dvh;
  padding: 0;
}
[data-theme="frame"][dir="rtl"],
[data-theme="frame"] [dir="rtl"],
[data-theme="frame"] [lang="ar"] { font-family: 'Noto Naskh Arabic', serif; }

[data-theme="frame"] #framehead {
  position: fixed; top: 0; left: 0;
  /* An EXPLICIT height, not `inset: 0`. With inset the box took its height from
     the fixed containing block, which is a second and independent idea of how
     tall the screen is — the panel was positioned from --fit-h while the
     photograph was centred in something else. One number, used by both. */
  width: 100vw; height: var(--fit-h);
  display: grid; place-items: center;
  pointer-events: none; z-index: 0; overflow: hidden;
}
[data-theme="frame"] #framehead img {
  width: var(--img-w); height: var(--img-h);
  max-width: none; object-fit: cover; display: block;
}

/* The panel. Positioned from the photograph's own dimensions: half the image
   back from centre, then in by the interior's offset, then inset for tolerance. */
[data-theme="frame"] main {
  position: fixed;

  /* Fallbacks FIRST. An invalid calc is dropped on its own, so if anything
     below ever goes wrong again the invitation still lands somewhere readable
     instead of disappearing entirely. */
  left: 8vw; top: 14vh; width: 84vw; height: 62vh;

  /* The photograph's own top-left corner, then in by the frame's offsets. */
  left:   calc((100vw - var(--img-w)) / 2 + var(--fx-left) * var(--img-w) + var(--fx-pad));
  top:    calc((var(--fit-h) - var(--img-h)) / 2 + var(--fx-top) * var(--img-h) + var(--fx-pad));
  width:  calc(var(--fx-w) * var(--img-w) - 2 * var(--fx-pad));
  height: calc(var(--fx-h) * var(--img-h) - 2 * var(--fx-pad));
  max-width: none; margin: 0;

  overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;

  background: var(--panel);
  border: 1px solid rgba(226, 196, 137, 0.30);
  box-shadow: 0 2px 18px rgba(0, 0, 0, 0.28);
  padding: 1.15rem 1rem 0;
  z-index: 1;
  text-align: center;
}

/* The switcher sits over the scene, so the whole thing lifts to clear it. */
/* The switcher sits over the scene, so the usable height shrinks and everything
   derived from it follows.

   Declared on :root, NOT on body. --img-w and --img-h are computed on :root, so
   an override further down the tree never reaches them: the photograph was
   being sized from 100dvh while the panel was positioned from 100dvh minus the
   switcher. Both now read the same value. */
:root[data-theme="frame"]:has(#themePicker) {
  --fit-h: calc(100dvh - 3.4rem);
}

/* Out of the panel entirely, onto the photograph above the frame. Floated
   inside it, the text wrapped around it and shoved the bride's family sideways;
   on its own line it ate scarce height in an already small panel. */
[data-theme="frame"] #framehead .toggle {
  position: absolute;
  /* Right edge in both languages — `right`, not `inset-inline-end`, so Arabic
     does not flip it to the other side. The right margin of the frame is
     whatever the left offset and the width leave over. */
  inset: calc((var(--fit-h) - var(--img-h)) / 2 + var(--fx-toggle-y) * var(--img-h) - 1.95rem)
         calc((100vw - var(--img-w)) / 2
              + (1 - var(--fx-left) - var(--fx-w)) * var(--img-w) + var(--fx-pad))
         auto auto;
  pointer-events: auto; margin: 0; white-space: nowrap;
  /* The same scrim as the panel. It was a white chip carrying color: var(--ink)
     — which became ivory the moment the panel went dark, leaving ivory text on
     white. Taking both from the theme's own tokens means it cannot drift out of
     step with the panel again. */
  background: var(--panel);
  border: 1px solid rgba(226, 196, 137, 0.35);
  color: var(--ink);
  padding: 0.22rem 0.6rem; font-size: 0.72rem;
}

/* --- the invitation on the panel ------------------------------------------ */

[data-theme="frame"] .hero { padding: 0; }
[data-theme="frame"] .families p { font-size: 0.8rem; color: var(--ink); line-height: 1.4; }
[data-theme="frame"] .families span { font-size: 0.7rem; color: var(--muted); }
[data-theme="frame"] .honour {
  color: var(--muted); font-size: 0.82rem; margin: 0.8rem 0 0.7rem; font-style: italic;
}
[data-theme="frame"] [dir="rtl"] .honour { font-style: normal; }
[data-theme="frame"] .rel {
  font-size: 0.62rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--accent-ink); text-shadow: var(--accent-glow);
}
[data-theme="frame"] [dir="rtl"] .rel { text-transform: none; letter-spacing: 0; font-size: 0.75rem; }

/* Aref Ruqaa echoes the calligraphy in the photograph behind it. */
[data-theme="frame"] .couple .names {
  font-size: clamp(1.3rem, 6.5vw, 1.9rem); font-weight: 600; line-height: 1.2;
}
[data-theme="frame"][dir="rtl"] .couple .names,
[data-theme="frame"] [dir="rtl"] .couple .names {
  font-family: 'Aref Ruqaa', serif; line-height: 1.6;
}

[data-theme="frame"] .when { color: var(--ink); font-size: 0.86rem; margin-top: 0.3rem; }
[data-theme="frame"] .when:first-of-type { margin-top: 0.9rem; }

[data-theme="frame"] .count { gap: 0.9rem; margin: 1rem 0; }
/* Bigger as well as gold: at 1.4rem these numerals are large text, where 3:1
   is the bar rather than 4.5:1 — so the size is doing real work here, not
   just the shadow. */
[data-theme="frame"] .count b { font-size: 1.4rem; font-weight: 500;
  color: var(--accent-ink); text-shadow: var(--accent-glow);
  font-variant-numeric: tabular-nums; }
[data-theme="frame"] .count span { font-size: 0.58rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--muted); }
[data-theme="frame"] [dir="rtl"] .count span { text-transform: none; letter-spacing: 0; font-size: 0.7rem; }

[data-theme="frame"] .alloc {
  border: 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  background: none; padding: 0.7rem 0; margin: 1rem 0; font-size: 0.85rem;
}
[data-theme="frame"] .nochildren { color: var(--muted); font-size: 0.78rem; margin-top: 0.4rem; }

[data-theme="frame"] h2 {
  font-size: 0.62rem; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--accent-ink); text-shadow: var(--accent-glow);
  font-weight: 600; margin: 1.3rem 0 0.4rem;
}
[data-theme="frame"] [dir="rtl"] h2 { text-transform: none; letter-spacing: 0; font-size: 0.85rem; }

[data-theme="frame"] label { font-size: 0.82rem; }
[data-theme="frame"] select { margin-inline: auto; border-radius: 0; font-size: 0.9rem;
  padding: 0.4rem; border: 1px solid var(--line);
  background: rgba(12, 9, 6, 0.55); color: var(--ink); }
[data-theme="frame"] button {
  font-family: inherit; font-size: 0.8rem; border-radius: 0;
  border: 1px solid var(--accent); background: transparent; color: var(--accent-ink);
  padding: 0.45rem 1rem; margin-top: 0.5rem;
}
/* Dark text on the champagne fill: 9:1, and the only strong shape on a dark
   panel, which is what a primary action should be. */
[data-theme="frame"] button[value="coming"] { background: var(--accent); color: #241a10; }
[data-theme="frame"] a { color: var(--accent-ink); text-shadow: var(--accent-glow); }

[data-theme="frame"] .more {
  position: sticky; bottom: 0; z-index: 3;
  margin: 0 -1rem; padding: 1.5rem 0 1rem;
  transition: opacity 0.25s ease;
  text-align: center; pointer-events: none; color: var(--accent-ink);
  background: linear-gradient(to bottom, rgba(61,42,18,0) 0%, rgba(61,42,18,0.88) 62%);
}
[data-theme="frame"] .more svg { width: 1rem; height: auto; display: inline-block; }
@media (prefers-reduced-motion: no-preference) {
  [data-theme="frame"] .more svg { animation: moreNudge 2.4s ease-in-out infinite; }
}

/* ==========================================================================
 * The holding page (no ?c=)
 *
 * It already gets the palette and the scenery — applyTheme and dressFor both
 * run before the page branches. What it did NOT get was the names, for two
 * reasons worth remembering:
 *
 *   every theme styles `.couple .names`, and here the names are direct
 *   children of .hero with no .couple wrapper;
 *
 *   and the Arabic rule is `[dir=rtl] .names`, which needs a PARENT carrying
 *   dir — but here each name carries its own, so the selector needs to be
 *   `.names[dir=rtl]` instead.
 * ========================================================================== */

:is([data-theme="rose"],[data-theme="lavender"]) .hero > .names {
  font-size: clamp(1.8rem, 8.5vw, 2.5rem); font-weight: 600;
  color: var(--ink); line-height: 1.3;
}
:is([data-theme="rose"],[data-theme="lavender"]) .hero > .names[dir="rtl"] {
  font-family: 'Aref Ruqaa', 'Amiri', serif; line-height: 1.8;
}

[data-theme="letter"] .hero > .names {
  font-family: 'Cormorant', 'EB Garamond', ui-serif, Georgia, serif;
  font-size: clamp(1.9rem, 9vw, 2.7rem); font-weight: 600;
  color: var(--ink); line-height: 1.25;
}
[data-theme="letter"] .hero > .names[dir="rtl"] {
  font-family: 'Amiri', serif; font-weight: 700; line-height: 1.6;
}

/* Smaller: the panel is roughly half the reading area of the other themes. */
[data-theme="frame"] .hero > .names {
  font-size: clamp(1.4rem, 7vw, 2rem); font-weight: 600;
  color: var(--ink); line-height: 1.25;
}
[data-theme="frame"] .hero > .names[dir="rtl"] {
  font-family: 'Aref Ruqaa', serif; line-height: 1.7;
}

/* 2rem of rule margin is a lot inside a small fixed panel. */
[data-theme="frame"] hr { margin: 1.1rem 0; border-color: var(--line); }
[data-theme="letter"] hr { margin: 1.6rem 0; border-color: var(--line); }

/* Holding page only: centre the content in the panel.
 *
 * Scoped on [data-page="holding"], so the invitation — which is taller than its
 * panel and must start at the top — is untouched.
 *
 * `margin-block: auto` on the child, NOT `justify-content: center` on the
 * container. They look identical while the content fits, but a centred flex
 * container makes overflowing content unreachable above the scroll origin;
 * auto margins simply collapse to zero and it scrolls normally. A phone turned
 * sideways is enough to hit that.
 */
[data-page="holding"]:is([data-theme="letter"],[data-theme="frame"]) main {
  display: flex; flex-direction: column;
}
[data-page="holding"]:is([data-theme="letter"],[data-theme="frame"]) main > .hero {
  margin-block: auto; width: 100%;
}

/* Holding page, gardens only: fill the window.
 *
 * The invitation is taller than the viewport, so the pool naturally lands at
 * the foot of the page. The holding page is short — names, a line, two
 * countdowns — so the document ended around 75% of a laptop screen and the
 * pool ended with it, while the petal canvas is fixed to the whole window and
 * carried on falling below it.
 *
 * Scoped on [data-page="holding"]: the invitation must not be forced to a
 * minimum height it does not need.
 */
[data-page="holding"]:is([data-theme="rose"],[data-theme="lavender"]) body {
  /* Padding removed rather than subtracted from the height below — leaving it
     in would push the page just past the viewport and add a scrollbar to a
     page with nothing to scroll. */
  padding: 0;
  min-height: 100dvh;
  display: flex; flex-direction: column;
}
[data-page="holding"]:is([data-theme="rose"],[data-theme="lavender"]) main {
  flex: 1; display: flex; flex-direction: column;
}
/* Auto margins above AND below: the free space splits evenly, which centres the
   names and puts the pool on the bottom edge in one move. */
[data-page="holding"]:is([data-theme="rose"],[data-theme="lavender"]) main > .hero {
  margin-block: auto;
}
/* The switcher is fixed over the page, so the pool needs to clear it — ONCE.
 *
 * base.css already pads the body by 4rem via `body:has(#themePicker)`, and
 * `:has()` carries the specificity of its argument — an id — making that rule
 * (1,0,1). The `padding: 0` above is only (0,2,1), and no number of attribute
 * selectors ever outranks an id, so the body kept its 4rem and the 3.4rem below
 * was added on top of it: 7.4rem of nothing under the pool.
 *
 * Restating `:has(#themePicker)` here brings this to (1,2,1), which wins.
 */
[data-page="holding"]:is([data-theme="rose"],[data-theme="lavender"]) body:has(#themePicker) {
  padding: 0;
}
[data-page="holding"]:is([data-theme="rose"],[data-theme="lavender"]) body:has(#themePicker) main {
  padding-bottom: 3.4rem;
}
