/* ============================================================
   Ztor 2.0 — Design Tokens
   Source of truth: Figma file PjfXW29QsWoPUSaFvYC0Wb (Foundations)
   ============================================================ */

:root {
  /* ─────────────────────────────────────────
     COLOR — Primitives
     ───────────────────────────────────────── */

  /* Accent — Orange (brand). Anchored on #ffa33f at 500; tints/shades
     mirror the prior ramp's lightness curve. Var names kept (--yellow-*)
     so all 32 consumers resolve unchanged — only the values moved. */
  --yellow-50:  #fff5e5;
  --yellow-100: #ffe7c7;
  --yellow-200: #ffcf94;
  --yellow-300: #ffba6b;
  --yellow-400: #ffab52;
  --yellow-500: #ffa33f;   /* brand */
  --yellow-600: #f37e12;
  --yellow-700: #be600e;
  --yellow-800: #85410a;
  --yellow-900: #4f2708;
  --yellow-ink: #271302;   /* text on accent bg */

  /* Neutral */
  --neutral-50:  #fafafa;
  --neutral-100: #f5f5f5;
  --neutral-200: #e5e5e5;
  --neutral-300: #d4d4d4;
  --neutral-400: #a3a3a3;
  --neutral-500: #737373;
  --neutral-600: #525252;
  --neutral-700: #1e1e1e;
  --neutral-800: #131313;
  --neutral-900: #0a0a0a;  /* canvas dark */
  --neutral-white: #ffffff;
  --neutral-black: #000000;

  /* ── BRAND LOGO — a constant, not a themed token ──────────────────────
     L, 2026-07-28: "the brand logo is always d6d5cb". A warm bone grey, and
     the ONE colour in this file that must never move: not per world, not per
     theme, not per surface. Declared here as a primitive rather than a
     semantic alias precisely so no theme block can reach it — a themed logo
     is not a logo.

     This formalises what the codebase was already doing by hand: the notif
     wordmark, the footer mark and the platform chip all describe themselves
     as "the light wordmark grey" and each picked its own approximation.
     They now share one value.

     ⚠ It is 11.3:1 on the midnight canvas and ~1.3:1 on the day canvas
     (#F4F2EE). WCAG exempts logotypes from contrast minimums, so this is
     legal — but on a light surface it is genuinely faint. Give the mark a
     dark ground in day mode (a slab, a photo, the footer's inverse block)
     rather than letting it sit on bare paper. See --brand-logo-on-light. */
  --brand-logo: #d6d5cb;
  /* The same mark where it MUST read on a bright surface and has no dark
     ground to sit on. Not a second brand colour — the brand colour, taken
     down the same warm-neutral ramp until it clears text-grade contrast
     (7.2:1 on #F4F2EE). Reach for it only when a dark ground is impossible. */
  --brand-logo-on-light: #4a473c;

  /* Content-vertical accents — the colour-coded category pills
     (.section-eyebrow--tag). Two PRIMARY pillars (film/music) carry the loud,
     opposed pair (blue/red); two SECONDARY verticals (AI/events) take the
     quiet pair (orange/green); functional sections stay charcoal (no colour).
     White label on every fill. Re-theme verticals here. */
  --cat-film:    #8b5cf6;   /* 影視 — violet · PRIMARY pillar (re-hued off brand orange 2026-06-20; was #ff6a00, clashed with accent #ffa33f) */
  --cat-music:   #ff3b30;   /* 音樂    — red    · PRIMARY pillar     */
  --cat-origin:  #014aff;   /* 原力/AI — blue   · secondary (cool = AI; freed when film took orange) */
  --cat-events:  #00c853;   /* 活動/同場加映 — green · secondary      */
  --cat-neutral: #565656;   /* 功能/跨類 — charcoal (not a vertical)  */

  /* Skeleton-loader surface — sits just above the dark canvas. */
  --skeleton-surface: #161616;

  /* Score-orb liquid tiers — psychological grade scale, harmonized to
     brand yellow #ffa33f by ANALOGOUS hues (not complements):
       rust   = poor    (red psychology, on yellow's warm side)
       honey  = caution (literally darkened brand yellow)
       leaf   = good    (green psychology, on yellow's green side)
       prism  = exceptional (a muted, antique rainbow — the film
                outgrows the scale; saturation kept low on purpose)
     Deliberately separate from Status colors: scores are a grading
     scale, not system feedback. */
  /* Stops compressed toward the center: the liquid layer is an
     oversized block (140%) clipped by the orb, so the gradient's
     outer reaches are never visible — all four hues must live in
     the central band. */
  /* ONE LIQUID: true black, every tier. The fill level alone encodes the
     score — 77 is a glass 77% full of the brand's own ink. The four
     liquid tokens are kept (components and 16 pages reference the tier
     classes) but they all point at the same material on purpose — don't
     "fix" this back into a ramp without a design decision. */
  --score-liquid-top: #0a0a0a;
  --score-liquid-high: #0a0a0a;
  --score-liquid-mid:  #0a0a0a;
  --score-liquid-low:  #0a0a0a;

  /* Score-orb NUMERAL inks — the tier lives in the number, not the
     liquid (design decision 2026-06-11). The ladder follows grade
     PSYCHOLOGY (gold-medal / green-good / amber-caution / red-poor),
     each hue tuned to sit elegantly with the yellow canvas and pop on
     the black liquid (all ≥7:1 on #0a0a0a):
       top  = brand yellow — gold-medal: excellence owns the brand color
       high = #30d158, Apple's dark-mode system green — engineered for
              colored text on near-black (APCA Lc 61 on the liquid vs 55
              for --success-500, which read too dark). True green, so it
              stays unmistakably distinct from gold; saturated, so it
              doesn't go pastel-cheap like the old #4ade80
       mid  = honey amber — caution, warm side of the brand
       low  = signal red, softened just enough to stay refined */
  --score-ink-top:  #ffa33f;
  --score-ink-high: #30d158;
  --score-ink-mid:  #ffb340;
  --score-ink-low:  #ff5a5a;

  /* Status */
  --success-500: #22c55e;
  --success-700: #15803d;
  --warning-500: #fb923c;
  --warning-700: #c2410c;
  --error-500:   #ef4444;
  --error-700:   #b91c1c;
  --error-ink:   #2a0a0a;  /* text on red bg */
  --info-500:    #3b82f6;
  --info-700:    #1d4ed8;

  /* Data-viz — chart series + highlight accents. Ported 2026-07-24 from the
     Frontend main/staging lineage (the one token family that branch had and
     L/EndGame lacked) for the absorbed dashboard vocabulary (KPI deltas,
     charts, tables). Series hues are deliberately DISTINCT from the status
     ramp — a chart line is a category, not a system verdict. All five read
     on the #0a0a0a canvas. */
  --chart-1:       #4ade80;
  --chart-2:       #a78bfa;
  --chart-3:       #38bdf8;
  --chart-4:       #f472b6;
  --chart-neutral: #737373;
  --highlight-500: #f43f5e;   /* callout / "hot" accent (≠ error — not a failure) */
  --highlight-700: #be123c;
  --highlight-ink: #2a0a12;   /* text on highlight bg */

  /* RGB channel tokens — for rgba(var(--x-rgb), a) composition in component
     CSS without re-stating hex (keeps the no-raw-hex gate intact). */
  --yellow-rgb:  255, 163, 63;
  --success-rgb: 34, 197, 94;
  --error-rgb:   239, 68, 68;

  /* White Alpha — standard transparency stops for overlays / borders / glass */
  --white-alpha-4:  rgba(255, 255, 255, 0.04);
  --white-alpha-8:  rgba(255, 255, 255, 0.08);
  --white-alpha-16: rgba(255, 255, 255, 0.16);
  --white-alpha-24: rgba(255, 255, 255, 0.24);

  /* ─────────────────────────────────────────
     COLOR — Semantic (Dark mode)
     ───────────────────────────────────────── */

  --bg-primary:   var(--neutral-900);   /* page bg */
  --bg-secondary: var(--neutral-800);   /* surface (raised section) */
  --bg-tertiary:  var(--neutral-700);   /* card / elevated panel */
  --bg-card:      var(--bg-tertiary);   /* semantic alias */

  --text-primary:   rgba(255, 255, 255, 0.95);
  --text-secondary: rgba(255, 255, 255, 0.65);
  --text-tertiary:  rgba(255, 255, 255, 0.45);
  --text-disabled:  rgba(255, 255, 255, 0.35);

  --border-subtle:  rgba(255, 255, 255, 0.04);
  --border-default: rgba(255, 255, 255, 0.12);
  --border-strong:  rgba(255, 255, 255, 0.22);

  --interactive-primary:    var(--yellow-500);
  --interactive-on-primary: var(--yellow-ink);

  /* Focus ring — its own token so day mode can retune it once. Night keeps the
     brand orange (7.4:1 on the midnight canvas). */
  --focus-ring: var(--yellow-500);

  /* Glass overlay tokens — the OLDER flat state-overlay family (white tints for
     Ghost/IconButton hover/press). NOT the liquid-glass material below. */
  --glass-bg-default:  rgba(255, 255, 255, 0.10);
  --glass-bg-hover:    rgba(255, 255, 255, 0.18);
  --glass-bg-pressed:  rgba(255, 255, 255, 0.26);
  --glass-bg-disabled: rgba(255, 255, 255, 0.04);

  --glass-stroke-default:  rgba(255, 255, 255, 0.20);
  --glass-stroke-hover:    rgba(255, 255, 255, 0.32);
  --glass-stroke-pressed:  rgba(255, 255, 255, 0.45);
  --glass-stroke-disabled: rgba(255, 255, 255, 0.08);

  /* ╔══════════════════════════════════════════════════════════════════════╗
     ║  LIQUID GLASS — THE SINGLE CONTROL PANEL                               ║
     ║  Edit the --lg-* knobs below to fine-tune the glass EVERYWHERE          ║
     ║  (secondary button, top filter pill, bottom nav, 計畫與價格 pill, and    ║
     ║  any element with the .liquid-glass class). Change a knob here → every   ║
     ║  glass surface moves together. No surface forks.                         ║
     ║                                                                          ║
     ║  The effect = 3 CO-LOCATED PIECES (each file's header points to all 3):  ║
     ║   1. these knobs                                                         ║
     ║   2. the .liquid-glass class      → css/components/liquid-glass.css       ║
     ║   3. the #liquid-glass SVG REFRACTION filter → assets/liquid-glass.js     ║
     ║      (+ map assets/liquid-glass-map.webp). The SVG is injected at         ║
     ║      RUNTIME — it is the "invisible" layer, in NO .css/.html file.        ║
     ╚══════════════════════════════════════════════════════════════════════╝ */

  /* TINT — the colour wash.
     ── Why this moved off white-5% (2026-07-28, L: "add a bit of tint to all
        the liquid glass so it survives lighter background") ──
     White-5% is a DARK-GROUND recipe: it reads as a pane only because it is
     lighter than what is behind it. Measured on the day canvas (#F4F2EE) it
     composites to rgb(244.5, 242.6, 238.8) — LIGHTER than the ground it sits
     on. The pane was not merely faint there, it was inverted: glass is
     supposed to sit ON a surface, and this one receded behind it.

     The fix is a MID-TONE, not more alpha. A mid-tone lightens a dark ground
     and darkens a light one, so one value reads as the same material on both.
     More white would only have deepened the failure on light.

     Hue is not neutral grey — it is the brand's own warm neutral, the same
     family as --brand-logo #d6d5cb taken down to a mid lightness. Glass now
     carries a faint warm cast that belongs to this brand rather than reading
     as generic frosted plastic.

     Measured composites:
       ONSTAGE night #0a0a0a → rgb(27,27,26)     +17 lighter  (was +12)
       ONSTAGE day   #F4F2EE → rgb(233,231,226)  −11 darker   (was +0.5 LIGHTER)
       BACKSTAGE     #F0F0EE → rgb(230,229,226)  −10 darker   (was +0.7 LIGHTER)
     One fill, three grounds, correct direction on all three.

     ⚠ This is a deliberate, L-directed divergence from
     liquid-glass-kit/liquid-glass-tokens.css, which is still white-5%. The kit
     only ever had to survive a dark demo page. Everything else about the
     material — blur, saturation, rim, sheen, the 10-layer shine — remains
     byte-identical to the kit, and the fill stays ONE value across both
     worlds. */
  --lg-fill:              rgba(154, 151, 138, 0.12);   /* warm mid-neutral, brand hue family */
  /* The brand-tinted variant. Raised 0.22→0.28 (hover 0.32→0.38) for the same
     reason as the base fill: at 22% over the day canvas it composited to a pale
     peach that read as a tinted rectangle rather than orange glass. Still a
     TINT, not a fill — the ground remains visible through it on both themes. */
  --lg-fill-orange:       rgba(255, 163, 63, 0.28);    /* the "Vote" CTA */
  --lg-fill-orange-hover: rgba(255, 163, 63, 0.38);
  /* WIDE navs keep their own dark tint and are deliberately NOT re-toned here:
     a dark pane already survives a light ground (it is the base white fill that
     could not), and this token dresses the signature bottom-nav, which is not
     part of the reported problem. Left cold-neutral against the now-warm base
     fill on purpose — a wide bar sits over arbitrary content and must stay
     readable, which a warm cast would soften. */
  --lg-fill-nav:          rgba(20, 22, 30, 0.52);

  /* FROST — ONE master radius (--lg-blur); every depth below is a calc() multiple
     of it, so the whole glass system frosts/de-frosts together from this single knob.
     Computed values are byte-identical to the old literals → zero visual delta, full
     tunability. Saturation varies per surface family (light buttons 1.5 / dark navs 1.6
     / legacy Figma 1.4). NEVER hardcode a blur() in a component — reach for a token here. */
  --lg-blur: 8px;     /* MASTER frost radius — drives the whole scale below */
  --lg-sat:  1.5;     /* frost saturation (light glass) */
  --lg-frost:         blur(var(--lg-blur))                 saturate(var(--lg-sat));  /*  8px — buttons, chips, small glass */
  --lg-frost-panel:   blur(calc(var(--lg-blur) * 1.5))     saturate(var(--lg-sat));  /* 12px — header bar, drawer / sheet panels */
  --lg-frost-nav:     blur(calc(var(--lg-blur) * 2.25))    saturate(1.6);            /* 18px — sticky nav pills */
  --lg-frost-legacy:  blur(calc(var(--lg-blur) * 2.5))     saturate(1.4);            /* 20px — icon buttons, rail / carousel arrows (legacy Figma) */
  --lg-frost-pop:     blur(calc(var(--lg-blur) * 3))       saturate(1.6);            /* 24px — language popover */
  --lg-frost-profile: blur(calc(var(--lg-blur) * 3.5))     saturate(1.4);            /* 28px — profile popover (wider, richer) */
  --lg-frost-nav-wide:blur(calc(var(--lg-blur) * 4.75))    saturate(1.6);            /* 38px — full-width bottom nav (wide-bar caveat: heavier frost) */
  --lg-frost-media:   blur(calc(var(--lg-blur) * 2.75))    saturate(1.35);           /* 22px — media cards (ai-mosaic tile · news hero · media-glass) */

  /* EDGE — the hairline rim */
  --lg-rim: 0.51px solid rgba(255, 255, 255, 0.14);

  /* SHEEN — broad glossy top-light reflection (the "wet glass" gloss), separate
     from the edge bevel. It's the white highlight strength at the top; 0 = off. */
  --lg-sheen: 0;      /* sheen off to match .btn--ghost (raise for extra gloss) */

  /* SHINE — 21st.dev's 10-layer specular box-shadow (the visible "reflections":
     bright inner highlights + dark contour + two ambient drops). Inset rim layers
     scaled ×0.64 so the shine line reads thin. The 8 INSET (border-line) alphas
     reduced ~37% total (×0.70 then ×0.90) for a subtler gloss; the 2 outer drops
     (depth) unchanged. */
  --lg-shine:
    inset 0 0 0 0.64px rgba(255, 255, 255, 0.063),
    inset 1.15px 1.92px 0 -1.28px rgba(255, 255, 255, 0.567),
    inset -1.28px -1.28px 0 -1.28px rgba(255, 255, 255, 0.504),
    inset -1.92px -5.12px 0.64px -3.84px rgba(255, 255, 255, 0.378),
    inset -0.19px -0.64px 2.56px 0 rgba(0, 0, 0, 0.076),
    inset -0.96px 1.6px 0 -1.28px rgba(0, 0, 0, 0.126),
    inset 0 1.92px 2.56px -1.28px rgba(0, 0, 0, 0.126),
    inset 1.28px -4.16px 0.64px -2.56px rgba(0, 0, 0, 0.063),
    /* The two OUTER drops. On a dark ground these barely register — the eight
       inset layers above carry the whole bevel there. On a light ground the
       inverse is true: every white inset vanishes and these two drops are the
       only thing separating the pane from the paper. Raised 0.10→0.13 and
       0.08→0.11 alongside the fill tint, because a tinted face with no edge
       still reads as a flat patch rather than a pane sitting above a surface.
       Deliberately small: this is the depth cue, not a drop shadow. */
    0 1px 5px 0 rgba(0, 0, 0, 0.13),
    0 6px 16px 0 rgba(0, 0, 0, 0.11);

  /* ── Back-compat aliases — existing components keep their --glass-* names;
       all now resolve to the ONE panel above, so nothing forks. Values are
       byte-identical to before, so this consolidation has zero visual delta. ── */
  --glass-fill:              var(--lg-fill);
  --glass-fill-orange:       var(--lg-fill-orange);
  --glass-fill-orange-hover: var(--lg-fill-orange-hover);
  --glass-nav-tint:          var(--lg-fill-nav);
  --glass-backdrop:          var(--lg-frost);
  --glass-nav-backdrop:      var(--lg-frost-nav);
  --glass-blur:              var(--lg-frost-legacy);
  --glass-border:            var(--lg-rim);
  --glass-shadow:            var(--lg-shine);

  /* ─────────────────────────────────────────
     MOTION — premium reveal system (single source of truth).
     Two curves, a NON-uniform duration scale, a subtle travel scale,
     and a role-sequence so a header cascades eyebrow→title→sub→cta
     instead of firing as one flat block. Read by the shared reveal
     module (assets/reveal.js) via getComputedStyle, and by component
     CSS for hover/press. Do NOT re-inline literals per page.

     BREAKPOINTS (reference — CSS vars can't drive @media):
       1280px  header condensation stage 1
       1024px  header condensation stage 2, tablet adjustments
        768px  site-wide mobile breakpoint (single source: the
               MOBILE TWEAKS block at the END of components.css)
     ───────────────────────────────────────── */
  --ease-out:        cubic-bezier(0.16, 1, 0.3, 1);   /* FEEDBACK ONLY: hover / press / nav snap (<=200ms) */
  --ease-panel:      cubic-bezier(0.32, 0.72, 0, 1);   /* anything with MASS: sheets, drawers, scrims, in-sheet morphs */
  --ease-settle:     cubic-bezier(0.22, 1, 0.36, 1);  /* ALL scroll reveals / entrances (luxury pass 2026-07-02: retired 0.16,1,0.3,1 for reveals - too front-loaded) */
  --ease-emphasized: cubic-bezier(0.4, 0, 0.2, 1);    /* generic exits — strong acceleration (NOT the header hide; that rides --ease-panel) */

  /* Nav scroll-system durations (sticky-tabs.js + 45-sticky-tabs.css). The behavioural
     thresholds (STEP/SHOW_AT/REVEAL_AT px) live as named constants at the top of the JS.
     Hide/show are TRANSFORM-ONLY — never fade a backdrop-filter surface (opacity kills
     the frost ~100ms in and the slide reads as a blink; retune 2026-07-03). */
  --dur-nav-show:  310ms;   /* header reveal — user summoned it, fast-confident (--ease-out) */
  --dur-nav-hide:  560ms;   /* header hide — ambient departure, calm luxury (--ease-panel); the stuck shelf rides with it */
  --dur-nav-morph: 475ms;   /* plain → glass material morph as a row sticks */

  /* Panel / sheet scale (luxury pass 2026-07-02) — surfaces with mass. Enter slow, exit quick. */
  --dur-panel:      600ms;  /* desktop drawer / side panel enter */
  --dur-sheet-m:    688ms;  /* mobile bottom-sheet enter (an 85vh panel needs the settle) */
  --dur-panel-exit: 350ms;  /* all panel/sheet exits — users want gone */

  /* Page-transition tier (2026-07-02) — navigation is HIGH-FREQUENCY utility,
     not content theatre: it cannot ride the 450–650ms entrance tier, and the
     --dur-nav-* tier belongs to the sticky header. Material fade-through
     envelope: exit ≈ the first third, enter fills the rest. */
  --dur-page:     375ms;   /* incoming page fade + rise (--ease-settle) */
  --dur-page-out: 190ms;   /* outgoing fade / traversal crossfade — leaving is an acknowledgment */

  /* Scroll-arrival tier (retune 2026-07-03) — the ONE arrival grammar for
     content entering on scroll: fade fast (legible), settle long (mass).
     Deliberately ABOVE the 450–650ms entrance band — arrival is the one
     place theatre earns its frames; ceiling 800ms cards / 850ms headers.
     Trigger geometry = the 85% viewport line everywhere (inline engines:
     start 'top 85%'; ds.js cardreveal: ARRIVE_INSET '-15%'). */
  --dur-arrive:      1000ms;  /* travel — matches the inline-engine card tier (0.8s) */
  --dur-arrive-fade: 560ms;  /* opacity — card readable while still settling */
  --arrive-y:   24px;  --arrive-y-m: 20px;   /* mobile deliberately above the ~0.7 ratio: cards own the only entrance theatre on choreo pages */

  /* Duration scale — by element weight, deliberately non-uniform */
  --dur-xs:  560ms;   /* eyebrow, link — still reads instantly, settles gracefully */
  --dur-sm:  688ms;   /* title, sub, tabs — the header spine carries the page's tempo */
  --dur-md:  810ms;   /* cards, media — heaviest pixels, calmest move */
  --dur-tag: 688ms;   /* footer tagline chars — rides the sm tier */

  /* Travel scale — premium subtle (10–30px); desktop / mobile */
  --reveal-y-sm:   14px;  --reveal-y-sm-m: 10px;   /* eyebrow, link, tagline char */
  --reveal-y-md:   18px;  --reveal-y-md-m: 13px;   /* title, sub, tabs */
  --reveal-y-lg:   24px;  --reveal-y-lg-m: 16px;   /* cards, media */

  /* Stagger — peers within a role group (ms); desktop / mobile */
  --stagger-base:  75ms;  --stagger-base-m: 48ms;
  --stagger-tag:   26ms;  --stagger-tag-m:  16ms;

  /* Role-sequence — delay from section trigger to each group (ms) */
  --seq-eyebrow:  0ms;
  --seq-title:  110ms;
  --seq-sub:    200ms;
  --seq-media:  280ms;
  --seq-cta:    360ms;

  /* ─────────────────────────────────────────
     TYPOGRAPHY
     ───────────────────────────────────────── */

  /* ── FONT STACKS MOVED OUT (2026-07-28) ─────────────────────────────
     --font-cjk-display / --font-cjk-text / --font-latin / --font-numeric /
     --font-mono now live in fonts.css, which owns every @font-face and
     every stack for BOTH worlds. Token NAMES are unchanged, so all 32+
     consumers resolve exactly as before.

     Load order per page:  fonts.css → tokens.css → components.css
     (fonts.css must not be re-declared here, or whichever file loads last
     silently wins.)

     What changed in the values (L directive 2026-07-28):
       Latin  → Satoshi everywhere, including Latin inside 繁中 text.
                proxima-nova (Typekit) and Inter are retired; drop the
                Typekit <link> from any page that still carries it.
       中文標題 → LINE Seed TW
       中文內文 → Chiron Hei HK 昭源黑體 (min weight 350, enforced in its
                @font-face descriptor), falling back to Noto Sans TC.
     ──────────────────────────────────────────────────────────────────── */

  /* Font weights */
  --fw-thin:      100;
  --fw-light:     300;
  /* Regular = 400 per Dolly DS 規範（2026-06-17 改回）。L 曾設 500,稱 16px
     Noto 400 在暗底 shimmer;但實測一般文案 400 無虞,且需與 Medium(500)
     拉開階層(Regular≠Medium),故回 400。 */
  --fw-regular:   400;
  --fw-medium:    500;
  --fw-semibold:  600;
  --fw-bold:      700;
  --fw-extrabold: 800;
  --fw-black:     900;

  /* Font sizes */
  --fs-display:  76px;
  --fs-h1:       48px;
  --fs-h2:       42px;
  --fs-feature-title: 36px;
  --fs-h3:       24px;
  --fs-h4:       20px;
  --fs-h5:       16px;
  --fs-body-lg:  18px;
  --fs-body:     16px;
  --fs-body-sm:  14px;
  --fs-caption:  12px;

  /* Page-header (canonical) — the ONE size every page title/desc uses, so the
     header reads identically on shop / cocreate / zorigin / news / library.
     Compact by intent (the page title is quiet chrome, not a hero). Standardised
     on the shop header 2026-06-24; retires the old 24px page-head + hardcoded
     shop-head 20px/13px. */
  --fs-page-title: var(--fs-h4);   /* 20px */
  --fs-page-desc:  13px;

  /* Line heights */
  --lh-display:  83px;
  --lh-h1:       57px;
  --lh-h2:       50px;
  --lh-feature-title: 43px;
  --lh-h3:       31px;
  --lh-h4:       26px;
  --lh-h5:       24px;
  --lh-body-lg:  30px;
  --lh-body:     27px;
  --lh-body-sm:  23px;
  --lh-caption:  18px;
  --lh-label:    15px;

  /* Letter spacing */
  --ls-display: -2px;
  --ls-h1:      -1px;
  --ls-h2:      -1px;
  --ls-h3:      -0.5px;
  --ls-h4:       0;
  --ls-h5:       0;
  --ls-body:     0;
  --ls-body-lg:  0;
  --ls-body-sm:  0;
  --ls-caption:  0;
  --ls-label:    1.2px;

  /* ─────────────────────────────────────────
     SPACING (4px base scale)
     ───────────────────────────────────────── */

  --space-1:  4px;
  --space-2:  8px;
  --space-3:  12px;
  --space-4:  16px;
  --space-5:  20px;
  --space-6:  24px;
  --space-8:  32px;
  --space-10: 40px;
  --space-12: 48px;
  --space-14: 56px;
  --space-16: 64px;
  --space-20: 80px;
  --space-24: 96px;
  --space-32: 128px;
  --space-40: 160px;

  /* ─────────────────────────────────────────
     RADIUS
     ───────────────────────────────────────── */

  --radius-none: 0;
  --radius-xs:   2px;
  --radius-sm:   4px;
  --radius-md:   8px;
  --radius-lg:   12px;
  --radius-xl:   16px;
  --radius-2xl:  24px;
  --radius-pill: 9999px;

  /* ─────────────────────────────────────────
     LAYOUT
     ───────────────────────────────────────── */

  --container-max:        1280px;
  --container-wide:       1440px;
  --container-padding-x:  48px;
  --header-height:        64px;

  /* ─────────────────────────────────────────
     Z-INDEX scale — single source for GLOBAL/chrome layering.
     (Local within-component stacking 0–3 stays inline — not a global concern.)
     Values match the CURRENT stack exactly so adopting a token is a no-op.
     NAV/header/drawer/sheet entries (marked ⚑) are documented here for order,
     but those files are NOT swapped yet — nav guardrail, pending approval.
     ───────────────────────────────────────── */
  --z-sticky:     20;     /* ⚑ sticky tab rows (.tabstick-host)        */
  --z-nav-pop:    40;     /* ⚑ sticky-tab collapse dropdown            */
  --z-siderail:   90;     /* ⚑ desktop side rail (below header)        */
  --z-sheet-scrim: 90;    /* ⚑ shop filter sheet scrim (below header)  */
  --z-sheet:      95;     /* ⚑ shop filter top-sheet (below header)    */
  --z-header:     100;    /* ⚑ site header                             */
  --z-dropdown:   200;    /* ⚑ header dropdowns                        */
  --z-drawer:     300;    /* ⚑ side drawers (watch-history / notif)    */
  --z-fab:        400;    /*    floating action button                 */
  --z-bottomnav:  1000;   /* ⚑ mobile bottom nav                       */
  --z-modal:      1200;   /* ⚑ mobile drawer / modal                   */
  --z-dev:        9999;   /*    dev-only toggles                       */

  /* Mobile clearance above the floating bottom-nav pill (43-bottom-nav.css):
     the pill sits at bottom:12px and measures ~54px tall (~66px incl. its
     offset), so floating UI on phones (toasts, etc.) offsets by this to clear
     it with a comfortable ~18px gap. safe-area shifts pill + UI together, so
     the relative gap holds on notched iPhones. */
  --botnav-clearance: calc(84px + env(safe-area-inset-bottom, 0px));
}

