/* ════════════════════════════════════════════════════════════════════════════
   Pankhuri & Tanmay — Save the Date
   Stage one: the card puzzle.  Plain CSS, no preprocessor, no framework.
   ════════════════════════════════════════════════════════════════════════════ */

/* ── Palette & type ───────────────────────────────────────────────────────── */
:root {
  --ink:        #2c2622;
  --ink-soft:   #6b6058;
  --ink-faint:  #a4988c;
  --paper:      #f7f2ea;
  --paper-warm: #efe6d9;
  --gold:       #b08d57;
  --gold-soft:  #d8c39b;
  --blush:      #d9a89a;
  --sage:       #7d8f7b;

  --display: 'Cormorant Garamond', 'Cormorant', Garamond, Georgia, 'Times New Roman', serif;
  --ui:      'Jost', 'Futura', 'Avenir Next', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  /* The Devanagari half of the save-the-date. Tiro is cut with the same
     old-style pen and stroke contrast as the Garamond, which is as close as
     the two scripts get to being the same typeface. */
  --devanagari: 'Tiro Devanagari Hindi', 'Noto Serif Devanagari', 'Martel', Georgia, serif;
  /* The one line on the page that gets to be handwriting. Pinyon is an
     engraved copperplate rather than a marker-pen script, which is what keeps
     it on the same piece of stationery as the Garamond. */
  --script: 'Pinyon Script', 'Snell Roundhand', 'Apple Chancery', cursive;

  /* Two rows of cards (frames + hand) have to share the height with the
     masthead and the buttons, so the card width is capped by the viewport
     height as well as its width — the whole board fits without scrolling.
     The 312px is everything that isn't cards (masthead + feedback line +
     buttons + padding + board gap); keep it in sync if those change height. */
  --card-w: min(clamp(118px, 17vw, 300px), calc((100vh - 312px) / 3));
  /* The card's own layout size. On desktop a card and a frame are the same
     size; on phones they part company — see the mobile block. */
  --card-base: var(--card-w);
  --card-r: 10px;
  --gap:    clamp(12px, 1.6vw, 22px);

  --ease: cubic-bezier(.22, .82, .28, 1);
  --shadow-rest: 0 2px 4px rgba(44, 38, 34, .12), 0 16px 34px -18px rgba(44, 38, 34, .55);
  --shadow-hover: 0 5px 12px rgba(44, 38, 34, .14), 0 28px 52px -22px rgba(44, 38, 34, .58);
  --shadow-lift: 0 8px 18px rgba(44, 38, 34, .16), 0 44px 78px -28px rgba(44, 38, 34, .62);
}

@supports (height: 100dvh) {
  :root { --card-w: min(clamp(118px, 17vw, 300px), calc((100dvh - 312px) / 3)); }
}

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

html, body { height: 100%; }

body {
  margin: 0;
  font-family: var(--ui);
  font-weight: 300;
  color: var(--ink);
  background:
    radial-gradient(120% 90% at 50% -10%, #fffdf9 0%, var(--paper) 42%, var(--paper-warm) 100%);
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

/* The scenery canvas: full-bleed, behind everything, never interactive. */
#scenery {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
  display: block;
}

/* A whisper of paper texture over everything. */
.paper-grain {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 999;
  opacity: .5;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E");
}

/* ── Sound toggle ─────────────────────────────────────────────────────────────
   Tucked into a corner, quiet enough to ignore and obvious enough to find. It
   has to read against paper, dusk, a night sky and a sunset print, so it's a
   translucent disc that borrows whatever is behind it. */
.sound-toggle {
  position: fixed;
  right: clamp(14px, 2.2vw, 26px);
  bottom: clamp(14px, 2.2vw, 26px);
  z-index: 900;
  width: 40px;
  height: 40px;
  padding: 0;
  display: grid;
  place-items: center;
  border: 1px solid rgba(107, 96, 88, .28);
  border-radius: 50%;
  background: rgba(255, 253, 249, .62);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  color: var(--ink-soft);
  cursor: pointer;
  opacity: .62;
  transition: opacity .3s var(--ease), color .3s var(--ease),
              background .3s var(--ease), border-color .3s var(--ease);
}
.sound-toggle:hover { opacity: 1; color: var(--ink); border-color: var(--gold); }
.sound-toggle:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; opacity: 1; }
.sound-toggle[hidden] { display: none; }

.sound-toggle svg {
  width: 21px;
  height: 21px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.sound-toggle__body { fill: currentColor; stroke: none; }
.sound-toggle__slash { opacity: 0; transition: opacity .2s var(--ease); }
.sound-toggle__waves { opacity: 1; transition: opacity .2s var(--ease); }
.sound-toggle.is-off .sound-toggle__waves { opacity: 0; }
.sound-toggle.is-off .sound-toggle__slash { opacity: 1; }

/* On a night sky, or over the darkened print on the reveal, invert it the same
   way everything else does. (`data-surface="dark"` is set by puzzle.js — the
   toggle lives outside #app, so no sibling selector can reach it.) */
:root[data-night] .sound-toggle,
:root[data-surface="dark"] .sound-toggle {
  background: rgba(255, 253, 249, .10);
  border-color: rgba(255, 253, 249, .34);
  color: #f0e8db;
}

/* ── Language toggle ──────────────────────────────────────────────────────────
   English / हिन्दी, in the corner opposite the sound toggle so the two balance
   the frame instead of stacking. Both labels stay on screen — a switch that
   shows only the language you're *not* reading makes you work out which way it
   points. The one you're in is lit; the other waits.

   It belongs to the save-the-date, so it's `hidden` until the reveal and fades
   in a frame later (`is-ready`), which is what the transition runs from. */
.lang-toggle {
  position: fixed;
  right: clamp(14px, 2.2vw, 26px);
  top: clamp(14px, 2.2vw, 26px);
  z-index: 900;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 15px 8px;
  border: 1px solid rgba(107, 96, 88, .28);
  border-radius: 999px;
  background: rgba(255, 253, 249, .62);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  font-family: var(--display);
  font-size: 14.5px;
  line-height: 1;
  color: var(--ink-soft);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity .8s var(--ease), color .3s var(--ease),
              background .3s var(--ease), border-color .3s var(--ease);
}
.lang-toggle[hidden] { display: none; }
.lang-toggle.is-ready { opacity: .78; pointer-events: auto; }
.lang-toggle.is-ready:hover { opacity: 1; border-color: var(--gold); }
.lang-toggle:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; opacity: 1; }

