
/* ==========================================================================
   kevinkaya.com — DESIGN TOKENS  v5 — final
   Kevin & Kaya · 3 July 2027 · Stone Tower Winery
   --------------------------------------------------------------------------
   Three layers:
     1. PRIMITIVES  — ordered ramps, named by colour + step. Components never
                      touch these.
     2. SEMANTIC    — named by job. This is all components may reference.
     3. COMPONENT   — declared inside the component (see components.css), so a
                      variant like .btn--danger is a four-line override.

   ONE PALETTE: Vesper. Four alternates were built during design and struck;
   what survives of them is the reasoning in section 2, kept deliberately.
   21 primitives on five ramps plus six singles. Same step number means the
   same role in every ramp.
   ========================================================================== */


/* ==========================================================================
   1. FONT LOADING — self-hosted. Never fonts.googleapis.com (China).
   The shipped Fraunces files carry opsz + wght ONLY; SOFT and WONK were
   subset out, so any font-variation-settings:'WONK' 1 is a silent no-op.
   Fraunces' wght DEFAULT is 700 — always set font-weight explicitly.
   ========================================================================== */

@font-face{ font-family:'Fraunces'; font-style:normal; font-weight:300 700; font-display:swap;
  src:url('../fonts/fraunces-roman.woff2') format('woff2'); }
@font-face{ font-family:'Fraunces'; font-style:italic; font-weight:300 700; font-display:swap;
  src:url('../fonts/fraunces-italic.woff2') format('woff2'); }
@font-face{ font-family:'Cinzel'; font-style:normal; font-weight:400 900; font-display:swap;
  src:url('../fonts/cinzel.woff2') format('woff2'); }
@font-face{ font-family:'Inter'; font-style:normal; font-weight:100 900; font-display:swap;
  src:url('../fonts/inter.woff2') format('woff2'); }


/* ==========================================================================
   2. PRIMITIVES — VESPER. Ramps run light to dark.

   RAMP      STEPS  CARRIES
   paper     1-4    warm neutral: surfaces in light, the white on the night
   ink       3-6    warm espresso: text, light mode only
   slate     1-5    cool blue: the night only
   thicket   1-2    the primary action, deliberately near-neutral
   honey     1-2    links and eyebrows
   + 6 singles      sand, sand-gold, brick, ember, and two sparks

   THREE RULES THIS PALETTE PRODUCED. Four earlier palettes were built and
   struck; these are what survived, and they are here so a settled question
   does not get reopened in six months.

   1. THE MUDDY ZONE IS A PLACE. Below L* 25 a colour may be as grey as it
      likes and still read as ink. Above it, low chroma reads as a colour that
      failed. Moss #4B5B34 was L* 36.4 / C* 24.7 — exactly there — and moving
      chroma in either direction made it worse. The green moved DOWN instead:
      thicket-1 is L* 23.2 / C* 6.9.
   2. GOLD IS A LIGHT-ON-DARK COLOUR. On parchment it must darken to bronze
      (honey-2) to clear 4.5:1. Bright gold as text on parchment tops out near
      4.0:1 and cannot be rescued. It is gold on the night and nowhere else.
   3. TWO TEMPERATURES, ON PURPOSE. Warm page, cool night. Daylight is warm and
      night is cool; the firework section is the one place a temperature shift
      is the point rather than a mistake. It holds only because the shift is a
      whole section and never mixes.
   ========================================================================== */

:root{
  /* paper — warm neutral */
  --paper-1:   #F2EDE5;   /* page background                                */
  --paper-2:   #EAE2D4;   /* alternating section, card fill                 */
  --paper-3:   #E1D5BF;   /* decorative panel. NOT a text surface           */
  --paper-4:   #C7BBA7;   /* decorative hairline, divider                   */

  /* ink — warm espresso, light mode only */
  --ink-3:     #8B7E6E;   /* control border      3.40:1, WCAG 1.4.11        */
  --ink-4:     #5E4A32;   /* muted text          7.20:1                     */
  --ink-5:     #3B1E03;   /* body text          13.13:1                     */
  --ink-6:     #2F1702;   /* headings, focus    14.48:1                     */

  /* slate — cool blue, the night only */
  --slate-1:   #C3CDD6;   /* body text on night  9.42:1                     */
  --slate-2:   #8898A5;   /* muted + border      5.12:1                     */
  --slate-3:   #384654;   /* decorative hairline                            */
  --slate-4:   #26333F;   /* surface, card fill                             */
  --slate-5:   #1A2733;   /* page background                                */

  /* thicket — the primary action */
  --thicket-1: #2F3A33;   /* rest   paper on it 10.16:1 · L* 23.2 C* 6.9    */
  --thicket-2: #222B26;   /* hover  paper on it 12.50:1                     */

  /* honey — links. Text BRIGHTENS on hover, so rest is the darker step. */
  --honey-1:   #BF8414;   /* link hover                                     */
  --honey-2:   #7D530A;   /* link + eyebrow, rest  5.79:1                   */

  /* singles */
  --sand:      #E4D3B4;   /* the night's white    10.33:1 · C* 17.5         */
  --sand-gold: #CFBE9C;   /* neutral gold, night links  8.32:1              */
  --brick:     #8C2F12;   /* danger, light mode         7.12:1              */
  --ember:     #D98254;   /* danger on night, and a spark  5.26:1           */
  --gold:      #E7C46A;   /* SPARK ONLY. Never UI.                          */
  --gold-hi:   #F3DFA8;   /* SPARK ONLY.                                    */
  --rose:      #CE7078;   /* SPARK ONLY.                                    */
}


/* ==========================================================================
   3. SEMANTIC TOKENS — LIGHT. Named by JOB. All a component may reference.
   ========================================================================== */

:root{
  --color-bg:               var(--paper-1);
  --color-surface:          var(--paper-3);  /* decorative. NOT a text surface:
                                                muted text on it fails. Only
                                                --color-text and --color-heading
                                                may sit here. */
  --color-surface-alt:      var(--paper-2);  /* cards, anything holding text  */

  /* two borders, deliberately.
     --color-border        decorative: dividers, section rules
     --color-border-strong control boundaries — inputs, checkboxes. WCAG 1.4.11
                           wants 3:1 for non-text UI; the hairline is 1.4:1. */
  --color-border:           var(--paper-4);
  --color-border-strong:    var(--ink-3);

  --color-text:             var(--ink-5);
  --color-heading:          var(--ink-6);
  --color-text-muted:       var(--ink-4);
  --color-text-inverse:     var(--paper-1);

  --color-primary:          var(--thicket-1);
  --color-primary-hover:    var(--thicket-2);
  --color-on-primary:       var(--paper-1);
  --color-secondary:        var(--ink-6);
  --color-secondary-hover:  var(--ink-5);
  --color-on-secondary:     var(--paper-1);

  --color-link:             var(--honey-2);
  --color-link-hover:       var(--honey-1);
  --color-eyebrow:          var(--honey-2);

  --color-danger:           var(--brick);
  --color-danger-hover:     #6E220C;
  --color-on-danger:        var(--paper-1);
  --color-success:          var(--thicket-1);

  /* Focus is INK, never an accent. It was once a red-brown one hue away from
     --color-danger, so a focused field looked like a rejected one. The ring is
     two-tone — inner in the page colour, outer in the focus colour — so it is
     legible against both the control and whatever sits behind it. */
  --color-focus:            var(--ink-6);
  --focus-ring:             0 0 0 2px var(--color-bg), 0 0 0 4px var(--color-focus);

  --color-disabled-bg:      var(--paper-3);
  --color-disabled-text:    var(--ink-4);
  --color-disabled-border:  var(--paper-4);

  --color-spark-1: var(--gold);
  --color-spark-2: var(--ember);
  --color-spark-3: var(--rose);

  --photo-shadow: #233042;
  --photo-light:  var(--paper-1);

  /* ========================================================================
     4. TYPOGRAPHY
     ======================================================================== */
  --font-serif:   'Fraunces', Georgia, 'Times New Roman', serif;
  --font-graphic: 'Cinzel', Georgia, serif;      /* lockups only, never <16px */
  --font-ui:      'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  /* CJK. Song/Ming is the modulated-stroke face — the closest thing Chinese has
     to a high-contrast serif. A Hei sans would change the register entirely.
     'Song Sub' is the subset webfont from §11; it is first, and everything after
     it is a system face, so nothing breaks before the subset is built. */
  --font-serif-zh: 'Song Sub', 'Songti SC', 'Noto Serif SC',
                   'Source Han Serif SC', 'SimSun', serif;
  --font-ui-zh:    'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei',
                   'Noto Sans SC', sans-serif;

  --weight-display:  300;   /* h1, h2 — engraved at large sizes */
  --weight-heading:  500;   /* h3 down — 300 goes weak under ~26px */
  --weight-light:    300;
  --weight-regular:  400;
  --weight-medium:   500;
  --weight-semibold: 600;   /* eyebrows — survives .22em tracking */
  --weight-bold:     700;   /* display ceiling is 600. Never here. */

  /* Two-speed scale. Prose steps climb at ~1.20 so body, lead and h4 read as
     one voice; display steps jump at ~1.50 so a headline is unmistakable.
     v3 was lumpy (1.15, 1.23, 1.80, 1.90) and had no h4 at all. */
  --text-xs:   0.75rem;      /* 12 · fine print, admin  */
  --text-sm:   0.875rem;     /* 14 · caption, label     */
  --text-base: 1.0625rem;    /* 17 · body               */
  --text-lg:   1.25rem;      /* 20 · lead, subline      */
  --text-xl:   1.5rem;       /* 24 · h4                 */
  --text-2xl:  clamp(1.75rem, 3.2vw, 2.25rem);   /* 28-36 · h3 */
  --text-3xl:  clamp(2.25rem, 5.2vw, 3.5rem);    /* 36-56 · h2 */
  --text-4xl:  clamp(3rem, 8vw, 5.2rem);         /* 48-83 · h1 */

  --leading-tight: 1.02; --leading-snug: 1.14; --leading-normal: 1.5;
  --leading-prose: 1.74; --leading-cjk: 1.95;
  --tracking-tight: -0.025em; --tracking-normal: 0; --tracking-wide: 0.1em;
  --tracking-eyebrow: 0.22em; --tracking-graphic: 0.4em;


  /* ========================================================================
     5. SPACING
     ======================================================================== */
  --space-1: 0.25rem; --space-2: 0.5rem; --space-3: 1rem;  --space-4: 1.5rem;
  --space-5: 2.5rem;  --space-6: 4rem;   --space-7: 6rem;  --space-8: 9rem;

  --section-block:  clamp(80px, 11vw, 150px);
  --section-inline: clamp(24px, 6vw, 90px);
  --measure: 64ch; --container: 840px; --container-wide: 1100px;


  /* ========================================================================
     6. SHAPE — 0. Confirmed. Never mix sharp and rounded.
     ======================================================================== */
  --radius-sm: 0; --radius-md: 0; --radius-lg: 0; --radius-pill: 0;
  --border-width: 1px;

  --btn-padding:       0.875rem 2rem;
  --btn-padding-small: 0.5rem 1.25rem;
  --btn-padding-large: 18px 46px;
  --btn-min-target:    44px;      /* WCAG 2.5.5 */
  --input-padding:     0.75rem 0.875rem;


  /* ========================================================================
     7. ELEVATION & MOTION — shadows tinted with ink, never neutral grey
     ======================================================================== */
  --shadow-soft: 0 2px 12px rgba(9, 47, 51, 0.08);
  --shadow-lift: 0 8px 30px rgba(9, 47, 51, 0.14);

  /* two curves, no more: --ease for state changes, --ease-out-soft for arrivals */
  --ease:            cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-out-soft:   cubic-bezier(0.16, 0.84, 0.44, 1);
  --duration-fast:   140ms;   /* colour, hover, focus            */
  --duration-base:   220ms;   /* fills, sweeps, press, errors    */
  --duration-slow:   420ms;   /* nav state, dropdown             */
  --duration-reveal: 620ms;   /* section arrivals                */
  --duration-settle: 900ms;   /* plates, rule draws              */
  --duration-scene:  34000ms; /* the cart. Only looping animation. */
  --stagger:         70ms;    /* caps at four items              */

  --parallax-far: 0.02; --parallax-mid: 0.03; --parallax-near: 0.04;
  /* pattern + photography. Ceiling measured in the design system, plate 06b:
     .05 = 1.15:1 on night (safe behind text) · .12 = 1.41:1 (no text) */
  --pattern-ink:          var(--color-heading);
  --pattern-opacity:      0.05;
  --pattern-opacity-bold: 0.12;   /* decorative panels with no text over them */

  --z-overlay: 900;
  --z-modal:   1000;
}


