/*
 * TUVE design tokens — verbatim mirror of principles/subs/visual_language_tokens.md
 * (the canonical token declaration, split out of visual_language.md §8 on
 * 2026-06-02, COMPACT-0048).
 * Imported by React Island entries; declares :root tokens, the eight canonical
 * keyframes, the global prefers-reduced-motion override, the motion master
 * switch, and the .tuve-root reset class. Component *.module.css files only
 * reference var(--tuve-*) — never edit values here without amending §8 in
 * the SSOT first (constitution amendment per visual_language.md §9).
 */

/*
 * @font-face — Alibaba PuHuiTi 3 (the SSOT-required webfont for
 * --tuve-font-sans, visual_language §8 ¶184: "The `@font-face` rule must
 * declare its `font-family` as exactly \"Alibaba PuHuiTi\" to match the
 * token"). Loaded from the SEE2AI CDN — the same delivery channel
 * `src/services/document_extraction/preview_shell.py:109` already uses
 * for the document-preview iframe — so the React Island entries that
 * import this file (chat, public-home) all paint Landing
 * surface text in the brand sans. Two weights cover the canonical
 * Landing usage: 400 for body, 700 for emphasis. `font-display: swap`
 * keeps text painted in the fallback stack (PingFang SC → system) until
 * the webfont loads, so first-paint is never invisible. Weight values
 * align with --tuve-font-weight-regular (400) and the semibold/bold
 * roles in §8 ¶218-220.
 */
@font-face {
  font-family: "Alibaba PuHuiTi";
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  /* ?v=2026-05-28 cache-buster: 11 天前 CDN 边缘节点曾返回过无 CORS header
     的响应，部分浏览器 disk cache 持续命中该旧版导致 workbench console 报
     CORS error。CDN 服务端当前配置已正确返回 access-control-allow-origin: *
     (curl 验证)，给 URL 加 query string 强制所有浏览器以新地址请求绕开旧缓存。
     未来如换字体或换 CDN，bump 该版本号让所有用户重拉。 */
  src: url("https://cdn1.see2ai.com/web-static/fonts/alibaba-puhuiti-3/v1/alibaba-puhuiti-3-55-regular.woff2?v=2026-05-28") format("woff2");
}

@font-face {
  font-family: "Alibaba PuHuiTi";
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  src: url("https://cdn1.see2ai.com/web-static/fonts/alibaba-puhuiti-3/v1/alibaba-puhuiti-3-85-bold.woff2?v=2026-05-28") format("woff2");
}