.lang-toggle__opt {
  opacity: .45;
  letter-spacing: .08em;
  transition: opacity .5s var(--ease), color .5s var(--ease);
}
.lang-toggle__opt.is-on { opacity: 1; color: var(--gold); }
/* Devanagari sits taller in its em than the Garamond does, so it comes down a
   step to keep the two labels optically the same size. */
.lang-toggle__opt--hi {
  font-family: var(--devanagari);
  font-size: .9em;
  letter-spacing: .02em;
}

/* The sound toggle sits at the bottom of the print, where it's dark. This one
   sits at the top, where the sky is at its brightest — so it can't be a light
   wash on light, it has to bring its own dusk with it. */
:root[data-night] .lang-toggle,
:root[data-surface="dark"] .lang-toggle {
  background: rgba(26, 15, 8, .40);
  border-color: rgba(250, 240, 224, .40);
  color: #f4ece0;
  text-shadow: 0 1px 2px rgba(16, 8, 3, .6);
}
:root[data-night] .lang-toggle.is-ready,
:root[data-surface="dark"] .lang-toggle.is-ready { opacity: .88; }
:root[data-surface="dark"] .lang-toggle__opt.is-on { color: var(--gold-soft); }

/* ── Stage machinery ──────────────────────────────────────────────────────── */
#app { position: relative; z-index: 1; height: 100%; height: 100dvh; }

.stage {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overflow-x: hidden;
  display: grid;
  align-content: center;
  align-content: safe center;   /* never clip the top when it does overflow */
  justify-items: center;
  padding: 24px clamp(16px, 3vw, 40px);
  opacity: 0;
  visibility: hidden;
  transform: scale(.985);
  transition: opacity .85s var(--ease), transform 1.1s var(--ease), visibility .85s;
}
.stage.is-active { opacity: 1; visibility: visible; transform: none; }
.stage.is-leaving { opacity: 0; transform: scale(1.02); filter: blur(3px); }

.stage__inner { width: 100%; max-width: 1340px; text-align: center; }

/* Nothing on the puzzle screen is meant to be selected — a drag that starts
   just off a card shouldn't paint the headline blue. */
#stage-puzzle { -webkit-user-select: none; user-select: none; }

/* ── Type over scenery ────────────────────────────────────────────────────────
   Only for scenes that repaint the sky (`data-sky`). The toy mouse runs across
   plain paper, so the type is left alone — dressing it for weather that isn't
   there just makes the page look muddy. */
:root[data-sky] .eyebrow,
:root[data-sky] .display,
:root[data-sky] .lede,
:root[data-sky] .feedback {
  text-shadow:
    0 0 14px rgba(247, 242, 234, .95),
    0 0 34px rgba(247, 242, 234, .8),
    0 1px 2px rgba(247, 242, 234, .9);
}
:root[data-sky] .lede { color: var(--ink); }
/* Gold on a night sky is the one colour that needs deepening, not lightening —
   it sits on the paper pool, so it wants more contrast against it. */
:root[data-sky] .eyebrow { color: #8a6835; }
/* Frames turn into translucent windows so the weather shows through them. */
:root[data-sky] .slot {
  background: rgba(255, 253, 249, .34);
  border-color: rgba(255, 253, 249, .72);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
}
:root[data-sky] .btn--ghost {
  background: rgba(255, 253, 249, .72);
  border-color: rgba(107, 96, 88, .42);
}
/* ── Night ────────────────────────────────────────────────────────────────────
   Fireworks black out the sky completely — no paper pools to sit the type on —
   so the page inverts instead: white words, outlined frames, a light primary
   button. Everything here is the mirror of the `data-sky` block above. */
:root[data-night] .display,
:root[data-night] .lede,
:root[data-night] .feedback {
  color: #f7f2ea;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .55), 0 0 26px rgba(0, 0, 0, .45);
}
:root[data-night] .feedback em { color: #fffdf9; }
:root[data-night] .eyebrow {
  color: var(--gold-soft);
  text-shadow: 0 1px 3px rgba(0, 0, 0, .5);
}

:root[data-night] .slot {
  background: rgba(255, 253, 249, .05);
  border-color: rgba(255, 253, 249, .40);
}
:root[data-night] .slot__num { color: rgba(255, 253, 249, .48); }
:root[data-night] .slot.is-target {
  background: rgba(216, 195, 155, .16);
  border-color: var(--gold-soft);
  box-shadow: 0 0 0 5px rgba(216, 195, 155, .14);
}

/* The primary button is normally dark-on-paper; on a black sky it has to
   become paper-on-dark or it simply disappears. */
:root[data-night] .btn--primary {
  background: var(--paper);
  border-color: var(--paper);
  color: #23201d;
}
:root[data-night] .btn--primary:hover:not(:disabled) {
  background: var(--gold-soft);
  border-color: var(--gold-soft);
  color: #23201d;
}
:root[data-night] .btn--primary:disabled { opacity: .34; }
:root[data-night] .btn--ghost {
  background: rgba(255, 253, 249, .05);
  border-color: rgba(255, 253, 249, .32);
  color: #ece5da;
}
:root[data-night] .btn--ghost:hover:not(:disabled) {
  color: #fffdf9;
  border-color: var(--gold-soft);
}

.eyebrow, .display, .lede, .feedback, .slot, .slot__num,
.btn--ghost, .btn--primary {
  transition: text-shadow .8s var(--ease), color .8s var(--ease),
              background .8s var(--ease), border-color .8s var(--ease);
}

/* ── Masthead ─────────────────────────────────────────────────────────────────
   Deliberately restrained. Every pixel spent here is a pixel the cards don't
   get — and the cards are the point. */
.masthead { margin: 0 auto 22px; max-width: 900px; }

.eyebrow {
  margin: 0 0 11px;
  font-size: clamp(10px, 1.1vw, 12px);
  letter-spacing: .42em;
  text-transform: uppercase;
  color: var(--gold);
  font-weight: 400;
}

.display {
  margin: 0;
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(24px, 2.9vw, 38px);
  line-height: 1.1;
  letter-spacing: .004em;
}
.display br { display: none; }      /* one line on desktop, two on phones */

.lede {
  margin: 14px auto 0;
  max-width: 80ch;
  /* This is the only line that tells anyone what to do, and it's competing
     with four illustrations for attention — so it's set nearly as large as
     body copy gets and a good deal darker than the usual soft ink. The
     max-width is in `ch`, which scales with the font, so the number of lines
     doesn't change when the size does. */
  font-size: clamp(14.5px, 1.35vw, 17px);
  line-height: 1.6;
  color: #574d45;
}

/* ── Board ────────────────────────────────────────────────────────────────── */
.board {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  touch-action: none;           /* dragging shouldn't scroll the page */
  transition: gap .55s var(--ease);
}

.rail {
  display: flex;
  justify-content: center;
  gap: var(--gap);
  width: 100%;
}

/* Numbered destination frames -------------------------------------------- */
.slot {
  position: relative;
  width: var(--card-w);
  aspect-ratio: 2 / 3;
  border-radius: var(--card-r);
  border: 1px dashed rgba(176, 141, 87, .55);
  background: rgba(255, 253, 249, .5);
  display: grid;
  place-items: center;
  transition: border-color .3s var(--ease), background .3s var(--ease),
              box-shadow .3s var(--ease), transform .3s var(--ease);
}

.slot__num {
  font-family: var(--display);
  font-size: calc(var(--card-w) * .26);   /* always proportionate to the frame */
  color: var(--gold-soft);
  line-height: 1;
  transition: opacity .3s var(--ease), transform .4s var(--ease);
}
.slot.is-filled .slot__num { opacity: 0; transform: scale(.8); }

.slot.is-target {
  border-color: var(--gold);
  border-style: solid;
  background: rgba(216, 195, 155, .22);
  box-shadow: 0 0 0 5px rgba(216, 195, 155, .2);
  transform: scale(1.035);
}

.slot.is-wrong { animation: shake .5s var(--ease); border-color: var(--blush); }
@keyframes shake {
  10%, 90% { transform: translateX(-3px); }
  30%, 70% { transform: translateX(5px); }
  50%      { transform: translateX(-5px); }
}

.slot.is-right { border-color: var(--sage); }

.slot.is-solved-pulse { animation: pulse .7s var(--ease); }
@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 rgba(176, 141, 87, .45); }
  100% { box-shadow: 0 0 0 26px rgba(176, 141, 87, 0); }
}

