/* ============================================================================
 * 爱车监控系统 · 统一设计令牌与组件层
 *
 * 为什么需要它：全站目前有 5184 处 gray-* 手工类 + 1830 处语义色手工类，
 * 每个都要手写一份 dark: 孪生。实测 1697 个带颜色的 class 属性里有 170 个
 * （10.0%）完全没有 dark: 变体，深色模式下会露白。
 *
 * 本文件的做法：把颜色收敛成语义 CSS 变量，深浅色各一套。
 * 组件只写一句 ui-card / ui-badge-ok，**不再写 dark: 变体**。
 *
 * ⚠ 第 1 节的**颜色令牌不是手写的**：下面的值是 /js/v2/palette.js 从**一个主色**
 *   派生出来的结果。要改规则就改 palette.js，然后重新生成这一段：
 *     node -e "console.log(require('./src/main/resources/static/js/v2/palette.js').css())"
 *   两处必须一致 —— palette.js 是唯一实现，别在这里手改单个值。
 *
 * 用法：
 *   <link rel="stylesheet" href="/css/app-ui.css?v=#{time}">
 *   Tailwind 侧把颜色映射到这些变量（见 /js/v2/design-tokens.js），
 *   这样 bg-surface / text-ink-3 这类工具类也能用，且同样免写 dark:。
 * ========================================================================= */

/* ---------------------------------------------------------------------------
 * 1. 颜色令牌（由 palette.js 从默认主色 #2563eb 派生）
 *
 * 三条规则（推导过程与出处见 palette.js 文件头）：
 *
 *   ① **每个颜色有三副面孔**，缺一个就会写出 bug：
 *        --x       当文字/线压在表面上的颜色
 *        --x-soft  当底的着色块
 *        --x-ink   压在**填充色 x 上面**的前景色（按钮/胶囊上那行白字就是它）
 *      第三条是补上的：深色主题为了「在深底上当文字」把语义色提到 L .71~.84，
 *      这些值**当背景**配白字只有 1.67~2.77:1（AA 要 4.5）—— 全站 22 处
 *      bg-*-600/text-white 全中。根因不是颜色挑错，是少了这个角色。
 *
 *   ② 明度是**解出来的**，不是挑出来的：--x 要同时满足「压在 --surface 上」和
 *      「压在 --x-soft 上」对比度 ≥ 4.5。所以不会再出现徽章文字 4.06 这种差一口气
 *      的值，也不会出现「换个主色就崩」。
 *
 *   ③ **中性色的色相跟着主色走**，彩度按主色饱和度缩放：主色是默认蓝时，
 *      算出来与改造前的 Tailwind gray 逐令牌只差 1~2/255（观感不变）；
 *      换成暖主色，整套灰会自动转暖，不会留着冷灰和品牌色打架。
 *
 *   语义色（success/warning/danger）的色相**写死不让位**：主色撞上它们时
 *   palette.js 只给提示，不偷偷改语义（红色主色下把「异常」挪走，比撞色更糟）。
 * ------------------------------------------------------------------------- */
:root {
    --surface-sunken: 249 250 251;
    --surface: 255 255 255;
    --surface-raised: 243 244 246;
    --rule: 229 231 235;
    --rule-raised: 210 213 219;
    --rule-strong: 210 213 219;
    --ink: 16 24 39;
    --ink-2: 56 65 81;
    --ink-3: 107 114 128;
    --ink-4: 156 163 175;
    --rule-grid: 194 197 200;
    --accent: 35 97 232;
    --accent-soft: 226 237 255;
    --accent-hover: 16 77 212;
    --accent-ink: 255 255 255;
    --cat-1: 35 97 232;
    --cat-1-soft: 226 237 255;
    --success: 13 124 58;
    --success-soft: 223 243 226;
    --success-ink: 255 255 255;
    --success-hover: 0 105 45;
    --warning: 174 78 0;
    --warning-soft: 255 232 220;
    --warning-ink: 255 255 255;
    --warning-hover: 149 65 0;
    --danger: 211 24 29;
    --danger-soft: 255 230 226;
    --danger-ink: 255 255 255;
    --danger-hover: 185 0 15;
    --cat-2: 13 124 58;
    --cat-2-soft: 223 243 226;
    --cat-3: 174 78 0;
    --cat-3-soft: 255 232 220;
    --cat-4: 147 52 234;
    --cat-4-soft: 240 232 254;
    --cat-5: 191 62 6;
    --cat-5-soft: 255 231 223;
    --cat-6: 79 70 229;
    --cat-6-soft: 231 236 255;
    --holiday: 211 24 29;
    --makeup: 174 78 0;
}
:root[data-theme="dark"] {
    --surface-sunken: 16 24 39;
    --surface: 33 40 55;
    --surface-raised: 56 65 81;
    --rule: 56 65 81;
    --rule-raised: 77 84 99;
    --rule-strong: 77 84 99;
    --ink: 243 244 246;
    --ink-2: 210 213 219;
    --ink-3: 156 163 175;
    --ink-4: 107 114 128;
    --rule-grid: 77 87 104;
    --accent: 123 168 255;
    --accent-soft: 48 62 90;
    --accent-hover: 168 198 255;
    --accent-ink: 17 24 39;
    --cat-1: 123 168 255;
    --cat-1-soft: 48 62 90;
    --success: 74 222 128;
    --success-soft: 41 70 49;
    --success-ink: 17 24 39;
    --success-hover: 109 252 156;
    --warning: 251 191 37;
    --warning-soft: 75 60 28;
    --warning-ink: 17 24 39;
    --warning-hover: 255 228 172;
    --danger: 255 130 128;
    --danger-soft: 87 51 50;
    --danger-ink: 17 24 39;
    --danger-hover: 255 179 175;
    --cat-2: 74 222 128;
    --cat-2-soft: 41 70 49;
    --cat-3: 251 191 37;
    --cat-3-soft: 75 60 28;
    --cat-4: 198 142 255;
    --cat-4-soft: 69 55 84;
    --cat-5: 251 146 60;
    --cat-5-soft: 84 55 35;
    --cat-6: 150 162 255;
    --cat-6-soft: 55 60 90;
    --holiday: 255 130 128;
    --makeup: 251 191 37;
}

/* 深色令牌**只在这一处定义**，由 <html data-theme="dark"> 触发。
 *
 * 为什么不是 `@media (prefers-color-scheme: dark)`：那样只认操作系统，
 * 用户手动选「深色」就没有落点。现在「跟随系统」由 /js/v2/theme.js 在**首屏绘制前**
 * 把解析结果同步写进 data-theme，所以媒体查询要表达的那件事仍然成立，
 * 只是多了一条「用户显式选择」的入口，且两者走同一段样式 —— 不会再出现
 * 「Tailwind 的 dark: 变体翻了一种颜色、这里的令牌翻了另一种」这种半深色状态。
 * 本文件是纯静态资源，不引入构建步骤，故不用 @media 兜底：
 * 全站样式本来就依赖 JS（Tailwind 是 CDN 版运行时编译），无 JS 场景不是有效场景。 */

/* ---------------------------------------------------------------------------
 * 1.1 不参与派生的令牌
 *
 * --weekend / --weekend-soft 是「日期属性」里唯一保留暖灰的：
 *   它必须**永远**是暖的才能和普通灰区分开，所以故意不跟主色走 ——
 *   主色是暖色时普通灰会变暖，周末暖灰更要守住自己的色相去拉开距离。
 * --chart-* 给 ECharts 读（Theme.chartTheme），--z-* 是全站层次阶梯。
 * ------------------------------------------------------------------------- */