/* ==========================================================================
   8. NOCTURNE — the dark finale. <section class="nocturne"> … </section>
   The one place the page changes temperature. Also serves as site-wide dark
   mode: duplicate this block under [data-theme="dark"] when that ships.
   ========================================================================== */

.nocturne{
  --color-bg:              var(--slate-5);
  --color-surface:         var(--slate-4);
  --color-surface-alt:     var(--slate-4);
  --color-border:          var(--slate-3);
  --color-border-strong:   var(--slate-2);

  --color-text:            var(--slate-1);
  --color-heading:         var(--paper-1);
  --color-text-muted:      var(--slate-2);
  --color-text-inverse:    var(--slate-5);

  /* The night's button is SAND used as the white, not a saturated gold.
     Chroma 49.3 -> 17.5. A gold fill was the only element on the page
     raising its voice. */
  --color-primary:         var(--sand);
  --color-primary-hover:   var(--paper-1);
  --color-on-primary:      var(--slate-5);
  --color-secondary:       var(--sand-gold);
  --color-secondary-hover: var(--sand);
  --color-on-secondary:    var(--slate-5);

  --color-link:            var(--sand-gold);
  --color-link-hover:      var(--sand);
  --color-eyebrow:         var(--sand-gold);

  --color-danger:          var(--ember);
  --color-danger-hover:    var(--rose);
  --color-on-danger:       var(--slate-5);

  --color-focus:           var(--sand);
  --focus-ring:            0 0 0 2px var(--color-bg), 0 0 0 4px var(--color-focus);
  --color-disabled-bg:     var(--slate-4);
  --color-disabled-text:   var(--slate-2);
  --color-disabled-border: var(--slate-3);

  --color-spark-1: var(--gold);
  --color-spark-2: var(--ember);
  --color-spark-3: var(--rose);

  --photo-shadow: #2A3949;
  --photo-light:  var(--sand);

  background: var(--color-bg);
  color: var(--color-text);
}

/* ==========================================================================
   9. CJK — scoped to :lang(zh). Put <html lang="zh-Hans"> on the Chinese pages.

   Chinese is not Latin with different words. It has no italic, no letter case,
   no small caps, a shorter comfortable line and more leading, and its own
   emphasis mark. Each of those is load-bearing in this design.
   ========================================================================== */

:lang(zh){
  --font-serif:   var(--font-serif-zh);
  --font-ui:      var(--font-ui-zh);
  --font-graphic: var(--font-serif-zh);   /* Cinzel has no CJK. Lockups stay Latin. */
  --measure:      34em;        /* ~34 characters. 64ch is a Latin measurement:
                                  ch is the width of a zero, so 64ch of Chinese
                                  is roughly a 120-character line. */
  --leading-prose:   var(--leading-cjk);
  --tracking-eyebrow: 0.14em;  /* .22em on full-width glyphs reads as broken   */
  --tracking-tight:   0;       /* negative tracking collides square glyphs     */
}
:lang(zh) body{ letter-spacing: 0.02em; line-height: var(--leading-cjk); }

/* Synthesised oblique on CJK is a shear, not a typeface. Emphasis is 着重号. */
:lang(zh) em, :lang(zh) i, :lang(zh) .em{
  font-style: normal;
  -webkit-text-emphasis: filled dot; text-emphasis: filled dot;
  -webkit-text-emphasis-position: under right; text-emphasis-position: under right;
}
/* uppercase does nothing to characters but mangles Latin mixed into the string */
:lang(zh) .eyebrow, :lang(zh) .btn, :lang(zh) .navbar a,
:lang(zh) .field label, :lang(zh) .card .k, :lang(zh) .count-lab{ text-transform: none; }

/* there is no 300-weight Song on any system: asking for it gets synthetic thinning */
:lang(zh) h1, :lang(zh) h2{ font-weight: var(--weight-regular); letter-spacing: 0; }
:lang(zh) .btn{ letter-spacing: 0.16em; font-weight: var(--weight-regular); }

:lang(zh) p, :lang(zh) li{
  line-break: strict;              /* punctuation never starts a line */
  overflow-wrap: break-word;
  text-wrap: pretty;
  hanging-punctuation: allow-end;
}

/* SUBSET FONT — the one place worth spending bytes. Build AFTER the copy is final:
     pyftsubset NotoSerifSC-Regular.otf --text-file=content/zh-strings.txt \
       --flavor=woff2 --layout-features='' --output-file=fonts/song-subset.woff2
   ~250 distinct characters of static UI copy comes to 30-60 KB, smaller than
   Inter. Guest names and typed input fall back to the system stack on purpose.
   Regenerate in the build whenever the Chinese copy changes.
@font-face{ font-family:'Song Sub'; font-display:swap;
  src:url('../fonts/song-subset.woff2') format('woff2');
  unicode-range:U+4E00-9FFF,U+3000-303F,U+FF00-FFEF; }
*/


/* ==========================================================================
   10. TINTS — mixed at point of use. Do NOT add a primitive for these.
   e.g. a selected row in the admin table:
     background: color-mix(in srgb, var(--color-primary) 10%, var(--color-bg));
   ========================================================================== */


/* ==========================================================================
   11. BASE RESET — minimal, token-driven
   ========================================================================== */

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

html{ font-optical-sizing: auto; -webkit-text-size-adjust: 100%; }

body{
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-serif);
  font-size: var(--text-base);
  font-weight: var(--weight-regular);
  line-height: var(--leading-prose);
  font-optical-sizing: auto;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, h4{
  font-family: var(--font-serif);
  color: var(--color-heading);
  font-optical-sizing: auto;
  margin: 0 0 var(--space-3);
}
h1{ font-size: var(--text-4xl); font-weight: var(--weight-display);
    line-height: var(--leading-tight); letter-spacing: var(--tracking-tight); }
h2{ font-size: var(--text-3xl); font-weight: var(--weight-display);
    line-height: var(--leading-snug); letter-spacing: -0.015em; }
h3{ font-size: var(--text-2xl); font-weight: var(--weight-heading);
    line-height: var(--leading-snug); }
h4{ font-size: var(--text-xl);  font-weight: var(--weight-heading);
    line-height: 1.2; }

p{ margin: 0 0 var(--space-3); max-width: var(--measure); }

a{ color: var(--color-link); text-decoration-thickness: 1px;
   text-underline-offset: 0.18em;
   transition: color var(--duration-fast) var(--ease); }
a:hover{ color: var(--color-link-hover); }

img, svg, video{ max-width: 100%; height: auto; display: block; }

:focus-visible{ outline: none; box-shadow: var(--focus-ring); }

@media (prefers-reduced-motion: reduce){
  *, *::before, *::after{
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}


/* ==========================================================================
   12. ICONS — Material Symbols Outlined, self-hosted as an inline SVG sprite.
   NOT the icon font: the variable font is 3.9 MB and even a subset is ~100 KB
   with a flash of missing glyphs. 17 icons of real Material geometry come to
   5.5 KB inlined, inherit currentColor, and cannot fail to load separately
   from the page. Google Fonts is unreachable from China, so a link is out.
   Source: github.com/google/material-design-icons — symbols/web/<name>/
   materialsymbolsoutlined/<name>_24px.svg
   Outlined only. Never mix with Rounded, Sharp or Filled.
   ========================================================================== */

.icon{
  width: 1em; height: 1em; flex: none;
  fill: currentColor; display: inline-block; vertical-align: -0.125em;
}
.icon--lg{ width: 1.5em; height: 1.5em; }

/* ==========================================================================
   kevinkaya.com — COMPONENT LIBRARY  v3
   Import AFTER tokens.css. Contains zero hex values, with one documented
   exception (.btn--plate, which sits on unpredictable painted imagery).
   Live reference: the design system site, plate 04.
   ========================================================================== */


/* ==========================================================================
   BUTTONS
   ========================================================================== */

/* Component tokens. Intent variants rewrite these four and nothing else.
   Legal combinations (16):
     primary|secondary|tertiary  x  sm|md|lg   (tertiary has no lg)
     + the same three at sm|md with --danger   (danger has no lg)
     + plate, which is lg-only and default-intent-only
   Forbidden combinations are listed at the bottom of this block. */
.btn{
  --btn-fill:       var(--color-primary);
  --btn-fill-hover: var(--color-primary-hover);
  --btn-on-fill:    var(--color-on-primary);
  --btn-ink:        var(--color-primary);
  --btn-edge:       var(--btn-fill);   /* a light fill sets this darker so the
                                          control boundary still reaches 3:1 */

  font-family: var(--font-serif);
  font-weight: var(--weight-light);
  font-size: 15px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.7em;
  cursor: pointer;
  min-height: var(--btn-min-target);
  padding: var(--btn-padding);
  border-radius: var(--radius-md);
  border: var(--border-width) solid var(--btn-edge);
  background: var(--btn-fill);
  color: var(--btn-on-fill);
  transition: background var(--duration-base) var(--ease),
              color var(--duration-base) var(--ease),
              border-color var(--duration-base) var(--ease),
              transform var(--duration-fast) var(--ease);
}
.btn:hover{ background: var(--btn-fill-hover); border-color: var(--btn-fill-hover); color: var(--btn-on-fill); }
.btn:active{ transform: translateY(1px); }          /* the press. Only motion on click. */
.btn:focus-visible{ outline: none; box-shadow: var(--focus-ring); }

/* --- emphasis ---------------------------------------------------------- */
.btn--secondary{ background: transparent; color: var(--btn-ink); border-color: var(--btn-ink); }
.btn--secondary:hover{ background: var(--btn-fill); border-color: var(--btn-fill); color: var(--btn-on-fill); }

.btn--tertiary{
  /* Underline at rest. On hover, solid ink sweeps in from the left, rises to
     the ascender line, and the label inverts to the page colour clipped in
     lockstep with the ink. The rest-state underline fades out on hover so the
     ink block's own bottom edge is the only rule; nothing ghosts through the
     invert.

     ALL vertical geometry is derived from Fraunces metrics (upm 2000):
       ascent .978  descent .255  cap .700  x-height .482  content-area 1.233
     Nothing here is eyeballed. Change --ter-pad-y, --ter-lh or --ter-rule-off
     and the rule, ink and clip all follow. These four constants are the only
     thing to re-measure if Fraunces is ever replaced.

     REQUIRES a duplicated label in the markup, because solid ink has to invert:
       <a class="btn btn--tertiary">
         <span class="btn-lbl">Change your answer</span>
         <span class="btn-lbl-inv" aria-hidden="true">Change your answer</span>
       </a>
     aria-hidden on the copy is mandatory or screen readers read it twice. */
  --ter-inset: 0.6em;
  --ter-pad-y: 0.45em;
  --ter-lh: 1;                          /* MUST be set; inheriting 1.74 anchors the ink wrong */
  --ter-A: 1.233em;
  --ter-descent: 0.255em;
  --ter-cap: 0.700em;
  --ter-asc: 0.978em;
  --ter-ink-top-metric: var(--ter-asc);
  --ter-rule-off: 0.20em;

  --ter-half-lead: calc((var(--ter-lh) * 1em - var(--ter-A)) / 2);
  --ter-base-from-center: calc(var(--ter-asc) - var(--ter-A) / 2); /* baseline below vertical center */
  /* all measured DOWN from the button's vertical center, so the ink stays put
     no matter how tall min-height makes the button */
  --ter-rule-y:  calc(var(--ter-base-from-center) + var(--ter-rule-off));   /* rule below center */
  --ter-floor-y: calc(var(--ter-base-from-center) + var(--ter-descent));    /* ink floor (descender) */
  --ter-top-y:   calc(var(--ter-base-from-center) - var(--ter-ink-top-metric)); /* ink top (ascender) above center */

  position: relative; isolation: isolate;
  display: inline-flex; align-items: center; justify-content: center;
  white-space: nowrap;         /* the label must not wrap: the inverted copy is a
                                  single line, so a wrapping original would render
                                  wider/taller and the two would diverge */
  align-self: center;          /* never stretch to a flex/grid row's height; the
                                  derived ink geometry assumes the button height
                                  is its own content, 2*pad + line-height */
  width: fit-content;          /* nor stretch horizontally; the invert centres on
                                  the label, not on a stretched box */
  line-height: var(--ter-lh); text-decoration: none;
  padding: var(--ter-pad-y) var(--ter-inset);
  margin-inline: calc(var(--ter-inset) * -1); min-height: auto;
  background: transparent; border-color: transparent; color: var(--btn-ink);
}
.btn--tertiary::after{             /* the rule, rest state only */
  content: ""; position: absolute; z-index: -1;
  left: var(--ter-inset); right: var(--ter-inset);
  top: 50%; height: 1px; margin-top: var(--ter-rule-y);
  background: currentColor; opacity: 0.85;
  transition: opacity var(--duration-fast) var(--ease);
}
.btn--tertiary::before{            /* the ink */
  content: ""; position: absolute; z-index: -1;
  left: var(--ter-inset); right: var(--ter-inset);
  top: 50%; margin-top: var(--ter-top-y);
  height: calc(var(--ter-floor-y) - var(--ter-top-y));
  background: var(--btn-ink);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--duration-base) var(--ease-out-soft);
}
.btn--tertiary:hover::before, .btn--tertiary:focus-visible::before{ transform: scaleX(1); }
.btn--tertiary:hover::after,  .btn--tertiary:focus-visible::after{ opacity: 0; }
.btn--tertiary:hover, .btn--tertiary:focus-visible{ background: transparent; border-color: transparent; }

