/* ============================================================
  见龙智能科技内部管理系统
  品牌视觉：深海军蓝 × 古铜金 · 高级简约科技感
  主题：浅色（白底 · 深藏蓝侧栏 · 金色点缀）
  扎根 · 破界 · 共赴 · 极致
  ============================================================ */
:root {
  /* 顶栏高度（内容区据此预留顶部空间，让内容能从顶栏背后滚过以呈现雾透）。

     不含顶部安全区 —— 安全区由 .layout 的 padding-top 承担，顶栏从安全区之下开始。

     ⚠️ 必须与 .topbar 的 height 完全一致：各媒体查询里改写了后者就要同步改写本变量，

     否则内容区预留的空间与实际顶栏不符、顶部会露出一条缝（见 600px / 900px 两处）。 */
  --topbar-h: 62px;
  --navy-900: #0a1428;   /* 瞳心墨色 */
  --navy-800: #0f1e3d;   /* 主色 深海军蓝 */
  --navy-700: #16294d;   /* 侧栏渐变 */
  --navy-600: #1f3a6e;   /* hover 深 */
  --navy-100: #e9eef7;   /* 浅蓝底 */
  --gold-400: #e9d9b0;   /* 浅金高光 */
  --gold-500: #c9a961;   /* 主金 */
  --gold-600: #b8923f;   /* 深金 */
  /* 页面根背景色（纯色）。三重身份，取值必须与 --topbar-bg 一致：

       1) Safari 橡皮筋回弹时露出的兜底色；

       2) 底部地址栏的取色来源（渐变无法被提取成单一颜色，故此处必须为纯色）；

       3) 顶部状态栏区域的取色/采样来源。

     原值为 #eef2f8，比顶栏的 #f4f5f7 暗 10 —— 当 Safari 以根背景取色时，

     顶部状态栏区域就会比网站顶栏深一档，形成肉眼可见的分界。现统一为顶栏色。

     注意：顶部优先级高于底部 —— 状态栏与顶栏直接相邻，而地址栏与底栏隔着整个页面。 */
  --bg: #f4f5f7;
  /* 顶栏实底色 —— 全站唯一来源。

     取值 = 页面渐变在视口顶部区域（0~62px）的实测平均色（按 393×852 视口采样得出）。

     这个变量同时被三处消费，务必保持单一来源、不要再各自写死颜色：

       1) .topbar 的 background

       2) JS applyTheme 里同步的 <meta name="theme-color">（Safari 顶部工具栏底色）

       3) index.html 里 theme-color 的初始值（JS 执行前的兜底）

     历史教训：这三处曾分别写 #f6f8fc（渐变首色）与 #f4f5f7（实测色），

     而 Safari 顶部工具栏与网站顶栏在视觉上直接相邻，差 5~8 肉眼可见，

     表现为"顶栏与 Safari 上边底色不一致"。 */
  --topbar-bg: #f4f5f7;
  /* 底栏底色（浅色 = 页面渐变末端区域实测色）。原先写死在 .tabbar 规则里，现收进来做单一来源。

     ⚠️ 底栏只能是实底：给它加 backdrop-filter 会破坏 Safari 底部地址栏取色，详见 .tabbar 处注释 */
  --tabbar-bg: #edf1f8;
  /* 顶栏毛玻璃（2026-10-15 起，用户要求"加模糊但颜色尽量不变"）：

     · 模糊取与浮层同一档 --glass-blur-lg，但**不加 saturate** —— §3 记着历史结论：

       顶栏/底栏长期覆盖大面积底色，saturate 会把浅蓝灰拉浓、显得比下方更深；

     · 底色用 color-mix 从原色派生（同色 + 84% 不透明度），观感仍以原色为主，

       所以 meta theme-color 的纯色同步依旧对得上；84% 写在 .topbar 的 color-mix 里；

     · 性能档（data-vfx="lite"）强制回实底，见文件末尾降级区块；

     · ⚠️ 底栏【不能】照抄这套 —— 原因见上一条令牌与 .tabbar 处的注释。 */
  --card: #ffffff;
  --text: #182440;
  --text-muted: #57647e;
  --border: #e1e7f1;
  /* 表单控件基础色：弹窗 / 工具栏 / 三端媒体查询统一读取同一变量，暗色模式只需覆盖变量 */
  --field-bg: #ffffff;
  --field-border: #dfe5f0;
  --field-label: #4a5a78;
  --field-placeholder: #9aa8c4;
  --select-arrow: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%234a5a78' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --primary: var(--navy-800);
  --primary-hover: var(--navy-600);
  --primary-50: #edf2fb;
  /* ⚠️⚠️ 这三个【直接沿用深色主题的亮色】，浅色主题也用它 ——

     用户明确要求："不用去压明度……深色模式的就很完美，直接沿用即可"。

     代价（实测 · 白底）：--warning-ink 2.33:1 / --success-ink 2.19:1 / --danger-ink 3.68:1，

     低于 AA 的 4.5 —— 这是【知情的取舍】：色相与深色完全一致（38° / 155° / 354°），

     亮色在白底上偏淡；而"压深一档"的版本（#8f620e / #1f7a5a）发暗、被判定难看。

     日后想回到达标线时，把这三个值压深即可（完整取值过程见规范第二十七、二十八轮）。

     ⚠️ 状态胶囊（.st-on_hold / .st-completed）与 9px 状态点【不跟随】这三个 ——

        胶囊自带一层浅色底，亮色文字压上去只有 ≈2.1:1（比白底还差），故各自保留原值。

     ⚠️ 深色主题里它们与 --warning / --success / --danger 同值，无需另设覆盖。 */
  --warning-ink: #d8a03c;
  --success-ink: #34c68a;
  --danger-ink: #e25563;
  --info-ink: #7da9f7;    /* 提示 / 进行中：暗底用的亮蓝（与 .st-in_progress 深色补同一值）*/
  --success: #2f9e77;
  --warning: #d08a2a;
  --danger: #d64545;
  /* ── 动效（全站唯一来源，组件只引令牌、不写裸值）─────────────────────
     4 档时长：
       dur-1 微反馈（hover 底色 / 描边 / 行 hover）
       dur-2 常规（按钮、输入、开关、提示条滑入、遮罩淡入）
       dur-3 容器（页级入场、面板底色过渡）
       dur-4 强调（图标弹跳、进度条生长）
     3 条曲线：std 通用缓动 / out 收尾柔和（弹窗、抽屉）/ pop 弹性（图标）。
     ⚠️ 收敛前全站有 12 个时长（.12/.15/.16/.18/.2/.22/.24/.25/.26/.28/.3/.4）
     与 5 条曲线，同一个"淡入"在不同组件上快慢不一 —— 现在只有这 4 + 3。 */
  --dur-1: .15s;
  --dur-2: .22s;
  --dur-3: .28s;
  --dur-4: .4s;
  --ease-std: cubic-bezier(.4, 0, .2, 1);
  --ease-out: cubic-bezier(.22, .61, .36, 1);
  --ease-pop: cubic-bezier(.25, .8, .35, 1);
  --radius: 14px;
  --shadow-sm: 0 1px 2px rgba(31, 58, 110, .06);
  --shadow: 0 8px 24px rgba(31, 58, 110, .1);
  --shadow-lg: 0 20px 50px rgba(31, 58, 110, .16);
  /* ── 毛玻璃材质（仅浮层 / 登录卡使用）──────────────────────────────

     风格基准：iOS 18 材质 —— 弱模糊 + 高透明 + 极细亮边。

     改这一组 = 全站浮层通透度同步变化；浮层内禁止再写 rgba 数字（铁律 2）。

     2026-10-15 调整：模糊 30/24/10/2 → 20/16/8/2，饱和 180/130 → 150/120，

     雾面底 .82/.78 → .70/.66（透明度更高）。 */
  --glass-blur-lg: 20px;      /* 站内弹窗 .modal / 浮层菜单 .more-card / 登录卡 .login-card */
  --glass-blur-md: 16px;      /* 提示条 .toast */
  --glass-blur-sm: 8px;       /* 弹窗遮罩 .modal-mask / 登录页玻璃遮罩 .login-glass-mask */
  --glass-blur-xs: 2px;       /* 侧栏抽屉遮罩 .sidebar-mask（无饱和增强） */
  --glass-sat-strong: 150%;   /* lg / md 两档配 strong */
  --glass-sat-soft: 120%;     /* sm 档（两层遮罩）配 soft */
  --glass-tint: rgba(255, 255, 255, .70);        /* 雾面底：.modal / .more-card */
  --glass-tint-thin: rgba(255, 255, 255, .66);   /* 更薄一档：.toast */
  --glass-tint-card-a: rgba(255, 255, 255, .55); /* 登录卡渐变（起） */
  --glass-tint-card-b: rgba(242, 247, 253, .40); /* 登录卡渐变（止） */
  --glass-hairline: rgba(255, 255, 255, .50);    /* 浮层 1px 亮边 */
  --glass-inset: rgba(255, 255, 255, .50);       /* 浮层顶部内高光 */
  --glass-veil-sm: rgba(15, 30, 60, .08);        /* 弹窗遮罩 / 登录页玻璃遮罩：轻压暗 */
  --glass-veil-md: rgba(10, 20, 40, .38);        /* 侧栏抽屉遮罩：中等压暗 */
  /* ── 强调实底（主按钮 / 主色图标块）与抬升面板 ─────────────────────
     深浅两套值的唯一住所：组件只写 `var(--accent-fill)`，**不再各自写
     `html[data-theme="dark"] .x { background: #hex }` 分支** ——
     深色里那 4 组"提亮藏蓝"（#2f5cb3 / #20428f / #2b4a86 / #3560b0 …）就是
     因为每个组件各自算一遍，才出现"同一个主色，五处五种蓝"。
     规则：深色 = 同色相 + 明度朝"远离背景"平移（背景变暗 → 实底变亮）。 */
  --accent-fill: linear-gradient(135deg, var(--navy-700), var(--navy-800));         /* 主操作实底 */
  --accent-fill-hover: linear-gradient(135deg, var(--navy-600), var(--navy-800));   /* 主操作 hover */
  --accent-fill-deep: linear-gradient(135deg, var(--navy-800), var(--navy-900));    /* 更深一档（次要主色块） */
  --surface-2: #ffffff;                          /* 抬升面板：次级按钮 / 设置卡 / 浮层底栏 */
  --surface-3: var(--navy-100);                  /* 再抬一档：滑块 / 轨道上的选中块 */
  /* ── 登录页（独立表面：背景恒为深藏蓝流光 + WebGL 液态金属，卡片随主题在

     「白雾透」与「深色玻璃」之间切换）────────────────────────────────

     卡片上每个颜色角色都收成一对令牌：浅色写在这里，深色写在 html[data-theme="dark"] 段。

     ⚠️ 两套必须一一对应 —— 同一角色只允许"换深浅"，不允许在深色里另造一个色相（铁律 2）。

     卡片底色/模糊/亮边沿用上面的 --glass-* 组（与站内弹窗同一套值）。 */
  --login-ink: #0c1a36;                 /* 品牌名 / h2 / 输入文字 */
  --login-ink-sub: #4c5c80;             /* 品牌副标（.sub） */
  --login-ink-body: #42547a;            /* 卡片说明段 */
  --login-label: #3a4a6e;               /* 表单 label */
  --login-placeholder: #64749a;         /* 输入框占位符 */
  --login-hairline: rgba(22, 41, 77, .10);        /* 品牌区分隔线 */
  --login-field-bg: rgba(255, 255, 255, .55);     /* 输入框底 */
  --login-field-bg-focus: rgba(255, 255, 255, .78);
  --login-field-border: rgba(216, 223, 236, .75);
  --login-field-inset: rgba(31, 58, 110, .04);    /* 输入框内阴影 */
  --login-focus-ring: rgba(201, 169, 97, .20);    /* 聚焦金色光圈 */
  --login-logo-glow: rgba(201, 169, 97, .40);     /* Logo 金色投影（两主题同值） */
  --login-glow-inset: rgba(201, 169, 97, .12);    /* 卡片内金色柔光 */
  --login-shadow: rgba(2, 6, 18, .45);            /* 卡片外投影 */
  --login-card-solid-a: #ffffff;        /* 性能档（无玻璃）卡片实底，与 --glass-tint-card-* 成对 */
  --login-card-solid-b: #f2f7fd;
  --login-btn-grad-b: #b89240;          /* 主按钮金色渐变末端（浅深同值） */
  --login-btn-ink: #fff;                /* 主按钮文字（浅深同值） */
  --login-btn-glow: rgba(184, 146, 63, .50);   /* 主按钮投影光晕（深色 .40，见深色段） */
  /* ── 性能档（data-vfx="lite"，无玻璃）的实底孪生色 ─────────────────

     与 --glass-* 一一对应：玻璃档关掉模糊后必须有实底顶上，否则文字叠影。

     三种遮罩在深色性能档下同值（原先各写一遍），故合成一个 --lite-veil。 */
  --lite-veil-modal: rgba(15, 30, 60, .42);    /* 弹窗遮罩 / 登录页玻璃遮罩（性能档） */
  --lite-veil-sidebar: rgba(10, 20, 40, .50);  /* 侧栏抽屉遮罩（性能档） */
  --login-lite-border: rgba(205, 217, 235, .90);
  --login-lite-shadow: 0 26px 64px -22px rgba(2, 6, 18, .35), 0 1px 0 rgba(255, 255, 255, .70) inset;
  --login-lite-field-bg: #ffffff;              /* 性能档：输入框实底 */
  --login-lite-field-border: rgba(203, 214, 233, .95);
  /* ── 控件高度（可交互控件的统一高度）──────────────────────────────

     只给「可交互控件」用：输入框 / 下拉 / 按钮 / 日期时间框。

     ⚠️ 头像、品牌图、图标块、列表行等【非控件】的高度不是控件高度，保持各自字面量。

     值由现有实现反推（= 当时的实测高度），故本次替换为纯令牌化、零视觉变化。

     2026-10-15：原先 38px 散落 10 处、40px 散落 7 处，改档位要满文件搜；

     其中 10 处还是「用 line-height 凑高度」的写法，一并收进同一组令牌。 */
  --ctl-h-xs: 34px;   /* 图标按钮 .btn-icon（方形 34×34） */
  --ctl-h: 38px;      /* 默认：输入框 / 下拉 / 工具条按钮 / 弹窗表单控件 / 日期时间框 */
  --ctl-h-lg: 40px;   /* 大：弹窗底栏按钮（含危险组方形图标按钮）/ 工具条大屏按钮 / 登录·资料按钮 */
  --ctl-h-xl: 44px;   /* 特大：登录提交 / 个人资料操作按钮 / 卡片内状态下拉 */
  /* 分段切换控件（.seg-toggle / .theme-toggle —— 全站只剩这两套，其余变体已随死样式清理删除）：

    内衬与格间距成对出现，而滑块的位置算式直接引用它们 —— 改一处即全联动，避免"算式里的数字"漂移。

    2026-10-15 就发生过一次：gap 由 3px 归并为 4px 后，滑块算式仍按 3px 写死，

    滑块因此偏宽 0.5px、切到第二格时偏左 1px（已修，算式改为引用本组令牌）。

     ⚠️ 3px 是间距阶梯的【唯一例外档】，只用于分段控件内衬（要贴合图标按钮与 7px 圆角滑块）。 */
  --seg-gap: 4px;
  --seg-inset: 3px;
  /* ── 禁用态两档（同一语义单一来源）──────────────────────────────

     常规控件（按钮 / 输入 / 卡片内动作）失能 = .55；

     浮层菜单项（.more-item，压在弹窗内、层级深）与危险区次要按钮 = .38 的旧值保留为 soft 档。

     2026-10-17：此前 .55 / .38 / .38 三个值散在三处，改档要满文件搜。 */
  --disabled-opacity: .55;
  --disabled-opacity-soft: .38;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
  /* 语义动作色：默认取 *-ink（深色底），浅色由 html:not([data-theme="dark"]) 覆盖 */
  --action-ok: var(--success-ink);
  --action-warn: var(--warning-ink);
  --action-danger: var(--danger-ink);
  /* 语义文字的浅色档：同族深端（= ⑥ 段图标渐变的深端），白底 5.32 / 4.71 / 5.63:1，全部 ≥ AA。
     只在浅色下被 --action-* 引用；深色仍用 *-ink，不受影响。 */
  --action-ok-deep: #22795a;
  --action-warn-deep: #8a6a28;
  --action-danger-deep: #b93a3a;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
h1, h2, h3, h4, h5, h6 { margin: 0; font-weight: 600; line-height: 1.4; }
p { margin: 0; }
/* 禁止 iOS Safari / 微信内核的文本自动放大，避免手机端页面初始呈现"放大"状态 */
html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  color-scheme: light;
  /* 纯色兜底，同时也是 Safari 底部栏的取色来源；该值由 JS 在 applyTheme 里
     以内联样式同步，确保主题切换必然生效。 */
  background-color: var(--bg);
}
/* 移动端禁止横向溢出：避免页面被浏览器整体缩小、需捏合才能恢复正常。
     viewport-fit=cover 下 100% 宽度会受安全区影响，导致内容不铺满、出现白块并可拖动；
     统一改用 100vw 锁死全屏宽度，内容真正铺满屏幕 */
html, body {
  height: 100%;
  font-family: var(--font);
  font-size: 14px;
  /* 基础行高入档（规范 §2.3 正文档 = 1.4）：此前只声明字号，未显式声明行高的 45 个规则块
     全部落到浏览器 normal —— 各内核/字体的度量差异会让同类文本高低不一（多行文案最明显）。
     入档后统一为 19.6px，单行控件（按钮/表头/徽标）几乎无位移，多行文本不再飘。 */
  line-height: 1.4;
  color: var(--text);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
body {
  background:
    radial-gradient(900px 520px at 88% -12%, rgba(201, 169, 97, .08), transparent 62%),
    radial-gradient(820px 620px at -12% 112%, rgba(31, 58, 110, .04), transparent 72%),
    linear-gradient(180deg, #f6f8fc 0%, #edf1f8 100%);
  /* 尺寸锁定为视口并禁止平铺：不写时尺寸为 auto（= 元素自身尺寸），

     背景坐标系会随 body 实际高度浮动；写死 100vw/100vh 后它恒定等于视口，

     位置和比例都稳定，也便于与顶栏/底栏的实底色精确对齐。 */
  background-size: 100vw 100vh;
  background-repeat: no-repeat;
  background-attachment: fixed;
}
button { font-family: inherit; cursor: pointer; }
/* iOS Safari：聚焦字号 <16px 的输入框会自动放大页面且跳转后不还原，
     因此移动端所有表单控件统一 ≥16px，从源头杜绝该问题 */
input, select, textarea { font-family: inherit; }
a { color: inherit; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: rgba(22, 41, 77, .18); border-radius: 8px; }
::-webkit-scrollbar-thumb:hover { background: rgba(22, 41, 77, .3); }
::-webkit-scrollbar-track { background: transparent; }
/* ================= 通用组件 ================= */
.hidden { display: none !important; }
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 9px 18px;
  border-radius: 10px;
  border: 1px solid transparent;
  font-size: 14px;
  font-weight: 500;
  /* 显式列属性，不用 all：.btn:active 的 transform 按压反馈保持瞬时（all 会带出拖影） */
  transition: background-color var(--dur-1) var(--ease-std), border-color var(--dur-1) var(--ease-std), color var(--dur-1) var(--ease-std), box-shadow var(--dur-1) var(--ease-std), filter var(--dur-1) var(--ease-std);
  white-space: nowrap;
  line-height: 1.4;
  position: relative;
  overflow: hidden;
}
/* 按下反馈：轻微下沉 + 缩小。
   刻意让"位移"只出现在按下这一态 —— 此前 hover 还会上浮 1px，
   一浮一沉净位移只有 2px，加上鼠标停留时已经处于浮起态，
   按下的感觉就被抵消了（PC 端尤其不明显）。 */
.btn:active { transform: translateY(1px) scale(.96); }
.btn:disabled { opacity: var(--disabled-opacity); cursor: not-allowed; }
.btn svg { width: 15px; height: 15px; }
.btn-icon svg { width: 18px; height: 18px; margin: 0; }
/* ================= 光感设计（克制，配合玻璃雾透材质） ================= */
/* 主按钮辉光（藏蓝 + 冷蓝光晕） */
.btn-primary {
  background: var(--accent-fill);       /* 深色由同名字符串令牌接管，此处不写主题分支 */
  color: #fff;
  /* 原「光感层」第二处定义已合并到此：藏蓝投影 + 金色光晕（不再用冷蓝 rgba(37,99,235)） */
  box-shadow: 0 4px 12px rgba(15, 30, 61, .22), 0 6px 18px -8px rgba(201, 169, 97, .35);
}
/* 悬停只做颜色/阴影变化，不做位移 —— 位移留给 :active，按下才"沉"。
   ⚠️ 所有按钮的 :hover 统一写在 @media (hover: hover) 内：
   触屏设备没有真正的"悬停"，但浏览器仍会在手指触摸后保留 hover 状态（sticky hover），
   让按钮卡在"加深的渐变 + 加重阴影"上，看起来是高光闪一下不消失。
   此前用"在触屏下逐个列举复位"的写法，必然漏掉部分变体，
   造成"有的按钮残留高光、有的不残留"——统一收口到 media query 才不会漏。 */
@media (hover: hover) {
  .btn-primary:hover:not(:disabled) { background: var(--accent-fill-hover); box-shadow: 0 8px 22px -8px rgba(201, 169, 97, .5), inset 0 1px 0 rgba(255, 255, 255, .14); }
  /* ⚠️ 悬停【不能】把文字也改成金色：--gold-600 在白底上实测只有 2.75:1（AA 要 4.5），
     而它在深色底上刚好 4.52:1 —— 同一个 hover，深浅两主题一个不合格一个刚好过，
     这正是"浅色和深色按下去效果不一样"的根源（用户反馈）。
     按规范 §3「hover 一律金色背景高光」，改为【金描边 + 金色浅底 + 文字保持深藏蓝】：
     文字对比 12.6:1，金色仍然作为"高光"存在，只是不再承担文字色的职责。 */
.btn-outline:hover { background: rgba(201, 169, 97, .14); border-color: var(--gold-500); color: var(--navy-700); box-shadow: 0 2px 10px rgba(201, 169, 97, .2); }
  /* 深色：底色得单独给 —— 上面 .btn-outline 的深色规则（html[data-theme="dark"] .btn-outline）
     特异性 (0,2,1) 高于单层 .btn-outline:hover 的 (0,2,0)，不覆盖的话那层金底在深色下根本不生效。
     文字用 --text（亮色）而不是 --navy-700（深色下是 #101c31，落在 #1a2642 上几乎读不出来）。 */
html[data-theme="dark"] .btn-outline:hover { background: rgba(201, 169, 97, .18); color: var(--text); }
  .btn-ghost:hover { background: var(--primary-50); color: var(--navy-700); }
  .btn-danger:hover { filter: brightness(1.08); }
}
/* 次级按钮：面 / 描边 / 投影全部走令牌，深色由令牌接管（不再单写 `html[data-theme="dark"]` 分支）。
   ⚠️ 触屏复位与这里必须逐项同值 —— 否则「停留」与「静止」差一个阴影或一档描边，
   会被 `card-action-audit` ⑫ 抓出来（同一按钮两种长相）。 */
.btn-outline { background: var(--surface-2); border-color: var(--field-border); color: var(--navy-700); box-shadow: var(--shadow-sm); }
.btn-ghost { background: transparent; color: var(--text-muted); }
/* 实心按钮（仅剩 .btn-danger）：底色用深色主题的亮红，文字改为【深墨色】——
   亮底配深字，深浅两主题都成立，且不必为浅色另调一档。
   对比（实测）：#e25563 + #0a1428 = 4.93:1；深色主题下文字取 --navy-900（#050b16）→ 5.34:1。
   ⚠️ 白字方案不可行：白字压在亮底上，--danger 只有 4.38:1。
   （历史遗留的 .btn-gold / .btn-success 两个变体在 2026-10-17 的死样式清理中删除：
   全站 0 引用 —— 登录提交用的是 .btn-primary，成功态一律走 toast 与语义胶囊。） */
.btn-danger { background: var(--danger-ink); color: var(--navy-900); }
.btn-sm { padding: 5px 12px; font-size: 13px; border-radius: 8px; }
.btn-icon { padding: 8px; width: var(--ctl-h-xs); height: var(--ctl-h-xs); }
.input, .select, .textarea {
  width: 100%;
  padding: 9px 12px;
  border: 1px solid var(--field-border);
  border-radius: 10px;
  font-size: 14px;
  background: var(--field-bg);
  color: var(--text);
  outline: none;
  transition: border-color var(--dur-1) var(--ease-std), box-shadow var(--dur-1) var(--ease-std);
}
/* 单行输入框：高度显式化（与 .modal-body .field .input / .toolbar .input / 登录页同一写法），
   上下内边距归零 —— 原先是「9px 上下内边距 + 行高」撑出 38px（实测五视口一致）。
   ⚠️ 只对 <input class="input"> 生效：.select 的原生高度随平台不同、.textarea 要能多行增高，两者都不能定高。 */
input.input { height: var(--ctl-h); padding-top: 0; padding-bottom: 0; }
/* 下拉统一：隐藏原生箭头，改用 CSS 箭头，避免原生箭头 + 背景箭头叠加成双箭头 */
.select {
  -webkit-appearance: none;
  appearance: none;
  padding-right: 32px;
  background-image: var(--select-arrow);
  background-repeat: no-repeat;
  background-position: right 11px center;
  background-size: 10px 6px;
}
.input::placeholder, .textarea::placeholder { color: var(--field-placeholder); }
/* 输入框 / 下拉 / 搜索聚焦金色光晕 */
.input:focus, .select:focus, .textarea:focus {
  border-color: var(--gold-500);
  /* 光感层第二处定义已合并：3px 环 + 2px 外发光 */
  box-shadow: 0 0 0 3px rgba(201, 169, 97, .18), 0 2px 16px -6px rgba(201, 169, 97, .25);
}
.textarea { resize: vertical; min-height: 80px; }
/* ================= 焦点可见（键盘可访问） =================
   输入类已有「金边 + 3px 光环」（上面那条 :focus），这里只补其余可聚焦元素。
   统一 2px 金色描边 + 2px 偏移，深浅两主题同值（金色对深蓝 / 白底都 ≥ 3:1）。
   ⚠️ 必须用 :focus-visible：鼠标点击不出现焦点框，不改变既有观感。 */
.btn:focus-visible,
.btn-icon:focus-visible,
.nav-item:focus-visible,
.modal-close:focus-visible,
.more-item:focus-visible,
.seg-btn:focus-visible,
.seg-toggle button:focus-visible,
.theme-opt:focus-visible,
.vfx-opt:focus-visible,
.color-swatch:focus-visible,
.defect-pick:focus-visible,
.pager-bar .btn:focus-visible,
.pager-num:focus-visible,
input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--gold-500);
  outline-offset: 2px;
}
/* 整卡点击的行（卡片模式下一行即一张卡）：焦点框画在内侧，避免被 .table-card 的圆角裁掉 */
#page-content tbody tr[tabindex="0"]:focus-visible {
  outline: 2px solid var(--gold-500);
  outline-offset: -2px;
  background: rgba(201, 169, 97, .07);
}
html[data-theme="dark"] #page-content tbody tr[tabindex="0"]:focus-visible { background: rgba(255, 255, 255, .06); }
.field { display: flex; flex-direction: column; gap: 6px; }
/* 表单 label（13px）同样取 snug 1.4（基座未声明 → 落到浏览器 normal ≈1.2） */
.field label { font-size: 13px; font-weight: 600; line-height: 1.4; color: var(--field-label); }
.field label .req { color: var(--danger); }
/* 第一级大卡片（首页统计卡 / 内容卡、项目·任务·用户·甘特页的容器卡）：
   实底，不做毛玻璃 —— 与 .profile-card（系统设置 / 个人中心）统一。
   依据：卡片背后只有一片【平滑渐变】，没有图案/文字，模糊看不出效果；
   而 backdrop-filter 会让每张卡单独开合成层，滚动时每帧回读重算（首页最多 9 张同时存在）。
   毛玻璃只保留给"背后有真内容"的浮层：.modal / .more-card / .toast / .login-card。 */
