:root {
  --bg: #0f1115;
  --panel: #171a21;
  --card: #1c2029;
  --border: #2a2f3a;
  --text: #e6e8ee;
  --dim: #9aa3b2;
  --accent: #4f8cff;
  --green: #22c55e;
  --red: #ef4444;
  --gold: #eab308;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
  background: var(--bg);
  color: var(--text);
  font-size: 14px;
  line-height: 1.6;
}
button { font-family: inherit; cursor: pointer; }
input, textarea {
  font-family: inherit;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  padding: 8px 12px;
  font-size: 14px;
  outline: none;
  width: 100%;
}
input:focus, textarea:focus { border-color: var(--accent); }
textarea { resize: vertical; font-family: Consolas, "Microsoft YaHei", monospace; line-height: 1.5; }

/* 登录 */
.login-wrap { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.login-card {
  width: 340px; background: var(--panel); border: 1px solid var(--border);
  border-radius: 14px; padding: 32px 28px; display: flex; flex-direction: column; gap: 14px;
}
.login-card h1 { font-size: 20px; text-align: center; }
.login-card .sub { text-align: center; color: var(--dim); margin-top: -8px; margin-bottom: 8px; }
#loginBtn {
  background: var(--accent); color: #fff; border: none; border-radius: 8px;
  padding: 10px; font-size: 15px; font-weight: 600;
}
.err { color: var(--red); font-size: 13px; min-height: 18px; text-align: center; }
/* 版本号：等宽字体 + 弱色，只作「一眼确认哪一版」用，不与正文争注意力 */
.login-ver { text-align: center; color: var(--dim); font-size: 12px; font-family: Consolas, Menlo, monospace; }
.app-ver { color: var(--dim); font-size: 12px; font-family: Consolas, Menlo, monospace; margin-right: 10px; }

/* 主界面 */
.main-wrap { min-height: 100vh; }
.topbar {
  display: flex; align-items: center; gap: 20px;
  background: var(--panel); border-bottom: 1px solid var(--border);
  padding: 0 20px; height: 56px; position: sticky; top: 0; z-index: 10;
}
.brand { font-weight: 700; font-size: 16px; white-space: nowrap; }
.tabs { display: flex; gap: 4px; flex: 1; }
.tab {
  background: transparent; border: none; color: var(--dim);
  padding: 8px 16px; border-radius: 8px; font-size: 14px;
}
.tab.active { background: var(--card); color: var(--text); }
.tab:hover { color: var(--text); }
.btn-ghost {
  background: transparent; border: 1px solid var(--border); color: var(--dim);
  padding: 6px 14px; border-radius: 8px;
}
.btn-ghost:hover { color: var(--text); border-color: var(--dim); }

/* 内容主体宽度。原来是 1100px 硬上限，而「任务日志」表 8 列（含详情原文）实测要 ~1300px
   —— 于是右侧内容被推出视口，必须横向拖动才能看见，客户明确反馈过这一点。
   改成 min(1760px, 96vw)：宽屏不再无谓留白，窄屏仍随视口收缩（96vw 而非 100vw 是给
   垂直滚动条让位，否则宽屏下会出现一条由滚动条引起的横向滚动）。 */
.content { padding: 20px 28px; max-width: min(1760px, 96vw); margin: 0 auto; }
.pane h2 { font-size: 18px; margin-bottom: 16px; }

/* 概览卡片 */
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; }
.card {
  background: var(--card); border: 1px solid var(--border);
  border-radius: 12px; padding: 18px;
}
.stat-label { color: var(--dim); font-size: 13px; }
.stat-value { font-size: 26px; font-weight: 700; margin-top: 6px; }

/* 配置 */
.config-grid { display: flex; flex-direction: column; gap: 14px; margin-bottom: 16px; }
.config-grid .card h3 { font-size: 15px; margin-bottom: 14px; color: var(--text); }
.field { display: block; margin-bottom: 12px; }
.field-label { display: block; color: var(--dim); font-size: 13px; margin-bottom: 5px; }
.field-hint { display: block; color: var(--dim); font-size: 11px; opacity: .7; margin-top: 3px; }
/* 模型字段（input + 自建下拉 + 拉取按钮）：hint 的 ok/err 是拉取结果反馈，
   必须比默认灰字更显眼，所以把 opacity 提回 1。 */
.field-hint.ok { color: var(--green); opacity: 1; }
.field-hint.err { color: var(--red); opacity: 1; word-break: break-all; }
.model-row { display: flex; gap: 8px; align-items: center; }
/* 自建下拉面板。为什么不用 datalist：它的候选会按输入框当前值做子串过滤，
   只要输入框里已有值（预填的第一个模型，或用户上次填的），点开就只剩 1 项 ——
   界面上表现为「明明拉到了十几个，下拉里只有 1 个」。见 app.js 同段注释。
   面板走**文档流**（不是绝对定位的浮层）：浮层会盖住下方的「启用 / 设为主」复选框
   和「取消 / 保存」按钮 —— 2026-09-28 真机截图里就是按钮直接压在模型项上，
   既看不清也容易点错（实测点「取消」改掉了模型的真实案例）。
   流内展开会把下方内容自然推开，代价是展开时表单会下移，这个代价值得付。 */
