/* free-llm-router 管理面板样式 */
:root {
  --bg: #0f1115; --panel: #171a21; --panel2: #1d2129; --line: #262b36;
  /*
   * ## 圆角令牌（用户要求「所有圆角统一成供应商与账号的大圆角」）
   *
   * ⚠️ 改之前全站有 **10 种**圆角值（3/4/6/8/10/12/14/16px…），
   * 同类容器在不同页面上各不相同 —— 那是「每处单独调」积累出来的漂移，
   * 不盯着看不出来，但整体观感就是不齐。
   *
   * ⇒ 收成**两个令牌**（对齐本项目「把 N 个独立值收敛成一个共享令牌」的纪律）：
   *   · `--radius`    卡片/面板/弹窗等**大容器**（供应商卡就是 14px）
   *   · `--radius-sm` 小控件（按钮/标签/输入框内的东西）
   * ⚠️ 药丸形（`999px`，`.tag` / `.credits-pill`）**不动** ——
   * 它是另一种设计语言（全圆），不是「圆角没统一」。
   */
  --radius: 14px; --radius-sm: 8px;
  --text: #e6e9ef; --dim: #9aa4b2;
  /*
   * ⚠️ accent **拆成两个令牌** —— 它们对底色的要求相反。
   *
   * 背景（按钮底、导航选中、进度条填充）：上面压**白字** ⇒ 要够深，
   *   否则白字对比度不足（WCAG AA 正文需 4.5:1）。
   * 文字（当强调色文字、描边）：压在**深色背景**上 ⇒ 要够**亮**。
   *
   * 原先共用一个 `#4f8cff`：当按钮底时白字只有 **3.22:1（不达标）**，
   * 但当文字时是 **5.97:1（很好）**。⇒ 无论往哪边调都会修一个、坏另一个。
   * 拆开后各取所需（实测：白字 on #3b6fd4 = 4.76:1；#4f8cff 当文字 = 5.97:1）。
   */
  --accent: #3b6fd4; --accent-text: #4f8cff;
  --ok: #34c77b; --warn: #e0a33e; --err: #ef5f5f;
  /*
   * ⚠️ `--err-solid`：**当底色用**的错误色（白字压在上面）。
   *
   * 与 `--accent` / `--accent-text` 是**同一个取舍**（见下方 accent 的说明）：
   * `--err`（#ef5f5f）当**文字**时压在深色底上很清楚（7.0:1），
   * 但当**按钮底色**时白字只有 **3.24:1** —— 不达 WCAG AA 正文 4.5:1。
   *
   * 实测确认（面板全量对比度扫描）：API Key 页的「删除」按钮报 3.24:1。
   * 同一个值不可能同时满足「当文字要够亮」与「当底色要够暗」，
   * 故拆成两个令牌 —— 与本项目对 accent 的处理逐字一致。
   *
   * #c0392b → 白字 5.44:1（两种主题下都达标，故只在 :root 定义一次）。
   */
  --err-solid: #c0392b;
}
* { box-sizing: border-box; }
/*
 * ⚠️ 全局防御：`[hidden]` 必须压过任何作者样式的 `display`。
 *
 * 根因（线上实测）：`.banner { display: flex }` 的优先级**高于**浏览器默认的
 * `[hidden] { display: none }`（作者样式 > UA 样式），于是 `hidden` 属性
 * **静默失效** —— 横幅永远显示，即使服务端说 `active: false`、
 * 点「立即解除」也没用（不报错，只是不生效）。
 *
 * 这里用 `!important` 一劳永逸：**任何**元素只要带 `hidden` 就一定不显示，
 * 不必记得给每个设了 `display` 的类补一条 `[hidden]` 规则。
 */
[hidden] { display: none !important; }
body {
  margin: 0; background: var(--bg); color: var(--text);
  font: 14px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", "Noto Sans SC", sans-serif;
}
header {
  display: flex; flex-wrap: wrap; gap: 12px; align-items: center; justify-content: space-between;
  padding: 12px 20px; border-bottom: 1px solid var(--line); background: var(--panel);
}
.brand { display: flex; gap: 12px; align-items: center; }
/* ⚠️ `h1` 已不在页面上（站名移到导航条内），但保留规则以防后续复用 */
h1 { font-size: 15px; margin: 0; letter-spacing: .3px; }
/*
 * 标题
 *
 * ⚠️ `h2` 的**下边距归零** —— 它现在总是放在 `.page-head` 里，
 * 而那个容器自己管间距（`margin-bottom: 10px`）。
 * 两处都加会让标题下方多出一段空白，越看越松。
 * ⚠️ `h3` 同理：它现在总在 `.panel-head` 里，下边距由容器给。
 * ⚠️ 但 `h3` **保留非布局场景的下边距**（少数地方 h3 是直接子元素，
 * 如任务页的「执行结果」），故用 `:only-child` 之外的场景保留。
 */
h2 { font-size: 14px; margin: 0; display: flex; gap: 10px; align-items: center; }
h3 { font-size: 13px; margin: 0; color: var(--dim); font-weight: 500; }
/* 直接坐在视图里的 h3（不在 .panel-head 内）仍需要上下间距 */
section.view > h3 { margin: 16px 0 8px; }

nav {
  display: flex; flex-wrap: wrap; gap: 4px; padding: 8px 16px;
  border-bottom: 1px solid var(--line); background: var(--panel);
}
.nav-item {
  background: transparent; border: 1px solid transparent; color: var(--dim);
  padding: 6px 12px; border-radius: var(--radius-sm); cursor: pointer; font: inherit;
}
.nav-item:hover { color: var(--text); background: var(--panel2); }
.nav-item.active { color: #fff; background: var(--accent); border-color: var(--accent); }

/*
 * 页面主容器（**纯布局**，视觉边界交给内部的 `.panel` / `.kpi`）。
 *
 * ⚠️ `max-width` 从 1180 提到 1240：视图容器去掉内边距后内容区反而变宽，
 * 而 KPI 卡在 1180 时刚好排成 **5 列**（我们正好 5 张卡）——
 * 每张被压到约 210px，数字稍长就折行。放宽后是 4 列，读起来舒服得多。
 */
main { padding: 20px; max-width: 1240px; margin: 0 auto; display: grid; gap: 16px; }
/*
 * ⚠️⚠️ **视图容器本身不再是「一张卡」**（用户报「按天趋势里面不要再套一个框」）。
 *
 * 根因：`section.view` 原先自带 `background + border + radius`，而它内部
 * 现在**全部是卡片**（`.panel` / `.kpi` / `.prov-card`）⇒ 每个内部卡片外围
 * 都又多了一圈同样的框，也就是「框里套框」。
 *
 * 这与用户的抱怨是同一件事：他要的是**扁平的一层卡片结构**。
 * ⇒ 视图容器退成**纯布局容器**（无背景、无边框、无内边距），
 *   由内部的 `.panel` / `.kpi` 自己承担视觉边界。
 *
 * ⚠️ 仍然保留 `min-width: 0`（网格项默认不收缩，去掉会让长表格撑破页面 ——
 * 见文件里 `main > *` 那段踩坑记录）。
 */
/*
 * 视图容器 = **纯布局容器**（无背景/边框/内边距）。
 *
 * ⚠️ 原先是 `background + border + radius + padding` —— 那是「一张卡」。
 * 而它内部现在**全是卡片**（`.panel` / `.kpi` / `.prov-card`）
 * ⇒ 每个内部卡片外围又多一圈同样的框，即用户抱怨的「框里套框」。
 * 视觉边界改由内部卡片自己承担。
 *
 * ⚠️ `min-width: 0` 必须保留：网格项默认不收缩，去掉会让长表格撑破页面
 *（见下方 `main > *` 那段踩坑记录）。
 * ⚠️ `display: grid; gap` 让视图内的多个面板有统一间距
 *（容器不再有 padding，间距得自己给）。
 */
section.view { min-width: 0; display: grid; gap: 14px; align-content: start; }
/*
 * ⚠️⚠️ **`section.view` 的子元素也必须能收缩**（用户报「配置与状态页面在手机上
 * 挤出屏幕了」）。
 *
 * 根因：上一轮我把 `section.view` 改成 `display: grid`（为拿统一下间距），
 * 于是它的子元素成了**网格项** —— 而网格项默认 `min-width: auto`
 *（「不小于内容的最小宽度」）。配置页的表格按 `max-content` 取宽后，
 * 这个最小值跟着变大 ⇒ 网格轨道被撑开 ⇒ **整个页面横向溢出**。
 *
 * ⚠️ `main > * { min-width: 0 }` 只覆盖了 main 的**直接**子元素（即 section.view），
 * 对**再下一层**无效。这是同一个陷阱的第二层，必须逐层清。
 *
 * ⚠️ 纪律：**每当引入一个新的 grid/flex 容器，就要给它的子元素加 `min-width: 0`。**
 * 项目此前已经为 `main` 和 `.count` 各踩过一次，这是第三次。
 */
section.view > * { min-width: 0; }

.auth { display: flex; gap: 8px; align-items: center; }
input, select, textarea, button {
  font: inherit; color: var(--text); background: #10131a;
  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 7px 9px;
}
textarea { width: 100%; font-family: ui-monospace, monospace; font-size: 12px; }
button { cursor: pointer; background: var(--accent); border-color: var(--accent); color: #fff; }
button.ghost { background: transparent; border-color: var(--line); color: var(--text); }
button:hover { filter: brightness(1.12); }
button.danger { background: var(--err-solid); border-color: var(--err-solid); }
button:disabled { opacity: .5; cursor: not-allowed; }

.row { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; margin-bottom: 10px; }
.row label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--dim); }
.pill { font-size: 12px; color: var(--dim); }
.pill.ok { color: var(--ok); } .pill.err { color: var(--err); }
/*
 * 说明文字。
 *
 * ⚠️ 上下边距**归零**：它现在多半是 `section.view` 或 `.panel` 的直接子元素，
 * 而那两个容器都有 `gap` —— 再加 margin 就是双重间距。
 *（`.hint` 在旧结构里靠 margin 撑开，容器改布局后必须跟着改。）
 */