:root {
    --weekend:        120 113 108;  /* stone-500 暖灰 */
    --weekend-soft:   231 229 228;  /* 对白 1.25，与 danger-soft 1.19 同一档存在感 */

    --chart-axis:     156 163 175;  /* 轴文字 */
    --chart-split:    229 231 235;  /* 分割线 */

    /* 层次：全站统一的 z-index 阶梯，替代现在的 8 个散落值 */
    --z-base:     0;
    --z-sticky:   10;   /* 表头、卡内吸顶 */
    --z-nav:      40;   /* 顶部导航 */
    --z-overlay:  50;   /* 弹窗遮罩 */
    --z-toast:    60;   /* 提示 */
}
:root[data-theme="dark"] {
    --weekend:        168 162 158;
    --weekend-soft:   68 64 60;     /* 对 gray-800 1.55；直接拿 stone-800(41 37 36) 只有 1.05，
                                       深色下周末格会整片糊进卡片底 */

    --chart-axis:     107 114 128;
    --chart-split:    56 65 81;
}


/* ---------------------------------------------------------------------------
 * 2. 基础
 * ------------------------------------------------------------------------- */
body {
    background-color: rgb(var(--surface-sunken));
    color: rgb(var(--ink-2));
}

/* 全站统一的键盘焦点环。现在只有 5 个 select 有焦点样式。 */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
    outline: 2px solid rgb(var(--accent));
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* ---------------------------------------------------------------------------
 * 3. 布局
 * ------------------------------------------------------------------------- */
.ui-shell      { max-width: 80rem; margin-inline: auto; padding-inline: 1rem; }
@media (min-width: 640px)  { .ui-shell { padding-inline: 1.5rem; } }
@media (min-width: 1024px) { .ui-shell { padding-inline: 2rem; } }

.ui-stack      { display: flex; flex-direction: column; gap: 1.5rem; }  /* 区块之间统一 24px */

/* ---------------------------------------------------------------------------
 * 4. 容器：三级卡片体系
 *
 *   L1  .ui-card        一个 tab / 一个页面区块一张。圆角 8px，描边，内容面
 *        .ui-card-pad   24px 内边距
 *        .ui-card-head  16px/24px + 下分隔线（区块头）
 *        .ui-card-body  16px/24px，**不画线**（夹在卡头与卡脚之间的内容）
 *        .ui-card-foot  16px/24px + 上分隔线（分页、底部操作行）
 *   L2  .ui-inset       卡内分区（筛选栏、明细块、代码块）。圆角 6px，**自带 12px 内边距**
 *        .ui-inset-flush 少数确实要贴边的场景（表格容器）
 *   L3  .ui-tile        指标格。圆角 8px，描边，自带 16px 内边距
 *       .ui-ledger      台账网格（N 个并列事实）。细线分格，**不铺底**
 *
 * 四条硬规则：
 *   1. **不嵌套 .ui-card**。L1 里面要分区就用 L2/L3 —— 白卡套白卡同色不可见，
 *      纯属层次噪声（老代码里 etc-tab 有 10 处、oil-tab 8 处、vehicle-tab 8 处，还有三层嵌套）。
 *   2. **内边距由组件给，调用方不要再写 p-***。
 *      老代码里 ui-inset 上分别加过 p-3 / p-4 / p-6（12 处），同一个组件三种留白。
 *   3. 圆角只有 8px（L1/L3）与 6px（L2）两档，不再出现 rounded / rounded-md / rounded-xl 混用。
 *   4. **别在 .ui-inset 里放 .ui-tile** —— 都是 --surface-raised，灰底套灰底。
 *      要并列 N 个指标用 .ui-ledger。
 * ------------------------------------------------------------------------- */
.ui-card {
    background-color: rgb(var(--surface));
    border: 1px solid rgb(var(--rule));
    border-radius: 0.5rem;          /* 8px */
}
.ui-card-pad { padding: 1rem; }
@media (min-width: 640px) { .ui-card-pad { padding: 1.5rem; } }   /* 手机 16px / 桌面 24px */
/* 区块头：标题 + 可选操作，底色与卡片一致，只用下分隔线分段 */
.ui-card-head {
    padding: 0.75rem 1rem;          /* 手机 12/16px */
    border-bottom: 1px solid rgb(var(--rule));
}
@media (min-width: 640px) { .ui-card-head { padding: 1rem 1.5rem; } }   /* 桌面 16/24px */
/* 卡内容区：**只给内边距，不画分隔线**。
   和 .ui-card-section 的分工是「这段下面要不要一条线」：
   夹在 .ui-card-head 与 .ui-card-foot 之间的内容（列表、图表、日历）用本类 ——
   上下两边已经各有一条线了，再要一条就是紧挨着的双线（1px + 1px = 2px）。
   踩过：行程分析 → 行程记录把 .ui-rows 直接放进 .ui-card，行卡片（自己有边框）贴死在
   卡片边框上，左右上下 0 间距；而同一张卡的卡头/卡脚是 16/24px，一张卡三种边距。
   **凡是卡片内的列表/网格容器，都必须带上本类或 .ui-card-section。** */
.ui-card-body { padding: 1rem; }
@media (min-width: 640px) { .ui-card-body { padding: 1.5rem; } }
/* 外壳卡（tab 条 + 子 tab 内容）里的分段：内容区自己不再加内边距，由各分段自带 */
.ui-card-section { padding: 1rem; border-bottom: 1px solid rgb(var(--rule)); }
@media (min-width: 640px) { .ui-card-section { padding: 1.5rem; } }
.ui-card-section:last-child { border-bottom: 0; }
/* 外壳卡内的**着色带**：筛选栏、提示条。与 ui-inset 的区别是它边缘到边缘，
   所以用方角 + 下分隔线，不用圆角（圆角贴边看起来像没对齐） */
.ui-card-band {
    padding: 1rem;
    background-color: rgb(var(--surface-raised));
    border-bottom: 1px solid rgb(var(--rule-raised));
}
@media (min-width: 640px) { .ui-card-band { padding: 1.5rem; } }
.ui-card-band:last-child { border-bottom: 0; }
/* 卡片脚：分页、底部操作行。上分隔线 + 内边距（原来 4 处手写 p-6 border-t border-rule） */
.ui-card-foot { padding: 1rem; border-top: 1px solid rgb(var(--rule)); }
@media (min-width: 640px) { .ui-card-foot { padding: 1.5rem; } }

/* 可点/可聚焦卡片的悬停反馈。原来叫 card-hover，定义在 index.html 的内联 <style> 里
   —— 设计系统的东西不该只对某一个页面生效（其它独立页用不到）。 */
.ui-card-hover { transition: box-shadow .2s ease, transform .2s ease, border-color .2s ease; }
.ui-card-hover:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 16px rgb(0 0 0 / .10);
    border-color: rgb(var(--rule-strong));
}
@media (prefers-reduced-motion: reduce) { .ui-card-hover { transition: none; } .ui-card-hover:hover { transform: none; } }

/* 卡内分区 */
.ui-inset {
    background-color: rgb(var(--surface-raised));
    border-radius: 0.375rem;        /* 6px */
    padding: 0.75rem;               /* 12px，默认值；不要再在外面写 p-* */
}
.ui-inset-flush { padding: 0; }     /* 表格/列表容器这类需要贴边的 */

/* 图标底座：卡片标题左侧 / 指标卡左侧的那个小方块。
   固定尺寸，不随里面的图标字号变 —— 原来 25 处分别用 p-1.5/p-2/p-2.5 + rounded-md/lg/full，
   同一屏里几个方块大小不一。 */
.ui-icon-box {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 0.5rem;
    background-color: rgb(var(--surface-raised));
}
.ui-icon-box-sm { width: 2rem; height: 2rem; }
.ui-icon-box-lg { width: 2.75rem; height: 2.75rem; }

/* 底座着色：这张卡的图标要跟着卡片的语义/分类色走时用（`ui-icon-box ui-icon-box-warning`）。
   为什么是选择器而不是让调用方再挂一个 `bg-*-soft`：两者都是单类选择器，
   谁生效只看样式表顺序（Tailwind CDN 注入的 <style> 与 app-ui.css 的先后不由我们定），
   于是「有时有底色、有时没有」这种最难查的 bug。一个类说完，不赌顺序。
   踩过的：2026-09-21 基础数据卡片上 `bg-accent-soft` 这类底色**没有内边距**，
   图标多大底色就多大，而「车辆状态」那张用的是 ui-icon-box（36px 方块）——同一排两种长相。 */