.model-combo { position: relative; flex: 1; min-width: 0; display: flex; align-items: center; }
.model-combo input { flex: 1; min-width: 0; padding-right: 28px; }
.model-arrow {
  position: absolute; right: 1px; top: 1px; bottom: 1px; width: 26px;
  background: transparent; border: 0; color: var(--dim); font-size: 10px;
  cursor: pointer; line-height: 1;
}
.model-arrow::before { content: '▼'; }
.model-pop {
  margin-top: 6px; max-height: 180px; overflow-y: auto; padding: 4px;
  background: var(--card); border: 1px solid var(--border); border-radius: 8px;
}
/* hidden 属性是弱约定：不显式写这条，会被上面的 .model-pop 规则覆盖掉而不生效。 */
.model-pop[hidden] { display: none; }
.model-item {
  display: flex; gap: 8px; align-items: center;
  padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 13px;
}
.model-item:hover, .model-item.active { background: var(--panel); }
.model-item .mi-id { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 「疑似非对话模型」只标注、不隐藏 —— 误判的代价只是排序靠后，不能让可用模型消失。 */
.model-item .mi-tag { flex: 0 0 auto; color: var(--dim); font-size: 11px; }
.model-empty { padding: 8px; color: var(--dim); font-size: 12px; }
#providerModelFetch { flex: 0 0 auto; white-space: nowrap; }
/* 面板在流内展开会把弹窗内容顶高，超出视口时要能在卡片内滚动，
   否则底部的「取消 / 保存」会被挤出屏幕且够不到。 */
/* 弹窗通用滚动（v10 修复「接入模型弹窗太长滚不动、底部按钮顶出屏幕」）：
   内容超长时卡片内部滚动，遮罩层本身不滚。之前只给个别弹窗挂过规则，
   新弹窗（费率）忘了挂就又出同样问题 —— 一锤子统一到所有 .modal-card。 */
.modal .modal-card { max-height: 88vh; overflow-y: auto; }
.row { display: flex; gap: 14px; flex-wrap: wrap; }
.row .field { flex: 1; min-width: 120px; }
.checks { display: flex; gap: 12px; flex-wrap: wrap; }
.check { display: flex; align-items: center; gap: 6px; color: var(--text); cursor: pointer; }
.check input { width: auto; accent-color: var(--accent); }
.rate-item {
  border: 1px solid var(--border); border-radius: 10px;
  padding: 10px 12px; margin-bottom: 10px;
}
.rate-item .rt-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 8px; }
.rate-item .rt-name { font-weight: 600; color: var(--gold); word-break: break-all; }
.rate-item .rt-fields { display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: flex-end; }
.rate-item .rt-fields label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--dim); }
.rate-item .rt-fields input, .rate-item .rt-fields select { width: 132px; }
.rate-add { display: flex; gap: 8px; margin-bottom: 12px; }
.rate-add input { flex: 1; }
.rate-add select { flex-shrink: 0; width: 170px; }
.rate-add .btn-primary { flex-shrink: 0; padding: 8px 16px; font-size: 14px; }
.btn-primary {
  background: var(--accent); color: #fff; border: none; border-radius: 8px;
  padding: 10px 22px; font-size: 15px; font-weight: 600;
}
.msg { margin-left: 12px; font-size: 13px; }
.msg.ok { color: var(--green); }
.msg.err { color: var(--red); }

