  :root {
    --border: #e2e5ea;
    --border-strong: #cbd2dc;
    /* 题卡专属边框色（浏览页题目卡片 + 右栏调用区题卡共用一套，保证两边一致）：
       比 --border 深一档、比 --border-strong 浅一档 —— 看得清但不刺眼。
       **--border 本身不动**：面板分隔线、文件树搜索框、标签栏都靠它，改了会全站一起变。 */
    --card-border: #d0d7e1;
    --card-border-hover: #b6bfcd;
    --bg: #f5f6f8;
    --panel: #ffffff;
    /* 「非激活面」底色（2026-10-08）：已关闭 / 失效 / 归档类容器专用。
       比白卡（--panel #fff）明确暗一档 → 一眼"沉下去"；也比页面底（--bg #f5f6f8）深且偏冷
       （三通道 rgb(238,241,246)，蓝最高 = 冷灰，不是暖灰）。
       值取站内多处沿袭的 #eef1f6（.ds-tabs / 面包屑 hover / 表单控件底色），不是新造色。 */
    --inactive-bg: #eef1f6;
    --text: #1f2430;
    --muted: #6b7280;
    --accent: #2f6fed;
    --accent-soft: #eaf1ff;
    --a-color: #1a7f37;
    --b-color: #b26a00;
    --c-color: #b3261e;
    --j-color: #6f42c1;
    /* 「作者」这一支的紫：跟灰（其它次要信息）、蓝（能点的按钮 / 选中的行）都分得开，
      审核页扫描"这条是谁交的"时一眼能挑出来。审核列表 / 详情头 / 我的草稿共用这一支。 */
    --author-color: #6f42c1;
    /* Cmd+点击跳转的「高亮」色：**源码页那行的底色**与**排版预览里那些黄框**
       必须看起来是同一个颜色（用户要求），所以两边只认这几个变量、别的地方不许再写死。
       色相与浓度分开：色相不透明，浓度放在外层 —— 这样"同一个框被引擎重复报很多次"
       叠在一起也只是同色叠同色，颜色不会被乘深（见 .rpreview-pdfflash 注释）。
       `#feedc1` 是浅黄：用户挑的那张参考图里高亮处的实测像素就是 rgb(254,237,193)。 */
    --sync-flash: #feedc1;
    --sync-flash-alpha: 1;
    --sync-flash-edge: #e9c46a;
    /* 代码编辑器（CodeMirror 5）三件套 —— 照 TeXstudio 的观感：Consolas 16px / 行高 18.5px。
       只被这四处引用：源码页（.handout-editor-host）、题目内容/TikZ/答案 6 个框（.code-edit）、
       补全弹窗（.CodeMirror-hints）、源码页 Cmd-F 的查找框输入（.cm-dialog-input）。
       改这三行 = 全站代码框一起变（其余等宽用法 —— 题号 ID / 电话号 / 工作区路径 —— 不引用它）。
       ⚠️ 字体栈：Consolas 排第一、现有那套兜底。macOS **不自带** Consolas（本机这份是 Office
       装的：/Library/Fonts/consola.ttf），没装的机器自动落到 SF Mono / Menlo，不会掉字。
       ⚠️ 行高**必须写死 px**（不能 normal / 117%）：站内代码是中英混排，Consolas 没有中文字形 →
       中文回退到苹方，而行盒高度按"这一行里最高的那个字体"算 —— 实测 Chrome 下纯英文行 18.5px、
       含中文行 22.5px（一行高一行低）；写死 18.5px 则两边都是 18.5px。
       18.5px ≈ 1.16em，正是 TeXstudio「行距 100%」那一档（= 字体的自然行距，实测 18.75px）。
       ⚠️ 最后动过的一次（2026-10-15）：西文换成 **"CodeConsolas"**（Consolas 缩 91%）——
       见下面那两条 @font-face。2026-10-16 又补了 **"CodeMenlo"**（Menlo 缩 83.1%），
       两族的**字宽完全相同**（都是 8.005px/字）：Safari 拿不到 Consolas 时靠它兜住，
       Chrome 照旧用真 Consolas。 */
    --code-font: "CodeConsolas", "CodeMenlo", "CodeCJK",
      Consolas, ui-monospace, SFMono-Regular, Menlo, monospace;
    --code-size: 16px;
    --code-line: 18.5px;
    /* 代码框语法配色 —— 抄 TeXstudio 4.4.1 的出厂配色（色号取自本机
       ~/.config/texstudio/texstudio.ini 的 [formats] 段，2026-10-15 对齐）。
       只被两个代码编辑器引用：题目内容/TikZ/答案（.code-edit）与源码页（.handout-editor-host）。 */
    --code-fg-command: #800000;     /* keyword        —— 普通命令 \storebackup \item \Qnum */
    --code-fg-env: #000080;         /* environment    —— 环境名 myenumerate */
    --code-fg-math: #008000;        /* numbers        —— 数学里的字母与数字（TeXstudio 里同色）*/
    --code-fg-math-delim: #509600;  /* math-delimiter —— 数学定界符 $ */
    --code-fg-comment: #808080;     /* comment        —— 注释 */
    --code-fg-bracket: #1f2430;     /* 括号：TeXstudio 不染色 → 跟正文同色 */
    /* extra-keyword —— `\begin`/`\end`：TeXstudio 独一档（亮蓝 **+ 加粗**）。
       基底把 `\begin` 和 `\item` 归成同一个 tag，只能靠 stex-tiku 那层 mode 单独打类。 */
    --code-fg-extracmd: #0095ff;
    /* 命令参数里的数字（`{13}` 的 13、`[2015]` 的 2015）：TeXstudio **不染色** → 跟正文同色。
       基底把"环境名"和"参数数字"都吐成 atom，靠 stex-tiku 把环境名单独摘出去（见 --code-fg-env）。 */
    --code-fg-arg: #1f2430;
    /* math-keyword —— **数学模式内**的命令（`\triangle`、`\frac`…）：TeXstudio 独一档的橄榄色。
       判据实测：它那屏里 `$\frac{x^{2}}{6}$` 的 `\frac` 也是橄榄色 → 是"在 $…$ 里"，不是固定命令表。 */
    --code-fg-mathcmd: #808000;
    /* align-ampersand —— 表格/对齐环境的 `&`：TeXstudio 是亮蓝 + 加粗（ini 里的 #0055ff）。 */
    --code-fg-amp: #0055ff;
    /* math-text —— 数学里 `\text{…}` 盒子中的内容（含中文）与那对 `{}`。
       ⚠️ 值取**实测**而不是 ini：用户同屏截图里 TeXstudio 把它渲染成和数学内容一样的绿
       （不是 ini 的 math-text #202000）。顺带解决：基底把数学里的中文标成 cm-error（红字）。 */
    --code-fg-mathtext: #008000;
    /* TikZ（tikzpicture 环境内）两档 —— 2026-10-16 拿用户两张真机截图**逐段量像素**定的：
       命令名（`\tkzDrawSegments`…）走 picture-keyword，它的参数（`{…}`/`[…]`/`(…)` 里）走 picture。 */
    --code-fg-tikzcmd: #c06000;    /* picture-keyword */
    --code-fg-tikzarg: #804000;    /* picture */
    /* 章节标题（`\chapter{…}` 里那串字）：TeXstudio 的 structure = 黑 + 加粗（用户截图确认）*/
    --code-fg-structtitle: #000000;
  }

  /* 代码框的西文字体：把宿主字体外面包一层，用 `size-adjust` 只缩**西文**（中文不受影响）。
     目标字宽 = **8.005px/字**，来自 TeXstudio 那屏的实测：同一串 `\begin{myenumerate}`
     （19 字符）它 306 设备像素、我们原来 336 → 比值 0.911。

       · CodeConsolas：Consolas（16px 步进 8.797）× 91% = 8.005 —— **Chrome / Windows 走这条**
       · CodeMenlo   ：Menlo    （16px 步进 9.633）× 83.1% = 8.005 —— **Safari 走这条**
       · CodeCJK     ：苹方     （16px 步进 16）× 93.75% = 15 —— **中文单独缩到 15px**
         （2026-10-16 用户要求"中文 15px、西文按当前的不动"：所以西文那两条**一个字节没改**，
          中文另开一族按 93.75% 缩。中日韩字形只在这族里有 → 只影响中文，西文不受影响。）
     ⚠️ 为什么要两条：**macOS 上的 Safari 根本不认我们那份 Consolas** —— 2026-10-16 用真机
     （Safari 26.6.2，`screencapture` 量像素）实测：连 `font-family: Consolas` 它都落到 Menlo
     （9.598px/字），`local("Consolas")` 更不会命中 → 上一轮只写 Consolas 时 Safari 等于没缩。
     而 `local("Menlo")` + `size-adjust` 在 Safari 上实测完全正常（Menlo×50% = 4.817 分毫不差）。
     所以给 Safari 单独铺一条"同一字宽"的路：**宽度一致，字形是 Menlo 的**（不是 Consolas 的）。
     （想让 Safari 也用真 Consolas：把 consola.ttf 用「字体册」正式安装一次即可，那条规则就生效。）
     ⚠️ Consolas / Menlo 里都没有中文字形 → 中文走后面的回退字体（苹方），**仍是 16px、一个字不缩**。
     这正是要的效果 —— TeXstudio 那边也是"西文小、中文大"的混搭。
     ⚠️ `size-adjust` 要较新的内核（Chrome 92+ / Safari 17+）；更老的内核会忽略它 → 退化成"不缩"。 */
  @font-face {
    font-family: "CodeConsolas";
    src: local("Consolas");
    size-adjust: 91%;
  }
  @font-face {
    font-family: "CodeMenlo";
    src: local("Menlo");
    size-adjust: 83.1%;
  }
  /* 加粗档（只有 `\begin`/`\end` 这一档用）：给两族各补一个**真粗体面**。
     ⚠️ 不补的话：WebKit 会对单面 @font-face 做**合成粗体**，字宽被撑大 5%
     （2026-10-16 实测：19 字符那串 328.211 → 344.794），把之前对齐的字宽毁掉。
     补上真 Menlo Bold 后宽度与常规完全一致（对照实验：纯 Menlo 400/700 都是 394.945）。
     Chrome 这边本机没装 Consolas Bold（只有 consola.ttf）→ 走合成粗体，但实测**宽度不变**
     （400/500/600/700 全是 328.219），所以两边都不会让行变宽。 */
  @font-face {
    font-family: "CodeConsolas";
    src: local("Consolas Bold"), local("Consolas-Bold");
    size-adjust: 91%;
    font-weight: 700;
  }
  @font-face {
    font-family: "CodeMenlo";
    src: local("Menlo Bold"), local("Menlo-Bold");
    size-adjust: 83.1%;
    font-weight: 700;
  }
  /* 中文（回退字体）：15px —— 把苹方单独包一族、按 93.75% 缩（16 × 0.9375 = 15）。
     为什么单开一族：上面两条管西文，这条只管中日韩；`--code-size` 仍是 16px，
     西文的 91% / 83.1% 一个字节都没动（用户 2026-10-16："西文先按当前的不动"）。
     ⚠️ 苹方没命中（老系统/别的平台）时这一族不可用 → 中文退回浏览器最后的 CJK 字体、
       按 16px 渲染（= 改之前的样子），不会坏。 */
  @font-face {
    font-family: "CodeCJK";
    src: local("PingFang SC"), local("PingFangSC-Regular"), local("Hiragino Sans GB");
    size-adjust: 93.75%;
  }
  /* ⚠️ 中文也要一档**真粗体面**（用户 2026-10-16："标题里的中文 TeXstudio 更明显"）：
     只声明 400 面时，Chrome **不会**给这一族合成粗体（西文那两族有真 700 面所以正常），
     于是 `font-weight: 700` 的中文看着没变粗。这里指到 PingFang 的 Semibold；
     找不到就退回 400 面（浏览器可能合成），不会掉字。 */
  @font-face {
    font-family: "CodeCJK";
    src: local("PingFangSC-Semibold"), local("PingFang SC Semibold"),
         local("PingFang SC"), local("PingFangSC-Regular");
    size-adjust: 93.75%;
    font-weight: 700;
  }

  * { box-sizing: border-box; }

  body {
    margin: 0;
    height: 100%;
    /* 整页固定：系统大页面不许上下滚，滚动一律发生在内部（左/中/右三栏各自的滚动区）。
       原来这里是 height:100% 都没有、.layout 又写死 calc(100vh - 45px)，而顶部工具栏
       实际高 49px —— 49 + (100vh-45) = 100vh + 4px，外层就多出 4px 可滚
       （实测 html.scrollHeight 1004 / clientHeight 1000）。 */
    overflow: hidden;
    background: var(--bg);
    color: var(--text);
    font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB",
      "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
    font-size: 14px;
    line-height: 1.7;
  }
  html { height: 100%; overflow: hidden; }
  #app { height: 100%; display: flex; flex-direction: column; overflow: hidden; }

  .topbar {
    display: flex;
    align-items: baseline;
    gap: 12px;
    padding: 10px 16px;
    background: var(--panel);
    /* 顶栏与内容区的分界线：比普通面板线深一档。Safari 某些缩放/Retina 组合下
       #e2e5ea 的 1px 线会被抹得几乎看不见（MacBook 实测），这里用 --border-strong
       保证两台机器、普通窗口和网页 App 都能看到同一条线。 */
    border-bottom: 1px solid var(--border-strong);
  }
  /* 网页 App 顶栏统一（2026-10-19）：Safari「添加到程序坞」和 Chrome「安装为应用」
     都直接使用浏览器自己的窗口标题栏。网页不再自绘 28px 标题栏，也不再额外加
     padding —— 之前按 Safari 26 的“无标题栏”行为补的那一条，在 Safari 27 上会和
     系统标题栏叠成两条，所以整段撤掉，统一走 Chrome 那套基准布局。 */
  /* 标题**不折行**：它是定长的一段字（"数学题库工作台"），弹性的那部分是右边的
     hint / 搜索框。不加 nowrap 的话，顶栏一拥挤就会把标题折成两行，很难看，
     而且"标题定长 → 左边按钮的 x 不随动态数字变"这条既有前提也就失效了。 */
  /* 双击这个标题 = 回工作台（审核页 / 后台管理区都适用）：所以别让双击选中文字 ——
     选中一片高亮再"顺手返回"看着像误操作。 */
  .topbar h1 {
    margin: 0;
    font-size: 16px;
    font-weight: 600;
    white-space: nowrap;
    flex: none;
    user-select: none;
    -webkit-user-select: none;
  }
  /* 顶栏窄了（非全屏 / 窗口拉小）时这行统计**不许竖排**：中文随处可断，不给
     nowrap 的话会被压成一列单字（实测 1200 宽就是那样）。放不下就出省略号，
     整条被挤走比"一个一个字往下掉"体面得多。min-width:0 是关键 ——
     flex 子项默认不肯缩到内容宽度以下。 */
  .topbar .hint {
    color: var(--muted);
    font-size: 12px;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* 「今日新增 N」用与批量页「已入库：」同一支绿（--a-color）：
     那两个数是一回事（今天入库了几道），颜色一致读起来才对得上。 */
  .topbar .hint .today-new { color: var(--a-color); font-weight: 600; }
  /* 顶栏靠右的留白：**有「找题」模块时**由它独占（.app-switch 的 margin-left:auto），
     那样「找题模块 + 搜索框」整组贴右、两者之间保持顶栏统一间距（12px）。
     没有那一块（账号级能力为假）时，这份职责交给搜索框（见 .search-box.push-right）。 */
  /* 搜索框宽度抽成变量：下面的「已选范围」框要跟它同宽，改一处两边一起走 */
  .search-box { display: flex; align-items: center; gap: 6px; --search-w: 320px; }
  .search-box.push-right { margin-left: auto; }
  .search-box input {
    width: var(--search-w);
    padding: 5px 10px;
    border: 1px solid var(--border-strong);
    border-radius: 16px;
    font-size: 13px;
    outline: none;
  }
  .search-box input:focus { border-color: var(--accent); }
  /* 自定义清空按钮：绝对定位在输入框内部，不参与布局，输入框宽度恒定 */
  .search-input { position: relative; display: inline-flex; }
  /* 左边 58px：让出框内那个「全库 / 局部」胶囊的位置 + 一个数字的余量
     （外框宽高不变；去掉 chip 的三角后 chip 变窄了 7px，这里跟着收 8px，
     让输入文字离 chip 近一点、又不贴上去） */
  .search-input input { padding-left: 58px; padding-right: 28px; }
  /* 清空按钮：两根细线拼 ✕，用 translate(-50%,-50%) 做几何居中，
     外观对齐输入框自带的原生清除按钮（无背景、无边框） */
  /* 选择器带 .search-input，优先级高于上面通用的 .search-box button，
     确保 padding / border / background 被真正清掉 */
  .search-input .search-clear {
    position: absolute;
    right: 6px;
    top: 50%;
    transform: translateY(-50%);
    width: 18px;
    height: 18px;
    padding: 0;
    border: none;
    background: none;
    box-shadow: none;
    cursor: pointer;
  }
  .search-input .search-clear::before,
  .search-input .search-clear::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    width: 9px;
    height: 1.4px;
    background: #8a9199;
    border-radius: 1px;
  }
  .search-input .search-clear::before { transform: translate(-50%, -50%) rotate(45deg); }
  .search-input .search-clear::after { transform: translate(-50%, -50%) rotate(-45deg); }
  .search-input .search-clear:hover::before,
  .search-input .search-clear:hover::after { background: #4a5464; }
  /* 隐藏浏览器自带的 ×，避免出现两个清除按钮 */
  .search-input input::-webkit-search-cancel-button { display: none; }
  .search-box button {
    padding: 4px 10px;
    border: 1px solid var(--border-strong);
    border-radius: 14px;
    background: #fff;
    color: var(--muted);
    cursor: pointer;
    font-size: 12px;
  }
  .search-box button:hover { background: #f2f5fa; color: var(--text); }
  /* 搜索按钮沿用主色，与「保存」同色系 */
  .search-box button.primary {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
  }
  .search-box button.primary:hover { background: #2560d4; color: #fff; }
  /* 关键词为空时搜索按钮禁用：灰底、不再有悬停手势 */
  /* 2026-10-08：**全站"搜索"类按钮的禁用态统一走这一条** —— 用户要求长得一样
     （淡蓝底 + 白字 + 半透明），所以是"同一处声明、多个选择器共用"，不是抄几份数值过去：
       · 顶栏「搜索」(.search-box button.primary)
       · 工作空间 → 其他工作区 的「接收」(.subtab-search .subtab-receive)
       · 真题卷集 → 试卷库 / 待审区 的两颗「搜索」(.ds-search-go)
     **每个都要带 `:disabled:hover`**（2026-10-08 修"禁用态悬停变白底黑字"）：
     通用按钮库里本来就有 `.admin-refresh:disabled:hover{background:#fff;color:#2c3444}`（:4613）
     这种"禁用悬停"规则，与这里**同为 (0,3,0)**、谁写在后面谁赢 —— 光写 `:disabled`
     会在鼠标一悬停时被它盖成白底黑字。加上 `:disabled:hover`（各 +1 伪类）就稳压一头，
     顺带把 cursor 也钉成 not-allowed（不被 `.admin-refresh:disabled{cursor:default}` 抢走）。
     ⚠️ 真题卷集那颗要带够 .ds-btn.ds-btn-blue 两段前缀：后面的
     `.ds-btn.ds-btn-blue:disabled`（:6173，灰底灰字）同优先级、写在后面，不加会被它盖掉。 */
  .search-box button.primary:disabled,
  .search-box button.primary:disabled:hover,
  .subtab-search .subtab-receive:disabled,
  .subtab-search .subtab-receive:disabled:hover,
  .ds-btn.ds-btn-blue.ds-search-go:disabled:hover,
  .ds-btn.ds-btn-blue.ds-search-go:disabled {
    background: #c8d2e4;
    border-color: #c8d2e4;
    color: #fff;
    opacity: 0.6;
    cursor: not-allowed;
  }

  /* ---- 搜索范围选择器（全库 / 局部）------------------------------------
     硬约束：搜索框外框的宽高一个像素都不许变。所以选择器做成「输入框内部左侧的
     绝对定位胶囊」，输入框只让出 padding-left（全局 box-sizing: border-box，
     改 padding 不动外框尺寸）；浮层也是绝对定位，不参与布局。

     另外：这里的每条选择器都带 .search-box 前缀，优先级高于上面通用的
     .search-box input / .search-box button —— 否则浮层里的 radio 会被撑成 320px 宽。
     同一个坑 .search-input .search-clear 那套已经踩过一次，照它的写法办。 */
  /* 撑满输入框高度 + flex 居中：chip 与输入框**几何中心完全重合**
     （上一版用 top:50% + translateY(-50%)，因为外层行盒比输入框高，实测偏了 1.85px） */
  .search-box .scope-wrap {
    position: absolute;
    left: 5px;
    top: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    z-index: 6;
  }
  .search-box .scope-chip {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    /* 上限比让出的 58px 更小：文字区不会撞上 chip（当前文案「全库/局部」实测 35px） */
    max-width: 50px;
    height: 19px;
    padding: 0 6px;
    border: none;
    border-radius: 10px;
    /* 「全库」态：加深一档的灰（原来是 #eef1f6，和面板底色几乎分不出来） */
    background: #e4e9f0;
    color: #4a5464;
    font-size: 11.5px;
    font-weight: 500;
    line-height: 1;
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
  }
  .search-box .scope-chip:hover { background: #d8dee8; }
  /* 局部（有选中节点）= 主色；局部但一个节点都没选 = 提示色，提醒先点左树 */
  .search-box .scope-chip.local { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
  .search-box .scope-chip.local:hover { background: #dde8ff; }
  .search-box .scope-chip.empty { background: #fdf1e6; color: #a15a12; font-weight: 600; }
  .search-box .scope-chip.empty:hover { background: #f9e6d5; }

  /* ---- 已选范围框：横排换行、与搜索框同宽 -------------------------------
     显示时机（三条都由 .scope-wrap 上的 hover + 一个 3 秒定时器驱动）：
       1) 选成多选后自动出现，约 3 秒后自己消失（不打扰）
       2) 鼠标悬停到「局部」chip 上（不用点）→ 随时再看
       3) 鼠标移进框里 → 不消失；移出 chip 和框 → 消失
     宽度 = var(--search-w)，与输入框同宽（同一个变量，天然一致）。 */
  .search-box .scope-range {
    position: absolute;
    left: -5px;
    top: calc(100% + 8px);
    width: var(--search-w);
    padding: 8px 8px 9px;
    background: #fff;
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    box-shadow: 0 8px 20px rgba(31, 36, 48, 0.14);
    text-align: left;
    cursor: default;
  }
  /* hover 连贯用的"透明桥"：正好填满 chip 下沿到框上沿之间那 8px，
     它是 .scope-wrap 的子元素 —— 鼠标从 chip 往下走时始终没离开这个子树，
     所以不会中途触发 mouseleave 把框关掉。 */
  .search-box .scope-range-bridge {
    position: absolute;
    left: -5px;
    top: 100%;
    width: var(--search-w);
    height: 8px;
  }
  .search-box .scope-range-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    color: var(--muted);
    font-size: 11.5px;
    margin-bottom: 6px;
  }
  .search-box .scope-range .scope-clear {
    border: none;
    background: none;
    box-shadow: none;
    padding: 1px 5px;
    border-radius: 5px;
    color: var(--accent);
    font-size: 11.5px;
    line-height: 1.4;
    cursor: pointer;
    flex: none;
  }
  .search-box .scope-range .scope-clear:hover { background: var(--accent-soft); }
  .search-box .scope-range .scope-clear:disabled { color: #b7bdc7; cursor: not-allowed; }
  .search-box .scope-range .scope-clear:disabled:hover { background: none; }
  /* 局部但还没点过左树：范围框里只显示这行提示 */
  .search-box .scope-range-hint { color: #a15a12; font-size: 11.5px; line-height: 1.5; }
  .search-box .scope-chips { display: flex; flex-wrap: wrap; gap: 4px; }
  .search-box .scope-pill {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    max-width: 100%;
    padding: 2px 4px 2px 7px;
    border-radius: 8px;
    background: var(--accent-soft);
    color: var(--accent);
    font-size: 11.5px;
  }
  .search-box .scope-pill-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .search-box .scope-pill button {
    border: none;
    background: none;
    box-shadow: none;
    padding: 0 2px;
    color: inherit;
    font-size: 12px;
    line-height: 1;
    cursor: pointer;
    opacity: 0.7;
  }
  .search-box .scope-pill button:hover { opacity: 1; }

  .layout {
    display: grid;
    /* 四列：左树 --tree-w｜中栏（下限 --mid-min）｜分隔条 --splitter-w｜右栏。
       宽度全抽成变量，是为了"收起右栏"那条规则只改列数、不改数字——
       否则 260/223 这些数要在 .right-hidden 和媒体查询里各抄一份。
       --right-w 只在用户拖过分隔条之后才有值（内联样式写在 .layout 上，见 colSplitStyle），
       没拖过时走下面的 4:3 默认比例。

       右栏曾经写死 612px：那是给定值（简版调用第一行"日期/班级/来源/解析/风格"五块
       要 577px 内容宽 = 栏宽 ≥599px，实测 590 折行、600 起才一行）。现在默认改成
       4:3 比例（用户要求），宽屏下右栏会比 612 更宽、更不会折行；窄屏下中栏先触到
       --mid-min、右栏再触到 --right-min，那时确实会折行，这是比例的固有代价。 */
    --tree-w: 260px;
    --mid-min: 380px;
    --splitter-w: 6px;
    /* 右栏下限：与拖动时分隔条那条限制（JS 的 COL_RIGHT_MIN）同一个数，
       免得"默认比例"和"手动拖"两套规矩各说各话。 */
    --right-min: 280px;
    /* 中栏 : 右栏 = **4 : 3**（左树宽度不变）。没拖过分隔条时（--right-w 未设）
       两栏按 4fr / 3fr 分掉左树之外的剩余宽度；一旦用户拖过，--right-w 变成固定
       px，这一项就退化成 minmax(280px, 612px) —— 也就是"用户说了算"，拖动照旧有效。 */
    grid-template-columns:
      var(--tree-w) minmax(var(--mid-min), 4fr) var(--splitter-w)
      minmax(var(--right-min), var(--right-w, 3fr));
    gap: 10px;
    padding: 10px;
    /* 高度交给 flex：顶栏多高就自动扣多少（不再写死 45 这个魔数）。
       #app 是 height:100% 的 flex 列，.layout flex:1 吃掉剩余高度 —— 两者相加恒等于视口高，
       所以外层永远没有可滚高度（原来写死 45px 时顶栏实际 49px，多出 4px）。 */
    flex: 1 1 auto;
    min-height: 0;
    align-items: stretch;
    overflow-x: auto;
  }

  /* 右栏收起：分隔条和输出区一起退场，中栏吃掉腾出来的空间。
     用 display:none 而不是把列宽写成 0——面板的 min-content 会把 0 宽的列重新撑开，
     进而在 .layout 上压出一条横向滚动条。DOM 保留（不销毁），所以编辑器实例、
     已打开的文件和未保存内容都不受影响。 */
  .layout.right-hidden {
    grid-template-columns: var(--tree-w) minmax(var(--mid-min), 1fr);
  }
  .layout.right-hidden .col-splitter,
  .layout.right-hidden .handout-panel { display: none; }

  /* 左树收起（**只有真题卷集页面会加上这个类**，见 toggleDsTree）：与右栏那条同一个套路 ——
     DOM 保留、用 display:none 退场，列模板整条换掉（写 0 宽会在左边缘留下 gap 的空档）。 */
  .layout.ds-tree-hidden {
    grid-template-columns: minmax(var(--mid-min), 4fr) var(--splitter-w)
                           minmax(var(--right-min), var(--right-w, 3fr));
  }
  .layout.ds-tree-hidden.right-hidden {
    grid-template-columns: minmax(var(--mid-min), 1fr);
  }
  .layout.ds-tree-hidden .tree-panel { display: none; }

  /* 窗口偏窄时收窄左右两栏，尽量避免横向滚动 */
  @media (max-width: 1400px) {
    .layout {
      --tree-w: 223px;
      --mid-min: 360px;
      /* 窄屏这一条只缩左树与中栏下限，**右栏不再跟着缩**（612 与宽屏一致）：
         简版调用第一行五个控件需要 577px 内容宽（= 599px 栏宽），实测 569px 那档
         会折成两行 —— 而"五个控件一行"是硬要求，跟窗口宽窄无关。
         代价：横向滚动条的出现阈值从 1208px 推到 1251px（223+360+6+612+50），
         实测 1400px 视口下中栏仍有 509px，远高于 360px 下限。 */
      grid-template-columns:
        var(--tree-w) minmax(var(--mid-min), 4fr) var(--splitter-w)
        minmax(var(--right-min), var(--right-w, 3fr));
    }
  }

  .panel {
    display: flex;
    flex-direction: column;
    min-height: 0;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 8px;
    overflow: hidden;
  }

  /* 三区：左标题 / 中标签 / 右开关。
     用 grid 而不是 flex——flex 下中间那块会被左右两块的宽度推偏，标签就不在几何中心了。 */
  .panel-head {
    display: grid;
    /* 右格用 max-content 下限：工具栏（多选提示 + 显示答案）不该被挤到换行；
       左格标题本来就有省略号，缺宽度时先让它让位。1100px 以下由下面的媒体查询改成两行。 */
    grid-template-columns: minmax(0, 1fr) auto minmax(max-content, 1fr);
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    border-bottom: 1px solid var(--border);
    background: #fbfcfd;
    font-weight: 600;
    font-size: 13px;
  }
  .panel-head .muted { font-weight: 400; color: var(--muted); font-size: 12px; }
  /* 左栏「目录树」标题格与**中栏标题格**等高（高度由 JS 量好写进 --panel-head-h）：
     - 宽屏下中栏标题 = 44px（一行文字 + 27px 的分段控件），左栏只有一行文字（39.1px）→ 差 4.9px；
     - 窗口变窄时中栏那几个 tab 会自己折行，标题格跟着长高（实测 1300px → 61px、1200px → 95px），
       固定 44px 就再也对不上了。
     所以这里不写死高度，直接跟中栏同步（见 syncPanelHeadHeight）。 */
  .tree-panel > .panel-head { min-height: var(--panel-head-h, 44px); }
  /* 「目录树编辑模式」那颗齿轮（2026-10-10 第①轮）：外观直接照真题卷集那颗 .ds-gear
     （同一档尺寸 / 同一套 hover 与反色态），只是挂在这一行的**右端**。 */
  /* 第④轮：这一行多了「新建大类」，它和齿轮一起包进 .tree-head-actions（一个 flex），
     于是**无论编辑模式开不开**，齿轮永远是这一行的最后一项、贴着内容盒右缘 ——
     不再出现"空出来的第三列 + gap 把齿轮顶左 10px"那种只有非编辑模式才有的错位。
     右内边距对齐 .tree-search 的 10px（那边是 `padding: 8px 10px`），
     这样齿轮的右边框正好压在下搜索框的右边框上（实测两边都是 259px）。 */
  .tree-panel > .panel-head.tree-head { grid-template-columns: minmax(0, 1fr) auto;
    padding-right: 10px; }
  .tree-head .tree-head-actions { display: flex; align-items: center; gap: 8px; min-width: 0; }
  .tree-head .tree-head-title { min-width: 0; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  /* 加粗到 600：与 app 里"有分量的字"（真题卷集那三颗按钮 / 用户管理表头）同一档 */
  .tree-head .tree-new-stage { white-space: nowrap; font-weight: 600; }
  /* 改名结果的那一行提示：常驻在树下面（不是浮层）—— 它同时兼作"现在处于编辑模式"的
     可见反馈，浮 3 秒就收掉的话用户回头根本看不到改没改成。 */
  .tree-notice { margin: 6px 10px 0; }
  /* 中栏、右栏标题格也跟同一条顶线（谁最高，--panel-head-h 就取谁；见 syncPanelHeadHeight）。
     `.layout > section.panel` 就是中栏那一个（左树、右栏都是 <aside>）。 */
  .layout > section.panel > .panel-head { min-height: var(--panel-head-h, 44px); }
  .handout-panel > .panel-head { min-height: var(--panel-head-h, 44px); }
  .head-left { display: flex; align-items: center; gap: 8px; min-width: 0; }
  /* 中栏标题行：标题 / 「N 道题」/「加载中…」都**只占一行**，缺宽度时由标题自己收省略号，
     整行的宽高不受"知识点名字多长"和"有没有在加载"影响。
     背景（用户截图 2026-09-28）：知识点「代数式恒等变形」+「583 道题」+「加载中…」一起出现时，
     这一格只剩 279.7px，内容要 289.7px —— 三个文本都是可压缩的 flex 项，而中文的 min-content
     就是**一个汉字**，于是标题折成「代数式恒等变 / 形」、「217 道 / 题」、「加载 / 中…」，
      标题格高度 44px → 61.2px，整行看着就乱了。
     两条规则合起来解决：① 标题（第一个非灰字 span）能缩、缩了给省略号；
     ② 灰字是短标签，几乎不参与压缩（保留 0.05 的收缩系数：标题先缩到 0，之后它才让位，
        这样任何宽度下都不会把「N 道题」截断，也绝不会溢出到 tab 那排）。
     ⚠️ 这里**不能**再用 `> span:first-child`：2026-09-28 加的「返回搜索 / 返回目录」按钮
     排在最前面，标题不再是 first-child —— 那两处正是用户截图里的场景，规则会整个失配。 */
  .layout > section.panel > .panel-head .head-left > span:not(.muted) {
    flex: 0 100 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .layout > section.panel > .panel-head .head-left > .muted {
    flex: 0 0.05 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* 中栏标题行改成**基线对齐**：flex 默认的 align-items:center 是"盒子居中"，
     13px 的黑字标题与 12px 的灰字盒子高度不同（22.1 / 20.4），居中之后灰字整体比黑字
     高 1.15px（用户截图：灰字浮在黑字那条底线之上，`方程` 和 `69 道题` 不在一条线上）。
     实测改成 baseline 后两者文字底边都是 89.3px —— 差 0；那一行的按钮只挪了 0.25px。 */
  .layout > section.panel > .panel-head .head-left { align-items: baseline; }
  /* 右栏顶部那组 tab（简版调用 / 源码页面 / 排版预览）：**标签永不折行、这一组永不压缩**。
     初始 1 秒右栏文件树还在加载，旁边会多出一个「读取中…」；它把这一格的可用宽度从
     286 挤到 277，4 个字的标签就差这几个像素 → 折成两行（按钮高 23→40，头部 27→44），
     加载完又弹回单行 —— 看起来就是"页面跳一下"。现在把宽度从「读取中…」那一侧扣。 */
  .handout-panel > .panel-head .tabs { flex: none; }
  .handout-panel > .panel-head .tabs button { white-space: nowrap; }
  /* 右栏标题格：左边要把「tab 那排 + 保存」一起放下，所以这一格的网格跟另外两栏
     不一样 ——
       · 第一格 max 用 auto（有地方就长到内容宽，没地方能缩到 min-content = tab 那排宽），
         用 1fr 的话它只肯拿"宽的一半"，保存 会被挤下来；
       · min 钉在 min-content：再窄也保证 tab 那排不被压散。
     面板真的窄到放不下时，.head-left 自己折行（保存 掉到第二行），
     高度由 syncPanelHeadHeight 拉齐三栏顶线。 */
  .handout-panel > .panel-head { grid-template-columns: minmax(min-content, auto) auto minmax(max-content, 1fr); }
  .handout-panel > .panel-head .head-left { flex-wrap: wrap; row-gap: 4px; }
  .handout-panel > .panel-head .head-left > .muted {
    white-space: nowrap;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* 标题行里的按钮（如搜索结果那句「回到目录」）：窄的时候宁可挤标题，也别把按钮文字拆两行 */
  .head-left .edit-btn { white-space: nowrap; flex: none; }
  .head-right { justify-content: flex-end; }
  /* 工具栏右侧的多选提示（已选 N 题 · 清空） */
  .pick-hint { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); white-space: nowrap; }
  /* 中栏顶部那组 tab：**永不折行、永不压缩**（与右栏那组同一套处理）。
     背景：中栏标题格里塞了三块（标题 / tabs / 工具条），而中栏面板最窄只有 360px；
     只要 tabs 一折行，标题格就从 44px 长到 61/95px，三条顶线立刻错开。 */
  .panel-head .tabs { flex: none; }
  .panel-head .tabs button { white-space: nowrap; }
  /* 窄档（中栏面板压到最小宽度附近）：把这几块都收窄一档，让「tabs + 工具条」
     在 44px 的一行里放得下 —— 谁也不折行、谁也不会被挤出面板。
     实测：360px 面板下，tabs 242→180px、工具条 170→~100px，合计 ~308px，留 50px 余量。
     「显示答案」这里只收掉那 4 个字（复选框留着，悬停有完整说明）——
     要它一行放下，这 52px 必须让出来；否则只能让标题格变两行（那就不止 44px 了）。 */
  @media (max-width: 1400px) {
    /* 第三列（工具条那一格）允许被压缩：不压缩的话，多选提示一出现就把
       「收起调用区」挤出面板（实测 1200px 下溢出 60px）。压缩只吃「多选提示」的文字，
       复选框与按钮一律 flex:none + nowrap，谁也压不动。 */
    /* 第三列（工具条那一格）允许被压缩：默认它是 minmax(max-content,1fr)，
       多选提示一出现就把「收起调用区」挤出面板（实测 1200px 下溢出 60px）。
       压缩只吃「多选提示」的文字；复选框与按钮一律 flex:none + nowrap，压不动。 */
    .panel-head {
      grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
      padding-left: 8px;
      padding-right: 8px;
      gap: 6px;
    }
    .panel-head .tabs button { padding: 3px 4px; }
    /* 这几条都带 `.panel-head` 前缀：`.toolbar { flex-wrap: wrap }` 写在本文件更靠后的位置，
       同权重时靠后的胜 —— 不加前缀，这里的 nowrap 会被它盖掉（实测中栏标题格又被撑到 63px）。
       flex-wrap: nowrap + 只有多选提示可压缩 ⇒ 这一行一定放得下，谁也不换行。 */
    .panel-head .head-right { gap: 6px; min-width: 0; flex-wrap: nowrap; }
    .panel-head .head-right > * { flex: none; white-space: nowrap; }
    .panel-head .head-right .mini-action { padding: 2px 5px; }
    /* 多选提示：整块可以缩，但**不设 min-width:0** —— 那样会被压成 0 宽，连「清空」
       按钮一起消失。让它按 flex 的默认下限（= 里面那个按钮的宽度）停住，
       缩的只是那行文字（下面 .pick-text 负责省略号）。 */
    .panel-head .pick-hint { flex: 0 1 auto; }
    .panel-head .pick-text { flex: 0 1 auto; min-width: 0;
                             overflow: hidden; text-overflow: ellipsis; }
  }
  /* 需要"缩按钮"的两档：1401～1506px 与 1202～1289px。
     右栏标题格里比另外两栏多一颗按钮（保存，2026-09-27 从底栏搬上来的），
     而「收窄 tab」那条媒体查询要 ≤1400px 才生效 —— 这一档正好卡在中间：整行差十几个
     像素，它会被挤到第二行（标题格 44→73px）。
     两条约束（用户拍的）：
       · **tab 那排一个字都不动** —— 让出空间的活全由这颗按钮自己干；
       · **不需要缩的时候保持原大小** —— 所以区间只取实测「原尺寸放不下」的那一段：
        · 1507px 起右栏宽到 520px，原尺寸正好放下 → 以上一律原样，不缩；
        · 1290px 起 tab 那排自己收窄了（≤1400 那条规则），原尺寸又放得下 → 也不缩。
      再窄（≤1201px）连缩过的两颗都塞不下 —— 那时右栏只剩 380px 上下，是"挤到极限"
      的状态，让它折行（三栏顶线仍由 syncPanelHeadHeight 拉齐）。 */
  @media (min-width: 1401px) and (max-width: 1506px),
         (min-width: 1202px) and (max-width: 1289px) {
    .handout-panel > .panel-head .head-save-group { gap: 3px; }
    .handout-panel > .panel-head .head-save-group button {
      padding: 3px 3px;
      font-size: 12px;
    }
  }
  /* 1401～1429px 这一小段最别扭：tab 刚跳回满尺寸（242px）而右栏只有 465～473px，
     上面那一档仍差几像素。再紧一档把这段也收进一行 —— 只有这 29px 宽的窗口会用到。 */
  @media (min-width: 1401px) and (max-width: 1429px) {
    .handout-panel > .panel-head .head-save-group { gap: 2px; }
    .handout-panel > .panel-head .head-save-group button {
      padding: 3px 2px;
      font-size: 11px;
    }
  }
  /* 再窄一档（中栏面板已经压到最小宽度 360px）：这时「显示答案」那 4 个字让出来才放得下
     （实测 360px 面板下：tabs ~180 + 复选框 ~18 + 收起调用区 ~78 + 间距 28 = ~304，
      留着那 4 个字要多 52px，合计 356 —— 只剩 4px 余量，字体稍变就溢出）。 */
  @media (max-width: 1240px) {
    .panel-head .head-right .chk-lbl { display: none; }
  }
  /* 顶部标签：分段控件。与预览栏那种动作按钮有意区分——这是长期停留的页面状态 */
  .tabs {
    display: flex;
    gap: 2px;
    padding: 2px;
    background: #eef1f6;
    border-radius: 8px;
  }
  .tabs button {
    padding: 3px 14px;
    border: 0;
    border-radius: 6px;
    background: transparent;
    color: #4a5464;
    font-size: 12.5px;
    font-weight: 400;
    font-family: inherit;
    cursor: pointer;
  }
  .tabs button:hover { background: #e3e8f0; }
  .tabs button.active {
    background: #fff;
    color: var(--accent);
    font-weight: 600;
    box-shadow: 0 1px 2px rgba(16, 24, 40, 0.08);
  }
  /* 这里原来有一条「中栏偏窄时头部换两行：标题与显示答案一行，三个标签独占一行居中」的
     规则（≤1100px 时 .panel-head 改两列、tabs 挪到第二行）。现在三个页面的 tabs 都
     **永不折行**、窄档把头里的几块一起收窄（见下面那组媒体查询），一行就放得下 ——
     再保留"tabs 挪第二行"只会让标题格从 44px 变成 73px，反而让三栏顶线不齐，所以删掉。 */

  .tree-search { padding: 8px 10px; border-bottom: 1px solid var(--border); }
  .tree-search input {
    width: 100%;
    padding: 5px 8px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    font-size: 13px;
    outline: none;
    /* 真放不下时**截断也走省略号**、不硬裁半个字（2026-10-13 用户要求；
       灰字**自带三个点**是另一件事 —— 见结课那颗的 placeholder 文案）。
       生效的是 `::placeholder` 那条：只写 input 上 Chrome 不认。 */
    text-overflow: ellipsis;
  }
  .tree-search input::placeholder { text-overflow: ellipsis; }
  .tree-search input:focus { border-color: var(--accent); }

  .tree { flex: 1; overflow: auto; padding: 4px 0 12px; }
  /* 左树空态（2026-10-11 P0-2）：无权限 / 搜不到时的一句话说明。
     以前这里只有 v-for —— 没配目录权限的账号登录后左栏是一片空白，
     看不出"是坏了"还是"没给我开"，所以空的时候必须说一句。 */
  .tree-empty { margin: 10px 12px; padding: 10px 12px; font-size: 12.5px; line-height: 1.7;
    color: var(--muted); background: #f7f9fb; border: 1px dashed var(--border-strong);
    border-radius: 8px; }
  /* 拖动大类时的落点线：绝对定位在 .tree 里（.tree 是滚动容器 → position: relative），
     一条 2px 主色横线画在"要插进去的那两行之间"。同一时刻只有一条（只有一个拖动态）。 */
  .tree { position: relative; }
  .tree-drop-line {
    position: absolute; left: 6px; right: 6px; height: 2px;
    background: var(--accent); border-radius: 1px; pointer-events: none; z-index: 3;
  }
  /* 拖动中：整页光标换成抓手势（与用户表 / 工作区那套一致），松手由 JS 摘掉 */
  body.tree-stage-dragging { cursor: grabbing; }
  body.tree-stage-dragging .tree-row { cursor: grabbing; }

  .tree-row {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 2px 10px 2px 0;
    cursor: pointer;
    white-space: nowrap;
    /* 整行是"点一下选中一处"的控件，留着可选中的文字只会打架：
       双击标签会触发浏览器默认的"选中词"（实测 getSelection() = "函数"），
       而 Safari 里那层蓝底**右键也不会自动清掉**，于是看着像"右键选中了文字"
       （用户 2026-10-10 报的现象）。与 .card / .users-row 同一套做法。
       ⚠️ 框选/⌘A 也可能留下旧选区 → 右键那条路还会主动清一次（见 onContextMenu）。 */
    user-select: none;
    -webkit-user-select: none;
  }
  .tree-row:hover { background: #f2f5fa; }
  .tree-row .caret { width: 12px; color: var(--muted); font-size: 10px; flex: none; }
  .tree-row .caret.placeholder { visibility: hidden; }
  .tree-label { overflow: hidden; text-overflow: ellipsis; }
  /* 期5b：「来自 XXX」——别人分享给我的节点。小胶囊，定死 line-height 保证不把 .tree-row
     撑高（行高只由字号最大的那个决定）。
     2026-10-09（用户要求）：① 只在**分享边界**那一层渲染（见模板里的 v-if）；
     ② 配色/圆角/字号/内边距**与保存弹窗那颗 `.dir-picker-tag.share` 逐项对齐**
     （琥珀色 #fdf6ec / #96601a、4px 圆角、10px 字号、无描边、padding 0 4px），
     外加 0 1 auto + 省略号 —— 超长时截断而不是把名字挤没。 */
  .tree-badge {
    flex: 0 1 auto; min-width: 0;
    margin-left: 6px; padding: 0 4px;
    border: 0; border-radius: 4px;
    background: #fdf6ec; color: #96601a;
    font-size: 10px; line-height: 15px; white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis;
  }
  .tree-row.stage > .tree-label { font-weight: 600; }
  .tree-row.group > .tree-label { font-weight: 500; color: #333c4d; }
  .tree-row.knowledge > .tree-label { color: #4a5464; }
  /* 100% 空题的知识点：灰显但保持可点击 */
  .tree-row.dim > .tree-label { color: #b7bdc7; }

  /* ---- 目录树「选中/高亮」----------------------------------------------
     一份状态两件事：单选（普通点击）= 只有这一处亮；Cmd/Ctrl 多选 = 选中的都亮。
     这份集合同时也是「局部搜索」的范围，所以高亮和范围不会打架。
     三类节点都带 3px 主色竖条（多选时一眼看出选了几行），再用**底色深浅**区分层级：
     范围越大底色越重 —— 大类 > 小类 > 知识点（知识点沿用原来 .active 的观感，
     实际最常用，保持熟脸）。三种底色都比 hover（#f2f5fa）重，不会和悬停混起来。 */
  .tree-row.selected { box-shadow: inset 3px 0 0 var(--accent); }
  .tree-row.selected.stage { background: #d9e5ff; }
  .tree-row.selected.group { background: #e4edff; }
  .tree-row.selected.knowledge { background: var(--accent-soft); }
  .tree-row.selected > .tree-label { color: var(--accent); font-weight: 600; }
  /* 空题知识点仍保持"灰"的语义，但竖条 + 底色照样说明它被选中了 */
  .tree-row.selected.dim > .tree-label { color: #8d99ab; }

  .cards { flex: 1; overflow: auto; padding: 10px; }
  .cards-empty { color: var(--muted); text-align: center; padding: 40px 12px; }

  .card {
    /* 边框 2px：比 1px 明显一圈，又不至于笨重（颜色是浅灰，视觉重量可控）。
       内边距同步各减 1px —— 全局 box-sizing: border-box，但卡片高度是内容撑的，
       不减内边距的话每张卡会高 2px，长列表就会整体下移。 */
    border: 2px solid var(--card-border);
    border-radius: 8px;
    padding: 9px 11px;
    margin-bottom: 10px;
    /* 整张卡 = 拖动把手（按住不动仍是"点击选中"，拖起来 = 投进右栏调用区）。
       按用户确认：卡片上不再保留"可选中的文字"，免得选字和拖拽打架。 */
    cursor: grab;
    user-select: none;
    -webkit-user-select: none;
    overflow-x: auto;
  }
  .card:active { cursor: grabbing; }
  /* **Windows** 上"白色的手"（grab / grabbing）在白色题卡上几乎看不见
     （同事实测反馈："鼠标变纯白、在题卡内看不到光标"）→ Windows 改深色四向箭头（move）。
     **Mac 保持原样**（原生抓手脚本来就带黑边，用不着替身）。
     2026-10-10 用户拍板：判据从"非管理员"改成"**只看系统**" —— 发白是 Windows 的
     系统主题把字形画成白的，跟身份无关；`body.is-win` 由 setup 里一行 UA 判定挂上。 */
  body.is-win .card,
  body.is-win .card:active { cursor: move; }
  /* 图形里的 <img> 别被浏览器当成"可拖的图片"拽走 */
  .card img { -webkit-user-drag: none; user-drag: none; }
  .card:hover { border-color: var(--card-border-hover); }
  .card.active { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
  /* 多选（⌘/Ctrl 点选）：左侧内阴影竖条 + 淡主色底，不动边框（不引起布局抖动）；
     与单选 .card.active 可叠加，叠加时两个 box-shadow 一起给 */
  .card.picked { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
  .card.active.picked { box-shadow: inset 3px 0 0 var(--accent), 0 0 0 2px var(--accent-soft); }
  /* 双击跳转后的临时高亮：用主题变量，2 秒后淡出（box-shadow 不占布局） */
  .card.flash {
    background: var(--accent-soft);
    box-shadow: 0 0 0 2px var(--accent);
    transition: background 0.6s ease, box-shadow 0.6s ease;
  }
  /* 搜索结果卡片第二行：来自哪个知识点 */
  .card-sub {
    margin: -2px 0 6px;
    font-size: 11.5px;
    /* 搜索卡上「来自 大类·小类·知识点」那行。原来跟别的次要信息一样用中性灰
       （--muted #6b7280），夹在题卡里跟"来源 / 标签"那两行糊在一起。
       改成**低饱和靛蓝**：色相跟主色（--accent #2f6fed）同一族、但明度压低、
       饱和度砍掉一半 —— 一眼能和灰色标签区分开，又不会比题干更抢眼（白底对比度约 5:1）。 */
    color: #5b6fa8;
  }
  .card-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    margin-bottom: 6px;
    font-size: 12px;
  }
  /* 拖着题卡走的时候：光标旁一个小牌子（写着要调用几张、哪几张） */
  .card-drag-ghost {
    position: fixed;
    z-index: 70;
    pointer-events: none;
    padding: 3px 9px;
    border: 1px solid var(--accent);
    border-radius: 999px;
    background: #fff;
    box-shadow: 0 4px 12px rgba(20, 24, 32, 0.18);
    font-size: 12px;
    color: var(--accent);
    white-space: nowrap;
  }
  /* 拖到调用区上方时：整块调用区高亮，明确"松手就放这儿" */
  .ct-callout.drop-target {
    background: var(--accent-soft);
    box-shadow: inset 0 0 0 2px var(--accent);
    border-radius: 6px;
  }
  .qid { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #3b4453; font-weight: 600; }

  .badge {
    padding: 0 6px;
    border-radius: 10px;
    font-size: 11px;
    border: 1px solid var(--border-strong);
    color: var(--muted);
  }
  .badge.diff-A { color: var(--a-color); border-color: #b7e0c2; background: #f2fbf4; }
  .badge.diff-B { color: var(--b-color); border-color: #f0d9ab; background: #fdf8ee; }
  .badge.diff-C { color: var(--c-color); border-color: #f2c2bf; background: #fdf3f2; }
  .badge.diff-J { color: var(--j-color); border-color: #d7c8f2; background: #f7f4ff; }
  .badge.diff-L { color: #0b7285; border-color: #b6e0e8; background: #effafc; }

  .src { color: var(--muted); }
  .tag { color: #0b7285; }

  .stem { word-break: break-word; }
  .answer-box {
    margin-top: 8px;
    padding: 6px 8px;
    border-left: 3px solid #b7e0c2;
    background: #f6fbf7;
    border-radius: 4px;
  }
  .answer-box > summary {
    cursor: pointer;
    color: var(--muted);
    font-size: 12px;
    user-select: none;
  }
  .answer-box[open] > summary { margin-bottom: 4px; }

  .figure-note {
    margin-top: 6px;
    font-size: 12px;
    color: #8a6d3b;
    background: #fff9ec;
    border: 1px dashed #e8d9b5;
    border-radius: 4px;
    padding: 3px 6px;
    display: inline-block;
  }

  /* 卡片里的 TikZ 图形：默认折叠，点标题才编译并展开 */
  .card-figure { margin-top: 8px; }
  .figure-toggle {
    padding: 2px 8px;
    border: 1px solid var(--border-strong);
    border-radius: 4px;
    background: #fff;
    color: var(--accent);
    cursor: pointer;
    font-size: 12px;
  }
  .figure-toggle:hover { background: var(--accent-soft); }
  .figure-body { margin-top: 6px; text-align: center; }
  /* 放大态：这一层负责横向滚动 —— 图放大后可能比卡片宽，
     交给它滚，卡片本身不被撑破、图也不被裁掉一边。 */
  .figure-body.zoomed { overflow-x: auto; }
  .card-figure-img {
    max-width: 100%;
    max-height: 200px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: #fff;
  }
  /* 只有**浏览卡片里那张可点的题图**给"可放大"的手型；审核页「原题卡」上的同名图片
     没有点击放大（那一栏是只读对照），不加 cursor 免得点了没反应像坏了。 */
  .figure-body .card-figure-img { cursor: zoom-in; }
  /* 点一下原地放大：放开两个上限，宽度由内联 style 给（= 点击时量到的显示宽 × 2）。
     矢量图在这一步是**重新栅格化**，所以放大后仍然锐利；回落位图跟着放大（会糊，符合预期）。 */
  .card-figure-img.zoomed {
    max-width: none;
    max-height: none;
    cursor: zoom-out;
  }
  .figure-reload {
    margin-top: 6px;
    padding: 2px 10px;
    border: 1px solid var(--border-strong);
    border-radius: 4px;
    background: #fff;
    color: var(--c-color);
    cursor: pointer;
    font-size: 12px;
  }
  .figure-reload:hover { background: #fdf3f2; }
  .card-figure-loading,
  .card-figure-error {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 80px;
    padding: 8px;
    border: 1px dashed var(--border-strong);
    border-radius: 6px;
    color: var(--muted);
    font-size: 12px;
  }
  .card-figure-error {
    flex-direction: column;
    border-color: #f2c2bf;
    background: #fdf3f2;
    color: var(--c-color);
  }
  .card-figure-error details { margin-top: 6px; width: 100%; text-align: left; }

  /* 题干里的 LaTeX 结构 */
  .blank {
    display: inline-block;
    min-width: 3em;
    border-bottom: 1px solid #4a5464;
    text-align: center;
    cursor: pointer;
    line-height: 1.3;
  }
  .blank .blank-fill { visibility: hidden; }
  .blank.revealed .blank-fill { visibility: visible; }
  .choice-box { white-space: nowrap; }
  .choices { display: grid; gap: 2px 14px; margin: 4px 0 0; }
  .choices.cols-1 { grid-template-columns: 1fr; }
  .choices.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .choices.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .choices.auto { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
  .choice { white-space: normal; }
  .subqs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2px 14px; }
  .enum { margin: 4px 0; padding-left: 22px; }
  .enum.paren { list-style: none; padding-left: 4px; counter-reset: parenitem; }
  .enum.paren > li { counter-increment: parenitem; }
  .enum.paren > li::before { content: "(" counter(parenitem) ")"; margin-right: 4px; }
  .figure-badge {
    display: inline-block;
    padding: 0 6px;
    border: 1px dashed #e8d9b5;
    background: #fff9ec;
    color: #8a6d3b;
    border-radius: 4px;
    font-size: 12px;
  }
  .latex-table { border-collapse: collapse; margin: 4px 0; }
  .latex-table td { padding: 1px 8px 1px 0; vertical-align: top; }

  /* 右侧预览 */
  .preview-body { flex: 1; overflow: auto; padding: 10px 12px; }
  .preview-placeholder {
    border: 1px dashed var(--border-strong);
    border-radius: 8px;
    padding: 18px;
    text-align: center;
    color: var(--muted);
    font-size: 13px;
    background: #fbfcfd;
  }
  .latex-source {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 12px;
    line-height: 1.6;
    white-space: pre-wrap;
    word-break: break-word;
    background: #f7f8fa;
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 8px;
    margin: 0 0 10px;
  }
  /* 中栏窄时允许整块换行（多选提示 + 显示答案）；文字自身不换行，否则会被挤成竖排 */
  .toolbar { display: flex; align-items: center; gap: 10px; row-gap: 4px; flex-wrap: wrap; }
  .toolbar label { display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--muted); white-space: nowrap; }
  /* 中栏上半区：卡片列表 / 编辑表单，占满剩余高度 */
  /* container-type 让里面的 .form-grid 能按「中栏自己的宽度」而不是视口宽度断点 */
  .mid-main {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    container-type: inline-size;
  }

  /* 中栏下半区：排版预览（默认收起，标题栏常驻） */
  .mid-preview {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
    border-top: 1px solid var(--border);
    background: #fff;
  }
  .preview-head {
    flex: none;
    display: flex;
    align-items: center;
    /* 中栏很窄时允许换行：开关与按钮都不该被挤出可视区（原来只有标题+两个按钮，一行够） */
    flex-wrap: wrap;
    gap: 6px;
    /* 原来是写死的 36px 高；加了开关后改成最小高度，展开态仍是 36px，换行时才长高 */
    min-height: 36px;
    padding: 4px 10px;
    background: #fbfcfd;
    font-size: 12.5px;
    font-weight: 600;
  }
  /* 「打印来源 / 打印答案」紧跟预览按钮右边，共用 .preview-head 的同一个 gap（左侧一组）；
     「收起/展开」自己 margin-left:auto 顶到最右。标题行常驻，折叠时开关照样看得见。 */
  .preview-head .preview-source {
    display: flex;
    align-items: center;
    gap: 4px;
    /* 标题行整体是 600，开关文本不跟着变粗 */
    font-weight: 400;
    font-size: 12px;
    color: var(--muted);
    cursor: pointer;
  }
  .preview-head button {
    padding: 2px 8px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #fff;
    color: var(--accent);
    cursor: pointer;
    font-size: 12px;
    font-weight: 400;
  }
  .preview-head button:hover:not(:disabled) { background: var(--accent-soft); }
  .preview-head button.active { background: var(--accent); border-color: var(--accent); color: #fff; }
  .preview-head button:disabled { opacity: 0.5; cursor: default; }
  .preview-head button.ghost { color: var(--muted); }
  /* 预览图出完之后表单又变了 */
  .preview-stale {
    margin: 0 0 6px;
    padding: 4px 8px;
    border: 1px solid #f0d9a0;
    border-radius: 4px;
    background: #fff8e6;
    color: #8a6d1f;
    font-size: 12px;
  }

  /* 可上下拖动的分隔条 */
  .splitter {
    flex: none;
    height: 6px;
    cursor: row-resize;
    background: var(--border);
    border-top: 1px solid var(--border-strong);
    border-bottom: 1px solid var(--border-strong);
  }
  .splitter:hover { background: var(--accent-soft); }

  /* 可左右拖动的分隔条（中栏 ↔ 右侧输出区）。
     所在列宽由 .layout 的 grid-template-columns 给（6px），这里只负责外观与光标；
     高度靠 .layout 的 align-items: stretch 撑满。左树那一列不参与本次拖动。
     刻意不加边框：两侧面板自带的 1px 边框已经画出界线，再描边会变成四条线。 */
  .col-splitter {
    cursor: col-resize;
    background: var(--border-strong);
    border-radius: 3px;
  }
  .col-splitter:hover { background: var(--accent-soft); }
  /* 拖动中给实色，反馈最强（指针停在分隔条上时 hover 也会命中，靠选择器权重让这条赢） */
  body.col-resizing .col-splitter { background: var(--accent); }

  /* 拖动分栏期间：整页禁掉文本选区、光标保持 col-resize
     （指针滑出那 6px 也不松手、不选字）。松手后由 JS 摘掉这个类。 */
  body.col-resizing {
    user-select: none;
    -webkit-user-select: none;
    cursor: col-resize;
  }
  /* **Windows**：分隔条的光标换成**系统默认箭头**。
     用户 2026-09-28 实测：`col-resize` / `row-resize` 那对白色双箭头在同事那台电脑上
     也是**纯白、像隐形**（跟抓手脚同一个毛病：字形本身是白的，压在白底面板上就看不见）。
     这里不用 move / pointer —— 它们也可能是白字形（抓手脚就是），
     而**系统默认箭头**长什么样完全由对面系统主题决定，一定看得见；"这里能拖"的提示
     交给本来就有的 hover 高亮（`.col-splitter:hover` / `.splitter:hover`）。
     **Mac 保持 col-resize / row-resize 不变**（原生双箭头带黑边，看得见）。 */
  body.is-win .splitter,
  body.is-win .col-splitter,
  body.is-win.col-resizing { cursor: default; }
  /* **Windows**：文本区域那枚**白色 I 形光标**同样会隐形 —— 用户 2026-09-28 实测：
     从 PDF ⌘+点击跳回「源码页面」之后，编辑器里的光标是纯白的（CodeMirror 自己给的是
     `cursor: text`，而对面系统主题把这枚 I 形画成了白的）。
     换成**我们自己画的一枚**。**2026-10-08 从 PNG 换成内联 SVG**（原先是 16×20 的
     base64 PNG）：那张位图是**矮胖版** I 形 —— 竖杆芯 2px + 两侧各 1px 白描边 = 光学 4px、
     上下横光学 10px、高 14px，宽高比 10:14 = **1.4 : 1**，比系统 I 形胖一倍；
     而且位图在 Windows 125%/150% 缩放下会被**非整数放大**糊掉。
     新图（同一个 16×20 网格）：竖杆 stroke 1.5、上下横 5、高 13.5 → **1 : 2.7**，
     和系统 I 形一致；矢量、任何缩放都清晰。热点 `8 10` 在新几何里**正好是竖杆中心**
     （x=8.0 / y=10.0；旧图 x 偏右 0.5px）。
     ⚠️ 两件事必须记住：
       · 光标图是**独立文档**，`currentColor` 在那里解析成**黑**（2026-10-08 实测：
         Chromium 与 WebKit 都把「页面 color=红 + stroke=currentColor」画成纯黑，
         像素级确认）。所以「颜色交给 CSS」做不到，等价于写死黑 ——
         顺带，原来那圈白描边也没了（深底上会看不见）。本规则只覆盖浅色文本区，
         够用；哪天要深浅都可见，得在同一枚图里画两层（白在外、黑在内）。
       · 兼容性 2026-10-08 实测：Chrome/Chromium 154 与 Safari/WebKit 26.6.2 都认 SVG
        光标（三分区对照页 A=SVG%-编码 / B=SVG base64 / C=PNG，鼠标进入每个区都由页面
        回报坐实，两引擎下三区全是红色 I 形）。Firefox 本机没装 → **未测**。
     末尾带 `text` 兜底：图万一没加载出来就退回系统光标（只是白，不会坏）。
      只作用于 Windows 的文本类区域；**Mac 保持系统 I 形光标不变**（原生带黑边）。
      2026-10-10 用户拍板：与上面 6 组关键字一起改判据 —— 从"非管理员"改成
      「**只看系统**」（`body.is-win`），Windows 上管理员也一起吃（发白是系统问题）。 */
  body.is-win .handout-editor-host .CodeMirror-lines,
  body.is-win .handout-editor-host .CodeMirror-code,
  body.is-win .handout-editor-host .CodeMirror textarea,
  body.is-win input:not([type]),
  body.is-win input[type="text"],
  body.is-win input[type="search"],
  body.is-win input[type="password"],
  body.is-win input[type="number"],
  body.is-win textarea {
    cursor: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%2716%27%20height%3D%2720%27%20viewBox%3D%270%200%2016%2020%27%3E%3Cg%20fill%3D%27none%27%20stroke%3D%27currentColor%27%20stroke-width%3D%271.5%27%3E%3Cpath%20d%3D%27M5.5%204h5M8%204v12M5.5%2016h5%27%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E") 8 10, text;
  }

  /* ---------------- 右侧输出区（S1：标签栏 + 编辑器 + 底栏） ---------------- */
  /* 标签栏那一行（原来是"给以后多文件预留的空白条"）。
     整行的厚度由标签决定，所以标签做得很薄；一个文件都没开时留一条同样厚的空白带，
     免得开第一个文件时整个右栏往下跳一下。 */
  .handout-files-bar {
    flex: none;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 2px 10px;
    /* 高度**写死** 28px（= 标签 22 + 上下内边距 + 一条分隔线）：
       ① 一个文件都没开时也占这么高 → 开第一个文件时下面的编辑区不会往下跳；
       ② 标签多到要横向滚动时，滚动条不占位（见下 .handout-tabs 那条）→ 也不会长高。
       实测过：不写死的话，空 22 / 有标签 28 / 8 个标签 39，每变一次编辑区就抖一下。 */
    height: 28px;
    overflow: hidden;
    border-bottom: 1px solid var(--border);
    background: #fbfcfd;
  }
  .handout-files-bar .mini-action { padding: 1px 8px; font-size: 11.5px; }
  /* 右栏标题行的「新建 / 打开」：字重加粗（600，与卡片动作条那档一致） */
  .handout-panel > .panel-head .toolbar button { font-weight: 600; }
  .handout-files-slot { flex: 1 1 auto; min-width: 0; }
  /* 多标签（用户 2026-09-28：这一行改成标签栏）：
     · 标签横排、**永不换行**；放不下时先让每个标签自己收窄（名字省略号），
       收到下限还放不下才横向滚动 —— 右栏宽度是用户拖出来的，一行能放几个不由我们决定，
       而"跟着标签数长高"是不能接受的（会把下面整块编辑区顶下去）；
     · 一个标签 = 文件名 + 关闭 ×；有未保存改动时前面亮一个小圆点；
     · 活动标签 = 浅蓝底 + 主色字，与右栏那排功能 tab 的选中态同一个色系。 */
  .handout-tabs {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 4px;
    overflow-x: auto;
    overflow-y: hidden;
    /* 滚动条**不占位**：这一行的高度是写死的 28px，占位的滚动条会把标签挤扁、
       连带把整行撑高。横向滚动照旧可用（触控板横滑 / 鼠标滚轮 —— 只在一个方向
       可滚的容器上，Chromium 会把滚轮纵向量喂给它）。 */
    scrollbar-width: none;
  }
  .handout-tabs::-webkit-scrollbar { display: none; height: 0; }
  .handout-tab {
    /* 放不下时**让标签自己收窄**（名字省略号），而不是把行撑高或让最后一个跑出屏幕；
       84px 是下限（还能看出两三个字 + 关闭 ×）。真到了几十个标签才会横向滚动。 */
    flex: 0 1 auto;
    display: flex;
    align-items: center;
    gap: 2px;
    /* 高度写死 22px：配合上面 .handout-files-bar 的 28px，这一行**有没有标签都一样高**，
       否则开第一个文件时下面整块编辑区会往下跳（实测 22 → 28）。 */
    height: 22px;
    min-width: 84px;
    max-width: 190px;
    padding: 1px 2px 1px 8px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: #fff;
    color: #4a5464;
    font-size: 11.5px;
    cursor: pointer;
    user-select: none;
  }
  .handout-tab:hover { background: #f4f6fa; }
  .handout-tab.active {
    background: #eaf0fb;
    border-color: #c5d5f2;
    color: var(--accent);
    font-weight: 600;
  }
  .handout-tab-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* 未保存 = 一个小圆点（与保存按钮右边那枚「• 未保存」同一个意思，缩到最小） */
  .handout-tab-dot { flex: none; color: #d9822b; font-size: 13px; line-height: 1; }
  /* 期6b：别人分享给我的 = ↗（只放一个图标：标签最窄 84px，塞文字会把文件名挤没）。
     flex:none + 定死行高，标签那 22px 的高度一点不变；全称在 tooltip 里。 */
  .handout-tab-share {
    flex: none; color: var(--accent); font-size: 11px; line-height: 1; font-weight: 600;
  }
  /* 关闭 ×：**视觉与可点击区域都放大**，但布局占位一像素都不动。
     用户 2026-09-28：原来的 × 又小又浅（12px / #97a0af，实测布局盒只有 13.02×14）。
     放大它的难点只有一个：标签宽度是"内容撑出来的"，× 一变宽就挤掉名字的空间
     （用户明确要求"宽度分配不变"）。所以：
       · 盒子做 19×18（可点击面积 182 → 342 px²），字号 12 → 15px，颜色加深到 #4a5566；
       · 左右各 `margin: 0 -3px` 把**布局占位收回 13px**（= 改前实测的 13.02），
         所以标签宽度、名字能显示多少、标签栏有没有溢出，全都与改前逐项相同；
       · 高度 18px = 标签内容区高度（标签本身 height:22 写死、标签栏 28px 写死），
         一行的高度照样钉死，标签栏不会被撑高。
     悬停态保留并跟着变大：粉底 + 深红（原来只有 13×14 那么大一块）。 */
  .handout-tab-x {
    flex: none;
    width: 19px;
    height: 18px;
    margin: 0 -3px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 4px;
    background: transparent;
    color: #4a5566;
    font-size: 15px;
    line-height: 1;
    padding: 0;
    cursor: pointer;
  }
  .handout-tab-x:hover { background: #f6dede; color: #b42318; }
  /* 「打开文件」弹层：**固定尺寸**（460×440），内容多了只在里面滚动 ——
     文件树多长都不改弹窗大小，位置也不会跳。 */
  /* 两个类一起写：基础 .modal 也有 width，单类同权重会被它盖掉（实测宽 380） */
  .modal.open-picker-modal {
    width: 460px;
    height: 440px;
    display: flex;
    flex-direction: column;
  }
  .open-picker-modal > p { flex: none; }
  .open-picker-modal > .modal-actions { flex: none; }
  .open-picker-tree {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    overscroll-behavior: contain;
    margin: 0 0 12px;
    padding: 4px 0 6px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--panel);
  }
  .open-picker-tree .tree-row { font-size: 12.5px; }
  /* 选中的那一行：与「新建/保存」对话框里选文件夹那套**同一份观感**
     （.dir-picker-row.current：浅主色底 + 加粗 + 主色字） */
  .open-picker-tree .tree-row.picked {
    background: var(--accent-soft);
    font-weight: 600;
    color: var(--accent);
  }

  /* ---------------- 右栏三功能页 ---------------- */
  /* 选择器就是标题格里那排 .tabs（原来那排小字 .rtabs 已取消，样式一并删掉）——
     与中栏同一套观感：分段控件、浅灰底、选中白底 + 主色。 */
  /* 三个页面共用的容器：谁在场谁撑满 */
  .rtab-body { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }

  /* ---------- 多视图（任意两 / 三个面板并排，用户 2026-10-01） ----------
     单页模式（默认）：column —— 三个功能页里只有 showXxx 为真的那一页在显示，
     其余 v-show=false（display:none），与改版前 v-if 的观感逐像素一致。
     多视图（⌘+点击两个 / 三个 tab → .rtab-body-multi）：切成 row，面板宽度由
     paneStyle() 现算（前 n-1 个 flex-basis 百分比、最后一个吃掉剩下的），
     面板之间夹 6px 的可拖分隔线。 */
  .rtab-body-multi { flex-direction: row; }
  .rtab-body-multi > .ct-page,
  .rtab-body-multi > .handout-editor,
  .rtab-body-multi > .rpreview-page { min-width: 0; }
  /* 并排时给每个面板一条右边线（分隔线自己也有底色，合起来就是清楚的分栏边界） */
  .rtab-body-multi > .ct-page,
  .rtab-body-multi > .handout-editor { border-right: 1px solid var(--border); }
  /* 分隔线：与左树 ↔ 中栏那条 .col-splitter 同一套观感（6px、圆角、hover 变蓝） */
  .dual-splitter {
    flex: none;
    width: 6px;
    cursor: col-resize;
    background: var(--border-strong);
    border-radius: 3px;
  }
  .dual-splitter:hover { background: var(--accent-soft); }
  body.dual-resizing .dual-splitter { background: var(--accent); }
  /* 拖动分栏期间：整页禁掉文本选区、光标保持 col-resize（与 .col-resizing 同规矩） */
  body.dual-resizing {
    user-select: none;
    -webkit-user-select: none;
    cursor: col-resize;
  }


  /* ---------- ① 简版调用 ---------- */
  .ct-page {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    /* 上内边距 4px（左右 / 下面仍是 10px）：这一页最上面就是「调用区」那一行。
       用户 2026-09-28 两轮要求 —— 先是"按钮上下方留白一样、都小一点"，再是
       "不光对齐，还要再减少一些"：上面这一档由页面内边距决定、下面那一档由
       .ct-sub 的 padding-bottom 决定，两边都收到 4px（实测 10 / 6 → 6 / 6 → 4 / 4）。
       再小就贴着标签栏/分隔线了，看着发挤。 */
    padding: 4px 10px 10px;
    background: var(--panel);
  }
  /* 这一页的**空态虚线框**（手里还没打开文档时的「先在上面打开一份 .tex」）：
     上面那 4px 是给"有文档时"最上面那行「调用区」调出来的，而「排版预览」页的空态
     落在它自己的容器内边距上（.rpreview-body 的 10px）—— 于是同一个空态在两个 tab 里
     差了 6px（实测 182 / 188）。这里只把**空态那一块**补到 10px 档，两边顶边对齐；
     不动 .ct-page 的 4px，也不动 PDF 预览那 10px 的四周留白。 */
  .ct-page > .preview-placeholder { margin-top: 6px; }
  .ct-note { margin: 0 0 8px; font-size: 11.5px; line-height: 1.65; color: var(--muted); }
  .ct-lead { color: var(--b-color); }
  .ct-warn { color: var(--c-color); }
  /* 调用区标题行：左边一行小标题，右边两个"插入条件分页"按钮。
     按钮沿用全仓统一的 .mini-action（和「‹ 返回浏览」「格式转换」同一套）。 */
  .ct-sub {
    /* 这一行里"控件"的统一高度：风格下拉与两颗分页按钮**用同一个变量**，
       不靠各写一个值对齐 —— 用户实测反馈过"风格框比按钮高"（旧写法是 .ct-input 的
       27px 与 .mini-action 的 23px 各管各的；<select> 各引擎的默认行高又不一样）。 */
    --ct-sub-ctl-h: 23px;
    display: flex;
    align-items: center;
    /* 右栏被拖到最窄时，"风格 + 两个按钮"这一组会自己折到第二行 —— 宁可这一行变高，
       也不要横向溢出把「插入解析版分页」挤到看不见。 */
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 6px 8px;
    /* 标题行下面加一条分隔线：1px + --border，与 .tree-search / .handout-foot 等处
       用的是同一套（不引入新颜色）。padding-bottom 给线和文字留呼吸，
       margin-bottom 给线和第一张卡片留间距。 */
    /* 上边距 = 0：原来写 10px 是给"日期/班级那一排"让位的，用户 2026-09-28 把那一排
       删掉之后，这 10px 加上 .ct-page 自己的 10px 内边距就成了标题上面一条空带
       （实测：从面板内容顶到「调用区」框有 20px）。现在只留页面内边距那一档 ——
       和左右内边距同宽，看起来是有意留的，不是没擦干净的空白。 */
    /* 标题上下留白：上面由 .ct-page 的 4px 内边距给，下面由这里 4px 给 —— 两边一样，
       而且都收得比较紧（用户 2026-09-28 要求"留白对齐 + 再减少一些"）。 */
    margin: 0 0 8px;
    padding-bottom: 4px;
    border-bottom: 1px solid var(--border);
    font-size: 11.5px;
    color: var(--muted);
  }
  .ct-sub-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;            /* 组内先折，别把左边的「调用区」挤走 */
    justify-content: flex-end;
    gap: 6px;
  }
  /* 字段宽度变量：**原来挂在 .ct-line 上**，但用户 2026-09-28 把那一排输入删了、
     风格下拉搬进了 .ct-sub（它不在 .ct-line 里）；新建课测那个弹窗又还要用 .ct-line，
     所以两个容器一起挂上，值只写一遍。 */
  .ct-page,
  .ct-line {
    --ct-date-w: 96px;
    --ct-code-w: 120px;
    --ct-style-w: 108px;
  }
  /* 字段行（现在只剩"新建课测"弹窗里的日期 / 班级用；风格下拉在 .ct-sub 里单挂）：
     两个输入框的宽度**按内容实测定死**，不是拍脑袋 ——
       '8888.88.88'      渲染 75.26px + padding 16 + border 2 ≈ 93.3 → 取 96（留 2.7px 余量）
       '七年级高联一试班' 渲染 100.00px + padding 16 + border 2 = 118  → 取 120（留 2px 余量）
     余量是给别的字体回退留的：这两个串必须"完整显示"，不能因为换了字体就截断。 */
  .ct-line {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 6px 12px;
    margin: 0 0 8px;
  }
  /* 字段本身不伸缩：宽度完全由里面的输入框决定（flex:none 也顺手避开了
     <input type="text"> 默认 size=20 那个约 170px 的内容基准宽度把整行挤爆的问题） */
  .ct-field {
    display: flex;
    align-items: center;
    gap: 6px;
    flex: none;
    min-width: 0;
  }
  /* 四个标签统一加粗一档：取 600 —— 全仓的"加粗"都收敛在 600（.format-action、
     .back-to-list、标题/标签等 15 处），700 没有先例。中文在等宽字体里字宽固定，
     加粗不会改变宽度，所以不会挤动相邻控件。 */
  .ct-field > label { flex: none; font-size: 12px; font-weight: 600; color: var(--muted); }
  .ct-field .ct-input { flex: none; }
  .ct-field .ct-input.ct-date { width: var(--ct-date-w); }
  .ct-field .ct-input.ct-code { width: var(--ct-code-w); }
  /* 「新建试卷」弹窗那一行只有一个「标题」框：让这一格吃掉整行宽度（课测那两个
     固定宽度的日期 / 班级框不受影响 —— 那条规则是 .ct-input.ct-date/ct-code 专属的）。 */
  .ct-line-exam { justify-content: flex-start; }
  .ct-line-exam .ct-field-grow { flex: 1 1 auto; }
  .ct-line-exam .ct-field-grow .ct-input.ct-title { flex: 1 1 auto; width: 100%; }
  /* 排版风格下拉：宽度按最宽选项实测定死 —— 'pxxsxnsty' 在 12.5px 等宽下渲染 67.73px，
     加左内边距 8 + 右内边距 24（留给箭头）+ 边框 2 ≈ 102 → 取 108，留 6px 给字体回退。
     用 appearance:none + 自绘箭头（内联 SVG），几何完全可算，不靠浏览器默认箭头留白。 */
  .ct-field .ct-input.ct-style {
    width: var(--ct-style-w);
    padding: 0 24px 0 8px;
    appearance: none;
    -webkit-appearance: none;
    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='6'%3E%3Cpath d='M1 1l3.5 3.5L8 1' fill='none' stroke='%236b7280' stroke-width='1.4' stroke-linecap='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 8px center;
    cursor: pointer;
  }
  .ct-field .ct-input.ct-style:disabled { background-color: #f7f8fa; cursor: default; }
  /* 风格下拉与同排那两颗分页按钮**高度必须一样**（用户 2026-09-28 两次提到：
     第一版是 .ct-input 的 27px vs .mini-action 的 23px；改成硬写 23px 之后他那边
     看着还是不一样 —— <select> 在不同引擎里自带的最小高度 / 行高不一样）。
     现在两头都钉在同一个变量上，并且给 <select> 补了 min-height：只有"显式高度"这一种
     算法，Safari / Chrome 都不会再各算各的。
     字号也从 12.5px 收到 12px（与按钮同号）—— 不然虽然盒子一样高，"mingsty"那行字
     看起来还是比按钮里的大一号。宽度 108px 是按 12.5px 测的，收到 12px 只会更宽裕。
     只改这一行：弹窗里的日期 / 班级输入框仍是 27px。 */
  .ct-sub .ct-field .ct-input.ct-style {
    height: var(--ct-sub-ctl-h);
    min-height: var(--ct-sub-ctl-h);
    font-size: 12px;
  }
  .ct-sub .mini-action { height: var(--ct-sub-ctl-h); }
  /* （.ct-check 那几条规则跟着「来源」复选框一起删了：那一排输入已经不在了。） */
  .ct-input {
    flex: 1 1 auto;
    min-width: 0;
    height: 27px;
    padding: 0 8px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #fff;
    color: var(--text);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 12.5px;
    outline: none;
  }
  .ct-input:focus { border-color: var(--accent); }
  .ct-input[readonly] { background: #f7f8fa; cursor: default; }
  /* 共用的手输框 + 日历按钮：两处弹窗都靠组件自身保证 6px 间距。 */
  .date-field { display: inline-flex; align-items: center; gap: 6px; }
  /* 日历按钮：同高同圆角，只用图标 */
  .ct-cal {
    flex: none;
    width: 27px;
    height: 27px;
    padding: 0;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #fff;
    font-size: 13px;
    line-height: 1;
    cursor: pointer;
  }
  .ct-cal:hover:not(:disabled) { background: var(--accent-soft); }
  .ct-cal:disabled { opacity: 0.45; cursor: default; }
  /* 藏起来的原生日期输入：必须"仍被渲染"（不能 display:none）showPicker() 才肯用，
     所以用 0 尺寸 + 透明，而不是 hidden。 */
  .ct-cal-native {
    position: absolute;
    width: 0;
    height: 0;
    padding: 0;
    border: 0;
    opacity: 0;
    pointer-events: none;
  }
  /* ---------------- 调用区：卡片流（干净） / 提示（复杂） ---------------- */
  .ct-callout { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
  /* 调用区同病：调用几张之后它也会开始滚，不留槽位的话卡片宽度会跟着跳一下 */
  .ct-cards { flex: 1 1 auto; min-height: 0; overflow: auto; padding-right: 2px;
    scrollbar-gutter: stable; }
  .ct-unit {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: 6px;
    margin: 0 0 6px;
    /* 与浏览页题卡同一套：加深色（--card-border）+ 2px 宽，两边保持一致；
       内边距 6 → 5px 抵掉加粗的 1px，卡片外尺寸不变。 */
    padding: 5px;
    border: 2px solid var(--card-border);
    border-radius: 6px;
    background: #fff;
    /* 整张卡就是一个拖拽把手：按住不动 = 点击选中，按着移动 = 拖动排序。
       所以卡片上**不留任何可选中的文字**（user-select:none），也就不存在
       "选字 vs 拖拽"的冲突；题面里的公式照旧由 MathJax 渲染，只是选不中。 */
    cursor: grab;
    user-select: none;
    -webkit-user-select: none;
  }
  /* Option+点击「源码 → 调用区」跳过来时，目标卡闪一下 —— **与源码侧、PDF 侧同一套**：
     同一个 `--sync-flash` 黄色、同一个 `syncFlashYellow` keyframes（2.6s 淡出）、
     同一条左侧琥珀色粗竖条。用伪元素盖在卡片上（`inset:0`），所以卡片自己的边框 /
     选中态 / 分组色调一个像素都不动；`multiply` 与 PDF 侧同理：黄 × 白卡 = 同色黄，
     黄 × 浅色调卡片 = 保留那层色调（不做正片叠底就会把色调盖掉）。 */
  .ct-unit.ct-flash::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 4px;
    background: var(--sync-flash);
    box-shadow: inset 4px 0 0 0 var(--sync-flash-edge);
    mix-blend-mode: multiply;
    pointer-events: none;
    animation: syncFlashYellow 2.6s ease-out forwards;
  }
  /* Windows：同上，避免白色的手在白卡上看不见；Mac 保持 grab */
  body.is-win .ct-unit { cursor: move; }
  .ct-unit:hover { border-color: var(--card-border-hover); }
  /* 选中态：**只把 2px 的边框染成主色**（四边同宽、单层）。
     ⚠️ 这里原来还跟了一条 `box-shadow: inset 2px 0 0 var(--accent)` —— 边框已经画了左边缘，
     阴影又在**边框内缘**画了第二条，放大看左边就是"两条线叠着"（用户 2026-10-03 报）。
     蓝色 / 2px / 圆角都不变，只是不再叠。 */
  .ct-unit.selected { border-color: var(--accent); }
  /* 分组结束（\end{XuanZeTi} 这类）：调用区**不再画卡**（用户 2026-09-29）。
     它仍然是一条单元（分组边界 / 拖动落点下标 / 偏移换算都靠它），所以占一个
     0 高度、不可见、不可点也不可拖的哨兵位 —— 视觉上就是"上一组排完直接接下一组"。
     拖动到它上面时那条落点指示线（.dropbefore::before）照旧画得出来，方便"插到这一组末尾"。 */
  .ct-unit.ct-sentinel {
    height: 0;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    pointer-events: none;
    cursor: default;
  }
  .ct-unit.ct-sentinel:hover { border-color: transparent; }
  /* 正在被搬的那一张：只改透明度与描边，**不动尺寸** ——
     拖动中的落点判定用的是按下瞬间量到的矩形，卡片一变形就会错位。 */
  .ct-unit.dragging { opacity: 0.45; border-style: dashed; border-color: var(--accent); }
  /* 落点指示（两张之间 / 最前）：一条粗主色线 + 左端圆点，比原来的 2px 细线清楚得多 */
  .ct-unit.dropbefore::before {
    content: '';
    position: absolute;
    left: 0; right: 0; top: -5px;
    height: 3px;
    background: var(--accent);
    border-radius: 2px;
    box-shadow: 0 0 0 2px var(--accent-soft);
  }
  .ct-unit.dropbefore::after {
    content: '';
    position: absolute;
    left: -2px; top: -8px;
    width: 9px; height: 9px;
    border-radius: 50%;
    background: var(--accent);
  }
  /* 讲义汇总：外层沿用 .ct-unit 的卡片框，内层用专用“深石板蓝目录卡”色块。
     与试卷填空（青绿）/选择（亮蓝）/解答（紫）/附加（琥珀）都区分开。 */
  .lecture-index-cards { display: flex; flex-direction: column; }
  .lecture-index-card {
    --lecture-index-tone: #334e68;
    --lecture-index-bg: #eef3f7;
    align-items: stretch;
    padding: 4px;
    gap: 0;
    background: #fff;
  }
  .lecture-index-surface {
    flex: 1 1 auto; min-width: 0;
    display: flex; align-items: baseline; gap: 8px;
    padding: 7px 10px;
    border-radius: 6px;
    border-left: 3px solid var(--lecture-index-tone);
    background: var(--lecture-index-bg);
  }
  .lecture-index-title {
    flex: 0 1 auto; min-width: 0;
    display: flex; align-items: baseline; gap: 0.55em;
    color: var(--lecture-index-tone); font-size: 13px; font-weight: 600;
  }
  .lecture-index-chapter-no { flex: none; }
  .lecture-index-chapter-edit {
    cursor: pointer;
    border-bottom: 1px dotted #9aa3b0;
  }
  .lecture-index-chapter-edit:hover {
    border-bottom-color: var(--lecture-index-tone);
  }
  .lecture-index-chapter-input {
    width: 42px;
    padding: 0 3px;
    border: 1px solid var(--lecture-index-tone);
    border-radius: 4px;
    background: #fff;
    color: var(--lecture-index-tone);
    font: inherit;
    font-size: 13px;
    font-weight: 600;
  }
  .lecture-index-chapter-name {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .lecture-index-file {
    flex: none; max-width: 42%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    color: #7a838f; font-size: 10.5px;
  }
  .lecture-index-missing { flex: none; color: var(--c-color, #b42318); font-size: 10.5px; }
  .lecture-index-check {
    flex: none; margin-left: auto; align-self: center;
    display: flex; align-items: center;
  }
  /* 不写 width/height：跟系统内其它原生 Checkbox 同一尺寸（macOS / Windows 各自原生）。 */
  .lecture-index-check input { margin: 0; cursor: pointer; }
  .lecture-index-toc {
    display: flex; align-items: center; gap: 5px;
    color: var(--muted); font-size: 12px; font-weight: 600;
    white-space: nowrap; cursor: pointer;
  }
  .lecture-index-toc input { margin: 0; cursor: pointer; }
  /* 目录补全：有未纳入的章节才出现；琥珀色表示"待处理"，不是错误。 */
  .mini-action.lecture-index-fill {
    background: #fff7e6;
    border-color: #e6cf9a;
    color: #8a6410;
    font-size: 12px;
    font-weight: 600;
  }
  .mini-action.lecture-index-fill:hover { background: #fdf0d4; }
  .mini-action.lecture-index-fill:active { background: #f8e6bd; }
  .lecture-index-card:hover .lecture-index-surface { background: #e4ebf2; }
  .lecture-index-card.disabled { opacity: 0.55; }
  .lecture-index-card.disabled .lecture-index-surface { background: #eef1f4; border-left-color: #9aa3af; }
  .lecture-index-card.disabled .lecture-index-title { color: #7b8491; }
  .lecture-index-card.disabled:hover .lecture-index-surface { background: #eef1f4; }
  .lecture-index-card.dropafter::after {
    content: '';
    position: absolute;
    left: 0; right: 0; bottom: -5px;
    height: 3px;
    background: var(--accent);
    border-radius: 2px;
    box-shadow: 0 0 0 2px var(--accent-soft);
  }
  /* 从浏览页拖卡片进来时的插入位指示：与上面那套**同一份观感**
     （3px 主色线 + 左端圆点），只是换一套 class，免得和调用区内部拖拽打架 */
  .ct-unit.card-drop-before::before {
    content: '';
    position: absolute;
    left: 0; right: 0; top: -5px;
    height: 3px;
    background: var(--accent);
    border-radius: 2px;
    box-shadow: 0 0 0 2px var(--accent-soft);
  }
  .ct-unit.card-drop-before::after {
    content: '';
    position: absolute;
    left: -2px; top: -8px;
    width: 9px; height: 9px;
    border-radius: 50%;
    background: var(--accent);
  }
  /* 插到最后一条**之后**：线画在它下面 */
  .ct-unit.card-drop-after::after {
    content: '';
    position: absolute;
    left: 0; right: 0; bottom: -5px;
    height: 3px;
    background: var(--accent);
    border-radius: 2px;
    box-shadow: 0 0 0 2px var(--accent-soft);
  }
  /* 落到**最后一位之后**的落点：列表末尾专门留一个空位，
     平时不占地方（height 0），拖动到末尾时才展开成一条虚线槽。 */
  .ct-drop-end {
    height: 0;
    margin: 0;
    border: 1px dashed transparent;
    border-radius: 6px;
    transition: height 0.08s ease;
  }
  .ct-drop-end.active {
    height: 30px;
    margin: 2px 0 2px;
    border-color: var(--accent);
    background: var(--accent-soft);
  }
  body.ct-dragging { user-select: none; -webkit-user-select: none; cursor: grabbing; }
  /* Windows：拖拽中也用深色箭头（Mac 保持 grabbing） */
  body.is-win.ct-dragging { cursor: move; }
  .ct-card-body { flex: 1 1 auto; min-width: 0; }
  /* ---- 题卡内容查重（2026-10-03）----
     底色**上在整张卡上**（`.ct-unit` = 2px 边框 + 5px 内边距 + 白底那一个），
     这样淡黄会一直铺到**边框内缘**；上在里面的 `.ct-card-body` 会四周留一圈白边（用户实测）。

     ⚠️ 条件**只有一处**：里面的 `.ct-card-body.dup-hit`（= 渲染「相似 ×N」胶囊的同一个判断）。
     外面这层用 CSS `:has()` 去认它 —— 这样黄底与胶囊**永远同步**，不会再出现
     "整片全黄 / 胶囊在底却白"（2026-10-03 用户报：之前 JS 那套判断漏传了单元下标）。
     橙色 = 现有那支"提醒"色（--b-color，和卡片上的 ⚠ 同一支）；不用红色（那是"错误"）。 */
  .ct-card-body { position: relative; border-radius: 4px; }
  /* 底色一档淡黄；⚠️ 只改颜色 —— 不加 padding / border，卡片内容的 x 坐标与普通卡逐像素相同。 */
  .ct-unit:has(.ct-card-body.dup-hit) { background: #fff3d9; }
  /* 点相似列表跳过去时闪一下（1 秒，与"高亮 1 秒"一致） */
  .ct-card-body.dup-flash { animation: ctDupFlash 1s ease-out; }
  @keyframes ctDupFlash {
    0% { background: #ffe9c2; }
    100% { background: #fff3d9; }
  }
  .ct-unit:has(.ct-card-body.dup-hit) .ct-card-head { padding-right: 64px; }  /* 给胶囊让位 */
  .ct-dup { position: absolute; right: 6px; top: 4px; z-index: 3; display: flex; flex-direction: column; align-items: flex-end; }
  /* 胶囊底色要比卡底明显（否则看不出形状）：卡底 #fff3d9 → 胶囊 #f7ddab（同色系再深一档）。 */
  .ct-dup-badge { border: 0; border-radius: 8px; padding: 0 6px; background: #f7ddab; color: #7d4508;
    font-family: inherit; font-size: 10.5px; font-weight: 600; line-height: 16px; cursor: pointer;
    white-space: nowrap; }
  .ct-dup-badge:hover { background: #f2d199; }
  /* 相似小列表：题号 + 来源 + 首句，点一条跳过去 */
  .ct-dup-list { margin-top: 4px; display: flex; flex-direction: column; gap: 2px; min-width: 260px;
    padding: 6px; border: 1px solid var(--border-strong); border-radius: 8px; background: #fff;
    box-shadow: 0 6px 18px rgba(16, 24, 40, 0.12); }
  .ct-dup-item { display: flex; align-items: baseline; gap: 6px; width: 100%; border: 0; background: none;
    padding: 3px 4px; border-radius: 6px; font-family: inherit; font-size: 11.5px; text-align: left;
    color: #4a5464; cursor: pointer; }
  .ct-dup-item:hover { background: #fff7e8; }
  .ct-dup-src { flex: none; color: #96601a; }
  .ct-dup-stem { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    color: var(--muted); }
  .ct-card-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    margin-bottom: 3px;
    font-size: 12px;
  }
  /* 题面：**完整显示**，不截断、不折叠、也没有"展开"按钮（用户拍板）。
     与中栏卡片同一套渲染（renderStem 出的 HTML），只是字号小一档；
     高度按实际内容自然撑开。 */
  .ct-stem {
    font-size: 12px;
    line-height: 1.65;
    color: #3b4453;
    overflow-wrap: anywhere;
  }
  .ct-stem.missing { color: var(--c-color); }
  /* 待审草稿的卡：标题那一行不写 ID，改说一句"该题待审批…"（琥珀 = 注意但不是故障） */
  .ct-card-head-pending .ct-pending-note {
    color: var(--b-color);
    font-size: 11.5px;
    white-space: normal;
  }
  .ct-del {
    flex: none;
    padding: 0 5px;
    border: 1px solid transparent;
    border-radius: 4px;
    background: transparent;
    color: #b7bdc7;
    font-size: 12px;
    line-height: 1.6;
    cursor: pointer;
  }
  .ct-del:hover { border-color: var(--c-color); color: var(--c-color); }
  /* 卡内浮动 ✕（类型卡用）：绝对定位在卡片右端，**不占行宽** —— 这样类型卡的
     底色长度和模块卡一模一样（用户 2026-09-30）。 */
  .ct-del-floating {
    position: absolute;
    right: 4px;
    top: 50%;
    transform: translateY(-50%);
    background: transparent;
  }
  /* 分页线：一条横线 + 「分页」二字，视觉上把前后两页分开 */
  .ct-unit.divider {
    align-items: center;
    padding: 1px 5px 1px 1px;      /* 抵掉边框 1→2px，分页线高度不变 */
    background: #f7f8fa;
  }
  .ct-divider {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 11.5px;
    color: var(--muted);
  }
  .ct-divider::before, .ct-divider::after {
    content: '';
    flex: 1 1 auto;
    height: 0;
    border-top: 1px dashed var(--border-strong);
  }
  /* 条件分页：\ifa 只在解析版分页、\ifna 只在学生版分页。
     用既有调色板里两个不撞车的颜色（b-color 琥珀 / accent 蓝）区分 ——
     绿 --a-color 与红 --c-color 在中栏已经固定表示难度，不抢它们。 */
  .ct-divider.cond-ifa, .ct-divider.cond-ifna { font-weight: 600; }
  .ct-divider.cond-ifa { color: var(--b-color); }
  .ct-divider.cond-ifa::before, .ct-divider.cond-ifa::after {
    border-top-color: var(--b-color);
  }
  .ct-divider.cond-ifna { color: var(--accent); }
  .ct-divider.cond-ifna::before, .ct-divider.cond-ifna::after {
    border-top-color: var(--accent);
  }
  /* 整行 % 注释：比题卡矮一档、灰底灰字，一眼看出"这不是一道题"。
     等宽字体保留注释里的制表符缩进；只整行删/搬，不参与选题。 */
  .ct-unit.comment { padding: 2px 5px 2px 1px; background: #fbfbfc; }
  .ct-comment {
    flex: 1 1 auto;
    min-width: 0;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 11.5px;
    line-height: 1.6;
    color: #9aa3b0;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
  }
  /* 复杂：不硬凑卡片，给提示 + 去源码页面 */
  /* 「原文结构块」：认不出来的整行（\item 分组行、\ifna{…} 里的图、minipage 图块、
     \vspace 之类）不再把整个调用区挡在门外 —— 原样显示成一块只读文本，能搬能删；
     块里引用到的题号照样在下面显示成题卡。整块的样式与"注释单元"同一套骨架。 */
  .ct-raw { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
  /* ---------- 试卷（pxxexam）专有的那几块 ---------- */
  /* 分组小节标题（\begin{TianKongTi}[4] / JieDaTi / FuJiaTi）：
     一条浅底横条 + 统计，读起来像"这一组从这里开始"。 */
  /* 分组开始卡：
     · 底色**填满整张卡** —— .ct-unit 是 flex 容器，子元素默认按内容宽，所以必须 flex:1；
     · 四种分组四种色调（选择蓝 / 填空青 / 解答紫 / 附加琥珀）；
     · 只有**开始**有卡：\end{…} 那一边 2026-09-29 起不再画卡（见下面 .ct-sentinel）。 */
  .ct-section {
    position: relative;      /* 类型卡里那个浮动 ✕ 的定位上下文（别的卡没有绝对定位子元素） */
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 5px 9px;
    border-radius: 6px;
    background: var(--ct-tone-bg, #f2f5fa);
    border-left: 3px solid var(--ct-tone, var(--accent));
  }
  .ct-section-name { font-weight: 600; color: var(--ct-tone, var(--text)); font-size: 12.5px; }
  .ct-section-stat { color: #7a838f; font-size: 11.5px; }
  .ct-section.tone-xuanze { --ct-tone: #2f6fd0; --ct-tone-bg: #eef4fd; }
  .ct-section.tone-tiankong { --ct-tone: #0e8a7d; --ct-tone-bg: #e9f6f4; }
  .ct-section.tone-jieda { --ct-tone: #6d5bd0; --ct-tone-bg: #f0eefc; }
  .ct-section.tone-fujia { --ct-tone: #b9761a; --ct-tone-bg: #fdf3e7; }
  /* 练习的两张结构卡（2026-09-30）：**复用**试卷分组卡那一套外壳（形状 / 布局 / 圆角 /
     字号 / 左侧色条），只换色调变量 —— 与试卷那四色（蓝 / 青 / 紫 / 琥珀）都不撞：
        · 模块卡 \unit{…} → 紫红（最醒目，它是最大的分组）
        · 类型卡 \means{…} → 橄榄绿，并且**矮一档**（它是组内小标题，不该压过模块卡） */
  .ct-section.ct-section-unit { --ct-tone: #9d2b6b; --ct-tone-bg: #fbeef4; }
  /* 讲义的小节卡（\section{…}）：形状 / 布局 / 圆角 / 字号都复用分组卡那套，
     只换一个色调（蓝），与模块卡（紫红）一眼分得开。 */
  .ct-section.ct-section-lecture { --ct-tone: #1d4ed8; --ct-tone-bg: #eef3ff; }
  .ct-section.ct-section-caption {
    --ct-tone: #4d7c0f;
    --ct-tone-bg: #f1f7e8;
    /* 用户 2026-09-30（第二轮）：类型卡的**底色长度与模块卡相同**（都是整行宽）——
       只比模块卡矮一档（padding 小一点），高度上的主次还在。 */
    flex: 1 1 auto;
    padding: 3px 9px;
  }
  .ct-section.ct-section-caption .ct-section-name { font-size: 12px; }
  .ct-unit-no { font-weight: 700; margin-right: 6px; }
  /* 模块卡 / 类型卡的就地改名输入框：跟卡片同字号同配色，宽度按内容自然撑开，不撑高卡片 */
  .ct-name-input {
    min-width: 60px;
    max-width: 100%;
    padding: 0 4px;
    border: 1px solid var(--ct-tone, var(--accent));
    border-radius: 4px;
    background: #fff;
    color: var(--ct-tone, var(--text));
    font: inherit;
    font-size: 12.5px;
    font-weight: 600;
  }
  .ct-name-editable { cursor: text; }
  .ct-name-editable:hover { text-decoration: underline dotted; }
  .ct-unit-title { color: var(--ct-tone); }
  /* 年份开关（\ifx\newyear…\fi）：一条灰虚线说明条，**不可拖不可删**（拖出手柄不给、
     删除按钮不出现），所以画得比两种结构卡都轻，一眼就知道它不是可编辑的卡。 */
  .ct-variant {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 3px 9px;
    border: 1px dashed #cbd2da;
    border-radius: 6px;
    background: #f6f7f9;
    color: #6b7280;
    font-size: 11.5px;
  }
  .ct-variant-badge {
    flex: none;
    padding: 0 5px;
    border-radius: 999px;
    background: #e7eaef;
    color: #4b5563;
    font-size: 11px;
  }
  .ct-variant-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* 练习题卡上的留白系数（\myref[n] 的 n）：只读小胶囊；课测 / 试卷的单元没有 pad 键，
     所以这一段对它们恒不渲染（单元形状与样式一个字节都不动）。 */
  .ct-pad {
    flex: none;
    padding: 0 6px;
    border: 1px solid #cfd6e0;
    border-radius: 999px;
    background: #f6f8fb;
    color: #55606d;
    font-size: 11px;
  }
  /* 可改的分值：点它 → 原位变输入框（回车 / 失焦提交，Esc 取消） */
  .ct-score-edit { cursor: pointer; border-bottom: 1px dotted #9aa3b0; }
  .ct-score-edit:hover { color: var(--accent); border-bottom-color: var(--accent); }
  .ct-score-input {
    width: 54px;
    padding: 0 3px;
    border: 1px solid var(--accent);
    border-radius: 4px;
    font-size: 11.5px;
  }
  /* 题卡上的两枚小徽标：分值（解答题写在宏参数里）与"只在某一版 / 两版写法不同" */
  .ct-score {
    padding: 0 5px;
    border-radius: 999px;
    background: #eef1f6;
    color: #5b6572;
    font-size: 11px;
  }
  .ct-cond {
    padding: 0 5px;
    border-radius: 999px;
    background: #fdf3e3;
    color: var(--b-color);
    font-size: 11px;
  }
  /* 试卷里的两种"断开"卡，颜色 + 图标都不同（一眼能分）：
       · ▤ 分栏（\newpage，试卷"插入分栏"）：灰蓝 #5b6572 / 底 #eef1f6
       · ⤓ 分页（\nextpage，课测与试卷"插入分页"）：青绿 #0e8a7d / 底 #e9f6f4
     只在**试卷**里上这两套色（examView 判定）；课测那三种条件色一字未改。 */
  .ct-divider.page-split, .ct-divider.page-break {
    font-weight: 600;
    padding: 0 6px;
    border-radius: 999px;
  }
  .ct-divider.page-split { color: #5b6572; background: #eef1f6; }
  .ct-divider.page-split::before, .ct-divider.page-split::after { border-top-color: #b6bfcd; }
  .ct-divider.page-break { color: #0e8a7d; background: #e9f6f4; }
  .ct-divider.page-break::before, .ct-divider.page-break::after { border-top-color: #8fd3c9; }
  /* 底栏正中间那格统计（.foot-exam-stat，规则在下面）：**它现在占底栏网格的第 2 列**。
     曾经的做法是 position:absolute + translateX(-50%)（"谁也挤不动它"），但正因为脱离
     了正常流，编译状态（"学生版 · 1 页 · 8.3s"）一出现就和它**叠字** —— 2026-09-29 用户
     报的那个 bug。改成"左右两列等宽 1fr + 它在中间那一格居中"之后：位置照样永远正中，
     但它在正常流里，结构上不可能再和任何东西重叠。 */
  /* 考试时长框：最多三位数 → 宽度按 3 位数字定；高度吃课测那套 .ct-input，与按钮同高 */
  .ct-input.ct-minutes { width: 48px; height: 23px; padding: 0 4px; text-align: center; }

  .foot-exam-stat {
    /* 底栏正中间那格统计：**绝对居中**，谁也挤不动它（数字变长 91→120→1234、
       两边提示变长变短，都不动它的位置）。
       ⚠️ 它脱离正常流 → **正中间同一时刻只允许有它一个**：底栏中段那些会冒字的
       东西全部收敛进 `footSlot`（见那里），槽位有人时它根本不渲染 —— 这就是
       "不可能再叠字"的保证（2026-09-29 用户报的 bug 是编译状态常驻、和它叠在一起）。 */
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    flex: none;
    max-width: 40%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 12.5px;
    color: #5b6572;
    pointer-events: none;   /* 纯展示，别挡住底下按钮的点击 */
  }
  /* 右栏被拖得很窄时（底栏 < 380px：三样东西一行放不下 —— 实测需要
     110(编译组) + 120(发送组) + 107(统计) + 12(gap) + 20(padding) ≈ 369px），
     **干脆不显示统计** —— 绝不让它压到左右两组的按钮上（那又是"叠字"）。
     这样底栏在**任何宽度**下都只有一行、高度不变：宁可少显示一样，也不折行、不加高。
     380 = 369 再留 ~11px 给字体回退；右栏最小 280px 也落在这一档里。 */
  @container (max-width: 380px) {
    .foot-exam-stat { display: none; }
  }
  .ct-raw-head {
    align-self: flex-start;
    padding: 0 6px;
    border-radius: 999px;
    background: #fdf3e3;
    color: var(--b-color);
    font-size: 11px;
    line-height: 17px;
  }
  .ct-raw-text {
    margin: 0;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 11.5px;
    line-height: 1.6;
    color: #7a838f;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
  }
  /* ---------- ③ 排版预览（编译真实文件） ---------- */
  .rpreview-page {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    background: var(--panel);
  }
  /* 原来这里有一行 .rpreview-bar（发送学生版 / 发送整合版 · 状态 · 编译解析版 / 编译学生版）。
     整行已删 —— 编译与发送搬到底栏 .handout-foot 里，状态文字也跟着搬过去了
     （见 .foot-group / .foot-check；编译结果那行由 footSlot 统一管，几秒自消）。 */
  .rpreview-body { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 10px; }
  /* 这里原来有一条 `.rpreview-pages img`（逐页 PNG 的降级显示）——
     2026-09-27 后端删掉栅格化之后它再也不会被渲染，规则一并删掉。 */
  /* PDF 直显（PDF.js 自绘）：撑满预览区，翻页 / 缩放都在这里。 */
  .rpreview-pages-pdf {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
  }
  .rpreview-pages-pdf .rpreview-stale { flex: none; }
  /* PDF.js 自绘的查看区：外框 + 内部滚动容器（HUD 要绝对定位在滚动容器之外，
     才不会被滚走 —— 所以这里两层，不是一层）。 */
  .rpreview-pdfhost {
    position: relative;
    flex: 1 1 auto;
    min-height: 320px;
    border-radius: 4px;
    overflow: hidden;
  }
  .rpreview-pdfscroll {
    position: absolute;
    inset: 0;
    overflow: auto;
    /* 页面之外那圈底色：**浅灰**，让白纸有边界又不抢眼
       （早先那版是 #7d8592 的灰蓝，在右栏里显得偏黑、压得住页面） */
    background: #eceef2;
  }
  /* 放大到"有得滚"之后才给抓手光标（用户 2026-09-28 要求拖动平移）：
     100% 时没有任何可平移的余地，光标保持默认箭头，不给假的可拖暗示。
     拖动中换成"抓紧"，并且**禁掉文字选择** —— 不然拖着拖着会把右栏的说明文字选蓝一片。 */
  .rpreview-pdfscroll.pannable { cursor: grab; }
  .rpreview-pdfscroll.pannable.dragging { cursor: grabbing; }
  .rpreview-pdfscroll.dragging { user-select: none; -webkit-user-select: none; }
  /* Windows：抓手脚换成四向箭头 —— 抓手脚是**白色**的，而 PDF 页面本来就是白纸，
     同事实测反馈"某些区域光标纯白、像隐形了"（这条是新加的拖动平移，之前漏了）。
     move 是标准四向箭头，各系统 / 各光标主题下都是深色描边，永远看得见。Mac 保持不变。 */
  body.is-win .rpreview-pdfscroll.pannable,
  body.is-win .rpreview-pdfscroll.pannable.dragging { cursor: move; }
  .rpreview-pdfpages {
    display: flex;
    flex-direction: column;
    /* ⚠️ 这里**不能用** `align-items: center` 去居中页面：内容比容器宽时（放大之后），
       flex 的居中会把溢出的部分**左右各分一半**，而滚动容器只能往右滚 ——
       左边那半截永远够不着。用户实测就是"横向滚动条已经拖到最左，左边还是被切掉"。
       改成 flex-start + 页面自己 `margin: 0 auto`：
         · 内容比容器窄 → auto 外边距把它居中，观感与改前一致；
         · 内容比容器宽 → auto 外边距解析成 0，左边缘老老实实贴在容器内边距上，
           左右两边都能滚到（滚动条最左 = 最左边缘）。 */
    align-items: flex-start;
    gap: 10px;
    padding: 10px;
  }
  /* 一页一个占位框（先按第一页宽高比撑住高度，渲染完再把 canvas 塞进去）——
     先摆框再画，滚动条不会一边渲染一边跳。 */
  .rpreview-pdfpage {
    position: relative;
    /* 居中交给 auto 外边距（见上），不是容器的 align-items —— 理由写在那边 */
    margin: 0 auto;
    background: #fff;
    /* 浅灰底上纸张靠这层淡阴影浮起来，别再加重（重了又会显脏） */
    box-shadow: 0 1px 3px rgba(15, 23, 42, 0.14);
    line-height: 0;
  }
  .rpreview-pdfpage canvas { display: block; }
  /* Cmd+点击跳转的「高亮」：**浅黄底 + 保持约 2 秒**再淡出（总 2.6 秒）。
     为什么不是"闪一下"：切 tab + 滚动本身要 0.3～0.6 秒，等眼睛落到目标上时动画已经
     过半了 —— 上一版就是 1.2 秒淡出，而且源码侧那个类**根本没写底色**（见下面注释），
     所以用户反馈"看不到高亮"。
       · 源码侧：CM5 会为挂了 background 类的行建一个 `.CodeMirror-linebackground`
         （`position:absolute; inset:0; z-index:0`，能画底色，文字在 z-index:2 之上）；
       · PDF 侧：在页面框里给 `synctex view` 回的**每一条记录**放一个色块，
         一条不合并、不加边距（块间的空白自然不涂）。 */
  /* 只动"浓度"（opacity）：色相由 --sync-flash 给。
     把浓度做成"整层的 opacity"而不是"每块自己的 background-color 透明度"，
     是为了让引擎重复报的同一块叠起来也不加深（两边一起用这条 keyframes）。 */
  @keyframes syncFlashYellow {
    0%   { opacity: var(--sync-flash-alpha); }
    76%  { opacity: var(--sync-flash-alpha); }
    100% { opacity: 0; }
  }
  /* 编译错误行（2026-09-30）：行底色泛红 + 左侧红条 + 行号前一个红点。
     只由**右栏「排版预览」**那次编译来标（它编的就是源码页里那份文本）；点/悬停看详情。 */
  .handout-err-line {
    background: #fdeceb;
    box-shadow: inset 4px 0 0 0 #d92d20;
  }
  .handout-err-dot {
    display: inline-block;
    width: 6px;
    height: 6px;
    margin-right: 2px;
    border-radius: 50%;
    background: #d92d20;
    vertical-align: middle;
  }

  /* ---------- 源码页「搜索 / 替换」浮层（自定义模板，见 openSearchDialog） ----------
     只由 handoutCm 的 Cmd-F / Cmd-R 打开，别的 CodeMirror 实例没有这套 UI。
     刻意**不改** vendor 的 dialog.min.css（它带 SHA-256 校验），全部在这里覆盖：
     vendor 那套只有 .1em 内边距 + 输入框去边框 + 按钮 70% 字号，与工作台设计语言差得远。 */
  .CodeMirror-dialog { padding: 0; border: 0; background: transparent; z-index: 40; }
  .CodeMirror-dialog-top, .CodeMirror-dialog-bottom { border: 0; }
  .cm-dialog-form {
    background: #fff;
    border-top: 1px solid var(--border);
    box-shadow: 0 -2px 10px rgba(15, 23, 42, 0.08);
    /* 上下留白做成一致：上面多 1px 是 border-top 占的，所以 padding-bottom 补成 9px
       （实测：上留白 = 1 + 8 = 9px，下留白 = 9px）。 */
    padding: 8px 8px 9px;
    font-size: 12.5px;
    color: inherit;
  }
  /* 两行用同一套栅格：[图标 14px][输入框 1fr][右侧固定 176px] ——
     右侧那一列等宽，两个输入框自然等长、左右边界对齐（不靠两行各自的长短去凑）。 */
  .cm-dialog-row {
    display: grid;
    grid-template-columns: 12px minmax(0, 1fr) 142px;
    align-items: center;
    gap: 10px;   /* 图标↔输入框、输入框↔右侧那一列，都是 10px —— 框右边界与右侧元素留出呼吸感 */
  }
  /* 右侧那一列内部照旧左右排；靠右对齐，与主色"替换"按钮的右边框对齐输入框右边界 */
  .cm-dialog-right { display: flex; align-items: center; justify-content: flex-end; gap: 6px; }
  .cm-dialog-row + .cm-dialog-row { margin-top: 6px; }
  .cm-dialog-icon { width: 14px; text-align: center; color: var(--muted); font-size: 12px; }
  .CodeMirror-dialog input.cm-dialog-input {
    flex: 1 1 auto; min-width: 0; width: auto;
    padding: 3px 8px; border: 1px solid var(--border-strong); border-radius: 6px;
    /* 查找框里打的是代码，所以跟着代码框的字形与字号走（--code-*，见 :root）。
       ⚠️ 右侧那一列（.cm-dialog-count / .cm-dialog-btn）**故意留在 12.5px**：
       那一列是固定 142px 的栅格（见 .cm-dialog-row），16px 的「全部替换」四个字
       加内边距会把它撑破；它们是按钮不是代码，不需要跟字号。 */
    background: #fff; color: inherit; font: inherit;
    font-family: var(--code-font); font-size: var(--code-size);
    outline: none;
  }
  .CodeMirror-dialog input.cm-dialog-input:focus {
    border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft);
  }
  .CodeMirror-dialog input.cm-dialog-input.cm-dialog-nohit { border-color: var(--c-color, #d92d20); }
  .cm-dialog-count { flex: none; min-width: 36px; text-align: center; color: var(--muted); }
  .CodeMirror-dialog button.cm-dialog-btn {
    flex: none; padding: 3px 10px; font: inherit; font-size: 12.5px; font-weight: 400;
    color: var(--accent); background: #fff; border: 1px solid var(--border-strong);
    border-radius: 6px; cursor: pointer;
  }
  .CodeMirror-dialog button.cm-dialog-btn:hover { background: var(--accent-soft); }
  .CodeMirror-dialog button.cm-dialog-btn.cm-dialog-primary {
    background: var(--accent); border-color: var(--accent); color: #fff;
  }
  .CodeMirror-dialog button.cm-dialog-btn.cm-dialog-primary:hover {
    background: #2459c8; border-color: #2459c8;
  }
  .CodeMirror-dialog button.cm-dialog-close { color: var(--muted); padding: 3px 8px; }
  /* 命中高亮：与项目主色一致（vendor 那套只给当前项一小段下划线） */
  .cm-dialog-match { background: rgba(47, 111, 237, 0.14); }
  .cm-dialog-match-current { background: rgba(47, 111, 237, 0.34); }
  /* ⚠️ 跳转高亮**画在伪元素上**，不占行自己的 background（2026-10-17 改）。
     原来就是直接写在 `.sync-flash-line` 上的，问题是：跳转后光标正好落在那行，
     `.cm-active-line`（当前行底色）跟本类是**同一个** `.CodeMirror-linebackground` div ——
     两条 background 只能活一条；而淡出用的是**整层的 opacity**，
     于是本类一旦赢，淡出过程中整层变透明：用户看到的是
     "先白一下（当前行底色被自己盖掉了）→ 2.6 秒后类被摘掉才回到灰底"
     （就是用户 2026-10-17 报的第二步现象）。
     拆成伪元素后：黄底盖在上面渐隐，**下面那层当前行底色自始至终都在**，
     淡出过程就是从黄慢慢透出灰，没有中间那一下白。
     （左边的琥珀色粗竖条一起搬进伪元素；`.CodeMirror-linebackground` 本身就是
      `position:absolute; inset:0`，所以伪元素直接 `inset:0` 即可，不必给本类加 position。） */
  .sync-flash-line::before {
    content: '';
    position: absolute;
    inset: 0;
    background: var(--sync-flash);
    animation: syncFlashYellow 2.6s ease-out forwards;
    /* 左边那根琥珀色粗竖条：黄色底之外再加一个"箭头"式标记，扫一眼就知道看哪儿 */
    box-shadow: inset 4px 0 0 0 var(--sync-flash-edge);
  }
  .rpreview-pdfflash {
    position: absolute;
    inset: 0;
    pointer-events: none;
    /* 高亮用**正片叠底**：黄 × 白纸 = 黄纸，黄 × 黑字 = 黑字 —— 题目文字 / 图 / 公式
       一个像素都不会被盖住（普通半透明叠加会把字"蒙"上一层黄，看着糊）。
       纸是白的，所以叠出来的**颜色与源码页那行底色逐像素相同**（实测两边都是
       rgb(254, 237, 193)）；不支持 mix-blend-mode 的浏览器就只是一块浅黄，也还能看。
       ⚠️ 正片叠底只放在**这一层**（里面那些小框不许各自带 mix-blend-mode）：
       `synctex view` 会把同一块重复报很多次（实测一道图题的那块重复 47 条），
       每个框各自去跟画布乘一次的话，G 通道会被乘到 0 —— 黄色变成纯红。
       浓度也放在这层（opacity），里面的框填**不透明**的同色黄 —— 于是不管叠几层，
       颜色都是"黄 × 白纸"这一种。 */
    mix-blend-mode: multiply;
    opacity: var(--sync-flash-alpha);
    animation: syncFlashYellow 2.6s ease-out forwards;
  }
  /* 一条 `synctex view` 记录 = 一个**纯矩形**：位置/尺寸就是后端给的那四个数（乘 scale），
     除此之外一个样式都不加 —— 没有圆角、没有边框、没有阴影、没有内边距、没有缩放进场。
     用户要的就是"引擎原生盒子的样子"（第一版高亮是圆角 + 琥珀描边 + 光晕，看着像
     一条条浮在文字外的胶囊，跟原生盒子不是一回事）。 */
  .rpreview-pdfflash-box {
    position: absolute;
    background: var(--sync-flash);
  }
  /* 渲染中 / 出错时的浮层：出错时用暖红，跟其它错误提示一个调性 */
  .rpreview-pdfnote {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    text-align: center;
    font-size: 12px;
    color: #f1f5f9;
    background: rgba(15, 23, 42, 0.55);
  }
  .rpreview-pdfnote.err { color: #fecdcd; background: rgba(69, 10, 10, 0.72); }
  /* HUD 行：适应宽度单独一个圆灰块，放在 - / 页码 / + 的左边。
     两个灰块用同一高度并垂直居中，避免圆块和胶囊上下错位。 */
  .rpreview-pdfhud-row {
    position: absolute;
    /* 给右侧滚动条留出空隙：整组 HUD 都落在 PDF 预览内容区内。 */
    right: 30px;
    bottom: 10px;
    display: flex;
    align-items: center;
    gap: 6px;
    opacity: 0.3;
    transition: opacity 0.15s;
  }
  .rpreview-pdfhud-row > .rpreview-pdfhud {
    position: static;
    right: auto;
    bottom: auto;
    height: 24px;
    box-sizing: border-box;
  }
  .rpreview-pdfhud-fit {
    width: 24px;
    padding: 2px;
    justify-content: center;
  }
  /* 用 SVG 画 ↔：水平线固定在 16×16 视框正中，
     不再受字体基线影响，箭尖与减号始终同一条水平线。 */
  .rpreview-pdfhud-fit button {
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .rpreview-pdfhud-fit-icon {
    display: block;
    width: 14px;
    height: 14px;
  }
  /* 右下角那个极小的角标：页码 + 缩放。没有工具条；鼠标不靠近几乎看不见。 */
  .rpreview-pdfhud {
    position: absolute;
    right: 10px;
    bottom: 10px;
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 2px 4px;
    border-radius: 999px;
    background: rgba(51, 65, 85, 0.82);
    color: #f8fafc;
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    user-select: none;
  }
  /* HUD 行是唯一的明暗状态源：无论悬停圆块、翻页胶囊还是其中按钮获得焦点，
     两个胶囊都一起变深，不会各自停在不同的透明度。 */
  .rpreview-pdfhost:hover .rpreview-pdfhud-row,
  .review-pdfhost:hover .rpreview-pdfhud-row,
  .rpreview-pdfhud-row:hover,
  .rpreview-pdfhud-row:focus-within { opacity: 1; }
  .rpreview-pdfhud button {
    width: 20px;
    height: 20px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: inherit;
    font-size: 13px;
    line-height: 1;
    cursor: pointer;
    transition: background 0.14s ease, transform 0.1s ease, opacity 0.14s ease;
  }
  .rpreview-pdfhud button:hover { background: rgba(255, 255, 255, 0.2); }
  .rpreview-pdfhud button:active:not(:disabled) { transform: scale(0.88); }
  /* 页码和左右翻页三角只是一组控件，不再额外绘制内层胶囊。 */
  .rpreview-pdfhud-pager {
    display: flex;
    align-items: center;
    gap: 1px;
  }
  .rpreview-pdfhud-page { min-width: 48px; text-align: center; }
  .rpreview-pdfhud-nav {
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .rpreview-pdfhud-nav-icon {
    width: 11px;
    height: 11px;
    fill: currentColor;
    stroke: none;
    transition: transform 0.14s ease, opacity 0.14s ease;
  }
  .rpreview-pdfhud-nav:not(:disabled):hover .rpreview-pdfhud-nav-prev {
    transform: translateX(-1px);
  }
  .rpreview-pdfhud-nav:not(:disabled):hover .rpreview-pdfhud-nav-next {
    transform: translateX(1px);
  }
  .rpreview-pdfhud-nav:disabled {
    cursor: default;
    opacity: 0.28;
  }
  .rpreview-pdfhud-nav:disabled:hover { background: transparent; }
  /* 局部放大镜：白底 + 多层柔灰光晕，不做生硬描边；pointer-events:none 不抢事件。 */
  .rpreview-pdf-lens {
    position: fixed;
    z-index: 1200;
    display: none;
    width: 300px;
    height: 300px;
    border-radius: 50%;
    overflow: hidden;
    background: #fff;
    pointer-events: none;
    box-shadow:
      0 0 0 1px rgba(255, 255, 255, 0.95),
      0 0 26px 12px rgba(100, 116, 139, 0.18),
      0 0 58px 26px rgba(100, 116, 139, 0.14),
      0 18px 40px rgba(15, 23, 42, 0.24);
  }
  .rpreview-pdf-lens canvas { display: block; width: 100%; height: 100%; }
  .rpreview-error { padding: 2px; }
  /* "这张图过期了"的提示条：跟卡片区那些 ct-note 一个观感 */
  .rpreview-stale { margin: 0 0 8px; }
  /* 发送相关的提示（下载中 / 已下载 / 失败）就贴在发送按钮下面一行 ——
     底栏那行离得远，用户根本看不到。 */
  .rpreview-sendnote {
    flex: none;
    margin: 0;
    padding: 6px 10px;
    border-bottom: 1px solid var(--border);
    background: #fbfcfd;
    font-size: 11.5px;
    line-height: 1.6;
    color: var(--muted);
  }
  .rpreview-sendnote.ok { color: var(--a-color); }
  .rpreview-sendnote.err { color: var(--c-color); }
  .rpreview-error details { margin-top: 6px; }
  .rpreview-error summary {
    cursor: pointer;
    font-size: 11.5px;
    color: var(--muted);
    user-select: none;
  }
  .rpreview-log {
    max-height: 260px;
    margin: 6px 0 0;
    padding: 8px;
    overflow: auto;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: #f7f8fa;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 11px;
    line-height: 1.5;
    white-space: pre-wrap;
    color: #3b4453;
  }
  .rp-why {
    margin-top: 12px;
    text-align: left;
    font-size: 11.5px;
    line-height: 1.9;
    color: var(--muted);
  }
  .handout-msg { padding: 8px 12px; color: var(--muted); font-size: 12.5px; line-height: 1.7; }
  .handout-msg.error { color: var(--c-color); }
  /* 编辑器下半区：填满剩余高度。宿主与 CM 都绝对定位撑满，滚动交给 CM 自己 */
  .handout-editor { position: relative; flex: 1 1 auto; min-height: 0; }
  .handout-editor-host { position: absolute; inset: 0; }
  .handout-editor-host .CodeMirror {
    height: 100%;
    /* 字体/字号/行高走 --code-* 变量（见 :root）。2026-10-15 由 12.5px / 1.55
       改成 TeXstudio 观感：Consolas 16px / 行高固定 18.5px。 */
    font-family: var(--code-font);
    font-size: var(--code-size);
    line-height: var(--code-line);
  }
  .handout-editor-host .CodeMirror-gutters { background: #f7f8fa; border-right: 1px solid var(--border); }
  .handout-hint {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 20px;
    text-align: center;
    color: var(--muted);
    font-size: 12.5px;
    line-height: 1.8;
    background: var(--panel);
  }
  /* 底栏：当前路径 + 预览状态 + 提示 + 编译组 / 发送组 / 保存组。
     **允许折行**：右栏最窄能拖到 280px（COL_RIGHT_MIN），那时候这一行的按钮组
     就整体折到第二行 —— 每组自带 flex:none，所以折行是按"组"断的，不会从中间劈开。 */
  .handout-foot {
    /* 底栏**永远只有一行**（用户 2026-09-29 明确要求：不许加高、不许改行）：
       左动作组 / 中段（提示·错误·编译结果）/ 右动作组。
       `container-type: inline-size` 只为一件事：让下面那条 @container 能按**底栏自己的
       宽度**判断"还放得下统计吗"（右栏宽度是用户拖出来的，媒体查询看窗口宽没用）。
       底栏的行内尺寸由外层栅格给、不依赖内容，所以加这条不改变任何布局。 */
    position: relative;
    container-type: inline-size;
    flex: none;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    row-gap: 6px;
    padding: 6px 10px;
    border-top: 1px solid var(--border);
    background: #fbfcfd;
  }
  /* 原来这里有一条 .handout-path（底栏最左边那份文件路径）—— 用户 2026-09-27 要求去掉，
     规则一并删掉。「• 未保存」那枚小字搬到了顶行保存按钮右侧（见 .head-save-hint），
     完整路径仍能从「保存」的悬停提示里看到（handoutSaveTitle）。 */
  /* 提示 / 状态都是"能挤就先挤我"：min-width:0 + 省略号，绝不把按钮顶出行外
     （完整文案一律在各自的 title 悬停里）。 */
  .handout-note {
    font-size: 12px;
    color: var(--muted);
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .handout-note.ok { color: var(--a-color); }
  .handout-note.err { color: var(--c-color); }
  /* 「一键排序」那三条提示里，两条是"没排成 / 排了一部分"（既不是成功也不是报错）
     —— 用第三档橙色（--b-color，与题卡上的 ⚠ 同一支）。 */
  .handout-note.warn { color: var(--b-color); }
  /* （原来这里有一条 .foot-status：编译结果常驻时的样式。现在编译结果走 footSlot
     那一个槽位、几秒自消，用不到单独一条了。） */
  /* 一组动作（按钮 + 勾选框）：整组不压缩、整组参与折行 —— 折行时必须同进同退 */
  .foot-group { flex: none; display: flex; align-items: center; gap: 6px; }
  /* 中间那段（提示 / 错误 / 编译结果 / 只读说明；绝对居中的统计也挂在这里面）：
       · flex-basis 给 0 —— 折行是按"假设主尺寸"算的，用 auto 的话一段长提示
         自己就能把这一行撑成两行（真机踩过）；
       · **flex-grow 1** —— 它吃掉左右两组之间的全部空白，于是左边「编译 + 解析版」
         贴左边、右边「整合版 + 发送」**贴右边**（用户 2026-09-29 明确要求：发送按钮
         与整合版框的位置不许动，就在右边）。这一条**不能删**：少了它 `.foot-mid`
         退化成不伸缩的普通元素，右边的发送组就会被挤到中间来。
       · min-width:0 + 省略号 —— 再长也只在自己这一格里截断，
         **永远不会挤动左边「编译」和右边「发送」**。 */
  .foot-mid {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    overflow: hidden;
  }
  .foot-mid > .handout-note {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* 勾选框按「保存」的视觉高度配：同一行里按钮是 padding 3px + 12.5px 行高 = 25px，
     这里用 height 钉死同一个数，字号/颜色沿用 .handout-note 那套灰字。 */
  .foot-check {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    height: 25px;
    font-size: 12.5px;
    color: var(--muted);
    white-space: nowrap;
    cursor: pointer;
    user-select: none;
  }
  .foot-check input {
    width: 13px;
    height: 13px;
    margin: 0;
    accent-color: var(--accent);
    cursor: pointer;
  }
  .foot-check.off { opacity: 0.5; cursor: not-allowed; }
  .foot-check input:disabled { cursor: not-allowed; }
  /* 底栏那排动作按钮，和顶行「保存」共用同一套尺寸（一样高、一样圆角、一样字号）
     —— 它就是从那排搬上去的，外观不该跟着变。 */
  .handout-foot button,
  .head-save-group button {
    flex: none;
    padding: 3px 10px;
    border-radius: 6px;
    border: 1px solid var(--border-strong);
    background: #fff;
    cursor: pointer;
    font-size: 12.5px;
  }
  .handout-foot button.primary,
  .head-save-group button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
  /* 底栏那两颗（编译 / 发送）= 保存同款：主色实心底 + 白字 + 600 字重。
     **必须写在上面那条 `.handout-foot button`（它把底色刷成 #fff）之后**：
     两条选择器权重相同（都是 1 类名 + 1 元素），靠后的胜 —— 写在前面会被白底盖掉，
     只剩白字白底，按钮看着就是两个空框（真机踩过一次）。 */
  .foot-group button {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
    font-weight: 600;
  }
  /* 「保存」加粗一档：与同一行的 新建 / 打开（.mini-action 本来就是 600）、
     以及 tab 那排选中的那一颗（.tabs button.active 也是 600）同一个字重 ——
     顶行里所有"有分量的字"都停在 600，不引入 700 那种更重的层次。 */
  .head-save-group button.primary { font-weight: 600; }
  .handout-foot button:disabled,
  .head-save-group button:disabled { opacity: 0.5; cursor: not-allowed; }
  /* 顶行那两颗（tab 右边）：整组不压缩，跟着 .head-left 一起折行 */
  .head-save-group { display: flex; align-items: center; gap: 6px; flex: none; }
  /* 保存按钮右边那枚「• 未保存 / 还没落盘」：原来跟文件路径挤在底栏最左边，
     现在贴着保存按钮（`.head-save-group` 的 gap 就是它们之间的间距）。
     warn = 真有东西没写回磁盘（未保存 / 草稿），用与原来 `.handout-path.dirty` 同一个
     琥珀色 + 600；「未上传」那种只是状态说明，保持灰字。 */
  .head-save-hint { font-size: 12px; color: var(--muted); white-space: nowrap; }
  .head-save-hint.warn { color: var(--b-color); font-weight: 600; }
  /* 右栏顶行的「新建 / 打开」：**尺寸对齐「保存」**（同宽、同高、同字号、同圆角、
     同内边距），只保留它们自己的配色（白底 + 主色字，`.mini-action` 那套）。
     作用域只圈右栏这一格 —— `.mini-action` 全仓几十处在用，一个字节都不能动。
     选择器比 ≤1400px 那条（.panel-head .head-right .mini-action）多一层，所以窄档
     也不会被它收小，"同宽"在任何窗口宽度下都成立。 */
  .handout-panel > .panel-head .head-right .mini-action {
    padding: 3px 10px;
    font-size: 12.5px;
  }
  /* 另存为对话框里的一行 */
  .handout-row { display: flex; align-items: center; gap: 8px; margin: 0 0 10px; }
  .handout-row label { flex: none; width: 44px; font-size: 13px; color: var(--muted); }
  .handout-row select,
  .handout-row input {
    flex: 1 1 auto;
    min-width: 0;
    padding: 5px 8px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    font-size: 13px;
    outline: none;
    background: #fff;
  }
  .handout-row input:focus, .handout-row select:focus { border-color: var(--accent); }
  /* 「新建 → 选模板」那五张卡：两列，画得像"可选的东西"而不是一行按钮 */
  .tpl-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
    /* 下边距 18px：要和**上面那处留白看起来一样宽**（用户 2026-09-28 要求）。
       实测两处"眼睛看到的"留白是：
         · 标题 → 卡片顶边 = **17.9px**（题干那行 13.5px 字，行框比字形多出约 3.9px 下沿）；
         · 卡片底边 → 按钮行 = 这里给的值。
       所以这里写 18 才是"看起来一样"；写 12（原来）或 14（标题的 margin 数值）都会显得
       按钮那处更挤 —— 用户实测反馈的就是这个。 */
    margin: 0 0 18px;
  }
  .tpl-card {
    /* 相对定位：选中角标（.selected::after）绝对定位挂在右上角，不影响布局 */
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    padding: 10px 12px;
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    background: #fff;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
  }
  .tpl-card:hover { border-color: var(--accent); background: var(--accent-soft); }
  /* 选中态（用户 2026-09-28：先选、再点「确定」才新建）。
     hover 也是"主色边框 + 浅主色底"，所以选中必须比 hover **更重一档**才有区分：
     多一圈**内阴影描边**（不占布局、不会让卡片跳一下）+ 右上角一枚「已选」小角标。 */
  .tpl-card.selected {
    border-color: var(--accent);
    background: var(--accent-soft);
    box-shadow: inset 0 0 0 1px var(--accent);
  }
  .tpl-card.selected b { font-weight: 700; }
  .tpl-card.selected::after {
    content: '已选';
    position: absolute;
    top: 6px;
    right: 8px;
    padding: 0 5px;
    border: 1px solid var(--accent);
    border-radius: 999px;
    background: #fff;
    color: var(--accent);
    font-size: 10.5px;
    font-weight: 600;
    line-height: 15px;
  }
  .tpl-card b { font-size: 13.5px; color: var(--accent); }
  .tpl-card span { font-size: 11.5px; line-height: 1.6; color: var(--muted); }
  /* 「新建 → 讲义汇总」：系列选择 + 左右两列（可选章节 / 已选顺序）。
     章节可能有几十个，两列各自滚动，弹窗本身不跟着长高。 */
  /* 汇总弹窗宽度 = 原 660px 的一半；两个字段都走 .ct-line/.ct-field，
     标签字体、控件高度与左右边沿由普通表单同一套规则保证。 */
  .modal.lecture-index-modal {
    width: 330px;
    max-width: calc(100vw - 40px);
    position: relative;
  }
  /* 汇总系列：自定义下拉。原生 <select> 的系统菜单会保留勾选槽，无法让候选项
     与上方标题输入框严格同一条左沿；这里自绘触发器与列表，边框内文字统一 8px。 */
  .li-series-select {
    position: relative;
    flex: 1 1 auto;
    min-width: 0;
  }
  .li-series-trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    width: 100%;
    height: 27px;
    box-sizing: border-box;
    padding: 0 8px;
    text-align: left;
    cursor: pointer;
  }
  .li-series-trigger:disabled { background: #f7f8fa; cursor: default; opacity: 0.72; }
  .li-series-trigger-text {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .li-series-trigger.is-placeholder .li-series-trigger-text { color: var(--muted); }
  .li-series-trigger.has-value .li-series-trigger-text {
    color: var(--accent);
    font-weight: 600;
  }
  .li-series-chevron {
    flex: none;
    width: 0;
    height: 0;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-top: 5px solid #6b7280;
    transition: transform 0.14s ease;
  }
  .li-series-trigger[aria-expanded="true"] .li-series-chevron {
    transform: rotate(180deg);
  }
  .li-series-menu {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    z-index: 90;
    max-height: 284px;
    overflow-y: auto;
    border: 1px solid var(--border-strong);
    border-radius: 7px;
    background: #fff;
    box-shadow: 0 10px 28px rgba(20, 30, 50, 0.2);
  }
  .li-series-option {
    display: block;
    width: 100%;
    min-height: 26px;
    padding: 4px 8px;
    border: 0;
    background: transparent;
    color: var(--text);
    font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC",
      "Helvetica Neue", sans-serif;
    font-size: 13px;
    line-height: 18px;
    text-align: left;
    white-space: normal;
    word-break: break-word;
    cursor: pointer;
  }
  .li-series-option.active { background: var(--accent-soft); }
  .li-series-option[aria-selected="true"] {
    color: var(--accent);
    font-weight: 600;
  }
  .li-series-option:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
  /* 模板卡 → 汇总弹窗是同一个流程。两层遮罩若先后替换，中间会露一帧背景；
     这里用一层持续存在的底遮罩，两个弹窗自己的 mask 只负责定位与点穿拦截。 */
  .modal-mask.summary-modal-mask { background: transparent; }
  .summary-modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: 49;
    background: rgba(24, 28, 36, 0.35);
    pointer-events: none;
  }
  /* 保存失败等红字：浮在弹窗下方，不占布局、不把按钮挤下去；5 秒后自动消失。 */
  .lecture-index-notice {
    position: absolute;
    top: calc(100% + 8px);
    left: 0;
    right: 0;
    z-index: 80;
    margin: 0;
    padding: 7px 10px;
    border: 1px solid #e7b4ae;
    border-radius: 7px;
    background: #fdeceb;
    color: #8f1d16;
    font-size: 12px;
    line-height: 1.55;
    box-shadow: 0 6px 18px rgba(90, 35, 30, 0.16);
    white-space: normal;
    word-break: break-word;
  }
  .li-hint { margin: 0 0 12px; font-size: 12px; color: var(--muted); line-height: 1.7; }
  .li-columns {
    display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
    margin: 0 0 12px;
  }
  .li-col {
    min-height: 188px; max-height: 300px; overflow: auto;
    border: 1px solid var(--border); border-radius: 8px; background: #fbfcfd;
  }
  .li-col-head {
    position: sticky; top: 0; z-index: 1;
    padding: 7px 9px; border-bottom: 1px solid var(--border);
    background: #f4f6f9; font-size: 12px; font-weight: 600; color: #4a5564;
  }
  .li-col-head span { float: right; color: var(--muted); font-weight: 400; }
  .li-check {
    display: flex; align-items: baseline; gap: 7px;
    padding: 7px 9px; border-bottom: 1px solid #eef1f5;
    font-size: 12.5px; cursor: pointer;
  }
  .li-check:last-child { border-bottom: 0; }
  .li-check input { flex: none; margin: 0; }
  .li-check span { flex: 1 1 auto; min-width: 0; }
  .li-check small {
    flex: none; max-width: 132px; overflow: hidden; text-overflow: ellipsis;
    color: var(--muted); font-size: 10.5px; white-space: nowrap;
  }
  .li-tools { display: flex; gap: 8px; margin: -2px 0 10px; }
  .li-tools button {
    padding: 2px 8px; border: 1px solid var(--border-strong); border-radius: 5px;
    background: #fff; font-size: 11.5px; cursor: pointer;
  }
  .li-order-row {
    display: flex; align-items: center; gap: 6px;
    padding: 6px 8px; border-bottom: 1px solid #eef1f5; font-size: 12.5px;
  }
  .li-order-row:last-child { border-bottom: 0; }
  .li-order-no { flex: none; width: 18px; color: var(--muted); text-align: right; }
  .li-order-title {
    flex: 1 1 auto; min-width: 0; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap;
  }
  .li-order-row button {
    flex: none; width: 24px; height: 22px; padding: 0;
    border: 1px solid var(--border-strong); border-radius: 5px;
    background: #fff; font-size: 12px; line-height: 20px; cursor: pointer;
  }
  .li-order-row button:disabled { opacity: 0.35; cursor: default; }
  .li-empty { padding: 16px 10px; color: var(--muted); font-size: 12px; text-align: center; }
  /* 「地址」那一格 = 输入框 + 「选择/确认」按钮（浮层挂在这一格里）。
     它一直延伸到行的右边缘，所以浮层的宽度与左边缘都跟下面的「命名」框一致。 */
  .address-field { position: relative; flex: 1 1 auto; min-width: 0; display: flex; gap: 8px; }
  /* 「选择/确认」按钮：原来是个**没写过样式**的原生按钮 —— 浏览器按系统外观画，
     边框又粗又黑（computed 值看不出这一层）。这里跟面板里的「新建文件夹」用同一套
     细边框/圆角/字号，appearance:none 关掉原生外观；底色用站点已有的浅灰 #eef1f6
     （跟白色输入框区分开，一眼看出它是"按钮"而不是输入框）。
     ⚠️ 必须写成**直接子选择器 `>`**（2026-10-09 修）：这一格（.address-field）里还嵌着
     整个下拉浮层（.dir-picker），里面有右键菜单项（.wsb-menu-item）和虚拟夹的 ✕
     （.dir-picker-del）。后代写法 `.address-field button` 的权重 (0,1,1) 会盖过
     `.wsb-menu-item` (0,1,0) —— 于是菜单项被画成"浅灰底 + 细边框"的小按钮框
     （用户报的"菜单项不该是按钮框"）。 */
  .address-field > button {
    flex: none;
    padding: 3px 10px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #eef1f6;
    appearance: none;
    cursor: pointer;
    font-size: 12.5px;
  }
  /* 「选择保存地址」面板：只列**当前根**（M课测 / M试卷 / …）下的目录（后端仍按最终路径拼，页面这里只是选名字）。
     目录树数据复用右栏文件树那一份，所以不会出现"选了个不存在的目录"。 */
  .dir-picker {
    /* 下拉浮层：绝对定位挂在「地址」行下面，盖住下面的行（不撑高弹窗）。
       位置与宽度由 .address-field（= 地址框那一格）决定：左右边缘与地址框逐像素对齐。 */
    position: absolute;
    left: 0;
    right: 0;
    top: calc(100% + 3px);
    z-index: 6;
    margin: 0;
    max-height: 232px;
    /* 面板本身不再滚动：上面那条「新建文件夹」栏固定、只有下面的列表滚 ——
       这样"钉在列表顶部的根行"永远不会盖住那颗按钮（用户 2026-09-28 要求）。 */
    display: flex;
    flex-direction: column;
    overflow: hidden;
    /* 滚轮滚到头也不再漏到页面后面去（页面背后那层不该跟着动） */
    overscroll-behavior: contain;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: #fff;
    box-shadow: 0 10px 24px rgba(20, 24, 32, 0.18);
  }
  /* 2026-10-09：面板顶部那条「新建文件夹」固定栏已删除 —— 新建 / 重命名改走**右键菜单**
     （.picker-menu，样式借用 .wsb-menu）。列表因此长高约 39px（.dir-picker-list 是 flex:1）。 */
  /* 面板自己的提示（改名被拒之类）：不复用弹窗那条报错 —— 那一条会被预览回包清掉。
     2026-10-09（用户要求）：改成**限时浮窗** —— 绝对定位浮在面板上沿，不再占一行、
     不再把下面的列表顶下去（用户反馈的"红字挤压下拉框"）；3 秒自消（走 flashNotice，
     与项目里其它 .float-notice 同一套口径）。出现期间 pointer-events:none，不挡底下那些行。 */
  .dir-picker-notice {
    position: absolute;
    left: 8px;
    right: 8px;
    top: 6px;
    z-index: 7;
    margin: 0;
    padding: 6px 10px;
    font-size: 12px;
    line-height: 1.5;
    color: var(--c-color);
    background: #fdf3f2;
    border: 1px solid #f2c2bf;
    border-radius: 8px;
    box-shadow: 0 6px 18px rgba(20, 24, 32, 0.16);
    pointer-events: none;
  }
  .dir-picker-row {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 3px 8px 3px 0;
    font-size: 12.5px;
  }
  .dir-picker-row:hover { background: var(--accent-soft); }
  .dir-picker-row.current { background: var(--accent-soft); font-weight: 600; color: var(--accent); }
  /* 第一行（根名，如「M课测」）：树根，加粗 + 一点点底色，跟里面的文件夹区分开 */
  .dir-picker-row.root { font-weight: 600; background: #fbfcfd; }
  .dir-picker-row.root:hover { background: var(--accent-soft); }
  /* 列表本体：面板里**唯一**能滚的那一块（栏与提示行固定在上面） */
  .dir-picker-list {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    overscroll-behavior: contain;
  }
  /* 2026-10-09（根下拉并入地址树）：顶层不再是**一个**根，而是并列的几个根 ——
     原来"把唯一的根行钉在列表顶部"的 sticky 规则在这里只会挡住别的根，去掉。 */
  .dir-picker-row .caret {
    width: 14px;
    flex: none;
    text-align: center;
    color: var(--muted);
    font-size: 10px;
    cursor: pointer;
  }
  .dir-picker-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
  /* 本次新建的文件夹（还没落盘）打一个小「新」标；改名时整行换成输入框 */
  .dir-picker-tag {
    flex: none;
    padding: 0 4px;
    border-radius: 4px;
    background: var(--accent-soft);
    color: var(--accent);
    font-size: 10px;
    line-height: 15px;
  }
  /* 期7b：保存树里"这是别人的可编辑目录"那颗胶囊 —— 与「新」同形状、换琥珀色系
     （一眼看出"不是自己的目录"，别跟"本地新建"混） */
  .dir-picker-tag.share {
    background: #fdf6ec;
    color: #96601a;
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .dir-picker-rename {
    flex: 1 1 auto;
    min-width: 0;
    padding: 2px 6px;
    /* 这个输入框比"名字那一行"高 6px（自带 padding + 边框），不抵掉的话
       双击改名时这一行会从 27px 长到 33px —— 下面的行跟着跳一下（用户 2026-09-28
       实测"双击改名，文件树会滑动一定距离"，那一下既有滚动也有这个高度差）。
       -3px 上下外边距把多出来的 6px 抵掉：行高不变，输入框正好填满这一行。 */
    margin: -3px 0;
    border: 1px solid var(--accent);
    border-radius: 5px;
    font-size: 12.5px;
    outline: none;
  }
  /* 虚拟文件夹行尾的删除（删错了才用得上；磁盘上本来就没有它） */
  .dir-picker-del {
    flex: none;
    margin-left: auto;
    padding: 0 5px;
    border: none;
    border-radius: 4px;
    background: transparent;
    color: var(--muted);
    cursor: pointer;
    font-size: 11px;
    line-height: 16px;
  }
  .dir-picker-del:hover { background: var(--accent-soft); color: var(--c-color); }
  .dir-picker-empty { padding: 8px; font-size: 12px; color: var(--muted); }
  /* 某个根展开后没有子目录：挂在那一行右边的浅字说明（2026-10-09） */
  .dir-picker-hint { font-size: 11px; color: var(--muted); }
  .modal .modal-error { margin: 0 0 12px; font-size: 12.5px; color: var(--c-color); line-height: 1.6; }
  /* 保存课测对话框的「结果区」（将保存到… / 报错）高度固定：
     弹窗是垂直居中的（.modal-mask align-items:center），这块内容一来一回会让弹窗变高
     变矮 —— 面板就跟着在鼠标底下上下移动；滚轮手势在开始那一刻就锁定了滚动目标，
     面板一跳就表现为"悬停着滚不动"。预留固定高度，滚的过程中弹窗不再跳。 */
  /* 高度按"一行提示"给够就行 —— 别在「取消/确定」上方留一大块空白（用户要求）。
     目标路径一行到底、太长就省略号（悬停看全），这样这块高度恒定，弹窗不会跳。 */
  .create-foot { min-height: 18px; }
  /* 这个对话框里三行之间也收紧一点（「确定」离上方更近） */
  .create-modal .handout-row { margin-bottom: 8px; }
  .create-target {
    line-height: 1.35;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* 子元素的上下外边距都归零 —— 否则 `.modal .modal-note` 那条负的上外边距会"穿透"
     这一块（外边距折叠），块的高度又跟着内容变，弹窗照样跳（实测还有 -8px）。 */
  .modal .create-foot > .modal-note,
  .modal .create-foot > .modal-error { margin: 0; }
  /* 保存课测对话框：标签只占文字本身的宽度（原来是固定 44px 的框，输入框就离字老远），
     三个标签都是两个字，所以还是对齐的。只作用于这个对话框，另存为那个不动。 */
  .create-modal .handout-row label { width: auto; }
  /* 「地址」是只读的：灰字 + 默认箭头（不是 I 形文本光标），一眼看出不能填 */
  .dir-address { color: var(--muted); cursor: default; }
  .dir-address:focus { border-color: var(--border-strong); }

  /* 预览面板里折叠的「原始 LaTeX 片段」 */
  .code-wrap { position: relative; }
  .raw-latex { margin-top: 10px; }
  .raw-latex > summary {
    cursor: pointer;
    font-size: 12px;
    color: var(--muted);
    user-select: none;
  }
  /* 语法高亮配色：用站点调色板，将来加暗色模式只需改这一处 */
  .latex-source .token.comment { color: #8a9199; font-style: italic; }
  .latex-source .token.keyword,
  .latex-source .token.function,
  .latex-source .token.regex { color: var(--accent); }
  .latex-source .token.punctuation { color: #b26a00; }
  .latex-source .token.string,
  .latex-source .token.equation { color: #0b7285; }
  .latex-source .token.number { color: #b3261e; }
  .latex-source .token.headline,
  .latex-source .token.class-name { color: #1a7f37; }
  .latex-source .token.url { color: #6f42c1; }

  .edit-btn {
    margin-left: auto;
    padding: 1px 8px;
    border: 1px solid var(--border-strong);
    border-radius: 4px;
    background: #fff;
    color: var(--accent);
    cursor: pointer;
    font-size: 12px;
  }
  .edit-btn:hover { background: var(--accent-soft); }
  /* 搜索页那几个「回退 / 定位」按钮：**蓝底白字**（与「保存」同款实心主色）。
     用户 2026-09-28 要求：「回到目录」「返回浏览 / 返回搜索」都要这一套。 */
  .edit-btn.primary,
  .mini-action.primary {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
  }
  .edit-btn.primary:hover,
  .mini-action.primary:hover { background: #2861d7; color: #fff; }
  /* 分享申请里的「拒绝」（2026-10-10）：色值直接复用「关分享」那颗
     `.admin-refresh.ws-revoke`（5050-5051 的 #fdf3f2 / #f0c8c4 / --c-color），
     但**尺寸留在 .mini-action 这一档** —— 它和同一行的「通过」「删除」并排，
     不能借用 .admin-refresh 的 32.5 档（那会把这行撑高、和「删除」错位）。 */
  .mini-action.ws-reject {
    background: #fdf3f2; border-color: #f0c8c4; color: var(--c-color);
  }
  .mini-action.ws-reject:hover { background: #fbe9e7; border-color: var(--c-color); color: var(--c-color); }
  /* 「回到目录」加粗一档：全仓的按钮字重默认是 400（全局 reset），而标题行里
     其它"有分量的字"（选中的 tab、收起调用区、返回搜索）都是 600 —— 用户要求对齐。 */
  .edit-btn.primary { font-weight: 600; }

  /* 卡片底部动作条：左下角「编辑该题 / 关联改编」，右下角「调用该题」。
     .edit-btn 本身带 margin-left:auto（那是给标题行用的），这里在动作条里清掉，
     让两个按钮真正靠左成组、另一个靠右；字重统一 600（与全仓"加粗"约定一致）。 */
  .card-actions {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    /* 把这一行做成"卡片底部的独立操作区"：上面一条分隔线（复用卡片描边用的
       --border，不新造颜色），线与题面留 10px、与按钮留 8px。
       纯 CSS 边框，不加任何新元素 —— 不会挡住整卡点击/多选/拖拽。 */
    margin-top: 10px;
    padding-top: 8px;
    border-top: 1px solid var(--border);
  }
  .card-actions-left, .card-actions-right { display: flex; align-items: center; gap: 6px; }
  /* 提示就贴在「调用该题」左侧：绝对定位（不占位、不推挤按钮）+ pointer-events:none
     （绝不拦截按钮点击），3 秒后自动消失。 */
  .card-actions-right { position: relative; }
  .call-notice {
    position: absolute;
    right: calc(100% + 8px);
    top: 50%;
    transform: translateY(-50%);
    white-space: nowrap;
    padding: 1px 8px;
    border: 1px solid var(--c-color);
    border-radius: 4px;
    background: #fff;
    color: var(--c-color);
    font-size: 11.5px;
    line-height: 1.7;
    pointer-events: none;
  }
  .card-actions .edit-btn { margin-left: 0; font-weight: 600; }
  .card-actions .edit-btn:disabled {
    cursor: default;
    opacity: 0.5;
    background: #f7f8fa;
  }
  /* scrollbar-gutter: stable —— 永远给纵向滚动条**留出那条槽**，不管它此刻在不在。
     实测（1600×1400，内容从短变长）：不留槽时 .edit-form 客户区 670 → 655（-15px），
     里面每个子元素跟着缩：.code-edit 644 → 629、.form-row 646 → 631 —— 整块重排，
     用户看到的就是"突然晃一下"。留槽之后两种状态下宽度都不变，滚动条在自己槽里出现/消失。
     （macOS 默认是覆盖式滚动条，本来就不占宽；这条只在"始终显示滚动条"的系统设置下起作用。） */
  .edit-form { flex: 1; overflow: auto; padding: 12px; scrollbar-gutter: stable; }
  /* 表单行内的轻量动作按钮：修改页面的「‹ 返回浏览」、题干行的「格式转换」共用一套 */
  .mini-action {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex: none;
    padding: 2px 8px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #fff;
    color: var(--accent);
    font-size: 12px;
    cursor: pointer;
  }
  .mini-action:hover { background: var(--accent-soft); }
  /* 「退出双视图」（用户 2026-10-01）：**只在双视图里出现**（v-if，不占位），蓝底白字。
     它同时带 .mini-action（形状 / 内边距 / 字号跟旁边那排按钮一致），颜色由这一条盖掉 ——
     所以这一块**必须**排在 .mini-action 之后，否则同权重下会被它改回白底蓝字。 */
  .mini-action.dual-exit {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
    font-weight: 600;
  }
  .mini-action.dual-exit:hover { background: #2560d4; border-color: #2560d4; color: #fff; }
  /* 右栏收起了、但里面还有没保存的改动：给展开入口加一圈警示色。
     收起来之后底栏那条「• 未保存」看不见了，这个提示负责不让改动"静默地"待在里面。 */
  .mini-action.has-unsaved {
    color: var(--b-color);
    border-color: var(--b-color);
  }
  /* 题干行的「格式转换」：一键原地转四个框，比同排的轻量按钮醒目一档（只加粗，不动配色） */
  .format-action { font-weight: 600; }
  /* 「格式转换」的结果：贴在按钮右边的一行小字，3 秒后消失。
     不占整行、不撑大底下那块（原来是 .form-notice 一个整行绿框）。 */
  .format-tip {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 12px;
    color: var(--a-color);
  }
  .format-tip.error { color: var(--c-color); }
  /* 「粘贴即转换」开关：贴在标签行末尾。做成常驻的小勾选而不是一次性提示条，
     因为它是「不再自动转换」之后唯一能改回来的地方。 */
  .paste-toggle {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex: none;
    margin-left: auto;
    color: var(--muted);
    font-size: 11.5px;
    cursor: pointer;
    user-select: none;
  }
  .paste-toggle input { margin: 0; cursor: pointer; }
  /* 中栏右端的「调用区」开关：同样"比同排的轻量按钮醒目一档"。
     取 600 而不是 700 —— 全仓的加粗都收敛在 600（.format-action、.back-to-list、
     标签/标题等 15 处），700 没有先例，会在这排工具条里跳出来。
     只加字重，配色/内边距/圆角/hover 一律沿用 .mini-action。 */
  .panel-toggle { font-weight: 600; }
  /* 顶栏「找题」模块（前置窗口）：整块靠右、紧挨搜索框左侧（margin-left:auto 把留白放在它左边）。
     flex: none —— 顶栏变窄时让 hint 先去换行/收缩，别把标签组挤扁。
     W5b：只有账号级能力 can_activate_app 的账号（LSM_EDU / lsm）才渲染这一块。 */
  .app-switch { display: inline-flex; align-items: center; gap: 6px; flex: none; margin-left: auto; }
  /* 顶栏提示（前置窗口切换失败之类）：**只占一格、不挤别的元素**。
     这类文案可能很长（比如"辅助功能权限 + 一个很深的路径"），直接摊开会把
     顶栏右侧的按钮全挤走 —— 所以限宽 + 省略号，完整内容走悬停 title。 */
  .app-error,
  .app-switch .hint {
    color: #b42318;
    font-size: 12px;
    max-width: 260px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .app-switch .hint { color: var(--muted); }
  /* 顶栏那个次要入口按钮（原来叫「最近操作」，阶段 4 搬去后台管理区之后只剩
     「审核队列」在用；类名保持不动，改名要动的地方比收益多）。
     刻意用中性描边（不是主色），它是"处理待办"用的次要入口，
     别和主色按钮抢注意力。
     align-self 抬高一点：顶栏是 baseline 对齐，按钮的文字基线会和 hint 齐，
     但按钮还有上下内边距，视觉上会显得偏下，往回收 2px。 */
  .journal-open {
    flex: none;
    align-self: center;
    position: relative;
    top: -1px;
    padding: 2px 9px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #fff;
    color: #4a5464;
    font-family: inherit;
    font-size: 12px;
    /* 顶栏上这个入口跟「全库」那排一样加粗（用户要求：字体加粗） */
    font-weight: 600;
    cursor: pointer;
  }
  .journal-open:hover { background: var(--accent-soft); color: var(--accent); }
  /* **有待审**时换成醒目的琥珀实心底 + 「题目待审」+ 更粗 —— 一眼看出"有活等着"；
     没有待审就退回素净的白底「审核队列」。琥珀是项目里"要注意但不是故障"那支颜色，
     不会跟别的按钮打架。 */
  .journal-open.pending {
    border-color: #b26a00;
    background: #b26a00;
    color: #fff;
    font-weight: 700;
  }
  .journal-open.pending:hover { background: #96601a; border-color: #96601a; color: #fff; }
  /* 大佬那个「我的待审」：有**没看过**的审核结果时（通过 / 被拒）也用琥珀实心 ——
     跟「题目待审」同一套"有活等着"的观感；看过就退回素净的白底。 */
  .mypending-open.feedback {
    border-color: #b26a00;
    background: #b26a00;
    color: #fff;
    font-weight: 700;
  }
  .mypending-open.feedback:hover { background: #96601a; border-color: #96601a; color: #fff; }
  /* 「前置窗口」是这里唯一会触发动作的按钮：实心主色底 + 白字，明显重过左侧标签。
     标签选中态是"白底蓝字"，这里是"蓝底白字"，一个轻一个重，不会打架（.tabs 是全局共用样式，未改动）。 */
  .app-run {
    display: inline-flex;
    align-items: center;
    flex: none;
    padding: 3px 12px;
    border: 1px solid var(--accent);
    border-radius: 6px;
    background: var(--accent);
    color: #fff;
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
  }
  .app-run:hover:not(:disabled) { background: #2459c8; border-color: #2459c8; }
  .app-run:active:not(:disabled) { background: #1e4bab; border-color: #1e4bab; }
  .app-run:disabled { background: #a9bdea; border-color: #a9bdea; cursor: default; }
  /* tab 行左侧的回退按钮：与标题共用同一个上下文位置，不继承行内按钮的外边距。
     字重比 .mini-action 默认（400）加粗一档；‹ 是拉丁标点，单独抬起来才看着居中。 */
  .panel-head .head-left > .back-to-list {
    flex: none;
    margin: 0;
    white-space: nowrap;
  }
  .back-to-list { font-weight: 600; }
  /* 「‹ 返回浏览 / 返回搜索」= 蓝底白字（用户 2026-09-28 要求，与「保存」同款主色底）。
     ⚠️ 2026-10-03：真题卷集那颗「‹ 返回分类」和「搜索」也**共用这一条**（.ds-btn-blue），
     全仓"蓝底白字"只有这一处色值定义。 */
  /* ⚠️ 选择器写得"重"一点（.ds-btn.ds-btn-blue）：`.ds-btn:hover` 是后面才定义的、
     优先级同为 0,2,0，不加重就会被它盖成"浅蓝底白字"。 */
  .back-to-list.primary, .ds-btn.ds-btn-blue, .mini-action.ds-btn-blue {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
  }
  .back-to-list.primary:hover, .ds-btn.ds-btn-blue:hover, .mini-action.ds-btn-blue:hover {
    background: #2861d7; border-color: #2861d7; color: #fff; }
  /* 待审区「已读」= **绿底白字**（与「完整」胶囊 `.ds-chip.ds-chip-btn.complete`、
     顶栏「今日新增」同一支 `--a-color`；不用新颜色）。写法与上面那颗蓝的同一套：
     选择器带 `.ds-btn` 前缀，压过后面的 `.ds-btn:hover`（否则 hover 会被刷成浅蓝底）。 */
  .ds-btn.ds-btn-green { background: var(--a-color); border-color: var(--a-color); color: #fff; }
  .ds-btn.ds-btn-green:hover { background: #166b2e; border-color: #166b2e; color: #fff; }
  .back-arrow { display: inline-block; position: relative; top: -1px; }
  /* 内容区的引导/占位：批量录入的「开发中」，以及「单题录入」未选知识点时的引导 */
  .pane-hint {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: auto;
  }
  /* 批量录入（Step A）：上方粘贴区 + 切分，下方左侧题目列表 / 右侧只读原文。
     切分结果只在前端内存里，不写题库、不接后端（提交入库是 Step C）。 */
  .batch-pane {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
    /* 上下都留 10px：底部留白与「排版预览」正文区的 10px 一致，
       展开态切分按钮下方不再贴住面板底边 */
    padding: 10px 12px;
    overflow: auto;
  }
  /* 粘贴区的标题行：与「排版预览」的标题栏同款（36px 高、浅灰底、加粗小标题），
     负外边距是为了吃掉 .batch-pane 的内边距，让这条横幅像预览栏一样顶到两边。 */
  .batch-head {
    flex: none;
    display: flex;
    align-items: center;
    gap: 6px;
    height: 36px;
    margin: -10px -12px 0;
    padding: 0 12px;
    background: #fbfcfd;
    font-size: 12.5px;
    font-weight: 600;
  }
  /* 折叠态：标题行与下方内容区之间加一条分隔线（与「排版预览」区那条 border-top 同款），
     展开态不需要 —— 下面紧挨着的就是文本框本身 */
  .batch-head.collapsed { border-bottom: 1px solid var(--border); }
  /* 标题行永远单行：标签不缩不折，提示文字自己吃掉剩余宽度（窄了省略号），按钮留在最右 */
  .batch-label { flex: none; white-space: nowrap; font-size: 12.5px; color: var(--muted); }
  .batch-tip {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 12px;
    color: var(--muted);
    font-weight: 400;
  }
  /* 折叠按钮：数值与「排版预览」的 .preview-head button.ghost 一致（那里一行未动）。
     改这里的时候记得两边一起看。 */
  .batch-fold {
    margin-left: auto;
    flex: none;
    padding: 2px 8px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #fff;
    color: var(--muted);
    cursor: pointer;
    font-size: 12px;
    font-weight: 400;
  }
  .batch-fold:hover { background: var(--accent-soft); }
  /* 灰质不可点（初次切分前：「收起/展开」还没有可折叠的下半区，见模板注释）。
     这里必须显式写 —— 全站没有通用的 `button:disabled` 规则，不写的话灰按钮
     看着跟能点的一模一样，用户会一直点。同时压掉上面那条 hover 高亮。 */
  .batch-fold:disabled { opacity: .45; cursor: default; }
  .batch-fold:disabled:hover {
    background: #fff;
    color: var(--muted);
    border-color: var(--border-strong);
  }
  /* 粘贴区改成代码框（2026-10-17）：宿主**直接复用 `.code-edit`** —— 边框 / 圆角 /
     聚焦描边 / `.CodeMirror` 的字体字号行高 / 内边距 / 整套语法配色规则全部共用
     （见上面 `.code-edit` 那几段），所以这里只覆盖"粘贴区特有的三条"。
     ⚠️ 选择器写成 `.code-edit.batch-paste-host`（0-2-0）压过 `.code-edit`（0-1-0），
     与两条规则在文件里的先后无关。 */
  .code-edit.batch-paste-host {
    /* flex-basis 用 auto 而不是 0：手动拖拽 resize 写的是内联 height，auto 才认它 */
    flex: 1 1 auto;
    width: 100%;
    min-height: 280px;
    /* 还没切分时**不许拖**（2026-10-14 用户要求）：这时粘贴区就是整块，拖着改高度
       没有任何东西要跟它让位；切出题目之后（.can-resize）才开放右下角手柄。 */
    resize: none;
  }
  /* 占位提示（替代 textarea 的 placeholder）：位置对齐第一行文字
     （`.code-edit .CodeMirror-lines` 的 6px 8px 内边距 + 1px 边框）。 */
  .batch-paste-hint {
    position: absolute;
    left: 9px;
    top: 7px;
    z-index: 1;
    color: #b7c0cc;
    pointer-events: none;
    /* 宿主为了"手柄落到左下角"是 RTL（见 .code-edit），提示自己改回 LTR，
       免得这段中文的排布/对齐跟着方向走 */
    direction: ltr;
    font-family: var(--code-font);
    /* 占位是辅助文字，不跟代码正文的 16px 走：14px 比正文小一档，
       行高仍用代码行高，保证它和第一行文字垂直对齐。 */
    font-size: 14px;
    line-height: var(--code-line);
  }
  /* 用户拖过一次右下角之后：把高度**钉住**，不再参与 flex 的伸缩。
     为什么必须钉（2026-10-14 实测的"拖拽不跟手"）：原生 resize 只是把高度写进
     内联 style，而这块文本域是 `flex: 1 1 auto` 的 flex 子项 —— 拖完 flex 会再
     分配一次：grow 把它撑回原样、shrink 又按比例削掉它、把空间让给下面的
     .batch-body。实测：第一次拖 200px → 高度纹丝不动（Δh=0）；切分后再拖 200px →
     高度反而矮了 87px（Δh=−87）。`flex: 0 0 auto` = 不伸长也不压缩，basis 仍是
     auto（认内联 height）→ 指针走多少、高度就变多少。
     没拖过时保持 `1 1 auto`，默认那份"两半平分"的观感一个字不变。 */
  .code-edit.batch-paste-host.pinned { flex: 0 0 auto; }
  /* 切出题目之后才允许拖高：下面出现半屏列表，两者要争高度，这时拖拽才有意义。 */
  .code-edit.batch-paste-host.can-resize { resize: vertical; }
  .batch-bar { display: flex; align-items: center; gap: 10px; }
  /* 被拦下的限时浮层（「已入库的题不能改知识点」）——单题录入 / 批量录入共用一套外观：
     **绝对定位**（不占文档流、不推挤任何一行）＋ pointer-events:none（绝不挡点击）。
     两个页面的锚点各一条，见下面那两行。 */
  .kp-blocked {
    position: absolute;
    z-index: 6;
    pointer-events: none;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    padding: 2px 8px;
    border: 1px solid #f2c2bf;
    border-radius: 6px;
    background: #fdf3f2;
    color: var(--c-color);
    font-size: 12px;
    box-shadow: 0 2px 8px rgba(15, 23, 42, 0.10);
  }
  /* 批量录入：**浮在操作行上方**，不盖「切分」按钮 / 题号勾选框。 */
  .batch-bar { position: relative; }
  .batch-bar > .kp-blocked {
    right: 0;
    top: auto;
    bottom: 100%;
    transform: none;
    margin-bottom: 6px;
  }
  /* 单题录入：钉在操作条**上方**的右下角 —— 贴着表单底部边缘，不盖按钮、不占高度。 */
  .form-actions { position: relative; }
  .form-actions > .kp-blocked { right: 12px; bottom: 100%; margin-bottom: 6px; }
  /* 折叠后的摘要头：整行可点（光标 + hover 反馈），右侧「展开」按钮复用 .mini-action */
  .batch-tip.warn { color: var(--b-color); }
  /* 文本改过还没重新切分 → 「切分」按钮描边加亮，提示要点它 */
  .batch-bar button.primary.attention {
    box-shadow: 0 0 0 2px #f6d9a8;
    border-color: var(--b-color);
  }
  /* 知识点提示的分段配色：改前灰字删除线 / 箭头灰 / 改后主色加粗 */
  .kp-before { color: var(--muted); text-decoration: line-through; }
  .kp-arrow { color: var(--muted); margin: 0 2px; }
  .kp-after { color: var(--accent); font-weight: 600; }
  /* 当前题还没选知识点：整行红字（与列表槽位的「缺知识点」同一个红） */
  .kp-notice.empty { color: var(--c-color); }
  /* Step C：逐题提交。主按钮紧跟在题头那串灰字右边（原来它靠 margin-left:auto 顶在最右，
     现在最右那个位置让给「调用该题」），与 .batch-fold 同尺寸但实心（它是这一屏的主操作） */
  .batch-submit {
    margin-left: 0;
    flex: none;
    padding: 3px 12px;
    border: 1px solid var(--accent);
    border-radius: 6px;
    background: var(--accent);
    color: #fff;
    font: inherit;
    font-size: 12px;
    cursor: pointer;
  }
  .batch-submit:hover:not(:disabled) { background: #2459c8; border-color: #2459c8; }
  .batch-submit:disabled { opacity: 0.55; cursor: default; }
  /* 入库按钮自己换文案（入库该题 → 已入库 → 覆盖保存）时，盒子宽度不跟着变，
     位置才能真正钉死（它右边就是「调用该题」，左边缘也不动）。 */
  .batch-submit { min-width: 74px; text-align: center; }
  /* 三态：待处理（灰）/ 已入库（绿 + 列表行变灰）/ 出错（红） */
  .batch-state.saved { color: var(--a-color); }
  .batch-state.saving { color: var(--accent); }
  .batch-state.failed { color: var(--c-color); }
  .batch-list li.saved .batch-preview { opacity: 0.55; }
  .batch-format-note.ok { color: var(--a-color); }
  .batch-format-note.warn { color: var(--b-color); }
  /* 预录 ID（灰，提交前）/ 预录与实际不一致（黄） */
  .batch-format-note.planned { color: var(--muted); }
  .batch-format-note.mismatch { color: var(--b-color); }
  .batch-status.ok { color: var(--a-color); }
  .batch-bar button.primary {
    background: var(--accent);
    border: 1px solid var(--accent);
    color: #fff;
    font: inherit;
    padding: 5px 14px;
    border-radius: 6px;
    cursor: pointer;
    flex: 0 0 auto;
    white-space: nowrap;
  }
  .batch-bar button.primary:hover { background: #2459c8; border-color: #2459c8; }
  .batch-bar button.primary:disabled,
  .batch-bar button.primary:disabled:hover {
    opacity: 0.55;
    cursor: not-allowed;
  }
  /* 切分行最右侧的「题号自动填写」；margin-left:auto 把它推到行尾 */
  .batch-number-auto {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex: 0 0 auto;
    white-space: nowrap;
    font-size: 12px;
    color: var(--muted);
    cursor: pointer;
    user-select: none;
  }
  .batch-number-auto input { margin: 0; accent-color: var(--accent); }
  .batch-status { font-size: 12px; color: var(--muted); }
  .batch-status.error { color: var(--c-color); }
  /* 下半区（列表 + 当前题）与文本框都是 1 1 auto：展开态两半平分剩余高度，
     文本框靠 min-height 保证不会太小；折叠时文本框不占位，空间全归下半区。 */
  /* 地板：文本框 280px、下半区 160px（外加标题行 36px 与两条 8px 间距）。
     1280 视口下中栏高 571px，两者共存不溢出；空间富余时两个 flex-grow 平分，
     折叠态下文本框不占位，整块高度全归下半区。 */
  /* overflow:hidden 只是一道保险：列表和表单各自内部都有滚动条，
     不让它们把 .batch-pane 顶出滚动条（窄屏改成上下堆叠时尤其明显）。 */
  .batch-body { display: flex; gap: 10px; flex: 1 1 auto; min-height: 160px; overflow: hidden; }
  .batch-list {
    flex: 0 0 190px;
    margin: 0;
    padding: 0;
    list-style: none;
    overflow: auto;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: #fbfcfd;
  }
  .batch-list li {
    display: block;
    padding: 6px 8px;
    border-bottom: 1px solid var(--border);
    font-size: 12px;
    cursor: pointer;
  }
  .batch-item-main { display: flex; align-items: baseline; gap: 6px; }
  .batch-list li:last-child { border-bottom: 0; }
  .batch-list li:hover { background: var(--accent-soft); }
  .batch-list li.active { background: var(--accent-soft); box-shadow: inset 2px 0 0 var(--accent); }
  .batch-no {
    flex: none;
    min-width: 18px;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
    text-align: right;
  }
  .batch-preview {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .batch-state { flex: none; color: var(--muted); }
  /* 知识点槽位（列表项里的一行 / 字段行里的只读块）：未设时红字 */
  .batch-kp {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--muted);
  }
  .batch-list .batch-kp { margin-top: 2px; padding-left: 24px; font-size: 11.5px; }
  .batch-kp.empty { color: var(--c-color); }
  /* 字段行里的知识点：与输入框同高同边框，但不可编辑 */
  .form-row .batch-kp {
    width: 100%;
    /* 盒模型（高度 / 内边距 / 边框）交给下面「四个框同规格」那条统一，这里只管
       只读框自己的事：底色浅灰表示不可编辑；行高 = 内容高（29 - 2 边框）让单行文字
       正好垂直居中，同时保住 text-overflow: ellipsis —— flex 容器上省略号不生效，
       框被按钮挤窄时会被外侧硬切。 */
    line-height: 27px;
    background: #f7f8fa;
  }
  .mini-action:disabled { opacity: 0.55; cursor: default; }
  .mini-action:disabled:hover { background: #fff; }
  .batch-detail { flex: 1; min-width: 0; display: flex; flex-direction: column; }
  .batch-detail-head {
    flex: none;
    display: flex;
    align-items: center;
    gap: 10px;
    /* 与「排版预览」的标题行同款：浅灰底 + 一条下边框，把题头和下面的表单分开。
       左右内边距**跟着下面 .batch-form 走**（左 0、右 2px = 它给滚动条留的那 2px）：
       这样「入库该题」的左边框与「调用该题」的右边框，正好压在下方各输入框的左右边缘上。 */
    padding: 6px 2px 6px 0;
    margin-bottom: 8px;
    background: #fbfcfd;
    border-bottom: 1px solid var(--border);
    font-size: 12px;
    color: var(--muted);
  }
  /* 逐题表单：字段区自己滚，题目列表和题头不跟着滚 */
  /* 同 .edit-form：给滚动条留固定槽位，内容变长时不再挤压水平空间（批量页的录入表单） */
  .batch-form { flex: 1; min-height: 0; overflow: auto; padding-right: 2px;
    scrollbar-gutter: stable; }
  /* 四个短字段一行（来源 / 题号 / 难度 / 知识点，知识点那列里还并排着「全部应用」）；
     中栏窄了就折成两行（见下面的容器查询）。
     每列都写下限（minmax 的第一个参数），因为"来源"这一列有硬指标：必须能完整显示
     "2025年全国高中数学联A卷一试"。实测该串 180.6px（输入框实际字号 13px），加上左右
     内边距 16px 与边框 2px，输入框的 clientWidth 至少要 ~197px —— 这里给 216px 留余量。
     题号（一两个字符）与难度（"A 基础"）压到各自内容的最小值，省下来的余量基本都给了
     知识点（要放全路径，还要并排一个「全部应用」）。fr 只决定多余空间怎么分，不破下限。 */
  .batch-fields {
    display: grid;
    grid-template-columns:
      minmax(216px, 0.6fr) minmax(58px, 0.5fr) minmax(82px, 0.9fr) minmax(166px, 5fr);
    gap: 0 8px;
  }
  /* 折成两行的阈值。四列下限合计 216+58+82+166 = 522px，加 3 个 8px 间隙 = 546px，
     即 .batch-fields 自己至少要 546px 才不会被下限撑出容器；而 .batch-fields 宽 ≈
     容器 − 226（容器 = .mid-main，实测 842→616），所以容器低于 ~772px 就该折行。
     取 780 留余量。 */
  @container (max-width: 780px) {
    .batch-fields { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  }
  /* 批量页的标签行（来源 / 知识点）：中栏变窄时允许整体换行，
     否则标签会被右边的按钮挤成「知识 点（必 填）」这种竖排 */
  .batch-fields .label-row { flex-wrap: wrap; row-gap: 2px; }
  /* 四个字段的 label 统一（字号 / 粗细 / 颜色）。知识点那行的 label 被包在 .label-row 里，
     不套这条会退回浏览器默认（14px、正文色），与旁边三个 12px 灰字不一致。 */
  .batch-fields .label-row > label:first-child { font-size: 12px; color: var(--muted); }
  /* 四个框（来源 / 题号 / 难度 / 知识点）完全同规格：UA 给 input 和 select 的默认高度差
     1.5px、默认内边距也不同（5px vs 0），不钉死就会高低不齐、文字基线错位。这里统一
     高度 / 内边距 / 边框 / 盒模型 / 字号。多带一层 .form-row 是为了压过上面的
     .form-row input[type="text"] / select（同优先级时后者在后面，靠特异性取胜更稳）。
     复选框在 .label-row 里，用属性选择器避开。只作用于批量页字段区。 */
  .batch-fields .form-row input[type="text"],
  .batch-fields .form-row select,
  .batch-fields .form-row .batch-kp {
    height: 29px;
    padding: 0 8px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    box-sizing: border-box;
    font-size: 13px;
  }
  /* 知识点框 + 「全部应用」按钮并排一行；按钮高度钉到和框一致（.mini-action 默认 22.5px） */
  .batch-fields .kp-line { display: flex; align-items: center; gap: 8px; }
  .batch-fields .kp-line .batch-kp { flex: 1 1 auto; width: auto; min-width: 0; }
  .batch-fields .kp-line button { flex: none; height: 29px; }
  .batch-format-note { font-size: 12px; color: var(--muted); }
  .batch-format-note.error { color: var(--c-color); }
  /* 中栏窄到 640px 以下时左右并排会挤成两条细缝：改成上下堆叠，列表给固定高度 */
  @container (max-width: 639px) {
    .batch-body { flex-direction: column; }
    .batch-list { flex: none; max-height: 180px; }
  }
  .form-row { margin-bottom: 10px; }
  .form-row > label {
    display: block;
    font-size: 12px;
    color: var(--muted);
    margin-bottom: 3px;
  }
  /* 来源字段、题干字段的标签挪进了 .label-row，基础样式得跟着覆盖进去，
     否则会掉回默认字号与颜色（:first-child 只命中真标签，不碰旁边的按钮） */
  .form-row.field-source .label-row > label:first-child,
  .form-row.field-content .label-row > label:first-child,
  .form-row.field-cmdhead .label-row > label:first-child,
  .form-row.field-id .label-row > label:first-child {
    display: block;
    font-size: 12px;
    color: var(--muted);
  }
  .form-row input[type="text"],
  .form-row select {
    width: 100%;
    padding: 5px 8px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    font-size: 13px;
    outline: none;
  }
  .form-row textarea {
    width: 100%;
    min-height: 96px;
    padding: 6px 8px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 12.5px;
    line-height: 1.6;
    resize: vertical;
    outline: none;
  }
  .form-row input:focus, .form-row select:focus, .form-row textarea:focus { border-color: var(--accent); }

  /* 可编辑的 LaTeX 编辑器：内核是 CodeMirror 5（stex 语法高亮）。
     外层 .code-edit 负责边框、圆角、高度与拖拽；里面 .CodeMirror 铺满它。
     高度由 JS 按内容算（见 autoGrowEditors），所以这里只给地板与字体。 */
  .code-edit {
    position: relative;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    overflow: hidden;
    background: #fff;
    min-height: 96px;
    resize: vertical;      /* 用户仍可拖高（拖过之后锁定，见 lockedEditors） */
    /* ⚠️ 拖拽手柄要落在**左下角**（用户 2026-10-17："顺便把其它框的拖手也放左下角"）。
       原生 resizer 在 LTR 下固定画在右下角 —— 那儿正好是 CodeMirror 纵向滚动条的落脚处：
       粘贴区一长就"手柄和滚动条重合、一拖既改高度又滚"（用户报的 bug）；
       而在右边留一条 18px 空白来避让又"太丑了"（也是用户的原话）。
       给宿主设 RTL、里面的编辑器立刻改回 LTR：手柄跑到左下角，
       正文方向 / 光标 / 滚动条位置 / 补全弹窗全不受影响
       （两引擎实测：拖左下角 50px → 高度 +50px，滚动位置一动不动，`.CodeMirror` 仍是 ltr）。 */
    direction: rtl;
  }
  /* 宿主的 RTL 只为"把手柄挪到左下角"这一件事，内容立刻改回从左到右 */
  .code-edit > .CodeMirror { direction: ltr; }
  .code-edit:focus-within { border-color: var(--accent); }
  /* TikZ 与答案两个编辑器默认更高：96 × 1.8 ≈ 173px（约 8 行，题干保持 4 行）。
     用专属类而不是 :nth-of-type——TikZ 那一行是 v-if，无图题不渲染，
     按位置选会错位到答案框上。 */
  .code-edit.code-edit-tall { min-height: 173px; }
  .code-edit .CodeMirror {
    height: 100%;
    /* 与源码页同一套 --code-* 变量（见 :root）。2026-10-15 由 12.5px / 1.6
       改成 TeXstudio 观感：Consolas 16px / 行高固定 18.5px。
       这 6 个框（单题录入 3 + 批量录入 3）共用这一条，改一处两边一起变。 */
    font-family: var(--code-font);
    font-size: var(--code-size);
    line-height: var(--code-line);
    background: transparent;
    color: var(--text);
  }
  /* 内边距与原来 textarea 的 6px 8px 对齐，编辑器内不会有额外的横向内边距 */
  .code-edit .CodeMirror-lines { padding: 6px 8px; }
  .code-edit .CodeMirror pre.CodeMirror-line { padding: 0; }
  /* 语法配色：**两个代码编辑器共用一套** —— 题目内容/TikZ/答案（.code-edit）与源码页
     （.handout-editor-host）各写一份选择器。源码页原先没吃站内配色，走的是 CodeMirror
     自带的调色板（关键字紫 #708 / 原子蓝 #219 / 注释棕 #a50），2026-10-15 一并统一。
     色号照 TeXstudio 4.4.1 的 [formats]（见 :root 里的 --code-fg-*）。
     不加粗、不斜体：片段里斜体也会让行宽抖动。
     ⚠️ 下面三档**对不上** TeXstudio —— 原因是 CodeMirror 的 stex 分词粒度，整档只能一个色，
        要分档得改 mode 或加 overlay（JS），本轮不碰：
          · `\begin`/`\end`（TeXstudio 是 #0095ff 亮蓝）、数学命令 `\triangle`（#808000 橄榄）、
            `\section`（黑）——在 CM 里**全都和普通命令同属 `cm-tag`** → 只好一起用 #800000；
          · 环境名（#000080）与命令参数里的数字（如 `{13}` 的 13，TeXstudio 是黑）
            **同属 `cm-atom`** → 只好一起用 #000080；
          · 数学运算符 `+ - =`、转义 `\\` `\,`、对齐符 `&` 在 CM 里**无类**（跟随正文色），
            TeXstudio 分别有 #008000 / #ff0088 / #0055ff —— 染不了。 */
  .code-edit .cm-tag,         .handout-editor-host .cm-tag         { color: var(--code-fg-command); }
  /* atom 里混着两档：环境名（environment，藏青）与参数里的数字（TeXstudio 不染色 → 正文色）。
     stex-tiku 那层把环境名单独打成 cm-tex-env，剩下的 atom 按正文色渲染。 */
  .code-edit .cm-atom,        .handout-editor-host .cm-atom        { color: var(--code-fg-arg); }
  .code-edit .cm-variable-2,  .handout-editor-host .cm-variable-2,
  .code-edit .cm-number,      .handout-editor-host .cm-number      { color: var(--code-fg-math); }
  .code-edit .cm-keyword,     .handout-editor-host .cm-keyword     { color: var(--code-fg-math-delim); }
  .code-edit .cm-comment,     .handout-editor-host .cm-comment     { color: var(--code-fg-comment); }
  .code-edit .cm-bracket,     .handout-editor-host .cm-bracket,
  .code-edit .cm-punctuation, .handout-editor-host .cm-punctuation { color: var(--code-fg-bracket); }
  /* 下面这几档 TeXstudio 的 LaTeX 配色里没有对应项，沿用站内原来的色（两个编辑器一起）*/
  .code-edit .cm-string,      .handout-editor-host .cm-string,
  .code-edit .cm-string-2,    .handout-editor-host .cm-string-2    { color: #0b7285; }
  .code-edit .cm-def,         .handout-editor-host .cm-def         { color: #1a7f37; }
  .code-edit .cm-link,        .handout-editor-host .cm-link,
  .code-edit .cm-url,         .handout-editor-host .cm-url         { color: #6f42c1; }
  .code-edit .cm-error,       .handout-editor-host .cm-error       { color: #b3261e; }
  /* 由 stex-tiku 那层 mode 单独打类的档（基底分不开的）：
     · cm-tex-extra = extra-keyword：`\begin`/`\end` → TeXstudio 是亮蓝 + 加粗 */
  .code-edit .cm-tex-extra,   .handout-editor-host .cm-tex-extra   {
    color: var(--code-fg-extracmd); font-weight: 700; }
  /* cm-tex-env = environment：`\begin{myenumerate}` 里的环境名（atom 里单独摘出来的那一档）*/
  .code-edit .cm-tex-env,     .handout-editor-host .cm-tex-env     {
    color: var(--code-fg-env); font-weight: 700; }
  /* cm-tex-envbrace = 环境名外面那对 `{` `}`：跟 `\begin` 一样亮蓝（不加粗 —— TeXstudio 里那对括号是细的）*/
  .code-edit .cm-tex-envbrace, .handout-editor-host .cm-tex-envbrace {
    color: var(--code-fg-extracmd); font-weight: 700; }   /* 用户 2026-10-16：这对括号也加粗 */
  /* cm-tex-mathop = 数学模式里的运算符/标点：跟数学内容同色（#008000）*/
  .code-edit .cm-tex-mathop,  .handout-editor-host .cm-tex-mathop  {
    color: var(--code-fg-math); }
  /* cm-tex-mathtext = math-text：`\text{…}` 盒子里的内容 / 数学里的中文 → 绿（实测，见 :root）。
     ⚠️ 这条必须排在 .cm-error 后面：基底把数学里的中文标成 cm-error，同权重下靠顺序盖回来。 */
  .code-edit .cm-tex-mathtext, .handout-editor-host .cm-tex-mathtext {
    color: var(--code-fg-mathtext); }
  /* TikZ 两档（进 tikzpicture 环境才生效）：命令名橙、参数棕 —— 实测自用户真机截图 */
  .code-edit .cm-tex-tikzcmd, .handout-editor-host .cm-tex-tikzcmd {
    color: var(--code-fg-tikzcmd); }
  .code-edit .cm-tex-tikzarg, .handout-editor-host .cm-tex-tikzarg {
    color: var(--code-fg-tikzarg); }
  /* cm-tex-mathcmd = math-keyword：数学模式内的命令（`\triangle`/`\frac`…）→ 橄榄色，不加粗 */
  /* cm-tex-pkg = 包名/文档类名（`\usepackage{mybook}` 的 mybook）：实测绿（同数学内容色）*/
  .code-edit .cm-tex-pkg,     .handout-editor-host .cm-tex-pkg     {
    color: var(--code-fg-math); }
  /* cm-tex-structtitle = 章节标题（`\chapter{…}` 里的字）：黑 + 加粗（真粗体面，字宽不变）*/
  .code-edit .cm-tex-structtitle, .handout-editor-host .cm-tex-structtitle {
    color: var(--code-fg-structtitle); font-weight: 700; }
  .code-edit .cm-tex-mathcmd, .handout-editor-host .cm-tex-mathcmd {
    color: var(--code-fg-mathcmd); }
  /* 另两档（都由 stex-tiku 打类）：
     · cm-tex-amp    = align-ampersand：`&` → 亮蓝 + 粗
     ⚠️ 章节类命令**不在这一档**：实测它们与 `\begin`/`\end` 同档（真机截图量像素 #0080f0），
        已并入 cm-tex-extra（原来按 ini 的 structure 落成黑，对不上，2026-10-16 改）。
     ⚠️ 加粗档都配了真粗体面（见上面 @font-face 的 700 面），实测加粗前后**行宽差 0.00**。 */
  .code-edit .cm-tex-amp,     .handout-editor-host .cm-tex-amp     {
    color: var(--code-fg-amp); font-weight: 700; }
  /* ⚠️ 两处**故意不做**（都量过了，不是漏）：
     · **环境名改成加粗了**（2026-10-16 用户拍板）：ini 那份写的是 `environmentold=false`，
       但用户在同屏截图里看到它是粗的 —— 以**看到的**为准（ini 那份可能不是完整运行时 scheme）。
       加粗走真粗体面，字宽实测不变。
     · **环境名外面那对 `{` `}` 现在是亮蓝**（用户 2026-10-16 要求），单独一档
       cm-tex-envbrace、**加粗**（用户 2026-10-16：这对括号也是粗的）。
     · **`\\` 不单独染色**：ini 里 `escapeseq=#ff0088`（粉），但同屏截图左栏整屏只有 18/24 个
       粉像素 —— 那些 `\\` 实际是按 keyword（暗红）渲染的，所以这边也不新开一档。 */
  /* 补全弹窗：沿用应用配色，不用 CodeMirror 默认的蓝底 */
  /* 括号配对高亮：**黄底 + 加粗** —— 颜色不猜，取自 TeXstudio ini 的
     `data\braceMatch\background=#ffff7f` 与 `data\braceMatch\bold=true`。
     光标左右两侧**同时**亮（见 installBraceMatch）。 */
  /* ⚠️ 黄块要**严丝合缝地占满整行**（用户 2026-10-17："黄块高度占满行的背景高度，
     不要留行背景色的灰边"）：只画字形盒子的话，粗体 Consolas 的内容盒只有 14.5px、
     而且在 18.5px 的行盒里是**从 3px 处**才开始 —— 上下就会各露一条行底色。
     给内联元素加纵向 padding：它会画进背景，但**不参与行盒高度计算**，
     所以行高一个字节不动（实测两引擎都还是 18.5）。
     上垫 3px（补到行顶）+ 下垫 1px = 3+14.5+1 = 18.5 = 行高，误差 0。
     ⚠️ 两条踩过的反例：`display:inline-block` + height 会把行盒顶高 1px（18.5 → 19.5）；
     box-shadow 撑会同时向左右各溢 2px。 */
  .cm-brace-match {
    background: #ffff7f; font-weight: 700;
    padding: 3px 0 1px 0; }
  /* 光标所在行加底色：TeXstudio 的 current 行 —— ini 里 `data\current\background=#eeeeff`。
     ⚠️ 选择器故意包一层 `:where()`（**权重 0**，2026-10-17）：
     「当前行」这层淡紫只是**背景板**，遇见"信号底色"必须让位 —— 它们写在**同一个**
     `.CodeMirror-linebackground` div 上，同权重时后写的赢，而这条在文件里比
     `.sync-flash-line`（Cmd/Option 点击跳转的浅黄高亮）/ `.handout-err-line`（编译错误行）**靠后**，
     结果就是用户报的"跳过去看不见黄色高亮，被选中行的背景挡住"（跳转后光标正好落在那行）。
     `:where()` 把权重压到 0 → 任何一条信号底色都压得住它，而且**与规则先后无关**，
     将来谁把这几条挪个位置也不会再翻车。（`:where()` 要求 Chrome 88+ / Safari 14+，
     比本站已经在用的 `size-adjust`(Chrome 92+ / Safari 17+) 门槛还低。） */
  :where(.cm-active-line) { background: #eeeeff; }
  /* ⚠️ **加粗档的行高陷阱**（2026-10-17 用户报"括号匹配会让页面细微抖动"，实测根因就在这）：
     加粗那一格用的是 Consolas Bold，它的 vertical metrics 比 Consolas 常规**高** ——
     同一行里，常规字的内联盒是 17px、粗体只有 14.5px，但 Chrome/Safari 的行盒不是取
     `line-height`（18.5px）而是会被这个更高的字体盒顶上去：
     实测「光标停在括号旁」前后，那一行的**行盒 18.5 → 19.5px**（Safari 19.83），
     下面所有行跟着上下跳 1px —— 就是用户看到的抖动。
     给这些档 `line-height: 0`：**只**把它们对行盒的贡献抹平（字号 / 字宽 / 字形一个字不动），
     行盒回到 strut 的 18.5px。两引擎实测都是 18.5px，黄底/粗体的观感不变。
     凡是"由 JS 动态挂上/摘下"的加粗档都必须在这里 —— 否则光标一动，行高就在 18.5↔19.5 之间跳。 */
  .cm-brace-match,
  .code-edit .cm-tex-extra, .handout-editor-host .cm-tex-extra,
  .code-edit .cm-tex-env,   .handout-editor-host .cm-tex-env,
  .code-edit .cm-tex-envbrace, .handout-editor-host .cm-tex-envbrace,
  .code-edit .cm-tex-structtitle, .handout-editor-host .cm-tex-structtitle,
  .code-edit .cm-tex-amp,   .handout-editor-host .cm-tex-amp { line-height: 0; }

  .CodeMirror-hints {
    z-index: 60;
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    box-shadow: 0 8px 20px rgba(20, 24, 32, 0.18);
    /* 跟代码框同一套（原来是 12px）：弹窗里的命令名与代码框里的字形/大小一致，
       单行高也写死 → 候选项里有中文（命令说明）时不会一行高一行低。 */
    /* ⚠️ 补全弹窗**不用代码框那款字体**：TeXstudio 的弹窗是 Qt 的界面字体（**比例字体**），
       比正文**小一号**。2026-10-16 量用户截图：`k` 的上升部墨高 20 设备px → em ≈ 13.7 css
       （正文是 Consolas 16px）；同一串里 `i`/`l` 明显窄 → 比例字体（等宽字体里它们一样宽）。
       所以这里换成系统界面字体。
       2026-10-17 再缩一档（用户："正文里的西文缩了 10%，弹窗也要小一些"）：
       **正文的西文是 Consolas 16px × size-adjust 91% = 14.56px 的有效 em**，
       而弹窗用的是系统比例字体（同字号下 x 高/大写高都比 Consolas 大 3~6%，实测：
       `\myakeywords` 在正文里 x 高 7.138px、弹窗 14px 时 7.369px，字号相同也显得更大）。
       按 TeXstudio 那个比例（弹窗 em 13.7 ÷ 正文 16 ≈ 0.855）折算到我们的 14.56px 正文，
       弹窗取 **12.5px**（≈ 14 × 0.9，与用户"缩 10%"的直觉一致）。 */
    font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif;
    font-size: 12.5px;
    /* 行距：实测 15~17.5 css（两次截图 30/35 设备px）。原来 17px（= 字号 14 的 1.214 倍）；
       字号缩到 12.5 后按**同一比例**跟到 15.2px —— 仍落在实测那一段里，
       每行的松紧与 TeXstudio 一致（原来沿用编辑器的 18.5px 行高、再叠 2px 内边距 → 22.5px，偏高近 5px）。 */
    line-height: 15.2px;
    padding: 3px 0;
  }
  .CodeMirror-hint { padding: 0 6px; border-radius: 5px; max-width: 560px; overflow: hidden; text-overflow: ellipsis; }
  li.CodeMirror-hint-active { background: var(--accent); color: #fff; }
  /* snippet 占位符：Tab 跳进去时整体选中，输入即替换 */
  .cm-snippet-field { background: #ffe9b3; border-radius: 2px; }
  /* 题干/解析框粘图识别时的行内占位（零宽 bookmark widget：不占正文，可继续打字） */
  .cm-ocr-placeholder {
    display: inline-block;
    margin: 0 2px;
    padding: 0 8px;
    border-radius: 999px;
    background: #eef3ff;
    color: #2f5bd3;
    font-size: 12px;
    line-height: 18px;
    vertical-align: baseline;
    user-select: none;
    cursor: progress;
  }
  .cm-ocr-placeholder.is-failed {
    background: #fff1f0;
    color: #c0392b;
    cursor: pointer;
  }
  /* 粘图前选中的那段文字：识别完成后会被整段替换（浅底提示"目标在这里"） */
  .cm-ocr-target {
    background: rgba(47, 91, 211, 0.10);
    border-radius: 2px;
  }

  /* 表单里的「命令头预览」：表单实时拼出来的只读结果 */
  .cmd-head {
    padding: 5px 8px;
    border: 1px dashed var(--border-strong);
    border-radius: 6px;
    background: #f7f8fa;
    color: #4a5464;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 12px;
    line-height: 1.5;
    word-break: break-all;
  }
  /* 来源字段的标签挪进了 .label-row（要和「记住来源」同行），高亮规则跟着覆盖进去。
     只取第一个 label，别把旁边那个「记住来源」也染成强调色。 */
  .form-row.changed > label,
  .form-row.changed.field-source .label-row > label:first-child,
  .form-row.changed.field-content .label-row > label:first-child { color: var(--accent); font-weight: 600; }
  /* 操作条是中栏的常驻底栏（在 .edit-form 滚动容器之外），
     所以内边距要自己对上 .edit-form 的 12px */
  .form-actions {
    flex: none;
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 8px 12px 10px;
    border-top: 1px solid var(--border);
    background: var(--panel);
  }
  /* 单题录入（create）专用的底部保存行微调：只挂在这个局部类上，
     不碰通用 .form-actions / .action-row / 通用按钮，也不影响「修改页面」（那边不落这个类）。
     ① 上下留白 = 6.5px，与下方「排版预览行」自身的上下留白（实测 6.5 / 6.5）相等：
        排版预览行 = .preview-head（min-height 36px + padding 4px，内容高 23px），
        靠 align-items:center 居中后实际留白 =(36-8-23)/2 + 4 = 6.5px；
     ② 「调用该题」带 font:inherit，行高被继承成 22.1px（比同行其它按钮高 4px），
        这里把行高归位到 normal，两个按钮 top / height 就一致了。 */
  .form-actions.form-actions-create { padding-top: 6.5px; padding-bottom: 6.5px; }
  .form-actions.form-actions-create .entry-call { line-height: normal; }
  .action-row { display: flex; gap: 8px; align-items: center; }
  /* 保存按钮的外壳：title 挂在这里，disabled 按钮也能弹出提示 */
  .save-hint { display: inline-flex; }
  /* 录入区（单题录入 / 批量录入）的「调用该题」：与浏览页卡片上那个同款观感
     （白底 + 主色描边），字重跟周围的按钮一样用常规粗细（用户要求：这两处不要加粗，
     加粗只管浏览页卡片上那个）。主操作（保存 / 入库该题）仍是实心主色。
     margin-left:auto 把它顶到整行最右。 */
  .entry-call {
    margin-left: auto;
    flex: none;
    padding: 3px 12px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #fff;
    color: var(--accent);
    font: inherit;
    font-size: 12px;
    cursor: pointer;
    white-space: nowrap;
  }
  .entry-call:hover:not(:disabled) { background: var(--accent-soft); }
  .entry-call:disabled { color: #b7bdc7; border-color: var(--border); cursor: not-allowed; }
  .entry-call:disabled:hover { background: #fff; }
  /* 「调用该题」的壳：相对定位，专门给那条就近提示当锚点 */
  .entry-call-wrap {
    position: relative;
    display: inline-flex;
    flex: none;
    margin-left: auto;
  }
  .batch-detail-head .entry-call-wrap { margin-left: 0; }
  /* 就近提示**不占位**：绝对定位贴在按钮左侧 + pointer-events:none
     （与浏览页卡片上那条 .call-notice 同一套做法）——
     它一出现绝不会把「入库该题 / 保存」这些按钮挤走。 */
  .entry-call-notice {
    position: absolute;
    right: calc(100% + 8px);
    top: 50%;
    transform: translateY(-50%);
    font-size: 11.5px;
    color: var(--muted);
    white-space: nowrap;
    pointer-events: none;
  }
  .entry-call-notice.err { color: var(--c-color); }
  .entry-call-notice.ok { color: var(--a-color); }
  /* 批量页那条提示放在"吃剩余宽度"的文字区里：贴到它的右端（= 紧挨着最右边的
     「调用该题」），只把左边的灰字挤成省略号 —— 左右两个按钮一像素都不动，
     也不会像绝对定位那样盖住按钮或灰字。 */
  .batch-detail-notes > .entry-call-notice {
    position: static;
    transform: none;
    margin-left: auto;
    flex: none;
  }
  /* 批量页题头：左边那串文字（原文 X 字 + 灰字/绿字）自己吃掉剩余宽度（窄了省略号），
     右边的两个按钮因此始终贴在这一行最右、且彼此位置固定 ——
     「入库该题」「调用该题」成对靠右，跟单题录入那对（保存 / 调用该题）左右呼应。 */
  .batch-detail-notes {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 10px;
    overflow: hidden;
  }
  .batch-detail-notes > .batch-format-note {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .batch-detail-head .entry-call { margin-left: 0; }
  /* 结果提示在按钮正下方整行展开：备份路径很长，挤在按钮右侧会折成窄条 */
  .action-feedback { min-width: 0; }
  /* 保存成功不再撑一个绿框：只在按钮右边显示一小行；详情（备份路径等）挂 title 悬停看 */
  .save-flash {
    font-size: 12px;
    color: var(--a-color);
    white-space: nowrap;
  }
  .action-feedback > .form-error,
  .action-feedback > .form-notice { margin-bottom: 0; }
  /* 编辑表单没渲染时（拉详情失败）的兜底提示，内边距对齐 .edit-form */
  .form-error.standalone { flex: none; margin: 10px 12px; }
  /* 新增成功后表单已关闭，结果提示显示在中栏顶部 */
  .form-notice.standalone { flex: none; margin: 10px 12px 0; }
  .form-actions button {
    padding: 5px 16px;
    border-radius: 6px;
    border: 1px solid var(--border-strong);
    background: #fff;
    cursor: pointer;
    font-size: 13px;
  }
  .form-actions button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
  .form-actions button:disabled { opacity: 0.6; cursor: default; }
  .form-error {
    margin-bottom: 10px;
    padding: 6px 8px;
    border-radius: 6px;
    background: #fdf3f2;
    border: 1px solid #f2c2bf;
    color: var(--c-color);
    font-size: 12.5px;
  }
  .form-notice {
    margin-bottom: 10px;
    padding: 6px 8px;
    border-radius: 6px;
    font-size: 12.5px;
    word-break: break-all;
  }
  .form-notice.success { background: #eef9f1; border: 1px solid #b7e0c2; color: #14612c; }
  .form-notice.error { background: #fdf3f2; border: 1px solid #f2c2bf; color: #a01f17; }
  .form-notice .notes {
    margin-top: 3px;
    font-size: 12px;
    color: #4a5464;
    word-break: break-word;
  }
  /* 标签框 = 轻量代码框（2026-10-17 用户："跟题干等类似的代码框，功能不变"）：
     宿主复用 `.code-edit` —— 边框 / 圆角 / 字体 / 整套语法配色 / 聚焦描边 /
     **左下角拖手**全都继承（见 .code-edit 的 direction 那条），这里只把它压回"一行高"。
     ⚠️ 必须压掉 `.code-edit` 的 min-height: 96px，否则标签行会高出一大截。
     （原来那三条 `direction/unicode-bidi/text-align` 是给原生 textarea 用的，
       换成代码框之后不再需要 —— `.code-edit` 的 RTL 只用来挪手柄，内容始终 LTR。）
     ⚠️ 高度取 **33px**（不是"一行刚好 30px"，也不取更高）为的是 Safari：
     实测（Playwright WebKit + Chromium 逐偏移拖拽）Safari 下原生 resizer 的**可用命中区
     ≈ 框高 − 28px**（28px 那一截被 CodeMirror 的文字行挡住，再往下的空白才归手柄）——
     30px 时只剩 2px（几乎点不到）、33px 约 5px、38px 才有 10px（Chrome 那边各偏移本来就都命中）。
     用户 2026-10-17："可以适当加框高，Safari 和 chrome 都加" → 两边统一；
     先试了 38px，用户看后"加太多了，取 33px" → 定为 33px（Chrome 全偏移仍命中）。
     多出来的那点高度落在文字**下方**（与原 textarea 的"文字靠上"一致，不是居中）。 */
  .code-edit.tags-host { min-height: 33px; }
  /* 左下角**自绘抓手**（2026-10-17 用户选方案 3）：
     外观一个字不变（全透明；原生那份 resizer 的斜线仍由浏览器画在原处），
     只是在左下角铺一块 14×14 的命中区，把拖动交给自己的 pointer 逻辑
     （见 onLiteGripPointerDown）。为什么需要它：Safari 下原生手柄的可用命中区
     ≈ 框高 − 文字行底，而这个框只有 33px、内边距又与其它框对齐（6px）→ 只剩 ~0.5px，
     实测各个偏移都点不到（Chrome 不受影响，但既然要统一，就两个引擎都走这条）。
     z-index 取 5：压过程序里的正文层，又低于 CM 自己的滚动条（那个是 6）。
     `cursor: ns-resize` 与"只改高度"的原生手感一致。 */
  .lite-resize-grip {
    position: absolute;
    left: 0;
    bottom: 0;
    width: 14px;
    height: 14px;
    z-index: 5;
    background: transparent;
    cursor: ns-resize;
  }
  /* ⚠️ **不覆盖 `.CodeMirror-lines` 的内边距**（用户 2026-10-17 两次口径合起来的要求：
     先是"第一行文字到框的留白比其它框少"→ 不能压成 4px；
     看了把 2px 挪到下方的版本又说"把少掉的那 2px 去掉，不挪到下方留白里"）。
     所以这里一个字都不写，直接继承 `.code-edit .CodeMirror-lines { padding: 6px 8px }`：
     四边留白与题干等框逐像素一致（实测首行留白都是 7px = 1px 边框 + 6px）。
     ⚠️ 代价（实测，2026-10-17）：Safari 那边原生拖手的可用命中区 ≈ 框高 − 文字行底，
     6px 下内边距把这截空白吃掉了 → 只剩 ~0.5px（基本点不到）；Chrome 不受影响，各偏移都命中。
     要救 Safari 只能抬高框（见上面 min-height 的注释）或把空白留在文字下方 —— 用户暂选"不加"。 */

  /* 新增表单：命令头预览那一行的标签 + 目标文件选择（同行，零额外高度） */
  .label-row {
    display: flex;
    align-items: center;
    gap: 10px;
    justify-content: space-between;
    margin-bottom: 3px;
  }
  .label-row > label { margin-bottom: 0; }
  /* 来源字段的标签行：靠左排，别用 space-between 把复选框推到列尾 */
  .label-row.tight { justify-content: flex-start; gap: 14px; }
  .remember-source {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex: none;
    font-size: 12px;
    color: var(--muted);
    cursor: pointer;
  }
  .file-choice {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    flex: none;
    font-size: 12px;
    color: var(--muted);
  }
  .file-choice select {
    width: auto;
    min-width: 62px;
    padding: 2px 6px;
    font-size: 12px;
  }
  /* 新增表单里挂在字段下方的小字（槽位原来源、ID 来源） */
  .slot-hint {
    margin-top: 3px;
    font-size: 11.5px;
    color: var(--muted);
    word-break: break-word;
  }
  /* 已入库那一档：与批量页「已入库：ID」同一个绿 */
  .slot-ok { color: var(--a-color); }
  /* 题目ID 的槽位/状态提示：从输入框下方挪到标签行右侧。
     用一个偏青的灰蓝（#4a6da0）——比通用灰字（--muted）略有色彩、好和"普通说明"区分，
     又远没到主色/成功绿/警告黄那种抢眼程度。
     标签 flex:none 且提示自己带省略号：列变窄时是提示先被截断，绝不挤压「题目ID」。 */
  .slot-chip {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 11.5px;
    color: #4a6da0;
  }
  .form-row.field-id .label-row > label { flex: none; }

  /* 四个元信息字段合成一行。
     比例按全库实测的最长内容定：来源列要放得下最长的一条（358px 文本），
     难度列要放得下「保持原值（J）」；其余两列本来就富余 2~3 倍。
     用 minmax(0, …) 而不是裸 fr，避免输入框的固有宽度把列撑开。 */
  .form-grid {
    display: grid;
    grid-template-columns: minmax(0, 3.5fr) minmax(0, 1fr) minmax(0, 1.5fr) minmax(0, 2fr);
    gap: 0 8px;
  }
  /* 断点量的是中栏自己的宽度，不是视口宽度——右栏将来从 400 扩到 560，
     这条规则一行都不用改。720 这个值由难度列的 125px 反推而来。 */
  @container (max-width: 719px) {
    .form-grid {
      grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr) minmax(0, 1fr);
    }
    .form-grid > .field-source { grid-column: 1 / -1; }
  }
  .form-row input[readonly] { background: #f3f5f8; color: #55606f; cursor: not-allowed; }
  .form-row select:disabled { background: #f3f5f8; color: #55606f; cursor: not-allowed; }
  .form-actions button.ghost { color: #4a5464; }

  /* 预录员的「我的草稿」（阶段 2a）：待在录入页顶部，管理员 / 只读员不渲染这块 */
  /* 审核队列（阶段 2b）：一个弹窗两屏 —— 列表 / 详情。宽度比普通二次确认大一些，
     内容高了在内部滚动，弹窗尺寸不跟着内容跳。 */
  .modal.review-modal {
    width: 700px;
    max-height: 84vh;
    display: flex;
    flex-direction: column;
  }
  .review-head { display: flex; align-items: center; gap: 8px; flex: none; }
  .review-title { font-size: 14px; font-weight: 600; color: #253244; }
  .review-head .mini-action:nth-last-child(1) { margin-left: auto; }
  .review-head .mini-action + .mini-action { margin-left: 0; }
  .review-notice {
    /* 提示做成**浮层**（fixed）：它出现/消失都不会把下面的详情与预览顶下去 ——
       审核页的两栏是"撑满剩余高度"的，任何占位的一行都会直接挤压预览区。 */
    position: fixed;
    top: 96px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 70;
    margin: 0;
    padding: 6px 9px;
    border-radius: 6px;
    background: #eef4ff;
    font-size: 12px;
    max-width: 70vw;
    box-shadow: 0 6px 20px rgba(20, 30, 50, 0.16);
  }
  .review-notice.ok { background: #e9f7ee; }
  .review-notice.err { background: #fdeceb; color: #b42318; }
  /* 审核区顶部的「原题」卡（改编草稿专有）：灰底 + 左侧深灰粗条 + 「原题」标签 ——
     与下面"改编题本身"那四个白底框（.review-field）一眼分得开；不抢主色（主色留给"通过"）。 */
  .review-base {
    margin: 0 0 12px;
    padding: 8px 10px 10px;
    border: 1px solid var(--border);
    border-left: 4px solid #8d99ab;
    border-radius: 6px;
    background: #f4f6f9;
  }
  .review-base-head {
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    margin: 0 0 8px;
  }
  .review-base-tag {
    flex: none; padding: 1px 8px; border-radius: 999px;
    background: #8d99ab; color: #fff; font-size: 11.5px; font-weight: 600;
  }
  .review-base-note { font-size: 11.5px; color: var(--muted); }
  .review-base-id {
    margin-left: auto;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 12px; font-weight: 600; color: #4a5464;
  }
  .review-base-card { background: #fff; box-shadow: none; }
  .review-base-figure { margin-top: 8px; }
  .review-list, .review-detail {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    scrollbar-gutter: stable;
    margin-top: 10px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: #fbfcfd;
  }
  .review-row { padding: 7px 10px; border-bottom: 1px solid var(--border); cursor: pointer; }
  .review-row:last-child { border-bottom: 0; }
  .review-row:hover { background: var(--accent-soft); }
  /* 被别人认领的行：半透明 + 锁标记 —— 一眼看出"这条有人在弄"，但**不是**禁用（
     点进去仍能看到只读详情；超时后谁都能认领） */
  .review-row-claimed { opacity: 0.62; }
  .review-row-claimed:hover { opacity: 0.85; }
  .mp-lock {
    margin-left: auto;
    flex: none;
    font-size: 11.5px;
    color: #96601a;
    background: #fdf7ea;
    border: 1px solid #ecd5a8;
    border-radius: 999px;
    padding: 1px 8px;
  }
  /* 认领徽章（详情头那一行里的）：绿 = 我认领了，琥珀 = 别人在处理。
     与列表里的 .mp-lock 同一套颜色语言，但**不参与换行膨胀** —— 一行里放得下。 */
  .mp-lock.ok { border-color: #b7e0c2; background: #f2fbf4; color: #1a7f37; }
  /* ---------------- 审核队列（独立整页，本轮） ----------------
     跟后台管理区同一套外壳：页头（标题 + 返回按钮）+ 下面一整块工作区；
     工作区是"左列表 / 右详情+预览"两栏 —— 审核这件事就是"在列表里挑一份，
     然后对着排版结果看"，两栏比弹窗那种上下摞着直观得多。 */
  .review-view {
    /* 与后台管理区**同一款外壳**：普通流里的 flex 子项（不 fixed、不盖顶栏），
       顶栏（含大标题「数学题库工作台」与「审核队列（N）」角标）照常显示。
       内边距跟 .admin-view 逐字一致，两个页面的页头才会长得一模一样。 */
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;         /* 内部各栏自己滚，整页不出现第二条滚动条 */
    padding: 16px 22px 24px;
    background: var(--bg);
  }
  .review-layout {
    flex: 1 1 0;
    min-height: 0;
    display: grid;
    grid-template-columns: 330px minmax(0, 1fr);
    gap: 12px;
  }
  .review-sidebar,
  .review-main {
    min-height: 0;
    display: flex;
    flex-direction: column;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--panel, #fff);
    overflow: hidden;
  }
  .review-sidebar-head {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: none;
    padding: 9px 12px;
    border-bottom: 1px solid var(--border);
    background: #f7f9fb;
    font-size: 12.5px;
    font-weight: 600;
    color: #2c3444;
  }
  /* 两栏的「卡片标题条」**高度写死成同一个值**，下沿那条分隔线才会严丝合缝：
     左边那条里最高的东西是筛选框（21.5px），右边是认领徽章（23.5px）——
     内容差 2px，两条底边就跟着差 2px（真机截图量到 1.5px）。
     写死 43px：减掉 9+9 内边距与 1px 底线，内容区正好 24px，23.5px 的徽章放得下。
     （全局 * { box-sizing: border-box }，这里的高度含内边距与底线。）
     **两条一起写**：只改一条的话，换一栏内容就又对不上了。 */
  .review-sidebar-head,
  .review-detail-head { height: 43px; }
  .review-filter {
    flex: 1 1 auto;
    min-width: 0;
    padding: 3px 8px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    font-family: inherit;
    font-size: 12px;
    font-weight: 400;
  }
  .review-list { flex: 1 1 0; min-height: 0; overflow-y: auto; }
  /* 被选中的那一条（正在审的）要一眼看得出来 */
  .review-row-active { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
  .review-empty {
    margin: auto;
    color: var(--muted);
    font-size: 13px;
  }
  .review-detail-head {
    display: flex;
    align-items: center;
    gap: 10px;
    /* 这行也是"次要信息"：跟左边列表里那行小字同号（11.5px）。
       原来没写字号 → 吃 body 的 14px，比上面「作者 zqh」大一圈，一进详情就觉得字发胖。 */
    font-size: 11.5px;
    /* **永远一行**：信息格可以各自省略号，但认领徽章不缩 —— 这样无论"我认领了 /
       别人在处理 / 没人认领"，这一行都是同样高度，下面的原文区和预览区不会被顶动。 */
    flex-wrap: nowrap;
    overflow: hidden;
    flex: none;
    padding: 9px 12px;
    border-bottom: 1px solid var(--border);
    background: #f7f9fb;
  }
  .review-detail-head > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .review-detail-head .mp-lock { flex: none; }
  /* 详情主体：左边原文、右边排版预览 —— 审题时两边对着看 */
  .review-body {
    flex: 1 1 0;
    min-height: 0;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 0;
  }
  .review-col {
    min-height: 0;
    overflow-y: auto;
    padding: 12px 14px;
  }
  /* 左栏：内容自己滚 + 底部一条操作栏 —— 按钮就落在两栏分界线左边（右边那栏
     排版预览因此能一直伸到页面最下面）。 */
  .review-col-source {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    padding: 0;
    border-right: 1px solid var(--border);
  }
  .review-col-source-body {
    flex: 1 1 0;
    min-height: 0;
    overflow-y: auto;
    padding: 12px 14px;
  }
  /* 预览这一栏：**整栏就是一张卡片**，卡片撑满右栏高度（下面留白用卡片背景填上，
     不再是"内容结束后剩一截灰底"）；内容多了在卡片内部滚。 */
  .review-col-preview {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: #fbfcfd;
  }
  .review-preview-body {
    flex: 1 1 0;
    min-height: 0;
    overflow-y: auto;
    padding: 10px 11px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: #fff;
  }
  .review-preview-head {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
    font-size: 12.5px;
    font-weight: 600;
    color: #2c3444;
  }
  .review-preview-img {
    display: block;
    width: 100%;
    margin-bottom: 10px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: #fff;
  }
  /* PDF 模式：容器交给 PDF 查看器自己滚（外层不再滚，免得两个滚动条打架）。
     里面的 .rpreview-pdfscroll / -pages / -page / -note / -hud 与右栏「排版预览」
     完全是同一套（那边是 PDF.js 自绘，无浏览器工具栏）。 */
  .review-preview-body.pdf-mode {
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  .review-pdfhost {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    border-radius: 6px;
    overflow: hidden;
  }
  /* 编译警告态（2026-09-30）：**有页但有 `!`** —— 图照给，但要说清"可能不完整"。
     ⚠️ 布局约束（用户 2026-09-30 明确）：这些文案**不许挤压别的卡片、不许让谁偏移或变高**。
     所以：① 常驻的只有一个**同行小胶囊**（塞进已有的 meta / 状态那一行，nowrap + 省略号，
     行高不变）；② 明细放在**绝对定位的悬浮气泡**里（position:absolute，不参与布局，
     点一下也能展开，触屏可用）。 */
  .preview-warn-chip {
    display: inline-block;
    margin-left: 6px;
    padding: 0 6px;
    border: 1px solid #e6cf9a;
    border-radius: 9px;
    background: #fdf6e3;
    color: #8a6410;
    font-size: 11.5px;
    line-height: 16px;
    white-space: nowrap;
    cursor: pointer;
  }
  .preview-warn-chip:hover { background: #fbf0d4; }
  .preview-warn-pop {
    position: absolute;
    z-index: 30;
    top: 100%;
    left: 0;
    max-width: min(560px, 100%);
    max-height: 240px;
    overflow: auto;
    margin-top: 4px;
    padding: 8px 10px;
    border: 1px solid #e6cf9a;
    border-radius: 8px;
    background: #fdf6e3;
    color: #7a5b12;
    font-size: 12px;
    line-height: 1.6;
    box-shadow: 0 6px 18px rgba(60, 45, 10, 0.16);
  }
  .preview-warn-pop b { color: #8a6410; }
  .preview-warn-list {
    margin: 4px 0 0;
    padding-left: 18px;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 11.5px;
  }
  .preview-warn-list li { margin: 1px 0; }
  /* 那一行本身也不许换行变高：超长就省略号（完整文案在 title 里） */
  .preview-meta { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .preview-meta-wrap { position: relative; }
  /* 审核页那一行同理：胶囊同行、气泡挂在它下面（绝对定位，不改行高） */
  .review-preview-head { position: relative; }
  /* 那一行是 flex（默认 nowrap）：胶囊不许被压缩，时长那截让位成省略号 —— 行高不变 */
  .review-preview-head > .preview-warn-chip { flex: none; }
  .review-preview-head > .muted {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* 右栏：胶囊钉在 PDF 区右上角，**绝对定位**——不占位、不挤压 PDF */
  .rpreview-warn-chip { position: absolute; z-index: 30; top: 6px; left: 6px; margin-left: 0; }
  /* 右栏的明细气泡：`.preview-warn-pop` 默认 `top:100%` 是相对"最近的定位祖先"算的，
     而这里的定位祖先是整个 .rpreview-pdfhost —— 它会落到 PDF 区**底边之外**，
     再被 host 的 overflow:hidden 整块裁掉（2026-10-19 真机：胶囊能点，但什么都没出现）。
     所以右栏这一处必须按胶囊的实际位置（top 6 + 高 18 + 间距 4）钉在它下面。 */
  .rpreview-pdfhost > .preview-warn-pop {
    top: 28px;
    left: 6px;
    max-width: calc(100% - 12px);
  }

  /* 编译失败：**显式**摆出来（这是"防止代码有误"的关键，不能静默） */
  .review-preview-error {
    margin-bottom: 10px;
    padding: 9px 11px;
    border: 1px solid #e7b4ae;
    border-radius: 8px;
    background: #fdeceb;
    color: #8f1d16;
    font-size: 12.5px;
    line-height: 1.6;
  }
  .review-preview-error b { color: #b42318; }
  .review-preview-log {
    max-height: 220px;
    overflow: auto;
    margin: 8px 0 0;
    padding: 8px 10px;
    border-radius: 6px;
    background: #fff;
    border: 1px solid #e7b4ae;
    color: #4a5464;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 11px;
    line-height: 1.5;
    white-space: pre-wrap;
    word-break: break-word;
  }
  .review-preview-tip {
    margin: 0 0 8px;
    color: var(--muted);
    font-size: 11.5px;
    line-height: 1.6;
  }
  /* TikZ 源码：跟题干 / 答案一样**完整展开**（不折叠、不内滚）——
     审图代码常常要一眼看到底，240px 的小窗口只会让人在里面来回滚；
     整栏（.review-col-source-body）本来就是可滚的，长了往下滚整栏即可。 */
  .review-code {
    margin: 0;
    padding: 8px 10px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: #fbfcfd;
    color: #2c3444;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 11.5px;
    line-height: 1.5;
    white-space: pre-wrap;
    word-break: break-word;
  }
  .review-actions-bar {
    flex: none;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border-top: 1px solid var(--border);
    background: #f7f9fb;
  }
  /* 两个按钮整体靠右（贴着右栏那一侧的边），左边留说明文字。
     flex: none + 按钮 nowrap：说明文字长的时候**只让说明省略**，不许把按钮压成两行。 */
  .review-actions-right { margin-left: auto; flex: none; display: inline-flex; align-items: center; gap: 8px; }
  .review-actions-bar .muted {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .review-actions-bar button { white-space: nowrap; flex: none; }
  /* 「拒绝」做成和「通过并写入题库」**一样大**的次级按钮：白底描边，不用小按钮 */
  .review-reject {
    padding: 4px 12px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #fff;
    color: #4a5464;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
  }
  .review-reject:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
  .review-reject:disabled { opacity: 0.55; cursor: default; }
  /* 窄屏（笔记本半屏）：预览落到原文下面 —— 宁可上下滚，也不把两栏挤成豆腐块 */
  @media (max-width: 1180px) {
    .review-body { grid-template-columns: minmax(0, 1fr); }
    .review-col-source { border-right: 0; border-bottom: 1px solid var(--border); }
  }
  .review-row-main { display: flex; align-items: center; gap: 8px; }
  .review-row-sub { display: flex; gap: 10px; margin-top: 2px; font-size: 11.5px; color: var(--muted); }
  .review-row-path {
    margin-top: 2px;
    font-size: 11.5px;
    color: var(--muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .review-detail { padding: 10px; }
  .review-meta { display: flex; flex-wrap: wrap; gap: 10px; font-size: 11.5px; color: var(--muted); }
  .review-field { margin-top: 10px; }
  /* 作者没写的那一项：框还在、里面用红字明说 —— 不能让"空"看起来像"没有这个字段" */
  .review-text.review-missing {
    color: var(--c-color);
    background: #fdf3f2;
    border-color: #f0c4c0;
  }
  /* 审核页四栏里的题干 / 答案 / 标签：显示**大佬提交的原式**（不渲染），
     所以走等宽 + 保留换行 —— 花括号、`\begin{cases}`、`$…$` 都原样看得见。
     渲染后的样子在右栏「排版预览」（服务端真编译出来的图）。 */
  .review-text.review-raw {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 11.5px;
    line-height: 1.6;
    white-space: pre-wrap;
    word-break: break-word;
  }
  .review-field > label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 3px; }
  /* 两栏的"抬头"要在同一条水平线上：左边是「题干」这个标签，右边是「排版预览」+ 那颗
     「重新预览」按钮。左栏第一个字段是**这一栏的第一行**，不该再吃 10px 的上边距；
     而右边那行的高度是那颗按钮撑出来的（23px），左边这行就跟到同样的高度 ——
     两边的**文字中线**于是完全重合（实测差 0.0px）。 */
  .review-col-source-body > .review-field:first-child { margin-top: 0; }
  .review-col-source-body > .review-field:first-child > label { line-height: 23px; }
  .review-text {
    padding: 7px 9px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: #fff;
    font-size: 13px;
    line-height: 1.7;
    overflow-wrap: anywhere;
  }
  /* align-items: flex-end：两个框的**输入框下沿**永远对齐。
     标签文字长短不一时（比如"难度（决定新 ID）"折成两行），也不会把其中一个框顶下去。 */
  .review-form { display: flex; align-items: flex-end; gap: 10px; margin-top: 10px; }
  .review-label {
    display: flex;
    flex-direction: column;
    gap: 3px;
    flex: 1 1 auto;
    min-width: 0;
    font-size: 12px;
    color: var(--muted);
  }
  /* 窄栏（难度）：给够一行放得下"难度（决定新 ID）"，并禁止折行 —— 折了就和左边那个框不平 */
  .review-label.narrow { flex: none; width: 124px; white-space: nowrap; }
  .review-label.narrow input { width: 100%; box-sizing: border-box; }
  .review-label input {
    padding: 4px 8px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    font: inherit;
    font-size: 12.5px;
    /* 与旁边那两个控件**同高**：三个控件的默认高度各不相同（input 28 / select 27 /
       button 31.3），底边对齐（align-items: flex-end）时上沿就参差不齐 —— 实测差 4.3px。
       钉死高度后，两框的**上沿 / 下沿 / 高度**全一致。 */
    height: 28px;
    box-sizing: border-box;
  }
  /* 难度改成下拉框、写入目标改成"点一下弹目录树"的框：两者尺寸 / 圆角 / 字号
     跟旁边那几个文本框**完全一致**（原来这两个都是手填的 input）。 */
  .review-label select {
    padding: 4px 8px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #fff;
    font: inherit;
    font-size: 12.5px;
    width: 100%;
    box-sizing: border-box;
    height: 28px;
  }
  .review-target-field {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    height: 28px;
    box-sizing: border-box;
    padding: 4px 8px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #fff;
    font: inherit;
    font-size: 12.5px;
    color: #2c3444;
    text-align: left;
    cursor: pointer;
  }
  .review-target-field:hover { border-color: var(--accent); background: var(--accent-soft); }
  .review-target-field .review-target-text {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* 还没选：跟别的输入框的 placeholder 一样是灰的 */
  .review-target-field .review-target-text.empty { color: #9aa2af; }
  .review-target-field .review-target-caret { flex: none; color: var(--muted); font-size: 10px; }
  .review-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
  .review-approve { background: var(--accent); border: 1px solid var(--accent); color: #fff;
    padding: 4px 12px; border-radius: 6px; cursor: pointer; font: inherit; font-weight: 600; }
  .review-approve:disabled { background: #a9bdea; border-color: #a9bdea; cursor: default; }

  /* 「我的待审」弹窗：**固定尺寸**（内容多少都不变高，不会一边点一边跳），
     里面分三块（待审 / 已拒绝 / 已通过），每块最多先显示 5 条、多的在框里滚。
     以前它是挂在录入区下面的小浮层 —— 空间小，还盖着表单。 */
  .modal.mypending-modal {
    width: 820px;
    height: 780px;
    max-width: calc(100vw - 48px);
    max-height: calc(100vh - 48px);
    display: flex;
    flex-direction: column;
  }
  /* 标题：蓝色 + 加粗（与页面里其它「区块标题」保持一致），下面直接跟三个 tab */
  /* 观感（accent / 15px / 700）已经并进上面那条共享的 `.modal-title` 组，
     这里只留「我的待审」自己那一档下边距 —— 那个弹窗比别的紧 2px。 */
  .modal.mypending-modal .mp-title { margin: 0 0 10px; }
  /* 关闭按钮离上面那张卡留一口气（之前是 0 —— 贴着列表底边，看着挤） */
  .modal.mypending-modal .modal-actions { margin-top: 14px; }
  .modal.mypending-modal .modal-note.ok { color: var(--a-color); }
  .modal.mypending-modal .modal-note.err { color: var(--c-color); }
  /* 三个 tab：一次只显示一类。**tab 宽度写死**（min-width + 居中）——
     数字从 9 → 10、99 → 100 时，别的 tab 一个像素都不挪（用户强调的"数字位置预留"）。
     数量用等宽数字（tabular-nums），位数变化时数字本身也不抖。 */
  .mp-tabs {
    display: flex;
    gap: 4px;
    flex: none;
    margin-bottom: 10px;
    padding: 3px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: #f7f9fb;
  }
  .mp-tabs button {
    flex: 1 1 0;
    min-width: 118px;
    padding: 5px 10px;
    border: 1px solid transparent;
    border-radius: 6px;
    background: transparent;
    color: #4a5464;
    font: inherit;
    font-size: 12.5px;
    font-variant-numeric: tabular-nums;
    text-align: center;
    white-space: nowrap;
    cursor: pointer;
  }
  .mp-tabs button:hover { background: #eef2f7; }
  .mp-tabs button.active {
    border-color: var(--border);
    background: #fff;
    color: var(--accent);
    font-weight: 600;
  }
  /* 「已通过」那一行的备注：共计 N 题通过审核 */
  .mp-passed-note {
    flex: none;
    margin: 0 0 8px;
    font-size: 12px;
    color: var(--muted);
  }
  .mp-passed-note b { color: #2c3444; font-size: 12.5px; }
  .mp-group-body {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: #fbfcfd;
  }
  .mypending-list-wrap {
    max-height: 46vh;
    overflow: auto;
    overscroll-behavior: contain;
    margin: 0 0 10px;
    padding: 0 10px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: #fbfcfd;
  }
  .mypending-tip { font-size: 11.5px; color: var(--muted); }
  .mypending-list {
    list-style: none;
    margin: 0;
    padding: 0 10px 6px;
    /* 弹窗里这个列表自己滚（外面那层只负责给高度）；
       滚动条槽**常驻**（stable）：条数在"能放下/放不下"之间变化时，列表宽度不跳 */
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-gutter: stable;
  }
  .mypending-list li {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 4px 2px;
    border-top: 1px solid var(--border);
    font-size: 12px;
  }
  /* 阶段 2b：一行装不下"状态 + 原因/结果"，所以 <li> 改成两行块，里面第一行还是 flex */
  .mypending-list li.mp-row { display: block; }
  .mp-line { display: flex; align-items: center; gap: 8px; }
  /* 行内说明（拒绝原因 / 已通过）：**永远一行** —— 长了省略号截断，悬停看全。
     这是硬约束：这几行一换行就会把那一行（以及整张卡）撑高，"点开一份 → 卡片跳一下"。 */
  .mp-note {
    margin-top: 2px;
    padding-left: 2px;
    font-size: 11.5px;
    color: var(--muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .mp-note.err { color: var(--c-color); }
  .mp-note.ok { color: var(--a-color); }
  /* 「编号已变」的提醒：光一个 ⚠️ 谁都看不出是什么意思，所以配一句话 + 琥珀底 + 加粗。
     只写**横向** padding / background：行内非替换元素的上下的 padding 和边框
     不参与行盒高度计算，所以这一行（以及整张卡）不会被撑高 —— 硬约束见上面 .mp-note。 */
  .mp-id-warn {
    margin-left: 6px;
    padding: 0 5px;
    border-radius: 3px;
    background: #fbe6bf;
    color: #8a5100;
    font-weight: 600;
  }
  /* 「已自动更新工作区引用」：同一条单行里的**成功**回执（绿色胶囊）。
     与 .mp-id-warn 同一套写法（只写横向 padding / background，不撑高行）。 */
  .mp-id-fixed {
    margin-left: 6px;
    padding: 0 5px;
    border-radius: 3px;
    background: #dcefdc;
    color: #1f6b2a;
    font-weight: 600;
  }
  /* 胶囊后面那颗「展开清单」小按钮：行内、下划线、跟正文同号 */
  .mp-id-link {
    margin-left: 6px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--accent);
    font: inherit;
    text-decoration: underline;
    cursor: pointer;
  }
  /* 展开出来的文件清单：**放在 .mp-note 外面**（那一行是 nowrap + 省略号，装不下列表），
     所以只有点开时才多占几行高度（用户自己点的，不是状态变化导致的跳动）。 */
  .mp-rewrite-files {
    margin: 3px 0 2px 18px;
    padding: 0;
    list-style: disc;
    font-size: 11.5px;
    color: var(--muted);
    max-height: 132px;
    overflow: auto;
  }
  .mp-rewrite-files li { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .mypending-list li:first-child { border-top: 0; }
  .mp-status {
    flex: none;
    padding: 1px 6px;
    border: 1px solid #f0d9ab;
    border-radius: 999px;
    background: #fdf8ee;
    color: var(--b-color);
    font-size: 11px;
  }
  .mp-status.draft { border-color: var(--border); background: #f3f5f8; color: var(--muted); }
  .mp-status.approved { border-color: #b7e0c2; background: #f2fbf4; color: var(--a-color); }
  .mp-status.rejected { border-color: #f0c4c0; background: #fdeceb; color: var(--c-color); }
  .mp-stem {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .mp-kp, .mp-time { flex: none; color: var(--muted); }
  /* 审核列表那一行小字（作者 · 知识点 · 时间）：**作者贴左、时间贴右**，
     中间剩下的全给"知识点"，放不下就省略号（悬停 title 看全）。

     两边各给**一块定宽**：名字有长有短，不定宽的话，同一个人的行与下一行的
     知识点宽度就会跟着名字长短跳；时间那一块定宽 108px（格式固定 =
     「YYYY-MM-DD HH:MM」实测 105px），保证**永远完整显示**，不会被压成
     「2026-09-25 20:」那种半截（真机截图里就是这样）。
     名字超出一格就省略号（模板里挂了 title，悬停看全名）。 */
  .review-row-sub .mp-author {
    flex: none;
    width: 80px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .review-row-sub .mp-kp {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .review-row-sub .mp-time {
    flex: none;
    width: 108px;
    text-align: right;
  }
  /* 作者名：紫色（--author-color）+ **跟着旁边那行同号**（不写死字号，
     审核列表里跟 11.5px 的知识点/时间一样大，"我的草稿"里跟 12px 那行一样大）。 */
  .mp-author { flex: none; color: var(--author-color); }
  .mp-time { font-variant-numeric: tabular-nums; }
  .mp-empty { color: var(--muted); }
  .modal .modal-note { margin: -8px 0 14px; font-size: 12px; color: var(--muted); }

  /* 查重弹窗：把"像的那几道"直接渲染成题卡，不用再去搜。
     固定高度 + 内部滚动：命中多长，弹窗尺寸都不变。 */
  .modal.dedup-modal { width: 660px; }
  .dedup-list {
    max-height: 400px;
    overflow: auto;
    padding: 0 10px 6px;
    margin-bottom: 14px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: #fbfcfd;
  }
  .dedup-group-title {
    position: sticky;
    top: 0;
    z-index: 1;
    margin: 0 -10px 8px;
    padding: 7px 10px;
    border-bottom: 1px solid var(--border);
    background: #f2f5fa;
    font-size: 12px;
    font-weight: 600;
    color: #4a5464;
  }
  /* 弹窗里的题卡沿用中栏那张卡的观感，但更紧凑、并且**不可拖动**（这里只负责看） */
  .dedup-card {
    cursor: default;
    margin-bottom: 8px;
    background: #fff;
  }
  .dedup-card:active { cursor: default; }
  .dedup-score { color: var(--b-color); font-size: 11.5px; }
  .dedup-answer {
    margin-top: 6px;
    padding-top: 6px;
    border-top: 1px dashed var(--border);
    font-size: 12px;
    color: #4a5464;
  }

  /* 审核页顶部的「可能有重复」提示（数据来自大佬提交前那次查重，存在草稿里）。
     琥珀 = 注意但不是故障，与"该题待审批"那条同一支颜色；卡片沿用查重弹窗那张
     （.card.dedup-card：紧凑、不可拖），管家就地就能对比题面，不用跳走。 */
  .review-dup {
    margin: 0 0 10px;
    padding: 8px 10px 2px;
    border: 1px solid #f0d9a8;
    border-radius: 8px;
    background: #fdf8ee;
  }
  .review-dup-head {
    margin-bottom: 8px;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--b-color);
  }
  .review-dup-sub {
    margin-left: 4px;
    font-weight: 400;
    font-size: 11.5px;
    color: var(--muted);
  }
  .review-dup .dedup-card { background: #fff; }
  .review-dup .dedup-card:last-child { margin-bottom: 8px; }

  /* 后台管理区（阶段 4）：顶栏之下的**整页视图**，不是弹窗。
     工作台那份 DOM 用 v-show 藏着（不卸载）—— CodeMirror 实例、没保存的表单都留着，
     返回时接着用；编辑器高度由已有的 ResizeObserver 在重新显示时自动 refresh。
     页面自己滚（小窗口），页面里的列表各自内滚（条数多了不长高）。 */
  .admin-view {
    flex: 1 1 auto;
    min-height: 0;
    /* 竖排 flex：面板用 flex:1 撑满"页头 + 页签"以下的全部高度（原来面板是固定高度，
       下面留一大块白）。页面整体在窗口很矮时才自己滚。 */
    display: flex;
    flex-direction: column;
    overflow: auto;
    /* 滚动条槽位常驻：万一内容高到需要滚动，出现/消失滚动条也不会让整页横向抖一下。
       （macOS 的覆盖式滚动条本来就不占宽度，这一条是给"总是显示滚动条"的系统兜底。） */
    scrollbar-gutter: stable;
    padding: 16px 22px 24px;
  }
  .admin-head { display: flex; align-items: flex-start; gap: 14px; flex: none; margin-bottom: 12px; }
  /* 「后台管理区」这一行与右上角那两颗按钮**同一条水平线**：按钮是 32px 高，
     标题的行盒也按 32px 走，两边的中线才对得上（原来是 h2/p 的自然行高，标题偏上）。
     按钮自己不动（它在页头里 flex-start，位置由页头决定）。 */
  .admin-title { margin: 0; font-size: 17px; font-weight: 700; line-height: 32px; }
  .admin-sub { margin: 5px 0 0; font-size: 12.5px; color: var(--muted); line-height: 1.6; }
  /* 「返回题库工作台」：后台区唯一的主按钮，钉在右上角（扫视动线 = 标题 → 右上角） */
  .admin-back {
    margin-left: auto;
    flex: none;
    padding: 6px 16px;
    border: 1px solid var(--accent);
    border-radius: 7px;
    background: var(--accent);
    color: #fff;
    font-size: 13px;
    cursor: pointer;
  }
  .admin-back:hover { background: #2f6ae0; border-color: #2f6ae0; }
  /* 页头右侧那一组：**只有一行** —— 状态标记 + 开关 + 局域网网址 + 复制 + 返回，
     全在同一行里（2026-10-05）。这样"打开对外访问"只是往这一行里**多塞两个元素**，
     行数不变 → 页头高度恒定（开 / 关都是 32px）→ 下面的 tab 与面板不会被顶下去。
     `flex: none` = 这一组不参与压缩；放不下时宁可溢出也不会挤成两行（见 probe 实测）。 */
  .admin-head-actions {
    margin-left: auto;
    display: flex;
    align-items: flex-end;
    flex: none;
  }
  .admin-head-row { display: inline-flex; align-items: center; gap: 8px; }
  .admin-head-actions .admin-back { margin-left: 0; }
  /* 开关的**状态标记**（开 = ● 已开启 / 关 = ○ 仅回环）：不靠按钮文案判断当前状态，
     文案说的是"点了会发生什么"（动作型），状态看这个小标记 + 颜色。 */
  .admin-state { font-size: 11.5px; white-space: nowrap; }
  .admin-state.on { color: var(--a-color); }
  .admin-state.off { color: var(--muted); }
  /* 局域网访问地址 + 复制按钮（开关开着、且确实绑在对外地址上时才出现） */
  .admin-url {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 12px;
    color: #2c3444;
    background: #fbfcfd;
    border: 1px solid var(--border);
    border-radius: 5px;
    padding: 0 7px;
    line-height: 18px;
    white-space: nowrap;
  }
  .admin-copy {
    padding: 0 9px;
    line-height: 18px;
    border: 1px solid var(--border-strong);
    border-radius: 5px;
    background: #fff;
    color: #4a5464;
    font-family: inherit;
    font-size: 11.5px;
    cursor: pointer;
    white-space: nowrap;
  }
  .admin-copy:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
  /* 「同事可访问 / 同事不可访问」开关：状态即文案（跟顶栏「全库/局部」那个 chip 同一套思路），
     开 = 主色描边、关 = 琥珀；只绑回环地址时是灰的、点不动（那种配置下这个开关没有意义）。 */
  .admin-toggle {
    padding: 6px 14px;
    border: 1px solid var(--accent);
    border-radius: 7px;
    background: #fff;
    color: var(--accent);
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
  }
  .admin-toggle:hover { background: var(--accent-soft); }
  .admin-toggle-off { border-color: #d99000; background: #fdf7ea; color: #96601a; }
  .admin-toggle-off:hover { background: #fbf0d9; }
  .admin-toggle-idle {
    border-color: var(--border-strong);
    background: #fff;
    color: var(--muted);
    font-weight: 400;
    cursor: default;
  }
  .admin-toggle-idle:hover { background: #fff; }
  .admin-toggle:disabled { cursor: default; }
  /* 白卡：页签条 + 当前页签的内容装在一起（一个整体）。
     卡自己撑满"页头"以下的全部高度，里面的列表自己滚 —— 条数多了页面也不长高。
     min-height 是给很矮的窗口保底：再矮就让 .admin-view 去滚，卡不会被压成一条缝。 */
  .admin-card {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 320px;
    background: #fff;
    border: 1px solid var(--border);
    border-radius: 10px;
    overflow: hidden;
  }
  /* 页签：卡里的第一行 —— 一条底线串起来，选中那格做成"连体卡片"（白底压在底线上） */
  .admin-tabs {
    display: flex;
    gap: 4px;
    flex: none;
    padding: 6px 10px 0;
    border-bottom: 1px solid var(--border);
    background: #fbfcfd;
    margin-bottom: 0;
  }
  .admin-tabs button {
    padding: 6px 14px;
    border: 1px solid transparent;
    border-bottom: 2px solid transparent;
    border-radius: 7px 7px 0 0;
    background: transparent;
    color: #4a5464;
    font-size: 13px;
    cursor: pointer;
  }
  .admin-tabs button:hover { background: var(--accent-soft); }
  .admin-tabs button.active {
    margin-bottom: -1px;
    border-color: var(--border);
    border-bottom-color: #fff;
    background: #fff;
    color: var(--accent);
    font-weight: 600;
  }
  /* 面板：卡片里页签条下面的那块，撑满剩余高度（flex:1），里面的列表自己滚。
     边框 / 圆角 / 底色都交给外面那张卡了（进卡之前它是自己一张卡）。 */
  .admin-panel {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    padding: 12px 14px;
    background: transparent;
    border: 0;
    border-radius: 0;
  }
  .admin-panel > p { flex: none; }
  /* 面板里的说明文字：沿用弹窗那套灰字小字号（.modal .modal-note 只对弹窗生效，
     这里不引用它，免得哪天弹窗样式改了把后台页带偏） */
  .admin-note { margin: 0 0 10px; font-size: 12px; color: var(--muted); line-height: 1.7; }
  /* 「最近操作 / 审计流水」共用固定单行说明高度：切换两页时列表顶边不上下跳；
     完整说明仍在 title 中。 */
  .journal-page-note { height: 20.4px; white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis; }
  /* 审核页页头：标题 + 一行数字（同一行、基线对齐）。
     b 用深色加粗（与「关键开关」那些数字一套），句读用灰色，读起来是一句话而不是一堆标签。
     整行 nowrap + 省略号：文案再长也只截断，不会把这行撑成两行、把下面的两栏顶下去。 */
  .review-head-line {
    display: flex;
    align-items: baseline;
    gap: 14px;
    min-width: 0;
  }
  /* 页头左半区：撑满到按钮之前（按钮组是右边的固定宽度），胶囊就钉在这条线的右端 */
  .review-view .review-head-main { flex: 1 1 auto; min-width: 0; }
  .review-view .review-head-line { width: 100%; }
  /* 审核页页头：把**文字行**提到与右边按钮同一条水平线（按钮本身不动）。
     浏览器给 h2 的默认外边距（≈15px）把这一行压低了 17px —— 去掉它即可；
     不去动 .admin-head 的 align-items（那是后台管理区靠它把说明顶到第一行的）。 */
  .review-view .admin-head { margin-bottom: 10px; }
  /* 页头标题「审核队列」与后台管理区那个「后台管理区」**同一号**（17px / 700）——
     它原来是 h2 的浏览器默认字号（比后台那个大一圈），两个整页切换时标题会跳大小。
     line-height 仍是 32px：这是把它与右边按钮对齐在一行的那个值。 */
  .review-view .admin-head h2 { margin: 0; line-height: 32px; font-size: 17px; font-weight: 700; }
  .review-head-stats {
    display: flex;
    /* 数字行**自己吃掉**长短变化（内容多了就截断）；不让它把右边的胶囊推来推去 */
    flex: 1 1 auto;
    gap: 4px;
    min-width: 0;
    margin: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .review-head-stats b { color: #2c3444; font-size: 12.5px; }
  .review-head-stats span { white-space: nowrap; }
  /* 「你已审核共计 N 题」：跟在左边那行队列数字后面，占页头中间那段留白。
     绿色（--a-color，跟顶栏「今日新增 N」同一支）= **你自己的产出**，
     与左边"队列是什么样"的灰色隔开：一边看队列，一边看自己干了多少。
     做成一颗**绿底小胶囊**（跟"已通过"那类绿标记同一套色），一眼看出是"我的状态"、
     不是左边那句话的延续。整颗高 24px < 标题行 32px → 数字再长也不会把页头撑高
     （页头一高，下面两栏就被顶下去）。0 份时退回灰胶囊：绿色留给"确实审过题"。 */
  .review-head-today {
    flex: none;
    padding: 2px 10px;
    border: 1px solid #b7e0c2;
    border-radius: 999px;
    background: #f2fbf4;
    font-size: 12px;
    line-height: 18px;
    color: var(--a-color);
    white-space: nowrap;
  }
  .review-head-today b { font-size: 12.5px; }
  .review-head-today.is-zero {
    color: var(--muted);
    background: #f7f8fa;
    border-color: var(--border);
  }
  .admin-foot { display: flex; justify-content: flex-end; gap: 8px; flex: none; margin-top: 10px; }
  /* 面板右下角的主操作按钮（刷新）：尺寸 / 圆角 / 字号跟后台其它按钮一套，
     但**加粗 + 更大的点区**，一眼看出"这里能点"。三个页签共用这一个类。 */
  .admin-refresh {
    padding: 6px 18px;
    border: 1px solid var(--border-strong);
    border-radius: 7px;
    background: #fff;
    color: #2c3444;
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
  }
  .admin-refresh:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
  .admin-refresh:disabled { opacity: 0.5; cursor: default; }
  .admin-refresh:disabled:hover { background: #fff; border-color: var(--border-strong); color: #2c3444; }
  /* 面板里的一行工具（筛选 / 刷新）：与列表同宽，右上角放刷新 */
  .admin-toolbar { display: flex; align-items: center; gap: 8px; flex: none; margin-bottom: 10px; }
  .admin-toolbar > .mini-action { margin-left: auto; }
  /* 「班级管理」（2026-10-11 第一轮）：新建表单 + 班列表 + 详情。
     一律新开 grade-* 类名，不动后台别的样式。 */
  .grade-form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px;
    margin: 0 0 12px; padding: 10px 12px; background: #fff;
    border: 1px solid var(--border); border-radius: 8px; }
  .grade-field { display: flex; flex-direction: column; gap: 4px;
    font-size: 12px; color: var(--muted); }
  .grade-field input, .grade-field select {
    font-family: inherit; font-size: 13px; padding: 4px 8px; border-radius: 6px;
    border: 1px solid var(--border-strong); background: #fff; color: #2c3444; }
  .grade-field-wide { flex: 1 1 220px; }
  .grade-field-wide input { width: 100%; box-sizing: border-box; }
  .grade-list { display: flex; flex-direction: column; gap: 8px; overflow: auto; }
  .grade-card { display: flex; flex-direction: column; gap: 3px; text-align: left;
    padding: 10px 12px; background: #fff; border: 1px solid var(--border);
    border-radius: 8px; cursor: pointer; font-family: inherit; }
  .grade-card:hover { border-color: var(--accent); background: var(--accent-soft); }
  .grade-card-name { font-size: 14px; font-weight: 600; color: #2c3444; }
  .grade-card-meta { font-size: 12px; color: var(--muted); }
  .grade-card-note { font-size: 12px; color: #4a5564; }
  .grade-detail { display: flex; flex-direction: column; gap: 8px; }
  .grade-detail-title { font-size: 15px; }
  .grade-students { margin: 0; padding-left: 18px; font-size: 13px; line-height: 1.9; }
  .grade-seq { display: inline-block; min-width: 20px; color: var(--muted); }
  .grade-phone { margin-left: 8px; font-size: 12px; color: var(--muted);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
  /* ── 「班级管理」三栏（2026-10-13「成绩分析·轮1」）──────────────────────────
     左 = 班级列表 / 中 = 横表（含「个人成绩单」占位）/ 右 = **成绩分析**（全新）。
     两条分隔条：① 左↔中（--grade-left，存 grade-split-left）② 中↔右（--grade-right，
     存 grade-split-mid —— 拖它改的是**最右那栏的宽**，中栏是 1fr，自动吃掉剩下的，
     这样轨道永远分满、不会在右边留一段空白）。
     最小宽 左 220 / 中 420 / 右 360 → 三栏合计 220+2+420+2+360 = **1004px**；
     容器比这还窄时，下面的 @media 把它改成**上下堆叠**（不横滚）。
     ⚠️ 用**变量**而不是内联 grid-template-columns：与 .users-split 同一个理由。 */
  .grade-split {
    display: grid;
    grid-template-columns:
      minmax(220px, var(--grade-left, 320px)) 2px
      minmax(420px, 1fr) 2px
      minmax(360px, var(--grade-right, 420px));
    flex: 1 1 auto;
    min-height: 0;
    overflow-x: auto;
  }
  .grade-col { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
  /* 卡片 ↔ 分隔条之间的留白：**6px**（2026-10-12 用户要求"与用户管理那条同款式"：
     那边是 14px 的栅格缝里居中画一条 2px 的线 → 两侧各 6px）。两栏同一档。 */
  /* ⚠️ 三栏时代要**每一条分隔条两边都是 6px**：左栏给右边留（左↔中的左边），
     中栏**左右都要留**（左↔中的右边 = padding-left；中↔右的左边 = padding-right），
     最右栏给左边留（中↔右的右边）。
     2026-10-14 用户报"中↔右那条的左边没有留白"—— 就是中栏漏了 `padding-right`。 */
  .grade-left { padding-right: 6px; }
  .grade-right { padding-left: 6px; padding-right: 6px; }
  /* ⚠️ `.grade-right` 这个名字是 2026-10-12 两栏时代留下的，现在是**中间那栏**（横表）——
     用户 2026-10-13 明确"中栏原样不动"，所以类名不动（重命名会牵动 CSS/JS/测试好几处）。 */
  .grade-analysis { padding-left: 6px; }
  /* ── 「成绩分析」不显示时（2026-10-14 起由总表标题行右边那个勾选框控制，默认不显示）──
     把栅格从**三栏**切回**两栏**：中栏继续是 1fr，自动吃掉原来分给最右栏的那 420px；
     那条中↔右分隔条与最右栏本身一起退场（`v-show` 让它们 `display:none`，
     元素仍在 DOM 里 —— 图表宽度那个 ResizeObserver 才不会盯住一个已被拆掉的节点）。 */
  .grade-split.no-analysis {
    grid-template-columns:
      minmax(220px, var(--grade-left, 320px)) 2px
      minmax(420px, 1fr);
  }
  .grade-split.no-analysis .grade-splitter-mid { display: none; }
  /* 三栏**各包一张卡片**（2026-10-12 用户要求）：外观借后台已有的那张卡
     （`.users-right` / `.backup-snap` 同一套：1px 边框 + 8px 圆角 + #fbfcfd 底）。
     卡片是**列里面**的一层（不是列本身）—— 这样列的 12px 内边距留给"卡片 ↔ 分隔条"，
     卡片自己的 10/12px 内边距留给内容，两层互不打架。 */
  .grade-panel {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    padding: 10px 12px;
    background: #fbfcfd;
    border: 1px solid var(--border);
    border-radius: 8px;
  }
  /* 分隔条：**与「用户管理」那条（.users-split-resizer）完全同一款式**（用户 2026-10-12
     指定）：可见线 2px、颜色 var(--border)、圆角 1px、居中；hover / 拖动时加粗到 3px
     并变主色。
     ⚠️ 线用 `::before` 画、**不是**背景：grid 轨道宽是写死的 2px（还牵着拖动的换算），
     靠背景"加粗"会撑动布局。::before 是绝对定位的浮层，从 2 → 3px 不占布局。
     ⚠️ 轨道宽 `2px`（上面 grid 里那两个）与 JS 的 GRADE_SPLITTER_W 必须**一起改**。 */
  .grade-splitter {
    position: relative;
    width: 2px;
    cursor: col-resize;
    background: transparent;
    align-self: stretch;
  }
  .grade-splitter::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 2px;
    border-radius: 1px;
    background: var(--border);
  }
  .grade-splitter:hover::before { width: 3px; background: var(--accent); }
  body.grade-resizing .grade-splitter::before { width: 3px; background: var(--accent); }
  body.grade-resizing { user-select: none; -webkit-user-select: none; cursor: col-resize; }
  /* 非编辑模式（2026-10-12 用户拍板）：条**整条不画**（`::before` 连生成都不生成，
     所以不留静态细线），也不收鼠标事件（悬停不出 col-resize、点了也拖不动）。
     ⚠️ **元素本身留着**（只挂这个类）：它占着栅格那一列 2px，栏宽与两栏卡片之间的
     空隙（左 6 + 条 2 + 右 6 = 14px）一个像素都不变 —— 开关编辑模式时页面不跳。
     换成 v-if / display:none 会让 5 列栅格的自动排布错位（中栏会被塞进 2px 的轨里）。 */
  .grade-splitter.is-off { cursor: default; pointer-events: none; }
  .grade-splitter.is-off::before { content: none; }
  /* 右栏**上下**再分两块（2026-10-13）：上「总表」、下「个人成绩单（占位）」。
     与左栏那条横分隔条**不是一个东西**：这条只在「展开 **且** 编辑模式」出现（收起时连
     元素都不生成 —— 竖分割不占栅格轨道，不需要像横条那样"元素留着占位"）。样式照抄横条：
     可见线 2px / var(--border) / hover 与拖动时变 3px 主色；线用 ::before 画，不撑布局。
     ⚠️ 高度 = **15px**（2026-10-14 用户要求：5 → 15），正好等于收起态那条缝，而且
     `.grade-personal.has-splitter` 会把下面那个 margin-top 让出来 —— 于是
     "收起 / 展开（编辑 / 非编辑）"三种状态下「上半底 → 下半标题顶」**逐像素都是 15px**。 */
  .grade-personal-splitter {
    position: relative;
    flex: none;
    height: 15px;
    cursor: row-resize;
    background: transparent;
  }
  .grade-personal-splitter::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    height: 2px;
    border-radius: 1px;
    background: var(--border);
  }
  .grade-personal-splitter:hover::before,
  body.grade-row-resizing .grade-personal-splitter::before {
    height: 3px;
    background: var(--accent);
  }
  body.grade-row-resizing { user-select: none; -webkit-user-select: none; cursor: row-resize; }
  /* 「个人成绩单」下半：**flex:none + 高度由 state 给**（收起 = auto，只剩标题卡；展开 = 固定 px）。
     ⚠️ 这里**不再自画一层卡**（改前是 #fbfcfd + 8px 圆角 + 10/12 padding，跟上半那张标题卡
     不是一个观感）—— 现在标题卡直接用上半那张 `.grade-grid-head`（同一批 CSS = 逐项同款），
     而且它整段**住在右栏那张卡片（`.grade-panel`）里面**（2026-10-13 用户要求：别挂到卡片外面），
     左右边界自然就跟上半同款（都是 997px）—— 所以本容器不需要任何水平 padding。
     ⚠️ 整段还在「选了班」那个 v-else 分支里面 → **没选班时根本不渲染**。 */
  .grade-personal {
    flex: none;
    display: flex;
    flex-direction: column;
    min-height: 0;
    margin-top: 15px;
  }
  /* 展开 **且** 编辑模式 → 那条缝由分隔条自己占着，这里让出 margin-top（否则翻倍）。 */
  .grade-personal.has-splitter { margin-top: 0; }
  /* 标题卡：与上半**同一批类**，只加一处 —— 相对定位，给最右那颗「展开 / 收起」当锚点
     （绝对定位 → 不挤走居中的标题）。
     ⚠️ `margin-bottom` **用 `.grade-grid-head` 自己的 6px**（2026-10-14 用户要求：
     "表格距离标题的留白 = 总表到总表标题的留白"）—— 所以下面 `.grade-personal-body`
     的 margin-top 必须是 0，否则两处相加变成 8px+（改前就是 8px，比总表多 2px）。 */
  .grade-personal-head { position: relative; }
  .grade-personal-body { flex: 1 1 auto; min-height: 0; margin: 0; color: var(--muted);
    overflow: auto; }
  /* ── 「个人成绩单」那张表（2026-10-14 落地）────────────────────────────────────
     横向：每列 = 一次测试 + 最右一列「总计」；纵向固定 6 行（名称 / 日期 / 最高·满分 /
     平均分 / <该生姓名> / 排名·总数）。
     样式**照着中栏那张成绩表来**（用户要求"与总表保持协调统一"，并给了示意图）：
       同一套灰 #f0f2f5、同一套 1px var(--border) 细线、同样的 12px 与等宽数字。
       · 标签列 = 灰底 + 600（与总表的姓名列同一档）
       · 名称 / 日期 / 最高·满分 三行是"这次测试的元信息" → **整行灰底 + 600**
         （与总表那三层表头同款，也正是示意图里那三行的样子）
       · 平均分 / 该生 / 排名 三行 = 白底数据
       · 总计列 = 灰底 + 左边一条线：**该生行用黑线**（与总表"白色成绩区到此为止"那道
         黑白分界同款，见 `.grade-grid tbody td.grade-grid-total`），其余行用 --border-strong。 */
  /* 2026-10-14 定稿的行字色：
     · 平均分 / 排名·总数 两行的**填写内容** → **黑色加粗**（与标签列同款）；
     · 该生那一行的填写内容 → **灰色**（正文色），**唯独总计列那格仍黑**；
     · 名称 / 日期 / 最高·满分 三行 → 灰底 + 黑色加粗（"这次测试的元信息"）。
     表体默认色因此改成正文色（改前继承 `.grade-personal-body` 的 --muted，
     会把平均分/排名两行也染成灰）。 */
  .gp-grid { border-collapse: separate; border-spacing: 0; font-size: 12px;
    font-variant-numeric: tabular-nums; background: #fff; color: var(--text); }
  .gp-grid th, .gp-grid td { height: 26px; font-weight: 400;
    border-right: 1px solid var(--border); border-bottom: 1px solid var(--border);
    text-align: center; white-space: nowrap; }
  /* **列宽与总表逐列一致**（2026-10-14 用户要求）—— 数值都抄自 `.grade-grid`：
       测试列 `width:64px; min-width:64px; padding:0 2px`（`td.grade-grid-cell`）；
       总计列 `width:auto; min-width:56px`（`td.grade-grid-total.grade-grid-cell`）；
       标签列 `min-width: calc(4em + 13px)` + `padding: 0 6px`（`.grade-grid-namecol`）。 */
  .gp-grid td { width: 64px; min-width: 64px; padding: 0 2px; }
  .gp-grid td.gp-total { width: auto; min-width: 56px; }
  .gp-grid tr:first-child th, .gp-grid tr:first-child td { border-top: 1px solid var(--border); }
  .gp-grid th:first-child { border-left: 1px solid var(--border); }
  /* 首列（标签列）的表头**居中**（2026-10-14 用户要求）—— 与总表姓名列同款
     （`.grade-grid .grade-grid-namecol { text-align: center }`）。 */
  .gp-grid th { text-align: center; font-weight: 600; color: #1f2430; background: #f0f2f5;
    position: sticky; left: 0; z-index: 1; padding: 0 2px; min-width: calc(4em + 13px); }
  .gp-grid tr.gp-meta td { background: #f0f2f5; font-weight: 600; color: #1f2430; }
  .gp-grid td.gp-total { background: #f0f2f5; font-weight: 600; color: var(--text);
    box-shadow: -1px 0 0 var(--border-strong); }
  .gp-grid tr.gp-stu td.gp-total { box-shadow: -1px 0 0 var(--text); }
  /* 平均分 / 排名·总数 两行：**整行同款灰底**（2026-10-14 用户要求）+ 黑色加粗 ——
     与总表那边"平均分行也是 #f0f2f5"同一档。该生那一行仍是白底（见下一条）。 */
  .gp-grid tr.gp-avg td, .gp-grid tr.gp-rank td { color: #1f2430; font-weight: 600;
    background: #f0f2f5; }
  /* 该生那一行的成绩 → **黑字、字重仍是 400**（2026-10-14 用户要求；改前是 --muted 灰）。 */
  .gp-grid tr.gp-stu td { color: var(--text); font-weight: 400; }
  /* 该生那一行是表里**唯一白底**的部分 → 给它描一圈**黑边**（2026-10-14 用户要求）——
     与总表"白色成绩区四周黑"同一套。四边各自落在"该由谁画"的那一格上：
       · 左：该生**姓名格**（标签列的 th）的**右边沿** → 黑（用户明确点名这一条）；
       · 上 / 下：该生行的**测试格**上、下边沿 → 黑（1px，后画的压住邻行的灰线）；
       · 右：总计格原有的**黑左线**（`.gp-grid tr.gp-stu td.gp-total`）。
     ⚠️ **总计列那一格的上/下边沿仍是灰**（下面那条）：它不属于白色成绩区，
        黑框到总计列的左沿为止 —— 与总表那张图里"黑线只围住白色区"是同一个样子。 */
  .gp-grid tr.gp-stu th { border-right: 1px solid var(--text); }
  .gp-grid tr.gp-stu td { border-top: 1px solid var(--text);
    border-bottom: 1px solid var(--text); }
  .gp-grid tr.gp-stu td.gp-total { border-top: 1px solid var(--border);
    border-bottom: 1px solid var(--border); }
  .gp-grid tr.gp-stu td.gp-total { color: var(--text); font-weight: 600; }
  .gp-grid td.gp-absent { color: var(--muted); font-weight: 400; }
  /* ⚠️ 上面那条 (0,2,1) **压不住**该生那一行的正文黑 ——
     `.gp-grid tr.gp-stu td` 是 (0,2,2)（2026-10-14 用户把成绩信息统一改成黑字时加的），
     于是个人表里「缺考」跟真分数一样黑、根本分不出来（用户截图实测）。
     这条带上 `tr.gp-stu`（(0,3,2)）才盖得住 —— 分数格黑、缺考灰，两者并存。 */
  .gp-grid tr.gp-stu td.gp-absent { color: var(--muted); font-weight: 400; }
  .gp-empty { font-size: 12px; color: var(--muted); padding: 6px 2px; }

  /* ── 跨班搜人弹窗 + 全览（2026-10-14；第三轮改**固定尺寸 + 左右两栏**）──────────
     弹窗**固定尺寸**（不随内容变）：宽 min(1000px, 92vw)、高 76vh —— 内容再多再宽，
     滚动只发生在左栏（结果列表）或右栏（成绩单）里，弹窗本身永远不撑破屏幕。
     ⚠️ 宽/高这两条必须带 `.modal.grade-modal` 前缀（0,3,0）：光写 `.gpw-modal`（0,1,0）
     会被上面 `.modal.grade-modal { width: 383px }`（0,2,0）压住。 */
  /* 高度**按"能完整放下两张个人成绩单 + 余量"定**（2026-10-14 用户要求）：
     一张单实测 209.3px（窄单）/ 224.3px（带横向滚动条时），两张 + 中间的 14px 间距
     = 462.6px；右栏再留 ~22px 余量 → 右栏可视高 485px。
     弹窗其它部分（标题 + 关闭行 + 上下内边距）实测吃掉 105px → 485 + 105 = **590px**。
     `max-height: 86vh` 是防矮窗口的兜底（那时右栏自己滚，见上面的栏内滚动）。 */
  .modal.grade-modal.gpw-modal {
    width: min(1000px, 92vw); height: 590px; max-height: 86vh;
    display: flex; flex-direction: column;
  }
  /* 整层遮罩：**fixed 铺满视口 + 卡片居中**（用户 2026-10-14："弹窗居中，不要放在 🔍 下"）。
     ⚠️ 上一版重写这段 CSS 时误删了这条规则 → 弹窗掉进文档流、就贴在左栏那颗 🔍 底下
     （实测中心 (537,583) vs 视口中心 (720,500)）。它必须跟 `.gpw-modal` 的尺寸规则成对存在。 */
  .gpw-layer { position: fixed; inset: 0; z-index: 78;
    display: flex; align-items: center; justify-content: center;
    background: rgba(20, 24, 32, 0.28); }
  /* 左右两栏 */
  .gpw-split { flex: 1 1 auto; min-height: 0; display: flex; align-items: stretch; }
  /* 左栏宽度**按内容算**（不写死像素）：
       4em  = 4 个汉字的姓名（字号 12.5px，1em = 12.5px）
       16ch = 一个完整电话（15 位数字 + 余量；表格数字宽度 ≈ 0.6em）
       44px = 结果卡左右内边距(20) + 列表边框(2) + 滚动条(12) + 余量(10)
     实测 214px：正好放得下「王馨舒雅 15936263862」不裁切，见 test_frontend_template。 */
  .gpw-left { flex: none; width: calc(4em + 16ch + 44px); min-width: 0;
    display: flex; flex-direction: column; }
  /* ⚠️ 宽度留 12px：**10px 自绘滚动条 + 2px 列表边框** —— 结果卡与搜索框的右边界
     要落在同一条竖线上（用户 2026-10-14 要求"搜索框与结果卡左右对齐"）。
     不这么写的话：系统滚动条 16px 且**只在需要时出现** → 有滚动条时卡片比搜索框窄 16px
     （实测 280 vs 296），有没有结果还会左右跳一下。 */
  /* ⚠️ 口径（用户 2026-10-15 明确）：搜索框的**右边界**要和**"内容 + 滚动条预留位"的
     右边界**对齐 —— 也就是列表**内容盒**的右沿（红虚线那条），不是结果卡的右沿。
     所以：`margin-left: 1px` 让过列表那 1px 边框、`width: calc(100% - 2px)` 正好到
     内容盒右沿（列表 border-box 宽 − 左右各 1px 边框）。
     结果卡本身只有"内容"那一段宽（= 搜索框 − 10px 滚动条预留位），这是对的。 */
  .gpw-input { flex: none; margin-left: 1px; width: calc(100% - 2px); box-sizing: border-box;
    padding: 5px 10px; font-size: 13px;
    border: 1px solid var(--border-strong); border-radius: 8px; outline: none; }
  .gpw-input:focus { border-color: var(--accent); }
  /* 结果列表：**只有这一层滚**（结果多了出滚动条，滚动条在左栏里、不在弹窗上）。
     滚动条**固定 10px 自绘**（不是系统那条）：宽度可预期，配合右边那条留白 →
     "有没有滚动条"卡片宽度都不变、也不跟搜索框错位。 */
  .gpw-list { flex: 1 1 auto; min-height: 0; margin-top: 8px;
    overflow-y: auto; overflow-x: hidden;
    border: 1px solid var(--border); border-radius: 8px;
    scrollbar-gutter: stable; display: block; }
  .gpw-list::-webkit-scrollbar { width: 10px; }
  .gpw-list::-webkit-scrollbar-track { background: transparent; }
  .gpw-list::-webkit-scrollbar-thumb { background: #cfd6e0; border-radius: 5px; }
  .gpw-list::-webkit-scrollbar-thumb:hover { background: #b6bfcd; }
  .gpw-cand { display: flex; align-items: center; gap: 10px;
    padding: 5px 10px; font-size: 12.5px; cursor: pointer;
    border-bottom: 1px solid #f0f2f5; }
  .gpw-cand:last-child { border-bottom: none; }
  .gpw-cand:hover { background: #f7f9fc; }
  .gpw-cand.on { background: var(--accent-soft); }
  .gpw-cand-name { flex: none; font-weight: 600; color: #2c3444; white-space: nowrap; }
  .gpw-cand-phone { flex: 1 1 auto; min-width: 0; color: var(--muted);
    font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden;
    text-overflow: ellipsis; }
  /* 中间一条**虚线**分隔左右两个区域（1px 宽 + 两侧留白 14px） */
  .gpw-divider { flex: none; width: 1px; margin: 0 14px;
    border-left: 1px dashed var(--border-strong); }
  /* 右栏：成绩单（自己那层滚，与左栏互不干扰） */
  /* ⚠️ `overflow-y: **scroll**`（不是 auto，2026-10-15 改）：纵向滚动条**永远占着它的位置**，
     右栏内容宽因此恒定 —— 虚线的右端（`right: 0`，= 成绩单卡片右沿）不会因为"这次内容
     溢不溢出"而左右跳。原来用 `auto` + `scrollbar-gutter: stable`，但 `scrollbar-gutter`
     在较老的 Safari / overlay 滚动条平台会被忽略 → 两个学生（一个 2 张单、一个 3 张单）
     的虚线长度就不一样（用户实测）。
     滚动条本身**保持原生**（用户："把滚动条还原，太丑了"）：macOS 上它平时是浮层、
     不占视觉位置；经典模式下也多出一条空轨道，但换来的是"任何时候都一样长"。
     ⚠️ 左栏结果列表 `.gpw-list` 那条自绘 10px 滚动条**不动**（那是用户点名要的样式）。 */
  .gpw-right { flex: 1 1 auto; min-width: 0; min-height: 0;
    overflow-y: scroll; overflow-x: hidden; }
  /* 未选学生时的空态（用户 2026-10-14 拍板：右栏给一句提示） */
  .gpw-empty { margin: 0; padding: 24px 8px; text-align: center;
    font-size: 12.5px; color: var(--muted); }
  /* 候选 hover 气泡：**fixed**（不占布局、不会被栏内滚动区裁掉） */
  .gpw-tip { position: fixed; transform: translateY(-100%); z-index: 92;
    display: flex; flex-direction: column; gap: 1px; padding: 5px 9px;
    background: rgba(31, 36, 48, 0.94); color: #fff; font-size: 11px; line-height: 1.5;
    border-radius: 6px; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.2);
    pointer-events: none; white-space: nowrap; }
  /* 翻到行**下方**时不翻上去（坐标已经是"行的下沿"） */
  .gpw-tip.below { transform: none; }
  .gpw-hint { margin: 6px 10px; font-size: 12px; color: var(--muted); }
  /* 每班一张单：标题在滚动容器**外面**（不跟横滚）；容器自己横滚；左标签列 sticky */
  /* **每张**个人成绩单的标题行上方一条水平虚线（2026-10-15 用户要求，与左右栏那条竖虚线同款
     `1px dashed var(--border-strong)`）：
       · 形状：**独立伪元素**、`left: 0; right: 0` → **长度 = 成绩单卡片的宽**
         （= 右栏内容盒宽 = 上面那张表的可见宽度；用户 2026-10-15："跟成绩单卡片同样长"）。
         原先它是这个块的 `border-top`，宽度跟着右栏内容盒走 —— 右栏一出现纵向滚动条，
         内容盒窄 15px，虚线右端就左移并**正好贴在滚动条左沿**（实测间距 0px）。
       · `padding-top: 5px` = 原来那 1px 边框 + 4px 内边距 ⇒ 虚线到标题行的留白没变，
         仍然等于"标题行到表格"的留白（实测两边都是 6px）。
       · 与上面那张表的间距 = `.gpw-report + .gpw-report` 的 20px（见下面那条）。 */
  .gpw-report { position: relative; padding-top: 5px; }
  .gpw-report::before { content: ""; position: absolute; left: 0; right: 0; top: 0;
    border-top: 1px dashed var(--border-strong); }
  /* 相邻两张单之间的留白：8 → 13px（用户 2026-10-15 "+5px"）→ **20px**（同日再 +7px）——
     给表格那条横向滚动条留出一档肉眼可辨的空隙（13px 时实机看仍然"虚线紧贴滚动条"）。
     ⚠️ 只动这一条：虚线两侧那对留白（虚线↔全览按钮 / 按钮↔表格）是另一条要求钉死的
     （两边必须相等，实测都是 6px），改 `padding-top` 会把它破坏掉。 */
  .gpw-report + .gpw-report { margin-top: 20px; }
  /* ⚠️ 与表格那 4px 用 **margin** 给、不用 padding-bottom：用 padding 的话行盒的
     "几何中心"比内容中心低 2px，右边那颗「⤢ 全览」就不在垂直中线上（实测偏差 2px）。 */
  .gpw-report-head { display: flex; align-items: center; gap: 8px; padding: 0 2px;
    margin: 0 0 4px; min-height: 24px;
    font-size: 12.5px; font-weight: 600; color: #1f2430; }
  .gpw-report-tag { font-size: 10.5px; font-weight: 600; color: #6b7686;
    background: #eef1f6; border-radius: 8px; padding: 0 6px; }
  .gpw-scroll { overflow-x: auto; overflow-y: hidden;
    border: 1px solid var(--border); border-radius: 6px; }
  /* 表格的滚动条**保持原生**（用户 2026-10-15："把滚动条还原，太丑了"）—— 别再自绘
     `::-webkit-scrollbar`（那会把滚动条变成"占位型"，还会跟下面那条 Safari 的坑撞上）。
     ⚠️ **已知的 Safari 专属毛病（2026-10-15 用 Playwright 的 WebKit 实测复现，未修）**：
        当这张表出现**占位型**横向滚动条（系统"总是显示滚动条"，或任何自绘
        `::-webkit-scrollbar`）时，WebKit 把滚动条的高度**加在表格盒子自己身上**，
        却**不告诉父元素**重新排版 —— 实测：卡片高 191px（按 158px 的表格算的）、
        表格盒子 173px，盒子因此**捅出卡片底边 15px**；下一张单的虚线只有
        **5px** 在盒子可见底边之下（`.gpw-report + .gpw-report` 的 20px 被吃掉 15px）。
        同一个班的表若不需要横滚 → 那一档留白又是完整的 20px → 看起来"两个人不一样"。
        Chrome 没这个问题（它会把滚动条高度算进父元素），所以只在 Safari 上看得见。
        实测可行的修法（用户 2026-10-15 拍板**先不做**："Chrome 正常即可"）：
        改成 `.gpw-scroll { overflow-x: scroll }` —— 横向滚动条**恒定占位**，
        WebKit 首次排版就把那 15px 算进去 → 三张单的留白恒为 20px（实测 5 → 20）。
        代价：窄表（如"2026年暑期…"那种 3 列）底下会多出一条空滚动条轨道。 */
  /* 「⤢ 全览」跟着**标题行**走（2026-10-14 第三轮）：放进 `.gpw-report-head` 的最右端
     （`margin-left: auto`），同一行、垂直居中 —— 表格右下角那颗已删，不留两份。 */
  .gpw-report-head .gpw-zoom { margin-left: auto; }
  .gpw-zoom { border: 1px solid var(--border-strong); background: #fff; border-radius: 6px;
    padding: 2px 8px; font-size: 11.5px; color: #4a5564; cursor: pointer; }
  .gpw-zoom:hover { color: var(--accent); border-color: var(--accent); }
  /* 全览：整屏白底、只留班名标题 —— 方便直接框选截图（不滚、不横滚） */
  .gpw-zoom-layer { position: fixed; inset: 0; z-index: 90; background: #fff;
    display: flex; align-items: center; justify-content: center; padding: 16px 24px; }
  /* `width: fit-content` → 整块宽度 = **max(表格宽, 标题卡宽)**：表格更宽时卡片拉满到表格宽，
     标题更长时改由标题撑开（两种都符合用户口径）。 */
  .gpw-zoom-inner { max-width: 100%; max-height: 100%; overflow: hidden;
    display: flex; flex-direction: column; width: fit-content; }
  /* 标题卡：**逐项照抄**中栏那张（`.grade-grid-head` ＋ `.grade-grid-title` 的 computed，实测：
     背景 rgb(233,235,238)、边框 1px rgb(203,210,220)、圆角 6px、padding 6px 8px、
     标题 17px / 700 / rgb(31,36,48) / 居中、卡↔表格 6px）。 */
  .gpw-zoom-head { flex: none; box-sizing: border-box; background: #e9ebee;
    border: 1px solid var(--border-strong); border-radius: 6px;
    padding: 6px 8px; margin-bottom: 6px;
    /* 卡高也照中栏：`min-height: 38px` + 内容居中（中栏那张卡实测 38px 高）。 */
    display: flex; align-items: center; justify-content: center; min-height: 38px; }
  .gpw-zoom-title { margin: 0; font-size: 17px; font-weight: 700; color: #1f2430;
    line-height: 1.3; letter-spacing: 0.5px; text-align: center; white-space: nowrap; }
  /* 全览那张表：列宽全部按 **em** 给 → 字号一缩，整张表跟着缩（一屏排下所有列） */
  .gp-zoom { margin: 0 auto; }
  .gp-zoom th, .gp-zoom td { height: 1.9em; }
  .gp-zoom td { width: 5.4em; min-width: 5.4em; padding: 0 0.15em; }
  .gp-zoom td.gp-total { width: auto; min-width: 4.4em; }
  .gp-zoom th { min-width: 5.1em; padding: 0 0.15em; }
  /* ── 最右栏「成绩分析」三态标题（2026-10-14 定稿 = 设计稿 **C2**）─────────────
     从"灰底标题卡"改成**轻量页眉**：班级名一行浅灰小字 + 状态行（强调色竖条 + 状态词 +
     右侧**对象胶囊** + 清除键）×，底下一根细线收口。不加背景，省纵向空间、也更轻。
     三态的差别全在**胶囊里的对象**和**状态色**上：
       · 整表 → 「整表分析」（黑）+ 胶囊「6 次测试 · 30 人」（浅蓝）
       · 测试 → 「测试分析」（蓝）+ 胶囊「课前测2 · 26/10/12」（浅蓝）
       · 学生 → 「学生分析」（绿）+ 胶囊「杜龙翰 · 第 4 名」（浅绿）
     ⚠️ 对象做成**浅底 + 加粗**的胶囊，就是为了"哪一次 / 哪个人"一眼看得出来（用户要求）。 */
  .ga-head { flex: none; padding: 0 2px 7px; border-bottom: 1px solid var(--border);
    margin-bottom: 8px; }
  .ga-head .ga-cls { font-size: 11px; color: #8b939f; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  .ga-head .ga-row { display: flex; align-items: center; gap: 8px; margin-top: 3px; }
  .ga-head .ga-bar { flex: none; width: 3px; height: 15px; border-radius: 2px;
    background: var(--accent); }
  .ga-head .ga-bar.s { background: var(--a-color); }
  .ga-head .ga-state { font-size: 14px; font-weight: 700; letter-spacing: .3px;
    white-space: nowrap; }
  .ga-head .ga-state.t { color: var(--accent); }
  .ga-head .ga-state.s { color: var(--a-color); }
  .ga-head .ga-pill { flex: none; margin-left: auto; display: block; max-width: 230px;
    padding: 3px 10px; border-radius: 999px; background: var(--accent-soft);
    color: var(--accent); font-size: 12px; font-weight: 700;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .ga-head .ga-pill em { font-style: normal; font-weight: 400; opacity: .75; }
  /* 浅绿底取站内已有的那一档（`.journal-notice.ok` 用的就是 #e9f7ee） */
  .ga-head .ga-pill.s { background: #e9f7ee; color: var(--a-color); }
  .ga-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding-right: 2px;
  }
  .ga-card { flex: none; background: #fff; border: 1px solid var(--border);
    border-radius: 8px; padding: 10px 12px; }
  .ga-cap { display: flex; align-items: center; gap: 6px; margin-bottom: 8px;
    font-size: 11.5px; font-weight: 600; color: #5b6472; }
  .ga-cap i { width: 3px; height: 11px; border-radius: 2px; background: var(--accent);
    flex: none; }
  /* 图表「显示数据」勾选框（2026-10-14 用户要求）—— 挂在**卡片标题行最右端**：
     `margin-left:auto` 把它推到卡片右沿（与标题同一水平线，不是紧贴标题文字）。
     样式与总表标题行那两个勾选框同一套（12px / 600、文案↔框 7px）。
     默认不勾 → 图表只画图、不标数值；y 轴刻度**属于坐标系**，不受它管。 */
  .ga-cap-check { margin-left: auto; display: inline-flex; align-items: center; gap: 6px;
    /* 字号 / 颜色 / 字重**照左边那行标题**（.ga-cap = 11.5px / #5b6472 / 600）——
       用户 2026-10-14：框和文案比标题大一号，看着不协调。 */
    font-size: 11.5px; font-weight: 600; color: #5b6472; white-space: nowrap; cursor: pointer; }
  /* 框也按同一比例收：13 → 12px（跟 11.5px 的标题一个视觉体量） */
  .ga-cap-check input { width: 12px; height: 12px; margin: 0; cursor: pointer;
    accent-color: var(--accent); }
  /* ① 概览条：5 个数字 */
  .ga-kpis { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px;
    text-align: center; }
  .ga-kpi b { display: block; font-size: 15px; font-weight: 700; letter-spacing: -.2px;
    font-variant-numeric: tabular-nums; }
  .ga-kpi small { display: block; margin-top: 2px; font-size: 10px; color: var(--muted);
    white-space: nowrap; }
  .ga-kpi.ok b { color: var(--a-color); }
  .ga-kpi.warn b { color: var(--c-color); }
  /* 轮3 概览只有 4 个数（平均得分率 / 最好 / 最差 / 缺考） */
  .ga-kpis.four { grid-template-columns: repeat(4, 1fr); }
  /* ② 分数段分布：CSS 横条 */
  .ga-seg { display: grid; grid-template-columns: 68px 1fr 44px; align-items: center;
    gap: 8px; margin-bottom: 6px; }
  .ga-seg:last-child { margin-bottom: 0; }
  .ga-seg .lb { font-size: 11.5px; color: #4b5563; white-space: nowrap; }
  .ga-seg .track { display: block; height: 10px; border-radius: 5px; background: #eef1f5;
    overflow: hidden; }
  .ga-seg .fill { display: block; height: 100%; border-radius: 5px;
    background: var(--accent); transition: width .18s ease; }
  .ga-seg.l1 .fill { background: #1a7f37; }
  .ga-seg.l2 .fill { background: #4b9c5f; }
  .ga-seg.l3 .fill { background: var(--accent); }
  .ga-seg.l4 .fill { background: #c9584f; }
  .ga-seg .n { font-size: 11.5px; color: var(--muted); text-align: right;
    font-variant-numeric: tabular-nums; white-space: nowrap; }
  /* ③ 趋势折线：**手写 SVG**（站内无任何图表先例 / 零依赖） */
  .ga-trend { display: block; width: 100%; height: 118px; }
  .ga-trend .axis { stroke: #eef1f5; stroke-width: 1; }
  .ga-trend .line { fill: none; stroke: var(--accent); stroke-width: 2;
    stroke-linejoin: round; stroke-linecap: round; }
  .ga-trend .dot { fill: #fff; stroke: var(--accent); stroke-width: 2; }
  .ga-trend .dot-last { fill: var(--accent); stroke: #fff; stroke-width: 2; }
  .ga-trend .xlab { font-size: 9.5px; fill: #8b939f; }
  .ga-trend .ylab { font-size: 9.5px; fill: #aab1bb; }
  .ga-trend .vlab { font-size: 10px; fill: var(--accent); font-weight: 700; }
  /* x 轴角那句小字「测试次序」（2026-10-14）：刻度改成裸序号之后，
     得有一句交代这串 1 2 3… 是什么 —— 挂在**左下角**（最后一枚 y 刻度文字的下方，
     与第一个序号之间还留着 16px，谁也压不着谁）。 */
  .ga-trend .xcap { font-size: 9px; fill: #aab1bb; }
  /* hover 热区：透明、比圆点大一圈，专收鼠标（不画） */
  .ga-trend .ga-hit { fill: transparent; cursor: crosshair; }
  /* 折线的壳：气泡挂它里面（相对定位），坐标直接用 SVG 那套 px */
  .ga-trend-wrap { position: relative; }
  .ga-tip { position: absolute; z-index: 6; transform: translate(-50%, -100%);
    background: rgba(31, 36, 48, 0.94); color: #fff; font-size: 10px; line-height: 1.4;
    padding: 3px 7px; border-radius: 5px; white-space: nowrap; pointer-events: none;
    box-shadow: 0 2px 6px rgba(15, 23, 42, 0.18); }
  /* 贴顶的点（整表趋势 / 名次的第 1 名）气泡改挂点的**下方**，免得顶出卡片 */
  .ga-tip.below { transform: translate(-50%, 0); }
  /* ④ 总分排行：前 5 / 后 5 两列 */
  .ga-rank { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 8px 16px; }
  /* 两榜之间拉一条**竖虚线**、并把两栏空隙从 10 → 16px：一眼看成"左一块 / 右一块"，
     而不是连成一张表（2026-10-14 用户要求；总分排行与轮2 的进步/退步榜**同款**）。 */
  .ga-rank > div + div { border-left: 1px dashed #dfe3e8; padding-left: 16px; }
  .ga-rank h5 { margin: 0 0 6px; font-size: 11px; font-weight: 600; color: #5b6472; }
  .ga-rank ol { margin: 0; padding: 0; list-style: none; }
  .ga-rank li { display: flex; align-items: center; gap: 6px; padding: 3px 0;
    border-top: 1px dashed #e6e9ee; font-size: 11.5px; }
  .ga-rank li:first-child { border-top: 0; }
  .ga-rank .no { flex: none; width: 14px; text-align: right; color: #8b939f;
    font-variant-numeric: tabular-nums; }
  .ga-rank .nm { flex: 1 1 auto; min-width: 0; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  .ga-rank .vl { flex: none; color: var(--muted); font-variant-numeric: tabular-nums; }
  .ga-rank .up .no, .ga-rank .up .vl { color: var(--a-color); font-weight: 600; }
  .ga-rank .down .no, .ga-rank .down .vl { color: var(--c-color); font-weight: 600; }
  /* 轮2 的名次变化榜：第一格是「上次→这次」（如 5→3），比整表那两个要宽。
     ⚠️ **左对齐**（2026-10-14 用户要求）：右对齐时 "24→1" 与 "29→17" 宽度不同，
     每行的起点会左右飘 → 后面的**姓名对不齐**；左对齐 + 定宽（46px，最宽的 "29→17" 也放得下）
     之后，这一格永远是同一段位置，姓名自然竖直对齐。 */
  .ga-rank.move .no { width: 46px; text-align: left; }
  /* 轮2「与上一次对比」：一行 = 指标 / 上次 → 这次 / 涨跌 */
  .ga-vs-row { display: grid; grid-template-columns: 62px 1fr 12px 1fr auto;
    align-items: center; gap: 6px; padding: 5px 0; border-top: 1px dashed #e6e9ee; }
  .ga-vs-row:first-child { border-top: 0; }
  .ga-vs-row .k { font-size: 11.5px; color: #4b5563; }
  .ga-vs-row .from { font-size: 12px; color: var(--muted); text-align: right;
    font-variant-numeric: tabular-nums; }
  .ga-vs-row .arrow { font-size: 11px; color: #b3b9c2; text-align: center; }
  .ga-vs-row .to { font-size: 12.5px; font-weight: 700; font-variant-numeric: tabular-nums; }
  .ga-vs-row .d { min-width: 52px; text-align: right; font-size: 11.5px; font-weight: 700;
    font-variant-numeric: tabular-nums; }
  .ga-vs-row .d.up { color: var(--a-color); }
  .ga-vs-row .d.down { color: var(--c-color); }
  .ga-vs-row .d.flat { color: var(--muted); font-weight: 400; }
  .ga-note { margin: 6px 0 0; font-size: 11px; color: #a3aab4; }
  .ga-note:first-child { margin-top: 0; }
  /* 轮3「他 vs 班级平均」：**固定高**柱状图（2026-10-14 用户要求，改前是逐行差值条）。
     零线居中、正差向上（绿）负差向下（红）；高度**钉死 130px** —— N=12 与 N=30 一样高，
     这张卡不再随测试次数越长越长。缺考那次不画柱（留空），日期 / 数值挪进 hover。 */
  .ga-diffchart { height: 180px; }
  .ga-diffchart .zero { stroke: #d7dde5; }
  .ga-diffchart .bar.up { fill: var(--a-color); }
  .ga-diffchart .bar.down { fill: var(--c-color); }
  /* 柱值（勾「显示数据」才画）：颜色跟柱子走 —— 正差绿、负差红 */
  .ga-diffchart .dval { font-size: 10px; font-weight: 700; }
  .ga-diffchart .dval.up { fill: var(--a-color); }
  .ga-diffchart .dval.down { fill: var(--c-color); }
  .ga-empty { color: var(--muted); font-size: 12px; line-height: 1.7; }
  /* 窄屏（2026-10-13 轮1）：三栏最小宽 220+2+420+2+360 = **1004** —— 窗口比它窄就
     **上下堆叠**（照「用户管理」@1190 那条先例），不再横向滚动。
     三行按 0.8 / 1.4 / 1.2 分高度：中间的横表最吃高度，给它最多。 */
  @media (max-width: 1004px) {
    .grade-split {
      grid-template-columns: minmax(0, 1fr);
      grid-template-rows: minmax(0, 0.8fr) minmax(0, 1.4fr) minmax(0, 1.2fr);
      overflow-x: hidden;
    }
    .grade-splitter { display: none; }
    .grade-left { padding-right: 0; padding-bottom: 6px; }
    .grade-right { padding-left: 0; padding-bottom: 6px; }
    .grade-analysis { padding-left: 0; }
  }
  /* 左栏顶行（2026-10-12 改）：**原来那个「班级」标题去掉了**，两个子 tab 挪到标题位
     （就是这一行），右边依次是「新建」（只在编辑态）与**永远最右**的齿轮。
     margin-right:auto 把子 tab 之外的空间推开 → 两颗按钮贴右。 */
  .grade-bar { display: flex; align-items: center; gap: 8px; flex: none; margin-bottom: 8px; }
  /* 「在授 / 结课」两个子 tab + 紧跟其后的 🔍 组成左边一组（`.grade-tabs-row`），
     整组吃掉"靠右"的那份留白 —— 这样 🔍 贴着胶囊组右边，而齿轮仍被推到最右
     （2026-10-14 用户要求："放大镜放在「结课」右侧、不要放进灰色块里"）。
     ⚠️ 编辑模式下 🔍 不画、组里只剩胶囊 → 这一组的 auto 右留白照旧，
        后面的「新建班级 / 齿轮」位置与改前一模一样（那颗齿轮永远最右）。 */
  .grade-subtabs { margin-right: 0; }
  .grade-tabs-row { display: flex; align-items: center; gap: 8px; margin-right: auto; }
  /* 「新建班级」（2026-10-12 用户要求）：蓝字 + 加粗 —— 与真题卷集那颗「新建试卷」
     （`.ds-btn.ds-newpaper`）同一套写法（`.ds-btn` 自己写了 font-weight:400，
     所以必须带 `.ds-btn` 前缀、并且写在它后面才盖得住）。 */
  .ds-btn.grade-newbtn { color: var(--accent); font-weight: 600; }
  /* 「新建班级」那一层的**定位**（2026-10-12：原来那个表单是贴着左栏弹的，现在整页居中）。
     ⚠️ 这一层**不是** `.modal-mask`（那个是全屏遮罩）：用户报"所有可拖动分隔条
       都不显示左右可拉的光标了" —— 根因是遮罩盖住了整页，鼠标悬到哪条分隔条上
       命中的都是遮罩（它是 cursor:auto）。所以这里 `pointer-events: none`，
       只有卡片自己 auto：卡片照旧居中、照旧能点，**底下的分隔条照旧能悬停 / 拖动**。
     z-index 70 = 在面板与右键菜单（60）之上、二次确认（80）之下。 */
  /* 两个"整页居中的卡片层"共用这一条（.grade-modal-layer = 班级管理那两张、
     .app-dialog-layer = 通用询问弹窗 AppDialog）。**都不是 .modal-mask**：
     全屏遮罩会盖住所有分隔条、让它们悬停不出 col-resize（2026-10-12 踩过）。 */
  .grade-modal-layer,
  .app-dialog-layer {
    position: fixed;
    inset: 0;
    z-index: 70;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
  }
  .grade-modal-layer > .grade-modal { pointer-events: auto; }
  .app-dialog-layer > .app-dialog { pointer-events: auto; }
  /* 「弹窗**之上**的弹窗」（2026-10-12 用户报：花名册里右键 →「学生详情」被花名册盖住）。
     两者都是 `.grade-modal-layer`（z-index 70），而花名册在 DOM 里更靠后 —— 同层就按
     DOM 序画，先写的那个被压在下面。**不靠 DOM 顺序，显式抬层级**（与站内那条老规矩
     一致：见上面 `.modal-mask.confirm-mask` 的注释 —— 当初也踩过"被面板盖住"）。
     76：高于普通弹层（70）与学生右键菜单（75），低于二次确认那档（80）。 */
  .grade-modal-layer.grade-layer-top { z-index: 76; }
  /* 通用询问弹窗（AppDialog）里的：单列表单 + 全宽输入框 + 那句说明。
     卡片本体沿用 .modal.grade-modal（383px、圆角、阴影、蓝粗标题、12px 按钮上留白）。 */
  /* ⚠️ 选择器必须带 .grade-modal（= `.grade-modal.app-dialog`，特异性 0,3,0）：
     下面 `.grade-modal .grade-form`（0,2,0）那条**写在文件更后面**，同级会被它盖掉 ——
     实测不这么写的话，AppDialog 会套上「新建班级」那套 3 个 auto 列，单字段只占第 1 列，
     输入框右边界比「确定」按钮少 **131px**。 */
  .grade-modal.app-dialog .grade-form { grid-template-columns: minmax(0, 1fr); }
  /* 标题上下留白（用户 2026-10-12 定稿）：**上方 16px、下方 12px**
     （上方就是 .modal 的 padding-top 16）；下方 = 标题自己的 margin-bottom 12。
     原来下方是 标题 margin 12 + 字段自己的 margin-top 10 = **22px** ——
     那条 margin-top 是给「新建班级」里"第一行 → 备注"那一行用的，AppDialog 用不上。 */
  .grade-modal.app-dialog .grade-modal-title { margin-bottom: 12px; }
  .grade-modal.app-dialog .grade-form .grade-field-wide { margin-top: 0; }
  .app-dialog .app-dialog-message {
    margin: 0 0 14px;
    font-size: 13.5px;
    white-space: pre-line;
  }
  .app-dialog .grade-field-wide input { width: 100%; box-sizing: border-box; }
  /* 卡片宽度**按那排字段算出来**，让左右留白一样宽（用户 2026-10-12："右边留白太多"）：
       名字框 11em+18px = 143+18 = 161 ｜ 期别 74 ｜ 年份 92 ｜ 两条列间距各 10
       合计 347，再加 .modal 自己的左右内边距各 18 → **383**。
     改上面任何一个宽度，这个数要跟着改（tests/test_frontend_template.py 里有一条
     把这笔加法算了一遍的断言，改漏了会红）。 */
  .modal.grade-modal { width: 383px; }
  /* 卡片弹窗的**统一标题**（第 2 批 2026-10-12）：一个共享类 `.modal-title`，
     外加两个历史类名做**别名**（`.grade-modal-title` = 班级管理那两张卡、
     `.mp-title` = 我的待审）—— 三处的观感值逐字相同（accent / 15px / 700），
     过去分散在两条规则里各写一遍；现在只在这一条里定义，谁都不许再抄第三份。
     `.mp-title` 只额外保留自己那一档下边距（10px，那个弹窗比别的紧）。 */
  /* ⚠️ 选择器必须带 `.modal` 前缀（特异性 0,2,0）——下面 `.modal p { font-size: 13.5px }`
     是 (0,1,1)，光写 `.modal-title` (0,1,0) 会被它按字号/上下边距一起盖掉
     （实测：字号退成 13.5px、下边距变成 14px）。带前缀才压得住。 */
  .modal .modal-title,
  .modal .grade-modal-title,
  .modal .mp-title {
    margin: 0 0 12px;
    color: var(--accent);
    font-size: 15px;
    font-weight: 700;
  }
  /* 弹窗本身就是白底 → 把 .grade-form 那份"卡片感"（边框 / 底色 / 圆角 / 外边距）去掉，
     只留字段的排布。 */
  .grade-modal .grade-form { margin: 0; padding: 0; border: 0; border-radius: 0;
    background: transparent; }
  /* 2026-10-12 用户要求（三条一起满足）：
     · **班级名字 / 期别 / 年份 挤在同一行**，备注另起一行；
     · **两行等长** —— 备注框的右边要和上面那行的右边对齐。用 grid 的 auto 列最省事：
       三列各由内容决定宽度，备注 `grid-column: 1 / -1` 跨满三列 → 它的宽度**自动等于
       上面三个框 + 两条间隙**，不用手算魔法数（也就不会写着写着又对不上）。
     · **三个框等高、三个标签齐平** —— input 与 select 的原生高度天生差 2px
       （实测 25 vs 27，标签跟着错开 2px），统一写死同一个 height 就齐了。
     · 名字框：刚好显示 **11 个汉字** —— 输入框字号 13px，一个汉字正好 1em，所以内容区
       取 11em；全局 border-box，再把左右 padding（8+8）与 2px 边框补回去。
     · 期别：只求"选项显示完整"。四个选项都是两个字，`width:auto` 就是最宽选项 + 原生箭头。
     · 年份：留出右侧拨轮的位置。 */
  .grade-modal .grade-form {
    display: grid;
    grid-template-columns: auto auto auto;
    /* ⚠️ 必须 `justify-content: start`：grid 的 `normal` = `stretch`，**auto 轨道会被
       拉满容器**（实测多出 19px），于是备注（跨三列）又比上面那行宽一截、白对齐一场。 */
    justify-content: start;
    /* 行列分开写：行的间距由各元素自己的 margin 给（下面那两条），
       这样"备注→按钮"那一档才能精确等于按钮下方的留白。 */
    column-gap: 10px;
    row-gap: 0;
    align-items: end;
  }
  .grade-modal .grade-field { min-width: 0; }
  .grade-modal .grade-field-name input { width: calc(11em + 18px); box-sizing: border-box; }
  .grade-modal .grade-field select { width: auto; min-width: 74px; box-sizing: border-box; }
  .grade-modal .grade-field input[type="number"] { width: 92px; box-sizing: border-box; }
  .grade-modal .grade-field-wide { grid-column: 1 / -1; margin-top: 10px; }
  .grade-modal .grade-field-wide input { width: 100%; box-sizing: border-box; }
  /* 「修改名称」面板只有两行**上下**排（名称 / 备注），不像新建那样三栏并排 ——
     所以那一层换成单列：两行都占满弹窗内容宽，按钮右边界也跟着这条线走。 */
  .grade-rename-modal .grade-form { grid-template-columns: minmax(0, 1fr); }
  .grade-rename-modal .grade-field-name { grid-column: 1 / -1; }
  .grade-rename-modal .grade-field-name input { width: 100%; }
  /* 四个框统一高度（**含 select**）：不统一的话它比 input 高 2px，三个标签就上下错开 */
  .grade-modal .grade-field input,
  .grade-modal .grade-field select {
    box-sizing: border-box; height: 26px; line-height: 16px;
  }
  /* 年份 = **上下拨动**的步进框：Chrome 默认只在 hover / focus 时才把上下箭头显形
     （常态 opacity:0），这里让它常显 —— 用户要的就是"一眼看见能上下拨"。 */
  .grade-modal input[type="number"]::-webkit-outer-spin-button,
  .grade-modal input[type="number"]::-webkit-inner-spin-button {
    opacity: 1; height: 22px; cursor: pointer;
  }
  /* 按钮行**上方**留白 = 下方留白（.modal 自己的 padding-bottom 12px）。
     原来上面是 0 —— 备注框紧贴着按钮，用户 2026-10-12 说"太少"。
     `grid-column: 1 / -1` 还兼着另一件事：让按钮的**右边界**跟着输入框那一排走
     （form 外面右对齐会按弹窗整宽算，比输入框多出一截）。 */
  .grade-modal .modal-actions { grid-column: 1 / -1; margin-top: 12px; }
  /* 建班失败（比如课测里已经有同名文件夹）时那句红字：弹窗开着、面板被遮住，
     错误必须画在弹窗里，否则用户只看到"点了没反应"。 */
  .grade-modal .grade-modal-error { grid-column: 1 / -1; margin: 10px 0 0; font-size: 12px;
    color: var(--c-color); }
  .grade-list { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
    gap: 6px; overflow: auto; }
  /* 「结课」子 tab 的筛选框（2026-10-13 用户要求）：**直接复用目录树那一颗**
     （.tree-search 的输入框样式：1px var(--border-strong) 边框、6px 圆角、13px 字）。
     只改容器三点、别照搬：① 左栏面板自带 `padding: 10px 12px` → 这里左右不再补 10px
     （补了就比下面的班级卡窄一截、对不齐）；② 不要目录树那条下边框线；
     ③ **上下留白按用户 2026-10-13 的口径重算**（实测，输入框边缘 → 边缘）：
        · 上：子 tab 行底 → 输入框顶 = **8px** —— 正好等于「在授」tab 的「子 tab 行 →
          第一个班级卡」那 8px（那 8px 来自 .grade-bar 自己的 margin-bottom）；
          所以这里 padding-top 必须是 **0**（原来 8，加上去变成 16，比在授松一倍）。
        · 下：输入框底 → 第一个班级卡顶 = **6px** —— 与「相邻两个班级卡之间」那 6px
          （.grade-list 的 gap）齐平；`.grade-list` 自己没有上外边距，所以全靠这里的
          padding-bottom 补足（原来 2，比卡间距紧 4px）。 */
  .tree-search.grade-filter { padding: 0 0 6px; border-bottom: none; }
  /* 一行两项（2026-10-12 改）：第 1 行班级名，第 2 行"年份·期别|人数·测试次数"。
     原来是"名字 + 元信息 + 一排操作按钮"的横向 flex —— 操作按钮已改成右键菜单，
     所以这里回到最朴素的上下两行。 */
  .grade-item { display: flex; flex-direction: column; gap: 2px; padding: 7px 10px;
    background: #fff; border: 1px solid var(--border); border-radius: 7px;
    cursor: pointer; font-family: inherit; text-align: left; }
  .grade-item:hover { border-color: var(--accent); background: var(--accent-soft); }
  .grade-item.selected { border-color: var(--accent); background: var(--accent-soft); }
  /* 已结课的班卡：**整体降对比**（卡片浅灰、文字灰一档），在授 / 已结一眼分得开。
     ⚠️ **不是禁用**：结课卡照样能点、能右键「放回在授」、能删 —— 所以这里**不写**
     pointer-events / 大把 opacity（后者还会把文字一起糊掉，读不清）。hover 与选中态保留，
     只是底色比在授那档浅一点，仍然看得出"鼠标在这儿"。 */
  .grade-item.closed { background: #f3f5f8; border-color: var(--border); }
  .grade-item.closed .grade-item-name { color: #6b7280; font-weight: 500; }
  .grade-item.closed .grade-item-when { color: #8a94a6; }
  .grade-item.closed .grade-item-meta { color: #9aa3b0; }
  .grade-item.closed:hover { border-color: var(--accent); background: #eef2f7; }
  .grade-item.closed.selected { border-color: var(--accent); background: var(--accent-soft); }
  /* 编辑模式下把班级卡**拖起来排序**（2026-10-12）：拖起来那张淡出、落点画一条主色线 ——
     观感与真题卷集的分类列表（.ds-item.dragging / .dropbefore / .dropafter）同一套。 */
  .grade-item.editing { cursor: grab; user-select: none; -webkit-user-select: none; }
  .grade-item.dragging { opacity: 0.45; }
  .grade-item.dropbefore { box-shadow: inset 0 2px 0 var(--accent); }
  .grade-item.dropafter { box-shadow: inset 0 -2px 0 var(--accent); }
  .grade-item-name { min-width: 0; font-size: 13px; font-weight: 600;
    color: #2c3444; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .grade-item-meta { min-width: 0; font-size: 11.5px; color: var(--muted);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* 「2026年·秋季」这一截：蓝 + 加粗（用户拍板）；后面「|0人·0次测试」保持上面那档灰 */
  .grade-item-when { color: var(--accent); font-weight: 600; }
  /* 第 2 行里的分隔符（· / ｜）：两侧各留 5px —— 字直接贴着点 / 竖线太挤
     （2026-10-12 用户："离两边的字太近了"）。inline-block 才吃得住左右 margin。 */
  .grade-item-sep { display: inline-block; margin: 0 5px; }
  /* 卡片**标题行**（2026-10-12）：名字 + 右端那颗「花名册」。
     名字那一格吃剩余宽度（放不下出省略号），按钮 flex:none 贴着行长右端。 */
  .grade-item-head { display: flex; align-items: center; gap: 6px; min-width: 0; }
  .grade-item-head .grade-item-name { flex: 1 1 auto; }
  /* 「花名册」：**只在左栏编辑模式出现**（与"新建班级"同一个开关：齿轮①）。
     观感照中栏那颗「新建测试」—— 主色描边胶囊，一眼是"能点开一个东西"。
     `@click.stop` / `@pointerdown.stop` 在模板上：别让它顺带触发选班 / 拖拽排序。 */
  .grade-item-roster { flex: none; margin-left: auto; padding: 0 8px;
    border: 1px solid var(--accent); border-radius: 999px; background: #fff;
    color: var(--accent); font-size: 11.5px; font-family: inherit; line-height: 18px;
    cursor: pointer; white-space: nowrap; }
  .grade-item-roster:hover { background: var(--accent-soft); }
  /* 中栏 / 右栏这一轮的占位（后续轮次才填内容） */
  .grade-placeholder { flex: 1 1 auto; min-height: 0; margin: 0;
    display: flex; align-items: center; justify-content: center;
    color: var(--muted); font-size: 13px; }
  /* ── 中栏 · 班级名单（2026-10-12 R1-B1）────────────────────────────────────
     一行一个学生：序号（等宽、灰） + 姓名（黑粗） + 电话（等宽、灰）。
     类名一律 `grade-stu-*`：左栏那两个 `grade-card` / `grade-detail` 被
     test_frontend_template 的守卫禁在这页上（见那条 assertNotIn），别复用。 */
  /* 名单改成**小卡片横向流式**（2026-10-12 用户拍板）：一行放不下自动换行。
     卡上**只有姓名**（序号 / 电话不再显示 —— 数据还在库里，进「学生详情」看）。 */
  .grade-stu-cards { flex: 1 1 auto; min-height: 0; overflow: auto;
    display: flex; flex-wrap: wrap; align-content: flex-start; gap: 6px; }
  /* 卡宽 = **5 个汉字 + 右侧给 × 留的那格**（字号 13px → 1em = 13px）：
     5em = 65px 正文 + 30px（左右内边距 8×2 + 编辑态那颗 × 的 14px 位）。
     宽度写死 → 一列一列对齐，"张三"和"王馨舒雅"的卡一样宽（用户要的就是这个）。 */
  .grade-stu-card { position: relative; box-sizing: border-box;
    width: calc(5em + 30px); height: 30px; display: flex; align-items: center;
    justify-content: center; padding: 0 8px; background: #fff;
    border: 1px solid var(--card-border); border-radius: 7px;
    font-size: 13px; font-family: inherit; text-align: center;
    /* 不许选中文字（2026-10-12 用户："花名册里右键姓名卡会选中名字"）——
       右键那两个字符会被浏览器顺手选中、右击菜单一起来就是一片蓝。
       `user-select: none` 一举盖住左键拖选与右键取词；卡是"名牌"，不是可复制文本。 */
    user-select: none; -webkit-user-select: none; }
  .grade-stu-card:hover { border-color: var(--accent); background: var(--accent-soft); }
  /* 指针：**默认箭头**（2026-10-12 用户："hover 姓名卡变手型了，要箭头"）。
     卡片可点 / 可右键，但光标一律是箭头 —— 编辑态原来给的是 context-menu（也是
     一只"手"），一并改掉，免得两处不一致。 */
  .grade-stu-card,
  .grade-stu-card.editing { cursor: default; }
  .grade-stu-card.editing { padding-right: 20px; }
  /* 名字**不加黑**（2026-10-12 用户要求）：与旁边的字同一档字重（400）——
     卡片本身就是"一个人的名牌"，再压一层粗体反而显重。 */
  .grade-stu-name { min-width: 0; font-weight: 400; color: #2c3444;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* 编辑模式下每张卡右侧那颗 ×（照右栏题卡关闭键那一档大小）：
     绝对定位、不参与布局，所以卡宽不受它影响。 */
  .grade-stu-x { position: absolute; right: 3px; top: 50%; transform: translateY(-50%);
    width: 16px; height: 16px; padding: 0; border: 0; border-radius: 4px;
    background: transparent; color: #98a2b3; font-size: 13px; line-height: 1;
    cursor: pointer; font-family: inherit; }
  .grade-stu-x:hover { background: #fdf3f2; color: var(--c-color); }
  /* 「花名册」弹窗（2026-10-12）：卡列表与中栏是**同一个组件**（GradeStuCards）——
     这里只补容器尺寸，卡本身一个字不动。 */
  /* 弹窗也不许超过窗口：给一条 max-height，真超了让弹窗自己滚（下方卡片区还有
     自己的一条 60vh 内滚，两条一起兜住 100 人那种长名单）。 */
  /* 宽度**按卡片算出来**，让一行正好放下 4 张、且左右留白都是 18px（用户 2026-10-12
     要求"弹窗左右留白相同"）：4 × 卡宽(5em+30px=95) + 3 × 间距 6 = 398，
     再加 .modal 自己的左右内边距各 18 → **434**。改卡宽/间距，这个数要跟着改。 */
  .modal.grade-modal.grade-roster-modal { width: 434px;
    max-height: calc(100vh - 48px); overflow: auto; }
  /* 卡片区（用户 2026-10-12 三条要求一起满足）：
     · **max-height 60vh + 内部滚动** —— 100 人也不把弹窗撑爆；
     · 排布从 flex-wrap 换成 **grid 等宽 4 列**（`repeat(4, minmax(0, 1fr))`）：一行的卡片
       **铺满**卡片区 —— 最后一张卡贴住右沿，于是"关闭按钮的右沿 = 卡片区右沿 =
       弹窗内边距右沿"，左右留白都是 18px。原来 flex-wrap 每行末尾空 26px
       （再叠一条滚动条 15px）→ 看着"右留白大"。
       列数**写死 4**、列宽 1fr：有没有滚动条都铺满，也不依赖 auto-fill 对最小宽度的
       取整（实测 auto-fill + minmax 在这 398px 里只肯给 3 列）。 */
  .grade-roster-modal .grade-stu-cards {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    align-content: start;
    gap: 6px;
    flex: none; min-height: 34px; max-height: 60vh; overflow: auto;
    margin-bottom: 12px; }
  /* 卡在花名册里**填满自己的格子**（宽度仍由那一列决定，列与列照样齐）；
     中栏那份是 flex-wrap 的固定 95px，一个字没动。 */
  .grade-roster-modal .grade-stu-card { width: 100%; }
  .grade-roster-modal .grade-stu-empty { margin: 0 0 12px; }
  /* ── 「快速录入」弹窗（2026-10-13）：表头右键 → 单次录分（一人一行 + 分数框）────
     · 顺序由 JS 算（未录在上、已录按录入时间沉底），这里只负责画；
     · 分数框**就用横表那套 `.grade-grid-input`**（观感/行为与横表一致）；
     · 已录行：打勾 + 变淡（`.done`）—— 但**仍可改**（用户要能点回去改）。 */
  /* 弹窗**收窄**（2026-10-13 用户："现在偏宽"）：360 够放"姓名 + 64px 分数框"。 */
  .modal.grade-modal.grade-quick-modal { width: 360px; display: flex; flex-direction: column; }
  .grade-quick-meta { margin: 0 0 8px; font-size: 12px; color: var(--muted); }
  /* 搜索框 + 右端那颗「×」清空键（2026-10-13）：壳子负责定位与下边距，输入框右边多留
     26px 给 ×。× 的观感照站内既有的清空键（顶栏搜索框 `.search-clear`、学生卡 `.grade-stu-x`
     同一档：18×18、无色底、hover 才显）。 */
  .grade-quick-search-wrap { position: relative; display: block; margin: 0 0 8px; }
  .grade-quick-search { width: 100%; box-sizing: border-box; margin: 0;
    padding: 5px 26px 5px 8px; border: 1px solid var(--border-strong); border-radius: 6px;
    font-size: 13px; font-family: inherit; }
  .grade-quick-search:focus { border-color: var(--accent); outline: none; }
  .grade-quick-clear { position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
    width: 18px; height: 18px; padding: 0; border: 0; border-radius: 4px;
    background: transparent; color: #98a2b3; font-size: 14px; line-height: 1;
    font-family: inherit; cursor: pointer; }
  .grade-quick-clear:hover { background: #f2f4f7; color: #4a5564; }
  /* 列表：**高度固定**（不是 max-height）+ 内部滚动（2026-10-13 用户："不管 30 人还是
     搜索后 3 人，弹窗一样高"）—— 行少时下面留白，弹窗不跟着缩。 */
  .grade-quick-list { height: 46vh; overflow: auto; margin: 0 0 12px;
    border: 1px solid var(--border); border-radius: 8px; }
  .grade-quick-row { display: flex; align-items: center; gap: 8px; padding: 4px 8px;
    border-bottom: 1px solid var(--border); }
  .grade-quick-row:last-child { border-bottom: 0; }
  .grade-quick-name { flex: 1 1 auto; min-width: 0; font-size: 13px; color: #2c3444;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* ⚠️ 选择器必须带上 .grade-quick-modal（特异性 0,2,0）：下面 `.grade-grid-input`
     是 (0,1,0) 但**写在文件更后面**，光写 `.grade-quick-input` 会被它的 `width: 100%`
     盖掉 → 输入框吃满整行、把姓名挤没、还撑出横向滚动条（实测踩过）。
     其余外观照旧借 `.grade-grid-input`（同样的框、hover/focus 才显边框）。 */
  .grade-quick-modal .grade-quick-input { flex: none; width: 64px;
    /* 默认就画一条 1px 浅灰边（2026-10-13 用户："看不出往哪打"）—— 横表里靠列线能看出
       格子在哪，弹窗里那一列是空白。hover/focus 仍变主色（下面 .grade-grid-input 那两条）。 */
    border-color: var(--border); }
  /* ✔️ 那一格：**固定宽度、永远在**（没录的行留空）—— 这样分数框的 x 不随"录没录"
     左右移动，整列对得齐（2026-10-13 用户要求：✔️ 从框右边挪到框左边）。 */
  .grade-quick-check { flex: none; width: 14px; text-align: center;
    color: var(--a-color); font-size: 13px; }
  .grade-quick-row.done .grade-quick-name,
  .grade-quick-row.done .grade-quick-input { color: #98a2b3; }
  /* 正在录入的这一名：姓名变站内主色并加粗；输入框本身的焦点边框照旧。 */
  .grade-quick-row.active .grade-quick-name { color: var(--accent); font-weight: 600; }
  .grade-quick-empty { margin: 0; padding: 12px; color: var(--muted); font-size: 13px;
    text-align: center; }
  /* 「关闭」按钮**上方 = 下方**（2026-10-13 用户："现在上大下小"）：
     上面本来叠了两笔留白 —— 列表自己的 12px + `.grade-modal .modal-actions` 的 12px
     = 24px，而下面只有 .modal 的下内边距 12px。把这一颗归零，上下就都是 12px。 */
  .grade-quick-modal .modal-actions { margin-top: 0; }
  /* 「学生详情」弹窗里的三行：左标签（灰）右值（黑，电话走等宽） */
  .grade-stu-detail { display: flex; flex-direction: column; gap: 6px; margin: 0; }
  .grade-stu-detail > p { display: flex; gap: 10px; margin: 0; font-size: 13px;
    line-height: 1.9; }
  .grade-stu-detail .k { flex: none; width: 4.5em; color: var(--muted); }
  .grade-stu-detail .v { min-width: 0; color: #2c3444; word-break: break-all; }
  .grade-stu-detail .v.phone { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
  .grade-stu-detail .v.empty { color: #b7c0cc; }
  /* ── 中栏「测试列表」（2026-10-12 R1-B2-a）────────────────────────────────
     卡片 = 名字（一行）+ 元信息（日期 · 满分 · 已录 X/Y），与左栏班级卡同一套观感；
     点一下选中（右栏录分那轮要用），编辑模式下右键出「修改 / 删除」。 */
  .grade-test-list { flex: 1 1 auto; min-height: 0; overflow: auto;
    display: flex; flex-direction: column; gap: 6px; }
  /* 一张卡**就一行**（2026-10-12 用户要求）：测试名在左（占满剩余、长了省略号），
     日期 / 满分 / 已录 X-Y 在右（不缩、右对齐）。 */
  .grade-test-card { display: flex; align-items: baseline; gap: 10px;
    padding: 7px 10px; background: #fff; border: 1px solid var(--border);
    border-radius: 7px; cursor: pointer; font-family: inherit; text-align: left;
    /* 同上：这一格也**不许选中文字**（右键/拖选同样会带走一片蓝）。 */
    user-select: none; -webkit-user-select: none; }
  .grade-test-card:hover { border-color: var(--accent); background: var(--accent-soft); }
  .grade-test-card.selected { border-color: var(--accent); background: var(--accent-soft); }
  .grade-test-card.editing { cursor: context-menu; }
  .grade-test-name { flex: 1 1 auto; min-width: 0; font-size: 13px; font-weight: 600;
    color: #2c3444;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .grade-test-meta { flex: none; font-size: 11.5px; color: var(--muted);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* 日期那一截蓝 + 粗（与左栏班级卡的「2026年·秋季」同一档） */
  .grade-test-when { color: var(--accent); font-weight: 600; }
  /* ── 右栏「横表总表」（2026-10-12 R1-B2-b2）────────────────────────────────
     行 = 学生（按 seq）、列 = 各次测试（按日期，旧的在左）+ 总计；底部一行平均分。
     列宽 64px 是算出来的：日期写两位年 `26.10.12`（8 字符 11px ≈ 53px）+ 左右各 4px；
     再宽就放不下 12 列（参考你那张表），所以容器 overflow-x:auto 横滚。
     ⚠️ **姓名列 + 表头三层 sticky**：横滚/竖滚时"这是谁""这是哪一次"永远看得见 ——
       表格用 border-collapse: separate（collapse 下 sticky 的边框会跟着滚、散架）。 */
  /* 表标题那条（照参考表）：灰底框里**只放标题**（2026-10-12 用户去掉下面那行
     "N 次测试 · N 人"），字号加大到 17px，**行内与竖直都居中**（flex 双向居中，
     框给一个最小高度，居中才看得出来）。窄了不折行 —— 出省略号。 */
  .grade-grid-head { flex: none; display: flex; align-items: center;
    justify-content: center; min-height: 38px; margin-bottom: 6px; padding: 6px 8px;
    background: #e9ebee; border: 1px solid var(--border-strong); border-radius: 6px;
    /* 右键弹菜单的那张卡**不许选中文字**（2026-10-12 用户要求：右键别带走一片蓝）——
       与表头 / 姓名卡同一套；卡里的标题跟着继承。 */
    user-select: none; -webkit-user-select: none; }
  .grade-grid-title { min-width: 0; font-size: 17px; font-weight: 700; color: #1f2430;
    letter-spacing: 0.5px; text-align: center; line-height: 1.3; white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis; }
  /* 总表标题行两端的两个勾选框（2026-10-14）：
     左「个人成绩 □」控中栏下方的个人成绩单，右「□ 成绩分析」控最右那栏。
     **绝对定位在卡片两端**，标题仍在正中（放进 flex 流会把标题挤偏）；
     两端各留 92px 内边距，免得长标题压到勾选框上（挤了就出省略号）。 */
  .grade-grid-head-main { position: relative; padding-left: 92px; padding-right: 92px; }
  .grade-grid-check { position: absolute; top: 50%; transform: translateY(-50%);
    display: inline-flex; align-items: center;
    /* 文案 ↔ 勾选框的间距照工作台中栏那颗「显示答案」量出来的**视觉间距 7px**
       （`gap:4px` 是那颗 label 自己写的，另有 UA 给 input 的默认 `margin-right:3px`；
       这里把 input 的 margin 归零了，所以 gap 直接写 4+3=7px —— 用户 2026-10-14 要求一致）。 */
    gap: 7px; font-size: 12px;
    /* 字重照工作台中栏那颗「显示答案」（.toolbar label = 12px / 600）—— 用户 2026-10-14：
       "字体加粗即可，样式我看是一样的"（实测那颗就是 600，其它值一致）。 */
    font-weight: 600;
    letter-spacing: 0; color: #4b5563; white-space: nowrap; cursor: pointer; }
  .grade-grid-check input { width: 13px; height: 13px; margin: 0; cursor: pointer;
    accent-color: var(--accent); }
  .grade-grid-check-left { left: 10px; }
  .grade-grid-check-right { right: 10px; }
  .grade-grid-wrap { flex: 1 1 auto; min-height: 0; overflow: auto; }
  .grade-grid { border-collapse: separate; border-spacing: 0; font-size: 12px;
    font-variant-numeric: tabular-nums; }
  .grade-grid th, .grade-grid td { border-right: 1px solid var(--border);
    border-bottom: 1px solid var(--border); padding: 0 4px; height: 24px;
    font-weight: 400; text-align: center; white-space: nowrap; }
  /* ⚠️ 数据格**自建层叠上下文**（position + z-index:0）：不这么做的话，格子里那个
     `<input>`（替换元素）会画到 sticky 表头**之上**，把表头下沿的分割线盖掉 ——
     2026-10-12 实测：滚动后那一像素 `elementFromPoint` 命中的是 `INPUT`。
     压到 0 之后，整块 tbody 内容都在表头（z-index 3 / 姓名列 5）之下，线自然露出来。
     （姓名列自己是 `sticky + z-index 2`，特异性更高，不受这条影响。） */
  .grade-grid tbody td { position: relative; z-index: 0; }
  .grade-grid thead th { position: sticky; z-index: 3; background: #f0f2f5; }
  /* 表头**不许选中文字**（2026-10-12 用户报：右键点表头会选住测试名，一片蓝）——
     `preventDefault` 只挡原生右键菜单，挡不住"按下鼠标顺手选词/拖选"，
     所以要在**元素上**禁掉选择。姓名卡（.grade-stu-card）早先也是这么修的。 */
  .grade-grid thead th { user-select: none; -webkit-user-select: none; }
  /* 三层表头（名称 / 日期 / 满分）的字重/字号/颜色**与姓名列对齐**
     （2026-10-12 用户："表头看着不如姓名列粗"）。实测差的是
     **字号（表头 11/11.5 → 姓名列 12）与颜色（灰 → 姓名列近黑 #1f2430）**，
     字重本来就都是 600 —— 所以这里把三个值统一到姓名列那一档，字重仍是 600。 */
  .grade-grid thead tr:nth-child(1) th,
  .grade-grid thead tr:nth-child(2) th,
  .grade-grid thead tr:nth-child(3) th {
    font-size: 12px; font-weight: 600; color: #1f2430;
    /* ⚠️ **行高必须钉死**（2026-10-14 用户报"滚动时满分行高度会变、视觉抖动"的根因）：
       不钉的话表头继承 body 的 `line-height: 1.7` → 12px 字 = **20.4px** 行盒，
       比下面声明的 `height: 20px` 还高 → 盒子被撑到 **21.4px**（20.4 + 1px 下边框）；
       而第 2、3 行 sticky 的 `top` 是按 22 / 42 算的（= 22 + 20 + 20）→ 对不上：
       第 3 行在滚动前**比它的 sticky 位置低 1.4px**，滚过头两三个像素时才"吸"上去 ——
       那 1.4px 的位移看起来就是"满分行高度在变"。
       19px 行盒 + 1px 下边框 = 正好 20px，三行 22 / 20 / 20 与粘滞偏移严丝合缝。 */
    line-height: 19px; }
  /* 三层各自钉在自己的高度上：0 / 22 / 42（22 + 20 + 20；标题在表格外面，不占偏移）。
     ⚠️ 同时把**堆叠顺序自上而下递减**（5 / 4 / 3）：上面那一行的下边沿必须画在
     下面那一行的背景**之上**。不这么做的话，第 3 行（sticky top:42，而第 2 行实际高
     21.39px）会往上压住第 2 行的下边框 —— 就是用户报的"**日期行和满分行之间的线
     会消失**"。这是让**每个格子自己的边沿**保持可见（不动行高、不动行距）。 */
  .grade-grid thead tr:nth-child(1) th { top: 0; height: 22px; z-index: 5; }
  /* 表头第一层（测试名）**可点**：给 pointer（2026-10-12 用户要求）。
     2026-10-13：第一列那三个标签格（名称 / 日期 / 满分）也能点了（点 = 清高亮），
     所以它们也拿到 pointer —— 见下面 `.grade-grid thead .grade-grid-namecol` 那条。 */
  .grade-grid thead tr:nth-child(1) th.grade-grid-col { cursor: pointer; }
  .grade-grid thead tr:nth-child(2) th { top: 22px; height: 20px; z-index: 4; }
  .grade-grid thead tr:nth-child(3) th { top: 42px; height: 20px; z-index: 3; }
  /* 表头第三层（满分）**也可点**了（2026-10-13：点它 = 选中该列 + 按该列成绩降序）
     → 与第一层测试名一样给 pointer。 */
  .grade-grid thead tr:nth-child(3) th.grade-grid-col { cursor: pointer; }
  /* 姓名列（表头那格 + 每行那格）：横滚时冻结在最左 */
  /* 姓名列：**不写死宽度**，交给表格自己按内容定宽 —— HTML 表格本来就把一列撑到
     "这一列最宽的那格" 那么宽，也就是**刚好放得下最长姓名**（+ 左右各 6px 内边距）。
     为什么不用 `width: max-content`：那是给 flex/grid 用的，**表格布局不认**它
     （写了也会被列宽算法忽略）；为什么不用固定 px：名字一长就得回来改数字。
     背景加深（与表头同色）、居中、600、横滚时冻结在最左 —— 全部保留。 */
  .grade-grid .grade-grid-namecol { position: sticky; left: 0; z-index: 2;
    background: #f0f2f5; text-align: center; padding: 0 6px; font-weight: 600;
    /* 姓名格现在**可点 / 可右键**（2026-10-13）→ 不让拖选、右键也不留蓝色选区
       （照花名册那张卡的做法：它是"名牌"，不是可复制文本）。 */
    user-select: none; -webkit-user-select: none; cursor: pointer;
    /* 名称列**最短**留够 4 个汉字（2026-10-13 用户要求）：字号 12px → 4em = 48px，
       加左右 padding 12 + 右边框 1 = **61px**（实测 4 个汉字墨迹就是 48px）。
       用 em 写是为了字号一改它跟着走；名字更长时列照样自己撑开（这是下限，不是定宽）。 */
    min-width: calc(4em + 13px); }
  /* 姓名列表头三格（名称 / 日期 / 满分）：比**同一行**其它格高一层（横滚时冻结在最左），
     行与行之间仍按 9 / 8 / 7 递减 —— 和第 1、2 行的递减是同一个理由。 */
  /* 2026-10-13 用户要求：点这三个标签格（名称 / 日期 / 满分）= **清掉所有测试列高亮**
     → 也给 pointer（能点的都给 pointer，一眼分得开哪格能点）。 */
  .grade-grid thead .grade-grid-namecol { background: #f0f2f5;
    text-align: center; font-weight: 600; color: #2c3444; cursor: pointer; }
  .grade-grid thead tr:nth-child(1) .grade-grid-namecol { z-index: 9; }
  .grade-grid thead tr:nth-child(2) .grade-grid-namecol { z-index: 8; }
  .grade-grid thead tr:nth-child(3) .grade-grid-namecol { z-index: 7; }
  /* ── 「总计」列（2026-10-13）：横滚时**冻结在最右**（与姓名列冻结在最左同一套）——
     列多了（12 次课测）滚起来还能一眼看见"这个学生整体怎么样"。
     · 三层表头 / 数据格 / 平均格都要 sticky right + **不透明底色**（否则会透出底下的格子）；
     · 表头那三格 z 由行规则给（5/4/3，与同行其它格同级、但它在行里**最后**，所以压得住）；
     数据格 2（压过普通数据格 0）、平均行 4（压过平均行其它格 2）。

     2026-10-13 晚（用户四条调整）：这一列**整体当"姓名列"那一档**看 ——
       · 所有格背景 = 姓名列同款灰 #f0f2f5（原来是表头灰 / 数据白 / 平均 #f7f9fc 三档）；
       · 左边那条线：**学生数据段用黑色**（= 中间白色成绩区的右边界），表头与平均行仍是浅灰；
       · 「100」那格的下边框改回**灰**（它原来跟着"满分行下沿黑线"那条走了）；
       · 平均分行上边框本来就是灰的（不动）。
     列宽：**不写死 64**，改成"够放下 88/18/28 这种串"的下限 —— 实测那串墨迹
     **50.78px**（字号 12px：6 个数字是 6ch、两条斜杠更窄），加左右 padding 4 + 边框 1
     ≈ **56px**；内容更长时列自己撑开（这是下限，不是定宽）。
     ⚠️ 下面每条都**必须带上够高的前缀**：数据格那条 `.grade-grid tbody td.grade-grid-cell
     {background:#fff}` 与平均行 `.grade-grid tr.grade-grid-avg td{background:#f7f9fc}`
     都是 (0,2,2)，光写 `.grade-grid .grade-grid-total`（0,2,0）压不住（实测底色没变）；
     「100」格下沿那条更要在**后面**那条"満分行测试格下沿黑"(0,3,3) 之上再加一层。 */
  .grade-grid .grade-grid-total { position: sticky; right: 0; z-index: 2;
    box-shadow: -1px 0 0 var(--border-strong); }
  .grade-grid thead th.grade-grid-total { background: #f0f2f5; }
  .grade-grid td.grade-grid-total.grade-grid-cell { width: auto;
    min-width: 56px; background: #f0f2f5; }
  /* 平均行那一格：底灰、左线浅灰、**上边框灰** ——
     ⚠️ 上边框要带 `.grade-grid-cell`（(0,4,2)）：否则会被后面那条
     `.grade-grid tr.grade-grid-avg td.grade-grid-cell{border-top:1px solid var(--text)}`
     （(0,3,2)）按"成绩区上沿黑"刷成黑的（实测就是黑的）。 */
  .grade-grid tbody tr.grade-grid-avg td.grade-grid-total.grade-grid-cell {
    z-index: 4; background: #f0f2f5; box-shadow: -1px 0 0 var(--border-strong);
    border-top: 1px solid var(--border); color: #1f2430; }
  /* 学生数据段（2026-10-13）：左线**黑**（白色成绩区到此为止）+ 字**黑**
     （2026-10-13 晚：先改成主色蓝，用户看过觉得还是黑字好 → 改回 var(--text)；
       同一格不再有两色，`color` 与平均行那格一致）。 */
  .grade-grid tbody td.grade-grid-total { box-shadow: -1px 0 0 var(--text);
    color: var(--text); }
  .grade-grid tbody tr.grade-grid-avg td.grade-grid-total { color: #1f2430; }
  /* 总计列的字重 = **与三层表头同款 600**（2026-10-14 用户要求；改前继承 `.grade-grid th,td`
     那个 400，跟旁边 600 的表头/姓名列一比就显得"轻"）。
     ⚠️ 这一列是 `width: auto + min-width: 56px`，靠数字撑开；数字用的是 tabular-nums，
     加粗**不会**改字宽（实测列宽不变）—— 与姓名列那条"不许用 font-weight"的坑不同。 */
  .grade-grid td.grade-grid-total { font-weight: 600; }
  /* 「100」那格的下边框：从"満分行下沿黑线"改回**灰**（总计列不是成绩区） */
  .grade-grid thead tr:nth-child(3) th.grade-grid-total.grade-grid-col {
    border-bottom: 1px solid var(--border); }
  /* ── 三条线改黑：**只落在"成绩区"那几格上**（用户 2026-10-12 反复强调：
     "是单独一个格子，不是一整行"）。名字列那四个**标签格**（名称 / 日期 / 满分 /
     平均分）保持灰，所以黑的只覆盖：
       · 满分行的**测试格** → 下边沿黑
       · 平均分行的**测试格** → 上边沿黑
       · **学生名字格**（tbody 里的姓名格）→ 右边沿黑
     对应用户图上画的那个"⊢"形：竖线只穿过 30 行学生名，横线只横在成绩区。 */
  .grade-grid thead tr:nth-child(3) th.grade-grid-col {
    border-bottom: 1px solid var(--text); }
  .grade-grid tr.grade-grid-avg td.grade-grid-cell {
    border-top: 1px solid var(--text); }
  .grade-grid tbody td.grade-grid-namecol {
    border-right: 1px solid var(--text); }
  /* 名字列的**标签格**（名称 / 日期 / 满分 / 平均分）：右边沿用灰 —— 把黑竖线截在
     第一个学生格上（等于图里那条竖线的起点 / 终点）。 */
  .grade-grid thead .grade-grid-namecol,
  .grade-grid tr.grade-grid-avg td.grade-grid-namecol {
    border-right: 1px solid var(--border); }
  /* 总计列：最右一列，底色浅一档 */
  .grade-grid td.grade-grid-cell { width: 64px; min-width: 64px; padding: 0 2px; }
  /* 数据格**一律白底**（2026-10-12 用户要求：不再用"空格浅灰"区分）——
     空格子本身就是空的，一眼看得出来。 */
  .grade-grid tbody td.grade-grid-cell { background: #fff; }
  /* 中栏选中的那一列：**整列**底色 —— 表头三层 + 每个数据格 + 平均行，一格不落。
     ⚠️ 三个选择器都得**压过**它们各自那条底色规则，否则就是"只有表头变色、整列不变"
     （用户 2026-10-12 实测报过这一条：数据格那条
     `.grade-grid tbody td.grade-grid-cell { background: #fff }` 是 (0,2,2)，
     而原来通用的 `.grade-grid .col-active` 只有 (0,2,0) → 被白底盖住）。
     这里把 `.col-active` **缀在同前缀后面**，特异性 (0,3,2)，稳赢。 */
  .grade-grid thead tr th.col-active,
  .grade-grid tbody td.grade-grid-cell.col-active,
  .grade-grid tr.grade-grid-avg td.grade-grid-cell.col-active {
    background: var(--accent-soft); }
  /* 表头那三格再叠一层"变蓝加粗"（⚠️ 带 `tr`：要压过"三层表头统一字色 #1f2430"） */
  /* ⚠️ 2026-10-13：这里**不许**再改 font-weight —— 表宽是按内容算的，600 → 700 会让
     那一列 64.16 → 65.38px（整表跟着 +1.22px），"点一下列就抖一下"。
     改装 **-webkit-text-stroke** 描边加粗：看着一样粗，**字形宽度一个像素都不变**
     （描边不参与排版）。实测：改前 565.16 / 改后（700）566.38 / 描边 565.16。 */
  .grade-grid thead tr th.col-active {
    color: var(--accent); -webkit-text-stroke: 0.4px currentColor; }
  /* ── 「行高亮」（2026-10-13 用户要求）：点**姓名格** → 那一整行亮 ──────────────
     与列高亮**互斥**（选中行时列高亮清掉、反之亦然，见 clickGradeRow / selectGradeTest）。
     只改**背景**、不碰 border / padding / 字重 —— 和列高亮同一个道理：行高亮不能把表格撑宽。
     为什么要写成四条：每一类格子都自带背景规则（数据格 #fff、姓名格 / 总计格 #f0f2f5、
     平均行 #f0f2f5），得逐类压过去，否则就是"只有一半格子变色"。 */
  .grade-grid tbody tr.row-active td,
  .grade-grid tbody tr.row-active td.grade-grid-cell,
  .grade-grid tbody tr.row-active td.grade-grid-namecol,
  .grade-grid tbody tr.row-active td.grade-grid-total {
    background: var(--accent-soft); }
  /* 行高亮时**姓名那格**也跟着"变蓝 + 加粗"（2026-10-13 用户要求：与列高亮后的表头同款）。
     ⚠️ 加粗同样**不能用 font-weight**：姓名列宽是按内容算的，600→700 会把这列撑宽
     （列高亮那一轮的实测：64.16 → 65.38px）→ 用同一条 text-stroke 写法，字形宽度不变。 */
  .grade-grid tbody tr.row-active td.grade-grid-namecol {
    color: var(--accent); -webkit-text-stroke: 0.4px currentColor; }
  /* 每格一个**常驻 input**：空格子不画边框，hover / focus 才显 —— 看着是表格，
     点一下就能打字（不是"点了才变输入框"两种状态）。 */
  .grade-grid-input { width: 100%; box-sizing: border-box; padding: 2px 3px;
    border: 1px solid transparent; border-radius: 4px; background: transparent;
    font-family: inherit; font-size: 12px; font-weight: 400; text-align: center;
    color: #2c3444; }
  .grade-grid-input:hover { border-color: var(--border-strong); background: #fff; }
  .grade-grid-input:focus { border-color: var(--accent); background: #fff;
    outline: none; }
  /* 非编辑模式（只读）下**分数就是"成绩信息"** → 用正文黑，不用灰
     （2026-10-14 用户要求：总表 + 个人表的成绩信息统一黑字；**字重仍是 400**）。
     ⚠️ 这一条管的是格子里的 `<input>` 文字 —— 分数与「缺考」都走这里，所以两者一起变黑。 */
  .grade-grid-input:read-only { color: var(--text); cursor: default; }
  .grade-grid-input:read-only:hover { border-color: transparent; background: transparent; }
  /* ⚠️ 但「缺考」两个字**不是分数** → 单独挂类保持灰（2026-10-14 用户要求：
     "分数格黑、缺考灰，两者并存"）。挂在 `:read-only` 那条**后面** —— 两条特异性相同
     （0,2,0），靠顺序压住它。模板里按草稿值 `=== '缺考'` 挂类（那也正是格子里显示的字）。 */
  .grade-grid-input.grade-grid-absent { color: #6b7280; }
  /* 底部平均分行 */
  /* 平均分行：**加粗**（2026-10-12 用户："除成绩格外其他格都加粗"） */
  .grade-grid tr.grade-grid-avg td { background: #f0f2f5;
    /* 底色 2026-10-13 晚统一成**姓名列/总计列同款灰 #f0f2f5**（原来 #f7f9fc，
       比上面那几行浅一档，整行看着"浮"起来）。 */
    /* 字体与**姓名列同款**（2026-10-12 用户）：12px + 近黑 #1f2430 + 600
       （原来 11px + muted 灰，比姓名列小一档、淡一档）。 */
    color: #1f2430; font-size: 12px; font-weight: 600;
    position: sticky; bottom: 0; z-index: 2;
    /* 平均分行**每个格子自己的上边沿**（原来 `border-top: 0`，你看到的那条是上一行的
       下边框、跟着上一行滚走）。颜色在下面按"成绩区 / 名字列标签"分别给。 */
    border-top: 1px solid var(--border); }
  .grade-grid tr.grade-grid-avg td.grade-grid-namecol { z-index: 4; background: #f0f2f5; }
  /* 「新建 / 修改测试」弹窗（2026-10-12 用户要求）：
     · **三个输入框排成一排**（类型 / 日期 / 满分），框高一致（.grade-modal .grade-field
       那条 26px 的规则管着，见上）；左对齐起排，不铺满弹窗；
     · 「名称」只有**改测试**时才有 → 单独跨满一行（建的时候根本没有这个框）；
     · 取消 / 创建贴在弹窗**右下角**，上下留白相同：按钮行 margin-top 12px
       = .modal 的下内边距 12px。 */
  .grade-test-modal .grade-form { display: grid; grid-template-columns: auto auto auto;
    justify-content: start; align-items: end; gap: 8px 10px; margin: 0; }
  /* 弹窗宽度**跟着内容走**（2026-10-12 用户要求："右边留白偏大，改成和左边一样"）：
     三个字段是"按内容定宽 + 左起排"的，用通用的 383px 会在右边多留 14px 空档
     （左边 18、右边 32）。改成 fit-content 之后宽度 = 字段行 + 左右内边距各 18px，
     两边**天然相等**；改测试时多出来的「名称」框跨满一行，也不会把弹窗撑宽。 */
  .modal.grade-modal.grade-test-modal { width: fit-content; max-width: 560px; }
  .grade-test-modal .grade-field-wide { grid-column: 1 / -1; margin-top: 0; }
  .grade-test-modal .grade-field select { width: auto; min-width: 84px; }
  /* 日期框在弹窗里没有 .ct-field 祖先，宽度靠这条兜住（够显示 2024.4.27） */
  .grade-test-modal .date-field .ct-input { width: 104px; box-sizing: border-box; }
  /* 按钮行：**在 grid 外面**（见模板里那段注释）—— 贴弹窗右下角，
     上下留白都取 12px（= .modal 的 padding-bottom / 12px 的 margin-top）。 */
  .grade-test-modal .modal-actions { margin: 12px 0 0; }
  .grade-test-modal .grade-modal-error { margin: 8px 0 0; }
  /* ── 家长端（2026-10-22）：设置 / 查询码 / 绑定管理 ─────────────────────── */
  .wsb-menu-item.parent { color: #0f766e; font-weight: 600; }
  .wsb-menu-item.parent:hover { background: #edf8f5; color: #0f766e; }
  .modal.grade-modal.grade-parent-profile-modal { width: 360px; }
  .grade-parent-help { grid-column: 1 / -1; margin: 2px 0 0; color: var(--muted);
    font-size: 12px; line-height: 1.7; }
  /* 固定尺寸：查询码 / 家长绑定各占一列，各自滚动；弹窗本身不随条数变化。 */
  .modal.grade-modal.grade-parent-manager-modal {
    width: min(780px, calc(100vw - 40px));
    height: min(620px, calc(100vh - 40px));
    max-width: none; max-height: none;
    display: flex; flex-direction: column;
  }
  /* 家长端是明确的管理弹窗：阻止点击穿透到后面班卡，避免“背景已换班、
     弹窗还管旧班”的不一致。班级切换统一走弹窗内下拉框。 */
  .grade-modal-layer.grade-layer-blocking { pointer-events: auto; }
  .grade-parent-manager-head { display: flex; align-items: center; gap: 14px;
    margin-bottom: 12px; }
  .grade-parent-manager-head .grade-modal-title { margin: 0; }
  .grade-parent-toolbar-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 12px; margin-bottom: 7px; }
  .grade-parent-profile-row { display: flex; align-items: center; gap: 8px; min-width: 0;
    min-height: 37px; padding: 4px 9px;
    border: 1px solid var(--border); border-radius: 7px; background: #f8fafb;
    color: var(--muted); font-size: 12px; }
  .grade-parent-profile-row > span { flex: none; }
  .grade-parent-profile-row b { min-width: 0; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; color: #2c3444; font-size: 13px; }
  .grade-parent-profile-row button { margin-left: auto; }
  .grade-parent-class-row select { flex: 1 1 auto; min-width: 0; width: 100%; height: 27px;
    padding: 0 8px; border: 1px solid var(--border-strong); border-radius: 6px;
    background: #fff; color: var(--text); font-family: inherit; font-size: 12px; }
  .grade-parent-manager-body {
    flex: 1 1 auto; min-height: 0; overflow: hidden;
    display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 16px; margin-top: 0;
  }
  .grade-parent-section { min-height: 0; display: flex; flex-direction: column; gap: 7px; }
  .grade-parent-section-head { display: flex; align-items: center; gap: 9px;
    flex: none; height: 24px; min-height: 24px; }
  .grade-parent-section-head b { flex: none; font-size: 13px; color: #2c3444; }
  .grade-parent-section-head span { color: var(--muted); font-size: 11.5px; line-height: 1.5; }
  .grade-parent-name-filter { flex: 0 1 104px; width: 104px; min-width: 72px;
    height: 24px; padding: 0 8px; border: 1px solid var(--border-strong);
    border-radius: 6px; background: #fff; color: var(--text); font-family: inherit;
    font-size: 11px; box-sizing: border-box; }
  .grade-parent-name-filter:focus { border-color: var(--accent);
    outline: none; }
  .grade-parent-head-tabs { flex: none; height: 24px; margin-left: auto;
    display: inline-flex; align-items: center; gap: 2px; padding: 2px;
    border-radius: 6px; background: #f1f4f7; box-sizing: border-box; }
  .grade-parent-head-tabs button { height: 20px; padding: 0 7px; border: 0; border-radius: 4px;
    background: transparent; color: #6b7280; font-family: inherit; font-size: 11.5px;
    line-height: 20px; cursor: pointer; }
  .grade-parent-head-tabs button:hover { color: #334e68; }
  .grade-parent-head-tabs button.active { background: #fff; color: #334e68;
    font-weight: 600; box-shadow: 0 1px 2px rgba(51, 78, 104, 0.12); }
  .grade-parent-list {
    flex: 1 1 auto; min-height: 0; max-height: none;
    display: flex; flex-direction: column;
    overflow-y: auto; overflow-x: hidden;
    scrollbar-gutter: stable;
    border: 1px solid var(--border); border-radius: 7px; background: #fff;
  }
  .grade-parent-section > .grade-parent-empty {
    flex: 1 1 auto; display: flex; align-items: center; justify-content: center;
  }
  .grade-parent-list > .grade-parent-empty {
    flex: 1 1 auto; min-height: 0; margin: 0; padding: 16px; border: 0;
    display: flex; align-items: center; justify-content: center;
  }
  .grade-parent-manager-body > .grade-parent-empty { grid-column: 1 / -1; align-self: center; }
  .grade-parent-row { flex: none; display: flex; align-items: center; gap: 9px; min-height: 38px;
    padding: 7px 9px; border-bottom: 1px solid var(--border); font-size: 12px; }
  .grade-parent-row:last-child { border-bottom: 0; }
  .grade-parent-row-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column;
    gap: 2px; }
  .grade-parent-row-main b { display: flex; flex-wrap: wrap; gap: 5px; color: #2c3444;
    font-size: 13px; }
  .grade-parent-row-main > span { color: var(--muted); font-size: 11.5px; }
  .grade-parent-child { display: inline-block; padding: 1px 6px; border-radius: 999px;
    border: 0; background: #eef3f7; color: #334e68; font-family: inherit;
    font-size: 11.5px; font-weight: 500; cursor: pointer; }
  .grade-parent-child.relation-mother,
  .grade-parent-parent-chip.relation-mother {
    background: #f3dfe8; color: #6f334b; font-weight: 600;
    box-shadow: inset 0 0 0 1px #d6afbf;
  }
  .grade-parent-child.relation-father,
  .grade-parent-parent-chip.relation-father {
    background: #d9e8f7; color: #173f67; font-weight: 600;
    box-shadow: inset 0 0 0 1px #a9c2db;
  }
  .grade-parent-student-row { flex: none; display: flex; align-items: center; gap: 10px;
    min-height: 40px; padding: 7px 10px; border-bottom: 1px solid var(--border); }
  .grade-parent-student-row:last-child { border-bottom: 0; }
  .grade-parent-student-name { flex: 1 1 auto; min-width: 0; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; color: #2c3444; font-size: 13px; }
  .grade-parent-student-bindings { flex: none; display: flex; align-items: center;
    justify-content: flex-end; gap: 5px; min-width: 74px; }
  .grade-parent-parent-chip { min-height: 22px; padding: 1px 8px; border: 0;
    border-radius: 999px; background: #eef3f7; color: #334e68; font-family: inherit;
    font-size: 11.5px; font-weight: 500; cursor: pointer; }
  .grade-parent-parent-chip:hover:not(:disabled) {
    box-shadow: inset 0 0 0 1px rgba(51, 78, 104, 0.2); }
  .grade-parent-parent-chip:disabled,
  .grade-parent-parent-chip.unbound {
    opacity: 1; background: #f6f7f9; color: #b0b8c2;
    font-weight: 400; box-shadow: none; cursor: default;
  }
  .grade-parent-status { flex: none; padding: 2px 7px; border-radius: 999px;
    background: #f1f4f7; color: #6b7280; font-size: 11px; }
  .grade-parent-status.active { background: #eaf7ef; color: #387a51; }
  .grade-parent-status.expired { background: #fff7e6; color: #8a6410; }
  .grade-parent-status.revoked { background: #f8eceb; color: #a44f45; }
  .grade-parent-row button.danger-text { flex: none;
    height: calc(1em * 1.7 + 4px); padding: 0 7px; border-radius: 6px;
    border: 1px solid #f0c8c4; background: #fdf3f2; color: var(--c-color);
    font-family: inherit; font-size: 11px; line-height: 1.7; white-space: nowrap; }
  .grade-parent-row button.danger-text:hover { background: #fbe9e7;
    border-color: var(--c-color); }
  .grade-parent-row button.danger-text:disabled { opacity: 0.55; background: #fafafa;
    border-color: var(--border); color: #9aa3af; }
  .grade-parent-empty { margin: 0; padding: 16px; border: 1px dashed var(--border);
    border-radius: 7px; color: var(--muted); font-size: 12px; text-align: center; }
  .wsb-menu.grade-parent-binding-menu { z-index: 75; min-width: 0; width: max-content; }
  .grade-parent-binding-menu .wsb-menu-item { padding: 6px 10px; white-space: nowrap; }
  .grade-parent-manager-actions { margin-top: 12px; }
  .modal.grade-modal.grade-parent-code-modal { width: 430px; }
  .grade-parent-qr-wrap { display: flex; flex-direction: column; align-items: center;
    gap: 9px; padding: 8px 0 4px; }
  .grade-parent-qr { width: 250px; height: 250px; object-fit: contain; padding: 8px;
    border: 1px solid var(--border); border-radius: 12px; background: #fff; }
  .grade-parent-qr-wrap p { max-width: 330px; margin: 0; color: var(--muted);
    font-size: 11.5px; line-height: 1.65; text-align: center; }
  .grade-parent-share-wrap { display: flex; flex-direction: column; align-items: center;
    gap: 8px; padding: 8px 0 4px; }
  .grade-parent-share-preview { display: block; width: min(100%, 360px); height: auto;
    max-height: 52vh; object-fit: contain; border: 1px solid var(--border-strong);
    border-radius: 10px; background: #fff;
    box-shadow: 0 2px 8px rgba(35, 58, 91, 0.12); }
  .grade-parent-share-wrap p { max-width: 360px; margin: 0; color: var(--muted);
    font-size: 11.5px; line-height: 1.65; text-align: center; }
  .grade-parent-code-modal .modal-actions { margin-top: 10px; }
  /* 「学生详情」那张卡**单独收窄**（2026-10-12 用户："太宽、右边一大片留白"）：
     内容只有三行短字段，383px 的通用宽度用不上。宽度按最宽的一行算 ——
     「联系电话」标签 4.5em(≈59px) + 间隔 10px + 11 位电话(等宽 ≈78px) + 左右内边距各 18px
     ≈ 183px，再放一点余量取 **240px**。左右内边距都走 .modal 的 18px，
     所以"左右留白相同"是天然成立的（不需要另写一条 padding）。
     ⚠️ 选择器三段（0,3,0）—— 压过上面的 `.modal.grade-modal { width: 383px; }`（0,2,0）。 */
  .modal.grade-modal.grade-stu-detail-modal { width: 240px; }
  /* 待确认那一条（导入回执弹窗里）：名字旁边一枚琥珀色小标 */
  .grade-imp-counts { display: flex; gap: 12px; margin: 2px 0 10px; font-size: 13px; }
  .grade-imp-counts b { font-variant-numeric: tabular-nums; }
  .grade-imp-ok { color: var(--a-color); }
  .grade-imp-new { color: var(--accent); }
  .grade-imp-warn { color: var(--b-color); }
  .grade-imp-group { margin: 0 0 8px; font-size: 12px; color: var(--muted); }
  .grade-imp-list { max-height: 240px; overflow: auto; display: flex;
    flex-direction: column; gap: 6px; }
  .grade-imp-row { display: flex; align-items: center; gap: 8px; padding: 6px 8px;
    border: 1px solid var(--border); border-radius: 6px; background: #fff; font-size: 13px;
    /* 同名多个 → 候选按钮会多、会换行（2026-10-13） */
    flex-wrap: wrap; }
  /* 名字**不许折行**（"张三"被拆成两行过，实测）——它是这一行的主信息，
     让旁边那句小字去换行。 */
  .grade-imp-row .grade-imp-name { flex: none; white-space: nowrap;
    font-weight: 600; color: #2c3444; }
  .grade-imp-row .grade-imp-why { flex: 1 1 auto; min-width: 0; color: var(--b-color);
    font-size: 11.5px; }
  /* 候选按钮组**右对齐**（2026-10-13）：名字在左、按钮在右，中间空着。
     `margin-left: auto` 顶到最右；同名多个时候选按钮会多起来 → 允许换行。 */
  .grade-imp-row .grade-imp-cands { margin-left: auto; display: flex; flex-wrap: wrap;
    gap: 6px; align-items: center; justify-content: flex-end; }
  .grade-imp-row .grade-imp-cands button { flex: none; }
  .grade-imp-row button { flex: none; }
  /* 二级「选择归属」的候选列表（2026-10-13 第二轮）：一行一位同名（姓名 + 电话 +
     「选这个」）。候选多了（>6 个）列表自己滚 —— 弹窗高度不会跟着长。 */
  .grade-pick-list { max-height: 240px; overflow: auto; display: flex;
    flex-direction: column; gap: 6px; }
  .grade-pick-row { display: flex; align-items: center; gap: 8px; padding: 6px 8px;
    border: 1px solid var(--border); border-radius: 6px; background: #fff; font-size: 13px; }
  .grade-pick-row .grade-pick-name { flex: none; font-weight: 600; color: #2c3444;
    white-space: nowrap; }
  .grade-pick-row .grade-pick-phone { flex: 1 1 auto; min-width: 0; color: var(--muted);
    font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .grade-pick-row button { flex: none; margin-left: auto; }
  /* ── 单加学生「待确认」的说明浮窗（2026-10-13 用户要求）────────────────────────
     原来那段说明写在弹窗**里面**（占卡片高度）。现在搬出来做成一个**常显小气泡**，
     像 tooltip 一样贴在弹窗上方（弹窗顶到屏幕边时由 JS 翻到下方 —— 见
     placeGradePendingHint）。三条要点：
       · fixed + z-index 78：压过弹窗那层（.grade-modal-layer = 70），
         低于二次确认（.modal-mask.confirm-mask = 80）；
       · pointer-events: none：它只是说明，别挡住底下的输入框 / 按钮；
       · 宽度由 JS 按弹窗宽度给（上限 560px），文字长了自己换行。 */
  .grade-hint-bubble { position: fixed; z-index: 78; pointer-events: none;
    box-sizing: border-box; padding: 8px 10px; border-radius: 8px;
    background: var(--panel); border: 1px solid var(--border-strong);
    color: #2c3444; font-size: 12px; line-height: 1.6;
    box-shadow: 0 6px 18px rgba(31, 36, 48, .14); }
  /* 指向弹窗的小箭头（默认朝下 = 气泡在弹窗上方） */
  .grade-hint-bubble::after { content: ''; position: absolute; left: 50%; bottom: -5px;
    width: 8px; height: 8px; transform: translateX(-50%) rotate(45deg);
    background: var(--panel);
    border-right: 1px solid var(--border-strong);
    border-bottom: 1px solid var(--border-strong); }
  /* 翻到弹窗下方时，箭头朝上 */
  .grade-hint-bubble.below::after { bottom: auto; top: -5px; border-right: none;
    border-bottom: none;
    border-left: 1px solid var(--border-strong);
    border-top: 1px solid var(--border-strong); }
  .grade-stu-empty { flex: 1 1 auto; display: flex; align-items: center;
    justify-content: center; color: var(--muted); font-size: 13px; }
  /* 弹窗里那排字段（新增学生）：与「新建班级」同一套 .grade-field，两列起排 */
  .grade-modal .grade-stu-form { display: grid; grid-template-columns: 1fr 1fr;
    gap: 8px 10px; margin: 0; }
  .grade-modal .grade-stu-form .grade-field-wide { grid-column: 1 / -1; margin-top: 0; }
  /* 待确认态按钮会变多（每个同名候选一颗「归到 XX」+ 取消 + 新建）→ 允许换行，
     否则 383px 的弹窗里会被挤出去（2026-10-13）。 */
  .grade-modal.grade-stu-modal .modal-actions { flex-wrap: wrap; }
  /* 邀请码（L7）：码用等宽字体（手抄 / 对眼睛都友好），有效期是个小下拉 */
  .invite-code {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 14px;
    letter-spacing: 1px;
    font-weight: 600;
    color: #1f2b3d;
    cursor: copy;
    user-select: all;
  }
  .invite-status.live { color: #1a7f37; font-weight: 600; }
  .invite-status.used { color: var(--muted); }
  .invite-status.revoked { color: #b42318; }
  .invite-status.expired { color: #b26a00; }
  .invite-ttl { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; color: #4a5464; }
  .invite-ttl select {
    font-family: inherit;
    font-size: 12.5px;
    padding: 3px 6px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #fff;
    color: #2c3444;
  }
  .invite-summary { font-size: 12.5px; color: #4a5464; margin: 0 0 8px; }
  .invite-summary b { color: #1f2b3d; }
  /* 胶囊里的数字：跟着胶囊自己的颜色走（选中态是白字 —— 别被上面那条 .invite-summary b
     的深色盖掉，那样在白底上一片糊）。tabular-nums 让 9→10 时胶囊宽度不跳。 */
  .invite-filter-count { font-variant-numeric: tabular-nums; opacity: 0.85; }
  /* 已作废 / 已过期：整行压暗。徽章颜色保留（为什么灰还看得出来）；
     **不**加 pointer-events:none —— 点码复制必须照旧能点。 */
  .invite-row-dead { opacity: 0.62; }
  /* "还没人用"：比正常文字淡一档，跟"被 X 用了"区分开（它不是动作，是占位） */
  .invite-unused { color: var(--muted); }
  .invite-revoke { margin-left: auto; color: #b42318; }
  /* 作废那一条（谁、什么时候）：从行尾往前排，跟"创建时间"分列 */
  .invite-revoked-note { margin-left: auto; }
  /* 邀请码行的副行有 4 段文字（创建 / 发送人 / 有效期 / 作废），比别的 journal 行挤 ——
     让它自己换行，别把内容挤出面板外（其它页签的 .journal-sub 不受影响）。 */
  .invite-row .journal-sub { flex-wrap: wrap; row-gap: 2px; }
  .primary-invite { border-color: var(--accent); color: var(--accent); font-weight: 600; }
  /* 胶囊式筛选（全部 / 下载 / 账号操作）：选中那格实心主色 */
  .admin-filter { display: inline-flex; gap: 5px; }
  .admin-filter button {
    padding: 3px 11px;
    border: 1px solid var(--border-strong);
    border-radius: 999px;
    background: #fff;
    color: #4a5464;
    font-family: inherit;
    font-size: 12.5px;
    cursor: pointer;
  }
  .admin-filter button:hover { background: var(--accent-soft); }
  .admin-filter button.active {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
    font-weight: 600;
  }
  /* 胶囊里的数字（用户管理那排筛选）：跟胶囊文字同色（选中态是白字）、等宽数字，
     9→10 时胶囊宽度不跳。 */
  .admin-filter .filter-count { font-variant-numeric: tabular-nums; opacity: 0.85; }
  /* 「已禁用」那颗有账号被禁用时变琥珀（与"已禁用"标签同一支颜色语言）——
     选中态（.active，主色实心）优先级更高，所以只在没选中时显示这层提示。 */
  .users-filter-warn { border-color: #f0d9a8; background: #fdf7ea; color: #96601a; }
  /* 审计行：类型徽章 + 操作者 + 右端字节数（与「最近操作」共用 .journal-row 那套骨架） */
  .audit-chip {
    flex: none;
    padding: 0 7px;
    border-radius: 8px;
    font-size: 11.5px;
    background: #eef4ff;
    color: #2f5fa8;
  }
  .audit-chip.account { background: #f1eefc; color: #5b46a8; }
  /* 「题库」那类（写盘 / 覆盖保存 / 改编 / 课测保存 / 待审与审核）：绿 —— 与下载（蓝）、
     账号（紫）分开，一眼看出这是"改了题库内容"的动作 */
  .audit-chip.content { background: #eaf7ef; color: #1a7f37; }
  .audit-who { color: #4a5464; font-size: 12px; }
  .audit-role { color: var(--muted); }
  .audit-bytes {
    margin-left: auto;
    color: var(--muted);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
  }
  /* 系统信息：宽屏第二行固定 5 张等宽卡片；上方 2/5 + 3/5 两块与下方列边界严格对齐。 */
  .sys-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    scrollbar-gutter: stable;      /* 卡片高度变化时面板宽度恒定（不横跳） */
    overscroll-behavior: contain;
  }
  .sys-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: 12px; align-items: stretch; }
  .sys-card {
    min-width: 0;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: #fbfcfd;
  }
  .sys-title {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0 0 8px;
    font-size: 12.5px;
    font-weight: 600;
    color: #2c3444;
  }
  .sys-title .mini-action { margin-left: auto; }
  .sys-title-tabs { margin-left: auto; display: inline-flex; align-items: center;
    gap: 2px; padding: 2px; border-radius: 6px; background: #eef1f5; }
  .sys-title-tabs button { height: 18px; padding: 0 7px; border: 0; border-radius: 4px;
    background: transparent; color: #6b7280; font-family: inherit; font-size: 10.5px;
    line-height: 18px; cursor: pointer; }
  .sys-title-tabs button.active { background: #fff; color: #334e68;
    font-weight: 600; box-shadow: 0 1px 2px rgba(51, 78, 104, 0.12); }
  .sys-row { display: grid; grid-template-columns: 132px minmax(0, 1fr); gap: 10px; padding: 3px 0; font-size: 12.5px; }
  .sys-key { color: var(--muted); }
  /* 路径很长：省略号 + 悬停看全（标题里有完整值） */
  .sys-val { color: #2c3444; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* 有东西该清的时候那格变琥珀色（与「已禁用」同一套颜色语言） */
  .sys-val-warn { color: #96601a; font-weight: 600; }
  /* 系统 tab 上半区：健康检查（左）与 题库健康度（右）并排 —— 题库那 8 个模块
     就能保持竖着一条一条排，比挤在整行里分两列清爽。窄屏自动落回上下。 */
  .sys-top {
    grid-column: 1 / -1;
    display: grid;
    /* 与第二行共用同一套 5 列轨道，再用 span 2 / 3 分区 —— 2fr/3fr 会把
       同一条列边界错开一个 gap，不能做到像素级对齐。 */
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: 12px;
  }
  .sys-top > .sys-card:first-child { grid-column: span 2; }
  .sys-top > .sys-card:last-child { grid-column: span 3; }
  @media (max-width: 1399px) {
    .sys-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .sys-top { grid-template-columns: minmax(0, 1fr); }
    .sys-top > .sys-card { grid-column: 1 / -1; }
    .sys-grid > .sys-card:last-child { grid-column: 1 / -1; }
  }
  @media (max-width: 760px) {
    .sys-grid { grid-template-columns: minmax(0, 1fr); }
    .sys-grid > .sys-card:last-child { grid-column: auto; }
  }
  /* 健康检查：一条一行（状态点 + 名称 + 人话说明），顶部一条总结 */
  .health-verdict {
    margin: 0 0 8px;
    padding: 7px 10px;
    border-radius: 6px;
    font-size: 12.5px;
    font-weight: 600;
  }
  .health-verdict-ok { background: #e9f7ee; color: #1f7a3f; }
  .health-verdict-warn { background: #fdeceb; color: #b42318; }
  .health-row {
    display: grid;
    /* 第 4 列是"快速处理"按钮（只有触发的那几项才有；没按钮的行这一格是空的，
       列宽 auto → 内容不会因此左右挪动，行高也不变） */
    grid-template-columns: 16px 112px minmax(0, 1fr) auto;
    gap: 10px;
    align-items: baseline;
    padding: 3px 0;
    font-size: 12.5px;
  }
  .health-dot { text-align: center; font-weight: 700; }
  .health-ok { color: #1f7a3f; }
  .health-warn { color: #b42318; }
  .health-info { color: var(--muted); }
  .health-key { color: #2c3444; }
  /* 检查结果**一律一行**（过长省略号 + 悬停看全）。
     这一条很重要：文案一变长就换行 → 卡片长高 → 下面两张卡跟着往下挪，看着就是"晃"。
     实测踩过：「注册 + 暴露」从短句切到长句时，健康检查卡整整长了一行。 */
  .health-text {
    color: #4a5464;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* 快速处理按钮：比 .mini-action 再矮一档（20px < 行高 21.3px），
     这样它出现/消失都不会把这一行（以及整张卡）撑高。 */
  .health-action {
    align-self: center;
    flex: none;
    padding: 1px 8px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #fff;
    color: var(--accent);
    font-family: inherit;
    font-size: 11.5px;
    line-height: 16px;
    white-space: nowrap;
    cursor: pointer;
  }
  .health-action:hover { background: var(--accent-soft); border-color: var(--accent); }
  .health-action:disabled { opacity: 0.5; cursor: default; }
  /* 全部缓存：分类勾选清理。复用站内 .modal，只补列表与汇总行。 */
  .modal.cache-clear-modal {
    width: min(600px, calc(100vw - 32px));
    box-sizing: border-box;
  }
  .cache-clear-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 16px;
    margin-bottom: 4px;
  }
  .cache-clear-head .modal-title { margin: 0; }
  .cache-clear-all {
    flex: none;
    padding: 1px 7px;
    border: 0;
    border-radius: 5px;
    background: transparent;
    color: var(--accent);
    font-family: inherit;
    font-size: 12px;
    cursor: pointer;
  }
  .cache-clear-all:hover { background: var(--accent-soft); }
  .cache-clear-all:disabled { opacity: 0.45; cursor: default; }
  .cache-clear-total {
    margin: 0 0 10px;
    color: var(--muted);
    font-size: 12px;
  }
  .cache-clear-list {
    display: grid;
    gap: 7px;
    margin-bottom: 11px;
  }
  .cache-clear-item {
    display: grid;
    grid-template-columns: 18px minmax(0, 1fr) auto;
    gap: 9px;
    align-items: start;
    padding: 9px 10px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: #fbfcfd;
    cursor: pointer;
    transition: border-color 0.15s ease, background 0.15s ease;
  }
  .cache-clear-item:hover { border-color: var(--border-strong); background: #fff; }
  .cache-clear-item.picked { border-color: #b8c9e8; background: #f7faff; }
  .cache-clear-item input {
    width: 15px;
    height: 15px;
    margin: 2px 0 0;
    accent-color: var(--accent);
    cursor: pointer;
  }
  .cache-clear-name { display: block; color: #2c3444; font-size: 13px; font-weight: 600; }
  .cache-clear-impact {
    display: block;
    margin-top: 3px;
    color: var(--muted);
    font-size: 11.5px;
    line-height: 1.45;
  }
  .cache-clear-size {
    padding-top: 1px;
    color: #4a5564;
    font-size: 11.5px;
    font-variant-numeric: tabular-nums;
    line-height: 1.45;
    text-align: right;
  }
  .cache-clear-summary {
    display: flex;
    justify-content: space-between;
    gap: 16px;
    margin: 0 0 12px;
    padding: 8px 10px;
    border-radius: 7px;
    background: #f4f6f9;
    color: #4a5564;
    font-size: 12.5px;
  }
  .cache-clear-summary b { color: #2c3444; font-weight: 650; }
  /* 题库健康度：上面几个大数字，下面 8 个顶层模块的条形分布 */
  /* 同理：这一排也只占一行。数字位数一变（0 → 14170）就换行的话，右卡会长高。 */
  .bank-metrics {
    display: flex;
    flex-wrap: nowrap;
    gap: 6px 18px;
    margin-bottom: 10px;
    min-width: 0;
    overflow: hidden;
  }
  .bank-metric {
    font-size: 12px;
    color: var(--muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .bank-metric b { font-size: 15px; color: #2c3444; margin-right: 4px; font-weight: 700; }
  .bank-metric-warn b { color: #96601a; }
  /* 8 个模块竖着一条一条（右卡只有半屏宽，竖排读起来最顺） */
  .bank-dist { display: grid; gap: 5px; }
  .bank-dist-row {
    display: grid;
    grid-template-columns: 76px 64px minmax(0, 1fr) 96px;
    gap: 10px;
    align-items: center;
    font-size: 12.5px;
  }
  .bank-dist-bar { height: 8px; border-radius: 4px; background: #eef2f7; overflow: hidden; }
  .bank-dist-bar > i { display: block; height: 100%; background: var(--accent); border-radius: 4px; }
  .bank-dist-num { text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; }
  /* 题库健康度下半部分：填充度 + 补题优先级（把"被左边撑高"多出来的那块用起来） */
  .bank-fill {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) 48px;
    gap: 10px;
    align-items: center;
    margin-top: 13px;
    font-size: 12.5px;
  }
  .bank-fill-label { color: #2c3444; }
  /* 填充度这条用**绿色**（--a-color 那支"正常 / 完成"的绿）：
     它跟上一条/下面那些蓝条说的不是一回事 —— 蓝条是"各模块题量占比"，
     这条是"整个题库填满了多少"，换个颜色一眼能分清。上面那 8 条**保持蓝色不变**。 */
  .bank-fill-bar { height: 10px; }
  .bank-fill-bar > i { background: var(--a-color); }
  .bank-fill-pct {
    text-align: right;
    font-weight: 600;
    color: var(--a-color);   /* 与进度条同一支绿，数字和条是一组 */
    font-variant-numeric: tabular-nums;
  }
  /* 备份（H1/H2/H3）：一块工具条 + 几张列表。样式沿用后台其它面板那套（字号 12.5、
     圆角 8、边框 --border），只多一个"主操作按钮"和类型徽章。 */
  .backup-head { display: flex; align-items: center; gap: 12px; flex: none; margin-bottom: 10px; }
  /* 「立即备份」是这一页的主操作：实心主色（与「返回题库工作台」同一档），
     但尺寸跟 .admin-refresh 一致，不喧宾夺主 */
  .backup-primary {
    padding: 6px 18px;
    border: 1px solid var(--accent);
    border-radius: 7px;
    background: var(--accent);
    color: #fff;
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
  }
  .backup-primary:hover { background: #2f6ae0; border-color: #2f6ae0; }
  .backup-primary:disabled { opacity: 0.55; cursor: default; }
  .backup-dir {
    margin-left: auto;
    font-size: 12px;
    color: var(--muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 60%;
  }
  .backup-list {
    /* flex:1 1 0（而不是 auto）：两块列表按**比例分**面板剩下的高度。
       用 auto 的话，"内容多的那块"会按内容撑开，把另一块挤成一条缝 ——
       44 行 .bak- 实测把「备份历史」挤到只剩表头。 */
    flex: 1 1 0;
    min-height: 150px;
    overflow-y: auto;
    scrollbar-gutter: stable;
    overscroll-behavior: contain;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: #fff;
  }
  .backup-row {
    display: grid;
    /* 五列：文件名（弹性）/ 类型 / 大小 / 时间 / 操作（删除 + 还原两个按钮，108px 够放） */
    grid-template-columns: minmax(200px, 2.2fr) 78px 90px 160px 108px;
    gap: 10px;
    align-items: center;
    padding: 7px 12px;
    border-bottom: 1px solid var(--border);
    font-size: 12.5px;
  }
  .backup-row:last-child { border-bottom: none; }
  .backup-row:not(.backup-row-head):hover { background: #f7f9fc; }
  .backup-row-head {
    position: sticky;
    top: 0;
    z-index: 2;
    background: #f7f9fb;
    color: var(--muted);
    white-space: nowrap;
  }
  .backup-row-head > span { position: relative; }
  .backup-kind-head { display: inline-flex; align-items: center; }
  .backup-kind-toggle {
    appearance: none;
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    cursor: pointer;
  }
  .backup-kind-toggle:hover,
  .backup-kind-toggle.active { color: var(--accent); }
  .backup-kind-toggle.active { font-weight: 600; }
  .backup-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .backup-kind { display: inline-block; padding: 0 7px; border-radius: 8px; font-size: 11.5px; }
  .backup-kind.manual { background: #eef4ff; color: #2f5fa8; }
  /* 老的 daily 包还会被列出来（用户自己 rm 之前），标签保留那一档绿色 */
  .backup-kind.daily { background: #eaf7ee; color: #1f7a3f; }
  .backup-kind.weekly { background: #f1eefc; color: #5b46a8; }
  .backup-kind.other { background: #f2f4f7; color: #6b7687; }
  .backup-when { color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
  /* 备份列表每行两个动作：删除（不可恢复 → 红）与还原（激进路径 → 琥珀，与"禁用"同一套警示色）。
     两个按钮都窄一档，跟用户管理那边的小按钮同一尺寸语言。 */
  .backup-actions { display: inline-flex; gap: 6px; justify-self: end; }
  /* 表头标签与右侧两个按钮组同宽并居中，标题中心正好落在两按钮中间。 */
  .backup-actions-head {
    display: grid;
    grid-template-columns: repeat(2, 48px);
    gap: 6px;
    justify-self: end;
    align-items: center;
  }
  .backup-actions-head-label {
    grid-column: 1 / -1;
    text-align: center;
    white-space: nowrap;
  }
  .backup-act {
    width: 48px;
    min-width: 48px;
    padding: 2px 0;
    border-radius: 6px;
    border: 1px solid var(--border-strong);
    background: #fff;
    font-family: inherit;
    font-size: 12px;
    white-space: nowrap;
    cursor: pointer;
  }
  .backup-act-del { color: #b42318; border-color: #e7b4ae; }
  .backup-act-del:hover { background: #fdeceb; border-color: #b42318; }
  .backup-act-restore { color: #96601a; border-color: #ecd5a8; }
  .backup-act-restore:hover { background: #fdf7ea; border-color: #d99000; }
  .backup-act:disabled { opacity: 0.5; cursor: default; }
  .backup-log-link { text-decoration: none; white-space: nowrap; }
  .modal-mask.restore-log-mask { z-index: 80; }
  .modal.restore-log-modal {
    width: min(900px, calc(100vw - 40px));
    height: min(620px, calc(100vh - 40px));
    max-width: none; max-height: none;
    padding: 16px 18px 12px;
    display: flex; flex-direction: column;
  }
  .restore-log-head { flex: none; display: flex; align-items: center; gap: 12px; }
  .restore-log-head .modal-title { flex: 1 1 auto; margin: 0 0 12px; }
  .restore-log-text {
    flex: 1 1 auto; min-height: 0; margin: 0; padding: 10px 12px;
    overflow: auto; scrollbar-gutter: stable;
    border: 1px solid var(--border); border-radius: 8px;
    background: #f7f9fb; color: #2c3444;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12px; line-height: 1.6;
    white-space: pre-wrap; overflow-wrap: anywhere;
  }
  .restore-log-error {
    flex: 1 1 auto; margin: 0; padding: 14px;
    border: 1px solid #efc4bf; border-radius: 8px;
    background: #fdf3f2; color: var(--c-color); font-size: 12.5px;
  }
  .restore-log-actions { flex: none; margin-top: 10px; }
  /* 「还原中…」那条提示：还原会把服务换掉、页面多半也刷不动了，所以这条要显眼且不消失。
     用**浮层**（fixed）而不是占位的一行字：它一出现就把下面的备份列表顶下去 30 多像素，
     看着就是卡片"晃"了一下 —— 这一页的列表和 .bak- 那块都不能被它推动。 */
  .backup-restore-note {
    position: fixed;
    top: 96px;                 /* 比全局飘层低一档，两条同时出现也不会叠在一起 */
    left: 50%;
    transform: translateX(-50%);
    z-index: 75;
    max-width: 72vw;
    margin: 0;
    padding: 9px 14px;
    border: 1px solid #ecd5a8;
    border-radius: 8px;
    background: #fdf7ea;
    color: #7a5310;
    font-size: 12.5px;
    line-height: 1.7;
    box-shadow: 0 6px 20px rgba(20, 30, 50, 0.16);
  }
  .backup-size { text-align: left; font-variant-numeric: tabular-nums; white-space: nowrap; }
  /* 进行中的那行提示（"打包中…已 1 分 20 秒"）：跟着 button 一起看不抢眼 */
  .backup-progress { font-size: 12px; color: var(--accent); white-space: nowrap; }
  /* H2 快照状态条：一行里塞下"上次每日 / 上次每周 / 下次检查 / 保留策略" + 两个手动按钮 */
  .backup-snap {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 16px;
    flex: none;
    margin-bottom: 10px;
    padding: 8px 12px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: #fbfcfd;
    font-size: 12px;
    color: #4a5464;
  }
  .backup-snap-title { font-weight: 600; color: #2c3444; }
  .backup-snap-item { white-space: nowrap; }
  .backup-snap-off { color: #96601a; }
  .backup-snap-actions { margin-left: auto; display: inline-flex; gap: 8px; }
  /* .bak- 清单：分组头做成 sticky，滚的时候知道自己在看"可删"还是"还原点" */
  .bak-list {
    flex: 1 1 0;
    min-height: 180px;
    overflow-y: auto;
    scrollbar-gutter: stable;
    overscroll-behavior: contain;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: #fff;
  }
  .bak-group {
    position: sticky;
    top: 0;
    z-index: 2;
    padding: 6px 12px;
    background: #f7f9fb;
    border-bottom: 1px solid var(--border);
    color: var(--muted);
    font-size: 12px;
  }
  .bak-row {
    display: grid;
    grid-template-columns: 26px minmax(220px, 1fr) 90px 150px;
    gap: 10px;
    align-items: center;
    padding: 6px 12px;
    border-bottom: 1px solid var(--border);
    font-size: 12.5px;
    cursor: pointer;
  }
  .bak-row:last-child { border-bottom: none; }
  .bak-row:hover { background: #f7f9fc; }
  .bak-row-readonly { cursor: default; color: var(--muted); }

  /* 注：`.workspace-row / -name / -role / -state / -dips / -actions / -chip`（2026-10-05 的
     "工作空间页签账号行"）在 2026-10-06「工作空间权限页表格化」之后**全仓 0 用处**，已删除；
     那一页现在用下面这套通用的 .perm-head / .perm-row（两页共用）。 */
  /* 工作空间的两个子页签：复用 .ds-tabs 的外观，只补一点与下方内容的间距 */
  .workspace-subtabs, .perm-subtabs { margin-bottom: 10px; }
  /* 「授权」= 绿（建设性动作）、「撤权」= 红（破坏性动作）：一眼分得开，不会点错 */
  .admin-refresh.ws-grant { background: #f2fbf4; border-color: #bfe3c8; color: var(--a-color); }
  .admin-refresh.ws-grant:hover { background: #e6f6ea; border-color: var(--a-color); color: var(--a-color); }
  .admin-refresh.ws-revoke { background: #fdf3f2; border-color: #f0c8c4; color: var(--c-color); }
  .admin-refresh.ws-revoke:hover { background: #fbe9e7; border-color: var(--c-color); color: var(--c-color); }
  /* 「我的工作区」文件浏览（2026-10-05 第 1 步）：图标网格 + 进出层 + 面包屑。
     类名一律 wsb-*（workspace browse），与上面的 workspace-* / bak-* 零重叠。 */
  .wsb { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
  /* 顶栏：一行面包屑（几段可点的 span + 分隔符），层数多了就换行 */
  .wsb-bar { flex: none; display: flex; align-items: center; gap: 2px; flex-wrap: wrap;
    padding: 2px 2px 8px; border-bottom: 1px solid var(--border); }
  .wsb-crumb { display: inline-flex; align-items: center; gap: 4px; max-width: 220px;
    padding: 2px 6px; border-radius: 6px; color: var(--accent); font-size: 12.5px;
    cursor: pointer; }
  .wsb-crumb:hover { background: #eef1f6; }
  .wsb-crumb.current { color: #2c3444; font-weight: 600; cursor: default; }
  .wsb-crumb.current:hover { background: transparent; }
  .wsb-crumb-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .wsb-sep { color: var(--muted); font-size: 12px; }
  /* 内联 SVG 图标（24×24 viewBox、currentColor，跟随文字色）—— 全仓第一处 SVG */
  .wsb-ico { width: 15px; height: 15px; flex: none; color: #4b6cae; }
  /* 图标网格（2026-10-05 第 4 步：任意槽位放置）。
     列数**固定 13**（不随窗口变）—— "第 r 行第 c 列"在任何窗口尺寸下都是同一格；
     窄屏放不下就横向滚动（跟 Finder 一致），**不重排**。
     grid-auto-rows 给一个最小行高：**整行全空的"空洞"也要占住那一行的高度**，
     否则空洞被压扁、下面的项会往上跳（实测：不加最小行高时第 3 行 y 从 240 掉到 136）。 */
  .wsb-grid { flex: 1 1 auto; min-height: 0; overflow: auto; align-content: start;
    display: grid; grid-template-columns: repeat(13, minmax(112px, 1fr));
    grid-auto-rows: minmax(104px, auto);
    gap: 10px; padding: 12px 2px; position: relative; }
  .wsb-item { display: flex; flex-direction: column; align-items: center; gap: 6px;
    padding: 12px 8px 10px; border: 1px solid transparent; border-radius: 8px;
    background: transparent; font-family: inherit; text-align: center; cursor: pointer;
    /* 显式 line-height：改名时该项会从 button 标签换成 div 标签，而 button 有 UA 的
       line-height:normal、div 会继承页面的 1.7 —— 不定死的话换标签就会长高（实测 +6.25px）。 */
    line-height: 1.35; }
  .wsb-item:hover { background: #f2f5fa; }
  .wsb-item.selected { border-color: var(--accent); background: var(--accent-soft); }
  /* 网格里图标放大到 40px；目录用主色系、文件用灰（目录 / 文件一眼分得开） */
  .wsb-item .wsb-ico { width: 40px; height: 40px; }
  .wsb-item.file .wsb-ico { color: #8a93a3; }
  .wsb-name { max-width: 100%; font-size: 12.5px; color: #2c3444; line-height: 1.35;
    word-break: break-all; display: -webkit-box; -webkit-line-clamp: 2;
    -webkit-box-orient: vertical; overflow: hidden; }
  .wsb-item.selected .wsb-name { color: var(--accent); font-weight: 600; }
  /* 期5b：网格项的「来自谁」小胶囊（格子里窄 → 只显示账号名，全称在 title 里）。
     期7 起网格改用**角标图标**（下面 .wsb-mark-badge），这颗文字胶囊只剩「列表视图」在用。 */
  .wsb-badge {
    flex: none; margin-top: -2px; padding: 0 6px;
    border: 1px solid #cdddf7; border-radius: 999px;
    background: var(--accent-soft); color: var(--accent);
    font-size: 10.5px; line-height: 15px; white-space: nowrap;
    max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  }
  /* 期7 角标（网格视图）：图标外包一层 relative，角标绝对定位到**右上角**。
     14×14 的内联 SVG + 1px 白圈底（压在文件夹图标上也要看得清）；只读=灰、可编辑=主色。
     放在图标那一层，所以**不影响** .wsb-item 的高度（名字 / 输入框的布局一行没动）。 */
  .wsb-mark { position: relative; display: inline-flex; width: 40px; height: 40px;
    align-items: center; justify-content: center; }
  .wsb-mark-badge {
    position: absolute; top: -5px; right: -7px;
    padding: 1px; border-radius: 50%; background: #fff;
  }
  .wsb-mark-badge.is-read { color: var(--muted); }
  .wsb-mark-badge.is-write { color: var(--accent); }
  /* 分享标记·方案A（网格）：名字下面那行「来自 LSM_EDU」——
     比名字小一档、muted 色；不换行 + 省略号，长账号名（如「浮沉的冬季」）也撑不破格子。
     它是**新增的一行文本**，所以格子会比原来高一点（网格行高是 minmax(104px, auto)，
     同一行里最高的那个说了算），这是"要看到分享人"必须付的代价。 */
  .wsb-from {
    max-width: 100%; font-size: 10.5px; line-height: 1.3; color: var(--muted);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /* 网格拖拽（2026-10-05 第 4 步）：源项半透明；落点画成一个**绝对定位的幽灵格**
     盖在目标槽位上。为什么不像以前那样用 .dropbefore::before 的竖缝：那是"线性插入"
     的视觉，而现在是二维槽位；而且空槽在 DOM 里**没有对应元素**，伪元素挂不上去。
     幽灵格跟着 .wsb-grid 的内容一起滚（absolute 相对 padding box）。 */
  .wsb-item.dragging { opacity: 0.45; }
  .wsb-ghost-cell { position: absolute; box-sizing: border-box; pointer-events: none;
    border: 2px dashed var(--accent); border-radius: 8px; background: var(--accent-soft);
    opacity: 0.75; }
  /* 行内改名输入框：**绝对定位覆盖在名字那一格上**，名字 span 保留（只 visibility:hidden）
     → 编辑中不改任何布局，行高/格高一个像素都不动（"不跳位"的硬要求）。
     网格：名字首行 y = padding-top 12 + 图标 40 + gap 6 = 58；左右 = 格子内边距 8。
     列表：文字起点 x = 图标 15 + gap 6 = 21；上下铺满名字那格（高度由隐藏的名字撑住）。 */
  .wsb-item, .wsb-row-name { position: relative; }
  .wsb-ghost { visibility: hidden; }
  .wsb-rename { border: 1px solid var(--accent); border-radius: 5px; background: #fff;
    color: #2c3444; font-family: inherit; font-size: 12.5px; line-height: 15px;
    padding: 0 4px; outline: none; box-sizing: border-box; text-align: center;
    min-width: 0; }
  /* 网格改名：宽度跟着卡片内宽走，长名字在框内换行（不再横向展开）。
     field-sizing 负责现代浏览器的高度；JS 的 fitWorkspaceRenameInput 给旧浏览器兜底。 */
  .wsb-item .wsb-rename {
    position: absolute; top: 58px; left: 8px; right: 8px;
    width: auto; min-width: 0;
    height: auto; min-height: 17px;
    padding: 2px 6px;
    line-height: 16px;
    white-space: pre-wrap; word-break: break-all;
    resize: none; overflow: hidden;
    z-index: 6;
    field-sizing: content;
  }
  .wsb-row-name .wsb-rename { position: absolute; left: 21px; right: 0; top: 0; bottom: 0;
    text-align: left; }
  /* 网格右键菜单（2026-10-05）：fixed 贴鼠标点；贴边翻向内侧（见 workspaceContextMenu）。
     z-index 60：在网格之上、模态遮罩（50）之上，但低于二次确认那档（80）。 */
  .wsb-menu { position: fixed; z-index: 60; min-width: 96px; padding: 4px;
    border: 1px solid var(--border-strong); border-radius: 8px; background: #fff;
    box-shadow: 0 6px 20px rgba(16, 24, 40, 0.16); }
  .wsb-menu-item { display: block; width: 100%; padding: 6px 14px; border: 0;
    border-radius: 6px; background: transparent; color: #2c3444; font-size: 12.5px;
    font-family: inherit; text-align: left; cursor: pointer; white-space: nowrap; }
  .wsb-menu-item:hover { background: var(--accent-soft); color: var(--accent); }
  /* 保存面板的右键菜单（.picker-menu 借用上面这套外观）：到 6 层上限时「新建文件夹」置灰 */
  .wsb-menu-item:disabled { opacity: 0.5; cursor: not-allowed; }
  .wsb-menu-item:disabled:hover { background: transparent; color: #2c3444; }
  /* 破坏性菜单项（目录树「删除该…」）：借站内已有的 --c-color（红）——
     「删除」跟普通项的蓝 hover 分开，一眼看出这一下不可逆。 */
  .wsb-menu-item.danger { color: var(--c-color); }
  .wsb-menu-item.danger:hover { background: #fdecea; color: var(--c-color); }
  /* 「分享」：紫色（站内 --j-color），与绿色导航、蓝色新建、红色删除区分。 */
  .wsb-menu-item.share { color: var(--j-color); font-weight: 600; }
  .wsb-menu-item.share:hover { background: #f3eefc; color: var(--j-color); }
  /* 「新增型」菜单项（班级卡右键的「新建测试」）：**蓝字 + 加粗**，排在最上面。
     与删除那支红分开：一支是"加东西"，一支是"不可逆"。 */
  .wsb-menu-item.accent { color: var(--accent); font-weight: 600; }
  .wsb-menu-item.accent:hover { background: var(--accent-soft); color: var(--accent); }
  /* 「录入型」菜单项（表头右键的「快速录入」）：**绿字**，排最上面。
     站内没有单独的"成功绿"变量 —— 绿就是 `--a-color`（#1a7f37，"今日新增"、
     难度 A、`.handout-note.ok` 都用它），照旧不新造色。 */
  .wsb-menu-item.success { color: var(--a-color); font-weight: 600; }
  .wsb-menu-item.success:hover { background: #f2fbf4; color: var(--a-color); }
  /* 学生卡右键菜单：**必须画在班级管理那些居中弹层之上**（2026-10-12 实修）。
     `.wsb-menu` 通用档是 60，而 `.grade-modal-layer`（花名册 / 新增学生 / 导入回执）
     是 70 —— 花名册里右键，菜单**确实建出来了**（DOM 里有"学生详情/删除"两项），
     但被弹窗整个盖住：实测那一点的 elementFromPoint 命中的是 `SPAN.grade-stu-name`，
     所以"右键没反应"。抬到 75：高于弹层（70）、低于二次确认那档（80）。 */
  .wsb-menu.grade-stu-menu { z-index: 75; }
  /* 「网格 / 列表」切换（2026-10-05 第 2 步）：外观借 .ds-tabs，只把它推到顶栏最右、
     并盖掉 .ds-tabs 自带的 align-self:flex-start（在 .wsb-bar 这行里要垂直居中）。 */
  /* 2026-10-13 用户改口径：面包屑**仍在最左**；「搜索框 + 胶囊 + 搜索按钮」整组挪到
     **「网格 / 列表」左边**。所以 `margin-left: auto` 从 .wsb-views 挪到 .wsb-search ——
     推右边的是"搜索那一组"，它后面紧跟网格/列表，三者贴在一起。 */
  .wsb-views { align-self: center; }
  /* 工作区搜索框：胶囊 + 输入框 + 搜索按钮整组借用真题卷集那套
     （.ds-search-wrap / .ds-scope-chip / .ds-search / .ds-search-go），宽度收一收 ——
     它和面包屑、网格/列表挤在同一行，太长会把面包屑顶没了。 */
  .wsb-search { margin-left: auto; flex: 0 1 240px; min-width: 140px; align-self: center; }
  .wsb-search .ds-search { flex: 1 1 auto; min-width: 0; }
  /* 工作区那颗「搜索」（2026-10-13 用户要求，四轮改到定稿）：
      ① **高度 / 字号 / 宽度**都与右边「网格」那块的白色底（.ds-tabs button.active）一致：
         实测白底那格 = 23px 高 / 12.5px 字 / 53px 宽（= 字宽 + 14px×2）；
         这颗带 1px 边框（box-sizing: border-box），所以内边距取 13px → **总宽也 53px**。
      ② **按钮外部**的左右留白（搜索框右边 → 按钮 → 网格白底）按用户定稿**不等**：
         **左 5px、右 8px**。`.wsb-bar` 自己的 `gap: 2px` 两边都在，所以这里再补
         `margin-left: 3px` + `margin-right: 6px` → 实测左边 5px、右边 8px。
         （写成 `margin: 0 6px 0 3px` 那种简写也行，但两边不一样，**分着写更好读**。）
     ⚠️ .ds-tabs 那几项哪天真改了字号，这里要跟着改。 */
  .wsb-bar .ds-search-go { height: 23px; padding: 3px 13px; font-size: 12.5px;
    margin-left: 3px; margin-right: 6px; }
  /* 搜索结果列表：沿用 .wsb-row 的观感，但排布换成"名字 + 它在哪"（列表视图那套
     列头 size/time 对搜索结果没意义）—— 只覆盖这一份，不动列表视图的 grid。 */
  .wsb-search-rows .wsb-row { display: flex; align-items: center; gap: 8px;
    cursor: pointer; }
  .wsb-search-rows .wsb-row-name { flex: 0 1 auto; min-width: 0; }
  .wsb-search-rows .wsb-search-path { margin-left: auto; flex: 0 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    color: var(--muted); font-size: 12px; }
  .wsb-search-empty { margin: 18px 10px; color: var(--muted); font-size: 13px; }
  /* 列表视图：表头 / 行**同一套 3 列 grid**（列才对得齐）；名称吃剩余、大小/时间固定。 */
  .wsb-list { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
  .wsb-head, .wsb-row { display: grid;
    grid-template-columns: minmax(0, 1fr) 96px 132px;
    align-items: center; column-gap: 12px; }
  .wsb-head { flex: none; padding: 4px 10px; border-bottom: 1px solid var(--border);
    background: #fbfcfd; }
  .wsb-rows { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 2px 0; }
  /* 列头：外观照抄 .ds-th / .ds-arrow（另起 wsb-* 一套，不借 ds-*） */
  .wsb-th { border: 0; background: none; padding: 0; font-family: inherit; font-size: 11.5px;
    color: var(--muted); cursor: pointer; display: inline-flex; align-items: center; gap: 2px;
    white-space: nowrap; }
  .wsb-th:hover { color: var(--accent); }
  .wsb-th:nth-child(2), .wsb-th:nth-child(3) { justify-self: end; }
  .wsb-arrow { color: var(--accent); font-weight: 600; }
  .wsb-row { width: 100%; padding: 6px 10px; border: 0; border-bottom: 1px solid #f0f2f5;
    background: transparent; font-family: inherit; font-size: 12.5px; text-align: left;
    cursor: pointer; line-height: 1.35; }   /* 同上：换 div 标签时行高不变 */
  .wsb-row:hover { background: #f7f9fc; }
  .wsb-row.selected { background: var(--accent-soft); }
  .wsb-row.selected .wsb-row-name { color: var(--accent); font-weight: 600; }
  .wsb-row-name { display: flex; align-items: center; gap: 6px; min-width: 0; color: #2c3444; }
  .wsb-row-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .wsb-row.file .wsb-ico { color: #8a93a3; }
  /* 大小 / 修改时间：右对齐 + 等宽数字（与表头同一列） */
  .wsb-row-size, .wsb-row-time { justify-self: end; text-align: right; color: var(--muted);
    font-variant-numeric: tabular-nums; white-space: nowrap; }
  /* 回收站视图（2026-10-05，只给 LSM_EDU）：另起 .wsb-trash-*，不动上面那批共享的 .wsb-*。
     列：原名 / 原路径 / 删除时间 / 大小 / 操作。骨架与 .wsb-head/.wsb-row 同一套 grid。 */
  .wsb-trash-head, .wsb-trash-row { display: grid;
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.4fr) 110px 80px 76px;
    align-items: center; column-gap: 12px; }
  .wsb-trash-head { flex: none; padding: 4px 10px; border-bottom: 1px solid var(--border);
    background: #fbfcfd; font-size: 11.5px; color: var(--muted); }
  .wsb-trash-head span:nth-child(3), .wsb-trash-head span:nth-child(4) { justify-self: end; }
  .wsb-trash-rows { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 2px 0; }
  .wsb-trash-row { padding: 6px 10px; border-bottom: 1px solid #f0f2f5; font-size: 12.5px; }
  .wsb-trash-row:last-child { border-bottom: none; }
  .wsb-trash-row:hover { background: #f7f9fc; }
  .wsb-trash-name { color: #2c3444; font-weight: 600; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  .wsb-trash-path { color: var(--muted); overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; }
  .wsb-trash-time, .wsb-trash-size { justify-self: end; color: var(--muted);
    font-variant-numeric: tabular-nums; white-space: nowrap; }
  .wsb-trash-restore { justify-self: end; }
  .wsb-trash-bar { flex: none; display: flex; align-items: center; gap: 8px;
    padding: 4px 2px 8px; border-bottom: 1px solid var(--border); }
  .wsb-trash-bar .wsb-trash-count { color: var(--muted); font-size: 12px; }
  /* 「工作空间」三个列表型子 tab 的**统一底部工具条**（2026-10-08）：
     左 = 统计灰字、右 = 按钮组（右对齐）；三个 tab 共用这一套，尺寸完全一致。
     贴底原理：外层容器是 flex column 且内容区 flex:1 1 auto（多余的空白全被内容区吃掉），
     工具条 flex:none 排在内容区之后 → 永远贴在**内容区底部**，不跟着列表滚动。 */
  .wsb-toolbar { flex: none; display: flex; align-items: center; gap: 8px;
    padding: 8px 2px 2px; border-top: 1px solid var(--border); margin-top: 8px; }
  .wsb-toolbar .wsb-toolbar-count { color: var(--muted); font-size: 12px; }
  .wsb-toolbar-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; }
  /* 空态 /「读取中…」那一行要**占满剩余高度**，否则工具条会往上飘、贴在文字下面
     （实测：其他工作区没有收到分享时，工具条落在 y≈311，而内容区底部在 683）。 */
  .wsb > .journal-empty { flex: 1 1 auto; }
  /* 子 tab 行（2026-10-08 第二次）：外层 .wsb-subtabs-row = 左（灰底胶囊 nav）+ 右（搜码框）。
     **整行固定高度 28px**（= .ds-tabs 的自然高度：2px padding×2 + 24px 按钮）——
     搜索框只在「其他工作区」渲染，但行高由这个固定值撑着，四个 tab 切换时页面一个像素都不跳。 */
  .wsb-subtabs-row { flex: none; display: flex; align-items: center; gap: 10px;
    height: 28px; margin-bottom: 10px; }
  /* ⚠️ 必须带 nav 前缀：`.workspace-subtabs, .perm-subtabs`（:5005）那条给了 margin-bottom:10px，
     而 .ds-tabs（:6069）给了 align-self:flex-start —— 同优先级时后写的赢，这里要高一级才行。 */
  nav.workspace-subtabs { align-self: center; margin-bottom: 0; }
  .subtab-search { display: flex; align-items: center; gap: 6px; margin: 0 0 0 auto; }
  /* 输入框尺寸对齐子 tab：**高度显式钉 24px**（= 实测 tab 按钮高度 24px；靠 padding 凑
     会差 1.5px），宽度只求"placeholder 显示得下"（实测 186px 刚好放下
     「输入分享码，例：ABCD2345」，scrollWidth ≤ clientWidth）。 */
  .subtab-search .share-search-input { flex: none; width: 186px; box-sizing: border-box;
    height: 24px; padding: 0 8px; font-size: 12.5px; letter-spacing: 0; }
  .subtab-search .admin-refresh { padding: 3px 12px; font-size: 12.5px; }
  /* 「接收」按钮（2026-10-08）：空输入 = 灰白禁用态（沿用 .admin-refresh 的原样）；
     有输入才点亮成**主色蓝底白字** —— 一眼能看出"现在可以点了"。
     只在 .subtab-search 作用域内生效，不动 .admin-refresh 的通用外观。 */
  .subtab-search .subtab-receive { background: var(--accent); border-color: var(--accent);
    color: #fff; font-weight: 600; }
  .subtab-search .subtab-receive:hover:not(:disabled) { background: #2a63d6; border-color: #2a63d6; }
  /* 两颗按钮整组靠右并列（只给**这一组** margin-left:auto —— 分别给每颗按钮会把它俩推散，
     实测就变成"刷新居中、清空贴右"）。 */
  .wsb-trash-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; }
  .bak-row-readonly:hover { background: transparent; }
  .bak-path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .bak-size { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
  .bak-dot { text-align: center; color: var(--muted); }
  .bak-actions { display: flex; justify-content: flex-end; padding: 8px 12px; }
  /* 删除是"真删文件"，用琥珀色警告档（与"已禁用"同一套颜色语言），不用主色 */
  .backup-danger {
    padding: 6px 16px;
    border: 1px solid #d99000;
    border-radius: 7px;
    background: #fff;
    color: #96601a;
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
  }
  .backup-danger:hover { background: #fdf7ea; }
  .backup-danger:disabled { opacity: 0.5; cursor: default; }
  .backup-danger:disabled:hover { background: #fff; }
  /* 备份页签是**两段**内容（上：整包备份 + 定时快照；下：题库里的 .bak-）。
     以前是两张独立的白卡、中间留一条缝；现在两块都在同一张卡里了，改用一条分隔线
     （同一个卡里再套一张卡反而糊）。只给第二段挂这个类，别的页签不受影响。 */
  .admin-panel-split { border-top: 1px solid var(--border); }
  .users-list {
    flex: 1 1 auto;
    min-height: 0;                 /* flex 子项默认 min-height:auto，不加这行滚不动 */
    overflow-y: auto;
    /* 始终预留纵向滚动条槽：行数变化导致滚动条出现/消失时，账号表与操作列不横向跳动。 */
    scrollbar-gutter: stable;
    overscroll-behavior: contain;
    margin: 6px 0 10px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: #fff;
    /* 2026-10-08：**去掉 max-width: 960px**。列宽改可拖之后这条上限会让
       「把分隔条拖到最右、左栏明明变宽了、表格却卡在 960」—— 左右分栏与表格宽度对不上。
       现在左栏多宽列表就多宽（列宽本身仍有自己的默认比例 + 可拖）。 */
  }
  /* 用户管理：左表格 + 右“这个账号的操作”。右侧给 ~42%，窄屏（<1190px）改成上下堆叠。 */
  .users-split {
    display: grid;
    /* 默认 1.15fr : 0.85fr；用户拖过分隔条之后由内联 `--users-cols` 接管（见 usersSplitStyle）。
       ★ 左栏下限 **768px**。九列内容最小宽 660 + 8 个间隙 64 + 行 padding 24
         + 始终预留的纵向滚动条槽 15 + 边框 2 = 765；取 768 留 3px 余量。
         旧值 758 是在没有 scrollbar-gutter 时测的，后来列表固定预留滚动条槽后少了 7px，
         展开审计时操作列会再次被裁；这个下限必须与 JS 的 USERS_LEFT_MIN 同步。
       ⚠️ 刻意用**变量**而不是直接写内联 `grid-template-columns`：下面
       `@media (max-width: 1200px)` 那条要能把两栏改成上下堆叠，若内联直接写死属性，
       媒体查询就再也盖不住了。 */
    grid-template-columns: var(--users-cols, minmax(768px, 1.15fr) minmax(320px, 0.85fr));
    gap: 14px;
    flex: 1 1 auto;
    min-height: 0;
    /* 2026-10-09：去掉这里的 margin-bottom:4px —— 另外三个子 tab 的内容容器到
       底部行是标准的 10px（.admin-foot 的 margin-top），只有这一页是 4+10=14px。
       去掉之后四页的"容器→底部行"间距一致，表格还多回 4px 高度。 */
  }
  /* 「用户管理」说明行（2026-10-09）：左 = 原来那句灰字说明、右 = 「展开/收起账号审计」按钮。
     挂一个**专属类**包一层，不动全站共用的 .admin-note（那一条被 7 个页签引用，
     直接改成 flex 会把审核队列 / 审计流水 / 备份那些页的说明行一起带偏）。
     ★ 2026-10-09 四页上沿对齐：这一行**固定高 22.5px + 下边距 8px**
       （22.5 = 右边那颗审计按钮的高度）。四个子 tab 的内容容器上沿因此都落在 246.5，
       切子 tab 不会上下跳（改前账号信息比目录权限低 23.6px）。
       代价：说明文字压成一行 + 省略号，全文在 title 里（悬停可看全）。
       ⚠️ 文字区 min-width:0 + ellipsis 才截得断（flex 子项默认不肯缩到内容宽以下）；
       右边那颗按钮靠 .mini-action 自带的 flex:none 保持完整、不被切。 */
  .users-note-row { display: flex; align-items: center; gap: 10px; flex: none;
    height: 22.5px; margin: 0 0 8px; }
  .users-note-row > .admin-note { flex: 1 1 auto; min-width: 0; margin: 0;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* 账号审计**收起**态（默认，2026-10-09）：右栏卡 + 分隔条一起退场，列模板整条换成单列。
     同「中栏调用区收起」（.layout.right-hidden）的做法：写 0 宽会留下 gap 的空档，
     而"空着第 2 列"会让 .users-split 的 min-content 把面板顶出横向滚动条。 */
  .users-split.audit-hidden { grid-template-columns: minmax(0, 1fr); }
  .users-split.audit-hidden .users-right,
  .users-split.audit-hidden .users-split-resizer { display: none; }
  .users-left, .users-right { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
  /* 左右两栏之间的拖手（2026-10-08）：骑在 14px 的栅格缝里（right:-14px = 正好铺满缝）。
     定位锚在 .users-left 上（所以要给它 position:relative）—— 这样左栏多宽它就跟到哪，
     不用随列模板重算位置。默认一条 2px 淡灰竖线（可发现），hover / 拖动中变主色 3px。 */
  .users-left { position: relative; }
  .users-split-resizer {
    position: absolute; top: 0; bottom: 0; right: -14px; width: 14px;
    cursor: col-resize; border-radius: 3px; background: transparent; z-index: 3;
    display: flex; align-items: center; justify-content: center;
  }
  .users-split-resizer::before {
    /* 2026-10-08：从 58% 改成 100% —— 拖手盒子本来就 top:0/bottom:0（满高），
       但里面那条可见竖线只有中间一段，看着像"缝里的一小截"。现在整条铺满两栏高度。 */
    content: ""; width: 2px; height: 100%; border-radius: 1px; background: var(--border);
  }
  .users-split-resizer:hover::before,
  body.users-split-resizing .users-split-resizer::before { width: 3px; background: var(--accent); }
  body.users-split-resizing { user-select: none; -webkit-user-select: none; cursor: col-resize; }
  .users-left > .users-list { margin: 0; }
  .users-right {
    padding: 8px 10px 10px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: #fbfcfd;
  }
  .users-right-head { display: flex; align-items: baseline; gap: 8px; flex: none; margin: 0 0 7px; }
  .users-right-title { margin: 0; font-size: 12.5px; font-weight: 600; color: #2c3444; }
  .users-right-sub { font-size: 11.5px; color: var(--muted); }
  /* 右侧一列记录：自己滚，样式与「审计流水」那张表同源（同一个 .journal-row 骨架） */
  .users-ops {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    scrollbar-gutter: stable;      /* 记录条数从少变多时，右栏文字不横跳 */
    overscroll-behavior: contain;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: #fff;
  }
  /* 这一栏比左边表格窄：一行放不下就换行（时间/动作一行，主体/对象一行的读法） */
  .users-ops .journal-main { flex-wrap: wrap; row-gap: 4px; }
  .users-ops .journal-row { padding: 8px 10px; }
  /* 选中那一行：沿用调用区卡片的选中态（主色左条 + 主色描边）+ 一层淡主色底；
     整行可点，所以给 pointer 光标。
     和调用区卡片同一条规矩：**整行是一个拖拽把手，行内文字一律不可选中** ——
     否则按住一拖，浏览器会顺手把用户名 / 时间 / 按钮文字拖成一片蓝色高亮
     （拖完还留着，看着很脏）。user-select 不影响任何点击行为。 */
  .users-row {
    cursor: pointer;
    user-select: none;
    -webkit-user-select: none;
    /* 2026-10-09：与三个权限页的行高统一到 37.5px。
       自己那一行（LSM_EDU）原来只 37.0 —— 它没有备注框、内容是"名字+标签"，
       比普通行（角色下拉 22px 撑起来）矮 0.5px；补一个 min-height 让两者齐平。 */
    min-height: 37.5px;
  }
  .users-row-selected {
    background: var(--accent-soft);
    box-shadow: inset 2px 0 0 var(--accent);
    outline: 1px solid var(--accent);
    outline-offset: -1px;
  }
  .users-row-selected:hover { background: var(--accent-soft); }
  /* 置顶行 = **唯一管理员 LSM_EDU**（W3 起固定第一，不能拖、不能改角色 / 禁用）。
     只做很轻的区分：淡底 + 一个「管理员」小标签；不削弱文字，因为它是主账号。 */
  .users-row-pinned { background: #fbfcfd; }
  .users-row-pinned:hover { background: #f5f8fc; }
  .users-tag-pinned { background: #eef2f7; color: #5b6675; }
  /* 拖动排序：能拖的行给 grab 光标；落点用一条主色横线标出来（跟调用区那套一致） */
  .users-row:not(.users-row-pinned) { cursor: grab; }
  body.users-dragging, body.users-dragging .users-row { cursor: grabbing; }
  /* Windows：同上换成四向箭头（白手在白底表格上看不清）；Mac 保持抓手脚不变。
     2026-10-10 改判据后这条**不再"走不到"**：用户管理只有管理员能进，而新判据只看系统
     —— 管理员在 Windows 上编辑表格时同样会吃到这条。 */
  body.is-win .users-row:not(.users-row-pinned),
  body.is-win.users-dragging,
  body.is-win.users-dragging .users-row { cursor: move; }
  .users-row-dragging { opacity: 0.45; }
  .users-row-dropbefore { box-shadow: inset 0 2px 0 var(--accent); }
  .users-row-dragdrop { box-shadow: inset 0 -2px 0 var(--accent); }
  /* 排序 / 筛选激活时不能拖（顺序由排序决定，筛出来的只是子集 —— 拖了会把没显示的那些
     挤出名单），光标换回普通 */
  .users-list.users-sorted .users-row,
  .users-list.users-filtered .users-row { cursor: pointer; }
  /* 表头「录入题数」是个可点的排序开关（数字列居中，箭头跟在字后面） */
  .users-head-sort { cursor: pointer; user-select: none; }
  .users-head-sort:hover { color: var(--accent); }
  .users-head-sort.on { color: var(--accent); font-weight: 600; }
  /* 窄屏：右侧面板落到表格**下方**（上下堆叠，不隐藏）。
     ★ 左栏下限 768 后，两栏布局最少要 768+14+320 = 1102 的内容宽；面板左右
       内边距约 89px，因此视口到 1191 左右才刚好放得下。断点取 1200，避免
       1191～1199 这段差几像素时仍强行并排、把操作列裁掉。 */
  @media (max-width: 1200px) {
    /* 堆叠时两行**按比例分掉面板剩下的高度**（而不是让内容各占各的自然高度）：
       这样窄屏下面板照样撑满、上下两块都有自己的滚动区，左表不会被压成一条缝。 */
    .users-split {
      grid-template-columns: minmax(0, 1fr);
      grid-template-rows: 1.05fr 0.95fr;
    }
    /* 上下堆叠时"左右两栏"不存在了 → 分栏拖手必须退场（否则一条竖线悬在两块面板中间） */
    .users-split-resizer { display: none; }
    /* 堆叠态 + 审计收起：只剩一块，别再按 1.05fr / 0.95fr 分两行
       （第二行没人，会留半屏空白） */
    .users-split.audit-hidden { grid-template-rows: minmax(0, 1fr); }
  }
  .users-empty { color: var(--muted); font-size: 12.5px; margin: 8px 10px; }
  /* 账号总览（阶段 4）：一行胶囊统计，放在说明下面、表格上面 —— 扫一眼就知道"几个账号、
     几种角色、有没有被禁用的、今天谁登录过"。数字跟着 /api/admin/users 走，不用额外请求。 */
  /* 这一行同时承担"操作结果提示"：胶囊靠左、提示靠右。**不许换行、固定一行高** ——
     换行或换行高都会把下面的表格顶下去（用户实测反馈的"卡片突然下移"）。 */
  .users-summary {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 6px;
    flex: none;
    min-height: 24px;
    margin: 0 0 10px;
    overflow: hidden;
  }
  .users-stat {
    flex: none;
    padding: 2px 10px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: #fbfcfd;
    color: #4a5464;
    font-size: 12px;
  }
  /* 操作结果那行字（改角色 / 禁用 / 删除成功 = 绿；出错 = 红）：
     占右侧剩下的宽度、右对齐；过长省略号（悬停有完整 title）。 */
  .users-notice {
    flex: 1 1 auto;
    min-width: 0;
    margin-left: auto;
    text-align: right;
    font-size: 12.5px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .users-notice.ok { color: var(--a-color); }
  .users-notice.error { color: var(--c-color); }
  .users-stat b { color: #2c3444; font-size: 12.5px; }
  /* 有账号被禁用时那颗变琥珀色（与"已禁用"标签同一套颜色语言） */
  .users-stat-warn { border-color: #f0d9a8; background: #fdf7ea; color: #96601a; }
  .users-stat-warn b { color: #96601a; }
  /* 2026-10-08：胶囊筛选组从表格上方**挪到**底部那一行（左=胶囊、右=刷新）。
     只覆盖这一页的 .admin-foot（通用那条是 justify-content:flex-end，别的页签只有一颗刷新按钮，
     改成 space-between 会把它们甩到左边去 —— 所以挂一个专属类，不动通用规则）。 */
  .admin-foot.users-foot { justify-content: space-between; }
  .users-foot .users-summary { margin: 0; }
  /* 2026-10-09：三个权限子 tab 的底部行也是"左筛选胶囊 + 右刷新" —— 同 .users-foot
     的做法，挂一个专属类只覆盖这一处，通用 .admin-foot（4621 的 flex-end）不动。 */
  .admin-foot.perm-foot { justify-content: space-between; }
  .perm-foot .perm-filter { flex-wrap: wrap; row-gap: 4px; }
  /* 审计流水：与用户管理一致 —— 左侧筛选胶囊、右侧刷新，列表占满剩余高度。 */
  .admin-foot.audit-foot { justify-content: space-between; align-items: center;
    flex-wrap: wrap; gap: 8px; }
  .audit-filter-cluster, .audit-foot-actions {
    display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  }
  .audit-filter-cluster { min-width: 0; }
  .audit-filter-control {
    height: 26px; box-sizing: border-box; padding: 2px 8px;
    border: 1px solid var(--border-strong); border-radius: 6px;
    background: #fff; color: #344054; font-family: inherit; font-size: 12px;
  }
  .audit-filter-control:focus { outline: none; border-color: var(--accent); }
  .audit-action-filter { width: 105px; }
  .audit-query-input { width: 160px; }
  .audit-operator-input { width: 92px; }
  .audit-reset-button { background: var(--inactive-bg); color: #4a5464; cursor: pointer; }
  .audit-reset-button:hover { background: var(--accent-soft); border-color: var(--accent);
    color: var(--accent); }
  .audit-reset-button:disabled { opacity: .55; cursor: default; }
  .audit-reset-button:disabled:hover { background: var(--inactive-bg);
    border-color: var(--border-strong); color: #4a5464; }
  .audit-foot-actions button { white-space: nowrap; }
  .audit-load-more-row { display: flex; justify-content: center; padding: 14px 0 6px; }
  .audit-load-more { min-width: 120px; }
  .admin-foot.invite-foot { justify-content: space-between; align-items: center;
    flex-wrap: wrap; row-gap: 8px; }
  .invite-foot .invite-summary { margin: 0; }
  .invite-create-row { margin-left: auto; display: flex; align-items: center;
    justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
  /* 表头与数据行共用同一套栅格 —— 改列宽只改这一处，两边永远对齐。
     列宽按内容定：账号弹性（用户名长短不一）、角色/时间按最长串给固定宽、状态与操作窄列。 */
  .users-head,
  .users-row {
    display: grid;
    /* 九列宽度（按内容定）：
       * 账号 / 备注 / 注册 / 最近登录 四条一起分弹性宽度（只让账号吃弹性它会涨到 200+，
         跟角色离得老远）；
       * 备注 78 起 —— 装得下"张三"这类两三个字的备注，也有富余显示"如：张三"这个占位符；
         有空间时它排在**第二顺位**长（账号之后）—— 它是给人看的标注，比时间列更值得占宽；
       * 角色 84 —— 下拉框本体缩到 74px（见 .users-role），列比它宽一点，框右边留出空白；
      * 录入题数 / 审核题数 各 58 —— 四个字的表头 ≈ 50px，排序箭头挂在字后面（挤一点没关系，
         箭头溢出的那几像素正好落在 8px 的栅格间距里）；
      * 操作 158 —— 要并排放下「重置密码 + 禁用 + 删除」三个按钮
        （62+4+42+4+42 = 154，留 4px 余量），这一列一像素没动；
      ★ 2026-10-08 二次瘦身（配合「列宽可拖」）：两个时间列 72→62、两个计数列 58→52，
        另把 账号 88→78 / 备注 78→70 / 角色 84→76 / 状态 58→52 —— 整表最小宽度
        814 → **662**，1280 窗口下的横向溢出 142px → **78px**（实测）。
        数字列与时间列再窄就放不下内容了（"09-29 09:45" 本身 ≈ 62px），到此为止。
       ⚠️ 加「备注」这一档时把其余列各让了一点（账号 -8 / 角色 -4 / 两个时间列各 -8 /
       两个计数列各 -6 / 状态 -10）：**整表的最小宽度必须留在加列之前那条线以内**
       （1600 宽的窗口里左栏只有 ≈844px；超出去就会长出横向滚动条、把最右边的「操作」截掉）。
       实测（1600×1000 窗口）：加列前 844-778=66px 富余 → 加列后 844-814=30px 富余，仍不横向溢出。 */
    grid-template-columns: minmax(78px, 1.2fr) minmax(70px, 0.9fr) 76px minmax(62px, 1fr) minmax(62px, 1fr) 52px 52px 52px 158px;
    align-items: center;
    gap: 8px;
    padding: 7px 12px;
    /* 全表一个字号：表头只用**颜色**分层级，不再靠"小一档"的字号 ——
       上一版表头 12px / 正文 12.5px / 按钮 12px 混着看，就像按钮和表头不是一套。 */
    font-size: 12.5px;
  }
  /* 表头：列表内部滚动时钉在最上面（sticky 挂在滚动容器里，不是挂在行上） */
  .users-head {
    position: sticky;
    top: 0;
    z-index: 2;
    padding-top: 8px;
    padding-bottom: 8px;
    background: #f7f9fb;
    border-bottom: 1px solid var(--border);
    color: var(--muted);
    white-space: nowrap;
  }
  /* 第 6 列往后（录入题数 / 审核题数 / 状态 / 操作）居中（表头与数据一起）——
     它们是"数字 / 数字 / 标记 / 动作"，窄列居中比左对齐稳 */
  .users-head > :nth-child(n + 6),
  .users-state,
  .users-power,
  .users-power-hold { text-align: center; }
  .users-state, .users-power, .users-power-hold { justify-self: center; }
  .users-entries { justify-self: center; text-align: center; }
  /* 表头每个单元格都要是**定位锚**：列宽拖手是 `position:absolute; right:-12px` ——
     没有这一条，它会相对最近的定位祖先（`.users-head` 自己是 sticky）算坐标，
     8 个拖手全叠在同一处（2026-10-08 加列宽拖拽时踩到）。`.perm-th` 早就有
     `position:relative`，而用户管理这张表的表头单元格是**裸 span、没有任何定位规则**，
     所以这里补一行。 */
  .users-head > span { position: relative; }
  /* 「审核题数」里"降级前审过"的那些：灰掉 —— 那个数不会再涨，跟现役审核人的数分开看 */
  .users-reviews.legacy { color: var(--muted); }
  /* 用户管理的列宽拖手：与 .perm-col-resizer 同一套观感（12px 热区骑在列边界上，
     平时透明、hover 显一条主色竖条）。差别只在类名 —— 免得两处互相影响。 */
  .users-col-resizer {
    position: absolute; top: 0; bottom: 0; right: -12px; width: 12px;
    cursor: col-resize; border-radius: 3px; background: transparent; z-index: 4;
    display: flex; align-items: center; justify-content: center;
  }
  .users-col-resizer::before {
    content: ""; width: 2px; height: 70%; border-radius: 1px; background: rgba(0, 0, 0, 0);
  }
  .users-col-resizer:hover::before { width: 3px; background: var(--accent); }
  .users-row { border-bottom: 1px solid var(--border); }
  .users-row:last-child { border-bottom: none; }
  /* hover 淡背景：比上一版稍明显一点，长表里扫行更清楚 */
  .users-row:hover { background: #f7f9fc; }
  /* 自己那一行：淡底 + 名字加粗（一眼看出"这行别动"） */
  .users-row-self { background: var(--accent-soft); }
  /* 自己那一行**没有备注框**（见模板里的 v-if）→ 空出来的那一格让账号名吃掉：
     名字格横跨「账号 + 备注」两列，标签（管理员 / 你自己）和名字才排得下。
     ⚠️ 只跨前两列，第 3 列起（角色 / 时间 / 计数 / 状态 / 操作）与别的行**逐像素对齐**
     —— 实现在同一套 9 列栅格上做 span，不是另起一套列。 */
  .users-row-self .users-name { font-weight: 600; grid-column: 1 / span 2; }
  /* 已禁用：整行灰掉 + 左边缘一条琥珀色 */
  .users-row-disabled { opacity: 0.62; box-shadow: inset 3px 0 0 #d99000; }
  .users-name { display: flex; align-items: center; gap: 5px; min-width: 0; }
  /* 用户名太长就省略号（悬停看全），不让它把角色下拉挤走 */
  .users-name-text {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .users-tag {
    font-size: 11px; padding: 0 5px; border-radius: 8px;
    background: var(--accent); color: #fff; flex: none;
  }
  .users-role {
    font-size: 12.5px; padding: 2px 4px; border-radius: 5px;
    border: 1px solid var(--border-strong); background: #fff;
    /* 下拉**显示框**比列窄：最长的选项「管理员」3 字 ≈ 67px（含箭头与内边距），74px 够用；
       框只占列左侧，右边留出的空白 + 栅格间距 = 与「注册时间」之间的呼吸空间。 */
    justify-self: start;
    width: 74px;
  }
  /* 备注列（2026-10-01）：一个**就地编辑**的输入框 —— 管理员给账号标真实身份。
     · 只有后台用户管理这一页有这个控件；用户自己那条线上显示的一律是用户名。
     · 平时看起来像"一格可以点的文字"（细描边、白底），聚焦时才亮主色描边，
       免得整张表被一堆输入框边框切碎。 */
  .users-note { min-width: 0; display: flex; }
  .users-note-input {
    /* 2026-10-09：从 `width:100%` 改成**固定外宽 64px**（12.5px 字号下正好装 4 个汉字：
       4×12.5 = 50 + 左右 padding 12 + 左右边框 2 = 64）。以前它是宽度的 100%，
       会把"备注"这一列的弹性（第 2 轨 minmax(70px, 0.9fr)）整份吃掉 ——
       实测左栏拖到 1100px 时输入框涨到 132.8px，又长又空。
       固定之后：左栏再宽，框也是 64px（左对齐，列内右侧留白属预期）。
       ⚠️ 不动第 2 轨的宽度（列宽拖拽仍是那一列自己的事）。 */
    width: 64px;
    min-width: 0;
    box-sizing: border-box;
    padding: 2px 6px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: #fff;
    color: #2c3444;
    font-family: inherit;
    font-size: 12.5px;
    /* 整行是"拖动排序 / 点行选中"的把手，行上写了 user-select:none；
       输入框里得把文字选回来（否则 Safari 里连改字都别扭）。 */
    user-select: text;
    -webkit-user-select: text;
  }
  .users-note-input::placeholder { color: #b7c0cc; }
  .users-note-input:hover { border-color: var(--border-strong); }
  .users-note-input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
  }
  .users-note-input:disabled { background: #f4f6f9; color: var(--muted); }
  /* 状态列：在线 / 离线。**只表示"此刻在不在"**，跟"账号能不能用"是两码事 ——
     "已禁用"挪到用户名旁边那个小标签上（见 .users-tag-disabled）。
     · 在线 = 绿（--a-color 那支"正常/完成"的绿），加粗一档，扫一眼就能数出几个人挂着；
     · 离线 = 中性灰。**不是错误**，所以不用红/琥珀 —— 大多数人本来就该是离线。 */
  .users-state { font-size: 12.5px; color: var(--muted); white-space: nowrap; }
  .users-state-on { color: var(--a-color); font-weight: 600; }
  .users-state-off { color: var(--muted); }
  /* 用户名旁边那个「已禁用」：琥珀胶囊（页面里"要注意但不是故障"那支颜色），
     与操作列里禁用按钮的琥珀描边同一套语言。 */
  .users-tag-disabled {
    background: #fdf1dc;
    color: #96601a;
  }
  /* 「用户管理」页签（2026-10-06 建；2026-10-09 合并成四个子 tab）：
     账号信息（原一级页签「用户管理」）+ 目录权限 / 工作空间权限 / 真题卷集权限，
     外观借 .ds-tabs；每段那句说明用 .perm-seg-note。
     两张表的骨架见下面那组 .perm-head / .perm-row（2026-10-06 起两页共用一套）。*/
  /* ★ 2026-10-09 与「账号信息」那行对齐：固定高 22.5px + 下边距 8px，文字单行 + 省略号
     （全文在各自的 title 里）。改前三张权限表的说明分别是 19.2 / 19.7 / 38.4，
     账号信息是 40.8 —— 四个子 tab 的内容容器上沿差最多 23.6px，切 tab 会上下跳。 */
  .perm-seg-note { height: 22.5px; margin: 0 0 8px; font-size: 12px; color: var(--muted);
    line-height: 22.5px; flex: none;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* 「用户管理」两张表的**通用**骨架（2026-10-06）：目录权限 5 列 / 工作空间权限 6 列，
     共用同一套表头、行、拖手与列宽逻辑，只有"默认列模板"和"列数"不同（见下面两条
     .perm-cols-*）。外观照抄真题卷集那套（.ds-paper-head / .ds-th / .ds-col-resizer），
     但**另起一套 .perm-\***：不动 .ds-*（那边是线上功能）。
     列模板全部 minmax(0, Nfr) —— 窄屏**挤压不横滚**（fr 的自动最小值是 min-content，
     不给 minmax(0,…) 就会把容器撑出横向滚动条）。 */
  .perm-head, .perm-row { display: grid; align-items: center; column-gap: 12px; }
  /* 目录权限（5 列）：账号吃剩余 / 角色 90 / 状态 90 / 内容吃剩余 / 操作 150。
     拖过之后由内联的 permColsStyle 覆盖这一条（两处都是"默认值"）。 */
  .perm-cols-dir { grid-template-columns: minmax(0, 12fr) 90px 90px minmax(0, 16fr) 150px;
    --perm-action-width: 40px; }
  /* 工作空间权限（6 列）：账号 / 角色 / 状态 / 内容 / **占用 120** / 操作 150（2026-10-06 拍板） */
  .perm-cols-ws { grid-template-columns: minmax(0, 12fr) 90px 90px minmax(0, 10fr) 120px 150px;
    --perm-action-width: 98px; }
  /* 真题卷集权限（5 列，2026-10-09）：账号 / 角色 / 状态 / 内容 / 操作。
     操作列要比另两个 tab 宽：那两页只有 1~2 颗按钮，这里一排是**三颗**
     （禁用 / 只读 / 可编辑，内边距与 .admin-refresh 同款 = 6px 18px）。 */
  .perm-cols-ds { grid-template-columns: minmax(0, 12fr) 90px 90px minmax(0, 16fr) 232px;
    --perm-action-width: 140px; }
  /* 班级管理权限（5 列，2026-10-13）：账号 / 角色 / 状态 / 内容 / 操作。
     操作列只要放**一颗**按钮（授权 or 撤权）→ 用 150px，与工作空间权限那页的观感一致。 */
  .perm-cols-grade { grid-template-columns: minmax(0, 12fr) 90px 90px minmax(0, 16fr) 150px;
    --perm-action-width: 40px; }
  /* 真题卷集档位：**三颗互斥按钮**（禁用 / 只读 / 可编辑），2026-10-09 从下拉框改过来。
     内边距与字号**照抄 .admin-refresh**（另两个 tab 操作列里就是它）—— 高度（32.5px）、
     左右留白（18px）两边完全同款，三张表的行高与按钮观感才对得齐。
     ⚠️ 三颗比另两个 tab 的 1~2 颗宽：操作列要相应加宽（见 .perm-cols-ds 的最后一列），
     否则会折行、把行高顶起来。 */
  .ds-lvl { display: inline-flex; gap: 4px; }
  .ds-lvl-btn {
    padding: 6px 18px; border: 1px solid var(--border); border-radius: 7px;
    background: #fff; color: var(--muted); font-family: inherit;
    font-size: 13px; font-weight: 400; cursor: pointer; white-space: nowrap;
  }
  .ds-lvl-btn:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
  .ds-lvl-btn:disabled { opacity: .5; cursor: default; }
  /* 选中态：底色 / 描边 / 字色三件套一起换；未选中的保持上面那套"灰"（用户要求"互斥、选中亮、
     两个灰"）。⚠️ 三种颜色都是站内已有的语言：中性灰 #e9edf2、只读蓝 --accent-soft + --accent、
     可编辑绿 #f2fbf4 + --a-color（与 .admin-refresh.ws-grant / .badge.diff-A 同一套）。 */
  .ds-lvl-btn.on { font-weight: 600; }
  .ds-lvl-btn.lvl-off.on { background: #e9edf2; border-color: #c3cbd6; color: #46505f; }
  .ds-lvl-btn.lvl-view.on { background: var(--accent-soft); border-color: #cdddf7; color: var(--accent); }
  .ds-lvl-btn.lvl-edit.on { background: #f2fbf4; border-color: #b7e0c2; color: var(--a-color); }
  /* 表头：左右 padding 必须与数据行**一致**（12px），否则列宽按 fr 重算会越往右越偏。
     ⚠️ 它必须待在 .bak-list **里面**并 sticky 顶住（见模板里的注释）：.bak-list 是滚动容器
     且 `scrollbar-gutter: stable`，会把内容盒收窄一条滚动条槽的宽度（实测 17px）——
     表头画在外面就会比行宽 17px，差值全堆在最右的操作列上（用户 2026-10-06 报的现象）。 */
  /* ★ 2026-10-09 表头与「账号信息」那一页对齐（原来只有 28.55 高，账号页是 38.25）：
     padding 4px 12px → **8px 12px**、底色 #fbfcfd → **#f7f9fb**、去掉 7px 上圆角
     —— 四项都与 .users-head（5483 起）逐项相同，高 28.55 → 38.25。
     代价：三张权限表的内容区各少 9.7px（.bak-list 是 flex 撑满剩下的高度）。 */
  .perm-head { position: sticky; top: 0; z-index: 1; padding: 8px 12px; background: #f7f9fb;
    border-bottom: 1px solid var(--border); }
  /* ⚠️ 表头单元格必须**拉满整列**（stretch）—— 否则 right:0 贴的是"文字右缘"，
     拖手会跟真实列边界差出很远。文字靠自己那格 justify-content 排。 */
  .perm-th { position: relative; display: inline-flex; align-items: center; gap: 2px;
    justify-self: stretch; justify-content: flex-start;
    font-size: 12.5px; color: var(--muted); white-space: nowrap; }
  /* 「账号」这一列表头（原来叫「全部账号」，2026-10-09 改名）：
     以前它单独加黑（fw600 / #2c3444 / 12px）以示"这是主列"，
     现在与其他表头一视同仁 —— 所以这一类**不再写任何样式**，直接吃 .perm-th 的
     11.5px / var(--muted)。类名保留（三个子 tab 的模板都挂着它），只当一个语义钩子。 */
  /* 「操作」这一列表头**必须和行里的按钮同侧**（**都居中**）：
     2026-10-06 曾把两边都改靠左（理由是"拖左边界时靠右的内容不动"），
     2026-10-09 用户要求改居中 —— 表头与行内按钮同时改（见下面 .perm-actions），
     两者仍同侧，所以当年那条"竖条走了、文字没动"的现象不会再出现。 */
  .perm-th-actions { justify-content: flex-end; }
  .perm-th-actions-label {
    width: var(--perm-action-width, 40px);
    text-align: center;
    white-space: nowrap;
  }
  .perm-actions-hold .perm-actions-hold-label {
    width: var(--perm-action-width, 40px);
    text-align: center;
    white-space: nowrap;
  }
  /* 列边界拖手：贴在表头单元格右缘、正好铺满列间缝（gap 12 ↔ right:-12 + width:12）。
     目录权限页**没有"编辑模式"**，所以它常显（不像真题卷集要先进设置模式）。 */
  .perm-col-resizer { position: absolute; top: 0; bottom: 0; right: -12px; width: 12px;
    cursor: col-resize; border-radius: 3px; background: transparent; z-index: 2;
    display: flex; align-items: center; justify-content: center; }
  /* 2026-10-09：可见的竖条**默认透明**（视觉上"没有分隔条"），hover / 拖动时才显形。
     照抄「用户管理」那套 .users-col-resizer::before 的正例，观感两边一致；
     热区宽度（12px）与 @pointerdown 一行没动 —— 照样拖得动。 */
  .perm-col-resizer::before { content: ""; width: 2px; height: 70%; border-radius: 1px;
    background: rgba(0, 0, 0, 0); }
  .perm-col-resizer:hover::before { width: 3px; background: var(--accent); }
  /* 2026-10-09：统一行高。行内按钮缩到 22.5 档之后，普通行由按钮撑到 37.5px，
   而 LSM_EDU 那一行**不画按钮**（v-if）→ 只有 36.3px，同一张表里两种行高。
   补一个 min-height 让两者齐平（37.5 也是「账号信息」页普通行的行高）。 */
  .perm-row { padding: 7px 12px; border-bottom: 1px solid var(--border);
    font-size: 12.5px; min-height: 37.5px;
    /* ★ 2026-10-09：三张权限表的行也能点了 —— 纯视觉高亮，**没有下游**
       （不像账号信息页点了会拉 /api/admin/audit 填右栏）。 */
    cursor: pointer; }
  .perm-row:last-child { border-bottom: none; }
  .perm-row:hover { background: #f7f9fc; }
  /* 置顶行 = 唯一管理员 LSM_EDU（2026-10-09）：对齐「账号信息」页 .users-row-self 的
     淡蓝底，四张表一眼就能认出主账号那一行。行根元素上挂 .perm-row-self（模板按
     isProtectedAdmin 判），名字的加粗仍归 .perm-name-admin。
     ⚠️ 必须再写一条 :hover —— `.perm-row:hover`（0,2,0）比 `.perm-row-self`（0,1,0）
     权重高，不补的话鼠标一划过这行淡蓝就没了（账号页 .users-row-selected:hover 同理）。 */
  .perm-row-self { background: var(--accent-soft); }
  .perm-row-self:hover { background: var(--accent-soft); }
  /* ★ 行选中（2026-10-09）：逐字对齐账号信息页的 .users-row-selected（5359‑5365）——
     淡蓝底 + 左侧 2px 主色内嵌条 + 1px 主色描边。
     ⚠️ 位置必须排在 .perm-row-self / .perm-row-self:hover **之后**：
     同权重（0,1,0）时靠源码顺序取胜，否则选中 LSM_EDU 那行会被 self 的底色规则压回去；
     而 `.perm-row:hover`（0,2,0）权重更高，所以还要补一条 `.perm-row-selected:hover`。 */
  .perm-row-selected { background: var(--accent-soft);
    box-shadow: inset 2px 0 0 var(--accent);
    outline: 1px solid var(--accent); outline-offset: -1px; }
  .perm-row-selected:hover { background: var(--accent-soft); }
  /* 账号名（2026-10-09）：**与「账号信息」页同款** —— 深色（var(--text)）、不加粗，
     只有受保护的唯一管理员 LSM_EDU 那一行加黑（.perm-name-admin，模板按
     isProtectedAdmin 挂类）。之前这里用的是灰字（var(--muted)），四页看着不像一套。 */
  .perm-name { color: var(--text); overflow: hidden; text-overflow: ellipsis; }
  .perm-name-admin { font-weight: 600; }
  /* ⚠️ 这两个格子必须能"缩到比文字还窄"：grid item 的自动最小尺寸 = min-content，
     而 `minmax(0, Nfr)` 只保证**轨道**能缩到 0 —— 轨道缩了、格子里是可见溢出的文字的话，
     溢出的部分会变成 .bak-list 的横向滚动条（用户要求"挤压不横滚"）。
     加 `overflow: hidden` 后这个 item 的自动最小尺寸变成 0，文字改为省略号。 */
  .perm-role { color: var(--muted); overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; }
  .perm-state { font-weight: 600; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; }
  .perm-state.on { color: var(--a-color); }
  .perm-state.off { color: var(--muted); font-weight: 400; }
  /* 权限内容：一串大类名，长了就省略号（hover 的 title 给全文） */
  .perm-stages { color: var(--muted); overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; }
  /* 「已开通 **但** 目录缺失」是**异常状态**（要点「初始化」补救），标红；未开通的"目录缺失"
     是正常的（本来就没目录，不标）。颜色用现有的危险色变量 --c-color，别写死。 */
  .perm-stages.is-warn { color: var(--c-color); font-weight: 600; }
  /* 空间占用（工作空间权限页）：等宽数字 + 省略号（"120.3 MB / 1.0 GB"） */
  .perm-usage { color: var(--muted); overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; font-variant-numeric: tabular-nums; }
  /* 与表头 .perm-th-actions 同侧（2026-10-09 起都**居中**）。
     `flex-wrap: wrap` + `min-width: 0`：工作空间页这一列有**两颗**按钮（授权/撤权 + 初始化），
     挤到放不下时**换行**而不是把整行撑出横向滚动条（同 .perm-role 那条的理由）。 */
  .perm-actions { display: flex; gap: 6px; justify-content: flex-end;
    flex-wrap: wrap; min-width: 0; }
  /* 行内小按钮（2026-10-09 四张表统一）：**只给行内按钮挂这个类**。
     尺寸对齐「账号信息」页行内那一档（.users-reset / .users-power / .users-danger）：
     12px 字号 / 400 字重 / 2px 7px 内边距 / radius 6 → 高 22.5px。
     挂它的地方：目录权限的「编辑」、工作空间权限的「授权/撤权 + 初始化」、
     真题卷集权限的「禁用/只读/可编辑」。
     ⚠️ 底部那颗「刷新」**不要挂**：它是 .admin-refresh 本体的 32.5 档（四个子 tab 已统一）。
     ⚠️ 必须写在 .ds-lvl-btn 本体**之后** —— 同权重（都是 0,1,0），靠源码顺序取胜。
     选中态 `.ds-lvl-btn.on{font-weight:600}`（0,2,0）仍压过这里 → 选中的那颗照旧加粗。 */
  .admin-btn-sm { font-size: 12px; font-weight: 400; padding: 2px 7px; border-radius: 6px; }
  /* 目录权限弹层：8 个复选框排成两列（四个学段 + 四个模块各一行更直观，
     但两列自适应最省事，窗口窄了自动变一列） */
  .stages-modal { min-width: 420px; max-width: 560px; }
  /* 2026-10-09：这条规则**只留真正生效的一条**。
     背景：原来它写的是 `margin:-4px 0 10px; font-size:12px; color:var(--muted)` —— 前两条
     在 `.modal` 里**从来没生效过**：`.modal p`（0,1,1）比 `.stages-note`（0,1,0）权重高，
     实测灰字吃的是 `margin:0 0 14px; font-size:13.5px`（只有 color 归它）。
     「目录权限」弹窗那句说明已随本轮删掉，但这个类还有**两处用户**：
       · 分享结果弹窗「把这个码发给对方…」（9670）
       · 接收确认弹窗「这条分享已经失效了…」（9723）
     所以只删掉两条死声明，**color 保留**（那两处靠它才是灰字）。 */
  .stages-note { color: var(--muted); }
  .stages-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 6px 10px;
    /* 2026-10-09：4px → 12px —— 与 `.modal` 的 padding-bottom(12px) 齐平，
       按钮行上下留白从此一样（改前实测：上 4 / 下 12）。 */
    margin-bottom: 12px;
  }
  .stages-item {
    display: flex; align-items: center; gap: 6px;
    padding: 5px 8px;
    border: 1px solid var(--border);
    border-radius: 7px;
    font-size: 12.5px;
    cursor: pointer;
  }
  .stages-item:hover { background: #f7f9fc; }
  .stages-item.picked { border-color: var(--accent); background: var(--accent-soft); }
  .stages-item input { margin: 0; }
  /* 「分享文件夹」弹窗（期3a）：权限 / 有效期两组沿用 .stages-item 那套胶囊，
     结果态换成一个大号分享码 + 复制按钮。只加这几个类，不动 .stages-* 本体。 */
  .share-modal { min-width: 380px; max-width: 460px; }
  /* 「分享文件夹」与「改期限」两个弹窗**共用**的这一套（2026-10-08）——只写这一份，别再各抄一份：
     ① `.share-grid` 三列**铺满**内容宽：原来是 `minmax(84px, max-content)`（三列各 84，共 272），
        而内容区有 344 → 右侧空出 72px，看着"右 padding 比左大"；铺满后左右都贴到 padding 内沿。
     ② 自定义天数框占第 2~3 列（`grid-column: 2 / 4`）→ 与「7 天 / 永久」共享同一条网格线，严格对齐。
     ③ 按钮行上下留白相等：上补 12，下本来就是 .modal 的 padding-bottom 12。 */
  .share-days-box {
    grid-column: 2 / 4; min-width: 0; min-height: 33px;   /* 33 = 胶囊高度，同一行等高 */
    display: flex; align-items: center; gap: 8px;
    padding: 5px 8px;
    border: 1px solid var(--border-strong); border-radius: 7px; background: #fff;
  }
  .share-days-box:focus-within { border-color: var(--accent); }
  /* 里面这颗 input 是"裸"的：描边/底色都交给外层容器，自己只管占满左边那段。
     `width:0 + flex:1 1 0` 是刻意的 —— number input 有约 20 字的固有宽度，
     不压掉的话会把网格里的 max-content 轨道撑宽，进而破坏上面那两列的对齐。 */
  .share-days-box input {
    flex: 1 1 0; width: 0; min-width: 0;
    padding: 0; border: 0; outline: none; background: transparent;
    font-size: 13px; color: inherit;
  }
  .share-days-box .share-days-unit { flex: none; font-size: 12.5px; color: var(--muted); }
  .share-modal .modal-actions { margin-top: 12px; }
  .share-modal .share-label {
    margin: 12px 0 6px; font-size: 12px; font-weight: 600; color: var(--muted);
  }
  /* 权限/有效期各占一行、宽度跟字数走（不像 .stages-grid 那样铺满整行） */
  .share-modal .share-grid {
    grid-template-columns: repeat(3, minmax(84px, 1fr));
    margin-bottom: 0;
  }
  .share-code-box {
    display: flex; align-items: center; justify-content: center; gap: 12px;
    margin: 4px 0 12px; padding: 12px 10px;
    border: 1px dashed var(--accent); border-radius: 8px; background: var(--accent-soft);
  }
  .share-code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 24px; font-weight: 700; letter-spacing: 3px; color: var(--accent);
    user-select: all;   /* 一键全选，方便手动复制 */
  }
  .share-copy {
    padding: 4px 10px; border-radius: 6px; border: 1px solid var(--border-strong);
    background: #fff; cursor: pointer; font-size: 12.5px;
  }
  .share-modal .share-meta { margin: 0 0 6px; font-size: 12.5px; color: #4a5564; }
  .share-modal .share-meta .share-meta-key { color: var(--muted); }
  /* 「分享管理」tab（期3b）：一条分享一张卡；失效的整卡置灰（改权限/改期限/关分享都不画） */
  /* 2026-10-08：两个列表型 tab 都改成"撑满 + 内部滚动"——底部工具条才能钉在内容区底部
     （内容区 flex:1 吃掉多余空白，工具条 flex:none 排在它后面）。 */
  .wsb-shares, .wsb-trash { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
  /* 2026-10-08（第三版）：**分享管理分左右两块**——左「有效」右「已关闭」，等宽并排（不叠）。
     两块各自是 flex column（标题 flex:none + 内容区 flex:1），所以高度天然一致、都撑到工具条上沿。
     ⚠️ 两个内容区都 `scrollbar-gutter: stable`：一边出滚动条时**预留**占位，
     既不会把本块内容挤窄，也不会让两块宽度不齐（这才是"防宽度跳"的关键）。 */
  .wsb-shares-split { flex: 1 1 auto; min-height: 0; display: flex; gap: 12px; align-items: stretch; }
  /* 2026-10-08（第四版）：两块各套一个**卡片容器** —— 用户截图反馈"左右两块视觉上没分界"。
     卡片外观对齐站内其它卡片：1px var(--border) 描边 + 10px 圆角 + 浅底 #fafbfc
     （比页面底 #f5f6f8 亮一档、比卡片里的白卡 #fff 暗一档 → 内外三层分得开）。
     标题放在**卡片内部**顶部（第一个子元素），不是挂在卡片外面。 */
  .share-col {
    flex: 1 1 0; min-width: 0; min-height: 0; display: flex; flex-direction: column;
    border: 1px solid var(--border); border-radius: 10px; background: #fafbfc;
    padding: 8px 10px 6px;
  }
  .share-col-title { flex: none; margin: 0 0 6px; font-size: 12.5px; font-weight: 600; color: #4a5564; }
  .wsb-shares-rows {
    flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;
    scrollbar-gutter: stable; padding: 2px 6px 2px 0;
  }
  /* 空态 / 读取中也占满剩余高度：不然工具条会往上飘、贴在文字下面 */
  .wsb-shares > .journal-empty, .wsb-trash > .journal-empty { flex: 1 1 auto; }
  .wsb-trash-rows { padding-right: 2px; }
  .share-row {
    border: 1px solid var(--border); border-radius: 9px;
    padding: 10px 12px; margin-bottom: 8px; background: #fff;
  }
  /* 已关闭 / 已过期：浅冷灰底（--inactive-bg）——原来的 #fafbfd 跟容器底 #fafbfc 差 1 个色阶，
     白卡一混就分不出"这条已经作废"。边框 / 圆角 / padding / 红胶囊都不动。 */
  .share-row.dead { background: var(--inactive-bg); border-color: #e6e9ef; }
  .share-row.dead .share-row-code { color: var(--muted); }
  /* 第一行 = 两栏：[左组：码/复制/胶囊] + [右：三颗按钮]，按钮贴右上角。

     为什么左组要写 `flex: 1 1 200px`（而不是 auto / 0）：
     ① 用 `auto`（=内容宽 ≈332px）时，带 wrap 的 flex 会**先换行、后收缩**，
        332+8+269=609 > 521 → 按钮整组被挤到第二行（实测卡片高 154 vs 114）。
     ② 改成 nowrap 能逼出收缩，但窄屏（900 实测）会把左组压到 56px —— 码都被切了。
     ③ 折中：**把基准宽定成 200px**。flex 的折行判断只看 flex-basis，
        200+8+269 = 477；够宽（卡片内容区 ≥477，即窗口 ≈1150 以上）就同排、
        左组再靠 flex-grow 吃满剩余；不够宽就整组按钮折到下一行、左组独占一行。
        两档都不挤：不会压坏码，也不会把按钮压变形。 */
  .share-row-head { display: flex; align-items: flex-start; gap: 8px; flex-wrap: wrap; }
  .share-row-left { flex: 1 1 200px; min-width: 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  .share-row-code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 17px; font-weight: 700; letter-spacing: 2px; color: var(--accent);
  }
  .share-chip {
    padding: 1px 8px; border-radius: 999px; border: 1px solid var(--border);
    background: #f7f9fc; color: #4a5564; font-size: 11.5px; white-space: nowrap;
  }
  .share-chip.is-active { border-color: #b7d2b8; background: #f0f8f0; color: #2f6b34; }
  .share-chip.is-closed, .share-chip.is-expired { border-color: #e2c9c6; background: #fdf3f2; color: var(--c-color); }
  /* 申请中（2026-10-10）：琥珀 —— 既不是"用上了"（绿）也不是"废了"（红），
     一眼能看出"还等分享者点一下"。用站内既有的 --a-color 那一档，不新造色。 */
  .share-chip.is-pending { border-color: #e6d3ae; background: #fdf8ec; color: var(--a-color); }
  .share-row-path { margin: 6px 0 0; font-size: 12.5px; color: #4a5564; word-break: break-all; }
  /* 三颗按钮挂在 .share-row-head 那一行里、靠右（margin-left:auto）。
     原来的 margin-top:8px 是"自己占一行"时的行距，现在并进第一行，靠 head 的 row-gap 兜底。 */
  .share-row-actions {
    display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end;
    margin-left: auto; flex: 0 0 auto;
  }
  .share-receivers { margin-top: 8px; border-top: 1px dashed var(--border); padding-top: 6px; }
  .share-receiver {
    display: flex; align-items: center; gap: 8px;
    padding: 3px 0; font-size: 12.5px;
  }
  .share-receiver-name { font-weight: 600; color: #4a5564; }
  .share-receiver-time { color: var(--muted); font-variant-numeric: tabular-nums; }
  .share-receiver .mini-action { margin-left: auto; }
  /* 2026-10-10：申请中那一行有**两颗**按钮（拒绝 / 通过）—— 每颗都带
     `margin-left:auto` 的话，flex 会把"多余空隙"摊到两颗之间，结果是两颗被扯到
     行的两端（实测：拒绝在 x≈430、通过在 x≈685，中间空一大片），和别的行的
     「删除」对不齐。这里只让**紧跟名字/胶囊的第一颗**吃 auto 边距 ——
     两颗就贴在一起、整组落在行尾，与「删除」那一列对齐。 */
  .share-receiver .mini-action + .mini-action { margin-left: 0; }
  /* 虚线**常显**（2026-10-08）：原来虚线只是 .share-receivers 的 border-top，没人接收时整块不渲染
     → 虚线跟着消失。现在把同一条虚线也画给 .share-row-empty —— 这两个分支二选一渲染，
     合起来就是"不管有没有接收人，虚线永远在"，且文案都在线的**下方**。 */
  .share-row-empty {
    margin: 8px 0 0; border-top: 1px dashed var(--border); padding-top: 6px;
    font-size: 12px; color: var(--muted);
  }
  /* 空态第二行（"在文件夹上右键 → 分享…"）：跟上面那句贴一起，别各占 14px padding */
  .share-empty-hint { padding-top: 0; }
  /* 「其他工作区」搜码（期4）：输入框跟 .handout-row input 同一套外观，宽度自己占满 */
  .share-search { flex: 1 1 auto; min-width: 0; display: flex; gap: 8px; margin: 0; }
  .share-search-input {
    flex: 1 1 auto; min-width: 0; padding: 5px 8px;
    border: 1px solid var(--border-strong); border-radius: 6px;
    font-size: 13px; outline: none; background: #fff;
    text-transform: uppercase;   /* 码是大写字母表：边打边变大写（JS 也兜一道） */
    letter-spacing: 1px;
  }
  .share-search-input:focus { border-color: var(--accent); }
  .share-chip.is-bad { border-color: #e2c9c6; background: #fdf3f2; color: var(--c-color); }
  /* 禁用 / 启用：一整列的窄按钮。禁用态用琥珀色描边（与"已禁用"标签同色），
     启用态是普通灰描边 —— 一眼能看出"这一行现在能不能用"。 */
  .users-power {
    /* 操作列要并排放下两个按钮：字号 / 内边距 / 最小宽度各降一档（12px / 2×7 / 42px），
       尺寸与全仓通用的 .mini-action（42×23）一致 —— 可点区域仍然够大，
       但不会再撑出操作列、压到行的高亮边界或滚动条。 */
    min-width: 42px;
    font-size: 12px;
    padding: 2px 7px;
    border-radius: 6px;
    white-space: nowrap;
    border: 1px solid var(--border-strong); background: #fff; color: #4a5464;
    cursor: pointer;
  }
  .users-power:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
  .users-power-off { border-color: #d99000; color: #a86b00; }
  .users-power-off:hover { background: #fdf7ea; border-color: #d99000; color: #96601a; }
  .users-power:disabled { opacity: 0.5; }
  /* 「删除」是**不可恢复**的动作 —— 用红色（与"暂时性"的琥珀色禁用区分开，
     也和弹窗里的错误红字同一套颜色语言）。 */
  .users-danger {
    justify-self: center;
    min-width: 42px;
    font-family: inherit;
    font-size: 12px;
    padding: 2px 7px;
    border-radius: 6px;
    white-space: nowrap;
    cursor: pointer;
  }
  /* 「白底红字」这套**危险色只此一份**（2026-10-11）：用户管理那颗「删除」与单题冲突
     弹窗那颗「覆盖保存」共用同一组值（#b42318 字 / #e7b4ae 描边 / hover #fdeceb）。
     加新的危险按钮时往这两个选择器里加，别另抄一套颜色。 */
  .users-danger,
  .modal .modal-actions button.stale-danger,
  .modal .modal-actions button.app-dialog-danger {
    border: 1px solid #e7b4ae;
    background: #fff;
    color: #b42318;
  }
  .users-danger:hover,
  .modal .modal-actions button.stale-danger:hover,
  .modal .modal-actions button.app-dialog-danger:hover {
    background: #fdeceb; border-color: #b42318;
  }
  .users-danger:disabled { opacity: 0.5; cursor: default; }
  /* 「重置密码」：普通动作（不是破坏性的），用主色描边；比禁用/删除宽一档，因为字多 */
  .users-reset {
    min-width: 62px;
    font-family: inherit;
    font-size: 12px;
    padding: 2px 7px;
    border-radius: 6px;
    white-space: nowrap;
    border: 1px solid var(--border-strong);
    background: #fff;
    color: var(--accent);
    cursor: pointer;
  }
  .users-reset:hover { background: var(--accent-soft); border-color: var(--accent); }
  .users-reset:disabled { opacity: 0.5; cursor: default; }
  /* 操作列要并排放下「重置密码」「禁用/启用」「删除」三个按钮 */
  .users-actions { display: inline-flex; gap: 4px; justify-self: end; }
  .users-actions-hold .users-actions-hold-label {
    width: 156px;
    text-align: center;
    white-space: nowrap;
  }
  .users-actions-head { display: flex; justify-content: flex-end; align-items: center; }
  .users-actions-head-label { width: 156px; text-align: center; white-space: nowrap; }
  /* 手敲用户名确认框里的输入框 + 那个红色主按钮 */
  .danger-typed {
    width: 100%;
    box-sizing: border-box;
    padding: 5px 8px;
    margin-bottom: 12px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    font-family: inherit;
    font-size: 13px;
  }
  .danger-typed:focus { outline: 2px solid #b42318; outline-offset: 1px; }
  /* 「删除账号」确认框（2026-10-12）：说明段整段删掉之后，剩下的四行
     （标题 / 输名句 / 输入框 / 按钮行）走**同一条 12px 段距**。
     原来那句 .modal-note 的 `-8px` 上外边距是给"标题 + 说明 + 问句"三行设计的，
     少一行之后直接沿用会让问句贴住标题（实测 4px）。这里把它归位到 12px ——
     与输入框的下外边距（.danger-typed: margin-bottom 12px）和弹窗下内边距（12px）同档，
     整屏从上到下是 16 / 12 / 12 / 12 / 12，无大片空白。
     ⚠️ 选择器必须带 .users-modal 这两层（0,3,0）：`.modal .modal-note`（0,2,0）
     在文件里更靠前，同优先级或更低都会被它那条 -8px 盖掉。 */
  .modal.users-modal > .modal-note { margin: 0 0 12px; }
  /* 密码相关的两个弹窗：临时密码那一行 + 改密码表单（字段比别处紧一档） */
  .pw-modal { width: 400px; }
  .pw-line {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0 0 12px;
  }
  .pw-value {
    flex: 1;
    padding: 5px 9px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #fbfcfd;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 14px;
    letter-spacing: 0.5px;
    user-select: all;   /* 点一下就整段选中，方便手动复制 */
  }
  /* 复制按钮给固定最小宽度：「复制」→「已复制」变长时不会把左边那串密码挤动 */
  .pw-copy {
    min-width: 56px;
    padding: 3px 8px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    background: #fff;
    color: #4a5464;
    font-family: inherit;
    font-size: 12px;
    cursor: pointer;
  }
  .pw-copy:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
  .pw-label { display: block; font-size: 12.5px; color: var(--muted); margin: 0 0 4px; }
  .pw-input {
    width: 100%;
    box-sizing: border-box;
    padding: 5px 8px;
    margin-bottom: 10px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    font-family: inherit;
    font-size: 13px;
  }
  .pw-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
  /* 提示行常驻：错误红字与普通说明共用一格，弹窗高度不跳 */
  .pw-error, .pw-hint {
    margin: 0 0 10px;
    min-height: 18px;
    line-height: 18px;
    font-size: 12.5px;
  }
  .pw-error { color: var(--c-color); }
  .pw-hint { color: var(--muted); }
  .modal .modal-actions button.danger-ok {
    background: #b42318;
    border-color: #b42318;
    color: #fff;
  }
  .modal .modal-actions button.danger-ok:disabled {
    background: #e7b4ae;
    border-color: #e7b4ae;
    color: #fff;
  }
  /* 自己那一行没有禁用按钮，用一格占位撑住列宽（免得其它行对不齐） */
  .users-power-hold { font-size: 12.5px; color: var(--muted); white-space: nowrap; }
  .users-when {
    color: var(--muted); white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis;
  }
  /* 录入题数：一个合并后的数（右对齐好对位）；细分（通过 + 直存）在悬停里 */
  .users-entries {
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    color: #2c3444;
  }
  /* 最近操作：一条一行，行内就是"什么时候 / 什么动作 / 哪道题 / 还原"。
     列表在后台管理区里**自己滚**（占满面板剩下的高度），条数多了页面也不长。 */
  .journal-list {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    scrollbar-gutter: stable;      /* 记录条数变化时列表宽度恒定（不横跳） */
    overscroll-behavior: contain;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: #fbfcfd;
  }
  .journal-empty { margin: 0; padding: 14px 12px; font-size: 12px; color: var(--muted); }
  .journal-notice {
    margin: -6px 0 10px;
    padding: 7px 10px;
    border-radius: 6px;
    font-size: 12px;
    background: #eef4ff;
    color: #253244;
  }
  .journal-notice.ok { background: #e9f7ee; }
  .journal-notice.error { background: #fdeceb; color: #b42318; }
  /* 「浮动提示」：操作结果（还原 / 清理会话 / 备份 / 换开关…）统一浮在视口顶部中间。
     加这一个类就够 —— position:fixed 让它**彻底脱离文档流**，所以它出现 / 消失都不会
     顶动下面的卡片或列表（用户实测反馈的"卡片突然下移"就是这些提示原来占了一行）。
     没有自动消失机制的提示（比如读取失败）会一直留着，直到切 tab / 再刷新一次把它清掉。 */
  .float-notice {
    position: fixed;
    top: 58px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 70;
    margin: 0;
    max-width: 72vw;
    border: 1px solid var(--border-strong);
    box-shadow: 0 6px 20px rgba(20, 30, 50, 0.16);
    /* 允许换行（比如"保存成功"后面还跟着一串 changed 明细）—— 反正是浮层，不占布局 */
    white-space: normal;
  }
  /* 万一同一次出现两条（例如两个请求都失败）：后面的往下错开，别叠在一起 */
  .float-notice ~ .float-notice { top: 96px; }
  .float-notice ~ .float-notice ~ .float-notice { top: 134px; }
  /* 「就地浮层」通用件（2026-10-09）：给**挂在某个面板 / 工具条 / 弹窗里**的短提示用。
     和上面的 .float-notice 是一对：那个 fixed 到视口顶部，这个 absolute 贴在宿主里 ——
     两者都是**脱离文档流**，出现 / 消失不顶动任何兄弟元素。取值一律走 flashNotice（3 秒自消）。
     宿主必须自己 position:relative（下面三条锚点里逐个给了），否则它会去找更远的定位祖先。 */
  .float-note-local {
    position: absolute;
    top: 6px;
    left: 8px;
    right: 8px;
    z-index: 7;
    margin: 0;
    padding: 6px 10px;
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    background: #fff;
    box-shadow: 0 6px 18px rgba(20, 24, 32, 0.16);
    pointer-events: none;      /* 浮层绝不挡下面的按钮 / 行 */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* 锚点①「我的工作区」：宿主 .wsb。提示原来顶在工具条（.wsb-bar，高约 34px）正下方，
     绝对定位后钉在同一个位置 —— 视觉零变化，只是不再把网格顶下去。 */
  .wsb { position: relative; }
  .wsb > .float-note-local { top: 40px; }
  /* 锚点②工具条类宿主：批量录入的提示**浮在操作行上方**，不盖切分按钮 / 题号勾选框；
     后台用户统计行仍贴右端垂直居中。 */
  .batch-bar > .float-note-local {
    left: auto;
    right: 0;
    top: auto;
    bottom: 100%;
    transform: none;
    margin-bottom: 6px;
    max-width: 100%;
  }
  .users-summary > .float-note-local {
    left: auto;
    top: 50%;
    transform: translateY(-50%);
    max-width: 60%;
  }
  .users-summary { position: relative; }
  .users-summary > .float-note-local { right: 8px; }
  /* 锚点③两个**固定尺寸**弹窗（「我的待审」820×780 / 「打开文件」460×440）：
     提示原来就在按钮行上方那一格，钉回原处；弹窗高度写死，所以按钮一个像素都不动。 */
  .modal.mypending-modal,
  .modal.open-picker-modal { position: relative; }
  .modal.mypending-modal > .float-note-local,
  .modal.open-picker-modal > .float-note-local {
    top: auto;
    bottom: 50px;
    left: 18px;
    right: 18px;
    margin: 0;      /* 盖掉 .modal .modal-note 的 -8px/14px 外边距 */
  }
  .journal-row { padding: 7px 10px; border-bottom: 1px solid var(--border); }
  .journal-row:last-child { border-bottom: 0; }
  .journal-main { display: flex; align-items: center; gap: 8px; }
  .journal-when { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
  .journal-kind { color: #4a5464; font-size: 12px; font-weight: 600; }
  .journal-qid { color: var(--accent); font-size: 12px; }
  .journal-restore { margin-left: auto; }
  .journal-restore:disabled { color: #9aa3b0; border-color: var(--border); cursor: default; }
  .journal-restore:disabled:hover { background: #fff; }
  .journal-sub { display: flex; gap: 10px; margin-top: 2px; font-size: 11.5px; color: var(--muted); }
  .journal-path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .journal-note { flex: none; }

  /* Mathpix 粘贴即转换的浮动提示：**贴在刚粘贴的那个编辑框旁边**（原来的右下角太远，
     实测经常看不到）。position: fixed + JS 按编辑框的矩形算 top/left，不参与布局、
     不会把任何东西顶动；滚动/改窗口大小时跟着重新定位。
     「撤销」是一次性的：只在下一次编辑之前有效（见 undoPasteFormat）。 */
  .paste-toast {
    position: fixed;
    z-index: 60;
    display: flex;
    align-items: center;
    gap: 10px;
    max-width: 420px;
    padding: 9px 12px;
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    background: #fff;
    box-shadow: 0 6px 20px rgba(20, 30, 50, 0.16);
    font-size: 12.5px;
    color: #253244;
  }
  .paste-toast .paste-toast-undo {
    flex: none;
    padding: 2px 10px;
    border: 1px solid var(--accent);
    border-radius: 6px;
    background: #fff;
    color: var(--accent);
    font-family: inherit;
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
  }
  .paste-toast .paste-toast-undo:hover { background: var(--accent-soft); }
  .paste-toast .paste-toast-off {
    flex: none;
    padding: 0;
    border: 0;
    background: none;
    color: var(--muted);
    font-family: inherit;
    font-size: 11.5px;
    text-decoration: underline;
    cursor: pointer;
  }

  /* LaTeX 排版预览 */
  .preview-stage { margin-bottom: 10px; }
  .preview-loading {
    padding: 18px;
    text-align: center;
    color: var(--accent);
    background: var(--accent-soft);
    border: 1px solid #cfe0ff;
    border-radius: 8px;
    font-size: 13px;
  }
  .preview-meta { font-size: 11.5px; color: var(--muted); margin-bottom: 4px; }
  .preview-image {
    display: block;
    width: 100%;
    border: 1px solid var(--border);
    border-radius: 6px;
    margin-bottom: 6px;
    background: #fff;
  }
  /* 中栏那条 PDF：外层 .preview-body 已经自带 padding，这里不再叠一层；
     页面本体沿用 .rpreview-pdfpage 的白底 + 淡阴影（与右栏同一套）。 */
  .mid-pdfpages { padding: 0; gap: 8px; }
  .preview-divider { border: none; border-top: 1px solid var(--border); margin: 10px 0; }

  .diff-panel { margin-top: 6px; }
  .diff-panel h3 { margin: 0 0 6px; font-size: 13px; }
  .diff-body {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 12px;
    line-height: 1.6;
    white-space: pre-wrap;
    word-break: break-word;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: #fbfcfd;
    padding: 6px 0;
    max-height: 340px;
    overflow: auto;
  }
  .diff-line { padding: 0 8px; }
  .diff-line.add { background: #e8f7ec; color: #14612c; }
  .diff-line.del { background: #fdeceb; color: #a01f17; }
  .diff-line.hunk { background: #eef1f6; color: #5b6472; }
  .diff-line.file { color: var(--muted); }
  .diff-line.ctx { color: #4a5464; }
  .diff-empty { color: var(--muted); padding: 0 8px; }

  .modal-mask {
    position: fixed;
    inset: 0;
    background: rgba(24, 28, 36, 0.35);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 50;
  }
  /* 二次确认 / 查重这类"在某个面板之上再问一句"的弹窗，层级必须比面板高。
     实测踩过：从「最近操作」面板里点还原，确认框和面板同层级、而面板在 DOM 里更靠后，
     结果确认框被盖住，得先关掉面板才能点确认。这里显式抬到面板之上。 */
  .modal-mask.confirm-mask,
  .modal-mask.dedup-mask { z-index: 80; }
  .modal {
    width: 380px;
    background: #fff;
    border-radius: 10px;
    padding: 16px 18px 12px;
    box-shadow: 0 12px 32px rgba(20, 24, 32, 0.25);
  }
  .modal p { margin: 0 0 14px; font-size: 13.5px; }
  .modal.confirm-compact {
    width: 380px; min-width: 0; max-width: none;
    padding: 16px 18px 12px;
  }
  .modal.confirm-compact .modal-title {
    margin: 0 0 12px; white-space: nowrap;
  }
  /* 「该文件还未保存，确认关闭么？」下面那行**文件名**：
     与主问句之间留 6px（-8px 上边距把它从 .modal p 的 14px 里"收"上来，和
     .modal-note 是同一套节奏），等宽字体 + 深一档的灰 —— 一眼能认出这是"文件名"，
     又不抢主问句。长文件名（课测那种带日期+班级码+考点的）必须能换行，不能撑破弹窗。 */
  .modal .confirm-file {
    /* 上 -8（把 14px 的段间距"收"成 6px，和 .modal-note 同一套）、下 10 ——
       这样**问句字底→文件名**与**文件名字底→按钮行**两处视觉留白都在 13px 左右。
       实测：下边距 14 时两处是 12.9 / 17.3，看着下面空一截；改 10 后两边齐了。 */
    margin: -8px 0 10px;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12.5px;
    font-weight: 600;
    color: #4a5564;
    text-align: center;   /* 用户 2026-09-28：文件名这一行居中 */
    word-break: break-all;
  }
  /* 「新建课测」弹窗：底部那句灰字删掉之后，"字段行 → 按钮行"的留白改由 .ct-line
     自己的下边距给（原来靠那句灰字的 -8 / 14 兜着）。14px 与 .modal p 的段间距同档 ——
     整个弹窗的竖向节奏就是 16(padding) / 14 / 14 / 12(padding)，看着是一套。 */
  .modal.new-ct-modal .ct-line { margin: 0 0 14px; }
  /* 2026-10-10：「文件名 / 分享码」那一行 **后面还跟着灰字说明**时（关分享、删接收人、
     退出该分享这三处确认框就是），上下两处留白原来实测是 2px / 14px —— 上面贴住了码、
     下面又空一截，整块看着"上紧下松"。这里改成 6px / 12px：
       · 6 = 与「问句 → 码那一行」同一档（.confirm-file 的 mt:-8 + 问句 mb:14 塌出来就是 6）；
       · 12 = 与弹窗下内边距（padding-bottom:12）一致，按钮行上下一样松。
     只对"紧跟 .confirm-file 的那条 .modal-note"生效，别的弹窗（没有 code 行的）一个像素不动。 */
  .modal .confirm-file + .modal-note { margin: -4px 0 12px; }
  .modal .modal-actions { display: flex; justify-content: flex-end; gap: 8px; }
  /* 单题冲突弹窗里「看差异」展开的那块（2026-10-11 P0-1）：等宽 + 可滚，
     最多占 240px 高 —— 一道题的 diff 通常十几行，不给上限会把弹窗撑出屏幕。 */
  .modal .stale-diff { margin: -4px 0 12px; }
  .modal .stale-diff-body {
    margin: 0; padding: 8px 10px; max-height: 240px; overflow: auto;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 11.5px; line-height: 1.5; color: #2c3444;
    background: #f7f9fb; border: 1px solid var(--border); border-radius: 8px;
    white-space: pre; tab-size: 2;
  }
  .modal .modal-actions button {
    padding: 4px 14px;
    border-radius: 6px;
    border: 1px solid var(--border-strong);
    background: #fff;
    cursor: pointer;
    font-size: 13px;
  }
  .modal .modal-actions button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
  .modal .modal-actions button:disabled { opacity: 0.5; cursor: default; }

  .more-bar { text-align: center; padding: 6px 0 14px; }
  .more-bar button {
    padding: 5px 14px;
    border: 1px solid var(--border-strong);
    background: #fff;
    border-radius: 6px;
    cursor: pointer;
    color: var(--accent);
  }
  .more-bar button:hover { background: var(--accent-soft); }

  /* ---------------- 阶段 1：登录 / 角色 / 只读模式 ----------------
     这一块只加"盖在最上面的那一层"：登录遮罩、顶栏身份胶囊、会话失效提示。
     主界面本身的布局一个像素都不动 —— 未登录时遮罩把整个 app 盖住，
     登录后遮罩消失，看到的就是原来那张页面。
     顶栏的搜索框靠「找题模块 / 自己的 margin-left:auto」贴右；身份胶囊放在最右边，
     所以这里是"再往右一格"，不影响前面任何一项的位置。 */

  /* 未登录 / 正在校验登录态：整屏遮罩（fixed，不参与布局、不顶动任何面板）。
     2026-10-10 加登录页背景：一张**纯 SVG 代码生成**的方块波浪底图（无位图、无外链，
     源文件 frontend/icons/login-bg.svg —— 与图标同目录，项目里图片资源就放这儿）。
     · center / cover / no-repeat：任何窗口比例都铺满；配合 SVG 自己的
       preserveAspectRatio="xMidYMid slice"，多出来的部分是**裁边**、不是拉伸变形；
     · fixed：背景贴着视口，不随内容滚动（这一层本身也是 fixed 满屏）；
     · 底图中央 420×330 是刻意留白的干净区（登录卡片位置），卡片落在正中间。
     深色模式本轮不管（只做默认浅色）。 */
  /* Vue 挂载前先把根节点藏起来：慢网时浏览器即使先画到原始 HTML，
     也不会把 {{ … }} / v-if 指令暴露出来。下面静态首屏负责占住这一小段等待。 */
  [v-cloak] { display: none !important; }
  .auth-mask {
    position: fixed;
    inset: 0;
    z-index: 200;
    display: flex;
    align-items: center;
    justify-content: center;
    /* 给底部备案信息留出固定安全区，业务卡片仍按剩余高度居中。 */
    padding: 32px 16px 88px;
    background: var(--bg) url("../icons/login-bg.svg") center center / cover no-repeat fixed;
  }
  .auth-mask.boot-mask {
    z-index: 9999;
    transition: opacity 0.14s ease;
  }
  .auth-mask.boot-mask.is-done { opacity: 0; pointer-events: none; }
  .boot-card { text-align: center; }
  .boot-card .auth-spin { min-height: 20px; line-height: 20px; }
  .auth-card {
    width: 340px;
    max-width: 100%;
    /* 上 28 / 下 22：去掉标题下与页脚那两段说明之后，留白重新分给"标题 → 表单"
       和"底部链接 → 卡片边"，省得框里上紧下松 */
    padding: 28px 26px 22px;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 12px;
    box-shadow: 0 12px 32px rgba(20, 24, 32, 0.12);
  }
  /* 标题居中：登录 / 注册两屏都是"标题 + 表单"，标题即视觉中心。
     2026-10-10：标题左边加应用 logo（横排居中）。
     · flex + gap:4px —— 图标与文字像一组整体居中，卡片宽度 / 其余留白一个像素不动；
       （gap 从 8 收到 4：PNG 字形本身在 26px 盒子里左右各有 ≈1.5px 白边，8px 的盒间隙
         看着像 10px，偏散；4px 后视觉间距 ≈5.5px，贴着标题刚刚好。）
     · 用项目里现成的 icons/icon-192.png（就是 favicon / 程序坞那枚蓝灰 M），不新增资源；
     · mix-blend-mode: multiply —— 那张 PNG 底是不透明白（251,251,251），直接贴上来在白卡上
       会有一圈几乎看不见的方块边；multiply 让"白"与卡片融合，只留下蓝灰的 M。 */
  .auth-card h2 {
    margin: 0 0 22px; font-size: 17px; font-weight: 600; text-align: center;
    display: flex; align-items: center; justify-content: center; gap: 4px;
  }
  .auth-logo { display: block; width: 26px; height: 26px; mix-blend-mode: multiply; }
  .boot-logo { display: block; width: 26px; height: 26px; mix-blend-mode: multiply; }
  .auth-card label { display: block; font-size: 12.5px; color: var(--muted); margin: 0 0 4px; }
  .auth-card input {
    width: 100%;
    padding: 6px 10px;
    margin-bottom: 12px;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
    font-size: 13.5px;
    outline: none;
  }
  .auth-card input:focus { border-color: var(--accent); }
  .auth-card button.submit {
    width: 100%;
    padding: 7px 0;
    border: 1px solid var(--accent);
    border-radius: 6px;
    background: var(--accent);
    color: #fff;
    font-size: 13.5px;
    cursor: pointer;
  }
  .auth-card button.submit:disabled { opacity: 0.55; cursor: default; }
  /* 提示行（登录失败 / 注册成功的绿字）**常驻占一行 18px**：
     这样提示出现或消失时卡片高度一个字都不变（line-height 也钉成 18px，
     否则有文字的那一行会比空行高 3px，卡片照样会轻轻一跳）。 */
  .auth-error { color: var(--c-color); font-size: 12.5px; margin: 0 0 10px;
                min-height: 18px; line-height: 18px; }
  /* 同一行换成"注册成功"的绿色（两个状态共用一格，不另起一行） */
  .auth-error.auth-ok { color: var(--a-color); }
  /* 「注册一个 / 返回登录」：做成文字链接的样子，不抢主按钮的注意力。
     它现在是卡片里最后一件事，所以和主按钮之间给一点气口（14px）。 */
  .auth-switch { margin: 14px 0 0; font-size: 12.5px; color: var(--muted); }
  .auth-switch button {
    border: none;
    background: none;
    padding: 0;
    margin: 0;
    color: var(--accent);
    font-size: 12.5px;
    cursor: pointer;
    text-decoration: underline;
  }
  /* 登录 / 注册页底部备案：公网首屏即展示，链接保留浏览器默认的新标签行为，
     第一行是网站与产品名，第二行是两项备案号，不抢登录表单注意力。 */
  .auth-filing {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 6px;
    bottom: max(6px, env(safe-area-inset-bottom));
    display: flex;
    flex-direction: column;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 1px;
    padding: 0 16px;
    color: var(--muted);
    background: none;
    font-size: 12px;
    line-height: 1.55;
    text-align: center;
  }
  .auth-filing-links {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 1px 14px;
  }
  .auth-filing-name { color: var(--text); font-weight: 500; }
  .auth-filing a { color: inherit; text-decoration: none; }
  .auth-filing a:hover,
  .auth-filing a:focus-visible { color: var(--accent); text-decoration: underline; }
  .auth-spin { color: var(--muted); font-size: 13px; }

  /* 顶栏右侧的身份胶囊：谁在登录 + 角色 + 退出 */
  /* flex:none + nowrap：顶栏是 flex 行，窗口窄的时候不许把这三个字挤成竖排 */
  .session-box { display: inline-flex; align-items: center; gap: 6px; flex: none; white-space: nowrap; }
  /* 实时连接断开时的小圆点：琥珀色（"需要注意但不是故障"，与其它警示同一套颜色语言） */
  .session-box .ws-down { font-size: 11.5px; color: #96601a; white-space: nowrap; }
  .session-box .who { font-size: 12px; color: var(--muted); white-space: nowrap; }
  .session-box .role-chip {
    font-size: 11.5px;
    padding: 1px 7px;
    border-radius: 10px;
    background: var(--accent-soft);
    color: var(--accent);
    font-weight: 600;
    cursor: help;
  }
  /* 只读角色（预录员 / 只读员）用琥珀色：和主色区分开，一眼能看出"权限不一样" */
  .session-box .role-chip.readonly { background: #fdf1dc; color: #96601a; }
  /* 管理员：胶囊本身就是「进后台管理区」的入口 —— 可点、悬停变实心；
     其它身份（管家 / 大佬 / 萌新）不加这个类，仍是纯展示 + cursor: help。 */
  /* 2026-10-11：胶囊**禁掉文字选中** —— 右键它时会顺手把「管理员」三个字选成蓝底
     （与目录树那次同一个毛病）。配合 @contextmenu 里清一次选区，双击/拖选也不留痕。 */
  .session-box .role-chip.admin-link {
    cursor: pointer; border: 0; font-family: inherit;
    user-select: none; -webkit-user-select: none;
  }
  .session-box .role-chip.admin-link:hover { background: var(--accent); color: #fff; }
  .session-box .role-chip.admin-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  .session-box button {
    font-size: 12px;
    padding: 2px 9px;
    border-radius: 12px;
    border: 1px solid var(--border-strong);
    background: #fff;
    cursor: pointer;
  }
  .session-box button:hover { background: #f2f5fa; }  /* ---------------- 真题卷集（2026-10-02 第 1 块）：全部 .ds-* 前缀，
       与四类 / 卷集 / 其它面板的类零重叠；不改上面任何一条既有规则。 ---------------- */
  .ds-pane { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
    padding: 8px 12px; overflow: hidden; }
  .ds-tabs { display: flex; gap: 2px; padding: 2px; background: #eef1f6;
    border-radius: 8px; flex: none; align-self: flex-start; }
  .ds-tabs button { padding: 3px 14px; border: 0; border-radius: 6px; background: transparent;
    color: #4a5464; font-size: 12.5px; font-weight: 400; font-family: inherit;
    cursor: pointer; white-space: nowrap; }
  .ds-tabs button:hover { background: #e3e8f0; }
  .ds-tabs button.active { background: #fff; color: var(--accent); font-weight: 600;
    box-shadow: 0 1px 2px rgba(16, 24, 40, 0.08); }
  /* 左右之间不再留 gap —— 分隔线本身就是那条缝（8px，带淡底色，看得见） */
  .ds-split { flex: 1 1 auto; min-height: 0; display: grid;
    /* 卷集这条分隔线**自己一个宽度**（4px）—— 不共用 --splitter-w，
       所以中栏↔右栏那条仍是 6px，两边互不牵连 */
    --ds-splitter-w: 4px;
    grid-template-columns: minmax(200px, 320px) var(--ds-splitter-w) minmax(0, 1fr);
    gap: 3px; margin-top: 8px; }   /* gap 3px = 分隔线两侧各 3px 留白 */
  .ds-cats { display: flex; flex-direction: column; min-height: 0;
    border: 1px solid var(--border); border-radius: 8px; background: #fff; overflow: hidden; }
  /* 真题卷集进入编辑模式时会多出「新建」按钮。必须保持单行：
     分类标题 + 搜索框 + 新建 + 齿轮在左栏最小宽度内仍能排下，
     搜索框自身可缩到 70px；若允许换行，点齿轮会从 34.25px 跳到 63px。
     该行按钮也必须钉成 21px：通用 .ds-btn 是 23px，会比搜索框多出 2px。 */
  .ds-cats-bar { display: flex; align-items: center; gap: 6px; flex-wrap: nowrap;
    padding: 6px 8px; border-bottom: 1px solid var(--border); background: #fbfcfd; }
  .ds-cats-bar > .ds-btn { height: 21px; padding: 0 9px; line-height: 1; }
  .ds-cats-title { font-weight: 600; font-size: 12.5px; color: #2c3444; flex: none; }
  .ds-search { flex: 1 1 90px; min-width: 70px; padding: 2px 8px; font-size: 12px;
    border: 1px solid var(--border-strong); border-radius: 12px; font-family: inherit;
    outline: none;
    /* 灰字放不下也走省略号（2026-10-13）—— 真题卷集那颗 + 工作区新加那颗共用这条 */
    text-overflow: ellipsis; }
  .ds-search::placeholder { text-overflow: ellipsis; }
  .ds-search:focus { border-color: var(--accent); }
  /* 右区搜索框 + 框内的「全 / 单」chip（交互跟顶部题库搜索框参照，但样式**自成一份**） */
  .ds-search-wrap { position: relative; display: inline-flex; flex: none; width: 210px; }
  /* 这个框比左区那个搜索框略高一点（上下各 4px 内边距）——为了框内那颗胶囊
     **左 / 上 / 下三边留白相等**（4px），高度由上下留白挤出来，不写死。 */
  .ds-search-wrap .ds-search { width: 100%; padding: 4px 8px; padding-left: 36px; }
  /* 真题卷集这颗「全 / 单」胶囊：**自己一份样式，跟顶部题库搜索栏完全解耦**
     （2026-10-03：上一轮把两边并进了同一组选择器，这一轮拆回来 —— 顶部那组恢复原样）。
     文案只有一个字，所以比顶部那颗窄：左右内边距 6px、圆角 9px；几何上
     左 / 上 / 下三边留白都 = 4px（高度由上下留白挤出来，不写死）。 */
  .ds-scope-chip { position: absolute; left: 4px; top: 4px; bottom: 4px; height: auto;
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 22px; padding: 0 6px; border: none; border-radius: 9px;
    background: #e4e9f0; color: #4a5464; font-size: 11.5px; font-weight: 500;
    font-family: inherit; line-height: 1; cursor: pointer; white-space: nowrap;
    z-index: 2; }
  .ds-scope-chip:hover { background: #d8dee8; }
  .ds-scope-chip.local { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
  .ds-scope-chip.local:hover { background: #dde8ff; }
  .ds-scope-chip.empty { background: #fdf1e6; color: #a15a12; font-weight: 600; }
  .ds-scope-chip.empty:hover { background: #f9e6d5; }
  /* 「确认建卷」弹窗里那个分类下拉：与上面的输入框同宽同高（复用 .pw-input 的外形） */
  .pw-select { appearance: auto; }
  /* 左区（可拖/可点那些）在**搜索态**下整块变灰且点不动 */
  .ds-cats.searching { opacity: 0.5; filter: grayscale(0.85); pointer-events: none; }
  /* 搜索框空着时那颗「搜索」是灰的（不是蓝的），也点不了 */
  .ds-btn.ds-btn-blue:disabled { background: #e4e9f0; border-color: var(--border-strong);
    color: #9aa4b2; opacity: 1; cursor: default; }
  /* ---------------- 待审区（第 1 步：扫描 + 列表） ---------------- */
  .ds-pending { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
  /* 卡片把「胶囊 + 刷新 + 列表」整块包住（用户在 2026-10-03 追加要求） */
  /* ⚠️ 左右**不让**（0px）：与「试卷库」那块内容区（.ds-split，margin 8px 0 0）的左右留白
     设定完全一致 —— 卡片左沿 = 试卷库内容区左沿 = 上方那排子 tab 的左沿。 */
  .ds-pending-card { flex: 1 1 auto; min-height: 0; margin: 8px 0 10px;
    display: flex; flex-direction: column; border: 1px solid var(--border);
    border-radius: 8px; background: #fff; overflow: hidden; }
  .ds-pending-bar { display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
    padding: 6px 10px; border-bottom: 1px solid var(--border); background: #fbfcfd; }
  /* 计数胶囊：跟顶部题库搜索栏那颗「全库 / 局部」同一套（灰底；选中的用主色淡底） */
  .ds-pill { display: inline-flex; align-items: center; height: 19px; padding: 0 9px;
    border: none; border-radius: 10px; background: #e4e9f0; color: #4a5464;
    font-size: 11.5px; font-weight: 500; font-family: inherit; line-height: 1;
    cursor: pointer; white-space: nowrap; }
  .ds-pill:hover { background: #d8dee8; }
  .ds-pill.on { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
  .ds-pill.on:hover { background: #dde8ff; }
  .ds-pending-hint { margin-left: auto; font-size: 11.5px; color: var(--muted); }
  /* 待审区工具栏右侧那一组：按年份排序 + 搜索框 + 搜索（这一格原来是一行灰字说明，
     2026-10-03 按用户要求换掉）。margin-left:auto 让整组贴到行尾。 */
  .ds-pending-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
  .ds-pending-year { display: inline-flex; align-items: center; gap: 4px; font-size: 12px;
    color: #2c3444; cursor: pointer; white-space: nowrap; user-select: none; }
  .ds-pending-year input { margin: 0; cursor: pointer; }
  .ds-pending-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
  /* 一行：来源名（左，可省略，名字后面挂「新」/⚠） / 题数（右） / 操作（忽略｜谁于何时+恢复） */
  .ds-pending-row { display: grid; grid-template-columns: minmax(0, 1fr) 64px minmax(0, auto);
    align-items: center; column-gap: 12px; padding: 7px 12px; font-size: 12.5px;
    border-bottom: 1px solid #f0f2f5; }
  .ds-pending-row:last-child { border-bottom: none; }
  .ds-pending-row:hover { background: #f7f9fc; }
  .ds-pending-name { display: flex; align-items: center; gap: 6px; min-width: 0; color: #2c3444; }
  .ds-pending-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* 「新」标记：与顶栏「今日新增」同一支绿（--a-color） */
  .ds-pending-new { flex: none; padding: 0 5px; border-radius: 8px; background: var(--a-color);
    color: #fff; font-size: 10.5px; font-weight: 600; line-height: 15px; }
  .ds-pending-num { justify-self: end; color: var(--muted); font-variant-numeric: tabular-nums;
    white-space: nowrap; }
  .ds-pending-same { flex: none; color: var(--b-color); cursor: help; font-size: 12px; }
  /* 行尾操作：忽略 / （已忽略页）谁于何时忽略 + 恢复 */
  .ds-pending-ops { display: flex; align-items: center; justify-content: flex-end; gap: 8px;
    min-width: 0; }
  /* 「已读」的**固定槽位**：有「新」才往里放按钮，没「新」就空着 —— 槽位宽度不变，
     所以三颗按钮的总宽不因"有没有已读"变化，前面几列（题数 / 状态 / 时间）也一格不动。
     宽度 = 那颗按钮的实际宽度（实测 42px）+ 2px 余量；按钮右对齐，与另外两颗贴齐。 */
  .ds-pending-read-slot { flex: none; display: inline-flex; justify-content: flex-end;
    min-width: 44px; }
  .ds-pending-by { color: var(--muted); font-size: 11.5px; white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis; }
  .ds-btn { display: inline-flex; align-items: center; gap: 4px; flex: none;
    padding: 2px 9px; border: 1px solid var(--border-strong); border-radius: 6px;
    background: #fff; color: #2c3444; font-size: 12px; font-family: inherit;
    font-weight: 400; cursor: pointer; white-space: nowrap; }
  .ds-btn:hover { background: var(--accent-soft); }
  .ds-btn:disabled { opacity: 0.45; cursor: default; }
  /* 真题卷集这三颗按钮统一字重 600（跟 app 里"有分量的字"一档）：
     「‹ 返回分类」蓝底白字、「搜索」蓝底白字（空框时灰）、「新建试卷」蓝字白底。
     ⚠️ 必须写在 `.ds-btn` **后面**并带 `.ds-btn` 前缀 —— `.ds-btn` 自己写了 font-weight:400。 */
  .ds-btn.ds-search-go, .mini-action.ds-back-cats, .ds-btn.ds-newpaper { font-weight: 600; }
  .ds-btn.ds-newpaper { color: var(--accent); }
  .ds-btn.tiny { padding: 1px 6px; font-size: 11.5px; }
  .ds-btn.tiny.danger { color: var(--c-color); border-color: #f2c2bf; }
  .ds-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 4px; }
  .ds-item { display: flex; align-items: center; gap: 6px; padding: 4px 6px;
    border: 1px solid transparent; border-radius: 7px; cursor: pointer; font-size: 12.5px; }
  .ds-item:hover { background: #f7f9fc; }
  .ds-item.selected { border-color: var(--accent); background: var(--accent-soft); }
  .ds-item-name { flex: 1 1 auto; min-width: 0; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; color: #2c3444;
    /* 分类名加粗（2026-10-03 用户要求）；右边那枚「N 套」**不动**（.ds-item-num 照旧） */
    font-weight: 600; }
  .ds-item-num { flex: none; color: var(--muted); font-variant-numeric: tabular-nums; }
  .ds-item-ops { flex: none; display: flex; gap: 3px; opacity: 0; transition: opacity 0.12s; }
  .ds-item:hover .ds-item-ops, .ds-item.selected .ds-item-ops { opacity: 1; }
  .ds-papers { min-height: 0; display: flex; align-items: flex-start; justify-content: center;
    border: 1px solid var(--border); border-radius: 8px; background: #fff; padding: 12px; }
  .ds-empty { margin: 0; padding: 14px 12px; font-size: 12px; color: var(--muted);
    text-align: center; }
  .ds-empty.big { padding: 40px 12px; }  /* 真题卷集（第 2 块）：设置按钮 / 编辑模式 / 拖动 */
  .ds-head-title { font-weight: 600; font-size: 13px; color: #2c3444; }
  /* 齿轮：跟搜索框等高（26px），比原来大一圈 */
  /* 齿轮：**正方形**，边长跟分类搜索框同高（实测 21px）；inline-flex 居中保证图标不偏 */
  .ds-gear { flex: none; width: 21px; height: 21px; padding: 0; box-sizing: border-box;
    display: inline-flex; align-items: center; justify-content: center;
    /* 2026-10-10（用户要求）**去掉按钮的外框**：原来那颗是"白底 + 1px 边框 + 5px 圆角"的
       小方块，跟新换的线性齿轮不搭。现在三个态都不画框（无边框、透明底），
       hover / 编辑态**只换图标的颜色**。
       尺寸档不变：还是 21×21（box-sizing: border-box，去掉那 1px 边框后内容盒多 2px，
       里面那颗 18px 的图标照旧居中）。文档里"齿轮右边框压着搜索框右边框"那句说的是**盒子的边**，
       盒子本身没动，对齐关系不变。 */
    border: 0; background: transparent;
    /* 2026-10-10：图标从 Unicode ⚙（系统字体画的，灰黑、边缘发糊、各系统不一致）
       换成自画的**线性齿轮 SVG**（见 GearIcon 组件）——单色描边、无填充、齿形圆角、
       深蓝 #4a5a75（照用户给的效果图）。颜色走 currentColor，所以 hover/.on 两态
       一个字都不用改。font-size 留着无害（图标不再是文字）。 */
    color: #4a5a75; font-size: 13px; line-height: 1; cursor: pointer; }
  /* hover = 图标变主色（没有底色/边框可换，就靠颜色给反馈） */
  .ds-gear:hover { color: var(--accent); }
  /* 编辑模式开着 = 图标常亮主色；这时再 hover 会更深一档，保留"有反应"的手感 */
  .ds-gear.on { color: var(--accent); }
  .ds-gear.on:hover { color: #2560d4; }
  /* ── 齿轮的两态（2026-10-10 用户拍板）────────────────────────────────────────
     两态**都是线性**（不填充、不加圆底）：常规态细线（SVG 上写的 stroke-width="1.0"），
     编辑模式开（.ds-gear.on）**只把线加粗到 1.5**，颜色照旧由上面的 color 规则给
     （常态灰蓝 → hover / 编辑态主色蓝）。
     中间试过"实心填充 + evenodd 中心镂空"，用户看过说"太丑"→ 撤回，改成"加粗线条"。
     1.5 这个数：比常态粗 50%、看得出一眼区别，又还没到用户当初嫌粗的那档（1.6 起
     中间那两个同心圆就开始糊在一起；本地 1.5/1.8/2.1/2.4 四档对照挑的）。
     ⚠️ 只改 stroke-width、**不碰 fill**：`fill: none` 在两态都得留着（写出来是给"两态都
     线性"一个显式锚点，也顺手钉住测试）。 */
  .ds-gear svg { fill: none; }
  .ds-gear.on svg { stroke-width: 1.5; }
  .ds-item.editing { cursor: grab; user-select: none; -webkit-user-select: none; }
  /* 分隔线：粗细 / 圆角 / 颜色**与中栏↔右栏那条同一套**（都是 --splitter-w = 6px、
     --border-strong、圆角 3px）；只有编辑模式才可拖（光标 + hover 变主色） */
  .ds-splitter { background: var(--border-strong); border-radius: 3px; }
  .ds-splitter.on { cursor: col-resize; }
  .ds-splitter.on:hover { background: var(--accent); }
  .ds-item.dragging { opacity: 0.45; }
  .ds-item.dropbefore { box-shadow: inset 0 2px 0 var(--accent); }
  .ds-item.dropafter { box-shadow: inset 0 -2px 0 var(--accent); }  /* 试卷库右区（第 3 块）：搜索条 + 列表 + 空态 */
  .ds-papers { flex-direction: column; align-items: stretch; justify-content: flex-start;
    padding: 0; overflow: hidden; }
  /* 「试卷库 / 待审区」那一行：左边两颗子 tab，右边搜索 + 新建 */
  .ds-subrow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  .ds-subrow-right { margin-left: auto; display: flex; align-items: center; gap: 6px; }
  .ds-subrow-right .ds-search-wrap { flex: none; }
  /* 表头：与试卷行**同一套五列 grid**（这样列才对得上） */
  .ds-paper-head { display: grid;
    grid-template-columns: minmax(0, 12fr) minmax(0, 3fr) minmax(0, 3fr)
                           minmax(0, 3fr) minmax(0, 3fr);
    align-items: center; column-gap: 12px; padding: 4px 10px;
    border-bottom: 1px solid var(--border); background: #fbfcfd; }
  /* ⚠️ 表头单元格必须**拉满整列**（stretch）—— 否则 `right: 0` 贴的是"文字右缘"，
     拖手会跟真实列边界差出很远（用户实测：竖线贴着文字）。文字靠 justify-content 排。 */
  .ds-th { border: 0; background: none; padding: 0; font-family: inherit; font-size: 11.5px;
    color: var(--muted); cursor: pointer; display: inline-flex;
    align-items: center; gap: 2px; justify-self: stretch; justify-content: flex-start;
    white-space: nowrap; }
  .ds-th:hover { color: var(--accent); }
  .ds-th-plain { cursor: default; }
  /* 列边界拖手：贴在各表头单元格**右缘**，编辑模式下**常显一条细竖线**（看得见才拖得动），
     hover 变主色并加粗；日常模式这块整个不渲染。 */
  .ds-paper-head { position: relative; }
  .ds-paper-head .ds-th { position: relative; }
  /* 竖线落在**列间缝的正中**：gap = 12px，right:-12 + width:12 → 元素正好铺满整条缝，
     中心 = 缝中线（= 左列右边界 + 6）。 */
  .ds-col-resizer { position: absolute; top: 0; bottom: 0; right: -12px; width: 12px;
    cursor: col-resize; border-radius: 3px; background: transparent; z-index: 2;
    display: flex; align-items: center; justify-content: center; }
  .ds-col-resizer::before { content: ""; width: 2px; height: 70%; border-radius: 1px;
    background: var(--border-strong); }
  .ds-col-resizer:hover::before { width: 3px; background: var(--accent); }
  /* 设置模式下名字右边那两颗 */
  .ds-paper-ops { display: inline-flex; align-items: center; gap: 4px; flex: none; }
  .ds-btn.danger { color: var(--c-color); border-color: #f2c2bf; }
  .ds-th-plain:hover { color: var(--muted); }
  .ds-arrow { color: var(--accent); font-weight: 600; }
  /* 后四列的文字居中（列本身已经 stretch 拉满，居中是靠 justify-content） */
  .ds-paper-head .ds-th:nth-child(n+2) { justify-content: center; text-align: center; }
  /* ⚠️ 列表**左右不留 padding**：表头不在这个滚动容器里，容器一有横向 padding，
     行就比表头窄 8px，五列的列宽按 fr 重算 → 越往右越偏（最后一列差 4px，看着像"没居中/右边空一格"）。 */
  .ds-papers-list { flex: 1 1 auto; min-height: 0; overflow-y: auto;
    overflow-x: hidden; padding: 4px 0; }   /* 横向永不滚动 */
  /* 一行五块：**用 grid 分列**（名字吃掉剩余宽度，其余四列各自成列、列宽固定）——
     比 flex 更整齐：多行之间列对齐，也不会出现"名字后面一大片空、四块挤在最右边"。 */
  /* ⚠️ 五个**直接子元素**才排得进五列（以前套了一层 .ds-paper-meta，被压成一列 56px，
     于是"题数/时间看不见、全挤在左边"）。列宽：名字吃剩余 / 题数 64 / 状态 auto(≥120) /
     时间 92 / 按钮 172；列间距 14px。 */
  /* ⚠️ 五列全部**可收缩**：名字 minmax(0,1fr)、其余列 max-content —— 面板再窄也不会
     撑出横向滚动条（上一版把固定列宽加起来 624px，一窄就出滚动条）。 */
  /* 试卷名 40%、其余四块各 15%（12:3:3:3:3 的 fr 分配 = 40 : 15 : 15 : 15 : 15）——
     五块加起来正好一行宽；fr 会自动把 gap 算进去，所以再窄也不出横向滚动条。 */
  .ds-paper-row { display: grid;
    grid-template-columns: minmax(0, 12fr) minmax(0, 3fr) minmax(0, 3fr)
                           minmax(0, 3fr) minmax(0, 3fr);
    align-items: center; column-gap: 12px; padding: 6px 10px;
    min-width: 0; overflow: hidden;
    border-bottom: 1px solid #f0f2f5; cursor: pointer; font-size: 12.5px; }
  .ds-paper-row.selected { background: var(--accent-soft); }
  /* 五列对齐：名字**左**，题数 / 状态 / 时间 / 操作**居中**（表头与数据同一套 grid，
     列边界天然对齐）。2026-10-02：原来那条"题数左移 30px"随这条一起撤了 ——
     改成居中之后那个位移没有意义。 */
  .ds-paper-num, .ds-paper-time, .ds-paper-state { justify-self: center; white-space: nowrap; }
  .ds-paper-row:last-child { border-bottom: none; }
  .ds-paper-row:hover { background: #f7f9fc; }
  /* 名字占左（有上限，别无限拉宽）；其余几块整体靠右 */
  /* 名字占第一列（吃掉剩余宽度，不设死宽；太长就省略号） */
  .ds-paper-name { display: flex; align-items: center; gap: 4px; min-width: 0;
    overflow: hidden; color: #2c3444; }
  .ds-paper-title { min-width: 0; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; }
  /* 「全库」模式才出现：这条试卷属于哪个分类 */
  .ds-paper-cat { flex: none; padding: 0 5px; border-radius: 8px; background: #eef1f6;
    color: #6b7686; font-size: 10.5px; line-height: 16px; white-space: nowrap; }
  .ds-paper-warn { flex: none; color: var(--b-color); cursor: help; font-size: 12px; }
  /* 缺题（题库里这个来源后来多了几道）：与悬空 ⚠ 并排挂在名字后，靠文字区分；
     「补齐」是一颗小按钮，**不占固定宽度**——有缺才出现，行里其它列一个像素都不动 */
  .ds-paper-gap { flex: none; color: var(--b-color); cursor: help; font-size: 12px;
    white-space: nowrap; }
  .ds-paper-fill { flex: none; }
  /* 第 3 列：状态 chip（悬空 ⚠ 叠在同一格里，不占第六列） */
  .ds-paper-state { display: flex; align-items: center; gap: 6px; min-width: 0; }
  .ds-paper-state .ds-chip { flex: none; }
  /* 第 5 列：两个按钮（标记完整 / 打开），右对齐 */
  .ds-paper-actions { display: flex; align-items: center; gap: 8px; justify-content: center; }
  /* 状态胶囊**可点**，视觉只靠底色（用户 2026-10-02 拍的最终版）：
       待补 → 主色蓝底（与 app 其它按钮同一个 --accent）+ 白字
       完整 → 「今日新增」那个绿（--a-color）+ 白字
     **没有** hover 特效（不要 brightness / 描边 / 下压）。 */
  /* 外观照**账号页那颗胶囊** `.users-tag`（font-size 11 / padding 0 5px / radius 8 / 纯色底白字）：
     border / outline / box-shadow 全部清掉 —— 老 `.ds-chip` 带 `border: 1px solid
     var(--border-strong)`，不清就会留半圈深色描边。
     ⚠️ 选择器要比 `.ds-chip.partial`（0,2,0）更具体，否则被它按住改不动。 */
  .ds-chip.ds-chip-btn { font-family: inherit; cursor: pointer;
    border: 0 none; outline: 0 none; box-shadow: none;
    padding: 0 5px; border-radius: 8px; font-size: 11px; font-weight: 600; line-height: 18px; }
  .ds-chip.ds-chip-btn:hover, .ds-chip.ds-chip-btn:active, .ds-chip.ds-chip-btn:focus {
    border: 0 none; outline: 0 none; box-shadow: none; }
  .ds-chip.ds-chip-btn.partial { background: var(--accent); color: #fff; }
  .ds-chip.ds-chip-btn.complete { background: var(--a-color); color: #fff; }
  /* 浮窗：钉在面板右上、绝对定位不占位、NOTICE_MS 后自消 */
  .ds-pane { position: relative; }
  .ds-float { position: absolute; top: 6px; right: 12px; z-index: 5; margin: 0;
    padding: 3px 10px; border-radius: 8px; font-size: 12px; pointer-events: none;
    box-shadow: 0 2px 8px rgba(16, 24, 40, 0.12); background: #f2fbf4; color: var(--a-color); }
  .ds-float.error { background: #fdf3f2; color: var(--c-color); }
  .ds-paper-num { flex: none; color: var(--muted); font-variant-numeric: tabular-nums; }
  .ds-paper-time { flex: none; color: var(--muted); font-size: 11.5px; }
  .ds-chip { flex: none; font-size: 11px; padding: 0 6px; border-radius: 10px;
    background: #eef2f7; color: #5b6675; white-space: nowrap; }
  .ds-chip.partial { background: #fdf7ea; color: #96601a; }
  .ds-chip.half { background: #eef2f7; color: #5b6675; }
  .ds-chip.complete { background: #f2fbf4; color: var(--a-color); }
  .ds-chip.warn { background: #fdf3f2; color: var(--c-color); }  .ds-newpaper-note { margin: -4px 0 8px; font-size: 12px; color: var(--muted); line-height: 1.7; }
  /* 「确认成套卷」弹窗：来源/题数一行 → 两个"标签 + 输入"行 → 按钮行（复选框贴左）。
     标签固定同宽（「名称」「分类」都是 2 个字，34px 够且左右对齐），行内垂直居中。 */
  .ds-confirm-paper-modal .ds-confirm-source { margin: -4px 0 12px; font-size: 12px;
    color: var(--muted); line-height: 1.7; }
  .ds-form-row { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
  /* 最后一行（分类）底下不留边距，改由按钮行统一给 —— 这样"分类框 → 按钮行"的留白
     与上面"来源行 → 名称框"完全相同（都是 12px，用户 2026-10-03 要求）。 */
  .ds-form-row-last { margin-bottom: 0; }
  .ds-confirm-paper-modal .modal-actions { margin-top: 12px; }
  .ds-form-label { flex: none; width: 34px; font-size: 12px; color: #2c3444;
    text-align: left; white-space: nowrap; }
  /* .pw-input 默认 width:100% + 下边距 10px（单栏表单用）—— 行内改成吃剩余宽度、外边距交给行 */
  .ds-form-row .pw-input { flex: 1 1 auto; width: auto; min-width: 0; margin-bottom: 0; }
  .ds-confirm-paper-modal .pw-error { margin: 2px 0 0; }   /* 下边距交给按钮行的 margin-top */
  /* 按钮行左侧那颗复选框（取消 / 确认建卷照旧贴右） */
  .ds-confirm-back { margin-right: auto; display: inline-flex; align-items: center; gap: 5px;
    font-size: 12px; color: #2c3444; cursor: pointer; white-space: nowrap; user-select: none; }
  .ds-confirm-back input { margin: 0; cursor: pointer; }
