/* ===========================================================================
   frontend/assets/app.css —— 应用界面样式（结构与组件规则）
   ---------------------------------------------------------------------------
   美术方向：Executive Navy / Porcelain（深海军蓝 + 瓷白），金发丝线点睛。

   纪律：
     1. 本文件只服务「应用界面」，不参与交付图片版式（图片色值真源在
        data/theme/*.theme.json + templates/summary_a4_v1/styles/*.css）；
     2. 设计令牌（色值 / 字体族 / 字号 / 间距 / 圆角 / 阴影 / 时长）只写在同目录
        theme.css 的 :root 里，本文件一律消费 var(...)，不散写十六进制或 rgba()；
     3. 要加令牌就改 theme.css，别在这里声明同名 token 跟它抢——重名时后加载的
        一方赢，令牌真源就变成两处了。本文件仅剩的 :root 只放布局算式
        （--page-head-h 这类与组件规则同生同死的数），不放色值。
   =========================================================================== */

/* 令牌层：同目录 theme.css，index.html 里排在前面。 */

*, *::before, *::after { box-sizing: border-box; }

/* [hidden] 强制生效（类选择器同优先级，作者样式表在后会盖掉，故 !important） */
[hidden] { display: none !important; }

/* ===========================================================================
   基础排版
   =========================================================================== */
html, body { margin: 0; padding: 0; }
html { scrollbar-gutter: stable; }

body {
  background: linear-gradient(180deg, var(--c-page) 0%, var(--c-page-2) 100%);
  background-attachment: fixed;
  color: var(--c-text);
  font-family: var(--ui-font);
  font-size: var(--fs-md);
  line-height: 1.6;
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
}

code { font-family: var(--ui-mono); font-size: .92em; }
h1, h2, h3, h4 { margin: 0; font-weight: 600; }
p { margin: 0; }

.visually-hidden {
  position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; }
.btn:focus-visible {
  outline: 2px solid var(--c-surface);
  outline-offset: 0;
  box-shadow: 0 0 0 4px var(--c-focus);
}
::selection { background: var(--c-primary-50); color: var(--c-text); }
.input { caret-color: var(--c-primary); }

/* 滚动条：细、灰、hover 显形（三档色值见 theme.css 的 --c-scroll-*） */
* { scrollbar-width: thin; scrollbar-color: transparent transparent; }
*:hover { scrollbar-color: var(--c-scroll-thumb) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; background: transparent; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: transparent; border-radius: var(--r-pill); }
*:hover::-webkit-scrollbar-thumb { background: var(--c-scroll-thumb-webkit); }
*::-webkit-scrollbar-thumb:hover { background: var(--c-scroll-thumb-hover); }

/* ===========================================================================
   布局骨架：侧栏 + 主区，两列 grid
   =========================================================================== */
.app {
  display: grid;
  grid-template-columns: var(--sidebar-w, var(--sidebar-w-default)) minmax(0, 1fr);
  align-items: stretch;
  min-height: 100vh;
  position: relative;
  /* 收 / 开侧栏时整栏宽度过渡一点点（时长与缓动只认 token，见 theme.css） */
  transition: grid-template-columns var(--dur-base) var(--ease-out);
}
/* 深蓝底画在整列上（::before 通高） */
.app::before {
  content: ''; position: absolute; top: 0; bottom: 0; left: 0; z-index: -1;
  width: var(--sidebar-w, var(--sidebar-w-default));
  background: linear-gradient(180deg, var(--c-navy-900) 0%, var(--c-navy-950) 100%);
  box-shadow: inset -1px 0 0 var(--c-gold-on-navy);
  /* 与上面那条 grid 列同参数：两者一旦不同步，金线就会和侧栏右缘脱开 */
  transition: width var(--dur-base) var(--ease-out);
}
/* 拖拽改宽必须逐帧跟手：按住把手期间把这两条过渡整个关掉 */
.app.is-resizing { transition: none; }
.app.is-resizing::before { transition: none; }
.app.is-sidebar-collapsed::before { width: var(--sidebar-w-collapsed); }
.app.is-sidebar-collapsed { grid-template-columns: var(--sidebar-w-collapsed) minmax(0, 1fr); }

.app-main { min-width: 0; display: flex; flex-direction: column; }
.shell {
  width: 100%; max-width: 1560px; margin: 0 auto;
  padding: var(--sp-6) var(--sp-7) 0;
  display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0;
}
.shell > .views { flex: 1 0 auto; padding-top: var(--sp-4); }

/* ===========================================================================
   主区页面头（显示当前二级菜单名）——滚动吸顶，查询行 / 表头依次吸在其下
   =========================================================================== */
/* 这几个高度是「吸顶/滚动算式」，与下面的组件规则同生同死，所以留在本文件、
   不进 theme.css（那边只收可换肤的量）；一个色值都不写。
   ⚠ 按钮与表单控件的高度**不在这里**——只有真源 theme.css 里那一处 --btn-h。 */
:root { --page-head-h: 52px; --history-query-h: 46px;
  /* 结果区**正文行**的高度上限（图片卡片用；生成规则卡片靠 overflow 自适应，不吃这个数）：
     卡片高 - 左卡上下内边距(2×--sp-4) - 上下边框(2×1px)。
     少减这 34px 就会在图片顶到上限时把卡片撑破、整页多出滚动（实测 1920×1080 与 1600×900 各 9px）。
     ⚠ 左卡（.result-media）的内边距/边框换值时必须同步改这里；右卡 .result-rules 的内边距
     是独立的一份，改它不动这个公式。 */
  --result-cap-h: calc(100vh - var(--page-head-h) - var(--sp-6) * 2 - var(--sp-4) * 2 - 2px);
  /* 图片栏「装得下 100%」所需的最小宽：`210mm` 是这张纸的真实打印宽（CSS 绝对单位
     1in = 96px → 793.7px；与 app.js 的 A4_PRINT_PX 是同一个物理量的两种写法，谁也不抄谁的数），
     再加 176px，逐项都是量出来的：**井左右内距 2×10 + 井左右边框 2 + 左卡左右内距 2×20
     + 左卡左右边框 2 + 井与那一列之间的缝 4 + 那一列控件本身 57 = 125**，
     剩下的 51 = **井内竖向滚动条 10** + **看得见的余量 41**。
     ⚠ 那 10px 不是可省的美化：100% 时图片必然比井高，真实浏览器里那条滚动条**从内容宽里扣**
     （headless 探针量不到它——它用覆盖式滚动条，所以脚本里 `maxX=0` 而人眼看着还能左右拖，
     2026-09-27 用户报的「到 100% 还能左右拖动」就是这一条）。
     余量 41 是给「拇指捏得住、左右各留一点白」的观感，不是越紧越好。
     栅格用它反算右栏最多能让出多少（见 .result-grid）。 */
  --poster-need-w: calc(210mm + 176px); }
.page-head {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3);
  height: var(--page-head-h);
  background: var(--c-page);
  border-bottom: 1px solid var(--c-gold-soft);
}
.page-head__title { font-size: var(--fs-xl); font-weight: 600; }
/* 页面头右侧动作区（返回 / 再次生成）：顶到行尾、位于下划线之上。
   两个按钮都 hidden 时本区宽度为 0，不会把标题挤开。 */
.page-head__actions { margin-left: auto; flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--sp-2); }

/* ===========================================================================
   侧栏
   =========================================================================== */
.sidebar {
  position: sticky; top: 0;
  display: flex; flex-direction: column;
  gap: var(--sp-4);
  height: 100vh;
  background: transparent;
  padding: var(--sp-5) var(--sp-3) var(--sp-4);
  overflow: hidden;
}
.sidebar :focus-visible { outline-color: var(--c-gold-on-navy); }
/* 折叠态：品牌与一级菜单名全部让位；菜单保留图标（「生成客户计划书」「历史记录」都还在） */
.app.is-sidebar-collapsed .navgroup__title,
.app.is-sidebar-collapsed .navitem__text { display: none; }
/* 折叠态不再保留品牌方框（「展」），品牌行只留收开按钮 */
.app.is-sidebar-collapsed .sidebar__title { display: none; }
/* 品牌行只剩按钮：居中，使按钮与下方菜单图标同一条竖向中轴（两者都居中于 60px 栏宽） */
.app.is-sidebar-collapsed .sidebar__brand { justify-content: center; padding-bottom: var(--sp-3); }
.app.is-sidebar-collapsed .sidebar__toggle { margin-left: 0; }
/* 折叠态图标翻转 180°（**无过渡**：2026-09-27 用户要求点击时直接翻过去，不要动画） */
.app.is-sidebar-collapsed .sidebar__toggle-icon { transform: rotate(180deg); }
.app.is-sidebar-collapsed .sidebar__resizer { display: none; }
.app.is-sidebar-collapsed .navgroup .sidebar__menu { margin-left: 0; padding-left: 0; border-left: 0; }
/* 折叠态是「图标模式」：组的收展状态由窄栏形态接管，手动关掉的组也必须把图标亮出来，
   否则折叠后只剩品牌行、菜单凭空消失。（选择器带上 .is-closed 提高优先级压过上面的隐藏规则） */
.app.is-sidebar-collapsed .navgroup.is-closed .sidebar__menu { display: flex; }
.app.is-sidebar-collapsed .navitem { justify-content: center; padding: var(--sp-2) var(--sp-1); }
/* 折叠态只剩图标，图标放大到可辨识尺寸 */
.app.is-sidebar-collapsed .navitem__icon { width: 24px; height: 24px; }

