/* ============================================================
   Zi Yun & Jet — Wedding Invitation
   Direction: "Garden at golden hour" — warm ivory ground,
   eucalyptus-sage signature accent, photographs that dissolve
   into the page like light through tulle.

   tokens → reset → frame → type → photo + dissolve → motion →
   Hero → Welcome → 她/他 → Details → RSVP
   ============================================================ */

:root {
  /* Color — drawn from the garden photos: ivory light, eucalyptus
     foliage, warm ink, a whisper of champagne.
     Colours that also appear as a translucent tint are stored as
     space-separated channels (`--*-rgb`) and composed with
     rgb(var(--*-rgb) / a), so every alpha use tracks its parent
     colour instead of being re-typed as a hand-tuned literal. */
  --ink-rgb:       59 52 43;     /* primary text, brush ink */
  --sage-rgb:      140 154 130;  /* SIGNATURE accent — garden green */
  --shadow-rgb:    91 74 52;     /* warm-brown elevation (surround + photo lift) */

  --porcelain:  #faf7f1;   /* page ground (also the dissolve target) */
  --ivory:      #f4eee4;   /* desktop surround / soft panels */
  --ivory-deep: #ece4d6;   /* photo placeholder / faint fills */
  --ink:        rgb(var(--ink-rgb));   /* primary text (opaque) */
  --ink-soft:   #6b6053;   /* body / verse */
  --muted:      #7c6f5b;   /* captions, labels (AA: 4.59:1 on porcelain) */
  --sage:       rgb(var(--sage-rgb));   /* SIGNATURE accent (decorative use) */
  --sage-deep:  #5e6b55;   /* functional green text + CTA label (AA: 5.29:1) */
  --champagne:  #c2a36b;   /* gold: the wedding-day calendar marker */
  --rose:       #e0879c;   /* used once: the hero love-mark (heart between the names) */
  --line:       #d8cfc0;   /* hairlines */
  --alert-rgb:  167 64 56;   /* warm error red — RSVP submit-time validation (nudge text + invalid border/ring). 5.30:1 on the --ivory box / 5.72:1 on porcelain → AA for the .82rem nudge; still clearly red, distinct from hero-only --rose */
  --alert:      rgb(var(--alert-rgb));

  /* Atmospheric washes — the radial grounds on .page (mobile) and body (≥540px).
     Channels, per the convention above, so each gradient's fade-out stop is the
     SAME colour at alpha 0 — rgb(var(--*-rgb) / 0) — instead of a re-typed rgba
     literal, and never `transparent` (which fades toward black and muddies). */
  --glow-rgb:     253 252 248;   /* lit centre — top highlight */
  --hem-rgb:      246 240 230;   /* warm hem — lower edge */
  --surround-rgb: 247 236 217;   /* golden-hour surround (≥540px) */

  --serif: "EB Garamond", Georgia, "Times New Roman", serif;
  --zh:    "Noto Serif SC", "Songti SC", serif;
  --brush: "Ma Shan Zheng", "Noto Serif SC", cursive;

  /* Type scale — shared sizes so sibling roles hold one value instead of drifting
     into near-duplicates. Sizes that live as literals elsewhere are deliberate
     single-use one-off tiers — e.g. the hero title, the 她/他 watermark, the
     我们/婚礼见 sign-off, 07/30, the countdown, the .caps label, the schedule
     time — each used once at its own scale, so a token would add
     indirection without removing a duplicate. */
  --fs-title: clamp(2.4rem, 11vw, 3.2rem);   /* chapter titles: 一起走过 · 等你 · 全心都是你 */
  --fs-small: .95rem;                         /* secondary supporting text — calendar, venue, schedule, attire, RSVP card */
  --fs-verse: clamp(1.08rem, 4.6vw, 1.32rem); /* the couple's bilingual prose anchor — the welcome blessing sits at it; the 她/他 profiles derive from it below; the RSVP couplet is a larger one-off */
  --fs-profile:    calc(var(--fs-verse)   * 0.9);   /* 她/他 prose — a step below the welcome blessing so it leads; the role + verse share it (Latin) */
  --fs-profile-zh: calc(var(--fs-profile) * 0.9);   /* the zh optical trim — Noto Serif SC reads larger than EB Garamond at one px, so the zh role + verse drop a further 0.9 */

  --maxw: 480px;
  --pad:  clamp(1.5rem, 7vw, 2.4rem);
  --gut:  clamp(3.5rem, 12vw, 5.5rem);   /* vertical rhythm BETWEEN sections (section padding) */

  /* Vertical-rhythm scale — the fluid gaps WITHIN a section, kept as one ladder
     so a given beat holds a single value instead of drifting into near-duplicate
     clamps (mirrors the type scale above). Tighter step = closer grouping. Layout
     one-offs stay literal: the section --gut, the -1.4rem hero-name tuck, the 她/他
     paddings, the collage gutter, and the small fixed label/caption gaps. The one
     small gap that repeats across
     sections — the title↔eyebrow bond — is tokenised (--space-bond); the rest
     stay literal. */
  --space-xs: clamp(1.1rem, 4.5vw, 1.7rem);   /* floret opener → chapter title */
  --space-sm: clamp(1.4rem, 6vw,   2rem);     /* tight pair: calendar ↔ countdown */
  --space-md: clamp(1.8rem, 7vw,   2.6rem);   /* standard gap: eyebrow → content, photo edge */
  --space-lg: clamp(2.2rem, 8.5vw, 3.1rem);   /* medium break: hero figure, welcome quote, countdown → schedule */
  --space-xl: clamp(2.6rem, 10vw,  3.4rem);   /* major break: schedule / deadline / verse → next */

  --space-bond: .6rem;   /* fixed tight bond pairing a brush title with its adjacent eyebrow/caption line (hero + three chapters) and each blessing line↔its translation */

  --countdown-gap: clamp(.6rem, 4vw, 1.4rem);   /* horizontal gap between countdown units, reused as each divider's padding-left so the rule sits centred between two numbers — the two MUST stay equal, so they share one token */

  --ease: cubic-bezier(.22,.7,.2,1);

  --radius-photo: 3px;                                          /* framed (non-dissolving) photo corner */
  --radius-collage: 5px;                                        /* the gallery's softer corner — a touch rounder than a framed photo */
  --shadow-photo: 0 .6rem 1.6rem rgb(var(--shadow-rgb) / .08);  /* raised photo card */

  /* Dissolve mask: a smoothstep-eased fade sampled every 1% so the ramp reads
     as continuous (no Mach banding) with no hard edge. Symmetric — clear core
     #000 from 20% to 80%, equal feather top (0–20%) and bottom (80–100%). */
  --dissolve-mask: linear-gradient(to bottom,
    transparent       0%,
    rgba(0,0,0,.007)  1%,
    rgba(0,0,0,.028)  2%,
    rgba(0,0,0,.061)  3%,
    rgba(0,0,0,.104)  4%,
    rgba(0,0,0,.156)  5%,
    rgba(0,0,0,.216)  6%,
    rgba(0,0,0,.282)  7%,
    rgba(0,0,0,.352)  8%,
    rgba(0,0,0,.425)  9%,
    rgba(0,0,0,.5)   10%,
    rgba(0,0,0,.575) 11%,
    rgba(0,0,0,.648) 12%,
    rgba(0,0,0,.718) 13%,
    rgba(0,0,0,.784) 14%,
    rgba(0,0,0,.844) 15%,
    rgba(0,0,0,.896) 16%,
    rgba(0,0,0,.939) 17%,
    rgba(0,0,0,.972) 18%,
    rgba(0,0,0,.993) 19%,
    #000             20%,
    #000             80%,
    rgba(0,0,0,.993) 81%,
    rgba(0,0,0,.972) 82%,
    rgba(0,0,0,.939) 83%,
    rgba(0,0,0,.896) 84%,
    rgba(0,0,0,.844) 85%,
    rgba(0,0,0,.784) 86%,
    rgba(0,0,0,.718) 87%,
    rgba(0,0,0,.648) 88%,
    rgba(0,0,0,.575) 89%,
    rgba(0,0,0,.5)   90%,
    rgba(0,0,0,.425) 91%,
    rgba(0,0,0,.352) 92%,
    rgba(0,0,0,.282) 93%,
    rgba(0,0,0,.216) 94%,
    rgba(0,0,0,.156) 95%,
    rgba(0,0,0,.104) 96%,
    rgba(0,0,0,.061) 97%,
    rgba(0,0,0,.028) 98%,
    rgba(0,0,0,.007) 99%,
    transparent     100%);

  /* Paper grain (--paper): faint warm grain for fine-stationery tactility —
     feTurbulence mapped to sparse specks on transparent, so it carries its own
     alpha (no blend mode needed) and tiles over the page/body grounds. The
     eucalyptus floret opener is defined inline on .floret further down. */
  --paper:  url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20150%20150%22%20width%3D%22150%22%20height%3D%22150%22%3E%3Cfilter%20id%3D%22n%22%3E%3CfeTurbulence%20type%3D%22fractalNoise%22%20baseFrequency%3D%220.9%22%20numOctaves%3D%222%22%20stitchTiles%3D%22stitch%22%2F%3E%3CfeColorMatrix%20type%3D%22matrix%22%20values%3D%220%200%200%200%200.231%20%200%200%200%200%200.204%20%200%200%200%200%200.169%20%200%200%200%200.55%20-0.34%22%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D%22150%22%20height%3D%22150%22%20filter%3D%22url(%23n)%22%2F%3E%3C%2Fsvg%3E");
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
/* Root-level guard so the off-page brush bleed can never cause horizontal
   scroll, including on Safari < 16 / iOS 15 where overflow:clip is unsupported. */
html, body { overflow-x: hidden; overflow-x: clip; }

/* hide scrollbar, keep scrolling */
html { scrollbar-width: none; }
body { -ms-overflow-style: none; }
::-webkit-scrollbar { width: 0; height: 0; display: none; }

html { scroll-behavior: smooth; }

/* Kill the mobile tap-highlight flash on every clickable item — the grey/blue box
   iOS/Android paint over a tapped link, button, or form control (the RSVP fields and the
   radio/checkbox option labels). -webkit- prefixed for the iOS 15 / Safari 15 floor; the
   property inherits but only renders on tappable elements. */
a, button, input, label { -webkit-tap-highlight-color: transparent; }

/* Keyboard focus ring (quality floor) — shown on every link; the form controls further
   down (.rsvp__*, etc.) set their own tighter rings. */
a:focus-visible { outline: 2px solid var(--sage-deep); outline-offset: 3px; border-radius: 2px; }

body {
  font-family: var(--zh);
  font-weight: 300;
  color: var(--ink);
  line-height: 1.9;
  background: var(--ivory);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* EB Garamond defaults to old-style (descending) figures; force lining figures
     page-wide so the calendar, countdown, dates and times read as one even height.
     Inherited, so it reaches the class-less calendar cells too; a no-op on the
     Chinese fonts, which have no old-style set. */
  font-variant-numeric: lining-nums;
}

/* ── Frame: the column sits as a lit panel on the warm ground ── */
.page {
  position: relative;
  max-width: var(--maxw);
  margin: 0 auto;
  min-height: 100vh;
  overflow-x: clip;
  /* Paper grain rides on top as a tiling layer (it carries its own faint alpha,
     so no blend mode is needed and photos — which paint above the background —
     are never touched); the two radial washes give the column its lit centre
     and warm hem, over the porcelain ground. */
  background-color: var(--porcelain);
  background-image:
    var(--paper),
    radial-gradient(135% 48% at 50% -6%, rgb(var(--glow-rgb)) 0%, rgb(var(--glow-rgb) / 0) 62%),
    radial-gradient(120% 60% at 50% 108%, rgb(var(--hem-rgb)) 0%, rgb(var(--hem-rgb) / 0) 55%);
  background-repeat: repeat, no-repeat, no-repeat;
  background-size: 150px 150px, 100% 100%, 100% 100%;
}
@media (min-width: 540px) {
  body {
    padding: 0;
    /* Golden-hour surround: one warm wash over the ivory so the lit column
       reads as resting on warm stationery (only the margins show on wide
       screens). Same grain keeps the whole surface one material. Fixed so the
       wash stays viewport-anchored down the long page instead of sliding off. */
    background-image:
      var(--paper),
      radial-gradient(125% 80% at 50% -6%, rgb(var(--surround-rgb)) 0%, rgb(var(--surround-rgb) / 0) 60%);
    background-repeat: repeat, no-repeat;
    background-size: 150px 150px, 100% 100%;
    background-attachment: fixed;
  }
  .page { box-shadow: 0 1.5rem 5rem rgb(var(--shadow-rgb) / .1), 0 .25rem 1rem rgb(var(--shadow-rgb) / .05); }
}

section { padding: var(--gut) var(--pad); }

/* ── Type & utility helpers ── */
.brush { font-family: var(--brush); font-weight: 400; color: var(--ink); line-height: 1.05; }
.serif { font-family: var(--serif); font-weight: 400; }
.serif-i { font-family: var(--serif); font-style: italic; font-weight: 400; }
.zh { font-family: var(--zh); font-weight: 300; color: var(--ink-soft); }
.caps {
  font-family: var(--serif);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .26em;
  font-size: .72rem;
  color: var(--muted);
}

/* Screen-reader-only text (e.g. "opens in a new tab" cues) */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Song-title links — each chapter's brush title + eyebrow are wrapped in one link to the
   song (shared by Welcome / Details / RSVP). */
.song-link { display: block; color: inherit; text-decoration: none; }
.song-link h2, .song-link p { transition: color .2s var(--ease); }
.song-link:hover h2, .song-link:hover p { color: var(--sage-deep); }

/* ── Photo + the signature dissolve ── */
.photo {
  position: relative;
  display: block;
  margin: 0;
  overflow: hidden;
  background: var(--ivory-deep);
}
.photo::after {            /* placeholder label, only visible if img is missing */
  content: attr(data-label);
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--muted);
  font-family: var(--serif); font-style: italic; letter-spacing: .08em; font-size: .8rem;
}
.photo img {
  position: relative; z-index: 1;
  width: 100%; height: 100%;
  object-fit: cover; object-position: center; display: block;
}

