/* ==========================================================================
   PaperOps 设计系统
   桌面优先、中文优先的内部业务界面。无构建步骤（ADR-010）。
   ========================================================================== */

/* --- 1. 设计令牌 ------------------------------------------------------- */
:root {
  /* 中性色。
     背景比卡片暗一档是整套视觉的地基：白卡片必须"浮"在页面上，
     否则一屏白底白框只剩下边框在承担层级，那正是上一轮"像原生后台"的来源。 */
  --c-bg: #f4f5f8;
  --c-surface: #ffffff;
  --c-surface-alt: #fafbfc;
  --c-surface-sunken: #f1f3f6;
  --c-border: #e4e7ec;
  --c-border-strong: #c9d0d9;
  /* 悬停面：比 sunken 更浅，用于行悬停与安静按钮，不与"禁用"混淆 */
  --c-hover: #f3f5f8;
  /* 输入控件的关闭态底色（CR-20260814-02 §五）。比卡片暗**一档**——
     这一档就是「这里可以输入」的信号。聚焦或已填写时回到纯白。
     刻意不用 sunken：那个色阶用于整块区域（筛选栏托盘），
     控件用它会与所在区域糊在一起。 */
  --c-surface-input: #fcfcfd;

  /* 文本 */
  --c-text: #1a1f27;
  --c-text-secondary: #5a6472;
  --c-text-muted: #8b95a3;
  --c-text-inverse: #ffffff;

  /* 主色 */
  --c-primary: #2563eb;
  --c-primary-hover: #1d4ed8;
  --c-primary-active: #1e40af;
  --c-primary-soft: #eff4ff;
  --c-primary-border: #bfd3ff;

  /* 语义色 */
  --c-success: #15803d;
  --c-success-soft: #eaf7ee;
  --c-success-border: #b7e3c6;
  --c-warning: #b45309;
  --c-warning-soft: #fef6e7;
  --c-warning-border: #f5dca6;
  --c-danger: #b91c1c;
  --c-danger-soft: #fdefef;
  --c-danger-border: #f5c2c2;
  --c-info: #0369a1;
  --c-info-soft: #eaf5fb;
  --c-info-border: #b3ddf0;
  --c-neutral-soft: #f0f2f5;
  --c-neutral-border: #d9dee5;

  /* 排版 */
  --font-sans: "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB",
    -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", sans-serif;
  --font-mono: "SFMono-Regular", "Cascadia Mono", "JetBrains Mono", Consolas,
    "Liberation Mono", monospace;

  --fs-xs: 0.75rem;
  --fs-sm: 0.8125rem;
  --fs-base: 0.875rem;
  --fs-md: 0.9375rem;
  --fs-lg: 1.0625rem;
  --fs-xl: 1.25rem;
  --fs-2xl: 1.5rem;

  /* 间距 */
  --sp-1: 0.25rem;
  --sp-2: 0.5rem;
  --sp-3: 0.75rem;
  --sp-4: 1rem;
  --sp-5: 1.25rem;
  --sp-6: 1.5rem;
  --sp-8: 2rem;
  --sp-10: 2.5rem;
  --sp-12: 3rem;

  /* 圆角与阴影 */
  --radius-sm: 4px;
  --radius: 6px;
  --radius-lg: 10px;
  --shadow-xs: 0 1px 2px rgba(16, 24, 40, 0.04);
  --shadow-sm: 0 1px 3px rgba(16, 24, 40, 0.07), 0 1px 2px rgba(16, 24, 40, 0.04);
  --shadow-md: 0 4px 12px rgba(16, 24, 40, 0.08);
  --shadow-lg: 0 12px 32px rgba(16, 24, 40, 0.14);

  /* 侧栏（深色反相面）。此前这五个值是散在 §3 里的字面量，
     改一次主色要翻三处；集中到令牌层，与 §6.1 的颜色角色一一对应。 */
  --c-nav-bg: #101720;
  --c-nav-text: #aeb8c6;
  --c-nav-text-strong: #ffffff;
  --c-nav-text-muted: #667283;
  --c-nav-border: rgba(255, 255, 255, 0.07);
  --c-nav-hover: rgba(255, 255, 255, 0.055);
  --c-nav-active: rgba(255, 255, 255, 0.1);
  --c-nav-accent: #6ea0ff;

  /* 布局 */
  --sidebar-w: 224px;
  --topbar-h: 52px;
  --content-max: 1560px;
  /* 图标底托：指标卡、操作卡、空状态共用同一个尺寸，
     不同页面的图标块大小不一致会立刻显得是拼出来的。 */
  --tile: 34px;
  --tile-sm: 28px;

  --transition: 140ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* --- 2. 基础重置 ------------------------------------------------------- */
*,
*::before,
*::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--fs-base);
  line-height: 1.6;
  color: var(--c-text);
  background: var(--c-bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4, h5, h6 { margin: 0; font-weight: 600; line-height: 1.35; }
h1 { font-size: var(--fs-2xl); }
h2 { font-size: var(--fs-xl); }
h3 { font-size: var(--fs-lg); }
h4 { font-size: var(--fs-md); }
p { margin: 0 0 var(--sp-3); }
p:last-child { margin-bottom: 0; }

a { color: var(--c-primary); text-decoration: none; transition: color var(--transition); }
a:hover { color: var(--c-primary-hover); text-decoration: underline; }

code, pre, .mono { font-family: var(--font-mono); font-size: 0.92em; }
pre {
  margin: 0;
  padding: var(--sp-3) var(--sp-4);
  background: var(--c-surface-sunken);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  overflow-x: auto;
  line-height: 1.55;
}
code:not(pre code) {
  padding: 0.1em 0.35em;
  background: var(--c-surface-sunken);
  border-radius: var(--radius-sm);
  border: 1px solid var(--c-border);
}

hr { border: 0; border-top: 1px solid var(--c-border); margin: var(--sp-6) 0; }

:focus-visible {
  outline: 2px solid var(--c-primary);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* 图标基类（`components/icon.html`）。默认 16px、跟随 currentColor、
   `flex-shrink:0` 防止在挤压的 Flex 行里被压扁成椭圆。
   尺寸只有四档——图标大小是"看起来是一套"最容易露馅的地方。 */
.icon { width: 16px; height: 16px; display: block; flex-shrink: 0; }
.icon--sm { width: 14px; height: 14px; }
.icon--lg { width: 18px; height: 18px; }
.icon--xl { width: 22px; height: 22px; }

/* --- 3. 应用骨架 ------------------------------------------------------- */
.app { display: flex; min-height: 100vh; }

/* 侧栏：品牌与页脚固定，只有导航区可滚（CR-20260816-06 / §12）。
 *
 * 此前 `overflow-y: auto` 挂在 `.sidebar` 上，于是超出的时候**整根侧栏**一起滚，
 * 页脚（时区）会被推到视口外——1366×768 实测 scrollHeight 916 / clientHeight 768，
 * 「系统设置」底边在 853、页脚底边在 916，两者都在折线以下。
 *
 * 改成滚动区落在 `.sidebar__nav`：品牌永远在顶、页脚永远在底，
 * 中间那段导航才是弹性区。即便将来权限矩阵再长出几个入口，
 * 掉出视口的也只会是中间的导航条目，而不是页脚。
 *
 * `min-height: 0` 是这套写法的关键：flex item 的默认 `min-height: auto`
 * 会让它拒绝收缩到内容高度以下，`overflow-y: auto` 因此永远不触发。
 */
.sidebar {
  width: var(--sidebar-w);
  flex: 0 0 var(--sidebar-w);
  background: var(--c-nav-bg);
  color: var(--c-nav-text);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
}

.sidebar__brand {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  height: var(--topbar-h);
  padding: 0 var(--sp-4);
  font-size: var(--fs-md);
  font-weight: 650;
  letter-spacing: -0.01em;
  color: var(--c-nav-text-strong);
  border-bottom: 1px solid var(--c-nav-border);
  flex-shrink: 0;
}
.sidebar__brand:hover { text-decoration: none; color: var(--c-nav-text-strong); }
.sidebar__brand-mark {
  width: 22px; height: 22px;
  display: block;
  flex-shrink: 0;
}

.sidebar__nav {
  padding: var(--sp-2) var(--sp-3);
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
/* 分组标题（总览 / 业务 / 邮件 / 工具 / 系统）。
   中文不存在大小写，`text-transform: uppercase` 对它没有任何作用——
   之前那条规则只是让夹在中间的拉丁字母变形。改用字号 + 字距 + 弱化色
   来拉开它与条目本身的层级。 */
/* 分组间距（CR-20260816-06 收紧）：原来是 20px 上 / 8px 下，五个分组光间距就吃掉
   216px。11px 的小标签不需要 20px 的上留白来和上一组拉开——8/4 之后分组依然
   一眼可辨，而 1366×768 下整根侧栏才装得下。字号一个像素没动。 */
/* CR-20260816-07 §39 曾试图把分组上留白从 8px 加到 9px，**实测之后撤销**。
 *
 * 字号 14→15 之后 1366×768 下 `.sidebar__nav` 的余量已经不足 1px：
 * 加 1px × 4 个分组（首个分组另有 `:first-child` 覆盖）就让
 * scrollHeight 689 > clientHeight 688，侧栏内部开始滚动——那是本节的硬约束。
 *
 * 字号与行高的收益（14→15px、32.4→34px）比分组间距明显得多，因此保留前者、
 * 放弃后者。这里留下记录是为了让下一个人知道：这一格不是忘了改，是量过之后没有余量。 */
.sidebar__section {
  padding: var(--sp-2) var(--sp-3) var(--sp-1);
  font-size: 11px;
  letter-spacing: 0.09em;
  color: var(--c-nav-text-muted);
  font-weight: 600;
}
.sidebar__section:first-child { padding-top: var(--sp-1); }


/* 导航条目（CR-20260814-01 / §3）。
 *
 * 此前 active 是一整块实心主色：一屏之内最饱和的色块落在"我现在在哪"上，
 * 比页面上任何一个业务动作都响。改成"抬起的中性面 + 左侧 2px 指示条 +
 * 提亮的图标"：位置信息照样一眼可见，但它退回背景层，不再和内容抢注意力。
 *
 * 指示条用 ::before 画而不是 border-left，这样条目本身不会因为选中而位移。
 */
.sidebar__link {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  /* 7px → 5px / 2px → 1px（CR-20260816-06）：15 个条目，每个省 5px 就是 75px。
     行高从 36.4 收到 32.4，仍在舒适点选区间内（图标 17px），字号未动。

     CR-20260816-07 §39：在 1366×768 不滚动这条硬约束**不变**的前提下把字号调回去。
     预算是量出来的，不是估的——改动前实测 1366×768 下最后一个导航项底边 703.8px、
     页脚顶边 731.8px，中间 28px 是纯黑留白（正是用户说的「底部无意义留白」）。
     再把页脚上下内边距从 8px 收到 4px（版权行本来就是低视觉权重，§38），
     合计可支配约 36px。

     花法：字号 14 → 15px。行盒随之从 22.4 长到 24，每行 +1.6px、15 行 +24px，
     行高因此从 32.4 回到 34（§39 点名的「font size」与「row height」一起兑现）。
     内边距刻意**不动**：+1px 内边距是每行 +2px、合计 +30px，一项就会吃光预算。 */
  padding: 5px var(--sp-3);
  margin-bottom: 1px;
  border-radius: var(--radius);
  color: var(--c-nav-text);
  font-size: var(--fs-md);
  transition: background var(--transition), color var(--transition);
}
.sidebar__link::before {
  content: "";
  position: absolute; left: calc(-1 * var(--sp-3) + 1px); top: 50%;
  width: 2px; height: 0;
  border-radius: 0 2px 2px 0;
  background: var(--c-nav-accent);
  transform: translateY(-50%);
  transition: height var(--transition);
}
.sidebar__link:hover { background: var(--c-nav-hover); color: var(--c-nav-text-strong); text-decoration: none; }
.sidebar__link--active { background: var(--c-nav-active); color: var(--c-nav-text-strong); font-weight: 550; }
.sidebar__link--active::before { height: 17px; }
.sidebar__link--active:hover { background: var(--c-nav-active); color: var(--c-nav-text-strong); }
/* 18px（CR-20260816-07 §39，原 17px）。图标仍小于 15px 字号的行盒（24px），
   因此**不增加任何行高**——这一格是纯粹的视觉收益，不花高度预算。 */
.sidebar__icon { width: 18px; height: 18px; flex-shrink: 0; opacity: 0.72; display: block; }
.sidebar__link:hover .sidebar__icon { opacity: 0.92; }
.sidebar__link--active .sidebar__icon { opacity: 1; color: var(--c-nav-accent); }
.sidebar__badge {
  margin-left: auto;
  background: var(--c-danger);
  color: var(--c-text-inverse);
  font-size: 11px;
  font-weight: 600;
  padding: 0 6px;
  border-radius: 9px;
  min-width: 18px;
  text-align: center;
  line-height: 17px;
}
/* 页脚：紧凑、低视觉权重、永不参与滚动（CR-20260816-06 / §11）。
   `flex-shrink: 0` 保证导航再长也压不扁它——它是那条「全站时间戳按哪个时区读」
   的参考信息，压成半行就没有意义了。 */
.sidebar__footer {
  flex-shrink: 0;
  padding: var(--sp-2) var(--sp-4);
  border-top: 1px solid var(--c-nav-border);
  font-size: var(--fs-xs);
  color: var(--c-nav-text-muted);
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
}

/* 版权页脚（CR-20260816-07 §38）。低视觉权重：单行、居中、弱化色。
 *
 * 上下内边距从 8px 收到 4px——省下的 8px 直接用于导航字号（§39）。
 * 这不是为了「更紧凑」，而是因为版权行不值得占用导航的高度。
 *
 * ⚠️ 必须写在 `.sidebar__footer` **之后**：两者特指度相同，
 * 写在前面会被基类的 `padding` 简写整个盖掉，改了等于没改。 */
.sidebar__footer--copyright {
  padding-top: var(--sp-1);
  padding-bottom: var(--sp-1);
  justify-content: center;
}

/* ── 高视口更舒展，低视口一个字节不变（CR-20260817-02 §50–§54）─────────────
 *
 * ## 为什么必须是视口高度条件，而不是把间距整体调大
 *
 * 上面两条注释已经记录过两次实测结论：1366×768 下 `.sidebar__nav` 的余量
 * **不足 1px**，CR-20260816-07 §39 想给分组加 1px 都被量出来会触发内部滚动。
 * 所以「用户觉得留白多」和「768 装不下」这两件事都是真的——它们说的是
 * **不同的视口**。任何全局加高都会用后者的代价换前者的收益。
 *
 * ## 预算是算出来的（2026-08-17 浏览器实测）
 *
 *   nav 内容实际占用      = 667.97px（三档视口下都一样，与视口无关）
 *   nav clientHeight     = 视口高 − 80px（品牌区 52 + 页脚约 28）
 *
 *   视口 768 → nav 688 → 余量  20.03px   ← 硬门禁所在，这里不加任何东西
 *   视口 900 → nav 820 → 余量 152.03px
 *   视口 1080 → nav 1000 → 余量 332.03px
 *
 * 一个 `min-height: H` 断点的最坏情况是视口正好等于 H，因此可支配预算是
 * `(H − 80) − 667.97`：
 *
 *   H = 860  → 可支配 112.03px，本轮只花 60px（留 52px 余量）
 *   H = 1000 → 可支配 252.03px，本轮只花 144px（留 108px 余量）
 *
 * 刻意只花掉一半多：预算是按**当前** 15 个导航项算的，将来加一个模块入口
 * （一项约 35px）不该立刻把某一档打穿。
 *
 * ## 为什么不用 justify-content: space-between
 *
 * §52 点名禁止。它会把五个分组按剩余空间**机械平分**，1080 下分组之间会
 * 出现 60px 以上的空档——那不是舒展，那是散架。这里加的是每一项自己的
 * 内边距，因此密度是均匀变化的。
 *
 * `min-width: 901px` 与既有的 900px 折叠断点对齐：900 以下侧栏收成图标条、
 * 分组标题整体隐藏，内容高度完全是另一回事，不该套用这里的预算。
 *
 * ⚠️ **必须写在全部 `.sidebar__*` 基类之后**，与紧邻上方 `.sidebar__footer--copyright`
 * 的告警是同一件事。本轮第一次写完放在了 `.sidebar__link` **之前**，同特指度下
 * 基类的 `padding` 简写胜出——1440×900 的实测数字与改动前**逐像素相同**，
 * 而 CSS 里明明白白写着新值。媒体查询不会让规则赢，顺序才会。
 */
@media (min-height: 860px) and (min-width: 901px) {
  /* 15 项 × 上下各 +2px = +60px */
  .sidebar__link { padding: 7px var(--sp-3); }
}

@media (min-height: 1000px) and (min-width: 901px) {
  /* 15 × 8 = 120，4 个分组 × 4 = 16（首个分组由 :first-child 覆盖），nav 自身 +8 → 144px */
  .sidebar__nav { padding: var(--sp-3) var(--sp-3); }
  .sidebar__link { padding: 9px var(--sp-3); }
  .sidebar__section { padding: var(--sp-4) var(--sp-3) var(--sp-1); }
  .sidebar__section:first-child { padding-top: var(--sp-1); }
}

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

.topbar {
  height: var(--topbar-h);
  background: var(--c-surface);
  border-bottom: 1px solid var(--c-border);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 0 var(--sp-5) 0 var(--sp-6);
  position: sticky;
  top: 0;
  z-index: 30;
  flex-shrink: 0;
}
/* 顶栏标题是**当前位置**，不是页面标题——页面标题由 `.page-header h1` 承担，
   两处用同样的分量会让人以为是同一行内容重复了一遍。这里弱化成一句定位文字。 */
.topbar__title { font-size: var(--fs-sm); font-weight: 550; color: var(--c-text-secondary); }
.topbar__spacer { flex: 1; }
.topbar__divider { width: 1px; height: 20px; background: var(--c-border); flex-shrink: 0; }
/* 只对读屏器可见的标签。输入框已有 placeholder，但 placeholder 不是 label。 */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* 编号直达（CR-20260813-01，本轮重做外观）。
 *
 * 此前是一个孤零零的裸输入框浮在顶栏中间——既不像搜索，也不属于任何结构。
 * 现在是一个完整的搜索控件：沉底面 + 前置放大镜图标 + 聚焦时升为白底描边。
 * 行为一个字没改，仍然只做"一个输入框 + 一次 302"。 */
.topbar__jump { display: flex; align-items: center; }
.topbar__jump-field { position: relative; display: flex; align-items: center; }
.topbar__jump-icon {
  position: absolute; left: 9px;
  width: 15px; height: 15px;
  color: var(--c-text-muted);
  pointer-events: none;
}
/* ⚠️ 选择器必须是 `.topbar .topbar__jump-input`（0,2,0），不能只写类名。
 * 表单系统里有一条 `input[type="search"], ...` 的通用规则，特异性 (0,1,1)
 * 且位置更靠后——单写 `.topbar__jump-input`（0,1,0）会被它的
 * `min-height: var(--control-h)` = 42px 压掉，改高度看起来"没生效"。
 *
 * 本仓库这已经是第三次栽在同一件事上（`.table th` 压 `.table__num`、
 * `--control-h` 被定义两次）。改控件尺寸前先算一遍特异性，或直接量计算值。 */
.topbar .topbar__jump-input {
  /* 22rem 而不是 15rem（CR-20260815-19）：它从顶栏右角的「编号直达」变成了
     左侧的全站搜索入口，宽度要撑得住「主题、发件人或文件名」这类真实查询词。 */
  width: 22rem; max-width: 38vw;
  /* 28px 而不是 32px（CR-20260815-12 / 用户裁定「高度太高了」）：
     顶栏是一条 56px 的窄条，里面的控件越接近内容高度越像"栏里的东西"，
     越接近表单控件高度就越像"一个表单掉进了顶栏"。 */
  min-height: 28px;
  padding: 0 .6rem 0 30px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: var(--c-surface-sunken);
  color: var(--c-text);
  font-size: var(--fs-sm);
  transition: background var(--transition), border-color var(--transition), box-shadow var(--transition);
}
.topbar .topbar__jump-input::placeholder { color: var(--c-text-muted); }
.topbar .topbar__jump-input:hover { background: var(--c-hover); }
.topbar .topbar__jump-input:focus {
  background: var(--c-surface);
  border-color: var(--c-primary);
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.13);
  outline: none;
}
@media (max-width: 900px) { .topbar__jump { display: none; } }

/* 滚动条（CR-20260815-12，CR-20260815-14 修正生效条件）。
 *
 * ## 上一版为什么没生效
 * 写成全局 `* { scrollbar-width: thin }` + `::-webkit-scrollbar{...}`，
 * 以为两套并存、各引擎各取所需。**恰恰相反**：Chromium 从 121 起规定，
 * 元素一旦设了标准属性 `scrollbar-width`，该元素的 `::-webkit-scrollbar*`
 * 规则**全部失效**。于是那条 `*` 把自己的伪元素样式关掉了，
 * 看到的是 Chrome 内置的 thin 滚动条——用户说「还是原生的」，是对的。
 *
 * 修法：用 `@supports not selector(::-webkit-scrollbar)` 把标准属性**只**给
 * 不支持伪元素的引擎（Firefox）。Chromium / Safari 那边一个 `scrollbar-width`
 * 都不设，伪元素因此正常生效。
 *
 * ## 深色容器要单独给颜色
 * 侧栏是深色面。浅灰滑块在浅色页面上刚好，放到深色侧栏上会太跳；
 * 所以侧栏用一档更暗、带透明度的白，随背景融合。
 *
 * ## 仍然不做 display: none
 * 藏掉滚动条会让「这里还能往下滚」失去唯一提示，长表格与代码块尤其致命。
 * 做的是变细、去掉两端箭头按钮、配色跟随容器。 */
@supports not selector(::-webkit-scrollbar) {
  * {
    scrollbar-width: thin;
    scrollbar-color: var(--c-border-strong) transparent;
  }
  .sidebar, .sidebar * { scrollbar-color: rgba(255, 255, 255, 0.28) transparent; }
}

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background-color: var(--c-border-strong);
  border-radius: 999px;
  /* 用透明边框把滑块"收窄"成 6px，同时保留 10px 的可抓取宽度——
     直接把 width 设成 6px 会让它变得很难用鼠标抓住。 */
  border: 2px solid transparent;
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background-color: var(--c-text-muted); }
/* 两端的箭头按钮是原生外观里最"系统"的一块，去掉。
   `width/height: 0` 与 `display: none` 一起写：某些 Chromium 版本对后者
   仍会保留按钮占位，前者才真正把它压没。 */
::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
::-webkit-scrollbar-corner { background: transparent; }

/* 深色侧栏：同一套形状，换一档颜色。 */
.sidebar::-webkit-scrollbar-thumb,
.sidebar *::-webkit-scrollbar-thumb { background-color: rgba(255, 255, 255, 0.28); }
.sidebar::-webkit-scrollbar-thumb:hover,
.sidebar *::-webkit-scrollbar-thumb:hover { background-color: rgba(255, 255, 255, 0.45); }

/* 右上角消息提醒（CR-20260816-01 / ADR-142）。
 *
 * 与右下角的任务坞是**一对**：坞回答「有什么在跑」，提醒回答「刚刚跑完了」。
 * 两者位置刻意分开——同在右下角会让「常驻状态」和「一次性通知」抢同一块地方。
 *
 * `z-index` 高于进度框（320）与顶栏：它是最上层的一次性消息。
 * 但**低于**模态对话框的遮罩不适用——这里没有模态需要盖住它。 */
.toast-stack {
  position: fixed;
  top: calc(var(--topbar-h) + var(--sp-3));
  right: var(--sp-5);
  z-index: 400;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  width: min(360px, calc(100vw - 2 * var(--sp-5)));
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius);
  border: 1px solid var(--c-border);
  background: var(--c-surface);
  box-shadow: var(--shadow-lg);
  animation: toast-in .2s ease-out;
}
.toast--clickable { cursor: pointer; }
.toast--clickable:hover { border-color: var(--c-primary); }
.toast--info    { border-left: 3px solid var(--c-primary); }
.toast--success { border-left: 3px solid var(--c-success); }
.toast--danger  { border-left: 3px solid var(--c-danger); }
.toast__body { flex: 1 1 auto; min-width: 0; }
.toast__title { font-weight: 600; font-size: var(--fs-sm); color: var(--c-text); }
.toast__text {
  margin-top: 2px; font-size: var(--fs-xs); color: var(--c-text-secondary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.toast__close {
  flex: 0 0 auto; border: 0; background: none; cursor: pointer;
  color: var(--c-text-muted); font-size: 18px; line-height: 1; padding: 0 2px;
}
.toast__close:hover { color: var(--c-text); }
@keyframes toast-in { from { opacity: 0; transform: translateX(12px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .toast { animation: none; } }

/* 页面切换的过渡（CR-20260815-14 / 用户裁定「跳转太直接」）。
 *
 * 顶部一条 2px 的进度线 + 正文淡入。两者合起来回答一个问题：
 * 「我点了，系统收到了吗」——服务端渲染的站点在旧页面与新页面之间有一段
 * 完全没有反馈的空窗，快时像卡了一下，慢时用户会以为没点上、再点一次。
 *
 * 进度条用 `transform: scaleX()` 而不是改 `width`：前者只触发合成，
 * 不会在导航正忙的时候再逼浏览器做一轮布局。
 *
 * `z-index` 要高过顶栏与任务坞——它是"整页正在换"的指示，不属于任何一层内容。 */
.nav-progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 2px;
  z-index: 900;
  transform: scaleX(0);
  transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--c-primary), var(--c-primary-hover));
  opacity: 0;
  pointer-events: none;
  transition: transform .18s ease-out, opacity .18s ease-out;
}
.nav-progress.is-active { opacity: 1; }

/* 正文淡入。只动 `.content`，不动侧栏与顶栏——那两块在页面之间基本不变，
   让它们跟着闪一下反而像整站重载。位移只有 3px：是"落位"，不是"飞入"。 */
/* 正文淡入 250ms（CR-20260815-20 再调；此前 220ms、更早 120ms）：
   120ms 快到只留下一次"闪"的印象，眼睛没跟上就结束了，反而比没有动效更晃。
   220ms 是"看得出来在落位"的下限，仍然远低于会让人等待的量级。
   位移 4px：够表达方向，不至于像元素飞进来。 */
/* ⚠️ `backwards` 而**不是** `both`。这不是风格选择，是一个真实缺陷的修复。
 *
 * `both` 会把 `to` 关键帧的值在动画结束后继续应用，于是 `.content` 上永远留着一个
 * `transform: matrix(1,0,0,1,0,0)`——单位矩阵，视觉上等于没有，但它让 `.content`
 * 成为**固定定位的包含块**。结果：拟题页那个 `position: fixed; inset: 0` 的中央
 * 处理框不再相对视口定位，而是相对正文——实测框的位置是 (0, 952)、尺寸 1440×2366，
 * 整个跑到折叠线以下，遮罩也只盖住正文而盖不住右栏（用户报的「不居中、右边是白的」）。
 *
 * `backwards` 只在动画**开始前**应用 `from`，结束后属性回到元素自身的值（`none`），
 * 包含块随之消失。淡入效果一模一样。
 *
 * 教训：给一个大容器加 `transform` 动画之前，先想清楚它里面有没有 `position: fixed`。 */
.content { animation: page-enter .25s cubic-bezier(.22, .61, .36, 1) backwards; }
@keyframes page-enter {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}

/* 减弱动效：淡入与进度条的补间全部关掉。
   进度条本身**仍然显示**——它传递的是状态（正在加载），不是装饰；
   把它一并去掉等于把这个偏好的用户退回到没有反馈的旧状态。 */
@media (prefers-reduced-motion: reduce) {
  .content { animation: none; }
  .nav-progress { transition: none; }
}

/* 用户区：头像 + 名字 + 角色是**一个**可点击面，退出是它旁边一个独立的图标按钮。
   此前三者是三个并排的元素，中间还夹着一个蓝色徽标，读起来像三个控件。 */
.topbar__user { display: flex; align-items: center; gap: var(--sp-2); }
.topbar__account {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 3px var(--sp-2) 3px 3px;
  border-radius: 999px;
  color: var(--c-text);
  font-size: var(--fs-sm);
  transition: background var(--transition);
}
.topbar__account:hover { background: var(--c-hover); text-decoration: none; color: var(--c-text); }
.topbar__account-name { font-weight: 550; white-space: nowrap; }
.topbar__account-role { font-size: var(--fs-xs); color: var(--c-text-muted); white-space: nowrap; }
@media (max-width: 640px) { .topbar__account-name, .topbar__account-role { display: none; } }
.avatar {
  width: 26px; height: 26px; border-radius: 50%;
  background: var(--c-primary-soft); color: var(--c-primary);
  display: grid; place-items: center;
  font-size: var(--fs-sm); font-weight: 600;
  border: 1px solid var(--c-primary-border);
  flex-shrink: 0;
}

.content { padding: var(--sp-6) var(--sp-6) var(--sp-10); max-width: var(--content-max); width: 100%; flex: 1; }

/* --- 3a. 页头 ---------------------------------------------------------- */
/* 每个页面的第一屏都从这里开始，所以它的结构就是全站的结构感。
 *
 * 分四层，顺序固定：面包屑 → 标题行（标题 + 编号 + 状态）→ 一行说明 →
 * 视图切换。动作区在右侧，层级由 §4 的按钮规则决定。
 * 底部一条通栏细线：把"页面身份"和"页面内容"明确分开——没有它，
 * 标题只是碰巧排在最上面的一段粗体字。 */
.page-header {
  display: flex;
  /* 有标识块时整行居中（标识块 38px 高于文字块的第一行），
     没有标识块时保持顶对齐——那是记录页与表单页的形态。 */
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-4);
  margin-bottom: var(--sp-5);
  padding-bottom: var(--sp-4);
  border-bottom: 1px solid var(--c-border);
  flex-wrap: wrap;
}
/* 少数页面（登录后的空壳页、无标题的嵌入页）不需要那条线 */
.page-header--plain { border-bottom: 0; padding-bottom: 0; }
/* `flex: 1 1 420px` 而不是让文本块自然撑满：说明文字长一点时，
   `page-header__actions` 会被挤到第二行、左对齐单独占一行，看起来像掉出来的
   （投稿看板就是这样）。给文本块一个可收缩的基准宽度，动作区就始终在右上角。 */
/* 页头标识块（CR-20260815-16 / 用户裁定「其他标题都很简陋」）。
 *
 * AI 拟题页早就有一个「图标 + 标题 + 一句说明」的页头（`.tool-header`），
 * 而其余页面只有一行加粗字。同一个系统里两种页头形态，前者看起来像产品、
 * 后者看起来像半成品。这里把那套视觉语言收成一个可复用的标识块。
 *
 * ## 只给**分区落地页**，不给记录页与表单页
 * 落地页（客户、稿件、邮件、后台任务……）回答「我在系统的哪一块」，
 * 图标是这块的身份，与侧栏那一项一一对应。
 * 记录页的标题是**这条记录的名字**（某位客户、某封邮件），
 * 在它旁边放一个分区图标会和记录本身抢位置——那里要的是编号与状态，不是分区标识。
 *
 * ## 比 `.tool-header` 轻
 * 不带卡片底与渐变：列表页的页头上方就是顶栏、下方紧接筛选栏，
 * 再加一层卡片会让第一屏出现三条横向分隔。这里只保留图标块本身。 */
