/* ============================================================
   Akso Vault —— 官网与控制台  ·  浅色版
   设计语言：**工程图纸 / 精密仪表（浅底）**

   与深色版的关系：**结构完全不变，只换底层色与投影语汇**。
   · 深底靠"发光"表达层次 → 浅底改为**多层柔和投影**（发光在浅色上会糊成一片）
   · 网格线压到 3.5% —— 浅底上同样的透明度会显得脏
   · 界面主色用原色 `var(--brand)`（浅底上它本来就够对比，不需要提亮）
   ============================================================ */

@font-face {
  font-family: "Space Grotesk";
  src: url("/static/fonts/SpaceGrotesk.ttf") format("truetype");
  font-weight: 300 700;
  font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("/static/fonts/IBMPlexMono.ttf") format("truetype");
  font-weight: 400 600;
  font-display: swap;
}

:root {
  color-scheme: light;

  /* ★★ 2026-10-12（P0）：**把"面"拉成四阶**，并说明各自管什么。
     改前 `--bg`（彩度 3.6%）与 `--panel-2`（2.8%）亮度只差 **ΔL 0.027** ——
     同层级却混用了两个几乎不可分辨的面，于是"层次"看着是随机的。
     更实际的一处：`.utable tbody tr:hover` 用的是 `--panel-2`，
     它跟白卡片只差 ΔL 0.055 ⇒ **悬停几乎看不见**（那是最该被看见的反馈之一）。

     | 令牌 | 值 | 管什么 | 相对亮度 |
     |---|---|---|---|
     | `--bg-soft` | `#e2eef1` | 控件**轨道** / tabs 槽（沉在页面底之下） | 0.837 |
     | `--bg` | `#F4F7FD` | 页面底（一整片） | 0.929 |
     | `--panel-2` | `#f5fafb` | 卡片**内嵌**面 / 表格 hover / 禁用输入 | 0.948 |
     | `--panel` | `#ffffff` | 卡片（唯一的最亮面） | 1.000 |
     ★★ 顺序是**中间亮、轨道深、卡片最亮** —— 页面底是一整片，
        分段控件/tabs 的"槽"要沉在它下面一点，卡片浮在最上面。
        （第一版我把 `--bg-soft` 设成比 `--bg` 还深却在注释里写"逐级变亮"，
          `tests/test_contrast.py` 的单调性判据当场指出 —— 判据比注释可靠。）
     ★ `--bg` 是**用户 2026-10-12 指定**的 `#F4F7FD`（比原来的 `#eef6f8` 更亮、更偏蓝）。
       它仍然比 `--panel-2`（0.948）暗 ⇒ 单调性成立（实算 0.929 < 0.948，差值 0.019）。
       ★ 这个差**已经很小**：`--panel-2` 只比页面底亮一档，两者几乎同色。
         判据只要求"严格递减"，所以它是合法的；但如果以后要再调 `--bg`，
         **先算这一对**（`test_the_surface_scale_is_monotonic` 会拦，但它不会告诉你"看不出区别"）。
     ★ 判据按**数值**排序，不按这里写的名字顺序：`test_the_surface_scale_is_monotonic`。 */
  --bg:        #F4F7FD;
  --bg-soft:   #e2eef1;
  --panel:     #ffffff;
  --panel-2:   #FAFCFF;
  /* ★★★ 2026-10-13（用户决定「akso-cloud 的标准跟着 akso-pass 走」）：
     上面那行从 `#f5fafb` 改成 **`#FAFCFF`**，与 akso-pass 的 `--sb-surface-2` 逐字一致。
     ★ 面阶仍然单调（实算）：panel 1.0000 > panel-2 **0.9716** > bg 0.9285 > bg-soft 0.8367
       —— 而且它比原来（0.948）**离页面底更远**，`test_the_surface_scale_is_monotonic`
       那条"四档互不相同"的余地反而更大了。
     ★ 跨仓一致性由 `tools/verify_two_ends_tokens.py` 守着（它在 acceptance 里跑）。 */

  /* ★★ 2026-10-12（重设计 A）：描边从**中性灰**改成**同源的灰蓝**，并压淡一档。
     改前 `rgba(20, 40, 72, .17)` 是"中性深灰"—— 在浅蓝底上它偏脏，
     而且它与背景**不同源**，所以界面读起来像"几个系统拼的"。
     ⇒ 用品牌色相的低透明度（`0 91 172`）：
       · 同源 ⇒ 整页是一个色系（这是"现代感"里最容易被忽略的一条）；
       · 更淡（.17 → .13）⇒ 线不抢内容，层次交给阴影与底色。
     ★ 只改描边色，不改任何尺寸 ⇒ 不影响任何布局判据。 */
  --line:      rgba(30, 111, 255, .08);
  --line-2:    rgba(30, 111, 255, .14);
  /* ★★ 2026-10-12：这三条描边用**界面主色**的通道 `rgb(10, 115, 130)`
     （= `--brand #0a7382`），与主色**同源**。
     ★ **必须与 `--brand` 同步**：只改 `--brand` 而漏了这三条 rgba，
       页面会表现为"按钮是青的、边框和阴影还是另一个色"，
       而这在浅色低饱和底上**肉眼很难发现**。
       ⇒ 换主色时要动的地方：`--brand` / `--brand-ink` + **这三条 + `--sh-*` 的通道值**
         （`color-mix` 填不进 `rgba()` 的通道位，所以它们只能手写，
          本文件实测共 10 处 —— 判据：`tests/test_contrast.py` 从**本文件**解析色值，不抄第二份）。 */
  --line-hot:  rgba(30, 111, 255, .45);

  /* ★★ 2026-10-12（方案 A 的 P0）：**三档文字全部通过 WCAG AA 正文标准（4.5:1）**。
     改前 `--text-3` 是 `#8496b3` ⇒ **3.00:1**，而它占了全部文字引用的 **27%**
     （实测：表格表头、字段标签、表单提示都在用它）——
     界面上一小半的字比"能轻松读清"低一档，眼睛不会说"对比度不足"，它说"死气沉沉"。
     | 令牌 | 改前 | 对比度 | 改后 | 对比度 |
     |---|---|---|---|---|
     | `--text`   | `#12203a` | 16.24 | 不变 | 16.24 |
     | `--text-2` | `#55688a` | 5.62 | `#4a5c7c` | 6.90 |
     | `--text-3` | `#8496b3` | **3.00 ❌** | `#5d697e` | 4.62～5.54 ✅ |
     ★ 判据：`tools/verify-contrast.py` 逐个算**四个真实底色**上的对比度，
  断言不达标数为 **0**（只对着白卡片算会漏掉 `.foot` 与 `.field-hint` 那种用法）。 */
  --text:      #0f2430;
  --text-2:    #44606b;
  --text-3:    #546e78;

  /* ================================================================ 色板
     ★★ 2026-10-12（用户驳回旧规则）：**本配色是独立定义的一套，不与 logo 挂钩。**

     旧规则是「主色 = 公司 logo 的真实像素值 `#005BAC`」，用户明确驳回：
     界面配色**不该以 logo 为参照**（无论"跟随"还是"解耦"都是在拿 logo 当基准）。
     ⇒ 现在 `--brand` / `--accent` 是**按界面自身需要选的**，理由就是下面那三条：
       色相取向、可读性、层次。**换它们不需要问 logo 的意见，也不受 logo 约束。**

     ★★ 为什么把"参照物"这件事写下来：上一版我把注释写成"与 logo 解耦" ——
       措辞上仍然把 logo 当基准，等于**半条旧规则还活着**。
       一条被驳回的规则如果只改数字、不改"谁说了算"，下次有人会顺着那句话把颜色改回旧值。

     ## ★★ 一条必须说清的边界：**图标资产与界面配色是两件事**

     实测（逐像素取最常见色）：
     · `logo-mark-96.png` / `favicon-128.png` 的主色仍是 **`#005bac` + `#f6a701`**
       （那是图片文件里的像素，**保持原样**）。
     · 网站界面主色是 **`#1E6FFF`**（用户指定）⇒ **favicon 与界面主色不同**，这是**预期的**。

     ⇒ **不要把 favicon / logo 图片改成界面主色，也不要把界面主色改回 `#005bac`。**
       两者回答的是不同的问题：图片是"这枚标识长什么样"，
       界面配色是"这一屏读起来舒不舒服"。把它们对齐**没有**任何依据。
       ★ 判据：本文件里**任何把 `--brand` / `--accent` 与 logo 的像素值关联起来的陈述**
         都不该再有 —— 即不得用"logo 用了什么色"来决定界面主色。
         `logo-mark-*.png` 与 favicon 图片本身照旧使用，不受影响。
       ★ 扩展端 `tools/verify/verify-icons.mjs` 断言"图标源必须同时含 #005BAC 与 #F6A701" ——
         那验的是**图标资产**，与本站配色无关，**不需要**跟着改。

     ## 三条选择依据（与 logo 无关）

     ① **色相**：亮蓝 `#1E6FFF` —— **用户 2026-10-12 指定的色号**。
        它比旧的公司深蓝亮得多、也更饱和，正是用户要的"青春"取向。
        与暖橙 `#f08a24` 形成冷暖对比 —— 这个对比是**配色自身**的构成。
     ② **可读性**：全部实算过（`tests/test_contrast.py` 复核，不抄第二份）。
     ③ **层次**：面阶四档亮度严格递减，层次交给"面 + 阴影"，不靠描边加重。

     | 令牌 | 值 | 管什么 |
     |---|---|---|
     | `--brand` | `#1E6FFF` | 界面主色（交互态、选中态、主按钮） |
     | `--brand-ink` | `#1550cc` | 主色的深一档（hover/active），同色相 |
     | `--accent` | `#f08a24` | 强调色（与主色冷暖对比） |

     ★★ 逐档实算结果：
     · `--brand #1E6FFF`：白字在其上 **4.40**、作为文字对 `--panel` **4.40** / 对 `--bg` **4.02**
     · 正文三档（`--text/-2/-3`）在**四个**真实底上全部 ≥4.72（判据仍是 AA 4.5）
     · 语义色：amber 4.63 / red 4.71 / green 4.87（判据仍是 AA 4.5）
     · 面阶亮度严格递减：panel 1.000 > panel-2 0.948 > bg 0.929 > bg-soft 0.837

     ★★ **`#1E6FFF` 达不到 AA 4.5 —— 这是用户明确接受的取舍。**
       实测它做按钮底 4.40、做文字最低 4.02，都低于 4.5。用户有两个选择
       （① 压深到 `#195ed9` 达标；② 完整用 `#1E6FFF` 并把标准降到 4.0），
       **选了 ②**："完整用 #1E6FFF，把对比度标准从 AA 4.5 降到 4.0"。
       ⇒ 于是判据里多了 `ACCENT_AA = 4.0`，**只给 `--brand` 用**：
         · 正文三档与语义色**仍然判 4.5** —— 把全局阈值一起降到 4.0 会让它们
           **悄悄退化而判据仍然绿**（那等于让判据失效，本仓记过很多次）；
         · `--brand` 在这个界面里**从不做正文**（实测只有链接、选中态、局部数字，13–18px）。
       ★ 判据没有被"放宽到没意义"：把 `--brand` 换成浅到 3.x 的色 ⇒ 那条判据照样红。
       ★ 这一档**只影响 `--brand` 的文字与按钮底**；`--bg-soft` 上的品牌色文字
         （4.02 之下的 3.72）**不存在** —— `bg-soft` 只做 hover/轨道底，配的是 `--text`（13.5）。

     ★ **`--brand-ink` / `--brand-dip` 是 `--brand` 派生的**；而 `--line*` 与 `--sh-*`
     的 rgba 通道是**手写的**（`color-mix` 填不进 `rgba()` 的通道位）
     ⇒ 换主色时必须连同那 10 处通道值一起改，否则页面会变成"按钮一个色、边框另一个色"。 */
  --brand:     #1E6FFF;
  /* ★★★ 2026-10-13（用户决定「akso-cloud 的标准跟着 akso-pass 走」）：
     这一档换成 **`akso-pass` 的 `--sb-brand-hover` = `#185ED6`** ——
     它就是 akso-pass 用于 hover / active / **文字**的那一档。
     ⇒ 两端从此是**同一个蓝**：`--brand` 同一个 `#1E6FFF`，深浅两档也是同一对值。
     实测对四个真实底：panel 5.82 / bg 5.42 / bg-soft 4.91 / panel-2 5.52，全过 4.5 ——
     比原来的 `#1550cc`（5.81–6.88）浅一档，仍在安全区内。 */
  --brand-ink: #185ED6;
  --brand-dip: color-mix(in srgb, var(--brand) 7%, transparent);
  --brand-subtle: color-mix(in srgb, var(--brand) 9%, transparent);
  --accent:    #f08a24;
  --accent-dip: color-mix(in srgb, var(--accent) 14%, transparent);

  /* ★★ 2026-10-12（P0）：语义色要**在它们自己的混色底上**也过 4.5 —— 不只是对白卡片。
     ★ 那一轮收敛出来的深值（`#0a7b49` / `#af5109` / `#c62a2a`）在 2026-10-13
       改成了**语义清楚的两档**（见下面那段），值本身沿用。 */
  /* ★★★ 2026-10-13（用户决定）：**语义色向 `akso-pass` 对齐**。
     用户原话：「不要动 akso-pass 的色，akso-cloud 的标准跟着 akso-pass 走。」

     ## 为什么是"两档"而不是"照抄那三个值"

     直接把 `akso-pass` 的 `#22C55E / #F59E0B / #EF4444` 抄过来**做不到**，
     而这不是审美问题 —— 我实测过（把这三个值放在本文件的真实底色上算）：

     | akso-pass 的值 | 对 `--panel` | 对 `--bg` |
     |---|---|---|
     | 绿 `#22C55E` | **2.28** | **2.12** |
     | 橙 `#F59E0B` | **2.15** | **2.00** |
     | 红 `#EF4444` | **3.76** | **3.51** |

     ⇒ 白底上当文字几乎读不清。**而 akso-pass 那边没这个问题**，因为它
       **从不把这三个色当文字**：它们只做圆点（`.ac-avatar .dot`）、
       混色底（`.notice.ok`）与边框，而正文一律走 `color-mix(色 62%, --text)`。

     ## 所以照抄的是**它的做法**，不是它的值

     | 用途 | 取什么 | 为什么 |
     |---|---|---|
     | **填充 / 边框 / 圆点 / 发光** | **akso-pass 的亮度** | 这是用户看到的主色调，两端逐像素一致 |
     | **文字** | 同色相的 `-ink` 深档 | 亮值当文字读不清；深档两端的**角色**一致（akso-pass 也是"亮做填充、深做文字"） |

     ★ 这一版先把 `--brand-ink` 也换成 `akso-pass` 的 `--sb-brand-hover`
       （`#185ED6`，它正是 akso-pass 用于 hover/active/文字的那一档）：
       实测对四个真实底 4.91–5.82，全过 4.5 —— 比原来的 `#1550cc`（5.81–6.88）
       浅一档，但**两端从此是同一个蓝**。 */
  /*: 填充/边框档 —— **与 akso-pass 逐字一致**（那三个值只做填充） */
  --amber:     #F59E0B;
  --red:       #EF4444;
  --green:     #22C55E;
  /*: 文字档 —— 同色相的深一档。`-ink` 这个后缀与 `--brand-ink` 同族：
       「亮的是填充，深的是文字」。**任何 `color:` 都该用这一组。** */
  --amber-ink: #a3540a;
  --red-ink:   #c62a2a;
  --green-ink: #097545;
  --danger-subtle: rgba(207, 47, 47, .08);
  --danger-subtle: rgba(207, 47, 47, .08);

  /* ★ 2026-10-12（P0）：改前 `5 / 9 / 14` 互不成比例（+4 / +5），
     读起来像三个随手挑的数。改成 `6 / 8 / 12`（+2 / +2 / +4）。 */
  --r-s: 8px;
  --r-m: 12px;
  --r-l: 16px;
  /* ★ 2026-10-12（P2）：补上 --r-full。它**早就在用**（.empty-ico 的圆形），
     但一直靠 ar(--r-full, 999px) 的兜底在工作 —— 而"靠兜底工作"意味着
     这个令牌事实上不存在，谁改了兜底值都不会有人发现。
     ★ 是 kso-pass 仓的 erify-css-vars.mjs 把我这个用法检出来的
       （它逐页累计解析链，专门抓"引用了却解析不到"）—— 跨仓的同类检查值得抄一份。 */
  --r-full: 999px;

  /* ================================================================ 间距刻度
     2026-10-09：**这是"58 处裸 px"的根因修复** —— 之前一个间距变量都没有，
     所以每个 padding/gap 都是当场手写一个数字，实测攒到了 **162 处、33 种取值**
     （4/6/7/8/9/10/11/12/13/16/20…）。相邻两档只差 1px 的取值**没有人分辨得出来**，
     但它们让"这里该留多少空"变成一个每次都要重新决定的问题。

     ★ 刻度是 **1.5 倍率**（4 的倍数为主），14 档覆盖实测全部区间。
       刻意保留 `--sp-1: 4px` 与 `--sp-2: 8px` 这些小档：徽标内边距真的需要 4px。
     ★ 旧的裸值映射：4→1, 6→2, 8→2, 9→2, 10→3, 11→3, 12→3, 13→4, 16→4, 20→5,
       22→6, 24→6, 26→7, 28→7, 30→7, 32→8, 40→8, 42→8, 46→9, 48→9, 56→10,
       62→11, 64→11, 76→12, 80→12, 88→13。

     ★ **刻意不在刻度上的三类**（硬套刻度反而更差）：
       | 形态 | 例子 | 为什么保留裸值 |
       |---|---|---|
       | 图标与文字的微调 | `gap: 5px` | 它跟的是**图标视觉重心**，不是布局节奏；套 4px 会让图标看着黏住文字 |
       | 徽标内边距 | `padding: 1px 8px` | 徽标是"压到最小"的元件，1px 与 8px 的组合是刻意的 |
       | 描边式间距 | `padding: 1px …` | 1px 不是"留白"，是**描边的一部分** |
       ⇒ 所以判据看的是"**间距声明的绝大多数是否在刻度上**"，
         而不是"零裸 px" —— 后者会逼出"把 5px 改成 4px 只为达标"的假改动。 */
  --sp-0:  2px;
  --sp-1:  4px;
  --sp-2:  8px;
  --sp-3:  12px;
  --sp-4:  16px;
  --sp-5:  20px;
  --sp-6:  24px;
  --sp-7:  30px;
  --sp-8:  40px;
  --sp-9:  48px;
  --sp-10: 56px;
  --sp-11: 64px;
  --sp-12: 80px;
  --sp-13: 88px;

  /* 骨架尺寸：方向 B（240px 侧边栏 + 64px 顶栏）—— 只在这里声明一次
     ★ 2026-10-13：`--sidebar-w-collapsed: 64px` 已删 —— 折叠功能整体移除后它没有使用者
       （留着就是"定义了但没人用"的死令牌，本仓对这类残留有判据）。 */
  --sidebar-w:  240px;
  --topbar-h:   64px;
  --content-max: 1200px;
  /* 为绝对定位的 `.lock-tag` 预留的宽度。**是尺寸不是留白** —— 见它的使用处 */
  --lock-tag-w: 92px;

  /* ================================================================ 字号刻度
     ★★ 2026-10-12（方案 A）：**这是"排列不均"的根因修复** —— 改前实测
     `font-size` 有 **20 种**，其中 10–13.5px 之间挤了 **8 种、56 处声明**：
     `11 / 11.5 / 12 / 12.5 / 13 / 13.5`。
     相邻两档只差 **0.5px** —— 那读起来不是层级，是"没对齐"。

     ⇒ 收到 **7 档**，正文基准按方案 A 提到 **--fs-15**：

     | 令牌 | 值 | 用途 | 归并自 |
     |---|---|---|---|
     | `--fs-11` | 11px | 等宽微标签 / 页脚 / 表头 | 9.5 / 10 / 10.5 |
     | `--fs-12` | 12px | 徽标、字段提示、`.mono` | 11 / 11.5 |
     | `--fs-13` | 13px | 次要信息、表体行内数据 | 12 / 12.5 |
     | `--fs-15` | 15px | **正文基准**：按钮 / 输入 / 导航 / 卡片标题 | 13 / 13.5 / 14 / 14.5 |
     | `--fs-18` | 16px | 区块标题、登录页标题 | 15.5 / 16 / 17 |
     | `--fs-32` | 32px | 页面标题、统计大数 | 20 / 22 / 26 / 27 |
     | `--fs-54` | 54px | 登录页主标题 | 40（落地页那个 54 保留） |

     ★ 判据：`tools/verify-type-scale.py` 断言"刻度外的字号声明数 = **0**"。 */
  --fs-11: 11px;
  --fs-12: 12px;
  --fs-13: 13px;
  --fs-15: 15px;
  --fs-18: 18px;
  --fs-32: 32px;
  --fs-54: 54px;

  --font-display: "Space Grotesk", "HarmonyOS Sans SC", "MiSans",
                  "Microsoft YaHei UI", "Microsoft YaHei", sans-serif;
  --font-body:    "HarmonyOS Sans SC", "MiSans", "PingFang SC",
                  "Microsoft YaHei UI", "Microsoft YaHei", "Segoe UI", sans-serif;
  --font-mono:    "IBM Plex Mono", "Cascadia Code", Consolas, monospace;

  --ease: cubic-bezier(.22, 1, .36, 1);

  /* 浅底的三层投影：贴地的一层管边界，中间一层管体量，最外一层管环境 */
  /* ★ 2026-10-12（重设计 A）：阴影改成**更细更长**，并把色相也换成同源蓝。
     浅底上"细而长"的影读起来是"浮起来"，"深而短"读起来是"贴了一张纸"。
     透明度反而降了（.05 → .04）—— 因为长距离扩散本身就有体量感。 */
  --sh-1: 0 1px 2px rgba(30, 111, 255, .04), 0 1px 3px rgba(30, 111, 255, .03);
  --sh-2: 0 2px 4px rgba(30, 111, 255, .04), 0 10px 26px rgba(30, 111, 255, .06);
  --sh-3: 0 4px 8px rgba(30, 111, 255, .05), 0 18px 46px rgba(30, 111, 255, .10),
          0 40px 90px rgba(30, 111, 255, .06);
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100vh;
  /* 三段式：顶栏 / 内容 / 页脚。`main` 吃掉剩余高度 ⇒ 页脚永远贴底，
     而不是"内容短的时候悬在半空"——后者在浅色大留白上尤其明显。 */
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.7 var(--font-body);
  /* ★★ 2026-10-12（重设计 A）：**去掉 44px 网格线与角落光晕**。
     那两样是"仪表盘模板"的标配，而它们让**每一页**的背景都在提醒使用者
     "这是个后台系统" —— 这是"土"最直接的一处。
     ⇒ 换成一个**极淡的双色纵向渐变**：页面顶部略亮、底部略沉，
       有空气感但不抢内容（对比度差 < 0.02，肉眼只感觉到"不是死平的"）。 */
  background-image: linear-gradient(180deg, #f4f8fd 0%, var(--bg) 42%, #eaf1fa 100%);
  background-attachment: fixed;
}

main { flex: 1; display: flex; flex-direction: column; }

/* ============================================================ 应用外壳（方向 B）
   2026-10-09：240px 侧边栏 + 64px 顶栏 + 内容区 max 1200px。

   ## ★ 为什么用 `body:has(> aside.side)` 而不是给每页加一个 class

   "这一页是不是后台"是**由它的结构决定的事实**（有侧边栏 ⇒ 它是后台），
   而不是一个需要人记着同步的标记。用 class 的话，新增一个后台页面时忘了加，
   它就会**静默地**按落地页布局渲染 —— 而"页面能用但排版不对"最难被发现。

   `:has()` 需要 Chrome 105+（比已在用的 `color-mix` 的 111+ 要求更低）
   ⇒ **不引入新的兼容性约束**。

   ## 骨架怎么排

       body（grid）
       ├─ aside.side   240px  │ 品牌区（上）· 主导航（中）· 身份（下）
       ├─ header.top   1fr    │ 面包屑 + 标题 + 右侧操作   ← 64px，sticky
       ├─ main.wrap    1fr    │ 内容区 max 1200px
       └─ footer.foot  1fr    │ 页脚

   ★ 侧边栏是 **grid 子项**而不是 `position: fixed`：fixed 会把它从文档流摘出去，
     于是"内容区从哪开始"要另外用 `padding-left` 补 —— 两处数字必须一致，
     不一致的表现是内容被侧边栏压住一点点。grid 子项**没有这个问题**。
   ★ 顶栏的 `sticky` 只在它自己那一列内生效 ⇒ 侧边栏可独立滚动，
     而顶栏始终在内容上方（标准 SaaS 后台的行为）。

   ★ **顺序很重要**：这一段必须在上面那条基础 `body` 规则**之后**。
     写在之前的话，同为 `body` 选择器的基础规则在后面胜出，`display: grid`
     会被 `display: flex` 覆盖 —— 而症状是"侧边栏跑到内容上面去了"，
     看起来像布局写错，实际是**规则顺序**问题。 */
body:has(> aside.side) {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "side top"
    "side main"
    "side foot";
  align-content: start;
}
/* grid 下 `main` 不该再吃 flex 的 `flex:1`（那是给 flex 列布局用的） */
body:has(> aside.side) > main { flex: none; min-width: 0; }

/* ---------- 侧边栏 ---------- */
.side {
  grid-area: side;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  /* 白底 + 右侧 1px 边框（方向 B 明确不用深色侧边栏） */
  background: var(--panel);
  border-right: 1px solid var(--line);
  padding: var(--sp-4) var(--sp-3);
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;                      /* ★ 卡片不滚，交给 body */
}
.side-brand {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3) var(--sp-5);
}
.side-brand .brand-name { line-height: 1.15; }
.side-brand small {
  display: block; font-size: var(--fs-11); letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-3); font-weight: 500;
}

