/* ===== 科技感主题 =====
   所有颜色走 CSS 变量，由后台配置中心下发（铁律⑪：无硬编码）
   主题: tech-cyan / tech-purple / tech-green / minimal-light
*/
:root {
  --primary: #00E5FF;
  --accent: #7C4DFF;
  --bg: #070B18;
  --radius: 14px;
  /* 圆角令牌（统一 UI）：全部由 --radius 派生，后台改「圆角」即全局同步。
     目的：消灭各处硬编码的 9/10/11/12/99px 混用 —— 截图里「有圆有变」的根因。 */
  --radius-xs: max(4px, calc(var(--radius) - 8px));
  --radius-sm: max(6px, calc(var(--radius) - 5px));
  --radius-md: var(--radius);
  --radius-lg: calc(var(--radius) + 4px);
  --radius-pill: 999px;
  --font: "HarmonyOS Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
  --ease: cubic-bezier(.22,.61,.36,1);

  --bg-2: #0B1226;
  --panel: rgba(17, 25, 48, .72);
  --panel-2: rgba(22, 32, 60, .58);
  --border: rgba(120, 160, 255, .14);
  --border-strong: rgba(120, 200, 255, .30);
  --text: #E8F1FF;
  --text-dim: #93A6C9;
  --text-mute: #64789E;
  --ok: #22E7A5;
  --warn: #FFB445;
  --danger: #FF5C7A;
  --grid-line: rgba(120, 170, 255, .045);
  --shadow: 0 18px 50px rgba(0, 12, 40, .55);
  --glow: 0 0 0 1px rgba(0,229,255,.22), 0 0 26px rgba(0,229,255,.16);
  /* 断点口径（JS/回归共用同一组数字，改这里即全站生效） */
  --bp-side: 1180px;   /* 以下：左右分栏（内容 | 预览面板）退化为上下堆叠 */
  --bp-mobile: 860px;  /* 以下：进入手机端布局（抽屉导航 + 单列） */

  /* 控件高度令牌（ERRORS.md E-UI-015）：所有单行控件按 height 控高，禁止按 padding 控高。
     不设这两个值，select 就会比旁边的 input 高 2px —— "选择框不整齐"会无限回潮。
     后台「外观配置」可下发 ui.ctlHeight / ui.ctlHeightSm 覆盖（铁律⑪：全量可配置）。 */
  --ctl-h: 42px;       /* 表单区单行控件（选择框 / 输入框 / 同排按钮 / 滑杆行） */
  --ctl-h-sm: 34px;    /* 表格行内控件（分镜表 / 任务列表等密集场景） */

  /* ---- 按钮尺寸刻度（与 E-UI-015 同族：**一律按 height 控高，禁止按 padding 控高**）----
     ⚠ 历史缺陷（2026-09-28 用户截图反馈「这些按钮比例都不协调，很不自然，不美观」）：
       `.btn` 用 `padding:10px 18px` 控高、`.btn-sm` 用 `padding:6px 12px` 控高 ⇒ 同一个分镜表里
         · 卡片区按钮 42px
         · 表格内按钮 ~31px（且被 `.tbl td .btn` 拉成 34px）
         · 胶囊里的「×」被同一条表格规则强行撑到 34px，而胶囊本身只有 ~22px ⇒ **把胶囊戳破**
       再加上单字按钮「↑ ↓ 删」的宽度＝字宽＋左右内边距，三个字形宽度不同 ⇒ “宽窄高矮各不同”。
     ⇒ 现在：高度只由 `--btn-h / --btn-h-sm` 决定，横向只由两个内边距令牌决定，**没有第三种尺寸**：
         · 单字 / 纯图标按钮 → `.btn-icon`：**宽 = 高**（正方形）
         · 胶囊内的小按钮   → `.btn-mini`：18px，**比胶囊矮**（它的职责是“待在胶囊里”）
     后台「外观配置」下发 ui.ctlHeight / ui.ctlHeightSm 时，这 4 个令牌一起跟着走。 */
  --btn-h: var(--ctl-h);
  --btn-h-sm: var(--ctl-h-sm);
  --btn-pad-x: 16px;     /* 常规按钮左右内边距 */
  --btn-pad-x-sm: 11px;  /* 密集 / 表格按钮左右内边距 */

  /* 芯片高度令牌（2026-09-29 新增，与 .chip / .chip-add / .btn-xs 共用）。
     为什么要单独一个令牌：用户红框反馈的"比例不协调"根因就是**同一行里 26px 芯片 + 34px 按钮**。
     把芯片与"加号 / 行内小按钮"绑到同一个高度上，这类不齐**结构上就不可能再发生**。 */
  --chip-h: 26px;
}
[data-theme="minimal-light"] {
  --panel: rgba(255,255,255,.82);
  --panel-2: rgba(248,251,255,.75);
  --border: rgba(30,60,120,.12);
  --border-strong: rgba(30,90,160,.22);
  --text: #0E1B33; --text-dim: #4A5C7C; --text-mute: #8296B6;
  --bg-2: #F2F6FD; --grid-line: rgba(40,80,160,.05);
  --shadow: 0 12px 34px rgba(30,60,120,.10);
}

* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0; font-family: var(--font); color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
a { color: var(--primary); text-decoration: none; }
::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-thumb { background: rgba(120,170,255,.22); border-radius: 6px; }
::-webkit-scrollbar-track { background: transparent; }

/* ---- 背景层：网格 + 极光光晕 ---- */
.bg-layer { position: fixed; inset: 0; z-index: -2; pointer-events: none; background: radial-gradient(1200px 800px at 78% -10%, rgba(124,77,255,.20), transparent 60%), radial-gradient(1000px 700px at 8% 8%, rgba(0,229,255,.14), transparent 60%), linear-gradient(180deg, var(--bg), var(--bg-2) 60%, var(--bg)); }
.bg-layer.no-grid::after { opacity: 0; }
.bg-layer::after {
  content: ''; position: absolute; inset: 0; opacity: 1;
  background-image: linear-gradient(var(--grid-line) 1px, transparent 1px), linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
  background-size: 46px 46px;
  -webkit-mask-image: radial-gradient(ellipse at 50% 30%, #000 55%, transparent 100%);
          mask-image: radial-gradient(ellipse at 50% 30%, #000 55%, transparent 100%);
}
.bg-scan { position: fixed; inset: 0; z-index: -1; pointer-events: none; opacity: .35;
  background: repeating-linear-gradient(180deg, rgba(255,255,255,.020) 0 1px, transparent 1px 3px); }

/* ---- 玻璃卡片 ---- */
.glass {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  backdrop-filter: blur(16px) saturate(140%);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
  box-shadow: var(--shadow);
}
.glass-hover { transition: transform .3s var(--ease), border-color .3s var(--ease), box-shadow .3s var(--ease); }
.glass-hover:hover { transform: translateY(-3px); border-color: var(--border-strong); box-shadow: var(--glow); }

/* ---- 按钮 ---- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: var(--btn-h); padding: 0 var(--btn-pad-x); border-radius: var(--radius-sm); border: 1px solid var(--border-strong);
  background: var(--panel-2); color: var(--text); cursor: pointer; font-size: 14px;
  font-family: inherit; transition: all .25s var(--ease); white-space: nowrap;
}
.btn:hover { border-color: var(--primary); color: #fff; box-shadow: var(--glow); }
.btn:active { transform: scale(.97); }
.btn:disabled { opacity: .45; cursor: not-allowed; box-shadow: none; }
.btn-primary {
  border: none; color: #04121C; font-weight: 700;
  background: linear-gradient(135deg, var(--primary), var(--accent));
  box-shadow: 0 8px 26px rgba(0,229,255,.28);
}
.btn-primary:hover { filter: brightness(1.12); color: #04121C; box-shadow: 0 10px 34px rgba(0,229,255,.42); }
.btn-ghost { background: transparent; }
.btn-sm { min-height: var(--btn-h-sm); padding: 0 var(--btn-pad-x-sm); font-size: 12.5px; border-radius: var(--radius-xs); }
/* btn-block（占满卡片宽度的块级按钮）必须允许换行：.btn 的 nowrap 是给行内小按钮的，
   块级按钮遇上长文案（如「真实合成自检（会真的读一句…）」）不换行就会捅出卡片边界
   （2026-09-24 用户截图反馈①）。行内小按钮不受影响。 */
.btn-block { width: 100%; white-space: normal; text-align: center; }
.btn-danger { border-color: rgba(255,92,122,.5); color: var(--danger); }
.btn-danger:hover { border-color: var(--danger); box-shadow: 0 0 22px rgba(255,92,122,.25); }
/* ⚠ `.btn-warn` 与 `.btn-xs` 曾经**只有调用点、没有定义**（2026-09-29 扫描发现，E-UI-028）。
   后果：① 「接管（真人上场）」用的是 `.btn-warn` ⇒ 一个**操作风险最高**的按钮长得和普通按钮一样，
   危险动作少了颜色提示；② 文字层的「居中 / 删除」用 `.btn-xs` ⇒ 退化成默认 42px 高的按钮，
   塞在窄侧栏里**比例完全不协调**（这正是用户"按钮比例不协调"的另一处来源）。
   补齐定义，并保持与 `.btn-*` 家族同一套视觉语言。 */
.btn-warn { border-color: rgba(255,180,69,.5); color: var(--warn); }
.btn-warn:hover { border-color: var(--warn); box-shadow: 0 0 22px rgba(255,180,69,.25); }
/* 超小号按钮：**只在"面板内的小工具条"里用**（文字层行内操作、卡片角落）。
   高度走 `--chip-h`（26px，与芯片同高）—— 一行里既有芯片又有按钮时才对得齐。 */
.btn-xs { height: var(--chip-h); min-height: var(--chip-h); padding: 0 9px; font-size: 12px; border-radius: var(--radius-pill); }

/* 单字 / 纯图标按钮：**宽必须等于高**。否则「↑ ↓ 删」的宽度＝字宽＋左右内边距，
   三个字形宽度不同 ⇒ 同一行出现三个宽窄不一的方块（用户截图观感）。 */
.btn-icon { width: var(--btn-h); min-width: var(--btn-h); padding: 0; }
.btn-sm.btn-icon { width: var(--btn-h-sm); min-width: var(--btn-h-sm); }

/* 胶囊（.badge）内的小按钮：**必须比胶囊矮**，否则会把胶囊戳破 ——
   用户截图里那个“戳出胶囊的 ×”就是这么来的。
   ⚠ 选择器刻意写满 `.tbl td .btn.btn-mini`（特异性 0,3,1 > 表格覆盖规则的 0,2,1），
   否则在表格里它会被 `.tbl td .btn` 的 height 令牌又拉回 34px，等于没修。

   ⚠⚠ 2026-09-29（r44）修「字出框」：这里原本是 `width: 18px`（**固定像素宽**）。
      用户截图反馈：管理后台「声优库」表里的 `试听 / 保存 / 删除` 两个字从 18px 的圆里捅出来了；
      全平台实测（scripts/diag-text-overflow.js，320 个扫描点）——
        · 试听/保存/删除：盒 18×18，墨 24×16 ⇒ 左右各溢出 3px
        · 加入声优库　　：盒 18×18，墨 60×16 ⇒ 左右各溢出 **21px**
      根因**不是**"谁把尺寸写错了"，而是**这个类名允许"固定像素宽 × 可变字数"这种组合存在**：
      它本来只服务单字（`×` / `改` / `删`），一旦有人拿它装两个字就必然出框
      （E-UI-029 同族 ——「类名是隐式契约、没人校验」）。
      ⇒ 改成 `width:auto; min-width:18px; padding:0 2px`：
         · 单字：内容 12px（全角字形 = 1em）+ 左右 4px + 边框 2px = **恰好 18px** ⇒ 仍是 18×18，视觉零变化
                 （**必须算上那 1px 边框**：`box-sizing:border-box` 下 width:auto 的用值 =
                  内容 + padding + border；漏算边框就会变成 20×18，单字形态被改掉 —— 本次就踩过，
                  由 scripts/diag-text-overflow.js 的对照②当场抓出来）
         · 多字：盒子跟着字长长大（`试听` → 30×18、`加入声优库` → 66×18）⇒ **结构上不可能再出框**
      这是"让不安全的写法物理上做不到"，而不是"指望下一轮记得别这么用"。 */
.btn.btn-mini,
.tbl td .btn.btn-mini, .tbl th .btn.btn-mini {
  width: auto; min-width: 18px; height: 18px; min-height: 0; padding: 0 2px; gap: 0;
  border-radius: var(--radius-pill); font-size: 12px; line-height: 1;
}
/* 安全网：将来任何人往胶囊里塞普通 .btn（忘了加 btn-mini），也自动被压回胶囊内 ——
   不靠“下一轮记得改”。这就是 E-UI-029「类名是隐式契约、没人校验」那一课的对策。 */
.badge .btn, .badge .btn-sm,
.tbl .badge .btn, .tbl .badge .btn-sm { height: 18px; min-height: 0; padding: 0 6px; font-size: 12px; line-height: 1; }

/* ---- 表单 ----
   ⚠ 控件高度一律用 height 令牌控高，**禁止再用 padding 控高**（ERRORS.md E-UI-015）：
   Chrome 对 select 的内建行盒比 input 高约 2px，只给 padding 的话同一套 .inp 会渲染出
   「select 45px / input 43px」两种高度 —— 这就是用户反复看到的"选择框不整齐"的机械根因。
   令牌由后台「界面外观」下发（ui.ctlHeight / ui.ctlHeightSm），这里是兜底默认值。

   结构上分三层，顺序不能动（否则特异性会打架 —— 第一版写了个 :not() 链，
   把 input 的选择器特异性抬到 (0,4,1)，结果表格覆盖规则 (0,2,1) 被盖住，
   select 变 34px 而 input 仍是 42px，等于没修）：
     ① 基础：单行控件高度的**唯一声明处**（容器无关）；
     ② 覆盖：表格/密集场景换小号令牌；
     ③ 例外：多行 / 滑杆 / 勾选类不受单行高度约束。
   禁止再新增"只给 padding 不给 height"的同族规则。 */
.inp, select.inp, textarea.inp {
  width: 100%; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: rgba(8,14,30,.6); color: var(--text);
  font-family: inherit; font-size: 14px; outline: none; transition: all .22s var(--ease);
  box-sizing: border-box;
}
/* ① 基础：select 与 input 由同一条规则决定高度 */
select.inp, input.inp, textarea.inp {
  height: var(--ctl-h); padding: 0 13px; line-height: calc(var(--ctl-h) - 2px);
}
/* ② 覆盖：表格行内控件统一到小号令牌（分镜表等密集场景） */
.tbl select.inp, .tbl input.inp, .tbl textarea.inp,
.tbl td .btn, .tbl th .btn {
  height: var(--btn-h-sm); padding: 0 var(--btn-pad-x-sm); font-size: 13px;
  line-height: calc(var(--btn-h-sm) - 2px); display: inline-flex; align-items: center; justify-content: center;
}
/* 表格里的单字/图标按钮同样要正方形（宽=高=小号令牌）；否则「↑ ↓ 删」三个宽度不一 */
.tbl td .btn-icon, .tbl th .btn-icon { width: var(--btn-h-sm); min-width: var(--btn-h-sm); padding: 0; }
/* ③ 例外：多行输入框与滑杆/勾选类控件不受单行高度约束 */
textarea.inp { resize: vertical; height: auto; min-height: 92px; padding: 11px 13px; line-height: 1.6; display: block; }
.tbl textarea.inp { height: auto; min-height: var(--ctl-h-sm); padding: 6px 10px; align-items: flex-start; }
input.inp[type=range], input.inp[type=checkbox], input.inp[type=radio], input.inp[type=color] { height: auto; padding: 0; }
.tbl td .inp { width: 100%; }
/* 数字输入框去掉浏览器原生上下箭头，让用户直接填数字 */
.inp-no-spinner { -moz-appearance: textfield; appearance: textfield; }
.inp-no-spinner::-webkit-outer-spin-button,
.inp-no-spinner::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

.inp:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(0,229,255,.12); }
.inp::placeholder { color: var(--text-mute); }
select.inp option { background: #0E1730; }
/* `.fs-badge`：字号滑杆右侧那个实时数字（曾只有调用点、没有定义，E-UI-028）。
   必须**等宽数字 + 定宽居中**：不然从 "12" 拖到 "100" 时徽章宽度一直在变，
   整行跟着左右抖动（用户看到的是"控件在动"）。 */
.fs-badge { min-width: 44px; justify-content: center; font-variant-numeric: tabular-nums; }

/* ---- 控件行：同一行内的「标签 / 选择框 / 输入框 / 滑杆 / 按钮」必须等高对齐 ----
   存在意义：过去把一长串控件塞进裸 .flex，flex-wrap 一换行就会把最后那个按钮
   单独甩到第二行并左对齐，看着像"掉了一个按钮"。统一走 .ctl-row 后，
   同一视觉行内的控件高度完全一致，换行也落成整齐的整行。 */
.ctl-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; }
/* ⚠ 这里的按钮高度写 `--ctl-h`（不是 `--btn-h`）是**刻意的**，别"统一"回去：
 *   两者数值等价（`--btn-h: var(--ctl-h)`），但 `.ctl-row` 的定义就是"一行表单控件"，
 *   它旁边站的 inline-lb / .ctl-box 用的都是 `--ctl-h` —— 同区块同一令牌，
 *   "按钮与输入框等高"这件事才是一件可读的事，而不是靠读者去追 `--btn-h` 的定义。
 *   回归 [5.7] 直接以 `--ctl-h` 断言，改回去即红（那是"改到看不出为什么"的典型）。 */
.ctl-row > .btn, .ctl-row > button.btn { height: var(--ctl-h); display: inline-flex; align-items: center; justify-content: center; }
.ctl-row .inline-lb { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--text-dim); height: var(--ctl-h); white-space: nowrap; }
.ctl-row .ctl-spacer { margin-left: auto; }
/* 按钮组作为**一个整体**换行：否则窄屏会把按钮逐个折行，最后一行只剩一个孤零零的按钮
   （用户截图里「保存到模板」单独掉到第二行就是这么来的）。 */