.page-header__mark {
  flex: 0 0 auto;
  width: 38px; height: 38px;
  display: grid; place-items: center;
  border-radius: var(--radius);
  background: var(--c-primary-soft);
  color: var(--c-primary);
  border: 1px solid var(--c-primary-border);
}
.page-header__mark .icon { width: 20px; height: 20px; }
/* 带标识块的页头：标识与文字块一起垂直居中，动作区已由 `align-self: center` 处理。 */
.page-header:has(.page-header__mark) { align-items: center; }
.page-header__text { min-width: 0; flex: 1 1 420px; }
/* 页头标题的行框高度。
 *
 * ⚠️ 这里曾经**同时**定义 `--control-h: 34.5px`（CR-20260814-13），
 *    但表单区还有第二个 `:root` 把它重定义成 42px（见下方「表单系统」一节）。
 *    同名 token 定义两次，后者一直在覆盖前者——那句「今天一个像素都不变」
 *    描述的值**从来没有生效过**。全站控件实际一直是 42px。
 *
 *    已把定义收归到唯一一处（表单系统那个 `:root`），这里只留标题行高。
 *    发现方式：量到 `.btn--sm` 的 `min-height` 计算值是 34px 而不是新写的 26px，
 *    顺着找到了第二个定义。改 token 前先 grep 一遍它被定义了几次。 */
:root {
  --page-title-line: 1.85rem;
}
.page-header h1 {
  font-size: 1.375rem; letter-spacing: -0.012em;
  line-height: var(--page-title-line);
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  min-width: 0;
}
.page-header h1 > .badge { font-size: var(--fs-xs); }
/* 标题旁的内部编号：等宽、弱化，但和标题同一行——它是这条记录的身份证。 */
.page-header__code {
  font-family: var(--font-mono); font-size: var(--fs-sm); font-weight: 500;
  color: var(--c-text-muted); letter-spacing: 0;
}
.page-header__desc { color: var(--c-text-secondary); font-size: var(--fs-sm); margin-top: var(--sp-2); }
/* 标题下的一行事实（编号 · 单位 · 状态），区别于 `__desc` 的一句说明。 */
.page-header__meta {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-3);
  margin-top: var(--sp-2);
  font-size: var(--fs-sm); color: var(--c-text-secondary);
}
.page-header__meta .badge { flex-shrink: 0; }
/* max-width:100% 让按钮多时（如拟题规则页 6 个入口）在窄屏内换行，
   而不是靠 flex-shrink:0 撑出页面级横向滚动（CR-20260722-01 E2E 发现）。 */
/* 动作区与**标题那一行**居中对齐，而不是与整个文本块（标题 + 说明 + 元信息 + 视图切换）
 * 顶端对齐。`.page-header` 是 `align-items: flex-start`，于是 34.5px 高的按钮与
 * 29.7px 高的标题行同顶起排，中心差 **2.41px**——刚好是那种「说不出哪里不对，
 * 但看着不正」的量（实测见 `scripts/ui_geometry_audit.py`）。
 * 给动作区一个等于标题行高的最小高度并在其中居中，两者的视觉中线就重合了。 */
.page-header__actions {
  display: flex; align-items: center; gap: var(--sp-2);
  /* 按钮与**整个文字块**（标题 + 下面那行编号/日期/说明）垂直居中，
   * 不是与标题那一行居中，也不是与标题顶端齐平（CR-20260815-07 / 用户裁定，两轮）。
   *
   * 走过的两版都被验收否掉，原因值得留着：
   *  1. 与标题**行框**居中 → 按钮上沿越过页面第一条视觉边界约 2.4px，
   *     最上面那条线因此不平；
   *  2. 与标题行框**顶端齐平** → 文字块有第二行时，按钮整体偏高、
   *     像挂在文字块的上半截。
   * 现在按 flex 行的交叉轴居中：文字块多高，按钮就落在它的正中间，
   * 一行标题时退化成「与标题居中」，与旧行为一致。
   *
   * `align-self` 覆盖 `.page-header` 的 `align-items: flex-start`——后者是给
   * 文字块用的（它必须顶格），这里只改按钮自己。 */
  align-self: center;
  flex-shrink: 0; flex-wrap: wrap; max-width: 100%;
}

/* 视图切换链接（CR-20260813-02 / §7.1）。
 *
 * 页头动作区此前把两类完全不同的东西混成一排等宽按钮：**会改数据的动作**
 * （立即同步、测试连接、重新发现文件夹）和**只是换一页看**（同步记录、
 * 同步错误、全部邮件、看板视图）。邮箱详情页因此顶着七个同样分量的按钮，
 * 谁是主操作看不出来。
 *
 * 规则：动作区里最多一个 `.btn--primary`；纯导航条目改用这一行安静的链接，
 * 排在标题下方而不是按钮区里。 */
.page-header__views {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-1);
  margin-top: var(--sp-3);
  font-size: var(--fs-sm);
}
/* 纯导航条目：安静的胶囊，悬停才显形。做成胶囊而不是裸链接，是为了
   让它明确区别于正文里的蓝色内联链接——那是"跳到某条记录"，
   这是"换一个视图看同一批数据"。 */
.page-header__views a {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px var(--sp-3);
  border-radius: 999px;
  color: var(--c-text-secondary);
  transition: background var(--transition), color var(--transition);
}
.page-header__views a:hover { color: var(--c-text); background: var(--c-surface-sunken); text-decoration: none; }
.page-header__views .icon { width: 14px; height: 14px; opacity: .75; }

/* ⚠️ **页头动作区里的跨页导航现在用 `.btn`**（CR-20260815-05）。
 *
 * 演进过程留在这里，因为它解释了上面那条规则的边界：
 *   1. 最初这些链接浮在标题与正文之间（`.page-header__views`，胶囊样式）——
 *      验收判为「很丑也不专业」；
 *   2. 搬进动作区、保持安静文字链接（`--inline` 修饰类，中点分隔）——
 *      验收判为「太简陋」。旁边就是一个实心主按钮，落差太大，像还没做完。
 *   3. 现在：直接挂 `.btn`，成为动作区的次级按钮。
 *
 * 这**不违反**上面那条「把导航做成按钮 = 七个同分量按钮」的告诫：那条防的是
 * **同分量**——当年七个全是实心按钮，谁是主操作看不出来。次级按钮（描边白底）
 * 加唯一一个 `.btn--primary`（实心蓝）本身就是层级。
 *
 * 规则因此收敛成一句：**动作区里最多一个 `.btn--primary`**，其余一律次级。
 * `.page-header__views`（标题下的胶囊行）保留给**详情页面包屑之外**的场景，
 * 当前所有列表页都已不用它。 */

.breadcrumb {
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-sm); color: var(--c-text-muted);
  margin-bottom: var(--sp-2);
}
.breadcrumb a { color: var(--c-text-secondary); }
.breadcrumb a:hover { color: var(--c-text); }
.breadcrumb__sep { color: var(--c-border-strong); }

/* --- 3b. 页签 ---------------------------------------------------------- */
/* 同一实体的多个编辑面（当前：用户的「基本信息 / 功能权限」）。
   用链接而不是 JS 切换：每个面是独立 URL，可以直接收藏、分享与后退。 */
.tabs {
  display: flex; gap: var(--sp-1);
  border-bottom: 1px solid var(--c-border);
  margin-bottom: var(--sp-5);
}
.tabs__item {
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-base); font-weight: 500;
  color: var(--c-text-secondary);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color .12s ease, border-color .12s ease;
}
.tabs__item:hover { color: var(--c-text); }
.tabs__item--active {
  color: var(--c-primary);
  border-bottom-color: var(--c-primary);
}

/* --- 4. 卡片 ----------------------------------------------------------- */
/* 卡片头的分隔线本轮改成**条件性**的（CR-20260814-01 / §4）。
 *
 * "白框 + 标题 + 一条横线 + 内容"是上一轮全站最像半成品的形状：一屏里
 * 五张卡片各画一条横线，页面被切成一堆等分的白格子，而那些线一条也没有
 * 在分隔真正不同的东西。
 *
 * 规则改成：**只有当卡片体是齐边内容（表格、列表、行式表单）时才画线**，
 * 因为那时线是表格的上边界，有结构意义；卡片体是普通内容时不画线，标题
 * 与内容靠间距成组，读起来是一块，而不是两块被线粘在一起。
 */
.card {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xs);
}
.card + .card { margin-top: var(--sp-5); }
.card__header {
  display: flex; align-items: center; justify-content: space-between;
  column-gap: var(--sp-3); row-gap: var(--sp-1);
  padding: var(--sp-4) var(--sp-5) var(--sp-3);
  flex-wrap: wrap;
}
.card__header:has(+ .card__body--flush),
.card__header:has(+ .table-wrap),
.card__header:has(+ .row-form__head),
.card__header:has(+ .row-form-group) {
  padding-bottom: var(--sp-4);
  border-bottom: 1px solid var(--c-border);
}
/* 标题与紧随其后的普通内容之间不要两份内边距叠加 */
.card__header + .card__body:not(.card__body--flush) { padding-top: var(--sp-1); }
.card__title {
  font-size: var(--fs-md); font-weight: 600; letter-spacing: -0.005em;
  display: flex; align-items: center; gap: var(--sp-2); min-width: 0;
}
/* 卡片标题右侧的计数，如「稿件 3」。比括号写法更好扫，也不会被误当成正文。 */
.card__count {
  font-size: var(--fs-xs); font-weight: 600;
  color: var(--c-text-secondary);
  background: var(--c-surface-sunken);
  border-radius: 999px;
  padding: 0 7px; line-height: 18px;
  font-variant-numeric: tabular-nums;
}
/* 卡片头右侧的一组动作。单个按钮直接放在 header 里即可；两个以上用它，
   否则 `space-between` 会把它们分散到两端。 */
.card__actions { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
/* 卡片副标题一律**在标题下方另起一行**，不再被 `space-between` 推到卡片右端
   （CR-20260813-02）。此前 22 个模板把 `.card__subtitle` 写成 `.card__title` 的
   兄弟，于是「共 3 条」贴在最右、「按署名顺序……」这类整句说明也贴在最右，
   与右侧的操作按钮抢同一个位置——同一个类在不同卡片里读起来是两种东西。
   `flex-basis:100%` + `order:9` 让它无条件落到第二行并左对齐，
   模板一个字都不用改，标题与操作按钮仍在第一行。 */
.card__subtitle {
  font-size: var(--fs-sm); color: var(--c-text-muted); font-weight: 400;
  flex: 1 1 100%; order: 9; min-width: 0;
}
/* 「标题下说明 + 表格」结构（CR-20260722-06 §十）：作为 .card 直接子元素的说明文字
   （非 .card__header 内的副标题）曾贴卡片最左边（0 内边距），与下方紧凑表格首列不对齐。
   统一给它复用紧凑表格单元格的水平内边距 var(--sp-3)，使左边起点与「稿件」列及稿件标题一致；
   下方留 var(--sp-3) 与表头的垂直间距。仪表盘与全系统同构卡片一并生效。 */
.card > .card__subtitle { padding: 0 var(--sp-3) var(--sp-3); }
.card__body { padding: var(--sp-5); }
.card__body--flush { padding: 0; }
.card__footer {
  padding: var(--sp-3) var(--sp-5);
  border-top: 1px solid var(--c-border);
  background: var(--c-surface-alt);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
}

/* --- 5. 统计卡 --------------------------------------------------------- */
/* 统计卡网格。
 *
 * 上一版桌面端**固定 4 列**，因为当时八张卡一次铺开、4×2 正好两行。
 * 本轮把指标卡按语气分成了两组（需要处理 / 业务规模），组内数量随权限
 * 变化（管理员 5 张、员工 4 张），固定列数立刻退化成「4 + 1 个孤儿」。
 *
 * 回到 `auto-fit`，但下限提到 212px：1440 下最多排 5 列，
 * 而 `auto-fit` 会把空轨道折叠掉——4 张就是 4 等列、5 张就是 5 等列，
 * 两种权限下都不会出现落单的那一张。 */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(212px, 100%), 1fr));
  gap: var(--sp-3);
  margin-bottom: var(--sp-5);
}
/* 指标卡（本轮重做外观）。
 *
 * 此前是"一个标签 + 一个大数字"，八张一模一样地铺在仪表盘顶部：
 * 「客户 8」和「失败任务 4」占同样的分量，而这两件事的紧迫程度差着数量级。
 *
 * 现在每张卡由三部分构成：**语气图标托**（中性/待办/告警三档）、标签、数值。
 * 语气**不靠给数字染色**（那会变成一面彩色报警墙），而是靠图标托的底色——
 * 一个 34px 的小方块，足够一眼分辨，又不会盖过内容。
 * 数值仍然是卡片里最大的元素；`--warning` / `--danger` 只在**计数非零**时
 * 由模板加到数值上，用于把"有 4 件事要处理"和"一件都没有"区分开。
 *
 * 右上角的小箭头是**可点击性**的唯一提示：并非每张指标卡都是链接。
 */
.stat {
  position: relative;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-lg);
  padding: var(--sp-4);
  box-shadow: var(--shadow-xs);
  transition: border-color var(--transition), box-shadow var(--transition), background var(--transition);
  display: block;
}
a.stat:hover {
  text-decoration: none;
  border-color: var(--c-border-strong);
  box-shadow: var(--shadow-sm);
}
a.stat:hover .stat__go { opacity: 1; color: var(--c-primary); transform: translateX(1px); }
/* 常驻但很轻。做成"悬停才出现"曾经是候选方案，但那样静止状态下
   完全看不出这张卡能点——指标卡里有一部分确实只是数字，不是链接。 */
.stat__go {
  position: absolute; top: var(--sp-4); right: var(--sp-4);
  width: 15px; height: 15px;
  color: var(--c-border-strong);
  opacity: .9;
  transition: opacity var(--transition), transform var(--transition), color var(--transition);
}
a.stat:focus-visible .stat__go { opacity: 1; color: var(--c-primary); }
.stat__head { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
.stat__tile {
  width: var(--tile); height: var(--tile); flex-shrink: 0;
  border-radius: 8px;
  display: grid; place-items: center;
  background: var(--c-surface-sunken);
  color: var(--c-text-secondary);
  border: 1px solid var(--c-border);
}
.stat__tile .icon { width: 17px; height: 17px; }
.stat--attention .stat__tile { background: var(--c-warning-soft); color: var(--c-warning); border-color: var(--c-warning-border); }
.stat--critical .stat__tile  { background: var(--c-danger-soft);  color: var(--c-danger);  border-color: var(--c-danger-border); }
.stat--positive .stat__tile  { background: var(--c-success-soft); color: var(--c-success); border-color: var(--c-success-border); }
.stat--primary .stat__tile   { background: var(--c-primary-soft); color: var(--c-primary); border-color: var(--c-primary-border); }
.stat__label {
  font-size: var(--fs-sm); color: var(--c-text-secondary);
  display: flex; align-items: center; gap: var(--sp-2);
  min-width: 0;
}
.stat__value {
  font-size: 1.625rem; font-weight: 650; line-height: 1.15;
  margin-top: var(--sp-3); color: var(--c-text);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}
/* 指标卡的三段文字共用**一个**左边界（CR-20260814-13）。
 *
 * 此前标签被图标托推到 `--tile + --sp-3` 处，而数值与副文案回到卡片内边距——
 * 一张卡里因此有两个左边界，读起来像台阶。实测偏差 **46px**（= 34 + 12），
 * 1440/1280/375 三档、全部 5 张卡一致（`scripts/ui_geometry_audit.py`）。
 * 用户看到的「数字没有相对图标对齐、有点歪」就是它。
 *
 * 用同级选择器而不是 `:has()`：语义正好是「上面有那一行头部时，下面的文字跟着缩进」，
 * 而且没有头部（不带图标托）的指标卡自动不受影响，不需要另加修饰类。 */
.stat__head ~ .stat__value,
.stat__head ~ .stat__meta { margin-inline-start: calc(var(--tile) + var(--sp-3)); }
/* 数值后的单位（CNY、封、条）：与数字同一行但明显弱化，
   避免「0.2988 CNY」读起来像一个 8 字符的数字。 */
.stat__unit { font-size: var(--fs-sm); font-weight: 500; color: var(--c-text-muted); margin-left: 4px; }
/* 长数字兜底：统计卡的数值区绝不允许把卡片撑破。 */
.stat__value { overflow-wrap: anywhere; }
.stat__value--danger { color: var(--c-danger); }
.stat__value--warning { color: var(--c-warning); }
.stat__value--success { color: var(--c-success); }
.stat__meta {
  font-size: var(--fs-xs); color: var(--c-text-muted); margin-top: var(--sp-2);
  line-height: 1.5;
}

/* 内容区里的分组标签（「需要处理」/「业务规模」）。
   比 h2 轻得多：它不是一个章节的标题，只是给下面这组卡片一个名字。 */
.section-label {
  font-size: var(--fs-xs); font-weight: 600; letter-spacing: 0.06em;
  color: var(--c-text-muted);
  margin: 0 0 var(--sp-3);
}
.section-label + .stat-grid { margin-bottom: var(--sp-5); }

/* --- 5a2. 量级带 ------------------------------------------------------ */
/* 背景量级（客户 / 稿件 / 进行中任务）。一条白底细带，内部用竖线分段，
   每段是一个真链接。它的存在感必须明显低于指标卡——同一屏里，
   "有 4 个失败任务"和"一共有 8 个客户"不是一个量级的信息。 */
.tally {
  display: flex; flex-wrap: wrap;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xs);
  margin-bottom: var(--sp-5);
  overflow: hidden;
}
.tally__item {
  display: flex; align-items: center; gap: var(--sp-2);
  flex: 1 1 0; /* 三段均分整条，右侧不留一片没人认领的白 */
  padding: var(--sp-3) var(--sp-5);
  color: var(--c-text-secondary);
  font-size: var(--fs-sm);
  min-width: 0;
  transition: background var(--transition);
}
.tally__item + .tally__item { border-left: 1px solid var(--c-border); }
.tally__item:hover { background: var(--c-hover); text-decoration: none; color: var(--c-text); }
.tally__icon { width: 15px; height: 15px; color: var(--c-text-muted); }
.tally__label { min-width: 0; }
.tally__value { font-size: var(--fs-md); font-weight: 650; color: var(--c-text); font-variant-numeric: tabular-nums; }
@media (max-width: 640px) {
  .tally__item { flex: 1 1 100%; }
  .tally__item + .tally__item { border-left: 0; border-top: 1px solid var(--c-border); }
}

/* --- 5a. 分布条 ------------------------------------------------------- */
/* 只有一种颜色、只有 3px 高：它要回答的是"哪一档更多"，
   不是"每一档分别是什么状态"——状态语义已经由标签文字承担。
   给七个状态各配一种颜色会把这张卡变成一面色卡（§10 禁止的方向）。 */
.dist { list-style: none; margin: 0; padding: 0; }
.dist__item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: var(--sp-3);
  padding: var(--sp-2) 0;
}
.dist__item + .dist__item { border-top: 1px solid var(--c-border); }
.dist__label { font-size: var(--fs-sm); color: var(--c-text-secondary); min-width: 0; }
.dist__value { font-size: var(--fs-base); font-weight: 600; font-variant-numeric: tabular-nums; }
.dist__track {
  grid-column: 1 / -1;
  height: 3px; margin-top: 5px;
  border-radius: 2px;
  background: var(--c-surface-sunken);
  overflow: hidden;
}
.dist__fill { display: block; height: 100%; background: var(--c-primary); opacity: .55; border-radius: 2px; }
/* 分布下面的**集中趋势**一行（返修轮次的中位数，CR-20260815-03）。
 * 与上面的桶不是同一种量：桶是分布，中位数是一个概括值。因此隔一道稍重的线
 * 并去掉分布条——混在桶里它会被读成「第五个桶」。
 * 复用 dist 的标签/数值排版，让它仍属于同一张卡的阅读体系。 */
.dist__aside {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: var(--sp-3);
  margin-top: var(--sp-2);
  padding-top: var(--sp-3);
  border-top: 2px solid var(--c-border);
}

/* --- 5b. 操作卡（CR-20260814-01 / §5）--------------------------------- */
/* 本轮点名要消除的那个形状：
 *
 *     快捷入口
 *     ─────────
 *     选刊助手
 *     AI 拟题
 *     邮箱同步状态
 *
 * 三条浏览器默认蓝色链接叠在一张白卡里。它的问题不是"不好看"，
 * 是**没有把自己表达成一个可操作的东西**：没有图标、没有说明、
 * 没有悬停面、命中区域只有那几个字宽，键盘焦点也只是一条下划线。
 *
 * 操作卡把整块面变成一个 `<a>`：图标托 + 标题 + 一行说明 + 右侧箭头。
 * 整块可点、整块有悬停态、整块有焦点环。里面**不再嵌任何第二个交互元素**
 * （§13 的嵌套交互约束），说明文字只是文字。
 */
.action-grid { display: grid; gap: var(--sp-2); }
.action-grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); }
.action-grid--3 { grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); }

.action-card {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface);
  color: var(--c-text);
  min-width: 0;
  transition: border-color var(--transition), background var(--transition), box-shadow var(--transition);
}
.action-card:hover {
  text-decoration: none; color: var(--c-text);
  border-color: var(--c-border-strong);
  background: var(--c-surface-alt);
  box-shadow: var(--shadow-xs);
}
.action-card:hover .action-card__tile { background: var(--c-primary-soft); color: var(--c-primary); border-color: var(--c-primary-border); }
.action-card:hover .action-card__chevron { color: var(--c-primary); transform: translateX(2px); }
.action-card:focus-visible { outline: 2px solid var(--c-primary); outline-offset: 2px; }
.action-card__tile {
  width: var(--tile); height: var(--tile); flex-shrink: 0;
  border-radius: 8px;
  display: grid; place-items: center;
  background: var(--c-surface-sunken);
  color: var(--c-text-secondary);
  border: 1px solid var(--c-border);
  transition: background var(--transition), color var(--transition), border-color var(--transition);
}
.action-card__tile .icon { width: 17px; height: 17px; }
/* 标题与说明各占一行。用 flex 列而不是让两个 <span> 自然内联——
   内联时它们会排成「选刊助手 按稿件方向筛选候选期刊」一整行，
   说明文字反而抢了标题的位置。 */
.action-card__body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.action-card__title { font-size: var(--fs-base); font-weight: 550; line-height: 1.4; }
.action-card__desc {
  font-size: var(--fs-xs); color: var(--c-text-muted); line-height: 1.5; margin-top: 1px;
}
.action-card__chevron {
  width: 16px; height: 16px; flex-shrink: 0;
  color: var(--c-border-strong);
  transition: color var(--transition), transform var(--transition);
}
/* 禁用态（有入口但当前用户不该用）不在这里做：没权限就不渲染，
   界面上不留一个点不动的死链接。 */

/* --- 6. 表格 ----------------------------------------------------------- */
/* PaperOps 是高信息密度的业务软件，表格必须留着，不能把每行改成大卡片。
 * 本轮做的是让它**看起来是被设计过的表格**而不是一张裸 <table>：
 *   · 表头弱化（更小、更淡、字距略开），让数据行成为视觉主体；
 *   · 单元格上下内边距略收，行高更稳；
 *   · 主对象列（`.table__primary`）是唯一加粗的东西，其余是次要信息；
 *   · 行悬停用极浅的中性面，不用蓝色——蓝色留给"可点击"，不表示"鼠标在这"。
 */
/* 常见服务商推荐配置（CR-20260816-07 §35–§36）。
 *
 * 硬约束：五项必须一次看全，卡片里**不得**出现任何内部滚动条。
 * 因此这里刻意不用 `.table-wrap`（它带 `overflow-x:auto`），
 * 也不设任何 `max-height`——卡片自然长高即可。
 * 长主机名靠 `overflow-wrap` 换行，而不是把内容推出去产生横向滚动。 */
.provider-presets { list-style: none; margin: 0; padding: 0; }
.provider-presets__item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--c-border-subtle, var(--c-border));
  min-width: 0;
}
.provider-presets__item:last-child { border-bottom: 0; }
.provider-presets__name { flex: 0 0 auto; font-weight: 550; font-size: var(--fs-sm); }
.provider-presets__conn {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  /* 长 host 换行，不横向溢出。 */
  overflow-wrap: anywhere;
  word-break: break-word;
  text-align: right;
}
.provider-presets__apply { flex: 0 0 auto; white-space: nowrap; }

.table-wrap { overflow-x: auto; }
.table { width: 100%; border-collapse: collapse; font-size: var(--fs-base); }
.table th {
  text-align: left;
  padding: var(--sp-2) var(--sp-4);
  background: var(--c-surface-alt);
  border-bottom: 1px solid var(--c-border);
  font-weight: 550;
  font-size: var(--fs-xs);
  letter-spacing: 0.03em;
  color: var(--c-text-muted);
  white-space: nowrap;
  position: sticky;
  top: 0;
  z-index: 1;
}
.table td {
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--c-border);
  /* 所有单元格**垂直居中**（CR-20260815-10 / 用户裁定「所有列的内容都应该垂直居中」）。
   *
   * 此前是 `top`，理由是「两行的单元格要和同排其它列的第一行对齐」。
   * 那个理由在表格普遍两三行高的时候成立；本轮把各列压成一行之后，
   * `top` 只剩下一个效果：偶尔出现的那个两行单元格，把同排所有内容
   * 都吊在行的上半截。行内容居中之后，行高不管是 47 还是 110，看起来都是齐的。 */
  vertical-align: middle;
}
/* 只装一个徽标的单元格**垂直居中**（CR-20260815-07 / 用户裁定）。
 *
 * 表格整体是 `vertical-align: top`，这对「标题 + 摘要」这种两行内容是对的：
 * 它们要跟同一行其它列的第一行对齐。但一个孤零零的徽标顶格贴在两行高的
 * 行里，看起来像是被挂在上边缘。徽标是这一格的**全部**内容，
 * 它的对齐基准应该是这一行，不是这一行的第一行文字。
 *
 * 用 `:has()` 而不是给每张表的 <td> 加类：全站十几张表都是这个形态，
 * 逐个加类必然漏。`:has()` 在本项目的目标浏览器上均已可用；
 * 万一不支持，退化成 `vertical-align: top`，即今天之前的样子——不会坏。 */
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr:hover { background: var(--c-hover); }
/* 紧凑表只压**上下**，左右与普通表保持一致（CR-20260815-12 / 用户裁定
   「最左侧和最右侧全部统一间距」）。此前左右也用 `--sp-3`，于是审计日志那张表
   的首末列比别的表窄 4px——同一个系统里两种边距。 */
.table--compact th, .table--compact td { padding: var(--sp-2) var(--sp-4); }
/* 七八列的业务列表给一个宽度下限（CR-20260814-01）。
 * 1024 下不设下限时，浏览器会把每一列压到 min-content：稿件标题折成五行、
 * 期刊折成三行，一屏八行没有两行等高——「有横向滚动」在这里反而是对的，
 * 因为 `.table-wrap` 已经把滚动关在自己这一块里，页面级仍然不滚。 */
.table--roomy { min-width: 860px; }
/* 六列的版本（CR-20260814-06）。860px 是按投稿列表那种八九列定的，用在
   期刊详情的「投稿记录」上会超出主栏约 46px——于是新加的「全程」列在 1440 上
   一进来就被 `.table-wrap` 剪掉，要横向滚一下才看得到。720px 同样挡得住
   375 下的 min-content 挤压，又能在带右栏的 1440 主栏里完整放下。 */
.table--roomy-sm { min-width: 720px; }
/* 数字列右对齐（CR-20260815-06 修表头）。
 *
 * ⚠️ **必须写成 `th.` / `td.` 限定符**。`.table th { text-align: left }` 的特异性是
 * (0,1,1)，光写 `.table__num`（0,1,0）压不过它——于是 `<th class="table__num">`
 * 照旧左对齐，而单元格右对齐：表头在左、数字在右，眼睛要横向找才能把标题和
 * 数字对上。全站 22 处数字列都是这个形状，验收时被点名。
 *
 * 同一个坑本仓库已经踩过一次，`th.user-actions-cell` 那两行注释记的就是它。
 *
 * **居中而不是右对齐**（用户裁定）。排版上右对齐更常见——它让个位数与两位数的
 * 末位落在同一条竖线上，扫一列时能直接比大小。但本系统的计数列都是 1–2 位数
 * （稿件数、投稿次数、候选数），量级差别一眼就看得出，不需要靠末位对齐；
 * 而右对齐会把数字推到列的右边缘、离表头更远。居中在这个数据规模下更整齐。 */
th.table__num, td.table__num { text-align: center; }
/* 列宽收到内容宽度：计数列的内容是 1–2 位数，给它 `table__col-name` 那种
   宽度会在数字和相邻列之间留下一大片空白（验收截图圈出的正是这个）。
   `width: 1%` + `nowrap` 是表格布局里「按内容收窄」的标准写法。 */
/* ⛔ 这里曾是 `width: 1%`（CR-20260815-06），已删除（CR-20260815-08 / 用户裁定）。
 * 计数列因此被压到内容宽度，富余全部流向相邻的文本列——客户页的「稿件」
 * 数字被挤到很右边，与「研究方向」之间空出一大片，列间距忽宽忽窄。
 * 让它和别的列一样参与宽度分配，数字在列内居中，列距就均匀了。 */
.table__num { font-variant-numeric: tabular-nums; white-space: nowrap; }
.table__nowrap { white-space: nowrap; }
/* 保留换行的自由文本（投稿要求摘要等）：录入时的分行是内容的一部分，
   折成一整段会把「1. …2. …」这类清单挤成一行。 */
.text-pre-wrap { white-space: pre-wrap; }
.table__primary { font-weight: 550; color: var(--c-text); }
/* 内部编号列：等宽、弱化、不换行。它是标识符，不是内容。 */
.table__code { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--c-text-secondary); white-space: nowrap; }
/* 次要文本列（客户、期刊、日期）统一弱化，避免整张表所有列同一个视觉权重。 */
.table__meta { font-size: var(--fs-sm); color: var(--c-text-secondary); }

/* 列宽下限（CR-20260813-02）。
 *
 * `table-layout: auto` 会把剩余宽度优先给内容最长的列，短列被压到
 * min-content——中文在任意两字之间都可以断行，于是「演示客户·李明」
 * 会断成「演示客 / 户·李明」，「员工一号」断成「员工一 / 号」。
 * 这在排版上合法，但把一个专有名词拆成两行看起来就是没做完。
 *
 * 解决办法不是禁止换行（那会把长客户名顶出表格），而是给这几类列一个
 * **下限宽度**：够放下常见长度的名字，超长仍然正常换行。
 * 下限用 em，跟随字号，不写死像素。
 *
 * 这些类加在 <th> 上即可——同列的 <td> 由表格布局算法跟随。
 */
