/* ============================================================================
 * FERRY SHARED DESIGN TOKENS  ·  F3 TOKEN FOUNDATION
 * ----------------------------------------------------------------------------
 * THE single physical token source for both Ferry frontend surfaces:
 *
 *   companion surface  the React SPA           frontend/src/**  served by /index.html
 *   story surface      the static HTML cluster frontend/public/** served at /*.html
 *
 * Both surfaces load THIS file and no surface keeps a second copy of it:
 *   · React      <link rel="stylesheet" href="/assets/css/ferry-tokens.css?v=f3.0">
 *                in frontend/index.html, plus data-theme="companion" on <html>
 *   · Story      @import url("/assets/css/ferry-tokens.css?v=f3.0"); at the top of
 *                frontend/public/ferry-ui.css, plus a <link> on the story pages that
 *                do not load ferry-ui.css
 *
 * ---------------------------------------------------------------------------
 * LAYERS - a component may reference the SEMANTIC layer only
 * ---------------------------------------------------------------------------
 *   L1  primitive   raw value, no meaning            --palette-*  --gap-*  --corner-*
 *   L2  semantic    a role in the UI                 --surface-page  --text-primary  ...
 *   L3  component   a deliberate deviation only      (empty in F3 - see section 5)
 *
 * A `var(--palette-...)` inside a component rule is a defect. A component token that
 * merely renames a semantic token is also a defect.
 *
 * ---------------------------------------------------------------------------
 * RULES THIS FILE EXISTS TO ENFORCE
 * ---------------------------------------------------------------------------
 *   1. ONE STRUCTURE. One spacing ramp, one radius grammar, one control scale, one
 *      safe-area vocabulary, one z ladder. Neither surface invents a second one.
 *   2. TWO THEMES ALLOWED. `companion` and `story` differ in colour values, surface
 *      mood and decorative treatment. They may NOT differ in names, scales, control
 *      heights, interaction states or layout grammar.
 *   3. Visually neutral by construction. F3 renames and declares; it does not
 *      re-colour, re-round or re-space anything. Every token here either already
 *      resolves to the value it carries on the surface that consumes it, or is not
 *      consumed yet. Adoption is a separate, per-round decision.
 *   4. Provenance is mandatory. Every value carries the file:line it was measured
 *      from, or is marked DERIVED with the reason. No value was invented silently.
 *   5. Legacy is documented, not silently rounded. Off-scale values that exist in
 *      production (7px, 9px, 10px radius, 14px radius, 34px controls) are recorded
 *      as LEGACY_EXCEPTION with a count. §3 of the F3 report lists them and the
 *      pixel changes they would cause. F3 does not perform those roundings.
 *
 * ---------------------------------------------------------------------------
 * THEME BINDING - read this before adding a theme
 * ---------------------------------------------------------------------------
 *   :root, [data-theme="story"], [data-theme="dark"]   the story dark treatment
 *   [data-theme="companion"]                           the companion warm-light treatment
 *
 * `:root` is the story scope because the story cluster's own `:root` (ferry-ui.css)
 * already IS the story dark theme, and most story pages carry no data-theme attribute
 * at all. Binding the default to story keeps every existing story page resolving
 * exactly as it does today, with no HTML edits.
 *
 * The story surface ALSO has a light treatment, selected by data-theme="light" and used
 * by the create/landing family. It is a third treatment that the two-theme contract does
 * NOT cover. F3 deliberately leaves it alone: mapping it is a design decision (report
 * section 'Known gaps'). Because `:root` always matches, a light story page resolves
 * these roles to the story DARK values - documented, and harmless this round because
 * nothing consumes them yet.
 *
 * No page may invent a third theme. A page MAY vary surface tint or decorative
 * treatment; it may NOT redefine the typography roles, the spacing ramp, the radius
 * grammar, the control scale or the safe-area contract.
 *
 * ---------------------------------------------------------------------------
 * DELIVERY / CACHE NOTE
 * ---------------------------------------------------------------------------
 * nginx serves /assets/* as `expires 30d; immutable`. The `?v=` query on every
 * reference is therefore REQUIRED: it is the cache key. When this file changes, bump
 * FERRY_TOKENS_RELEASE in the same commit at both reference sites (frontend/index.html
 * and frontend/public/ferry-ui.css) and on the story pages that link it directly.
 * verify_token_contract.py asserts that all references agree.
 *
 *   FERRY_TOKENS_RELEASE = f3.0
 *
 * ---------------------------------------------------------------------------
 * COMMENT SAFETY - same trap ferry-ui.css documents
 * ---------------------------------------------------------------------------
 * Never write an asterisk immediately followed by a forward slash inside a comment in
 * this file. It terminates the comment early and swallows the next rule block whole.
 * ==========================================================================*/