/* 导航项：默认 文字 --text-2 / 透明底；hover 底 --bg-soft；
   选中 底 --brand-subtle + 文字 --brand + 左 2px --brand 指示条 */
.side-nav { display: flex; flex-direction: column; gap: var(--sp-0); }
.side-nav a {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3);
  border-radius: var(--r-m);
  color: var(--text-2);
  font-size: var(--fs-15);
  font-weight: 500;
  text-decoration: none;
  /* 左侧 2px 指示条：默认透明占位，避免选中时文字横向跳动 */
  border-left: 2px solid transparent;
  transition: background .16s var(--ease), color .16s var(--ease);
}
.side-nav a:hover { background: var(--bg-soft); color: var(--text); text-decoration: none; }
.side-nav a[aria-current="page"] {
  /* ★ 2026-10-12（重设计 A）：选中态的浅底再收一档（9% → 7%）。
     理由是它**挨着**左侧那条 2px 品牌色条：两条视觉线索都在说"选中的是这个"，
     底色再重就会显得吵。 */
  background: var(--brand-dip);
  color: var(--brand);
  border-left-color: var(--brand);
  font-weight: 600;
}
.side-nav a svg { width: 20px; height: 20px; flex: none; }
/* ==================================================== 一级 + 二级导航（控制台）
 * ★★ 2026-10-12（用户要求）：「将 akso-cloud 的控制台，也更新为一二级导航栏的形式，
 *   将几个模块分开」。
 *
 * 结构：`.side-group`（一级，可折叠）> `.side-group-head`（按钮）+ `.side-sub`（二级项）
 *   · 一级项是 **button 而不是 a** —— 它在 /app 页上没有可去的地方，
 *     而"指向当前页的链接"会让 `aria-current` 的语义自相矛盾。
 *   · 二级项是 `a[href="#sec-x"]`，点它 = 滚到那一块（由 `site.js` 的 `initConsoleNav`
 *     接管并 preventDefault，避免 URL 里堆 hash、也避免浏览器"跳一下"的突变）。
 *
 * ★ 视觉上与普通导航项**同构**（同样的 padding / 圆角 / 左侧 2px 指示条），
 *   只把二级项做小一号、并加一条竖向引导线 —— 层级靠"缩进 + 字号 + 引导线"表达，
 *   而不是换个颜色（那会与 `--brand` 的选中态抢注意力）。
 *
 * ★★ 判据（`tests/test_pages.py`）：一级项与二级项**双向**都要在 ——
 *   "二级项的 `data-sec` 必须在 HTML 里有对应 `id`"（否则点了不动，而它**不报错**），
 *   反向也要（`sec-*` 区块必须有二级项指向它，否则那个模块在导航里是隐形的）。 */
.side-group { display: flex; flex-direction: column; }
.side-group-head {
  display: flex; align-items: center; gap: var(--sp-3);
  width: 100%;
  padding: var(--sp-3);
  border: 0;
  border-left: 2px solid transparent;
  border-radius: var(--r-m);
  background: none;
  color: var(--text-2);
  font: inherit;
  font-size: var(--fs-15);
  font-weight: 500;
  text-align: left;
  cursor: pointer;
  transition: background .16s var(--ease), color .16s var(--ease);
}
.side-group-head:hover { background: var(--bg-soft); color: var(--text); }
.side-group-head > svg:first-child { width: 20px; height: 20px; flex: none; }
/* 折叠箭头：靠 `aria-expanded` 旋转，不另设类名（少一个状态就少一处不一致） */
.side-caret {
  width: 16px; height: 16px; flex: none;
  margin-left: auto;
  opacity: .6;
  transition: transform .18s var(--ease);
}
.side-group-head[aria-expanded="false"] .side-caret { transform: rotate(-90deg); }
/* ★ 一级项在"它下面有二级项被选中"时也给品牌色 —— 否则用户看不出自己在哪一段里 */
.side-group:has(.side-sub a[aria-current="true"]) .side-group-head { color: var(--brand); }
.side-group-head[aria-expanded="false"] + .side-sub { display: none; }

.side-sub {
  display: flex; flex-direction: column; gap: 2px;
  /* 左侧引导线：与一级项图标的中线对齐（20px 图标 + 12px padding ⇒ 缩进 32px） */
  margin: 2px 0 2px 32px;
  padding-left: var(--sp-3);
  border-left: 1px solid var(--line-2);
}
.side-sub a {
  display: block;
  padding: 6px var(--sp-3);
  border-radius: var(--r-s);
  color: var(--text-3);
  font-size: var(--fs-13);
  text-decoration: none;
  transition: background .16s var(--ease), color .16s var(--ease);
}
.side-sub a:hover { background: var(--bg-soft); color: var(--text); text-decoration: none; }
.side-sub a[aria-current="true"] {
  background: var(--brand-dip);
  color: var(--brand);
  font-weight: 600;
}
/* ★ 2026-10-13：这里原先是 `.side-collapsed .side-sub / .side-caret { display:none }`
   （折叠时藏起二级项）—— 随折叠功能一起删除。
   ★ `.side-caret` 本身**保留**：它现在表示"一级项可折叠"（`nav-console`），
     与侧边栏折叠是两件事 —— 别把两者混为一谈。 */