/* 表格 */
table { width: 100%; border-collapse: collapse; background: var(--card); border-radius: 10px; overflow: hidden; }
th, td { text-align: left; padding: 10px 14px; border-bottom: 1px solid var(--border); font-size: 13px; }
th { background: var(--panel); color: var(--dim); font-weight: 600; }
tr:last-child td { border-bottom: none; }
.tag {
  display: inline-block; padding: 2px 8px; border-radius: 6px; font-size: 12px;
  background: var(--panel); border: 1px solid var(--border); color: var(--dim);
}
.tag.chat { color: var(--accent); border-color: var(--accent); }
.tag.gift { color: var(--gold); border-color: var(--gold); }
.tag.follow { color: var(--green); border-color: var(--green); }
.tag.like { color: #f472b6; border-color: #f472b6; }
.tag.warn { color: #f59e0b; border-color: #f59e0b; }
/* 估算标记：中性灰、比普通标签小一号 —— 它表示「这个数是估的」，不是报错，
   用告警色会让人以为账出问题；但要能一眼看见，所以紧跟在数字后面。 */
.tag.est { font-size: 11px; padding: 1px 6px; margin-left: 6px; white-space: nowrap; }
/* 事件流水：整行可点，展开该条的处理明细 */
.event-row { cursor: pointer; }
.event-detail td { padding-top: 0; }
.balance-pos { color: var(--green); }
.balance-neg { color: var(--red); }
.btn-sm {
  background: var(--accent); color: #fff; border: none; border-radius: 6px;
  padding: 4px 12px; font-size: 12px;
  /* 按钮文字（如「一键复刻」）窄列里会被挤成竖排三行，禁止换行保持一行 */
  white-space: nowrap;
}
.muted { color: var(--dim); }

.sub-tabs { display: flex; gap: 8px; margin-bottom: 14px; }
.subtab {
  background: var(--card); border: 1px solid var(--border); color: var(--dim);
  padding: 6px 16px; border-radius: 8px;
}
.subtab.active { background: var(--accent); color: #fff; border-color: var(--accent); }

/* 弹窗 */
.modal {
  position: fixed; inset: 0; background: rgba(0,0,0,.55);
  display: flex; align-items: center; justify-content: center; z-index: 100;
}
.modal-card {
  width: 360px; background: var(--panel); border: 1px solid var(--border);
  border-radius: 14px; padding: 24px; display: flex; flex-direction: column; gap: 14px;
}
.modal-card h3 { font-size: 16px; }
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
.btn-cancel { background: var(--card); border: 1px solid var(--border); color: var(--dim); padding: 8px 18px; border-radius: 8px; }
/* 两种按钮组合时以「小按钮」为准：.btn-cancel 自带 8px 纵向内边距 + 1px 边框，
   比 .btn-sm 高出 10px。若混在同一行，整行高度会被最高的那个撑起来
   （卡片列表的「整批」就是这种情况，把批次首行从 44px 顶到 53px）。
   纵向收到 3px 后总高与 .btn-sm 一致（6 + 16 字高 + 2 边框 = 24），横向保留 12px。 */
.btn-sm.btn-cancel { padding: 3px 12px; }

/* 语音库配置页（音色/三方API/AI消耗） */
.pane-toolbar { display: flex; justify-content: flex-end; margin-bottom: 14px; }
.btn-danger { background: var(--red); color: #fff; }
select {
  font-family: inherit; background: var(--panel); border: 1px solid var(--border);
  border-radius: 8px; color: var(--text); padding: 8px 12px; font-size: 14px; width: 100%;
}
.agg-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 14px; margin-top: 14px; }
.agg-grid h3 { font-size: 14px; margin-bottom: 8px; color: var(--dim); }
.agg-grid td, .agg-grid th { padding: 7px 10px; font-size: 12px; }

/* 一键自测 */
.selftest-box { border: 1px solid var(--border); border-radius: 10px; margin-bottom: 12px; overflow: hidden; }
.selftest-title { background: var(--panel); padding: 8px 14px; font-size: 13px; font-weight: 600; color: var(--dim); }
.selftest-body { padding: 12px 14px; font-size: 13px; word-break: break-all; }
.selftest-body.ok { background: rgba(34, 197, 94, .08); color: var(--text); }
.selftest-body.err { background: rgba(239, 68, 68, .08); color: var(--text); }
.selftest-body a { color: var(--accent); }

/* ---------- 客户管理：筛选栏 / 宽表 / 分页 ---------- */
.filter-bar {
  display: flex; gap: 10px; flex-wrap: wrap; align-items: center;
  background: var(--card); border: 1px solid var(--border);
  border-radius: 12px; padding: 12px 14px; margin-bottom: 12px;
}
.filter-bar .grow { flex: 1; min-width: 180px; }
.filter-bar select, .filter-bar input { width: auto; }
.filter-bar select { min-width: 100px; }
.filter-bar .check { white-space: nowrap; font-size: 13px; color: var(--dim); }
/* 日期框：不声明 color-scheme 时原生日历图标是深色的，在深色底上几乎看不见。
   「账目」页（AI 消耗 / 成本台账两个子页都有日期筛选）统一生效。 */
#p-billing input[type="date"] { color-scheme: dark; }
/* 客户页的批量工具栏靠左排列（默认 .pane-toolbar 是右对齐，那是给「新增音色」这类单按钮用的） */
#p-users .pane-toolbar { justify-content: flex-start; flex-wrap: wrap; gap: 8px; align-items: center; }

/* 宽表：客户列表列多，撑满容器并允许横向滚动，避免挤成一团 */
.table-wrap { width: 100%; overflow-x: auto; }
.table-wrap table { min-width: 1020px; }
/* 表头禁止折行：auto 布局下短表头（如「角色」）会被逐字竖排，极难读 */
#p-users .table-wrap thead th { white-space: nowrap; }
/* 15 列的宽表：把内边距从 14px 收到 6px。8px 时操作列改横向后会超出容器约 30px
   （1440 视口下），收到 6px 正好放下，且各列都是 nowrap，收紧内边距不会截断文字。 */
#p-users .table-wrap td, #p-users .table-wrap thead th { padding: 9px 6px; }
#p-users .table-wrap td { white-space: nowrap; }
/* 概览页的日志表：与客户列表同理，短字段（时间 / 来源 / 结果）在 auto 布局下会被
   逐字竖排，必须 nowrap；而「详情 / 说明」是长文本，反而要允许换行并限宽 ——
   否则一行超长详情会把整张表拉得极宽，为了看时间还得横向拖回去。 */
#p-overview .table-wrap td { white-space: nowrap; }
/* 详情列装的是失败原因原文：可能几百字，也可能是一条长 URL（如上游报错里的
   https://ark.../api/v3/chat/completions）。必须换行，否则整列被撑到几千像素、
   把时间列推到左边的视口外，客户只能横向拖回来才看得见时间。
   ★ 限宽**不能写在 td 上**：table-layout:auto 下浏览器按内容算列宽，td 的
   max-width 不可靠（本仓库已在 .lib-text-inner 处踩过同款坑）。
   所以把「确定宽度 + 换行」压到内层 div —— 固定宽度是必须的，只写
   overflow-wrap:anywhere 会让该列最小宽度塌成 1 个字，渲染成每行 2~3 字的麻花。 */
#p-overview .table-wrap td.wrap { white-space: normal; }
#p-overview .table-wrap .wrap-cell {
  width: 480px;
  white-space: normal;
  word-break: break-word;
  overflow-wrap: anywhere;
}
/* 任务日志表比另两张日志多 3 列，同样把内边距收窄，让 8 列在常见视口下放得下。 */
#p-overview .table-wrap td, #p-overview .table-wrap thead th { padding: 9px 8px; }

/* 0.76.0：任务日志加到 9 列（新增「类别」+ 详情里的「查看详情」按钮）。
   1440 视口下按 480px 给详情列，短列会被压到逐字换行（「耗时」竖排成一列字）。
   这里统一 nowrap + 详情列收到 360px —— 只有真正长的详情允许换行。 */
#p-overview #logsHead th, #p-overview #logsBody td { white-space: nowrap; }
#p-overview #logsBody td.wrap { white-space: normal; }
#p-overview #logsBody .wrap-cell { width: 360px; }
td.cb, th.cb { width: 36px; text-align: center; }
td.cb input, th.cb input { width: auto; accent-color: var(--accent); }
/* 操作列给固定宽度并允许内部换行：自动布局会把它撑到几百像素，
   同时把「累计消耗」这类短列挤成 40 来像素；固定宽度 + 两行排布反而更稳。 */
td.ops { width: 188px; white-space: normal; }
/* 客户列表例外：它有 6 个操作按钮，188px 里会折成 3~4 行，把行高撑到三倍，
   一屏看不到几个客户。横向排成一行约需 250px，给足宽度即保持全程单行。
   按钮内边距同步收到 6px（原 12px）：6 个按钮省下约 100px，
   否则 1440 视口下操作列会被挤出容器、必须横向滚动才能点到「删除」。 */
/* 客户列表例外：它有 7 个操作按钮（详情/检测/充值/编辑/改密/停用/删除），
   窄了会折成 3~4 行，把行高撑到三倍，一屏看不到几个客户。
   按钮都是 2 个字，横向排成一行约需 290px，给 300px 即保持全程单行；
   超宽的部分交给 .table-wrap 横向滚动，而不是折行。
   按钮内边距同步收到 6px（原 12px）：7 个按钮省下约 100px。 */
#p-users .table-wrap td.ops { width: 300px; white-space: nowrap; }
/* 单行排布时底部外边距只会凭空加高行，去掉（左右间距保留） */
#p-users .table-wrap td.ops .btn-sm,
#p-cards .table-wrap td.ops .btn-sm { margin: 0 3px 0 0; }
#p-users .table-wrap td.ops .btn-sm { padding: 4px 6px; }
td.ops .btn-sm { margin: 0 4px 4px 0; }
/* 卡片列表同理：未使用的卡一行有 4 个按钮（复制/作废/删除/整批），
   在 188px 里折行后，行高会在 59~87px 之间忽高忽低（「已使用」的卡只有 2 个按钮就更矮）。
   按 4 个按钮单行给宽度，行高才齐。 */
#p-cards .table-wrap td.ops { width: 240px; white-space: nowrap; }
.mono { font-family: Consolas, Menlo, monospace; font-size: 12px; color: var(--dim); }

/* 任务日志详情里的完整原文（0.76.0）：等宽、可横向滚动、限高 —— 一段 8000 字的
   上游报错不能让弹窗无限长；保留空白与换行，方便整段复制给开发。 */
/* 详情弹窗的摘要行允许换行（摘要 + 上下文两行） */
#taskDetailSummary { white-space: pre-line; margin: 0; color: var(--dim); font-size: 13px; line-height: 1.6; }

.log-pre {
  margin: 0;
  max-height: 46vh;
  overflow: auto;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px;
  font-family: Consolas, Menlo, monospace;
  font-size: 12px;
  line-height: 1.6;
  white-space: pre-wrap;
  word-break: break-word;
  overflow-wrap: anywhere;
  user-select: text;
}
/* 软删的行整行变淡，一眼能与正常行区分 */
.row-deleted td { opacity: .45; }

/* 徽标：状态展示比 tag 更紧凑 */
.badge {
  display: inline-block; padding: 1px 7px; border-radius: 5px; font-size: 11px;
  background: var(--panel); border: 1px solid var(--border); color: var(--dim);
}
.badge-on { color: var(--green); border-color: var(--green); }
.badge-off { color: var(--dim); }

/* 直播中：这是「他正在赚钱」的信号，必须一眼看见 —— 用红色描边 + 常亮，
   与「在线」的绿色区分开。在线只说明软件在跑，直播中才说明他正在用。 */
.badge-live { color: var(--red); border-color: var(--red); font-weight: 600; }

/* 直播列：徽章 + 一行小标（采集中 / 口播中）。竖排而不是横排，
   是为了不把这一列撑得比「操作」列还宽。 */
.live-cell { display: flex; flex-direction: column; gap: 3px; align-items: flex-start; }
.live-tags { display: flex; gap: 4px; flex-wrap: wrap; }
/* 小标只做「有/无」的提示，不抢主徽章的注意力 */
.mini-tag {
  display: inline-block; padding: 0 5px; border-radius: 4px; font-size: 10px;
  line-height: 15px; border: 1px solid var(--border); color: var(--dim); white-space: nowrap;
}
.mini-tag.on { color: var(--accent); border-color: var(--accent); }
.mini-tag.play { color: var(--green); border-color: var(--green); }

/* 直播间号：等宽 + 可点击复制。手抄十几位数字必然会错，所以做成一点就复制，
   并在原位置闪一句「已复制」——不弹窗，免得连续核对几个号时挡路。 */
.room-copy { cursor: pointer; user-select: none; }
.room-copy:hover { color: var(--accent); text-decoration: underline; }

.pager { display: flex; align-items: center; gap: 12px; justify-content: flex-end; margin-top: 14px; }
.pager button:disabled { opacity: .4; cursor: not-allowed; }

/* 窄输入：筛选栏里的小数字框，别占满整行 */
.narrow { width: 90px; }
.narrow-field { display: flex; align-items: center; gap: 6px; margin-bottom: 0; }
.narrow-field .field-label { margin-bottom: 0; white-space: nowrap; }
.pane-toolbar .narrow-field { margin-bottom: 0; }

/* 合成排队页工具栏：四个带标签的数字框 + 保存/刷新 + 预留提示。
   默认 .pane-toolbar 是「右对齐 + 不换行」，四个带标签的输入挤在一行会把标签
   和输入拆到两行（实测截图里第三、四组直接错位）。这里改成靠左、允许换行、
   按行居中，并给数字框固定宽度，保证每组标签和输入始终在同一行。 */
#p-queue .pane-toolbar { justify-content: flex-start; flex-wrap: wrap; gap: 10px 16px; align-items: center; }
#p-queue .narrow-field input { width: 96px; }
/* 保存用强调色小按钮、刷新用次按钮，两者默认高度不一样（24px vs 34px），
   并排会一高一低，这里统一按 32px 收口。 */
#p-queue .pane-toolbar .btn-sm,
#p-queue .pane-toolbar .btn-ghost { height: 32px; padding: 0 16px; font-size: 13px; border-radius: 8px; }
/* 预留提示是这一页唯一的「当前生效值」，别跟着标签一起变灰 */
#p-queue #qReserveNow { opacity: 1; }

/* 邀请码：等宽 + 放大字距，方便肉眼核对与抄写 */
.invite-code {
  font-family: Consolas, Menlo, monospace; font-size: 15px; letter-spacing: 2px;
  color: var(--gold); font-weight: 600;
}

/* ---------- 客户详情抽屉 ---------- */
.drawer { position: fixed; inset: 0; z-index: 90; }
.drawer-mask { position: absolute; inset: 0; background: rgba(0,0,0,.55); }
.drawer-panel {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: 660px; max-width: 92vw;
  background: var(--bg); border-left: 1px solid var(--border);
  display: flex; flex-direction: column;
  box-shadow: -12px 0 32px rgba(0,0,0,.35);
}
.drawer-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 18px; border-bottom: 1px solid var(--border); background: var(--panel);
}
.drawer-head h3 { font-size: 16px; }
.drawer-body { flex: 1; overflow-y: auto; padding: 16px 18px 28px; }
.drawer-section { margin-bottom: 18px; }
.drawer-section > h4 {
  font-size: 13px; color: var(--dim); font-weight: 600;
  margin-bottom: 8px; text-transform: none;
}
.drawer-section table { min-width: 0; }
.drawer-section td, .drawer-section th { font-size: 12px; padding: 7px 10px; }
/* 抽屉里的席位表：抽屉 660px 放不下 7 列。按列表页同一约定 ——
   列宽不足时交给 .table-wrap 横向滚动，而不是折行。不这么做会同时出两个问题：
   ① 徽标被折成「正 常」「已激 活」两行，行高翻倍且几乎读不出来；
   ② 没有滚动容器时表格横向溢出会被**裁掉左端**，「登录名」整列不可见 ——
      而客服恰恰要把登录名交付给客户。 */
.drawer-section .table-wrap table { min-width: 640px; }
.drawer-section .table-wrap td,
.drawer-section .table-wrap thead th { white-space: nowrap; padding: 7px 8px; }
/* 概览小卡片：一行铺开几个关键指标 */
.mini-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 10px; }
.mini-card {
  background: var(--card); border: 1px solid var(--border); border-radius: 10px;
  padding: 10px 12px; display: flex; flex-direction: column; gap: 2px;
}
.mini-card span { color: var(--dim); font-size: 12px; }
.mini-card b { font-size: 18px; }
/* 配置代改表单：两列栅格，避免抽屉里输入框竖排过长 */
.drawer-cfg {
  background: var(--card); border: 1px solid var(--border); border-radius: 10px;
  padding: 14px; display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px;
}
.drawer-cfg .field { margin-bottom: 0; }
/* 键值表：概览里的「字段 → 值」 */
table.kv { background: transparent; }
table.kv td { border-bottom: 1px solid var(--border); }
table.kv td:first-child { color: var(--dim); width: 132px; }