.card {
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 0 0 .5px rgba(31, 58, 110, .05), 0 12px 32px -14px rgba(31, 58, 110, .13);
  border-radius: var(--radius);
  /* 光感层曾追加第二处 .card 覆盖 transition，已合并到此（主题切换过渡也在此声明） */
  transition: transform var(--dur-2) var(--ease-std), box-shadow var(--dur-2) var(--ease-std), background-color var(--dur-3) var(--ease-std), border-color var(--dur-3) var(--ease-std), backdrop-filter var(--dur-3) var(--ease-std);
}
/* 卡片 hover 金色细环微光 */
.card:hover { transform: translateY(-2px); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 0 0 .5px rgba(31, 58, 110, .05), 0 20px 44px -18px rgba(31, 58, 110, .2); }
.badge {
  display: inline-flex; align-items: center; justify-content: center; gap: 4px;
  padding: 2px 8px; border-radius: 999px;
  font-size: 12px; font-weight: 500; line-height: 1.5;
  text-align: center;   /* 与 .status-chip 同步：卡头里不再继承父级的 start */
}
.badge-ghost { background: var(--primary-50); color: var(--text-muted); }
/* 首页 / 弹窗 / 通知卡头像在小屏统一缩小（侧栏 / 顶栏 / 甘特各处已有自己的覆盖，不受影响） */
.avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 50%;
  color: #fff; font-size: 13px; font-weight: 600;
  flex-shrink: 0;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .4), 0 2px 6px rgba(15, 30, 61, .14);
}
.progress { width: 100%; height: 6px; background: #e9edf5; border-radius: 999px; overflow: hidden; }
/* 进度条内高光（统一质感） */
.progress > div { height: 100%; border-radius: 999px; transition: width var(--dur-4) var(--ease-std); }
/* ================= Toast ================= */
#toast-container {
  position: fixed; top: 20px; right: 20px; z-index: 10000;
  display: flex; flex-direction: column; gap: 8px;
}
.toast {
  min-width: 260px; max-width: 380px;
  padding: 13px 18px;
  /* 通知文案可能换行（max-width 380px）→ 显式入「说明段」1.6 档（规范 §2.3），不靠继承 */
  line-height: 1.6;
  border-radius: 12px;
  background: var(--glass-tint-thin);
  backdrop-filter: blur(var(--glass-blur-md)) saturate(var(--glass-sat-strong));
  -webkit-backdrop-filter: blur(var(--glass-blur-md)) saturate(var(--glass-sat-strong));
  box-shadow: var(--shadow-lg);
  border: 1px solid var(--glass-hairline);
  border-left: 4px solid var(--navy-800);
  font-size: 14px;
  color: var(--text);
  animation: slideIn var(--dur-2) var(--ease-std);
  transition: background var(--dur-3) var(--ease-std), border-color var(--dur-3) var(--ease-std), box-shadow var(--dur-3) var(--ease-std), backdrop-filter var(--dur-3) var(--ease-std);
}
.toast.success { border-left-color: var(--success); }
.toast.error { border-left-color: var(--danger); }
.toast.warning { border-left-color: var(--warning); }
.toast.fade { opacity: 0; transform: translateX(24px); transition: opacity var(--dur-3) var(--ease-std), transform var(--dur-3) var(--ease-std); }
@keyframes slideIn { from { opacity: 0; transform: translateX(24px); } to { opacity: 1; transform: none; } }
/* 深色模式：右上角提示弹窗（登录成功 / 失败等）跟随深色背景 */
html[data-theme="dark"] .toast {
  background: var(--glass-tint-thin);
  border-color: var(--glass-hairline);
  color: var(--text);
}
/* 深色下保留类型色条：一律取「-ink 亮色档」令牌 —— 原先是 tailwind 三个散值
   （#34d399 / #fbbf24 / #60a5fa），与语义令牌同色相但不同值，属"同一角色两套色"。
   ⚠️ 新语义色统一加 `-ink` 后缀，不再单独造值。 */
html[data-theme="dark"] .toast.success { border-left-color: var(--success-ink); }
html[data-theme="dark"] .toast.error { border-left-color: var(--danger-ink); }   /* 原 #f87171 是第 5 个红色变体 */
html[data-theme="dark"] .toast.warning { border-left-color: var(--warning-ink); }
html[data-theme="dark"] .toast.info { border-left-color: var(--info-ink); }
/* ================= Modal ================= */
.modal-mask {
  position: fixed; inset: 0; z-index: 1000;
  background: var(--glass-veil-sm);
  backdrop-filter: blur(var(--glass-blur-sm)) saturate(var(--glass-sat-soft));
  -webkit-backdrop-filter: blur(var(--glass-blur-sm)) saturate(var(--glass-sat-soft));
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
  /* 遮罩淡入与弹窗 modalIn 同步，保证三端/桌面同一节奏；主题切换过渡一并在此声明 */
  transition: opacity var(--dur-2) var(--ease-out), background var(--dur-3) var(--ease-std), backdrop-filter var(--dur-3) var(--ease-std);
  /* 遮罩淡入与弹窗 modalIn 同步，保证三端/桌面同一节奏 */
  animation: maskFade var(--dur-2) var(--ease-std);
}
@keyframes maskFade { from { opacity: 0; } to { opacity: 1; } }
/* 弹窗统一布局：顶栏固定、正文独立滚动、底栏固定 —— 三端所有弹窗同一套结构与视觉 */
.modal {
  width: 100%; max-width: 620px;
  max-height: min(76vh, 640px);
  display: flex; flex-direction: column;
  overflow: hidden;
  background: var(--glass-tint);
  backdrop-filter: blur(var(--glass-blur-lg)) saturate(var(--glass-sat-strong));
  -webkit-backdrop-filter: blur(var(--glass-blur-lg)) saturate(var(--glass-sat-strong));
  border: 1px solid var(--glass-hairline);
  border-radius: 16px;
  padding: 24px;
  box-shadow: inset 0 1px 0 var(--glass-inset), 0 28px 64px -18px rgba(15, 30, 60, .3);
  animation: modalIn var(--dur-2) var(--ease-out);
  /* 主题切换过渡（原「光感层」第二处 .modal 定义，已合并） */
  transition: background var(--dur-3) var(--ease-std), border-color var(--dur-3) var(--ease-std), box-shadow var(--dur-3) var(--ease-std), backdrop-filter var(--dur-3) var(--ease-std), height var(--dur-2) var(--ease-out);
}
@keyframes modalIn { from { opacity: 0; transform: translateY(14px) scale(.98); } to { opacity: 1; transform: none; } }
/* 弹窗宽度四档（唯一允许的取值，语义类而不是内联 max-width）：
   sm 420（二次确认）/ md 620（默认，单列表单）/ lg 760（双列·图文并排）/ xl 920（多列，预留）。
   ⚠️ 必须写在 .modal 之后：同为单类选择器，顺序决定优先级。 */
.modal--sm { max-width: 420px; }
.modal--md { max-width: 620px; }
.modal--lg { max-width: 760px; }
.modal--xl { max-width: 920px; }
.modal-header { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; margin-bottom: 20px; padding-bottom: 14px; border-bottom: 1px solid #eef1f7; }
.modal-header h3 { font-size: 17px; font-weight: 700; color: var(--navy-800); letter-spacing: .01em; }
/* 关闭键的 × 是【图标字符】，不是文本层级 —— 居中用 flex（规范 §2.3 明令禁止 line-height: 1
   这类"用行高凑居中"）；尺寸固定 34×34（与 .btn-icon 同档，满足最小触控目标）。 */
.modal-close {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0;
  background: none; border: none; border-radius: 8px;
  font-size: 20px; line-height: 1.2; color: var(--text-muted);
  transition: color var(--dur-1) var(--ease-std);
}
.modal-close:hover { color: var(--text); }
/* 正文区统一为弹窗内独立滚动容器：内容短不出现滚动；内容超高时在弹窗内滚动，顶栏/底栏不动 */
.modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin; /* Firefox：细窄滚动条 */
  scrollbar-color: rgba(22, 41, 77, .28) transparent; /* Firefox：滚动条颜色（thumb / track） */
  /* 底部留缓冲：避免滚动贴底时最后一个控件（如头像颜色色块的高亮环）被滚动边缘裁切 */
  padding: 0 0 14px;
}
/* 弹窗正文滚动条：统一细窄圆角样式，三端一致；滚动贴底仍有底部缓冲，控件特效不会被裁切 */
.modal-body::-webkit-scrollbar { width: 6px; height: 6px; }
.modal-body::-webkit-scrollbar-track { background: transparent; }
.modal-body::-webkit-scrollbar-thumb { background: rgba(22, 41, 77, .24); border-radius: 999px; transition: background var(--dur-2) var(--ease-std); }
.modal-body::-webkit-scrollbar-thumb:hover { background: rgba(22, 41, 77, .42); }
html[data-theme="dark"] .modal-body { scrollbar-color: rgba(148, 168, 205, .32) transparent; }
html[data-theme="dark"] .modal-body::-webkit-scrollbar-thumb { background: rgba(148, 168, 205, .32); }
html[data-theme="dark"] .modal-body::-webkit-scrollbar-thumb:hover { background: rgba(148, 168, 205, .5); }
/* 方案 A：Android 等触屏 WebView 的滚动条既不能自动显隐、也不能拖动 → 彻底隐藏，无条无占位。
   iOS Safari 支持 -webkit-touch-callout 而被排除：保留其系统 overlay 原生体验（自动显隐/可长按拖动/触感反馈）。
   Windows / macOS 桌面属 hover:hover + pointer:fine，不命中此媒体 → 走上方基础规则的常显细条（可拖动、可看进度） */
@supports not (-webkit-touch-callout: none) {
  @media (hover: none), (pointer: coarse) {
    .modal-body { scrollbar-width: none; scrollbar-color: transparent transparent; }
    .modal-body::-webkit-scrollbar { display: none; width: 0; height: 0; }
    }
}
/* ⚠️ 这里原先写 `.modal { max-width: 640px }`，会把 sm/md/lg/xl 四档在宽屏下一律压成 640，
     语义档形同摆设（实测标注弹窗被撑到 640、图片只剩 322 宽）。弹窗宽度只由调用方选档决定，
     媒体查询不再改宽度。 */
.modal-body .grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }
/* 弹窗内直接子字段统一间距：field 和 grid（字段组）之间都固定 14px，避免混排时忽大忽小 */
.modal-body > .field,
.modal-body > .grid { margin-top: 14px; }
.modal-body > .field:first-child,
.modal-body > .grid:first-child { margin-top: 0; }
.modal-body p { line-height: 1.6; color: var(--text); }
/* 弹窗内独立成行的说明小字：与上方字段留 6px（与 .field 内部 gap 同档），不贴控件底边 */
.modal-body > .form-hint { margin-top: 6px; }
/* 弹窗底部按钮栏：【弹窗里所有按钮都只出现在这一处】。
   结构统一为「左侧危险图标组 + 右侧常规动作组」：
   危险组用 margin-right: auto 把自己顶到最左端，取消/保存随 justify-content: flex-end 贴右。
   于是破坏性操作与「取消 / 保存」同处一条底栏，但水平方向被推到两端、
   与主按钮保持最大距离 —— 且危险动作是【纯图标小按钮】，双重防误触。 */
.modal-footer { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 10px; margin-top: 24px; flex-shrink: 0; }
/* 常规动作组（取消 + 主按钮）必须包成 .footer-actions 一组。
   ⚠️ 这一层不是为了好看，是为了让两个按钮【要么同排、要么整体换行】：
   若它们直接挂在 .modal-footer 下，窄屏换行时浏览器只会把后一个挤到下一行，
   结果是"取消与图标同排、保存修改独占一行"的歪版式（320px 实测踩到）。
   组内按钮一律等宽：标签都是 2~4 个汉字，min-width 取 96px 即可全部对齐
   （最长 4 字 ≈ 92px < 96px）。 */
.modal-footer .footer-actions { display: flex; align-items: center; gap: 10px; margin-left: auto; }
/* ⚠️ 高度必须【定死 40px】，不能只靠 padding + line-height 撑 ——
   后者算出来的高度常带亚像素小数（如 40.4），与危险组（图标按钮硬高 40.0）
   并排居中时就会差出 1px，肉眼看着就是"两组按钮没对齐"。
   定死之后两组都是精确 40，同一行居中即严格同高。 */
.modal-footer .footer-actions .btn { min-width: 96px; height: var(--ctl-h-lg); }
/* 弹窗「危险动作组」：删除项目 / 删除任务 / 禁用账号 / 删除用户。
   ⚠️ 它必须【待在 .modal-footer 之内】——早期版本放在 .modal-body 里、用 border-top 与表单分隔，
   结果删除按钮看着像表单内容而不是"操作"。
   危险色走语义令牌 var(--danger)（深色自动提亮为 #e25563），
   不要硬编码 #dc2626（它在深色卡片上只有 3.52:1）。 */
.modal-footer .modal-danger { display: flex; align-items: center; gap: 10px; margin-right: auto; }
/* 危险动作 = 【带方框的纯图标小按钮】。
   ⚠️ 尺寸必须与动作按钮完全一致：40×40（.btn 实测高 40px）——
   先前用 38px，比旁边的"取消 / 保存修改"矮 2px，看起来就是"按钮没对齐、很歪"。
   gap 也必须是 10px（与 .footer-actions 相同），否则会出现"左右间距不一样"。
   语义配色：删除是破坏性 → 实心红底 + 白色图标；
   可逆操作（用户弹窗的「⋯」，内含停用/启用 + 删除）→【中性色相】，但必须与邻居同"重量"：
   白底 + 细描边 + 深藏蓝图标，即与「取消」同一套外观（.btn-outline）。
   ⚠️ 这里曾写成 rgba(22,41,77,.08) 灰底 + var(--text-muted) 灰图标来表达"中性"，
   结果它比左右两颗明显发灰、读起来像 disabled 态（用户反馈"和隔壁两个按钮不太一样"）。
   **"中性"指不用红色色相，不是压低对比度** ——
   --text-muted 那一档是给"辅助文字"的，按钮图标应当与它旁边按钮的文字同档（--navy-700）。 */
.modal-footer .modal-danger .btn {
  flex: 0 0 auto;
  position: relative;
  width: var(--ctl-h-lg);
  height: var(--ctl-h-lg);
  min-width: 0;              /* 压过 .footer-actions .btn 的 96px */
  padding: 0;
  border: 1px solid var(--field-border); /* 与 .btn-outline 同款描边；全局 box-sizing: border-box 保证仍是 40×40 */
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--navy-700);
  transition: background-color var(--dur-1) var(--ease-std), border-color var(--dur-1) var(--ease-std), color var(--dur-1) var(--ease-std), filter var(--dur-1) var(--ease-std);
}
/* ⚠️ 必须用 :not(.danger-btn) 排除删除键 —— 这条深色规则的特异性 (0,3,1)
   高于 `.modal-footer .modal-danger .danger-btn` 的 (0,3,0)，
   不排除就会把删除键的【红底】在深色下刷成中性灰（白图标浮在灰底上）。 */
/* 深色下取 .btn-outline 的同一组令牌（--surface-2 / --field-border），保持"与取消同款" */
html[data-theme="dark"] .modal-footer .modal-danger .btn:not(.danger-btn) { background: var(--surface-2); border-color: var(--field-border); color: var(--text); }
.modal-footer .modal-danger .btn svg { width: 18px; height: 18px; }
/* 触控目标扩到 46px：视觉 40px 保持克制，可点面积仍达标准 */
.modal-footer .modal-danger .btn::before { content: ''; position: absolute; inset: -3px; }
/* 悬停与 .btn-outline:hover 完全一致：金描边 + 金色浅底 + 文字保持深藏蓝 + 轻投影。
   ⚠️ 不能拿金色当文字色（--gold-600 在白底上只有 2.75:1）。 */
.modal-footer .modal-danger .btn:hover:not(:disabled) { background: rgba(201, 169, 97, .14); border-color: var(--gold-500); color: var(--navy-700); box-shadow: 0 2px 10px rgba(201, 169, 97, .2); }
html[data-theme="dark"] .modal-footer .modal-danger .btn:not(.danger-btn):hover:not(:disabled) { background: rgba(201, 169, 97, .18); border-color: var(--gold-500); color: var(--text); }
.modal-footer .modal-danger .btn:disabled { opacity: var(--disabled-opacity-soft); }
/* 删除键：实心红底 + 白色图标。
   ⚠️ 这里【不再需要】html[data-theme="dark"] 变体 —— 历史上那套变体是为了救
   "透明底 + 红字"被 .btn-outline 复位反杀的问题。现在底色走 var(--danger) 令牌，
   深色下自动提亮为 #e25563，白图标在其上对比依然充足。 */
.modal-footer .modal-danger .danger-btn,
.modal-footer .modal-danger .danger-btn svg { color: #fff; }
/* 描边也必须染成红：基础规则现在带 1px 描边，不改的话实心红底外会留一圈中性浅边（发白的一环） */
.modal-footer .modal-danger .danger-btn { background: var(--danger); border-color: var(--danger); }
.modal-footer .modal-danger .danger-btn:hover:not(:disabled) {
  background: var(--danger);
  color: #fff;
  filter: brightness(.9);
}
/* ⚠️ 上面那两组 html[data-theme="dark"] 变体不能省：深色下 `html[data-theme="dark"] .btn-outline`
   的特异性是 (0,2,1)，会压过单层 .modal-danger .danger-btn 的 (0,2,0) ——
   表现为危险色丢掉、文字被拉回 var(--text)、透明底被拉回实底 #1a2642。
   ⚠️ 本段注释务必闭合，且【正文里不能出现注释结束符本身】——
   CSS 注释不支持嵌套，正文里一旦出现它就会提前终止本段，
   紧随其后的规则会被当成选择器的一部分而整条失效（不是"看起来没样式"，是规则被丢弃）。 */
.modal-footer .modal-danger .btn.active { background: rgba(201, 169, 97, .28); color: var(--text); }
html[data-theme="dark"] .modal-footer .modal-danger .btn.active { background: rgba(201, 169, 97, .34); }
/* ================= 弹窗内的「更多」二级卡片 =================
   用户编辑弹窗的底栏把"停用/启用 + 删除"收进一个「⋯」按钮，点开就是这张小卡片。
   外观与 .modal【全面对齐】——玻璃底、描边、圆角 16px、模糊与投影参数，
   深色也各取 .modal 的对应值。这样它读起来是"弹窗家族里的一层"，而不是又一套新浮层。
   ⚠️ 必须挂在 document.body 上：.modal 是 overflow:hidden，
   放进弹窗内部会被裁掉（底栏本来就贴底，卡片会直接看不见）。
   z-index 高于 .modal-mask（1000）。 */
.more-card {
  position: fixed;
  z-index: 1100;
  min-width: 172px;
  padding: 6px;
  border: 1px solid var(--glass-hairline);
  border-radius: 16px;               /* 与 .modal 一致 */
  background: var(--glass-tint);     /* 与 .modal 一致 */
  backdrop-filter: blur(var(--glass-blur-lg)) saturate(var(--glass-sat-strong));
  -webkit-backdrop-filter: blur(var(--glass-blur-lg)) saturate(var(--glass-sat-strong));
  box-shadow: 0 28px 64px -18px rgba(15, 30, 60, .3), 0 4px 14px -6px rgba(15, 30, 60, .12);
  animation: maskFade var(--dur-1) var(--ease-std);        /* 与遮罩同一段淡入，三端节奏一致 */
}
html[data-theme="dark"] .more-card {
  /* 2026-10-15 起与 .modal 完全统一（同一个 --glass-tint）。

     ⚠️ 它是全站浮层里面积最小的一个：深色下若觉得背后的字干扰可读性，

     请调 :root / 深色 :root 里的 --glass-tint（不要在这里另写数字）。 */
  background: var(--glass-tint);
  border-color: var(--glass-hairline);
  box-shadow: inset 0 1px 0 var(--glass-inset), 0 28px 64px -18px rgba(0, 0, 0, .6);
}
.more-card .more-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 44px;                     /* 触控标准（带说明文字时会自然增高） */
  padding: 6px 12px;
  border: 0;
  border-radius: 10px;
  background: none;
  font-size: 14px;
  color: var(--text);
  text-align: left;
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease-std);
}
.more-card .more-item svg { width: 17px; height: 17px; flex-shrink: 0; color: var(--text-muted); }
/* 菜单项文字区：标签 + 可选的原因说明（如"不能对自己的账号操作"）。
   ⚠️ 为什么用【可见文字】而不是 title 属性：铁律 #7 全站禁止原生悬停小方框，
   而 disabled 按钮不参与键盘聚焦，光靠 aria-label 也传不到用户耳朵里。
   颜色走 --text-muted（自身已适配深浅色），不写死色值。 */
/* 标签与说明之间 4px —— 与 .pref-txt / .vfx-txt / .card-desc / .cell-name .d 的
   「标题 + 说明」间距同为 4px（全站唯一一档，不再各写各的）。 */
.more-card .more-item .more-text { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; line-height: 1.2; }
.more-card .more-item .more-hint { font-size: 12px; line-height: 1.6; color: var(--text-muted); }
.more-card .more-item:hover:not(:disabled) { background: rgba(201, 169, 97, .14); }
/* 三项的语义色一律走【文字版】令牌（--*-ink）：
   卡面是接近白的玻璃底（rgba(255,255,255,.82)），实测原来的 --warning 只有 2.86:1、
   --success 3.35:1、--danger 4.38:1，当 14px 文字都不达 AA 的 4.5；
   换成 ink 版后为 5.0 / 5.3 / 4.8:1。深色主题里 ink 就等于原令牌（亮色配深底本就达标）。
   ⚠️ 必须与【列表行那三颗按钮】取同一个值 ——
   同一个动作在两处显示成两种黄/两种绿，是这一栏最容易看出来的不一致。
   ⚠️ 图标与文字【同色】是刻意的：这些项只有"图标 + 短标签"，
   图标若浅一档（默认的 --text-muted）整行看上去就发灰、像不可点。 */
.more-card .more-item.warn,
.more-card .more-item.warn svg { color: var(--warning-ink); }
.more-card .more-item.success,
.more-card .more-item.success svg { color: var(--success-ink); }
.more-card .more-item.danger,
.more-card .more-item.danger svg { color: var(--danger-ink); }
.more-card .more-item.warn:hover:not(:disabled) { background: rgba(180, 83, 9, .1); }
.more-card .more-item.success:hover:not(:disabled) { background: rgba(31, 122, 90, .1); }
.more-card .more-item.danger:hover:not(:disabled) { background: rgba(209, 59, 59, .1); }
html[data-theme="dark"] .more-card .more-item.warn:hover:not(:disabled) { background: rgba(216, 160, 60, .18); }
html[data-theme="dark"] .more-card .more-item.success:hover:not(:disabled) { background: rgba(52, 198, 138, .18); }
html[data-theme="dark"] .more-card .more-item.danger:hover:not(:disabled) { background: rgba(226, 85, 99, .16); }
.more-card .more-item:disabled { opacity: var(--disabled-opacity-soft); cursor: default; }
/* 弹窗表单控件：三端统一 38px 紧凑高度（桌面 / iPad / 手机一致） */
.modal-body .field .input,
.modal-body .field .select { height: var(--ctl-h); line-height: var(--ctl-h); padding: 0 12px; }
/* 弹窗下拉框：三端统一自定义箭头，去掉浏览器原生外观差异 */
.modal-body .field .select {
  -webkit-appearance: none;
  appearance: none;
  padding-right: 30px;
  background-image: var(--select-arrow);
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 10px 6px;
}
/* ================= 登录页（浅色玻璃） ================= */
.login-wrap {
  position: relative; min-height: 100vh; min-height: 100dvh; overflow: hidden;
  background: linear-gradient(135deg, var(--navy-800) 0%, var(--navy-900) 55%, #050a18 100%);
  display: flex; align-items: center; justify-content: center;
  isolation: isolate;
}
/* ============ 登录页 WebGL 液态金属流光背景（JANELON LIQUID METAL） ============
   规格落地：canvas 由 GPU fragment shader 实时生成大片不规则的水平液态金属色面
   （深藏蓝 → 蓝 → 钢蓝 → 古铜金 → 香槟金，最低层为墨底）——连续起伏、波长/波幅
   受低频噪声调制、三条流带互相穿插流动。本层【绝对不 blur / 不用 backdrop-filter /
   无 glow / 无 bloom / 无 screen】，玻璃质感全部由其上既有 .login-glass-mask 与
   登录卡承担。性能：WebGL2→WebGL1 自动降级、DPR≤2、切后台暂停。
   改 Logo 配色：见 assets/liquid-metal.js 片段着色器顶部的 navy900…gold2 常量。 */
.login-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
/* 背景容器：铺满视口、不挡交互、墨底兜底 */
.dynamic-flow-background {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
  isolation: isolate;
  filter: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  mix-blend-mode: normal;
  background: #0a1428; /* 墨底，与 .login-wrap 原有最深色一致 */
}
/* WebGL 画布：铺满容器、绝对不 blur */
.dynamic-flow-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  filter: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  transform: translate3d(0, 0, 0);
  will-change: contents;
}
/* 登录页玻璃遮罩：与站内弹窗遮罩 .modal-mask 取同一组值
   （var(--glass-veil-sm) + blur(var(--glass-blur-sm)) saturate(var(--glass-sat-soft))），压在动态流光之上、
   登录卡之下 —— 底部清晰色流透过此层被实时柔化成磨砂玻璃质感 */