/* ══════════════════════════════════════════════════════════════════════════
 * 1 · L1  PRIMITIVES - raw values, no meaning, theme independent
 * --------------------------------------------------------------------------
 * Two ramps, one per surface identity, plus the shared feedback set. Both themes
 * express their semantic roles in terms of these; neither surface's own legacy
 * primitive names (--fr-*, --moon/--mist/--gold, --c-*) are touched by F3.
 *
 * Provenance: every value below is measured from the production sources named in
 * the comment. Nothing was selected by eye.
 * ══════════════════════════════════════════════════════════════════════════ */

:root {
  /* ---- companion warm ramp -------------------------------------------------
     index.css:21 body gradient #FAF8F5 → #F8F2EC → #F5ECE4
     persona.css:28-34 --moon #f8f5f0, --moon-soft #fcfaf7, --moon-deep #f2eee8,
                      --mist #efeae3, --mist-deep #e4dcd2
     tailwind.config.js:10 'ferry-bg' #F7F4EF */
  --palette-warm-100: #FCFAF7;
  --palette-warm-200: #FAF8F5;
  --palette-warm-300: #F8F2EC;
  --palette-warm-400: #F5ECE4;
  --palette-warm-500: #F7F4EF;
  --palette-warm-600: #F2EEE8;
  --palette-mist-100: #EFEAE3;
  --palette-mist-200: #E4DCD2;

  /* ---- companion ink ------------------------------------------------------
     index.css:23 body colour #1A1A1A
     tailwind.config.js:16-17 'ferry-text-secondary' #6B6B6B, 'ferry-muted' #9B9B9B
     tailwind.config.js:34 'ferry-muted-2' #86909C */
  --palette-ink-900: #1A1A1A;
  --palette-ink-700: #6B6B6B;
  --palette-ink-500: #9B9B9B;
  --palette-ink-300: #86909C;

  /* ---- companion navy (brand + text-on-light-accent) ----------------------
     persona.css:45-48 --ink #16324a, --ink-soft #2c4a63, --ink-muted #6b7f90
     tailwind.config.js:12 'ferry-brand' #16324A, :11 'ferry-brand-deep' #0E2233
     tailwind.config.js:14 'ferry-highlight' #4A7A9E */
  --palette-navy-900: #0E2233;
  --palette-navy-800: #16324A;
  --palette-navy-700: #2C4A63;
  --palette-navy-500: #4A7A9E;
  --palette-navy-300: #6B7F90;

  /* ---- companion gold (the single accent) --------------------------------
     persona.css:51-53 --gold #e8a87c, --gold-light #f2cdb0, --gold-deep #c98a5e
     tailwind.config.js:13 'ferry-accent' #E8A87C
     index.css:141 .text-gradient-gold #E8A87C */
  --palette-gold-300: #F2CDB0;
  --palette-gold-400: #E8A87C;
  --palette-gold-500: #C98A5E;

  /* ---- story candle ramp (warm brown, no neutral grey in any step) --------
     ferry-ui.css:38-49 --fr-ink-0 .. --fr-ink-11 */
  --palette-candle-0: #140C09;
  --palette-candle-1: #1A100C;
  --palette-candle-2: #211410;
  --palette-candle-3: #291A14;
  --palette-candle-4: #34211A;
  --palette-candle-5: #4A3128;
  --palette-candle-6: #8A6A58;
  --palette-candle-7: #B08D77;
  --palette-candle-8: #D4B49C;
  --palette-candle-9: #F7EADC;

  /* ---- story bronze (the only metal) -------------------------------------
     ferry-ui.css:52-57 --fr-gold-100 .. --fr-gold-600 */
  --palette-bronze-200: #F7E4BF;
  --palette-bronze-300: #EDD2A0;
  --palette-bronze-400: #DEBB80;
  --palette-bronze-500: #CBA25F;
  --palette-bronze-600: #A87F42;

  /* ---- story wine (the accent / emotion colour) --------------------------
     ferry-ui.css:65-69 --fr-wine-200 .. --fr-wine-600 */
  --palette-wine-200: #E2A79A;
  --palette-wine-300: #C4715F;
  --palette-wine-400: #A6402F;
  --palette-wine-500: #8D2520;
  --palette-wine-600: #621B14;

  /* ---- feedback, story side ---------------------------------------------
     ferry-ui.css:76-79 --fr-green-400 #8FB892, --fr-amber-400 #D9A85C,
                         --fr-red-400 #D9736A, --fr-red-300 #E9A79E */
  --palette-sage-400: #8FB892;
  --palette-amber-400: #D9A85C;
  --palette-rose-400: #D9736A;

  /* ---- feedback, companion side -----------------------------------------
     DERIVED. The companion surface has no feedback tokens: it writes Tailwind
     utility classes (text-red-*, text-green-*, text-amber-*). These are the
     Tailwind scale values for the utilities already in use.
       danger  Tailwind red-600    also present in backend/templates/admin/base.html
       success Tailwind emerald-500 value #0E9F6E, measured from the repo's --ok set
       warning tailwind.config.js:35 'ferry-warning-orange' #D97706
     Marked DERIVED / NEEDS-DESIGN-SIGNOFF: they carry a role, not a measurement. */
  --palette-red-600: #DC2626;
  --palette-emerald-500: #0E9F6E;
  --palette-amber-500: #D97706;

  /* ---- opaque ends ------------------------------------------------------ */
  --palette-white: #FFFFFF;
  --palette-black: #000000;
}

