/* Undusk Deliver（未夜交付）管理后台
 *
 * 只有深色一套。后台是长时间盯着看的工作界面，
 * 而且不需要迎合访客口味——省下配色切换的复杂度。
 *
 * 设计红线（wave2 统一决策 · 14 号令牌方案，改样式前先读）：
 * 1. 单主题：只有深色，不加浅色、不加主题切换；
 *    color-scheme: dark 与 forced-colors 分支维持现状。
 * 2. 层次三件套 = 边框 + 阴影 + hover 面色，不开更亮的表面色
 *    （论证见下方 --shadow 注释）。
 * 3. accent 双槽制：--accent 只碰非文本（焦点环、竖标、趋势条、主按钮底），
 *    --accent-text 才能碰文字；--accent-contrast 仅用于 accent 底上的文字，
 *    --accent-hover 仅用于主按钮 hover 底。四槽 + dim 封口，不再加派生。
 *    accent 不作大面积铺底（主按钮是唯一例外，一屏至多一个 btn-primary），
 *    不用于状态语义（成功/警告/危险各有其色）。
 * 4. 色相不漂移：新值一律由现有色板派生（同色相调明度/透明度），不引新色相。
 * 5. :root 之外禁止色值字面量（hex/rgb/rgba/hsl）；
 *    唯一豁免：forced-colors 分支里的系统关键字。
 * 6. 字号只取 --fs-* 九级、结构性间距只取 --sp-* 十级、圆角三档；
 *    1–3px 几何微调与逐处注明的豁免项保留字面量。
 *    觉得阶梯都不合适时先改 14 号文档，不写新的字面值。
 */

:root {
  /* 后台只有暗色一套，声明出来原生滚动条与表单控件才会跟着变深，
     不然一个深色后台里会插进几条白色滚动条 */
  color-scheme: dark;

  /* Organic 暗色。与 Undusk 产品族表面色板同源 */
  --bg: #211b13;
  --bg-alt: #2a241a;
  --surface: #2a241a;
  --surface-hover: #37301f;

  /* 与 Undusk 色板同一套分级：装饰性分隔线用 --border，
     输入框、按钮这类「边框即控件边界」的用 --border-strong（对背景 ≥3:1） */
  --border: #463d28;
  --border-strong: #a99d87;

  --text: #f1e8d6;
  --text-muted: #c9bfae;
  /* 原为旧主题的蓝灰色，在 --surface 上只有 3.61:1，而它铺在统计标签、
     表头、空态这些 12px 小字上，全部达不到 AA */
  --text-dim: #a99d87;
  /* 强调色分两槽：terracotta 只调到 3:1，
     够画图形不够当正文。后台的链接与强调数字走 --accent-text。
     焦点环口径（与上面对比度论证同级维护）：环色统一 --accent，
     对 --bg ≈7:1、对 --surface ≈6:1，满足非文本对比 ≥3:1；
     唯一例外是与环同色的 .btn-primary 底——环画在那儿等于消失，
     该处环色单独换 --text（见 .btn-primary:focus-visible）。 */
  --accent: #e09659;
  --accent-text: #ffc89d;
  /* 主按钮 hover 底。仅 .btn-primary:hover 一处使用 */
  --accent-hover: #f0ad75;
  /* accent 底上的文字（btn-primary、skip-link）。取与 --bg 同值的深棕
     （两处同步维护），在 --accent 与 --accent-hover 底上均 ≥4.5:1；
     替换掉换肤前遗留的旧蓝前景色 */
  --accent-contrast: #211b13;
  --accent-dim: rgba(224, 150, 89, 0.14);
  /* 第二强调色（橄榄 #a3b284）已随色板预留，需要图表多序列时再取回 */
  --success: #dce9c2;
  --warn: #ffdfc4;
  --danger: #ff6961;

  /* 状态色派生：每个状态两档，淡底（dim）与描边（border），
     与 --accent-dim（0.14）同一节奏。color-mix 派生，改主色时自动跟 */
  --success-dim:    color-mix(in srgb, var(--success) 15%, transparent);
  --success-border: color-mix(in srgb, var(--success) 40%, transparent);
  --warn-dim:       color-mix(in srgb, var(--warn) 15%, transparent);
  --warn-border:    color-mix(in srgb, var(--warn) 40%, transparent);
  --danger-dim:     color-mix(in srgb, var(--danger) 15%, transparent);
  --danger-border:  color-mix(in srgb, var(--danger) 45%, transparent);

  /* 全屏遮罩：modal ::backdrop 与移动端抽屉遮罩共用一档 */
  --overlay: rgba(0, 0, 0, 0.6);

  /* 二维码底：扫码器需要高对比的浅底，暗色主题下唯一一块白面（仅 .totp-qr） */
  --qr-bg: #fff;

  /* 层次靠阴影与边框，不再另开一档更亮的面：
   * 这套暗色里把面提亮到肉眼能分辨时，铺在上面的 12px --text-dim
   * （统计标签、表头）就会跌破 4.5:1。层次不值这个代价。
   *
   * 三档表达的是「离地高度」不是重要性，静息/hover 只许相邻档跳变：
   *   sm = 静息贴地卡片（card、stat、banner）
   *   md = 介于两者之间的独立焦点面（login-card：全屏唯一悬浮面、
   *        skip-link 焦点滑入态）；hover 抬升类无用例
   *        （.stat:hover 已按 03 号方案移除，裁决 5）
   *   lg = 遮罩之上的模态层（modal、移动端抽屉） */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.30);
  --shadow-md: 0 3px 10px rgba(0, 0, 0, 0.45);
  --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.60);

  /* 圆角三档：--radius 给独立浮在背景上的容器（card、modal、stat、error-state），
     --radius-sm 给容器内部的控件与嵌块（btn、field、banner、th、key-list、warn-box…），
     --radius-full 给胶囊（pill、role-badge）。不加第四档 */
  --radius: 10px;
  --radius-sm: 6px;
  --radius-full: 999px;

  /* 字号九级。新样式只准从阶梯取值 */
  --fs-micro: 11px;    /* 徽章、pill、辅注 */
  --fs-small: 12px;    /* 标签、表头、次要说明（--text-dim 的 AA 论证基于此档） */
  --fs-body-sm: 13px;  /* 后台默认密度字号：表格、按钮、表单、说明段 */
  --fs-body: 14px;     /* body 基准、导航 */
  --fs-title-sm: 15px; /* 卡片标题 */
  --fs-title: 17px;    /* 弹窗标题、错误页标题 */
  --fs-headline: 20px; /* 窄屏统计数值降档 */
  --fs-page: 22px;     /* 页面 h1（含登录页 h1，裁决 6：同级标题一个字号） */
  --fs-display: 24px;  /* 统计大数字（配 tabular-nums） */

  /* 间距十级：4px 基准 + 低区紧凑半档（6/10/14——wave1 14 §6.1 点名的
     三个半档；14 当年漏进令牌表，R01 补齐）。节奏约定——
     复合内边距一律「纵 < 横」，纵横都取刻度值；
     组件内元素间距 ≤12、组件之间 16、区块之间 20–24、页面级留白 32；
     1–3px 的几何微调（竖标定位、timeline 连线、gap: 1/2px）
     是造型不是布局，不属于间距节奏，保留字面量 */
  --sp-4: 4px;   --sp-6: 6px;   --sp-8: 8px;
  --sp-10: 10px; --sp-12: 12px; --sp-14: 14px;
  --sp-16: 16px; --sp-20: 20px; --sp-24: 24px;
  --sp-32: 32px;

  --dur: 0.16s;
  --ease: cubic-bezier(0.32, 0.72, 0, 1);
  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue",
    "PingFang SC", "Microsoft YaHei", sans-serif;
  --font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--fs-body);
  /* 行高不令牌化（全文件仅 4 处，各有场景理由）。此处：正文基准行高 */
  line-height: 1.6;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

/* 键盘可达性：这里原先只给 .field / .btn / .checkbox-grid 写了焦点环，
 * 侧栏那 8 个导航链接是裸 <a>，Tab 过去完全看不出焦点落在哪。 */
a:focus-visible,
[tabindex]:focus-visible,
summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* 后台原本没有这条：一旦加了动效，前庭敏感的人就没有退路。
 * 骨架屏的流光和弹窗的入场都必须能关掉。
 * 口径（16 P3-11）：全局一刀切是有意的——新增动效无需单独处理，
 * 自动被本条覆盖；不改成逐条豁免（收益低、漏网风险高）。
 * 若某个动效本身承载信息（如进度），再单独豁免。 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

.hidden { display: none !important; }
.muted { color: var(--text-muted); }
.small { font-size: var(--fs-small); }
.mono { font-family: var(--font-mono); }

/* 只给读屏的文字。骨架屏是纯视觉的，得有句话告诉听的人「在加载」 */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* 跳到主内容。可访问性入口——
 * 而后台用户每切一个页面都要先 Tab 过整条侧栏。 */
.skip-link {
  position: absolute;
  left: 12px;
  top: -60px;
  z-index: 100;
  padding: var(--sp-10) var(--sp-16);
  background: var(--accent);
  color: var(--accent-contrast);
  font-size: var(--fs-body-sm);
  font-weight: 600;
  border-radius: var(--radius-sm);
  text-decoration: none;
  transition: top var(--dur) var(--ease);
}

/* 深背景上滑入时补一档投影浮起（02 A5）；md 档用例登记见 :root 阴影注释 */
.skip-link:focus { top: 12px; box-shadow: var(--shadow-md); }

/* ── 登录 ─────────────────────────────── */

