/* HR工作台 · 外壳层样式
 * 设计稿：设计稿/工作台-20260929/
 * 主色 #3B82F6 · 底色 #F7F8FC · 双端断点 768px
 * 约定：新增模块的样式放 css/<模块>.css，不要都堆在这里
 */

:root {
  --brand: #3B82F6;
  --brand-dark: #2563EB;
  --brand-weak: #EFF6FF;
  --bg: #F7F8FC;
  --card: #FFFFFF;
  --line: #EBEDF0;
  --line-2: #F2F3F5;
  --text: #1F2329;
  --text-2: #646A73;
  --text-3: #8A8F99;
  --ok: #07A050;
  --ok-weak: #E8F5EC;
  --warn: #B8860B;
  --warn-weak: #FDF3E5;
  --danger: #D9363E;
  --danger-weak: #FDECEC;

  /* ── 图表色板 B6（2026-10-20）────────────────────────────────────────────
     🔴 规则（用户 2026-10-20 定）：
       ① 分类色**上限 6 色**，不要求每张图用满；
       ② **单指标直接用 `--brand`**（不另占分类位）；
       ③ **同一分类跨页面必须同色**（例：实习生在所有页都是 `--chart-2`）；
       ④ `--ok / --warn / --danger` 只表达**状态**，不当分类色；
       ⑤ 超过 6 类优先合并为「其他」，图例保留文字说明。
     实现保持现状：成本/报表用 HTML+CSS 条形，人员趋势用内联 SVG，**不引入图表库**。

     六位分配（当前只用到 1–2 位；3 位起为规范预留，**不设令牌避免死变量**）：
       1 → `--brand`(#3B82F6)   人员「正式」· 单指标 · 排名第一 · 选中态
       2 → `--chart-2`(#14B8A6) 人员「实习」
       3 → 预留 #8B5CF6（紫）    4 → 预留 #F97316（橙，与 --warn 暗金可区分）
       5 → 预留 #EC4899（品红）  —— 出现新分类时按此序补令牌，色相体系一致

     ⚠️ **「其他」不占编号**：中性灰 `--text-3`(#8A8F99) 是**兜底色**，专给
        「合并后的其他 / 无数据」用。若让灰占第 3 位，第 3 个真实分类出现时色序会变成
        「蓝-青-灰-紫」，灰被读成"禁用/缺省"而不是一个分类。
        所以规则⑤应理解为：**5 个彩色分类 + 1 个灰色兜底 = 6**。
     ⚠️ **禁止靠覆盖 `--brand` / `--text-3` 调图表色**：两者同时承担基础语义
        （品牌色、三级文字），要改色位就改本表；将来做主题/皮肤需连图表一起评审。
     ─────────────────────────────────────────────────────────────────────── */
  --chart-2: #14B8A6;
  /* 单系列条形的**非强调**填充：--brand 的柔和版。
     不用 --brand-weak(#EFF6FF)：那个在 10px 高的条上几乎看不见。 */
  --chart-fill: #94C5F8;

  --r: 10px;
  --sidebar-w: 200px;
  --topbar-h: 56px;
  --tabbar-h: 58px;
  /* 内容区宽度上界（2026-10-17 · 桌面为主）：
     🔴 只给「一处」上界，页面级 `max-width` 一律禁止 —— 历史上 cost/report 各封 1240，
     导致 1920 屏下从「成本」切到「人员」内容宽度 1240→1632 跳 392px（切页跳宽）。
     取值 1440 的理由：冒烟桌面视口即 1440（改完可被现有截图直接验证）、
     1440 下名册 12 列可完整显示、1920 屏左右各留 240px 白属「可读」而非浪费。 */
  --page-max: 1440px;

  /* ── 设计令牌 W0-1（2026-10-14 追加）────────────────────────────────────
     🔴 本次**只追加变量、不替换任何现有选择器** ⇒ 线上视觉零变化。
     用法：新代码一律引用令牌；存量在 Wave 2 才替换（W2-1 色 / W2-2 字号 / W2-3 圆角）。
     校验：scripts/design_lint.py（W0-4）以本段为参照集。

     字号映射（实测 16 种 → 8 级）：
       10, 10.5 → --fs-xs ｜ 11.5, 12 → --fs-sm ｜ 12.5, 13 → --fs-base
       13.5, 14, 15 → --fs-md ｜ 16 → --fs-lg ｜ 18, 19 → --fs-xl
       20, 22 → --fs-2xl ｜ 26 → --fs-3xl
     圆角映射（实测 14 种 → 4 档）：
       3, 4, 5, 6, 7 → --r-sm ｜ 8, 10, 11 → --r-md ｜ 12, 14 → --r-lg ｜ 999 → --r-pill
       ⚠️ `50%`（正圆）与 `4px 4px 0 0`（半圆角）是**形态**、不是档位，保留原样不映射。
     ✅ `--r`(10px) 是历史变量：**B5b 已把 11 处 `border-radius: var(--r)`
        全部改为 `var(--r-md)`(8px)，当前零引用**；定义暂留兼容，待兼容层批次再删。

     📌 B5a 字号等值替换（2026-10-20）：上面「16 种 → 8 级」是**收敛**映射，
        照它替换会真改字号 ⇒ 属 B5b 视觉变更，本批不采用。
        B5a 只做等值替换：11/12/13/14/16/18/20/26 八个值 ↔ 上面八个 --fs-*，
        **--fs-* 的定义值在本批内不变**（改了就失去等值前提，
        scripts/check_css_tokens.py 会判令牌漂移）。
        无等值令牌的 27 处字面量（10 / 10.5 / 11.5 / 12.5 / 13.5 / 15 / 19 / 22px）
        本批保留，留 B5b 与收敛一起做。
     ─────────────────────────────────────────────────────────────────────── */
  --fs-xs: 11px;
  --fs-sm: 12px;
  --fs-base: 13px;
  --fs-md: 14px;
  --fs-lg: 16px;
  --fs-xl: 18px;
  --fs-2xl: 20px;
  --fs-3xl: 26px;

  --r-sm: 4px;
  --r-md: 8px;
  --r-lg: 12px;
  --r-pill: 999px;

  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;
  --sp-7: 32px;
  --sp-8: 40px;

  --sh-1: 0 4px 16px rgba(31, 35, 41, .18);
  --sh-2: 0 6px 20px rgba(0, 0, 0, .18);
  --sh-3: 0 8px 24px rgba(0, 0, 0, .10);
  --ring: 0 0 0 3px var(--brand-weak);

  /* 层级（B5a-3 等值令牌化 · 2026-10-20）：
     --z-raised 是本批唯一新增令牌，供「表格吸顶表头」这类需要抬升 1 层、
     但又必须低于一切外壳/弹层（--z-sticky 20）的元素使用。 */
  --z-raised: 1;
  --z-sticky: 20;
  --z-dropdown: 30;
  --z-modal: 60;
  --z-toast: 80;
  --z-top: 90;

  --dur-1: 120ms;
  --dur-2: 200ms;
  --ease: cubic-bezier(.2, .8, .2, 1);
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { height: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-size: var(--fs-md);
  font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
  -webkit-font-smoothing: antialiased;
}
.hidden { display: none !important; }
a { color: var(--brand); text-decoration: none; }
svg { flex: 0 0 auto; }

/* 统一图标描边：用 currentColor，随容器文字色变化 */
.ico {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ---------------- 通用组件 ---------------- */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 9px 16px; border: 1px solid transparent; border-radius: var(--r-md);
  background: var(--brand); color: #fff; font-size: var(--fs-md); font-weight: 500;
  font-family: inherit; cursor: pointer;
}
.btn:hover { background: var(--brand-dark); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn.block { display: flex; width: 100%; padding: 12px; font-size: var(--fs-md); }
.btn.ghost { background: var(--card); color: var(--text); border-color: var(--line); }
.btn.ghost:hover { background: var(--bg); }
.btn.sm { padding: 5px 12px; font-size: var(--fs-base); }

/* 语义化按钮：把「这一击有多重后果」写在按钮本身上，而不是指望用户读完标签再判断。
   用户 2026-10-13 的原始反馈是「改名和停用挨太近，容易点错」—— 三颗按钮同色同号，
   只靠两三个汉字区分，本来就是让人靠猜。
   约定：ghost 版（描边 + 彩色文字）= 常态，视觉安静；
        实心版（填充色）= **只在二次确认那一击**上用，避免破坏性动作长得像主操作。
   🔴 这两条原本不在基础样式里 —— account.css 与 dataimport.css 各自补了一份
      （同一件事写两遍，且后加载的那个悄悄覆盖前一个的 hover 色）。统一收在这里。 */
.btn.danger { background: var(--danger); border-color: var(--danger); color: #fff; }
.btn.danger:hover { background: #C22C34; }
.btn.warn { background: var(--warn); border-color: var(--warn); color: #fff; }
.btn.warn:hover { background: #9C7209; }
.btn.ghost.danger { background: var(--card); border-color: var(--line); color: var(--danger); }
.btn.ghost.danger:hover { background: var(--danger-weak); border-color: var(--danger); }
.btn.ghost.warn { background: var(--card); border-color: var(--line); color: var(--warn); }
.btn.ghost.warn:hover { background: var(--warn-weak); border-color: var(--warn); }

/* 操作组里的竖线分隔：把「安全动作」与「破坏性动作」分成两组。
   间距 = 两边各 8px(gap) + 4px×2(margin) + 1px ≈ 25px，是普通按钮间距的三倍，
   扫一眼就知道这是两组、不会顺手点过去。
   ⚠️ `display: inline-block` 是必须的：这个分隔条既用在 flex 容器里
     （.page-head .acts / .ac-acts / .dt-item-ops），也用在**纯文本流**里
     （er.js 的工作明细 <td> 是 `acts.join(" ")`）—— inline 元素上的 width/height 不生效，
     少了这一句在表格里就是一条看不见的空 span。flex 里它会被 blockify，无副作用。 */
.act-sep {
  display: inline-block; vertical-align: middle;
  /* 颜色比 --line(#EBEDF0) 深一档：--line 是为**卡片描边**调的，画在浅底页头上几乎看不见，
     那这道线就等于没画（分隔全靠 25px 间距）。取值行里那条同款，见 dictionary.css 的 .dt-opsep。 */
  width: 1px; height: 16px; background: #D5D9E0; margin: 0 4px; flex: none;
}

.tag {
  display: inline-block; padding: 2px 8px; border-radius: var(--r-sm);
  font-size: var(--fs-sm); background: var(--brand-weak); color: var(--brand-dark);
}
.tag.mute { background: var(--line-2); color: var(--text-2); }
.tag.ok { background: var(--ok-weak); color: var(--ok); }
.tag.warn { background: var(--warn-weak); color: var(--warn); }
.tag.danger { background: var(--danger-weak); color: var(--danger); }

.msg {
  font-size: var(--fs-base); line-height: 1.5; padding: 10px 12px; border-radius: var(--r-md);
  margin-bottom: 14px; background: var(--warn-weak); color: var(--warn);
}
.msg.ok { background: var(--ok-weak); color: var(--ok); }
.msg.err { background: var(--danger-weak); color: var(--danger); }

.card { background: var(--card); border: 1px solid var(--line); border-radius: var(--r-md); margin-bottom: 14px; }
.card > h2 {
  margin: 0; padding: 14px 16px; font-size: var(--fs-md); font-weight: 500;
  border-bottom: 1px solid var(--line-2); display: flex; align-items: center; gap: 8px;
}
.card > h2 .more { margin-left: auto; font-size: var(--fs-base); font-weight: 400; color: var(--brand); cursor: pointer; }
.card > .body { padding: 16px; }

.row { display: flex; align-items: center; gap: 12px; padding: 13px 0; border-bottom: 1px solid var(--line-2); }
.row:last-child { border-bottom: none; }
.row .grow { flex: 1; min-width: 0; }
.row .k { color: var(--text-2); font-size: var(--fs-md); }
.row .v { font-size: var(--fs-md); font-weight: 500; }

.empty { text-align: center; padding: 40px 16px; color: var(--text-3); font-size: var(--fs-base); line-height: 1.8; }
.empty .t { color: var(--text-2); font-size: var(--fs-md); margin-bottom: 6px; }

.mono { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: var(--fs-base); }

/* ---------------- 认证视图（登录 / 改密） ---------------- */

.auth { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
.auth .panel {
  width: 100%; max-width: 380px; background: var(--card);
  border: 1px solid var(--line); border-radius: var(--r-lg); padding: 28px 24px;
}
.auth .logo-lg {
  width: 44px; height: 44px; border-radius: var(--r-md); background: var(--brand); color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: var(--fs-md); font-weight: 500; margin-bottom: 16px;
}
.auth h1 { margin: 0 0 6px; font-size: var(--fs-xl); font-weight: 500; }
.auth .desc { color: var(--text-3); font-size: var(--fs-base); margin-bottom: 22px; }

.field { margin-bottom: 14px; }
.field label { display: block; font-size: var(--fs-base); color: var(--text-2); margin-bottom: 6px; }
.field input {
  width: 100%; padding: 11px 12px; font-size: var(--fs-md); font-family: inherit; color: var(--text);
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-md);
}
.field input:focus { border-color: var(--brand); background: var(--card); }
.hint { font-size: var(--fs-sm); color: var(--text-3); line-height: 1.7; margin-top: 14px; }

.boot { min-height: 100vh; display: flex; align-items: center; justify-content: center;
        color: var(--text-3); font-size: var(--fs-md); }

/* ---------------- 应用外壳 ---------------- */

.layout { display: flex; min-height: 100vh; }

.sidebar {
  width: var(--sidebar-w); flex: 0 0 var(--sidebar-w); background: var(--card);
  border-right: 1px solid var(--line); display: flex; flex-direction: column;
  position: sticky; top: 0; height: 100vh;
}
.side-brand { display: flex; align-items: center; gap: 10px; padding: 18px 16px 14px; }
.side-brand .logo {
  width: 32px; height: 32px; border-radius: var(--r-md); background: var(--brand); color: #fff;
  display: flex; align-items: center; justify-content: center; font-size: var(--fs-sm); font-weight: 500;
}
.side-brand .t { font-size: var(--fs-md); font-weight: 500; line-height: 1.2; }
.side-brand .s { font-size: var(--fs-sm); color: var(--text-3); margin-top: 2px; }

.side-nav { padding: 4px 10px; display: flex; flex-direction: column; gap: 2px; }
.side-group { margin-top: auto; padding: 8px 10px 0; border-top: 1px solid var(--line-2); }
.side-group .glabel { font-size: var(--fs-sm); color: var(--text-3); padding: 8px 12px 4px; }
.side-foot { padding: 14px 16px 18px; font-size: var(--fs-sm); color: var(--text-3); }

.nav-item {
  display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--r-md);
  color: var(--text-2); font-size: var(--fs-md); cursor: pointer; user-select: none;
}
.nav-item:hover { background: var(--bg); }
.nav-item.active { background: var(--brand); color: #fff; }
.nav-item .badge {
  margin-left: auto; min-width: 20px; text-align: center; padding: 1px 6px; border-radius: var(--r-md);
  background: var(--danger-weak); color: var(--danger); font-size: var(--fs-sm);
}
.nav-item.active .badge { background: rgba(255, 255, 255, .26); color: #fff; }

.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }

.topbar {
  height: var(--topbar-h); flex: 0 0 var(--topbar-h); background: var(--card);
  border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 16px;
  padding: 0 20px; position: sticky; top: 0; z-index: var(--z-sticky);
}
.crumb { display: flex; align-items: baseline; gap: 6px; font-size: var(--fs-base); }
.crumb .c1 { font-size: var(--fs-md); font-weight: 500; color: var(--text); }
.crumb .c2 { color: var(--text-3); }
.topbar .spacer { flex: 1; }

.search { position: relative; width: 260px; }
.search input {
  width: 100%; padding: 8px 12px 8px 32px; border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--bg); font-size: var(--fs-base); font-family: inherit; color: var(--text-2);
}
.search input:disabled { cursor: not-allowed; }
.search .ico { position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
               width: 15px; height: 15px; color: var(--text-3); }

.who { display: flex; align-items: center; gap: 8px; cursor: pointer; }
.avatar {
  width: 32px; height: 32px; border-radius: 50%; background: var(--brand); color: #fff;
  display: flex; align-items: center; justify-content: center; font-size: var(--fs-base); font-weight: 500;
}
.who .nm { font-size: var(--fs-base); font-weight: 500; line-height: 1.2; }
.who .rl { font-size: var(--fs-sm); color: var(--text-3); }

.content { flex: 1; padding: 20px; min-width: 0; }

/* 超宽屏内容居中（2026-10-17 · 桌面为主）
   🔴 只在 ≥1440 生效 ⇒ **≤1440 视口像素级不变**（含冒烟的 1440 桌面段），零回归。
   用 padding-inline 而不是加 max-width 容器：不动 11 个页面的根节点、不加 DOM 包装层，
   改一处全站生效，回滚也只是删这一条。
   为什么不是 `max-width` + `margin:auto`：`.content` 是 flex 子项且要撑满滚动区，
   用内边距让它「自己让出空间」，横向滚动条与 sticky 表头的行为都不会变。 */
@media (min-width: 1440px) {
  .content { padding-inline: max(20px, calc((100% - var(--page-max)) / 2)); }
}

/* ---------------- 页面骨架 ---------------- */

.page-head { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
.page-head h1 { margin: 0; font-size: var(--fs-2xl); font-weight: 500; }
.page-head .sub { color: var(--text-3); font-size: var(--fs-base); margin-top: 6px; }
.page-head .acts { margin-left: auto; display: flex; gap: 8px; }

.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 14px; }
.stat { background: var(--card); border: 1px solid var(--line); border-radius: var(--r-md); padding: 16px; }
.stat .cap { font-size: var(--fs-base); color: var(--text-2); }
.stat .num { font-size: var(--fs-3xl); font-weight: 500; margin: 8px 0 6px; letter-spacing: -.5px; }
.stat .num .u { font-size: var(--fs-base); font-weight: 400; color: var(--text-3); margin-left: 3px; }
.stat .num.pending { color: var(--text-3); font-size: var(--fs-2xl); }
.stat .foot { font-size: var(--fs-sm); color: var(--text-3); }
.stat .foot.ok { color: var(--ok); }

.grid-2 { display: grid; grid-template-columns: 1.6fr 1fr; gap: 14px; align-items: start; }
/* 🔴 跨列规则（2026-10-14 补）：**对宽度敏感的卡必须跨满整行** ——
   图表 / 宽表 / 时间线放在 1fr 窄栏里会被压扁（案例：人员概览的在职人数趋势图，
   原先只有约 460px 宽，12 个月时每点仅 38px）。这是审计 §三.1「长短内容不平衡」与
   P0-4「网格列数与跨列规则」的第一条落地；完整规则（容器最大宽度 / 左右留白 /
   按内容密度选断点 / 页面长度上限）见设计线 **W2-11**。
   ⚠️ 移动端（≤768px 变单列）无需覆盖：`1 / -1` 在单列网格里就是那一列。 */
.grid-2 > .span-full { grid-column: 1 / -1; }

/* ---------------- 主题 tab（员工关系等板块切换） ---------------- */

.tabs { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
.tab {
  padding: 7px 14px; border-radius: var(--r-md); border: 1px solid var(--line); background: var(--card);
  color: var(--text-2); font-size: var(--fs-base);
}
.tab.active {
  background: var(--brand-weak); border-color: var(--brand);
  color: var(--brand-dark); font-weight: 500;
}
.side-group .nav-item { margin-bottom: 2px; }
.nav-empty { padding: 12px; color: var(--text-3); font-size: var(--fs-base); line-height: 1.7; }

/* ---------------- 手机端（底部 tab 栏） ---------------- */

.tabbar { display: none; }
.tab-item {
  flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; font-size: var(--fs-sm); color: var(--text-3); cursor: pointer; user-select: none;
}
.tab-item.active { color: var(--brand); }
.tab-item .badge {
  position: absolute; transform: translate(14px, -10px);
  min-width: 16px; padding: 0 4px; border-radius: var(--r-md); background: var(--danger);
  color: #fff; font-size: var(--fs-xs); line-height: 16px; text-align: center;
}

@media (max-width: 768px) {
  .layout { display: block; }
  .sidebar { display: none; }
  .topbar { padding: 0 14px; gap: 10px; }
  .search { display: none; }
  .who .txt { display: none; }
  .crumb .c2 { display: none; }
  .content { padding: 12px 12px calc(var(--tabbar-h) + 20px + env(safe-area-inset-bottom)); }
  .page-head { margin-bottom: 12px; }
  .page-head h1 { font-size: var(--fs-xl); }
  .stats { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  .stat { padding: 13px; }
  .stat .num { font-size: var(--fs-2xl); }
  .grid-2 { grid-template-columns: 1fr; }
  .tabbar {
    display: flex; position: fixed; left: 0; right: 0; bottom: 0;
    height: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
    padding-bottom: env(safe-area-inset-bottom);
    background: var(--card); border-top: 1px solid var(--line); z-index: 30;
  }
}

/* ── 会话过期 · 「提示 + 回登录页」覆盖层（线 7 / 2026-10-15 简化）──────────
   🔴 覆盖层**只讲发生了什么**，不再就地收账号密码 —— 就地登录会把用户留在
      被 401 打断的旧页面上，看着像没登录成功；一个按钮回登录页，登录后整体重渲染。
   ⚠️ 层级用 `--z-top`（90）：它必须盖住 toast（80）与所有弹层。
   ⚠️ 全部走设计令牌，不新造字号 / 圆角 / 颜色。 */
.session-mask {
  position: fixed; inset: 0; z-index: var(--z-top);
  display: flex; align-items: center; justify-content: center;
  background: rgba(16, 24, 40, .45); padding: var(--sp-4);
}
.session-box {
  width: min(380px, 100%); background: var(--card);
  border-radius: var(--r-lg); padding: var(--sp-5);
  box-shadow: var(--sh-3);
}
.session-box h3 { margin: 0 0 var(--sp-1); font-size: var(--fs-lg); color: var(--text); }
.session-desc {
  margin: 0 0 var(--sp-4); color: var(--text-2);
  font-size: var(--fs-base); line-height: 1.6;
}
/* 「未提交内容会丢」那一句：与上一段同字号，但用警示色 + 下边距收窄，不抢主文案 */
.session-warn { color: var(--warn); margin-bottom: var(--sp-4); }
.session-acts { display: flex; gap: var(--sp-2); }
.session-acts .btn { flex: 1; }

/* ── 弹层 · 未保存内容保护（全站统一，逻辑在 ui.js）─────────────────────────
   起因（2026-10-15 用户反馈 #2）：弹层里填了半天的表，鼠标滑出去点一下遮罩
   就整个关掉、内容全清空。现在表单型弹层**点遮罩不再关闭**（只给一条提示），
   且关闭前留草稿、重开自动回填。
   这两条样式是**全站共用**的（er / project / account 三套弹层都会用到），
   所以放外壳层，不放进任何一个模块 CSS —— 放模块里会立刻复制出第二份。 */
.wm-nudge,
.wm-draft-hint {
  display: flex; align-items: center; gap: var(--sp-2);
  border-radius: var(--r-md);
  padding: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-3);
  font-size: var(--fs-sm); line-height: 1.6;
}
.wm-nudge { background: var(--warn-weak); color: var(--warn); }
.wm-draft-hint { background: var(--brand-weak); color: var(--brand-dark); }
.wm-draft-hint .btn { margin-left: auto; }

/* ── 弹层外壳（B7 起唯一来源）────────────────────────────────────────────
   遮罩 `.wm-modal` / 面板 `.wm-panel`（+ 调用方 `panelClass` 的模块差异：
   ac 620 / pj 460 / er 520，宽版 `wm-wide` 760）。旧三套外壳类名与模块 CSS
   的遮罩规则已随 B7 退出，这里就是唯一出处。
   ⚠️ 层级用 `--z-modal`（60）：必须低于 toast（80）与会话覆盖层（90）。
   📌 圆角：B5b 视觉收敛后为 `--r-lg`(12px)。 */
.wm-modal {
  position: fixed; inset: 0; z-index: var(--z-modal);
  background: rgba(31, 35, 41, .38);
  display: flex; align-items: center; justify-content: center;
  padding: var(--sp-5);
}
.wm-panel {
  width: 100%; max-width: 520px; max-height: 88vh; overflow: auto;
  background: var(--card); border-radius: var(--r-lg); padding: var(--sp-5);
}
.wm-panel.wm-wide { max-width: 760px; }
/* 标准标题栏：只有调用方传 `title` 时才出现（老表单自带 `<h3>`，视觉不动） */
.wm-head { display: flex; align-items: center; margin-bottom: var(--sp-4); }
.wm-title { margin: 0; font-size: var(--fs-lg); font-weight: 500; }
/* 弹层内播报区：与 `#ui-live` 同款「视觉隐藏但仍在无障碍树」——
   `display:none` / `visibility:hidden` 会把节点摘出树，读屏照样读不到。 */
.wm-live {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ══════════ W1-6 公共表格容器（2026-10-17）══════════
   全站唯一表格出口，配合 `ui.table()`（js/ui.js）使用。
   覆盖：列优先级 / 最小列宽 / 数字右对齐 / 表头吸顶 / 行点击与键盘可达 /
        选中态 / 空·加载·失败·无权限。
   🔴 存量 4 套 `.?-table` 还在（er/cs/imp/rp），逐个换；换的时候用 `cls` 参数
   把旧类名一起挂上（`ui-table pp-table`），避免视觉与断言同时变化。 */
.ui-tablewrap { overflow-x: auto; }
.ui-table {
  width: 100%; border-collapse: collapse;
  font-size: var(--fs-base, 13px);
}
.ui-table thead th {
  text-align: left; font-weight: 500; color: var(--text-3);
  font-size: var(--fs-sm, 12px); padding: 8px 10px;
  border-bottom: 1px solid var(--line); white-space: nowrap;
}
/* 吸顶要背景色，否则滚动时内容会从表头底下透出来 */
.ui-tablewrap.is-sticky .ui-table thead th {
  position: sticky; top: 0; z-index: var(--z-raised); background: var(--card);
}
.ui-table tbody td {
  padding: 9px 10px; border-bottom: 1px solid var(--line-2);
  color: var(--text-2); vertical-align: middle;
}
.ui-table tbody tr:last-child td { border-bottom: none; }
.ui-table .is-num { text-align: right; font-variant-numeric: tabular-nums; }
/* 列宽收敛（S3 / 2026-10-17）：`table({fixed:true})` 时启用。
   为什么需要：不加 fixed 时 `width` 只落成 `min-width`，列宽仍由内容均分 ——
   实测名册「职位」列吃掉一大段、「部门/子部门」被压到两三个字。
   ⚠️ fixed 下未给 width 的列会被均分剩余空间，所以除「吃剩余的那一列」外都要给 width。
   截断后不逐个加 title（太吵）：要看全值走「点行进详情」这条既有路径。 */
.ui-table.is-fixed { table-layout: fixed; }
.ui-table.is-fixed tbody td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ui-table tr.is-clickable { cursor: pointer; }
.ui-table tr.is-clickable:hover { background: var(--brand-weak); }
/* 键盘可达的可见焦点：只对键盘操作显示，鼠标点击不出现方框 */
.ui-table tr.is-clickable:focus-visible {
  outline: 2px solid var(--brand); outline-offset: -2px;
}
.ui-table tr.is-selected { background: var(--brand-weak); }
/* 截断与悬浮提示：内容超长时省略号收尾，完整值靠 `title`（渲染层负责加） */
.ui-table .is-ellip {
  max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ══════════ P1-0 表格容器补 4 项能力（2026-10-18）══════════
   存量 4 套表各有一样 ui.table 原来给不了的东西；不补齐就换表，等于删功能：
     · 合计行  ← cost 三张表（cs-total）+ report 的 total_row
     · 整表 nowrap ← .cs-table（3 张成本表靠它防备注逐字竖排）
     · 首列强调 ← .rp-table td:first-child
     · 等宽列  ← .er-table .er-emp（工号）
   全部挂在 .ui-table 下，**不新建类名族** —— 否则又是一次「五套表」重演。 */
.ui-table tfoot td {
  padding: 9px 10px; font-weight: 600; color: var(--text);
  border-top: 1px solid var(--line); border-bottom: none;
}
.ui-table.is-nowrap thead th,
.ui-table.is-nowrap tbody td,
.ui-table.is-nowrap tfoot td { white-space: nowrap; }
.ui-table .is-mono {
  font-family: var(--mono, ui-monospace, Menlo, Consolas, monospace); color: var(--text);
}
.ui-table .is-strong { color: var(--text); font-weight: 500; }
.ui-table .is-nowrap { white-space: nowrap; }

/* 对齐的**列级**工具类（P1 片 3 / 2026-10-18）
   · `is-center` —— 报表的居中列。原先靠 `.rp-table td.c`，而删旧 CSS 时居中是**静默**
     丢的（不报错，只是悄悄变回左对齐），所以容器必须自己能表达居中对齐。
   · `is-vtop` —— 长文本列顶对齐。`.ui-table tbody td` 默认 middle（金额/日期/状态这类
     短字段合适）；「工作内容」会换行成多行，顶对齐才能让各行首行对齐、便于逐行比较。
     用 (0,2,2) 明确压过 (0,1,2)，不依赖样式书写顺序。 */
.ui-table .is-center { text-align: center; }
.ui-table tbody td.is-vtop { vertical-align: top; }
.ui-tstate {
  padding: 28px 16px; text-align: center;
  color: var(--text-3); font-size: var(--fs-base, 13px); line-height: 1.7;
}

/* ══════════ P1-2 公共分页（2026-10-18）══════════
   三套分页（er / pp / cs）合并成 `ui.pager()` 一个出口。
   这里只放**三套的公共部分**；各页的布局差异仍由旧类名承担：
     · `.pp-pager { justify-content: center }` —— 人员模块居中，这一版不改
     · `.cs-pager { margin-top: 12px }` —— 比通用的 14 略紧
   片 6 删旧 CSS 时，把「居中」升级成 `is-center` 变体再删旧块。 */
.ui-pager {
  display: flex; align-items: center; gap: 8px;
  margin-top: 14px; flex-wrap: wrap;
}
.ui-pager-info { font-size: var(--fs-sm); color: var(--text-3); margin: 0 4px; }

/* 列优先级（不是排序）：**须显式开启** `ui.table({ compact: true })` 才生效。
   存量页面换容器时先不启用 —— 它们现有「全列显示 + 容器内横向滚动」的行为与
   冒烟断言（12 列 / wrapScroll）都依赖全列，等 W2-6 移动端切片一起打开。 */
@media (max-width: 1200px) { .ui-table.is-compact [data-p="3"] { display: none; } }
@media (max-width: 900px) { .ui-table.is-compact [data-p="2"] { display: none; } }

/* ══════════ P2-1 公共轻提示（2026-10-18 · B1）══════════
   8 套提示（pp/er/pj/ac/rp/cs/imp/dt）合并成：容器 `#ui-toasts` + 条目 `.ui-toast[data-kind]`。
   为什么容器与条目分开：
     ① 多条要能**竖向堆叠不重叠** —— 原来每条各自 `position:fixed; bottom:28px`，连发 3 条就叠成一坨；
     ② 播报区 `#ui-live` 必须**常驻**（切页不清空），不能跟着条目一起被删；
     ③ 层级收敛成一个出口 `--z-toast`（原来是 60/80/90 各写一遍）。
   旧模块类名（`.pp-toast` 等）已随 B7 退出：条目只有 `.ui-toast` + 语义类，
   视觉出处唯一（本文件）。堆叠/间距/时长的变化属**有意变更**，走人工验收。 */
#ui-toasts {
  position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
  z-index: var(--z-toast);
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  width: max-content; max-width: min(86vw, 460px);
  pointer-events: none; /* 提示不该挡住下面的按钮 */
}

.ui-toast {
  pointer-events: auto;
  max-width: 100%;
  padding: 10px 16px; border-radius: var(--r-md);
  background: var(--text); color: #fff;
  font-size: var(--fs-base, 13px); line-height: 1.5;
  box-shadow: 0 4px 16px rgba(31, 35, 41, .18);
  animation: ui-toast-in .16s ease-out;
}

.ui-toast[data-kind="ok"] { background: var(--ok); }
.ui-toast[data-kind="warn"] { background: var(--warn); }
.ui-toast[data-kind="err"] { background: var(--danger); }

@keyframes ui-toast-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) { .ui-toast { animation: none; } }

@media (max-width: 768px) {
  #ui-toasts { bottom: calc(var(--tabbar-h) + 24px); }
}

/* ══════════ 「结果未知」出口条（F4 · 2026-10-21）══════════
   写操作超时不是普通提示：服务端可能已经写入，直接重做就是重复建单 / 二次删除，
   所以它**常驻**（toast 3.2s 消失，用户来不及读完），并且独占顶部、不被 toast 顶掉。
   与 toast 同层（`--z-toast`）：两者都是提示，不需要再分胜负。 */
.ui-outcome {
  position: fixed; left: 50%; top: 16px; transform: translateX(-50%);
  z-index: var(--z-toast);
  display: flex; align-items: flex-start; gap: 14px;
  width: max-content; max-width: min(92vw, 640px);
  padding: 12px 14px; border-radius: var(--r-md);
  background: var(--card); color: var(--text);
  border: 1px solid var(--danger);
  box-shadow: 0 6px 24px rgba(31, 35, 41, .16);
}

.ui-outcome-title { font-weight: 600; }
.ui-outcome-text { margin-top: 2px; color: var(--text-2); font-size: var(--fs-base, 13px); line-height: 1.5; }
.ui-outcome-acts { display: flex; gap: 8px; flex: none; align-self: center; }
.ui-outcome-acts .btn { white-space: nowrap; }

@media (max-width: 768px) {
  /* 🔴 移动端放**顶部**：底部是 toast 的地盘（`#ui-toasts` 在 tabbar 上方），
     两者同层时后插入的条会盖住 toast（2026-10-21 首审 P2-6）。 */
  .ui-outcome {
    flex-direction: column; align-items: stretch;
    /* 顶栏下面，别压住 56px 的返回/标题（首审 P2-6 的第一版会压上去） */
    top: calc(var(--topbar-h) + 8px); bottom: auto; max-width: 92vw;
  }
  .ui-outcome-acts { justify-content: flex-end; align-self: auto; }
}

/* 读屏播报区：视觉不可见但**不能** display:none / visibility:hidden（那样读屏也不播）。
   与可见条目分离，避免同一条提示被播两遍。 */
#ui-live {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ══════════ P2-2 键盘焦点可见（2026-10-18）══════════
   盘点实测：全站 13 处 `outline: none`，其中 **4 组完全没有替代**
   （dictionary 编辑输入 / er 表单控件 / people 筛选 / project 面板）——
   键盘用户在这些控件上根本看不到焦点在哪。其余 9 处只靠 1px 边框变色，
   而 `--line`(#EBEDF0) → `--brand`(#3B82F6) 在浅底上非文本对比不足 3:1。

   做法：13 处「抹掉焦点」的声明**全部删除**，改为这一处统一定义 ——
     · `:focus-visible`：键盘聚焦一定出 2px 环；鼠标点击不出（保持原视觉）；
     · `:focus:not(:focus-visible)`：只抹掉浏览器给表单控件的默认框（仅鼠标路径）。
   🔴 不要改回「各页自己写 outline:none + border-color」：那样每加一个控件就要
     重复一次，漏一个就静默失去焦点可见性 —— 13 处正是这么攒出来的。
   ⚠️ `.ui-table tr.is-clickable:focus-visible`（上方约 448 行）特异性更高，
     它的 `outline-offset: -2px` 继续生效，行内焦点环不被本段覆盖。
   验收：冒烟 [B2a] 逐页读 computedStyle —— outlineStyle 非 none 且 width ≥ 2px。 */
:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}
:focus:not(:focus-visible) { outline: none; }

/* 减少动效（P2-2）：系统开启「减少动态效果」时，动画与过渡都压到一帧。
   为什么用全局兜底而非逐条补：现有 2 处 `transition` 分散在 dataimport（上传区）
   与 project（进度条）两个模块，以后还会继续加 —— 逐条补必然漏。
   `animation-iteration-count: 1` 顺带收掉循环动画；`scroll-behavior` 对前庭敏感
   用户同样是动效，一并归零。（上面 `.ui-toast { animation: none }` 更彻底，保留。） */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}