.ui-icon-box-accent  { background-color: rgb(var(--accent-soft)); }
.ui-icon-box-success { background-color: rgb(var(--success-soft)); }
.ui-icon-box-warning { background-color: rgb(var(--warning-soft)); }
.ui-icon-box-danger  { background-color: rgb(var(--danger-soft)); }

/* 指标格。描边用 --rule-raised 而不是 --rule：这个组件既可能落在卡片面（--surface）上，
   也可能落在灰底块（ui-inset / ui-card-band）里，而深色下 --rule 与 --surface-raised 同值，
   用 --rule 的话只要进了灰底块边框就消失。--rule-raised 在两种底上都量得出来。 */
.ui-tile {
    background-color: rgb(var(--surface-raised));
    border: 1px solid rgb(var(--rule-raised));
    border-radius: 0.5rem;          /* 8px */
    padding: 1rem;                  /* 16px */
}

/* 台账网格：**一个块里放 N 个并列事实**的默认形态（本次油箱统计、明细汇总这类）。
   为什么不给格子铺灰底：灰底表达结构的前提是「容器底 ≠ 格子底」，
   而 L2 容器（ui-inset）用的就是 --surface-raised —— 灰底套灰底同色不可见，
   深色下连描边都同值，4 个格子糊成一整块（2026-09-21 用户报的就是这个）。
   细线分格不依赖「底色差多少」，深浅色下都成立，也更贴台账感。
   用法：<div class="ui-ledger ui-ledger-4"><div><p class="ui-ledger-label">…</p>
        <p class="ui-ledger-value">0.63<span class="ui-ledger-unit">元/km</span></p></div>…</div> */
.ui-ledger {
    display: grid;
    gap: 1px;                                       /* 缝里露出的是下面的 --rule，即 1px 细线 */
    background-color: rgb(var(--rule));
    border: 1px solid rgb(var(--rule));
    border-radius: 0.5rem;                          /* 8px */
    overflow: hidden;
}
.ui-ledger > * {
    background-color: rgb(var(--surface));          /* 格子跟随所在卡片面，不引第三种灰 */
    padding: 0.875rem 1rem;
    min-width: 0;
}
.ui-ledger-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* 手机上宁可少显示也不截断：3/4/5 栏在窄屏一律先落成两栏，
   「油价均成本 7.44 元/升」这种标签挤进 1/4 宽会折行。
   5 栏（数据诊断 · 登录日志的五个计数）多一档：640~1023px 先落成 3 栏 ——
   5 个格子挤在 768px 里每格不到 150px，标签会折行。 */
.ui-ledger-3,
.ui-ledger-4,
.ui-ledger-5 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 640px) {
    .ui-ledger-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .ui-ledger-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .ui-ledger-5 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
    .ui-ledger-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}
/* 5 格在窄屏是 2 栏（5 = 2+2+1）、640~1023px 是 3 栏（5 = 3+2），两种都除不尽：
   `.ui-ledger` 的「1px 缝」是**容器底色**透出来的，空出来的那格就成了一块没有内容的灰底。
   让最后一格跨掉剩下的列（2 栏时跨满一行，3 栏时补上第 3 列）。 */
.ui-ledger-5 > :last-child { grid-column: span 2; }
@media (min-width: 1024px) {
    .ui-ledger-5 > :last-child { grid-column: auto; }
}
.ui-ledger-label {
    font-size: 0.75rem;                             /* 12px */
    color: rgb(var(--ink-3));
    white-space: nowrap;
}
.ui-ledger-value {
    margin-top: 0.375rem;
    font-size: 1.25rem;                             /* 20px，比 ui-tile 的 18px 大一档：这里数字是主角 */
    font-weight: 700;
    line-height: 1.2;
    color: rgb(var(--ink));
    font-variant-numeric: tabular-nums;             /* 一列数字对齐 */
    letter-spacing: -0.01em;
}
.ui-ledger-unit {
    margin-left: 0.25rem;
    font-size: 0.75rem;
    font-weight: 400;
    color: rgb(var(--ink-3));
    letter-spacing: 0;
}

/* 带分类色的指标卡：图标底色 + 数值同色，一张卡一个分类色。
   这是「一眼能分清几张卡」的视觉手段，不是状态语义 —— 状态一律用 ui-badge-* 表达。 */
.ui-tile-cat { display: flex; flex-direction: column; gap: 0.5rem; border-radius: 0.5rem; padding: 1rem; border: 1px solid; }
.ui-tile-cat-1 { background-color: rgb(var(--cat-1-soft));   border-color: rgb(var(--cat-1) / .3); }
.ui-tile-cat-2 { background-color: rgb(var(--cat-2-soft));  border-color: rgb(var(--cat-2) / .3); }
.ui-tile-cat-3 { background-color: rgb(var(--cat-3-soft));  border-color: rgb(var(--cat-3) / .3); }
.ui-tile-cat-4 { background-color: rgb(var(--cat-4-soft)); border-color: rgb(var(--cat-4) / .3); }
.ui-tile-cat-5 { background-color: rgb(var(--cat-5-soft)); border-color: rgb(var(--cat-5) / .3); }
.ui-tile-cat-6 { background-color: rgb(var(--cat-6-soft)); border-color: rgb(var(--cat-6) / .3); }

/* 分类标签：口径/分类用，形状与 ui-badge 一致，只是取色来自分类色 */
.ui-badge-cat-1 { background-color: rgb(var(--cat-1-soft));   color: rgb(var(--cat-1)); }
.ui-badge-cat-2 { background-color: rgb(var(--cat-2-soft));  color: rgb(var(--cat-2)); }
.ui-badge-cat-3 { background-color: rgb(var(--cat-3-soft));  color: rgb(var(--cat-3)); }
.ui-badge-cat-4 { background-color: rgb(var(--cat-4-soft)); color: rgb(var(--cat-4)); }
.ui-badge-cat-5 { background-color: rgb(var(--cat-5-soft)); color: rgb(var(--cat-5)); }
.ui-badge-cat-6 { background-color: rgb(var(--cat-6-soft)); color: rgb(var(--cat-6)); }

/* 带语义色的指标卡：这一格的颜色**表示状态**（严重/中等/轻微/总计），不是分类 */
.ui-tile-danger { background-color: rgb(var(--danger-soft));  border-color: rgb(var(--danger) / .3); }
.ui-tile-warn   { background-color: rgb(var(--warning-soft)); border-color: rgb(var(--warning) / .3); }
.ui-tile-ok     { background-color: rgb(var(--success-soft)); border-color: rgb(var(--success) / .3); }
.ui-tile-info   { background-color: rgb(var(--accent-soft));  border-color: rgb(var(--accent) / .3); }

/* 提示条：全站只有一种形态。游客提示、降级提示、迁移告警都用它，靠 tone 区分严重度 */
.ui-alert {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 1rem;
    border-radius: 0.5rem;
    border: 1px solid rgb(var(--accent) / 0.3);
    background-color: rgb(var(--accent-soft));
    color: rgb(var(--accent));
    font-size: 0.875rem;
}
.ui-alert-title { font-weight: 500; }
.ui-alert-warn   { border-color: rgb(var(--warning) / 0.3); background-color: rgb(var(--warning-soft)); color: rgb(var(--warning)); }
.ui-alert-danger { border-color: rgb(var(--danger) / 0.3);  background-color: rgb(var(--danger-soft));  color: rgb(var(--danger)); }
.ui-alert-ok     { border-color: rgb(var(--success) / 0.3); background-color: rgb(var(--success-soft)); color: rgb(var(--success)); }