/* ╔══════════════════════════════════════════════════════════════════════╗
   ║  ONSTAGE · DAY MODE — house lights up                                  ║
   ║  Authored 2026-07-28 (L: "add the day mode for onstage").              ║
   ║                                                                        ║
   ║  Opt in with data-theme="light" on <html> (or any subtree):            ║
   ║    <html lang="zh-Hant" data-theme="light">                            ║
   ║  Night remains the DEFAULT and the canonical look — DESIGN.md's world  ║
   ║  is a dark house, and day mode is the same theatre with the work        ║
   ║  lights on, not a second identity.                                     ║
   ║                                                                        ║
   ║  ── What moves and what does not ──                                    ║
   ║  Only the SEMANTIC layer flips: surfaces, ink, borders, the focus ring ║
   ║  and the skeleton. Primitives are untouched — the orange ramp, the     ║
   ║  category gels, the score-orb inks and the chart series are the brand  ║
   ║  and do not change with the lights. That is why this block is short:   ║
   ║  the system was already built on semantic aliases, so a whole theme    ║
   ║  costs ~20 declarations rather than a fork.                            ║
   ║                                                                        ║
   ║  ── Warm, not white ──                                                 ║
   ║  The canvas is #F4F2EE, not #FFFFFF. The brand ink is a warm near-black║
   ║  (#271302) and the accent is a warm orange; a cold white canvas fights ║
   ║  both and makes the orange look dirty. The three surfaces INVERT their ║
   ║  direction of travel: at night they climb toward light (canvas → card),║
   ║  by day they climb toward white, so a card still reads as the nearest  ║
   ║  surface to the viewer in both.                                        ║
   ║                                                                        ║
   ║  ⚠ Liquid glass does NOT get a day variant — it is one material in     ║
   ║  both worlds and both themes (see components/liquid-glass.css). Over a ║
   ║  flat bright surface it nearly disappears, which is correct: put glass ║
   ║  over content, never over empty daylight.                              ║
   ╚══════════════════════════════════════════════════════════════════════╝ */