:root {
  /* Brand — anchor + variants (all mode-universal) */
  --tuve-brand: #FF6B01;
  --tuve-brand-hover: rgba(255, 107, 1, .12);
  --tuve-brand-active: #E55F00;
  --tuve-brand-soft: rgba(255, 107, 1, .14);
  --tuve-brand-glow: rgba(255, 107, 1, .55);
  --tuve-brand-tint: #FFE9D6;

  /* Brand — ink (flips with mode) */
  --tuve-ink: #1E1E22;
  --tuve-ink-soft: #4B4B4B;

  /* Neutral — surfaces */
  --tuve-bg: #FBF9F5;
  --tuve-bg-deep: #F2EFE9;
  --tuve-surface: #FFFFFF;
  --tuve-sunken: #F4F1EB;
  --tuve-border: rgba(30, 30, 34, .08);
  --tuve-border-strong: rgba(30, 30, 34, .14);

  /* Neutral — text (--tuve-text mirrors --tuve-ink) */
  --tuve-text: var(--tuve-ink);
  --tuve-text-muted: #6F6E73;
  --tuve-text-subtle: #9F9D9E;

  /* Semantic — status only */
  --tuve-success: #1F9D57;
  --tuve-warning: #E8A317;
  --tuve-danger: #E5484D;
  --tuve-info: #2F8CFF;

  /* Typography — families */
  --tuve-font-sans: "Alibaba PuHuiTi", "PingFang SC", -apple-system, "Inter", "Microsoft YaHei", sans-serif;
  --tuve-font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --tuve-font-display: "Alibaba PuHuiTi", "PingFang SC", -apple-system, "Inter", "Microsoft YaHei", sans-serif;

  /* Typography — size scale */
  --tuve-font-size-xs: 12px;
  --tuve-font-size-sm: 13px;
  --tuve-font-size-base: 14px;
  --tuve-font-size-md: 16px;
  --tuve-font-size-lg: 18px;
  --tuve-font-size-xl: 22px;
  --tuve-font-size-2xl: 30px;
  --tuve-font-size-3xl: 44px;
  --tuve-font-size-4xl: 56px;
  --tuve-font-size-display: 96px;

  /* Typography — weights and line height */
  --tuve-font-weight-regular: 400;
  --tuve-font-weight-medium: 500;
  --tuve-font-weight-semibold: 600;
  --tuve-font-weight-bold: 700;
  --tuve-line-height-body: 1.55;
  --tuve-line-height-heading: 1.2;
  --tuve-line-height-tight: 1.05;

  /* Spacing — 4px grid */
  --tuve-space-2: 2px;
  --tuve-space-3: 3px;
  --tuve-space-4: 4px;
  --tuve-space-8: 8px;
  --tuve-space-12: 12px;
  --tuve-space-16: 16px;
  --tuve-space-20: 20px;
  --tuve-space-24: 24px;
  --tuve-space-32: 32px;
  --tuve-space-40: 40px;
  --tuve-space-48: 48px;
  --tuve-space-64: 64px;
  --tuve-space-80: 80px;
  --tuve-space-120: 120px;

  /* Control heights — the interactive-control ladder. ONE ladder for every
     control that owns a hit area: button, input, select, segmented row.

     加葱 ruled this TWICE on 2026-07-26 (both AskUserQuestion), so read BOTH
     ledger entries or you will read the wrong value:
       * `control_height_ladder_grid_aligned` took 32/40/48, the grid-aligned
         option, explicitly rejecting 32/44/48. It carries the scoped Red Line
         #6 authorization for this whole token family, which still governs.
       * `control_height_ladder_md_reverts_to_44` amends md back to 44px once
         PRD-0076 reached the table. sm and lg are untouched by the amendment.
     The values below are the amended ladder.

     The steps are deliberately UNEVEN. 44 is not a grid step; it is the tap
     floor, and the tap floor outranks a tidy progression.

     A control height is also NOT a spacing step: never spell one as
     --tuve-space-* (that spelling reads as a gap, and the next spacing edit
     silently resizes every control that borrowed it).
       sm  32px  dense toolbars / inline row actions — below the tap floor,
                 so pointer-first surfaces only
       md  44px  the default, pinned to the PRD-0076 AC3 >=44px tap floor and
                 NOT to the 4px grid. Lane 1 of that PRD already had to undo
                 this once (composer send 40->48, login ghost 40->44) — do not
                 "tidy" it back down to 40.
       lg  48px  touch standard (the 48px target /ziwei mandates) */
  --tuve-control-h-sm: 32px;
  --tuve-control-h-md: 44px;
  --tuve-control-h-lg: 48px;

  /* Radius */
  --tuve-radius-sm: 6px;
  --tuve-radius-md: 10px;
  --tuve-radius-lg: 16px;
  --tuve-radius-xl: 24px;
  --tuve-radius-full: 9999px;

  /* Elevation — shadow (mode-universal) */
  --tuve-shadow-sm: 0 1px 3px rgba(16, 16, 20, .06), 0 1px 2px rgba(16, 16, 20, .04);
  --tuve-shadow-md: 0 8px 28px rgba(16, 16, 20, .10), 0 2px 4px rgba(16, 16, 20, .04);
  --tuve-shadow-lg: 0 24px 56px rgba(16, 16, 20, .14);
  --tuve-shadow-glow: 0 0 60px var(--tuve-brand-glow);

  /* Elevation — z-index (1200 reserved for a future overlay layer) */
  --tuve-z-dropdown: 1000;
  --tuve-z-sticky: 1100;
  --tuve-z-modal: 1300;
  --tuve-z-toast: 1400;

  /* Breakpoint — literal mirror; @media uses the bare 1024px */
  --tuve-breakpoint-desktop: 1024px;

  /* Motion — durations and easing */
  --tuve-duration-fast: 160ms;
  --tuve-duration-base: 280ms;
  --tuve-duration-slow: 520ms;
  --tuve-duration-pulse: 2200ms;  /* 镜头 chip highlight 注意力脉冲（精确值 token 化，零视觉变化）*/
  --tuve-duration-thinking: 1400ms;  /* 「正在思考」三点循环一拍；与 templates/chat/styles.html 的 typing 动画同值，两面等待信号同频 */
  --tuve-ease-standard: cubic-bezier(.2, 0, 0, 1);
  --tuve-ease-soft: cubic-bezier(.4, 0, .2, 1);
  --tuve-duration-instant: 1ms;   /* reduced-motion kill-switch (prefers-reduced-motion) */

  /* Exact-value tokens minted for Red Line #18 tokenization (WI2-frontend-source-debt,
     2026-06-05). Value-identical to the literals they replace — zero visual change.
     Declared here (the canonical token sheet, not a scanned *.module.css). */
  --tuve-space-18: 18px;                  /* AccountSettings toggle/icon control box */
  --tuve-space-10: 10px;                  /* AccountSettings reaction-radio inner dot */
  --tuve-account-card-min-height: 116px;  /* AccountSettings card min-height */
  --tuve-account-grid-track-min: 180px;   /* AccountSettings auto-fill grid track min */

  /* Apps directory surface sizing (owner-authorized ruling 1, 2026-07-25).
     Both values preserve the pre-tokenization layout exactly. They are scoped
     to the surface because neither dimension is a reusable global scale step. */
  --tuve-size-apps-directory-content-max-w: 1080px;
  --tuve-size-apps-directory-grid-track-min: 200px;

  /* Admin external-source form sizing (PRD-0191). */
  --tuve-size-admin-external-source-grid-track-min: 220px;
  --tuve-size-admin-playbook-content-max-w: 1120px;
  --tuve-size-admin-playbook-copy-max-w: 760px;
  --tuve-size-admin-playbook-dialog-max-w: 580px;

  /* Chat in-conversation card contract — shared layout tokens (PRD-0103 /
     principles/subs/chat_card_contract.md §5; 加葱 2026-06-26 立项 popup).
     The single SSOT for "how wide may a chat card be", the in-card scroll
     bound that keeps a long body from pushing the action footer off-screen,
     and the safe gap that keeps a revealed card clear of the bottom composer.
     Every in-conversation card (approval / ConfirmOp / question / stitch /
     placeholder / result) references these instead of a bare px width. */
  --tuve-chat-card-max-width: 780px;                      /* decision-card width cap */
  --tuve-chat-card-scroll-max-height: min(50vh, 28rem);   /* in-card scrollable body bound */
  --tuve-chat-card-footer-safe-gap: var(--tuve-space-16); /* gap below a revealed card / footer */
  /* In-card local stacking ladder (PRD-0103 §10 follow-up debt; retires the
     media-generation placeholder STATUS card's bare z-index: 2/3/4 literals —
     values equal the pre-edit integers, ZERO visual change). The card sets
     `isolation: isolate`, so these index a card-LOCAL stacking context, not the
     page-chrome band (`--tuve-z-page-*`, sub-100 global chrome). Any
     in-conversation card with overlapping layers consumes this shared ladder. */
  --tuve-chat-card-z-frame: 2;    /* decorative inset frame / sheen above the media base */
  --tuve-chat-card-z-badge: 3;    /* status / progress badge above the frame */
  --tuve-chat-card-z-overlay: 4;  /* top layer: error scrim + meta footer above the frame */
  /* Theme palette preview — ModuleE_Appearance renders BOTH palettes at once, so these
     are intentionally theme-invariant literals (a live preview can't use theme tokens).
     PREVIEW ONLY: these name "what the light/dark palette looks like", not "what to
     paint with", and carry no semantic role. Do NOT borrow one as a colour — a swatch
     value drifting for preview reasons would silently repaint whatever borrowed it.
     Need a theme-invariant foreground? Use --tuve-text-on-brand (below). */
  --tuve-palette-light-bg: #FBF9F5;
  --tuve-palette-light-surface: #FFFFFF;
  --tuve-palette-light-ink: #1E1E22;
  --tuve-palette-dark-bg: #0E0E12;
  --tuve-palette-dark-surface: #16161B;
  --tuve-palette-dark-ink: #ECEAE4;

  /* Dark-canvas family — the media-preview lightbox canvas (加葱-authorized
     constitution amendment via AskUserQuestion popup 2026-06-10). This file is
     <link>ed raw (not bundled) by three templates, so its comments ship
     byte-for-byte to every visitor — keep internal ledger identifiers out of
     it. The preview canvas is dark in BOTH themes, so like the
     palette-* preview tokens above these are intentionally theme-invariant
     (declared once, no dark-mode override). Values equal the pre-edit rgba()
     literals in MediaModal.module.css verbatim — zero visual change.

     The canvas is effectively a small fixed theme, so it carries its own
     bg / surface / text trio mirroring the global neutrals. Those three opaque
     bases were missing until 2026-07-25: the family shipped only the alpha
     variants, so the lightbox had nothing to name its plate and its body text
     with and reached into the palette-* preview swatches above instead. Those
     are PREVIEW ONLY, so the bases now live here and the alpha variants derive
     from them instead of repeating the same RGB triplet. Chrome-verified: each
     derivation composites to pixel-identical output vs the rgba() literal it
     replaced (compare rendered pixels, not getComputedStyle strings — Chrome
     serialises color-mix() as color(srgb …), so a string compare false-alarms).
     scrim keeps its literal: its base is #07070A (dark bg-deep), which has no
     opaque member here to derive from. */
  --tuve-dark-canvas-bg: #0E0E12;                             /* opaque canvas plate — dialog / viewport / video / doc */
  --tuve-dark-canvas-surface: #16161B;                        /* opaque raised surface on the canvas — media frame */
  --tuve-dark-canvas-text: #ECEAE4;                           /* primary text on the canvas */
  --tuve-dark-canvas-scrim: rgba(7, 7, 10, .94);              /* full-canvas backdrop scrim */
  --tuve-dark-canvas-pill: color-mix(in srgb, var(--tuve-dark-canvas-surface) 72%, transparent);        /* glass pill base — title */
  --tuve-dark-canvas-pill-strong: color-mix(in srgb, var(--tuve-dark-canvas-surface) 78%, transparent); /* glass pill base — tool cluster */
  --tuve-dark-canvas-card: color-mix(in srgb, var(--tuve-dark-canvas-surface) 82%, transparent);        /* glass card base — audio player */
  --tuve-dark-canvas-card-strong: color-mix(in srgb, var(--tuve-dark-canvas-surface) 86%, transparent); /* glass card base — error card */
  --tuve-dark-canvas-stroke: rgba(255, 255, 255, .12);        /* default hairline on glass */
  --tuve-dark-canvas-stroke-strong: rgba(255, 255, 255, .14); /* emphasized hairline — tool cluster */
  --tuve-dark-canvas-stroke-control: rgba(255, 255, 255, .18); /* outlined-control border on the canvas */
  --tuve-dark-canvas-text-muted: color-mix(in srgb, var(--tuve-dark-canvas-text) 72%, transparent);     /* secondary text on the canvas */
  --tuve-dark-canvas-text-subtle: color-mix(in srgb, var(--tuve-dark-canvas-text) 36%, transparent);    /* disabled / non-essential text */
  /* Dark-stage extension (same 加葱-authorized constitution amendment via
     AskUserQuestion popup 2026-06-10): the five dark-stage
     literals that predated the family — RegenerateModalView.module.css
     (cinematic regen stage) + GenerationParamsOverlays.module.css (modal
     backdrops). Same mode-universal rationale; values equal the pre-edit
     rgba() literals verbatim — zero visual change. NOTE: these sit on
     neutral-black bases (0,0,0 / 10,10,10 / 32,32,32), not the dark-palette
     bases above; consolidating the hues onto the family bases would be a
     visible change and needs its own design decision. */
  --tuve-dark-canvas-stage: rgba(10, 10, 10, 1);              /* opaque cinematic stage plate */
  --tuve-dark-canvas-stage-shade: rgba(0, 0, 0, 0.22);        /* stage bottom-darkening gradient end */
  --tuve-dark-canvas-badge: rgba(32, 32, 32, 0.76);           /* status badge scrim on the stage */
  --tuve-dark-canvas-overlay-scrim: rgba(0, 0, 0, 0.72);      /* dialog backdrop above the canvas */
  --tuve-dark-canvas-overlay-scrim-strong: rgba(0, 0, 0, 0.82); /* nested-preview backdrop (top layer) */

  /* W31 Landing POC tokens (加葱-ratified 2026-05-24) — mirrors §8 in visual_language.md */
  --tuve-border-width-hairline: 1px;
  --tuve-border-width-thin: 2px;
  --tuve-border-width-medium: 3px;
  --tuve-z-page-body: 1;
  --tuve-z-page-footer: 2;
  --tuve-z-page-header: 3;
  --tuve-size-landing-header-height: 56px;
  --tuve-size-landing-thought-box-max: 260px;
  --tuve-size-landing-voice-block-max: 520px;
  --tuve-size-landing-compact-curve-col: 100px;
  --tuve-size-landing-compact-gain-col: 70px;
  --tuve-size-landing-featured-curve-col: 160px;
  --tuve-size-landing-detail-curve-col: 220px;

  /* PRD-0017 W33 Landing redesign tokens — 加葱-ratified 2026-05-25 popup Q5 */
  --tuve-size-landing-header-h-w33: 60px;
  --tuve-size-landing-page-max-width: 1280px;
  --tuve-size-landing-page-h-pad: 48px;
  --tuve-size-landing-column-gap: 80px;
  --tuve-size-landing-hero-h1-max: 540px;
  --tuve-size-landing-hero-h1-fs: 52px;
  --tuve-size-landing-hero-sub-max: 480px;
  --tuve-size-landing-hero-sub-fs: 17px;
  --tuve-size-landing-cta-h: 44px;
  --tuve-size-landing-cta-px: 22px;
  --tuve-size-landing-cta-fs: 15px;
  --tuve-size-landing-cta-mt: 32px;
  --tuve-size-landing-thinking-mt: 22px;
  --tuve-size-landing-thinking-fs: 13px;
  --tuve-size-landing-thinking-max: 520px;
  --tuve-size-landing-kbd-px: 8px;
  --tuve-size-landing-kbd-py: 3px;
  --tuve-size-landing-kbd-fs: 11px;
  --tuve-size-landing-kbd-radius: 5px;
  --tuve-size-landing-signin-h: 30px;
  --tuve-size-landing-signin-px: 14px;
  --tuve-size-landing-signin-fs: 13px;
  --tuve-size-landing-theme-toggle: 28px;
  --tuve-size-landing-lang-h: 26px;
  --tuve-size-landing-lang-px: 9px;
  --tuve-size-landing-lang-fs: 12px;
  --tuve-size-landing-button-radius: 8px;
  --tuve-size-landing-card-radius: 10px;
  --tuve-size-landing-row-radius: 8px;
  --tuve-size-landing-card-px: 18px;
  --tuve-size-landing-card-pt: 16px;
  --tuve-size-landing-card-pb: 14px;
  --tuve-size-landing-row-py: 10px;
  --tuve-size-landing-row-px: 14px;
  --tuve-size-landing-row-gap: 12px;
  --tuve-size-landing-projectlist-gap: 6px;
  --tuve-size-landing-studio-gap: 16px;
  --tuve-size-landing-featured-curve-w: 420px;
  --tuve-size-landing-featured-curve-h: 36px;
  --tuve-size-landing-compact-curve-w: 72px;
  --tuve-size-landing-compact-curve-h: 18px;
  --tuve-size-landing-compact-gain-min: 44px;
  --tuve-size-landing-projectthumb-feat: 36px;
  --tuve-size-landing-projectthumb-compact: 28px;
  --tuve-size-landing-h1-fs: 18px;
  --tuve-size-landing-pulse-fs: 14px;
  --tuve-size-landing-kind-fs: 15px;
  --tuve-size-landing-compact-kind-fs: 13px;
  --tuve-size-landing-gain-fs: 18px;
  --tuve-size-landing-gain-pct-fs: 13px;
  --tuve-size-landing-gain-label-fs: 10px;
  --tuve-size-landing-kicker-fs: 11px;
  --tuve-size-landing-footer-py: 18px;
  --tuve-size-landing-footer-pb: 22px;
  --tuve-size-landing-footer-fs: 12px;
  --tuve-size-landing-focus-outline: 2px;
  /* Foreground pair for the OPAQUE brand fills — pick by ROLE, not by which one
     looks "on brand". Text / readable glyph on an opaque brand fill takes
     --tuve-text-on-brand (AA: 5.82:1 on --tuve-brand, 4.72:1 on -active,
     14.14:1 on -tint); white is only for shapes where the white IS the shape
     (switch knobs, play triangles, sheens) — as text it fails AA on every
     opaque brand fill (2.86 / 3.52 / 1.17:1). Neither flips with theme: the
     fills are theme-invariant, so the foreground must be too. --tuve-brand-soft
     is alpha, composites over the themed bg, and takes the ordinary
     --tuve-text. Full rationale: visual_language_tokens.md. */
  --tuve-text-on-brand: #1E1E22;
  --tuve-color-on-brand: #FFFFFF;

  /* Landing Hero polish 2026-05-27 — token de-debt sweep (加葱-ratified
     2026-05-27T13:38Z popup "§8 授权" Recommended). Mirrors the §8 block
     in principles/subs/visual_language.md verbatim; values match the
     pre-edit bare px literals in Hero.module.css (zero visual change). */
  --tuve-size-landing-hero-max-w: 980px;
  --tuve-size-landing-hero-h1-max-w: 880px;
  --tuve-size-landing-hero-h1-fs-desktop: 80px;
  --tuve-size-landing-hero-h1-fs-tablet: 56px;
  --tuve-size-landing-hero-h1-fs-mobile: 40px;
  --tuve-size-landing-hero-sub-max-w: 560px;
  --tuve-size-landing-hero-cta-row-mt: 36px;
  --tuve-size-landing-hero-cta-h: 50px;
  --tuve-size-landing-hero-cta-h-mobile: 46px;
  --tuve-size-landing-hero-cta-px: 26px;
  --tuve-size-landing-hero-cta-arrow-fs: 17px;
  --tuve-size-landing-hero-cta-secondary-mt: 22px;

  /* Landing ClosingCta polish 2026-05-27 — same authorization. */
  --tuve-size-landing-closing-section-max-w: 980px;
  --tuve-size-landing-closing-title-fs-min: 22px;
  --tuve-size-landing-closing-title-fs-max: 48px;
  --tuve-size-landing-closing-row-mt: 36px;
  --tuve-size-landing-closing-cta-h: 50px;
  --tuve-size-landing-closing-cta-h-mobile: 46px;
  --tuve-size-landing-closing-cta-px: 26px;
  --tuve-size-landing-closing-cta-arrow-fs: 17px;
  --tuve-size-landing-closing-proof-max-w: 560px;

  /* Companion to --tuve-size-landing-focus-outline above. */
  --tuve-size-landing-focus-offset: 2px;

  /* PRD-0050 Landing token-debt sweep 2026-06-02 (加葱-ratified popup) — clears
     the ~91 raw px / z-index literals across the 8 Landing *.module.css files.
     Every value below equals the exact pre-edit literal: ZERO visual change,
     proven by a resolve-and-compare pass over each file. The off-grid
     --tuve-size-landing-space-N values are deliberate 4px-grid exceptions the
     Landing redesign uses; the *-fs values preserve half-pixel sizes verbatim.
     Documentation mirror: principles/subs/visual_language_tokens.md §8 (the
     canonical declaration split out of visual_language.md §8 on 2026-06-02,
     COMPACT-0048; the §8↔css mirror was briefly out of sync while that file sat
     at its 910-line cap and could not grow, now restored). tuve-tokens.css
     remains the runtime
     SSOT the resolution guard (test_tuve_tokens_css_module_resolution_guard)
     checks against, so the Landing modules resolve correctly today. */
  --tuve-size-landing-space-3: 3px;
  --tuve-size-landing-space-6: 6px;
  --tuve-size-landing-space-10: 10px;
  --tuve-size-landing-space-14: 14px;
  --tuve-size-landing-space-18: 18px;
  --tuve-size-landing-space-22: 22px;
  --tuve-size-landing-space-28: 28px;
  --tuve-size-landing-space-46: 46px;
  --tuve-size-landing-space-56: 56px;
  --tuve-size-landing-content-max-w: 1180px;
  --tuve-size-landing-proof-max-w: 1100px;
  --tuve-size-landing-howto-title-max-w: 760px;
  --tuve-size-landing-howto-sub-max-w: 640px;
  --tuve-size-landing-header-h: 64px;
  --tuve-size-landing-signin-w: 104px;
  --tuve-size-landing-bubble-w: 280px;
  --tuve-size-landing-bubble-tail: 7px;
  --tuve-size-landing-gain-min-w: 52px;
  --tuve-size-landing-closing-pb: 140px;
  --tuve-size-landing-proof-list-radius: 12px;
  --tuve-size-landing-section-h-fs: 40px;
  --tuve-size-landing-section-h-fs-sm: 32px;
  --tuve-size-landing-section-h-fs-xs: 26px;
  --tuve-size-landing-body-md-fs: 14.5px;
  --tuve-size-landing-body-sm-fs: 12.5px;
  --tuve-size-landing-label-xs-fs: 10.5px;
  --tuve-z-landing-bubble: 5;

  /*
   * Legacy HSL channel bridge for pre-React workbench surfaces.
   *
   * Canonical tokens remain the --tuve-* values above. These unprefixed
   * aliases exist only because the still-vanilla chat/workspace sidecars
   * consume shadcn-era `hsl(var(--...))` channels.
   *
   * These channels are an APPROXIMATION of the --tuve-* palette, not an
   * equivalent of it, and must not be treated as a safe substitute. Measured
   * 2026-07-25 by two independent methods that agreed channel-for-channel
   * (browser computed style of `hsl(var(--alias))` vs `var(--tuve-anchor)` in
   * both themes, and CSS Color 4 hsl() -> sRGB arithmetic) across the 32
   * alias/anchor pairs below:
   *
   *   - Only 7 pairs render an identical sRGB triple.
   *   - 21 land within +/-1 per channel of some canonical token, because a
   *     hex anchor round-tripped through integer HSL channels does not come
   *     back: hsl(var(--brand-orange)) paints rgb(255, 106, 0) where
   *     --tuve-brand paints rgb(255, 107, 1).
   *   - 11 are >= 2 away from EVERY opaque --tuve-* token in their own mode —
   *     the worst by 14/255 on one channel, and light --accent sits 21/255 off
   *     the brand tint it reads as. Rounding does not explain those; they were
   *     authored independently and never matched the palette.
   *   - --border / --input are opaque here while --tuve-border carries alpha,
   *     so those two cannot agree over any backdrop, at any precision.
   *
   * Consequence, and the reason this block is debt rather than plumbing: while
   * a surface still reads these channels, one brand orange can paint two
   * different pixels on one screen. That is the Red Line #18 consistency case
   * for retiring the bridge, not for extending it. Do not add consumers; new
   * rules reference var(--tuve-*) directly.
   *
   * Retirement is approved but not yet numbered. These aliases were originally
   * scoped to retire with the PRD-0018 vanilla->React program, but that program
   * closed 2026-05-27 with all children archived and the bridge outlived it,
   * leaving the debt unowned; five stylesheets under static/css/ still read
   * these channels. A migration PRD is authorized and pending a number ruling.
   * Doing that migration is scoped, incremental PRD work per
   * visual_language.md §9, never a side effect of an unrelated change.
   */
  --brand-orange: 25 100% 50%;
  --background: 40 43% 97%;
  --foreground: 240 6% 13%;
  --card: 0 0% 100%;
  --card-foreground: 240 6% 13%;
  --popover: 0 0% 100%;
  --popover-foreground: 240 6% 13%;
  --muted: 40 31% 94%;
  --muted-foreground: 240 2% 44%;
  --accent: 25 100% 96%;
  --accent-foreground: 240 6% 13%;
  --border: 240 6% 90%;
  --input: 240 6% 90%;
  --ring: 25 100% 50%;
  --success: 145 67% 37%;
  --warning: 40 82% 50%;
  --destructive: 358 77% 58%;
}

