/* ============================================================
   雙城囍宴 · Wedding invitation stylesheet
   Traditional red & gold · 囍 · 祥雲
   ============================================================ */

/* 楷書 for the 請柬 card. Self-hosted and cut down to the 44 characters the two
   cards actually use (15KB), because the card must look the same for everyone:
   relying on installed 楷體 meant macOS/Windows readers saw one thing, Android
   readers saw 宋體, and this machine saw AR PL UKai. Google Fonts would fix the
   consistency but not for the mainland, where it is blocked — hence self-hosted.
   See assets/fonts/README.txt to re-cut it if the card's wording changes. */
@font-face {
  font-family: "Card Kai";
  /* Cut by scripts/build-font.mjs from the full LXGW WenKai TC — see
     assets/fonts/README.txt. Medium rather than Regular: the gold ink needs a
     stroke wide enough to carry a gradient and an edge, and Regular's is not.
     No unicode-range here on purpose. The old one was a hand-typed list of 44
     codepoints that the card's wording outgrew, so 攜同雙方父母 and every guest
     name silently fell back to 宋體 mid-column. The subset file is now derived
     from the card itself and is the single source of what it covers. */
  src: url("../assets/fonts/card-kai.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* A single, locally hosted 囍. At only a few KB it normally arrives with the
   first CSS response; swap leaves a readable system-font 囍 on slow or offline
   connections. The surrounding ring belongs to CSS, not the font. */


:root {
  --red-950: #420b12;
  --red-900: #521019;
  --red-800: #641622;   /* page background base */
  --red-700: #781e2a;
  --red-600: #932936;
  --red-500: #ad3440;   /* lit face — envelope, seal */
  --gold-300: #f3d68b;
  --gold-400: #e6c37a;
  --gold-500: #d0a24e;
  --gold-600: #a97e2f;
  /* the running-text ink. The banquet pages override both to gold (see the
     per-page accents near the end) so no word on them leaves the gold family;
     the landing page keeps this warmer ivory, which sits better on its brown. */
  --ink: #f6ecd4;
  --ink-dim: rgba(246, 236, 212, 0.88);
  /* 請柬 · a red card written in gold marker.
     The card sits a step BRIGHTER than the page ground (--red-800) so that once
     the envelope has sunk away it still reads as a separate sheet of paper
     rather than as a panel of the page. */
  --card: #b02c38;
  --card-shade: #8c1f2a;
  --card-edge: rgba(243, 214, 139, 0.5);
  /* the marker itself: one warm gold, and a solid value to fall back to */
  --card-ink: #e6cb84;

  --serif: "Songti TC", "Noto Serif CJK TC", "Noto Serif TC",
           "Songti SC", "Noto Serif CJK SC", "Noto Serif SC",
           "Source Han Serif SC", "STSong", "SimSun", serif;

  /* 楷書 for the 請柬 card. The subset webfont above answers for every character
     on the card; the installed faces are only a fallback for the seconds before
     it loads, or if it fails to. */
  --kai: "Card Kai", "Kaiti TC", "STKaiti", "BiauKai", "DFKai-SB", "標楷體",
         "Kaiti SC", "KaiTi", "楷体", var(--serif);

  /* per-page accent, overridden by .page-gz / .page-bj */
  --accent: var(--gold-400);
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  /* The default tap flash is a grey box that reads as damage over the red and
     gold. Every tappable thing here has its own :active state instead. */
  -webkit-tap-highlight-color: transparent;
}

body {
  position: relative;
  font-family: var(--serif);
  font-size: 17px;
  color: var(--ink);
  background:
    url("../assets/img/clouds.svg") repeat,
    radial-gradient(120% 90% at 50% 0%, var(--red-700) 0%, var(--red-800) 46%, var(--red-900) 78%, var(--red-950) 100%);
  background-size: 480px 340px, cover;
  min-height: 100vh;
  line-height: 1.9;
  letter-spacing: 0.06em;
  overflow-x: hidden;
}

/* Mainland-style punctuation: keep full-width stops optically at the foot
   of the em square rather than centred by Traditional Chinese font metrics. */
.cn-comma {
  display: inline-block;
  transform: translate(-0.22em, 0.32em);
  margin-right: -0.22em;
}

.nowrap { white-space: nowrap; }
[hidden] { display: none !important; }

/* Gold double frame around the viewport — the classic 請柬 border. The outer
   edge is an opaque mat: content scrolls behind it and is visually clipped,
   rather than drawing over or under a loose line. */
body::before,
body::after {
  content: "";
  position: fixed;
  pointer-events: none;
}
body::before {
  inset: 0;
  z-index: 40;
  background:
    linear-gradient(var(--red-950), var(--red-950)) top / 100% 10px no-repeat,
    linear-gradient(var(--red-950), var(--red-950)) bottom / 100% 10px no-repeat,
    linear-gradient(var(--red-950), var(--red-950)) left / 10px 100% no-repeat,
    linear-gradient(var(--red-950), var(--red-950)) right / 10px 100% no-repeat;
}
body::after {
  inset: 17px;
  z-index: 41;
  border: 1px solid rgba(230, 195, 122, 0.45);
  outline: 2px solid rgba(208, 162, 78, 0.85);
  outline-offset: 5px;
}
@media (max-width: 480px) {
  body::before {
    background-size: 100% 6px, 100% 6px, 6px 100%, 6px 100%;
  }
  body::after {
    inset: 12px;
    outline-offset: 4px;
  }
}

main {
  position: relative;
  max-width: 660px;
  margin: 0 auto;
  padding: 56px 26px 72px;
}
@media (max-width: 480px) {
  main { padding: 44px 20px 60px; }
}

/* ---------- hero ---------- */

.hero {
  position: relative;
  text-align: center;
  padding: 26px 0 10px;
}

/* 龍鳳 decals — the couple's own drawing (dragon.svg / phoenix.svg), flanking the hero */
.decal {
  position: absolute;
  top: 36px;
  width: 96px;
  opacity: 0.5;
  pointer-events: none;
  z-index: -1; /* behind the hero text, never on top of it */
}
/* sides are class-based so creatures can swap per page (see .decal.flip) */
.decal.side-l { left: -10px; }
.decal.side-r { right: -10px; }
.decal.ox-rabbit-ox,
.decal.ox-rabbit-rabbit {
  top: 18px;
  opacity: 0.28;
}
.decal.ox-rabbit-ox { left: 8px; width: 150px; }
.decal.ox-rabbit-rabbit { right: 8px; width: 130px; }
.page-bj .decal.ox-rabbit-ox,
.page-gz .decal.ox-rabbit-ox {
  left: 54px;
  top: 32px;
  width: 112px;
  opacity: 0.34;
}
.page-bj .decal.ox-rabbit-rabbit,
.page-gz .decal.ox-rabbit-rabbit {
  right: 54px;
  top: 8px;
  width: 98px;
  opacity: 0.34;
}
/* No Guangzhou override any more. These two rules existed because gz put the
   rabbit in the ox slot and vice versa, so each slot needed the other
   creature's silhouette tuning. Both pages now carry the same creature in the
   same slot, and keeping the swap would have sized each one for its opposite. */
@media (max-width: 560px) {
  .decal { width: 64px; top: 4px; opacity: 0.35; }
  .decal.ox-rabbit-ox { left: 2px; width: 116px; top: 8px; opacity: 0.23; }
  .decal.ox-rabbit-rabbit { right: 2px; width: 102px; top: 8px; opacity: 0.23; }
  .page-bj .decal.ox-rabbit-ox,
  .page-gz .decal.ox-rabbit-ox { left: 22px; top: 70px; width: 88px; opacity: 0.28; }
  .page-bj .decal.ox-rabbit-rabbit,
  .page-gz .decal.ox-rabbit-rabbit { right: 22px; top: 48px; width: 78px; opacity: 0.28; }

}

.seal {
  width: 92px;
  height: 92px;
  margin: 0 auto 26px;
  display: grid;
  place-items: center;
  /* The mark's circle is 0.88 of its em, so a font-size equal to the disc
     leaves it filling only ~61%. 0.84x the box lands it at ~74%, matching
     the hero seal. */
  font-size: 77px;
  line-height: 1;
  color: var(--gold-300);
  background:
    radial-gradient(80% 80% at 30% 25%, rgba(255, 236, 180, 0.14), transparent 60%),
    linear-gradient(160deg, var(--red-600), var(--red-800));
  border: 1px solid var(--gold-500);
  box-shadow:
    inset 0 0 0 4px var(--red-800),
    inset 0 0 0 5px var(--gold-600),
    0 6px 26px rgba(0, 0, 0, 0.35),
    0 0 40px rgba(230, 195, 122, 0.12);
  border-radius: 50%;
}
.seal span {
  display: inline-block;
  /* The glyph is centred on its own em box, so the old baseline nudge is gone.
     The shadow is safe again now that the mark IS the text — while it was a
     mask over a transparent 囍, text-shadow drew the fallback glyph behind it. */
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.28);
}

@media (max-width: 560px) {
  /* Keep the seal subordinate to the animals at phone widths. Desktop uses
     the larger ceremonial seal; here it scales with the smaller ornaments. */
  .seal {
    width: 60px;
    height: 60px;
    margin: -12px auto 10px;
    font-size: 50px;
  }
}

.eyebrow {
  font-size: 16px;
  letter-spacing: 0.55em;
  text-indent: 0.55em; /* balance the tracking */
  color: var(--gold-400);
  margin-bottom: 18px;
}

.names {
  /* the same 楷書 the 請柬 is written in, so the hero and the card speak in one
     hand rather than the hero in 宋 and the invitation in 楷 */
  font-family: var(--kai);
  font-size: clamp(25px, 7.4vw, 42px);
  line-height: 1.35;
  letter-spacing: 0.12em;
  /* Gold, but flat — the marker treatment belongs to the 請柬 alone, where the
     ink is the point. Here it would only compete with the 囍 above it.
     --gold-300 is the brightest of the golds (7.3:1 on the hero ground), so the
     names stay the dominant line while joining the palette. */
  color: var(--gold-300);
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.35);
}
.names .with {
  display: inline-block;
  font-size: 0.42em;
  color: var(--gold-400);
  margin: 0 0.5em;
  vertical-align: 0.45em;
  letter-spacing: 0.2em;
}