.ui-divide > * + * { border-top: 1px solid rgb(var(--rule)); }

/* 一列行的容器：行程列表这类「同构行」的间距由它拥有。
   踩过的坑：把间距留给调用方写 `space-y-*`，四处里必有一处忘了写，行就贴在一起。 */
.ui-rows { display: flex; flex-direction: column; gap: 0.75rem; }

/* ---------------------------------------------------------------------------
 * 5. 区块标题
 *    现在 16 种写法，font-medium 20 次 / font-semibold 16 次，mb-4 时有时无。
 * ------------------------------------------------------------------------- */
.ui-section-title {
    font-size: 1.125rem;      /* text-lg */
    line-height: 1.75rem;
    font-weight: 600;
    color: rgb(var(--ink));
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
.ui-subtitle {
    font-size: 0.875rem;
    font-weight: 500;
    color: rgb(var(--ink-2));
}

/* ---------------------------------------------------------------------------
 * 6. 导航
 *    下划线式 tab 的类串在全站被复制了 27 次，且已漂移出 3 个变体
 *    （etc 的 hover 色不同、diagnostic 缺 dark 变体）。
 * ------------------------------------------------------------------------- */
.ui-tabbar {
    display: flex;
    overflow-x: auto;
    /* 纵轴一律不滚。overflow-x 不是 visible 时，overflow-y 的计算值会被提升成 auto
       （规范里 visible 与其它值混用的规则），而 .ui-tab 那句 margin-bottom: -1px 让 tab
       的边框盒比容器内容盒高 1px —— 正好 1px 可滚动溢出，于是在经典滚动条的桌面浏览器上
       每条 tab 条右端都常驻一条竖滚动条（可滚范围只有 1px，滑块几乎填满整条轨道）。
       这里只关掉纵轴：横轴（窄屏下 tab 横滚）和「下划线压底线」的叠法都不动。
       二级 tab 条没用 .ui-tabbar（各自那串 border-b/overflow-x-auto 是历史遗留），
       所以它们身上也各自带了一个 overflow-y-hidden，改的是同一件事。 */
    overflow-y: hidden;
    border-bottom: 1px solid rgb(var(--rule));
}
.ui-tab {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 1rem 0.75rem;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    font-size: 0.875rem;
    font-weight: 500;
    white-space: nowrap;
    color: rgb(var(--ink-3));
    transition: color 150ms, border-color 150ms;
}
@media (min-width: 640px) { .ui-tab { padding-inline: 1.5rem; } }
.ui-tab:hover { color: rgb(var(--ink-2)); border-bottom-color: rgb(var(--rule-strong)); }
.ui-tab[aria-selected="true"] {
    color: rgb(var(--accent));
    border-bottom-color: rgb(var(--accent));
}

/* 分段按钮组（区间 90 天/180 天/1 年/全部、年份切换） */
.ui-segment {
    display: inline-flex;
    border: 1px solid rgb(var(--rule-strong));
    border-radius: 0.5rem;
    overflow: hidden;
}
.ui-segment > button {
    padding: 0.375rem 0.75rem;
    font-size: 0.8125rem;
    color: rgb(var(--ink-2));
    background-color: rgb(var(--surface));
}
.ui-segment > button + button { border-left: 1px solid rgb(var(--rule)); }
.ui-segment > button[aria-pressed="true"] {
    background-color: rgb(var(--accent));
    color: rgb(var(--accent-ink));
}

/* ---------------------------------------------------------------------------
 * 7. 表单
 *    输入框那一串类被复制了 26 次（且 placeholder 色时有时无）。
 * ------------------------------------------------------------------------- */
.ui-label {
    display: block;
    margin-bottom: 0.5rem;
    font-size: 0.875rem;
    font-weight: 500;
    color: rgb(var(--ink-2));
}
.ui-input,
.ui-select,
.ui-textarea {
    width: 100%;
    /* 必需：flex/grid 子项的 min-width 默认是 auto，`type=month/date` 的固有宽度
       会把所在列撑破并**盖到相邻字段上**（加油记录的「加油站」输入框就这么被压过）。 */
    min-width: 0;
    padding: 0.5rem 1rem;
    font-size: 0.875rem;
    color: rgb(var(--ink));
    background-color: rgb(var(--surface));
    border: 1px solid rgb(var(--rule-strong));
    border-radius: 0.5rem;
}
.ui-input::placeholder,
.ui-textarea::placeholder { color: rgb(var(--ink-4)); }
.ui-select option { background-color: rgb(var(--surface)); color: rgb(var(--ink)); }
.ui-input:disabled,
.ui-select:disabled,
.ui-textarea:disabled {
    background-color: rgb(var(--surface-raised));
    color: rgb(var(--ink-4));
    cursor: not-allowed;
}

/* 按钮：四种角色各一个类，替代现在「同角色不同样式、同样式不同角色」的混乱 */
.ui-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.5rem 1rem;
    font-size: 0.875rem;
    font-weight: 500;
    border-radius: 0.5rem;
    border: 1px solid transparent;
    transition: background-color 150ms, border-color 150ms, color 150ms;
    cursor: pointer;
}
.ui-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.ui-btn-primary   { background-color: rgb(var(--accent));  color: rgb(var(--accent-ink)); }
.ui-btn-primary:hover:not(:disabled)   { background-color: rgb(var(--accent-hover)); }
.ui-btn-secondary { background-color: rgb(var(--surface)); color: rgb(var(--ink-2)); border-color: rgb(var(--rule-strong)); }
.ui-btn-secondary:hover:not(:disabled) { background-color: rgb(var(--surface-raised)); }
.ui-btn-danger    { background-color: rgb(var(--danger));  color: rgb(var(--danger-ink)); }
.ui-btn-danger:hover:not(:disabled)    { background-color: rgb(var(--danger-hover)); }
.ui-btn-ghost     { background-color: transparent; color: rgb(var(--ink-3)); }
.ui-btn-ghost:hover:not(:disabled)     { background-color: rgb(var(--surface-raised)); color: rgb(var(--ink)); }

/* ---------------------------------------------------------------------------
 * 7.1 只读态（游客 / 临时 token 会话）
 *   唯一开关是容器上的 [data-readonly]（App.applyConfigReadonly 打在 #config-tab 上、
 *   App.hideAdminOnlyFeatures 打在 #etc-tab 上），
 *   **要收起的「写」按钮自己带 data-write** —— 别想用结构选择器去猜哪些 button 是写操作：
 *   子 tab 的按钮也是 button，按结构猜的结果是把导航一起藏了（踩过）。
 * ------------------------------------------------------------------------- */
[data-readonly] :is(input, select, textarea):disabled {
    background-color: rgb(var(--surface-raised));
    color: rgb(var(--ink-3));
    cursor: not-allowed;
}

[data-readonly] [data-write] {
    display: none;
}

/* 图标按钮（顶部工具条、表格行内操作） */
.ui-btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.5rem;
    border-radius: 0.5rem;
    color: rgb(var(--ink-2));
    cursor: pointer;
}
.ui-btn-icon:hover { background-color: rgb(var(--surface-raised)); color: rgb(var(--ink)); }

/* 复选框：现有 4 套并存（refuel-discount-day / w-5 h-5 accent-* / w-4 h-4 focus:ring-* / sr-only 开关），
   且多数没有焦点样式。统一为原生控件 + accent-color。 */
.ui-check {
    width: 1rem;
    height: 1rem;
    border-radius: 0.25rem;
    accent-color: rgb(var(--accent));
    cursor: pointer;
}
.ui-check-label {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.875rem;
    color: rgb(var(--ink-2));
    cursor: pointer;
}

/* ---------------------------------------------------------------------------
 * 8. 筛选栏
 *    现在 22 个实例 / 11 种布局族，padding 从 p-3 到 p-6 都有，gap 从 2 到 6 都有。
 * ------------------------------------------------------------------------- */