/* dvh 而不是 vh：移动端地址栏收起时 100vh 会比可视区高一截，
   登录卡片被顶得需要滚动才能看全。
   safe-area 与 .mobile-bar / .sidebar 同款 max() 写法：登录态下 shell 整个
   display:none，那三处覆盖不到这里；键盘弹起卡片滚到底时按钮别压进 home 条 */
.login-screen {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: max(var(--sp-24), env(safe-area-inset-top))
    max(var(--sp-24), env(safe-area-inset-right))
    max(var(--sp-24), env(safe-area-inset-bottom))
    max(var(--sp-24), env(safe-area-inset-left));
}

.login-card {
  width: 100%;
  max-width: 360px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--sp-32);
  /* --surface 与 --bg 明度极近，1px 边框撑不起「悬浮」；
     全站唯一无投影的悬浮面，补 md 档（sm 给贴地卡片、lg 给模态层） */
  box-shadow: var(--shadow-md);
}

.login-card h1 {
  /* 与内页 h1.page 同级同字号（裁决 6，原 20px 上调） */
  font-size: var(--fs-page);
  letter-spacing: 0.04em;
  margin: 0 0 var(--sp-8);
}

/* 登录页的未夜标：独立一行压在标题上方，作为整张卡片的锚 */
.login-logo {
  display: block;
  width: 44px;
  height: 44px;
  margin-bottom: var(--sp-16);
  color: var(--text);
}

/* 分组间距（01 §2.5）：品牌块内 8px → 品牌块与表单 24px →
   字段间 16px → 提交区 20px，三段节奏取就近的 --sp 阶梯值 */
#login-form .muted { margin: 0; }
#login-form .field-label:first-of-type { margin-top: var(--sp-24); }

/* 全宽后与两个 .field 左右对齐，「登录 / 登录中…」文案切换不再抖宽；
   上距略大于字段间 16px，给「填写区 → 提交区」一次明确收束 */
#login-form .btn-primary {
  display: block;
  width: 100%;
  margin-top: var(--sp-20);
  padding: var(--sp-10) var(--sp-12);
  font-size: var(--fs-body);
}

/* 320px 视口下 24+32 双层内边距只剩约 206px 净宽，
   长错误文案要折 4 行；窄屏卡片内边距降一档 */
@media (max-width: 380px) {
  .login-card { padding: var(--sp-24) var(--sp-20); }
}

/* 外边距反转（13 §2.4）：基类不再自带 16px 顶距——反转前多数使用点
   （弹窗表单的 labeled()、渠道页）都在用内联 margin-top:0 推翻它。
   字段间纵距改由容器负责：弹窗内走 .labeled-field 兄弟间距，
   登录卡单独补回（见下两条） */
.field-label {
  display: block;
  margin: 0 0 var(--sp-6);
  font-size: var(--fs-body-sm);
  color: var(--text-muted);
}

/* 登录卡补回字段间 16px 顶距；首字段 24px 的分组节奏由
   #login-form .field-label:first-of-type（特异性更高）继续接管 */
.login-card .field-label { margin-top: var(--sp-16); }

/* 弹窗表单相邻字段的纵距（13 §2.4）：反转前是 0——上一格输入框贴着下一格
   label，新建账号四个字段挤成一团；12px 对齐「组件内 ≤12」的节奏。
   限定 .modal：页面上的 labeled() 全部住在 form-grid 网格里，
   间距由 gap 管，sibling margin 不得混进网格错行 */
.modal .labeled-field + .labeled-field { margin-top: var(--sp-12); }

.field {
  width: 100%;
  /* 控件高度对齐（13 §2.1）：与 .btn 同字号同纵距，
     工具栏里输入框、下拉、按钮三类控件上下沿齐平（icon-btn 差 1px 不可辨） */
  padding: var(--sp-8) var(--sp-12);
  font: inherit;
  font-size: var(--fs-body-sm);
  color: var(--text);
  background: var(--bg-alt);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
}

.field:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}

/* 禁用/只读与 .btn:disabled 同一条立场：不用 opacity 整体压暗，
 * 显式给一组降级配色。强边框退为装饰边框 =「这不是可交互边界」，
 * 底色不变（输入框底本来就是 --bg-alt），靠边框与文字降级区分。
 * 两个选择器都要写，才能覆盖将来只挂其一的用法。 */
.field:disabled,
.field[readonly] {
  cursor: not-allowed;
  border-color: var(--border);
  color: var(--text-dim);
}

/* 校验出错的视觉钩子（裁决 1）：JS 在出错字段置 aria-invalid="true"，
 * input 时移除。错误文案不进字段旁，只走弹窗内错误行或页面 banner。 */
.field[aria-invalid="true"] { border-color: var(--danger); }

/* placeholder 统一 --text-dim（13 §2.3。原先零规则，全靠 color-scheme: dark
   下的 UA 默认，各浏览器不一、多为半透明当前色）。对比度豁免理由：
   placeholder 不承担必读信息——可访问名已走 label / aria-label，
   不按正文 4.5:1 要求 */
.field::placeholder { color: var(--text-dim); }

/* 全站自动填充收编：.field 底全站唯一 --bg-alt，垫回的永远是字段自己的底。
   UA 的自动填充底色盖不掉（内部样式优先），用大值内阴影垫回输入框
   自己的底；文字色只有 -webkit-text-fill-color 能赢过 UA 接管。
   R05 C1 解除 01 §2.1 的登录页限定，登录与弹窗密码框同吃本规则；
   焦点环画在 border 外，与内阴影不同层互不遮盖 */
.field:-webkit-autofill,
.field:-webkit-autofill:hover,
.field:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 1000px var(--bg-alt) inset;
  -webkit-text-fill-color: var(--text);
  caret-color: var(--text);
}

textarea.field { min-height: 72px; resize: vertical; }

.field-error {
  /* 13px × 1.6 ≈ 21px：预留高度对齐真实行高，单行报错出现时容器零位移。
     行高派生的几何值不属间距阶梯，保留字面量（原 18px 差 3px 仍会撑高） */
  min-height: 21px;
  margin: var(--sp-8) 0 0;
  font-size: var(--fs-body-sm);
  color: var(--danger);
}

/* 服务端透传的英文长 token / URL 无断点会横向顶破 360px 卡片；
   中文本就逐字可断，对既有文案零影响。挂 id 不挂 .field-error：
   别让将来复用通用类的人背上这条登录页决定 */
#login-error { overflow-wrap: anywhere; }

/* 非错误消息（「已退出」「密码已更新」）不该渲染成危险红：
   用户刚改完密码看到红字，第一反应是「改失败了？」。JS 按语气挂 .notice */
#login-error.notice { color: var(--text-muted); }

/* 常驻占位、visibility 切换（JS 挂 .on 点亮）：display 切换会插拔一行高度，
   CapsLock 每次开关「登录」按钮都上下跳。min-height 19px = 12px × 1.6
   的一行行高，几何派生值保留字面量 */
.caps-hint {
  min-height: 19px;
  margin: var(--sp-6) 0 0;
  color: var(--warn);
  font-size: var(--fs-small);
  visibility: hidden;
}

.caps-hint.on { visibility: visible; }

/* 13×13 行内提示图标（wave2 §四：viewBox 24、笔画 1.8、随 currentColor，
   与 .icon-btn svg 同一套笔画语言）；尺寸与基线微调是造型值，保留字面量 */
.caps-hint svg {
  width: 13px;
  height: 13px;
  margin-right: var(--sp-4);
  vertical-align: -2px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ── 骨架 ─────────────────────────────── */

.shell {
  display: grid;
  grid-template-columns: 220px 1fr;
  min-height: 100dvh;
}

.mobile-bar,
.sidebar-backdrop,
.sidebar-close { display: none; }

.icon-btn {
  width: 38px;
  height: 38px;
  display: inline-grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
}

.icon-btn svg {
  width: 19px;
  height: 19px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
}

/* 按钮族里唯一没有 hover 态的成员，补齐（13 §1.5）；
   transition 不补——汉堡 / 关闭钮低频到不值一次过渡 */
.icon-btn:hover { background: var(--surface-hover); }
.icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.sidebar-close.icon-btn { display: none; }

.sidebar {
  display: flex;
  flex-direction: column;
  background: var(--bg-alt);
  border-right: 1px solid var(--border);
  padding: var(--sp-20) var(--sp-14);
  /* 长页面往下滚时导航钉在原位（02 B1）：侧栏从此恒为一屏高，.nav 的内滚
     从「矮窗口兜底」升格为常态机制。align-self: start 必写——grid 默认
     stretch 会把 sticky 元素拉满行高，sticky 失效；height: 100dvh 与之配套，
     两行缺一不可。≤820px 抽屉态被媒体查询内 position: fixed 整套覆盖，不受影响 */
  position: sticky;
  top: 0;
  height: 100dvh;
  align-self: start;
}

.brand {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  font-weight: 600;
  padding: 0 var(--sp-8) var(--sp-20);
}

.brand .sidebar-close { margin-left: auto; }

/* 未夜标：品牌资产 undusk-icon-white-small 的几何，描边随 currentColor，
   与品牌名同色（品牌规范：Logo 是锚，不加发光、不加动效）。
   22px / 44px 是图案造型尺寸，非排版字号，豁免于阶梯 */
.brand-logo {
  width: 22px;
  height: 22px;
  flex: none;
}

.brand-name { letter-spacing: 0.04em; }

.nav {
  display: grid;
  gap: 2px;
  flex: 1;
  /* 不加这句的话，grid 会把 flex 撑开的高度平分给每一行，
     导航项之间拉出一大片空隙 */
  align-content: start;
  /* 矮窗口（高约 <560px）时导航区内滚，品牌与「改密码 / 退出」恒定可见。
     min-height: 0 抵消 flex 子项默认的 min-height: auto——不写它这个
     flex 子项会拒绝收缩，overflow 永远不触发，foot 照样被挤出视口 */
  min-height: 0;
  overflow-y: auto;
}

.nav a {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--sp-10);
  padding: var(--sp-10) var(--sp-12);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  text-decoration: none;
  font-size: var(--fs-body);
  transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}