/* Resting places for un-played cards ------------------------------------- */
.rail--hand {
  gap: var(--gap);
  height: calc(var(--card-w) * 1.5);   /* explicit, so it can animate to 0 */
  overflow: hidden;
  transition: height .55s var(--ease), opacity .35s var(--ease);
}
/* Once every frame is full there's nothing left to hold — fold the row away
   so the board doesn't sit on top of a dead half-screen. Cards are positioned
   relative to the board, so nothing already placed shifts. */
.rail--hand.is-collapsed { height: 0; opacity: 0; }

/* While the cards are being shown off on a phone, the numbered frames stay out
   of it — four empty boxes beside them only splits the attention. */
.rail--slots { transition: opacity .5s var(--ease); }
.board.is-dealing .rail--slots { opacity: 0; }
.board.is-tight { gap: 0; }

.hand-anchor {
  width: var(--card-w);
  aspect-ratio: 2 / 3;
  border-radius: var(--card-r);
}

/* ── Cards ────────────────────────────────────────────────────────────────── */
.card-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 5;
}

.card {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--card-base);
  aspect-ratio: 2 / 3;
  z-index: var(--z, 1);
  pointer-events: auto;
  cursor: grab;
  transform-origin: center center;
  perspective: 1100px;
  touch-action: none;
  transition: transform .62s var(--ease), opacity .5s var(--ease);
  -webkit-user-select: none;
  user-select: none;
  will-change: transform;
}
.card:focus { outline: none; }
.card:focus-visible .card__inner { box-shadow: 0 0 0 3px var(--gold), var(--shadow-lift); }

.card.is-dragging {
  cursor: grabbing;
  z-index: 40;
  /* No transition: the transform is driven once per frame from the pointer, so
     any easing here just makes the card lag behind the finger and wobble. */
  transition: none;
}
.card.is-dragging .card__inner { box-shadow: var(--shadow-lift); }

.card.is-selected .card__inner { box-shadow: 0 0 0 2px var(--gold), var(--shadow-lift); }

.card__inner {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: var(--card-r);
  transform-style: preserve-3d;
  transform: rotateY(180deg);
  transition: transform .72s var(--ease), box-shadow .35s var(--ease);
  box-shadow: var(--shadow-rest);
}
.card.is-faceup .card__inner { transform: rotateY(0deg); }

/* Pointer devices: the card rises to meet you. Done on .card__inner because the
   outer .card's transform belongs to the drag layout. */
@media (hover: hover) {
  .card.is-faceup:hover .card__inner {
    transform: rotateY(0deg) scale(1.035);
    box-shadow: var(--shadow-hover);
    transition-duration: .3s;
  }
}

.card__face {
  position: absolute;
  inset: 0;
  border-radius: var(--card-r);
  overflow: hidden;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  background: #fffdf9;
}

/* front */
.card__art {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  pointer-events: none;
}
.card__title {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 26px 8px 11px;
  font-family: var(--display);
  font-size: clamp(13px, 1.32vw, 18px);
  letter-spacing: .04em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: #fffdf9;
  text-shadow: 0 1px 10px rgba(30, 24, 20, .55);
  background: linear-gradient(to top, rgba(32, 26, 22, .62), rgba(32, 26, 22, 0));
}
.card__frame {
  position: absolute;
  inset: 6px;
  border: 1px solid rgba(255, 253, 249, .42);
  border-radius: calc(var(--card-r) - 5px);
  pointer-events: none;
}