/* ---------- 充值卡 ---------- */
/* 未激活标记：这是「客户还没付过钱」的信号，用告警色而不是中性灰 */
.badge-warn { color: var(--gold); border-color: var(--gold); }

/* ---------- 服务期（0.88.0 时间卡） ---------- */
/* 主账号专用：客户明确说绿色不显眼，要红色才一眼看出「这个号能开子账号」。
   单独一个 class（不复用 badge-on）是为了不连带改掉其它绿色徽章的含义。 */
.badge-owner {
  color: #fff; background: var(--red); border-color: var(--red); font-weight: 600;
}
/* 时间卡：与点卡区分开，用蓝色（中性、非告警），避免被误读成「有问题」。 */
.badge-time { color: var(--accent); border-color: var(--accent); }
/* 表格里的文字级强调：剩余时长偏少 / 即将到期 / 成本超限 */
.warn-text { color: var(--gold); }
.danger-text { color: var(--red); font-weight: 600; }
/* 卡号等宽 + 字距，便于肉眼核对与手工抄写（与邀请码同款处理） */
.card-code {
  font-family: Consolas, Menlo, monospace; font-size: 14px; letter-spacing: 1.5px;
  color: var(--gold); font-weight: 600;
}
/* 批次分色圆点：同批同色（色相由 batch_id 哈希得出），
   让「哪几张是一起生成的」不靠读时间也能一眼看出来。
   颜色只做**区分**用途，不承载语义，故用 65% 饱和度的中间调，深浅主题都不刺眼。 */