/* 导航分组（10-19 改）：入口涨到 20 个后只靠间距已分不清哪段是店铺、
   哪段是平台，组首加一行小标题。组内项间距沿用 .nav 的 2px */
.nav-group {
  display: grid;
  gap: 2px;
}

/* 整组入口都被门控隐藏（商家看「平台」组）时连标题一起收起，不留空标题。
   用 :has() 纯样式完成，enterShell 的单遍门控结构不动 */
.nav-group:not(:has(a:not(.hidden))) { display: none; }

.nav-group + .nav-group { margin-top: var(--sp-14); }

.nav-label {
  margin: 0;
  padding: var(--sp-4) var(--sp-12);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: 0.08em;
  color: var(--text-dim);
}

/* 导航图标（wave2 §四）：不单独定义图标色，随 currentColor 继承文字
   状态色——默认 muted、hover 提亮、当前项 --accent-text，forced-colors
   下自动适配，无需兜底。16px 是图标造型尺寸，非排版字号，豁免于阶梯 */
.nav a svg {
  width: 16px;
  height: 16px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* .nav 成为滚动容器后，画在盒外的焦点环（2px 环 + 2px offset）会被
   左右两侧裁掉。内收画在链接盒内，任何滚动位置都完整可见 */
.nav a:focus-visible { outline-offset: -2px; }

.nav a:hover { background: var(--surface-hover); color: var(--text); }

.nav a.active {
  background: var(--accent-dim);
  /* 文字槽违例修复：--accent 只够画图形不够当正文（见 :root 注释），
     当前项文字走 --accent-text；竖标与淡底仍是图形，继续用 --accent 系 */
  color: var(--accent-text);
  font-weight: 500;
}

/* 当前位置多给一条竖标。八个入口只靠一块淡橙底，
 * 眼睛扫下来还得停一下才认得出自己在哪 */
.nav a.active::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  width: 3px;
  height: 15px;
  margin-top: -7px;
  /* 竖标造型，几何派生值，豁免保留字面量 */
  border-radius: 0 2px 2px 0;
  background: var(--accent);
}

/* 两个按钮并排。竖着摞在侧栏底部会占掉一大块，
 * 而它们都是低频操作，不值这个面积 */
.sidebar-foot {
  border-top: 1px solid var(--border);
  padding-top: var(--sp-14);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-6);
}

.sidebar-foot .who { grid-column: 1 / -1; }

.who {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-8);
  margin: 0 0 var(--sp-6);
  /* 0 4px 是与 .brand 的 4px 级光学对齐（wave1 §6.2 点名的精调值），
     原判不动，等值 var 化也不做，豁免保留字面量 */
  padding: 0 4px;
  font-size: var(--fs-body-sm);
}

/* 长用户名截断成省略号，不许把角色徽章挤扁或撑破侧栏；
   完整名字挂在 title 上（app.js 的 enterShell 设置） */
#who-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.role-badge {
  /* 截断的是用户名，徽章永不收缩变形 */
  flex: none;
  font-size: var(--fs-micro);
  /* 胶囊上下 padding 被行高吃掉，1px 7px 是视觉配平结果，豁免保留 */
  padding: 1px 7px;
  border-radius: var(--radius-full);
  background: var(--surface-hover);
  color: var(--text-muted);
}

.content {
  /* 底部 64px 是滚动余量（2×--sp-32），超出刻度上限，保留字面量 */
  padding: var(--sp-24) var(--sp-32) 64px;
  max-width: 1320px;
  min-width: 0;
  width: 100%;
}

/* skip-link 跳过来时焦点环画在内侧，免得那圈焦点环糊到侧栏上 */
.content:focus-visible { outline-offset: -2px; }

.banner {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-12);
  margin-bottom: var(--sp-16);
  padding: var(--sp-12) var(--sp-16);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  border-left-width: 3px;
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  font-size: var(--fs-body-sm);
}

.banner-text { flex: 1; }

/* 自动消失只是便利，不能是唯一出路——尤其失败提示压根不会自动收 */
.banner-close {
  flex: none;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  /* 「×」的字形尺寸，是图形不是排版字号，豁免于 --fs 阶梯；
     2px 4px 是字形周边的图形微调，同样豁免于间距阶梯 */
  font-size: 16px;
  line-height: 1;
  padding: 2px 4px;
  cursor: pointer;
  opacity: 0.7;
  border-radius: var(--radius-sm);
}

.banner-close:hover { opacity: 1; }
.banner-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.banner.ok { border-color: var(--success-border); border-left-color: var(--success); color: var(--success); }
.banner.warn { border-color: var(--warn-border); border-left-color: var(--warn); color: var(--warn); }
.banner.bad { border-color: var(--danger-border); border-left-color: var(--danger); color: var(--danger); }
/* 中性横幅（无 kind，如「没有改动」）：左边条维持 --border 色。事实上的
   第四种档位——既非成功也非失败的陈述，转正为口径，不另配色（12 号 P3） */

/* ── 通用 ─────────────────────────────── */

h1.page { font-size: var(--fs-page); margin: 0 0 var(--sp-4); }
p.page-desc { margin: 0 0 var(--sp-20); color: var(--text-muted); font-size: var(--fs-body-sm); }

.btn {
  font: inherit;
  font-size: var(--fs-body-sm);
  /* 与 .field 同横距，工具栏里并排时对得齐 */
  padding: var(--sp-8) var(--sp-12);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease),
    color var(--dur) var(--ease);
}

/* 锚点形按钮（批次台账入口，lane-06 R03）：<a> 走 .btn 视觉但保留链接语义
 * （可中键新开、可复制地址），去下划线、补 inline-block 让 padding 生效 */
a.btn { display: inline-block; text-decoration: none; }

/* hover 一律 :not(:disabled)：在途禁用的「生成中…」按钮悬停时
 * 背景不许亮回可点的样子。不靠「disabled 规则写在后面」的源码顺序——
 * 那种正确性下一个往文件尾加规则的人就会破坏，意图写进选择器本身。 */
.btn:hover:not(:disabled) { background: var(--surface-hover); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* 禁用态不用 opacity 整体压暗：0.5 会把文字拉到 2.9:1，
   连「为什么这个按钮点不了」都看不清。显式给一组降级配色。 */
.btn:disabled {
  cursor: not-allowed;
  background: var(--bg-alt);
  border-color: var(--border);
  color: var(--text-dim);
}

/* 按钮层级守则（13 §1.4）：
   ① 一屏至多一个 btn-primary——留给区块级唯一主操作（保存封顶、确认签发
     这类往前走的动作），「关闭 / 取消」是中性动作不配 primary；
   ② btn-danger 只给「点下去立即产生破坏性效果」的按钮，同一行不得两个红钮；
   ③ 行内（表格内）按钮一律 btn-sm、默认无 primary——「哪行需要保存」的
     可见性由 dirty 态（.field.dirty + 「未保存」pill）承担，不靠满屏橙钮。 */
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-contrast); font-weight: 500; }
.btn-primary:hover:not(:disabled) { background: var(--accent-hover); border-color: var(--accent-hover); }

/* 焦点环兜底色是 --accent，画在同色的主按钮底上时环与钮融为一体，
   只剩 offset 露出的 1px 背景缝。换 --text：环主要落在按钮外的页面底上
   （对 --bg ≈14:1），与按钮底也拉得开。口径见 :root 的焦点环注释 */
.btn-primary:focus-visible { outline-color: var(--text); }
.btn-ghost { background: transparent; }
.btn-danger { border-color: var(--danger-border); color: var(--danger); }
.btn-danger:hover:not(:disabled) { background: var(--danger-dim); }
.btn-sm { padding: var(--sp-6) var(--sp-10); font-size: var(--fs-small); }

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: var(--sp-20);
}

.card + .card { margin-top: var(--sp-16); }

/* 左右分栏。三列的趋势表独占整行时右边会空一大片，
   跟窄卡片并排后两边都填满。窄屏自动堆叠。 */
.split {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: var(--sp-16);
  align-items: start;
}

/* 栅格自己管间距，否则相邻卡片会多出一段 margin */
.split > .card + .card { margin-top: 0; }

/* 900 与 820 分立是有意的（16 P3-9）：1.6:1 分栏在 900px 时窄列已
   不足 320px，堆叠临界比侧栏塌缩更早到。别顺手并进 820 档 */
@media (max-width: 900px) {
  .split { grid-template-columns: minmax(0, 1fr); }
}

.card-title {
  font-size: var(--fs-title-sm);
  font-weight: 600;
  margin: 0 0 var(--sp-4);
}

.card-note {
  margin: 0 0 var(--sp-14);
  font-size: var(--fs-small);
  color: var(--text-muted);
  /* 小字说明段，行高略收不至松散 */
  line-height: 1.5;
}

.stat-grid {
  display: grid;
  /* 显式列数而不是 auto-fit：内容区 600–800px 宽时 auto-fit(190px) 会排成
     3+1，第四张卡独占整行拉得极宽。断点归属（裁决 8）——列定义分三档：
     这里 4 列、≤1080px 两列、≤420px 改回 auto-fit(130px) 兜 320px；
     三档作用域互斥，互不覆盖 */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-12);
  margin-bottom: var(--sp-16);
}

/* 四列每列已窄于 190px，收成工整的 2×2 */
@media (max-width: 1080px) {
  .stat-grid { grid-template-columns: 1fr 1fr; }
}