.ctl-row .ctl-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-left: auto; }
.ctl-row .ctl-actions .btn { height: var(--ctl-h); display: inline-flex; align-items: center; justify-content: center; }
/* 控件行里的图标按钮也要正方形（否则它比同排按钮“瘦”，且宽窄随字形变） */
.ctl-row > .btn-icon, .ctl-row .ctl-actions .btn-icon { width: var(--btn-h); min-width: var(--btn-h); padding: 0; }

/* 控件位（E-UI-017）：字段里如果不是「单行输入控件」本身，而是多选芯片 / 复选芯片行 / 滑杆行
   这类组合，必须把内容放进 .ctl-box —— 它给出与 --ctl-h 同高的基线，芯片在盒内垂直居中，
   于是同一行的 select / input / 芯片组天然齐平。
   禁止再用 `padding-top:8px` 之类的魔法数字去"凑"对齐：不同字号/缩放下必然漂移，
   而一旦漂移用户看到的就是"选择框不整齐"（已三次回潮，几何锁会拦住）。 */
.ctl-box { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 10px; min-height: var(--ctl-h); }
label.lb { display: block; font-size: 12.5px; color: var(--text-dim); margin: 0 0 6px; letter-spacing: .3px; }
.field { margin-bottom: 14px; }
.row { display: flex; gap: 12px; }
.row > * { flex: 1; min-width: 0; }
.chk { display: inline-flex; align-items: center; gap: 8px; font-size: 13.5px; color: var(--text-dim); cursor: pointer; }
.chk input { accent-color: var(--primary); width: 16px; height: 16px; }

/* ---- 文本工具类 ---- */
.t-dim { color: var(--text-dim); } .t-mute { color: var(--text-mute); }
.t-primary { color: var(--primary); } .t-ok { color: var(--ok); } .t-warn { color: var(--warn); } .t-danger { color: var(--danger); }
.mono { font-family: "JetBrains Mono", Consolas, monospace; }
.h1 { font-size: 26px; font-weight: 800; margin: 0; letter-spacing: .5px; }
.h2 { font-size: 19px; font-weight: 700; margin: 0; }
.small { font-size: 12.5px; }
.center { text-align: center; }
.mt0 { margin-top: 0 } .mt4 { margin-top: 4px } .mt6 { margin-top: 6px } .mt8 { margin-top: 8px } .mt12 { margin-top: 12px } .mt16 { margin-top: 16px } .mt24 { margin-top: 24px }
.mb4 { margin-bottom: 4px } .mb6 { margin-bottom: 6px } .mb8 { margin-bottom: 8px } .mb16 { margin-bottom: 16px } .mb24 { margin-bottom: 24px }
.flex { display: flex }
/* `.between` 补 `gap: 8px`（2026-09-29）：`justify-content: space-between` 只在**有富余空间**时
   才把两端推开；内容一长（标题 + 徽章）两端就会**贴成 0 间距**，看起来像文字黏在徽章上。
   `gap` 给的是"最小间距"——有富余时它不生效，挤满时它兜底。 */
.between { display: flex; justify-content: space-between; align-items: center; gap: 8px }
/* ⚠ 间距工具类必须**定义齐全**（2026-09-29，E-UI-028）。
   起因（用户反馈原文：「这些框比例不协调、都变形了、上下紧凑」）：
   `gap4` / `gap6` / `mt4` / `mt6` / `mt12` / `mb4` / `mb6` 这 7 个类**只有调用点、没有定义**，
   浏览器对不存在的类**静默按 0 处理** —— 页面不报错、控制台不报错、静态检查也过，
   但几十处该有间距的地方全是 0，用户看到的就是"糊在一起、上下紧凑"。
   这类缺陷肉眼查代码抓不到（0 间距与"没写间距"长得一模一样）⇒ 必须靠机械扫描：
   `scripts/scan-undefined-classes.js` + 回归 `[5.30]`。新增间距档位时**两边一起加**。 */
.wrap { flex-wrap: wrap }
.gap4 { gap: 4px } .gap6 { gap: 6px } .gap8 { gap: 8px } .gap10 { gap: 10px } .gap12 { gap: 12px } .gap16 { gap: 16px }
.hidden { display: none !important; }

/* ---- 徽章 / 进度 ---- */
/* ⚠ `.badge` 必须 `white-space: nowrap` + `flex: 0 0 auto`（2026-09-29，E-UI-029）。
   起因：徽章是**胶囊形**（`border-radius: 999px`），一旦文字折行，胶囊就变成一个又窄又高的**椭圆** ——
   用户的原话是"都变形了"。实测（`scripts/diag-sb-boxes.js`）：分镜表「可选截法」的
   "10 种"在 1440px 下是 49×23 的单行胶囊，到 900px 变成 43.8×38（比 1.15）、375px 变成 34×38（比 0.9）
   —— 同一枚徽章被压成竖椭圆。徽章**不是一个可以换行的文本容器**，这是它的形状契约。 */
.badge { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: var(--radius-pill); font-size: 11.5px; font-weight: 600; border: 1px solid var(--border-strong); color: var(--text-dim); white-space: nowrap; flex: 0 0 auto; max-width: 100%; }
.badge.ok { color: var(--ok); border-color: rgba(34,231,165,.4); background: rgba(34,231,165,.10); }
.badge.warn { color: var(--warn); border-color: rgba(255,180,69,.4); background: rgba(255,180,69,.10); }
.badge.danger { color: var(--danger); border-color: rgba(255,92,122,.4); background: rgba(255,92,122,.10); }
.badge.info { color: var(--primary); border-color: rgba(0,229,255,.4); background: rgba(0,229,255,.10); }
.progress { height: 7px; border-radius: var(--radius-pill); background: rgba(120,170,255,.12); overflow: hidden; }
.progress > i { display: block; height: 100%; border-radius: var(--radius-pill); background: linear-gradient(90deg, var(--primary), var(--accent)); transition: width .5s var(--ease); box-shadow: 0 0 12px rgba(0,229,255,.5); }

/* ---- 芯片行（chip-row / chip / chip-add）----------------------------------------
 * 用途：任何「一列可以挂多个的小标签 + 一个加号按钮」的形态 —— 分镜表的素材池、
 *       贴纸池、素材分组、话术池……全站统一走这三个类。
 *
 * 为什么要组件化（2026-09-29，用户红框反馈的直接产物）：
 *   以前每处各写一遍，结果是**同一行里两种控件两个高度**、间距还是 0：
 *   实测 1440px 下，芯片 26px 与「＋加素材」按钮 34px **列距 0px**（贴在一起）；
 *   到 900px 折行后**行距 0px**（上下贴住），芯片与按钮高度差 −8px。
 *   用户看到的就是"比例不协调、上下紧凑"。
 *   ⇒ 三个类把「高度 / 圆角 / 字号 / 间距 / 加号形态」一次定死，谁也别再各写一份。
 *   ⚠ 芯片与加号**必须同高**（都是 `--chip-h`）：它们在同一行里等价，高度不同就是没对齐。 */