/* the inverted copy: same box and same flex centring as the button, NO padding
   of its own (inset:0 already spans the padding box; re-adding padding would
   inset it twice and it drifts by one padding width). Clipped to the ink
   rectangle in both axes, so each letter splits at the ink edge. */
.btn--tertiary .btn-lbl-inv{
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);   /* centre on the button's own centre, the same
                                          point flex uses for .btn-lbl, so the two
                                          stay aligned at any button width or height */
  color: var(--color-bg); pointer-events: none; white-space: nowrap;
  clip-path: inset(calc(50% + var(--ter-top-y)) 100% calc(50% - var(--ter-floor-y)) 0);
  transition: clip-path var(--duration-base) var(--ease-out-soft);
}
.btn--tertiary:hover .btn-lbl-inv, .btn--tertiary:focus-visible .btn-lbl-inv{
  clip-path: inset(calc(50% + var(--ter-top-y)) 0 calc(50% - var(--ter-floor-y)) 0);
}
@media (prefers-reduced-motion: reduce){
  .btn--tertiary::before, .btn--tertiary::after, .btn--tertiary .btn-lbl-inv{ transition: none; }
}

/* class-driven equivalents for the state matrix in the reference site. Real
   interaction uses :hover / :focus-visible above; these mirror them so the
   plate can show a resting example of the hover state. */
.btn--tertiary.is-hover::before{ transform: scaleX(1); }
.btn--tertiary.is-hover::after{ opacity: 0; }
.btn--tertiary.is-hover .btn-lbl-inv{
  clip-path: inset(calc(50% + var(--ter-top-y)) 0 calc(50% - var(--ter-floor-y)) 0);
}



/* DOCUMENTED EXCEPTION: sits on painted imagery of unknown value, so it cannot
   theme. Sand hairline + 2px backdrop blur to stay legible. lg-only. */
.btn--plate{
  font-size: 18px; letter-spacing: 0.22em; padding: var(--btn-padding-large);
  border-width: 1.5px;
  border-color: rgba(228, 203, 169, 0.55);   /* paper-3 @ 55% */
  background: rgba(20, 30, 25, 0.16);
  color: #E4CBA9;                            /* paper-3 */
  backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);
}
.btn--plate:hover{
  background: #E4CBA9; border-color: #E4CBA9; color: #243C3E;   /* ink-5 */
  backdrop-filter: none; -webkit-backdrop-filter: none;
}

/* --- size -------------------------------------------------------------- */
/* sm drops below the 44px target, so it is admin / desktop only. */
.btn--sm{ font-size: 13px; padding: var(--btn-padding-small); letter-spacing: 0.12em; min-height: 36px; }
.btn--lg{ font-size: 18px; letter-spacing: 0.22em; padding: var(--btn-padding-large); border-width: 1.5px; }
.btn--lg.btn--secondary{ border-color: color-mix(in srgb, var(--btn-ink) 55%, transparent); }
.btn--lg.btn--secondary:hover{ border-color: var(--btn-fill); }

/* --- intent ------------------------------------------------------------ */
.btn--danger{
  --btn-fill:       var(--color-danger);
  --btn-fill-hover: var(--color-danger-hover);
  --btn-on-fill:    var(--color-on-danger);
  --btn-ink:        var(--color-danger);
}

/* --- modifiers --------------------------------------------------------- */
.btn--block{ display: flex; width: 100%; }
.btn--icon{ padding-left: 1rem; padding-right: 1rem; gap: 0; min-width: var(--btn-min-target); }
.btn svg{ width: 1em; height: 1em; flex: none; }   /* aria-hidden, stroke 1.2 */

/* --- states ------------------------------------------------------------ */
/* Not opacity: opacity over the page grain goes muddy. Prefer aria-disabled
   plus a sentence saying why, over disabled, which is skipped by screen
   readers browsing by control. */
/* Quiet companion to .btn--plate: a second action over imagery that must not
   compete with the first. No fill, no box, Sand text on a hairline that sweeps
   in. Same documented-exception status as .btn--plate, because it also sits on
   unpredictable photograph and cannot theme. */
.btn--plate.btn--quiet{background:none;border-color:transparent;padding-inline:.2rem;
  backdrop-filter:none;-webkit-backdrop-filter:none;position:relative;font-size:16px;
  letter-spacing:.18em}
.btn--plate.btn--quiet::after{content:"";position:absolute;left:.2rem;right:.2rem;bottom:12px;
  height:1px;background:currentColor;opacity:.55;transform:scaleX(0);transform-origin:left;
  transition:transform var(--duration-base) var(--ease)}
.btn--plate.btn--quiet:hover{background:none;border-color:transparent;color:#E4D3B4}
.btn--plate.btn--quiet:hover::after{transform:scaleX(1)}

.btn[disabled], .btn[aria-disabled="true"]{
  background: var(--color-disabled-bg);
  border-color: var(--color-disabled-border);
  color: var(--color-disabled-text);
  cursor: not-allowed; transform: none;
}
.btn--secondary[disabled], .btn--secondary[aria-disabled="true"],
.btn--tertiary[disabled],  .btn--tertiary[aria-disabled="true"]{
  background: transparent;
  color: var(--color-disabled-text);
  border-color: var(--color-disabled-border);
}
/* Label swaps to the present participle; pair with aria-live="polite". */
.btn[data-busy="true"]{ cursor: progress; pointer-events: none; }
.btn[data-busy="true"]::before{
  content: ""; width: 0.7em; height: 0.7em;
  border: 1.5px solid currentColor; border-right-color: transparent; border-radius: 50%;
  animation: btnspin 700ms linear infinite;
  position: relative; z-index: 1;          /* in front of the label, not behind */
}
/* a busy tertiary must drop its ink machinery: the ink ::before would otherwise
   fight the spinner ::before, and the negative z-index would bury it. */
.btn--tertiary[data-busy="true"]{ isolation: auto; }
.btn--tertiary[data-busy="true"]::before{
  position: relative; z-index: 1; inset: auto; left: auto; right: auto; top: auto;
  height: 0.7em; width: 0.7em; margin: 0;
  background: transparent;                  /* not the ink block, just the spinner ring */
  border: 1.5px solid currentColor; border-right-color: transparent; border-radius: 50%;
  transform-origin: center;                 /* the ink set this to left; the spinner needs
                                               its own centre or it orbits its edge */
  transition: none;
  animation: btnspin 700ms linear infinite; /* same spinner as primary/secondary */
}
.btn--tertiary[data-busy="true"]::after{ display: none; }        /* no rule while busy */
.btn--tertiary[data-busy="true"] .btn-lbl-inv{ display: none; }   /* no invert copy */
@keyframes btnspin{ to{ transform: rotate(360deg); } }

/* --- FORBIDDEN ---------------------------------------------------------
   .btn--tertiary.btn--lg     18px text button competes with h3
   .btn--danger.btn--lg       destructive is never the largest thing on screen
   .btn--plate at sm or md    it exists for one job, the hero
   .btn--plate off imagery    hardcoded colour, cannot theme, fails on cream
   .btn--danger on a guest page   nothing a guest presses is destructive
   two primaries in one view  then neither is primary
   two .btn--lg in one section    it is a closing gesture, not a toolbar
   disabled on the RSVP submit    use aria-disabled + say what is missing
   ----------------------------------------------------------------------- */

/* text link styled as an action (inline, inside prose) */
.link-text{
  font-family: var(--font-serif);
  font-weight: var(--weight-light);
  font-size: 15px;
  letter-spacing: 0.06em;
  color: var(--color-link);
  text-decoration: none;
  background: none;
  border: 0; border-bottom: 1px solid currentColor;
  padding-bottom: 2px;
  cursor: pointer;
}
.link-text:hover{ color: var(--color-link-hover); }


/* ==========================================================================
   FORM FIELDS  (Inter throughout — no serif in labels)
   ========================================================================== */

.field{ display: flex; flex-direction: column; gap: 6px; max-width: 340px; width: 100%; }

.field label{
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.17em;
  text-transform: uppercase; color: var(--color-text-muted);
  transition: color var(--duration-fast) var(--ease);
}
.field:focus-within label{ color: var(--color-heading); }
.field .req{ color: var(--color-danger); margin-left: 0.35em; }  /* on the label, not colour-only */

.field input, .field select, .field textarea{
  font-family: var(--font-ui); font-size: 14px;
  padding: var(--input-padding);
  border: var(--border-width) solid var(--color-border-strong);   /* WCAG 1.4.11 */
  border-radius: var(--radius-sm);
  background: var(--color-bg); color: var(--color-text); width: 100%;
  transition: border-color var(--duration-fast) var(--ease),
              box-shadow   var(--duration-fast) var(--ease);
}
.field input:hover{ border-color: var(--color-text); }

/* FOCUS — transient and positional: "you are here". Identical to the button
   treatment so focus looks like one thing everywhere. NO width change. */
.field input:focus-visible{
  border-color: var(--color-focus);
  outline: none;
  box-shadow: var(--focus-ring);
}

/* INVALID — persistent and evaluative: "this is wrong". Structural, not
   chromatic, and the same left-rule language as .notice, so trouble reads the
   same everywhere on the site.
   Why inset box-shadow and not a heavier border: a 2px border under border-box
   shrinks the content box and the text inside jumps when validation fires.
   This costs zero layout and composes with the focus ring by comma-chaining. */
.field.is-invalid input{
  box-shadow: inset 3px 0 0 var(--color-danger);
  padding-left: calc(0.875rem + 3px);
}
.field.is-invalid input:focus-visible{
  box-shadow: inset 3px 0 0 var(--color-danger), var(--focus-ring);
}

.field input[readonly]{
  background: var(--color-surface-alt); border-style: dashed;
  color: var(--color-text-muted);
}
.field input[disabled]{
  background: var(--color-disabled-bg); border-color: var(--color-disabled-border);
  color: var(--color-disabled-text); cursor: not-allowed;
}

.field .hint{ font-family: var(--font-ui); font-size: 11.5px; color: var(--color-text-muted); }
.field .err{
  font-family: var(--font-ui); font-size: 11.5px; color: var(--color-danger);
  display: flex; gap: 6px; align-items: flex-start; line-height: 1.5;
  animation: errIn var(--duration-base) var(--ease-out-soft) both;
}
@keyframes errIn{ from{ opacity: 0; transform: translateY(-4px); } to{ opacity: 1; transform: none; } }

/* Errors need aria-invalid + aria-describedby on the input, the ✕ mark in the
   message, and copy that says what to do rather than what went wrong. */


/* ==========================================================================
   CARDS — surface-alt, never Sand (muted + link text fail on Sand)
   ========================================================================== */

.card{
  border: var(--border-width) solid var(--color-border);
  background: var(--color-surface-alt);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  max-width: 330px;
}
.card .k{
  font-family: var(--font-ui); font-size: 10.5px; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 0.9em;
}
.card .v{
  font-family: var(--font-serif); font-weight: var(--weight-light);
  font-size: 1.5rem; line-height: 1.2; color: var(--color-heading);
}
.card .d{ font-size: 13.5px; color: var(--color-text); margin: 0.7em 0 0; }


/* ==========================================================================
   CALENDAR CTA + DROPDOWN
   .ics is a static file, not generated per request.
   Closes on outside click and Escape; focus returns to the trigger.
   ========================================================================== */

.cal{ position: relative; display: inline-block; }
.cal-menu{
  position: absolute; top: calc(100% + 8px); left: 0; min-width: 220px;
  background: var(--color-bg);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lift);
  padding: 6px 0; display: none; z-index: var(--z-overlay);
}
.cal-menu[data-open="true"]{ display: block; }
.cal-menu a{
  display: block; font-family: var(--font-ui); font-size: 13px;
  padding: 9px 16px; color: var(--color-text); text-decoration: none;
}
.cal-menu a:hover{ background: var(--color-surface-alt); color: var(--color-heading); }

/* roman, never italic — italic at 15px on cream goes soft */
.fineprint{
  font-family: var(--font-serif); font-weight: var(--weight-light);
  font-size: 15px; font-style: normal; color: var(--color-text-muted); margin-top: 14px;
}


/* ==========================================================================
   EYEBROW · SECTION HEADER · DIVIDER
   ========================================================================== */