/* back */
.card__face--back {
  transform: rotateY(180deg);
  display: grid;
  place-items: center;
  background:
    repeating-linear-gradient(45deg, rgba(176,141,87,.10) 0 2px, transparent 2px 9px),
    repeating-linear-gradient(-45deg, rgba(176,141,87,.10) 0 2px, transparent 2px 9px),
    linear-gradient(160deg, #fffdf9, var(--paper-warm));
}
.card__monogram {
  font-family: var(--display);
  font-size: clamp(15px, 1.7vw, 20px);
  letter-spacing: .16em;
  color: var(--gold);
  padding: 9px 13px;
  border: 1px solid rgba(176, 141, 87, .5);
  border-radius: 3px;
  background: rgba(255, 253, 249, .8);
}

/* ── Feedback line ────────────────────────────────────────────────────────── */
.feedback {
  min-height: 2.2em;
  margin: 16px auto 0;
  max-width: 52ch;
  font-size: clamp(12.5px, 1.3vw, 14.5px);
  line-height: 1.65;
  color: var(--ink-soft);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity .4s var(--ease), transform .4s var(--ease);
}
.feedback.is-visible { opacity: 1; transform: none; }
.feedback em { font-family: var(--display); font-style: italic; font-size: 1.12em; color: var(--ink); }

/* ── Buttons ──────────────────────────────────────────────────────────────── */
.actions {
  margin-top: 12px;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: clamp(10px, 2vw, 22px);
}

.btn {
  font-family: var(--ui);
  font-weight: 400;
  font-size: 12px;
  letter-spacing: .2em;
  text-transform: uppercase;
  padding: 13px 26px;
  border-radius: 2px;
  border: 1px solid transparent;
  cursor: pointer;
  transition: all .35s var(--ease);
  background: none;
}

.btn--primary {
  background: var(--ink);
  color: var(--paper);
  border-color: var(--ink);
}
.btn--primary:hover:not(:disabled) {
  background: var(--gold);
  border-color: var(--gold);
  transform: translateY(-1px);
  box-shadow: 0 10px 22px -12px rgba(44, 38, 34, .7);
}
.btn--primary:disabled {
  opacity: .28;
  cursor: not-allowed;
}

.btn--ghost {
  color: var(--ink-soft);
  border-color: rgba(107, 96, 88, .3);
  text-transform: none;
  letter-spacing: .08em;
  font-size: 12.5px;
  padding: 12px 20px;
}
.btn--ghost:hover:not(:disabled) { color: var(--ink); border-color: var(--gold); }
.btn--ghost:disabled { opacity: .35; cursor: not-allowed; }

.btn:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }

/* ── Reveal ───────────────────────────────────────────────────────────────────
   The save-the-date sits on a full-bleed print of the venue, in one of two
   treatments (`revealStyle` in config.js sets `data-style` on the section):

     plate — white type straight on the picture. The whole print stays visible;
             the picture is darkened so the words can hold their own.
     card  — the type on a frosted ivory card. Keeps the print bright and the
             words on paper, at the cost of covering part of it.               */
#stage-reveal {
  background-image: url('../assets/std-background.jpg');
  background-size: cover;
  background-position: 64% 44%;
  background-repeat: no-repeat;
}

#stage-reveal::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* ── plate: white type, no card ───────────────────────────────────────────── */
/* Dusk falls on the print. Warm rather than neutral, so it reads as evening at
   the palace instead of a grey filter — and heavier top and bottom, where the
   type actually sits, than through the middle where the building is. */
#stage-reveal[data-style="plate"]::before {
  background:
    /* the pool the words actually stand on — soft-edged, so the building
       around it stays lit */
    radial-gradient(60% 62% at 50% 50%,
      rgba(20, 10, 5, .62) 0%,
      rgba(20, 10, 5, .34) 56%,
      rgba(20, 10, 5, 0) 100%),
    /* and a light overall dusk, kept off the middle so the palace reads */
    linear-gradient(to bottom,
      rgba(26, 14, 8, .42) 0%,
      rgba(30, 17, 9, .24) 30%,
      rgba(30, 17, 9, .24) 66%,
      rgba(22, 12, 6, .48) 100%);
}

#stage-reveal[data-style="plate"] .stage__inner { max-width: 780px; }

#stage-reveal[data-style="plate"] .reveal {
  position: relative;
  z-index: 1;
  padding: 10px 0;
  /* Every line gets the same soft drop, which is what lets thin serif strokes
     survive over a picture that still has detail in it. */
  text-shadow: 0 1px 2px rgba(18, 9, 4, .5), 0 2px 22px rgba(18, 9, 4, .55);
}

#stage-reveal[data-style="plate"] .reveal__eyebrow { color: #e6d4ad; }
#stage-reveal[data-style="plate"] .reveal__names   { color: #fffcf6; }
#stage-reveal[data-style="plate"] .reveal__names .amp { color: #e8cf9c; }
#stage-reveal[data-style="plate"] .reveal__date    { color: #fffcf6; }
#stage-reveal[data-style="plate"] .reveal__year,
#stage-reveal[data-style="plate"] .reveal__city,
#stage-reveal[data-style="plate"] .reveal__note    { color: #ece0cd; }
#stage-reveal[data-style="plate"] .reveal__venue   { color: #fdf7ec; }
/* The small lines land lowest, over the lit facade — they need more shadow
   than the names do up in the sky. */
#stage-reveal[data-style="plate"] .reveal__venue,
#stage-reveal[data-style="plate"] .reveal__city,
#stage-reveal[data-style="plate"] .reveal__note,
#stage-reveal[data-style="plate"] .replay {
  text-shadow: 0 1px 3px rgba(16, 8, 3, .8), 0 2px 18px rgba(16, 8, 3, .65);
}
#stage-reveal[data-style="plate"] .rule            { color: #e8cf9c; }
#stage-reveal[data-style="plate"] .rule span {
  background: linear-gradient(to var(--dir, right), transparent, rgba(240, 225, 192, .8));
}
#stage-reveal[data-style="plate"] .replay {
  color: rgba(240, 231, 216, .82);
  border-bottom-color: rgba(240, 231, 216, .45);
}
#stage-reveal[data-style="plate"] .replay:hover {
  color: #fffcf6;
  border-bottom-color: #e8cf9c;
}

/* No card to constrain it, so the names can be as big as the picture is wide.
   The names are the largest thing on the plate and "Save the Date" sits just
   under them — it's the label, they're the point. (It's set in a script whose
   capitals run tall, so it still reads as the bigger of the two at a smaller
   em; matching the numbers would put it back on top.) */
#stage-reveal[data-style="plate"] .reveal__names   { font-size: calc(clamp(36px, 7.4vw, 78px) * var(--lang-scale, 1)); }
#stage-reveal[data-style="plate"] .reveal__eyebrow { font-size: calc(clamp(31px, 6.3vw, 67px) * var(--lang-scale, 1)); }
#stage-reveal[data-style="plate"] .reveal__year    { font-size: calc(clamp(20px, 2.9vw, 29px) * var(--lang-scale, 1)); }
/* The date is the one fact anyone came for, so it leads the information block;
   the venue and the city sit a step down from it, and the closing line comes
   back up a little because it was reading as fine print. */