.batch-dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 6px; vertical-align: middle;
}
/* 「共 N 张」比时间弱一档，避免与卡号抢注意力 */
.batch-cnt { color: var(--dim); font-size: 12px; }
/* 同批的后续行：隐去行间分隔线，把一组卡在视觉上连成一块 */
#p-cards .table-wrap tr.row-batch-cont td { border-top-color: transparent; }
/* 卡片页筛选栏的搜索框不参与「批次」语义，宽度收窄一点留给按钮 */
#p-cards #cKeyword { max-width: 220px; }
/* 卡片页工具栏与筛选栏一样靠左排布（默认 .pane-toolbar 是右对齐） */
#p-cards .pane-toolbar { justify-content: flex-start; flex-wrap: wrap; gap: 8px; align-items: center; }
#p-cards .pane-toolbar select { min-width: 190px; }
/* 卡列表列不多，不必强制最小宽度 */
#p-cards .table-wrap table { min-width: 860px; }
#p-cards .table-wrap thead th { white-space: nowrap; }
/* 禁止数据单元格换行（客户列表早就这么做了，卡片列表当初漏了）。
   不设的话 auto 布局会把列压到「最小可断行宽度」——CJK 标签「未使用」可以在
   任意两字之间断行，于是状态列被压成一列一字（真机上就是「未使/用」三行），
   行高随之虚高到 65~87px 且各列不一致。nowrap 让最小宽度 = 偏好宽度，
   列宽不足时交给 .table-wrap 横向滚动，而不是把内容折成麻花。 */