/* 卡片不可点击，不给 hover 反馈与 transition——按钮式反馈是误导（03 B1） */
.stat {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: var(--sp-16);
  /* 与列定义的 minmax(0, 1fr) 双保险：解决同一个 grid 最小尺寸问题的两层 */
  min-width: 0;
}

.stat .label { font-size: var(--fs-small); color: var(--text-dim); }

/* 等宽数字：四张卡的数值要能上下对齐着扫，比例字宽会让它们参差不齐 */
.stat .value {
  font-size: var(--fs-display);
  font-weight: 600;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  /* 多币种收入是「CN¥1,234.56 + US$78.90」这类长串：在卡内换行，
     不许撑破布局。行高不令牌化，此处：大数字折行后的紧凑行距 */
  overflow-wrap: anywhere;
  line-height: 1.3;
}

.stat .sub {
  font-size: var(--fs-small);
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

/* 订单表 8 列、表头又不许折行，窄屏必然把卡片撑破。
 * 包一层能横滚的容器；配套 tabindex + role="region" 让它也能用键盘滚——
 * 只有鼠标能滚的区域，对键盘用户等于不存在。 */
.table-wrap {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.table-scroll-hint {
  display: flex;
  align-items: center;
  gap: var(--sp-6);
  margin: var(--sp-10) 0 0;
  color: var(--text-muted);
  font-size: var(--fs-small);
}

.table-scroll-hint::before {
  content: "↔";
  color: var(--accent-text);
  font-family: var(--font-mono);
}

.table-wrap:focus-visible,
.permission-matrix-wrap:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* 表格。数据密集，行高压紧一点 */
table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-body-sm);
}

th {
  text-align: left;
  font-weight: 500;
  color: var(--text-dim);
  font-size: var(--fs-small);
  padding: var(--sp-8) var(--sp-10);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
  /* 只靠一条下边框的话，深色下表头和第一行数据几乎糊在一起 */
  background: var(--bg-alt);
  letter-spacing: 0.02em;
}

th:first-child { border-top-left-radius: var(--radius-sm); }
th:last-child { border-top-right-radius: var(--radius-sm); }

td {
  padding: var(--sp-10);
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}

/* 最后一行的下边框在卡片里会变成一条悬空的线 */
tbody tr:last-child td { border-bottom: none; }

/* 行 hover 铺面色是宽表的视线追踪辅助线（13 §3.2），不是可点击暗示——
   行本身不可点（可点的是行内按钮与订单号），别「顺手优化」成点击态 */
tbody tr:hover { background: var(--surface-hover); }

/* 键盘用户在几十行里 Tab 到某个操作按钮时，只有按钮本身有焦点环，
 * 「我现在在哪一行」得靠数。给整行也标一下。 */
tbody tr:focus-within { background: var(--surface-hover); box-shadow: inset 2px 0 0 var(--accent); }

/* 数字列表头随数据右对齐（由 table() 按列元数据输出 th.num），
   表头与数字不再错位 */
th.num { text-align: right; }
/* 单元格约定（13 §3.2）：空值一律「—」；时间戳挂 .small.muted；
   标识符（订单号 / 前缀 / 指纹 / 批次）挂 .mono；状态列只放 pill；
   操作列恒为末列、右对齐、按钮一律 btn-sm */
td.num { text-align: right; font-variant-numeric: tabular-nums; }
td.actions { text-align: right; white-space: nowrap; }
/* 商品名可长可短：给个下限宽度，窄屏宁可整表横滚也不逐字竖排；
   渠道是两到四字的短词，不许折行 */
td.cell-product { min-width: 9em; }
td.cell-nowrap { white-space: nowrap; }

/* 行内相邻控件间距（13 §1.6）：DOM API 建的节点间没有文本空格，
   「设备|吊销」「未保存|保存|下架」原本贴死。.pill + .btn 比 13 原文多列：
   07 落地的「未保存」pill 就住在价目行操作列里。操作列右对齐，
   pill 隐藏时首个按钮左侧多出的边距落在空白侧，不可见 */
td.actions .btn + .btn,
td.actions .pill + .btn { margin-left: var(--sp-6); }

/* 相邻 pill 间距（10 A3，替代账号页 JS 行内 margin-left:6px）：
   现阶段唯一用例「已锁定」等 locked 字段透出才上屏 */
.pill-gap { margin-left: var(--sp-6); }

/* 趋势条。让三列表格右边的空白变成一眼能读的走势；
   准确数值在同一行的「收入」列里，这里不承担精确表达。 */
.bar-cell { width: 38%; padding-right: var(--sp-14); }

.bar {
  height: 6px;
  /* 3px = 高度 6px 的一半，几何派生值，豁免保留字面量 */
  border-radius: 3px;
  /* 渐变起点 45% 而不是 --accent-dim（14%）：收入很低的短条整体
     落在渐变最淡的一段，深色底上近乎隐形，与 min-width「零收入也留
     痕迹」的意图相悖。color-mix 派生，改主色时自动跟 */
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--accent) 45%, transparent), var(--accent));
  /* 零收入那天也留一点痕迹，否则整行看起来像渲染坏了 */
  min-width: 2px;
}

.pill {
  display: inline-flex;
  align-items: center;
  /* 图标与文字距原 5px 归并到 4 档（-1px，与 .caps-hint 图标距同档） */
  gap: var(--sp-4);
  font-size: var(--fs-micro);
  /* 胶囊上下 padding 被行高吃掉，2px 8px 是视觉配平结果，豁免保留 */
  padding: 2px 8px;
  border-radius: var(--radius-full);
  background: var(--surface-hover);
  color: var(--text-muted);
  white-space: nowrap;
}

.pill-icon { font-size: var(--fs-micro); line-height: 1; }

.pill.ok { background: var(--success-dim); color: var(--success); }
.pill.warn { background: var(--warn-dim); color: var(--warn); }
.pill.bad { background: var(--danger-dim); color: var(--danger); }

/* 单元格可点：订单号（<button> 开详情）与卡密批次（<a> 即筛）共用的
   一套视觉语言。正文链接色走 --accent-text（--accent 对深底只有 3:1，
   不许碰文字）；下划线常驻，给色弱用户留一条颜色之外的「可点」通道。
   font: inherit 让 <button> 吃掉 UA 默认字体，字体字号随所在单元格
   （订单号列的 mono、批次列的 small）。3px 下划线间距是几何微调，豁免 */
.cell-link {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--accent-text);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

.cell-link:hover { text-decoration-thickness: 2px; }

/* 焦点环兜底只覆盖裸 <a> / [tabindex]，按钮形态在此补齐同款 */
button.cell-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* 设备满员（如 2/2）文字换警示色——「设备满了激活不上」是卡密页最常见
   的工单原因，2/2 和 1/2 不该长得一模一样。只加颜色语义，数字与格式
   不变；forced-colors 下前景色被系统接管，「N / N」原文本仍在，信息不丢 */
td.device-full { color: var(--warn); }

/* 临期到期时间（7 天内，阈值在 app.js 的 EXPIRY_WARN_DAYS）：与满员标色
   同一套口径——只加颜色语义，日期格式不变；forced-colors 下前景色被系统
   接管，「2026/09/01 14:00」原文本仍在，信息不丢 */
td.expiry-soon { color: var(--warn); }

.toolbar {
  display: flex;
  gap: var(--sp-8);
  flex-wrap: wrap;
  align-items: center;
  margin-bottom: var(--sp-14);
}

.toolbar .field { width: auto; min-width: 200px; }
select.field { min-width: 130px; }

/* 日期区间组（审计页）：date input 不支持 placeholder，
 * 用「从 … 至 …」文字连接两个框，起止一眼可辨 */
.date-range { display: flex; align-items: center; gap: var(--sp-6); }
.date-range .field { min-width: 0; }

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  /* 横向原 18px 归并到 16 档（-2px，14 号孪生值归并同一手法） */
  gap: var(--sp-14) var(--sp-16);
}

.checkbox-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: var(--sp-8);
  margin-top: var(--sp-8);
}

.checkbox-grid label {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  padding: var(--sp-6) var(--sp-10);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: var(--fs-body-sm);
}

.checkbox-grid label:hover { background: var(--surface-hover); }
.checkbox-grid input { accent-color: var(--accent); }

/* 已勾未存（免费权益页的 dirty 标记）：accent 只碰非文本，描边合规 */
.checkbox-grid label.dirty { border-color: var(--accent); }

/* 勾选框本身够小，键盘用户需要看到焦点落在整个选项上 */
.checkbox-grid label:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.error-state {
  max-width: 520px;
  /* 垂直留白 64px = 2×--sp-32，超出刻度上限，
     与 .content 底部 64px 同一豁免口径；内边距原 28px 归并到 24 档（-4px）。
     h2/p 排版由 state-block 系接管（R05 退休了旧两行） */
  margin: 64px auto;
  padding: var(--sp-24);
  text-align: center;
  background: var(--surface);
  border: 1px solid var(--danger-border);
  border-radius: var(--radius);
}

.health-alerts {
  display: grid;
  gap: var(--sp-8);
  margin-bottom: var(--sp-16);
}

.health-alert {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-10);
  padding: var(--sp-12) var(--sp-16);
  border: 1px solid var(--border);
  /* 兜底色是 warn 不是 success：未知 kind 的「告警」至少呈现为
     「需要注意」，绿色兜底会把告警渲染成一切正常（03 H4） */
  border-left: 3px solid var(--warn);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.health-alert.warn { border-left-color: var(--warn); }
.health-alert.bad { border-left-color: var(--danger); }
.health-alert strong { display: block; font-size: var(--fs-body-sm); }
.health-alert > div { min-width: 0; }