/* Dissolve: feather the photo into the porcelain ground. The mask makes pixels
   transparent, revealing the page beneath. The fade is vertical only (to bottom)
   so the sides stay crisp to the column edges while top/bottom melt into ivory.
   Every dissolving photo (hero, 她/他 portraits, RSVP) shares one eased mask. */
.dissolve {
  background: transparent;
  -webkit-mask-image: var(--dissolve-mask);
          mask-image: var(--dissolve-mask);
}
.dissolve::after { display: none; }   /* never label a dissolving frame */

/* ── Botanical accent (eucalyptus) ──
   Purely decorative, aria-hidden. A floret — two stems meeting at a seed, a
   quiet union mark — opens the three centred "chapters" (Welcome, Details,
   RSVP). The 她/他 interlude keeps its own brush watermarks, so the motif
   never competes with itself. */
.floret {
  display: block; margin: 0 auto var(--space-xs);
  width: clamp(96px, 30vw, 124px); aspect-ratio: 200 / 36;
  background: center / contain no-repeat url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20200%2036%22%20width%3D%22200%22%20height%3D%2236%22%20fill%3D%22none%22%3E%3Cdefs%3E%3Cpath%20id%3D%22l%22%20d%3D%22M0%2C-1%20C5.4%2C-3.2%206.2%2C-9.8%200%2C-15%20C-6.2%2C-9.8%20-5.4%2C-3.2%200%2C-1%20Z%22%2F%3E%3C%2Fdefs%3E%3Cg%20fill%3D%22%238C9A82%22%20fill-opacity%3D%220.46%22%3E%3Cg%20transform%3D%22translate(14%206)%22%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(14.41%2020.41)%20rotate(50.03)%20scale(0.92)%22%2F%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(14.41%2020.41)%20rotate(130.03)%20scale(0.92)%22%2F%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(36.27%2019.3)%20rotate(49.03)%20scale(0.77)%22%2F%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(36.27%2019.3)%20rotate(129.03)%20scale(0.77)%22%2F%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(59.35%2017.02)%20rotate(48.03)%20scale(0.61)%22%2F%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(59.35%2017.02)%20rotate(128.03)%20scale(0.61)%22%2F%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(82.63%2015.14)%20rotate(47.19)%20scale(0.46)%22%2F%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(82.63%2015.14)%20rotate(127.19)%20scale(0.46)%22%2F%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(86%2015)%20rotate(87.95)%20scale(0.37)%22%2F%3E%3C%2Fg%3E%3Cg%20transform%3D%22translate(186%206)%20scale(-1%201)%22%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(14.41%2020.41)%20rotate(50.03)%20scale(0.92)%22%2F%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(14.41%2020.41)%20rotate(130.03)%20scale(0.92)%22%2F%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(36.27%2019.3)%20rotate(49.03)%20scale(0.77)%22%2F%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(36.27%2019.3)%20rotate(129.03)%20scale(0.77)%22%2F%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(59.35%2017.02)%20rotate(48.03)%20scale(0.61)%22%2F%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(59.35%2017.02)%20rotate(128.03)%20scale(0.61)%22%2F%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(82.63%2015.14)%20rotate(47.19)%20scale(0.46)%22%2F%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(82.63%2015.14)%20rotate(127.19)%20scale(0.46)%22%2F%3E%3Cuse%20href%3D%22%23l%22%20transform%3D%22translate(86%2015)%20rotate(87.95)%20scale(0.37)%22%2F%3E%3C%2Fg%3E%3C%2Fg%3E%3Cpath%20d%3D%22M18%2C26%20C42%2C28%2072%2C22%20100%2C21%22%20stroke%3D%22%238C9A82%22%20stroke-opacity%3D%220.4%22%20stroke-width%3D%22.9%22%20stroke-linecap%3D%22round%22%2F%3E%3Cpath%20d%3D%22M182%2C26%20C158%2C28%20128%2C22%20100%2C21%22%20stroke%3D%22%238C9A82%22%20stroke-opacity%3D%220.4%22%20stroke-width%3D%22.9%22%20stroke-linecap%3D%22round%22%2F%3E%3Ccircle%20cx%3D%22100%22%20cy%3D%2221%22%20r%3D%221.7%22%20fill%3D%22%238C9A82%22%20fill-opacity%3D%220.55%22%2F%3E%3C%2Fsvg%3E");
}

