/*=============================================================================
  ConfigCenter — 样式
  布局: 左侧固定侧边栏 + 右侧配置区
=============================================================================*/
:root {
  --sb-bg: #1e2430;
  --sb-bg-2: #262e3c;
  --sb-fg: #c8d0dc;
  --sb-fg-dim: #7d8798;
  --sb-active: #3b82f6;

  --bg: #f4f6fa;
  --panel: #ffffff;
  --border: #e3e7ee;
  --border-2: #d3d9e3;
  --fg: #1f2530;
  --fg-dim: #6b7484;
  --fg-dim2: #9aa3b2;

  --blue: #2563eb;
  --blue-dark: #1d4ed8;
  --blue-soft: #eff5ff;
  --green: #16a34a;
  --red: #dc2626;
  --amber: #d97706;

  --radius: 8px;
  --shadow: 0 1px 3px rgba(20, 28, 44, .07), 0 1px 2px rgba(20, 28, 44, .04);
  --mono: "Cascadia Mono", "Consolas", "Courier New", monospace;
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  font-family: "Microsoft YaHei", "微软雅黑", -apple-system, "Segoe UI", sans-serif;
  font-size: 14px;
  color: var(--fg);
  background: var(--bg);
}

button, input, select, textarea { font-family: inherit; font-size: inherit; }

/*=============================================================================
  登录页
=============================================================================*/
.login-wrap {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(140deg, #1b2333 0%, #223047 55%, #1a2a45 100%);
}
.login-card {
  width: 380px;
  background: #fff;
  border-radius: 14px;
  padding: 34px 32px 28px;
  box-shadow: 0 18px 50px rgba(10, 18, 32, .35);
}
.login-logo {
  width: 46px; height: 46px; border-radius: 12px;
  background: linear-gradient(135deg, #3b82f6, #1d4ed8);
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-size: 22px; font-weight: 700; margin-bottom: 16px;
}
.login-card h1 { font-size: 20px; margin: 0 0 6px; }
.login-card .sub { color: var(--fg-dim); font-size: 13px; margin-bottom: 24px; }
.login-card .field { margin-bottom: 15px; }
.login-card label { display: block; font-size: 13px; color: var(--fg-dim); margin-bottom: 6px; }
.login-card input {
  width: 100%; height: 40px; padding: 0 12px;
  border: 1px solid var(--border-2); border-radius: var(--radius);
  outline: none; transition: border-color .15s, box-shadow .15s;
}
.login-card input:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); }
.login-card button {
  width: 100%; height: 42px; margin-top: 8px;
  background: var(--blue); color: #fff; border: 0; border-radius: var(--radius);
  font-size: 15px; font-weight: 600; cursor: pointer; transition: background .15s;
}
.login-card button:hover { background: var(--blue-dark); }
.login-card button:disabled { opacity: .6; cursor: default; }
.login-err {
  margin-top: 14px; padding: 9px 12px; border-radius: 6px;
  background: #fef2f2; color: var(--red); font-size: 13px; display: none;
}
.login-err.on { display: block; }
.login-foot { margin-top: 20px; font-size: 12px; color: var(--fg-dim2); text-align: center; line-height: 1.7; }