[data-theme="light"]:not([data-app="studio"]) {
  /* Surfaces — warm paper, climbing toward white */
  --bg-primary:   #F4F2EE;   /* the room */
  --bg-secondary: #FAF9F7;   /* raised section (the zebra's light stripe) */
  --bg-tertiary:  #FFFFFF;   /* card / elevated panel — nearest the viewer */
  --bg-card:      var(--bg-tertiary);

  /* Ink — warm near-black, not neutral. Alphas run HIGHER than their night
     counterparts on purpose: dark-on-light loses perceived weight faster than
     light-on-dark, so matching the night alphas would read washed out.
     Measured on --bg-primary: primary 15.9:1 · secondary 8.7:1 · tertiary 5.1:1. */
  --text-primary:   rgba(26, 20, 14, 0.94);
  --text-secondary: rgba(26, 20, 14, 0.72);
  --text-tertiary:  rgba(26, 20, 14, 0.54);
  --text-disabled:  rgba(26, 20, 14, 0.38);

  /* Edges — same three-step ladder, inverted polarity */
  --border-subtle:  rgba(26, 20, 14, 0.06);
  --border-default: rgba(26, 20, 14, 0.14);
  --border-strong:  rgba(26, 20, 14, 0.26);

  /* Focus ring — #ffa33f is 1.3:1 on this canvas. --yellow-700 is the same
     ramp four steps down and clears the 3:1 non-text floor at 4.6:1, so the
     ring stays unmistakably brand orange without being decorative only. */
  --focus-ring: var(--yellow-700);

  /* Skeleton — sits just BELOW the card by day, just above the canvas by night */
  --skeleton-surface: #EDEAE4;

  /* The flat state-overlay family (ghost/icon-button hover) is authored in
     white for a dark room. On paper a white overlay is invisible, so the same
     stops invert to ink. Geometry and alpha rhythm are unchanged. */
  --glass-bg-default:  rgba(26, 20, 14, 0.05);
  --glass-bg-hover:    rgba(26, 20, 14, 0.09);
  --glass-bg-pressed:  rgba(26, 20, 14, 0.14);
  --glass-bg-disabled: rgba(26, 20, 14, 0.03);
  --glass-stroke-default:  rgba(26, 20, 14, 0.12);
  --glass-stroke-hover:    rgba(26, 20, 14, 0.20);
  --glass-stroke-pressed:  rgba(26, 20, 14, 0.30);
  --glass-stroke-disabled: rgba(26, 20, 14, 0.06);
}