/* ══════════════════════════════════════════════════════════════════════════
 * 2 · L2  SEMANTIC SCALES - theme independent, shared by both surfaces
 * --------------------------------------------------------------------------
 * Spacing, radius, control, hit target, layering, motion, safe area, measure and
 * the nine typography roles. None of these differ between the two themes, and none
 * of them is consumed by an existing rule yet, so declaring them renders nothing
 * different. Adoption is tracked in TOKEN_ADOPTION_MAP.csv.
 * ══════════════════════════════════════════════════════════════════════════ */

:root {
  /* ---- spacing: 2px base, 4px rhythm ------------------------------------
     Derived from DESIGN_TOKEN_VALUE_FREQUENCY.csv, not chosen: the top values are
     0 (x217), 8 (x154), 10 (x112), 12 (x109), 6 (x96), 4 (x92), 2 (x80), 14 (x70),
     16 (x47), 20 (x39).

     LEGACY_EXCEPTION (F3 does NOT round these, they are rendered pixels):
       7px  x54  sits between --gap-3 (6) and --gap-4 (8)
       9px  x41  sits between --gap-4 (8) and --gap-5 (10)
     14px is promoted rather than rounded, because 70 uses is too many to be an
     accident - it is --gap-7.

     Note on the name: React's phone.css already owns `--space-1..10`, meaning 4px
     steps (--space-1 = 4px). Reusing `--space-*` here would silently rescale every
     consumer of that ramp. The shared ramp is therefore named by px value under a
     currently free prefix. `--gap-*`, `--corner-*`, `--layer-*`, `--type-*` and
     `--palette-*` were each verified free across both surfaces before use. */
  --gap-0: 0;
  --gap-1: 2px;
  --gap-2: 4px;
  --gap-3: 6px;
  --gap-4: 8px;
  --gap-5: 10px;
  --gap-6: 12px;
  --gap-7: 14px;
  --gap-8: 16px;
  --gap-10: 20px;
  --gap-12: 24px;
  --gap-16: 32px;
  --gap-20: 40px;
  --gap-24: 48px;
  --gap-32: 64px;

  /* ---- radius grammar: five steps, two shapes ---------------------------
     "pill" is currently spelled five ways in production: 999px (x74), 50% (x73),
     var(--r-pill) (x59), var(--r-full) (x41), 1000px (x22). One name now.

     LEGACY_EXCEPTION (visible change to snap, deferred, see report):
       10px (x36) sits between --corner-sm (8) and --corner-md (12)
       14px (x36) sits between --corner-md (12) and --corner-lg (16)
     Story's --r-xs=10 / --r-sm=14 and React's --radius-xs=10 / --radius-sm=14
     are exactly those two values, so the legacy set is a real, shared, documented
     exception rather than drift. */
  --corner-xs: 6px;
  --corner-sm: 8px;
  --corner-md: 12px;
  --corner-lg: 16px;
  --corner-xl: 20px;
  --corner-pill: 999px;
  --corner-circle: 50%;

  /* ---- control scale: one height per function ---------------------------
     Measured from the four chat pages' own send/back buttons:
       34px  story solo back + send         -> --control-sm
       40px  story group + world send       -> --control-md
       46px  companion send and voice       -> --control-lg
       52px  create family primary CTA      -> --control-xl  (create-ui.css:54 --h-btn)
     The visual box comes from this scale and never changes height while loading
     (companion swaps its arrow for a spinner inside a fixed 46px box: the pattern).

     ACCESSIBILITY, recorded as a real defect and NOT fixed this round:
     --control-sm (34px) is below the 44px touch-target guidance used by both iOS
     and Android. Raising the visual box to 44px is a visible change and needs
     sign-off. The contract therefore separates the two: the VISUAL box may be 34px,
     the HIT AREA must be at least --hit-target-min, reached with padding or a
     pseudo-element. */
  --control-sm: 34px;
  --control-md: 40px;
  --control-lg: 46px;
  --control-xl: 52px;
  --hit-target-min: 44px;

  /* ---- layering: one semantic ladder ------------------------------------
     Adopted from the story cluster, which already publishes named levels
     (ferry-ui.css:135-136 --z-base 0 --z-sticky 10 --z-header 20 --z-drawer 40
      --z-sheet 60 --z-modal 80 --z-toast 100; create-ui.css:62-67 adds 70 for the
      guide and maps --z-crop onto 80).

     React has no scale at all: DESIGN_TOKEN_VALUE_FREQUENCY.csv records 30 raw
     z-index values, 12 of them inside the 40-60 band with no rule, plus the legacy
     `z-index: 2147482000` on .mobile-nav (mobile-bottom-nav.css:4, carried over
     verbatim from ferry-entry.js during F1).

     F3 publishes the ladder only. It does NOT remap React onto it: stacking today
     is a shipped behaviour and remapping it is a visible change with its own
     verification. Mapping table is in the F3 report. */
  --layer-base: 0;
  --layer-sticky: 10;
  --layer-header: 20;
  --layer-nav: 40;
  --layer-overlay: 60;
  --layer-modal: 80;
  --layer-toast: 100;

  /* ---- motion: one duration ramp, one easing pair -----------------------
     React already tokenises durations but defines them per stylesheet, so the
     SAME name resolves to different values depending on which chunk loaded:
       home.css:42  --motion-fast 180ms      landing.css:42  --motion-fast 180ms
       login.css:43 --motion-fast 170ms      persona.css:59  --motion-fast 180ms
     That is the drift the contract removes. --ease-standard is React's --ease-out
     (cubic-bezier(.16,1,.3,1), declared identically in four React stylesheets).
     --ease-emphasized is the story cluster's --ease-spring (ferry-ui.css:132).

     Story writes its own rhythm (--dur-1 200ms, --dur-2 320ms, --dur-3 520ms,
     ferry-ui.css:133). Those are NOT unified by F3: changing an animation's speed is
     a visible change. The alias table is in the F3 report and adoption is F4's. */
  --dur-micro: 120ms;
  --dur-fast: 180ms;
  --dur-base: 280ms;
  --dur-slow: 480ms;
  --ease-standard: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-emphasized: cubic-bezier(0.34, 1.42, 0.48, 1);

  /* ---- safe area: one vocabulary ----------------------------------------
     The companion surface had NO env(safe-area-inset-*) anywhere before F3, while
     the three story chats handled it three different ways (ferry-ui.css:140-142
     --sa-b / --safe-b / --safe-t, a raw env() in the group chat, another in the
     world chat). Every fixed bottom element must now consume --safe-bottom. */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-right: env(safe-area-inset-right, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);

  /* ---- viewport / measure / shell contract ------------------------------
     F2 §1 sets the viewport contract (PRIMARY 390, SECONDARY 430, DESKTOP 1440) and
     the single reading measure at 768px. The shell/bottom-nav offsets are vocabulary
     F4 (Unified Chat) consumes; they change nothing today. */
  --viewport-height: 100dvh;
  --measure-read: 768px;
  --measure-narrow: 720px;
  --shell-max-width: 560px;
  --bottom-nav-height: 60px;
  --bottom-nav-offset: calc(var(--bottom-nav-height) + var(--safe-bottom));
  --composer-safe-bottom: calc(var(--safe-bottom) + var(--gap-4));

  /* ---- typography: nine roles, not 70 font sizes ------------------------
     Each size below is the DOMINANT current value for that role in
     DESIGN_TOKEN_VALUE_FREQUENCY.csv, so the first migration step can be a pure
     rename wherever the dominant value is adopted:
       display   24px        (24px x?, .dock-title 1.4375rem, aw-title 24px)
       title     17px        (companion chat h1)
       section   15px        (x55)
       body      14.5px      (the chat bubble size on BOTH surfaces)
       body-sm   13.5px      (x30)
       secondary 13px        (the single most used size, x157)
       caption   12px        (x155)
       micro     11px        (x83)
       narration 15.5px      (story prose)
     font sizes with no role are LEGACY_EXCEPTION and are not replaced in F3. */
  --type-display-size: 24px;
  --type-display-leading: 1.35;
  --type-display-weight: 600;

  --type-title-size: 17px;
  --type-title-leading: 1.3;
  --type-title-weight: 600;

  --type-section-size: 15px;
  --type-section-leading: 1.4;
  --type-section-weight: 600;

  --type-body-size: 14.5px;
  --type-body-leading: 1.7;
  --type-body-weight: 400;

  --type-body-sm-size: 13.5px;
  --type-body-sm-leading: 1.6;

  --type-secondary-size: 13px;
  --type-secondary-leading: 1.6;

  --type-caption-size: 12px;
  --type-caption-leading: 1.4;
  --type-caption-weight: 500;

  --type-micro-size: 11px;
  --type-micro-leading: 1.3;
  --type-micro-weight: 500;

  --type-narration-size: 15.5px;
  --type-narration-leading: 1.85;
  --type-narration-family: var(--font-prose);

  /* ---- numeric weights --------------------------------------------------
     Production declares 11 distinct font-weights including 650 and 800. The scale is
     4 steps; the two off-scale values are LEGACY_EXCEPTION. */
  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;
}