/* ── Motion ── */
/* Hidden states only apply once JS confirms it's running (html.js),
   so the page is fully legible with scripting disabled. */
.js .reveal { opacity: 0; transform: translateY(26px); }
.reveal { transition: opacity 1.05s var(--ease), transform 1.05s var(--ease); }
.reveal.is-visible { opacity: 1; transform: none; }

/* Hero load sequence */
.js .hero__eyebrow,
.js .hero__title,
.js .hero__caption,
.js .hero__figure { opacity: 0; transform: translateY(18px); }
.hero__eyebrow, .hero__title, .hero__caption,
.hero__figure {
  transition: opacity 1s var(--ease), transform 1s var(--ease);
}
.is-loaded .hero__eyebrow { opacity: 1; transform: none; transition-delay: .15s; }
.is-loaded .hero__title   { opacity: 1; transform: none; transition-delay: .32s; }
.is-loaded .hero__caption { opacity: 1; transform: none; transition-delay: .62s; }
.is-loaded .hero__figure  { opacity: 1; transform: none; transition-delay: .80s; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .js .reveal,
  .js .hero__eyebrow, .js .hero__title, .js .hero__caption,
  .js .hero__figure {
    opacity: 1; transform: none; transition: none;
  }
  /* the hero heartbeat is movement too, so neutralize it (the button keeps its
     colour/shadow hover feedback, which isn't motion). */
  .hero__heart { animation: none; }
  /* the verse-toggle pill slide is motion too */
  .lang-toggle__pill { transition: none; }
}