.hero .event {
  margin-top: 16px;
  font-size: clamp(22px, 5.2vw, 28px);
  letter-spacing: 0.4em;
  text-indent: 0.4em;
  color: var(--gold-300);
}

.hero .sub {
  margin-top: 8px;
  font-size: 16px;
  color: var(--ink-dim);
  letter-spacing: 0.3em;
  text-indent: 0.3em;
}

/* date + blessing chips */
.chips {
  margin-top: 26px;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
}
.chip {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  padding: 9px 18px;
  border: 1px solid rgba(208, 162, 78, 0.65);
  border-radius: 999px;
  font-size: 17px;
  letter-spacing: 0.18em;
  color: var(--ink);
  background: rgba(63, 10, 16, 0.35);
}
.chip b { color: var(--gold-300); font-weight: normal; }
.chip .dot { color: var(--gold-500); }

.blessing {
  margin-top: 22px;
  font-size: 18px;
  letter-spacing: 0.5em;
  text-indent: 0.5em;
  color: var(--gold-400);
}
.blessing::before,
.blessing::after {
  content: "❋";
  color: var(--gold-600);
  margin: 0 0.8em;
  font-size: 13px;
  vertical-align: 0.2em;
}

/* ---------- countdown ---------- */

.countdown {
  margin: 34px auto 0;
  display: flex;
  justify-content: center;
  gap: 10px;
}
.cd-cell {
  min-width: 70px;
  padding: 10px 8px 8px;
  text-align: center;
  border: 1px solid rgba(208, 162, 78, 0.55);
  border-radius: 8px;
  background: rgba(63, 10, 16, 0.35);
}
.cd-num {
  display: block;
  font-size: 30px;
  line-height: 1.2;
  color: var(--gold-300);
  font-variant-numeric: tabular-nums;
}
.cd-label {
  font-size: 14px;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  color: var(--ink-dim);
}
.countdown-caption {
  text-align: center;
  margin-top: 10px;
  font-size: 15px;
  letter-spacing: 0.35em;
  text-indent: 0.35em;
  color: var(--ink-dim);
}
.countdown.is-done { display: none; }

/* ---------- sections ---------- */

section { margin-top: 64px; }

.sec-title {
  text-align: center;
  font-size: 24px;
  letter-spacing: 0.5em;
  text-indent: 0.5em;
  color: var(--gold-300);
  margin-bottom: 28px;
}
/* Only sections with something specific to add carry a sub-line; the rest let the
   title stand. The negative margin lets one rule cover both cases without :has(). */
.sec-sub {
  text-align: center;
  font-size: 14px;
  letter-spacing: 0.4em;
  text-indent: 0.4em;
  color: var(--ink-dim);
  margin-top: -20px;
  margin-bottom: 28px;
}
.sec-title::after {
  content: "";
  display: block;
  width: 64px;
  height: 1px;
  margin: 14px auto 0;
  background: linear-gradient(90deg, transparent, var(--gold-500), transparent);
}

/* ---------- 請柬 paper scroll (vertical text) ---------- */

.paper {
  position: relative;
  font-family: var(--kai);
  background: linear-gradient(172deg, var(--card) 0%, var(--card-shade) 100%);
  color: var(--card-ink);
  border-radius: 6px;
  padding: 34px 24px;
  box-shadow:
    0 14px 44px rgba(0, 0, 0, 0.5),
    inset 0 0 0 1px rgba(243, 214, 139, 0.22);
}
.paper::before {
  content: "";
  position: absolute;
  inset: 9px;
  border: 1px solid var(--card-edge);
  border-radius: 3px;
  pointer-events: none;
}

/* Vertical text like classical Chinese: chars top→bottom, columns right→left.
   Each phrase is its own block and short enough to never wrap mid-column. */
.vtext {
  /* Safari only took these unprefixed in 14, and an older iPhone or Mac that
     ignores them lays the card out horizontally, or leaves text-orientation at
     its "mixed" default. The prefixed pair goes first so the standard one wins
     wherever both are understood. */
  -webkit-writing-mode: vertical-rl;
  -webkit-text-orientation: upright;
  writing-mode: vertical-rl;
  text-orientation: upright;
  height: clamp(330px, 58vh, 380px);
  margin: 0 auto;
  /* 4.3vw keeps all six columns inside the paper down to 320px screens;
     clamp() floor of 340px keeps columns from wrapping on short viewports */
  font-size: min(21px, 4.3vw);
  line-height: 1.9;
  letter-spacing: 0.28em;
}