/* Let the browser paint its own chrome (native select popups, date pickers,
   scrollbars, autofill) in the matching theme. Without this the OS draws a
   light popup over a dark page and vice versa. */
:root { color-scheme: dark; }
[data-theme="light"]:not([data-app="studio"]) { color-scheme: light; }

/* ╔══════════════════════════════════════════════════════════════════════╗
   ║  ANTIALIASING POLICY — Windows and macOS, one rule                     ║
   ║  Authored 2026-07-28 (L: "make sure antialiasing is done properly for  ║
   ║  fonts for both windows and mac").                                     ║
   ║                                                                        ║
   ║  ── What each platform actually does ──                                ║
   ║  macOS renders subpixel-antialiased text by default, which THICKENS    ║
   ║  glyphs. `-webkit-font-smoothing: antialiased` switches to greyscale   ║
   ║  AA, which THINS them. Windows uses DirectWrite and ignores the        ║
   ║  property entirely — it is a no-op there, in every browser.            ║
   ║                                                                        ║
   ║  ── Why the old blanket rule was wrong ──                              ║
   ║  It was applied to html/body unconditionally, which means it:          ║
   ║    · did nothing on Windows (most of this product's users)             ║
   ║    · thinned ALL text on macOS, including dark-on-light, where the     ║
   ║      subpixel rendering it disabled is exactly what makes small text   ║
   ║      crisp. Body copy at 14px lost stroke weight for no gain.          ║
   ║  Greyscale AA is also an a11y regression for low-vision users, who     ║
   ║  benefit from the extra apparent weight.                               ║
   ║                                                                        ║
   ║  ── The policy ──                                                      ║
   ║  Apply greyscale AA ONLY where it earns its place: LIGHT TEXT ON A     ║
   ║  DARK GROUND. That is the one case where macOS subpixel rendering      ║
   ║  visibly over-thickens and fringes glyphs, and thinning genuinely      ║
   ║  improves them. Every light surface keeps the OS default, so Windows   ║
   ║  ClearType and macOS subpixel AA both do their best work.              ║
   ║                                                                        ║
   ║  Net effect: ONSTAGE night → greyscale (light on dark).                ║
   ║              ONSTAGE day / BACKSTAGE light → OS default.               ║
   ║              BACKSTAGE dark → greyscale.                               ║
   ║  Windows is unaffected either way, which is correct: DirectWrite       ║
   ║  already renders both cases well and has no equivalent knob to abuse.  ║
   ║                                                                        ║
   ║  `font-synthesis: none` (set on html/body above) is the Windows-and-   ║
   ║  macOS-relevant half of this: it stops the browser FAKING a missing    ║
   ║  weight or italic by mechanically smearing the glyph. This system has  ║
   ║  two faces with real gaps — LINE Seed TW ships no 500, Satoshi ships   ║
   ║  no 600 — so without it those weights render as synthesised sludge     ║
   ║  rather than falling cleanly to the nearest real cut.                  ║
   ║                                                                        ║
   ║  NOT USED, deliberately: `text-rendering: optimizeLegibility`. It has  ║
   ║  a long history of disabling ligatures/kerning inconsistently and      ║
   ║  costing layout time on long documents, and it gives nothing the font  ║
   ║  files do not already provide.                                         ║
   ╚══════════════════════════════════════════════════════════════════════╝ */