/* 严重度图标（×/!）：颜色与左侧色条同源，字号对齐标题行——
   原先被正文选择器连坐成 12px 灰字，bad 与 warn 的区分只剩 3px 边框。
   仍用字符不升级 SVG（wave2 §四.5 的克制边界） */
.health-alert-icon {
  font-size: var(--fs-body-sm);
  font-weight: 600;
}
.health-alert.warn .health-alert-icon { color: var(--warn); }
.health-alert.bad .health-alert-icon { color: var(--danger); }

/* 收窄为「正文块里的 span」，不再误伤图标 span */
.health-alert > div > span {
  color: var(--text-muted);
  font-size: var(--fs-small);
  overflow-wrap: anywhere;
}

.readiness-list { display: grid; gap: var(--sp-6); margin: var(--sp-14) 0; }
.readiness-row {
  display: grid;
  grid-template-columns: minmax(110px, 1fr) auto;
  gap: var(--sp-12);
  align-items: center;
  padding: var(--sp-8) 0;
  border-bottom: 1px solid var(--border);
}
.readiness-row:last-child { border-bottom: 0; }

.timeline {
  position: relative;
  display: grid;
  gap: 0;
  margin: var(--sp-16) 0 var(--sp-4);
}

.timeline-item {
  position: relative;
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr);
  gap: var(--sp-10);
  padding-bottom: var(--sp-14);
}

.timeline-item::before {
  content: "";
  position: absolute;
  left: 5px;
  top: 12px;
  bottom: -2px;
  width: 1px;
  background: var(--border-strong);
}
.timeline-item:last-child::before { display: none; }
.timeline-dot {
  width: 11px;
  height: 11px;
  margin-top: 4px;
  border: 2px solid var(--surface);
  border-radius: 50%;
  background: var(--text-dim);
  box-shadow: 0 0 0 1px var(--border-strong);
}
.timeline-item.ok .timeline-dot { background: var(--success); }
.timeline-item.bad .timeline-dot { background: var(--danger); }
.timeline-title { font-size: var(--fs-body-sm); }
.timeline-meta { color: var(--text-muted); font-size: var(--fs-small); }

.impact-summary,
.permission-summary,
.price-preview {
  margin-top: var(--sp-12);
  padding: var(--sp-12) var(--sp-16);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-alt);
  color: var(--text-muted);
  font-size: var(--fs-small);
}

/* 确认弹窗影响清单里的「取消免费」条目：收窄是免费权益页头 warn-box 明说
   要谨慎的方向，弹窗里给同一信号。色值走令牌，不新增字面量。 */
.impact-summary .impact-removal { color: var(--warn); }

/* 确认摘要的「（无变化）」行与留空说明行（07 r05 方案 7/8）：容器本身已是
 * --text-muted，光挂 .muted 在这里原地踏步——降一档到 --text-dim 才有灰阶差
 * （对 --bg-alt ≈5.8:1，小字 AA 达标）。只此一处、只用现有令牌，
 * 方案原判「CSS 零新增」的前提失效经落地说明登记（r06-code/07.md）。 */
.impact-summary .muted { color: var(--text-dim); }

/* 编辑账号摘要的高危态（10 A1）：降权 / 停用 / 重置密码任一命中即由灰转红，
   否则「踢掉全部会话」和「权限不变」长得一模一样。配方与 .banner.bad 同款，
   只用现有状态色令牌 */
.permission-summary.danger {
  border-color: var(--danger-border);
  border-left: 3px solid var(--danger);
  color: var(--danger);
}

.price-preview strong { color: var(--text); }
.price-preview .up { color: var(--warn); }
.price-preview .down { color: var(--accent-text); }

/* 已改未存的金额格（07 P1-1）：accent 只碰非文本，描边合规。
   套餐金额、模块单价矩阵、组合封顶三处共用同一条规则。 */
.field.dirty { border-color: var(--accent); }

/* 金额窄框（yuanInput 出口）：110px 是控件造型量，不属间距节奏，
   保留字面量（r01 12-16 §2.6，替代原 JS 行内 width:110px） */
.field-amount { width: 110px; }

/* 权限矩阵（10 A2）：div grid 换真表格找回行列语义。1px 缝隙由
   border-spacing + 表格底色实现，观感与原 gap: 1px 版一致；圆角与窄屏
   横滚由外层 wrap 承接——overflow: hidden 在 table 盒模型上裁不干净。
   th/td 逐项覆写全局表格样式（表头底色 / 下边框 / nowrap / 字距），
   回到原 grid 版的中性格子观感；首列 width: 100% 即原 1fr 的角色 */
.permission-matrix-wrap {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.permission-matrix {
  border-collapse: separate;
  border-spacing: 1px;
  background: var(--border);
  font-size: var(--fs-small);
}
.permission-matrix th,
.permission-matrix td {
  padding: var(--sp-8) var(--sp-10);
  background: var(--surface);
  border-bottom: 0;
  border-radius: 0;
  color: var(--text);
  font-weight: 400;
  white-space: normal;
  letter-spacing: normal;
}
.permission-matrix thead th { font-weight: 700; }
.permission-matrix thead th:first-child { width: 100%; }
.permission-matrix .yes { color: var(--success); }
.permission-matrix .no { color: var(--text-dim); }

/* 变更前 / 变更后并排对照：两个 detail-list 各自 auto 1fr 两列、内容不宽，
 * 560px 的弹窗放得下；上下堆叠时对照要来回滚。420px 窄屏回落单列。 */
.audit-diff { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-12); }
.audit-diff section { min-width: 0; }
.audit-diff .audit-diff-context { grid-column: 1 / -1; }
.audit-diff h3 { margin: 0 0 var(--sp-6); font-size: var(--fs-body-sm); }
.audit-diff dl { margin: 0; }
/* 差异标记：变了的行 warn 淡底 + 键名左侧 2px 描边条，没变的降为
 * --text-dim 退到背景里。透明左边框常驻，行与行的键名不因高亮错位；
 * 颜色只用现有状态色令牌，不新增颜色。 */
.audit-diff dt { border-left: 2px solid transparent; padding-left: var(--sp-6); }
.audit-diff dt.diff-changed { border-left-color: var(--warn); }
.audit-diff dt.diff-changed,
.audit-diff dd.diff-changed { background: var(--warn-dim); }
.audit-diff dt.diff-same,
.audit-diff dd.diff-same { color: var(--text-dim); }

.note-disclosure { margin-top: var(--sp-4); }
.note-disclosure summary { color: var(--accent-text); cursor: pointer; font-size: var(--fs-small); }
.note-disclosure p { max-width: 320px; margin: var(--sp-6) 0 0; overflow-wrap: anywhere; color: var(--text-muted); }

.feature-reason {
  flex-basis: 100%;
  display: block;
  margin-left: var(--sp-24);
  color: var(--text-dim);
  font-size: var(--fs-micro);
  /* 11px 辅注挤在勾选项内，行高压紧防止占两行位 */
  line-height: 1.35;
}

.checkbox-grid label.disabled { cursor: not-allowed; color: var(--text-dim); }
.checkbox-grid label.disabled:hover { background: transparent; }
.checkbox-grid label:has(.feature-reason) { flex-wrap: wrap; align-content: flex-start; }

/* 平台切换导致勾选被自动取消的提示（06 P0-2）。挂在权益区标题行下方，
 * 复用 feature-reason 的字号规格；无文案时不占位。 */
.feature-sync-hint {
  margin-top: var(--sp-4);
  color: var(--warn);
  font-size: var(--fs-micro);
  line-height: 1.35;
}

.feature-sync-hint:empty { display: none; }

/* 签发提交行的实时计数（06 P1-3）。字号走 .small，一项权益没选时标红，
 * 与提交时「至少要选一项权益」的拦截呼应。 */
.issue-count.danger { color: var(--danger); }

/* 签发页行内样式收编（r01 06 P1-3）。间距归 --sp-* 阶梯取邻值：
 * 提交行原 22px→--sp-24、下载行原 14px→--sp-16（1–2px 差异属既有豁免口径）。 */
.issue-section { margin-top: var(--sp-20); }

.issue-section .section-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.issue-section .section-head .field-label { margin: 0; }

.issue-submit-row {
  margin-top: var(--sp-24);
  display: flex;
  align-items: center;
  gap: var(--sp-12);
  flex-wrap: wrap;
}

/* 结果弹窗的下载按钮行 */
.key-actions {
  display: flex;
  gap: var(--sp-8);
  margin-top: var(--sp-16);
}

/* 加载骨架。原先只有一行「加载中…」，切视图时整个内容区会先空一下再弹出来，
 * 看着像卡了。骨架至少让人知道马上要出什么形状的东西。 */
.skeleton {
  display: grid;
  gap: var(--sp-10);
}

.skeleton-row {
  height: 15px;
  border-radius: var(--radius-sm);
  background: linear-gradient(90deg, var(--surface) 25%, var(--surface-hover) 50%, var(--surface) 75%);
  background-size: 240% 100%;
  animation: skeleton-sweep 1.3s linear infinite;
}

.skeleton-row.tall { height: 78px; border-radius: var(--radius); }
.skeleton-row.short { width: 38%; }

@keyframes skeleton-sweep {
  from { background-position: 240% 0; }
  to { background-position: -240% 0; }
}

/* ── 弹窗 ─────────────────────────────── */

/* <dialog> 自己会居中（margin: auto），遮罩交给 ::backdrop，
 * 不再需要外面套一层定位容器。
 * 但 dialog 的 UA 默认色是 Canvas/CanvasText，不显式指定的话
 * 暗色主题下会渲染出一块白底黑字。 */
