/* ==========================================================================
   后台样式：在 tokens / base / components 之上叠加管理界面
   ========================================================================== */

/* ================================================================ v25 批（2026-09-24）：三栏布局
   照目录站后台（`F:\赫蒙\产品目录网站\`）：
     · 56px 固定顶栏（#wpadminbar）→ .ad-top
     · 220px 固定左导航（.wp-menu）→ .ad 的第 1 列（R7·Q3：176 → 220，对齐规范样机）
     · 内容区里「有列表又有详情」的模块 = 左右分栏（.ad-split，280px|1fr）→ 我们给产品用 340px|1fr
   ★ 三栏**只对产品模块生效**（.ad--split 由 JS 加）—— 一刀切会把概览 / 站点设置挤成窄条。
   ★ 新写的样式一律用 --fs-12 / --fs-14；--fs-13 这个变量根本不存在（老代码有 8 处是这个账，不在本批）。 */

/* ---------------------------------------------------------------- 顶栏（56px 固定 · var(--ad-top-h)） */

/* ★★ 2026-10-03 SAGE 严格对齐（第三轮 · 余总原话「**左侧侧边栏应该上顶  顶部通栏放在右侧**」
   ＋ 拍板「**左侧栏顶放赫蒙logo+「赫蒙管理系统」颜色不变  右侧保持深色橄榄不变去掉LOGO**」）：
   布局从「顶栏横跨全宽压在最上、侧栏从顶栏下开始」改为「**侧栏占满整高（上顶）＋ 顶栏只在右侧内容区之上**」。

   ★ 改法：`.ad-top` 从 `fixed; left:0; right:0` 改为 **`fixed; left: var(--ad-side-w); right:0`**
     （顶栏左缘 = 侧栏宽 ⇒ 不再盖住侧栏）。
   ★ `.ad` 的 `padding-top: var(--ad-top-h)` **保留**（右栏内容仍要让开顶栏）；
     侧栏靠 `position: fixed; top:0; height:100vh` 自己上顶、盖过那 56px 的 padding。
   ⚠️ **顶栏必须继续 `fixed`（而不是改 `sticky`）**：`.ad-tabbar` / `.ad-bar` 的
     `top: calc(var(--ad-top-h) + var(--ad-tabh))` 是**按"顶栏 fixed 压在上方"算出来的**；
     顶栏一旦回到文档流，这两条 sticky 的吸附位置全错（滚动才发作、截图看不出来）。
   ⚠️ **侧栏必须继续 `fixed`（而不是 `sticky; top:0`）**：`sticky` 的包含块是 `.ad`（有
     `padding-top:56px`）⇒ 它永远上不到 `top:0`，实测会停在 56px 下方 ⇒ **改了个寂寞**。
     `fixed` 才是「相对视口、真上顶」的唯一写法。
   ⚠️ `height: 100vh` 与 `.ad-nav` 原来的 `calc(100vh - var(--ad-top-h))` 差 56px：
     那是"顶栏占掉的那段"，现在侧栏自己上顶，这 56px 归它所有 ⇒ 菜单多出一行可见空间。 */

.ad-top {
  position: fixed; top: 0; left: var(--ad-side-w); right: 0; z-index: 100;
  height: var(--ad-top-h);
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 0 var(--sp-5);
  /* ★★ 2026-10-03 SAGE 严格对齐（第二轮 · 余总标注「顶部导航条 #263819」）：
     原 `var(--ad-ink)` = #0E1405（近黑，橄榄味不足）→ 改 `var(--ad-accent)` = #263819
     （SAGE --olive）。★ 为什么这两支看着像却要换：--ad-ink 是**文字色**（要求极高对比），
     --ad-accent 是**填充色**（要求大面积压得住）。顶栏是**大面积填充**，该用后者。
     ⚠️ 连带：顶栏内文字仍走 `color: var(--ad-bg)`（#FAFBF4），压 #263819 实测 11.2:1 ✓。
     ★★ 第三轮拍板「**右侧保持深色橄榄不变**」⇒ 这条底色**一个字没动**（仍是 --ad-accent）。 */
  background: var(--ad-accent);
  color: var(--ad-bg);
  font-size: var(--fs-12);
}
/* ★★ 2026-10-03（第三轮）**这两个类已退役** —— 顶栏的 LOGO 与站点名都删了
   （品牌位整体上移到侧栏 `.ad-nav__brand`）。
   ⚠️ 规则**故意保留**：① 它们是"曾经存在"的记录（本项目铁律：删代码要留据）
     ② 万一将来回退 DOM，样式还在。判据 A6 反守「顶栏内不许再出现 `.ad-top__logo` / `.ad-top__site`」。 */
.ad-top__logo { height: 22px; width: auto; max-width: 110px; object-fit: contain; }
.ad-top__site {
  color: var(--ad-bg); font-size: var(--fs-14); font-weight: var(--fw-medium);
  text-decoration: none; white-space: nowrap;
}
.ad-top__site:hover { color: var(--ad-accent); }
.ad-top__spacer { flex: 1; }
.ad-top .btn--ghost { color: var(--ad-bg); border-color: rgba(255,255,255,.28); }
.ad-top .btn--ghost:hover { background: rgba(255,255,255,.12); color: var(--ad-white); }
/* 顶栏里的状态文字：深底上要能看清，且不能太长（挤掉按钮） */
.ad-top .ad-status { color: rgba(255,255,255,.72); max-width: 260px; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
/* ★ R7：深顶栏上的「未保存」改吃规范警告色 —— #FF7D00 压深顶栏 **6.26:1** ✓
   （旧值 #ffd7a8 是自调浅琥珀，属"另一套色"，已并掉）
   ★ 2026-10-03 SAGE：深顶栏底现为 `--ad-ink` ⇒ **#0E1405**（历史值 #1D2129 已废）。 */
.ad-top .ad-status.is-dirty { color: var(--ad-dark-warn); }

/* 连接状态：一行小字 + 一个圆点（照目录站的 .ad-conn，但做成 <button> 可点重测）
   ★★ R7·Q6 更正：三态**不是**吃 `--ad-ok/--ad-warn/--ad-err` 那三个亮色 ——
   那是**白底填充色**（对白只有 2.78 / 2.57 / 3.71，全在 AA 线下），拿它们当文字本来就违规。
   深底用的是 **DESIGN.md §2.4「Dark 区块（顶栏专用）」** 早就定死的一套**浅色**：
       ok #b7e8d2 · warn #ffe0a8 · fail #ffc2c2 ＋ 各自 rgba 描边 ＋ 圆点色
   压深顶栏 `--ad-ink`（现 #0E1405；历史值 #1D2129 已废）实测 **11.90 / 12.66 / 10.56** —— 全部远超 AA 4.5，
   文字与圆点**同色系**、语义一眼可辨（原先 fail 只有 4.35，还被迫借页底色当文字）。
   ⇒ 令牌 `--ad-dark-*`（三个文字 + 三个描边 + 三个圆点）。*/
.ad-conn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 9px; border-radius: var(--r-pill);
  border: 1px solid rgba(255,255,255,.24);
  background: rgba(255,255,255,.06);
  color: rgba(255,255,255,.8);
  font-size: var(--fs-12); line-height: 1.4;
  font-family: inherit; cursor: pointer;
  transition: background var(--tr), border-color var(--tr), color var(--tr);
}
.ad-conn:hover { background: rgba(255,255,255,.14); border-color: rgba(255,255,255,.4); }
.ad-conn__dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--ad-ink-2); flex: none;
}
.ad-conn.ok   { color: var(--ad-dark-ok);   border-color: var(--ad-dark-ok-line); }
.ad-conn.ok   .ad-conn__dot { background: var(--ad-dark-ok-dot); }
.ad-conn.warn { color: var(--ad-dark-warn); border-color: var(--ad-dark-warn-line); }
.ad-conn.warn .ad-conn__dot { background: var(--ad-dark-warn-dot); }
.ad-conn.fail { color: var(--ad-dark-fail); border-color: var(--ad-dark-fail-line); }
.ad-conn.fail .ad-conn__dot { background: var(--ad-dark-fail-dot); }

/* ★ R7·Q5：**loading spinner**（规范 §7：16px · 1s 循环）。
   一层细边框 ＋ 顶边实色，转起来就是 spinner —— 不用图、不用字体图标（规则④）。
   两档色分开：**轨道走装饰线、动的那段走当前文字色** ⇒ 放在深色顶栏里也看得见
   （`.ad-conn` 里 currentColor 是白 / 红 / 绿，跟着走）。

   ★ 唯一使用点 = **顶栏连接状态「检测中…」**（`probeConn()` 期间）：
     见下方 `.ad-conn.is-busy` 两条 —— 那一刻圆点换成它。
     这也是全站**唯一**一处真的会转的加载态（其余页面都是本地 JSON，快到不值得给 spinner）。 */
.ad-spin {
  display: inline-block; flex: none;
  width: var(--ad-ico-s); height: var(--ad-ico-s);
  border: 2px solid currentColor; border-top-color: transparent;
  border-radius: 50%;
  animation: ad-spin-w 1s linear infinite;
}
@keyframes ad-spin-w { to { transform: rotate(360deg); } }
/* 顶栏：平时藏起来，`.is-busy` 期间顶掉圆点。 */
.ad-conn .ad-spin { display: none; }
.ad-conn.is-busy .ad-spin { display: inline-block; }
.ad-conn.is-busy .ad-conn__dot { display: none; }
/* 尊重系统「减少动态效果」：**不取消**（那会让"还在转"这个信息消失），只放慢。 */
@media (prefers-reduced-motion: reduce) { .ad-spin { animation-duration: 3s; } }

/* ---------------------------------------------------------------- 三栏骨架 */

/* 默认：导航 220px（R7·Q3 由 176 改；v25 批曾从 236px 收窄）+ 内容区
   ★★ 2026-10-03 第三轮（余总「**右侧没有内容了显示不正常**」—— 我改坏了，这是修复）：
      改前 `.ad-nav` 是 `sticky` ⇒ 它是 grid 的**第 1 列**，占着 220px。
      第三轮把它改成 `fixed`（要"上顶"）—— **fixed 元素脱离文档流、不再占 grid 格位**
      ⇒ `.ad` 的第一列塌成 0，`.ad-main` 被顶到第 1 列（只有 220px 宽），
      而第 2 列（1220px）**整块空着** —— 实测 `main.box = {x:0, w:220}`，
      截图里就是"右栏全白、内容挤在侧栏那么窄的一条里"。

      ⇒ **口径改为「侧栏用 padding 占位」**：`.ad` 改成**单列** grid ＋
        `padding-left: var(--ad-side-w)`，侧栏 fixed 浮在那段 padding 上。
        这样内容区天然从 220px 开始、拿到整宽（1440 − 220 = 1220）——
        与改前视觉**完全一致**，只是侧栏从"占格"变成"浮层占位"。

      ⚠️ 为什么不用 `grid-template-columns: 220px 1fr` ＋ 空占位元素：
         fixed 的侧栏不属于任何格，要占位就得再塞一个空 `.ad-nav` 影子元素 ——
         **两份 DOM 才能表达一件事**，下次改侧栏宽必然漏改一处。
      ⚠️ 为什么不用 `margin-left`：padding 写在 `.ad` 上，`100%` 宽度类都自动跟着走；
         margin 要逐个容器补。 */
.ad { display: block; min-height: 100vh; padding-top: var(--ad-top-h); padding-left: var(--ad-side-w); }
/* ★ 产品页的钩子类。**它不加列** —— 中栏是靠 .ad-body 里那个 .ad-split 分出来的
   （导航是 .ad 的第 1 列；中栏与右栏都在内容区**里面**）。
   ⚠️ 第一版把这里写成 176px 340px 1fr，结果 `.ad-body` 只分到 1/3 宽，里面的
      `.ad-split` 再把那 1/3 切成 340px+剩余 ⇒ 右栏被挤成一条缝（截图当场看出来）。
      教训：**分栏要在同一个容器里做一次**，不能在外层和内层各切一刀。 */
.ad--split { }
/* 分栏容器（由 JS 渲染进 .ad-body）。这才是「中栏 340px + 右栏自适应」那一刀。 */
.ad-split {
  display: grid; grid-template-columns: 340px minmax(0, 1fr);
  gap: var(--sp-5); align-items: start;
}
.ad-split .ad-side {
  position: sticky; top: calc(var(--ad-top-h) + var(--ad-tabh));   /* 顶栏之下；v26：再让开页签栏 */
  max-height: calc(100vh - var(--ad-top-h) - var(--ad-tabh));
  display: flex; flex-direction: column; overflow: hidden;
}
.ad-split .ad-editor { min-width: 0; }

/* ⚠️ 这里**曾经**有个顺序坑：`.ad-body--split { max-width:none }` 写在本段（骨架区），
   被后面的 `.ad-body { max-width:1180px }` 反盖（同为单类、特异性 0,1,0 ⇒ 行序决定胜负），
   于是产品编辑页右栏只拿到 1180px、右边白掉一大块（2026-09-25 20:1x）。
   ★ B22（21:5x）已把这个坑**从根上拆掉**：`.ad-body` 不再有 `max-width`，
     两个变体类也不再有 max-width 规则 ⇒ 没有第二处争同一个属性，行序不再要紧。 */


/* ---------------------------------------------------------------- 侧栏 */

.ad-nav {
  /* ★★ 2026-10-03 SAGE 严格对齐（第三轮 · 余总「**左侧侧边栏应该上顶**」）：
     改前 `position: sticky; top: var(--ad-top-h); height: calc(100vh - var(--ad-top-h))`
     —— 侧栏被顶栏压在下面，顶部空出 56px 一条（正是余总截图里指出的问题）。
     ⇒ 改 `fixed; top:0; left:0; height:100vh`：**真上顶、占满整高**。

     ⚠️⚠️ **为什么不是 `sticky; top: 0`**（这是最容易写错的一步）：
        `.ad-nav` 是 `.ad`（grid）的**子项**，而 `.ad` 有 `padding-top: var(--ad-top-h)`
        ⇒ sticky 的"粘住位置"被**包含块的 padding 边**挡住，侧栏最高只能到 56px 处，
        **永远上不到 0**。实测（1440 视口）它停在 `y=56`，等于没改。
        `fixed` 相对**视口**定位，不受祖先 padding 约束 ⇒ 才能真正 top:0。
     ⚠️ 连带：`.ad` 的 grid 第一列仍在（只是被 fixed 的侧栏**盖住**）；
        `padding-top` 那条也仍要给右栏用 ⇒ 两者都不动，侧栏自成一列浮在上层。
        z-index 取 90 —— **低于顶栏的 100**（窄屏抽屉外仍要让汉堡键可点），
        高于主区任何内容（`.ad-bar` 20 / `.ad-tabbar` 30）。
     ★ 宽度仍吃 `--ad-side-w`（R7·Q3 定的 220px），一处口径不变。 */
  position: fixed; top: 0; left: 0; z-index: 90;
  width: var(--ad-side-w);
  height: 100vh;
  overflow-y: auto;
  /* ★ R7·Q7 丙案「整站去灰」把侧栏提了白；★★ R7·Q8（余总 2026-10-01 15:2x 选 **C**）：
     只靠一条 1px 右边线太糊 ⇒ 侧栏改**极淡底**与主区重新分层；
     1px `--ad-line` 右线**保留** ⇒ 底 + 线双保险。
     ★★ 2026-10-03 SAGE 换皮后现值 = `--ad-bg-soft` ⇒ **#E9EAE2**（SAGE --paper-sunken），
        与页面底 `#F0F1E9`、卡面 `#FAFBF4` 同属暖纸族（色相 68），三档明度递降。
        ⚠️ 下面注释里 `#F7F9FB` / `#F2F4F7` 是历史值（冷白/冷灰），**已被 SAGE 换皮取代**，
           保留仅为记录演变，勿按它判断现状。 */
  background: var(--ad-bg-soft);
  border-right: 1px solid var(--ad-line);
  display: flex;
  flex-direction: column;
}
/* ★★ 2026-10-03 SAGE 严格对齐（第三轮 · 余总拍板原话）：
   「**左侧栏顶放赫蒙logo+「赫蒙管理系统」颜色不变**」
   ＋ 追加订正「**左侧上下通栏为统一浅色而不是分两个颜色**」

   ⇒ 品牌条**不吃橄榄底**，与侧栏其余部分**同一底色** `--ad-bg-soft` (#E9EAE2)，
     上下贯通成一条浅色栏（这正是 `.ad-nav` 自己的底，所以这里**不写 background**
     —— "不写"就等于"跟着侧栏走"，日后侧栏换底自动跟队，少一处独立口径）。

   ⚠️ **第一版错在哪**：给了品牌条 `--ad-accent` #263819 橄榄底 ＋ 青柠字。
      那是我按"应用最外层标识"自作主张加的 —— 余总不要：侧栏是**一整条**导航栏，
      顶上突然一块深色会把它切成"两块"（上面深、下面浅），与他对侧栏"一整条"的认知冲突。
      SAGE 规范里也确实**没有**"侧栏品牌条必吃深底"这一条 —— 是我越界了。

   ★ 颜色口径（余总「颜色不变」）：
     · 底 = 侧栏底 `--ad-bg-soft` #E9EAE2（与列表 / 页脚**同底**，通栏一致）
     · 字 = 主文字色 `--ad-ink`（浅底上用深字 —— 压在 #E9EAE2 上实测对比度充足）
     · 下边线 = `--ad-line`（与侧栏右线同源，一条线收口，不新造颜色）
   ★ 它是 `.ad-nav`（flex column）的第一项，须 `position: sticky; top:0` 钉住 ——
     侧栏整体 `overflow-y:auto` 时首项本来会跟着滚走。 */
.ad-nav__brand {
  position: sticky; top: 0; z-index: 2;
  padding: var(--sp-4) var(--sp-5);
  /* ★ 显式写 `--ad-bg-soft`（而不是留空继承）：品牌条是 sticky 浮在滚动内容之上，
     必须**自己有底**，否则列表滚过时会从它下面透出来。值 = 侧栏底，视觉上"同一块"。 */
  background: var(--ad-bg-soft);
  color: var(--ad-ink);
  /* ★★ 2026-10-03 SAGE 落地「其它页面」（批 S1）：**实线 → 虚线**。
     依据 = 样机 `.side .logo` 的下边线是 `1px dashed var(--line-2)`（见样机 CSS），
     而 SAGE 七铁律第 ⑤ 条「**虚线分组、实线分栏**」——
     品牌条是"分组"（把标识与导航分开），故走**虚线**。
     ⚠️ 颜色用 `--ad-line`（与侧栏右线同源）而非 `--ad-line-2`：
        样机用 `--line-2` 是因为它的 brand 有白底、需更明显；我们的侧栏底是 `--ad-bg-soft`，
        `--ad-line-2`（1.35:1）压上去几乎看不见 —— 这是「**照抄值 vs 照抄意图**」的分野，
        本批照抄的是**意图（虚线分组）**，值按我们自己的底色取。 */
  border-bottom: 1px dashed var(--ad-line);
  /* ★★ 2026-10-03 第三轮**订正**（余总原话）：
     「**LOGO和管理系统应该分为二行 现在管理系统 字都看不到了**」
     改前是 `display: flex` **横排**：LOGO 占 110px ＋ 文字只剩 49px
     ⇒ 「赫蒙管理系统」5 个字（约 70px）被压掉后两个字（截图实测宽 49）。
     ⇒ 改 **两行竖排**：`flex-direction: column` ＋ `align-items: flex-start`。
        LOGO 一行、名称一行，各自拿满 220 − 40(padding) = 180px 可用宽。
     ⚠️ 为什么不是"把 LOGO 缩小、继续横排"：220px 里塞「标 ＋ 5 字中文」
        必然有一边被牺牲（要么标糊、要么字截）。**上下两行是唯一两边都完整**的排法。 */
  display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--sp-2);
}
/* ★ LOGO 单行：高 26px 不变（比改前的 30px 略收 —— 两行加起来才 40px，
   侧栏顶不至于占掉太多纵向空间）。宽度上限放到 150px，让横向标能展开看清。 */
.ad-nav__brand img { height: 26px; width: auto; max-width: 150px; object-fit: contain; }
/* ★ 定名「赫蒙管理系统」：字号从 `--fs-11` 提到 `--fs-14`
   （改前是"副标题式"的浅灰小字 —— 那是给「内容后台」当后缀用的；现在它是**主标识**，
     必须一眼看得见）。
   ⚠️⚠️ **选择器必须是 `.ad-nav__brand span.ad-nav__brandname`（两级）**：
      下面那条"兜底"的 `.ad-nav__brand span` 特异性是 0,1,1 —— 如果这里只写
      `.ad-nav__brandname`（0,1,0），**会被它反盖**，字号停在 11px（实测踩到）。
      本项目最贵的一类坑：**改了个寂寞，且不报错**。 */
.ad-nav__brand span.ad-nav__brandname {
  font-size: var(--fs-14); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide); color: var(--ad-ink);
  white-space: nowrap;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}
/* 兜底：品牌条内其余 span（窄屏会 `display:none` 收掉）统一吃主文字色。 */
.ad-nav__brand span { font-size: var(--fs-11); letter-spacing: var(--ls-wider); color: var(--ad-ink); }
.ad-nav__sep {
  border-left: 1px solid var(--ad-line-2);
  padding-left: var(--sp-3);
  white-space: nowrap;
}
.ad-nav__list { padding: var(--sp-4) var(--sp-3); display: grid; gap: 2px; }
.ad-nav__group {
  padding: var(--sp-5) var(--sp-3) var(--sp-2);
  font-size: var(--fs-10);
  letter-spacing: var(--ls-widest);
  text-transform: uppercase;
  color: var(--ad-ink-2);
}
/* ★ R7·Q5：加 24px 图标后布局从 `space-between` 改 `gap` ＋ 「文字吃满」——
   原来靠 space-between 把文字与数字顶到两头；现在中间多了图标，
   space-between 会把**图标单独顶到最左、文字居中**，反而散架。
   ⇒ 改为 `gap: var(--sp-2)`（图标↔文字 8，规范值）＋ `span { flex: 1 }` 把数字推到右。 */
.ad-nav__item {
  display: flex; align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-2);
  font-size: var(--fs-13);
  color: var(--ad-ink-2);
  transition: background var(--tr), color var(--tr);
}
/* 图标：24 档 · 不吃宽（`flex: none`）· 颜色**继承** currentColor
   ⇒ 悬停 / 选中时跟文字一起变，不必另写一套。 */
.ad-nav__ico { flex: none; width: var(--ad-ico-l); height: var(--ad-ico-l); display: block; }
/* 文字吃满剩余宽 ⇒ 数字自然贴右（取代原来的 `justify-content: space-between`）。 */
.ad-nav__item > span { flex: 1 1 auto; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.ad-nav__item:hover { background: var(--ad-bg-sunk); color: var(--ad-ink); }
/* ★★ 2026-10-03 SAGE 严格对齐：激活态改「深橄榄底 + 青柠字」（规范 §7.1 导航胶囊
   「激活＝橄榄底青柠字，靠反色，不靠阴影」）。
   原为 `background: var(--ad-ink); color: var(--ad-bg)` —— 近黑底 + 暖纸字，虽然也是反色，
   但**不带橄榄色相**，与顶栏 `--ad-ink` 底的深橄榄 #0E1405 撞成一片、选中感弱。
   ⇒ 底 --ad-accent（SAGE --olive #263819）· 字 --ad-lime-2（SAGE --lime-2 #E1E9C2）：
     实测 9.87:1，远超 AA；且青柠是「高光笔」，正好标出「你在这里」。 */
.ad-nav__item.is-on { background: var(--ad-accent); color: var(--ad-lime-2); }
.ad-nav__item em { font-style: normal; font-size: var(--fs-10); opacity: .6; }
/* ★★ 2026-10-03 SAGE 落地「其它页面」（批 S1）：底注三处改。
   ① `border-top` 实线 → **虚线**（同品牌条口径，SAGE 铁律⑤「虚线分组」）。
   ② 新增 `.ad-nav__foot-note` —— 原来那三段技术说明（"内容存在 data/*.json…" /
      "快捷键 Ctrl/⌘+S"）是**内联 `style` 写在 index.html 里**的，绕过了令牌体系
      （正是 R7·Q5 记录过的老毛病："绕过阶梯硬编码"）。⇒ 收进本文件，并精简成 2 行。
      ⚠️ 为什么精简：那三行原文对**天天用的余总**是噪音 —— 保存按钮就在顶栏常驻、
         备份是自动的，"内容存在哪"是开发者视角。样机 `.sf` 底注只放**两个入口链接**。
      ⇒ 保留"查看前台 / 站点设置"两入口，技术说明收成一行小字。
   ③ `font-size` 由 `--fs-11` 提到 `--fs-12`（底注是**要读的**文字，11px 偏小）。 */
.ad-nav__foot {
  margin-top: auto;
  padding: var(--sp-5);
  border-top: 1px dashed var(--ad-line);
  font-size: var(--fs-12);
  color: var(--ad-ink-2);
  line-height: var(--lh-snug);
}
.ad-nav__foot a { display: block; margin-bottom: var(--sp-2); color: var(--ad-ink-2); }
.ad-nav__foot a:hover { color: var(--ad-ink); }
.ad-nav__foot-note { margin-top: var(--sp-3); font-size: var(--fs-11); color: var(--ad-muted); line-height: var(--lh-normal); }

/* ---------------------------------------------------------------- 主区 */

/* ★★ 2026-10-03 SAGE 严格对齐（第二轮 · 余总「**右栏背景色应该是 #f0f1e9**」）：
   改前 `.ad-main` **没有背景**（透明）⇒ 透出 `body` 的 `--ad-bg #FAFBF4`（= 卡片色）。
   后果：**页底与卡面同色、L1/L2 层级塌掉** —— `.ad-panel` 只剩 1px 边框在硬撑，
   余总截图里「卡片浮不起来」就是这个原因。
   规范 §05「深度层级（无阴影）」：**L1 画布 `--paper` → L2 卡面 `--paper-2`，相邻只差一档明度**。
   ⇒ 右栏（画布层）取 `--ad-bg-alt #F0F1E9`，卡片（`--ad-panel` / `--ad-card`）留在 `--ad-bg #FAFBF4`，
     两档明度差 = 真正的「浮起」，且全靠明度、零阴影（合规范铁律⑦）。
   ⚠️ 为什么不改 `body`：`base.css` 的 `body` 是**前后台共用**的，改它会让前台一并遭殃
     （本项目铁律：共用层一律在 `admin.css` 覆盖写）。
   ⚠️ 为什么不改 `--ad-bg-alt` 令牌值：它还被**表头底**引用（余总定「表头默认为 #F0F1E9」），
     令牌值必须保持 #F0F1E9 ⇒ 只在 `.ad-main` 上按需取用。 */
.ad-main { min-width: 0; display: flex; flex-direction: column;
  /* ★ 2026-10-04（批 T3 · 需求 1）：**画布必须铺满视口** ——
     高度若只由内容撑开（此前是 646px），内容短的页（如空间案例列表）
     下方会露出 `body` 的**卡片色** `--ad-bg`，与上方画布的页底色 `--ad-bg-alt`
     对不上（真浏览器实测：下方 198px 异色）。加 `min-height:100vh` 后
     画布永远铺满，页底色到底。⚠️ 不用改 `body` 底色 —— 那会牵动登录页与全站根底。 */
  min-height: 100vh;
  background: var(--ad-bg-alt); }

/* ---------------------------------------------------------------- 页签栏（v26 批 · 2026-09-29）

   ★ 高度写进 `--ad-tabh` —— 因为下面 `.ad-bar` 与几个 sticky 中栏都要「接在它下面」，
     而它们的 top 都是算出来的（顶栏 + 页签栏）。**只在这一处定高度**，
     以后调高矮只改这一个数，不会出现「改了页签栏、标题条被压住一半」。

   ★★ 为什么是 sticky、还排在最上面：滚长列表时页签必须够得着 ——
     它是**切页的入口**，跟着内容滚走就等于没有。
     层级 z-index 30 > `.ad-bar` 的 20，但两者 top 不同（46 / 84），实际不重叠。

   ⚠️⚠️ **类名不许叫 `.ad-tabs`** —— 那个名字**早被产品编辑页与站点设置页占了**
     （它们内部的分组页签组：`.ad-tabs` + `.ad-tabs__item` + `.ad-tabpanel`，
     CSS 在下面「产品编辑页」段，JS 由 `[data-tabscope]` 驱动）。
     这一批第一版就叫了 `.ad-tabs`，`node --check` 全绿、页面也能开 —— **但那是撞车：
     这里的 sticky / 38px 高 / 横滚会**静默**套到那两个页的页签组上**（不报任何错）。
     ⇒ 改名 `.ad-tabbar`（tab strip）。**教训：新起一个通用类名之前，先
       `grep -rn "\.ad-xxx" v2/` 全站查一遍** —— CSS 撞车没有任何报错，只有肉眼能发现。 */
:root { --ad-tabh: 38px; }

/* ★★ 无签状态（登录页 / 还没进路由）：页签栏藏掉，**并且把高度一并归零**。
   ⚠️ 为什么必须归零：`.ad-bar` 的 `sticky top` 是 `calc(var(--ad-top-h) + var(--ad-tabh))`。
     只藏不归零的话，未滚动时标题条好好待在原位，**一滚动就吸附到 84px** ⇒
      凭空往下跳 38px（页签栏明明不在）。这种"滚动才发作"的错最难被截图看见。
   ★ 做法是**覆盖那一个变量**（而不是逐条重写 top / max-height）——
     一处口径，以后再加 sticky 元素自动跟上，不会漏。 */
.ad--notabs { --ad-tabh: 0px; }
.ad--notabs .ad-tabbar { display: none; }

.ad-tabbar {
  position: sticky; top: var(--ad-top-h); z-index: 30;
  height: var(--ad-tabh);
  display: flex; align-items: stretch; gap: 3px;
  padding: 5px var(--sp-5) 0;
  /* ★ R7·Q7：标签条同样提白（未选中签仍是 --ad-bg-sunk #E7EBF0，靠签自身深浅区分）。 */
  background: var(--ad-bg);
  border-bottom: 1px solid var(--ad-line);
  /* 标签多了横滚。滚动条藏掉：这一条只有 38px 高，一条滚动条会吃掉一大半
     （用鼠标滚轮横滚，见 admin.js 的 tabsInit）。 */
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none; -ms-overflow-style: none;
}
.ad-tabbar::-webkit-scrollbar { width: 0; height: 0; display: none; }
.ad-tabbar:empty { display: none; }   /* 登录页 / 还没进路由：不留一条空条 */

.ad-tabbar__tab {
  flex: none;
  display: inline-flex; align-items: center; gap: 5px;
  max-width: 220px;                    /* 记录名很长时截断，不许一个签占满整条 */
  padding: 0 var(--sp-1) 0 var(--sp-4);
  border: 1px solid transparent; border-bottom: 0;
  border-radius: var(--r-4) var(--r-4) 0 0;
  /* ★ R7·Q8：**非激活签提白**（与「激活＝浅灰」交叉替换）—— 白条上靠文字/✕ 区分。 */
  background: var(--ad-bg);
  color: var(--ad-ink-2);
  font-size: var(--fs-12); line-height: 1;
  white-space: nowrap; cursor: pointer; user-select: none;
  transition: background var(--tr), color var(--tr);
}
.ad-tabbar__tab:hover { background: var(--ad-line); color: var(--ad-ink); }
.ad-tabbar__tab:focus-visible { outline: 2px solid var(--ad-ink); outline-offset: -2px; }
/* ★★ R7·Q8（余总 2026-10-01 15:2x 原话「**激活去掉下划线　激活 #F2F4F7**」）：
   原来那 2px 近黑顶条（`box-shadow: inset 0 2px 0`）**删掉**；
   激活态改**浅灰底「凹」进白条**（非激活是白）——
   两态从此靠「底色 ＋ 字重」区分，不再靠一道线。1px `--ad-line` 边两态都留。 */
.ad-tabbar__tab.is-on {
  background: transparent;
  color: var(--ad-ink);
  border-color: transparent;
  font-weight: var(--fw-medium);
  /* ★★ 2026-10-04（批 T3 · 需求 2-①）：**选中加下划线**，对齐样机 2px 橄榄。
     ⚠️ 【铁律⑩ 冲突⇒定死优先】本行**覆盖** 2026-10-01 R7·Q8「激活去掉下划线」。
     —— 样机真值（`后台主题_SAGE终端风.html` `.stab.on`）：
        `border-bottom-color: var(--olive)` ＋ `font-weight:500`；非选中的 2px 底边**透明**。
     ★ 用 `box-shadow` 而**不是** `border-bottom`：本签 `border-bottom:0`（要跟卡片头贴合），
       且签高不足 40px ⇒ 真加 2px 边框会**撑高整条签**（`--ad-tabh` 写死 38px）⇒ 跑到溢出去。
       `box-shadow: inset` 画在**盒子内部**，零布局影响。
     ★★ 2026-10-04（批 T4 · 需求 2）：余总「**去掉框　保留（选中）下划线即可**」＋
        「**里面颜色透明**」⇒ `background` 由 `--ad-bg` 改 **`transparent`**、
        `border-color` 由 `--ad-line` 改 **`transparent`**。选中态**只剩** 2px 橄榄下划线。 */
  box-shadow: inset 0 -2px 0 var(--ad-accent);
}
.ad-tabbar__t { overflow: hidden; text-overflow: ellipsis; padding-right: 2px; }
/* 未保存圆点：与侧栏 `.ad-nav__dot` **同色同尺寸**（同一个 --ad-accent），
   两个地方都表示"这份数据没存" —— 口径必须一致。 */
.ad-tabbar__tab.is-dirty::before {
  content: ''; flex: none;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--ad-accent);
}
.ad-tabbar__x {
  flex: none;
  width: 16px; height: 16px;
  display: grid; place-items: center;
  padding: 0; border: 0; border-radius: 50%;
  background: rgba(15, 19, 25, .07);
  color: var(--ad-muted-ink);
  font-family: inherit; line-height: 1;
  cursor: pointer;
  transition: background var(--tr), color var(--tr);
}
.ad-tabbar__x:hover { background: var(--ad-ink); color: var(--ad-bg); }
.ad-tabbar__x svg { display: block; }

.ad-bar {
  position: sticky; top: calc(var(--ad-top-h) + var(--ad-tabh)); z-index: 20;   /* v25 批：顶栏之下；v26：再让开页签栏 */
  /* ★★ 2026-10-03 SAGE 严格对齐（余总「后台现状颜色与样机不一致请严格对齐样机」）：
     原 `rgba(252,251,252,.95)` 是 R7 时代的旧冷白（色相 300，发紫），与暖纸底
     `--ad-bg #FAFBF4`（色相 68）并排是一条冷带。改为 `--ad-bg` 的 95% 透明版
     —— 保留 backdrop 毛玻璃（滚动时透出内容），但底色回到暖纸族。 */
  background: color-mix(in srgb, var(--ad-bg) 95%, transparent);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--ad-line);
  /* ★★ R7 · Q3（S5）：标题区 68 → 56，与顶栏同值（--ad-bar-h）。
     ★ 为什么是 56 而不是规范的 48：48 − 40（按钮）= 上下各 4px，按钮会贴边；
       56 = 40 + 上下各 8，正好放得下且不换行（判据 W10 守）。 */
  padding: var(--sp-2) var(--sp-5);
  display: flex; align-items: center; gap: var(--sp-4);
  min-height: var(--ad-bar-h);
}
/* ★★ R7 · Q3（S5 配套）：标题区要塞进 56 ⇒ 里面那两行必须 ≤ **40**（56 − 上下各 8）。
   真跑实测原值：标题 `lh 30.6`（fs 18 × 1.7）＋ 副标题 `lh 18.7` = **49** ⇒ 把 `.ad-bar`
   顶到 **66**，`min-height: 56` 根本压不住（min-height 只在内容更小时才起作用）。
   ⇒ **收行高**：标题 30.6 → **24**（×1.33）· 副标题 18.7 → **15**（×1.36），合计 **39**；
     加下边 **1px 描边** = **56** 总量 ⇒ `--ad-bar-h` 记的是**含描边的总高**。
     ⚠️ 第一版把副标题收成 16（合 40）⇒ 实测 **57**（40 ＋ padding 16 ＋ border 1）——
        `min-height` 在 border-box 下**含 padding 与 border**，40 就已经越界 1px。
        ⇒ 内容上限是 **39**，不是 40。这条是「差 1px 也算没到」的实例。
   ★ 为什么收行高、而不是把副标题挪到标题右边同行：**不动视觉结构** —— 副标题仍在标题
     下方，只把两行的"气"收紧；中文标题 1.33 仍在可读区间（≥1.3），18px 字不会被裁。
   ★★ 副标题必须 `nowrap` ＋ 省略号：**它一变两行，高度立刻反弹回 66** ——
     这条正是「改了但没生效」的典型（判据 W10 同时守高度和 `scrollHeight`）。 */
.ad-bar__title { font-size: var(--fs-18); font-weight: var(--fw-normal); line-height: 24px; }
.ad-bar__sub { font-size: var(--fs-11); color: var(--ad-muted); letter-spacing: var(--ls-wide);
  line-height: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ★★ 批 K2（2026-10-05）：副标题（`#adSub`）**空就不占位** ——
   五张报表页的副标题与绿条里的说明句**逐字同一句**（余总「把那两条说明去掉」）
   ⇒ 副标题改为**停渲染**（`sub = ''`）。但空 `<div>` 仍会占**一行高** → 页头留个空档，
   看起来像“没排好”。`:empty` 恰好只匹配**真没内容**的（初始“正在读取数据…”不会被误伤）。 */
#adSub:empty { display: none; }
.ad-bar__actions { margin-left: auto; display: flex; align-items: center; gap: var(--sp-3); }

/* ★★ 2026-09-25 21:5x（B22）：**页面主体一律全宽** —— 这里以前是 `max-width: 1180px`。
   余总原话：「系列页面 空间案例 后台设置页面尺寸还是没有铺满 请全面排查一下后台页面尺寸」。
   实测（1600 视口）：`.ad-main` 1424、`.ad-body` 卡在 1180 ⇒ 每种**非宽屏**页面右边
   白掉 244px；他的窗口约 2100+ CSS px ⇒ 白掉 700+px（截图里就是那块空地）。
   ★ 为什么干脆把 cap 拿掉、而不是调成 1600/1800：**只有一处口径才不会分叉**。
     留一个数字就得为每类页面（列表 / 表单 / 三栏 / 素材）判断「够不够宽」，
     而 `.ad-fields--2` / `.ad-fields--3` 已经把表单列数交给视口决定了 —— 再叠一层
     页面级 cap 就是第二套口径。全宽 = 没有这一层。
   ⚠️ 因此 `.ad-body--wide` / `.ad-body--split` 的 `max-width:none` **已删**（成了死规则）：
      JS 仍会给 `.ad-body` 加这两个类（`adMain()` 里），但它们现在**只作语义标记**，
      CSS 里不再有任何规则绑定它们 —— 别再把「宽度」写在这两个类上。
      （B19 那轮踩过的顺序坑从此不存在：不再有两处争同一个属性。） */
/* ★★ R7 · Q3（S4）：内容区 padding 48 → 24。
   ⚠️⚠️ **不能改 `--sp-7`** —— 它是**前后台共用**令牌（前台多处引用），
     改令牌值会让前台一并遭殃。所以只在这里覆盖，令牌本身保持 48px（判据 W11 守着）。 */
.ad-body { padding: var(--sp-5); width: 100%; }

/* ---------------------------------------------------------------- 概览 */

.ad-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--sp-5); }
.ad-card {
  background: var(--ad-bg);
  /* ★★ 2026-10-03 SAGE 落地（批 S1）：**实线 → 虚线** + 圆角 `--ad-r-card`。
     依据 = 样机 `.card{background:var(--paper-2);border:1px dashed var(--line-2);
     border-radius:var(--r-card)}` —— 逐字照抄（含 `--line-2` 这一档）。
     ★ 为什么用 `--line-2`（1.57:1，压卡面几乎看不见）而不加深：**忠实还原样机**。
       样机的虚线卡是**大面积轮廓**、靠整体感知，不是靠单条线的对比度；
       加深到 `--line-3` 会变成"显眼的框"，反而比样机重 —— 那是另一种偏离。
       ⚠️ 若截图目视发现"虚线看不见"，再回来调（留待 S1-G 走查裁决）。 */
  border: 1px dashed var(--ad-line-2);
  border-radius: var(--ad-r-card);
  padding: var(--sp-5);
  display: flex; flex-direction: column; gap: var(--sp-2);
}
.ad-card__k { font-size: var(--fs-11); letter-spacing: var(--ls-wider); text-transform: uppercase; color: var(--ad-muted); }
.ad-card__v { font-size: 34px; font-weight: var(--fw-light); line-height: 1.1; }
.ad-card__n { font-size: var(--fs-12); color: var(--ad-ink-2); }

/* ═══════════════════════════════════════════════ ★★★ E5-1（2026-10-06）报表中心（卡片墙）

   余总逐字：「**将报表以卡面形式在一个页面集中展示**」。
   ★★ 版式口径（照 SAGE 七铁律「**虚线分组 · 实线分栏**」）：
      卡片是**独立的可点分栏**（不是"把东西圈在一起"的分组框）⇒ 用**实线** `--ad-line`。
      ⚠️ 别照抄上面 `.ad-card` 的虚线 —— 那是**分组卡**（概览页的统计块），语义不同。
   ★★ 卡片是 `<a>` ⇒ **必须显式** `color: inherit; text-decoration: none`，
      否则浏览器给未访问链接上蓝字 + 下划线（本项目在"顺手加个链接"上咬过好几次）。
   ★ 只有三种视觉状态，且都**由 JS 的 class 驱动**（不靠标签猜）：
      `a.ad-rpc` 正常可点 · `.ad-rpc.is-todo` 灰一档 · `.is-hidden` 被分类签滤掉 */
.ad-rpc__chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: var(--sp-5) 0; }
.ad-rpc__chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 12px;
  border: 1px solid var(--ad-line);
  border-radius: 999px;
  background: transparent;
  color: var(--ad-ink-2);
  font-family: inherit; font-size: var(--fs-12);
  cursor: pointer;
}
.ad-rpc__chip em { font-style: normal; font-size: var(--fs-11); color: var(--ad-muted-ink); }
.ad-rpc__chip:hover { border-color: var(--ad-line-3); }
/* 选中 = **浅底 ＋ 深字**（不用深底反白）—— 与"青柠只提亮"的口径一致，对比度也稳。 */
.ad-rpc__chip.is-on {
  background: var(--ad-acc-bg);
  border-color: var(--ad-acc-line);
  color: var(--ad-accent-3);
}
.ad-rpc__chip.is-on em { color: var(--ad-accent-3); opacity: .75; }

/* ★ `grid-auto-rows: minmax(…, auto)` —— **统一行高**。
   ⚠️ 实测（首版截图目视）：卡片的字数并不一样（有的多一行「待并入…」说明）
   ⇒ 行与行之间高矮不齐，一堵卡片墙看着就"歪"。
   给每行一个**下限**，内容多的照样长高（第二个值是 `auto`）—— 不是截断。 */
.ad-rpcs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(232px, 1fr));
  grid-auto-rows: minmax(110px, auto);
  gap: var(--sp-4);
}
.ad-rpc {
  display: flex; flex-direction: column; gap: var(--sp-2);
  padding: var(--sp-4);
  background: var(--ad-bg);
  border: 1px solid var(--ad-line);
  border-radius: var(--ad-r-card);
  color: inherit; text-decoration: none;
}
a.ad-rpc:hover { border-color: var(--ad-line-3); }
/* ⚠️ 待做卡是 `<span>`（没有 href、点不动）—— 这里只负责"看起来是灰的",
    **不负责"拦住点击"**（拦点击是标签类型决定的，不是 CSS）。 */
.ad-rpc.is-todo { opacity: .55; cursor: default; }
.ad-rpc.is-hidden { display: none; }
.ad-rpc__h { display: flex; align-items: center; gap: var(--sp-2); }
.ad-rpc__no { flex: 0 0 auto; min-width: 11px; font-size: var(--fs-11); color: var(--ad-muted-ink); }
.ad-rpc__t { font-size: var(--fs-14); }
.ad-rpc__d { font-size: var(--fs-12); color: var(--ad-ink-2); }
.ad-rpc__m { font-size: var(--fs-11); color: var(--ad-muted-ink); }
.ad-rpc__tag {
  margin-left: auto; flex: 0 0 auto;
  padding: 1px 7px;
  border: 1px dashed var(--ad-line-2);
  border-radius: 999px;
  font-size: var(--fs-10); letter-spacing: var(--ls-wide);
  /* ★ 这是**要读的字**（"待做" / "待并入"）⇒ 必须用 `--ad-muted-ink`（6.33:1）。
     `--ad-muted` 只有 4.05:1，那是**装饰件**（图标 / 占位符）的档，不能拿来排字。 */
  color: var(--ad-muted-ink);
}
.ad-rpc__tag--merge { border-style: solid; border-color: var(--ad-acc-line); color: var(--ad-accent-3); }

.ad-panel {
  background: var(--ad-bg);
  border: 1px solid var(--ad-line);
  /* ★★ 2026-10-03 SAGE 落地（批 S1）**圆角 4 → --ad-r-card(10)**。
     依据 = 样机里"装表格 / 装内容"的容器是 **`.tablewrap`**：
         `.tablewrap{background:var(--paper-2);border:1px solid var(--line-1);border-radius:var(--r-card)}`
     —— 注意样机的 `.card`（虚线）与 `.tablewrap`（实线）是**两种容器**，
     后台 `.ad-panel` 对应的是**后者（实线容器）**，所以这里 **保留实线、只改圆角**。
     ⚠️ 别把 `.ad-panel` 改成虚线：那会让全站装表格的容器变虚线边
        （表格本身有线，虚线框 + 表格线会"双层碎线"，视觉很乱）。
        虚线分组另有其类（样机 `.card` / 我们已有的 `.sage-grp`）。
     ⚠️ `overflow: hidden` 保留 —— 它让表头（sticky）与圆角不打架（不裁会露出直角）。 */
  border-radius: var(--ad-r-card);
  margin-top: var(--sp-6);
  overflow: hidden;
}
.ad-panel__head {
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--ad-line);
  display: flex; align-items: center; gap: var(--sp-4);
  font-size: var(--fs-13);
  letter-spacing: var(--ls-wide);
}
.ad-panel__body { padding: var(--sp-5); }
.ad-panel__body--flush { padding: 0; }

/* ---------------------------------------------------------------- 表格 */

/* ★★ 2026-10-07 表内字号：--fs-13(15px) ⇒ --fs-td(12px)（余总定「表里面的字体应该保持 12」）。
   改前是「表头 12 ＜ 表内 15」= 标签比数据还小 —— A 段抬变量值时没复核引用留下的。 */
.ad-table { width: 100%; border-collapse: collapse; font-size: var(--fs-td); }
/* ★ 2026-10-07 ：表内**显式**声明 12px —— 过去靠继承，`.ad-quiet`（空值占位「—」）这类工具类一盖就变 13px。
   特异性 (0,1,1) > `.ad-quiet` (0,1,0) ⇒ 表内空值也回到 12px。 */
.ad-table td { font-size: var(--fs-td); }
/* ★ 2026-09-28 22:2x（AA 1.4.3 对比度）：原为 10px + letter-spacing .14em +
   text-transform:uppercase + var(--ad-muted)=#9C9186 —— 压在页面底上实测 **2.99:1**，
   只有 AA 门槛 4.5 的 66%，是后台里最读不清的一格（DESIGN.md §4.20）。
   颜色换 --ad-muted-ink（现 #5C6350 ⇒ 6.33:1），并补 500 字重。
   ★★ 2026-10-03 SAGE 严格对齐（余总拍板「**严格照规范加 uppercase**」）：
     §04 字体表「表头 = --fs-10 · 500 · .14em · 全大写」⇒ 字号回到 --fs-10、恢复 .14em 与 uppercase。
     ⚠️ 与上面 AA 那条的口径差异：AA 只要求**对比度**达标 —— `--ad-muted-ink #5C6350`
        压 `--ad-bg #FAFBF4` 实测 **6.33:1** ✓（远超 4.5），所以「回到 10px + 拉散」之后
        **对比度并未回退**，回退的只是「字号大小」这一个观感项。这就是本轮敢恢复的底气。
     ⚠️ 中文无大小写 ⇒ uppercase 对中文列名（占九成）**无视觉影响**，只对英文/缩写列生效
        （统一列序的实验已经证明：英文列变全大写后与中文列混排依然齐整）。 */
.ad-table th {
  /* ★★ 2026-09-30（余总定 · UI 真源 DESIGN.md §4.34）：**列表一律居中 —— 含列头**。
     本条**推翻**了旧口径「数字列右对齐 + `tabular-nums`」（§4.8 Do 第 2 条 ·
     `.cell--num` · §4.19/§4.22 · 本处原来的 `left`）。★ `tabular-nums` **保留**
     （居中了更需要它，否则列宽随数字值抖）。 */
  text-align: center;
  padding: var(--sp-3) var(--sp-4);
  /* ★★ 2026-10-07 表头字号：--fs-10(12px) ⇒ --fs-th(14px)（余总定「表头字体应该是 14」）。 */
  font-size: var(--fs-th);
  font-weight: var(--fw-medium);
  letter-spacing: .14em;
  text-transform: uppercase;
  /* ★★ 2026-10-07：表头字色接皮肤令牌（余总定「表头文字深绿」）。 */
  color: var(--ad-th-ink);
  /* ★★ 2026-10-03 SAGE 严格对齐（第三轮 · 余总截图问「表头没有固定颜色」）：
     改前 `.ad-table th` **没有 background**（实测 `rgba(0,0,0,0)` = 透明）
     ⇒ 表头直接透出页面底 `--ad-main` 的 `#F0F1E9`，读起来像"忘了上色"。
     规范 §05「深度层级（无阴影）」把**表头**归到 L1 画布层 —— 与页面底同色但
     **必须显式声明**（否则卡片里的表就会透出卡片色 `#FAFBF4`，两处表头两样）。
     ⇒ 显式取 `var(--ad-bg-alt)` = `#F0F1E9`（余总定「表头默认为 #F0F1E9」）。
     ⚠️ 为什么不改 `--ad-bg-alt` 令牌值：它还被**页面底**（`.ad-main`）引用，
        令牌值必须保持 #F0F1E9 ⇒ 只在表头按需取用。
     ⚠️ 冻结列 `thead th:first-child`（L1877）**必须同值**，否则横滚时表头第一格
        与其余表头分家 —— 那一处原本就已是 `--ad-bg-alt`，本句等于把它"扶正为通则"。 */
  background: var(--ad-bg-alt);
  /* ★★ 2026-10-03 SAGE 落地「其它页面」（批 S1）：表头下边线 **`--ad-line` → `--ad-line-3`**。
     依据 = 样机 `thead th` 是 `border-bottom: 1px solid var(--line-3)`，
     而数据行（`tbody td`）才是 `--line-1` —— **表头线比数据行线重一档**，
     目的是让"表头 / 数据"在视觉上**分得开**（读宽表时视线不串行）。
     ★ 现值对比（本批新算）：
         `--ad-line  #DADFCC` 压卡面 1.31:1  ← 几乎看不见，表头与首行连成一片
         `--ad-line-3 #A1A696` 压卡面 2.32:1  ← 一眼看得出"这是表头"
     ⚠️ 它**不是控件边界**（那是 `--ad-ctl-line` 的职责，2.32 ＜ 3:1 不够格），
        只是**分隔线** ⇒ 不违反 WCAG 1.4.11（该条只约束控件边界）。 */
  border-bottom: 1px solid var(--ad-line-3);
  white-space: nowrap;
}
/* ★★ 批 J4（2026-10-05）：表头筛选 —— 点表头那枚漏斗 ⇒ 表头下**长出一行筛选框**。
   余总原话「表头要有筛选功能」＋「表格打印配置要参考 ruoyi」；做法参考若依
   （bootstrap-table 的 filter-control：列头直接给筛选入口）。
   ★ 风格沿用 SAGE：筛选行底色比表头再深一档（表头 `--ad-bg-alt` / 筛选行 `--ad-bg-sunk`），
     「这一行是控件、不是数据」一眼分得开；**零阴影**（铁律）。 */
.ad-thf {
  width: 16px; height: 16px; padding: 0; margin-left: 4px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: transparent; color: var(--ad-muted-ink);
  border-radius: var(--r-1); cursor: pointer; vertical-align: -3px;
  opacity: .5; transition: opacity var(--tr), color var(--tr);
}
.ad-table th:hover .ad-thf { opacity: 1; }
.ad-thf:hover { color: var(--ad-accent); }
.ad-thf.is-on { opacity: 1; color: var(--ad-ink); }
.ad-frow th {
  padding: 4px var(--sp-3);
  background: var(--ad-bg-sunk);
  border-bottom: 1px solid var(--ad-line-3);
  white-space: normal;
}
.ad-frow__i {
  width: 100%; min-width: 56px; height: 24px;
  padding: 0 6px; font-size: var(--fs-12);
  border: 1px solid var(--ad-ctl-line); border-radius: var(--r-1);
  background: var(--ad-bg); color: var(--ad-ink);
}
.ad-frow__i:focus { outline: 2px solid var(--ad-accent); outline-offset: -1px; }
/* 被筛掉的行：走 `hidden` 属性。★ 显式补一条兜底 ——
   万一将来某条 CSS 给 `tr` 设了 `display`，`hidden` 就会被静态样式盖掉（本项目血训形状：
   「写了却不生效、也不报错」）。 */
.ad-table tbody tr[hidden] { display: none; }
.ad-table td { padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--ad-line); vertical-align: middle;
  text-align: center; }   /* ★ 2026-09-30：列表数据一格一律居中（同表头口径，§4.34） */
/* ★★ 2026-10-03 SAGE 严格对齐（第二轮 · 余总标注「鼠标划过色 #e9eae2」）：
   原 `var(--ad-bg-alt)` = #F0F1E9（= 页面底色）—— 与页面底**同色** ⇒ 悬停时行只是
   「亮了一下边界」、几乎没有反馈。改 `var(--ad-bg-sunk)` = #E9EAE2（SAGE --paper-sunken）：
   比页面底深一档、比卡片底深两档 ⇒ 悬停是明确的「下沉」反馈。
   ★ 为什么用现成令牌而不新加色：`--ad-bg-sunk` / `--ad-zebra` 本就 = #E9EAE2，语义是
     「凹陷 / 下沉区」，行悬停正是这个语义 —— 不需要第五支同值令牌。 */
.ad-table tr:hover td { background: var(--ad-bg-sunk); }
/* ★★ 2026-10-04（批 T4 · 需求 1 · 余总第 3 次更正后定死，逐字）：
   「整行恢复到之前的 SAGE换皮　单号列悬停 去掉底色 ＋ 加粗 ＋ 文字色 #E4757E（单号列)　整行不变」
   ⇒ ① **整行悬停** = 恢复到 SAGE 换皮原状：`background: var(--ad-bg-sunk)` = #E9EAE2
        （**不加粗、不改字色** —— 行里其它列「整行不变」）
      ② **单号列**（`td.ad-ellink`）**单独**处理：悬停时**去掉底色** ＋ **加粗** ＋ **字色 #E4757E**
   ⚠️ 中途三版已废（余总三次更正）：
     · 首版：整行青柠底 #CFDCA8 ＋ `color: --ad-ink` ⇒ 把日期/金额列橄榄绿盖成深墨
     · 二版：整行青柠底 ＋ 保字色 ⇒ 余总要**去掉底**
     · 三版：整行**无色块** ＋ 整行玫红加粗 ⇒ 余总「**整行不变**」，只有**单号列**变
   ★ `.ad-docno` 令牌见 tokens.css（#E4757E，2.82:1，余总品牌指定）。
   ★ 死令牌已清：`--ad-hover-bg` 无人用 ⇒ 已从 tokens.css 删掉（不留负债）。 */
.ad-table img.thumb { width: 40px; height: 50px; object-fit: cover; border-radius: var(--r-1); }
.ad-table .num { color: var(--ad-ink-2); font-variant-numeric: tabular-nums; }
.ad-table__actions { white-space: nowrap; text-align: center; }   /* ★ 2026-09-30：动作列一起居中（不留例外） */
.ad-drag { cursor: grab; color: var(--ad-ink-2); user-select: none; }
.ad-table tr.is-dragging { opacity: .4; }
.ad-table tr.is-over td { box-shadow: inset 0 2px 0 var(--ad-accent); }

/* ---- ★★ 批 22-A1：明细表（`.ad-table--lines`）的列宽 ＋ 表头齿轮 · 2026-09-30 ----
   ★ `table-layout:fixed` 是**列宽能生效的唯一前提** —— `<input>` 默认固有宽 ≈ 20 字符，
     auto 布局下会把「单位 / 数量 / 单价」顶到 140+px（销售单样机 `_probe_b22_proto.py` 实测 141px），
     反把「金额」挤到 61px。余总 00:14：「列距要合理分配，个 / 数量 / 单价这些要窄一些」。
   ★ 只加在**明细表**上（`.ad-table--lines`）—— 全站另有 47 处 `.ad-table` 是已验收页面，
     不碰它们（改 `table-layout` 会整体动版）。
   ★ 明细表比列表页**密**：左右内边距收到 8px（与样机同款），否则「单位」这种窄列连标题都放不下。 */
.ad-table--lines { table-layout: fixed; }
/* ★★ R7 · Q3（S7）：六域行表表头 **47 → 32** —— 与报价单页 `table.ad-lines th`、
   以及格内控件 32 三处同高（余总定「明细表头 = 格内控件 32」，一条口径）。
   ⚠️⚠️ **必须同时清掉上下 padding**：`.ad-table th` 的 `padding: 12px 16px` 会把它
      原样撑回 47 —— 那就是「height 写了却不生效」，本项目最贵的一类静默错。
   ⚠️ 六域与报价单页是**两套**（`.ad-table--lines` / `table.ad-lines`），漏一套就只改了一半。 */
.ad-table--lines th { height: 32px; padding-top: 0; padding-bottom: 0; }
.ad-table--lines th, .ad-table--lines td { padding-left: var(--sp-2); padding-right: var(--sp-2); }
.ad-table--lines th.ad-lops__cell { width: 96px; }
.ad-table--lines th:nth-child(2) { width: 52px; }
.ad-table--lines td input, .ad-table--lines td select { width: 100%; min-width: 0; }

/* 齿轮：**在「操作」两字左边**。
   ⚠️ 铁律（本机血案族）：**不许给 th 写 `display:flex`** —— 单元格会脱离表格布局，
      变成**静默空档 ＋ 边框断开**（`.ad-pick` 套 `td/th` 那次）。
      ⇒ 并排靠内层 `.ad-lops__th`（inline-flex），**不碰 th 自己的 display**。 */
.ad-lops__th { display: inline-flex; align-items: center; gap: var(--sp-1); }
.ad-lops__gear {
  width: 18px; height: 18px; padding: 0; border: 0; border-radius: var(--r-2); background: none;
  color: var(--ad-muted-ink); cursor: pointer; display: inline-flex;
  align-items: center; justify-content: center; flex: 0 0 auto;
}
.ad-lops__gear:hover { color: var(--ad-ink); background: var(--ad-bg-sunk); }

/* ---- ★★ 批 22-A2：明细列**右缘拖动热区**（列宽自己调）· 2026-09-30 ----
   ★ 热区 = 绝对定位的 `span`（`th` 上开 `position:relative`）⇒ **不占文档流**，
     既不挤表头文字，也**不会**把 th 变成 flex（血案族：`th{display:flex}` ⇒ 静默空档＋边框断开）。
   ★ 7px 宽、`right:-3px`：一半搭在分隔线上、一半伸进本格，手感跟桌面表格软件一致。 */
.ad-table--lines th { position: relative; }
.ad-table--lines th.ad-lops__cell,
.ad-table--lines th:nth-child(2) { position: static; }   /* 操作列 / 行号列**不给拖**（宽度是结构性的） */
.ad-lrz {
  position: absolute; top: 0; right: -3px; width: 7px; height: 100%;
  cursor: col-resize; z-index: 3; user-select: none; touch-action: none;
}
.ad-lrz::after {
  content: ''; position: absolute; left: 3px; top: 22%; bottom: 22%; width: 1px;
  background: transparent; transition: background var(--tr);
}
.ad-lrz:hover::after { background: var(--ad-accent); }
body.ad-lrz-on { cursor: col-resize; user-select: none; }
body.ad-lrz-on .ad-lrz::after { background: var(--ad-accent); }

/* ★★ 批 K2（2026-10-05）：**凡挂 `.ad-lrz` 的表，`th` 必须自带定位上下文** ——
   热区是 `position:absolute`；`th` 没定位时它就按**更高层**的已定位祖先定位 ⇒
   全表热区**叠到容器右缘、并探出 3px** ⇒ 页面级横向滚动条。
   实测受害：批 J4 六个维护页（dicts / supplier / customer / sys_param /
   warehouse / admin_menu）与本批五张报表页 —— 均 **3px**。
   给 `th` 定位后，热区回到本格、多出的 3px 被**面板的 `overflow:hidden`** 兜住。
   ★ 对本就**没有**绝对定位子元素的表 = **零视觉影响**（只改包含块）。 */
.ad-table th { position: relative; }

/* ---- ★★ 批 G16（2026-10-05）：**单据列表页**也能「拖列宽 ＋ 图钉冻结列」 ----
   ⚠️ 只给**单据列表页**那一个表（类 `.ad-table--dl`）—— 全站另有 47 处 `.ad-table`
      是**已验收页面**，一个字不碰（改 `table-layout` 会整体动版）。
   ★★ `table-layout:fixed` **只在用户真拖过 / 真冻结过之后**才挂（类 `.ad-table--rz`）——
      没动过的表保持 auto（与现在逐像素一致）⇒ **默认视觉零变化**。
   ★ 热区**直接复用** `.ad-lrz`（绝对定位 span，不占文档流、不把 th 变成 flex）——
      与明细表同一套先例，不另立第二种拖法。 */
.ad-table--dl th { position: relative; }
.ad-table--dl th.ad-rowchk,
.ad-table--dl th.ad-table__actions { position: static; }   /* 勾选列 / 操作列不给拖（结构性宽度） */
.ad-table--dl th:hover .ad-lrz::after { background: var(--ad-line-2); }
.ad-table--rz { table-layout: fixed; }
/* 图钉：**悬停才现** —— 表头已有「列名 ＋ 漏斗」，常显会挤成一团。 */
.ad-table--dl .ad-pin {
  border: 0; background: none; cursor: pointer; padding: 1px; margin-left: 3px;
  border-radius: var(--r-2); color: var(--ad-line-3); line-height: 0; vertical-align: middle;
  opacity: 0; transition: opacity .15s ease, background .15s ease;
}
.ad-table--dl th:hover .ad-pin, .ad-table--dl th.ad-fix .ad-pin { opacity: 1; }
.ad-table--dl .ad-pin:hover { background: var(--ad-bg-sunk); color: var(--ad-ink-2); }
.ad-table--dl .ad-pin.is-on { color: var(--ad-accent); }
/* 冻结列：`border-collapse: collapse` 下单元格边框会**跟着滚走**（边框属表格、不属格）
   ⇒ 冻结线用 `box-shadow` 画（既有先例：`.ad-body--wide` 首列 · 规格表）。
   ⚠️ 背景必须**自己写**且**跟着 `tr:hover` 变** —— 否则横滚时下层内容透出来。 */
.ad-table th.ad-fix, .ad-table td.ad-fix {
  position: sticky; z-index: 2; background: var(--ad-bg);
  box-shadow: 1px 0 0 var(--ad-line);
}
.ad-table thead th.ad-fix { z-index: 4; background: var(--ad-bg-alt); }
.ad-table tbody tr:hover td.ad-fix { background: var(--ad-bg-sunk); }

/* ★★ 批 G17（2026-10-05）：**行拖拽换序**（列表页单据行 ＋ 单据明细行）。
   ★ 落点指示用 `box-shadow` 而不是 `border-bottom` —— `border-collapse` 下给行加 border
     会**撑动行高**（拖一行，整表跳一下），而 `box-shadow` 不参与布局。同 G16 冻结线那条口径。
   ★ `is-dragging` 用半透明而不是 `display:none` —— 拖源留在原位，落点才有个参照物。
   ★★ 2026-10-05 修：原本这里多一个「星号斜杠」收尾符，把注释提前关掉了，
     ⇒ CSS 解析到那里错乱，**吃掉了紧跟的 `.ad-drag-h { … }` 整条规则**（手柄尺寸/行高全失效）。
   ★ 拖源 / 落点这两个类**复用批 22-A1 已有的**「`.ad-table tr.is-dragging`」与
     「`.ad-table tr.is-over td`」（就在上面 `.ad-drag` 那三行）—— **不新造第二套同名不同值的规则**。
     同名选择器写两份 = 谁后谁赢；将来改其中一份会显得「改了没反应」（本项目最贵的一类静默错）。 */
/* 明细行的拖把手：坐在操作列里那几个按钮**之前**（明细行里有输入格，
   整行 draggable 会跟"在格子里拖选文字"打架 ⇒ 只让这把小手柄发起拖拽）。 */
.ad-drag-h { display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; margin-right: 3px; color: var(--ad-muted-ink);
  cursor: grab; vertical-align: middle;
  /* ★★ 2026-10-05：**手柄不许被 flex 挤缩** —— 在 `.ad-lops` 里它原本被挤成 13.5px，
     里面的 16px 图标跟着缩到 13.5px ⇒ `check_q5_ui.py` X4（尺寸只 16/24/48）当成越界。 */
  flex: none; }
.ad-drag-h:active { cursor: grabbing; }

/* ---------------------------------------------------------------- 表单 */

/* ★ 2026-09-25 20:2x（B19）：**必须有 minmax(0,1fr)**。
   `display:grid` 不写列模板 = 单列 `auto`，而 auto 列的下限是**内容的 min-content**
   ⇒ 内部有固定 min-width 的宽表（如 `.ad-spec-tbl` 1408px）会把整列顶到自己的 min-content，
   `.ad-form` / `.ad-editor` 跟着被撑爆、页面出横向滚动条 —— 该滚的 `.ad-spec-wrap`
   （overflow-x:auto）反而滚不起来。实测 1920 档：列被撑到 1510px、`.ad` scrollWidth 2098。
   ⇒ `minmax(0,1fr)` 把下限压回 0，超宽内容回到「自己的滚动容器里滚」。 */
.ad-form { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-6); }
.ad-fieldset {
  background: var(--ad-bg);
  border: 1px solid var(--ad-line);
  border-radius: var(--r-4);
  padding: var(--sp-5);
}
.ad-fieldset > legend,
.ad-fieldset__title {
  font-size: var(--fs-11);
  letter-spacing: var(--ls-wider);
  text-transform: uppercase;
  color: var(--ad-muted);
  margin-bottom: var(--sp-5);
  padding: 0;
}
.ad-fields { display: grid; gap: var(--sp-5); }
.ad-fields--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 767px) { .ad-fields--2 { grid-template-columns: minmax(0,1fr); } }

/* 三列。★ 2026-09-24 19:5x 加 —— 报价条款分区的「顺序值 / 印在哪 / 是否启用」一行放得下。
   ⚠️ 加之前我先在 admin.js 里写了 `ad-fields--3`，但**这个类当时不存在** ⇒
      三个字段静默落回单列、各占一整行（截图目视才发现）。
      ⇒ 这是「CSS 类名拼错不报错」的典型：**类名要跟 CSS 一起确认存在**，别照记忆写。 */
.ad-fields--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* ★★ 2026-09-29（批 17 · §4.8.1 铁律「不要一列到底」）：
   多列容器里**宽控件必须跨列**（占满整行）—— 长文本 / 图墙 / 块 / 列表塞进半栏会挤成一团。
   `is-wide` 由 `wrap()` 按**控件形状**（kind）打，不是按键名 —— 见 admin.js 的 `WIDE_KINDS`。
   没有 `is-wide`（短控件：文本 / 数字 / 日期 / 下拉）的字段才参与并排。 */
.ad-fields--2 > .ad-f.is-wide,
.ad-fields--3 > .ad-f.is-wide { grid-column: 1 / -1; }
@media (max-width: 1199px) { .ad-fields--3 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
@media (max-width: 767px) { .ad-fields--3 { grid-template-columns: minmax(0,1fr); } }

/* 产品编辑页的分组标题栏：左标题 + 右「展开 / 收起」。
   默认全展开，只有被收起的才加 .is-closed（见 admin.js viewProductEdit）。 */
.ad-fieldset__bar {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-5);
}
.ad-fieldset__bar .ad-fieldset__title { margin-bottom: 0; flex: 1; }
.ad-fieldset.is-closed { padding-bottom: var(--sp-4); }
.ad-fieldset.is-closed .ad-fieldset__bar { margin-bottom: 0; }
.ad-fieldset.is-closed .ad-fieldset__body { display: none; }
/* ★★★ 批 CLEAN · C-4（2026-10-06）—— **折叠的隐藏口径**（余总㉓③「折叠面板（设置页分组）」）
   -------------------------------------------------------------------
   `.is-closed` 时，除标题栏外的**直接子元素**一律不显示。
   ★★ 为什么是 `:not(.ad-fieldset__bar)` 而不是只认 `.ad-fieldset__body`：
     给**已有页面**补折叠时（本批：站点设置 12 组 · 导航与页脚 4 组 · 页面与导航 2 组），
     那些分组的正文**已经是若干并列的 div**（`.ad-fields` / `.ad-note` / `.ad-item`…）。
     要把它们再包一层 `__body`，就得在**拼出来的 HTML 字符串**里找出配对的那个 `</div>`
     —— 而正文里到处都是 `'</div>' +` 片段，靠数数配平**极易改错、且改错了不报错**。
     ⇒ 改成「正文一个字符不动、只换标题那一行」（见 `admin.js` 的 `fsLead` / `fsTog`）。
   ★ 老写法（`__bar` ＋ `__body`，产品/品类/字典/供应商…那十来页）在这条规则下
     **行为逐字不变** —— 它们的直接子元素正好就是 `__bar` ＋ `__body`
     （`__body` 被本条命中 ⇒ 与上一行 `.is-closed .ad-fieldset__body{display:none}` 同效）。
   ⇒ **一条规则同时管两种写法**，不是两套实现。上一行保留（belt & braces）。 */
.ad-fieldset.is-closed > *:not(.ad-fieldset__bar) { display: none; }

/* ★★ 2026-09-27 全铺（批 6.7之五）：「**字段即列头**」—— 飞书 §4.8 第 ② 条 · 样机的 `.ff`。
   改前 = `flex column`（标签压在控件**上方**）⇒ 十来组字段竖着排就滚出好几屏；
   改后 = **左标签 / 右控件**同处一行（170px ＋ minmax(0,1fr)），纵向高度砍掉近一半。
   ★ 用 `minmax(0,1fr)` 不能用 `1fr`：`1fr` 的下限是内容的 min-content，
     内部有宽控件的字段（色板 / 媒体面板）会把右列顶出去（B19 那条老教训，同一个坑）。
   ★ 标签给 `padding-top:9px` 与右侧控件**文字基线对齐**（控件高 34px、文字居中）。 */
.ad-f {
  display: grid;
  grid-template-columns: 170px minmax(0, 1fr);
  column-gap: var(--sp-4);
  row-gap: var(--sp-1);
  align-items: start;
}
.ad-f__label {
  grid-column: 1;
  grid-row: 1;
  font-size: var(--fs-11);
  letter-spacing: var(--ls-wide);
  color: var(--ad-muted);
  display: flex; align-items: center; gap: var(--sp-2);
  padding-top: 9px;
}
.ad-f__label .req { color: var(--ad-accent); }
/* 控件与 hint 都在**右列**（hint 自动落到控件下方那一行）—— 显式写列号，别靠自动放置。 */
.ad-f > :not(.ad-f__label) { grid-column: 2; }
.ad-f__hint { font-size: var(--fs-11); color: var(--ad-ink-2); }

/* ★★ 批 G17（2026-10-05）：**必填校验的「字段下红字」**（余总当日定死口径：「字段下红字 ＋ 输入框描红边」）。
   ★ 描红边**复用既有 `.ad-bad`**（红描边＋红底，原先只有销售单 `salesMarkBad` 在用）——
     不另造第二套"标红"：两套颜色/粗细早晚会漂，那时就没人分得清哪套是对的。
   ★ 为什么两件必须一起给：只描红边 ⇒ 长表单里还得一行行找是哪格；只给红字 ⇒ 看不出是哪一格。
   ★ `.ad-f__err` 由 `adBadMark()` 就地插进字段外框里；`:empty` 收掉空壳（免得留一行空隙）。 */
.ad-f__err { font-size: var(--fs-12); color: var(--ad-err-text); line-height: 1.5; }
.ad-f__err:empty { display: none; }

/* ★★ 批 G17：**单选控件**（`kind: 'radio'`）—— 与既有多选 `.ad-checks` 同观感：
   一行铺开、窄容器自动折行；`accent-color` 跟全站一起走（别让浏览器给蓝）。 */
.ad-radios { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5); align-items: center; min-height: 34px; }
/* ★★ `flex: none` ＋ `white-space: nowrap` **两个都不能少**（批 G17 截图目视抓到的）——
   单选常落在**窄格**里（单头 `.f w2` / 明细行），flex 默认 `shrink:1` ⇒ 总宽超出时
   每枚被压窄，中文**逐字换行**（「已 发 出」竖着排），看着像坏了。
   ★ 正确表现 = 装不下就**整枚折到下一行**（外层本来就 `flex-wrap: wrap`），不是压扁每一枚。 */
.ad-radio { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-12); color: var(--ad-ink-2); cursor: pointer; flex: none; white-space: nowrap; }
.ad-radio input { flex: none; margin: 0; accent-color: var(--ad-ink); }

/* ★ 多列容器里标签要**收窄**：产品编辑页实测（三栏窄容器 · 每列 **380px**）——
   170px 标签 + 194px 控件 ⇒ **标签占掉 45%，控件只剩一半**。
   样机的 170px 是给**单栏宽容器**定的，两列/三列里照搬就挤。 */
.ad-fields--2 .ad-f, .ad-fields--3 .ad-f { grid-template-columns: 110px minmax(0, 1fr); }

/* 双语输入 */
.ad-bi { display: grid; gap: var(--sp-2); }
.ad-bi__row { display: grid; grid-template-columns: 26px minmax(0, 1fr); align-items: start; gap: var(--sp-2); }
.ad-bi__tag {
  font-size: var(--fs-10);
  color: var(--ad-ink-2);
  letter-spacing: var(--ls-wide);
  padding-top: 14px;
  text-transform: uppercase;
}
.ad-bi__row.is-empty .ad-bi__tag { color: var(--ad-line-2); }

/* 可重复条目 */
.ad-list { display: grid; gap: var(--sp-3); }
.ad-item {
  border: 1px solid var(--ad-line);
  border-radius: var(--r-2);
  background: var(--ad-bg);
  padding: var(--sp-4);
}
.ad-item__head {
  display: flex; align-items: center; gap: var(--sp-3);
  margin-bottom: var(--sp-4);
  font-size: var(--fs-11);
  color: var(--ad-muted);
  letter-spacing: var(--ls-wide);
}
.ad-item__head .spacer { flex: 1; }
.ad-item__body { display: grid; gap: var(--sp-4); }

.ad-empty {
  padding: var(--sp-6);
  text-align: center;
  color: var(--ad-ink-2);
  font-size: var(--fs-13);
  border: 1px dashed var(--ad-line-2);
  border-radius: var(--r-2);
}
/* ★ R7·Q5：空状态图标（规范 §7：**48 / 64**）—— 48px「空盒子」，浅灰、居中。
   由 `decorateEmpty()` 在渲染收尾**统一补**进每一处 `.ad-empty` 的最前
   （不逐个改 10+ 个调用点，新加的空状态也自动带上）。

   ⚠️ **表格里的空状态不出图标**：`<td class="ad-empty">` 是"这一格没数据"，
      放 48px 图标会把那一行撑成一块牌子，把整张表的节奏打断。 */
.ad-empty__ico { display: block; margin: 0 auto var(--sp-4); color: var(--ad-ink-off); }
td.ad-empty .ad-empty__ico, th.ad-empty .ad-empty__ico { display: none; }

/* ---------------------------------------------------------------- 内容块 */

.ad-blocks { display: grid; gap: var(--sp-4); }
.ad-block {
  border: 1px solid var(--ad-line);
  /* ★★ 2026-10-03 SAGE 落地（批 S1）：圆角 4 → `--ad-r-card`(10)。
     依据 = 样机 `.card.hard{border-style:solid;border-color:var(--line-3);border-radius:var(--r-card)}`
     —— 块容器是"硬边卡"，圆角同 10。这里**保留实线 + 原 `--ad-line`**
     （样机用 `--line-3`；但 `.ad-block` 是**页面级块**，下辖多层内容，
      边线加深会与内部各层线抢视线 —— 属"结构线不抢内容"的一贯做法，暂保持）。 */
  border-radius: var(--ad-r-card);
  background: var(--ad-bg);
  overflow: hidden;
}
.ad-block.is-open { border-color: var(--ad-line-2); }
.ad-block__head {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  background: var(--ad-bg-alt);
  cursor: default;
}
.ad-block__type {
  font-size: var(--fs-12);
  letter-spacing: var(--ls-wide);
  color: var(--ad-ink);
  font-weight: var(--fw-medium);
}
/* ★★ E2（2026-09-27 · §4.9）：块头改成**有视觉身份**的行 ——
   ① 形状图标 ② 序号 ③ 主图缩略 ④ 类型名 ⑤ **真实首句**（空块才退回 desc）。
   ★ `.ad-block__hint` 由"一句类型说明"升级成"这一块里**实际写了什么**" ⇒
     让出弹性宽度（`flex:1` + 省略号），它现在是**信息**不是注解。 */
.ad-block__ico { display: flex; flex: none; color: var(--ad-ink-2); }
.ad-block__no {
  flex: none; min-width: 16px; text-align: center;
  font-size: var(--fs-10); color: var(--ad-ink-2);
  font-variant-numeric: tabular-nums;
}
.ad-block__thumb {
  flex: none; width: 28px; height: 28px; border-radius: var(--r-1);
  object-fit: cover; background: var(--ad-bg-sunk);
}
.ad-block__hint {
  font-size: var(--fs-11); color: var(--ad-ink-2);
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ad-block__tools { margin-left: auto; display: flex; align-items: center; gap: var(--sp-1); }
.ad-block__body { padding: var(--sp-5); display: grid; gap: var(--sp-4); border-top: 1px solid var(--ad-line); }
.ad-block.is-off { opacity: .55; }

.mini {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 28px; height: 28px; padding: 0 var(--sp-2);
  border: 1px solid var(--ad-line-2);
  border-radius: var(--r-2);
  font-size: var(--fs-11);
  color: var(--ad-ink-2);
  background: var(--ad-bg);
  transition: background var(--tr), color var(--tr), border-color var(--tr);
}
/* ★★ 2026-10-03 SAGE 严格对齐（第三轮 · 余总第二张截图原话，**这一版是订正**）：
   「**页面面上这些突兀有按钮默认状态下透明底 加边框 黑字  选中色 #E1E9C2**」

   ⇒ 口径（余总定死）：
     · **默认** = `透明底` ＋ `1px 边框` ＋ `黑字`（不用实心色块 —— 那正是他说"突兀"的原因）
     · **选中 / 激活** = 底 `#E1E9C2`（`--ad-lime-2`，浅青柠）

   ⚠️⚠️ **上一版（我做的）错在哪**：把 `.mini` 默认改成**实心橄榄底 `--ad-accent` #263819
      ＋ 青柠字**。那是我按自己的理解"填充＝强调"推的 —— 但 `.mini` 在后台**到处都是**
     （列表行内 ↑↓ / 编辑 / 删除、块头"收起/展开"、chip、规格表按钮…），
     全变成深色小块之后**整页到处是黑疙瘩**，余总截图当场圈出来了。
     教训：**"强调"是少数元素的特权，不是默认态** —— 一个全站复用的小按钮基类，
     默认就该是"最低存在感"（透明＋细边＋黑字），强调只在个别场景叠类。

   ★ 为什么"透明底"要写 `transparent` 而不是留空：`.mini` 会被放进**卡片**
     （`--ad-bg`）、**表格行**（`--ad-bg-sunk` 悬停）、**顶栏**等不同底色上；
     透明就等于"永远跟当前所在的底走"，不需要为每个场景各写一条。

   ★ 边框色 `--ad-line-2`（#C8CCBE）—— 留给"这是个可点的东西"的最小信号；
     比它更浅就看不见了（SAGE 零阴影，全靠边线分层）。

   ★ 详见下方「选中态」两条（`.mini.is-on` / `.mini[aria-pressed=true]`）。 */
.mini { background: transparent; color: var(--ad-ink); border-color: var(--ad-line-2); }
.mini:hover { background: var(--ad-bg-alt); color: var(--ad-ink); border-color: var(--ad-line); }
/* ★★ 选中 / 激活态：底 `--ad-lime-2` #E1E9C2（余总定「选中色 #E1E9C2」）。
   两种标记都认：`.is-on`（本项目既有约定，chip / 首屏按钮在用）
   ＋ `[aria-pressed="true"]`（标准属性，键盘/读屏也能读到状态）。
   ⚠️ 必须写在 `:hover` **之后** —— 否则同为 0,1,0 特异性下 hover 会反盖选中态
     （鼠标停在选中的按钮上，底色又变回悬停色 ⇒ "选中感"消失）。 */
.mini.is-on,
.mini[aria-pressed="true"] {
  background: var(--ad-lime-2); color: var(--ad-ink); border-color: var(--ad-lime-2);
}
.mini--danger { color: var(--ad-err-text); border-color: var(--ad-err-line); }
.mini--danger:hover { background: var(--ad-err-bg); color: var(--ad-err-text); border-color: var(--ad-err-text); }
.mini[disabled] { opacity: .35; pointer-events: none; }

.ad-add {
  width: 100%;
  padding: var(--sp-4);
  border: 1px dashed var(--ad-line-2);
  border-radius: var(--r-2);
  font-size: var(--fs-12);
  color: var(--ad-muted);
  letter-spacing: var(--ls-wide);
  transition: border-color var(--tr), color var(--tr), background var(--tr);
}
.ad-add:hover { border-color: var(--ad-ink); color: var(--ad-ink); background: var(--ad-bg-alt); }

/* ---------------------------------------------------------------- 素材 */

.ad-media-grid {
  display: grid;
  /* v24 批 4B：列更窄 + 间距更小，一屏多看好几张（343 张，翻页很费手）。
     只动列宽和间距，**不动 aspect-ratio** —— 灯具多是竖构图，裁狠了就看不出形。 */
  grid-template-columns: repeat(auto-fill, minmax(124px, 1fr));
  gap: var(--sp-3);
}
/* ★★★ G1（2026-09-27）：素材库「密度四档」——
   上一轮「全铺」把档位挂在 `.ad-table` / `.ad-spec-tbl` 上，**够不着图库**，
   ⇒ 素材库成了全站唯一没有档位的同类记录页（341 张图，只能一路滚）。
   ★ 复用**同一个变量** `--row-h`（顶栏那组按钮直接驱动它，不新造第二套控件）。
   ★★ 系数 **3.1 刻意对齐现状**：中档 `--row-h:40px` ⇒ `40×3.1 = 124px` = 改动前的列宽。
      ⇒ **一进来看到的和图库原来一模一样**，只有点档位才变 —— 否则「页面自己变样」会被当成回归。
   ⚠️ 本规则必须**写在 `.ad-media-grid` 之后**（同特异性，靠顺序取胜）；
      挪到前面会被上面那条 `grid-template-columns: ... 124px ...` 盖掉 —— 静默失效、不报错。 */
.ad-media-grid { grid-template-columns: repeat(auto-fill, minmax(calc(var(--row-h) * 3.1), 1fr)); }
.ad-media {
  border: 1px solid var(--ad-line);
  border-radius: var(--r-2);
  overflow: hidden;
  background: var(--ad-bg);
  display: flex; flex-direction: column;
  transition: border-color var(--tr);
}
.ad-media:hover { border-color: var(--ad-line-2); }
.ad-media.is-on { border-color: var(--ad-accent); }
.ad-media__img {
  aspect-ratio: 4/5; position: relative; overflow: hidden;
  /* 极淡的棋盘格：既是透明底提示，也能让白底/透明底的 LOGO 看得见 */
  background-color: var(--ad-bg-soft);
  background-image:
    linear-gradient(45deg, var(--ad-bg-sunk) 25%, transparent 25%, transparent 75%, var(--ad-bg-sunk) 75%),
    linear-gradient(45deg, var(--ad-bg-sunk) 25%, transparent 25%, transparent 75%, var(--ad-bg-sunk) 75%);
  background-size: 14px 14px;
  background-position: 0 0, 7px 7px;
}
.ad-media__img img { width: 100%; height: 100%; object-fit: cover; }
/* 矢量图（LOGO 之类）多是完整构图，别裁，留白居中 */
.ad-media__img img[src$=".svg" i] { object-fit: contain; padding: var(--sp-3); }
.ad-media__body { padding: var(--sp-3); display: grid; gap: var(--sp-1); }
.ad-media__name { font-size: var(--fs-11); word-break: break-all; line-height: 1.35; }
.ad-media__meta { font-size: var(--fs-10); color: var(--ad-ink-2); }
/* 引用数从 <span> 改成 <button>（可点开明细），把浏览器默认按钮样式清干净 */
.ad-media__use {
  font-size: var(--fs-10);
  color: var(--ad-ok-text);
  cursor: pointer;
  background: none;
  border: 0;
  padding: 0;
  font-family: inherit;
  text-align: left;
  width: max-content;
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}
.ad-media__use:hover { color: var(--ad-ink); }
.ad-media__use--none {
  color: var(--ad-ink-2);
  cursor: default;
  text-decoration: none;
}
.ad-media__use--none:hover { color: var(--ad-ink-2); }

/* ---- 素材库批量操作（2026-09-20 #26） ----
   勾选框贴在卡片左上角、常驻可见（不靠 hover 才出现 —— 触屏没有 hover）。
   选中态用 accent 描边 + 底色，和「未被引用」那类状态色区分开。 */
.ad-media { position: relative; }
.ad-media.is-picked { border-color: var(--ad-accent); box-shadow: 0 0 0 1px var(--ad-accent); }
.ad-media__pick {
  position: absolute; top: 6px; left: 6px; z-index: 2;
  width: 22px; height: 22px; border-radius: var(--r-2);
  background: rgba(255, 255, 255, .92); border: 1px solid var(--ad-line-2);
  display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.ad-media__pick input { width: 14px; height: 14px; margin: 0; cursor: pointer; accent-color: var(--ad-accent); }
.ad-media.is-picked .ad-media__img { background-color: var(--ad-bg-alt); }

.ad-batchbar {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  border: 1px solid var(--ad-accent); background: var(--ad-acc-bg);
  border-radius: var(--r-4); padding: var(--sp-3) var(--sp-4);
  margin: var(--sp-4) 0;
}
.ad-batchbar b { font-size: var(--fs-13); }
.ad-batchbar .input--sm { height: 26px; font-size: var(--fs-11); padding: 0 6px; min-width: 120px; }

.ad-drop {
  border: 1px dashed var(--ad-line-2);
  border-radius: var(--r-4);
  padding: var(--sp-8);
  text-align: center;
  color: var(--ad-muted);
  font-size: var(--fs-13);
  transition: border-color var(--tr), background var(--tr);
}
.ad-drop.is-over { border-color: var(--ad-accent); background: var(--ad-bg-alt); }

/* 素材选择弹层 */
.ad-picker {
  position: fixed; inset: 0; z-index: var(--z-modal);
  /* ★ R7-Q4：45% → 60% */
  background: var(--ad-dlg-scrim);
  display: grid; place-items: center;
  padding: var(--sp-5);
  opacity: 0; pointer-events: none;
  transition: opacity var(--tr);
}
.ad-picker.is-open { opacity: 1; pointer-events: auto; }
.ad-picker__panel {
  /* ★ R7-Q4：宽 1000 → 上限 900（超规范）· 圆角 4 → 8 · 补投影 */
  width: min(var(--ad-dlg-max-w), 100%);
  max-height: 86vh;
  background: var(--ad-bg);
  border-radius: var(--ad-dlg-r);
  box-shadow: var(--ad-dlg-sh);
  display: flex; flex-direction: column;
  overflow: hidden;
}
.ad-picker__head {
  /* ★ R7-Q4：标题区收成规范 56 高（原 padding 自然撑 ⇒ 实测 73）。
     ★★ `flex: 0 0 auto` **不能省** —— `.ad-picker__panel` 是 `flex-direction: column`
        且带 `max-height: 86vh`：内容一高，这个头会作为 flex 项**被压扁**
        （第一版实测 **41px** 而不是 56 —— 写了 height 也白写，因为 `flex-shrink` 默认 1）。
        ⇒ 凡「面板是 flex 列」的头 / 底栏，都要显式禁止收缩。 */
  flex: 0 0 auto;
  height: var(--ad-dlg-head-h);
  padding: 0 var(--sp-5);
  border-bottom: 1px solid var(--ad-line);
  display: flex; align-items: center; gap: var(--sp-4);
}
/* C 族**无独立底栏**（操作按钮在头区/行内，如「选它」「关闭」）——
   这是本族的结构事实，不是漏做；判据只守它的头区 56，不它对不存在的底栏下断言。 */
.ad-picker__body { padding: var(--sp-5); overflow-y: auto; }

/* 并发冲突弹层（S1.6 乐观锁）
   ★ 为什么自建而不是用 confirm()：要放差异清单、要能点击断言、要能截图；
     而且原生框在自动化里没人接就默认取消（本项目被咬过 —— 保存链路的原生框
     一弹，四个断言全红，而那是真用户每天会遇到的问题）。
   ★ 出口顺序是有意的：安全的「用服务器上的」在左，不可逆的「覆盖」在右且单独着色，
     别让人手滑点到。 */
.ad-cf {
  position: fixed; inset: 0; z-index: var(--z-modal);
  /* ★ R7-Q4：45% → 60%（走令牌，三族同一个数） */
  background: var(--ad-dlg-scrim);
  display: grid; place-items: center;
  padding: var(--sp-5);
}
.ad-cf__box {
  width: min(720px, 100%); max-height: 88vh;
  background: var(--ad-bg);
  /* ★ R7-Q4：圆角 4 → 8 · 补上投影（原来三族全无阴影） */
  border-radius: var(--ad-dlg-r);
  box-shadow: var(--ad-dlg-sh);
  display: flex; flex-direction: column;
  overflow: hidden;
}
.ad-cf__hd {
  /* ★ R7-Q4：标题区收成规范 56 高的一条（原靠 padding 自然撑，≈53 且不等高）。
     ★ 顺带给右侧那枚关闭图标留位（`justify-content` 不设，图标靠 `margin-left:auto` 顶到右边）。
     ★ `flex: 0 0 auto` —— 同 `.ad-picker__head`：`.ad-cf__box` 是 flex 列 ＋ `max-height: 88vh`，
       不禁收缩就会被内容压扁（血训见 `.ad-picker__head` 那段）。 */
  flex: 0 0 auto;
  height: var(--ad-dlg-head-h);
  padding: 0 var(--sp-5);
  border-bottom: 1px solid var(--ad-line);
  display: flex; align-items: center; gap: var(--sp-3);
  font-size: var(--fs-15);
  font-weight: var(--fw-medium);
  color: var(--ad-err-text);
}
.ad-cf__hd > b, .ad-cf__hd > span:first-child { flex: 1 1 auto; min-width: 0; }
/* ★ 中性确认（新建 / 改状态这类**非破坏性**问句）—— `.ad-cf__hd` 默认是错误红，
   那是给「不可逆」用的（删单 / 覆盖）；中性问句套红标题会**把红贬值**，以后真危险就不红了。
   ★ `--ask` 同时把盒子收窄：720px 是给「两边比对」那类内容的，一句话的确认框用不着那么宽。 */
.ad-cf--plain .ad-cf__hd { color: var(--ad-ink); }
.ad-cf--ask .ad-cf__box { width: min(460px, 100%); }
.ad-cf__sub {
  padding: var(--sp-4) var(--sp-5);
  font-size: var(--fs-12);
  color: var(--ad-ink-2);
  line-height: var(--lh-normal);
}
.ad-cf__hd2 {
  padding: 0 var(--sp-5) var(--sp-2);
  font-size: var(--fs-10);
  letter-spacing: var(--ls-widest);
  text-transform: uppercase;
  color: var(--ad-muted);
}
.ad-cf__diff {
  margin: 0 var(--sp-5) var(--sp-4);
  max-height: 38vh; overflow-y: auto;
  background: var(--ad-bg-alt);
  border: 1px solid var(--ad-line);
  border-radius: var(--r-2);
  padding: var(--sp-3) var(--sp-4);
}
.ad-cf__line {
  font-size: var(--fs-12);
  line-height: 1.9;
  color: var(--ad-ink-2);
  word-break: break-word;
}
.ad-cf__ft {
  /* ★ R7-Q4：底栏收成规范 64 高（原 padding 自然撑 ≈56）· 按钮间距 12 → 16。
     ★ `flex: 0 0 auto` 同上 —— 底栏被压扁 = 按钮被切。 */
  flex: 0 0 auto;
  display: flex; align-items: center; gap: var(--sp-4);
  height: var(--ad-dlg-foot-h);
  padding: 0 var(--sp-5);
  border-top: 1px solid var(--ad-line);
}
/* 关闭图标顶到标题区最右（三族一致的口径）。 */
.ad-cf__hd .ad-dlgx { margin-left: auto; }
.ad-cf__gap { flex: 1 1 auto; }
/* 覆盖是不可逆的：单独着色，跟「保存」按钮默认不动是同一个道理 */
.ad-cf__danger { border-color: var(--ad-err-text); color: var(--ad-err-text); }
.ad-cf__danger:hover { background: var(--ad-err-text); color: var(--ad-white); border-color: var(--ad-err-text); }

/* 媒体字段预览 */
.ad-mediafield { display: flex; align-items: center; gap: var(--sp-3); }
.ad-mediafield__thumb {
  width: 64px; height: 80px; object-fit: cover;
  border: 1px solid var(--ad-line); border-radius: var(--r-2);
  background: var(--ad-bg-alt);
  flex: none;
}
.ad-mediafield__thumb[src$=".svg" i] { object-fit: contain; padding: 6px; background: var(--ad-bg-soft); }
.ad-mediafield__path { flex: 1; min-width: 0; }

/* ---------------------------------------------------------------- 提示条 */

.ad-note {
  display: flex; gap: var(--sp-3);
  padding: var(--sp-4);
  border-radius: var(--r-2);
  /* ★ R7·Q7 提白后只剩 1px 外框（太糊）⇒ ★★ R7·Q8（余总 2026-10-01 15:2x 选 **C**）
     改回**极淡底**（比 Q7 前的 `--ad-bg-alt` 淡一档），
     与 1px `--ad-line` 外框合起来 = 双重分层，不再靠一道线硬撑。
     ★★ 2026-10-03 SAGE 换皮后 = `--ad-bg-soft` ⇒ **#E9EAE2**（历史值 #F7F9FB 已废）。 */
  background: var(--ad-bg-soft);
  border: 1px solid var(--ad-line);
  font-size: var(--fs-12);
  color: var(--ad-ink-2);
  line-height: var(--lh-normal);
}
.ad-note--warn { background: var(--ad-warn-bg); border-color: var(--ad-warn-line); }
.ad-note--ok { background: var(--ad-ok-bg); border-color: var(--ad-ok-line); }

/* ---------------------------------------------------------------- 客户一屏（阶段 4 · 批 C3）
   一屏的四组数字（报价 / 商机 / 跟进 / 关联）。★ 数字来自视图 v_customer_360，
   这里只管**排版**，不重新组织语义。
   ⚠️ 用 grid + auto-fit ⇒ 加一个指标不用改 CSS（口径 ⑬：别把布局写死成"总是四项"）。 */
.c360 {
  display: grid; gap: var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  margin-bottom: var(--sp-4);
}
.c360__group {
  padding: var(--sp-4);
  border: 1px solid var(--ad-line);
  border-radius: var(--r-2);
  background: var(--ad-bg-alt);
}
.c360__title {
  font-size: var(--fs-11); color: var(--ad-muted);
  letter-spacing: var(--ls-wide); margin-bottom: var(--sp-3);
}
.c360__row { display: grid; gap: var(--sp-2); }
.c360__cell { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); }
.c360__k { font-size: var(--fs-12); color: var(--ad-ink-2); }
.c360__v { font-size: var(--fs-14); font-weight: 600; color: var(--ad-ink); }
/* ★ 2026-09-24：免登录横幅。它是**常驻**的（不是出错才闪一下），挂在 .ad-main 里
   .ad-bar 与 #adBody 之间 —— 所以要自己带上边距，不然会贴着工具条。 */
.ad-keyoff { margin: var(--sp-5) var(--sp-7) 0; }

/* 保存状态 */
.ad-status { font-size: var(--fs-11); color: var(--ad-muted); letter-spacing: var(--ls-wide); }
.ad-status.is-dirty { color: var(--ad-warn-text); }
.ad-status.is-saved { color: var(--ad-ok-text); }

/* ---------------------------------------------------------------- 窄屏 */

/* ★ R7·Q5：原来的「窄屏侧栏」块（900px）**已整体搬走** ——
   它把 34 个菜单项 `grid-auto-flow: column` 横排成一条（中文逐字竖叠、项高 138、
   `.ad-nav` 高 1518–2548px），是 `<768` 崩掉的直接原因。
   现在这三档统一在新段（文件末尾「R7·Q5 · 响应式三档」）里写，**此处不留第二套**。 */

/* 引用多选（空间案例的「关联灯具」）：项目多时用网格排，别拉成一长条 */
.ad-checks {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: var(--sp-2) var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--ad-line);
  background: var(--ad-bg-alt);
}
.ad-check {
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-13); cursor: pointer;
}
.ad-check input { flex: none; accent-color: var(--ad-ink); }
.ad-check span { line-height: 1.4; }
/* 规格区的维度勾选：一列里项目多，排密一点 */
.ad-checks--sm { grid-template-columns: 1fr; gap: var(--sp-1); padding: var(--sp-2) var(--sp-3); }

/* ================================================================
   属性选择器（2026-09-25 · B20）
   把「N 项勾选墙」换成「已选 chips ＋ 选择（弹窗）」。
   ★ 原生 checkbox 留在 .ad-pick__box 里、**只是视觉隐藏**：
     收集器与既有判据读的都是它们（见 admin.js → dictMultiHTML 抬头）。
     用 clip 而不是 display:none —— 保留在无障碍树里，键盘也能过。
   ================================================================ */
.ad-pick { display: block; position: relative; }
.ad-pick__box {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* B21（2026-09-25 21:1x 余总「去掉按钮 直接点输入框就可以弹出」）：
   整行就是开关 —— 边框 / 底色 / 最小高度都挪到 .ad-pick__row 上，
   chips 退成纯内容，右边只留计数与一个 ▾（都是装饰、不参与点击）。
   行内唯一还能单独点的是 chip 上的 ×，所以 admin.js → bindPick 必须**先判 ×**。 */
.ad-pick__row {
  display: flex; align-items: center; gap: var(--sp-3);
  min-height: 38px; padding: 5px var(--sp-3);
  border: 1px solid var(--ad-line); background: var(--ad-bg-alt);
  cursor: pointer; transition: border-color var(--ad-dur-hover) var(--ease), background var(--ad-dur-hover) var(--ease);
}
.ad-pick__row:hover { border-color: var(--ad-line-2); background: var(--ad-bg); }
.ad-pick__row:focus-visible { outline: 2px solid var(--ad-accent); outline-offset: 1px; }
.ad-pick__chips { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.ad-pick__cnt { flex: none; color: var(--ad-ink-2); font-size: var(--fs-12); white-space: nowrap; }
.ad-pick__caret { flex: none; color: var(--ad-ink-2); font-size: var(--fs-12); line-height: 1; }
/* ★ B22：`.ad-pick--gen`（批量生成器那几列的紧凑样式）已随生成器一起删除 ——
   没有 DOM 会再用到它。同时删掉的还有下面「批量生成器：各维度并排成列」整段。 */
.ad-pick__none { color: var(--ad-ink-2); font-size: var(--fs-13); }
.ad-pick__meta { margin-top: 6px; color: var(--ad-ink-2); font-size: var(--fs-12); }

/* ★★★ R7·Q8（2026-10-01 15:2x 余总：「这5种全部统一」「大小全部统一为27PX」）——
   **小块控件「一套」的真源**：高 27px · 药丸 999px · 白底 · 1px `--ad-line-2` **单线**
   · 12px · 选中＝`--ad-ink` 实心＋白字。
   同一套几何另在两处就地写（判据守「解出的值一致」，见 `check_q8_ui.py`）：
     · `.ad-sertabs__item`（产品页顶「同系列」，~1377）
     · `.ad-versbar .ad-vers`（规格表上方版本 TAB，~1793）
   ★★ **为什么以前长得是「2px 方角 ＋ 双层描边」**：这个类名在文件里被写了**三套**
     （此处 / ~2125 筛选 chips / ~2970 列浮层）—— 同特异性「后到者胜」⇒ 2970 那套
     把这里的 `999px` 盖成 `--r-2`、**又叠一层 `box-shadow` 描边环** ⇒ 全站 chip 变双层方角。
     Q8 处置：2125 那套**并入本条**；2970 那套**加 `.ad-cols` 父作用域**
     （列浮层里是密集列名标签，不跟着变大）⇒ 撞车从根上拆掉。
   ★ **不带 ×** 的用 `.ad-chip--plain`（单选值 / 筛选 chip），左右都是 11px。 */
.ad-chip {
  display: inline-flex; align-items: center; gap: 6px;
  height: 27px; padding: 0 var(--sp-1) 0 11px; /* 默认＝带 ×（右侧收回给 18px 的 .ad-chip__x） */
  border-radius: var(--r-pill);
  border: 1px solid var(--ad-line-2); background: var(--ad-bg);
  font-size: var(--fs-12); color: var(--ad-ink);
  font-family: inherit; max-width: 100%;
}
/* ★★ 2026-10-03 SAGE 落地（批 S1）：选中态 **近黑底 → 橄榄底**。
   依据 = 样机 `.fchip.on{background:var(--olive);border-color:var(--olive);color:var(--paper-2)}`
   —— 逐字照抄。改前是 `--ad-ink`（#0E1405，近黑），不带橄榄色相，
   与顶栏的深橄榄 `--ad-ink` 撞成一片、且与侧栏激活态（橄榄底）口径不统一。
   ⇒ 全后台"选中/激活"从此统一为**橄榄底 + 暖纸字**（侧栏 `.is-on` / 本处 / 页签同族）。 */
.ad-chip.is-on { background: var(--ad-accent); border-color: var(--ad-accent); color: var(--ad-bg); }
.ad-chip__t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ad-chip__x {
  flex: none; width: 18px; height: 18px; line-height: 1;
  border: 0; border-radius: 50%; background: transparent;
  color: var(--ad-ink-2); font-size: var(--fs-14); cursor: pointer;
}
.ad-chip__x:hover { background: var(--ad-ink); color: var(--ad-bg); }
/* ★ B27（2026-09-26 12:xx）：**不带 ×** 的 chip（单选值 · 筛选 chip）左右对称 11px。
   ★ R7·Q8：宽度跟着统一规格（默认 `.ad-chip` 是「带 ×」＝右 4px，见上）。 */
.ad-chip--plain { padding: 0 11px; }

/* ---- 弹窗内部 ---- */
.ad-pickm { display: grid; gap: var(--sp-3); }
.ad-pickm__bar { display: flex; align-items: center; gap: var(--sp-3); }
.ad-pickm__bar .input { flex: 1; }
.ad-pickm__n { color: var(--ad-ink-2); font-size: var(--fs-12); white-space: nowrap; }
.ad-pickm__list {
  max-height: 46vh; overflow: auto;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 2px var(--sp-4); padding: var(--sp-3);
  border: 1px solid var(--ad-line); background: var(--ad-bg-alt);
}
.ad-pick__tag {
  font-style: normal; color: var(--ad-ink-2); font-size: var(--fs-11);
}
.ad-pickm__add {
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--ad-line-2); background: var(--ad-bg-alt);
}
.ad-pickm__addhd { font-size: var(--fs-12); color: var(--ad-ink-2); margin-bottom: var(--sp-2); }
.ad-pickm__addrow { display: flex; gap: var(--sp-3); }
.ad-pickm__addrow .input { flex: 1; }
/* B21：`vref` 源（版本档位）要一次填「标识 / 显示名 / 说明」三项 —— 窄屏就换行 */
/* ★ B22：`.ad-pickm__addrow--3`（选择弹窗里的「标识 / 显示名 / 说明」三输入行）
   随 vref 源自定义区一起删除 —— 版本档位改用专用弹窗（`.ad-verm`，见下）。 */
.ad-pickm__dimrow { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.ad-pickm__dimf { display: flex; align-items: center; gap: 6px; font-size: var(--fs-12); }
.ad-pickm__dimf .input { width: 92px; }
.ad-pickm__dimf--u .input { width: 58px; }
.ad-pickm__dimfoot {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); margin-top: var(--sp-3);
}
.ad-pickm__dimfoot span { font-size: var(--fs-12); color: var(--ad-ink-2); }
.ad-pickm__dimfoot b { color: var(--ad-ink); font-size: var(--fs-14); font-weight: var(--fw-medium); }
.ad-pickm__note { margin-top: 6px; font-size: var(--fs-12); color: var(--ad-ink-2); }
.ad-pickm__note:empty { display: none; }
/* ★★ 批 G17（2026-10-05）：**校验红字统一走错误红**（余总定「字段下红字」）。
   改前这里是 `--ad-accent`（橄榄）—— 与 `.ad-fillm__note.is-bad`（红）**同族不同色**，
   同一句"必填"在两种弹层里长得不一样。既然口径定了红字，就一处都不留橄榄。 */
.ad-pickm__note.is-bad { color: var(--ad-err-text); }

/* ★ B27（2026-09-26 11:xx）：这里原来是 B22 的「版本档位弹窗」（`.ad-verm`）样式 ——
   入口按钮已随 B24 的字典化永不渲染（schema 全站无 `localRef`），整族样式一并删掉。
   版本值的增删改移现在在「字典 → 版本」页做，那边本来就有四件套的样式。 */

/* ---- B27 · 新建产品弹窗（`.ad-npd`）-------------------------------------
   入口 = 产品列表底部那颗「＋ 新建产品」。它把**三样一级概念**一次问清：
   型号（必填 + 当场查重）/ 版本（字典多选，可现场新增）/ 品类（字典多选）。
   ★ 候选框里"这本字典还没有值"那句直接复用 `.ad-pick__none` —— 不再造第二套空状态。 */
.modal__panel--npd { width: min(560px, 100%); }
.ad-npd__grp { margin-top: var(--sp-4); }
.ad-npd__lb { font-weight: var(--fw-medium); color: var(--ad-ink); margin-bottom: 2px; }
.ad-npd__req { color: var(--ad-accent); }
.ad-npd__msg { margin-top: var(--sp-1); font-size: var(--fs-12); color: var(--ad-ink-2); }
.ad-npd__msg:empty { display: none; }
.ad-npd__msg.is-bad { color: var(--ad-err-text); }
.ad-npd__opts {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
  margin-top: var(--sp-2); padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--ad-line-2); background: var(--ad-bg-alt);
}
.ad-npd__opt {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-12); color: var(--ad-ink-2); cursor: pointer;
}
.ad-npd__add { display: flex; gap: var(--sp-2); margin-top: var(--sp-2); }
.ad-npd__add .input { flex: 1; }
.ad-npd__foot {
  display: flex; justify-content: flex-end; gap: var(--sp-3);
  margin-top: var(--sp-5); padding-top: var(--sp-4);
  border-top: 1px solid var(--ad-line);
}


/* ================================================================
   规格编辑区（2026-09-21 v9 阶段 2.x 新增）
   SKU 角标 / 组合键重复标红 / 批量生成器。
   配色沿用后台既有变量，不引新色。
   ================================================================ */

/* 规格条头上的 SKU 文本：等宽，一眼能看出「型号-版本-尺寸-颜色」的拼法 */
.ad-sku {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--fs-12);
  letter-spacing: 0;
  padding: 2px var(--sp-2);
  border-radius: var(--r-pill);
  background: var(--ad-bg-alt);
  border: 1px solid var(--ad-line);
  color: var(--ad-ink-2);
  text-transform: none;
}
/* ★ B22：SKU **还没算出来**（一个维度都没选）—— 用虚线边 + 灰字，与「已算好」在视觉上分开。
   文案由 admin.js → specSkuText 决定（固定一句「— 选参数即自动生成 —」），
   这里只管长相：一眼扫下去，**没算出来的行不会伪装成算好了**。
   ★ 注意与 `.ad-sku__miss` 的分工：「还缺什么」**不并进这句文案**，见下。 */
.ad-sku--todo {
  background: transparent;
  border-style: dashed;
  color: var(--ad-ink-2);
  font-family: inherit;
}
.ad-sku.is-dup {
  background: var(--ad-err-bg);
  border-color: var(--ad-err-text);
  color: var(--ad-err-text);
  font-weight: 500;
}
/* 「+ 添加一条」那一行的容器：左边一句计数/指路，右边按钮。
   ★ B22 新加 —— 那句话要能被 syncSpecTable 原地改（`[data-spec-count]`），
     所以按钮与文案得**分成两个兄弟节点**，不能像以前那样挤在按钮文案里。 */
.ad-spec-foot {
  display: flex; align-items: center; gap: var(--sp-4);
  margin-top: var(--sp-3);
}
.ad-spec-foot .ad-f__hint { flex: 1; }
.ad-spec-foot .ad-add { width: auto; padding: var(--sp-3) var(--sp-5); flex: none; }
/* 刚「+ 添加一条」出来的那一行闪一下 —— 表尾几十行时，这是「点了有反应」的唯一线索 */
.sp-row.is-new td { animation: ad-newrow 1.2s ease-out; }
@keyframes ad-newrow {
  from { background: var(--ad-warn-bg); }
  to { background: transparent; }
}
.ad-sku__flag {
  font-size: var(--fs-11);
  color: var(--ad-err-text);
  letter-spacing: 0;
}
/* ★ B22：「这一行还缺哪几个维度」—— 和 SKU 文本**并排**显示，不互相顶替。
   为什么不把它并进 SKU 文本：那会变成二选一 —— 要么把「已选好的」藏起来，
   要么把「没选全」说成已算好。拆成两块之后，两个事实同时成立、互不遮蔽。
   hidden 由 admin.js → syncSpecTable 与首次渲染共同定夺。 */
.ad-sku__miss {
  margin-left: 6px;
  font-size: var(--fs-11);
  color: var(--ad-ink-2);
  letter-spacing: 0;
}
/* 整条规格带红边：重复的规格在长列表里也要一眼认出来，不能只靠一个小角标 */
.ad-item.is-dup { border-color: var(--ad-err-text); background: var(--ad-err-bg); }

/* ★ B22：「批量生成器：各维度并排成列」整段（`.ad-gen` / `.ad-gen__col` / `.ad-gen__hd` /
   `.ad-gen__col .ad-checks` / `.ad-gen__out` / `.ad-gen__list`）已删除 ——
   生成器整块按余总要求下线，这些选择器在 DOM 里再也找不到落点。
   ⚠️ 判据里若还有 `.ad-gen__list code` 这类断言，说明它在守一条已经不存在的东西 —— 一并改掉。 */

/* 字典页的排序按钮组排成一行不换行 */
.ad-table__actions .mini + .mini { margin-left: 2px; }

/* ================================================================
   登录页（2026-09-20 鉴权轮新增）
   口令没通过时，整个后台只显示这一块 —— 不拉数据、不画侧栏。
   样式跟后台同一套变量，不引新色。
   ================================================================ */
.ad-login {
  display: flex; align-items: flex-start; justify-content: center;
  padding: 12vh var(--sp-5) var(--sp-5);
  min-height: 60vh;
}
.ad-login__card {
  width: 100%; max-width: 420px;
  padding: var(--sp-5);
  border: 1px solid var(--ad-line);
  background: var(--ad-bg-alt);
}
.ad-login__brand {
  font-size: var(--fs-18); font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide);
  margin-bottom: var(--sp-3);
}
.ad-login__tip {
  font-size: var(--fs-13); line-height: var(--lh-normal);
  color: var(--ad-muted); margin: 0 0 var(--sp-4);
}
.ad-login__tip code {
  font-size: var(--fs-12);
  padding: 1px var(--sp-1);
  background: var(--ad-bg-sunk);
  border: 1px solid var(--ad-line-2);
}
.ad-login__form {
  display: flex; gap: var(--sp-2); align-items: stretch;
  margin-top: var(--sp-3);
}
.ad-login__input {
  flex: 1; min-width: 0;
  padding: 9px var(--sp-3);
  font: inherit; font-size: var(--fs-13);
  color: var(--ad-ink);
  background: var(--ad-bg);
  /* ★ AA 1.4.11：表单控件的边界要 3:1 —— 发丝线（1.2）算不出「框在哪」 */
  border: 1px solid var(--ad-field-line);
  border-radius: var(--r-2);
}
.ad-login__input:focus {
  outline: none;
  border-color: var(--ad-ink);
}
.ad-login__card .ad-note { margin: var(--sp-3) 0 0; }

@media (max-width: 767px) {
  .ad-login { padding: var(--sp-5) var(--sp-4); align-items: stretch; }
  .ad-login__form { flex-direction: column; }
}

/* ================================================================ Excel 导入导出
   两块：① 站点/导航这类手写视图顶部的工具条（.ad-iobar）
        ② 导入预览弹层里的内容（.ad-io__*），骨架复用 .modal__panel */

.ad-iobar {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--ad-bg-sunk);
  border: 1px solid var(--ad-line-2);
  border-radius: var(--r-2);
}
.ad-iobar__hint { font-size: var(--fs-12); color: var(--ad-muted-ink); }

.ad-io__body { display: grid; gap: var(--sp-3); }
.ad-io__body .ad-kv {
  font-size: var(--fs-13); color: var(--ad-ink-2);
  padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--ad-line-2);
}
.ad-io__body .ad-kv b { font-size: var(--fs-15); color: var(--ad-ink); margin-right: var(--sp-1); }
.ad-io__cap { font-size: var(--fs-12); color: var(--ad-muted-ink); margin-bottom: var(--sp-2); }
.ad-io__sample, .ad-io__warn, .ad-io__note {
  border: 1px solid var(--ad-line-2);
  border-radius: var(--r-2);
  padding: var(--sp-3) var(--sp-4);
}
.ad-io__sample ul, .ad-io__warn ul, .ad-io__note ul { margin: 0; padding-left: 1.2em; }
.ad-io__sample li, .ad-io__warn li, .ad-io__note li {
  font-size: var(--fs-13); color: var(--ad-ink-2); line-height: 1.6;
}
.ad-io__warn { background: var(--ad-warn-bg); border-color: var(--ad-warn-line); }
.ad-io__note { background: var(--ad-bg-sunk); }
.ad-io__note { font-size: var(--fs-13); color: var(--ad-ink-2); }
/* ★ R7·Q4：A 族里唯一自带底栏的一档（导入预览）—— 底栏高吃 `--ad-dlg-foot-h`。
   ★ 就地写死值，**不再在 R7 段另立重复规则**（同选择器两条 = 判据会命中旧的、看着像没改）。 */
.ad-io__foot {
  display: flex; justify-content: flex-end; gap: var(--sp-3);
  align-items: center; height: var(--ad-dlg-foot-h); margin-top: var(--sp-4);
}
.modal__panel--io { width: min(840px, 100%); }
/* 属性选择器弹窗：要放下「两列候选 + 自定义行」，比默认 720px 宽一点。
   ★ R7·Q4：上限从 980 收到规范值 900（= `--ad-dlg-max-w`）。 */
.modal__panel--pick { width: min(var(--ad-dlg-max-w), 100%); }
/* B22 · 版本档位弹窗：五列（标识 / 显示名 / 说明 / 引用 / 删除），
   默认 720px 减去内边距只剩 624 会给「显示名 + 说明」挤成两行 —— 加宽。 */
/* ★ B27：`.modal__panel--ver`（B22 版本档位弹窗的宽度档）已随整族删除。 */
/* ★ B23 · 规格表里那两个小弹窗（填空 / 包装尺寸）：内容就是两三个输入框，
   用默认 720px 会显得空荡、视线还得横着找。
   ★ B23 二改（余总 2026-09-26 01:1x：「**这些弹窗都太大了**」）—— 从 560px 收到 400px，
     连内边距一起收：`.modal__panel` 的 `--sp-7`（48px）是按"整页级弹窗"定的，
     装这种"就地补一格"的小弹窗，光四边留白就吃掉 96px。 */
.modal__panel--sm { width: min(400px, 100%); padding: var(--sp-5); }
.modal__panel--sm .modal__head { margin-bottom: var(--sp-4); }

/* ★★ 批 J3（2026-10-05）：通用弹窗的**底部按钮条** —— 这一类一直**只挂了类名、没有样式**
   （全项目 `grep` `.ad-fmfoot` 在 CSS 里零定义）⇒ 两个按钮按默认文档流排在**左缘**、
   贴着最后一个输入框，看着像"忘了排版"（截图目视逮到）。
   补上：右对齐 ＋ 上间距 —— 与已有 `.ad-io__foot`（导入预览底栏）**同口径**。 */
.ad-fmfoot {
  display: flex; justify-content: flex-end; align-items: center;
  gap: var(--sp-3); margin-top: var(--sp-5);
}

/* ==========================================================================
   ★★ R7 · Q4：三族弹窗量化（A `.modal` / B `.ad-cf` / C `.ad-picker`）
   --------------------------------------------------------------------------
   规范值 = 圆角 8 · 阴影 `0 8px 24px rgba(0,0,0,.12)` · 遮罩 60% ·
            标题区 56 · 底栏 64 · 宽 ≤900 · 关闭图标 16。令牌见 `tokens.css --ad-dlg-*`。

   ★★ **A 族为什么在这里覆盖写、而不是改源头**：
      `.modal` / `.modal__panel` 定义在 **`components.css`（前后台共用层）**
      ⇒ 改源头会把**前台弹窗**一起改掉。所以源头一个字不动，全在这里覆盖。
      ⚠️ 这是本批唯一踩在共用层上的改动，`check_q4_ui.py [X10]` 专门守它。

   ★★ **`padding: 0 24 24`（原 48）怎么来的**：标题区要变成"独立 56 高的一条"，
      就不能再让面板的四边留白包着标题 —— 上下留白归标题区自己管。
      左右仍 24（与规范「标题区左右 padding 24」一致）。
   ========================================================================== */

/* ---- A 族：`.modal`（通用模态：属性选择器 / 导入预览 / 规格填空 / 型号）---- */
/* ★ 批 NAV-1（2026-10-04）：弹窗**入场过渡 300ms** —— 遮罩淡入 + 面板上滑同为这一档。
   源头 `.modal` / `.modal__panel` 的 transition 在 components.css（共用层，一个字不动），
   这两条是 admin 层覆盖。★ 时长写字面 300ms：项目令牌里没有 300ms 档
   （--ad-dur-hover 100 / --ad-dur-panel 320），本批要的就是 300ms ⇒ 按"没有合适的令牌
   再写字面值"处理；曲线仍引用令牌 --ad-ease-out。 */
.modal { background: var(--ad-dlg-scrim); transition: opacity 300ms var(--ad-ease-out); }
.modal__panel,
.modal__panel--sm,
.modal__panel--io,
.modal__panel--npd,
.modal__panel--pick {
  border-radius: var(--ad-dlg-r);
  box-shadow: var(--ad-dlg-sh);
  padding: 0 var(--sp-5) var(--sp-5);
  transition: transform 300ms var(--ad-ease-out);
}
.modal__head {
  min-height: var(--ad-dlg-head-h);
  /* ★ 原 `align-items: flex-start` 是给"标题很长会折行"定的；收成 56 高的一条后
     要改 `center`，否则标题贴在条顶、关图标浮在条中，看着像没对齐。 */
  align-items: center;
  margin-bottom: var(--sp-4);
}
/* ★ 宽上限（`--pick` → 900）与 A 族底栏高（`.ad-io__foot` → 64）**就地改在原规则上**
   （见上方 `.modal__panel--pick` / `.ad-io__foot`）—— 这里不再另立重复规则。
   ⚠️ 原因：同选择器出现两条时，`re.search` 会命中**先出现的那条**，
      判据读到的就是旧值 ⇒ 表现成「明明改了却报没改」。 */

/* ---- 弹窗关闭图标（三族共用一枚 16px 内联 SVG「×」）-----------------------
   ★ 规范要素：关闭图标 16px、位置 = 标题区右侧。
   ★ 点击热区仍给 24×24（16 的图标直接当按钮太小，鼠标够不着）——
     「图标视觉 16 / 热区 24」是两件事，判据守的是 **SVG 的 16**。
   ★ 依赖内联 SVG（铁律④：图标只许内联 SVG 且线宽统一）。 */
.ad-dlgx {
  flex: 0 0 auto;
  width: 24px; height: 24px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: transparent; color: var(--ad-muted-ink);
  border-radius: var(--r-2); cursor: pointer;
  transition: background var(--tr), color var(--tr);
}
.ad-dlgx:hover { background: var(--ad-bg-alt); color: var(--ad-ink); }
.ad-dlgx svg { width: 16px; height: 16px; display: block; }

/* ---- B23 · 填空弹窗 / 包装尺寸弹窗 -------------------------------------
   两个弹窗共用一套骨架：左边一个固定宽度的标签、右边一个占满的控件。
   骨架只有这一份 —— 以后再有小弹窗直接套，不再各写各的间距。 */
.ad-fillm, .ad-packm { display: grid; gap: var(--sp-3); }
.ad-fillm__row, .ad-packm__row { display: flex; align-items: center; gap: var(--sp-3); }
.ad-fillm__k, .ad-packm__k {
  flex: 0 0 76px; color: var(--ad-ink-2); font-size: var(--fs-12);
}
.ad-fillm__row .select, .ad-fillm__row .input, .ad-packm__row .input { flex: 1; min-width: 0; }
/* ★ B23 二改：包装的三个数**并成一行**（长｜宽｜高），
   单位统一挪到下面的说明里 —— 每格尾巴挂一个 `mm` 在 400px 里挤不开。 */
.ad-packm__row3 { display: flex; align-items: center; gap: var(--sp-3); }
.ad-packm__c { flex: 1 1 0; min-width: 0; display: flex; align-items: center; gap: var(--sp-2); }
.ad-packm__c .ad-packm__k { flex: 0 0 auto; }
.ad-packm__c .input { flex: 1; min-width: 0; }
.ad-fillm__note { min-height: 17px; font-size: var(--fs-12); color: var(--ad-ink-2); }
.ad-fillm__note.is-bad { color: var(--ad-err-text); }
.ad-fillm__ops { display: flex; justify-content: flex-end; }

/* ---- 字典：类型 / 归档（v9 阶段 2.6）-----------------------------------
   色卡列：有 hex 画色块、有图显示缩略图、都没有就留虚线空框（提示「这里可以填」）。 */
.ad-swatch {
  display: inline-block; width: 26px; height: 26px;
  border-radius: 3px; border: 1px solid var(--ad-line-2);
  object-fit: cover; vertical-align: middle; background: var(--ad-bg-sunk);
}
.ad-swatch--empty { border-style: dashed; background: transparent; }
/* 已停用 / 已归档的字典行：整行降调，一眼能和「使用中」的分开 */
.ad-row--off td { color: var(--ad-muted-ink); background: var(--ad-bg-sunk); }
/* ★★ 2026-10-03 SAGE 严格对齐（第二轮·悬停收口）：
   改前 `.ad-row--off:hover td` = `--ad-bg-alt` = #F0F1E9 —— 而这条**禁用行**的静态基色
   是 `--ad-bg-sunk` = #E9EAE2（比它更深）⇒ ★ **悬停时行反而变浅**，方向与全站其余行相反。
   （这是 2026-10-03 之前就存在的隐性 bug：当时页底色也是 #F0F1E9、无人注意；本轮右栏
     底色改为 #F0F1E9 后才显形。）
   口径（余总 2026-10-03 标注「鼠标划过色 #e9eae2」）：**悬停一律「下沉」**。
   本行基色已是 sunk ⇒ 悬停再深一档走 `--ad-zebra`（同值 #E9EAE2，但语义是「行底纹」；
   ⚠️ 二者当前同色，视觉上等价 ⇒ 实际做法是**沿用 sunk**、靠 `td` 的静态/悬停对比由
   `.ad-row--off` 文字灰化承担。若将来 `--ad-zebra` 与 `--ad-bg-sunk` 分家，本行自动跟随。 */
.ad-row--off:hover td { background: var(--ad-bg-sunk); }
/* ★ 2026-10-04（批 T4 · 余总第 3 次更正）：恢复 SAGE 换皮原状（这条**不是**单号列，
   余总「整行不变」⇒ 不参与玫红/加粗）。 */


/* ================================================================ v23 · 系列成员管理 + 同系列跳转标签 */
.ad-ser__bar { gap: var(--sp-3); flex-wrap: wrap; }
.ad-ser__bar .select { max-width: 280px; }
/* ★ R7 · Q2（B10）：原 `min-height: 30px` 是个**页面级补丁**，把系列条按钮压到 30
   —— 全后台就它一处跳出规范。改成**紧凑档 `--ad-btn-h-sm`（32）**，纳入双档，
   不再是一个孤立值。（判据 `check_q2_ui.py` V5 扫的就是这类孤立高度。） */
.ad-ser__bar .btn { min-height: var(--ad-btn-h-sm); }
.ad-ser__no {
  min-width: 16px; color: var(--ad-ink-2);
  font-variant-numeric: tabular-nums; font-size: var(--fs-12);
}
.ad-ser__sku {
  min-width: 84px; color: var(--ad-ink-2);
  font-variant-numeric: tabular-nums; font-weight: 500;
}
.ad-ser__name { color: var(--ad-ink); }
.ad-ser__meta { font-size: var(--fs-12); color: var(--ad-ink-2); }

.ad-sertabs {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-4);
  border: 1px solid var(--ad-line);
  border-radius: var(--r-2);
  background: var(--ad-bg-alt);
  font-size: var(--fs-12);
}
.ad-sertabs__label { color: var(--ad-ink-2); margin-right: var(--sp-1); }
/* ★ R7·Q8：与通用 `.ad-chip`（~986）**同一套几何**：高 27 · 药丸 999px · 白底
   · 1px `--ad-line-2` · 12px · 选中＝`--ad-ink` 实心＋白字。
   它坐在灰底 `.ad-sertabs`（`--ad-bg-alt`）上 ⇒ 白药丸会**浮出来**（Q8 预期观感）。 */
.ad-sertabs__item {
  display: inline-flex; align-items: center; gap: 6px;
  height: 27px; padding: 0 11px; border-radius: var(--r-pill);
  border: 1px solid var(--ad-line-2); background: var(--ad-bg);
  color: var(--ad-ink); text-decoration: none; font-size: var(--fs-12);
}
a.ad-sertabs__item:hover { border-color: var(--ad-ink); }
.ad-sertabs__item.is-on {
  background: var(--ad-ink); border-color: var(--ad-ink); color: var(--ad-bg);
}
.ad-sertabs__hint { margin-left: auto; font-size: var(--fs-12); color: var(--ad-ink-2); }

/* ---- ★ B17（2026-09-25）：表单 Tab 页签（产品编辑页；归属来自 schema.productFormTabs）
   切页签只切 DOM、不重绘 ⇒ 这里只管「看起来像页签」，不带任何动画 / 重排。 */
/* ★★★ 2026-10-03 SAGE 落地（批 S2-A）：形态由「凸起式」改为样机 `.stab` 的**「下划线式」**。
   依据 = `后台主题_SAGE终端风.html` 逐字真值：
     .settings-tabs { display:flex; gap:var(--sp-5); border-bottom:1px solid var(--line-2);
                      margin-bottom:var(--sp-5) }
     .stab { font-size:var(--fs-13); color:var(--muted); padding:0 0 var(--sp-3);
             border:0; border-bottom:2px solid transparent; background:none; cursor:pointer;
             letter-spacing:var(--ls-wide); text-transform:uppercase;
             transition:color var(--dur-hover) var(--ease) }
     .stab:hover { color:var(--ink-2) }
     .stab.on { color:var(--ink); font-weight:var(--fw-medium); border-bottom-color:var(--olive) }
   ★ 改前（批 S1 留）是"凸起式"：上圆角 + 左右边框 + `--ad-bg-alt` 底色 + `gap:2px`；
     样机是"无色块、只有 2px 底线"⇒ 本次**去掉** 边框 / 圆角 / 底色，`gap` 放宽到 `--sp-5`。
   ★ 选中态**沿用 `box-shadow: inset 0 -2px 0`**（批 S1 写法）而不换成 `border-bottom`：
     渲染等价、且 `inset` 阴影不参与布局 ⇒ **不改 padding、不会撑高页签条**，
     对 `.ad-ptop`（L2657 产品编辑页顶栏）里的页签组**零位移影响**。 */
.ad-tabs {
  display: flex; align-items: flex-end; flex-wrap: wrap; gap: var(--sp-5);
  border-bottom: 1px solid var(--ad-line-2);
  margin-bottom: var(--sp-5);
}
.ad-tabs__item {
  padding: 0 0 var(--sp-3);
  border: 0; border-radius: 0;
  background: none; color: var(--ad-muted);
  font: inherit; font-size: var(--fs-13); line-height: 1.5;
  letter-spacing: var(--ls-wide); text-transform: uppercase;
  cursor: pointer;
  transition: color var(--ad-dur-hover) var(--ad-ease-out);
}
.ad-tabs__item:hover { color: var(--ad-ink-2); }
.ad-tabs__item.is-on {
  background: none; border-color: transparent;
  box-shadow: inset 0 -2px 0 var(--ad-accent);
  color: var(--ad-ink); font-weight: var(--fw-medium);
}
.ad-tabpanel[hidden] { display: none; }

/* ★★★ G5（2026-09-27）：Tab **面板自身**要有 `grid + gap`。
   ★ 实测逮到的既有缺陷：改前 `.ad-tabpanel` 是 `display:block`（gap normal）⇒
     面板**内**相邻的两个 `.ad-fieldset`（分组卡片）间距 = **0px**，卡片阴影直接叠一起
     （产品编辑页「基本」页签有 5 张卡、内容页签 11 张，全是 0px）。
     根因：卡片间距原来由 `.ad-form` 的 `gap:var(--sp-6)` 提供，而分组卡片一旦被
     塞进 `.ad-tabpanel` 就不是 `.ad-form` 的直接子元素了 ⇒ **间距静默丢掉，不报错**。
   ★ 这里用同一个 `var(--sp-6)` —— 与 `.ad-form` 一致，**不新造第二套间距**。 */
.ad-tabpanel { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-6); }


/* ================================================================ ★★★ 2026-10-03 批 S2-B
   列表页骨架：`list-head`（计数条）＋ `list-foot`（分页行）。
   ★ 依据 = 样机 `后台主题_SAGE终端风.html`：
       .list-head { display:flex; align-items:center; gap:var(--sp-3); padding:var(--sp-4) }
       .list-head .tt { font-size:var(--fs-13); color:var(--ink-2) }   ← 计数文案
       .list-foot { display:flex; align-items:center; gap:var(--sp-3);
                    padding:var(--sp-3) var(--sp-4); border-top:1px solid var(--line-1);
                    font-size:var(--fs-12); color:var(--muted) }
   ★★ 为什么不新造分页逻辑：单据页**早就有** `.ad-pager`（CSS 全套 + `docPagerInner()` 生成器
      + `[data-d-count-txt]` 计数口）⇒ 本批**只把口径统一过来**，`list-foot` 直接复用
      `.ad-pager` 的既有样式链，**不写第二套分页**。
   ★ 为什么加 `.ad-list-head` 而不是直接用样机的 `.list-head`：
     `base.css` / `components.css` 是**前后台共用层**，`.list-*` 这种泛名有撞车风险
     （本项目史上已因 `.ad-tabs` 撞车吃过一次血案）⇒ 统一加 `ad-` 前缀、限定此后台页面。
     ⚠️ 这两个类**当前尚无 JS 产出**（本批只做样式/留空占位，接数据属后续批次）。 */

/* ① 计数条：坐在 `.ad-panel__head` 里，替换原来那句内联 `<span>共 N …</span>`。
   口径与 `.ad-panel__head` 对齐（同字号 / 同字距），只把「计数」这一格独立出来。 */
.ad-list-head { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
.ad-list-head__tt {
  font-size: var(--fs-13); color: var(--ad-ink-2);
  letter-spacing: var(--ls-wide); white-space: nowrap;
}
.ad-list-head__tt b { color: var(--ad-ink); font-weight: var(--fw-medium); }
/* ★ 警示后缀（如「3 条偏低/待补货」）：原先内联 `style="color:var(--ad-err-text)"`，
   归口后收成这一个类 —— 语义不变、写法统一。
   ★★ 2026-10-03 走查逮到的**真 bug**：`warn` 是 `<b>` 且在 `.ad-list-head__tt` 之内，
      而被 `.ad-list-head__tt b { color: var(--ad-ink) }`（权重 0,2,1）**压过**本类（0,1,0）
      ⇒ 实测渲染成黑 `rgb(14,20,5)`，红色告警**静默消失**（不报错、只有肉眼/判据能发现）。
      修法 = 把选择器写成 `.ad-list-head__tt .ad-list-head__warn`（0,2,0 仍不够）
      ⇒ 直接 **`b.ad-list-head__warn`（0,1,1）+ 后代化 `.ad-list-head__tt b.ad-list-head__warn`（0,2,1）**
         与上面那条**同权重、后出现者胜** ⇒ 稳。 */
.ad-list-head__tt .ad-list-head__warn,
.ad-list-head__tt b.ad-list-head__warn {
  color: var(--ad-err-text); font-weight: var(--fw-medium);
  margin-left: var(--sp-2);
}

/* ② 分页行：坐在 `.ad-panel` 底部（表格之后）。
   ★★★ 2026-10-03 自查修正（**差点造出重复类**）：
     本项目**早就有 `.ad-pag`**（客户列表页脚，见下方「客户」段），其样式与本类**逐字等价**：
       `.ad-pag { display:flex; align-items:center; gap:var(--sp-3);
                  padding:var(--sp-3) var(--sp-5); border-top:1px solid var(--ad-line);
                  font-size:var(--fs-12); color:var(--ad-ink-2); flex-wrap:wrap }`
     ⇒ 按本项目铁律「**加类前先 grep**」（`.ad-tabs` 撞车血案的教训），
       这里**不再新造第二套**，`.ad-list-foot` 只作 `.ad-pag` 的**语义别名**：
       两个选择器共用同一组声明，谁的页面用哪个都不出错。
     ⚠️ 唯一的差：`.ad-pag` 是 `color:var(--ad-ink-2)`，本类别名沿用同值（不再写 `--ad-muted-ink`，
        免出「同一位置两种灰」）。 */
.ad-list-foot,
.ad-pag-list {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-5);
  border-top: 1px solid var(--ad-line);
  font-size: var(--fs-12); color: var(--ad-ink-2);
  flex-wrap: wrap;
}
.ad-list-foot__sp { flex: 1; }
.ad-list-foot__pg { display: inline-flex; gap: 2px; }
.ad-list-foot__pg button {
  min-width: var(--ad-field-h-dense); height: var(--ad-field-h-dense);
  padding: 0 var(--sp-2); cursor: pointer;
  font: inherit; font-size: var(--fs-13); color: var(--ad-ink-2);
  border: 1px solid var(--ad-line); border-radius: var(--ad-r-field);
  background: var(--ad-bg);
}
.ad-list-foot__pg button:hover { border-color: var(--ad-ink); color: var(--ad-ink); }
.ad-list-foot__pg button.is-on {
  background: var(--ad-ink); border-color: var(--ad-ink); color: var(--ad-bg);
}
.ad-list-foot__pg button:disabled { opacity: .45; cursor: default; }
.ad-list-foot__pg button:focus-visible {
  outline: 2px solid var(--ad-accent); outline-offset: 1px; border-color: var(--ad-accent);
}
/* ★★ 批 E2E-2（2026-10-06）：页脚的**每页条数**控件（真分页的一半 —— 另一半是页码）。
   高度与页码按钮同档（`--ad-field-h-dense`），否则一行里高高低低。 */
.ad-list-foot__size {
  width: auto; height: var(--ad-field-h-dense);
  padding: 0 var(--sp-2); font-size: var(--fs-12);
}
/* 接不上表时的页脚（`is-off`）——只留「共 N 条」，不摆点了没反应的页码。 */
.ad-list-foot.is-off .ad-list-foot__pg { display: none; }

/* ★★ 批 E2E-2：**列头排序**（点一下 升 → 降 → 还原）。
   ★ 只有 `adHeadOf` 产出的数据列表头带 `data-sort`（操作列不带）——
     挂上去点不动的那种"哑按钮"是本项目反复咬到的一族。 */
thead th[data-sort] { cursor: pointer; user-select: none; }
thead th[data-sort]:hover { color: var(--ad-ink); }
.ad-ths {
  display: inline-flex; vertical-align: -2px; margin-left: var(--sp-1);
  color: var(--ad-line);            /* 未排序：低调到近乎装饰（别抢标题的字） */
}
thead th.is-sort .ad-ths { color: var(--ad-ink); }
/* 单箭头高亮：升序亮上箭头、降序亮下箭头（另一个压到 25%）。 */
thead th.is-asc .ad-ths__dn,
thead th.is-desc .ad-ths__up { opacity: .25; }

/* ★★ 批 E2E-2：**负值着色**（库存 / 报表列表）。
   ★ 口径：负数是**要你看一眼**的信号（超卖 / 盘亏），一律走 `--ad-err-text`；
     正常值一个字都不动 —— 全表染色＝没有强调。

   ⚠️⚠️ **血案（2026-10-06，靠判据升级才抓出来）**：这里原来只有一行 `.ad-num-neg`，
   类名**加对了**、颜色**一点没变** ——
     · `.ad-table .num { color: var(--ad-ink-2) }`（第 657 行）特异性 **0,2,0**
     · `.ad-table.ad-p3sub .num { … }`（第 5145 行）特异性 **0,3,0**
     · 而 `.ad-num-neg` 只有 **0,1,0** ⇒ 两条都压得住它 ⇒ 计算色仍是 `#3E4A30`。
   ★ 之所以老判据报 PASS 而新判据报 FAIL：老判据只 grep「有没有那个类名」（**只守 CSS 字面**），
     新判据（UI 侧 `W04`）读的是 `getComputedStyle(td).color` 并**与正常格比对**
     —— 「值算对了 ≠ 渲染对了」。
   ⇒ 所以这一组选择器**故意写长**：每条都要在特异性上压过对应的 `.num` 规则，
     少写一条就有一张表悄悄不生效（p3sub 那张表就中过招）。 */
.ad-num-neg,
.ad-table .num.ad-num-neg,
.ad-table.ad-p3sub .num.ad-num-neg,
table.ad-lines td.num.ad-num-neg { color: var(--ad-err-text); }


/* ================================================================ ★★★ 2026-10-03 批 S2-C
   报表页骨架：`report-kpi`（指标卡条）＋ `report-chart`（图表块）。
   ★ 依据 = 样机 `后台主题_SAGE终端风.html`：
       .report-kpi { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
                     gap:var(--sp-3); margin-bottom:var(--sp-5) }
       .report-kpi .k { background:var(--paper-2); border:1px solid var(--line-1);
                        border-radius:var(--r-card); padding:var(--sp-4) }
       .report-kpi .k .v { font-family:var(--font-mono); font-size:var(--fs-20); color:var(--ink) }
       .report-kpi .k .l { font-size:var(--fs-11); color:var(--muted);
                           letter-spacing:var(--ls-wide); text-transform:uppercase }
       .report-chart { background:var(--paper-2); border:1px solid var(--line-1);
                       border-radius:var(--r-card); padding:var(--sp-5); margin-bottom:var(--sp-5) }
   ★★ 与既有 `sage-kpis` / `sage-kpi` 的关系（余总 2026-10-03 拍板：**新增平行类**）：
      `sage-*` 是 SAGE 批给**概览页**造的一组（`admin.css` L4230+），命名带批次烙印；
      本批照样机原名立 `report-*`，**视觉口径从 `sage-kpi` 移植**（同一套 --ad-* 令牌 ⇒ 观感一致）。
      ⚠️ 两套**并存、不互相改名** —— 改 `sage-*` 会牵动概览页 + 180 行，回归面不值当。
   ⚠️ 同样**尚无 JS 产出**（本批只做样式/留空占位）。 */

/* ① 指标卡条：自适应列数（`auto-fit`），窄屏自动折行。
   ★ 口径**逐项对齐既有 `sage-kpi`**（同一套观感、同一批令牌），只改类名前缀。 */
.report-kpi {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--sp-3); margin-top: var(--sp-4);
}
.report-kpi__k {
  background: var(--ad-bg); border: 1px solid var(--ad-line);
  border-radius: var(--ad-r-data); padding: var(--sp-4);
  display: flex; flex-direction: column; gap: var(--sp-1);
}
.report-kpi__l {
  font-size: var(--fs-11); font-weight: 400; color: var(--ad-muted);
  letter-spacing: .02em;
}
.report-kpi__v {
  /* ⚠️ `--fs-20` **不存在**（本项目字号令牌只到 --fs-18 / --fs-22）。
     这里**照抄 `sage-kpi__v` 的既有写法 `var(--sp-5)`**（= 24px）——
     与该组件同值，避免"两套 KPI 数字不一样大"。 */
  font-size: var(--sp-5); font-weight: 500; color: var(--ad-ink);
  font-family: var(--font-mono); font-variant-numeric: tabular-nums; line-height: 1.2;
}
/* 环比色点：色**只做圆点**，文字保持 `--ad-ink-2`（同 `sage-kpi__c` 口径）。
   ⚠️ 默认灰点；涨跌方向由使用方加 `--up` / `--down` 决定 —— **CSS 不猜涨跌色**。 */
.report-kpi__d {
  font-size: var(--fs-11); color: var(--ad-ink-2);
  display: flex; align-items: center; gap: var(--sp-1);
}
.report-kpi__d::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%;
  background: var(--ad-muted); flex-shrink: 0;
}
.report-kpi--up   .report-kpi__d::before { background: var(--ad-err); }
.report-kpi--down .report-kpi__d::before { background: var(--ad-ok); }

/* ② 图表块：只给容器（画布内部由 JS 决定，本批不接）。 */
.report-chart {
  background: var(--ad-bg); border: 1px solid var(--ad-line);
  border-radius: var(--ad-r-card); padding: var(--sp-5);
  margin-bottom: var(--sp-5);
}
.report-chart__hd {
  display: flex; align-items: center; gap: var(--sp-3);
  font-size: var(--fs-13); color: var(--ad-ink-2);
  letter-spacing: var(--ls-wide); margin-bottom: var(--sp-4);
}
.report-chart__sp { flex: 1; }
.report-chart__cv { min-height: 180px; display: flex; align-items: center; justify-content: center; }
.report-chart__ph { font-size: var(--fs-12); color: var(--ad-placeholder); }

/* ================================================================ ★★ 2026-10-05 批 K
   商品流水页「三个图表并排」（余总直接指令：「还是商品流水里面的三个图表并排」）。
   ★ 为什么单开一层 `.rp-charts`、而不是给 `.report-chart` 加列数：
     `.report-chart` 是**全站通用**的图表卡（其它页也在用），在它身上写列数
     ＝把"这一页的排法"变成"全站的排法"，会牵动别处。故只在本页容器上加约束。
   ★★ 三条口径（余总 2026-10-05 拍板）：
     ① 三块外壳**统一成实线卡**（本页 `.sage-grp` 虚线分组框退役）；
     ② ★ **永远三列**（窄屏也不折行）—— 与「不许出现横向滚动条」同时成立的
        唯一写法 = `repeat(3, minmax(0, 1fr))`：`minmax` 下限给 **0**，
        列可以一直缩窄、永不把容器撑宽。⚠️ 写成 `repeat(3, 1fr)` 不行 ——
        `1fr` 隐含下限 `auto`，内容一宽就顶出横滚；
     ③ 三列**等高**靠 `align-items: stretch`。 */
.rp-charts {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-5); align-items: stretch; margin-top: var(--sp-4);
  container-type: inline-size;
}
/* 并排后卡内边距收一档（三列各约 390px，40px 内边距吃掉太多画布）；
   `margin-bottom` 归零 —— 竖向间距改由 grid 的 `gap` 统一给。 */
.rp-charts .report-chart {
  margin-bottom: 0; padding: var(--sp-4);
  display: flex; flex-direction: column;
}
.rp-charts .report-chart__hd { margin-bottom: var(--sp-3); }
/* ★ 画布区 `flex:1` ⇒ 卡等高的差额由它吸收（三张卡的内容各自垂直居中）。 */
.rp-charts .report-chart__cv { flex: 1 1 auto; }
/* ★★ 关键一条：卡内的图表**一律撑满整列宽**。
   不写这条，flex 容器会按"内容自然宽"收缩子元素 —— 那正是"占比条被挤成
   255px 宽、居中飘着、20 格糊成一片"的真凶（真机实测，非估计）。
   `min-width:0` 防长串/表格把列撑宽（grid 项默认 `min-width:auto`）。 */
.rp-charts .report-chart__cv > * { width: 100%; min-width: 0; }
/* 占比条：**标签列允许被压缩**。原 `.sage-seg__l` 写死 `width:88px` ＋
   `flex-shrink:0`，窄列时会把中间 20 格轨道挤到不到 1px/格（"永远三列"的
   必然代价）；这里放开收缩并用省略号兜住，保住轨道可读。 */
.rp-charts .sage-seg__l {
  flex: 0 1 88px; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rp-charts .sage-seg__v { width: auto; min-width: 34px; padding-left: var(--sp-2); }
.rp-charts .sage-seg__track { gap: 2px; }
/* 成交密度矩阵：格子**封顶 44px**。`aspect-ratio:1` 的方格若按列宽撑满
   （约 190px 见方）会把整行高度反撑上去 —— 实测未封顶时该卡 485px 高，
   并排后反而比并排前更占地方。 */
.rp-charts .sage-heat__c { max-width: 44px; }
/* ★★ 批 K2（2026-10-05 · 余总直接指令）：「**矩阵图下方的小数点**前台不要选显示
   隐藏就可以了」⇒ 那行图例的样式（原 `.rp-chart__hint`）**整块删掉** ——
   只留一个没人再用的类名就是死样式（本项目规矩：不留死代码）。
   图例文字改为挂 `title` 悬停提示（见 `viewReportFlow` 的 `heatTip`）—— 信息不丢、版面干净。 */
/* ★★ 窄列的物理极限与兜底（余总口径「永远三列」⇒ 不能靠折行腾地方，只能"就地缩"）。
   占比条一行 = 标签 + 间隙12 + 轨道 + 间隙12 + 值34。卡内宽 169px（768 视口实测）时，
   轨道只剩 38px ÷ 20 格 ⇒ 每格 0px，图表等于没了（真机实测，非估计）。
   ⇒ 按**容器宽**（不是视口宽！侧栏折叠会改变列宽，视口宽推不出列宽）分两档让位：
        < 1000px：标签收窄到 60px（省略号）
        < 760px ：标签隐藏（名称仍在 `title` 悬停提示里，百分值 65% 也仍在）
     这样中间那 20 格永远有地方，图还在。
   ⚠️ 用 `@container` 而非媒体查询，正是为了避开"侧栏折叠"这个变量。
   ⚠️⚠️ **必须放在本段最末**：容器查询不提升特异性，它和上面的
      `.rp-charts .sage-seg__l { flex: 0 1 88px }` 同为 (0,2,0) ⇒ **靠先后定胜负**。
      此前把它写在上面，被后面的 `flex` 简写覆盖 ⇒ 规则"看着在、实际不生效"、
      且**不报任何错**（真机实测：容器宽 912 < 999 但 `flex-basis` 仍是 88px）。
      正是本项目最贵的一类坑 —— 代码在、规则在、就是不生效。 */
@container (max-width: 999px) {
  .rp-charts .sage-seg__l { flex-basis: 60px; }
}
@container (max-width: 759px) {
  .rp-charts .sage-seg__l { display: none; }
}

/* ══ ★★ 批 K2（2026-10-05）：报表筛选条 —— **整段可折行、段内永不折** ══════════

   ★ 余总四问之一：「**日期看起来很挤** 是什么问题？」—— 真机实测（1440 视口）：
     筛选条整条高 **113px**（正常一行约 40px）。「日期」那一段被压到 **329px**，
     可它自己要 **412px**（标签 40 ＋ 框 176 ＋ `~` ＋ 框 176）⇒ 两个日期框在
     **段内**折成上下两行、`~` 孤零零留在上一行末尾（看着就像"两个独立日期"）。
   ★ 根因两层：
     ① `.ad-panel__head` 是 `flex-wrap: nowrap` ⇒ 一行放不下时**不折行**，
        改成**压缩子项**；② 框上那个内联 `style="width:150px"` **是废的** ——
        被 `.ad-q` 的 `flex: 0 1 22rem` ＋ `min-width: 11rem`(=176px) 盖掉。
   ★ 修法：段不压缩、整段可折；日期框单独给一个**够用且不超**的宽度。
     ⚠️ 只加在报表页这 5 条（类 `rp-filters`）—— `.ad-panel__head` 是全站共用的，
        别的页面（尤其右侧带按钮组、靠 `spacer` 顶开的那种）依赖 nowrap，不能一起动。 */
.ad-panel__head.rp-filters { flex-wrap: wrap; row-gap: var(--sp-3); }
.ad-panel__head.rp-filters > span { flex: 0 0 auto; white-space: nowrap; }
/* ★★ 宽度**统一由这里说了算**：原先每个框上都写了内联 `style="width:150px"` 之类的宽度，
   但它们是**废的** —— 被 `.ad-q` 的 `min-width: 11rem`(=176px) 顶住，实测从来不是写的那个值
   （本项目典型「看着在、其实不生效」）。⇒ 把内联宽度全删了，改由这里三行决定，
     顺带把整条压进一行：日期段 412→303、SKU 段 207→170、搜索段 207→168
     （实测总宽 1097px < 内容区 1122px ⇒ 一行排完，不再折行）。 */
.ad-panel__head.rp-filters .ad-q { min-width: 0; }
/* ★ 日期框 `120px` 是**实测出来的预算上限**：整条塞进 1170px 内容区后，日期框给到
   132px 就会**挤出第二行**（真机实测 headH 117px），120px 才是一行（73px）。
   ⚠️ 120px 下 `clientWidth == scrollWidth`（118 == 118）⇒ 内容**没溢出**、没真被裁，
     只是 Chromium 的日期控件把日历图标压在右端、文字几乎贴着它（截图看着像被截断）。
     ⇒ 所以这里再压缩一下图标自身的 margin/padding，把这点空间还给文字。 */
.ad-panel__head.rp-filters input[type="date"] { width: 120px; }
.ad-panel__head.rp-filters input[type="date"]::-webkit-calendar-picker-indicator {
  margin: 0; padding: 0; opacity: .55;
}
.ad-panel__head.rp-filters input[type="search"] { width: 140px; }


/* ================================================================ v23 批 2 · 规格区表格化
   一条规格一行；列由 schema 的 variants.itemFields 决定，加字段自动多一列。
   横向滚动 + 前两列（序号 / SKU）冻结 —— 滚到右边也知道自己在改哪条 SKU。 */
.ad-spec-wrap {
  overflow-x: auto;
  border: 1px solid var(--ad-line);
  border-radius: var(--r-2);
  background: var(--ad-bg);
}
/* ⚠️ table-layout 必须是 fixed：auto 布局下 table-cell 的 width/min-width 只是「建议」，
   浏览器按内容最小宽度分配 —— 实测下拉框被压到只显示「23∨」「黄∨」（内容截断成一两个字），
   版本列（该产品没填）更是被压到 0 宽、整列贴在一起，看着像列错位。
   fixed + th 上的明确 width 才是可控的。
   min-width = 下面各列 width 之和：比它窄就横向滚动（前两列冻结才有用武之地）。 */
.ad-spec-tbl {
  border-collapse: separate; border-spacing: 0;
  table-layout: fixed; width: 100%; min-width: 1408px;
}
.ad-spec-tbl th, .ad-spec-tbl td {
  padding: var(--sp-2);
  border-bottom: 1px solid var(--ad-line);
  text-align: left;
  vertical-align: top;
}
.ad-spec-tbl tbody tr:last-child td { border-bottom: 0; }
.ad-spec-tbl thead th {
  background: var(--ad-bg-alt);
  font-size: var(--fs-th);
  font-weight: 500;
  color: var(--ad-th-ink);   /* ★ 2026-10-07 表头字色接皮肤令牌 */
  /* ★★ B22：这里原来是 `white-space: nowrap`。加第二颗按钮（「档位」）之后，
     120px 的「版本」列**装不下**「版本（从上面「版本」里选）＋档位＋填空」——
     nowrap + table-layout:fixed 的组合下，溢出的内容**不裁不缩**，
     直接**盖到隔壁列上**。后果不是难看，而是**点不到**：
     隔壁「尺寸」列的「填空」按钮成了命中目标，
     真实点击（Playwright `page.click`，会做命中检测）持续被它截获 ⇒ 点「档位」没反应。
     ⚠️ 而 JS `element.click()` **绕过命中检测**，照样触发 —— 所以探针若用 JS 点击，
     这个 bug 会永远看不见（B22 就是这么被 `check_product_dual_tree` 真点击逮出来的）。
     ⇒ 改回允许折行：宁可表头两行，也不许元素互相盖住。 */
  white-space: normal;
}
/* 冻结的前两列 */
.sp-fz { position: sticky; background: var(--ad-bg); z-index: 1; }
.ad-spec-tbl thead .sp-fz { background: var(--ad-bg-alt); z-index: 3; }
.sp-fz--no {
  left: 0; width: 44px; min-width: 44px;
  color: var(--ad-muted-ink); font-size: var(--fs-11);
  text-align: center; font-variant-numeric: tabular-nums;
}
.sp-fz--sku { left: 44px; width: 208px; min-width: 208px; box-shadow: 1px 0 0 var(--ad-line-2); }
/* 列宽（按 kind 分档，不按字段名分）。必须是 width 而不是 min-width ：
   fixed 布局按 th 的 width 定列，min-width 在这里不参与；改动这些值要同步改上面的 min-width。
   ⚠️ pick 列为什么要 120px 而不是 104px：
      数据里存的可能是字典的**同义写法**（p-d021 的 size 存的是 "230mm"），
      而 dictValuesFor 会给这类值补上「230mm（同义写法 ≡ 230）」这样一长串说明。
      104px 只显示得下一个「2」，运营根本认不出是哪个尺寸 —— 卡片流时代列宽宽，看不出这个问题。
      120px 下「230mm（同义…」足够区分 230 / 370 / 直径300 这些值；要看全文点开下拉即可。 */
.sp-c--pick { width: 120px; }
.sp-c--text { width: 84px; }
.sp-c--media { width: 124px; }
.sp-c--ops { width: 132px; }
/* 格内不留 label / hint：列名在表头，说明进表头 title（JS 侧因此零侵入） */
.ad-spec-tbl td .ad-f__label, .ad-spec-tbl td .ad-f__hint { display: none; }
.ad-spec-tbl td .ad-f { margin: 0; display: block; }
.ad-spec-tbl td .input, .ad-spec-tbl td .select {
  width: 100%; min-width: 0; box-sizing: border-box;
}
/* 图片格压扁（表格里要的是小缩略图 + 选择按钮，不是一整块 media 面板）
   ★ B32 二改：原来是 `flex-wrap: wrap` —— 缩略图(34px) + 按钮在 124~150px 的窄格里
     **换行成两行**（≈48px），这一格就把**整行**顶到 56px（td 会被最高的格拉平）。
     ⇒ 改 `nowrap` + 居中：一行 34px，行高才真正由 `--row-h` 决定。 */
.ad-spec-tbl td .ad-mediafield { flex-wrap: nowrap; align-items: center; gap: var(--sp-1); }
/* ★ B32 三改：原来写死 34px ⇒ 内容 34 + 上下内边距 8 = **42px**，这一格把整行顶到 42。
   改成跟 `--row-h` 联动（低 24 / 中 32 / 高 48 / 超高 64），四档才**精确**。 */
.ad-spec-tbl td .ad-mediafield__thumb {
  width: calc(var(--row-h) - 8px); height: calc(var(--row-h) - 8px);
}
.ad-spec-tbl td .ad-mediafield__path { display: none; }
/* ★★★ B32（2026-09-27 18:0x 余总：「表格还是乱」）——
   上面 1124 行定的原则是「格内不留 label / hint」，但 `.ad-pick` **自带**的
   说明（`__meta`）与计数（`__cnt`）**不属于** `.ad-f__hint` ⇒ 成了漏网之鱼。
   实测（`tools/_diag_spec_tab.py`）：102px 的窄格里
   `.ad-pick__row`（chips 换行）78px ＋ `.ad-pick__meta`（长句换行 6~7 行）102px
   ⇒ 一行被撑到 **218px**，`--row-h` 四档**形同虚设**（td 的 height 只是最小高度）。
   ⇒ 格内只留「值 + ▾」；说明归表头圆点 `?`。与 1124 行同一条原则，补漏。 */
.ad-spec-tbl td .ad-pick__meta { display: none; }
/* `__cnt`（「已选 N」）只在**多选**时才生成（admin.js:1222 `single ? '' : …`）；
   规格表当前列全是单选 ⇒ 命中不到。**保留**是因为列是数据驱动的（`specColClass`），
   将来加多选列（如 tags）会用到；不写判据（免得写出恒真的假绿）。 */
.ad-spec-tbl td .ad-pick__cnt { display: none; }
.ad-spec-tbl td .ad-pick__row { min-height: 28px; padding: 2px var(--sp-2); }
.ad-spec-tbl td .ad-pick__chips { flex-wrap: nowrap; overflow: hidden; min-width: 0; }
.ad-spec-tbl td .ad-pick__chips > * { white-space: nowrap; }
/* ★ 让 `--row-h` 真正驱动行高：内边距压到 4px，格内控件跟 `--row-h` 联动。
   两处都是「同优先级靠后写赢」—— 1081 行那条 `padding: var(--sp-2)` 在本行之前。
   改前实测：控件 50px + 上下各 8px = **82px**（`--row-h` 四档完全无效）。 */
.ad-spec-tbl td { padding: var(--sp-1) var(--sp-2); }
.ad-spec-tbl td .input, .ad-spec-tbl td .select {
  height: calc(var(--row-h) - 8px); min-height: calc(var(--row-h) - 8px);
  padding-top: 0; padding-bottom: 0;
}
.ad-spec-tbl td .ad-pick__row { min-height: calc(var(--row-h) - 8px); padding: 0 var(--sp-2); }
/* 「填长×宽×高」按钮原来 padding 4px 撑到 30px ⇒ 低档（32px）被它顶到 38、与中档只差 2px。
   同样联动后低档才有意义。 */
.ad-spec-tbl td .sp-pack { min-height: calc(var(--row-h) - 8px); padding-top: 0; padding-bottom: 0; }
/* ★ R7·Q8：原来这里把**表格内** chip 的上下 padding 压到 1px（于是它只有 23px、比其他几处矮）。
   统一后 chip 一律 `height: 27px`，而本表 `--row-h` = 48px ⇒ 放得下 ⇒ 这条覆写**删除**。 */
/* ★★ B23（2026-09-26 余总：「**表头文字改大加粗**」）——
   11px/500 的表头在两千多像素宽的表格里几乎沦为背景。提到 13px/700、与正文同色。
   ★ 敢放大的前提是列名已经**只剩短名字**：那串「（从上面「版本」里选）」「（可空，空则用主图）」
     一类说明全挪进了右侧圆点 `?` 的弹窗（见 .sp-th__q）——长句子不再占表头的位置。 */
.sp-th__t {
  margin-right: var(--sp-1);
  font-size: var(--fs-13);
  font-weight: 700;
  letter-spacing: .01em;
  color: var(--ad-ink);
}
/* 表头圆点 `?`：讲这一列怎么填。★ B23 二改（余总 2026-09-26 01:1x）——
   从"点开弹窗"（首版）改成**悬停一行气泡、移开自动收**（样式见下面的 .sp-tip）。
   替掉的原生 `title` 悬停：延迟约 1s、样式不可控、还容易被当成列名的一部分。 */
.sp-th__q {
  width: 15px; height: 15px; padding: 0; margin: 0 2px 0 0;
  border: 1px solid var(--ad-line-2); border-radius: 50%;
  background: var(--ad-bg); color: var(--ad-muted-ink);
  font-size: var(--fs-10); font-weight: 700; line-height: 1;
  /* ★ 二改后它**不再可点**（悬停即出气泡）⇒ 光标用 `help` 而不是 `pointer`：
     `pointer` 会让人点了等反应，而点它什么都不会发生。 */
  cursor: help; vertical-align: 1px;
}
.sp-th__q:hover { border-color: var(--ad-ink-2); color: var(--ad-ink); background: var(--ad-bg-alt); }
/* ★ B23 二改 · 列说明气泡（`#spTip`，body 下单例，由 spTipShow 定位）。
   ★ 深底浅字**沿用 `.toast` 那一套色** —— 后台里"临时浮在最上层的一句话"只有这一种长相。
   ★ `white-space: nowrap`：正常就一行（余总：「显示一行文字就可以了」）；
     真长到超出 `max-width` 才折行兜底（比如包装列那句 60 字）。
   ★ `pointer-events: none`：气泡不吃鼠标，光标从圆点滑向气泡不会打断悬停。 */
.sp-tip {
  position: fixed; z-index: 400;
  max-width: min(560px, calc(100vw - 16px));
  padding: 6px 10px;
  background: var(--ad-ink); color: var(--ad-bg);
  border-radius: var(--r-2);
  font-size: var(--fs-12); line-height: 1.5;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .16);
  visibility: hidden; opacity: 0;
  transition: opacity var(--ad-dur-hover) var(--ease);
  pointer-events: none;
}
.sp-tip.is-on { visibility: visible; opacity: 1; }
/* ★ 合并列（包装）：一格显示「长 × 宽 × 高」，点开弹窗一次填完。
   三个数在数据里仍是三个独立的键 —— 合并只发生在这一层（见 admin.js 的 groups 折叠）。 */
.sp-c--pack { width: 150px; }
.sp-pack {
  width: 100%; padding: var(--sp-1) 6px; text-align: left;
  border: 1px dashed var(--ad-line-2); border-radius: var(--r-2);
  background: var(--ad-bg); color: var(--ad-ink);
  font-size: var(--fs-12); font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.sp-pack:hover { border-color: var(--ad-ink-2); background: var(--ad-bg-alt); }
.sp-pack.is-empty { color: var(--ad-ink-2); border-style: dashed; }
.sp-pack__u { margin-left: 3px; color: var(--ad-ink-2); font-size: var(--fs-10); }
/* ★ 0 规格产品：主属性原文在字典里没有对应项时显示的小字。
   余总原话：「应该读取该品的主属性显示 **而不是空着**」——
   所以宁可显示一个"字典里没有"的提示，也不留一个看着像"这里本来就没信息"的空格子。 */
.sp-base {
  display: block; margin-top: 2px;
  font-size: var(--fs-10); line-height: 1.4; color: var(--ad-ink-2);
  word-break: break-all;
}
/* ★ B22：按钮自己不许折行（「档位」被拆成两行就没法认），但**允许表头单元格折行**
   —— 上面 thead th 的注释说了为什么 `nowrap` 是「点不到」的元凶。 */
.sp-fill {
  font-size: var(--fs-10); line-height: 1.5;
  white-space: nowrap;
  padding: 0 6px; border-radius: var(--r-pill);
  border: 1px solid var(--ad-line-2);
  background: var(--ad-bg); color: var(--ad-muted-ink);
  cursor: pointer;
}
.sp-fill:hover { border-color: var(--ad-ink-2); color: var(--ad-ink); }
.sp-ops { display: flex; flex-wrap: wrap; gap: var(--sp-1); }

/* ================================================================
   ★ 2026-09-27 批 6.7之四 · 飞书编辑体验对齐（落地）
   口径出处 = `v2/_docs/DESIGN.md` §4.8。样机对照 = `_docs/样机_后台飞书对齐_2026-09-27.html`。
   落地范围（只碰规格表 + 它的工具栏/脚注，不动已验收的详情页 .ad-f 布局）：
     ① 行高四档（`.ad-rowh`）  ② 批量提交条（`.ad-batch`）  ③ hover 才显动作（`.sp-ops`）
   ================================================================ */

/* ---- ① 行高四档（飞书「低/中等/高/超高」）—— 只动一个变量 `--row-h` ---- */
.ad-rowh {
  display: inline-flex; align-items: center; gap: 2px; padding: 2px;
  background: var(--ad-bg-alt); border-radius: var(--r-2);
  box-shadow: var(--shadow-hairline);
}
.ad-rowh__lb { font-size: var(--fs-11); color: var(--ad-muted-ink); margin-right: var(--sp-1); }
.ad-rowh button {
  width: 26px; height: 24px; padding: 0; border: 0; border-radius: var(--r-1);
  background: none; cursor: pointer; display: grid; place-items: center; gap: 2px;
}
.ad-rowh button i { display: block; width: 12px; background: var(--ad-muted); }
.ad-rowh button:nth-of-type(1) i { height: 1px; }
.ad-rowh button:nth-of-type(2) i { height: 3px; }
.ad-rowh button:nth-of-type(3) i { height: 5px; }
.ad-rowh button:nth-of-type(4) i { height: 7px; }
.ad-rowh button.is-on { background: var(--ad-bg); box-shadow: var(--shadow-hairline-2); }
.ad-rowh button.is-on i { background: var(--ad-ink); }
/* ★ 行高落点：规格表每一格的高度读 `--row-h`。
   ⚠️ 只给 `.ad-spec-tbl` 加，别写到全站 `td` —— 列表页 `.ad-table` 是另一套行距，改了会动到已验收页面。
   ⚠️ 用 `min-height` 不用 `height`：格内是 input/select 时 `height` 会把控件压扁。 */
.ad-spec-tbl td.sp-c, .ad-spec-tbl td.sp-fz { min-height: var(--row-h); }

/* ---- ① 原位编辑（飞书：平时是纯文本，点进去才出控件）----
   ★★★ 实现要点：**原控件一个字不改** —— 它仍在 DOM 里、仍有 data-path、仍能聚焦。
     做法 = 用一个**不透明底**的 `.cell` 盖在真控件上（平时看不出来），
     聚焦/悬停时把 `.cell` 藏掉 ⇒ 真控件露出来，成了输入框。
     ⇒ 保存链路（data-path / input-change 委托）完全没动，不存在第二套取值逻辑。

   ★★ 两个实测订正（首版踩过）：
     ① `.cell` 必须**有不透明背景**（`--c-bg`），`opacity:0` 盖不住 ——
        原生 `select` 的选中文字由 UA 绘制，`opacity:0` 对它无效 ⇒ "文本 + 控件"叠着显示。
     ② `.cell` 必须 `pointer-events:none` —— 否则它盖住真控件，点下去只点到文本。
     ③ 只包 `text/number/date/select` 四种（见 JS `specCellHTML`）——
        字典列走 `.ad-pick`（chips + 弹窗），媒体/开关各有完整控件，包了会打架。 */
.cellwrap { position: relative; display: block; min-width: 0; }
.cellwrap > .cell {
  position: absolute; inset: 0; z-index: 1;
  display: flex; align-items: center; gap: var(--sp-1);
  padding: 0 var(--sp-2); border: 1px solid transparent; border-radius: var(--r-2);
  background: var(--ad-bg); font-size: var(--fs-13); color: var(--ad-ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  pointer-events: none;                       /* ★ 点击要落到真控件上 */
  transition: opacity var(--tr), border-color var(--tr);
}
/* ★ 2026-09-30：原 `flex-end`（数字右对齐）按 §4.34 改**居中**；`tabular-nums` 保留。 */
.cell--num { justify-content: center; font-variant-numeric: tabular-nums; }
.cell--dim { color: var(--ad-ink-2); }

/* ★ 2026-09-27 全铺·**订正**：这里原本有 `.cellwrap--f`（编辑页字段的**原位编辑**盖层）。
   已回退 —— 样机里编辑页字段走的是「**字段即列头**」（左标签 / 右控件，见上面 `.ad-f` 那段），
   不是「点开才出控件」。⇒ `.cellwrap` 一族现在**只服务规格表格子**（`specCellHTML`）。
   记录：撤之前实测全站 14 个编辑页 221 个字段行、值一致率 100% —— **实现没问题，是方向错了**。
   ⇒ 教训：**判据全绿 ≠ 做对了事**；设计对不对只能拿权威参照（样机）逐条对。 */
/* 真控件铺满格子（这是它的**真实**尺寸，`.cell` 只是盖上去的一层）
   ★★ 必须用**后代**选择器（空格），不能用 `>` 直接子 —— 实测：`blockFieldHTML`
   产出的是 `<div class="ad-f">…<input class="input">…</div>`，真控件在**第二层**
   ⇒ `.cellwrap > .input` 永远匹配不到（静默失效：没报错、只是规则不生效）。
   `.cell` 自己仍是 `.cellwrap` 的直接子，所以它的规则继续用 `>`。 */
.cellwrap .input, .cellwrap .select {
  width: 100%; min-width: 0; box-sizing: border-box;
}
/* 悬停：先让"可编辑"的边露出来，并且**藏掉盖层**（用户一悬停就看到真控件 = 知道能改） */
.cellwrap:hover > .cell { opacity: 0; }
.cellwrap:focus-within > .cell { opacity: 0; }
/* 悬停 / 聚焦时真控件给出可见边框 */
.cellwrap:hover .input, .cellwrap:hover .select,
.cellwrap:focus-within .input, .cellwrap:focus-within .select {
  background: var(--ad-bg); border-color: var(--ad-field-line);   /* ★ AA 1.4.11 */
}
.cellwrap:focus-within .input, .cellwrap:focus-within .select {
  border-color: var(--ad-ink); box-shadow: 0 0 0 2px rgba(1, 1, 1, .06);
}
/* ★ 表格行的 hover 底色要透到格子底，否则整行变灰、只有编辑格是白的（一道白块）
   ★★ 2026-10-03 SAGE 严格对齐（第二轮）：值 `--ad-bg-alt` → `--ad-bg-sunk`。
      本句的**目的**是「让 `.cell` 盖层与**行悬停底色**一致」（避免白块），
      所以它必须**永远等于 `.ad-table tr:hover td` 的取值**，不能独立选色。
      本轮行悬停 #F0F1E9 → #E9EAE2，本句没跟 ⇒ 规格表行悬停时格子仍是 #F0F1E9（浅一档白块）。
      ⇒ 改为同色，与 A5.2 对齐。★ 判据 A5.5 就是逮这个「行悬停族不同步」的。 */
.ad-spec-tbl tbody tr:hover .cellwrap > .cell { background: var(--ad-bg-sunk); }
/* ★ 2026-10-04（批 T4 · 余总第 3 次更正）：恢复 SAGE 换皮原状（本句必须**永远等于**
   `.ad-table tr:hover td` 的取值 ⇒ 行悬停底 = --ad-bg-sunk 时本句也是它）。 */

/* ---- ③ hover 才显动作（飞书：不 hover 不打扰） ----
   ★ 两个"必须"：① 键盘 Tab 进来也要看得见 ⇒ `:focus-within` 一起显（否则键盘用户点不到按钮）；
                  ② 不 hover 时用 `opacity:0` 而非 `display:none` —— 后者会让元素消失、布局跳动。 */
.sp-ops { transition: opacity var(--tr); }
.ad-spec-tbl tbody tr .sp-ops { opacity: 0; }
.ad-spec-tbl tbody tr:hover .sp-ops,
.ad-spec-tbl tbody tr:focus-within .sp-ops { opacity: 1; }
/* ghost 行不给按钮（它只有一个提示语）⇒ 不必显隐，常显 */
.ad-spec-tbl tbody tr.is-ghost .sp-ops { opacity: 1; }
/* 触屏/无 hover 设备：hover 永不触发 ⇒ 一律常显，不能让按钮点不到 */
@media (hover: none) { .ad-spec-tbl tbody tr .sp-ops { opacity: 1; } }

/* ---- ② 批量提交条（飞书：改动未提交时冒出来，带条数）----
   ★ 数据流口径：改动只进内存 `edits{}`，点「批量提交」才走既有 save()（单次 PUT / 一个 If-Match）。
     ⇒ 这条不是"另一个保存按钮"，它是**把 N 次改动合成一次 PUT** 的可见凭据。 */
.ad-batch {
  display: none; align-items: center; gap: var(--sp-3);
  margin-top: var(--sp-4); padding: var(--sp-3) var(--sp-4);
  background: var(--ad-ink); color: var(--ad-bg-alt); border-radius: var(--r-4);
}
.ad-batch.is-on { display: flex; }
.ad-batch__n { font-size: var(--fs-12); letter-spacing: var(--ls-wide); }
.ad-batch__n b { font-weight: var(--fw-medium); color: var(--ad-white); }
.ad-batch .sp { flex: 1; }
.ad-batch button {
  min-height: 30px; padding: 0 var(--sp-4); border-radius: var(--r-2);
  border: 1px solid rgba(242, 238, 235, .4); background: transparent; color: var(--ad-bg-alt);
  font: inherit; font-size: var(--fs-11); letter-spacing: var(--ls-wider);
  text-transform: uppercase; cursor: pointer;
}
.ad-batch button:hover { background: rgba(242, 238, 235, .14); }
.ad-batch button.primary {
  background: var(--ad-accent); border-color: var(--ad-accent); color: var(--ad-white);
}
.ad-batch button.primary:hover { background: var(--ad-accent-2); }
/* pending 改动的高亮（飞书：改过的格有痕迹，提交前看得见自己改了哪些） */
.ad-spec-tbl .cell.is-dirty { box-shadow: inset 0 -2px 0 var(--ad-accent); }

/* ================================================================
   ★★★ 2026-09-27 批 6.7之五 · 飞书对齐「全铺」（第二轮）
   余总 16:5x 定：按《样机_后台飞书对齐》**全铺** · **一次铺完统一验收**。
   上一轮只落在规格表一处 ⇒ 本轮把同一套机制泛化到全站：
     · 列表页（`.ad-table` / `.ad-spec-tbl`）：③ 行高四档 · ④ 冻结列 · ⑥ hover 才显动作
     · 编辑页（`.ad-f`）：② 字段即列头（**改由 CSS 两列 grid 实现**，见下方 `.ad-f` 块）
   ★ 泛化原则 = **同一个变量 / 同一套类名**，不新造第二套（本项目最贵的一类坑是两套各自漂移）。
   ★★ **订正（09-27 17:0x）**：编辑页**不做**「原位编辑」—— 偏离样机（样机的 `.cell` 只用在表格）。
      原始说明留档在 `_docs/DESIGN.md` §4.8「执行中的订正」。
   ★★★ **G1 补（09-27 19:0x）**：上面两类只覆盖「表格」与「表单」，**够不着「非表格的同类记录」**
      ⇒ 产品三栏左栏（`.ad-prow`）与素材库（`.ad-media-grid`）曾是全站唯一两个没档位的。
      本轮补齐，**规则紧贴各自的基础定义**（`.ad-media-grid` 后 / `.ad-prow__thumb` 后），
      不放在本块 —— 本块在它们**前面**，同特异性下会被盖掉（静默失效）。
   ================================================================ */

/* ---- ③ 行高四档 → 全站列表页 ----
   ★ `height` 在 `table-cell` 上按规范就是**最小高度**（内容多时照样撑开）——
     这正是飞书「行高」的语义，不是把内容裁掉。
   ★ 同时把上下 padding 清零：否则 `padding:12px` ＋ 内容会把行顶到 42px，
     档位（32/40/56/72）根本量不出来 —— **设了变量却不生效**是最难查的一类。 */
.ad-table td { height: var(--row-h); padding-top: 0; padding-bottom: 0; }

/* ---- ⑥ 行内操作按钮 → 全站列表页 ----
   ★★ 2026-10-03 SAGE 严格对齐（第三轮 · 余总截图原话「**这些按钮颜色返了 默认状态下看不到**」）：
   **原口径已作废** —— 那时是「`opacity: 0` 常隐，hover / focus 才现」（飞书式省空间）。
   余总看截图当场判定「默认状态下看不到」＝**毛病**（他打开列表页时第一眼找不到 ↑↓ / 编辑 / 删除）。
   ⇒ 改为**常显**（`opacity: 1`）。

   ★ 为什么干脆删掉整族 opacity 规则、而不是把它改成 1：
     留着 `opacity: 1` 的"显"规则 ＋ 下面 `.ad-table tbody tr td:first-child .mini { opacity: .45 }`
     的"弱化"规则，两条会按**行序**打架（同为 0,2,1 特异性）——
     而 `td:first-child` 那条**在源序上更靠后 ⇒ 会赢**，第一列仍然只有 45% 透明度。
     余总说的是"看不到"，第一列 45% 同样叫看不到。⇒ 一族**整体清掉**，不留半条。
   ★ 键盘可达性不再需要 `:focus-within` 补显（本来就常显了）。
   ★ 触屏媒体查询（`@media (hover:none)`）也随之成为死规则 —— 一并清掉，
     否则它会与"常显"重叠、日后被误读成"这一族还需要 hover 兜底"。 */
/* ★ 表头是 L1 画布层（`--ad-bg-alt` #F0F1E9），行内按钮是 L3 强调实体（橄榄底）——
   两者本来就靠明度差分层，不需要再"弱化"。 */

/* ---- ④ 冻结列 → 只给**宽表**（`.ad-body--wide` 下的 `.ad-table` 首列）----
   ★ 为什么只给宽表：窄表本来就不横滚，冻结无意义、还多一份风险；
     而 wide 正是「列多到要横滚」的那批（客户 / 供应商 / 商品供应商 / 报价单 / 报表 / odoo）。
   ★★ 冻结列 `background` **必须自己写**且**跟着 `tr:hover` 变** ——
     否则横滚时下层内容透出来、悬停时冻结列白一块（这两条在规格表那边实测过）。
   ★★★ `border-collapse: collapse` 下单元格边框会**跟着滚走**（边框属表格、不属单元格）
     ⇒ 冻结线用 `box-shadow` 画在首列右缘，不用 `border-right`。 */
.ad-body--wide .ad-table th:first-child,
.ad-body--wide .ad-table td:first-child {
  position: sticky; left: 0; z-index: 2;
  background: var(--ad-bg);
  box-shadow: 1px 0 0 var(--ad-line);
}
.ad-body--wide .ad-table thead th:first-child { z-index: 4; background: var(--ad-bg-alt); }
.ad-body--wide .ad-table tbody tr:hover td:first-child { background: var(--ad-bg-sunk); }
/* ★ 2026-10-04（批 T4 · 余总第 3 次更正）：恢复 SAGE 换皮原状。
   ⚠️ 这是宽表**冻结首列**（单据列表 = 勾选框列，**不是单号列**）⇒ 随整行「下沉」，
      不参与单号列的「去底＋加粗＋玫红」。 */

/* ★★ R7·Q9（2026-10-01 · 余总定「按需显示」）—— **冻结列滚动阴影只在真横滚过才出**。
   规范 §5.1 给的是 `2px 0 8px rgba(0,0,0,.08)` —— 它是**滚动提示**（"左边还有内容"），
   不是静态装饰。★ 常显的代价实测过：不滚时投影会在冻结列右缘留一条 8px 淡带，
   比现在的 1px 分界线**更脏**；而 12 个宽表页**一页都不横滚**
   （`.ad-body--wide .ad-table` 是 `width:100%` + `table-layout:auto`，装得下就永不滚）。
   ★ 真载体 = **产品规格表**：`.ad-spec-wrap` 的 `min-width:1408px` 让它 1600/1440/1280
     三档全在横滚（实测 `cw 658/498/338` vs `sw 1408`），冻结的「序号 + SKU」两列才需要提示。
   ★ 加类的是**滚动容器**（不是列），由 admin.js 的捕获阶段 scroll 监听切 `is-xscroll`
     （`scrollLeft > 0` 才加、回到 0 就摘）。
   ⇒ 口径：**1px 分隔线打底 ＋ 真横滚时叠规范投影**；不滚时一个像素都不多。 */
.ad-spec-wrap.is-xscroll .sp-fz--sku {
  box-shadow: 1px 0 0 var(--ad-line-2), 2px 0 8px rgba(0, 0, 0, .08);
}
.ad-body--wide.is-xscroll .ad-table th:first-child,
.ad-body--wide.is-xscroll .ad-table td:first-child {
  box-shadow: 1px 0 0 var(--ad-line), 2px 0 8px rgba(0, 0, 0, .08);
}

/* 重复行整行浅红（表头那层另有 SKU 角标 ——「在长表格里也要一眼认出来」） */
.ad-spec-tbl tr.is-dup td { background: var(--ad-err-bg); }
.ad-spec-tbl tr.is-dup .sp-fz { background: var(--ad-err-bg); }

/* ---- B24 · 规格表上方的版本切换条（语汇对齐前台单品页的 .pdp-vers） ----
   为什么是胶囊按钮而不是下拉：两个版本一眼看全、点一下就到；
   下拉要先展开再选，而且原生下拉面板是系统样式，跟本站暖白语汇对不上。 */
.ad-versbar { display: flex; flex-wrap: wrap; gap: var(--sp-1); padding: 0 0 var(--sp-3); }
/* ★ R7·Q8：与通用 `.ad-chip`（~986）**同一套几何**：高 27 · 药丸 · 白底 · 1px `--ad-line-2`。
   三处改动：字号 10px → **12px**（原来这条是全站最小的控件文字）· 高 23 → **27**
   · 选中态由「**只换边色、不实心**」改成**实心 `--ad-ink` ＋白字**（与另四处同一个"选中"语言）。 */
.ad-vers {
  display: inline-flex; align-items: center; gap: 6px;
  height: 27px; padding: 0 11px; border-radius: var(--r-pill);
  border: 1px solid var(--ad-line-2); background: var(--ad-bg);
  color: var(--ad-ink); font-size: var(--fs-12); font-family: inherit; cursor: pointer;
}
.ad-vers:hover { border-color: var(--ad-ink-2); }
.ad-vers.is-on {
  background: var(--ad-ink); border-color: var(--ad-ink); color: var(--ad-bg);
  font-weight: 500;
}
/* ★ 收起来的行必须真的不占位：`tr` 的 `display: table-row` **盖掉** `[hidden]` 的
   `display: none`（UA 样式表的 `[hidden]` 优先级更低）⇒ 不写这条，`hidden` 形同虚设，
   表现是「点了 tab 什么都没变」，而且不报任何错。 */
.ad-spec-tbl tr.sp-row[hidden] { display: none !important; }

/* ---- B28 · 规格页顶部「版本」多选 + 按版本分 TAB（2026-09-26 12:1x 余总） ----
   基本信息页**不再有**版本选择；选版本挪到规格页顶部。
   选了 N 个版本 ⇒ 下面就是 N 个 TAB，一格管一个版本的规格。
   （TAB 条本身仍是上面那套 .ad-versbar / .ad-vers，只是它的"格集合"换了来源。） */
.ad-vers-pick { margin-top: var(--sp-5); }
.ad-vers-pick__hd { font-size: var(--fs-13); color: var(--ad-ink); margin-bottom: var(--sp-3); }
.ad-vers-pick__hd .ad-f__hint { display: block; margin-top: 2px; }
.ad-vers-pick .ad-pick { max-width: 640px; }

/* TAB 上的「0 条」角标：哪一格还空着，一眼看得出来 */
.ad-vers__n {
  display: inline-block; margin-left: 6px; padding: 0 5px;
  border-radius: var(--r-pill);
  background: var(--ad-line-2); color: var(--ad-muted-ink); font-size: var(--fs-10);
}
/* ★ R7·Q8：选中态改**实心近黑**后，角标要跟着反过来 ——
   原来的「浅灰底 ＋ 深字」压在深底上会糊成一团。 */
.ad-vers.is-on .ad-vers__n { background: rgba(255, 255, 255, .22); color: var(--ad-bg); }

/* 每一格（pane）= 一张表。`[hidden]` 在 div 上本来生效，这里显式写一条，
   免得将来谁给它加了 display 又变成"点了 tab 什么都没反应"。 */
.ad-spec-pane[hidden] { display: none !important; }

/* 版本列：**只读** —— 值由"这一格是哪个版本"决定，在这一格里不可改 */
.sp-c--ver { width: 118px; }
.sp-ver--none { color: var(--ad-muted-ink); font-size: var(--fs-10); }

/* ghost 行：还没成为数据的那一行 —— 虚线 + 微底，与真行一眼区分。
   ⚠️ 它**不是**数据：计数不算它、保存不带它，填任意一格它才转正。 */
.ad-spec-tbl tr.is-ghost td { background: var(--ad-bg-soft); border-bottom-style: dashed; }
.ad-spec-tbl tr.is-ghost .sp-fz { background: var(--ad-bg-soft); }
.sp-ghosthint { font-size: var(--fs-10); color: var(--ad-muted-ink); white-space: nowrap; }

/* ================================================================
   v23 批 3：列表搜索框 + 规格 Excel 按钮
   1. 搜索框只过滤**已渲染的行**（admin.js → listFilter），不重渲染 ——
      输入框在页面里，整页重绘会让它丢焦点，打第二个字就断。
   2. 「导出规格 / 导入规格」复用 .btn--sm：两者都只改内存，要点保存才落盘。
   ================================================================ */

.ad-q {
  flex: 0 1 22rem;
  min-width: 11rem;
  padding: 6px var(--sp-3);
  border: 1px solid var(--ad-line-2);
  /* ★ R7 · Q2（F12）：原为 `var(--r-pill)` 胶囊 —— 后台表单控件一律圆角 4
     （规格见文件末尾「Q2（2/2）· 表单控件覆盖」段）。 */
  border-radius: var(--ad-r-field);
  background: var(--ad-bg-alt);
  color: var(--ad-ink);
  font: inherit;
  font-size: var(--fs-12);
}
.ad-q:focus {
  outline: none;
  border-color: var(--ad-ink);
  background: var(--ad-bg);
}
.ad-q::placeholder { color: var(--ad-muted); }
.ad-q::-webkit-search-cancel-button { cursor: pointer; }
.ad-q__count {
  font-size: var(--fs-11);
  color: var(--ad-muted);
  white-space: nowrap;
}

/* 过滤掉的行必须**真的不占位**。.ad-table 目前没给 tr 写 display，hidden 属性本可生效；
   显式补一条，免得以后有人给 tr 加了 display 就静默失效（同「.card 自带 flex 让 [hidden] 失灵」那个坑）。 */
.ad-table tbody tr[hidden] { display: none; }

/* ==========================================================================
   v24 批 4A · 历史版本对比弹层
   ⚠️ 令牌只用 tokens.css 里**确实存在**的 —— --fs-13 全站未定义
      （9 处引用静默失效），本批一个都没用它。
   语义：from = 现在的值（红），to = 恢复后的值（绿）。
   本站在中文语境下「涨红跌绿」，但这里的红绿表达的是
   「即将丢失 / 即将恢复」，与涨跌无关，故用 err/ok 令牌而非价格色。
   ========================================================================== */

.ad-diff {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin: var(--sp-4) 0;
}
.ad-diff__s {
  font-size: var(--fs-12);
  padding: 3px 10px;
  border-radius: var(--r-pill);
  border: 1px solid var(--ad-line-2);
  background: var(--ad-bg-alt);
  color: var(--ad-ink-2);
}
.ad-diff__s--add { border-color: var(--ad-ok-text); color: var(--ad-ok-text); }
.ad-diff__s--rem { border-color: var(--ad-err-text); color: var(--ad-err-text); }
.ad-diff__s--chg { border-color: var(--ad-warn-text); color: var(--ad-warn-text); }

.ad-diff__sec { margin-top: var(--sp-5); }
.ad-diff__t {
  font-size: var(--fs-12);
  color: var(--ad-muted);
  padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--ad-line);
  margin-bottom: var(--sp-3);
}
.ad-diff__t.is-add { color: var(--ad-ok-text); }
.ad-diff__t.is-rem { color: var(--ad-err-text); }
.ad-diff__t.is-chg { color: var(--ad-warn-text); }

.ad-diff__body {
  max-height: 300px;
  overflow: auto;
  border: 1px solid var(--ad-line);
  border-radius: var(--r-2);
  background: var(--ad-bg-alt);
}
.ad-diff__row {
  padding: var(--sp-3);
  border-bottom: 1px solid var(--ad-line);
  font-size: var(--fs-12);
  color: var(--ad-ink);
}
.ad-diff__row:last-child { border-bottom: 0; }
.ad-diff__f {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
  padding-left: var(--sp-3);
}
.ad-diff__f code {
  font-family: var(--font-mono);
  font-size: var(--fs-11);
  color: var(--ad-ink-2);
  background: var(--ad-bg-sunk);
  padding: 1px 5px;
  border-radius: var(--r-2);
}
.ad-diff__from {
  color: var(--ad-err-text);
  text-decoration: line-through;
  word-break: break-all;
  max-width: 42%;
}
.ad-diff__arrow { color: var(--ad-muted); }
.ad-diff__to {
  color: var(--ad-ok-text);
  word-break: break-all;
  max-width: 42%;
}


/* ==========================================================================
   v24 批 4B · 素材库：搜索 / 只看未引用 / 引用明细
   ========================================================================== */

/* ★ 卡片自带 display:flex，不给 [hidden] 显式兜底的话「隐藏」会**静默失效**
   （与 .ad-table tbody tr[hidden] 同一个坑，批 3 已经记过一次）。 */
.ad-media[hidden] { display: none; }

/* 引用明细（弹层内）：数据集 · 条目 · 字段，整行可点、点了跳过去 */
.ad-refs { display: grid; gap: 6px; max-height: 48vh; overflow: auto; }
.ad-ref {
  display: grid; grid-template-columns: 68px 1fr auto;
  gap: var(--sp-3); align-items: center;
  width: 100%; text-align: left; font: inherit; cursor: pointer;
  border: 1px solid var(--ad-line); background: var(--ad-bg);
  border-radius: var(--r-2); padding: 6px var(--sp-3);
}
.ad-ref:hover { border-color: var(--ad-accent); }
.ad-ref__ds { font-size: var(--fs-10); color: var(--ad-ink-2); }
.ad-ref__who { font-size: var(--fs-12); }
.ad-ref__where {
  font-size: var(--fs-10); color: var(--ad-ink-2); font-family: var(--font-mono);
  /* 显示条目内的完整路径（blocks[2].images[0]）而不是顶层字段名（blocks）——
     否则同一张图被同一款灯引用多次时，几行会长得一模一样、看不出区别。
     路径可能很长，截断 + title 兜底看全。 */
  max-width: 190px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---------------------------------------------------------------- v24 批 4C：图片控件
   单图字段 = 大预览（灯具是竖构图，用 contain 不裁）；图组 = 网格卡片。
   表格 / 列表子项里仍然走**紧凑形态**（mediaHTML 的 opts.compact），
   所以下面的 --big 样式不会渗进规格表格（那边靠 .ad-spec-tbl td .ad-mediafield__thumb
   压到 34px，见本文件上方「图片格压扁」一节）。 */

.ad-mediafield--big { display: grid; gap: var(--sp-3); }
.ad-mediafield__big {
  position: relative; display: block; width: 100%; height: 200px; padding: 0;
  border: 1px solid var(--ad-line); border-radius: var(--r-2);
  background: var(--ad-bg-alt); cursor: pointer; overflow: hidden;
  transition: border-color var(--tr);
}
.ad-mediafield__big:hover { border-color: var(--ad-accent); }
.ad-mediafield__big img { display: block; width: 100%; height: 100%; object-fit: contain; }
.ad-mediafield__big.is-empty { border-style: dashed; }
.ad-mediafield__ph {
  display: flex; align-items: center; justify-content: center; height: 100%;
  color: var(--ad-ink-2); font-size: var(--fs-12);
}
.ad-mediafield__hint {
  position: absolute; right: var(--sp-2); bottom: var(--sp-2);
  padding: 2px var(--sp-2); border-radius: var(--r-pill);
  background: rgba(20, 18, 16, .58); color: var(--ad-white);
  font-size: var(--fs-10); letter-spacing: var(--ls-wide);
  opacity: 0; transition: opacity var(--tr);
}
.ad-mediafield__big:hover .ad-mediafield__hint { opacity: 1; }
.ad-mediafield__foot { display: flex; align-items: center; gap: var(--sp-2); }
.ad-mediafield__foot .input { flex: 1; min-width: 0; font-size: var(--fs-11); }

/* 图组网格卡片 */
.ad-mgrid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(124px, 1fr));
  gap: var(--sp-3);
}
.ad-mcard {
  display: grid; border: 1px solid var(--ad-line); border-radius: var(--r-2);
  background: var(--ad-bg); overflow: hidden;
}
.ad-mcard.is-hero { border-color: var(--ad-accent); }
.ad-mcard__thumb {
  position: relative; display: block; width: 100%; height: 110px; padding: 0;
  border: 0; border-bottom: 1px solid var(--ad-line); background: var(--ad-bg-alt);
  cursor: pointer; overflow: hidden;
}
.ad-mcard__thumb img { display: block; width: 100%; height: 100%; object-fit: contain; }
.ad-mcard__thumb:hover { border-bottom-color: var(--ad-accent); }
.ad-mcard__ph {
  display: flex; align-items: center; justify-content: center; height: 100%;
  color: var(--ad-ink-2); font-size: var(--fs-11);
}
.ad-mcard__no {
  position: absolute; left: 4px; top: 4px; padding: 0 var(--sp-1);
  border-radius: var(--r-pill); background: rgba(20, 18, 16, .55); color: var(--ad-white);
  font-size: var(--fs-10); line-height: 16px;
}
.ad-mcard__badge {
  position: absolute; right: 4px; top: 4px; padding: 0 5px;
  border-radius: var(--r-pill); background: var(--ad-accent); color: var(--ad-white);
  font-size: var(--fs-10); line-height: 16px;
}
.ad-mcard__ops { display: flex; flex-wrap: wrap; gap: 2px; padding: var(--sp-1); }
/* ★★ 2026-10-03 第三轮订正：本句原为 `border-color: --ad-accent; color: --ad-accent`
   （橄榄边 ＋ 橄榄字）—— 那是"在白底上用橄榄描边"的旧口径。
   现在 `.mini` 的选中态统一定为**底 `--ad-lime-2`**（余总「选中色 #E1E9C2」），
   本句若仍只改 border/color，会与上面 `.mini.is-on` 的 `background: --ad-lime-2`
   叠加成「青柠底 ＋ 橄榄边」—— 边与底撞色。
   ⇒ 收掉本句（**删除**），让"首屏"按钮**完全吃 `.mini.is-on` 那一条**，全站一个口径。
     本项目铁律：同一语义不要两条各自漂移的规则。 */

.ad-mcard__more { border-top: 1px solid var(--ad-line); }
/* 去掉了浏览器自带的 marker，就得自己补一个展开指示符 —— 否则这一行看着只是
   个灰标签，用户不知道能点开（这是拍交付截图时看出来的）。 */
.ad-mcard__more > summary {
  display: flex; align-items: center; gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-2); font-size: var(--fs-10); color: var(--ad-muted-ink);
  cursor: pointer; list-style: none;
}
.ad-mcard__more > summary::-webkit-details-marker { display: none; }
.ad-mcard__more > summary::before {
  content: '\25B8'; font-size: var(--fs-10); line-height: 1;
  transition: transform var(--tr);
}
.ad-mcard__more[open] > summary::before { transform: rotate(90deg); }
.ad-mcard__more[open] > summary { color: var(--ad-ink-2); border-bottom: 1px solid var(--ad-line); }
.ad-mcard__fields { display: grid; gap: var(--sp-2); padding: var(--sp-2); }
.ad-mcard__fields .ad-f { gap: var(--sp-1); }
.ad-mcard__fields .input { font-size: var(--fs-11); }
.ad-mcard__fields .ad-bi__tag { padding-top: var(--sp-2); }

/* ---------------------------------------------------------------- v24 批 4D：编辑效率
   「上一条 / 下一条」是编辑页顶部的一条窄行（.ad-pn）；
   左侧导航的「有未保存改动」圆点（.ad-nav__item.is-dirty + .ad-nav__dot）。
   令牌全部是既有的（与 4C 同一批，见 tokens.css）。 */

.ad-pn {
  display: flex; align-items: center; gap: var(--sp-2);
  margin: 0 0 var(--sp-4); padding: 6px var(--sp-3);
  border: 1px solid var(--ad-line); border-radius: var(--r-2);
  background: var(--ad-bg-alt);
}
.ad-pn__btn { white-space: nowrap; }
.ad-pn__btn[disabled] { opacity: .35; cursor: default; }
.ad-pn__pos {
  flex: 1; min-width: 0; text-align: center;
  color: var(--ad-muted-ink); font-size: var(--fs-11); letter-spacing: var(--ls-wide);
}
/* 只给「没被选中」的那条上色 —— 选中态是深底药丸，红字压在近黑底上对比不够
   （拍交付截图时看出来的）。圆点则不分选中与否，一直在。 */
.ad-nav__item.is-dirty:not(.is-on) { color: var(--ad-accent); }
.ad-nav__dot {
  display: inline-block; width: 6px; height: 6px; margin-left: var(--sp-1);
  border-radius: 50%; background: var(--ad-accent); vertical-align: middle;
}

/* ================================================================ 报价单（批 4，2026-09-24）
   单据类界面：单头 + 行表 + 合计。行表列多，给它一个横向滚动兜底，
   免得窄屏上「删」按钮被挤到看不见 —— 而那一列正是最需要够得着的。 */
.ad-qline { min-width: 1080px; }
.ad-qline td { vertical-align: middle; }
.ad-qline .input { padding: var(--sp-1) 6px; font-size: var(--fs-12); }
.ad-qline select[multiple] { padding: 2px; }

/* 合计条：三个数并排，数用等宽数字（对齐时不会左右跳）。 */
.ad-qsum {
  display: flex; flex-wrap: wrap; gap: var(--sp-4) var(--sp-6);
  align-items: baseline;
}
.ad-qsum span { color: var(--ad-muted-ink); font-size: var(--fs-12); }
.ad-qsum b {
  margin-left: 6px; color: var(--ad-ink);
  font-size: var(--fs-16); font-variant-numeric: tabular-nums;
}

/* ================================================================ v25 批：中栏列表栏（.ad-side）
   一行 = 40px 缩略图 + 型号 + 品名 + 状态点。紧凑列表（不是 8 列表格）——
   340px 宽里塞得下 22 款、一屏看到 12~14 条，这正是「数据查找方便」的落点。 */

.ad-side__bar { padding: var(--sp-3); border-bottom: 1px solid var(--ad-line); display: grid; gap: var(--sp-2); }
.ad-side__bar .ad-q { width: 100%; }
.ad-side__tools { display: flex; gap: var(--sp-2); align-items: center; }
.ad-side__tools .input { flex: 1; min-width: 0; font-size: var(--fs-12); padding: var(--sp-1) var(--sp-2); }
.ad-side__tools .ad-q__count { white-space: nowrap; }

/* 筛选 chips —— ★ R7·Q8：**这里原本又写了整套 `.ad-chip`**（与 ~986 通用那条重复，
   还与 ~2970 列浮层三方撞车、靠行序「后到者胜」互相盖）。几何已并入通用那条，
   此处只留**容器**与筛选特有交互（可点 + hover）。 */
.ad-chips { display: flex; flex-wrap: wrap; gap: var(--sp-1); }
.ad-chips .ad-chip { cursor: pointer; transition: background var(--tr), border-color var(--tr), color var(--tr); }
.ad-chips .ad-chip:not(.is-on):hover { background: var(--ad-bg-sunk); }

/* 条目列表：可滚动（高 = 视口 - 顶栏 - 工具栏，免得整页跟着滚） */
.ad-plist {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: var(--sp-2);
  display: grid; gap: 2px; align-content: start;
}
.ad-prow {
  display: flex; align-items: center; gap: var(--sp-3);
  width: 100%; text-align: left;
  padding: 5px var(--sp-2); border-radius: var(--r-2);
  border: 1px solid transparent; background: none;
  color: var(--ad-ink-2); font-family: inherit; cursor: pointer;
  text-decoration: none;                    /* 条目是 <a>（内嵌 ↑↓ 按钮，button 不能套 button） */
  transition: background var(--tr), border-color var(--tr);
}
.ad-prow:hover { background: var(--ad-bg-sunk); }
/* ★ 2026-10-04（批 T4 · 余总第 3 次更正）：恢复 SAGE 换皮原状（这是型号弹出卡里的行，
   不是单据列表单号列）。 */
/* 选中态：浅底 + 左描边（不是纯黑药丸 —— 黑底会把型号的红色压掉，读不清） */
.ad-prow.is-on {
  background: var(--ad-bg-alt);
  border-color: var(--ad-line-2);
  box-shadow: inset 3px 0 0 var(--ad-accent);
  color: var(--ad-ink);
}
.ad-prow__thumb {
  width: 40px; height: 44px; flex: none; border-radius: var(--r-1);
  object-fit: cover; background: var(--ad-bg-sunk);
}
.ad-prow__ph {
  width: 40px; height: 44px; flex: none; border-radius: var(--r-1);
  background: var(--ad-bg-sunk); border: 1px dashed var(--ad-line-2);
}
.ad-prow__mid { min-width: 0; flex: 1; display: grid; gap: 1px; }
.ad-prow__sku {
  font-size: var(--fs-12); font-variant-numeric: tabular-nums; color: var(--ad-ink);
}
.ad-prow__name {
  font-size: var(--fs-12); color: var(--ad-muted-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* ★★ 2026-10-03 批 S1 · 余总拍板「A 补『推荐』文字标签」——
   原来「这是**粒什么**」只有 7px 实心圆 + title 悬停提示，而该行右端**没有别的内容**
   ⇒ 圆点周围 300px 全空白，脱离文字语境后读起来像画面上的一粒污点，不像状态标记。
   ★ 修法照样机 `.sq`（= 我们后台上层的命名体系）：**色块 + 文字**成对出现，
     色块从 7px 圆 → **8px 方块**（`--ad-r-data` 4px，与样机 `.sq::before` 逐值一致），
     旁边补 `--fs-11` 文字标签。文字用深色（`--ad-ink-2`），色块用语义色 ——
     ⚠️ 不用「同色文字」：样机 `.sq` 的 `currentColor` 方块在**橄榄绿**上压浅底只有 ~2:1，
        放到 11px 小字上不可读（本项目铁律：亮色系只做填充 / 描边 / 圆点，不做文字）。
   ★ 两条并排（推荐 + 草稿）时靠 `gap: --sp-2` 分开，与样机 `.sq` 的 gap 同值。 */
.ad-prow__flags {
  display: flex; gap: var(--sp-2); align-items: center; flex: none;
  font-size: var(--fs-11); font-weight: var(--fw-medium); color: var(--ad-ink-2);
  white-space: nowrap;
}
.ad-prow__flag { display: inline-flex; align-items: center; gap: var(--sp-1); }
.ad-prow__dot {
  width: 8px; height: 8px; border-radius: var(--ad-r-data);
  background: var(--ad-accent); flex: none;
}
.ad-prow__dot--draft { background: var(--ad-warn); }
/* 调序 ↑↓：默认藏起来，hover / 选中才露（不占地方，也不干扰读列表） */
.ad-prow__mv { display: flex; gap: 1px; flex: none; opacity: 0; transition: opacity var(--tr); }
.ad-prow:hover .ad-prow__mv, .ad-prow.is-on .ad-prow__mv { opacity: 1; }
.ad-prow__mv .mini { padding: 1px 5px; font-size: var(--fs-10); }
/* ★★★ G1（2026-09-27）：产品三栏左栏「行高四档」——
   它是**全站主力列表**（28 个产品），却是**唯一**没吃到档位的同类记录视图
   （上一轮 `.ad-table` 选择器够不着它：它是 `<a class="ad-prow">`，不是 `<tr>`）。
   ★ 复用同一个变量 `--row-h`，顶栏那组按钮直接驱动。
   ★★ 两个系数**刻意对齐现状**（否则一进来页面就变样，会被当成回归）：
      · `+14px` ⇒ 中档 40+14 = **54px** = 改动前的行高（含上下各 5px 内边距）
      · `+4px` × `0.91` ⇒ 中档 **40×44** = 改动前的缩略图尺寸
   ★★★ **不变式（别破坏）**：`.ad-prow` 是 `.ad-plist`（grid）的 item，会被拉伸 ⇒
      它的高度**只由 `min-height` 决定，内容撑不开它**（实测：`min-height:0` 时行塌成 29px，
      而缩略图仍有 76px ⇒ **图片溢出**，且 `overflow` 是 `visible`、**不报错**）。
      所以必须让 `缩略图高(--row-h+4) + 上下内边距(5+5) == min-height(--row-h+14)` ——
      两个系数是**绑定的**，改一个必须改另一个。这也是「中档 == 现状」能成立的原因。
   ⚠️ 本规则必须**写在 `.ad-prow` / `.ad-prow__thumb` 之后** —— 它们各自声明了
      `padding` 与 `width/height`，同特异性下按顺序取胜；放前面会被盖掉（静默失效）。 */
.ad-prow { min-height: calc(var(--row-h) + 14px); }
.ad-prow__thumb,
.ad-prow__ph { height: calc(var(--row-h) + 4px); width: calc((var(--row-h) + 4px) * 0.91); }
.ad-plist__empty { padding: var(--sp-5); text-align: center; color: var(--ad-ink-2); font-size: var(--fs-12); }

/* 右栏「还没选」时的提示 */
.ad-side-hint {
  display: flex; align-items: center; justify-content: center;
  min-height: 46vh; color: var(--ad-ink-2); font-size: var(--fs-14);
}

/* ---------------------------------------------------------------- Odoo 只读页（阶段 5 · T5.D.4）
   订单 / 库存两页共用。★ 只加**本页专属**的类（一律 `od-` 前缀），
   不动任何既有类 —— 免得改到已经验收过的页面（CRM / 采购那两批）。
   ⚠️ `.od-badge` 与 CRM 页面在用的 `.badge` **不是同一个类**：
      `.badge` / `.badge--ok` 在本文件里**根本没有定义**（CRM 批写了 class 没写样式），
      那是既有缺陷 —— 本批**不顺手改**（会动到已验收页面的外观，要另走一次截图验收），
      已登记到待办。 */
.od-bar { display: flex; align-items: flex-start; gap: var(--sp-4); }
.od-bar > div:first-child { flex: 1; min-width: 0; }
.od-bar__act { display: flex; align-items: center; gap: var(--sp-2); flex: none; }
.od-busy { color: var(--ad-accent); font-size: var(--fs-12); white-space: nowrap; }
/* 状态徽章：Odoo 的 state。★ 「已确认 / 已完成」给绿底，取消给灰字，其余默认中性。 */
.od-badge {
  display: inline-block; padding: 1px 7px; border-radius: var(--r-pill);
  border: 1px solid var(--ad-line); background: var(--ad-bg-alt);
  font-size: var(--fs-11); color: var(--ad-ink-2); white-space: nowrap;
}
.od-badge--on { background: var(--ad-ok-bg); border-color: var(--ad-ok-line); color: var(--ad-ok-text); }
.od-badge--mute { color: var(--ad-ink-2); }
/* 「未挂客户 / 对不上」这类**如实留空**的占位 —— 灰一点，但别做成「没有数据」的错觉。 */
.od-miss { color: var(--ad-ink-2); }
/* 订单明细的单头：键值网格。★ auto-fit ⇒ 以后加一项不用改 CSS（口径 ⑬）。 */
.od-kvs { display: grid; gap: var(--sp-3); grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
.od-kv { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.od-kv > span { font-size: var(--fs-11); color: var(--ad-ink-2); }
.od-kv > b { font-size: var(--fs-14); color: var(--ad-ink); font-weight: var(--fw-medium); }

/* ---------------------------------------------------------------- B32 续：材料库存页徽标（`.ms-badge`）

   ★★ 为什么新造一个类，而不是复用 `.badge--ok / --warn / --danger`：
     `.badge` 与 `.badge--ok` 在本文件里**从来没有定义过**（CRM 批只写了 class 没写样式，
     属既有缺陷，登记在 `待批与待办总表.md` 第 2 条）。若在这里补 `.badge--warn/--danger`，
     会**顺带**改掉 CRM 那几个**已验收**页面的外观（它们同名同 class）——
     「改一个页面却动了别处」是本项目明令避免的。⇒ 新页面用新类名，影响面只有这一页。
   ⏳ 等余总点头要统一修 `.badge` 时，把下面几条并回 `.badge--*`（那时是一次专门的截图验收）。

   ★ 语义色取自 `v2/assets/css/tokens.css`（`--c-ok` / `--c-warn` / `--c-err`）——
     admin.css 里没有自己的颜色变量，别在这里硬写十六进制。 */
.ms-badge {
  display: inline-block; padding: 1px 7px; border-radius: var(--r-pill);
  border: 1px solid var(--ad-line); background: var(--ad-bg-alt);
  font-size: var(--fs-11); color: var(--ad-ink-2); white-space: nowrap;
}
/* 充足 / 启用：绿。★ 底色用 `.od-badge--on` 那一套（同文件里已有的绿），不再新调一个绿。 */
.ms-badge--ok { background: var(--ad-ok-bg); border-color: var(--ad-ok-line); color: var(--ad-ok-text); }
/* 偏低：黄。 */
.ms-badge--warn { background: var(--ad-warn-bg); border-color: var(--ad-warn-line); color: var(--ad-warn-text); }
/* 待补货：红。★ 用 `--c-err`（实心红字），不用品牌红 `--c-accent` ——
   品牌红是"可点 / 重点"，红字才是"要注意"，两者语义不同，别混。 */
.ms-badge--danger { background: var(--ad-err-bg); border-color: var(--ad-err-line); color: var(--ad-err-text); }

/* ---------------------------------------------------------------- B31：BOM 页（主区 + 右栏成本）

   布局意图与「产品三栏」同源（都在**一个**容器里切一刀 —— 见本文件第 63–69 行的教训），
   但这里**不复用** `.ad-split`：
     · `.ad-split` 是「中栏常驻列表」的形态（产品页专用），
     · BOM 页要的是「左边整块编辑区 + 右边一块窄的成本卡」，
       左边要能放下 10 列的物料行表 ⇒ 右栏不能太宽（220~260px 够）。
   ⚠️ 断点与 `.ad-split` 对齐（1000px 摞成单列）—— 别自己另定一套，
      两套断点以后必然对不上（本项目栽过分栏断点）。
   ⚠️ 右栏 `position: sticky` 只是「滚长表时成本还看得见」的顺手好处；
      窄屏退化成 static（sticky 在单列里会跟内容打架）。 */
.ad-bom-wrap { display: grid; grid-template-columns: minmax(0, 1fr) 260px; gap: var(--sp-5); align-items: start; }
.ad-bom-side { position: sticky; top: var(--sp-6); }
.ad-bom-side .ad-panel { margin-top: 0; }
@media (max-width: 1199px) {
  .ad-bom-wrap { grid-template-columns: minmax(0, 1fr); }
  .ad-bom-side { position: static; }
}

/* BOM 行表里的内联控件 —— 表格窄，控件要收着点，不然每格都顶爆列宽。 */
.ad-table .ad-inp { padding: var(--sp-1) 6px; font-size: var(--fs-12); }
.ad-table .ad-inp.num { text-align: right; }

/* ★ B31「物料行」表的列宽纪律（目视凭证逮出的两处）：
   ① 物料下拉要能看清「型号 · 规格」；
   ② 备注那格**必须能在剩余宽度里伸缩** —— 原来给输入框写死 `min-width:10em`
      且内容长，整表超出容器、右侧的单价/小计/删列被切掉（只看得到半句备注）。
   ⇒ 做法：备注列 `width:100%`（吃掉剩余宽度）+ 输入框 `width:100%`，
     其余列按内容收窄。表格外层的 `--flush` 已带横向滚动兜底。 */
.ad-table .ad-bom-note { width: 100%; min-width: 9em; }
.ad-table .ad-bom-note .ad-inp { width: 100%; min-width: 0; }
/* 行表里的内联 select 别被浏览器默认宽度撑开 */
.ad-table select.ad-inp { max-width: 100%; }

/* ★ 批 6.6（2026-09-27 10:3x）：报表三页的表样式。
   ① 「产品 BOM 报表」：头行可点（整行是按钮语义）+ 用料子表（展开行里再套一张表）。
      子表**不能**用 `.ad-table` 的全套（那样会有内边距/边框重影）⇒ 单独一条 `--sub`。
   ② 「成本报表」：没 BOM 的行（`rp-row-warn`）淡黄底，让"算不出成本"一眼可见
      —— 与「显示成 0」区分开：这里是"明确告诉你没有"。 */
.ad-table--rp-bom .rp-bom-head { cursor: pointer; }
.ad-table--rp-bom .rp-bom-head:hover { background: var(--ad-bg-soft, rgba(127,127,127,.06)); }
.ad-table.rp-bom-sub { width: 100%; }
.ad-table.rp-bom-sub th {
  font-weight: 500; color: var(--ad-ink-2); text-align: left;
  padding: var(--sp-1) var(--sp-3); border: 0;
}
.ad-table.rp-bom-sub td {
  padding: var(--sp-1) var(--sp-3); border: 0; font-size: var(--fs-td);
}
.ad-table--rp-cost .rp-row-warn { background: rgba(212, 160, 23, .07); }

/* ★★ E2E-3（2026-10-06 · 余总「报表导出模板 按这个」）：**客户销售流水**的总计行。
   ★ 模板那行**紧跟表头**（A5=总计，不是压在表尾）—— 版式照抄，所以这里只做"一眼认得出"：
     底 = `--ad-bg-soft`（与侧栏同源，不新造颜色）＋ 字重 600。
   ★★ **零阴影**（SAGE 铁律⑦）、**不加圆角、不加粗边框** —— 只换底色 + 字重，
      免得跟"行 hover / 选中"那几套底色打架（本项目吃过的形状）。
   ⚠️ 用 `.ad-table--rp-cf` 作用域前缀（不是光写 `.rp-row-total`）：
      别页若有同名的行也不会被顺手改掉（CSS 类名撞车**不报错**，只静默变样）。 */
.ad-table--rp-cf .rp-row-total > td {
  background: var(--ad-bg-soft);
  font-weight: 600;
}

/* ---------------------------------------------------------------- 响应式（照目录站，但简化）
   本机以桌面为主 ⇒ 窄屏只要「不坏」，不追求好用。 */
/* ★ R7·Q5：原来这里的**两条骨架断点**（1280 收中栏 + 1000 退两列）**已搬走**：
   · 1000px 那条 `grid-template-columns: 160px …` 是**反覆盖元凶** ——
     它与 900px 那条**同时命中**，而它在文件里更靠后 ⇒ **单列规则被它盖掉**，
     `≤900` 的单列意图**从来没生效过**（判据 X1 现在守这条）。
   · 两条的内容都并进文件末尾「R7·Q5 · 响应式三档」的 `768–1199` 档。 */


/* ================================================================ ★ 2026-09-27 批 6.7 之二
   产品详情编辑页专项优化（余总「按优化的落地」）。
   五项对策：① 左侧分组导航 + 进度 ② 切 TAB 自动铺开前 2 组 ③ 顶/底保存态同源
             ④ 页头紧凑步进器 ⑤ 分组卡片上 shadow-as-border（不再 border 双线）
   设计依据 = `_docs/DESIGN.md`（shadow-as-border 取代 border · 数字 tnum · 三档字重）。
   ⚠️ 类名一律与本文件同批新增（本项目最恨"类名拼错不报错"）—— 逐条都在这儿有定义。 */

/* ① 页头：TAB 与紧凑步进器同一行（原来步进器占一整行） */
.ad-ptop {
  display: flex; align-items: center; gap: var(--sp-3);
  flex-wrap: wrap;
}
.ad-ptop .ad-tabs { flex: 1; min-width: 0; margin-bottom: 0; }

/* ④ 紧凑步进器：`‹ 1 / 28 ›` 一颗药丸（原来是两个带字的按钮 + 中间位置） */
.ad-pn {
  display: inline-flex; align-items: center; gap: 2px;
  margin: 0; padding: 2px;
  border: 0; border-radius: var(--r-pill);
  background: var(--ad-bg-alt);
  box-shadow: var(--shadow-hairline);
  flex: 0 0 auto;
}
.ad-pn__btn {
  width: 26px; height: 24px; padding: 0;
  border: 0; border-radius: var(--r-pill);
  background: none; color: var(--ad-ink-2);
  font: inherit; font-size: var(--fs-13); line-height: 1;
  cursor: pointer;
}
.ad-pn__btn:hover:not([disabled]) { background: var(--ad-bg-sunk); color: var(--ad-ink); }
.ad-pn__btn[disabled] { opacity: .28; cursor: default; }
.ad-pn__pos {
  min-width: 52px; padding: 0 var(--sp-1); text-align: center;
  color: var(--ad-muted-ink); font-size: var(--fs-11); letter-spacing: var(--ls-wide);
  font-variant-numeric: tabular-nums;
}

/* ② + ③ 主体：左「分组导航」＋ 右「表单」 */
.ad-pedit {
  display: grid; grid-template-columns: 188px minmax(0, 1fr);
  gap: var(--sp-5); align-items: start;
  margin-top: var(--sp-5);
}
.ad-pedit > .ad-form { margin-top: 0 !important; }

.ad-pnav {
  position: sticky; top: calc(var(--ad-top-h) + var(--ad-tabh) + var(--sp-6));   /* v26：让开页签栏 */
  max-height: calc(100vh - var(--ad-top-h) - var(--ad-tabh) - var(--sp-9));
  overflow: auto;
  background: var(--ad-bg-alt);
  border-radius: var(--r-4);
  padding: var(--sp-3);
  box-shadow: var(--shadow-hairline);
}
.ad-pnav__head {
  display: flex; align-items: baseline; gap: var(--sp-2);
  padding: 0 var(--sp-2) var(--sp-2);
  font-size: var(--fs-10); letter-spacing: var(--ls-wider); text-transform: uppercase;
  color: var(--ad-muted);
}
.ad-pnav__head em {
  margin-left: auto; font-style: normal; font-size: var(--fs-11);
  color: var(--ad-ink); font-variant-numeric: tabular-nums; letter-spacing: 0;
}
.ad-pnav__bar {
  height: 3px; border-radius: var(--r-pill);
  background: var(--ad-bg-sunk); overflow: hidden;
  margin-bottom: var(--sp-2);
}
.ad-pnav__bar > i {
  display: block; height: 100%; width: 0;
  background: var(--ad-ink); border-radius: var(--r-pill);
  transition: width var(--dur) var(--ease);
}
.ad-pnav__list { display: flex; flex-direction: column; }
.ad-pnav__it {
  display: flex; align-items: center; gap: var(--sp-2);
  width: 100%; padding: 6px var(--sp-2);
  border: 0; border-radius: var(--r-2);
  background: none; color: var(--ad-muted-ink);
  font: inherit; font-size: var(--fs-12); text-align: left;
  cursor: pointer; line-height: 1.3;
}
.ad-pnav__it:hover { background: var(--ad-bg-sunk); color: var(--ad-ink); }
/* 展开态 = 近黑 + 中间字重（照 Linear：强调靠亮度台阶，不靠加粗） */
.ad-pnav__it.is-open { color: var(--ad-ink); font-weight: var(--fw-medium); }
.ad-pnav__it.is-on { background: var(--ad-bg-sunk); color: var(--ad-ink); }
.ad-pnav__t {
  flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ad-pnav__c {
  flex: 0 0 auto; font-size: var(--fs-10); color: var(--ad-ink-2);
  font-variant-numeric: tabular-nums;
}

/* ③ 底部保存条：与顶栏状态同源（updateStatus 一处改两边）。
   ★★ 2026-09-27 修（两次）：
   v1 写成 `position:sticky` 的**卡片**（在文档流末、高 80px）⇒ 盖住文档末尾的元素；
   v2 改成 `position:fixed` 全宽底栏 ⇒ Playwright / 浏览器 `scrollIntoView({block:'nearest'})`
      把目标对齐到**视口最底边**，正好落在固定底栏下面 ⇒ 依然点不到（实测
      `elementsFromPoint` 栈 = [BUTTON(底栏) → DIV.ad-pfoot → 目标]）。
   ⇒ ★ 最终口径：**底栏不进覆盖层**（回到文档流），只在**粘住时**贴底。
      · `position: sticky` 本身没问题，问题是它当时是**最后一个块**且高度很大；
      · 改为在 `.ad-pedit` 之下、给它一个**独立的容纳块**（`.ad-pfoot-wrap`），
        该容纳块只有底栏那么高 ⇒ sticky 生效范围 = 页面剩余滚动区，
        文档末尾仍留有（`.ad-pedit` 的 padding-bottom 已提供）等高空隙，
        目标元素永远有地方可以「滚到可见且不被盖」。 */
.ad-pfoot {
  position: sticky; bottom: 0; z-index: 15;
  display: flex; align-items: center; gap: var(--sp-3);
  margin-top: var(--sp-6);
  padding: var(--sp-3) var(--sp-5);
  /* ★★ 2026-10-03 SAGE 严格对齐：原 `rgba(252,251,252,.95)` 旧冷白 → 暖纸 95%（同 .ad-bar）。 */
  background: color-mix(in srgb, var(--ad-bg) 95%, transparent);
  backdrop-filter: blur(8px);
  border-radius: var(--r-4);
  box-shadow: var(--shadow-hairline);
}
/* 底栏的容纳块：撑满一行、不吃高度（底栏 sticky 时不至于盖住文档末尾） */
.ad-pfoot-wrap { display: block; }
.ad-pfoot__s {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-11); letter-spacing: var(--ls-wide);
  color: var(--ad-muted-ink);
}
.ad-pfoot__s.is-dirty { color: var(--ad-warn-text); }
.ad-pfoot__s .d {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--ad-ok);
}
.ad-pfoot__s.is-dirty .d { background: var(--ad-warn); }

/* ⑤ 分组卡片：shadow-as-border 取代 border（不占布局、不与内线叠双线） */
.ad-fieldset {
  border: 0;
  box-shadow: var(--shadow-hairline);
}

/* 分组标题栏更紧凑一点（原来 margin-bottom 太大，19 组叠起来很空） */
.ad-fieldset__bar { margin-bottom: var(--sp-4); }

@media (max-width: 1199px) {
  .ad-pedit { grid-template-columns: minmax(0, 1fr); }
  .ad-pnav { position: static; max-height: none; }
  .ad-pnav__list { flex-direction: row; flex-wrap: wrap; }
}

/* ================================================================
   ★★★ §4.9 · 内容编辑范式（2026-09-27 · 批次 G2 起）
   内容页 = 「编排图文」（不是填表、不是表格）⇒ 单独一套结构。
   ★ `.ed-*` 是**新命名空间**，与 `.ad-*` 分开 —— 硬套 `.ad-*` 会让两套语义互相污染。
   ★★ 颜色 / 间距**一律取 `tokens.css`**，不新造第二套变量（全项目铁律）。
   ================================================================ */

/* ---- E3 图文成对：一组「图片组 ＋ 文案」左右并排 ----
   原来两个字段**上下排、中间还夹着别的东西** ⇒ 看不出这段字配哪几张图
   （余总「表格还是乱」的一支）。现在同一张卡、左右并排，配对关系**看得见**。
   ★ 图片侧略宽（1.15 : 1）—— 灯具图多是竖构图，等宽会让图显得局促。
   ★ 窄屏回落单列（沿用既有 1100px 断点口径，不新造第二个断点）。 */
.ed-pair {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: var(--sp-5);
  align-items: start;
}
.ed-pair__a, .ed-pair__b { min-width: 0; }
/* ★★★ **内容页的字段不做「字段即列头」**（标签在左、占 170px）——
   实测：图文对的图片侧总共 407px，走「字段即列头」后控件只剩 **221px**
   ⇒ 图墙一行挤不下两张（`minmax(112px,1fr)` 在 221px 里只放得下 1 列）。
   ⇒ 内容页字段一律**标签在上、控件占满**。
   ★ 为什么不能像列表页那样一刀切到全站：数据类编辑页字段短、标签在左更好扫；
     内容类字段（图墙 / 块 / 长文）**要的是宽度**。两类的取舍相反 ⇒ 分开。
   ⚠️ 写在 `.ad-f` 的基础规则**之后**（`admin.css` 末尾）⇒ 同特异性下靠顺序取胜。 */
.ed-pair .ad-f { display: block; }
.ed-pair .ad-f__label { padding-top: 0; margin-bottom: var(--sp-2); }
.ed-pair .ad-f__hint { margin-top: var(--sp-1); }
/* 文字侧一道竖线 —— 把「这一对是一件事」画出来 */
.ed-pair__b { border-left: 1px solid var(--ad-line); padding-left: var(--sp-5); }

/* ---- E4 图墙：一组图 = 一格一个方缩略图（**以图看图**）----
   原来（`.ad-mgrid` / `.ad-mcard`）一张图 = 一张大卡片、竖排 ⇒ 一屏只看 3 张，
   而一组常有 5 张 ⇒ 「有哪些图」要滚着看。图墙一行 4~6 张 ⇒ **先看全，再谈改**。
   ★★ 数据管道**一个字没改** —— 复用同一套 `data-pick` / `data-mv` / `data-del` /
      `data-hero-set` / `data-add-media`（见 `mediaListHTML` 的注释）。
   ★ 操作按钮（↑↓ / 设首屏 / 删除）**hover 才显**，与全站列表页同一条口径（⑥）；触屏常显。
   ★ 尺寸跟着 `--row-h` 走 ⇒ 顶栏那组档位按钮**顺便也能调图墙密度**（同一变量，不新造第二套）。 */
.ed-wall {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(calc(var(--row-h) * 2.8), 1fr));
  gap: var(--sp-2);
  align-items: start;
}
.ed-wall__i {
  position: relative; min-width: 0;
  border: 1px solid var(--ad-line); border-radius: var(--r-2);
  background: var(--ad-bg); overflow: hidden;
}
.ed-wall__i.is-hero { border-color: var(--ad-accent); }
/* 缩略图是正方形（图墙要的是"一眼看全"，不是"看清细节"；点开才看大图）
   ⚠️ 必须写 `height: auto` —— `.ad-mcard__thumb` 基础规则里有**固定 `height: 110px`**，
      高度一旦是确定值，`aspect-ratio` 会被**静默忽略**（实测缩略图 221×111、比例 1.99）——
      单靠 `aspect-ratio` 打不过一个明确的 `height`。 */
.ed-wall__i .ad-mcard__thumb { display: block; width: 100%; height: auto; aspect-ratio: 1; padding: 0; border: 0; border-bottom: 0; background: var(--ad-bg-sunk); cursor: pointer; position: relative; }
.ed-wall__i .ad-mcard__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* 操作条盖在缩略图下缘 —— 不占额外高度（图墙的密度就是靠"不占高度"换来的） */
.ed-wall__ops {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; gap: 2px; justify-content: center; flex-wrap: wrap;
  padding: 3px 2px;
  background: linear-gradient(to top, rgba(20, 20, 22, .92), rgba(20, 20, 22, .55));
  opacity: 0; transition: opacity var(--tr);
}
/* 瓦片里按钮**收小** —— 130px 宽的瓦片放不下常规尺寸的 4 个按钮（会撑到 66px、盖掉半张图） */
.ed-wall__ops .mini { padding: 0 var(--sp-1); font-size: var(--fs-10); line-height: 1.7; min-height: 18px; }
.ed-wall__i:hover .ed-wall__ops,
.ed-wall__i:focus-within .ed-wall__ops { opacity: 1; }
/* 首屏角标常显（它是状态，不是操作） */
.ed-wall__i .ad-mcard__badge { position: absolute; top: 4px; left: 4px; z-index: 1; }
.ed-wall__i .ad-mcard__no {
  position: absolute; top: 4px; right: 4px; z-index: 1;
  min-width: 16px; height: 16px; border-radius: var(--r-1);
  background: rgba(20, 20, 22, .62); color: var(--ad-white);
  font-size: var(--fs-10); line-height: 16px; text-align: center; padding: 0 3px;
}
/* 「⋯」说明与路径：折叠着只占一行小字，展开时才长高（用户主动动作，不是默认占位） */
.ed-wall__more { border-top: 1px solid var(--ad-line); }
.ed-wall__more > summary {
  list-style: none; cursor: pointer; text-align: center;
  font-size: var(--fs-10); color: var(--ad-ink-2); padding: 2px 0;
}
.ed-wall__more > summary::-webkit-details-marker { display: none; }
.ed-wall__more > summary:hover { color: var(--ad-ink); background: var(--ad-bg-alt); }
.ed-wall__more .ad-mcard__fields { padding: var(--sp-2); display: grid; gap: var(--sp-2); }
@media (hover: none) { .ed-wall__ops { opacity: 1; } }

@media (max-width: 1199px) {
  .ed-pair { grid-template-columns: minmax(0, 1fr); }
  .ed-pair__b { border-left: 0; padding-left: 0; }
}

/* ==========================================================================
   ★★★ §4.9 · E1 三栏骨架 ＋ E5 实时预览（2026-09-27 · 批次 **G6**）
   --------------------------------------------------------------------------
   样机 `样机_后台内容编辑v3_2026-09-27.html` 的**左目录 / 中流 / 右预览**。
   ★ 落点 = **块卡页**（`#adBody` 里真有 `.ad-block` 的页：系列编辑 / 页面与版块编辑）。
     产品「页面内容·模块区」**不套** —— 它已经是三栏（左产品列表 ＋ 左分组导航 ＋ 正文），
     再加预览会挤成四栏。
   ★★ 宽度沿用样机实测值（196 / 340），**不新造第二套栅格**；断点与样机同档。
   ★★ 预览栏是 `position:sticky` —— 滚到哪都能看见；窄屏（≤1280）塌成单列、顺序 = 目录→内容→预览。
   ========================================================================== */
.ed-3col {
  display: grid;
  grid-template-columns: 196px minmax(0, 1fr) 340px;
  gap: var(--sp-5);
  align-items: start;
}
.ed-flow { display: grid; gap: var(--sp-5); min-width: 0; }

/* ---- 左：本页目录（锚点＝分组卡片，点了滚过去并展开） ---- */
.ed-toc {
  position: sticky; top: var(--sp-5);
  max-height: calc(100vh - 120px); overflow-y: auto;
  display: flex; flex-direction: column; gap: 2px;
  border: 1px solid var(--ad-line); border-radius: var(--r-4);
  background: var(--ad-bg); box-shadow: var(--shadow-hairline);
  padding: var(--sp-2);
}
.ed-toc__k {
  padding: var(--sp-2) var(--sp-2) var(--sp-1);
  font-size: var(--fs-10); letter-spacing: var(--ls-wider);
  text-transform: uppercase; color: var(--ad-ink-2);
}
.ed-toc__i {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 6px var(--sp-2); border: 0; border-radius: var(--r-2);
  background: none; color: var(--ad-ink); cursor: pointer;
  font: inherit; font-size: var(--fs-11); text-align: left; width: 100%;
}
.ed-toc__i:hover { background: var(--ad-bg-alt); }
.ed-toc__i.is-on { background: var(--ad-accent); color: var(--ad-lime-2); }
.ed-toc__i b { font-weight: var(--fw-normal); flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ed-toc__i i { font-style: normal; font-size: var(--fs-10); color: var(--ad-ink-2); }
.ed-toc__i.is-on i { color: color-mix(in srgb, var(--ad-lime-2) 55%, transparent); }

/* ---- 右：实时预览（**只读草稿** · 不拉接口 · 不写库） ---- */
.ed-live {
  position: sticky; top: var(--sp-5);
  max-height: calc(100vh - 120px);
  display: flex; flex-direction: column;
  border: 1px solid var(--ad-line); border-radius: var(--r-4);
  background: var(--ad-bg); box-shadow: var(--shadow-hairline);
  overflow: hidden;
}
.ed-live__hd {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--ad-line);
  font-size: var(--fs-10); letter-spacing: var(--ls-wider);
  text-transform: uppercase; color: var(--ad-ink-2);
}
.ed-live__hd .spacer { flex: 1; }
.ed-live__bd { overflow-y: auto; padding: var(--sp-3); display: grid; gap: var(--sp-2); }
.ed-live__foot {
  padding: var(--sp-2) var(--sp-3); border-top: 1px solid var(--ad-line);
  font-size: var(--fs-10); color: var(--ad-ink-2);
}
.ed-pv__hd { font-size: var(--fs-12); font-weight: var(--fw-medium); color: var(--ad-ink); }
.ed-pv__b {
  display: grid; grid-template-columns: 20px 34px auto minmax(0, 1fr);
  gap: var(--sp-2); align-items: center;
  padding: var(--sp-2); border-radius: var(--r-2); background: var(--ad-bg-alt);
}
.ed-pv__n { font-size: var(--fs-10); color: var(--ad-ink-2); text-align: right; }
.ed-pv__im, .ed-pv__ph {
  width: 34px; height: 34px; border-radius: var(--r-1);
  object-fit: cover; background: var(--ad-bg);
  border: 1px solid var(--ad-line);
}
.ed-pv__ph { display: block; }
.ed-pv__ty { font-size: var(--fs-10); color: var(--ad-muted); white-space: nowrap; }
.ed-pv__tx { font-size: var(--fs-11); color: var(--ad-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ed-pv__em { font-size: var(--fs-11); color: var(--ad-ink-2); padding: var(--sp-2); }
/* 高亮「当前滚到的那一组」（与左目录同一个 is-on 语义）。
   ★ 用**左侧 2px 强调条**而不是整圈描边：一个块组里可能有 9 个块（系列页就是），
     整圈描边会让 9 行全像"被选中"（目视截图逮到过）；强调条读作"这组在这里"。 */
.ed-pv__b.is-on { box-shadow: inset 2px 0 0 var(--ad-ink); }

@media (max-width: 1199px) { .ed-3col { grid-template-columns: 176px minmax(0, 1fr) 300px; } }
@media (max-width: 1199px) {
  .ed-3col { grid-template-columns: minmax(0, 1fr); }
  .ed-toc, .ed-live { position: static; max-height: none; }
  .ed-toc { order: 2; } .ed-live { order: 3; }
}

/* ==========================================================================
   ★★★ W1 丙 · 所见即所得三栏（2026-09-28 · DESIGN §4.12）
   「目录 ｜ 真预览 iframe ｜ 字段面板」

   ★ 与 `.ed-3col`（目录 ｜ 表单流 ｜ 结构预览）是**同一骨架**，只换中右两栏的内容：
     中 = 真前台页面的 iframe（不是画出来的结构清单）；右 = 该分组的字段面板。
   ★ 左栏复用 `.ed-toc`、右栏复用 `.ed-live` 的**全部既有样式**（零新视觉），
     本段只加"中栏是 iframe"这一件事需要的新规则 —— 不新造第二套卡片/目录外观。
   ★ 宽度：中栏是**真页面**要宽（1fr），右栏要放得下 picker / 图墙（400px）。
   ========================================================================== */
.ed3w {
  display: grid;
  grid-template-columns: 196px minmax(0, 1fr) 440px;
  gap: var(--sp-5);
  align-items: start;
}
/* 中：真预览 */
.ed3w__pv {
  position: sticky; top: var(--sp-5);
  max-height: calc(100vh - 120px);
  display: flex; flex-direction: column;
  border: 1px solid var(--ad-line); border-radius: var(--r-4);
  background: var(--ad-bg); box-shadow: var(--shadow-hairline);
  overflow: hidden; min-width: 0;
}
.ed3w__pbar {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--ad-line);
  font-size: var(--fs-10); letter-spacing: var(--ls-wider);
  text-transform: uppercase; color: var(--ad-ink-2);
}
.ed3w__pbar .spacer { flex: 1; }
.ed3w__dev { display: flex; gap: 2px; }
.ed3w__dev button {
  border: 1px solid var(--ad-line); background: var(--ad-bg); color: var(--ad-muted);
  border-radius: var(--r-2); font: inherit; font-size: var(--fs-10);
  padding: 2px var(--sp-2); cursor: pointer; text-transform: none; letter-spacing: 0;
}
.ed3w__dev button:hover { background: var(--ad-bg-alt); }
.ed3w__dev button.is-on { background: var(--ad-ink); color: var(--ad-bg); border-color: var(--ad-ink); }
/* 舞台：底色与页面区分开，才能看出"页面有多宽" */
.ed3w__stage {
  flex: 1; min-height: 0; overflow: auto; position: relative;
  background: var(--ad-bg-alt);
  display: flex; justify-content: center;
}
/* ★ iframe 宽度按**真实设备宽度**走 —— desk=1400 / tab=834 / phone=390。
   为什么不用「width:100% 跟着栏宽」：栏宽只有 700–1000px，前台在这个宽度下会切到
   **平板/窄屏布局** ⇒ 用户看到的**不是成品外观**，而"所见即所得"的全部意义就是
   "看到的就是成品"。
   ★★ 缩放用 `transform: scale`（由 JS 写），**不用 `zoom`** —— 实测 zoom 会让
     Playwright 的点击命中错位（真用户点元素也会点偏），因为
     ★ **iframe 的 CSS `width` 就是它的内部视口宽度**（不像普通 div 会被父容器挤压），
       所以 `width:1400px` 已经保证前台按**桌面版**渲染；`transform` 只负责视觉缩小，
       命中测试由浏览器按变换后的几何处理 ⇒ 点击位置正确。
   ★ `.ed3w__box` 是必要的：transform 不改变布局占位（元素仍占 1400px），
     所以外面包一层 `overflow:hidden` 的盒子，由 JS 把它设成**视觉尺寸**。 */
.ed3w__box { overflow: hidden; flex: 0 0 auto; position: relative; }
.ed3w__frame {
  border: 0; background: var(--ad-bg); display: block;
  width: 1400px; height: 800px;
  transform-origin: top left;
}
.ed3w__pv[data-dev="tab"] .ed3w__frame { width: 834px; }
.ed3w__pv[data-dev="phone"] .ed3w__frame { width: 390px; }
.ed3w__pv[data-dev="tab"] .ed3w__stage,
.ed3w__pv[data-dev="phone"] .ed3w__stage { padding: var(--sp-3); }
.ed3w__note {
  padding: var(--sp-2) var(--sp-3); border-top: 1px solid var(--ad-line);
  font-size: var(--fs-10); color: var(--ad-ink-2);
}
/* ---- W2 ⑥ 段名浮标（hover 报告段名，后台画、不进前台 DOM）----
   ★ `pointer-events:none` 是必须的：浮标压在 iframe 上方，若可命中，
     鼠标划到浮标上就离开元素 ⇒ 浮标闪一下又消失（自己把自己关掉的经典自锁）。 */
.ed3w__hint {
  position: absolute; z-index: 3; pointer-events: none;
  padding: 2px 6px; border-radius: var(--r-2);
  background: var(--ad-ink); color: var(--ad-bg);
  font-size: var(--fs-10); line-height: 16px; white-space: nowrap;
  max-width: 260px; overflow: hidden; text-overflow: ellipsis;
}
/* ---- A5 浏览态提示 ----
   ★ 放在**工具条**里而不是浮在画面上：浮在画面里会**压住前台导航**（目视截图逮到：
     提示条把 LOGO 与一级导航盖掉了一半）—— 浏览态的全部意义就是"像顾客一样看"，
     结果自己先把顾客要看的东西挡住了。 */
.ed3w__bstate {
  margin-left: var(--sp-3);
  padding: 2px 7px; border-radius: var(--r-2);
  background: var(--ad-ink); color: var(--ad-bg);
  font-size: var(--fs-10); letter-spacing: .02em; white-space: nowrap;
}
/* 悬停高亮（与"选中" `is-on` 区分开：`is-on` = 当前在编的那组，`is-hov` = 鼠标划过的那组） */
.ed-toc__i.is-hov { background: var(--ad-bg-sunk); }
.ed-toc__i.is-on.is-hov { background: var(--ad-ink); }
/* 右：字段面板 —— 复用 `.ed-live` 的容器外观，只补"一次只显示一组"与脚注位 */
.ed3w__fld .ad-fieldset { border: 0; box-shadow: none; padding: 0; }
/* ★★★ 窄栏里"多列字段组"必须落回单列（2026-09-28 目视截图逮到）：
   `.ad-fields--2/--3` 的折叠断点是按**视口宽度**判的（≤900px / ≤1100px），
   而视口有 1680px ⇒ 在 **440px 的右栏里它照样排两/三列** ⇒ 每列 ~200px，
   再减去 `.ad-fields--2 .ad-f` 的 **110px 标签列** ⇒ **控件只剩几十像素**，
   文字挤成**一字一行**（截图里"共 9 个可选值…"竖着排）。
   ⚠️ 这类错**所有断言都照样全绿** —— "挤"不违反任何一条几何/文本断言，
      只有**目视**能逮到（同族教训：§3 静默的错 · 「值算对了 ≠ 渲染对了」）。
   ⇒ 口径：**窄容器里由容器决定列数，不由视口决定**。这条只作用于丙三栏右栏，
      动不到其它页面（选择器带 `.ed3w__fld` 前缀）。 */
.ed3w__fld .ad-fields--2,
.ed3w__fld .ad-fields--3 { grid-template-columns: minmax(0, 1fr); }
/* 标签列 170 → 92px：440px 里要留给控件 ~310px，否则长控件（双语/选择器）放不下。 */
.ed3w__fld .ad-f { grid-template-columns: 92px minmax(0, 1fr); column-gap: var(--sp-3); }
.ed3w__fld .ad-f__label { font-size: var(--fs-10); letter-spacing: normal; }
.ed3w__fld .ad-f__hint { margin-top: var(--sp-1); }
.ed3w__fld .ad-fieldset__bar { position: sticky; top: 0; z-index: 2; background: var(--ad-bg); }
.ed3w__fld .ad-fieldset__title { font-size: var(--fs-12); }
/* 被选中的那组才有 `.is-cur`；其余 `hidden`（不是 `display:none` 手写，交给 hidden 属性） */
.ed3w__fld [hidden] { display: none !important; }
.ed3w__empty {
  padding: var(--sp-5) var(--sp-3); color: var(--ad-ink-2); font-size: var(--fs-11);
}
/* 左栏底部的操作区（保存/返回/历史）—— 原来是页面底部一整行，现在收进左栏 */
.ed3w__ops {
  display: grid; gap: var(--sp-2); margin-top: var(--sp-3);
  padding-top: var(--sp-3); border-top: 1px solid var(--ad-line);
}
/* 撤销 / 重做 —— 放在操作区最上面（"敢改"的入口） */
.ed3w__undo { display: flex; align-items: center; gap: var(--sp-2); }
.ed3w__undo .mini { flex: 0 0 auto; }
.ed3w__ucount { font-size: var(--fs-10); color: var(--ad-ink-2); margin-left: auto; }
.ed3w__ops .row { display: grid; gap: var(--sp-2); }
.ed3w__ops .btn { width: 100%; justify-content: center; }

/* ---------------------------------------------------------------- W3 ⑨ 行列表（分组下面的第二层）
   ★ 它挂在**选中的那条分组按钮下面**（由 `ed3wSel` 用 `insertBefore` 搬过去），
     所以左边留一道竖线把"属于上面那条"这件事画出来 —— 没有它两层会糊在一起。 */
.ed3w__rows {
  display: grid; gap: 1px;
  margin: var(--sp-1) 0 var(--sp-2) 14px;
  padding-left: var(--sp-3);
  border-left: 1px solid var(--ad-line);
}
.ed3w__row {
  display: flex; align-items: center; gap: var(--sp-2);
  min-width: 0; padding: 3px var(--sp-2) 3px 0;
  border-radius: var(--r-4); position: relative;
}
.ed3w__row:hover { background: var(--ad-bg-sunk); }
/* 隐藏态：整条**划掉 + 变淡**。★ 必须能一眼看出来 —— 被隐藏的行在预览里已经不见了，
   左栏是唯一还能把它找回来的地方，看不出状态就等于找不回来。 */
.ed3w__row.is-off { opacity: .55; }
.ed3w__row.is-off .ed3w__rlbl { text-decoration: line-through; }
.ed3w__rnum {
  flex: 0 0 16px; text-align: right;
  font: 400 var(--fs-10) var(--font-mono); color: var(--ad-ink-2); font-style: normal;
}
.ed3w__rlbl {
  flex: 1 1 auto; min-width: 0;
  font-size: var(--fs-11); font-weight: 400; color: var(--ad-ink-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ed3w__roff {
  flex: 0 0 auto; font-style: normal; font-size: var(--fs-10);
  color: var(--ad-ink-2); border: 1px solid var(--ad-line); border-radius: 3px;
  padding: 0 var(--sp-1);
}
/* 四件套：**平时不占位、悬停/键盘聚焦才出现**（同飞书"hover 才显动作"）。
   ⚠️ 必须 `position:absolute` —— 用 `visibility:hidden` 它**仍然占着 86px 宽**，
      在 196px 的左栏里把行标签挤成「建…」「光…」（目视截图逮到；同族教训
      「窄容器里的宽度由容器定」）。浮起来之后标签才有整行宽度。
   ⚠️ 用 `visibility` 而不是 `display:none` —— 后者会让按钮**收不进 Tab 序列**，
      键盘用户永远够不到（a11y 上是真缺陷，不是风格问题）。 */
.ed3w__rops {
  position: absolute; right: 2px; top: 50%; transform: translateY(-50%);
  display: flex; gap: 2px; padding-left: var(--sp-2);
  background: var(--ad-bg-sunk); border-radius: var(--r-4);
  visibility: hidden;
}
.ed3w__row:hover .ed3w__rops,
.ed3w__row:focus-within .ed3w__rops { visibility: visible; }
.ed3w__rops button {
  width: 20px; height: 20px; padding: 0; border: 0; border-radius: var(--r-4);
  background: transparent; color: var(--ad-ink-2); cursor: pointer;
  font: 400 11px/1 var(--font-mono);
}
.ed3w__rops button:hover { background: var(--ad-ink); color: var(--ad-bg); }
.ed3w__rops button:disabled { opacity: .25; cursor: default; background: transparent; color: var(--ad-ink-2); }
.ed3w__rops button[data-ed3w-op="del"]:hover { background: var(--ad-accent); color: var(--ad-white); }

/* ---------------------------------------------------------------- W3 ⑫ 手机随行（浮层小窗）
   ★ 浮层挂在舞台右下角：并排会把中间那栏挤窄 ⇒ 用"看得见手机"换来"看不清桌面"。
   ★ `position:absolute` 落在 `.ed3w__stage`（它已是定位上下文）里。 */
.ed3w__side {
  position: absolute; right: var(--sp-4); bottom: var(--sp-4); z-index: 4;
  width: 200px; padding: var(--sp-2);
  background: var(--ad-bg); border: 1px solid var(--ad-line);
  border-radius: var(--r-8); box-shadow: 0 8px 28px rgba(0, 0, 0, .16);
}
.ed3w__sidehd {
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-10); color: var(--ad-ink-2); margin-bottom: var(--sp-2);
}
.ed3w__sidehd b { font-weight: 500; letter-spacing: .02em; color: var(--ad-ink-2); }
.ed3w__sidehd button {
  width: 18px; height: 18px; padding: 0; border: 0; border-radius: var(--r-4);
  background: transparent; color: var(--ad-ink-2); cursor: pointer; font: 400 12px/1 var(--font-mono);
}
.ed3w__sidehd button:hover { background: var(--ad-ink); color: var(--ad-bg); }
.ed3w__sidebox { overflow: hidden; border-radius: 6px; border: 1px solid var(--ad-line); background: var(--ad-bg-sunk); }
/* 里面那块 iframe 是**真 390px 宽**（内联宽高由 `fitSide` 写），靠 scale 缩进这个小盒子里
   —— 与主预览同一套算法。`transform-origin` 必须左上，否则缩放会以中心为基准跑偏。 */
.ed3w__sideframe {
  display: block; border: 0; transform-origin: 0 0; background: var(--ad-bg);
}
.ed3w__sidenote { margin-top: var(--sp-2); font-size: var(--fs-10); color: var(--ad-ink-2); line-height: 1.4; }
/* 工具条那颗开关 */
.ed3w__sidetog {
  height: 22px; padding: 0 var(--sp-3); margin-right: var(--sp-2);
  border: 1px solid var(--ad-line); border-radius: var(--r-4); background: var(--ad-bg);
  color: var(--ad-ink-2); font-size: var(--fs-10); cursor: pointer;
}
.ed3w__sidetog:hover { color: var(--ad-ink); border-color: var(--ad-ink-2); }
.ed3w__sidetog.is-on { background: var(--ad-ink); border-color: var(--ad-ink); color: var(--ad-bg); }

@media (max-width: 1199px) { .ed3w { grid-template-columns: 176px minmax(0, 1fr) 360px; } }
@media (max-width: 1199px) {
  .ed3w { grid-template-columns: minmax(0, 1fr); }
  .ed3w__pv, .ed3w__fld { position: static; max-height: none; }
  .ed3w__pv { order: 1; } .ed3w__nav { order: 2; } .ed3w__fld { order: 3; }
  .ed3w__frame { height: 70vh; }
}

/* ==========================================================================
   ★ 2026-09-28：单据页骨架（汇总页四段 ＋ 开单页编辑体验）
   --------------------------------------------------------------------------
   骨架照余总 19:2x 给的 10 张参考图（秒账 7 张 ＋ 汇总页/列设置 3 张），
   **视觉守赫蒙** —— 参考图那套 Ant 蓝 / 8px 圆角 / 阴影卡片一律没抄，
   一律走 tokens.css 的暖白底 / 近黑字 / 品牌红 / 圆角只 1·2·4 / 字重只 300·400·500。
   ★ 这套骨架的**归属**：七个单据页共用（报价单 / 订单 / 销售单 / 采购单 /
     加工单 / 退货单 / 库存），换列配置就是新页。
   ========================================================================== */

/* ---------------------------------------------------------------- 金额三列（汇总页顶） */
.ad-money3 {
  display: flex; gap: 1px; margin-bottom: var(--sp-4);
  background: var(--ad-line); border-radius: var(--r-4); overflow: hidden;
  box-shadow: 0 0 0 1px var(--ad-line);
}
.ad-money3 > div { flex: 1; background: var(--ad-bg); padding: 10px var(--sp-4); }
.ad-money3 .k { font-size: var(--fs-11); color: var(--ad-muted-ink); letter-spacing: .04em; }
.ad-money3 .v { font-size: var(--fs-18); font-weight: var(--fw-medium); margin-top: 2px;
  font-variant-numeric: tabular-nums; }
.ad-money3 .v.is-red { color: var(--ad-err-text); }

/* ---------------------------------------------------------------- ① 筛选条 */
.ad-filter {
  display: flex; align-items: flex-end; gap: var(--sp-3); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--ad-line);
}
.ad-filter__fld { display: flex; flex-direction: column; gap: var(--sp-1); }
.ad-filter__fld > label { font-size: var(--fs-11); color: var(--ad-muted-ink); letter-spacing: .04em; }
.ad-filter__fld input, .ad-filter__fld select {
  font: inherit; font-size: var(--fs-12); height: 28px; padding: 0 var(--sp-2);
  border: 1px solid var(--ad-field-line); border-radius: var(--r-2);   /* ★ AA 1.4.11 */
  background: var(--ad-bg); color: var(--ad-ink); min-width: 130px;
}
.ad-filter__fld input:focus, .ad-filter__fld select:focus { outline: none; border-color: var(--ad-ink); }
.ad-filter__sp { flex: 1; }

/* ---- ★★ 批 G16（2026-10-05）：筛选条**可收起**（条件多时省一屏） ----
   ★ 用**整格显隐**（`display:none`），**绝不用 `display:contents`** —— 血案：
     `display:contents` 让包装元素不生成盒子，label 与 input 变成两个独立栅格项，
     换行时被拆散（label 留上一行、输入框甩下一行）。样机阶段踩过。 */
.ad-filter:not(.is-open) .ad-filter__fld--more { display: none; }
.ad-filter__tog { display: inline-flex; align-items: center; gap: 5px; }
.ad-filter__tog svg { transition: transform .2s ease; }
.ad-filter.is-open .ad-filter__tog svg { transform: rotate(180deg); }

/* ---------------------------------------------------------------- ② 工具条 */
.ad-toolbar {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--ad-line);
}
.ad-toolbar__sp { flex: 1; }

/* ---------------------------------------------------------------- ③ 分页 */
.ad-pager {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4); font-size: var(--fs-12); color: var(--ad-muted-ink);
}
.ad-pager__sp { flex: 1; }
.ad-pager select {
  font: inherit; font-size: var(--fs-12); height: 26px; padding: 0 var(--sp-1);
  border: 1px solid var(--ad-field-line); border-radius: var(--r-2);   /* ★ AA 1.4.11 */
  background: var(--ad-bg);
}
.ad-pager__pg { display: inline-flex; gap: 2px; }
.ad-pager__pg button {
  min-width: 26px; height: 26px; padding: 0 7px; cursor: pointer;
  font: inherit; font-size: var(--fs-12); color: var(--ad-muted-ink);
  border: 1px solid var(--ad-line); border-radius: var(--r-2); background: var(--ad-bg);
}
.ad-pager__pg button:hover { border-color: var(--ad-ink); color: var(--ad-ink); }
.ad-pager__pg button.is-on { background: var(--ad-ink); border-color: var(--ad-ink); color: var(--ad-bg); }

/* ---------------------------------------------------------------- 列设置浮层（挂工具条右侧） */
.ad-cols__wrap { position: relative; display: inline-flex; }
/* ★★ R7-Q4 · A 案：`absolute`（挂宿主）→ `fixed`（挂 body，定位交给 JS `adFloat()`）。
   铁证：宿主 `.ad-panel{overflow:hidden}` 把报表页「明细列设置」裁掉 65px、
   还越出视口下缘 48px —— 这就是余总截图里那个现象。
   ★ 挂 body 之后**必须** `fixed`：`absolute` 挂 body 会随页面滚动一起飘，
     浮层一滚就跟触发按钮分家（`.sp-tip` 早踩过这个坑，所以它也是 `fixed`）。 */
.ad-cols {
  position: fixed; z-index: 60;
  width: 320px; background: var(--ad-bg); border-radius: var(--ad-dlg-r);
  box-shadow: var(--ad-dlg-sh);
  border: 1px solid var(--ad-line); overflow: hidden;
}
.ad-cols__h { padding: 10px var(--sp-3); border-bottom: 1px solid var(--ad-line);
  font-size: var(--fs-12); font-weight: var(--fw-medium); display: flex; align-items: center; }
.ad-cols__sec { padding: 10px var(--sp-3); border-bottom: 1px solid var(--ad-line); }
.ad-cols__sec h4 { margin: 0 0 var(--sp-2); font-size: var(--fs-11); color: var(--ad-muted-ink);
  font-weight: var(--fw-normal); letter-spacing: .04em; }
.ad-cols__f { padding: 10px var(--sp-3); display: flex; gap: var(--sp-2); justify-content: flex-end; }
/* ★★ R7·Q8：这一套原本是**裸 `.ad-chip`**、靠「后到者胜」把上面所有同名规则全盖掉
   （圆角 999px → 2px、还叠一层 box-shadow 描边环 ⇒ 全站 chip 长成"双层方角"）。
   现**加 `.ad-cols` 父作用域** —— 只管列设置浮层里那些**密集的列名标签**（不跟着 Q8 变大）。 */
.ad-cols .ad-chip {
  display: inline-flex; align-items: center; gap: 6px; margin: 0 6px 6px 0;
  font-size: var(--fs-11); padding: 3px 7px; border-radius: var(--r-2);
  box-shadow: 0 0 0 1px var(--ad-line-2); background: var(--ad-bg); cursor: default;
  color: var(--ad-ink-2);
}
.ad-cols .ad-chip button {
  border: 0; background: none; padding: 0; cursor: pointer; font: inherit;
  font-size: var(--fs-11); color: var(--ad-muted-ink); line-height: 1;
}
.ad-cols .ad-chip button:hover { color: var(--ad-accent); }
.ad-cols .ad-chip button[disabled] { opacity: .3; cursor: default; }
.ad-cols .ad-chip--off { color: var(--ad-muted-ink); background: var(--ad-bg-alt); box-shadow: 0 0 0 1px var(--ad-line); }
.ad-sw { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-12);
  color: var(--ad-ink-2); margin: 5px 0; cursor: pointer; }
.ad-sw input { margin: 0; }
/* ============================================================================
   ★★★ 2026-10-04（批 T5 · 需求2）**整行可点进详情** —— 六域撤「打开」按钮后的入口
   ----------------------------------------------------------------------------
   ★ 余总原话：「**去掉找开关按钮 改整行可点击**」⇒ 行上挂 `.is-openable`（`docListHTML` 产出），
     真正的跳转在 `bindClicks` 的 `[data-d-row]` 分支（勾选框 / 单号链接 / 选中文字三处放行）。
   ★ 光标语义要**分别说清**：整行是 `pointer`（可点），但
     勾选框 / 输入框 / 单号链接**各自恢复自己的光标** —— 否则「能点」和「能选/能敲」在视觉上分不开。
   ========================================================================== */
.ad-table tbody tr.is-openable { cursor: pointer; }
.ad-table tbody tr.is-openable td.ad-rowchk,
.ad-table tbody tr.is-openable td.ad-rowchk input { cursor: default; }
.ad-table tbody tr.is-openable td input,
.ad-table tbody tr.is-openable td select,
.ad-table tbody tr.is-openable td textarea { cursor: auto; }
.ad-table tbody tr.is-openable td.ad-ellink > a { cursor: pointer; }

/* ============================================================================
   ★★★ 2026-10-04（批 T5 · 需求3）**列设置 = 一条可拖列表**（显示列 / 隐藏列两区合并）
   ----------------------------------------------------------------------------
   ★ 余总原话：「**改为拖拽设置 去掉小图标**」＋ 拍板「两区合并可拖」「两处都改、样式共用」。
   ★ 一套样式，**列表页列设置**与**明细列设置**共用（渲染共用 `colListHTML()`）。
   ★ 交互：**拖 = 排序**（`.ad-colist__i` 整行可抓）· **点 = 显隐**（没挪过就算点）。
     ⇒ 列表里**一枚图标都没有**（原来那排 ↑ ↓ × ＋ 全撤）。
   ★ 隐藏列**留在原位、只变淡** —— 合并列表的前提：它要是挪走，"拖到哪儿"就没有参照了。
   ========================================================================== */
.ad-colist { display: flex; flex-direction: column; gap: 2px;
  max-height: 300px; overflow-y: auto; }
.ad-colist__i { display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) 6px; border-radius: var(--r-2); cursor: grab;
  font-size: var(--fs-12); color: var(--ad-ink); user-select: none; }
.ad-colist__i:hover { background: var(--ad-bg-alt); }
.ad-colist__i input { margin: 0; flex: none; cursor: pointer; }
.ad-colist__t { flex: 1; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
/* ★ 隐藏列：**留在原位、只变淡**（不划掉、不挪走 —— 见上面口径）。 */
.ad-colist__i.is-off .ad-colist__t { color: var(--ad-muted-ink); opacity: .7; }
/* 正在拖的那一条：加一档底 ＋ 轻微透明（SAGE 铁律：**零阴影**）。 */
.ad-colist__i.is-dragging { background: var(--ad-bg-alt); opacity: .85; }
body.ad-colidnd, body.ad-colidnd .ad-colist__i { cursor: grabbing; }
body.ad-colidnd { user-select: none; }

/* ============================================================================
   ★★★ 2026-10-04（批 T5 · 需求1）**表头列筛选** —— 列名后的漏斗 ＋ 取值勾选浮层
   ----------------------------------------------------------------------------
   ★ 余总原话：「**表头加筛选功能**」（箭头指「销售单号 … 操作」那一行）⇒ A1 案：
     **列名后挂一枚漏斗图标**，点开浮层做该列筛选（多选，AND 进 `docApplyFilter` 那条链）。
   ★ 三条口径：
     ① `.ad-thfw` 是 th **内层**的 inline-flex 壳 —— **绝不给 th 写 display**
        （单元格脱离表格布局 ⇒ 静默空档 ＋ 边框断开，本项目踩过）。
     ② 浮层**复用 `.ad-cols` 那一套**（同列设置，不另立第二套样式）。
     ③ SAGE 口径：零阴影、暖纸底、橄榄墨；**「已筛」态 = 橄榄实底小方块**
        （离散、无阴影、不与主色抢），不是亮色文字。
   ========================================================================== */
/* ★★ 2026-10-06 修（余总报「冻结图标和筛选图标重叠」）：**外壳由 `.ad-thf` 改名为 `.ad-thfw`**。
   原外壳与批 J4 那枚通用表的**漏斗按钮**（本文档 680 行的 `.ad-thf`）**同名撞车** ——
   J4 的 `width/height:16px · margin-left:4px · vertical-align:-3px · opacity:.5` 全部残留，
   本批（T5）只覆盖了 display/align-items/gap ⇒ 壳被压成 16×16、「列名＋漏斗」向右溢出 59px，
   而图钉按壳的 16px 右缘排 ⇒ 两者压在一起（实测重叠 12px）；顺带把表头文字拖成半透明。
   ★ 改名而非「加 `[data-adf]` 限定」：同名两义是**不报错的静默错**（本项目铁律），换名才根治。 */
.ad-thfw { display: inline-flex; align-items: center; gap: 3px; }
.ad-thf__t { white-space: nowrap; }
.ad-thf__b {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; padding: 0; border: 0; border-radius: var(--r-2);
  background: transparent; color: var(--ad-muted-ink); cursor: pointer; flex: none;
}
.ad-thf__b:hover { color: var(--ad-ink); background: var(--ad-bg-sunk); }
.ad-thf__b:focus-visible { outline: 2px solid var(--ad-ink); outline-offset: 1px; }
/* ★ 「已筛」态：橄榄实底 ＋ 纸底字 —— 离散、无阴影（SAGE 铁律）。 */
.ad-thf__b.is-on { background: var(--ad-accent); color: var(--ad-bg); }
.ad-thf__b.is-on:hover { background: var(--ad-accent); color: var(--ad-bg); }
/* 被筛过的整列：给列名一点提示（不抢、不加粗变色以免破「表头统一」）。 */
.ad-table th > .ad-thfw > .ad-thf__t { color: var(--ad-muted-ink); }
.ad-table th > .ad-thfw:has(.ad-thf__b.is-on) > .ad-thf__t { color: var(--ad-ink); }

/* 列筛选浮层：比列设置窄（它只是一串取值），其余完全沿用 `.ad-cols`。 */
.ad-cols--filt { width: 248px; }
.ad-cols--filt .ad-cols__sec { max-height: 264px; overflow-y: auto; }
.ad-sw--filt { display: flex; align-items: center; gap: var(--sp-2); margin: var(--sp-1) 0; }
.ad-sw--filt .ad-sw__t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-size: var(--fs-12); color: var(--ad-ink-2); }
.ad-sw--filt input:checked ~ .ad-sw__t { color: var(--ad-ink); }
.ad-sw--filt .ad-sw__n { flex: none; font-size: var(--fs-11); color: var(--ad-muted-ink);
  font-variant-numeric: tabular-nums; }


/* ---------------------------------------------------------------- 开单页：单头四列栅格 */
.ad-headgrid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-3) var(--sp-4); }
.ad-headgrid > .f { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.ad-headgrid > .f > label { font-size: var(--fs-11); color: var(--ad-muted-ink); letter-spacing: .04em; }
.ad-headgrid > .f.w2 { grid-column: span 2; }
.ad-headgrid > .f.w4 { grid-column: span 4; }
.ad-headgrid input, .ad-headgrid select, .ad-headgrid textarea {
  font: inherit; font-size: var(--fs-12); height: 30px; padding: 0 var(--sp-2); width: 100%;
  border: 1px solid var(--ad-field-line); border-radius: var(--r-2);   /* ★ AA 1.4.11 */
  background: var(--ad-bg); color: var(--ad-ink);
}
.ad-headgrid textarea { height: auto; min-height: 52px; padding: 6px var(--sp-2); resize: vertical; line-height: 1.6; }
.ad-headgrid input:focus, .ad-headgrid select:focus, .ad-headgrid textarea:focus { outline: none; border-color: var(--ad-ink); }
.ad-headgrid input[readonly] { background: var(--ad-bg-alt); color: var(--ad-muted-ink); }
.ad-headgrid .f--unit { position: relative; }

/* 敲字带候选（开单速度之一）
   ★ R7-Q4 · A 案：`absolute`（挂 `.f--unit`）→ `fixed`（挂 body）。
     宽度原来靠 `left:0;right:0` 跟宿主持平 ⇒ 挂 body 后改由 JS 写死 = 触发输入框的宽。
     ★ `max-height` 从**写死 168** 改成「距视口底的可用高」（JS clamp）——
       写死就会在窗口矮时被压到底边外面（同族的裁剪隐患）。 */
.ad-sug {
  position: fixed; z-index: 55;
  background: var(--ad-bg); border: 1px solid var(--ad-ink); border-radius: var(--r-2);
  box-shadow: var(--ad-dlg-sh); max-height: 168px; overflow: auto;
}
.ad-sug > div { padding: 6px var(--sp-2); font-size: var(--fs-12); cursor: pointer;
  display: flex; gap: var(--sp-2); align-items: baseline; }
.ad-sug > div + div { border-top: 1px solid var(--ad-line); }
.ad-sug > div:hover, .ad-sug > div.is-cur { background: var(--ad-bg-alt); }
.ad-sug .s { margin-left: auto; color: var(--ad-muted-ink); font-size: var(--fs-11); white-space: nowrap; }
.ad-sug .hl { color: var(--ad-accent); }
.ad-sug .none { color: var(--ad-muted-ink); cursor: default; }
/* ★ 候选里最后那条「＋ 新建客户」（余总 2026-09-29：档案里没有就**就地建**一条）——
   它必须**看起来不是一个已有客户**：点下去开的是表单，不是选中一条记录。 */
.ad-sug > div.is-new { color: var(--ad-accent); font-weight: var(--fw-medium); }
.ad-sug > div.is-new:hover { background: var(--ad-bg-alt); }

/* ★★★ 2026-09-29（批 18）：
   ① **删掉 `.ad-lines__tip`（键盘快捷键说明条）** —— 余总原话「这些说明文字也去掉」。
      它原本是"键盘流"的图例，但 ⑥ 落地后"末行 Enter 自动加一行"已经**自我说明**，
      铺在表头上天天占一行高、每次开单都看见一次，不如去掉。
   ② 行表操作列从**末列挪到第一列** ＋ 换成**内联 SVG 小图标**（余总：「操作＝复制 / 上移 / 下移 / 删除，
      全部内联 SVG 换一种更好看的」）⇒ 新的 `.ad-lops` 一族（见下）。
   ③ 链接一律**不上划线**（余总定死），改由**颜色**区分（令牌 `--ad-link`）。 */

/* ---- 链接：定死规则「全站链接一律不用下划线」----
   ⚠️ 连 hover 态也不给下划线 —— 悬停反馈走**颜色加深**。
   范围 = 整个后台（admin.css 只发给后台，前台不吃这份）。 */
a, a:hover, a:focus, a:active { text-decoration: none; }
a { color: var(--ad-link); }
a:hover { color: var(--ad-link-2); }

/* ---- 明细行「操作」列（第一列）：四个内联 SVG 小图标 ----
   ★ 命中区 **22×22**（图标 15px ＋ 内边距）—— 15px 裸图标难点中，这是**可用性**不是美观：
     一行要连点「上移」几次时，命中区小一半就是点不中、点错行。
   ★ 三态色：默认灰 → 悬停近黑 → **删除**悬停红（走令牌，不写死）。 */
.ad-lops { display: flex; align-items: center; gap: 1px; }
.ad-lop {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; padding: 0;
  border: 0; border-radius: var(--r-1); background: none; cursor: pointer;
  color: var(--ad-muted-ink);
  transition: background var(--tr), color var(--tr);
}
.ad-lop:hover { color: var(--ad-ink); background: var(--ad-bg-alt); }
.ad-lop--danger:hover { color: var(--ad-err-text); background: var(--ad-err-bg); }
.ad-lop svg { display: block; }
/* 六域的行表用 `.ad-table`，报价单页用 `table.ad-lines` —— 两套都要给列宽，否则会被挤成 0。 */
table.ad-lines td.ad-lops__cell,
table.ad-table td.ad-lops__cell { width: 96px; white-space: nowrap; padding: 0 var(--sp-1); }

.ad-lines__scroll { overflow-x: auto; }
table.ad-lines { width: 100%; border-collapse: collapse; font-size: var(--fs-td); min-width: 1080px; }
/* ★★ R7 · Q3（S7）：报价单页明细表头 30 → **32** —— 余总 2026-09-30 23:5x 定
   「明细表头 = 明细行内控件 32」⇒ 表头与格内控件同高，一条口径。
   ⚠️ 六域行表走的是**另一套**（`.ad-table--lines th`，见下方同批段）——
      只改这里 = 六域表头还是原样（「改了≠改全了」的老坑）。 */
table.ad-lines th {
  text-align: left; font-weight: var(--fw-medium); font-size: var(--fs-th);
  color: var(--ad-th-ink); letter-spacing: .04em; white-space: nowrap;   /* ★ 2026-10-07 */
  height: 32px; padding: 0 var(--sp-2); border-bottom: 1px solid var(--ad-line);
  background: var(--ad-bg-alt);
}
/* ★★ R7 · Q4（兑现 Q3-S8「明细行高」）—— 报价单明细行与**六域明细行对齐**：
   · 六域 `.ad-table--lines td` 早已跟 `--row-h`（中档 48）；报价单这边却**写死 34px**。
   · 34 是"税列被 54 的多选框顶着时"的遗留值 —— 税控件收成小标签后，写死 34 只会让
     同一种明细行在**两张页面上不一样高**（「两套必须同改」正是 Q3 §15.4 那条老坑）。
   · 格内控件同步收成 Q2 定的**紧凑档 32**（`--ad-field-h-dense`；原来是 33，属 §14.6「未动的两处」）。
   ★ `vertical-align: middle` 让 32 的控件在 48 的行里**上下各留 8**（与六域同一观感）。 */
table.ad-lines td {
  padding: 0; height: var(--row-h); vertical-align: middle;
  border-bottom: 1px solid var(--ad-line);
}
table.ad-lines td input, table.ad-lines td select {
  /* ★ 2026-10-07：明细格内输入框的字也是「表内」⇒ 跟格子一起 12px。 */
  font: inherit; font-size: var(--fs-td); width: 100%; height: var(--ad-field-h-dense);
  padding: 0 var(--sp-2);
  border: 0; background: transparent; color: var(--ad-ink); border-radius: 0;
}
table.ad-lines td input:focus, table.ad-lines td select:focus {
  outline: 2px solid var(--ad-ink); outline-offset: -2px; background: var(--ad-bg);
}
table.ad-lines td input[readonly] { color: var(--ad-muted-ink); }
table.ad-lines td.num input { text-align: right; font-variant-numeric: tabular-nums; }
table.ad-lines td.num.is-ro { text-align: right; padding: 0 var(--sp-2);
  font-variant-numeric: tabular-nums; color: var(--ad-ink-2); }
/* ★ 2026-10-07 ：行号列**不写死字号**，让它继承 —— `th.rowno`得 14px、`td.rowno`得 12px。
   （原写死 `--fs-11`=13px，两边都不对；这是 `check_fs.py` ⑤ 在 `#/quotes/__new__` 上逮到的。） */
table.ad-lines .rowno { width: 30px; text-align: center; color: var(--ad-muted-ink);
  cursor: default; }
table.ad-lines tr.is-ghost td input { color: var(--ad-muted); }
table.ad-lines tr.is-ghost td { background: var(--ad-bg); }
/* ★ 2026-09-29（批 18）：原 `td.del`（末列三个文字箭头 ↑↓×）已作废 →
   操作列改为**第一列**的 `.ad-lops` 内联 SVG 图标（见上面那一族规则）。 */
.ad-lines__foot {
  display: flex; justify-content: flex-end; gap: var(--sp-6);
  padding: var(--sp-4); border-top: 1px solid var(--ad-line);
}
.ad-lines__foot .t { text-align: right; }
/* ★ 2026-09-30（⑥）：`.k` 里现在除了文字还有 `<em>`（口径副标）和 `?` 提示（hintHTML）
   ⇒ 给成 flex 让它们并排右对齐，别各占一行。 */
.ad-lines__foot .k { font-size: var(--fs-11); color: var(--ad-muted-ink); letter-spacing: .04em;
  display: flex; align-items: center; gap: 3px; justify-content: flex-end; }
.ad-lines__foot .k em { font-weight: 400; font-style: normal; color: var(--ad-muted); }
.ad-lines__foot .v { font-size: var(--fs-15); font-weight: var(--fw-medium); font-variant-numeric: tabular-nums; }
.ad-lines__foot .t--grand .v { font-size: var(--fs-22); }

/* ★★ 2026-09-30（⑥ · 金额行段）：`docEditHTML` 那套骨架的**金额行** ——
   照秒账「② 单据体」，明细下面一条右对齐的汇总行（独立块，不像报价单页那样贴在行表底）。
   ★ 取值口径与 `.ad-lines__foot` **同一份**（`qMoneyParts` / `moneyRow*`）——
     只有外壳不同，数字不会有两个来源。 */
.ad-moneyrow { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: var(--sp-6);
  padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-4);
  background: var(--ad-bg-sunk); border: 1px solid var(--ad-line); border-radius: var(--r-8); }
.ad-moneyrow .t { text-align: right; }
.ad-moneyrow .k { font-size: var(--fs-11); color: var(--ad-muted-ink); letter-spacing: .04em;
  display: flex; align-items: center; gap: 3px; justify-content: flex-end; }
.ad-moneyrow .k em { font-weight: 400; font-style: normal; color: var(--ad-muted); }
.ad-moneyrow .v { font-size: var(--fs-15); font-weight: var(--fw-medium); font-variant-numeric: tabular-nums; }
.ad-moneyrow .t--grand .v { font-size: var(--fs-22); }

/* 行表微调（★ 2026-09-29 批 18：操作列已挪到**第一列**，原末列 `.del` 的宽度账作废） */

/* ==========================================================================
   ★★ R7 · Q4 · ⑤：税控件 —— `select[multiple size=2]` → 「小标签 ＋ 弹层」
   --------------------------------------------------------------------------
   ★★ **为什么要换掉原生多选**：`select[multiple size="2"]` 天生**两行高**
      （54px），顶在明细行里 ⇒ 整行被撑到 **58px**（Q3 实测，余总截图里那行明显高出一截）。
      原注释那句「宁可让行高一点，也不要看起来坏了」—— 这是**在错误的前提里做取舍**：
      真正该做的是**不要两行高的控件**。⇒ 换成「行内一枚小标签 ＋ 点开弹层勾选」：
        · 行内只占一格高（32），行高回 **48**（兑现 Q3-S8，判据 `[X9]` / `check_q3_ui [W12]`）；
        · 弹层挂 body（`adFloat()`）⇒ 不撑行、不被 `.ad-lines__scroll` 裁。
   ★ 这也是**第 5 只跟随型浮层**（Q3 §15.5 移交过来的那条）。
   ★ 值语义**一个字没改**：还是「一行可多税率」⇒ 仍写进 `quote_line_tax`。
   ========================================================================== */
.ad-tax { display: flex; align-items: center; }
.ad-tax__btn {
  width: 100%; height: var(--ad-field-h-dense);
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-1);
  font: inherit; font-size: var(--fs-12);
  border: 1px solid var(--ad-line); border-radius: var(--ad-r-field);
  background: var(--ad-bg); color: var(--ad-ink-2); cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ad-tax__btn:hover { border-color: var(--ad-ink-2); color: var(--ad-ink); }
.ad-tax__btn.is-empty { color: var(--ad-muted-ink); }
.ad-tax__btn.is-on { border-color: var(--ad-accent); color: var(--ad-ink); }

/* 税弹层（`#adTaxPop`，body 下单例，由 `adFloat()` 定位） */
.ad-taxpop {
  position: fixed; z-index: 60;
  width: 200px; padding: var(--sp-2) 0;
  background: var(--ad-bg); border: 1px solid var(--ad-line);
  border-radius: var(--ad-dlg-r); box-shadow: var(--ad-dlg-sh);
  max-height: 240px; overflow-y: auto;
}
.ad-taxpop__h {
  padding: var(--sp-1) var(--sp-3) var(--sp-2);
  font-size: var(--fs-11); color: var(--ad-muted-ink); letter-spacing: .04em;
}
.ad-taxpop__o {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 5px var(--sp-3); font-size: var(--fs-12); color: var(--ad-ink-2); cursor: pointer;
}
.ad-taxpop__o:hover { background: var(--ad-bg-alt); }
.ad-taxpop__f {
  display: flex; justify-content: flex-end;
  padding: var(--sp-2) var(--sp-3) 0; margin-top: var(--sp-1);
  border-top: 1px solid var(--ad-line);
}

/* 列设置浮层：选项多时（显示列 ＋ 隐藏列 ＋ 打印列）会超出视口，
   底部「恢复默认 / 完成」就点不到了 ⇒ 限高 + 内部滚动。 */
.ad-cols { max-height: 72vh; overflow-y: auto; }

/* ==========================================================================
   ★★ 2026-09-28 22:4x：后台作用域收口 —— 色系落地的最后一环
   --------------------------------------------------------------------------
   后台的 CSS 链是：tokens.css → base.css → components.css → admin.css
   其中 base.css / components.css 是**前台与后台共用**的（前台 20+ 个页面都在引）
   ⇒ **不能**去改它们里面的 var(--c-*)：改了前台会跟着变。
   这里改用**变量重定向**收口：在后台的根容器上把前台层那几个令牌重指到后台层。
   CSS 变量按继承解析 ⇒ base.css / components.css 里那 **141 处** var(--c-*)
   在后台页面上会全部拿到 --ad-* 的值：**一处声明、全场生效，前台零影响**。
   ★ 这才是「把画廊的规矩搬进工作台」的最后一刀 —— 连共享层也一起收口，
     否则后台会「自己这一半是新色系、共享层那一半还是旧暖白」（首跑就逮到了：
     ★★ 2026-10-03 SAGE 换皮后：body 底已由 `--ad-bg` 接住 ⇒ **#FAFBF4**（暖纸））。
   ⚠️ 以后 --ad-* 加了新令牌，这里同步加一行；守卫 = `v2/tools/_audit_wcag.py`。
   ========================================================================== */
body {
  /* 前台层令牌 → 后台层（只在后台作用域内覆盖，前台一个值都不动） */
  --c-bg:          var(--ad-bg);
  --c-bg-alt:      var(--ad-bg-alt);
  --c-bg-sunk:     var(--ad-bg-sunk);
  --c-line:        var(--ad-line);
  --c-line-2:      var(--ad-line-2);
  --c-ink:         var(--ad-ink);
  --c-ink-2:       var(--ad-ink-2);
  --c-muted:       var(--ad-muted);
  --c-muted-2:     var(--ad-ink-2);      /* ★ 2.16:1 那档在后台彻底废掉 */
  --c-muted-ink:   var(--ad-muted-ink);
  --c-white:       var(--ad-white);
  --c-accent:      var(--ad-accent);     /* ★ 品牌红：两层同值 */
  --c-accent-ink:  var(--ad-white);
  --c-accent-soft: var(--ad-acc-bg);
  --c-ok:          var(--ad-ok);
  --c-warn:        var(--ad-warn);
  --c-err:         var(--ad-err);
  --c-zebra:       var(--ad-zebra);
  /* --c-dark-* 不重定向：后台没有深色区块，那三条留给前台的 .section--dark。 */

  /* ★★ R7·Q7（余总 2026-10-01 13:3x 拍「丙案 · 整站去灰」）：
     页面底由**次级层 `--ad-bg-alt`** 提为**卡片白**。
     连带四处同批提白：`body`（此处）· `.ad-nav` · `.ad-tabbar` · `.ad-note`。
     后果（已报余总）：① 卡片与页底之间不再靠底色分层，改靠 `--ad-line` 1px 边线；
     ② 判据「层级：页面底 vs 卡片底 ≥1.05」**换靶**为「卡片外框 vs 白底 ≥1.2」（见 `_audit_wcag.py`）。
     `--ad-bg-alt` 令牌本身**不动**，仍用于表头底 / 只读底 / 悬停底。 */
  background: var(--ad-bg);
  color: var(--ad-ink);
}

/* ★ 共享层的 .badge--ok 底色是**硬编码的旧绿** `rgba(46,125,91,.12)`（前台旧 --c-ok
   #2E7D5B 的 12% 版），文字却已重定向到 --ad-ok ⇒ 一深一浅对不上。
   这两条只写在 admin.css 里 ⇒ 只影响后台（前台仍用共享层原样）。 */
.badge--ok { background: var(--ad-ok-bg); }
.badge--draft { background: var(--ad-bg-sunk); }

/* ---- 字典徽标·自动色板（批「状态 chip」，2026-10-12）-------------------------
   `.badge--d0 ~ .badge--d4` 共 **5 档**，只给 `ADFMT.dictTag`（客户分类 / 销售阶段
   这类「档位字典」列）用。★ 档数须与 `admin.js` 的 `AD_TAG_N` **同值**（改就两处同改）。
   ★★ 为什么是「按字典 order 排序后循环取色」而不是「每档配一个色」：
      铁律 ⑬「档位类不许定死」⇒ 颜色**不进字典数据**、也**不按 key 硬编码** ——
      否则余总在字典页加一档 / 调个序，颜色就跟内容脱节。这里只认「第几档」。
   ★ 顺序**刻意打散色相**（浅绿 → 暖砂 → 橄榄 → 中性 → 淡红）：徽标在列表里
      是按档位挨着出现的，同色系相邻会看着像「没生效」（正是本项目最常见的假绿）。
   ★ `--d3`（中性灰）= 字典里**查不到**的值（孤儿值 / 同义写法）落的档。
   ⚠️ 只加新变体，**不动**上面 `.badge` 基类、既有 `--ok` / `--draft` 与共享层 `--accent`
      —— 那三档是单据状态 / CRM 已有页面在用的，动它们得另走一轮截图验收。
   ★ 实测（`_tmp/_tag_selftest.js`，2026-10-12）：**5 档底 vs 字的对比度全部 ≥ AA**
      （11.95 / 6.95 / 5.79 / 7.78 / 5.26 : 1）。
      ⚠️ **但要如实记一条**：`--d0` 底 #F1FED3 与卡片底 #FAFBF4 只有 **1.016:1** ⇒
      它几乎是「无底色」的（字仍清楚）。这是**刻意保留**的 —— `.badge` 基类与既有三变体
      **全都没有边框**（全站 20+ 处已在用），单独给 dictTag 加边就成了「同一页两套徽标」。
      ⇒ 「要不要给整套 `.badge` 加 1px 描边」**不在本批擅自决定**，正是总表第 2 条那笔欠账
      （「等你点头时一并办掉」）⇒ 本批把实测数摆在这儿，**留给余总验收时圈**。 */
.badge--d0 { background: var(--ad-acc-bg);  color: var(--ad-accent); }     /* 浅绿（底最淡，见上注） */
.badge--d1 { background: var(--ad-warn-bg); color: var(--ad-warn-text); }  /* 暖砂 */
.badge--d2 { background: var(--ad-ok-bg);   color: var(--ad-ok-text); }    /* 橄榄 */
.badge--d3 { background: var(--ad-bg-sunk); color: var(--ad-ink-2); }      /* 中性·兜底 */
.badge--d4 { background: var(--ad-err-bg);  color: var(--ad-err-text); }   /* 淡红 */

/* ============================================================================
   打印设置（「站点设置 → 打印设置」分区）
   ★ 2026-09-30 R2：从「一张报价单的两栏清单」改成 **8 类单据 × 照单排** ——
     面板直接复刻打印件的**四个区**（品牌 / 表头 / 明细 / 条款），
     表头横排、明细是真 `<table>` 且**列序 = 打印列序**（余总两轮圈图定稿）。
   ★★ 三条硬约束（都是实测量出来的，改版时**别丢**）：
      ① 明细表必须 `border-collapse:separate` —— 用 `collapse` 时 Chrome 的表格**实宽**
         会比 `width:100%` 多出「所有列边框之和」（11 列 = **+12px**）⇒ 外面那个
         `overflow-x:auto` 的壳子平白多出一条横向滚动条。
      ② 长文本（key 标注）必须 `word-break:break-all` 折行 ——
         `overflow:hidden + text-overflow:ellipsis` **只剪"画"、不剪布局宽度**，
         被截掉的字符照样计入 `scrollWidth`，th / tr / table 一路被顶出去。
      ③ 表格里的复选框**显式定死 16×16** —— Chromium 原生复选框实测 **20px**（不是 13），
         11 列时第一格（内宽 86）就会**静默溢出**。
   ★ `.is-off` 整体压暗 —— 一眼看出「这条不印」，不靠去读复选框的勾。
   ============================================================================ */
.ad-pfpane[hidden] { display: none; }
.ad-pfstat { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  margin: var(--sp-4) 0 var(--sp-3); }
.ad-pfstat .badge { padding: 2px var(--sp-2); border-radius: var(--r-2); font-size: var(--fs-11); }
.ad-pfstat__t { font-size: var(--fs-12); color: var(--ad-ink-2); }
.ad-pfstat__src { margin-left: auto; font-size: var(--fs-11); color: var(--ad-muted); }
.ad-pflater { margin-top: var(--sp-4); padding-top: var(--sp-3); border-top: 1px dashed var(--ad-line-2); }

/* 分区里的单据页签 —— 复用已批的 `.ad-sertabs`（搜索页先例），
   只把「按钮版」缺的两条补齐（原先是给 `<a>` 用的，不写这两条会露出系统灰底）。 */
.ad-sertabs button.ad-sertabs__item { background: none; font: inherit; cursor: pointer; }
/* ★★ R3（2026-09-30）：打印设置**页签上的「接线状态」小点** —— 是**空 `<i>`、不含文字**，
   所以页签名一个字不变（判据 [G2b] 读 `inner_text`，照旧全绿）。
   绿 =「已生效」（这里改的**会被版式读走**）· 黄 = 版式在跑但**还没接**这里的配置 ·
   灰 = 版式还没做（R4）。
   ★ 为什么值得单开一个类：8 类都能配，但**真读配置出纸的目前只有报价单** ——
     不标出来，运营配了半天以为生效，就是「界面骗人」。 */
.pfwire { display: inline-block; width: 7px; height: 7px; margin-left: 5px;
  border-radius: 50%; vertical-align: middle; background: var(--ad-muted); }
.pfwire--live  { background: var(--ad-ok); }
.pfwire--sheet { background: var(--ad-warn); }
.pfwire--none  { background: var(--ad-muted); }

/* ---- 纸：1/2 A4（210 × 148.5mm）横置，比例照真纸 —— 一眼看出「一页能放几行」。
   用 `aspect-ratio` 而**不写死高度**：内容多时（比如 11 列明细）它会自然长高；
   写死高度就会静默溢出。 ---- */
.ad-pfpaper { position: relative; background: var(--ad-bg); border: 1px solid var(--ad-line-2);
  border-radius: var(--r-4); box-shadow: var(--shadow-hairline); padding: var(--sp-4);
  display: flex; flex-direction: column; gap: var(--sp-4); aspect-ratio: 210/148.5; }
.ad-pfsize { position: absolute; top: 5px; right: 9px; font-size: var(--fs-10); color: var(--ad-muted); }

/* ---- 四个区：虚线框 ＋ 左上角小标 ---- */
.ad-pfzone { position: relative; border: 1px dashed var(--ad-line-2); border-radius: var(--r-4);
  padding: var(--sp-4) 14px var(--sp-3); }
.ad-pfzone--fixed { background: var(--ad-bg-soft); }
.ad-pfzone--foot { margin-top: auto; }
.ad-pfzoneb { position: absolute; top: -10px; left: 10px; background: var(--ad-bg); padding: 0 6px;
  font-size: 10.5px; color: var(--ad-ink-2); border: 1px solid var(--ad-line); border-radius: var(--r-2); }
.ad-pfzone--fixed .ad-pfzoneb { background: var(--ad-bg-soft); }
.ad-pfzoneb i { font-style: normal; color: var(--ad-muted); border-left: 1px solid var(--ad-line);
  margin-left: var(--sp-1); padding-left: var(--sp-1); }
.ad-pfzonehint { font-size: 10.5px; color: var(--ad-muted); }
.ad-pfzonehint b { color: var(--ad-ink-2); }

/* ① 品牌信息区（固定） */
.ad-pfbrand { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--sp-4);
  padding-bottom: var(--sp-2); border-bottom: 2px solid var(--ad-ink); }
.ad-pfbrand__l { display: flex; align-items: baseline; gap: var(--sp-2); }
.ad-pfbrand__lg { flex: none; width: 34px; height: 22px; border: 1px solid var(--ad-line-2);
  border-radius: var(--r-2); font-size: 9px; color: var(--ad-muted);
  display: inline-flex; align-items: center; justify-content: center; }
.ad-pfbrand__nm { font-size: var(--fs-17); font-weight: var(--fw-medium); }
.ad-pfbrand__en { font-size: var(--fs-11); letter-spacing: .22em; color: var(--ad-muted); }
.ad-pfbrand__r { text-align: right; display: flex; flex-direction: column; }
.ad-pfbrand__dn { font-size: var(--fs-15); color: var(--ad-accent); font-weight: var(--fw-medium); }
.ad-pfbrand__sub { font-size: var(--fs-11); color: var(--ad-muted-ink); }

/* ② 表头区域 —— ★ 横排：一行两对「☑ 标题 │ ← → │ key」，与打印件表头同排法。
   ⚠️ 必须 `minmax(0,1fr)`：写 `1fr 1fr` 时内容会**静默溢出**（第三栏跑出卡片、不报错）。 */
.ad-pfhg { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 18px; }
@media (max-width: 1199px) { .ad-pfhg { grid-template-columns: minmax(0, 1fr); } }
.ad-pfc { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; padding-bottom: var(--sp-1);
  border-bottom: 1px solid var(--ad-line); }
.ad-pfc.is-off { opacity: .5; }
/* 标题框宽度定死 —— 让 key 标注挤不动它（唯一会伸长的只有中间那段留白） */
.ad-pfc__in { flex: 0 0 116px; font-size: var(--fs-12); padding: 2px 7px; }
.ad-pfc__ord { flex: none; display: inline-flex; gap: var(--sp-1); }
.pfkey { margin-left: 2px; flex: none; max-width: 44%; font-size: var(--fs-10); color: var(--ad-muted);
  font-family: ui-monospace, Consolas, monospace; background: var(--ad-bg-alt);
  border: 1px solid var(--ad-line); border-radius: var(--r-2); padding: 1px 5px; word-break: break-all; }
.ad-pfzonefoot { display: flex; align-items: flex-start; gap: 10px; margin-top: var(--sp-3); }

/* ③ 明细列表区 —— ★ 真表格：列的左右顺序就是单子上的左右位置 */
.ad-pfgrp + .ad-pfgrp { margin-top: var(--sp-4); }
.ad-pfgrp__t { display: flex; align-items: center; gap: 10px; font-size: var(--fs-12);
  color: var(--ad-ink-2); margin-bottom: 6px; }
.ad-pfgrp__t em { font-style: normal; font-size: 10.5px; color: var(--ad-accent); }
.ad-pftbwrap { overflow-x: auto; }
.ad-pftb { width: 100%; table-layout: fixed; border-collapse: separate; border-spacing: 0;
  font-size: var(--fs-12); }
/* 第一列（#/序号）给 96px —— 格子里要放下「输入框 + ☑ + ←28 + →28 + key」，
   实测最少 88px 内宽；给 84px 会**静默溢出**（`table-layout:fixed` 不报错）。 */
.ad-pftb__cfg th:first-child { width: 96px; }
.ad-pftb th, .ad-pftb td { border-right: 1px solid var(--ad-line); border-bottom: 1px solid var(--ad-line);
  padding: 3px var(--sp-1); vertical-align: middle; text-align: left; background: none; font-weight: 400; }
.ad-pftb tr > *:first-child { border-left: 1px solid var(--ad-line); }
.ad-pftb thead tr:first-child th { border-top: 1px solid var(--ad-line); }
.ad-pftb th { background: var(--ad-bg-alt); color: var(--ad-ink-2); }
.ad-pftb__cfg th { border-bottom: 0; }
.ad-pftb__c.is-off { opacity: .5; }
.ad-pftb .ad-pftb__name { width: 100%; font-size: var(--fs-12); padding: 2px 6px; }
.ad-pftb__ctl { display: flex; align-items: center; justify-content: flex-start; gap: var(--sp-1);
  margin-top: 5px; padding: 0; }
/* ★ 表格里的复选框**定死 16×16**（Chromium 原生 20px）—— 见文件顶部硬约束 ③ */
.ad-pftb__ctl input[type="checkbox"] { width: 16px; height: 16px; margin: 0; flex: none; }
/* ★ key 标注允许折行（`word-break:break-all`）—— 见文件顶部硬约束 ② */
.ad-pftb__keys th { background: var(--ad-bg); border-top: 0; padding: 1px var(--sp-1); font-size: 9px;
  line-height: 1.3; font-family: ui-monospace, Consolas, monospace; color: var(--ad-muted);
  word-break: break-all; }
/* ⚠️ 必须 `.ad-pftb .ad-pftb__hint`（(0,2,0)）—— 单写 `.ad-pftb__hint` 是 (0,1,0)，
   会被上面的 `.ad-pftb td{text-align:left}`（(0,1,1)）**压住**（样机踩过同款）。 */
.ad-pftb .ad-pftb__hint { color: var(--ad-muted); font-size: var(--fs-11); text-align: center; }

/* 「＋ 增加字段 / ＋ 增加列」＋ 点开后的候选池 */
.ad-pfadd { margin-left: auto; flex: none; display: inline-flex; align-items: center; gap: 6px;
  white-space: nowrap; }
.ad-pfadd__n { font-style: normal; font-size: 10.5px; color: var(--ad-muted); background: var(--ad-bg-alt);
  border: 1px solid var(--ad-line); border-radius: var(--r-pill); padding: 0 6px; line-height: 1.6; }
.ad-pfadd__n.is-none { color: var(--ad-ok-text); border-color: var(--ad-ok-line); background: var(--ad-ok-bg); }
.ad-pfpool { margin-top: 6px; padding: 7px 9px; border: 1px dashed var(--ad-line-2);
  border-radius: var(--r-2); background: var(--ad-bg-soft);
  display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
/* ★ 必须显式写 —— `flex` 会把 `[hidden]` 的 `display:none` 压下去 */
.ad-pfpool[hidden] { display: none; }
.ad-pfpool__t { font-size: 10.5px; color: var(--ad-muted); }
.ad-pfpool__chip { font-size: 11.5px; line-height: 1.6; padding: 2px 10px; border-radius: var(--r-pill);
  border: 1px dashed var(--ad-line-2); background: var(--ad-bg); color: var(--ad-ink-2); cursor: pointer; }
.ad-pfpool__chip:hover { border-color: var(--ad-ink); color: var(--ad-ink); }
/* 规划中的列：如实列出来、但置灰不可点 —— 「点了没反应」比「没有这个入口」更糟 */
.ad-pfpool__chip.is-planned { border-style: dotted; color: var(--ad-muted); cursor: not-allowed; }
.ad-pfpool__chip.is-planned:hover { border-color: var(--ad-line-2); color: var(--ad-muted); }
.ad-pfpool__chip em { font-style: normal; font-size: 9.5px; color: var(--ad-accent); margin-left: 3px; }
/* ★ 已在单子里的：chip **常驻**（不能滤掉，否则撤不下来）⇒ 实心表示"已印"，
   再点一下就是撤下。未印的是虚线描边（可加）。 */
.ad-pfpool__chip.is-on { border-style: solid; background: var(--ad-ink); color: var(--ad-bg);
  border-color: var(--ad-ink); }
.ad-pfpool__chip.is-on:hover { opacity: .82; }
.ad-pfpool__chip.is-on em { color: var(--ad-bg); opacity: .75; }

/* ④ 条款说明区（固定） */
.ad-pfnote { font-size: var(--fs-11); color: var(--ad-muted-ink); padding-bottom: 10px; }
.ad-pfsig { display: flex; gap: 26px; flex-wrap: wrap; padding-top: 10px;
  border-top: 1px solid var(--ad-line); font-size: 11.5px; color: var(--ad-ink-2); }

/* 旧「两栏清单」留下的那个勾选小件 —— 新的照单排**仍在用**，所以留着。 */
.ad-pfchk { flex: none; display: flex; align-items: center; gap: 3px; font-size: var(--fs-11);
  color: var(--ad-ink-2); cursor: pointer; white-space: nowrap; }
.ad-pfchk input { margin: 0; }

/* ============================================================================
   客户维护（批 13 · 2026-09-29）—— 按 §4.8.1 铁律「维护页 = 多维表格，不许一列到底」
   ① 列表页：可点 KPI 卡行（点卡 = 按该阶段筛）＋ 筛选行 ＋ 主表格 ＋ 分页
   ② 编辑页：明细**并排卡片**（联系人 / 地址 / 跟进 …）—— 同屏至少两张，不竖着堆
   ③ 地址弹窗：类型 chip（固定三类）＋ **两列并排**字段 ＋ 省市区联动
   ============================================================================ */

/* ① 可点统计卡。★ 是 <button> 不是 <div> —— 键盘可达、语义对（Polaris 那条"别把控件塞进可点行"反过来用）。 */
.ad-kpibar { display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); gap: var(--sp-3); }
.ad-kpi {
  display: flex; flex-direction: column; gap: 2px; text-align: left; font: inherit;
  background: var(--ad-bg); border: 1px solid var(--ad-line); border-radius: var(--r-4);
  padding: var(--sp-3) var(--sp-4); cursor: pointer; color: var(--ad-ink);
  transition: border-color var(--tr), box-shadow var(--tr);
}
.ad-kpi:hover { border-color: var(--ad-ink-2); }
.ad-kpi.is-on { border-color: var(--ad-accent); box-shadow: inset 0 0 0 1px var(--ad-accent); }
/* 只读的统计卡（金额那四张）：不许点，所以也不能有「可点」的指针与悬停反馈 ——
   界面上"能点"和"不能点"必须长得不一样，否则人点半天没反应。 */
.ad-kpi.is-static { cursor: default; }
.ad-kpi.is-static:hover { border-color: var(--ad-line); }
.ad-kpi__k { font-size: var(--fs-11); letter-spacing: var(--ls-wider); text-transform: uppercase; color: var(--ad-muted); }
.ad-kpi__v { font-size: var(--fs-22); font-weight: var(--fw-light); line-height: 1.15; }

/* ② 明细卡片并排网格 —— 反「一列到底」的核心一笔。
      ★ 判据量的是 `getBoundingClientRect()`（横向 ≥2 列），**不是**查这里的 class 名。 */
.ad-cardgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(330px, 1fr));
  gap: var(--sp-5); align-items: start; }
.ad-cardgrid > .ad-panel { margin-top: 0; margin-bottom: 0; }

/* ③ 列表页页脚（合计 / 清除筛选）。★ 原来只打算给分页用，批 13 改成"合计条" ——
      分页没做：全站其它列表页都是"全渲染 + DOM 级搜索"，这里跟它们保持一致
      （另起一套分页会让搜索只搜当前页，那是**界面上骗人**）。 */
.ad-pag { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) var(--sp-5);
  border-top: 1px solid var(--ad-line); font-size: var(--fs-12); color: var(--ad-ink-2); flex-wrap: wrap; }
.ad-pag .spacer { flex: 1; }

/* ④ 地址弹窗：类型 chip ＋ 两列字段栅格 */
.ad-addrchips { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.ad-addrchip {
  font: inherit; font-size: var(--fs-12); cursor: pointer;
  border: 1px solid var(--ad-line); background: var(--ad-bg);
  border-radius: var(--r-pill); padding: var(--sp-1) var(--sp-3); color: var(--ad-ink-2);
}
.ad-addrchip:hover { border-color: var(--ad-ink-2); }
.ad-addrchip.is-on { border-color: var(--ad-accent); color: var(--ad-ink);
  box-shadow: inset 0 0 0 1px var(--ad-accent); }
.ad-addrgrid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-4); }
.ad-addrgrid .full { grid-column: 1 / -1; }

@media (max-width: 767px) {
  .ad-cardgrid { grid-template-columns: 1fr; }
  .ad-addrgrid { grid-template-columns: 1fr; }
}

/* ==========================================================================
   ★★★ 2026-09-29（批 18 · 单据页）—— 说明小标 · 汇总页勾选 · 单据页收紧
   --------------------------------------------------------------------------
   余总原话：「隐藏所有说明文字 鼠标放在需要说明的位置 展示说明 鼠标移开隐藏」
             「能设计成表格的全部用表格」「另模块与模块间距紧凑一些」
   ========================================================================== */

/* ---- ① 说明小标 `.qm`：**默认只有一个「?」，说明文字悬停才出现** ----------------
   ★ 为什么不直接把 `.ad-f__hint` 设成 `display:none`：那会让"说明"**彻底丢失**
     （键盘 / 读屏都够不到），而且以后没人知道这里原本有一句解释。
     ⇒ 换成"**内容还在、只是不铺在页面上**"：`data-tip` 里存着，`:hover` / `:focus` 时显示。
   ★ 用纯 CSS 的 `::after` 而不是样机那套「一只 JS 浮层 `.qpop`」：
     少一个**会漂的移动部件**（定位、滚动跟随、z-index 三层都要维护），
     而需求只有"悬停显示、移开隐藏"两个动作，CSS 天生就能表达。
   ★★ 2026-10-01（R7 · Q4）**已把上面那条「局限」修掉** —— 原样保留这段是为了留痕：
     那个"已知接受"在真跑里是**假的** —— 说明文字**真的会被祖先裁**（实测：宿主
     bottom 363、祖先 `.ad-panel` 裁到下 590 ⇒ 可用高度只有 227px，再长就切一半）。
     现在改成 **body 下的真元素 `#adQmTip` ＋ `adFloat()`**（`fixed` ＋ 四边翻面 ＋ 高度上限），
     与 `.sp-tip` / 列设置 / 敲字候选**同一套底座** —— 不再有"别放进滚动容器"这种口头约束。
     ⚠️ `::after` 天生**无法脱离宿主**（伪元素永远长在宿主盒里）⇒ 只要还用它，就等于没治。 */
.qm {
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; margin-left: 2px;
  border: 1px solid var(--ad-line-2); border-radius: 50%;
  font-size: var(--fs-10); line-height: 1; color: var(--ad-muted-ink);
  background: var(--ad-bg); cursor: help;
  vertical-align: middle; user-select: none;
}
.qm:hover, .qm:focus-visible { color: var(--ad-ink); border-color: var(--ad-ink); outline: none; }
/* 说明气泡本体（`#adQmTip`，body 下单例，由 `adFloat()` 定位）。
   ★ 白底深字（沿用原 `::after` 的观感）· `white-space: normal` 让 60 字的那种长句折行。 */
.ad-ftip {
  position: fixed; z-index: 60;
  width: max-content; max-width: 300px; white-space: normal;
  padding: 7px 10px; border-radius: var(--r-2);
  border: 1px solid var(--ad-line-2); background: var(--ad-bg); color: var(--ad-ink-2);
  font-size: var(--fs-11); font-weight: var(--fw-normal); line-height: 1.5;
  text-align: left; letter-spacing: 0;
  box-shadow: 0 6px 20px rgba(0, 0, 0, .14);
  pointer-events: none;
}

/* ---- ② 单据页：**模块间距收一档**（⑬）--------------------------------------
   ★ 只作用于 `.ad-doc`（`docRender` / `renderQ` 给单据页加的包裹类）——
     **不动全局 `.ad-panel`**：那会波及全后台 40+ 页（克制，同 §4.30.4）。
   ★ 面板内边距 24 → 16；面板间距 16/24 → 12（由包裹类统一管，不再靠每处写 inline margin）。 */
.ad-doc .ad-panel__body { padding: var(--sp-4); }
.ad-doc .ad-panel + .ad-panel { margin-top: var(--sp-3) !important; }
.ad-doc .ad-note, .ad-doc .ad-actions { margin-top: var(--sp-3) !important; }

/* ---- ③ 汇总页：勾选列 ＋ 批量操作条（①）--------------------------------------
   ★ 勾选框**只对当前可见行生效**（筛选 / 分页后重算）—— 避免"选了看不见的行"。
   ★★ 2026-09-29（余总实拍）**这个类名不许叫 `.ad-pick`** ——
     `.ad-pick` 是**表单字段选择器**的类（见上面 §字段选择器：`.ad-pick { display: block; position: relative; }`），
     `display:block` 套到 `<td>` / `<th>` 上会让这个单元格**脱离表格布局**（不再是 `table-cell`）
     ⇒ 后果全是**静默**的（零报错）：
       · 首列（勾选列）右侧**凭空多出一道 34px 空档**（那一段落不进任何格子里，`elementFromPoint` 只命中 `TR`）；
       · 这道空档**没有单元格 ⇒ 没有 `border-bottom`** ⇒ 表头下边框在左端**断开**；
       · 表头与行看着"错位/缺一块"，就像被谁挡住了一样。
     ⇒ 单独立名 `.ad-rowchk`，与表单选择器彻底分家（`<td>`/`<th>` 保持默认的 `table-cell`）。
     ★ 取证：`v2/tools/_tmp/_probe_sales_list.py`（量到 `th.ad-pick` 的 `display:block` 与 259→293 那道空档）。 */
.ad-rowchk { width: 34px; padding-left: 0; padding-right: 0; text-align: center; }
.ad-rowchk input { width: 15px; height: 15px; cursor: pointer; vertical-align: middle; }
.ad-batchbar {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding: var(--sp-2) var(--sp-5);
  border-bottom: 1px solid var(--ad-acc-line); background: var(--ad-acc-bg);
  font-size: var(--fs-12);
}
.ad-batchbar[hidden] { display: none; }
.ad-batchbar b { color: var(--ad-ink); }

/* ---- ④ 汇总页单号：**热区撑满整格**（②）------------------------------------
   ★ 症状（`_probe_qlink.py` 实测）：`<a>` 只包住文字、盒子 111×16px ⇒
     点单号**右边那片空白**什么都不发生（而那片空白看起来和链接是一体的）= "点了没反应"。
   ★ 做法 = `display:block` ＋ padding 撑满格子 ＋ **负 margin 抵掉**（不撑高行高）。 */
.ad-table td.ad-ellink { padding: 0; }
.ad-table td.ad-ellink > a {
  display: block; padding: 10px var(--sp-3); margin: 0;
  color: inherit;                /* ★★ 静态色 = 与整行其余列一样（继承 td 的 --ad-ink） */
  font-weight: inherit;          /* ★ 静态态不加粗（悬停才加粗，见下） */
}
/* ★★★ 2026-10-04（批 T4 · 需求 1 · 余总**第 4 次**更正后最终定死，逐字）：
   「整行其余列悬停 = 灰底，不加粗、不变色　**单号列灰底(与其余列一样）文字加粗 + 玫红**
     · 鼠标离开后恢复原样　**单号列静态色文字色 = 与其余列一样**
     **单号列不需要专门底色、与整行共用底色**」
   ⇒ ① **单号列静态** = 与其余列同色（`inherit` ⇒ 默认 `--ad-ink`，不再是玫红）
      ② **单号列悬停** = **灰底（与整行共用 sunk 底，不去底）** ＋ **加粗** ＋ **玫红 `#E4757E`**
      ③ 鼠标离开 ⇒ 自动恢复（`:hover` 天然如此）
   ⚠️ 前四版已废（余总四次更正，全留档不删）：
     · v1 青柠底 #CFDCA8 ＋ 深墨整行
     · v2 青柠底 ＋ 保字色
     · v3 整行**无色块** ＋ 整行玫红加粗（「整行不变」）
     · v4 单号列悬停**去底** ＋ 加粗 ＋ 玫红；静态玫红
     · **v5（本版·终）** 单号列**共用整行灰底**（不去底）；静态**同其余列**色（非玫红）；
       只有**悬停时**才变玫红＋加粗。 */
.ad-table td.ad-ellink > a:hover {
  color: var(--ad-docno);
  font-weight: var(--fw-medium);
}
/* ★ 静态态单号列不加粗（悬停才加粗）—— 已在上面 `> a` 里 `font-weight: inherit` 交代。 */

/* ---- ⑤ 两个「不是说明文字」的小字类（⑪ 的配套）-----------------------------
   余总 ⑪ 要收的是**说明文字**（"随客户带出 · 在客户里维护"这种解释句）——
   但页面上还有些**短标签 / 数据**，它们不该被收进 `?`：
     · `.ad-quiet` = 「转到：」「（终态）」这种**功能标签**（3~6 字，读的人一眼就懂）
     · `.ad-rev`   = 「第 3 版」这种**数据**
   ⇒ 给它们自己的类。判据 `check_doc_sheet.py` 的 L6 守的是「单据页上不存在**可见的**
     `.ad-f__hint`」——说明文字一律走 `.qm`。 */
.ad-quiet, .ad-rev { font-size: var(--fs-11); color: var(--ad-muted-ink); }


/* ==========================================================================
   ★★ R7 · Q2（1/2）· 按钮覆盖 —— 对齐《后台 UI 量化规范》
   --------------------------------------------------------------------------
   ★ 为什么写在这里而不是 components.css：
     `.btn` 是**前台与后台共用类**（`components.css` 顶部注释明写「前台与后台共用」），
     前台 11 处在用。`admin.css` **只被 `v2/admin/index.html` 引用**（前台不加载它）
     ⇒ 覆盖写在这里 = **前台零影响**。这是本批唯一安全的路径。
   ★ 依据：`v2/_docs/UI量化规范清单_R7.md` §2（尺寸与排版）· §14.1（B1–B11）。
   ★ 判据：`v2/tools/check_q2_ui.py` V1–V4 / V6（每条含反向变异）。
   ========================================================================== */
.btn {
  min-height: var(--ad-btn-h);        /* 40 · B1 */
  padding: 0 var(--sp-5);                    /* B1 */
  gap: var(--sp-2);                           /* B9 · 图标与文字间距 8 */
  border-radius: var(--ad-r-field);   /* 4 · B1 */
  font-size: var(--fs-14);                    /* B1 */
  font-weight: 500;                   /* B1 */
  text-transform: none;               /* B2 · 去掉 uppercase（后台非品牌展示位） */
  letter-spacing: normal;             /* B2 */
}
.btn svg { width: 16px; height: 16px; }              /* B9 */

/* 五态 —— 主按钮 B4–B6（旧写法 `.btn--accent:hover { filter: brightness(.92) }` 作废） */
.btn--accent { background: var(--ad-accent); border-color: var(--ad-accent); color: var(--ad-white); }
.btn--accent:hover  { filter: none; background: var(--ad-accent-2); border-color: var(--ad-accent-2); }
.btn--accent:active { background: var(--ad-accent-3); border-color: var(--ad-accent-3); }

/* 次按钮 B3 —— ★★ 2026-10-03 SAGE 落地（批 S1）按样机 `.btn.ghost` 改值。
   余总原话（本轮）：**「按钮默认状态下透明底 加边框 黑字 选中色 #E1E9C2」**
   —— 逐字对应样机：
       `.btn.ghost{background:transparent;border-color:var(--ctl-line);color:var(--ink)}`
       `.btn.ghost:hover{background:var(--lime-2);color:var(--olive);border-color:var(--olive)}`
   ★ 改前 vs 改后：
       边框  `--ad-line  #DADFCC`(1.31:1) → `--ad-ctl-line #6E785A`(4.48:1) ← **边框终于看得见**
       悬停底 `--ad-bg-alt #F0F1E9`(与页底同色、几乎无反馈) → `--ad-lime-2 #E1E9C2`(余总点名)
       悬停字 仍 `--ad-ink` → `--ad-accent`（配青柠底，规范 §7.1 反色）
   ⚠️ 悬停**边框**也从 `--ad-line-2` 收到 `--ad-accent`：样机就是 `border-color: var(--olive)`，
      让"悬停"是一次**整体换装**（底+字+边同时变），而不是只亮一下底。 */
.btn--ghost { background: transparent; color: var(--ad-ink); border-color: var(--ad-ctl-line); }
.btn--ghost:hover { background: var(--ad-lime-2); color: var(--ad-accent); border-color: var(--ad-accent); }

/* 小型档 B7 */
.btn--sm { min-height: var(--ad-btn-h-sm); padding: 0 var(--sp-4); font-size: var(--fs-13); }

/* 禁用态 B8 —— 底 / 字均取令牌：
   现值 底 `--ad-fill-off` = #DADFCC（SAGE --line-1）· 字 `--ad-placeholder` = #6A7058。
   WCAG 1.4.3 对**失效控件豁免**对比度，故原值可用。
   （历史值：底 #E5E6EB / 字 #86909C，SAGE 换皮后已废） */
.btn[disabled], .btn[aria-disabled="true"] {
  opacity: 1;
  background: var(--ad-fill-off); border-color: var(--ad-fill-off);
  color: var(--ad-placeholder); pointer-events: none;
}


/* ==========================================================================
   ★★ R7 · Q2（2/2）· 表单控件覆盖 —— 六套收敛为**两档**
   --------------------------------------------------------------------------
   改前实测（`_probe_q2.py` 真跑）：筛选 28 · 头部网格 30 · 明细行内 33 ·
   搜索/弹窗 46（胶囊）· textarea 52 —— 同一后台里**六套高度、四种圆角、三种边框色**。
   改后只剩两档：**舒适 40**（列表页 / 弹窗）· **紧凑 32**（详情页密集表单 / 明细行）。
   ★ 边框取 `--ad-field-line`（现 #7E8470；历史值 #848A94 已废）—— 余总 2026-09-30 定：「保留深边框；
     `#DCDFE6` / `#E5E6EB` 专用于表格分割线与卡片外框（纯装饰位豁免）」；★ SAGE 换皮后装饰线取
     `--ad-line #DADFCC` / `--ad-line-2 #C8CCBE`，上面那句话里的两个旧值已不再出现。
   ★ 依据：§3 · §14.2（F1–F9）。判据：`check_q2_ui.py` V5 / V7 / V8。
   ========================================================================== */

/* F1 / F2 —— 舒适档 40（`.input` 是共享类，同 14.0 的理由写在 admin.css） */
.input, .select {
  height: var(--ad-field-h);
  min-height: 0;
  padding: 0 var(--sp-3);
  border: 1px solid var(--ad-field-line);
  border-radius: var(--ad-r-field);      /* F2 · 去掉胶囊 999px */
  background: var(--ad-bg);
  color: var(--ad-ink);
  font-size: var(--fs-14);
}
.select { padding-right: var(--sp-6);
  background-position: calc(100% - 20px) 17px, calc(100% - 15px) 17px; }  /* 箭头随 40 高重定位 */

/* F7 —— 多行文本 */
.textarea {
  min-height: 88px; padding: 10px var(--sp-3);
  border: 1px solid var(--ad-field-line); border-radius: var(--ad-r-field);
  background: var(--ad-bg); color: var(--ad-ink); font-size: var(--fs-14);
}

/* F6 —— 占位符（原走 --c-muted-2，在后台是 2.16:1 那档废色） */
.input::placeholder, .textarea::placeholder { color: var(--ad-placeholder); }

/* F5 —— 聚焦环统一：主色 2px（AA 2.4.11「焦点可见」）；替换旧的 `border-color: var(--ad-ink)`
   ★★ 2026-10-03 SAGE 落地（批 S1）追加：样机的聚焦是**软环** ——
       `border-color: var(--olive-3)` ＋ `box-shadow: 0 0 0 3px rgba(38,56,25,.14)`
     比硬 outline 柔和、且"贴边"（outline 会向外扩 1px）。
     ⚠️ **为什么不直接删 outline 换成 box-shadow**：`outline` 是 WCAG 2.4.11 的
        "焦点指示"标准件 —— 在 Windows 高对比模式下 `box-shadow` 会被系统吞掉、
        而 `outline` 不会。⇒ **两层都留**：outline 保合规底线、软环给视觉质感。
     ★ 值取 `--ad-accent-3`(#4A5C36 = SAGE --olive-3)：比主色 `--ad-accent` 亮一档，
       在"已聚焦"的实心底上分得出来。 */
.input:focus, .select:focus, .textarea:focus {
  outline: 2px solid var(--ad-accent); outline-offset: 1px;
  border-color: var(--ad-accent-3);
  box-shadow: 0 0 0 3px rgba(38, 56, 25, .14);
}

/* F3 —— 详情页头部网格 → 紧凑档 32 */
.ad-headgrid input, .ad-headgrid select {
  height: var(--ad-field-h-dense); padding: 0 var(--sp-2); border-radius: var(--ad-r-field);
}
.ad-headgrid textarea { height: auto; min-height: 72px; padding: var(--sp-2) 10px; border-radius: var(--ad-r-field); }
.ad-headgrid input:focus, .ad-headgrid select:focus, .ad-headgrid textarea:focus {
  outline: 2px solid var(--ad-accent); outline-offset: 1px; border-color: var(--ad-accent);
}

/* F4 —— 明细行内 → 紧凑档 32（原 33 / 圆角 0 / 无边框）
   格内补 `1px solid transparent` 占位（不跳版），聚焦才显主色。 */
table.ad-lines td input, table.ad-lines td select {
  height: var(--ad-field-h-dense); border: 1px solid transparent; border-radius: var(--ad-r-field);
}
table.ad-lines td input:focus, table.ad-lines td select:focus {
  outline: none; border-color: var(--ad-accent); background: var(--ad-bg);
}

/* F9 —— 列表页筛选控件 → 舒适档 40（原 28，是"六套"里偏差最大的一档之一） */
.ad-filter__fld input, .ad-filter__fld select {
  height: var(--ad-field-h); padding: 0 var(--sp-3); border-radius: var(--ad-r-field); font-size: var(--fs-14);
}
.ad-filter__fld input:focus, .ad-filter__fld select:focus {
  outline: 2px solid var(--ad-accent); outline-offset: 1px; border-color: var(--ad-accent);
}

/* F9b —— 批量条内联小框 26 → 紧凑档 32（与 `--ad-field-h-dense` 同一档） */
.ad-batchbar .input--sm { height: var(--ad-field-h-dense); min-height: var(--ad-field-h-dense); font-size: var(--fs-13); }

/* F9c —— 登录页输入框 → 舒适档 40（独立页面，同属后台控件） */
.ad-login__input { height: var(--ad-field-h); padding: 0 var(--sp-3); border-radius: var(--ad-r-field); font-size: var(--fs-14); }
.ad-login__input:focus { outline: 2px solid var(--ad-accent); outline-offset: 1px; border-color: var(--ad-accent); }

/* F9d —— 列表页主表内联编辑框（`.ad-table .ad-inp`）→ 紧凑档 32 */
.ad-table .ad-inp { height: var(--ad-field-h-dense); padding: 0 6px; border-radius: var(--ad-r-field); }

/* F10 —— 六域明细行（`.ad-table--lines`）→ 紧凑档 32
   ★★ 2026-09-30 真跑逮到：**六域的行表用 `.ad-table--lines`，报价单页用 `.ad-lines`** ——
     两套类名（admin.css:2903 早有注释，我 F4 只写了 `.ad-lines` ⇒ 六域 24 格全是 28px/圆角 0）。
     实测（`_probe_q2d.py`）：销售单详情 `.ad-table--lines` 里 input = 28px / r=0 / bd 2px。
   ⇒ 与 F4 同规格：32 紧凑 · 圆角 4 · 透明占位边框（聚焦才显主色）。 */
.ad-table--lines td input, .ad-table--lines td select {
  height: var(--ad-field-h-dense); padding: 0 var(--sp-2);
  border: 1px solid transparent; border-radius: var(--ad-r-field); background: transparent;
}
.ad-table--lines td input:focus, .ad-table--lines td select:focus {
  outline: none; border-color: var(--ad-accent); background: var(--ad-bg);
}
.ad-table--lines td input[readonly] { color: var(--ad-muted-ink); }

/* F11 —— 分页控件（每页条数下拉 ＋ 页码按钮）→ 统一紧凑档 32
   ★ 实测两件：下拉 26px / 页码按钮 26px —— 都低于紧凑档，且整组不一致（旁边筛选已是 40）。
     ⇒ 一组同时抬到 32，保持组内一致。 */
.ad-pager select {
  height: var(--ad-field-h-dense); padding: 0 var(--sp-2);
  border-radius: var(--ad-r-field); font-size: var(--fs-13);
}
.ad-pager__pg button {
  min-width: var(--ad-field-h-dense); height: var(--ad-field-h-dense);
  padding: 0 var(--sp-2); border-radius: var(--ad-r-field); font-size: var(--fs-13);
}
.ad-pager select:focus, .ad-pager__pg button:focus-visible {
  outline: 2px solid var(--ad-accent); outline-offset: 1px; border-color: var(--ad-accent);
}

/* F12 —— 列表页头部搜索 / 筛选控件 `.ad-q` → 舒适档 40
   ★ 实测：`<input class="ad-q">` 26px / `<select class="ad-q">` 31px · **圆角 999 胶囊** ·
     字号 12px。它是**列表页头部**的筛选位（客户页 / 报表页 / BOM页 / 规格页同款）
     ⇒ 落舒适档 40（与 `.ad-filter__fld` 同档），去胶囊改 4。
   ⚠️ 有些 `.ad-q` 同时带 `input` 类（实测已 40）⇒ 本条与 `.input` 同值，不打架。 */
.ad-q {
  height: var(--ad-field-h); padding: 0 var(--sp-3);
  border-radius: var(--ad-r-field); font-size: var(--fs-14);
}
select.ad-q { padding-right: var(--sp-6); }
.ad-q:focus {
  outline: 2px solid var(--ad-accent); outline-offset: 1px;
  border-color: var(--ad-accent); background: var(--ad-bg);
}

/* ==========================================================================
   ★★★ R7 · Q5 · 响应式三档（2026-10-01）

   规范三档 = **≥1200 / 768–1199 / <768**。改前有 **6 个散落的 max-width 值**
   （720 / 900 / 1000 / 1100 / 1280 / 1500）＋ 一处**反覆盖 bug**：
   1000px 那条 `grid-template-columns: 160px …` 与 900px 那条**同时命中**，
   而它在文件里更靠后 ⇒ **单列规则被盖掉**，`≤900 单列` 这个意图**从未生效过**
   ⇒ `<768` 实测：34 个菜单项横排成一条（中文逐字竖叠、项高 138、`.ad-nav` 高 1518–2548px）。

   ★★ 本段是**骨架那一层**。其余 15 条零散断点已全部对齐到 {767, 1199}
     （1500/1280/1100/1000 → 1199；900/720 → 767），这里不再重复它们。

   ★★ 三档各自负责什么（一句话）：
     ≥1200      侧栏 220 展开 ＋ 内容自适应 —— 现状已达标，本档只做**回归守**
     768–1199   侧栏收成 **64px 图标栏**（只 24px 图标、无文字）★ 依赖 Q5-A 补的 34 枚图标
     <768       侧栏改 **抽屉**（滑出 ＋ 遮罩）＋ 顶栏汉堡键 ＋ 表格横向滚动
   ========================================================================== */

/* ---- 抽屉的两个新元素（默认不现身，只在 <768 出现）---- */

/* 汉堡键：**新增元素**，挂顶栏最左（logo 之前）。32 热区 · 16 图标 · 吃顶栏的字色。 */
.ad-burger {
  display: none;                      /* 由 <768 档打开 */
  align-items: center; justify-content: center;
  width: 32px; height: 32px; flex: none;
  padding: 0; margin: 0;
  border: 0; border-radius: var(--ad-r-field);
  background: transparent; color: inherit;
  cursor: pointer;
}
.ad-burger:hover { background: rgba(255,255,255,.14); }
.ad-burger svg { width: var(--ad-ico-s); height: var(--ad-ico-s); display: block; }

/* 抽屉遮罩：**新增元素**，挂 `<body>` 下（`.ad` 外面 —— `.ad` 是 grid，塞进去会占一格）。
   由 `hidden` 属性开关（`navDrawer()` 只管这一个属性 + 一个类）。 */
.ad-navscrim { position: fixed; inset: 0; z-index: 89; background: rgba(1,1,1,.45); }
.ad-navscrim[hidden] { display: none; }

/* ---- 档 1 · ≥1200：侧栏展开（回归守）-------------------------------------------------
   ★★ 2026-10-03（第三轮）**这一档现在守「侧栏 fixed 上顶」** —— 改前守的是
      `sticky; top: var(--ad-top-h)`（贴在顶栏下方）。三轮把侧栏改成 fixed 上顶后，
      这里若还写 sticky，**宽屏会被这条反盖回旧布局**（媒体查询特异性相同、源序更靠后 ⇒ 它赢）
      ⇒ 桌面端看不到任何变化，只有"改了没生效"的闷棍。⇒ 同值改写为 fixed。
   ⚠️ `.ad-burger` / `.ad-navscrim` 在这里显式 `display:none` —— 否则从窄屏拉宽回来时
      它们会赖着不走（媒体查询只加不撤的典型坑）。 */
@media (min-width: 1200px) {
  /* ★★ 第三轮：`.ad` 已改单列 ＋ `padding-left` 占位（见骨架段说明）。
     这里守「宽屏侧栏 220px、不收起」，用 padding 表达。 */
  .ad { padding-left: var(--ad-side-w); }
  .ad-nav { position: fixed; top: 0; width: var(--ad-side-w); height: 100vh; transform: none; box-shadow: none; }
  .ad-burger, .ad-navscrim { display: none; }
}

/* ---- 档 2 · 768–1199：侧栏 → 64px 图标栏 ---------------------------------------------
   ★★ 这一档**必须有图标才成立** —— 所以它是 Q5-A（补 34 枚 24px 菜单图标）的直接下游。
   文字 / 计数 / 未保存圆点全收掉，只留 24px 图标居中，鼠标悬停由 `title` 兜底说明。
   ★★ 2026-10-03（第三轮）：本档侧栏宽从 220 → **64** ⇒ ① `.ad` 的左占位跟着收成 64
      ② 顶栏左缘也走 `left: 64px`（否则顶栏会盖住图标栏那 64px）。 */
@media (min-width: 768px) and (max-width: 1199px) {
  .ad { padding-left: 64px; }
  .ad-nav { position: fixed; top: 0; width: 64px; height: 100vh; transform: none; box-shadow: none; }
  .ad-top { left: 64px; }
  /* 品牌区：只留 logo（"赫蒙管理系统"五字在 64px 里放不下）。 */
  .ad-nav__brand { padding: var(--sp-4) 0; justify-content: center; }
  .ad-nav__brand span, .ad-nav__sep { display: none; }
  .ad-nav__brand img { max-width: 36px; }
  /* 分组标题与页脚整块收掉（它们是纯文字，图标栏里没有位置）。 */
  .ad-nav__group, .ad-nav__foot { display: none; }
  .ad-nav__list { padding: var(--sp-2); gap: 2px; }
  .ad-nav__item { justify-content: center; padding: var(--sp-2) 0; }
  .ad-nav__item > span, .ad-nav__item > em, .ad-nav__dot { display: none; }
  /* 结构降级（原 1280 + 1000 两条并到这里）：
     ★ 原「1280 收中栏到 260」这一级**作废** —— 三档体系里 1200+ 就是宽屏默认（340），
       768–1199 直接走单列，中间不存在"再收窄一点"的那一档（少一档 = 少一套口径）。 */
  .ad-split { grid-template-columns: minmax(0, 1fr); }
  .ad-split .ad-side { position: static; max-height: 46vh; }
  .ad-top__site { font-size: var(--fs-12); }
  .ad-top .ad-status { display: none; }
  .ad-body { padding: var(--sp-5); }
  .ad-bar { padding: var(--sp-3) var(--sp-5); }
  .ad-burger, .ad-navscrim { display: none; }
}

/* ---- 档 3 · <768：侧栏 → 抽屉 ---------------------------------------------------------
   ★★ 为什么是**抽屉**而不是"横排一长条"：改前那条 `grid-auto-flow: column` 把 34 项
      排成一条，每列仅约 58px ⇒ 中文逐字竖叠、`.ad-nav` 高 2548px，内容区只剩 230px
      （截图 `_shots_q5_probe/q5_narrow_quotes.png`）。抽屉把导航**移出文档流**，
      内容区拿回整宽 —— 这才是 390px 宽能用的唯一解法。 */
@media (max-width: 767px) {
  /* ★★ 第三轮：本档侧栏是**抽屉**（浮层、默认滑出屏幕外）⇒ `.ad` **不留左占位**、
     顶栏**横跨全宽**。否则收起状态也会白掉一条 220px。 */
  .ad { padding-left: 0; }
  .ad-top { left: 0; }
  .ad-nav {
    position: fixed; z-index: 90;
    top: var(--ad-top-h); bottom: 0; left: 0;
    width: min(280px, 82vw);
    height: auto;
    /* ★ 2026-10-03 SAGE 换皮：原 `--ad-white`（纯白）—— 全站转到暖纸底后，
       抽屉浮在 `--ad-bg` 纸面上会出现一道冷白，与宽屏侧栏（--ad-bg-soft）也不一致。
       ⇒ 改用 `--ad-bg-soft`，与宽屏侧栏**同底**，浮起关系靠 `--ad-dlg-sh` 表达。 */
    background: var(--ad-bg-soft);
    border-right: 1px solid var(--ad-line);
    box-shadow: var(--ad-dlg-sh);
    transform: translateX(-100%);
    transition: transform var(--tr);
  }
  /* ★ 抽屉从 `top: var(--ad-top-h)` 开始 ⇒ 品牌条得下移同样距离，否则会被顶栏压住一半。
     这一档品牌条**不 sticky**（抽屉本身是浮层、且 `top` 已让开顶栏）。 */
  .ad-nav__brand { position: static; }
  .ad-nav.is-open { transform: none; }
  .ad-burger { display: inline-flex; }
  .ad-navscrim { display: block; }          /* 开不开由 `hidden` 属性说了算（见 JS） */
  /* 内容区：页边距收一档 ＋ **表格横向滚动兜底**（列多的表在 390px 必然超出）。 */
  .ad-body { padding: var(--sp-4); overflow-x: auto; }
  .ad-bar { padding: var(--sp-3) var(--sp-4); flex-wrap: wrap; }
  .ad-split { grid-template-columns: minmax(0, 1fr); }
  .ad-split .ad-side { position: static; max-height: 46vh; }
  .ad-top { padding: 0 var(--sp-4); gap: var(--sp-2); }
  .ad-top__site { display: none; }
  /* ★ 390px 实测：顶栏放不下全部 ⇒ 隐去两枚**次要**按钮（历史版本 / 查看前台）
     ＋ 连接状态只留圆点、保存状态文字收掉 —— 把宽度让给「汉堡键 ＋ 保存」这两个必需的。
     ⚠️ 「保存」**不隐**：它是这页唯一必须够得着的动作。 */
  .ad-top [data-history], .ad-top [data-front] { display: none; }
  .ad-conn #adConnText { display: none; }
  .ad-conn { padding: 0 var(--sp-2); }
  .ad-status { display: none; }
}

/* ==========================================================================
   ★★ 2026-10-03 SAGE 终端风（余总拍板「B 全站换皮 ＋ 报表页先落地」）
   --------------------------------------------------------------------------
   真源 = `F:/2026-10-02-20-02-59/UI设计规范_鼠尾草终端风.html`（v1.1）
   本段只加**新组件**（珠链柱 / 分段条 / 方格热力 / KPI 卡 / 虚线分组）。
   ★★ 九类既有规范（字体/字重/字距/行高/圆角/间距/组件尺寸/动效）**已 100% 对齐**，
      一处都不在这里重写 —— 只在 tokens.css 换了 8 组色值。
   ★★ **不碰前台**：admin/index.html 不加载 site.css，且这里全用 --ad-* / --sage-* 前缀。
   ★★ 铁律⑦「零阴影」—— 这里所有新组件**无 box-shadow**；
      但**弹窗/浮层豁免**（--ad-dlg-sh 保留），否则浮层与底页糊在一起。
   ========================================================================== */

:root {
  /* ---- SAGE 专有令牌（规范 §03 / §11 附录）----
     ★★ 2026-10-03 SAGE 落地「其它页面」（批 S1）**别名化** —— 这 23 支原来是
        **重复定义**：值全部与 `tokens.css` 的 `--ad-*` 逐条相同（已用脚本逐条比对）。
        ⇒ 两套名字、同一批色 = 项目最恨的「第二套口径」：将来改色要改两处，
          漏一处 ⇒ 有的组件变了、有的没变（静默不一致）。
     ⇒ 处理：**保留 `--sage-*` 名字**（`.sage-*` 类名与概览页已验收，改名成本高、风险大），
        但**值改成引用 `--ad-*`** —— 一处定义（tokens.css）、两处引用。
        ⚠️ 别名方向**不可反转**：`--ad-*` 是主、`--sage-*` 是从；谁改 `--sage-*` 都不该
           影响 `--ad-*`（CSS 变量单向）。
     ★ 唯一例外：`--sage-lime-3 #DCE6BE` 在 `--ad-*` 里**没有对应**
       （它是热力图色阶第 3 档）⇒ 本批在 tokens.css 补 `--ad-lime-3`，这里指过去。 */
  --sage-paper:      var(--ad-bg-alt);       /* 画布（暖纸）= #F0F1E9 */
  --sage-paper-2:    var(--ad-bg);           /* 卡面（纸面）= #FAFBF4 */
  --sage-paper-sunk: var(--ad-bg-sunk);      /* 下沉区 = #E9EAE2 */
  --sage-line-1:     var(--ad-line);         /* 分隔线（虚线分组用）= #DADFCC */
  --sage-line-2:     var(--ad-line-2);
  --sage-line-3:     var(--ad-line-3);       /* 最重一档 = #A1A696 */
  --sage-ctl-line:   var(--ad-ctl-line);     /* 控件描边（AA 合规）= #6E785A */
  --sage-ink:        var(--ad-ink);
  --sage-ink-2:      var(--ad-ink-2);
  --sage-ink-muted:  var(--ad-muted-ink);
  --sage-olive:      var(--ad-accent);
  --sage-olive-2:    var(--ad-accent-2);
  --sage-olive-3:    var(--ad-accent-3);
  --sage-lime-1:     var(--ad-acc-bg);
  --sage-lime-2:     var(--ad-lime-2);
  --sage-lime-3:     var(--ad-lime-3);
  --sage-lime-4:     var(--ad-acc-line);
  /* ★ 信号色：**只做填充 / 描边 / 圆点，绝不作文字色**（实测 1.88–2.58 全低于 AA） */
  --sage-sig-high:   var(--ad-err);
  --sage-sig-mod:    var(--ad-warn);
  --sage-sig-ok:     var(--ad-ok);
  --sage-sig-low:    var(--ad-line-3);
  --sage-band-a:     var(--ad-lime-2);
  --sage-band-b:     var(--ad-acc-bg);
}

/* ------------------------------ ① KPI 卡（概览 / 报表共用） ------------------------------ */
.sage-kpis {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--sp-3); margin-top: var(--sp-4);
}
.sage-kpi {
  background: var(--sage-paper-2);
  border: 1px solid var(--sage-line-1);
  border-radius: var(--ad-r-data);
  padding: var(--sp-4);
  display: flex; flex-direction: column; gap: var(--sp-1);
}
.sage-kpi__l { font-size: var(--fs-11); font-weight: 400; color: var(--sage-ink-muted); letter-spacing: .02em; }
.sage-kpi__v {
  font-size: var(--sp-5); font-weight: 500; color: var(--sage-ink);
  font-family: var(--font-mono); font-variant-numeric: tabular-nums; line-height: 1.2;
}
.sage-kpi__c { font-size: var(--fs-11); color: var(--sage-ink-2); display: flex; align-items: center; gap: var(--sp-1); }
/* 环比色点：色**只做圆点**，文字保持 --sage-ink */
.sage-kpi__c::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%;
  background: var(--sage-sig-low); flex-shrink: 0;
}
.sage-kpi--up   .sage-kpi__c::before { background: var(--sage-sig-high); }
.sage-kpi--down .sage-kpi__c::before { background: var(--sage-sig-ok); }
.sage-kpi--flat .sage-kpi__c::before { background: var(--sage-sig-low); }

/* ------------------------------ ② 珠链柱状图（SAGE 标志元素） ------------------------------
   铁律⑥「数据必须离散」—— 柱子由**圆点串成**，不是实心矩形。 */
.sage-bead {
  display: flex; align-items: flex-end; gap: var(--sp-2);
  height: 160px; padding: var(--sp-5) var(--sp-4) var(--sp-3);
  background: var(--sage-paper-2);
  border: 1px solid var(--sage-line-1); border-radius: var(--ad-r-data);
}
.sage-bead__col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: var(--sp-1); height: 100%; }
.sage-bead__stack {
  flex: 1; width: 100%; display: flex; flex-direction: column-reverse;
  align-items: center; justify-content: flex-start; gap: 3px; overflow: hidden;
}
.sage-bead__dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--sage-olive-3); flex-shrink: 0;
  animation: sage-bead-in 320ms var(--ease) both;
}
.sage-bead__dot--hot  { background: var(--sage-sig-mod); }   /* 高值：暖色点 */
.sage-bead__dot--peak { background: var(--sage-sig-high); }  /* 峰值：最亮 */
.sage-bead__x { font-size: var(--fs-10); color: var(--sage-ink-muted); font-family: var(--font-mono); }
@keyframes sage-bead-in { from { opacity: 0; transform: scale(.4); } to { opacity: 1; transform: scale(1); } }

/* ------------------------------ ③ 分段进度条（SAGE 标志元素） ------------------------------
   铁律⑥：进度条 = **分段方块**，不是一条连续填充。 */
.sage-seg { display: flex; flex-direction: column; gap: var(--sp-2); }
.sage-seg__row { display: flex; align-items: center; gap: var(--sp-3); }
.sage-seg__l { font-size: var(--fs-11); color: var(--sage-ink-muted); width: 88px; flex-shrink: 0; }
.sage-seg__track { flex: 1; display: flex; gap: 3px; height: 14px; }
.sage-seg__blk {
  flex: 1; border-radius: var(--r-2); background: var(--sage-paper-sunk);
  transition: background var(--dur) var(--ease);
}
.sage-seg__blk.is-on   { background: var(--sage-olive-3); }
.sage-seg__blk.is-warn { background: var(--sage-sig-mod); }
.sage-seg__blk.is-high { background: var(--sage-sig-high); }
.sage-seg__v {
  font-size: var(--fs-11); font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  color: var(--sage-ink); width: 52px; text-align: right; flex-shrink: 0;
}

/* ------------------------------ ④ 方格热力图（SAGE 标志元素） ------------------------------ */
.sage-heat { display: grid; gap: 3px; }
.sage-heat__c {
  aspect-ratio: 1; border-radius: var(--r-2); background: var(--sage-lime-1);
  /* ★★ 2026-10-03 SAGE 落地（批 S1）：原来写 `var(--dur-hover,100ms)` —— **该令牌不存在**，
     只是靠 `,100ms` 兜底才没失效（`--dur-hover` 是**样机**的名字，我们这边没有）。
     ⇒ 改为 `--ad-dur-hover`（本批在 tokens.css 新立，值 = 样机的 100ms）。
     ⚠️ 这类"靠兜底值活着"的写法不报错、但会让"改令牌"这个动作静默无效 ——
        正是本项目最贵的一类坑（变量名拼错不报错），故本批顺手清掉。
        ⚠️⚠️ 顺带改掉后**视觉零变化**（兜底值与新令牌值都是 100ms）—— 这是"清理"不是"改动"。
     ★ 用 `--ad-dur-hover` 而非新增 `--sage-dur-hover`：SAGE 组件**本来就混用**两套
       （同文件里 `.sage-kpi` 的圆角就是 `--ad-r-data`）⇒ 不再多一层别名。 */
  transition: transform var(--ad-dur-hover) var(--ease);
}
.sage-heat__c:hover { transform: scale(1.18); }
.sage-heat__c--1 { background: var(--sage-lime-2); }
.sage-heat__c--2 { background: var(--sage-lime-3); }
.sage-heat__c--3 { background: var(--sage-lime-4); }
.sage-heat__c--4 { background: var(--sage-olive-3); }
.sage-heat__c--5 { background: var(--sage-olive); }

/* ------------------------------ ⑤ 虚线分组 / 实线分栏（铁律⑤） ------------------------------ */
.sage-grp {
  border: 1px dashed var(--sage-line-1); border-radius: var(--ad-r-data);
  padding: var(--sp-4); margin-top: var(--sp-4);
}
.sage-grp__t {
  font-size: var(--fs-11); font-weight: 500; color: var(--sage-ink-muted);
  letter-spacing: .06em; text-transform: uppercase; margin-bottom: var(--sp-3);
}
.sage-cols { display: flex; gap: 0; }
.sage-cols > * { flex: 1; padding: 0 var(--sp-4); }
.sage-cols > * + * { border-left: 1px solid var(--sage-line-1); }

/* ------------------------------ ⑥ 橄榄底卡（概览「销售指数」用） ------------------------------ */
.sage-olive-card {
  background: var(--sage-olive); color: var(--sage-lime-1);
  border-radius: var(--ad-r-data); padding: var(--sp-5);
  display: flex; flex-direction: column; gap: var(--sp-2);
}
.sage-olive-card__l { font-size: var(--fs-11); color: var(--sage-lime-2); letter-spacing: .04em; }
.sage-olive-card__v {
  font-size: var(--sp-6); font-weight: 500; font-family: var(--font-mono);
  font-variant-numeric: tabular-nums; line-height: 1.1; color: var(--sage-lime-1);
}

/* ------------------------------ ⑦ 报表四段式容器 ------------------------------ */
.sage-rep { display: flex; flex-direction: column; gap: var(--sp-4); margin-top: var(--sp-4); }
.sage-rep__sum {
  display: flex; align-items: center; gap: var(--sp-5);
  background: var(--sage-paper-2); border: 1px solid var(--sage-line-1);
  border-top: 2px solid var(--sage-olive); border-radius: var(--ad-r-data);
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-13); font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  color: var(--sage-ink);
}
.sage-rep__sum b { font-weight: 500; color: var(--sage-olive); }
.sage-rep__sum .sp { flex: 1; }

/* 珠链柱「零基线」点：值为 0 的列画一颗淡点（标明位置，不是数据） */
.sage-bead__dot--zero { background: var(--sage-paper-sunk); opacity: .8; }

/* ================================================================ ★★★ C1（2026-10-06）
   销售单详情页**内嵌业务已签**（本单 / 送货单 / 收款单）—— 新类 `.ad-dtab*`。
   ★ 为什么**另起 `.ad-dtab*`**、不复用 `.ad-tabs` / `[data-formtab]` / `data-tabscope`：
     那一套是「**表单字段分组**页签」（产品编辑 / 站点设置 / 单据字段页）的机制，
     当前态写 S.pdTab / S.siteTab / S.dfTab…；内嵌业务签是**另一件事**（语义 / 作用域 /
     渲染位置都不同），复用会让“切内嵌签”与“切表单分组签”互相 hidden（同名撞车）。
   ★ 视觉走 SAGE 鼠尾草终端风：暖纸底、橄榄墨、**零阴影**、虚线分组、实线分栏
     —— 与 `.ad-tabs` 同一底色口径（透明底 ＋ 1px 下边线 ＋ 2px 橄榄选中条）。
   ⚠️ 只引用令牌、**不写任何 hex**（守 SAGE「无硬编码」那条）。 */
.ad-dtabs {
  display: flex; align-items: flex-end; flex-wrap: wrap; gap: var(--sp-5);
  border-bottom: 1px dashed var(--ad-line-2);
  margin: var(--sp-2) 0 var(--sp-4);
}
.ad-dtabs__item {
  padding: 0 0 var(--sp-3);
  border: 0; border-radius: 0; background: none;
  color: var(--ad-muted);
  font: inherit; font-size: var(--fs-13); line-height: 1.5;
  letter-spacing: var(--ls-wide);
  cursor: pointer;
  transition: color var(--ad-dur-hover) var(--ad-ease-out);
}
.ad-dtabs__item:hover { color: var(--ad-ink-2); }
.ad-dtabs__item.is-on {
  background: none; border-color: transparent;
  box-shadow: inset 0 -2px 0 var(--ad-accent);
  color: var(--ad-ink); font-weight: var(--fw-medium);
}
.ad-dtabpanel[hidden] { display: none; }
/* 面板内部与 `.ad-tabpanel` 同口径：grid + 单列 + 同一档间距（不新造第二套间距）。 */
.ad-dtabpanel { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-3); }

/* ── ★ 批 G3（2026-10-04）：销售单明细「图片」列的缩略图 ──
   28px 小图。★ 与列表页的 `.ad-table img.thumb`（40×50 竖版大图）分开 ——
   明细行的图片只是“一眼认出是哪款”，不该把行高撑起来。 */
table.ad-table--lines td img.ad-line-thumb {
  width: 28px; height: 28px; object-fit: cover;
  border-radius: var(--r-1); display: block; margin: 0 auto;
}

/* ── ★★★ 批 G4（2026-10-05）：**保存校验没过时的红框**（`admin.js` 的 `salesMarkBad()` 挂的类）──
   余总口径：缺客户 / 缺型号 / 缺规格 ⇒ **跳到那一格 ＋ 标红 ＋ 说第几行**。
   ★ 为什么用 `outline` 而不是 `border`：明细格是 `<td>` 里的 `<input>`，
     改 `border` 会让格子**尺寸变一下**（边框要占位）⇒ 整行跟着抖；`outline` **不占布局**。
   ★ 颜色一律取 SAGE 那套**已有的**错误色变量（`--ad-err-text` / `--ad-err-line` / `--ad-err-bg`，
     见 `.mini--danger` / `.ad-cf__danger`）—— **不新造一个红**，免得两处红不是一个红。
   ★ `td.ad-bad-td` 是"把整个格子也圈一下"：单看 input 的小红框，在密密麻麻的明细行里不够显眼。 */
.ad-bad {
  outline: 2px solid var(--ad-err-text) !important;
  outline-offset: 1px;
  background: var(--ad-err-bg) !important;
  border-color: var(--ad-err-line) !important;
}
td.ad-bad-td {
  box-shadow: inset 0 0 0 2px var(--ad-err-text);
  background: var(--ad-err-bg);
}
/* ★★ 批 G13（2026-10-05）：送货单单头「关联单号」—— 只读格里的一枚链接（跳来源销售单）。
   尺寸 / 底色照相邻只读格（30px 高 · --ad-bg-alt）⇒ 在单头网格里看起来就是一格、不突兀；
   颜色走全局链接令牌 --ad-link（#263819，与全站链接一致）。 */
.ad-headgrid .ad-headlink {
  display: inline-flex; align-items: center; height: 30px; padding: 0 var(--sp-2);
  border: 1px solid var(--ad-field-line); border-radius: var(--r-2);
  background: var(--ad-bg-alt); color: var(--ad-link);
  font-size: var(--fs-12); text-decoration: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ad-headgrid .ad-headlink:hover { text-decoration: underline; }

/* ★★ 批 H2（2026-10-05）：销售单「未保存 · 实时试算」态 ——
   金额行**如实**区别于「已存好的数」（口径：界面不许拿一个“也许对”的数冒充库里的数）。
   ★ 只换外壳（淡暖砂底 ＋ 虚线边）＋ 右下角一句标注，**数字本身不虚化**（要看得清）。
   ★ 令牌全用现成的（`--ad-warn-bg` = #F5EEDC、`--ad-muted-ink`），**不新加色**（遵 R7 换皮纪律）。 */
.ad-moneyrow.is-trial { background: var(--ad-warn-bg); border-style: dashed; }
.ad-moneyrow__trial { flex-basis: 100%; text-align: right; margin-top: 2px;
  font-size: var(--fs-11); color: var(--ad-muted-ink); letter-spacing: .04em; }

/* ================================================================ ★ I3（2026-10-05）
   自研 Sparkline（纯内联 SVG · 零依赖 · 零 CDN）。
   ★★★ 取色**一律走后台既有令牌** --ad-*（SVG 内联属性里写 var(...)），**不新增一支颜色**。
   ★★ 三条硬约束：① 亮色系只做填充/描边/圆点、不作文字色；② **零阴影**；③ 数字等宽。 */
.ad-spark { display: block; width: 100%; }
.ad-sparkrow { margin-top: var(--sp-3); }
.ad-sparkx { display: flex; margin-top: 2px; }
.ad-sparkx span {
  flex: 1 1 0; min-width: 0; text-align: center;
  font-size: var(--fs-10); line-height: 1.5; color: var(--ad-muted-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* ================================================================ ★★ 批 I · I2（2026-10-05）
   菜单管理页 ＋ 图标选择器 —— 五件小样式。

   ★★ 口径（与全站一致，别破例）：
     · 间距 / 圆角 / 字号**一律走令牌**（`--sp-*` / `--r-*` / `--fs-*`）—— 不写字面量
     · 颜色**一律走 `--ad-*`** —— 本批**不新增一支颜色**（SAGE 换皮纪律）
     · **零阴影**（SAGE 七铁律）
     · 图标尺寸只用现成两档 `--ad-ico-s` (16) / `--ad-ico-l` (24) */
/* 分组概览：一行「分组 × 项数」的签（只读 —— 拖拽排序不在本批） */
.ad-menu-chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-3); }
.ad-menu-chip {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-3);
  border: 1px dashed var(--ad-line);
  border-radius: var(--r-2);
  font-size: var(--fs-12); color: var(--ad-ink-2);
}
.ad-menu-chip em { font-style: normal; color: var(--ad-muted-ink); font-variant-numeric: tabular-nums; }
/* 表格「图标」列：缩到 **16 档** —— 24 档会把整行顶高（表格里是配角，不是主角） */
.ad-menu-ico { display: inline-flex; vertical-align: middle; margin-right: var(--sp-2); color: var(--ad-ink-2); }
.ad-menu-ico svg { width: var(--ad-ico-s); height: var(--ad-ico-s); }
/* 编辑页那张"侧栏上会长这样"的预览：一个虚线小框，里面就是真实的侧栏两件 */
.ad-menu-prev {
  display: inline-block; margin-left: var(--sp-3); vertical-align: middle;
  padding: var(--sp-2) var(--sp-3);
  border: 1px dashed var(--ad-line); border-radius: var(--r-2);
  background: var(--ad-bg-sunk);
  min-width: 160px;
}
/* 预览里那一条菜单项：**不可点**（`.is-off` 另给一个"已隐藏"的淡色态） */
.ad-menu-prev .ad-nav__item { cursor: default; }
.ad-nav__item.is-off { color: var(--ad-ink-off); }
/* 图标选择器：下拉 ＋ 右边一枚实时预览（光看代号 `report_flow` 看不出长什么样） */
.ad-icopick { display: inline-flex; align-items: center; gap: var(--sp-2); }
.ad-icopick__prev {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--sp-6); height: var(--sp-6);
  border: 1px solid var(--ad-line); border-radius: var(--r-2);
  color: var(--ad-ink-2);
}

/* ══════════════════════════════════════════════════════════════════════════
   ★★★ 批 K3（2026-10-06）· 产品主数据「一表三层」（形态 A）
   ---------------------------------------------------------------
   产品行 → 规格展开行 → 四档价（出厂 / 优惠 / VIP / 零售，**分列**）。
   整页表格，与「系列 / 品类 / 空间案例」三签同形（`adHeadOf` + `adCellsOf`）。
   ★ SAGE 口径：暖纸底 · 橄榄墨 · **零阴影** · 虚线分组实线分栏 · 数据等宽。
   ⚠️ 选择器一律带父级（`.ad-table .x`）—— 基础规则 `.ad-table td` 是 (0,1,1)，
      单写 `.ad-p3tbl__x` 只有 (0,1,0)，**会被基础规则盖掉且不报错**（本项目血训形状）。
   标记：K3-P3-CSS
   ══════════════════════════════════════════════════════════════════════════ */

/* 面板头的控件比别的页多（搜索 / 排序 / 快筛 / 列设置 / 导入导出 / ＋新建）——
   放不下就换行，不要横向挤成一条。 */
.ad-panel__head.ad-panel__head--wrap { flex-wrap: wrap; row-gap: var(--sp-2); }
.ad-panel__head.ad-panel__head--wrap > .ad-q { flex: 0 1 200px; min-width: 160px; }
.ad-panel__head.ad-panel__head--wrap > .ad-chips { flex: 0 0 auto; }
.ad-panel__head.ad-panel__head--wrap > .spacer { flex: 1 1 0; }

/* ---- ① 展开列：**自成一列**（不做进封面格里）----
   ★ 为什么：用户用「列设置」一关封面列，展开入口不能跟着消失
     —— 那是一个"没有任何提示就没了"的功能（本项目铁律：不许静默）。 */
.ad-table th.ad-p3tbl__tgcol { width: 36px; padding-right: 0; }
.ad-table td.ad-p3tbl__tgcol { width: 36px; padding: var(--sp-3) 0 var(--sp-3) var(--sp-3); }
.ad-p3tbl__tg {
  width: 22px; height: 22px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid transparent; border-radius: var(--r-2);
  background: transparent; color: var(--ad-ink-2); cursor: pointer;
  transition: transform var(--tr), color var(--tr), border-color var(--tr);
}
.ad-p3tbl__tg:hover { color: var(--ad-ink); border-color: var(--ad-ctl-line); }
.ad-p3tbl__tg.is-open { transform: rotate(90deg); color: var(--ad-accent); }
.ad-p3tbl__tg:focus-visible { outline: 2px solid var(--ad-accent); outline-offset: 1px; }

/* ---- ② 封面格：32px 小图（与明细行同档，**不撑行高**）---- */
.ad-table td.ad-p3tbl__cover { width: 44px; padding-left: var(--sp-3); padding-right: var(--sp-3); }
.ad-p3tbl__cover img {
  width: 32px; height: 32px; object-fit: cover;
  border-radius: var(--r-2); display: block; margin: 0 auto;
}
.ad-p3tbl__ph {
  display: block; width: 32px; height: 32px; margin: 0 auto;
  border: 1px dashed var(--ad-line-3); border-radius: var(--r-2); background: var(--ad-bg-soft);
}

/* 型号可点（进三栏编辑页）。★ 保持表格读感，不做成大按钮。 */
.ad-p3tbl__row a { color: var(--ad-ink); text-decoration: none; }
.ad-p3tbl__row a:hover { color: var(--ad-accent); text-decoration: underline; }

/* 版本标签（本型号做哪几档）—— 一格里可能有多个档 */
.ad-p3tbl__tag {
  display: inline-block; margin: 0 2px; padding: 1px 6px;
  font-size: var(--fs-11); line-height: 1.5;
  border: 1px solid var(--ad-line-3); border-radius: var(--r-2);
  color: var(--ad-ink-2); white-space: nowrap;
}

/* 状态：已发布 / 草稿 —— 只用文字色区分（不做彩色块，SAGE 的警示色只留给"真该报警"的） */
.ad-p3tbl__pub { color: var(--ad-ok-text); }
.ad-p3tbl__draft { color: var(--ad-muted-ink); }

/* ---- 展开着的产品行：底色下沉一档，与它的展开块连成一体 ---- */
.ad-table tr.ad-p3tbl__row.is-open > td { background: var(--ad-bg-alt); }
.ad-table tr.ad-p3tbl__row.is-open:hover > td { background: var(--ad-bg-sunk); }

/* ---- ③ 展开行：**整行一个跨列 td**，里面放规格子表 ---- */
.ad-table tr.ad-p3tbl__exp > td { padding: 0; background: var(--ad-bg-alt); }
.ad-table tr.ad-p3tbl__exp:hover > td { background: var(--ad-bg-alt); }   /* 盖掉 `tr:hover td` —— 展开块不该跟着变深 */
.ad-p3tbl__expbox { padding: var(--sp-3) var(--sp-4) var(--sp-4) var(--sp-5); }

/* ---- ④ 规格子表（第二、三层）---- */
.ad-table.ad-p3sub { width: 100%; border: 1px solid var(--ad-line); background: var(--ad-bg); }
.ad-table.ad-p3sub th {
  padding: var(--sp-2) var(--sp-3);
  background: var(--ad-bg-sunk);
  border-bottom: 1px solid var(--ad-line-3);
  font-size: var(--fs-th); letter-spacing: .1em;
  color: var(--ad-th-ink); white-space: nowrap;   /* ★ 2026-10-07 */
}
.ad-table.ad-p3sub td {
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--ad-line);
  color: var(--ad-ink); vertical-align: middle;
}
.ad-table.ad-p3sub tbody tr:last-child td { border-bottom: 0; }
.ad-table.ad-p3sub tbody tr:hover td { background: var(--ad-bg-sunk); }
.ad-table.ad-p3sub .ad-p3sub__no {
  width: 36px; color: var(--ad-muted-ink); font-variant-numeric: tabular-nums;
}
.ad-table.ad-p3sub .ad-p3sub__img { width: 44px; }
.ad-p3sub__img img {
  width: 28px; height: 28px; object-fit: cover;
  border-radius: var(--r-1); display: block; margin: 0 auto;
}
.ad-table.ad-p3sub .num { font-variant-numeric: tabular-nums; color: var(--ad-ink-2); }

/* ★ 退基价的那一格（该档没单独配、按基价显示）：灰一档 + 点线下划。
   为什么必须能看出来：报价口径是"未配档位按基价"，但列表里若**看起来**就是零售价 85，
   人会以为真配过零售价 —— 那是本项目最贵的一类错（不报错、只是不对）。 */
.ad-table.ad-p3sub td.is-fell {
  color: var(--ad-muted-ink);
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}

.ad-p3sub__foot {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) 0 0; font-size: var(--fs-11); color: var(--ad-muted-ink);
}
.ad-note.ad-p3sub__note { margin: 0; }
/* ================================================================ K3-P3-CSS2（2026-10-06）
   产品页「一表三层」列宽定型 —— 真浏览器实测（1440 视口 · 23 行 × 10 数据列）：

     方案                             表宽    越出面板   折行格
     现状（内边距 sp-4，不 nowrap）     1170    -1         39
     数据列全 nowrap（内边距不动）      1201    +30 ★      4
     数据列全 nowrap ＋ 内边距 sp-3     1170    -1         4

   ⇒ ① 数据列一律 `nowrap`：型号 `HM-D021` 断成 "HM-" / "D021" 两行会被读成两个东西
        （实测折行的是 HM-D021 / HM-B002 / HM-T006 / HM-D022 —— 码长 ≥7 就折）。
      ② 内边距从 `sp-4`(16px) 收到 `sp-3`(12px)，把省下的宽度让给内容 —— **这步必须**：
         不加就撑破面板 30px。为什么加了会撑破：`table-layout:auto` ＋ `width:100%`
         只保证「**至少** 100%」，内容 min-content 更大时照样溢出；而桌面档 `.ad-body`
         **没有**横向滚动兜底（`overflow-x:auto` 只在窄屏那档媒体查询里）。
      ③ 只吃**本表直系** th/td：规格子表 `table.ad-p3sub` 嵌在展开行的 `<td>` 里，
         用后代选择器（`.ad-p3tbl thead th`）会连它一起改 —— 子表自己的内边距被顶掉。

   ★ 用 `:not(.ad-p3tbl__tgcol)` 而不是 `nth-child(n+2)`：用户用「列设置」关掉一列，
     位置会整体前移，`nth-child` 会**静默错位**（去 nowrap 了别的列 / 漏掉真数据列）。
     `tgcol`（展开箭头列）必须排除 —— 它自带 `padding-right:0`，加了就与箭头不贴。
     `.ad-table__actions` 也已自带 `nowrap`，重复无害但不必要。 */

.ad-table.ad-p3tbl > thead > tr > th:not(.ad-p3tbl__tgcol),
.ad-table.ad-p3tbl > tbody > tr.ad-p3tbl__row > td:not(.ad-p3tbl__tgcol) {
  padding-left: var(--sp-3);
  padding-right: var(--sp-3);
}
.ad-table.ad-p3tbl > tbody > tr.ad-p3tbl__row > td:not(.ad-p3tbl__tgcol):not(.ad-table__actions) {
  white-space: nowrap;
}

/* ==========================================================================
   K3-P3-CSS3（2026-10-06 · 批 K3-B）：产品展开区的「行内折叠页签」
   ---- 页签条（规格 / 用料 / 供货 / 库存 / 素材 / 空间）＋ 各页签的面板 ----
   ★★ 与 K3-A 的关系：把 `.ad-p3tbl__expbox` 的内边距**原值挪到 `.ad-p3pane`** ——
      这样页签条能贴到展开块的左右边缘（正常的标签页形态），而页签 1「规格」里那张表
      的落点与 K3-A **逐像素一致**（同一个 padding 值，只是换了个宿主）。
      ⚠️ 改 expbox 的 padding 前先看这句：两个值是一对，单独改一个会错位。
   ========================================================================== */

.ad-p3tbl__expbox { padding: 0; }

/* ---- 页签条 ---- */
.ad-p3tabs {
  display: flex; flex-wrap: wrap; gap: 2px;
  padding: 0 var(--sp-2);
  background: var(--ad-bg-sunk);
  border-bottom: 1px solid var(--ad-line);
}
.ad-p3tab {
  appearance: none; border: 0; background: transparent;
  padding: var(--sp-2) var(--sp-3);
  font: inherit; font-size: var(--fs-12); line-height: 1.5;
  color: var(--ad-muted-ink);
  border-bottom: 2px solid transparent;
  display: inline-flex; align-items: center; gap: 6px;
  cursor: pointer;
}
.ad-p3tab:hover { color: var(--ad-ink-2); background: var(--ad-bg-alt); }
.ad-p3tab.is-on {
  background: var(--ad-bg-alt);
  border-bottom-color: var(--ad-accent);
  color: var(--ad-ink); font-weight: 700;
}
.ad-p3tab__n {
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-10); line-height: 1.6;
  min-width: 18px; padding: 0 5px; border-radius: var(--r-pill);
  background: var(--ad-line); color: var(--ad-ink-2); font-weight: 400; text-align: center;
}
.ad-p3tab.is-on .ad-p3tab__n { background: var(--ad-accent); color: var(--ad-bg-alt); }

/* ---- 页签面板（内边距 = K3-A 时 expbox 的原值）---- */
.ad-p3pane { padding: var(--sp-3) var(--sp-4) var(--sp-4) var(--sp-5); }

/* ---- 面板里行尾那个「跳去原页改」的链接 ---- */
.ad-p3lnk {
  display: inline-block; padding: 1px 8px;
  border: 1px solid var(--ad-line); border-radius: var(--r-2);
  background: var(--ad-bg); color: var(--ad-ink-2);
  font-size: var(--fs-11); text-decoration: none; white-space: nowrap;
}
.ad-p3lnk:hover { background: var(--ad-bg-sunk); color: var(--ad-ink); border-color: var(--ad-line-3); }

/* ⚠️ `.ad-table.ad-p3sub td`（0,2,1）会盖掉全局 `.ad-table__actions`(0,2,0) ⇒
   这里补一条，让「操作」列仍然右对齐（否则链接会按表格的居中/默认对齐）。 */
.ad-table.ad-p3sub td.ad-table__actions { text-align: right; white-space: nowrap; }

/* ---- 供货页签：「默认」标 ---- */
.ad-p3sup__def {
  display: inline-block; padding: 0 6px; border-radius: var(--r-pill);
  font-size: var(--fs-10);
  border: 1px solid var(--ad-accent); color: var(--ad-accent);
}

/* ---- 素材页签：缩略图墙（同一张图被几个字段用，就列几格）---- */
.ad-p3media { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.ad-p3media__i {
  margin: 0; width: 124px;
  border: 1px solid var(--ad-line); border-radius: var(--r-2);
  background: var(--ad-bg); overflow: hidden;
}
.ad-p3media__i img {
  display: block; width: 100%; height: 84px; object-fit: cover; background: var(--ad-bg-sunk);
}
.ad-p3media__i figcaption {
  padding: 3px 6px; font-size: var(--fs-10); color: var(--ad-muted-ink);
  border-top: 1px solid var(--ad-line);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ==========================================================================
   ★★★ 批 E2E-4（2026-10-06）：打印设置里的「纸型档位」一行
   --------------------------------------------------------------------------
   销售单 / 送货单各自印哪一张纸（小票版 = 新 · 默认 / 正式版 = 旧）。
   零阴影、暖纸底（SAGE 七铁律）；类名带 `ad-pfv` 前缀防撞车（本项目头号病：同名撞车）。
   ========================================================================== */
.ad-pfvrow {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  background: var(--ad-bg-sunk);
  border-radius: var(--r-2);
}
.ad-pfvrow__t { font-weight: var(--fw-medium); color: var(--ad-ink); white-space: nowrap; }
/* ⚠️ `flex: 0 0 auto` ＋ `nowrap` 两个都要：只写 `nowrap` 时 flex 仍会把这一格**压窄**，
   于是「销售单」被拆成「销售 / 单」竖着两行（2026-10-06 截图目视逮到）。 */
.ad-pfv { display: inline-flex; align-items: center; gap: var(--sp-2); flex: 0 0 auto; }
.ad-pfv__n { color: var(--ad-ink-2); font-size: var(--fs-12); white-space: nowrap; }
.ad-pfvrow__h {
  flex-basis: 100%; margin-top: var(--sp-1);
  font-size: var(--fs-11); color: var(--ad-muted-ink); line-height: 1.65;
}
/* ==========================================================================
   ★★ 2026-10-06 批 SKIN-1（余总拍板）：后台皮肤机制 + 第一套皮肤「Excel 绿」
   --------------------------------------------------------------------------
   余总原话链：「我们可以做一套皮肤吗」→「全库统一」→「做成 EXCEL 风格」
   → 「浅绿底深绿字 不要有大块蓝 仅文字有部分蓝 绿色改为 Excel 那套绿 低饱和度的」
   →「视觉一定要克制」。

   ★ 为什么只改本文件就能换皮：经实测复核（2026-10-06），admin.css 里
     **生效样式零裸色值** —— 所有颜色都走 var(--ad-*)。换皮肤 = 换一组变量值。
   ★ 切换口径：<html data-skin="sage|excel">，sage = 默认（现役 SAGE），excel = 本批新增。
   ★ 偏好记忆：localStorage['adSkin']。
   ========================================================================== */

/* ---- ① 后台字号整体上调（余总：「字太小了 后台管理 工作太累了」）----
   原本 82% 的后台文字 ≤12px ⇒ 主力档 12→14。
   ⚠️ 必须写在这里而不是 tokens.css：tokens.css 是前后台共用的，
      改它会把前台字号一起放大（破「前台一个值不动」）。
   admin.css 只在 admin/index.html 里加载 ⇒ 天然只作用于后台。 */
:root {
  --fs-10: 12px;
  --fs-11: 13px;
  --fs-12: 14px;
  --fs-13: 15px;
  --fs-14: 16px;

  /* ★★ 2026-10-07 表格字号口径（余总定）：「表头字体应该是 14  表里面的字体应该保持 12」。
     ⚠️ **别直接写 --fs-12 / --fs-10** —— A 段把变量**值**抬了一档（10→12 · 13→15），
        `号`与`值`已经**错位**：`--fs-10` 其实 = 12px、`--fs-12` 其实 = 14px。
        直接换引用会留坑（写成“表头用 10 号变量、却显示 14px”）。
     ⇒ 给表格**立语义变量**，口径只此一处（下面四处引用它）： */
  --fs-th: 14px;
  --fs-td: 12px;

  /* ★★ BJ-1（2026-10-07 · 余总「全套」）：**中文字体族换序** + **控件圆角 4→6**。
     ⚠️ 为什么必须写在这里：`--font-sans` 定义在 tokens.css（前后台共用），
        改它会动到前台 ⇒ 只在 admin.css 的 `:root` 覆盖（admin.css 仅后台加载）。
     ★ 字体族真相（2026-10-07 用 CDP `CSS.getPlatformFontsForNode` 实测）：
       现状中文**落在「思源黑体 Medium」**（笔画厚、字面大）—— 因为原栈里
       `Microsoft YaHei` 排在 `Source Han Sans CN` **之后**，Windows 上永远轮不到它。
       样机锁「微软雅黑 Regular」⇒ 这就是「明显更精致」的**最大一条根因**。
       本次只把 `Microsoft YaHei` **提前**，中文栈整体口径不变（Mac 仍走 PingFang SC）。 */
  --font-sans: "Outfit", -apple-system, BlinkMacSystemFont, "Segoe UI",
               "PingFang SC", "Microsoft YaHei", "Source Han Sans CN",
               "Source Han Sans SC", "Hiragino Sans GB", "Noto Sans CJK SC",
               "Noto Sans SC", Arial, sans-serif;

  /* ★ 控件圆角 4 → 6（样机 `.sel/.fsearch/.tbtn` 全是 6px）。
     tokens.css:451 定义 4px ⇒ 此处覆盖（全站控件一并生效，属「全套」本意）。 */
  --ad-r-field: 6px;
}

/* ---- ①b 表头字色（皮肤相关）----
   ★★ 2026-10-07（余总看图定「丙」）：Excel 皮肤下**表头文字**用深绿 `#375623`，
      **表头底一律不动**（仍是背景灰 `#F7F7F7`）。
   ⚠️ 为什么不直接覆盖 `--ad-muted-ink`：它全站 20+ 处引用（输入框提示 / 页码 /
      说明字 / 规格表「无」值…），覆盖它会把一大批**非表头文字**一起染绿。
   ⇒ 只有「表头」这一类取 `--ad-th-ink`；SAGE 下 = `--ad-muted-ink`（**逐像素不变**）。 */
:root { --ad-th-ink: var(--ad-muted-ink); }

/* ---- ② 皮肤：Excel 绿 ----
   色板（余总 10-06 定）：主绿 #217346 · 表头浅绿底 #E2EFDA · 成功 #548235 ·
   文字 #1A1A1A / #666666 · 边框 #E5E5E5 · 表头下沿 #D9D9D9 · 背景灰 #F7F7F7。
   ★ 不动项：--ad-docno #E4757E（余总定死的单据单号玫红）· 语义警告/错误色（待余总定）。 */
:root[data-skin="excel"] {
  /* 底 */
  --ad-bg:        #FFFFFF;
  --ad-bg-alt:    #F7F7F7;
  --ad-bg-sunk:   #EFEFEF;
  --ad-bg-soft:   #FAFAFA;
  --ad-white:     #FFFFFF;
  --ad-zebra:     #F7F7F7;

  /* 线 */
  --ad-line:      #E5E5E5;
  --ad-line-2:    #D9D9D9;
  --ad-line-3:    #D9D9D9;
  --ad-field-line:#8C8C8C;
  --ad-ctl-line:  #B3B3B3;

  /* 字 */
  --ad-ink:       #1A1A1A;
  --ad-ink-2:     #444444;
  --ad-muted-ink: #666666;
  /* ★ 仅表头用（余总定「深绿字」）—— `#375623` 压表头底 `#F7F7F7` 实测 **7.79:1**（旧色 #666666 只有 5.36:1）。 */
  --ad-th-ink:    #375623;
  --ad-muted:     #8C8C8C;
  --ad-ink-off:   #BFBFBF;
  --ad-placeholder:#8C8C8C;

  /* 主色 + 淡底 */
  --ad-accent:    #217346;
  --ad-accent-2:  #1B5E39;
  --ad-accent-3:  #164A2D;
  --ad-acc-bg:    #E2EFDA;
  --ad-acc-line:  #A9D18E;
  --ad-lime-2:    #E2EFDA;
  --ad-lime-3:    #D5E8C8;

  /* 语义（成功改用 Excel 绿族；警告/错误暂留原值待余总定） */
  --ad-ok:        #548235;
  --ad-ok-bg:     #E2EFDA;
  --ad-ok-line:   #A9D18E;
  --ad-ok-text:   #375623;

  /* 链接 —— 余总：「仅文字有部分蓝就可以了」⇒ 链接用低饱和蓝，其余全绿 */
  --ad-link:      #2F6FB5;
  --ad-link-2:    #215490;

  /* 填充 */
  --ad-fill-off:  #E5E5E5;
}

/* ★★ 2026-10-07（批 BC-2）：Excel 皮肤下**所有**表头格的文字统一深绿。
   为什么还要这一块：分布统计逗到四条「比通用表头更专」的规则把部分表头拉走了：
     · `.ad-table .num`（0,2,0）、`.ad-table.ad-p3sub .num`（0,3,0） —— 数字列头
     · `.ad-table.ad-p3sub .ad-p3sub__no`（0,3,0）、`table.ad-lines .rowno`（0,2,1） —— 行号列头
   ★ 这四条在 **SAGE 下各自有其色**（深一档的橄榄 / 就是表头色）—— **不许动**。
   ⇒ 只在 Excel 皮肤里覆盖（选择器带 `:root[data-skin="excel"]` 前缀 ⇒ SAGE 连匹配都不匹配）。
   ★ td 侧一个字不动：行号 / 数字**格**仍是各自的灰。 */
:root[data-skin="excel"] .ad-table th.num,
:root[data-skin="excel"] .ad-table.ad-p3sub th.num,
:root[data-skin="excel"] .ad-table.ad-p3sub th.ad-p3sub__no,
:root[data-skin="excel"] table.ad-lines th.rowno { color: var(--ad-th-ink); }

/* ★ SKIN-1（2026-10-06）：皮肤按钮激活态。类名新立（grep 确认未占用）。 */
.ad-top .btn--on { background: var(--ad-acc-bg); border-color: var(--ad-acc-line); color: var(--ad-accent); }

/* ══════════════════════════════════════════════════════════════════════════
   ★★★ 批 CLEAN-1（2026-10-06 · 余总「先砍再做皮肤」「一口气做完」）
   「视觉克制」全站通则（余总 10-06 定死，见 MEMORY §2A ㉒/㉓）：
     不必要的信息排掉 · 工具能收则收 · 表格可用表内编辑 · 信息多走弹窗
     —— 扩展到后台所有页面 · **可见数据操作信息要占页面的 70% 以上**。
   ⇒ 本批四组：B1 说明条收编 / B2 chrome 瘦身 / B3 工具进表头 / B4 表内编辑。
   量法（唯一判据）：`hub/tools/_tmp/_probe_all_pages.py`（数据占屏率 ≥70%）。
   ══════════════════════════════════════════════════════════════════════════ */

/* ---- B1 说明条收编 -------------------------------------------------------
   132 处 `.ad-note`（单条 54–224px）默认**折成一行 ＋ ⓘ**，点开才展开。
   ★ 关着时显示 `.ad-note__sum`（一行纯文本摘要），展开时显示 `.ad-note__c`（原 DOM）。
   ★ 两个 `min-width:0` 是必需的 —— flex 子项默认 `min-width:auto`，
     会把 `text-overflow:ellipsis` 顶成"撑破容器"（本项目老坑：省略号不生效）。 */
.ad-note--fold { display: flex; align-items: flex-start; gap: var(--sp-2);
  padding: 5px var(--sp-3); cursor: pointer; }
.ad-note--fold:hover { border-color: var(--ad-line-3); }
.ad-note--fold .ad-note__i { flex: none; width: 16px; height: 16px; padding: 0; margin-top: 1px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: transparent; color: var(--ad-muted-ink);
  cursor: pointer; border-radius: var(--r-1); }
.ad-note--fold .ad-note__i:hover { color: var(--ad-accent); }
.ad-note--fold:not(.is-open) .ad-note__c { display: none; }
.ad-note--fold .ad-note__sum { flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ad-note--fold.is-open .ad-note__sum { display: none; }
.ad-note--fold .ad-note__c { flex: 1; min-width: 0; }
/* 展开后：原文常是多个 `<div>`，原先靠 `.ad-note` 的 flex `gap` 排开 ——
   收进 `.ad-note__c` 后 flex 语境变了，这里把段间距补回来（否则几段贴成一块）。 */
.ad-note--fold.is-open .ad-note__c > * + * { margin-top: var(--sp-3); }

/* ---- B2 全局 chrome 瘦身（一处改 · 31 页全生效） ---------------------------
   实测（改前）：顶栏 56 ＋ 页签栏 38 ＋ 页头 56 ＋ 正文上内边距 24 = **174px**
   —— 每一页、在**任何数据出现之前**就先花掉 19% 的屏幕。这是"70% 目标"里
   唯一一处**与页面无关的固定成本**，所以必须先压。
   ★ 三处令牌都在 `admin.css` 覆盖 —— **不动 `tokens.css`**：
     那些令牌是**前后台共用**的（改它会连带改前台，不是本批的范围）。
     写在 admin.css 里同为 `:root`、后加载 ⇒ 只作用于后台。
   ⚠️ 改完必须重量：`_probe_all_pages.py`（首行数据位置会跟着整体上移）。 */
:root {
  --ad-top-h: 48px;    /* 顶栏 56 → 48（装得下 28 高按钮 ＋ 上下各 10） */
  --ad-bar-h: 40px;    /* 页头 56 → 40（标题与副标题**并成一行**，见下） */
}
:root { --ad-tabh: 32px; }   /* 页签栏 38 → 32（覆盖 admin.css 自己那份定义） */

/* ★ 标题 ＋ 副标题**并成一行** —— 这是把页头从 56 压到 40 的前提。
   原布局是两行（标题 24 ＋ 副标题 15 = 39 ＋ 上下内边距 16 ＋ 描边 1 = 56）。
   ⇒ 改成同一基线上并排：主标题在前、副标题在后（弱化色），一行 22 即可装下。
   ⚠️ 副标题必须能缩（`min-width:0`）—— 否则长副标题会把按钮挤出屏幕，
     而这**不会报错**，只是按钮没影了（本项目一类经典静默错）。 */
.ad-bar { padding: 3px var(--sp-5); }
.ad-bar > div:first-child { display: flex; align-items: baseline; gap: var(--sp-3); min-width: 0; }
.ad-bar__title { font-size: var(--fs-16); line-height: 22px; }
.ad-bar__sub { line-height: 14px; min-width: 0; }
/* 正文上内边距 24 → 12（下面紧接的就是面板，双份留白没有意义） */
.ad-body { padding: var(--sp-3) var(--sp-5) var(--sp-5); }

/* ---- B3 面板头（工具行）压成「表头的一块」 ---------------------------------
   现状：`.ad-panel__head` 高 65–73px，与下面的表格是**两条分离的横条**
   （底色不同、中间还有一道线）⇒ 看着像"两个东西"。
   改法（对齐余总给的参考图「表头里可以放很多工具」）：
     ① 高度 73 → **36**（内边距 16/24 → 4/12）
     ② **底色取表头同色 `--ad-bg-alt`**、下沿取表头同线 `--ad-line-3`
        ⇒ 它与 `thead` 合成**同一个视觉块**，读起来就是"表头带工具"
     ③ 按钮/输入框本来就 28px（`.btn--sm` / `.input--sm`），这里只收外框留白
   ★ 面板与上文的间距 `--sp-6`(32) → `--sp-3`(12)：说明条刚被收成一行，
     再留 32 的空档就又把省下的空间还回去了。 */
.ad-panel { margin-top: var(--sp-3); }
.ad-panel__head {
  padding: 4px var(--sp-3);
  min-height: 36px;
  background: var(--ad-bg-alt);
  border-bottom: 1px solid var(--ad-line-3);
  gap: var(--sp-2);
}
/* 计数文案在工具行里不该抢镜 —— 收到 12px 并弱化（它只是"共 N 条"）。 */
.ad-panel__head .ad-list-head__tt { font-size: var(--fs-12); }

/* ---- B3b 单据族（`.ad-doc`）：KPI ＋ 筛选 ＋ 工具 三块一起瘦身 ---------------
   单据族 10 页共用这一套骨架（实测首行**十页一模一样** 475px）⇒ 改这里十页同时生效。
   ① KPI 三格：原 75px 的**三块等分色块**（各自带背景与内边距）→ **一行内联文字条**
      （`台账行数 12 行 · 库存总量 320 件 · 库存总额 ¥ 12,345`），约 28px。
   ⚠️ 这三格里的数字是**真数据**（不是装饰）—— 所以是"压扁保留"，不是砍掉。 */
.ad-doc .ad-money3 {
  display: flex; align-items: baseline; gap: var(--sp-4);
  margin: 0 0 var(--sp-2); padding: 0 2px;
  background: transparent; box-shadow: none; border-radius: 0; overflow: visible;
}
.ad-doc .ad-money3 > div { flex: 0 0 auto; display: flex; align-items: baseline;
  gap: var(--sp-2); background: transparent; padding: 0; }
.ad-doc .ad-money3 .k { font-size: var(--fs-11); letter-spacing: .02em; }
.ad-doc .ad-money3 .v { font-size: var(--fs-13); font-weight: var(--fw-medium); margin-top: 0; }
/* ② 筛选条 / 工具条：内边距收到与面板头一致 ⇒ 三条横杠并起来共约 72px（原约 194）。
   ★ 筛选条的下沿线去掉 —— 它下面紧跟工具条，两条线挨着是"碎线"。 */
.ad-doc .ad-filter, .ad-doc .ad-toolbar {
  padding: 4px var(--sp-3); gap: var(--sp-2);
  background: var(--ad-bg-alt); border-bottom: 1px solid var(--ad-line);
}
.ad-doc .ad-filter { border-bottom-color: transparent; }
/* ③ 单据页的面板：本体就是"表头块 ＋ 表格"，上边距收紧。 */
.ad-doc .ad-panel { margin-top: 0; }

/* ---- B3c 工具行控件统一收小（余总原话「**减小按钮大小**」） -------------------
   `.btn--sm` 全站是 38px 高 / 左右 16px 内边距（那是**前台共用件**，不能动）。
   后台工具行里它太占地方 ⇒ **只在这些容器内**收到 28px。
   ⚠️ 作用域写死在三个容器里（面板头 / 单据合并头 / 批量条）—— 绝不改 `.btn` 本体，
     否则会把前台按钮一起缩小（本项目铁律：共用件只覆盖、不改源头）。 */
.ad-panel__head .btn, .ad-dochead .btn, .ad-batchbar .btn {
  min-height: 28px; padding: 0 10px; font-size: var(--fs-12);
}
.ad-panel__head .ad-q, .ad-panel__head .input, .ad-panel__head .select,
.ad-dochead .ad-q, .ad-dochead .input, .ad-dochead .select,
.ad-dochead .ad-filter__fld input, .ad-dochead .ad-filter__fld select {
  height: 28px; font-size: var(--fs-12);
}

/* ---- B3d 表头行收紧 --------------------------------------------------------
   表头 45px → 33px（`padding` 12 → 6）。它是**每一页表格都有的固定成本**，
   31 页同省。★ 只改纵向内边距，左右不动（列距保持原样，不然列宽观感会变）。 */
.ad-table th { padding: 6px var(--sp-4); }

/* ---- B3e 单据合并头（`.ad-dochead`）---------------------------------------
   = 「筛选条 ＋ 工具条」并成的一行。它取代了原来两条各自带内边距的横杠。
   ★ 底色与下沿取表头的同一套（`--ad-bg-alt` / `--ad-line-1`）⇒ 与 `thead` 连成一块，
     读起来就是"表头带工具"（对齐余总给的参考图）。
   ★ 筛选项由**竖排（label 在上）改成横排** —— 竖排会让这一行变成 47px 高，
     横排只要 28px（label 与输入框同一基线）。
   ⚠️ `flex-wrap: wrap` 必留：窄屏（<1100）装不下时**宁可换行**，不许横向溢出。
      溢出会让右边按钮跑到屏幕外，而且**不报错**。 */
.ad-dochead {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding: 4px var(--sp-3); min-height: 36px;
  background: var(--ad-bg-alt);
  border-bottom: 1px solid var(--ad-line);
}
.ad-dochead .ad-filter__fld { flex-direction: row; align-items: center; gap: var(--sp-2); }
.ad-dochead .ad-filter__fld > label { white-space: nowrap; }
.ad-dochead .ad-toolbar__sp { flex: 1; }
.ad-dochead .ad-filter__tog { gap: 4px; }
.ad-dochead .ad-quiet { font-size: var(--fs-12); }
/* ---- BD1 工具行进表头（2026-10-07 · 余总「照样机排版」· 试点只开报价单）----------------------
   `.ad-dochead` 从「表格外的独立横杠」改成 `<thead>` 的第一行 `tr.ad-tools`：
   ① 与列边框对齐，读起来就是表头的一部分；
   ② 挪掉了面板的内距 —— 表头直接顶到面板上沿。
   ⚠️ 坑：`th` 自带内边距 / 背景 / 右边框 ⇒ 必须逐项清掉，否则工具行被撑高、右侧多一条竖线。
   ⚠️ 坑：加了一行 `tr` ⇒ 所有读表头的 JS 都走 `adHeadTr()` / `adHeadThs()`（见 admin.js），否则整体错位一格且不报错。 */
/* ★★ 批 KN（2026-10-07）：选择器由 `.ad-table--dl` **放宽到 `.ad-table`** ——
   把这套工具行给「非单据表」（字典 / 供应商 / 客户 / …，它们的表没有 `--dl` 类）。
   `.ad-table--dl` 的元素本身就带 `.ad-table` 类 ⇒ 放宽＝原范围 ＋ 新范围，已铺 11 页一字不动。
   ⚠️ 特异性由 (0,3,1) 降到 (0,2,2) —— 已 grep 确认没有别的规则抢这两条属性。 */
.ad-table > thead > tr.ad-tools > th {
  padding: 0; border-right: 0; background: var(--ad-bg-alt);
  border-bottom: 1px solid var(--ad-line);
}
.ad-table > thead > tr.ad-tools .ad-dochead {
  border-bottom: 0; border-radius: 0;
}

/* ══════════════════════════════════════════════════════════════════════
   ★★ 批 BJ-1（2026-10-07 · 余总「全套」＋「控件 12px 照样机」）
   控件精致化：字号 / 字重 / 放大镜 / 页头按钮高 / 「更多筛选」蓝字。
   ★ 字号一律**照样机的精确值**（样机本身就 12/13 混用，不是一刀切 12）：
       13px = 搜索框 input · 页头/面板头按钮（样机 `.tbtn` / `.fsearch input`）
       12px = 筛选下拉 · 「更多筛选」 · 重置 · 计数 · 金额串 · 批量条（样机 `.sel/.more/.cnt`）
   ⚠️ **绝不改 `--fs-12`** —— 那块 `号`与`值`已错位（`--fs-12` 实即 14px）
      且全站 800+ 处引用；这里只落在**这四个容器作用域**内。
   ⚠️ 必须放在这些段的**之后**（本文件末尾附近）—— 靠源序取特异性，别依赖 `!important`。
   ══════════════════════════════════════════════════════════════════════ */

/* ②③ 字号 12px 组 ＋ 字重 400（样机全部是 400；原为 500） */
.ad-dochead .ad-filter__fld input,
.ad-dochead .ad-filter__fld select,
.ad-dochead .ad-filter__fld > label,
.ad-dochead .btn,
.ad-dochead .ad-quiet,
.ad-batchbar .btn,
.ad-pagehead .ad-toolmoney {
  font-size: var(--ad-ctl-fs); font-weight: 400;
}

/* ②③ 字号 13px 组（工具按钮 —— 样机 `.tbtn` 13px / `.fsearch input` 13px） */
.ad-pagehead .btn,
.ad-panel__head .btn {
  font-size: var(--ad-ctl-fs-btn); font-weight: 400;
}

/* ⑤ 搜索框放大镜（样机 `.fsearch svg` = 14×14 / stroke 1.9 / 色 `--ink-3 #B5B5B5`）
   ★ 走 `background-image`（**不覆盖** background-color），故不影响底色/聚焦态。
   ★ `padding-left: 28px` = 图标 14 ＋ 左 8 ＋ 间隔 6。 */
.ad-dochead .ad-filter__fld input[type="search"] {
  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23B5B5B5' stroke-width='1.9' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='6.5'/%3E%3Cpath d='M16 16l4 4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: 8px center;
  background-size: 14px 14px;
  padding-left: 28px;
}

/* ⑥ 页头按钮高 32 → 28（样机 `.tbtn` h=28；现状被 `.btn--sm` 的 `--ad-btn-h-sm:32` 撑到 32） */
.ad-pagehead .btn { min-height: 28px; height: 28px; }

/* ⑦ 「更多筛选」文字用低饱和蓝（样机 `.more` = `#1677FF`；本库对应 `--ad-link`）
   ★ 只染 `.ad-filter__tog`（＝「更多筛选」那一枚），旁边的「重置」仍是黑字。 */
.ad-dochead .ad-filter__tog .lbl { color: var(--ad-link); }

/* ---- BD3 页头（`.ad-pagehead`）—— 照样机：标题 ＋ 统计 ＋ 按钮 一行，压在表格上方 -------
   ★ 为什么要有这一层：实测「把 11 件东西全塞进表头工具行」**装不下**
     （内容宽 1389px > 可见宽 1146px）⇒ 折两行；强制一行则右边被裁、按钮静默消失。
   ★ 样机的分法是两行各司其职：页头 = 标题 ＋ 统计 ＋ 按钮；表头工具行 = 搜索 ＋ 筛选 ＋ 共 N 条。
   ★ 与下面 `.ad-panel` 拼成一张卡：页头出上/左/右边，面板出下边 —— 接缝处不留双线、不外圆。 */
.ad-pagehead {
  display: flex; align-items: center; gap: var(--sp-2);
  min-height: 36px; padding: 2px var(--sp-3);
  background: var(--ad-bg);
  border: 1px solid var(--ad-line); border-bottom: 0;
  border-radius: var(--ad-r-card) var(--ad-r-card) 0 0;
}
.ad-pagehead + .ad-panel {
  margin-top: 0; border-top: 0;
  border-radius: 0 0 var(--ad-r-card) var(--ad-r-card);
}
.ad-pagehead__t { font-size: var(--fs-14); font-weight: var(--fw-medium); color: var(--ad-ink); }
.ad-pagehead__sp { flex: 1; }
/* 图标按钮（列设置 / 导出）—— 28×28，按样机排法 */
.ad-pagehead .btn--icon { width: 28px; min-width: 28px; padding: 0; justify-content: center; }
/* 统计文字（原独立 `.ad-money3` 那一层，BD-2 并进工具行、BD-3 移到页头） */
.ad-pagehead .ad-toolmoney { white-space: nowrap; }
.ad-pagehead .ad-toolmoney b { font-weight: var(--fw-medium); color: var(--ad-ink); }
/* 行高控件的新容器（`rowhMount()` 挂这里） */
.ad-pagehead .ad-tools__extra { display: inline-flex; align-items: center; }
/* ---- BD3 收掉整条 `.ad-bar`（报价单页试点） ------------------ */
.ad-bar.is-gone { display: none; }


/* ---- B3f 菜单管理：分组概览签收进面板头后要**一行内联** ---------------------- */
.ad-panel__head .ad-menu-chips { display: inline-flex; align-items: center; gap: var(--sp-2);
  margin: 0; flex-wrap: nowrap; overflow: hidden; }
.ad-panel__head .ad-menu-chip { padding: 2px 8px; font-size: var(--fs-11); gap: var(--sp-1); }
.ad-panel__head .ad-menu-chip em { font-size: var(--fs-11); }
/* ---- B3g 产品页工具行（`--wrap`）必须**装进一行** --------------------------
   实测：改前它折成 3 行、高 **109px**（搜索框 flex-basis 200 太宽 ＋ chip 27 高）。
   ★ 选择器要加 `--wrap` 那一层：`admin.css:5168` 已有一条**同属性、更高权重**的
     `.ad-panel__head.ad-panel__head--wrap > .ad-q`，只写 `.ad-panel__head .ad-q`
     **权重不够、不生效**（本项目一类静默错：写了、没报错、但没赢）。
   ⚠️ `flex-wrap: wrap` 保留（窄屏仍要能折），这里只是把宽度收进一行能装下的量。 */
.ad-panel__head--wrap > .ad-q { flex: 0 1 150px; min-width: 120px; }
.ad-panel__head .ad-chip { height: 24px; padding: 0 8px; font-size: var(--fs-11); }
/* ---- B1b 折叠说明条再收 4px（34 → 30）------------------------------------ */
.ad-note--fold { padding: 3px var(--sp-3); }

/* ---- B3h 工具行里的下拉 / 输入框：**不许撑满整行** --------------------------
   实测血案（产品页）：排序那个 `<select class="input">` 被 `components.css` 的
   `.input { width: 100% }` 撑成 **1146px** ⇒ 工具行被挤成 **3 行、109px 高**。
   ★ `width:100%` 在「一行里并排放好几个控件」的语境下是错的来源 ——
     百分数永远是相对**父容器**算，不管这一行还有别的兄弟。
   ⇒ 工具行内一律 `width:auto` ＋ `flex:0 0 auto`（按内容宽，参与一行排布）。
   ⚠️ 只作用于 `.ad-panel__head` 与 `.ad-dochead`（表单页里 `.input` 仍要 100% 宽）。 */
.ad-panel__head .input, .ad-panel__head .select,
.ad-dochead .input, .ad-dochead .select { width: auto; flex: 0 0 auto; }
/* ★ 上一条 `B3g` 把选择器写弱了（(0,1,1) 输给 admin.css:5168 的 (0,2,1)）——
   这里用**同权重**补一条（后出现者胜），别删上面那条（留着说明来由）。 */
.ad-panel__head.ad-panel__head--wrap > .ad-q { flex: 0 1 150px; min-width: 120px; }

/* ---- B3i 页内页签（`.ad-tabs`）的下边距 24 → 12 -----------------------------
   实测（产品页）：页签 36 ＋ 下边距 24 ＋ 面板上边距 12 = 72px 才轮到表格。
   页签本身不能砍（它是"同一主体的多类配置"的正当容器，余总 10-06 也认这一条），
   但**它下面的空档没有理由比别处大一倍**。 */
.ad-tabs { margin-bottom: var(--sp-3); }

/* ---- B3j 概览的 KPI 卡：6 张要在一行里（原为 5＋1，右边空一大片）------------
   实测：栅格 `minmax(200px,1fr)` ＋ 24px 间距 ⇒ 1220px 正文只放得下 5 列
   ⇒ 第 6 张（报价留言）单独折到第二行，右边留白 ≈ 4 张卡的宽度。
   ⇒ 列宽下限 200 → 178、间距 24 → 16 ⇒ 6 列正好一行。
   ⚠️ 只改这两项；`auto-fit` 保留 —— 窄屏仍会自动减列（不改死列数）。 */
.ad-cards { grid-template-columns: repeat(auto-fit, minmax(178px, 1fr)); gap: var(--sp-4); }
/* ---- B3k 搜索框宽度回到 200（上面为排查折行先收到 150，现证明一行装得下）----- */
.ad-panel__head.ad-panel__head--wrap > .ad-q { flex: 0 1 200px; min-width: 150px; }

/* ---- B4 表内编辑（`.ad-xcell`）---------------------------------------------
   余总口径：「**表格有些可以用表内编辑策略**」—— 枚举 / 数字 / 短文本 / 布尔四类
   双击就地改，不跳页、不开弹窗。登记表在 admin.js 的 `ADEDIT`（一处登记、全页生效）。
   ★ 观感只做两件事：**悬停给一圈描边**（告诉人"这里能改"）＋ 编辑态给主色描边。
     不加背景色块 —— 铁律"视觉克制"，一格一个底色会花。 */
.ad-table td.ad-xcell { cursor: cell; }
.ad-table td.ad-xcell:hover { box-shadow: inset 0 0 0 1px var(--ad-acc-line); }
.ad-table td.ad-xcell.is-editing {
  box-shadow: inset 0 0 0 2px var(--ad-accent);
  background: var(--ad-bg);
  padding: 2px var(--sp-2);
  text-align: left;
}
.ad-xinp, .ad-xsel {
  width: 100%; min-width: 56px; height: 30px; padding: 0 6px;
  font: inherit; font-size: var(--fs-13); line-height: 1;
  border: 1px solid var(--ad-ctl-line); border-radius: var(--r-1);
  background: var(--ad-bg); color: var(--ad-ink);
}
.ad-xinp:focus, .ad-xsel:focus { outline: none; border-color: var(--ad-accent); }
/* 列头那支铅笔：默认很淡，悬停表头才亮 —— 它只是"这列能改"的提示，不是主角。 */
.ad-thx { display: inline-flex; width: 16px; height: 16px; vertical-align: -3px;
  margin-left: 3px; color: var(--ad-muted-ink); opacity: .35; }
.ad-table th:hover .ad-thx { opacity: .8; }
.ad-thx svg { width: 13px; height: 13px; }

/* ===================================================================
   ★★★ 批 CLEAN-1 · B3-报表（2026-10-06）—— 报表族工具条压薄

   实测：五张报表页最上面那条 `.od-bar`（报表名 ＋ 列设置 / 打印 / 导出）
   高 **66px**（= 上下内边距 16×2 ＋ 32 高按钮 ＋ 2 边框）；
   它下面还压着一行 39px 的筛选行 ⇒ KPI 卡被推到 **269px**
   （视口 900 的 29.9%）—— 刚好卡在 70% 判据线上、**零余量**。
   这里**只压内边距与按钮高度、不动 DOM**（五张报表共用 `rpBar()` 一处 ⇒ 改一处全生效）。
   ⇒ 实测 KPI 269 → 247px，数据区占比 70.1% → 72.6%。
   ------------------------------------------------------------------- */
.ad-note.od-bar { padding: 7px var(--sp-3); align-items: center; }
.ad-note.od-bar > div:first-child { line-height: 18px; }
.ad-note.od-bar .btn--sm { min-height: 28px; padding: 0 9px; font-size: var(--fs-12); }

/* ===================================================================
   ★★★ 批 CLEAN-2 · C-1（2026-10-06）—— **通用右侧抽屉**（「零跳转」容器之一）

   余总口径：查看明细 / 改状态 / 备注 / 启用禁用 / 批量 —— **禁止开新页**；
   四容器＝① 页内 TAB ② **抽屉侧滑**（查看 / 简改，优先于详情页）③ 折叠面板 ④ 弹窗。
   -------------------------------------------------------------------
   ★ 只吃**已定义**的设计令牌（`--ad-line` / `--ad-bg-alt` / `--ad-ctl-line` …）——
     ⚠️ 本批踩过一次：写了 `var(--ad-line-1)` 而真名是 `--ad-line`，
        未定义的自定义属性会让**整条声明作废**（`border-bottom` 悄悄回落到 `currentColor`），
        **不报错、不告警**，肉眼几乎看不出 —— 这就是本项目元规则里的「CSS 静默失效」。
   ★ 零阴影（SAGE 七铁律之一）：抽屉与页面之间靠 1px 边线 ＋ 半透明遮罩分层，不用 box-shadow。
   ★ 三段式：`__hd`（标题/关闭）· `__bd`（可滚）· `__ft`（动作）。
     `__bd` 必须 `min-height:0` 才能在 flex 列里正常滚动（不加就是"内容把页脚顶出屏幕"）。
   =================================================================== */
/* ★★★ `position: fixed; inset: 0; z-index: var(--z-modal)` —— **层级是本批的坑之一**：
   初版写成 `z-index: 60`，而**顶栏 `.ad-bar` 是 100** ⇒ 顶栏压在抽屉头上，
   把**标题和那个 × 一起盖住**（标题看不见、关闭键点不到）。
   ⚠️ 这个错**判据一条都抓不到** —— 抽屉"开了"、字段数对、底部按钮也在；
      只有**真去看图**才发现最上面 63px 是空的。⇒ 它是模态（`aria-modal="true"`），
      就该和 `#adModal` / `#adPicker` 同一层（`--z-modal` = 300，见 `tokens.css`）。
   ★ 旁证：`.ad-cols` / `.ad-taxpop` / `.ad-ftip` 那些 **60 是对的** —— 它们是"贴着某个控件"的
     小浮层、出现在顶栏之下，不需要盖住顶栏；抽屉是整屏遮罩，性质不同。 */
.ad-drawer { position: fixed; inset: 0; z-index: var(--z-modal); pointer-events: none; }
.ad-drawer__mask {
  position: absolute; inset: 0; background: rgba(26, 27, 28, .26);
  opacity: 0; pointer-events: none; transition: opacity .16s ease;
}
.ad-drawer.is-open { pointer-events: auto; }
.ad-drawer.is-open .ad-drawer__mask { opacity: 1; pointer-events: auto; }
.ad-drawer__panel {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: 460px; max-width: 92vw;
  display: flex; flex-direction: column;
  background: var(--ad-bg);
  border-left: 1px solid var(--ad-line-2);
  transform: translateX(100%);
  transition: transform .18s ease;
}
.ad-drawer.is-open .ad-drawer__panel { transform: translateX(0); }
.ad-drawer__hd {
  flex: none; display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--ad-bg-alt); border-bottom: 1px solid var(--ad-line);
}
.ad-drawer__hdt { flex: 1; min-width: 0; }
.ad-drawer__tt { font-size: var(--fs-16); line-height: 22px; }
.ad-drawer__sb { font-size: var(--fs-11); line-height: 16px; color: var(--ad-muted-ink); }
.ad-drawer__x {
  flex: none; width: 28px; height: 28px; padding: 0;
  border: 1px solid var(--ad-ctl-line); border-radius: var(--r-1);
  background: var(--ad-bg); color: var(--ad-ink);
  font-size: 17px; line-height: 1; cursor: pointer;
}
.ad-drawer__x:hover { background: var(--ad-bg-alt); }
.ad-drawer__bd { flex: 1; min-height: 0; overflow: auto; padding: var(--sp-4); }
.ad-drawer__ft {
  flex: none; display: flex; justify-content: flex-end; gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  background: var(--ad-bg-alt); border-top: 1px solid var(--ad-line);
}
/* 字段表：左标签窄列 / 右值宽列。`display: contents` 让每对 `.k`/`.v` 直接成为网格项 ⇒
   DOM 里仍是"一条字段一个壳"（好读、好加），但**没有多余的一层盒**（不然网格对不齐）。 */
.ad-drawer__grid { display: grid; grid-template-columns: minmax(104px, 32%) 1fr; column-gap: var(--sp-3); }
.ad-drawer__f { display: contents; }
.ad-drawer__f > .k {
  padding: 7px 0; color: var(--ad-muted-ink); font-size: var(--fs-12);
  border-bottom: 1px solid var(--ad-line);
}
.ad-drawer__f > .v {
  padding: 7px 0; font-size: var(--fs-13); word-break: break-word;
  border-bottom: 1px solid var(--ad-line);
}
@media (prefers-reduced-motion: reduce) {
  .ad-drawer__mask, .ad-drawer__panel { transition: none; }
}

/* ══════════════════════════════════════════════════════════════════════
   ★★ 批 BJ-2（2026-10-07 · 余总「第二批：label 搬进框内 ＋ 收窄 ＋ 图标重画」）
   ----------------------------------------------------------------
   ① 筛选控件「label 进框」：外壳 `.ad-dsel` 自带前缀与细箭头（形状对齐样机 `.sel`：
      28 高 · 圆角 6 · 11px 箭头），原生控件被压在里面。
   ② `<label>` 元素**保留但视觉隐藏**（点它仍能聚焦控件、读屏仍读得到 ⇒ 无障碍不回退）。
   ③ `svg` 视框 16 → 24（线宽仍 2、渲染仍 16）⇒ 线条视觉细 1/3（`.ad-dsel` 内那枚也在内）。
   ⚠️ 全部限定 `.ad-dochead` 作用域 ⇒ 另外 7 个域（筛选条是独立竖排条）外观一个字不动。
   ⚠️ 内层控件的重置规则**必须比 `.ad-dochead .ad-filter__fld select`（0,2,1）更高**，
      否则边框/内距/最小宽会被它盖回去 ⇒ 外壳里多出一个带框的小控件（**静默的错**）。
   ══════════════════════════════════════════════════════════════════════ */

/* ① 外壳 */
/* ★ 控件字号令牌（批 BJ-2c 起）—— 这两档原先直接写字面量，撞 `check_q5_ui.py` X11
   （「有令牌的档位不许再写字面量」）。
   ⚠️ **不能借用 `--fs-12`**：本库那块令牌**名字与实值已错位**（`--fs-12` 实即 14px，
      全站 800+ 处在引用）—— 借它会把「控件 12px」静默改成 14px。
   ★ 定义**自定义属性**不触发 X11（它只扫 padding/margin/gap/border-radius/font-size）。 */
:root { --ad-ctl-fs: 12px; --ad-ctl-fs-btn: 13px; }

.ad-dochead .ad-dsel {
  position: relative; display: inline-flex; align-items: center; gap: 5px;
  height: 28px; padding: 0 9px;
  border: 1px solid var(--ad-field-line); border-radius: 6px;
  background: var(--ad-bg); color: var(--ad-ink-2);
  font-size: var(--ad-ctl-fs); white-space: nowrap;
}
.ad-dochead .ad-dsel__k { font-weight: 400; color: var(--ad-ink); }
.ad-dochead .ad-dsel__v { color: var(--ad-ink-2); }
.ad-dochead .ad-dsel__v.is-ph { color: var(--ad-placeholder); }
/* 箭头：CSS 伪元素画的 V（**故意不用 `<svg>`** —— 见 admin.js 里 BJ-2b 的说明）。
   6×6px 方块只留右/下两条 1.5px 边，转 45° 就是一枚细 V；视觉与样机那枚 11px 箭头同档。
   ★ 箭头必须排在 `.ad-dsel__v` **之后** —— 原生 select 是 `position:absolute`，不占位。 */
.ad-dochead .ad-dsel:not(.ad-dsel--date)::after {
  content: ''; flex: 0 0 auto; width: 6px; height: 6px; margin-left: 1px;
  border-right: 1.5px solid var(--ad-muted-ink);
  border-bottom: 1.5px solid var(--ad-muted-ink);
  transform: translateY(-2px) rotate(45deg);
}
/* 焦点环挂在**外壳**上 —— 里面那层是透明的，让它自己出 outline 会画在看不见的地方 */
.ad-dochead .ad-dsel:focus-within { outline: 2px solid var(--ad-accent); outline-offset: 1px; }

/* ② 原生 select：铺满整壳 · 全透明（弹的仍是系统列表、可键盘可访问）
      ★ 特异性 (0,3,1) > `.ad-dochead .ad-filter__fld select` 的 (0,2,1)。 */
.ad-dochead .ad-filter__fld .ad-dsel > select {
  position: absolute; left: 0; top: 0; width: 100%; height: 100%;
  min-width: 0; padding: 0; margin: 0; opacity: 0;
  border: 0; border-radius: 0; background: transparent; cursor: pointer;
}
.ad-dochead .ad-filter__fld .ad-dsel > select:focus { outline: 0; }
.ad-dochead .ad-filter__fld .ad-dsel > select::-ms-expand { display: none; }

/* ③ 日期：原生控件**保持可见**（用户要看得到日期、点得到日历图标）——
      只把它的边框/底色/内距去掉，盒子交给外壳。 */
.ad-dochead .ad-filter__fld .ad-dsel--date > input[type="date"] {
  position: static; width: auto; height: 24px; min-width: 0;
  padding: 0; margin: 0; opacity: 1;
  border: 0; border-radius: 0; background: transparent;
  color: var(--ad-ink-2);
}
.ad-dochead .ad-filter__fld .ad-dsel--date > input[type="date"]:focus { outline: 0; }

/* ④ label 进框 = 「框内已有同样文字」⇒ `<label>` 视觉隐藏（保留语义与「点 label 聚焦」） */
.ad-dochead .ad-filter__fld { position: relative; }
.ad-dochead .ad-filter__fld > label {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0; padding: 0; margin: 0;
}

/* ⑤ 搜索框照样机（`.fsearch` 宽 230）：去 label 后只剩放大镜 ＋ placeholder（放大镜由 BJ-1 落） */
.ad-dochead .ad-filter__fld input[type="search"] { width: 230px; }


/* ══════════════════════════════════════════════════════════════════════════
   ★★★ 批 KN（2026-10-07 · 余总「其他有表格的地方铺上这套控件」）
   把「页头 ＋ 表头工具行」从 11 个单据/库存域（`DOC_REG[…].toolsInHead`）
   铺到**不在 DOC_REG 里的手写列表页**（字典 / 供应商 / 客户 / …）。
   --------------------------------------------------------------------------
   ★ 收益账（真注入实测，非推算）：**收掉 `.ad-bar`(−40) ＋ 说明条(−32)**
     就是**全部收益来源**；「工具行进表头」自身是 **+36px 的成本**。
     ⇒「说明条必须收走」不是可选项 —— 不收，整批只剩 ~3px（等于白铺）。
   ★ 说明条的去处：页头一枚 ⓘ，点开是**浮层**（`position:absolute` ⇒ 不占文档流）。
   ══════════════════════════════════════════════════════════════════════════ */

/* ① 页头要当说明浮层的定位父级（`.ad-pagehead` 本体在 BD3 段，这里只补定位） */
.ad-pagehead { position: relative; }

/* ② 页头里的 ⓘ（16 视框 · 线宽 2 —— 与全站图标同一套口径，见 DESIGN §4.42）
   ★ 它是**页头里唯一的说明入口**：说明全文收进浮层后，页面上就只剩这一枚图标。 */
.ad-pagehead__i {
  flex: none; width: 22px; height: 22px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid transparent; background: transparent; border-radius: var(--r-1);
  color: var(--ad-muted-ink); cursor: pointer;
}
.ad-pagehead__i:hover { color: var(--ad-accent); border-color: var(--ad-line-3); }
.ad-pagehead__i[aria-expanded="true"] { color: var(--ad-accent); border-color: var(--ad-line-3); }

/* ③ 说明浮层（点 ⓘ 开 · 点外 / Esc 关）—— 与页头拼成一张卡（上边不留双线） */
.ad-notepop {
  position: absolute; left: 0; right: 0; top: 100%; z-index: 45;
  background: var(--ad-bg); border: 1px solid var(--ad-line); border-top: 0;
  border-radius: 0 0 var(--ad-r-card) var(--ad-r-card);
  box-shadow: var(--shadow-pop); padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-12); line-height: 1.75; color: var(--ad-ink-2);
}
.ad-notepop[hidden] { display: none; }
.ad-notepop b { color: var(--ad-ink); }
/* 浮层里原文常是多个 `<div>`（作者靠 `.ad-note` 的 flex gap 排开）——
   这里 flex 语境没了，把段间距补回来，否则几段贴成一块。 */
.ad-notepop > * + * { margin-top: var(--sp-2); }

/* ④ 阶段统计卡 → 页头里的一行小 chip（客户页 · 批 KN）
   ★ 结构一个字不改（还是 `button.ad-kpi[data-cust-stage]`）⇒ 点击筛选逻辑原样；
     这里只覆盖**外形**：`.ad-kpibar` 由 grid 改 flex、`.ad-kpi` 由 118px 方块改 22px 小 chip。
   ★ 口径：这一带**不许出现 `999px` / `--r-pill`**（[V8b]/[V8c] 守表单控件不给胶囊）
     ⇒ 用 `--r-1`(4px) 方角。原 75px 的六宫格就是这么压掉的。 */
.ad-pagehead .ad-kpibar {
  display: flex; grid-template-columns: none; gap: var(--sp-1);
  margin: 0; flex: 0 1 auto; min-width: 0; overflow: hidden;
}
.ad-pagehead .ad-kpi {
  min-height: 22px; padding: 0 8px; border-radius: var(--r-1);
  /* ⚠️ `flex-direction: row` **必须显式写**：`.ad-kpi` 本体是 `column`（卡片竖排 k/v），
     漏了这一条 ⇒ chip 里「档位名」与「数量」还是**竖着叠**，整个页头被撑到 42px
     （实测血案：页头 47px、首行 212，别页都是 36/201）。本项目老雷：覆盖不完整＝静默失效。 */
  display: inline-flex; flex-direction: row; align-items: center; gap: 5px;
  font-size: var(--fs-11);
  white-space: nowrap; flex: 0 0 auto;
}
.ad-pagehead .ad-kpi__k {
  font-size: var(--fs-11); letter-spacing: 0; text-transform: none;
}
.ad-pagehead .ad-kpi__v { font-size: var(--fs-11); font-weight: var(--fw-medium); line-height: 1; }
.ad-pagehead .ad-kpi.is-on .ad-kpi__k { color: var(--ad-ink); }


/* ⑤ 页头里的「外来控件」瘦身（批 KN 续 · 铺其余 8 页时真跑逮出）
   --------------------------------------------------------------------------
   ★ 现象（真跑量出来的，不是推演）：`.ad-pagehead` 只有 `min-height:36px`，
     而里面若塞进**按 40px 档**做的控件，页头就被顶高 ——
       · 商品供应商 / BOM 的「选规格」是 `<select class="ad-q">`
         ⇒ 走 L4726 的 `height: var(--ad-field-h)`（=40px）⇒ 页头 **45px**
       · 菜单管理的分组签 `.ad-menu-chips` 本体带 `margin-top: var(--sp-3)`（12px，
         那是给它当年待在 `.ad-panel__head` **外面**时用的）⇒ 页头 **51px**
     ⇒ 首行 y 就比别页低 9～15px（201 → 210/216），「各页首行齐平」当场破功。
   ★ 修法：与 `.ad-dochead`（工具行）**同一口径** —— 页头里的控件一律 28px 高、
     字号 12；分组签按 `.ad-panel__head` 里那套（L5782）搬一份到页头。
   ★ 为什么用 `.ad-pagehead` 前缀而不是改本体：这些控件（`.ad-q` / `.ad-menu-chips`）
     在别处（搜索框、面板头）**还要保持原档**，不能为页头把本体改了。 */
.ad-pagehead .ad-q,
.ad-pagehead .input,
.ad-pagehead .select { height: 28px; font-size: var(--fs-12); }
.ad-pagehead .ad-menu-chips {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  margin-top: 0; flex: 0 1 auto; min-width: 0; overflow: hidden;
}
.ad-pagehead .ad-menu-chip { padding: 2px 8px; font-size: var(--fs-11); gap: var(--sp-1); }
.ad-pagehead .ad-menu-chip em { font-size: var(--fs-11); }