/* ⚠️ 试过用百分比宽度做全站列宽再平衡（CR-20260815-07），**已回退**，别再试第二遍。
 *
 * 做法是给固定形状的列 `width: 1%`、给标题列 `width: 22%~26%`。两个发现：
 *  1. auto 布局里 `width: 1%` 不是「按内容收缩」，是「占表宽 1%」——富余仍按
 *     指定宽度成比例分配，「投稿编号」照样拿到 137px，和内部编号一模一样；
 *  2. 一旦给标题列指定百分比，同一张表里没写百分比的列（客户、发件人）
 *     会被压到 `min-width` 以下（实测客户列 74px→60px，人名折成两行），
 *     结果是行更高、更难看。
 * 真正有效的是**按内容选对列类**——见「投稿编号」改用 `table__col-shrink`。
 * 只留 `min-width` 让 auto 布局自己算，是目前最稳的形态。 */
/* 表格单元格**默认不折行**（CR-20260816-03 / 用户裁定，同一类问题第四次提出）。
 *
 * 前几轮是逐列加 `nowrap`：`table__col-center` 一批、`table__col-name` 一批……
 * 结果必然是「有些地方做了有些地方没做」——每加一列就得记得再标一次。
 * 与下拉搜索框那次是同一个教训：**判据要写进规则，不能靠逐处标注**。
 *
 * 规则反过来定：单元格默认一行，放不下收省略号；**确实需要多行的**用
 * `.table__wrap` 显式声明（目前只有邮件列表的「主题 / 摘要」与稿件的
 * 「当前投稿」——它们一格里本来就装着两三条独立信息）。
 *
 * 块级子元素（`.table__sub` 之类）不受影响：`nowrap` 管的是一行内的文字折行，
 * 两个 `<div>` 仍然各占一行。 */
.table th, .table td {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.table th.table__wrap, .table td.table__wrap {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}

/* ⛔ 截断只对**文字**成立（CR-20260816-06 / §21–§24）。
 *
 * 上面那条 `.table th, .table td` 是个**很宽的选择器**：它把
 * `white-space:nowrap; overflow:hidden; text-overflow:ellipsis` 加到**每一个**
 * 单元格上，包括「里面只有一个勾选框、一个字都没有」的那种。
 *
 * 邮件列表的全选表头就是这样一格：`<th><input type=checkbox></th>`。
 * 实测（1440 与 375 都一样）：clientWidth 44 / scrollWidth 60 —— 左右各 16px
 * 内边距 + 16px 勾选框 + 折叠后的空白文本节点，超出了这一列的 44px。
 * 于是 `text-overflow: ellipsis` 生效，Chrome 给**被裁掉的行内内容**画了一个
 * 省略号——用户看到的就是勾选框旁边那个莫名其妙的「...」。
 * （同列的 `<td>` 因为下面的规则拿到 `overflow: visible`，所以只有表头出现，
 *   这与用户截图里「只有最上面那一格有」完全吻合。）
 *
 * 判据回到 §23：省略号只能落在**真正的文本容器**上。只装控件的格子既没有文字
 * 可省略，也不该因为差几个像素就把控件本身裁掉——那不是截断，是把控件弄坏。
 *
 * 用 `:has()` 而不是给模板逐个加 class：这样**所有**表格（现在的和以后的）
 * 都自动受保护，不需要谁记得去标注。本文件已经在用 `:has()`
 * （`select:has(option[value=""]:checked)`），不是新引入的依赖。 */
.table th:has(> input[type="checkbox"]),
.table td:has(> input[type="checkbox"]),
.table th:has(> input[type="radio"]),
.table td:has(> input[type="radio"]) {
  overflow: visible;
  text-overflow: clip;
  white-space: nowrap;
  /* 顺带把这一列真正放得下勾选框。
   *
   * 原来的 44px 列宽里有 16+16 的左右内边距，内容区只剩 **12px**，
   * 而勾选框是 16px —— 也就是说这一格从一开始就装不下它自己的控件，
   * 这才是 scrollWidth(60) > clientWidth(44) 的来源。
   * 把内边距收到 --sp-3（12px）后内容区有 20px，勾选框有余量，
   * 列的视觉宽度仍是 44px，表头与数据行的对齐一个像素没动。 */
  width: 2.75rem;
  padding-left: var(--sp-3);
  padding-right: var(--sp-3);
}

/* 省略号必须落在**真正溢出的那个盒子**上（CR-20260816-03 / 用户裁定）。
 *
 * 邮件列表的主题那一格是 `<td><div class="table__primary"><a>…</a></div></td>`。
 * `overflow: hidden` 加在 `td` 上，被裁掉的却是里面那个 `div`——
 * `text-overflow` 只对**自己发生溢出的那个块**生效，于是文字被齐刷刷切断，
 * 一个省略号都没有，看起来像数据丢了半截。
 *
 * 因此内层的文字容器要一起带上。`white-space` 不在这里重设：它从 `td` 继承，
 * `.table__wrap` 一改，内外一起跟着改，不会出现「格子允许换行、里层还锁着一行」。 */
.table td > .table__primary,
.table td > .table__sub,
.table td > div,
.table td > a {
  overflow: hidden;
  text-overflow: ellipsis;
}
.table td.table__wrap > .table__primary,
.table td.table__wrap > .table__sub,
.table td.table__wrap > div,
.table td.table__wrap > a {
  overflow: visible;
  text-overflow: clip;
}

/* 固定布局里**清掉列的 min-width**（CR-20260816-03 / 用户裁定）。
 *
 * `table-layout: fixed` 只保证按百分比分配**富余**——列上的 `min-width`
 * 仍然是硬下限，`table__col-code` 9.5em + `col-time` 7em + `col-name` 5em×2……
 * 九列加起来超过容器，表格就顶着下限撑宽，最右边的「操作」列被推出可视区。
 * 这正是用户截图里那一条：右侧半列被切掉，且切口上没有省略号。
 *
 * 百分比预算已经替这些列各留了位置，下限在这里没有存在的必要。 */
.table--fixed th, .table--fixed td { min-width: 0; }

/* 装着表单控件的单元格**不裁切**（CR-20260816-03）。
 *
 * `overflow: hidden` 会连聚焦描边一起切掉——描边画在控件盒子外面，
 * 单元格一裁，键盘用户就看不出焦点在哪。作者表与权限表都是这种表。
 * 这些格子里本来也没有会溢出的长文本，裁切对它们没有意义。 */
.table td:has(input, select, textarea, .form-select) {
  overflow: visible;
}

/* 只装徽标或按钮的单元格**不画省略号**（CR-20260816-03）。
 *
 * 省略号的意思是「这里还有文字没显示出来」。一个徽标或一个按钮溢出一两个像素时
 * 画出来的「…」不表达任何信息——它后面没有被截断的文字，只有控件的内边距。
 * 实测长成「标记为已处理 …」，读起来像按钮名字还没写完。
 *
 * 这些列的正解是**给够宽度**（上面各表的预算已按按钮实宽给），
 * 这一条只负责兜住亚像素级的舍入误差。
 *
 * 选择器只认**操作列与直接装着表单的格子**。第一版写成 `:has(> .badge, …)`，
 * 结果把「刊名 + SCI 徽标」「同步状态 + 失败原因」这种**既有徽标又有长文本**的
 * 格子一并豁免了——那些格子的文字是真的被截断的，省略号必须留着。 */
.table td.table__actions,
.table td.table__col-badge,
.table td:has(> form) {
  text-overflow: clip;
}

/* 只装徽标的列（CR-20260816-03 / 用户裁定「有附件标签后为什么还要省略号」）。
 *
 * 徽标是一个**整块**，它没有「后面还有几个字没显示出来」这回事——
 * 溢出一两个像素时画出来的省略号纯属噪声，实测长成「有附件 …」。
 *
 * 三件事一起做，缺一件都还会漏出来：
 * 1. 不画省略号（上面那条选择器）；
 * 2. 内边距从 16px 收到 8px——一个 3 字徽标实测 54px，两侧 16px 的内边距
 *    等于要求这一列多给 32px，差不多是徽标本身的六成；
 * 3. 各表的列宽预算按**徽标实宽**给，不按字数猜。
 *
 * 判据是「这一格里只有徽标、没有自己的文字」。既有徽标又有长文本的格子
 * （刊名 + SCI、同步状态 + 失败原因）**不属于**这一类——那里的文字是真会被
 * 截断的，省略号必须留着。第一版写成 `:has(> .badge)` 就是栽在这一点上。 */
.table th.table__col-badge, .table td.table__col-badge {
  text-align: center;
  padding-left: var(--sp-2);
  padding-right: var(--sp-2);
}

/* 名称类列**不折行**（CR-20260816-03 / 用户裁定，第 N 次提到同一类问题）。
 *
 * 这一列装的是人名、账号、状态词——「管理员」被折成「管理」+「员」
 * 是这轮验收截图里最刺眼的一处。短词折行没有任何收益：它不省空间
 * （行高翻倍抵消了宽度节省），只让人多读一次。
 *
 * 放不下时收省略号而不是折行，与标题类列同一条规则；完整值进 `title=`。 */
.table__col-name    { min-width: 5em; }    /* 客户名、负责人、发起人 */
.table th.table__col-name, .table td.table__col-name,
.table__code, .table__num {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.table__col-code    { min-width: 9.5em; white-space: nowrap; }   /* C000001-M01-S01 与外部投稿编号 */
.table__col-status  { min-width: 5em; white-space: nowrap; }    /* 状态徽标 */
.table__col-time    { min-width: 7em; white-space: nowrap; }     /* Y-m-d H:i 与相对时长 */
/* ⛔ 试过让标题列 `width: 100%` 吸走全部富余，好让其余列各自贴合内容、
 *    列距只剩内边距（CR-20260815-08）。**已回退**——它把不均匀集中到了一处：
 *    内容间距极差从 103px 涨到 364px（标题列很宽、内容很短，那一格空出一大片）。
 *
 * 表格是 `width: 100%`，富余总得落到某处；落在哪儿，哪儿的间距就变大。
 * 把富余**摊给所有列**（只写 `min-width`、宽度交给 auto 布局）反而让最大间距最小，
 * 是这几种做法里最均匀的。真正等距要求表宽等于内容宽，那样卡片右侧会空一条。 */
.table__col-title   { min-width: 9em; }    /* 稿件标题、邮件主题、期刊名 */
/* 表格**最后一列**贴合内容（CR-20260815-11 / 用户裁定「不得留过多留白空隙」）。
 *
 * auto 布局把富余按内容需求摊给所有列，末列因此也分到一份——但它右边没有
 * 下一列来「用掉」这份宽度，那份富余就变成表格右缘一条空白（客户页实测：
 * 「客户关系状态」列 166px，里面的徽标只有约 60px）。中间列的富余看不出来，
 * 末列的富余是直接看得见的一条白。
 *
 * 只排除标题类列：末列如果是标题（少数详情页内嵌小表是这样），它本来就该吃宽度。
 * 名称类列（负责人 / 发起人）曾一并排除，结果稿件、邮箱账户、后台任务三张表的
 * 右内缩是 34–44px 而别的表是 16px。它们的 `min-width: 5em` 已经兜住下限，
 * 不需要再额外留一份富余。
 * 用 `th` 上的类判断即可——同列的 `td` 由表格布局算法跟随。 */
/* ⚠️ 不适用于 `table--fixed`：那些表的列宽是逐列量过、加起来正好 100% 的预算，
 * 这条 (0,3,2) 的规则会把末列压回 1%，预算就不成立了（本轮实测：投稿任务的
 * 「操作」列被压到 13px，按钮溢出 110px）。固定布局表自己说了算。 */
.table:not(.table--fixed) thead th:last-child:not(.table__col-title) { width: 1%; }

/* 主要用于「操作」列：内容宽度即列宽，不参与剩余宽度分配。 */
.table__col-shrink  { width: 1%; white-space: nowrap; }
/* 内容很短的列**居中**（CR-20260815-12 / 用户裁定）。
 *
 * 判据是「这一列装的是不是一小块东西」——徽标、计数、两三个字的分类词。
 * 这类内容左对齐时，列宽一旦大于内容（表头比内容长时必然如此），
 * 内容就贴在左边、右边空一条，一列列排下来像被推歪了。居中让它待在列的正中。
 *
 * 长文本列**不适用**：标题、单位、邮件主题居中会让每一行的起始位置都不同，
 * 眼睛沿列往下扫时找不到一条对齐线，比留白难读得多。
 *
 * ⚠️ 必须**同时**加在 `<th>` 与 `<td>` 上。CSS 没有「列」选择器，
 *    只加一边会得到「表头居中、内容左对齐」这种更难看的结果。
 *    加不上类的场合（徽标单元格）由下面那条 `:has()` 兜底，
 *    但它兜不到表头——所以能加类的地方一律加类。 */
.table th.table__col-center, .table td.table__col-center {
  text-align: center;
  /* 居中列按定义装的都是短内容，一律不换行。
     居中之后列宽会收得更紧（`width: 1%` 的末列尤其），
     「未指派」这种三字词会被折成两行——用户验收点名。 */
  white-space: nowrap;
}
/* ⛔ 这里曾有一条 `:has(> .badge:only-child)` 的兜底规则，**已删除**。
 * 它只能命中 `<td>`，`<th>` 跟不过来——结构走查立刻报出 4 个页面
 * 「表头左对齐、单元格居中」。差一半的对齐比不对齐更显眼。
 * 结论：这件事没有纯 CSS 的做法，必须在模板里成对加类，`density_check.py`
 * 的表头/单元格对齐一致性检查就是守这一条的。 */

/* 固定列宽（CR-20260815-22 / 用户裁定「在确保整个视图能完整显示的前提下，
 * 尽可能塞下所有文字，实在塞不下才省略号」）。
 *
 * ## 为什么必须是 `table-layout: fixed`
 * auto 布局下 `white-space: nowrap` 的单元格会把列撑到**全文宽度**：
 * 投稿任务表实测 1633px 塞在 1166px 容器里，横向溢出 467px。
 * 溢出各由一列造成——「目标期刊」需要 447px、期刊库的「期刊」需要 638px，
 * 都是被一两个超长刊名拉出来的。
 *
 * 试过在单元格上加 `max-width`（22% 与 0 都试了）：**两种都无效**。
 * CSS 2.1 规定表格单元格的 width 类属性只是建议值，`max-width` 在 auto 布局里
 * 被直接忽略（实测加了之后 1633px 一个像素没变）。
 * `table-layout: fixed` 是唯一能让列真正收缩的机制。
 *
 * ## 预算怎么定的
 * 逐列量过「内容实际需要多宽」（scripts 见验证记录），再按需求分配，
 * 合计正好 100%。给得够的列**一个字都不会省**；只有被超长值撑爆的那一列
 * 会收省略号——这正是「尽可能塞下，实在塞不下才省略」。
 *
 * ## 为什么按表写死而不是给通用类
 * 每张表的列数与内容分布都不同，一套通用百分比必然在别的表上出错——
 * 本轮先试过通用值，结果稿件页与期刊库反而各溢出 56–77px。
 * 固定布局的宽度预算**是每张表自己的事**，写在一起才看得出加起来是不是 100%。 */
.table--fixed { table-layout: fixed; }

/* 投稿任务：9 列。溢出源是「目标期刊」（内容需 33%，给 20% 后收省略号）。 */
/* 合计 100%。定宽列（编号、状态、时间、操作）按实测**恰好够用**给，
   富余全部留给三个省略号列——它们才是「尽可能塞下文字」的受益方。 */
.table--fixed-submissions th:nth-child(1) { width: 13%; }  /* 任务编号：C000025-M05-S01，15 个等宽字符 */
.table--fixed-submissions th:nth-child(2) { width: 13%; }  /* 稿件（省略号列） */
.table--fixed-submissions th:nth-child(3) { width: 9%; }   /* 客户（省略号列） */
.table--fixed-submissions th:nth-child(4) { width: 11%; }  /* 目标期刊（省略号列） */
.table--fixed-submissions th:nth-child(5) { width: 11.5%; } /* 状态：5 字徽标实测 78px */
.table--fixed-submissions th:nth-child(6) { width: 10.5%; } /* 投稿编号：DJAO-D-26-00311 */
.table--fixed-submissions th:nth-child(7) { width: 11%; }  /* 负责人：「演示员工（只读）」 */
.table--fixed-submissions th:nth-child(8) { width: 10%; }  /* 当前状态持续：「129 天 20 小时」 */
.table--fixed-submissions th:nth-child(9) { width: 11%; }  /* 操作：按钮实测 111px */

/* 邮件列表：9 列。**定长列给 px，可伸缩列给百分比**（CR-20260816-03）。
 *
 * 纯百分比预算在这张表上不成立：实测九列的需求合计 1520px，而 1280 视口下
 * 只有 1006px。差额必须从某处让出来——问题是百分比**同时**压缩了
 * 「能让」的列和「不能让」的列：收件时间被截成「2026-07-20 02…」，
 * 分钟数没了；而这是个定长字段，截断它是在丢信息，不是在省地方。
 *
 * 因此按列的性质分两类：
 * - **定长**（勾选框、时间戳、两个徽标、按钮）：给 px，宽度就是实测需求；
 * - **可伸缩**（主题、发件人、来源邮箱）：给百分比，放不下收省略号。
 *
 * 好处是宽屏下的富余**全部流向可伸缩列**：1680 视口下主题从 189px 涨到 389px，
 * 而收件时间始终是它需要的 142px——纯百分比方案会把富余平摊给时间列，
 * 让它拿到 204px 去显示一个只要 142px 的值。 */
/* 拟题记录（CR-20260817-02）。改动前实测：表格 3806px / 容器 1092px，
 * 「原始输入」独占 3161px——它的副行是研究对象，没有宽度约束就一路撑开，
 * 整张表必须横向拖动才看得全。
 *
 * 定宽列按实测**恰好够用**给，富余全部留给「原始输入」这一个省略号列——
 * 它才是「尽可能塞下文字」的受益方。省略号机制本身已经在
 * `.table td > .table__primary / .table__sub` 上（CR-20260816-03），
 * 这里补的是它唯一缺的那一半：**一个不会无限长大的宽度**。
 *
 * ⚠️ 两套规则，因为列数随角色变化：Token 与轮次只对管理员渲染（ADR-057）。
 * 共用一套 `nth-child` 会让员工侧的「发起人」拿到管理员侧「轮次」的宽度。 */

/* 列宽是**量出来的**，不是估的。第一版用 em 给，结果每一列都比内容窄
 * （等级要 109px 只给了 78、发起人要 136px 只给了 78、创建时间要 142px 只给了 114），
 * 于是表头自己被截成「候…」「轮…」「已完成 …」。下面的像素值取自
 * 逐列 `scrollWidth` 实测（1440 视口，含表头与全部数据行的最大值）再留 4–6px 余量。
 *
 * 只有「原始输入」是省略号列：它吃掉全部富余，其余列一律**完整显示**。 */

/* 管理员：8 列。定宽合计 694px，1280 视口下仍给「原始输入」留约 312px。 */
.table--fixed-topics-admin th:nth-child(1) { width: auto; }   /* 原始输入（唯一的省略号列） */
.table--fixed-topics-admin th:nth-child(2) { width: 112px; }  /* 等级：「科技核心」徽标实测 109 */
.table--fixed-topics-admin th:nth-child(3) { width: 100px; }  /* 状态：「已完成」徽标实测 97 */
.table--fixed-topics-admin th:nth-child(4) { width: 60px; }   /* 候选：表头「候选」实测 57 */
.table--fixed-topics-admin th:nth-child(5) { width: 76px; }   /* Token：表头实测 72 */
.table--fixed-topics-admin th:nth-child(6) { width: 60px; }   /* 轮次：表头实测 57 */
.table--fixed-topics-admin th:nth-child(7) { width: 140px; }  /* 发起人：实测 136 */
.table--fixed-topics-admin th:nth-child(8) { width: 146px; }  /* 创建时间：Y-m-d H:i 实测 142 */

/* 员工：6 列（无 Token / 轮次）。定宽合计 558px。 */
.table--fixed-topics th:nth-child(1) { width: auto; }
.table--fixed-topics th:nth-child(2) { width: 112px; }
.table--fixed-topics th:nth-child(3) { width: 100px; }
.table--fixed-topics th:nth-child(4) { width: 60px; }
.table--fixed-topics th:nth-child(5) { width: 140px; }
.table--fixed-topics th:nth-child(6) { width: 146px; }

/* 省略号**只给第一列**。其余列已按实测宽度给足，不该出现截断——
 * 把 nowrap 加到整表会让徽标列与计数列也变成「已完成 …」那种样子，
 * 而那正是用户指出来的问题。 */
.table--fixed-topics td:first-child .table__sub,
.table--fixed-topics-admin td:first-child .table__sub,
.table--fixed-topics td:first-child .table__primary,
.table--fixed-topics-admin td:first-child .table__primary {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 列宽按 `data-col` 指定，**不按 nth-child**（CR-20260819-08，同
 * `.table--fixed-review` 已定的模式，CR-20260816-07 §42）。
 *
 * 勾选列（select）与操作列（actions）都是按 `module_perms.mail.write` 条件
 * 渲染的。用 `th:nth-child(N)` 定宽时，没有写权限的人看到的每一列都会往左
 * 错一位：主题/摘要拿到本该属于勾选框的 44px 宽度，最左侧于是出现一条又窄
 * 又空的列——用户报的「最左侧明显空白列」就是这个，只是这次出现在「邮件
 * 列表」而不是「待办邮件」。按名字定宽之后，少一列不会让其余各列错位。 */
.table--fixed-maillist th[data-col="select"] { width: 44px; }      /* 勾选框 */
.table--fixed-maillist th[data-col="subject"] { width: 50%; }      /* 主题 / 摘要（可伸缩） */
.table--fixed-maillist th[data-col="sender"] { width: 26%; }       /* 发件人（可伸缩） */
.table--fixed-maillist th[data-col="source"] { width: 24%; }       /* 来源邮箱 / 原文件夹（可伸缩） */
.table--fixed-maillist th[data-col="time"] { width: 142px; }       /* 收件时间：Y-m-d H:i 实测 */
.table--fixed-maillist th[data-col="attachment"] { width: 86px; }  /* 附件：3 字徽标 54 + 内边距 */
.table--fixed-maillist th[data-col="status"] { width: 86px; }      /* 处理状态：同上 */
.table--fixed-maillist th[data-col="submission"] { width: 122px; } /* 匹配的投稿任务：5 字徽标 78 + 内边距 */
.table--fixed-maillist th[data-col="actions"] { width: 150px; }    /* 操作：按钮实测 130 + 内边距 */

/* 后台任务：7 列。溢出源是「任务」那一列的长名字。 */
.table--fixed-tasks th:nth-child(1) { width: 30%; }   /* 任务 */
.table--fixed-tasks th:nth-child(2) { width: 11%; }   /* 队列 */
.table--fixed-tasks th:nth-child(3) { width: 13%; }   /* 状态 */
.table--fixed-tasks th:nth-child(4) { width: 9%; }    /* 耗时 */
.table--fixed-tasks th:nth-child(5) { width: 11%; }   /* 执行次数 */
.table--fixed-tasks th:nth-child(6) { width: 15%; }   /* 创建时间 */
.table--fixed-tasks th:nth-child(7) { width: 11%; }   /* 发起人 */

/* 稿件列表：7 列。 */
.table--fixed-manuscripts th:nth-child(1) { width: 11%; }   /* 编号 */
.table--fixed-manuscripts th:nth-child(2) { width: 27%; }   /* 标题 */
.table--fixed-manuscripts th:nth-child(3) { width: 14%; }   /* 客户 */
.table--fixed-manuscripts th:nth-child(4) { width: 11%; }   /* 业务状态 */
.table--fixed-manuscripts th:nth-child(5) { width: 16%; }   /* 当前投稿 */
.table--fixed-manuscripts th:nth-child(6) { width: 9%; }    /* 投稿次数 */
.table--fixed-manuscripts th:nth-child(7) { width: 12%; }   /* 负责人 */

/* 稿件详情页的「投稿任务」卡：6 列，容器只有半幅宽。 */
.table--fixed-mssubs th:nth-child(1) { width: 19%; }   /* 任务编号 */
.table--fixed-mssubs th:nth-child(2) { width: 27%; }   /* 期刊 */
.table--fixed-mssubs th:nth-child(3) { width: 16%; }   /* 投稿编号 */
.table--fixed-mssubs th:nth-child(4) { width: 13%; }   /* 投稿日期 */
.table--fixed-mssubs th:nth-child(5) { width: 14%; }   /* 状态 */
.table--fixed-mssubs th:nth-child(6) { width: 11%; }   /* 负责人 */

/* 稿件详情页的「作者」表：5 列。 */
.table--fixed-authors th:nth-child(1) { width: 9%; }    /* 顺序 */
.table--fixed-authors th:nth-child(2) { width: 20%; }   /* 姓名 */
.table--fixed-authors th:nth-child(3) { width: 33%; }   /* 单位 */
.table--fixed-authors th:nth-child(4) { width: 16%; }   /* 署名角色 */
.table--fixed-authors th:nth-child(5) { width: 22%; }   /* 备注 */

/* 待确认邮件：7 列。 */
/* 待确认队列：列宽按**列名**指定，不按位置（CR-20260816-07 §42）。
 *
 * 勾选列只对有批量确认权限的人渲染。用 `nth-child` 定宽时，没有该权限的人
 * 看到的每一列都往左错一位——「邮件」拿到本该属于勾选框的 4%，
 * 最左侧于是出现一条又窄又空的列。那不是多了一列，是所有列都错位了。
 *
 * 用 `data-col` 之后，少一列不影响其余各列。剩余 4% 由表格按比例分摊。 */
.table--fixed-review th[data-col="select"] { width: 4%; }
.table--fixed-review th[data-col="mail"] { width: 26%; }
.table--fixed-review th[data-col="object"] { width: 20%; }
.table--fixed-review th[data-col="status"] { width: 12%; }
.table--fixed-review th[data-col="confidence"] { width: 9%; }
.table--fixed-review th[data-col="basis"] { width: 17%; }
.table--fixed-review th[data-col="time"] { width: 12%; }

/* 邮箱账户：8 列。 */
.table--fixed-mailboxes th:nth-child(1) { width: 15%; }   /* 名称 */
.table--fixed-mailboxes th:nth-child(2) { width: 17%; }   /* 邮箱地址 */
.table--fixed-mailboxes th:nth-child(3) { width: 10%; }   /* 服务商 */
.table--fixed-mailboxes th:nth-child(4) { width: 14%; }   /* 同步状态：徽标 + 失败原因副行 */
.table--fixed-mailboxes th:nth-child(5) { width: 11%; }   /* 同步文件夹 */
.table--fixed-mailboxes th:nth-child(6) { width: 14%; }   /* 最近成功同步 */
.table--fixed-mailboxes th:nth-child(7) { width: 9%; }    /* 累计邮件 */
.table--fixed-mailboxes th:nth-child(8) { width: 10%; }   /* 负责人 */

/* 审计日志：7 列。 */
.table--fixed-audit th:nth-child(1) { width: 14%; }   /* 时间 */
.table--fixed-audit th:nth-child(2) { width: 12%; }   /* 操作者 */
.table--fixed-audit th:nth-child(3) { width: 15%; }   /* 动作 */
.table--fixed-audit th:nth-child(4) { width: 29%; }   /* 摘要 */
.table--fixed-audit th:nth-child(5) { width: 14%; }   /* 实体 */
.table--fixed-audit th:nth-child(6) { width: 8%; }    /* 级别 */
.table--fixed-audit th:nth-child(7) { width: 8%; }    /* 结果 */

/* 仪表盘的三张小表：各 4 列，容器只有三分之一屏宽。 */
.table--fixed-mini th:nth-child(1) { width: 40%; }
.table--fixed-mini th:nth-child(2) { width: 22%; }
.table--fixed-mini th:nth-child(3) { width: 20%; }
.table--fixed-mini th:nth-child(4) { width: 18%; }

/* 仪表盘「最近后台任务」：两列，且**没有 `<thead>`**（CR-20260816-06 / §26 §27）。
 *
 * 因此这里必须写 `td`，不能写 `th`——上面那些 `th:nth-child()` 规则在这张表上
 * 一条都匹配不上，`table-layout: fixed` 于是把两列等分成 50/50：
 * 一个只装「成功 / 失败」短徽标的列白拿一半宽度，任务名被挤到提前截断，
 * 行右侧却是大片空白。
 *
 * 现在文本列 `width: auto` 吃掉剩余空间（固定布局里这就是「minmax(0,1fr)」
 * 的等价写法），状态列按徽标实际需要定宽。状态列仍是 `text-right`，
 * 定宽保证它逐行对齐，不会因为「已完成 / 排队中」字数不同而左右跳动。 */
.table--fixed-tasks td:nth-child(1) { width: auto; }
.table--fixed-tasks td:nth-child(2) { width: 5.5rem; }
/* 省略号要落在**真正溢出的那个盒子**上。这一格是
 * `<td><a><div class="text-sm">…</div><div class="table__sub">…</div></a></td>`，
 * 两个 div 是 `<a>` 的子元素、不是 `<td>` 的直接子元素，所以上面那条
 * `.table td > div` 够不着它们——不补这条，文字只会被硬切，一个省略号都没有。
 * 判据仍是像素：放得下就整条显示，`scrollWidth > clientWidth` 时才出现省略号。 */
.table--fixed-tasks td > a > div {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 邮件「已自动应用」：5 列。 */
/* 无法匹配队列：6 列（CR-20260816-07 §40，与另外两个标签页统一为定宽表）。 */
.table--fixed-mailunmatched th[data-col="mail"] { width: 32%; }
.table--fixed-mailunmatched th[data-col="mailbox"] { width: 18%; }
.table--fixed-mailunmatched th[data-col="reason"] { width: 18%; }
.table--fixed-mailunmatched th[data-col="received"] { width: 14%; }
.table--fixed-mailunmatched th[data-col="waited"] { width: 9%; }
.table--fixed-mailunmatched th[data-col="state"] { width: 9%; }

.table--fixed-mailauto th:nth-child(1) { width: 30%; }  /* 主题 */
.table--fixed-mailauto th:nth-child(2) { width: 18%; }  /* 发件人 */
.table--fixed-mailauto th:nth-child(3) { width: 17%; }  /* 来源邮箱 */
.table--fixed-mailauto th:nth-child(4) { width: 15%; }  /* 收件时间 */
.table--fixed-mailauto th:nth-child(5) { width: 20%; }  /* 匹配的投稿任务 */

/* 期刊库：9 列。溢出源是「期刊」（内容需 52%，给 30% 后收省略号）。 */
.table--fixed-journals th:nth-child(1) { width: 29%; }  /* 期刊（省略号列） */
.table--fixed-journals th:nth-child(2) { width: 9%; }   /* ISSN */
.table--fixed-journals th:nth-child(3) { width: 14%; }  /* 出版社（省略号列） */
.table--fixed-journals th:nth-child(4) { width: 15%; }  /* 投稿系统 */
.table--fixed-journals th:nth-child(5),
.table--fixed-journals th:nth-child(6),
.table--fixed-journals th:nth-child(7),
.table--fixed-journals th:nth-child(8) { width: 5%; }   /* 四个计数列 */
.table--fixed-journals th:nth-child(9) { width: 13%; }  /* 最近投稿：Y-m-d 需 91px 富余 */

/* 单元格**强制一行 + 省略号**（CR-20260815-07 / 用户裁定「都需要改成一行的高度」）。
 *
 * 为什么不是调列宽：投稿任务表有 9 列，1440px 下每列约 130px。
 * 「含高比例新能源电网的示例调度策略」这类标题需要 ~240px，
 * 无论怎么分配都会折行——列宽能解决的是「本来放得下却折了」，
 * 解决不了「本来就放不下」。行高要恒定，就只能截断。
 *
 * 完整内容进 `title=`，鼠标悬停可读；点进详情页永远是全文。
 * `min-width: 0` 是必需的：表格单元格的默认 min-width 是 auto，
 * 不清零的话 `text-overflow` 不会触发，单元格会把整张表撑宽。 */
.table__ellipsis {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* ⚠️ 两条都别再试（各踩过一次）：
 *  1. **不要加 `max-width: 0`**。它在 auto 布局里把整列压到 min-width——
 *     实测投稿任务表的「客户」只剩「演示...」四个字符，比折行还难用。
 *  2. **不要在模板里用 `truncatechars` 兜底**。那是按**字符数**截，不看实际
 *     放不放得下：期刊库列表右边明明空着一大片，「Cambridge Uni...」却已经
 *     截断了（用户验收点名）。
 *
 * 正确的形态是**只靠这条 CSS**：表格是 `width: 100%`，各列按内容分配；
 * 内容总宽超过表宽时浏览器才压缩列，这些格子于是收省略号。
 * 也就是说——**放得下就全显示，放不下才省略**，判据是像素不是字数。
 * 完整值仍在 `title=` 里，点进详情页永远是全文。 */
/* 列表主对象的真链接（CR-20260813-01）。
   在此之前，客户/稿件/投稿/邮箱/拟题记录等列表的行只有 `tr[data-href]` + JS 点击：
   键盘用户进不了任何详情页，鼠标用户也无法中键或 Ctrl+点击开新标签页，
   浏览器状态栏更不会显示目标地址。现在每一行的主对象都是一个真正的 <a href>，
   整行点击退回为**渐进增强**（`paperops.js` 的 row-click 会跳过 a/button/input 等，
   所以操作列里的按钮不会误触发导航）。
   视觉保持原样：继承单元格颜色、不加下划线，只在悬停/聚焦时给出反馈。 */
.row-link { color: inherit; text-decoration: none; }
.row-link:hover { text-decoration: underline; }
.row-link:focus-visible {
  outline: 2px solid var(--c-primary);
  outline-offset: 2px;
  border-radius: 2px;
}
.table__sub { font-size: var(--fs-xs); color: var(--c-text-muted); margin-top: 2px; }
/* 操作列同理：不加 `th.` 限定符，表头会留在左边而按钮靠右（同上一段的原因）。 */
/* 「操作」列**居中**（CR-20260816-03 / 用户裁定）。
 *
 * 此前是右对齐——那是为**贴着表格右边缘的窄列**定的：列宽 1%，
 * 按钮紧靠右边框，右对齐正好。现在各表都走固定列宽预算，
 * 「操作」是一列有固定份额的普通列，右对齐会把按钮甩到列的边上，
 * 表头与按钮各偏一边（用户截图里就是这样）。
 *
 * 顺带把用户管理页那条 `.user-actions-cell` 的特例并了回来：
 * 它当初正是为了在这一列做居中而单开的局部类。 */
th.table__actions, td.table__actions { text-align: center; }
.table__actions { white-space: nowrap; }
/* 操作列的左右内边距收窄：这一格里只有按钮，按钮自己带内边距，
 * 单元格再补一份就等于让按钮凭空多占 28px——在 1280 宽下正是这 28px
 * 把「标记为已处理」挤断的。 */
.table td.table__actions { padding-left: var(--sp-2); padding-right: var(--sp-2); }
/* 用户管理「操作」列严格居中（CR-20260722-06 §十一）：单元 text-align:center 使表头
   与内容居中；按钮组 width:100% 铺满单元后再用 Flex 居中，保证单/双按钮的中心都与
   列中心对齐（偏差 ≤8px）。局部语义类，不动共享的 .table__actions（右对齐）。 */
/* 用 th./td. 限定符把特异性提到 (0,1,1)，压过 `.table th{text-align:left}`，使表头也居中。 */
th.user-actions-cell, td.user-actions-cell { text-align: center; }
.user-actions-group {
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--sp-2);
  /* `flex-wrap: wrap` 改成 `nowrap`（CR-20260815-08 / 用户裁定
   * 「最后一栏那个按钮不要占两行」）：两个 `btn--sm` 换行会让整行高一倍，
   * 而这一列的内容是固定的两个短按钮，本来就不需要换行能力。
   * 列宽由 `table__col-shrink` 那类规则保证够放——放不下时宁可整表横向滚动，
   * 也不要让**每一行**都为最宽的那一行付出双倍高度。 */
  flex-wrap: nowrap;
  white-space: nowrap;
}

/* 详情页的「标签 : 值」清单——全站每个详情页的主要信息载体。
 *
 * 列宽此前是 `max-content 1fr`，两处都有问题：
 *   · `max-content` 无上限，一个长标签（如「四档规则数（PKU/CSTPCD/SCI/EI）」）
 *     会把标签列撑到容器的一大半，值列只剩百来像素；
 *   · `1fr` 展开是 `minmax(auto, 1fr)`，那个 auto 下限等于内容的 min-content，
 *     于是一串 64 位 SHA 会把整个网格顶得比容器还宽——中文核心资产页在 375
 *     下 scrollWidth 415 > 375 就是这么来的（本轮实测）。
 *
 * `fit-content(34%)` 给标签列一个上限：短标签照常收缩，长标签最多占 34%。
 * 值列用 `minmax(0, 1fr)` 明确允许收缩到内容宽度以下，配合 dd 的换行规则，
 * 长哈希在列内折行而不是撑破页面。
 */
/* --- 6b. 分组行与批量操作条（CR-20260813-02 / ADR-095）----------------- */
/* 待确认队列里「同投稿 + 同结论」的一组邮件，用一条浅色分隔行标出边界。
   不是又一种卡片，也不做折叠——这一页的重点是逐条核对，
   折叠会让人在没看内容的情况下整组确认。 */
.group-row td {
  background: var(--c-surface-sunken);
  padding: var(--sp-2) var(--sp-4);
  border-bottom: 1px solid var(--c-border);
}
.table tbody tr.group-row:hover td { background: var(--c-surface-sunken); }
.group-row__body { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
/* 表格里的勾选框：整格可点，命中区域不止 16px 见方。 */
.row-check { display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 2px; }
.row-check input[type="checkbox"] {
  width: 16px; height: 16px; min-height: 0; padding: 0; margin: 0;
  accent-color: var(--c-primary); cursor: pointer;
}

/* 批量操作条。贴在视口底部而不是页面底部：长队列翻到一半时它仍在手边。
   没有勾选任何一项时整条隐藏（`.hidden`），不留一条「已选 0 项」的空条。 */
.bulk-bar {
  position: sticky; bottom: var(--sp-4);
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin-top: var(--sp-4);
  padding: var(--sp-3) var(--sp-5);
  background: var(--c-surface);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  z-index: 20;
}
.bulk-bar__count { font-size: var(--fs-base); font-weight: 550; }
.bulk-bar__hint { min-width: 0; }
@media (max-width: 640px) {
  .bulk-bar { flex-direction: column; align-items: stretch; }
  .bulk-bar .btn { width: 100%; }
}

.definition-list {
  display: grid;
  grid-template-columns: fit-content(34%) minmax(0, 1fr);
  gap: var(--sp-3) var(--sp-5);
}
.definition-list dt { color: var(--c-text-muted); font-size: var(--fs-sm); }
.definition-list dd { margin: 0; min-width: 0; word-break: break-word; overflow-wrap: anywhere; }
/* 详情页右栏的信息清单：行与行之间一条极浅的分隔，逐行扫的时候
   眼睛有落点。不加边框、不做成表格——它是元信息，不是数据。 */
/* 列间距改用 dt 的右内边距，而不是 grid 的 column-gap：
   分隔线画在 dt 与 dd 各自的上边框上，中间一留 gap，线就断成两截，
   看起来像没画完。间距换成内边距之后两段边框首尾相接，是一条连续的线。
   （`gap: 0` 也不行——那会让标签和值贴成「研究方向材料科学」。） */
.definition-list--rows { row-gap: 0; column-gap: 0; }
.definition-list--rows > dt,
.definition-list--rows > dd {
  padding: 7px 0;
  border-top: 1px solid var(--c-border);
}
.definition-list--rows > dt { padding-right: var(--sp-5); }
.definition-list--rows > dt:first-of-type,
.definition-list--rows > dt:first-of-type + dd { border-top: 0; padding-top: 0; }
.definition-list--rows > dd:last-child { padding-bottom: 0; }

/* --- 6c. 详情页分区（CR-20260814-01 / §6）----------------------------- */
/* 「不要把所有东西都做成卡片」的落地形态。
 *
 * 详情页此前是一列白卡片，每张卡片都有边框、圆角、阴影、标题和横线——
 * 一屏五个同样的白框套在同一个白框感的页面里，section 之间的层级为零。
 *
 * `.detail-section` 是**平的**：一个小标题 + 内容，靠一条上边线与间距
 * 与前一段分开。真正需要边界的内容（表格、可编辑区、高危区）才继续用卡片。
 */
.detail-section + .detail-section {
  margin-top: var(--sp-6);
  padding-top: var(--sp-6);
  border-top: 1px solid var(--c-border);
}
.detail-section__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); flex-wrap: wrap;
  margin-bottom: var(--sp-3);
}
.detail-section__title {
  font-size: var(--fs-base); font-weight: 600;
  display: flex; align-items: center; gap: var(--sp-2); min-width: 0;
}
.detail-section__desc { font-size: var(--fs-xs); color: var(--c-text-muted); flex: 1 1 100%; margin-top: -2px; }
/* 窄屏折成单列：两列在 ~280px 的内容区里谁都放不下。
   标签变小号弱化文字压在值上方，读起来仍是「标签 → 值」。 */
@media (max-width: 640px) {
  .definition-list { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .definition-list dt { font-size: var(--fs-xs); color: var(--c-text-muted); margin-top: var(--sp-3); }
  .definition-list dt:first-child { margin-top: 0; }
  .definition-list dd { margin-top: 2px; }
}

/* --- 7. 徽章与状态 ----------------------------------------------------- */
.badge {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 8px;
  border-radius: 20px;
  font-size: var(--fs-xs);
  font-weight: 550;
  line-height: 18px;
  white-space: nowrap;
  border: 1px solid transparent;
}
.badge--neutral { background: var(--c-neutral-soft); color: var(--c-text-secondary); border-color: var(--c-neutral-border); }
.badge--primary { background: var(--c-primary-soft); color: var(--c-primary); border-color: var(--c-primary-border); }
.badge--success { background: var(--c-success-soft); color: var(--c-success); border-color: var(--c-success-border); }
.badge--warning { background: var(--c-warning-soft); color: var(--c-warning); border-color: var(--c-warning-border); }
.badge--danger  { background: var(--c-danger-soft);  color: var(--c-danger);  border-color: var(--c-danger-border); }
.badge--info    { background: var(--c-info-soft);    color: var(--c-info);    border-color: var(--c-info-border); }
/* 小号徽章，与 .btn--sm 同一档字号。表格与详情页里徽章挨着正文时用，
   默认尺寸会把行高顶高一截。 */
.badge--sm { padding: 0 6px; font-size: 11px; line-height: 16px; }

.dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
.dot--success { background: var(--c-success); }
.dot--warning { background: var(--c-warning); }
.dot--danger { background: var(--c-danger); }
.dot--neutral { background: var(--c-text-muted); }
.dot--primary { background: var(--c-primary); }
/* `status_badge_class` 会返回 info（已投稿、修回已提交、执行中……），
   而这里此前没有对应的圆点，于是客户详情的「已接单」独独没有点。 */
.dot--info { background: var(--c-info); }

/* --- 8. 按钮 ----------------------------------------------------------- */
/* 五档，用途不重叠（§4）：
 *   .btn--primary      这一页的目的，每页至多一个
 *   .btn               会改数据的次要动作
 *   .btn--quiet        行内、卡片头、表格里的低权重动作（无边框，悬停才显形）
 *   .btn--danger-soft  高危
 *   .btn--danger       已经过确认、这一下就是最终执行
 *
 * 描边比上一版浅一档（`--c-border` 而不是 `--c-border-strong`）：
 * 一排次要按钮不该像一排边框。悬停时才把描边加深，这是"这个能点"的反馈。
 */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  /* 高度钉在 token 上（CR-20260814-13）。今天与内边距+行高算出来的值相等，
     所以视觉上一个像素都不变；意义在于让「控件多高」成为一个可被其它组件
     引用的显式取值——页头动作区的居中偏移就是靠它算出来的。 */
  min-height: var(--control-h);
  padding: 0.36rem 0.8rem;
  border-radius: var(--radius);
  border: 1px solid var(--c-border);
  background: var(--c-surface);
  color: var(--c-text);
  font-family: inherit;
  font-size: var(--fs-base);
  font-weight: 500;
  line-height: 1.5;
  cursor: pointer;
  transition: background var(--transition), border-color var(--transition), box-shadow var(--transition), color var(--transition);
  white-space: nowrap;
  text-decoration: none;
  box-shadow: var(--shadow-xs);
}
.btn:hover { background: var(--c-hover); border-color: var(--c-border-strong); text-decoration: none; color: var(--c-text); }
.btn:active { transform: translateY(0.5px); }
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.55; cursor: not-allowed; box-shadow: none; }
/* 按钮里的图标跟随字号缩放，并且**光学**上比文字略小一点点，
   否则 16px 的描边图标配 14px 的中文会显得图标在往外顶。 */
.btn .icon { width: 15px; height: 15px; flex-shrink: 0; opacity: .85; }
.btn--sm .icon { width: 14px; height: 14px; }

.btn--primary {
  background: var(--c-primary); border-color: var(--c-primary); color: #fff;
  box-shadow: 0 1px 2px rgba(37, 99, 235, 0.24);
}
.btn--primary:hover { background: var(--c-primary-hover); border-color: var(--c-primary-hover); color: #fff; }
.btn--primary .icon { opacity: 1; }
.btn--danger { background: var(--c-danger); border-color: var(--c-danger); color: #fff; }
.btn--danger:hover { background: #991b1b; border-color: #991b1b; color: #fff; }
/* 低权重动作。表格行里一排"编辑 / 更新状态 / 重新尝试"用它——
   八行八个描边按钮会让整列看起来像一条控件带，而那一列并不是页面的重点。 */
.btn--quiet {
  background: transparent; border-color: transparent; box-shadow: none;
  color: var(--c-text-secondary);
  font-weight: 500;
}
.btn--quiet:hover { background: var(--c-surface-sunken); border-color: var(--c-border); color: var(--c-text); }
.btn--quiet .icon { opacity: .7; }
.btn--quiet:hover .icon { opacity: .95; }
/* 纯图标按钮：必须自带 aria-label 或 .sr-only 文字，图标不承担语义。 */
.btn--icon { padding: 0.3rem; }
.btn--icon .icon { width: 16px; height: 16px; }
/* 高危动作的默认形态（CR-20260813-02 / §7.10）。
   实心红是**页面上最响的一个元素**——把它放进详情页右栏，等于让「删除客户」
   比「保存」还醒目。这里用红字 + 红边 + 白底：语义仍然是红色，但视觉层级
   落在 primary 之下。实心 `.btn--danger` 保留给「已经输入确认串、这一下就是
   最终执行」的场合。 */
.btn--danger-soft {
  background: var(--c-surface); border-color: var(--c-danger-border); color: var(--c-danger);
}
.btn--danger-soft:hover { background: var(--c-danger-soft); border-color: var(--c-danger); color: var(--c-danger); }
/* 幽灵按钮（CR-20260814-02 §五）：完全透明 + 无边框的关闭态，与旁边的
   纯文本无法区分——「清除筛选」在投稿列表上就是一段看不出能点的文字。
   现在关闭态带一条极淡的边框：仍然比 `.btn--quiet` 轻，但**看得出是个按钮**。 */
.btn--ghost {
  background: transparent; border-color: var(--c-border); box-shadow: none;
  color: var(--c-text-secondary);
}
.btn--ghost:hover { background: var(--c-surface-sunken); border-color: var(--c-border-strong); color: var(--c-text); }
.btn--sm { min-height: var(--btn-h-sm); padding: 0.18rem 0.55rem; font-size: var(--fs-sm); gap: 5px; }
.btn--lg { padding: 0.5rem 1.2rem; font-size: var(--fs-md); }
.btn--block { width: 100%; }
.btn-group { display: inline-flex; gap: var(--sp-2); flex-wrap: wrap; }

/* --- 8b. 分段控件（CR-20260814-01 / §8）------------------------------- */
/* 同一批数据的几个互斥视图（置信度 全部/较高/较低、范围 进行中/未开始/全部）。
 * 此前每个页面各写各的：有的是三个并排的 `.btn`，有的是一个下拉框，
 * 有的是三条蓝字——同一种语义在三个页面长成三个样子。
 *
 * 统一成一条分段控件：一个沉底槽，选中项是槽里抬起的那一格。
 * 每一格都是真 `<a href>`（各自是独立 URL，可收藏可后退），不是 JS 切页。 */
.segmented {
  display: inline-flex; align-items: center;
  padding: 2px;
  gap: 2px;
  background: var(--c-surface-sunken);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  flex-wrap: wrap;
}
.segmented__item {
  display: inline-flex; align-items: center; justify-content: center; gap: 5px;
  /* 这个类同时挂在 `<a>` 和 `<button>` 上（目标期刊版本切换用的是按钮，因为它
     切的是同一页的同一批数据，给它三个 URL 只会多两个内容相同的可收藏地址）。
     `<a>` 本来就没有按钮外观，`<button>` 有：浏览器默认给它灰底、2px 凹凸边框、
     Arial 13.3px 和自己的 padding。不显式清掉，同一个类在两种标签上会长成两个样子——
     实测里按钮版本看起来就是一组「原生浏览器按钮」，与设计语言完全不搭。 */
  appearance: none; -webkit-appearance: none;
  background: transparent;
  border: 1px solid transparent;
  margin: 0;
  font-family: inherit;
  line-height: 1.35;
  cursor: pointer;
  padding: 3px var(--sp-3);
  min-height: 26px;
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm); font-weight: 500;
  color: var(--c-text-secondary);
  white-space: nowrap;
  transition: background var(--transition), color var(--transition),
              border-color var(--transition), box-shadow var(--transition);
}
.segmented__item:hover {
  color: var(--c-text);
  background: var(--c-surface);
  text-decoration: none;
}
/* 选中态用全站既有的「蓝色选中面」语汇（与 .filter-chip、.mail-item--active、
   .badge--primary 同一套 token），而不是只靠一层白底加阴影——后者在
   sunken 轨道上的对比度太弱，四个页签并排时看不出哪个是当前项。 */
.segmented__item--active {
  background: var(--c-primary-soft);
  border-color: var(--c-primary-border);
  color: var(--c-primary);
  box-shadow: var(--shadow-xs);
  font-weight: 550;
}
.segmented__item--active:hover {
  color: var(--c-primary);
  background: var(--c-primary-soft);
}
.segmented__item:focus-visible { outline-offset: 1px; }
.segmented__count { font-size: var(--fs-xs); color: var(--c-text-muted); font-variant-numeric: tabular-nums; }

/* --- 9. 表单设计系统（ADR-031）----------------------------------------- */
/* 命名规则：容器 `form-field`，标签 `form-label`，控件 `form-control` /
   `form-select` / `form-switch`，辅助文字 `form-help`，错误 `form-error`。
   `.field*` 是同一套东西的旧别名，60 多个模板仍在用，保留以免制造无意义 diff。

   ⚠️ 新模板一律用 `form-*`。测试 tests/test_cr20260719_04_form_system.py
   会断言「模板里出现的每个类名都能在本文件里找到定义」，防止再次出现
   本轮修复的那种「用了但从未定义」的类名（form-field / radio-row / grid--2 ...）。 */

:root {
  /* 所有单行控件共用同一高度，这是"看起来是一套"的最关键一条 */
  --control-h: 42px;
  --control-h-sm: 34px;
  /* 小号**按钮**比小号输入框更矮（CR-20260815-10 / 用户裁定
   * 「这种按钮的高度都缩短，让能一行塞下」）。
   *
   * 单开一个 token 而不是改 `--control-h-sm`：后者还钉着 `.input--sm` 与
   * `.form-select--sm`，把它压到 26px 会连带缩掉筛选栏里的小号输入框。
   * 按钮里只有一行短文字，输入框还要容纳光标与占位符，两者本来就不必等高。
   *
   * 26px 让带按钮的表格行落到约 50px，与不带按钮的 47px 在同一档；
   * 此前是 34px，那些行是 59px，同一个系统里两种行高。 */
  --btn-h-sm: 26px;
  --control-px: 0.7rem;
}

.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--sp-4) var(--sp-5); }
.form-grid--single { grid-template-columns: 1fr; }
.grid { display: grid; gap: var(--sp-4) var(--sp-5); }
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }

/* --- 9-0. 表单分区（CR-20260814-01 / §7）------------------------------ */
/* 上一轮的表单页在视觉上就是 Django 的默认渲染：一张白卡里
 * 「标签 / 输入框 / 说明」重复十几遍，没有任何结构信号说明
 * 哪几个字段是一件事。新建稿件页有 10 个字段，读者要自己去分。
 *
 * `.form-section` 把字段按**业务含义**分组：一个分区一条细线、
 * 一个小标题、必要时一句说明。分区标题用 `.fs-sm` 而不是 h3——
 * 它是导览，不该和页面标题抢层级。
 *
 * 结构：
 *   .form-sections > .form-section > .form-section__head + .form-grid
 */
.form-sections > .form-section + .form-section {
  margin-top: var(--sp-5);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--c-border);
}
.form-section { min-width: 0; }
.form-section__head { margin-bottom: var(--sp-4); }
.form-section__title {
  font-size: var(--fs-md); font-weight: 600; color: var(--c-text); letter-spacing: -0.005em;
  display: flex; align-items: center; gap: var(--sp-2);
}
.form-section__title .icon { width: 15px; height: 15px; color: var(--c-text-muted); }
.form-section__desc { font-size: var(--fs-xs); color: var(--c-text-muted); margin-top: 3px; line-height: 1.55; }