@media (prefers-reduced-motion: no-preference) {
  .lang-toggle__pill { transition: transform .35s ease; }
}

/* ============================================================
   Hero
   ============================================================ */
.hero { text-align: center; }   /* top/bottom padding inherits the section --gut rhythm */
.hero__eyebrow {
  font-size: 1.35rem; letter-spacing: .55em; padding-left: .55em;
  margin: 0 0 var(--space-bond); color: var(--sage-deep);   /* float the kicker above the brush title; same bond as the chapter title↔eyebrow gap */
}
.hero__title {
  font-size: clamp(4rem, 23vw, 7rem);
  margin: 0; color: var(--ink);
  text-shadow: 0 .08em .5em rgb(var(--ink-rgb) / .06);
}
.hero__caption { margin: var(--space-bond) 0 0; }   /* title↔caption bond (matches the chapter title↔eyebrow gap); size + colour inherited from .caps */
.hero__figure { margin: var(--space-lg) 0 0; }
.hero__figure .photo {
  width: auto; aspect-ratio: 3 / 4.15;
  margin-inline: calc(-1 * var(--pad));   /* bleed to the full column width */
}
.hero__names {
  display: grid;
  grid-template-columns: 1fr auto 1fr;   /* 袁梓芸 | ❤ | 简业缙; equal outer columns put the centre column — the heart — on the section axis */
  align-items: center;       /* each item centres in its row; the heart spans both rows (see below) */
  justify-items: center;     /* each romanization centres under its name */
  row-gap: .5rem;            /* brush line → romanization */
  margin: -1.4rem 0 0;       /* tuck up into the dissolved hem */
  position: relative; z-index: 2;
  font-size: 1.85rem; color: var(--ink); letter-spacing: .02em;
}
.hero__heart { grid-column: 2; grid-row: 1 / 3; display: inline-block; width: .58em; margin: 0 .3em; color: var(--rose); animation: heartbeat 1.4s ease-in-out infinite; }
.hero__heart svg { display: block; width: 100%; height: auto; fill: currentColor; }
/* Zi Yun auto-places into column 1 (under 袁梓芸); Jet is pinned to column 3
   (under 简业缙). The heart owns the centre column across both rows, so it
   sits vertically centred between the brush names and the romanizations. */
.hero__name-en--him { grid-column: 3; }
/* a single soft beat, then a rest. transform-only — no reflow,
   and disabled for prefers-reduced-motion (see the motion block above). */
@keyframes heartbeat {
  0%   { transform: scale(1); }
  15%  { transform: scale(1.12); }   /* beat */
  35%  { transform: scale(1); }
  100% { transform: scale(1); }      /* rest */
}

/* ============================================================
   Welcome
   ============================================================ */
.welcome { text-align: center; }
/* Header + eyebrow match the Details / RSVP chapters: a brush Chinese title
   over a caps English eyebrow, at the same scale and gaps. */