/* ---------- 金色馬克筆 ----------
   Not foil. Foil is a mirror — it swings to white and needs a dark ground to
   read. A gold paint marker lays opaque mid-tone pigment, so it can sit on the
   card at one steady value, pool a little darker where the nib slows, and catch
   the light in small glints rather than one long sweep.

   Two things that are easy to get wrong here:

   · SCALE. The ramp must be smaller than a glyph. Put it on the block and each
     character samples one slice of it, so 朱 comes out all highlight and 麗 all
     shadow — variation between characters instead of along a stroke.

   · THE EDGE. background-clip:text makes the gold the BACKGROUND, and paint
     order is background -> text-shadow -> glyph. A text-shadow therefore lands
     on top of the gold and dims it; enough of it and the ink goes darker than
     its own card. drop-shadow() filters the rendered element, so the dark stays
     outside the stroke. */

.vtext {
  /* the value everything falls back to — see the @supports guard below */
  color: var(--card-ink);
}
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .vtext {
    background-image:
      /* pigment glints: pale champagne, never white, and sparse */
      radial-gradient(circle at 31% 27%, rgba(255, 238, 178, 0.5) 0 0.5px, transparent 1.1px),
      radial-gradient(circle at 68% 71%, rgba(246, 226, 156, 0.34) 0 0.4px, transparent 0.95px),
      /* the body of the ink: one gold, turning */
      linear-gradient(148deg, #dfbf72, #f4dfa2 42%, #e6cb84 72%, #d8b768);
    /* co-prime tiles for the glints; the body period stays under glyph size */
    background-size: 13px 15px, 19px 23px, 14px 14px;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    /* where the ink pools: one soft pass, downward, behind the gold */
    filter: drop-shadow(0 0.5px 0.5px rgba(70, 16, 22, 0.55));
  }
}

.vtext > span { display: block; }
.vtext > span + span { margin-block-start: 0.6em; } /* column gap (block axis = horizontal here) */
.vtext .card-addressee {
  margin-block-end: 0.35em;
  font-size: 1.14em;
  /* The face ships one weight, so 600 was a synthesised bold — it thickened the
     strokes unevenly and closed the counters. Size and tracking carry the
     emphasis instead, and the ink stays the same gold as the rest of the card. */
  font-weight: 400;
  letter-spacing: 0.34em;
}
.vtext .lead { margin-block-start: 1.2em; }          /* extra separation before the host columns */
/* One ink, one size, for everything on the card — both families and 誠邀 alike.
   Column order already says which family hosts; setting one of them, or the verb,
   quieter read as ranking them. */
/* nothing on the card sets its own colour — addressee, couple and both sets of
   parents all take the one gold above */
.vtext .hosts { color: inherit; }
.vtext .hosts em { font-style: normal; }
@media (max-width: 390px) {
  .vtext { letter-spacing: 0.22em; }
}

.paper .stamp {
  position: absolute;
  right: 20px;
  bottom: 20px;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: 2px solid rgba(243, 214, 139, 0.75);
  border-radius: 6px;
  color: rgba(243, 214, 139, 0.92);
  /* Noto's 囍 fills more of its em than the 楷書 it replaced, so the same 32px
     read heavier in the box; 28px sits it back down. The padding correction
     below is in em, so it follows this without retuning. */
  font-size: 28px;
  line-height: 1;
  background: rgba(243, 214, 139, 0.08);
}

/* sits on the red page, not on the ivory card — must use the page's ink */
.paper-note {
  text-align: center;
  margin-top: 20px;
  font-size: 15px;
  letter-spacing: 0.25em;
  color: var(--ink-dim);
}

/* ---------- 請柬 · shared shell ---------- */

.invite {
  position: relative;
  cursor: pointer;
}
.invite .paper { position: relative; z-index: 1; }

/* hidden until JS arms the animation, so no-JS users get the open card */
.invite .hint {
  position: absolute;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  z-index: 3;
  display: none;
  font-size: 14px;
  letter-spacing: 0.4em;
  text-indent: 0.4em;
  color: var(--gold-300);
  transition: opacity 0.46s ease 0.72s;
}
.invite.is-anim .hint { display: block; }
.invite.open .hint {
  opacity: 0;
  transition-delay: 0.16s;
}

/* ---------- 信封取柬 ----------

   Physical staging, one motion per stage: the 囍 seal breaks, the flap folds
   back and away from the viewer, the card rises out of the mouth, and the
   emptied envelope sinks away with the flap still attached to it.

   The headroom the card rises into is BELOW the card, not above: as padding-top
   it left a void between 請柬 and a sealed envelope, which is not what an
   envelope sitting on a page looks like. Below the card it is invisible while
   sealed (the envelope fills it) and collapses once the card has settled.

   One edge treatment throughout — a single gold hairline around the panel and
   along the flap's fold. The panel used to carry an inset double frame the flap
   had no way to match, so the two halves of one piece of paper did not agree.
   ------------------------------------------------------------------------ */

:root {
  --env-edge: rgba(230, 195, 122, 0.8);   /* every envelope edge, one colour */
  --env-radius: 6px;
}

/* Option C: a persistent sleeve moves down while the card rises slightly.
   Panel dimensions are measured from the card, never from animated padding. */
.invite-envelope.motion-c.is-anim {padding-top:100px;padding-bottom:96px;clip-path:none;transition:none}
.invite-envelope.motion-c .paper {transform:none;transition:none!important}
.invite-envelope.motion-c .envelope-front,
.invite-envelope.motion-c .envelope-flap-wrap {top:140px;bottom:auto;height:var(--sleeve-height);transition:none!important;opacity:1;transform:none}
.invite-envelope.motion-c .envelope-flap-wrap {z-index:4;perspective-origin:50% 0}
.invite-envelope.motion-c .envelope-flap {left:0.5px;right:0.5px;top:0.5px;transition:none!important;clip-path:none;background:none;filter:none}
.invite-envelope.motion-c .envelope-pocket {
  border-radius:0 0 var(--env-radius) var(--env-radius);
  box-shadow:0 22px 38px -14px rgba(0,0,0,.62),0 4px 10px -4px rgba(0,0,0,.4),inset 0 12px 18px -12px rgba(0,0,0,.5);
}
.invite-envelope.motion-c .envelope-flap::before,
.invite-envelope.motion-c .envelope-flap::after {display:none}
.invite-envelope.motion-c .envelope-flap svg {display:block;width:100%;height:100%;overflow:visible}
.invite-envelope.motion-c .envelope-xi {transition:none!important}
.invite-envelope.motion-c .hint {bottom:24px;transition:none}

/* Loose prints fan out below the opened envelope. On narrow screens they
   become a level strip that scrolls at the page edge. */