.modal {
  width: calc(100% - 48px);
  max-width: 560px;
  max-height: 82dvh;
  overflow-y: auto;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: var(--sp-24);
}

/* 签发、退款、吊销都从这个窗口出去。给它一个短促的入场，
 * 让人看清它是「新弹出来的东西」而不是页面本来就长这样。 */
.modal[open] { animation: modal-in var(--dur) var(--ease); }

@keyframes modal-in {
  from { opacity: 0; transform: translateY(-6px) scale(0.985); }
}

/* 弹窗拒关反馈：ESC / 点遮罩被拦下不能毫无动静，短促晃动 + 边框转警示色
 * 说明「不是坏了，是不许关」。签发结果弹窗（06 P0-4）、在途锁窗与带输入
 * 弹窗拦遮罩（12 号方案 7、4）共用。
 * reduced-motion 下动画被全局关掉，边框高亮仍在（类由 JS 定时摘除）。 */
.modal.modal-refused {
  animation: modal-refuse calc(var(--dur) * 2) var(--ease);
  border-color: var(--warn-border);
}

@keyframes modal-refuse {
  /* 7px 是动效位移不是布局间距，不取间距阶梯 */
  25% { transform: translateX(-7px); }
  75% { transform: translateX(7px); }
}

.modal::backdrop {
  background: var(--overlay);
  animation: backdrop-in var(--dur) var(--ease);
}

@keyframes backdrop-in {
  from { opacity: 0; }
}

.modal h2 { font-size: var(--fs-title); margin: 0 0 var(--sp-14); }

/* 弹窗内小节标题（详情类）。字号字重同正文标题惯例，UA 的 h3 加粗保持。
   弹窗内小节标题只有两个合法出身：本类（详情类）与 .audit-diff h3
   （对照类），新弹窗从中选，不写行内（r01 12-16 §3.3） */
.modal-subtitle { font-size: var(--fs-body); margin: var(--sp-20) 0 var(--sp-8); }

.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-8);
  margin-top: var(--sp-20);
}

/* 签发结果。卡密明文只出现这一次，必须扎眼 */
.key-list {
  max-height: 300px;
  overflow-y: auto;
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--sp-12) var(--sp-16);
  font-family: var(--font-mono);
  font-size: var(--fs-body-sm);
  /* 卡密逐行抄读的场景，行距放宽到 1.9 便于对行 */
  line-height: 1.9;
  user-select: all;
  /* textContent 里的换行不加这句不会渲染出来：
     多张卡密会挤成一行，审计详情的 JSON 缩进也会全部塌掉 */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.warn-box {
  padding: var(--sp-12) var(--sp-16);
  border-radius: var(--radius-sm);
  border: 1px solid var(--warn-border);
  /* 原 0.08 归并到 --warn-dim（0.15，裁决 7）：警示框略实一点 */
  background: var(--warn-dim);
  color: var(--warn);
  font-size: var(--fs-body-sm);
  margin-bottom: var(--sp-14);
}

.detail-list {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-8) var(--sp-16);
  margin: 0;
  font-size: var(--fs-body-sm);
}

.detail-list dt { color: var(--text-muted); }
.detail-list dd { margin: 0; overflow-wrap: anywhere; }

/* ── Windows 高对比模式 ─────────────────── */

/* 高对比会接管所有背景色。后台里「你现在在哪个页面」全靠一块淡橙底 +
 * 一条竖标，两者的颜色都会被系统换掉，那一刻当前项就认不出来了。 */
@media (forced-colors: active) {
  .skip-link { border: 1px solid; }

  .nav a.active { outline: 2px solid Highlight; outline-offset: -2px; }
  .nav a.active::before { background: Highlight; }

  .btn-primary { border-color: currentColor; }

  /* 状态标签的底色也会没，但 .pill 里的文字本身就是状态名
     （生效中 / 已吊销 / 已发货），信息不丢，不额外补。
     .banner 的 ok/warn/bad 左边框色同理被接管，但横幅文字本身
     就描述了成败，同样判「不补」（16 P2-4） */

  /* 趋势条的渐变背景被整体清除，整列空白像渲染坏了。用边框重画：
     box-sizing: border-box 下 6px 下边框正好填满 6px 高的条，长度
     语义保住；用系统关键字而不是 forced-color-adjust: none 锁原色，
     尊重用户自选的色板 */
  .bar { border-bottom: 6px solid CanvasText; }

  /* 时间线点的三态填充（灰=未发生 / 绿=完成 / 红=失败）会被抹平成
     三个一样的空心圈。文字兜不住底——「授权履约 · 尚未完成履约」在
     失败（bad）与未发生（无 kind）两种情形下逐字相同。改用形状区分：
     完成实心、失败双线圈、未发生维持空心细圈 */
  .timeline-item.ok .timeline-dot { background: CanvasText; }
  .timeline-item.bad .timeline-dot { border: 3px double CanvasText; }

  /* 表格行的键盘焦点靠 hover 同款底色是不行的——高对比下底色被接管，
     那一刻键盘用户就找不到自己停在哪一行了 */
  tbody tr:focus-within { outline: 2px solid Highlight; outline-offset: -2px; }
}

/* 820 是骨架切换档（16 P3-9）：侧栏变抽屉、顶部出 mobile-bar、内容区
   收边距、权限矩阵允许横滚。只管「壳」的形态，分栏堆叠归 900 档管 */
@media (max-width: 820px) {
  /* 原「单列两行网格（58px 定高行）」改常规块级流（R05 16 方案 1）：
     流内子元素只有 mobile-bar 与 content 两个，单列两行网格 ≡ 块级流；
     而定高行恰等于 bar 自身高度——sticky 按规范被钳在网格区域包含块内
     零活动余地（侧栏 sticky 注释记过同族陷阱），coarse 的 44px 汉堡也
     装不进 37px 内容盒。实测校准（R06 headless Chrome 148 复现记录）：
     本内核 sticky 实现宽松、改前其实钉住了，规范钳制是其余内核的风险面；
     coarse 溢出 ±3.5px 当时被 10px padding 吸收、未及边框——两种内核
     结果下改法均成立，只解除约束、不新增行为。块级流下包含块是整个
     shell，bar 恢复自然高度，两笔一并消解；min-height: 100dvh 块级下
     继续生效，桌面 >820px 零触碰 */
  .shell { display: block; }

  .mobile-bar {
    position: sticky;
    top: 0;
    z-index: 35;
    display: flex;
    align-items: center;
    gap: var(--sp-12);
    /* 刘海机安全区：竖屏顶部（状态栏 / 挖孔，R05 16 方案 2 补齐）与
       横屏左右都至少让出；底边不接 env——bar 钉屏顶，够不着底部横条 */
    padding: max(var(--sp-10), env(safe-area-inset-top))
      max(var(--sp-14), env(safe-area-inset-right))
      var(--sp-10) max(var(--sp-14), env(safe-area-inset-left));
    background: color-mix(in srgb, var(--bg-alt) 94%, transparent);
    border-bottom: 1px solid var(--border);
    backdrop-filter: blur(14px);
  }

  .mobile-title { font-weight: 600; }

  .sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: 50;
    width: min(310px, calc(100vw - 48px));
    /* 纵向原 18px 归并到 16 档（-2px） */
    padding: var(--sp-16) var(--sp-14);
    /* 抽屉贴着屏幕左缘与上下缘，给刘海、状态栏与底部横条让位
       （顶部为 R05 16 方案 2 补齐：inset 贴物理屏顶，品牌行别压进状态栏） */
    padding-top: max(var(--sp-16), env(safe-area-inset-top));
    padding-left: max(var(--sp-14), env(safe-area-inset-left));
    padding-bottom: max(var(--sp-16), env(safe-area-inset-bottom));
    border-right: 1px solid var(--border);
    transform: translateX(-104%);
    transition: transform var(--dur) var(--ease);
    box-shadow: var(--shadow-lg);
    /* 触屏在抽屉内滚到头时不把动量链给背景（02 A3）：
       与 body.drawer-open 滚动锁互补 */
    overscroll-behavior: contain;
  }

  .sidebar.open { transform: translateX(0); }
  .sidebar-close.icon-btn { display: inline-grid; }

  .sidebar-backdrop {
    position: fixed;
    inset: 0;
    z-index: 45;
    display: block;
    border: 0;
    background: var(--overlay);
    /* 与抽屉 0.16s 滑入同步淡入（02 A4）；显示态 visibility 即时翻转，
       只让 opacity 走过渡。reduced-motion 由全局一刀切自动压平 */
    transition: opacity var(--dur) var(--ease), visibility 0s;
  }

  /* 全局 .hidden 是 display: none !important 瞬切，遮罩要过渡必须在本块内
     以更高特异性同为 !important 覆写成「可见性隐藏」：opacity 淡出，
     visibility 补 var(--dur) 延迟——淡出走完才真正不可点、出可访问树。
     JS 仍只切 .hidden 一个类；桌面不受影响（覆写只存在于本媒体查询内，
     桌面仍走基线 display: none） */
  .sidebar-backdrop.hidden {
    display: block !important;
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--dur) var(--ease), visibility 0s var(--dur);
  }

  /* 抽屉开启锁背景滚动（02 A3）：类由 openSidebar / closeSidebar 加减，
     syncSidebarMode 的桌面分支必须同步清掉——抽屉开着时旋屏 / 放大到
     桌面宽的路径不清，桌面会锁死滚动 */
  body.drawer-open { overflow: hidden; }

  .brand { padding: 0 var(--sp-4) var(--sp-10); }

  /* 抽屉里导航区沿用桌面的内滚（overflow-y: auto 不覆写）：10-19 加分组
     标题后，商家 15 项 + 3 行组名在 844px 高的手机上已超出一屏，原先的
     overflow: visible 会让导航项画到「改密码 / 退出」上面。
     原先这里还有两行 mask-image: none 的重置——桌面从未设置过 mask，
     属死代码，已删（02 D5） */
  .nav {
    display: grid;
    padding: var(--sp-4) 0;
  }

  .sidebar-foot { margin-top: var(--sp-10); }
  /* 底部 48px = 1.5×--sp-32 的窄屏滚动余量，超刻度豁免保留字面量 */
  .content {
    padding: var(--sp-20) max(var(--sp-16), env(safe-area-inset-right))
      max(48px, env(safe-area-inset-bottom)) max(var(--sp-16), env(safe-area-inset-left));
  }

  /* 窄屏矩阵：滚动挪到 wrap（10 A2），表格 min-width 取原窄屏列模板总宽
     （96 + 64×3），极窄时横滚、其余宽度照旧折行，行为不回归；
     滚动链遏制与键盘可达（R05 16 方案 3）与 .table-wrap 同口径 */
  .permission-matrix-wrap {
    overflow-x: auto;
    overscroll-behavior-x: contain;
  }
  .permission-matrix { min-width: 288px; }
}