/* ★★ 2026-10-13（用户要求：三个模块要**单独的页面**）：面板切换。
 *
 * 点二级项时，除了当前那一块，其余 `.pane` 都加 `.hidden`（= `display:none`）。
 *
 * ★★ 为什么是 `display:none` 而**不是**把节点移出 DOM：`site.js` 的渲染函数
 *   （`renderAccounts` / `fillSiteOptions` / `fillBoxOptions` / `renderChips` / `renderSites`）
 *   直接写 `el.className` / `el.innerHTML`，**没有一个有空值守卫** ⇒
 *   节点一没就抛 TypeError，而那个错在 `refresh()` 的链上 ⇒ **整个刷新中断**。
 *   ⇒ 隐藏而非移除：`refresh()` 始终写完**全部**面板，切回来就是最新的
 *     （也就不存在"隐藏时渲染、显示时已过期"那类缺陷）。
 * ★ 复用已有的 `.hidden`（本仓所有页面都在用它），不新造类名 —— 少一个类名就少一处漂移。 */

/* 页面切换后，非当前面板不占位；`.pane` 本身不需要额外样式，
   这里只留一条"注释性判据"给将来的人：面板的可见性**只由 `.hidden` 决定**，
   不要再加 `is-active` / `display:block` 之类的第二条通路（本仓反复出现两套机制不一致）。 */

/* ★ 2026-10-09：删掉了 `.side-nav a.is-danger:hover`（死 CSS —— 没有任何导航项带 `is-danger`）。
   如果将来真的需要一个"危险导航项"（如「退出登录」放侧边栏），
   把这条规则连同那处 `class="is-danger"` 一起加回来即可 —— 别只加规则或只加类名。 */

.side-foot { margin-top: auto; padding-top: var(--sp-4); border-top: 1px solid var(--line); }

/* ============================================================ 右上角账号菜单
 *
 * ★★ 2026-10-11（用户要求）：身份块从**侧边栏底部**搬到这里，并且把
 *   "个人信息"与"退出"两个入口**收进一个点一下才弹出的菜单**。
 *
 *   三条理由（第一条是用户实测发现的）：
 *     ① 管理员登录页上，左下的身份块显示"你其实已经登录了"——而那页要的是
 *        **管理员**账号，普通登录态在那里出现只会让人以为"我已经进来了"；
 *     ② 右上角是"账号"的自然位置（顶栏右侧本来就是身份区）；
 *     ③ "退出"原来的位置太显眼：它紧挨着"导出数据""为应用授权"，误点的代价是
 *        **当场掉线**，而重新登录要收一次验证码。
 *
 * ★ 为什么用**点击**而不是 `:hover` 弹出：本仓没有任何 `@media (hover)` 判断，
 *   而触屏上"悬停"根本不存在 —— 悬停菜单在手机上表现为"点了没反应"。
 *   ⇒ 点击开合 + `Esc` 关 + 点外面关（见 `initMeMenu`），三种设备行为一致。
 */
.top-me { position: relative; display: flex; align-items: center; }

.top-me-chip {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  max-width: 220px; padding: var(--sp-1) var(--sp-2) var(--sp-1) var(--sp-1);
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--panel); color: var(--text);
  font-family: inherit; font-size: var(--fs-15); font-weight: 500; cursor: pointer;
  transition: border-color .14s var(--ease), background .14s var(--ease);
}
.top-me-chip:hover, .top-me-chip[aria-expanded="true"] {
  border-color: var(--brand); background: var(--brand-dip);
}
.top-me-chip:focus-visible { outline: none; box-shadow: 0 0 0 4px var(--brand-dip); }
#me-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 折叠按钮那套箭头：展开时转 180°。★ `aria-expanded` 与视觉**同源**（同一个属性），
   所以"看得见的状态"和"朗读出来的状态"不可能不一致。 */
.top-me-caret { transition: transform .16s var(--ease); }
.top-me-chip[aria-expanded="true"] .top-me-caret { transform: rotate(180deg); }

.me-menu {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 30;
  min-width: 168px; padding: var(--sp-1);
  display: flex; flex-direction: column; gap: 2px;
  border: 1px solid var(--line-2); border-radius: var(--r-m);
  background: var(--panel);
  /* ★★ 2026-10-11 实测改：原先用 `--sh-2`（卡片级的柔和投影），而截图里
     菜单**压住页面说明文字**时几乎看不出"浮起来" —— 表现是"那句话被切掉了"，
     而不是"上面有一层菜单"。⇒ 换成 `--sh-3`（模态级的抬升）。
     判据不是"我觉得好看"，而是"它下面压着别的文字时，边界能不能一眼看出来"。 */
  box-shadow: var(--sh-3);
}
/* ★ `.me-menu.hidden` 必须显式写：`.me-menu { display: flex }` 与工具类
   `.hidden { display: none !important }` 目前是 !important 赢，但**依赖
   !important 的顺序**是脆的 —— 写死这一条，将来谁调了工具类都还成立。 */
.me-menu.hidden { display: none; }

.me-item {
  display: block; width: 100%; text-align: left;
  padding: var(--sp-2) var(--sp-3); border: none; border-radius: var(--r-s);
  background: none; color: var(--text); text-decoration: none;
  font-family: inherit; font-size: var(--fs-15); cursor: pointer;
}
.me-item:hover { background: var(--bg-soft); text-decoration: none; }
/* 退出做成危险动作（红字），因为它的代价是**当场掉线**。 */
.me-item-danger { color: var(--red-ink); }
.me-item-danger:hover { background: color-mix(in srgb, var(--red) 10%, transparent); }

/* 触屏设备：没有悬停，可点区域要更大（44px 是通行下限）。 */
@media (hover: none) {
  .me-item { padding: var(--sp-3); }
  .top-me-chip { padding: var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-2); }
}

/* ============================================================ 顶栏
 *
 * ★ 2026-10-09：**从「两套并行实现」改成「一条公共规则 + 两处差异覆盖」。**
 *
 *   决策 4 点名要"清掉两套顶栏"。实测（在死类 / 孤儿 id 都零命中的情况下）
 *   公开页 `.nav` 与后台页 `.top` 里**有 10 条声明逐字相同**：
 *   display / align-items / justify-content / gap / flex-wrap /
 *   border-bottom / backdrop-filter / position / top / z-index。
 *
 *   ⇒ 那不是"两套设计"，是**同一套东西抄了两遍**。后果正是本仓 #27 的形态：
 *     **改了一处、另一处没改，页面照样正常、不报错。**
 *     （同向先例：`.modal` 曾被定义两次、后者静默覆盖前者，而当时的注释
 *      还写着"两套类名不重叠，不冲突" —— **注释里的错误断言比没有注释更坏**。）
 *
 *   ★ 公共部分只在这里声明一次；**有意不同**的部分留在各自规则里并写明原因。
 *   ★ 判据：`tests/test_pages.py::test_there_is_only_one_topbar_implementation`
 *     （逐字重复声明数必须为 0，且这条公共规则必须真的承担那 10 条）。
 */
.nav, .top {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4);
  border-bottom: 1px solid var(--line);
  backdrop-filter: blur(14px) saturate(1.4);
  position: sticky; top: 0; z-index: 20;
  /* 两条顶栏在窄屏下都必须能换行 —— 后台页那边更挤（胶囊统计条 + 邮箱 +
     两个按钮），不换行的表现是**邮箱被挤出屏幕**，而不是报错。 */
  flex-wrap: wrap;
}

/* 后台页（方向 B）：`.top` 是骨架 grid 的具名区，所以必须声明 `grid-area` */
.top {
  grid-area: top;
  min-height: var(--topbar-h);
  padding: var(--sp-2) var(--sp-8);
  background: rgba(255, 255, 255, .86);
}
.top-title { display: flex; align-items: baseline; gap: var(--sp-3); min-width: 0; }
.top-title .crumb {
  font-family: var(--font-mono); font-size: var(--fs-12); letter-spacing: .12em;
  text-transform: uppercase; color: var(--text-3);
}
.top-title h1 {
  margin: 0; font-family: var(--font-display);
  font-size: var(--fs-32); font-weight: 600; letter-spacing: .01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.top-right { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }

/* ============================================================ 管理端（独立骨架）
 *
 * ★★ 2026-10-12（用户要求 ⑦⑧）：管理页**不再共用后台骨架**。
 *
 *   · `body` 上没有 `aside.side` ⇒ 走基础那条 `display:flex; flex-direction:column`
 *     （顶栏 / 内容 / 页脚三段式）⇒ **顶栏天然整宽、内容在它下面**，
 *     不需要再给这一页写 grid（这正是"少写规则"的那种情况）。
 *   · 顶栏左侧的品牌块：管理端没有侧边栏，所以品牌要**自己带**（logo + 字 + 端名）。
 *   · 内容区：管理面板比控制台宽（表格 + 统计卡），所以用 `--content-max-宽` 那一档——
 *     仍然限宽居中，但比 `.wrap` 的 1120px 宽一点。
 */
.admin-top {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4); flex-wrap: wrap;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
}
.admin-top .top-title { gap: var(--sp-3); }
.admin-brand { display: flex; flex-direction: column; line-height: 1.25; }
.admin-brand strong { font-family: var(--font-display); font-size: 15px; font-weight: 700; }
.admin-brand small {
  font-family: var(--font-mono); font-size: var(--fs-11); letter-spacing: .14em;
  text-transform: uppercase; color: var(--text-3);
}
.admin-wrap { max-width: 1240px; padding-top: var(--sp-6); }

/* 门槛（登录卡）：**这一页的门槛是"首页 + 单独一张卡"的形状**，
   所以给它自己一点上下留白，让它看起来像一页、而不是"内容区里的一个块"。 */
.admin-wrap .gate { margin-top: var(--sp-6); }

@media (max-width: 900px) {
  /* ★★ 2026-10-12（P3）：**移动端顶栏瘦身**。
     实测改前顶栏高 **188px**（占 844px 屏高的 22%）—— 因为 `.top-right` 里
     四件东西（账号胶囊 / 版本徽标 / 三个统计 / 导出按钮）在 390px 宽下各自换行堆叠，
     它自己就占了 101px。
     ⇒ 按**重要性**取舍，而不是把四件都挤小：
       · 版本徽标（`v0.8.0`）与「导出数据」**只在桌面显示** —— 它们不是高频操作，
         而窄屏上"能看清主体"比"工具齐全"重要；
       · 统计改成**单行横向滚动**（`overflow-x: auto` + `flex-wrap: nowrap`），
         而不是换行堆成三行。 */
  /* ★ 这两个是 **id** 不是 class（`app.html` 里写的是 `id="ver-chip"` / `id="btn-export"`）。
     第一版我按类名写（`.ver-chip` / `.btn-export`）—— 那样**一条都不会生效**，
     而且完全静默：CSS 里"选择器匹配不到"不报错，页面只是"没变化"。
     判据 `tests/test_mobile_layout.py` 会核对"媒体查询里引用的 id 在 HTML 里真的存在"。 */
  #ver-chip, #btn-export { display: none; }
  .stats { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
  .stats::-webkit-scrollbar { display: none; }
  .top-right { gap: var(--sp-2); }
  /* 移动端：管理端顶栏会换行，把标题与操作分两行排（原来 `.top-title` 是横向 flex）。 */
  .admin-top { padding: var(--sp-3) var(--sp-5); }
  .admin-top .top-title { flex-wrap: wrap; }
  .admin-wrap { padding: var(--sp-5) var(--sp-5) var(--sp-8); }
}

/* 骨架下的内容区：不再自己居中（顶栏已给出左边界），但仍限宽 */
body:has(> aside.side) > .wrap {
  grid-area: main;
  max-width: var(--content-max);
  margin: 0;
  padding: var(--sp-8) var(--sp-8) var(--sp-10);
}
body:has(> aside.side) > .foot { grid-area: foot; }

/* ★★★ 2026-10-13（用户要求：侧面导航栏无需折叠按钮）：**折叠态那一整套 CSS 已删除**。
   删掉的是：`body.side-collapsed` 的栅格切换与文字隐藏、`.side-collapse` 按钮样式、
   以及它在移动端媒体查询里的隐藏规则。
   ★ 为什么连 CSS 一起删（而不是留着"以防将来要折叠"）：留着会形成本仓最忌讳的那种
     残留 —— **规则在、入口没了**，下一个读的人会以为侧边栏还能折；
     而真正危险的是 `app.html` 里那段"早期恢复折叠态"的脚本若也留着，
     用户历史 localStorage 还是 `1` 时侧边栏会莫名其妙收起、且无按钮可展开。
   ★ `--sidebar-w-collapsed` 这个变量若已无人引用，一并删（见变量区）。 */

/* `.side-foot`：**原先放「身份块 + 折叠按钮」，2026-10-11 起只放折叠按钮，
   2026-10-13 起按钮也删了** —— 身份块搬到了顶栏右上角（见下面的 `.top-me`），
   因为它在左下的位置会在管理员登录页那种"你还没通过这道门"的页面上，
   显示"你其实已经登录了"的信息。现在这个容器是空的占位（保留它是因为
   `body:has(> aside.side)` 的栅格把 `.side-foot` 当作 footer 区参与布局）。 */
.side-foot { display: flex; align-items: center; justify-content: flex-end; gap: var(--sp-2); }

/* 移动端：侧边栏变抽屉。
   ★ 用 checkbox 的 `:checked` 而不是 JS：不需要任何脚本（本服务 CSP 是
     `script-src 'self'`，而"少一个脚本"永远是好的），且**默认关闭** ——
     窄屏进来看到的是内容，不是盖住屏幕的抽屉。 */
.side-toggle { display: none; }
.side-scrim { display: none; }

@media (max-width: 900px) {
  body:has(> aside.side) {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "top" "main" "foot";
  }
  .side {
    position: fixed; inset: 0 auto 0 0; z-index: 40;
    transform: translateX(-100%);
    transition: transform .22s var(--ease);
    box-shadow: var(--sh-3);
  }
  #side-toggle:checked ~ .side { transform: none; }
  #side-toggle:checked ~ .side-scrim {
    display: block; position: fixed; inset: 0; z-index: 30;
    background: rgba(18, 32, 58, .38);
  }
  .side-toggle {
    display: inline-flex; align-items: center; justify-content: center;
    width: 34px; height: 34px; flex: none;
    border: 1px solid var(--line-2); border-radius: var(--r-s);
    background: var(--panel); color: var(--text-2); cursor: pointer;
  }
  .top { padding-inline: var(--sp-4); }
  body:has(> aside.side) > .wrap { padding: var(--sp-5) var(--sp-4) var(--sp-9); }
}

/* 落地页吃掉剩余高度 ⇒ 两栏在可用空间里垂直居中，而不是顶着顶栏、下面空一大片。
   （控制台页不受影响：它没有 .hero，子元素在 flex 列里的排布与块流一致。） */
.hero { flex: 1; }

a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 3px; }

.hidden { display: none !important; }
/* ★ 2026-10-09：这行删掉了 `.muted { color: var(--text-2) }`。
   它是**死 CSS**：五个页面的 HTML 里没有任何元素用 `class="muted"`，
   `site.js` 的字符串字面量里也没有。
   ★ 判据见 tmp 探针 `av-deadcss-final.py` 的三档判定（HTML 独立类名 / JS 字面量 / CSS 规则数）。
   同类一起删的还有 `.center` / `.is-danger` / `.code-box` / `.brand-ring` / `.brand-text` /
   `.head-right` / `.head-actions` / `.modal-bg`（逐个都实测过零引用）。 */
.row { display: flex; gap: var(--sp-3); flex-wrap: wrap; }
.row.center-x { justify-content: center; }