.envelope-photo {
  position: relative;
  width: min(760px, calc(100vw - 32px));
  margin: 24px auto;
}
.envelope-photo-track {
  position: relative;
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  align-items: center;
  justify-items: center;
  gap: 20px 14px;
  padding: 24px 8px 30px;
}
.envelope-photo-card {
  position: relative;
  grid-column: span 3;
  width: min(100%, 230px);
  padding: 8px 8px 26px;
  background: linear-gradient(115deg, #ad862d, #d3b35e 23%, #e7cd87 43%, #c5a04b 65%, #d8bb6d 83%, #b48e36);
  border: 1px solid #b38d39;
  box-shadow: 0 5px 14px #20070b66, inset 0 0 0 1px #f5df9b55;
  transform: translateY(14px) rotate(-7deg);
}
.envelope-photo-card:nth-child(2) { transform: translateY(-14px) rotate(5deg); }
.envelope-photo-card:nth-child(3) { transform: translateY(8px) rotate(-4deg); }
.envelope-photo-card:nth-child(4) { transform: translateY(-10px) rotate(5deg); }
.envelope-photo-card--landscape {
  width: min(100%, 320px);
}
.envelope-photo-card:nth-child(5) { transform: translateY(8px) rotate(-5deg); }
.envelope-photo-card:nth-child(6) { transform: translateY(-8px) rotate(5deg); }
.envelope-photo-card button {
  display: block;
  width: 100%;
  border: 0;
  padding: 0;
  background: none;
  cursor: zoom-in;
}
.envelope-photo-card button:focus-visible {
  outline: 2px solid var(--red-900);
  outline-offset: 3px;
}
.envelope-photo-card img {
  display: block;
  width: 100%;
  height: auto;
  filter: sepia(.17) saturate(.88) contrast(.94) brightness(1.05);
}
.envelope-photo-card--portrait img {
  aspect-ratio: 3 / 4;
  object-fit: cover;
  object-position: center 60%;
}
.envelope-photo-card::after {
  content: '';
  position: absolute;
  inset: 8px 8px 26px;
  background: linear-gradient(#ffe3af08, #af7f6308);
  pointer-events: none;
}
.envelope-photo-hint {
  display: none;
  margin-top: 2px;
  color: var(--gold-400);
  text-align: center;
  font-size: 12px;
  letter-spacing: .18em;
}
@media (max-width: 739px) {
  .envelope-photo { width: 100%; }
  .envelope-photo-track {
    display: flex;
    align-items: center;
    justify-items: normal;
    gap: 20px;
    overflow-x: auto;
    overflow-y: hidden;
    padding: 16px 20px 24px;
    scrollbar-width: thin;
    scrollbar-color: var(--gold-600) transparent;
    overscroll-behavior-x: contain;
    touch-action: auto;
  }
  .envelope-photo-track:focus-visible {
    outline: 2px solid var(--gold-300);
    outline-offset: -2px;
  }
  .envelope-photo-card,
  .envelope-photo-card:nth-child(2),
  .envelope-photo-card:nth-child(3),
  .envelope-photo-card:nth-child(4),
  .envelope-photo-card:nth-child(5),
  .envelope-photo-card:nth-child(6) {
    flex: 0 0 min(62vw, 230px);
    grid-column: auto;
    width: min(62vw, 230px);
    transform: none;
  }
  .envelope-photo-card.envelope-photo-card--landscape {
    flex-basis: min(85vw, 320px);
    width: min(85vw, 320px);
  }
  .envelope-photo-hint { display: block; }
}
.invite-envelope.motion-c .envelope-photo {
  position: absolute;
  top: 100px;
  left: 50%;
  translate: -50% 0;
  margin: 0;
  z-index: 2;
  opacity: 0;
  transform-origin: 50% 50%;
}

.photo-lightbox {
  position: fixed;
  max-width: min(94vw, 1500px);
  max-height: 94dvh;
  margin: auto;
  padding: 18px;
  border: 1px solid var(--gold-500);
  border-radius: 8px;
  background: #2b070e;
  box-shadow: 0 20px 70px rgba(0, 0, 0, .5);
  overflow: hidden;
}
.photo-lightbox[open] { display: grid; place-items: center; }
.photo-lightbox::backdrop {
  background: rgba(20, 2, 7, .88);
  backdrop-filter: blur(5px);
}
.photo-lightbox img {
  display: block;
  min-width: 0;
  min-height: 0;
  max-width: 100%;
  max-height: calc(94dvh - 42px);
  width: auto;
  height: auto;
  object-fit: contain;
}
.photo-lightbox-close {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 1;
  width: 40px;
  height: 40px;
  border: 1px solid var(--gold-500);
  border-radius: 50%;
  background: var(--red-900);
  color: var(--gold-300);
  font-size: 28px;
  line-height: 1;
  cursor: pointer;
}

.invite-envelope {
  /* An envelope is a hand-held object; at full section width it became a 624px
     near-square with the 龍鳳 stranded in the middle of it. */
  max-width: 460px;
  margin: 26px auto 0;
  /* generous overhead so the folded-back flap is never cut off — it may cross
     the heading for the second it is open */
  clip-path: inset(-45% -7% 0 -7%);
  transition: padding-bottom 1s cubic-bezier(0.3, 0.7, 0.25, 1);
}
.invite-envelope.is-anim { padding-bottom: 96px; }
.invite-envelope.settled { padding-bottom: 0; }

.invite-envelope .paper {
  z-index: 2;
  width: 96%;
  margin: 0 auto;
}

/* The envelope stands 40px above the card and fills the padding below it, so it
   is taller than the card on both edges and there is no empty band anywhere. */
.invite-envelope .envelope-flap-wrap,
.invite-envelope .envelope-front {
  position: absolute;
  left: -8px;
  right: -8px;
  top: 40px;
  bottom: 0;
  display: none;
  pointer-events: none;
}
.invite-envelope.is-anim .envelope-flap-wrap,
.invite-envelope.is-anim .envelope-front { display: block; }
.invite-envelope .envelope-front { z-index: 3; }
.invite-envelope .envelope-flap-wrap {
  z-index: 4;
  perspective: 1000px;
}

/* the panel: fully hides the sealed card behind it */
.envelope-pocket {
  position: absolute;
  inset: 0;
  border: 1px solid var(--env-edge);
  border-radius: var(--env-radius);
  background:
    /* Deep oxblood, well below the page ground. The card inside is now red too,
       and a red card drawn out of an equally red envelope is no reveal at all —
       so the sleeve goes darker than the page while the card sits brighter than
       it, and the 龍鳳 gain contrast instead of losing it. */
    linear-gradient(155deg, #5e1019, #3b0810);
  box-shadow:
    0 22px 38px -14px rgba(0, 0, 0, 0.62),
    0 4px 10px -4px rgba(0, 0, 0, 0.4),
    /* the mouth: light along the folded edge, shadow falling inward from it */
    inset 0 2px 0 rgba(243, 214, 139, 0.45),
    inset 0 14px 18px -10px rgba(0, 0, 0, 0.5);
}

/* the flap: hinged on the top edge, folded down over the panel when sealed.
   clip-path cuts through a border, so its fold edge is drawn as a shadow — same
   colour and weight as the panel's. */
.envelope-flap {
  position: absolute;
  display: block;
  inset: 0 0 auto;
  height: 38%;
  clip-path: polygon(0 0, 100% 0, 50% 100%);
  transform-origin: top center;
  background: rgba(230, 195, 122, 0.94);
  filter: none;
}
.envelope-flap::before {
  content: "";
  position: absolute;
  inset: 2px 2px auto;
  height: calc(100% - 4px);
  clip-path: inherit;
  background:
    linear-gradient(170deg, var(--red-700), var(--red-900));
  pointer-events: none;
}
.envelope-flap::after {
  content: "";
  position: absolute;
  inset: 3px 3px auto;
  height: calc(100% - 6px);
  clip-path: inherit;
  background: linear-gradient(155deg, rgba(246, 236, 212, 0.07), transparent 48%);
  pointer-events: none;
}

@media (max-width: 480px) {
  /* Keep the envelope's own outline visually separate from the fixed page
     frame; the desktop overhang remains unchanged. */
  .invite-envelope .envelope-flap-wrap,
  .invite-envelope .envelope-front {
    left: 2px;
    right: 2px;
  }
}

.envelope-creature {
  position: absolute;
  top: 50%;
  z-index: 4;
  width: 27%;
  height: 38%;
  object-fit: contain;
  opacity: 0.72;
}
.envelope-creature.side-l { left: 11%; }
.envelope-creature.side-r { right: 11%; }
.envelope-creature.flip { transform: scaleX(-1); }
.envelope-creature.ox-rabbit-envelope {
  left: 50%;
  width: 64%;
  transform: translateX(-50%);
}
.envelope-creature.ox-rabbit-envelope.equal-pair {
  top: 48%;
  width: 68%;
  height: 41%;
}
.page-gz .envelope-creature.ox-rabbit-envelope.gz-mirrored {
  transform: translateX(-50%) scaleX(-1);
}

/* the seal rides the flap's point — broken before the flap folds */
.envelope-xi {
  position: absolute;
  left: 50%;
  top: 38%;
  z-index: 5;
  transform: translate(-50%, -50%);
  display: grid;
  place-items: center;
  width: 68px;
  height: 68px;
  border-radius: 50%;
  color: var(--gold-300);
  font-size: 57px;
  line-height: 1;
  /* No padding nudge here. The 1px/3px that used to sit here was tuned to the
     old traced face and, with place-items:center, shifted the mark half its
     value — 0.5px left and 1.5px up — which read as the seal being lopsided
     even though the glyph itself is mirror-symmetric to 0.44%. */
  background:
    radial-gradient(75% 75% at 32% 26%, rgba(255, 236, 180, 0.18), transparent 62%),
    linear-gradient(155deg, var(--red-500), var(--red-700));
  box-shadow:
    inset 0 0 0 1px var(--env-edge),
    inset 0 0 0 4px var(--red-700),
    inset 0 0 0 5px rgba(230, 195, 122, 0.62),
    0 4px 14px rgba(0, 0, 0, 0.4);
}
.invite-envelope .hint {
  top: auto;
  bottom: 5%;
  z-index: 6;
  font-size: 18px;
  white-space: nowrap;
}

/* ---------- choreography ---------- */

/* stage 0 → 1: the seal breaks, then the flap folds back and away from the
   viewer (negative rotateX swings the apex away) */
.invite-envelope .envelope-xi {
  transition: transform 0.44s cubic-bezier(0.5, 0, 0.75, 0.4), opacity 0.44s ease 0.7s;
}
.invite-envelope.open .envelope-xi {
  transform: translate(-50%, -50%) scale(0.55);
  opacity: 0;
  transition-delay: 0s;
}
.invite-envelope.is-ready .envelope-flap {
  transition: transform 1.1s cubic-bezier(0.55, 0.06, 0.35, 1) 0.2s;
}
.invite-envelope.open .envelope-flap { transform: rotateX(-152deg); }

/* The flap does not fade on its own — it stayed open for a moment and then
   simply vanished. It is part of the envelope, so it leaves when the envelope
   leaves. All it does here is drop behind the card, once it is folded clear. */
.invite-envelope .envelope-flap-wrap { transition: z-index 0s linear 1.2s; }
.invite-envelope.open .envelope-flap-wrap {
  z-index: 1;
  transition: z-index 0s linear 1.2s;
}

/* stage 1 → 2: the card rises out of the mouth */
.invite-envelope.is-anim .paper { transform: translateY(82px) rotate(-0.6deg); }
.invite-envelope.is-ready .paper {
  transition: transform 1.44s cubic-bezier(0.3, 1.06, 0.35, 1) 0.84s;
}
.invite-envelope.open .paper { transform: none; }

/* stage 2 → 3: the emptied envelope sinks away, flap and all, together */
.invite-envelope .envelope-flap-wrap,
.invite-envelope .envelope-front {
  transition: transform 0.9s ease, opacity 0.7s ease;
}
.invite-envelope.open .envelope-flap-wrap,
.invite-envelope.open .envelope-front {
  transform: translateY(150px) rotate(0.6deg);
  opacity: 0;
  transition:
    transform 1.24s cubic-bezier(0.35, 0.02, 0.4, 1) 2.04s,
    opacity 0.84s ease 2.32s,
    z-index 0s linear 1.2s;
}

/* Closing has its own physical order: the open envelope returns beneath the
   card, the card slides inside, and only then does the flap fold shut. */
.invite-envelope.closing .envelope-flap-wrap,
.invite-envelope.closing .envelope-front {
  transform: none;
  opacity: 1;
  transition:
    transform 0.9s cubic-bezier(0.3, 0.75, 0.35, 1) 0.6s,
    opacity 0.55s ease 0.6s,
    z-index 0s linear;
}
.invite-envelope.closing .envelope-flap-wrap { z-index: 1; }
.invite-envelope.closing-card .paper {
  transform: translateY(82px) rotate(-0.6deg);
  transition: transform 1.15s cubic-bezier(0.55, 0, 0.7, 0.45);
}
.invite-envelope.closing .hint { opacity: 0; transition-delay: 0s; }

/* ---------- city divider / skyline ---------- */

.skyline {
  display: block;
  width: 100%;
  height: auto;
  margin-top: 26px;
  opacity: 0.95;
}

/* In an event hero the landmark identifies the city; it is not a second
   headline. Keep it close to the event title and deliberately understated. */
.page-bj .hero > .skyline {
  width: min(52%, 250px);
  margin: 16px auto 4px;
  opacity: 0.62;
}
.page-gz .hero > .skyline {
  width: min(26%, 116px);
  margin: 14px auto 2px;
  opacity: 0.62;
}

/* Hold each skyline's box before its SVG loads — without this the countdown and
   everything under it jump up the moment the file lands. Ratios come from the
   files' own viewBox (gz 262x322, bj 328x152). */
.page-gz .hero > .skyline { aspect-ratio: 262 / 322; }
.page-bj .hero > .skyline { aspect-ratio: 328 / 152; }

/* ---------- venue panel ---------- */

/* Sits inside 請柬, under the card. The date and time already live in the hero
   chips and the countdown, so this says *where* and offers the one action that
   follows from it — not a row of boxes restating the rest of the page. */
.venue {
  margin-top: 34px;
  border: 1px solid rgba(208, 162, 78, 0.5);
  border-radius: 8px;
  background: rgba(30, 6, 10, 0.45);
  padding: 32px 24px 28px;
  text-align: center;
}

/* 匾額 — the venue's name set in the invitation's own type and framed like the
   seals elsewhere on the page. Deliberately not a reproduction of its logo. */
.venue .marque {
  display: inline-block;
  padding: 10px 20px 8px;
  font-size: clamp(24px, 6vw, 30px);
  line-height: 1.25;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  color: var(--gold-300);
  background: linear-gradient(160deg, var(--red-600), var(--red-800));
  border: 1px solid var(--gold-500);
  border-radius: 5px;
  box-shadow:
    inset 0 0 0 3px var(--red-800),
    inset 0 0 0 4px rgba(169, 126, 47, 0.5),
    0 6px 22px rgba(0, 0, 0, 0.3);
}
.venue .store {
  margin-top: 16px;
  font-size: 21px;
  letter-spacing: 0.2em;
  text-indent: 0.2em;
  color: var(--ink);
}

/* The Guangzhou hall is part of the venue name, not a branch subtitle. */
.venue .addr span { white-space: nowrap; }

.venue .venue-name-line {
  padding-inline: 12px;
  font-size: clamp(14px, 4.3vw, 22px);
  letter-spacing: 0.08em;
  text-indent: 0.08em;
  white-space: nowrap;
}

.venue .addr {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid rgba(208, 162, 78, 0.22);
  font-size: 16px;
  line-height: 1.85;
  letter-spacing: 0.06em;
  color: var(--ink-dim);
}

.venue .when {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid rgba(208, 162, 78, 0.22);
  font-size: 17px;
  letter-spacing: 0.15em;
  color: var(--ink);
}
.venue .when b { color: var(--gold-300); font-weight: normal; }
/* date and time each stay whole; the line breaks between them, never inside 開席 */
.venue .when .d,
.venue .when b { display: inline-block; }
.venue .when small {
  display: block;
  margin-top: 4px;
  font-size: 14px;
  letter-spacing: 0.12em;
  color: var(--ink-dim);
}

/* the one action of this section — a pill, echoing the hero chips */
/* Sits under the map buttons, closing the block that tells a guest everything
   they need in order to turn up. Quieter than the address — it is an invitation,
   not a requirement — and tracked wide so the four characters read as a line
   rather than a label. */
.venue .dress {
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid rgba(208, 162, 78, 0.22);
  font-size: 15px;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  color: var(--gold-400);
}

.venue .go {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 22px;
}
.venue .go a {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 11px 18px;
  border: 1px solid rgba(208, 162, 78, 0.65);
  border-radius: 999px;
  background: rgba(63, 10, 16, 0.35);
  color: var(--gold-300);
  text-decoration: none;
  font-size: 16px;
  letter-spacing: 0.1em;
  transition: border-color 0.25s ease, background 0.25s ease;
}
.venue .go img {
  width: 20px;
  height: 20px;
  object-fit: contain;
  border-radius: 4px;
}
/* :hover is a lie on a touchscreen — it latches on tap and stays lit until you
   touch something else. Gate it on a real pointer and give phones :active
   instead, so the pill answers the finger and then lets go. */
@media (hover: hover) {
  .venue .go a:hover {
    border-color: var(--gold-300);
    background: rgba(63, 10, 16, 0.6);
  }
}
.venue .go a:active {
  border-color: var(--gold-300);
  background: rgba(63, 10, 16, 0.7);
}

.tbd {
  color: var(--gold-400);
  font-size: 0.85em;
  letter-spacing: 0.2em;
}

/* ---------- story timeline ---------- */

.timeline {
  position: relative;
  margin: 8px auto 0;
  max-width: 500px;
  padding-left: 30px;
}
.timeline::before {
  content: "";
  position: absolute;
  left: 7px;
  top: 8px;
  bottom: 8px;
  width: 1px;
  background: linear-gradient(180deg, transparent, var(--gold-600) 6%, var(--gold-600) 94%, transparent);
}
.tl-item {
  position: relative;
  padding: 0 0 30px 22px;
}
.tl-item:last-child { padding-bottom: 4px; }
.tl-item::before {
  content: "";
  position: absolute;
  left: -30px;
  top: 9px;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  border: 1px solid var(--gold-400);
  background: var(--red-800);
  box-shadow: inset 0 0 0 2px var(--red-800), inset 0 0 0 10px var(--gold-400);
}
.tl-item .when {
  font-size: 20px;
  letter-spacing: 0.12em;
  color: var(--gold-400);
}
.tl-item .what {
  font-size: 18px;
  margin-top: 4px;
  letter-spacing: 0.1em;
}
.tl-item .what small {
  display: block;
  font-size: 14px;
  color: var(--ink-dim);
  letter-spacing: 0.06em;
  margin-top: 4px;
}

.story-poem {
  margin-top: 34px;
  text-align: center;
  font-size: 18px;
  line-height: 2.4;
  letter-spacing: 0.3em;
  color: var(--ink);
}
.story-poem em {
  font-style: normal;
  color: var(--gold-300);
}
.story-poem .br-m { display: none; } /* mobile-only line breaks, see below */

/* ---------- photo gallery ---------- */

.photos {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px;
}
.photo {
  border: 1px solid rgba(208, 162, 78, 0.5);
  border-radius: 8px;
  padding: 10px;
  background: rgba(30, 6, 10, 0.45);
  text-align: center;
}
.photo--featured {
  grid-column: 1 / -1;
  padding: 12px;
  border-color: var(--accent);
  background: rgba(30, 6, 10, 0.72);
  box-shadow:
    inset 0 0 0 1px rgba(243, 214, 139, 0.16),
    0 12px 32px rgba(24, 3, 7, 0.28);
}
.photo .ph {
  aspect-ratio: 4 / 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: 1px dashed rgba(208, 162, 78, 0.55);
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.14);
}
.photo .ph span {
  color: var(--gold-400);
  font-size: 16px;
  letter-spacing: 0.25em;
  text-indent: 0.25em;
}
.photo .ph small {
  color: var(--ink-dim);
  font-size: 13px;
  letter-spacing: 0.1em;
}
.photo picture { display: block; }
.photo img {
  width: 100%;
  /* The width/height attributes on these <img>s are there to reserve the box
     before the file lands. They also make the used height definite, which
     switches aspect-ratio off and stretched every photo to a literal 900px —
     height:auto hands the ratio back the job of sizing the box. */
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 4px;
  display: block;
}
.photo .cap {
  margin-top: 10px;
  font-size: 16px;
  letter-spacing: 0.15em;
  color: var(--ink);
}
.photo .cap small {
  display: block;
  margin-top: 2px;
  font-size: 14px;
  letter-spacing: 0.1em;
  color: var(--ink-dim);
}
.photo.photo--featured .ph span,
.photo.photo--featured .cap {
  color: var(--gold-300);
  font-size: 20px;
}

/* ---------- gift note ---------- */

.gift {
  border: 1px solid rgba(208, 162, 78, 0.6);
  border-radius: 8px;
  padding: 30px 24px;
  text-align: center;
  background: rgba(30, 6, 10, 0.45);
}
.gift .headline {
  font-size: 26px;
  letter-spacing: 0.5em;
  text-indent: 0.5em;
  color: var(--gold-300);
}
.gift p {
  margin-top: 12px;
  font-size: 17px;
  color: var(--ink-dim);
  letter-spacing: 0.15em;
}
.gift p > span + span { margin-left: 1.4em; }
.gift .headline > span + span { margin-left: 0.9em; }
/* On a phone neither sentence fits comfortably. Break only at the intentional
   phrase boundary; the line break replaces the desktop's punctuation-free gap. */
@media (max-width: 480px) {
  .gift .headline > span,
  .gift p > span { display: block; }
  .gift p > span + span,
  .gift .headline > span + span { margin-left: 0; }
}

/* ---------- footer / cross links ---------- */

footer {
  margin-top: 72px;
  text-align: center;
}

.footer-close {
  margin-top: 14px;
  font-size: 17px;
  letter-spacing: 0.5em;
  text-indent: 0.5em;
  color: var(--gold-300);
}

.footer-names {
  margin-top: 42px;
  font-size: 16px;
  letter-spacing: 0.3em;
  color: var(--ink-dim);
}
.footer-囍 {
  margin-top: 18px;
  font-size: 34px;
  color: var(--gold-500);
  opacity: 0.9;
}

/* ---------- index page ---------- */

.page-soon main {
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding-top: 48px;
  padding-bottom: 48px;
}
.coming-soon {
  width: 100%;
  text-align: center;
}
.coming-soon .seal { margin-bottom: 28px; }
.coming-soon .eyebrow { margin-bottom: 22px; }
/* With 未完待續 removed the names are the page, so they take the display size —
   but a step below the old heading, since a name does not want to shout. */
.soon-names {
  /* gold, like the names on the two banquet pages — the ivory was the last
     white on the page and read as a different system */
  color: var(--gold-300);
  font-size: clamp(26px, 7.4vw, 40px);
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: 0.22em;
  text-indent: 0.22em;
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.28);
}
.soon-rule {
  width: 92px;
  height: 1px;
  margin: 28px auto 6px;
  background: linear-gradient(90deg, transparent, var(--gold-500), transparent);
}
.soon-copy {
  color: var(--ink-dim);
  font-size: clamp(15px, 4vw, 18px);
  line-height: 2;
  letter-spacing: 0.2em;
  text-indent: 0.2em;
}
.soon-creatures {
  display: block;
  width: min(230px, 58vw);
  margin: 24px auto 8px;
  opacity: 0.76;
}
.soon-close {
  color: var(--gold-400);
  font-size: 14px;
  letter-spacing: 0.5em;
  text-indent: 0.5em;
}
@media (max-width: 480px) {
  .coming-soon .seal { margin-bottom: 24px; }
  .soon-creatures { margin-top: 18px; }
}

.guest-dedication {
  margin: -2px auto 18px;
  color: var(--gold-300);
  text-align: center;
}
.guest-dedication span {
  margin-right: 0.8em;
  font-size: 13px;
  letter-spacing: 0.25em;
}
.guest-dedication strong {
  font-size: 19px;
  font-weight: normal;
  letter-spacing: 0.18em;
}

.city-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 18px;
  margin-top: 8px;
}
.city-card {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: var(--ink);
  background: rgba(30, 6, 10, 0.45);
  border: 1px solid rgba(208, 162, 78, 0.55);
  border-radius: 10px;
  padding: 28px 20px 0;
  overflow: hidden;
  transition: transform 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
}
@media (hover: hover) {
  .city-card:hover {
    transform: translateY(-3px);
    border-color: var(--gold-300);
    box-shadow: 0 16px 40px rgba(0, 0, 0, 0.35);
  }
}
/* the phone equivalent: the card presses in under the thumb rather than
   lifting, and springs back when released */