/* 筛选栏 = 贴着所在容器顶部的着色带。全站只有这一种形态：
   外壳卡里的直接贴边；没有外壳卡的套一张 ui-card，让带落在卡内。
   踩过的坑：同一种「筛选」曾有四种写法（卡中卡 / 独立卡 / 卡内着色块 / 贴边带），
   四个 tab 看起来像四个应用。 */
.ui-filter {
    padding: 1rem;
    background-color: rgb(var(--surface-raised));
    border-bottom: 1px solid rgb(var(--rule-raised));
}
@media (min-width: 640px) { .ui-filter { padding: 1.5rem; } }
/* 字段成栅格排布的筛选栏。**只有当字段是这个容器的直接子元素时才加** ——
   如果里面已经自带了 `<div class="grid grid-cols-4">`，再加 grid 会把那唯一一层压成 1/4 宽
   （ETC 四个筛选栏就这么被压变形过：标签挤成一行、按钮竖排成一列字）。
   下面这条护栏让误用退化成「不生效」而不是「排版崩掉」。 */
.ui-filter-grid:has(> :only-child) { display: block; }
.ui-filter-grid {
    display: grid;
    grid-template-columns: repeat(1, minmax(0, 1fr));
    gap: 1rem;
}
@media (min-width: 768px)  { .ui-filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .ui-filter-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
/* 操作行横跨整行，主操作在左、重置在右 —— 与现有 17 处显式按钮的既有习惯一致 */
.ui-filter-actions {
    grid-column: 1 / -1;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    padding-top: 0.5rem;
}
/* 字段少的筛选栏（行程趋势 / 热力图：2~3 个字段 + 两个按钮）让操作**留在同一行**，
   整条带就是一行，而不是「字段一行 + 按钮一行」占掉两行。
   两条必要的覆盖：`grid-column: auto` 不跨行，落在字段后面那一格；
   `align-items: flex-end` —— 栅格项默认拉伸到整行高，居中会让按钮比同排输入框高出一截，
   压到底边才和输入框对齐（两者高度本来就都是 38px，底边对齐 = 中线对齐）。
   字段总数（含跨度）要凑满 4 列：不够时给操作格加 `lg:col-start-4` 钉到最后一列，
   这样几个子 tab 的按钮落在同一个 x 上。
   （行程日历原来也在这一族里，现在它没有筛选带了：月份是导航不是筛选，
   见 travel-tab.html 段序注释与 .ui-month-nav。） */
.ui-filter-actions-inline {
    grid-column: auto;
    padding-top: 0;
    align-items: flex-end;
}

/* ---------------------------------------------------------------------------
 * 9. 表格
 *    现在 9 套表格配方，td padding 从 px-3 py-1 到 px-6 py-4，表头 4 种处理。
 * ------------------------------------------------------------------------- */
.ui-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.ui-table thead th {
    position: sticky;
    top: 0;
    z-index: var(--z-sticky);
    padding: 0.75rem 1rem;
    text-align: left;
    font-size: 0.75rem;
    font-weight: 500;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: rgb(var(--ink-3));
    background-color: rgb(var(--surface-raised));
    border-bottom: 1px solid rgb(var(--rule-raised));
    white-space: nowrap;
}
.ui-table tbody td {
    padding: 0.75rem 1rem;
    color: rgb(var(--ink));
    border-bottom: 1px solid rgb(var(--rule));
}
.ui-table tbody tr:hover { background-color: rgb(var(--surface-raised)); }
.ui-table .ui-num { text-align: right; font-variant-numeric: tabular-nums; }

/* 「不换行」表格：列内容一律不折行，宽度不够就让表格长出去、由外层 `overflow-x-auto`
   横滚看全。口径：单元格一律 nowrap，窄屏靠横向滚动，
   而不是把 cron、时间折成两三行」。ETC 账单的四个表用它（fragments/etc-tab.html）。
   两个配套动作，缺一个就退回折行：
     ① `<table>` 上**不要**再写 `table-fixed`。固定布局会把「比列宽宽」的内容压进列宽里折行
        （改造前 ETC 发票的时间列就是这么折成「日期 / 时刻」两行的）；auto 布局下列宽随内容长。
        因此 th 上的宽度用 `min-w-[Npx]` 而不是 `w-[Npx]` —— auto 布局下 `width` 只是建议值，
        语义上「至少这么宽、可以更宽」才是意图；
     ② 单元格里的**长自由文本要自己兜底**：`max-w-[Npx] truncate`（见 schedule-task-manager.js
        的错误信息列）。加了本类还指望它折行的列，会直接把表格顶得很宽。 */
.ui-table-nowrap tbody td { white-space: nowrap; }

/* ---------------------------------------------------------------------------
 * 10. 列表行（行程列表等「不用表格」的场景 —— 全站既有习惯就是不用 <table>）
 * ------------------------------------------------------------------------- */
.ui-row {
    background-color: rgb(var(--surface));
    border: 1px solid rgb(var(--rule));
    border-radius: 0.5rem;
    padding: 1rem;
}
.ui-row:hover { border-color: rgb(var(--rule-strong)); }

/* ---------------------------------------------------------------------------
 * 11. 徽章 / 标签
 *    现在同一概念在不同 Tab 用不同色相（amber vs yellow）、不同形状
 *    （rounded-full px-2 py-0.5 text-xs vs rounded px-1.5 py-0.5 text-[10px]）。
 *    收敛成一套：状态五色 + 中性 + 计数。
 * ------------------------------------------------------------------------- */
/* 色调工具（只有底色+字色，**不带任何形状**）。
   ui-badge-* 是「形状 + 色调」的组合类；当一段颜色要用在**自带尺寸**的元素上
   （行程量胶囊、排序按钮、筛选胶囊）时用 ui-tone-*，避免把徽章的内边距/字号带过去。 */
.ui-tone-info    { background-color: rgb(var(--accent-soft));  color: rgb(var(--accent)); }
.ui-tone-ok      { background-color: rgb(var(--success-soft)); color: rgb(var(--success)); }
.ui-tone-warn    { background-color: rgb(var(--warning-soft)); color: rgb(var(--warning)); }
.ui-tone-danger  { background-color: rgb(var(--danger-soft));  color: rgb(var(--danger)); }
.ui-tone-neutral { background-color: rgb(var(--surface-raised)); color: rgb(var(--ink-3)); }

.ui-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.125rem 0.5rem;
    border-radius: 9999px;
    font-size: 0.75rem;
    font-weight: 500;
    line-height: 1.25rem;
    white-space: nowrap;
}
.ui-badge-info    { background-color: rgb(var(--accent-soft));  color: rgb(var(--accent)); }
.ui-badge-ok      { background-color: rgb(var(--success-soft)); color: rgb(var(--success)); }
.ui-badge-warn    { background-color: rgb(var(--warning-soft)); color: rgb(var(--warning)); }
.ui-badge-danger  { background-color: rgb(var(--danger-soft));  color: rgb(var(--danger)); }
.ui-badge-neutral { background-color: rgb(var(--surface-raised)); color: rgb(var(--ink-3)); }

/* 计数胶囊：用于行程数、条数这类「数值要一眼可比」的地方 */
.ui-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.5rem;
    padding: 0.0625rem 0.375rem;
    border-radius: 9999px;
    font-size: 0.75rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.ui-count-1 { background-color: rgb(var(--accent) / 0.12); color: rgb(var(--accent)); }
.ui-count-2 { background-color: rgb(var(--accent) / 0.24); color: rgb(var(--accent)); }
.ui-count-3 { background-color: rgb(var(--accent) / 0.40); color: rgb(var(--accent)); }

/* 可点击筛选胶囊（优惠类型多选、只看某类记录） */
.ui-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.25rem 0.75rem;
    border-radius: 9999px;
    border: 1px solid rgb(var(--rule-strong));
    background-color: rgb(var(--surface));
    color: rgb(var(--ink-2));
    font-size: 0.75rem;
    font-weight: 500;
    cursor: pointer;
}
.ui-chip[aria-pressed="true"] {
    background-color: rgb(var(--accent));
    border-color: rgb(var(--accent));
    color: rgb(var(--accent-ink));
}