/* Dark grounds → greyscale AA (macOS only; a no-op on Windows by design). */
:root,
[data-app="studio"][data-theme="dark"],
[data-theme="dark"] [data-app="studio"] {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Light grounds → hand rendering back to the OS. `auto` is the reset that
   re-enables macOS subpixel AA; Windows ClearType was never affected. */
[data-theme="light"]:not([data-app="studio"]),
[data-app="studio"]:not([data-theme="dark"]) {
  -webkit-font-smoothing: auto;
  -moz-osx-font-smoothing: auto;
}

/* ─────────────────────────────────────────
   TYPOGRAPHY UTILITY CLASSES
   18 styles matching Figma Foundations
   ───────────────────────────────────────── */

.t-display {
  font-family: var(--font-cjk-display);
  font-size: var(--fs-display);
  line-height: var(--lh-display);
  letter-spacing: var(--ls-display);
  font-weight: var(--fw-bold);
}

.t-h1 {
  font-family: var(--font-cjk-display);
  font-size: var(--fs-h1);
  line-height: var(--lh-h1);
  letter-spacing: var(--ls-h1);
  font-weight: var(--fw-bold);
}

.t-h2 {
  font-family: var(--font-cjk-display);
  font-size: var(--fs-h2);
  line-height: var(--lh-h2);
  letter-spacing: var(--ls-h2);
  font-weight: var(--fw-bold);
}

.t-feature-title {
  font-family: var(--font-cjk-display);
  font-size: var(--fs-feature-title);
  line-height: var(--lh-feature-title);
  letter-spacing: var(--ls-h2);
  font-weight: var(--fw-bold);
}

.t-h3 {
  font-family: var(--font-cjk-display);
  font-size: var(--fs-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--ls-h3);
  font-weight: var(--fw-bold);
}

.t-h4 {
  font-family: var(--font-cjk-display);
  font-size: var(--fs-h4);
  line-height: var(--lh-h4);
  font-weight: var(--fw-bold);
}

.t-h5 {
  font-family: var(--font-cjk-text);
  font-size: var(--fs-h5);
  line-height: var(--lh-h5);
  font-weight: var(--fw-bold);
}

.t-body-lg {
  font-family: var(--font-cjk-text);
  font-size: var(--fs-body-lg);
  line-height: var(--lh-body-lg);
  font-weight: var(--fw-regular);
}

.t-body {
  font-family: var(--font-cjk-text);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  font-weight: var(--fw-regular);
}

.t-body-sm {
  font-family: var(--font-cjk-text);
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  font-weight: var(--fw-regular);
}

.t-body-reading {
  font-family: var(--font-cjk-text);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  font-weight: var(--fw-light);
}

.t-body-preview {
  font-family: var(--font-cjk-text);
  font-size: var(--fs-body-lg);
  line-height: var(--lh-body-lg);
  font-weight: var(--fw-light);
}

.t-caption {
  font-family: var(--font-cjk-text);
  font-size: var(--fs-caption);
  line-height: var(--lh-caption);
  font-weight: var(--fw-regular);
}

.t-eyebrow {
  font-family: var(--font-cjk-text);
  font-size: var(--fs-caption);
  line-height: var(--lh-label);
  letter-spacing: var(--ls-label);
  font-weight: var(--fw-medium);
  text-transform: uppercase;
}

.t-nav {
  font-family: var(--font-cjk-display);
  font-size: var(--fs-h5);
  line-height: var(--lh-h5);
  font-weight: var(--fw-regular);
}

.t-button-lg {
  font-family: var(--font-cjk-display);
  font-size: var(--fs-h5);
  line-height: var(--lh-h5);
  font-weight: var(--fw-bold);
}

.t-button-md {
  font-family: var(--font-cjk-text);
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  font-weight: var(--fw-medium);
}

.t-button-sm {
  font-family: var(--font-cjk-text);
  font-size: var(--fs-caption);
  line-height: var(--lh-caption);
  font-weight: var(--fw-medium);
}

/* ─────────────────────────────────────────
   GLOBAL RESET / BASE
   ───────────────────────────────────────── */

*, *::before, *::after {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg-primary);
  color: var(--text-primary);
  font-family: var(--font-cjk-text);
  /* ── ANTIALIASING — see the block below. Deliberately NOT set here. ── */
  /* A blanket `-webkit-font-smoothing: antialiased` used to live on this rule.
     It is removed because it is a macOS-only instruction that made every light
     surface worse and did nothing at all on Windows. Policy is theme-aware and
     lives in one place below. */
  font-synthesis: none;
}