.welcome__title { font-size: var(--fs-title); margin: 0; color: var(--ink); }
.welcome__eyebrow { margin: var(--space-bond) 0 var(--space-md); }
/* The bilingual blessing, interleaved as translation-pairs: each Chinese line is
   shadowed by its English just beneath it — a tight bond within a pair, an open
   gap between the two pairs. Both share the same size and warm-soft tone, so
   neither out-shouts the other; only the script and leading differ. */
.welcome__blessing { margin: 0 0 var(--space-lg); }
.welcome__zh,
.welcome__en {
  color: var(--ink-soft); font-size: var(--fs-verse);
  text-wrap: balance;
}
.welcome__zh { margin: 0 0 var(--space-bond); line-height: 1.95; letter-spacing: .03em; }   /* tight bond down to its English */
.welcome__en { margin: 0 0 var(--space-xs);   line-height: 1.7;  letter-spacing: .01em; }   /* beat to the next pair */
.welcome__en:last-child { margin-bottom: 0; }

.collage {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(.55rem, 2.2vw, .8rem);
}
/* Wide establishing shots span the row, the two portraits sit between; a
   slightly softer corner than the framed photos elsewhere, no border. */
.collage .photo { border-radius: var(--radius-collage); }
.collage__a, .collage__d { grid-column: 1 / -1; aspect-ratio: 16 / 10; }
.collage__b, .collage__c { aspect-ratio: 3 / 4; }

/* ============================================================
   她 & 他
   ============================================================ */
.profiles { padding-top: 0; padding-bottom: var(--gut); }
.profile { position: relative; padding: clamp(2.5rem, 9vw, 3.8rem) 0; }
.profile + .profile { padding-top: clamp(1rem, 4vw, 2rem); }

.profile__brush {
  position: absolute; top: -.18em; z-index: 0;
  font-size: clamp(7.5rem, 42vw, 13rem); line-height: .8;
  color: rgb(var(--sage-rgb) / .22);   /* translucent sage watermark */
  pointer-events: none; -webkit-user-select: none; user-select: none;   /* prefix for the iOS 15 / Safari 15 floor, as with mask-image above */
}
.profile--her .profile__brush { left: -.06em; }
.profile--him .profile__brush { right: -.06em; }

.profile__body { position: relative; z-index: 1; padding-top: clamp(3.2rem, 16vw, 5rem); }
.profile--her .profile__body { text-align: left; }
/* 他's lower-right hook descends further than 她's strokes, and the right-aligned
   name sits on the same side as the brush — so the 他 body needs extra top
   clearance for "Groom / Jet" to sit cleanly below the character, like 她. */
.profile--him .profile__body { text-align: right; padding-top: clamp(4.8rem, 26vw, 7.5rem); }

/* The role shares the verse's size (--fs-profile), keeping .caps's uppercase + .26em
   tracking and medium 500 weight — only the size is taken over from .caps's .72rem. */
.profile__role { margin: 0 0 1.1rem; color: var(--ink); font-size: var(--fs-profile); }
.profile__slash { color: var(--sage); margin: 0 .35em; font-weight: 400; }

.profile__verse { font-size: var(--fs-profile); line-height: 2.15; color: var(--ink-soft); }
.profile__verse p { margin: 0; }
.profile__verse p.profile__verse-break { margin-top: 1.4em; }

.profile__photo {
  position: relative; z-index: 1;
  width: auto; aspect-ratio: 3 / 4;
  margin-block: var(--space-md) 0;
  margin-inline: calc(-1 * var(--pad));   /* full-bleed to the column edges */
}

/* ── Language toggle (中 / EN) ──────────────────────────────
   Both verses live in the static DOM. With NO JS the switch is hidden and both
   verses show stacked (no-JS legible). Under JS, Chinese is the resting state and
   the EN verse is hidden — gated on html.js so there is no flash before first paint
   (.profiles has no data-lang yet → EN hidden). display:none also drops the inactive
   verse from the a11y tree, so screen readers announce only the active language. */
.lang-toggle { display: none; }   /* no-JS: hidden; both verses stack */
html.js .lang-toggle {
  display: grid;
  grid-template-columns: 1fr 1fr;   /* two equal segments → the pill slides cleanly */
  position: relative;
  width: max-content;
  margin: 0 auto var(--space-md);   /* centred above both profiles */
  border: 1px solid rgb(var(--sage-rgb) / .35);
  border-radius: 999px;
  overflow: hidden;                 /* clip the pill to the rounded track */
}

.lang-toggle__opt {
  position: relative; z-index: 1;   /* labels sit above the pill */
  -webkit-appearance: none; appearance: none;   /* -webkit- for the Safari 15 floor */
  background: none; border: 0; cursor: pointer;
  -webkit-user-select: none; user-select: none;   /* don't let a tap select the label text — it flashes the selection colour before the pill slides (prefix for the iOS 15 / Safari 15 floor); the tap-highlight itself is killed globally on a, button */
  min-height: 44px;                  /* ≥44px touch target — mobile-first */
  padding: 0 1.1rem;
  color: var(--muted);              /* inactive label (AA) */
  transition: color .35s ease;      /* label colour cross-fade (not movement) — intentionally left active */
}
.lang-toggle__opt.is-active { color: var(--sage-deep); }   /* active label (AA) */

.lang-toggle__pill {
  position: absolute; z-index: 0; inset: 0;
  width: 50%;                       /* covers the left (中文) segment */
  background: rgb(var(--sage-rgb) / .16);
}
/* EN selected: slide the pill to the right segment and swap which verse shows. */
html.js .profiles[data-lang="en"] .lang-toggle__pill { transform: translateX(100%); }

html.js .profiles:not([data-lang="en"]) .profile__verse--en { display: none; }
html.js .profiles[data-lang="en"] .profile__verse.zh { display: none; }