#stage-reveal[data-style="plate"] .reveal__date   { font-size: calc(clamp(23px, 3.5vw, 35px) * var(--lang-scale, 1)); }
#stage-reveal[data-style="plate"] .reveal__note   { font-size: calc(clamp(20px, 3vw, 30px) * var(--lang-scale, 1)); }
#stage-reveal[data-style="plate"] .reveal__venue,
#stage-reveal[data-style="plate"] .reveal__city  { font-size: calc(clamp(18px, 2.7vw, 27px) * var(--lang-scale, 1)); }
#stage-reveal[data-style="plate"] .replay        { font-size: calc(14px * var(--lang-scale, 1)); }

/* The phone sizes for all of this live at the bottom of the file, after the
   base rules — a media query carries no extra specificity, so an unscoped
   `.reveal__venue` up here would quietly lose to the unscoped `.reveal__venue`
   further down and only the plate (which is ID-scoped, and so wins) would
   actually get its phone size. */

/* ── card: type on frosted paper ──────────────────────────────────────────── */
/* Warms the picture down a little and pulls the corners in, so the card reads
   as sitting *on* something rather than floating in a bright field. */
#stage-reveal[data-style="card"]::before {
  background:
    radial-gradient(120% 100% at 50% 45%,
      rgba(58, 38, 24, 0) 0%,
      rgba(58, 38, 24, .16) 62%,
      rgba(44, 28, 18, .42) 100%),
    linear-gradient(to bottom, rgba(62, 40, 24, .10), rgba(62, 40, 24, .04));
}

#stage-reveal[data-style="card"] .stage__inner { max-width: 600px; }

#stage-reveal[data-style="card"] .reveal {
  position: relative;
  z-index: 1;
  padding: clamp(28px, 4.8vh, 54px) clamp(22px, 4vw, 46px);
  /* Frosted, not opaque. The heavy blur is what buys the transparency: it
     turns whatever is behind into a smooth wash, so the type still has clean
     ground to sit on at 70%. */
  background: rgba(250, 246, 238, .70);
  -webkit-backdrop-filter: blur(16px) saturate(1.12);
  backdrop-filter: blur(16px) saturate(1.12);
  border: 1px solid rgba(255, 250, 240, .55);
  border-radius: 2px;
  box-shadow:
    0 2px 6px rgba(46, 30, 18, .18),
    0 40px 90px -34px rgba(46, 30, 18, .66);
}

/* Browsers without backdrop-filter get opacity instead — otherwise the type
   would sit straight on the picture. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  #stage-reveal[data-style="card"] .reveal { background: rgba(250, 246, 238, .93); }
}

/* The second hairline — stationery, not a web card. */
#stage-reveal[data-style="card"] .reveal::after {
  content: '';
  position: absolute;
  inset: 7px;
  border: 1px solid rgba(176, 141, 87, .28);
  border-radius: 1px;
  pointer-events: none;
}

/* Wide screens: step the card off to the left so the palace — the whole point
   of the picture — is never behind it. Below this it stays centred, because
   there isn't room to give anything away. */
@media (min-width: 900px) {
  #stage-reveal[data-style="card"] {
    justify-items: start;
    padding-left: clamp(44px, 6.5vw, 130px);
  }
  #stage-reveal[data-style="card"] .stage__inner { max-width: 540px; }
}

.reveal-line {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity .9s var(--ease), transform .9s var(--ease);
}
.stage.is-active .reveal-line { opacity: 1; transform: none; }
.stage.is-active .reveal-line:nth-child(1) { transition-delay: .15s; }
.stage.is-active .reveal-line:nth-child(2) { transition-delay: .32s; }
.stage.is-active .reveal-line:nth-child(3) { transition-delay: .48s; }
.stage.is-active .reveal-line:nth-child(4) { transition-delay: .60s; }
.stage.is-active .reveal-line:nth-child(5) { transition-delay: .74s; }
.stage.is-active .reveal-line:nth-child(6) { transition-delay: .86s; }
.stage.is-active .reveal-line:nth-child(7) { transition-delay: .98s; }
.stage.is-active .reveal-line:nth-child(8) { transition-delay: 1.2s; }
.stage.is-active .reveal-line:nth-child(9) { transition-delay: 1.4s; }

/* The save-the-date is set in one face throughout — the Garamond the names
   were always in — with a single exception: "Save the Date" is the one line
   written by hand.

   `--lang-scale` is how हिन्दी is re-proportioned without any of these sizes
   being written twice; see the block below. */

/* The copperplate line. No caps, no tracking: a script is drawn joined, and
   letter-spacing prises the strokes apart at exactly the joins that make it a
   script. It's set far larger than its neighbours in px and still reads
   smaller — a copperplate spends most of its em on ascenders, flourishes and
   descenders, so only about half the size you set ends up as letter. */
.reveal__eyebrow {
  margin: 0 0 clamp(6px, 1.2vh, 16px);
  font-family: var(--script);
  font-weight: 400;
  font-size: calc(clamp(32px, 5.1vw, 59px) * var(--lang-scale, 1));
  line-height: 1.24;
  letter-spacing: 0;
  text-indent: 0;
  text-transform: none;
  color: var(--gold);
}

.reveal__names {
  margin: 0;
  font-family: var(--display);
  font-weight: 300;
  font-size: calc(clamp(24px, 4.8vw, 41px) * var(--lang-scale, 1));
  line-height: 1.06;
  letter-spacing: .01em;
}
.reveal__names .amp { font-style: italic; color: var(--gold); padding: 0 .06em; }

.rule {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin: clamp(18px, 3vh, 32px) auto;
  max-width: 320px;
  color: var(--gold);
  font-size: 11px;
}
.rule span {
  height: 1px;
  flex: 1;
  background: linear-gradient(to var(--dir, right), transparent, rgba(176, 141, 87, .65));
}
.rule span:last-of-type { --dir: left; }

/* The information block — the date, the place, the promise — is all one size.
   Weight, case, italics and colour carry the hierarchy instead, which is how a
   printed invitation does it: the date leads because it is roman and full
   strength, not because it is bigger. Kept as one rule so the four can't drift
   apart later. The year is the exception, a subordinate line to the date. */