#p-cards .table-wrap td { white-space: nowrap; }
/* 内边距从 14px 收到 10px：禁止换行后表格自然宽度会顶到容器边界（差 3px 就出现
   横向滚动条），收一点让它稳稳落在容器内。 */
#p-cards .table-wrap td, #p-cards .table-wrap thead th { padding: 9px 10px; }
/* 生成结果弹窗：要能一次看完整批卡号，比普通表单弹窗宽得多 */
.modal-wide { width: 520px; max-width: 92vw; }
/* 内容库弹窗比 genResultModal 宽得多：里面是一张 6 列表格，且有一列要放内容原文。
   980px 是实测出来的：780px 时名称列 + 状态徽标就把表格推到卡片外，
   「操作」列只能靠横向滚动才看得见 —— 页面上有滚动条≠用户知道要滚。 */
.modal-lib { width: 980px; max-width: 94vw; }
/* 列表与详情各自限高滚动，保证弹窗本身不超出视口（否则底部的「关闭」按钮会被推出屏幕）。 */
.lib-scroll { max-height: 48vh; overflow-y: auto; }
.lib-scroll table { min-width: 0; }
.lib-scroll td, .lib-scroll th { font-size: 12px; padding: 7px 8px; white-space: nowrap; }
/* 内容预览列是唯一折行的列：定长 + 换行，比横向滚动更便于扫读。
   注意必须给**内容块**一个确定宽度：只写 `overflow-wrap: anywhere` 的话，
   该列的最小宽度会塌成 1 个字，渲染出来是「纯棉材\n质，48\n小时发…」这种每行 3 字的麻花。
   （`max-width` 在 table-layout:auto 的 td 上不可靠，所以约束下在内层 div 上。） */
.lib-scroll td.lib-text { white-space: normal; }
.lib-scroll .lib-text-inner { width: 240px; white-space: normal; overflow-wrap: anywhere; }
.lib-scroll td.ops { white-space: nowrap; }
.lib-scroll td.ops .btn-sm { margin: 0 3px 0 0; }
/* 工具栏里的下拉（内容库类型筛选、AI 调试的提示词选择）：
   全局 `select` 是 width:100%，直接放进 flex 工具栏会把右侧按钮挤成一条细缝，
   这里按内容宽度收窄（同 `.narrow` 的思路）。用类选择器而不是元素选择器，
   就不会被全局 `select` 规则波及到其他页面。 */
.pill-select { width: 170px; flex: 0 0 auto; }
#promptSelect { width: 240px; }
/* 名称下方的次要信息（更新时间）比正文小一号、用弱色，避免跟条目名抢注意力。 */
.lib-sub { font-size: 11px; opacity: .65; }
.lib-ver { border-left: 2px solid var(--border); padding-left: 10px; margin: 6px 0; }
.lib-ver .lib-ver-text { font-size: 12px; line-height: 1.6; }
.lib-kv td:first-child { width: 96px; color: var(--dim); }
.codes-box {
  font-family: Consolas, Menlo, monospace; font-size: 14px; letter-spacing: 1px;
  line-height: 1.8; color: var(--gold);
}

/* ---------- AI 调试台结果区 ----------
   排版原则：先结论、再明细、最后原文。运营是在「结果不对」时才往下读的，
   所以每一段都要能单独看懂，不要依赖上一段的语境。 */