/* Sticky footer：main 自己撐到「視窗高 − header 高」，內部 flex column，
   footer 用 margin-top:auto 推到底。內容少的短頁（如活動頁）下方留白落在
   main 內，footer 不浮上來。footer 位於 main 內部（全站結構一致）。
   main 自帶 min-height，不依賴 body 是否為 flex 容器。 */
main {
  display: flex;
  flex-direction: column;
  min-height: calc(100vh - var(--header-height));
}

/* Reserve the scrollbar gutter so navigating between a tall page (scrollbar
   shown) and a short one (no scrollbar) doesn't shift centred content — the
   "jump" on page change. No-op on overlay-scrollbar systems (e.g. macOS). */
html {
  scrollbar-gutter: stable;
}

a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; }
img { max-width: 100%; display: block; }

/* Accessibility — global keyboard focus ring (WCAG 2.4.7 Focus Visible).
   :focus-visible only → never shows on mouse/touch, only keyboard. Components
   with their own :focus-visible (higher specificity) keep their custom ring;
   this is the baseline for everything else that had none. Additive — no
   change to mouse/touch interaction anywhere. */
:focus-visible {
  /* --focus-ring, not --yellow-500 directly: brand orange is 1.3:1 on a light
     canvas, below the 3:1 WCAG non-text floor. Day mode retunes this ONE token
     instead of every component that draws a ring. */
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Text selection — brand yellow with ink text, replacing the browser
   default magenta/blue. */
::selection {
  background: var(--yellow-500);
  color: var(--yellow-ink);
}
::-moz-selection {
  background: var(--yellow-500);
  color: var(--yellow-ink);
}

/* ─────────────────────────────────────────
   LAYOUT UTILITIES
   ───────────────────────────────────────── */

.container {
  max-width: var(--container-max);
  margin: 0 auto;
  padding-left: var(--container-padding-x);
  padding-right: var(--container-padding-x);
}

.container--wide {
  max-width: var(--container-wide);
}

.full-bleed {
  width: 100%;
}

/* ─────────────────────────────────────────
   SECTION RHYTHM
   Each homepage chapter sits inside a .section.
   Backgrounds alternate page <-> surface for cinematic zebra rhythm.
   ───────────────────────────────────────── */

.section {
  position: relative;
  padding-block: var(--space-section);
}

.section--tight { padding-block: clamp(48px, 6vw, 80px); }
.section--loose { padding-block: clamp(96px, 12vw, 160px); }

/* ── SECTION BACKGROUNDS + THEMES moved out ───────────────────────────────
   The zebra background classes (.section-bg-page / .section-bg-surface) and
   all their foreground contrast rules now live in their own file:
       section-themes.css   (linked after components.css on every page)
   Keep section-background work there — do NOT re-add it here. ──────────── */

/* ─────────────────────────────────────────
   SECTION EYEBROW
   Yellow brand label that opens each homepage section.
   ───────────────────────────────────────── */

.section-eyebrow {
  display: inline-flex;
  align-items: center;
  font-family: var(--font-cjk-text);
  font-size: var(--fs-caption);
  line-height: 1.33;
  letter-spacing: 0.12em;
  font-weight: var(--fw-medium);
  text-transform: uppercase;
  color: var(--yellow-500);
}

/* ─────────────────────────────────────────
   SECTION HEADER
   Pattern: [chapter-eyebrow] → [title row: title/sub on left, link on right]
   Used at the top of every chapter on the homepage.
   ───────────────────────────────────────── */

.section-header {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-bottom: var(--space-10);
}

.section-header__row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-8);
  flex-wrap: wrap;
}