.sidebar__brand {
  display: flex; align-items: center; gap: 6px;
  padding: 0 0 var(--sp-4);
  border-bottom: 1px solid var(--c-navy-line);
}
.sidebar__logo {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 30px; height: 30px; padding: 0 8px; border-radius: var(--r-sm);
  background: transparent; color: var(--c-gold-on-navy);
  border: 1px solid var(--c-gold-on-navy);
  font-family: var(--ui-serif); font-size: 15px; font-weight: 700; line-height: 1;
  letter-spacing: .04em; white-space: nowrap;
}
.sidebar__title {
  /* h1 默认 font-size:2em + line-height:normal 会把行盒撑到 ~38px，
     30px 的 .sidebar__logo 按基线塞进去就被顶偏，品牌行的 align-items:center
     居中的是 38px 盒对 30px 按钮盒 → 视觉上没对齐。重置字号/行高 + 改 flex，
     让标题盒高度收回到内容高度，由 flex 居中接管。 */
  display: flex; align-items: center;
  margin: 0; min-width: 0;
  font-size: inherit; line-height: 1;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* 收开按钮：豆包式无边框幽灵图标钮（panel-left 线框图） */
.sidebar__toggle {
  margin-left: auto; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0;
  border: 0; border-radius: var(--r-xs);
  background: transparent; color: var(--c-on-navy);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.sidebar__toggle:hover { background: var(--c-navy-veil-chip-hover); color: var(--c-on-navy); }
.sidebar__toggle:active { background: var(--c-navy-veil-chip-active); }
.sidebar__toggle-icon {
  flex: 0 0 auto; width: 22px; height: 22px;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}

.sidebar__nav { flex: 1 1 auto; }
/* 一级菜单：可点击收展的组头 + 二级菜单缩进引导线。
   字号用 --fs-md(15px)：比二级菜单的 --fs-base(14px) 略大一点点，保持层级区分但不抢眼。 */
.navgroup__title {
  width: 100%; display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3) var(--sp-1);
  border: 0; border-radius: var(--r-sm); background: transparent;
  color: var(--c-on-navy); cursor: pointer; text-align: left;
  font: inherit; font-size: var(--fs-md); font-weight: 400; letter-spacing: .02em;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.navgroup__title:hover { background: var(--c-navy-veil-row-hover); color: var(--c-on-navy); }
.navgroup__title:active { background: var(--c-navy-veil-row-active); }
.navgroup__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.navgroup__chevron {
  flex: 0 0 auto; width: 18px; height: 18px;
  fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
  color: var(--c-on-navy-muted);
  transition: transform var(--dur-base) var(--ease-out);
}
.navgroup.is-closed .navgroup__chevron { transform: rotate(-90deg); }
.navgroup.is-closed .sidebar__menu { display: none; }
.navgroup .sidebar__menu {
  /* 一级菜单与首个二级菜单之间留出呼吸（组头自身只有 4px 下内距 + 列表 2px 行距） */
  margin: var(--sp-2) 0 0 var(--sp-3); padding-left: var(--sp-2);
  border-left: 1px solid var(--c-navy-line);
}
/* 侧栏右缘拖拽把手 */
.sidebar__resizer {
  position: absolute; top: 0; bottom: 0; right: 0; width: 6px;
  cursor: col-resize; z-index: 5;
}
.sidebar__resizer::after {
  content: ''; position: absolute; top: 0; bottom: 0; left: 2px; width: 2px;
  background: transparent;
  transition: background var(--dur-fast) var(--ease-out);
}
.sidebar__resizer:hover::after,
.sidebar__resizer.is-active::after { background: var(--c-gold-on-navy); }
.sidebar__menu { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.navitem {
  width: 100%; display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3); border: 0; border-radius: var(--r-sm);
  background: transparent; color: var(--c-on-navy-muted);
  font: inherit; font-size: var(--fs-base); text-align: left; cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.navitem:hover { background: var(--c-navy-veil-row-hover); color: var(--c-on-navy); }
.navitem:active { background: var(--c-navy-veil-row-active); }
.navitem.is-active {
  background: linear-gradient(90deg, rgba(var(--c-gold-soft-rgb), .18), rgba(var(--c-gold-soft-rgb), .05));
  color: var(--c-on-primary); font-weight: 600;
  box-shadow: inset 0 0 0 1px rgba(var(--c-gold-soft-rgb), .38);
}
/* 当前页那一颗也要有 hover 反馈：`.navitem.is-active` 与 `.navitem:hover` 特异性相同、
   写在后面，所以不补这条的话，鼠标移到「正在看的这一项」上完全不动。 */
.navitem.is-active:hover {
  background: linear-gradient(90deg, rgba(var(--c-gold-soft-rgb), .32), rgba(var(--c-gold-soft-rgb), .10));
  box-shadow: inset 0 0 0 1px rgba(var(--c-gold-soft-rgb), .60);
}
.navitem.is-active:active {
  background: linear-gradient(90deg, rgba(var(--c-gold-soft-rgb), .44), rgba(var(--c-gold-soft-rgb), .16));
}
.navitem__icon {
  flex: 0 0 auto; width: 18px; height: 18px;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
.navitem.is-active .navitem__icon { color: var(--c-gold-on-navy); }
.navitem__text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.run-id {
  background: var(--c-surface-3); padding: 1px 6px; border-radius: var(--r-xs);
  border: 1px solid var(--c-border-faint); color: var(--c-text-2);
  overflow-wrap: anywhere;
}
.muted { color: var(--c-text-muted); }

/* ===========================================================================
   面板 / 按钮 / 表单
   =========================================================================== */
.panel {
  background: var(--c-surface);
  border: 1px solid var(--c-border-soft);
  border-radius: var(--r-lg);
  padding: var(--sp-4) var(--sp-5);
  box-shadow: var(--sh-sm);
}
.panel + .panel { margin-top: var(--sp-4); }
.panel__title { font-size: var(--fs-lg); font-weight: 600; margin: 0; }
/* 文件名与大小**挨着**（2026-09-27 用户点名「隔得太远了」）：从前 `space-between` 把大小钉在卡片
   右缘，实测那一空 **819px**（整行内容宽 956，即 86% 是空的），读起来像两条不相干的信息。
   与「执行过程主项目那一行整组靠左」同一条口径：整组靠左、行尾留空，不靠右边缘对齐。 */
.panel__head {
  display: flex; justify-content: flex-start; gap: var(--sp-3);
  align-items: center; flex-wrap: wrap;
  padding-bottom: var(--sp-3); margin-bottom: var(--sp-3);
  border-bottom: 1px solid var(--c-gold-soft);
}

/* 全站一颗按钮档：高度只从 theme.css 的 --btn-h 来，纵向 padding 一律归零，
   由 flex 居中把文字摆在 28px 的中轴上。以前「主/次按钮字号不同就会差几像素」
   是各容器另钉 height 的理由（.generate-actions、--history-control-h、.btn--sm），
   现在那三处都收掉了，本条是唯一的高度来源。 */
/* 描边整体**退一档浅色**（2026-09-27 用户报「线条太重」）：按钮 #C9D2DC→#D8DEE5，
   输入框 / 幽灵按钮 / 排序方块 #7E8794→#C9D2DC。变浅之后 hover 才有地方「变深」——
   各条 `:hover` 早就在改 border-color（.btn→--c-primary-300、.input→--c-primary-300、
   .btn--ghost→--c-primary-400、.sortbtn→--c-primary）， resting 越浅，抬手那一下越明显。
   ⚠ 这四档 resting 色只许用 --c-border / --c-border-strong 两个 token，别在组件里另写十六进制。 */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: var(--btn-h); min-height: var(--btn-h); padding: 0 var(--sp-4);
  border-radius: var(--r-sm);
  border: 1px solid var(--c-border);
  background: var(--c-surface); color: var(--c-text-2);
  font: inherit; font-size: var(--fs-base); font-weight: 500;
  /* 结果页抬头那颗「预览」是 `<a class="btn">`（要 target=_blank 才开得了新标签），
     UA 样式表给链接默认画下划线——不在这里抹掉，屏上就是一颗带下划线的"按钮"，
     跟别的按钮差一眼看得见（2026-09-28 截图实测）。对 <button> 无副作用：它本来就不画。 */
  text-decoration: none;
  cursor: pointer; white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
/* 悬停与按下必须**看得出来**（2026-09-26 用户报：原先 hover 只是白→--c-surface-2、active 再深一档，
   投影屏上几乎无变化）。所以一次动效同时改底色、描边、阴影并抬 1px；按下时落回并压深一档，
   给出「按得动」的物理反馈。禁用态一律不参与（:not(:disabled)）。 */
.btn:hover:not(:disabled) {
  background: var(--c-primary-50); border-color: var(--c-primary-300); color: var(--c-text);
  box-shadow: var(--sh-sm); transform: translateY(-1px);
}
.btn:active:not(:disabled) {
  background: var(--c-primary-100); border-color: var(--c-primary-400);
  box-shadow: none; transform: translateY(0);
}
.btn:disabled { opacity: .48; cursor: not-allowed; }
.btn--primary {
  background: var(--c-primary); border-color: var(--c-primary); color: var(--c-on-primary); font-weight: 600;
  letter-spacing: .02em; box-shadow: var(--sh-sm);
}
.btn--primary:hover:not(:disabled) {
  background: var(--c-primary-600); border-color: var(--c-primary-600);
  color: var(--c-on-primary); box-shadow: var(--sh-lg-md);
}
.btn--primary:active:not(:disabled) { background: var(--c-primary-700); border-color: var(--c-primary-700); }
.btn--ghost { background: transparent; border-color: var(--c-border-strong); color: var(--c-text-2); }
.btn--ghost:hover:not(:disabled) {
  background: var(--c-primary-50); color: var(--c-primary-600); border-color: var(--c-primary-400);
}
.btn--ghost:active:not(:disabled) { background: var(--c-primary-100); color: var(--c-primary-700); }

.actions { display: flex; gap: var(--sp-3); flex-wrap: wrap; margin-top: var(--sp-3); }

.input {
  width: 100%; max-width: 420px; padding: 6px 10px;
  border: 1px solid var(--c-border-strong); border-radius: var(--r-sm);
  font: inherit; font-size: var(--fs-base); background: var(--c-surface); color: var(--c-text);
  transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.input:hover { border-color: var(--c-primary-300); }
.input:focus {
  border-color: var(--c-primary-400);
  box-shadow: 0 0 0 3px var(--c-primary-50);
}
.inline-hint { margin-top: var(--sp-2); font-size: var(--fs-sm); }
.banner { border-radius: var(--r-md); padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-3); font-size: var(--fs-base); }
.banner--danger { background: var(--c-danger-soft); border: 1px solid var(--c-danger-line); color: var(--c-danger-ink); }

/* ===========================================================================
   上传
   =========================================================================== */
.dropzone {
  border: 1.5px dashed var(--c-border-strong); border-radius: var(--r-lg);
  background: var(--c-surface-2); padding: var(--sp-6) var(--sp-5); text-align: center;
  transition: border-color var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out);
}
.dropzone:hover { border-color: var(--c-primary-200); background: var(--c-surface); }
.dropzone.is-drag { border-color: var(--c-primary); background: var(--c-primary-soft); }
.dropzone__icon {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 46px; height: 46px; margin: 0 auto var(--sp-2);
  border-radius: var(--r-sm); background: var(--c-primary-soft); color: var(--c-primary);
  border: 1px solid var(--c-primary-100);
  font-family: var(--ui-mono); font-weight: 600; letter-spacing: .06em; font-size: var(--fs-sm);
}
.dropzone__idle { margin-top: var(--sp-2); font-size: var(--fs-lg); font-weight: 500; color: var(--c-text); }
.dropzone__sub { color: var(--c-text-muted); font-size: var(--fs-sm); margin-bottom: var(--sp-3); }

/* ===========================================================================
   结果图
   =========================================================================== */
.poster-host {
  background: var(--c-surface-2); border: 1px solid var(--c-border-faint);
  border-radius: var(--r-md); padding: var(--sp-4); min-height: 240px;
  display: flex; align-items: center; justify-content: center;
}
.poster-image {
  /* 外观一份（边框 / 圆角 / 阴影 / 底）；**宽与缩放不在这里**——那一条住
     `.result-media .poster-image`，因为全站只有结果页这一处用这张图（index.html 里
     唯一一份 .poster-host > .poster-image）。早先这里也写了一份 `var(--zoom)`，
     那是上一代缩放口径留下的影子通道，已随它一起删。 */
  max-width: 608px; height: auto; background: var(--c-surface);
  border: 1px solid var(--c-border-strong); border-radius: var(--r-xs);
  box-shadow: var(--sh-lg); cursor: default;
}

/* 「生成结果」这一页：整页**不支持鼠标滚动**（图片在其区内滚 / 缩放拖动都在区内完成）。
   由 app.js 的 syncGenerateActions() 按 `#result` 这个事实挂到 body 上；
   「生成完成」那一屏（上面一整块都留着）不挂，照常滚。 */
body.no-page-scroll { overflow: hidden; }

/* ---- 结果区：**两块各自独立的白色卡片**（图片区 / 生成规则），左卡按内容收宽、右卡吃剩余宽 ----
   外层 .result-panel 只负责「按视口收高 + 双栏栅格」，自己**不再是**白底面板
   （面板外观搬到了两张卡片上，见 .result-media / .result-rules）。 */
.result-panel {
  display: flex; flex-direction: column;
  height: calc(100vh - var(--page-head-h) - 2 * var(--sp-6));
  min-height: 0;
  background: transparent; border: 0; box-shadow: none; padding: 0;
}
.result-panel > .result-grid { flex: 1 1 auto; min-height: 0; }
.result-panel .result-media,
.result-panel .result-rules { min-height: 0; }
/* 两张卡片共用同一套外观；内边距**各一份**：左卡（图片）的上下内边距进 `--result-cap-h`
   的公式（见 :root），所以动它必须同步改那条 calc；右卡（生成规则）的上下左右都只是留白，
   不参与任何计算，只要「文字别贴着白边」。 */
.result-media,
.result-rules {
  background: var(--c-surface); border: 1px solid var(--c-border-soft);
  border-radius: var(--r-lg); padding: var(--sp-4) var(--sp-5);
  box-shadow: var(--sh-sm);
}
.result-rules { padding: var(--sp-6) var(--sp-7); }
/* 图片区高度上限：**直接算**，不依赖百分比穿透 grid/flex（那一路 Chrome 会按内容反推、
   图片反而撑破整页）。超过上限就在图片区**内部**滚，所以整页不出现纵向滚动。 */
.result-panel .result-media .poster-host {
  max-height: var(--result-cap-h);
}

/* 结果双栏：左栏 = 图片区（弹性宽，且**至少**给到「装得下 100% 这张纸」的 `--poster-need-w`），
   右栏 = 生成规则（目标 40%，但为了让出那一格可以一路让到 280px 的下限）。
   右轨的 max 写成 `min(40%, 100% - 所需宽 - 间距)`：宽窗口时它仍是 40%，窄窗口时先让图片栏
   吃饱、吃不饱才轮到 280px 下限兜住（2026-09-27 实测 1920 与 1600 两档图片栏都是 919.7px，
   正好放得下 794px 的图 + 那一列工具；1440 及以下放不开，图片在井**内**横向滚，不顶破页面）。
   **两栏等高、底边对齐**：栅格只有一行，`grid-template-rows: minmax(0, 1fr)` 把这一行
   钉成栅格自己的高（栅格高 = `.result-panel` 扣掉头之后的可视高，见上面 `flex: 1 1 auto`），
   两张白卡再被默认的 `align-items: stretch` 拉满这一行 —— 谁的短都不影响底边齐平，
   规则很长时在卡内滚（`.result-rules { overflow: auto }`）。
   ⚠ 别把右轨写成 `auto`：文本的 max-content 是「整段一句话一行」，那会把左栏顶到 0，
   等于把 2026-09-26 那次「右栏被图片压成 0 宽」的故障照抄一遍、只是换了方向。
   ⚠ 左轨的下限必须是 0（所需宽放在**右轨的 max** 里反着让）：把 `--poster-need-w` 写成左轨的
   min，就和右轨的 280px 下限一起构成「两条最小值之和」，窄桌面（901–1240）会直接顶出横向滚动。
   中间那条缝是 `--sp-7`(32px)。 */
.result-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr)
    minmax(280px, min(40%, calc(100% - var(--poster-need-w) - var(--sp-7))));
  grid-template-rows: minmax(0, 1fr);
  column-gap: var(--sp-7); row-gap: var(--sp-5);
}
/* 左栏横向排：左图片（吃满剩余）、右一列工具。
   那一列**始终钉在卡片最右边**（图片井 `flex: 1 1 auto` 把它顶到右缘），图片在井里居中，
   所以图片放大缩小都不会带着那一列左右跑。
   图片与工具列之间只留 `--sp-1`(4px)。 */
.result-media { min-width: 0; display: flex; flex-direction: row; align-items: flex-start; gap: var(--sp-1); min-height: 0; }
/* 两颗操作按钮 + 缩放一列：竖排在图片**右边**、贴着卡片右缘。
   `align-items: stretch` 让这一列里的东西一样宽：列宽由最宽的一颗（「下载」多一个下拉箭头）定，
   「预览」被拉伸到同宽；按钮自己不写 width。 */
.result-media .media-tools {
  display: flex; flex: 0 0 auto;
  flex-direction: column; align-items: stretch; gap: var(--sp-2);
}
/* 图片旁这一列：只把**横向**内边距收到 --sp-1(4px)，好让按钮贴紧图片右沿；
   高度/字号/圆角一律沿用统一的 .btn 档，这里不再另写一份。
   按钮宽度交给 stretch（见上），不加 min-width。 */
.result-media .media-tools .btn {
  width: auto; min-width: 0; padding-left: var(--sp-1); padding-right: var(--sp-1);
}
/* 结果页那颗「下载」的挂载点。必须 `display: contents`：不抹掉这一层的话，
   它就成了 `.media-tools`（flex 列、`align-items: stretch`）的 flex item，
   「下载」不再和「预览」被拉成等宽，两颗按钮右边对不齐。 */
.dl-slot { display: contents; }
/* 「下载 ↓」的下拉：贴着按钮下沿往右弹，浮在图片之上 */
.dl { position: relative; display: inline-flex; }
.dl__btn { display: inline-flex; align-items: center; }
.dl__caret { display: inline-flex; margin-left: 2px; }
/* `position: fixed` + 由 placeDlMenu 写死 top/left/minWidth：absolute 会被
   历史表那条 `overflow-x: auto` 的滚动容器裁掉半截，fixed 不属于任何祖先的裁剪区。
   所以这里**不能**留 top/left（百分比高度会算进视口）；min-width 交给内联值，
   写 100% 的话包含块变成视口。
   z-index 必须高于吸顶的页面头(40)与查询行(35)：fixed 脱离了表格的层叠上下文，
   现在是和它们在全局抢绘制顺序；仍低于 toast(80)。 */
.dl__menu {
  position: fixed; z-index: 60;
  padding: var(--sp-1);
  display: flex; flex-direction: column; gap: 1px;
  background: var(--c-surface); border: 1px solid var(--c-border-soft);
  border-radius: var(--r-sm); box-shadow: var(--sh-lg-md);
}
.dl__menu[hidden] { display: none; }
.dl__item {
  padding: 3px 10px; border: 0; border-radius: var(--r-xs);
  background: transparent; color: var(--c-text-2);
  /* 下拉里这两项从 --fs-sm(12px) 提到正文档 14px（2026-09-27 用户要求）：它是**可点的选项**，
     不是注释性说明，比按钮上的字还小一档会读成脚注。行高跟着 20→22，别让 14px 的字挤在 20px 里。
     生成结果页与历史页每一行吃的是**同一份模板**（index.html 的 `.dl` 子树 cloneNode），
     所以这一处改完两处一起变，不存在「只改了一页」。 */
  font: inherit; font-size: var(--fs-base); line-height: 22px;
  text-align: left; white-space: nowrap; cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.dl__item:hover { background: var(--c-primary-100); color: var(--c-primary-700); }
.dl__item:active { background: var(--c-primary); color: var(--c-on-primary); }
.dl__item:focus-visible { background: var(--c-primary-soft); color: var(--c-text); outline: 2px solid var(--c-primary-400); outline-offset: -2px; }
/* 「打印 ↓」那个离屏框（见 app.js 的 printArtifact）：必须有真实尺寸——`display: none` 或 0×0
   会让浏览器打出一张白纸，所以整框挪到视口左边一万个像素之外（负偏移不撑出横向滚动条）。
   尺寸与 `@page` 同为 A4，页面里只有一张图，不参与本站版面。 */
.print-frame {
  position: fixed; top: 0; left: -10000px;
  width: 210mm; height: 297mm; border: 0;
}
/* 「已选文件」那一行：状态文字在左，「重新选择文件」紧随其后 */
.progress__row {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
}
.progress__status { margin: 0; }
/* 生成页主操作行：「已选文件」面板之外、下方那一行。两颗按钮的高度由统一的 .btn 给，
   这里只排行间距，不再另钉一份高度。 */
.generate-actions {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin-top: var(--sp-4);
}

/* ---- 「智能体执行过程」（2026-09-27 用户要求：点开始生成后在按钮下方逐行落屏）----
   这里只管**怎么画**；**多快**没有一份常量可管——一行在它被观察到的那一刻整行落屏
   （2026-09-27 用户第三次点名「实际有多快就打印多快」，那一档 `TRACE_PACE` 已随打字队列一起删）。
   ⚠ 色值一律 var(...)，新色进 theme.css 的 :root，本文件不写字面量（§5 铁律）。
   **不成卡片**（2026-09-27 用户第二次点名，样式参照「一步一行、当前那步摊开」的步骤条）：
   整块无底色 / 无描边 / 无圆角 / 无阴影，标题行与每一档直接坐在页面背景上。
   ⚠ 别再往 `.trace` 上补 `border` / `background` / `box-shadow`：那三样就是被点掉的那圈「盒子」。 */
.trace { margin-top: var(--sp-4); }
/* 标题行左缘与下面各档的 `<summary>` 对齐（同一份横向内距），否则「智能生成引擎」会缩进去一截 */
.trace__head {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-4);
}
.trace__pulse {
  flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%;
  background: var(--c-gold);
  animation: trace-pulse var(--dur-blink) var(--ease-inout) infinite;
}
.trace__title { font-size: var(--fs-sm); font-weight: 600; color: var(--c-text-2); letter-spacing: .02em; }
/* 容器只管「什么字」；一行的内距住在每一档自己身上（`.trace__seg__body`）。
   等宽：行里全是数字与 sha 前缀，比例字体会让同一列数字左右错开。
   ⚠ 这里**不再**有 `max-height` + `overflow-y`（同日用户点名去掉滚动条）：面板要多高就多高，
   跟着落屏往下走的是**整页**——见 app.js 的 `scrollTraceToEnd`，它只在最后一行掉出视口时才动。
   ⚠ 字号与主项目（`.trace__seg__head` 的 `--fs-sm`）**同档 12px**：从前小一档（11px），
   2026-09-27 用户点名「改大一点点」后收齐这一格——整块没有卡片兜着，
   「哪几行属于这一档」从此全靠下面那块的缩进、底色与那一层灰字说。 */
.trace__lines {
  font-family: var(--ui-mono); font-size: var(--fs-xs); line-height: 1.75;
}
/* ---- 一档 = 一个 <details>（2026-09-27 用户要求分段，且每段可展开/收起）----
   还在等云端的那一档摊开，观察到下一阶段时由 `openSegment` 收起、由 `markTraceDone` 在终态一并收起。
   用原生 details 而不是照侧栏那套「按钮 + aria-expanded」自己管：键盘（Enter/Space）、
   读屏的展开态、点开即收的手势都是白得的，这一屏的行又只由脚本增删，没有别的状态要存。 */
.trace__seg { border-top: 1px solid var(--c-border-faint); }
.trace__seg:first-child { border-top: 0; }
.trace__seg__head {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  list-style: none;            /* 摘掉浏览器那颗三角，箭头由下面的 ::after 自己画 */
  cursor: pointer; user-select: none;
  font-family: var(--ui-font); font-size: var(--fs-sm); font-weight: 600;
  letter-spacing: .02em; color: var(--c-text-2);
  transition: background var(--dur-fast) var(--ease-out);
}
.trace__seg__head::-webkit-details-marker { display: none; }
.trace__seg__head:hover { background: var(--c-surface-2); }
/* 箭头：收起时尖朝右、展开时尖朝下（与侧栏一级菜单同一套手势语言）。
   两条边框拼出这个角，比引一张图标省一个请求，也不会随字体缺失而变形。 */
.trace__seg__head::after {
  content: ''; flex: 0 0 auto; width: 7px; height: 7px;
  border-right: 1.5px solid var(--c-text-muted); border-bottom: 1.5px solid var(--c-text-muted);
  transform: rotate(-45deg);
  transition: transform var(--dur-base) var(--ease-out);
}
.trace__seg[open] > .trace__seg__head::after { transform: rotate(45deg); }
.trace__seg__mark { flex: 0 0 14px; text-align: center; }
/* 名称**不许**撑满剩余宽度（从前是 `flex: 1 1 auto`）：它一撑开，「13 条」和那颗箭头就被推到
   行尾，与名称之间空出大半行（2026-09-27 用户点名「隔得太远」）。整组靠左，行尾留空。 */
.trace__seg__title { flex: 0 1 auto; min-width: 0; }
/* 行数只在**收起之后**才有（`settleSegment` 那一刻才数得准）；摊开给人读的那一档还在往下落行，报数是噪音 */
.trace__seg__count { font-weight: 400; color: var(--c-text-muted); }
.trace__seg.is-running .trace__seg__count { display: none; }
/* 还在等云端的那一档：记号跟着面板顶部的呼吸点一起闪，「还在动 = 还没完」的信号不断档 */
.trace__seg.is-running .trace__seg__mark {
  color: var(--c-gold);
  animation: trace-pulse var(--dur-blink) var(--ease-inout) infinite;
}
.trace__seg.is-ok .trace__seg__mark { color: var(--c-ok); }
.trace__seg.is-fail .trace__seg__mark,
.trace__seg.is-fail > .trace__seg__head { color: var(--c-danger); }
/* 子项目那一块（摊开档位里那一行一句的过程）：从主项目左缘再往里缩一格，并铺一层底色，
   于是在「整块不成卡片」的面板里仍能一眼看出这几行归哪一档。
   ⚠ 底色只给这一块，不给 `.trace`——用户点掉的是外面那圈盒子（见本节开头）。 */
.trace__seg__body {
  list-style: none;
  margin: 0 var(--sp-4) var(--sp-3) var(--sp-7);
  padding: var(--sp-2) var(--sp-3);
  background: var(--c-surface-2);
  border-radius: var(--r-xs);
}
.trace__line { display: flex; gap: var(--sp-2); color: var(--c-text-muted); }
.trace__mark { flex: 0 0 14px; text-align: center; }
.trace__body { min-width: 0; white-space: pre-wrap; word-break: break-word; }
.trace__line--ok { color: var(--c-text-2); }
.trace__line--ok .trace__mark { color: var(--c-ok); }
/* 关键结论：「轻重」只由字重与主色说（2026-09-27 起不再靠打字速度差——一行落屏就是落屏） */
.trace__line--key { color: var(--c-primary); font-weight: 600; }
.trace__line--key .trace__mark { color: var(--c-primary); }
.trace__line--fail { color: var(--c-danger); }
.trace__line--fail .trace__mark { color: var(--c-danger); }
/* 小标题行：一条横线 + 一句字，把「这些数字从哪儿来」这句交代与下面那一串读数分开。
   （分段之后档名已经在 `<summary>` 上了，所以这一行不再重复「核验回执」四个字。） */
.trace__line--head {
  align-items: center; gap: var(--sp-2);
  margin-top: var(--sp-2); color: var(--c-text-muted); font-weight: 600;
}
.trace__line--head::after {
  /* 底色是子项目那一块的 `--c-surface-2`，横线得比档间那条重一档才量得出来 */
  content: ''; flex: 1 1 auto; height: 1px; background: var(--c-border-soft);
}
.trace__line--head .trace__mark { display: none; }
/* 光标只挂在**正在等云端**那一行（`kind === 'run'`），结算即摘（见 `appendTraceLine` / `settleOpenRun`）
   —— 它说的是「这件事还没回来」，不再是「正在打字」。它住在 `.trace__body` 里、
   紧跟正文末尾，所以必须是 inline-block：块级会另起一行，纯 inline 又撑不出 width/height。
   ⚠ 基础态必须是不透明：`prefers-reduced-motion` 那条把动画压成 .01ms，
   光标就停在基础态上——基础态给透明的话，减少动效的人等于没有光标。 */
.trace__caret {
  display: inline-block; width: 7px; height: 1.05em; vertical-align: text-bottom;
  margin-left: 2px; background: var(--c-primary-200);
  animation: trace-blink var(--dur-blink) steps(2, jump-none) infinite;
}
@keyframes trace-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
@keyframes trace-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
/* 这一段跑完了（成功或停在半路）：呼吸点定住并换成状态色，让「还在动 = 还没完」这个信号
   不会在打完之后继续骗人。色值来自 token，本行不写字面量。 */
.trace.is-settled .trace__pulse { animation: none; background: var(--c-ok); opacity: 1; }
.trace.is-settled.is-failed .trace__pulse { background: var(--c-danger); }

/* ---- 打印完的收尾：**不自动跳页**，点「查看结果」才走 ----
   不成卡片：无底色 / 无描边 / 无圆角 / 无阴影，只留一条绿色左规（成功色，见 §3 色值只走 token）。
   竖排：标题、说明、按钮各占一行，按钮落在说明**下面**并与文字左缘对齐。 */
.done {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-3);
  margin-top: var(--sp-4); padding-left: var(--sp-4);
  border-left: 3px solid var(--c-ok);
}
.done__text { min-width: 0; display: flex; flex-direction: column; gap: var(--sp-1); }
.done__title { font-size: var(--fs-lg); font-weight: 600; color: var(--c-primary); }
.done__sub { font-size: var(--fs-sm); color: var(--c-text-muted); }