/* ── 触屏（pointer: coarse）────────────────
 * 只按输入方式生效，不动桌面密度。两件事（16 号 P1-1 / P1-2）：
 * 1）iOS Safari 对 <16px 的输入框聚焦时会强制放大整页；
 * 2）触控目标照 WCAG 2.5.8 兜到 ≥44px 热区。 */
@media (pointer: coarse) {
  /* 16px 是 iOS 的防缩放阈值，不是排版字号，豁免于 --fs 阶梯 */
  .field { font-size: 16px; }

  .icon-btn { width: 44px; height: 44px; }

  /* 抽屉里的导航项加高到 ≥44px */
  .nav a { padding-top: var(--sp-12); padding-bottom: var(--sp-12); }

  /* 视觉尺寸不变，用伪元素把热区扩到 ≥44px；
     伪元素属于按钮本身，点在扩出的区域上照常触发 */
  .btn-sm,
  .banner-close { position: relative; }
  .btn-sm::after { content: ""; position: absolute; inset: -9px 0; }
  .banner-close::after { content: ""; position: absolute; inset: -12px -10px; }
}

/* ── 减弱透明度偏好（16 P2-6）──────────────
 * mobile-bar 的半透明毛玻璃（94% 底色 + blur）是全站唯一的透明表面，
 * 给要求实底的用户退成不透明 --bg-alt。模态遮罩 --overlay 不动：
 * 压暗遮罩是「聚焦弹窗」的信息本身，不是要退掉的透明装饰。 */
@media (prefers-reduced-transparency: reduce) {
  .mobile-bar {
    background: var(--bg-alt);
    backdrop-filter: none;
  }
}

/* 420 是手机竖屏档（16 P3-9）：常见 360–414px 宽机型全部落入，
   控件撑满整行、弹窗按钮等宽换行、表单退单列。不另设 320 断点，
   320px（旧 SE / 分屏）由本档内的弹性列定义兜底 */
@media (max-width: 420px) {
  .toolbar .field { width: 100%; min-width: 0; }
  .toolbar .btn { flex: 1; }
  /* 日期区间组整行占满，两个框平分剩余宽度，「从 … 至 …」保持一行 */
  .date-range { width: 100%; }
  .date-range .field { width: auto; flex: 1; }
  /* 变更前后对照回落上下堆叠 */
  .audit-diff { grid-template-columns: 1fr; }
  /* 窄屏弹窗内边距原 18px 归并到 16 档（-2px） */
  .modal { width: calc(100% - 24px); padding: var(--sp-16); }
  .modal-actions { flex-wrap: wrap; }
  .modal-actions .btn { flex: 1; }
  /* 320px 兜底（16 P2-7，裁决 8）：≤1080px 中档的显式两列在 320px
     视口下每列只剩约 140px，容不下 20px 等宽数字的六位数。改回
     auto-fit：容量不够时自然退单列，够时仍是两列。与 ≥421px 的
     显式列数并存——本档作用域独立，互不覆盖 */
  .stat-grid { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); }
  .stat { padding: var(--sp-14); }
  .stat .value { font-size: var(--fs-headline); }
  .form-grid { grid-template-columns: 1fr; }
}

/* ── 弹窗内错误槽（12 号方案 2）───────────────
 * 页面横幅的紧凑变体。弹窗开着时 banner() 把 bad/warn 消息投递到
 * #modal-error（index.html 里常驻的 role="alert" 节点）——背景横幅
 * 被遮罩压住且 inert，读屏连播报都收不到。文案与横幅同一来源。 */
.modal-error {
  margin-top: var(--sp-16);
  padding: var(--sp-8) var(--sp-12);
  border: 1px solid var(--border);
  border-left-width: 3px;
  border-radius: var(--radius-sm);
  background: var(--bg-alt);
  font-size: var(--fs-body-sm);
}

.modal-error.bad { border-color: var(--danger-border); border-left-color: var(--danger); color: var(--danger); }
.modal-error.warn { border-color: var(--warn-border); border-left-color: var(--warn); color: var(--warn); }

/* E4 深链动作位：#modal-error 的相邻兄弟节点（R07 裁决②：alert 守
 * text-only 纪律，链接不入 live region，Tab 在 <dialog> 焦点圈内可达）。 */
.modal-error-action { margin-top: var(--sp-8); font-size: var(--fs-body-sm); }

/* ── 状态区块（15 号）────────────────────
 * 空 / 错 / 载三种区块级状态共用一套版式：居中、统一的垂直留白、
 * 主句（state-main）/ 副句（state-sub）/ 动作（state-action）按
 * 6px / 16px 阶梯排布。红描边与宽度约束由同节点上的 .error-state
 * 容器继续提供，这里只统一节奏，不新增颜色。 */
.state-block {
  /* 40px 大留白超出刻度上限（32），豁免保留字面量——本规则是 40px 的
     唯一持有方（原 .empty 已随 R05 退休，豁免账随迁到此）；错误卡从
     28px 归并到同一档，「空」与「错」在同一个内容区里不再是两套呼吸 */
  padding: 40px var(--sp-24);
  text-align: center;
}

.state-block--empty { color: var(--text-dim); }

/* 弹窗内的紧凑档：40px 留白塞进 560px 的窗里太空旷 */
.state-block--compact { padding: var(--sp-24) var(--sp-16); }

.state-block .state-main { margin: 0; }

.state-block--error .state-main {
  font-size: var(--fs-title);
  font-weight: 600;
  color: var(--text);
}

.state-block .state-sub {
  margin: var(--sp-6) 0 0;
  font-size: var(--fs-body-sm);
  color: var(--text-muted);
}

.state-block .state-action { margin-top: var(--sp-16); }

/* ── 表格页骨架档（15 号）────────────────
 * 订单 / 卡密 / 审计 / 账号四页的真实内容是「工具栏 + 表格」，
 * 共用「一短两高」的卡片形骨架会在内容落地时明显跳动。
 * 高度对齐真实控件与行高（造型量，非间距节奏，保留字面量）：
 * control ≈ 筛选栏输入框、thead ≈ 表头行、line ≈ 数据行。 */
.skeleton-row.control { height: 42px; width: 56%; border-radius: var(--radius-sm); }
.skeleton-row.thead { height: 35px; border-radius: var(--radius-sm) var(--radius-sm) 0 0; }
.skeleton-row.line { height: 41px; }

/* ── 弹窗加载壳（15 号方案 A）────────────────
 * 取数弹窗点击即开壳：标题已知先亮出来，内容区放几行骨架占位，
 * 数据到位后由 modal() 原地替换。骨架行复用 .skeleton-row（流光动画
 * 已被全局 reduced-motion 兜底），行距对齐 .skeleton 的 10px，
 * 垂直留白对齐 state-block--compact 的 24px 紧凑档。 */
.state-block--loading {
  display: grid;
  gap: var(--sp-10);
  padding: var(--sp-24) 0;
}

/* ── 导入进度条（B-L06 P2b-2）────────────────
 * 原生 <progress>：value/max 只吃服务端 ack 的 importedCount/totalCount。
 * 轨道用装饰级 --border（非控件边界），已完成段走 accent 非文本槽；
 * 8px 是造型量（非间距节奏），保留字面量。 */
.import-progress {
  width: 100%;
  height: 8px;
  appearance: none;
  -webkit-appearance: none;
  border: none;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--border);
}
.import-progress::-webkit-progress-bar { background: var(--border); }
.import-progress::-webkit-progress-value { background: var(--accent); }
.import-progress::-moz-progress-bar { background: var(--accent); }

/* ── 页内行内样式收编（r05 07-09 §3.2）────────────
 * 三类只含布局属性，本批先落 07 价目区。.form-actions 是 07/08/09
 * 三页保存行共用的一类（08 保存行要的 flex-wrap / gap 已预置，
 * 08/09 两页挂入归 08-09 落地批）；顶距 16/18/20px 归并 --sp-16 档
 * （18→16 -2px、20→16 -4px，与 .form-grid 横向 18→16 归并同一手法，
 * 呈现漂移逐处在落地说明登记）。 */
.form-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-12);
  flex-wrap: wrap;
  margin-top: var(--sp-16);
}

/* form-grid 里的说明行（封顶兼容口径行与 07 方案 2 空态提示共用）：
 * p 清默认外边距，行距交给网格 gap */
.grid-hint { margin: 0; }

/* 主行下 4px 挂靠行：矩阵模块名下的 tags 行、封顶输入下的参照行 */
.sub-line { margin-top: var(--sp-4); }

