diff --git a/client/electron/src/components/AddressInput.tsx b/client/electron/src/components/AddressInput.tsx
index acc535e..b8875e8 100644
--- a/client/electron/src/components/AddressInput.tsx
+++ b/client/electron/src/components/AddressInput.tsx
@@ -181,7 +181,7 @@ export default function AddressInput({
{open && items.length > 0 && (
{
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
+ const p = join(dir, e.name);
+ if (e.isDirectory()) walk(p);
+ else if (/\.tsx?$/.test(e.name) && !/\.test\./.test(e.name)) out.push(readFileSync(p, 'utf8'));
+ }
+ };
+ walk(join(HERE, '..', 'src'));
+ return out.join('\n');
+}
+const src = componentsSrc();
+
+/** 取出所有 @keyframes 名 + 每条规则的 selector 文本 */
+const keyframes = [...css.matchAll(/@keyframes\s+([a-zA-Z0-9_-]+)/g)].map(m => m[1]);
+const rules = [...css.matchAll(/(^|\n)([^{}\n][^{}]*)\{([^{}]*)\}/g)].map(m => ({
+ sel: m[2].trim().replace(/\s+/g, ' '),
+ body: m[3]
+}));
+
+/**
+ * 一条 selector 在**最终**要不要靠某个类名才能命中?
+ * 只取"最后一个复合选择器"上的类(`html.view-switch .pane-enter` → `pane-enter`),
+ * 祖先里的类(html.view-switch、.app-shell)是状态开关,不算穿的人。
+ */
+function wearerClasses(sel) {
+ return sel
+ .split(',')
+ .map(part => part.trim().split(/\s+/).pop() || '')
+ .filter(last => last.startsWith('.'))
+ .map(last => last.split(/[\s.:[>]/)[0].replace(/^\./, ''))
+ .filter(Boolean);
+}
+
+const dead = [];
+const unwrapped = [];
+for (const k of keyframes) {
+ const users = rules.filter(r => new RegExp(`animation:\\s*${k}\\b`).test(r.body));
+ if (users.length === 0) {
+ dead.push(`${k}(没有任何规则用它)`);
+ continue;
+ }
+ // 这条 keyframes 的每一个使用者都必须"有人穿" —— 否则它还是不会播
+ const worn = users.some(r => {
+ const cls = wearerClasses(r.sel);
+ if (cls.length === 0) return true; // 元素选择器/通配:不算死
+ return cls.some(c => new RegExp(`["'\`\\s]${c}(?=["'\`\\s]|$)`).test(src) || src.includes(c));
+ });
+ if (!worn) unwrapped.push(`${k} ← ${users.map(u => u.sel).join(' | ')}`);
+}
+
+check(
+ '每个 @keyframes 都有人穿(没有死动画)',
+ dead.length === 0 && unwrapped.length === 0,
+ `只定义了没人穿的动画(看着有、永远不播):${[...dead, ...unwrapped].join(';')}`
+);
+
+check(
+ '弹层菜单带着菜单入场类',
+ /animate-menu-in/.test(rules.map(r => r.sel).join(',')) &&
+ /popup-surface[^`]*animate-menu-in|animate-menu-in[^`]*popup-surface/.test(src),
+ '菜单入场类存在但没人穿 —— 下拉/候选菜单是"啪"地出现(2026-09-15 盘点抓到的第二处死动画)'
+);
+
+// 只在**顶层规则**里查:reduced-motion 块里那句 `html.view-switch .glass-control` 是
+// "把它关掉"的名单,不是"让它动"的规则 —— 第一版没区分,判据自己假红。
+const topLevel = css.replace(/@media[^{]*\{(?:[^{}]|\{[^{}]*\})*\}/g, '');
+check(
+ '没有"整档控件一起动"的切视图规则',
+ !/html\.view-switch\s+\.glass-control/.test(topLevel),
+ 'view-switch 又把 .glass-control 整档带上了 —— 每次切视图几十个按钮/输入框一起动(闪与卡顿的现成来源)'
+);
+
+const reducedBlocks = css.match(/@media \(prefers-reduced-motion: reduce\)\s*\{[\s\S]*?\n\}/g) || [];
+check(
+ '挂载即播那档被 reduced-motion 显式关掉',
+ reducedBlocks.some(b => /(^|\n)\s*\.rise-in\s*,/.test(b)),
+ 'prefers-reduced-motion 覆盖不到 .rise-in —— 关掉动画的人照样会看到它'
+);
+
+finish('动画盘点');