.section-header__copy {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.section-header__title {
  font-family: var(--font-cjk-display);
  font-weight: var(--fw-bold);
  font-size: clamp(28px, 3.4vw, 42px);
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  margin: 0;
}

.section-header__sub {
  font-family: var(--font-cjk-text);
  font-weight: var(--fw-regular);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--text-secondary);
  max-width: 520px;
  margin: 0;
}

/* "查看所有 →" right-side link */
.section-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-cjk-text);
  font-size: var(--fs-body-sm);
  font-weight: var(--fw-medium);
  color: var(--yellow-500);
  white-space: nowrap;
  /* Invisible hit-area expansion: padding grows the tap target to ~44px,
     the matching negative margin cancels the layout shift, so the link
     looks identical but is far easier to hit. Visual weight unchanged
     on purpose — the section's CTA is its content, not this exit. */
  padding: 12px 14px;
  margin: -12px -14px;
  border-radius: var(--radius-pill);
  transition: gap 150ms ease;
}

.section-link:hover { gap: var(--space-3); }

.section-link__arrow {
  width: 16px;
  height: 16px;
  display: inline-block;
  /* Plain arrow in the link's own colour (currentColor) — at 16px the glass
     material is too small to read, so we drop it for a clean single-colour
     glyph. currentColor means it adapts to any section background (yellow on
     dark, --yellow-ink on the yellow surface) with no per-theme image. */
  background: currentColor;
  -webkit-mask: url('assets/icons/arrow_right.svg') center / contain no-repeat;
          mask: url('assets/icons/arrow_right.svg') center / contain no-repeat;
  /* The "beckon" loop lives below, gated behind .is-beckoning — a per-link
     IntersectionObserver (index.html / screening-room.html) adds the class
     when THE LINK enters the viewport, removes it when fully off-screen.
     So the knock plays the moment you arrive at a section, every visit.
     Without JS the arrow is simply still — motion is enhancement. */
}