.form-field, .field { display: flex; flex-direction: column; gap: var(--sp-2); min-width: 0; }
.form-field--full, .field--full { grid-column: 1 / -1; }
.form-label, .field__label {
  font-size: var(--fs-sm); font-weight: 550; color: var(--c-text);
  display: block; margin: 0;
}
.form-label--required::after, .field__label--required::after { content: " *"; color: var(--c-danger); }
/* 说明行**不得比它解释的那个框更宽**（CR-20260816-01 / 用户裁定）。
 *
 * 系统设置页的几条说明此前会横着铺满整个卡片，比上方 260px 的输入框长出好几倍——
 * 读起来不像「这个框的说明」，像卡片自己的一段话。
 * `max-width: 100%` 在这里不够：`.form-field` 是网格项，说明行会撑开它。
 * 用 `--control-w` 兜住：没有显式宽度时退回 100%，与今天一致。 */
.form-help, .field__help {
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
  margin: 0;
  line-height: 1.55;
  max-width: var(--control-w, 100%);
}
.form-error, .field__error { font-size: var(--fs-xs); color: var(--c-danger); margin: 0; }
/* Django 把 errors 渲染成 <ul class="errorlist">；这里去掉项目符号与缩进，
   否则错误信息会以带圆点的列表形式挂在控件下方，和其它文字对不齐。 */
.form-error ul, .field__error ul, ul.errorlist { list-style: none; margin: 0; padding: 0; }
ul.errorlist { font-size: var(--fs-xs); color: var(--c-danger); }

/* --- 9a. 文本类控件 ---------------------------------------------------- */
/* 关闭态（CR-20260814-02 §五）：文本输入是一个**要往里填东西的井**。
   上一版给的是「白底 + 一条浅边框」，与卡片底色同色，于是一屏表单看上去
   是一片空白加几条细线，分不出哪里能输入。这里加一道极浅的顶部内阴影
   （0.5px，不是装饰性的立体感），让控件在白卡上有一个可读的下沉边界。
   `--c-surface-input` 比卡片略暗一档，这一档就是「这里可以输入」的信号。 */
input[type="text"], input[type="password"], input[type="email"], input[type="url"],
input[type="number"], input[type="date"], input[type="datetime-local"], input[type="time"],
input[type="search"], input[type="tel"], select, textarea,
.form-control, .input {
  width: 100%;
  min-height: var(--control-h);
  padding: 0.45rem var(--control-px);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius);
  background: var(--c-surface-input);
  box-shadow: inset 0 1px 1px rgba(26, 31, 39, 0.035);
  font-family: inherit;
  font-size: var(--fs-base);
  line-height: 1.5;
  color: var(--c-text);
  transition: border-color var(--transition), box-shadow var(--transition), background var(--transition);
}
/* 有值 vs 空：填过的控件回到纯白，扫一眼就知道哪些填了。
   用 :placeholder-shown 而不是 JS——不需要任何脚本参与，也不会在
   浏览器自动填充时失灵。

   **必须限定 `[placeholder]`。** `:placeholder-shown` 只会命中「有 placeholder
   且正在显示它」的控件；没有 placeholder 的控件**永远**不匹配它，于是
   `:not(:placeholder-shown)` 恒为真——空的日期输入与三个没有 placeholder 的
   textarea 会一直是纯白，看起来像已经填过了。实测投稿登记页 15 个控件里有 4 个
   这样：同一种「空」状态出现两种底色，正好把这条规则要传达的信号弄反。
   限定之后，没有 placeholder 的控件一律保持关闭态底色，宁可少一个信号，
   也不要一个方向相反的信号。 */
input[placeholder]:not(:placeholder-shown):not([type="checkbox"]):not([type="radio"]),
textarea[placeholder]:not(:placeholder-shown) { background-color: var(--c-surface); }
/* 下拉框同理：选了真实取值就回到纯白，停在「---------」时保持关闭态底色。
   `<select>` 没有 placeholder，只能靠「当前选中项的 value 是不是空」来判。 */
select:not([multiple]):not([size]):not(:has(option[value=""]:checked)) {
  background-color: var(--c-surface);
}
textarea, .form-control--textarea { min-height: 92px; resize: vertical; line-height: 1.6; }

input:hover:not(:disabled):not([readonly]),
select:hover:not(:disabled),
textarea:hover:not(:disabled):not([readonly]) { border-color: var(--c-text-muted); }

input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--c-primary);
  /* 必须是 background-color：`background` 简写会把 <select> 的箭头背景图一并清掉，
     于是聚焦的下拉框看起来像个文本框。 */
  background-color: var(--c-surface);
  /* 内阴影在聚焦时清零：正在编辑的控件应该是这一屏最亮的那一个。 */
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.13);
}
input:disabled, select:disabled, textarea:disabled {
  /* 降低对比度但保持文字可读——禁用不等于看不见 */
  background: var(--c-surface-sunken); color: var(--c-text-secondary);
  border-color: var(--c-border); cursor: not-allowed;
}
input[readonly], textarea[readonly] {
  background: var(--c-surface-sunken); color: var(--c-text-secondary); cursor: default;
}
.input--sm, .form-control--sm { min-height: var(--control-h-sm); padding: 0.2rem 0.5rem; font-size: var(--fs-sm); }

/* 只读展示块（CR-20260814-09 / §八）。
   「这一格改不了」此前是用 `disabled` 的下拉框表达的——浏览器把它画成斜纹底 +
   低对比文字，于是这一格既改不了**也读不清**，而它恰恰是编辑投稿时最该读清楚
   的一格（这条任务投的是哪篇稿）。
   这里的形态是：与输入框同尺寸、浅灰底、**正常文字对比度**、无边框强调、
   无下拉箭头、不可聚焦。它看起来就是一段已经定下来的事实，不是一个坏掉的控件。 */
.form-static {
  width: 100%;
  min-height: var(--control-h);
  padding: 0.45rem var(--control-px);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface-sunken);
  font-size: var(--fs-base);
  line-height: 1.5;
  color: var(--c-text);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  min-width: 0;
}
/* 静态展示块的正文**单行**（CR-20260816-06 / §45）。
 *
 * 此前是 `overflow-wrap: anywhere`：投稿编辑页的「稿件」那一格内容是
 * `C000001-M01 · 一种用于……`，长标题于是折成两行，这一格比同排其它控件
 * 高出一截，整排标签跟着错位。
 *
 * 改成单行 + 真实溢出才省略。**判据是像素不是字数**——放得下就整条显示，
 * 放不下才收省略号，完整内容在父块的 `title=` 里（见 ReadOnlyDisplay）。
 * `min-width: 0` 不能省：`.form-static` 是 flex 容器，子项默认
 * `min-width:auto` 会拒绝收缩到内容宽度以下，`text-overflow` 因此永不触发。 */
.form-static__text {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.form-static__hint { font-size: var(--fs-xs); color: var(--c-text-secondary); }
.input--auto { width: auto; }

input[type="file"] {
  width: 100%; min-height: var(--control-h); padding: var(--sp-3);
  border: 1px dashed var(--c-border-strong); border-radius: var(--radius);
  background: var(--c-surface-alt); font-size: var(--fs-sm); cursor: pointer;
}

/* --- 9b. 下拉框 -------------------------------------------------------- */
/* 单值枚举的统一控件（ADR-031）。`appearance:none` + 内联 SVG 箭头，
   这样 Windows / macOS / Linux 三端外观一致——浏览器默认箭头差异很大，
   是"同一页面控件看起来不是一套"最主要的来源。 */
/* 关闭态（CR-20260814-02 §五）：下拉框与文本框此前只差一个 12px 的箭头，
   一排混合控件里根本分不出哪个能点开列表。现在箭头右侧多一条**竖分隔线**，
   形成一个「按钮井」——这是原生 <select> 在各平台上共有的形态语言，
   不需要用户学习。仍然是 `appearance:none` + 背景图，没有自建任何 listbox：
   键盘、读屏器、移动端选择器全部走原生。 */
select, .form-select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  cursor: pointer;
  padding-right: 2.35rem;
  background-image:
    url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1.5 1.75L6 6.25l4.5-4.5' stroke='%235a6472' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
    linear-gradient(var(--c-border), var(--c-border));
  background-repeat: no-repeat, no-repeat;
  background-position: right 0.8rem center, right 2.1rem center;
  background-size: 12px 8px, 1px 60%;
  /* 长选项文字不撑破布局 */
  text-overflow: ellipsis;
}
select:hover:not(:disabled), .form-select:hover:not(:disabled) { background-color: var(--c-hover); }
/* 尚未选择时文字降一档：`---------` 与一个真实取值不该看起来一样重。 */
select:has(option[value=""]:checked) { color: var(--c-text-muted); }
select:disabled, .form-select:disabled {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1.5 1.75L6 6.25l4.5-4.5' stroke='%238b95a3' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
/* 多选框不是下拉，不要箭头，也不要固定单行高度 */
select[multiple], select[size]:not([size="1"]) {
  appearance: auto; -webkit-appearance: auto; -moz-appearance: auto;
  background-image: none; padding-right: var(--control-px);
  height: auto; cursor: default;
}
.form-select--sm { min-height: var(--control-h-sm); padding: 0.2rem 1.9rem 0.2rem 0.5rem; font-size: var(--fs-sm); }
.form-select--inline { width: auto; min-width: 130px; }

/* 多选列表框：明确**不是**下拉框（需求：多选字段不能改成单选下拉框）。
   保留原生外观，只统一边框与圆角。 */
.form-multiselect {
  appearance: auto; -webkit-appearance: auto;
  background-image: none;
  padding: var(--sp-1) var(--sp-2);
  min-height: 132px; height: auto;
  cursor: default;
}
.form-file { border-style: dashed; background: var(--c-surface-alt); }
/* 若将来确实需要横向单选组（选项 ≤3 且需并列对比），用这个类；
   本轮的管理型枚举一律走 form-select，见 ADR-031。 */
.form-radio-group { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; }
.form-radio-group label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }

