diff --git a/design/prototype/icons.js b/design/prototype/icons.js
index 2f93c4b..5904581 100644
--- a/design/prototype/icons.js
+++ b/design/prototype/icons.js
@@ -56,6 +56,7 @@
'arrow-right': '',
'compass': '',
'chart-no-axes-column': '',
+ 'chart': '',
'map-pin': '',
'laptop': '',
'monitor-smartphone': '',
diff --git a/docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md b/docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md
index 2a1647c..69269dd 100644
--- a/docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md
+++ b/docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md
@@ -58,14 +58,14 @@
现状:Web 的 `build-tokens.mjs` 只是「原样拷 css,删 Google Fonts 行」。升级为受闸守护的同源关系。
-- [ ] 2.1 确认两端 token 落点与生成链:website→`src/styles/tokens.gen.css`、
+- [x] 2.1 确认两端 token 落点与生成链:website→`src/styles/tokens.gen.css`、
usercenter→`public/colors_and_type.css`;源统一指向 `design/prototype/tokens.css`(Phase 1 后)
-- [ ] 2.2 建 `tools/check-l1-sync.mjs`(照搬 jiu 裁剪):
+- [x] 2.2 建 `tools/check-l1-sync.mjs`(照搬 jiu 裁剪):
- ① website `tokens.gen.css` token 值 ≡ 原型 tokens.css(逐值)
- ② usercenter `public/colors_and_type.css` ≡ 原型(逐值)
- ③ icons 同源:website / usercenter / Flutter 三处图标集 ⊆ 原型 icons.js sprite
- ④ Web 硬编码色扫描(白名单 `#fff/#000/logo 固定色`,其余报警)
-- [ ] 2.3 codegen 幂等:重跑 `build-tokens.mjs` 后 `git diff` 零差异(纳入 CI,见 Phase 5)
+- [x] 2.3 codegen 幂等:重跑 `build-tokens.mjs` 后 `git diff` 零差异(纳入 CI,见 Phase 5)
---
diff --git a/tools/check-l1-sync.mjs b/tools/check-l1-sync.mjs
new file mode 100644
index 0000000..56515e3
--- /dev/null
+++ b/tools/check-l1-sync.mjs
@@ -0,0 +1,144 @@
+#!/usr/bin/env node
+// tools/check-l1-sync.mjs — L1 设计真相源跨端同源闸(纯 Node 零依赖)
+//
+// 治理依据:根 CLAUDE.md「前端设计系统治理(ds-flow)」——tokens/icons 以
+// design/prototype/ 为单一真源,Web 两端(website/usercenter)只能是同步副本。
+// Flutter 的 token 同源由 ci/check-codegen-drift.sh 守护(codegen 零 diff),
+// 本闸补 Web token 值同源 + 三端图标 ⊆ 原型 sprite + Web 硬编码色扫描。
+//
+// 本地/CI 直接跑:node tools/check-l1-sync.mjs
+//
+// 四道检查(任一违规 exit 1):
+// ① website token 值同源 web/website/src/styles/tokens.gen.css 每个 token
+// 值 ≡ design/prototype/tokens.css(:root + dark)。
+// ② usercenter token 同源 web/usercenter/public/colors_and_type.css 同上。
+// ③ 图标 ⊆ 原型 sprite usercenter LUCIDE(键+路径)+ Flutter pangolin_icons
+// _byName(键)必须 ⊆ 原型 icons.js ICONS。
+// ④ Web 硬编码色扫描 website src / usercenter app|components|lib 禁裸 hex;
+// 白名单 #fff/#000 + 品牌 logo 固定色;行内 `ds-allow` 豁免;
+// 排除生成的 token 定义文件。
+
+import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
+import { join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const ROOT = fileURLToPath(new URL('..', import.meta.url));
+const read = (p) => readFileSync(join(ROOT, p), 'utf8');
+const problems = [];
+
+// 解析 CSS 块里的 --var: value;(压掉空白便于比较)
+function parseVars(cssBlock) {
+ const out = {};
+ for (const [, k, v] of cssBlock.matchAll(/(--[\w-]+)\s*:\s*([^;}]+)[;}]/g)) {
+ out[k] = v.replace(/\s+/g, ' ').trim();
+ }
+ return out;
+}
+// 取 tokens.css 的 :root + [data-theme="dark"] 全量 token
+function protoTokens() {
+ const css = read('design/prototype/tokens.css');
+ const root = css.match(/:root\s*\{([^}]*)\}/s)?.[1] ?? '';
+ const dark = css.match(/\[data-theme="dark"\]\s*\{([^}]*)\}/s)?.[1] ?? '';
+ return { ...parseVars(root), ...parseVars(dark) };
+}
+
+// ── ①② Web token 值同源 ────────────────────────────────────
+function checkWebTokenSync(label, webPath) {
+ if (!existsSync(join(ROOT, webPath))) {
+ problems.push(`[${label}] 找不到 ${webPath} —— 先跑 npm run gen:tokens`);
+ return;
+ }
+ const proto = protoTokens();
+ const web = parseVars(read(webPath));
+ for (const [k, v] of Object.entries(proto)) {
+ if (!(k in web)) {
+ problems.push(`[${label}] 缺 token ${k} —— ${webPath} 未同步原型(重跑 gen:tokens)`);
+ } else if (web[k] !== v) {
+ problems.push(`[${label}] ${k} 值漂移:原型=${v} Web=${web[k]} —— ${webPath} 应由 build-tokens 从原型再生,勿手改`);
+ }
+ }
+}
+
+// ── ③ 图标 ⊆ 原型 sprite ────────────────────────────────────
+{
+ // 原型 icons.js: var ICONS = { 'name': '', ... }
+ const protoJs = read('design/prototype/icons.js');
+ const protoBody = protoJs.match(/var ICONS\s*=\s*\{([\s\S]*?)\n\s*\};/)?.[1] ?? '';
+ const protoIcons = {};
+ for (const [, id, body] of protoBody.matchAll(/'([^']+)'\s*:\s*'([^']*)'/g)) protoIcons[id] = body;
+ if (!Object.keys(protoIcons).length) {
+ problems.push('[icons] 解析原型 icons.js ICONS 为空 —— 检查文件结构');
+ }
+
+ // usercenter: export const LUCIDE: Record = { 'name': '', ... }
+ const ucJs = read('web/usercenter/components/icons.tsx');
+ const ucBody = ucJs.match(/LUCIDE\s*:[^=]*=\s*\{([\s\S]*?)\n\};/)?.[1] ?? ucJs.match(/LUCIDE\s*=\s*\{([\s\S]*?)\n\};/)?.[1] ?? '';
+ for (const [, id, body] of ucBody.matchAll(/'([^']+)'\s*:\s*'([^']*)'/g)) {
+ if (!(id in protoIcons)) {
+ problems.push(`[icons] usercenter 图标「${id}」不在原型 sprite —— 先登记 design/prototype/icons.js 再用`);
+ } else if (protoIcons[id] !== body) {
+ problems.push(`[icons] usercenter 图标「${id}」路径与原型 sprite 不一致 —— 以原型为准`);
+ }
+ }
+
+ // Flutter: static const Map _byName = { 'name': ..., }
+ const dart = read('client/lib/widgets/pangolin_icons.dart');
+ const dartBody = dart.match(/_byName\s*=\s*\{([\s\S]*?)\};/)?.[1] ?? '';
+ for (const [, id] of dartBody.matchAll(/'([^']+)'\s*:/g)) {
+ if (!(id in protoIcons)) {
+ problems.push(`[icons] Flutter 图标「${id}」不在原型 sprite —— 新图标先登记 design/prototype/icons.js`);
+ }
+ }
+}
+
+// ── ④ Web 硬编码色扫描 ──────────────────────────────────────
+{
+ // 白名单:纯白/纯黑 + 品牌 logo 固定色(SVG 内联 fill,非业务散色)
+ const ALLOW = new Set(['fff', 'ffffff', '000', '000000', 'b96a3d', 'faf3ed', '9e5630', '3d2213']);
+ // 排除:生成的 token 定义文件 + 构建产物
+ const SKIP = new Set([
+ 'web/website/src/styles/tokens.gen.css',
+ 'web/usercenter/public/colors_and_type.css',
+ ]);
+ const SKIP_DIR = new Set(['node_modules', 'dist', 'out', '.next', 'build', '.astro', 'public']);
+ const ROOTS = ['web/website/src', 'web/usercenter/app', 'web/usercenter/components', 'web/usercenter/lib'];
+ const files = [];
+ const walk = (dir) => {
+ if (!existsSync(join(ROOT, dir))) return;
+ for (const name of readdirSync(join(ROOT, dir))) {
+ if (SKIP_DIR.has(name)) continue;
+ const rel = `${dir}/${name}`;
+ const st = statSync(join(ROOT, rel));
+ if (st.isDirectory()) walk(rel);
+ else if (/\.(astro|jsx|tsx|ts|js|css)$/.test(name) && !SKIP.has(rel)) files.push(rel);
+ }
+ };
+ ROOTS.forEach(walk);
+ for (const f of files) {
+ read(f).split('\n').forEach((line, i) => {
+ if (line.includes('ds-allow')) return;
+ for (const [hex] of line.matchAll(/#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g)) {
+ const v = hex.slice(1).toLowerCase();
+ if (!ALLOW.has(v)) {
+ problems.push(`[web-hex] ${f}:${i + 1} 硬编码色 ${hex} —— 改用 var(--token),确需保留(如品牌 logo)加行内 ds-allow 注释`);
+ }
+ }
+ });
+ }
+}
+
+checkWebTokenSync('website-token', 'web/website/src/styles/tokens.gen.css');
+checkWebTokenSync('usercenter-token', 'web/usercenter/public/colors_and_type.css');
+
+// ── 汇总 ────────────────────────────────────────────────────
+const line = '='.repeat(60);
+if (problems.length) {
+ console.error(line);
+ console.error(`✗ L1 跨端同源闸未过(${problems.length} 处):`);
+ for (const p of problems) console.error(' · ' + p);
+ console.error(line);
+ process.exit(1);
+}
+console.log(line);
+console.log('✓ 通过:L1 跨端同源(website/usercenter token 值 · 三端图标 ⊆ 原型 sprite · Web 无硬编码色)');
+console.log(line);