.hint { color: var(--dim); font-size: 12px; margin: 0; }

.out {
  background: #0c0f14; border: 1px solid var(--line); border-radius: var(--radius);
  padding: 10px; font-family: ui-monospace, monospace; font-size: 12px;
  white-space: pre-wrap; word-break: break-word; max-height: 420px; overflow: auto;
}
/*
 * ⚠️ 空的输出框不该画出一个空框。
 *
 * 实测（P3）：`#login-result` / `#import-result` 在无内容时仍渲染成
 * 22px 高的带边框空盒子 —— 看起来像「有个东西没加载出来」。
 * 这些容器是**先占位、后填内容**的，所以空的时候直接不显示。
 */
.out:empty { display: none; }

/*
 * ⚠️ `.out` **放在 `.panel` 里时不要再套一层框**。
 *
 * `.out` 自己就有 `border + radius + background`，而 `.panel` 也是同样的三件套
 * ⇒ 又是「框里套框」（用户明确抱怨过这个观感）。
 *
 * 处理：保留 `.out` 的**深色背景**（它表达「这是原始输出」，是有意义的
 * 层级差），但**去掉描边**，让外层的 `.panel` 独自承担边框。
 * —— 相邻嵌套面必须有视觉差异（背景步进或阴影），这里用**背景步进**。
 */
.panel > .out { border: 0; }

/*
 * ⚠️ 空面板不应留下一个空框：`.out:empty` 自己会 `display:none`，
 * 但外层 `.panel` 的 padding 还在，看起来像个莫名的空卡片。
 * `:has()` 现代浏览器（Chrome 105+ / Safari 15.4+）都支持；
 * 不支持时只是多一个空框，不影响功能。
 */
.panel:has(> .out:empty) { display: none; }

.notice {
  background: #1a2432; border: 1px solid #2a3d57; border-radius: var(--radius); padding: 14px 16px;
}
.notice p { margin: 6px 0 0; font-size: 13px; color: var(--dim); }
/*
 * ⚠️ `[hidden]` 必须显式声明 `display: none`。
 *
 * 根因（线上实测）：`.banner { display: flex }` 的**优先级高于**
 * 浏览器默认的 `[hidden] { display: none }`（作者样式压过 UA 样式），
 * 于是 `hidden` 属性完全失效 —— 横幅**永远显示**，
 * 即使服务端 `active: false`、点「立即解除」也没用。
 *
 * 教训：任何给元素设了 `display` 的类，都要配一条 `[hidden]` 规则，
 * 否则 `hidden` 属性会静默失效（不报错、只是不生效）。
 */