/* ---------------------------------------------------------------- 图标
   内联 SVG（`Site.icon()` 产出，来源 Lucide / ISC）。**尺寸与对齐统一在这里定** ——
   不在 JS 里写 width/height，否则同一个图标在按钮、空态、表格里会各长一样。

   为什么不用 `vertical-align: middle` 对齐文字：中文字体的基线盒与拉丁字体不同，
   `middle` 在中英混排时会飘。改用「图标独占一个 flex 行」的做法：
   凡是同时含图标与文字的容器，都加 `.ico` + flex 对齐。
   ★ 2026-10-13：这句原先举例 `.chip-add` / `.chip-act`（账号页芯片上那些"图标+文字"
     的行内操作）—— 那两处已随用户要求删除。**当前活着的 `.ico` 用法只剩
     `.empty-ico` 那类"图标单独一行"的空态**；将来若再做图标+文字的按钮/芯片，
     照上面这条规则加 flex 对齐（例子没了，规则还在）。 */
.ico {
  width: 16px; height: 16px; flex: none;
  display: inline-block; vertical-align: -0.18em;
  color: currentColor;
}
/* 空态里图标当主视觉，放大到 22px（对齐原来 emoji 的视觉重量） */
.empty .empty-ico .ico { width: 22px; height: 22px; }

/* 等宽微标签 —— 这套设计的签名元素 */
.mono {
  font-family: var(--font-mono);
  font-size: var(--fs-12);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* ============================================================ 顶栏 */
/* ★ `.nav` / `.top` 的**公共声明**在上面那条 `.nav, .top` 规则里（见"顶栏"节）。
   这里只留公开页与后台页**有意不同**的部分，并写明为什么不同。 */
/* 公开页（无侧边栏）：`.nav` 是 body 的普通流式子元素，靠 sticky 停在顶部；
   比后台页矮一点、更透一点（落地页观感更轻）。 */
.nav {
  padding: var(--sp-3) var(--sp-7); min-height: 60px;
  background: rgba(255, 255, 255, .82);
}
.brand { display: flex; align-items: center; gap: var(--sp-3); }
.brand-logo {
  /* ★★ 2026-10-11：这里**以前是**一个文字方块（`.logo` + 硬写的 `AW`）——
     那不是 logo，是用品牌底色拼出来的占位块；真 logo（蓝 A + 橙弧）一直只在
     `favicon-*` 里。⇒ 现在直接用真 logo 图（无构建、无外链，尺寸 30×30 与旧方块一致，
     所以侧边栏折叠态那组规则不用动）。
     ★ `display:block` 是必须的：`<img>` 默认是 inline，会在基线下方留出几像素，
       表现是"品牌字与 logo 没对齐"，而那种 1–3px 的偏移**很难被看出来**。 */
  display: block; width: 30px; height: 30px; flex: none;
  border-radius: var(--r-s);
}
.brand-name {
  font-family: var(--font-display); font-size: 15px; font-weight: 600; letter-spacing: .01em;
}
.nav-right { display: flex; align-items: center; gap: var(--sp-3); }
.who { font-family: var(--font-mono); font-size: var(--fs-13); color: var(--text-2); }

@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }

/* ============================================================ 按钮
   ## 上一版为什么"生硬"——三个具体原因，不是主观感受

   ① **`.btn-ghost` 只覆盖了 background**，基类的 `border: 1px solid var(--line-2)`
      与 `box-shadow` 还在 ⇒ 每个"次要按钮"都是一个**描边方块**。
      而次要按钮本该**静止时没有边框也没有底色**，悬停才浮出来。
   ② **`.btn-primary` 是平涂的**。扩展用的是 `linear-gradient(180deg, …)` +
      `inset 0 1px 0 rgba(255,255,255,.18)` —— 那道**内高光**才是"实体感"的来源，
      缺了它就是一块色板。
   ③ **`.btn-danger` 带永久红边框**。静止时就在喊"危险"，
      一屏里几个删除键会让整个界面显得紧张 —— 危险色应该只在**悬停**时出现。

   ⇒ 三条对应三次修改，且都借用了扩展 `theme.css` 的做法（色值换成本站主色）。
   ============================================================ */
/* ★ 裸 `button` 兜底：扩展的 `theme.css` 里有这一条，本站原先**漏了**。
   漏掉的后果不是"少了一条样式"，而是**凡是没挂 `.btn` 的按钮会渲染成浏览器默认样式**
   （灰色、斜角边、系统圆角）—— 那正是"生硬"的来源。
   下面三个变体类也各自带上了完整基类，所以 `class="btn-primary"` 单独用也不会掉样式。 */
button {
  font: inherit; border: none; cursor: pointer;
  border-radius: var(--r-m); background: transparent; color: inherit;
}

.btn, .btn-primary, .btn-ghost, .btn-danger {
  --btn-fg: var(--text);
  --btn-bd: var(--line-2);
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4); border-radius: var(--r-m);
  border: 1px solid var(--btn-bd); background: var(--panel); color: var(--btn-fg);
  font: inherit; font-size: var(--fs-15); font-weight: 500; line-height: 1.4;
  cursor: pointer; white-space: nowrap; user-select: none; text-decoration: none;
  box-shadow: 0 1px 2px rgba(20, 40, 72, .05);
  transition: background .14s var(--ease), border-color .14s var(--ease),
              color .14s var(--ease), box-shadow .14s var(--ease), transform .14s var(--ease);
}
.btn:hover, .btn-primary:hover, .btn-ghost:hover, .btn-danger:hover {
  box-shadow: 0 2px 8px rgba(20, 40, 72, .09); transform: translateY(-1px);
}
.btn:active, .btn-primary:active, .btn-ghost:active, .btn-danger:active {
  transform: translateY(0); box-shadow: 0 1px 2px rgba(20, 40, 72, .05);
}
.btn:focus-visible, .btn-primary:focus-visible,
.btn-ghost:focus-visible, .btn-danger:focus-visible {
  outline: none; box-shadow: 0 0 0 3px var(--brand-dip), 0 2px 8px rgba(20, 40, 72, .09);
}

/* 主操作：渐变 + 内高光 + 悬停抬升。
   ★ 渐变两端用**写死的两档蓝**而不是 `--brand`/`--brand-ink` ——
     浅色主题里这两个变量是同一个值，那样渐变等于不存在。 */
.btn-primary {
  --btn-fg: #fff; --btn-bd: transparent;
  /* ★★ 2026-10-12（重设计 A）：**去掉那层 180° 渐变与 inset 高光**。
     `linear-gradient(180deg, #2f86f0 → #004a8c)` + `inset 0 1px 0 rgba(255,255,255,.2)`
     是 2015 年前后"拟物按钮"的写法：顶上一条亮边、底下一层深色。
     ⇒ 实色 + **一层**柔影（现代按钮的层次来自阴影，不来自渐变）。 */
  background: var(--brand);
  font-weight: 600;
  box-shadow: 0 1px 2px color-mix(in srgb, var(--brand) 30%, transparent),
              0 6px 16px color-mix(in srgb, var(--brand) 16%, transparent);
}
.btn-primary:hover { background: color-mix(in srgb, var(--brand) 88%, #000); }
.btn-primary:active { transform: translateY(.5px); box-shadow: 0 1px 2px color-mix(in srgb, var(--brand) 34%, transparent); }
.btn-primary:hover {
  background: linear-gradient(180deg, #4d9dff, #1a7ff0);
  box-shadow: 0 4px 12px color-mix(in srgb, var(--brand) 32%, transparent), 0 12px 26px color-mix(in srgb, var(--brand) 24%, transparent),
              inset 0 1px 0 rgba(255, 255, 255, .26);
}

/* 次要操作：静止时无边框、无底色（这是"生硬"的解药） */
.btn-ghost {
  --btn-fg: var(--text-2); --btn-bd: transparent;
  background: transparent; box-shadow: none;
}
.btn-ghost:hover {
  --btn-fg: var(--brand); --btn-bd: var(--line);
  background: var(--brand-dip); box-shadow: none;
}

/* 危险操作：红色只在悬停时出现 */
.btn-danger {
  --btn-fg: var(--text-2); --btn-bd: transparent;
  background: transparent; box-shadow: none;
}
.btn-danger:hover {
  --btn-fg: var(--red); --btn-bd: rgba(207, 47, 47, .22);
  background: rgba(207, 47, 47, .07); box-shadow: none;
}

.btn-sm { padding: 5px var(--sp-3); font-size: var(--fs-13); border-radius: var(--r-m); gap: 5px; }
.btn:disabled, .btn-primary:disabled, .btn-ghost:disabled, .btn-danger:disabled {
  opacity: .45; cursor: not-allowed; transform: none !important; box-shadow: none;
}

/* ============================================================ 面板 */
.panel {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-l);
  box-shadow: var(--sh-2);
}
/* 四角刻度：仪表盘/工程图的语汇，成本极低但一眼就不像模板 */
.panel::before, .panel::after {
  content: ""; position: absolute; width: 11px; height: 11px; pointer-events: none;
  border-color: var(--line-hot); border-style: solid; opacity: .8;
}
.panel::before { top: -1px; left: -1px; border-width: 1px 0 0 1px; border-top-left-radius: var(--r-l); }
.panel::after  { bottom: -1px; right: -1px; border-width: 0 1px 1px 0; border-bottom-right-radius: var(--r-l); }

.panel-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--line);
  position: relative; background: var(--panel-2);
}
.panel-head::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
  background: linear-gradient(90deg, transparent, var(--brand), transparent);
  opacity: .45;
}
.panel-body { padding: var(--sp-6) var(--sp-5); }

/* ============================================================ 落地页 */
/* ★ `width: 100%` 不能省：body 是 flex 列容器，而这里带 `margin: 0 auto` ——
   交叉轴上的 `margin: auto` 会**覆盖** `align-self: stretch`，元素于是按内容宽度收缩。
   症状很有迷惑性：落地页正常（`.hero` 的 `flex: 1` 撑住了），
   而控制台的卡片栅格塌成一列，看起来像"栅格写错了"。 */
.wrap { width: 100%; max-width: 1120px; margin: 0 auto; padding: var(--sp-7) var(--sp-7) var(--sp-10); }

.hero {
  display: grid; grid-template-columns: 1.1fr .9fr; gap: var(--sp-11);
  align-items: center; padding: var(--sp-12) 0 var(--sp-13);
}
@media (max-width: 940px) {
  .hero { grid-template-columns: 1fr; gap: var(--sp-8); padding: var(--sp-9) 0 var(--sp-11); }
}

.eyebrow { display: flex; align-items: center; gap: var(--sp-3); margin-bottom: 24px; }
.eyebrow .rule { flex: 1; max-width: 56px; height: 1px; background: var(--line-2); }

h1.display {
  font-family: var(--font-display);
  font-size: clamp(34px, 5vw, 54px);
  line-height: 1.1; letter-spacing: -.028em; font-weight: 600;
  margin: 0 0 18px;
}
h1.display em {
  font-style: normal; color: var(--brand);
  background: linear-gradient(transparent 68%, color-mix(in srgb, var(--brand) 14%, transparent) 68%);
}
.lede { color: var(--text-2); font-size: var(--fs-18); margin: 0 0 36px; }

/* SPEC 表：把真实事实排成数据表，既是设计元素也是产品说明 */
.spec { border-top: 1px solid var(--line); }
.spec-row {
  display: grid; grid-template-columns: 92px 1fr; gap: var(--sp-4);
  padding: var(--sp-3) 0; border-bottom: 1px solid var(--line);
}
.spec-row dt { margin: 0; }
.spec-row dd { margin: 0; font-size: var(--fs-15); color: var(--text); }

/* ============================================================ 认证面板 */
.auth-panel { padding: var(--sp-7); box-shadow: var(--sh-3); }

.tabs {
  display: flex; gap: 3px; padding: 3px; margin-bottom: 22px;
  background: var(--bg-soft); border-radius: var(--r-full);
}
.tab {
  flex: 1; padding: var(--sp-2); border: 0; border-radius: var(--r-full); background: transparent;
  color: var(--text-3); font: inherit; font-size: var(--fs-15); cursor: pointer;
  transition: background .16s var(--ease), color .16s var(--ease);
}
.tab:hover { color: var(--text-2); }
.tab.active {
  background: var(--panel); color: var(--text); font-weight: 500;
  box-shadow: var(--sh-1);
}

/* ★ 2026-10-12：弹窗里的表单纵向收到 `--sp-3`（原来 `--sp-4`）。
   6 个字段 × 16px 的间距，在 84vh 的弹窗里就是"必须滚"的直接原因之一。
   ★ 只动**弹窗内**的表单，页面上的大表单保持原来的呼吸感。 */
.modal-card .form { gap: var(--sp-3); }
.form { display: flex; flex-direction: column; gap: var(--sp-4); }
.form label { display: flex; flex-direction: column; gap: var(--sp-2); }
.form .lbl {
  font-family: var(--font-mono); font-size: var(--fs-12); letter-spacing: .13em;
  text-transform: uppercase; color: var(--text-3);
}
.form input, .form select, .form textarea {
  padding: var(--sp-3) var(--sp-4); border: 1px solid var(--line-2); border-radius: var(--r-m);
  background: var(--panel); color: var(--text); font: inherit; font-size: var(--fs-15); width: 100%;
  transition: border-color .16s var(--ease), box-shadow .16s var(--ease);
}
.form input::placeholder { color: var(--text-3); }
.form input:focus, .form select:focus, .form textarea:focus {
  outline: none; border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-dip);
}
.form small { color: var(--text-3); font-size: var(--fs-13); line-height: 1.55; }

/* 验证码行：输入框 + 「发送」按钮同排 */
.code-row { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-3); align-items: stretch; }
.code-row input {
  font-family: var(--font-mono); font-size: var(--fs-18); letter-spacing: .34em; text-align: center;
}
.code-row .btn { white-space: nowrap; }

.msg {
  margin-top: var(--sp-4); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-m);
  font-size: var(--fs-15); border: 1px solid transparent;
}
.msg.err { background: rgba(207, 47, 47, .07); border-color: rgba(207, 47, 47, .25); color: var(--red-ink); }
.msg.ok  { background: var(--brand-dip); border-color: color-mix(in srgb, var(--brand) 25%, transparent); color: var(--brand); }

/* 入场：一次编排好的错峰揭示，比一堆散落的小动效更值 */
.rise { opacity: 0; transform: translateY(12px); animation: rise .62s var(--ease) forwards; }
@keyframes rise { to { opacity: 1; transform: none; } }
.d1 { animation-delay: .04s; } .d2 { animation-delay: .12s; }
/* ★ 2026-10-09：这一行原先还带着 `.d4 { animation-delay: .28s }` —— **只删了 `.d4`**。
   ★ `.d3` 必须留着：`admin.html` 与 `profile.html` 里都在用（实测 2 处）。
   ★ `.d4` 是死 CSS（零引用）。实测踩过一次：我第一版把**整行**删掉，
     连带把活的 `.d3` 一起去掉了 —— 所以"删一个类名"要用**最小的字面量**，
     不要用"那一行"当锚点（同 PITFALLS #18：锚点要唯一且紧贴目标）。 */
.d3 { animation-delay: .2s; }
@media (prefers-reduced-motion: reduce) {
  .rise, .dot { animation: none; opacity: 1; transform: none; }
}