.login-glass-mask {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: var(--glass-veil-sm);
  backdrop-filter: blur(var(--glass-blur-sm)) saturate(var(--glass-sat-soft));
  -webkit-backdrop-filter: blur(var(--glass-blur-sm)) saturate(var(--glass-sat-soft));
  transition: background var(--dur-3) var(--ease-std), backdrop-filter var(--dur-3) var(--ease-std);
}
html[data-theme="dark"] .login-glass-mask { background: var(--glass-veil-sm); }
/* 整体柔光覆盖，避免对比过强 */
.login-wrap::after {
  content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: radial-gradient(ellipse at center, transparent 0%, rgba(5, 10, 24, .35) 100%);
}
.login-main {
  position: relative; z-index: 1;
  width: 100%; padding: 32px 20px;
  display: flex; align-items: center; justify-content: center;
}
/* 白雾透登录卡片（浅色主题默认，与深蓝背景形成柔和对比） */
.login-card {
  width: 100%; max-width: 440px;
  background: linear-gradient(160deg, var(--glass-tint-card-a) 0%, var(--glass-tint-card-b) 100%);
  backdrop-filter: blur(var(--glass-blur-lg)) saturate(var(--glass-sat-strong)); /* 与站内弹窗 .modal 同一组值 */
  -webkit-backdrop-filter: blur(var(--glass-blur-lg)) saturate(var(--glass-sat-strong));
  border: 1px solid var(--glass-hairline);
  box-shadow:
    0 30px 80px -20px var(--login-shadow),
    0 1px 0 var(--glass-inset) inset,
    0 0 60px var(--login-glow-inset) inset;
  border-radius: 22px;
  padding: 34px 36px 30px;
  /* 性能档位若在登录页即时应用（首次探测/手动重测），玻璃 ↔ 实底切换带平滑过渡，降低偶发合成异常 */
  transition: background var(--dur-3) var(--ease-std), border-color var(--dur-3) var(--ease-std), box-shadow var(--dur-3) var(--ease-std), backdrop-filter var(--dur-3) var(--ease-std);
}
.login-card .login-brand {
  display: flex; align-items: center; gap: 12px; justify-content: center;
  padding-bottom: 20px; margin-bottom: 22px;
  border-bottom: 1px solid var(--login-hairline);
}
/* 双层投影（金色外发光）：原尾部第二处定义，已合并 */
.login-card .login-brand img { width: 40px; height: 40px; object-fit: contain; filter: drop-shadow(0 4px 12px var(--login-logo-glow)) drop-shadow(0 0 18px rgba(201, 169, 97, .35)); }
.login-card .login-brand .name { font-size: 17px; font-weight: 700; color: var(--login-ink); letter-spacing: .02em; line-height: 1.4; }
.login-card .login-brand .sub { font-size: 11px; color: var(--login-ink-sub); margin-top: 4px; letter-spacing: .1em; }
.login-card h2 { font-size: 23px; font-weight: 700; color: var(--login-ink); margin-bottom: 4px; text-align: center; letter-spacing: .02em; }
/* 说明文字常换行 → 显式「说明段」1.6 档（规范 §2.3），不靠全局继承 */
.login-card > p { color: var(--login-ink-body); margin-bottom: 24px; font-size: 13px; line-height: 1.6; text-align: center; }
.login-form { display: flex; flex-direction: column; gap: 14px; }
.login-form .btn { height: var(--ctl-h-xl); font-size: 14px; margin-top: 6px; }
/* 白雾透卡片内的表单控件 */
.login-card .field label { color: var(--login-label); }
.login-card .input {
  /* 高度与站内控件统一：原先靠 base .input 的 9px 上下内边距 + 行高撑出 39px，

     比 --ctl-h=38px 多 1px。写法与 .toolbar .input 一致（定高 + 上下内边距归零）。 */
  height: var(--ctl-h);
  padding-top: 0; padding-bottom: 0;
  background: var(--login-field-bg);
  border: 1px solid var(--login-field-border);
  color: var(--login-ink);
  box-shadow: 0 1px 2px var(--login-field-inset) inset;
}
.login-card .input::placeholder { color: var(--login-placeholder); }
.login-card .input:focus { border-color: var(--gold-500); background: var(--login-field-bg-focus); box-shadow: 0 0 0 3px var(--login-focus-ring); }
/* 登录页：按钮辉光 + logo 光晕 */
.login-card .btn-primary {
  background: linear-gradient(135deg, var(--gold-500) 0%, var(--login-btn-grad-b) 100%);
  color: var(--login-btn-ink); border: none;
  /* 内描边高光：原尾部第二处定义，已合并 */
  box-shadow: 0 8px 20px -6px var(--login-btn-glow), 0 0 0 1px rgba(255, 255, 255, .16) inset;
}
@media (hover: hover) {
  .login-card .btn-primary:hover { filter: brightness(1.06); box-shadow: 0 10px 24px -6px rgba(184, 146, 63, .55); }
}
/* ================= 登录页 · 手机横屏适配 =================
   手机横屏可视高度只有 320~430px，而登录卡按常规尺寸排布约需 486px
   （main padding 64 + card padding 64 + 品牌区 82 + 标题 35 + 说明 40 + 表单 202），
   会把「登录」按钮直接挤到屏幕外。
   而 .login-wrap 原为 overflow:hidden，移动端 html/body 也是 overflow:hidden，
   超出部分连滚动都做不到 —— 属于硬缺陷，不只是观感问题。
   这里整体收紧间距与控件尺寸（约 353px，393px 高的主流机型可完整放下），
   并在极矮视口（小屏机横屏）下允许卡片区内部滚动兜底。 */
@media (orientation: landscape) and (max-height: 560px) {
  /* 纵向滚动兜底：用 height 而非 min-height，让滚动发生在元素内部
     （移动端 html/body 都是 overflow:hidden，元素自己不滚就没救了）。
     align-items 由 center 改为 flex-start，配合 .login-main 的 margin:auto：
     内容不足时仍然垂直居中，内容超出时顶部不会被裁掉（flex 居中的经典陷阱）。 */
.login-wrap { height: 100dvh; overflow-y: auto; overflow-x: hidden; align-items: flex-start; }
  .login-main { padding: 14px 20px; margin: auto; }
  /* 水平与卡片同轴（本断点卡片 = 16px） */
.login-card { padding: 18px 26px 16px; max-width: 400px; }
  .login-card .login-brand { padding-bottom: 10px; margin-bottom: 12px; }
  .login-card .login-brand img { width: 30px; height: 30px; }
  .login-card .login-brand .name { font-size: 14px; }
  .login-card h2 { font-size: 18px; }
  .login-card > p { margin-bottom: 12px; font-size: 12px; }
  .login-form { gap: 8px; }
  .login-form .btn { height: var(--ctl-h-lg); margin-top: 2px; }
}
/* 深色模式：登录卡片切换为深色玻璃（跟随网站系统设置） */
html[data-theme="dark"] .login-wrap { background: var(--login-page-grad); }
html[data-theme="dark"] .login-card {
  background: linear-gradient(160deg, var(--glass-tint-card-a) 0%, var(--glass-tint-card-b) 100%);
  border-color: var(--glass-hairline);
  box-shadow:
    0 30px 80px -20px var(--login-shadow),
    0 1px 0 var(--glass-inset) inset,
    0 0 60px var(--login-glow-inset) inset;
}
html[data-theme="dark"] .login-card .login-brand { border-bottom-color: var(--login-hairline); }
html[data-theme="dark"] .login-card .login-brand .name,
html[data-theme="dark"] .login-card h2 { color: var(--login-ink); }
html[data-theme="dark"] .login-card .login-brand .sub { color: var(--login-ink-sub); }
html[data-theme="dark"] .login-card > p { color: var(--login-ink-body); }
html[data-theme="dark"] .login-card .field label { color: var(--login-label); }
html[data-theme="dark"] .login-card .input {
  background: var(--login-field-bg);
  border-color: var(--login-field-border);
  color: var(--login-ink);
  box-shadow: 0 1px 2px var(--login-field-inset) inset;
}
html[data-theme="dark"] .login-card .input::placeholder { color: var(--login-placeholder); }
html[data-theme="dark"] .login-card .input:focus {
  border-color: var(--gold-500);
  background: var(--login-field-bg-focus);
  box-shadow: 0 0 0 3px var(--login-focus-ring);
}
/* ================= 布局 ================= */
/* box-sizing 必须为 border-box：下方 @supports 块会给 .layout 加安全区内边距，
   而它同时固定了 height: 100vh/100dvh，若用 content-box 会把总高撑破、整页出现纵向溢出。 */