.eyebrow{
  font-family: var(--font-serif);
  font-weight: var(--weight-semibold);
  font-size: 12.5px;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--color-eyebrow);
  margin-bottom: 1.1em;
}

.sechead{ text-align: center; max-width: 52ch; margin: 0 auto; }
.sechead h2, .sechead h5{
  font-weight: var(--weight-display);
  font-size: clamp(2rem, 4.6vw, 3.2rem);
  line-height: 1.08; letter-spacing: -0.012em;
  color: var(--color-heading); margin: 0 0 0.5em;
}
.sechead h2 em, .sechead h5 em{ font-style: italic; color: var(--color-link); }

/* one ornament per section at most */
.orn{ display: flex; align-items: center; gap: 14px; margin: var(--space-5) 0; }
.orn::before, .orn::after{ content: ""; flex: 1; height: 1px; background: var(--color-border); }
.orn svg{ width: 34px; height: 12px; color: var(--color-border); flex: none; }


/* ==========================================================================
   NAV — transparent over the hero, frosted cream on scroll
   ========================================================================== */

.navbar{
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px var(--section-inline);
}
.navbar .mono{
  font-family: var(--font-graphic); font-weight: 600;
  font-size: 15px; letter-spacing: 0.3em; color: var(--color-heading);
}
.navbar ul{ display: flex; gap: 26px; list-style: none; margin: 0; padding: 0; }
.navbar a{
  font-family: var(--font-serif); font-weight: var(--weight-light);
  font-size: 14px; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--color-text); text-decoration: none; position: relative; padding: 3px 0;
}
.navbar a::after{
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
  background: currentColor; transform: scaleX(0); transform-origin: left;
  transition: transform var(--duration-base) var(--ease);
}
.navbar a:hover::after{ transform: scaleX(1); }


/* ==========================================================================
   TEXTURE OVERLAYS — inline SVG turbulence, no extra request, no CDN
   Apply to a positioned parent; each is an absolutely-positioned child.
   ========================================================================== */

.tex-weave, .tex-grain, .tex-dark, .tex-warm{
  position: absolute; inset: 0; pointer-events: none; display: block;
}
.tex-weave{
  mix-blend-mode: multiply; opacity: 0.35;
  background-image:
    repeating-linear-gradient(0deg,  rgba(9,47,51,.16) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(90deg, rgba(9,47,51,.16) 0 1px, transparent 1px 3px);
}
.tex-grain{
  mix-blend-mode: multiply; opacity: 0.5;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='g'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/><feColorMatrix type='saturate' values='0'/></filter><rect width='180' height='180' filter='url(%23g)' opacity='0.4'/></svg>");
}
.tex-warm{
  mix-blend-mode: soft-light;
  background: radial-gradient(120% 80% at 50% 88%, rgba(175,80,49,.28), transparent 62%);
}
/* legibility gradient — warm, never neutral black */
.tex-dark{
  background: linear-gradient(to top,
    rgba(9,47,51,.72) 0%, rgba(9,47,51,.28) 40%, rgba(9,47,51,0) 78%);
}


/* ==========================================================================
   LAYOUT HELPERS
   ========================================================================== */

.section{ padding: var(--section-block) var(--section-inline); }
.section-inner{ max-width: var(--container-wide); margin: 0 auto; }
.section-inner--text{ max-width: var(--container); }
.prose{ max-width: var(--measure); }


/* ==========================================================================
   NOTICE — state is never signalled by colour alone, so the first line always
   says what happened and the second says what to do.
   ========================================================================== */

.notice{
  display: flex; gap: 12px;
  border: var(--border-width) solid var(--color-border-strong);
  border-left-width: 3px;
  padding: 14px 18px; max-width: 60ch;
  background: var(--color-surface-alt);
  font-family: var(--font-ui); font-size: 13.5px; line-height: 1.6;
}
.notice b{ display: block; font-weight: 500; margin-bottom: 2px; color: var(--color-heading); }
.notice--danger{ border-color: var(--color-danger); }
.notice--action{ border-color: var(--color-primary); }


/* ==========================================================================
   TAG — admin guest tagging. Tint is mixed, never a new primitive.
   ========================================================================== */

.tag{
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.09em;
  text-transform: uppercase; padding: 3px 9px;
  border: var(--border-width) solid var(--color-border-strong);
  color: var(--color-text);
  background: color-mix(in srgb, var(--color-primary) 8%, var(--color-bg));
}
.tag--muted{ background: transparent; color: var(--color-text-muted); }


/* ==========================================================================
   SCHEDULE — Inter tabular numerals keep the time column straight; the event
   itself is Fraunces. Two faces, two jobs, one row.
   ========================================================================== */

.sched{ border-top: var(--border-width) solid var(--color-border); max-width: var(--container); }
.sched-row{
  display: grid; grid-template-columns: 118px 1fr; gap: 22px;
  padding: 18px 0; border-bottom: var(--border-width) solid var(--color-border);
}
.sched-time{
  font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--color-text-muted);
  font-variant-numeric: tabular-nums; padding-top: 0.35em;
}
.sched-what{
  font-family: var(--font-serif); font-weight: var(--weight-light);
  font-size: 1.32rem; line-height: 1.25; color: var(--color-heading);
}
.sched-note{ font-size: var(--text-sm); color: var(--color-text); margin: 0.4em 0 0; }
@media (max-width: 560px){ .sched-row{ grid-template-columns: 1fr; gap: 6px; } }


/* ==========================================================================
   FAQ — native <details>. Keyboard accessible for free, and it still works if
   a script fails to load, which in China is a real scenario.
   ========================================================================== */

.faq{ border-bottom: var(--border-width) solid var(--color-border); max-width: var(--measure); }
.faq summary{
  list-style: none; cursor: pointer; padding: 16px 0;
  display: flex; justify-content: space-between; gap: 18px; align-items: baseline;
  font-family: var(--font-serif); font-weight: var(--weight-heading);
  font-size: 1.12rem; color: var(--color-heading);
}
.faq summary::-webkit-details-marker{ display: none; }
.faq summary::after{ content: "+"; font-family: var(--font-ui); font-size: 15px;
  color: var(--color-text-muted); flex: none; }
.faq[open] summary::after{ content: "–"; }
.faq summary:focus-visible{ outline: none; box-shadow: var(--focus-ring); }
.faq p{ font-size: var(--text-sm); margin: 0 0 16px; color: var(--color-text); }


/* ==========================================================================
   COUNTDOWN
   ========================================================================== */

.count{ display: flex; gap: clamp(18px, 5vw, 60px); justify-content: center; }
.count-cell{ display: flex; flex-direction: column; align-items: center; }
.count-num{
  font-family: var(--font-serif); font-weight: var(--weight-display);
  font-size: clamp(2.6rem, 7vw, 4.6rem); line-height: 1;
  color: var(--color-heading); font-variant-numeric: tabular-nums;
}
.count-lab{
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.22em;
  text-transform: uppercase; color: var(--color-text-muted); margin-top: 0.9em;
}
:lang(zh) .count-lab{ letter-spacing: 0.14em; }


/* ==========================================================================
   LANGUAGE TOGGLE
   The inactive label is written in its own language and marked with lang, so a
   screen reader pronounces 中文 in Chinese and the browser renders it in the CJK
   stack rather than in a Fraunces fallback.
   Links must point at the EQUIVALENT page, never the home page.
   ========================================================================== */

.langbar{ display: inline-flex; border: var(--border-width) solid var(--color-border-strong); }
.langbar a{
  font-family: var(--font-ui); font-size: 12px; letter-spacing: 0.08em;
  padding: 6px 13px; text-decoration: none; color: var(--color-text);
  border-right: var(--border-width) solid var(--color-border-strong);
  transition: background var(--duration-fast) var(--ease),
              color var(--duration-fast) var(--ease);
}
.langbar a:last-child{ border-right: 0; }
.langbar a[aria-current="true"]{ background: var(--color-primary); color: var(--color-on-primary); }
.langbar a:lang(zh){ font-family: var(--font-ui-zh); }


/* ==========================================================================
   MOTION
   Opacity and transform only — both composite on the GPU. Never animate width,
   height, top, letter-spacing or shadow blur: they force layout every frame.

   Reveals are gated on html[data-motion="on"], which JS sets ONLY when
   prefers-reduced-motion is not set. With no JS nothing is hidden and nothing
   animates; the page is static and complete. Never hide content in CSS and
   depend on JS to reveal it.

     var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
     if (!reduce && 'IntersectionObserver' in window) {
       document.documentElement.setAttribute('data-motion','on');
       // observe [data-reveal], add .is-in, set --rd stagger, then unobserve
     }
   ========================================================================== */

html[data-motion="on"] [data-reveal]{ opacity: 0; transform: translateY(12px); }
html[data-motion="on"] [data-reveal].is-in{
  opacity: 1; transform: none;
  transition: opacity   var(--duration-reveal) var(--ease-out-soft) var(--rd, 0ms),
              transform var(--duration-reveal) var(--ease-out-soft) var(--rd, 0ms);
}

/* hairline rules draw out from the centre */
.orn::before{ transform-origin: right; }
.orn::after{ transform-origin: left; }
html[data-motion="on"] [data-reveal] .orn::before,
html[data-motion="on"] [data-reveal] .orn::after{ transform: scaleX(0); }
html[data-motion="on"] [data-reveal].is-in .orn::before,
html[data-motion="on"] [data-reveal].is-in .orn::after{
  transform: scaleX(1);
  transition: transform var(--duration-settle) var(--ease-out-soft) 120ms;
}

/* plates settle rather than pop */
html[data-motion="on"] [data-reveal].plate-img{ transform: scale(1.025); }
html[data-motion="on"] [data-reveal].plate-img.is-in{
  transform: none;
  transition: transform var(--duration-settle) var(--ease-out-soft),
              opacity   var(--duration-reveal) var(--ease-out-soft);
}

/* cards lift by exactly one pixel. More than that and it stops being letterpress. */
.card{ transition: border-color var(--duration-base) var(--ease),
                   transform    var(--duration-base) var(--ease); }
.card:hover{ border-color: var(--color-border-strong); transform: translateY(-1px); }

/* countdown digits crossfade on change */
.count-num{ transition: opacity var(--duration-base) var(--ease); }
.count-num.ticking{ opacity: 0.25; }

/* Reveals fire once. Re-animating on scroll-up is the tell of a template.
   Stagger caps at four items: past that, 70ms becomes a queue you wait on. */


/* ============================================================
   PATTERN — tone-on-tone tiling behind whole sections.
   Drawn as SVG MASKS, not images, so the ink is a token and the
   pattern themes with the palette. An image would be baked to one
   colour and would need re-exporting per palette.

   CONTRAST CEILING, measured, ink over background:
     opacity .05 on night  = 1.15:1     .07 on light = 1.15:1
     opacity .09 on night  = 1.29:1     .09 on light = 1.19:1
     opacity .12 on night  = 1.41:1     .12 on light = 1.27:1
   Behind body text: never above 1.15:1, so .05 dark / .07 light.
   Decorative panels with no text may go to --pattern-opacity-bold.
   ============================================================ */
.pattern-host{position:relative;isolation:isolate}
.pattern-host>*:not(.pattern){position:relative;z-index:1}
.pattern{position:absolute;inset:0;z-index:0;pointer-events:none;
  background-color:var(--pattern-ink);opacity:var(--pattern-opacity);
  -webkit-mask-repeat:repeat;mask-repeat:repeat;
  -webkit-mask-position:center;mask-position:center}