.reveal__date,
.reveal__venue,
.reveal__city,
.reveal__note {
  font-size: calc(clamp(16px, 2.7vw, 27px) * var(--lang-scale, 1));
}

.reveal__date {
  margin: 0;
  font-family: var(--display);
  font-weight: 400;
  letter-spacing: .01em;
}
.reveal__year {
  margin: .3em 0 0;
  font-family: var(--display);
  font-style: italic;
  font-size: calc(clamp(19px, 2.8vw, 28px) * var(--lang-scale, 1));
  color: var(--ink-soft);
}

.reveal__venue {
  margin: clamp(24px, 4vh, 44px) 0 0;
  font-family: var(--display);
  font-weight: 500;
  /* Tracking comes down from the quarter-em it wore at 17px: at the date's
     size that spacing costs a third of the measure, and "Radisson Udaipur
     Palace Resort" is the longest line on the page. Where it still doesn't
     fit — phones — it breaks, and `balance` keeps the break near the middle
     rather than stranding one word. */
  letter-spacing: .14em;
  text-indent: .14em;
  text-transform: uppercase;
  text-wrap: balance;
}
.reveal__city {
  margin: .7em 0 0;
  font-family: var(--display);
  font-weight: 400;
  letter-spacing: .1em;
  text-indent: .1em;
  color: var(--ink-soft);
}

.reveal__note {
  margin: clamp(26px, 4.4vh, 50px) 0 0;
  font-family: var(--display);
  font-style: italic;
  text-wrap: balance;
  color: var(--ink-soft);
}

.replay {
  margin-top: clamp(22px, 3.6vh, 40px);
  background: none;
  border: none;
  border-bottom: 1px solid rgba(164, 152, 140, .5);
  padding: 2px 1px;
  font-family: var(--display);
  font-weight: 500;
  font-size: calc(13.5px * var(--lang-scale, 1));
  letter-spacing: .2em;
  text-indent: .2em;
  text-transform: uppercase;
  color: var(--ink-faint);
  cursor: pointer;
  transition: color .3s var(--ease), border-color .3s var(--ease);
}
.replay:hover { color: var(--gold); border-color: var(--gold); }

/* ── हिन्दी ───────────────────────────────────────────────────────────────────
   Everything above reads `--display`, so pointing that one variable at the
   Devanagari face inside the reveal switches the whole plate over in one move —
   no line has its font named twice.

   Two things then have to give. Devanagari sets larger on the body than a
   Garamond does at the same size, so the whole plate comes down a notch
   (`--lang-scale`); and it needs headroom above and below the line for the
   matras, so the leading opens up. Tracking mostly goes: space between clusters
   pulls a word apart in a way it doesn't in Latin, and the shirorekha — the
   line along the top — stops reading as one stroke. What's left is a hair,
   enough to keep the tracked-out lines feeling tracked out.

   No italics and no script anywhere: Devanagari has no cursive companion. A
   sloped Devanagari is a mechanical shear, and the "handwriting" Devanagari
   faces are marker-pen, not copperplate — either would read as a downgrade
   next to the Pinyon. So तिथि याद रखें is set in the same Tiro as the rest and
   earns its place as the display line on size alone. */
:root[data-lang="hi"] #stage-reveal {
  --display: var(--devanagari);
  --script:  var(--devanagari);
  --lang-scale: .94;
}

/* The eyebrow's px sizes are cut for a copperplate, which spends half its em
   on flourishes. Devanagari spends nearly all of it on letter, so it needs
   roughly half the number to land at the same optical size. */
:root[data-lang="hi"] .reveal__eyebrow {
  --lang-scale: .65;
  letter-spacing: .08em;
  text-indent: .08em;
  line-height: 1.5;
}

/* The date and the place, a step up again in हिन्दी. Devanagari carries its
   detail — the matras, the nuqtas, the difference between ड and ड़ — below the
   size at which a Latin serif is still perfectly comfortable, and these are
   the lines a guest actually has to read off the screen. */
:root[data-lang="hi"] .reveal__date,
:root[data-lang="hi"] .reveal__year,
:root[data-lang="hi"] .reveal__venue,
:root[data-lang="hi"] .reveal__city,
:root[data-lang="hi"] .reveal__note {
  --lang-scale: 1.08;
}
:root[data-lang="hi"] .reveal__names {
  letter-spacing: 0;
  line-height: 1.36;
}
:root[data-lang="hi"] .reveal__names .amp,
:root[data-lang="hi"] .reveal__year,
:root[data-lang="hi"] .reveal__note { font-style: normal; }
:root[data-lang="hi"] .reveal__names .amp { font-size: .78em; }
:root[data-lang="hi"] .reveal__date  { letter-spacing: 0; line-height: 1.4; }
:root[data-lang="hi"] .reveal__year  { line-height: 1.5; }
:root[data-lang="hi"] .reveal__venue { letter-spacing: .07em; text-indent: .07em; line-height: 1.55; }
:root[data-lang="hi"] .reveal__city  { letter-spacing: .04em; text-indent: .04em; line-height: 1.5; }
:root[data-lang="hi"] .reveal__note  { line-height: 1.5; }
:root[data-lang="hi"] .replay        { letter-spacing: .08em; text-indent: .08em; }

/* ── The save-the-date on a phone ─────────────────────────────────────────────
   Deliberately after everything above: these are unscoped class selectors, and
   a media query adds no specificity, so they have to come last to beat the
   desktop rules for both treatments rather than only for the plate.

   Only two things need saying here. The script comes down harder than a linear
   scale would take it — "Save the Date" is three words of copperplate with a
   long tail on the S, and it's the one line that must not break. And the venue
   is the longest line on the page, so on the narrowest phones its tracking
   gives before its words do. Each is written for both treatments: the plate's
   own rules are ID-scoped and would otherwise outrank a bare class here
   however late it comes. */