/* The role line carries both languages and rides the same toggle: stacked when both
   show (no-JS), one revealed under JS. The Chinese half takes the zh prose size
   (--fs-profile-zh), one shade below the EN role, with .14em tracking not the Latin .26em. */
.profile__role > span[lang] { display: block; }
.profile__role > span[lang="zh"] { font-size: var(--fs-profile-zh); letter-spacing: .14em; }
html.js .profiles:not([data-lang="en"]) .profile__role > span[lang="en"] { display: none; }
html.js .profiles[data-lang="en"] .profile__role > span[lang="zh"] { display: none; }

/* English verse: same size/leading family as the verse base, but set in the Latin
   serif (the .zh class supplies the CJK face; the EN block has no .zh). A touch
   tighter than the CJK 2.15 leading, which is too airy for Latin. */
.profile__verse--en { font-family: var(--serif); line-height: 1.9; }
.profile__verse.zh { font-size: var(--fs-profile-zh); }

/* ============================================================
   Wedding details
   ============================================================ */
.details { text-align: center; }
.details__title { font-size: var(--fs-title); margin: 0; color: var(--ink); }
.details__eyebrow { margin: var(--space-bond) 0 var(--space-md); }

/* Calendar */
/* The calendar + countdown read as ONE "the day, and the count to it" unit, so
   the calendar tucks closer to the countdown than the labelled blocks below. */
.cal { position: relative; max-width: 318px; margin: 0 auto var(--space-sm); }
.cal__big { font-size: 3.2rem; font-weight: 500; color: var(--ink); margin: 0 0 .5rem; letter-spacing: .02em; }   /* 500: the page's focal number gets a touch more presence (the slash stays light) */
.cal__month-year { margin: 0 0 .7rem; font-size: var(--fs-small); color: var(--ink-soft); letter-spacing: .04em; line-height: 1.2; }
.cal__month-year [lang="zh"] { font-family: var(--zh); }
.cal__slash { color: var(--sage); margin: 0 .18em; font-weight: 300; }
.cal__weekdays, .cal__grid { display: grid; grid-template-columns: repeat(7, 1fr); }
.cal__weekdays {
  color: var(--sage-deep); font-size: .8rem; letter-spacing: .04em;
  padding-bottom: .5rem; border-bottom: 1px solid var(--line);
}
.cal__grid { gap: .4rem 0; padding-top: .6rem; }
.cal__grid > span { font-family: var(--serif); font-size: var(--fs-small); color: var(--ink-soft); padding: .22rem 0; }
/* Wedding-day marker: a flat champagne disc sits BEHIND a normal number cell,
   so "30" keeps the exact row position/baseline of its neighbours and isn't
   raised. The disc is a pseudo-element (z-index local to the cell) so it can
   exceed the cell box without changing layout. */
.cal__day.is-wedding {
  position: relative; z-index: 0;
  color: var(--ink); font-weight: 600;            /* ink-on-gold: 5.11:1, reads as gold-leaf */
}
.cal__day.is-wedding::before {
  content: ""; position: absolute; z-index: -1;
  top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 1.9em; height: 1.9em; border-radius: 50%;
  background: var(--champagne);
}

/* Countdown */
.countdown {
  display: flex; justify-content: center; align-items: stretch;
  gap: var(--countdown-gap); margin: 0 0 var(--space-lg);
}
.countdown__unit { display: flex; flex-direction: column; align-items: center; gap: .25rem; }
.countdown__unit + .countdown__unit { border-left: 1px solid var(--line); padding-left: var(--countdown-gap); }
.countdown__num {
  font-size: 2.1rem; color: var(--ink); line-height: 1; font-weight: 400;
  /* The serif's figures aren't reliably tabular, so digit advances differ ("11" is
     narrower than "00"). A fixed-width, centred box keeps every field the same
     width no matter the value, so the row never recentres / jitters per second. */
  display: inline-block; min-width: 1.5em; text-align: center;
  font-variant-numeric: lining-nums tabular-nums;   /* keep lining (a direct declaration would otherwise drop the inherited value), plus fixed-width digits */
}
.countdown__unit small { color: var(--muted); font-size: .68rem; letter-spacing: .08em; }

/* Schedule */
.schedule { margin: 0 0 var(--space-xl); }
.schedule__label { display: block; margin: 0 0 1.2rem; }
.schedule__label [lang="zh"] { font-family: var(--zh); }   /* keep 流程 in the page's Chinese serif, not the Latin serif's fallback */
.timeline {
  list-style: none; margin: 0 auto; padding: 0;
  max-width: 230px; text-align: left; position: relative;
}
.timeline::before { content: ""; position: absolute; left: 4px; top: .55rem; bottom: .55rem; width: 1px; background: var(--line); }
.timeline__item { position: relative; padding: 0 0 1.3rem 1.7rem; }
.timeline__item:last-child { padding-bottom: 0; }
.timeline__item::before {
  content: ""; position: absolute; left: 0; top: .5rem;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--porcelain); border: 1.5px solid var(--sage);
}
.timeline__time { display: block; font-size: 1.12rem; color: var(--ink); line-height: 1.25; }
.timeline__event { display: block; font-size: var(--fs-small); color: var(--ink-soft); }

/* Venue */
.venue { margin: 0; }
.venue__label { display: block; margin: 0 0 1.2rem; }   /* heads the "where" block, mirroring 流程 · Schedule */
.venue__label [lang="zh"], .venue__map-link [lang="zh"], .attire__label [lang="zh"] { font-family: var(--zh); }
.venue__name { font-size: 1.5rem; color: var(--ink); margin: 0 0 .7rem; line-height: 1.2; }
.venue__addr { margin: 0 0 .4rem; color: var(--ink-soft); font-size: var(--fs-small); }
.venue__map { display: block; text-decoration: none; }
.venue__map-img {
  width: 100%; aspect-ratio: 16 / 9; border-radius: var(--radius-photo);
  border: 1px solid var(--line); margin-bottom: .8rem;
  box-shadow: var(--shadow-photo);
}
.venue__map-link { display: inline-block; color: var(--sage-deep); transition: color .2s var(--ease); }
.venue__map:hover .venue__map-link { color: var(--ink); }