.banner {
  display: flex; flex-wrap: wrap; gap: 12px; align-items: center; justify-content: space-between;
  background: #3a2020; border: 1px solid #5b2424; color: #ffd9d9;
  border-radius: var(--radius); padding: 12px 16px; font-size: 13px;
}
.banner button { background: transparent; border-color: #7a3a3a; color: #ffd9d9; }

/*
 * 计数卡片组（账号总数 / 可用 / 冷却中 / 模型限流 / 已禁用 / 供应商）。
 *
 * ## 🔴 为什么用 grid 而不是 flex（用户报「6 个卡片在电脑上太窄了」）
 *
 * 原先是 `flex-wrap` + `.count { min-width: 92px }`：6 张卡只占 **602px**，
 * 而桌面容器有 1200px ⇒ **右侧空出近 600px**，卡片又窄又挤在一角。
 *
 * ⚠️ 也不能简单地给 flex 加 `flex-grow: 1` —— 项目此前记录过那个坑：
 * 卡片个数为奇数时（曾经是 5 个），`grow` 会让**最后一张被拉伸成整行宽**，
 * 看起来像被选中的高亮项。
 *
 * ⇒ 改用 `grid` + `auto-fit`：**所有轨道等宽**，卡片自动铺满整行，
 *   且因为 `auto-fit` 会折叠空轨道，**不存在「最后一张被拉宽」**的问题
 *  （奇数个时最后一行只是左侧对齐、宽度与上面一致）。
 *   这与 `.kpi-grid` 是同一套做法，观感统一。
 */
.counts {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.count {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 9px 13px; min-width: 92px;
  box-shadow: 0 1px 2px rgba(0,0,0,.03);
}
/* ⚠️ 数字用 tabular-nums：计数块并排时数字宽度一致，不会左右跳动 */
.count b { display: block; font-size: 19px; font-weight: 600; font-variant-numeric: tabular-nums; }
.count span { font-size: 11px; color: var(--dim); }
/*
 * 可点击的计数块（冷却中 / 模型限流）。
 *
 * ⚠️ **边框必须与其它计数块一致**（用户明确要求「不要蓝色描边」）——
 * 用户不想要一个视觉上突兀的卡片。可点性靠三件更克制的事表达：
 * hover 背景、手型光标、右上角的小箭头。
 */
.count { position: relative; }
.count.clickable { cursor: pointer; }
.count.clickable:hover { background: var(--hover, rgba(127, 127, 127, 0.12)); }
.count.clickable::after {
  content: '▸';
  position: absolute; top: 6px; right: 8px;
  font-size: 10px; color: var(--dim); opacity: 0.65;
}

.card {
  border: 1px solid var(--line); border-radius: var(--radius); padding: 10px 12px; margin-bottom: 8px;
  display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
  background: var(--panel);
  /* ⚠️ 与 `.panel` / `.kpi` 用同一套阴影与圆角 —— 用户要求「把其它界面的 UI
     也统一成这种风格」。旧值 8px 圆角 + 无阴影，与新的 16px 卡片并排会明显
     不是一套。 */
  box-shadow: 0 1px 2px rgba(0,0,0,.03);
}
/* ⚠️ uid 是长串（如 `raccoon:raccoon-9696afc62b16c498`），窄屏会撑破卡片 */
.card .uid {
  font-family: ui-monospace, monospace; font-size: 12px; color: var(--dim);
  overflow-wrap: anywhere;
}
.card .grow { flex: 1 1 220px; }
.tag { font-size: 11px; padding: 2px 7px; border-radius: 999px; border: 1px solid var(--line); }
/*
 * ⚠️ 「免费」徽章给一层**淡绿底**（用户要求「免费用绿色显示免费」）——
 * 只有绿字在密集表格里不够显眼，而「免费」是最值得一眼看到的信息。
 * ⚠️ 底色用 `rgba` 而不是写死的绿：深色主题下才不会变成一块刺眼的亮绿。
 */
.tag.ok { color: var(--ok); border-color: #1e5b3a; background: rgba(34, 197, 94, .12); }
.tag.warn { color: var(--warn); border-color: #5b4a1e; }
.tag.err { color: var(--err); border-color: #5b2424; }
.metrics { color: var(--dim); font-size: 11px; font-family: ui-monospace, monospace; }

/*
 * ⚠️ 整段错误文案（积分包页）。
 *
 * 用块级而不是 `.tag`（999px 药丸）：错误原因往往是一整句话，
 * 塞进药丸形标签里既难看又会把卡片挤变形。
 * 这里显式允许换行 —— 原先前端把错误 `slice(0, 60)` 硬截断，
 * 用户只看到「Unauthorized: Please make sure you're」这种半句话，
 * 完全不知道该重新登录还是等一会。
 */
.err-msg {
  flex: 1 1 100%; font-size: 11px; line-height: 1.5;
  color: var(--err); background: rgba(239, 95, 95, 0.08);
  border: 1px solid #5b2424; border-radius: var(--radius-sm); padding: 6px 9px;
  overflow-wrap: anywhere; word-break: break-word;
}
html[data-theme='light'] .err-msg { background: #fdecec; border-color: #f0a0a0; }

table { border-collapse: collapse; width: 100%; font-size: 12px; }
th, td { text-align: left; padding: 5px 8px; border-bottom: 1px solid var(--line); }
/*
 * ⚠️ 表格单元格必须能断行。
 *
 * 配置页的值里有 `AES-GCM（密钥来自 CREDENTIAL_KEY secret）`、
 * `每小时 1 次（0 * * * *）` 这类**含长 token 的句子**。单元格默认不折行，
 * 于是「值」这一列按最长内容取宽 ⇒ 表格变宽 ⇒ 配合网格项的
 * `min-width: auto` 直接撑破页面（用户报「配置与状态页面在手机上挤出屏幕了」）。
 *
 * ⚠️ 用 `overflow-wrap: anywhere`（不是 `word-break: break-all` ——
 * 后者会把正常英文单词也拦腰截断，项目单测明确禁止）。
 */
th, td { overflow-wrap: anywhere; }
th { color: var(--dim); font-weight: 500; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }

.bars { display: flex; flex-direction: column; gap: 3px; }
.bar-row { display: flex; align-items: center; gap: 8px; font-size: 11px; }
/* ⚠️ 时段标签是 `10-03 20:00` 这类固定宽度文本，断行会破坏对齐 ⇒ 用 nowrap */
.bar-label {
  width: 78px; color: var(--dim); font-family: ui-monospace, monospace;
  white-space: nowrap;
}
.bar-track { flex: 1; background: #10131a; border-radius: 3px; height: 14px; position: relative; overflow: hidden; }
.bar-fill { background: var(--accent); height: 100%; }
.bar-fill.bad { background: var(--err); }
/* ⚠️ 数值列要够宽才放得下带单位的文案（如「22 次请求（失败 3）」） */
.bar-val { width: 132px; text-align: right; color: var(--dim); font-variant-numeric: tabular-nums; }
/* 该时段的 token 量（原版没有这一列，用户不知道每小时花了多少） */
.bar-tokens { width: 78px; text-align: right; color: var(--dim); font-variant-numeric: tabular-nums; }
/* 迷你表头：告诉用户右侧两个数字分别是什么 */
.bar-head { font-size: 10px; color: var(--dim); opacity: .8; margin-bottom: 2px; }
.bar-head span:nth-child(2) { flex: 1; }
.bar-head span:nth-child(3) { width: 132px; text-align: right; }
.bar-head span:nth-child(4) { width: 78px; text-align: right; }

/* ─────────── 用量页：KPI 卡片（照 cpa-usage-keeper 的观感） ─────────── */
/*
 * 参考项目的观感要点（已从其总览截图确认）：
 *   · 大号数字（约 28px / 粗体）+ 小号带单位副标题
 *   · 左上角一个**彩色图标章**，每张卡一个色系（蓝/紫/绿/橙/青/粉）
 *   · 卡片圆角 16–20px、极淡的描边 + 柔和阴影
 *   · 深色主题下卡片比背景**略亮**，而不是更黑
 *
 * ⚠️ 我们的面板是**手写 CSS + 明暗两套主题**，故这里只用 CSS 变量，
 * 不引第三方 UI 库 —— 否则要么打包体积暴涨，要么与既有主题打架。
 */
.kpi-grid {
  display: grid; gap: 12px;
  /* ⚠️ 自适应列数由 `minmax` 决定（窄屏 1 列 → 宽屏 4~5 列）。
     ⚠️ 不写 `margin-bottom`：它总是放在 `section.view`（`gap: 14px`）里，
     再加下边距就是**双重间距**（本项目改布局时反复踩到这一点）。 */
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.kpi {
  position: relative;
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 14px 16px 13px;
  background: var(--panel);
  /* 柔和阴影（参考项目用 0 10px 18px -3px）——比纯描边更有层次 */
  box-shadow: 0 1px 2px rgba(0,0,0,.04), 0 8px 18px -8px rgba(0,0,0,.10);
  overflow: hidden;
}
/* 左上角的彩色图标章 */
.kpi-chip {
  width: 26px; height: 26px; border-radius: var(--radius-sm);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 13px; line-height: 1; margin-bottom: 10px;
  background: var(--kpi-soft, rgba(79,140,255,.14));
  color: var(--kpi, var(--accent-text));
}
.kpi-label { font-size: 11px; color: var(--dim); margin-bottom: 3px; }
/* ⚠️ 数字要大：这是整张卡的主体（参考项目约 28px/900） */
.kpi-value {
  font-size: 25px; font-weight: 700; line-height: 1.15;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  /* ⚠️ 用 `overflow-wrap` 而不是 `word-break: break-all` —— 后者会把正常
     英文单词也拦腰截断（项目单测明确禁止，见 tests/panel.test.ts）。 */
  overflow-wrap: anywhere;
}
.kpi-sub { font-size: 11px; color: var(--dim); margin-top: 5px; }
/* 每张卡的色系（照参考项目的蓝/紫/绿/橙/青/粉） */
.kpi.blue   { --kpi: #4f8cff; --kpi-soft: rgba(79,140,255,.14); }
.kpi.purple { --kpi: #a855f7; --kpi-soft: rgba(168,85,247,.14); }
.kpi.green  { --kpi: #22c55e; --kpi-soft: rgba(34,197,94,.14); }
.kpi.orange { --kpi: #f59e0b; --kpi-soft: rgba(245,158,11,.15); }
.kpi.cyan   { --kpi: #06b6d4; --kpi-soft: rgba(6,182,212,.14); }
.kpi.pink   { --kpi: #ec4899; --kpi-soft: rgba(236,72,153,.14); }
.kpi.red    { --kpi: var(--err); --kpi-soft: rgba(239,95,95,.14); }

/* ─────────── KPI 卡内的迷你趋势线（sparkline） ─────────── */
/*
 * 照参考项目：KPI 卡底部一条细折线，让「数字」带上「走向」。
 * ⚠️ `preserveAspectRatio: none`（在 JS 里设）+ `height` 固定、
 * `width: 100%` ⇒ 折线自适应卡片宽度而不留边距。
 * ⚠️ `vector-effect: non-scaling-stroke`（在 JS 里设）保证横向拉伸时
 * **描边粗细不变**，否则宽卡上的线会被拉成一片色块。
 */
.spark {
  display: block; width: 100%; height: 28px;
  margin-top: 8px; overflow: visible;
}
.spark path { stroke: var(--kpi, var(--accent-text)); }
/* 每张卡用自己色系的线（与图标章同色） */
.spark-blue path   { stroke: #4f8cff; }
.spark-purple path { stroke: #a855f7; }
.spark-green path  { stroke: #22c55e; }
.spark-orange path { stroke: #f59e0b; }
.spark-cyan path   { stroke: #06b6d4; }
.spark-pink path   { stroke: #ec4899; }
.spark-red path    { stroke: var(--err); }

/* ─────────── 用量页：排名列表（照参考项目的 Top Models） ─────────── */
.rank-row {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 2px; border-bottom: 1px solid var(--line);
}
.rank-row:last-child { border-bottom: 0; }
/* 序号徽章：前三名给一个强调色，其余保持中性（不喧哗） */
.rank-idx {
  flex: none; width: 20px; height: 20px; border-radius: var(--radius-sm);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 600; color: var(--dim);
  background: var(--panel2);
}
.rank-idx.top1 { background: rgba(79,140,255,.16); color: #4f8cff; }
.rank-idx.top2 { background: rgba(168,85,247,.16); color: #a855f7; }
.rank-idx.top3 { background: rgba(34,197,94,.16); color: #22c55e; }
.rank-main { flex: 1 1 auto; min-width: 0; }
.rank-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; margin-bottom: 5px;
}
.rank-name {
  font-size: 12px; font-weight: 500;
  /* ⚠️ 长模型名/uid 必须能断行，且用 `overflow-wrap` 而非 `break-all`
     （后者会截断正常英文单词，项目单测禁止）。 */
  overflow-wrap: anywhere; min-width: 0;
}
.rank-share {
  flex: none; font-size: 11px; color: var(--dim);
  font-variant-numeric: tabular-nums;
}
.rank-track {
  height: 6px; border-radius: 3px; background: #10131a; overflow: hidden;
}
.rank-fill { height: 100%; background: var(--accent); border-radius: 3px; }
.rank-fill.bad { background: var(--err); }
.rank-metrics {
  flex: none; display: flex; gap: 10px; align-items: baseline;
  font-size: 11px; color: var(--dim); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.rank-failed { color: var(--err); }
/* ⚠️ 浅色主题下轨道底色要换 —— **两套都要覆盖**（见 AGENTS.md §9.1b ②g：
   显式 light 与「跟随系统」是两套独立规则，漏一处就只在某种模式下出错）。 */
html[data-theme='light'] .rank-track { background: #e8ebf0; }
html[data-theme='light'] .rank-idx { background: #eef1f5; }

/* ─────────── 用量页：通用面板容器 ─────────── */
.panel {
  border: 1px solid var(--line); border-radius: var(--radius);
  /* ⚠️ 不写 `margin-bottom`：面板总是 `section.view`（`gap: 14px`）的子元素，
     间距由容器统一给。自己再加一份就是双重间距。 */
  background: var(--panel); padding: 14px 16px;
  box-shadow: 0 1px 2px rgba(0,0,0,.03);
}
.panel-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; margin-bottom: 12px; flex-wrap: wrap;
}
.panel-head h3 { margin: 0; font-size: 13px; font-weight: 600; }

/* ─────────── 用量页：页头（标题 + 工具行） ─────────── */
.page-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  /* ⚠️ `margin-bottom: 0` —— 间距全部交给 `section.view` 的 `gap: 14px`。
     这里再加一点就会变成「gap + margin」双重间距，标题与内容之间显得空旷。 */
  margin-bottom: 0;
}
.page-head h2 { margin: 0; }
.page-tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* 页头里的分段控件不需要下边距（原先 .seg 自带 margin-bottom:12px） */
.page-tools .seg { margin-bottom: 0; }

/* ─────────── 时间窗分段控件（用量页） ─────────── */
.seg {
  display: inline-flex; gap: 2px; padding: 3px; margin-bottom: 12px;
  background: var(--panel2); border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.seg-item {
  background: transparent; border: 0; color: var(--dim);
  font-size: 12px; padding: 5px 12px; border-radius: var(--radius-sm); cursor: pointer;
  font-family: inherit; transition: background .12s, color .12s;
}
.seg-item:hover { color: var(--text); }
.seg-item.active { background: var(--accent); color: #fff; }
html[data-theme='light'] .seg-item.active { color: #fff; }
/* ⚠️ 用量页 KPI 卡：浅色下阴影要略重一点才有层次（深色下靠亮色卡片本身）。
   ⚠️ **必须与下方 `prefers-color-scheme` 那一份成对出现** ——
   两套浅色规则是独立维护的，只写一处就会在「跟随系统」模式下漏掉
   （本项目已为此踩过一次，见 AGENTS.md §9.1b ②g）。 */
html[data-theme='light'] .kpi {
  box-shadow: 0 1px 2px rgba(0,0,0,.04), 0 10px 20px -12px rgba(0,0,0,.18);
}

/* ─────────── 按天趋势柱 ─────────── */
/*
 * ⚠️ 用 flex 横排 + 每列固定最小宽度，而不是等分整行：
 * 等分会让「30 天窗口」的柱子细到看不见，而「唯一一根柱」被拉成整条白块
 *（参考实现记录过这个真实缺陷）。
 */
.trend {
  display: flex; align-items: flex-end; gap: 4px;
  overflow-x: auto; padding: 8px 2px 0;
  /* ⚠️ **不要再套一层框**（用户报「按天趋势里面不要再套一个框」）。
     它本身已经在一个 `.panel`（有边框/圆角/底色）里了，
     再加一层就是「框里套框」—— 视觉上冗余，也让层级变模糊。
     ⇒ 去掉 `border` / `background` / `margin-bottom`（外层 panel 已有内边距）。 */
  /* ## 🔴 必须限宽（用户报「太宽不好看」）
   *
   * `.trend-col` 的 `flex-basis` 是 26px（为了让窄窗口的柱子不至于细到看不见），
   * 而列**只多不少**：30 天窗口 ⇒ 30 列 ⇒ 光柱子就要 780px+，
   * 在 1180px 的主容器里被 `flex-grow` 拉得更宽，整条图横铺满屏、很难看。
   *
   * ⇒ 给整块一个**上限**，超过就横向滚动（`overflow-x: auto` 已有）。
   * 只加 `max-width` 而不动 `flex-basis`：前者限制**整块**的观感，
   * 后者保证**单根柱**在窄屏仍可见 —— 两件事互不冲突。
   *
   * ⚠️ 用 `max-width: 100%` 兜底窄屏：不能让它溢出容器。
   */
  max-width: 760px;
  width: 100%;
}
.trend-col {
  /* ⚠️ **每列必须有宽度上限**（原先是 `flex: 1 0 26px`，grow=1 会撑满剩余空间）：
   * 30 列时每列被拉宽、柱子跟着变胖；单列时更糟 —— 一根柱被拉成整条白块
   *（这正是参考实现记录过的那个观感缺陷）。
   *
   * ⇒ `1 1 26px` 允许长高但**封顶 56px**：单列时是一根正常的柱（不是白块），
   * 30 列时每列被压到 21px 左右仍可见（且不会被拉胖）。
   */
  flex: 1 1 26px;
  max-width: 56px;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
}
.trend-val {
  font-size: 10px; color: var(--dim); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.trend-track {
  width: 100%; height: 64px; display: flex; align-items: flex-end;
  background: #10131a; border-radius: 4px; overflow: hidden;
}
.trend-fill {
  width: 100%; background: var(--accent); border-radius: 3px 3px 0 0;
  min-height: 2px; transition: height .15s;
}
.trend-fill.bad { background: var(--err); }
/* 无数据的那天：一条极矮的底衬，表示「这天存在但没请求」 */
.trend-fill.empty { background: var(--line); }
.trend-label {
  font-size: 10px; color: var(--dim); font-family: ui-monospace, monospace;
  white-space: nowrap;
}
.trend-label.today { color: var(--accent-text); font-weight: 600; }
html[data-theme='light'] .trend-track { background: #e8ebf0; }

/* ─────────── 供应商网格 ─────────── */
.prov-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 10px; }
.prov-card {
  border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 14px;
  cursor: pointer; background: var(--panel); transition: border-color .12s, box-shadow .12s;
  box-shadow: 0 1px 2px rgba(0,0,0,.03);
}
.prov-card:hover { border-color: var(--accent-text); }
.prov-card .prov-name {
  font-weight: 600; font-size: 13px;
  min-width: 0; overflow-wrap: anywhere;
}
/*
 * ⚠️ 右侧要**预留**积分药丸的位置，否则长名称会钻到它下面
 *（药丸是绝对定位、不参与布局，只能靠内边距让位）。
 *
 * ⚠️ 但**不能无条件预留** —— 没有积分的卡片（未登录 / 不支持查积分的家）
 * 会白空出右侧一大块。故用 `:has()` 只在**药丸真的显示时**才让位。
 * 现代浏览器（Chrome 105+ / Safari 15.4+）均支持；不支持时只是可能重叠，
 * 不影响功能。
 */
.prov-card:has(.prov-credits:not([hidden])) .prov-name { padding-right: 96px; }
/* ⚠️ 同上（provider id 也是长串）；顺带补断行 */
.prov-card .prov-id {
  font-family: ui-monospace, monospace; font-size: 11px; color: var(--dim);
  margin-bottom: 6px; overflow-wrap: anywhere;
}
.prov-card .caps { display: flex; flex-wrap: wrap; gap: 4px; }
.cap { font-size: 10px; padding: 1px 6px; border-radius: 999px; border: 1px solid var(--line); color: var(--dim); }
.cap.on { color: var(--ok); border-color: #1e5b3a; }
/*
 * ⚠️ 未具备的能力标签的颜色**必须能在两种主题下都读清**。
 *
 * 实测（axe 式对比度扫描，沿祖先链合成真实背景）：
 * 原先深色 `#6b7280`（3.6:1）、浅色 `#8a93a3`（3.1:1）**都不达 WCAG AA 正文 4.5:1**。
 * 这两个值看着「低调」，实则是把「这家不支持签到」这条**有用信息**
 * 变得难以辨认 —— 而 `.cap.off` 恰恰是用户判断能力差异的唯一依据。
 *
 * ⚠️ 仍要**明显弱于** `.cap.on`（视觉层级不能丢）：故用 `var(--dim)`，
 * 它在两套主题里都是「次要文字」令牌，天然满足对比度且天然比强调色弱。
 * 保留 `line-through` 作为第二重「不支持」的视觉信号。
 */
.cap.off { color: var(--dim); text-decoration: line-through; }

/* ─────────── 动效降级（`prefers-reduced-motion`） ─────────── */
/*
 * ⚠️ 面板只有 4 处 transition（按钮/卡片 hover、主题切换），量很小；
 * 但对**前庭功能敏感**的用户，任何非必要动效都可能引起不适 ——
 * 系统里既然开了这个开关，就该尊重它。
 *
 * ⚠️ 这里**不是**把 `transition` 全删掉，而是压到 `0.01ms`：
 * 保留「状态变化会立即生效」的语义（有些代码依赖 `transitionend`），
 * 同时让肉眼看不到动画。
 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}

/* ─────────── 弹窗 ─────────── */
.modal {
  position: fixed; inset: 0; background: rgba(0,0,0,.6);
  display: flex; align-items: center; justify-content: center; padding: 20px; z-index: 50;
}
.modal[hidden] { display: none !important; }
/*
 * 弹窗（主弹窗与二级弹窗共用）。
 *
 * ⚠️ 圆角与阴影**与 `.panel` / `.kpi` 同一套**（16px + 柔和阴影）——
 * 用户要求「把二级弹窗的 UI 也统一一下」。旧值 12px 无阴影，
 * 与新版卡片并排明显不是一套。
 */
.modal-box {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
  width: min(760px, 100%); max-height: 86vh; display: flex; flex-direction: column;
  box-shadow: 0 18px 48px rgba(0,0,0,.28);
}
.modal-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 16px; border-bottom: 1px solid var(--line);
}
.modal-head h3 { margin: 0; font-size: 15px; }
.modal-tabs { display: flex; gap: 4px; padding: 8px 12px 0; border-bottom: 1px solid var(--line); }
.mtab {
  background: transparent; border: 1px solid transparent; border-bottom: none;
  color: var(--dim); padding: 6px 12px; border-radius: var(--radius-sm) var(--radius-sm) 0 0; cursor: pointer; font: inherit;
  /* ⚠️ 同一排的标签**字号必须一致**：状态切换时字号差 1px 会肉眼可见地抖。 */
}
.mtab.active { color: var(--text); background: var(--panel2); border-color: var(--line); }
.modal-body { padding: 14px 16px; overflow: auto; }
.mtab-panel[hidden] { display: none !important; }
/*
 * ⚠️ 弹窗里的卡片**必须用 `--panel2`**（比弹窗底亮/暗一档）。
 *
 * ⚠️ 我一度把它改成 `--panel`「统一底色」，那是**错的**：
 * `.modal-box` 本身就是 `--panel` ⇒ 卡片与弹窗底**完全相同**
 *（实测对比度 **1.00:1**）⇒ 卡片**看不见**，只剩一圈边框。
 *
 * ⚠️ 这正是「相邻嵌套面必须有视觉差异」那条纪律：
 * 弹窗（`--panel`）与其中的卡片（`--panel2`）必须差一档。
 */
.modal .card { background: var(--panel2); }
/*
 * ⚠️ 二级弹窗（`openInfo`）的内容是**表格 + 说明**，
 * 与主弹窗的卡片列表形态不同 ⇒ 统一给它一点内部节奏：
 * 说明文字与表格之间留白，表格自身可横向滚动（`.table-wrap` 已有）。
 */
.modal-body .hint + .table-wrap { margin-top: 10px; }
.modal-body .table-wrap + .hint { margin-top: 10px; }

/* ─────────── 任务卡片 ─────────── */
/* ⚠️ 不写下边距（容器有 gap） */
.task-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 12px; }
.task-card {
  border: 1px solid var(--line); border-radius: var(--radius); padding: 14px 16px;
  background: var(--panel2); display: flex; flex-direction: column; gap: 8px;
}
.task-card h3 { margin: 0; font-size: 14px; }
.task-card p { margin: 0; flex: 1; }
.task-card button { align-self: flex-start; }

/* ─────────── 模型开关 ─────────── */
.model-row { display: flex; align-items: center; gap: 8px; padding: 5px 8px; border-bottom: 1px solid var(--line); font-size: 12px; }
.model-row .mid { flex: 1; font-family: ui-monospace, monospace; }
.model-row .mname { color: var(--dim); width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.model-row.off .mid, .model-row.off .mname { color: #5b6472; text-decoration: line-through; }
.switch {
  width: 38px; height: 20px; border-radius: 999px; border: 1px solid var(--line);
  background: #10131a; position: relative; cursor: pointer; flex: none; padding: 0;
}
.switch::after {
  content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px;
  border-radius: 50%; background: #6b7280; transition: left .12s, background .12s;
}
.switch.on { background: #1e5b3a; border-color: #1e5b3a; }
.switch.on::after { left: 20px; background: var(--ok); }

/* ─────────── 剩余总积分徽标 ─────────── */
.credits-pill {
  font-size: 12px; font-weight: 500; color: var(--ok);
  background: #14301f; border: 1px solid #1e5b3a; border-radius: 999px;
  padding: 2px 10px; margin-left: 4px;
}

/* ─────────── 浅色模式 ───────────
 * ⚠️ 所有颜色都走 CSS 变量，故浅色模式只需覆盖 `:root` 的变量值 ——
 * 不必给每个组件写一份浅色样式（那会立刻产生两份需要同步维护的规则）。
 * 少数几个硬编码的深色值（如 #10131a 的输入框背景）单独覆盖。
 */
html[data-theme='light'] {
  --bg: #f6f7f9; --panel: #ffffff; --panel2: #f1f3f6; --line: #d8dde5;
  --text: #1a1d23; --dim: #5c6675; --accent: #2563eb; --accent-text: #2563eb;
  --ok: #15803d; --warn: #b45309; --err: #dc2626;
}
html[data-theme='light'] input,
html[data-theme='light'] select,
html[data-theme='light'] textarea,
html[data-theme='light'] button.ghost { background: #ffffff; }
html[data-theme='light'] .out { background: #f8f9fb; }
html[data-theme='light'] .bar-track { background: #e8ebf0; }
html[data-theme='light'] .switch { background: #e8ebf0; }
html[data-theme='light'] .tag.ok { border-color: #86c79a; background: #e7f5ec; }
html[data-theme='light'] .tag.warn { border-color: #e0b878; }
html[data-theme='light'] .tag.err { border-color: #f0a0a0; }
html[data-theme='light'] .credits-pill { background: #e7f5ec; border-color: #86c79a; }
html[data-theme='light'] .banner { background: #fdecec; border-color: #f0a0a0; color: #7f1d1d; }
html[data-theme='light'] .banner button { border-color: #d99; color: #7f1d1d; }
html[data-theme='light'] .notice { background: #eef4ff; border-color: #b9d0f5; }
/* ⚠️ 与上方 `.cap.off` 的浅色覆盖**成对**（见该规则说明）。
 * 浅色下 `--dim` 已是 #5c6675（5.4:1），无需再单独指定颜色；
 * 这里只删掉原先不达标的硬编码覆盖。 */
html[data-theme='light'] .model-row.off .mid,
html[data-theme='light'] .model-row.off .mname { color: #9aa3b2; }

/* ─────────── 自定义 API Key ─────────── */
/*
 * 密钥卡片网格。
 *
 * ⚠️ 用 `auto-fill + minmax` 而不是固定列数：卡片内含「配额条 + KV 行」
 * 这种不定宽度内容，固定列数会在窄屏把文字挤成竖排。
 * `minmax(0, 1fr)` 的 `0` 是必需的 —— 网格项默认不收缩（`auto` 最小尺寸 = 内容），
 * 一个长 IP 列表就能把整行撑破（本项目在 `.counts` 上踩过同一个坑）。
 */
.key-grid {
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
}
.key-card {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 14px 16px; display: flex; flex-direction: column; gap: 10px;
  min-width: 0;
}
/* 已停用：整卡去饱和 + 降透明度（一眼区分「这把不在跑」） */
.key-card.off { opacity: .62; }
.key-card.off .key-name { text-decoration: line-through; }

.key-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.key-name {
  font-size: 14px; font-weight: 600; min-width: 0;
  /* ⚠️ 长名称要能断行，且用 `overflow-wrap: anywhere` 而非 `break-all`
     —— 后者会把正常英文单词从中间截断（本项目单测禁止）。 */
  overflow-wrap: anywhere;
}
.key-prefix {
  font-family: ui-monospace, monospace; font-size: 12px; color: var(--dim);
  background: var(--panel2); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 3px 8px; align-self: flex-start;
  overflow-wrap: anywhere;
}

.key-usage { display: flex; flex-direction: column; gap: 11px; }
/*
 * ⚠️ 配额块内部用 `gap: 2px`（小）而**外面**用 11px（大）：
 * 「请求 / 0 / 7」「每 12 小时」「进度条」是**同一个指标的三行**，
 * 它们必须靠得比不同指标之间更近 —— 否则两块内容的行距一样，
 * 眼睛分不清「哪个数字属于哪个标签」（实测：截图里看着像一整块）。
 */
.key-quota { display: flex; flex-direction: column; gap: 2px; }
.key-quota-head {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: 11px; color: var(--dim); gap: 8px;
}
.key-quota-val { font-variant-numeric: tabular-nums; }
.key-quota-track {
  height: 5px; border-radius: 999px; background: var(--panel2);
  overflow: hidden; margin-top: 3px;
}
.key-quota-fill {
  height: 100%; border-radius: 999px; background: var(--accent);
  transition: width .18s;
}
/* 配额接近 / 达到上限：给一个前置信号，而不是等被拒才知道 */
.key-quota-fill.warn { background: var(--warn); }
.key-quota-fill.full { background: var(--err); }
.key-quota-note { font-size: 10px; color: var(--dim); }

.key-scope {
  display: flex; flex-direction: column; gap: 3px;
  border-top: 1px solid var(--line); padding-top: 8px;
}
.key-kv { display: flex; gap: 8px; font-size: 11px; align-items: baseline; }
.key-kv-k { color: var(--dim); flex: none; width: 62px; }
.key-kv-v {
  color: var(--text); min-width: 0; overflow-wrap: anywhere;
  font-variant-numeric: tabular-nums;
}

.key-actions {
  display: flex; flex-wrap: wrap; gap: 6px;
  border-top: 1px solid var(--line); padding-top: 10px;
}
.key-actions button { font-size: 12px; padding: 5px 10px; }

/* 一次性明文展示区（新建 / 重置后） */
.key-reveal {
  display: flex; gap: 8px; align-items: stretch; flex-wrap: wrap;
  margin: 8px 0 4px;
}
.key-reveal code {
  flex: 1 1 240px; min-width: 0;
  font-family: ui-monospace, monospace; font-size: 12px;
  background: var(--panel2); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 10px 12px; overflow-wrap: anywhere; user-select: all;
}
.key-reveal button { flex: none; }

/* 编辑表单（弹窗内） */
.key-editor { display: flex; flex-direction: column; gap: 10px; }
.key-field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--dim); }
.key-field input, .key-field textarea { font-family: inherit; }
.key-field textarea { font-family: ui-monospace, monospace; font-size: 12px; resize: vertical; }

html[data-theme='light'] .key-prefix { background: #f1f3f6; }
html[data-theme='light'] .key-reveal code { background: #f1f3f6; }

@media (max-width: 720px) {
  /* 手机：卡片撑满一行（两列会把 KV 行挤到几乎不可读） */
  .key-grid { grid-template-columns: 1fr; }
  .key-actions button { flex: 1 1 auto; }
  .key-kv-k { width: 56px; }
}

/* ─────────── 图标按钮 ─────────── */
.icon-btn {
  /* ⚠️ 缩小内边距（用户要求「右上角按钮缩小一点」），但**不缩触控区** ——
     见下方 `header .icon-btn` 的 min-width/min-height 说明。 */
  padding: 4px 6px; font-size: 15px; line-height: 1.2;
  display: inline-flex; align-items: center; justify-content: center;
}

/*
 * ⚠️ 顶栏图标按钮的**可点区域必须大于图标本身**。
 *
 * 实测（P1）：主题按钮曾因图标 SVG 渲染成 0×0 而缩到 **20×10px** ——
 * 20×10 在手机上基本点不中（远低于 44×44 的触控下限）。
 * 图标大小不该决定可点区域，故这里给按钮一个独立的最小尺寸，
 * 图标仍居中保持 16px。用 min-* 而不是固定 px，桌面端不受影响。
 */
/*
 * ⚠️ **可点区域的下限不能跟着视觉一起缩**（用户要求缩小外观）。
 *
 * 40px → **34px**：视觉上明显小了一圈，而手机上仍远高于「点不中」的临界
 *（图标本身 16px，34px 的触控区仍有 2 倍于图标的命中面积）。
 *
 * ⚠️ 不要再往 24px 缩 —— 那会重复踩一次 P1 缺陷（图标渲染成 0×0 时按钮
 * 缩到 20×10px，手机上基本点不中）。
 */
header .icon-btn { min-width: 34px; min-height: 34px; }

/* 供应商卡片：积分放右上角 */
.prov-card { position: relative; }
.prov-card .prov-credits {
  position: absolute; top: 8px; right: 10px;
  font-size: 11px; font-weight: 600; color: var(--ok);
  background: #14301f; border: 1px solid #1e5b3a; border-radius: 999px;
  padding: 1px 8px;
  /* ⚠️ 药丸补了「积分」二字后变宽（约 85px），而它是**绝对定位、不占位** ——
     名称一长就会钻到它下面。两个约束一起给：
       1. 名称预留出药丸的位置（`padding-right`）；
       2. 药丸自身**不换行**，避免「积分」两字被折到第二行。 */
  white-space: nowrap;
}
html[data-theme='light'] .prov-card .prov-credits { background: #e7f5ec; border-color: #86c79a; }

/* 模型页工具条（一键关闭） */
.model-toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }

/* ─────────── 自适应主题（auto） ───────────
 * ⚠️ 只在**没有**显式 data-theme 时生效 ——
 * 显式选择浅色/深色的用户不该被系统设置覆盖。
 * 用户若选 auto，JS 会移除 data-theme 属性，于是这里的媒体查询接管。
 */
@media (prefers-color-scheme: light) {
  html:not([data-theme]) {
    --bg: #f6f7f9; --panel: #ffffff; --panel2: #f1f3f6; --line: #d8dde5;
    --text: #1a1d23; --dim: #5c6675; --accent: #2563eb; --accent-text: #2563eb;
    --ok: #15803d; --warn: #b45309; --err: #dc2626;
  }
  html:not([data-theme]) input,
  html:not([data-theme]) select,
  html:not([data-theme]) textarea,
  html:not([data-theme]) button.ghost { background: #ffffff; }
  html:not([data-theme]) .out { background: #f8f9fb; }
  /* ⚠️ `.bar-track` 与 `.trend-track` 是**两个**元素，都要覆盖。
     实测缺陷（用户报「浅色主题下柱形图背景还是深色的」）：
     我只把 `.trend-track` 加进了 `[data-theme='light']` 那一套，
     而**跟随系统（auto）**模式走的是这段 `prefers-color-scheme` ——
     两套规则是**独立维护**的，漏一处就只在某一种模式下出错，极难发现。
     ⇒ 凡是在浅色下需要改底色的元素，**两处都要加**。 */
  html:not([data-theme]) .bar-track,
  html:not([data-theme]) .trend-track,
  html:not([data-theme]) .switch { background: #e8ebf0; }
  html:not([data-theme]) .credits-pill,
  html:not([data-theme]) .prov-credits { background: #e7f5ec; border-color: #86c79a; }
  /* ⚠️ 免费徽章：与 `html[data-theme='light'] .tag.ok` **成对** ——
     两套浅色规则独立维护，只写一处就只在某种模式下生效
     （本项目已为此踩过两次，见 AGENTS.md §9.1b ②g）。 */
  html:not([data-theme]) .tag.ok { border-color: #86c79a; background: #e7f5ec; }
  html:not([data-theme]) .banner { background: #fdecec; border-color: #f0a0a0; color: #7f1d1d; }
  html:not([data-theme]) .notice { background: #eef4ff; border-color: #b9d0f5; }
  /* ⚠️ 与 `html[data-theme='light'] .cap.off` 成对：跟随系统（auto）模式
   * 走的是这段 `prefers-color-scheme`，是**独立维护的一套**。
   * 浅色下 `--dim` 已是 #5c6675，无需硬编码。 */
  /* ⚠️ 与上面 `html[data-theme='light'] .rank-track` 成对 —— 漏一个就是「某种模式下出错」 */
  html:not([data-theme]) .rank-track { background: #e8ebf0; }
  html:not([data-theme]) .rank-idx { background: #eef1f5; }
  /* ⚠️ 与上面 `html[data-theme='light'] .kpi` 成对 —— 漏一个就是「只有某种模式出错」 */
  html:not([data-theme]) .kpi {
    box-shadow: 0 1px 2px rgba(0,0,0,.04), 0 10px 20px -12px rgba(0,0,0,.18);
  }
  html:not([data-theme]) .model-row.off .mid,
  html:not([data-theme]) .model-row.off .mname { color: #9aa3b2; }
}

/* ─────────── 登录页 ─────────── */
.login-body {
  display: flex; align-items: center; justify-content: center;
  min-height: 100vh; margin: 0;
  /* ⚠️ 必须留页面内边距：否则 `.login-card { width: min(400px, 100%) }`
   * 在 390px 宽的手机上会**顶到屏幕左右边缘**，圆角被切掉、看着像没做完。
   * 全局 `* { box-sizing: border-box }` 让 min-height 含 padding，不会溢出。 */
  padding: 16px;
}
.login-card {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 28px 26px; width: min(400px, 100%);
}
.login-card h1 { font-size: 17px; margin: 0 0 6px; }
.login-card form { display: flex; flex-direction: column; gap: 10px; margin: 16px 0 10px; }
.login-card input { padding: 10px 12px; font-size: 14px; }
.login-card button { padding: 10px 12px; font-size: 14px; }
.login-msg { font-size: 13px; min-height: 20px; margin: 0 0 8px; }
.login-msg.ok { color: var(--ok); }
.login-msg.err { color: var(--err); }
.login-foot { font-size: 11px; line-height: 1.6; }

/*
 * 扫码登录的二维码容器。
 *
 * ⚠️ 必须给白底：二维码在深色主题下若跟着面板底色，对比度不足会**扫不出来**
 *（微信的识别依赖深模块与浅背景的稳定反差）。故这里恒用白底 + 内边距，
 * 不随主题变化 —— 功能性优先于配色一致性。
 */
.qr-box {
  display: inline-block; margin: 10px 0; padding: 10px;
  background: #fff; border-radius: var(--radius-sm); line-height: 0;
}
.qr-box svg { width: 200px; height: 200px; display: block; }

/*
 * 设备码登录的**用户码**（cline：授权页要求用户手输这个码）。
 *
 * ⚠️ 必须做得**显眼且可整段选中**：用户要把它抄进浏览器里的另一个页面。
 * 字号偏大、等宽字体、加字距，避免把 0/O、1/I 看错。
 * 与 .qr-box 同理恒用白底深字 —— 深色主题下低对比会让用户抄错码，
 * 而抄错码的表现是「授权页一直报代码无效」，用户完全看不出是我们的锅。
 */
.user-code {
  display: inline-block; margin: 10px 0; padding: 10px 16px;
  background: #fff; color: #10131a; border-radius: var(--radius-sm);
  font-family: ui-monospace, monospace; font-size: 26px; font-weight: 700;
  letter-spacing: 4px; user-select: all;
}


/* ═══════════════════════════════════════════════════════════════════
   移动端适配（用户报障：手机上界面超出屏幕、文字换行错乱）

   ## 三个真实成因（逐条对应下面的规则）

   1. **表格是「不可收缩」的**：`table { width: 100% }` 只保证「尽量 100%」，
      但单元格内容（长 uid、长模型名、长原因）会把表撑宽 —— 因为默认
      `table-layout: auto` 会按内容计算最小宽度。手机上必然横向溢出。
   2. **固定宽度元素**：`.bar-label`（78px）、`.bar-val`（60px）、
      `.model-row .mname`（150px）在小屏上占比过大，把中间内容挤到换行。
   3. **长串不换行**：uid / token / URL 这类无空格长串不会自动断行，
      会直接顶破容器。

   ⚠️ 断行只用 `overflow-wrap: anywhere`（配合 `word-break: break-word`），
   **不用** `word-break: break-all` —— 后者会把正常英文单词也拦腰截断，
   中文界面里读起来很难受。
   ═══════════════════════════════════════════════════════════════════ */

/* 长串（uid / URL / token）必须能在任意位置断行，否则顶破容器 */
.uid, .out, td, th, .hint, .card strong, .model-row .mname {
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* 表格：允许横向滚动而不是撑破页面（手机上的标准做法） */
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table { table-layout: auto; }
/*
 * ⚠️ 表格必须按内容取宽，否则 .table-wrap 的横向滚动**永远不会触发**。
 *
 * 根因（线上实测，P1）：全局的 `table { width: 100% }` 让表格永远塞满容器、
 * 永不溢出，于是 `overflow-x: auto` 形同虚设 —— 表格不是「滚动」，而是
 * **被压缩**。代价在手机上很直观：五列明细表（冷却中 / 模型限流）的表头被
 * 挤成竖排单字（「类/型」「原/因」），供应商名 `codearts` 断成 `codea/rts`。
 * 实测对照：原样式下 表格宽 309px == 容器 309px（不滚动）；
 *          `width: max-content` 下 表格宽 397px > 容器 340px（真正可滚动）。
 * `min-width: 100%` 保证窄表（如「配置」两列）仍占满容器，不会缩成一小块。
 */
.table-wrap > table { width: max-content; min-width: 100%; }

/*
 * ⚠️ 必须允许网格项收缩，否则上面那条规则反而会把**整个页面**顶宽。
 *
 * `main` 是 `display: grid`，而网格项默认 `min-width: auto` —— 也就是
 * 「不小于内容的最小宽度」。表格一旦按 max-content 取宽，这个最小值就跟着
 * 变大，网格轨道被撑开、页面横向溢出。
 *
 * 实测（390px 视口，用量视图）：加 `min-width: 0` 前
 *   docScrollW 422 > 视口 375（溢出 47px），.table-wrap 自己被撑到 376px、
 *   横向滚动依旧不触发；加上之后 section 回到 351px、wrap 317px、
 *   表格 335px → **真正在容器内滚动**，页面溢出归零。
 *
 * 这与 `.count { min-width: 0 }` 是同一类陷阱（flex/grid 项默认不收缩）。
 * 用 `main > *` 覆盖所有网格项（含 WAF 横幅），将来新增区块也不会再踩。
 */
main > * { min-width: 0; }

@media (max-width: 720px) {
  /* ── 整体留白收紧，把宽度让给内容 ── */
  main { padding: 12px; gap: 12px; }

  /* ── 顶栏：标题与按钮在小屏上容易挤成两行 ── */
  /* 纵向 padding 收到 8px，给 40px 的图标按钮留出空间（否则顶栏会过高） */
  header { flex-wrap: wrap; gap: 8px; padding: 8px 12px; }

  /* ── 计数卡片：手机上 2 列，比「一列太长、三列太窄」都好读 ── */
  /*
   * 计数卡：手机上**两列**。
   *
   * ⚠️ 这里曾经写 `flex: 0 1 calc(50% - 4px)` —— 那是 `.counts` 还是
   * `flex` 容器时代的写法。现已改成 `grid`，**flex 属性在网格项上完全不生效**
   *（会成为需要一并清理的死代码）。改用 `grid-template-columns` 显式两列。
   *
   * ⚠️ 「两列」不是拍脑袋：项目此前**实测**过一列时 5 张卡纵排要占 344px，
   * 把首个供应商卡片推到 578px（在 667 高的机型上直接落到首屏之外），
   * 而卡片才是本页主要内容。两列能把这段压回约一半。
   */
  .counts { gap: 8px; grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .count {
    min-width: 0;          /* ⚠️ 网格项默认不收缩，不清掉会被长内容撑破 */
    padding: 7px 10px;
  }
  .count b { font-size: 16px; }

  /* ── 柱状图：标签与数值改窄，把空间留给进度条 ──
   * ⚠️ 58px 是量出来的：`10-03 20:00` 在 10px 字号下的自然宽度是 **55px**，
   * 原先的 52px / 44px 都会让它折成两行，柱状图每行高度从 16px 变成 31px。 */
  .bar-label { width: 58px; font-size: 10px; }
  .bar-val { width: 46px; font-size: 10px; }

  /* ── 模型开关行：名字可换行，不再固定 150px ── */
  .model-row .mname { width: auto; flex: 1 1 auto; min-width: 0; white-space: normal; }

  /* ── 弹窗：手机上几乎全屏，避免内部内容被压窄后疯狂换行 ── */
  .modal-box { width: calc(100vw - 24px); max-width: none; max-height: 88vh; }
  .modal-head, .modal-tabs { padding-left: 12px; padding-right: 12px; }
  .modal-body { padding: 12px; }

  /* ── 二维码：小屏上略缩，避免把弹窗撑到需要滚动 ── */
  .qr-box svg { width: 168px; height: 168px; }

  /* ── 表格字号再收一档，配合横向滚动 ── */
  table { font-size: 11px; }
  th, td { padding: 5px 6px; }

  /* ── 导航项：手机上横向滚动，不换行堆成两排 ── */
  nav { overflow-x: auto; flex-wrap: nowrap; -webkit-overflow-scrolling: touch; }
  .nav-item { white-space: nowrap; flex: 0 0 auto; }
}

/* ─────────── 手机端（≤720px）：用量页新组件的适配 ─────────── */
/*
 * ⚠️ 这些是**本轮新增**的组件（KPI 卡 / 排名行 / sparkline / 页头），
 * 它们此前没有任何窄屏规则 —— 而宽屏下的尺寸直接缩到 375px 会挤成一团。
 *
 * 原则（与既有移动端规则一致）：
 *   1. **能在容器内滚动的就滚动**，不要撑破页面（横向溢出最难用）；
 *   2. **不靠缩小字号硬塞** —— 字号低于 11px 在手机上已经不好读；
 *   3. 实在放不下的**次要信息**宁可省略（鼠标悬停仍可看 title），
 *      也不要把主信息挤没。
 */
@media (max-width: 720px) {
  /* KPI 卡：单列铺满，数字略收（25px → 22px）以免长数字折行 */
  .kpi-grid { grid-template-columns: 1fr; gap: 10px; }
  .kpi { padding: 12px 14px; }
  .kpi-value { font-size: 22px; }
  .spark { height: 24px; }

  /* 排名行：手机上把「指标」换到第二行 ——
     ⚠️ 不换行的话，`metrics` 是 flex:none + nowrap，
     会把名称栏挤到只剩 100px 左右，长模型名直接变成一列碎字。 */
  .rank-row { flex-wrap: wrap; row-gap: 6px; }
  .rank-main { flex: 1 1 100%; order: 1; }
  .rank-idx { order: 0; }
  .rank-metrics { order: 2; flex: 1 1 100%; padding-left: 32px; gap: 12px; }

  /* 页头：标题与工具行各占一行（工具行可横向滚动，避免按钮换行凌乱） */
  .page-head { align-items: flex-start; }
  .page-tools { width: 100%; overflow-x: auto; flex-wrap: nowrap; }
  .page-tools .seg { flex: 0 0 auto; }

  /* 面板内边距收紧，把宽度让给内容 */
  .panel { padding: 12px 13px; }  /* ⚠️ 圆角不再硬编码 —— 继承 --radius */
  .panel-head { margin-bottom: 10px; }
}

/* ── 二维码：小屏上略缩，避免把弹窗撑到需要滚动 ── */
@media (max-width: 420px) {
  .qr-box svg { width: 148px; height: 148px; }
  /* 更窄的屏：KPI 数字再收一档；排名指标的字号保持 11px（不硬缩） */
  .kpi-value { font-size: 20px; }
  .kpi { padding: 11px 12px; }
  .rank-metrics { padding-left: 0; }
}

/*
 * 极窄屏（≤360：老机型 / 分屏）再收一档。
 *
 * ⚠️ 「计数块收成一列」原先挂在 **420px** 上，注释写的是「极窄屏（老机型/分屏）」，
 * 但 420 实际覆盖了 375/390/393/402/412 —— 也就是**几乎所有在用手机**。
 * 线上实测每行几个：375→1、390→1、412→1，只有 430 及以上才是 2 列。
 * 后果是 5 个计数块纵排占掉 344px，首个供应商卡片被推到 **578px**
 *（844 高的屏上已过半；667 高的机型直接落到首屏之外），而卡片才是本页主要内容。
 * 故把单列规则降到 360 —— 常见手机走 720 断点的两列布局。
 */
@media (max-width: 360px) {
  /* 一列，避免数字被压到换行。
     ⚠️ 用 `grid-template-columns`（容器属性），不是 `flex` ——
     `.counts` 已是 grid，flex 属性在网格项上无效。 */
  .counts { grid-template-columns: 1fr; }
  .bar-label { width: 52px; }  /* 此时折成两行可以接受 */
}

/* ── 供应商管理（计数块样式复用 .count，这里只加拖动手柄与已关闭标记）── */

/*
 * 「启用 / 关闭」开关按钮。
 *
 * ## 为什么需要它（实测的观感缺陷）
 *
 * 按钮文案是**动作名**（当前启用 → 显示「关闭」），于是 12 家全部启用时，
 * 弹窗里就是一整列**红色实心「关闭」** —— 远看像一整屏错误告警，
 * 而实际上一切正常。真正需要被一眼看出的「哪家被关了」反而被淹没。
 *
 * 故把颜色语义反过来用在**状态**上：
 *   · 已启用（动作=关闭，可逆）→ 中性底色 + 红字，红色只落在文字上，不成片；
 *   · 已关闭（动作=启用）→ **强调色实心**，因为「这家现在没在跑」才是异常态，
 *     它需要最高对比度来被发现（同时「启用」本身是安全的恢复动作）。
 *
 * ⚠️ 仍然是 `ghost`/`danger` 按钮（沿用项目既有体系），这里只做覆盖。
 */
button.prov-sw {
  flex: none;
  background: transparent; border: 1px solid var(--line); color: var(--err);
  padding: 4px 12px; font-size: 12px;
}
button.prov-sw.is-off {
  background: var(--accent); border-color: var(--accent); color: #fff;
  font-weight: 600;
}
/* ⚠️ hover 的 `filter: brightness(1.12)` 对**透明底**的按钮几乎无效
 * （没有底色可提亮），故给已启用态一个明确的悬停回馈。 */
button.prov-sw:hover { background: var(--panel2); }
button.prov-sw.is-off:hover { background: var(--accent); filter: brightness(1.12); }
html[data-theme='light'] button.prov-sw { background: #ffffff; }

/*
 * 拖动排序手柄。
 * ⚠️ 只有手柄可拖（不是整行）：整行可拖会让「启用/关闭」按钮几乎点不中 ——
 * 按下即开始拖动，click 事件不会触发。
 * `cursor: grab` 明示可拖；手机上由浏览器映射成长按拖动。
 */
/*
 * 拖动手柄。
 *
 * ## 🔴 `touch-action: none` 是 **iOS 能拖起来的关键**
 *
 * 用户报「拖动排序在 iOS 上只会选择文字」。原因是 iOS Safari 对普通元素
 * **根本不支持 HTML5 拖放**（`draggable` 只对链接/图片等原生可拖元素有效），
 * 故已改用 Pointer Events（见 app.js 的说明）。
 *
 * 但换事件还不够：**必须显式关掉浏览器的手势接管**，否则 iOS 会把长按
 * 解释成文字选择 / 滚动，`pointermove` 根本不会持续派发。
 * `touch-action: none` 就是那个开关。
 *
 * ⚠️ 只加在**手柄**上（不是整行）—— 加在整行会让列表无法滚动。
 * ⚠️ `user-select: none` 是第二道防线（桌面端长按拖时也会选中文字）。
 */
.drag-handle {
  cursor: grab;
  color: var(--dim);
  font-size: 18px;
  line-height: 1;
  /* 加大触摸目标：移动端 4px 内边距太窄，手指按不准 */
  padding: 6px 10px;
  margin: -6px -6px -6px -2px;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
}
.drag-handle:active { cursor: grabbing; }

/* 拖动进行中：禁止整页选中文字（iOS 上尤其明显） */
.dragging-active { user-select: none; -webkit-user-select: none; }

/* 拖动中的行：降透明度，让「正在拖谁」一目了然 */
.card.dragging { opacity: 0.45; }
/* 悬停中的落点：用左侧高亮条标出「会插到这里」 */
.card.drop-target { border-color: var(--accent, #4a9eff); }

/*
 * 已关闭的供应商卡片：降透明度。
 * ⚠️ 这是**必需的**：否则用户会以为它坏了，而它只是按你的选择不参与。
 */
.prov-card.off { opacity: 0.55; }
.prov-off-tag { margin-top: 6px; font-size: 12px; color: var(--err); }

/* ⚠️ 手机断点：账号卡片元素变多（积分 + 停用 + 删除），
   让积分单独占一行，否则会把 uid/昵称挤成一条竖线。 */
@media (max-width: 720px) {
  .card .acct-credits { flex-basis: 100%; }
  /* ⚠️ 用量「按小时」在窄屏要收窄：三列（78+132+78=288px）+ 轨道
     会让轨道被挤到几乎没有宽度。
     ⇒ 隐藏 token 列（它在 title 提示里仍有），请求数列收窄。 */
  .bar-tokens { display: none; }
  .bar-head span:nth-child(4) { display: none; }
  .bar-val { width: 84px; }
  .bar-head span:nth-child(3) { width: 84px; }
  .bar-label { width: 54px; }
  /* KPI 卡片在窄屏单列铺满，数字略小一点以免折行 */
  .kpi-value { font-size: 22px; }
}