.city-card:active {
  transform: scale(0.985);
  border-color: var(--gold-300);
}
.city-card .tag {
  font-size: 14px;
  letter-spacing: 0.45em;
  text-indent: 0.45em;
  color: var(--gold-400);
}
.city-card .city {
  margin-top: 10px;
  font-size: 38px;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  color: var(--gold-300);
}
.city-card .date {
  margin-top: 6px;
  font-size: 17px;
  letter-spacing: 0.18em;
}
.city-card .motto {
  margin-top: 14px;
  font-size: 17px;
  letter-spacing: 0.4em;
  text-indent: 0.4em;
  color: var(--gold-400);
}
.city-card .skyline {
  width: calc(100% + 40px);
  height: 180px;
  margin: 18px -20px 0;
  object-fit: contain;
  object-position: center bottom;
}

.jing-guang {
  margin-top: 40px;
  text-align: center;
  font-size: 17px;
  letter-spacing: 0.28em;
  color: var(--ink-dim);
  line-height: 2.3;
}
.jing-guang em { font-style: normal; color: var(--gold-300); }

/* ---------- per-page accents ---------- */

/* Both banquet pages speak entirely in gold. The running copy used to sit in
   ivory, which read as a second, whiter ink beside the gold display text; and
   GZ borrowed an orange accent for its featured photo that no other element
   shared. Display text keeps --gold-300 (7.3:1 on the hero ground), running
   copy steps down to --gold-400 — two values of one gold, nothing else. */