/* Attire */
.attire { margin: 0 0 var(--space-xl); }
.attire__label { display: block; margin: 0 0 1.2rem; }
.attire__dress { font-size: var(--fs-small); color: var(--muted); margin: 0; }

/* ============================================================
   RSVP
   ============================================================ */
.rsvp { text-align: center; }
.rsvp__title { font-size: var(--fs-title); margin: 0; color: var(--ink); }
.rsvp__eyebrow { margin: var(--space-bond) 0 var(--space-md); }
.rsvp__photo {
  width: auto; aspect-ratio: 3 / 3.7;
  margin-block: 0 var(--space-md);
  margin-inline: calc(-1 * var(--pad));   /* bleed to the full column width; dissolve mask is on .dissolve */
}
/* RSVP form box — "Pressed card + botanical crown". The on-page reply form, set as fine
   stationery and posting back to the couple's Google Form. A SINGLE inset hairline frame
   (::before) rings the card in the garden's signature green — the old engraved double-rule
   retired for one quiet sage line; the card's own outer edge is left to the soft shadow,
   not a drawn border. The RSVP wordmark, letterpressed into the stock, sits over a tapered
   sage rule with a centre seed-dot — the chapter eucalyptus floret, distilled. Else quiet:
   boxed inputs, one shared spacing ladder, precise caps tracking. Brush stays OUT of the
   box so the closing couplet below keeps the louder envoi. Sage as a decorative line uses
   rgb(var(--sage-rgb)/a) (not --sage-deep, which is reserved for text); --line stays the
   hex for the plain input/panel hairlines. */
.rsvp__box {
  position: relative;                          /* anchors the inset frame */
  width: 100%; margin: 0 0 var(--space-xl);   /* fills the content column on every screen (no max-width cap) */
  padding: clamp(1.9rem, 7.5vw, 2.4rem) clamp(1.6rem, 6.5vw, 2rem);
  text-align: left;
  background: var(--ivory);
  border-radius: var(--radius-collage); box-shadow: var(--shadow-photo);
}
.rsvp__box::before {                           /* single inset sage hairline frame; decorative, never taps */
  content: ""; position: absolute; inset: 8px;
  border: 1px solid rgb(var(--sage-rgb) / .42); border-radius: var(--radius-photo);
  pointer-events: none;
}
.rsvp__box-head {                              /* RSVP wordmark over the botanical rule */
  display: flex; flex-direction: column; align-items: center;
  gap: var(--space-bond); margin: 0 0 var(--space-sm);
}
.rsvp__card-label {                            /* wordmark; .caps supplies family/weight/uppercase */
  color: var(--sage-deep); font-size: 1.5rem; line-height: 1;
  letter-spacing: .3em; text-indent: .3em;     /* text-indent re-centres the trailing letter-space */
  /* Letterpress: a faint shadow above the glyphs + a warm-light lip below them presses the
     wordmark INTO the stock (light from the top). */
  text-shadow: 0 -1px 0 rgb(var(--shadow-rgb) / .18), 0 1px 0 rgb(var(--glow-rgb) / .95);
}
/* The botanical hairline: sage fading to rgb(var(--sage-rgb)/0) at both ends (never the
   transparent keyword), one seed-dot at its true centre — the floret distilled to a rule. */
.rsvp__card-rule {
  position: relative; width: 3.2rem; height: 1px; border-radius: 1px;
  background: linear-gradient(to right,
    rgb(var(--sage-rgb) / 0) 0%, rgb(var(--sage-rgb) / .6) 28%,
    rgb(var(--sage-rgb) / .6) 72%, rgb(var(--sage-rgb) / 0) 100%);
}
.rsvp__card-rule::before {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: 3px; height: 3px; border-radius: 50%;
  background: rgb(var(--sage-rgb) / .85); transform: translate(-50%, -50%);
}

/* Fields share one vertical ladder so the optical rhythm is even. */
.rsvp__field { margin: 0 0 var(--space-md); }
.rsvp__label { display: block; margin: 0 0 .55rem; }
.rsvp__label-text [lang="zh"] { font-family: var(--zh); letter-spacing: .14em; }   /* zh keeps the page serif, not the wide Latin track */

/* Inputs: boxed + obviously editable, pressed into the porcelain, hairline-framed like
   the panel. -webkit-appearance kept for the iOS 15 / Safari 15 floor. */
.rsvp__input {
  width: 100%; min-height: 46px;
  -webkit-appearance: none; appearance: none;
  background: var(--porcelain); border: 1px solid var(--line); border-radius: var(--radius-photo);
  padding: .6rem .8rem; margin: 0;
  font: 1rem var(--serif); color: var(--ink); line-height: 1.4;
  transition: border-color .25s var(--ease), box-shadow .25s var(--ease);
}
.rsvp__input::placeholder { color: var(--muted); font-style: italic; }
.rsvp__input:focus { outline: none; border-color: var(--sage-deep); box-shadow: 0 0 0 3px rgb(var(--sage-rgb) / .22); }
.rsvp__input:focus-visible { outline: 2px solid var(--sage-deep); outline-offset: 2px; }   /* clear keyboard focus, not just the hairline shift */

/* Choice groups — Main Course (radio) + Dietary (checkboxes). Same ladder, sage accent,
   full-row ≥44px targets; bilingual option text wraps with the control pinned to the first line. */