/* ---- 右栏「生成规则」的标题：不在这里另立一条 ----
   从前这里是 `.result-head__title`（全站标题 16px，右栏这一条压到 15px，底下还画一条金色横线，
   并且靠 `min-height: 38px` + `padding-bottom` 撑出一条标题带）。2026-09-27 用户要求
   **横线去掉、字号跟「免责声明」一样大** —— 改完它与 `.rules-subtitle` 一字不差，
   于是整条删掉、HTML 上直接挂 `.rules-subtitle`（一处定义，两块标题不会各自漂移）。
   整页不滚动（见 .result-panel 收高），所以这里也从来没有 sticky 的事。 */
/* 左区内容：图片井（缩放 / 拖动都在这一块里）。
   图片的显示宽由 app.js 写在井上的 `--poster-w`（一个具体像素数）决定，
   所以放大只会让图片在井**内部**溢出、不会把页面布局顶来顶去；溢出后在区内滚动或按住拖动。
   井 `flex: 1 1 auto` 吃满卡片剩余宽 —— 那一列工具因此被顶到卡片最右边，
   图片自己在井里居中（见 .poster-image 的 margin-inline），两边互不影响。
   （早先这里还套着一层 .poster-frame，只为给悬浮缩放条当定位祖先；缩放条搬去工具列后删了。） */