.page-gz,
.page-bj {
  --accent: var(--gold-300);
  --ink: var(--gold-300);
  --ink-dim: var(--gold-400);
}

/* ---------- reveal animation ---------- */

.reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.9s ease, transform 0.9s ease;
}
.reveal.in { opacity: 1; transform: none; }
.reveal.d1 { transition-delay: 0.12s; }
.reveal.d2 { transition-delay: 0.24s; }

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  .invite-envelope,
  .invite-envelope .paper,
  .invite-envelope .envelope-xi,
  .invite-envelope .envelope-flap-wrap,
  .invite-envelope .envelope-flap,
  .invite-envelope .envelope-front { transition: none; }
  .city-card:hover,
  .city-card:active { transform: none; }
}

/* ---------- 龍鳳 flip: on pages where 麗鑫 leads, swap sides + mirror ---------- */

.decal.flip { transform: scaleX(-1); }

/* ---------- mobile hardening: no adversarial wrapping ---------- */

/* stack the names around 與 instead of letting the pair break mid-line */
@media (max-width: 400px) {
  .names .with {
    display: block;
    margin: 0.1em 0;
  }
}
@media (max-width: 480px) {
  /* countdown: 4 cells must fit side by side */
  .countdown { gap: 8px; }
  .cd-cell { min-width: 56px; }
  /* index city-card dates stay on one line */
  .city-card .date { font-size: 15px; letter-spacing: 0.1em; }
  /* poem: intentional breaks at the · instead of mid-phrase */
  .story-poem .br-m { display: block; }
  /* paper note lines are pre-split; just shrink tracking */
  .paper-note { font-size: 13px; letter-spacing: 0.15em; }
}
@media (max-width: 360px) {
  .city-card .date {
    white-space: nowrap;
    font-size: 14px;
    letter-spacing: 0.04em;
  }
  .chip {
    padding-right: 14px;
    padding-left: 14px;
    font-size: 15px;
    letter-spacing: 0.1em;
  }
}
/* Landscape phones. The clamp floor of 330px plus the paper's own padding makes
   a card ~400px tall, which does not fit a 390px-high screen — it ran off both
   edges at once. Shrink the type and the padding together so the whole 請柬 sits
   on screen, keeping the columns longer than the box so none of them wrap. */