/* ══════════════════════════════════════════════════════════════════════════
 * 3 · L2  SEMANTIC THEMES - same names, different values
 * --------------------------------------------------------------------------
 * Every role in section 3.a is REQUIRED in both themes. Adding a role means adding
 * it to both. verify_token_contract.py fails the build if a required role is missing
 * from a theme, or if a role is defined twice inside one theme scope.
 * ══════════════════════════════════════════════════════════════════════════ */

/* ── 3.a · STORY THEME (also the default, see THEME BINDING above) ───────
 * The story dark treatment: warm brown ground, cream text, bronze metal, wine accent.
 * Values delegate through `var(<story owner>, <literal>)` so that this file states the
 * role while the story surface keeps owning its own primitive ramp. The two are equal
 * by construction - if story ever changes --fr-ink-0 the role follows it. */
:root,
[data-theme="story"],
[data-theme="dark"] {
  /* surfaces — ferry-ui.css:174-179 --bg-0..--bg-3, --bg-scrim */
  --surface-page: var(--fr-ink-0, #140C09);
  --surface-raised: var(--fr-ink-2, #211410);
  --surface-overlay: var(--fr-ink-4, #34211A);
  --surface-inverse: var(--fr-ink-11, #F7EADC);

  /* text — ferry-ui.css:181-186 --fg-0..--fg-3 */
  --text-primary: var(--fr-ink-11, #F7EADC);
  --text-secondary: var(--fr-ink-10, #D4B49C);
  --text-muted: var(--fr-ink-9, #B08D77);
  --text-inverse: var(--fg-on-gold, #2A1608);
  --text-on-accent: var(--fg-on-accent, #F9EDD9);

  /* borders — ferry-ui.css:190-192 --line-0..--line-2 */
  --border-subtle: var(--line-0, rgba(247, 234, 220, 0.07));
  --border-strong: var(--line-2, rgba(247, 234, 220, 0.22));

  /* accent — ferry-ui.css:201-211. Story's primary action colour is wine; the
     bronze is the metal, not the accent, and stays a decorative treatment. */
  --accent-primary: var(--accent, #A6402F);
  --accent-primary-hover: var(--accent-hover, #C4715F);
  --accent-primary-soft: var(--wine-soft, rgba(141, 37, 32, 0.16));
  --accent-on: var(--fg-on-accent, #F9EDD9);

  /* feedback — ferry-ui.css:217-220 */
  --feedback-danger: var(--fr-red-400, #D9736A);
  --feedback-success: var(--fr-green-400, #8FB892);
  --feedback-warning: var(--fr-amber-400, #D9A85C);

  --focus-ring-color: var(--fr-gold-300, #DEBB80);

  /* elevation — ferry-ui.css:119-128 --e-1..--e-3 */
  --lift-1: var(--e-1, 0 1px 2px rgba(28, 12, 6, 0.3));
  --lift-2: var(--e-2, 0 8px 24px rgba(28, 12, 6, 0.38));
  --lift-3: var(--e-3, 0 18px 48px rgba(28, 12, 6, 0.48));

  /* fonts.
     --font-ui and --font-mono are DELIBERATELY ABSENT here: the story surface already
     owns them at ferry-ui.css:111 and :115, and this file is @imported BEFORE
     ferry-ui.css, so a definition here would be shadowed and would exist only as a
     second home for the same value. F3 therefore records the role as
     SATISFIED-BY-EXISTING-OWNER rather than forking it. See TOKEN_CONTRACT_AUDIT.json,
     field STORY_ROLE_SATISFIED_BY_SHARED_OWNER.
     --font-prose is the exception: story-chat.html:45 defines it page-locally as
     var(--font-serif), so the shared name needs a home in the shared layer too. */
  --font-prose: var(--font-serif, "Songti SC", "STSong", serif);
  --font-decorative: 'Ma Shan Zheng', 'ZCOOL XiaoWei', 'STKaiti', 'Kaiti SC', 'KaiTi',
                     '楷体', 'Noto Serif SC', 'Songti SC', 'SimSun', Georgia, serif;
}

/* ── 3.b · COMPANION THEME ───────────────────────────────────────────────
 * The React warm-light treatment. Selected by <html data-theme="companion">, which
 * frontend/index.html sets. Nothing keyed off that attribute before F3, so adding it
 * changes no computed style.
 *
 * Provenance is per-role. Where the companion surface has no existing token for a
 * role the value is marked DERIVED and needs design sign-off - it names a role that
 * genuinely had no home, which is exactly what F2 finding #15 counts as a defect. */
[data-theme="companion"] {
  /* surfaces */
  --surface-page: var(--palette-warm-200);          /* index.css:21 #FAF8F5 */
  --surface-raised: rgba(255, 255, 255, 0.7);       /* index.css:73 .glass-card  */
  --surface-overlay: var(--palette-warm-100);       /* persona.css:29 --moon-soft */
  --surface-inverse: var(--palette-navy-900);       /* tailwind ferry-brand-deep */

  /* text */
  --text-primary: var(--palette-ink-900);           /* index.css:23 */
  --text-secondary: var(--palette-ink-700);         /* tailwind ferry-text-secondary */
  --text-muted: var(--palette-ink-500);             /* tailwind ferry-muted */
  --text-inverse: var(--palette-white);             /* Tailwind text-white; see note below */
  --text-on-accent: #2A1608;                        /* same warm ink both surfaces use on gold:
                                                       ferry-ui.css:186 --fg-on-gold */

  /* borders — persona.css:55-56 --line/--line-soft, index.css:266 scrollbar thumb */
  --border-subtle: rgba(22, 50, 74, 0.05);
  --border-strong: rgba(30, 58, 95, 0.15);

  /* accent — persona.css:51-53 --gold/--gold-light/--gold-deep */
  --accent-primary: var(--palette-gold-400);
  --accent-primary-hover: var(--palette-gold-500);
  --accent-primary-soft: rgba(232, 168, 124, 0.16);
  --accent-on: #2A1608;

  /* feedback — DERIVED, see the L1 note. needs design sign-off */
  --feedback-danger: var(--palette-red-600);
  --feedback-success: var(--palette-emerald-500);
  --feedback-warning: var(--palette-amber-500);

  --focus-ring-color: var(--palette-navy-800);      /* index.css:253 outline #16324A */

  /* elevation — tailwind.config.js:44-48 ferry-glow-sm / ferry-card / ferry-card-hover */
  --lift-1: 0 3px 12px rgba(30, 58, 95, 0.12);
  --lift-2: 0 6px 24px rgba(30, 58, 95, 0.07);
  --lift-3: 0 14px 36px rgba(30, 58, 95, 0.12);

  /* fonts — all four roles are defined here because the companion surface owns none
     of them: React declares 9 raw font-family stacks and one weight token (F2 report
     §2). --font-decorative is letters.css:65 --ltr-hand, which becomes its alias. */
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC",
             "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
             /* index.css:17-19 body stack, byte identical */
  --font-prose: "Noto Serif SC", "Songti SC", "STSong", Georgia, serif;
             /* tailwind.config.js:40 ferry-serif, byte identical */
  --font-mono: ui-monospace, Consolas, monospace;
             /* the stack React already falls back to for var(--font-mono, ui-monospace, monospace) */
  --font-decorative: 'Ma Shan Zheng', 'ZCOOL XiaoWei', 'STKaiti', 'Kaiti SC', 'KaiTi',
                     '楷体', 'Noto Serif SC', 'Songti SC', 'SimSun', Georgia, serif;
             /* letters.css:65 --ltr-hand */
}

/* ── 3.c · NIGHT THEME（2026-10-20 新增）────────────────────────────────
 * 用户口径：「再做一个纯黑极简的，按一下就整个全站变黑」。
 *
 * 与 3.a 的故事深色（暖褐 #140C09）不同：这里是**纯黑极简**——
 * 近黑底 + 纯白字，唯一保留的彩色是香槟金强调色（它在纯黑上比什么都贵气）。
 *
 * ⚠️ 这套 token 只能管住"本来就吃语义角色"的地方。实际局面是：页面样式里
 *    var(--surface-page) 这类引用几乎为零，颜色都写在各页自己的局部变量和
 *    rgba() 里 ⇒ 光靠切换 data-theme 不会让页面自己变黑，必须给每个页面补一层
 *    深色覆盖（写法见 src/styles/home.css 末尾的 [data-theme="night"] 段）。
 *    这一块是"底色与文字的可信兜底"，不是"全站深色的全部"。
 *
 * 定义在 companion 之后 ⇒ 同特异性下后者胜，切到 night 时整体覆盖暖浅色。 */
[data-theme="night"] {
  /* surfaces：纯黑极简。raised 用低透明白而不是灰 —— 深底上灰卡片会显脏 */
  --surface-page: #0A0A0B;
  --surface-raised: rgba(255, 255, 255, 0.06);
  --surface-overlay: #141416;
  --surface-inverse: #F5F5F7;

  /* text */
  --text-primary: #F2F2F4;
  --text-secondary: rgba(255, 255, 255, 0.72);
  --text-muted: rgba(255, 255, 255, 0.46);
  --text-inverse: #0A0A0B;
  --text-on-accent: #14100C;

  /* borders */
  --border-subtle: rgba(255, 255, 255, 0.08);
  --border-strong: rgba(255, 255, 255, 0.18);

  /* accent：香槟金保留（深色底上要抬亮一档，否则会暗沉） */
  --accent-primary: #E8A87C;
  --accent-primary-hover: #F2CDB0;
  --accent-primary-soft: rgba(232, 168, 124, 0.14);
  --accent-on: #14100C;

  /* feedback */
  --feedback-danger: #E8796A;
  --feedback-success: #6ECF9B;
  --feedback-warning: #E8B45C;

  --focus-ring-color: #FFFFFF;

  /* elevation：深色下阴影要更黑更沉，浅色那套蓝灰影在纯黑上看不见 */
  --lift-1: 0 1px 2px rgba(0, 0, 0, 0.50);
  --lift-2: 0 8px 24px rgba(0, 0, 0, 0.55);
  --lift-3: 0 18px 48px rgba(0, 0, 0, 0.65);
}

/* ══════════════════════════════════════════════════════════════════════════
 * 4 · REDUCED MOTION CONTRACT
 * --------------------------------------------------------------------------
 * The tokens are the contract, so honouring prefers-reduced-motion is a property of
 * the token layer rather than of each rule. Both surfaces already have their own
 * blanket reduce block (index.css:311, ferry-ui.css:284); this adds the token form so
 * that a future consumer of --dur-* inherits the preference automatically.
 * Nothing consumes --dur-* yet, so zero rendered change this round.
 * ══════════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-micro: 0.01ms;
    --dur-fast: 0.01ms;
    --dur-base: 0.01ms;
    --dur-slow: 0.01ms;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
 * 5 · L3  COMPONENT TOKENS
 * --------------------------------------------------------------------------
 * INTENTIONALLY EMPTY in F3 - and that is the deliverable, not an omission.
 *
 * COMPONENT_TOKEN_COUNT = 0.
 *
 * A component token may be added only when all three hold:
 *   1. the component needs a value that no semantic role expresses;
 *   2. the gap has been written down as a missing role, so the next round can decide
 *      whether to promote it to the semantic layer instead;
 *   3. the token is defined in the same theme scopes as section 3, never at :root only.
 *
 * A component token that merely renames a semantic token is drift with extra steps.
 * ==========================================================================*/