.rsvp__group { border: 0; margin: 0 0 var(--space-md); padding: 0; min-inline-size: 0; }   /* min-inline-size:0 stops the fieldset forcing width at 320px */
.rsvp__group-legend { margin: 0 0 .55rem; padding: 0; }
.rsvp__option { display: flex; align-items: flex-start; gap: .7rem; min-height: 44px; padding: .25rem 0; font: 1rem var(--serif); color: var(--ink); line-height: 1.45; cursor: pointer; }
.rsvp__option [lang="zh"] { font-family: var(--zh); }   /* bilingual option text keeps zh in the page serif */
.rsvp__option input[type="radio"], .rsvp__option input[type="checkbox"] { flex: 0 0 auto; width: 18px; height: 18px; margin: .2rem 0 0; accent-color: var(--sage-deep); }   /* margin-top pins the control to the first text line */
.rsvp__option input:focus-visible { outline: 2px solid var(--sage-deep); outline-offset: 2px; }
/* "Other" row: an inline free-text field that grows to fill the row and wraps below on narrow widths. */
.rsvp__option--other { flex-wrap: wrap; }
.rsvp__other-input {
  flex: 1 1 8rem; min-width: 6rem; min-height: 44px;
  -webkit-appearance: none; appearance: none;
  background: var(--porcelain); border: 1px solid var(--line); border-radius: var(--radius-photo);
  padding: .3rem .6rem; font: .95rem var(--serif); color: var(--ink);
  transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
}
.rsvp__other-input:focus { outline: none; border-color: var(--sage-deep); box-shadow: 0 0 0 3px rgb(var(--sage-rgb) / .22); }
.rsvp__other-input:focus-visible { outline: 2px solid var(--sage-deep); outline-offset: 2px; }

/* Submit-time validation — quiet until a send attempt, then a warm-red border + an italic
   nudge (the stationer's correction hand). main.js toggles .is-invalid on the field/fieldset
   and clears it on edit; native `required` is the no-JS fallback. */
.rsvp__nudge { display: none; margin: .5rem 0 0; font: italic .82rem var(--serif); letter-spacing: .01em; color: var(--alert); }
.rsvp__nudge [lang="zh"] { font-family: var(--zh); font-style: normal; }   /* CJK has no true italic */
.rsvp__field.is-invalid .rsvp__nudge,
.rsvp__group.is-invalid .rsvp__nudge { display: block; }
.rsvp__field.is-invalid .rsvp__input { border-color: var(--alert); box-shadow: 0 0 0 3px rgb(var(--alert-rgb) / .14); }

/* Network fallback — surfaced by initRsvp if the post stalls (offline / blocker). Hidden via the
   `hidden` attribute (not display here), so JS just toggles it; never seen on the no-JS path. */
.rsvp__error { margin: var(--space-md) 0 0; text-align: center; font: italic .85rem var(--serif); letter-spacing: .01em; color: var(--alert); line-height: 1.6; }
.rsvp__error [lang="zh"] { font-family: var(--zh); font-style: normal; }

/* One green action — engraved caps on the sage ground; hover deepens to warm ink. */
.rsvp__submit {
  display: block; width: 100%; min-height: 48px; margin: var(--space-md) 0 0;
  -webkit-appearance: none; appearance: none; cursor: pointer;
  background: var(--sage-deep); color: var(--porcelain);
  border: 0; border-radius: var(--radius-photo);
  font: 500 .9rem var(--serif); text-transform: uppercase; letter-spacing: .26em; text-indent: .26em;
  transition: background .25s var(--ease), box-shadow .25s var(--ease);
}
.rsvp__submit [lang="zh"] { font-family: var(--zh); letter-spacing: .14em; }
.rsvp__submit:hover { background: var(--ink); }
.rsvp__submit:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }   /* visible focus on the green ground */

/* Reply-by note, centred under the action. */
.rsvp__card-note { text-align: center; margin: var(--space-md) 0 0; font-size: var(--fs-small); color: var(--ink-soft); letter-spacing: .04em; line-height: 1.7; }
.rsvp__card-note span { display: block; }

/* Hidden submission sink: rendered (so its load event fires after a post) but visually
   gone — the sr-only recipe. JS points form.target here so the page never navigates. */
.rsvp__sink { position: absolute; width: 1px; height: 1px; margin: -1px; border: 0; padding: 0; overflow: hidden; clip: rect(0 0 0 0); }

/* Thank-you (revealed by JS after a successful send) — the same framed card, centred. */
.rsvp__thanks { text-align: center; }
.rsvp__thanks-zh { margin: 0; font-size: var(--fs-verse); color: var(--ink-soft); }
.rsvp__thanks-en { margin: .4rem 0 0; color: var(--muted); }

@media (prefers-reduced-motion: reduce) {
  .rsvp__input, .rsvp__other-input, .rsvp__submit { transition: none; }
}
/* Closing couplet — the page's emotional envoi (it threads both names into the
   verse), set in the Ma Shan Zheng brush like a love note in the couple's own
   hand. Brush reads denser than the serif, so it's sized up; kept below the
   我们/婚礼见 sign-off and given open leading so the hierarchy of brush holds.
   Tinted sage (--sage-deep, the text-green — not decorative --sage) so the envoi
   closes on the garden's signature accent while staying legible. */
.rsvp__verse {
  border: 0; margin: 0 0 var(--space-xl); padding: 0;
  font-family: var(--brush);
  font-size: clamp(1.3rem, 5.6vw, 1.62rem); line-height: 1.85; letter-spacing: .05em; color: var(--sage-deep);
}
.rsvp__verse p { margin: 0; }
.rsvp__footer { font-size: clamp(1.9rem, 8.8vw, 2.9rem); color: var(--ink); margin: 0; }
.rsvp__footer-mark { color: var(--sage); padding: 0 .1em; }
.rsvp__farewell { margin: var(--space-bond) 0 .4rem; }
.rsvp__sig { display: block; margin: 0; }