/* 勾选类控件（CR-20260814-02 §五）：全站统一 16px、统一主色、统一焦点环。
   **保留原生控件**（只设 accent-color 与尺寸，不用 appearance:none 自绘）——
   自绘的勾选框要自己处理 indeterminate、高对比度模式与 Windows 触屏，
   而原生控件在这三件事上开箱即用。 */
/* `.form-switch__input` 走的是自己那套（一条轨道 + 一个滑块，见 §10），
   被这条通用规则按成 16×16 就变成一个孤零零的圆点——排除它。
   排除写在选择器里而不是靠后面再覆盖一遍：覆盖会漏掉 width/height 之外的属性
   （`background` 与 `appearance` 都是 switch 自己设的）。 */
/* 勾选框与单选钮**自绘**（CR-20260815-20 / 用户裁定「还是原生 UI」）。
 *
 * 上一版只设 `accent-color`——那是给**原生**控件换个主色，形状、边框、
 * 勾的画法全部还是系统的，Windows 上因此与本站其它控件不是一套。
 *
 * `appearance: none` 之后要自己补回三件事，少一件就是可用性倒退：
 *   1. **勾/点本身**：勾用内联 SVG 背景（只在选中时出现），单选用 `box-shadow`
 *      画一个内圆点——比 `::before` 省一层伪元素，也不会被 `overflow` 裁掉。
 *   2. **焦点可见**：`:focus-visible` 的描边环仍在，且用 `box-shadow` 而不是
 *      `outline`，这样圆角能跟着走。
 *   3. **半选态**：`:indeterminate` 是「全选」表头复选框的真实状态
 *      （本站的批量操作栏用得到），漏掉它会显示成未选中，等于说谎。 */
input[type="checkbox"]:not(.form-switch__input), input[type="radio"] {
  appearance: none;
  -webkit-appearance: none;
  width: 16px; height: 16px;
  min-height: 0; padding: 0; margin: 0;
  flex: 0 0 auto;
  cursor: pointer;
  border: 1.5px solid var(--c-border-strong);
  background-color: var(--c-surface);
  background-repeat: no-repeat;
  background-position: center;
  transition: background-color .12s ease, border-color .12s ease;
}
input[type="checkbox"]:not(.form-switch__input) { border-radius: 4px; }
input[type="radio"] { border-radius: 50%; }

input[type="checkbox"]:not(.form-switch__input):hover:not(:disabled),
input[type="radio"]:hover:not(:disabled) { border-color: var(--c-primary); }

input[type="checkbox"]:not(.form-switch__input):checked,
input[type="checkbox"]:not(.form-switch__input):indeterminate,
input[type="radio"]:checked {
  background-color: var(--c-primary);
  border-color: var(--c-primary);
}
input[type="checkbox"]:not(.form-switch__input):checked {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3 6-6'/%3E%3C/svg%3E");
}
/* 半选：一条横杠。与勾用同一种画法，保证两种状态的线宽一致。 */
input[type="checkbox"]:not(.form-switch__input):indeterminate {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M4 8h8'/%3E%3C/svg%3E");
}
/* 单选钮的内圆点用内阴影画：不占伪元素，也不会被裁。 */
input[type="radio"]:checked { box-shadow: inset 0 0 0 3px var(--c-surface); }

input[type="checkbox"]:not(.form-switch__input):focus-visible, input[type="radio"]:focus-visible {
  outline: none;
  border-color: var(--c-primary);
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.18);
}
input[type="radio"]:checked:focus-visible {
  box-shadow: inset 0 0 0 3px var(--c-surface), 0 0 0 3px rgba(37, 99, 235, 0.18);
}
input[type="checkbox"]:disabled, input[type="radio"]:disabled {
  cursor: not-allowed;
  opacity: .55;
  background-color: var(--c-surface-sunken);
}

/* --- 9b2. 下拉框搜索（CR-20260720-01）---------------------------------- */
/* 选项超过阈值（paperops.js 里的 SELECT_SEARCH_MIN）时，JS 在下拉框上方插入
   一个搜索框，过滤 <option>。**原生 <select> 始终是唯一的提交元素**，
   也是键盘与读屏器唯一要打交道的东西——自绘 listbox 能做的这里一样能做，
   但要重写方向键、Home/End、首字母跳转、分组朗读与移动端选择器，
   每一条都是读屏器上的回归风险。搜索框只是个额外的过滤入口。 */
.select-search { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
/* 关闭态（CR-20260814-02 §五）：这个搜索框此前是一个独立的方框，
   悬在下拉框下方，读起来像**第二个字段**——投稿列表的「期刊」因此看着有两个输入。
   现在它与下拉框贴合成一组：上圆下方、共用一条边、缩进 + 更小的字号，
   从形态上就说明「我属于上面那个控件」。 */
.select-search__input {
  min-height: var(--control-h-sm);
  margin-top: -1px;
  padding: 0.2rem 0.5rem 0.2rem 1.6rem;
  font-size: var(--fs-sm);
  border: 1px solid var(--c-border-strong);
  border-top-color: var(--c-border);
  border-radius: 0 0 var(--radius) var(--radius);
  background: var(--c-surface-sunken);
  box-shadow: none;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='13' height='13' viewBox='0 0 20 20' fill='none' stroke='%238b95a3' stroke-width='1.8' stroke-linecap='round'%3E%3Ccircle cx='8.75' cy='8.75' r='5'/%3E%3Cpath d='m12.5 12.5 4.25 4.25'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: left 0.45rem center;
}
.select-search__input:focus { background-color: var(--c-surface); }
/* 成组之后，上面那个 select 的下缘不再画圆角。 */
.select-search select { border-radius: var(--radius) var(--radius) 0 0; }
.select-search__input::placeholder { color: var(--c-text-muted); }
/* 过滤后一个选项都不剩时给出提示，否则用户会以为下拉坏了 */
.select-search__empty { font-size: var(--fs-xs); color: var(--c-danger); }

/* --- 9b2. PaperOps 下拉外壳（CR-20260816-06 / §14–§20）----------------- */
/*
 * 全站普通下拉的统一外观。此前 `appearance:none` 只能改**关闭态**——
 * 一点开，弹出的仍是 Windows 自己画的那个白底方列表，
 * 与「目标期刊」的 PaperOps 组合框完全不是一套（用户报的就是这个）。
 * 弹出层的样式浏览器不给控制权，所以列表必须自绘。
 *
 * 关闭态的触发器刻意与 `select` 关闭态**逐条对齐**：同样的 --control-h、
 * --control-px、边框、圆角、hover 底色、focus 光圈，以及那个「箭头 + 竖分隔线」
 * 的按钮井。这样统一之后没有任何一格的高度或基线发生位移。
 *
 * 展开态则对齐 `.combobox__list`（期刊选择器）：同样的圆角、边框、阴影、
 * 260px 限高、`.is-active` 高亮。两者从此是同一种视觉语言。
 */
.po-select { position: relative; min-width: 0; }

.po-select__trigger {
  /* 与 input/select 同一套盒模型 */
  width: 100%;
  min-height: var(--control-h);
  padding: 0.45rem 2.35rem 0.45rem var(--control-px);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface);
  font-size: var(--fs-base);
  line-height: 1.5;
  color: var(--c-text);
  text-align: left;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  /* 按钮默认字体不继承，显式对齐表单控件 */
  font-family: inherit;
  font-weight: inherit;
}
.po-select__trigger:hover:not(:disabled) {
  background-color: var(--c-hover);
  border-color: var(--c-text-muted);
}
.po-select__trigger:focus-visible,
.po-select.is-open .po-select__trigger {
  outline: none;
  border-color: var(--c-primary);
  background-color: var(--c-surface);
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.13);
}
.po-select__trigger:disabled {
  background: var(--c-surface-sunken);
  color: var(--c-text-secondary);
  border-color: var(--c-border);
  cursor: not-allowed;
}
/* 未选择真实取值时文字降一档——与 `select:has(option[value=""]:checked)` 同口径。 */
.po-select--placeholder .po-select__value { color: var(--c-text-muted); }

/* 值只占一行；放不下才省略（§23：这是真正的长文本，允许截断）。 */
.po-select__value {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 箭头 + 竖分隔线组成的「按钮井」，与原生 select 的关闭态形态一致。 */
.po-select__caret {
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  width: 2.35rem;
  pointer-events: none;
  background-image:
    url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1.5 1.75L6 6.25l4.5-4.5' stroke='%235a6472' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
    linear-gradient(var(--c-border), var(--c-border));
  background-repeat: no-repeat, no-repeat;
  background-position: right 0.8rem center, left 0.25rem center;
  background-size: 12px 8px, 1px 60%;
}
.po-select__trigger:disabled .po-select__caret {
  background-image:
    url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1.5 1.75L6 6.25l4.5-4.5' stroke='%238b95a3' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
    linear-gradient(var(--c-border), var(--c-border));
}

.po-select__panel {
  position: absolute;
  z-index: 40;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  min-width: 100%;
  padding: var(--sp-1);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg, 0 10px 30px rgba(15, 23, 42, .12));
}
.po-select__panel[hidden] { display: none; }

/* 面板里的搜索框。是否出现由 needsSearchBox() 决定，不由这里决定。 */
.po-select__search {
  width: 100%;
  min-height: var(--control-h-sm);
  margin-bottom: var(--sp-1);
  padding: 0.2rem 0.5rem 0.2rem 1.6rem;
  font-size: var(--fs-sm);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface-sunken);
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='13' height='13' viewBox='0 0 20 20' fill='none' stroke='%238b95a3' stroke-width='1.8' stroke-linecap='round'%3E%3Ccircle cx='8.75' cy='8.75' r='5'/%3E%3Cpath d='m12.5 12.5 4.25 4.25'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: left 0.45rem center;
}
.po-select__search::placeholder { color: var(--c-text-muted); }

.po-select__list { max-height: 260px; overflow-y: auto; outline: none; }
.po-select__group {
  padding: var(--sp-2) var(--sp-3) var(--sp-1);
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--c-text-muted);
}
.po-select__option {
  padding: 6px var(--sp-3);
  border-radius: var(--radius-sm, 4px);
  font-size: var(--fs-sm);
  color: var(--c-text);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.po-select__option[hidden] { display: none; }
/* 高亮用 `.is-active` 而不是只靠 `:hover`：键盘上下移动时鼠标没动，
   只有 `:hover` 的话键盘用户看不到自己走到了哪一项。 */
.po-select__option.is-active, .po-select__option:hover {
  background: var(--c-accent-soft, var(--c-surface-sunken));
}
.po-select__option.is-selected { font-weight: 600; }
.po-select__option.is-disabled { color: var(--c-text-muted); cursor: not-allowed; }
.po-select__empty { padding: 6px var(--sp-3); font-size: var(--fs-sm); color: var(--c-text-muted); }
.po-select__empty[hidden] { display: none; }

/* --- 9b3. 统一文件上传（CR-20260720-01）-------------------------------- */
/* 目标：全站不再出现浏览器原生的「选择文件 / 未选择任何文件」。
   分层见 templates/components/file_field.html 的注释。 */
.file-field { min-width: 0; }
.file-drop { min-width: 0; }

/* 增强前拖放区只是个容器，虚线框由 input 自己的 .form-file 画。
   增强后反过来：框画在拖放区上，input 收进视觉隐藏。 */
.file-drop__ui { display: none; }
/* 关闭态（CR-20260814-02 §五）：虚线 + 近白底在白卡上几乎看不见。
   改成下沉底色 + 略粗的虚线，拖放区在页面上成为一个可辨认的**区域**，
   而不是一条虚线框。悬停与拖入分两档：悬停只提示可点，拖入才整块变主色。 */
/* CR-20260814-03：拖放区改成**竖向居中的一列**。
   改前是一条横向 flex，1440 下内容只占 778px 里的左边 290px，右边 490px
   空着——量出来的数字（左留白 17 / 右留白 17，而子元素最右只到 x≈290）
   说明那不是留白而是没排版：三个子元素挨着左边排完就没有然后了。
   现在图标 / 标题 / 辅助说明 / 按钮各占一行，层级靠字号与颜色表达。 */
.file-field--ready .file-drop {
  display: flex; flex-direction: column; align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-6) var(--sp-5);
  border: 1.5px dashed var(--c-border-strong);
  border-radius: var(--radius);
  background: var(--c-surface-sunken);
  cursor: pointer;
  text-align: center;
  transition: border-color var(--transition), background var(--transition);
}
.file-field--ready .file-drop:hover { border-color: var(--c-primary-border); background: var(--c-hover); }
.file-field--ready .file-drop__ui {
  display: flex; flex-direction: column; align-items: center;
  gap: var(--sp-2);
  width: 100%; min-width: 0;
}
.file-drop__icon {
  width: 40px; height: 40px; flex-shrink: 0;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--c-surface); color: var(--c-text-secondary);
  border: 1px solid var(--c-border-strong);
  margin-bottom: var(--sp-1);
}
.file-drop__icon .icon { width: 18px; height: 18px; }
.file-drop__title {
  font-size: var(--fs-base); font-weight: 550; color: var(--c-text); line-height: 1.4;
}
.file-drop__hint { font-size: var(--fs-xs); color: var(--c-text-muted); line-height: 1.5; }
/* 拖动经过时才换成「可以放在这里」。默认那条藏起来的是 --drop，
   拖动中反过来——两条都在 DOM 里，切换不经过 JS。 */
.file-drop__title--drop { display: none; }
.file-field--dragging .file-drop__title { display: none; }
.file-field--dragging .file-drop__title--drop { display: block; color: var(--c-primary-active); }
/* 按钮与上面的文字之间多留一档：它是这一列里唯一可点的东西。 */
.file-field--ready .file-drop__ui .btn { margin-top: var(--sp-1); }
/* 格式 / 数量 / 体积三条约束。放在拖放区里、按钮之下，窄屏自然换行。 */
.file-drop__limits {
  max-width: 100%;
  margin: 0;
  font-size: var(--fs-xs); color: var(--c-text-muted); line-height: 1.55;
  text-wrap: pretty;
}

/* 真实 <input type="file">。**刻意不用 display:none**——那样会把它一起
   踢出 Tab 序与无障碍树，键盘用户就再也打不开文件选择框了。
   这里用的是标准的视觉隐藏写法：仍可聚焦、读屏器仍读得到。 */
.file-field--ready .file-field__input {
  position: absolute;
  width: 1px; height: 1px;
  min-height: 0;
  padding: 0; margin: -1px;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* 键盘聚焦到隐藏的 input 时，焦点环画在拖放区上，否则焦点会"消失" */
.file-field--ready .file-drop:has(.file-field__input:focus-visible) {
  border-color: var(--c-primary);
  border-style: solid;
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.13);
}
/* 拖拽悬停 */
.file-field--dragging .file-drop {
  border-color: var(--c-primary);
  border-style: solid;
  background: var(--c-primary-soft);
}
/* 禁用与只读：不接受拖放，也不要显示成可点 */
.file-field--disabled .file-drop,
.file-field--ready .file-drop:has(.file-field__input:disabled) {
  cursor: not-allowed;
  background: var(--c-surface-sunken);
  border-color: var(--c-border);
  opacity: 0.7;
}
.file-field--disabled .file-drop:hover { border-color: var(--c-border); background: var(--c-surface-sunken); }
/* 错误态：虚线框也要变红，否则错误只体现在下方一行小字上 */
.form-field--error .file-drop { border-color: var(--c-danger); }
.file-field--ready.form-field--error .file-drop { background: var(--c-danger-soft); }

/* 已选文件与已保存文件共用同一张卡片。
   CR-20260814-03：整份清单收成**一张带分隔线的表**，而不是几张各自带边框的卡片
   ——三个文件时那是三个圆角框叠在一起，看着像三个独立控件。 */
.file-list { display: none; flex-direction: column; margin-top: var(--sp-3); }
.file-list--filled {
  display: flex;
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface);
  overflow: hidden;
}
.file-card {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  background: var(--c-surface);
  min-width: 0;
}
.file-list--filled > .file-card + .file-card { border-top: 1px solid var(--c-border); }
/* 已保存文件（编辑表单）不在清单里，自己成一张卡 */
.file-card--saved {
  background: var(--c-surface-alt);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
}
/* 类型标：用扩展名本身，不另造一套文件类型图标——用户在系统里看到的
   就是文件名末尾那几个字符（与 supported_format_label 同一个道理）。 */
.file-card__type {
  flex: 0 0 auto;
  min-width: 42px;
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  background: var(--c-surface-sunken);
  border: 1px solid var(--c-border);
  color: var(--c-text-secondary);
  font-size: 10px; font-weight: 600; line-height: 1.5;
  letter-spacing: 0.03em;
  text-align: center; text-transform: uppercase;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.file-card__body {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: baseline; gap: var(--sp-3);
}
/* 文件名单行省略而不是折行：折行会让一个长文件名把整行撑成三行，
   清单就再也扫不动了。完整文件名仍可由 title 悬停查看。 */
.file-card__name {
  flex: 1 1 auto; min-width: 0;
  font-size: var(--fs-sm); color: var(--c-text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.file-card__meta {
  flex: 0 0 auto;
  font-size: var(--fs-xs); color: var(--c-text-muted);
  font-variant-numeric: tabular-nums;
}
/* 移除：图标按钮 + aria-label（文字标签在一行三个动作时太重） */
.file-card__remove {
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: 26px; height: 26px; min-height: 0;
  padding: 0;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: none; color: var(--c-text-muted);
  cursor: pointer;
  transition: color var(--transition), background var(--transition), border-color var(--transition);
}
.file-card__remove .icon { width: 14px; height: 14px; }
.file-card__remove:hover { color: var(--c-danger); background: var(--c-danger-soft); border-color: var(--c-danger-border); }
.file-card__remove:focus-visible { outline: 2px solid var(--c-primary); outline-offset: 1px; }
.file-card__clear {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  flex-shrink: 0;
  font-size: var(--fs-sm); color: var(--c-danger);
  cursor: pointer; white-space: nowrap;
}
.file-card__clear input[type="checkbox"] {
  width: 16px; height: 16px; min-height: 0; padding: 0; margin: 0;
  accent-color: var(--c-danger); cursor: pointer; flex-shrink: 0;
}
/* 汇总行：数量 + 总大小 + 清空。它是清单这张表的表尾，用下沉底色与
   一条上边框和文件行分开——不这样处理，它读起来会像第四个文件。 */
.file-summary {
  display: flex; align-items: center; gap: var(--sp-3);
  flex-wrap: wrap;
  padding: var(--sp-2) var(--sp-3);
  border-top: 1px solid var(--c-border);
  background: var(--c-surface-alt);
  font-size: var(--fs-xs); color: var(--c-text-secondary);
}
.file-summary__spacer { flex: 1 1 auto; }

/* --- 9c. 错误态 -------------------------------------------------------- */
.form-field--error input, .form-field--error select, .form-field--error textarea,
.field--error input, .field--error select, .field--error textarea,
input[aria-invalid="true"], select[aria-invalid="true"], textarea[aria-invalid="true"] {
  border-color: var(--c-danger);
  background: var(--c-danger-soft);
}
.form-field--error input:focus, .form-field--error select:focus, .form-field--error textarea:focus,
.field--error input:focus, .field--error select:focus, .field--error textarea:focus {
  border-color: var(--c-danger);
  box-shadow: 0 0 0 3px rgba(185, 28, 28, 0.13);
}

/* --- 9d. Switch（布尔字段统一控件，ADR-031）---------------------------- */
/* 底层是原生 checkbox：无 JS 也能正常提交，键盘可达，读屏器读到的是复选框。
   整行是一个 <label>，因此点标题或说明文字都能切换——这是"整行可点击"。
   三列 grid 保证开关、主标签、说明文字**垂直居中**且说明另起一行，
   修掉了原来 checkbox 偏上、文字偏下、说明挤在同一行的问题。 */
.form-switch {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "label control" "help control";
  align-items: center;
  gap: 2px var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface);
  cursor: pointer;
  transition: border-color var(--transition), background var(--transition);
}
.form-switch:hover { border-color: var(--c-border-strong); background: var(--c-surface-alt); }
.form-switch__label { grid-area: label; font-size: var(--fs-base); font-weight: 550; color: var(--c-text); }
.form-switch__help { grid-area: help; font-size: var(--fs-xs); color: var(--c-text-muted); line-height: 1.5; }
/* 没有说明文字时，让标题在整行里垂直居中而不是顶到上面 */
.form-switch:not(:has(.form-switch__help)) { grid-template-areas: "label control"; }

.form-switch__input {
  grid-area: control;
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  position: relative;
  width: 40px; height: 22px;
  min-height: 0;
  padding: 0;
  margin: 0;
  flex-shrink: 0;
  border-radius: 11px;
  border: 1px solid var(--c-border-strong);
  background: var(--c-surface-sunken);
  cursor: pointer;
  transition: background var(--transition), border-color var(--transition);
}
.form-switch__input::after {
  content: "";
  position: absolute;
  top: 2px; left: 2px;
  width: 16px; height: 16px;
  border-radius: 50%;
  background: #fff;
  box-shadow: var(--shadow-xs);
  transition: transform var(--transition);
}
.form-switch__input:checked {
  background: var(--c-primary);
  border-color: var(--c-primary);
}
.form-switch__input:checked::after { transform: translateX(18px); }
.form-switch__input:focus-visible {
  outline: 2px solid var(--c-primary);
  outline-offset: 2px;
}
.form-switch__input:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}
.form-switch:has(.form-switch__input:disabled) { cursor: not-allowed; background: var(--c-surface-sunken); }
.form-switch--flush { border: 0; background: none; padding: 0; }
.form-switch--flush:hover { background: none; }

/* 旧的行内复选框（筛选栏的"仅看逾期"等）——保留，但也居中对齐 */
.checkbox-field { flex-direction: row; align-items: center; gap: var(--sp-2); }
.checkbox-field input[type="checkbox"], .checkbox-field input[type="radio"] {
  width: 16px; height: 16px; min-height: 0; padding: 0;
  accent-color: var(--c-primary); cursor: pointer; margin: 0; flex-shrink: 0;
}
.checkbox-field label { font-size: var(--fs-base); cursor: pointer; margin: 0; }

/* --- 9d2. 可选择胶囊（少量并列的多选枚举）----------------------------- */
/* 选刊助手的「期刊等级」「JCR 分区」「中科院分区」此前是一排 13px 的裸复选框
 * 配一个小标签——命中区域只有那个方块，未选中时几乎看不见，
 * 一整块看起来像还没做完的表单。
 *
 * 胶囊里**仍然是原生 checkbox + 真 label**（`<label for>` 包住 `<input>`）：
 * 键盘可达、读屏器读得到、无 JS 也能提交。变的只有命中区域与选中态的可见性。
 * 刻意不自绘方块（那要重写 indeterminate、focus、高对比度模式三件事）。
 */
.choice-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.choice-chip {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: 5px var(--sp-3);
  border: 1px solid var(--c-border-strong);
  border-radius: 999px;
  background: var(--c-surface);
  font-size: var(--fs-sm);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--transition), border-color var(--transition), color var(--transition);
}
.choice-chip:hover { background: var(--c-hover); border-color: var(--c-text-muted); }
.choice-chip input[type="checkbox"] {
  width: 15px; height: 15px; min-height: 0; padding: 0; margin: 0;
  accent-color: var(--c-primary); cursor: pointer; flex-shrink: 0;
}
.choice-chip:has(input:checked) {
  background: var(--c-primary-soft);
  border-color: var(--c-primary-border);
  color: var(--c-primary);
  font-weight: 550;
}
.choice-chip:has(input:focus-visible) {
  outline: 2px solid var(--c-primary);
  outline-offset: 2px;
}

/* --- 9e. 复选清单（多选字段的统一形态）-------------------------------- */
/* 「指定客户」「指定邮箱」用它，而不是无限纵向铺开的 SelectMultiple。
   限高 + 内部滚动，让卡片高度可预期。 */
.checkbox-list {
  display: flex; flex-direction: column;
  max-height: 232px; overflow-y: auto;
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius);
  background: var(--c-surface);
}
.checkbox-list__item {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--c-border);
  font-size: var(--fs-base);
  cursor: pointer;
}
.checkbox-list__item:last-child { border-bottom: 0; }
.checkbox-list__item:hover { background: var(--c-surface-alt); }
.checkbox-list__item input[type="checkbox"] {
  width: 16px; height: 16px; min-height: 0; padding: 0; margin: 0;
  accent-color: var(--c-primary); flex-shrink: 0; cursor: pointer;
}
.checkbox-list__item span { min-width: 0; overflow-wrap: anywhere; }
.checkbox-list__empty { padding: var(--sp-4); text-align: center; color: var(--c-text-muted); font-size: var(--fs-sm); }
/* 指定期刊选择器（CR-20260816-07 §11）。
 *
 * 与 `.checkbox-list` 的区别只有两点，但都是硬要求：
 *
 * 1. **多列**。32 本刊排一列有 32 行；桌面上 3–4 列一屏就能看完。
 *    用 `auto-fill` + `minmax` 而不是写死列数——375px 下同一条规则
 *    自然回落到 1–2 列，不需要额外断点。
 * 2. **不设 `max-height`，不出内部滚动条**。`.checkbox-list` 有
 *    `max-height: 232px; overflow-y: auto`，那正是「32 本看不全」的原因。
 *    整块默认折叠，展开后就该一次看完。
 */
.journal-picker {
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  /* ⚠️ 覆盖 `.form-field` 的 `display: flex; gap: var(--sp-2)`（CR-20260817-02）。
   *
   * 这一块的标记同时带着 `form-field` 类，于是 `<details>` 成了 flex 容器，
   * 而那个 8px 的 gap 在**折叠状态下依然生效**——折叠头下方凭空多出 8px，
   * 结果是文字在外框里上下不等距：实测 label 上 11.6px、下 19.6px。
   * 用户看到的「没有垂直居中」就是这 8px。
   *
   * 折叠头与展开体各自有自己的内边距，本来就不需要容器 gap，因此改回块级。 */
  display: block;
}

/* 指定期刊与上下两段之间的垂直节奏（CR-20260817-02 §46–§48）。
 *
 * ## 实测：这里的间距是 **0px**，不是「偏小」
 *
 * 这一块是 `.topic-step__body` 的直接子元素，前后各是一个 `.form-sections`。
 * 而 `.topic-step__body` 只有 `min-width: 0`——它不是 grid 也不是 flex，**没有 gap**；
 * `.journal-picker` 自己没有 margin；`.form-sections > .form-section + .form-section`
 * 的 `--sp-5` 只在同一个 `.form-sections` **内部**生效，跨兄弟不生效。
 * 三条规则各自都对，合起来的结果是三块的边框直接贴住（2026-08-17 浏览器实测 0px / 0px）。
 *
 * ## 为什么取 `--sp-4`
 *
 * 同一份表单里「字段 → 字段」的行间距实测 16px（`.form-grid` 的 `row-gap: var(--sp-4)`）。
 * 指定期刊在语义上就是这一列字段中的一个（模板上它带着 `form-field--full`），
 * 所以它与上下的间距应当等于那个节奏，而不是另立一档。
 *
 * 用子选择器而不是直接给 `.journal-picker` 加 margin：margin 属于**这个上下文里的
 * 排版关系**，不属于组件本身。哪天它被放进 `.form-grid`（那里 gap 已经负责间距），
 * 组件自带的 margin 就会变成一个双份的间距。
 */
.topic-step__body > .journal-picker { margin: var(--sp-4) 0; }

.journal-picker__summary {
  display: flex; align-items: center; gap: var(--sp-3);
  /* 折叠头的高度对齐普通单行控件（CR-20260817-02 §48）。
   *
   * 改之前：上下各 12px padding + 20.8px 行盒 = **44.8px**，比普通输入框
   * （`--control-h` = 42px）高 2.8px。差得不多，但它是这一列里唯一一个「比别人高」的
   * 控件，于是折叠状态下看起来像个异类。
   *
   * 上下改 8px 之后内容高度是 36.8px，由 `min-height` 兜到 42px——
   * 和 `.form-control` 用的是**同一个** token，因此两者永远相等，
   * 而不是「这次调成了差不多」。 */
  padding: var(--sp-2) var(--sp-3);
  min-height: var(--control-h);
  cursor: pointer;
  list-style: none;
}
.journal-picker__summary::-webkit-details-marker { display: none; }
.journal-picker__summary .form-label { margin: 0; }
.journal-picker__count {
  margin-left: auto;
  font-size: var(--fs-xs);
  color: var(--c-text-secondary);
  font-variant-numeric: tabular-nums;
}
.journal-picker__caret { width: 16px; height: 16px; transition: transform var(--transition); }
.journal-picker[open] .journal-picker__caret { transform: rotate(180deg); }
.journal-picker__body { padding: 0 var(--sp-3) var(--sp-3); display: grid; gap: var(--sp-2); }
.journal-picker__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(184px, 1fr));
  gap: 0 var(--sp-3);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius);
  background: var(--c-surface);
  /* ⛔ 刻意没有 max-height / overflow：见上。 */
}
.journal-picker__grid .checkbox-list__empty { grid-column: 1 / -1; }

/* 已选项以标签形式回显在清单上方 */
.chip-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 9px;
  background: var(--c-primary-soft); color: var(--c-primary);
  border: 1px solid var(--c-primary-border);
  border-radius: 20px;
  font-size: var(--fs-xs); font-weight: 550;
}

/* --- 9f. 设置卡片与权限矩阵 -------------------------------------------- */
.settings-card {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xs);
  margin-bottom: var(--sp-5);
  overflow: hidden;
}
.settings-card__header {
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--c-border);
  background: var(--c-surface-alt);
}
.settings-card__title { font-size: var(--fs-md); font-weight: 600; }
.settings-card__desc { font-size: var(--fs-sm); color: var(--c-text-secondary); margin-top: 3px; }
.settings-card__body { padding: var(--sp-5); }
.settings-card__body--flush { padding: 0; }

/* 权限矩阵：每行「模块名 + 下拉框」，行高一致、垂直居中、下拉宽度统一。
   用 grid 而不是 table，是为了在窄屏下能干净地折成卡片布局。 */