.pattern--vine{-webkit-mask-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27240%27%20height%3D%27240%27%20viewBox%3D%270%200%20240%20240%27%3E%3Cg%20transform%3D%27translate%2852%20196%29%20rotate%28-8%29%20scale%281.00%29%27%3E%3Cpath%20d%3D%27M0%200c0%20-14%206%20-26%2015%20-35c9%20-9%2013%20-20%2012%20-33%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M0%20-14c-13%20-3%20-21%20-14%20-20%20-27c13%202%2022%2013%2020%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Cpath%20d%3D%27M8%20-34c13%20-3%2021%20-14%2020%20-27c-13%202%20-22%2013%20-20%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Ccircle%20cx%3D%2727%27%20cy%3D%27-68%27%20r%3D%273.4%27%20fill%3D%27%23fff%27%2F%3E%3C%2Fg%3E%3Cg%20transform%3D%27translate%28160%20150%29%20rotate%28168%29%20scale%280.86%29%27%3E%3Cpath%20d%3D%27M0%200c0%20-14%206%20-26%2015%20-35c9%20-9%2013%20-20%2012%20-33%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M0%20-14c-13%20-3%20-21%20-14%20-20%20-27c13%202%2022%2013%2020%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Cpath%20d%3D%27M8%20-34c13%20-3%2021%20-14%2020%20-27c-13%202%20-22%2013%20-20%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Ccircle%20cx%3D%2727%27%20cy%3D%27-68%27%20r%3D%273.4%27%20fill%3D%27%23fff%27%2F%3E%3C%2Fg%3E%3Cg%20transform%3D%27translate%28200%20232%29%20rotate%2814%29%20scale%280.72%29%27%3E%3Cpath%20d%3D%27M0%200c0%20-14%206%20-26%2015%20-35c9%20-9%2013%20-20%2012%20-33%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M0%20-14c-13%20-3%20-21%20-14%20-20%20-27c13%202%2022%2013%2020%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Cpath%20d%3D%27M8%20-34c13%20-3%2021%20-14%2020%20-27c-13%202%20-22%2013%20-20%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Ccircle%20cx%3D%2727%27%20cy%3D%27-68%27%20r%3D%273.4%27%20fill%3D%27%23fff%27%2F%3E%3C%2Fg%3E%3Cg%20transform%3D%27translate%2896%2092%29%20rotate%28196%29%20scale%280.78%29%27%3E%3Cpath%20d%3D%27M0%200c0%20-14%206%20-26%2015%20-35c9%20-9%2013%20-20%2012%20-33%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M0%20-14c-13%20-3%20-21%20-14%20-20%20-27c13%202%2022%2013%2020%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Cpath%20d%3D%27M8%20-34c13%20-3%2021%20-14%2020%20-27c-13%202%20-22%2013%20-20%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Ccircle%20cx%3D%2727%27%20cy%3D%27-68%27%20r%3D%273.4%27%20fill%3D%27%23fff%27%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");mask-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27240%27%20height%3D%27240%27%20viewBox%3D%270%200%20240%20240%27%3E%3Cg%20transform%3D%27translate%2852%20196%29%20rotate%28-8%29%20scale%281.00%29%27%3E%3Cpath%20d%3D%27M0%200c0%20-14%206%20-26%2015%20-35c9%20-9%2013%20-20%2012%20-33%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M0%20-14c-13%20-3%20-21%20-14%20-20%20-27c13%202%2022%2013%2020%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Cpath%20d%3D%27M8%20-34c13%20-3%2021%20-14%2020%20-27c-13%202%20-22%2013%20-20%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Ccircle%20cx%3D%2727%27%20cy%3D%27-68%27%20r%3D%273.4%27%20fill%3D%27%23fff%27%2F%3E%3C%2Fg%3E%3Cg%20transform%3D%27translate%28160%20150%29%20rotate%28168%29%20scale%280.86%29%27%3E%3Cpath%20d%3D%27M0%200c0%20-14%206%20-26%2015%20-35c9%20-9%2013%20-20%2012%20-33%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M0%20-14c-13%20-3%20-21%20-14%20-20%20-27c13%202%2022%2013%2020%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Cpath%20d%3D%27M8%20-34c13%20-3%2021%20-14%2020%20-27c-13%202%20-22%2013%20-20%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Ccircle%20cx%3D%2727%27%20cy%3D%27-68%27%20r%3D%273.4%27%20fill%3D%27%23fff%27%2F%3E%3C%2Fg%3E%3Cg%20transform%3D%27translate%28200%20232%29%20rotate%2814%29%20scale%280.72%29%27%3E%3Cpath%20d%3D%27M0%200c0%20-14%206%20-26%2015%20-35c9%20-9%2013%20-20%2012%20-33%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M0%20-14c-13%20-3%20-21%20-14%20-20%20-27c13%202%2022%2013%2020%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Cpath%20d%3D%27M8%20-34c13%20-3%2021%20-14%2020%20-27c-13%202%20-22%2013%20-20%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Ccircle%20cx%3D%2727%27%20cy%3D%27-68%27%20r%3D%273.4%27%20fill%3D%27%23fff%27%2F%3E%3C%2Fg%3E%3Cg%20transform%3D%27translate%2896%2092%29%20rotate%28196%29%20scale%280.78%29%27%3E%3Cpath%20d%3D%27M0%200c0%20-14%206%20-26%2015%20-35c9%20-9%2013%20-20%2012%20-33%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M0%20-14c-13%20-3%20-21%20-14%20-20%20-27c13%202%2022%2013%2020%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Cpath%20d%3D%27M8%20-34c13%20-3%2021%20-14%2020%20-27c-13%202%20-22%2013%20-20%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Ccircle%20cx%3D%2727%27%20cy%3D%27-68%27%20r%3D%273.4%27%20fill%3D%27%23fff%27%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
  -webkit-mask-size:240px 240px;mask-size:240px 240px}
.pattern--bold{opacity:var(--pattern-opacity-bold)}
.pattern--fade{-webkit-mask-composite:source-in;
  mask-composite:intersect}

/* ============================================================
   PORTRAIT — the one place the zero-radius rule does not apply.
   That rule governs UI chrome. A masked photograph is a picture
   frame, and picture frames have shapes.

   GRADING IS DONE IN CSS, NOT BAKED. Two blend layers split-tone
   the image into the palette: shadows lift toward the section's
   dark, highlights pull toward parchment. Re-palette the site and
   the photographs follow. Note what is NOT here: the warm
   soft-light push used on generated plates. Over a face it reads
   as jaundice, which is why photographs need their own recipe.
   ============================================================ */
.portrait{ display: block; margin: 0; padding: 0; border: 0; background: none; max-width: 340px; }
.portrait-inner{
  position: relative; overflow: hidden; isolation: isolate;
  aspect-ratio: var(--portrait-ratio, 1 / 1);
  border-radius: 0;              /* square. The frame and the arch were dropped:
                                    a mount is one more thing to get wrong on a
                                    phone, and the grading already separates the
                                    photograph from the page. */
}
.portrait img{
  width: 100%; height: 100%; object-fit: cover; display: block;
  filter: saturate(0.84) contrast(1.04) brightness(1.01);
}
.portrait-inner::before, .portrait-inner::after{
  content: ""; position: absolute; inset: 0; pointer-events: none;
}
.portrait-inner::before{ background: var(--photo-shadow); mix-blend-mode: lighten; opacity: 0.46; }
.portrait-inner::after{  background: var(--photo-light);  mix-blend-mode: darken;  opacity: 0.22; }
.portrait-grain{
  position: absolute; inset: 0; pointer-events: none; z-index: 3;
  mix-blend-mode: multiply; opacity: 0.34;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='150' height='150'><filter id='pg'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/><feColorMatrix type='saturate' values='0'/></filter><rect width='150' height='150' filter='url(%23pg)' opacity='0.4'/></svg>");
}
.portrait figcaption{
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--color-text-muted); padding-top: 11px;
}


/* ==========================================================================
   SCRIPT OVERLAP — a line set across the edge of a photograph, half on the
   paper and half on the image.

   Uses Fraunces italic rather than a fourth webfont. A true script face costs
   another 60-80 KB and a fourth family to govern; italic Fraunces at 300
   already reads as written rather than typeset. If a real script is wanted
   later it drops in here and nowhere else.

   In Chinese the script variant switches to Song at normal style, because
   there is no italic and a synthesised slant on characters is a shear.
   ========================================================================== */

.figure-script{ position: relative; display: block; max-width: 520px; }
.figure-script .script{
  position: absolute; z-index: 4;
  font-family: var(--font-serif); font-style: italic; font-weight: var(--weight-light);
  font-size: clamp(1.35rem, 3.2vw, 2.05rem); line-height: 1.15; letter-spacing: 0.01em;
  color: var(--color-eyebrow);
  text-shadow: 0 1px 14px rgba(26, 39, 51, 0.35);   /* legibility over any photo */
  max-width: 11ch;
}
.figure-script .script--tl{ left: -6%; top: 8%; }
.figure-script .script--br{ right: -4%; bottom: 9%; text-align: right; }
.figure-script .script--across{ left: 6%; bottom: -0.55em; max-width: none; }
:lang(zh) .figure-script .script{ font-style: normal; letter-spacing: 0.1em; line-height: 1.35; }
@media (max-width: 640px){
  .figure-script .script{ font-size: 1.25rem; max-width: 9ch; }
  .figure-script .script--tl{ left: 0; }
  .figure-script .script--br{ right: 0; }
}


/* ==========================================================================
   PAPER — a heavier, dirtier grain than the page texture, for sections that
   should read as stock rather than as screen. Multiply on light, overlay on
   the night, because multiply on a dark field just makes mud.
   ========================================================================== */

.paper{
  position: absolute; inset: 0; pointer-events: none; z-index: 0;
  mix-blend-mode: multiply; opacity: var(--paper-opacity, 0.5);
  background-image:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='s'><feTurbulence type='fractalNoise' baseFrequency='0.62' numOctaves='4' seed='7'/><feColorMatrix type='saturate' values='0'/></filter><rect width='220' height='220' filter='url(%23s)' opacity='0.55'/></svg>"),
    repeating-linear-gradient(96deg, rgba(0,0,0,.05) 0 1px, transparent 1px 4px);
  background-size: 220px 220px, auto;
}
.paper--heavy{ --paper-opacity: 0.72; }
.nocturne .paper{ mix-blend-mode: overlay; opacity: 0.4; }


/* ==========================================================================
   SCRIPT OVERLAP — a line set across the edge of a photograph, half on the
   paper and half on the image.

   Uses Fraunces italic rather than a fourth webfont. A true script face costs
   another 60-80 KB and a fourth family to govern; italic Fraunces at 300
   already reads as written rather than typeset. If a real script is wanted
   later it drops in here and nowhere else.

   In Chinese the script variant switches to Song at normal style, because
   there is no italic and a synthesised slant on characters is a shear.
   ========================================================================== */

.figure-script{ position: relative; display: block; max-width: 520px; }
.figure-script .script{
  position: absolute; z-index: 4;
  font-family: var(--font-serif); font-style: italic; font-weight: var(--weight-light);
  font-size: clamp(1.35rem, 3.2vw, 2.05rem); line-height: 1.15; letter-spacing: 0.01em;
  color: var(--color-eyebrow);
  text-shadow: 0 1px 14px rgba(26, 39, 51, 0.35);   /* legibility over any photo */
  max-width: 11ch;
}
.figure-script .script--tl{ left: -6%; top: 8%; }
.figure-script .script--br{ right: -4%; bottom: 9%; text-align: right; }
.figure-script .script--across{ left: 6%; bottom: -0.55em; max-width: none; }
:lang(zh) .figure-script .script{ font-style: normal; letter-spacing: 0.1em; line-height: 1.35; }
@media (max-width: 640px){
  .figure-script .script{ font-size: 1.25rem; max-width: 9ch; }
  .figure-script .script--tl{ left: 0; }
  .figure-script .script--br{ right: 0; }
}


/* ==========================================================================
   PAPER — a heavier, dirtier grain than the page texture, for sections that
   should read as stock rather than as screen. Multiply on light, overlay on
   the night, because multiply on a dark field just makes mud.
   ========================================================================== */

.paper{
  position: absolute; inset: 0; pointer-events: none; z-index: 0;
  mix-blend-mode: multiply; opacity: var(--paper-opacity, 0.5);
  background-image:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='s'><feTurbulence type='fractalNoise' baseFrequency='0.62' numOctaves='4' seed='7'/><feColorMatrix type='saturate' values='0'/></filter><rect width='220' height='220' filter='url(%23s)' opacity='0.55'/></svg>"),
    repeating-linear-gradient(96deg, rgba(0,0,0,.05) 0 1px, transparent 1px 4px);
  background-size: 220px 220px, auto;
}
.paper--heavy{ --paper-opacity: 0.72; }
.nocturne .paper{ mix-blend-mode: overlay; opacity: 0.4; }


/* ring caption — text set around the frame, as in the reference */
/* ==========================================================================
   NOTICE — state is never signalled by colour alone, so the first line always
   says what happened and the second says what to do.
   ========================================================================== */

.notice{
  display: flex; gap: 12px;
  border: var(--border-width) solid var(--color-border-strong);
  border-left-width: 3px;
  padding: 14px 18px; max-width: 60ch;
  background: var(--color-surface-alt);
  font-family: var(--font-ui); font-size: 13.5px; line-height: 1.6;
}
.notice b{ display: block; font-weight: 500; margin-bottom: 2px; color: var(--color-heading); }
.notice--danger{ border-color: var(--color-danger); }
.notice--action{ border-color: var(--color-primary); }


/* ==========================================================================
   TAG — admin guest tagging. Tint is mixed, never a new primitive.
   ========================================================================== */

.tag{
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.09em;
  text-transform: uppercase; padding: 3px 9px;
  border: var(--border-width) solid var(--color-border-strong);
  color: var(--color-text);
  background: color-mix(in srgb, var(--color-primary) 8%, var(--color-bg));
}
.tag--muted{ background: transparent; color: var(--color-text-muted); }


/* ==========================================================================
   SCHEDULE — Inter tabular numerals keep the time column straight; the event
   itself is Fraunces. Two faces, two jobs, one row.
   ========================================================================== */