.layout { display: flex; height: 100vh; height: 100dvh; overflow: hidden; box-sizing: border-box; }
.sidebar {
  width: 236px;
  background: linear-gradient(180deg, #122a52 0%, #0a1a38 100%);
  border-right: 1px solid rgba(255, 255, 255, .06);
  color: rgba(255, 255, 255, .88);
  display: flex; flex-direction: column;
  position: fixed; top: 0; bottom: 0; left: 0; z-index: 50;
  box-shadow: 6px 0 24px -12px rgba(10, 20, 40, .35);
}
/* 品牌区在手机/平板端（抽屉模式）：仅调整边距与图标尺寸，
     字号/行高/字距与桌面端完全一致，保证三端视觉统一 */
.sidebar-brand { display: flex; align-items: center; gap: 12px; padding: 18px 20px 16px; border-bottom: 1px solid rgba(255, 255, 255, .08); }

.sidebar-brand img { width: 38px; height: 38px; object-fit: contain; filter: drop-shadow(0 3px 10px rgba(201, 169, 97, .4)); }
/* 2. 半像素碎字号清零（侧栏 / 登录页历史值 15.5 / 11.5 / 10.5） */
.sidebar-brand .bn { display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 700; color: #fff; letter-spacing: .06em; line-height: 1.4; }
.sidebar-brand .bs { font-size: 11px; opacity: .55; margin-top: 4px; letter-spacing: .08em; }
.sidebar-nav { flex: 1; overflow-y: auto; padding: 16px 12px; }
.nav-group { font-size: 11px; text-transform: uppercase; letter-spacing: .14em; color: rgba(255, 255, 255, .5); padding: 14px 10px 8px; font-weight: 600; }
.nav-item {
  display: flex; align-items: center; gap: 12px;
  /* 曾为 10.5px —— 全站唯一的半像素值，会让行高落在半像素上、渲染时出现亚像素错位，

     2026-10-15 对齐检查归整为 10px（行高 41 → 40） */
  padding: 10px 12px; border-radius: 10px; margin-bottom: 4px;
  color: rgba(255, 255, 255, .86); text-decoration: none; font-size: 14px; line-height: 1.4;
  transition: background var(--dur-1) var(--ease-std), padding-left var(--dur-2) var(--ease-out), color var(--dur-1) var(--ease-std); border: none; background: none; width: 100%; text-align: left; position: relative;
}
/* 5. 侧栏项与统计卡：悬停时分别有 padding 左移、上浮位移，同样会被 sticky hover 粘住 */
.nav-item:hover { background: rgba(255, 255, 255, .07); color: #fff; padding-left: 16px; }
.nav-item .ico { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; transition: transform var(--dur-2) var(--ease-pop); }
.nav-item:hover .ico { transform: translateX(2px) scale(1.06); }
.nav-item.active .ico { animation: iconPop var(--dur-4) var(--ease-pop); }
@keyframes iconPop { 0% { transform: scale(1); } 50% { transform: scale(1.07); } 100% { transform: scale(1); } }
.nav-item:active { transform: scale(.98); }
/* 侧边栏激活项辉光 */
.nav-item.active { background: linear-gradient(90deg, rgba(201, 169, 97, .24), rgba(201, 169, 97, .06)); color: #fff; font-weight: 600; box-shadow: 0 6px 18px -8px rgba(201, 169, 97, .4), inset 0 0 16px rgba(201, 169, 97, .08); }
.nav-item.active::before { content: ''; position: absolute; left: 0; top: 8px; bottom: 8px; width: 3px; border-radius: 3px; background: linear-gradient(180deg, var(--gold-400), var(--gold-600)); }
.nav-item .ico svg { width: 18px; height: 18px; }
.nav-item.active .ico svg { stroke: var(--gold-400); }

/* 只读账号块整体不可点（原尾部兜底覆盖，已合并）：光标与悬停一律保持静态 */
.sidebar-user { padding: 14px; border-top: 1px solid rgba(255, 255, 255, .08); cursor: default; transition: background var(--dur-2) var(--ease-std); }
.sidebar-user:hover { background: transparent; }
.sidebar-user .urow { display: flex; align-items: center; gap: 10px; }
.sidebar-user .uname { font-size: 14px; font-weight: 600; color: #fff; }
.sidebar-user .utitle { font-size: 12px; opacity: .75; margin-top: 1px; }
.sidebar-user .urow .avatar { width: 38px; height: 38px; font-size: 14px; }
/* .main 作为 .topbar 绝对定位的参照系（原尾部 .main 覆盖层，已合并） */
.main { position: relative; flex: 1; margin-left: 236px; display: flex; flex-direction: column; min-width: 0; height: 100%; overflow: hidden; }
/* 顶栏：质量档 ↔ 性能档切换时，实底与雾面之间同样走 var(--dur-3)，避免瞬时跳变
   （底栏是实底、无 backdrop-filter，不需要这条过渡） */
.topbar {
  /* 高度不含安全区 —— 状态栏那块区域【不归顶栏】，而是让给 body 背景。

     做法是整体下移（见 .layout 的 padding-top），顶栏仍从安全区之下开始，高度恒为 62px。

     为什么不让顶栏向上盖住状态栏：Safari 状态栏的底色是对页面顶部【采样后平滑过渡】的，

     它会随页面一起明暗变化（侧栏遮罩展开时可见），但永远追不上我们用固定色画出来的顶栏，

     两者注定对不齐。让那块区域露出 body 背景才能天然连续。 */
  height: 62px;
  /* 纯色实底，颜色来自 --topbar-bg（全站唯一来源，详见 :root 处说明）。

     该值 = 页面渐变在视口顶部区域（0~62px）的【实测平均色】：

       浅色 #f4f5f7（同区域背景实测 #f4f6f8，差 2）

       深色 #0a0f17（同区域背景实测 #090e17，差 2）

     与下方内容相比接缝色差仅 2（满量程 765），肉眼不可见。



     ⚠️ 不要改回「渐变首色 #f6f8fc / #060b15」：顶栏区域还叠着右上角的金色径向光晕

     （浅色 .08 / 深色 .05），实际色比首色偏暖偏亮（深色差达 10），暗色下肉眼可见。



     为什么用纯色而不是复刻背景渐变：复刻需依赖 background-attachment，而 iOS Safari

     不支持该属性、sticky 元素上各家实现也不一致；一旦失效，整条渐变会被压进 62px 高度里，

     顶栏自己上下就出现色带。纯色零依赖 —— 这也正是底栏的做法（取渐变末端区域的实际色）。 */
  /* 2026-10-15 毛玻璃实验：第一行是实底兜底（= 实验前的观感），

     第二行 color-mix 让它在支持该函数的浏览器上变成"同色 84% 不透明"的雾面；

     模糊用浮层同一档 --glass-blur-lg，【不加 saturate】（避免把浅蓝灰拉浓，见 :root 说明）。 */
  background: var(--topbar-bg);
  background: color-mix(in srgb, var(--topbar-bg) 84%, transparent);
  -webkit-backdrop-filter: blur(var(--glass-blur-lg));
  backdrop-filter: blur(var(--glass-blur-lg));
  border-bottom: 1px solid rgba(31, 58, 110, .07);
  display: flex; align-items: center; padding: 0 26px; gap: 16px;
  /* 布局与主题切换过渡（原尾部两处 .topbar 覆盖层，已合并到此） */
  position: absolute; top: 0; left: 0; right: 0; width: 100%; z-index: 40;
  transition: background var(--dur-3) var(--ease-std), border-color var(--dur-3) var(--ease-std), box-shadow var(--dur-3) var(--ease-std), backdrop-filter var(--dur-3) var(--ease-std);
}
.topbar .crumb { font-size: 12px; color: var(--text-muted); line-height: 1.5; }
.topbar .crumb b { color: var(--gold-600); font-weight: 600; }
.topbar .title { font-size: 18px; font-weight: 700; color: var(--navy-800); line-height: 1.4; letter-spacing: .01em; }
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 10px; }
/* 顶栏已改为绝对定位，内容区用 padding-top 让出顶栏高度（原尾部 .content 覆盖层，已合并） */
.content { flex: 1; min-height: 0; padding: 20px 24px; padding-top: calc(var(--topbar-h, 62px) + 20px) !important; overflow: auto; overscroll-behavior: contain; }
/* 移动端适配 */
.mobile-toggle { display: none; }
/* 侧边栏抽屉遮罩：展开时显示并拦截背后内容点击；点击遮罩收回侧边栏，
   淡入淡出动画与 sidebar 的 0var(--dur-2) var(--ease-std) 保持一致（桌面端从不加 .open，遮罩永不显示） */
.sidebar-mask {
  position: fixed; inset: 0; z-index: 49;
  background: var(--glass-veil-md);
  -webkit-backdrop-filter: blur(var(--glass-blur-xs));
  backdrop-filter: blur(var(--glass-blur-xs));
  opacity: 0; visibility: hidden;
  /* 主题切换过渡（原第二处 .sidebar-mask 定义，已合并） */
  transition: opacity var(--dur-2) var(--ease-std), visibility var(--dur-2) var(--ease-std), background var(--dur-3) var(--ease-std), backdrop-filter var(--dur-3) var(--ease-std);
  touch-action: pan-y;
}
.sidebar.open ~ .sidebar-mask { opacity: 1; visibility: visible; }
/* ================= 底部导航（竖屏专属，见下方竖屏媒体查询） =================
   定位固定于视口底部，是 .layout（display:flex）之外的独立图层 —— fixed 元素脱离文档流，
   因此不会参与 .layout 的 flex 排布。
   层级 z-index:45：高于下拉检索面板(30)与顶栏(40)，低于侧栏遮罩(49)、侧栏(50)、弹窗遮罩(1000)，
   保证抽屉/弹窗打开时它被正确压住。
   ⚠️ 这里曾写着"下拉检索面板(99999)能盖住它"—— 那是被推翻的设计：
   检索面板是【临时内容】，顶栏/底栏是【常驻框架】，框架不该被一张下拉卡片盖住
   （用户明确指出）。故面板已降到 30，永远低于底栏。 */
.tabbar {
  display: none;
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 45;
  /* 底色取页面渐变的最末端色（--tabbar-bg：浅 #edf1f8 / 深 #070e1b），与紧邻的上方内容区同色。

  ⚠️⚠️ 底栏【禁止】加 backdrop-filter —— 这是硬约束，不是风格偏好：

    backdrop-filter 会为元素创建独立合成层，而 Safari 为【底部地址栏】取色时看到的是

    "最顶层的绘制结果"＝这一层，因而无法判定单一颜色、退回默认值，

    表现就是"切换深浅色后浏览器底部不跟随"（只有刷新/打开侧栏时才顺带更新）。

    2026-10-15 曾把顶栏与底栏一起做成同色毛玻璃，当天即因此回退底栏；顶栏不受影响

    （底部取色只看页面最底部，1961c8a 实测结论）。

    2026-10-09 已经验证过三条"兼得"路径全部不成立：① 底栏下方垫纯色层 ② 把毛玻璃移到

    子元素 ③ 提高不透明度 —— 只要 backdrop-filter 存在就会建层，与透明度无关。

    规范出处：§1.1「浏览器 chrome 取色」与 §3 玻璃小节。 */
  background: var(--tabbar-bg);
  border-top: 1px solid rgba(31, 58, 110, .07);
  box-shadow: 0 -10px 28px -18px rgba(31, 58, 110, .28);
  padding-bottom: env(safe-area-inset-bottom); /* iPhone 底部横条：避让，否则最下面的图标点不到 */
}
.tabbar-item {
  /* 高度 54px、图标 22px、文字 11px（字号阶梯最低档，原 10px 不在阶梯内）。

     微信 tab bar 是 49pt，但它与 iOS 底部安全区融为一体；Web 页面没有这层融合，

     反而会被浏览器自身的底部工具栏"切"掉下半截，所以略高一点更协调。

     4 项等分铺满，图标与文字上下居中 */
  flex: 1 1 0; min-width: 0; height: 54px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  border: none; background: none; cursor: pointer; padding: 0;
  color: var(--text-muted); font-size: 11px; line-height: 1.2; font-family: inherit;
  transition: color var(--dur-1) var(--ease-std);
  -webkit-tap-highlight-color: transparent;
}
.tabbar-item .ico { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; }
.tabbar-item .ico svg { width: 22px; height: 22px; }
.tabbar-item.active { color: var(--gold-600); font-weight: 600; }
.tabbar-item:active { transform: scale(.96); }
html[data-theme="dark"] .tabbar {
  /* 深色底栏色已收进 --tabbar-bg（#070e1b），故此处【不再声明 background】——

     声明了会以更高特异性压掉 .tabbar 里的 background: var(--tabbar-bg)，色值就脱离单一来源。

     （原注释写"会压掉 color-mix、深色毛玻璃就没了" —— 那是 2026-10-09～10-15 期间的旧实现；

     底栏的 color-mix 已随 2026-10-15「底栏回退实底」一并移除，此处注释更正，勿再按旧描述理解。）

     去掉原来的 inset 内白线，与顶栏（已无内白线）保持一致 */
  border-top-color: rgba(255, 255, 255, .07);
  box-shadow: 0 -10px 28px -18px rgba(0, 0, 0, .6);
}
html[data-theme="dark"] .tabbar-item.active { color: var(--gold-400); }
/* 抽屉断点：侧栏滑出/滑入 + 内容区让位。
   动画沿用侧栏那一套：var(--dur-2) var(--ease-std)。
   ⚠️ 项目里【没有全局动画令牌】——时长都是逐组件写的，这里与侧栏取同一个值，
   改一处就要一起改（若日后引入全局令牌，这几处应一起换掉：.sidebar / .sidebar-mask）。 */
@media (max-width: 900px) {
  .sidebar { transform: translateX(-100%); transition: transform var(--dur-2) var(--ease-std); box-shadow: none; overscroll-behavior: contain; }
  .sidebar-nav { overscroll-behavior: contain; touch-action: pan-y; }
  .sidebar.open { transform: translateX(0); box-shadow: 20px 0 60px rgba(10, 20, 40, .3); }
  /* ================= 顶栏雾透落地 =================
   之前内容区(.content)从顶栏下方才开始，内容永远不经过顶栏背后，backdrop-filter 无内容可模糊，
   所以顶栏看着"没有雾透"（并非 blur 参数问题）。
   现在：顶栏绝对悬浮于内容区顶部，内容区满高滚动并预留顶部空间（--topbar-h + 间距），
   页面滚动时内容会从顶栏背后经过，毛玻璃雾透即可生效。深浅色沿用弹窗同套参数。 */
.main { margin-left: 0; }
  .mobile-toggle { display: inline-flex; }
  /* 内容区不播放整页淡入：父级 opacity 动画会让其中的下拉检索面板"渐显"；移除后全站检索栏
   打开均为即时闪现，行为完全一致（保留表格行 / 甘特任务条等局部动效） */
.content { padding: 16px; }
  .sidebar-brand { padding: 16px 18px 14px; }
  .sidebar-brand img { width: 36px; height: 36px; }
}
/* ================= 窄屏专用布局（底部导航 + 移动端工具栏） =================
   判定条件：max-width: 900px 且 min-height: 540px —— 刻意【不用】orientation。
   原因：折叠屏（Galaxy Z Fold 内屏约 870×725）展开后长宽比接近 1:1，再减去浏览器
   UI 占掉的高度，可视区往往变成"宽略大于高"，被 CSS 判定为 orientation: landscape，
   底栏就会莫名消失 —— 但用户手持姿态是竖直的，认知上就是竖屏。
   本质上，底栏该不该出现取决于【可视高度够不够】，而不是方向：
   横屏手机与折叠屏的差别从来不在方向，而在前者高度只有 393px、后者有 725px。
   改为按高度判断后各形态都正确：
     · 手机竖屏  393×852  宽≤900 高≥540 → 有底栏 ✓
     · 手机横屏  852×393  高 393<540    → 无底栏 ✓（高度不够，这是有意的）
     · 折叠屏展开 870×725  宽≤900 高≥540 → 有底栏 ✓（此前会消失，即本次修复）
     · 折叠屏折叠态横屏 832×375  高<540  → 无底栏 ✓
     · 平板竖屏  768×1024 宽≤900 高≥540 → 有底栏 ✓
     · 平板横屏  1024×768 宽>900        → 桌面布局 ✓
   540px 与既有横屏断点（max-height: 540px）保持一致。 */
@media (max-width: 900px) and (min-height: 540px) {
  /* 底部导航：4 个高频一级入口。低频入口（用户账户、系统设置）仍在侧栏抽屉中。 */
.tabbar { display: flex; }
  /* 内容区为固定底栏让出空间（含 iPhone 底部安全区），避免最后一行被压住。
     !important 用于压过后续 ≤600px 媒体查询里的 .content { padding: 14px }。 */
.content { padding-bottom: calc(54px + env(safe-area-inset-bottom) + 16px) !important; }
  /* 工具栏内所有控件（关键词框 / 下拉 / 动作按钮）一律通栏 —— 形成整齐的单列节奏。
     必须用 !important：部分控件在 HTML 上带了内联宽度，内联样式优先级高于类选择器，
     不覆盖就会出现"有的通栏、有的只占一小块"的参差观感。
     注：曾把新建按钮做成顶栏右上角的「页面主操作位」，实测引导不明显、同事找不到入口，已回退。 */
.toolbar > .input,
  .toolbar > .select,
  .toolbar > .btn {
    flex: 1 1 100% !important;
    width: 100% !important;
    max-width: none !important;
  }
  /* 通栏后 .spacer 已无意义；它还会因 flex 换行占位，凭 gap 多撑出一行高度 */
.toolbar > .spacer { display: none; }
}
/* ================= 首页 ================= */
.welcome-banner {
  position: relative; overflow: hidden;
  background:
    radial-gradient(600px 300px at 90% -20%, rgba(201, 169, 97, .3), transparent 60%),
    linear-gradient(135deg, var(--navy-700), var(--navy-900));
  /* 水平内边距与卡片体系对齐：原 32px 比卡片多缩进 9px（实测 292 vs 283），2026-10-15 统一为 20px */
  color: #fff; border-radius: 16px; padding: 28px 20px;
  border: 1px solid rgba(201, 169, 97, .35);
  display: flex; align-items: center; justify-content: space-between; gap: 24px;
  margin-bottom: 20px;
  box-shadow: 0 14px 36px rgba(15, 30, 61, .22);
}
.welcome-banner h2 { font-size: 23px; font-weight: 700; margin-bottom: 8px; letter-spacing: .01em; }
/* 欢迎横幅长句不超出窄屏（避免在 ≤560 视口宽度上被截断） */
.welcome-banner p { opacity: .82; font-size: 14px; line-height: 1.6; max-width: 560px; }
/* 首页顶部 5 张统计卡：固定 5 列等宽（不再 auto-fit 随窗口自动换列，
   避免中途出现 4+1 / 2+2+1 等孤卡悬空的"猎奇"断行；降列只在下方明确断点发生） */
.stat-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { padding: 20px; display: flex; align-items: center; gap: 16px; }
.stat-card .ico {
  width: 48px; height: 48px; border-radius: 13px;
  display: flex; align-items: center; justify-content: center;
  color: #fff; box-shadow: 0 6px 14px rgba(15, 30, 61, .14);
  transition: transform var(--dur-2) var(--ease-pop), box-shadow var(--dur-2) var(--ease-std);
}
.stat-card .ico svg { width: 22px; height: 22px; }
.stat-card .num { font-size: 25px; font-weight: 700; line-height: 1.2; color: var(--navy-800); letter-spacing: -.25px; }
/* 统计卡小字：12px 元信息取 normal 1.5（基座未声明行高） */
.stat-card .lbl { font-size: 12px; line-height: 1.5; color: var(--text-muted); margin-top: 4px; letter-spacing: .02em; }
.dash-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 16px; }
.dash-col { display: flex; flex-direction: column; gap: 16px; }
/* 卡头定高 60px、上下不留内边距（原尾部第二处 .card-head 覆盖，已合并） */
.card-head { padding: 0 20px; min-height: 60px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
.card-head h3 { font-size: 16px; font-weight: 700; color: var(--navy-800); letter-spacing: .01em; }
.card-head h3::before { content: ''; display: inline-block; width: 3px; height: 14px; border-radius: 3px; background: linear-gradient(180deg, var(--gold-500), var(--gold-600)); margin-right: 8px; vertical-align: -2px; }
.card-head .more { color: var(--gold-600); font-size: 13px; text-decoration: none; font-weight: 500; }
.card-head .more:hover { color: var(--navy-700); }
html[data-theme="dark"] .card-head .more { color: var(--gold-400); }
html[data-theme="dark"] .card-head .more:hover { color: var(--text); }
.card-body { padding: 12px 20px 20px; }
/* 卡体内「字段 / 字段组」的间距：与 .modal-body 同构（14px，首项归零）。
   此前为 0 —— 上一个字段的说明小字会直接贴住下一行的 label。 */
.card-body > .field,
.card-body > .grid { margin-top: 14px; }
.card-body > .field:first-child,
.card-body > .grid:first-child { margin-top: 0; }
/* 卡体内独立成行的说明小字：与上方字段留 6px（与 .field 内部 gap 同档） */
.card-body > .form-hint { margin-top: 6px; }
.dist-list { display: flex; flex-direction: column; gap: 8px; }
.dist-item { display: flex; align-items: center; gap: 10px; font-size: 13px; line-height: 1.4; }
/* 状态分布可点击：跳项目管理并筛选对应状态，悬停给可点反馈（与统计卡一致的金色系） */
.dist-item[data-stat-go] { cursor: pointer; padding: 6px 8px; margin: -6px -8px; border-radius: 8px; transition: background var(--dur-1) var(--ease-std); }
.dist-item[data-stat-go]:hover { background: rgba(201, 169, 97, .1); }
html[data-theme="dark"] .dist-item[data-stat-go]:hover { background: rgba(255, 255, 255, .06); }
.dist-item .dot { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; background: var(--gold-500); }
.dist-item .cnt { margin-left: auto; font-weight: 700; color: var(--navy-800); letter-spacing: -.01em; }
.dist-bar { height: 6px; background: #e9edf5; border-radius: 999px; overflow: hidden; flex: 1; }
/* 概览条目的柱条与色块：统一取品牌金（Top N 的长度本身就是区分度，
   原先按序号分配彩虹色 → 出现青 / 紫 / 粉等非语义色相，且深色下深藏蓝柱条看不见）。 */
.dist-bar > div { height: 100%; border-radius: 999px; background: var(--gold-500); }
/* text-align:center 与 .badge 同步：胶囊 / 徽标在卡头里会继承父级 start，在 td 里继承 center，
   全站实测出现 325 center + 6 start 的少数派。显式声明后两种位置一致（单行文字视觉不变）。 */
.status-chip { display: inline-block; vertical-align: middle; font-size: 12px; padding: 3px 12px; border-radius: 999px; font-weight: 500; white-space: nowrap; line-height: 1.5; text-align: center; }
.t { font-size: 12px; color: var(--text-muted); }
/* 行内辅助说明（12px 元信息）取 normal 1.5 —— 基座只声明了字号与颜色 */
/* 说明小字统一规格：12px / 1.5 —— 与 .pref-hint / .pref-desc / .vfx-desc / .vfx-current 同档。
   只保留「定位」差异（字段下方 / 工具条内 / 偏好行内），字号与行高全站只有这一套。
   原先的 -2px 上边距是"贴紧 label"的旧调法，会让说明粘在控件底边上，现已归零。 */
.form-hint { font-size: 12px; line-height: 1.6; color: var(--text-muted); margin-top: 0; }
/* ================= 项目管理 ================= */
.toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
.toolbar .spacer { flex: 1; }
.toolbar .select, .toolbar .input { width: auto; height: var(--ctl-h); }
/* select native 渲染文字基线偏下，用 line-height 顶开 + 上下 padding 0 强制垂直居中（input 无此问题） */
.toolbar .select { line-height: var(--ctl-h); padding-top: 0; padding-bottom: 0; text-align: left; }
.toolbar .btn { height: var(--ctl-h); padding-top: 0; padding-bottom: 0; }
/* ---------- A. 容器与表格元素重置 ---------- */
/* 容器不再成卡片：卡片化后【每一行就是一张卡】，
     外层再套一层会形成"卡中卡"，且它的毛玻璃会让卡片底色发灰 ——
     这正是"卡片外围多出一个矩形底衬"的来源。 */
.table-card { overflow: hidden; }
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
thead th {
  /* 表头统一居中（原第二处 thead th 覆盖，已合并到此） */
  text-align: center; font-size: 12px; font-weight: 600; color: var(--text-muted);
  /* 13px → 14px：与 tbody td 的上下内边距一致（实测表头/表体存在 1px 节奏差）；

     vertical-align 显式写 middle（与 td 同基准，此前靠 UA 默认值，不够保险） */
  padding: 14px 16px; background: #f4f7fb; vertical-align: middle;
  border-bottom: 1px solid var(--border);
  white-space: nowrap; letter-spacing: .02em;
}
/* 操作列常驻：仅当表格含操作列（.cell-actions，管理员视角）时，最后一列 sticky 固定在右侧 */
.table-card table { border-collapse: separate; border-spacing: 0; }
.table-card table:has(.cell-actions) thead th:last-child { position: sticky; right: 0; background: #f4f7fb; z-index: 3; }
/* 解除 sticky 操作列：卡片模式下会残留定位与投影 */
.table-card table:has(.cell-actions) tbody td:last-child { position: sticky; right: 0; background: var(--card); z-index: 2; box-shadow: -1px 0 0 var(--border), -10px 0 16px -14px rgba(31, 58, 110, .3); }
.table-card table:has(.cell-actions) tbody tr:hover td:last-child { background: #f6f8fc; }
/* sticky 列头必须【不透明】（否则横向滚动时下面的行会透出来）：
   取"半透明白 .05 叠在卡片实底 #121c31 上"的等效实色，与表头带子同色 */
html[data-theme="dark"] .table-card table:has(.cell-actions) thead th:last-child { background: var(--surface-3); }
html[data-theme="dark"] .table-card table:has(.cell-actions) tbody td:last-child { background: var(--card); box-shadow: -1px 0 0 var(--border), -10px 0 16px -14px rgba(0, 0, 0, .6); }
html[data-theme="dark"] .table-card table:has(.cell-actions) tbody tr:hover td:last-child { background: var(--primary-50); }
/* 表格横向滚动条隐藏：保留触摸/滚轮滚动，视觉更干净 */
.table-card .table-wrap { scrollbar-width: none; }
.table-card .table-wrap::-webkit-scrollbar { display: none; }
tbody td { padding: 14px 16px; border-bottom: 1px solid #edf1f7; vertical-align: middle; text-align: center; }
/* 项目管理表表头排序：可点击，箭头指示当前排序字段与方向 */
.proj-table th[data-sort] { cursor: pointer; user-select: none; white-space: nowrap; }
.proj-table th[data-sort]:hover { color: var(--gold-600); }
/* 项目管理表首列（项目）：表头与项目名称整列居中
   .n/.d 有 max-width 限制，需 margin auto 使容器相对 td 真正居中（宽屏下名称不偏左） */
.proj-table .cell-name { text-align: center; }
.proj-table .cell-name .n { justify-content: center; margin: 0 auto; }
.proj-table .cell-name .d { text-align: center; margin: 3px auto 0; }
/* 表格行不再做入场位移动画：页面切换会重建 DOM，动画每次重播，表现为整片内容"往上拱"。
   仅保留 hover 与背景过渡（详见文件末尾"动效"段的说明）。 */
tbody tr { transition: background var(--dur-1) var(--ease-std); }
tbody tr:hover { background: rgba(201, 169, 97, .07); }
tbody tr:last-child td { border-bottom: none; }
.cell-name .n { font-weight: 600; color: var(--navy-800); line-height: 1.4; max-width: 240px; display: flex; align-items: center; gap: 6px; }
.cell-name .n .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.cell-name .d { font-size: 12px; color: var(--text-muted); margin-top: 4px; max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.4; }
.cell-owner { display: flex; align-items: center; gap: 8px; justify-content: center; flex-wrap: nowrap; }
.cell-owner .info { line-height: 1.4; white-space: nowrap; }
.cell-owner .info .t { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
/* 操作列按钮间距 8px（原尾部第二处 .cell-actions 覆盖，已合并；≤600px 另收 4px） */
.cell-actions { display: flex; gap: 8px; justify-content: center; align-items: center; }
.cell-actions .btn { padding: 5px 8px; font-size: 12px; }
.empty-state { padding: 60px 20px; text-align: center; color: var(--text-muted); line-height: 1.6; }
.empty-state .ico { width: 56px; height: 56px; margin: 0 auto 14px; border-radius: 16px; background: var(--primary-50); display: flex; align-items: center; justify-content: center; }
.empty-state .ico svg { width: 26px; height: 26px; stroke: var(--gold-600); }
/* 状态颜色（浅色主题） */
.st-planning { background: #eef1f6; color: #5b6a86; }
.st-in_progress { background: #e4edff; color: #2563eb; }
/* ⚠️ 胶囊文字色【不跟随 --*-ink 的亮色】：胶囊自带一层浅底（#fdf1de / #e0f4ec），
   把亮色文字压上去只有 ≈2.1:1（实测 #d8a03c on #fdf1de = 2.11:1），比白底还差。
   故这一族继续用它自己的深浅（色相与深色同族：36° / 158°）。
   实测 3.29 / 3.76:1 —— 仍低于 AA 4.5，但这是沿用至今、观感稳定的原值（用户认可）；
   不再压深：压深会发暗，丢掉"软胶囊"的轻感。
   深色态见下方 html[data-theme="dark"] 那组：亮字配深底，实测 7.7 / 8.2:1，达标。 */
/* ⚠️ 状态胶囊的浅 / 深两套色值【故意不同】，不要为了"跨主题只用一对色值"而互换（见 §8 第五十 / 五十一轮）：
   互换后深色胶囊的可读性明显变差（用户实测否决）。这里以可读性优先，浅深各自取合适的明度。 */
.st-on_hold { background: #fdf1de; color: #b5791d; }
.st-completed { background: #e0f4ec; color: #1f8a63; }
.st-delayed { background: #fde8e8; color: #d13b3b; }
/* ---------- ② 统计栅格：4 列 → 2 列（≤900） ----------
   4 卡栅格降到 2 列即无孤卡；基座在 ≤480 已有 .stat-card 紧凑档
   （padding 14/16、图标 38、数字 22px），2 列在 360px 屏上仍放得下，
   故不再降到 1 列 —— 首页有 8 张统计卡，单列会拉出近 900px 的纯卡片滚动。 */

/* 首页「项目进度概览」行在 ≤600 竖屏：改为与「我的工作概览」一致的两段式 —
   名称/状态/日期在上方完整换行显示，进度条折到独立行并拉长，不再与名称挤在一行被压缩 */

/* ================= 响应式 ================= */
@media (max-width: 1200px) {
  /* 5 张统计卡 → 3 列（3+2 两行），避免 2 列时出现 2+2+1 的孤卡行 */
.stat-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .dash-grid { grid-template-columns: 1fr; }
}
@media (max-width: 900px) {
  /* 5 张统计卡 → 2 列（仅在较窄窗口；≤600 时再降为 1 列，见下方断点） */
.stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* 手机/小平板横屏：收起侧边栏，内容全宽 */
@media (max-width: 1000px) and (orientation: landscape) and (max-height: 540px) {
  .sidebar { transform: translateX(-100%); box-shadow: none; }
  .sidebar.open { transform: translateX(0); box-shadow: 20px 0 60px rgba(10, 20, 40, .3); }
  .main { margin-left: 0; }
  .mobile-toggle { display: inline-flex; }
  /* 卡片再宽一点：内容区左右内边距从 14px 收到 10px（390px 视口下卡片 362 → 370px）。
     只收左右两侧 —— 上下沿用既有值（其中 padding-bottom 那条带 !important，不去碰）。
     卡片变宽的直接收益是第一行的名称 / 备注能多显示几个字，不再那么快打省略号。 */
.content { padding: 14px 18px; }
}
/* ---------- ③ 顶栏：窄屏把宽度让给页面标题 ----------
   基座顶栏只放一个用户胶囊，窄屏够用；bzbj 另加了「数据大屏 / H5 演示」两个文字按钮，
   ≤600px 时 .topbar-right 要吃掉约 274px，标题被压到 ~36px → 显示成"系统…"。
   做法：两个演示入口收成纯图标（文案移入 aria-label，语义与可访问性不变），
        标题独占剩余宽度、溢出走省略号。 */
@media (max-width: 600px) {
  /* ================= ⑲ 浅色模式·语义「动作色」统一（bzbj ⑲ · 2026-10-16）=================
   现象：操作列里的「启用 / 停用 / 删除」三个语义按钮走
   `.btn-ghost.is-ok / .is-warn / .is-danger`，而这三条规则直接消费 `--success-ink` /
   `--warning-ink` / `--danger-ink` —— 这三个令牌**在浅色下沿用的是深色主题的亮色**
   （#34c68a / #d8a03c / #e25563，见文件顶部注释）。压在白底上实测只有
   **2.19 / 2.33 / 3.68 : 1**；而同一行里的「编辑 / 重置密码」是 `--text-muted`（≈5.9:1）。
   于是同一排按钮里三个发虚、两个实心，观感明显不齐（截图核对过；深色下不存在该问题，
   深色底上亮色本来就够亮）。`.t-warn`（报表页队列数「29」）同理，2.33:1。
   做法：浅色下改用**与 `.avatar` 实底同族的三个语义令牌** `--success` / `--warning` / `--danger`，
   对比度提到 **3.35 / 2.86 / 4.42 : 1**，全站语义色由「浅色 6 档」收到「3 档」。
   ⚠️ **深色一行不动**：深色下 `--*-ink` 与 `--*` 本来就同值（#34c68a / #d8a03c / #e25563），
      所以这里用 `html:not([data-theme="dark"])` 门控 —— 深色渲染逐字节不变。
   ⚠️ `--*-ink` 令牌**自身不动**（用户明确要求沿用深色亮色，见文件顶部那两个令牌的注释）；
      改的只是「动作按钮 / 警示文字」这两类消费点的取值。
   ⚠️ 若日后要回到与 d.janelon.com 逐字一致（基座就是直接用 `--*-ink`），
      把下面 `html:not([data-theme="dark"])` 那一条删掉即可，其余不用动。 */
:root { --topbar-h: 50px; }
  .stat-grid { grid-template-columns: 1fr; }
  .welcome-banner { flex-direction: column; align-items: flex-start; padding: 20px 16px; }
  .login-card { padding: 30px 24px; }
  .topbar { padding: 0 14px; }
  .topbar .crumb { display: none; }
  .content { padding: 14px; }
  .card-head, .card-body { padding-left: 16px; padding-right: 16px; }
}
/* ================= 页面数据加载占位 =================
   历史：此处曾有一套"骨架屏"（.sk / .sk-grid / .sk-toolbar / .sk-toggle 等，
   按路由生成对应布局的灰色占位并带扫光动画）。
   移除原因：数据有 15 秒缓存，站内切页时数据同步就绪、骨架与真实内容在同一帧完成替换，
   根本不会被绘制；只有刷新/首屏才会短暂出现，实测约一瞬间、基本看不到 —— 即价值近于零。
   而它是手写的静态模板、需与页面真实布局逐一对齐，页面一改就会"对不上"，
   那一瞬间的布局跳变反而比空白更刺眼（且每改一次页面就要同步维护一次）。
   改为下面这个不承诺任何布局的极简文字占位：缓慢网络下避免白屏，也不会再"对不上"。 */
.page-loading {
  padding: 44px 0;
  text-align: center;
  color: var(--text-muted);
  font-size: 13px;
  /* 占位文案可能带说明、会在窄屏换行 → 显式「说明段」1.6 档（规范 §2.3） */
  line-height: 1.6;
}
/* ================= 动效 ================= */
/* 页面切换过渡（2026-10-08 三次调整，最终版）：
   历史：内容区与表格行曾带"从下方 8px 升起"（rise / rowIn）的位移动画 + 错峰延迟。
   由于切换页面会重建 DOM，动画每次重播 —— 表现为内容"往上拱一下"，故已移除。
   现在改用【仅淡入、只作用于内容区直接子元素】：
     · 纯 opacity，不含位移，因此不会"拱"；
     · 作用于 .content 的直接子元素，**不用容器级动画**（父级 opacity 会让容器里的一切一起渐显）；
     · 不做错峰延迟（逐块延迟会造成"波浪涌现"，是之前观感违和的原因之一）；
     · var(--dur-3) + var(--ease-std)：起步更缓、收尾更长，比快起曲线更柔和。
   功能性动效（弹窗、Toast、波纹、进度条生长）保持不变。 */
@keyframes pageFade { from { opacity: 0; } to { opacity: 1; } }
/* ⚠️ 排除 .toolbar：父级 opacity 动画会让容器内的元素分层绘制，
   容器底色先画出来、内部控件还没跟上，于是看到"控件只剩外框在发亮"的残影。
   深色模式下尤其明显（工具栏控件底为半透明白，在暗底上对比强）；
   浅色模式底色淡，所以基本看不出来 —— 这正是"只在深色模式出现"的原因。
   工具栏是纯容器、没有入场动效需求，排除后即时绘制，反而更稳。 */
/* ⚠️ 带 data-enter="auto" 的容器【自身不参与淡入】，只淡它的子块 ——
   否则容器与子项会叠加两层 opacity（子项 = 容器曲线 × 自身曲线），
   看到的是"更慢、更暗"的另一套节奏，正是全站不统一的来源。 */
.content > *:not(.toolbar):not([data-enter="auto"]) { animation: pageFade var(--dur-3) var(--ease-std) both; }
/* 晚到内容同样入场（**通用机制，不是给某个页面打补丁**）：
   页面初渲染时先出骨架、数据回来才填充的情况（图片网格的分页加载、任何异步列表），
   容器声明 `data-enter="auto"` 后，其子项在【插入 DOM 的那一刻】自动播同一条入场动画 ——
   首屏与"加载更多"因此走同一套节奏，不会出现"整页只有图片那一屏没有动效"。
   ⚠️ 新增此类容器只加属性，不要再写各自的 keyframes / 时长。
   全站统一口径（不要再按页面特例处理）：
     · 数据先到、结构一次写死 → 各直接子块淡入（本页无需任何标记）；
     · 先铺骨架/占位、数据回来再换 → 容器加 data-enter="auto"，容器自身不淡入；
     · 整块重建（切筛选 / 切范围 / 加载更多）→ 与首屏同一节奏，重建即淡入；
     · 只换表格行（tbody）→ 不淡入（行级更新，规则见下方"表格行入场动画已移除"）。 */
[data-enter="auto"] > * { animation: pageFade var(--dur-3) var(--ease-std) both; }
.modal-mask.closing { opacity: 0; pointer-events: none; }
.modal-mask.closing .modal { animation: modalOut var(--dur-2) var(--ease-std) forwards; }
@keyframes modalOut { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(10px) scale(.98); } }
/* 统计卡图标 hover 冷蓝光晕 */
.stat-card:hover .ico { transform: translateY(-1px) scale(1.04); box-shadow: 0 6px 14px rgba(15, 30, 61, .14), 0 0 0 3px rgba(201, 169, 97, .14), 0 4px 16px -4px rgba(201, 169, 97, .3); }
/* ================= 桌面/平板横屏优化 ================= */
/* 横屏模式补齐：桌面宽屏放大弹窗、加大列间距；iPad 横屏/小桌面紧凑宽度；
   手机竖屏单列规则见下方 ≤900px 媒体查询 */
@media (min-width: 1100px) {
  /* 表单两列 → 单列：标签不再被硬挤成两行；minmax(0,1fr) 防止日期框等内联控件撑破列宽 */
.modal-body .grid { gap: 16px; }
  /* 横屏宽桌面：表格工具栏一行展示，不换行 */
.toolbar { flex-wrap: nowrap; }
  /* 工具栏：筛选控件整行显示 */
.toolbar .input, .toolbar .select { width: 200px; flex: 0 0 auto; }
}
/* 同上：901–1099 的 `.modal { max-width: 560px }` 已删除（560 不在四档内，同样是「改宽度」而非「适配」）。 */
/* ================= 手机竖屏适配 ================= */
/* 竖屏手机 ≤900px；横屏设备（≤960px 横向）同样适用。
   覆盖：竖屏手机、横屏手机、iPad 竖屏（768~900px），保证弹窗/表单/表格/工具栏在移动端一致 */
@media (max-width: 900px), (max-width: 960px) and (orientation: landscape) {
  html, body { width: 100vw; max-width: 100vw; overflow: hidden; overscroll-behavior: none; }
  .layout {
    width: 100vw;
    max-width: 100vw;
    height: 100vh;
    height: 100dvh; /* 动态视口：地址栏收起/展开自动适配高度，避免纵向拖动 */
    overflow: hidden;
  }
  input, select, textarea {
    font-size: 16px !important;
  }
  /* 弹窗：保留居中浮窗，视口内收缩，禁止横向滑动 */
.modal-mask {
    padding: 12px;
    align-items: center;
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior: contain;
    touch-action: pan-y;
  }
  /* height 也纳入过渡：弹窗内的二级视图两页高度不同，切换时由 JS 把目标高度写进内联 height，
   靠这条过渡"滑"过去（否则弹窗会啪地跳一下，用户实测反馈）。属"必要动效（避免布局突变）"，
   性能档下不关（见下方 lite 块注释）。 */
.modal {
    width: 100%;
    max-width: 540px;
    max-height: 86vh;
    border-radius: 16px;
    padding: 16px 14px;
    overflow-x: hidden;
    margin: auto;
  }
  .modal-header { margin-bottom: 10px; padding-bottom: 10px; }
  /* 窄屏（≤500）下底栏固定为【两行】：
     4 个按钮（禁用/启用 + 删除 + 取消 + 保存）约需 380px，窄屏一行放不下，必须有一行让位；
     让【危险组】让位、独占一行，而不是把"取消 / 保存"拆散到两行。
     ⚠️ 原来这里是 flex-direction: column-reverse —— 它会按 DOM 逆序排列，
     把最先出现的危险组翻到【最底部】，等于把最危险的按钮放在离拇指最近的位置，
     而主按钮反被顶到最上方。已改为正向 wrap。
     （宽屏触屏不换行：弹窗封顶 540px、底栏 510px，一行刚好放得下 40 + 10 + 225×2 + 10。） */
.modal-footer { margin-top: 14px; flex-wrap: wrap; }
  /* 只作用于常规动作组的按钮；危险组的方形图标按钮由基础规则接管，不会被拉伸。 */
.modal-footer .footer-actions .btn { flex: 1 1 auto; }
  /* 弹窗打开时锁定 body，阻止整个页面横向拖动 */
body.modal-open { overflow: hidden; overscroll-behavior: contain; }
  /* iOS Safari 的 date/time 输入框自带巨大默认样式，强制归一 */
input[type="date"],
  input[type="datetime-local"],
  input[type="time"] {
    -webkit-appearance: none;
    appearance: none;
    font-family: inherit;
    font-size: 14px;
    min-height: var(--ctl-h);
    height: var(--ctl-h);
    padding: 0 12px;
    line-height: var(--ctl-h);
    background: var(--field-bg);
    width: 100%;
    min-width: 0;
    max-width: 100%;
    box-sizing: border-box;
  }
  /* WebKit 私有伪元素：归一化 iOS date picker 内的文本渲染高度 */
input[type="date"]::-webkit-date-and-time-value,
  input[type="datetime-local"]::-webkit-date-and-time-value,
  input[type="time"]::-webkit-date-and-time-value {
    font-size: 14px;
    line-height: var(--ctl-h);
    height: var(--ctl-h);
    margin: 0;
    padding: 0;
    text-align: left;
    color: var(--text);
  }
  /* iOS 16+ 新增的 indicator 容器也强制归一 */
input[type="date"]::-webkit-datetime-edit,
  input[type="datetime-local"]::-webkit-datetime-edit,
  input[type="time"]::-webkit-datetime-edit {
    display: flex;
    align-items: center;
    height: var(--ctl-h);
    line-height: var(--ctl-h);
    padding: 0;
  }
  .modal-body .grid { grid-template-columns: minmax(0, 1fr); gap: 12px; }
  /* 表格：保持完整列宽 + 横向滚动，长文本不被挤压换行 */
.table-card table { min-width: 720px; }
  /* iPad 横屏表格：压缩单元格内边距 / 操作按钮 / 进度条，减少内容超宽导致的横向滚动量 */
.table-card table th,
  .table-card table td { white-space: nowrap; }
  /* 操作列：按钮 8px 等距、垂直居中（基座 .cell-actions 无样式，间距原先由标签间空白决定） */
.cell-actions { flex-wrap: wrap; }
  .toolbar .input,
  .toolbar .select { width: 100% !important; flex: 1 1 100%; }
  .toolbar .btn { height: var(--ctl-h-lg); }
  :root { --topbar-h: 50px; }
  .topbar {
    height: 50px;
    padding: 0 12px;
    gap: 8px;
    flex-wrap: nowrap;
  }
  .topbar > div:not(.topbar-right) { min-width: 0; flex: 1 1 auto; overflow: hidden; }
  .topbar .title {
    font-size: 16px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .topbar .crumb { display: none; }
  .topbar-right { flex-shrink: 0; gap: 0; margin-left: auto; }
  .topbar #logout-btn { display: none; }
  /* 弹窗表单：统一 label/input 行高节奏，修复"每行间距不均" */
.modal-body .field {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin: 0;
    min-width: 0;
  }
  .modal-body .field > label {
    line-height: 16px;
    height: 16px;
    display: block;
    font-size: 13px;
    font-weight: 600;
    color: var(--field-label);
    margin: 0;
  }
  .modal-body .field > label .req { color: var(--danger); }
  .modal-body .field .input,
  .modal-body .field .select {
    height: var(--ctl-h);
    line-height: var(--ctl-h);
    padding: 0 12px;
    font-size: 14px;
    -webkit-appearance: none;
    appearance: none;
    box-sizing: border-box;
    border: 1px solid var(--field-border);
    border-radius: 10px;
    background: var(--field-bg);
    color: var(--text);
    outline: none;
    width: 100%;
    min-width: 0;
    max-width: 100%;
  }
  /* 自定义 select 箭头，去掉 iOS 原生外观 */
.modal-body .field .select {
    padding-right: 30px;
    background-image: var(--select-arrow);
    background-repeat: no-repeat;
    background-position: right 10px center;
    background-size: 10px 6px;
  }
  .modal-body .field .textarea {
    height: auto;
    min-height: 84px;
    line-height: 1.5;
    padding: 10px 12px;
    resize: vertical;
    -webkit-appearance: none;
    appearance: none;
    border: 1px solid var(--field-border);
    border-radius: 10px;
    background: var(--field-bg);
    font-size: 14px;
  }
  .modal-body .grid { row-gap: 10px; column-gap: 10px; }
  /* 弹窗内只补下边距，字号行高不再覆盖（原 1.4 是全站唯一一处"更紧"的说明行距） */
  .modal-body .form-hint { margin-bottom: 8px; }
  /* 弹窗内直接子字段统一间距：field 和 grid（字段组）之间都固定 10px，避免混排时忽大忽小 */
  .modal-body > .field,
  .modal-body > .grid { margin-top: 10px; }
  .modal-body > .field:first-child,
  .modal-body > .grid:first-child { margin-top: 0; }
  /* 卡体内字段间距同步收窄（与弹窗同一档，避免窄屏下卡体比弹窗还松） */
  .card-body > .field,
  .card-body > .grid { margin-top: 10px; }
}
/* iPad 专属适配：日期/时间控件用紧凑尺寸（iPadOS 原生渲染过大，且不在手机媒体查询范围内） */
@media (min-width: 768px) and (max-width: 1366px) {
  input[type="date"],
  input[type="datetime-local"],
  input[type="time"] {
    -webkit-appearance: none;
    appearance: none;
    font-family: inherit;
    font-size: 14px;
    min-height: var(--ctl-h);
    height: var(--ctl-h);
    line-height: var(--ctl-h);
    padding: 0 12px;
    border: 1px solid var(--field-border);
    border-radius: 10px;
    background: var(--field-bg);
    color: var(--text);
    outline: none;
    box-sizing: border-box;
    width: 100%;
    max-width: 100%;
  }
  input[type="date"]::-webkit-date-and-time-value,
  input[type="datetime-local"]::-webkit-date-and-time-value,
  input[type="time"]::-webkit-date-and-time-value {
    font-size: 14px;
    line-height: var(--ctl-h);
    height: var(--ctl-h);
    margin: 0;
    padding: 0;
    text-align: left;
    color: var(--text);
  }
  input[type="date"]::-webkit-datetime-edit,
  input[type="datetime-local"]::-webkit-datetime-edit,
  input[type="time"]::-webkit-datetime-edit {
    display: flex;
    align-items: center;
    height: var(--ctl-h);
    line-height: var(--ctl-h);
    padding: 0;
  }
  input[type="date"]::-webkit-calendar-picker-indicator,
  input[type="datetime-local"]::-webkit-calendar-picker-indicator,
  input[type="time"]::-webkit-calendar-picker-indicator {
    width: 18px;
    height: 18px;
    padding: 0;
  }
  /* 覆写窄屏下的 nowrap（见 .table-card table th, td 的窄屏规则），
     否则卡片内文字仍会横向撑开 */
.table-card table th,
  .table-card table td { padding-left: 12px; padding-right: 12px; }
  .table-card .cell-actions .btn { padding-left: 8px; padding-right: 8px; }
}
/* 深色：同样 html 纯色 + body 渐变(fixed)，与浅色保持一致的分工 */
html[data-theme="dark"] {
  color-scheme: dark;
  --navy-900: #050b16;
  --navy-800: #0c1526;
  --navy-700: #101c31;
  --navy-600: #1a2b4a;
  --navy-100: #223252;
  --gold-400: #e9d9b0;
  --gold-500: #c9a961;
  --gold-600: #a9853f;
  /* 深色根背景，同浅色统一为顶栏色（详见 :root 处说明；原值 #05090f 与顶栏差 19） */
  --bg: #0a0f17;
  /* 深色顶栏色，同浅色（详见 :root 处说明）：实测视口顶部区域 = #0a0f17 */
  --topbar-bg: #0a0f17;
  /* 深色底栏色（= 页面渐变末端区域实测色），原先写在 html[data-theme="dark"] .tabbar 里 */
  --tabbar-bg: #070e1b;
  --card: #121c31;
  --text: #e6ebf5;
  --text-muted: #8b9ab8;
  --border: #2a3854;
  /* 表单控件基础色（暗色）：覆盖 :root 同名变量，弹窗 / 工具栏 / 三端媒体查询自动生效 */
  --field-bg: #0d1728;
  --field-border: #26324e;
  --field-label: #9fb0cc;
  --field-placeholder: #5c6b8c;
  /* 强调实底 / 抬升面板（深色一套；浅色版见文件顶部 :root —— 两套必须一一对应）*/
  --accent-fill: linear-gradient(135deg, #3560b0, #23448c);        /* 比卡片亮两档，白字 5.3:1 */
  --accent-fill-hover: linear-gradient(135deg, #3a6cc6, #27509f);  /* hover 再提一档 */
  --accent-fill-deep: linear-gradient(135deg, #2b4a86, #1b2f5c);   /* 更深一档（次要主色块）*/
  --surface-2: #1a2642;      /* 抬升面板：次级按钮 / 设置卡 / 浮层底栏 / 瑕疵选择胶囊 */
  --surface-3: var(--navy-100);   /* 再抬一档：分段滑块 / 表格粘性列（#223252）*/
  /* 页面底色渐变（深色）：与 --bg 同一族，只换明度；顶栏/底栏取的是本渐变的两端 */
  --page-grad: linear-gradient(180deg, #060b15 0%, #070e1b 100%);
  /* 登录页底色（独立表面：深藏蓝流光，故不跟随 --bg） */
  --login-page-grad: linear-gradient(135deg, #0a1630 0%, #060d1f 55%, #03070f 100%);
  /* 登录页深色一套（与 :root 的 --login-* 一一对应；同一角色只换深浅，不换色相）。

     --login-logo-glow 两主题同值，故此处不重复声明。 */
  --login-ink: #eaf0fb;
  --login-ink-sub: #8fa3c9;
  --login-ink-body: #9fb0d0;
  --login-label: #c4d2ea;
  --login-placeholder: #6f82a8;
  --login-hairline: rgba(148, 172, 220, .12);
  --login-field-bg: rgba(10, 18, 34, .55);
  --login-field-bg-focus: rgba(10, 18, 34, .75);
  --login-field-border: rgba(148, 172, 220, .20);
  --login-field-inset: rgba(2, 6, 18, .35);
  --login-focus-ring: rgba(201, 169, 97, .18);
  --login-glow-inset: rgba(201, 169, 97, .08);
  --login-shadow: rgba(2, 6, 18, .60);
  --login-card-solid-a: #0d1526;
  --login-card-solid-b: #101b30;
  --login-btn-glow: rgba(184, 146, 63, .40);   /* 与浅色 .50 成对（几何尺寸不同，故不共用 box-shadow） */
  /* 性能档实底孪生色（深色一套） */
  --lite-veil: rgba(3, 7, 16, .60);            /* 三种遮罩在性能档 + 深色下同值 */
  --login-lite-border: rgba(148, 172, 220, .30);
  --login-lite-shadow: 0 26px 64px -18px rgba(2, 6, 18, .75), 0 1px 0 rgba(148, 172, 220, .12) inset;
  --login-lite-field-bg: rgba(10, 18, 34, .90);
  --login-lite-field-border: rgba(148, 172, 220, .24);
  --select-arrow: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%239fb0cc' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --primary: var(--navy-700);
  --primary-hover: var(--navy-600);
  --primary-50: #16213a;
  --success: #34c68a;
  --warning: #d8a03c;
  --danger: #e25563;
  /* ⚠️ 这里【不再】重定义 --*-ink：它们已直接取深色值（#d8a03c / #34c68a / #e25563），

     深浅两主题同值 —— 用户要求"直接沿用深色模式的那些颜色"，浅色也照用。 */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
  --shadow: 0 8px 24px rgba(0, 0, 0, .55);
  --shadow-lg: 0 20px 50px rgba(0, 0, 0, .65);
  /* 毛玻璃材质（深色）：同一组令牌的深色取值，浮层通透度与浅色同步（2026-10-15 下调）。

     模糊 / 饱和与主题无关，仍取 :root 同名值，此处不重复。 */
  --glass-tint: rgba(13, 20, 36, .60);        /* 原 .72（.more-card 原为 .92，已统一到此） */
  --glass-tint-thin: rgba(13, 20, 36, .60);   /* 原 .78 */
  --glass-tint-card-a: rgba(12, 20, 38, .62); /* 原 .78 */
  --glass-tint-card-b: rgba(7, 13, 27, .46);  /* 原 .62 */
  --glass-hairline: rgba(255, 255, 255, .10); /* 原 .08 ~ .12 */
  --glass-inset: rgba(255, 255, 255, .06);    /* 原 .05 */
  --glass-veil-sm: rgba(3, 7, 16, .34);       /* 原 .42 */
  --glass-veil-md: rgba(3, 7, 16, .46);       /* 侧栏遮罩：原先直接沿用浅色的深蓝 .45 */
  /* 深色页面底色兜底（原第二处 html[data-theme="dark"] 定义，已合并） */
  background-color: var(--bg);
}
html[data-theme="dark"] body {
  background:
    radial-gradient(900px 500px at 90% -10%, rgba(201, 169, 97, .05), transparent 65%),
    radial-gradient(800px 600px at -10% 110%, rgba(20, 35, 60, .12), transparent 70%),
    var(--page-grad);
  background-attachment: fixed;
}
html[data-theme="dark"] ::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, .16); }
html[data-theme="dark"] ::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, .28); }
/* 顶栏（深色）：背景色由 --topbar-bg 变量自动切换（见 :root / html[data-theme="dark"]），
   此处只需覆盖描边颜色 */
html[data-theme="dark"] .topbar {
  border-bottom-color: rgba(255, 255, 255, .07);
}
html[data-theme="dark"] .topbar .title { color: var(--text); }
/* 表格：表头带子用【半透明白】自适应卡片实底
   （浅色对应值 #f4f7fb ≈ 白卡片压暗 11；这里 .05 叠在 #121c31 上提亮约 12，同级）
   历史：原为固定 #111c31，与卡片实底 #121c31 只差 1 → 卡片由半透明改实底后表头带子会消失 */
html[data-theme="dark"] thead th { background: rgba(255, 255, 255, .05); color: var(--field-label); }
html[data-theme="dark"] tbody td { border-color: var(--field-border); }
html[data-theme="dark"] tbody tr:hover { background: rgba(26, 43, 74, .45); }
html[data-theme="dark"] .stat-card .num,
html[data-theme="dark"] .card-head h3,
html[data-theme="dark"] .dist-item .cnt,
html[data-theme="dark"] .cell-name .n { color: var(--text); }
html[data-theme="dark"] .btn-outline { background: var(--surface-2); border-color: var(--field-border); color: var(--text); box-shadow: none; }
html[data-theme="dark"] .btn-ghost:hover { background: rgba(26, 43, 74, .5); color: var(--text); }
/* 主按钮在暗色下只补【阴影】：底色已由 --accent-fill 令牌在深色段接管
   （旧写法在这里又写了一遍 `#2f5cb3 → #20428f`，与统计卡图标的 `#3560b0 → #23448c`
   各算各的，于是"同一个主色"在深色下出现两组蓝 —— 现统一到令牌，色值只存一处）。 */
html[data-theme="dark"] .btn-primary {
  box-shadow: 0 4px 14px rgba(0, 0, 0, .4), 0 6px 20px -8px rgba(201, 169, 97, .4);
}
@media (hover: hover) {
  html[data-theme="dark"] .btn-primary:hover:not(:disabled) {
    box-shadow: 0 8px 24px -6px rgba(201, 169, 97, .5), inset 0 1px 0 rgba(255, 255, 255, .12);
  }
}
/* 深色登录主按钮：基座与悬停拆成两段（原文件尾部有一处只重写 box-shadow 的同名覆盖，
   已合并进来 —— 现每个属性只有一处来源）。 */
html[data-theme="dark"] .login-card .btn-primary {
  background: linear-gradient(135deg, var(--gold-500) 0%, var(--login-btn-grad-b) 100%);
  color: var(--login-btn-ink);
  box-shadow: 0 10px 24px -6px var(--login-btn-glow), 0 0 0 1px rgba(255, 255, 255, .12) inset;
}

html[data-theme="dark"] .login-card .btn-primary:hover:not(:disabled) {
  background: linear-gradient(135deg, var(--gold-500) 0%, var(--login-btn-grad-b) 100%);
  color: var(--login-btn-ink);
  box-shadow: 0 8px 20px -6px var(--login-btn-glow);
}
/* 进度条 / 分布条（深色）：用【半透明白】而不是固定色。
   原因：固定色 #101b30 与卡片实底 #121c31 每通道只差 1~2 ——
   脚下只要从"半透明玻璃"变成实底，轨道就会被吃掉（首页卡片行踩过）。
   半透明白随任何承载底色自适应，无需再逐处补分支。 */
html[data-theme="dark"] .progress,
html[data-theme="dark"] .dist-bar { background: rgba(255, 255, 255, .16); }
/* 状态胶囊 */
html[data-theme="dark"] .st-planning { background: rgba(148, 163, 195, .14); color: #aeb9d2; }
html[data-theme="dark"] .st-in_progress { background: rgba(59, 130, 246, .16); color: #7da9f7; }
/* 深色：淡色低透明底 + 同色相提亮字（可读性优先；不追求"与浅色共用同一对色值"，见上方注释） */
html[data-theme="dark"] .st-on_hold { background: rgba(217, 143, 45, .16); color: #e0ae66; }
html[data-theme="dark"] .st-completed { background: rgba(16, 185, 129, .16); color: #55d3a8; }
html[data-theme="dark"] .st-delayed { background: rgba(239, 68, 68, .16); color: #ff8d8d; }

/* 深色卡片：实底 + 微亮描边 + 顶部内高光 + 深影（与 .profile-card 同底色 #121c31） */
/* 弹窗 */
html[data-theme="dark"] .modal { background: var(--glass-tint); border-color: var(--glass-hairline); box-shadow: inset 0 1px 0 var(--glass-inset), 0 28px 64px -18px rgba(0, 0, 0, .6); }
/* 卡片深色：底色由 .card 的 var(--card) 自动取 #121c31，此处只需压深描边与投影 */
html[data-theme="dark"] .card {
  border-color: var(--border);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05), 0 12px 32px -14px rgba(0, 0, 0, .55);
}
html[data-theme="dark"] .modal-header { border-color: var(--field-border); }
html[data-theme="dark"] .modal-header h3 { color: var(--text); }
html[data-theme="dark"] .modal-mask { background: var(--glass-veil-sm); }
.theme-opt {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
  padding: 15px 10px; border: 1.5px solid var(--border); border-radius: 12px;
  background: #fff; color: var(--navy-700); font-size: 13px; cursor: pointer;
  transition: border-color var(--dur-1) var(--ease-std), box-shadow var(--dur-1) var(--ease-std), background var(--dur-1) var(--ease-std), color var(--dur-1) var(--ease-std);
}
.theme-opt i { width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; line-height: 0; color: var(--text-muted); transition: color var(--dur-1) var(--ease-std); }
.theme-opt:hover { border-color: var(--gold-400); }
.theme-opt.active { border-color: var(--gold-500); background: #fdf8ec; color: var(--navy-800); box-shadow: 0 3px 10px rgba(201, 169, 97, .18); }
.theme-opt.active i { color: var(--gold-600); }
html[data-theme="dark"] .theme-opt { background: var(--surface-2); color: var(--text); }
html[data-theme="dark"] .theme-opt.active { border-color: var(--gold-500); background: rgba(201, 169, 97, .12); box-shadow: none; }
html[data-theme="dark"] .theme-opt.active i { color: var(--gold-400); }
/* 显示效果档位（系统设置）：视觉参数与外观模式卡片完全同套 */
.vfx-picker { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; max-width: 520px; }
.vfx-opt { padding: 13px 10px; }
.vfx-txt { display: flex; flex-direction: column; align-items: center; gap: 4px; min-width: 0; }
.vfx-name { font-size: 13px; font-weight: 600; line-height: 1.4; }
.vfx-desc { font-size: 11px; line-height: 1.5; color: var(--text-muted); overflow-wrap: break-word; }
.theme-opt.active .vfx-desc { color: inherit; opacity: .85; }
/* 显示效果的「当前：…」状态行（12px 元信息，同为 flex 行）—— 基座未声明行高，
   落到浏览器默认 normal 后跳出 4 档之外（ui-audit ② 会报）。 */
.vfx-current { margin-top: 12px; font-size: 12px; line-height: 1.5; color: var(--text-muted); display: flex; align-items: center; gap: 6px; }
.vfx-current::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--gold-500); flex-shrink: 0; }
/* 档位跟随规则说明（账号级 / 设备级） */
.vfx-note { margin-top: 10px; font-size: 12px; line-height: 1.6; color: var(--text-muted); }
/* ---------- ④ 报警详情：关联图像竖屏改上下排 ----------
   横排时 160px 缩略图 + 12px 间距会把右侧文字列挤到 ~110px，逐字折行。 */
@media (max-width: 600px) {
  .vfx-picker { grid-template-columns: 1fr; max-width: none; }
  .vfx-opt { flex-direction: row; justify-content: flex-start; text-align: left; gap: 12px; padding: 12px 14px; }
  .vfx-txt { align-items: flex-start; }
  .vfx-desc { text-align: left; }
}
/* ================= 分段切换控件（全站唯一标准） =================
   视觉参数与首页「全部 / 我的」「临期 / 逾期」滑块完全同款：
   浅灰底胶囊（dark rgba(255,255,255,.08)）+ 白色滑块滑动（dark #22314f）+ 选中项文字转深。
   任何两态切换（顶栏深浅色 theme-toggle、偏好开/关 seg-toggle、各类筛选 tab）一律用这套，
   禁止另起炉灶（拨动式 switch / 单选按钮等一律不使用）。 */
.seg-toggle,
.theme-toggle { position: relative; display: inline-flex; align-items: center; gap: var(--seg-gap); padding: var(--seg-inset); background: rgba(15, 30, 60, .06); border-radius: 9px; flex-shrink: 0; }
html[data-theme="dark"] .seg-toggle,
html[data-theme="dark"] .theme-toggle { background: rgba(255, 255, 255, .08); }
/* 初始 opacity:0 —— 滑块在 CSS 里的初始态是 left:0/width:0，但 box-shadow 仍会渲染出
   一个可见的阴影小点。异步定位（数据返回后才摆放）的控件因此会先闪一下。
   约定：由 JS 的 placePillNoAnim() 在摆放到位的同时把 opacity 置为 1。 */
.seg-toggle .seg-pill,
.theme-toggle .tt-pill { position: absolute; top: var(--seg-inset); bottom: var(--seg-inset); left: 0; width: 0; opacity: 0; border-radius: 7px; background: #fff; box-shadow: 0 1px 5px rgba(15, 30, 60, .14); transition: left var(--dur-3) var(--ease-std), width var(--dur-3) var(--ease-std); z-index: 0; pointer-events: none; }
html[data-theme="dark"] .seg-toggle .seg-pill,
html[data-theme="dark"] .theme-toggle .tt-pill { background: var(--surface-3); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .06), 0 1px 5px rgba(0, 0, 0, .35); }
.seg-toggle .seg-btn,
.theme-toggle .tt-btn { position: relative; z-index: 1; display: inline-flex; align-items: center; justify-content: center; border: none; background: transparent; border-radius: 7px; color: var(--text-muted); cursor: pointer; transition: color var(--dur-2) var(--ease-std); }
.seg-toggle .seg-btn { padding: 4px 12px; font-size: 12px; line-height: 1.6; }
.theme-toggle .tt-btn { width: 26px; height: 26px; padding: 0; }
.seg-toggle svg, .theme-toggle svg { width: 15px; height: 15px; }
.seg-toggle .seg-btn.active,
.theme-toggle .tt-btn.active { color: var(--navy-700); font-weight: 600; }
html[data-theme="dark"] .seg-toggle .seg-btn.active,
html[data-theme="dark"] .theme-toggle .tt-btn.active { color: #fff; }
/* 偏好设置：行式条目（文字说明左、切换控件右、垂直居中），条目间细虚线分隔 */
.pref-row { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 12px 0; }
.pref-row + .pref-row { border-top: 1px dashed var(--border); }
/* .pref-row 本身由基座定义（gap 18 / padding 12px 0），此处只补结构 */
.pref-txt { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; text-align: left; }
.pref-name { display: block; font-size: 13px; font-weight: 600; color: var(--navy-800); line-height: 1.5; }
html[data-theme="dark"] .pref-name { color: var(--text); }
/* 间距交给 .pref-txt 的 gap 4px，这里不再叠加（原 2px 会让它变 6px，与同排的 .pref-hint 不一致） */
.pref-desc { display: block; font-size: 12px; color: var(--text-muted); line-height: 1.5; }
@media (max-width: 600px) {
  .pref-row { align-items: flex-start; gap: 12px; }
  .theme-toggle .tt-btn { width: 24px; height: 24px; }
  .topbar-right { gap: 4px; }
}
/* 外观分类标题：区分「外观模式」与未来的更多外观选项 */
.settings-sec-title { font-size: 12px; font-weight: 700; line-height: 1.5; letter-spacing: .08em; color: var(--text-muted); text-transform: uppercase; margin: 6px 0 10px; }
.tl-item { position: relative; display: flex; gap: 12px; padding: 0 0 18px; }
.tl-item:last-child { padding-bottom: 0; }
.tl-item::before {
  content: ''; position: absolute; left: 5px; top: 16px; bottom: 0;
  width: 2px; background: var(--border); border-radius: 2px;
}
.tl-item:last-child::before { display: none; }
.tl-dot {
  position: relative; z-index: 1; width: 12px; height: 12px; border-radius: 50%;
  background: var(--gold-500); border: 2px solid var(--card);
  box-shadow: 0 0 0 2px rgba(201, 169, 97, .35);
  flex-shrink: 0; margin-top: 4px;
}
.tl-body { flex: 1; min-width: 0; }
/* 时间线副信息：11px 行尾元信息取 normal 1.5 */
.tl-time { font-size: 11px; line-height: 1.5; color: var(--text-muted); flex-shrink: 0; }
.tl-text { font-size: 13px; color: var(--navy-700); line-height: 1.6; margin-top: 4px; }
html[data-theme="dark"] .tl-text { color: var(--field-label); }
html[data-theme="dark"] .tl-item::before { background: var(--border); }
html[data-theme="dark"] .tl-dot { border-color: var(--primary-50); }
/* ================= 只读数据块（JSON / 快照展示） =================
   规范 §4「只读数据块」的唯一写法：把一段数据原样摊开给人看（当前用于操作留痕的快照详情）。
   ⚠️ 全条只消费主题令牌，所以【不需要】html[data-theme="dark"] 分支 —— 令牌本身随主题翻转。
   ⚠️ 颜色不用 --navy-*：它在深色下当文字色会掉到 ≈1:1（规范 §1.2 的高频陷阱）。
   字号 12px 属规范 §2 的"副信息"层级，圆角 10px 属"行与控件"层级 —— 都不新增档位。 */
.json-block {
  background: var(--field-bg);
  border: 1px solid var(--field-border);
  border-radius: 10px;
  padding: 10px 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  line-height: 1.6;
  color: var(--text);
  white-space: pre-wrap;
  word-break: break-all;
  text-align: left;          /* ⚠️ 必须写死：<pre> 会继承外层的居中（系统设置卡片就是居中的），

                                数据块一旦居中，JSON 的缩进结构就全乱了 */
  max-height: 46vh;          /* 快照可能很长：块内滚动，不撑破弹窗 */
  overflow: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
/* stat-card ≤480 紧凑：图标缩小、padding 收紧 */

/* 首页跳转过来的目标任务曾用「金色闪烁高亮」（rowFlash / cardFlash + tr.row-highlight）。
   2026-10-17 复核：app.js 已不再产出 .row-highlight（跳转筛选改为直接回填筛选条件），
   这两个 keyframes 与 4 条规则成为死样式，一并删除。若日后要恢复"跳过来闪一下"，
   恢复点：navInit() 取用一次性筛选后给目标行加 class + 这里补回 keyframes。 */

/* 首页顶部统计卡：可点击跳转并筛选 */
.stat-card[data-stat-go] { cursor: pointer; transition: transform var(--dur-2) var(--ease-std), box-shadow var(--dur-2) var(--ease-std); }
.stat-card[data-stat-go]:hover { transform: translateY(-2px); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 0 0 .5px rgba(31, 58, 110, .05), 0 12px 32px -14px rgba(31, 58, 110, .18); }

@media (max-width: 600px) {
  .avatar { width: 28px; height: 28px; font-size: 13px; }
  .welcome-banner p { max-width: 100%; }
}

/* ================= 三端横竖屏补充优化 ================= */
/* 顶栏：≤720px 进一步压缩系统管理员区，只保留头像（节省横向空间） */

/* ================= 弹窗底部按钮：【窄屏】或【触屏】→ 撑满两键 =================
   两个入口条件（取并集）：
     · max-width: 500px  —— 窄窗口（桌面把浏览器拖窄、极窄老机型），与历史行为完全一致；
     · pointer: coarse   —— 触屏设备，【不看屏多宽】：
                            折叠屏展开 ≈884、iPad 竖屏 768 / 横屏 1024、手机横屏 844。
   ⚠️ 为什么必须补上触屏条件（用户反馈）：底栏形态原本只由【屏宽】决定，于是同一台
   触屏设备会出现两副样子 —— 手机竖屏（390）是大按钮，折叠屏展开（884）、iPad（768）
   却又和小窗口一样是小按钮。这三者是【同一类输入方式】，按钮形态应当跟着
   【输入方式】走，不该跟着屏宽走；否则同一个用户在不同屏宽下看到两套按钮规格。
   ⚠️ 为什么触屏上也敢放弃靠右布局留出的 268px 防误触距离：
   底栏防误触其实【不靠】这段距离 —— 破坏性动作（禁用/启用、删除）都收在「⋯」
   二级卡片（.more-card）里，点开还【再弹一层确认框】（showConfirm / confirmDelete，
   见 app.js openMoreCard 的调用点），误触「取消」旁边那颗图标到不了删除。
   鼠标端那段距离照旧保留（见上方 .footer-actions 的基础规则）。
   ⚠️ 只写 (pointer: coarse)，【不】加 (hover: none)：coarse 已覆盖 iOS / Android / 折叠屏，
   而 (hover: none) 在本地验证环境里无法命中（触屏是靠 --blink-settings=
   primaryPointerType=2,availablePointerTypes=2 模拟的，只影响 pointer）——
   加一条测不到的规则不值；万一某台设备漏判，代价也只是"保持今天的样子"。
   ⚠️ 窄屏换行规则（原 ≤500px 的内容）仍然适用于本块，见下方 .modal-footer 注释。 */
@media (max-width: 500px), (pointer: coarse) {
  .modal-footer { flex-wrap: wrap; gap: 10px; }
  /* 让动作组撑满剩余宽度、组内两键等分（取消 / 保存修改 宽度严格一致）。
     ⚠️ margin-left 必须是 0：动作组既然已经撑满，剩余空间为 0；
     若再给它 10px 左边距，图标与"取消"之间的间距就变成 10(底栏 gap) + 10 = 20，
     而其它相邻间距都是 10 —— 看起来就是"左右间距不一样、很歪"（实测踩到）。
     危险动作是 40px 图标，390 / 360 下与动作组同在【一行】；
     320px 这种极窄屏放不下时动作组会【整体】换到第二行（换行以整组为单位）。
     ⚠️ 面板自身封顶（≤900 时 540px），所以撑满不会失控：折叠屏展开 ≈225px/键、
     iPad 横屏 ≈245px/键，仍远小于"一个按钮占半屏"。 */
.modal-footer .footer-actions { flex: 1 1 auto; margin-left: 0; }
  .modal-footer .footer-actions .btn { flex: 1 1 0; }
}
@media (max-width: 480px) {
  .stat-card { padding: 14px 16px; gap: 10px; }
  .stat-card .num { font-size: 23px; }
  .stat-card .lbl { font-size: 12px; }
  .stat-card .ico { width: 38px; height: 38px; }
  .stat-card .ico svg { width: 18px; height: 18px; }
}
/* 安全区适配：iPhone notch / 灵动岛
   padding-top 的作用不只是避开刘海，更关键的是【把整个界面下移，让顶部的状态栏区域空出来】。
   viewport-fit=cover 下页面会延伸到 iOS 状态栏下方，那块区域若被元素占住（例如让顶栏从 top:0 起），
   就会与 Safari 状态栏的采样底色对不齐、出现一道分界。
   这里主动让开：状态栏区域露出 body 背景，与下方页面背景天然连续 ——
   这也正是那些"顶部没有固定栏"的站点能完美融合的原因（它们天然就是空的）。
   padding-left/right 则是避让横屏时的刘海。 */
@supports (padding: max(0px)) {
  .layout {
    padding-top: env(safe-area-inset-top);
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
  }
}
@media (max-width: 900px) {
  .nav-item { min-height: 44px; padding: 12px 12px; }
}
@media (max-width: 600px) {
  .cell-actions { gap: 4px; }
  .cell-actions .btn { padding-left: 6px; padding-right: 6px; }
}

/* 「光感层」的同名二次覆盖已各回其位（合并到各组件基座规则）：
   .btn-primary / .input:focus / .nav-item.active / .card:hover / .card / .main /
   .topbar / .content / .modal / .modal-mask / .sidebar-mask /
   .login-card .btn-primary / .login-card .login-brand img /
   .stat-card:hover .ico / .progress > div。
   此处只保留深色主题覆盖 —— 浅色基座 + 深色覆盖是唯一允许的两层。 */
@media (hover: hover) {
  .login-card .btn-primary:hover { box-shadow: 0 12px 30px -8px rgba(184, 146, 63, .75), inset 0 1px 0 rgba(255, 255, 255, .28); }
}
html[data-theme="dark"] .input:focus, html[data-theme="dark"] .select:focus, html[data-theme="dark"] .textarea:focus {
  box-shadow: 0 0 0 3px rgba(201, 169, 97, .2), 0 2px 18px -6px rgba(201, 169, 97, .3);
}
html[data-theme="dark"] .nav-item.active { box-shadow: 0 6px 20px -8px rgba(201, 169, 97, .5), inset 0 0 18px rgba(201, 169, 97, .1); }
html[data-theme="dark"] .stat-card:hover .ico { box-shadow: 0 6px 14px rgba(0, 0, 0, .4), 0 0 0 3px rgba(201, 169, 97, .16), 0 4px 18px -4px rgba(201, 169, 97, .4); }
html[data-theme="dark"] .card:hover { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05), 0 0 0 1px rgba(201, 169, 97, .2), 0 20px 44px -18px rgba(0, 0, 0, .6); }

/* ================= 性能模式降级（显示效果档 = 性能） =================
   通过 html[data-vfx="lite"] 统一关闭毛玻璃与装饰性动画。
   不影响任何布局与功能，仅质感降级：玻璃 → 半透明实底，动效只保留必要过渡。 */
/* 清单 = 全部"浮层类"（背后有真内容的那些）。第一级卡片已改实底，不在其中。
   .more-card（「⋯」浮层菜单）此前漏登记，2026-10-14 补入。 */
html[data-vfx="lite"] .toast,
html[data-vfx="lite"] .modal,
html[data-vfx="lite"] .more-card,
html[data-vfx="lite"] .login-card,
html[data-vfx="lite"] .modal-mask,
html[data-vfx="lite"] .sidebar-mask {
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}
/* 去掉玻璃后补足底色，避免"透明"导致文字叠影。
   注意：浅色 / 深色均为同一 <html> 上的属性，需写成 [data-vfx="lite"][data-theme="dark"]，
   不能嵌套两个 html（否则深色规则永不匹配，深色模式会错用浅色实底而变白）。
   .card 不在本清单：第一级卡片已改成实底，本来就无玻璃可关。 */
/* ⚠️ .topbar 自 2026-10-15 起有了 backdrop-filter；性能档按用户要求维持实底，
   故在此显式改回纯色并关掉模糊（不并入上面那条统一清单：它要读自己的底色令牌）。
   .tabbar 是实底、本来就没有玻璃可关，不进本条。 */
html[data-vfx="lite"] .topbar { background: var(--topbar-bg); backdrop-filter: none !important; -webkit-backdrop-filter: none !important; }
html[data-vfx="lite"] .modal { background: rgba(255, 255, 255, .98); }
html[data-vfx="lite"] .toast { background: rgba(255, 255, 255, .97); }
/* .more-card 同样在本清单里关掉了模糊，必须一并补实底（2026-10-15 补：它原先只关了模糊没补底色，
   浅色 .82 / 深色 .92 尚能撑住，改成 .70 / .60 后无模糊档下会太透）。 */
html[data-vfx="lite"] .more-card { background: rgba(255, 255, 255, .97); }
html[data-vfx="lite"] .login-card { background: linear-gradient(160deg, var(--login-card-solid-a) 0%, var(--login-card-solid-b) 100%); border-color: var(--login-lite-border); box-shadow: var(--login-lite-shadow); }
html[data-vfx="lite"] .modal-mask { background: var(--lite-veil-modal); }
html[data-vfx="lite"] .sidebar-mask { background: var(--lite-veil-sidebar); }
/* 注：原先有一段「仅 iOS Safari：关闭底栏毛玻璃」的 @supports 降级块（起因是
   backdrop-filter 的合成层会让 iPhone 底部地址栏取色失败）。现顶栏与底栏已全平台统一为实底，
   该降级不再需要，已移除。 */
html[data-vfx="lite"][data-theme="dark"] .modal { background: rgba(13, 20, 36, .98); }
html[data-vfx="lite"][data-theme="dark"] .toast { background: var(--primary-50); }
html[data-vfx="lite"][data-theme="dark"] .more-card { background: var(--primary-50); }
html[data-vfx="lite"][data-theme="dark"] .login-card { background: linear-gradient(160deg, var(--login-card-solid-a) 0%, var(--login-card-solid-b) 100%); border-color: var(--login-lite-border); box-shadow: var(--login-lite-shadow); }
/* 性能模式 · 登录页专项：背景大模糊源降级 + 卡片实底化后的边界/层次补齐。
   玻璃 ↔ 实底即时切换时，低端设备常因合成层重建出现偶发残影/白闪/错位，此规则减轻该风险并完善深浅色观感 */
html[data-vfx="lite"] .dynamic-flow-background { display: none; }
/* 性能档：关闭登录页动态流光背景，只留深蓝底色 */
html[data-vfx="lite"] .login-glass-mask { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; background: var(--lite-veil-modal); }
html[data-vfx="lite"][data-theme="dark"] .login-glass-mask { background: var(--lite-veil); }
html[data-vfx="lite"] .login-card .input { background: var(--login-lite-field-bg); border-color: var(--login-lite-field-border); }
html[data-vfx="lite"][data-theme="dark"] .login-card .input { background: var(--login-lite-field-bg); border-color: var(--login-lite-field-border); }
html[data-vfx="lite"] .login-card .login-brand img,
html[data-vfx="lite"] .login-card .btn-primary,
html[data-vfx="lite"] .login-card .btn-primary:hover,
html[data-vfx="lite"][data-theme="dark"] .login-card .btn-primary,
html[data-vfx="lite"][data-theme="dark"] .login-card .btn-primary:hover { filter: none; box-shadow: none; }
/* ================= 触屏设备：关闭悬停态反馈 =================
   触屏浏览器的 :hover 会在手指触摸后【粘住】（sticky hover），
   且手指滚动经过的元素也会被判定为 hover —— 于是：
     · 位移类（translateY / scale）让控件一直"浮着" → 看起来错位
     · 金色描边与光晕一直亮着 → 看起来廉价地闪光
     · 滚动过程中连续触发 → "抽风"
   这里只对【无真实悬停能力】的设备复位这些效果（@media (hover: none)），
   鼠标设备完全不受影响。
   注意：保留 :active 按压反馈 —— 触屏上它是有用的，不该一起关掉。
   维护约定：
   1) 今后新增的 hover 效果若含位移或金色光，需同步追加到本块；
   2) 若某条复位值【依赖卡片化断点】（例如"恢复为卡片本色"），不能并进本块
      （本块无条件生效，会污染触屏大屏设备），需另起一个
      `@media (hover: none) and (卡片断点)` 块 —— 见本块之后的第 6 条；
   3) 本块内每条【把颜色写死】的复位，都要检查深色下是否仍可读：
      .btn-outline 曾因写死 --navy-700 而在深色触屏上掉到 ≈1.2:1。 */
@media (hover: none) {
  /* 1. 位移复位 —— 解决"错位" */
.btn:hover,
  .btn-primary:hover,
  .card:hover,
  .stat-card:hover .ico,
  .color-swatch:hover,
  .nav-item:hover .ico { transform: none !important; }
  /* 2. 金色描边与光晕复位 —— 解决"金色光" */
/* 主按钮 / 金色按钮的悬停变化同样要复位：触屏粘住时按钮会保持"加深的渐变 + 加重的阴影"，
     看起来就是"高光闪一下还留在那里"。这里把悬停态直接还原为静止态。
     !important 足以压过深色主题里更高特异性的同名规则。 */
.btn-primary:hover,
  .btn-primary:hover:not(:disabled) {
    background: var(--accent-fill) !important;
    box-shadow: 0 4px 12px rgba(15, 30, 61, .22) !important;
  }
  /* ⚠️ 触屏复位只写一套：面/描边/文字全走令牌（--surface-2 / --field-border / --text），
     深浅各自由令牌取值 —— 原先这里"浅色一套 + 深色再写一遍"，
     两份写死的色值必然各漂 1~2 个色阶（曾经深色那份写死 #1a2642 / #26324e）。 */
  .btn-outline:hover {
    /* background 必须一起复位：悬停态现在是【金色浅底】，不复位就会在触屏上粘住

       （正是本块开头列出的"金色描边与光晕一直亮着 → 看起来廉价地闪光"）。 */
    background: var(--surface-2) !important;
    border-color: var(--field-border) !important;
    color: var(--navy-700) !important;
    box-shadow: var(--shadow-sm) !important;
  }
  /* 深色只差「文字档 + 无投影」两项（静态值见 .btn-outline 的深色覆盖），
     面与描边已是令牌，不再重复写死。 */
  html[data-theme="dark"] .btn-outline:hover { color: var(--text) !important; box-shadow: none !important; }
  /* 弹窗危险组的按钮：上面那批 .btn / .btn-outline 复位会把它们拉成透明底或深色实底，
     这里逐个还原静止态。
     !important 是必需的（对方也带 !important），靠更高特异性取胜。
     ⚠️ 中性按钮那条必须用 :not(.danger-btn) 排除删除键 ——
     否则它特异性更高，会在悬停时把红底也刷成中性色。 */
.modal-danger .danger-btn:hover:not(:disabled) {
    background: var(--danger) !important;
    color: #fff !important;
    filter: brightness(.9);
  }
  /* ⚠️⚠️ 这里的 :not(.active) 不能省 —— 漏掉的话，这条 !important 会把 .active 的
     金色高光一起复位成中性底，于是"按下 ⋯ 没有黄色点击反馈"。
     复位只写一套（令牌化），深色不再另写 —— 两份写死的色值正是"触屏深色缺高光"那类
     主题相关 bug 的温床。 */
  .modal-footer .modal-danger .btn:not(.danger-btn):not(.active):hover:not(:disabled) {
    background: var(--surface-2) !important;
    border-color: var(--field-border) !important;
    color: var(--navy-700) !important;
  }
  html[data-theme="dark"] .modal-footer .modal-danger .btn:not(.danger-btn):not(.active):hover:not(:disabled) { color: var(--text) !important; }
  /* ⚠️ 同样不能吃掉选中态：.theme-opt.active 的描边是金色（--gold-500），
     不带 :not(.active) 会让"当前选中项"在触屏悬停时把金色描边复位成 --border
     —— 与上面 ⋯ 的 .active 金色高光被抹掉是同一类问题（按容器身份复位时的通病），
     只是影响小（只丢描边，背景与阴影还在）。 */
.theme-opt:hover:not(.active) { border-color: var(--border) !important; }
  .card:hover {
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 0 0 .5px rgba(31, 58, 110, .05),
                0 12px 32px -14px rgba(31, 58, 110, .13) !important;
  }
  html[data-theme="dark"] .card:hover {
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05), 0 12px 32px -14px rgba(0, 0, 0, .55) !important;
  }
  /* 压过 .card:hover 的抬升与投影（否则容器会跟着悬停动） */
.table-card:hover {
    box-shadow: none !important;
    transform: none !important;
    background: transparent !important;
  }
  html[data-theme="dark"] .table-card:hover {
    box-shadow: none !important;
    background: transparent !important;
  }
  tbody tr:hover,
  .card-act-btn:hover,
  .dist-item[data-stat-go]:hover { background: transparent !important; }
  /* 表头排序项：只复位「非当前排序列」的悬停变金 —— 用 :not(.sorted) 避免盖掉
     排序中表头本该保持的金色高亮 */
.proj-table th[data-sort]:not(.sorted):hover { color: var(--text-muted) !important; }
  html[data-theme="dark"] .proj-table th[data-sort]:not(.sorted):hover { color: var(--field-label) !important; }
  .nav-item:hover { padding-left: 12px !important; }
  .stat-card[data-stat-go]:hover { transform: none !important; }
}
/* 6. 竖屏卡片专属：sticky hover 的「静止态」复位
   ⚠️ 必须单独起一个媒体查询、带上卡片化断点，不能并进上面那个块：
   上面第 3 条的 `tbody tr:hover { background: transparent !important }` 会连带命中卡片行，
   而 !important 无视特异性，卡片在触屏上就变成【透明】（露出页面底），
   看起来正像"点过的效果一直留着"。卡片的静止态应当是自身的 --card 底色。
   带上 (max-width:900px) and (min-height:540px) 后，该复位只在卡片形态下生效，
   触屏大屏设备（卡片形态不启用）里的表格行不会被误加底色。
   注意：仍然【保留 :active 按压反馈】—— 触屏上它是有用的，见本块开头的约定。 */
@media (hover: none) and (max-width: 900px) and (min-height: 540px) {
  /* 可点反馈：按规范用金色高光（深浅两套），不做位移/变色 */
.table-card table tbody tr:hover { background: var(--card) !important; }
}
/* ============ 全站视觉统一规范 v2 ============
   收敛历史遗留的硬编码差异（分隔线色 / 碎字号 / 同类胶囊 / 硬编码灰蓝字色），
   全部改走设计变量，深浅色与性能模式自动生效 */
/* 1. 分隔线统一为 --border：此前存在 #e1e7f1 / #eef1f7 / #edf1f7 / #f0f3f9 多种浅灰混用 */
.modal-header,
tbody td { border-color: var(--border); }

/* 旧胶囊类（.chip-todo / .chip-in_progress / .chip-blocked / .chip-done / .chip-due / .chip-overdue）
   已删除：chip() 只产出 .st-*（见 app.js 的 CHIP 映射），这 12 条规则从未命中，
   且是 #dc2626 / #ff9b9b 两个红色变体的唯一来源。语义红现收敛为：
   --danger（实底）/ --danger-ink（亮色）/ .st-delayed（浅底+强字）三处，各有明确角色。 */
html[data-vfx="lite"][data-theme="dark"] .modal-mask { background: var(--lite-veil); }
html[data-vfx="lite"][data-theme="dark"] .sidebar-mask { background: var(--lite-veil); }
/* 关掉装饰性动画：逐行动画 / 红点呼吸 / 点击涟漪 / hover 位移；
   面板高度过渡等必要动效保留（避免布局突变） */
html[data-vfx="lite"] tbody tr,
html[data-vfx="lite"] .log-red-dot,
html[data-vfx="lite"] .fade-in,
html[data-vfx="lite"] .stat-card .ico,
/* ⚠️ 页面入场必须按【子元素】关：原先写的是 `.content`（容器本身没有动画），
   于是性能档下页级淡入照旧 —— "关闭装饰性动效"这条规则形同虚设。 */
html[data-vfx="lite"] .content > *,
html[data-vfx="lite"] [data-enter="auto"] > * { animation: none !important; transition: none !important; }
/* 卡片形态专属节点（「…」快捷菜单按钮、名称行内的次要小字）在【非卡片形态】下必须彻底隐藏。
   这两个节点是渲染函数无条件输出的（桌面与移动同一份 DOM），不隐藏就会污染桌面表格。
   这里用卡片断点的【补集】表达，好处是条件只此一处、与下方 v4 块的断点互为镜像：
     卡片形态 = (max-width:900px) and (min-height:540px)
     非卡片   = (min-width:901px) or (max-height:539px)
   ⚠️ 改卡片断点时这两个条件必须一起改（另见 app.js 的 cardModeQuery）。 */
@media (min-width: 901px), (max-height: 539px) {
  .card-acts,
  .card-desc { display: none; }
}
/* ============ 全站视觉统一规范 v4：竖屏卡片化 ============
   目标：手机竖屏下把横向表格"翻译"成纵向卡片流 —— 一屏读完，不再需要横向拖动。

   实现：单套 DOM + 纯 CSS 重排。渲染函数仍只产出【一份表格 HTML】，
   本块负责在断点内把 thead 隐藏、tr/td 重排为卡片。
   由此换来三个收益：零 JS 重渲染、零方向监听（旋转屏幕即时互换形态）、
   桌面与移动共享同一份数据与事件绑定。

   ⚠️ 断点与 .tabbar 的显示条件【完全一致】（见上方"窄屏专用布局"段的推导）：
   两者同属"移动形态"，条件一致才能保证"有底栏即有卡片"的认知统一。
   横屏手机（高 393px）与平板横屏（宽 > 900px）不满足本断点，继续走表格 ——
   横屏可用宽度下 720px 表格本就几乎无需滚动，卡片反而浪费纵向空间。

   ⚠️ 本块是自我闭环的：整块删除即可完整回滚，不改动任何既有桌面规则。

   结构钩子（由渲染函数补，桌面端无任何样式影响，仅作卡片内的网格锚点）：
     行：.empty-row（空态）
     单元格：col-name / col-owner / col-status / col-prog / col-period /
             col-role / col-unit / col-phone / col-hire / col-actions
   其中 col-name 用 display:contents 消掉 td 盒子，让内部的 .n（标题）与
   .d（副信息：备注 / 日期）直接成为卡片网格项，从而拆到不同行。 */
@media (max-width: 900px) and (min-height: 540px) {
  .table-card {
    background: transparent;
    border: 0;
    box-shadow: none;
    border-radius: 0;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    overflow: visible;
  }
  .table-card:hover { transform: none; box-shadow: none; }
  html[data-theme="dark"] .table-card,
  html[data-vfx="lite"] .table-card,
  html[data-vfx="lite"][data-theme="dark"] .table-card {
    background: transparent;
    border: 0;
    box-shadow: none;
  }
  html[data-theme="dark"] .table-card:hover,
  html[data-vfx="lite"][data-theme="dark"] .table-card:hover {
    transform: none;
    box-shadow: none;
  }
  /* 解除横向滚动容器：卡片已无 720px 最小宽度，不应再出现横向滚动条 */
.table-card .table-wrap { overflow-x: visible; }
  .table-card table { display: block; min-width: 0 !important; width: 100%; }
  .table-card table tbody { display: block; }
  /* 表头隐藏。代价：项目页的表头点击排序在手机端不可用（默认仍按名称拼音序）。
     这是"不新增任何可见控件"约束下的必然取舍，详见规范 v4 的说明。 */
.table-card table thead { display: none; }
  .table-card table th,
  .table-card table td { white-space: normal; }
  .table-card table td {
    display: block;
    padding: 0;
    border: 0;
    background: none;
    text-align: left;
    min-width: 0;
  }
  .table-card table:has(.cell-actions) tbody td:last-child {
    position: static;
    box-shadow: none;
    background: none;
  }
  /* 操作列整体隐藏：卡片内不出现任何按钮 ——
     项目页 / 任务页的动作走"整行点击 → 动作弹窗"；
     用户页则"整行点击 → 编辑弹窗"（禁用/删除已整合进该弹窗的危险区）。 */
.table-card table td.col-actions { display: none; }
  .content { padding-left: 10px; padding-right: 10px; }
  /* ---------- B. 卡片外壳：一行 = 一张卡 ---------- */
.table-card table tbody tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: 10px;
    /* 行间距 8px → 10px：卡片内通常有 3 行（标题 / 元信息 / 进度），

       8px 会让它们贴得偏紧；用户卡的 10px 覆盖与此一致，全站一个节奏 */
    row-gap: 10px;
    align-items: center;
    text-align: left;
    box-sizing: border-box;
    /* 触控目标：整卡可点，高度下限 88px（远超 44px 标准） */
    min-height: 88px;
    padding: 14px;
    margin-bottom: 10px;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: background-color var(--dur-1) var(--ease-std), border-color var(--dur-1) var(--ease-std);
  }
  .table-card table tbody tr:last-child { margin-bottom: 0; }
  .table-card table tbody tr:hover { background: rgba(201, 169, 97, .1); }
  html[data-theme="dark"] .table-card table tbody tr:hover { background: rgba(201, 169, 97, .14); }
  /* 按压反馈：瞬时轻微下沉（不加过渡，避免拖影） */
.table-card table tbody tr:active { transform: scale(.995); }
  /* 空态行不是卡片：不参与卡片外壳，避免出现"空卡" */
.table-card table tbody tr.empty-row {
    background: transparent;
    border-color: transparent;
    min-height: 0;
    padding: 0;
  }
  .table-card table tbody tr.empty-row:hover { background: transparent; }
  .table-card table tbody td[colspan] { grid-column: 1 / -1; }
  .table-card table .empty-state { padding: 32px 16px; }
  /* ---------- C. 卡片通用部件 ----------
     本段只定"长什么样"（字号 / 色 / 截断），"放哪儿"交给各页的 D / E / F 段。
     这样三类卡片能各自排布，又不重复抄同一套排版值。

     卡片标题 16px/700（规范 §2「卡片标题」层级）；
     副信息与元信息一律 12px / 11px + --text-muted ——
     首页卡片之所以"不堆砌"，靠的就是把次要信息统一压成浅灰小字、
     只让名称与状态胶囊保持高对比。 */
/* 标题与副信息"提"到卡片网格：display:contents 让 td 不再生成盒子，
     内部的 .n / .d 直接成为网格项，因而能拆到不同行 */
.table-card table td.col-name { display: contents; }
  .table-card table td.col-name .n {
    max-width: none;
    margin: 0;
    justify-content: flex-start;
    text-align: left;
    align-items: center;
    font-size: 16px;   /* 规范 §2「卡片标题」层级 */
    font-weight: 700;
    line-height: 1.4;
  }
  /* 标题允许换行（最多两行），超出截断。
     flex 用 0 1 auto 而非 1 1 auto —— 与首页 .pname .nm 一致：
     名称不吃满整行，逾期角标才会紧贴名称之后，而不是被推到卡片最右端遥相呼应
     （被推开的角标与名称"不成组"，观感上就是散落）。 */
.table-card table td.col-name .n .nm {
    flex: 0 1 auto;
    min-width: 0;
    white-space: normal;
    overflow: hidden;
    text-overflow: clip;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
  }
  /* 副信息（项目备注 / 任务日期）：浅灰小字，单行截断 */
.table-card table td.col-name .d {
    max-width: none;
    margin: 0;
    text-align: left;
    display: flex;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    line-height: 1.5;
  }
  .table-card table td.col-name .d > * { min-width: 0; }
  /* 深色进度轨道：已统一到基础区的
     html[data-theme="dark"] .progress { background: rgba(255,255,255,.16) } ——
     半透明白可自适应任何承载底色（实底卡片 / 页面底色 / 弹窗），
     所以此处不再需要针对"卡片形态"的单独特异性覆盖。
     历史：这里原有一条 (0,7,1) 覆盖，起因是卡片行底色与轨道固定色太接近；
     2026-10-14 第一级卡片由半透明改实底后，首页卡片行也出现同样问题，
     于是把自适应做法上提到全局规则，两处一并解决。 */
/* 元信息行（负责人 / 起止日期 / 入职日期）：浅灰小字，不抢视觉重心。
     日期在桌面是 var(--text)，手机卡片上压成 muted —— 与首页 .pmeta 的日期同一层级 */
.table-card table .cell-owner { justify-content: flex-start; gap: 8px; min-width: 0; }
  .table-card table .cell-owner .avatar { width: 22px; height: 22px; font-size: 11px; }
  /* 负责人块压成【单行】：桌面是"姓名 / 任务数"两行，放进元信息行会把整行撑高，
     导致旁边的状态胶囊与日期不在同一视觉基线上。改为姓名 + 任务数并排，
     姓名可截断、任务数不压缩。 */
.table-card table .cell-owner .info {
    display: flex;
    align-items: baseline;
    gap: 4px;
    font-size: 12px;
    min-width: 0;
    overflow: hidden;
  }
  .table-card table .cell-owner .info > div { white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .table-card table .cell-owner .info > .t { flex-shrink: 0; font-size: 11px; margin-top: 0; }
  .table-card table td.col-period,
  .table-card table td.col-hire {
    font-size: 11px;      /* 规范 §2「行尾时间」层级 */
    color: var(--text-muted);
    white-space: nowrap;
    min-width: 0;
  }
  /* ---------- D. 项目管理卡 ----------
     版式（3 行 + 右上「…」）：
       第 1 行：项目名 + 备注（小字）+ 逾期角标（如有）
       第 2 行：[状态胶囊] 起止日期              (头)负责人
       第 3 行：进度条 + 百分比

     第 1 行的"名称 + 小字 + 角标"同处一行，学的是首页 .mine-t-name 的信息层级
     （名称 + 次要小字 + 逾期角标同处一个 flex 容器）。

     ⚠️ 这件事纯 CSS 做不到：单个网格里 .n 与 .d 是两个独立网格项，无法流式并排。
     因此渲染函数在 .n 内额外输出一个 .card-desc（桌面与横屏下由补集媒体查询隐藏），
     卡片模式再隐藏原来的 .d —— 二者是同一份数据的两种呈现，不是两份数据。

     第 2 行三列用 max-content / minmax(0,auto) / minmax(0,1fr)：
     日期列取 minmax 而非纯 auto，是为了在极窄屏（如 320px）下允许它收缩并被省略号截断，
     而不是把整张卡撑出横向滚动。 */
.table-card table.proj-table tbody tr {
    grid-template-columns: max-content minmax(0, auto) minmax(0, 1fr);
    /* 项目卡的行最多（名称行 + 元信息行 + 进度行，名称行在有逾期角标时还会折成两行），

       间距再放宽到 12px，读起来才不拥挤 */
    row-gap: 12px;
    position: relative;          /* 右上角两个图标按钮的绝对定位基准 */
    /* 标题行右侧让位量：给 `right: 11px` 的两个 30px 图标按钮（间隙 4px）腾地方。
       76 = 11 右边距 + 30 + 4 + 30 + 1px 余量。
       ⚠️ 该值由按钮尺寸反推，落不进 §3.1 的 4/8/…/64 阶梯，已在规范 §3.2 登记例外；
          原值 74px 比实际占位 75px 少 1px，长名称末尾会被最左那颗按钮压住。 */
    --card-act-reserve: 76px;
  }
  .table-card table.proj-table td.col-name .n {
    grid-column: 1 / -1;
    grid-row: 1;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    /* 给右上角那两个绝对定位的图标按钮让位，否则长名称会钻到按钮底下 */
    padding-right: var(--card-act-reserve);
  }
  /* 名称：单行 + 省略号。卡片上不再允许换两行 —— 否则会与备注争行、卡高不可控 */
.table-card table.proj-table td.col-name .n .nm {
    flex: 0 1 auto;
    min-width: 0;
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    -webkit-line-clamp: none;
  }
  /* 备注小字：优先吃掉整行剩余空间；剩余不足 88px 时整段换到第二行，
     而不是被压成两三个字后强行截断 —— 这是 flex-wrap 给出的自适应结果 */
.table-card table.proj-table td.col-name .n .card-desc {
    flex: 1 1 0;
    min-width: 88px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* 换行优先级：名称 → 逾期角标 → 备注。
     备注在 DOM 里排在角标之前，但它是最不紧急的一条，应当【最先被挤下去】；
     用 order 把它排到最后即可（纯 CSS，不动 DOM，桌面端 order 无影响因为不换行）。
     390px 下"名称 + 备注 + 角标"三者放不进一行（名称 160 + 备注 88 + 角标 80 + 间距 > 298），
     因此必须有一条让位 —— 让备注让位比让角标让位更合理：角标是告警，不该沉到第二行。 */
.table-card table.proj-table td.col-name .n .nm { order: 0; }
  .table-card table.proj-table td.col-name .n .card-desc { order: 2; flex-shrink: 1; }
  /* 桌面用的备注行在卡片上由上面的 .card-desc 承担 */
.table-card table.proj-table td.col-name .d { display: none; }
  .table-card table.proj-table td.col-status { grid-column: 1; grid-row: 2; justify-self: start; align-self: center; }
  .table-card table.proj-table td.col-period { grid-column: 2; grid-row: 2; justify-self: start; }
  .table-card table.proj-table td.col-owner { grid-column: 3; grid-row: 2; justify-self: end; }
  /* 负责人只要名字，不要"N 个任务" —— 整行宽度有限，任务数让位给状态胶囊与日期 */
.table-card table.proj-table td.col-owner .info .t { display: none; }
  .table-card table.proj-table td.col-prog { grid-column: 1 / -1; grid-row: 3; }
  /* ---- 卡片右上角两个快捷按钮（编辑项目 / 查看日志）----
     用一个绝对定位的容器装住，不走网格列 —— 免得为两个 30px 的按钮再切一列、
     把名称的可读宽度压窄。整卡点击是"进该项目任务页"，这两个按钮是它的两个例外动作。
     为什么用容器而不是逐个按钮绝对定位：编辑按钮对"非管理员且非负责人"不渲染，
     逐个写 right 偏移会在只剩一个按钮时把它留在半空。 */
.table-card table tbody tr .card-acts {
    position: absolute;
    top: 11px;
    right: 11px;
    display: flex;
    gap: 4px;
  }
  .table-card table tbody tr .card-act-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    width: 30px;
    height: 30px;
    padding: 0;
    border: 0;
    border-radius: 8px;
    background: none;
    color: var(--text-muted);
    cursor: pointer;
    transition: background-color var(--dur-1) var(--ease-std), color var(--dur-1) var(--ease-std);
  }
  .table-card table tbody tr .card-act-btn svg { width: 17px; height: 17px; }
  /* 触控目标：视觉尺寸保持 30px（右上角要克制），但用伪元素把【可点区域】扩到 46px，
     达到规范要求的 44px —— 手指点的准是刚需，视觉大小是另一回事。
     ⚠️ 两个按钮各扩 8px 后相邻边缘会重叠 12px：这是刻意的，
     手指落在缝里时命中谁由 DOM 顺序决定（最后一个在最上层），不会两边都不响应。 */
.table-card table tbody tr .card-act-btn::before {
    content: '';
    position: absolute;
    inset: -8px;
  }
  .table-card table tbody tr .card-act-btn:hover { background: rgba(201, 169, 97, .16); color: var(--text); }
     }
/* 动作弹窗（原 H 节）已整条下线：三个页面的整卡点击落点各自确定后它没有调用方了 ——
     项目卡进任务页 / 任务卡与用户卡直接开编辑弹窗，删除则统一进各弹窗的 .modal-danger 危险区。
     其 .action-list / .action-item 样式、openActionSheet()、以及仅它使用的 chevron-right 图标
     一并移除，避免留死代码。 */
/* ============ 竖屏端（顶栏 + 底栏并存）：两条 chrome 统一为实底 ============
      用户要求（2026-10-15）：只要顶栏与底栏【同时存在】，两条 chrome 一律实心、不透明、不模糊；
      只有顶栏时（手机横屏 / 平板横屏 / 桌面）顶栏保持毛玻璃不变。

      ⚠️ 判定条件必须与 .tabbar 的显示条件【完全一致】：@media (max-width: 900px) and (min-height: 540px)
         该条件同时是「竖屏卡片化」的断点（有底栏即有卡片化，见上方竖屏卡片块与规范 v4）。
         改一处必须改另一处，否则会出现"有底栏、顶栏却还是玻璃"的材质不一致。

      为什么：底栏因 Safari 底部地址栏取色【必须是实底】（硬约束，见 .tabbar 处注释）；
      而且 Safari 顶部工具栏取色只认 <meta name="theme-color"> 的纯色（= --topbar-bg），
      顶栏若仍是玻璃就与它注定有色差。改实底后顶部工具栏与顶栏完全同色。

      与性能档的关系：html[data-vfx="lite"] 下顶栏本就已是实底（见上方性能档块），
      本块只管「质量档 + 竖屏」；两条规则结论一致、互不冲突。

      回滚：整块删除即可，不改动任何桌面规则。 */
@media (max-width: 900px) and (min-height: 540px) {
  .topbar {
       background: var(--topbar-bg);   /* 与 <meta theme-color> 同源 —— Safari 顶部工具栏随之同色 */
       -webkit-backdrop-filter: none;
       backdrop-filter: none;
     }
  /* 底栏本来就是实底、无模糊；这里显式写一遍，让"两条 chrome 同材质"在代码里可读，
        也防未来有人给底栏加回 backdrop-filter（那会破坏 Safari 底部取色）。 */
.tabbar {
       background: var(--tabbar-bg);
       -webkit-backdrop-filter: none;
       backdrop-filter: none;
     }
     }
/* ============================================================
   边织边检后台 —— 业务组件补充样式（bzbj 新增）
   基座沿用 janelon-admin 视觉语言；此处全部走既有令牌，
   深浅色随 data-theme 自动适配，不引入新的魔法色值。
   ============================================================ */
/* ---- 键值对信息列表（标注弹窗 / 报警详情） ---- */
.kv-list { display: flex; flex-direction: column; gap: 8px; }
.kv-list .row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding-bottom: 8px; border-bottom: 1px solid var(--border); font-size: 13px;
  /* 右侧取值 word-break: break-all，长文本会折行；内容属「正文」→ 显式 1.4 档（规范 §2.3） */
  line-height: 1.4;
}
.kv-list .row span { color: var(--text-muted); flex: none; }
.kv-list .row b { color: var(--text); font-weight: 600; text-align: right; word-break: break-all; }
.kv-list .row:last-child { border-bottom: none; padding-bottom: 0; }
/* 末行不画分隔线 */
/* ---- 配置页 / 设置页的辅助说明 ---- */
.pref-hint { display: block; font-size: 12px; line-height: 1.5; color: var(--text-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
/* 行内小字在 pref 行里不再需要基座的 -2px 上边距（原先是内联 margin:0 逐个补的） */
.pref-txt .form-hint, .rel-item .form-hint { margin: 0; }
/* 阈值 / 设置的「标签 + 控件」行：控件宽度统一走 f-sel-lg 一档，不再逐行写宽度 */
.pref-row .input, .pref-row .select { width: 200px; }
/* ---- 时间线（配置变更日志） ---- */
.tl-list { display: flex; flex-direction: column; gap: 12px; }
/* ---- 图片标注：瀑布流网格 ---- */
.img-grid { column-count: 5; column-gap: 16px; margin-top: 4px; }
@media (max-width: 1400px) {
  .img-grid { column-count: 4; }
 }
@media (max-width: 1100px) {
  .img-grid { column-count: 3; }
 }
/* ⚠️ 卡片形态（竖屏）下不能透明 —— `!important` 会无视特性性，把卡片行刷成透明、
   露出页面底，看起来正像"点过的效果一直留着"。必须与基座第 6 条同构。 */
@media (max-width: 900px) and (min-height: 540px) {
  .img-grid { column-count: 2; }
 }
@media (max-width: 600px) {
  .img-grid { column-count: 2; column-gap: 10px; }
 }
.img-card {
  position: relative; break-inside: avoid; margin: 0 0 16px; overflow: hidden;
  background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
  cursor: pointer; transition: box-shadow var(--dur-1) var(--ease-std), border-color var(--dur-1) var(--ease-std);
}
.img-card.has-defect { border-color: var(--danger); }
.img-card:hover { box-shadow: var(--shadow); border-color: var(--gold-500); }
/* 勾选框：缩略图底色是浅网格，裸 checkbox 几乎看不见 → 给一个白色底座 */
.img-check {
  position: absolute; top: 8px; left: 8px; z-index: 2;
  width: 22px; height: 22px; border-radius: 6px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255, 255, 255, .92); border: 1px solid var(--field-border);
  box-shadow: 0 1px 3px rgba(15, 30, 61, .14);
}
.img-check input { width: 14px; height: 14px; margin: 0; accent-color: var(--gold-600); cursor: pointer; }
.img-thumb { position: relative; background: var(--navy-100); line-height: 0; }
/* 画幅统一 4:3（与真实检测相机一致）：显式锁定比例，避免依赖 SVG 内在尺寸的浏览器差异 */
.img-thumb img, .img-viewer img { width: 100%; aspect-ratio: 4 / 3; height: auto; display: block; }
/* 图片角标属「白字压彩色实底」家族，浮在图片上、不吃页面底色 → 在两套主题下都要自带对比度。
   底色沿用 ⑥ 段的手法：同色相加深一档（不引入新色相），白字实测 ≥5.3:1（AA 小字 4.5）。
   ⚠️ 这里的深红 / 深绿只给角标底色用，不回写 --danger / --success —— 那两个是页面底色与
   图标色，改了会连带影响按钮与图表。 */
.img-flag {
  position: absolute; top: 8px; right: 8px; font-size: 11px; padding: 2px 8px; border-radius: 999px;
  background: #b93a3a; color: #fff; line-height: 1.4;   /* 白 5.63 */
}
/* 标注状态与瑕疵标记同为「图片级旗标」→ 同一位置（右上角），不再一个右上、一个右下 */
.img-flag.ok { background: #22795a; }                     /* 白 5.32 */
.img-meta { padding: 12px; display: flex; flex-direction: column; gap: 4px; }
.img-meta .ln { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 13px; line-height: 1.4; color: var(--text); }
.img-meta .sub { font-size: 12px; line-height: 1.5; color: var(--text-muted); }
html[data-theme="dark"] .img-thumb { background: var(--field-bg); }
/* ---- 标注大图查看器（占位） ---- */
.img-viewer {
  position: relative; background: var(--navy-100); border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
}
.img-viewer-hint { font-size: 12px; line-height: 1.5; color: var(--text-muted); text-align: center; padding-bottom: 12px; }
html[data-theme="dark"] .img-viewer { background: var(--field-bg); }
/* ---- 瑕疵类型快速选择 ---- */
.defect-picks { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.defect-pick {
  display: inline-flex; align-items: center; gap: 8px; font-size: 13px;
  height: var(--ctl-h-xs); padding: 0 12px; border-radius: 999px; cursor: pointer;
  background: var(--field-bg); border: 1px solid var(--field-border); color: var(--text);
  transition: background-color var(--dur-1) var(--ease-std), border-color var(--dur-1) var(--ease-std), color var(--dur-1) var(--ease-std);
}
/* ⚠️ hover 必须排除 .active —— 两者特异性相同（都是 0,2,0），hover 写在后面就会
   把"已选中"的整块底色覆盖成浅底，鼠标停在选中项上时选中态直接消失（白字落白底）。 */
.defect-pick:hover:not(.active) { border-color: var(--gold-500); background: rgba(201, 169, 97, .10); }
.defect-pick.active { background: var(--navy-800); border-color: var(--navy-800); color: #fff; }
.defect-pick .idx {
  font-size: 11px; width: 16px; height: 16px; border-radius: 4px; display: inline-flex;
  align-items: center; justify-content: center; background: var(--field-border); color: var(--text-muted);
}
.defect-pick.active .idx { background: rgba(255, 255, 255, .22); color: #fff; }
html[data-theme="dark"] .defect-pick { background: var(--surface-2); border-color: var(--border); }
/* 深色选中态：浅色用「深藏蓝实底 + 白字」，深色里照搬会变成"更暗的块"，贴在深色弹窗上
   完全看不出选中（`html[data-theme="dark"] .defect-pick` 的特异性 0,2,1 还压过了 .active 的 0,2,0）。
   按深色模式的既有语言翻过来：**金色实底 + 深墨字**（同 `.nav-item.active` / `.theme-opt.active`
   的金色强调），既沿用全局调色板，又保证「选中」一眼可辨。 */
html[data-theme="dark"] .defect-pick.active {
  background: var(--gold-500); border-color: var(--gold-500); color: var(--navy-900);
  box-shadow: 0 2px 10px -2px rgba(201, 169, 97, .35);
}
html[data-theme="dark"] .defect-pick.active .idx { background: rgba(5, 11, 22, .18); color: var(--navy-900); }
/* ---- 织布机关联（微信绑定） ---- */
.rel-list { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; max-height: 340px; overflow: auto; }
@media (max-width: 600px) {
  .rel-list { grid-template-columns: 1fr; }
 }
.rel-item {
  display: flex; align-items: center; gap: 8px; font-size: 13px;
  padding: 8px 12px; border: 1px solid var(--border); border-radius: 10px; cursor: pointer;
  background: var(--field-bg); color: var(--text);
}
.rel-item:hover { border-color: var(--gold-500); }
.rel-item input { accent-color: var(--gold-600); }
/* ---- 统计卡片内的大数字与竖排小字 ---- */
.stat-card .num small { font-weight: 400; color: var(--text-muted); }
.table-card table td code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
/* bzbj：分段控件（#th-scope / #rp-range）未挂滑块时的选中态兜底 —— 白底卡面上给出明确选中块 */
.seg-toggle .seg-btn.active { background: #fff; box-shadow: 0 1px 5px rgba(15, 30, 60, .14); }
html[data-theme="dark"] .seg-toggle .seg-btn.active { background: var(--surface-3); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .06), 0 1px 5px rgba(0, 0, 0, .35); }
/* ============================================================
   边织边检后台 —— UI 一致性统一层（bzbj）
   ------------------------------------------------------------
   只做「收敛 / 补齐」，不改基座既有取值。规则依据《视觉语言统一规范》：
     · 间距阶梯   4 / 8 / 12 / 16 / 20 / 24 / 28 / 32
     · 行高 4 档  1.2(tight) / 1.4(snug) / 1.5(normal) / 1.6(relaxed)
     · 左轴对齐   卡片内文 20px；表格内文 16px；同一容器只允许一条左轴
     · 控件高度   --ctl-h-xs 34 / --ctl-h 38 / --ctl-h-lg 40 / --ctl-h-xl 44
   已登记的基座例外（不是偏差，勿在「间距审计」里误判）：
     · 列表行间距 6 / 控件组内 10 / 分段控件内衬 3 + 格间距 4
     · .status-chip 垂直 3（定高会破坏基线对齐）、.pref-row gap 18、.cell-owner .info .t
       margin-top 2 —— 均属「高度无法显式化的元素」，见基准 §3「例外与暂缓」
   详细规范见 docs/rules/UI统一规范.md；配套自检脚本 scripts/ui-audit.js
   ============================================================ */
/* ---------- ① 控件宽度：全站只允许这三档 ---------- */
.f-kw { width: 240px; }
/* 关键词搜索框 */
.f-sel-lg { width: 200px; }
/* 内容较长：设备 / 织布机 / 阈值目标 / 设置项 */
.f-sel { width: 160px; }
/* 常规下拉筛选 */
/* ---------- ② 栅格比例语义化（替代内联 grid-template-columns） ---------- */
.dash-grid.split-7-5 { grid-template-columns: 1.4fr 1fr; }
.dash-grid.split-3-2 { grid-template-columns: 1.5fr 1fr; }
.dash-grid.split-1-1 { grid-template-columns: 1fr 1fr; }
.stat-grid.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.stat-grid.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
/* ---------- ③ 纵向堆叠容器（替代 margin-top 逐个拼间距） ---------- */
.stack { display: flex; flex-direction: column; gap: 12px; }
.stack-16 { display: flex; flex-direction: column; gap: 16px; }
/* 弹窗正文的左右分栏：图片 + 信息（左轴各自独立，间距 20 走阶梯） */
.modal-split { display: grid; grid-template-columns: 1.3fr 1fr; gap: 20px; align-items: start; }
@media (max-width: 720px) {
  .modal-split { grid-template-columns: 1fr; gap: 16px; }
}
/* 标注弹窗「原地切图」的内容动画：只位移 + 淡入内容层，不碰弹窗本体。
   下一张从右进、上一张从左进，时长/缓动沿用全局 --dur-2 / --ease-out；
   关闭动画仍由 .modal-mask.closing 负责（两者互不干扰）。 */
@keyframes splitInNext { from { opacity: .35; transform: translateX(16px); } to { opacity: 1; transform: none; } }
@keyframes splitInPrev { from { opacity: .35; transform: translateX(-16px); } to { opacity: 1; transform: none; } }
.modal-split.is-next { animation: splitInNext var(--dur-2) var(--ease-out) both; }
.modal-split.is-prev { animation: splitInPrev var(--dur-2) var(--ease-out) both; }
@media (prefers-reduced-motion: reduce) {
  .modal-split.is-next, .modal-split.is-prev { animation: none; }
}
/* ---------- ④ 文本语义类（替代内联 color / font-size / 等宽字体） ---------- */
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }
.t-muted { color: var(--text-muted); }
.t-13 { font-size: 13px; line-height: 1.4; color: var(--text-muted); }
/* ⚠️ 语义「动作色」走 --action-* 而不是直接用 --*-ink：浅色下这两个取值不同，
   原因与取值见文件末尾 ⑲ 段。--*-ink 令牌本身不动（用户要求沿用深色亮色）。 */
.t-warn { color: var(--action-warn); }
/* ---------- ⑤ 幽灵按钮的语义着色（替代内联 color） ---------- */
.btn-ghost.is-danger { color: var(--action-danger); }
.btn-ghost.is-warn { color: var(--action-warn); }
.btn-ghost.is-ok { color: var(--action-ok); }
@media (hover: hover) {
  .btn-ghost.is-danger:hover:not(:disabled) { background: rgba(214, 69, 69, .10); color: var(--action-danger); }
  .btn-ghost.is-warn:hover:not(:disabled) { background: rgba(208, 138, 42, .12); color: var(--action-warn); }
  .btn-ghost.is-ok:hover:not(:disabled) { background: rgba(47, 158, 119, .12); color: var(--action-ok); }
}
/* ---------- ⑥ 统计卡：图标底色收敛到「藏蓝 / 古铜金 / 红 / 绿」四族 ----------
   白字形压在这些底色上，按 WCAG 非文本对比 3:1 校验（实测值写在各行尾）：
   · 藏蓝 / 红两族本身够深，直接用品牌令牌；
   · 金 / 绿两族品牌令牌偏亮（金 #c9a961 只有 2.25:1、绿 #34c68a 只有 2.19:1），
     图标块改用同色相加深一档的专用值（仍是「古铜金 / 深绿」，不引入新色相）。
   ⚠️ 只改图标块的底色，正文里的金色 / 绿色文字仍用 `--gold-*` / `--success*` 令牌。 */
.stat-card .ico.ico-navy { background: linear-gradient(135deg, var(--navy-700), var(--navy-800)); }   /* 白 14.4 ~ 16.5 */
.stat-card .ico.ico-primary { background: linear-gradient(135deg, var(--navy-600), var(--navy-700)); }  /* 白 11.1 ~ 14.4 */
.stat-card .ico.ico-ok { background: linear-gradient(135deg, #2f9e77, #22795a); }        /* 白 3.35 ~ 4.9 */
.stat-card .ico.ico-warn { background: linear-gradient(135deg, var(--warning), var(--warning-ink)); }   /* 白 3.6 ~ 4.4 */
.stat-card .ico.ico-gold { background: linear-gradient(135deg, #b08a3a, #8a6a28); }      /* 白 3.2 ~ 4.4 */
.stat-card .ico.ico-danger { background: linear-gradient(135deg, var(--danger), var(--danger-ink)); }   /* 白 3.7 ~ 4.4 */
/* ⚠️ 这 6 个色调类是【动态拼接】的：app.js 写 `` `ico-${c.tone}` ``，tone 枚举为
   navy / primary / gold / ok / danger / warn。静态搜 `ico-gold` 是搜不到的 —— 2026-10-17 的
   死样式清理就以"0 引用"为由删过 gold 与 danger 两条，结果首页 4 张统计卡（设备 / 待标注 /
   待处理报警 / 降级设备）的图标失去底色，只剩 `.stat-card .ico` 基类的 `color:#fff` →
   白图标落在浅色卡面上，**可读性≈0**。现已恢复，并在 `ui-audit` 加了 C15 断言
   （每个 `.stat-card .ico` 必须有非透明底色）+ `css-usage.js` 把 `ico-*` 计入动态前缀。 */
/* 大数字后的单位（「8.4 分」）：不参与大数字层级，12px + 4px 间距，基线对齐 */
.stat-card .num .unit { font-size: 12px; font-weight: 400; color: var(--text-muted); margin-left: 4px; }
/* 概览条目的名称列：定宽起点，保证同名长不齐时柱条左端仍对齐 */
.dist-item .nm { flex: 1; min-width: 64px; }
/* ---------- ⑦ 表格：单元格结构与对齐统一 ---------- */
/* 表体不折行（基座 thead th 已有 nowrap，tbody td 缺失 → 曾出现「边织边/检」「见龙纺织一/厂」碎行）。
   表变宽后由 .table-wrap 横向滚动，操作列 sticky 常驻，符合基座窄屏约定。 */
.table-card table tbody td { white-space: nowrap; line-height: 1.4; }
/* 单元格副行：12px / muted / 与主行 4px 间距 / 行高 1.5（基座只在移动端媒体查询里定义过） */
.table-card table tbody td .card-desc { font-size: 12px; color: var(--text-muted); line-height: 1.5; margin-top: 4px; }
/* 操作列按钮 = 【图标 + 文字】，对齐基准项目 d.janelon.com 的 `.btn.btn-ghost.btn-sm`
   （`<button class="btn btn-ghost btn-sm"><i></i> 编辑</button>`）。
   尺寸不再另设字号 —— 基座 `.cell-actions .btn`（padding 5px 8px / font-size 12px，
   与基准项目逐字一致，见上方 1317 行那一条）就是目标值；这里只把图标收小一档，
   使 12px 文字与图标视觉重量相当（15px 图标在大图标按钮上合适，配 12px 文字就偏重）。 */
.cell-actions .btn svg { width: 14px; height: 14px; }
/* 分页条 / 列表尾部：上间距一律交给 `#page-content > * + *` 的 16px，
   这里不再自带 padding-top（原先自带 16px、又在文件尾部被一条同名规则清零，
   同一属性两处来源 → 已合并为 0）。 */
/* 分页条：左「共 N 条」/ 右「上一页 · 页码组 · 下一页」。
   页码组沿用分段控件（.seg-toggle）的语言 —— 轨道 = surface-2 + border，当前页 = card 底 + 加粗，
   不新增色相 / 阴影；≤600px 收成「第 x/y 页」+ 纯图标上下页（与顶栏 .btn-txt 同一收起规则）。 */
.pager-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 0; flex-wrap: wrap; }
.pager-info { font-size: 13px; line-height: 1.4; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.pager-ctrl { display: flex; align-items: center; gap: 8px; margin-left: auto; }
.pager-nums {
  display: inline-flex; align-items: center; gap: 2px; padding: 2px;
  border: 1px solid var(--border); border-radius: 999px; background: var(--field-bg);
}
.pager-num {
  min-width: 26px; height: 26px; padding: 0 6px; border: 0; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; color: var(--text-muted); cursor: pointer;
  font: inherit; font-size: 12px; line-height: 1.2; font-variant-numeric: tabular-nums;
}
.pager-num:hover { color: var(--text); }
/* 当前页 = 主按钮那套实底（accent-fill + 白字），一眼可辨，且与「主操作」同一语言 */
.pager-num.is-on { background: var(--accent-fill); color: #fff; font-weight: 600; }
.pager-gap { padding: 0 2px; font-size: 12px; line-height: 1.4; color: var(--text-muted); }
.pager-page-m { display: none; font-size: 12px; line-height: 1.4; color: var(--text-muted); font-variant-numeric: tabular-nums; }
@media (max-width: 600px) {
  .pager-nums { display: none; }
  .pager-page-m { display: inline; }
  .pager-bar .btn-txt { display: none; }
}
/* 无边框工具条（设置页 Token 行、底部保存行） —— 替代内联 border/background/padding */
.toolbar-plain { border: none; background: none; padding: 12px 0 0; }
.toolbar-plain.is-foot { padding: 0; }
/* 「加载更多 / 已加载全部」所在的行 */
.list-foot { display: flex; align-items: center; justify-content: center; gap: 12px; padding: 0; }
/* ---------- ⑨ 报警详情：关联图片块 ---------- */
.alarm-media { display: flex; align-items: center; gap: 12px; margin-top: 16px; }
.thumb-43 { width: 160px; aspect-ratio: 4 / 3; height: auto; border-radius: 10px; border: 1px solid var(--border); flex: none; display: block; }
/* ---------- ⑩ 图片瀑布流空态：多列容器内退化为单列，空态才能整行居中 ---------- */
.img-grid.is-empty { column-count: 1; }
/* ---------- ⑪ 统一层：工具条内小字、对齐与间距工具 ---------- */
/* .form-hint 基座带 -2px 上边距（为贴紧 label）；在工具条里是独立文本，归零 */
.toolbar .form-hint { margin: 0; }
/* 详情弹窗里「图 + 文字」的文字列 */
.col-fill { flex: 1; min-width: 0; }
/* 已由 .toolbar 提供的右对齐（替代内联 justify-content:flex-end） */
.justify-end { justify-content: flex-end; }
/* 卡片之间的标准 16px 纵向间距（替代内联 margin-top:16px） */
.mt-16 { margin-top: 16px; }
/* 统计卡文字列 */
.stat-card .stat-body { min-width: 0; }
/* 卡体内的统计栅格：去掉基座为「页面级栅格」预留的 24px 下边距 */
.card-body > .stat-grid { margin-bottom: 0; }
/* 生效配置预览块（固定在 280px 内滚动，不占满弹窗高度） */
.json-block.is-preview { max-height: 280px; }
/* ---------- ⑫ 统一层：补齐两处未声明的行高 ---------- */
/* 表头 12px 与表体 14px 同取 snug 1.4 —— 行高档位统一优先于"用内边距凑等高"；
   表头带比数据行矮 2.8px 是 12px/14px 字号差带来的，属正常排版结果。 */
.table-card table thead th { line-height: 1.4; }
/* ---------- ⑬ 统一层：深浅色成对补丁 ---------- */
/* 藏蓝族图标块在深色下必须比卡片亮一档 —— 否则方块与卡片同色，只剩图标"悬空"。
   取值同基座深色主按钮一族（#2f5cb3 / #20428f），保持藏蓝色相不变。 */
html[data-theme="dark"] .stat-card .ico.ico-navy { background: var(--accent-fill-deep); }
html[data-theme="dark"] .stat-card .ico.ico-primary { background: var(--accent-fill); }
/* ---------- ⑭ 统一层：页面级区块的纵向节奏 = 16px ----------
   目标：`#page-content` 下任意两个同级区块（工具条 / 统计栅格 / 卡片组 / 分页条 / 空态）
   之间恒定 16px —— 与基准「卡片之间 16px」同一取值。
   基座原值各用各的：.toolbar 18 / .welcome-banner 20 / .stat-grid 24 / .dash-grid 0，
   相邻两个 .dash-grid 之间**完全没有间距**（首页「报警趋势 + Top6」与「最近报警 + 在线率」贴住）。
   做法：下外边距不再承担间距（一律归零），间距只由「相邻兄弟的 margin-top」唯一提供，
   这样无论区块自身带什么 margin，节奏都不会跑偏。 */
#page-content > * { margin-top: 0; margin-bottom: 0; }
#page-content > * + * { margin-top: 16px; }
/* ============================================================
   边织边检后台 —— 竖屏 / 窄屏适配统一层（bzbj ⑮）
   ------------------------------------------------------------
   机制学自 janelon-admin《视觉语言统一规范》§3 / §3.1 / §4 / §5，只做两件事：
     ① 断点与移动形态门控与基座完全对齐 —— 600 / 900 / 1200；
        移动形态 = `max-width: 900px and min-height: 540px`，与基座 .tabbar 的显示条件
        逐字一致（"有底栏即有移动形态"，改一处必须改另一处）；
     ② 补回被 bzbj 语义栅格「压掉」的降列规则。

   ⚠️ 主因（本轮竖屏之所以糟糕）：
      UI 一致性统一层 ② 引入的语义栅格是【双类选择器】——
      `.dash-grid.split-7-5` / `.stat-grid.cols-4` 特异性 (0,2,0)，
      高于基座单类的降列规则 `.dash-grid` / `.stat-grid` (0,1,0)。
      于是基座在 1200 / 900 / 600px 三处的 grid-template-columns 覆盖【全部失效】：
        · 首页 / 报表 / 阈值页的 .dash-grid.split-* 竖屏仍是两栏并排
          → 右栏被压成一条缝（阈值页的 JSON 预览不可读、被挤出视口）
        · 首页 .stat-grid.cols-4 竖屏仍是 4 列
          → 文字竖排折行（"织布机"→"织布/机"）、"78.6%"被切掉
      这里用同等特异性把降列补回来。
   ============================================================ */
/* ---------- ① 分栏栅格：≤1200 一律纵向单列（对齐基座 .dash-grid 的降列点） ---------- */
@media (max-width: 1200px) {
  .dash-grid.split-7-5,
  .dash-grid.split-3-2,
  .dash-grid.split-1-1 { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 900px) {
  .stat-grid.cols-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
}
@media (max-width: 600px) {
  .topbar { gap: 10px; }
  .topbar-title { flex: 1 1 auto; min-width: 0; }
  .topbar .title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .topbar-right { flex: 0 0 auto; gap: 8px; }
  .topbar-right .btn .btn-txt { display: none; }
  .topbar-right .btn { padding-left: 10px; padding-right: 10px; }
}
@media (max-width: 600px) {
  .alarm-media { flex-direction: column; align-items: stretch; }
  .alarm-media .thumb-43 { width: 100%; }
}
/* ---------- ⑤ 移动形态的表格卡片化：bzbj 表格 → 信息卡（对齐 janelon v4） ----------
   ⚠️ 背景：基座自带的卡片化是为 janelon 的表格设计的 —— 它在渲染时给单元格挂
   `col-name / col-status / col-actions` 等语义钩子 + `.card-acts / .card-desc` 卡片专用节点，
   卡片内由钩子决定"放哪儿"。bzbj 原先只有 `.proj-table` 类、没有这些钩子，
   于是 `tr { display: grid }` 只剩"按每格内容自动分列"—— 同一页出现 2 列 / 3 列混排、
   列宽逐行不同，末列胶囊被顶出卡片 → 整页横向溢出。

   本轮照 janelon 的范式把钩子补齐（渲染层），本层负责把它们摆进网格：
     · 标题行：`td.col-name` 卡片内 display:contents，`.n`（名称 + 次要小字 + 徽标）整行横跨
     · 状态格：`td.col-status` 落在第 2 行第 1 列，紧接标题（基座 .proj-table 规则已给位）
     · 其余字段：两列等宽网格 + `data-lbl` 字段标签
       （表头在卡片形态下被隐藏，没有标签就只剩一串裸值，读不出"设备数 / 4G 信号"）
     · 操作格：`td.col-actions` 卡片内隐藏，动作改走右上角 `.card-acts` 图标按钮
     · 已提升到标题副信息的列（如备注）由 `td.card-hide` 在卡片内收起，避免重复
   整块自我闭环：删除本块即可完整回滚，不改动任何既有桌面规则。 */
/* 卡片专用节点在【非卡片形态】下必须彻底隐藏（否则会污染桌面表格）。
   `.card-desc` 基座已在同一条件里管住；这里补齐本轮新增的 `.card-only`。 */
@media (min-width: 901px), (max-height: 539px) {
  .card-only { display: none; }
}
/* ================= ㉓ 竖屏卡头换行（bzbj · 2026-10-17）=================
   现象（≤900 竖屏）：首页「报警趋势（近 7 天 · 按等级）」卡头里同时有标题与
   `.badge` 图例，两者挤在 358px 宽里 → 标题被压成两行、断在「按等 / 级)」，
   图例也折成两行「…L3 / 提示」，卡头被撑到 81.8px，是全站最难看的卡头。
   做法：允许换行，并给标题 `flex-shrink:0` —— 先让图例整块掉到第二行（成组不拆），
   标题保持一行；两行之间 8px、左右 12px 的呼吸。
   ⚠️ 只在移动形态生效；桌面不换行，卡头恒 60px（见 ⑳）。
   ⚠️ `flex-shrink:0` 仅在有换行时安全：标题宽度 ~250px，最窄视口（320px）下仍可容纳。 */
@media (max-width: 900px) and (min-height: 540px) {
  /* ⚠️ 选择器必须带上 .proj-table：基座在「项目卡」段用了
     `.table-card table.proj-table tbody tr`（三列版式）；bzbj 的列表都沿用了该类，
     不盖住就仍是"每行三个字段"。两条并列书写，是为了让将来不带该类名的表格也拿到同一版式。 */
.table-card table tbody tr,
  .table-card table.proj-table tbody tr {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 12px;
    align-items: start;
  }
  /* 首格（标题）与操作格整行；标题行内的名称/小字/徽标由基座 .proj-table 段摆放 */
.table-card table tbody tr > td:first-child { grid-column: 1 / -1; }
  .table-card table tbody td:has(.cell-actions) { grid-column: 1 / -1; }
  /* nowrap 会把列顶宽（上注）；卡片内允许换行，但胶囊 / 徽标本身不拆行 */
.table-card table tbody td { white-space: normal; overflow-wrap: anywhere; }
  .table-card table tbody td .status-chip,
  .table-card table tbody td .badge { white-space: nowrap; }
  .table-card table tbody td:has(.cell-actions) .cell-actions { justify-content: flex-end; }
  /* 桌面端「操作列 sticky 常驻」的底色 + 左侧发丝投影，在卡片里会变成
     操作行下面一条突兀的色带（卡片宽度本来就装得下，不需要常驻）。
     基座那三条带 :has() 的选择器特异性最高到 (0,4,3)，这里逐条同特异性覆盖。 */
.table-card table:has(.cell-actions) tbody td:last-child,
  .table-card table:has(.cell-actions) tbody tr:hover td:last-child,
  html[data-theme="dark"] .table-card table:has(.cell-actions) tbody td:last-child,
  html[data-theme="dark"] .table-card table:has(.cell-actions) tbody tr:hover td:last-child {
    position: static;
    background: transparent;
    box-shadow: none;
  }
  /* ---------- 标题行 ---------- */
/* 基座已给 `.proj-table td.col-name .n { grid-column:1/-1; grid-row:1; display:flex; padding-right:var(--card-act-reserve) }`，
     这里只补三件事：头像（用户卡/微信卡）、徽标不被挤压、标题行整体顶部对齐 */
.table-card table tbody tr { align-items: start; }
  .table-card table tbody td.col-name { align-self: start; }
  html[data-theme="dark"] .table-card table tbody td.col-name .n .nm { color: var(--text); }
  /* 桌面专用节点（头像表的 .cell-owner 桌面形态）在卡片里让位给 .n 内的卡片版 */
.table-card table tbody td.col-name .desk-only { display: none; }
  /* 标题行内的头像（用户卡 / 微信绑定卡） */
.table-card table tbody td.col-name .n .avatar {
    width: 38px; height: 38px; font-size: 14px;
    flex: 0 0 auto; order: 0;
  }
  .table-card table tbody td.col-name .n .nm.card-only { order: 0; }
  /* 标题行内的徽标（降级 / 已禁用 / 平台级…）：不压缩、不换行 */
.table-card table tbody td.col-name .n > .status-chip { flex: 0 0 auto; order: 1; }
  /* ---------- 字段标签（表头隐藏后靠它把语义带回来） ---------- */
.table-card table tbody td[data-lbl] {
    /* ⚠️ 用 block 而不是 flex：flex 会把「文本 + 元素」拆成多个 flex 项，

       `实测/阈值 142 / 120` 这种"文本夹着 <b>"的值会被拆成两行。

       block 下子元素照常按行内流排版；胶囊 / 徽标本身是 inline-block，自然贴左不拉伸。 */
    display: block;
    min-width: 0;
    justify-self: stretch;
  }
  .table-card table tbody td[data-lbl]::before {
    content: attr(data-lbl);
    display: block;
    font-size: 11px;
    line-height: 1.4;
    color: var(--text-muted);
    margin-bottom: 3px;
  }
  /* 字段内的次要小字（如报警卡的织机 / 设备）：标签之下另起一行 */
.table-card table tbody td[data-lbl] .card-desc { display: block; }
  /* 状态格的胶囊贴左（标签在上一行） */
.table-card table tbody td.col-status { align-self: start; }
  /* 卡片内不出现操作列（动作走右上角 .card-acts） */
.table-card table tbody td.col-actions { display: none; }
  /* 已提升到标题副信息的列在卡片内不再重复 */
.table-card table tbody td.card-hide { display: none; }
  /* ---------- 汇总卡（首页「最近报警」/ 报表页表格）----------
     这类容器卡自带 .card-head（标题 + 「查看全部」按钮），把它也"去卡片化"会留下一颗
     悬空的卡头。所以：保留卡片外壳，内部表格在卡片形态下翻译成【卡头 + 一叠行卡】——
     每行一条记录，首格当标题、其余字段带标签两列排；比"一屏被裁掉两列的横表"可读得多。 */
.table-card.sum-card {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  html[data-theme="dark"] .table-card.sum-card,
  html[data-vfx="lite"] .table-card.sum-card,
  html[data-vfx="lite"][data-theme="dark"] .table-card.sum-card {
    background: var(--card);
    border: 1px solid var(--border);
    box-shadow: none;
  }
  .table-card.sum-card:hover { transform: none; }
  .table-card.sum-card .table-wrap { overflow-x: visible; }
  .table-card.sum-card table { display: block; min-width: 0 !important; width: 100%; }
  .table-card.sum-card table thead { display: none; }
  .table-card.sum-card table tbody { display: block; }
  .table-card.sum-card table tr {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 12px;
    row-gap: 10px;
    align-items: start;
    padding: 12px 14px;
    margin: 0;
    min-height: 0;
    border: 0;
    border-bottom: 1px solid var(--border);
    border-radius: 0;
    background: none;
  }
  .table-card.sum-card table tr:last-child { border-bottom: 0; }
  .table-card.sum-card table tr:hover { background: none; }
  .table-card.sum-card table td {
    display: block;
    padding: 0;
    border: 0;
    background: none;
    text-align: left;
    white-space: normal;
  }
  .table-card.sum-card table tr > td.sum-name {
    font-size: 14px;
    font-weight: 700;
    color: var(--text);
    line-height: 1.4;
  }
  /* 底部优先（后出现者胜）：平板竖屏（700~900px）字段区放得下三列，两列会拉出大片空白。
     ⚠️ 必须写在所有定列规则之后，否则会被上面那些 repeat(2,…) 覆盖。 */
@media (min-width: 700px) {
    .table-card table tbody tr,
    .table-card table.proj-table tbody tr,
    .table-card.sum-card table tr { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    }
}
/* ---------- ⑥ 触屏 sticky hover 复位（基座 §3.2 铁律 10 的登记义务） ----------
   基座的 @media (hover: none) 复位块写在文件更靠前处，覆盖不到 bzbj 之后新增的组件，
   表现为触屏上"手指划过就留下金色描边、移开也不消失"。 */
@media (hover: none) {
  .img-card:hover { box-shadow: none; border-color: var(--border); }
  .img-card.has-defect:hover { border-color: var(--danger); }
  .defect-pick:hover:not(.active) { background: var(--field-bg); border-color: var(--field-border); }
  .rel-item:hover { border-color: var(--border); }
}
/* ---------- ⑯ 首页卡片的「跳转 + 临时筛选」（bzbj ⑯）----------
   对齐基准项目 d.janelon.com 的首页：统计卡 / 分布条 / 最近报警行整块可点，
   跳到对应模块并带上一次性筛选（筛选由 app.js 的 navGo/navInit 传递，不在这里）。

   ⚠️ `.stat-card[data-stat-go]` 与 `.dist-item[data-stat-go]` 的样式【基座早已内置】——
   见本文件 1239 行（dist-item：cursor + 悬停底色 + 深色成对）与 2663~2665 行
   （stat-card：cursor + 悬停上浮），触屏 sticky-hover 复位也已在基座的 `@media (hover:none)` 块里。
   所以这里只补基座未覆盖的第三种容器：首页「最近报警」表格行。
   行的悬停底色复用基座的 `tbody tr:hover`，无需重复声明。 */
.table-card tbody tr[data-stat-go] { cursor: pointer; }
/* ================= ⑰ 触屏「粘滞悬停」兜底（bzbj ⑰ · 2026-10-16）=================
   用户报告：弹窗里的「取消 / 保存」等按钮，手指（或触摸板轻敲）碰一下就保持
   "悬停 / 按下"的样子不恢复，看起来"沉下去、浮不起来"，且只有部分按钮出现。

   根因：本文件更靠前处的 `@media (hover: none)` 复位块【只对真触屏设备生效】。
   触摸屏笔记本 / 带触摸的桌面设备主指针是鼠标，媒体查询报 (hover: hover)，
   复位块完全不命中；但手指触摸后浏览器仍会把 :hover 粘在最后被触摸的元素上
   （sticky hover），位移与金色高光便一直留着。

   修法：改用【运行时状态】驱动 —— app.js 在触摸时给 <html> 打 .is-touch，
   真实鼠标移动时摘掉（见 app.js「触屏粘滞悬停兜底」段）。
   本段即上方媒体查询复位块的 `.is-touch` 等价物，两组互为保险：
     · 真触屏设备：媒体查询与本类同时成立（JS 开机即按 (hover: none) 判定）；
     · 触摸屏笔记本：只有本类成立 —— 用户报的正是这个场景。
   ⚠️ 今后新增含位移 / 金色高光的悬停效果时，两边都要登记（基座 §3.2 铁律 10）。 */
/* ① 位移复位：卡片上浮、统计卡图标位移、色块放大、导航图标右移，
      以及【按压位移】—— sticky :active 会把按钮长时间卡在"下沉 1px + 缩小 4%"。 */
html.is-touch .card:hover,
html.is-touch .stat-card[data-stat-go]:hover,
html.is-touch .stat-card:hover .ico,
html.is-touch .nav-item:hover .ico,
html.is-touch .color-swatch:hover,
html.is-touch .btn:active,
html.is-touch .nav-item:active,
html.is-touch .tabbar-item:active,
html.is-touch .table-card table tbody tr:active { transform: none !important; }
/* ② 侧栏项悬停左移复位 */
html.is-touch .nav-item:hover { padding-left: 12px !important; }
/* ③ 金色描边与光晕复位 —— 白弹窗上"金色浅底 + 金边 + 金色光晕"读起来正像"凹下去"，
      这是「取消 / 保存」被报异常的主力观感来源。深色必须单独给，理由见基座复位块。 */
/* 只写一套：面/描边走令牌（--surface-2 / --field-border），深浅由令牌各自取值，
   不再需要「深色再写一遍」——原来的双份写死正是深浅色差 1~2 色阶的来源。 */
html.is-touch .btn-outline:hover {
  background: var(--surface-2) !important;
  border-color: var(--field-border) !important;
  color: var(--navy-700) !important;
  box-shadow: var(--shadow-sm) !important;
}
html[data-theme="dark"].is-touch .btn-outline:hover { color: var(--text) !important; box-shadow: none !important; }
html.is-touch .btn-primary:hover,
html.is-touch .btn-primary:hover:not(:disabled) {
  background: var(--accent-fill) !important;
  box-shadow: 0 4px 12px rgba(15, 30, 61, .22) !important;
}
html[data-theme="dark"].is-touch .btn-primary:hover,
html[data-theme="dark"].is-touch .btn-primary:hover:not(:disabled) {
  background: var(--accent-fill) !important;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .4) !important;
}
html.is-touch .modal-footer .modal-danger .btn:not(.danger-btn):not(.active):hover:not(:disabled) {
  background: var(--surface-2) !important;
  border-color: var(--field-border) !important;
  color: var(--navy-700) !important;
}
html[data-theme="dark"].is-touch .modal-footer .modal-danger .btn:not(.danger-btn):not(.active):hover:not(:disabled) { color: var(--text) !important; }
/* ④ 幽灵按钮（表格操作列 / 顶栏）：粘住时会一直保持浅蓝底或语义浅底 */
html.is-touch .btn-ghost:hover { background: transparent !important; color: var(--text-muted) !important; }
html.is-touch .btn-ghost.is-danger:hover:not(:disabled) { background: transparent !important; color: var(--action-danger) !important; }
html.is-touch .btn-ghost.is-warn:hover:not(:disabled) { background: transparent !important; color: var(--action-warn) !important; }
html.is-touch .btn-ghost.is-ok:hover:not(:disabled) { background: transparent !important; color: var(--action-ok) !important; }
/* ⑤ 行级 / 卡片级背景高亮复位（粘住时读起来像"这一行被选中了"） */
html.is-touch tbody tr:hover,
html.is-touch .card-act-btn:hover,
html.is-touch .dist-item[data-stat-go]:hover { background: transparent !important; }
@media (max-width: 900px) and (min-height: 540px) {
  html.is-touch .table-card table tbody tr:hover { background: var(--card) !important; }
}
/* ⑥ bzbj 实际存在的元素 —— 触摸粘住后各自读起来都像"被按下 / 被选中"：
     · .modal-close（弹窗右上「×」）粘住 → 一直深色
     · .more-card .more-item（危险区「⋯」二级卡片动作项）粘住 → 一直金色浅底
     · .theme-opt（系统设置·主题选项）粘住 → 一直是金边，像"已选中"
     · .sidebar-user（侧栏底部账号块）粘住 → 一直亮一层
     · .btn-danger / .danger-btn 粘住 → brightness 一直挂着（发亮或发暗）
     · 表格 sticky 操作列：基座 `tr:hover td:last-child` 会给它铺 #f6f8fc，
       粘住 → 整整一列一直浅灰，像"这一行被选中了" */
html.is-touch .modal-close:hover { color: var(--text-muted) !important; }
html.is-touch .more-card .more-item:hover:not(:disabled),
html.is-touch .more-card .more-item.warn:hover:not(:disabled),
html.is-touch .more-card .more-item.success:hover:not(:disabled),
html.is-touch .more-card .more-item.danger:hover:not(:disabled) { background: none !important; }
html.is-touch .theme-opt:hover,
html.is-touch .theme-opt:hover:not(.active) { border-color: var(--border) !important; }
html.is-touch .sidebar-user:hover { background: transparent !important; }
html.is-touch .btn-danger:hover,
html.is-touch .modal-danger .danger-btn:hover:not(:disabled),
html.is-touch .modal-footer .modal-danger .danger-btn:hover:not(:disabled) { filter: none !important; }
html.is-touch .table-card table:has(.cell-actions) tbody tr:hover td:last-child { background: var(--card) !important; }
/* ⑥ bzbj 新增组件的等价复位（与本文件「⑥ 触屏 sticky hover 复位」段同源） */
html.is-touch .img-card:hover { box-shadow: none; border-color: var(--border); }
html.is-touch .img-card.has-defect:hover { border-color: var(--danger); }
html.is-touch .defect-pick:hover:not(.active) { background: var(--field-bg); border-color: var(--field-border); }
html.is-touch .rel-item:hover { border-color: var(--border); }
/* 末卡吸高 + 让 body 铺到底（原第二处同名定义，已合并） */
#page-content > .dash-grid > .dash-col > .card:last-child { flex: 1 1 auto; display: flex; flex-direction: column; }
/* ⑱-补 · 末卡被拉高后，**内容也要跟着分布**，否则全都挤在顶部、视觉上像"内容整体上移了"。
   `.vfx-card`（系统设置页「显示效果」卡）的 body 改成纵向弹性容器：
   三档选择器（`.vfx-picker`）吸收剩余高度 —— `.vfx-picker` 是 3 列 grid，
   其 grid 项默认 `align-self: stretch` → 三个选项一并等高变高（`.theme-opt` 内部
   `justify-content:center`，图标与文字始终垂直居中），下方的说明句与「当前」保持自然高度。
   ⚠️ 只对 `.vfx-card` 生效，且 ≤1200px 降单列时无剩余高度 → 三个选项回到自然高度，零影响。 */
#page-content .vfx-card > .card-body { display: flex; flex-direction: column; }
#page-content .vfx-card .card-body > .field { flex: 1 1 auto; }
#page-content .vfx-card .vfx-picker { flex: 1 1 auto; }
/* ⑱-补 2 · 同页别处的末卡也会被拉高（如 1280 宽下左列「偏好设置」多出 15px），
   同样不能让内容只贴顶部：非 `.vfx-card` 的末卡统一让 body 纵向居中，
   `.vfx-card` 由上面三条单独接管（所以这里 :not(.vfx-card) 排除，避免特异性反压）。 */
#page-content > .dash-grid > .dash-col > .card:last-child:not(.vfx-card) > .card-body {
  display: flex; flex-direction: column; justify-content: center;
}
#page-content > .dash-grid > .dash-col > .card:last-child > .card-body { flex: 1 1 auto; }
/* ⑲-补 · 浅色下「语义文字色」统一到一条门槛（WCAG AA 小字 4.5:1）。
   --action-* 是【文本 / 图标】专用角色，与 --success / --warning / --danger（页面底色、徽标底、
   图表色、图标块）彻底分离 —— 所以只换这一个角色的取值，不会连锁改底色与徽标：
   · 原取值直接用实底色：金 #d08a2a 仅 2.86:1、绿 #2f9e77 3.34:1、红 #d64545 4.38:1，三类小字
     （表格里的启用/禁用/删除、未同步 / 队列 / 待下发）全部低于 AA；
   · 统一改取【同族深端】—— 即 ⑥ 段图标渐变用的那三个深值，色相不新增：
       绿 #22795a 5.32 · 金 #8a6a28 4.71 · 红 #b93a3a 5.63
   · 深色主题不动：那边 --action-* = --*-ink（亮色压深底），本块只在浅色生效。 */
html:not([data-theme="dark"]) {
  --action-ok: var(--action-ok-deep);        /* 白 5.32 */
  --action-warn: var(--action-warn-deep);    /* 白 4.71 */
  --action-danger: var(--action-danger-deep);/* 白 5.63 */
}
/* ================= ㉑ 被栅格拉高的卡·内容不再贴顶（bzbj · 2026-10-17）=================
   现象：`.dash-grid` 的行默认 `align-items: stretch`，一行里内容少的那张卡会被
   拉到和邻卡一样高，而 `.card-body` 并不会跟着变高 —— 多出来的高度全堆在卡底。
   实测：首页「设备在线率（按工厂）」3 行数据，卡高 432、内容仅 103，**底部空 268px**；
   报表「报警趋势」空 62px、「瑕疵类型 Top6」空 35px、报表「设备在线率」空 28px。
   并排看就是"内容挤在顶部、下半张卡空着"，与用户反馈的"上下间距不统一"完全对应。

   做法与 ⑱（`.dash-col` 末卡）同源，补上 `.card` 直接作为栅格子项的三种形态：
     ① 卡片本身纵向弹性化；
     ② `.card-body` 吸收剩余高度（基座 `.card` 是 `display:block`，少了这条下面的
        `justify-content` 会因没有剩余空间可分而**静默失效**）；
     ③ 只在**卡体只有一个内容块**时垂直居中（图表 / 列表 / 2×2 统计块 / JSON 预览）。
       多子块的卡（阈值配置那种一长串 `.pref-row`）不居中，保持自上而下排列 ——
       表单行被拉开会像"内容散架"。
   ⚠️ `:has(> :only-child)` 是必要的收敛条件，别图省事改成无条件居中。
   ⚠️ `.table-card` 不走 ③（表格行高不能居中），它由下面 ⑤ 单独处理。 */
#page-content .dash-grid > .card { display: flex; flex-direction: column; }
#page-content .dash-grid > .card > .card-body { flex: 1 1 auto; }
#page-content .dash-grid > .card > .card-body:has(> :only-child) {
  display: flex; flex-direction: column; justify-content: center;
}
/* ④ 唯一例外：卡体里只有一条 `.dist-list`（首页「瑕疵 Top6」「设备在线率」、报表「瑕疵 Top N」）
   时，**不整体居中而是让列表自己铺开** —— 整体居中会让首行离卡头 130px，
   看着像"列表掉到卡片中间"；`space-around` 则把余量均分到行间
   （实测 3 行卡：首行距卡头 37px、行距 113px，既是居中又是列表）。
   `.dist-list` 本身已是 flex column，只需它吃掉剩余高度。 */
#page-content .dash-grid > .card > .card-body > .dist-list {
  flex: 1 1 auto; justify-content: space-around;
}
/* ⑤ 表卡同样处理（报表「设备在线率（按工厂）」3 行表格被拉到 294 高，底部空 28px）：
   让 `.table-wrap` 吃掉剩余高度、`table` 撑满 wrap —— 行高按比例分摊多出来的 28px
   （实测每行 53→62px），最后一行直接落到底边，与首页「最近报警」表卡一致。
   ⚠️ 只在「被栅格拉高的表卡」上生效：表格自然高度 ≥ 卡高时 `height:100%` 不起作用。 */
#page-content .dash-grid > .card.table-card { display: flex; flex-direction: column; }
#page-content .dash-grid > .card.table-card > .table-wrap { flex: 1 1 auto; }
#page-content .dash-grid > .card.table-card table { height: 100%; }
/* ================= ㉒ 卡内末尾工具条·残余 margin 归零（bzbj · 2026-10-17）=================
   现象：系统设置页「数据大屏（DL-06）」卡底部比其它卡多出 18px ——
   卡内最后一个区块是 `.toolbar`（Token 说明 + 两个按钮），而基座 `.toolbar` 自带
   `margin-bottom: 18px`（页面级区块靠它拉开间距）。页面级有 ⑭ 层统一归零，
   **卡内的它管不到** → 卡体 12/20 的内边距之外又叠了 18px，实际底部留白 38px。
   做法：卡体里最后一个区块若带下外边距，一律归零（不止 toolbar，`.stat-grid` 24px 同理，
   它此前已由 ⑪ 层单独清零，这里做统一兜底）。
   ⚠️ 只针对「最后一个子块」——中间的块保留自身 margin，否则节奏会变。 */
.card-body > :last-child { margin-bottom: 0; }
@media (max-width: 900px) and (min-height: 540px) {
  /* 卡片内文左轴 = 20px（2026-10-15 对齐检查统一：卡头/卡体/统计卡/欢迎横幅同轴）
   原为 22px，与同一屏内的 .stat-card(20) / 表头(16) 各差 2 / 6px */
.card-head { flex-wrap: wrap; row-gap: 8px; column-gap: 12px; padding-top: 14px; padding-bottom: 14px; }
  .card-head > h3 { flex-shrink: 0; }
}
/* ============================================================
   ㉔ 密筛工具条（is-dense）：筛选控件 ≥5 个时允许换行
   ------------------------------------------------------------
   背景：⑧ 定的「桌面工具条一行不换行 + 控件 200px 定宽」是为"按钮被压成窄条"兜底，
   但需求书 FR-DEV-02 / FR-ALM-06 要求 5~7 个筛选项，200px × N 必然撑爆 1100px 内容宽。
   做法：仅这几页挂 `.is-dense`（图片标注 / 报警记录 / 设备管理 / 统计报表 / 操作审计），
       改为允许换行；`spacer` 在窄宽下让位，动作按钮用 `margin-left:auto` 自己贴右，
       从而「按钮永远 ≥ 自身宽度」这条底线不破（card-action-audit ⑧ 仍守着）。
   ⚠️ 不挂 is-dense 的页面行为完全不变，仍是单行 + 200px。 */
.toolbar.is-dense { flex-wrap: wrap; row-gap: 10px; }
.sidebar-user .uinfo { flex: 1 1 auto; min-width: 0; }
.sidebar-user .su-logout { margin-left: auto; flex: 0 0 auto; color: rgba(255, 255, 255, .72); }
.sidebar-user .su-logout:hover { color: #fff; background: rgba(255, 255, 255, .12); }
/* 只读角色的灰化字段：作用域放开到全站（弹窗挂在 body 上，原先写死 #page-content 时
   弹窗内的字段吃不到灰化）。桌面与竖屏、页面与弹窗必须同一套观感。 */
.is-readonly-field:disabled,
.is-readonly-field[aria-disabled="true"] {
  color: var(--text-muted);
  background: rgba(0, 0, 0, .04);
  cursor: not-allowed;
  opacity: 1;
}
html[data-theme="dark"] .is-readonly-field:disabled,
html[data-theme="dark"] .is-readonly-field[aria-disabled="true"] { background: rgba(255, 255, 255, .05); }
.is-readonly-field:disabled:hover,
.is-readonly-field[aria-disabled="true"]:hover { border-color: var(--field-border); }
/* 只读角色（观察员）：表格行整卡点击已在 JS 侧关闭（stripWriteActions 置空 rowTapHandler），
   这里把"可点"手势一并复位 —— 手指按下去没有任何反应时，光标还在暗示"能点"更让人困惑。
   ⚠️ 图片卡不在复位范围内：观察员点图片卡会打开【只读详情】（可看大图与标注信息），仍然可点。 */
html.is-readonly #page-content tbody tr,
html.is-readonly #page-content .table-card tbody tr[data-stat-go] { cursor: default; }
/* 窄宽下 spacer 会独占一行留下空洞，直接收起，让按钮紧跟最后一个筛选 */
.toolbar.is-dense > .spacer { flex: 1 0 0; min-width: 0; }
.toolbar.is-dense > .btn { margin-left: auto; }
@media (max-width: 1100px) {
  .toolbar.is-dense > .spacer { display: none; }
}
/* ============================================================
   ㉖ 报警记录页工具条（横屏桌面）：下拉筛选独占首行 + 搜索框与主按钮同排
   ------------------------------------------------------------
   现象（1920×1080 实测）：8 个控件按 DOM 顺序排（搜索框在最前），200px 定宽下
   一行只放得下 7 个 ——「全部时间」被挤到第二行、左边缘与上一行对不齐。
   目标版式（横屏）：
     第 1 行：全部可展开的筛选（全部时间 / 工厂 / 织布机 / 类型 / 等级 / 状态 / 通知状态）
     第 2 行：搜索框（保持 200px 统一宽）+ 空格 + 「批量重推」贴右
   做法（只作用于 ≥1100px；竖屏 / 平板 / 手机一行都不动，DOM 顺序也不改）：
     ① `#al-time` order:-1 → 时间排到所有下拉的最前；
     ② `.f-kw` order:1 → 搜索框排到全部下拉之后（首行放不下 7 个下拉时才折行）；
     ③ `.spacer` / 动作按钮 order:2 → 紧跟搜索框，于是「搜索框 + 按钮」同处一行；
     ④ 搜索框不再特殊加宽，与其余控件同为 200px（≥1100px 的既有口径）。
   实测 1920：首行 7 个下拉正好排满（1472px ≤ 1636px），第二行 = 搜索框 + 按钮；
   1600 / 1440 / 1366：下拉按 6 / 5 / 5 个折行，搜索框始终与按钮同排；
   两行控件左边缘都落在 `n × 212px` 的同一组列上（可量测，不靠目测）。 */
@media (min-width: 1100px) {
  .toolbar.is-alarm > #al-time { order: -1; }
  .toolbar.is-alarm > .f-kw { order: 1; }
  .toolbar.is-alarm > .spacer,
  .toolbar.is-alarm > .btn { order: 2; }
}