.permission-group__title {
  padding: var(--sp-3) var(--sp-5);
  background: var(--c-surface-sunken);
  border-bottom: 1px solid var(--c-border);
  font-size: var(--fs-xs);
  font-weight: 650;
  letter-spacing: 0.05em;
  color: var(--c-text-secondary);
}
.permission-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 168px;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-2) var(--sp-5);
  min-height: 56px;
  border-bottom: 1px solid var(--c-border);
  transition: background var(--transition);
}
.permission-row:last-child { border-bottom: 0; }
.permission-row:hover { background: var(--c-surface-alt); }
.permission-row__name { font-size: var(--fs-base); font-weight: 500; min-width: 0; }
.permission-row__hint { font-size: var(--fs-xs); color: var(--c-text-muted); margin-top: 1px; }
.permission-row__control { min-width: 0; }
.permission-row__control select { width: 100%; }
.permission-row--head {
  min-height: 0; padding-top: var(--sp-2); padding-bottom: var(--sp-2);
  background: var(--c-surface-alt);
  font-size: var(--fs-sm); font-weight: 600; color: var(--c-text-secondary);
}

/* --- 9g. 表单动作条 ---------------------------------------------------- */
.form-actions {
  display: flex; gap: var(--sp-3); align-items: center;
  padding-top: var(--sp-5); margin-top: var(--sp-5);
  border-top: 1px solid var(--c-border);
  flex-wrap: wrap;
}
.form-actions--right { justify-content: flex-end; }
/* 表单页的动作条是**卡片页脚**，不是正文里最后一段（CR-20260814-01）。
 *
 * 此前它是卡片体内部的一条上边线 + 两个按钮，左右各留 20px 内边距，
 * 于是「保存」浮在一片白里，看不出这是提交区。用负外边距把它撑到卡片全宽、
 * 换成沉底色，就是一条正式的表单页脚——12 个表单页一次全部生效，
 * 不需要逐个模板去包一层 `.card__footer`。 */
.card__body > form > .form-actions {
  margin: var(--sp-6) calc(-1 * var(--sp-5)) calc(-1 * var(--sp-5));
  padding: var(--sp-4) var(--sp-5);
  background: var(--c-surface-alt);
  border-top: 1px solid var(--c-border);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
/* 卡片内部的动作条不需要再画一条分隔线 */
.settings-card__body > .form-actions:first-child { border-top: 0; padding-top: 0; margin-top: 0; }
/* 长表单里的保存条：贴在内容底部、位置稳定 */
.form-actions--sticky {
  position: sticky; bottom: 0;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-lg);
  padding: var(--sp-3) var(--sp-5);
  margin-top: var(--sp-5);
  box-shadow: var(--shadow-md);
  z-index: 10;
}

/* 表格单元格里的小表单（改负责人、改状态等） */
.inline-form { display: inline-flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; margin: 0; }
.inline-form select, .inline-form input { width: auto; min-width: 130px; }

/* --- 9g2. 行式编辑表单（CR-20260813-02）-------------------------------- */
/* 「一行记录 = 一个可直接编辑的表单行」这种形态在本系统里只有一处
 * （稿件详情的作者名单），但它此前是全站最像未完成页面的地方：
 * 六个只有 placeholder 没有标签的输入框，用 `style="flex:1 1 120px"`
 * 这样的行内样式横着排，两个原生复选框夹在中间，删除按钮还另起一整行。
 *
 * 这里给它一套正式的语法：
 *   .row-form            —— 一行（grid，列宽由 --row-form-cols 决定）
 *   .row-form__head      —— 列标题行，替代 placeholder 充当标签
 *   .row-form__cell      —— 一格
 *   .row-form__flags     —— 并排的布尔项
 * 真实的 <label> 用 .sr-only 保留给读屏器：视觉上靠列标题，
 * 无障碍上靠 label，两边都不缺。
 */
/* 列宽**必须是确定值**，不能用 `auto`（CR-20260816-01 / 用户裁定「上下位置不一样」）。
 *
 * 每一个 `.row-form` 是一个**独立的 grid**（它是一个 `<form>`，各行各自提交）。
 * `auto` 与 `fr` 在每个 grid 里各算各的——于是「已保存的行」有两个按钮、
 * 「新增行」只有一个按钮，两行的动作列宽度不同，前面所有列跟着错位。
 * 用户看到的「上下位置不一样」就是这个。
 *
 * 换成固定值后，六列在每一行上解出的宽度完全相同，无论那一行有几个按钮。
 * （`subgrid` 也能解，但它要求所有行在同一个 grid 容器里，
 *  而这里每行必须是独立 `<form>`。）
 *
 * 默认值写在这里，模板不再用行内 style 覆盖——行内样式有守卫盯着，
 * 而这套列宽是**组件的一部分**，不是某一页的数据。 */
.row-form-group {
  display: flex;
  flex-direction: column;
  --row-form-cols: 4.5rem minmax(0, 1.1fr) minmax(0, 1.5fr) 11.5rem minmax(0, 1fr) 8.5rem;
}
.row-form {
  display: grid;
  grid-template-columns: var(--row-form-cols);
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-2) var(--sp-5);
  border-top: 1px solid var(--c-border);
  margin: 0;
}
.row-form:hover { background: var(--c-surface-alt); }
.row-form__head {
  display: grid;
  grid-template-columns: var(--row-form-cols);
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-2) var(--sp-5);
  background: var(--c-surface-alt);
  border-bottom: 1px solid var(--c-border);
  font-size: var(--fs-sm); font-weight: 600; color: var(--c-text-secondary);
}
.row-form__cell { min-width: 0; }
.row-form__cell input, .row-form__cell select { width: 100%; }
.row-form__flags { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap; }
.row-form__flag {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-sm); white-space: nowrap; cursor: pointer;
}
.row-form__flag input[type="checkbox"] {
  width: 16px; height: 16px; min-height: 0; padding: 0; margin: 0;
  accent-color: var(--c-primary); cursor: pointer; flex-shrink: 0;
}
.row-form__actions { display: flex; align-items: center; gap: var(--sp-2); justify-content: flex-end; }
/* 卡片底部那一行「新增作者」在 `.row-form-group` **之外**（它在 `card__footer` 里），
   拿不到组上的列宽变量，因此这里再声明一次——**必须与组里那份一模一样**，
   否则新增行与既有行又会错位（那正是本轮修的问题）。 */
.row-form--bare {
  --row-form-cols: 4.5rem minmax(0, 1.1fr) minmax(0, 1.5fr) 11.5rem minmax(0, 1fr) 8.5rem;
  flex: 1 1 100%;
  border-top: 0;
  padding-left: 0;
  padding-right: 0;
}
/* 新增行：与既有行同一套列宽，靠背景色区分「这是还没保存的一行」。 */
.row-form--new { background: var(--c-surface-alt); border-top: 1px solid var(--c-border); }
.row-form--new:hover { background: var(--c-surface-alt); }
@media (max-width: 900px) {
  /* 窄屏：列标题失去意义（每格各占一行），改回逐格显示可见标签。 */
  .row-form, .row-form__head { grid-template-columns: minmax(0, 1fr); padding-left: var(--sp-4); padding-right: var(--sp-4); }
  .row-form__head { display: none; }
  .row-form__label { display: block; font-size: var(--fs-xs); color: var(--c-text-muted); margin-bottom: 2px; }
  .row-form__actions { justify-content: flex-start; }
}
/* 桌面端不重复显示格内标签——列标题已经说明了每一列是什么。 */
@media (min-width: 901px) { .row-form__label { display: none; } }

/* --- 9h. 高危操作区（CR-20260813-02 / §7.10）--------------------------- */
/* 此前是一张标题染红的卡片 + 一个铺满整宽的实心红按钮，放在详情页右栏，
 * 结果整页视觉最重的元素是「删除」。现在：卡片本身保持中性，只有标题
 * 带一个小号 danger 徽标；按钮改用 `.btn--danger-soft`，宽度按内容。 */
/* 本轮再收一档：默认折叠成一行，展开后才是完整的确认表单。
   收起时它是一条中性的灰线；只有展开之后才用 danger 边框把范围圈出来。 */
.danger-zone {
  border: 1px solid var(--c-border);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
}
.danger-zone[open] { border-color: var(--c-danger-border); }
.danger-zone__summary {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-sm); font-weight: 500;
  color: var(--c-text-secondary);
  cursor: pointer;
  list-style: none;
  border-radius: var(--radius-lg);
  transition: background var(--transition), color var(--transition);
}
.danger-zone__summary::-webkit-details-marker { display: none; }
.danger-zone__summary:hover { background: var(--c-surface-alt); color: var(--c-danger); }
.danger-zone__summary:focus-visible { outline: 2px solid var(--c-primary); outline-offset: 2px; }
.danger-zone__summary .icon { color: var(--c-text-muted); }
.danger-zone__summary:hover .icon { color: var(--c-danger); }
.danger-zone[open] .danger-zone__summary { color: var(--c-danger); border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
.danger-zone[open] .danger-zone__summary .icon { color: var(--c-danger); }
.danger-zone__caret { margin-left: auto; transition: transform var(--transition); }
.danger-zone[open] .danger-zone__caret { transform: rotate(180deg); }
.danger-zone__body { padding: 0 var(--sp-4) var(--sp-4); border-top: 1px solid var(--c-border); padding-top: var(--sp-4); }

.stat-row { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap; }
.stat-row__item { display: flex; align-items: baseline; gap: var(--sp-2); font-size: var(--fs-sm); }
.stat-row__value { font-size: var(--fs-lg); font-weight: 650; font-variant-numeric: tabular-nums; }

/* 卡片内的一排计数（客户详情的业务进度、邮箱详情的同步概况）。
   与仪表盘的 `.stat` 区别开：那是页面级指标卡，这是**卡片内**的一组数字，
   所以不带边框、不带图标托，只靠标签在上、数字在下的层级说明。 */
.metric-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(120px, 100%), 1fr));
  gap: var(--sp-3) var(--sp-5);
}
.metric-row__item { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.metric-row__label {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-sm); color: var(--c-text-secondary); min-width: 0;
}
.metric-row__value { font-size: var(--fs-xl); font-weight: 650; font-variant-numeric: tabular-nums; line-height: 1.2; }
/* 同一排里混着日期时用它：`2026-04-25` 用 fs-xl 会比旁边的个位数计数
   长出四五倍，把整排的重心拽到最后一格。 */
.metric-row__value--sm { font-size: var(--fs-base); font-weight: 600; }
/* 同一排里混着**不同种类**的量时用它划一道界（返修轮次：前四格是分布，
   最后一格是中位数）。没有这条线时「中位轮次 0」看起来像第五个分布桶。 */
.metric-row__item--aside {
  padding-left: var(--sp-5);
  border-left: 1px solid var(--c-border);
}
@media (max-width: 640px) {
  .metric-row__item--aside { padding-left: 0; border-left: 0; padding-top: var(--sp-3); border-top: 1px solid var(--c-border); }
}

/* 指标网格（CR-20260814-06 / §12）。八项处理周期各一格，三行：
   量的是哪两点 → 中位数 → 区间与样本量。
   刻意不是八张 `.stat` 卡片：这八个数不是页面级指标，它们是一组同类观测，
   给每个都配边框和图标托只会让「哪个更重要」这个问题凭空出现。 */
.metric-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
  gap: var(--sp-4) var(--sp-5);
}
.metric-grid__item { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.metric-grid__label { font-size: var(--fs-sm); color: var(--c-text-secondary); }
.metric-grid__value { font-size: var(--fs-lg); font-weight: 650; font-variant-numeric: tabular-nums; line-height: 1.3; }
.metric-grid__meta { font-size: var(--fs-xs); color: var(--c-text-muted); font-variant-numeric: tabular-nums; }

/* 低样本标记（CR-20260814-06 / §13）。
 *
 * 上一版这里是「样本较少，仅供内部参考」这句话，在一页上出现五次。
 * 五遍之后它不再是提示，它是噪音——而它要说的事其实已经由
 * `Rate.display` 表达了：样本不足时那里根本不出百分比。
 * 这个标记只是说明「不出百分比不是漏印」，解释放进 tooltip，全页只有一份。
 *
 * `title` 而不是常驻文字：`cursor: help` 让它看起来可悬停；
 * 触屏读不到 tooltip，但触屏用户读到的仍然是 `2 / 5` 这个完整事实，
 * 不会因为读不到解释而误解数字本身。 */
.low-sample {
  display: inline-block;
  padding: 0 6px;
  border-radius: var(--radius-sm);
  background: var(--c-surface-sunken);
  color: var(--c-text-muted);
  font-size: var(--fs-xs);
  font-weight: 500;
  white-space: nowrap;
  cursor: help;
}

/* 投稿系统入口（CR-20260814-06 / ADR-106）。
 *
 * 上一版这是「投稿信息」定义列表中间的三行普通字段。那三行回答的是同一个
 * 问题——现在去哪登、用哪个账号——所以它们该是一块紧凑模块，
 * 而不是散在十几行字段之间。
 *
 * 刻意**不做成大卡片**：这一块的内容通常只有两行字加一个按钮，
 * 给它一整屏的视觉权重会让它盖过这一页真正的主体（状态时间线）。 */
.portal { display: flex; flex-direction: column; gap: var(--sp-3); }
.portal__name { font-size: var(--fs-base); font-weight: 600; line-height: 1.4; }
.portal__account { font-size: var(--fs-sm); color: var(--c-text-secondary); }
.portal__account-label { color: var(--c-text-muted); margin-right: var(--sp-2); }
.portal__action { margin-top: var(--sp-1); }
/* 备注是次级信息：与上面隔一条线，字号压到 sm——它是「登录时要注意什么」，
   不是这一块的主体。 */
.portal__note {
  padding-top: var(--sp-3);
  border-top: 1px solid var(--c-border);
  font-size: var(--fs-sm);
  color: var(--c-text-secondary);
  line-height: 1.6;
}
.portal--empty { gap: var(--sp-3); align-items: flex-start; }
.portal--empty .btn { align-self: flex-start; }

/* 表单页宽度只有三档（CR-20260813-02）。
 * 此前 15 个表单页各写各的 `style="max-width:NNNpx"`：
 * 860 / 900 / 960 / 1000 / 1100 五种值，视觉上就是「每页都不一样宽」。
 * 收成三档，按列数选：单列窄表单 / 双列常规 / 双列宽字段（长 URL、Prompt）。 */
.form-page { max-width: 960px; }
.form-page--narrow { max-width: 720px; }
.form-page--wide { max-width: 1100px; }
/* 短枚举控件（时区、角色、单列映射）不该拉满整行。 */
.measure-sm { max-width: 340px; }

/* ⛔ `.form-subsection`（投稿登记页里折叠的「新增期刊」分区）已随
 *    CR-20260814-05 / ADR-112 删除。目标期刊改为一个文本框，
 *    没有二级新增流程了，这组样式在仓库里没有任何使用者。 */

/* 投稿阶段漏斗（CR-20260814-05 / Part E，CR-20260814-06 重做外观）。
 *
 * 上一版是一列带小圆点的文字，视觉上就是一棵 debug tree：所有数字挤在
 * 一行里靠空格分开，层级只由缩进表达。现在每一层是一个**行**：
 * 左边一条连续的导轨 + 阶段标记，中间是阶段名，右边是计数与带分母的比例。
 * 出口（在这一层被退掉的 / 走到这一层之后录用的）缩进一级挂在下面。
 *
 * 仍然不画渐窄的梯形、不用宽度或颜色深浅代表好坏：那会让人凭视觉读出
 * 一个并不存在的精度，而这一页的样本常常只有个位数。颜色只用来区分
 * **出口的方向**（退稿 / 录用），不表示程度。
 *
 * 导轨用伪元素画在 `::before` 上，最后一层不画——它是终点，
 * 一条通向空白的线看起来像「还有下一步没渲染出来」。 */
/* 宽度上限：员工侧这一页没有右栏（右栏里全是管理员内容），主栏因此有 1200px。
   不设上限时「已投出」在最左、「10」在一千像素外的最右，读一行要横扫整屏。
   68ch 是这几行内容的自然长度，剩下的留白比一条被拉长的行好读。 */
.funnel, .dist { max-width: 68ch; }
.funnel { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.funnel__stage { position: relative; padding: 0 0 var(--sp-4) var(--sp-6); }
.funnel__stage:last-child { padding-bottom: 0; }
/* 阶段标记：实心点 + 一圈底色，让它在导轨上读起来是一个「站点」而不是一段线头。 */
.funnel__stage::before {
  content: ""; position: absolute; left: 0; top: 6px;
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--c-surface);
  border: 3px solid var(--c-primary);
  box-sizing: border-box;
  z-index: 1;
}
.funnel__stage:not(:last-child)::after {
  content: ""; position: absolute; left: 5px; top: 6px; bottom: 0;
  width: 1px; background: var(--c-border);
}
/* 标签与它的数值是**一个阅读单元**（CR-20260814-09 / §六）。
   上一版把标签放进 1fr、数值靠右，于是屏幕上是左边一列文字、右边一列悬空的
   数字，中间隔着一整片空白——要横向找才对得上哪个数字属于哪一阶段。
   现在两者上下贴着，行与行之间靠间距和左侧导轨分组，不需要任何横向扫视。 */
.funnel__main {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  min-height: 24px;
}
.funnel__label { font-weight: 600; min-width: 0; }
/* 数值行：条数与比例并排贴着，中间只留一个词的距离。 */
.funnel__metric {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  column-gap: var(--sp-2);
  row-gap: 2px;
  min-width: 0;
}
.funnel__count { font-size: var(--fs-lg); font-weight: 650; font-variant-numeric: tabular-nums; }
.funnel__rate {
  font-size: var(--fs-base); color: var(--c-text);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.funnel__exits { display: flex; flex-direction: column; gap: var(--sp-1); margin-top: var(--sp-2); }
/* 出口是从主路径分出去的一支：自己的左边线 + 缩进半级，字号比阶段行浅一档。 */
.funnel__exit {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  padding: var(--sp-1) 0 var(--sp-1) var(--sp-3);
  border-left: 2px solid var(--c-border);
  font-size: var(--fs-sm);
}
.funnel__exit--reject { border-left-color: var(--c-danger-border); }
.funnel__exit--accept { border-left-color: var(--c-success-border); }
.funnel__exit-label { color: var(--c-text-secondary); min-width: 0; }
.funnel__exit-count { font-weight: 650; font-variant-numeric: tabular-nums; color: var(--c-text); }
.funnel__exit .funnel__rate { font-size: var(--fs-sm); }

/* `align-items: flex-start` 而不是 `flex-end`（CR-20260813-02 / §7.3）。
 *
 * 筛选栏里的字段不一定等高：选项超过阈值的下拉框会被 JS 加上一个搜索框
 * （见 paperops.js 的 initSelectSearch），因此比邻居高出一个控件。
 * `flex-end` 会把这个字段的**标签**顶高一截，于是「搜索 / 状态 / 期刊 /
 * 负责人 / 检索类型」五个标签不在同一条水平线上——投稿列表就是这样。
 *
 * 改为顶部对齐后，标签一律齐平；控件本身高度统一（--control-h），
 * 所以控件那一行也齐平；多出来的搜索框像说明文字一样挂在下面。
 */
/* 托盘（CR-20260814-02 §五）：筛选栏此前是 `--c-surface-alt`（近白），
   里面的控件也是白的，于是整条筛选栏看起来是一片空白加几条细线。
   改成 `--c-surface-sunken` 之后控件在其中是**浮起**的，
   「哪些是可操作的东西」不需要靠边框去分辨。 */
.filter-bar {
  display: flex; gap: var(--sp-3); align-items: flex-start;
  flex-wrap: wrap;
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--c-border);
  background: var(--c-surface-sunken);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
/* 托盘里的控件回到纯白：它们是这一块里唯一"浮起"的东西。 */
.filter-bar input, .filter-bar select, .filter-bar textarea { background-color: var(--c-surface); }
.filter-bar .field, .filter-bar .form-field { flex: 0 1 184px; }
.filter-bar .field--wide, .filter-bar .form-field--wide { flex: 1 1 260px; }
/* 在一行里吃掉**剩余**宽度（站内搜索那个大搜索框）。
   与 `--wide` 的区别：`--wide` 给的是更大的 flex-basis，仍与同排元素分配空间；
   这个是「其余都归我」，适合「一个长输入框 + 一个按钮」这种两件套。 */
.field--grow, .form-field--grow { flex: 1 1 auto; min-width: 0; }
/* 筛选栏里的标签比表单页的更轻：这里一排七八个标签，用表单页的字重会
   形成一条粗黑的横带，把下面的控件压住。 */
.filter-bar .form-label, .filter-bar .field__label {
  font-size: var(--fs-xs); font-weight: 500; color: var(--c-text-secondary);
}
/* 动作按钮与**控件行**对齐，而不是和标签行对齐。
   上一版 `.filter-bar__actions` 只设了 min-height，于是它顶在容器顶部、
   比旁边的下拉框高出一整个标签的高度——一排筛选控件里唯独按钮是歪的。 */
.filter-bar__actions { align-self: flex-end; }
/* 组合框（CR-20260815-11）。自绘 listbox 的外观——原生 `<datalist>` 的弹出框
 * 由浏览器绘制，既不跟输入框左对齐，也不可能与本站控件是一套（用户验收点名）。
 *
 * `position: absolute` + `left: 0; right: 0` 让弹出框与输入框**同宽同左缘**，
 * 这是「对齐」这条要求的实现方式——不是靠调 margin 凑。
 * 父层 `.combobox` 必须 `position: relative`，否则它会相对整页定位。 */
.combobox { position: relative; }
.combobox__input { width: 100%; }
.combobox__list {
  position: absolute;
  z-index: 40;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  max-height: 260px;
  overflow-y: auto;
  margin: 0;
  padding: var(--sp-1);
  list-style: none;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg, 0 10px 30px rgba(15, 23, 42, .12));
}
.combobox__option {
  padding: 6px var(--sp-3);
  border-radius: var(--radius-sm, 4px);
  font-size: var(--fs-sm);
  color: var(--c-text);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* 高亮用 `.is-active` 而不是 `:hover`：键盘上下移动时鼠标没动，
 * 只有 `:hover` 的话键盘用户看不到自己走到了哪一项。 */
.combobox__option.is-active, .combobox__option:hover {
  background: var(--c-accent-soft, var(--c-surface-sunken));
  color: var(--c-text);
}
.combobox__empty {
  padding: 6px var(--sp-3);
  font-size: var(--fs-sm);
  color: var(--c-text-muted);
}

/* 搜索框的前置放大镜。
 *
 * 用背景图而不是在每个模板里插一个 <svg>：全站十几个筛选栏的搜索框
 * 一律是 `input[type="search"]`（Django 侧的 widget 也带 `type: search`），
 * 一条属性选择器就能全覆盖，不需要动任何模板，将来新增筛选栏也自动生效。
 * 图标本身与 `components/icon.html` 的 `search` 是同一条路径。 */
input[type="search"] {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 20 20' fill='none' stroke='%238b95a3' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='8.75' cy='8.75' r='5'/%3E%3Cpath d='m12.5 12.5 4.25 4.25'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: left 10px center;
  padding-left: 32px;
}
/* 顶栏的编号直达自带一个真 <svg>（它要跟随聚焦态变色），不要再叠一层背景图。 */
.topbar__jump-input { background-image: none; padding-left: 30px; }
/* 筛选栏里的字段说明不应该把控件顶高——`align-items:flex-end` 会让更高的
   那一列的标签独自上移，一排标签就不再齐平。筛选栏本来也不该有整段说明
   （见 §12.7），个别确需的一行小字压到控件下方。 */
.filter-bar .form-help { order: 9; }
/* 已生效的筛选条件（CR-20260813-02 / §7.3）。
   此前只有「重置」按钮，但页面上看不出**当前到底筛了什么**——
   尤其是从别处带 querystring 跳进来的时候（如 `?submission=61`）。 */
.filter-bar__active {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--sp-2);
  flex: 1 1 100%;
  font-size: var(--fs-sm); color: var(--c-text-secondary);
}
.filter-chip {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: 1px 4px 1px 9px;
  background: var(--c-primary-soft); color: var(--c-primary);
  border: 1px solid var(--c-primary-border);
  border-radius: 20px;
  font-size: var(--fs-xs); font-weight: 550;
}
.filter-chip__clear {
  display: inline-grid; place-items: center;
  width: 16px; height: 16px; border-radius: 50%;
  color: var(--c-primary); line-height: 1;
}
.filter-chip__clear:hover { background: var(--c-primary-border); text-decoration: none; }
/* 置信度筛选（CR-20260722-06 §九）：标签 + 三个按钮在桌面端**同一行**（横排 Flex、不换行）；
   不受 190px 的 .field 基宽约束；仅窄屏允许换行。 */
.confidence-filter { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: nowrap; flex: 0 0 auto; min-width: 0; }
.confidence-filter__label { font-size: var(--fs-sm); font-weight: 550; color: var(--c-text); white-space: nowrap; }
.confidence-filter__buttons { flex-wrap: nowrap; }
@media (max-width: 640px) {
  /* 窄屏：占满整行并可收缩换行，避免三个筛选按钮把页面撑出横向滚动。 */
  .confidence-filter { flex: 1 1 100%; flex-wrap: wrap; }
  .confidence-filter__buttons { flex-wrap: wrap; }
}
/* `margin-left:auto` 把动作推到所在行的右端——筛选栏因此读起来是一条
   工具条（左边一排条件、右边两个动作），而不是"控件排完了后面接着两个按钮"。 */
/* 动作按钮跟在最后一个筛选控件**后面**，不另起一行（CR-20260815-06）。
 *
 * `margin-left: auto` 会把它推到当前行的最右端——当那一行还有位置时这没问题，
 * 但控件排满一行后它就成了下一行唯一的元素，于是筛选栏平白多出一整行高度
 * （邮件页实测：5 个控件一行 + 1 个控件一行 + 按钮一行 = 三行）。
 *
 * 改成只在**确实还宽裕**时才右推：`margin-left: auto` 换成 `margin-inline-start: auto`
 * 配合 `flex-wrap` 的自然换行——按钮先尝试挤进上一行的剩余空间，挤不下才换行。
 * 这样最常见的形态（控件没排满）与之前完全一样，排满时省掉一行。 */
/* 动作按钮与**同一行的控件**齐平（CR-20260815-07）。
 *
 * 这里换过两版，两版都因为同一个前提不成立而失败：**筛选格不等高**。
 * 期刊那一格曾经是「下拉框 + 下方搜索框」两个控件叠在一起，负责人那一格
 * 也长出过搜索框，于是 `flex-end` 会把按钮拽到那一格的底部（掉一行），
 * 而补偿用的 `margin-top` 只是猜一个标签高度，差几个像素肉眼就看得出来。
 *
 * 真正的修法是**消灭不等高**——期刊改组合框、负责人去掉搜索框，都在本 CR 里
 * 做完了。所有筛选格现在都是「一行标签 + 一个 `--control-h` 控件」，
 * `align-self: flex-end` 于是精确成立：底边对齐 ⇒ 等高 ⇒ 顶边也对齐，
 * 一个补偿像素都不需要，且按钮换到第几行都自动跟那一行齐平。
 *
 * 别再往这条规则上加 `margin-top`。如果哪天按钮又不齐，去查是哪一格变高了。 */
.filter-bar__actions {
  display: flex; gap: var(--sp-2); align-items: center;
  min-height: var(--control-h);
  margin-left: auto;
  align-self: flex-end;
  flex: 0 1 auto;
}
@media (max-width: 640px) { .filter-bar__actions { margin-left: 0; } }
/* 筛选栏与列表之间的一条事实行（「共 8 条」「其中 3 封尚未处理」）。
   此前这段文字直接裸放在卡片里，左边缘既不对齐表头也不对齐筛选栏。 */
.context-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3) var(--sp-4); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-5);
  border-bottom: 1px solid var(--c-border);
  background: var(--c-surface);
  font-size: var(--fs-sm); color: var(--c-text-secondary);
}
.context-bar__facts { display: flex; align-items: center; gap: var(--sp-2) var(--sp-4); flex-wrap: wrap; min-width: 0; }
.context-bar__fact { display: inline-flex; align-items: baseline; gap: 5px; }
.context-bar__value { font-weight: 600; color: var(--c-text); font-variant-numeric: tabular-nums; }
/* 筛选栏里的布尔项。用 min-height 与其它控件对齐，而不是靠 padding 顶下去——
   后者在字号或行高变化时立刻错位，也正是需求里点名禁止的「用空白强行布局」。 */
.filter-bar__check {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  flex: 0 0 auto;
  min-height: var(--control-h);
  font-size: var(--fs-base);
  cursor: pointer;
  white-space: nowrap;
}
.filter-bar__check input[type="checkbox"] {
  width: 16px; height: 16px; min-height: 0; padding: 0; margin: 0;
  accent-color: var(--c-primary); cursor: pointer; flex-shrink: 0;
}

/* --- 10. 提示条 -------------------------------------------------------- */
/* 底色比上一版更淡（`-soft` 系列本来就很淡，这里进一步只保留一层描边 +
   左侧的语义色图标）：整页顶着一条大红或大黄的横带，会让所有页面看起来
   都在报警。语义靠图标与描边表达，正文仍用常规文字色，保证可读。 */
.alert {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius);
  border: 1px solid;
  font-size: var(--fs-base);
  margin-bottom: var(--sp-4);
}

/* **带右侧动作**的提示条整体垂直居中（CR-20260815-15 / 用户裁定）。
 *
 * `.alert` 默认 `flex-start` 是对的：多行说明型提示里，图标应该落在**第一行**
 * 文字旁边，而不是整段的正中间。
 *
 * 但一旦右边挂了个按钮（`btn--sm` 27.3px，比一行文字的 21px 高），
 * 它就成了这一行最高的元素，图标与文字被一起吊在上半截——实测中线差 3.13px。
 * 此时这条提示必然是**一句话 + 一个动作**的形态，整体居中才是对的。
 *
 * 用 `:has(.alert__actions)` 精确地只命中这一种形态，长说明型提示不受影响。
 * 这也解释了为什么只给 `.alert__actions` 加 `align-self: center` 没用——
 * 它是在把最高的元素居中到自己身上，什么都没发生。 */
.alert:has(.alert__actions) { align-items: center; }
/* 图标位：过去是一个裸的 `!` 或 `i` 字符（`font-weight:700`），
   在不同字体下大小差别很大，和全站的描边 SVG 完全不是一套。
   现在统一由 `components/icon.html` 提供，这条规则只管定位与颜色。 */
.alert__icon { flex-shrink: 0; line-height: 1.5; display: flex; align-items: center; height: 1.5em; }
.alert__icon .icon { width: 17px; height: 17px; }
/* `align-self: center` 是这一条的关键（CR-20260815-15 / 用户裁定）。
 *
 * `.alert` 是 `align-items: flex-start`——那对**多行**提示是对的：图标应该
 * 落在第一行文字旁边，而不是整段文字的正中间。
 *
 * 但右侧动作按钮（`btn--sm`，27.3px）往往比一行文字高，于是它成了这一行里
 * 最高的元素；`.alert__actions` 上那句 `align-self: center` 因此是在把自己
 * 居中到自己身上，什么也没做，真正被吊在顶上的是**文字**（实测中线差 3.13px）。
 *
 * 让 body 自己居中就对了：body 是最高元素时这句是空操作（多行提示的形态不变），
 * body 比动作矮时它下移到中线——两种形态各自正确，不需要判断行数。 */