.dbg-box {
  border: 1px solid var(--border); border-left-width: 3px; border-radius: 10px;
  padding: 12px 14px; margin-top: 14px; background: var(--panel);
}
.dbg-box.ok { border-left-color: var(--green); }
.dbg-box.err { border-left-color: var(--red); }
.dbg-hd { font-size: 14px; font-weight: 600; margin-bottom: 6px; }
/* 统计行是次要信息，弱色小字号，别跟结论抢注意力 */
.dbg-meta { color: var(--dim); font-size: 12px; line-height: 1.9; }
.dbg-err { color: var(--red); font-size: 13px; margin-top: 6px; white-space: pre-wrap; word-break: break-all; }
.dbg-seg { margin-top: 14px; }
.dbg-seg-h { font-size: 13px; font-weight: 600; color: var(--dim); margin-bottom: 6px; }
.dbg-warn { color: #f59e0b; font-size: 13px; }
/* 结果表：内容列要能整段读完，所以它折行、其余列不折 */
.dbg-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.dbg-table th {
  text-align: left; font-weight: 600; color: var(--dim); font-size: 11px;
  padding: 6px 8px; border-bottom: 1px solid var(--border); white-space: nowrap;
}
.dbg-table td { padding: 6px 8px; border-bottom: 1px solid var(--border); vertical-align: top; white-space: nowrap; }
.dbg-table td.dbg-text { white-space: normal; }
/* 徽标本身不许内部折行：「没有触发词，弹幕永远命不中」这类长徽标被折成两行后，
   看起来像一个两行的说明块，而不是一个标记。 */
.dbg-table .tag { white-space: nowrap; }
/* 原文用等宽 + 限高滚动：JSON 动辄几千字，不设高度会把页面顶到看不见结论。
   `overflow-wrap: anywhere` 是必要的 —— 模型偶尔吐一整行超长 JSON，不折行会横向溢出。 */
.dbg-pre {
  font-family: Consolas, Menlo, monospace; font-size: 12px; line-height: 1.6;
  background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
  padding: 10px 12px; margin-top: 6px; max-height: 320px; overflow: auto;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.dbg-fold { margin-top: 12px; }
.dbg-fold > summary { cursor: pointer; font-size: 13px; color: var(--dim); }
.dbg-fold > summary:hover { color: var(--text); }

/* AI 消耗流水的「详情」行：展开后是一块深色区域，用来和正常流水行区分开 */
.aidet-row > td { background: var(--bg); padding: 12px 14px; }
.aidet-row .dbg-seg:first-child { margin-top: 0; }
.aidet-row .dbg-pre { max-height: 260px; }
/* 详情按钮是次要操作，别做成实心按钮抢眼 */
#aiRecords .btn-sm { padding: 3px 12px; }
/* 提示词编辑框：长文编辑，给足高度并用等宽字体做「代码感」区分 */
#promptText { min-height: 260px; }
#p-aidebug .field-hint { margin-top: 8px; }

/* ---------- 文稿整理调试台（后台）----------
   每一步一张卡：头部一行放「第 N/M 步 · 标题 · 状态 · 用量 · 操作按钮」，
   长内容（产出）折在 <details> 里，避免一屏被一篇稿子顶满。 */
.stage-card {
  background: var(--card); border: 1px solid var(--border); border-radius: 10px;
  padding: 12px 14px; margin-bottom: 10px;
}
/* 游标处的那一步高亮：管理员最关心的就是「下一步是哪步」。 */
.stage-card.stage-current { border-color: var(--accent); }
.stage-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.stage-seq { color: var(--dim); font-size: 12px; }
/* 操作按钮推到最右，与左侧的标题/状态分开，扫读时不会混在一起。 */
.stage-ops { margin-left: auto; display: flex; gap: 6px; flex-wrap: wrap; }
.stage-err { color: var(--red); font-size: 12px; margin-top: 6px; overflow-wrap: anywhere; }
.stage-out { margin-top: 8px; }
.stage-out > summary { cursor: pointer; font-size: 12px; color: var(--dim); }
.stage-out > summary:hover { color: var(--text); }
.stage-out pre {
  font-family: Consolas, Menlo, monospace; font-size: 12px; line-height: 1.6;
  background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
  padding: 10px 12px; margin-top: 6px; max-height: 320px; overflow: auto;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
/* 失败徽标用红：badge-off 是「未运行」的弱色，两者语义完全不同，不能共用。 */
.badge-failed { color: var(--red); border-color: var(--red); }


/* ---------- 参考音频（样本）弹窗 ----------
   默认弹窗 360px 放不下五列表格：auto 布局把「原始文件名」压成逐字竖排、
   三个操作按钮被挤出卡片边界（0.60.2 真机截图实测）。加宽到 760px 并禁止
   单元格折行；再窄的屏交给 .sample-scroll 横向滚动（同客户列表 .table-wrap 约定）。 */
#samplesModal .modal-card { width: 760px; max-width: 94vw; max-height: 88vh; overflow-y: auto; }
#samplesModal table { min-width: 660px; }
#samplesModal th, #samplesModal td { white-space: nowrap; }
.sample-scroll { overflow-x: auto; }

/* 每日巡检面板（0.65.0）：留痕按轮分组，异常轮整块标橙提醒 */
.inspect-history { display: flex; flex-direction: column; gap: 8px; max-height: 280px; overflow-y: auto; }
.inspect-run { border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; }
.inspect-run.warn { border-color: var(--gold); }
.inspect-run-time { font-size: 12px; color: var(--dim); margin-bottom: 4px; }
.inspect-run-summary { margin: 0; font-size: 12px; white-space: pre-wrap; word-break: break-all; font-family: inherit; }

/* 费率弹窗的价格字段（2026-10-18 四档）：两列排开，别让输入/输出/缓存/推理
   四个输入框把弹窗拉成一条长龙（每个都占满一行时，时段成本价会被挤到屏幕外）。 */
/* min-width:0 是关键：input 自带 width:100%，留着 min-width:auto 会让
   每个 label 被撑满整行，flex-basis 45% 形同虚设。 */
#rateModal .row > label { flex: 1 1 45%; min-width: 0; }

/* ============================================================
   手机自适应（0.81.1）：只对窄屏生效，桌面端视觉零变化。
   断点取 820px —— 手机横竖屏、小平板都覆盖。
   原则：卡片单列、页签横滑、表格横滚（已有）+ 触屏尺寸、弹窗占满。
   ============================================================ */
@media (max-width: 820px) {

  /* ---- 顶栏：品牌与页签分两行，页签横向滑动（手机上最常见的导航模式） ---- */
  .topbar {
    flex-wrap: wrap;
    padding: 10px 12px;
    row-gap: 8px;
    height: auto;       /* ★ 桌面版定死 56px，两行内容必须放开高度，否则页签溢出盖住正文 */
    position: sticky;   /* 页签条吸顶，长页面往回翻不用滚到顶 */
    top: 0;
    z-index: 50;
  }
  .tabs {
    order: 3;            /* 页签排到第二行 */
    flex-basis: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding-bottom: 2px;
  }
  .tabs::-webkit-scrollbar { display: none; }
  .tab {
    white-space: nowrap;         /* 页签文字不许折行，靠横滑 */
    padding: 9px 14px;           /* 触屏命中区 ≥ 40px 高 */
    flex: 0 0 auto;
  }
  .brand { font-size: 15px; flex: 1; }
  .app-ver { display: none; }    /* 手机上没地方放版本号，省一行 */

  /* ---- 内容区：窄边距，卡片单列 ---- */
  .content { padding: 14px 12px 90px; max-width: 100%; }
  .cards { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  .cards .card { padding: 12px; }
  h2 { font-size: 19px; }

  /* ---- 行式表单：窄屏一律纵向堆叠，输入框占满整行 ---- */
  .row { gap: 10px; }
  .row > .field { flex: 1 1 100%; min-width: 0; }
  .row > button { flex: 0 0 auto; }
  .pane-toolbar, .filter-bar {
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: 8px;
  }
  .pane-toolbar > * { flex: 0 0 auto; }
  .filter-bar .grow { flex: 1 1 100%; }
  .filter-bar select, .filter-bar input { flex: 1 1 46%; }

  /* ---- 触屏尺寸：按钮与输入加大（手机手指点击目标 ≥ 44px） ---- */
  input, textarea, select { padding: 11px 12px; font-size: 16px; }
  /* 16px 输入字号：iOS 聚焦时不会再自动放大页面（<16px 会触发缩放，很烦） */
  button { font-size: 15px; }
  .btn-sm { padding: 10px 14px; }
  .tab-btn, .subtab { padding: 9px 14px; white-space: nowrap; flex: 0 0 auto; }
  /* 子页签（账目/日志的二级页签）同样横滑，否则「余额流水」会被压成一字一行 */
  .sub-tabs { overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; flex-wrap: nowrap; }
  .sub-tabs::-webkit-scrollbar { display: none; }

  /* ---- 表格：保持横向滚动（已有），加触屏顺滑与两端渐隐提示 ---- */
  .table-wrap {
    -webkit-overflow-scrolling: touch;
    position: relative;
  }
  .table-wrap table { min-width: 720px; }  /* 手机上不必强撑 1020，能横滑即可 */

  /* ---- 弹窗：窄屏占满宽度，动作按钮固定底部好按 ---- */
  .modal-card {
    width: calc(100vw - 20px) !important;
    max-width: calc(100vw - 20px);
    max-height: 88vh;
    padding: 16px !important;
  }
  .modal-actions { flex-wrap: wrap; }
  .modal-actions button { flex: 1 1 auto; min-height: 44px; }

  /* ---- 登录卡：留出边距不被顶出屏幕 ---- */
  .login-card { width: min(340px, calc(100vw - 28px)); padding: 26px 20px; }


  /* 费率添加行：默认 flex 不换行，窄屏挤压成一条 —— 允许换行纵排 */
  .rate-add { flex-wrap: wrap; }
  .rate-add input, .rate-add select { flex: 1 1 100% !important; min-width: 0 !important; }
  .rate-add .btn-primary { flex: 1 1 100%; }
  /* ---- 概览统计数字：两列排布时字号收一点，避免溢出 ---- */
  .stat-value { font-size: 20px !important; }  /* 实际类名是 stat-value（stat-num 是笔误修正） */
}

/* 超窄屏（<420px）：统计卡也单列，彻底告别挤压 */
@media (max-width: 420px) {
  .cards { grid-template-columns: 1fr; }
  .content { padding: 12px 10px 90px; }
}

/* ---------- 音色管理：两层视图（音色 + 副本矩阵，2026-10-28）----------
   主表一行 = 一个音色（客户端看到的名字）；展开行 = 它在各账号×模型上的副本。
   展开行是 <tr><td colspan>，所以内层表格要自己收住宽度，不要把主表撑变形。 */
.voice-copy-sum { font-size: 11px; color: var(--dim); margin-top: 2px; }
.voice-copy-sum.warn { color: #f59e0b; opacity: .95; }
.copy-panel > td {
  background: rgba(127, 127, 127, .06);
  border-left: 3px solid var(--accent);
  padding: 12px 14px !important;
}
.copy-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 4px; }
.copy-head b { font-size: 13px; }
.copy-pools {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin: 8px 0 10px; padding: 8px 10px;
  border: 1px dashed rgba(127, 127, 127, .35); border-radius: 6px;
}
.copy-pool { font-size: 12px; }
.copy-pool-off { font-size: 12px; color: var(--dim); opacity: .55; text-decoration: line-through; }
.copy-table { width: 100%; margin-top: 6px; font-size: 12px; table-layout: auto; }
.copy-table th, .copy-table td { padding: 6px 8px !important; font-size: 12px; }
.copy-table th { white-space: nowrap; }
.copy-form {
  display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap;
  margin: 8px 0; padding: 10px;
  border: 1px solid rgba(127, 127, 127, .3); border-radius: 6px;
}
.copy-form .field { margin-bottom: 0; }
.copy-form .field-label { font-size: 11px; }