:root[data-theme="dark"] {
  --tuve-ink: #ECEAE4;
  --tuve-ink-soft: #9C9A95;

  --tuve-bg: #0E0E12;
  --tuve-bg-deep: #07070A;
  --tuve-surface: #16161B;
  --tuve-sunken: #0A0A0E;
  --tuve-border: rgba(255, 255, 255, .06);
  --tuve-border-strong: rgba(255, 255, 255, .12);

  --tuve-text-muted: #9C9A95;
  --tuve-text-subtle: #5D5C58;

  --tuve-success: #3DBE7C;
  --tuve-warning: #EFB949;
  --tuve-danger: #F0585D;
}

:root[data-theme="dark"] {
  --background: 240 13% 6%;
  --foreground: 40 19% 91%;
  --card: 240 10% 10%;
  --card-foreground: 40 19% 91%;
  --popover: 240 10% 10%;
  --popover-foreground: 40 19% 91%;
  --muted: 240 17% 5%;
  --muted-foreground: 40 4% 60%;
  --accent: 240 10% 14%;
  --accent-foreground: 40 19% 91%;
  --border: 240 4% 18%;
  --input: 240 4% 18%;
  --success: 145 53% 49%;
  --warning: 40 84% 61%;
  --destructive: 358 84% 64%;
}