/* 卡头行：标题与状态胶囊两端对齐、垂直居中（09 渠道卡逐张同构，一类多用；
 * app.js 渠道卡注释点名「整体收编归 09 页篇」的落点）。h2 原行内 margin:0
 * 一并接管——.card-title 自带的 4px 下距在卡头行里清零，呈现不变 */
.card-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.card-head h2 { margin: 0; }

/* 价目体检卡的警示条目：条距 8px、含末条，与原行内值等值。
 * .warn-box / .banner 自带 14/16px 下距，靠本条文件尾同特异性后置
 * 覆盖成 8px——r06-code/07.md 登记的 warningBox 口径注脚，r07 批销账 */
.warn-item { margin-bottom: var(--sp-8); }

/* 字段脚注（09 渠道卡两处同构：商户号说明 / disabledReason，全站仅此
 * 二处同款）：p 默认外边距收为仅留 6px 顶距，与原行内 margin:6px 0 0
 * 等值零漂移。r05 07-09 §3.2「无 ≥2 处同构」的前提对这一对不成立，
 * r07 08-09 批改判收编 */
.field-note { margin: var(--sp-6) 0 0; }
.field-note.field-note-bad { color: var(--danger); }

/* ── TOTP 两步验证（W1-19）──────────────────
   QR 必须垫白底：暗色主题下二维码直接贴深底会扫不出来 */
.totp-qr {
  display: inline-block;
  background: var(--qr-bg);
  padding: var(--sp-10);
  border-radius: var(--radius-sm);
  margin: var(--sp-10) 0;
  line-height: 0;
}
.totp-qr img { display: block; max-width: 100%; height: auto; image-rendering: pixelated; }

.totp-secret {
  font-family: var(--font-mono);
  font-size: var(--fs-title-sm);
  letter-spacing: 1px;
  word-break: break-all;
  user-select: all;
  padding: var(--sp-8) var(--sp-10);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

/* ── 行内样式收编（UI 巡检批）────────────────
 * app.js 里反复出现的布局型 style 属性收成类，值全部落在 --sp-* / --fs-*
 * 阶梯上（8/10/14px 原值即刻度，零漂移；唯一例外是 .section-title
 * 顶距原 18px 不在刻度上，归并 --sp-16，-2px）。 */

/* 并排的一组次要按钮：可换行、8px 间距 */
.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-8);
  margin-top: var(--sp-10);
}
.btn-row--loose { margin-top: var(--sp-14); }

/* 复选框 + 文案的行内标签：整行可点，框与字垂直居中 */
.inline-check {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  margin-top: var(--sp-10);
  cursor: pointer;
}
.inline-check input { flex: none; }

/* 卡内小节标题（审核详情等）：比卡片标题低一档 */
.section-title {
  font-size: var(--fs-body);
  font-weight: 600;
  margin: var(--sp-16) 0 var(--sp-4);
}

.text-danger { color: var(--danger); }
.stack-top,
.card.stack-top { margin-top: var(--sp-10); }

/* ── 卡网视图（assets/kawang-merchant.js / kawang-ops.js）──────────
 * 全部沿用上面的令牌与既有类，只补几处现成类表达不了的小形制。 */

/* 三张统计卡一排（店铺概况 / 结算 / 运营台）：3 列起步，窄屏降 2 列 */
.stat-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 700px) {
  .stat-grid--3 { grid-template-columns: 1fr 1fr; }
}

/* 统计数字的语义色：待办（warn）、到手（ok）。仅改文字色，不动字号 */
.stat .value.warn { color: var(--warn); }
.stat .value.ok { color: var(--success); }

/* 健康提示补两处：成功条左线 + 条内逐行列表（退款失败订单清单） */
.health-alert.ok { border-left-color: var(--success); }
.health-alert .kw-line { display: block; margin-top: var(--sp-4); }

/* 店铺链接一行：链接换行不撑破、按钮跟在后面 */
.kw-link-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-8);
}
.kw-link {
  min-width: 0;
  overflow-wrap: anywhere;
  font-size: var(--fs-body-sm);
}

/* 卡密文本框：等宽、不折行（每行一张，横向滚动），纵向可拉伸 */
.kw-keys { resize: vertical; line-height: 1.7; white-space: pre; overflow-x: auto; }

/* 表格内强调（投诉率超标）：加粗而非只靠颜色 */
.kw-strong { font-weight: 600; }

/* 导航徽标：走 data-badge + ::after，不进 textContent
   （route() 取导航文字做 mobile-title / document.title，数字不能混进去） */
.nav a[data-badge]::after {
  content: attr(data-badge);
  margin-left: auto;
  min-width: 18px;
  padding: 0 var(--sp-6);
  border-radius: var(--radius-full);
  background: var(--warn-dim);
  border: 1px solid var(--warn-border);
  color: var(--warn);
  font-size: var(--fs-micro);
  font-weight: 600;
  line-height: 16px;
  text-align: center;
}

/* 被平台下架的商品：操作排在状态下面（两列布局，手机上商品说明才不会被挤窄） */
.kw-takedown-action {
  margin-top: var(--sp-8);
}

/* 手机横滚表（QA 4 号 10-19）：首列（订单号 / 商品名）钉在左边，滚到金额、
 * 时间时仍看得见是哪一行。底色要实：透明的话滚过去的列会从下面透出来。
 * 只在窄屏生效，桌面整表放得下，没必要钉 */
@media (max-width: 820px) {
  .table-wrap th:first-child,
  .table-wrap td:first-child {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--surface);
    box-shadow: 1px 0 0 var(--border);
  }
  .table-wrap th:first-child { background: var(--bg-alt); }
  tbody tr:hover td:first-child { background: var(--surface-hover); }
}

/* 单元格里的文字按钮（订单号）字高只有 21px，触屏补热区，视觉不变 */
@media (pointer: coarse) {
  button.cell-link { position: relative; }
  button.cell-link::after { content: ""; position: absolute; inset: -12px -4px; }
}

/* 手机竖屏的宽表摊成卡片行（QA 4 号 10-20：库存表「状态」、已有商品表「合规」列裁在右侧，
 * 横滚提示也救不回来——商家根本不会往右划）。只对 stackOnPhone() 标过的表生效：
 * 商品名独占一行，其余格子在下面按行流排；数字格前写列名（data-label）。
 * 选择器带 .table-wrap 是为了压过 820 档的首列 sticky 底色 */
@media (max-width: 560px) {
  table.kw-stack thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
  table.kw-stack,
  table.kw-stack tbody { display: block; }
  table.kw-stack tr {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-6) var(--sp-12);
    padding: var(--sp-12) var(--sp-4);
    border-bottom: 1px solid var(--border);
  }
  table.kw-stack tbody tr:last-child { border-bottom: none; }
  table.kw-stack td { display: block; padding: 0; border: 0; }
  .table-wrap table.kw-stack td:first-child {
    flex: 1 0 100%;
    position: static;
    background: transparent;
    box-shadow: none;
  }
  table.kw-stack td.num { text-align: left; }
  table.kw-stack td[data-label]::before {
    content: attr(data-label) " ";
    color: var(--text-muted);
    font-family: inherit;
    font-size: var(--fs-small);
  }
  table.kw-stack td.mono[data-label]::before { font-family: system-ui, sans-serif; }
  table.kw-stack td.actions { margin-left: auto; white-space: normal; }
  table.kw-stack td:empty { display: none; }
}

/* 类目按钮（上架商品）：名字一行、「即将开放」小字副行；手机两列等宽等高，
 * 不再被 .toolbar .btn 的 flex:1 挤成一字一行（QA 4 号） */
.kw-cat-bar .btn {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  line-height: 1.35;
}
.kw-cat-soon { font-size: var(--fs-micro); color: var(--text-muted); }
@media (max-width: 420px) {
  .kw-cat-bar { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .kw-cat-bar .btn { min-height: 52px; }
}

/* 窄屏触控尺寸（QA 4 号 10-20 复测）：上面 pointer:coarse 的伪元素热区只在真触屏生效，
 * 窄屏实测按钮本体仍 33px、订单号 21px。手机宽度下直接把本体撑到 44px，不靠伪元素 */
@media (max-width: 820px) {
  .btn-sm {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .cell-link {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-top: 11px;
    padding-bottom: 11px;
  }
  /* 5 轮 QA 4 号：筛选/重置/导出/标记退款/加备注/关闭等普通按钮 39px。窄屏统一 ≥44px；
   * 弹窗动作条文字不折行（「关闭待付订单」不再拆成两行） */
  .btn {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .modal-actions .btn { white-space: nowrap; }
  /* 6 轮 QA 4 号：长详情弹窗里操作按钮行在折叠线外，需先滚动才看得到。动作条贴弹窗底 */
  .modal-actions {
    position: sticky;
    bottom: calc(-1 * var(--sp-16));
    margin-bottom: calc(-1 * var(--sp-16));
    padding: var(--sp-8) 0 var(--sp-16);
    background: var(--surface);
  }
  /* 输入框、下拉、日期框 37–41px → 44px */
  .field, input.field, select.field { min-height: 44px; }
  /* 8 轮 QA 5 号：顶栏图标按钮 38px、侧栏菜单项 42px */
  .icon-btn { width: 44px; height: 44px; }
  .nav a { min-height: 44px; }
  /* 运营台一级页签：五个页签挤一排会折成 3 行、词中断开；手机上改两列网格、单行、保底 44px（9 轮 QA 5 号 N4） */
  .toolbar[aria-label="运营队列"] { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  .toolbar[aria-label="运营队列"] .btn { min-height: 44px; white-space: nowrap; }
  .toolbar[aria-label="运营队列"] .btn:last-child:nth-child(odd) { grid-column: 1 / -1; }
}