/* ============================================================ 控制台
   逐类对齐浏览器扩展 akso-pass（原 quick-login）的「并行管理工作台」（`ui/parallel/parallel.css`）。

   ## 只换色值，不换骨架

   扩展的 `--sb-*` 变量与本站的语义一一对应，映射关系是固定的：

     --sb-surface → --panel      --sb-border  → --line      --sb-brand  → --brand-ink
     --sb-surface-2 → --panel-2  --sb-border-strong → --line-2  --sb-success → --green
     --sb-text → --text          --sb-radius  → --r-m        --sb-danger → --red
     --sb-text-secondary → --text-2  --sb-radius-lg → --r-l  --sb-shadow-sm → --sh-1

   ★ 为什么"骨架照抄、色值替换"而不是重新设计：这套布局的每一处都在解决
   一个具体问题（盒子 chips 的悬停操作、批量条的常驻位置、卡片栅格的列宽下限），
   而它们是在真实使用里调出来的。重新设计等于把那些调整重做一遍，且**未必做得更好**。

   ★ 唯一实质改动：站点那一栏从扩展的「授权 host 清单」改成
   「带名字的平台环境」（服务端有 `name` 字段），并加了域名预处理的实时预览。
   ============================================================ */

/* ---- 页头 ----
   ★★ 2026-10-09：这里原有一整组**旧顶栏的残留**，全部实测零引用后删掉：
     `.head` / `.brand-ring` / `.brand-ring i` / `.brand-text h1` / `.brand-text p`
     / `.head-right` / `.head-actions`
   —— 它们是 `header.head` 那套（圆形渐变 logo + 品牌字）的样式。
   骨架重构后顶栏改成 `<header class="top">`、元素换成 `.top-title` / `.top-right`，
   而这一组**留了下来**：不报错、页面正常、没有任何门禁覆盖。

   ★ 同组里**只有一个类名还活着**：`.brand` —— 落地页与授权页仍在用它。
     所以**不能整段删**，只能逐条删；`.brand` 的规则在下方 `.nav` 段里（原处未动）。
   ⇒ 删除判据（三档都为零才删）：HTML 的 `class=` 里作为**独立类名**出现 /
     `site.js` 的**字符串字面量**里出现 / CSS 里被别的选择器引用。
     见 `tmp/av-deadcss-final.py`。 */

/* 胶囊统计条（扩展的 .stats / .stat / .pulse-dot） */
.stats {
  display: flex; align-items: center; gap: var(--sp-4);
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 999px; box-shadow: var(--sh-1); padding: var(--sp-2) var(--sp-4);
}
.stat { display: flex; align-items: baseline; gap: 5px; font-size: var(--fs-13); color: var(--text-2); }
.stat b { font-size: var(--fs-18); font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text); }
/* ★★ 2026-10-12：`.sys` 与 `.pulse-dot`（落地页右上角"运行中"那一块、控制台"站点"那一项的
   呼吸灯）已随元素一起删除 —— 用户要求"不要显示运行中 / 不需要呼吸灯"。
   ★★ 但 `.dot` **必须保留**：它不只是那个指示灯的圆点，**登录卡片上的 TLS 指示点也是它**
      （`index.html:59` 与 `authorize.html:56` 的 `<i class="dot">TLS</i>`）。
      第一版我把它一起删了 —— 那样 TLS 那个点会变成一个**没有任何样式的 `<i>`**
      （在页面上完全看不见），而"指示灯不见了"不报错、也没有判据会红。
   ★ 同时去掉 `animation: pulse …`：脉冲原本是为"运行中"那个状态服务的，
      现在这个点只是"TLS 已启用"的静态标记（`@keyframes pulse` 随之没有引用者了）。 */
.dot {
  display: inline-block;
  width: 6px; height: 6px; border-radius: 50%; background: var(--green);
  box-shadow: 0 0 0 3px rgba(15, 138, 82, .13);
}
.status-chip {
  font-family: var(--font-mono); font-size: var(--fs-12); padding: var(--sp-1) var(--sp-3); border-radius: 999px;
  border: 1px solid var(--line); background: var(--panel); color: var(--text-2);
  font-variant-numeric: tabular-nums;
}

/* ---- 区块卡（扩展的 .card.block / .block-head） ---- */
.card {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-l); box-shadow: var(--sh-1);
}
.block { padding: var(--sp-6); display: flex; flex-direction: column; gap: var(--sp-4); }
.block-head { display: flex; flex-direction: column; gap: 3px; }
.block-head.row {
  flex-direction: row; align-items: flex-start;
  justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap;
}
.block-head h2 { margin: 0; font-family: var(--font-display); font-size: var(--fs-18); font-weight: 700; letter-spacing: .01em; }
.desc { margin: 0; font-size: var(--fs-13); line-height: 1.6; color: var(--text-2); }

/* ---- 盒子 chips（扩展的 .chips / .chip）——★ 只做筛选，没有行内操作了 ---- */
.chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.chip {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font: inherit; font-size: var(--fs-13); padding: var(--sp-2) var(--sp-4); border-radius: 999px;
  border: 1px solid var(--line-2); background: var(--panel); color: var(--text-2);
  cursor: pointer; transition: all .15s var(--ease); font-variant-numeric: tabular-nums;
}
.chip:hover { border-color: var(--brand); color: var(--brand); }
.chip.active {
  background: var(--brand-ink); border-color: var(--brand-ink); color: #fff;
  box-shadow: 0 4px 12px color-mix(in srgb, var(--brand) 26%, transparent);
}
.chip .chip-n {
  font-size: var(--fs-11); background: rgba(20, 40, 72, .08); border-radius: 999px;
  padding: 1px var(--sp-2); font-weight: 700;
}
.chip.active .chip-n { background: rgba(255, 255, 255, .24); color: #fff; }
/* ★★ 2026-10-13（用户要求）：这里原先有一组 `.chip-act`（悬停浮现的行内操作：
   改名 / 启用禁用 / 删除）与 `.chip-add`（虚线边框的「+ 新建盒子」）——
   **两组都已删除**：账号页那排芯片现在**只做筛选**，盒子的管理动作全在
   「盒子管理」台账里（那里用的是 `.btn`，不是 `.chip-act`）。
   ★ 删这两组不是顺手清理：本仓有一条判据专门抓"定义了但没人用的类名"
     （`tests/test_pages.py::test_no_dead_css_classes`，实测一次清出过 10 个死类）——
     留着它们，下一次跑门禁就会红，而那时读的人会以为是**新**引入的问题。 */
.chip-off > span:first-child { text-decoration: line-through; }
.chip-off { opacity: .55; }
.chip-off.active { opacity: 1; }

/* ---- 批量工具条（扩展的 .batch-bar） ---- */
.batch-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); flex-wrap: wrap; padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-m); background: var(--brand-dip);
  border: 1px solid color-mix(in srgb, var(--brand) 22%, transparent);
}
.batch-info { font-size: var(--fs-13); color: var(--text); }
.batch-info b { font-weight: 700; color: var(--brand); font-variant-numeric: tabular-nums; }
.batch-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; }

/* ---- 账号卡片栅格（扩展的 .account-grid / .account-card） ---- */
.account-grid {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(268px, 100%), 1fr)); gap: var(--sp-3);
}
.account-grid .empty { grid-column: 1 / -1; }
.account-card {
  position: relative; overflow: hidden;
  /* ★★ 2026-10-12（第 ⑤ 条）：底色原先是 `--panel-2`（#f6f9fd，一层很淡的蓝灰），
     压在同样是浅色的页面底（`--bg` #f2f6fb）上 ⇒ 卡片**几乎浮不起来**，
     整片账号区看着像一块没有层次的浅灰板。改成 `--panel`（纯白）后，
     卡片与页面底之间才真的有对比，右边那条站点色条也随之跳出来。 */
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-l); padding: var(--sp-4) var(--sp-4);
  display: flex; flex-direction: column; gap: var(--sp-3);
  transition: box-shadow .16s var(--ease), transform .16s var(--ease),
              border-color .16s var(--ease), opacity .16s var(--ease);
  opacity: 0; animation: card-in .42s var(--ease) forwards;
  animation-delay: calc(var(--i, 0) * 35ms);
}
@keyframes card-in { from { opacity: 0; transform: translateY(8px) scale(.985); } to { opacity: 1; transform: none; } }
@keyframes card-out { to { opacity: 0; transform: scale(.96); } }
.card-removing { animation: card-out .28s var(--ease) forwards; }
@media (prefers-reduced-motion: reduce) { .account-card { animation: none; opacity: 1; } }
/* 左侧强调条：色相由站点 id 哈希决定 —— 同一站点永远同色，颜色在这里是身份标记。
   ★ 2026-10-12（第 ⑤ 条）：宽度 3→4px、不透明度 .85→1。它是卡片上**唯一的彩色**，
     压到 85% 之后那种"灰扑扑"的感觉主要就来自这里。 */
.account-card::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
  background: var(--accent, var(--brand));
}
.account-card:hover {
  box-shadow: 0 8px 24px rgba(20, 40, 72, .1);
  border-color: var(--line-2); transform: translateY(-1px);
}
.account-card.selected {
  border-color: var(--brand);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--brand) 18%, transparent), 0 8px 24px rgba(20, 40, 72, .08);
}
.ac-check {
  position: absolute; top: 11px; right: 12px; width: 17px; height: 17px;
  accent-color: var(--brand); cursor: pointer; display: none;
}
.batch-on .ac-check { display: block; }
.batch-on .badge { margin-right: 22px; }