@media (max-width: 480px) {
  /* Both clamps bottom out at this width, so without their own numbers here
     the eyebrow would end up larger than the names again — the one thing the
     desktop sizes were arranged to avoid. */
  .reveal__eyebrow                                   { font-size: calc(clamp(26px, 6.6vw, 33px) * var(--lang-scale, 1)); }
  #stage-reveal[data-style="plate"] .reveal__eyebrow { font-size: calc(clamp(28px, 7.2vw, 36px) * var(--lang-scale, 1)); }

  /* The venue is the one line that still can't hold a phone's width at the
     information block's size, so its tracking gives before its words do. */
  .reveal__venue, #stage-reveal[data-style="plate"] .reveal__venue {
    letter-spacing: .08em;
    text-indent: .08em;
  }
  :root[data-lang="hi"] .reveal__venue,
  :root[data-lang="hi"] #stage-reveal[data-style="plate"] .reveal__venue {
    letter-spacing: .03em;
    text-indent: .03em;
  }

  /* The switch is a control, not part of the print — on a small screen it has
     to give the save-the-date its corner back. */
  .lang-toggle { gap: 8px; padding: 6px 12px 7px; font-size: 13px; }
}

/* The oldest phones still in circulation are 320px across, and that is exactly
   where "Pankhuri & Tanmay" stops fitting on one line — it's the couple's names
   on their own save-the-date, so it comes down rather than breaking in half. */
@media (max-width: 359px) {
  .reveal__names,
  #stage-reveal[data-style="plate"] .reveal__names   { font-size: calc(clamp(27px, 9.5vw, 34px) * var(--lang-scale, 1)); }
  .reveal__eyebrow,
  #stage-reveal[data-style="plate"] .reveal__eyebrow { font-size: calc(clamp(22px, 7.6vw, 28px) * var(--lang-scale, 1)); }
}

/* ── The dust dissolve ────────────────────────────────────────────────────────
   Changing language doesn't swap the words, it blows them away. Each word
   lifts, blurs and drifts off on the same breeze, a few frames behind the one
   before it, and the replacement gathers itself back out of the air arriving
   from the side the old one left towards.

   The stagger is the whole thing. A line that leaves all at once is a
   crossfade; a line that leaves left to right is weather. js/lang.js sets --dx
   / --dy / --r / --d per word — one prevailing direction, jittered just enough
   that it doesn't tick like a metronome. */
.dust-word {
  display: inline-block;
  will-change: transform, opacity, filter;
}
.is-dust-out .dust-word {
  animation: dust-out 820ms var(--ease) both;
  animation-delay: var(--d, 0ms);
}
.is-dust-in .dust-word {
  animation: dust-in 1000ms var(--ease) both;
  animation-delay: var(--d, 0ms);
}

@keyframes dust-out {
  0%   { opacity: 1; filter: blur(0); transform: none; }
  35%  { opacity: .8; }
  100% {
    opacity: 0;
    filter: blur(7px);
    transform: translate3d(var(--dx, 34px), var(--dy, -14px), 0)
               rotate(var(--r, 4deg)) scale(1.08);
  }
}
@keyframes dust-in {
  0% {
    opacity: 0;
    filter: blur(8px);
    transform: translate3d(calc(var(--dx, 34px) * -1), calc(var(--dy, -14px) * -1), 0)
               rotate(calc(var(--r, 4deg) * -1)) scale(.94);
  }
  55%  { opacity: .78; }
  100% { opacity: 1; filter: blur(0); transform: none; }
}

/* The motes the words leave behind. Lifted off the block of type in viewport
   coordinates, which is why the field is fixed rather than absolute. */
.dust-field {
  position: fixed;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  overflow: hidden;
}
.mote {
  position: absolute;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--dust-tint, rgba(240, 222, 184, .92));
  opacity: 0;
  animation-name: mote-drift;
  animation-timing-function: cubic-bezier(.2, .7, .3, 1);
  animation-fill-mode: both;
}
#stage-reveal[data-style="plate"] { --dust-tint: rgba(244, 228, 192, .95); }
#stage-reveal[data-style="card"]  { --dust-tint: rgba(150, 118, 74, .62); }

@keyframes mote-drift {
  0%   { opacity: 0; transform: translate3d(0, 0, 0) scale(var(--ms, 1)); }
  20%  { opacity: .8; }
  100% {
    opacity: 0;
    transform: translate3d(var(--mx, 140px), var(--my, -60px), 0)
               scale(calc(var(--ms, 1) * .35));
  }
}

/* Falling petals on the reveal ------------------------------------------- */
.petals {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}
.reveal-line { position: relative; z-index: 1; }   /* text stays above the petals */
.petal {
  position: absolute;
  top: -8vh;
  width: 9px;
  height: 12px;
  border-radius: 60% 0 60% 0;
  background: var(--blush);
  opacity: 0;
  animation: fall linear infinite;
}
@keyframes fall {
  0%   { opacity: 0; transform: translate3d(0, -10vh, 0) rotate(0deg); }
  8%   { opacity: .7; }
  85%  { opacity: .55; }
  100% { opacity: 0; transform: translate3d(var(--drift, 40px), 118vh, 0) rotate(540deg); }
}

/* ── Mobile ───────────────────────────────────────────────────────────────────
   A phone can't show four cards *and* four frames side by side at any size
   worth looking at — 2 × 2 frames with a row of cards underneath left the
   artwork about 100px wide, which is no artwork at all.

   So the two stop being the same size. The frames become a slim numbered
   column down the right; the undealt cards become a fanned deck on the left at
   roughly 3× that, each one showing its title strip below the card in front of
   it. Cards shrink as they're placed and bloom back out when the board is full.
   ──────────────────────────────────────────────────────────────────────────── */