.alert__body { flex: 1; min-width: 0; align-self: center; }
.alert__title { font-weight: 600; margin-bottom: 2px; }
/* 提示条右侧的动作区（如「全部标记为已处理」）。不换行、不被正文挤扁，
   窄屏时整体落到下一行而不是把按钮压成两行字。 */
.alert__actions { flex-shrink: 0; align-self: center; }
.alert--info { background: var(--c-info-soft); border-color: var(--c-info-border); color: #0c4a6e; }
.alert--success { background: var(--c-success-soft); border-color: var(--c-success-border); color: #14532d; }
.alert--warning { background: var(--c-warning-soft); border-color: var(--c-warning-border); color: #78350f; }
.alert--danger { background: var(--c-danger-soft); border-color: var(--c-danger-border); color: #7f1d1d; }
.alert--error { background: var(--c-danger-soft); border-color: var(--c-danger-border); color: #7f1d1d; }
.alert--debug { background: var(--c-neutral-soft); border-color: var(--c-neutral-border); color: var(--c-text-secondary); }

/* --- 10b. 折叠说明块（CR-20260814-01 / §11）--------------------------- */
/* 「每次打开都在、内容从不改变」的参考说明。删不得（里面有安全口径与
   不可逆约束），但也不该每次都占掉第一屏——收成一行，需要时点开。
   用 `<details>` 而不是 JS：无脚本可用、键盘可达、读屏器读得到展开状态。 */
.note {
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface);
  margin-bottom: var(--sp-4);
}
.note__summary {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  font-size: var(--fs-sm); font-weight: 500;
  color: var(--c-text-secondary);
  cursor: pointer; list-style: none;
  border-radius: var(--radius);
  transition: background var(--transition), color var(--transition);
}
.note__summary::-webkit-details-marker { display: none; }
.note__summary:hover { background: var(--c-surface-alt); color: var(--c-text); }
.note__summary:focus-visible { outline: 2px solid var(--c-primary); outline-offset: 2px; }
.note__summary .icon { color: var(--c-text-muted); }
/* 折叠态摘要（CR-20260814-09 / §七）：标题右边一行紧凑事实，
   看得见但不抢分量。窄屏折到第二行，不挤压标题。 */
.note__summary-meta {
  font-weight: 400; color: var(--c-text-muted);
  font-variant-numeric: tabular-nums; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.note__caret { margin-left: auto; transition: transform var(--transition); }
.note[open] .note__caret { transform: rotate(180deg); }
.note[open] .note__summary { border-radius: var(--radius) var(--radius) 0 0; }
.note__body {
  padding: var(--sp-4);
  border-top: 1px solid var(--c-border);
  font-size: var(--fs-sm); color: var(--c-text-secondary); line-height: 1.65;
}
.note__body ul { margin: 0; padding-left: 1.3em; display: flex; flex-direction: column; gap: var(--sp-2); }
/* `.note` 自带 margin-bottom 是给「卡片之前的一段说明」用的。放进 `.stack`
   （右栏）时那段外边距会和 stack 的 gap 叠起来，于是它上下的间距比邻居宽一截。 */
.stack > .note { margin-bottom: 0; }

.messages { position: fixed; top: calc(var(--topbar-h) + 12px); right: var(--sp-6); z-index: 200; width: min(420px, calc(100vw - 48px)); }
.messages .alert { box-shadow: var(--shadow-lg); background-color: var(--c-surface); }
.messages .alert--info { background: #f0f9ff; }
.messages .alert--success { background: #f0fdf4; }
.messages .alert--warning { background: #fffbeb; }
.messages .alert--error, .messages .alert--danger { background: #fef2f2; }

/* --- 11. 空状态 -------------------------------------------------------- */
/* 空状态出现在几乎每一张列表卡片里，因此它的分量决定了「整个系统空着的时候
   是什么样」。此前是 2rem 的 Unicode 几何字符 + 0.4 透明度——不同平台
   字形差别很大（有些直接渲染成方框），看起来像缺字而不像图标。
   现在换成一个 28px 的描边 SVG，与侧栏图标同一套语言；纵向内边距从
   --sp-12（48px）收到 --sp-8（32px），空列表不再占掉整屏。 */
.empty {
  padding: var(--sp-8) var(--sp-6);
  text-align: center;
  color: var(--c-text-muted);
}
/* 图标托而不是裸图标：一个 44px 的浅色圆角方块托着 20px 的描边图标。
   裸线条图标浮在大片留白里显得像加载失败；有了托，空状态本身也是
   一个被设计过的元素。 */
.empty__tile {
  width: 44px; height: 44px;
  margin: 0 auto var(--sp-4);
  border-radius: 10px;
  display: grid; place-items: center;
  background: var(--c-surface-sunken);
  border: 1px solid var(--c-border);
  color: var(--c-text-muted);
}
.empty__icon { width: 20px; height: 20px; display: block; }
.empty__title { font-size: var(--fs-md); font-weight: 600; color: var(--c-text); margin-bottom: var(--sp-2); }
.empty__desc { font-size: var(--fs-sm); max-width: 420px; margin: 0 auto var(--sp-4); line-height: 1.6; }
.empty__desc:last-child { margin-bottom: 0; }
/* 表格内联空态：卡片里已经有表头时，不需要再来一个居中的大空白块。 */
.empty--inline { padding: var(--sp-6) var(--sp-4); }
.empty--inline .empty__tile { width: var(--tile); height: var(--tile); margin-bottom: var(--sp-3); }
.empty--inline .empty__icon { width: 16px; height: 16px; }
.empty--inline .empty__title { font-size: var(--fs-base); }

/* --- 12. 分页 ---------------------------------------------------------- */
.pagination {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4); padding: var(--sp-3) var(--sp-5);
  border-top: 1px solid var(--c-border); flex-wrap: wrap;
}
.pagination__info { font-size: var(--fs-sm); color: var(--c-text-secondary); }
.pagination__links { display: flex; gap: 2px; align-items: center; }
/* 页码是**导航**不是动作：中性描边 + 等宽数字，不用主色实心块。
   当前页用抬起的白面 + 加深描边表示，与分段控件同一套语言。 */
.pagination__link {
  min-width: 28px; height: 28px;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0 var(--sp-2); border-radius: var(--radius-sm);
  border: 1px solid transparent; color: var(--c-text-secondary);
  font-size: var(--fs-sm); background: transparent;
  font-variant-numeric: tabular-nums;
  transition: background var(--transition), border-color var(--transition), color var(--transition);
}
.pagination__link:hover { background: var(--c-surface-sunken); border-color: var(--c-border); color: var(--c-text); text-decoration: none; }
.pagination__link--current {
  background: var(--c-surface); border-color: var(--c-border-strong);
  color: var(--c-text); font-weight: 600; box-shadow: var(--shadow-xs);
}
.pagination__link--current:hover { background: var(--c-surface); color: var(--c-text); }
.pagination__link--disabled { opacity: 0.35; pointer-events: none; }
.pagination__link .icon { width: 15px; height: 15px; }

/* --- 13. 时间线 -------------------------------------------------------- */
.timeline { position: relative; padding-left: var(--sp-6); }
.timeline::before {
  content: ""; position: absolute; left: 7px; top: 6px; bottom: 6px;
  width: 2px; background: var(--c-border);
}
.timeline__item { position: relative; padding-bottom: var(--sp-5); }
.timeline__item:last-child { padding-bottom: 0; }
.timeline__marker {
  position: absolute; left: calc(-1 * var(--sp-6)); top: 3px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--c-surface); border: 2px solid var(--c-border-strong);
  display: grid; place-items: center;
}
.timeline__marker--primary { border-color: var(--c-primary); background: var(--c-primary); }
.timeline__marker--success { border-color: var(--c-success); background: var(--c-success); }
.timeline__marker--warning { border-color: var(--c-warning); background: var(--c-warning); }
.timeline__marker--danger { border-color: var(--c-danger); background: var(--c-danger); }
/* 「修回已提交」用 info：球已经踢回给期刊，是个「等待」而不是「待办」，
   与「返修中」的警示色区分开（见 SubmissionStatusEvent.marker_class）。 */
.timeline__marker--info { border-color: var(--c-info); background: var(--c-info); }
.timeline__header { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.timeline__title { font-weight: 550; }
.timeline__time { font-size: var(--fs-xs); color: var(--c-text-muted); }
.timeline__body { font-size: var(--fs-sm); color: var(--c-text-secondary); margin-top: var(--sp-1); }
.timeline__source {
  font-size: var(--fs-xs); color: var(--c-text-muted); margin-top: var(--sp-1);
  display: flex; gap: var(--sp-3); flex-wrap: wrap;
}

/* --- 14. 看板 ---------------------------------------------------------- */
.board { display: flex; gap: var(--sp-4); overflow-x: auto; padding-bottom: var(--sp-4); align-items: flex-start; }
.board__column {
  flex: 0 0 292px;
  background: var(--c-surface-sunken);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-lg);
  display: flex; flex-direction: column;
  max-height: calc(100vh - 220px);
}
.board__header {
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--c-border);
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  font-weight: 600; font-size: var(--fs-sm);
  position: sticky; top: 0; background: var(--c-surface-sunken);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.board__count {
  background: var(--c-surface); border: 1px solid var(--c-border);
  border-radius: 10px; padding: 0 7px; font-size: var(--fs-xs); color: var(--c-text-secondary);
}
.board__body { padding: var(--sp-3); overflow-y: auto; display: flex; flex-direction: column; gap: var(--sp-3); }
.board__card {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  padding: var(--sp-3);
  box-shadow: var(--shadow-xs);
  display: block;
  transition: box-shadow var(--transition), border-color var(--transition);
}
.board__card:hover { text-decoration: none; box-shadow: var(--shadow-md); border-color: var(--c-border-strong); }
/* 「未开始」列与卡片（CR-20260722-09 / ADR-080）。
   这一列里是**稿件**而不是投稿任务，虚线边框让两种记录类型一眼可辨——
   看板上混着两种东西时，只靠列标题的文字是不够的。 */
.board__column--not-started { border-style: dashed; border-color: var(--c-border-strong); }
.board__card--manuscript { border-style: dashed; }
.board__card--manuscript:hover { border-style: solid; }
.board__card-title { font-weight: 550; font-size: var(--fs-sm); color: var(--c-text); margin-bottom: var(--sp-2); line-height: 1.45; }
.board__card-meta { font-size: var(--fs-xs); color: var(--c-text-muted); display: flex; flex-direction: column; gap: 3px; }
.board__card-footer { display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-2); flex-wrap: wrap; }

/* 以稿件为单位的卡片（ADR-034）。信息分三层：
   第一层客户 + 检索类型，第二层稿件标题，第三层期刊，页脚是弱化的次要信息。
   每层都有明确的字号/字重/颜色差异，避免"堆一坨等宽小字"。 */
.board__card-top {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-2); margin-bottom: var(--sp-2);
}
.board__card-client {
  font-size: var(--fs-sm); font-weight: 600; color: var(--c-text);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.board__card-journal {
  font-size: var(--fs-xs); color: var(--c-text-secondary);
  margin-top: var(--sp-2); padding-top: var(--sp-2);
  border-top: 1px dashed var(--c-border);
  display: flex; align-items: center; gap: 5px;
  overflow-wrap: anywhere;
}
.board__card-journal--none { color: var(--c-text-muted); font-style: normal; }
.board__card-sub {
  display: flex; align-items: center; gap: var(--sp-2);
  flex-wrap: wrap;
  margin-top: var(--sp-1);
  font-size: var(--fs-xs); color: var(--c-text-muted);
}
.board__card-code { font-family: var(--font-mono); font-size: 11px; color: var(--c-text-muted); }
.board__empty {
  padding: var(--sp-5) var(--sp-3);
  text-align: center;
  font-size: var(--fs-xs);
  color: var(--c-text-muted);
  border: 1px dashed var(--c-border);
  border-radius: var(--radius);
}

/* --- 15. 标签页 -------------------------------------------------------- */
.tabs { display: flex; gap: var(--sp-1); border-bottom: 1px solid var(--c-border); margin-bottom: var(--sp-5); overflow-x: auto; }
.tabs__item {
  padding: var(--sp-2) var(--sp-4);
  color: var(--c-text-secondary);
  font-size: var(--fs-base); font-weight: 500;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  white-space: nowrap;
}
.tabs__item:hover { color: var(--c-text); text-decoration: none; }
.tabs__item--active { color: var(--c-primary); border-bottom-color: var(--c-primary); }

/* --- 16. 布局工具 ------------------------------------------------------ */
/* `min(340px, 100%)` 而不是裸的 `340px`（CR-20260720-02）。
 *
 * `minmax(340px, 1fr)` 的下限是**硬的**：容器只有 293px 时（375px 视口减去
 * 图标侧栏与内边距），网格列仍然会是 340px，于是整页被撑出横向滚动，
 * 表格右侧的操作按钮永远够不着——这正是本文件顶部 `.table-wrap`
 * 想要避免的情况，只不过溢出发生在它的**外层**，它自己的 overflow-x 拦不住。
 *
 * `min(340px, 100%)` 让下限在窄容器里退化为 100%：宽屏行为完全不变
 * （两列各至少 340px），窄屏则老实收成一列。
 * 紧邻的 `.grid-sidebar` 用的 `minmax(0, 1fr)` 是同一类写法。
 *
 * 由 tests/e2e/test_e2e_responsive.py 在 375×812 下量到 scrollWidth 469 > 375
 * 发现——这是那套用例第一次真实执行时抓到的布局缺陷。
 */
.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr)); gap: var(--sp-5); align-items: start; }
/* 恰好四项时锁成两列（拟题规则的四个等级）。`auto-fit` 在 1440 下会排成
   三列，第四张卡片孤零零掉到第二行——四个并列的同类项应该是 2×2。 */
@media (min-width: 900px) { .grid-2--pair { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* flex/grid 子项的 min-width 默认是 auto，会拒绝收缩到内容宽度以下，
 * 从而让内层的 overflow-x 容器失效。显式归零，让 .table-wrap 真的能滚。
 * `.grid-2 > *` 同理（CR-20260722-01：拟题规则页卡片内含版本表，窄屏溢出）。 */
.stack > *, .grid-2 > * { min-width: 0; }
/* 网格用 gap 排间距，卡片不应再带兄弟外边距——否则 `.card + .card{margin-top}`（第 307 行，
 * 为纵向堆叠卡片而设）会把网格里第一张之后的每张卡片下移一个 gap，导致同一行卡片顶部不齐
 * （拟题规则页 科技核心 比其它三张高一截）。这里为网格子卡片归零该外边距。 */
.grid-2 > .card { margin-top: 0; }
/* `.stack` 同理（CR-20260815-08 / 用户裁定「每个框块之间的上下距离要相等」）。
 *
 * `.stack` 用 `gap` 排间距，但 `.card + .card { margin-top: --sp-5 }` 会在 gap 之上
 * **再加**一段——于是同一列里「卡片接卡片」是 36px，而「卡片接 `.grid-2`」只有
 * gap 的 16px（`.grid-2` 不是 `.card`，选择器命中不了它）。期刊详情页因此上下
 * 间距忽大忽小：36 / 16 / 16。
 *
 * 归零之后这一列的间距**只由 `gap` 一处决定**，无论子元素是卡片、网格还是别的什么。
 * 这与上面 `.grid-2 > .card` 是同一条道理：容器负责间距，子元素不再自带外边距。 */
.stack > .card { margin-top: 0; }
/* `.grid-sidebar` 漏在上面两条之外（CR-20260816-06 / §42）。
 *
 * 同一个根因第三次出现：`.card + .card { margin-top: --sp-5 }` 是给**纵向堆叠**
 * 的卡片写的，而 `.grid-sidebar` 的两张卡是**左右两列**，第二张于是白白下移
 * 一个 gap。绑定邮箱页实测左卡 top 226.2、右卡 246.2 —— 差的正是 20px，
 * 也就是 `--sp-5`。用户报的「右侧推荐配置没和左侧邮箱账户对齐」就是它。
 *
 * `align-items: start` 早就在了，挡不住这条：那管的是**拉伸**，
 * 管不了子元素自带的外边距。所以修在容器一侧把外边距归零，
 * 而不是给右卡补一个反向的 margin（那只是把两个错误凑成一个巧合）。 */
.grid-sidebar > .card { margin-top: 0; }
.grid-sidebar { display: grid; grid-template-columns: minmax(0, 1fr) 336px; gap: var(--sp-5); align-items: start; }
.stack { display: flex; flex-direction: column; gap: var(--sp-4); }
.stack--sm { gap: var(--sp-2); }
.row { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.row--between { justify-content: space-between; }
/* `.row` 是 flex 容器，父级的 `text-align: center` 管不到它里面的项——
   居中的表格单元格里若装着 `.row`（如「● 启用」），要用这个变体。 */
.row--center { justify-content: center; }
.row--end { justify-content: flex-end; }
.spacer { flex: 1; }

.text-sm { font-size: var(--fs-sm); }
.text-xs { font-size: var(--fs-xs); }
.text-muted { color: var(--c-text-muted); }
.text-secondary { color: var(--c-text-secondary); }
.text-danger { color: var(--c-danger); }
.text-success { color: var(--c-success); }
.text-warning { color: var(--c-warning); }
.text-right { text-align: right; }
.text-center { text-align: center; }
.text-bold { font-weight: 600; }
.text-break { word-break: break-word; overflow-wrap: anywhere; }
/* 保留用户输入的换行（摘要、备注、基金信息、原始措辞）。
   此前每处都写 `style="white-space:pre-wrap"`，共 9 处。 */
.text-prewrap { white-space: pre-wrap; }
/* 语义上是列表、视觉上不要项目符号（提示条里的投稿任务清单等）。 */
.plain-list { list-style: none; margin: 0; padding: 0; }
/* 事实清单：几条**并列的硬信息**（「发送给模型的内容」「本次检索到什么」）。
   写成连续三段散文时读者要自己数有几条；分条之后一眼看得出边界。
   项目符号用一个小方块而不是圆点——与状态点 `.dot` 区分开，那个有语义色。 */
.fact-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.fact-list li {
  position: relative;
  padding-left: var(--sp-4);
  font-size: var(--fs-sm); color: var(--c-text-secondary); line-height: 1.6;
}
.fact-list li::before {
  content: ""; position: absolute; left: 0; top: 0.62em;
  width: 4px; height: 4px; border-radius: 1px;
  background: var(--c-border-strong);
}
/* 省略号截断。**刻意不带 `max-width`**（CR-20260814-11）：`.truncate` 排在
   `.measure-sm` 之后，带上 `max-width: 100%` 就会把同时写了两个类的元素上的
   340px 上限覆盖掉——而 `100%` 在一个 `table-layout: auto` 的单元格里等于
   「不限宽」，于是 `class="truncate measure-sm"` 是一句自我抵消的写法，
   登录记录的 User-Agent 列因此撑破了卡片。宽度约束由调用方给
   （`.measure-sm` 或列宽类），这里只负责怎么截。
   也不再设 `display: block`：那个值此前被写在 `<td>` 上，会让单元格
   脱离表格布局。需要块级盒时套一个 `<span class="truncate">`。 */
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 表格里给单元格套的截断块：`display:block` 才能让 ellipsis 生效，
   宽度上限由并列的 `.measure-*` 给。 */
.truncate-block { display: block; }
.mt-0 { margin-top: 0; } .mt-1 { margin-top: var(--sp-1); }
.mt-2 { margin-top: var(--sp-2); } .mt-3 { margin-top: var(--sp-3); }
.mt-4 { margin-top: var(--sp-4); } .mt-5 { margin-top: var(--sp-5); } .mt-6 { margin-top: var(--sp-6); }
.mb-0 { margin-bottom: 0; } .mb-2 { margin-bottom: var(--sp-2); } .mb-3 { margin-bottom: var(--sp-3); }
.mb-4 { margin-bottom: var(--sp-4); } .mb-5 { margin-bottom: var(--sp-5); }
.ml-2 { margin-left: var(--sp-2); }
.hidden { display: none !important; }
/* 最小工具集（CR-20260813-02）。只收口那些在多个模板里重复出现的行内样式，
   不是要建一套原子 CSS——命名保持语义化，数量保持可数。 */
.min-w-0 { min-width: 0; }
.shrink-0 { flex-shrink: 0; }
.d-inline { display: inline; }
.w-full { width: 100%; }
.cursor-pointer { cursor: pointer; }
/* 卡片页脚里放的不是一排按钮而是一整块内容（表单行、说明 + 动作）时用它：
   取消 flex 横排，改为块级堆叠。 */
.card__footer--stack { display: block; }

/* --- 16b. 最后一批收口类（CR-20260813-02）----------------------------- */
/* 图标 + 文字的紧凑行（状态点 + 「启用」）。比 .row 的默认间距更紧。 */
.row--tight { gap: var(--sp-1); }
/* 顶栏用户区：继承文字色，不要蓝色链接色。 */
.row--inherit { gap: var(--sp-2); color: inherit; }
/* 「标签 : 值」两列表格里的标签列。用比例而不是像素，跟随容器宽度。 */
.col-label { width: 40%; }
.col-quarter { width: 24%; }
.col-third { width: 30%; }
/* 选刊结果的一条：卡片内的分隔行，内边距与表格单元格一致。 */
.pick-row { padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--c-border); }
.pick-row:last-child { border-bottom: 0; }
.pick-score { font-size: 1.3rem; }
.pick-meter { width: 96px; }
/* 时间线里的「修正该判断」内联表单：一个下拉 + 一个输入 + 一个按钮。 */
.correct-form { gap: var(--sp-2); }
.correct-form__status { flex: 0 1 190px; }
.correct-form__note { flex: 1 1 180px; }
.correct-form__order { width: 60px; }

/* --- 17. 认证页 -------------------------------------------------------- */
.auth-page {
  min-height: 100vh; display: grid; place-items: center;
  padding: var(--sp-6);
  background:
    radial-gradient(1100px 520px at 50% -12%, #e8eefb 0%, transparent 62%),
    var(--c-bg);
}
.auth-card {
  width: 100%; max-width: 408px;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--sp-8);
}
.auth-card--wide { max-width: 560px; }
.auth-card__brand { display: flex; align-items: center; gap: var(--sp-3); margin-bottom: var(--sp-6); }
.auth-card__mark {
  width: 38px; height: 38px;
  display: block;
  flex-shrink: 0;
}
.auth-card__name { font-size: var(--fs-xl); font-weight: 650; }
.auth-card__tagline { font-size: var(--fs-sm); color: var(--c-text-muted); }
.auth-card h1 { font-size: var(--fs-xl); margin-bottom: var(--sp-2); }
.auth-card__desc { color: var(--c-text-secondary); font-size: var(--fs-sm); margin-bottom: var(--sp-5); }
.auth-footer { text-align: center; margin-top: var(--sp-5); font-size: var(--fs-xs); color: var(--c-text-muted); }

.qr-box {
  display: grid; place-items: center;
  padding: var(--sp-4);
  background: #fff; border: 1px solid var(--c-border); border-radius: var(--radius);
}
.qr-box svg { display: block; width: 190px; height: 190px; }
.secret-box {
  font-family: var(--font-mono); font-size: var(--fs-md);
  letter-spacing: 0.08em; text-align: center; word-break: break-all;
  padding: var(--sp-3); background: var(--c-surface-sunken);
  border: 1px solid var(--c-border); border-radius: var(--radius);
}
.code-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--sp-2);
  font-family: var(--font-mono);
}
.code-grid span {
  padding: var(--sp-2) var(--sp-3); background: var(--c-surface-sunken);
  border: 1px solid var(--c-border); border-radius: var(--radius);
  text-align: center; font-size: var(--fs-base); letter-spacing: 0.04em;
}

/* --- 18. 邮件与预览 ---------------------------------------------------- */
.mail-layout { display: grid; grid-template-columns: 392px minmax(0, 1fr); gap: var(--sp-5); align-items: start; }
/* 阅读页把顺序反过来（CR-20260814-09 / §五）：读信是主，归类是辅。
   DOM 顺序也跟着反过来，因此窄屏堆叠时先读到信、后读到归类。 */
.mail-layout--reader { grid-template-columns: minmax(0, 1fr) 392px; }
/* 阅读头：发件人一行、时间一行右对齐；下面是一个紧凑的字段表。
   这几行是「读一封信」时真正会看的，不做成通用 definition-list——
   那会把「收件人」与「Message-ID」排成同一种分量。 */
.mail-reader__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-3); flex-wrap: wrap; margin-bottom: var(--sp-3);
}
.mail-reader__from { font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
.mail-reader__time { font-size: var(--fs-sm); color: var(--c-text-muted); white-space: nowrap; }
.mail-reader__fields {
  display: grid; grid-template-columns: max-content minmax(0, 1fr);
  gap: 2px var(--sp-4); margin: 0 0 var(--sp-4);
  font-size: var(--fs-sm); line-height: 1.6;
}
.mail-reader__fields dt { color: var(--c-text-muted); }
.mail-reader__fields dd { margin: 0; min-width: 0; }
.mail-reader__foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); flex-wrap: wrap; margin-top: var(--sp-3);
}
@media (max-width: 560px) {
  .mail-reader__fields { grid-template-columns: minmax(0, 1fr); gap: 0 0; }
  .mail-reader__fields dd { margin-bottom: var(--sp-2); }
}
.mail-list { max-height: calc(100vh - 190px); overflow-y: auto; }
.mail-item {
  display: block; padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--c-border); color: var(--c-text);
}
.mail-item:hover { background: var(--c-surface-alt); text-decoration: none; }
.mail-item--active { background: var(--c-primary-soft); border-left: 3px solid var(--c-primary); padding-left: calc(var(--sp-4) - 3px); }
.mail-item__subject { font-weight: 550; font-size: var(--fs-sm); margin-bottom: 3px; line-height: 1.45; }
.mail-item__meta { font-size: var(--fs-xs); color: var(--c-text-muted); display: flex; gap: var(--sp-2); flex-wrap: wrap; align-items: center; }
.mail-body {
  padding: var(--sp-4);
  background: var(--c-surface-alt);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  white-space: pre-wrap;
  word-break: break-word;
  font-size: var(--fs-base);
  line-height: 1.7;
  max-height: 620px;
  overflow-y: auto;
}
.mail-body--html { white-space: normal; }
.mail-body--html img { max-width: 100%; height: auto; }
.mail-body--html table { max-width: 100%; }

.log-view {
  background: #0f1622; color: #cfd8e3;
  padding: var(--sp-4); border-radius: var(--radius);
  font-family: var(--font-mono); font-size: var(--fs-xs); line-height: 1.65;
  max-height: 520px; overflow: auto; white-space: pre-wrap; word-break: break-word;
}
.log-view mark { background: #7f1d1d; color: #fee2e2; padding: 0 2px; border-radius: 2px; }

/* --- 19. 进度与加载 ---------------------------------------------------- */
.progress { height: 6px; background: var(--c-surface-sunken); border-radius: 3px; overflow: hidden; }
.progress__bar { height: 100%; background: var(--c-primary); transition: width 300ms ease; }
.progress__bar--success { background: var(--c-success); }
.progress__bar--danger { background: var(--c-danger); }

.spinner {
  display: inline-block; width: 14px; height: 14px;
  border: 2px solid var(--c-border-strong); border-top-color: var(--c-primary);
  border-radius: 50%; animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.is-loading { opacity: 0; transition: opacity 180ms; }
.is-busy .is-loading { opacity: 1; }

/* --- 20. 模态 ---------------------------------------------------------- */
.modal-backdrop {
  position: fixed; inset: 0; background: rgba(16, 24, 40, 0.5);
  display: grid; place-items: center; z-index: 300; padding: var(--sp-6);
}
.modal {
  background: var(--c-surface); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg); width: min(620px, 100%);
  max-height: 88vh; display: flex; flex-direction: column;
}
.modal__header { padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--c-border); font-weight: 600; }
.modal__body { padding: var(--sp-5); overflow-y: auto; }
.modal__footer { padding: var(--sp-3) var(--sp-5); border-top: 1px solid var(--c-border); display: flex; justify-content: flex-end; gap: var(--sp-2); }

/* 确认框：一句问题 + 两个按钮（CR-20260817-02）。
 *
 * `.modal` 的 620px 是给撤稿弹窗那种**带投稿清单**的大框定的。同样的尺寸套在
 * 「确认退出当前账号吗？」上，会得到一个横向空了大半、两个按钮孤零零挤在
 * 右下角的框——宽度本身就是一种「这里有很多内容」的暗示，而这里没有。
 *
 * 分隔线同理：它们的作用是把标题 / 可滚动正文 / 操作区划开，而确认框的正文
 * 就一行、不滚动，三条横线只是把一个小框切成三段。
 */
.modal--confirm { width: min(380px, 100%); }
.modal--confirm .modal__header {
  padding: var(--sp-5) var(--sp-5) 0;
  border-bottom: 0;
  font-size: var(--fs-lg, 1rem);
}
.modal--confirm .modal__body { padding: var(--sp-3) var(--sp-5) var(--sp-5); }
.modal--confirm .modal__footer { padding: 0 var(--sp-5) var(--sp-5); border-top: 0; }

/* 原生 `<dialog>` 版模态（CR-20260816-07 §43）。
 *
 * 用 `<dialog>` 而不是 div + `.modal-backdrop`：焦点陷阱、Esc 关闭、
 * 惰性化背景内容、`::backdrop` 全部由浏览器提供。自己实现这些只会做出
 * 一个更差的版本——尤其是焦点管理，那是键盘用户能不能用这个弹窗的关键。
 *
 * `<dialog>` 自带 border / padding / margin:auto，这里显式清掉，
 * 让它复用 `.modal` 的外观。 */
dialog.modal {
  border: 0;
  padding: 0;
  margin: auto;
  color: inherit;
  overflow: visible;
}

/* ⚠️ 关闭状态必须**显式**隐藏（CR-20260817-02）。
 *
 * 浏览器默认样式里有 `dialog:not([open]) { display: none }`，但那是 UA 样式——
 * **任何**作者样式的 `display` 都压得过它，与特指度无关。而上面 `.modal` 为了
 * 让内容纵向排布写了 `display: flex`，于是一个关闭的 `<dialog class="modal">`
 * 会被当成普通块级元素**渲染在页面流里**。
 *
 * 这个缺陷在 CR-20260817-02 之前一直潜伏着：当时全站唯一的 `dialog.modal`
 * 是撤稿弹窗，它带 `data-open-on-load`、载入即 `showModal()`，从来没有处于
 * 关闭状态。退出确认框默认关闭，一登录就把它顶了出来——页面底部凭空多出
 * 一个「退出登录？」的方框。
 *
 * 写成 `dialog.modal:not([open])` 而不是给 `.modal` 换个 display：`.modal`
 * 同时服务非 dialog 的旧版弹窗（`.modal-backdrop` 那一套），改它会波及别处。
 */