.sched{ border-top: var(--border-width) solid var(--color-border); max-width: var(--container); }
.sched-row{
  display: grid; grid-template-columns: 118px 1fr; gap: 22px;
  padding: 18px 0; border-bottom: var(--border-width) solid var(--color-border);
}
.sched-time{
  font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--color-text-muted);
  font-variant-numeric: tabular-nums; padding-top: 0.35em;
}
.sched-what{
  font-family: var(--font-serif); font-weight: var(--weight-light);
  font-size: 1.32rem; line-height: 1.25; color: var(--color-heading);
}
.sched-note{ font-size: var(--text-sm); color: var(--color-text); margin: 0.4em 0 0; }
@media (max-width: 560px){ .sched-row{ grid-template-columns: 1fr; gap: 6px; } }


/* ==========================================================================
   FAQ — native <details>. Keyboard accessible for free, and it still works if
   a script fails to load, which in China is a real scenario.
   ========================================================================== */

.faq{ border-bottom: var(--border-width) solid var(--color-border); max-width: var(--measure); }
.faq summary{
  list-style: none; cursor: pointer; padding: 16px 0;
  display: flex; justify-content: space-between; gap: 18px; align-items: baseline;
  font-family: var(--font-serif); font-weight: var(--weight-heading);
  font-size: 1.12rem; color: var(--color-heading);
}
.faq summary::-webkit-details-marker{ display: none; }
.faq summary::after{ content: "+"; font-family: var(--font-ui); font-size: 15px;
  color: var(--color-text-muted); flex: none; }
.faq[open] summary::after{ content: "–"; }
.faq summary:focus-visible{ outline: none; box-shadow: var(--focus-ring); }
.faq p{ font-size: var(--text-sm); margin: 0 0 16px; color: var(--color-text); }


/* ==========================================================================
   COUNTDOWN
   ========================================================================== */

.count{ display: flex; gap: clamp(18px, 5vw, 60px); justify-content: center; }
.count-cell{ display: flex; flex-direction: column; align-items: center; }
.count-num{
  font-family: var(--font-serif); font-weight: var(--weight-display);
  font-size: clamp(2.6rem, 7vw, 4.6rem); line-height: 1;
  color: var(--color-heading); font-variant-numeric: tabular-nums;
}
.count-lab{
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.22em;
  text-transform: uppercase; color: var(--color-text-muted); margin-top: 0.9em;
}
:lang(zh) .count-lab{ letter-spacing: 0.14em; }


/* ==========================================================================
   LANGUAGE TOGGLE
   The inactive label is written in its own language and marked with lang, so a
   screen reader pronounces 中文 in Chinese and the browser renders it in the CJK
   stack rather than in a Fraunces fallback.
   Links must point at the EQUIVALENT page, never the home page.
   ========================================================================== */

.langbar{ display: inline-flex; border: var(--border-width) solid var(--color-border-strong); }
.langbar a{
  font-family: var(--font-ui); font-size: 12px; letter-spacing: 0.08em;
  padding: 6px 13px; text-decoration: none; color: var(--color-text);
  border-right: var(--border-width) solid var(--color-border-strong);
  transition: background var(--duration-fast) var(--ease),
              color var(--duration-fast) var(--ease);
}
.langbar a:last-child{ border-right: 0; }
.langbar a[aria-current="true"]{ background: var(--color-primary); color: var(--color-on-primary); }
.langbar a:lang(zh){ font-family: var(--font-ui-zh); }


/* ==========================================================================
   MOTION
   Opacity and transform only — both composite on the GPU. Never animate width,
   height, top, letter-spacing or shadow blur: they force layout every frame.

   Reveals are gated on html[data-motion="on"], which JS sets ONLY when
   prefers-reduced-motion is not set. With no JS nothing is hidden and nothing
   animates; the page is static and complete. Never hide content in CSS and
   depend on JS to reveal it.

     var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
     if (!reduce && 'IntersectionObserver' in window) {
       document.documentElement.setAttribute('data-motion','on');
       // observe [data-reveal], add .is-in, set --rd stagger, then unobserve
     }
   ========================================================================== */

html[data-motion="on"] [data-reveal]{ opacity: 0; transform: translateY(12px); }
html[data-motion="on"] [data-reveal].is-in{
  opacity: 1; transform: none;
  transition: opacity   var(--duration-reveal) var(--ease-out-soft) var(--rd, 0ms),
              transform var(--duration-reveal) var(--ease-out-soft) var(--rd, 0ms);
}

/* hairline rules draw out from the centre */
.orn::before{ transform-origin: right; }
.orn::after{ transform-origin: left; }
html[data-motion="on"] [data-reveal] .orn::before,
html[data-motion="on"] [data-reveal] .orn::after{ transform: scaleX(0); }
html[data-motion="on"] [data-reveal].is-in .orn::before,
html[data-motion="on"] [data-reveal].is-in .orn::after{
  transform: scaleX(1);
  transition: transform var(--duration-settle) var(--ease-out-soft) 120ms;
}

/* plates settle rather than pop */
html[data-motion="on"] [data-reveal].plate-img{ transform: scale(1.025); }
html[data-motion="on"] [data-reveal].plate-img.is-in{
  transform: none;
  transition: transform var(--duration-settle) var(--ease-out-soft),
              opacity   var(--duration-reveal) var(--ease-out-soft);
}

/* cards lift by exactly one pixel. More than that and it stops being letterpress. */
.card{ transition: border-color var(--duration-base) var(--ease),
                   transform    var(--duration-base) var(--ease); }
.card:hover{ border-color: var(--color-border-strong); transform: translateY(-1px); }

/* countdown digits crossfade on change */
.count-num{ transition: opacity var(--duration-base) var(--ease); }
.count-num.ticking{ opacity: 0.25; }

/* Reveals fire once. Re-animating on scroll-up is the tell of a template.
   Stagger caps at four items: past that, 70ms becomes a queue you wait on. */


/* ============================================================
   PATTERN — tone-on-tone tiling behind whole sections.
   Drawn as SVG MASKS, not images, so the ink is a token and the
   pattern themes with the palette. An image would be baked to one
   colour and would need re-exporting per palette.

   CONTRAST CEILING, measured, ink over background:
     opacity .05 on night  = 1.15:1     .07 on light = 1.15:1
     opacity .09 on night  = 1.29:1     .09 on light = 1.19:1
     opacity .12 on night  = 1.41:1     .12 on light = 1.27:1
   Behind body text: never above 1.15:1, so .05 dark / .07 light.
   Decorative panels with no text may go to --pattern-opacity-bold.
   ============================================================ */
.pattern-host{position:relative;isolation:isolate}
.pattern-host>*:not(.pattern){position:relative;z-index:1}
.pattern{position:absolute;inset:0;z-index:0;pointer-events:none;
  background-color:var(--pattern-ink);opacity:var(--pattern-opacity);
  -webkit-mask-repeat:repeat;mask-repeat:repeat;
  -webkit-mask-position:center;mask-position:center}
.pattern--vine{-webkit-mask-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27240%27%20height%3D%27240%27%20viewBox%3D%270%200%20240%20240%27%3E%3Cg%20transform%3D%27translate%2852%20196%29%20rotate%28-8%29%20scale%281.00%29%27%3E%3Cpath%20d%3D%27M0%200c0%20-14%206%20-26%2015%20-35c9%20-9%2013%20-20%2012%20-33%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M0%20-14c-13%20-3%20-21%20-14%20-20%20-27c13%202%2022%2013%2020%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Cpath%20d%3D%27M8%20-34c13%20-3%2021%20-14%2020%20-27c-13%202%20-22%2013%20-20%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Ccircle%20cx%3D%2727%27%20cy%3D%27-68%27%20r%3D%273.4%27%20fill%3D%27%23fff%27%2F%3E%3C%2Fg%3E%3Cg%20transform%3D%27translate%28160%20150%29%20rotate%28168%29%20scale%280.86%29%27%3E%3Cpath%20d%3D%27M0%200c0%20-14%206%20-26%2015%20-35c9%20-9%2013%20-20%2012%20-33%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M0%20-14c-13%20-3%20-21%20-14%20-20%20-27c13%202%2022%2013%2020%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Cpath%20d%3D%27M8%20-34c13%20-3%2021%20-14%2020%20-27c-13%202%20-22%2013%20-20%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Ccircle%20cx%3D%2727%27%20cy%3D%27-68%27%20r%3D%273.4%27%20fill%3D%27%23fff%27%2F%3E%3C%2Fg%3E%3Cg%20transform%3D%27translate%28200%20232%29%20rotate%2814%29%20scale%280.72%29%27%3E%3Cpath%20d%3D%27M0%200c0%20-14%206%20-26%2015%20-35c9%20-9%2013%20-20%2012%20-33%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M0%20-14c-13%20-3%20-21%20-14%20-20%20-27c13%202%2022%2013%2020%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Cpath%20d%3D%27M8%20-34c13%20-3%2021%20-14%2020%20-27c-13%202%20-22%2013%20-20%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Ccircle%20cx%3D%2727%27%20cy%3D%27-68%27%20r%3D%273.4%27%20fill%3D%27%23fff%27%2F%3E%3C%2Fg%3E%3Cg%20transform%3D%27translate%2896%2092%29%20rotate%28196%29%20scale%280.78%29%27%3E%3Cpath%20d%3D%27M0%200c0%20-14%206%20-26%2015%20-35c9%20-9%2013%20-20%2012%20-33%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M0%20-14c-13%20-3%20-21%20-14%20-20%20-27c13%202%2022%2013%2020%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Cpath%20d%3D%27M8%20-34c13%20-3%2021%20-14%2020%20-27c-13%202%20-22%2013%20-20%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Ccircle%20cx%3D%2727%27%20cy%3D%27-68%27%20r%3D%273.4%27%20fill%3D%27%23fff%27%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");mask-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27240%27%20height%3D%27240%27%20viewBox%3D%270%200%20240%20240%27%3E%3Cg%20transform%3D%27translate%2852%20196%29%20rotate%28-8%29%20scale%281.00%29%27%3E%3Cpath%20d%3D%27M0%200c0%20-14%206%20-26%2015%20-35c9%20-9%2013%20-20%2012%20-33%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M0%20-14c-13%20-3%20-21%20-14%20-20%20-27c13%202%2022%2013%2020%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Cpath%20d%3D%27M8%20-34c13%20-3%2021%20-14%2020%20-27c-13%202%20-22%2013%20-20%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Ccircle%20cx%3D%2727%27%20cy%3D%27-68%27%20r%3D%273.4%27%20fill%3D%27%23fff%27%2F%3E%3C%2Fg%3E%3Cg%20transform%3D%27translate%28160%20150%29%20rotate%28168%29%20scale%280.86%29%27%3E%3Cpath%20d%3D%27M0%200c0%20-14%206%20-26%2015%20-35c9%20-9%2013%20-20%2012%20-33%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M0%20-14c-13%20-3%20-21%20-14%20-20%20-27c13%202%2022%2013%2020%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Cpath%20d%3D%27M8%20-34c13%20-3%2021%20-14%2020%20-27c-13%202%20-22%2013%20-20%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Ccircle%20cx%3D%2727%27%20cy%3D%27-68%27%20r%3D%273.4%27%20fill%3D%27%23fff%27%2F%3E%3C%2Fg%3E%3Cg%20transform%3D%27translate%28200%20232%29%20rotate%2814%29%20scale%280.72%29%27%3E%3Cpath%20d%3D%27M0%200c0%20-14%206%20-26%2015%20-35c9%20-9%2013%20-20%2012%20-33%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M0%20-14c-13%20-3%20-21%20-14%20-20%20-27c13%202%2022%2013%2020%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Cpath%20d%3D%27M8%20-34c13%20-3%2021%20-14%2020%20-27c-13%202%20-22%2013%20-20%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Ccircle%20cx%3D%2727%27%20cy%3D%27-68%27%20r%3D%273.4%27%20fill%3D%27%23fff%27%2F%3E%3C%2Fg%3E%3Cg%20transform%3D%27translate%2896%2092%29%20rotate%28196%29%20scale%280.78%29%27%3E%3Cpath%20d%3D%27M0%200c0%20-14%206%20-26%2015%20-35c9%20-9%2013%20-20%2012%20-33%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M0%20-14c-13%20-3%20-21%20-14%20-20%20-27c13%202%2022%2013%2020%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Cpath%20d%3D%27M8%20-34c13%20-3%2021%20-14%2020%20-27c-13%202%20-22%2013%20-20%2027z%27%20fill%3D%27%23fff%27%2F%3E%3Ccircle%20cx%3D%2727%27%20cy%3D%27-68%27%20r%3D%273.4%27%20fill%3D%27%23fff%27%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
  -webkit-mask-size:240px 240px;mask-size:240px 240px}