:root[data-density="cozy"] {
  --tuve-font-size-xs: 13px;
  --tuve-font-size-sm: 14px;
  --tuve-font-size-base: 15px;
  --tuve-font-size-md: 17px;
  --tuve-font-size-lg: 19px;
  --tuve-font-size-xl: 23px;
  --tuve-font-size-2xl: 31px;
  --tuve-font-size-3xl: 45px;
  --tuve-font-size-4xl: 57px;
  --tuve-font-size-display: 97px;
}
:root[data-density="dense"] {
  --tuve-font-size-xs: 11px;
  --tuve-font-size-sm: 12px;
  --tuve-font-size-base: 13px;
  --tuve-font-size-md: 15px;
  --tuve-font-size-lg: 17px;
  --tuve-font-size-xl: 21px;
  --tuve-font-size-2xl: 29px;
  --tuve-font-size-3xl: 43px;
  --tuve-font-size-4xl: 55px;
  --tuve-font-size-display: 95px;
}

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

:root[data-motion="off"] *,
:root[data-motion="off"] *::before,
:root[data-motion="off"] *::after {
  animation-play-state: paused !important;
}

@keyframes tuve-breathe {
  0%, 100% { transform: scale(1); opacity: .9; }
  50%      { transform: scale(1.06); opacity: 1; }
}
@keyframes tuve-pulse-soft {
  0%, 100% { opacity: .55; }
  50%      { opacity: 1; }
}
@keyframes tuve-drift {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-6px); }
}
@keyframes tuve-caret {
  0%, 50%   { opacity: 1; }
  51%, 100% { opacity: 0; }
}
.tuve-blinking-caret {
  animation: tuve-caret 1s steps(1) infinite;
}
@keyframes tuve-glow-sweep {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(200%); }
}
@keyframes tuve-fade-up {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes tuve-aurora-a {
  0%, 100% { transform: translate(0, 0) scale(1);    opacity: .85; }
  33%      { transform: translate(60px, 30px) scale(1.08); opacity: 1; }
  66%      { transform: translate(-30px, 50px) scale(.95);  opacity: .75; }
}
@keyframes tuve-aurora-b {
  0%, 100% { transform: translate(0, 0) scale(1.05); opacity: .7; }
  40%      { transform: translate(-50px, -40px) scale(1.15); opacity: .95; }
  70%      { transform: translate(40px, -20px) scale(.95);   opacity: .65; }
}

/* Page-level surface — let <html> + <body> consume --tuve-bg / --tuve-text so
 * dark mode covers the full viewport (not just whichever inner wrapper sets
 * its own background). Without this, body's content height < viewport leaks
 * the browser default white from the bare <html> root → visible "white edge".
 * Same rule is duplicated as inline hardcoded fallback in base.html so the
 * first paint (before this stylesheet loads) is already dark when needed.
 * SSOT: principles/subs/visual_language_tokens.md §8 (token value definitions). */
html, body {
  background: var(--tuve-bg);
  color: var(--tuve-text);
}

/* TUVE scrollbar skin — quiet by default, visible enough when a dense surface
 * needs native scroll. Uses existing tokens only; no new token amendment. */
:where(html, body, *) {
  scrollbar-color:
    color-mix(in srgb, var(--tuve-text-subtle) 58%, transparent)
    color-mix(in srgb, var(--tuve-sunken) 82%, transparent);
  scrollbar-width: thin;
}

:where(*)::-webkit-scrollbar {
  width: var(--tuve-space-10);
  height: var(--tuve-space-10);
}

:where(*)::-webkit-scrollbar-track {
  background-color: color-mix(in srgb, var(--tuve-sunken) 82%, transparent);
  border-radius: var(--tuve-radius-full);
}

:where(*)::-webkit-scrollbar-thumb {
  background-color: color-mix(in srgb, var(--tuve-text-subtle) 58%, transparent);
  background-clip: content-box;
  border: var(--tuve-space-2) solid transparent;
  border-radius: var(--tuve-radius-full);
}

:where(*)::-webkit-scrollbar-thumb:hover {
  background-color: color-mix(in srgb, var(--tuve-brand) 72%, var(--tuve-text-subtle));
}

:where(*)::-webkit-scrollbar-corner {
  background-color: transparent;
}

.tuve-scrollbar-stable {
  scrollbar-gutter: stable;
}

.tuve-scrollbar-stable-both {
  scrollbar-gutter: stable both-edges;
}

.tuve-root {
  font-family: var(--tuve-font-sans);
  font-size: var(--tuve-font-size-base);
  color: var(--tuve-text);
  background: var(--tuve-bg);
  line-height: var(--tuve-line-height-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
.tuve-root *, .tuve-root *::before, .tuve-root *::after { box-sizing: border-box; }
/* Resets wrapped in :where() so they sit at specificity (0,0,1) and never
 * out-rank module classes. Without :where(), `.tuve-root button` was
 * (0,1,1) and silently flattened any module's button styling (lang pill
 * active state, avatar ring, hamburger hover) to transparent + ink. */
:where(.tuve-root) button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
:where(.tuve-root) a { color: inherit; text-decoration: none; }
.tuve-root h1, .tuve-root h2, .tuve-root h3, .tuve-root h4 {
  margin: 0;
  font-weight: var(--tuve-font-weight-semibold);
  line-height: var(--tuve-line-height-heading);
  letter-spacing: -0.01em;
}
.tuve-root p { margin: 0; }

/* Accessibility — respect user's reduced-motion preference globally.
 * Animations and transitions collapse to ~0ms so vestibular-sensitive
 * users do not get vertigo from pulse / sweep / parallax. Functional
 * state changes still apply; only the motion duration disappears. */
@media (prefers-reduced-motion: reduce) {
  .tuve-root *,
  .tuve-root *::before,
  .tuve-root *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