dialog.modal:not([open]) { display: none; }
dialog.modal::backdrop { background: rgba(16, 24, 40, 0.5); }
.modal__panel { display: flex; flex-direction: column; min-height: 0; max-height: 88vh; }
.modal__title { margin: 0; font-size: var(--fs-lg, 1rem); font-weight: 650; }
.modal__subtitle {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-sm);
  font-weight: 400;
  color: var(--c-text-secondary);
}
/* 把「全选」推到左边、确认/取消留在右边：破坏性程度不同的按钮不该挨在一起。 */
.modal__spacer { flex: 1 1 auto; }

/* --- 20b. 拟题中央处理框（CR-20260722-04 / ADR-064）--------------------- */
.spinner--lg { width: 24px; height: 24px; border-width: 3px; }

/* 弹层打开时锁住页面滚动（CR-20260816-03）。
   遮罩之下的正文还能滚，会让人以为遮罩是个浮层而不是「现在只能等这件事」。
   `body.has-overlay` 由 paperops.js 在渲染弹层时加上、隐藏时去掉。 */
body.has-overlay { overflow: hidden; }

.topic-progress {
  position: fixed; inset: 0; z-index: 320;
  display: grid; place-items: center; padding: var(--sp-6);
  background: rgba(16, 24, 40, 0.45);
}
.topic-progress__card {
  width: min(460px, 100%); background: var(--c-surface);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
  padding: var(--sp-6); display: flex; flex-direction: column; gap: var(--sp-4);
  max-height: calc(100vh - 2 * var(--sp-6)); overflow-y: auto;
}
.topic-progress__head { display: flex; align-items: center; gap: var(--sp-3); }
.topic-progress__title { font-size: var(--fs-lg); font-weight: 600; margin: 0; }
.topic-progress__stage { margin: 0; color: var(--c-text-secondary); font-size: var(--fs-sm); min-height: 1.2em; }
.topic-progress__steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.topic-progress__step { display: flex; align-items: center; gap: var(--sp-3); font-size: var(--fs-sm); color: var(--c-text-muted); }
.topic-progress__label { min-width: 0; }
.topic-progress__marker {
  flex: 0 0 auto; width: 20px; height: 20px; border-radius: 999px;
  border: 2px solid var(--c-border-strong); background: var(--c-surface);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12px; line-height: 1;
}
.topic-progress__step.is-pending { color: var(--c-text-muted); }
.topic-progress__step.is-done { color: var(--c-text); }
.topic-progress__step.is-done .topic-progress__marker { border-color: var(--c-success); background: var(--c-success); color: #fff; }
.topic-progress__step.is-done .topic-progress__marker::after { content: "✓"; }
.topic-progress__step.is-active { color: var(--c-text); font-weight: 600; }
.topic-progress__step.is-active .topic-progress__marker { border-color: var(--c-primary); border-top-color: transparent; animation: spin 0.8s linear infinite; }
.topic-progress__step.is-failed { color: var(--c-danger); }
.topic-progress__step.is-failed .topic-progress__marker { border-color: var(--c-danger); background: var(--c-danger); color: #fff; }
.topic-progress__step.is-failed .topic-progress__marker::after { content: "✕"; }
.topic-progress__meta { font-size: var(--fs-sm); color: var(--c-text-secondary); font-variant-numeric: tabular-nums; }
.topic-progress__elapsed { font-weight: 600; }
.topic-progress__error { margin-top: var(--sp-1); }
.topic-progress__actions { margin-top: var(--sp-2); }
.topic-progress__foot { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; border-top: 1px solid var(--c-border); padding-top: var(--sp-3); }
.topic-progress__foot-actions { display: flex; align-items: center; gap: var(--sp-3); }
.topic-progress__hint { font-size: var(--fs-xs); color: var(--c-text-muted); margin: 0; }
.topic-progress__link { font-size: var(--fs-sm); }

@media (max-width: 480px) {
  .topic-progress { padding: var(--sp-4); }
  .topic-progress__card { padding: var(--sp-5); }
  .topic-progress__foot { flex-direction: column; align-items: stretch; }
  .topic-progress__foot-actions { justify-content: space-between; }
}

/* --- 19b. 拟题任务坞（CR-20260814-09 / ADR-118）------------------------- */
/* 详细进度框的「隐藏」之后，这里接手显示任务还在跑。
 *
 * 位置：桌面端右下角固定。z-index 低于进度框（320）——两者同屏时进度框在上，
 * 坞退到 `is-suppressed`（半透明 + 不接事件），不与它抢注意力。
 * 刻意避开表格分页与表单主操作：那两处都在内容流里、贴着卡片底边，
 * 而这块浮在视口右下角且宽度只有 300px。 */
.task-dock {
  position: fixed; right: var(--sp-6); bottom: var(--sp-6); z-index: 240;
  width: min(300px, calc(100vw - 2 * var(--sp-6)));
  display: flex; flex-direction: column; gap: var(--sp-2);
}
/* `hidden` 必须真的隐藏（CR-20260816-05 / §10）。
 *
 * 任务坞里有**四个**元素靠 `hidden` 控制显隐：坞本身、多任务时的摘要/折叠按钮、
 * 单条卡片、以及每条右侧的「×」。而它们各自都带了一条作者写的 `display:`
 * （flex / inline-flex）——作者样式表无条件压过浏览器默认样式表里的
 * `[hidden]{display:none}`，与选择器权重无关。于是这四处的 `hidden` 全部是空操作：
 *
 *   - `.task-dock`：卡片被轮询删光后 `dock.hidden = true` 不起作用，
 *     `<aside>` 里只剩一个空的 `.task-dock__panel`，而它有 1px 边框 + 阴影 + 圆角，
 *     **画出来正好是一条横线**——用户报的「最终只剩下一条横线 / border」就是它；
 *   - `.task-dock__dismiss`：任务还在跑时那颗「×」本该不渲染（模板里写着
 *     `{% if not entry.finished %}hidden{% endif %}`，注释明说「不要渲染一个
 *     点了没反应的控件」），实际却一直看得见——由本轮的浏览器验收用例抓到；
 *   - `.task-dock__summary` / `.task-dock__item`：同一类，只是后果没那么显眼。
 *
 * 四个一起修，而不是只修被报上来的那一个：它们是同一个缺陷的四个实例，
 * 留下三个只会让下一个人再排查一遍。
 *
 * 这一条与 JS 那一侧的修复是两道独立的闸：那边保证在跑的卡片不会被删光，
 * 这边保证即使真的空了也不会留下一个空壳边框。 */
.task-dock[hidden],
.task-dock__summary[hidden],
.task-dock__item[hidden],
.task-dock__dismiss[hidden] { display: none; }
.task-dock.is-suppressed { opacity: 0.25; pointer-events: none; }
/* 正文底部留出坞的高度。分页与表单底部的主操作都在右下角，不留这段留白
   它们会被**永久**盖住——滚到底也躲不开一个 fixed 元素。 */
.has-task-dock .content { padding-bottom: 108px; }
.task-dock__panel {
  background: var(--c-surface); border: 1px solid var(--c-border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
  overflow: hidden;
}
.task-dock__summary {
  width: 100%; display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4); background: var(--c-surface-alt);
  border: 0; border-bottom: 1px solid var(--c-border);
  font: inherit; font-size: var(--fs-sm); font-weight: 550; color: var(--c-text);
  cursor: pointer; text-align: left;
}
.task-dock__summary-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.task-dock__summary:hover { background: var(--c-hover); }
.task-dock__summary:focus-visible { outline: 2px solid var(--c-primary); outline-offset: -2px; }
.task-dock__chevron { margin-left: auto; display: inline-flex; color: var(--c-text-muted); }
.task-dock__list { list-style: none; margin: 0; padding: 0; }
/* 折叠态只留摘要那一行：多任务时不让浮窗长成半屏。 */
.task-dock__list--collapsed { display: none; }
.task-dock__item { display: flex; align-items: stretch; border-top: 1px solid var(--c-border); }
.task-dock__item:first-child { border-top: 0; }
.task-dock__card {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--sp-3) var(--sp-4);
  background: none; border: 0; font: inherit; color: var(--c-text);
  text-align: left; cursor: pointer;
}
.task-dock__card:hover { background: var(--c-hover); }
.task-dock__card:focus-visible { outline: 2px solid var(--c-primary); outline-offset: -2px; }
.task-dock__title { font-size: var(--fs-xs); color: var(--c-text-muted); }
.task-dock__label {
  font-size: var(--fs-sm); font-weight: 550;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.task-dock__meta {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2);
  font-size: var(--fs-xs); color: var(--c-text-secondary); min-width: 0;
}
.task-dock__meta > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.task-dock__elapsed { font-variant-numeric: tabular-nums; white-space: nowrap; }
.task-dock__item--done .task-dock__meta { color: var(--c-success); }
.task-dock__item--failed .task-dock__meta { color: var(--c-danger); }
.task-dock__dismiss {
  flex: 0 0 auto; width: 34px;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 0; color: var(--c-text-muted); cursor: pointer;
}
.task-dock__dismiss:hover { color: var(--c-text); background: var(--c-hover); }
.task-dock__dismiss:focus-visible { outline: 2px solid var(--c-primary); outline-offset: -2px; }

/* 窄屏：不用固定宽浮窗，收成贴底的一条 pill；展开后是一份窄屏列表。 */
@media (max-width: 767px) {
  .task-dock {
    right: var(--sp-3); left: var(--sp-3); bottom: var(--sp-3); width: auto;
  }
  .task-dock__panel { border-radius: var(--radius); }
  .task-dock__list { max-height: 50vh; overflow-y: auto; }
}

/* 尊重系统「减少动态效果」偏好（ADR-064）：进度旋转改为静态环，不闪不转。 */
@media (prefers-reduced-motion: reduce) {
  .spinner, .spinner--lg,
  .topic-progress__step.is-active .topic-progress__marker { animation: none; }
  .topic-progress__step.is-active .topic-progress__marker { border-top-color: var(--c-primary); }
}

/* --- 21. 匹配理由 ------------------------------------------------------ */
.reason-list { display: flex; flex-direction: column; gap: var(--sp-2); }
.reason {
  display: flex; align-items: flex-start; gap: var(--sp-2);
  font-size: var(--fs-sm); padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-sm); background: var(--c-surface-alt);
  border-left: 3px solid var(--c-border-strong);
}
.reason--match { border-left-color: var(--c-success); background: var(--c-success-soft); }
.reason--miss { border-left-color: var(--c-danger); background: var(--c-danger-soft); }
.reason--info { border-left-color: var(--c-info); background: var(--c-info-soft); }
.reason__score { margin-left: auto; font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; }

/* --- 22. 打印与响应式 -------------------------------------------------- */
@media (max-width: 1180px) {
  /* `minmax(0, 1fr)` 而不是 `1fr`（CR-20260720-02）。
   *
   * `1fr` 展开是 `minmax(auto, 1fr)`，那个 `auto` 下限等于轨道内容的
   * min-content 宽度——于是一张宽表格照样能把单列网格顶得比容器还宽，
   * 整页出现横向滚动。折成一列**不等于**这一列就会收缩。
   *
   * 这是本文件 1122 行 `.grid-sidebar` 基础规则里已经写对的写法，
   * 只是这条折叠规则漏掉了。仪表盘在 375×812 下 scrollWidth 469 > 375，
   * 由 tests/e2e/test_e2e_responsive.py 量出来。 */
  .grid-sidebar { grid-template-columns: minmax(0, 1fr); }
  .mail-layout, .mail-layout--reader { grid-template-columns: minmax(0, 1fr); }
  .mail-list { max-height: 460px; }
}

@media (max-width: 900px) {
  :root { --sidebar-w: 60px; }
  .sidebar__link span:not(.sidebar__icon):not(.sidebar__badge) { display: none; }
  .sidebar__brand span { display: none; }
  .sidebar__section { display: none; }
  .sidebar__link { justify-content: center; }
  .sidebar__link::before { display: none; }
  /* 60px 的图标条放不下「时区 Asia/Shanghai」——它会挤成两行竖排的碎字，
     在本轮的 375 截图里就是那样。窄屏下整块隐藏：时区是一条参考信息，
     不是导航。 */
  .sidebar__footer { display: none; }
  .content { padding: var(--sp-4); }
  .messages { right: var(--sp-4); left: var(--sp-4); width: auto; }
}

/* 窄屏：权限矩阵折成卡片——两列 grid 在 640px 以下会把下拉压到 100px 以内，
   比换行更难用。折行后模块名在上、下拉框在下，各占整行。 */
@media (max-width: 640px) {
  .permission-row {
    grid-template-columns: 1fr;
    gap: var(--sp-2);
    padding: var(--sp-3) var(--sp-4);
  }
  .permission-row--head { display: none; }
  .settings-card__body { padding: var(--sp-4); }
  .permission-group__title { padding-left: var(--sp-4); padding-right: var(--sp-4); }
  .form-switch { grid-template-columns: 1fr auto; padding: var(--sp-3); }
  .board__column { flex-basis: 84vw; }
  .filter-bar { padding: var(--sp-3) var(--sp-4); }
  .filter-bar .field, .filter-bar .form-field { flex: 1 1 100%; }
  .form-actions { position: static; }
  .form-actions .btn { flex: 1 1 auto; }
  /* 窄屏：拖放区本来就已经是纵向居中的一列（CR-20260814-03），这里只收紧内边距，
     并把「选择文件」按钮拉宽到便于点击——手机上拖不了文件，这一屏的重点就是那个按钮。 */
  .file-field--ready .file-drop { padding: var(--sp-5) var(--sp-4); }
  .file-field--ready .file-drop__ui .btn { width: 100%; max-width: 260px; }
  /* 一行放不下「类型标 + 文件名 + 大小 + 移除」时，大小换到第二行跟在文件名下面，
     文件名仍然单行省略——绝不让长文件名把行撑开。
     **必须是 `stretch` 而不是 `flex-start`**：竖向排列时 `min-width: 0` 管不到
     交叉轴，`flex-start` 会让文件名收缩到内容宽度，于是长名字直接顶穿到
     「移除」按钮底下（375 实测过一次，被 .file-list 的 overflow:hidden 剪掉半截）。
     `stretch` 才让它拿到容器宽度，省略号也才有的可省。 */
  .file-card__body { flex-direction: column; align-items: stretch; gap: 0; }
  .file-card__meta { font-size: 11px; }
}

/* 拟题详情两段式：题目汇总区 + 方法与创新说明区（CR-20260721-01 / ADR-044）。
   一条题目一行，不再是六个厚重的大卡片；编号在两个区块严格一致。 */
.topic-list { list-style: none; margin: 0; padding: 0; }
.topic-list__item { display: flex; gap: var(--sp-3); padding: var(--sp-4) 0; border-top: 1px solid var(--c-border); }
.topic-list__item:first-child { border-top: 0; padding-top: 0; }
.topic-list__num {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.6rem; height: 1.6rem; border-radius: 999px;
  background: var(--c-primary-soft); color: var(--c-primary);
  font-size: var(--fs-sm); font-weight: 600;
}
.topic-list__body { flex: 1 1 auto; min-width: 0; }
.topic-list__zh { font-weight: 600; word-break: break-word; overflow-wrap: anywhere; }
/* 中英文题目共用同一内容容器（.topic-list__body），左边起点完全一致（CR-20260722-06 §三）：
   英文题目**不**用 margin-left/padding-left/空格缩进——中文加粗、英文用较小较弱的辅助文字，
   长标题换行后第二行仍与题目文本列对齐。剪贴板英文行首无空格由服务端去缩进 + JS 逐行 trimStart 保证。 */
.topic-list__en { color: var(--c-text-secondary); font-size: var(--fs-sm); margin-top: var(--sp-1); word-break: break-word; overflow-wrap: anywhere; }
/* 与历史产出的近似提示（CR-20260813-02 / ADR-097）。刻意做得很轻：
   它是一条参考信息，不是错误，不该比题目本身还醒目。 */
.topic-list__similar {
  display: flex; align-items: baseline; gap: var(--sp-2);
  flex-wrap: wrap;
  margin-top: var(--sp-2);
  font-size: var(--fs-sm);
}
.topic-list__field { margin-top: var(--sp-2); }
.topic-list__label { color: var(--c-text-muted); font-size: var(--fs-xs); }
.topic-list__value { font-size: var(--fs-sm); margin-top: 2px; white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; }
/* 创新说明：2—3 个编号短项（ADR-068）。紧凑、可换行、与题目汇总编号风格一致。 */
.topic-list__innovations { margin: 2px 0 0; padding-left: 1.4em; font-size: var(--fs-sm); }
.topic-list__innovations li { margin-top: 2px; }
/* 文档式有序/无序列表（运行逻辑与成本等只读说明页）。 */
.doc-list { margin: var(--sp-1) 0 0; padding-left: 1.5em; }
.doc-list li { margin-top: var(--sp-1); line-height: 1.6; }

/* 管理员「近期技术检索证据」全宽响应式卡片（CR-20260722-06 §四 / ADR-076）。
   桌面：全宽表格；中宽：仅在 .table-wrap 内横向滚动，页面 scrollWidth 不超 clientWidth；
   窄屏：切换为逐条证据卡片。外层 min-width:0，避免被 flex/grid 撑破。 */
.evidence-card { min-width: 0; margin-top: var(--sp-5); }
.evidence-stats {
  display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-5);
  padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--c-border);
}
.evidence-stat { display: inline-flex; flex-direction: column; min-width: 0; }
.evidence-stat__k { font-size: var(--fs-xs); color: var(--c-text-muted); }
.evidence-stat__v { font-size: var(--fs-sm); font-weight: 600; word-break: break-word; overflow-wrap: anywhere; }
.evidence-table-wrap { overflow-x: auto; min-width: 0; }
.evidence-table { min-width: 720px; }             /* 保证六列可读；不足视口时在 .table-wrap 内滚动 */
.evidence-col-title { min-width: 240px; white-space: normal; word-break: break-word; overflow-wrap: anywhere; }
.evidence-col-source { min-width: 140px; white-space: normal; overflow-wrap: anywhere; }
/* 窄屏证据卡片：默认隐藏（桌面看表格），仅在窄屏显示。 */
.evidence-cards { display: none; list-style: none; margin: 0; padding: 0; }
.evidence-card-item { padding: var(--sp-3) var(--sp-5); border-top: 1px solid var(--c-border); }
.evidence-card-item:first-child { border-top: 0; }
.evidence-card-item__title { font-size: var(--fs-sm); font-weight: 550; }
.evidence-card-item__meta { margin-top: 2px; overflow-wrap: anywhere; }
.evidence-card-item__q { margin-top: 2px; overflow-wrap: anywhere; }
@media (max-width: 640px) {
  .evidence-table-wrap { display: none; }
  .evidence-cards { display: block; }
}


/* --- 22. AI 拟题页（CR-20260814-02 §四 / ADR-105）----------------------- */
/* 本节只服务 `templates/ai_topics/generate.html` 一个页面。单独成节而不是
   往通用组件里塞，是因为这里的层级安排是**这一屏特有的**：一个高成本决定
   （期刊等级）+ 一个主输入 + 一堆可留空的修饰。别的表单页没有这个形状。 */

/* ⛔ `.tool-header*` 一组已删除（CR-20260815-17）。
 * 它是 AI 拟题页专用的带卡片底页头，全站独一份。CR-20260815-16 把
 * 「图标 + 标题 + 一句说明」收成了 `.page-header__mark` 并铺到所有落地页之后，
 * 这一组就成了同一件事的第二套写法，且只有一个调用方。已改用 `.page-header`。 */

/* 拟题详情页尾部的管理员卡片（检索证据、中文核心资产）与**左栏对齐**
 * （CR-20260816-01 / 用户裁定「不该强制占用两列的宽度」）。
 *
 * 它们在 DOM 上位于 `.grid-sidebar` 之外，因此默认占满整幅宽度——
 * 而它们是拟题结果的补充材料，属于左边那条阅读线。右栏的输入/要素/附件
 * 停在上面，尾部却突然变宽，读起来像换了一页。
 *
 * ## 为什么用宽度约束而不是把 DOM 搬进左栏
 * 那一段是**两张卡交织在一个条件块**里（`{% if is_admin and ... %}` 跨越了
 * 检索证据与中文核心资产两张卡）。按行剪切必然带走半个条件块——
 * 本轮试过一次，`{% endif %}` 被带走后「调用与成本」整块跟着消失。
 * 宽度约束达到同样的视觉结果，且不碰模板结构。
 *
 * 宽度取值与 `.grid-sidebar` 的定义一致：总宽 − 右栏 336px − 一个 gap。 */
.evidence-card {
  max-width: calc(100% - 336px - var(--sp-5));
}
@media (max-width: 1080px) {
  /* 右栏在这个断点堆叠到下方，左栏恢复满宽——约束随之取消。 */
  .evidence-card { max-width: 100%; }
}

/* 两栏：表单是主体，右栏是参考。1080px 以下堆叠。 */
.topic-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  gap: var(--sp-5);
  align-items: start;
}

/* 表单本体是**一张**面，不是三张卡片叠在一起——三个步骤属于同一件事。 */
/* **不要在这里写 `overflow: hidden`。** 它会让 `.topic-form` 成为
   `position: sticky` 的滚动容器，而这个容器自己不滚动——底栏因此永远粘不住，
   在 720px 视口下提交按钮又回到了首屏之外。圆角改由子元素自己收边。 */
.topic-form {
  min-width: 0;
  border: 1px solid var(--c-border);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
  box-shadow: var(--shadow-sm);
}
.topic-form > .topic-step:first-of-type { border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
.topic-form > .alert { margin: var(--sp-5) var(--sp-6) 0; }

.topic-step { padding: var(--sp-5) var(--sp-6); }
.topic-step + .topic-step { border-top: 1px solid var(--c-border); }
.topic-step__head { display: flex; align-items: flex-start; gap: var(--sp-3); margin-bottom: var(--sp-4); }
.topic-step__num {
  flex: 0 0 auto;
  width: 22px; height: 22px; margin-top: 1px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--c-surface-sunken); color: var(--c-text-secondary);
  border: 1px solid var(--c-border);
  font-size: var(--fs-xs); font-weight: 600; font-variant-numeric: tabular-nums;
}
.topic-step__title { font-size: var(--fs-md); font-weight: 600; line-height: 1.4; margin: 0; }
.topic-step__desc {
  margin: 2px 0 0; font-size: var(--fs-xs); color: var(--c-text-muted); line-height: 1.55;
}
.topic-step__body { min-width: 0; }
/* 主输入去掉**视觉上**重复的字段标签——步骤标题已经承担了标签的作用。
   刻意用视觉隐藏而不是 `display:none`：按 accname 规范，隐藏的 <label for>
   不参与可访问名称计算，于是那个 textarea 在读屏器里会**没有名字**。
   这里的写法仍在无障碍树内，读屏器照常朗读「研究方向或拟研究内容」。 */
.form-field--bare > label {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.topic-step__body textarea { min-height: 148px; }

/* 期刊等级选择卡片。原生 radio 仍是唯一提交元素与焦点目标，
   `:has(:checked)` 只负责外观；不支持 :has() 的浏览器退化成
   「卡片外观 + 可见的原生 radio」，仍然可用可选。 */
.tier-picker {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(158px, 1fr));
  gap: var(--sp-3);
  border: 0; padding: 0; margin: 0;
}
.tier-option {
  position: relative;
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius);
  background: var(--c-surface);
  cursor: pointer;
  transition: border-color var(--transition), background var(--transition), box-shadow var(--transition);
}
.tier-option:hover { border-color: var(--c-primary-border); background: var(--c-hover); }
.tier-option input[type="radio"] {
  width: 15px; height: 15px; min-height: 0; padding: 0; margin: 0;
  flex: 0 0 auto; accent-color: var(--c-primary); cursor: pointer;
}
.tier-option__body { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.tier-option__label { font-size: var(--fs-base); font-weight: 550; color: var(--c-text); line-height: 1.35; }
.tier-option__note { font-size: var(--fs-xs); color: var(--c-text-muted); line-height: 1.45; margin-top: 1px; }
/* 选中态**只靠面本身**表达：主色边框 + 浅主色底 + 选中的原生 radio。
   上一版在选中时 `display:none` 掉 radio、另画一个对勾图标——那会把这个 radio
   从 Tab 序与无障碍树里一起摘掉，于是选中之后键盘再也回不到它上面，
   读屏器也读不到当前选了哪一项。原生控件必须始终可见、可聚焦。 */
.tier-option:has(input:checked) {
  border-color: var(--c-primary);
  background: var(--c-primary-soft);
  box-shadow: inset 0 0 0 1px var(--c-primary);
}
.tier-option:has(input:checked) .tier-option__label { color: var(--c-primary-active); }
.tier-option:has(input:focus-visible) { outline: 2px solid var(--c-primary); outline-offset: 2px; }

/* 补充信息折叠（delta：默认展开，仍可随时收起）。
   `<summary>` 直接**就是**第三步的标题行，所以这里不再画盒子——默认展开之后
   「标题行 + 一条另起的展开横条 + 字段」会叠成三层同义信息，而现在只有一层。
   与前两步保持同一套 `.topic-step__num / __title / __desc`，层级因此完全一致，
   唯一多出来的是右端那枚随开合旋转的 chevron。 */
.topic-extra { border: 0; border-radius: 0; background: none; }
/* Grid 而不是 Flex：`<summary>` 只允许 phrasing content，说明文字不能和标题
   一起塞进一个包裹元素里再换行，只能作为第四个直接子元素自己占第二行。
   两行三列把序号、标题、箭头、说明摆成与前两步完全一致的对齐关系，
   说明文字的左缩进由 grid 列自动对齐，不写死「序号宽 + 间距」这种魔数。 */
.topic-extra__summary {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: start;
  column-gap: var(--sp-3); row-gap: 2px;
  padding: 0; cursor: pointer; list-style: none;
  border-radius: var(--radius-sm);
}
.topic-extra__summary::-webkit-details-marker { display: none; }
.topic-extra__summary:hover .topic-step__title { color: var(--c-primary-active); }
.topic-extra__summary:hover .icon { color: var(--c-text-secondary); }
.topic-extra__summary > .topic-step__num { grid-area: 1 / 1; }
.topic-extra__summary > .topic-step__title { grid-area: 1 / 2; }
.topic-extra__summary > .icon {
  grid-area: 1 / 3;
  width: 16px; height: 16px; margin-top: 3px;
  color: var(--c-text-muted); transition: transform var(--transition);
}
.topic-extra__summary > .topic-step__desc { grid-area: 2 / 2 / 3 / 4; margin: 0; }
.topic-extra[open] > .topic-extra__summary > .icon { transform: rotate(180deg); }
/* 展开后的字段与标题行之间用间距分层，不再用边框——边框会把这一步重新框成
   一张独立的卡片，而它和前两步属于同一张表单面。 */
.topic-extra__body { padding-top: var(--sp-4); }

/* 表单底栏：提交按钮常驻，不必滚到页面末尾。粘在视口底部（sticky），
   不用 fixed——fixed 会盖住页面其它内容，而这一栏属于表单。 */
.topic-form__footer {
  position: sticky; bottom: 0; z-index: 2;
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  flex-wrap: wrap;
  padding: var(--sp-4) var(--sp-6);
  border-top: 1px solid var(--c-border);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  background: var(--c-surface-alt);
}
.topic-form__hint {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--fs-xs); color: var(--c-text-muted); line-height: 1.5; min-width: 0;
}
.topic-form__hint .icon { width: 14px; height: 14px; flex: 0 0 auto; }

/* 右栏 */
.topic-rail { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }
.rail-card {
  border: 1px solid var(--c-border); border-radius: var(--radius-lg);
  background: var(--c-surface); box-shadow: var(--shadow-sm); overflow: hidden;
}
.rail-card__head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--c-border);
}
.rail-card__title { font-size: var(--fs-base); font-weight: 600; margin: 0; }

/* 最近拟题：状态点带头的列表，不是密排表格。整行可点，行内不嵌第二个交互元素。 */
.recent-list { list-style: none; margin: 0; padding: 0; }
.recent-item + .recent-item { border-top: 1px solid var(--c-border); }
.recent-item__link {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  color: inherit; text-decoration: none;
  transition: background var(--transition);
}
a.recent-item__link:hover { background: var(--c-hover); text-decoration: none; color: inherit; }
a.recent-item__link:focus-visible { outline: 2px solid var(--c-primary); outline-offset: -2px; }
.recent-item__link--plain { cursor: default; }
.recent-item__dot {
  width: 7px; height: 7px; margin-top: 6px; flex: 0 0 auto;
  border-radius: 50%; background: var(--c-text-muted);
}
.recent-item--success .recent-item__dot { background: var(--c-success); }
.recent-item--danger .recent-item__dot { background: var(--c-danger); }
.recent-item--warning .recent-item__dot { background: var(--c-warning); }
.recent-item--info .recent-item__dot { background: var(--c-info); }
.recent-item__body { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.recent-item__title {
  font-size: var(--fs-sm); line-height: 1.45; color: var(--c-text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.recent-item__meta { font-size: var(--fs-xs); color: var(--c-text-muted); margin-top: 2px; line-height: 1.4; }
.recent-item__status { font-size: var(--fs-xs); color: var(--c-text-secondary); flex: 0 0 auto; margin-top: 1px; }
.recent-item--danger .recent-item__status { color: var(--c-danger); }
.recent-item--success .recent-item__status { color: var(--c-success); }

/* 「发送给模型的内容」：ADR-100 的折叠形态保留，**默认展开**（delta）。
   视觉权重刻意压在「最近拟题」之下、更在左侧表单之下：下沉底色、无投影、
   标题 fs-sm，而 `.rail-card` 是 surface 底 + 投影 + fs-base/600 的标题。 */
.rail-note { border: 1px solid var(--c-border); border-radius: var(--radius-lg); background: var(--c-surface-alt); }
.rail-note__summary {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-sm); font-weight: 550; color: var(--c-text-secondary);
  cursor: pointer; list-style: none; border-radius: var(--radius-lg);
}
.rail-note__summary::-webkit-details-marker { display: none; }
.rail-note__summary:hover { color: var(--c-text); }
.rail-note__summary .icon { width: 15px; height: 15px; color: var(--c-text-muted); flex: 0 0 auto; }
.rail-note__summary .icon:last-child { margin-left: auto; transition: transform var(--transition); }
/* 展开后标题行与正文之间画一条分隔线：四条事实本来就该读成清单而不是一段话，
   没有这条线时标题会和第一条粘在一起、整块看着像一段连续说明。 */
.rail-note[open] > .rail-note__summary {
  border-bottom: 1px solid var(--c-border);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.rail-note[open] > .rail-note__summary .icon:last-child { transform: rotate(180deg); }
.rail-note .fact-list { padding: var(--sp-3) var(--sp-4) var(--sp-4); margin: 0; }

@media (max-width: 1080px) {
  .topic-layout { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 640px) {
  .tool-header { padding: var(--sp-4); gap: var(--sp-3); flex-wrap: wrap; }
  .tool-header__actions { width: 100%; }
  .topic-step { padding: var(--sp-4); }
  .topic-form__footer { padding: var(--sp-3) var(--sp-4); }
  .topic-form__footer .btn { width: 100%; justify-content: center; }
}

@media print {
  .sidebar, .topbar, .page-header__actions, .form-actions, .pagination, .messages { display: none !important; }
  body { background: #fff; }
  .card { border: 1px solid #ccc; box-shadow: none; page-break-inside: avoid; }
}