.pattern--bold{opacity:var(--pattern-opacity-bold)}
.pattern--fade{-webkit-mask-composite:source-in;
  mask-composite:intersect}

/* ============================================================
   PORTRAIT — the one place the zero-radius rule does not apply.
   That rule governs UI chrome. A masked photograph is a picture
   frame, and picture frames have shapes.

   GRADING IS DONE IN CSS, NOT BAKED. Two blend layers split-tone
   the image into the palette: shadows lift toward the section's
   dark, highlights pull toward parchment. Re-palette the site and
   the photographs follow. Note what is NOT here: the warm
   soft-light push used on generated plates. Over a face it reads
   as jaundice, which is why photographs need their own recipe.
   ============================================================ */
.portrait{ display: block; margin: 0; padding: 0; border: 0; background: none; max-width: 340px; }
.portrait-inner{
  position: relative; overflow: hidden; isolation: isolate;
  aspect-ratio: var(--portrait-ratio, 1 / 1);
  border-radius: 0;              /* square. The frame and the arch were dropped:
                                    a mount is one more thing to get wrong on a
                                    phone, and the grading already separates the
                                    photograph from the page. */
}
.portrait img{
  width: 100%; height: 100%; object-fit: cover; display: block;
  filter: saturate(0.84) contrast(1.04) brightness(1.01);
}
.portrait-inner::before, .portrait-inner::after{
  content: ""; position: absolute; inset: 0; pointer-events: none;
}
.portrait-inner::before{ background: var(--photo-shadow); mix-blend-mode: lighten; opacity: 0.46; }
.portrait-inner::after{  background: var(--photo-light);  mix-blend-mode: darken;  opacity: 0.22; }
.portrait-grain{
  position: absolute; inset: 0; pointer-events: none; z-index: 3;
  mix-blend-mode: multiply; opacity: 0.34;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='150' height='150'><filter id='pg'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/><feColorMatrix type='saturate' values='0'/></filter><rect width='150' height='150' filter='url(%23pg)' opacity='0.4'/></svg>");
}
.portrait figcaption{
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--color-text-muted); padding-top: 11px;
}


/* ==========================================================================
   SCRIPT OVERLAP — a line set across the edge of a photograph, half on the
   paper and half on the image.

   Uses Fraunces italic rather than a fourth webfont. A true script face costs
   another 60-80 KB and a fourth family to govern; italic Fraunces at 300
   already reads as written rather than typeset. If a real script is wanted
   later it drops in here and nowhere else.

   In Chinese the script variant switches to Song at normal style, because
   there is no italic and a synthesised slant on characters is a shear.
   ========================================================================== */

.figure-script{ position: relative; display: block; max-width: 520px; }
.figure-script .script{
  position: absolute; z-index: 4;
  font-family: var(--font-serif); font-style: italic; font-weight: var(--weight-light);
  font-size: clamp(1.35rem, 3.2vw, 2.05rem); line-height: 1.15; letter-spacing: 0.01em;
  color: var(--color-eyebrow);
  text-shadow: 0 1px 14px rgba(26, 39, 51, 0.35);   /* legibility over any photo */
  max-width: 11ch;
}
.figure-script .script--tl{ left: -6%; top: 8%; }
.figure-script .script--br{ right: -4%; bottom: 9%; text-align: right; }
.figure-script .script--across{ left: 6%; bottom: -0.55em; max-width: none; }
:lang(zh) .figure-script .script{ font-style: normal; letter-spacing: 0.1em; line-height: 1.35; }
@media (max-width: 640px){
  .figure-script .script{ font-size: 1.25rem; max-width: 9ch; }
  .figure-script .script--tl{ left: 0; }
  .figure-script .script--br{ right: 0; }
}


/* ==========================================================================
   PAPER — a heavier, dirtier grain than the page texture, for sections that
   should read as stock rather than as screen. Multiply on light, overlay on
   the night, because multiply on a dark field just makes mud.
   ========================================================================== */

.paper{
  position: absolute; inset: 0; pointer-events: none; z-index: 0;
  mix-blend-mode: multiply; opacity: var(--paper-opacity, 0.5);
  background-image:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='s'><feTurbulence type='fractalNoise' baseFrequency='0.62' numOctaves='4' seed='7'/><feColorMatrix type='saturate' values='0'/></filter><rect width='220' height='220' filter='url(%23s)' opacity='0.55'/></svg>"),
    repeating-linear-gradient(96deg, rgba(0,0,0,.05) 0 1px, transparent 1px 4px);
  background-size: 220px 220px, auto;
}
.paper--heavy{ --paper-opacity: 0.72; }
.nocturne .paper{ mix-blend-mode: overlay; opacity: 0.4; }


/* ==========================================================================
   SCRIPT OVERLAP — a line set across the edge of a photograph, half on the
   paper and half on the image.

   Uses Fraunces italic rather than a fourth webfont. A true script face costs
   another 60-80 KB and a fourth family to govern; italic Fraunces at 300
   already reads as written rather than typeset. If a real script is wanted
   later it drops in here and nowhere else.

   In Chinese the script variant switches to Song at normal style, because
   there is no italic and a synthesised slant on characters is a shear.
   ========================================================================== */

.figure-script{ position: relative; display: block; max-width: 520px; }
.figure-script .script{
  position: absolute; z-index: 4;
  font-family: var(--font-serif); font-style: italic; font-weight: var(--weight-light);
  font-size: clamp(1.35rem, 3.2vw, 2.05rem); line-height: 1.15; letter-spacing: 0.01em;
  color: var(--color-eyebrow);
  text-shadow: 0 1px 14px rgba(26, 39, 51, 0.35);   /* legibility over any photo */
  max-width: 11ch;
}
.figure-script .script--tl{ left: -6%; top: 8%; }
.figure-script .script--br{ right: -4%; bottom: 9%; text-align: right; }
.figure-script .script--across{ left: 6%; bottom: -0.55em; max-width: none; }
:lang(zh) .figure-script .script{ font-style: normal; letter-spacing: 0.1em; line-height: 1.35; }
@media (max-width: 640px){
  .figure-script .script{ font-size: 1.25rem; max-width: 9ch; }
  .figure-script .script--tl{ left: 0; }
  .figure-script .script--br{ right: 0; }
}


/* ==========================================================================
   PAPER — a heavier, dirtier grain than the page texture, for sections that
   should read as stock rather than as screen. Multiply on light, overlay on
   the night, because multiply on a dark field just makes mud.
   ========================================================================== */

.paper{
  position: absolute; inset: 0; pointer-events: none; z-index: 0;
  mix-blend-mode: multiply; opacity: var(--paper-opacity, 0.5);
  background-image:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='s'><feTurbulence type='fractalNoise' baseFrequency='0.62' numOctaves='4' seed='7'/><feColorMatrix type='saturate' values='0'/></filter><rect width='220' height='220' filter='url(%23s)' opacity='0.55'/></svg>"),
    repeating-linear-gradient(96deg, rgba(0,0,0,.05) 0 1px, transparent 1px 4px);
  background-size: 220px 220px, auto;
}
.paper--heavy{ --paper-opacity: 0.72; }
.nocturne .paper{ mix-blend-mode: overlay; opacity: 0.4; }


/* ring caption — text set around the frame, as in the reference */



/* ==========================================================================
   SCATTER — a draggable field of photographs. Click to zoom.

   Three things this has to survive, and most scattered galleries
   do not:
     1. KEYBOARD. Every photograph is a real <button> in DOM order,
        so Tab reaches it and Enter opens it. Focus auto-pans the
        canvas so the focused item is actually on screen. Arrow keys
        pan. A drag-only interface is not an interface.
     2. NO JS. Without script the canvas simply does not transform:
        the photographs are still there, still laid out, still
        openable as links. Nothing is hidden waiting to be revealed.
     3. REDUCED MOTION. Idle drift off, zoom instant, drag still on
        because dragging is input, not decoration.

   The idle drift is the SECOND and LAST looping animation in the
   system, after the cart. It is 20s, translates under 1%, and is
   staggered per item so the field never pulses in unison. Any third
   loop needs a better reason than this one had.
   ========================================================================== */

.scatter{position:relative;overflow:hidden;height:clamp(420px,68vh,640px);
  background:var(--color-bg);border-block:1px solid var(--color-border);
  cursor:grab;touch-action:none;isolation:isolate}
.scatter.is-dragging{cursor:grabbing}
.scatter:focus-visible{outline:none;box-shadow:inset var(--focus-ring)}
.scatter-canvas{position:absolute;top:50%;left:50%;width:1640px;height:1120px;
  margin:-560px 0 0 -820px;will-change:transform}
html[data-motion="on"] .scatter-canvas{transition:transform var(--duration-slow) var(--ease-out-soft)}
.scatter-canvas.no-ease{transition:none}

.scatter-item{position:absolute;left:var(--x);top:var(--y);width:var(--w);
  padding:0;border:0;background:none;cursor:inherit;display:block;
  transform:rotate(var(--r));transform-origin:50% 50%;
  animation:drift 20s var(--ease) infinite;animation-delay:var(--d,0s)}
.scatter-item .shot{display:block;position:relative;overflow:hidden;isolation:isolate;
  aspect-ratio:var(--ar,1/1);background-image:var(--img);background-size:cover;
  background-position:center;filter:saturate(.84) contrast(1.04) brightness(1.01)}
.scatter-item .shot::before,.scatter-item .shot::after{content:"";position:absolute;inset:0}
.scatter-item .shot::before{background:var(--photo-shadow);mix-blend-mode:lighten;opacity:.46}
.scatter-item .shot::after{background:var(--photo-light);mix-blend-mode:darken;opacity:.22}
.scatter-item:focus-visible{outline:none}
.scatter-item:focus-visible .shot{box-shadow:var(--focus-ring)}
.scatter-item:hover{z-index:5}
.scatter-item .cap{display:block;font-family:var(--font-ui);font-size:10px;
  letter-spacing:.14em;text-transform:uppercase;color:var(--color-text-muted);
  padding-top:7px;text-align:left;opacity:0;
  transition:opacity var(--duration-base) var(--ease)}
.scatter-item:hover .cap,.scatter-item:focus-visible .cap{opacity:1}
@keyframes drift{0%,100%{translate:0 0}33%{translate:0 -8px}66%{translate:5px 3px}}

.scatter-hint{position:absolute;right:14px;bottom:12px;z-index:6;pointer-events:none;
  font-family:var(--font-ui);font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--color-text-muted);background:color-mix(in srgb,var(--color-bg) 82%,transparent);
  padding:5px 10px;border:1px solid var(--color-border)}

/* zoom overlay */
.scatter-zoom{position:fixed;inset:0;z-index:var(--z-modal);display:none;
  align-items:center;justify-content:center;padding:clamp(16px,5vw,64px)}
.scatter-zoom[data-open="true"]{display:flex}
.scatter-zoom-bg{position:absolute;inset:0;background:var(--slate-5);opacity:0;
  transition:opacity var(--duration-base) var(--ease)}
.scatter-zoom[data-open="true"] .scatter-zoom-bg{opacity:.93}
.scatter-zoom figure{position:relative;margin:0;width:min(760px,92vw);z-index:1;
  transform-origin:top left}
.scatter-zoom .shot{display:block;width:100%;aspect-ratio:var(--ar,1/1);
  background-image:var(--img);background-size:cover;background-position:center}
.scatter-zoom figcaption{font-family:var(--font-ui);font-size:11.5px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--slate-1);padding-top:12px;text-align:center}
.scatter-close{position:absolute;top:14px;right:14px;z-index:2}

@media(prefers-reduced-motion:reduce){
  .scatter-item{animation:none}
  .scatter-canvas{transition:none}
}
@media(max-width:640px){
  .scatter{height:clamp(360px,58vh,480px)}
  .scatter-item .cap{display:none}
}


/* ============================================================
   PROTOTYPE CHROME — not part of the design system.
   ============================================================ */