.ac-head { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
.ac-avatar {
  width: 40px; height: 40px; flex: none; border-radius: var(--r-m);
  display: grid; place-items: center; font-size: var(--fs-18); font-weight: 700;
  /* ★★ 2026-10-12（用户要求"色彩更多样"）：配色改成**按账号**从精选 5 色里取
     （见 `site.js` 的 `CARD_HUES` —— 色相与 `akso-pass` 的 `SESSION_COLORS` 一一对应）。
     · 主色 `--ring`（46% 亮度）给色条、描边、头像环用；
     · **文字用 `--ring-ink`（38%）** —— 46% 的蓝做正文只有约 3.4:1，而首字母是文字，
       必须过 4.5:1。这类"颜色好看但读不清"的缺陷不会报错，只能靠判据拦。
       判据：`tests/test_card_palette.py`。 */
  color: var(--ring-ink, var(--ring, var(--brand-ink)));
  background: color-mix(in srgb, var(--ring, var(--brand)) 14%, #fff);
  border: 1.5px solid color-mix(in srgb, var(--ring, var(--brand)) 55%, #fff);
}
.account-card .meta { flex: 1; min-width: 0; }
.account-card .alias {
  font-weight: 600; font-size: var(--fs-15);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.account-card .sub {
  font-size: var(--fs-12); color: var(--text-2); margin-top: 2px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.box-tag {
  display: inline-block; font-size: var(--fs-11); padding: 1px var(--sp-2); margin-right: 5px;
  border-radius: 999px; background: var(--brand-dip); color: var(--brand); vertical-align: 1px;
}
.badge {
  font-size: var(--fs-12); padding: 3px var(--sp-2); border-radius: 999px; white-space: nowrap;
  flex: none; font-variant-numeric: tabular-nums; border: 1px solid transparent;
}
/* ★★ 2026-10-12（用户：「云端账号库相关的 UI/按钮色彩较暗淡，看起来死气沉沉」）：
   两个彩色徽章的不透明度都太低了 —— **9% 的绿在白底上几乎看不出是绿色**，
   而徽章是每张卡上都有的元素，它一发灰整页就灰。
   ⇒ 底色 9% → 16%、描边 22% → 42%，两个徽章同一口径（并排时浓淡不一致会更显脏）。
   ★ 只动**不透明度**，不动 `--brand` / `--accent` —— 那两个是"logo 真色"，
     有判据钉着（`acceptance.py` 的「品牌色 = logo 真色」），而且它们本来不是发灰的原因。 */
/* ★ 2026-10-12（P0）：底的不透明度 .16 → **.10**。
   原因由判据抓到：`.badge.on` 是「绿字 + 绿 16% 底」，叠在白卡片上底色是 `#dcece2`，
   而绿字在那上面的对比度只有 **4.18**（要 4.5）—— 而徽章是每张卡上都有的元素。
   ⇒ 降到 10%（叠白后 `#e7f2ea`，绿字约 4.6）。**变淡的是底，不是文字** ——
     用户反馈的「发灰」主要来自文字色，把文字加深、底放松才是对的方向。 */
.badge.on   { background: rgba(10, 143, 79, .10); color: var(--green-ink); border-color: rgba(10, 143, 79, .38); }
.badge.off  { background: var(--bg-soft); color: var(--text-2); border-color: var(--line); }
.badge.warn { background: rgba(180, 83, 9, .10); color: var(--amber-ink); border-color: rgba(180, 83, 9, .36); }
.ac-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.ac-actions .btn { font-size: var(--fs-13); padding: 5px var(--sp-3); border-radius: var(--r-m); }

/* ---- 底部双卡（扩展的 .bottom-grid / .form / .field） ---- */
.bottom-grid {
  display: grid;
  /* ★★ 2026-10-12（P3）：`minmax(380px, 1fr)` → `minmax(min(380px, 100%), 1fr)`。
     改前：380px 是**硬下限**，而 390px 视口减掉内边距只剩 330px 可用
     ⇒ 网格把自己撑到 380 ⇒ 页面出**横向滚动条**（实测 scrollWidth 396 > 390）。
     `min(380px, 100%)` 让 380px 变成"**想要的**列宽"而不是"不容协商的下限"：
     宽屏行为一个字没变（依然自动分两列），窄屏自动塌成一列。 */
  grid-template-columns: repeat(auto-fit, minmax(min(380px, 100%), 1fr));
  /* ★★ 2026-10-12（用户反馈"页面底部都没有对齐"）：去掉 `align-items: start`。
     它让每栏各自按内容高度 ⇒ **底边必然不齐**（左栏"添加账号" 5 个字段、
     右栏"站点管理"只有输入框 + 列表）。改成默认的 `stretch`：两栏等高、底边对齐。 */
  gap: var(--sp-5); align-items: stretch;
}
.form { display: flex; flex-direction: column; gap: var(--sp-3); }
/* ★ 2026-10-12：并排字段**横向**松一格（12 → 16px），纵向保持 12px。
   用户看到的拥挤主要是横向的（两个输入框几乎贴在一起）。 */
.field-pair { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--sp-3) var(--sp-4); }
@media (max-width: 560px) { .field-pair { grid-template-columns: 1fr; } }
.field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.field span { font-size: var(--fs-12); color: var(--text-2); letter-spacing: .04em; }
.field input, .field select { width: 100%; }
.btn-group { display: flex; gap: var(--sp-2); align-items: center; margin-top: 2px; flex-wrap: wrap; }
.btn-group button { white-space: nowrap; }

/* 表单控件（控制台内的，与落地页同源但更紧凑） */
.form input, .form select, .site-grid input {
  padding: var(--sp-2) var(--sp-3); border: 1px solid var(--line-2); border-radius: var(--r-m);
  background: var(--panel); color: var(--text); font: inherit; font-size: var(--fs-15);
  transition: border-color .14s var(--ease), box-shadow .14s var(--ease);
}
.form input:focus, .form select:focus, .site-grid input:focus {
  outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-dip);
}

/* ---- 批量导入面板（扩展的 .import-panel） ---- */
.import-panel {
  display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-4);
  border-radius: var(--r-m); background: var(--panel-2);
  border: 1.5px dashed var(--line-2);
}
.import-panel textarea {
  font-family: var(--font-mono); font-size: var(--fs-13); line-height: 1.7;
  resize: vertical; min-height: 110px;
  padding: var(--sp-2) var(--sp-3); border: 1px solid var(--line-2); border-radius: var(--r-m);
  background: var(--panel); color: var(--text);
}
.import-panel code {
  background: var(--brand-dip); padding: 1px var(--sp-2); border-radius: var(--r-s);
  font-size: var(--fs-12); color: var(--brand); font-family: var(--font-mono);
}

/* ---- 站点管理（扩展的 .site-grid / .plain / .site-list） ---- */
/* ★★★ 2026-10-13（用户要求：站点要能填名称）：从两列改成**三列**
   （名称 / 地址 / 按钮）。★ 名称那列给 `minmax(0, 0.8fr)`：
   它通常比地址短，但**必须能被压缩**（不加 `minmax(0, …)` 时长站点名会把
   地址列挤出去 —— 那正是 `test_mobile_layout.py` 那条判据管的事）。
   ★ 窄屏（≤720px）改成两行：名称与地址各占一行，按钮跨两列 ——
     三列在 390px 下每列只有一百来像素，输入框会窄到没法用。 */
.site-grid {
  display: grid; grid-template-columns: minmax(0, .8fr) minmax(0, 1.6fr) auto;
  gap: var(--sp-2); align-items: center;
}
@media (max-width: 720px) {
  .site-grid { grid-template-columns: minmax(0, 1fr) auto; }
  #s-name { grid-column: 1 / -1; }
}
/* 建站点时可选的「同步建同名盒」。★ 独立一行（不塞进 .site-grid）：
   三列表单里再塞一个控件会把窄屏的输入框挤到没法用。 */
.create-box-row {
  display: flex; align-items: center; gap: var(--sp-2);
  margin-top: var(--sp-2);
  font-size: var(--fs-13); color: var(--text-2);
  cursor: pointer; user-select: none;
}
.create-box-row input { width: 16px; height: 16px; accent-color: var(--brand); flex: none; }
.plain { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
/* ★★ 2026-10-12（用户要求，与 akso-pass 同步）：站点从"一行"改成**卡片**。
   三件事各占一个明确的视觉位置：名称 / 域名 / **账号数量**。
   ★ 用 `grid` 而不是 `flex`：域名与账号数量是**两列**，名称要跨列；
     flex 做跨列需要额外包一层，grid 一行就能表达，窄屏也只改一处。
   ★ 保留 `border: 1px solid transparent`（**不画线**）—— P2 那条判据要求
     "卡片内部的面不许再画边框"（框套框），而 `transparent` 保留占位、
     免得去掉边框后尺寸跳 2px。悬停时才显色。
   ★ 类名与扩展侧一致（`.site-card*`）⇒ `tests/test_contrast.py` 的
     `NESTED_IN_CARD` 与 `tests/test_pages.py` 的死类名扫描都要跟着改。 */
.site-list .site-card {
  display: grid;
  grid-template-columns: 1fr auto auto;
  grid-template-areas:
    'name   count actions'
    'domain count actions';
  align-items: center;
  gap: 2px var(--sp-3);
  padding: var(--sp-3) var(--sp-3);
  background: var(--panel-2); border: 1px solid transparent; border-radius: var(--r-l);
  transition: border-color .14s var(--ease);
}
.site-list .site-card:hover { border-color: var(--line-2); }

.site-card-name {
  grid-area: name;
  font-weight: 600; font-size: var(--fs-15);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.site-card-domain {
  grid-area: domain;
  font-size: var(--fs-12); color: var(--text-2); margin-top: 1px;
  font-family: var(--font-mono);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* 账号数量：数字大、单位小 —— 扫视时先看到数字 */
.site-card-count {
  grid-area: count;
  display: flex; align-items: baseline; gap: 4px; flex: none;
  padding: 0 var(--sp-1);
}
.site-card-count b {
  font-size: var(--fs-18); font-weight: 700; color: var(--brand);
  font-variant-numeric: tabular-nums;
}
.site-card-count span { font-size: var(--fs-11); color: var(--text-2); white-space: nowrap; }

/* 操作区不参与收缩（长域名会把它挤成竖排 ⇒ "删除"断成两行） */
.site-actions { grid-area: actions; display: flex; gap: var(--sp-2); align-items: center; flex: none; white-space: nowrap; }

/* 窄屏：数量与操作换到第二行 */
@media (max-width: 720px) {
  .site-list .site-card {
    grid-template-columns: 1fr auto;
    grid-template-areas:
      'name   actions'
      'domain actions'
      'count  actions';
  }
}

/* 域名预处理的实时预览（本站新增，扩展里没有对应物） */
.preview {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding: var(--sp-2) var(--sp-3); border-radius: var(--r-m); font-size: var(--fs-13);
  background: var(--brand-dip); border: 1px solid color-mix(in srgb, var(--brand) 20%, transparent);
  color: var(--text-2);
}
.preview.bad { background: rgba(207, 47, 47, .06); border-color: rgba(207, 47, 47, .24); color: var(--red-ink); }
.preview .k { font-family: var(--font-mono); color: var(--brand); font-weight: 600; }
.preview .why { margin-left: auto; font-family: var(--font-mono); font-size: var(--fs-11); opacity: .75; }

/* ---- 空态（扩展的 .empty） ---- */
/* ★★ 2026-10-12（P2）：**空状态是最常被看到的一屏**（每个新用户第一眼就是它），
   而改前它是"接近白的底 + 虚线框 + 灰图标（22px）"—— 最没存在感的一处。
   ⇒ 图标加**品牌色浅圆底**、放大到 52px；虚线换实线浅描边 + 轻投影。
   ★ 只加**一处**品牌色，不多刷：空状态的职责是"告诉你怎么开始"，不是装饰。
   ★ 锚点取**多行**（`.empty` 这个名字在三处出现：主规则 + 两处媒体查询覆盖，
     只用 `.empty {` 会命中三次 —— 第一版就是这么红的）。 */
.empty {
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-2);
  padding: var(--sp-7) var(--sp-4); text-align: center; color: var(--text-2);
  font-size: var(--fs-13); line-height: 1.7;
  background: radial-gradient(220px 90px at 50% 0%, color-mix(in srgb, var(--brand) 5%, transparent), transparent 70%), var(--panel-2);
  border: 1px solid var(--line); box-shadow: var(--sh-1); border-radius: var(--r-l);
}
/* ★ P2：图标从 `font-size: 32px` 的裸字形改成**品牌色圆底 + 48px 方形**。
   裸字形在浅底上没有体积感，而这一屏恰恰需要"有东西在那里"。 */
.empty .empty-ico {
  width: 52px; height: 52px; display: grid; place-items: center;
  border-radius: var(--r-full, 999px);
  color: var(--brand);
  background: color-mix(in srgb, var(--brand) 10%, #fff);
  border: 1px solid color-mix(in srgb, var(--brand) 22%, #fff);
}
.empty .empty-ico .ico { width: 24px; height: 24px; }

/* ============================================================ 授权页 */
/* ★ 2026-10-09：删掉了 `.code-box` 那一组（规则头 + `span` + 560px 媒体查询）。
   它是**死 CSS** —— 授权页的 HTML 里没有任何元素用 `class="code-box"`。
   ★ 它对应的功能还在：授权码是用**逐字符 `<span>` 拼**出来的，
     但那些 span 靠内联样式与 `.mono` 排版（见 `authorize.html`），不靠这个类。
   ⇒ 也就是说这一组是**设计阶段留下、实现时走了另一条路**的残留，
     而它不会报错、也不会被任何门禁抓到。 */

.note {
  margin-top: 24px; padding: var(--sp-4) var(--sp-4); font-size: var(--fs-15); line-height: 1.65;
  border-left: 2px solid var(--amber); background: rgba(180, 83, 9, .06);
  border-radius: 0 var(--r-m) var(--r-m) 0; color: var(--text-2);
}
.note b { color: var(--amber-ink); }

/* ============================================================ 弹层
   ★★ 2026-10-09：这里原本是**两套并存的弹层**，而它们**都定义了 `.modal`**：

     | 位置 | `.modal` 的含义 | 类名 |
     |---|---|---|
     | 这里（落地页那套） | **卡片**（宽 500px） | `.modal-bg` 遮罩 + `.modal` 卡片 |
     | 下面「弹窗」段（控制台那套） | **遮罩**（fixed inset:0） | `.modal` 遮罩 + `.modal-card` 卡片 |

   而 HTML 里**只用控制台那套**（`class="modal hidden"` 是遮罩、里面是 `.modal-card`），
   `.modal-bg` **零引用**。

   ⇒ 结果是：下面那份 `.modal` 把上面这份**整个覆盖**，
     所以落地页那套的"宽卡 + `.modal h3`"**从来没有生效过**；
     而 `.modal-bg` 是一条**完全没人用的死规则**。

   ★ 那条注释当时写的是"**两套的类名不重叠，所以不冲突**" ——
     这句话是**错的**（两份都叫 `.modal`），而它让下一个人不会去查。
     **注释里的错误断言比没有注释更坏。**

   ⇒ 处置：删掉死掉的 `.modal-bg` 与落地页那份 `.modal` / `.modal h3`，
     只留控制台那一份（唯一的真实实现）。
   ★ 同时删掉了这里重复的 `@keyframes fade` —— 它在本文件里**定义了两次**
     （这里与「弹窗」段各一次，内容逐字相同）。CSS 里后一份生效，所以
     两份同时存在时**行为正确、看不出问题**，这也是它一直没被发现的原因。
     现在只留「弹窗」段那一份（在它唯一的消费者 `.modal` 旁边）。 */

/* ============================================================ 管理页
   2026-10-07 重做。原先这一页是**两张只读列表**（用户卡 / 发信卡），
   与「管理」这个词不符 —— 它一个写操作都做不了。

   重做后承担四件事：① 门槛（令牌）② 存量总览 ③ 用户 CRUD 表格 ④ 发信记录。
   骨架仍是本仓既有语汇（`.card` / `.block` / `.badge` / `.mono` / `.empty` /
   `.form` / `.field` / `.modal`），只新增管理页专属的三块：
   门槛卡、数字卡、以及一张**真正的表格**（列表超过 ~8 行时卡片栅格就不如表格了 ——
   管理页要能横向比较"谁的账号最多"，而卡片栅格没法比较）。 */

/* ---- 门槛 ---- */
.gate { padding: var(--sp-6); }
.gate-grid { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--sp-5); align-items: stretch; }
@media (max-width: 620px) { .gate-grid { grid-template-columns: 1fr; } }
.gate-icon {
  display: grid; place-items: center; width: 48px; height: 48px; border-radius: var(--r-l);
  color: var(--brand); background: var(--brand-dip);
  border: 1px solid color-mix(in srgb, var(--brand) 20%, transparent);
}
.gate-body h2 { margin: 0 0 6px; font-family: var(--font-display); font-size: var(--fs-18); font-weight: 700; }
.gate-body code {
  background: var(--brand-dip); padding: 1px var(--sp-2); border-radius: var(--r-s);
  font-size: var(--fs-12); color: var(--brand);
}
.gate-form { display: flex; gap: var(--sp-3); align-items: flex-end; flex-wrap: wrap; margin-top: 15px; }
.gate-btns { display: flex; gap: var(--sp-2); }

/* ---- 存量总览 ---- */
.intro {
  display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  gap: var(--sp-7); align-items: center; margin-bottom: 18px;
}
@media (max-width: 860px) { .intro { grid-template-columns: 1fr; gap: var(--sp-5); } }
.stat-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); }
@media (max-width: 420px) { .stat-cards { grid-template-columns: 1fr; } }
.stat-card {
  position: relative; overflow: hidden;
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--sp-4) var(--sp-4); border-radius: var(--r-l);
  background: var(--panel); border: 1px solid var(--line); box-shadow: var(--sh-1);
}
/* 左侧刻度条：本设计语言的签名元素，顺带把"这一格是不是活的"编码进颜色 */
.stat-card::before {
  content: ""; position: absolute; left: 0; top: 12px; bottom: 12px; width: 3px;
  border-radius: 0 var(--r-s) var(--r-s) 0; background: var(--line-2);
}
.stat-card.good::before { background: var(--green); }
/* ★ P2：11px + `.14em` 字距把两三个字拉散了，读起来像装饰而不是标签。
   ⇒ 字距收到 `.06em`、字号回到 `--fs-12`。 */
.stat-k { font-size: var(--fs-12); letter-spacing: .06em; color: var(--text-3); }
.stat-v {
  font-family: var(--font-display); font-size: var(--fs-32); font-weight: 700;
  line-height: 1.15; font-variant-numeric: tabular-nums; color: var(--text);
}
.stat-card.good .stat-v { color: var(--green-ink); }
.stat-sub { font-size: var(--fs-12); color: var(--text-2); }

/* ---- 工具条（搜索 + 状态分段 + 刷新） ---- */
.toolbar { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.search {
  display: flex; align-items: center; gap: var(--sp-2); flex: 1 1 220px; min-width: 0;
  padding: 0 var(--sp-3); border: 1px solid var(--line-2); border-radius: var(--r-m);
  background: var(--panel); color: var(--text-3);
  transition: border-color .14s var(--ease), box-shadow .14s var(--ease);
}
.search:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-dip); }
.search input {
  border: none; background: none; outline: none; flex: 1; min-width: 0;
  padding: var(--sp-2) 0; font: inherit; font-size: var(--fs-15); color: var(--text);
}
.search input::-webkit-search-cancel-button { cursor: pointer; }

.seg { display: flex; gap: 2px; padding: 3px; background: var(--bg-soft); border-radius: var(--r-m); }
.seg-btn {
  padding: var(--sp-2) var(--sp-4); border: 0; border-radius: var(--r-s); background: transparent;
  color: var(--text-3); font: inherit; font-size: var(--fs-13); cursor: pointer;
  /* `nowrap` 不能省：flex 子项默认可以收缩到 min-content，
     「已停用」在窄容器里会被折成两行，而它看起来像"这两个字分家了"而不是换行。 */
  white-space: nowrap;
  transition: background .14s var(--ease), color .14s var(--ease);
}
.seg-btn:hover { color: var(--text-2); }
.seg-btn.active { background: var(--panel); color: var(--text); font-weight: 600; box-shadow: var(--sh-1); }

/* ---- 表格 ----
   窄屏**不横向滚动整页**，而是让表格自己滚 —— 后者会把顶栏也一起推走。 */
.table-wrap { overflow-x: auto; margin: 0 -4px; padding: 0 var(--sp-1); }
.utable { width: 100%; border-collapse: collapse; font-size: var(--fs-15); min-width: 620px; }
.utable th {
  text-align: left; padding: var(--sp-2) var(--sp-3); white-space: nowrap;
  font-family: var(--font-mono); font-size: var(--fs-11); letter-spacing: .13em;
  text-transform: uppercase; color: var(--text-3); font-weight: 500;
  border-bottom: 1px solid var(--line-2);
}
.utable th.num, .utable td.num { text-align: right; font-variant-numeric: tabular-nums; }
.utable th.acts { text-align: right; }
.utable td { padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--line); vertical-align: middle; }
.utable tbody tr { animation: card-in .34s var(--ease) backwards; animation-delay: calc(var(--i, 0) * 22ms); }
.utable tbody tr:hover { background: var(--panel-2); }
.utable tbody tr:last-child td { border-bottom: none; }
.utable td.acts { text-align: right; white-space: nowrap; }
.utable td.acts .btn-ghost, .utable td.acts .btn-danger { font-size: var(--fs-13); padding: var(--sp-1) var(--sp-2); }
.utable .udate { font-size: var(--fs-12); color: var(--text-2); letter-spacing: 0; text-transform: none; }
.utable .umail { letter-spacing: 0; text-transform: none; font-size: var(--fs-12); color: var(--text-2); }
.utable .empty { grid-column: auto; margin: 4px 0; }