/* 身份切换 */
.role-switch {
  display: flex; gap: 6px; background: #f1f4f9; border-radius: 9px;
  padding: 4px; margin-bottom: 20px;
}
.role-btn {
  flex: 1; text-align: center; padding: 9px 0; border-radius: 7px;
  font-size: 13.5px; color: var(--fg-dim); cursor: pointer;
  transition: background .15s, color .15s;
}
.role-btn:hover { color: var(--fg); }
.role-btn.active { background: #fff; color: var(--blue); font-weight: 600; box-shadow: 0 1px 3px rgba(20,28,44,.1); }

/*=============================================================================
  主框架
=============================================================================*/
.shell { display: flex; height: 100vh; overflow: hidden; }

/* ---- 侧边栏 ---- */
.sidebar {
  width: 236px; flex: 0 0 236px;
  background: var(--sb-bg);
  display: flex; flex-direction: column;
  color: var(--sb-fg);
}
.sb-brand {
  padding: 18px 18px 16px;
  display: flex; align-items: center; gap: 10px;
  border-bottom: 1px solid rgba(255,255,255,.07);
}
.sb-brand .mark {
  width: 30px; height: 30px; border-radius: 8px; flex: 0 0 30px;
  background: linear-gradient(135deg, #3b82f6, #1d4ed8);
  box-shadow: 0 2px 8px rgba(59,130,246,.35);
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-weight: 700; font-size: 15px;
}
.sb-brand .txt b { display: block; font-size: 14px; color: #fff; font-weight: 600; }
.sb-brand .txt span { font-size: 11px; color: var(--sb-fg-dim); }

.sb-scroll { flex: 1; overflow-y: auto; padding: 12px 0 16px; }
.sb-scroll::-webkit-scrollbar { width: 6px; }
.sb-scroll::-webkit-scrollbar-thumb { background: #3a4354; border-radius: 3px; }

/* 十一期: 侧栏分组标题更"分组"一点(小字 + 大写间距 + 上一组一条极细分隔线) */
.sb-label {
  padding: 14px 18px 6px; font-size: 10.5px; letter-spacing: .8px;
  color: var(--sb-fg-dim); text-transform: uppercase;
  display: flex; justify-content: space-between; align-items: center;
}
.sb-label:not(:first-child) { margin-top: 8px; border-top: 1px solid rgba(255,255,255,.06); }
.sb-item {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 18px; cursor: pointer; font-size: 13px;
  border-left: 3px solid transparent;
  transition: background .14s, color .14s;
}
.sb-item:hover { background: var(--sb-bg-2); color: #fff; }
/* 选中态: 左侧蓝条 + 由左向右渐隐的蓝底, 比纯色块更有方向感 */
.sb-item.active {
  background: linear-gradient(90deg, rgba(59,130,246,.22), rgba(59,130,246,.10));
  border-left-color: var(--sb-active); color: #fff; font-weight: 600;
}
.sb-item .dot {
  width: 8px; height: 8px; border-radius: 50%; flex: 0 0 8px;
  background: var(--sb-fg-dim);
}
.sb-item.active .dot { background: var(--sb-active); }
.sb-item .nm { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 右侧计数做成小胶囊, 与菜单项文字分层 */
.sb-item .rv {
  font-size: 11px; color: var(--sb-fg); background: rgba(255,255,255,.09);
  border-radius: 999px; padding: 1px 7px;
}
.sb-item.active .rv { background: rgba(255,255,255,.2); color: #fff; }
.sb-item .off { font-size: 10px; color: #f59e0b; }

.sb-sub { display: flex; flex-direction: column; }
.sb-sub .sb-item { padding-left: 34px; font-size: 12.5px; }
.sb-sub .sb-item .dot { width: 6px; height: 6px; flex-basis: 6px; }

.sb-add {
  margin: 6px 14px; padding: 8px; text-align: center;
  border: 1px dashed #3f4a5e; border-radius: 6px; cursor: pointer;
  font-size: 12.5px; color: var(--sb-fg-dim);
}
.sb-add:hover { border-color: var(--sb-active); color: #fff; }

/* 左下角用户卡已移除: 账号入口统一在右上角用户区(.userbox) */

/* ---- 右侧主体 ---- */
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; }

.topbar {
  background: var(--panel); border-bottom: 1px solid var(--border);
  padding: 0 24px; height: 62px; flex: 0 0 62px;
  display: flex; align-items: center; gap: 14px;
}
.topbar .ttl { min-width: 0; flex: 1; }
.topbar .ttl h2 { margin: 0; font-size: 16px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topbar .ttl p { margin: 3px 0 0; font-size: 12px; color: var(--fg-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topbar .acts { display: flex; align-items: center; gap: 8px; }

/*=============================================================================
  八期: 页面选项卡栏 / 页面工具栏 / 右上角用户菜单 / 软件列表左右两栏
=============================================================================*/

/* ---- 右上角用户区: 头像 + 名字 + ▾, hover 出下拉 ---- */
.userbox { flex: 0 0 auto; }
.ubinner {
  position: relative; display: flex; align-items: center; gap: 8px;
  height: 36px; padding: 0 11px 0 6px; border-radius: 18px;
  cursor: pointer; user-select: none; border: 1px solid transparent;
}
.ubinner:hover { background: #f4f6fa; border-color: var(--border); }
.ubav {
  width: 26px; height: 26px; border-radius: 50%; flex: 0 0 26px; overflow: hidden;
  background: var(--blue); color: #fff; font-size: 12.5px; font-weight: 600;
  display: flex; align-items: center; justify-content: center;
}
.ubav img { width: 100%; height: 100%; object-fit: cover; }
.ubnm { font-size: 13px; max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ubcar { font-size: 10px; color: var(--fg-dim2); }
.ubmenu {
  position: absolute; right: 0; top: calc(100% + 6px); min-width: 150px; z-index: 60;
  background: #fff; border: 1px solid var(--border); border-radius: 10px;
  box-shadow: 0 10px 26px rgba(16,24,40,.14); padding: 6px;
  display: none;
}
.ubinner:hover .ubmenu, .ubmenu.show { display: block; }
.ubmi {
  padding: 8px 12px; font-size: 13px; border-radius: 7px; color: var(--fg); cursor: pointer;
  white-space: nowrap;
}
.ubmi:hover { background: var(--blue-soft); color: var(--blue); }
/* 批次50 需求①-a: 悬停死区修补 —— 菜单被推离触发器 5px(.ubinner 有 1px 透明边框, 实测
   触发器底边 y=48.5、菜单顶边 y=53.5), 指针**慢速逐像素**下移跨过这 5px 时 :hover(.ubinner)
   与 :hover(.ubmenu) 同时为假 → 菜单立刻 display:none, 且此后指针再往下也命中不到它
   (表现为「鼠标一往下移菜单就消失」; 快速甩过去只发一次 mouseMoved 反而正常)。
   修法: 给**触发器**加一条 10px 高的透明「桥」(≥8px, 覆盖 5px 死区)。桥必须挂在触发器上,
   挂到菜单上没用 —— 菜单自己一隐藏就再没有命中区域了。伪元素属 .ubinner 的盒子、参与命中测试,
   因此指针跨过死区时 :hover(.ubinner) 始终成立。
   桥只占触发器下方 10px 的条带、z-index:auto, 而 .ubmenu 是 z-index:60 的定位元素,
   二者重叠的那 4px 仍归菜单(已用 elementFromPoint 实测确认, 见 _review-b50/impl-B-profile.md)。 */
.ubinner::after {
  content: ''; position: absolute; left: 0; right: 0; top: 100%; height: 10px;
}
/* 批次50 需求①-b: 点过菜单项后菜单必须真的收起 —— 那一下指针还停在这一项上, 光摘掉 .show
   的话 CSS :hover 仍会让它保持显示。加这个标记后, 带标记期间即使命中 :hover 也不显示;
   标记由 app.js 在 mouseleave 时摘掉, 之后照旧可以悬停打开。 */
.ubinner.ub-done:hover .ubmenu { display: none; }

/* ---- 页面自己的工具栏(setHeader 的第三个参数渲染到这里) ---- */
.pagetoolbar {
  background: var(--bg); border-bottom: 1px solid var(--border);
  padding: 10px 24px; display: none; align-items: center; gap: 8px; flex: 0 0 auto;
  flex-wrap: wrap;
}

/* ---- 软件列表: 左分类 + 右列表 ---- */
.appsplit { display: flex; gap: 16px; align-items: flex-start; }
.appsplit > #appTableHost { flex: 1; min-width: 0; }
.catcol {
  flex: 0 0 250px; width: 250px; background: var(--panel);
  border: 1px solid var(--border); border-radius: 10px; padding: 0;
  box-shadow: 0 1px 2px rgba(16,24,40,.04);
  position: sticky; top: 0; max-height: calc(100vh - 200px);
  display: flex; flex-direction: column; overflow: hidden;
}
/* 九期: 分类标题行与右栏 .card > header 用同一套规则(字号/字重/颜色/高/内边距),
   这样左右两个标题行高度一致、文字基线也齐平 */
.cathead {
  padding: 13px 18px; border-bottom: 1px solid var(--border);
  display: flex; align-items: center; gap: 10px; flex: 0 0 auto;
  background: linear-gradient(#fcfdff, #f6f8fc);
}
.cathead h3 { margin: 0; font-size: 14px; font-weight: 600; flex: 1; }
.catlist { padding: 8px 6px 10px; overflow-y: auto; flex: 1; min-height: 0; }
/* 分类多了以后左栏会滚动: 细滚动条, 和右栏 .content 的观感统一 */
.catlist::-webkit-scrollbar { width: 8px; }
.catlist::-webkit-scrollbar-thumb { background: #c8cfdb; border-radius: 4px; border: 2px solid var(--panel); }
.catitem {
  display: flex; align-items: center; gap: 8px; padding: 9px 10px;
  border-radius: 8px; font-size: 13px; line-height: 1.35; cursor: pointer; color: var(--fg-dim);
  position: relative; margin-bottom: 2px;
  transition: background .14s, color .14s, box-shadow .14s;
}
.catitem:hover { background: #f3f6fb; color: var(--fg); }
/* 选中态: 淡蓝底 + 左侧 3px 蓝条(用 inset 阴影画, 不占布局、也不动伪元素) */
.catitem.active {
  background: var(--blue-soft); color: var(--blue); font-weight: 600;
  box-shadow: inset 3px 0 0 var(--blue);
}
/* 图标固定 18px 宽并居中: emoji 宽度不一, 固定后同层文字左边缘始终对齐 */
.catitem .ico { font-size: 14px; flex: 0 0 18px; width: 18px; text-align: center; }
.catitem .nm { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 批次50 需求④: 分类统计(卡密数胶囊)下线时**保留**本组规则 —— 它不只为「分类列表」服务:
   实探(见 _review-b50/impl-B-probe-out.json 的 catSide 段)在用的还有
   · 卡密分类页左栏  biz.js:4992  `<span class="ct" title="卡密数（含子分类）">`
   · 卡密模板页左栏  biz.js:9780  同上
   这两处渲染的 `.catitem .ct` 与下面两条规则一一对应, 删掉会把胶囊样式一起打掉。
   分类管理页(`.ccat-split` 段)那处专属覆盖 `.ccat-split .catitem.lv1.active .ct`
   已随批次50 的统计下线删除 —— 它的唯一含义就是「活动行的胶囊反白」, 现在没有使用者。 */
.catitem .ct {
  font-size: 11px; color: var(--fg-dim2); background: #eef1f6;
  border-radius: 9px; padding: 1px 7px; min-width: 26px; text-align: center;
}
.catitem.active .ct { background: #fff; color: var(--blue); }
/* 拖拽排序的反馈: 被拖项半透明 + 落点一条蓝线(用伪元素画线, 不动 DOM 结构) */
.catitem.dragging { opacity: .45; }
.catitem.dropbefore::before, .catitem.dropafter::after {
  content: ''; position: absolute; left: 6px; right: 6px; height: 2px;
  border-radius: 2px; background: var(--blue);
}
.catitem.dropbefore::before { top: -1px; }
.catitem.dropafter::after { bottom: -1px; }

/* ---- 十一期: 分类树的折叠箭头加大 + 子分类引导线 ----
   箭头: 固定 16px 宽占位(有子分类的显示 ▶/▼, 没有的留空但宽度不变, 同层图标与文字始终对齐);
   字号从 10px 提到 14px、字符从 ▸/▾ 换成更实的 ▶/▼, 实测 rect 由 12x10 变成 16x14。 */
.catitem .tri {
  flex: 0 0 auto; width: 16px; margin-left: -2px;
  font-size: 14px; line-height: 1; text-align: center;
  color: var(--fg-dim2); user-select: none;
}
.catitem .tri[data-tri="1"] { cursor: pointer; }
.catitem .tri[data-tri="1"]:hover { color: var(--blue); }
/* 引导线: 子分类行左侧一条竖线 + 一个横向小拐角, 把子分类和上级分类连起来(不用图片);
   竖线从上一行(上级分类)底部延伸下来, 同一父类下最后一个子分类只画到中间(└─)。 */
.catitem .rail { position: absolute; left: 0; top: 0; bottom: 0; width: 26px; pointer-events: none; }
.catitem .rail::before {
  content: ''; position: absolute; left: 18px; top: -8px; bottom: 0;
  width: 1px; background: #dbe3ef;
}
.catitem .rail.last::before { bottom: 50%; }
.catitem .rail::after {
  content: ''; position: absolute; left: 18px; top: 50%; margin-top: -0.5px;
  width: 9px; height: 1px; background: #dbe3ef; border-radius: 1px;
}
/* 选中行/悬停行里把线加深一点, 免得被浅蓝底吃掉 */
.catitem.active .rail::before, .catitem.active .rail::after,
.catitem:hover .rail::before, .catitem:hover .rail::after { background: #b9c8e4; }
/* 层级感: 一级分类用正常前景色, 子分类灰一档; 选中态再统一回到蓝
   (两条 .active 显式写出来, 保证不被上面的层级规则盖掉) */
.catitem.lv0 { color: var(--fg); }
.catitem.lv1 { color: var(--fg); }
.catitem.lv2 { padding-left: 32px; font-size: 12.5px; color: var(--fg-dim); }
.catitem.lv2 .ico { font-size: 13px; }
.catitem.lv1.active, .catitem.lv2.active { color: var(--blue); }
/* 「全部软件」和下面的大分类之间留一道分组间隔 */
.catitem.lv0 + .catitem.lv1 { margin-top: 6px; }
.catlist .empty { padding: 22px 8px; font-size: 12.5px; line-height: 1.6; }

/* ---- 分类选项卡的右键菜单 ---- */
.catmenu {
  position: fixed; z-index: 300; min-width: 132px; padding: 5px;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(10,18,32,.16);
}
.catmenu .mi {
  padding: 7px 12px; border-radius: 6px; font-size: 13px;
  color: var(--fg); cursor: pointer; white-space: nowrap;
}
.catmenu .mi:hover { background: var(--blue-soft); color: var(--blue); }
.catmenu .mi.danger { color: var(--red); }
.catmenu .mi.danger:hover { background: #fef2f2; color: var(--red); }

.tabs {
  background: var(--panel); border-bottom: 1px solid var(--border);
  padding: 0 24px; display: flex; gap: 2px; flex: 0 0 auto;
}
.tab {
  padding: 11px 15px; font-size: 13.5px; color: var(--fg-dim); cursor: pointer;
  border-bottom: 2px solid transparent; transition: color .12s, border-color .12s;
}
.tab:hover { color: var(--fg); }
.tab.active { color: var(--blue); border-bottom-color: var(--blue); font-weight: 600; }
.tab .cnt {
  display: inline-block; margin-left: 5px; padding: 0 5px; border-radius: 8px;
  background: var(--border); color: var(--fg-dim); font-size: 11px;
}

/* ---- 软件切换 tab 行 ---- */
.apptabs {
  background: #eef1f6; border-bottom: 1px solid var(--border-2);
  padding: 8px 24px 0; display: flex; gap: 6px; flex: 0 0 auto;
  overflow-x: auto; align-items: flex-end;
}
.apptabs:empty { display: none; }
.apptabs::-webkit-scrollbar { height: 5px; }
.apptabs::-webkit-scrollbar-thumb { background: #c3cad6; border-radius: 3px; }

.apptab {
  display: flex; align-items: center; gap: 7px;
  padding: 8px 13px; font-size: 13px; cursor: pointer;
  background: #e2e7ef; color: var(--fg-dim);
  border: 1px solid transparent; border-bottom: 0;
  border-radius: 8px 8px 0 0; white-space: nowrap;
  max-width: 260px; transition: background .12s, color .12s;
}
.apptab:hover { background: #dde3ec; color: var(--fg); }
.apptab.active {
  background: var(--panel); color: var(--fg); font-weight: 600;
  border-color: var(--border); box-shadow: 0 -1px 3px rgba(20,28,44,.05);
}
.apptab .nm { overflow: hidden; text-overflow: ellipsis; }
.apptab .rev {
  font-size: 10.5px; font-weight: 400; padding: 1px 5px;
  border-radius: 8px; background: #cfd6e0; color: #55607a;
}
.apptab.active .rev { background: var(--blue-soft); color: var(--blue); }
.apptab .off { font-size: 10px; color: var(--amber); }
.apptab.add {
  background: transparent; border: 1px dashed #b9c1cf; color: var(--fg-dim);
  border-radius: 8px; margin-bottom: 4px; padding: 7px 12px;
}
.apptab.add:hover { border-color: var(--blue); color: var(--blue); background: #fff; }

/* ---- 上传 ini 弹窗 ---- */
.dropzone {
  border: 2px dashed var(--border-2); border-radius: 10px;
  padding: 30px 20px; text-align: center; cursor: pointer;
  background: #fafbfd; transition: border-color .15s, background .15s;
}
.dropzone:hover, .dropzone.over { border-color: var(--blue); background: var(--blue-soft); }
.dropzone .big { font-size: 30px; opacity: .5; }
.dropzone .t { font-size: 14px; margin: 8px 0 4px; font-weight: 500; }
.dropzone .s { font-size: 12px; color: var(--fg-dim2); }

.detect-summary { display: flex; gap: 10px; flex-wrap: wrap; margin: 14px 0; }
.detect-summary .item {
  background: var(--blue-soft); border: 1px solid #c7ddff; border-radius: 6px;
  padding: 7px 12px; font-size: 12.5px; color: #1e40af;
}
.detect-summary .item b { font-size: 15px; }

.fieldlist { max-height: 280px; overflow-y: auto; border: 1px solid var(--border); border-radius: 6px; }
.fieldlist table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.fieldlist th {
  position: sticky; top: 0; z-index: 2; background: #f7f9fc; text-align: left;
  padding: 7px 10px; font-size: 11.5px; color: var(--fg-dim);
  border-bottom: 1px solid var(--border); font-weight: 600;
}
.fieldlist td { padding: 3px 6px; border-bottom: 1px solid #f0f3f8; }
.fieldlist tr:last-child td { border-bottom: 0; }
.fieldlist input, .fieldlist select {
  width: 100%; border: 1px solid transparent; border-radius: 4px;
  padding: 5px 6px; background: transparent; font-size: 12.5px; outline: none;
}
.fieldlist input:hover, .fieldlist select:hover { background: #fafbfd; border-color: var(--border); }
.fieldlist input:focus, .fieldlist select:focus { background: #fff; border-color: var(--blue); }
.fieldlist .secrow td {
  background: #f7f9fc; font-weight: 600; font-size: 12px; color: var(--fg-dim);
  padding: 6px 10px;
}

/* 批次30: 修「视口底部/右侧露出 body 底色」的版式缺陷。
   .content 是唯一铺满右侧主体可视区域的容器(卡片只占其中一部分), 以前它 background 全透明,
   于是卡片下方(1600x900 下 195px)与右侧内边距(24px)直接露出 body 的 --bg 灰底。
   这里给 .content 补上与卡片一致的底色(--panel), 使可视区域不再露出 body 底色;
   衬底铺满的是 .content 的整个 border box(含 padding), 所以不需要为「铺满」去压缩或拉伸 .card。
   不采用 flex 拉伸卡片: 卡片高于容器时(oplogs 2964px / tpl 759px)会被压扁, 破坏卡片内部滚动区。
   min-height:0 显式声明矮视口下本 flex 项必须能收缩(不依赖 overflow:auto 推出的自动最小尺寸),
   保证 .content 在任何视口高度下都是自洽的可滚动容器, 内容不会被裁掉且滚不到。 */
.content { flex: 1; overflow-y: auto; padding: 22px 24px 60px; background: var(--panel); min-height: 0; }
.content::-webkit-scrollbar { width: 10px; }
.content::-webkit-scrollbar-thumb { background: #c8cfdb; border-radius: 5px; border: 3px solid var(--panel); }

/*=============================================================================
  子用户后台 / 买家中心 (无侧边栏的独立页面)
=============================================================================*/
.portal-body { background: var(--bg); min-height: 100vh; }
.portal-shell { max-width: 1200px; margin: 0 auto; padding: 22px 24px 60px; }

.portal-head {
  display: flex; align-items: center; gap: 13px; margin-bottom: 20px;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 14px 18px; box-shadow: var(--shadow);
}
.portal-head .mark {
  width: 38px; height: 38px; border-radius: 10px; flex: 0 0 38px;
  background: linear-gradient(135deg, #3b82f6, #1d4ed8);
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-weight: 700; font-size: 17px;
}
.portal-head .who { flex: 1; min-width: 0; }
.portal-head .who b { display: block; font-size: 15px; }
.portal-head .who span { font-size: 12.5px; color: var(--fg-dim); }
.portal-head .badge {
  padding: 5px 12px; border-radius: 20px; background: var(--blue-soft);
  color: var(--blue); font-size: 12.5px; font-weight: 600;
}

.money-big { font-size: 26px; font-weight: 700; letter-spacing: -.5px; }
.pos { color: var(--green); }
.neg { color: var(--red); }

/* ---- 登录页(portals) ---- */
.portal-login {
  min-height: 100vh; display: flex; align-items: center; justify-content: center;
  padding: 24px;
}
.portal-login .login-card { width: 400px; }
.portal-login .switcher {
  margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--border);
  text-align: center; font-size: 12.5px; color: var(--fg-dim2);
}
.portal-login .switcher a { color: var(--blue); text-decoration: none; margin: 0 6px; }
.portal-login .switcher a:hover { text-decoration: underline; }

/* ---- portal 内的 tab ---- */
.portal-tabs {
  display: flex; gap: 2px; background: var(--panel);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0 14px; margin-bottom: 18px; box-shadow: var(--shadow);
}
.portal-tabs .tab {
  padding: 12px 16px; font-size: 13.5px; color: var(--fg-dim);
  cursor: pointer; border-bottom: 2px solid transparent;
}
.portal-tabs .tab:hover { color: var(--fg); }
.portal-tabs .tab.active { color: var(--blue); border-bottom-color: var(--blue); font-weight: 600; }

/*=============================================================================
  图表 / 排行榜
=============================================================================*/
.chart { width: 100%; height: auto; display: block; }
.chart-empty { padding: 34px 0; text-align: center; color: var(--fg-dim2); font-size: 13px; }
.chart-legend {
  display: flex; gap: 18px; flex-wrap: wrap; margin-top: 8px;
  font-size: 12.5px; color: var(--fg-dim);
}
.chart-legend i {
  display: inline-block; width: 9px; height: 9px; border-radius: 2px;
  margin-right: 5px; vertical-align: 0;
}

.rank-list { display: flex; flex-direction: column; gap: 9px; }
.rank-row { display: flex; align-items: center; gap: 9px; font-size: 13px; }
.rank-no {
  width: 20px; height: 20px; flex: 0 0 20px; border-radius: 50%;
  background: var(--border); color: var(--fg-dim); font-size: 11.5px;
  display: flex; align-items: center; justify-content: center; font-weight: 600;
}
.rank-no.gold { background: #fef3c7; color: #b45309; }
.rank-no.silver { background: #e5e7eb; color: #4b5563; }
.rank-no.bronze { background: #fde8d7; color: #b45309; }
.rank-name {
  width: 130px; flex: 0 0 130px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap;
}
.rank-bar { flex: 1; height: 8px; background: #f0f3f8; border-radius: 4px; overflow: hidden; min-width: 50px; }
.rank-bar i { display: block; height: 100%; border-radius: 4px; transition: width .35s; }
.rank-val { width: 84px; flex: 0 0 84px; text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }
.rank-sub { width: 96px; flex: 0 0 96px; text-align: right; color: var(--fg-dim2); font-size: 12px; }

.donut-wrap { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; }
.donut { flex: 0 0 auto; }
.donut-legend { flex: 1; min-width: 150px; display: flex; flex-direction: column; gap: 7px; }
.dl-row { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.dl-row i { width: 9px; height: 9px; border-radius: 2px; flex: 0 0 9px; }
.dl-name { flex: 1; color: var(--fg-dim); }
.dl-row b { font-variant-numeric: tabular-nums; }

.prog {
  display: inline-block; height: 7px; background: #f0f3f8; border-radius: 4px;
  overflow: hidden; vertical-align: middle; margin-right: 6px;
}
.prog i { display: block; height: 100%; border-radius: 4px; }

/* 待办提醒条 */
.todo-list { display: flex; flex-wrap: wrap; gap: 10px; }
.todo-item {
  display: flex; align-items: center; gap: 8px; padding: 9px 14px;
  border-radius: 8px; font-size: 13px; border: 1px solid;
}
.todo-item.warn { background: #fffbeb; border-color: #fde68a; color: #92400e; }
.todo-item.danger { background: #fef2f2; border-color: #fecaca; color: #991b1b; }
.todo-item.info { background: var(--blue-soft); border-color: #c7ddff; color: #1e40af; }
.todo-item b { font-size: 15px; }
.todo-item a { color: inherit; text-decoration: underline; cursor: pointer; }

/* 控制台排行榜分栏 */
.lb-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 18px; }

/* 子用户权限开关 */
.perm-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 8px; margin-top: 6px;
}
.perm-item {
  display: flex; gap: 9px; align-items: flex-start; cursor: pointer;
  padding: 9px 11px; border: 1px solid var(--border); border-radius: 7px;
  transition: border-color .12s, background .12s;
}
.perm-item:hover { border-color: var(--blue); background: var(--blue-soft); }
.perm-item input { margin-top: 2px; flex: 0 0 auto; }
.perm-txt { display: flex; flex-direction: column; min-width: 0; }
.perm-txt b { font-size: 13px; font-weight: 600; }
.perm-txt i { font-size: 11.5px; color: var(--fg-dim2); font-style: normal; margin-top: 2px; }

/* 简约趋势图底部四个数字 */
.trend-foot {
  display: flex; gap: 28px; flex-wrap: wrap;
  padding: 12px 4px 2px; border-top: 1px solid var(--border); margin-top: 4px;
}
.trend-foot span { display: flex; flex-direction: column; }
.trend-foot b { font-size: 17px; font-weight: 650; letter-spacing: -.3px; font-variant-numeric: tabular-nums; }
.trend-foot i { font-style: normal; font-size: 11.5px; color: var(--fg-dim2); margin-top: 2px; }

/* 分段切换(销售额 / 单数) */
.seg { display: flex; gap: 3px; background: #f1f4f9; border-radius: 7px; padding: 3px; }
.seg-item {
  padding: 4px 11px; border-radius: 5px; font-size: 12px; color: var(--fg-dim);
  cursor: pointer; transition: background .14s, color .14s; font-weight: 400;
}
.seg-item:hover { color: var(--fg); }
.seg-item.active { background: #fff; color: var(--blue); font-weight: 600; box-shadow: 0 1px 2px rgba(20,28,44,.1); }

/* ---- 软件切换栏 ----
   十一期: 右端那个 Alt+K「快速切换器」入口已按用户要求整块删除, 相关样式一并清掉, 不留死样式 */
.appbar { display: flex; align-items: flex-end; background: #eef1f6; border-bottom: 1px solid var(--border-2); flex: 0 0 auto; }
.appbar .apptabs { flex: 1; border-bottom: 0; background: transparent; min-width: 0; }

/* ---- 软件列表卡片 ---- */
.appgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
.appcard {
  background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
  padding: 16px 18px; cursor: pointer; box-shadow: var(--shadow);
  transition: border-color .15s, transform .12s, box-shadow .15s;
}
.appcard:hover { border-color: var(--blue); transform: translateY(-2px); box-shadow: 0 6px 18px rgba(20,28,44,.1); }
.appcard.off { opacity: .72; }
.ac-head { display: flex; align-items: center; gap: 11px; }
.ac-ico {
  width: 38px; height: 38px; flex: 0 0 38px; border-radius: 10px;
  background: linear-gradient(135deg, #3b82f6, #1d4ed8); color: #fff;
  display: flex; align-items: center; justify-content: center; font-size: 17px; font-weight: 700;
}
.ac-ttl { flex: 1; min-width: 0; }
.ac-ttl b { display: block; font-size: 14.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ac-ttl span { font-size: 11.5px; color: var(--fg-dim2); font-family: var(--mono); }
.ac-desc {
  margin-top: 11px; font-size: 12.5px; color: var(--fg-dim); line-height: 1.6;
  height: 40px; overflow: hidden;
}
.ac-stats { display: flex; gap: 16px; margin-top: 11px; font-size: 12px; color: var(--fg-dim); }
.ac-stats b { color: var(--fg); font-size: 14px; }
.ac-foot {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: 13px; padding-top: 11px; border-top: 1px solid var(--border);
  font-size: 11.5px;
}

/* ---- 个人资料 ---- */
.profile-head { display: flex; align-items: center; gap: 15px; margin-bottom: 16px; }
.avatar-big {
  width: 62px; height: 62px; flex: 0 0 62px; border-radius: 16px; overflow: hidden;
  background: linear-gradient(135deg, #3b82f6, #1d4ed8); color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: 26px; font-weight: 700;
}
.avatar-big img { width: 100%; height: 100%; object-fit: cover; }
.avatar-big .emo { font-size: 30px; line-height: 1; }
.ph-info b { display: block; font-size: 17px; }
.ph-info span { font-size: 12.5px; color: var(--fg-dim); }

.stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; }
.stat.sm { padding: 10px 12px; box-shadow: none; }
.stat.sm .k { font-size: 11.5px; }
.stat.sm .v { font-size: 16px; margin-top: 2px; }

.avatar-pick { display: flex; gap: 7px; flex-wrap: wrap; }
.av-opt {
  width: 34px; height: 34px; border-radius: 9px; border: 1px solid var(--border);
  display: flex; align-items: center; justify-content: center; font-size: 17px;
  cursor: pointer; background: #fff; transition: border-color .12s, transform .12s;
}
.av-opt:hover { border-color: var(--blue); transform: scale(1.06); }
.av-opt.active { border-color: var(--blue); background: var(--blue-soft); box-shadow: 0 0 0 2px var(--blue-soft); }
.av-opt.av-up, .av-opt.av-clear { color: var(--fg-dim); font-size: 13px; }

/* ---- 控制台左右布局 ---- */
.console-grid { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 18px; align-items: start; }
@media (max-width: 1180px) { .console-grid { grid-template-columns: 1fr; } }
.console-side { display: flex; flex-direction: column; gap: 18px; }
.console-side .card { margin-bottom: 0; }
.console-main { display: flex; flex-direction: column; gap: 18px; min-width: 0; }
.console-main .card { margin-bottom: 0; }

/* 控制台详细数据表 */
.dtable { display: flex; flex-direction: column; }
.drow {
  display: grid; grid-template-columns: 1fr 120px 1fr; gap: 10px; align-items: center;
  padding: 9px 4px; border-bottom: 1px solid #f0f3f8; font-size: 13px;
}
.drow:last-child { border-bottom: 0; }
.drow > span:first-child { color: var(--fg-dim); }
.drow b { font-variant-numeric: tabular-nums; font-weight: 600; }
.drow.dhead { font-size: 11.5px; color: var(--fg-dim2); font-weight: 600; padding-bottom: 6px; }
.drow.dhead span { color: var(--fg-dim2) !important; }

/*=============================================================================
  通用组件
=============================================================================*/
.card {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow); margin-bottom: 18px;
}
.card > header {
  padding: 13px 18px; border-bottom: 1px solid var(--border);
  display: flex; align-items: center; gap: 10px;
}
.card > header h3 { margin: 0; font-size: 14px; font-weight: 600; flex: 1; }
.card > header .hint { font-size: 12px; color: var(--fg-dim2); font-weight: 400; }
.card > .body { padding: 18px; }
.card > .body.tight { padding: 10px 18px 18px; }

.btn {
  height: 32px; padding: 0 14px; border-radius: 6px; cursor: pointer;
  border: 1px solid var(--border-2); background: #fff; color: var(--fg);
  font-size: 13px; transition: all .12s; white-space: nowrap;
}
.btn:hover { border-color: #b9c1cf; background: #fafbfd; }
.btn:disabled { opacity: .5; cursor: default; }
.btn.primary { background: var(--blue); border-color: var(--blue); color: #fff; font-weight: 500; }
.btn.primary:hover { background: var(--blue-dark); border-color: var(--blue-dark); }
.btn.danger { color: var(--red); border-color: #f0c4c4; }
.btn.danger:hover { background: #fef2f2; border-color: #e79a9a; }
.btn.sm { height: 27px; padding: 0 10px; font-size: 12.5px; }
.btn.icon { padding: 0 8px; height: 27px; }

.tag {
  display: inline-block; padding: 2px 8px; border-radius: 20px;
  font-size: 11.5px; background: var(--border); color: var(--fg-dim);
}
.tag.blue { background: var(--blue-soft); color: var(--blue); }
.tag.green { background: #ecfdf3; color: var(--green); }
.tag.red { background: #fef2f2; color: var(--red); }
.tag.amber { background: #fffbeb; color: var(--amber); }
/* 卡密列表「软件授权范围」标签(十一期): 复用上面的 .tag 胶囊, 只加作用域化配色。
   single 不渲染标签, 因此这里没有 .tag.scope-single。 */
.tag.scope-multi { margin-left: 6px; background: #eef2ff; color: #4f46e5; }
.tag.scope-all   { margin-left: 6px; background: #f5f3ff; color: #7c3aed; }

.grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 14px; }
.stat {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 15px 17px; box-shadow: var(--shadow);
}
.stat .k { font-size: 12px; color: var(--fg-dim); }
.stat .v { font-size: 24px; font-weight: 600; margin-top: 5px; letter-spacing: -.5px; }

.empty { padding: 46px 20px; text-align: center; color: var(--fg-dim2); font-size: 13.5px; line-height: 2; }
.empty .big { font-size: 34px; opacity: .35; }

.notice {
  padding: 10px 14px; border-radius: 6px; font-size: 13px;
  margin-bottom: 16px; display: flex; gap: 9px; align-items: flex-start;
}
.notice.warn { background: #fffbeb; border: 1px solid #fde68a; color: #92400e; }
.notice.info { background: var(--blue-soft); border: 1px solid #c7ddff; color: #1e40af; }

/* ---- 表单 ---- */
.frow { margin-bottom: 15px; }
.frow:last-child { margin-bottom: 0; }
.frow > label.lb {
  display: block; font-size: 13px; font-weight: 500; margin-bottom: 5px;
}
.frow .key {
  font-family: var(--mono); font-size: 11px; color: var(--fg-dim2);
  background: #f3f5f9; padding: 1px 5px; border-radius: 3px; margin-left: 6px;
}
.frow .help { font-size: 12px; color: var(--fg-dim); margin-top: 5px; line-height: 1.6; }
.inp, .sel, .txa {
  width: 100%; border: 1px solid var(--border-2); border-radius: 6px;
  padding: 8px 11px; outline: none; background: #fff; color: var(--fg);
  transition: border-color .15s, box-shadow .15s;
}
.inp:focus, .sel:focus, .txa:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); }
.inp { height: 36px; }
.sel { height: 36px; cursor: pointer; }
.txa { min-height: 92px; resize: vertical; line-height: 1.65; font-size: 13px; }
.inp.mono { font-family: var(--mono); font-size: 12.5px; }
.req { color: var(--red); margin-left: 3px; }

/* ---- 表格字段编辑 ---- */
.tblwrap { border: 1px solid var(--border); border-radius: 6px; overflow: auto; max-height: 560px; }
table.tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
table.tbl th {
  background: #f7f9fc; text-align: left; padding: 8px 10px; font-weight: 600;
  font-size: 12px; color: var(--fg-dim); border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 2; white-space: nowrap;
}
table.tbl td { padding: 3px 4px; border-bottom: 1px solid #eef1f6; vertical-align: middle; }
table.tbl tr:last-child td { border-bottom: 0; }
table.tbl td input {
  width: 100%; border: 1px solid transparent; border-radius: 4px;
  padding: 6px 8px; outline: none; background: transparent;
}
table.tbl td input:hover { background: #fafbfd; border-color: var(--border); }
table.tbl td input:focus { background: #fff; border-color: var(--blue); box-shadow: 0 0 0 2px var(--blue-soft); }
table.tbl td.act { width: 42px; text-align: center; }
table.tbl .rowno { width: 38px; text-align: center; color: var(--fg-dim2); font-size: 12px; }
.tblbar { display: flex; gap: 8px; align-items: center; margin-top: 10px; }
.tblbar .sp { flex: 1; }

table.list { width: 100%; border-collapse: collapse; font-size: 13px; }
table.list th {
  text-align: left; padding: 9px 12px; background: #f7f9fc; font-size: 12px;
  color: var(--fg-dim); font-weight: 600; border-bottom: 1px solid var(--border);
}
table.list td { padding: 9px 12px; border-bottom: 1px solid #eef1f6; }
table.list tr:last-child td { border-bottom: 0; }
table.list tr:hover td { background: #fafbfd; }
table.list code { font-family: var(--mono); font-size: 12px; }

/* ---- 代码块 ---- */
.code {
  background: #1e2430; color: #d6deeb; border-radius: 6px; padding: 13px 15px;
  font-family: var(--mono); font-size: 12.5px; line-height: 1.7;
  overflow-x: auto; white-space: pre; margin: 0;
}
.code .c { color: #6b7d95; }
.code .s { color: #a5d6a7; }
.code .k { color: #82aaff; }
.code-head {
  display: flex; align-items: center; gap: 8px; margin-bottom: 7px;
}
.code-head .t { font-size: 12.5px; font-weight: 600; flex: 1; color: var(--fg-dim); }

/* ---- 弹窗 ---- */
.mask {
  position: fixed; inset: 0; background: rgba(16, 22, 34, .5);
  display: flex; align-items: center; justify-content: center; z-index: 100; padding: 30px;
}
.modal {
  background: #fff; border-radius: 10px; width: 100%; max-width: 780px;
  max-height: 88vh; display: flex; flex-direction: column;
  box-shadow: 0 24px 60px rgba(10,18,32,.3);
}
.modal > header {
  padding: 15px 20px; border-bottom: 1px solid var(--border);
  display: flex; align-items: center;
}
.modal > header h3 { margin: 0; font-size: 15px; flex: 1; }
.modal > .body { padding: 18px 20px; overflow-y: auto; flex: 1; }
.modal > footer {
  padding: 13px 20px; border-top: 1px solid var(--border);
  display: flex; gap: 9px; justify-content: flex-end;
}
.modal.wide { max-width: 1020px; }
.x {
  background: none; border: 0; font-size: 21px; line-height: 1; cursor: pointer;
  color: var(--fg-dim2); padding: 0 4px;
}
.x:hover { color: var(--fg); }

/* ---- 提示条 ---- */
#toasts {
  position: fixed; right: 22px; bottom: 22px; z-index: 200;
  display: flex; flex-direction: column; gap: 9px; align-items: flex-end;
  /* ★ 批次50: 提示条是纯展示元素(toast() 无任何点击处理, 到点自动淡出),
     但它是 fixed + z-index 200, 堆叠多条时会盖住弹窗页脚的主按钮中心 ——
     实测点在被盖住的「确定」上既不校验也不发请求。这里让整块区域对点击透明,
     点击直接穿透到下面的按钮; 由于 toast 自身没有交互, 不损失任何功能。 */
  pointer-events: none;
}
.toast {
  background: #1f2937; color: #fff; padding: 11px 17px; border-radius: 7px;
  font-size: 13px; box-shadow: 0 8px 24px rgba(10,18,32,.28);
  animation: slideIn .18s ease-out; max-width: 420px; line-height: 1.6;
}
.toast.ok { background: #15803d; }
.toast.err { background: #b91c1c; }
@keyframes slideIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* 未保存提示 */
.dirty-dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--amber);
  display: inline-block; margin-right: 5px;
}

.spin {
  display: inline-block; width: 13px; height: 13px; border-radius: 50%;
  border: 2px solid var(--border-2); border-top-color: var(--blue);
  animation: sp .7s linear infinite; vertical-align: -2px;
}
@keyframes sp { to { transform: rotate(360deg); } }

.muted { color: var(--fg-dim2); font-size: 12.5px; }
.mono { font-family: var(--mono); }
.nowrap { white-space: nowrap; }
.sec-title {
  font-size: 12px; font-weight: 700; color: var(--fg-dim); letter-spacing: .6px;
  text-transform: uppercase; margin: 0 0 10px;
}
.kv { display: flex; gap: 10px; font-size: 13px; padding: 5px 0; }
.kv .k { color: var(--fg-dim); min-width: 92px; }

/*=============================================================================
  七期新增样式
=============================================================================*/

/* ---- 软件选项卡最右的「返回列表」---- */
.apptab.backtolist {
  background: transparent;
  border: 1px dashed var(--border);
  color: var(--muted);
  font-weight: 500;
  flex: 0 0 auto;
  margin-left: 6px;
}
.apptab.backtolist:hover {
  border-style: solid;
  border-color: var(--blue);
  color: var(--blue);
  background: #fff;
}

/* ---- 软件列表: 更好看的边框与行悬停 ---- */
.tblwrap.soft {
  border: 1px solid #e6eaf1;
  border-radius: 10px;
  box-shadow: 0 1px 2px rgba(16, 24, 40, .04);
  background: #fff;
}
table.tbl.applist { font-size: 13px; }
table.tbl.applist thead th {
  background: linear-gradient(#f8fafc, #f1f5f9);
  border-bottom: 1px solid #e2e8f0;
  padding: 11px 12px;
  font-weight: 600;
  color: #475569;
  white-space: nowrap;
}
table.tbl.applist tbody td {
  padding: 12px;
  border-bottom: 1px solid #f1f5f9;
  vertical-align: middle;
}
table.tbl.applist tbody tr:hover { background: #f8fbff; }
table.tbl.applist tbody tr:hover td:first-child { box-shadow: inset 3px 0 0 var(--blue); }
table.tbl.applist tbody tr:last-child td { border-bottom: 0; }

/* ---- 教程列表(子用户看教程) ---- */
.tut-list { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
.tut-item {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; border: 1px solid var(--border);
  border-radius: 8px; cursor: pointer; background: #fff;
}
.tut-item:hover { border-color: var(--blue); background: #f8fbff; }
.tut-item.active { border-color: var(--blue); background: var(--blue-soft); font-weight: 600; }
.tut-body { margin-top: 12px; }
.tut-content {
  font-size: 13.5px; line-height: 1.9; color: var(--fg);
  border: 1px solid var(--border); border-radius: 8px;
  padding: 14px 16px; background: #fff; margin-top: 8px;
}
.tut-content img { max-width: 100%; border-radius: 6px; }
.tut-content h1, .tut-content h2, .tut-content h3 { margin: 14px 0 8px; }
.tut-content p { margin: 8px 0; }
.tut-content ul, .tut-content ol { margin: 8px 0 8px 22px; }
.tut-content blockquote {
  margin: 10px 0; padding: 8px 14px;
  border-left: 3px solid var(--blue); background: var(--blue-soft);
}
.tut-content pre {
  background: #f6f8fa; border-radius: 6px; padding: 10px 12px;
  overflow: auto; font-size: 12.5px;
}
.tut-content table { border-collapse: collapse; }
.tut-content td, .tut-content th { border: 1px solid var(--border); padding: 5px 9px; }

/* ---- 常驻顶部区域 ---- */
.topbar-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.topbar-actions .btn { white-space: nowrap; }
.tb-badge {
  display: inline-block; min-width: 16px; height: 16px; line-height: 16px;
  padding: 0 4px; margin-left: 5px; border-radius: 8px;
  background: #dc2626; color: #fff; font-size: 10.5px; text-align: center;
}

/* ---- 站内信未读弹窗 ---- */
.msg-pop .body { max-height: 60vh; overflow: auto; }
.msg-pop .msg-meta { color: var(--muted); font-size: 12px; margin-bottom: 10px; }
.msg-pop .msg-text { font-size: 14px; line-height: 1.9; white-space: pre-wrap; }

/* ---- 宣传页(独立页面, 供子用户推广) ---- */
.promo-wrap { font-family: system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; }

/* ---- 页内二级选项卡(卡密列表 / 生成批次 等) ---- */
.subtabs {
  display: flex; gap: 8px; margin-bottom: 14px;
  border-bottom: 1px solid var(--border); padding-bottom: 0;
}
.subtab {
  padding: 9px 16px; font-size: 13.5px; cursor: pointer;
  color: var(--muted); border-bottom: 2px solid transparent;
  border-radius: 7px 7px 0 0; white-space: nowrap;
}
.subtab:hover { color: var(--fg); background: #f6f8fb; }
.subtab.active {
  color: var(--blue); font-weight: 600;
  border-bottom-color: var(--blue); background: var(--blue-soft);
}
.subtab .cnt {
  display: inline-block; margin-left: 6px; padding: 1px 7px;
  border-radius: 9px; background: #e7ecf3; color: #5b6675;
  font-size: 11.5px; font-weight: 500;
}
.subtab.active .cnt { background: #fff; color: var(--blue); }

/* ---- 软件商店: 入口区域(顶栏之下、页内子页签之上) ---- */
.shopbar {
  display: flex; align-items: center; gap: 10px;
  margin: 0 0 14px; padding: 12px 14px;
  border: 1px solid var(--border); border-radius: 10px;
  background: #f7f9fc;
}
.shopbar .btn {
  display: inline-flex; align-items: center; justify-content: center;
  margin: 0; text-decoration: none;
}

/* ---- 卡密详情: 基础信息 / 激活与使用 并排 ---- */
.two-col {
  display: grid; grid-template-columns: 1fr 1fr; gap: 26px;
  align-items: start;
}
@media (max-width: 900px) { .two-col { grid-template-columns: 1fr; gap: 14px; } }

/* ---- 选项卡上的计数徽标 ---- */
.tabs .tab .cnt {
  display: inline-block; margin-left: 5px; padding: 0 6px;
  border-radius: 9px; background: #e7ecf3; color: #5b6675; font-size: 11px;
}
.tabs .tab.active .cnt { background: #fff; color: var(--blue); }

/* ---- 富文本编辑器 ---- */
.rte-toolbar {
  display: flex; flex-wrap: wrap; gap: 4px; align-items: center;
  padding: 7px 8px; background: #f7f9fc;
  border: 1px solid var(--border); border-bottom: 0;
  border-radius: 8px 8px 0 0;
}
.rte-toolbar .btn { padding: 3px 9px; font-size: 12px; }
.rte-sep { width: 1px; height: 18px; background: var(--border); margin: 0 3px; }
.rte {
  min-height: 320px; padding: 14px 16px; background: #fff;
  border: 1px solid var(--border); border-radius: 0 0 8px 8px;
  font-size: 13.5px; line-height: 1.9; outline: none; overflow: auto;
}
.rte:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(37, 99, 235, .08); }

/* ---- 教程管理: 左栏目 / 右编辑 ---- */
.tut-admin { display: grid; grid-template-columns: 260px 1fr; gap: 18px; align-items: start; }
@media (max-width: 1000px) { .tut-admin { grid-template-columns: 1fr; } }
.tut-nav {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 10px 12px; border-radius: 8px; cursor: pointer;
  border: 1px solid transparent; margin-bottom: 4px;
}
.tut-nav:hover { background: #f6f8fb; }
.tut-nav.active { background: var(--blue-soft); border-color: var(--blue); }

/* ---- 常驻顶部操作区: 任何页面都在 ---- */
.global-acts {
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  padding-right: 14px; margin-right: 4px;
  border-right: 1px solid var(--border);
}
.global-acts:empty { display: none; }

/* ---- 十一期: 日志中心 ---- */
/* 筛选行: 原来是内联样式, 加时间范围后挪到这里, 只作用于日志页 */
.logfilters {
  display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 12px;
}
/* 查询失败时的错误行(非 2xx): 醒目提示, 不静默显示空表 */
.errline {
  background: #fef2f2; border: 1px solid #f0c4c4; color: var(--red);
  border-radius: 8px; padding: 10px 12px; font-size: 13px; line-height: 1.6;
}
.errline code { font-family: var(--mono); font-size: 11.5px; }
.global-acts .btn { white-space: nowrap; }
@media (max-width: 1180px) {
  .global-acts { border-right: 0; padding-right: 0; }
}

/*=============================================================================
  t11: 软件详情页(#/apps/<id>/<tab>)—— 顶部不再放页面标题/副标题, 高度真收掉,
       标题/副标题改由当前选项卡承载(#detailHead, 由 app.js 创建并填充)。
       只涉及顶栏(.topbar)与选项卡条, 不碰分类栏(.catcol/.catitem/.cathead/.tri)
       与主侧边栏的任何规则。
=============================================================================*/
/* 顶部常驻操作区(全局按钮 + 右上角用户区)保留, 只是高度收到内容本身那么高 */
body.app-detail .topbar { height: auto; min-height: 0; flex: 0 0 auto; padding: 8px 24px; justify-content: flex-end; }
/* 旧标题块整块不显示(连 62px 里的那一段高度一起收掉, 不留空白条) */
body.app-detail .topbar .ttl { display: none; }

/* 当前选项卡的标题条: 挂在选项卡栏之下、内容区之上, 在文档流里, 不与谁重叠 */
.detailhead { display: none; }
body.app-detail .detailhead {
  display: block; flex: 0 0 auto; background: var(--panel);
  border-bottom: 1px solid var(--border); padding: 10px 24px 12px;
}
.detailhead .dh-title { font-size: 16px; font-weight: 600; color: var(--fg); }
/* 副标题允许换行(原来是 nowrap + 省略号), 长文案在 1440px 下自行折行, 不撑破布局 */
.detailhead .dh-sub { margin-top: 3px; font-size: 12px; color: var(--fg-dim); line-height: 1.6; overflow-wrap: anywhere; }

/*=============================================================================
  从 ini 导入（重做版）—— 整块界面只有「上传入口 + 一张表」。
  表本身复用下载列表字段编辑器那一套(.frow/.tblwrap/table.tbl/.tblbar)，
  这里只补 4 条新规则，全部限定在自己容器 class .ini-import 内，不碰任何既有规则。
=============================================================================*/
.ini-import .tbl td.ini-empty {
  padding: 26px 16px; text-align: center; color: var(--fg-dim2);
  font-size: 13px; line-height: 1.9;
}
.ini-import .tblwrap { max-height: 46vh; }
.ini-import .tblbar .muted { font-size: 12px; text-align: right; }
.ini-import .ini-paste {
  margin-top: 10px; padding: 12px 14px; border: 1px solid var(--border);
  border-radius: 6px; background: #fafbfd;
}

/*=============================================================================
  t12 交付B: 软件详情页区域条(左「← 返回上一层」· 右「删除此软件」)
    只在软件详情路由(#/apps/<id>/...)上由 app.js 挂载, 离开详情页整条移除。
    视觉口径照商店页的 .shopbar 抄; 新增规则全部限定在 .detailbar 里, 不写裸元素选择器。
=============================================================================*/
.detailbar {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin: 12px 24px 14px; padding: 12px 14px;
  border: 1px solid var(--border); border-radius: 10px;
  background: #f7f9fc;
  flex: 0 0 auto;
}
.detailbar .btn {
  display: inline-flex; align-items: center; justify-content: center;
  margin: 0; text-decoration: none;
}
/* 「删除此软件」直接复用项目现成的红按钮类 .btn.danger(见 .btn.danger 那一条), 这里不重复定义颜色 */

/* 「确认删除」弹窗里的警告文案(红字)与软件信息行 */
.delwarn { color: var(--red); font-size: 13.5px; line-height: 1.8; font-weight: 600; }
.delmeta { margin-top: 8px; font-size: 12.5px; color: var(--fg-dim); }

/*=============================================================================
  t12 交付C: 「配置与字段」页的分组折叠头
    默认只有「下载列表」展开, 软件信息 / 界面文字 / 提示信息等分组默认收起。
    折叠只切 .fgroup-body 的 display, 输入框始终留在 DOM 里, 「保存并下发」不受影响。
=============================================================================*/
.fgroup-head {
  display: flex; align-items: center; gap: 8px;
  cursor: pointer; user-select: none;
  padding: 5px 8px; margin-left: -8px; border-radius: 7px;
}
.fgroup-head:hover { background: #eef2f8; }
.fgroup-head .tri { width: 12px; text-align: center; font-size: 10px; color: var(--fg-dim2); }
.fgroup-head .cnt {
  margin-left: auto; padding: 1px 7px; border-radius: 9px;
  background: #eef1f6; color: var(--fg-dim2);
  font-size: 11.5px; font-weight: 500; letter-spacing: 0; text-transform: none;
}
.fgroup-body { margin-bottom: 6px; }

/*=============================================================================
  [frontend-style] 追加段 · 左侧栏二级子菜单 (cardmgmt 下发菜单)
  契约 DOM: #sbNav .sb-sub > .sb-subitem[data-nav][data-sub] ; .sb-subitem.active
            #sbNav .sb-item.open ; #sbNav .sb-caret (▾/▸)
  取值口径(全部沿用上方既有规则, 不新增变量/不用 !important):
    .sb-item         = padding 10px 18px / font-size 13px / border-left 3px solid transparent
    .sb-item:hover   = background var(--sb-bg-2) / color #fff
    .sb-item.active  = linear-gradient(90deg, rgba(59,130,246,.22), rgba(59,130,246,.10))
                       + border-left-color var(--sb-active) + color #fff + font-weight 600
    既有下级先例 .sb-sub .sb-item = font-size 12.5px
    次级文字色 = var(--sb-fg-dim) (与 .sb-add 同级)
  本段只新增选择器, 不改动上方任何既有行。
=============================================================================*/

/* 二级项: 让「子项文字」真正比「父项文字」右移 ≈18px, 字号沿用既有下级先例。
   父项文字左边界 = border 3 + padding 18 + 圆点 8 + gap 10 = 39px(圆点占位必须算进去);
   子项无圆点, 文字左边界 = 3 + padding-left; 取 padding-left 54px → 3+54 = 57px, 57-39 = 18px,
   落在目标区间 16~20px 内, 且容器 padding-left 差 54-18 = 36px ≥ 20px。
   (旧值 42px 对应 45-39 = +6px, 视觉上几乎看不出缩进, 故按实测改为 54px) */
#sbNav .sb-sub .sb-subitem {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 18px 8px 54px; cursor: pointer;
  font-size: 12.5px; color: var(--sb-fg-dim);
  border-left: 3px solid transparent;
  transition: background .14s, color .14s;
}
#sbNav .sb-sub .sb-subitem:hover { background: var(--sb-bg-2); color: #fff; }

/* 激活态: 与 .sb-item.active 同一套高亮(蓝渐变底 + 3px 蓝色左竖条) */
#sbNav .sb-sub .sb-subitem.active {
  background: linear-gradient(90deg, rgba(59,130,246,.22), rgba(59,130,246,.10));
  border-left-color: var(--sb-active); color: #fff; font-weight: 600;
}
#sbNav .sb-sub .sb-subitem .nm { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* 折叠态: 只有"明确折叠"才隐藏; 无任何标记的初始渲染保持可见。
   故不使用 .sb-item:not(.open) + .sb-sub (那会让默认渲染被藏掉), 只认 [hidden] 与 .collapsed。 */
#sbNav .sb-sub[hidden] { display: none; }
#sbNav .sb-item.collapsed + .sb-sub { display: none; }

/* 展开指示符: 右对齐、小号、不换行; 字形(▾/▸)由 app.js 文本内容切换, 不做旋转 */
#sbNav .sb-caret {
  margin-left: auto; flex: 0 0 auto;
  font-size: 10px; line-height: 1; white-space: nowrap;
  color: var(--sb-fg-dim); user-select: none;
}
#sbNav .sb-item.open .sb-caret { color: #fff; }

/*=============================================================================
  [frontend-style] 追加段 · 卡密「分类管理」两级分类树
  契约 DOM(已冻结, 由 biz.js 生成):
    .ccat-group                     一级容器
      .ccat-row.lv1                 主分类行
        .ccat-caret                 折叠指示符(▾/▸ 字形由脚本切换)
      .ccat-children                子分类区域(挂在主分类行下方)
        .ccat-row.lv2               子分类行(缩进)
        .ccat-addchild              空态/末尾「新建子分类」虚线入口(按钮)
  取值口径(全部沿用上方既有规则, 不新增 CSS 变量 / 不用强制覆盖 / 不写裸元素选择器):
    .card          = 背景 var(--panel) / 边框 var(--border) / 圆角 var(--radius)
    .shopbar/.detailbar 的浅底 = #f7f9fc + 1px solid var(--border) + 圆角 10px
    table.tbl      = font-size 13px / th 12px var(--fg-dim) / td 分隔线 #eef1f6
    .fgroup-head   = 折叠头 hover #eef2f8, 指示符 .tri 小号 var(--fg-dim2)
    .sb-add        = 空态虚线先例(1px dashed + 次要色 + hover 变亮), 此处改走浅色底
  本段只新增 .ccat-* 选择器, 不改动上方任何既有行。
=============================================================================*/

/* 一级容器: 与 .card 里的表格/表单同呼吸, 块间只留 10px */
.ccat-group { margin-bottom: 10px; }
.ccat-group:last-child { margin-bottom: 0; }

/* 主/子行共用骨架 */
.ccat-row {
  display: flex; align-items: center; gap: 8px;
  border-radius: 6px;
  transition: background .14s, color .14s;
}

/* 主分类行: 比子分类更"重"——略深底色(同 .tbl th 的 #f7f9fc)+ 加粗字重 + 完整描边 */
.ccat-row.lv1 {
  padding: 9px 10px;
  background: #f7f9fc; border: 1px solid var(--border);
  font-size: 13px; font-weight: 600; color: var(--fg);
}
.ccat-row.lv1:hover { background: #eef2f8; }

/* 子分类行: 左内边距比主分类行多 24px(10px → 34px), 字号略小、字色用次要色体现从属 */
.ccat-row.lv2 {
  padding: 7px 10px 7px 34px;
  font-size: 12.5px; color: var(--fg-dim);
}
.ccat-row.lv2:hover { background: #f6f8fb; color: var(--fg); }

/* 子分类区域: 视觉上"挂在"主分类行下方, 左侧一条 1px 竖线做从属暗示;
   空态时里面就只有 .ccat-addchild 一个虚线入口 */
.ccat-children {
  margin: 2px 0 4px 11px; padding: 4px 0 4px 8px;
  border-left: 1px solid var(--border-2);
}

/* 空态/末尾「新建子分类」入口: 虚线边框 + 次要文字色 + 透明底, 明确不像实心主按钮;
   宽度占满 .ccat-children 可用宽度, 文字缩进与 .ccat-row.lv2 对齐; hover 时边框/文字/底色一起变亮 */
.ccat-addchild {
  display: block; width: 100%; text-align: left;
  margin: 2px 0; padding: 8px 10px 8px 34px;
  background: transparent; color: var(--fg-dim2);
  border: 1px dashed var(--border-2); border-radius: 6px;
  font-size: 12.5px; cursor: pointer;
  transition: border-color .14s, color .14s, background .14s;
}
.ccat-addchild:hover {
  border-color: var(--blue); color: var(--blue); background: var(--blue-soft);
}

/* 折叠指示符: 小号、不换行、可点击感; 字形(▾/▸)由脚本切换, 不做动画 */
.ccat-caret {
  flex: 0 0 auto; width: 12px; text-align: center;
  font-size: 10px; line-height: 1; white-space: nowrap;
  color: var(--fg-dim2); cursor: pointer; user-select: none;
}
.ccat-row.lv1:hover .ccat-caret { color: var(--blue); }

/*=============================================================================
  [frontend-style] 追加段 · 顶部「已打开页面」选项卡条 (#pageViewTabs) + 右键浮层 (#tabMenu)
  契约 DOM(由 app.js 生成, 见备份 _tools/app.js.bak 第 786-882 行):
    #pageViewTabs
      .pvtab[data-hash][title]        单个页面页签, 当前页额外带 .pvtab.active
        .nm                           页签文字(超长省略)
        .x[data-pvclose][title]       关闭按钮(×)
    #tabMenu(.tabmenu)                右键浮层, position:fixed, left/top 由脚本按鼠标坐标写入
      .tmi[data-act]                  菜单项, data-act = refresh/close/closeOthers/closeLeft/closeRight/closeAll
        .ico / .tx                    图标 / 文字
      .tmi.off                        禁用项(脚本判 on=false 时追加, 不可点)
  取值口径(全部沿用上方既有规则, 不新增 CSS 变量 / 不用强制覆盖 / 不写裸元素选择器):
    .topbar/.tabs  = 底色 var(--panel) + 1px solid var(--border), 左右内边距 24px
    高度 39px      = 当年实测容器尺寸 1248x39(box-sizing:border-box 已由 line 32 全局开启)
    .tab.active    = var(--blue) 文字 + 底部 2px 蓝条(此处用 inset 阴影画条, 同 .catitem.active 的写法)
    .catmenu       = 右键菜单先例(fixed / 1px var(--border) / 圆角 var(--radius) / 浅阴影)
    z-index 90     = 高于内容与 .ubmenu(60), 低于 .mask 模态框(100)与 #toasts(200)
  本段只新增 #pageViewTabs / .pvtab / #tabMenu 相关选择器, 不改动上方任何既有行。
  批次35(需求②: 「优化选项卡效果, 好看些」): 只重写本段里 #pageViewTabs / .pvtab / .nm / .x 的
    视觉, DOM 契约与上面这段注释里写的**完全不变**(仍是 .pvtab[data-hash].active > .nm + .x[data-pvclose],
    仍由 app.js 生成)。做法: 圆角 8px + 1px 描边 + 前置状态小圆点(::before, 不新增 DOM) + 激活态白底蓝字
    加底部蓝条; 容器高度**仍是 39px**(锁死 flex:0 0 39px, 不动整体布局), 只收紧内边距与页签间距。
    菜单部分(#tabMenu / #ccatMenu 那几条)本次一个字都没改。
=============================================================================*/

/* 容器: 顶栏之下、.appbar 之上; 单行不换行, 超出横向滚动且不露滚动条; 底部 1px 分隔线 */
#pageViewTabs {
  display: flex; align-items: center; gap: 4px;
  flex: 0 0 39px; height: 39px; padding: 0 18px;
  background: var(--panel); border-bottom: 1px solid var(--border);
  overflow-x: auto; overflow-y: hidden; white-space: nowrap;
  scrollbar-width: none; -ms-overflow-style: none;
}
#pageViewTabs::-webkit-scrollbar { width: 0; height: 0; }
#pageViewTabs:empty { display: none; }

/* 页签(批次35 改版): 圆角胶囊 + 1px 透明描边 + 前置状态小圆点(::before, **不新增 DOM**);
   非激活态是「浅灰底 / 次要文字色」, 悬停浮起一点点(用 1px 阴影而不是 transform, 免得文字发虚)。
   那个 6px 小圆点表示「这一页还开着」, 未激活灰、激活蓝。 */
#pageViewTabs .pvtab {
  display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto;
  max-width: 208px; height: 27px; padding: 0 6px 0 10px;
  border: 1px solid transparent; border-radius: 8px; background: #f5f7fb;
  font-size: 12.5px; line-height: 1; color: var(--fg-dim);
  cursor: pointer; user-select: none;
  transition: background .14s, color .14s, border-color .14s, box-shadow .14s;
}
#pageViewTabs .pvtab::before {
  content: ''; flex: 0 0 6px; width: 6px; height: 6px; border-radius: 50%;
  background: var(--border-2); transition: background .14s, box-shadow .14s;
}
#pageViewTabs .pvtab:hover {
  background: #edf1f8; color: var(--fg); border-color: var(--border-2);
  box-shadow: 0 1px 2px rgba(16,24,40,.06);
}
#pageViewTabs .pvtab:hover::before { background: var(--fg-dim2); }
/* 按下时轻微下沉, 给一次「点到了」的手感 */
#pageViewTabs .pvtab:active { background: #e6ecf6; }

/* 激活态(批次35): 白底 + 蓝字 + 淡蓝描边 + 底部 2px 蓝条(inset 阴影画条, 不占布局)
   + 一圈很淡的蓝晕; 与 .tab.active / .catitem.active 的「蓝字 + 蓝条」口径一致。
   小圆点点亮成蓝色并带光晕, 让「当前在哪一页」一眼可见。 */
#pageViewTabs .pvtab.active {
  background: var(--panel); color: var(--blue); font-weight: 600;
  border-color: #cddffb;
  box-shadow: 0 1px 3px rgba(37,99,235,.14), inset 0 -2px 0 var(--blue);
}
#pageViewTabs .pvtab.active::before {
  background: var(--blue); box-shadow: 0 0 0 3px rgba(37,99,235,.12);
}
#pageViewTabs .pvtab.active:hover { background: var(--panel); border-color: #b9d1fa; }

/* 页签文字: 单行省略, 免得长标题把页签撑破
   (批次35: 156px -> 150px, 给新加的状态圆点和关闭按钮留出宽度, 总量仍在 max-width 208px 之内) */
#pageViewTabs .pvtab .nm {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 150px;
}

/* 关闭按钮: 小号居中, 平时淡一点(整条页签栏更清爽), 鼠标移到这个页签上才完全显形;
   自己 hover 时仍是红底红字(与 .btn.danger 的 var(--red) 同色, 底 #fef2f2 亦为现成取值)。 */
#pageViewTabs .pvtab .x {
  flex: 0 0 auto; width: 16px; height: 16px; padding: 0; margin: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: 5px; background: transparent;
  font-size: 14px; line-height: 1; color: var(--fg-dim2); cursor: pointer;
  opacity: .5; transition: background .12s, color .12s, opacity .12s;
}
#pageViewTabs .pvtab:hover .x { opacity: .9; }
#pageViewTabs .pvtab.active .x { opacity: .75; }
#pageViewTabs .pvtab.active:hover .x { opacity: 1; }
#pageViewTabs .pvtab .x:hover { background: #fef2f2; color: var(--red); opacity: 1; }

/* 右键浮层: 白底 / 1px var(--border) / 圆角 6px / 浅阴影, 位置由脚本写 left/top 故用 fixed
   批次15(追加需求⑤): 卡密分类左栏主分类的右键菜单(#ccatMenu)用同一套外观 —— 同一组选择器, id 各自独立 */
:is(#tabMenu, #ccatMenu) {
  position: fixed; z-index: 90; min-width: 148px; padding: 5px;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: 6px; box-shadow: 0 8px 24px rgba(10,18,32,.16);
}
:is(#tabMenu, #ccatMenu) .tmi {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 12px; border-radius: 6px;
  font-size: 13px; color: var(--fg); cursor: pointer; white-space: nowrap;
  transition: background .12s, color .12s;
}
:is(#tabMenu, #ccatMenu) .tmi:hover { background: var(--blue-soft); color: var(--blue); }
:is(#tabMenu, #ccatMenu) .tmi .ico {
  flex: 0 0 14px; width: 14px; text-align: center;
  font-size: 12px; color: var(--fg-dim2);
}
:is(#tabMenu, #ccatMenu) .tmi:hover .ico { color: var(--blue); }
:is(#tabMenu, #ccatMenu) .tmi .tx { flex: 1; }

/* 禁用项: 次要色 + 默认光标, hover 不给任何反馈(写在 :hover 之后以同权重后置覆盖) */
:is(#tabMenu, #ccatMenu) .tmi.off { color: var(--fg-dim2); cursor: default; }
:is(#tabMenu, #ccatMenu) .tmi.off:hover { background: transparent; color: var(--fg-dim2); }
:is(#tabMenu, #ccatMenu) .tmi.off .ico { color: var(--fg-dim2); }

/*=============================================================================
  [frontend-style] 批次15 · 卡密「分类管理」左右两栏布局
  背景: 该页从「上下堆叠的树」改成「左栏主分类 + 右栏子分类」, 观感对齐「软件管理」页。
  做法(批次15 返工): 左栏**直接挂上「软件管理」左栏那一套 class**, 不再自己写一份数值相近的:
    .catcol   250px 定宽 + sticky + 圆角/描边/阴影/最大高度
    .cathead  13px 18px 内边距 + 渐变底 + 分割线;  .catlist 8px 6px 10px + 细滚动条
    .catitem  9px 10px 内边距 / 13px 字号 / 8px 间距 / .ico .nm .ct / hover / .active 高亮
    右栏沿用 .tblwrap.soft + table.tbl.applist + .empty, 本段不再覆盖行高与空态内边距。
  契约 DOM(由 biz.js 的 viewCardCategories 生成):
    .card > header > #ccNew                      卡片头(批次13 的「+ 新建主分类」, 原样保留)
    .card > .body.tight > .ccat-split            左右两栏容器(display:flex; gap:16px)
      > aside.catcol.ccat-side#ccatSide          左栏(与软件管理同宽 250px)
          .cathead.ccat-sidehead > h4 + .muted + #ccatNewMain   左栏标题行(字号对齐 .cathead h3; 最右一个 + 控件)
          .catlist.ccat-sidelist#ccatSideList    主分类列表(可滚动)
            .catitem.lv1[data-ccmain][.active]   单个主分类条目(含空 .tri 占位, 缩进与软件管理一致)
      > div.ccat-main#ccatMain                   flex:1; min-width:0
          .ccat-mainhead                         右栏顶行: 名字 + 统计 + 上移/下移/改名/+ 添加子分类 + #ccatAddChild(+)
          .tblwrap.soft > table.tbl.applist      子分类表(只有数据行; 空列表时表头在、tbody 无数据行)
          .empty.ccat-empty                      空状态(两句文案, 入口在上方操作行)
  ⚠️ 本段只留「软件管理那边没有对应元素」的几条差异规则。凡是 .catcol/.cathead/.catlist/.catitem/
     .tblwrap/.applist/.empty 已经给过的数值, 一律不要在这里重复声明 —— 否则 240 vs 250、
     10px vs 13px、9px vs 12px 这类偏差会重新长回来。
=============================================================================*/

/* 左右两栏: 与「软件管理」的 .appsplit 同一套(16px 间距、顶对齐) */
.ccat-split { display: flex; gap: 16px; align-items: flex-start; }

/* 左栏容器/标题行/列表: 样式全部来自 .catcol / .cathead / .catlist(软件管理那一套), 本段不再
   声明宽度、内边距、圆角、阴影、最大高度、滚动条, 也不碰悬停/选中态。
   ⚠️ .ccat-side / .ccat-sidehead / .ccat-sidelist 三个 class 仍然留在 DOM 上(选择器钩子不删),
      只是故意不在这里写样式, 保证上层那些数值不会被覆盖。 */
/* 差异1: 左栏标题是 <h4>(软件管理是 <h3>), 字号/字重/弹性补成与 .cathead h3 完全一致 */
.ccat-sidehead h4 { margin: 0; font-size: 14px; font-weight: 600; flex: 1; }
/* 差异2: 条目里多一句「· N 个子分类」的次要文字, 整行选中加粗时它不跟着粗 */
.ccat-sidelist .catitem .nm .muted { font-weight: 400; }
/* 只有「左栏一个主分类都没有」时才会用到(正常情况下 0 个主分类走的是下面的 .ccat-blank) */
.ccat-sideempty { padding: 22px 10px; font-size: 12.5px; line-height: 1.7; text-align: center; color: var(--fg-dim2); }

/* 右栏: 占满剩余宽度; min-width:0 保证里面的表格能正常收缩、需要时出滚动条 */
.ccat-main { flex: 1; min-width: 0; }

/* 右栏顶行: 对应「软件管理」右栏那张 .card > header(标题 + 统计 + 操作按钮)。
   数值取 .card > header 的口径: 上下 13px、间距 10px、底部分割线、透明底;
   左右不留内边距 —— 本页已经在 .card > .body 的 18px 里了, 再留一次标题就会比表格右移 18px。
   (软件管理那张卡的 .body.tight 自带 10px 上内边距, 这里用 margin-bottom:10px 给出同样的间距。)
   flex-wrap 保持 wrap: 这行是 4 个按钮的操作条, 软件管理只有 1 个, 窄屏下换行比溢出好。 */
.ccat-mainhead {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 13px 0; margin: 0 0 10px;
  background: transparent; border: 0; border-bottom: 1px solid var(--border); border-radius: 0;
}
.ccat-mainhead .ttl { flex: 1; font-size: 14px; font-weight: 600; color: var(--fg); }
.ccat-mainhead .hint { font-size: 12px; color: var(--fg-dim2); }
.ccat-mainhead .sp { flex: 1; }

/* 子分类表: 沿用 .tblwrap.soft + table.tbl.applist —— 行高(12px 内边距)由 .applist 给出, 这里不再覆盖。
   (末尾那行「+ 新建子分类」(.ccat-addrow/.ccat-addchild) 与空态里的按钮入口已按需求从 DOM 删掉) */

/* 空状态: 完全复用 .empty(46px/20px 内边距、34px 图标), 只剩两句文案, 不再放按钮 */

/* 批次15(追加需求): 一个主分类都没有时, 正文区域就是一块空白 ——
   不渲染左栏主分类列表、也不渲染右栏子分类表格/表头, 只有一句很短的引导文字;
   等建出第一个主分类(>=1)之后, 左右两栏(.ccat-split)才会出现。 */
.ccat-blank { padding: 8px 0; }

/* 批次15(追加需求⑤): 左栏主分类的拖拽排序反馈 —— 被拖那一条半透明 + 插入位置那根蓝色指示线。
   拖拽只用 pointer 事件实现(不用 HTML5 draggable), 所以这里只需要「看得见」的样式:
   .catitem.dragging 由脚本加上 / 松手去掉; #ccatDropLine 是脚本临时插进 #ccatSideList 的指示线,
   松手时被移除, 因此它不会影响任何既有选择器。 */
.ccat-sidelist { position: relative; }
.ccat-sidelist .catitem { touch-action: none; user-select: none; }   /* 只限本页左栏: 拖拽时别让浏览器把手势/文字选择吃掉 */
.catitem.dragging { opacity: .45; }
.ccat-dropline {
  height: 3px; margin: 3px 6px; border-radius: 2px;
  background: var(--blue); box-shadow: 0 0 0 1px rgba(37,99,235,.18);
}
/* 批次15(追加需求⑦): 子分类「配置详情」弹窗 —— 页签栏直接沿用上面既有的 .tabs/.tab/.tab.active,
   这里只补两点: 窄屏能换行、切页签是纯前端 display 切换(.ccd-pane[hidden] 必须真的不占位) */
.ccd-tabs { flex-wrap: wrap; row-gap: 2px; }
.ccd-pane[hidden] { display: none; }
.ccd-pane .notice { margin-bottom: 0; }

/*===========================================================================
  批次18: 分类管理页(两栏) 与 卡密模板页(三栏) **共用同一批声明** —— 联动 + 简约大气
  两页的栏容器都叫 .ccat-split, 所以下面一律以 .ccat-split 打头;
  「软件管理」页左栏那些 .catcol/.cathead/.catlist/.catitem 不在 .ccat-split 里, 完全不受影响。
  契约 DOM(两页同构; id 清单见 biz.js 里 viewCardCategories / viewCardTemplates 上面那两段注释):
    .catcrumb#ccatCrumb / #ctplCrumb        整卡上方的面包屑(未选的层级是 .ck.ph 灰字占位)
    .ccat-split > aside.catcol.ccat-side[.is-active]   选择栏(.cathead + .catlist > .catitem.lv1[.active] > .cv)
                > div.ccat-main                        内容栏(.ccat-mainhead + .tblwrap.soft / .empty)
  外观口径(实测数字见 _tools/out/b18-link-ui.json):
    ① 一个外框: 白底 #fff + 圆角 12px + 1px 边框 var(--border) + 极淡阴影 + gap:0 → 三栏连成一张卡
    ② 两条竖分隔线: 第 1/2 栏 border-right:1px → col1.right === col2.left、col2.right === col3.left, 栏间无空隙
    ③ 条目: 高 40px / 左右内边距 12px / 字号 14px(选中 600); hover #f5f7fb, 选中 #eef4ff + 文字 #1d4ed8
    ④ 选中行最右一个小箭头 .cv(›, 14px, 颜色跟随文字) —— 只有选中行有, 未选中/悬停都不出现
    ⑤ 活动列(.is-active = 「最深那个选择」所在栏; 内容栏永远是白的)提亮为 #fff;
       非活动列 #fbfcfe + 条目文字 #5b6472 —— 分类管理页两栏里左栏始终是活动列, 所以那页实测两栏都是白底
    ⑥ 栏目名 13px #5b6472 + 上下文名 14px + 计数 12px #8a93a3; 所有头条 min-height 52px(含 27×27 的「+」也正好等高)
    ⑦ 切换过渡 background-color .15s(条目/栏底色), 第三栏内容 .16s 淡入
    ⑧ 卡片内部除滚动条外不再有别的框线: 内容栏 .tblwrap.soft 的内框/圆角/阴影归零, 只留浅色行分隔线
===========================================================================*/
.ccat-split {
  gap: 0;                                        /* ① 合成一张卡: 栏与栏紧贴, 只用 1px 竖线相连 */
  align-items: stretch;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: #fff;
  box-shadow: 0 1px 2px rgba(16, 24, 40, .04);
  overflow: hidden;
}
/* 选择栏/内容栏各自的内框线、圆角、阴影、吸顶、最大高度全部归零 —— 整卡只有上面那一个外框 */
.ccat-split > .catcol, .ccat-split > .ccat-side, .ccat-split > .ccat-main {
  border: 0; border-radius: 0; box-shadow: none; position: static; max-height: none;
}
.ccat-split > .catcol, .ccat-split > .ccat-side {
  flex: 0 0 240px; width: 240px; min-height: 420px;      /* 分类管理页: 左栏 240px */
  border-right: 1px solid var(--border);                /* ② 竖分隔线挂在左边那一栏的右侧 */
  background: #fbfcfe;                                  /* ⑤ 默认 = 非活动列(极淡灰蓝) */
  transition: background-color .15s;
}
.ccat-split > .catcol.is-active, .ccat-split > .ccat-side.is-active { background: #fff; }
.ccat-split > .ccat-main { flex: 1 1 auto; min-width: 0; background: #fff; }
.ccat-split > .catcol:not(.is-active) .catitem,
.ccat-split > .ccat-side:not(.is-active) .catitem { color: #5b6472; }        /* 非活动列文字压淡一档 */
/* 卡密模板页: 220/220/自适应, 第三栏至少 460px */
.ccat-split.ctpl-split > .catcol, .ccat-split.ctpl-split > .ccat-side { flex: 0 0 220px; width: 220px; }
.ccat-split.ctpl-split > .ccat-main { flex: 1 1 460px; min-width: 460px; }

/* ---- 栏目头条: 13px 灰栏目名 + 14px 上下文名 + 12px 更淡的计数; 去掉原来那条渐变底 ---- */
.ccat-split .cathead, .ccat-split .ccat-mainhead {
  min-height: 52px; height: 52px; padding: 12px 14px; background: transparent; flex: 0 0 auto;
  border-bottom: 1px solid var(--border); flex-wrap: nowrap; overflow: hidden;   /* 三个头条等高 52px, 长名字省略号而不是把头条撑高 */
}
.ccat-split .cathead h4, .ccat-split .ccat-mainhead h4 {
  margin: 0; font-size: 13px; font-weight: 500; color: #5b6472; flex: 0 0 auto;
}
.ccat-split .cathead .ttl, .ccat-split .ccat-mainhead .ttl {
  flex: 0 1 auto; min-width: 0; font-size: 14px; font-weight: 600; color: #1f2937;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ccat-split .cathead .muted, .ccat-split .ccat-mainhead .hint {
  font-size: 12px; color: #8a93a3; flex: 0 1 auto;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* 栏内那个「+」永远贴最右, 且 flex:0 0 auto → 窄栏下也不会被 flex 压扁(仍保持 27×27 紧凑方形) */
.ccat-split .cathead .btn, .ccat-split .ccat-mainhead .btn { margin-left: auto; flex: 0 0 auto; }
.ccat-split .cathead .sp, .ccat-split .ccat-mainhead .sp { flex: 1; }
.ccat-split .catlist { padding: 8px; max-height: 520px; }

/* ---- 条目: 40px 行高 / 12px 左右内边距 / 14px; 去掉 .tri 占位与「左侧色条」那种重装饰 ---- */
.ccat-split .catitem .tri { display: none; }
.ccat-split .catitem.lv1 {
  height: 40px; padding: 10px 12px; margin-bottom: 2px; font-size: 14px; border-radius: 8px;
  transition: background-color .15s, color .15s;        /* ⑦ 120~160ms 的切换过渡 */
}
.ccat-split .catitem.lv1:hover { background: #f5f7fb; color: var(--fg); }
.ccat-split .catitem.lv1.active { background: #eef4ff; color: #1d4ed8; font-weight: 600; box-shadow: none; }
/* 批次50 需求④: 原来这里有一条 `.ccat-split .catitem.lv1.active .ct { background:#fff; color:#1d4ed8; }`,
   它唯一的服务对象是分类管理左栏主分类条目上的「卡密数」胶囊(biz.js:4914), 该胶囊随
   「分类统计下线」一并移除, 这条覆盖规则已无使用者, 故删除。通用的 `.catitem .ct`
   (style.css 上方 + app.js:1555/1565 的软件数胶囊)不受影响, 必须保留。 */
.ccat-split .catitem .cv { font-size: 14px; line-height: 1; font-weight: 600; color: inherit; margin-left: 2px; flex: 0 0 auto; }

/* ---- 内容栏: 去掉内框线/圆角/阴影, 表格与空态左右各留 14px; 表头压淡一档 ---- */
.ccat-split .tblwrap.soft { border: 0; border-radius: 0; box-shadow: none; background: transparent; }
.ccat-split table.tbl.applist thead th { background: #fbfcfe; color: #8a93a3; }
.ccat-split table.tbl.applist tbody tr:hover td:first-child { box-shadow: none; }
.ccat-split .ccat-main > .tblwrap, .ccat-split .ccat-main > .empty,
.ccat-split .ccat-main > .notice, .ccat-split .ccat-main > .tblbar { margin-left: 14px; margin-right: 14px; }
.ccat-split .ccat-main > .tblwrap { margin-top: 0; }
@keyframes ccatFade { from { opacity: 0 } to { opacity: 1 } }
.ccat-split .ccat-main > .tblwrap, .ccat-split .ccat-main > .empty { animation: ccatFade .16s ease-out; }

/* ---- 面包屑: 已选的层级是深色文字, 没选的层级是灰字占位; 只有真正能点的层级用 button ---- */
.catcrumb { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; padding: 0 2px 10px; font-size: 13px; color: #5b6472; }
.catcrumb .sep { color: #c3cad6; }
.catcrumb .ck { font: inherit; color: #5b6472; padding: 2px 6px; border: 0; border-radius: 6px; background: transparent; }
.catcrumb button.ck { cursor: pointer; }
.catcrumb button.ck:hover { color: #1d4ed8; background: #f3f6fb; }
.catcrumb .ck.plain, .catcrumb .ck.ph, .catcrumb .ck.cur { cursor: default; }
.catcrumb .ck.ph { color: #8a93a3; }
.catcrumb .ck.cur { color: #1f2937; font-weight: 600; }

/* ---- 窄屏: 卡密模板页第三栏最小 460px, 从 ≤1280px 就整行下落(1121~1280px 之间不落会挤出一整页横向滚动条);
     分类管理页两栏在 ≤1120px 也整行下落。两页都不出现整页横向滚动。 ---- */
@media (max-width: 1280px) {
  .ccat-split.ctpl-split { flex-wrap: wrap; }
  .ccat-split.ctpl-split > .ccat-main { flex: 1 1 100%; min-width: 0; }
}
@media (max-width: 1120px) {
  .ccat-split { flex-wrap: wrap; }
  .ccat-split > .ccat-main { flex: 1 1 100%; min-width: 0; }
  .ccat-split > .catcol, .ccat-split > .ccat-side { min-height: 0; }
}

/*===========================================================================
  批次18(追加1): 顶栏不再显示「页面标题 + 副标题」
  现场: index.html 的 .topbar > .ttl > h2#pageTitle + p#pageSub, 由 app.js 的 setHeader 填。
  这里只用 CSS 把它整块隐藏 —— **不动 public/app.js**:
    · document.title(浏览器标签页标题) 与 setHeader 的行为完全不变, 只是不在页面里显示;
    · .topbar 本身是 height:62px / flex:0 0 62px 固定高, 隐藏 .ttl 不会塌陷、不会多出空白;
    · 原来 .ttl 靠 flex:1 把 #globalActs / #pageActs / #userBox 顶到最右, 隐藏后改用等价的 ::before
      弹性占位顶替(伪元素在 flex 容器里就是一个 flex 项), 所以右侧那几块的位置一像素都不动。
===========================================================================*/
.topbar .ttl { display: none; }

/*===========================================================================
  批次19: 「卡密管理」页重写 —— 顶部两行查询区 + 下方「卡密详情」表
  外观口径: 白底 / 圆角 12px / 1px var(--border) / 极淡阴影; 表行高 40px;
            hover #f5f7fb, 选中行 #eef4ff(文字 #1d4ed8), 过渡 .15s。
  查询区 = 5 列网格 × 2 行:
    第 1 行 所属软件 / 单码 / 充值过期(占 2 列, 起 → 止 两个日期) / 是否售出
    第 2 行 是否已用 / 是否代理制卡 / 单码状态 / 批次号 / 搜索 + 重置
  窄屏: ≤1500px 收 4 列, ≤1120px 收 2 列(日期范围占整行), ≤760px 单列 —— 都不出现整页横向滚动。
  只新增 .gm* 类, 不覆盖 .ccat-* / .ctpl-* / .card / .tbl 等既有样式。
==========================================================================*/
.gmcard { border-radius: 12px; box-shadow: 0 1px 2px rgba(16, 24, 40, .04); }
.gmfilters { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px 18px; align-items: center; min-width: 0; }
.gmf { display: flex; align-items: center; gap: 8px; min-width: 0; }
.gmf > .gmflb { flex: 0 0 92px; margin: 0; text-align: right; font-size: 13px; color: var(--fg-dim); white-space: nowrap; }
.gmf .sel, .gmf .inp { flex: 1 1 auto; min-width: 0; height: 34px; }
.gmf-range { grid-column: span 2; }
.gmrange { display: flex; align-items: center; gap: 8px; flex: 1 1 auto; min-width: 0; }
.gmrange .inp { flex: 1 1 0; min-width: 0; }
.gmrange .sep { flex: 0 0 auto; font-size: 12px; color: var(--fg-dim2); }
.gmf-btns { justify-content: flex-end; gap: 8px; }

.gmtbl { max-height: none; }
.gmtbl table.tbl { font-size: 13px; }
.gmtbl table.tbl thead th { background: #f7f9fc; color: var(--fg-dim); }
.gmtbl table.tbl tbody tr { transition: background-color .15s; }
.gmtbl table.tbl tbody tr:hover td { background: #f5f7fb; }
.gmtbl table.tbl tbody tr.sel td { background: #eef4ff; }
.gmtbl table.tbl tbody tr.sel td:first-child { background: #eef4ff; box-shadow: none; }
.gmtbl table.tbl tbody td { height: 40px; }

@media (max-width: 1500px) { .gmfilters { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 1120px) { .gmfilters { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 760px) {
  .gmfilters { grid-template-columns: minmax(0, 1fr); }
  .gmf-range { grid-column: auto; }
}

/*===========================================================================
  动作按钮行(批次19 定稿): 按需求落在**查询区正下方**、与两行筛选同处一个内容块,
  本页 #pageToolbar 保持为空(不往页面顶部再冒一排重复按钮)。
==========================================================================*/
.gmactions {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border);
}
.gmactions .btn { flex: 0 0 auto; }
.gmactions-hint { font-size: 12px; margin-left: 2px; }

/*===========================================================================
  批次20(样式代理 D): ① 搜索面板 select 文字对齐 ② 顶栏右侧整组靠最右
                      ③ 账号唯一标识小徽标
  只动 public/style.css 与 public/gen-modal.css; 不改任何 .js, 不改 DOM 结构。
==========================================================================*/

/* ---- ① 卡密管理页搜索面板(#/cardmgmt/list)的 <select> 文字偏下 ----
   实测(CDP dsf=4 墨迹扫描, 窗口内缩与原验收脚本一致):
     修前  select 墨迹 顶 148.25 / 中心 154.13 / 高 11.75
           input  墨迹 顶 146.25 / 中心 153.13 / 高 13.75   → 中心低 1.00px、字高小 2.00px。
   成因: .sel 是 height:34px + 上下 padding:8px → 内容盒只有 34−2−16 = 16px, 小于 14px 字号的
     行盒(约 19px)。Chrome 对 select 的内部文本按「行盒从内容盒顶起排」, 而 input 是「在内容盒里
     居中」, 于是 select 整体下沉约 1px, 文字上下沿还被内容盒裁掉 → 墨迹变矮。
   修法: 只把本页 select 的上下内边距归零。内容盒回到 32px, Chrome 内部改为居中, 文字与同行/同列
     input 的墨迹中心对齐。高度/宽度/左右内边距/边框/字号全部不变 → 面板与表格几何零变化。
   注: line-height 在 Chrome 的 <select> 上实测不生效(16/28/32/34px 结果逐像素相同), 故不写。
   范围: 只作用于 .gmf .sel(搜索面板那 5 个下拉); 其它位置的 select 不在本次验收范围, 不动。 */
.gmf .sel { padding-top: 0; padding-bottom: 0; }

/* ---- ② 顶栏: 「3 个常驻按钮 + 用户头像」整组推到最右 ----
   现场: .topbar 是 flex, 子元素为 .ttl(display:none) / #globalActs / #pageActs(当前恒 0×0) /
     #userBox; 主轴未设置(justify-content:normal) → 整组贴左, #userBox 右边缘 746.73, 右侧空 753px。
   修法(4 处, 互相不冲突):
     1) .topbar 主轴设为 flex-end;
     2) 右侧组的第一个成员 #globalActs 再加 margin-left:auto —— 与 1) 原理等价但互为兜底:
        即使某页面 #globalActs 被 display:none, flex-end 仍然把余下成员贴右;
     3) #pageActs 为空时不生成盒子, 否则它两侧的两个 flex 间隙会把按钮组和头像隔开 16px;
     4) 清掉 .global-acts 右侧那截「分隔条」(padding-right:14 + border-right:1 + margin-right:4),
        并把 .topbar 的 gap 14px 改成 8px —— 挂载后按钮组与头像之间只剩这一个间隙,
        契约要求 8px(实测 gapToAvatar = 14+1+4+8+8 = 47px 记为修前值)。
        #pageActs 恒空时那截分隔条右侧已无第二组按钮可分隔, 故删掉; 空 #pageActs 用 3) 收敛。
        (窄屏 media query 里 .global-acts 本来就已清 padding-right/border-right, 语义一致。)
   不动: .topbar 高度 62px、.ttl 仍 display:none(#pageTitle/#pageSub 保持 0×0 且文本保留)、
     #pageViewTabs 的高度与 .topbar → #pageViewTabs → .appbar 的 DOM 顺序。
   有 #pageActs 内容时整组仍靠右: 见 1)+2) —— 推理性保证, 未实测(#pageActs 当前无法构造非空)。 */
.topbar { justify-content: flex-end; gap: 8px; }
.topbar > #globalActs { margin-left: auto; padding-right: 0; margin-right: 0; border-right: 0; }
.topbar > #pageActs:empty { display: none; }

/* ---- ③ 账号唯一标识(3 位大写)小徽标 ----
   用法: <span class="b20ident">ABC</span> [+ <button class="b20ident-copy">复制</button>]
   口径: 等宽字体 / 浅色底 / 1px 边框 / 圆角 6px / 字号 12px / 字距 .5px; 色值全部沿用 :root 变量。 */
.b20ident {
  display: inline-block; vertical-align: middle;
  font-family: var(--mono); font-size: 12px; letter-spacing: .5px; line-height: 18px;
  padding: 0 6px; border: 1px solid var(--border); border-radius: 6px;
  background: #f3f5f9; color: var(--fg-dim); white-space: nowrap;
}
.b20ident-copy {
  display: inline-flex; align-items: center; justify-content: center;
  height: 20px; margin-left: 5px; padding: 0 7px;
  font-family: inherit; font-size: 11.5px; line-height: 1; color: var(--fg-dim);
  background: #fff; border: 1px solid var(--border); border-radius: 6px;
  cursor: pointer; vertical-align: middle; white-space: nowrap;
  transition: background-color .15s, color .15s, border-color .15s;
}
.b20ident-copy:hover { background: var(--blue-soft); color: var(--blue); border-color: #c7dbff; }
.b20ident-copy:active { background: #e2edff; }
.b20ident-copy[disabled] { color: var(--fg-dim2); background: #fbfcfe; cursor: default; }
/* 徽标落在表格单元格里时不留多余行高 */
table.tbl td .b20ident { line-height: 18px; }

/*=============================================================================
  b21 UI kit — 「配置软件」弹窗表单可复用纯 CSS 组件
  本块仅【新增】选择器，全部以 .b21- 前缀命名，不改动上方任何既有规则。
  组件: .b21-sw / .b21-num / .b21-timerange / .b21-unit / .b21-help
        .b21-panel (+ .b21-panel-hd / .b21-panel-grid) / .b21-frow
  HTML 结构契约 + 实测尺寸: Z:\WLLOS\_tools\out\b21-ui-kit.md
  纯 CSS，无 JS 依赖；禁用态用 :disabled 与 :has()（Chrome/Edge 105+）。
=============================================================================*/

/* ---- 1) 开关滑块 40x22，未选灰 / 选中蓝 / 滑块白 ---- */
.b21-sw {
  position: relative; display: inline-flex; align-items: center; flex: 0 0 auto;
  vertical-align: middle; line-height: 1; cursor: pointer;
}
.b21-sw > input {
  position: absolute; width: 1px; height: 1px; margin: 0; padding: 0;
  opacity: 0; pointer-events: none;
}
.b21-sw > span {
  position: relative; display: block; width: 40px; height: 22px; border-radius: 999px;
  background: #cbd3e1; transition: background-color .18s ease;
}
.b21-sw > span::after {
  content: ""; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px;
  border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(20, 28, 44, .3);
  transition: transform .18s ease;
}
.b21-sw > input:checked + span { background: var(--blue); }
.b21-sw > input:checked + span::after { transform: translateX(18px); }
.b21-sw > input:focus-visible + span { box-shadow: 0 0 0 3px var(--blue-soft); }
.b21-sw > input:disabled + span { opacity: .5; }
.b21-sw:has(> input:disabled) { cursor: not-allowed; }

/* ---- 2) 数字步进器 总高 34，按钮 28x34 满高，输入居中无原生 spinner ---- */
.b21-num {
  display: inline-flex; align-items: stretch; height: 34px; overflow: hidden;
  border-radius: 6px; background: #fff;
  box-shadow: inset 0 0 0 1px var(--border-2); transition: box-shadow .15s;
}
.b21-num:focus-within { box-shadow: inset 0 0 0 1px var(--blue), 0 0 0 3px var(--blue-soft); }
.b21-num > button {
  flex: 0 0 28px; width: 28px; height: 34px; padding: 0; border: 0;
  background: #f7f9fc; color: var(--fg-dim); font-family: inherit; font-size: 15px;
  line-height: 1; cursor: pointer; transition: background-color .15s, color .15s;
}
.b21-num > button:hover { background: #eef2f9; color: var(--blue); }
.b21-num > button:active { background: var(--blue-soft); }
.b21-num > button:disabled { background: #fafbfd; color: var(--fg-dim2); cursor: not-allowed; }
.b21-num > input {
  flex: 1 1 auto; min-width: 0; width: 64px; height: 34px; margin: 0;
  border: 0; border-left: 1px solid var(--border); border-right: 1px solid var(--border);
  background: transparent; color: var(--fg); font: inherit; font-size: 13.5px;
  text-align: center; outline: none;
}
.b21-num > input::-webkit-outer-spin-button,
.b21-num > input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.b21-num > input[type="number"] { -moz-appearance: textfield; appearance: textfield; }

/* ---- 3) 时间段输入 一行 34，圆角 6，1px 边框，两个 time 等宽 ---- */
.b21-timerange {
  display: flex; align-items: center; gap: 6px; width: 100%; height: 34px;
  padding: 0 8px 0 9px; border: 1px solid var(--border-2); border-radius: 6px;
  background: #fff; transition: border-color .15s, box-shadow .15s;
}
.b21-timerange:focus-within { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); }
.b21-timerange > .ico { flex: 0 0 auto; font-size: 13px; line-height: 1; color: var(--fg-dim2); }
.b21-timerange > .b21-time {
  flex: 1 1 0; min-width: 0; width: 100%; height: 30px; margin: 0; padding: 0 2px;
  border: 0; border-radius: 0; background: transparent; color: var(--fg);
  font: inherit; font-size: 13px; text-align: center; outline: none;
}
/* 批次29: 批次28 在这个共用组件上**就地**改了分隔符(半角 '-' -> en dash '–' + 抬对比度),
   连带改了「配置软件」弹窗试用页签里同款时间段控件的外观。用户已拍板: 「配置软件」弹窗
   冻结成批次27 以前的旧样子, 批次28 的观感只保留在「配置详情」弹窗 —— 故本规则恢复旧参数,
   批次28 那两笔(含 ::before 画 '–')整体挪到文末 .ccd-pane 限定的 T6 段。 */
.b21-timerange > .sep { flex: 0 0 auto; color: var(--fg-dim2); font-size: 13px; }
.b21-timerange .b21-time::-webkit-datetime-edit { padding: 0; }
.b21-timerange .b21-time::-webkit-calendar-picker-indicator { opacity: .5; cursor: pointer; margin: 0; padding: 0; }
.b21-timerange .b21-time::-webkit-calendar-picker-indicator:hover { opacity: 1; }

/* ---- 4) 带单位下拉：输入自适应 + 下拉固定 72，中间无缝（批次27 以前的旧参数）----
   批次28 曾在这里**就地**改三笔(共用规则, 未加 .ccd-pane 限定):
     · .b21-unit 加 gap:8px(「+ 号与下拉之间留一条缝」, 整组宽度仍是 200px, 8px 从下拉里扣);
     · 下拉 72 -> 64, 并去掉 border-left:0 / border-*-left-radius:0(缝两侧各成一个完整圆角盒子);
     · + 按钮补回右侧圆角。
   这三笔同时改了「配置软件」弹窗试用页签的同款控件。用户已拍板: 该弹窗冻结成批次27 以前的
   旧样子, 批次28 的观感只保留在「配置详情」弹窗 —— 故这里恢复旧参数, 批次28 那三笔整体挪到
   文末 .ccd-pane 限定的 T6 段。
   另: 下拉的内边距不再就地覆盖, 交回基础 .sel 的 8px/11px —— 批次28 在此写死的
   padding-right:6px 会让「配置软件」弹窗里同款下拉的右侧内边距与全站其它 .sel 不一致。
   作用域: 只有带单位的行(trial 面板的 试用总开关周期/时长)用到 .b21-unit,
   两个弹窗各 2 处 = 4 个实例; 其它任何控件都不含 .b21-unit / .b21-unit-sel。 */
.b21-unit { display: flex; align-items: stretch; width: 100%; }
.b21-unit > .inp {
  flex: 1 1 auto; min-width: 0; width: auto;
  border-top-right-radius: 0; border-bottom-right-radius: 0;
}
.b21-unit > .b21-unit-sel {
  flex: 0 0 72px; width: 72px;
  border-left: 0; background: #f7f9fc;
  border-top-left-radius: 0; border-bottom-left-radius: 0;
}
/* ⚠ 必须是 background-color 而不是 background 简写: 简写会把批次28 画在下拉右侧的
   箭头(SVG 背景图)重置成 none, 悬停时箭头会消失。 */
.b21-unit > .b21-unit-sel:hover { background-color: #f1f5fb; }
.b21-unit > .b21-unit-sel:focus { position: relative; z-index: 2; box-shadow: 0 0 0 3px var(--blue-soft); }

/* ---- 5) 问号帮助图标 14 圆，hover 变蓝（说明走原生 title） ---- */
.b21-help {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: 14px; height: 14px; border: 1px solid var(--border-2); border-radius: 50%;
  background: #fff; color: var(--fg-dim2); font-size: 10px; font-weight: 700;
  font-style: normal; line-height: 1; cursor: help; vertical-align: middle;
  user-select: none; transition: color .15s, border-color .15s, background-color .15s;
}
.b21-help:hover { color: var(--blue); border-color: var(--blue); background: var(--blue-soft); }

/* ---- 6) 子面板 + 两列网格（窄屏单列） ---- */
.b21-panel { padding: 10px 12px 12px; border: 1px solid var(--border); border-radius: 8px; background: #fff; }
.b21-panel + .b21-panel { margin-top: 12px; }
.b21-panel > .b21-panel-hd {
  display: flex; align-items: center; gap: 6px; padding-bottom: 8px; margin-bottom: 10px;
  border-bottom: 1px solid var(--border); font-size: 13px; font-weight: 700; color: var(--fg);
}
.b21-panel-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
@media (max-width: 760px) { .b21-panel-grid { grid-template-columns: 1fr; } }

/* ---- 7) 行内字段行：标签 110 右对齐 + 控件占满，行距 12 ---- */
.b21-frow { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 12px; }
.b21-frow:last-child { margin-bottom: 0; }
.b21-frow > .lb {
  flex: 0 0 110px; width: 110px; min-height: 34px; display: flex; align-items: center;
  justify-content: flex-end; gap: 4px; text-align: right; font-size: 13px;
  font-weight: 500; color: var(--fg-dim); line-height: 1.4;
}
.b21-frow > .lb .b21-help { margin-left: 2px; }
.b21-frow > .ctl { flex: 1 1 auto; min-width: 0; }
.b21-frow > .b21-help { align-self: center; }

/* ---- 8) 组合辅助：网格内小节标题 ---- */
.b21-mini { font-size: 12px; color: var(--fg-dim); margin-bottom: 6px; }

/*=============================================================================
  批次21 追加 —— 「配置软件」弹窗(#appCfgModal)专属布局
  纯新增, 不改上方任何既有规则; 类名继续 b21- 前缀;
  控件本身的尺寸契约仍由上面 1651-1774 那段负责, 这里只做容器与列表排版。
=============================================================================*/
/* 页签条：复用站点既有 .tabs 观感, 只补同页「配置详情」弹窗那套边距 */
.b21-tabs { margin: -4px -20px 16px; padding: 0 20px; flex-wrap: wrap; row-gap: 2px; }
/* 5 个面板：非当前面板由 JS 打 hidden, 必须真的不显示 */
.b21-pane[hidden] { display: none; }
/* 左右两栏面板(试用设置: 左「按时间试用」/ 右「按次数试用」) */
.b21-panel-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; align-items: start; }
.b21-panel-row > .b21-panel + .b21-panel { margin-top: 0; }
/* 密钥行：只读输入 + 复制 / 重置 */
.b21-krow { display: flex; align-items: center; gap: 8px; }
.b21-krow > .inp { flex: 1 1 auto; min-width: 0; }
.b21-krow > .btn { flex: 0 0 auto; }
/* 静态接口清单 */
.b21-doc { padding: 10px 0; border-top: 1px dashed var(--border); }
.b21-doc:first-child { padding-top: 0; border-top: 0; }
.b21-doc:last-child { padding-bottom: 0; }
.b21-doc-hd { display: flex; align-items: center; gap: 8px; }
.b21-doc-hd code { font-family: var(--mono); font-size: 12.5px; color: var(--fg); word-break: break-all; }
.b21-doc-m { flex: 0 0 auto; padding: 1px 6px; border-radius: 4px; background: var(--blue-soft); color: var(--blue); font-size: 11px; font-weight: 700; }
.b21-doc-d { margin: 4px 0 7px; font-size: 12.5px; color: var(--fg-dim); line-height: 1.6; }
.b21-doc .code { max-height: 132px; }
/* 在线试调的结果区(在 .code 深色底上按正文换行) */
.b21-out { margin-top: 10px; white-space: pre-wrap; word-break: break-all; }
/* 步进器 + 单位下拉的组合(试用设置的「可重复试用周期 / 每次可试用时长」)
   —— .b21-unit 容器里放 .b21-num: 步进器撑满左侧、右边接 72px 单位下拉, 中间无缝
   (批次27 以前的旧样子)。
   批次28: 这里曾把 .b21-num 的右侧圆角归零一并删掉(为了让它成为独立圆角盒子, 配合中间
   8px 缝)。批次29 按用户拍板把「配置软件」弹窗冻结成旧样子, 故恢复这两条归零; 批次28
   的「独立圆角盒子」改为 .ccd-pane 限定, 见文末 T6 段。
   只加组合规则, 不改 .b21-num(34 高)与 .b21-unit-sel(72x36)自身的尺寸契约。 */
.b21-unit > .b21-num {
  flex: 1 1 auto; min-width: 0; align-self: center;
  border-top-right-radius: 0; border-bottom-right-radius: 0;
}

@media (max-width: 760px) {
  .b21-panel-row { grid-template-columns: 1fr; }
  .b21-panel-row > .b21-panel + .b21-panel { margin-top: 12px; }
}

/*=============================================================================
  批次22 追加 —— 「配置软件」弹窗新增第6个页签「API接口信息」专属排版
  纯新增, 不改上方任何既有规则; 类名用 b22- 前缀, 与 b21- 段落互不干扰。
============================================================================*/
/* 键值行：接口基址 / 鉴权方式 / 调用密钥 */
.b22-kv { display: grid; gap: 7px; }
.b22-kv-row { display: flex; align-items: baseline; gap: 10px; font-size: 13px; }
.b22-kv-row > .k { flex: 0 0 76px; text-align: right; color: var(--fg-dim); font-size: 12.5px; }
.b22-kv-row > code { font-family: var(--mono); font-size: 12.5px; color: var(--fg); word-break: break-all; }
/* 弱化说明文字 */
.b22-dim { font-size: 12.5px; color: var(--fg-dim2); line-height: 1.6; }
/* 接口清单：按 group 分组的小标题 + 表 */
.b22-doc-group { margin-top: 12px; }
.b22-doc-group:first-child { margin-top: 0; }
.b22-doc-group > .hd { font-size: 12.5px; font-weight: 600; color: var(--fg-dim); margin-bottom: 6px; }
.b22-doc-tbl { width: 100%; border-collapse: collapse; font-size: 12.5px; table-layout: fixed; }
.b22-doc-tbl th {
  text-align: left; padding: 7px 9px; background: #f7f9fc; font-size: 12px;
  color: var(--fg-dim); font-weight: 600; border-bottom: 1px solid var(--border);
}
.b22-doc-tbl td { padding: 7px 9px; border-bottom: 1px solid #eef1f6; vertical-align: top; line-height: 1.6; }
.b22-doc-tbl tr:last-child td { border-bottom: 0; }
.b22-doc-tbl code { font-family: var(--mono); font-size: 12px; word-break: break-all; }
.b22-doc-tbl .m {
  display: inline-block; padding: 1px 6px; border-radius: 4px;
  background: var(--blue-soft); color: var(--blue); font-size: 11px; font-weight: 700;
}
.b22-doc-tbl .m.post { background: #e8f7ee; color: #1a7f45; }
/* 入参 / 返回字段 子表 */
.b22-doc-sub { margin-top: 7px; }
.b22-doc-sub > .t { font-size: 11.5px; color: var(--fg-dim2); margin-bottom: 3px; }
.b22-doc-sub .b22-doc-tbl { border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
.b22-doc-sub .b22-doc-tbl td { padding: 4px 8px; font-size: 12px; }
/* 示例调用折叠块 */
.b22-doc-sample { margin-top: 7px; }
.b22-doc-sample > summary { font-size: 12px; color: var(--blue); cursor: pointer; }
.b22-doc-sample > .code { margin-top: 6px; max-height: 128px; }
/* 必填校验失败的行高亮(批次22 起红星真的会拦保存) */
.b22-invalid > .ctl > * { border-color: var(--red) !important; box-shadow: 0 0 0 3px #fee2e2 !important; }

/*=============================================================================
  批次24 —— 「绑定设置 / 限开设置 / 试用设置」三个策略面板的**行对齐 + 标签换行**修复
  · 纯覆盖, 不删除上方任何既有规则: 整段注释掉/删掉即回到批次21 的观感, 便于回滚。
  · 作用域: .b21-pane[data-acfgpane=bind|limit|trial]  (「配置软件」弹窗)
          + .ccd-pane[data-ccpane=绑定设置|限开设置|试用设置] (分类「配置详情」弹窗)
    两处共用 acfgFrow 生成的 DOM, 因此两边同时生效, 这是预期行为。
  · 实测依据: Z:\WLLOS\_tools\out\b24-css-check.json (CDP 真机渲染)
      单元格宽 471px; 标签单行需求最宽 124.7px(「同IP可试用设备数」含 ? 图标)
      19 字「试用有效时间内重复登录不增加试用次数」单行需求 266px
  · 修三件事:
      (a) .b21-frow 由 flex 改 grid(标签列 172px + 控件列 1fr) → 所有行左边缘一致
      (b) 单行输入类控件统一 260px(不再无脑 100%); 开关 40 / 步进器 120 保持自身宽度左对齐
      (c) align-items:center → 标签换行时控件仍垂直居中, 不再贴顶
  注意: 只覆盖 .b21-frow, 完全不碰 .frow(基本信息表单) 与 .tbl(表格)。
=============================================================================*/
/* (a) 行容器: grid 两列 —— 标签 172 / 控件 1fr, 垂直居中。
   172 的由来(均为 CDP 真机实测):
     · 除最长那条外, 18 条标签的单行需求上限是 124.7px(「同IP可试用设备数」含 ? 图标);
     · 最长的 19 字「试用有效时间内重复登录不增加试用次数」单行需 266px, 任何 ≤201 的
       标签列都无法让它单行(试用户签行宽 458 - 控件 260 - gap 10 = 上限 188);
       132 时它会折成 3 行, 且第 3 行只剩一个孤零零的 ? 图标, 观感最差。
     · 取 172 后该标签降为 2 行、? 归位, 其余 17 行仍全部单行。
   关键: 172 + gap10 + 控件 260 = 442 ≤ 试用户签行宽 458(余 16px), 不溢出;
        控件宽度与行内偏移量对全部 18 行保持唯一值 → 对齐零误差。 */
.b21-frow {
  display: grid; grid-template-columns: 172px minmax(0, 1fr);
  align-items: center; gap: 10px;
}
/* 列宽交给 grid, 取消批次21 写死的 110px(否则 grid 列被内容压回 110);
   min-height:34px 保留作行高兜底, 保证单行标签的行间距一致。
   刻意**不加** white-space:nowrap: 万一某条标签超出 172, 宁可换成 2 行并居中,
   也绝不允许它 nowrap 后向左溢出压到相邻卡片/控件上。 */
.b21-frow > .lb { width: auto; }
/* (b) 控件宽度分级: 单行输入类统一 260px, 左边缘全部落 142px。
   只作用于三个策略面板 —— 「配置软件」弹窗的 basic/api 页签不受影响。 */
:is(.b21-pane[data-acfgpane="bind"], .b21-pane[data-acfgpane="limit"], .b21-pane[data-acfgpane="trial"],
    .ccd-pane[data-ccpane="绑定设置"], .ccd-pane[data-ccpane="限开设置"], .ccd-pane[data-ccpane="试用设置"])
  .b21-frow > .ctl > :is(.inp, .sel, .txa, .b21-timerange, .b21-unit) { width: 260px; max-width: 100%; }
/* 开关(40) / 步进器(120) 仍按 kit 契约的自身宽度左对齐, 只把 inline-flex 改成块级 flex,
   去掉行内基线下沉 → .ctl 高度 = 控件高度, 与 align-items:center 配合才是真的居中 */
:is(.b21-pane[data-acfgpane="bind"], .b21-pane[data-acfgpane="limit"], .b21-pane[data-acfgpane="trial"],
    .ccd-pane[data-ccpane="绑定设置"], .ccd-pane[data-ccpane="限开设置"], .ccd-pane[data-ccpane="试用设置"])
  .b21-frow > .ctl > .b21-sw { display: flex; width: 40px; align-items: center; }
:is(.b21-pane[data-acfgpane="bind"], .b21-pane[data-acfgpane="limit"], .b21-pane[data-acfgpane="trial"],
    .ccd-pane[data-ccpane="绑定设置"], .ccd-pane[data-ccpane="限开设置"], .ccd-pane[data-ccpane="试用设置"])
  .b21-frow > .ctl > .b21-num { display: flex; width: 120px; }
/* (c) 已废弃并删除 —— 曾计划用 .b21-frow--wide(276px)单独放宽最长那条标签到单行。
   实测该做法会把这一行的控件右移 286px, 与同列其余 17 行不再对齐 —— 恰恰是本批要修的
   「不齐」；且 276 + 260 已超出 471 的行宽, 会挤到相邻卡片。用户诉求里「不齐」优先于
   「不换行」, 故改用「标签列整体放宽到 172」的方案: 全部 18 行共用一个偏移量。
   全项目无任何 JS 引用此类, 一并删除以免后人误用。 */

/*=============================================================================
  批次25 —— 两个共用策略弹窗(分类「配置详情」/ 软件「配置软件」)的 6 项整改
  · 纯追加覆盖: 上方任何规则(含批次24)一条都没删; 把本段整体注释掉即回到批次24 观感。
  · 作用域与批次24 完全一致 —— 只覆盖这三个策略面板:
      .b21-pane[data-acfgpane=bind|limit|trial]           («配置软件»弹窗)
      .ccd-pane[data-ccpane=绑定设置|限开设置|试用设置]     (分类「配置详情»弹窗)
    两个弹窗的 基本信息/配置导入/API 页签, 以及另外 10 个 .modal.wide 弹窗, 一律不受影响。
  · 实测依据: Z:\WLLOS\_tools\out\b25-css-check.json (CDP 真机渲染, pre/post 两轮)
  · 修六件事:
      A 弹窗单独加宽 1020 -> 1180
      B 标签列 172 -> 272(让 18 字长标签单行)
      C 盒状控件宽度统一 200px(原 260/120 混排, 右边缘参差)
      D 试用两个子卡严格等宽
      E 子卡组与上方内容留出上边距
      F 选项卡吸顶(滚动时不消失)
=============================================================================*/

/* ---- A. 弹窗加宽 -----------------------------------------------------------
   绝不能动 .modal.wide{max-width:1020px} —— 全站 22 个弹窗共用它。
   用 :has() 只命中「内部有这两套页签」的那两个弹窗(Chrome 105+ 支持)。
   实测: 两个弹窗 offsetWidth 1020 -> 1180, 其余弹窗不变。
   批次28: 两个弹窗拆成两条独立规则各自定宽 ——
     · 分类「配置详情」(.ccd-tabs) 1180 -> 1000。「配置导入」页签要放 200px 左栏 +
       右表, 1000 是够用的; 同时批次27 之后正文已改成「标签贴文字」, 1180 的余量过剩。
     · 软件「配置软件」(.b21-tabs) 保持 1180 不动 —— 它有 API 密钥行等更宽的内容,
       本批未提整改, 行为与批次25 逐像素一致。
   两条规则各自独立, 谁都不会命中对方; 其余 20 个 .modal.wide 仍 1020。 */
.modal.wide:has(.ccd-tabs) {
  max-width: 1000px;
}
.modal.wide:has(.b21-tabs) {
  max-width: 1180px;
}

/* ---- B. 标签列 定宽 272 -> max-content --------------------------------------
   批次25 用「定宽 272px 标签列」换「最长标签不换行」, 代价是短标签与自己控件之间
   空出一大段(实测: 是否启用 230px、绑定模式 216.7px、登录用户数限制 162.3px)。
   批次27 用户诉求改为「控件要紧贴标签文字, 只留一点小间距」——
   改 max-content: 列宽 = 该行标签的自然宽度, 行内恒等于 gap(10px), 不再有空档。
   实测(CDP, 18 行): 间隙 216.7/230/162.3… -> 全部 10px; 标签仍全部单行;
   控件仍 200px(C 段不受影响, 第 2 列 minmax(0,1fr) 未变)。
   代价(用户已确认接受): 各行控件左边缘不再纵向对齐成一条线 —— 这正是「控件贴文字」的必然结果。
   不加 white-space:nowrap: max-content 本身就是「不折行的理想宽度」, 遇到超长标签时
   列宽会跟着变宽, 由第 2 列 minmax(0,1fr) 保证不把卡片撑破(实测 scrollWidth<=clientWidth)。 */
:is(.b21-pane[data-acfgpane="bind"], .b21-pane[data-acfgpane="limit"], .b21-pane[data-acfgpane="trial"],
    .ccd-pane[data-ccpane="绑定设置"], .ccd-pane[data-ccpane="限开设置"], .ccd-pane[data-ccpane="试用设置"])
  .b21-frow {
  grid-template-columns: max-content minmax(0, 1fr);
}

/* ---- C. 盒状控件统一 200px -------------------------------------------------
   用户诉求: 下拉框/输入框/步进器宽度不一, 右边缘参差不齐, 要「靠左对齐」。
   批次24 是 sel/txa/timerange/unit=260 而 num=120, 同一列右边缘出现 537/677/1033/1160/1173
   多个值 -> 视觉上就是参差。统一成一个宽度后, 同一列内所有盒状控件的
   getBoundingClientRect().left 与 .right 各自只有一个唯一值(本批验收指标)。
   取 200(>=190)而不是 260: 用户要的是"齐", 不是"更宽"; 200px 足够放下
   「绑定模式」「达到登录上限后」这类选项文本, 也给 1180 弹窗的两列布局留足余量。
   .b21-num 一并并入 200 —— 此前 120 与同列 260 的下拉框右边缘差 140px, 是最明显的错位来源。 */
:is(.b21-pane[data-acfgpane="bind"], .b21-pane[data-acfgpane="limit"], .b21-pane[data-acfgpane="trial"],
    .ccd-pane[data-ccpane="绑定设置"], .ccd-pane[data-ccpane="限开设置"], .ccd-pane[data-ccpane="试用设置"])
  .b21-frow > .ctl > :is(.inp, .sel, .txa, .b21-timerange, .b21-unit, .b21-num) {
  width: 200px;
  max-width: 100%;
}

/* ---- C2. 开关为什么保持 40px、不参与统一 ------------------------------------
   开关是二态控件(kit 契约 40x22 的滑块), 拉宽到 200px 只会把滑块拉变形,
   并不会让它"更整齐"。它的左边缘与同列其它盒状控件完全相同(实测同一列 cL 取值唯一),
   用户要的「靠左对齐」已经满足; 这里显式声明一遍, 便于后人一眼看到全部宽度规则。 */
:is(.b21-pane[data-acfgpane="bind"], .b21-pane[data-acfgpane="limit"], .b21-pane[data-acfgpane="trial"],
    .ccd-pane[data-ccpane="绑定设置"], .ccd-pane[data-ccpane="限开设置"], .ccd-pane[data-ccpane="试用设置"])
  .b21-frow > .ctl > .b21-sw {
  display: flex;
  width: 40px;
}

/* ---- D. 试用两个子卡(按时间试用 / 按次数试用)严格等宽 ----------------------
   原来 .b21-panel-row 是 grid 2xminmax(0,1fr)。改成 flex 布局:
   两个子卡 flex:1 1 0 + min-width:0 -> 无论卡内内容多少, 宽度严格相等,
   卡内标签/控件从同一偏移量起步(实测两卡相对 left 与 width 各自唯一)。
   flex-wrap:wrap 保留窄屏下的换行能力。 */
:is(.b21-pane[data-acfgpane="bind"], .b21-pane[data-acfgpane="limit"], .b21-pane[data-acfgpane="trial"],
    .ccd-pane[data-ccpane="绑定设置"], .ccd-pane[data-ccpane="限开设置"], .ccd-pane[data-ccpane="试用设置"])
  .b21-panel-row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: flex-start;
}
:is(.b21-pane[data-acfgpane="bind"], .b21-pane[data-acfgpane="limit"], .b21-pane[data-acfgpane="trial"],
    .ccd-pane[data-ccpane="绑定设置"], .ccd-pane[data-ccpane="限开设置"], .ccd-pane[data-ccpane="试用设置"])
  .b21-panel-row > .b21-panel {
  flex: 1 1 0;
  min-width: 0;
  margin-top: 0;
}
/* 原 grid 的窄屏规则(@media 760 下 grid-template-columns:1fr)对 flex 不再生效,
   这里补回等价行为: 窄屏时两张子卡上下堆叠、各占满一行(行间距由 gap:12px 提供)。 */
@media (max-width: 760px) {
  :is(.b21-pane[data-acfgpane="bind"], .b21-pane[data-acfgpane="limit"], .b21-pane[data-acfgpane="trial"],
      .ccd-pane[data-ccpane="绑定设置"], .ccd-pane[data-ccpane="限开设置"], .ccd-pane[data-ccpane="试用设置"])
    .b21-panel-row > .b21-panel {
    flex: 1 1 100%;
  }
}

/* ---- E. 子卡组上边距 -------------------------------------------------------
   用户诉求: 「按时间试用 / 按次数试用」这块边框区域要往上保持些边距。
   它上方是「是否开启试用 / 同IP可试用设备数」的网格, 原来紧贴在一起。 */
:is(.b21-pane[data-acfgpane="bind"], .b21-pane[data-acfgpane="limit"], .b21-pane[data-acfgpane="trial"],
    .ccd-pane[data-ccpane="绑定设置"], .ccd-pane[data-ccpane="限开设置"], .ccd-pane[data-ccpane="试用设置"])
  .b21-panel-row {
  margin-top: 16px;
}

/* ---- F. 选项卡吸顶 ---------------------------------------------------------
   用户诉求: 基本信息/配置导入/绑定设置/限开设置/试用设置 在正文滚动时始终停在顶部。
   两个弹窗的滚动容器都是 .modal > .body(padding-top:18px, overflow-y:auto)。

   ★ 批次26 实测更正(b26-polish-check.mjs, Chrome 152):
     原注释「padding-top 撑出的白色块把 body 顶部那 18px 内边距一并盖住」**不成立**。
     实测: body 边框盒顶 = 88, 页签条边框盒顶恒为 106(= 88 + padding-top 18),
     滚动 0/中/底三态完全不变 —— 也就是说页签条只是「停在内容盒顶」, body 的
     18px 内边距带(y = 89..105, 共 17px)没有任何东西覆盖, 滚动时正文(label.lb /
     textarea.txa / div.frow)就从这个缝里透出来。
     机理: 本浏览器把 sticky 的吸附基准取在滚动容器的**内容盒**顶(106), 不是
     padding 盒顶(88); 而 margin-top:-18px 让自然位置是 88, 比吸附基准更靠上,
     于是被吸附基准向下推回 106 —— 负 margin 与 padding 恰好互相抵消,
     既没改变可见位置, 也没盖住那条缝。

     修复选择: **不动页签条的任何几何**(top / margin-top / padding-top 全部保持原值,
     可见位置与正文首行位置实测零位移), 只用 ::before 在上面补一块 18px 高的白色护罩。
     没有改用「负 top + 更大 padding-top」, 是因为那个写法依赖上面这条「吸附基准在
     内容盒」的浏览器行为(规范取 padding 盒), 在不按此行为的浏览器里会把页签条
     整体上移 18px 顶到弹窗标题栏上; ::before 护罩与吸附基准无关, 各浏览器一致。

   左右外扩不动: 原有 -20px 负 margin + 20px 内边距保留, 底部分隔线仍通栏。
   z-index:6 —— 高于页签下方所有正文, 低于弹窗遮罩。 */
#appCfgTabs {
  position: sticky;
  top: 0;
  z-index: 6;
  background: #fff;
  /* .b21-tabs 是类选择器(0,1,0), id(1,0,0) 足以覆盖, 不需要 !important */
  margin-top: -18px;
  padding-top: 14px;
}
#appCfgTabs::before {
  /* 护罩: 盖住滚动容器 padding-top 那 18px(高度 = .modal > .body 的 padding-top),
     底边贴住页签条边框盒顶, 因此正文永远不会从页签条上方透出。
     必须是真实盒子(不能用 box-shadow/背景) —— 只有盒子参与命中测试。
     pointer-events 保持默认 auto, 缝里点到的是页签条本身, 不会误触正文控件。
     ⚠ 批次27: 本规则原先与 #ccdTabs::before 共用同一个块。批次27 把「配置详情」弹窗的
     .body padding-top 归零(见下方 H), 该弹窗已无缝隙可盖, 其护罩必须去掉(否则会盖到
     标题栏下沿 18px)。此处**只摘掉 #ccdTabs::before 这一半**, #appCfgTabs 的护罩
     与其它属性一字未动。 */
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 18px;
  background: #fff;
}
#ccdTabs {
  position: sticky;
  top: 0;
  z-index: 6;
  background: #fff;
  /* 该节点在 biz.js 里带内联 style="margin:-4px -20px 16px;padding:0 20px;border-bottom:…",
     内联样式普通选择器覆盖不了, 因此只对「上侧」这两个属性用 !important;
     左右的 -20px 外扩与 border-bottom 分隔线保持原样, 不受影响。
     批次27: margin-top 由 -18px 改为 0 —— 配合下面 H 段把本弹窗 .body 的 padding-top
     归零后, 页签条的「自然位置」与「吸顶位置」重合于标题栏下沿, 不再依赖 sticky 吸附
     去中和负 margin(旧写法的副作用: 未滚动时页签条上方留 18px 白缝, 需要护罩遮挡)。
     改后实测: 页签条顶 - 标题栏底 = 0px(滚动 0/中/底三态恒定), 底 18px 内边距保留。 */
  margin-top: 0 !important;
  padding-top: 14px !important;
}

/* ---- H. 「配置详情」弹窗: 页签贴标题栏 + 基本信息卡片分组 ---------------------
   H1 只作用于「配置详情」弹窗(靠 :has(.ccd-tabs) 收窄, 与 A 段同一手法):
      去掉 .body 的 18px 上内边距。全站共用的 .modal > .body{padding:18px 20px}
      (见本文件 810 行)一字未动, 其它 20 个弹窗不受影响; 左右 20px 与底 18px 保留。
   H2/H3 只作用于「配置详情」弹窗的「基本信息」页签: 4 张卡排成 2 行,
      并中和 .b21-panel + .b21-panel 的 12px 上边距 —— 否则第 2/4 张卡会比第 1/3 张
      低 12px(该规则在 1753 行, 批次25 只对 .b21-panel-row 里的卡片中和过)。 */
.modal.wide:has(.ccd-tabs) > .body { padding-top: 0; }
.ccd-pane[data-ccpane="基本信息"] .b21-panel-grid > .b21-panel + .b21-panel { margin-top: 0; }
.ccd-pane[data-ccpane="基本信息"] > .notice { margin-top: 12px; }

/* ---- G. 标签文字由「右对齐」改为「左对齐」 -----------------------------------
   ★ 这一条才是用户反复说的「靠左对齐」的真正落点 —— 批次24 与本批的 A~F 都只改了
     控件宽度, 从头到尾没动过标签本身。
   用户原话: 「绑定设置里的绑定模式和下方的选项都靠左对齐」
             「试用设置里的是否启用**文字**也是靠左对齐」(他点的是「文字」, 不是控件)
   批次21 kit 原设计是右对齐(见本文件 1764-1767), 批次24 也一直沿用:
       .b21-frow > .lb { … justify-content: flex-end; gap: 4px; text-align: right; }
   右对齐下各标签**右边缘**对齐、**左边缘参差**; 实测(视觉判读 b25-post-ccd-trial.png):
       按时间试用卡内:「是否启用」左边缘 x≈378, 「可试用时间段」x≈337
       按次数试用卡内:「是否启用」x≈944, 「可重复试用周期 / 每周期试用次数 /
                       每次可试用时长」x≈878, 最长那条 x≈747
   即长短不一的标签, 左边缘最大参差约 197px —— 这就是用户从批次24 起说的「有点不齐」。
   改为左对齐后: 标签列 272px 内所有标签**左边缘唯一**, 配合 C 的控件右边缘唯一,
   行内形成「标签左齐 + 控件左齐」的双齐观感。
   代价(预期内, 非缺陷): 短标签(如「是否启用」)与其控件之间会空出一段 —— 这是
   定宽标签列的必然结果, 也是同时满足「左边缘对齐」与「最长标签不换行」必须付的代价。
   回滚: 删掉本段即回到批次21 的右对齐。 */
:is(.b21-pane[data-acfgpane="bind"], .b21-pane[data-acfgpane="limit"], .b21-pane[data-acfgpane="trial"],
    .ccd-pane[data-ccpane="绑定设置"], .ccd-pane[data-ccpane="限开设置"], .ccd-pane[data-ccpane="试用设置"])
  .b21-frow > .lb {
  justify-content: flex-start;
  text-align: left;
}

/*=============================================================================
  [frontend-style] 批次26 · 分类管理页 / 卡密模板页 面板区域的右键「刷新页面」菜单
  #ccReloadMenu 与 #tabMenu / #ccatMenu 是**同一套浮层外观**(id 各自独立, 样式同组复制一份)。
  刻意不改上面那组 :is(#tabMenu,#ccatMenu) 选择器 —— 本批次规定只追加, 不动既有声明。
  回滚: 删掉本段即可(菜单本身仍会出现在正确坐标, 只是没有浮层外观)。
=============================================================================*/
:is(#ccReloadMenu) {
  position: fixed; z-index: 90; min-width: 148px; padding: 5px;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: 6px; box-shadow: 0 8px 24px rgba(10,18,32,.16);
}
:is(#ccReloadMenu) .tmi {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 12px; border-radius: 6px;
  font-size: 13px; color: var(--fg); cursor: pointer; white-space: nowrap;
  transition: background .12s, color .12s;
}
:is(#ccReloadMenu) .tmi:hover { background: var(--blue-soft); color: var(--blue); }
:is(#ccReloadMenu) .tmi .ico {
  flex: 0 0 14px; width: 14px; text-align: center;
  font-size: 12px; color: var(--fg-dim2);
}
:is(#ccReloadMenu) .tmi:hover .ico { color: var(--blue); }
:is(#ccReloadMenu) .tmi .tx { flex: 1; }

/*=============================================================================
  [frontend-style] 批次28 · 分类「配置详情」弹窗 4 项 + 共用「配置导入」页签 1 项

  本段全部是**纯追加**: 上方任何规则一条都没删、没改语义(只改了 A 段那一条的注释与
  拆分, 以及 .b21-unit / .b21-timerange>.sep 两个组件的自身参数, 见各自注释)。
  作用域三档, 逐条都不外溢:
    T3 详情弹窗内的下拉    —— `.ccd-pane select.sel`。`.ccd-pane` 只在分类「配置详情」
        弹窗里出现(biz.js 里是「配置软件」弹窗用 .b21-pane、分类弹窗用 .ccd-pane),
        且选择器里带 `select` 元素名 —— 卡密管理页的「选中行」是 <tr class="sel">
        (见本文件 .gmtbl table.tbl tbody tr.sel td), 元素名是 tr, 永远匹配不到。
    T5 「配置导入」页签     —— `.ccd-ini-*` 与 `#ccdImportContent > .b21-panel-hd`。
        .ccd-ini-* 是本批新 DOM 独有的类名(全项目无第二处引用);
        搜索框那条以 id 开头, 只命中这一个面板的标题行, 其它 10 个 .b21-panel-hd 不受影响。
    T4 时间段的 240px 宽度  —— 复用批次27 C 段那 6 个面板的 :is() 列表, 只多一层 .b21-timerange,
        写在 C 段之后, 同特异性(0,5,0)靠源序靠后取胜。
  回滚: 删掉本段即回到批次27 观感(T3/T5 消失, T4 回到 200px)。
=============================================================================*/

/* ---- T3. 配置详情弹窗的下拉: 自绘箭头 ---------------------------------------
   背景: 原生 select 的箭头由浏览器画在 padding 盒右边缘内 4.74px 处, 实测改
   padding-right / font-size / background-* 都纹丝不动, 只有改 border-right-width 才会
   跟着挪 —— 而这些 select 的右边框是全站共用的, 不能动。故改用 appearance:none 关掉
   原生箭头, 自己画一张 SVG。
   箭头几何(实测口径: 从 select 右边框**外沿**到箭头墨迹最右点的水平距离):
     = 背景图右偏移 9px + 右边框宽 1px + SVG 盒内右侧余白 (12 - 10.15) = 11.85px
     原生箭头同口径约 5.75px, 改后为 11.85px(目标区间 11-12px)。
   SVG: 12x12 盒, 折线 M2.6 4.3 → 6 7.7 → 9.4 4.3, 描边 1.5 圆头, 墨迹 x∈[1.85,10.15]、
   y∈[3.55,8.45](几何中心正好落在盒子中心); 颜色 #6b7484 = 既有变量 --fg-dim。
   padding-right 26px >= 箭头占宽(9+12=21px) + 5px 余量, 文字不会压到箭头上。
   ⚠ 若日后有人把这几个 select 的 border-right 改了, 上面那 1px 要跟着重算。 */
.ccd-pane select.sel {
  -webkit-appearance: none;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2.6 4.3 6 7.7 9.4 4.3' fill='none' stroke='%236b7484' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 9px center;
  background-size: 12px 12px;
  padding-right: 26px;
}

/* ---- T4. 「可试用时间段」整行 200 -> 240px ----------------------------------
   两个 <input type=time> 是 flex:1 1 0 平分剩余宽度, 行宽 +40 → 每个约 +20(实测见报告)。
   代价(用户已确认接受): 这一行的右边缘不再与同列 200px 的盒状控件对齐。
   原生的时钟图标 .b21-time::-webkit-calendar-picker-indicator(opacity:.5) 明确**保留**,
   本段不碰那条规则。
   批次29: 列表里**移除** .b21-pane[data-acfgpane="trial"] —— 它是「配置软件」弹窗的试用页签,
   不该被「配置详情」弹窗的新宽度波及; 移除后该页签的 .b21-timerange 由批次25 C 段的
   200px 规则接管(实测 240 -> 200), 即冻结成批次27 以前的旧样子。
   留在列表里的 bind/limit 与三个 .ccd-pane 不受影响。 */
:is(.b21-pane[data-acfgpane="bind"], .b21-pane[data-acfgpane="limit"],
    .ccd-pane[data-ccpane="绑定设置"], .ccd-pane[data-ccpane="限开设置"], .ccd-pane[data-ccpane="试用设置"])
  .b21-frow > .ctl > .b21-timerange {
  width: 240px;
}

/* ---- T5. 「配置导入」页签: 左栏分组 + 右侧参数表 -----------------------------
   DOM 契约见 biz.js 的 mkPaneImport: #ccdImportContent.b21-panel
     > .b21-panel-hd( <span>导入的内容</span> + input.inp.ccd-ini-search )
     > .ccd-ini-split > .ccd-ini-main > .ini-import > .tblwrap > table.tbl , .tblbar
   改动只在样式层, 不动 DOM。 */
#ccdImportContent .ccd-ini-split {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  width: 100%;
}
/* 主栏: 吃掉整行宽度; min-width:0 让里面的表格可以比内容窄(否则表会把 flex 项顶宽) */
#ccdImportContent .ccd-ini-main { flex: 1 1 auto; min-width: 0; }
/* 搜索框: 标题行 .b21-panel-hd 的基规则(:1773)已提供 display:flex / align-items:center /
   gap:6px —— 批次28 在这里重述过一遍, 属冗余声明, 批次29 已删(只删重述, 不留空规则)。
   下面只保留本面板特有的事: 定宽 + 高度/内边距/字号。
   ⚠ 以 #ccdImportContent 开头是刻意的: 全站还有 10 个 .b21-panel-hd, 一个都不许动。
   搜索框的「顶到最右」由 .b29-undo-btn 上的 margin-left:auto 承担, 见文末 T7 段
   —— 若把 auto 放在搜索框自己身上, 撤回/重做会被留在左侧与「历史版本」挤在一起。 */
#ccdImportContent > .b21-panel-hd > .ccd-ini-search {
  flex: 0 0 auto;
  width: 200px; height: 32px; padding: 6px 10px; font-size: 13px;
}
/* 右表: 固定布局(列宽完全由第一行的 th 决定, 长值再也撑不宽表格)。
   「#」列 38px **来自本文件 table.tbl .rowno{width:38px}, 不是 biz.js 内联样式**;
   「值」列吃掉其余宽度, 由 biz.js 里 th 的行内百分比决定 —— 行内样式优先级高于任何选择器,
   这里不再重复写宽度, 写了也是死代码。
   值列放不下时长值不再做任何处理: 该单元格装的是 <input>(替换元素), overflow-wrap 对它无效,
   原 `td:nth-child(3){overflow-wrap:anywhere}` 是死代码, 批次29 已删; 由 .tblwrap 自带的
   overflow:auto 兜底。 */
#ccdImportContent .ccd-ini-main table.tbl { table-layout: fixed; }

/*=============================================================================
  [frontend-style] 批次29 · 「配置导入」重做(标题行 / 撤回重做 / 两层树 / 分割设置弹窗)
  · 纯追加: 上方规则一条都没删(只在 T4 列表里摘掉一个选择器, 并把批次28 的就地修改
    收窄到 .ccd-pane 限定, 见 T6)。
  · 命名全部带 b29- 前缀 或 .ccd-import- 前缀, 全项目此前无第二处引用, 不会外溢。
  · 回滚: 删掉本段即回到批次28 状态(再加上 T4 列表里的 acfgpane="trial")。
=============================================================================*/

/* ---- T6. 批次28 的「就地修改」收窄为 .ccd-pane 限定 -------------------------
   依据: 批次28 把 .b21-unit / .b21-timerange>.sep 的自身参数直接改在共用组件规则上
   (:1732 段与 :1722 段, 两处注释当时就自承「就地修改、未加 .ccd-pane 限定」), 于是
   「配置软件」弹窗(.b21-pane[data-acfgpane="trial"])试用页签里的同款控件也被波及:
   时间段整行 240px、单位下拉被削到 64px 并丢了左边框/左侧圆角、'+' 与下拉之间多出 8px 缝。
   用户已拍板: 「配置软件」弹窗**冻结成批次27 以前的旧样子**, 批次28 的观感只保留在
   「配置详情」弹窗 —— 所以这里把这四笔原样补回, 只对 .ccd-pane 生效。
   ⚠ 下拉的 padding-right 刻意**不写**: 「配置详情」弹窗里那条 26px 来自 T3 的
   `.ccd-pane select.sel`(0,2,1), 本段若在此再写 padding-right 就会把它压掉,
   自绘箭头(SVG 12px + 右偏移 9px)会被文字压住。padding-left 8px 才是批次28 的原参数。 */
.ccd-pane .b21-unit { gap: 8px; }
.ccd-pane .b21-unit > .b21-num {
  border-top-right-radius: 6px;
  border-bottom-right-radius: 6px;
}
.ccd-pane .b21-unit > .b21-num > button:last-child {
  border-top-right-radius: 6px;
  border-bottom-right-radius: 6px;
}
.ccd-pane .b21-unit > .b21-unit-sel {
  flex: 0 0 64px; width: 64px; padding-left: 8px;
  border-left: 1px solid var(--border-2);
  border-top-left-radius: 6px; border-bottom-left-radius: 6px;
}
.ccd-pane .b21-timerange > .sep { color: var(--fg-dim); font-size: 0; padding: 0 2px; }
.ccd-pane .b21-timerange > .sep::before { content: "–"; font-size: 13px; }

/* ---- T7. 「配置导入」标题行 + 撤回复原 ------------------------------------------
   DOM 契约(实现方严格照此出 markup, 类名/ id 不得改名):
     A1 #ccdImportContent > .b21-panel-hd.ccd-import-hd
          > .ccd-import-title + button.btn.sm.b29-undo-btn#ccdImportUndo
          + button.btn.sm.b29-redo-btn#ccdImportRedo + input.inp.ccd-ini-search#ccdImportSearch
     A2 【已删除】左栏两级分类树 .b29-tree / .b29-node(.b29-lv1/.b29-lv2/.b29-caret/.b29-label/.b29-n)
          —— 批次32 需求①: DOM 宿主、JS 渲染与本节样式一并删掉(搜索/命中高亮走表格, 不依赖它)。
     A3 【已删除·批次33】body > .b29-mask#b29SplitMask > .b29-modal 一族(b29-split.js 批次32 已删):
          页面里既无 markup 也无 JS 入口, 规则空转 —— 批次33 已连同注释整段删掉(全仓 grep 零消费者)。
   与 markup 的耦合点只有类名, 本段不产生任何 JS。 */

/* A1 标题行: [标题] ……… [撤回][重做][搜索框]
   ⚠ margin-left:auto 放在**撤回**按钮上而不是搜索框上: 两个 auto 会平分空隙、把
   撤回/重做甩在中间; 只放在搜索框上则撤回/重做被留在左侧与「历史版本」挤成一团。
   放在撤回上, 视线结果正是规格书 4.1 的 [标题][设置]……[撤回][重做][搜索框]（「设置」批32 已删）。 */
.ccd-import-hd > .ccd-import-title { flex: 0 0 auto; }
.ccd-import-hd > .b29-undo-btn { flex: 0 0 auto; margin-left: auto; }
.ccd-import-hd > .b29-redo-btn { flex: 0 0 auto; }
/* 禁用态: 全局 .btn:disabled 已给 opacity .5 + cursor:default, 这里再压一档并换成
   not-allowed, 让「无可撤回/重做」这件事一眼可见(按钮是 .btn.sm, 无其它禁用样式)。 */
.ccd-import-hd > .b29-undo-btn:disabled,
.ccd-import-hd > .b29-redo-btn:disabled { opacity: .45; cursor: not-allowed; }

/*=============================================================================
  [frontend-style] 批次30 · 搜索命中高亮 + 撤回/重做方形图标按钮
  · 纯追加段: 上方规则(A1 标题行 等)保留未删; A3 弹窗一族(.b29-mask#b29SplitMask 那组)已在批次33 随规则一并删除 ——
    A2 左栏两层树已在批次32 删除; 本段全部是新选择器, 且只命中批次30 新引入的 .b29-hl
    与既有的 .b29-undo-btn / .b29-redo-btn。
  · 命名: 新类只有 .b29-hl(biz.js 插入 <mark class="b29-hl">), 全项目此前零引用。
  · 回滚: 删掉本段即回到批次29 状态。
=============================================================================*/

/* ---- S1. 搜索命中高亮 (为「搜索支持主类/子类 + 高亮显示」铺路) ---------------
    biz.js 的搜索会在导入表格单元格里插入 <mark class="b29-hl">命中片段</mark>
    (左栏两层树在批次32 已删除; 表格自批次31 起退出**版面**, 但仍是搜索高亮的落点与程序化断言对象)。
   配色**复用本文件既有的琥珀色系**(与 .tag.amber / .notice.warn 同族的
   #fffbeb / #fde68a / #b45309 / #92400e), 不新造一套色板; 这里没有现成的琥珀背景
   变量, 故取该系列的 #fde68a(浅)/ #fef08a(更浅)/ #fbbf24(实) 三档。
   用 background-color 承载「高亮」语义(不是只改 color), 字色同时加深保证对比。
   ⚠ 刻意不写 padding / border / margin: 它们会改变行盒尺寸, 让表格行在搜索时
   上下抖动、行高跳变; 只用 border-radius 2px(不占布局空间的小圆角)。 */
.b29-hl {
  background-color: #fde68a;
  color: #92400e;
  border-radius: 2px;
}
/* 导入表格单元格: 表格是 table.tbl(斑马纹/悬停行 #f5f7fb/选中行 #eef4ff 都可能出现),
   用更浅的 #fef08a 打底, 与白底、极淡灰蓝底都能拉开, 又不会把单元格里的输入框衬得刺眼。
   ⚠ 本段是**新增**规则, 不改动任何既有的 #ccdImportContent 规则(批次29 的作用域限定
   原样保留); 这里以 id 开头是为把影响面钉死在「配置导入」这一个面板里。 */
#ccdImportContent table.tbl td .b29-hl {
  background-color: #fef08a;
  color: #92400e;
}
/* 表格里被标记为选中的行(与左树 .sel 同一套语义): 底色更深, 高亮换成实琥珀 */
#ccdImportContent table.tbl tr.sel td .b29-hl,
#ccdImportContent table.tbl tr.is-on td .b29-hl,
#ccdImportContent table.tbl tr.on td .b29-hl {
  background-color: #fbbf24;
  color: #92400e;
}

/* ---- S2. 撤回 / 重做: 方形图标按钮 ----------------------------------------
   批30 把两个按钮的文字「撤回」「重做」换成 Unicode 字形 ↶ / ↷
   (#ccdImportUndo.b29-undo-btn / #ccdImportRedo.b29-redo-btn), id 与类名都不变。
   ⚠ 上方 2349-2356 段的四条规则**一条未改**: 本段靠「同特异性 + 更靠后」覆盖
     .btn.sm(:693) 的 padding/font-size, 并**保留** .b29-undo-btn 上的 margin-left:auto
     —— 右对齐结果完全不变, 仍是 [标题][设置]……[撤回][重做][搜索框]。
   · 27×27 与 .btn.sm 的高度对齐; 图标用 flex 居中而不用 line-height 对齐基线,
     字形因此不会偏心; line-height:1 防止箭头把按钮撑高。
   · **刻意不写 font-family**: 按钮继承 body 的微软雅黑(:37), 该字体缺字形时浏览器会
     逐字回退到 Segoe UI Symbol 等含 ↶/↷ 的字体; 显式指定字体族反而有豆腐块风险。 */
.ccd-import-hd > .b29-undo-btn,
.ccd-import-hd > .b29-redo-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 27px; height: 27px; min-width: 27px; padding: 0;
  font-size: 15px; line-height: 1;
  color: var(--fg); background: #fff; border-color: var(--border-2);
  transition: background-color .12s, border-color .12s, color .12s, opacity .12s;
}
/* hover / active: 只对可用按钮生效(:not(:disabled)), 特异性 (0,4,0) 压过 .btn:hover(0,1,1) */
.ccd-import-hd > .b29-undo-btn:not(:disabled):hover,
.ccd-import-hd > .b29-redo-btn:not(:disabled):hover {
  background: #eef4ff; border-color: #9fb0c8; color: var(--blue-dark);
}
.ccd-import-hd > .b29-undo-btn:not(:disabled):active,
.ccd-import-hd > .b29-redo-btn:not(:disabled):active {
  background: #dde9fb; border-color: #7f93b0;
}
/* 键盘可达性: 方形图标按钮没有可见文字, 焦点环必须显式给(浏览器默认环在浅底上偏淡) */
.ccd-import-hd > .b29-undo-btn:focus-visible,
.ccd-import-hd > .b29-redo-btn:focus-visible {
  outline: 2px solid var(--blue); outline-offset: 1px;
  background: var(--blue-soft);
}
/* 禁用态(「无可撤回/重做」出现极频繁, 必须一眼可辨): 比全局 .btn:disabled 的 .5
   再狠一档到 .38, 同时变灰(去底白底、灰边、灰字)。
   这里**显式写死 background / border-color / color**: 该规则特异性 (0,3,0) 高于
   .btn:hover (0,1,1), 于是禁用按钮被悬停时外观零变化 —— 禁用态没有 hover 反馈。
   :disabled:hover 一并列入只为把 cursor 钉成 not-allowed。 */
.ccd-import-hd > .b29-undo-btn:disabled,
.ccd-import-hd > .b29-redo-btn:disabled,
.ccd-import-hd > .b29-undo-btn:disabled:hover,
.ccd-import-hd > .b29-redo-btn:disabled:hover {
  opacity: .38; cursor: not-allowed;
  color: var(--fg-dim2); background: #f7f8fa; border-color: var(--border);
}

/* ==========================================================================
   批次31: 配置文本编辑器嵌进「配置管理」弹窗的**宿主布局**
   编辑器自己的样式全在 b31-editor.css 里, 且选择器一律限定在 #b31EditorRoot 内;
   这里只补分栏尺寸、编辑器宿主任期、查找命中数, 以及旧表格的降级隐藏。
   ========================================================================== */
#ccdImportContent .ccd-ini-main {
  display: flex; flex-direction: column; min-width: 0; min-height: 0;
  height: 560px;                       /* 兜底: 不支持 min() 的老内核也不至于塌成 0 高 */
  height: min(560px, 58vh);            /* 与左侧分类栏的 max-height:560px 同高, 矮屏按视口收缩 */
}
/* 编辑器根节点(#b31EditorRoot)是 .b31-host 的唯一子元素, 且自身 height:100% —— 这里必须给出确定高度 */
#ccdImportContent .ccd-ini-main > .b31-host {
  flex: 1 1 auto; min-height: 0; min-width: 0; overflow: hidden;
  display: flex; flex-direction: column;
  border: 1px solid var(--border); border-radius: 8px; background: #fff;
}
/* 查找命中数「第 N / 共 M 个」 */
#ccdImportContent .b31-findcnt {
  flex: 0 0 auto; margin-left: 2px; font-size: 12px; color: var(--fg-dim2); white-space: nowrap;
}
/* 「加一行」是给两列表格用的; 批次31 起录入口是编辑器, 这个按钮留着只会被误点 */
#ccdImportContent > .tblbar > #ccdImportAdd { display: none; }
/* 窄屏兜底: 左侧分类栏收窄, 把宽度让给编辑器与校验面板, 且不产生横向滚动 */
@media (max-width: 1400px) {
  #ccdImportContent .ccd-ini-search { width: 148px; }
}

/* ============================================================================
   批次31 改造②: 配置文本编辑器 = 唯一版面主体
   —— 分类树在批次32 已**整体删除**(DOM + JS + 样式); 扁平表格仍退出版面(hidden + display:none,
      节点留在 DOM 里, 供程序化断言); 编辑器独占整栏; 查找命中数 / 保存状态提示留在标题行;
      历史版本是一层覆盖在编辑区上的面板。
   ★ 本段只做**追加**: 没有删掉任何一条既有规则(.ccd-ini-main 定高、.ccd-import-hd 一族、.ccd-ini-search)。
   ============================================================================ */
/* 1) 单栏: 主栏吃掉整行宽度(.ccd-ini-split 仍是 flex, 主栏 flex:1 自然铺满) */
#ccdImportContent .ccd-ini-main { position: relative; }
/* 2) 保存状态提示: 「已保存 / 保存中… / 未保存 / 保存失败」 */
#ccdImportContent .b31-savestate {
  flex: 0 0 auto; margin-left: 8px; padding: 2px 8px; border-radius: 10px;
  font-size: 12px; line-height: 18px; white-space: nowrap;
  color: #4b5563; background: #f3f4f6; border: 1px solid var(--border, #e5e7eb);
}
#ccdImportContent .b31-savestate[data-state="saving"] { color: #92400e; background: #fef3c7; border-color: #fde68a; }
#ccdImportContent .b31-savestate[data-state="dirty"] { color: #b45309; background: #fffbeb; border-color: #fcd34d; }
#ccdImportContent .b31-savestate[data-state="error"] { color: #b91c1c; background: #fee2e2; border-color: #fecaca; }
/* 3) 历史版本按钮 */
#ccdImportContent .b31-hist-btn { flex: 0 0 auto; margin-left: 8px; }
/* 4) 历史版本面板: 批次32 起改为**独立浮窗**(.mask.b31h-nest, 由 b31-history.js 挂在 #modalHost 下,
 *    样式在 b31-history.css)。这里只剩宿主占位 —— 必须**去掉**原来的 inset:0 覆盖层属性, 否则编辑区
 *    上会留一块白板; 宿主由 biz.js 造成 hidden + display:none, b31-history.js 恒不改它。 */
#ccdImportContent .b31-hist {
  position: static; inset: auto; z-index: auto; overflow: visible;
  background: none; border: 0; border-radius: 0;
}
#ccdImportContent .b31-hist[hidden] { display: none; }
/* 5) 窄屏(<=1400px): 标题行允许折行, 免得状态提示 / 历史按钮把搜索框挤没 */
@media (max-width: 1400px) {
  #ccdImportContent .ccd-import-hd { flex-wrap: wrap; }
}

/* ============================================================================
 * 批次34: 「版本管理」弹窗(public/b34-version.js)
 *   全部规则都以 b34v- 前缀自限定, 只追加在文件末尾, 不改动任何既有规则。
 *   三层弹窗(列表 100 / 表单 120 / 删除确认 140)的 z-index 由 JS 内联设置,
 *   这里只管版式。复用 .mask/.modal/.tblwrap/table.tbl/.tblbar/.btn/.tag/.notice。
 * ==========================================================================*/
.b34v-list-modal > .body { display: flex; flex-direction: column; }
/* 浮层模式: 遮罩只铺「右侧内容区」(左侧侧边栏 .sidebar 宽 236px 不被盖),
 *   左右各留 16px 边距; 弹窗本体宽度上限 = calc(100vw - 236px - 32px)。
 *   替代原先的内联 width:96vw / max-width:1520px。 */
.mask.b34v-list-mask { left: 236px; padding: 16px; }
.mask.b34v-list-mask > .b34v-list-modal { max-width: calc(100vw - 236px - 32px); }
/* 弹窗内的「当前分类：xxx」上下文提示(open() 传入 categoryName 时才出现) */
.b34v-list-modal > header .b34v-list-ctx { font-size: 12.5px; color: var(--fg-dim); margin-right: 10px; }

/* ---- 筛选栏 ---- */
.b34v-filters {
  display: flex; flex-wrap: wrap; gap: 12px 14px; align-items: flex-end;
  padding-bottom: 14px; margin-bottom: 14px; border-bottom: 1px solid var(--border);
}
.b34v-filter { display: flex; flex-direction: column; gap: 5px; min-width: 150px; }
.b34v-filter > label { font-size: 12px; color: var(--fg-dim); font-weight: 500; }
.b34v-daterange { display: flex; align-items: center; gap: 6px; }
.b34v-daterange .inp { width: 148px; }
.b34v-dash { color: var(--fg-dim2); }
.b34v-filter-btns { flex-direction: row; gap: 8px; align-items: flex-end; margin-left: auto; }

/* ---- 操作行 ---- */
.b34v-ops { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.b34v-ops .sp { flex: 1; }
.b34v-selinfo { font-size: 12px; }

/* ---- 表格 ---- */
.b34v-tblwrap { max-height: calc(88vh - 330px); min-height: 180px; }
table.tbl.b34v-table { min-width: 1560px; }
table.tbl.b34v-table td { padding: 8px 10px; vertical-align: top; }
table.tbl.b34v-table tbody tr:hover td { background: #fafbfd; }
.b34v-clip { max-width: 280px; word-break: break-all; word-wrap: break-word; white-space: normal; line-height: 1.55; font-size: 12.5px; }
.b34v-c-chk { width: 40px; text-align: center; }
.b34v-c-act { width: 68px; text-align: center; }
table.tbl.b34v-table td input.b34v-chk { width: auto; height: auto; padding: 0; margin: 3px 0 0; background: none; cursor: pointer; }
table.tbl.b34v-table td input.b34v-chk:hover { background: none; border-color: transparent; }

/* ---- 分页 ---- */
.b34v-pgs { display: inline-flex; gap: 4px; align-items: center; }
.b34v-pg.on { background: var(--blue); border-color: var(--blue); color: #fff; font-weight: 600; }
.b34v-ell { color: var(--fg-dim2); font-size: 12px; padding: 0 2px; }
.b34v-sizelb { display: flex; align-items: center; gap: 6px; margin: 0; }
.b34v-sizesel { width: 76px; height: 27px; }

/* ---- 表单 ---- */
.b34v-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 16px; }
.b34v-radios { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; min-height: 36px; }
.b34v-radio { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; cursor: pointer; margin: 0; }
.b34v-ro {
  padding: 8px 11px; background: #f7f9fc; border: 1px solid var(--border);
  border-radius: 6px; font-size: 13px;
}
.b34v-roid { font-family: var(--mono); font-size: 11px; color: var(--fg-dim2); }

/* ---- 删除确认 ---- */
.b34v-confirm-mask .notice { margin-bottom: 14px; }
.b34v-dellist {
  max-height: 208px; overflow: auto; font-size: 13px; line-height: 1.9;
  background: #f7f9fc; border: 1px solid var(--border); border-radius: 6px;
  padding: 8px 12px; word-break: break-all;
}

@media (max-width: 900px) {
  .b34v-grid2 { grid-template-columns: 1fr; }
}

/*=============================================================================
  批次35: 站点设置 ·「网站结构」选项卡
    一张「网址 → 页面/接口 → 源码位置」的树状索引。样式全部 sst- 前缀,
    与其它页面零冲突; 只有 .sst-bar 里复用了全局 .inp / .btn.sm。
=============================================================================*/
.sst-note {
  font-size: 12.5px; color: var(--fg-dim); line-height: 1.75;
  background: var(--blue-soft); border: 1px solid #dbe7fb; border-radius: 8px;
  padding: 9px 12px;
}
.sst-note .mono { color: var(--fg); }

.sst-bar { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.sst-bar .inp { flex: 0 1 380px; }
.sst-bar .sp { flex: 1; }          /* 把两个按钮推到右边 */

.sst-tree {
  border: 1px solid var(--border); border-radius: 10px; background: #fcfdff;
  padding: 8px 12px; font-size: 13px; line-height: 2;
  overflow-x: auto;                 /* 窄屏时横向滚动, 不挤压列 */
}
.sst-row {
  display: flex; align-items: baseline; gap: 8px;
  padding: 1px 6px; border-radius: 6px; white-space: nowrap;
}
.sst-row:hover { background: #f1f6fd; }
.sst-row.grp { font-weight: 600; }
.sst-row.root {
  margin-top: 10px; padding-top: 4px; padding-bottom: 4px;
  border-bottom: 1px solid var(--border);
}
.sst-row.root:first-child { margin-top: 0; }

/* 树形连线(├─ └─ │) —— 空白用 pre 保留 */
.sst-guide { font-family: var(--mono); color: #c6d0de; white-space: pre; }
.sst-caret {
  display: inline-block; width: 13px; flex: none; cursor: pointer;
  color: var(--fg-dim2); font-size: 11px; user-select: none;
}
.sst-caret:hover { color: var(--blue); }
.sst-caret.off { cursor: default; }

/* 名称这一列做成定宽容器: 连线缩进只在它内部变化,
   右边的 网址 / 源码位置 / 说明 三列才不会随层级左右漂 */
.sst-lead { display: flex; align-items: baseline; gap: 6px; flex: none; width: 290px; min-width: 0; }
.sst-t { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.sst-head {
  display: flex; align-items: baseline; gap: 8px; padding: 0 6px 6px 18px;
  font-size: 11.5px; color: var(--fg-dim2); letter-spacing: .3px;
}
.sst-head .sst-u { color: var(--fg-dim2); }
.sst-u {
  flex: none; width: 210px; overflow: hidden; text-overflow: ellipsis;
  font-family: var(--mono); font-size: 12px; color: var(--blue); text-decoration: none;
}
.sst-u:hover { text-decoration: underline; }
.sst-u.plain { color: var(--fg-dim2); }
.sst-f {
  flex: none; width: 235px; overflow: hidden; text-overflow: ellipsis;
  font-family: var(--mono); font-size: 11.5px; color: #7c5cae;
}
.sst-d { color: var(--fg-dim2); font-size: 12px; white-space: normal; min-width: 260px; }

@media (max-width: 900px) {
  .sst-lead, .sst-u, .sst-f { width: auto; }
  .sst-d { min-width: 0; }
}