/* Ambient "beckon": the arrow alone knocks twice (6px, then a 3px echo)
   the moment its section arrives, then rests — 840ms of motion at the
   START of a 3.5s cycle (76% silence after). Knock-first matters: the
   observer adds the class on entry, so the eye sees the loop begin.
   Horizontal on purpose: it rehearses the hover's gap-widen instead of
   introducing a second motion language. Whole-link bounce is banned —
   moving text can't be read. Viewport entry IS the stagger: links arrive
   one at a time, so no per-section delays are needed.
   Percentages = absolute knock times (240/480/660/840ms) over the cycle —
   change the duration and you change the knock speed; recompute, don't
   eyeball. */
.section-link.is-beckoning .section-link__arrow {
  animation: section-link-beckon 3.5s infinite;
}

@keyframes section-link-beckon {
  0%     { transform: translateX(0);
           animation-timing-function: var(--ease-out); }
  6.9%   { transform: translateX(6px);
           animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
  13.7%  { transform: translateX(0);
           animation-timing-function: var(--ease-out); }
  18.9%  { transform: translateX(3px);
           animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
  24%    { transform: translateX(0); }
  100%   { transform: translateX(0); }
}

/* On hover the invitation goes quiet and the gap-widen does its job.
   Pause (not none) — `none` would snap a mid-knock arrow back. */
@media (hover: hover) and (pointer: fine) {
  .section-link:hover .section-link__arrow { animation-play-state: paused; }
}

/* The loop is pure decoration — remove it entirely, keep hover feedback. */
@media (prefers-reduced-motion: reduce) {
  .section-link__arrow { animation: none; }
}

/* ── ENGLISH LOCALE TYPE TIER — RETIRED (2026-07-28) ──────────────────────
   This block used to swap the English locale onto Satoshi + Afacad, with an
   Afacad allow-list of ~30 title selectors and per-selector +2px optical
   compensation for Afacad's smaller x-height.

   All of it is now unnecessary: Satoshi is the Latin face in BOTH locales, so
   English needs no swap and no compensation. Afacad is retired per L's
   directive ("use satoshi for all english fonts"). The residual
   html[lang='en'] declarations live in fonts.css purely to override any
   stale copies elsewhere.
   ──────────────────────────────────────────────────────────────────────── */

/* ═══════════════════════════════════════════════════════════════════════════
   RESPONSIVE SPACING TOKENS (foundation)
   Semantic spacing on top of the --space-* primitive scale, redefined at the
   phone breakpoint so spacing flows from ONE place instead of being hand-tuned
   per component. Desktop and mobile get genuinely different values here.
   Components reference these (--space-section, --gap-card, --gap-rail, --stack-*)
   so a single edit retunes density site-wide. Migration is incremental: new +
   refactored components use these; the legacy px get converted in the sweep.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
  --space-section: clamp(64px, 9vw, 100px);   /* .section vertical rhythm */
  --gap-card: var(--space-6);                 /* card inner padding (24px) */
  --gap-rail: var(--space-6);                 /* rail / grid gap (24px) */
  --stack-md: var(--space-5);                 /* medium vertical stack (20px) */
  --stack-lg: var(--space-10);                /* section-header bottom (40px) */
}
@media (max-width: 767.98px) {
  :root {
    --space-section: clamp(40px, 10vw, 64px); /* tighter vertical rhythm on phone */
    --gap-card: var(--space-4);               /* 24 → 16 */
    --gap-rail: var(--space-4);               /* 24 → 16 */
    --stack-lg: var(--space-8);               /* 40 → 20 */
  }
}