/* 行程日历格子语义（底色只表示日期属性，行程量用计数胶囊编码） */
.ui-day-holiday { color: rgb(var(--holiday)); }
.ui-day-makeup  { color: rgb(var(--makeup)); }
.ui-day-weekend { color: rgb(var(--weekend)); }

/* 42 格的格线：每个格子自己画 border-r / border-b（内部线一格一条就够），
   **最外圈那两条抹掉** —— 面板本身已经有一圈 1px 边框，最后一列格子的 border-r、
   最后一行格子的 border-b 紧贴着它，两条同色线连起来看着就是 2px，
   外框比里面的格线粗一倍（左下角放大 6 倍最明显）。容器自己也不要 border-l。
   用 nth-child 而不是给格子传「我是不是最后一列」：格子数固定是 7 的倍数，
   :nth-child(7n) 正好最后一列、:nth-last-child(-n+7) 正好最后一行。
   这两个选择器特异度高于 Tailwind 的 .border-r / .border-b，不受注入顺序影响
   （AGENTS.md 那条「同名工具类与组件类谁生效看顺序，别赌」正是不用工具类覆盖的原因）。 */
.ui-cal-grid > *:nth-child(7n) { border-right-width: 0; }
.ui-cal-grid > *:nth-last-child(-n + 7) { border-bottom-width: 0; }

/* 行程日历的月份切换：‹ [2026年09月] › + 本月。
   月份是**导航**（时间游标）而不是筛选字段，所以它住在日历面板的头里，不进 .ui-filter
   —— 同一条规矩的先例是油价 → 成品油调价日历（‹ 2026 › 今年 就在 ui-card-head 里）。 */
.ui-month-nav {
    display: inline-flex;
    align-items: stretch;
    overflow: hidden;
    border: 1px solid rgb(var(--rule-strong));
    border-radius: 0.5rem;
    background-color: rgb(var(--surface));
}
/* 子项圆角归零：不归零的话 hover 底色是 8px 圆角，压在 8px 圆角的分组框里四角会露出缺口 */
.ui-month-nav > .ui-btn { border-radius: 0; padding-left: 0.625rem; padding-right: 0.625rem; }
.ui-month-nav > * + * { border-left: 1px solid rgb(var(--rule-strong)); }
/* 中间这个 input[type=month] **就是标题本身**：点它弹原生月历，保留「一步跳到任意月」
   （只剩 ‹ › 的话，从 2026-09 翻回最早数据月份 2024-06 要点 27 次），键盘也能直接改。
   高度由它撑开（原生控件的行高说不准，写死 height 反而不稳），两侧箭头顶到同高。 */
.ui-month-nav input[type="month"] {
    width: 8.75rem;
    border: 0;
    background: transparent;
    padding: 0.4rem 0.5rem;
    text-align: center;
    font-size: 0.875rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: rgb(var(--ink));
    cursor: pointer;
}
.ui-month-nav input[type="month"]:hover { background-color: rgb(var(--surface-raised)); }
.ui-month-nav input[type="month"]:focus-visible {
    outline: 2px solid rgb(var(--accent));
    outline-offset: -2px;
}

/* 选「月」的月格（flatpickr 官方 monthSelect 插件，日期控件统一走 date-picker.js）。
   配色**逐值对齐**旁边那两套厂商皮肤，别自己发挥：
     浅色 material_blue：选中 #42a5f5 / hover #e2e2e2 / 今天描边 #bbb / 文字 #484848
     深色 dark.css：      选中 #80cbc4 / hover #646c8c / 今天描边 #eee / 文字 rgba(255,255,255,.95)
   插件自带的 style.css 是按它的浅色写死的（选中 #569ff7、月格文字 #393939），
   深色主题下会变成「深底压深字」——所以这里必须按 data-theme 覆盖一遍。
   为什么不换成品牌令牌？因为旁边的日格还是 material_blue / dark.css 这两套：
   月格跟主色、日格不跟，同一页两个弹层就是两种蓝，那是新的不统一。
   真要整体跟主色，得把 17 个日格一起换皮肤，是另一件事（见 AGENTS.md 的日期控件条目）。
   选择器都带 :root 前缀：app-ui.css 在 flatpickr 的 CSS **之前**加载，同特异度会输给厂商规则。 */