.chrome{position:fixed;top:0;left:0;right:0;z-index:9999;display:flex;flex-wrap:wrap;gap:20px;
  align-items:center;justify-content:center;padding:9px 16px;
  background:rgba(20,20,18,.93);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
.chrome-g{display:flex;align-items:center;gap:9px}
.chrome-l{font-family:var(--font-ui);font-size:9.5px;letter-spacing:.2em;text-transform:uppercase;
  color:#9a9384}
.chrome-o{display:flex;border:1px solid #4a463d}
.chrome-o button{font-family:var(--font-ui);font-size:11px;letter-spacing:.06em;padding:5px 12px;
  background:none;border:0;border-right:1px solid #4a463d;color:#d9d2c4;cursor:pointer}
.chrome-o button:last-child{border-right:0}
.chrome-o button[aria-pressed="true"]{background:#e9e2d2;color:#1a1a18}
/* body padding-top removed with prototype chrome */

/* ============================================================
   PAGE SHELL
   ============================================================ */
.page[hidden]{display:none}
.sec{padding:var(--section-block) var(--section-inline);background:var(--color-bg)}
.sec--alt{background:var(--color-surface-alt)}
.sec-inner{max-width:var(--container-wide);margin:0 auto}
.sec-inner--text{max-width:var(--container)}
.sechead{text-align:center;max-width:52ch;margin:0 auto var(--space-5)}
.sechead h2{font-weight:var(--weight-display);font-size:var(--text-3xl);line-height:1.08;
  letter-spacing:-.012em;color:var(--color-heading);margin:0 0 .4em}
.sechead h2 em{font-style:italic;color:var(--color-link)}
.sechead p{margin:0 auto;color:var(--color-text)}
.lead{font-size:var(--text-lg);font-weight:300;font-style:italic;color:var(--color-text)}

/* ============================================================
   NAV
   ============================================================ */
.nav{position:fixed;top:0;left:0;right:0;z-index:900;display:flex;align-items:center;
  justify-content:space-between;padding:14px var(--section-inline);
  transition:background var(--duration-slow) var(--ease),border-color var(--duration-slow) var(--ease);
  border-bottom:1px solid transparent}
.nav.scrolled{background:color-mix(in srgb,var(--color-bg) 90%,transparent);
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border-bottom-color:var(--color-border)}
.nav-logo{display:block;width:40px;color:var(--paper-3);transition:color var(--duration-slow) var(--ease)}
.nav.scrolled .nav-logo{color:var(--color-heading)}
.nav-logo svg{width:100%;height:auto;display:block}
.nav-links{display:flex;gap:26px}
.nav-links a{font-family:var(--font-serif);font-weight:300;font-size:13.5px;letter-spacing:.17em;
  text-transform:uppercase;text-decoration:none;color:var(--paper-3);position:relative;padding:3px 0;
  text-shadow:0 1px 10px rgba(0,0,0,.5);transition:color var(--duration-slow) var(--ease)}
.nav.scrolled .nav-links a{color:var(--color-text);text-shadow:none}
.nav-links a::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;background:currentColor;
  transform:scaleX(0);transform-origin:left;transition:transform var(--duration-base) var(--ease)}
.nav-links a:hover::after{transform:scaleX(1)}

/* language toggle — same hero-light / scrolled-dark treatment as .nav-links, so
   both the active and inactive language read over the dark hero and then flip to
   ink once the nav gains its background. Was missing entirely, so the inactive
   language fell to the default dark link colour and vanished on the hero. */
/* language toggle — segmented box, active item is a SAND fill (the night's
   white), not green: sand reads as "selected" over the hero without dropping the
   green primary into the corner where it competes with nothing. Full words, not
   codes. Hero layer keeps border + inactive text legible on the dark hero, then
   flips to ink tokens on scroll. */
.langbar{display:inline-flex;border:1px solid color-mix(in srgb,var(--paper-3) 55%,transparent);
  transition:border-color var(--duration-slow) var(--ease)}
.langbar a{font-family:var(--font-ui);font-size:12.5px;letter-spacing:.06em;
  padding:8px 16px;text-decoration:none;color:var(--paper-3);line-height:1;
  transition:background var(--duration-fast) var(--ease),color var(--duration-base) var(--ease),
             border-color var(--duration-slow) var(--ease)}
.langbar a:not([aria-current="true"]){opacity:.82}
.langbar a[aria-current="true"]{background:var(--sand);color:var(--slate-5)}
.langbar a:lang(zh){font-family:var(--font-ui-zh);letter-spacing:.04em}
/* scrolled: nav has a light background */
.nav.scrolled .langbar{border-color:var(--color-border-strong)}
.nav.scrolled .langbar a{color:var(--color-text)}
.nav.scrolled .langbar a:not([aria-current="true"]){opacity:1}
.nav.scrolled .langbar a[aria-current="true"]{background:var(--slate-5);color:var(--sand)}
@media(max-width:720px){.nav-links{display:none}}

/* ============================================================
   HERO — the plate, unchanged in structure from the shipped page
   ============================================================ */
.hero{position:relative;height:100svh;min-height:560px;overflow:hidden;background:var(--ink-6)}
.stage{position:absolute;inset:0}
.px{position:absolute;inset:-5%;will-change:transform}
.fill{position:absolute;inset:0;background-repeat:no-repeat;background-size:cover;
  background-position:center bottom}
.cloudsA{position:absolute;inset:0;height:100%;width:200%;background-repeat:repeat-x;
  background-size:auto 100%;animation:drift 190s linear infinite;opacity:.9}
@keyframes drift{from{transform:translateX(0)}to{transform:translateX(-50%)}}
.cart{position:absolute;height:3%;aspect-ratio:1.919;background-repeat:no-repeat;
  background-size:contain;background-position:center bottom;transform-origin:50% 90%;
  will-change:left,top,transform;z-index:3;opacity:.92}
.treeA{position:absolute;inset:0;width:100%;height:100%;background-position:center bottom;
  background-size:100% 100%;background-repeat:no-repeat;transform-origin:50% 100%;
  animation:sway 13s ease-in-out infinite}
@keyframes sway{0%,100%{transform:rotate(-.05deg)}50%{transform:rotate(.05deg)}}
.darken{position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(to top,rgba(9,25,18,.74) 0%,rgba(9,25,18,.30) 38%,rgba(9,25,18,0) 76%)}
.grain{position:absolute;inset:0;pointer-events:none;mix-blend-mode:multiply;opacity:.45;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='a'><feTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2'/><feColorMatrix type='saturate' values='0'/></filter><rect width='160' height='160' filter='url(%23a)' opacity='0.3'/></svg>")}
.hero-content{position:absolute;left:0;right:0;bottom:clamp(48px,9vh,110px);z-index:6;
  display:flex;flex-direction:column;align-items:center;gap:clamp(22px,4vh,40px);
  padding:0 var(--section-inline)}
.headline{width:min(760px,86vw);color:var(--paper-3);filter:drop-shadow(0 2px 18px rgba(0,0,0,.45))}
.headline svg{width:100%;height:auto}
.headline.mobile{display:none}
@media(max-width:640px){.headline.desktop{display:none}.headline.mobile{display:block;width:min(360px,80vw)}}
.cta-zone{display:flex;flex-direction:column;align-items:center;gap:14px}
.fine{font-family:var(--font-serif);font-weight:300;font-size:14.5px;letter-spacing:.02em;
  color:var(--paper-3);opacity:.85;text-shadow:0 1px 10px rgba(0,0,0,.5)}
.fine .dot{margin:0 .55em;opacity:.6}
.menu-wrap{position:relative}
.menu{position:absolute;top:calc(100% + 10px);left:50%;transform:translateX(-50%);min-width:220px;
  background:var(--color-bg);border:1px solid var(--color-border);box-shadow:var(--shadow-lift);
  padding:6px 0;display:none;z-index:20}
.menu.open{display:block}
.menu a{display:block;font-family:var(--font-ui);font-size:13px;padding:9px 16px;
  color:var(--color-text);text-decoration:none}
.menu a:hover{background:var(--color-surface-alt);color:var(--color-heading)}

/* ============================================================
   COUNTDOWN · ESSENTIALS · ARCADE · FOOTER
   ============================================================ */
.count-row{display:flex;align-items:baseline;justify-content:center;gap:clamp(10px,3vw,34px)}
.count-sep{font-family:var(--font-serif);font-size:1.6rem;color:var(--color-border);
  transform:translateY(-.5em)}
.ess{display:grid;grid-template-columns:repeat(3,1fr);border:1px solid var(--color-border)}
.ess>div{padding:clamp(22px,3vw,38px);border-right:1px solid var(--color-border)}
.ess>div:last-child{border-right:0}
.ess b{display:block;font-family:var(--font-ui);font-size:10.5px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--color-text-muted);font-weight:400;margin-bottom:1em}
.ess strong{display:block;font-family:var(--font-serif);font-weight:300;font-size:1.55rem;
  line-height:1.2;color:var(--color-heading);margin-bottom:.5em}
.ess p{font-size:14.5px;margin:0;color:var(--color-text)}
@media(max-width:680px){.ess{grid-template-columns:1fr}
  .ess>div{border-right:0;border-bottom:1px solid var(--color-border)}
  .ess>div:last-child{border-bottom:0}}
.split{display:grid;grid-template-columns:1.15fr .85fr;gap:clamp(26px,5vw,64px);align-items:start}
@media(max-width:760px){.split{grid-template-columns:1fr}}
.stack-cards{display:flex;flex-direction:column;gap:16px}
.stack-cards .card{max-width:none}
.foot{padding:clamp(70px,10vw,120px) var(--section-inline) 60px;text-align:center;
  background:var(--color-bg);color:var(--color-text)}
.foot-logo{width:56px;margin:0 auto 26px;color:var(--color-primary)}
.foot-logo svg{width:100%;height:auto;display:block}
.fnames{font-family:var(--font-serif);font-weight:300;font-size:clamp(2rem,5vw,3rem);
  color:var(--color-heading);line-height:1.1;margin-bottom:.4em}
.fmeta{font-family:var(--font-ui);font-size:11.5px;letter-spacing:.2em;text-transform:uppercase;
  color:var(--color-text-muted)}
.pagehead{padding:clamp(120px,16vh,190px) var(--section-inline) clamp(44px,6vw,72px);
  background:var(--color-bg);border-bottom:1px solid var(--color-border)}
.pagehead .sec-inner{max-width:var(--container)}
.pagehead h1{font-size:var(--text-3xl);font-weight:var(--weight-display);line-height:1.06;
  letter-spacing:-.018em;margin:0 0 .35em}
.pagehead h1 em{font-style:italic;color:var(--color-link)}
.row{display:flex;flex-wrap:wrap;gap:16px;align-items:center}
.prose p{max-width:var(--measure)}


.sky{background-image:url('../img/asset-1.webp')}
.cloudsA{background-image:url('../img/asset-2.webp')}
.town{background-image:url('../img/asset-3.webp')}
.towncover{background-image:url('../img/asset-4.webp');z-index:4}
.treeA{background-image:url('../img/asset-5.webp')}

:root{--sc-p1: url('../img/sc-p1.webp');--sc-p2: url('../img/sc-p2.webp');--sc-p3: url('../img/sc-p3.webp');--sc-p4: url('../img/sc-p4.webp');--sc-p5: url('../img/sc-p5.webp');--sc-p6: url('../img/sc-p6.webp');--sc-p7: url('../img/sc-p7.webp');--sc-p8: url('../img/sc-p8.webp');--sc-p9: url('../img/sc-p9.webp');--sc-p10: url('../img/sc-p10.webp');}


/* ---------- nav Arcade link: bilingual label ---------- */
.nav-arcade .nav-lbl-zh{display:none}
html[lang="zh-Hans"] .nav-arcade .nav-lbl-en{display:none}
html[lang="zh-Hans"] .nav-arcade .nav-lbl-zh{display:inline;font-family:var(--font-ui-zh);letter-spacing:.12em}

/* The hero's sky is bright at the top-centre, where .nav-links sits; the
   inherited single soft shadow left the label washed out. Tighter + doubled
   shadow so it reads over sky as well as canopy. Scrolled state is unchanged
   (it flips to ink on the frosted bar). */
.nav .nav-arcade{text-shadow:0 1px 2px rgba(26,39,51,.55), 0 2px 14px rgba(26,39,51,.65)}
.nav.scrolled .nav-arcade{text-shadow:none}

/* Tertiary on dark imagery: the default tertiary ink is dark and would be
   invisible over the hero art, so give these instances sand ink and invert to
   slate night. Per COMPONENTS-FOR-CLAUDE-CODE.md. */
.hero .btn--tertiary,
.cta-zone .btn--tertiary{ --btn-ink: #E4D3B4; }
.hero .btn--tertiary .btn-lbl-inv,
.cta-zone .btn--tertiary .btn-lbl-inv{ color: #1A2733; }

/* Arcade link sits with the language toggle on the right, not centred by the
   nav's space-between. */
.nav-right{display:flex;align-items:center;gap:clamp(18px,2.4vw,34px)}
.nav-right .nav-links{gap:0}

/* Countdown digits: the shipped Fraunces subset carries opsz + wght only, so
   font-variant-numeric:tabular-nums above is inert — glyph widths still vary
   ("11" 56px vs "00" 88px at desktop size), and the row re-flowed every second,
   shoving the neighbouring cells. Reserve the width of the widest digit instead:
   1ch is the '0' advance, which is the widest figure in this face, so 2ch fits
   any two-digit value and 3ch any three-digit day count. Flex items honour
   min-width, and the label sits centred under a now-stable box. */
.count-num{ min-width: 2ch; text-align: center; }
.count-num[data-d], .count-num[data-d-zh]{ min-width: 3ch; }