.result-media .poster-host {
  position: relative; display: block; min-height: 0; min-width: 0;
  flex: 1 1 auto;
  padding: var(--zoom-pad, 10px); overflow: auto;
  /* 图片区**不出滚动条**（用户点名）：`overflow: auto` 留着，所以放大后照样能滚、能拖动平移，
     只是那条 10px 不再占位——于是「适宽」比有条时宽出 10px，这是要的。
     「滚动条」那一节是按 `*` 通配的，这里用更长的选择器压掉它。 */
  scrollbar-width: none;
  /* 图片区整块是白卡片，井底也用白：灰色井底会在白卡里再多出一层灰框 */
  background: var(--c-surface);
}
.result-media .poster-host::-webkit-scrollbar { width: 0; height: 0; display: none; }
/* 只有真的溢出、需要平移时，才把触摸手势从浏览器手里拿走
   （`is-touch-pan` 由 app.js 按实测溢出与否挂上）。放在常比例下会吞掉手机上的滑动。 */
.result-media .poster-host.is-touch-pan { touch-action: none; }
.result-media .poster-image {
  display: block;
  /* 宽是唯一的驱动轴：`--poster-w` 没落笔时退回「占满井宽」，两种都不是固有宽 2640，
     所以图片永远不会把左轨顶开（那正是 2026-09-26 右栏被压成 0 宽的成因）。
     高一律交给固有比例推导（A4 竖版 2640×3736），井的 `max-height: --result-cap-h`
     负责把超出视口的部分收进井内滚动。
     `margin-inline: auto` 让图片在井里居中；比井宽时按 CSS 的规则两侧 auto 归 0、
     图片左对齐 + 井内横向滚 —— 不会像 `justify-content: center` 那样把左半边裁到够不着。 */
  width: var(--poster-w, 100%); height: auto; max-width: none;
  margin-inline: auto; user-select: none;
}
/* 溢出后可拖动：抓手光标 + 拖动中禁止选中 */
.result-media .poster-image.is-pannable { cursor: grab; }
.result-media .poster-image.is-panning { cursor: grabbing; }
/* 缩放那一列：放大按钮 / 竖向拖动条 / 缩小按钮 / 读数 / 档位名 / 两颗预设按钮，竖排，
   跟在「下载」下面，与预览·下载同属 `.media-tools` 那一列（所以它始终靠着图片栏的右缘、
   不随图片宽窄跑）。它是**常驻控件**，不再是悬浮在图上的半透明条：早先那套「平时 opacity: 0、
   hover 才现、子控件 pointer-events 穿透」的命中测试机器全部随之删掉——它已经不在图片上面了。 */