.chip-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }
.chip {
  display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto; max-width: 100%;
  height: var(--chip-h); padding: 0 5px 0 9px; border-radius: var(--radius-pill);
  border: 1px solid var(--border-strong); background: rgba(120,170,255,.06);
  color: var(--text-dim); font-size: 12.5px; font-weight: 600; white-space: nowrap;
}
.chip .chip-name { overflow: hidden; text-overflow: ellipsis; max-width: 190px; }
.chip .chip-sub { color: var(--text-mute); font-weight: 500; }
/* 芯片里的「×」：18px 的圆按钮。用 <button> 是为了键盘可达（Tab 能到、Enter 能删）。 */
.chip .chip-x {
  width: 18px; height: 18px; min-height: 0; padding: 0; margin: 0; flex: 0 0 auto;
  display: grid; place-items: center; line-height: 1;
  border: 0; border-radius: var(--radius-pill); background: transparent;
  color: var(--text-mute); font-size: 13px; font-family: inherit; cursor: pointer;
}
.chip .chip-x:hover { background: rgba(255,92,122,.9); color: #fff; }
/* 加号：**虚线**把它和"已有素材的真实芯片"区分开（实线芯片 = 已选，虚线 = 动作）。
   ⚠ 与 `.chip` 同高，`flex: 0 0 auto` ⇒ 不会被压扁、也不会把芯片挤变形。 */
.chip-add {
  display: inline-flex; align-items: center; justify-content: center; gap: 4px;
  flex: 0 0 auto; height: var(--chip-h); padding: 0 11px; border-radius: var(--radius-pill);
  border: 1px dashed rgba(120,200,255,.45); background: rgba(0,229,255,.06);
  color: var(--primary); font-size: 12.5px; font-weight: 600; font-family: inherit;
  white-space: nowrap; cursor: pointer; transition: all .18s var(--ease);
}
.chip-add:hover { border-color: var(--primary); background: rgba(0,229,255,.14); }
.chip-add:disabled { opacity: .45; cursor: not-allowed; border-color: rgba(120,200,255,.25); }
.chip-empty { font-size: 12.5px; color: var(--text-mute); flex: 0 0 auto; }

/* ---- 表格 ---- */
.tbl { width: 100%; border-collapse: collapse; font-size: 13.5px; }
/* ⚠ 表头**不许折行**：`text-transform: uppercase` + 中文四字表头在窄列里会被拆成「可选截／法」。
   表头是**标签**不是正文，折行只会让列宽算法跟着抖（列宽由最宽内容决定 → 越窄越折 → 越折越窄）。
   窄屏放不下就整表横向滚动（`.tbl-wrap { overflow-x: auto }`），这是预期行为。 */
.tbl th { text-align: left; padding: 11px 12px; color: var(--text-mute); font-weight: 600; font-size: 12px; letter-spacing: .6px; text-transform: uppercase; border-bottom: 1px solid var(--border); white-space: nowrap; }
.tbl td { padding: 11px 12px; border-bottom: 1px solid rgba(120,170,255,.07); vertical-align: middle; }
.tbl tr:hover td { background: rgba(120,170,255,.045); }
.tbl-wrap { overflow-x: auto; }
/* 表格里的徽章不许被列宽压扁（配合上面的 nowrap：宁可让表格横向滚动，也不把胶囊压成椭圆）。 */
.tbl .badge { max-width: none; }

/* ---- 分镜表（`.tbl-sb`）列宽 -------------------------------------------------
 * 为什么必须**显式给列宽**而不是靠 `table-layout:auto` 自动算：
 *   自动列宽由"最宽内容"决定，而内容的宽度又受列宽影响 ⇒ 会形成**正反馈**：
 *   列越窄 → 表头「可选截法」折成两行 → 列宽按折行后的最宽行算 → 更窄。
 *   实测（`scripts/diag-sb-boxes.js`，改前）：1440px 下「可选截法」列 94.4px、徽章 49×23（正常）；
 *   到 900px 列只剩 67.8px → 徽章被压成 43.8×38（宽高比 1.15）；375px 更只剩 34.2×38（比 0.9，竖椭圆）。
 *   用户说的"都变形了"就是这个。⇒ 列宽与最小宽度写死，放不下就整表横向滚动。 */
.tbl-sb th:nth-child(1), .tbl-sb td:nth-child(1) { width: 44px; }
.tbl-sb th:nth-child(2), .tbl-sb td:nth-child(2) { min-width: 300px; }
.tbl-sb th:nth-child(3), .tbl-sb td:nth-child(3) { width: 110px; }
.tbl-sb th:nth-child(4), .tbl-sb td:nth-child(4) { width: 96px; }
.tbl-sb th:nth-child(5), .tbl-sb td:nth-child(5) { width: 132px; }
/* 表格行内控件的高度/内边距统一在「表单」段的三层结构里声明（② 覆盖层），
   此处**不再重复一份** —— 同一套规则写两遍必然漂移（谁后改一处就会出现
   「select 走了小号令牌、input 还是大号」这种只看一处代码永远查不出来的问题）。 */

/* ---- 布局：侧边栏 ---- */
.layout { display: flex; min-height: 100vh; }
.sidebar {
  width: 224px; flex: 0 0 224px; padding: 18px 14px;
  border-right: 1px solid var(--border); background: rgba(7,11,24,.62);
  backdrop-filter: blur(14px); position: sticky; top: 0; height: 100vh; overflow-y: auto;
}
.brand { display: flex; align-items: center; gap: 10px; padding: 6px 8px 18px; }
.brand-logo { width: 34px; height: 34px; border-radius: var(--radius-sm); background: linear-gradient(135deg, var(--primary), var(--accent)); display: grid; place-items: center; font-weight: 900; color: #04121C; box-shadow: 0 6px 20px rgba(0,229,255,.3); }
.brand-name { font-weight: 800; font-size: 15px; letter-spacing: .4px; }
.nav-item {
  display: flex; align-items: center; gap: 11px; padding: 10px 12px; border-radius: var(--radius-sm);
  color: var(--text-dim); cursor: pointer; font-size: 13.8px; margin-bottom: 3px;
  transition: all .22s var(--ease); border: 1px solid transparent; position: relative;
}
.nav-item:hover { color: var(--text); background: rgba(120,170,255,.07); }
.nav-item.active { color: #fff; background: linear-gradient(90deg, rgba(0,229,255,.16), rgba(124,77,255,.10)); border-color: var(--border-strong); }
.nav-item.active::before { content: ''; position: absolute; left: -14px; top: 50%; transform: translateY(-50%); width: 3px; height: 20px; border-radius: 0 3px 3px 0; background: var(--primary); box-shadow: 0 0 12px var(--primary); }
.nav-ico { width: 18px; text-align: center; opacity: .9; }
/* `.nav-txt` 曾经只有调用点、没有定义（E-UI-028）⇒ 侧栏里**长板块名不会省略**，
   会把整行撑宽（移动端抽屉里直接顶出边界）。补上：占满剩余宽度 + 单行省略。 */
.nav-txt { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ---- 二级菜单（2026-09-19，用户要求「左边菜单栏都设置成二级菜单，这样页面不乱」）----
   两处外壳（admin.html / console.html）共用同一套类名与同一份样式，避免两边各写一份而漂移。
   折叠靠 `.collapsed` 一个类切换（`display:none` 收 body），不用 JS 逐个改行高 ⇒ 无抖动。 */
.nav-group { margin-bottom: 4px; }
.nav-group-head {
  display: flex; align-items: center; gap: 8px; padding: 7px 9px 6px;
  font-size: 11.5px; letter-spacing: .6px; font-weight: 700; color: var(--text-mute);
  cursor: pointer; user-select: none; border-radius: var(--radius-sm);
  transition: color .2s var(--ease), background .2s var(--ease);
}
.nav-group-head:hover { color: var(--text-dim); background: rgba(120,170,255,.06); }
.nav-group-head:focus-visible { outline: 2px solid rgba(0,229,255,.6); outline-offset: 1px; }
.nav-group-arrow { font-size: 10px; line-height: 1; opacity: .75; transition: transform .22s var(--ease); }
.nav-group.collapsed .nav-group-arrow { transform: rotate(-90deg); }
.nav-group-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-group-body { display: flex; flex-direction: column; }
.nav-group.collapsed .nav-group-body { display: none; }
/* 子项：缩进 + 一条极细引导线，让层级用眼睛就能看出来（不靠字号差 —— 字号差在 12~14px 区间肉眼分不出） */
.nav-item.sub { padding-left: 25px; font-size: 13.4px; }
.nav-item.sub::after {
  content: ''; position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
  width: 1px; height: 14px; background: var(--border-strong); opacity: .6;
}
/* ---- 面板内二级菜单（2026-09-20，用户要求「所有板块里边内容多的，做成 2 级菜单」）----
   与上面 `.nav-group`（侧栏板块分组）是**两个层级**：那个决定"去哪个板块"，这个决定"看板块里的哪一区"。
   两处外壳（admin.html / console.html）与所有页面共用这一份样式，避免各页各写一套而漂移。
   桌面：左侧竖排「真·二级菜单」+ 粘性定位（滚动内容时它不动，随时可切区）。
   ≤860px：变成顶部横排可横滑 —— 两端对等（铁律⑫），不砍端、不隐藏。 */
.sec-wrap { display: grid; grid-template-columns: 212px minmax(0, 1fr); gap: 16px; align-items: start; }
.sec-nav {
  position: sticky; top: 14px; z-index: 2;
  display: flex; flex-direction: column; gap: 2px;
  padding: 8px; max-height: calc(100vh - 32px); overflow-y: auto;
}
.sec-item {
  display: flex; flex-direction: column; align-items: stretch; gap: 1px; width: 100%; text-align: left;
  padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: rgba(120,170,255,.06); color: var(--text-dim); cursor: pointer;
  font-family: inherit; font-size: 13.2px; line-height: 1.45;
  transition: color .18s var(--ease), background .18s var(--ease), border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.sec-item:hover { color: var(--text); background: rgba(120,170,255,.12); border-color: var(--border-strong); }
.sec-item:focus-visible { outline: 2px solid rgba(0,229,255,.6); outline-offset: 1px; }
/* 区名 + 计数同一行；说明（`descKey`）另起一行小字。
   说明**必须可见**而不是只挂 title：触屏看不到悬浮提示，且拼接进 title 会让它永远锚不上（见 core.js）。 */
.sec-item-top { display: flex; align-items: center; gap: 8px; min-width: 0; }
.sec-item-txt { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sec-item-n { font-size: 11px; color: var(--text-mute); font-variant-numeric: tabular-nums; }
.sec-item-desc { font-size: 11px; line-height: 1.5; color: var(--text-mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sec-item.on {
  color: #fff; border-color: var(--primary);
  background: linear-gradient(90deg, rgba(0,229,255,.22), rgba(124,77,255,.16));
  box-shadow: 0 0 0 1px rgba(0,229,255,.15) inset, 0 4px 12px rgba(0,0,0,.15);
}
.sec-item.on .sec-item-n { color: #fff; }
.sec-item.on .sec-item-desc { color: rgba(255,255,255,.75); }
.sec-all {
  margin-top: 4px; padding: 6px 10px; border: 0; border-top: 1px solid var(--border);
  background: transparent; color: var(--text-mute); cursor: pointer;
  font-family: inherit; font-size: 11.5px; text-align: left; border-radius: 0 0 var(--radius-sm) var(--radius-sm);
}
.sec-all:hover { color: var(--primary); }
.sec-body { min-width: 0; }
.sec-pane.off { display: none; }
/* 「顶部横排」变体（`.sec-top`）：宿主本身已是两栏骨架的一栏（`.split-main`）时由 core.js 挂上。
   起因是一个真机实测出来的布局缺陷：那种宿主再放 212px 竖排轨道，等于把这一栏再扣掉 228px，
   1280px 下混剪页 3 列表单每列只剩 ~120px ⇒ 标签换行、控件错位
   （真浏览器几何锁 1064/1280/1600 三档只在 1280 报红，正是"容器够窄才溢出"的形状）。
   下面 5 条与 ≤860px 横排**逐条一致** —— 同一套观感，只是触发条件从"视口窄"换成"容器窄"；
   两处并存是刻意的：媒体查询管视口，`.sec-top` 管容器，改一处不影响另一处。 */
.sec-wrap.sec-top { grid-template-columns: 1fr; gap: 10px; }
.sec-wrap.sec-top .sec-nav {
  position: static; flex-direction: row; overflow-x: auto; overflow-y: hidden;
  padding: 6px; gap: 6px; max-height: none;
}
.sec-wrap.sec-top .sec-item { width: auto; white-space: nowrap; padding: 7px 12px; }
.sec-wrap.sec-top .sec-item-txt { overflow: visible; }
.sec-wrap.sec-top .sec-all { margin: 0 0 0 auto; border-top: 0; border-left: 1px solid var(--border); border-radius: var(--radius-sm); white-space: nowrap; }
@media (max-width: 1180px) { .sec-wrap { grid-template-columns: 180px minmax(0, 1fr); gap: 12px; } }
@media (max-width: 860px) {
  .sec-wrap { grid-template-columns: 1fr; gap: 10px; }
  .sec-nav {
    position: static; flex-direction: row; overflow-x: auto; overflow-y: hidden;
    padding: 6px; gap: 6px; max-height: none;
  }
  .sec-item { width: auto; white-space: nowrap; padding: 7px 12px; }
  .sec-item-txt { overflow: visible; }
  .sec-all { margin: 0 0 0 auto; border-top: 0; border-left: 1px solid var(--border); border-radius: var(--radius-sm); white-space: nowrap; }
}
.main { flex: 1; min-width: 0; padding: 22px 26px 60px; }
.topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 22px; gap: 14px; flex-wrap: wrap; }
/* 积分余额顶部徽章：醒目、可点击 */
.credit-top-badge { cursor: pointer; font-size: 13px; padding: 5px 10px; border-radius: 20px; }
.credit-top-badge:hover { filter: brightness(1.15); }

/* ---- 卡片网格 ---- */
.grid { display: grid; gap: 16px; }
.g2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
.g3 { grid-template-columns: repeat(3, minmax(0,1fr)); }
.g4 { grid-template-columns: repeat(4, minmax(0,1fr)); }
/* ⚠ 2026-09-23（用户反馈「这空的地方看着多难看」）：
   原为 auto-fill —— 它会**保留空轨道**：整个页面只有 1 个直播间卡片时，
   栅格仍按 240px 切出 5 条轨道，卡片只占第 1 条，右侧留下 4/5 的空白。
   auto-fit 会把空轨道**折叠**掉，卡片自动铺满可用宽度（项目多了行为不变）。
   这是「自适应」本该有的语义，不是特例补丁。 */
.gauto { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
@media (max-width: 1100px) { .g4 { grid-template-columns: repeat(2, 1fr); } .g3 { grid-template-columns: repeat(2, 1fr); } }
/* ⚠ 这里曾有一条 `.sidebar { display: none; }` —— 手机端侧栏被直接隐藏、又**没有任何替代导航**，
   等于手机上根本没法切板块（铁律⑫ 要求两端对等，不能砍端）。
   2026-09-19 改为：侧栏在 ≤860px 变成**抽屉**（见下方 .nav-toggle / .nav-scrim），不再 `display:none`。 */
@media (max-width: 720px) { .g2,.g3,.g4 { grid-template-columns: 1fr; } .main { padding: 16px; } }

.stat { padding: 16px 18px; border-radius: var(--radius); position: relative; overflow: hidden; }
.stat::after { content: ''; position: absolute; right: -30px; top: -30px; width: 110px; height: 110px; border-radius: 50%; background: radial-gradient(circle, rgba(0,229,255,.20), transparent 70%); }
.stat .v { font-size: 27px; font-weight: 800; letter-spacing: .5px; }
.stat .k { font-size: 12.5px; color: var(--text-dim); margin-top: 3px; }

/* ---- 弹窗 ---- */
/* 2026-09-29（r42）：`.modal` 追加 `overscroll-behavior: contain` —— 弹窗内容滚到底/顶时
   不再把滚动**链式**传给背景页面（用户报障"鼠标在素材框外面滚，主界面动"的另一半）。
   ⚠ 只是给既有规则补一个属性，**没有新增第二份 `.modal` 规则**（E-UI-015：同类规则两份必漂移）。 */
.mask { position: fixed; inset: 0; background: rgba(3,7,18,.72); backdrop-filter: blur(6px); z-index: 90; display: grid; place-items: center; padding: 20px; }
.modal { width: 100%; max-width: 560px; max-height: 88vh; overflow-y: auto; overscroll-behavior: contain; padding: 22px; animation: pop .28s var(--ease); }
@keyframes pop { from { opacity: 0; transform: translateY(14px) scale(.97); } }
.modal-wide { max-width: 860px; }

/* ---- 弹窗动作条吸底（2026-09-29 r43）----
   用户报障：「所有板块全平台检查，只要有这种确认、确定的按钮…根据滑动条固定下来，
            这样不管怎么滑动鼠标，选好素材都能直接确定、确认」。
   `.modal` 是滚动容器，而"取消 / 确定(N)"这行动作条原本是它的最后一个子元素 ⇒ 内容一长就被推到视口外。
   `core.js` 负责挑出唯一动作条并挂 `.m-foot`（**全站只此一处实现**），这里负责把它钉住。
   ⚠ 这几条不是随手写的：
     · `bottom:0` 能贴到弹窗**底边**，靠的是同一刻给 `.modal` 加的 `.m-hasfoot{padding-bottom:0}` ——
       否则 sticky 会被"包含块下沿 = 内容框下沿（比底边高 22px）"卡住，动作条浮在底边上方 22px，
       而滚动的内容正从那道缝里穿过去（比不吸底更难看）。
       顺带：`.modal` 原有的 22px 下内边距由动作条自己的 `padding-bottom` 承接，视觉留白不丢。
     · 背景**必须两层**（`var(--panel)` 叠在 `var(--bg-2)` 上）：滚动的内容会从动作条背后穿过去，
       只写 `var(--panel)`（0.72 透明）文字会透出来。
     · `z-index:3`：动作条后面若曾留着节点，它们**晚于**它绘制，不给层级就会盖在按钮上
       （r43 起由 core.js 把它们挪到动作条之前，这里是第二道保险）。
     · 不横向出血：动作条与内容同在 `.modal` 的 content box 内、等宽，内容不会从两侧 22px 内边距里
       露出来，所以不需要 `margin:0 -22px` 那一套。
   ⚠ 不设 `margin-top`：保留调用点自己写的 `mt8/mt16` —— 那才是内容与动作条之间的间距。 */
.modal > .m-foot { position: sticky; bottom: 0; z-index: 3; padding: 12px 0 18px; background: linear-gradient(var(--panel), var(--panel)) var(--bg-2); border-top: 1px solid var(--border); }
.modal.m-hasfoot { padding-bottom: 0; }

/* ---- 文件拖拽上传 ---- */
.drop { border: 1.5px dashed var(--border-strong); border-radius: var(--radius); padding: 26px; text-align: center; color: var(--text-dim); transition: all .25s var(--ease); cursor: pointer; }
.drop:hover, .drop.over { border-color: var(--primary); background: rgba(0,229,255,.06); color: var(--text); }

/* ---- 视频预览 ---- */
video.preview { max-width: 100%; border-radius: var(--radius-sm); background: #000; border: 1px solid var(--border); object-fit: contain; }
/* r48 ②：任务详情里的**成片预览框**必须按成片的真实宽高比开框，且整幅可见、不用滚动。
 * 改前只有 `video.preview{width:100%}`：9:16 的成片在 560px 宽的弹窗里会撑到 ~1450px 高
 * ⇒ 用户只能看到最上面一截，必须滑动才看得到其余部分（他原话：「这个框看不全，必须滑动才行
 * ……生成的视频预览的时候必须要在预览框直接能看到整个视频画面」）。
 * 收口方式：舞台容器写死 aspect-ratio = 成片宽/高（由 jobDetail 用成片真实像素填 --pv-ar，
 * 元数据到了再按 videoWidth/videoHeight 校准一次），再用 max-width 把高度压进弹窗可用高度：
 * 高度由比例反推 ⇒ **框与成片比例严格一致**，视频恰好填满框、不裁切、不留黑边。
 * ⚠ 只收口"高度"，不改任何其它尺寸 —— 横屏成片仍按宽度铺满（对它来说宽度才是约束）。
 * ⚠ 留白 290px 是**实测**出来的，不是拍的：真浏览器量到弹窗 clientH=790 / scrollH=816，
 *   即"视频以外的内容"要用掉 276px。写 250 时仍差 26px 会出滚动条（第一次跑就红了），
 *   290 给出 14px 余量。这条数与视口高度无关（视频高度随 vh 缩放、其它内容固定），
 *   所以小窗口同样成立。`max(120px, …)` 是极矮窗口的兜底：宁可视频变小，也不让它变成 0 或者负数。
 * 闸门：REGRESSION [5.39] ②（锁机制 + 断言这个留白 ≥260px）。 */
.pv-stage { width: 100%; max-width: min(100%, max(120px, calc((88vh - 290px) * var(--pv-ar, .5625)))); aspect-ratio: var(--pv-ar, .5625); margin: 0 auto; background: #000; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
.pv-stage > video.preview { display: block; width: 100%; height: 100%; border: 0; border-radius: 0; object-fit: contain; }
.thumb { width: 84px; height: 50px; object-fit: cover; border-radius: var(--radius-xs); background: rgba(0,0,0,.4); border: 1px solid var(--border); }

/* ---- 动画 ---- */
@keyframes floatY { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-7px); } }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes pulseGlow { 0%,100% { opacity: .55; } 50% { opacity: 1; } }
@keyframes shimmer { 0% { background-position: -400px 0; } 100% { background-position: 400px 0; } }
.anim-float { animation: floatY 4.5s ease-in-out infinite; }
.anim-pulse { animation: pulseGlow 2.2s ease-in-out infinite; }
.spinner { width: 15px; height: 15px; border: 2px solid rgba(255,255,255,.22); border-top-color: var(--primary); border-radius: 50%; animation: spin .8s linear infinite; display: inline-block; }
.live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--danger); box-shadow: 0 0 10px var(--danger); animation: pulseGlow 1.2s infinite; }

/* ---- Toast ---- */
#toast { position: fixed; right: 18px; bottom: 18px; z-index: 200; display: flex; flex-direction: column; gap: 10px; }
.toast { padding: 12px 16px; border-radius: var(--radius-sm); background: var(--panel); border: 1px solid var(--border-strong); backdrop-filter: blur(14px); box-shadow: var(--shadow); animation: pop .28s var(--ease); max-width: 340px; font-size: 13.5px; }
.toast.ok { border-left: 3px solid var(--ok); } .toast.err { border-left: 3px solid var(--danger); } .toast.info { border-left: 3px solid var(--primary); } .toast.warn { border-left: 3px solid var(--warn); }

/* ---- 登录页 ---- */
.auth-wrap { min-height: 100vh; display: grid; place-items: center; padding: 20px; }
.auth-card { width: 100%; max-width: 420px; padding: 34px 30px; }
.auth-tabs { display: flex; gap: 4px; background: rgba(120,170,255,.08); padding: 4px; border-radius: var(--radius-sm); margin-bottom: 22px; }
.auth-tabs button { flex: 1; padding: 9px; border: none; background: transparent; color: var(--text-dim); border-radius: var(--radius-xs); cursor: pointer; font-family: inherit; font-size: 13.5px; transition: all .25s var(--ease); }
.auth-tabs button.active { background: linear-gradient(135deg, var(--primary), var(--accent)); color: #04121C; font-weight: 700; }
.feature-line { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--text-dim); margin-top: 10px; }
.feature-line i { color: var(--primary); font-style: normal; }

/* ============================================================
   内嵌实时预览（Part ①：每个板块内容旁即预览；Part ②：画面全适应；Part ③：手机端抽屉）
   所有圆角走 --radius-* 令牌，消灭「圆有变」。
   ============================================================ */

/* ---- 左右分栏：内容 | 预览面板（PC 常驻右侧） ---- */
.split { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 22px; align-items: start; }
.split-main { min-width: 0; }
.split-side { position: sticky; top: 18px; max-height: calc(100vh - 36px); overflow-y: auto; }

/* ---- 预览面板骨架 ---- */
.pv-aside { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 14px; box-shadow: var(--shadow); }
.pv-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
.pv-title { font-size: 14px; font-weight: 800; color: var(--text); letter-spacing: .3px; }
.pv-tag { font-size: 11px; padding: 2px 9px; border-radius: var(--radius-pill); border: 1px solid var(--border-strong); color: var(--text-dim); white-space: nowrap; }
.pv-tag.ok { color: var(--ok); border-color: rgba(34,231,165,.4); background: rgba(34,231,165,.10); }
.pv-hint { margin-bottom: 10px; }
.pv-body { min-height: 120px; }
.pv-foot { margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap; }
/* 同族（2026-09-29 r43 顺带）：预览面板的底部操作区也住在一个可滚容器（`.split-side`
   有 `overflow-y:auto`）里，面板一高（画布预览）它同样会被滚出视野 —— 与弹窗动作条同一类缺陷。
   ⚠ `:not(:empty)` 是必须的：`PV.set(…, '')` 会把它清空，空着还吸底会在面板底部留一条**空条**。
   ⚠ `:has()` 那一条同上：不把 `.pv-aside` 的下内边距让出来，动作条会浮在底边上方 14px、
     滚动内容从缝里穿过去。（`:has()` 不支持时只损失这 14px 的贴合度，功能不受影响。） */
.pv-aside:has(> .pv-foot:not(:empty)) { padding-bottom: 0; }
/* ⚠ r46b：页脚必须**有高度上限**，否则它能高到盖住播放区（用户截图原话「整个预览框都挡住了」）。
   现象复盘：`.pv-foot` 是 sticky 吸底 + 内容想多高就多高，一旦内容被撑长（如素材名过长、
   或后台把提示文案配得很长），它就压到播放区上面 —— 预览面板里"说明"盖过"内容"是最坏的一种。
   ⚠ 上限必须**宽松到既有内容永远碰不到**（铁律⑭：新约束不许改动存量表现）：
     实测存量页脚最大高 = 137px（混剪·手机 375px；桌面 1440px 为 121px）⇒ 取 260px（≈ 存量最大 × 1.9）。
     正常内容照旧不滚动；只有畸形内容（本轮那个 313/393px）才会被截住并可滚动查看。
   ⚠ `overflow-y:auto` 要在**同一个规则**里写：只给 max-height 不给 overflow，超出部分会直接
     被裁掉且看不全（等于把内容变没了）—— 那比原来的"盖住预览"更糟。

   ⚠⚠ r46c 更正（**上面那套"限高"思路只能兜底，治不了本**）：
     用户第二次报同一处：「这个还是挡住预览框了」。实测（scripts/verify-r46c-preview-tips.js）：
     `.pv-foot` 是 `position:sticky; bottom:0` 且住在可滚容器 `.split-side`（`overflow-y:auto`）里 ——
     只要「头部 + 画布 + 页脚」超过容器可见高，容器就开始滚，sticky 页脚被**钉在容器底边**
     ⇒ 它压在画布上是**必然**，压多高只取决于"页脚有多高"，与"上限设多少"无关。
     1440×673（用户那台机器的窗口比例）下容器可见高 637px、画布 430px ⇒ 页脚超过 ≈157px 就必然遮挡；
     260px 的上限远大于 157px ⇒ 等于没限（这就是 r46b 没解决的原因）。
     ⇒ 两层修：① 内容变短（详细说明移进「操作提示」弹层，见 console.js `mixerTips()`）；
               ② 只放说明/入口、**不放动作**的页脚改为不吸底（`.pv-foot-flow`）。
     上限同步收紧到 128px（仍在"不遮挡余量 157px"之下）；反面样本见 REGRESSION `[5.36]`。 */
.pv-foot:not(:empty) { position: sticky; bottom: 0; z-index: 2; padding: 10px 0 14px; background: linear-gradient(var(--panel), var(--panel)) var(--bg-2); border-top: 1px solid var(--border); max-height: 128px; overflow-y: auto; overscroll-behavior: contain; }
/* r46c：**不吸底**的页脚（混剪预览用）。吸底只对"动作条"（如模板中心的「出真实预览帧」）有意义；
   对"说明 + 入口"这种内容，吸底只会换来"永远压在画布上"。改为随内容排布 ⇒ 任意窗口高度都不可能遮挡。 */
.pv-foot.pv-foot-flow { position: static; z-index: auto; display: block; }
/* r46c：页脚 = 一行来源说明 + 末尾「操作提示」入口。
   ⚠ 刻意用 **inline 排版**（不是 flex）：flex 会把"说明"和"按钮"当成两个**整块**，
     窄屏上说明占满整行 ⇒ 按钮被挤到**独立第二行** ⇒ 页脚平白高出一行（实测 375px 下 121px）。
     改成 inline 之后按钮跟在说明最后一个字后面，窄屏只是多折一行，不会多出一个按钮行。 */
.pv-foot .pv-cap-tx { font-size: 12px; line-height: 1.6; color: var(--text-dim); }
.pv-foot.pv-foot-flow .pv-foot-tip { display: inline-block; vertical-align: baseline; margin-left: 8px; white-space: nowrap; }
/* r46c：「操作提示」弹层正文的排版（内容来自 ui-keys，逐条 esc 后以 <br> 分行） */
.tips-list { line-height: 1.95; font-size: 13px; }

/* ---- 模板画布（核心）：contain 语义，永不拉伸变形 ---- */
.pvc { position: relative; margin: 0 auto; border-radius: var(--radius-sm); overflow: hidden; border: 1px solid var(--border-strong); background: #000; box-shadow: 0 10px 30px rgba(0,12,40,.45); }
.pvc-layer { position: absolute; top: 0; left: 0; }
/* 画面口径必须与成片一致（server/services/render.js）：
   背景 / 播放区 = scale increase + crop → 对应 CSS cover（铺满、居中裁切，不拉伸、不留黑边）
   贴纸 / 固定素材 = scale decrease      → 对应 CSS contain（等比贴合框）
   此前预览用 contain、成片用 crop，导致「预览有黑边、成片被裁」的不一致（用户反馈的画面问题）。 */
.pvc-bg, .pvc-media { width: 100%; height: 100%; object-fit: cover; }
.pvc-sticker { object-fit: contain; }
/* 任务级自定义贴纸：可拖拽编辑态视觉反馈 */
.pvc-stk-edit { touch-action: none; filter: drop-shadow(0 2px 6px rgba(0,0,0,.55)); outline: 1px dashed rgba(0,229,255,.0); transition: outline-color .15s var(--ease); }
.pvc-stk-edit:hover { outline-color: rgba(0,229,255,.55); }
.pvc-stk-edit.pvc-dragging, .pvc-stk-edit:active { outline-color: var(--primary); }
/* 贴纸编辑器面板：范围滑杆与卡片 */
#stickerList .glass { border: 1px solid var(--border); }
#stickerList input[type="range"] { width: 100%; accent-color: var(--primary); margin: 2px 0 2px; }
#stickerList .lb { margin-bottom: 2px; }
/* 贴纸显示时间窗 · 可视化时间轴（拖动两端圆点改出现/消失时刻） */
.stk-tl { margin-top: 4px; }
.stk-tl-track { position: relative; height: 22px; background: rgba(120,170,255,.10); border: 1px solid var(--border); border-radius: var(--radius-pill); cursor: pointer; }
.stk-tl-fill { position: absolute; top: 0; bottom: 0; background: linear-gradient(90deg, rgba(0,229,255,.34), rgba(124,77,255,.34)); border-radius: var(--radius-pill); pointer-events: none; }
.stk-tl-h { position: absolute; top: 50%; width: 14px; height: 14px; transform: translate(-50%, -50%); background: var(--primary); border: 2px solid #04121C; border-radius: 50%; box-shadow: 0 0 10px rgba(0,229,255,.5); cursor: grab; touch-action: none; }
.stk-tl-h[data-h="end"] { background: var(--accent); box-shadow: 0 0 10px rgba(124,77,255,.5); }
.stk-tl-h.dragging { cursor: grabbing; filter: brightness(1.18); }
/* 贴纸市场：已加入的那几张要有明确标记（r38）——「点过了」必须留痕，不能靠回忆。
   选中样式只给**描边 + 角标**，不改缩略图尺寸（市场是网格，改尺寸会让整排跳动）。 */
.stk-tile { position: relative; }
.stk-tile.on { border-color: var(--ok); box-shadow: inset 0 0 0 1px rgba(34,231,165,.35); }
.stk-added {
  position: absolute; right: 4px; top: 4px; font-size: 10px; line-height: 1.5;
  color: #04121C; font-weight: 800; background: var(--ok);
  padding: 1px 6px; border-radius: var(--radius-pill); pointer-events: none; white-space: nowrap;
}

/* ---------------- 长名称统一显示口径（r38 需求③）----------------
   用户原话：「这个文件名称……这么长的名称占地方，还不美观，修改优化，可以不显示全，
   鼠标放上去或者点击一下，隐藏的内容显出来，松手就不显示」。
   ⚠ 本块是**全站唯一**的"名称过长"显示口径：界面上任何"可能很长的名字"
     （素材名 / 成品名 / 贴纸名 / 文件名 / BGM 名）一律用 `UI.nm()` 渲染，
     不要再各自写 `text-overflow` —— 分散实现必然漂移（E-UI-015「同一件事两个口径」同族；
     本仓库此前已有 4 处各写各的省略，同一条素材名在 A 页截断、在 B 页把表格撑开三行）。
   ⚠ 气泡必须 `position:fixed`：`.nm` 常常挂在 `overflow:auto` 的表格单元格里，
     容器内绝对定位会被裁掉 —— 那正是"长名称永远看不到全貌"的病根。 */
.nm {
  display: inline-block; max-width: var(--nm-max, 12em);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  /* ⚠ `min-width:0` 不能省：`.nm` 常作为 `.badge`（inline-flex）的弹性子项，
     弹性项的 `min-width:auto` 默认按内容撑开 → 名称长了会把整行顶宽、省略号失效。
     （`overflow:hidden` 理论上已让自动最小尺寸归零，这里显式写死，不依赖该细节。） */
  min-width: 0;
  vertical-align: bottom; cursor: help;
}
.nm-pop {
  position: fixed; left: 0; top: 0; z-index: 400; display: none; max-width: min(560px, 86vw);
  padding: 8px 11px; border-radius: 10px; font-size: 12.5px; line-height: 1.55;
  color: var(--text); background: rgba(6, 14, 26, .97); border: 1px solid var(--border-strong);
  box-shadow: 0 16px 44px rgba(0, 0, 0, .55); white-space: pre-wrap; word-break: break-word;
  pointer-events: none;
}
.nm-pop.on { display: block; }

/* ---- 上屏 / 下屏固定素材槽（播放区之外的上下两条黑区） ---- */
.pvc-slot { display: grid; place-items: center; cursor: pointer; border: 1.5px dashed rgba(120,200,255,.42); background: rgba(0,229,255,.05); transition: all .2s var(--ease); }
.pvc-slot:hover { border-color: var(--primary); background: rgba(0,229,255,.13); }
.pvc-slot-hint { font-size: 11px; line-height: 1.4; color: #9FD8FF; background: rgba(0,0,0,.45); padding: 2px 9px; border-radius: var(--radius-pill); white-space: nowrap; max-width: 96%; overflow: hidden; text-overflow: ellipsis; }
.pvc-slot.filled { border-color: transparent; background: transparent; }
.pvc-slot.filled .pvc-slot-hint { opacity: 0; transform: translateY(4px); transition: opacity .2s var(--ease), transform .2s var(--ease); }
.pvc-slot.filled:hover { border-color: var(--primary); background: rgba(0,229,255,.09); }
.pvc-slot.filled:hover .pvc-slot-hint { opacity: 1; transform: none; }
.pvc-slot-x { position: absolute; right: 4px; top: 4px; width: 18px; height: 18px; padding: 0; line-height: 1; border: none; border-radius: var(--radius-pill); background: rgba(255,92,122,.88); color: #fff; font-size: 11px; font-family: inherit; cursor: pointer; display: grid; place-items: center; }
.pvc-slot-x:hover { background: var(--danger); }
.pvc-empty { display: grid; place-items: center; text-align: center; color: var(--text-mute); background: repeating-linear-gradient(45deg, rgba(120,170,255,.05) 0 10px, transparent 10px 20px); line-height: 1.5; }
.pvc-empty span { font-size: 12px; opacity: .8; }
.pvc-text { pointer-events: none; overflow: hidden; text-shadow: 0 1px 3px rgba(0,0,0,.55); }
/* 需求②：可编辑文字层 —— 放开指针事件才能拖拽，hover/拖拽时给出青色描边提示 */
.pvc-text-edit { pointer-events: auto; cursor: grab; outline: 1px dashed transparent; outline-offset: 2px; transition: outline-color .15s var(--ease); }
.pvc-text-edit:hover { outline-color: rgba(0,229,255,.55); }
.pvc-text-edit:active, .pvc-text-edit.pvc-dragging { cursor: grabbing; outline-color: var(--primary); }
/* ---- 选中态（点选文字层 / 贴纸）—— r38 修复「幽灵类名」----
   ⚠ `public/js/console.js` 的 `mixSelApply()` 一直在加 `.pvc-sel`（JS 里 5 处引用），
     但本文件**从来没有定义过它**（`grep -c '\.pvc-sel' theme.css` = 0）⇒
     点选文字/贴纸时**没有任何视觉反馈**。用户原话正是「没有任何提示，我都不知道添加过了」。
     与 E-UI-026（`items-end` 幽灵类名）同族：**只在 JS 里存在的类名 = 一行没写出来的承诺**。
   用 `outline` 而非 border/padding：不占布局、不改尺寸；且**只描边、不填充背景** ——
   高亮绝不能把画面或贴纸本身遮住（与"框挡住预览面"是同一条原则）。
   ⚠ 必须排在 `.pvc-text-edit:hover` / `.pvc-stk-edit:hover` **之后**：
     两者特异性同为 (0,2,0)，靠源码顺序决胜，放前面会被 hover 规则盖掉。 */
.pvc-text-edit.pvc-sel, .pvc-sticker.pvc-sel, .pvc-sel { outline: 2px solid var(--primary); outline-offset: 2px; }
.pvc-text.pvc-sel, .pvc-sticker.pvc-sel { filter: drop-shadow(0 0 6px rgba(0, 229, 255, .8)); }
/* ---- r45：预览元素「点一下 → 左侧面板跳到对应分区」的可点提示 ----
   用户原话：「用户在预览页面选中哪个素材，左边的编辑面板直接跳转到相应的边界面板」。
   ⚠ 本类**只负责"看起来能点"**（光标），**不接管任何交互** ——
     点击语义在 `preview.js` 的 `bindJump()`（pointerup 且位移 ≤4px 才算点击），
     拖拽归各元素原有实现（bindText / bindStickers / bindStage）。
   ⚠ **必须让位给"可拖拽"**：可拖元素的第一语义是拖，光标必须是 grab 而不是 pointer。
     `.pvc-jump` 与 `.pvc-text-edit` 特异性同为 (0,1,0)，靠源码顺序决胜 ⇒
     下面那条 (0,2,0) 的复合选择器必须写在**后面**，且用显式 `grab` 重申（不用 `cursor: inherit`
     —— 那会继承容器光标变成默认箭头，"这块能拖"的提示当场消失）。
   ⚠ 不给 `.pvc-stage-drag` 写规则：它的光标是 JS 内联设的（`el.style.cursor = 'grab'`），
     内联样式优先级高于本表任何选择器，写了也不会生效（写了就是一行假代码）。 */
.pvc-jump { cursor: pointer; }
.pvc-text-edit.pvc-jump, .pvc-stk-edit.pvc-jump { cursor: grab; }
/* 跳转后给目标分区菜单项一个短脉冲：纯色闪，不动布局（改 padding/尺寸会把菜单挤歪）。
   ⚠ 与 `.sec-item.on` 并存 —— 这里是**瞬时反馈**，不是持久状态，动画结束就自动消失。 */
@keyframes secJumpFlash { 0% { box-shadow: 0 0 0 0 rgba(0,229,255,.75); } 100% { box-shadow: 0 0 0 10px rgba(0,229,255,0); } }
.sec-item.jump-flash { animation: secJumpFlash .8s var(--ease) 1; }
/* r49 ①：点预览里的素材 → 跳到它的**来源**面板之后，把那个素材条目本身指出来（短脉冲）。
   用户原话：「如果是分镜上传的素材，点击预览框素材的时候联动到分镜界面素材，
              如果素材来源上传的素材预览框素材的时候联动到素材来源」。
   ⚠ 只用 `outline` + `box-shadow`，**不许用 border / padding / 尺寸**：
     `.chip` 的高度是回归锁死的不变量（--chip-h），加 border 会当场改变盒高 ⇒ 几何锁报红。
     outline 不参与布局，`outline-offset` 也只影响绘制。
   ⚠ 选择器认 `data-sb-asset` / `data-mp-asset` 这两个**稳定属性**，不认 class 也不认文本 ——
     素材名会被 UI.nm() 截断、还会被转义，拿文本定位必然时对时错。 */
@keyframes srcJumpMark {
  0% { outline-color: rgba(0, 229, 255, .95); box-shadow: 0 0 0 0 rgba(0, 229, 255, .55); }
  100% { outline-color: rgba(0, 229, 255, 0); box-shadow: 0 0 0 10px rgba(0, 229, 255, 0); }
}
[data-sb-asset].jump-mark, [data-mp-asset].jump-mark {
  outline: 2px solid rgba(0, 229, 255, .95); outline-offset: 1px;
  animation: srcJumpMark 1.6s var(--ease) 1; }
/* 需求⑤：上下屏边界拖拽手柄 —— 默认半透明，悬停高亮 */
.pvc-stg-edge { height: 8px; margin-top: -4px; background: linear-gradient(90deg, var(--primary), var(--accent)); opacity: 0; border-radius: 4px; cursor: ns-resize; transition: opacity .15s var(--ease); z-index: 6; }
.pvc-stg-edge:hover, .pvc-stg-edge:active { opacity: .85; }
/* 播放区描边，让「画布 / 上屏 / 下屏」三者边界一眼分清（解决"画布尺寸和上屏素材重叠看不清"）。
   ⚠ r38（2026-09-28 用户反馈「这个框挡住了预览面，我都不知道添加过了」）：
     描边是**参照线**不是内容，但它原来**常显并且抢眼**，注意力被框吸走、反而看不清画面。
     改为：默认压到很淡、鼠标移进播放区才点亮（那时才真正需要"看清边界"）。
     宽度**保持 1px 不加粗** —— 加粗就是真的挡画面了。 */
.pvc-stagebox { outline: 1px solid rgba(0,229,255,.16); outline-offset: -1px; transition: outline-color .18s var(--ease); }
.pvc-stagebox:hover { outline-color: rgba(0,229,255,.55); }
.pvc-clock { top: auto; left: auto; right: 8px; bottom: 8px; font-family: "JetBrains Mono", Consolas, monospace; color: #fff; background: rgba(0,0,0,.4); padding: 2px 9px; border-radius: var(--radius-pill); text-shadow: 0 1px 4px rgba(0,0,0,.85); }
.pvc-marquee { left: 0; right: 0; bottom: 0; width: 100%; text-align: center; color: #fff; background: linear-gradient(90deg, rgba(0,0,0,.6), rgba(0,0,0,.35)); padding: 4px 0; white-space: nowrap; overflow: hidden; text-shadow: 0 1px 3px rgba(0,0,0,.8); animation: pvMarquee 9s linear infinite; }
.pvc-badge { left: 8px; top: 8px; font-size: 10.5px; color: #d6f3ff; background: rgba(0,0,0,.5); border: 1px solid rgba(255,255,255,.18); padding: 2px 9px; border-radius: var(--radius-pill); backdrop-filter: blur(4px); }
.pvc-badge.ok { color: var(--ok); border-color: rgba(34,231,165,.5); }
.pvc-noise { background: repeating-linear-gradient(0deg, rgba(255,255,255,.05) 0 1px, transparent 1px 3px); mix-blend-mode: overlay; }
.pvc-ailabel { right: 8px; top: 8px; left: auto; font-size: 10.5px; color: #04121C; font-weight: 800; background: linear-gradient(135deg, var(--primary), var(--accent)); padding: 2px 9px; border-radius: var(--radius-pill); box-shadow: 0 4px 14px rgba(0,229,255,.3); }
@keyframes pvMarquee { from { transform: translateX(60%); } to { transform: translateX(-100%); } }

/* ---- 通用媒体预览（素材 / 成片） ---- */
.pv-cap { margin-top: 10px; }
.pv-cap-t { font-size: 13.5px; font-weight: 700; color: var(--text); }
.pv-media-box { width: 100%; display: block; border-radius: var(--radius-sm); background: #000; border: 1px solid var(--border); object-fit: contain; }
.pv-media-box.pv-contain { max-height: 62vh; }
.pv-audio { display: flex; align-items: center; gap: 12px; background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 14px 16px; }
.pv-audio-ico { font-size: 24px; color: var(--primary); }
.pv-audio audio { flex: 1; min-width: 0; }
.pv-empty-box { display: grid; place-items: center; min-height: 150px; color: var(--text-mute); background: var(--panel-2); border: 1px dashed var(--border-strong); border-radius: var(--radius-sm); }
.pv-actions { margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap; }

/* ---- 移动端：预览面板抽屉 FAB + 遮罩 ---- */
.pv-toggle { position: fixed; right: 16px; bottom: 78px; z-index: 150; display: none; align-items: center; gap: 6px; padding: 12px 17px; border: none; border-radius: var(--radius-pill); background: linear-gradient(135deg, var(--primary), var(--accent)); color: #04121C; font-weight: 800; font-size: 13.5px; font-family: inherit; cursor: pointer; box-shadow: 0 10px 30px rgba(0,229,255,.35); }
.pv-toggle:active { transform: scale(.96); }
.pv-scrim { position: fixed; inset: 0; background: rgba(3,7,18,.55); backdrop-filter: blur(3px); z-index: 140; opacity: 0; visibility: hidden; transition: opacity .25s var(--ease); }

/* ---- 移动端：侧边栏抽屉 FAB + 遮罩 ----
   PC 端侧栏常驻；≤860px 变成从左滑出的抽屉，靠左下角 FAB 呼出。
   `.pv-toggle` 在右下角（bottom:78px），本按钮在左下角 ⇒ 两个 FAB 不会叠在一起。 */
.nav-toggle {
  position: fixed; left: 16px; bottom: 78px; z-index: 150; display: none;
  align-items: center; gap: 6px; padding: 12px 17px;
  border: 1px solid var(--border-strong); border-radius: var(--radius-pill);
  background: var(--panel-2); color: var(--text);
  font-weight: 800; font-size: 13.5px; font-family: inherit; cursor: pointer;
  box-shadow: 0 10px 30px rgba(0,0,0,.45);
}
.nav-toggle:active { transform: scale(.96); }
.nav-scrim { position: fixed; inset: 0; background: rgba(3,7,18,.55); backdrop-filter: blur(3px); z-index: 130; opacity: 0; visibility: hidden; transition: opacity .25s var(--ease); }

/* ---- 响应式：断点口径与 JS 共用 ----
   ⚠️ 媒体查询的条件里【不能写 var()】（CSS 规范不允许自定义属性参与媒体查询求值，
   写了会导致整个 @media 块被浏览器直接丢弃 —— 手机端抽屉曾因此完全不生效）。
   所以这里必须是字面量，且与 :root 里的 --bp-side / --bp-mobile 保持一致。 */
@media (max-width: 1180px) {   /* = var(--bp-side) */
  .split { grid-template-columns: 1fr; }
  .split-side { position: static; max-height: none; }
}
@media (max-width: 860px) {    /* = var(--bp-mobile) */
  .pv-toggle { display: inline-flex; }
  /* 侧栏 → 左抽屉（与右下角的预览抽屉对称：一个进一个出，不抢同一个角） */
  .nav-toggle { display: inline-flex; }
  .sidebar {
    position: fixed; top: 0; left: 0; height: 100vh; width: min(80vw, 268px);
    z-index: 135; background: var(--bg-2); border-right: 1px solid var(--border-strong);
    transform: translateX(-102%); transition: transform .28s var(--ease);
    box-shadow: 20px 0 60px rgba(0,0,0,.5);
  }
  body.nav-open .sidebar { transform: translateX(0); }
  body.nav-open .nav-scrim { opacity: 1; visibility: visible; }
  .split-side {
    position: fixed; top: 0; right: 0; height: 100vh; width: min(86vw, 380px);
    max-height: none; background: var(--bg-2); border-left: 1px solid var(--border-strong);
    padding: 16px; z-index: 145; overflow-y: auto; transform: translateX(100%);
    transition: transform .28s var(--ease); box-shadow: -20px 0 60px rgba(0,0,0,.5);
  }
  body.pv-open .split-side { transform: translateX(0); }
  body.pv-open .pv-scrim { opacity: 1; visibility: visible; }
}

/* ===== 需求②：播放区裁剪盒 + 拖拽提示（与 render.stageFitChain 同一套几何） ===== */
.pvc-stagebox { overflow: hidden; }
.pvc-stagebox .pvc-empty { position: absolute; inset: 0; }
.pvc-stage-drag { touch-action: none; }
/* 播放区拖拽提示（r38 重做 · 这是**真正挡住画面**的那一个）。
   ⚠ 原实现是 `left:0; right:0; bottom:0` + `rgba(4,18,28,.55)` 的**通栏深色横条**，贴在播放区底部 ——
     而那一带正是字幕/贴纸/画面主体所在的位置。用户截图里"框挡住了预览面"看到的就是它：
     一条深色横带盖在最该看的区域上，常驻不退。
   改为：居中**小胶囊**（宽度随文字自适应，不超过播放区 92%）+ 出现 2.6s 后自动淡出，
   鼠标移进播放区再唤回 —— 定位从"常驻遮罩"降级为"一次性的引导提示"。 */
.pvc-stage-hint {
  position: absolute; left: 50%; bottom: 6px; transform: translateX(-50%);
  max-width: calc(100% - 12px);
  padding: 3px 10px; font-size: 10px; line-height: 1.5;
  color: #EAF6FF; background: rgba(4, 18, 28, .62); border-radius: var(--radius-pill);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  text-align: center; pointer-events: none;
  animation: pvHintFade .9s var(--ease) 2.6s forwards;
}
.pvc-stagebox:hover .pvc-stage-hint { animation: none; opacity: 1; }
@keyframes pvHintFade { to { opacity: 0; } }
/* ===== 需求①：成品库 ===== */
.pd-layout { display: grid; grid-template-columns: 210px 1fr; gap: 16px; align-items: start; }
/* 2026-09-24 需求①：≤860px 分组列不再占 210px —— 否则成品网格被挤到最小轨道 190px 之下，
   整张成品卡（含操作按钮）捅出视口右缘（375px 实测 card 右缘 507 > 375）。分组上移、网格全宽。 */
@media (max-width: 860px) {
  .pd-layout { grid-template-columns: 1fr; }
  .pd-groups { max-height: 40vh; }
}
.pd-groups { display: flex; flex-direction: column; gap: 4px; max-height: 62vh; overflow: auto; }
.pd-group {
  display: flex; align-items: center; justify-content: space-between; gap: 6px;
  padding: 8px 10px; border-radius: 10px; cursor: pointer;
  border: 1px solid transparent; font-size: 13px; color: var(--text-dim);
}
.pd-group:hover { background: rgba(120, 170, 255, .08); }
.pd-group.on { background: rgba(0, 229, 255, .12); border-color: rgba(0, 229, 255, .45); color: var(--text); }
.pd-group .pd-n { font-size: 11px; opacity: .7; }
.pd-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
.pd-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 12px; }
.pd-card { padding: 8px; border-radius: var(--radius-sm); border: 1px solid var(--border); background: rgba(10, 20, 34, .55); }
.pd-card.on { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(0, 229, 255, .22); }
.pd-thumb { position: relative; width: 100%; aspect-ratio: 9 / 12; background: #04101A; border-radius: 8px; overflow: hidden; cursor: pointer; }
.pd-thumb img, .pd-thumb video { width: 100%; height: 100%; object-fit: contain; display: block; }
/* E-UI-019（2026-10-01 移动端闸门抓出）：这个勾选框的**有效点击区必须 ≥32px**。
   为什么：它长在缩略图上，而缩略图/卡片的点击语义是"播放"（`.pd-thumb{cursor:pointer}`），
   素材库卡片更是**连 onclick 都没有** ⇒ 手机上唯一能选中它的，就是那个 16×16 的框子
   （实测 `LABEL` 仅 23×22，低于触控下限 32）。
   做法：给 LABEL 补 9px 内边距 + 9px 负外边距 —— 内容（框子）**位置一点没动**（6-9+9=6），
   可点区从 23×22 撑到 ~34×34。改的是热区，不是视觉。 */
.pd-pick { position: absolute; left: 6px; top: 6px; padding: 9px; margin: -9px; }
.pd-meta { font-size: 12px; margin-top: 6px; line-height: 1.6; }
.pd-tools { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
/* ===== 需求③④：AI 创作 ===== */
.ai-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
.ai-tab { padding: 7px 14px; border-radius: 10px; border: 1px solid var(--border); background: rgba(120,170,255,.06); color: var(--text-dim); cursor: pointer; font-size: 13px; transition: color .18s var(--ease), background .18s var(--ease), border-color .18s var(--ease), box-shadow .18s var(--ease); }
.ai-tab:hover { background: rgba(120,170,255,.12); border-color: var(--border-strong); color: var(--text); }
.ai-tab.on { background: linear-gradient(90deg, rgba(0,229,255,.22), rgba(124,77,255,.16)); border-color: var(--primary); color: #fff; box-shadow: 0 0 0 1px rgba(0,229,255,.15) inset, 0 4px 12px rgba(0,0,0,.15); }
.ai-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
.ai-card { padding: 8px; border-radius: var(--radius-sm); border: 1px solid var(--border); background: rgba(10, 20, 34, .55); }
.ai-card .ai-th { width: 100%; aspect-ratio: 3 / 4; background: #04101A; border-radius: 8px; overflow: hidden; }
.ai-card .ai-th img, .ai-card .ai-th video { width: 100%; height: 100%; object-fit: contain; display: block; }
.ai-kf { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.ai-kf img { width: 62px; height: 62px; object-fit: cover; border-radius: 6px; border: 1px solid var(--border); }
.ai-ta { width: 100%; min-height: 92px; resize: vertical; }

/* ---------------- 需求①：语音时长口径展示（累计录制 / 有效语音 / 静音） ---------------- */
.vr-stat { display: flex; flex-wrap: wrap; gap: 8px; }
.vr-chip { display: flex; flex-direction: column; gap: 2px; padding: 7px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: rgba(10, 20, 34, .5); min-width: 104px; }
.vr-chip b { font-size: 15px; font-weight: 800; font-family: "JetBrains Mono", Consolas, monospace; line-height: 1.2; }
.vr-chip em { font-style: normal; font-size: 11px; color: var(--text-mute); }
.tbl .num, .num { text-align: right; font-family: "JetBrains Mono", Consolas, monospace; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ---------------- 需求④：混剪预览统一画框（上屏 / 播放区 / 下屏 对齐成一体） ---------------- */
.pvc-frame { position: relative; margin: 0 auto; max-width: 100%; border-radius: var(--radius-sm); overflow: hidden; border: 1px solid var(--border); box-shadow: 0 10px 30px rgba(0, 0, 0, .35); }
.pvc-frame .pvc { border-radius: 0; }
/* ⚠ `.pvc-wrap` / `.pvc-meta` / `.pvc-live` 曾经**只有调用点、没有定义**（2026-09-29 扫描发现，E-UI-028）。
   `preview.js` 的每一处内嵌预览都在用它们 ⇒ 三个类一个都没生效：
     · `.pvc-wrap` 缺 `margin:0 auto` ⇒ 预览画框在宽栏里**贴左**（本该居中）；窄栏里还会溢出。
     · `.pvc-meta`（画框**下方**那行「播放区 W×H · 上屏 … · 画布 …」）没有样式 ⇒ 与画框贴死、
       字号跟正文一样大，看起来像画框的一部分而不是注释。
     · `.pvc-live`（AI 直播模拟画面）没有自身盒模型 ⇒ 直播预览与普通预览长得一样，分不清。
   补上定义：三者都是**纯样式**，不动 preview.js 的任何结构。 */
.pvc-wrap { display: block; margin: 0 auto; max-width: 100%; }
.pvc-meta {
  margin-top: 8px; text-align: center; font-size: 11.5px; line-height: 1.6;
  color: var(--text-mute); font-variant-numeric: tabular-nums;
}
.pvc-live { border-color: rgba(0, 229, 255, .38); box-shadow: 0 10px 30px rgba(0, 12, 40, .45), 0 0 0 1px rgba(0, 229, 255, .12) inset; }
.pvc-band-tag { position: absolute; left: 6px; top: 4px; font-size: 10px; line-height: 1.5; color: #9FD8FF; background: rgba(0, 0, 0, .5); padding: 1px 6px; border-radius: var(--radius-pill); pointer-events: none; }
.pvc-band-tag.stage { color: #B9FFE9; }
.pvc-layer.pvc-slot { border-style: dashed; border-radius: 0; margin: 0; }
/* ---- r47 ②：上屏/下屏固定素材 —— 「铺满」画框、常驻"调整"入口、选中后的工具条 ----
   ⚠ 圆角一律走令牌：REGRESSION/verify-fixslot.js 会断言"非令牌数值圆角只剩 6px 一处"。
   ⚠ .pvc-fixbox 必须 overflow:hidden —— 它是"条带裁切"的唯一实现（放大后多余部分要裁掉）。 */
.pvc-fixbox { overflow: hidden; line-height: 0; }
/* ⚠ 固定素材的选中态用 `.pvc-fixsel`，**不是** `.pvc-sel`：
   `.pvc-sel` 是「文字/贴纸点选」的既有类，`console.js` 的 `mixSelApply()` 会
   `body.querySelectorAll('.pvc-sel')` 全量摘掉再按 MIX_SEL 加回 —— 挂 `.pvc-sel` 的槽位
   会被当场摘掉（r47 探针实测：工具条已出、class 里却查不到选中类）。失败过一次的教训，写在这儿。 */
.pvc-fixbox.pvc-fixsel { outline: 2px solid var(--primary); outline-offset: -2px; }
/* ⚠ 选中态的槽位必须**抬到文案层之上**（z-index 8 > 上屏/下屏拖拽手柄的 6）：
   预览的 DOM 顺序是 …固定素材 → 槽位 → 文本层…，文本层在后 ⇒ 天然盖住槽位。
   r47 实测：工具条本身画出来了，但正中那条带被文案层压住 ⇒ 「＋」按钮的坐标上最上层是文字层，
   真鼠标点下去点到的是文字（"有按钮、点不动"，最坏的一种假象）。
   只在**选中时**抬层：没选中时槽位仍在文案层之下，点文字选中的老行为一字不动。
   代价（如实写）：某条带被选中期间，落在该条带矩形**内部**的文字点不到 —— 再点一次「调整」即可取消选中。 */
/* r50：选中的条带**本体就是拖动面** —— 用户原话「上下屏素材优化可以随意拖动，
   整个调整逻辑和播放区的逻辑一样」。播放区靠 `.pvc-stage-drag` 的 `touch-action:none`
   才拖得动（否则触屏手势会被浏览器抢去滚页面），条带这边同理。
   ⚠ 为什么必须由**槽位层**当拖动面：槽位是编辑器叠加层，选中时 z-index:8 盖在
     `.pvc-fixbox` 与文案层之上 ⇒ 真正吃到 pointerdown 的永远是槽位。
     r47 那个 bindFix 把 pointerdown 绑在 `.pvc-fixbox` 上，所以**一次都没触发过**（E-UI-051）。
   ⚠ 只改「外框颜色/描边/抬层/光标/触摸手势」，不碰尺寸与定位 —— r49 的几何闸门照旧。 */
.pvc-layer.pvc-slot.pvc-fixsel { border-color: var(--primary); border-style: solid; z-index: 8; cursor: grab; touch-action: none; }
.pvc-slot-act { position: absolute; right: 26px; top: 4px; height: 18px; padding: 0 7px; line-height: 1;
  border: none; border-radius: var(--radius-pill); background: rgba(0, 229, 255, .22); color: #EAF8FF;
  font-size: 10px; font-family: inherit; cursor: pointer; }
.pvc-slot-act:hover { background: rgba(0, 229, 255, .42); }
/* ---------------- r49 ②：「选中固定素材」调整工具条 —— 永远装在条带里 ----------------
   用户原话：「选中素材，调整的板块提示按钮都溢出了，修复好，上下屏都是这个问题」。
   改前：单行 flex、无宽度上限 ⇒ 实测单行内容 ≈340px，而预览框最大 320px
   ⇒ 左右各被 `.pvc` 的 overflow:hidden 裁掉一截，控件看着"缺半个"（截图里「已选中固定素材」
   只剩「定素材」）。**上屏、下屏是同一段代码，所以两处一起坏**。

   ⚠⚠ 两条改法缺一不可 —— 只做任何一条都挡不住同类回潮：
     ① **结构分行**（真正的修法）：控件一行、说明一行。两行各自都远窄于条带
        （控件行 ≈191px / 说明行 ≈150px < 320px）⇒ 正常情况根本不需要裁。
     ② **CSS 兜底**（防将来）：`max-width` 夹住 + `flex-wrap` 换行 + 居中。
        多一个按钮、或换语言后文字变长 ⇒ 换行，**不是溢出**。
        兜底不能代替结构：只加 max-width 会让内容挤成两行乱序，控件位置跳来跳去。
   ⚠ `max-width: calc(100% - 8px)` 的百分比对**绝对定位元素**是相对**包含块的 padding box**
     = `.pvc-slot` 的条带宽度（已扣掉它的 1.5px 虚线边框）⇒ 天然落在条带内部。
   ⚠⚠ **`width: max-content` 不是可选项，是必需的**（r49 实测踩到，`E-UI-049`）：
     绝对定位元素只给 `left` 不给 `right`/`width` 时，shrink-to-fit 的**可用宽度 = 包含块宽 − left**
     = 条带宽的一半（实测 241.91/2−1.5 = **119.45px**）。光加 `max-width` 夹不住这个因素 ——
     于是 `flex-wrap` 会把控件折成 4 行、工具条缩成 119px 宽的一条窄柱：
     **"不溢出"了，但完全不协调**（用户的第二句要求是"协调在框内"，不是"只要不出框"）。
     写死 `width: max-content` 后：单行内容天然宽度（≈197px）即为宽度，再由 `max-width` 夹住、
     由 `flex-wrap` 兜底换行。**三个条件缺一不可**。
   ⚠ 圆角从 `--radius-pill` 改成小圆角：两行内容套胶囊半径，两端会鼓出两个"耳朵"。
     本条的圆角只影响这一个组件，不动 `--radius-pill` 令牌本身。 */
.pvc-fixbar { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  padding: 4px 7px; border-radius: var(--radius); box-sizing: border-box;
  width: max-content; max-width: calc(100% - 8px);
  background: rgba(4, 16, 26, .88); box-shadow: 0 4px 14px rgba(0, 0, 0, .5); line-height: 1; }
/* 两行容器：各自居中、各自可换行、各自不许超过工具条 */
.pvc-fixrow, .pvc-fixnote { display: flex; align-items: center; justify-content: center;
  gap: 5px; flex-wrap: wrap; max-width: 100%; }
.pvc-fixnote { gap: 6px; }
/* ⚠ 短条带（预览折算后 < 46px）走紧凑档：单行 20px、按钮压到 16px。
   为什么需要它：非紧凑档单行是 18+8=26px，条带只要矮于 26px 就会**上下**溢出（横向已由上面夹住）。
   紧凑档把下限推到 20px。**再矮的条带（预览折算 < 20px，约等于真实 < 68px）本组件放弃内嵌** ——
   如实写在验收报告里，不假装解决（这类条带连「⇄ 上屏 · 更换/移除」的占位提示也放不下）。 */
.pvc-fixbar.compact { padding: 2px 5px; gap: 3px; }
.pvc-fixbar.compact .pvc-fixbtn { height: 16px; min-width: 16px; padding: 0 5px; }
.pvc-fixtag { font-size: 10px; color: #9FD8FF; white-space: nowrap; }
.pvc-fixz { font-size: 11px; color: #fff; min-width: 32px; text-align: center; font-variant-numeric: tabular-nums; }
.pvc-fixbtn { height: 18px; min-width: 18px; padding: 0 6px; border: 1px solid rgba(120, 200, 255, .38);
  border-radius: var(--radius-pill); background: rgba(0, 229, 255, .10); color: #E8F6FF;
  font-size: 11px; font-family: inherit; line-height: 1; cursor: pointer; }
.pvc-fixbtn:hover { background: rgba(0, 229, 255, .26); border-color: var(--primary); }
/* r50：到顶/到底的缩放按钮显示为不可点。只改透明度与光标，**不动尺寸**
   ⇒ r49 的两条几何闸门（条带内不溢出 / 逐行首控件顶边极差 ≤2px）不会因此抖动。 */
.pvc-fixbtn:disabled { opacity: .38; cursor: not-allowed; }
.pvc-fixbtn:disabled:hover { background: rgba(0, 229, 255, .10); border-color: rgba(120, 200, 255, .38); }
.pvc-fixhint { font-size: 10px; color: var(--text-mute); white-space: nowrap; }
.pvc-layer.pvc-slot.filled { border-style: solid; }
.pvc-slot-bar { position: absolute; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, var(--primary), var(--accent)); opacity: .55; }

/* ---------------- 需求⑧-⑤：工作台「帮助」抽屉（整板块说明书） ----------------
   从右侧滑出、不占左侧菜单 —— 用户明确要求"工作台画面不乱"，所以说明不铺在页面上，
   收进一个随时可开可关的抽屉；只有顶栏多一个「帮助」按钮。
   ⚠ 与 .modal 分开：抽屉是辅助面板，不做焦点劫持，开合不影响当前页面状态。 */
.help-scrim { position: fixed; inset: 0; z-index: 190; background: rgba(3, 7, 18, .55); backdrop-filter: blur(3px); opacity: 0; visibility: hidden; transition: opacity .25s var(--ease); }
.help-scrim.on { opacity: 1; visibility: visible; }
.help-drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(560px, 92vw); z-index: 200; display: flex; flex-direction: column; background: var(--panel); border-left: 1px solid var(--border); box-shadow: -18px 0 50px rgba(0, 0, 0, .38); transform: translateX(103%); transition: transform .28s var(--ease); }
.help-drawer.on { transform: none; }
.help-drawer .help-head { display: flex; align-items: flex-start; gap: 10px; padding: 16px 18px; border-bottom: 1px solid var(--border); }
.help-drawer .help-head .h2 { font-size: 17px; }
.help-drawer .help-body { flex: 1; overflow-y: auto; padding: 14px 18px 28px; }
.help-sec { border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; margin-bottom: 12px; background: var(--panel-2, rgba(10, 20, 34, .42)); }
.help-sec h3 { margin: 0 0 4px; font-size: 14.5px; font-weight: 800; }
.help-sec .help-one { font-size: 12.5px; color: var(--text-dim); line-height: 1.7; }
.help-sec ol { margin: 8px 0 0; padding-left: 20px; }
.help-sec ol li { margin: 5px 0; font-size: 12.5px; line-height: 1.7; color: var(--text-dim); }
.help-shot { display: block; width: 100%; margin-top: 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); cursor: zoom-in; }
.help-shot-miss { margin-top: 10px; padding: 18px 12px; text-align: center; font-size: 12px; color: var(--text-mute); border: 1px dashed var(--border); border-radius: var(--radius-sm); }

/* ---------------- 铁律⑬：日期一律「点选」，禁止手输（2026-09-28 · 全平台统一） ----------------
   用户原话：「年月日选择……点开直接就出现，用户只需要点击选择就行了，不要让输入」。
   实现体在 public/js/datepicker.js（DP.field / DP.toggle / DP.set）。
   ★ 这里**不许**再出现任何 input[type=date] 的样式 —— 原生日期框已全站禁用（回归有断言）。
   ★ 高度一律走 --ctl-h 令牌（E-UI-015），与同排的 select/button 天然齐平：这就是
     用户截图里「年月日后面的全部不协调」的机械根因（当时那行用的是裸 .flex + 不存在的
     items-end 类 ⇒ 按钮被 align-items:stretch 拉满整行高）。 */
.dp-field {
  display: inline-flex; align-items: center; justify-content: space-between; gap: 8px;
  height: var(--ctl-h); padding: 0 10px 0 13px; line-height: 1;
  text-align: left; cursor: pointer; white-space: nowrap;
}
.dp-field .dp-txt { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
.dp-field .dp-ico { flex: none; font-size: 10px; line-height: 1; opacity: .6; transition: transform .2s var(--ease); }
.dp-field.is-open { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(0,229,255,.12); }
.dp-field.is-open .dp-ico { transform: rotate(180deg); }
.dp-field.is-empty .dp-txt { color: var(--text-mute); }

/* ---------------- 「▾ 三角」的盒子必须装得下它的墨迹（2026-09-29 · r44 全平台实测） ----------------
   用户原话：「还有漏掉的，字都出框了，全平台检查，这些比例失调的提示或者标语、文字等等都优化一下」。
   实测（scripts/diag-text-overflow.js，同时命中侧栏分组头与日期选择器两处）：
     `▾`（U+25BE）在 font-size:10px 下，**墨迹是 9.2×14** —— 三角字形来自回退字体，
     而 `line-height:1` 只给 10px 行盒 ⇒ 上下各捅出 2px。
   两处（`.nav-group-arrow` / `.dp-field .dp-ico`）是同一条根因（"行盒比墨迹矮"），
   所以**一条规则同时收口**，不给"下次新增一个 ▾ 又忘了"留口子。
   盒子给 14×14：装得下实测墨迹，且与 `.nav-ico{width:18px}` 同族的"图标占位"口径一致。 */
.nav-group-arrow,
.dp-field .dp-ico {
  flex: 0 0 auto; width: 14px; height: 14px;
  display: inline-flex; align-items: center; justify-content: center;
}
/* 放在横向控件行（.ctl-row）里时不占满整行：与旁边的按钮排成一行 */
.ctl-row .dp-field { width: auto; min-width: 148px; }

/* ---- 日历弹层 ---- */
.dp-pop {
  position: fixed; z-index: 320; width: 272px; padding: 10px;
  background: var(--bg-2); border: 1px solid var(--border-strong);
  border-radius: var(--radius); box-shadow: var(--shadow);
  animation: dp-in .14s var(--ease);
}
@keyframes dp-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.dp-head { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
.dp-nav {
  flex: none; width: 30px; height: 30px; border: 1px solid var(--border); border-radius: var(--radius-xs);
  background: transparent; color: var(--text); font-family: inherit; font-size: 15px; line-height: 1;
  cursor: pointer; transition: all .18s var(--ease);
}
.dp-nav:hover { border-color: var(--primary); color: var(--primary); }
.dp-title {
  flex: 1; min-width: 0; height: 30px; border: 1px solid transparent; border-radius: var(--radius-xs);
  background: transparent; color: var(--text); font-family: inherit; font-size: 13px; font-weight: 800;
  cursor: pointer; transition: all .18s var(--ease);
}
.dp-title:hover { border-color: var(--border-strong); }
.dp-wd { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; margin-bottom: 2px; }
.dp-wd span { text-align: center; font-size: 11px; color: var(--text-mute); line-height: 20px; }
.dp-grid { display: grid; gap: 2px; }
.dp-days { grid-template-columns: repeat(7, 1fr); }
.dp-months, .dp-years { grid-template-columns: repeat(3, 1fr); }
.dp-cell {
  height: 32px; border: 1px solid transparent; border-radius: var(--radius-xs);
  background: transparent; color: var(--text); font-family: inherit; font-size: 12.5px;
  font-variant-numeric: tabular-nums; cursor: pointer; transition: all .16s var(--ease);
}
.dp-cell:hover:not(:disabled) { background: rgba(120,200,255,.12); border-color: var(--border-strong); }
.dp-cell.is-sel { background: linear-gradient(135deg, var(--primary), var(--accent)); color: #04121C; font-weight: 800; }
.dp-cell.is-today { border-color: var(--primary); color: var(--primary); }
.dp-cell.is-today.is-sel { color: #04121C; }
.dp-cell.is-dis { opacity: .28; cursor: not-allowed; }
.dp-cell.is-blank { visibility: hidden; cursor: default; }
.dp-foot { display: flex; gap: 8px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border); }
.dp-quick {
  flex: 1; height: 30px; border: 1px solid var(--border); border-radius: var(--radius-xs);
  background: var(--panel-2); color: var(--text); font-family: inherit; font-size: 12.5px;
  cursor: pointer; transition: all .18s var(--ease);
}
.dp-quick:hover { border-color: var(--primary); color: var(--primary); }
/* 手机端：手指要够得着 ⇒ 日期格放大到 40px，弹层已由 JS 居中（见 place()） */
@media (max-width: 860px) {
  .dp-cell { height: 40px; font-size: 13.5px; }
  .dp-nav, .dp-title { height: 36px; }
  .dp-foot .dp-quick { height: 36px; }
}

/* ============================================================
 * E-UI-019（2026-10-01 · 移动端闸门在真实实例上抓出）
 *
 * 混剪预览画布上的两个悬浮小按钮，桌面上是 18×18（`.pvc-slot-x` ✕ 移除）
 * 与 34×18（`.pvc-slot-act` 调整）—— **鼠标点得到，手指够不着**。
 * 它们长在 `.pvc-slot`（固定素材条带。实测 242×78，桌面/手机同宽）的右上角，
 * 而 `.pvc-slot` 自己点下去是"选中/拖动"，**不是移除** ⇒ 想移除就只能戳那个 18×18。
 *
 * ⚠ 两者是**并排浮标**，放大时必须一起挪（这是本条最容易出错的地方）：
 *   ✕ 原本 `right:4` + 宽 18 ⇒ 占据 right 4~22；
 *   撑到 34 后占据 right 4~38 ⇒ **「调整」必须从 right:26 推到 right:42**，
 *   否则 ✕ 的新热区会直接压住「调整」—— 用户想点"移除"，打中的却是"调整"。
 *   面积算得对、位置算错，比不改还糟（这正是"改热区"必须回读几何的原因）。
 *
 * ⚠ 只改移动档（≤860 = var(--bp-mobile)）。桌面 >860 保持 18×18/34×18 原样，
 *   已用探针取过改前基线（✕ 18×18 距容器右 5 · 调整 34×18 距容器右 27）做对照。
 * ============================================================ */
@media (max-width: 860px) {
  .pvc-slot-x { width: 34px; height: 34px; font-size: 14px; }
  .pvc-slot-act { right: 42px; height: 34px; min-width: 38px; font-size: 12px; }
}