.ucell { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
.avatar.av-xs {
  width: 30px; height: 30px; font-size: var(--fs-13); border-radius: var(--r-s);
}
.umeta { display: flex; flex-direction: column; min-width: 0; }
.uname { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sess-live { color: var(--green-ink); font-weight: 700; }

/* ---- 实体按钮：`--solid` 变体 ----
   `.btn-danger` 是"静止时安静、悬停才红"的次要操作（见按钮体系那一段）。
   而删除确认弹窗里的主按钮**必须一进来就是红的** —— 它是那一步里唯一的动作，
   且用户刚刚手输了邮箱来表达意图。两者语义不同，所以是两个类，不是覆盖。 */
.btn-danger-solid {
  --btn-fg: #fff; --btn-bd: transparent;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4); border-radius: var(--r-m);
  background: linear-gradient(180deg, #e05555, var(--red));
  color: #fff; font: inherit; font-size: var(--fs-15); font-weight: 600;
  border: 1px solid transparent; cursor: pointer; white-space: nowrap;
  box-shadow: 0 2px 6px rgba(207, 47, 47, .26), inset 0 1px 0 rgba(255, 255, 255, .2);
  transition: background .14s var(--ease), box-shadow .14s var(--ease), transform .14s var(--ease);
}
.btn-danger-solid:hover:not(:disabled) {
  background: linear-gradient(180deg, #ec6b6b, #d93a3a);
  box-shadow: 0 4px 12px rgba(207, 47, 47, .32), inset 0 1px 0 rgba(255, 255, 255, .26);
  transform: translateY(-1px);
}
.btn-danger-solid:disabled { opacity: .4; cursor: not-allowed; transform: none; box-shadow: none; }
.btn-danger-solid:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(207, 47, 47, .2); }

/* ---- 初始密码：只在创建成功那一次出现 ---- */
.created-box {
  display: flex; flex-direction: column; gap: var(--sp-1);
  padding: var(--sp-4) var(--sp-4); border-radius: var(--r-m); background: var(--panel-2);
  border: 1px dashed var(--line-2);
}
.created-val {
  font-size: var(--fs-15); color: var(--text); letter-spacing: 0; text-transform: none;
  overflow-wrap: anywhere; user-select: all;
}
.created-pw { font-size: var(--fs-18); font-weight: 600; color: var(--brand); }
.note.danger {
  border-left-color: var(--red); background: rgba(207, 47, 47, .06);
}
.note.danger b { color: var(--red-ink); }

/* ============================================================ 页脚 */
.foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  /* ★ 2026-10-13 加 `flex-wrap`：页脚从两项变成三项（品牌 / 备案号 / 版本号），
     而窄屏（390px）下三项一行放不下 ⇒ 不加它就会**出横向滚动条**，
     而那种缺陷是"页面能开、只是能左右拖"，静态判据看不出来
     （几何那部分由 tools/verify-mobile-live.mjs 量 scrollWidth）。 */
  flex-wrap: wrap;
  width: 100%; max-width: 1120px; margin: 40px auto 0; padding: var(--sp-7) var(--sp-7) var(--sp-9);
  border-top: 1px solid var(--line);
  font-family: var(--font-mono); font-size: var(--fs-12); letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-3);
}

/* 备案号链接（工信部要求：网站底部标明备案号 + 链接到备案系统）。
   ★ 刻意**不用**全局 `<a>` 那个品牌蓝：页脚是弱化区，备案号与旁边的
     "Akso Cloud / 版本号"同级 ⇒ 用 `--text-3`。它在 `.foot` 这个底色上有
     对比度判据守着（tests/test_contrast.py 的"四个真实底色"里就含页脚那一档），
     悬停时才变品牌色。 */
.foot .icp { color: var(--text-3); }
.foot .icp:hover { color: var(--brand); }

/* ============================================================ 弹窗
   这是**唯一**一套弹层实现：`.modal` 是**遮罩**（fixed inset:0），
   `.modal-card` 是里面的卡片。

   ★★ 2026-10-09 更正：这段的注释原先写着
   > 与落地页那套 `.modal-bg/.modal` 并存：……**两套的类名不重叠，所以不冲突**。

   **那句话是错的** —— 两套**都定义了 `.modal`**（另一个在「弹层」段），
   所以后出现的这一份把那一份整个覆盖了。改成"并存且不冲突"的描述
   会让下一个人不去查，而真相是：那一套**从未生效过**，且 `.modal-bg` 零引用。
   ⇒ 那两处已删（见「弹层」段的说明）。**注释里的错误断言比没有注释更坏。** */
.modal {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center; padding: var(--sp-5);
  background: rgba(18, 32, 58, .34); backdrop-filter: blur(4px);
  animation: fade .18s var(--ease);
}
@keyframes fade { from { opacity: 0; } }
/* ★★ 2026-10-12（用户反馈）：改前是 `width: min(430px, 92vw)` +
   `max-height: 84vh; overflow-y: auto` —— **整张卡一起滚**，
   于是标题与"取消/保存"跟着内容滚走，而 430px 装不下并排的两个字段。
   ⇒ 改成三段式：卡片自己**不滚**，只有中间的 `#edit-modal-body` 滚；
     宽度按"内容形态"给（见下面的 `:has(.field-pair)`）。 */
.modal-card {
  width: min(430px, 92vw); max-height: min(86vh, 760px);
  background: var(--panel); border: 1px solid var(--line-2);
  border-radius: var(--r-l); box-shadow: var(--sh-3); padding: var(--sp-5);
  display: flex; flex-direction: column; gap: var(--sp-3);
  overflow: hidden;                      /* ★ 卡片不滚，交给 #edit-modal-body */
  animation: rise .3s var(--ease);
}
/* ★★ 有**并排字段**的弹窗 = 宽表单（编辑账号/编辑站点都是），自动加宽。
   用 `body:has(> aside.side)` 判"这一页是不是后台"是同一手法：
   **形态由结构决定**，不需要在每个弹窗上记得加一个 class（加了忘了 = 静默地窄）。
   `:has()` 需要 Chrome 105+，而本文件已在用 `color-mix()`（111+）⇒ 不新增兼容约束。 */
.modal-card:has(.field-pair) { width: min(660px, 94vw); }

/* 弹窗的滚动区：**只有它滚**，并隐藏滚动条外观
   （窄弹窗里那条灰滚动条本身就很扎眼，而那正是用户截图里框出来的东西）。
   ★★ 选择器是 `.modal-body`（新加的那一层），**不是** `.modal-card` ——
     改前 `#edit-modal-body` 与卡片是同一个元素，那条规则等于"让整卡滚"。 */
.modal-body {
  overflow-y: auto; overscroll-behavior: contain;
  scrollbar-width: none;
  /* 负外边距 + 等量内边距：让滚动区的可滚范围贴到卡片边缘，
     但内容仍与卡片内边距对齐（否则内容会顶到边框上）。 */
  margin: 0 calc(var(--sp-5) * -1); padding: 0 var(--sp-5);
}
.modal-body::-webkit-scrollbar { display: none; }
/* ★ 2026-10-12：弹窗标题加一条细分隔 —— 头部不再跟着内容滚之后，
   它需要一点"这是头部"的视觉交代。 */
.modal-card h3 {
  margin: 0; padding-bottom: var(--sp-3); border-bottom: 1px solid var(--line);
  font-family: var(--font-display); font-size: var(--fs-18); font-weight: 700;
}
.box-options { display: flex; flex-direction: column; gap: var(--sp-2); max-height: 260px; overflow-y: auto; }
.box-option {
  display: flex; align-items: center; justify-content: space-between;
  text-align: left; padding: var(--sp-3) var(--sp-4); border-radius: var(--r-m);
  border: 1.5px solid var(--line); background: var(--panel-2);
  font: inherit; font-size: var(--fs-15); font-weight: 600; color: var(--text); cursor: pointer;
  transition: border-color .14s var(--ease), background .14s var(--ease), color .14s var(--ease);
}
.box-option:hover { border-color: var(--brand); }
.box-option.active { border-color: var(--brand); background: var(--brand-dip); color: var(--brand); }
.box-option .n {
  font-size: var(--fs-11); background: rgba(20, 40, 72, .08); border-radius: 999px;
  padding: 1px var(--sp-2); font-weight: 700; color: var(--text-2); font-variant-numeric: tabular-nums;
}
.box-option.active .n { background: color-mix(in srgb, var(--brand) 15%, transparent); color: var(--brand); }
.modal-actions {
  display: flex; justify-content: flex-end; gap: var(--sp-2);
  /* ★ 2026-10-12：操作区**不进滚动区**（它在 `#edit-modal-body` 里，但用 sticky
     把它钉在可视底部）—— 这样"取消/保存"永远够得着。 */
  position: sticky; bottom: 0;
  padding-top: var(--sp-3); margin-top: var(--sp-1);
  background: linear-gradient(to top, var(--panel) 72%, transparent);
}
.modal-card input {
  padding: var(--sp-2) var(--sp-3); border: 1px solid var(--line-2); border-radius: var(--r-m);
  background: var(--panel); color: var(--text); font: inherit; font-size: var(--fs-15); width: 100%;
}

/* ============================================================ 设备授权页
   两个纯 CSS 的图形，刻意不用图标字体/图片：
   它们只在"正在授权"和"授权成功"两个瞬间出现，为此多一个网络请求不值。 */
.spin {
  width: 34px; height: 34px; margin: 0 auto; border-radius: 50%;
  border: 3px solid var(--line-2); border-top-color: var(--brand);
  animation: spin .9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spin { animation-duration: 2.4s; } }

/* 授权成功标记。原先是文字符号 `✓`（font-size 26px 撑着），
   2026-10 换成内联 SVG ⇒ 尺寸交给 svg 自己，并把 stroke 对齐到圆的视觉中心。 */
.tick {
  width: 52px; height: 52px; margin: 0 auto; border-radius: 50%;
  display: grid; place-items: center;
  color: #fff; background: linear-gradient(180deg, #34d399, var(--green));
  box-shadow: 0 6px 18px rgba(15, 138, 82, .32), inset 0 1px 0 rgba(255,255,255,.3);
  animation: pop .34s cubic-bezier(.22, 1, .36, 1);
}
.tick svg { display: block; }
@keyframes pop { from { transform: scale(.7); opacity: 0; } to { transform: none; opacity: 1; } }

/* ============================================================ 顶栏「当前身份」小胶囊
   头像 + 用户名，点进个人信息页。**头像设过才显示圆形图**，没设就是首字母色块 ——
   空着一个灰色圆圈比显示首字母更像"加载失败"。 */

/* ============================================================ 头像（`.avatar` 基类）
   ★ 2026-10-07 修：这个基类**原先根本不存在**。

   在它出现之前，`.avatar` 只被两条**后代选择器**碰过：
   `.ac-avatar`（控制台账号卡，是个完全不同的类名）与 `.me-chip .avatar`
   （只给了 26×26 的尺寸）。于是个体信息页上那三个 `<span class="avatar">`
   **一条样式都没匹配到** —— 具体表现是：

     · 首字母**不居中**（没有 `display: grid; place-items: center`），
       全靠 `profile.html` 里一串行内 `style` 撑着；
     · 有头像时 `<img>` 的 `width:100%` 相对一个**由内容决定宽度**的 span，
       于是图片把卡片**撑破**；
     · 圆角/描边由行内样式各写一份，三处（大图、预览、胶囊）**互不一致**。

   这不是审美问题，是"样式表里少了一个类"。基类补上后，尺寸交给 `av-*`
   修饰类，行内样式全部撤掉。 */
.avatar {
  display: grid; place-items: center; flex: none;
  overflow: hidden; line-height: 1; user-select: none;
  width: 36px; height: 36px; border-radius: 50%;
  font-family: var(--font-display); font-weight: 700; font-size: 15px;
  color: var(--brand-ink);
  background: linear-gradient(160deg,
              color-mix(in srgb, var(--brand-ink) 13%, #fff),
              color-mix(in srgb, var(--brand-ink) 5%, #fff));
  border: 1.5px solid color-mix(in srgb, var(--brand-ink) 28%, #fff);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .8);
}
/* 有头像时：图片填满，并把首字母的文字色清掉（否则它会从图片底下透出来） */
.avatar.has-img { color: transparent; border-color: var(--line-2); background: var(--panel-2); }
.avatar > img { width: 100%; height: 100%; object-fit: cover; display: block; }

.avatar.av-sm { width: 30px; height: 30px; font-size: var(--fs-15); border-radius: var(--r-s); }
.avatar.av-xl {
  width: 104px; height: 104px; font-size: var(--fs-54); border-radius: 30px;
  border-width: 2px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .8), var(--sh-2);
}

/* ============================================================ 个人信息页
   2026-10-07 重做。原先这一页是**四个等重的小卡片 + 一堆行内样式**：
   头像一张、表单一张、预览一张，彼此没有主次，页面上没有一处告诉你
   "你是谁、你有多少东西"。重做后的结构是**一张身份卡 + 两张功能卡**：

     ① `.prof-hero` —— 头像 / 身份 / 库内存量三段式。它回答"我是谁"，
        且把「库内存量」从一个数字变成三个刻度的仪表盘（本设计语言的签名元素）；
     ② 编辑表单 —— 加了 dirty 跟踪与「撤销改动」，因为"改了没保存就离开"
        原先没有任何提示；
     ③ 客户端预览 —— 保留原来的用意，但改成**两个真实的客户端形态**
        （控制台胶囊 / 扩展行），而不是一个孤零零的假卡片。

   骨架仍然复用 `.card` / `.block` / `.form` / `.field` / `.rise`，不引入新体系。 */
.prof-hero { padding: var(--sp-6); }
.prof-hero-grid {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--sp-7); align-items: center;
}
@media (max-width: 860px) { .prof-hero-grid { grid-template-columns: auto 1fr; gap: var(--sp-5); } }
@media (max-width: 560px) {
  .prof-hero-grid { grid-template-columns: 1fr; justify-items: center; text-align: center; }
}

.prof-avatar-col { display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); }
.avatar-btn {
  padding: 0; border: none; background: none; line-height: 0;
  border-radius: 30px; cursor: pointer;
  transition: transform .16s var(--ease), box-shadow .16s var(--ease);
}
.avatar-btn:hover { transform: translateY(-2px); }
.avatar-btn:focus-visible { outline: none; box-shadow: 0 0 0 4px var(--brand-dip); }
.avatar-btn:hover .avatar.av-xl { border-color: var(--brand); }
.prof-avatar-note { letter-spacing: .1em; }

.prof-id { min-width: 0; }
.prof-name {
  margin: 0; font-family: var(--font-display);
  font-size: clamp(26px, 3.4vw, 34px); font-weight: 700; letter-spacing: -.02em;
  line-height: 1.15; overflow-wrap: anywhere;
}
.prof-mail {
  margin: 7px 0 0; font-size: var(--fs-15); color: var(--text-2);
  letter-spacing: 0; text-transform: none; overflow-wrap: anywhere;
}
.prof-facts {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-7); margin: 20px 0 0;
  padding-top: var(--sp-4); border-top: 1px solid var(--line);
}
.fact { display: flex; flex-direction: column; gap: var(--sp-1); }
.fact dt { margin: 0; }
.fact dd { margin: 0; font-size: var(--fs-15); color: var(--text); }

/* 库内存量：三个数字并排，直接复用控制台顶栏 `.stat` 的语汇 */
.prof-figures {
  display: grid; grid-template-columns: repeat(3, auto); gap: var(--sp-1) var(--sp-5);
  align-content: center; padding: var(--sp-4) var(--sp-6);
  /* ★ P2：它在 `.card.prof-hero` **里面** ⇒ 去掉边框，只留底色。 */
  border: 1px solid transparent; border-radius: var(--r-l); background: var(--panel-2);
}
.prof-figures-head { grid-column: 1 / -1; margin-bottom: 6px; }
.prof-figures-foot {
  grid-column: 1 / -1; margin: 8px 0 0; font-size: var(--fs-12); color: var(--text-3);
}
@media (max-width: 560px) { .prof-figures { justify-items: center; } }

.figure { margin: 0; display: flex; flex-direction: column; align-items: center; gap: 1px; }
.figure-val {
  font-family: var(--font-display); font-size: var(--fs-32); font-weight: 700;
  font-variant-numeric: tabular-nums; color: var(--text); line-height: 1;
}
.figure figcaption { font-size: var(--fs-11); letter-spacing: .13em; }

/* ---- 表单细节 ---- */
.input-locked { position: relative; display: flex; }
/* ★ `--lock-tag-w` 是**尺寸**不是留白：它为绝对定位的 `.lock-tag` 预留横向空间。
   写成裸值时，改一次锁标记的文字（"已锁定"→"LOCKED"）就要回来找这个数字，
   而**找不到的表现是文字被输入框压住** —— 不会报错，只是叠在一起。
   ⇒ 提到令牌里，与 `.lock-tag` 的定位共用同一个数。 */
.input-locked input { padding-right: var(--lock-tag-w); color: var(--text-2); }
.input-locked input:disabled { background: var(--panel-2); cursor: not-allowed; }
.lock-tag {
  position: absolute; right: var(--sp-3); top: 50%; transform: translateY(-50%);
  font-size: var(--fs-11); letter-spacing: .12em; color: var(--text-3);
  pointer-events: none;
}
.field-hint { color: var(--text-3); font-size: var(--fs-13); line-height: 1.5; }

.prof-actions {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  margin-top: 4px; padding-top: var(--sp-4); border-top: 1px solid var(--line);
}
/* 「未保存」/「已保存」标记。
   ★ 2026-10：符号从文本（● / ✓）改为 CSS 绘制 ——
     `✓`(U+2713) 在 emoji 候选区，不同字体/平台上会被渲染成彩色 emoji 或不同字重；
     而 `●` 是文字符号。两者都不该出现在"统一用图标"的界面里。
     现在文本只剩纯文字，符号用 ::before 画：圆点表示未保存，对勾表示已保存。 */
.prof-dirty {
  color: var(--amber-ink); letter-spacing: .1em;
  display: inline-flex; align-items: center; gap: 5px;
}
.prof-dirty:empty::before { content: none; }
.prof-dirty::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; flex: none;
}
/* 已保存：把圆点换成对勾（用两条边画，不引字体也不引图片） */
.prof-dirty.saved::before {
  width: 9px; height: 5px; border-radius: 0;
  background: none; border-left: 1.6px solid currentColor;
  border-bottom: 1.6px solid currentColor;
  transform: rotate(-45deg) translateY(-1px);
}
.prof-dirty.saved { color: var(--green-ink); }