.result-media .zoom-tools {
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-1);
  /* 与上面那颗「下载」之间留一档呼吸（用户要求整组再往下挪一点：8 → 20px） */
  margin-top: var(--sp-5);
  padding: 0; border: 0; background: none; border-radius: 0;
}
.zoom-tools__field { display: flex; align-items: center; justify-content: center; }
/* 档位名：跟在百分比读数下面，比读数低一档色。只有**没有那颗预设亮着**时才出现
   （现在只剩窄屏的「适合栏宽」这一档，见 app.js 里 label 那段），不出现时整块 `hidden` 不占位。 */
.zoom-tools__fit {
  font-size: var(--fs-xs); color: var(--c-text-muted); white-space: nowrap;
}
.zoom-tools__range {
  /* 竖向拖动条：向上推 = 放大，与头顶那颗放大按钮同向。
     `writing-mode` 是标准写法（厂商旧写法 `appearance: slider-vertical` 已不必用），
     但**光换 writing-mode 不够**：2026-09-27 在本机 Chrome 逐个实测（点上端 / 点下端 / ArrowUp），
     `vertical-lr` 与 `vertical-rl` 都是**最小值在上端**（上端→30、下端→200，ArrowUp 反而减档），
     只有 `direction: rtl` 会把方向翻过来（上端→200、下端→30，ArrowUp 增档），
     与旧写法 `appearance: slider-vertical` 的实测结果同向。所以这条 rtl 是承重的，不是装饰。 */
  writing-mode: vertical-lr; direction: rtl;
  /* 自绘：`appearance: none` 之后 UA 那根灰粗条连同它自带的悬停高亮一起消失，剩下的全写在
     下面这一组伪元素里。22×116 是**命中盒**，画出来的轨道只有 3px——拇指要捏得住，线要够细。 */
  appearance: none; -webkit-appearance: none;
  /* 悬停要改的三件事全部抽成**变量**，伪元素只负责「在哪个轴上消费它」：
     颜色两个（渐变只写一处，重抄 --zoom-fill 就等于把「金线停在哪」写第二遍，早晚和拇指脱开）、
     粗细一个（`--zoom-track-t`）。粗细必须是变量而不是 `:hover { width: 4px }`：
     竖屏时轨道的**粗细**住在 `width`，窄屏躺平成横条之后住在 `height`，
     把物理属性写进 hover 会在窄屏那一条上跨轴失灵（2026-09-27 实测：横条加粗无效，
     而拇指的 `margin-left` 被 hover 规则带着往左挪 4.5px）。
     拇指的居中偏移由粗细算出来（半径差），所以粗细一涨它就自动重新压线正中。 */
  --zoom-track-on: var(--c-gold-soft);
  --zoom-track-off: var(--c-primary-50);
  --zoom-track-t: 3px;
  --zoom-thumb-d: 13px;
  --zoom-thumb-off: calc((var(--zoom-track-t) - var(--zoom-thumb-d)) / 2);
  /* 光标**保持箭头**（2026-09-27 用户点名）：原先给 `ns-resize`，鼠标一移到那一列就变双箭头，
     看着像整块区域在等着被拖，而这一列的其余成员（按钮 / 读数 / 预设）全是箭头。
     ⚠ 但「反馈」还是要给的——见下面那组 :hover：轨道加粗、拇指描边加深。上一版把
     「鼠标停在条上不许变样」理解成整组不写 :hover，那是过头了：用户不要的是指针变形，
     不是不要悬停反馈。光标与反馈这两件事由两条不同的见证分别钉住。 */
  width: 22px; height: 116px; margin: 0; cursor: default;
  background: transparent;
}
/* 轨道：一根发丝线，下半段（从底边到拇指）走金、上半段走冷灰。
   已选段的比例 `--zoom-fill` 由 app.js 落在输入框上（真源是滑块**贴过档之后**的值，
   不是读数——拇指停在哪，金线就该到哪）。渐变用物理方向 `to top`，与 rtl 无关。 */
.zoom-tools__range::-webkit-slider-runnable-track {
  width: var(--zoom-track-t); height: 116px; margin: 0 auto; border-radius: var(--r-pill);
  background: linear-gradient(to top,
    var(--zoom-track-on) 0 var(--zoom-fill, 50%),
    var(--zoom-track-off) var(--zoom-fill, 50%) 100%);
}
.zoom-tools__range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  /* 全局那条 `*` 选不到厂商伪元素，不显式钉 border-box 的话描边一加粗，
     圆点外圈就从 13 涨到 15——悬停一下拇指自己会“长大半格”。 */
  box-sizing: border-box;
  width: var(--zoom-thumb-d); height: var(--zoom-thumb-d);
  margin-left: var(--zoom-thumb-off);   /* 圆点压在线正中：(粗细 − 直径) / 2 */
  border: 1px solid var(--c-primary-300); border-radius: 50%;
  background: var(--c-surface); box-shadow: var(--sh-sm);
}
/* 悬停：线 3→4px、圆点描边 1→2px 并加深两档，两段轨道色也各深一档（用户要求「加粗 + 变深」）。
   这里**一个几何属性都不写**——粗细与居中偏移全走上面那三枚变量，所以窄屏躺平成横条之后
   同一条 hover 依然有效（那一条的粗细住在 height）。只粗不放大：外圈仍 13px、
   命中盒仍 22×116，悬停不会顶动任何布局。 */