@media (max-height: 480px) and (orientation: landscape) {
  .vtext {
    font-size: 15px;
    height: min(330px, 66vh);
  }
  .paper { padding: 20px 24px; }
  .paper .stamp {
    right: 14px;
    bottom: 14px;
    width: 34px;
    height: 34px;
    font-size: 22px;
  }
}

/* ============================================================
   背景音樂
   The invitation pages' one floating control.
   It has to be built from these pages' vocabulary rather than a media player's.
   The scrim and the hairline are .chip's values verbatim — a deep wine at 0.35
   under --gold-500 at 0.65 — because that is the material every translucent
   panel on these two pages is made of (.chip, .cd-cell, .venue), and the disc
   echoes .seal, the only other circle in the design.
   The first version borrowed .lang-switch's neutral black scrim and faint
   gold edge instead. That is the landing page's language, sitting on its
   browner ground, and on these pages it read as a control from another site.
   ============================================================ */

.music-toggle {
  position: fixed;
  /* Inside the 請柬 frame, not on it. body::after holds a fixed double hairline
     at inset 17px with an outline 5px outside that — so the decoration occupies
     roughly 12-18px from every edge, and 8-12px under 480px. An 18px corner sat
     the button directly on the line. 30/28 clears it at both sizes and lands in
     main's top padding, above the hero. */
  top: 30px;
  right: 28px;
  /* above the page content, below the frame at 40 — the 請柬 border is the one
     thing that should be able to draw over this */
  z-index: 20;
  /* 44px, the smallest a thumb hits reliably. The map links below are wide
     enough not to need the help; this one stands alone in the corner. */
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  appearance: none;
  cursor: pointer;
  border: 1px solid rgba(208, 162, 78, 0.65);
  /* 50% rather than the chip's 999px: the two are identical on a square, and
     this form matches .seal, which is the shape it is imitating */
  border-radius: 50%;
  background: rgba(63, 10, 16, 0.35);
  /* the resting note is --gold-500, so it reads as available rather than as an
     instruction; the playing state lifts it to --gold-300 below */
  color: var(--gold-500);
  transition: color 0.22s ease, border-color 0.22s ease,
              background 0.22s ease, box-shadow 0.22s ease;
}
/* The icon carries the state and js/main.js swaps the accessible name with it.
   A class rather than aria-pressed: the button's job is "play" or "pause", not
   "pressed or not", and stacking both is how toggle buttons end up announcing
   two contradictory things. */