@media (max-width: 760px) {
  :root {
    /* A column of four frames is six card-heights tall, so the frame size is
       set by the height available — same trick as desktop, different divisor. */
    --card-w:  min(25vw, max(52px, calc((100vh - 326px) / 6.4)));
    /* The deck: as wide as the space left of the column allows, but never so
       tall that the fan outruns the column beside it. */
    --card-base: min(calc(100vw - 108px), calc(var(--card-w) * 2.78));
    --fan-step: calc(var(--card-base) * .24);
    /* 1.5 card-heights + three offsets. Precomputed: nesting var()-in-calc
       arithmetic this deep gets dropped as invalid by the parser. */
    --fan-h: calc(var(--card-base) * 2.22);
    --gap: 10px;
  }
  @supports (height: 100dvh) {
    :root { --card-w: min(25vw, max(52px, calc((100dvh - 326px) / 6.4))); }
  }

  .display { font-size: clamp(27px, 7.4vw, 40px); }
  .display br { display: inline; }
  .lede { font-size: 15.5px; line-height: 1.6; max-width: 34ch; }

  /* Everything around the board is trimmed to the bone here. A column of four
     frames is six frame-heights tall and it is the tallest thing on the page,
     so whatever the masthead and the buttons don't use becomes frame size. */
  .stage { padding-top: 16px; padding-bottom: 16px; }
  .masthead { margin-bottom: 14px; }

  /* deck on the left, frames on the right */
  .board {
    flex-direction: row;
    align-items: flex-start;
    justify-content: center;
    gap: 12px;
    transition: gap .55s var(--ease);
  }

  /* ── the frame column ── */
  .rail--slots {
    order: 2;
    flex: 0 0 auto;
    width: auto;
    display: grid;
    grid-template-columns: var(--card-w);
    gap: 5px;
  }
  .slot__num { font-size: calc(var(--card-w) * .34); }

  /* ── the fanned deck ── */
  .rail--hand {
    order: 1;
    flex: 0 0 var(--card-base);
    width: var(--card-base);
    position: relative;
    display: block;
    /* four cards, each offset by one step, plus the last card's full height */
    height: var(--fan-h);
    transition: height .55s var(--ease), flex-basis .55s var(--ease),
                width .55s var(--ease), opacity .35s var(--ease);
  }
  .hand-anchor {
    position: absolute;
    left: 0;
    width: var(--card-base);
  }
  /* A placed card is the deck card under a scale transform, so its title
     shrinks along with the artwork and needs no rule of its own. The deck card
     does: at 211px wide it's nearly twice a desktop card, and the shared
     viewport-based size leaves the title looking like a caption on a poster. */
  .card.is-in-hand .card__title { font-size: calc(var(--card-base) * .082); }

  /* In the fan the cards overlap, so each needs a lip of its own — a bright
     top edge and a shadow cast onto the card behind it. Without this the four
     of them read as a single confused picture. */
  .card.is-in-hand .card__inner {
    box-shadow:
      0 -1px 0 0 rgba(255, 253, 249, .9),
      0 -5px 14px -5px rgba(44, 38, 34, .5),
      var(--shadow-rest);
  }

  .hand-anchor:nth-child(1) { top: 0; }
  .hand-anchor:nth-child(2) { top: var(--fan-step); }
  .hand-anchor:nth-child(3) { top: calc(var(--fan-step) * 2); }
  .hand-anchor:nth-child(4) { top: calc(var(--fan-step) * 3); }

  /* Once every frame is full the deck has nothing left to hold, so it folds
     away and the finished timeline blooms out to fill the width — the payoff
     shouldn't be four thumbnails in a strip. */
  /* The deck keeps its box when it empties — collapsing it here would slide the
     frame column sideways while the player is still placing cards. It just
     fades. */
  .rail--hand.is-collapsed {
    height: var(--fan-h);
    flex-basis: var(--card-base);
    width: var(--card-base);
    opacity: 0;
    pointer-events: none;
  }
  .board.is-tight { gap: 12px; }

  /* Only a correct order rearranges anything: the deck folds away for real and
     the finished timeline blooms out of its strip.

     `is-inorder` is the moment the fourth card lands in the right frame, before
     anyone has pressed anything; `is-won` is the same picture after the button.
     They're kept as two classes because only the second one is a commitment —
     take a card back out and the board comes apart again, which is the whole
     reason the bloom couldn't be fired on "four cards are down" alone. */
  .board.is-inorder .rail--hand,
  .board.is-won .rail--hand { height: 0; flex-basis: 0; width: 0; }
  .board.is-inorder,
  .board.is-won { gap: 0; }
  .board.is-inorder .rail--slots,
  .board.is-won .rail--slots {
    grid-template-columns: repeat(2, var(--bloom-w));
    gap: 10px;
    justify-content: center;
  }
  .board.is-inorder .slot,
  .board.is-won .slot { width: var(--bloom-w); }
  .board.is-inorder .slot__num,
  .board.is-won .slot__num { font-size: calc(var(--bloom-w) * .3); }
  :root {
    --bloom-w: min(calc((100vw - 46px) / 2), calc((100vh - 330px) / 3.28));
  }
  @supports (height: 100dvh) {
    :root { --bloom-w: min(calc((100vw - 46px) / 2), calc((100dvh - 330px) / 3.28)); }
  }

  .feedback { min-height: 2em; margin-top: 10px; }

  .actions { flex-direction: column; width: 100%; gap: 8px; margin-top: 10px; }
  .btn { width: 100%; max-width: 300px; padding: 10px 18px; }
  .btn--ghost { font-size: 12px; }
}


/* Short, wide windows: shed the headline's second line and tighten up, so the
   cards keep as much of the remaining height as possible. */
@media (max-height: 760px) and (min-width: 761px) {
  :root { --card-w: min(clamp(110px, 17vw, 300px), calc((100vh - 292px) / 3)); }
  @supports (height: 100dvh) {
    :root { --card-w: min(clamp(110px, 17vw, 300px), calc((100dvh - 292px) / 3)); }
  }
  .display  { font-size: clamp(24px, 3vw, 34px); }
  .lede     { font-size: 15px; margin-top: 8px; }
  .masthead { margin-bottom: 14px; }
  .board    { gap: 16px; }
  .feedback { min-height: 2em; margin-top: 12px; }
}

/* ── Reduced motion ───────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) *,
  :root:not([data-motion="full"]) *::before,
  :root:not([data-motion="full"]) *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .12s !important;
  }
  :root:not([data-motion="full"]) .petals { display: none; }
  /* No gust here, so no motes to leave behind. */
  :root:not([data-motion="full"]) .dust-field { display: none; }

  /* The one animation that earns an exemption from the clamp above. Reduce
     Motion asks for nothing to move; it does not ask for the words to jump
     between scripts with no warning, which is what a hard cut looks like when
     the languages take turns on their own — less like a design and more like
     the page glitching. Nothing here travels, blurs or scales: it is opacity
     and nothing else, so there is no motion to be sick from. */
  :root:not([data-motion="full"]) .is-lang-out .dust-word {
    animation: lang-fade-out 380ms ease both !important;
  }
  :root:not([data-motion="full"]) .is-lang-in .dust-word {
    animation: lang-fade-in 560ms ease both !important;
  }
}

@keyframes lang-fade-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes lang-fade-in  { from { opacity: 0; } to { opacity: 1; } }