.zoom-tools__range:hover {
  --zoom-track-on: var(--c-gold); --zoom-track-off: var(--c-primary-100);
  --zoom-track-t: 4px;
}
.zoom-tools__range:hover::-webkit-slider-thumb {
  border-width: 2px; border-color: var(--c-primary-400);
}
/* Firefox：同一套设计（`::-moz-range-progress` 天生就是「已选段」，不必再读 --zoom-fill） */
.zoom-tools__range::-moz-range-track {
  width: var(--zoom-track-t); height: 116px; margin: 0 auto; border-radius: var(--r-pill);
  background: var(--zoom-track-off);
}
.zoom-tools__range::-moz-range-progress {
  width: var(--zoom-track-t); border-radius: var(--r-pill); background: var(--zoom-track-on);
}
.zoom-tools__range::-moz-range-thumb {
  box-sizing: border-box;   /* 同上：加粗描边不许把圆点顶大 */
  width: 11px; height: 11px;
  border: 1px solid var(--c-primary-300); border-radius: 50%;
  background: var(--c-surface); box-shadow: var(--sh-sm);
}
.zoom-tools__range:hover::-moz-range-thumb { border-width: 2px; border-color: var(--c-primary-400); }
.zoom-tools__range:focus { outline: none; }
.zoom-tools__range:focus-visible::-webkit-slider-thumb { border-color: var(--c-primary); }
/* 放大 / 缩小一档：26px 的图标控件（不参与「全站按钮等高」那条） */
.zoom-tools__step {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0;
  border: 0; border-radius: 50%;
  background: transparent; color: var(--c-text-2);
  font: inherit; font-size: var(--fs-base); line-height: 1; cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.zoom-tools__step:hover { background: var(--c-primary-100); color: var(--c-primary-600); }
/* 按下只比 hover 深一档（原先直接落 `--c-primary` 实心 + 白字，在 26px 的小圆上像被涂了一笔，
   用户报「太重」）。图标控件要的是「按到了」，不是「按出一个黑块」。 */
.zoom-tools__step:active { background: var(--c-primary-200); color: var(--c-primary-700); }
.zoom-tools__step:focus-visible { outline: 2px solid var(--c-primary); outline-offset: 1px; }
.zoom-tools__value {
  min-width: 34px; text-align: center;
  font-size: var(--fs-xs); font-variant-numeric: tabular-nums;
  color: var(--c-text-2);
}
/* 读数下面那两颗具名预设。列宽**不写**：这一列由 `.media-tools` 的 stretch 定宽（最宽的「下载」
   = 57px），两颗「四字 + 左右各 --sp-1」实测 56px，正好不超过它 —— 于是 `--poster-need-w`
   里那一项 57 仍然成立。这里若再加宽（更大字号 / 更多内距），那一列会被撑过 57，
   图片井就比公式算的窄，100% 重新变得能左右拖（verifier 有一条专门钉这件事）。 */
.zoom-tools__presets {
  display: flex; flex-direction: column; align-items: center;
  gap: var(--sp-2);   /* 原先 1px：两颗挨成一行字，看不出是两颗（2026-09-27 用户要求拉开） */
  /* 与上面百分比读数的距离 = 本行 margin-top + 列的 `gap`(--sp-1 4px)。这里 4px→12px
     把整组往下挪 8px（实测读数底→预设盒顶 8px→16px）：2026-09-27 用户「整体往下移一点点」。
     读数是**当前值**、这两颗是**动作**，原来 8px 挤在一起读起来像同一段字。
     ⚠ 用 margin 不用 gap：列的 `gap` 是这一列每一条之间的公共距离（± 按钮、拖动条、读数
     都吃它），改它会整列一起散开；只有这一组想单独立出来。 */
  margin-top: var(--sp-3);
}
.zoom-tools__preset {
  /* 纵向内距 1px→5px：接触面 18px→**26px**，与上面两颗放大镜同一档（2026-09-27 用户要求
     「上下接触面扩大一点」）。上下各多出 4px，整列里就数这两颗最好点不中。
     ⚠ 只动纵向：**横向内距一涨，两颗就撑过 57px 的列宽**，
     `--poster-need-w` 的分解式随之失效（verifier 有「预设按钮不撑宽工具列」这一条）。 */
  padding: 5px var(--sp-1); border: 0; border-radius: var(--r-xs);
  background: transparent; color: var(--c-text-muted);
  font: inherit; font-size: var(--fs-xs); line-height: 16px; white-space: nowrap;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.zoom-tools__preset:hover { background: var(--c-primary-soft); color: var(--c-primary-600); }
.zoom-tools__preset:active { background: var(--c-primary-100); color: var(--c-primary-700); }
.zoom-tools__preset:focus-visible { outline: 2px solid var(--c-primary); outline-offset: 1px; }
/* 当前就停在这一档：字色提到主色并加一档底色，让人一眼看出「现在不是手动档」 */
.zoom-tools__preset.is-on { background: var(--c-primary-soft); color: var(--c-primary); }
/* 右栏（生成规则）：整页已不滚动（见 .result-panel 收高），所以不再需要 sticky。
   **不写 max-height**：卡片的高由栅格那一行给（与左卡等高、底边对齐），
   这里只负责「内容比卡高时在卡内滚」。 */
.result-rules {
  min-width: 0;
  overflow: auto;
  /* 右栏两块（「本次运行」四行与「生成规则」）共用**一条标签轨**：两块各自是 grid，
     只有写死同一个数，四行抬头的值与下面每条规则的描述才从同一个 x 起头。
     60px 按最宽的标签量：「PDF文件名」在 12px 下 ≈57px；规则标签最长四个汉字
     （真源 data/text_library.json::business_rules.labels）在 12px 下 48px。
     ⚠ 从前这里是 `.rules-item` 自己写 `4em`（跟着字号走）。现在两块字号不同档，
     em 会各算各的，所以改成一处一个像素数——**动字号必须回来重量这一格**。 */
  --rules-label-w: 60px;
}
/* ---- 抬头四行（2026-09-27 用户先要求「放在生成规则上面」，同日夜里再要求删掉「本次运行」这个
   小标题）---- 与规则块同一套两列骨架（标签次要色 + 值），所以这里只写「这一块」自己的边界：
   下面那条 faint 横线是它与「生成规则」的分家处，不另加底色——整张卡片本来就是白的。
   横线**下面不再自己挤间距**：那块距离归「生成规则」标题的 `margin-top`（与「免责声明」同一条
   规则），两块标题到各自正文的距离因此必然相等。 */
.run-meta {
  padding-bottom: var(--sp-4);
  border-bottom: 1px solid var(--c-border-faint);
}
.run-meta__list { margin: 0; display: grid; gap: var(--sp-2); }
.run-meta__item {
  display: grid; grid-template-columns: var(--rules-label-w) minmax(0, 1fr);
  gap: var(--sp-3); align-items: baseline;
}
/* 四个值**同一个字号**（用户 2026-09-27 口径：这一块的字体要一样大）。从前值是 14px、
   编号与状态各是 12px，三种大小挤在四行里看着像三份不同的东西。统一之后取 12px 而不是 14px，
   凭的是编号那一行（数字都是 2026-09-27 在 1440×900 上量出来的）：
   值列宽 142px；编号 25 字符在 12px 等宽下 165px、在 14px 下 192.4px，两档都必然折行，
   区别在断点还在不在连字符上——首段 `20260927T154936Z-` 是 17 字符，12px 下 112px（余 30px），
   14px 下 131px（只剩 11px）。本机 Cascadia Mono 一档 0.55em，14px 勉强站得住；
   换一套 0.6em 的等宽回退（Courier 那一类）就是 142.8px，超的正好是那 11px，
   断点立刻从连字符掉进时间戳中间（`20260927T154` / `936Z-…`，读起来像缺字）。
   12px 那一档同样的回退是 122.4px，还留着 20px 余量。 */
.run-meta__item dt { color: var(--c-text-muted); font-size: var(--fs-sm); }
.run-meta__item dd { margin: 0; font-size: var(--fs-sm); line-height: 18px; overflow-wrap: anywhere; }
/* 编号是一整串没有空格的字符，所以折不断是设计出来的（量法同上）：右卡内容宽 214px，
   扣掉标签轨 60px 与列间距 12px，值列只剩 **142px**，而 25 字符的编号裸字 **165px**
   （带历史页那颗药丸底是 **179px**）—— 两列怎么排都装不下，必然折行。折行点落在连字符
   （`20260927T154936Z-` / `5ab5688f`），正好是「时间戳 · 随机后缀」那道缝，读起来不碎，
   于是排版定成**让它断在缝上**：
   ① 不摘标签轨（60→56 只多出 4px，165px 一样放不下，白破坏与规则共用那条轨）；
   ② 不让编号独占一行（试过了：值要从卡片最左边起头，那一行会明显凸出值列，窄卡里像排错了）；
   ③ **不带 `.run-id` 那颗药丸底**（四种排法放大比对：灰底一折行就被切成两块，看着像两个值；
      历史页那颗留着是因为那边表格宽、不折行）；
   ④ 也不许给编号加 `white-space: nowrap`（M5 探针：文字画到 1388，值列右沿 1366，
      越界被右卡那 32px 留白吃掉、`scrollWidth` 一点不变，所以横向溢出那条判据根本看不见它）。
   字号仍钉在 `--fs-sm`：全局 `code` 是 `.92em`，配 12px 的值得到 11.04px 这种小数，
   这一屏的字号阶梯是整数 px 一档一档排出来的，不为一个元素开口子。
   ⚠ 值列宽跟着右卡走（1440 及以下就是上面那 142px 的最坏情况；1600 那档实测 178px），
   所以这里只管「断了不难看」，不追求一行；改标签轨宽度必须回来重量 142 这一格。 */
.run-meta__item code { font-size: var(--fs-sm); }
/* 「预览」那颗跟在文件名后面（2026-09-27 用户要求：PDF文件名 后面加一颗预览）。
   从前它沿用的是历史页「查看详情」那颗文字链的长相（`.cell-link`，无边框），同日夜里用户
   改成口径：「要和其他按钮一样，有边框」——所以它直接穿全站那一档 `.btn .btn--ghost`，
   边框、28px 高、圆角、悬停抬 1px 全从那里来，这里**一处都不重写**（重写就等于又造出第二种按钮）。
   它是个 `<a>`（要 `target=_blank` 交给浏览器自带的 PDF 阅读器），而 `.btn` 本来就是按 class
   写的、不挑元素，所以不需要为链接另开一条规则。
   这里只留一条**几何间距**：文件名那个 <span> 与按钮中间没有空白字符，不隔开就等于把描边贴在
   字母上；8px 是这一栏里两颗东西之间的既有节奏（`.cell-actions--inline` 同值）。 */
.run-meta__preview { margin-left: var(--sp-2); }
.rules-list { margin: 0; display: grid; gap: var(--sp-3); }
/* 字段（dt）与描述（dd）挨紧：标签列从前是写死的 92px，白让出 36px（鼠标一看就是「字段和描述
   离得老远」），收成跟着字号走的 `4em`；2026-09-27 上面多出「本次运行」那一块之后，两条轨道
   合并成 `.result-rules` 上的一个 `--rules-label-w`（理由见那里）。列间距从 --sp-2(8px)
   提到 --sp-3(12px) 补一点呼吸。
   ⚠ 前提是所有 .rules-item 共用这一条轨道宽（每个 item 各自是 grid，不是列表级 grid），
   所以描述永远从同一 x 起头；改成 max-content 会让每行各算各的，描述就参差了。 */
.rules-item {
  display: grid; grid-template-columns: var(--rules-label-w) minmax(0, 1fr);
  gap: var(--sp-3); align-items: baseline;
}
/* 整块**小一号**（2026-09-27 用户要求）：标签 14→12、描述 15→14。要省的理由是这一屏的高度
   钉死了（右卡与左卡等高、内容在卡内滚），上面多出四行抬头就得从这里省回来。
   ⚠ 别把这句读反：抬头那四行是**更低**的一档（12px，理由见上面 `.run-meta__item dd` 那段），
   这里省的是规则块自己从前那一号，不是「比抬头还小一号」。 */
.rules-item dt { color: var(--c-text-muted); font-size: var(--fs-sm); }
/* 描述是**多行**的（最长一条八行），14px 字配 21px 行高偏散，跟着字号一起收到 20px */
.rules-item dd { margin: 0; font-size: var(--fs-base); line-height: 20px; overflow-wrap: anywhere; }
/* 两块小节标题（「生成规则」与「免责声明」）**共用这一条**（2026-09-27 用户要求两处一样大，
   并把「生成规则」底下那条下横线去掉）：14px + 600，无边框。从前「生成规则」另有一条
   `.result-head__title`（15px ＋金色下横线），横线按用户要求去掉后它与本条已无差别，于是整条删掉
   —— 一处定义，两块标题不会各自漂移。 */
.rules-subtitle {
  margin: var(--sp-4) 0 var(--sp-2);
  font-size: var(--fs-base); font-weight: 600; line-height: 20px;
}
/* 「免责声明」的正文行：跟在标题之后，比规则描述低一档（--fs-sm 12px）并压成次要色，
   层级靠字号和颜色分，不另加边框。内容后端给（doc.disclaimer.lines），这里只管排版。 */
.disclaimer-list {
  margin: 0; padding-left: var(--sp-5);
  display: grid; gap: var(--sp-2);
}
.disclaimer-list li {
  color: var(--c-text-muted); font-size: var(--fs-sm);
  line-height: 18px; overflow-wrap: anywhere;
}
/* ===========================================================================
   历史记录 / 空态 / 徽标
   =========================================================================== */
.empty {
  text-align: center; padding: var(--sp-8) var(--sp-5);
  background: var(--c-surface); border: 1.5px dashed var(--c-border-strong);
  border-radius: var(--r-lg);
}
.empty p { margin-bottom: var(--sp-3); color: var(--c-text-muted); }

/* 历史查询行：独立于表格之外，吸在页面头之下 */
.history-query {
  position: sticky; top: var(--page-head-h); z-index: 35;
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  min-height: var(--history-query-h);
  padding: var(--sp-2) 0;
  background: var(--c-page);
  /* 与下方表格拉开距离（点6） */
  margin-bottom: var(--sp-6);
}
.history-query__field {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  color: var(--c-text-2); white-space: nowrap;
}
/* 查询行内的输入框：宽度在此定；字号与高度统一见下方「历史页控件统一」段 */
.history-query__field .input {
  width: 150px;
  padding: 3px 8px;
}
/* 创建日期区间：起 / 至 / 止 */
.history-query__range { display: inline-flex; align-items: center; gap: var(--sp-1); }
.history-query__sep { color: var(--c-text-muted); }
.input--date { width: 132px !important; }
.input--select { width: 150px; }
/* 行最右的翻页 + 每页行数整组 */
.history-query__pager {
  margin-left: auto;
  display: inline-flex; align-items: center; gap: var(--sp-2);
  color: var(--c-text-2); white-space: nowrap;
}
.history-query__pager .input--xs { padding: 0 6px; }
/* 查询 / 刷新按钮、行最右分页组、分页条：字号统一见下方「历史页控件统一」段 */

/* 历史表格：单钮三态排序表头（吸顶）+ 单元格内动作 */
.table-wrap { overflow: visible; }
.htable { width: 100%; border-collapse: collapse; }
.htable th, .htable td {
  /* 行距：上下各 --sp-3(12px)；相邻两行内边距相加 = 24px 视觉行距（原先 8px，偏挤） */
  padding: var(--sp-3) var(--sp-3); text-align: left; vertical-align: middle;
  border-bottom: 1px solid var(--c-border-soft); font-size: var(--fs-base);
}
.htable thead th {
  position: sticky; top: calc(var(--page-head-h) + var(--history-query-h)); z-index: 20;
  background: var(--c-surface-2); color: var(--c-text-2); font-weight: 600;
  white-space: nowrap; border-bottom: 1px solid var(--c-border-strong);
}
.sortbtn {
  display: inline-flex; align-items: center; justify-content: center; gap: 2px;
  min-width: 20px; height: 20px; padding: 0 3px; margin-left: var(--sp-1);
  border: 1px solid var(--c-border-strong); border-radius: var(--r-xs);
  background: var(--c-surface); color: var(--c-text-muted);
  font-size: var(--fs-xs); line-height: 1; cursor: pointer; vertical-align: middle;
  transition: color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}
.sortbtn:hover { color: var(--c-primary-600); border-color: var(--c-primary); background: var(--c-primary-50); }
.sortbtn:active { color: var(--c-on-primary); border-color: var(--c-primary-700); background: var(--c-primary-700); }
.sortbtn.is-on { color: var(--c-primary); border-color: var(--c-primary-300); background: var(--c-primary-soft); }
/* 已选中的排序列被 hover 时也要动：`.sortbtn.is-on` 与 `.sortbtn:hover` 特异性相同、写在后面 */
.sortbtn.is-on:hover { border-color: var(--c-primary); background: var(--c-primary-100); }
.sortbtn.is-on:active { color: var(--c-on-primary); border-color: var(--c-primary-700); background: var(--c-primary-700); }
.sortbtn__order { font-size: 10px; font-weight: 700; color: inherit; }
.htable__none { text-align: center; color: var(--c-text-muted); padding: var(--sp-5); }
/* 生成结果列：「查看详情」链接 + 一颗「下载 ▾」（png/jpg/html 三选一，与结果页同一颗控件） */
.cell-actions--inline { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: nowrap; }
/* 这一列整体是「文字动作」，不是「按钮组」：同行的「查看详情」无边框，那颗「下载」
   就得跟着无边框，否则一格两种重量；横向内边距也收到 --sp-2，与文字链的节奏对齐
   （默认 --sp-4 会让两颗文字之间空出一大截）。
   ⚠ 用 `border-color: transparent` 而不是 `border: 0`：实测两种写法**高度都是 28px**
   （`box-sizing: border-box` + 显式 `height`，边框盒本就在那 28px 里，删掉不会矮），
   但 `border: 0` 会把内容盒撑大 2px、按钮窄 2px（65 → 63），与同一列那颗文字链对不齐。
   代价是 hover / 按下必须各覆盖一次 transparent——`.btn:hover:not(:disabled)` 改的正是
   `border-color` 且特异性更高，只写在基础规则里会被它压过，描边在鼠标上来时闪一下。 */
.cell-actions--inline .btn,
.cell-actions--inline .btn:hover:not(:disabled),
.cell-actions--inline .btn:active:not(:disabled) { border-color: transparent; }
.cell-actions--inline .btn { padding-left: var(--sp-2); padding-right: var(--sp-2); }
.cell-link {
  display: inline-flex; align-items: center;
  color: var(--c-primary); font-size: var(--fs-base); text-decoration: none;
  white-space: nowrap; cursor: pointer;
  padding: 2px 6px; border-radius: var(--r-xs);
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.cell-link:hover { color: var(--c-primary-600); background: var(--c-primary-50); }
.cell-link:active { color: var(--c-primary-700); background: var(--c-primary-100); }

.pager {
  display: flex; align-items: center; justify-content: flex-end; gap: var(--sp-2); flex-wrap: wrap;
  margin-top: var(--sp-3); padding-top: var(--sp-3);
  border-top: 1px solid var(--c-border-soft);
  /* 整行字号统一为正文号（点3：原先按钮/文字/页码字号不一） */
  font-size: var(--fs-base);
}
/* 页码信息两侧留白拉开一点（点3：「第 1 页 / 共 16 页」与「共 319 条」间隔） */
.pager__info, .pager__meta {
  font-variant-numeric: tabular-nums; white-space: nowrap;
  font-size: var(--fs-base);
}
.pager__info { margin: 0 var(--sp-2); }
.pager__meta { margin-right: var(--sp-2); }
.pager__size, .pager__jump {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  font-size: var(--fs-base); color: var(--c-text-2); white-space: nowrap;
}
/* 每页行数下拉：「100」是三位数，52px 会被右缘的箭头挤到显示不全，放宽到 68px；
   跳转页码框仍保持窄（只输页码） */
.pager__size .input--xs { width: 68px; max-width: 68px; }
.pager__jump .input--xs { width: 52px; max-width: 52px; }
.input--xs { width: 64px; max-width: 64px; padding: 3px 6px; font-size: var(--fs-base); }

/* ---------------------------------------------------------------------------
   历史页控件统一（表格上方的查询行 / 表格下方的分页行）
   * 高度：按钮由统一的 `.btn` 给（真源 theme.css 的 --btn-h = 28px，全站只有这一档）；
     这里的 input / select 吃**同一个数**——同一行里框和钮必须一条基线，
     原先同行并存 26 / 28 / 29 / 30 / 36.4px 五种，就是这一条没钉住。
   * 字号：整行 --fs-base(14px)；原先查询行 12px、分页行 14px 两种。
   * 分页那一串钮只把**横向**内边距收到 --sp-3(12px)：六颗短钮加页码框挤在一行，
     用默认的 --sp-4(16px) 在窄屏会顶到换行。纵向仍由 .btn 归零。
   --------------------------------------------------------------------------- */
.history-query,
.pager { font-size: var(--fs-base); }
.history-query input,
.history-query select,
.pager input,
.pager select {
  height: var(--btn-h);
  min-height: var(--btn-h);
  font-size: var(--fs-base);
  line-height: 1.4;
  padding-top: 0;
  padding-bottom: 0;
}
.history-query__pager .btn,
.pager .btn { padding-left: var(--sp-3); padding-right: var(--sp-3); }
/* 查询行的标签（创建日期 / 运行编号 / PDF文件名 / 状态）与行最右整组也跟着统一字号 */
.history-query__field,
.history-query__pager,
.pager__size,
.pager__jump { font-size: var(--fs-base); }

.badge {
  display: inline-block; padding: 1px 8px; border-radius: var(--r-pill);
  font-size: var(--fs-xs); font-weight: 600; white-space: nowrap;
  border: 1px solid transparent; letter-spacing: .01em;
}
.badge--danger { background: var(--c-danger-soft); color: var(--c-danger); border-color: var(--c-danger-line); }
.badge--warn { background: var(--c-warn-soft); color: var(--c-warn); border-color: var(--c-warn-line); }
.badge--info { background: var(--c-primary-soft); color: var(--c-info); border-color: var(--c-info-line); }
.badge--muted { background: var(--c-surface-2); color: var(--c-text-muted); border-color: var(--c-border-faint); }

/* ===========================================================================
   Toast
   =========================================================================== */
.toast-host {
  position: fixed; right: var(--sp-5); bottom: var(--sp-5); display: grid; gap: var(--sp-2);
  z-index: 80; max-width: min(380px, calc(100vw - 36px));
}
.toast {
  position: relative; background: var(--c-surface); border: 1px solid var(--c-border-soft);
  border-left: 3px solid var(--c-info); border-radius: var(--r-md);
  padding: var(--sp-3) var(--sp-5);
  box-shadow: var(--sh-lg-md); animation: toast-in var(--dur-base) var(--ease-out);
}
.toast--success { border-left-color: var(--c-ok); }
.toast--warn { border-left-color: var(--c-warn); }
.toast--danger { border-left-color: var(--c-danger); }
@keyframes toast-in { from { transform: translateY(8px); opacity: 0; } to { transform: none; opacity: 1; } }

/* ===========================================================================
   登录闸门（index.html 的 #auth-gate + app.js 的 gateEntry）
   =========================================================================== */
/* 盖住整屏的一层实底。两件事是这一屏存在的全部理由：
     1. 「先闪一下空页面再跳」比「转圈」更像系统坏了，所以这一层必须**不透明**，
        底色的算法与 body 那条同源（同一对 token，不另调一支色值）；
     2. 它是**第一帧**：写死在 index.html 里、不靠 JS 点亮，JS 还没跑完时它就该在屏上。
   z-index 落在下载菜单(60)与 toast(80)之间：闸门盖住整台应用，但toast 那一句
   （「后端未连接」这类）仍要读得到——它说的正是这一屏卡住的原因。
   按钮不在这里另立一档：出口那颗吃全站唯一的 `.btn`，高度只从 --btn-h 来。 */
.auth-gate {
  position: fixed; inset: 0; z-index: 70;
  display: flex; align-items: center; justify-content: center;
  padding: var(--sp-6);
  background: linear-gradient(180deg, var(--c-page) 0%, var(--c-page-2) 100%);
}
.auth-gate__card {
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-3);
  max-width: min(460px, 100%); text-align: center;
}
/* 呼吸点：与「执行过程」那一枚同一个周期 token（同一句「还在动＝还在办这件事」）。
   结算／失败那一刻由 app.js 直接 `hidden` 掉——停在那儿不动的点是「卡住」，不是「办完」。 */
.auth-gate__pulse {
  flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%;
  background: var(--c-gold);
  animation: trace-pulse var(--dur-blink) var(--ease-inout) infinite;
}
.auth-gate__text { font-size: var(--fs-base); color: var(--c-text-muted); }
/* 后端原话那一行：左对齐（整句是给人读的一段话，居中折行会切成两截读不顺），
   外观直接吃全站那一条 `.banner banner--danger`，这里只收它在本屏里的外边距。 */
.auth-gate .banner { width: 100%; margin-bottom: 0; text-align: left; }
.auth-gate__note { font-size: var(--fs-sm); color: var(--c-text-muted); }
.auth-gate__card .btn { margin-top: var(--sp-2); }

/* ===========================================================================
   响应式 / 动效偏好
   =========================================================================== */
@media (max-width: 900px) {
  /* 侧栏横铺到顶部成一条深色带，主区回到浅色页面底。
     三处必须一起改，缺一不可：
       1. 深蓝底默认画在 .app::before（只有侧栏宽、通高）；横铺后若留它，
          要么只黑左边一条，要么整页被染黑——直接撤掉，改由 .sidebar 自己带底；
       2. grid 默认 align-content: stretch 会把 auto 行拉满 100vh，
          侧栏条会被撑出大片空白——改为 start，行高=内容高；
       3. 横铺下没有「可折叠的宽度」，折叠按钮与其隐藏标签的状态一并失效。 */
  .app, .app.is-sidebar-collapsed { grid-template-columns: minmax(0, 1fr); align-content: start; }
  .app::before, .app.is-sidebar-collapsed::before { content: none; }
  .sidebar {
    position: static; height: auto;
    background: linear-gradient(180deg, var(--c-navy-900) 0%, var(--c-navy-950) 100%);
    box-shadow: inset 0 -1px 0 var(--c-gold-on-navy);
  }
  .sidebar__menu { flex-direction: row; flex-wrap: wrap; }
  .navgroup .sidebar__menu { margin-left: 0; padding-left: 0; border-left: 0; }
  .navitem { width: auto; }
  .sidebar__toggle { display: none; }
  .sidebar__resizer { display: none; }
  .app.is-sidebar-collapsed .navitem__text { display: block; }
  .app.is-sidebar-collapsed .navgroup__title { display: flex; }
  /* 横铺态没有「图标模式」：撤销折叠态的品牌行居中 / 菜单强制显示 */
  .app.is-sidebar-collapsed .sidebar__brand { justify-content: flex-start; padding-bottom: var(--sp-4); }
  .app.is-sidebar-collapsed .sidebar__title { display: block; }
  .app.is-sidebar-collapsed .navgroup.is-closed .sidebar__menu { display: none; }
  /* 窄屏：表格横向滚动优先，取消查询行 / 表头吸顶；结果面板也不再按视口收高 */
  .history-query { position: static; }
  .htable thead th { position: static; }
  .table-wrap { overflow-x: auto; }
  .app.is-sidebar-collapsed .navitem { justify-content: flex-start; padding: var(--sp-2) var(--sp-3); }
  /* 单栏：先图 + 按钮，再「生成规则」。行高必须松开回 `auto`——
     桌面那条 `minmax(0, 1fr)` 是按「只有一行」钉的，窄屏变成两行后若留着，
     两栏会被各分一半面板高，图片与规则都够不着自己的内容高。 */
  .result-grid { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto; }
  .result-media { order: 1; }
  /* 单栏时右卡只有整屏宽：32px 的左右留白在 420px 屏上会吃掉近 1/6 栏宽，
     所以窄屏退一档到 24px（仍比改动前的 20px 宽）。 */
  .result-rules { order: 2; padding: var(--sp-5) var(--sp-6); }
  .result-panel { height: auto; }
  .result-panel .result-media .poster-host { max-height: none; }
  /* 单栏没有「按视口收高」这回事：井的 max-height 一摘，app.js 那条「适合栏高」
     就读不到封顶了（`max-height: none`），于是它自己退成「适合栏宽」——图片按栏宽铺满、
     整页纵向滚。所以这里**不需要**再对图片本身做任何覆盖（早先那条
     `.poster-image { max-height: none }` 是给上一代「图片按高封顶」的模型兜的，已随之删掉）。 */
  .result-rules { overflow: visible; }
  /* 单栏：图片在上、那一列控件在下（改成纵向排）。留着横排的话，缩放那一组会去和
     两颗按钮**抢同一行的宽**：2026-09-27 把缩放条搬进 `.media-tools` 之后，880 视口下
     那一列被撑到 353px 宽，图片井只剩 439px、图片跟着从 ~700 掉到 417 —— 控件挪个位置
     却把图缩小了，这是反的。 */
  .result-media { flex-direction: column; align-items: stretch; gap: var(--sp-3); }
  /* 井 `flex: 0 0 auto` 交回自然高（这里已不按视口收高）。
     `scroll-margin-top` 服务于「实际大小 → 回到纸的开头」：单栏井内根本不滚（上面刚摘掉
     `max-height`，实测 880×700 `scrollHeight === clientHeight`），app.js 那一路于是把井整页
     滚回视口（`scrollIntoView`）；不预留吸顶头那 53px，图片顶边就正好被 `.page-head` 盖住，
     「回到开头」变成「回到看不见的开头」。 */
  .result-media .poster-host {
    flex: 0 0 auto; scroll-margin-top: calc(var(--page-head-h) + var(--sp-4));
  }
  /* 那一列在窄屏横排之后，缩放那一组跟着横排、拖动条也躺回水平方向
     （竖条夹在一横排按钮中间会凭空多出一截高度）。 */
  .result-media .zoom-tools { flex-direction: row; align-items: center; margin-top: 0; }
  /* 窄屏没有那一列的位置了：缩放条退回横排。`direction` 必须一起复位 ——
     宽屏那条 rtl 是为竖条服务的（见 .zoom-tools__range），留着它会让横条变成「左端=最大」。
     ⚠ 这里**不许**再写 `cursor: ew-resize`：用户点名「鼠标停在条上仍然是箭头」，那条要求认的是
     这个控件，不是某个朝向；旧版只改了宽屏那一条，窄屏一悬停照样变双箭头。 */
  .zoom-tools__range { writing-mode: horizontal-tb; direction: ltr; width: 104px; height: 20px; cursor: default; }
  /* 自绘轨道/拇指的几何也要跟着躺平：宽屏那一对是「竖着的 3×116 线 + 居中拇指」，
     照搬到横条上就变成一条 3px 高的线配一个偏上半格的拇指。
     ⚠ 躺平＝**粗细从 width 换到 height、居中偏移从 margin-left 换到 margin-top**，
     两者都继续吃 .zoom-tools__range 上那三枚变量，所以 hover 那一条不用抄第二遍
     （抄一遍就有一份会腐烂；旧版正是把物理属性写进了 hover，横条才加粗无效）。
     金线的方向同时翻成 `to right`（横排时最小值在左端）。 */
  .zoom-tools__range::-webkit-slider-runnable-track {
    width: 104px; height: var(--zoom-track-t); margin: auto 0;
    background: linear-gradient(to right,
      var(--zoom-track-on) 0 var(--zoom-fill, 50%),
      var(--zoom-track-off) var(--zoom-fill, 50%) 100%);
  }
  .zoom-tools__range::-webkit-slider-thumb {
    margin-left: 0; margin-top: var(--zoom-thumb-off);
  }
  .zoom-tools__range::-moz-range-track { width: 104px; height: var(--zoom-track-t); }
  .zoom-tools__range::-moz-range-progress { width: auto; height: var(--zoom-track-t); }
  /* 窄屏那一档「适合栏高」根本不存在（井的封顶高已摘，app.js 会如实退成适合栏宽），
     所以这颗按钮藏掉，不留一个骗人的名字在那儿。 */
  #zoom-fith { display: none; }
  .zoom-tools__presets { flex-direction: row; align-items: center; gap: var(--sp-1); margin-top: 0; }
  /* 单栏后结果是好几屏高（2026-09-27 实测 900×700 时整页 1892px，随该次 run 的规则条数浮动），
     面板已不再按视口收高，
     所以这里必须把「结果页禁滚」松开：否则图片下半截与整个「生成规则」栏都够不着。
     与上面三条「解锁高度」是同一个判断，漏了这条等于白解锁。 */
  body.no-page-scroll { overflow: auto; }
  /* 窄屏按钮横排更省纵向空间（竖排两颗会把图片挤得更小）。
     这里的 `align-items: center` **不是**冗余：桌面那一列靠 `stretch` 把两颗拉成等宽，
     横排后若不覆盖回来，两颗会被各自拉成整列高。 */
  .result-media .media-tools { flex-direction: row; flex-wrap: wrap; align-items: center; justify-content: flex-start; }
  .shell { padding: var(--sp-4) var(--sp-4) 0; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}