.music-toggle .ico-on { display: none; }
.music-toggle.is-playing .ico-off { display: none; }
.music-toggle.is-playing .ico-on { display: block; }
.music-toggle.is-playing {
  color: var(--gold-300);
  border-color: rgba(230, 195, 122, 0.85);
  background: rgba(63, 10, 16, 0.5);
  /* .seal's glow, scaled from 92px to 44px. It also gives "playing" a cue that
     is not colour, which the two golds alone do not. */
  box-shadow: 0 0 18px rgba(230, 195, 122, 0.22);
}
.music-toggle:focus-visible {
  outline: 1px solid var(--gold-400);
  outline-offset: 2px;
}
@media (hover: hover) {
  .music-toggle:hover {
    color: var(--gold-300);
    border-color: rgba(230, 195, 122, 0.85);
  }
}

/* ============================================================
   中 / EN
   Both languages ship in the markup and CSS hides the inactive one, so the page
   is whole without JavaScript and a switch costs no round trip. The toggle's own
   labels carry lang= but not the data hooks, so they are never hidden.
   ============================================================ */

html[data-lang="zh"] [data-en],
html[data-lang="en"] [data-zh] { display: none; }

.lang-switch {
  position: absolute;
  top: 18px;
  right: 20px;
  z-index: 3;
  display: flex;
  gap: 2px;
  padding: 2px;
  border: 1px solid rgba(230, 195, 122, 0.34);
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.16);
}
.lang-switch button {
  appearance: none;
  border: 0;
  cursor: pointer;
  /* 40px of height minimum — at 5px padding these were 31px tall, below the
     size a thumb can reliably hit */
  padding: 10px 16px 11px;
  border-radius: 999px;
  background: transparent;
  /* the resting label is a dimmer gold rather than ivory, so the switch belongs
     to the same palette; the active one lifts to --gold-300 below */
  color: var(--gold-500);
  font-family: inherit;
  font-size: 12.5px;
  letter-spacing: 0.14em;
  line-height: 1.6;
  transition: color 0.22s ease, background 0.22s ease;
}
.lang-switch button[aria-pressed="true"] {
  background: rgba(230, 195, 122, 0.16);
  color: var(--gold-300);
}
@media (hover: hover) {
  .lang-switch button:not([aria-pressed="true"]):hover { color: var(--gold-400); }
}
.lang-switch button:focus-visible {
  outline: 1px solid var(--gold-400);
  outline-offset: 1px;
}
/* English sets at a smaller optical size than upright Chinese at the same px,
   and the wide tracking that suits 未 完 待 續 pulls Latin words apart. */
html[data-lang="en"] .soon-names { letter-spacing: 0.06em; text-indent: 0; }
html[data-lang="en"] .soon-close { letter-spacing: 0.22em; text-indent: 0.22em; }

/* ---------- the landing page is not a 答謝宴 ----------
   The banquet pages are deliberately saturated red; this one is the front door
   to the 2027 reception and is read in both languages, so it steps back to a
   warm ink and keeps red for the 囍 alone. The 牛兔 drawing is filled #e6c37a,
   so the ground has to stay dark enough to carry gold line art. */
body.page-soon {
  background:
    url("../assets/img/clouds.svg") repeat,
    radial-gradient(120% 90% at 50% 0%, #372b26 0%, #2b211d 44%, #201a17 76%, #171210 100%);
  background-size: 200px auto, cover;
}
body.page-soon::before { border-color: rgba(208, 162, 78, 0.5); }
body.page-soon::after  { border-color: rgba(230, 195, 122, 0.26); }

/* ---------- centring a lone 囍 ----------
   letter-spacing puts its space AFTER a character, so a single glyph in a
   centred box lands half a space to the left of centre — 0.5px here, which is
   exactly the drift that keeps being patched with padding nudges and keeps
   coming back when a font or size changes. Every 囍 on the site is one
   character in a centred box, so they all opt out of the inherited tracking
   rather than each carrying its own correction. */
.seal span,
.envelope-xi,
.paper .stamp,
.footer-囍 { letter-spacing: normal; }

/* ---------- hero balance on the two banquet pages ----------
   The 囍 leads and the names sit under it, rather than the pair competing.
   Scoped to .page-gz / .page-bj: the landing page's seal is sized against
   different neighbours and stays as it is. */
.page-gz .hero .seal,
.page-bj .hero .seal {
  width: 116px;
  height: 116px;
  /* The mark's circle is 0.88 of its em, so it read as only 62% of the disc at
     82px and left the seal looking oversized around it. */
  font-size: 104px;
  margin-bottom: 30px;
}
@media (max-width: 400px) {
  .page-gz .hero .seal,
  .page-bj .hero .seal { width: 104px; height: 104px; font-size: 93px; }
}

/* ---------- 囍 · the mark ----------
   Built by scripts/build-xi.mjs from measured geometry, then resolved into real
   outlines — the strokes unioned, the counters subtracted, the silhouette
   intersected — so it can ship as a one-glyph face rather than as a mask. That
   makes the 囍 ordinary text again: it takes colour, font-size and text-shadow
   like any character, needs no ::before, and is selectable and readable to a
   screen reader without a hidden duplicate behind it. 1.1KB. */
@font-face {
  font-family: "Xi Seal";
  src: url("../assets/fonts/xi-seal.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  /* NO unicode-range here, deliberately. It looks like an optimisation — the
     face holds one glyph — but it moves the mark. The strut that sets a line
     box's baseline is measured from the "first available font", and a font
     restricted by unicode-range does not apply to the strut, which has no
     character in it. So the baseline came from the FALLBACK (Songti on a Mac,
     Noto Serif here), and the mark hung wherever that font's ascent put it:
     0.933 of the box instead of 0.5, and different on each platform. Without
     the restriction this face is itself the first available font, the strut
     takes its symmetric 500/500, and the mark centres identically everywhere. */
}
/* The square stamp needs a symmetric 囍. This is not a stylistic preference: a
   楷書 囍 is calligraphy, so its two 喜 are drawn differently — LXGW's measures
   18.6% asymmetric about its own axis and every 楷 face tested was 19-21%, which
   in a square seal reads as crooked. Noto Sans CJK TC comes in at 2.4%.
   XI_SHAPE=square in scripts/build-xi.mjs also builds a geometric 囍 from the
   same strokes as the round seal (0.6%), kept as an alternative. */
@font-face {
  font-family: "Xi Noto";
  src: url("../assets/fonts/xi-noto.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  /* Same reason as above: the square stamp's baseline was coming from 楷書's
     metrics, not this face's. */
}
/* The round mark suits the round seals — the hero disc, the envelope's wax, the
   footer. It does NOT suit the 請柬's stamp, which is a SQUARE box: a circle
   inside a square leaves four awkward corners and reads as two shapes fighting.
   That one keeps the card's own 楷書, whose 囍 is square and sits in it. */
.seal span,
.envelope-xi,
.footer-囍 { font-family: "Xi Seal", var(--serif); }
.paper .stamp {
  font-family: "Xi Noto", var(--kai);
  /* Noto's 囍 sits ON its baseline the way ordinary text does, rather than being
     centred about it like the seal face, so it needs a small lift. This is the
     glyph's own design, not a platform difference — the baseline itself is now
     fixed by the @font-face above, so this number is stable across engines
     rather than compensating for whichever fallback happened to be in play. */
  padding-bottom: 0.125em;
}