:root .flatpickr-monthSelect-month { color: #484848; }
:root .flatpickr-monthSelect-month.today { border-color: #bbb; }
:root .flatpickr-monthSelect-month.inRange,
:root .flatpickr-monthSelect-month:hover,
:root .flatpickr-monthSelect-month:focus {
    background: #e2e2e2;
    border-color: #e2e2e2;
}
:root .flatpickr-monthSelect-month.today:hover,
:root .flatpickr-monthSelect-month.today:focus {
    background: #bbb;
    border-color: #bbb;
    color: #fff;
}
:root .flatpickr-monthSelect-month.selected,
:root .flatpickr-monthSelect-month.startRange,
:root .flatpickr-monthSelect-month.endRange {
    background-color: #42a5f5;
    border-color: #42a5f5;
    color: #fff;
}
:root .flatpickr-monthSelect-month.flatpickr-disabled,
:root .flatpickr-monthSelect-month.flatpickr-disabled:hover {
    color: rgba(72, 72, 72, 0.3);
    background: none;
}

:root[data-theme="dark"] .flatpickr-monthSelect-month { color: rgba(255, 255, 255, 0.95); }
:root[data-theme="dark"] .flatpickr-monthSelect-month.today { border-color: #eee; }
:root[data-theme="dark"] .flatpickr-monthSelect-month.inRange,
:root[data-theme="dark"] .flatpickr-monthSelect-month:hover,
:root[data-theme="dark"] .flatpickr-monthSelect-month:focus {
    background: #646c8c;
    border-color: #646c8c;
}
:root[data-theme="dark"] .flatpickr-monthSelect-month.today:hover,
:root[data-theme="dark"] .flatpickr-monthSelect-month.today:focus {
    background: #eee;
    border-color: #eee;
    color: #3f4458;
}
:root[data-theme="dark"] .flatpickr-monthSelect-month.selected,
:root[data-theme="dark"] .flatpickr-monthSelect-month.startRange,
:root[data-theme="dark"] .flatpickr-monthSelect-month.endRange {
    background-color: #80cbc4;
    border-color: #80cbc4;
    color: #fff;
}
:root[data-theme="dark"] .flatpickr-monthSelect-month.flatpickr-disabled,
:root[data-theme="dark"] .flatpickr-monthSelect-month.flatpickr-disabled:hover {
    color: rgba(255, 255, 255, 0.1);
    background: none;
}

/* ---------------------------------------------------------------------------
 * 12. 状态占位：加载 / 空 / 错误
 *    现在 8 种加载、6 种空态、12 种空态文案。
 * ------------------------------------------------------------------------- */
.ui-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 2rem;
    text-align: center;
    color: rgb(var(--ink-3));
}
/* 并排三段式（加载 / 空 / 错误）时的分隔 */
.ui-state-row { display: grid; grid-template-columns: repeat(1, minmax(0, 1fr)); }
@media (min-width: 768px) { .ui-state-row { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.ui-state-row > * + * { border-top: 1px solid rgb(var(--rule)); }
@media (min-width: 768px) {
    .ui-state-row > * + * { border-top: 0; border-left: 1px solid rgb(var(--rule)); }
}
.ui-state i { font-size: 1.875rem; }
.ui-state-sm { padding: 1.5rem; }
.ui-state-error { color: rgb(var(--danger)); }

.ui-skeleton {
    background-color: rgb(var(--surface-raised));
    border-radius: 0.375rem;
    animation: ui-pulse 1.5s ease-in-out infinite;
}
@keyframes ui-pulse { 50% { opacity: 0.5; } }

/* ---------------------------------------------------------------------------
 * 19. 悬停说明气泡（.ui-help / [data-help] / .ui-tip）
 *    口径与操作说明不放正文里，也不靠原生 `title` —— 原生提示延迟约 1 秒、
 *    样式不可控、在嵌入式浏览器或截图里不出现，回归脚本也没法断言。
 *    用法：<i class="fas fa-circle-question ui-help" data-help="说明" tabindex="0"></i>
 *    气泡由 Utils.initHelpTips() 统一创建与定位（事件委托，动态插入的图标同样生效）。
 * ------------------------------------------------------------------------- */
/* 只给「有说明」的提示光标与悬停反馈；本色由各处的 text-* 决定，
   不要在这里写死颜色，否则套在文字 span 上会把正文染淡 */
.ui-help { cursor: help; transition: color .12s; }
.ui-help:hover { color: rgb(var(--ink-2)); }
/* 键盘 Tab 到说明图标时既要有焦点框（可访问性），也要弹气泡（focusin 已接） */
.ui-help:focus-visible { color: rgb(var(--ink-2)); outline: 2px solid rgb(var(--accent)); outline-offset: 2px; }

.ui-tip {
    position: fixed;
    left: 0;
    top: 0;
    z-index: var(--z-toast);          /* 要盖住表头（sticky）与弹窗遮罩 */
    max-width: 22rem;
    padding: 0.5rem 0.625rem;
    border: 1px solid rgb(var(--rule-strong));
    border-radius: 0.5rem;
    background-color: rgb(var(--surface));
    color: rgb(var(--ink-2));
    box-shadow: 0 8px 24px rgb(0 0 0 / 0.16);
    font-size: 0.75rem;
    line-height: 1.5;
    text-align: left;
    white-space: pre-line;            /* 说明里的 \n 按行渲染 */
    pointer-events: none;             /* 气泡不能挡住鼠标，否则会来回闪 */
    opacity: 0;
    visibility: hidden;
    transition: opacity .12s;
}
.ui-tip[data-show="1"] { opacity: 1; visibility: visible; }

/* ---------------------------------------------------------------------------
 * 20. 主题切换（.ui-theme）
 *     图标按钮 + 三选一菜单，结构由 /js/v2/theme.js 生成（三个页面共用一份，
 *     免得像 tab 条那样被复制几次就漂移出几种样子）。
 *     页面侧只要放一个空挂载点：<div data-theme-switch></div>
 *
 *     为什么是「按钮 + 菜单」而不是常驻的分段控件：顶部工具栏在手机上只有
 *     ~350px 可用，已经有刷新 / 定位 / 墨水屏几个图标，再塞三个
 *     带字的按钮会把这行挤断（放不下宁可收起，不硬塞）。
 *     （原来还有一个「年度报告」图标，年报并进 Tab 条之后就去掉了。）
 * ------------------------------------------------------------------------- */
.ui-theme {
    position: relative;
    display: inline-flex;
    align-items: center;
}
.ui-theme-trigger { /* 尺寸与配色继承 .ui-btn-icon */ }
.ui-theme-menu {
    position: absolute;
    top: calc(100% + 0.375rem);
    right: 0;
    z-index: var(--z-overlay);
    min-width: 9.5rem;
    padding: 0.25rem;
    border: 1px solid rgb(var(--rule-strong));
    border-radius: 0.5rem;
    background-color: rgb(var(--surface));
    box-shadow: 0 8px 24px rgb(0 0 0 / 0.16);
}
.ui-theme-menu[hidden] { display: none; }
.ui-theme-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0.5rem 0.625rem;
    border-radius: 0.375rem;
    font-size: 0.875rem;
    line-height: 1.25rem;
    white-space: nowrap;
    text-align: left;
    color: rgb(var(--ink-2));
    cursor: pointer;
}
.ui-theme-item:hover { background-color: rgb(var(--surface-raised)); color: rgb(var(--ink)); }
/* 左侧图标定宽，三项的文字才对齐成一条竖线 */
.ui-theme-item > i:first-child { width: 1rem; text-align: center; color: rgb(var(--ink-3)); }
.ui-theme-check { margin-left: auto; font-size: 0.75rem; visibility: hidden; }
.ui-theme-item[aria-checked="true"] { color: rgb(var(--accent)); }
.ui-theme-item[aria-checked="true"] > i:first-child { color: rgb(var(--accent)); }
.ui-theme-item[aria-checked="true"] .ui-theme-check { visibility: visible; }

/* ---------------------------------------------------------------------------
 * 21. 主色调选择（.ui-brand-*）
 *     结构由 /js/v2/palette.js 生成，追加在主题菜单底部 —— 用户找「外观」时
 *     眼睛就在那个按钮上，另起一个入口只会让人找不到。
 *     色块显示的是**派生后的主色**（不是选中的那个 hex）：派生会为了对比度微调明度，
 *     色块要和按钮真正变成的颜色一致，否则点了会「和看到的不是一个色」。
 * ------------------------------------------------------------------------- */
.ui-theme-sep { height: 1px; margin: 0.25rem 0; background-color: rgb(var(--rule)); }
.ui-brand-label { padding: 0.25rem 0.5rem 0; font-size: 0.75rem; color: rgb(var(--ink-3)); }
.ui-brand-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    width: 11.5rem;
    padding: 0.5rem;
}
.ui-brand-sw {
    width: 1.25rem;
    height: 1.25rem;
    border-radius: 9999px;
    border: 1px solid rgb(var(--rule-strong));
    cursor: pointer;
    /* 选中环画在色块**外面**：box-shadow 不占位，多一圈也不会把色板挤变形 */
    box-shadow: 0 0 0 0 transparent;
    transition: box-shadow 150ms ease;
}
.ui-brand-sw[aria-pressed="true"] {
    box-shadow: 0 0 0 2px rgb(var(--surface)), 0 0 0 4px rgb(var(--accent));
}
/* 任意取色：原生 color input 自带系统取色器，比自制色轮可靠得多。
   外面套一层 label 是为了「点哪都能开」—— input 本身只有 1.25rem 见方 */
.ui-brand-custom {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.25rem;
    height: 1.25rem;
    border-radius: 9999px;
    border: 1px dashed rgb(var(--rule-strong));
    cursor: pointer;
    overflow: hidden;
    background: conic-gradient(from 0deg, #f43f5e, #f59e0b, #84cc16, #06b6d4, #6366f1, #d946ef, #f43f5e);
}
.ui-brand-custom input {
    width: 0;
    height: 0;
    padding: 0;
    border: 0;
    opacity: 0;
}
/* 撞色提示：不阻止选择，只讲清会发生什么（语义色不会为此让位） */
.ui-brand-warn {
    width: 11.5rem;
    padding: 0 0.5rem 0.375rem;
    font-size: 0.6875rem;
    line-height: 1.4;
    color: rgb(var(--warning));
}
.ui-brand-warn[hidden] { display: none; }
.ui-brand-reset[hidden] { display: none; }

/* 主题切换的 300ms 过渡。标记由 theme.js 在切换瞬间挂上、300ms 后摘掉 ——
   所以它只作用于「换主题」这一个动作，不会给全站每个 hover 常驻一条 transition。
   「减少动态效果」由 theme.js 直接不挂标记（见 allowTransition()），这里不用再兜一层。 */
:root[data-theme-transition] *,
:root[data-theme-transition] *::before,
:root[data-theme-transition] *::after {
    transition: background-color .3s ease, color .3s ease, border-color .3s ease, fill .3s ease !important;
}

/* ---------------------------------------------------------------------------
 * 22. 登录页底面（.ui-login-bg / .ui-login-card）
 *
 *     登录页原来只有一层 --surface-sunken（#f9fafb），白卡（--surface #fff）压在上面
 *     只差 1.04:1 —— 整页读起来就是「空白」，卡片的边界得靠找。
 *
 *     做法：**三团同色相的光斑 + 一块玻璃卡片**。
 *
 *     一、为什么光斑只有三个、且全用主色系
 *       光斑只取 --accent / --accent-hover：它们是 palette.js 从**一个主色**派生出来的
 *       同色相两档明度。全站有取色器，主色可以是任意 hex（绿、橙、紫都行），
 *       一旦在这里引入第二个色相（紫、粉、青），碰上某些主色就会脏掉；
 *       同色相三团则对任何主色都成立。深浅两套主题各自调 alpha，不是同一份。
 *
 *     二、位置要**压到卡片上**，光不能只待在四角
 *       第一版把三团光都放在四角（14% 8% / 88% 90% / 84% 6%），实图里卡片背后是干净的
 *       —— 玻璃没东西可透，backdrop-filter 等于白做一次合成。现在第一团挪到 20% 38%，
 *       落点正好让它的**衰减边缘扫过卡片左半边**，卡片上能看到一层由蓝到白的过渡，
 *       玻璃这才成立（合成底从纯白 255,255,255 变成 245,248,253）。
 *
 *     三、但光不能扫到 --ink-3 / --ink-4 的文字上
 *       这两个令牌在纯 --surface 上只有 4.8:1（见 palette.js 的派生），余量极薄，
 *       再叠一层主色就掉到 4.5 以下。所以副标题（y≈342）和页脚（y≈681）那两条带
 *       必须留在光的衰减范围之外 —— 上面第一团的圆心 y 就选在 38%（≈342px）却把
 *       半径控制在 34rem，让它横向够到卡片、纵向够不到那两行字。
 *       审计脚本量的结果（加游客入口后重跑，浅色）：标题 16.9:1、副标题 4.60:1、
 *       游客入口按钮 10.1:1、入口说明 / 图标 4.78:1、卡内 label 9.7:1、记住口令 9.9:1、
 *       忘记口令 5.26:1、页脚 4.78:1 —— 全部达标（深色最差 6.18:1）。
 *       ⚠ 页脚原来是 text-ink-4，只有 2.43:1（深色 3.68:1）：这里记的 4.78 一直是
 *       **ink-3** 的值，等于文档写达标、代码没达标。已改回 ink-3。
 *       **改这里的半径/坐标/alpha 都要重跑 .scratch/contrast-audit.mjs。**
 *
 *     四、玻璃只在卡片上，不在别处
 *       只有 .ui-login-card 这一处 backdrop-filter。页面上没有第二块玻璃，
 *       也没有拿模糊去装饰任何一个不需要透的东西。
 *
 *     ⚠ 光斑挂在 ::before 这一层，**不能直接挂在 body 上**：::before 是独立的绘制层，
 *       正文压在它上面；直接写在 body 的 background 上则没法把光斑放到内容之下再做混合。
 *       这一层 position: fixed + inset: 0，光斑以视口为准，不随内容高度变形；
 *       radial-gradient 自带边界，不会撑出滚动条，所以也不需要 overflow: hidden
 *       （那个会连带影响 body 的滚动与 sticky，能不写就不写）。
 *
 *     ⚠ 不支持 backdrop-filter 的浏览器会走 @supports 外那份 0.94 的近实心底：
 *       半透明的白卡糊在彩色光斑上、又不模糊，会读成一张「发灰的脏卡」，
 *       比不透明更难看，所以兜底要往**实**的方向走，不是往透明走。
 *
 *     半径与 alpha 都是照着实图定的：46rem 那种大半径会把整屏连成一片糊、看不出形状；
 *     浅色 alpha 从 0.30 起往上调会开始和文字抢对比度，深色底更能吃光所以给了更大的值。
 *     两套主题的 alpha 是各调各的，不是同一份。
 * ------------------------------------------------------------------------- */
.ui-login-bg {
    position: relative;
    background-color: rgb(var(--surface-sunken));

    --login-glass: 0.72;
    /* 三个光斑的几何。坐标也提成变量，是为了让「对比度审计」脚本能读到同一份来源 ——
       审计脚本第一版把坐标抄成了字面量，改完 CSS 忘了同步，量出来的还是旧位置。 */
    --login-r1: 34rem;  --login-p1: 20% 38%;   /* 46rem 那种大半径会把整屏连成一片糊（试过） */
    --login-r2: 32rem;  --login-p2: 84% 84%;
    --login-r3: 22rem;  --login-p3: 86% 14%;
    --login-glow-1: 0.32;   /* 左中主光斑：唯一一团要够到卡片的 */
    --login-glow-2: 0.26;   /* 右下副光斑 */
    --login-glow-3: 0.15;   /* 右上补光，补一点不对称 */
}
:root[data-theme="dark"] .ui-login-bg {
    --login-glass: 0.70;
    --login-glow-1: 0.50;
    --login-glow-2: 0.42;
    --login-glow-3: 0.24;
}
.ui-login-bg::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -2;                /* 压在 body 的 --surface-sunken 之上、正文之下 */
    pointer-events: none;
    background-repeat: no-repeat;
    background-image:
        radial-gradient(var(--login-r1) var(--login-r1) at var(--login-p1),
            rgb(var(--accent) / var(--login-glow-1)), transparent 68%),
        radial-gradient(var(--login-r2) var(--login-r2) at var(--login-p2),
            rgb(var(--accent-hover) / var(--login-glow-2)), transparent 68%),
        radial-gradient(var(--login-r3) var(--login-r3) at var(--login-p3),
            rgb(var(--accent) / var(--login-glow-3)), transparent 68%);
}

/* 玻璃卡片。用固定黑透明而不是 --ink 当阴影：深色下 --ink 是近白色，
   拿它当阴影会变成一圈白光晕（全站既有的 .ui-card-hover / .ui-theme-menu 也是这么写的）。 */
.ui-login-card {
    /* 兜底：不支持 backdrop-filter 时用接近不透明的底（理由见上面那段 ⚠） */
    background-color: rgb(var(--surface) / 0.94);
    border-color: rgb(var(--rule) / 0.8);
    box-shadow: 0 1px 2px rgb(0 0 0 / 0.04), 0 24px 48px -28px rgb(0 0 0 / 0.28);
}
@supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
    .ui-login-card {
        background-color: rgb(var(--surface) / var(--login-glass));
        -webkit-backdrop-filter: blur(22px) saturate(180%);
                backdrop-filter: blur(22px) saturate(180%);
        /* 顶边那道 1px 亮线是玻璃的「厚度」：没有它，这块就只是一张压暗的半透明色块 */
        box-shadow:
            inset 0 1px 0 0 rgb(var(--ink) / 0.06),
            0 1px 2px rgb(0 0 0 / 0.04),
            0 24px 48px -28px rgb(0 0 0 / 0.32);
    }
}

/* ---------------------------------------------------------------------------
 * 23. 站点标记（.ui-logo）
 *    页头与登录页那枚「H + 道路」，与标签页图标是同一份几何
 *    （唯一实现在 /js/v2/favicon.js，改了几何跑 node scripts/icons/build-icons.mjs --print-logo 重新贴）。
 *
 *    这里只解决**颜色**，而且一行 JS 都不需要：
 *      底色（方块 + 挖空的道路虚线）= currentColor，调用处给 text-accent
 *      标记（H 本身）               = --accent-ink
 *    于是用户换主色、切深浅色，logo 跟着走 —— 与页面里 bg-accent / text-accent-ink 的按钮同一条规则。
 *    别改成 <img src="/icons/icon.svg">：图里的颜色是烤死的，换主色它不会跟。
 * ------------------------------------------------------------------------- */
.ui-logo {
    display: block;
    flex: none;          /* 在 flex 行里不许被压扁（不是方形就露馅） */
}
.ui-logo-tile { fill: currentColor; }
.ui-logo-mark { fill: rgb(var(--accent-ink)); }
