diff --git a/docs/governance/architecture-inventory.json b/docs/governance/architecture-inventory.json
index a8cc486..2d5102d 100644
--- a/docs/governance/architecture-inventory.json
+++ b/docs/governance/architecture-inventory.json
@@ -244,6 +244,7 @@
}
],
"css_layers": [
+ "/shared/tokens.css?v=20260729-1",
"/styles.css",
"/renovation.css?v=20260725-5",
"/redesign-v2.css?v=20260728-1",
@@ -259,23 +260,23 @@
},
{
"path": "static/styles.css",
- "bytes": 364385,
- "lines": 15549
- },
- {
- "path": "static/redesign-v2.css",
- "bytes": 264960,
- "lines": 8616
+ "bytes": 361780,
+ "lines": 15465
},
{
"path": "server.py",
"bytes": 263744,
"lines": 5856
},
+ {
+ "path": "static/redesign-v2.css",
+ "bytes": 263539,
+ "lines": 8570
+ },
{
"path": "static/index.html",
- "bytes": 133815,
- "lines": 1875
+ "bytes": 133880,
+ "lines": 1876
},
{
"path": "database.py",
@@ -294,8 +295,8 @@
},
{
"path": "static/renovation.css",
- "bytes": 86245,
- "lines": 1636
+ "bytes": 83949,
+ "lines": 1553
},
{
"path": "static/wentian-v2.css",
@@ -309,8 +310,8 @@
},
{
"path": "static/theme.css",
- "bytes": 41279,
- "lines": 1407
+ "bytes": 36427,
+ "lines": 1253
}
]
}
diff --git a/docs/governance/stage-16-css-tokens.md b/docs/governance/stage-16-css-tokens.md
new file mode 100644
index 0000000..099ef9b
--- /dev/null
+++ b/docs/governance/stage-16-css-tokens.md
@@ -0,0 +1,49 @@
+# Stage 16: CSS Tokens and Cascade Governance
+
+## Scope
+
+This stage changes CSS ownership, not visual design. Existing selectors, page geometry,
+responsive behavior, theme behavior, and the isolated Wentian visual layer remain intact.
+
+## Canonical Token Layer
+
+`static/shared/tokens.css` is the only owner of global application tokens. It is loaded before
+all application styles and follows a three-layer contract:
+
+1. Primitive tokens contain raw palette, dimension, elevation, and motion values.
+2. Semantic tokens describe interface meaning such as canvas, surface, border, text, action,
+ market direction, and warning states.
+3. Component tokens define shared card, control, shell, table, chart, and profile contracts.
+
+Dark mode overrides semantic and compatibility values in the same file. It does not redefine
+component geometry. The `--wt-*` namespace remains owned by `wentian-v2.css` because Wentian
+has a deliberately isolated visual language.
+
+## Compatibility
+
+The historical `--xb-*`, `--r2-*`, and flat variables such as `--blue`, `--line`, and `--up`
+remain compatibility aliases. Their effective light and dark values are unchanged. New CSS
+must use semantic or component tokens; compatibility names exist only so feature selectors can
+be migrated incrementally without a broad visual rewrite.
+
+The stylesheet order is fixed as:
+
+1. shared tokens;
+2. legacy/base styles;
+3. renovation and page redesign rules;
+4. canonical non-Wentian design-system rules;
+5. runtime theme selectors;
+6. isolated Wentian rules.
+
+## Enforcement
+
+`tests/test_css_governance.py` verifies the load order, the three token layers, single global
+ownership, compatibility coverage, and Wentian isolation. A new global `:root` token block in
+any legacy stylesheet fails the regression suite.
+
+## Residual Migration
+
+Page styles still contain literal values and historical variable references. Removing those is
+an incremental page-by-page task because blindly replacing them would risk changing already
+accepted visuals. Stage 16 establishes the ownership boundary that prevents new drift while
+leaving safe migration points for later stages.
diff --git a/static/design-system.css b/static/design-system.css
index 2770c65..dcc452c 100644
--- a/static/design-system.css
+++ b/static/design-system.css
@@ -1,48 +1,5 @@
/* Canonical non-heaven design system. Question-to-Heaven remains isolated. */
/* ========== 小白复盘 · 打样设计系统 ========== */
-:root{
- --blue:#2563eb; --blue-d:#1d4ed8; --blue-soft:#eff4ff; --blue-line:#c7d8fb;
- --up:#e04536; --up-soft:#fdecea; --down:#16a34a; --down-soft:#e9f7ee;
- --amber:#b45309; --amber-soft:#fdf3e3;
- --ink:#1f2937; --sub:#6b7280; --faint:#9ca3af;
- --line:#e5e7eb; --line-soft:#eef0f3; --bg:#f4f5f7; --card:#fff;
- --radius:10px;
- --shadow:0 1px 2px rgba(16,24,40,.05);
- /* Shell dimensions are tokens because data-heavy pages reuse them in viewport calculations. */
- --sidebar-width:200px;
- --topbar-height:46px;
- --summary-height:34px;
- --statusbar-height:30px;
- --page-pad-y:14px;
- --page-pad-x:16px;
- --card-gap:12px;
- --workspace-height:calc(100vh - var(--topbar-height) - var(--statusbar-height));
- --content-height:calc(var(--workspace-height) - var(--summary-height));
- --table-wide:1180px;
- --table-medium:930px;
- --table-compact:720px;
- --col-rank:44px;
- --col-date:94px;
- --col-stock:160px;
- --col-number:96px;
- --col-action:96px;
- --col-text:220px;
- --right-rail-wide:372px;
- --pool-table-max-height:calc(var(--content-height) - var(--topbar-height) - var(--page-pad-y) - var(--page-pad-y) - var(--card-gap));
- --sentiment-history-max-height:510px;
- --sentiment-history-min-height:220px;
- --primary-share:1.45fr;
- --secondary-share:.75fr;
- --mobile-nav-height:58px;
- --mobile-header-height:50px;
- --mobile-tab-height:54px;
- --mobile-shell-pad:8px;
- --mobile-page-pad:10px;
- --mobile-min-width:320px;
- --space-4:4px;
- --font-aux:10.5px;
-}
-
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
diff --git a/static/index.html b/static/index.html
index ecfdf8c..cdcea87 100644
--- a/static/index.html
+++ b/static/index.html
@@ -17,6 +17,7 @@
document.documentElement.style.colorScheme = theme;
})();
+
diff --git a/static/redesign-v2.css b/static/redesign-v2.css
index b4b7bac..1460dc6 100644
--- a/static/redesign-v2.css
+++ b/static/redesign-v2.css
@@ -3,52 +3,6 @@
* Values and composition are transferred from the approved prototypes in
* ../界面优化. Keep later page migrations in this file until stage 19 cleanup.
*/
-:root {
- --r2-blue: #2563eb;
- --r2-blue-dark: #1d4ed8;
- --r2-blue-soft: #eff4ff;
- --r2-blue-line: #c7d8fb;
- --r2-up: #e04536;
- --r2-up-soft: #fdecea;
- --r2-down: #16a34a;
- --r2-down-soft: #e9f7ee;
- --r2-amber: #b45309;
- --r2-amber-soft: #fdf3e3;
- --r2-ink: #1f2937;
- --r2-sub: #6b7280;
- --r2-faint: #9ca3af;
- --r2-line: #e5e7eb;
- --r2-line-soft: #eef0f3;
- --r2-bg: #f4f5f7;
- --r2-card: #fff;
- --r2-radius: 10px;
- --r2-shadow: 0 1px 2px rgba(16, 24, 40, .05);
- /* Dragon profile component tokens. */
- --dragon-profile-list-width: 340px;
- --dragon-profile-detail-min-height: 460px;
- --dragon-profile-list-max-height: 320px;
- --dragon-profile-row-min-height: 64px;
- --dragon-profile-row-avatar-size: 36px;
- --dragon-profile-avatar-size: 72px;
- --dragon-profile-control-height: 33px;
- --dragon-profile-gap: 12px;
- --dragon-profile-panel-padding: 16px;
- --dragon-profile-row-padding: 10px 12px;
- --dragon-profile-title-font: 18px;
- --dragon-profile-name-font: 13px;
- --dragon-profile-body-font: 12px;
- --dragon-profile-meta-font: 11px;
- --dragon-profile-transition: 160ms ease;
- --dragon-profile-border-width: 1px;
- --dragon-profile-focus-width: 2px;
- --dragon-profile-focus-offset: -2px;
- --dragon-profile-radius-inset: 2px;
- --dragon-profile-body-line-height: 1.75;
- --dragon-profile-icon-stroke: 1;
- --dragon-profile-weight-strong: 750;
- --dragon-profile-weight-semibold: 600;
-}
-
html,
body {
min-height: 100%;
diff --git a/static/renovation.css b/static/renovation.css
index 4df2cd3..f6b23b0 100644
--- a/static/renovation.css
+++ b/static/renovation.css
@@ -2,60 +2,6 @@
Structural layer built on semantic tokens. Page-specific character (notably Wentian)
remains owned by the underlying feature styles. */
-:root {
- /* primitive */
- --xb-gray-25: #fcfcfd;
- --xb-gray-50: #f8f9fb;
- --xb-gray-100: #f2f4f7;
- --xb-gray-200: #e5e8ee;
- --xb-gray-300: #d4d9e2;
- --xb-gray-500: #697386;
- --xb-gray-700: #344054;
- --xb-gray-900: #172033;
- --xb-blue-50: #eef4ff;
- --xb-blue-100: #dce8ff;
- --xb-blue-500: #356ae6;
- --xb-blue-600: #2858c7;
- --xb-red-50: #fff1f0;
- --xb-red-500: #df493b;
- --xb-green-50: #ecf9f1;
- --xb-green-500: #179b55;
- --xb-amber-50: #fff7e8;
- --xb-amber-500: #d7820b;
-
- /* semantic */
- --surface-canvas: var(--xb-gray-100);
- --surface-raised: #ffffff;
- --surface-subtle: var(--xb-gray-50);
- --surface-selected: var(--xb-blue-50);
- --text-primary: var(--xb-gray-900);
- --text-secondary: var(--xb-gray-500);
- --text-tertiary: #98a1b2;
- --border: var(--xb-gray-200);
- --border-strong: var(--xb-gray-300);
- --primary: var(--xb-blue-500);
- --primary-hover: var(--xb-blue-600);
- --danger: var(--xb-red-500);
- --success: var(--xb-green-500);
- --warning: var(--xb-amber-500);
- --radius-sm: 5px;
- --radius-md: 8px;
- --radius-lg: 10px;
- --shadow-xs: 0 1px 2px rgba(16, 24, 40, .04);
- --shadow-sm: 0 3px 10px rgba(16, 24, 40, .055);
- --shadow-float: 0 14px 38px rgba(16, 24, 40, .14);
- --duration-fast: 150ms;
- --duration-normal: 220ms;
-
- /* component */
- --card-bg: var(--surface-raised);
- --card-border: var(--border);
- --card-radius: var(--radius-lg);
- --card-shadow: var(--shadow-xs);
- --control-height: 34px;
- --page-gap: 12px;
-}
-
html { background: var(--surface-canvas); }
body {
@@ -870,35 +816,6 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
.curated-strategy-card { animation: none !important; transition: none !important; }
}
-/* Visual master alignment: based on the approved desktop prototypes. */
-:root {
- --xb-blue-500: #2563eb;
- --xb-blue-600: #1d4ed8;
- --xb-red-500: #e04536;
- --xb-green-500: #16a34a;
- --xb-amber-500: #b45309;
- --surface-canvas: #f4f5f7;
- --surface-subtle: #f8fafc;
- --text-primary: #1f2937;
- --text-secondary: #6b7280;
- --text-tertiary: #9ca3af;
- --border: #e5e7eb;
- --border-strong: #d1d5db;
- --primary: var(--xb-blue-500);
- --primary-hover: var(--xb-blue-600);
- --danger: var(--xb-red-500);
- --success: var(--xb-green-500);
- --warning: var(--xb-amber-500);
- --radius-sm: 5px;
- --radius-md: 7px;
- --radius-lg: 10px;
- --shadow-xs: 0 1px 2px rgba(16, 24, 40, .05);
- --shadow-sm: 0 4px 14px rgba(16, 24, 40, .06);
- --card-radius: 10px;
- --card-shadow: var(--shadow-xs);
- --control-height: 32px;
-}
-
body {
grid-template-columns: 200px minmax(0, 1fr);
grid-template-rows: 46px minmax(0, 1fr) 30px;
diff --git a/static/shared/tokens.css b/static/shared/tokens.css
new file mode 100644
index 0000000..a600fea
--- /dev/null
+++ b/static/shared/tokens.css
@@ -0,0 +1,402 @@
+/*
+ * Canonical frontend tokens.
+ *
+ * Ownership flows in one direction:
+ * primitive values -> semantic meaning -> component contracts.
+ * Historical variable names remain aliases so page CSS can migrate without
+ * changing the rendered interface.
+ */
+
+:root {
+ color-scheme: light;
+
+ /* Primitive tokens */
+ --color-white: #ffffff;
+ --color-gray-25: #fcfcfd;
+ --color-gray-50: #f8f9fb;
+ --color-gray-100: #f2f4f7;
+ --color-gray-200: #e5e8ee;
+ --color-gray-300: #d4d9e2;
+ --color-gray-500: #697386;
+ --color-gray-700: #344054;
+ --color-gray-900: #172033;
+ --color-shell-canvas: #f1f4f6;
+ --color-page-canvas: #f4f5f7;
+ --color-surface-muted: #f6f8fa;
+ --color-surface-subtle: #f8fafc;
+ --color-border: #e5e7eb;
+ --color-border-strong: #d1d5db;
+ --color-text-primary: #1f2937;
+ --color-text-secondary: #6b7280;
+ --color-text-tertiary: #9ca3af;
+ --color-action-base: #1769c2;
+ --color-action-base-hover: #10569f;
+ --color-action-base-soft: #eaf2fb;
+ --color-action: #2563eb;
+ --color-action-hover: #1d4ed8;
+ --color-action-soft: #eff4ff;
+ --color-action-line: #c7d8fb;
+ --color-market-up-base: #d33f49;
+ --color-market-up-base-soft: #fff0f1;
+ --color-market-up: #e04536;
+ --color-market-up-soft: #fdecea;
+ --color-market-down-base: #07805b;
+ --color-market-down-base-soft: #eaf7f2;
+ --color-market-down: #16a34a;
+ --color-market-down-soft: #e9f7ee;
+ --color-warning-base: #aa6800;
+ --color-warning-base-soft: #fff6e5;
+ --color-warning: #b45309;
+ --color-warning-soft: #fdf3e3;
+
+ --size-radius-sm: 5px;
+ --size-radius-md: 7px;
+ --size-radius-lg: 10px;
+ --size-control: 32px;
+ --size-sidebar: 200px;
+ --size-topbar: 46px;
+ --size-summary: 34px;
+ --size-statusbar: 30px;
+ --size-page-pad-y: 14px;
+ --size-page-pad-x: 16px;
+ --size-card-gap: 12px;
+
+ --elevation-card: 0 1px 2px rgba(16, 24, 40, .05);
+ --elevation-soft: 0 1px 2px rgba(22, 34, 46, .04), 0 5px 18px rgba(22, 34, 46, .035);
+ --elevation-raised: 0 4px 14px rgba(16, 24, 40, .06);
+ --elevation-float: 0 14px 38px rgba(16, 24, 40, .14);
+ --motion-instant: 100ms;
+ --motion-fast: 140ms;
+ --motion-medium: 200ms;
+ --motion-deliberate: 260ms;
+ --motion-slow: 560ms;
+ --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
+
+ /* Semantic tokens */
+ --canvas: var(--color-shell-canvas);
+ --surface: var(--color-white);
+ --surface-muted: var(--color-surface-muted);
+ --surface-subtle: var(--color-surface-subtle);
+ --surface-canvas: var(--color-page-canvas);
+ --surface-raised: var(--color-white);
+ --surface-selected: #eef4ff;
+ --border: var(--color-border);
+ --border-strong: var(--color-border-strong);
+ --text-primary: var(--color-text-primary);
+ --text-secondary: var(--color-text-secondary);
+ --text-tertiary: var(--color-text-tertiary);
+ --action: var(--color-action-base);
+ --action-hover: var(--color-action-base-hover);
+ --action-soft: var(--color-action-base-soft);
+ --market-up: var(--color-market-up-base);
+ --market-up-soft: var(--color-market-up-base-soft);
+ --market-down: var(--color-market-down-base);
+ --market-down-soft: var(--color-market-down-base-soft);
+ --warning-color: var(--color-warning-base);
+ --warning-soft: var(--color-warning-base-soft);
+ --primary: var(--color-action);
+ --primary-hover: var(--color-action-hover);
+ --danger: var(--color-market-up);
+ --success: var(--color-market-down);
+ --warning: var(--color-warning);
+
+ /* Component tokens */
+ --card-bg: var(--surface-raised);
+ --card-border: var(--border);
+ --card-radius: var(--size-radius-lg);
+ --card-shadow: var(--elevation-card);
+ --control-height: var(--size-control);
+ --page-gap: var(--size-card-gap);
+ --radius-sm: var(--size-radius-sm);
+ --radius-md: var(--size-radius-md);
+ --radius-lg: var(--size-radius-lg);
+ --shadow-xs: var(--elevation-card);
+ --shadow-sm: var(--elevation-raised);
+ --shadow-float: var(--elevation-float);
+ --duration-fast: 150ms;
+ --duration-normal: 220ms;
+
+ --sidebar-width: var(--size-sidebar);
+ --topbar-height: var(--size-topbar);
+ --summary-height: var(--size-summary);
+ --statusbar-height: var(--size-statusbar);
+ --page-pad-y: var(--size-page-pad-y);
+ --page-pad-x: var(--size-page-pad-x);
+ --card-gap: var(--size-card-gap);
+ --workspace-height: calc(100vh - var(--topbar-height) - var(--statusbar-height));
+ --content-height: calc(var(--workspace-height) - var(--summary-height));
+ --table-wide: 1180px;
+ --table-medium: 930px;
+ --table-compact: 720px;
+ --col-rank: 44px;
+ --col-date: 94px;
+ --col-stock: 160px;
+ --col-number: 96px;
+ --col-action: 96px;
+ --col-text: 220px;
+ --right-rail-wide: 372px;
+ --pool-table-max-height: calc(var(--content-height) - var(--topbar-height) - var(--page-pad-y) - var(--page-pad-y) - var(--card-gap));
+ --sentiment-history-max-height: 510px;
+ --sentiment-history-min-height: 220px;
+ --primary-share: 1.45fr;
+ --secondary-share: .75fr;
+ --mobile-nav-height: 58px;
+ --mobile-header-height: 50px;
+ --mobile-tab-height: 54px;
+ --mobile-shell-pad: 8px;
+ --mobile-page-pad: 10px;
+ --mobile-min-width: 320px;
+ --space-4: 4px;
+ --font-aux: 10.5px;
+
+ --dragon-profile-list-width: 340px;
+ --dragon-profile-detail-min-height: 460px;
+ --dragon-profile-list-max-height: 320px;
+ --dragon-profile-row-min-height: 64px;
+ --dragon-profile-row-avatar-size: 36px;
+ --dragon-profile-avatar-size: 72px;
+ --dragon-profile-control-height: 33px;
+ --dragon-profile-gap: 12px;
+ --dragon-profile-panel-padding: 16px;
+ --dragon-profile-row-padding: 10px 12px;
+ --dragon-profile-title-font: 18px;
+ --dragon-profile-name-font: 13px;
+ --dragon-profile-body-font: 12px;
+ --dragon-profile-meta-font: 11px;
+ --dragon-profile-transition: 160ms ease;
+ --dragon-profile-border-width: 1px;
+ --dragon-profile-focus-width: 2px;
+ --dragon-profile-focus-offset: -2px;
+ --dragon-profile-radius-inset: 2px;
+ --dragon-profile-body-line-height: 1.75;
+ --dragon-profile-icon-stroke: 1;
+ --dragon-profile-weight-strong: 750;
+ --dragon-profile-weight-semibold: 600;
+
+ --chart-background: #fbfcfd;
+ --chart-grid: #e2e8ec;
+ --chart-axis: #6c7983;
+ --chart-zero: #aeb7c1;
+ --chart-line: #1d65c1;
+ --chart-average: #b7791f;
+ --chart-up: #c93f45;
+ --chart-down: #087a55;
+ --chart-up-volume: rgba(201, 63, 69, .58);
+ --chart-down-volume: rgba(8, 122, 85, .58);
+ --chart-area: rgba(37, 99, 235, .07);
+ --chart-alert-area: rgba(224, 69, 54, .05);
+ --chart-moving-average: #d1d5db;
+ --chart-repair: #f59e0b;
+ --chart-ma-10: #a76500;
+ --chart-ma-20: #626c78;
+
+ /* Compatibility aliases. Do not add new usage of these names. */
+ --xb-gray-25: var(--color-gray-25);
+ --xb-gray-50: var(--color-gray-50);
+ --xb-gray-100: var(--color-gray-100);
+ --xb-gray-200: var(--color-gray-200);
+ --xb-gray-300: var(--color-gray-300);
+ --xb-gray-500: var(--color-gray-500);
+ --xb-gray-700: var(--color-gray-700);
+ --xb-gray-900: var(--color-gray-900);
+ --xb-blue-50: #eef4ff;
+ --xb-blue-100: #dce8ff;
+ --xb-blue-500: var(--color-action);
+ --xb-blue-600: var(--color-action-hover);
+ --xb-red-50: #fff1f0;
+ --xb-red-500: var(--color-market-up);
+ --xb-green-50: #ecf9f1;
+ --xb-green-500: var(--color-market-down);
+ --xb-amber-50: #fff7e8;
+ --xb-amber-500: var(--color-warning);
+
+ --bg: var(--surface-canvas);
+ --card: var(--surface-raised);
+ --ink: var(--text-primary);
+ --sub: var(--text-secondary);
+ --faint: var(--text-tertiary);
+ --line: var(--border);
+ --line-soft: #eef0f3;
+ --line-strong: var(--border-strong);
+ --text: var(--text-primary);
+ --text-muted: var(--text-secondary);
+ --blue: var(--primary);
+ --blue-d: var(--primary-hover);
+ --blue-dark: var(--action-hover);
+ --blue-soft: var(--color-action-soft);
+ --blue-line: var(--color-action-line);
+ --up: var(--danger);
+ --up-soft: var(--color-market-up-soft);
+ --down: var(--success);
+ --down-soft: var(--color-market-down-soft);
+ --coral: var(--market-up);
+ --coral-soft: var(--market-up-soft);
+ --green: var(--market-down);
+ --green-soft: var(--market-down-soft);
+ --amber: var(--color-warning);
+ --amber-soft: var(--color-warning-soft);
+ --radius: var(--size-radius-lg);
+ --shadow: var(--elevation-card);
+ --shadow-soft: var(--elevation-soft);
+
+ --r2-blue: var(--primary);
+ --r2-blue-dark: var(--primary-hover);
+ --r2-blue-soft: var(--color-action-soft);
+ --r2-blue-line: var(--color-action-line);
+ --r2-up: var(--danger);
+ --r2-up-soft: var(--color-market-up-soft);
+ --r2-down: var(--success);
+ --r2-down-soft: var(--color-market-down-soft);
+ --r2-amber: var(--color-warning);
+ --r2-amber-soft: var(--color-warning-soft);
+ --r2-ink: var(--text-primary);
+ --r2-sub: var(--text-secondary);
+ --r2-faint: var(--text-tertiary);
+ --r2-line: var(--border);
+ --r2-line-soft: #eef0f3;
+ --r2-bg: var(--surface-canvas);
+ --r2-card: var(--surface-raised);
+ --r2-radius: var(--size-radius-lg);
+ --r2-shadow: var(--elevation-card);
+
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI", "PingFang SC", "Microsoft YaHei UI", sans-serif;
+ font-size: 14px;
+}
+
+:root[data-theme="dark"] {
+ color-scheme: dark;
+ --canvas: #121416;
+ --surface: #1b1e21;
+ --surface-muted: #202428;
+ --surface-subtle: #24282d;
+ --surface-canvas: var(--canvas);
+ --surface-raised: var(--surface);
+ --surface-selected: #23364a;
+ --border: #343a40;
+ --border-strong: #474f57;
+ --text-primary: #e8eaed;
+ --text-secondary: #adb5bd;
+ --text-tertiary: #7f8993;
+ --action: #6ca8e8;
+ --action-hover: #8bbcf0;
+ --action-soft: #23364a;
+ --market-up: #f06d73;
+ --market-up-soft: #40262a;
+ --market-down: #43bc8a;
+ --market-down-soft: #1d382f;
+ --warning-color: #e2ad58;
+ --warning-soft: #3d3220;
+ --primary: var(--action);
+ --primary-hover: var(--action-hover);
+ --danger: var(--market-up);
+ --success: var(--market-down);
+ --warning: var(--warning-color);
+ --card-bg: var(--surface);
+ --card-border: var(--border);
+
+ --xb-gray-25: var(--surface);
+ --xb-gray-50: var(--surface-muted);
+ --xb-gray-100: var(--canvas);
+ --xb-gray-200: var(--border);
+ --xb-gray-300: var(--border-strong);
+ --xb-gray-500: var(--text-secondary);
+ --xb-gray-700: #cbd1d7;
+ --xb-gray-900: var(--text-primary);
+ --xb-blue-50: var(--action-soft);
+ --xb-blue-100: #294866;
+ --xb-blue-500: var(--action);
+ --xb-blue-600: var(--action-hover);
+ --xb-red-50: var(--market-up-soft);
+ --xb-red-500: var(--market-up);
+ --xb-green-50: var(--market-down-soft);
+ --xb-green-500: var(--market-down);
+ --xb-amber-50: var(--warning-soft);
+ --xb-amber-500: var(--warning-color);
+
+ --bg: var(--canvas);
+ --card: var(--surface);
+ --ink: var(--text-primary);
+ --sub: var(--text-secondary);
+ --faint: var(--text-tertiary);
+ --line: var(--border);
+ --line-soft: #2a2f34;
+ --line-strong: var(--border-strong);
+ --text: var(--text-primary);
+ --text-muted: var(--text-secondary);
+ --blue: var(--action);
+ --blue-d: var(--action-hover);
+ --blue-dark: var(--action-hover);
+ --blue-soft: var(--action-soft);
+ --blue-line: #42698e;
+ --up: var(--market-up);
+ --up-soft: var(--market-up-soft);
+ --down: var(--market-down);
+ --down-soft: var(--market-down-soft);
+ --coral: var(--market-up);
+ --coral-soft: var(--market-up-soft);
+ --green: var(--market-down);
+ --green-soft: var(--market-down-soft);
+ --amber: var(--warning-color);
+ --amber-soft: var(--warning-soft);
+ --r2-blue: var(--action);
+ --r2-blue-dark: var(--action-hover);
+ --r2-blue-soft: var(--action-soft);
+ --r2-blue-line: #42698e;
+ --r2-up: var(--market-up);
+ --r2-up-soft: var(--market-up-soft);
+ --r2-down: var(--market-down);
+ --r2-down-soft: var(--market-down-soft);
+ --r2-amber: var(--warning-color);
+ --r2-amber-soft: var(--warning-soft);
+ --r2-ink: var(--text-primary);
+ --r2-sub: var(--text-secondary);
+ --r2-faint: var(--text-tertiary);
+ --r2-line: var(--border);
+ --r2-line-soft: #2a2f34;
+ --r2-bg: var(--canvas);
+ --r2-card: var(--surface);
+ --r2-shadow: var(--shadow-soft);
+
+ --dialog-ink: var(--text-primary);
+ --dialog-line: var(--border);
+ --heaven-paper: #191a18;
+ --heaven-paper-soft: #20211e;
+ --heaven-ink: #e5e0d4;
+ --heaven-muted: #aaa497;
+ --heaven-rule: #3d3a34;
+ --chart-background: #181b1e;
+ --chart-grid: #30363c;
+ --chart-axis: #a3adb6;
+ --chart-zero: #68737d;
+ --chart-line: #6ca8e8;
+ --chart-average: #e2ad58;
+ --chart-up: #f06d73;
+ --chart-down: #43bc8a;
+ --chart-up-volume: rgba(240, 109, 115, .52);
+ --chart-down-volume: rgba(67, 188, 138, .52);
+ --chart-area: rgba(108, 168, 232, .12);
+ --chart-alert-area: rgba(240, 109, 115, .09);
+ --chart-moving-average: #69737d;
+ --chart-repair: #e2ad58;
+ --chart-ma-10: #d39a45;
+ --chart-ma-20: #9aa5af;
+ --on-action: #101418;
+ --warning-line: #6d5a38;
+ --warning-line-strong: #66502d;
+ --control-shadow: 0 1px 3px rgba(0, 0, 0, .3);
+ --dialog-backdrop: rgba(0, 0, 0, .62);
+ --ladder-level-1: #2d2426;
+ --ladder-level-2: #2b2822;
+ --ladder-level-3: #252a2d;
+ --ladder-level-4: #20282b;
+ --ladder-level-5: #202428;
+ --heat-strong-bg: #304f7a;
+ --heat-strong-ink: #f2f6fb;
+ --heat-warm-bg: #2b405f;
+ --heat-warm-ink: #dfeaf7;
+ --heat-mild-bg: #293440;
+ --heat-mild-ink: #c7d2dc;
+ --heaven-field-bg: #23241f;
+ --shadow-soft: 0 1px 2px rgba(0, 0, 0, .28), 0 8px 24px rgba(0, 0, 0, .16);
+ --shadow: 0 18px 50px rgba(0, 0, 0, .46);
+}
diff --git a/static/styles.css b/static/styles.css
index 375e419..ae4c0fb 100644
--- a/static/styles.css
+++ b/static/styles.css
@@ -1,47 +1,3 @@
-:root {
- color-scheme: light;
- --canvas: #f5f6f8;
- --surface: #ffffff;
- --surface-muted: #f8f9fb;
- --border: #e1e6eb;
- --border-strong: #c7d0d9;
- --text-primary: #171a1f;
- --text-secondary: #626c78;
- --action: #1d65c1;
- --action-hover: #164f99;
- --action-soft: #eaf2fc;
- --market-up: #c93f45;
- --market-up-soft: #fcecee;
- --market-down: #087a55;
- --market-down-soft: #e8f6f0;
- --warning-color: #a76500;
- --warning-soft: #fff4df;
- --bg: var(--canvas);
- --line: var(--border);
- --line-strong: var(--border-strong);
- --text: var(--text-primary);
- --text-muted: var(--text-secondary);
- --blue: var(--action);
- --blue-dark: var(--action-hover);
- --blue-soft: var(--action-soft);
- --coral: var(--market-up);
- --coral-soft: var(--market-up-soft);
- --green: var(--market-down);
- --green-soft: var(--market-down-soft);
- --amber: var(--warning-color);
- --amber-soft: var(--warning-soft);
- --shadow: 0 14px 36px rgba(24, 34, 45, 0.14);
- --shadow-soft: 0 2px 8px rgba(24, 34, 45, 0.055);
- --motion-instant: 100ms;
- --motion-fast: 140ms;
- --motion-medium: 200ms;
- --motion-deliberate: 260ms;
- --motion-slow: 560ms;
- --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI", "PingFang SC", "Microsoft YaHei UI", sans-serif;
- font-size: 14px;
-}
-
/* Strategy library and quantitative screener */
.screener-mode-tabs {
min-height: 48px;
@@ -13844,46 +13800,6 @@ button.account-role-badge:focus-visible { outline: 2px solid var(--blue); outlin
#screenerView .screener-result-frame tbody tr td { transition: none; }
}
-/* Unified visual system 2026-07 */
-:root {
- --canvas: #f1f4f6;
- --surface: #ffffff;
- --surface-muted: #f6f8fa;
- --surface-subtle: #fafbfc;
- --border: #dfe4e9;
- --border-strong: #cbd3dc;
- --text-primary: #17202a;
- --text-secondary: #607080;
- --text-tertiary: #8995a2;
- --action: #1769c2;
- --action-hover: #10569f;
- --action-soft: #eaf2fb;
- --market-up: #d33f49;
- --market-up-soft: #fff0f1;
- --market-down: #07805b;
- --market-down-soft: #eaf7f2;
- --warning-color: #aa6800;
- --warning-soft: #fff6e5;
- --bg: var(--canvas);
- --line: var(--border);
- --line-strong: var(--border-strong);
- --text: var(--text-primary);
- --text-muted: var(--text-secondary);
- --blue: var(--action);
- --blue-dark: var(--action-hover);
- --blue-soft: var(--action-soft);
- --coral: var(--market-up);
- --coral-soft: var(--market-up-soft);
- --green: var(--market-down);
- --green-soft: var(--market-down-soft);
- --amber: var(--warning-color);
- --amber-soft: var(--warning-soft);
- --shadow-soft: 0 1px 2px rgba(22, 34, 46, .04), 0 5px 18px rgba(22, 34, 46, .035);
- --shadow: 0 18px 50px rgba(22, 34, 46, .18);
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI", "PingFang SC", "Microsoft YaHei UI", sans-serif;
- font-size: 14px;
-}
-
html,
body {
background: var(--canvas);
diff --git a/static/theme.css b/static/theme.css
index 1ddb7b0..08d117f 100644
--- a/static/theme.css
+++ b/static/theme.css
@@ -1,22 +1,4 @@
-/* Runtime theme layer. Existing page geometry stays untouched; only semantic color tokens change. */
-:root {
- --chart-background: #fbfcfd;
- --chart-grid: #e2e8ec;
- --chart-axis: #6c7983;
- --chart-zero: #aeb7c1;
- --chart-line: #1d65c1;
- --chart-average: #b7791f;
- --chart-up: #c93f45;
- --chart-down: #087a55;
- --chart-up-volume: rgba(201, 63, 69, .58);
- --chart-down-volume: rgba(8, 122, 85, .58);
- --chart-area: rgba(37, 99, 235, .07);
- --chart-alert-area: rgba(224, 69, 54, .05);
- --chart-moving-average: #d1d5db;
- --chart-repair: #f59e0b;
- --chart-ma-10: #a76500;
- --chart-ma-20: #626c78;
-}
+/* Runtime theme rules. Canonical light and dark variables live in shared/tokens.css. */
/* Theme changes are captured as one page-level transition. Descendant effects are
suppressed briefly so tables and cards cannot repaint on separate timelines. */
@@ -43,142 +25,6 @@
from { opacity: 0; }
}
-:root[data-theme="dark"] {
- color-scheme: dark;
- --canvas: #121416;
- --surface: #1b1e21;
- --surface-muted: #202428;
- --surface-subtle: #24282d;
- --border: #343a40;
- --border-strong: #474f57;
- --text-primary: #e8eaed;
- --text-secondary: #adb5bd;
- --text-tertiary: #7f8993;
- --action: #6ca8e8;
- --action-hover: #8bbcf0;
- --action-soft: #23364a;
- --market-up: #f06d73;
- --market-up-soft: #40262a;
- --market-down: #43bc8a;
- --market-down-soft: #1d382f;
- --warning-color: #e2ad58;
- --warning-soft: #3d3220;
- /* Bridge every historical design-token layer to the same dark palette. */
- --xb-gray-25: var(--surface);
- --xb-gray-50: var(--surface-muted);
- --xb-gray-100: var(--canvas);
- --xb-gray-200: var(--border);
- --xb-gray-300: var(--border-strong);
- --xb-gray-500: var(--text-secondary);
- --xb-gray-700: #cbd1d7;
- --xb-gray-900: var(--text-primary);
- --xb-blue-50: var(--action-soft);
- --xb-blue-100: #294866;
- --xb-blue-500: var(--action);
- --xb-blue-600: var(--action-hover);
- --xb-red-50: var(--market-up-soft);
- --xb-red-500: var(--market-up);
- --xb-green-50: var(--market-down-soft);
- --xb-green-500: var(--market-down);
- --xb-amber-50: var(--warning-soft);
- --xb-amber-500: var(--warning-color);
- --surface-canvas: var(--canvas);
- --surface-raised: var(--surface);
- --surface-selected: var(--action-soft);
- --card-bg: var(--surface);
- --card-border: var(--border);
- --primary: var(--action);
- --primary-hover: var(--action-hover);
- --danger: var(--market-up);
- --success: var(--market-down);
- --warning: var(--warning-color);
- --r2-blue: var(--action);
- --r2-blue-dark: var(--action-hover);
- --r2-blue-soft: var(--action-soft);
- --r2-blue-line: #42698e;
- --r2-up: var(--market-up);
- --r2-up-soft: var(--market-up-soft);
- --r2-down: var(--market-down);
- --r2-down-soft: var(--market-down-soft);
- --r2-amber: var(--warning-color);
- --r2-amber-soft: var(--warning-soft);
- --r2-ink: var(--text-primary);
- --r2-sub: var(--text-secondary);
- --r2-faint: var(--text-tertiary);
- --r2-line: var(--border);
- --r2-line-soft: #2a2f34;
- --r2-bg: var(--canvas);
- --r2-card: var(--surface);
- --r2-shadow: var(--shadow-soft);
- --bg: var(--canvas);
- --card: var(--surface);
- --ink: var(--text-primary);
- --sub: var(--text-secondary);
- --faint: var(--text-tertiary);
- --line: var(--border);
- --line-soft: #2a2f34;
- --line-strong: var(--border-strong);
- --text: var(--text-primary);
- --text-muted: var(--text-secondary);
- --blue: var(--action);
- --blue-d: var(--action-hover);
- --blue-dark: var(--action-hover);
- --blue-soft: var(--action-soft);
- --blue-line: #42698e;
- --up: var(--market-up);
- --up-soft: var(--market-up-soft);
- --down: var(--market-down);
- --down-soft: var(--market-down-soft);
- --coral: var(--market-up);
- --coral-soft: var(--market-up-soft);
- --green: var(--market-down);
- --green-soft: var(--market-down-soft);
- --amber: var(--warning-color);
- --amber-soft: var(--warning-soft);
- --dialog-ink: var(--text-primary);
- --dialog-line: var(--border);
- --heaven-paper: #191a18;
- --heaven-paper-soft: #20211e;
- --heaven-ink: #e5e0d4;
- --heaven-muted: #aaa497;
- --heaven-rule: #3d3a34;
- --chart-background: #181b1e;
- --chart-grid: #30363c;
- --chart-axis: #a3adb6;
- --chart-zero: #68737d;
- --chart-line: #6ca8e8;
- --chart-average: #e2ad58;
- --chart-up: #f06d73;
- --chart-down: #43bc8a;
- --chart-up-volume: rgba(240, 109, 115, .52);
- --chart-down-volume: rgba(67, 188, 138, .52);
- --chart-area: rgba(108, 168, 232, .12);
- --chart-alert-area: rgba(240, 109, 115, .09);
- --chart-moving-average: #69737d;
- --chart-repair: #e2ad58;
- --chart-ma-10: #d39a45;
- --chart-ma-20: #9aa5af;
- --on-action: #101418;
- --warning-line: #6d5a38;
- --warning-line-strong: #66502d;
- --control-shadow: 0 1px 3px rgba(0, 0, 0, .3);
- --dialog-backdrop: rgba(0, 0, 0, .62);
- --ladder-level-1: #2d2426;
- --ladder-level-2: #2b2822;
- --ladder-level-3: #252a2d;
- --ladder-level-4: #20282b;
- --ladder-level-5: #202428;
- --heat-strong-bg: #304f7a;
- --heat-strong-ink: #f2f6fb;
- --heat-warm-bg: #2b405f;
- --heat-warm-ink: #dfeaf7;
- --heat-mild-bg: #293440;
- --heat-mild-ink: #c7d2dc;
- --heaven-field-bg: #23241f;
- --shadow-soft: 0 1px 2px rgba(0, 0, 0, .28), 0 8px 24px rgba(0, 0, 0, .16);
- --shadow: 0 18px 50px rgba(0, 0, 0, .46);
-}
-
:root[data-theme="dark"] :is(html, body, .main, .app-main) {
background: var(--canvas);
color: var(--text-primary);
diff --git a/tests/test_css_governance.py b/tests/test_css_governance.py
new file mode 100644
index 0000000..c730ebd
--- /dev/null
+++ b/tests/test_css_governance.py
@@ -0,0 +1,82 @@
+from __future__ import annotations
+
+import re
+import unittest
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[1]
+STATIC = ROOT / "static"
+TOKENS = STATIC / "shared" / "tokens.css"
+LEGACY_STYLESHEETS = (
+ "styles.css",
+ "renovation.css",
+ "redesign-v2.css",
+ "design-system.css",
+ "theme.css",
+)
+
+
+class CssGovernanceTests(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls) -> None:
+ cls.html = (STATIC / "index.html").read_text(encoding="utf-8")
+ cls.tokens = TOKENS.read_text(encoding="utf-8")
+
+ def test_token_layer_loads_before_application_styles(self) -> None:
+ expected_order = (
+ "/shared/tokens.css",
+ "/styles.css",
+ "/renovation.css",
+ "/redesign-v2.css",
+ "/design-system.css",
+ "/theme.css",
+ "/wentian-v2.css",
+ )
+ positions = [self.html.index(path) for path in expected_order]
+ self.assertEqual(positions, sorted(positions))
+
+ def test_token_file_has_three_layer_contract(self) -> None:
+ for heading in (
+ "/* Primitive tokens */",
+ "/* Semantic tokens */",
+ "/* Component tokens */",
+ "/* Compatibility aliases.",
+ ):
+ self.assertIn(heading, self.tokens)
+ for variable in (
+ "--color-action:",
+ "--surface-canvas:",
+ "--text-primary:",
+ "--card-bg:",
+ "--control-height:",
+ "--sidebar-width:",
+ ):
+ self.assertIn(variable, self.tokens)
+
+ def test_light_and_dark_semantics_share_one_owner(self) -> None:
+ self.assertIn(':root[data-theme="dark"] {', self.tokens)
+ global_root = re.compile(r'(?m)^:root(?:\[data-theme="dark"\])?\s*\{')
+ for filename in LEGACY_STYLESHEETS:
+ stylesheet = (STATIC / filename).read_text(encoding="utf-8")
+ self.assertIsNone(global_root.search(stylesheet), filename)
+
+ def test_wentian_tokens_remain_isolated(self) -> None:
+ self.assertNotRegex(self.tokens, r"--wt-[a-z0-9-]+\s*:")
+ wentian = (STATIC / "wentian-v2.css").read_text(encoding="utf-8")
+ self.assertRegex(wentian, r"--wt-[a-z0-9-]+\s*:")
+
+ def test_compatibility_aliases_cover_historical_layers(self) -> None:
+ for variable in (
+ "--blue:",
+ "--up:",
+ "--xb-blue-500:",
+ "--r2-blue:",
+ "--chart-background:",
+ "--dragon-profile-list-width:",
+ ):
+ self.assertIn(variable, self.tokens)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_frontend_contract.py b/tests/test_frontend_contract.py
index 605507f..0cdf9fa 100644
--- a/tests/test_frontend_contract.py
+++ b/tests/test_frontend_contract.py
@@ -27,6 +27,7 @@ class FrontendContractTests(unittest.TestCase):
cls.ui_core = (STATIC_DIR / "ui-core.js").read_text(encoding="utf-8")
cls.design_system = (STATIC_DIR / "design-system.css").read_text(encoding="utf-8")
cls.theme = (STATIC_DIR / "theme.css").read_text(encoding="utf-8")
+ cls.tokens = (STATIC_DIR / "shared" / "tokens.css").read_text(encoding="utf-8")
collector = IdCollector()
collector.feed(cls.html)
cls.ids = collector.ids
@@ -268,7 +269,7 @@ class FrontendContractTests(unittest.TestCase):
self.assertIn("#sentimentCycleView .sentiment-history-frame {", self.theme)
self.assertIn("margin-bottom: var(--card-gap);", self.theme)
self.assertIn("padding-bottom: var(--card-gap);", self.theme)
- self.assertIn("--sentiment-history-max-height:510px;", self.design_system)
+ self.assertIn("--sentiment-history-max-height: 510px;", self.tokens)
self.assertIn("max-height:var(--sentiment-history-max-height);", self.design_system)
self.assertIn("overflow:auto;", self.design_system)