.prof-avatar-actions {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding-top: var(--sp-4); border-top: 1px solid var(--line);
}
.prof-avatar-actions .desc { flex: 1 1 200px; }

/* ---- 客户端预览：两个真实形态 ---- */
.client-row { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap; }
.client-label { width: 76px; flex: none; letter-spacing: .12em; }
.client-line {
  flex: 1 1 220px; min-width: 0;
  display: flex; align-items: center; gap: var(--sp-3);
  /* ★ P2：它在卡片里 ⇒ 去掉边框（框套框）。
     ★ 它还有 `flex: 1 1 220px` —— 在窄卡片里会**换行堆叠**，
       所以去掉边框之后它读起来是"一组行"而不是"一叠盒子"。 */
  padding: var(--sp-3) var(--sp-4); border: 1px solid transparent;
  border-radius: var(--r-m); background: var(--panel-2);
}
.client-line .alias { font-weight: 600; font-size: var(--fs-15); }
.client-line .sub {
  font-size: var(--fs-12); color: var(--text-2); margin-top: 1px;
  font-family: var(--font-mono);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}


/* ================================================================ 钉钉扫码登录
   （2026-10-12）

   ★★ 只用**已有令牌**：本仓有两条判据盯着样式表 ——
     `test_type_scale.py`（每一处 font-size 要么用令牌、要么是正文基准字面量）
     与 `test_contrast.py`（文字色在**真实底色**上 ≥4.5:1）。
     新写死一个 `13.5px` 或一个自定义灰，就会当场红 —— 这是刻意的。
   ★ 无外链资源（本服务可能部署在内网）：钉钉图标是**内联 SVG**，不是图标字体/CDN。 */

/* ---- 登录方式的**唯一切换入口**（2026-10-12，用户意见 ①②③）----
   ## 为什么做成"一个按钮、按模式换文案"

   用户上一版看到的是**两个并列的入口**（"用钉钉扫码登录" + "用账号密码登录"），
   原话"太杂乱，毫无逻辑性"。而且并列本身也没道理：
   当前正在用的那种方式，不该再给一个"切到它"的按钮（点了什么也不会发生）。
   ⇒ 现在**任何时刻只显示一个**，而且是"切到另一种方式"那一个。
   ★ 显隐由 `#login-card[data-mode]` 驱动（**唯一事实源**），
     而不是 JS 逐个 toggle 按钮 —— 两处都能改就会漂。 */
.auth-switch {
  display: flex; align-items: center; justify-content: center;
  margin-top: var(--sp-4);
}
[data-mode="password"] .pw-only,
[data-mode="dingtalk"] .dt-only { display: none; }
/* ★★ 用户意见 ③：**注册页上不许出现这个切换入口**。
   ★ `~` 相邻兄弟选择器：`#auth-switch` 紧跟在 `.tabs` 之后（同一个 card 内）。
   ★ 用显式规则而不是只靠元素上的 `hidden` 属性 —— `.auth-switch` 带 `display:flex`，
     `[hidden]{display:none}` 会被类选择器盖掉（本仓踩过的形态）。
   ★ `data-tab` 挂在 `.tabs` **容器**上：挂在按钮上的话，按钮不是 `#auth-switch`
     的兄弟、也没有能选中它的祖先。 */
.tabs[data-tab="register"] ~ #auth-switch { display: none !important; }

/* 看起来是超链接、实际是 `<button>`（可聚焦、可键盘操作）。
   ★★ 用户意见 ②：**必须一眼看出这是能点的**。上一版它是普通文字，
      用户当链接去点、点不动 ⇒ 报"用钉钉扫码登录无法点击"；
      再上一版我改成下划线但用弱化的灰蓝色，用户仍说"让用户知晓这是个按钮"。
      ⇒ 现在用**界面主色 + 下划线 + 加粗**，hover 时加粗下划线。
   ★ 字号用 `--fs-13`：刻度里**没有 14**（11/12/13/15/18/32/54），
     而 `test_type_scale.py` 会抓刻度外的令牌（我第一版写了 `--fs-14`，
     当场被它抓住）。"能点的样子"靠**颜色与下划线**达成，不靠加大字号。 */
.link-btn {
  padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: var(--fs-13); font-weight: 600;
  color: var(--brand); text-decoration: underline; text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}
.link-btn:hover { color: var(--brand-ink); text-decoration-thickness: 2px; }
.link-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; border-radius: var(--r-s); }
/* ★ `.dt-retry-tip` 已删（2026-10-12 返工）：那句"二维码没出来？"是多余的 ——
   失败提示自己已经说清了原因，「重新加载」直接挂在它后面（见 `showQrMsg`）。 */

/* 钉钉块的显隐。★ 用它而不是 `hidden` 属性：这几个容器是 `<span>`/`<div>` 混用，
   而 `hidden` 会被 `display:flex` 之类的规则**盖掉**（`[hidden]{display:none}` 的
   优先级低于类选择器）—— 那种"以为藏起来了其实还在"是本仓踩过的形态。 */
.dt-hide { display: none !important; }
/* ★ `.dt-box` 与 `.dt-or` 已删（2026-10-12）：改成**互斥切换**之后，钉钉那块不再
   "挂在表单下面、用一条『或』分隔"，而是**整张卡片换一屏** ⇒ 那个分隔线布局没有了。
   留着它们会让人以为还有两栏并存（`test_no_dead_css_classes` 会抓死类名）。 */
/* 二维码区（2026-10-12 改内嵌方案；同日加框与失败出口）。
   ★★ 为什么必须有**可见的边框与底色**：
     实测（用户截图）—— 钉钉拒绝画二维码时，这个容器就是**一片空白**，
     而它旁边那行标签是附近唯一像能点的东西 ⇒ 用户去点它、然后报"点不动"。
     加框之后它读起来是"一个待填的格子"，而不是"这里什么都没有"。
   ★ 高度固定 288：钉钉的 iframe 是 280×280，留 8px 让边框不顶到内容。 */
.dt-qr-wrap {
  position: relative;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: var(--sp-2);
  min-height: 300px; margin: var(--sp-3) 0 0; padding: var(--sp-3);
  border: 1px dashed var(--line-2); border-radius: var(--r-m);
  background: var(--panel-2);
}
.dt-qr {
  display: flex; align-items: center; justify-content: center;
  min-height: 280px; min-width: 280px;
}
.dt-qr iframe { display: block; border: 0; }
/* 二维码区里的说明 / 失败原因。★ 它**必须比 .dt-hint 更显眼** ——
   钉钉回绝时这句话是用户唯一的线索（原先用浅灰小字，等于没写）。 */
.dt-qr-msg {
  max-width: 300px; text-align: center; line-height: 1.7;
  font-size: var(--fs-12); color: var(--text-2);
}
/* 失败时的强调态（由 JS 加 .bad）。 */
.dt-qr-msg.bad { color: var(--red-ink); font-weight: 600; }
.dt-retry { margin-top: var(--sp-1); }
/* ★ `.dt-label` 已删（2026-10-12，用户意见 ①）：用户要求去掉「请用手机钉钉扫码」
   那行标题 —— 二维码自己就说明了一切，多一行字只是噪音。
   ★ `.dt-hint` 也已删：它被 `.dt-qr-msg` 取代 ——
   原来那行是**浅灰小字**，而它承载的是"钉钉为什么没给二维码"这类
   **需要用户处理**的信息，用弱化的样式等于没写。`test_no_dead_css_classes` 会抓死类名。 */
/* 首次使用的提示条：说明"为什么要先注册"（不说清楚会被当成报错） */
.dt-note {
  padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-4);
  border-left: 3px solid var(--brand); border-radius: var(--r-s);
  background: var(--brand-dip);
  font-size: var(--fs-13); color: var(--text-2); line-height: 1.65;
}
.dt-note b { color: var(--text); }

/* ---- 个人信息页：绑定卡片 ---- */
.dt-row {
  display: flex; align-items: center; gap: var(--sp-3);
  flex-wrap: wrap;
}
.dt-state {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-13); font-weight: 600;
}
/* ★ 用**已有的** `--green`（语义色令牌就在 `:root` 里）。
   第一版我写的是 `var(--ok, #137a3b)` —— 而 `--ok` **并不存在**。
   带 fallback 时浏览器会静默用 fallback，所以"看起来是对的"；
   但那意味着这个颜色**不受主题与对比度判据管辖**
   （`test_contrast.py` 只认令牌）。⇒ 一律用真令牌，不靠 fallback 兜。 */
.dt-state.on { color: var(--green-ink); }
.dt-state.off { color: var(--text-3); }
.dt-id {
  font-family: var(--font-mono); font-size: var(--fs-12); color: var(--text-2);
  word-break: break-all;
}

/* ============================================================ 下载页 */
/* 公开页的下载区：两件正式构建（桌面安装包 / 浏览器扩展 ZIP）。
   这里只负责排版；版本与产物都以 GitHub Releases 列表页为准（清单里只写 `releasesUrl`），
   清单若显式给出 `version` / `size` / `sha256`，则照旧逐个展示。 */
.nav-links { display: flex; align-items: center; gap: var(--sp-3); }
.dl-hero { padding: var(--sp-12) 0 var(--sp-7); max-width: 820px; }
.dl-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr));
  gap: var(--sp-5);
  margin-bottom: var(--sp-4);
}
.dl-grid > .panel { height: 100%; }
.dl-card { overflow: hidden; display: flex; flex-direction: column; }
.dl-card .panel-body { display: flex; flex-direction: column; gap: var(--sp-4); height: 100%; }
.dl-head { display: flex; align-items: flex-start; gap: var(--sp-3); }
.dl-mark {
  width: 46px; height: 46px; flex: none;
  display: grid; place-items: center;
  border-radius: var(--r-m);
  background: linear-gradient(145deg, var(--brand), color-mix(in srgb, var(--brand) 62%, var(--green)));
  color: #fff; font-family: var(--font-display); font-size: var(--fs-18); font-weight: 600;
  box-shadow: 0 7px 18px color-mix(in srgb, var(--brand) 22%, transparent);
}
.dl-head-copy { min-width: 0; flex: 1; }
.dl-name { font-family: var(--font-display); font-size: var(--fs-18); font-weight: 600; line-height: 1.25; }
.dl-kind { color: var(--text-3); font-size: var(--fs-12); margin-top: 4px; }
.dl-stable { flex: none; }
.dl-desc { color: var(--text-2); font-size: var(--fs-15); line-height: 1.7; margin: 0; }
.dl-facts {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3);
  margin: 0; padding: var(--sp-4) 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
}
.dl-fact { margin: 0; min-width: 0; }
.dl-fact dt { margin: 0 0 3px; color: var(--text-3); font-size: var(--fs-11); letter-spacing: .04em; text-transform: uppercase; }
.dl-fact dd { margin: 0; color: var(--text); font-family: var(--font-mono); font-size: var(--fs-13); overflow-wrap: anywhere; }
.dl-actions { margin-top: auto; display: flex; flex-direction: column; gap: var(--sp-3); }
.dl-actions .btn, .dl-actions .btn-primary { width: 100%; }
.dl-hash { margin: 0; color: var(--text-3); font-family: var(--font-mono); font-size: var(--fs-11); line-height: 1.55; overflow-wrap: anywhere; }
.dl-hash b { color: var(--text-2); font-weight: 500; }
.dl-install { margin: 0; color: var(--text-3); font-size: var(--fs-12); line-height: 1.6; }
.dl-status { min-height: 22px; color: var(--text-3); font-size: var(--fs-12); margin: var(--sp-2) 0 var(--sp-6); }
.dl-status.error { color: var(--red-ink); }
.dl-loading { grid-column: 1 / -1; }
.dl-help { margin-top: var(--sp-6); }
.dl-help h2 { font-size: var(--fs-18); margin: 0 0 var(--sp-4); }
.dl-help h3 { font-family: var(--font-display); font-size: var(--fs-15); margin: 0 0 var(--sp-2); }
.dl-help p { color: var(--text-2); font-size: var(--fs-13); line-height: 1.7; margin: 0; }
.dl-help-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); gap: var(--sp-5); }
.dl-note { margin-top: var(--sp-5) !important; padding-top: var(--sp-4); border-top: 1px solid var(--line); color: var(--text-3) !important; }
@media (max-width: 640px) {
  .dl-hero { padding-top: var(--sp-8); }
  .dl-facts { grid-template-columns: 1fr; }
}
.site-option-sub {
  display: block;
  margin-top: 2px;
  color: var(--text-3);
  font-size: var(--fs-12);
  font-weight: 400;
  overflow-wrap: anywhere;
}


/* 个人页：客户端登录状态 */
.client-status-list { display: flex; flex-direction: column; gap: var(--sp-3); }
.client-status-list .client-row { align-items: stretch; }
.client-status-list .client-line { align-items: center; }
