/* ==========================================================================
   ExamCNing 四端共享样式
   --------------------------------------------------------------------------
   视觉方向：主流管理后台（政务蓝 + 石板灰 + 白卡片）
   --------------------------------------------------------------------------
   一句话定调：把原型 `PPT/pages/console.html` 的那套观感搬进真实系统 ——
   深藏青侧栏 + 白色顶栏 + #f4f6fb 平底，内容都是圆角白卡片配柔阴影，
   一屏之内只让蓝这一个强调色出现。

   四条设计支柱的落点（对齐原型）
   ----------------------------
   Typography
     正文      = "Inter Tight","Source Han Sans SC","Noto Sans SC","PingFang SC","Microsoft YaHei"
     等宽      = "JetBrains Mono","Cascadia Mono",Consolas,...
     —— 等宽只用于**编号 / 哈希 / 查验码**这类逐位读的字符串（等宽在那里是
        功能：数位对齐、0 与 O 分得开）。指标卡的大数字走正文字体 +
        font-variant-numeric: tabular-nums —— 对齐靠数值特性而不是字体。
        这是原型的做法，也是主流后台的做法。
     ★ 中文字体只按本机真实存在的顺序排，绝不引外链字体（本机 CDN 不通）。

   Color
     石板灰（--ink-*）承担底色、边框与次级文字；政务蓝（--brand-*）承担
     数据、链接、主操作与当前态；语义色（success / warning / danger）只标
     状态，绝不拿来装饰。强调色（--signal-*）**与主色同族** —— 原型里没有
     第二个强调色，橙色的"仪器面板"口吻已经撤掉。
     ★ 图表色序（--c1..--c6）与语义色**同源不同值**：语义色要压得住白字
       （偏深），图表色要在浅底上有辨识度（偏亮），两组各自达标，不互相借用。

   Motion
     只有一次编排好的入场：侧栏项目错峰浮现（≤330ms 内全部结束）。
     其余是卡片 hover 的 -2px 抬升、按钮与链接的 140ms 反馈。
     不做"每个元素都来一段动画"——那正是反 AI 味清单第 7 条。
     ★ 有意偏离技能基准里的"3~6 组、间隔 60~120ms"：tools/check_web.js 在
       页面加载完成后只等 300ms 就截图，更慢的编排会被拍成半透明。

   Composition
     圆角 8/14 + 发丝边框 + 三级柔阴影构成层次。上一版的"档案夹"语言
     （2px 顶部墨线 + 左侧刻度 + 几何网格底纹 + 颗粒）已整体撤掉：
     它和原型不是同一种方言，混在一起只会两个都不像。
     留白走 4/8/12/16/24/32 的固定梯度。

   零依赖 —— 不引任何框架/CSS 库；图标全是内联 SVG。
   ★ 类名一律**不改**：`tools/check_chart.js` 会把 chart.js 里用到的 67 个
     类名和本文件做 1:1 对照，改名字等于同时改三处（js / 44 个页面 / 本文件）。
     换肤只动色值与盒模型，不动选择器。
   ========================================================================== */

:root {
    /* ==================== 中性色：冷调石板灰（Slate） ====================
       原型（`PPT/pages/console.html`）用的是 Slate 系中性色：比纯灰冷，
       压在白卡片下面层次清楚，在投影与屏幕截图里都不会发黄。
       ★ 保留 `--ink-*` 这套旧命名（全站 200 多处引用），只换色值 ——
         换名字要动 200 处，一次漏改就是一处静默失效（`var()` 取不到值
         不会报错，只是那条声明被丢掉）。 */
    --ink-950: #0b1220;
    --ink-900: #0f172a;
    --ink-800: #1e293b;
    --ink-700: #334155;
    --ink-600: #475569;
    --ink-500: #64748b;
    --ink-400: #94a3b8;
    --ink-300: #cbd5e1;
    --ink-200: #e2e8f0;
    --ink-100: #f1f5f9;
    --ink-50:  #f8fafc;

    /* ==================== 主色：政务蓝（结构 / 数据 / 链接） ====================
       色相从原来的"钢灰蓝"拉回标准政务蓝（#2563eb 一档）——
       这是主流管理后台最不容易出错的信任色，也是原型用的那一支。
       保留 `--brand-*` 命名。 */
    --brand-900: #1c366f;
    --brand-800: #1b3a8a;
    --brand-700: #1a41ad;
    --brand-600: #1d4ed8;
    --brand-500: #2563eb;
    --brand-100: #dbe7ff;
    --brand-50:  #eef4ff;

    /* ==================== 强调色：与主色同族（原型只用蓝） ====================
       原来是"测绘橙"，是上一版"仪器面板"视觉的签名色。原型里**没有第二个
       强调色**，主操作、当前态、焦点环通通是蓝 —— 所以这里把强调色并回蓝族。

       ★ 仍然拆成多档而不是一个 --signal：-700 是"能扛白字"的那一档
         （填充按钮），-500 更亮、只用在**非文字**的地方（导轨 / 焦点环 /
         小圆点）。一个颜色同时满足"填充够深"和"发光够亮"做不到。 */
    --signal-800: #1a41ad;
    --signal-700: #1d4ed8;
    --signal-600: #2563eb;
    --signal-500: #3b82f6;
    --signal-100: #dbe7ff;
    --signal-50:  #eef4ff;

    /* ==================== 语义色（通用语义，不混用涨跌口径） ====================
       前景/底色都是成对校验过对比度的：正文级 ≥4.5:1。底色用 -50/-100 档，
       和原型的徽章（浅底 + 深字）一致。 */
    --success: #047857;
    --success-bg: #d1fae5;
    --warning: #b45309;
    --warning-bg: #fef3c7;
    --danger:  #b91c1c;
    --danger-bg: #fee2e2;
    --info:    #1d4ed8;
    --info-bg: #dbe7ff;

    /* ==================== 中性语义别名 ==================== */
    --text: #0f172a;
    --text-2: #475569;
    --text-3: #64748b;
    --line: #e2e8f0;
    --line-2: #f1f5f9;
    --bg: #f4f6fb;              /* 页面底色（原型 --bg-app） */
    --surface: #ffffff;
    --surface-1: #ffffff;       /* ★ 补上：`.chip` 一直在引用它，但从来没定义过，
                                   `var()` 取不到值时整条声明被丢弃 —— 主色底
                                   变成透明，在灰底上就是"这块看起来没上色"。 */
    --surface-2: #f8fafc;

    /* ==================== 图表色序 ====================
       1 蓝（总量 / 主体）2 翡翠绿（正向结果）3 琥珀（需要关注）
       4 红（异常）5 紫（辅助分类）6 石板灰（未知 / 其它）
       与 labels.js 的徽章 kind 一一对应（ok→c2 / warn→c3 / danger→c4），
       所以"已出分"在表里是绿的、在环图里也是绿的。同一张图最多 4 色。
       ★ 与语义色**同源但不同值**：语义色要满足"白字压得住"（偏深），
         图表色要满足"色块在浅底上有辨识度"（偏亮），两组各自达标。 */
    --c1: #2563eb;
    --c2: #10b981;
    --c3: #f59e0b;
    --c4: #ef4444;
    --c5: #8b5cf6;
    --c6: #94a3b8;
    --c1-soft: #eef4ff;
    --c2-soft: #ecfdf5;
    --c3-soft: #fffbeb;
    --c4-soft: #fef2f2;

    /* ==================== 尺寸 ====================
       圆角放大一档（6/10 → 8/14）：主流管理后台是"卡片 + 柔阴影"，
       小圆角配柔阴影会显得没做完。胶囊（药丸、徽章）走 999px。 */
    --r-sm: 6px;
    --r: 8px;
    --r-lg: 14px;
    --sp-1: 4px;
    --sp-2: 8px;
    --sp-3: 12px;
    --sp-4: 16px;
    --sp-5: 24px;
    --sp-6: 32px;
    --sidebar-w: 232px;
    --topbar-h: 60px;

    /* 阴影用原型那一套（冷调近黑、多层、不黑不脏）；
       三级高度 Y 偏移 2/12/28。柔是真柔 —— 卡片多的时候不能糊成一片。 */
    --shadow-sm: 0 1px 2px rgba(16, 24, 40, .05);
    --shadow: 0 4px 12px rgba(16, 24, 40, .08);
    --shadow-lg: 0 24px 48px rgba(16, 24, 40, .14);

    /* ==================== 动效令牌 ==================== */
    --ease: cubic-bezier(.16, 1, .3, 1);
    --ease-out: cubic-bezier(.22, .61, .36, 1);
    --dur-fast: 140ms;
    --dur: 200ms;
    --dur-slow: 420ms;

    /* ==================== 字体 ====================
       展示 / 数字用等宽（有仪表感），正文用黑体（中文优先）。

       本机实测可用性（PowerShell 枚举 InstalledFontCollection 得到）：
         Cascadia Mono ✔（已装 → 全站等宽数字实际落在 Cascadia Mono）
         Consolas ✔ / Courier New ✔（兜底）
         Noto Sans SC ✔（已装 → 正文的拉丁与中文都由它承担）
       ★ 等宽栈尾部**额外补了两个中文字面**（Noto Sans SC / Microsoft YaHei）：
         原栈里 Cascadia/Consolas/Courier New 都不含汉字，汉字会掉到 generic
         `monospace` 上，在中文 Windows 上通常取到宋体 —— 侧栏分组名、端名、
         登录页副标题会变成宋体，和黑体标题同屏是一眼能看出的破绽。
         补上之后：拉丁走 Cascadia Mono，汉字走 Noto Sans SC，两边都是"方"的。

       ★ 与上一版的差别：等宽**不再用于大数字**。指标卡的数字现在走正文字体
         + `tabular-nums`（对齐靠数值特性，不靠字体），这是原型的做法；
         等宽只留给"编号 / 哈希 / 查验码"这类**逐位读**的字符串 —— 那里
         等宽是功能（数位对齐、0 与 O 分得开），这里只是风格。
         侧栏分组名、端名同样改回正文字体：全大写 + 字距撑开的等宽小字
         是"仪器面板"的口吻，管理后台里显得密而旧。 */
    /* ★ iOS 系统字体前置（10-17 轮）：真机在 iOS 上会拿到
       SF Pro / 苹方 —— 这才是「iOS 风格」的**根基**，
       任何圆角、配色都对不出「就是 iOS 那个味道」。
       `PingFang SC` 提到前面是因为**中文界面里它才是主角**，
       拉丁字形反而无所谓；安卓/Windows 由后面的兜底接住。
       `Inter Tight` 保留在最后一位：它仍然管着中文不落到难看的默认体上。 */
    --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display",
            "PingFang SC", "Helvetica Neue", "Inter Tight",
            "Source Han Sans SC", "Noto Sans SC", "Microsoft YaHei", sans-serif;
    /* iOS 语义色。**不要**拿它们当「设计令牌」到处套 ——
       它们记录的是「iOS 界面上那个蓝/那条线/那块灰」的具体值，
       换主题时要一起换，所以放在一起而不是散落各处。 */
    --ios-blue: #007aff;
    --ios-blue-fill: rgba(0, 122, 255, .12);
    --ios-sep: rgba(60, 60, 67, .29);       /* 发丝分隔线（发丝=1物理像素） */
    --ios-sep-opaque: #c6c6c8;
    --ios-grouped: #f2f2f7;
    --ios-label: #000;
    --ios-secondary: rgba(60, 60, 67, .6);
    --ios-tertiary: rgba(60, 60, 67, .3);
    --mono: "JetBrains Mono", "Cascadia Mono", "IBM Plex Mono", Consolas,
            "Courier New", "Noto Sans SC", "Microsoft YaHei", monospace;

    /* ==================== 氛围层：已撤掉 ====================
       上一版在 body 上铺了 32px 几何网格 + 颗粒噪点（"测绘图纸"的质感）。
       原型是**纯平底色**（#f4f6fb），任何纹理都会让白卡片的边缘显得脏，
       在 1366 这类小屏上还会和表格的发丝线打架。
       ★ 这两个变量保留为 `none` 而不是直接删掉：`--paper-grid-dark` 还被
         登录页（深色底）引用着，删变量会让那几条声明静默失效。 */
    --paper-grid: none;
    --paper-grid-dark: none;
    --paper-noise: none;
}

/* ============================ reset ============================ */
* { box-sizing: border-box; }

html, body {
    margin: 0;
    padding: 0;
    height: 100%;
}

body {
    font-family: var(--font);
    font-size: 14px;
    line-height: 1.6;
    color: var(--text);
    /* 平底 —— 上一版的 32px 几何网格 + 颗粒噪点已撤（见文末令牌区的说明）。
       白卡片压在平底上边缘才干净；有纹理时卡片的发丝边框会显得脏。 */
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

a { color: var(--brand-600); text-decoration: none; }
a:hover { color: var(--brand-700); text-decoration: underline; text-underline-offset: 2px; }

h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -.01em; }
h1 { font-size: 22px; }
h2 { font-size: 18px; }
h3 { font-size: 15px; }

p { margin: 0 0 var(--sp-3); }

code, .mono { font-family: var(--mono); font-size: 12.5px; }

/* 隐藏但可被脚本读取 */
.hidden { display: none !important; }
/* 字段「保留但不展示」专用（10-07 轮 #134）。
   语义与 .hidden 同为 display:none（留 !important 是为了压过
   `.kv-list__row { display:flex }` 这类布局规则 ——
   不加会被 flex 覆盖，规则「写了却不生效」比不写更难查），
   区别只在于**用途**：这是业务口径，不是视觉工具。 */
.hidden-score { display: none !important; }
.sr-only {
    position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* ---- 键盘焦点环：全站统一走信号橙，鼠标点击不触发（:focus-visible） ---- */
:where(a, button, summary, [tabindex]):focus-visible {
    outline: 2px solid var(--signal-500);
    outline-offset: 2px;
}
.btn:focus-visible,
.pill:focus-visible,
.seg__item:focus-visible,
.grid-nav__item:focus-visible,
.login-tab:focus-visible {
    outline: 2px solid var(--signal-500);
    outline-offset: 2px;
}

/* ============================ 布局 ============================ */
.app-shell { display: flex; min-height: 100vh; }

/* 侧栏 = 深藏青底板（原型的 #101828）。
   右侧不再压一条发丝高光：那层高光是给"石墨灰纸面"配的，
   在 #f4f6fb 这种更亮的底色上会变成一条脏线。 */
.sidebar {
    width: var(--sidebar-w);
    flex: 0 0 var(--sidebar-w);
    background: #101828;
    color: rgba(255, 255, 255, .7);
    display: flex;
    flex-direction: column;
    position: sticky;
    top: 0;
    height: 100vh;
    overflow-y: auto;
    z-index: 20;
}

.sidebar__brand {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 20px 18px 18px;
    border-bottom: 1px solid rgba(255, 255, 255, .07);
}

/* 标识：十字准星（测绘靶心）。纯几何、内联 SVG，不引图标库。
   色相跟着主色走 —— 原型里"品牌"与"主操作"是同一个蓝。 */
.sidebar__mark {
    flex: 0 0 30px;
    width: 30px;
    height: 30px;
    color: #60a5fa;
}

.sidebar__brand-text { min-width: 0; }

.sidebar__brand b {
    display: block;
    color: #fff;
    font-size: 14px;
    line-height: 1.35;
    letter-spacing: .2px;
}

/* 端名（"管理员端"这类）。原是等宽 + .18em 字距的"铭牌"写法，
   改成普通字体小字：等宽小字在管理后台里密而旧，且和正文不同族。 */
.sidebar__brand span {
    display: block;
    font-size: 11px;
    color: rgba(255, 255, 255, .45);
    margin-top: 2px;
}

.sidebar__group {
    padding: var(--sp-4) var(--sp-4) var(--sp-2);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .6px;
    color: rgba(255, 255, 255, .32);
    text-transform: uppercase;
}
/* 分组前的短刻度线已撤：原型的分组名只是一行小字。
   刻度线 + 大写 + 字距三者叠在一起，侧栏会长出"仪表刻度盘"的味道，
   和白色顶栏不是一种方言。 */
.sidebar__group::before { content: none; }

.sidebar__nav { padding: 0 var(--sp-3) var(--sp-5); }

.sidebar__link {
    position: relative;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 10px;
    margin-bottom: 2px;
    border-radius: var(--r-sm);
    color: rgba(255, 255, 255, .7);
    font-size: 13.5px;
    user-select: none;
    transition: background var(--dur-fast) var(--ease),
                color var(--dur-fast) var(--ease);
}

.sidebar__link:hover {
    background: #1d2939;
    color: #fff;
    text-decoration: none;
}

/* 当前态 = 整块填充主色 + 一点外发光。
   上一版是"橙色左导轨 + 右上小方块"的空心写法（为了不把"当前在哪"和
   "这里能点"混成一种信号）。原型是实心药丸 —— 侧栏底色已经很深，
   实心填充在这里读起来是"选中的菜单项"，不会被误读成按钮。 */
.sidebar__link.is-active {
    background: var(--brand-600);
    color: #fff;
    font-weight: 500;
    box-shadow: 0 4px 12px rgba(37, 99, 235, .35);
}
.sidebar__link.is-active::before,
.sidebar__link.is-active::after { content: none; }

.sidebar__link svg { flex: 0 0 16px; opacity: .85; }
.sidebar__link.is-active svg { opacity: 1; }

/* 「建设中」的项：靠灰阶而不是行内 opacity（行内样式会盖掉 hover 反馈） */
.sidebar__link.is-soon { color: rgba(255, 255, 255, .28); }
.sidebar__link.is-soon:hover { color: rgba(255, 255, 255, .38); cursor: not-allowed; }

.main {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.topbar {
    height: var(--topbar-h);
    flex: 0 0 var(--topbar-h);
    /* 半透明白 + 背景虚化：内容滚到顶栏下面时不会"切断"，而是糊过去。
       这是主流后台的通用做法，也是原型用的那一套。 */
    background: rgba(255, 255, 255, .85);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--line);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    padding: 0 var(--sp-5);
    position: sticky;
    top: 0;
    z-index: 20;
}

/* 左端 = 端名 / 当前页。原来是"标题前一个橙色方块"——
   橙色撤掉后，方块只剩装饰意义，改成原型的面包屑写法：
   端名做浅色上阶、页名做深色当前项，一次给到"我在哪个端、在哪一页"。 */
.topbar__lead {
    display: flex;
    align-items: center;
    gap: 7px;
    font-size: 13px;
    color: var(--text-3);
    min-width: 0;
}

.topbar__crumb { color: var(--text-3); white-space: nowrap; }
.topbar__sep { color: var(--ink-300); }

.topbar__title {
    font-size: 15px;
    font-weight: 600;
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.topbar__title::before { content: none; }

.topbar__right {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    font-size: 13px;
    color: var(--text-2);
}

.topbar__user { white-space: nowrap; }

/* 头像：名字首字 + 蓝紫渐变底。纯装饰（aria-hidden），
   但它让顶栏右端有了一个"人"的锚点，而不是三个并排的纯文字。 */
.avatar {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: linear-gradient(135deg, var(--brand-500), var(--c5));
    color: #fff;
    display: grid;
    place-items: center;
    font-size: 13px;
    font-weight: 600;
    flex: 0 0 32px;
}

.content {
    /* 宽度上限：政务系统的表格最长也就十来列，铺满 2K 屏反而难读。
       224px 侧栏 + 1400px 正文在 1560 视口下刚好，更宽的屏幕左右留白。 */
    padding: var(--sp-5);
    flex: 1 1 auto;
    width: 100%;
    max-width: 1400px;
    margin: 0 auto;
}

/* 页头（标题 + 说明 + 右侧按钮）。原来的下边框已撤：
   原型里页头与下面的卡片之间靠留白分隔，多加一条线会和卡片的上边框
   在同一屏里形成"双线"，看着像没对齐。 */
.page-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--sp-4);
    margin-bottom: var(--sp-5);
    flex-wrap: wrap;
}

.page-head h1 { font-size: 22px; letter-spacing: -.3px; }
.page-head__desc { color: var(--text-2); font-size: 13.5px; margin-top: 4px; }

/* ============================ 卡片 ============================ */
/* 白卡片 + 发丝边框 + 最轻一档柔阴影。上一版的"2px 顶部墨线"已撤 ——
   那是"档案夹"的语言，和柔阴影叠在一起会让卡片的顶边显得比左右重。
   卡片一多，阴影宁愿欠一点：糊成一片灰比少一点层次更糟。 */
.card {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-sm);
    margin-bottom: var(--sp-4);
}

.card__head {
    padding: var(--sp-4) 18px;
    border-bottom: 1px solid var(--ink-100);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    border-radius: var(--r-lg) var(--r-lg) 0 0;
}

/* 卡片标题左侧一小段蓝色刻度：让每张卡的标题都在同一条竖直基准上起头，
   也给一片白卡片一个统一的识别点。原型没有这一段 ——
   但它只有 3px 宽，不会把版式带偏，这里保留。 */
.card__head h3 {
    display: flex;
    align-items: center;
    gap: 9px;
    font-size: 15px;
    font-weight: 600;
    color: var(--text);
}
.card__head h3::before {
    content: '';
    width: 3px;
    height: 13px;
    background: var(--brand-500);
    border-radius: 2px;
    flex: 0 0 3px;
}

.card__body { padding: 18px; }
.card__body--tight { padding: 0; }

/* 指标卡（KPI） —— 大数字 + 单位小字，右上角可选图标。
   数字走正文字体 + tabular-nums：一列卡片的数字上下对得齐，
   靠的是数值特性而不是等宽字体（原型的做法）。 */
.stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: var(--sp-4);
    margin-bottom: var(--sp-4);
}

.stat {
    position: relative;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    padding: 16px 18px;
    box-shadow: var(--shadow-sm);
    transition: transform var(--dur) var(--ease),
                box-shadow var(--dur) var(--ease);
}
.stat:hover { transform: translateY(-2px); box-shadow: var(--shadow); }

.stat__label {
    font-size: 12.5px;
    color: var(--text-3);
    /* 右上角可能有一个图标（30~34px），标签不许钻到它下面 */
    padding-right: 6px;
}

.stat__value {
    font-size: 28px;
    font-weight: 700;
    letter-spacing: -.8px;
    line-height: 1.2;
    margin-top: 6px;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}

.stat__unit {
    font-size: 14px;
    font-weight: 500;
    color: var(--ink-400);
    margin-left: 3px;
    letter-spacing: 0;
}

.stat__hint { font-size: 12px; color: var(--text-3); margin-top: 8px; }

/* 趋势行：↑12.4% 较昨日。没有真实可比数据就别渲染这一行 ——
   编一个同比数字比不显示更糟（会被当真）。 */
.stat__trend {
    margin-top: 8px;
    font-size: 12px;
    color: var(--text-3);
    display: flex;
    align-items: center;
    gap: 4px;
    font-variant-numeric: tabular-nums;
}

.stat__trend--up { color: var(--success); }
.stat__trend--down { color: var(--danger); }

/* 右上角图标槽：装饰性的，不承载信息（信息都在数字与标签里） */
.stat__icon {
    position: absolute;
    top: var(--sp-4);
    right: var(--sp-4);
    width: 34px;
    height: 34px;
    border-radius: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--c1-soft);
    color: var(--c1);
    font-size: 17px;
}

.stat__icon--ok { background: var(--c2-soft); color: var(--c2); }
.stat__icon--warn { background: var(--c3-soft); color: var(--c3); }
.stat__icon--danger { background: var(--c4-soft); color: var(--c4); }

/* 需要引人注意的指标卡：左侧一条色带 + 数字跟着变色。
   全站少数几处"数字可以有颜色"的地方之一 —— 因为它被显式标注过。 */
.stat--accent { border-left: 3px solid var(--c1); }
.stat--accent .stat__value { color: var(--brand-700); }
.stat--accent.stat--ok { border-left-color: var(--c2); }
.stat--accent.stat--ok .stat__value { color: var(--success); }
.stat--accent.stat--warn { border-left-color: var(--c3); }
.stat--accent.stat--warn .stat__value { color: var(--warning); }
.stat--accent.stat--danger { border-left-color: var(--c4); }
.stat--accent.stat--danger .stat__value { color: var(--danger); }

/* ============================ 按钮 ============================ */
/* 高度仍然用固定的 34px（不是 padding 撑）—— 表格行内的按钮要和行高对齐，
   一行里出现两种按钮高度会立刻看出没对齐。 */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    height: 34px;
    padding: 0 var(--sp-4);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--text);
    font-size: 13.5px;
    font-weight: 500;
    font-family: inherit;
    cursor: pointer;
    white-space: nowrap;
    text-decoration: none;              /* a.btn 也走这套样式，不带下划线 */
    transition: background var(--dur-fast) var(--ease),
                border-color var(--dur-fast) var(--ease),
                box-shadow var(--dur-fast) var(--ease),
                color var(--dur-fast) var(--ease);
}

.btn:hover:not(:disabled) { background: var(--ink-50); border-color: var(--ink-400); color: var(--ink-900); }
/* 按压缩一下：主流后台的通用反馈，比换底色更"实" */
.btn:active:not(:disabled) { transform: scale(.975); }
.btn:disabled { opacity: .5; cursor: not-allowed; }

/* 主操作 = 政务蓝填充。hover 时**变亮**（而不是变暗）+
   一层同色外发光：这是原型的手感，"下一步点哪"一眼可见。 */
.btn--primary {
    background: var(--brand-600);
    border-color: var(--brand-600);
    color: #fff;
    box-shadow: 0 1px 2px rgba(37, 99, 235, .2);
}

.btn--primary:hover:not(:disabled) {
    background: var(--brand-500);
    border-color: var(--brand-500);
    color: #fff;
    box-shadow: 0 8px 20px rgba(37, 99, 235, .24);
}
.btn--primary:active:not(:disabled) {
    background: var(--brand-700);
    border-color: var(--brand-700);
}

.btn--danger { background: var(--danger); border-color: var(--danger); color: #fff; }
.btn--danger:hover:not(:disabled) { background: #991b1b; border-color: #991b1b; color: #fff; }

/* .btn--ghost 是"次要动作"（原型里的 .btn--text）：无边框无底色，
   只在 hover 时浮出一层淡蓝底。表格行内的操作列大量用它。 */
.btn--ghost { background: transparent; border-color: transparent; color: var(--brand-600); }
.btn--ghost:hover:not(:disabled) { background: var(--brand-50); border-color: transparent; color: var(--brand-700); }

.btn--sm { height: 28px; padding: 0 var(--sp-3); font-size: 12.5px; }
.btn--block { width: 100%; }

.btn-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; }

/* ============================ 表格 ============================ */
.table-wrap { overflow-x: auto; }

/* ★ 宽表（列多时）：给表格一个下限宽度，让它**横向滚动**，
   而不是把每一列挤成"一个字一行"。

   症状（实测）：16 列的台账表在 1560px 宽的屏上，
   「单位名称」竖着排成一行字 —— 看上去像内容坏了，
   实际上只是 `width:100%` + `border-collapse` 把列压扁了。
   横向滚动才是数据表的正确形态（Excel 也是这么滚的）。

   `white-space: nowrap` 让长文本（机构名、企业名）保持一行，
   表格自然超出容器宽度 → 触发 `.table-wrap` 的横向滚动。 */
table.table.is-wide { min-width: 1400px; }
table.table.is-wide td,
table.table.is-wide th { white-space: nowrap; }
table.table.is-wide td .hint { white-space: nowrap; }

table.table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13.5px;
}

/* 表头 = 浅灰底一行小字（原型写法）。
   上一版靠"字距拉开 + 深色降级"来弱化表头，这一版直接用 ink-50 底色，
   底色本身就把它和正文行分开了，不必再靠字距。 */
table.table th {
    text-align: left;
    padding: 11px 14px;
    background: var(--ink-50);
    border-bottom: 1px solid var(--ink-200);
    color: var(--ink-600);
    font-weight: 600;
    font-size: 12.5px;
    white-space: nowrap;
}

table.table td {
    padding: 12px 14px;
    border-bottom: 1px solid var(--ink-100);
    vertical-align: middle;
    color: var(--ink-700);
}

/* 行悬停：淡蓝底。上一版还叠了一条 3px 的左侧导轨 ——
   在圆角卡片 + 柔阴影这套语言里，导轨会显得像"没擦干净的边"，
   底色已经足够把行分开。 */
table.table tbody tr { transition: background var(--dur-fast) var(--ease); }
table.table tbody tr:hover { background: var(--brand-50); }
table.table tbody tr:last-child td { border-bottom: none; }

/* 数字列：靠 tabular-nums 对齐，不再借等宽字体（原型做法） */
.table__num { text-align: right; font-variant-numeric: tabular-nums; font-size: 13.5px; }
.table__actions { white-space: nowrap; text-align: right; }

/* ============================ 徽标 ============================ */
/* 胶囊形（原型写法）：3px 10px + 999px 圆角。
   上一版是 4px 小圆角 + 1px 8px，贴在表格里偏"标签"；
   胶囊在"浅底深字"的配色下更像状态，也更符合主流后台的观感。 */
.badge {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 3px 10px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 500;
    line-height: 1.6;
    background: var(--ink-100);
    color: var(--ink-600);
    white-space: nowrap;
}

.badge--ok { background: var(--success-bg); color: var(--success); }
.badge--warn { background: var(--warning-bg); color: var(--warning); }
.badge--danger { background: var(--danger-bg); color: var(--danger); }
.badge--info { background: var(--info-bg); color: var(--info); }

/* 失效 / 已驳回。用中性灰而不是红色：驳回是流程的正常分支（材料不全很常见），
   画成红色会让机构以为出了事故；真正的错误用 badge--danger。 */
.badge--off { background: var(--ink-100); color: var(--text-3); }

/* 「题目种类」这类**自由文本**标签。刻意不复用 .badge —— 徽标的配色是被
   TONES 表管着的（ok=做成了 / danger=出事了），而种类只是个分类名，
   给它配任何语义色都是在暗示某种状态。所以单独一个中性描边样式。 */
.chip {
    display: inline-flex;
    align-items: center;
    padding: 2px 9px;
    border: 1px solid var(--ink-200);
    border-radius: var(--r-sm);
    font-size: 12px;
    line-height: 18px;
    color: var(--text-2);
    background: var(--surface-1);
    white-space: nowrap;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ============================ 表单 ============================ */
.form-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: var(--sp-4);
}

.field { display: flex; flex-direction: column; gap: 6px; }
.field--full { grid-column: 1 / -1; }

.field > label {
    font-size: 13px;
    color: var(--text-2);
    font-weight: 500;
}

.field .req { color: var(--danger); margin-left: 2px; }

/*表单控件统一样式。
   ★ 用`:not()` 而不是枚举 `type="text"`（10-07 轮修的一类隐蔽事故）。
     原来这里写的是 `input[type="text"], input[type="password"], …` 一串枚举。
     **属性选择器匹配的是 HTML 源码里有没有写那个属性**，不是DOM 计算后的值——
     `<input id="name">`（压根没写 `type`）**不匹配 `input[type="text"]`**，
     整条规则直接对它不命中，于是它退回浏览器默认样式：无边框、无高度、
     在 `display:flex` 的 `.field` 里被 `align-items:stretch` 拉成撑满整行。
     症状是「同一个表单里只有第一个输入框坏了、下面几个都正常」，
     因为只有它漏写了 `type` —— 这种"局部正常"最容易让人怀疑是布局问题
     （本项目就曾误判过一次）。而且它是**静默**的：没有报错，只是看起来不对。

     所以这里反过来：凡是**排除掉的少数几个非文本控件**（checkbox / radio /
     file / range / color）之外的input，全部纳入。将来新增任何 input
     都不需要在这里登记一次，漏写 `type` 也不会中招。
     仍然保留了 `type="text"` 等显式写法 —— 双保险，两条都命中。 */
input:not([type]),
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]):not([type="color"]),
input[type="text"], input[type="password"], input[type="number"],
input[type="date"], input[type="datetime-local"], input[type="search"],
input[type="email"], input[type="tel"], input[type="url"], select, textarea {
    height: 34px;
    padding: 0 var(--sp-3);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    font-size: 13.5px;
    font-family: inherit;
    color: var(--text);
    background: var(--surface);
    width: 100%;
    transition: border-color var(--dur-fast) var(--ease-out),
                box-shadow var(--dur-fast) var(--ease-out);
}

/* 文件选择框：**不能**套用上面那条。
   它天生就不该有 34px 固定高度和 `width:100%`（那会把「选择文件」按钮拉成
   一整行宽，看着像一个禁用掉的文本框），而且 `padding:0` 会让按钮贴边。
   这里显式覆盖成浏览器默认的紧凑形态 + 只给一个虚线框作为可点区域提示。
   同理`color`/`range` 也不该被当文本框处理，见下方。 */
input[type="file"] {
    height: auto;
    padding: 5px var(--sp-2);
    border: 1px dashed var(--line);
    border-radius: var(--r-sm);
    background: var(--surface-2);
    font-size: 13px;
    color: var(--text-2);
    width: 100%;
}

input[type="file"]::file-selector-button {
    height: 24px;
    margin-right: var(--sp-2);
    padding: 0 var(--sp-3);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--text);
    font-family: inherit;
    font-size: 12.5px;
    cursor: pointer;
}

input[type="file"]:hover::file-selector-button { border-color: var(--signal-500); }

/* 取色器：原生控件自带一个色块方框，给固定高度会把它压扁 */
input[type="color"] {
    width: 44px;
    height: 30px;
    padding: 2px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface);
    cursor: pointer;
}

/* 区间滑块：宽度交给布局（通常 100%），高度必须放开，
   且 padding 会让滑轨左右缩进、thumb 位置失真 */
input[type="range"] {
    width: 100%;
    height: auto;
    padding: 0;
    border: none;
    background: transparent;
    accent-color: var(--signal-500);
}

/* 数字类输入走等宽：工号、金额、分值竖排时才对得齐 */
input[type="number"], input[type="date"], input[type="datetime-local"], input[type="tel"] {
    font-family: var(--mono);
    font-variant-numeric: tabular-nums;
}

textarea { height: auto; min-height: 84px; padding: var(--sp-2) var(--sp-3); resize: vertical; }

input:focus, select:focus, textarea:focus {
    outline: none;
    border-color: var(--signal-500);
    box-shadow: 0 0 0 3px var(--signal-100);
}
input:focus-visible, select:focus-visible, textarea:focus-visible {
    outline: 2px solid var(--signal-500);
    outline-offset: 1px;
}

input:disabled, select:disabled, textarea:disabled {
    background: var(--surface-2);
    color: var(--text-3);
    cursor: not-allowed;
}

.field__hint { font-size: 12px; color: var(--text-3); }
.field__error { font-size: 12px; color: var(--danger); }

/* 复选框行。整行可点（label 包住 input），说明文字跟在后面。
   checkbox 被上面那条大规则的 `:not()` 明确排除掉了（对它套height:34px
   会变成一个34px 高的大方块），所以这里单独给尺寸。 */
.check {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-2);
    font-size: 13.5px;
    color: var(--text-2);
    cursor: pointer;
    margin-top: var(--sp-3);
}
/* 渠道被平台关掉时的那一格（10-19 第 22 轮）。
 * ★ 只降透明度不够：`disabled` 的复选框本身就变灰了，
 *   而那说明不了"为什么点不了"。真正的解释在旁边的 `field__hint` 里，
 *   这里只是让整行看起来"确实不可用"，而不是"我点歪了"。 */
.check.is-off { opacity: .55; }
.check.is-off label { cursor: not-allowed; }

.check input[type="checkbox"] {
    width: 15px;
    height: 15px;
    margin: 2px 0 0;
    flex: 0 0 auto;
    cursor: pointer;
    accent-color: var(--signal-700);
}

/* 勾选清单：弹窗里「从一批候选项里挑几个」用。
 * `.check` 自带 `margin-top`，在列表里会一行行错开，这里压掉。 */
.check-list {
    max-height: 260px;
    overflow-y: auto;
    border: 1px solid var(--line);
    border-radius: var(--r);
    padding: var(--sp-2) var(--sp-3);
    background: var(--surface);
}
.check-list .check {
    margin-top: var(--sp-2);
}
.check-list .check:first-child { margin-top: 0; }
.check-list .check input[type="checkbox"] { margin-top: 1px; }
.check-list .check span { line-height: 1.5; }

/* 工具条（筛选区） */
.toolbar {
    display: flex;
    align-items: flex-end;
    gap: var(--sp-3);
    flex-wrap: wrap;
    padding: var(--sp-3) var(--sp-4);
    background: var(--surface);
    border: 1px solid var(--line);
    border-left: 3px solid var(--ink-700);
    border-radius: var(--r);
    margin-bottom: var(--sp-4);
}

.toolbar .field { min-width: 150px; }
.toolbar__spacer { flex: 1 1 auto; }

/* 参数设置页：保存栏随滚动**粘在顶部**（10-19 第 21 轮）。
 *
 * ★ 为什么是 `sticky` 而不是 `fixed`：
 *   fixed 会脱离 `.content` 的滚动容器，压在侧栏上，而且滚到底部时
 *   它会盖住最后一行参数。sticky 只在本页内粘住，滚到底自动归位。
 * ★ 一定要给 `z-index` 与阴影：滚动时它下面是白色卡片，
 *   两块白贴在一起时看不出边界，用户会以为保存栏"粘进"了内容里。
 */
.toolbar--sticky {
    position: sticky;
    top: 0;
    z-index: 20;
    box-shadow: 0 8px 16px -12px rgba(15, 23, 42, .45);
}

/* 参数设置页的两个标签页。用下划线而不是按钮块：
 * 它切换的是**同一页里的两叠内容**，不是两个不同的页面 ——
 * 按钮块会让人以为点了会跳转。 */
.set-tabs {
    display: flex;
    gap: 2px;
    margin-bottom: var(--sp-4);
    border-bottom: 1px solid var(--line);
}
.set-tab {
    padding: 9px 18px;
    border: 0;
    background: transparent;
    cursor: pointer;
    font: inherit;
    font-size: 14px;
    color: var(--ink-500);
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
}
.set-tab:hover { color: var(--ink-900); }
.set-tab.is-active {
    color: var(--ink-900);
    font-weight: 600;
    border-bottom-color: var(--brand-600);
}

/* 模拟考有效期的两块配置（10-19 第 25 轮美化）。
 *
 * ★ 为什么不用 `.card` 嵌套：外层已经是卡片，内层再套一层卡片会得到
 *   两层边框 + 两层 padding，而在设置页这种"一屏很多卡片"的布局里，
 *   嵌套卡片看起来像"卡片没对齐"。
 * ★ 为什么按钮行不再用 `.toolbar`：`.toolbar` 有 `border-left: 3px`
 *   与完整边框，而它里面只有一个 `toolbar__spacer` + 两个按钮时，
 *   flex 把 spacer 撑满 → 左边那圈边框渲染成一个高瘦的 `[` 形框，
 *   什么也不表示。这是第 25 轮用户说"看着太乱"的主要来源。 */
.ret-block {
    border: 1px solid var(--line);
    border-radius: var(--r);
    padding: 14px 16px;
}
.ret-block + .ret-block { margin-top: 12px; }
.ret-block__head {
    display: flex;
    align-items: baseline;
    gap: 10px;
    flex-wrap: wrap;
    margin-bottom: 6px;
}
.ret-block__head b { font-size: 14.5px; }

/* 当前生效值：浅底一条，与表单字段明确分开 */
.ret-sum {
    display: flex;
    align-items: baseline;
    gap: 10px;
    flex-wrap: wrap;
    margin-top: 12px;
    padding: 9px 12px;
    background: var(--ink-100);
    border-radius: var(--r);
    font-size: 13px;
    color: var(--ink-700);
}
.ret-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 12px;
}

/* 底部"临时清理"那一行：与上面两块隔开，但不要再多一圈边框 */
.ret-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    margin-top: 16px;
    padding-top: 14px;
    border-top: 1px solid var(--line);
}

/* 表单底部的按钮行（不带筛选区的卡片背景），按钮靠右 */
.toolbar--end {
    justify-content: flex-end;
    background: transparent;
    border: 0;
    padding: var(--sp-4) 0 0;
    margin-bottom: 0;
}

/* ============================ 批量操作条 ============================
 *
 * 有勾选时才出现（approval.js::paintBatchBar 负责显隐）。
 * 刻意做成**与列表同宽的横条**而不是浮在表格上方的小气泡：
 * 批量是不可逆操作，"当前将要改动几项" 必须在提交前一直可见，
 * 浮层会被表格滚动带出视口 —— 而那正是用户点「确认」的时刻。
 */
.batchbar {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    flex-wrap: wrap;
    padding: var(--sp-2) var(--sp-4);
    background: var(--signal-50);
    /* ★ 边框用 `--signal-100`：色板里**没有** `--signal-200`，
       写错变量名不会报错、也不会高亮，只是边框静默变成透明 ——
       表现是「批量条好像没有框」，很难归因。 */
    border: 1px solid var(--signal-100);
    border-left: 3px solid var(--signal-600);
    border-radius: var(--r);
    margin-bottom: var(--sp-3);
}

.batchbar__count { font-size: 13px; color: var(--text); }
.batchbar__count b { font-size: 15px; font-variant-numeric: tabular-nums; }

/* 勾选列里的 checkbox 要跟行高对齐 —— 表格 td 是 vertical-align: middle，
   而原生 checkbox 的基线会偏下，不加这句就是「框比文字低半个身位」。 */
.table input[type="checkbox"].js-pick,
.table input[type="checkbox"].js-pick-all {
    width: 16px;
    height: 16px;
    vertical-align: middle;
    cursor: pointer;
}

/* ============================ 分页 ============================ */.pagination {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--sp-2);
    padding: var(--sp-3) var(--sp-4);
    border-top: 1px solid var(--line-2);
    font-size: 13px;
    color: var(--text-2);
}

.pagination__info { margin-right: auto; font-variant-numeric: tabular-nums; }

/* 分页器右侧的「每页 N 条」。高度压到 28px：它是分页器的一部分，
   跟旁边 32px 的按钮做同一视觉重量才不会显得像另一个控件掉进来了。 */
.pagination__size {
    display: inline-flex; align-items: center; gap: 6px;
    margin-right: var(--sp-1); color: var(--text-3); font-size: 12.5px;
    /* ★ 必须 nowrap：分页器是 flex 容器且宽度吃紧，
       没有它时「每页」两字会被压成上下两行 ——
       症状是「每页」竖着排在左边，下拉框在右边，
       中间还夹一条竖线似的缝，而布局本身不报错。 */
    white-space: nowrap;
}
.pagination__size select {
    height: 28px; padding: 0 24px 0 9px;
    border: 1px solid var(--line); border-radius: var(--r-sm);
    background: var(--surface); color: var(--text-2);
    font: inherit; font-size: 12.5px; cursor: pointer;
}
.pagination__size select:focus-visible { outline: 2px solid var(--signal-500); outline-offset: -2px; }

/* ============================ 空态 / 加载 ============================ */
.empty {
    padding: var(--sp-6) var(--sp-4);
    text-align: center;
    color: var(--text-3);
    font-size: 13.5px;
}

.empty__title { color: var(--text-2); font-size: 14px; margin-bottom: 4px; }

/* 空态图标：内联线性 SVG（ui.js 注入），只表达"这里是空的" */
.empty__glyph {
    display: block;
    margin: 0 auto var(--sp-3);
    width: 44px;
    height: 44px;
    color: var(--ink-300);
}

/* ============================ 提示条 ============================
   用于「这件事的状态不是数字能表达的」场合。
   典型是行政端的确认事项：功能没上线时**必须**说清楚，不能显示 0。
   两条变体刻意做成不同色系 —— notice--off 用中性灰而不是警示色，
   因为它表示的是「未上线」而非「出问题了」。 */
.notice {
    padding: 10px 14px;
    border-radius: var(--r-sm);
    font-size: 13.5px;
    line-height: 1.65;
    border: 1px solid var(--line-2);
    border-left: 3px solid var(--ink-400);
    background: var(--surface-2);
    color: var(--text-2);
}

.notice + .notice { margin-top: var(--sp-2); }

.notice--todo {
    background: var(--warning-bg);
    border-color: #fde68a;
    border-left-color: var(--warning);
    color: var(--text);
}

.notice--warn {
    background: var(--warning-bg);
    border-color: #fde68a;
    border-left-color: var(--warning);
    color: var(--text);
}

.notice--ok {
    background: var(--success-bg);
    border-color: #a7f3d0;
    border-left-color: var(--success);
    color: var(--text);
}

.notice--off {
    background: var(--surface-2);
    border-color: var(--line-2);
    border-left-color: var(--ink-400);
    color: var(--text-3);
}

.notice--info {
    background: var(--info-bg);
    border-color: #bcd3ff;
    border-left-color: var(--info);
    color: var(--text);
}

.notice b { color: var(--text); font-weight: 600; }
.notice--off b { color: var(--text-2); }

.skeleton {
    background: linear-gradient(90deg, var(--line-2) 25%, var(--ink-50) 50%, var(--line-2) 75%);
    background-size: 200% 100%;
    animation: sk 1.2s infinite linear;
    border-radius: var(--r-sm);
    height: 14px;
}

@keyframes sk { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

/* ============================ Toast ============================ */
.toast-host {
    position: fixed;
    top: 20px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 9999;
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    pointer-events: none;
}

.toast {
    position: relative;
    min-width: 220px;
    max-width: 460px;
    padding: 10px var(--sp-4) 10px 30px;
    border-radius: var(--r);
    background: var(--surface);
    border: 1px solid var(--line);
    box-shadow: var(--shadow-lg);
    font-size: 13.5px;
    animation: toast-in .18s var(--ease-out);
}

/* 左侧小方标：靠颜色就能分辨类型，不必读文字 */
.toast::before {
    content: '';
    position: absolute;
    left: 12px;
    top: 50%;
    margin-top: -4px;
    width: 8px; height: 8px;
    border-radius: 50%;
    background: var(--ink-400);
}
.toast--ok { border-left: 3px solid var(--success); }
.toast--ok::before { background: var(--success); }
.toast--err { border-left: 3px solid var(--danger); }
.toast--err::before { background: var(--danger); }
.toast--warn { border-left: 3px solid var(--warning); }
.toast--warn::before { background: var(--warning); }
.toast--info { border-left: 3px solid var(--brand-500); }
.toast--info::before { background: var(--brand-500); }

@keyframes toast-in {
    from { opacity: 0; transform: translateY(-8px); }
    to { opacity: 1; transform: translateY(0); }
}

/* ============================ Modal ============================ */
.modal-mask {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, .45);
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
    z-index: 9000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--sp-4);
    animation: mask-in var(--dur) var(--ease-out);
}

.modal {
    position: relative;
    background: var(--surface);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-lg);
    width: 100%;
    max-width: 560px;
    max-height: 88vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    animation: modal-in var(--dur) var(--ease);
}

/* 顶部 3px 主色线：弹窗打开时"当前操作在这"的锚点。
   `.modal` 自带 overflow: hidden，所以这条线会被圆角裁掉两端，
   不会在 14px 圆角外露出直角。 */
.modal::before {
    content: '';
    position: absolute;
    left: 0; right: 0; top: 0;
    height: 3px;
    background: var(--signal-500);
}

.modal--wide { max-width: 860px; }

.modal__head {
    padding: var(--sp-4) var(--sp-5);
    border-bottom: 1px solid var(--line-2);
    background: var(--surface-2);
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.modal__title {
    font-size: 16px;
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: 9px;
}
.modal__title::before {
    content: '';
    width: 3px; height: 15px;
    background: var(--brand-500);
    border-radius: 2px;
    flex: 0 0 3px;
}

.modal__close {
    border: none;
    background: transparent;
    font-size: 20px;
    line-height: 1;
    color: var(--text-3);
    cursor: pointer;
    padding: 0 4px;
    width: 32px;
    height: 32px;
    border-radius: var(--r-sm);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.modal__close:hover { background: var(--ink-100); color: var(--text); }

.modal__body { padding: var(--sp-5); overflow-y: auto; }

.modal__foot {
    padding: var(--sp-3) var(--sp-5);
    border-top: 1px solid var(--line-2);
    background: var(--surface-2);
    display: flex;
    justify-content: flex-end;
    gap: var(--sp-2);
}

@keyframes mask-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes modal-in {
    from { opacity: 0; transform: translateY(10px) scale(.994); }
    to { opacity: 1; transform: none; }
}

/* ============================ 登录页 ============================ */
/* 深藏青底 + 两团低透明蓝紫光晕。
   上一版是"蓝图网格 + 钢蓝 / 测绘橙双光晕"，橙色已撤（全站只有一个强调色）。
   网格底纹一并去掉 —— 满屏 32px 细网格在 2K 屏上会变成摩尔纹。 */
.login-page {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: var(--ink-950);
    background-image: var(--paper-grid-dark);
    background-size: 32px 32px;
    background-image:
        var(--paper-noise),
        radial-gradient(760px 420px at 82% -8%, rgba(37, 99, 235, .30), transparent 62%),
        radial-gradient(680px 460px at 4% 106%, rgba(139, 92, 246, .22), transparent 60%),
        var(--paper-grid-dark);
    background-size: 140px 140px, auto, auto, 32px 32px;
    padding: var(--sp-4);
}

.login-card {
    position: relative;
    width: 100%;
    max-width: 400px;
    background: var(--surface);
    border-radius: var(--r-lg);
    box-shadow: 0 24px 60px rgba(0, 0, 0, .38);
    padding: var(--sp-6);
}

.login-card__brand {
    text-align: center;
    margin-bottom: var(--sp-5);
    padding-bottom: var(--sp-4);
    border-bottom: 1px solid var(--line-2);
}

.login-card__brand b {
    display: block;
    font-size: 20px;
    color: var(--ink-900);
    letter-spacing: .02em;
}

.login-card__brand span {
    display: block;
    font-size: 12px;
    color: var(--text-3);
    margin-top: 6px;
}

/* 登录方式切换（index.html 的 .login-tabs / .login-tab）。
   原先这两个类名在 app.css 里没有定义 —— 标签是裸的、没有当前态，
   这次补上（并保留与原「两段式开关」一致的交互语言）。 */
.login-tabs {
    display: flex;
    gap: 4px;
    padding: 4px;
    margin-bottom: var(--sp-5);
    background: var(--surface-2);
    border: 1px solid var(--line);
    border-radius: var(--r);
}

.login-tab {
    flex: 1 1 0;
    appearance: none;
    border: 0;
    background: transparent;
    font: inherit;
    font-size: 13.5px;
    padding: 7px 12px;
    border-radius: var(--r-sm);
    color: var(--text-2);
    cursor: pointer;
    transition: background var(--dur-fast) var(--ease-out),
                color var(--dur-fast) var(--ease-out);
}
.login-tab:hover { color: var(--text); }
.login-tab.is-active {
    background: var(--surface);
    color: var(--brand-600);
    font-weight: 600;
    box-shadow: var(--shadow-sm);
}

/* 验证码行（输入框 + 获取按钮）。原先也没有定义：输入框与外挂按钮
   会各自占满一行宽度，挤成上下两条。 */
.code-row {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}
.code-row > input { flex: 1 1 auto; min-width: 0; }
.code-row > .btn { flex: 0 0 auto; }

.login-card .field { margin-bottom: var(--sp-4); }

.login-card__foot {
    margin-top: var(--sp-4);
    padding-top: var(--sp-4);
    border-top: 1px solid var(--line-2);
    font-size: 12px;
    color: var(--text-3);
    text-align: center;
    line-height: 1.8;
}

.login-role-hint {
    margin-top: var(--sp-4);
    padding: var(--sp-3);
    background: var(--signal-50);
    border: 1px solid var(--brand-100);
    border-left: 3px solid var(--brand-500);
    border-radius: var(--r-sm);
    font-size: 12px;
    color: var(--ink-700);
    line-height: 1.9;
}

/* ============================ 进度条 ============================ */
.progress-row {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
}

.progress {
    flex: 1 1 auto;
    min-width: 70px;
    height: 6px;
    background: var(--ink-100);
    border-radius: 3px;
    overflow: hidden;
}

.progress__bar {
    height: 100%;
    width: 0;
    background: var(--brand-500);
    border-radius: 3px;
    transition: width .3s var(--ease-out);
}

.progress__bar--ok { background: var(--success); }
.progress__bar--warn { background: var(--warning); }

/* 发送中的进度条：斜纹在滚动，表示"它还在动"（10-19 第 22 轮）。
 * ★ 为什么需要这个动画：进度是**轮询**来的（3 秒一次），
 *   两次之间数字不动 —— 光看数字和"卡住了"完全一样。
 *   条纹滚动是唯一能说明"后台还在跑"的视觉信号。
 * ★ 用 `background-image` 叠加而不是换背景色：进度条本身仍按
 *   ok/fail 的颜色走，条纹只是加一层纹理，不改语义。 */
@keyframes progress-stripes {
    from { background-position: 0 0; }
    to   { background-position: 28px 0; }
}
.progress__bar--live {
    background-image: linear-gradient(45deg,
        rgba(255, 255, 255, .28) 25%, transparent 25%, transparent 50%,
        rgba(255, 255, 255, .28) 50%, rgba(255, 255, 255, .28) 75%,
        transparent 75%, transparent);
    background-size: 28px 28px;
    animation: progress-stripes .8s linear infinite;
}

/* ---------------------------------------------------------------
 * 任务进度（10-28 第 27 轮）
 *
 * 建在既有的 `.progress` 之上，只补"标题 + 百分比 + 说明"这几行，
 * 与蓝图编辑页、通知页那两处的进度条保持同一套视觉 ——
 * 同一个系统里出现两种进度条，用户会以为它们含义不同。
 * --------------------------------------------------------------- */
.task-progress {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 10px 12px;
    margin: 10px 0;
    border: 1px solid var(--line);
    border-radius: var(--r);
    background: var(--surface);
}

.task-progress__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-3);
    font-size: 13px;
    color: var(--text);
}

.task-progress__pct {
    font-variant-numeric: tabular-nums;
    color: var(--ink-500);
    flex: 0 0 auto;
}

.task-progress__hint {
    font-size: 12px;
    color: var(--ink-500);
    line-height: 1.6;
}

.task-progress__hint:empty { display: none; }

.task-progress.is-done { border-color: #a7f3d0; background: #f0fdf9; }
.task-progress.is-fail { border-color: #fecaca; background: #fef2f7; }

/* 弹窗里的"常驻进度槽"（目录批量导入）。
 *
 * ★ 为什么 sticky 在底部：一次导入几十个文件要十几分钟，结果列表会不断变长，
 *   排在上面的进度条几分钟后就被顶出可视区 —— 而"跑到第几个文件了"是
 *   唯一需要**全程**看得见的东西。
 * ★ 负左右边距 + 正内边距：`.modal__body` 有 `var(--sp-5)` 的内边距，
 *   不"出血"的话进度条两边会各缩进一格，看起来像是没对齐的临时元素。
 * ★ `:empty` 时不留白：没开始导入时这块应该是**看不见的**，
 *   而不是先空出一截。 */
.modal-progress {
    position: sticky;
    bottom: 0;
    z-index: 2;
    background: var(--surface);
    margin: 8px calc(var(--sp-5) * -1) 0;
    padding: 0 var(--sp-5);
}
.modal-progress:not(:empty) { padding-bottom: 4px; }

/* 批量测试的负载面板（11-01 第 49 轮）。
 *
 * ★ 为什么要它：长任务只有一个百分比时，看不出"它是在跑，还是卡住了"。
 *   面板把**服务器此刻**的连接池 / 数据库线程 / 内存 / CPU 摆出来，
 *   一眼就能判断该继续等还是该去查锁。
 * ★ 每个数字取不到时显示"—"，不显示 0 —— "采不到"和"真的是 0"
 *   是两回事，混起来会误导排查。 */
.metrics {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
    gap: 8px;
}
.metric {
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--surface);
    padding: 8px 10px;
    min-width: 0;
}
.metric__k {
    font-size: 12px;
    color: var(--text-3);
    line-height: 1.4;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.metric__v {
    font-size: 18px;
    font-weight: 600;
    color: var(--text);
    line-height: 1.35;
    font-variant-numeric: tabular-nums;
}
.metric__v small {
    font-size: 12px;
    font-weight: 400;
    color: var(--text-3);
    margin-left: 2px;
}
/* 池子打满 / 线程数飙高：颜色提醒，但**不隐藏数字** —— 要看的正是它。 */
.metric--warn {
    border-color: var(--warning);
    background: var(--warning-bg);
}
.metric--warn .metric__v { color: var(--warning); }

/* 没有分母的"在跑"：一块 30% 宽的条来回滑。
 * ★ 不用"转到 100% 再重来"那种：那看起来像"完成了又重做"，
 *   而这东西的真实含义是"还在做、不知道还要多久"。 */
@keyframes task-indeterminate {
    from { transform: translateX(-110%); }
    to   { transform: translateX(350%); }
}

.progress__bar--indet {
    width: 30% !important;
    animation: task-indeterminate 1.15s var(--ease-out) infinite;
}

/* 卡片头里的按钮靠右（`space-between` 只管首尾，中间那些会均分） */
.card__head > .btn { margin-left: auto; }

/* ============================ 其他 ============================ */
.divider { height: 1px; background: var(--line-2); margin: var(--sp-4) 0; }

.text-2 { color: var(--text-2); }
.text-3 { color: var(--text-3); }
.text-sm { font-size: 12.5px; }
.text-danger { color: var(--danger); }
.text-success { color: var(--success); }
.mt-0 { margin-top: 0; }
.mt-2 { margin-top: var(--sp-2); }
.mt-4 { margin-top: var(--sp-4); }
.mb-0 { margin-bottom: 0; }
.mb-2 { margin-bottom: var(--sp-2); }
.mb-4 { margin-bottom: var(--sp-4); }
.nowrap { white-space: nowrap; }

/* 响应式：窄屏把侧栏收成顶部横条 */
@media (max-width: 860px) {
    .app-shell { flex-direction: column; }
    .sidebar {
        width: 100%;
        flex: none;
        height: auto;
        position: relative;
        flex-direction: column;
        box-shadow: inset 0 -1px 0 rgba(255, 255, 255, .06);
    }
    .sidebar__brand { padding: var(--sp-4); }
    .sidebar__mark { flex-basis: 26px; width: 26px; height: 26px; }
    .sidebar__nav { display: flex; overflow-x: auto; gap: 4px; padding-bottom: var(--sp-2); }
    .sidebar__link { white-space: nowrap; margin-bottom: 0; }
    .sidebar__group { display: none; }
    .content { padding: var(--sp-3); }
    .topbar { padding: 0 var(--sp-3); }
}

/* ==========================================================================
   P7 新增组件
   --------------------------------------------------------------------------
   都是「原设计系统里没有、但新页面真的需要」的东西：
   播放器、作答选项、倒计时、键值表、查验码、通知卡片。
   一律只用 :root 里的令牌，不引入新的颜色常量 ——
   组件一多，最容易失控的就是"这里随手写了 #3a3a3a"。
   ========================================================================== */

/* ---- 键值表：已由 .kv-list（Chart.kv 产出，见文末「图表与统计」一节）取代 ----
   旧的 .kv 是「132px 固定列 + 左键右值」的栅格，窄屏会整体堆成两行一格，
   与新的扁平分隔线行（.kv-list__row）并存时一眼能看出是两代东西。
   全站 5 处调用点（admin/audit · admin/exams ×2 · admin/paper · health/confirm-paper ·
   student/profile）已迁到 Chart.kv，此处只留说明，**不要再新增 .kv 用法**。 */

/* ---- 等宽小字（查验码 / 编号 / 哈希）：tabular-nums 让数字等宽对齐 ---- */
.mono {
    font-family: var(--mono);
    font-variant-numeric: tabular-nums;
    letter-spacing: .5px;
}

/* ---- 查验码盒子 ---- */
.code-box {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-3);
    padding: 10px var(--sp-4);
    border: 1px dashed var(--brand-500);
    border-radius: var(--r);
    background: var(--brand-50);
}
.code-box__label { font-size: 12px; color: var(--text-3); }
.code-box__value { font-family: var(--mono); font-size: 18px; letter-spacing: 2px; color: var(--brand-800); }

/* ---- 播放器 ---- */
.player { background: var(--ink-950); border-radius: var(--r); overflow: hidden; }
.player__video { display: block; width: 100%; max-height: 60vh; background: var(--ink-950); }
.player__bar {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    background: var(--surface-2);
    border: 1px solid var(--line); border-top: 0;
    border-radius: 0 0 var(--r) var(--r);
    font-size: 12.5px; color: var(--text-3);
    font-variant-numeric: tabular-nums;
}

/* ---- 作答选项 ---- */
.choice-list { display: flex; flex-direction: column; gap: var(--sp-2); }
/* 选项行 = 可点的整行（label）+ 右侧删除键。
   删除键刻意放在 label **外面**：放进 label 里点它会连带触发 label 的激活行为，
   症状是「删一个选项，顺手把正确答案的勾选也改了」——而且删完才知道。
   空白选项也占一行（`{A:'', C:'x'}` 这种缺口要能被看见），所以这里允许整行留空。 */
.choice-row { display: flex; align-items: flex-start; gap: var(--sp-2); }
.choice-row .choice { flex: 1 1 auto; }
.choice {
    display: flex; align-items: flex-start; gap: var(--sp-3);
    padding: 10px var(--sp-4);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface);
    cursor: pointer;
    transition: border-color var(--dur-fast) var(--ease-out),
                background var(--dur-fast) var(--ease-out);
}
.choice:hover { border-color: var(--brand-500); background: var(--brand-50); }
.choice__key {
    flex: none; width: 22px; height: 22px; line-height: 22px; text-align: center;
    border: 1px solid var(--line); border-radius: 50%;
    font-family: var(--mono); font-size: 12px; color: var(--text-2); background: var(--surface-2);
}
.choice__text { flex: 1 1 auto; }
/* 选中态用主色描边 + 实心标号：只靠背景色区分，在投影仪上会看不出来 */
.choice.is-picked { border-color: var(--signal-600); background: var(--signal-50); }
.choice.is-picked .choice__key { border-color: var(--signal-700); background: var(--signal-700); color: #fff; }
.choice.is-right { border-color: var(--success); background: var(--success-bg); }
.choice.is-right .choice__key { border-color: var(--success); background: var(--success); color: #fff; }
.choice.is-wrong { border-color: var(--danger); background: var(--danger-bg); }
.choice.is-wrong .choice__key { border-color: var(--danger); background: var(--danger); color: #fff; }

/* 题干：比正文稍大、行高放宽，长题干（本项目题目多是长句）才读得下去 */
.stem { font-size: 15px; line-height: 1.75; color: var(--text); }
.stem__no { color: var(--text-3); font-size: 13px; margin-right: var(--sp-2); font-family: var(--mono); }

/* ---- 考试倒计时 ----
   等宽 + 信号色：考场上它是"正在流逝"的那一个数，配得上唯一的强调色。 */
.timer {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 4px 12px; border-radius: var(--r-sm);
    background: var(--signal-50); color: var(--signal-800);
    border: 1px solid var(--signal-100);
    font-family: var(--mono); font-size: 15px; font-weight: 600;
    font-variant-numeric: tabular-nums;
}
/* 最后 5 分钟变红：这是考场上唯一需要"抢时间"的信号 */
.timer.is-urgent { background: var(--danger-bg); color: var(--danger); border-color: var(--danger); }

/* ---- 答题卡（题号网格），考场上用来跳题与看未答 ---- */
.grid-nav { display: flex; flex-wrap: wrap; gap: 6px; }
.grid-nav__item {
    width: 30px; height: 30px; line-height: 28px; text-align: center;
    border: 1px solid var(--line); border-radius: var(--r-sm);
    background: var(--surface); font-size: 12.5px; cursor: pointer;
    font-variant-numeric: tabular-nums; font-family: var(--mono);
    transition: border-color var(--dur-fast) var(--ease-out),
                background var(--dur-fast) var(--ease-out);
}
.grid-nav__item:hover { border-color: var(--ink-300); }
.grid-nav__item.is-done { border-color: var(--success); background: var(--success-bg); color: var(--success); }
.grid-nav__item.is-active { border-color: var(--signal-700); background: var(--signal-700); color: #fff; }

/* ---- 通知 / 消息卡片 ---- */
.msg-list { display: flex; flex-direction: column; }
.msg {
    padding: var(--sp-4);
    border-bottom: 1px solid var(--line-2);
}
.msg:last-child { border-bottom: 0; }
.msg.is-unread { background: var(--brand-50); }
.msg__head { display: flex; align-items: baseline; gap: var(--sp-2); }
.msg__title { font-weight: 600; flex: 1 1 auto; }
.msg__body { margin-top: 6px; color: var(--text-2); white-space: pre-wrap; }
.msg__foot { margin-top: var(--sp-2); display: flex; align-items: center; gap: var(--sp-3); font-size: 12.5px; color: var(--text-3); font-variant-numeric: tabular-nums; }
.msg__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--brand-500); flex: none; }

/* ---- 缩略图墙（通知图片） ---- */
.thumbs { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-2); }
.thumbs img {
    width: 84px; height: 84px; object-fit: cover;
    border: 1px solid var(--line); border-radius: var(--r-sm); cursor: zoom-in;
}

/* ---- 认定材料附件（学员端 kind.html）
   视觉语言沿用上面的 .thumbs（84px 缩略图 + 同样的圆角与描边），
   差别有两处，都是被PDF 逼出来的：
     · PDF 不能进 <img>（浏览器显示裂图），所以另给一个文件行；
     · 文件行必须有「查看/下载」入口 —— 图片是点开看，PDF 是跳新页。 */
.att {
    position: relative; display: flex; align-items: center; gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3); margin-top: var(--sp-2);
    border: 1px solid var(--line); border-radius: var(--r-sm);
    background: var(--surface);
}
.att--img { padding: 0; display: inline-flex; cursor: zoom-in; }
.att--img img { width: 84px; height: 84px; object-fit: cover; border-radius: var(--r-sm); display: block; }
.att__name {
    /* 文件名可能很长且是中文：省略而不是撑破布局，
       完整名放在 title 里（JS 已用原始 name渲染 title）。 */
    max-width: 220px; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; font-size: 13px;
}
.att__meta { font-size: 12px; color: var(--text-3); flex: 1; }
.att__dl { font-size: 12.5px; color: var(--brand-600); text-decoration: none; }
.att__dl:hover { text-decoration: underline; }
.att__x {
    /* 用 × 字符而不是图标字体：删除是最常见的操作，
       不值得为它引一套图标依赖。 */
    position: absolute; top: -6px; right: -6px; z-index: 1;
    width: 20px; height: 20px; line-height: 1; padding: 0;
    border-radius: 50%; border: 1px solid var(--line);
    background: var(--surface); color: var(--text-2);
    cursor: pointer; font-size: 14px;
}
.att__x:hover { color: var(--danger); border-color: var(--danger); }
/* 图片格里的 × 落在角上；文件行的 × 靠右排，避免盖住文件名 */
.att:not(.att--img) .att__x { position: static; margin-left: var(--sp-1); }
/* hover 描边用 --brand-100：色阶里没有 300 档（只有 50/100/500…900），
   写一个未定义的 --brand-300 会让这条声明静默失效 —— 表现为
   「hover 时边框一点变化都没有」，而没有任何报错。 */
.att:not(.att--img):hover { border-color: var(--brand-100); }

/* ---- 证书卡 ---- */
.cert {
    border: 1px solid var(--line);
    border-left: 3px solid var(--brand-500);
    border-radius: var(--r);
    background: var(--surface); padding: var(--sp-4);
    display: flex; flex-direction: column; gap: var(--sp-2);
}
.cert--revoked { background: var(--surface-2); border-style: dashed; border-left-style: solid; }
.cert__title { font-size: 15px; font-weight: 600; }
.cert__meta { font-size: 12.5px; color: var(--text-3); display: flex; flex-wrap: wrap; gap: var(--sp-3); }

/* ---- 迷你条形图（旧版手写实现，已由 .chart 取代）
   保留占位说明：三处报表页原本各写一份 .bar-row 内联网格，
   现在统一走 assets/js/chart.js 的 Chart.bars —— 一处定义、处处一致。
   这里的类名已全站无引用，但先留着一条注释，避免以后有人照着别处的
   截图重新发明它。 ---- */

/* ---- 工具条上的说明文字（避免每页各写一段 inline style） ---- */
.hint { font-size: 12.5px; color: var(--text-3); }
.hint--full { width: 100%; }

/* =========================================================================
   设置项行（参数设置页）
   -------------------------------------------------------------------------
   左「中文名 + 说明 + key」/ 右「控件」的固定两栏。为什么不用 form-grid：
   参数表里每一项的说明都是一两句中文，两栏网格下会被压成难读的窄条，
   而"这一项到底管什么"恰恰是运营唯一需要的判断依据。
   ========================================================================= */
.set-list { display: flex; flex-direction: column; }
.set {
    display: flex; align-items: flex-start; gap: var(--sp-5);
    padding: var(--sp-3) var(--sp-4);
    border-top: 1px solid var(--line-2);
}
.set:first-child { border-top: 0; }
.set:hover { background: var(--surface-2); }
.set__info { flex: 1 1 auto; min-width: 0; }
.set__name {
    font-size: 14px; font-weight: 600; color: var(--text);
    display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
}
.set__desc { font-size: 12.5px; color: var(--text-3); margin-top: 3px; line-height: 1.65; }
/* 「默认值 / 上次修改」这一行：小字、最下面、不抢视觉。
   这里**刻意不再放英文 key**（`video_gate.fail_mode` 之类）——
   它对运营是噪声，会让人怀疑自己进错了页面；要按 key 回溯的是开发与运维，
   他们看的是代码与审计日志，那里本来就有 key。 */
.set__key { margin-top: 5px; font-size: 11.5px; color: var(--text-3); }
.set__ctrl { flex: 0 0 280px; }
.set__note { font-size: 12px; color: var(--text-3); margin-top: 5px; line-height: 1.65; }
@media (max-width: 760px) {
    .set { flex-direction: column; gap: var(--sp-2); }
    .set__ctrl { flex: 1 1 auto; width: 100%; }
}

/* ---- 两段式开关（比下拉少一次点击，且当前状态一眼可见） ---- */
.seg {
    display: inline-flex; border: 1px solid var(--line);
    border-radius: var(--r); overflow: hidden; background: var(--surface);
}
.seg__item {
    appearance: none; border: 0; border-left: 1px solid var(--line);
    background: transparent; font: inherit; font-size: 13px;
    padding: 7px 16px; cursor: pointer; color: var(--text-2);
    transition: background var(--dur-fast) var(--ease-out),
                color var(--dur-fast) var(--ease-out);
}
.seg__item:first-child { border-left: 0; }
.seg__item:hover { background: var(--surface-2); }
.seg__item.is-on { background: var(--signal-700); color: #fff; font-weight: 600; }
.seg__item.is-on.is-keep { background: var(--ink-500); }
.seg__item:focus-visible { outline: 2px solid var(--signal-500); outline-offset: -2px; }

/* ---- 页签上的待办计数（红底白字的小圆角块） ----
   为什么红底而不是红字：`.seg__item.is-on` 是深色底白字，红字落在上面
   会糊掉；而「有多少条等着办」恰恰要看**当前选中那一项**上的数字
   —— 选中态最需要读得清的那一行，恰恰是红字最看不清的一行。
   为什么 0 时不加深色：0 不是「有 0 件事」，是「这一类没有待办」，
   画成红底会让三个页签一直红着，红色就失去了"有事"的含义。 */
.seg__n {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 20px; height: 18px; margin-left: 7px; padding: 0 6px;
    border-radius: 999px; background: var(--danger); color: #fff;
    font-size: 11.5px; font-weight: 600; line-height: 1;
    font-variant-numeric: tabular-nums;
}
.seg__n--zero { background: var(--ink-200); color: var(--text-3); font-weight: 500; }

/* ---- 输入框带单位后缀（「7」旁边写「天」，免得运营猜是周还是月） ---- */
.input-unit { position: relative; display: block; }
.input-unit > input { width: 100%; padding-right: 44px; }
.input-unit__suffix {
    position: absolute; right: 11px; top: 50%; transform: translateY(-50%);
    font-size: 12.5px; color: var(--text-3); pointer-events: none;
}

/* ---- 组卷规则矩阵（题型 × 题目种类 × 难度） ---- */
.bp-row {
    display: grid;
    grid-template-columns: minmax(120px, 1.3fr) repeat(3, minmax(78px, 1fr)) 34px;
    gap: 10px; align-items: start; margin-bottom: 10px;
}
.bp-row--head { font-size: 12px; color: var(--text-3); margin-bottom: 6px; }
.bp-row > select, .bp-row > .bp-cell > input { width: 100%; }

/* 每个格子下面那行「库里 N 道」。它才是这一页真正防事故的东西：
   配额超出题量时，数字当场变红，不必等到点保存。 */
.bp-cell__hint {
    display: block; margin-top: 3px;
    font-size: 11.5px; color: var(--text-3); line-height: 1.4;
}
.bp-cell__hint.is-short { color: var(--danger); font-weight: 600; }
.bp-cell > input.is-over {
    border-color: var(--danger); background: var(--danger-bg); color: var(--danger);
}
@media (max-width: 860px) {
    .bp-row { grid-template-columns: 1fr 1fr; }
    .bp-row--head { display: none; }
}

/* ---- 三格结果卡（导入结果、批量操作结果） ---- */
.kpi-row { display: flex; gap: var(--sp-3); flex-wrap: wrap; }
.kpi {
    flex: 1 1 0; min-width: 96px; text-align: center;
    background: var(--surface-2); border: 1px solid var(--line-2);
    border-radius: var(--r); padding: var(--sp-3) var(--sp-2);
}
.kpi__num {
    font-size: 23px; font-weight: 700; line-height: 1.15; color: var(--text);
    font-variant-numeric: tabular-nums;
    letter-spacing: -.4px;
}
.kpi__label { font-size: 12px; color: var(--text-3); margin-top: 2px; }
.kpi--ok .kpi__num { color: var(--success); }
.kpi--warn .kpi__num { color: var(--warning); }
.kpi--danger .kpi__num { color: var(--danger); }

/* ---- 键值对里的 key 提示（表单里说明"这一项对应哪个后端字段"） ---- */
.field__key {
    font-family: var(--mono); font-size: 11.5px; color: var(--text-3);
    background: var(--ink-100); padding: 1px 6px; border-radius: var(--r-sm);
}

/* ---- 通知条里可折叠的补充说明 ----
   一屏提示条如果占掉十几行，用户滚动到正文之前就先看到一大段字。
   主干一句话留在外面，其余折进 <details>：想深究的人点开，其余人直接往下走。 */
.notice__more { margin-top: var(--sp-2); }
.notice__more > summary {
    cursor: pointer; font-size: 12.5px; color: var(--text-2);
    user-select: none; list-style: none;
}
.notice__more > summary::-webkit-details-marker { display: none; }
.notice__more > summary::before { content: '▸ '; color: var(--text-3); }
.notice__more[open] > summary::before { content: '▾ '; }
.notice__more > summary:hover { color: var(--signal-700); }


/* ==========================================================================
   图表与统计
   --------------------------------------------------------------------------
   为什么自己画：本机 unpkg 被墙、项目又不引任何前端库，所以图表全部用
   「div + CSS」或「内联 SVG」直接产出（见 assets/js/chart.js）。
   div 版（条形/漏斗/进度）能随容器自适应、文字可选中；SVG 版（环图/柱图/
   迷你趋势）用于必须精确绘制的地方。两者都不产生额外依赖。

   配色一律走 :root 的 --c1~--c6，不写魔法色值。深浅色系改一处即全局生效。
   图表是"读数"的地方：数字全部等宽、右对齐，刻度线用发丝级对比度。
   ========================================================================== */

/* ---- 两栏布局：左内容 + 右统计卡 ---- */
.grid-2 {
    display: grid;
    grid-template-columns: minmax(0, 1.9fr) minmax(0, 1fr);
    gap: var(--sp-4);
    align-items: start;
}
/* 等宽两栏。`grid-2` 的 1.9:1 是给「左主右辅」（左表单 / 右预览）用的；
   而当左右是**性质相同**的两块内容（两套配置、两组检查项）时，
   1.9:1 会把右边挤成一行几个字。这时改用等宽。
   注意：媒体查询里也要覆盖 —— 行内 `style` 能压过媒体查询，所以不许用行内写法。 */
.grid-2--even { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
/* 证书模板页：左列是元素编辑卡（可滚动），右列是实时预览。
   默认 1.9:1 把右栏压到约 1/3 宽，预览被缩到很小（k≈0.47）。
   这里给右栏更多空间（≈1.45:1.25），预览放大到 k≈0.63，
   管理员看版式、拖元素都更清楚；窄屏（≤1080）仍回落单栏。 */
.grid-2--certs { grid-template-columns: minmax(0, 1.45fr) minmax(0, 1.25fr); }
.grid-2 > .card { margin-bottom: 0; }
@media (max-width: 1080px) { .grid-2, .grid-2--even, .grid-2--certs { grid-template-columns: 1fr; } }

/* ==========================================================================
   证书模板编辑页（10-11 轮 #156）
   左列是可滚动的元素编辑卡，右列是**吸顶**的实时预览。
   为什么必须吸顶：元素卡有七八张、每张都要边改边看效果，
   预览一旦滚出视口，管理员就得来回滚动对比 ——
   「可视化编辑」的可视化就没了。
   `top` 用 `calc(var(--topbar-h) + var(--sp-3))`：
   顶栏是 sticky 的，不留出它的高度预览会被压在顶栏底下。
   ========================================================================== */
.tpl-aside {
    position: sticky;
    top: calc(var(--topbar-h) + var(--sp-3));
}
.tpl-aside > .card { margin-bottom: 0; }
/* 预览舞台：iframe 按**证书真实纸宽**渲染，再用 transform 缩放塞进来。
   为什么必须这么绕：直接让 iframe 适应右栏宽度的话，证书会按窄栏排版，
   于是副标题被折成三行、落款挤到框外 —— 管理员照着这个预览调字号，
   改完发现真实证书完全不是那个样子。**预览会骗人比没有预览更糟。** */
.tpl-stage {
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface-2);
}
/* 预览框：高度由 JS 按内容量出来（写死会裁掉落款那一段）。
   缩放基准在左上角，配合 .tpl-stage 的 overflow:hidden 正好裁到边。 */
.tpl-frame {
    width: 100%;
    border: 0;
    display: block;
    transform-origin: top left;
    background: #fff;
}
/* 元素卡：标题行可点开/收起，收起后只留一行摘要 */
.tpl-block { margin-bottom: var(--sp-3); }
.tpl-block__head {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    flex-wrap: wrap;
}
.tpl-block__title { font-weight: 600; margin-right: auto; }
.tpl-block__body { padding-top: var(--sp-3); }
/* 变量插入按钮组 */
.tpl-vars { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
.tpl-vars .btn { font-size: 12px; height: 26px; padding: 0 8px; }
/* 颜色控件：色板 + 令牌下拉并排。
   两个控件写同一个字段 —— 色板给"精确调色"，令牌给"跟着全局改"。
   只留色板的话，改一次主色要逐个元素重挑；只留令牌就调不了具体色值。 */
.tpl-color { display: flex; gap: 6px; align-items: center; }
.tpl-color input[type="color"] {
    width: 40px; height: 32px; padding: 2px; flex: 0 0 auto;
    border: 1px solid var(--line); border-radius: var(--r-sm);
    background: var(--surface); cursor: pointer;
}
.tpl-color select { flex: 1 1 auto; min-width: 0; }
/* 图片元素的上传区 */
.tpl-img { display: flex; flex-direction: column; gap: var(--sp-2); }
.tpl-img__preview {
    padding: var(--sp-3); border: 1px dashed var(--line);
    border-radius: var(--r-sm); background: var(--surface-2);
    text-align: center;
}
.tpl-img__preview img { max-width: 180px; max-height: 180px; display: inline-block; }
.tpl-img__row { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
/* 错误清单（服务端返回的逐条校验结果） */
.tpl-errors {
    margin: var(--sp-3) 0 0;
    padding: var(--sp-3) var(--sp-3) var(--sp-3) calc(var(--sp-3) + 18px);
    border: 1px solid var(--danger);
    border-radius: var(--r-sm);
    background: var(--danger-bg);
    color: var(--danger);
    font-size: 13px;
    line-height: 1.8;
}
.tpl-errors li { margin: 0; }
@media (max-width: 1080px) {
    /* 单栏时吸顶没有意义（内容比视口长，吸住反而挡路） */
    .tpl-aside { position: static; }
}

/* ---- 药丸标签页 ---- */
.pills { display: flex; gap: 6px; flex-wrap: wrap; }

.pill {
    display: inline-flex; align-items: center; gap: 6px;
    height: 32px; padding: 0 14px;
    border: 1px solid var(--line); border-radius: 999px;
    background: var(--surface); color: var(--text-2);
    font: inherit; font-size: 13px; cursor: pointer;
    transition: background var(--dur-fast) var(--ease-out),
                border-color var(--dur-fast) var(--ease-out),
                color var(--dur-fast) var(--ease-out);
}
.pill:hover { border-color: var(--ink-300); background: var(--surface-2); color: var(--text); }
.pill.is-on { background: var(--signal-700); border-color: var(--signal-700); color: #fff; }
.pill:disabled { opacity: .5; cursor: not-allowed; }
.pill__n { font-variant-numeric: tabular-nums; opacity: .85; }

/* ---- 圆点状态 ----
   状态词前面一个小色点，比纯文字更快扫读；比徽章更轻，适合表格里密集出现。 */
.dot-state {
    display: inline-flex; align-items: center; gap: 5px;
    font-size: 12.5px; color: var(--text-2); white-space: nowrap;
}
.dot-state::before {
    content: ''; width: 6px; height: 6px; border-radius: 2px;
    background: var(--c6); flex: 0 0 6px;
}
.dot-state--ok::before { background: var(--c2); }
.dot-state--warn::before { background: var(--c3); }
.dot-state--danger::before { background: var(--c4); }
.dot-state--info::before { background: var(--c1); }
.dot-state--off::before { background: var(--c6); }

/* ---- 细进度条（表格单元格里用）---- */
.meter { display: flex; align-items: center; gap: 8px; min-width: 110px; }
.meter__track {
    flex: 1 1 auto; min-width: 44px; height: 6px;
    border-radius: 3px; background: var(--ink-100); overflow: hidden;
}
.meter__fill { height: 100%; border-radius: 3px; background: var(--c1); }
.meter__text {
    font-size: 12px; color: var(--text-2);
    font-variant-numeric: tabular-nums; white-space: nowrap;
}
.meter--ok .meter__fill { background: var(--c2); }
.meter--warn .meter__fill { background: var(--c3); }
.meter--danger .meter__fill { background: var(--c4); }
.meter--muted .meter__fill { background: var(--c6); }

/* ---- 横向条形图 ----
   label / 轨道 / 数值 三列定宽，多行之间数值才能对齐（不对齐的条形图读不出大小关系）。 */
.chart { display: flex; flex-direction: column; gap: 10px; }

.chart__row {
    display: grid;
    grid-template-columns: minmax(64px, 32%) minmax(60px, 1fr) auto;
    align-items: center; gap: 10px; font-size: 13px;
}
.chart__label {
    color: var(--text-2);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chart__track {
    position: relative; height: 10px;
    border-radius: 5px; background: var(--ink-100); overflow: hidden;
}
.chart__fill {
    position: absolute; top: 0; bottom: 0; left: 0;
    border-radius: 5px; background: var(--c1); min-width: 3px;
}
.chart__value {
    min-width: 52px; text-align: right;
    font-weight: 600; font-variant-numeric: tabular-nums;
}

.chart__fill--c1 { background: var(--c1); }
.chart__fill--c2 { background: var(--c2); }
.chart__fill--c3 { background: var(--c3); }
.chart__fill--c4 { background: var(--c4); }
.chart__fill--c5 { background: var(--c5); }
.chart__fill--c6 { background: var(--c6); }

/* 次要数值（百分比、人次）跟在主数值后面，不抢视线 */
.chart__value small {
    display: block; font-weight: 400; font-size: 11.5px; color: var(--text-3);
    letter-spacing: 0;
}

/* ---- 转化漏斗 ----
   每一步 = 名称/数值行 + 条形，包在 .funnel__step 里；
   如果让名称行与条形直接做 .funnel 的子元素，外层 gap 会把"同一段的上下两行"拆开。 */
.funnel { display: flex; flex-direction: column; gap: 12px; }
.funnel__step { display: flex; flex-direction: column; gap: 5px; }
.funnel__top {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 8px; font-size: 12.5px;
}
.funnel__name { color: var(--text-2); }
/* 环比（相对上一步）—— 与"占入口百分比"是两个口径，分开显示避免混淆 */
.funnel__delta { margin-left: 6px; font-size: 11.5px; font-variant-numeric: tabular-nums; }
.funnel__delta--up { color: var(--c2); }
.funnel__delta--down { color: var(--c4); }
.funnel__val { font-variant-numeric: tabular-nums; color: var(--text); }
.funnel__val b { font-size: 13.5px; }
.funnel__val i { color: var(--text-3); font-style: normal; margin-left: 4px; font-size: 11.5px; }
.funnel__bar {
    height: 8px; border-radius: 4px;
    background: var(--ink-100); overflow: hidden;
}
.funnel__bar > i { display: block; height: 100%; border-radius: 4px; background: var(--c1); }

/* ---- 环图 / 饼图 ----
   中心文字用**绝对定位的 HTML**压在 SVG 上，不用 SVG <text>：
   SVG 文字既不能选中也不能换行，字号还得跟着 viewBox 一起换算。 */
.donut-wrap { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap; }
.donut { position: relative; flex: 0 0 132px; width: 132px; height: 132px; }
.donut__svg { display: block; width: 100%; height: 100%; }
.donut__center {
    position: absolute; inset: 0; pointer-events: none;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 1px; text-align: center;
}
/* 环图中心是一屏里的头条读数，值得用主色 ——
   （上一版这里用的是唯一的橙色，橙色撤掉后自然回到政务蓝。）
   不再借等宽字体：`tabular-nums` 已经保证刷新时数字不左右跳。 */
.donut__center b {
    font-size: 22px; font-weight: 700; line-height: 1.1;
    color: var(--brand-700); font-variant-numeric: tabular-nums;
    letter-spacing: -.5px;
}
.donut__center span { font-size: 11px; color: var(--text-3); }

/* ---- 竖向柱图 ----
   柱高用百分比，所以必须有一个**高度确定**的画布层（.colchart__plot）——
   直接把百分比高度写在 flex 列的柱子上，参照物是含数值行的整列，会溢出去。 */
.colchart { display: flex; align-items: stretch; gap: 6px; }
.colchart__item { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.colchart__val {
    height: 16px; line-height: 16px; text-align: center;
    font-size: 12px; font-weight: 600;
    font-variant-numeric: tabular-nums;
    overflow: hidden; white-space: nowrap;
}
/* 柱底一条发丝基线：没有它，矮柱看起来像"浮在半空" */
.colchart__plot { height: 96px; display: flex; align-items: flex-end; border-bottom: 1px solid var(--line); }
.colchart__bar {
    width: 100%; border-radius: 4px 4px 0 0; background: var(--c1); height: 0;
    transition: height .3s var(--ease-out);
}
.colchart__label {
    height: 16px; line-height: 16px; text-align: center;
    font-size: 11.5px; color: var(--text-3);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.colchart__bar--c1 { background: var(--c1); }
.colchart__bar--c2 { background: var(--c2); }
.colchart__bar--c3 { background: var(--c3); }
.colchart__bar--c4 { background: var(--c4); }
.colchart__bar--c5 { background: var(--c5); }
.colchart__bar--c6 { background: var(--c6); }

/* ---- 堆叠条（一条横条由多段拼成：已用 / 剩余 这类）----
   段间留 2px 缝隙，靠底色分隔；不要用 border —— 段宽为 0 时 border 还在，
   会看到一条悬空的细线。 */
.stack { display: flex; gap: 2px; height: 10px; border-radius: 5px; overflow: hidden; background: var(--ink-100); }
.stack > i { display: block; height: 100%; min-width: 0; }
.stack > i:first-child { border-radius: 5px 0 0 5px; }
.stack > i:last-child { border-radius: 0 5px 5px 0; }
.stack > i:only-child { border-radius: 5px; }
.stack > i.s1 { background: var(--c1); }
.stack > i.s2 { background: var(--c2); }
.stack > i.s3 { background: var(--c3); }
.stack > i.s4 { background: var(--c4); }
.stack > i.s5 { background: var(--c5); }
.stack > i.s6 { background: var(--c6); }

/* ---- 图例 ----
   加宽度上限：图例只是「环图/堆叠条」的注解，让它铺满整张卡会把
   标签和数值拉得隔着一整个屏幕，读起来要来回扫。 */
.legend { display: flex; flex-direction: column; gap: 6px; font-size: 12.5px; flex: 1 1 140px; max-width: 380px; }
.legend__item { display: flex; align-items: center; gap: 7px; }
.legend__sw { width: 9px; height: 9px; border-radius: 2px; background: var(--c1); flex: 0 0 9px; }
/* 名称与数值**紧邻**，不要让名称把整行撑满 ——
   「通过人次」和它的「1 人次 33.3%」隔开 200px 时，眼睛要来回扫才敢确认对应关系。
   代价是不同行的数值不再右对齐，但图例本来就只有两三行，谈不上"对齐看得更清"。 */
.legend__name {
    color: var(--text-2); flex: 0 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.legend__val { color: var(--text); font-variant-numeric: tabular-nums; white-space: nowrap; flex: 0 0 auto; font-family: var(--mono); }

/* ---- 迷你趋势线 ---- */
.spark { display: block; width: 100%; height: 34px; }

/* ---- 键值行 + 状态点（「系统健康」那种一列五行）---- */
.kv-list { display: flex; flex-direction: column; }
.kv-list__row {
    display: flex; align-items: center; gap: var(--sp-2);
    padding: 9px 0; border-bottom: 1px solid var(--line-2); font-size: 13px;
}
.kv-list__row:last-child { border-bottom: 0; }
.kv-list__k { flex: 1 1 auto; color: var(--text-2); }
.kv-list__v {
    color: var(--text); font-variant-numeric: tabular-nums;
    display: flex; align-items: center; gap: 6px;
}

/* ---- Top-N 排行（带序号徽标）---- */
.rank { display: flex; flex-direction: column; }

.rank__item {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: 9px 0; border-bottom: 1px solid var(--line-2); font-size: 13px;
}
.rank__item:last-child { border-bottom: 0; }

.rank__no {
    flex: 0 0 20px; width: 20px; height: 20px; border-radius: var(--r-sm);
    display: flex; align-items: center; justify-content: center;
    background: var(--ink-100); color: var(--text-2);
    font-family: var(--mono); font-size: 11.5px; font-variant-numeric: tabular-nums;
}
.rank__item:nth-child(1) .rank__no { background: var(--signal-100); color: var(--signal-800); }
.rank__item:nth-child(2) .rank__no { background: var(--c2-soft); color: var(--c2); }
.rank__item:nth-child(3) .rank__no { background: var(--c3-soft); color: var(--c3); }

.rank__name {
    flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rank__meta { color: var(--text-3); font-size: 12px; }
.rank__val { font-weight: 600; font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* ---- 图表空态 ---- */
.chart-empty {
    padding: var(--sp-5) var(--sp-4); text-align: center;
    color: var(--text-3); font-size: 13px;
}

/* ==========================================================================
   入场编排 / 触控 / 降级
   --------------------------------------------------------------------------
   全站只有**一次**编排好的入场：侧栏项目按顺序错峰浮现。
   为什么只做侧栏：
     · tools/check_web.js 在页面渲染完成后只等 300ms 就截图，
       更慢/更分散的编排会被拍成半透明，反过来让验收证据失效；
     · "每个元素都淡入上浮"正是反 AI 味清单第 7 条点名的毛病。
   页面主体（.content 的直接子元素）只做一次 200ms 的整体落位，不成序列。
   ========================================================================== */

@keyframes nav-in {
    from { opacity: 0; transform: translateX(-6px); }
    to   { opacity: 1; transform: none; }
}

@keyframes rise {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: none; }
}

@keyframes focus-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* 侧栏：每个条目在 shell.js 里带 --i（组序号，已在上游夹到 ≤9），
   16ms 一档 × 最多 10 档 = 144ms 延迟，加 200ms 时长 = 全部 ≤344ms 收尾。 */
.sidebar__group,
.sidebar__link {
    animation: nav-in var(--dur) var(--ease) both;
    animation-delay: calc(var(--i, 0) * 16ms);
}

/* 主体：同起同落，不做错峰（见上面的理由） */
.content > * {
    animation: rise var(--dur) var(--ease) both;
}

/* 触控设备：桌面端的 28~34px 按钮在手指下太小，这档提高到 44px。
   指针设备（鼠标）不受影响 —— 高密度表格是这套界面的立身之本，
   为它牺牲列宽不划算。 */
@media (pointer: coarse) {
    .btn { min-height: 44px; }
    .btn--sm { min-height: 40px; }
    /* 同上面那条大规则的理由：不用 `type` 枚举，用排除法兜住所有文本类input。
       漏写 `type` 的 input 在手机上会拿不到 44px 触控高度 —— 而这恰恰是
       手机上最容易出问题的场景（用户在手机浏览器里填表）。
       注意 `file` / `range` / `color` 仍被排除：它们的高度另有安排，
       硬拉到 44px 反而坏掉（尤其 `color` 会被压成竖条）。 */
    input:not([type]),
    input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]):not([type="color"]),
    input[type="text"], input[type="password"], input[type="number"],
    input[type="date"], input[type="datetime-local"], input[type="search"],
    input[type="email"], input[type="tel"], input[type="url"], select { min-height: 44px; }
    .sidebar__link { min-height: 44px; }
    .pill { min-height: 44px; }
    .grid-nav__item { width: 44px; height: 44px; line-height: 42px; }
    .seg__item { min-height: 44px; }
    .login-tab { min-height: 44px; }
    .choice { min-height: 44px; }
    .modal__close { width: 44px; height: 44px; }
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .001ms !important;
        animation-delay: 0ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .001ms !important;
        scroll-behavior: auto !important;
    }
}

/* ==========================================================================
   门户版式组件（最早为端总览页 `web/student/hub.html` 而移植；**该页已删**）
   --------------------------------------------------------------------------
   这一组类**不是新的设计语言**，而是把原型门户页的版式
   （`PPT/pages/student-hub.html` 的 `hub-parts.js` 产出）翻译成本文件已有的令牌：
   白卡 + 发丝边框 + 政务蓝强调色，一个色值都没换。

   ★ hub 页删除后，这一块（`.grid*` / `.alert*` / `.page-head__*` / `.part-*` /
     `.goal-*` / `.node-*` / `.role-tag*` / `.flow-step*` / `.mod-*` / `.mb-*` /
     `.btn--soft`）**目前没有任何页面或 JS 引用**，整块是 hub 遗留的死样式。
     刻意**不删**：删它的收益趋近于零（零构建链、无打包体积考量，不删不影响任何
     渲染），而逐类核对使用点的成本是真实的 —— 历史上 `.grid` 一类"看似没人用"
     的类曾被列表骨架借走，删错会让多端页面同时丢版式；死类清单在这里有注释
     钉着，比"看似干净实则查无对证"更可审计。将来真要清理，先全仓 grep 再动手。

   ★ 为什么不是直接抄原型那份 CSS：原型跑在 `PPT/assets/css/design-system.css`
     上，那是另一套 :root（色值相近但变量名不同）。同一个页面同时引两份 CSS
     会让"改一处色值、另一处没变"变成常态 —— 这类漂移没有任何守卫能发现。
     所以这里只借**版式**，色值一律取本文件的令牌。
   ========================================================================== */

/* ---- 栅格。本文件原来只有 `.grid-2`；门户页要的是等分 3/4 列 ---- */
.grid { display: grid; gap: var(--sp-4); }
.grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 1080px) {
    .grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .grid--3 { grid-template-columns: 1fr; }
}
@media (max-width: 760px) {
    .grid--4 { grid-template-columns: 1fr; }
}

/* ---- 提示条。
   与 `.notice` 的分工：`.notice` 是"整页须知"（页面最顶上一整条），
   `.alert` 是"某一段内容的边界说明"，可以出现在页面中间、和卡片同宽。
   两者混用会让读者分不清"这是页面级警告还是这一块的注解"。 ---- */
.alert {
    display: flex;
    gap: var(--sp-3);
    padding: 14px 16px;
    border: 1px solid transparent;
    border-radius: var(--r-lg);
    font-size: 13.5px;
    line-height: 1.8;
    color: var(--text-2);
}
.alert__icon { flex: 0 0 auto; font-size: 18px; line-height: 1.45; }
.alert__body { flex: 1 1 auto; min-width: 0; }
.alert__title {
    font-weight: 600;
    color: var(--text);
    margin-bottom: 2px;
}
.alert--info { background: var(--brand-50); border-color: var(--brand-100); }
.alert--warn { background: var(--warning-bg); border-color: #fde68a; }
.alert--ok   { background: var(--success-bg); border-color: #a7f3d0; }

/* ---- 页头里的两块：左标题组 / 右动作组 ---- */
.page-head__text { min-width: 0; }
.page-head__actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; }

/* ---- 端标题：方形图标徽章 + 端名。
   `--pc` 由页面按端注入（学员端 = 政务蓝），和其他组件共用同一个变量，
   所以同一个端在标题、入口卡、底部互跳里的强调色一定一致。 ---- */
.part-title {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    font-size: 23px;
    letter-spacing: -.3px;
}
.part-badge {
    flex: 0 0 44px;
    width: 44px;
    height: 44px;
    display: grid;
    place-items: center;
    border-radius: var(--r-lg);
    background: var(--pc, var(--brand-500));
    color: #fff;
    font-size: 22px;
    line-height: 1;
}

/* ---- 卡片里放"标题 + 一句说明"两行时用（`.card__head h3` 只放得下一行标题） ---- */
.card__title { font-size: 15px; font-weight: 600; color: var(--text); }
.card__desc {
    font-size: 12.5px;
    color: var(--text-3);
    margin-top: 3px;
    line-height: 1.75;
}

/* ---- 目标卡（三列） ---- */
.goal-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--sp-4);
}
@media (max-width: 1080px) { .goal-grid { grid-template-columns: 1fr; } }

.goal-item {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 16px;
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: var(--surface-2);
}
.goal-item__icon { font-size: 20px; line-height: 1.3; }
.goal-item__t { font-size: 14px; font-weight: 600; color: var(--text); }
.goal-item__d { font-size: 12.5px; color: var(--text-2); line-height: 1.85; }

/* ---- 功能范围表里的两个文本块：模块名要能一眼找到、功能点要能读 ---- */
.mod-name { font-weight: 600; color: var(--text); }
.mod-func { color: var(--text-2); }

/* ---- 角色标签。四端各一色，和侧栏端名一一对应 ---- */
.role-tag {
    display: inline-block;
    padding: 2px 9px;
    border-radius: 999px;
    background: var(--ink-100);
    color: var(--text-2);
    font-size: 12px;
    font-weight: 500;
    line-height: 1.75;
    white-space: nowrap;
}
.role-tag--student { background: var(--brand-50);   color: var(--brand-700); }
.role-tag--org     { background: #fff7ed;           color: #c2410c; }
.role-tag--admin   { background: var(--success-bg); color: var(--success); }
.role-tag--health  { background: #f5f3ff;           color: #6d28d9; }

/* ---- 关键节点卡（三列，图标 + 标题 + 一段话） ---- */
.node-cards {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--sp-4);
}
@media (max-width: 1080px) { .node-cards { grid-template-columns: 1fr; } }

.node-card {
    display: flex;
    gap: var(--sp-3);
    padding: 16px;
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: var(--surface);
    box-shadow: var(--shadow-sm);
}
.node-card__icon { flex: 0 0 auto; font-size: 18px; line-height: 1.5; }
.node-card__t { font-size: 13.5px; font-weight: 600; color: var(--text); }
.node-card__d { font-size: 12.5px; color: var(--text-2); line-height: 1.85; margin-top: 3px; }

/* ---- 入口大卡。
   这是一张**整块可点**的卡（a 标签），所以 hover 必须把
   `a:hover` 默认的"下划线 + 变蓝"盖掉 —— 否则链接样式会压在卡片版式上，
   看起来像"卡片里所有字都变成了超链接"。 ---- */
.part-cards { display: grid; gap: var(--sp-4); grid-template-columns: repeat(3, minmax(0, 1fr)); }
.part-cards--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.part-cards--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 1080px) {
    .part-cards, .part-cards--2, .part-cards--3 { grid-template-columns: 1fr; }
}

.part-card {
    display: block;
    padding: 18px;
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: var(--surface);
    color: var(--text);
    text-decoration: none;
    transition: border-color var(--dur-fast) var(--ease),
                box-shadow var(--dur-fast) var(--ease),
                transform var(--dur-fast) var(--ease);
}
.part-card:hover,
.part-card:focus-visible {
    border-color: var(--pc, var(--brand-500));
    box-shadow: var(--shadow);
    color: var(--text);
    text-decoration: none;
    transform: translateY(-2px);
}
.part-card__no {
    font-size: 12px;
    font-weight: 600;
    color: var(--pc, var(--brand-600));
}
.part-card__name { font-size: 16px; font-weight: 600; margin-top: 4px; }
.part-card__desc { font-size: 13px; color: var(--text-2); line-height: 1.85; margin-top: 6px; }
.part-card__mods { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
.part-card__mods > span {
    padding: 2px 8px;
    border-radius: 999px;
    background: var(--ink-100);
    color: var(--text-2);
    font-size: 11.5px;
}
.part-card__foot {
    margin-top: 12px;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--pc, var(--brand-600));
}
.part-card__foot .arrow { margin-left: 2px; }

/* ---- 底部互跳（两栏） ---- */
.part-nav {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--sp-4);
}
@media (max-width: 1080px) { .part-nav { grid-template-columns: 1fr; } }

.part-nav__item {
    display: block;
    padding: 16px 18px;
    border: 1px solid var(--line);
    border-left: 3px solid var(--pc, var(--brand-500));
    border-radius: var(--r-lg);
    background: var(--surface);
    color: var(--text);
    text-decoration: none;
    transition: background var(--dur-fast) var(--ease);
}
.part-nav__item:hover {
    background: var(--surface-2);
    color: var(--text);
    text-decoration: none;
}
.part-nav__k { font-size: 12px; color: var(--text-3); }
.part-nav__v { font-size: 15px; font-weight: 600; margin-top: 3px; }
.part-nav__h { font-size: 12.5px; color: var(--text-2); margin-top: 4px; line-height: 1.8; }

/* ---- 流程步骤条。
   原型那一页用 mermaid 画流程图；本系统不引任何图表库（零依赖是硬约束），
   所以这里用**一条横向的步骤条**表达同一件事：谁先谁后、哪里会回退。
   步骤条也会换行，窄屏下自动折成两行 —— 不需要媒体查询。 ---- */
.flow-steps { display: flex; flex-wrap: wrap; align-items: stretch; gap: var(--sp-2); }
.flow-step {
    flex: 1 1 150px;
    min-width: 0;
    padding: 12px 14px;
    border: 1px solid var(--line);
    border-radius: var(--r);
    background: var(--surface-2);
}
.flow-step__no {
    font-size: 11.5px;
    font-weight: 600;
    color: var(--brand-600);
    letter-spacing: .3px;
}
.flow-step__t { font-size: 13.5px; font-weight: 600; color: var(--text); margin-top: 2px; }
.flow-step__d { font-size: 12px; color: var(--text-2); line-height: 1.8; margin-top: 4px; }
.flow-step--back { border-style: dashed; border-color: var(--danger); }
.flow-step__back {
    margin-top: 6px;
    font-size: 11.5px;
    color: var(--danger);
}

/* ---- 少量间距工具类（本文件原来只有 mb-0/2/4、mt-0/2/4） ---- */
.mb-8  { margin-bottom: var(--sp-2); }
.mb-16 { margin-bottom: var(--sp-4); }
.mb-20 { margin-bottom: 20px; }
.mb-24 { margin-bottom: var(--sp-5); }

/* ---- 次要按钮：有底色、无边框。
   与 `.btn--ghost` 的区别：ghost 是"无边框无底色"（表内操作列），
   soft 是"有淡底无边框"（页头里和主按钮并排的那个次要动作）——
   两个都无边框时并排会看不出哪个是次要动作。 ---- */
.btn--soft {
    background: var(--brand-50);
    border-color: transparent;
    color: var(--brand-700);
}
.btn--soft:hover:not(:disabled) {
    background: var(--brand-100);
    border-color: transparent;
    color: var(--brand-800);
}

/* ==========================================================================
   学员端手机形态（`web/student/m/*`）
   --------------------------------------------------------------------------
   来源：`PPT/assets/css/mobile.css`（演示原型）。移植时做了**两处**改动，
   其余逐条保持原值 —— 这样"原型长什么样、系统里就长什么样"是能被逐行核对的。

   改动一：变量名走一层显式别名（见下方 `:root`），不逐行手改 700 行。
       原型那套是 `design-system.css` 的命名（--bg-app / --r-md / --sh-xs /
       --t-fast …），本文件用的是另一套。手改 700 行必出错，而 `var()` 取不到值
       **不会报错、只是那条声明被丢掉** —— 症状是"某个圆角没了"，没人会发现。
   改动二：语义色与圆角**并入本文件的令牌**，不引入第二套色值 ——
       手机端和电脑端必须看起来是同一个系统。

   布局模型：桌面浏览时是一个 390×844 的真机外框（演示形态，也是"你这是手机端"的
   可视化说明）；视口 ≤460px 时外框消失、内容铺满整屏（真机形态）。
   ========================================================================== */

/* 原型的令牌名 → 本文件的既有令牌。只做别名，不定义新色值。 */
:root {
    --bg-app: var(--bg);
    --r-xs: 4px;
    --r-md: var(--r);
    --r-full: 999px;
    --sh-xs: var(--shadow-sm);
    --sh-sm: 0 1px 3px rgba(16, 24, 40, .08), 0 1px 2px rgba(16, 24, 40, .04);
    --sh-xl: var(--shadow-lg);
    --t-fast: var(--dur-fast);
    --t-base: var(--dur);
    --t-slow: var(--dur-slow);
    --success-50: var(--success-bg);
    --success-700: var(--success);
    --warning-50: var(--warning-bg);
    --warning-600: var(--warning);
    --danger-50: var(--danger-bg);
    --danger-600: var(--danger);
}

/* ---------- 演示台：深色底 + 居中真机 ----------
   `.stage` 是**桌面预览**的舞台：深色渐变底 + 居中 390×844 的手机。
   `.stage--real` 是 10-07 轮加的：真机（微信/手机浏览器）上不再演这一套，
   直接铺满 —— 深色渐变底在真机上会从状态栏一直渗到底，
   而学员要的是「这就是那个系统」，不是「这是某个系统的演示」。 */
.stage {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 28px 16px 40px;
    background:
        radial-gradient(1200px 600px at 50% -10%, #1e293b 0%, #0b1220 60%),
        #0b1220;
}

.stage--real {
    padding: 0;
    background: var(--bg-app);
}
.stage--real .phone--real {
    width: 100%;
    max-width: 100%;
    height: 100vh;
    height: 100dvh;      /* 移动端浏览器地址栏收放时 100vh 会跳，用 dvh 稳定 */
    border: none;
    border-radius: 0;
    box-shadow: none;
}
.stage--real .phone--real::before { display: none; }   /* 假刘海 */

.stage__head {
    max-width: 720px;display: none;
    margin-bottom: 22px;
    text-align: center;
}
.stage__title { color: #fff; font-size: 22px; font-weight: 700; letter-spacing: -.4px; }
.stage__desc {
    margin-top: 7px;
    color: rgba(255, 255, 255, .5);
    font-size: 13px;
    line-height: 1.75;
}
.stage__switcher {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 7px;
    margin-top: 16px;
}
.stage__chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 7px 14px;
    border: 1px solid rgba(255, 255, 255, .1);
    border-radius: var(--r-full);
    background: rgba(255, 255, 255, .05);
    color: rgba(255, 255, 255, .75);
    font-family: inherit;
    font-size: 12.5px;
    cursor: pointer;
    text-decoration: none;
    transition: all var(--t-fast) var(--ease);
}
.stage__chip:hover {
    background: rgba(37, 99, 235, .18);
    border-color: rgba(37, 99, 235, .42);
    color: #fff;
    text-decoration: none;
}
.stage__chip.is-active {
    background: var(--brand-600);
    border-color: var(--brand-500);
    color: #fff;
    font-weight: 500;
    box-shadow: 0 6px 16px rgba(37, 99, 235, .4);
}

/* ---------- 真机外框 ---------- */
.phone {
    /* ★ 这段曾经整块被注释掉（连假刘海的 `.phone::before` 也是），
       后果不是"少了个手机边框"，是**布局链断了一整节**：
         · `.phone` 不是 flex 容器 → `.pbody` 的 `flex:1` 完全无效
         · `.pbody` 的 `min-height:900px` 于是按内容全高撑开，且没有祖先约束
           → 内容溢出到视口外、滚不动
         · 底部标签栏是 `position:fixed`，内容在它下面流过去
           → 实测末块底边 1171 / 1760，而视口只有 812 高

       为什么不多写 border / border-radius / box-shadow：
       那是「假手机外框」的装饰。`.stage--real .phone--real` 已把它们清零
       （真机形态不需要外框，宽屏靠 `.stage--real` 居中 + 两侧补底色）。
       装饰性属性留在 `.phone` 上只会让人以为学员端还有个手机边框。

       为什么用 `100dvh` 而不是 `100vh`：移动端浏览器地址栏收放时
       `100vh` 会跳，底部标签栏跟着闪一下。 */
    position: relative;
    display: flex;
    flex-direction: column;
    flex-shrink: 0;
    width: 100%;
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
    background: var(--bg-app);
}

/* ---------- 状态栏 ---------- */
.pstatus {
    position: relative;
    z-index: 55;
    flex-shrink: 0;
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    height: 44px;
    padding: 0 22px 6px;
    background: transparent;
    color: #111827;
    font-size: 12.5px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.pstatus--light { color: #fff; }

/* ---------- 微信内置浏览器顶栏（说明"这是在微信里打开的"） ---------- */
.pwechat {
    position: relative;
    z-index: 54;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 10px;
    height: 40px;
    padding: 0 14px;
    border-bottom: 1px solid #dcdcdc;
    background: #ededed;
}
.pwechat__close { font-size: 19px; color: #333; line-height: 1; cursor: pointer; }
.pwechat__url {
    flex: 1;
    overflow: hidden;
    padding: 4px 12px;
    border-radius: var(--r-full);
    background: #fff;
    color: #2c2c2c;
    font-size: 12px;
    text-align: center;
    white-space: nowrap;
    text-overflow: ellipsis;
}
.pwechat__menu { font-size: 15px; color: #333; letter-spacing: 1px; }

/* ---------- 滚动区 ---------- */
.pbody {
    position: relative;
    flex: 1;
    /* ★ 原来的 `min-height: 900px` 是桌面原型留下的（为了让演示台里内容撑满）。
       在 `height:100dvh` 的 flex 容器里它会**顶破**容器高度：内容溢出到视口外，
       而滚动容器是自己（overflow-y:auto）→ 底部标签栏压住最后一段内容。
       现在改成 `min-height: 0`，由 flex:1 撑满剩余空间。
       ★ flex 子项默认 `min-height: auto`，不写 0 就收缩不下去 ——
         这是 flex 布局最常见的「明明设了 height 却还是溢出」的原因。 */
    min-height: 0;
    overflow-y: auto;
    background: var(--bg-app);
    -webkit-overflow-scrolling: touch;
}
.pbody::-webkit-scrollbar { width: 0; }

/* ---------- 页内导航栏 ---------- */
.pnavbar {
    position: sticky;
    top: 0;
    z-index: 30;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 8px;
    height: 46px;
    padding: 0 12px;
    background: var(--brand-600);
    color: #fff;
}
.pnavbar__back { width: 30px; font-size: 18px; text-align: center; cursor: pointer; }
.pnavbar__title { flex: 1; font-size: 16px; font-weight: 600; text-align: center; }
.pnavbar__action {
    padding: 4px 10px;
    border: 1px solid rgba(255, 255, 255, .65);
    border-radius: var(--r-sm);
    font-size: 13px;
    white-space: nowrap;
    cursor: pointer;
}
/* 原生蓝标题栏：复刻截图里那根 #4a6fa5 主色（微信内 H5 的原生观感） */
.pnavbar--native { background: #4a6fa5; }

/* ---------- 登录页 ---------- */
.login-hero {
    position: relative;
    overflow: hidden;
    padding: 34px 0 46px;
    background: linear-gradient(160deg, #5b8ff9 0%, #4a7cf0 45%, #3f6fe0 100%);
    text-align: center;
}
/* 底部白色弧形分隔 */
.login-hero::after {
    content: "";
    position: absolute;
    left: 50%;
    bottom: -58px;
    transform: translateX(-50%);
    width: 150%;
    height: 96px;
    border-radius: 50%;
    background: #fff;
}
.login-hero__logo {
    position: relative;
    z-index: 2;
    display: grid;
    place-items: center;
    width: 66px;
    height: 66px;
    margin: 0 auto 16px;
    border-radius: 50%;
    background: #fff;
    font-size: 30px;
    box-shadow: 0 8px 20px rgba(0, 0, 0, .16);
}
.login-hero__title {
    position: relative;
    z-index: 2;
    color: #fff;
    font-size: 16.5px;
    font-weight: 600;
    letter-spacing: .5px;
}
.login-form {
    position: relative;
    z-index: 3;
    margin-top: -22px;
    padding: 6px 26px 0;
}
.login-input {
    display: flex;
    align-items: center;
    gap: 9px;
    height: 46px;
    margin-bottom: 14px;
    padding: 0 12px;
    border: 1px solid var(--ink-200);
    border-radius: var(--r-sm);
    background: #fff;
    box-shadow: var(--sh-xs);
}
.login-input__icon { flex-shrink: 0; width: 20px; color: #4a6fa5; font-size: 16px; text-align: center; }
.login-input input {
    flex: 1;
    min-width: 0;
    border: none;
    outline: none;
    background: transparent;
    color: var(--ink-900);
    font-size: 14.5px;
}
.login-input input::placeholder { color: var(--ink-400); }
.login-sms-btn {
    flex-shrink: 0;
    padding: 6px 12px;
    border-radius: var(--r-xs);
    background: #ee4d4d;
    color: #fff;
    font-size: 12.5px;
    font-weight: 500;
    white-space: nowrap;
}
.login-sms-btn:disabled { background: var(--ink-300); cursor: not-allowed; }
.btn-web {
    display: block;
    width: 100%;
    height: 44px;
    margin-top: 22px;
    border-radius: var(--r-sm);
    background: #4a6fa5;
    color: #fff;
    font-size: 15.5px;
    font-weight: 500;
    line-height: 44px;
    letter-spacing: 1px;
    text-align: center;
}
.btn-web:hover { background: #40628f; }
.login-tip {
    margin-top: 18px;
    color: var(--ink-400);
    font-size: 11.5px;
    line-height: 1.8;
    text-align: center;
}

/* ---------- 首页 ---------- */
.home-banner {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 14px 16px 18px;
    background: linear-gradient(135deg, #4a6fa5, #3f6fe0);
    color: #fff;
}
.home-banner__title {
    flex: 1;
    font-size: 16px;
    font-weight: 600;
    letter-spacing: .4px;
    text-align: center;
}
.home-banner__icon { font-size: 17px; cursor: pointer; }
.home-banner__icon:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

.home-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 0;
    padding: 20px 6px 22px;
    /* 这条 8px 的粗分隔线是原型的做法：四宫格属于"功能入口"，
       下面的列表属于"我的数据"，两者需要一条可见的边界。 */
    border-bottom: 8px solid var(--ink-100);
    background: #fff;
}
.home-cell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    padding: 6px 2px;
    border-radius: var(--r-md);
    cursor: pointer;
    transition: background var(--t-fast) var(--ease);
}
.home-cell:hover { background: var(--brand-50); }
.home-cell__icon {
    position: relative;
    display: grid;
    place-items: center;
    width: 46px;
    height: 46px;
    font-size: 25px;
}
/* 图标下的一小段装饰线（模仿截图里的图标风格） */
.home-cell__icon::after {
    content: "";
    position: absolute;
    bottom: 2px;
    left: 50%;
    transform: translateX(-50%);
    width: 30px;
    height: 2px;
    border-radius: 2px;
    background: currentColor;
    opacity: .28;
}
.home-cell__label { color: var(--ink-700); font-size: 12.5px; font-weight: 500; white-space: nowrap; }
/* 四宫格配色：截图里四个入口同色（原生蓝），这里保留变量名以便单端改色 */
.home-cell--enroll,
.home-cell--exam,
.home-cell--cert,
.home-cell--practice { color: #4a6fa5; }

/* ---------- 空状态（NoData 插画） ---------- */
.pempty { padding: 54px 20px 40px; text-align: center; }
.pempty__art { position: relative; width: 150px; height: 112px; margin: 0 auto 16px; }
.pempty__paper {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%) rotate(-7deg);
    display: grid;
    place-items: center;
    width: 96px;
    height: 82px;
    border: 1px solid var(--ink-200);
    border-radius: 5px;
    background: #fff;
    color: var(--ink-300);
    font-size: 13px;
    font-weight: 700;
    letter-spacing: .5px;
    box-shadow: var(--sh-sm);
}
.pempty__paper--back {
    transform: translateX(-50%) rotate(7deg) translateY(6px);
    background: #f1f5f9;
}
.pempty__box {
    position: absolute;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 128px;
    height: 44px;
    border-radius: 6px 6px 10px 10px;
    background: linear-gradient(180deg, #6b8ff9, #4a6fa5);
}
.pempty__box::before {
    content: "";
    position: absolute;
    top: -12px;
    left: 50%;
    transform: translateX(-50%);
    width: 128px;
    height: 20px;
    border-radius: 6px 6px 3px 3px;
    background: #7ea0fb;
    clip-path: polygon(0 100%, 0 30%, 50% 0, 100% 30%, 100% 100%);
}
.pempty__text { color: var(--ink-400); font-size: 13px; }

/* ---------- 手机端模态（"请补充个人信息"这类必须回应的提示） ---------- */
.psheet-mask {
    position: fixed;
    inset: 0;
    z-index: 70;
    display: grid;
    place-items: center;
    padding: 34px;
    background: rgba(15, 23, 42, .45);
    opacity: 0;
    animation: mask-in var(--t-base) var(--ease-out) forwards;
}
.psheet {
    width: 100%;
    overflow: hidden;
    border-radius: var(--r-lg);
    background: #fff;
    box-shadow: var(--sh-xl);
    transform: scale(.94);
    animation: modal-in var(--t-slow) var(--ease-out) forwards;
}
.psheet__body {
    padding: 30px 20px;
    color: var(--ink-800);
    font-size: 15px;
    font-weight: 500;
    text-align: center;
}
.psheet__foot { border-top: 1px solid var(--ink-100); }
.psheet__btn {
    display: block;
    width: 100%;
    padding: 14px;
    border: 0;
    background: #fff;
    color: var(--brand-600);
    font-family: inherit;
    font-size: 15.5px;
    font-weight: 600;
    text-align: center;
    cursor: pointer;
}
.psheet__btn:hover { background: var(--ink-50); }

/* ---------- 个人资料表单 ---------- */
.profile-list { margin-bottom: 10px; background: #fff; }
.pfield {
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: 52px;
    padding: 0 16px;
    border-bottom: 1px solid var(--ink-100);
}
.pfield:last-child { border-bottom: none; }
.pfield__label { flex-shrink: 0; width: 118px; color: var(--ink-800); font-size: 14px; }
.pfield__label .req { margin-left: 2px; color: #ee4d4d; }
.pfield__value { flex: 1; color: var(--ink-900); font-size: 14px; text-align: right; }
.pfield input,
.pfield select {
    flex: 1;
    min-width: 0;
    border: none;
    outline: none;
    background: transparent;
    color: var(--ink-900);
    font-family: inherit;
    font-size: 14px;
    text-align: right;
}
.pfield input::placeholder { color: var(--ink-400); }
.pfield--readonly .pfield__value { color: var(--ink-500); }
.pfield select {
    appearance: none;
    padding-right: 18px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='%2394a3b8' d='M6 8L2 4h8z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 2px center;
}
.profile-actions { padding: 14px 16px 30px; }

/* ---------- 通用小组件 ---------- */
.pcard {
    margin: 10px 12px;
    overflow: hidden;
    border-radius: var(--r-md);
    background: #fff;
}
.pcard__head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 13px 15px;
    border-bottom: 1px solid var(--ink-100);
    color: var(--ink-900);
    font-size: 14.5px;
    font-weight: 600;
}
.prow {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 13px 15px;
    border-bottom: 1px solid var(--ink-100);
    font-size: 13.5px;
}
.prow:last-child { border-bottom: none; }
.prow__label { flex-shrink: 0; color: var(--ink-500); }
.prow__value { flex: 1; color: var(--ink-900); text-align: right; }
.p-sec-title {
    padding: 14px 15px 8px;
    color: var(--ink-500);
    font-size: 12.5px;
    font-weight: 600;
    letter-spacing: .3px;
}
.p-list-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 14px 15px;
    border-bottom: 1px solid var(--ink-100);
    background: #fff;
    cursor: pointer;
}
.p-list-item:hover { background: var(--brand-50); }
.p-list-item:last-child { border-bottom: none; }
.p-list-item__title { flex: 1; color: var(--ink-900); font-size: 14px; }
.p-list-item__arrow { color: var(--ink-300); font-size: 15px; }

/* ---------- 手机端表格（成绩 / 证书） ---------- */
.ptable { width: 100%; border-collapse: collapse; background: #fff; font-size: 12.5px; }
.ptable th {
    padding: 9px 10px;
    border-bottom: 1px solid var(--ink-200);
    background: var(--ink-50);
    color: var(--ink-500);
    font-size: 11.5px;
    font-weight: 600;
    text-align: left;
    white-space: nowrap;
}
.ptable td {
    padding: 10px;
    border-bottom: 1px solid var(--ink-100);
    color: var(--ink-700);
    vertical-align: middle;
}
.ptable tbody tr:last-child td { border-bottom: none; }
.cal__day--today {
    background: var(--p-blue-50);
}
/* 底部安全区：让最后一行内容不贴着屏幕底边 */
.pfoot-space { height: 26px; }

/* ---------- 手机端提示条 ---------- */
.palert {
    display: flex;
    gap: 9px;
    margin: 10px 12px;
    padding: 12px 14px;
    border-radius: var(--r-md);
    font-size: 12.5px;
    line-height: 1.7;
}
.palert--info    { background: var(--brand-50);   color: var(--brand-800); }
.palert--warning { background: var(--warning-50); color: var(--warning-600); }
.palert--success { background: var(--success-50); color: var(--success-700); }
.palert--danger  { background: var(--danger-50);  color: var(--danger-600); }

/* ---------- 手机端答题（练习 / 考试共用的版式） ----------
   来源：原型的 `mobile-practice.html` 内联 <style>。搬进来的理由和上面一样：
   手机端答题是**学员用得最多的一屏**，它不该带着一份页面内联样式到处跑 ——
   那样每个答题页各自演化，两页的选项间距会慢慢不一致。 */
.q-progress {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 15px;
    border-bottom: 1px solid var(--ink-100);
    background: #fff;
    color: var(--ink-500);
    font-size: 12.5px;
}
.q-progress__bar {
    flex: 1;
    height: 6px;
    overflow: hidden;
    border-radius: var(--r-full);
    background: var(--ink-100);
}
.q-progress__fill {
    height: 100%;
    border-radius: var(--r-full);
    background: linear-gradient(90deg, var(--brand-500), var(--brand-600));
    transition: width .35s var(--ease-out);
}
.q-stem {
    padding: 18px 16px 14px;
    background: #fff;
    color: var(--ink-900);
    font-size: 15px;
    font-weight: 500;
    line-height: 1.8;
}
.q-meta {
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 0 16px 14px;
    border-bottom: 8px solid var(--ink-100);
    background: #fff;
}
.q-option {
    display: flex;
    align-items: flex-start;
    gap: 11px;
    padding: 14px 16px;
    border-bottom: 1px solid var(--ink-100);
    background: #fff;
    cursor: pointer;
    transition: background var(--t-fast) var(--ease);
}
.q-option:hover { background: var(--brand-50); }
.q-option__key {
    display: grid;
    place-items: center;
    flex-shrink: 0;
    width: 25px;
    height: 25px;
    margin-top: 1px;
    border: 1.5px solid var(--ink-300);
    border-radius: 50%;
    color: var(--ink-500);
    font-size: 12.5px;
    font-weight: 600;
    transition: all var(--t-fast) var(--ease);
}
.q-option__text { flex: 1; color: var(--ink-800); font-size: 14.5px; line-height: 1.7; }
.q-option.is-picked .q-option__key {
    border-color: var(--brand-600);
    background: var(--brand-600);
    color: #fff;
}
.q-option.is-picked .q-option__text { color: var(--brand-800); font-weight: 500; }
/* 判定后：对的绿、我选错的红。顺序必须是「先判对、再判我选的」，
   否则"选对了的那一项"会被后一条规则染成红的。 */
.q-option.is-right .q-option__key { border-color: var(--success); background: var(--success); color: #fff; }
.q-option.is-right .q-option__text { color: var(--success); }
.q-option.is-wrong .q-option__key { border-color: var(--danger); background: var(--danger); color: #fff; }
.q-option.is-wrong .q-option__text { color: var(--danger); }

.q-analysis {
    margin: 10px 12px;
    padding: 14px;
    border-radius: var(--r-md);
    background: var(--brand-50);
    color: var(--brand-800);
    font-size: 13px;
    line-height: 1.8;
}
.q-analysis b { color: var(--brand-900); }

.q-footbar {
    position: sticky;
    bottom: 0;
    display: flex;
    gap: 9px;
    padding: 12px 15px;
    border-top: 1px solid var(--ink-200);
    background: #fff;
}
.q-footbar .btn { flex: 1; }

/* 选项重贴标号的说明卡（虚线边框：这是一段"解释"，不是内容）。 */
.shuffle-note {
    margin: 10px 12px;
    padding: 13px 14px;
    border: 1px dashed var(--brand-100);
    border-radius: var(--r-md);
    background: #fff;
    color: var(--ink-700);
    font-size: 12.5px;
    line-height: 1.8;
}
.shuffle-note b { color: var(--brand-700); }

/* ---------- 手机端页签条（白底 + 选中项底部一条蓝线） ---------- */
.ptabs {
    display: flex;
    border-bottom: 1px solid var(--ink-200);
    background: #fff;
}
.ptab {
    flex: 1;
    padding: 13px 0;
    border: 0;
    border-bottom: 2px solid transparent;
    background: #fff;
    color: var(--ink-500);
    font-family: inherit;
    font-size: 14.5px;
    cursor: pointer;
}
.ptab.is-active {
    border-bottom-color: var(--brand-600);
    color: var(--brand-600);
    font-weight: 600;
}

/* ---------- 登录页的真机形态 ----------
   ≤460px 时把登录卡片摊平成一整屏、顶端压一条渐变头，主按钮换成手机端那根
   #4a6fa5 原生蓝 —— 与 `web/student/m/*` 的手机形态看起来是同一个系统。

   ★ 刻意**不加新 DOM**。登录页是四端共用的，也是自动化验收依赖最重的一页
     （`#username` / `#password` / `#submit` 必须首屏可见可填，见 tools/check_web.js）。
     只改观感、不动结构，是这一页唯一安全的改法 ——
     "为了好看把登录页重构一遍"的代价是四端 E2E 全红。 */
@media (max-width: 460px) {
    .login-page {
        align-items: flex-start;
        padding: 0;
        background-color: var(--bg);
        background-image: none;
    }
    .login-card {
        max-width: none;
        min-height: 100vh;
        padding: 0 0 26px;
        border-radius: 0;
        background: transparent;
        box-shadow: none;
    }
    .login-card__brand {
        margin-bottom: 0;
        padding: 32px 26px 26px;
        border-bottom: 0;
        background: linear-gradient(160deg, #5b8ff9 0%, #4a7cf0 45%, #3f6fe0 100%);
    }
    .login-card__brand b { color: #fff; font-size: 17px; letter-spacing: .5px; }
    .login-card__brand span { color: rgba(255, 255, 255, .78); }
    .login-tabs,
    .login-card #login-form,
    .login-card #sms-form,
    .login-card__foot,
    .login-role-hint { margin-left: 20px; margin-right: 20px; }
    .login-tabs { margin-top: 16px; }
    .login-card .btn--block {
        height: 44px;
        border-color: #4a6fa5;
        background: #4a6fa5;
        font-size: 15.5px;
        letter-spacing: 1px;
    }
    .login-card .btn--block:hover:not(:disabled) {
        border-color: #40628f;
        background: #40628f;
    }
}

/* ---------- 真机形态：外框消失，内容铺满 ---------- */
@media (max-width: 460px) {
    .stage { padding: 0; }
    .stage__head, .stage__switcher { display: none; }
    .phone {
        width: 100%;
        height: 100vh;
        border: none;
        border-radius: 0;
    }
    .phone::before { display: none; }
}

/* 触控设备：手机端的点击目标本来就按 44px 设计，不必再加高；
   但桌面端那档 `@media (pointer: coarse)` 会把 .btn 抬到 44px，
   而手机端底部按钮（.btn-web）本来就是 44px —— 不冲突，这里不重复声明。 */


/* ==========================================================================
   学员手机端 · 原生页面（10-05 轮）
   --------------------------------------------------------------------------
   这套类**只服务 `web/student/m/*.html`**，与电脑端完全平行：
   手机端不再是把电脑端页面塞进窄屏，而是原生版式。

   ★ 一条铁律：本节**只新增 `.p-*` 类，不改动任何已有类名**。
     `tools/check_chart.js` 拿 67 个类名与本文件做 1:1 对照，
     改掉或重命名任何一个已有类名，那道守卫立刻红 —— 而它红的方式
     是"类名对不上"，不是"页面坏了"，极难定位。
     所以手机端要什么样式就在这里新加什么，绝不复用/改写电脑端的类。
   ========================================================================== */

/* ---------- 手机端主色：复刻截图那根原生蓝 ----------
   ★ 定义在 `:root` 而不是 `.pbody`（10-07 轮修一个真事故）。
     原来的挂载点是 `.pbody { --p-blue: … }`，于是**只有 `.pbody` 的后代
     能读到它**。但有两处消费者在 `.pbody` 之外：

       ① `Mobile.prompt/confirm/pickRegion` 把弹层挂在 `.phone` 下
          （见 mobile.js 里 `host = _els.phone`）—— `.pdlg` 是 `.pbody` 的
          **兄弟**不是后代。后果：弹层里的 `.pbtn--primary` 拿到
          `background: var(--p-blue)` = 无效值 → 回退成透明，而字色仍是
          `#fff` → **白底白字，按钮整个消失**（学员截图里「保存」是一片空白）。
       ② 管理端 `orgs.html` 的 `.scope__dist.is-active` 用同一个变量做
          选中态，而机构管理页完全不在手机壳里 → 同样是白底白字，
          **选中的区县名整行消失**（截图里「武进区」那行看不见）。

     同一个变量、两个相隔很远的受害点，而根因只有一处。
     提到 `:root` 后它们都能解析，且本节**不新增、不改名任何类名**
     （`check_chart.js` 的 67 类名 1:1 对照不受影响）。
     值一个都没改 —— 改的只是「谁能读到」。 */
:root {
    /* ★ 原来这四个是另起的一套灰调蓝（#4a6fa5 / #3f6089），截图上标题条与
       标签栏选中态都发灰，和管理端/机构端的蓝对不上 —— 同一个产品两种蓝。
       现在直接引用项目主色色板，以后调主题色这里自动跟着走。
       （配色没有契约守卫，只能靠截图发现，靠这一处单一真相防它再漂。） */
    --p-blue: var(--brand-600);
    --p-blue-dark: var(--brand-700);
    --p-blue-50: var(--c1-soft);
    --p-blue-100: #dbe7ff;
    --p-ok: #16a34a;
    --p-ok-bg: #ecfdf3;
    --p-warn: #d97706;
    --p-warn-bg: #fffbeb;
    --p-err: #dc2626;
    --p-err-bg: #fef2f2;
}

/* ---------- 原生标题条（截图最上面那根蓝条） ----------
   截图里标题条是**纯色 + 白字 + 左右两个图标**，没有返回箭头。
   子页才用 `.pnavbar`（有 ‹ 返回）。 */
.ptitlebar {
    position: sticky;
    top: 0;
    z-index: 30;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 10px;
    height: 48px;
    padding: 0 14px;
    background: var(--p-blue);
    color: #fff;
}
.ptitlebar__title {
    flex: 1;
    font-size: 16px;
    font-weight: 600;
    letter-spacing: .4px;
    text-align: center;
}
.ptitlebar__icon {
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    font-size: 17px;
    cursor: pointer;
}
.ptitlebar__icon:hover { background: rgba(255, 255, 255, .16); }
.ptitlebar__icon:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }
.ptitlebar__icon--dot { position: relative; }
.ptitlebar__icon--dot::after {
    content: "";
    position: absolute;
    top: 5px;
    right: 5px;
    width: 7px;
    height: 7px;
    border: 1.5px solid var(--p-blue);
    border-radius: 50%;
    background: #ef4444;
}

/* ---------- 原生子页导航条（有返回箭头） ---------- */
.pnav {
    position: sticky;
    top: 0;
    z-index: 30;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 8px;
    height: 46px;
    padding: 0 12px;
    background: var(--p-blue);
    color: #fff;
}
.pnav__back {
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
}
.pnav__back:hover { background: rgba(255, 255, 255, .16); }
.pnav__title { flex: 1; font-size: 16px; font-weight: 600; text-align: center; }
.pnav__right {
    min-width: 32px;
    padding: 4px 10px;
    font-size: 13px;
    text-align: right;
    white-space: nowrap;
    cursor: pointer;
}

/* ---------- 四宫格（截图第二块） ----------
   截图里四个入口是**彩色 emoji + 下划小字**，两行两列不是四列。
   之前按 4 列排（.home-grid）是照搬桌面横屏的密度；真机 390px 宽
   里挤 4 个 46px 图标 + 4 个 4 字标签，最小的那格只有 88px，
   「培训报名」四个字会被压到换行。两行两列才是手机上的正确密度。 */
.pgrid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 10px;
    padding: 16px 14px 18px;
    border-bottom: 8px solid var(--ink-100);
    background: #fff;
}
.pgrid__cell {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 12px;
    border-radius: var(--r-md);
    background: linear-gradient(135deg, var(--p-blue-50), #fff);
    border: 1px solid var(--p-blue-100);
    cursor: pointer;
    transition: transform var(--t-fast) var(--ease);
}
.pgrid__cell:active { transform: scale(.98); }
.pgrid__cell:hover { border-color: var(--p-blue); }
.pgrid__icon {
    display: grid;
    place-items: center;
    flex-shrink: 0;
    width: 40px;
    height: 40px;
    border-radius: 10px;
    background: #fff;
    font-size: 22px;
    box-shadow: var(--sh-xs);
}
.pgrid__text { min-width: 0; }
.pgrid__label {
    color: var(--ink-900);
    font-size: 14.5px;
    font-weight: 600;
    white-space: nowrap;
}
.pgrid__desc {
    margin-top: 2px;
    color: var(--ink-500);
    font-size: 11.5px;
    line-height: 1.5;
}

/* ---------- 区块标题（截图里「我的培训」那行小灰字） ---------- */
.psec {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 15px 15px 8px;
}
.psec__title {
    color: var(--ink-800);
    font-size: 14.5px;
    font-weight: 600;
}
.psec__more {
    margin-left: auto;
    color: var(--ink-400);
    font-size: 12.5px;
    cursor: pointer;
}

/* ---------- 培训卡片（截图第三块：标题 + 右侧状态 + 副行 + chevron） ---------- */
.pitem {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0 12px 10px;
    padding: 14px 15px;
    border-radius: var(--r-md);
    background: #fff;
    box-shadow: var(--sh-xs);
    cursor: pointer;
}
.pitem:active { background: var(--p-blue-50); }
.pitem--warn { border-left: 3px solid var(--p-warn); }
.pitem--err  { border-left: 3px solid var(--p-err); }
.pitem__main { flex: 1; min-width: 0; }
.pitem__title {
    /* ★ display:block 不能只写在 .stage--desk 里（那是已取消的电脑形态，
       规则永远匹配不到手机页）。父级 .pitem__main 是纵向 flex 容器，
       flex 容器里的 inline span 会被匿名块包起来 → 标题与副行并排一行。 */
    display: block;
    color: var(--ink-900);
    font-size: 14.5px;
    font-weight: 600;
    line-height: 1.45;
}
.pitem__sub {
    display: block;
    margin-top: 4px;
    color: var(--ink-500);
    font-size: 12px;
    line-height: 1.6;
}
.pitem__badge {
    flex-shrink: 0;
    padding: 3px 9px;
    border-radius: var(--r-full);
    font-size: 11.5px;
    font-weight: 600;
    white-space: nowrap;
}
.pitem__badge--ok   { background: var(--p-ok-bg);   color: var(--p-ok); }
.pitem__badge--warn { background: var(--p-warn-bg); color: var(--p-warn); }
.pitem__badge--err  { background: var(--p-err-bg);  color: var(--p-err); }
.pitem__badge--off  { background: var(--ink-100);   color: var(--ink-500); }
.pitem__arrow { flex-shrink: 0; color: var(--ink-300); font-size: 16px; }

/* 考试卡下面的操作按钮条（`m/exams.html` / `m/result.html`）。
   它拼在 `.pitem` **外面**，所以左右内边距必须自己写一份，
   才能和卡片的左右缘对齐（卡片是 `margin: 0 12px`）。
   原先这两处是**内联样式** `style="padding:0 12px 12px"`，
   电脑端那一档（`.stage--desk`）没有选择器能盖掉内联样式，
   于是按钮在宽屏上永远缩在左边 12px 处 —— 换成类名就是为了这里能覆盖。 */
.pitem-actions { padding: 0 12px 12px; }

/* ---------- 结果大卡（交卷后那一屏） ---------- */
.presult {
    margin: 14px 12px 0;
    padding: 26px 18px 22px;
    border-radius: var(--r-lg);
    text-align: center;
}
.presult--ok  { background: linear-gradient(160deg, #e8f8ee, #fff); border: 1px solid #bbebcd; }
.presult--err { background: linear-gradient(160deg, #fdeeee, #fff); border: 1px solid #f6c9c9; }
.presult--wait{ background: linear-gradient(160deg, var(--p-blue-50), #fff); border: 1px solid var(--p-blue-100); }
.presult__icon { font-size: 46px; line-height: 1; }
.presult__verdict {
    margin-top: 12px;
    font-size: 24px;
    font-weight: 700;
    letter-spacing: 2px;
}
.presult--ok   .presult__verdict { color: var(--p-ok); }
.presult--err  .presult__verdict { color: var(--p-err); }
.presult--wait .presult__verdict { color: var(--p-blue); font-size: 20px; }
.presult__meta {
    margin-top: 10px;
    color: var(--ink-600);
    font-size: 13px;
    line-height: 1.8;
}
.presult__note {
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px dashed var(--ink-200);
    color: var(--ink-500);
    font-size: 12px;
    line-height: 1.75;
    text-align: left;
}

/* ---------- 补考提示条 ---------- */
.pretake {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin: 12px;
    padding: 13px 14px;
    border-radius: var(--r-md);
    background: var(--p-warn-bg);
    border: 1px solid #fde68a;
}
.pretake--off { background: var(--ink-50); border-color: var(--ink-200); }
.pretake__icon { flex-shrink: 0; font-size: 17px; }
.pretake__text { flex: 1; color: var(--ink-700); font-size: 13px; line-height: 1.7; }
.pretake__text b { color: var(--ink-900); }

/* ---------- 底部固定操作条 ----------
   手机上主按钮必须在拇指够得到的位置，不能混在卡片里。 */
.pfoot {
    flex-shrink: 0;
    display: flex;
    gap: 10px;
    padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
    border-top: 1px solid var(--ink-100);
    background: #fff;
}
.pbtn {
    flex: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    height: 44px;
    padding: 0 16px;
    border: 1px solid var(--ink-200);
    border-radius: var(--r-md);
    background: #fff;
    color: var(--ink-800);
    font-family: inherit;
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
}
.pbtn:hover:not(:disabled) { background: var(--ink-50); }
.pbtn:disabled { opacity: .5; cursor: not-allowed; }
.pbtn--primary {
    border-color: var(--p-blue);
    background: var(--p-blue);
    color: #fff;
}
.pbtn--primary:hover:not(:disabled) { background: var(--p-blue-dark); border-color: var(--p-blue-dark); }
.pbtn--ok { border-color: var(--p-ok); background: var(--p-ok); color: #fff; }
.pbtn--ok:hover:not(:disabled) { background: #15803d; border-color: #15803d; }
.pbtn--ghost { border-color: transparent; background: transparent; color: var(--p-blue); }
.pbtn--sm { flex: none; height: 34px; padding: 0 12px; font-size: 13px; font-weight: 500; }

/* ---------- 机构卡（属地匹配结果） ---------- */
.porg {
    margin: 0 12px 10px;
    border-radius: var(--r-md);
    background: #fff;
    box-shadow: var(--sh-xs);
    overflow: hidden;
}
.porg__head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 13px 14px 11px;
}
.porg__name { flex: 1; color: var(--ink-900); font-size: 14.5px; font-weight: 600; }
.porg__tag {
    flex-shrink: 0;
    padding: 2px 8px;
    border-radius: var(--r-full);
    background: var(--p-blue-50);
    color: var(--p-blue);
    font-size: 11px;
    font-weight: 600;
}
.porg__tag--near { background: var(--ink-100); color: var(--ink-500); }
.porg__addr {
    padding: 0 14px 11px;
    color: var(--ink-500);
    font-size: 12px;
    line-height: 1.6;
}
.porg__class {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 14px;
    border-top: 1px solid var(--ink-100);
    background: #fcfdff;
}
.porg__class:active { background: var(--p-blue-50); }
.porg__class-main { flex: 1; min-width: 0; }
.porg__class-name { color: var(--ink-800); font-size: 13.5px; font-weight: 500; }
.porg__class-time {
    margin-top: 3px;
    color: var(--ink-500);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
}
.porg__enrolled {
    flex-shrink: 0;
    padding: 3px 9px;
    border-radius: var(--r-full);
    background: var(--p-ok-bg);
    color: var(--p-ok);
    font-size: 11.5px;
    font-weight: 600;
}

/* 锁住的班次行（同一时间只能参加一个培训班时，非本班一律锁住）：
   视觉上必须"明显地不可点" —— 只改文案不改样式的话，
   学员会照旧去点，然后抱怨"点了没反应"。 */
.porg__class--locked {
    opacity: .55;
    cursor: not-allowed;
}
.porg__class--locked .porg__enrolled {
    background: var(--ink-100);
    color: var(--ink-500);
}

/* ---------- 三级联动（省市区镇选择，截图里那一行） ---------- */
.picker { position: relative; }
.ppicker__mask {
    position: fixed;
    inset: 0;
    z-index: 80;
    background: rgba(15, 23, 42, .45);
    animation: mask-in var(--t-base) var(--ease-out) forwards;
}
.ppicker {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 81;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    background: #fff;
    transform: translateY(100%);
    animation: sheet-up var(--t-base) var(--ease-out) forwards;
}
@keyframes sheet-up { to { transform: translateY(0); } }
.ppicker__head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 13px 14px;
    border-bottom: 1px solid var(--ink-100);
}
.ppicker__title { flex: 1; color: var(--ink-900); font-size: 15px; font-weight: 600; }
.ppicker__btn {
    padding: 4px 10px;
    border: 0;
    background: transparent;
    color: var(--ink-500);
    font-family: inherit;
    font-size: 14px;
    cursor: pointer;
}
.ppicker__btn--ok { color: var(--p-blue); font-weight: 600; }
.ppicker__tabs {
    display: flex;
    gap: 6px;
    padding: 10px 14px;
    border-bottom: 1px solid var(--ink-100);
    overflow-x: auto;
}
.ppicker__tab {
    flex-shrink: 0;
    padding: 5px 12px;
    border: 0;
    border-radius: var(--r-full);
    background: var(--ink-100);
    color: var(--ink-600);
    font-family: inherit;
    font-size: 13px;
    cursor: pointer;
}
.ppicker__tab.is-active { background: var(--p-blue); color: #fff; font-weight: 600; }
.ppicker__list { max-height: 240px; overflow-y: auto; }
.ppicker__opt {
    display: flex;
    align-items: center;
    gap: 9px;
    width: 100%;
    padding: 13px 16px;
    border: 0;
    border-bottom: 1px solid var(--ink-100);
    background: #fff;
    color: var(--ink-800);
    font-family: inherit;
    font-size: 14.5px;
    text-align: left;
    cursor: pointer;
}
.ppicker__opt:hover { background: var(--ink-50); }
.ppicker__opt.is-active { color: var(--p-blue); font-weight: 600; background: var(--p-blue-50); }
.ppicker__opt-arrow { margin-left: auto; color: var(--ink-300); font-size: 15px; }
.ppicker__empty { padding: 34px 16px; color: var(--ink-400); font-size: 13px; text-align: center; }

/* ---------- 属地选择（10-06 轮五级地址） ----------
   省 / 市是常量，做成一行只读抬头而不是下拉框：
   多一个「只有一个选项的下拉框」只会在页面上占位置，
   并让使用者以为漏了东西。 */
.ppicker__hint {
    padding: 7px 14px;
    background: var(--ink-50);
    color: var(--ink-500);
    font-size: 12.5px;
    border-bottom: 1px solid var(--ink-100);
}
/* 第二列（街道/乡镇/园区）的分组小标题 */
.ppicker__sub {
    padding: 8px 16px 4px;
    color: var(--ink-400);
    font-size: 12px;
}

/* ---------- 两段联动（左区县 / 右街道）—— 按 iOS 地址选择器的规格 ----------
   布局：两栏**各 50%**，各自独立滚动 + 滚动吸附（`scroll-snap`）。
   ★ 为什么不是「左窄右宽」：iOS 的地址选择器两栏等宽，
     而 50% 之后左栏有约 187px，8 个字的 «常州市经济开发区»
     在 16px 字号下约 128px —— **放得下**。
   ★ 为什么仍保留 `nowrap + ellipsis`：字典里将来可能出现更长的名字，
     而「折行」会让两列行高对不上（当初写死 104px 就是为了避免这个）。
     宽度可以放宽，**不折行这条不能松**。
   ★ 吸附居中为什么需要 JS：要让首项/末项也能停在中间，
     容器上下必须各留半行高的 padding，
     而**百分比 padding 相对的是宽度不是高度** ——
     纯 CSS 写不出来，只能实测后写成 `--wheel-pad`。 */
.ppicker--two {
    display: flex;
    flex-direction: column;
    max-height: 78vh;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    background: var(--ios-grouped);
}

/* 头部做成 iOS 导航栏：标题居中、按钮是蓝色文字、底部一条发丝线 */
.ppicker--two .ppicker__head {
    padding: 12px 16px;
    border-bottom: 0.5px solid var(--ios-sep);
    background: var(--ios-grouped);
}
.ppicker--two .ppicker__title {
    flex: 1;
    min-width: 0;
    text-align: center;
    font-size: 17px;
    font-weight: 600;
    color: var(--ios-label);
    /* ★ 标题**不折行**：14 字的「选择属地（省 / 市 / 区县 / 街道）」在 375 宽里
       放不下，实测「道」字掉到第二行、整个头高了一倍。
       长层级信息属于次级行（hint），不属于标题 —— 这是 iOS 的分层习惯。 */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ppicker--two .ppicker__btn {
    min-width: 44px;                 /* iOS 触摸下限 */
    min-height: 44px;
    padding: 0;
    color: var(--ios-blue);
    font-size: 16px;
    font-weight: 400;
    text-align: center;
}
/* 左侧「取消」需要占位对齐，居中按钮按文字宽度即可 */
.ppicker--two .ppicker__btn[data-x="cancel"] { text-align: left; }
.ppicker--two .ppicker__btn[data-x="ok"] { text-align: right; }
.ppicker--two .ppicker__btn:active { opacity: .35; }

/* 「江苏省 / 常州市」是常量，做成 iOS 的次级说明行 */
.ppicker--two .ppicker__hint {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 16px 6px;
    background: var(--ios-grouped);
    border-bottom: 0;
    color: var(--ios-secondary);
    font-size: 13px;
}
.ppicker__hint-t { flex: 1; min-width: 0; }
/* 「清除」是破坏性动作，压在次级行右侧而不是头部 ——
   头部右侧那个位置是「确定」，两者并排会让人犹豫点哪个。 */
.ppicker__hint-x {
    flex: none;
    padding: 6px 2px;
    border: 0;
    background: transparent;
    color: var(--p-err);
    font-family: inherit;
    font-size: 13px;
    cursor: pointer;
}
.ppicker__hint-x:active { opacity: .35; }

/* 页签 → iOS 分段控件：灰轨 + 白色选中块 */
.ppicker--two .ppicker__tabs {
    /* 轨道按内容定宽（inline-flex），而不是撑满一行 */
    display: inline-flex;
    gap: 2px;
    margin: 6px 16px 10px;
    padding: 2px;
    overflow: visible;
    border: 0;
    border-radius: 9px;
    background: rgba(118, 118, 128, .12);
}
.ppicker--two .ppicker__tab {
    /* ★ `flex:none` 而不是 `flex:1`：只有一个 chip 时 `flex:1` 让它撑满整条轨，
       白色选中块变成一条通栏横幅，完全不像分段控件。
       iOS 的分段控件本来就是**按内容定宽**、轨道随之收窄的。 */
    flex: none;
    min-height: 30px;
    padding: 0 12px;
    border-radius: 7px;
    background: transparent;
    color: var(--ios-label);
    font-size: 13px;
    font-weight: 500;
    text-align: center;
    /* ★ 必须显式复位：第一版只改了 background/颜色，
       而基础规则里没有 `box-shadow` 默认值，
       于是选中块的投影从**继承来的按钮态**漏了出来。 */
    box-shadow: none;
    transition: background .16s var(--ease-out), box-shadow .16s var(--ease-out);
}
.ppicker--two .ppicker__tab.is-active {
    background: #fff;
    color: var(--ios-label);
    font-weight: 600;
    box-shadow: 0 3px 8px rgba(0, 0, 0, .12), 0 1px 1px rgba(0, 0, 0, .04);
}

/* ---------- 双栏本体 ---------- */
.ppicker__body {
    display: flex;
    flex: 1;
    min-height: 0;                /* 不写 0 收缩不下去，右栏不会滚 */
    overflow: hidden;
    background: var(--ios-grouped);
}
/* ★ 双栏模式改「固定 5 行高的滚轮窗」（iOS UIPickerView 的观感）。
   之前 body 高度是被**最高那栏的内容**撑出来的（街道多 → 480px），
   于是区县多的城市里左栏内容比容器矮 → scrollTop 恒为 0 → 选不中。
   行高/窗高由 padColumns 实测写进 --wheel-h / --wheel-vh。 */
.ppicker--two .ppicker__body {
    flex: 0 0 auto;
    height: var(--wheel-vh, 240px);
    position: relative;
    /* 关住层叠：两栏不去和 `.ppicker__head` 的白底抢层级 */
    isolation: isolate;
}
/* 中心指示带：上下两条线 + 极浅底。吸附后选中行正好落在这条带里，
   上下各露半行 —— 「滚轮」就是这么来的。
   ★ pointer-events:none 是必须的，否则会吃掉两栏的点击。 */
.ppicker--two .ppicker__body::before {
    content: '';
    position: absolute;
    /* ★ 必须比两栏高。原来写 `z-index:1` 却看不见：
       `.ppicker__col` 是它的**子元素**且自带 `overflow-y:auto`，
       在 DOM 顺序里排在 `::before` **之后** —— 同层内后画的盖住先画的，
       所以指示带被两栏内容整块压住。 */
    z-index: 5;
    left: 0;
    right: 0;
    top: 50%;
    height: var(--wheel-h, 48px);
    transform: translateY(-50%);
    /* ★ 用 1px 而不是 0.5px：0.5px 是 iOS 在 2x/3x 屏上的发丝线写法，
       但浏览器缩放截图 / 1x 屏上常被渲染成 0 像素 —— 等于不存在。
       这里要的是「一定看得见」，不是像素级还原。 */
    border-top: 1px solid rgba(60, 60, 67, .32);
    border-bottom: 1px solid rgba(60, 60, 67, .32);
    background: rgba(118, 118, 128, .1);
    pointer-events: none;
}
.ppicker__col {
    flex: 0 0 50%;                /* ★ 各占 50% */
    width: 50%;
    min-width: 0;
    /* ★ 加固，不是当前 bug 的修复手段（反向测试推翻过最初的归因）。
       指示带 `.ppicker--two .ppicker__body::before` 是绝对定位 + z-index:5，
       而 static 元素**不参与层叠比较** —— 所以它本来就画在两栏**之上**，
       与文档序无关。实测：把 z-index 改回 1、去掉这里两行，
       指示带依然画出来（三档视口全 ✓）；改成 -1 才真的被盖住（三档全红）。

       那为什么还留着？因为 `transform` / `filter` / `will-change`
       都会让元素**生成新的层叠上下文**。哪天给两栏加了其中任何一个，
       z-index:0 这一行就是指示带不被吞掉的唯一保险。
       —— 注释必须如实写「加固」：写成「修复」会让下一个人
       以为删掉也没事，而那正是当初被误导的方向。
       ★ 反过来 `isolation:isolate` 不能替代：它把两栏关进自己的上下文，
         指示带反而永远盖不住（方向反了）。 */
    position: relative;
    z-index: 0;
    overflow-y: auto;
    overscroll-behavior: contain;  /* 滚到头不要连带滚背后的页面 */
    -webkit-overflow-scrolling: touch;
    scroll-snap-type: y proximity;
    /* 上下各留半行高，首项/末项也能吸附到中间。
       值由 JS 按实测行高写进 --wheel-pad（见 pickRegion 的 padColumns）。 */
    padding-top: var(--wheel-pad, 0px);
    padding-bottom: var(--wheel-pad, 0px);
}
.ppicker__col--d { border-right: 0.5px solid var(--ios-sep); }
/* 左栏是「索引」感：略浅的底，让人一眼分清两栏的职责 */
.ppicker__col--d { background: rgba(118, 118, 128, .06); }

/* ---------- 列表项（两栏共用一套，iOS 分组列表的观感） ---------- */
.ppicker__d,
.ppicker__opt {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: 48px;            /* ≥iOS 44pt 触摸下限 */
    /* ★ 内边距 12px 而不是 16px：半宽只有 187px，
       16px×2 加上「就选『天宁区』」与尾注，实测正文只剩约 65px，
       文字被挤成「就选『天…』」。iOS 自己的紧凑列表也是 12~16。 */
    padding: 0 12px;
    border: 0;
    background: transparent;
    color: var(--ios-label);
    font-family: inherit;
    font-size: 16px;              /* iOS 正文 */
    line-height: 1.35;
    text-align: left;
    scroll-snap-align: center;
    cursor: pointer;
}
.ppicker__d {
    justify-content: space-between;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ppicker__opt { justify-content: flex-start; }

/* 选中态：iOS 是**蓝色文字**，不是蓝底 —— 底色反而是「分隔」用的 */
.ppicker__d.is-active,
.ppicker__opt.is-active {
    background: transparent;
    color: var(--ios-blue);
    font-weight: 600;
}
/* 左栏选中项给一道左侧蓝条。
   ★ 画在**左边**：右边紧挨白底右列（iOS 这里是 0.5px 发丝线），
     3px 的条会被右列盖住 —— 实测「选中了但看不出选了哪个」。 */
.ppicker__d.is-active::before {
    content: '';
    position: absolute;
    /* ★ `left:0` 是相对**内容盒**的，而 `.ppicker__d` 左右各有 16px padding，
       于是蓝条落在内缩 16px 处、看起来像浮在栏外。
       负 16px 让它正好压在两栏之间那条发丝线上。 */
    left: -12px;
    top: 50%;
    width: 3px;
    height: 22px;
    border-radius: 0 2px 2px 0;
    background: var(--ios-blue);
    transform: translateY(-50%);
}
/* 有下级的区县给一个 ›，让「点它右边会变」这件事可预期。
   用绝对定位而不是 float：float 参与行内布局，
   碰上 `nowrap + ellipsis` 时会把名字挤掉一个字。 */
.ppicker__d[data-has]::after {
    content: '›';
    position: absolute;
    right: 12px;
    top: 50%;
    color: var(--ios-tertiary);
    font-size: 17px;
    transform: translateY(-50%);
}
.ppicker__d { position: relative; }

/* 行分隔线：iOS 是从文字起点开始的发丝线，不是通栏 */
.ppicker__d + .ppicker__d,
.ppicker__opt + .ppicker__opt {
    box-shadow: inset 0 -0.5px 0 var(--ios-sep);
}

/* ---------- 「仅选到区县」 ----------
   10-17 轮**整条删除**（连同 JS 里那一个按钮）。
   原因不是「样式不好看」，是它和「滚动即选中」打架：
   右列第一格被一个「不是街道」的动作占着，指示带落在它上面时
   用户看到的是「就选『新北区』」被选中，而它其实不表示任何街道。
   「只到区县」这个合法状态改由「该区县无下级 → 右列空态 → 确定」承担。 */

.ppicker__empty {
    padding: 40px 16px;
    color: var(--ios-secondary);
    font-size: 15px;
    text-align: center;
}

/* ---------- 机构负责范围（需求 6：沿用旧系统两栏式） ----------
   左栏区县 + 右栏复选框。宽度写死是有意的：
   区镇名最长 4 字（«遥观镇»），固定宽能让两栏在不同区县间
   切换时**不跳动** —— 会跳的布局在快速勾选时很难受。 */
.scope {
    display: flex;
    gap: 0;
    border: 1px solid var(--ink-200);
    border-radius: var(--r-md);
    overflow: hidden;
    background: #fff;
}
.scope__side {
    flex: 0 0 128px;
    max-height: 260px;
    overflow-y: auto;
    border-right: 1px solid var(--ink-200);
    background: var(--ink-50);
}
.scope__dist {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
    width: 100%;
    padding: 9px 12px;
    border: 0;
    border-bottom: 1px solid var(--ink-100);
    background: transparent;
    color: var(--ink-700);
    font-family: inherit;
    font-size: 13.5px;
    text-align: left;
    cursor: pointer;
}
.scope__dist:hover { background: var(--ink-100); }
.scope__dist.is-active { background: var(--p-blue); color: #fff; font-weight: 600; }
/* 已勾数量徽标。数字为 0 时不显示 —— 一个恒为 0 的徽标
   是纯噪声，而「这个区一个都没勾」在勾选框那里本来就看得出来。 */
.scope__badge { font-size: 11.5px; opacity: .85; }
.scope__badge:empty, .scope__dist .scope__badge[data-zero] { display: none; }
.scope__main { flex: 1 1 auto; max-height: 260px; overflow-y: auto; padding: 6px 0; }
.scope__opt {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 7px 14px;
    font-size: 13.5px;
    color: var(--ink-800);
    cursor: pointer;
}
.scope__opt:hover { background: var(--ink-50); }
.scope__opt--dist {
    border-bottom: 1px solid var(--ink-100);
    background: var(--p-blue-50);
    font-weight: 600;
    color: var(--ink-900);
}
/* 「原已勾选」提示：新建机构时全是 false，只在编辑既有机构时出现。
   作用是让操作人知道哪些是本来就有的，而不是他刚勾上的 —— 勾错了
   才发现「原来这家就负责这儿」就晚了。 */
.scope__was {
    margin-left: auto;
    color: var(--ink-400);
    font-size: 11.5px;
    font-style: normal;
}
.scope__opt input[type="checkbox"] { width: 15px; height: 15px; accent-color: var(--p-blue); }

@media (max-width: 640px) {
    /* 窄屏下两栏会挤到看不清：改为上下（区县在下拉里选） */
    .scope { flex-direction: column; }
    .scope__side { flex: none; max-height: 120px; border-right: 0; border-bottom: 1px solid var(--ink-200); }
    .scope__main { max-height: 200px; }
}

/* ---------- 属地字典右栏（#119 `web/admin/regions.html`） ----------
   复用 `.scope` 的两栏骨架，只补右栏「标题栏 + 表格」这两块。
   为什么不用 `.card__head` 的写法：那套是「整页一个卡片」，
   这里右栏本身被 `.scope__main` 限高滚动，标题必须**粘在表格上方** ——
   30 个街道的表格滚到第 20 行时，看不见自己在哪个区名下。 */
.dict-panel__head {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: 10px 14px;
    border-bottom: 1px solid var(--ink-200);
    background: var(--ink-50);
}
.dict-panel__head > div { flex: 1 1 auto; min-width: 0; }
.dict-panel__empty { padding: 26px 18px; color: var(--ink-500); font-size: 13px; line-height: 1.85; }
.dict-panel__empty b { color: var(--ink-800); }

/* ---------- 历史成绩行（需求 3 的输出） ---------- */
.phist { margin: 0 12px 10px; border-radius: var(--r-md); background: #fff; box-shadow: var(--sh-xs); }
.phist__top {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 12px 14px 8px;
}
.phist__name { flex: 1; color: var(--ink-900); font-size: 14px; font-weight: 600; }
.phist__score { color: var(--ink-900); font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; }
.phist__score small { color: var(--ink-400); font-size: 11.5px; font-weight: 400; }
.phist__rows { padding: 0 14px 11px; }
.phist__row {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 4px 0;
    font-size: 12.5px;
    line-height: 1.6;
}
.phist__k { flex-shrink: 0; width: 62px; color: var(--ink-400); }
.phist__v { flex: 1; min-width: 0; color: var(--ink-700); }

/* ---------- 统计条（历史成绩顶部：共 N 次 / 合格 M / 通过率） ---------- */
.pstats {
    display: flex;
    margin: 12px 12px 0;
    border-radius: var(--r-md);
    background: linear-gradient(135deg, var(--p-blue), #3f6fe0);
    color: #fff;
    overflow: hidden;
}
.pstats__cell {
    flex: 1;
    padding: 14px 6px;
    text-align: center;
}
.pstats__cell + .pstats__cell { border-left: 1px solid rgba(255, 255, 255, .2); }
.pstats__n { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
.pstats__k { margin-top: 3px; font-size: 11.5px; opacity: .85; }

/* ---------- 学员端首页指标卡（2×2，与桌面 .pstats 横条不是同一套） ---------- */
.pstats2 {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
    padding: 12px 12px 0;
}
.pstat2 {
    padding: 12px 12px 11px;
    border: 1px solid var(--line);
    border-radius: var(--r);
    background: #fff;
    /* 左侧色条：一眼分出 ok / warn / err / off，不靠颜色文字双重编码。
       色名取 app.css 根级真正定义过的那几个（--success/--warning/--danger），
       不是 --p-green 之类 —— 后者不存在，`var()` 取不到会把整条声明丢掉。 */
    box-shadow: inset 3px 0 0 var(--ink-300);
}
.pstat2--ok   { box-shadow: inset 3px 0 0 var(--success); }
.pstat2--warn { box-shadow: inset 3px 0 0 var(--warning); }
.pstat2--err  { box-shadow: inset 3px 0 0 var(--danger); }
.pstat2__label {
    font-size: 12.5px;
    color: var(--ink-500);
}
/* 数字用 tabular-nums：四个卡里两个是数字（3 / 2），等宽才不左右跳 */
.pstat2__value {
    margin-top: 6px;
    font-size: 22px;
    font-weight: 700;
    line-height: 1.15;
    font-variant-numeric: tabular-nums;
    color: var(--ink-900);
}
.pstat2__hint {
    margin-top: 4px;
    font-size: 11.5px;
    line-height: 1.4;
    color: var(--ink-400);
    /* 提示语最长 11 个汉字（完成全部课程才能参加受限考试），
       不 clamp 的话会把整张卡拉成三行，四张卡高度参差 */
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

/* ---------- 提示条（手机端窄，用 12.5px） ---------- */
.pnote {
    display: flex;
    /* ★ 必须允许换行：`.pnote` 是 flex 容器（为了「图标 + 文字」两段式），
       而正文里的 <b> **也是直接子元素** —— 默认不换行时它会被当成一个
       独立 flex 项、被压缩成一列竖排的单字（实测：「由街道决定」竖着排）。
       正确写法是把正文整体包进一个 <span>，这条规则是"忘了包"时的兜底。 */
    flex-wrap: wrap;
    gap: 8px;
    margin: 12px;
    padding: 11px 13px;
    border-radius: var(--r-md);
    background: var(--p-blue-50);
    border: 1px solid var(--p-blue-100);
    color: var(--ink-700);
    font-size: 12.5px;
    line-height: 1.75;
}
.pnote--warn { background: var(--p-warn-bg); border-color: #fde68a; }
.pnote--ok   { background: var(--p-ok-bg);   border-color: #bbebcd; }
.pnote--err  { background: var(--p-err-bg);  border-color: #f6c9c9; }
.pnote b { color: var(--ink-900); flex: 0 0 auto; }

/* ---------- 手机端模态（确认框） ----------
   复用 `.psheet` 的视觉语言但**底部弹出** —— 手机上从底部弹更符合
   拇指操作，也避免遮住顶部标题条。 */
.pdlg-mask {
    position: fixed;
    inset: 0;
    z-index: 90;
    display: flex;
    align-items: flex-end;
    background: rgba(15, 23, 42, .45);
    animation: mask-in var(--t-base) var(--ease-out) forwards;
}
.pdlg {
    width: 100%;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    background: #fff;
    transform: translateY(100%);
    animation: sheet-up var(--t-base) var(--ease-out) forwards;
}
.pdlg__body { padding: 22px 20px 18px; color: var(--ink-800); font-size: 14.5px; line-height: 1.8; }
.pdlg__title { margin-bottom: 8px; color: var(--ink-900); font-size: 16px; font-weight: 600; }
.pdlg__foot { display: flex; gap: 10px; padding: 0 14px calc(14px + env(safe-area-inset-bottom)); }
.pdlg__foot .pbtn + .pbtn { margin-left: 0; }

/* ---------- prompt 弹层里的输入框（10-06 轮） ----------
   字号必须 ≥16px：iOS Safari 对小于 16px 的输入框会在聚焦时
   自动放大页面，弹层随之被顶出可视区。 */
.pinput {
    display: block;
    width: 100%;
    padding: 10px 12px;
    border: 1px solid var(--ink-200);
    border-radius: var(--r-md);
    background: #fff;
    color: var(--ink-900);
    font-family: inherit;
    font-size: 16px;      /* 见上方说明，勿下调 */
    line-height: 1.5;
}
.pinput:focus { outline: 2px solid var(--p-blue); outline-offset: -1px; border-color: var(--p-blue); }
.pinput--area { resize: vertical; min-height: 72px; }

/* ---------- `<select>` 必须单列一条（实测切字事故） ----------
   `.pinput` 的 `padding:10px 12px` 是按 `<input>` 定的。
   `<select>` 是 replaced 元素，**UA 默认 `overflow: clip`**，
   竖向 padding 一旦被 `min-height` 压掉，文字就从上下两边被切 ——
   症状是「下拉框里只剩字的中间一条」，不报错、无 console、静态检查全绿。
   同一个筛选条里按钮（`padding:0`）好好的，于是更像是字体问题。

   这里把竖向 padding 清零、行高交给 `line-height`，
   让**文字由 `min-height` 居中**而不是被裁。左右 padding 保留，
   右侧要给系统箭头让位（去掉会在部分安卓 WebView 上压到文字）。 */
select.pinput {
    padding: 0 28px 0 12px;
    line-height: normal;
    /* `appearance` 不改：去掉后各平台会画出风格完全不同的原生箭头，
       而这里要的就是「看起来就是个正常下拉框」，不该由我们去模仿。 */
}
/* 高度统一：`.pinput` 的 16px 字号配 40px 盒子是实测不裁的最小值 */
select.pinput { min-height: 40px; }

/* ---------- 手机端 toast（挂在手机壳内，不跑到 body 上） ---------- */
.ptoast {
    position: absolute;
    left: 50%;
    bottom: 78px;
    z-index: 95;
    max-width: 80%;
    padding: 10px 16px;
    border-radius: var(--r-md);
    background: rgba(15, 23, 42, .88);
    color: #fff;
    font-size: 13.5px;
    line-height: 1.6;
    text-align: center;
    transform: translateX(-50%);
    animation: mask-in var(--t-base) var(--ease-out) forwards;
}
.ptoast--ok   { background: rgba(22, 101, 52, .93); }
.ptoast--warn { background: rgba(146, 64, 14, .93); }
.ptoast--err  { background: rgba(153, 27, 27, .93); }
@keyframes mask-in { from { opacity: 0; } to { opacity: 1; } }

/* ---------- 证书卡 ---------- */
.pcert { margin: 0 12px 10px; border-radius: var(--r-md); background: #fff; box-shadow: var(--sh-xs); overflow: hidden; }
.pcert__head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 13px 14px 10px;
    border-bottom: 1px solid var(--ink-100);
}
.pcert__title { flex: 1; color: var(--ink-900); font-size: 14px; font-weight: 600; }
.pcert__body { padding: 10px 14px 12px; }
.pcert__code {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 10px;
    padding: 9px 11px;
    border-radius: var(--r-sm);
    background: var(--ink-50);
}
.pcert__code-v { flex: 1; color: var(--ink-800); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; }
.pcert__foot { display: flex; gap: 8px; padding: 0 14px 13px; }

/* ---------- 可拖拽行（管理端视频课程/章节排序，10-07 轮新增） ----------
 *
 * 为什么手写一套而不用现有的 .pill / 表格样式：
 * 这三块 —— 手柄列、拖动中的半透明、当前选中态 —— 在表格里都有对应物，
 * 但表格是「一格一格对齐」的，拖拽时会整行移动，
 * 套表格样式会出现「半个边框」「列宽跳动」。行式布局才拖得动。
 *
 * 手柄的 `cursor: grab` 与 `.is-dragging` 的 `grabbing` 是一对：
 * 没有 grabbing 时，用户松手前不会知道「现在是在拖还是在选」。
 * 手柄同时是可聚焦的（tabindex 由 HTML 给出），键盘用户能聚焦但无法拖动 ——
 * 这是已知的可访问性缺口，排序另有「编辑」入口可改，不靠拖拽作为唯一途径。
 */
.drag-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border-bottom: 1px solid var(--ink-100);
    background: var(--surface);
    transition: background var(--dur-fast) var(--ease-out);
}
.drag-row:last-child { border-bottom: 0; }
.drag-row:hover { background: var(--surface-2); }

/* 选中态（当前正在编辑的那一门课）。用左边框而不是底色 ——
   底色在 hover 时会被盖掉，拖动时也会闪。 */
.drag-row.is-on { background: var(--brand-50); box-shadow: inset 3px 0 0 var(--signal-700); }

.drag-row.is-dragging { opacity: .45; }

.drag-row__handle {
    flex: 0 0 auto;
    padding: 2px 4px;
    color: var(--text-3);
    font-size: 15px;
    line-height: 1;
    cursor: grab;
    user-select: none;
    border-radius: var(--r-sm);
}
.drag-row__handle:hover { background: var(--ink-100); color: var(--text-2); }
.drag-row__handle:active { cursor: grabbing; }

.drag-row__main { flex: 1; min-width: 0; cursor: pointer; }
.drag-row__title {
    color: var(--text);
    font-size: 14px;
    font-weight: 600;
    display: flex; align-items: center; gap: 8px;
}
/* 次要行（时长 + 地址）会很长，min-width:0 + 省略号防止把右侧按钮挤出屏幕 */
.drag-row__main > .text-3 {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.drag-row__actions { flex: 0 0 auto; display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }

@media (max-width: 720px) {
    .drag-row { flex-wrap: wrap; }
    .drag-row__actions { width: 100%; justify-content: flex-start; }
}

/* ---------- 课程分组（学员端视频页，10-07 轮新增） ---------- */
.course-block { border-bottom: 1px solid var(--ink-100); }
.course-block:last-child { border-bottom: 0; }

.course-head {
    padding: 10px 14px 8px;
    background: var(--surface-2);
    border-bottom: 1px solid var(--ink-100);
}
.course-head__title {
    color: var(--text);
    font-size: 14.5px;
    font-weight: 600;
    margin-bottom: 2px;
}
/* 空课程必须显式说明「0 章」，而不是把这一整块藏起来 ——
   课程存在但没录章节是一个**真实且常见**的中间状态，
   藏起来会让运营以为保存失败、学员以为系统坏了。 */
.course-block__empty {
    padding: 14px;
    color: var(--text-3);
    font-size: 13px;
}

.lesson-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 14px;
    border-bottom: 1px solid var(--ink-100);
}
.lesson-row:last-child { border-bottom: 0; }
.lesson-row:hover { background: var(--surface-2); }
/* min-width:0 是必需的：右侧按钮是 flex:0 0 auto，
   而左侧内容不设它就撑不开容器，长标题会把「开始学习」挤出可视区。 */
.lesson-row__main { flex: 1; min-width: 0; }
.lesson-row__title {
    color: var(--text);
    font-size: 13.5px;
    font-weight: 600;
    display: flex; align-items: center; gap: 8px;
}
.lesson-row__act { flex: 0 0 auto; }

@media (max-width: 640px) {
    .lesson-row { flex-wrap: wrap; }
    .lesson-row__act { width: 100%; }
}

/* ---------- 排班网格（#138）：日期 × 时段序号 ---------- */
/* 为什么单独一套类名而不复用 .table：这里的语义是「一张占位表」，
   单元格要能容纳班次名 + 机构 + 操作三行，而 .table 的 td 是单行文本。
   硬套 .table 会得到一个挤到换行的窄列。 */
.schedgrid-wrap { overflow-x: auto; }
.schedgrid { width: 100%; border-collapse: collapse; }
.schedgrid th,
.schedgrid td { padding: var(--sp-3); border-bottom: 1px solid var(--border);
                vertical-align: top; text-align: left; }
.schedgrid thead th { font-size: 13px; color: var(--text-2); font-weight: 500;
                       background: var(--surface); position: sticky; top: 0; }
.schedgrid__date { width: 132px; white-space: nowrap; font-variant-numeric: tabular-nums; }
.schedgrid__sum { width: 64px; text-align: right !important;
                  color: var(--text-2); font-variant-numeric: tabular-nums; }
/* 已占用的格子给一点底色，空格子留白 —— 一眼看出哪天还有空位。 */
.schedgrid td:has(.schedslot) { background: var(--surface-1); }
.schedslot { display: flex; flex-direction: column; gap: 2px; min-width: 140px; }
.schedslot__name { font-weight: 500; }
.schedslot__del { align-self: flex-start; margin-top: 4px; }

/* ---------- 结算商户区（#144 微信支付商户管理） ---------- */
/* 证书一格两行（API 证书 / 平台证书）。用小字与次级色：
   它是「可维护的凭据」而不是「表格主数据」，不该和商户号抢注意力。 */
.mch-certs { display: flex; flex-direction: column; gap: 3px; }
.mch-cert { font-size: 12px; line-height: 1.6; white-space: nowrap; }
/* 密钥明文用等宽字体：便于一眼看出位数差异（少一位/多一位都很常见），
   而比例字体的数字宽度不均，看不出位数。 */
.mch-certs + td code, .table code { font-family: var(--font-mono, monospace); }

/* 商户表的证书列：两行（API / 平台）在 1560px 下挤成三行，
   而「替换/下载/删除」三个按钮挤在一行里读不清。
   给这一列更明确的宽度，并让按钮各自不换行。 */
.mch-certs { min-width: 210px; }
.mch-cert { white-space: normal; }
.mch-cert .btn { white-space: nowrap; margin: 1px 0; }

/* ---------- 批量操作条（#141 审核页 / #149 数据管理台共用）---------- */
/* ★ 这个类名**是补上的，不是新起的**：
   `approval.js` 从 #141 起就在用 `.batchbar` / `.batchbar__count`
   （审核页的批量通过/拒绝），而 app.css 里**一直没有对应的样式** ——
   于是那条操作条是靠裸div 在跑，没有任何视觉分隔。
   #149 的数据管理台要用同一个控件，所以一次性把样式补齐，
   两边从此**真的是同一套**（同名同样式），而不是「长得像的两套」。
   另见细节 §1.8bis。 */
.batchbar {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    margin: 0;
    border-top: 1px solid var(--border);
    background: var(--surface-1);
}
.batchbar__count { white-space: nowrap; }
.batchbar .btn[disabled] { cursor: not-allowed; }


/* ==========================================================================
   学员端 v2（2026-10-16 · 参考 PPT/ys.mp4 的那套答题 App）
   --------------------------------------------------------------------------
   追加而不是改写：旧 `.p*` 那套全部保留（`closed.html` 等仍在用，
   改掉会牵连守卫的类名断言）。

   借来的版式（只借版式，不借与业务无关的壳）
   -----------------------------------------
   ① `.mtop` 实心蓝顶栏（那一套就是纯色蓝 + 白字 + 左返回 + 右操作）
   ② `.mhero` 浅渐变头图区（头像 + 名牌 + 进度条）
   ③ `.mgrid` 彩色圆形图标入口，每格一色
   ④ `.mogrid` 线性描边宫格（「我的」那种）
   ⑤ `.mqopt` 选项药丸行：字母圆徽 + 文字 + 选中打勾
   ⑥ `.mact` 底部等宽 4 键操作条
   ⑦ `.mbacktop` 悬浮回顶圆钮
   ⑧ `.mload` 深色圆角方块 + 转圈

   ★ 深色模式：本节全部用 `:root` 里已有的变量，没有硬编码深色值，
     所以 `prefers-color-scheme` 下这些类会自动跟着变。
   ========================================================================== */

/* ---------- ① 顶部导航条 ---------- */
.mtop {
    flex-shrink: 0;
    display: grid;
    /* ★ 右轨用 `max-content` 而不是 44px：「查本人」这种文字按钮
       与返回箭头**互斥** —— 44px 只够放一个图标。
       原来固定 44px 把「查本人」压成两行（第二行只剩「人」字），
       并且顶部还被裁掉一截。
       ★ 为什么不用 `auto`：它会被中间 1fr 影响，在部分浏览器里
         退回最小内容宽度，标题一长就把右侧按钮挤没。 */
    grid-template-columns: 44px minmax(0, 1fr) max-content;
    align-items: center;
    height: 52px;
    padding: 0 4px;
    /* 顶到刘海屏：safe-area 的量加进 padding，标题才不会被刘海压住 */
    padding-top: env(safe-area-inset-top, 0px);
    height: calc(52px + env(safe-area-inset-top, 0px));
    background: linear-gradient(180deg, var(--brand-500) 0%, var(--brand-600) 100%);
    color: #fff;
    position: sticky;
    top: 0;
    z-index: 30;
}
.mtop__title {
    margin: 0;
    font-size: 17px;
    font-weight: 600;
    text-align: center;
    /* 单行省略：考试名有 20 多个字（「XX 职业卫生培训（第 2 轮）」），
       不截断会把左右两个按钮挤出去 */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    letter-spacing: .2px;
}
.mtop__btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: #fff;
    cursor: pointer;
    padding: 0;
}
.mtop__btn:active { background: rgba(255, 255, 255, .18); }
.mtop__btn--text {
    width: auto;
    min-width: 44px;
    padding: 0 10px;
    border-radius: 14px;
    font-size: 13.5px;
    /* 双保险：真放不下时宁可横向溢出，也不要折成两行的怪形状。
       折行会让按钮高度超过 `.mtop` 的 52px → 顶部被裁。 */
    white-space: nowrap;
}
.mtop__slot { display: block; }

/* ---------- ② 首页头图区 ---------- */
.mhero {
    padding: 16px 16px 18px;
    background:
        radial-gradient(120% 90% at 88% -10%, #eaf1ff 0%, rgba(234, 241, 255, 0) 60%),
        linear-gradient(180deg, var(--brand-50) 0%, var(--ink-50) 100%);
    border-bottom: 1px solid var(--ink-200);
}
.mhero__row { display: flex; align-items: center; gap: 12px; }
.mhero__avatar {
    width: 52px;
    height: 52px;
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: #fff;
    color: var(--brand-600);
    box-shadow: 0 2px 8px rgba(29, 78, 216, .14);
}
.mhero__id { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.mhero__name {
    font-size: 17px;
    font-weight: 650;
    color: var(--ink-900);
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.mhero__tag {
    align-self: flex-start;
    padding: 1px 8px;
    border-radius: 999px;
    background: var(--brand-600);
    color: #fff;
    font-size: 11px;
    line-height: 17px;
}
.mhero__region {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-top: 10px;
    font-size: 12.5px;
    color: var(--ink-600);
}
.mhero__prog { margin-top: 14px; }
.mhero__progTop {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 6px;
}
.mhero__progLabel { font-size: 12.5px; color: var(--ink-600); }
.mhero__progVal { font-size: 13px; font-weight: 600; color: var(--brand-700); }
.mbar {
    height: 6px;
    border-radius: 999px;
    background: var(--ink-200);
    overflow: hidden;
}
.mbar__fill {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, var(--brand-500), var(--brand-700));
    transition: width .3s ease;
}

/* ---------- ③ 彩色圆形图标入口 ---------- */
.mgrid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 4px 0;
    padding: 16px 6px 14px;
    background: #fff;
    border-bottom: 8px solid var(--ink-100);
}
.mgrid__cell {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 7px;
    padding: 4px 2px;
    border: 0;
    background: transparent;
    cursor: pointer;
    /* 整格可点：手机上 44px 是最小舒适触控高度 */
    min-height: 76px;
}
.mgrid__cell:active { opacity: .68; }
.mgrid__ico {
    width: 46px;
    height: 46px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    color: #fff;
}
/* 五个色相轮转。★ 同一档明度（都是中调），不然会有一格看着"更重" */
.mgrid__cell--blue   .mgrid__ico { background: #2f6bf0; }
.mgrid__cell--green  .mgrid__ico { background: #12b76a; }
.mgrid__cell--orange .mgrid__ico { background: #f79009; }
.mgrid__cell--cyan   .mgrid__ico { background: #06aed4; }
.mgrid__cell--violet .mgrid__ico { background: #7a5af8; }
.mgrid__cell--rose   .mgrid__ico { background: #f04438; }
.mgrid__cell--amber  .mgrid__ico { background: #eaaa08; }
.mgrid__cell--teal   .mgrid__ico { background: #0d9488; }
.mgrid__label {
    font-size: 12.5px;
    color: var(--ink-800);
    line-height: 1.25;
    text-align: center;
}
.mgrid__badge {
    position: absolute;
    top: -1px;
    left: 50%;
    margin-left: 6px;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    border-radius: 999px;
    background: var(--p-err);
    color: #fff;
    font-size: 10px;
    line-height: 16px;
    text-align: center;
}

/* ---------- ④ 线性描边宫格 ---------- */
.mogrid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 16px 0;
    padding: 16px 6px;
    background: #fff;
    border-bottom: 8px solid var(--ink-100);
}
.mogrid__cell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 7px;
    padding: 2px;
    border: 0;
    background: transparent;
    cursor: pointer;
}
.mogrid__cell:active { opacity: .68; }
.mogrid__ico { color: var(--brand-600); }
.mogrid__ico--green  { color: #12b76a; }
.mogrid__ico--orange { color: #f79009; }
.mogrid__ico--violet { color: #7a5af8; }
.mogrid__ico--rose   { color: #f04438; }
.mogrid__ico--teal   { color: #0d9488; }
.mogrid__label { font-size: 12.5px; color: var(--ink-800); }

/* ---------- ⑤ 区块标题（竖条） ---------- */
.mbar-t {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 14px 16px 8px;
    background: var(--ink-50);
}
.mbar-t__t {
    position: relative;
    padding-left: 10px;
    font-size: 15px;
    font-weight: 650;
    color: var(--ink-900);
}
/* 竖条用 ::before 而不是 border —— border 会把文字的基线抬高 1px */
.mbar-t__t::before {
    content: '';
    position: absolute;
    left: 0;
    top: 50%;
    width: 3px;
    height: 14px;
    margin-top: -7px;
    border-radius: 2px;
    background: var(--brand-600);
}
.mbar-t__more {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    border: 0;
    background: transparent;
    color: var(--ink-500);
    font-size: 12.5px;
    cursor: pointer;
    padding: 4px 0 4px 8px;
}

/* ---------- ⑥ 公告条 ---------- */
.mnotice {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 11px 14px;
    border: 0;
    background: var(--brand-50);
    color: var(--brand-700);
    font-size: 13px;
    text-align: left;
    cursor: pointer;
}
.mnotice__ico { display: inline-flex; flex: none; }
.mnotice__t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mnotice__sep { width: 1px; height: 12px; background: var(--brand-200, #bfd4ff); flex: none; }
.mnotice__x { flex: none; opacity: .8; }

/* ---------- ⑦ 题头 ---------- */
.mqhead {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 14px;
    margin: 12px;
    background: #fff;
    border-bottom: 1px solid var(--ink-200);
    border-radius: 12px;
}
.mqhead__tag {
    padding: 2px 9px;
    border-radius: 6px;
    font-size: 12.5px;
    line-height: 19px;
    color: #fff;
    background: var(--brand-600);
}
.mqhead__tag--green  { background: #12b76a; }
.mqhead__tag--orange { background: #f79009; }
.mqhead__tag--violet { background: #7a5af8; }
.mqhead__fav {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border: 0;
    background: transparent;
    color: var(--ink-400);
    cursor: pointer;
    padding: 0;
}
.mqhead__fav.is-on { color: #f04438; }
.mqhead__fav.is-on .mic { fill: #f04438; }
.mqhead__score {
    margin-left: auto;
    font-size: 12.5px;
    color: var(--ink-500);
}
.mqhead__count { font-size: 13.5px; color: var(--ink-900); font-weight: 600; }
.mqhead__count i { font-style: normal; color: var(--ink-400); margin: 0 1px; }

/* ---------- ⑧ 选项药丸行 ---------- */
.mqopt {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 12px 12px;
    margin-bottom: 10px;
    border: 1px solid transparent;
    border-radius: 12px;
    background: var(--ink-50);
    color: var(--ink-900);
    font-size: 14.5px;
    line-height: 1.5;
    text-align: left;
    cursor: pointer;
}
.mqopt:active { background: var(--ink-100); }
.mqopt__k {
    flex: none;
    width: 26px;
    height: 26px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--ink-200);
    color: var(--ink-600);
    font-size: 13px;
    font-weight: 600;
}
.mqopt__t { flex: 1; min-width: 0; }
.mqopt__mark { flex: none; display: inline-flex; color: var(--brand-600); }
/* 选中态：整行泛蓝 + 字母徽章变蓝 + 右侧打勾。
   ★ 不只靠底色区分 —— 色觉障碍者也要能看出选了哪一项，
     所以「位置（字母徽）+ 勾」这两个非颜色通道必须都在。 */
.mqopt.is-picked {
    background: var(--brand-50);
    border-color: var(--brand-100);
}
.mqopt.is-picked .mqopt__k { background: var(--brand-600); color: #fff; }
.mqopt.is-right {
    background: var(--p-ok-bg);
    border-color: var(--p-ok);
}
.mqopt.is-right .mqopt__k { background: var(--p-ok); color: #fff; }
.mqopt.is-right .mqopt__mark { color: var(--p-ok); }
.mqopt.is-wrong {
    background: var(--p-err-bg);
    border-color: var(--p-err);
}
.mqopt.is-wrong .mqopt__k { background: var(--p-err); color: #fff; }
.mqopt.is-wrong .mqopt__mark { color: var(--p-err); }

/* ---------- ⑨ 题干 ---------- */
.mqstem {
    padding: 16px 14px 6px;
    font-size: 15.5px;
    line-height: 1.72;
    color: var(--ink-900);
    background: #fff;
}
.mqstem__no { color: var(--brand-600); font-weight: 600; margin-right: 3px; }

/* ---------- ⑩ 底部操作条 ---------- */
.mact {
    /* ★ 必须是 `fixed` 而不是 `absolute`：
       `.pbody` 自己就是滚动容器（overflow-y:auto），`absolute; bottom:0`
       的底边是**内容底边**而不是视口底边 —— 一滚动操作条就跟着内容跑了。
       同 `.ptabbar`（那里是实测过的写法）。 */
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 60;
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    gap: 0;
    padding: 6px 4px calc(6px + env(safe-area-inset-bottom, 0px));
    background: rgba(255, 255, 255, .97);
    border-top: 1px solid var(--ink-200);
    backdrop-filter: blur(8px);
}
.mact__b {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 5px 2px;
    min-height: 50px;
    border: 0;
    background: transparent;
    color: var(--ink-600);
    font-size: 11.5px;
    cursor: pointer;
}
.mact__b:active { opacity: .62; }
.mact__b.is-primary { color: var(--brand-600); font-weight: 600; }
.mact__b.is-off { color: var(--ink-300); cursor: not-allowed; }
.mact__b.is-off .mic { opacity: .55; }
/* 「交卷」那格做成实心蓝药丸：它是不走寻常路的动作
   （别的键是导航，这一键会结束考试），视觉上要能一眼分开。 */
.mact__b.is-primary .mact__ico {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: var(--brand-600);
    color: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.mact-space { height: calc(68px + env(safe-area-inset-bottom, 0px)); }

/* ---------- ⑪ 悬浮回顶 ---------- */
.mbacktop {
    /* 同上：`fixed`，否则答题页滚到第 5 题它就跟着题干跑走了。 */
    position: fixed;
    right: 14px;
    /* 让开底部操作条（68px + 安全区） */
    bottom: calc(80px + env(safe-area-inset-bottom, 0px));
    z-index: 58;
    /* ★ 默认**不可点且不可见**，滚上去才加 `.is-on`。
       常驻时它正好压在矮窗口上的最后一个选项上 ——
       学员想点那个选项，点到的是「回到顶部」，而页面 console 全绿。
       `visibility:hidden` 而不是 `display:none`：后者会让它从 tab 序里消失，
       但这里本来就要靠 `pointer-events` 屏蔽，两者一起写才不留缝。 */
    visibility: hidden;
    opacity: 0;
    pointer-events: none;
    transition: opacity .18s var(--ease-out), transform .18s var(--ease-out);
    transform: translateY(6px);
    width: 42px;
    height: 42px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 50%;
    background: #fff;
    color: var(--ink-600);
    box-shadow: 0 3px 12px rgba(15, 23, 42, .16);
    cursor: pointer;
}
.mbacktop:active { background: var(--ink-100); }
/* 滚上去之后的启用态。
   ★ 默认态写成 `visibility:hidden + opacity:0 + pointer-events:none`
     而这里必须**同时**恢复三者：只恢复 opacity 的话按钮仍然点不动
     （pointer-events 还是 none），只恢复 visibility 的话会有一帧硬跳。 */
.mbacktop.is-on {
    visibility: visible;
    opacity: 1;
    pointer-events: auto;
    transform: translateY(0);
}

/* ---------- ⑫ 深色方块转圈（载入态） ---------- */
.mload {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 12px;
    margin: 40px auto;
    width: 148px;
    height: 148px;
    border-radius: 18px;
    background: rgba(15, 23, 42, .88);
    color: #fff;
}
.mload__spin {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    border: 3px solid rgba(255, 255, 255, .28);
    border-top-color: #fff;
    animation: mspin .8s linear infinite;
}
@keyframes mspin { to { transform: rotate(360deg); } }
.mload__t { font-size: 14px; letter-spacing: .5px; }
@media (prefers-reduced-motion: reduce) {
    .mload__spin { animation-duration: 2s; }
}

/* ---------- ⑬ 空态 ---------- */
.mempty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    padding: 46px 24px;
    color: var(--ink-400);
}
.mempty__ico {
    width: 58px;
    height: 58px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--ink-100);
    color: var(--ink-400);
}
.mempty__t { font-size: 13.5px; color: var(--ink-500); text-align: center; }

/* ---------- ⑭ 底部标签栏（5 键） ---------- */
.mtabbar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 60;
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    padding: 5px 0 calc(4px + env(safe-area-inset-bottom, 0px));
    background: rgba(255, 255, 255, .98);
    border-top: 1px solid var(--ink-200);
    backdrop-filter: blur(10px);
}
.mtab {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    min-height: 50px;
    padding: 4px 2px 2px;
    color: var(--ink-400);
    text-decoration: none;
}
.mtab__ico { display: inline-flex; }
.mtab__label { font-size: 11px; line-height: 1.3; }
.mtab.is-on { color: var(--brand-600); }
.mtab.is-on .mtab__label { font-weight: 600; }
.mtab-space { height: calc(64px + env(safe-area-inset-bottom, 0px)); }

/* ---------- ⑮ 白卡列表（那一套的列表项样式） ---------- */
.mlist { background: #fff; border-bottom: 8px solid var(--ink-100); }
.mlist__i {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 14px 16px;
    border: 0;
    border-bottom: 1px solid var(--ink-100);
    background: #fff;
    text-align: left;
    cursor: pointer;
    color: inherit;
    margin: 1px 0;
    text-decoration: none;
}
.mlist__i:last-child { border-bottom: 0; }
.mlist__i:active { background: var(--ink-50); }
.mlist__ico {
    flex: none;
    width: 34px;
    height: 34px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 10px;
    background: var(--brand-50);
    color: var(--brand-600);
}
.mlist__ico--green  { background: var(--p-ok-bg);  color: var(--p-ok); }
.mlist__ico--orange { background: var(--p-warn-bg); color: var(--p-warn); }
.mlist__ico--rose   { background: var(--p-err-bg); color: var(--p-err); }
.mlist__ico--violet { background: #f4f1ff;            color: #7a5af8; }
.mlist__body { flex: 1; min-width: 0; }
/* ★ 这两条必须有 `display: block`：标题与副行是 `<span>`（为了让整行
   能放进 `<a>`/`<button>` 里而不额外套一层 div），默认 `display:inline`
   会让它们**并排**在同一行 —— 症状是「初训费后台已配置，2026-01-01 起生效」
   挤成一行，主副不分。
   `.mlist__s` 上的 `margin-top: 3px` 也只有在块级布局下才生效，
   这条 margin 本身就说明了原意就是要堆叠。
   （2026-10-16 学员端 v2 改版时发现；`home.html` / `exams.html`
     同样中招，它们也是 `<span class="mlist__t">` + `<span class="mlist__s">`。） */
.mlist__t {
    display: block;
    font-size: 14.5px;
    color: var(--ink-900);
    line-height: 1.35;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.mlist__s {
    display: block;
    margin-top: 3px;
    font-size: 12px;
    color: var(--ink-500);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.mlist__x { flex: none; display: inline-flex; color: var(--ink-400); }

/* 通知卡片折叠/展开（10-19 第 28 轮）。
 * ★ 箭头旋转是**唯一**的分组信号：两行文字（标题+时间）在收起与展开时
 *   完全一样，没有箭头转动就看不出"这一条打开了没有"。
 * ★ `mcard.is-open` 由 JS 维护，与 `mcard__b.hidden` 同步。 */
.mcard.is-open .mlist__x { transform: rotate(90deg); }
.mlist__x { transition: transform .16s var(--ease-out); }

/* 展开后的正文：保留换行（通知正文是多行写的，折叠成一坨很难读） */
.mcard__text {
    color: var(--ink-700);
    font-size: 13.5px;
    line-height: 1.7;
    white-space: pre-wrap;
}

/* 未读通知的强提示条 + 数字角标（10-19 第 28 轮）。
 * ★ 用 danger 系而不是原来的浅蓝：它是一条"有事等你处理"的告警，
 *   视觉权重必须高于下面的报名/考试列表卡片。
 * ★ 角标用 `min-width` + `padding`，两位数不会变成椭圆。 */
.mnotice--alert {
    background: #fff5f5;
    border: 1px solid #fecaca;
    color: #991b1b;
    cursor: pointer;
}
.mnotice--alert .mnotice__ico { color: #dc2626; }
.mnotice__badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 19px;
    height: 19px;
    padding: 0 6px;
    margin: 0 1px;
    border-radius: 999px;
    background: #dc2626;
    color: #fff;
    font-size: 12px;
    font-weight: 600;
    line-height: 1;
    vertical-align: 1px;
}
.mnotice--alert .mnotice__x { display: inline-flex; align-items: center; gap: 2px; }

/* ---------- ⑯ 卡片 ---------- */
.mcard {
    background: #fff;
    border-bottom: 8px solid var(--ink-100);
}
.mcard__b { padding: 14px 16px; }
/* `.mcard__t` 的 padding 在下面那条里 —— 它**默认是给 `.mcard__b` 里面用的**
   （那里已经有一圈 14px 16px 了）。但证书卡 / 机构卡 / 空课程卡都是把它
   直接挂在 `.mcard` 当**卡头**（标题 + 右侧状态徽章），这时外层没有
   `.mcard__b`，标题就会贴到卡片左右边缘上。
   所以补一条**直接子元素**的规则：只影响当卡头用的情况，
   嵌在 `.mcard__b` 里的那一份（`padding: 0`）一个字都没动。 */
.mcard > .mcard__t { padding: 14px 16px 0; }
.mcard__t {
    font-size: 14px;
    font-weight: 600;
    color: var(--ink-800);
    margin-bottom: 10px;
    display: flex;
    align-items: center;
    gap: 6px;
    /* ★ flex item 的 `min-width` 默认是 `auto`（= 内容宽度），
       它**不肯被压过内容宽度** —— 所以右端 `margin-left:auto`
       的徽标被挤出去时，`justify-content` / `margin:auto` 都救不了。
       写 0 才是让「文字可压缩、可省略」的那把钥匙。
       症状：错题本标题行「最近错于 2026-10-17」比容器宽 5px，
       最右几个字被切掉，而页面级 `scrollWidth` 量不到（没有滚动条）。 */
    min-width: 0;
    overflow: hidden;
}
.mcard__t > * { flex: none; }
/* 中间的题型名让出弹性空间，徽标贴在右边 */
.mcard__t > .pitem__badge { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mcard__t .mic { color: var(--brand-600); }

/* ---------- ⑰ 底部大按钮 ---------- */
.mbtn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    width: 100%;
    min-height: 48px;
    padding: 12px 16px;
    border: 0;
    border-radius: 999px;
    background: var(--brand-600);
    color: #fff;
    font-size: 15.5px;
    font-weight: 600;
    cursor: pointer;
}
.mbtn:active { background: var(--brand-700); }
.mbtn[disabled] { background: var(--ink-300); cursor: not-allowed; }
.mbtn--ghost {
    background: #fff;
    color: var(--brand-600);
    box-shadow: inset 0 0 0 1px var(--brand-100);
}
.mbtn--ghost:active { background: var(--brand-50); }
.mbtn-row {
    padding: 14px 16px calc(18px + env(safe-area-inset-bottom, 0px));
    background: #fff;
    display: flex;
    gap: 10px;
}
.mbtn-row .mbtn { flex: 1; }

/* ============================ 月历占用图（calendar.js） ============================
   跨端共用：机构端「已开班信息」/ 管理端 / 行政端看到的是同一张图。
   配色全部走主题变量，不写死色值 —— 深色主题下也能用。 */
.cal__head {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.cal__title { font-size: 15px; font-weight: 600; color: var(--ink-900); min-width: 118px; text-align: center; }
.cal__legend { margin-left: auto; font-size: 12px; color: var(--ink-500); display: inline-flex; align-items: center; gap: 6px; }
.cal__legend-box {
    display: inline-block; width: 18px; height: 10px; border-radius: 3px;
    background: var(--p-blue-100); border: 1px solid var(--p-blue);
}
.cal__grid { border: 1px solid var(--ink-200); border-radius: var(--r-md); overflow: hidden; }
.cal__weekhead,
.cal__days { display: grid; grid-template-columns: repeat(7, 1fr); }
.cal__weekhead {
    background: var(--surface-2);
    border-bottom: 1px solid var(--ink-200);
    font-size: 12px; color: var(--ink-500);
}
.cal__weekhead > div { padding: 6px 0; text-align: center; }
.cal__week + .cal__week { border-top: 1px solid var(--ink-200); }
.cal__day {
    min-height: 34px;
    padding: 4px 6px;
    border-left: 1px solid var(--ink-100);
    font-size: 12.5px;
    color: var(--ink-700);text-align: center;
}
.cal__day:first-child { border-left: none; }
.cal__day--out { background: var(--surface-2); color: var(--ink-400); }
.cal__day--today { background: #b5d9b5; }
.cal__day--today .cal__num { color: #007aff; font-weight: 700;text-align: center; }
.cal__bars { padding: 0 4px 6px; display: flex; flex-direction: column; gap: 3px; }
.cal__bar {
    display: flex; align-items: center; gap: 4px;
    /* 横条高度固定，便于「每条一行」地叠起来看 */
    height: 20px; padding: 0 6px;
    border-radius: 3px;
    background: var(--p-blue-100);
    border-left: 3px solid var(--p-blue);
    color: var(--ink-800);
    font-size: 11.5px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cal__bar-text { overflow: hidden; text-overflow: ellipsis; }
.cal__clip { color: var(--p-blue); font-weight: 700; }
.cal__more { font-size: 11.5px; color: var(--ink-500); padding-left: 4px; }
.cal__list { margin-top: 10px; display: flex; flex-direction: column; gap: 4px; }
.cal__item { font-size: 12.5px; color: var(--ink-700); display: flex; align-items: baseline; gap: 4px; }
.cal__item-dot {
    width: 7px; height: 7px; border-radius: 50%;
    background: var(--p-blue); flex: 0 0 auto; align-self: center;
}

/* ==========================================================================
   学员端 · 电脑版自适应（`.stage--desk`，10-12 轮）
   --------------------------------------------------------------------------
   谁会用到这里
   ------------
   `web/student/m/*` 这一套**手机原生页**，在宽屏（电脑）上打开时就是这一段。
   这不是可选的观感优化 —— `tools/check_portal_mode.js` 明文要求
   「只开手机端时，学员在 1440×900 的电脑上打开必须落到 `m/home.html`」，
   所以这批页面**一定会**被电脑看到。之前宽屏看到的是「深灰演示台里一根
   ~480px 的窄条」，在电脑上既不占满也读不出层次。

   做法：**只加类、只加选择器，一个已有类名都不改**
   ------------------------------------------------
   ① `.stage--desk` 由 `mobile.js` 的 `mount()` 在「非窄屏」时挂上，
      与 `.stage--real`（真机，铺满整屏）互斥，永远不会同时出现；
   ② 下面每一条都是「`.stage--desk` + 已有类名」，没有新造组件名，
      也没有改写 `.pitem` / `.pcard` 这些基础类的定义 ——
      手机形态（窄屏）看到的还是逐字原来的那一套；
   ③ 容器宽度走 `--stu-max` 一个变量：正文与顶栏用**同一个算式**
      (`max(24px, (100% - var(--stu-max)) / 2)`) 求出左右内边距，
      所以两者在任意窗口宽度下都对齐，不会出现「标题比卡片多缩进 12px」。

   为什么集中在 CSS 而不是各页面改结构：这 7 个页面的 DOM 是 JS 拼出来的，
   改结构要动 7 个文件、还得让手机形态一字不变；改样式只动一处，
   且窄屏那一档本来就管不到。
   ========================================================================== */
.stage--desk {
    --stu-max: 1180px;
    display: block;
    min-height: 100vh;
    padding: 0;
    background: var(--bg-app);
}

/* 外框彻底退场：电脑上不再有「手机」这个概念，内容直接铺满 */
.stage--desk .phone {
    display: block;
    width: 100%;
    height: auto;
    overflow: visible;
    border: 0;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
}

/* ---------- 顶栏：全宽白底，内容对齐正文那一列 ---------- */
.stage--desk .pnavbar,
.stage--desk .pnav {
    position: sticky;
    top: 0;
    z-index: 40;
    height: var(--topbar-h);
    padding-left: max(24px, calc((100% - var(--stu-max)) / 2));
    padding-right: max(24px, calc((100% - var(--stu-max)) / 2));
    border-bottom: 1px solid var(--line);
    background: var(--surface);
    color: var(--text);
    box-shadow: var(--shadow-sm);
}
/* 没有返回箭头的页面会渲染一个 30px 的占位 —— 手机上它让标题居中，
   电脑上白占一块左边距、标题看着像被推歪了，这里去掉。 */
.stage--desk .pnavbar__back[aria-hidden] { display: none; }
.stage--desk .pnavbar__back,
.stage--desk .pnav__back {
    width: 32px;
    color: var(--text-2);
    font-size: 22px;
}
.stage--desk .pnavbar__back:hover,
.stage--desk .pnav__back:hover { background: var(--ink-100); }
.stage--desk .pnavbar__title,
.stage--desk .pnav__title {
    flex: 1;
    font-size: 17px;
    font-weight: 600;
    letter-spacing: 0;
    text-align: left;
}
.stage--desk .pnavbar__action {
    padding: 5px 12px;
    border-color: var(--line);
    border-radius: var(--r-sm);
    color: var(--text-2);
    font-size: 13px;
}

/* ---------- 首页那张蓝标题条：改成页面级卡片头 ----------
   它在 `.pbody` 里（不是 `.pnavbar` 那种壳里的兄弟节点），拿不到全宽，
   所以做成一张圆角白卡 —— 「🔔 标题 …… 👤」正好就是一张页头。 */
.stage--desk .ptitlebar {
    height: 58px;
    margin-bottom: 16px;
    padding: 0 18px;
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: var(--surface);
    color: var(--text);
    box-shadow: var(--shadow-sm);
}
.stage--desk .ptitlebar__title { flex: 1; font-size: 17px; text-align: left; }
.stage--desk .ptitlebar__icon { width: 34px; height: 34px; color: var(--text-2); }
.stage--desk .ptitlebar__icon:hover { background: var(--ink-100); }
.stage--desk .ptitlebar__icon--dot::after { border-color: var(--surface); }

/* ---------- 正文列 ---------- */
.stage--desk .pbody {
    flex: none;
    min-height: 0;
    overflow: visible;
    box-sizing: border-box;
    width: 100%;
    padding: 22px max(24px, calc((100% - var(--stu-max)) / 2)) 56px;
    background: transparent;
}

/* ---------- 四宫格：窄屏 2×2（原样），宽屏 4 列 ----------
   刻意**不用** `auto-fit`：它会在 900px 附近给出「3 列 + 第 4 个掉到下一行」
   这种半空一行，而这里正好是 **4 个**入口。两档（2 / 4）永远铺满一行，
   也永远不会有半空行。断点取 1000px：再窄下去每个格子不足 220px，
   「按属地找机构并报名」那行小字会压成两行。 */
.stage--desk .pgrid {
    grid-template-columns: repeat(2, 1fr);
    gap: 14px;
    padding: 0;
    margin-bottom: 6px;
    border-bottom: 0;
    background: transparent;
}
.stage--desk .pgrid__cell {
    padding: 18px 16px;
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: var(--surface);
    box-shadow: var(--shadow-sm);
}
.stage--desk .pgrid__cell:hover {
    border-color: var(--brand-500);
    box-shadow: var(--shadow);
}
.stage--desk .pgrid__icon { width: 44px; height: 44px; font-size: 23px; }
.stage--desk .pgrid__label { font-size: 15px; }
.stage--desk .pgrid__desc { font-size: 12.5px; }
@media (min-width: 1000px) {
    .stage--desk .pgrid { grid-template-columns: repeat(4, 1fr); }
}

/* ---------- 各卡片：把手机上的 12px 左右外边距收掉 ----------
   窄屏那套外边距是为了「卡片之间露出灰底」；电脑上正文列自己有内边距，
   再各留 12px 会让每张卡左右对不齐标题。 */
.stage--desk .psec { padding: 4px 0 10px; }
.stage--desk .psec__title { font-size: 15.5px; }
.stage--desk .pcard,
.stage--desk .pitem,
.stage--desk .porg,
.stage--desk .phist,
.stage--desk .pcert,
.stage--desk .presult,
.stage--desk .pstats,
.stage--desk .pnote,
.stage--desk .pretake,
.stage--desk .palert { margin-left: 0; margin-right: 0; }
.stage--desk .pcard { margin-top: 0; margin-bottom: 16px; border-radius: var(--r-lg); box-shadow: var(--shadow-sm); }
.stage--desk .pitem,
.stage--desk .porg,
.stage--desk .phist,
.stage--desk .pcert { margin-bottom: 12px; border-radius: var(--r-lg); box-shadow: var(--shadow-sm); }
.stage--desk .pitem { padding: 16px 18px; }
/* 标题与副行在手机上是**同行连排**（窄屏放得下，换行反而更费竖向空间）；
   电脑端一行的宽度够，副行另起一行才读得清「这场考试叫什么」和
   「几题几分钟」是两件事。 */
/* display:block 已提到全局（见上面的 .pitem__title），这里只管字号微调 */
.stage--desk .pitem__title { font-size: 15px; }
.stage--desk .pitem__sub { margin-top: 3px; font-size: 12.5px; }
/* 卡片的外边距在电脑上被收成 0（见上），按钮条跟着收，并右对齐到卡片右缘 */
.stage--desk .pitem-actions {
    display: flex;
    justify-content: flex-end;
    padding: 0 0 12px;
}
.stage--desk .pstats { max-width: 560px; margin-top: 0; margin-bottom: 16px; }
.stage--desk .presult { max-width: 720px; margin: 0 auto 16px; }
.stage--desk .pnote { margin-top: 12px; margin-bottom: 12px; font-size: 13px; }
.stage--desk .pretake { margin-top: 12px; margin-bottom: 12px; }
.stage--desk .palert { margin-top: 12px; margin-bottom: 12px; font-size: 13px; }
.stage--desk .ptable { font-size: 13.5px; }
.stage--desk .ptable th { padding: 11px 14px; font-size: 12.5px; }
.stage--desk .ptable td { padding: 13px 14px; }
.stage--desk .pfoot-space { height: 40px; }

/* ---------- 表单行：手机上是「值靠右」，电脑上改回左对齐 ----------
   一行的宽度在电脑上有 ~1100px，值贴在最右边会让「标签」和「值」之间
   隔着一整个屏幕，读起来得来回找。左对齐 + 标签列加宽才是主流的做法。 */
.stage--desk .pfield { min-height: 54px; padding: 0 18px; }
.stage--desk .pfield__label { width: 150px; }
.stage--desk .pfield__value,
.stage--desk .pfield input,
.stage--desk .pfield select { text-align: left; }

/* ---------- 底部按钮：不再整行铺满 ---------- */
/* 按钮靠右：与电脑端那批页面（`student/profile.html` 的「保存」）一致 ——
   同一件事在两种形态下摆在同一个位置，学员换设备时不用重新找。 */
.stage--desk .pfoot {
    justify-content: flex-end;
    padding: 12px 0;
    border-top: 0;
    background: transparent;
}
/* `.pbtn` 本身是 `flex: 1`（手机上要撑满一行），电脑端要的是"一个正常大小的
   按钮" —— 但**不能全局改 `.pbtn`**：弹层里的 `.pdlg__foot .pbtn` 就靠那个
   `flex: 1` 让"取消/确定"各占一半，改了它两个按钮会挤在中间。
   所以只在这两处（页面底部条、卡片下的操作条）改。 */
.stage--desk .pfoot .pbtn,
.stage--desk .pitem-actions .pbtn { flex: 0 0 auto; min-width: 148px; padding: 0 24px; }
.stage--desk .profile-actions { display: flex; justify-content: flex-end; padding: 14px 0 26px; }

/* ---------- 浮层：电脑上从底部弹「整宽的一条」很怪，改成居中对话框 ---------- */
.stage--desk .psheet-mask,
.stage--desk .pdlg-mask { align-items: center; justify-content: center; padding: 24px; }
.stage--desk .psheet,
.stage--desk .pdlg {
    width: 100%;
    max-width: 480px;
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-lg);
    transform: none;
    animation: mask-in var(--t-base) var(--ease-out) forwards;
}
.stage--desk .pdlg__foot,
.stage--desk .psheet__foot { padding-bottom: 14px; }
.stage--desk .ppicker__mask { align-items: center; justify-content: center; padding: 24px; }
.stage--desk .ppicker {
    position: relative;
    left: auto;
    right: auto;
    bottom: auto;
    max-width: 480px;
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-lg);
    transform: none;
    animation: mask-in var(--t-base) var(--ease-out) forwards;
}
/* toast 在手机壳里是 `absolute`（壳是定位容器）；电脑上没有壳了，
   改成贴着视口底部固定，否则它会挂在页面最底下、滚上来才看得到。 */
.stage--desk .ptoast { position: fixed; bottom: 32px; z-index: 200; }

/* ---------- 右下角「💻 电脑端」（`mobile.js` 的 backToWeb） ----------
   形态选择只活在当前标签页里，而这一套页面上原先没有任何回电脑端的入口 ——
   在电脑上误点一次「📱 手机端」，这个标签页就再也回不去了。
   `fixed` 而不是放进顶栏：首页的顶栏右边已经有 👤，再插一个会打架；
   浮在右下角则与所有页面的布局都不冲突。 */
.stage--desk .stage__back {
    position: fixed;
    right: 22px;
    bottom: 22px;
    z-index: 60;
    height: 38px;
    padding: 0 16px;
    border: 1px solid var(--line);
    border-radius: var(--r-full);
    background: var(--surface);
    color: var(--text-2);
    font-family: inherit;
    font-size: 13px;
    cursor: pointer;
    box-shadow: var(--shadow);
}
.stage--desk .stage__back:hover { border-color: var(--brand-500); color: var(--brand-600); }

/* ==========================================================================
   学员端 · 纯手机形态（2026-10-16，取消电脑端）
   --------------------------------------------------------------------------
   背景
   ----
   学员端原先有两套形态：`web/student/*.html`（电脑端，侧栏 + 顶栏）与
   `web/student/m/*.html`（手机原生页）。现按需求收敛为**一套** ——
   全部页面在 `m/` 下，电脑端页面已删除。

   本段只做三件事，**一个已有类名都不改**（`tools/check_chart.js` 与
   `check_labels.js` 都拿类名与本文件做对照，改名会直接把守卫打红）：

     ① `.stage--real` 在**宽屏**下的表现：不再有演示台，把这一列居中，
        两侧补底色；窄屏下本来就是铺满，规则不生效。
     ② 底部标签栏 `.ptabbar` / `.ptab` / `.ptab-space`（新增类）。
     ③ 答题内核与视频播放器在窄屏下需要的几个新组件类（新增类）。

   为什么宽屏不另做一套
   -------------------
   一套版式 + 一个 `max-width` 就是移动优先的完整答案：
   宽屏看到的是**同一份 DOM**，居中放大而不是另一张页面。
   分叉点（答题卡、心跳、唤起微信）本来就不在样式上，
   真正的复用发生在 `app/` 那一侧的接口与判分内核上。
   ========================================================================== */

/* ---------- ① 宽屏：居中一列，两侧补底色 ---------- */
@media (min-width: 720px) {
    .stage--real {
        background: var(--bg-app);
    }
    .stage--real .phone--real {
        /* 1180px 与原电脑版一致，但作用在同一套 DOM 上：
           宽屏不再有另一份页面，只是这一列变宽。 */
        --stu-max: 1180px;
        max-width: var(--stu-max);
        margin: 0 auto;
        border-left: 1px solid var(--line);
        border-right: 1px solid var(--line);
        box-shadow: var(--shadow);
    }
    /* 内部滚动交给 `.pbody`，机身保持整屏高 */
    .stage--real .phone--real .pnavbar {
        position: sticky;
        top: 0;
        z-index: 40;
    }
}

/* ---------- 触摸友好：点按目标不小于 44px ---------- */
/* iOS 与 Android 的 HIG 都把 44×44 CSS px 作为最小可点尺寸。
   低于它，手指边缘点不中 —— 表现是"点了没反应"，而它不是 bug。 */
.pgrid__cell,
.pitem,
.p-list-item,
.ptab,
.pbtn,
.pinput {
    min-height: 44px;
}
/* 触摸设备上不给 hover 反馈（它只在鼠标悬停时触发，
   触摸时会导致"点完一下按钮颜色变一下"的粘滞感） */
@media (hover: none) {
    .pitem:hover,
    .p-list-item:hover,
    .pgrid__cell:hover,
    .pbtn:hover:not(:disabled) { background: var(--surface); }
}

/* ---------- ② 底部标签栏 ---------- */
/* ---------- 底部标签栏的高度常量（单一真相，声明在 :root） ---------- */
.ptabbar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 60;
    display: flex;
    /* iPhone 全面屏底部那条 Home 指示条：
       没有这一行，最后一个标签会被黑条压住一半。 */
    padding-bottom: var(--ptab-safe);
    border-top: 1px solid var(--line);
    background: rgba(255, 255, 255, .96);
    backdrop-filter: saturate(180%) blur(12px);
    -webkit-backdrop-filter: saturate(180%) blur(12px);
    /* ★ 这条规则原来分散在文件两个位置（这里 + 三十行之后的 :root 段旁），
       改高度时只看到其中一处 → 占位块与标签栏对不上（实测压了 3px）。
       现在统一在这里，:root 那边只留变量。 */
    min-height: var(--ptab-h);
    box-sizing: border-box;
}
@media (min-width: 720px) {
    /* 宽屏时标签栏与正文列同宽（与 `.phone--real` 的 max-width 一致） */
    .ptabbar {
        max-width: 1180px;
        margin: 0 auto;
        border-left: 1px solid var(--line);
        border-right: 1px solid var(--line);
    }
}
.ptab {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    min-height: 52px;
    text-decoration: none;
    color: var(--ink-500);
    font-size: 11px;
}
.ptab__icon { font-size: 20px; line-height: 1.1; }
.ptab__label { font-size: 11px; }
.ptab.is-active { color: var(--brand-500); }
.ptab.is-active .ptab__label { font-weight: 600; }
.ptab:active { background: var(--ink-50); }

/* ---------- 底部标签栏的高度常量（单一真相） ----------
   ★ 为什么提到 :root：`.ptab-space` 是 `.ptabbar` 的**兄弟**，读不到它身上的变量。
     定义在 `.ptabbar` 上时占位块会走 fallback，数字就又变成两份 —— 魔数没消掉。
     变量必须挂在两者的共同祖先上。
   ★ 为什么不能让占位块自己写死一个数（原来 66px / 82px）：
     标签栏真实高度 = min-height + 上下 padding + env(safe-area-inset-bottom)。
     iPhone 带 home indicator 时安全区是 34px，两处数字就对不上了 ——
     症状是"最后一张卡被标签栏盖住"，而**静态守卫抓不到**
     （两条 CSS 各自语法都对，只有真截图才看得出来）。 */
:root {
    /* ⚠️ `--ptab-h` 是**下限**，不是标签栏的真实高度。
       `.ptabbar` 是 flex 容器，高度由子项（`.ptab` 的 min-height:52px）
       加自身 padding 决定，`min-height` 压不住它。
       实测（tools/_probe.js，375×812）：标称 60 → 实际 71px。
       所以 `--ptab-gap` 是**必须留的余量**，不是可选装饰：
         · 多 1px = 多一点空白，看不出来
         · 少 1px = 最后一段内容被标签栏盖住，看得见
       余量也不能只按当前字体算 —— 中文标签在部分 Android 字体下行高更大，
       `.ptab` 会超过它自己的 min-height。 */
    --ptab-h: 61px;                                  /* 标签栏本体高度下限 */
    --ptab-safe: env(safe-area-inset-bottom, 0px);   /* iPhone home indicator */
    --ptab-gap: 12px;                                /* 补 border-top + 上边框阴影 + 字体余量 */
}
/* 占位块 = 标签栏的真实占位。
   ★ 三个细节少一个就对不上（实测 375×812 下压 3px）：
     ① `box-sizing: border-box` —— 标签栏有 border-top: 1px，
        content-box 下 min-height 不含 border，真实高度多 1px；
     ② 安全区 **只能加一次** —— 标签栏用 padding-bottom 吃它，
        占位块再 `height` + `padding-bottom` 各加一遍就会多出一个安全区；
     ③ `--ptab-gap` 给标签栏的上边框与阴影留余量。 */
.ptab-space {
    box-sizing: border-box;
    height: calc(var(--ptab-h) + var(--ptab-safe) + var(--ptab-gap));
}
@media (min-width: 720px) {
    :root { --ptab-h: 66px; }
}

/* ==========================================================================
   2026-10-16 新增：角色与权限 / 有效期设置 / 学员错题本
   ========================================================================== */

/* ---------- 三态复选框（`.pcheck`） ----------
   为什么必须区分三态（选中 / 半选 / 未选）：三级权限会出现"只勾了
   「导出 PDF」没勾「导出 Excel」"。折算成两态的话，用户以为两个都能导
   或都不能导，而实际只行一个 —— 这种错误要到真正点下去 403 时才暴露。 */
.pcheck {
    position: relative;
    flex: none;
    width: 18px;
    height: 18px;
    margin: 2px 0 0;
    border: 1.5px solid var(--ink-300);
    border-radius: 4px;
    background: #fff;
    cursor: pointer;
    transition: background .12s, border-color .12s;
}
.pcheck:hover { border-color: var(--brand-500); }
.pcheck.is-on {
    background: var(--brand-500);
    border-color: var(--brand-500);
}
.pcheck.is-on::after {
    content: "";
    position: absolute;
    left: 5px;
    top: 1px;
    width: 5px;
    height: 10px;
    border: solid #fff;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}
.pcheck.is-half {
    background: var(--brand-500);
    border-color: var(--brand-500);
}
.pcheck.is-half::after {
    /* 横杠：半选的标准视觉语言，和勾区分开 */
    content: "";
    position: absolute;
    left: 3px;
    top: 7px;
    width: 10px;
    height: 2px;
    background: #fff;
    border-radius: 1px;
}
@media (hover: none) { .pcheck:hover { border-color: var(--ink-300); } }

/* ---------- 权限页布局：左角色列表 + 右权限树 ---------- */
.perm-layout {
    display: grid;
    grid-template-columns: 264px minmax(0, 1fr);
    gap: 16px;
    align-items: start;
}
@media (max-width: 900px) {
    /* 窄屏改成上下堆叠：两列并排在 900px 以下每列只有 400px 出头，
       权限树的代码列会被挤没。 */
    .perm-layout { grid-template-columns: minmax(0, 1fr); }
}
.perm-roles .card__body { padding: 8px; max-height: 68vh; overflow-y: auto; }

.perm-role {
    padding: 10px 12px;
    border-radius: var(--r);
    cursor: pointer;
    border: 1px solid transparent;
    transition: background .12s, border-color .12s;
}
.perm-role:hover { background: var(--line-2); }
.perm-role.is-active {
    background: var(--c1-soft);
    border-color: var(--brand-500);
}
.perm-role__top {
    display: flex;
    align-items: center;
    gap: 6px;
}
.perm-role__name {
    font-weight: 600;
    font-size: 14px;
    color: var(--ink-900);
}
.perm-role__meta {
    margin-top: 3px;
    font-size: 12px;
    color: var(--ink-500);
}
/* 角色卡的行内操作（编辑 / 删除）。
 *
 * ★ 为什么默认 `opacity: 0` 而不是一直显示：
 *   这一栏的主交互是"点卡片选中角色"，两个常驻按钮会让每张卡
 *   都像是在喊"点我删掉"。hover / focus 时显形，键盘用户靠
 *   `:focus-within` 也能拿到（只用 hover 的话键盘根本触发不了 —— 
 *   那是无障碍缺陷，不是"设计选择"）。
 * ★ `position: static` 时它参与布局：**不能**用 `position:absolute`
 *   压到卡片右上角，那会盖住角色名（长名字直接看不全）。 */
.perm-role__ops {
    display: flex;
    gap: 6px;
    margin-top: 6px;
    opacity: 0;
    transition: opacity .12s;
}
.perm-role:hover .perm-role__ops,
.perm-role:focus-within .perm-role__ops,
.perm-role.is-active .perm-role__ops { opacity: 1; }
/* 触屏没有 hover：`@media (hover: none)` 下常显，
   否则手机上这些按钮**永远点不到**（等于功能不存在）。 */
@media (hover: none) {
    .perm-role__ops { opacity: 1; }
}

/* ---------- 权限树 ---------- */
.ptree-group {
    border: 1px solid var(--line);
    border-radius: var(--r);
    margin-bottom: 12px;
    overflow: hidden;
}
.ptree-l1 {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 9px 12px;
    background: var(--line-2);
    border-bottom: 1px solid var(--line);
}
.ptree-l1__name { font-weight: 600; font-size: 13.5px; color: var(--ink-800); }
.ptree-l1__cnt {
    margin-left: auto;
    font-size: 12px;
    color: var(--ink-500);
    font-variant-numeric: tabular-nums;
}
.ptree-l2 { padding: 4px 0; }

.ptree-row {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 8px 12px;
    flex-wrap: wrap;
}
/* 「信息配置项」那一组用浅底标出来 —— 平台管理员的排除规则靠它可核对 */
.ptree-row.is-config { background: #fffbeb; }
.ptree-l2__name { flex: 1; min-width: 0; font-size: 13.5px; color: var(--ink-800); }
.ptree-code {
    font-size: 11.5px;
    color: var(--ink-400);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    flex: none;
}
.ptree-l3 {
    flex-basis: 100%;
    display: flex;
    flex-wrap: wrap;
    gap: 6px 18px;
    padding: 4px 0 2px 28px;
}
.ptree-act {
    display: flex;
    align-items: center;
    gap: 7px;
    font-size: 13px;
    color: var(--ink-700);
}

/* ---------- 权限树的三层结构：端 → 功能分组 → 权限（10-31 第 40 轮） ---------- */
/* 端层是**外框**，功能分组缩在里面 —— 端是"这套权限给谁用"，
   功能分组是"这套权限管哪一块"。两层视觉上必须能一眼分开，
   否则管理员看到 20 个平铺的组，分不清哪些是给机构的。 */
.ptree-scope {
    border: 1px solid var(--line);
    border-radius: var(--r);
    margin-bottom: 18px;
    overflow: hidden;
}
.ptree-scope__head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 11px 14px;
    background: var(--line-2);
    border-bottom: 1px solid var(--line);
}
.ptree-scope__name {
    font-weight: 700;
    font-size: 15px;
    color: var(--ink-900);
}
/* 端层右侧的「适用角色」—— 需求里「按角色分类」的那一半 */
.ptree-scope__roles {
    font-size: 12px;
    color: var(--ink-600);
    background: var(--line);
    border-radius: 4px;
    padding: 1px 7px;
}
.ptree-scope__body { padding: 12px; }
/* 功能分组在端层里是**次级**，边框淡一些、字号小一点 */
.ptree-group--nested { margin-bottom: 10px; }
.ptree-group--nested:last-child { margin-bottom: 0; }
.ptree-group--nested .ptree-l1 { padding: 7px 10px; }
.ptree-group--nested .ptree-l1__name { font-weight: 600; font-size: 13px; }

/* 权限行右侧的「菜单入口」标注 ——
   这条标注回答"这条权限管的是侧栏里的哪个入口"，
   是需求「所有菜单都纳入权限控制中」在界面上的落点。 */
.ptree-menu {
    flex: none;
    font-size: 11.5px;
    color: var(--ink-600);
    background: var(--line-2);
    border: 1px solid var(--line);
    border-radius: 4px;
    padding: 1px 6px;
    white-space: nowrap;
}
/* 没有对应菜单入口的权限（功能开关）标出来 ——
   不标的话管理员会以为"这条权限漏配了菜单"。 */
.ptree-menu--none {
    color: var(--ink-400);
    border-style: dashed;
    background: transparent;
}

/* ---------- 有效期设置（`.retention-*`） ---------- */
.retention-sum {
    margin-top: 14px;
    padding: 10px 12px;
    border-radius: var(--r);
    background: var(--c1-soft);
    font-size: 13.5px;
    color: var(--ink-800);
}
.form-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 14px;
}

/* ---------- 学员端错题卡（`.pwrong-*`） ---------- */
.pwrong { margin: 12px 12px 0; }
.pwrong__top {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    margin-bottom: 8px;
}
.pwrong__type {
    font-size: 12px;
    color: var(--ink-500);
    background: var(--line-2);
    border-radius: 999px;
    padding: 2px 9px;
}
.pwrong__stem {
    font-size: 14.5px;
    line-height: 1.6;
    color: var(--ink-900);
    font-weight: 500;
}
.pwrong__opts { margin: 10px 0 0; }
.pwrong__opt {
    padding: 8px 10px;
    border-radius: var(--r-sm);
    font-size: 13.5px;
    line-height: 1.5;
    color: var(--ink-700);
    background: var(--surface-2);
    margin-bottom: 6px;
}
.pwrong__opt.is-right {
    background: var(--success-bg);
    color: var(--success);
    font-weight: 600;
}
.pwrong__ans {
    margin-top: 10px;
    padding: 8px 10px;
    border-radius: var(--r-sm);
    background: var(--c2-soft);
    font-size: 13.5px;
    color: var(--ink-800);
}
.pwrong__ana {
    margin-top: 8px;
    padding: 10px;
    border-radius: var(--r-sm);
    background: var(--surface-2);
    font-size: 13px;
    line-height: 1.65;
    color: var(--ink-700);
}
.pwrong__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-top: 12px;
    padding-top: 10px;
    border-top: 1px solid var(--line-2);
}
.pwrong__time { font-size: 12px; color: var(--ink-400); }

/* 题型分段控件（错题本顶部的「全部/单选/多选/判断」） */
.pseg {
    display: flex;
    gap: 6px;
    padding: 10px 12px 0;
}
.pseg__btn {
    flex: 1;
    min-height: 36px;          /* 比 44 小一点：这是筛选项不是主操作，
                                  且一行四个必须放得下 */
    border: 1px solid var(--line);
    border-radius: var(--r);
    background: #fff;
    font-size: 13px;
    color: var(--ink-600);
    cursor: pointer;
}
.pseg__btn.is-active {
    background: var(--brand-500);
    border-color: var(--brand-500);
    color: #fff;
    font-weight: 600;
}

/* ---------- ③ 答题内核（`.pexam-*`） ---------- */
/* 顶部倒计时条：固定在导航栏之下，滚动时不跟着走 ——
   考试时最需要的就是"还剩多久"始终可见。 */
.pexam-bar {
    position: sticky;
    top: var(--topbar-h);
    z-index: 35;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 12px;
    background: var(--surface);
    border-bottom: 1px solid var(--line);
}
.pexam-bar__timer {
    font-size: 17px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--ink-900);
}
.pexam-bar__timer.is-urgent { color: #dc2626; }
.pexam-bar__hint { flex: 1; color: var(--ink-500); font-size: 12px; }
.pexam-bar__btn {
    flex: none;
    min-height: 32px;
    padding: 0 14px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--surface);
    color: var(--ink-700);
    font-size: 13px;
    cursor: pointer;
}

/* 题干区 */
.pexam-q { padding: 14px 14px 4px; }
.pexam-q__no {
    display: inline-block;
    margin-right: 6px;
    color: var(--brand-500);
    font-weight: 700;
}
.pexam-q__stem {
    color: var(--ink-900);
    font-size: 15.5px;
    line-height: 1.75;
}
.pexam-q__meta {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
    color: var(--ink-500);
    font-size: 12px;
}

/* 选项：整行可点，高度够手指，落选态清晰 */
.pexam-opts { padding: 4px 10px 10px; }
.pexam-opt {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 12px 12px;
    margin-bottom: 8px;
    border: 1px solid var(--line);
    border-radius: var(--r-md, 10px);
    background: var(--surface);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.pexam-opt:active { background: var(--ink-50); }
.pexam-opt.is-picked {
    border-color: var(--brand-500);
    background: var(--brand-50, #eff6ff);
}
.pexam-opt__key {
    flex: none;
    width: 24px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--line);
    border-radius: 50%;
    background: var(--surface-2);
    color: var(--ink-600);
    font-size: 12.5px;
    font-weight: 600;
}
.pexam-opt.is-picked .pexam-opt__key {
    border-color: var(--brand-500);
    background: var(--brand-500);
    color: #fff;
}
.pexam-opt__text {
    flex: 1;
    min-width: 0;
    color: var(--ink-800);
    font-size: 14.5px;
    line-height: 1.7;
    word-break: break-word;
}
/* 判定后的两种落点（练习与模拟复盘用）。
   ★ 判定顺序在 JS 里是「先判是否正确答案、再判是否我选的」，
     所以 `is-right` 必须与 `is-picked` 同优先级 —— 靠后面的选择器覆盖是不行的，
     这里用同一条规则里显式排除：已判定的题不再显示"已选中"的蓝。 */
.pexam-opt.is-right {
    border-color: var(--p-ok);
    background: var(--p-ok-bg);
}
.pexam-opt.is-right .pexam-opt__key {
    border-color: var(--p-ok);
    background: var(--p-ok);
    color: #fff;
}
.pexam-opt.is-wrong {
    border-color: var(--p-err);
    background: var(--p-err-bg);
}
.pexam-opt.is-wrong .pexam-opt__key {
    border-color: var(--p-err);
    background: var(--p-err);
    color: #fff;
}

/* 底部操作条：上一题 / 答题卡 / 下一题。
   固定在底部，拇指可达 —— 这是手机答题与电脑答题最大的差别。 */
.pexam-actions {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 55;
    display: flex;
    gap: 8px;
    padding: 8px 12px;
    padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--line);
    background: rgba(255, 255, 255, .97);
}
@media (min-width: 720px) {
    .pexam-actions {
        max-width: 1180px;
        margin: 0 auto;
        border-left: 1px solid var(--line);
        border-right: 1px solid var(--line);
    }
}
.pexam-actions .pbtn { flex: 1; min-height: 42px; }
.pexam-actions .pbtn--card { flex: 0 0 96px; }
.pexam-space { height: 74px; }

/* 答题卡：题号网格（绿=已答，橙=当前，红字=未答） */
.pexam-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
    gap: 8px;
}
.pexam-grid__item {
    min-height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--line);
    border-radius: var(--r-sm, 8px);
    background: var(--surface);
    color: var(--ink-700);
    font-size: 14px;
    cursor: pointer;
}
.pexam-grid__item.is-done {
    border-color: var(--p-ok);
    background: var(--p-ok-bg);
    color: var(--p-ok);
}
.pexam-grid__item.is-active {
    border-color: var(--brand-500);
    background: var(--brand-500);
    color: #fff;
    font-weight: 600;
}
.pexam-grid__item.is-todo { color: var(--p-err); }

/* ---------- 视频播放器（`.pvideo-*`） ---------- */
.pvideo {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 9;
    background: #000;
    border-radius: var(--r-lg);
    overflow: hidden;
}
.pvideo video { width: 100%; height: 100%; display: block; background: #000; }
.pvideo__hint {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 8px 12px;
    background: linear-gradient(transparent, rgba(0, 0, 0, .7));
    color: #fff;
    font-size: 12px;
}
.pvideo__lesson {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 14px;
    border-bottom: 1px solid var(--line);
    cursor: pointer;
}
.pvideo__lesson.is-playing { background: var(--brand-50, #eff6ff); }
.pvideo__lesson__no {
    flex: none;
    width: 22px;
    height: 22px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--surface-2);
    color: var(--ink-600);
    font-size: 11.5px;
}
.pvideo__lesson.is-playing .pvideo__lesson__no {
    background: var(--brand-500);
    color: #fff;
}
.pvideo__lesson__main { flex: 1; min-width: 0; }
.pvideo__lesson__title {
    color: var(--ink-900);
    font-size: 14px;
    line-height: 1.5;
}
.pvideo__lesson__sub { margin-top: 2px; color: var(--ink-500); font-size: 11.5px; }
/* 学时进度条 */
.pvideo__bar {
    height: 6px;
    border-radius: 999px;
    background: var(--ink-100);
    overflow: hidden;
}
.pvideo__bar > i {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: var(--brand-500);
    transition: width .3s ease;
}
.pvideo__bar.is-ok > i { background: var(--p-ok); }

/* ---------- 列表项右侧按钮（缴费/初复训用） ---------- */
.p-list-item__act {
    flex: none;
    min-height: 32px;
    padding: 0 12px;
    border: 1px solid var(--brand-500);
    border-radius: 999px;
    background: var(--surface);
    color: var(--brand-500);
    font-size: 13px;
    line-height: 30px;
    cursor: pointer;
    white-space: nowrap;
}
.p-list-item__act:active { background: var(--brand-50, #eff6ff); }
.p-list-item__act--primary { background: var(--brand-500); color: #fff; }
.p-list-item__act--primary:active { background: var(--brand-600); }
.p-list-item__act--muted { border-color: var(--line); color: var(--ink-500); }


/* =====================================================================
   学员端全局 iOS 化（2026-10-17）
   ---------------------------------------------------------------------
   ★ 为什么全部挂在 `.stage--real` 下：学员端 DOM 由 mobile.js 生成，
     根容器 class 固定是 `stage stage--real`（mobile.js:92）；
     管理端 / 机构端用别的根，不加前缀会把它们一起改掉。

   ★ 为什么能一次性加在文件末尾：学员端是纯静态 CSS 的最后一段，
     同特异度下后来者胜；而这里刻意**不加 `!important`** ——
     要是日后有人给它加上 `!important` 就会静默压不掉旧规则，
     「新样式不生效」比「样式有冲突」难查得多。
     ===================================================================== */

/* ---------- ① 内容底色：灰底分层（iOS grouped background） ---------- */
/* 原来 `.pbody` 用 `var(--bg-app)`（白），于是白色卡片和白色底
   只靠一条极淡的边线区分，整屏发「平」。iOS 是灰底 + 白块。 */
.stage--real .pbody {
    background: var(--ios-grouped);
}
/* ★ 白色分组块的主类是 `.mcard`（61 处）与 `.mlist`（11 处），
   不是 `.pitem` —— 第 10 手改错了类，底色变了这几块却一个没变。
   ★ `border-bottom: 8px solid var(--ink-100)` 原本用「加一条粗灰边」当分隔，
     在灰底上会变成一道**更深的灰带**（两层灰叠一起）。
     改成：白块自身留白 + 发丝边，靠底色差分层 —— 这才是 iOS 的做法。 */
.stage--real .mcard,
.stage--real .mlist,
.stage--real .pitem,
.stage--real .porg {
    border-bottom: 0;
    margin: 0 12px 10px;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: none;
    border: 0.5px solid var(--ios-sep);
    background: #fff;
}
/* 首块要离顶栏有距离（原来靠 8px 粗边撑出来的） */
.stage--real .mcard:first-child,
.stage--real .mlist:first-child { margin-top: 12px; }
/* 分区标题在灰底上，iOS 是浅灰说明字（不是黑标题） */
.stage--real .psec__title {
    color: var(--ios-secondary);
    font-size: 13px;
    font-weight: 400;
    letter-spacing: .3px;
}
.stage--real .psec__more { color: var(--ios-blue); }
/* 列表行的分隔线用发丝线，且从文字起点开始（iOS 分组列表的行线） */
.stage--real .mlist__i { border-bottom: 0.5px solid var(--ios-sep); }
.stage--real .mlist__i:last-child { border-bottom: 0; }

/* ---------- ③ 按钮：iOS 实心药丸 + 浅灰次按钮 ---------- */
/* ★ 主按钮类是 `.mbtn`（28 处），`.pbtn`（3 处）也要一起覆盖。 */
.stage--real .mbtn,
.stage--real .pbtn {
    border-radius: 12px;
    border: 0;
    background: var(--ios-blue);
    color: #fff;
    font-weight: 600;
}
/* 次按钮：iOS 的 gray button 是**浅灰底、无边框、蓝字**，
   不是白底描边（描边是 Ant Design 的语言）。 */
.stage--real .mbtn--ghost,
.stage--real .pbtn:not(.pbtn--primary):not(.pbtn--ok) {
    background: rgba(118, 118, 128, .12);
    color: var(--ios-blue);
    box-shadow: none;
}
/* iOS 的 disabled 是**整体降到 1/3 不透明**，不是换色 ——
   换色会让人以为「另一种按钮」。 */
.stage--real .mbtn:disabled,
.stage--real .pbtn:disabled {
    opacity: .34;
}
/* 底部固定操作条：iOS 是**半透明毛玻璃**，不是纯白 */
.stage--real .mbtn-row,
.stage--real .mact {
    background: rgba(249, 249, 251, .94);
    backdrop-filter: saturate(180%) blur(20px);
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    border-top: 0.5px solid var(--ios-sep);
}
/* 标签栏同理 */
.stage--real .mtabbar,
.stage--real .ptabbar {
    background: rgba(249, 249, 251, .94);
    backdrop-filter: saturate(180%) blur(20px);
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    border-top: 0.5px solid var(--ios-sep);
}

/* ---------- ② 顶部条：实色而非渐变 ---------- */
/* 渐变蓝是 Material AppBar 的语言；iOS 导航栏是**一块实色**。 */
.stage--real .mtop {
    background: var(--ios-blue);
}

/* ---------- ③ 按钮：见下方「④ 按钮」一节 ---------- */

/* ---------- ④ 宫格图标：圆角方块（iOS 图标观感） ---------- */
/* 原来 46px 实心圆 + 高饱和填充，六个色相转一圈 —— 那是 Android 桌面。
   iOS 的图标是**圆角方块**，颜色少而统一。 */
.stage--real .mgrid__ico {
    border-radius: 13px;
    opacity: .92;
}
.stage--real .mgrid__label {
    color: var(--ios-label);
    font-size: 12px;
}

/* ---------- ⑤ 标签/徽章：胶囊 + iOS 语义色 ---------- */
.stage--real .pitem__badge,
.stage--real .mqopt__k {
    border-radius: var(--r-full);
}

/* ---------- ⑥ 分区标题：iOS 的次级说明字 ---------- */
.stage--real .psec__title {
    color: var(--ios-secondary);
    font-size: 13px;
    font-weight: 400;
    letter-spacing: .3px;
}
.stage--real .psec__more { color: var(--ios-blue); }

/* ---------- ⑦ 列表项：iOS 分组列表的行高与分隔线 ---------- */
.stage--real .mlist { border-radius: 12px; overflow: hidden; }

/* ---------- ⑧ 分段控件（iOS segmented control） ----------
   ★ 灰轨 + 白色选中块 + 投影。与属地弹层的 `.ppicker__tabs` 同一套观感，
     学员端凡是「两组互斥模式」的地方都用它。 */
.stage--real .mseg {
    display: flex;
    gap: 2px;
    padding: 2px;
    border-radius: 9px;
    background: rgba(118, 118, 128, .12);
}
.stage--real .mseg__b {
    flex: 1;
    min-height: 34px;
    padding: 0 12px;
    border: 0;
    border-radius: 7px;
    background: transparent;
    color: var(--ios-label);
    font-family: inherit;
    font-size: 13.5px;
    font-weight: 400;
    cursor: pointer;
}
.stage--real .mseg__b.is-on {
    background: #fff;
    color: var(--ios-label);
    font-weight: 600;
    /* 必须显式复位：基础规则里没有 box-shadow 默认值，
       不写这句会漏出继承来的按钮态投影。 */
    box-shadow: 0 3px 8px rgba(0, 0, 0, .12), 0 1px 1px rgba(0, 0, 0, .04);
}

/* ---------- ⑨ 选项行 / 题干：iOS 列表观感 ---------- */
/* 原来选项底色是 `--ink-50`（灰），选中才泛蓝 —— iOS 是反过来的：
   未选=白底发丝边（静态、可读），选中=蓝底（明确的状态）。 */
.stage--real .mqopt {
    border: 0.5px solid var(--ios-sep);
    border-radius: 12px;
    background: #fff;
    color: var(--ios-label);
}
.stage--real .mqopt:active { background: rgba(118, 118, 128, .12); }
.stage--real .mqopt.is-picked {
    border-color: var(--ios-blue);
    background: rgba(0, 122, 255, .09);
}
/* 判定态的蓝/绿/红沿用 iOS 语义色（原来走 --brand-*） */
/* 圆徽自适应：原来只适合「A/B/C/D」单个字母，
   kind.html 传「系统」「人工说明」时被撕成两行并溢出到行外。
   `border-radius:999px` 在非正方时自动渲染成胶囊 —— 观感更好，
   而且**任何页面误用都不会再撕字**。 */
.stage--real .mqopt__k {
    border-radius: 999px;
    /* ★ `width: auto` 不能省：基础规则写了 `width: 26px`，
       而 `width` 与 `min-width` 同时存在时是 **`width` 生效**、
       `min-width` 只是下限（26 < 28 时连下限也用不上）。
       只写 `min-width` 的症状 = 盒子仍 26px 宽、里面装着 4 个字
       → 溢出到行左侧 + 文字被裁掉一半。 */
    width: auto;
    min-width: 28px;
    height: 28px;
    padding: 0 9px;
    font-size: 13px;
    line-height: 28px;
    white-space: nowrap;
    flex: none;
}
.stage--real .mqopt.is-picked .mqopt__k { background: var(--ios-blue); color: #fff; }
.stage--real .mqopt.is-right .mqopt__k { background: #34c759; color: #fff; }
.stage--real .mqopt.is-right .mqopt__mark { color: #34c759; }
.stage--real .mqopt.is-wrong .mqopt__k { background: #ff3b30; color: #fff; }
.stage--real .mqopt.is-wrong .mqopt__mark { color: #ff3b30; }
/* 题干是**不装在白块里的**（选项上方一段留白文字），
   原来给的白底让它跟选项粘成一片。 */
.stage--real .mqstem {
    background: transparent;
    color: var(--ios-label);
    font-size: 15px;
    line-height: 1.65;
}
.stage--real .mqstem__no { color: var(--ios-blue); }

/* ---------- ⑪ 单行输入控件：统一 44px（iOS 最小舒适触控） ----------
   ★ 守卫抓到的三处真实缺陷：
       result.html  select#f-exam=40px
       practice.html select#f-qtype/#f-limit=36px
       kind.html    input#f-att=36px，且同页另一控件 42px（自相矛盾）
   ★ 为什么「加 min-height」不够：局部规则里写的是 `height`，
     而 `min-height` 压不住 `height`。所以两样都要写。
   ★ 44 是 iOS 的最小舒适触控高度（Human Interface Guidelines），
     原来那个「40px 是实测不裁的最小值」是**排版**诉求；
     两者冲突时取 44 —— 少 4px 换来的是手指点不准，不是少一行字。 */
.stage--real select.pinput,
.stage--real input.pinput,
.stage--real .pfield select,
.stage--real .pfield input,
.stage--real .pfield textarea {
    min-height: 44px;
}
.stage--real select.pinput {
    /* 原来 `min-height: 40px` 是为「不裁字」调的，这里升到 44 */
    height: 44px;
}
/* 文件选择框：浏览器对 `input[type=file]` 有自己的内部布局，
   44px 的盒子会把「选择文件」按钮挤扁，所以放宽到 48 并让文字小一号。 */
.stage--real .pinput--file {
    min-height: 48px;
    height: auto;
    padding: 10px 12px;
    font-size: 13px;
    line-height: 1.6;
}

/* 多行文本框不受 44 约束（它本来就多行），只保证内边距 */
.stage--real .pfield textarea { min-height: 88px; height: auto; }
.stage--real .pinput--area { min-height: 88px; height: auto; }

/* ---------- ⑫ 宫格图标：收敛到 4 个 iOS 色相 ---------- */
/* 原来 8 个色相轮转（蓝绿橙青紫红黄青），相邻两格必撞色，看着很「杂」。
   iOS 的图标是同色系为主。收敛成 4 色：蓝 / 绿 / 橙 / 紫，
   同一屏最多 4 种，多出来的复用。 */
.stage--real .mgrid__cell--blue   .mgrid__ico { background: #0a84ff; }
.stage--real .mgrid__cell--green  .mgrid__ico { background: #30d158; }
.stage--real .mgrid__cell--orange .mgrid__ico { background: #ff9f0a; }
.stage--real .mgrid__cell--violet .mgrid__ico { background: #bf5af2; }
/* 未在上表的色（cyan/rose/amber/teal）落到中性蓝 —— 宁可重复，不要第五种色 */
.stage--real .mgrid__cell--cyan .mgrid__ico,
.stage--real .mgrid__cell--rose .mgrid__ico,
.stage--real .mgrid__cell--amber .mgrid__ico,
.stage--real .mgrid__cell--teal .mgrid__ico { background: #5e5ce6; }


/* =====================================================================
   学员端 iOS 现代美学 v2（2026-10-17）
   ---------------------------------------------------------------------
   ★ 这一轮不是「再补几个 iOS 特征」，而是**减法**。

   上一轮把学员端做成了「iOS 形」（灰底 / 白块 / 发丝线 / 分段控件），
   截图一看形状对了，但仍有一处强烈的「2016 年安卓 App」气质：
   **整条蓝色顶栏 + 蓝色按钮 + 彩色实心宫格 + 实心高饱和徽章**。
   同一个蓝在一屏里出现四次、两种蓝（brand-500 与 ios-blue）并存，
   这就是「不现代」的真正来源 —— 不是不够 iOS，是**不够克制**。

   ★ 为什么整块仍挂在 `.stage--real` 下：管理端 / 机构端共用这份 CSS，
     去掉前缀会把它们的顶栏、按钮、徽章一起改掉。

   ★ 为什么能追加在文件末尾：学员端是纯静态 CSS 的最后一段，
     同特异度下后来者胜；这里刻意**不加 `!important`** ——
     有人日后给它加上就会静默压不掉旧规则，「样式不生效」比
     「样式冲突」难查得多。
   ===================================================================== */

/* ---------- ① 顶部导航条：浅色 + 深字 + 蓝色强调 ---------- */
/* 现代 iOS（iOS 13+）的导航条是**浅色系统底 + 深色标题**，
   蓝色只留给「返回箭头 / 右侧动作」这类可点元素。
   一整条实心蓝顶栏是 iOS 7-10 时代的样子，那一屏的蓝跟下面
   蓝色主按钮、蓝色徽章、蓝色选中态撞在一起，整屏发「吵」。 */
.stage--real .mtop {
    background: rgba(249, 249, 251, .94);
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    backdrop-filter: saturate(180%) blur(20px);
    border-bottom: 0.5px solid var(--ios-sep);
    color: var(--ios-label);
}
/* 标题：现代 iOS 导航栏标题是 17px semibold，不是 18px bold */
.stage--real .mtop__t {
    color: var(--ios-label);
    font-size: 17px;
    font-weight: 600;
    letter-spacing: -.2px;
}
/* 返回 / 右操作：蓝色（iOS 的强调色只给可点元素） */
.stage--real .mtop__btn {
    color: var(--ios-blue);
    border-radius: 50%;
    transition: background .15s ease;
}
.stage--real .mtop__btn:active { background: rgba(118, 118, 128, .16); }
.stage--real .mtop__btn--text {
    font-weight: 400;
    font-size: 16px;
    color: var(--ios-blue);
}
/* 顶部条上如果有页签（「我的」那四个），选中态走 iOS 的
   「文字变蓝 + 底部 2px 蓝线」，而不是蓝底白块。 */
.stage--real .mtabs { background: rgba(249, 249, 251, .94); }
.stage--real .mtabs__b.is-on {
    color: var(--ios-blue);
    box-shadow: none;
    border-bottom: 2px solid var(--ios-blue);
    border-radius: 0;
    background: transparent;
    font-weight: 600;
}
.stage--real .mtabs__b:not(.is-on) { color: var(--ios-label); }

/* ---------- ② 徽章：tinted（浅色底 + 同色系深字） ---------- */
/* 原来四种状态是高饱和**实心**底 + 白字：绿底白「已缴费」、
   红底白「不合格」。现代 iOS 的 badge 是 systemGreen/systemRed 的
   **prominent background**：约 15% 的色底 + 同色系深色文字。
   视觉重量降一档，一屏里的彩色方块从「4 个」变成「几处淡色标记」。 */
.stage--real .pitem__badge {
    padding: 0 8px;
    height: 22px;
    line-height: 22px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 500;
    letter-spacing: .2px;
    border: 0;
    background: rgba(118, 118, 128, .12);
    color: var(--ios-secondary);
    white-space: nowrap;
    flex: none;
}
.stage--real .pitem__badge--ok {
    background: rgba(52, 199, 89, .15);
    color: #248a3d;
}
.stage--real .pitem__badge--warn {
    background: rgba(255, 159, 10, .16);
    color: #a75e00;
}
.stage--real .pitem__badge--err {
    background: rgba(255, 59, 48, .13);
    color: #c4271e;
}
.stage--real .pitem__badge--off {
    background: rgba(118, 118, 128, .12);
    color: var(--ios-secondary);
}
/* 题干旁的题型标签（「判定题」）也一起降重量 */
.stage--real .mqhead__tag {
    background: rgba(118, 118, 128, .12);
    color: var(--ios-secondary);
    font-weight: 500;
}

/* ---------- ③ 圆角体系：全局收敛到 iOS 的 3 档 ---------- */
/* 原来同一个学员端里同时存在 8 / 10 / 12 / 14 / 16 / 999px 的圆角，
   视觉上每一块都在喊「我是不同的组件」。iOS 的实际做法是
   **内容块 10px / 按钮 12px / 胶囊 999px**，只有三档。 */
.stage--real .mcard,
.stage--real .mlist,
.stage--real .pitem,
.stage--real .porg { border-radius: 10px; }
.stage--real .mqopt,
.stage--real .pfield input,
.stage--real .pinput,
.stage--real .mselect { border-radius: 10px; }

/* ---------- ④ 主蓝收敛：只在每页一个最关键动作上出现 ---------- */
/* 「去支付 / 返回首页 / 提交本题」这类**页面主操作**保留实心蓝；
   同一屏里的次要动作一律降到 tinted 蓝底（iOS 的 tinted button），
   而不是再画一个实心蓝 —— 两个实心蓝在同一屏是「没想清楚层级」。 */
.stage--real .mbtn--tint {
    background: rgba(0, 122, 255, .12);
    color: var(--ios-blue);
    font-weight: 600;
}
/* 圆形提交按钮（答题页那个蓝圆勾）→ 与其它按钮同一套半径。
   圆钮在 iOS 里只有两种合法用途：Tab Bar 图标、导航栏返回。
   页面里出现第三个圆钮就会让人以为它是「特殊重要」。 */
.stage--real .mact .mbtn--round {
    border-radius: 12px;
    width: auto;
    min-width: 96px;
    padding: 0 16px;
}

/* ---------- ⑤ 列表行：iOS grouped 的行高与按压反馈 ---------- */
/* 现代 iOS 的分组列表行在按压时会整行变灰（`highlightedBackground`），
   原来只有按钮有 :active 反馈，列表行点下去「没反应」。 */
.stage--real .mlist__i,
.stage--real .pitem {
    transition: background .12s ease;
}
.stage--real .mlist__i:active,
.stage--real .pitem:active { background: rgba(118, 118, 128, .08); }
/* 行内小图标：统一淡色圆角方块，不再五颜六色 */
.stage--real .mlist__ico,
.stage--real .pitem__ico {
    border-radius: 8px;
    flex: none;
}

/* ---------- ⑥ 正文文字层级：iOS 的四级字阶 ---------- */
/* 原来标题 15-16px、正文 14px、说明 12px，字重几乎全是 400，
   一屏看起来「所有字一样重要」。iOS 的做法是用**字号 + 颜色**
   建立层级，正文保持黑色，说明文字降到 secondary。 */
.stage--real .mlist__t,
.stage--real .pitem__t {
    font-size: 16px;
    font-weight: 400;
    color: var(--ios-label);
    letter-spacing: -.2px;
}
.stage--real .mlist__s,
.stage--real .pitem__s,
.stage--real .pfield__label {
    font-size: 13px;
    color: var(--ios-secondary);
}
/* 分区标题：iOS 的 section header 是全大写或小号 secondary，
   而且**不带左侧竖线**（那是 Ant Design 的 language）。 */
.stage--real .mbar-t__t,
.stage--real .psec__title {
    color: var(--ios-secondary);
    font-size: 13px;
    font-weight: 500;
    letter-spacing: .3px;
    text-transform: none;
}
.stage--real .mbar-t,
.stage--real .psec { border-left: 0; }
.stage--real .mbar-t__t::before,
.stage--real .psec__title::before { display: none; }

/* ---------- ⑦ 分隔线：iOS inset 分组列表的行线从文字起点起 ---------- */
/* 原来 `.mlist__i` 的线是从最左通到底 —— 那是「全宽表」的做法。
   iOS grouped 的 inset 行线会**从左侧留白之后起**，
   因为左端已经有分组块的圆角边界了。 */
.stage--real .mlist__i { position: relative; }
.stage--real .mlist__i::after {
    content: "";
    position: absolute;
    left: 44px;
    right: 0;
    bottom: 0;
    height: 0.5px;
    background: var(--ios-sep);
    pointer-events: none;
}
.stage--real .mlist__i:last-child::after { display: none; }
.stage--real .mlist__i { border-bottom: 0; }

/* ---------- ⑧ 宫格：iOS 图标观感（浅底圆角方块 + 深色图标） ---------- */
/* 现代 iOS 的入口图标是**浅色系统底 + 同色系深色符号**
   （Settings 的图标就是这么画的），不是实心高饱和块。
   实心块是 Android 桌面 / 微信小程序那一档。 */
.stage--real .mgrid {
    gap: 2px 0;
    padding: 18px 6px 16px;
    background: #fff;
    border-bottom: 0;
}
.stage--real .mgrid__ico {
    width: 50px;
    height: 50px;
    border-radius: 14px;
    /* 背景色由各 tone 规则给（tint 10-14%），这里只给形状 */
    background: rgba(0, 122, 255, .12);
}
.stage--real .mgrid__cell--blue   .mgrid__ico { background: rgba(0, 122, 255, .12);  color: #007aff; }
.stage--real .mgrid__cell--green  .mgrid__ico { background: rgba(52, 199, 89, .14);  color: #248a3d; }
.stage--real .mgrid__cell--orange .mgrid__ico { background: rgba(255, 159, 10, .16); color: #a75e00; }
.stage--real .mgrid__cell--violet .mgrid__ico { background: rgba(175, 82, 222, .14); color: #8944ab; }
/* 未在上表的色统一落到主蓝的淡底 —— 宁可重复，不要第五种色 */
.stage--real .mgrid__cell--cyan .mgrid__ico,
.stage--real .mgrid__cell--rose .mgrid__ico,
.stage--real .mgrid__cell--amber .mgrid__ico,
.stage--real .mgrid__cell--teal .mgrid__ico { background: rgba(0, 122, 255, .12); color: #007aff; }
/* 宫格标签：iOS 用 12px + 深灰，选中不靠颜色而靠位置 */
.stage--real .mgrid__label {
    font-size: 12px;
    color: var(--ios-label);
    letter-spacing: -.1px;
}

/* ---------- ⑨ 空状态：iOS 的极简空态 ---------- */
.stage--real .mempty__ico,
.stage--real .pempty__ico {
    background: rgba(118, 118, 128, .1);
    border-radius: 50%;
    color: var(--ios-tertiary);
}

/* ---------- ⑩ 强调色的其它散点 ---------- */
/* 输入框聚焦：现代 iOS 的焦点环是系统蓝 2px，不是品牌深蓝 */
.stage--real .pinput:focus,
.stage--real .pfield input:focus,
.stage--real .pfield textarea:focus {
    border-color: var(--ios-blue);
    box-shadow: 0 0 0 3px rgba(0, 122, 255, .14);
}
/* 进度条 / 视频进度统一走系统蓝，不再走品牌色 */
.stage--real .pvideo__bar > i { background: var(--ios-blue); }


/* =====================================================================
   学员端 iOS 现代美学 v3（2026-10-17）
   ===================================================================== */

/* ---------- ① 图标语言统一：淡底圆角方块，全站一套 ---------- */
/* 首页 `.mgrid` 与个人中心 `.mogrid` 原来是两套处理：
   前者「彩色实心圆角块」，后者「无底色线条」。同一个 App 里
   两种图标语言并排出现，观感立刻分裂 —— 这不是「两种组件」，
   是「两种风格」。iOS Settings 的所有入口图标是同一种处理。 */
.stage--real .mogrid {
    gap: 6px 0;
    padding: 18px 6px 16px;
    background: #fff;
    border-bottom: 0;
}
.stage--real .mogrid__cell { gap: 8px; padding: 0; }
.stage--real .mogrid__ico {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: 12px;
    background: rgba(0, 122, 255, .12);
    color: #007aff;
    flex: none;
}
.stage--real .mogrid__ico--green  { background: rgba(52, 199, 89, .14);  color: #248a3d; }
.stage--real .mogrid__ico--orange { background: rgba(255, 159, 10, .16); color: #a75e00; }
.stage--real .mogrid__ico--violet { background: rgba(175, 82, 222, .14); color: #8944ab; }
/* 未登记的 tone（rose/teal/空串）落到主蓝淡底 */
.stage--real .mogrid__ico--rose,
.stage--real .mogrid__ico--teal,
.stage--real .mogrid__ico-- { background: rgba(0, 122, 255, .12); color: #007aff; }
.stage--real .mogrid__label {
    font-size: 12px;
    color: var(--ios-label);
    letter-spacing: -.1px;
}

/* ---------- ② 列表行内的小图标：同样统一成淡底方块 ---------- */
/* `.mlist__ico` 原本在学员端各页混用「淡蓝圆角方块」与
   「淡橙圆角方块」两种，看起来像「有的行是主功能、有的行是次功能」，
   其实只是 tone 传得随意。统一为系统蓝淡底，
   语义色只留给「状态」（合格/不合格）而不是「图标」。 */
.stage--real .mlist__ico {
    width: 32px;
    height: 32px;
    border-radius: 9px;
    background: rgba(0, 122, 255, .11);
    color: #007aff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.stage--real .mlist__ico--orange {
    background: rgba(255, 159, 10, .15);
    color: #a75e00;
}
/* 状态类图标（合格绿/不合格红）保留语义色，但同样降为淡底 */
.stage--real .mlist__i.is-ok   .mlist__ico { background: rgba(52, 199, 89, .14);  color: #248a3d; }
.stage--real .mlist__i.is-err  .mlist__ico { background: rgba(255, 59, 48, .13);  color: #c4271e; }

/* ---------- ③ 答题底栏：圆钮 → 10px 药丸，但保留底色 ---------- */
/* 「交卷」和不走寻常路（它结束考试），所以该一眼分开；
   但**形状必须跟全站一致** —— 圆钮是 iOS 里 Tab Bar 图标与
   导航栏返回的专属语言，页面里出现第三个圆钮会让人误判层级。 */
.stage--real .mact__b.is-primary .mact__ico {
    width: 36px;
    height: 28px;
    border-radius: 8px;
    background: var(--ios-blue);
}
/* 底栏本身：iOS 的 tab/action bar 是毛玻璃 + 发丝顶线 */
.stage--real .mact {
    background: rgba(249, 249, 251, .94);
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    backdrop-filter: saturate(180%) blur(20px);
    border-top: 0.5px solid var(--ios-sep);
    padding: 6px 4px calc(6px + env(safe-area-inset-bottom, 0px));
}
.stage--real .mact__b { color: var(--ios-secondary); min-height: 54px; }
.stage--real .mact__b.is-primary { color: var(--ios-blue); }
.stage--real .mact__b.is-off { color: var(--ios-tertiary); }
.stage--real .mact__label { font-size: 11px; letter-spacing: .1px; }
.stage--real .mact__hint {
    font-size: 10px;
    color: var(--ios-tertiary);
    line-height: 1.25;
}

/* ---------- ④ 顶栏下方的呼吸 ---------- */
/* 顶栏改成浅色 + 发丝线之后，第一块内容紧贴分隔线会显得「顶满」。
   iOS 的做法是内容自带上边距；这里给第一块白卡 12px 净空。
   ★ 只作用于 `:first-child`，不要给所有卡片加 padding ——
     那样卡片之间会出现不均匀的缝隙。 */
.stage--real .pbody > .mogrid:first-child,
.stage--real .pbody > .mgrid:first-child,
.stage--real .pbody > .mcard:first-child,
.stage--real .pbody > .mlist:first-child { margin-top: 12px; }

/* ---------- ⑤ 分段控件与页签：iOS 的两套，别混 ---------- */
/* 分段控件（灰轨 + 白块）用于「两组互斥模式」；
   顶部页签（文字 + 下划线）用于「同级页面切换」。
   上一轮把 profile 顶部那四个当成分段控件了，实际它们的语义是页签。 */
.stage--real .mbar-t {
    background: transparent;
    padding: 18px 16px 8px;
}
.stage--real .mbar-t__t {
    font-size: 15px;
    font-weight: 600;
    color: var(--ios-label);
    letter-spacing: -.2px;
}
.stage--real .mbar-t__more {
    color: var(--ios-blue);
    font-size: 14px;
    display: inline-flex;
    align-items: center;
    gap: 2px;
}

/* ---------- ⑥ 顶部条上的分隔线只在有滚动时出现 ---------- */
/* iOS 的导航条分隔线是为了区分「正在滚的内容」与「固定的条」；
   顶部永远是同色块时这条线是多余的。而这里内容区本身是
   浅色（灰底），所以线必须留 —— 但要够淡。 */
.stage--real .mtop { border-bottom: 0.5px solid var(--ios-sep); }


/* =====================================================================
   学员端 iOS 现代美学 v4（2026-10-17）
   ===================================================================== */

/* ---------- ① 行分隔线起点对齐文字列 ---------- */
/* 实测：行 padding-left=16px、图标 32px、图标与文字间距 12px
   → 文字列起点是 60px。之前写死 44px，线有一段压在图标下面。 */
.stage--real .mlist__i::after { left: 60px; }

/* ---------- ② 选项行：内容内边距 ---------- */
/* 原来 `.mqopt` 只有垂直内边距，文字紧贴 0.5px 发丝边 ——
   iOS 的 inset 行内容一律留 12px。 */
.stage--real .mqopt {
    padding: 10px 12px;
    align-items: flex-start;
    gap: 10px;
}
/* 徽章与第一行文字对齐，而不是垂直居中 —— 徽章只有 28px 高，
   文字两行时居中会让它悬在半空（kind 页长选项）。 */
.stage--real .mqopt__k {
    margin-top: 1px;
}
/* 文字列独立占剩余宽度并允许换行，徽章不被压 */
.stage--real .mqopt__t,
.stage--real .mqopt__body {
    flex: 1;
    min-width: 0;
    line-height: 1.55;
}
/* 选中勾在右侧，垂直居中（它是操作结果不是文本） */
.stage--real .mqopt__mark { align-self: center; flex: none; }

/* ---------- ③ 字段行（成绩/轮次/机构/签发）统一左留白 ---------- */
/* ★ 真实类名是 `.prow`（certs/history/kind 三页在用）。
   我第一版凭印象写成 `.plist__row` / `.cert__row` —— 那是**编的**，
   空规则既不报错也不生效，是最隐蔽的一类假修复。
   ★ label 必须给固定宽度：原来只有 `flex-shrink: 0`，
     「轮次」和「签发」字号相同但 `flex` 会按内容分配，
     value 右对齐时两行的左边界就参差了。 */
.stage--real .prow {
    padding: 13px 16px;
    border-bottom: 0.5px solid var(--ios-sep);
    gap: 12px;
}
.stage--real .prow:last-child { border-bottom: 0; }
.stage--real .prow__label {
    flex: none;
    width: 62px;
    color: var(--ios-secondary);
    font-size: 14px;
}
.stage--real .prow__value {
    flex: 1;
    min-width: 0;
    color: var(--ios-label);
    font-size: 15px;
    text-align: right;
}
.stage--real .p-sec-title {
    padding: 20px 16px 8px;
    color: var(--ios-secondary);
    font-size: 13px;
    font-weight: 500;
    letter-spacing: .3px;
}
.stage--real .mlist__i .mono,
.stage--real .mcard__t .mono { color: var(--ios-blue); }

/* ---------- ④ 分区标题与首卡的间距 ---------- */
/* iOS 的 section header 与下方分组块之间有 6px，
   与上方内容之间有 18px。标题不再是「贴在卡片上的标签」。 */
.stage--real .mbar-t { padding: 20px 16px 8px; }
.stage--real .psec { padding: 20px 16px 8px; }

/* ---------- ⑤ 说明文字与危险操作的层级 ---------- */
/* 「手机号是短信验证码登录」这类说明是 tertiary，
   不是 secondary —— 它比字段标签还低一级。 */
.stage--real .mnote,
.stage--real .pnote,
.stage--real .mhint {
    color: var(--ios-tertiary);
    font-size: 12.5px;
    line-height: 1.6;
}
/* 退出登录：iOS 的 destructive 是**红字**，不做成红底块 */
.stage--real .mlist__i.is-danger .mlist__t,
.stage--real .mlist__i.is-danger { color: #ff3b30; }
.stage--real .mlist__i.is-danger .mlist__ico {
    background: rgba(255, 59, 48, .13);
    color: #ff3b30;
}
.stage--real .mlist__i.is-danger .mlist__s { color: var(--ios-tertiary); }


/* =====================================================================
   学员端 iOS 现代美学 v5（2026-10-17）
   ===================================================================== */

/* ---------- ① 选中胶囊：长文本自动降为 tinted ---------- */
/* `.mqopt.is-picked .mqopt__k` 统一「实心蓝底 + 白字」是给
   A/B/C/D 设计的：单字母必须反白才看得清。
   但 kind.html 传的是「按历史合格成绩」这种 7 字胶囊 ——
   实心蓝压满整行，成为全屏最重的色块，比主按钮还重。
   `.is-long` 由 Mobile.qopt 按字数打上（>4 字）。 */
.stage--real .mqopt.is-picked .mqopt__k.is-long {
    background: rgba(0, 122, 255, .14);
    color: var(--ios-blue);
}
/* 判定态（对/错）同理：长说明文字不该整块实心 */
.stage--real .mqopt.is-right .mqopt__k.is-long {
    background: rgba(52, 199, 89, .15);
    color: #248a3d;
}
.stage--real .mqopt.is-wrong .mqopt__k.is-long {
    background: rgba(255, 59, 48, .13);
    color: #c4271e;
}

/* ---------- ② 表单区的标签层级 ---------- */
/* 「认定依据」「申请说明」「历史证书材料」这些是 section 级标签，
   不是字段标签 —— iOS 的 section header 比字段标签低半级，
   但比正文高。用 secondary + 13px 是对的，
   原来给的是 12.5px + ink-500（偏小偏淡）。 */
.stage--real .pfield__label {
    color: var(--ios-secondary);
    font-size: 13.5px;
    font-weight: 400;
    margin-bottom: 7px;
    display: block;
}
/* 字段之间的垂直节奏：iOS 的表单 section 之间是 22-28px */
.stage--real .pfield { margin-bottom: 18px; }

/* ---------- ③ 文本域与文件框：iOS 的填充式控件 ---------- */
.stage--real .pfield textarea,
.stage--real .pinput--area {
    background: #fff;
    border: 0.5px solid var(--ios-sep);
    border-radius: 10px;
    color: var(--ios-label);
    font-size: 16px;
    line-height: 1.6;
    padding: 12px;
}
.stage--real .pfield textarea::placeholder { color: var(--ios-tertiary); }
.stage--real .pinput--file {
    background: rgba(118, 118, 128, .07);
    border: 0.5px dashed var(--ios-sep);
    border-radius: 10px;
    color: var(--ios-secondary);
}

/* ---------- ④ 空状态卡：iOS 的 centered empty state ---------- */
/* 空态原来是一个大白卡套一个圆图标，视觉上像「这里有内容但没显示」。
   iOS 的空态是**直接放在分组块里**的，不需要额外的框。 */
.stage--real .mcard:has(.mempty) { background: transparent; border: 0; }
.stage--real .mempty {
    padding: 48px 24px;
    text-align: center;
}
.stage--real .mempty__ico {
    width: 72px;
    height: 72px;
    border-radius: 50%;
    background: rgba(118, 118, 128, .1);
    color: var(--ios-tertiary);
    margin: 0 auto 16px;
    display: flex;
    align-items: center;
    justify-content: center;
}
.stage--real .mempty__t {
    color: var(--ios-secondary);
    font-size: 14px;
    line-height: 1.6;
}


/* =====================================================================
   学员端 iOS 现代美学 v6（2026-10-17）
   ===================================================================== */

/* ---------- ① 卡片内的小标题（替掉逐个手写的内联样式） ---------- */
/* 出现 4 处（kind 页的「本场认定依据 / 主张档位 / 认定依据 /
   申请说明 / 历史证书材料」）。内联样式的代价是同一个值有四个真相源。 */
.stage--real .msub {
    color: var(--ios-secondary);
    font-size: 13.5px;
    line-height: 1.5;
    margin-bottom: 8px;
}
/* 同级小标题之间要有上边距，否则它会贴死上方控件 */
.stage--real .msub--gap { margin-top: 20px; }
/* 弱一档（卡片首行的小说明） */
.stage--real .msub--weak {
    color: var(--ios-tertiary);
    font-size: 12.5px;
    margin-bottom: 6px;
}

/* ---------- ② sticky 顶栏与首块内容的净空 ---------- */
/* `.pbody` 是滚动容器，顶栏是 `position: sticky; top: 0`。
   首块内容若没有上边距，会滑到顶栏底下被切掉一截
   （kind 页的「认定依据」就是这样少的）。 */
/* `.pbody` 是滚动容器，顶栏是 `position: sticky; top: 0`。
   首块内容若没有任何上边距，它会**滑到顶栏底下**被切掉 ——
   截图里 kind 页的「认定依据」就是这样少了一截。
   ★ 这里不能给 `.pbody` 加 padding-top：那会让「滚到顶时
     内容与顶栏之间多出一条空隙」——正确做法是给**首块**留白，
     因为块本身滚走后留白也跟着走。 */
.stage--real .pbody > *:first-child:not(.mtop):not(.mact-space):not(.mbacktop),
.stage--real .pbody > .mtop + *:first-of-type { margin-top: 12px; }


/* =====================================================================
   学员端 iOS 现代美学 v7（2026-10-17）
   ===================================================================== */

/* ---------- ① 列表行的值列 ---------- */
/* 原来 4 个页面各自内联写 `flex:none;text-align:right`，其中两处
   写成了 `flex:none` —— 值列按内容取宽，长值（「不公布（正式考试
   只公布是否合格）」）会把标签挤掉并被省略号截断。
   值列必须 `flex:1 + min-width:0` 才能换行。 */
.stage--real .mlist__v {
    flex: 1;
    min-width: 0;
    text-align: right;
    color: var(--ios-label);
    font-size: 15px;
    line-height: 1.5;
    word-break: break-word;
}
/* 标签列固定宽度，与 iOS 的 inset 列表对齐 */
.stage--real .mlist__body { min-width: 0; }

/* ---------- ② 卡片内部的 section 标题 ---------- */
/* `.mbar-t` 的 20px 上边距是为「灰底上的分区」调的；
   同一个类被用在卡片内部（kind 的「我的认定申请」）时，
   20px 会让标题离上方控件太远，而 12px 才是卡片内部的节奏。
   `.mcard > .mbar-t` 精确命中这个场景，不影响页面级分区。 */
.stage--real .mcard > .mbar-t {
    padding: 14px 16px 8px;
}
/* 卡片内标题的标题字重降一档 —— 它是次级标题不是页面标题 */
.stage--real .mcard > .mbar-t .mbar-t__t {
    font-size: 14px;
    font-weight: 500;
    color: var(--ios-secondary);
}

/* ---------- ③ 说明段落与分隔 ---------- */
.stage--real .pnote-block {
    margin-top: 14px;
    padding-top: 12px;
    border-top: 0.5px solid var(--ios-sep);
    color: var(--ios-secondary);
    font-size: 12.5px;
    line-height: 1.75;
    text-align: left;
}

/* ---------- ④ 底部按钮条：iOS 的双按钮布局 ---------- */
/* 「去错题本 / 返回首页」两个等宽并排，是 iOS 的 standard pair。
   主次要靠**填充**区分（实心 vs tint），不靠宽度。 */
.stage--real .mbtn-row {
    padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
    gap: 10px;
}
.stage--real .mbtn-row .mbtn { flex: 1; }

/* =====================================================================
   学员端 · 液态毛玻璃 + 浅蓝小程序风格（2026-10-17 第 3 轮）
   ---------------------------------------------------------------------
   ★ 全部挂在 `.stage--real` 下：管理端/机构端/卫健委端共用这份 CSS，
     去掉前缀会把它们的卡片与弹层一起改掉。

   ★ 追加在文件末尾：学员端是纯静态 CSS 的最后一段，同特异度下后来者胜。
     这里刻意**不加 `!important`** —— 日后有人加上就会静默压不掉旧规则，
     「样式不生效」比「样式冲突」难查得多。

   本块解决两件事：
     ① 视觉：半透明渐变底 + 背景模糊 + 柔和光晕 + 高光描边 + 细腻阴影；
     ② 弹窗规范：**全部居中**，禁止从底部/边缘滑出。
   ===================================================================== */

/* =====================================================================
   ① 液态毛玻璃（Liquid Glass）
   ===================================================================== */

/* ---------- 底：浅蓝渐变 + 三处柔和光晕 ---------- */
/* 为什么不用纯色底：毛玻璃的"玻璃感"来自**背后有东西可透**。
   纯色底上开 backdrop-filter 只会得到"糊掉的纯色"，看不出是玻璃。
   所以这一层必须先有渐变与光斑，卡片才有东西可折射。 */
.stage--real .pbody {
    background:
        radial-gradient(120% 62% at 8% -6%, rgba(0, 122, 255, .16) 0%, rgba(0, 122, 255, 0) 62%),
        radial-gradient(92% 48% at 104% 8%, rgba(90, 200, 250, .18) 0%, rgba(90, 200, 250, 0) 58%),
        radial-gradient(120% 52% at 50% 112%, rgba(120, 160, 255, .14) 0%, rgba(120, 160, 255, 0) 64%),
        linear-gradient(178deg, #eaf3ff 0%, #f2f7ff 34%, #eef4fd 68%, #e9f1fb 100%);
    background-attachment: local;
}

/* ---------- 玻璃卡：半透明 + 背景模糊 + 高光描边 + 细腻阴影 ---------- */
/* ★ 四层各司其职，缺一层就不成立：
     · `rgba(255,255,255,.72)` —— 不是纯白也不是纯透。纯白会让
       backdrop-filter 白做（后面没东西可透就看不出模糊），
       全透明则深色背景下文字不可读（见下面"深浅背景"一节）。
     · `backdrop-filter` —— 模糊**背后已经画好的渐变**。
     · `border: .5px solid rgba(255,255,255,.85)` —— **高光描边**。
       这一层最容易被忽略：没有它，玻璃卡和灰底之间没有边界，
       视觉上会"化开"。半透明白描边模拟玻璃棱边反光。
     · `box-shadow` 外投影给"悬浮"，`inset 0 1px 0` 内高光给"厚度"。
       两者一起才有 Liquid Glass 的塑料感，而不是半透明灰色块。
   ★ `overflow: hidden` 保留：`.mlist__i::after` 的行分隔线是绝对定位，
     不裁剪会溢出圆角。 */
.stage--real .mcard,
.stage--real .mlist,
.stage--real .pitem,
.stage--real .porg {
    background: rgba(255, 255, 255, .72);
    -webkit-backdrop-filter: saturate(180%) blur(18px);
    backdrop-filter: saturate(180%) blur(18px);
    border: .5px solid rgba(255, 255, 255, .85);
    box-shadow:
        0 8px 24px rgba(31, 86, 180, .10),
        0 2px 6px rgba(31, 86, 180, .06),
        inset 0 1px 0 rgba(255, 255, 255, .9);
}
/* 宫格与描边宫格本体也是玻璃块（原来硬写 `background:#fff`） */
.stage--real .mgrid,
.stage--real .mogrid {
    background: rgba(255, 255, 255, .72);
    -webkit-backdrop-filter: saturate(180%) blur(18px);
    backdrop-filter: saturate(180%) blur(18px);
    border-bottom: 0;
}

/* ---------- 深浅背景下的文字可读性 ---------- */
/* 毛玻璃是**半透**的，文字能不能读取决于玻璃底与底色的合成亮度。
   这里给三条硬保证，缺一条就会出现"玻璃上的灰字"：
     ① 正文用近黑（`--ios-label` = #000），与最亮的玻璃底也有
        19:1 对比，远超 WCAG AA 的 4.5:1。
     ② 次级/三级说明字原本是 `rgba(60,60,67,.6/.3)`，在半透玻璃上
        再透一层底色后会掉到 3:1 以下 —— 这里抬到 .72/.5。
     ③ 选项行、题干等"读内容"的区域改成**不透明白**，
        内容可读性不该为了玻璃感让渡。 */
.stage--real {
    --ios-secondary: rgba(28, 44, 68, .72);
    --ios-tertiary: rgba(28, 44, 68, .5);
    --lg-text: #0b1f33;
}
.stage--real .mqopt,
.stage--real .pinput,
.stage--real .pfield input,
.stage--real .pfield textarea,
.stage--real .pinput--area,
.stage--real select.pinput {
    background: rgba(255, 255, 255, .92);
    box-shadow: inset 0 1px 2px rgba(31, 86, 180, .05);
}
/* 按下反馈：玻璃块变实一点，比换色更像"被按住" */
.stage--real .mlist__i:active,
.stage--real .pitem:active,
.stage--real .mqopt:active {
    background: rgba(0, 122, 255, .10);
}

/* ---------- 固定条：玻璃 + 发丝高光 ---------- */
/* 顶栏 / 底栏 / 标签栏 / 答题操作条都是"浮在内容之上"的，
   不透明白会让玻璃感在滚动到一半时突然消失 —— 必须是半透玻璃。 */
.stage--real .mtop,
.stage--real .mtabs {
    background: rgba(244, 249, 255, .74);
    -webkit-backdrop-filter: saturate(180%) blur(22px);
    backdrop-filter: saturate(180%) blur(22px);
    border-bottom: .5px solid rgba(255, 255, 255, .9);
    box-shadow: 0 1px 12px rgba(31, 86, 180, .07);
    color: var(--lg-text);
}
.stage--real .mact,
.stage--real .mtabbar,
.stage--real .ptabbar,
.stage--real .mbtn-row {
    background: rgba(244, 249, 255, .78);
    -webkit-backdrop-filter: saturate(180%) blur(22px);
    backdrop-filter: saturate(180%) blur(22px);
    border-top: .5px solid rgba(255, 255, 255, .9);
    box-shadow: 0 -1px 14px rgba(31, 86, 180, .08);
}

/* ---------- 主按钮：渐变 + 内高光（小程序那套观感） ---------- */
.stage--real .mbtn,
.stage--real .pbtn--primary {
    background: linear-gradient(135deg, #3aa0ff 0%, #0a72e8 100%);
    box-shadow:
        0 6px 16px rgba(10, 114, 232, .28),
        inset 0 1px 0 rgba(255, 255, 255, .38);
}
.stage--real .mbtn:active,
.stage--real .pbtn--primary:active {
    background: linear-gradient(135deg, #2f92f5 0%, #0866d4 100%);
}

/* =====================================================================
   ② 弹窗规范：全部居中，禁止从底部/边缘滑出
   ===================================================================== */

/* ---------- 三种遮罩统一居中 ---------- */
/* 原来的三套遮罩有三种行为：
     `.psheet-mask` 已居中（place-items:center）；
     `.pdlg-mask` 是 `align-items:flex-end`（**从底部滑出**）；
     `.ppicker__mask` 的面板是 `position:absolute; bottom:0`（**贴底**）。
   现在三者统一为居中 + 缩放淡入。 */
.stage--real .pdlg-mask,
.stage--real .psheet-mask,
.stage--real .ppicker__mask {
    position: fixed;
    inset: 0;
    z-index: 90;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 22px;
    background: rgba(18, 42, 74, .38);
    -webkit-backdrop-filter: blur(6px) saturate(140%);
    backdrop-filter: blur(6px) saturate(140%);
    animation: lg-mask-in .22s var(--ease-out) forwards;
}
/* `transform: scale(.94)` + 透明度，而不是位移 ——
   位移动画（translateY / translate 百分比）就是"从边缘滑出"的观感，
   换掉动画本身就是这条规范的一半。 */
@keyframes lg-mask-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes lg-panel-in {
    from { opacity: 0; transform: scale(.94); }
    to { opacity: 1; transform: scale(1); }
}

/* ---------- 面板本体：居中玻璃卡 ---------- */
.stage--real .pdlg,
.stage--real .psheet,
.stage--real .ppicker {
    position: relative;
    top: auto;
    left: auto;
    right: auto;
    bottom: auto;
    width: 100%;
    max-width: 420px;
    max-height: calc(100% - 8px);
    overflow: hidden;
    border-radius: 22px;
    background: rgba(252, 254, 255, .88);
    -webkit-backdrop-filter: saturate(180%) blur(26px);
    backdrop-filter: saturate(180%) blur(26px);
    border: .5px solid rgba(255, 255, 255, .92);
    box-shadow:
        0 24px 64px rgba(18, 42, 74, .28),
        0 6px 18px rgba(18, 42, 74, .14),
        inset 0 1px 0 rgba(255, 255, 255, .95);
    transform: none;
    animation: lg-panel-in .26s var(--ease-out) forwards;
}
/* 属地选择器更高，单独放宽（它的滚轮窗固定 5 行高 240px） */
.stage--real .ppicker--two { max-width: 460px; }

/* ---------- 标题条：左标题 + 右关闭 ---------- */
/* 规范要求「可关闭的弹窗统一在右上角提供关闭按钮」。
   原来三处各写一份头部：答题卡把「关闭」放**左上**、属地选择器
   左边是「取消」右边是「确定」—— 关闭与取消、确定三件事挤在一条
   44px 高的窄条上，44px 的触摸区只有标题那么宽。
   统一成「左标题 + 右上角 × 之后」，取消/确定仍留在底部按钮区。 */
.stage--real .psheet__bar {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 14px 14px 12px 18px;
    border-bottom: .5px solid rgba(60, 60, 67, .18);
}
.stage--real .psheet__title {
    flex: 1;
    min-width: 0;
    color: var(--lg-text);
    font-size: 16px;
    font-weight: 600;
    letter-spacing: -.2px;
}
/* 关闭按钮：44×44 触摸区，视觉上一枚 28px 的圆形 */
.stage--real .psheet__x {
    flex: none;
    width: 44px;
    height: 44px;
    margin: -8px -8px -8px 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: rgba(28, 44, 68, .55);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.stage--real .psheet__x:active { background: rgba(118, 118, 128, .16); }
.stage--real .psheet__x svg { width: 20px; height: 20px; }

/* 不可关闭弹窗的标注条。
   规范要求「不可关闭的弹窗需明确标注」—— 不写出来的后果是：
   学员点右上角没反应、点遮罩也没反应，只能以为页面卡死。 */
.stage--real .psheet__lock {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 4px 9px;
    border-radius: 999px;
    background: rgba(255, 159, 10, .16);
    color: #a75e00;
    font-size: 11.5px;
    font-weight: 600;
    white-space: nowrap;
}
.stage--real .psheet__lock svg { width: 13px; height: 13px; }

/* ---------- 面板正文与按钮区 ---------- */
.stage--real .psheet__body {
    padding: 18px 18px 16px;
    color: var(--lg-text);
    font-size: 15px;
    line-height: 1.75;
    overflow-y: auto;
}
.stage--real .pdlg__body { padding: 22px 20px 16px; color: var(--lg-text); }
.stage--real .pdlg__title { color: var(--lg-text); }
/* 底部按钮：并排等宽，取消是浅灰、确认是渐变主色 */
.stage--real .psheet__foot,
.stage--real .pdlg__foot {
    display: flex;
    gap: 10px;
    padding: 0 14px calc(14px + env(safe-area-inset-bottom, 0px));
    border-top: 0;
}
.stage--real .psheet__foot .pbtn,
.stage--real .pdlg__foot .pbtn { flex: 1; }

/* `.ppicker__head` 沿用旧结构但改成玻璃标题条，
   右上角「确定」移到右下角按钮区、左上角「取消」由右上角 × 承担。 */
.stage--real .ppicker--two .ppicker__head {
    padding: 14px 16px 12px;
    background: transparent;
    border-bottom: .5px solid rgba(60, 60, 67, .18);
}
.stage--real .ppicker--two .ppicker__hint { background: rgba(228, 238, 252, .7); }

/* ---------- 答题卡宫格：标记题目的独立视觉标识 ---------- */
/* 「已标记」与「当前题」是两个**正交**维度，形状也不同：
   当前题是**实心蓝块**（位置感），标记是**右上角角标**（状态感）。
   用实心块表示标记会与当前题撞色，学员分不清"我在第几题"和"哪题待定"。 */
.stage--real .pexam-grid { gap: 9px 9px; }
.stage--real .pexam-grid__item {
    position: relative;
    min-height: 46px;
    border-radius: 12px;
    border: .5px solid rgba(60, 60, 67, .2);
    background: rgba(255, 255, 255, .9);
    color: var(--lg-text);
    font-variant-numeric: tabular-nums;
    overflow: hidden;
}
.stage--real .pexam-grid__item.is-done {
    background: rgba(52, 199, 89, .16);
    border-color: rgba(52, 199, 89, .5);
    color: #1f7a37;
}
.stage--real .pexam-grid__item.is-active {
    background: linear-gradient(135deg, #3aa0ff 0%, #0a72e8 100%);
    border-color: transparent;
    color: #fff;
    font-weight: 600;
}
/* 角标：右上角一枚实心橙点 + 下缘一条橙条。
   形状（点+条）与状态色（橙=待定）双重承载，不只靠颜色。 */
.stage--real .pexam-grid__item.is-mark::after {
    content: "";
    position: absolute;
    top: 6px;
    right: 6px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #ff9f0a;
    box-shadow: 0 0 0 1.5px rgba(255, 255, 255, .9);
}
.stage--real .pexam-grid__item.is-mark::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 3px;
    background: #ff9f0a;
}
/* 已标记 + 当前题：橙条要在蓝块上仍然看得见 */
.stage--real .pexam-grid__item.is-mark.is-active::after {
    box-shadow: 0 0 0 1.5px rgba(10, 114, 232, .55);
}

/* 答题卡图例 */
.stage--real .pexam-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 8px 16px;
    margin-top: 14px;
    padding-top: 12px;
    border-top: .5px solid rgba(60, 60, 67, .16);
    color: rgba(28, 44, 68, .72);
    font-size: 12px;
}
.stage--real .pexam-legend i {
    display: inline-block;
    width: 12px;
    height: 12px;
    border-radius: 4px;
    vertical-align: -2px;
    margin-right: 5px;
}
.stage--real .pexam-legend .lg-now   { background: linear-gradient(135deg, #3aa0ff, #0a72e8); }
.stage--real .pexam-legend .lg-done  { background: rgba(52, 199, 89, .3); border: .5px solid rgba(52, 199, 89, .6); }
.stage--real .pexam-legend .lg-todo  { background: rgba(255, 255, 255, .9); border: .5px solid rgba(60, 60, 67, .3); }
.stage--real .pexam-legend .lg-mark  { background: #ff9f0a; border-radius: 50%; width: 10px; height: 10px; }

/* =====================================================================
   ③ 新增组件：标记列表 / 状态筛选标签 / 玻璃分段控件
   ===================================================================== */

/* 标记列表（答题区快速跳转） */
.stage--real .mlist-mark {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
    gap: 10px;
}
.stage--real .mlist-mark__b {
    min-height: 52px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    border: .5px solid rgba(255, 159, 10, .5);
    border-radius: 12px;
    background: rgba(255, 159, 10, .12);
    color: #a75e00;
    font-family: inherit;
    font-size: 17px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
}
.stage--real .mlist-mark__b:active { background: rgba(255, 159, 10, .24); }
.stage--real .mlist-mark__b small {
    font-size: 11px;
    font-weight: 400;
    opacity: .8;
}

/* 状态筛选标签（「进行中 / 已结束 / 已取消」+ 计数） */
.stage--real .mfilter {
    display: flex;
    gap: 8px;
    padding: 4px;
    margin: 12px 12px 0;
    border-radius: 12px;
    background: rgba(255, 255, 255, .6);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
    border: .5px solid rgba(255, 255, 255, .8);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .85);
}
.stage--real .mfilter__b {
    flex: 1;
    min-height: 42px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 5px;
    border: 0;
    border-radius: 9px;
    background: transparent;
    color: rgba(28, 44, 68, .72);
    font-family: inherit;
    font-size: 13.5px;
    cursor: pointer;
    transition: background .16s var(--ease-out), box-shadow .16s var(--ease-out);
}
.stage--real .mfilter__b.is-on {
    background: rgba(255, 255, 255, .96);
    color: var(--lg-text);
    font-weight: 600;
    box-shadow: 0 2px 8px rgba(31, 86, 180, .14);
}
/* 计数胶囊：数量为 0 时也要显示 —— "0"本身是一条信息（这个状态没有记录） */
.stage--real .mfilter__n {
    min-width: 20px;
    height: 20px;
    padding: 0 6px;
    border-radius: 999px;
    background: rgba(118, 118, 128, .16);
    color: rgba(28, 44, 68, .72);
    font-size: 12px;
    font-weight: 600;
    line-height: 20px;
    text-align: center;
    font-variant-numeric: tabular-nums;
}
.stage--real .mfilter__b.is-on .mfilter__n {
    background: rgba(0, 122, 255, .14);
    color: var(--ios-blue);
}

/* =====================================================================
   ④ 小程序风格细节：滚动条、选中态、禁用态
   ===================================================================== */
/* 选中文本：iOS/小程序默认是灰色选中，几乎看不出；
   长题干里高亮一个关键词是真实需求。 */
.stage--real .pbody ::selection {
    background: rgba(0, 122, 255, .22);
}
/* 禁用态：降不透明 + 去掉阴影（阴影还在会显得"还能按"） */
.stage--real .mbtn:disabled,
.stage--real .pbtn:disabled,
.stage--real .mact__b.is-off {
    box-shadow: none;
    opacity: .34;
}
/* 卡片内的分隔线在玻璃上要更淡，否则玻璃感被一排黑线打散 */
.stage--real .mlist__i::after { background: rgba(60, 60, 67, .16); }
.stage--real .mlist__i { border-bottom: 0; }

/* =====================================================================
   ⑤ 补充：横向胶囊（学科筛选）、标记按钮选中态、弹层说明行
   ===================================================================== */

/* 横向滚动的胶囊条。学科筛选用它而不是 `<select>`：
   学科是中文短词（必考题/法规/化学/物理/其他），横排一眼看完全部；
   `<select>` 在手机上要展开一层，而点「化学」的人往往就是要确认
   「还有哪几个学科可切」。横向滚动在 5~6 个学科时最省操作。 */
.stage--real .mchips {
    display: flex;
    gap: 8px;
    padding: 10px 12px 2px;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}
.stage--real .mchips::-webkit-scrollbar { display: none; }
.stage--real .mchips__b {
    flex: none;
    min-height: 34px;
    padding: 0 14px;
    border: .5px solid rgba(60, 60, 67, .18);
    border-radius: 999px;
    background: rgba(255, 255, 255, .7);
    color: rgba(28, 44, 68, .72);
    font-family: inherit;
    font-size: 13px;
    white-space: nowrap;
    cursor: pointer;
    transition: background .16s var(--ease-out), color .16s var(--ease-out);
}
.stage--real .mchips__b.is-on {
    background: rgba(0, 122, 255, .14);
    border-color: rgba(0, 122, 255, .3);
    color: var(--ios-blue);
    font-weight: 600;
}

/* 弹层标题条下面那一行说明。
   `.pexam-legend` 的四条图例是"图例"，这是一行"说明"，两者视觉上必须分开，
   否则一行说明会被读成图例（"这些色块是什么意思？"）。 */
.stage--real .psheet__intro {
    margin-bottom: 12px;
    padding-bottom: 12px;
    border-bottom: .5px solid rgba(60, 60, 67, .16);
    color: rgba(28, 44, 68, .72);
    font-size: 12.5px;
    line-height: 1.6;
}

/* 标记按钮的选中态：橙色淡底 + 橙字（不是蓝 —— 蓝是"选中"，
   而这里的语义是"待定"，用蓝会与「已答」混在一起）。 */
.stage--real .mbtn.is-on {
    background: rgba(255, 159, 10, .18);
    color: #a75e00;
}

/* 卡片内的横向胶囊条不该再带上下 padding（卡片自己已有内边距） */
.stage--real .mcard .mchips { padding: 0 0 2px; }

/* ==========================================================================
   2026-10-17 · 把新增的内联样式收成语义类
   --------------------------------------------------------------------------
   为什么单独一块：`check_inline_styles` 的基线是**只能往下走的数字**
   （基线是 243，本轮一度到 259 = 红）。抬基线就等于把守卫改成永远通过，
   所以新写的样式一律走类。

   ★ 全部挂在 `.stage--real` 下：这几个页面都在学员端根容器里，
     不加前缀会连管理端 / 机构端 / 卫健委端一起改。
   ========================================================================== */
.stage--real .mt-top { padding: 10px 12px 0; }
.stage--real .mpad-b { padding: 0 12px 12px; }
.stage--real .mpad-h { padding: 0 12px; }
.stage--real .mlab-s { flex: 1; color: var(--ink-500); font-size: 12.5px; }
.stage--real .mlab-v { color: var(--ink-500); font-size: 12.5px; }
.stage--real .mans-c { font-size: 13px; color: var(--ink-600); line-height: 1.7; }
.stage--real .mans-a { color: var(--ink-900); }
.stage--real .manl-c { font-size: 13px; color: var(--ink-700); line-height: 1.75; }
.stage--real .mdim { font-size: 13px; color: var(--ink-500); }
.stage--real .mcard--gap { margin: 12px 12px 0; }
.stage--real .mseg--gap { margin: 10px 12px 0; }

/* ==========================================================================
   主按钮渐变的例外：`.mbtn--ghost` / `--muted`
   --------------------------------------------------------------------------
   上面那条 `.mbtn { background: 渐变 }` 是**无条件**的，会把次要按钮
   （ghost）也染成蓝底 —— 而 ghost 的字色是 `--ios-blue`，
   结果就是**蓝字压蓝底**，按钮文字整条看不见（截图才发现）。
   同特异度下必须显式排除，不能靠"写在后面"（选择器里没有 .mbtn--ghost，
   匹配不上就轮不到它）。
   ========================================================================== */
.stage--real .mbtn--ghost,
.stage--real .mbtn--muted {
    background: rgba(255, 255, 255, .72);
    color: var(--ios-blue);
    box-shadow:
        inset 0 0 0 1px rgba(60, 60, 67, .16),
        0 2px 8px rgba(31, 86, 180, .08);
}
.stage--real .mbtn--ghost:active,
.stage--real .mbtn--muted:active {
    background: rgba(255, 255, 255, .92);
}

/* ==========================================================================
   2026-10-17 · 第 5 轮：表单弹窗的字段行 + 同行附属控件
   --------------------------------------------------------------------------
   为什么单独一块：上一块（把新增内联样式收成语义类）只收了学员端**页面**
   里的行内样式，`Mobile.form()` 自己的模板还在写 style=""。
   `check_inline_styles` 的基线只能往下走，所以先在这里立类再改模板。

   ★ 全部挂在 `.stage--real` 下：`pfield` 这一族在学员端根容器里，
     不加前缀会连管理端/机构端/卫健委端一起改。
   ========================================================================== */
.stage--real .pfield__item { margin-bottom: 10px; }
.stage--real .pfield__lab {
    font-size: 12.5px;
    color: var(--ink-600);
    margin: 8px 0 4px;
}
/* ★ 验证码输入框 + 「获取验证码」按钮**同一行**：
   input 用 flex:1 吃掉剩余宽度，按钮 flex:none 不被压扁。 */
.stage--real .pfield__row {
    display: flex;
    align-items: stretch;
    gap: 8px;
}
.stage--real .pfield__row .pinput { flex: 1 1 auto; min-width: 0; }
.stage--real .pfield__aside { flex: none; display: flex; }
.stage--real .pfield__aside .pbtn { white-space: nowrap; }

/* 必填星号（红色）。与下面的「选填」小灰标是**一对**：
   每个字段的标签上必居其一 —— 只标"选填"会让没标的字段性质不明。 */
.stage--real .pfield__req {
    margin-left: 2px;
    color: var(--p-err);
    font-style: normal;
    font-weight: 600;
}
/* 脚注里的同一个星号，颜色要一致。 */
.stage--real .mhint__req { color: var(--p-err); font-weight: 600; }

/* 选填标记（字段标签右侧的小灰标）。 */
.stage--real .pfield__tag {
    margin-left: 6px;
    padding: 0 6px;
    border-radius: 6px;
    background: var(--ink-100);
    color: var(--ink-500);
    font-size: 11px;
    font-weight: 500;
    line-height: 16px;
}

/* 区域选择行：外观是输入框，实则是按钮（整行可点开选择器）。
   ★ 必须是 flex：右侧要放一枚 chevron 提示"这里能点开"，
     纯 inline 布局下它会被挤到文字后面（长地址时换行错位）。 */
.stage--real .pinput--link {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    text-align: left;
    cursor: pointer;
    background: #fff;
    color: var(--ink-800);
}
.stage--real .pinput--link:active { background: var(--brand-50); }
.stage--real .pinput--link .pinput__x {
    flex: none;
    display: inline-flex;
    color: var(--ink-400);
}
/* 未选属地时那行字用占位色，与输入框的 placeholder 一致。 */
.stage--real .pinput--link.is-empty { color: var(--ink-400); }

/* 手机号行的「更换」按钮。 */
.stage--real .pfield__aside-btn {
    flex: none;
    padding: 0 14px;
    border: 0;
    border-radius: 10px;
    background: var(--brand-50);
    color: var(--brand-600);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
}
.stage--real .pfield__aside-btn:active { background: var(--brand-100); }

/* ==========================================================================
   hero 的公司名行 + 「不显示学时进度」开关
   --------------------------------------------------------------------------
   `.mhero__org` 复用 `.mhero__region` 的排版（图标 + 文字），
   但放在 region **上方**（需求：在地址上方显示公司名称）。
   ========================================================================== */
.stage--real .mhero__org {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-top: 10px;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--ink-700);
}

/* ==========================================================================
   「修改资料」按钮的容器 + 缺填值的红色值列
   --------------------------------------------------------------------------
   ★ 容器**不能叫 `.mact`** —— 那个类已经是"底部等宽 4 键操作条"
   （`.mact` / `.mact__b` / `.mact-space`），复用会把按钮挤进固定在
   底部的那一条里，页面立刻错位。名字取 `.mfoot-act`（footer action），
   与底部条那族在名字上就分得开。
   ========================================================================== */
.stage--real .mfoot-act {
    display: flex;
    padding: 0 12px 4px;
}
.stage--real .mfoot-act .mbtn { flex: 1 1 auto; }

/* 脚注说明（资料页底部的"保存后立即生效"一类）。
   为什么单独成类：`.mcard__b` 的 14px 内边距对一行小字太厚，
   而内联 `font-size:12px;color:…` 每加一处就推高一次基线。 */
.stage--real .mhint {
    padding: 10px 16px 2px;
    font-size: 12px;
    color: var(--ink-500);
    line-height: 1.65;
}
.stage--real .mhint b { color: var(--ink-700); font-weight: 600; }

.stage--real .mlist__v--warn { color: var(--p-err); }

/* ==========================================================================
   `.pfields`：表单弹窗的**字段区外层容器**
   --------------------------------------------------------------------------
   ★ 为什么不能复用 `.pfield`：它在 app.css:2954 是
     `display: flex` 的「标签 + 控件**同一行**」行容器（管理端表单在用）。
     `Mobile.form()` 原来把 N 个字段直接塞进 `.pfield`，于是它们成了
     flex 子项被**横排** —— 四个字段挤成每格 56px、输入框只剩 36px 宽。
     ★ 症状是纯视觉的：DOM 完全正常、字段顺序也对、所有静态断言全绿
       （`pfield__item` 一个不缺），只有截图能看出来（见 1018 截图）。

   名字只差一个 `s`，所以单独写一条守卫断「`form()` 用的是 `.pfields`」。
   ========================================================================== */
.stage--real .pfields { display: block; }

/* =====================================================================
   行分隔线：提高到可辨识程度（2026-10-17 第 4 轮，需求「列表页全部用分割线分开」）
   =====================================================================
   上面第 ④ 节把 `.mlist__i::after` 的背景压到了 `rgba(60,60,67,.16)`，
   理由是「玻璃上要更淡，否则玻璃感被一排黑线打散」。
   那个理由在**液态毛玻璃**下成立，但实测后果是**分割线几乎看不见** ——
   而分割线正是学员端区分「这些行是同属一组」的唯一视觉线索。
   需求明确要求用它来分隔，于是这里把它抬回可辨识的档位。

   ★ 为什么用 `--ios-sep`（.29）而不是再写一个 rgba：
     同一个视觉概念**只留一个真相源**。上一轮就是因为有人嫌太淡、
     直接在后面补了一条硬编码的 rgba，两者从此各说各话 ——
     改 `--ios-sep` 不会影响那条，写这条也不会影响全局分隔线。
     现在两者合成一处，改一处即可。

   ★ 仍然避开 .29 以上：.29 是 iOS grouped 的原生发丝线，
     再深就变成「边框」而不是「分隔线」，卡片会显得被切开。
   ===================================================================== */
.stage--real .mlist__i::after { background: var(--ios-sep); }
/* ======================================================================
   下拉角标（10-17 第 5 轮 · 学员端模拟考试「题量」）
   ----------------------------------------------------------------------
   为什么需要它：原生 `<select>` 在手机上只画一个系统三角，
   「这里能点开」完全靠学员猜。角标把它说破 —— 而且是**可点**的：
   页面里挂的是 `<label for="f-mock-count">`，点它等于点 select，
   走标准 HTML 行为，不自己实现「打开下拉」（那在部分 WebView 上
   会被静默忽略，症状是角标能点但什么也不发生）。

   ★ 它带**文字**（「下拉可改」）而不只是一枚箭头：可点性如果只用
     图形/位置/颜色表达，色觉障碍者与读屏用户读不出来。
     `Mobile.icon` 自带 `aria-hidden`，文字才是被读到的那个。
   ====================================================================== */
.stage--real .mpulldown {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    margin-left: 8px;
    padding: 2px 8px 2px 9px;
    border-radius: 999px;
    background: var(--ios-blue-fill);
    color: var(--ios-blue);
    font-size: 11.5px;
    font-weight: 500;
    line-height: 1.6;
    vertical-align: 1px;
    cursor: pointer;
    -webkit-user-select: none;
    user-select: none;
    /* 角标是给「这里能点」加的暗示，不是主操作 ——
       按下去要有反馈，否则点了没反应会被当成页面坏了。 */
    -webkit-tap-highlight-color: transparent;
    transition: opacity .12s ease;
}
.stage--real .mpulldown:active { opacity: .55; }
.stage--real .mpulldown__t { white-space: nowrap; }
.stage--real .mpulldown .mic { flex: none; }
/* 键盘可达：label 不可 focus，用 select 自身的 focus 环做落点提示 */
.stage--real .mpulldown:focus-within { outline: none; }
/* ======================================================================
   ① 列表分组标题行 `.mlist__h`（10-17 第 5 轮 · 报名页机构列表）
   ----------------------------------------------------------------------
   为什么不再用 `.mcard`：机构一多，「每家一张玻璃卡」会变成十几张卡
   摞在一起，每张卡上下都带圆角与阴影 —— 学员看到的是一堆卡片，
   而不是「一家机构下面有几个班次」。降成同一张列表里的一行标题后，
   机构与班次的层级由**一条线 + 一行标题**说清，不需要容器。

   ★ 分割线方向和 `.mlist__i::after` 相反：`.mlist__i` 的线画在行**下方**
     （`::after`），而这里要画在标题**上方** —— 上一组的最后一行与下一组的
     标题必须分开。标题不是 `.mlist__i`，拿不到那条 `::after`，自己画。
   ★ `:first-child` 不画线：每家机构一张 `.mlist`，第一行不需要「上面那条线」。
   ====================================================================== */
.stage--real .mlist__h {
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 13px 14px 7px;
    border-top: 0.5px solid var(--ios-sep);
    color: var(--ink-900);
}
.stage--real .mlist > .mlist__h:first-child,
.stage--real .mlist__h--first {
    border-top: 0;
    padding-top: 11px;
}
.stage--real .mlist__h-ico {
    display: inline-flex;
    flex: none;
    color: var(--ios-blue);
}
.stage--real .mlist__h-t {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 14.5px;
    font-weight: 600;
    line-height: 1.45;
    /* 机构名可能很长（「XX县XX镇XX职业技能培训中心」），不截断的话
       会把右边的「同区县」标签挤出屏幕。省略号 + title 兜底。 */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.stage--real .mlist__h .pitem__badge { flex: none; }

/* ======================================================================
   ② 步骤药丸三态（10-17 第 5 轮 · 报名页）
   ----------------------------------------------------------------------
   ★ 为什么必须在这里「重定义」而不是复用既有的 `.mqhead__tag--green`：
     那个类在 4880 行，特异度 `(0,1,0)`；基线 `.stage--real .mqhead__tag`
     在 6642 行，特异度 `(0,2,0)` 且位置更靠后 —— **绿底从来没生效过**，
     步骤药丸一直是灰的（需求暴露出来的既有 bug，不是需求本身）。
     这里三态全部在 `.stage--real` 下重定义，与基线同级、位置更靠后，
     靠「同级 + 更后」取胜，不靠 `!important`（那是掩盖而不是解决）。

   ★ `--done` 的底色沿用 `#12b76a`（与 `.mlist__ico--green` 同族），
     `--now` 用品牌蓝，`--todo` 是中性灰 —— 三态在色相上真的分得开，
     而不是「同色不同透明度」。
   ★ 白字：绿底/蓝底上的 `#fff` 对比度 ≥ 4.5:1，灰底改用 `--ink-600`
     （`#fff` 落在那个灰上只有 2:1，读不清）。
   ====================================================================== */
.stage--real .mqhead__tag--done {
    background: #12b76a;
    color: #fff;
}
.stage--real .mqhead__tag--now {
    background: var(--brand-600);
    color: #fff;
}
.stage--real .mqhead__tag--todo {
    background: rgba(118, 118, 128, .16);
    color: var(--ink-600);
}
/* 分组标题的**下边线**：机构名与它下面的班次行之间要有分隔。
   ★ 为什么需要它：`.mlist__i::after` 的线画在行**下方**，而标题
     不是 `.mlist__i` —— 于是「机构名」和「第一个班次」之间正好
     落在两者的空隙里，一条线都没有。截图实证：不加这条时
     机构名与第一个班次视觉上糊成一块，读不出「这家有几个班次」。
   ★ 只给**每组的最后一条线**让位：`.mlist__i:last-child::after`
     本来就 `display:none`，所以组内最后一个班次下面不会有双线。 */
.stage--real .mlist__h {
    padding-bottom: 9px;
    margin-bottom: 2px;
    border-bottom: 0.5px solid var(--ios-sep);
}
/* ======================================================================
   显式下拉框 `.msel`（10-18 第 1 轮 · 模拟考试「题量」）
   ----------------------------------------------------------------------
   需求：「需要让人知道他是个下拉框」。
   原来只有原生系统三角 —— 安卓 WebView 上又小又是系统灰，
   在浅色玻璃卡上几乎看不见，学员看到的是一个「装着数字的方框」。

   结构：`<span class="msel">` 里是 `<select class="msel__in">`
   （铺满整框）+ 一个 `aria-hidden` 的箭头装饰。

   ★ 竖分隔线：这是 UI 里「这块区域和右边不是一回事」的最强信号 ——
     比单个箭头更容易被注意到，而且不依赖颜色（灰线在深色底上也能画）。
     用 `linear-gradient` 画，是因为 `<select>` 是 replaced 元素、
     **没有 `::after`**（`::before/::after` 在 replaced 元素上不渲染），
     而包一层 `<span>` 去画分隔线就得让 select 透明，多一层 DOM。
   ====================================================================== */

/* ======================================================================
   纵向字段区 `.pfield--stack`（10-18 第 1 轮 · 模拟考试「题量」）
   ----------------------------------------------------------------------
   `.pfield` 的默认形态是 `display:flex` 的**行**容器（app.css:2954，
   带 `gap:12px`）—— 「标签 + 控件」挤在一行。控件能分到的宽度 = 容器宽
   减去标签的固有宽度再除掉 gap，短控件（`<select>` 内在宽只有 48px）
   于是几乎不增长，表现为「框窄得只放得下两个字」。

   纵向排布（标签在上、控件在下）是移动端表单的惯例，同时让控件
   拿满整行宽度。★ 这里必须**显式声明 `flex-direction:column`**：
   记忆里的教训是「同名不同容器最隐蔽」—— 光靠 `display:block`
   改不掉 flex，必须改 `flex-direction`。
   ====================================================================== */
.stage--real .pfield--stack {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
}
.stage--real .pfield--stack .pfield__label {
    margin-bottom: 0;
}

.stage--real .msel {
    position: relative;
    /* ★ 宽度塌成 48px 的真正原因（实测）：
       `.pfield` 是 `display:flex` 的**行容器**（app.css:2954，
       带 `gap:12px`），子项默认 `flex: 0 1 auto`。
       写 `flex:1 1 0%` 时基准是 0，而 `<select>` 是 replaced 元素、
       内在宽度只有 48px → 整个框只剩 48px（箭头被挤到框外）。
       ★ 这正是记忆里那条「复用类名前先查它在 CSS 里的 display」：
         `.pfield` 看着像纵向表单块，实际是横向 flex 行。 */
    display: flex;
    /* `grow` 要比 label 大得多：label 是 fixed 内容的行，
       剩下的宽度理应全给下拉框。 */
    flex: 8 1 auto;
    width: auto;
    min-width: 0;
}
/* ★ 选择器必须是 `select.msel__in`（`(0,2,2)`）而不是 `.msel__in`
   （`(0,2,0)`）—— app.css:7285 的
   `.stage--real … select.pinput { background: rgba(255,255,255,.92) }`
   是 `(0,2,1)`，**特异度更高**。`background` 是**简写**，它会把
   `background-image` 重置成 `initial` —— 于是箭头和分隔线被无声抹掉，
   而 `appearance:none` 却生效了：实测 `background-image` 算出来是 `none`，
   框里空空如也。这是「样式写了但被更早的简写吃掉」的典型，
   静态看代码完全正常，只有截图 / 实算值能抓到。 */
.stage--real select.msel__in {
    width: 100%;
    /* 原生 select 的选项文字默认**右对齐**（跟随值而非标签），
       在中文界面里读起来像「输入框里回显了一个值」。
       改成左对齐，和下面的说明文字、其它表单控件对齐。 */
    text-align: left;
    /* ★ 只在这一处去掉原生外观。全局 `select.pinput` 不动 ——
       管理端/机构端的下拉维持系统原生箭头。 */
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    /* 右侧留出「分隔线 + 箭头」的位置：28px 只有一点余量，
       实测 16px 箭头 + 12px 边距刚好，再多就会压到「50 题」的字。 */
    padding-right: 34px;
    border: 1px solid var(--line);
    /* 沿用玻璃卡的半透白而非纯白 —— 这张卡本身是玻璃的，
       纯白会像贴上去的一块。 */
    background-color: rgba(255, 255, 255, .92);
    color: var(--ink-900);
    font-weight: 500;
    cursor: pointer;
    /* ★ 竖分隔线用 background 的第一层画（replaced 元素没有 ::after），
       箭头像第二层。两层 `no-repeat` + 各自的 `position/size`。 */
    background-image:
        linear-gradient(var(--ios-sep), var(--ios-sep)),
        url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23007aff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 9.5 7 7 7-7'/%3E%3C/svg%3E");
    background-repeat: no-repeat, no-repeat;
    /* 分隔线：1px 宽、距右 30px、上下各留 7px（不贴边，像一列按钮） */
    background-position: right 30px center, right 10px center;
    background-size: 1px 22px, 16px 16px;
}
/* 点下去要有按压反馈：没有反馈时「点了没反应」会被当成页面坏了 */
.stage--real select.msel__in:active { background-color: var(--ios-blue-fill); }
.stage--real select.msel__in:focus-visible {
    outline: 2px solid var(--ios-blue);
    outline-offset: 1px;
}
/* 说明文字：图形之外必须有文字（读屏 / 色觉障碍 / 灰度打印） */
.stage--real .msel-hint {
    /* ★ 必须换行到独立一行：`.pfield` 是 flex **行**容器，
       说明文字若作为它的直接子项，会和「题量」「下拉框」挤在同一行，
       宽度被压到 ~80px 后**竖排折成三行**（截图实证）。
       `flex-basis:100%` 让它先占满一整行宽度，从而被换行。 */
    flex: 0 0 100%;
    display: block;
    margin-top: 6px;
    font-size: 12px;
    line-height: 1.5;
    color: var(--ink-500);
}

/* ---------- 管理端章节弹窗里的「当前视频」预览（2026-10-18 第 6 轮） ----------
   需求是「已经存在的视频直接展示出来，窗口小一点即可」，所以：
   · 宽度钉死在 320px（≈9:16 竖屏视频的一半宽），横屏也不会撑满弹窗；
   · `aspect-ratio: 16/9` 给一个占位高度，**必须在 `src` 生效前就有** ——
     视频元数据到达前 `<video>` 的默认尺寸是 300×150，放完再补会跳一下。
   · 圆角 + 灰底：黑底视频贴在白底弹窗里像个洞。
   为什么用 `<video>` 而不是 `<iframe>`：学员端走的是 H5 播放器，
   管理端要的是「一眼确认是不是这一版」，原生的`controls` 够用，
   而 iframe 会引入一份没法跟随主题的第二套 UI。 */
.lprev {
    width: 320px;
    max-width: 100%;
    margin: 0 0 8px;
    border-radius: var(--r-md, 8px);
    overflow: hidden;
    background: var(--ink-950, #0b1220);
    line-height: 0;
}
.lprev__v {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    background: var(--ink-950, #0b1220);
}

/* ---------- 学员端状态三色（2026-10-18 第 7 轮） ----------
   需求给的配色原文：「已完成的用绿色显示、进行中用
   `background: rgb(111 100 71 / 15%); color: #df8a06;` 这个颜色、
   未完成那种不合格的用红色」。

   ★ 为什么**新增 `--pay` 而不直接改 `--warn`**
     ---------------------------------------------
     `.stage--real .pitem__badge--warn` 的实算值是
     `rgba(255,159,10,.16)` + `#a75e00`，**不是**需求要的那一套。
     直接改它会把所有用 `--warn` 的语义（待审核、需先取消当前班、
     未开放……）一起染成土黄 —— 那些场景与「进行中」无关。
     需求给的是一个**具体场景**（进行中/待缴费）的颜色，不是
     「所有警示色都改成这个」。所以新增一个变体，只给这一类用。

   ★ 挂在 `.stage--real` 下
     ----------------------
     学员端的根容器是 `.stage--real`（`mobile.js` 建的）。不加前缀会
     连管理端/机构端一起改 —— 而那些页面用的正是另一套 `--warn`。

   ★ 位置在文件末尾 = 同特异度、写在基线之后
     ------------------------------------------
     `.stage--real .pitem__badge--pay` 是 `(0,2,0)`，
     与基线 `.stage--real .pitem__badge--warn` 同级；同特异度下
     **后写的赢**，所以不需要 `!important`（加了反而掩盖问题）。
     守卫判据用**实算色**（见 `tools/check_status_colors.js`），
     不用「样式里写了这句话」—— 后者对覆盖完全无感。 */
.stage--real .pitem__badge--pay {
    background: rgb(111 100 71 / 15%);
    color: #df8a06;
}
/* 圆点图标同色系：`.mlist__ico--orange` 用的是旧橙，
   与徽章并排时两个橙不一样会显得像两个状态。 */
.stage--real .mlist__ico--pay {
    background: rgb(111 100 71 / 15%);
    color: #df8a06;
}

/* ==========================================================================
   排班日历按档位分色（10-18 第 10 轮 · 开班申请「初训复训分开」）
   ----------------------------------------------------------------------
   查重已经按 `initial` / `retrain` 分开（`approval.ensure_schedule_slot_free`
   的 `TrainingClass.kind == kind`），所以同一时段可以同时有一个初训班与
   一个复训班。若日历横条还是**同一个颜色**，机构看到一条占用就会以为
   「这段时间全满了」，而他要开的另一个档位其实空着 —— 页面在骗人。

   ★ 选择器必须带 `.cal__bar`：基线 `.cal__bar`（app.css:5311）是 `(0,1,0)`，
     这里写 `.cal__bar.cal__bar--initial`（`(0,2,0)`）且位置更靠后，稳赢；
     写成 `.cal__bar--initial`（`(0,1,0)`）虽然**位置在后也能赢**
     （同特异度后来者胜），但两处规则挨着、顺序一换就静默失效 ——
     与 MEMORY 里「修饰类被基线按特异度压死」那条同源。
   ★ `background` 用**简写**：这里要覆盖的是基线的整条 background
     （`--p-blue-100` 底 + `border-left` 蓝边），用 `background-color`
     只改底色而把 `border-left-color` 留成蓝色，会得到「橙底蓝边」。
     所以底色与左边框**同一条规则里都写**。
   ★ 颜色不是唯一线索：横条与清单里都有**文字**小标「初训 / 复训」
     （`calendar.js` 的 `cal__bar-kind` / `cal__item-kind`），
     灰度打印与色觉障碍下靠它区分。
   ★ 不用 `.stage--real` 前缀：日历是**三端共用**组件
     （机构端 / 管理端 class-review / 行政端 confirm-class），
     加了前缀反而只有学员端能上色。
   ====================================================================== */
.cal__bar.cal__bar--initial,
.cal__item-dot.cal__bar--initial,
.cal__legend-box.cal__bar--initial {
    background: var(--p-blue-100);
    border-left-color: var(--p-blue);
    border-color: var(--p-blue);
}
.cal__bar.cal__bar--retrain,
.cal__item-dot.cal__bar--retrain,
.cal__legend-box.cal__bar--retrain {
    /* 复训用琥珀色（--p-warn 一族），与初训的蓝在色相上真的分得开，
       而不是「同色不同透明度」—— 后者在日历上一眼分不出是两个班。 */
    background: var(--p-warn-bg);
    border-left-color: var(--p-warn);
    border-color: var(--p-warn);
}
/* 档位小标：圆角小胶囊，跟着横条底色走 */
.cal__bar-kind {
    flex: none;
    padding: 0 4px;
    border-radius: 3px;
    font-size: 10.5px;
    font-weight: 600;
    line-height: 15px;
    color: #fff;
    background: var(--p-blue);
}
.cal__bar--retrain .cal__bar-kind { background: var(--p-warn); }
.cal__item-kind {
    flex: none;
    padding: 0 4px;
    border-radius: 3px;
    font-size: 11px;
    font-weight: 600;
    line-height: 16px;
    color: #fff;
    background: var(--p-blue);
}
.cal__item-dot + .cal__item-kind { margin-right: 2px; }
/* ★ 清单里的圆点不再承担"档位"信息（旁边已有文字小标），
   统一收敛成 6px 实心圆，颜色跟档位走 —— 22px 的大圆点加一行字
   视觉上太重，会把注意力从「谁占了哪几天」拉走。 */
.cal__item-dot { width: 6px; height: 6px; }
.cal__legend-box { margin-left: 6px; }

/* 冲突提示（`apply-class.html` 的 #slot-hint）：占了就是红色，
   不是只在提交失败后才 toast —— 用户不该"填完一屏点提交"才知道。 */
.field__hint--err {
    color: var(--p-err);
    font-weight: 500;
}

/* 身份证号匹配的结果提示（授权补考弹窗，10-31 第 42 轮）。
   ★ 用**成功态**而不只是错误态：匹配到人时要说清"匹配上了、而且他在这场
     里到过场" —— 这是管理员决定要不要点「授权」的唯一依据，
     只给一个灰字等于让他自己猜。 */
.field__hint--ok {
    color: var(--p-ok);
    font-weight: 500;
}

/* ==========================================================================
   驾驶舱 · 深色数据大屏（统计报表专用，10-25 第 38 轮）
   --------------------------------------------------------------------------
   只作用在 `.cockpit` 容器内 —— 侧栏、顶栏、其它页面一个字都不变。

   ★ 换肤靠**覆盖 CSS 变量**，不是逐条重写选择器：
     `.card` / `.stat` / `.table` / 图表全部取色于 --surface / --text / --line
     与 --c1..--c6，改这二十几个值就等于整体换肤（`chart.js` 用的是同一批
     --c1..--c6，所以图也跟着变）。
     反过来做（深色作用域里一条条写 background/color）必然漏，而漏掉的那条
     **不报错**，只是那块区域在深底上仍是一张白卡片。

   ★ 刻意没有做的：不做全局深色主题、不加 `.dark` 类到 body。
     报表页要的是"一眼看完"，其它页面要的是"能长时间填表"，
     两种需求的对比度取向相反，混成一个主题两头都不到位。
   ========================================================================== */

/* 这一页不要 1400px 的阅读上限：驾驶舱就是要把屏用满 */
.content--cockpit {
    max-width: none;
    padding: 0;
}

.cockpit {
    --bg: #070d19;
    --surface: #0f1e37;
    --surface-1: #0f1e37;
    --surface-2: #17284a;
    --text: #e9eefb;
    --text-2: #a3b4d0;
    --text-3: #7d8eac;
    --line: rgba(122, 162, 224, .18);
    --line-2: rgba(122, 162, 224, .10);
    /* --ink-* 全站 200 多处引用，深色下就地反向；不改名字（改名字要动 200 处） */
    --ink-50: #0b1526;
    --ink-100: #101c31;
    --ink-200: #17253d;
    --ink-300: #223350;
    --ink-400: #4a5d7d;
    --ink-500: #6c7f9e;
    --ink-600: #93a5c2;
    --ink-700: #b6c4da;
    --ink-800: #d3ddec;
    --ink-900: #e9eefb;
    --ink-950: #f5f8ff;
    /* 政务蓝在深底上要提亮，否则"主操作"和"禁用"分不出来 */
    --brand-50: rgba(96, 165, 250, .14);
    --brand-100: rgba(96, 165, 250, .26);
    --brand-500: #60a5fa;
    --brand-600: #4b93ec;
    --brand-700: #7db8ff;
    --brand-800: #3b82f6;
    --brand-900: #2f6fdc;
    --signal-500: #60a5fa;
    --signal-600: #4b93ec;
    --signal-700: #7db8ff;
    --signal-800: #3b82f6;
    /* 图表色序列：同一批色相，深底上提饱和 + 提亮 */
    --c1: #4a9eff;
    --c2: #34d399;
    --c3: #fbbf24;
    --c4: #f87171;
    --c5: #a78bfa;
    --c6: #7c8aa5;
    --c1-soft: rgba(74, 158, 255, .16);
    --c2-soft: rgba(52, 211, 153, .16);
    --c3-soft: rgba(251, 191, 36, .16);
    --c4-soft: rgba(248, 113, 113, .16);
    --success: #34d399;
    --success-bg: rgba(52, 211, 153, .15);
    --warning: #fbbf24;
    --warning-bg: rgba(251, 191, 36, .13);
    --danger: #f87171;
    --danger-bg: rgba(248, 113, 113, .15);
    --info: #60a5fa;
    --info-bg: rgba(96, 165, 250, .15);
    --shadow-sm: 0 1px 0 rgba(148, 197, 255, .05), 0 6px 18px rgba(0, 0, 0, .30);
    --shadow: 0 10px 30px rgba(0, 0, 0, .45);

    position: relative;
    min-height: calc(100vh - var(--topbar-h));
    padding: var(--sp-5) var(--sp-5) var(--sp-6);
    color: var(--text);
    /* 两团极淡的光晕给深底一点纵深；纯 #070d19 平铺会显得像"深色模式没做完" */
    background:
        radial-gradient(1100px 460px at 8% -10%, rgba(56, 132, 255, .22), transparent 70%),
        radial-gradient(880px 420px at 96% 2%, rgba(52, 211, 153, .10), transparent 72%),
        var(--bg);
}

/* ---------------- 头部：标题 + 当前范围 + 数据源 ---------------- */

.cp-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--sp-4);
    flex-wrap: wrap;
    margin-bottom: var(--sp-4);
}

.cp-title {
    margin: 0;
    font-size: 21px;
    font-weight: 700;
    letter-spacing: .6px;
    color: var(--text);
}

.cp-title::before {
    /* 左侧一道 3px 亮蓝：驾驶舱的"仪表上电"提示，比图标省一层 DOM */
    content: '';
    display: inline-block;
    width: 3px;
    height: 16px;
    margin-right: 10px;
    vertical-align: -2px;
    border-radius: 2px;
    background: linear-gradient(180deg, var(--c1), rgba(74, 158, 255, .2));
}

.cp-sub {
    margin-top: 6px;
    font-size: 12.5px;
    color: var(--text-3);
    letter-spacing: .2px;
}

.cp-sub b { color: var(--text-2); font-weight: 600; }

/* 右上角"数据源在线"指示：不写死时间，只说明数据从哪来 ——
   写一个假的"最后更新 12:00"比不写更糟。 */
.cp-live {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 6px 12px;
    font-size: 12px;
    color: var(--text-2);
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--surface);
    white-space: nowrap;
}

.cp-live::before {
    content: '';
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--c2);
    box-shadow: 0 0 0 3px rgba(52, 211, 153, .18);
}

/* ---------------- 分区标题（把两套口径切成两屏） ---------------- */

.cp-divider {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    margin: var(--sp-5) 0 var(--sp-3);
    font-size: 13px;
    font-weight: 600;
    letter-spacing: .6px;
    color: var(--text-2);
}

.cp-divider::after {
    content: '';
    flex: 1 1 auto;
    height: 1px;
    background: linear-gradient(90deg, var(--line), transparent);
}

.cp-divider span {
    display: inline-flex;
    align-items: baseline;
    gap: 8px;
}

.cp-divider em {
    font-style: normal;
    font-weight: 400;
    font-size: 12px;
    color: var(--text-3);
}

/* ---------------- 深色下的对照组 ----------------
   只兜住"写了固定浅色值"的那几处；其余全靠变量。 */

.cockpit .notice,
.cockpit details.notice {
    background: var(--surface-2);
    border-color: var(--line);
}

.cockpit .notice--todo {
    border-left-color: var(--c3);
    border-color: var(--line);
    background: rgba(251, 191, 36, .08);
}

.cockpit .notice--off {
    background: var(--surface-2);
}

.cockpit .stat,
.cockpit .card {
    box-shadow: none;
}

/* 数字放大：驾驶舱第一眼要看到数，不是看到"卡片" */
.cockpit .stat__value {
    font-size: 32px;
}

.cockpit .stat__label {
    font-size: 12.5px;
    color: var(--text-3);
    letter-spacing: .3px;
}

.cockpit .stat:hover,
.cockpit .card:hover {
    /* 大屏上每块都在动反而像坏了；hover 只留边框提亮 */
    transform: none;
    box-shadow: none;
    border-color: rgba(122, 162, 224, .34);
}

/* 指标卡：近五张自动铺满一行，不出现"第 6 张孤零零换行" */
.cockpit .stat-grid {
    grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
    gap: var(--sp-3);
}

.cockpit .card__head h3 {
    font-size: 14px;
    letter-spacing: .3px;
}

.cockpit .hint,
.cockpit .field__hint {
    color: var(--text-3);
}

/* 表格在深底上要能扫行：表头压暗、隔行给极淡的底色 */
.cockpit .table thead th {
    background: var(--surface-2);
    color: var(--text-3);
}

.cockpit .table tbody tr:nth-child(even) {
    background: rgba(122, 162, 224, .04);
}

/* 折叠条的 summary 在深色下不能是浏览器默认的黑色小三角 */
.cockpit summary {
    cursor: pointer;
    color: var(--text-2);
}

.cockpit summary::marker {
    color: var(--text-3);
}

/* ★ 试过让顶栏也跟着变深（`:has()` + 覆盖 --text/--ink）：
   面包屑与「退出」在深底上几乎读不出来 —— 顶栏内部的取色除了 --text
   还散落着 --ink-*、--surface-* 好几套，覆盖不全会得到"半深半浅"的顶栏，
   而它**不会报错**，只是字变暗。
   结论：白顶栏 + 深内容区。这不是妥协 —— 仪表盘的白标题栏本来就是常态，
   而且顶栏要长期可读，内容区只要一眼看清。 */

/* 指标卡：顶部一道亮蓝光条 + 极淡的自上而下提亮 ——
   深底上纯色卡片会"陷"进背景，光条是驾驶舱里"这块有读数"的通用语汇。
   前缀 `.cockpit` 保证不影响管理端那两个报表页。 */
.cockpit .stat {
    background:
        linear-gradient(180deg, rgba(74, 158, 255, .07), rgba(74, 158, 255, 0) 58%),
        var(--surface);
}

.cockpit .stat::before {
    content: '';
    position: absolute;
    left: 14px;
    right: 14px;
    top: 0;
    height: 2px;
    border-radius: 0 0 2px 2px;
    background: linear-gradient(90deg, var(--c1), rgba(74, 158, 255, 0) 78%);
    opacity: .85;
}

/* 通过率那张卡是"结论"，给它语义绿；其余保持中性蓝 */
.cockpit .stat__value[data-tone="ok"] {
    color: var(--c2);
}

/* ==========================================================================
   驾驶舱 · 多维面板布局（10-25 第 39 轮，按参考大屏重构）
   --------------------------------------------------------------------------
   参考图的结构是「**三列面板网格 + 底部指标条**」，每块面板 = 标题条 + 多维内容
   （紧凑表格 / 大数字 / 横条 / 环图），而不是一排大卡片。

   只在这一页用：全部挂在 `.cockpit` 下。类名前缀 `panel` / `kv` / `mini`，
   与既有 `.card` / `.stat` 不冲突（那两个还有 40 多个页面在用）。
   ========================================================================== */

/* ---------------- 顶部：标题 + 筛选 ---------------- */

.dash-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-4);
    flex-wrap: wrap;
    padding-bottom: var(--sp-3);
    margin-bottom: var(--sp-3);
    border-bottom: 1px solid var(--line);
}

.dash-brand {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
}

/* 左上角那个小方块：参考图里是品牌 mark，这里当"仪表上电"的指示 */
.dash-brand::before {
    content: '';
    flex: 0 0 auto;
    width: 10px;
    height: 18px;
    border-radius: 2px;
    background: linear-gradient(180deg, var(--c1), rgba(74, 158, 255, .15));
}

.dash-brand h1 {
    margin: 0;
    font-size: 19px;
    font-weight: 700;
    letter-spacing: .8px;
    color: var(--text);
    white-space: nowrap;
}

.dash-brand__sub {
    font-size: 12px;
    color: var(--text-3);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dash-brand__sub b { color: var(--text-2); font-weight: 600; }

.dash-filters {
    display: flex;
    align-items: flex-end;
    gap: 10px;
    flex-wrap: wrap;
}

.dash-filters .field { margin: 0; }

.dash-filters label {
    font-size: 11.5px;
    color: var(--text-3);
    letter-spacing: .3px;
}

.dash-filters select,
.dash-filters input[type="date"] {
    height: 30px;
    font-size: 12.5px;
    padding: 0 8px;
    min-width: 96px;
    background: var(--surface-2);
    color: var(--text);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
}

.dash-filters select:focus,
.dash-filters input:focus {
    outline: 2px solid rgba(74, 158, 255, .45);
    outline-offset: 1px;
}

/* ---------------- 三列面板网格 ---------------- */

.dash-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 12px;
}

@media (max-width: 1180px) {
    .dash-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .panel--wide2 { grid-column: 1 / -1; }
}

@media (max-width: 820px) {
    .dash-grid { grid-template-columns: minmax(0, 1fr); }
    .panel--wide2, .panel--full { grid-column: auto; }
}

.panel--wide2 { grid-column: span 2; }
.panel--full { grid-column: 1 / -1; }

.panel {
    display: flex;
    flex-direction: column;
    min-width: 0;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r);
    overflow: hidden;
}

.panel__hd {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 9px 12px;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: .4px;
    color: var(--text);
    border-bottom: 1px solid var(--line-2);
    background: linear-gradient(90deg, rgba(74, 158, 255, .10), rgba(74, 158, 255, 0) 62%);
}

/* 标题左侧竖条：参考图每块面板都有，是"这一块在读数"的最小标记 */
.panel__hd::before {
    content: '';
    width: 2px;
    height: 12px;
    border-radius: 1px;
    background: var(--c1);
}

.panel__hd em {
    margin-left: auto;
    font-style: normal;
    font-weight: 400;
    font-size: 11px;
    color: var(--text-3);
    letter-spacing: 0;
}

.panel__bd {
    flex: 1 1 auto;
    min-height: 0;
    padding: 12px;
}

.panel__bd--flush { padding: 0; }

.panel__empty {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 96px;
    color: var(--text-3);
    font-size: 12.5px;
}

/* ---------------- 大数字三连（参考图左上那块） ---------------- */

.kv-row {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 8px;
    margin-bottom: 10px;
}

.kv {
    min-width: 0;
    padding: 8px 10px;
    border-radius: var(--r-sm);
    background: rgba(74, 158, 255, .07);
    border: 1px solid var(--line-2);
}

.kv span {
    display: block;
    font-size: 11px;
    color: var(--text-3);
    letter-spacing: .2px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.kv b {
    display: block;
    margin-top: 3px;
    font-size: 22px;
    font-weight: 700;
    line-height: 1.15;
    letter-spacing: -.6px;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}

.kv b.is-ok { color: var(--c2); }
.kv b.is-warn { color: var(--c3); }
.kv b.is-bad { color: var(--c4); }

/* ---------------- 紧凑多维表格（参考图的主要语汇） ---------------- */

.mini {
    width: 100%;
    border-collapse: collapse;
    font-size: 12.5px;
    font-variant-numeric: tabular-nums;
}

.mini th {
    padding: 5px 8px;
    font-size: 11px;
    font-weight: 500;
    color: var(--text-3);
    text-align: right;
    border-bottom: 1px solid var(--line);
    white-space: nowrap;
}

.mini th:first-child,
.mini td:first-child {
    text-align: left;
}

.mini td {
    padding: 6px 8px;
    text-align: right;
    color: var(--text-2);
    border-bottom: 1px solid var(--line-2);
    white-space: nowrap;
}

.mini tbody tr:last-child td { border-bottom: 0; }

.mini td:first-child {
    color: var(--text);
    font-weight: 500;
    /* ★ 不能用 `max-width: 0`（那是"自动省略"的经典写法，但它只在
       `table-layout: fixed` 且给了宽度时才成立）；这里表格是 auto 布局，
       写 0 会把整列压没 —— 症状是第一列只剩「去…」「未…」。 */
    max-width: 220px;
    overflow: hidden;
    text-overflow: ellipsis;
}

.mini tbody tr:hover td { background: rgba(74, 158, 255, .06); }

/* 通过率这种"越高越好"的列：给数值直接上色，比再加一根进度条省空间 */
.mini .t-ok { color: var(--c2); font-weight: 600; }
.mini .t-warn { color: var(--c3); font-weight: 600; }
.mini .t-bad { color: var(--c4); font-weight: 600; }
.mini .t-mute { color: var(--text-3); }

/* 表格区限高 + 内部滚动：面板高度对齐，不让某一块把整行撑高 */
.mini-scroll {
    max-height: 208px;
    overflow: auto;
}

/* ---------------- 横条列表（属地覆盖 / 工种） ---------------- */

.bars { display: flex; flex-direction: column; gap: 7px; }

.bars__row {
    display: grid;
    /* 名称 | 条形 | 人数 | 占比 —— 占比那列是参考图里"86.3%"的位置：
       只有绝对数时，"236 人多到什么程度"要靠目测条长，加了占比就不用。 */
    grid-template-columns: minmax(64px, 104px) minmax(0, 1fr) 48px 52px;
    align-items: center;
    gap: 8px;
    font-size: 12px;
}

.bars__name {
    color: var(--text-2);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.bars__track {
    /* ★ 这两个都必须是 block：`<span>` 默认 inline，width/height 都不生效 ——
       表现是"条只剩一条灰色空槽、没有填充"，而且不报错。 */
    display: block;
    height: 8px;
    border-radius: 4px;
    background: rgba(122, 162, 224, .12);
    overflow: hidden;
}

.bars__fill {
    display: block;
    height: 100%;
    border-radius: 4px;
    background: linear-gradient(90deg, rgba(74, 158, 255, .95), rgba(74, 158, 255, .55));
}

.bars__fill--mute {
    background: linear-gradient(90deg, rgba(124, 138, 165, .8), rgba(124, 138, 165, .35));
}

.bars__val {
    text-align: right;
    color: var(--text);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.bars__pct {
    text-align: right;
    color: var(--text-3);
    font-variant-numeric: tabular-nums;
}

/* ---------------- 环图 + 图例 ---------------- */

.donut-wrap {
    display: flex;
    align-items: center;
    gap: 14px;
}

.donut-wrap > :first-child { flex: 0 0 auto; }

.legend {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin: 0;
    padding: 0;
    list-style: none;
    min-width: 0;
}

.legend li {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    color: var(--text-2);
    white-space: nowrap;
}

.legend i {
    width: 8px;
    height: 8px;
    border-radius: 2px;
    flex: 0 0 auto;
}

.legend b {
    color: var(--text);
    font-variant-numeric: tabular-nums;
}

/* ---------------- 底部指标条（参考图最下面那一条） ---------------- */

.dash-strip {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
    gap: 1px;
    margin-top: 12px;
    background: var(--line);
    border: 1px solid var(--line);
    border-radius: var(--r);
    overflow: hidden;
}

.dash-strip .item {
    padding: 10px 12px;
    background: var(--surface);
}

.dash-strip .item span {
    display: block;
    font-size: 11px;
    color: var(--text-3);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dash-strip .item b {
    display: block;
    margin-top: 2px;
    font-size: 19px;
    font-weight: 700;
    letter-spacing: -.4px;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}

.dash-strip .item b.is-zero { color: var(--text-3); font-weight: 500; }

/* ---------------- 页脚口径 ---------------- */

.dash-notes {
    margin-top: 12px;
    padding: 9px 12px;
    font-size: 12px;
    color: var(--text-2);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r);
}

.dash-notes summary {
    cursor: pointer;
    color: var(--text-3);
    font-size: 12px;
}

.dash-notes ul {
    margin: 8px 0 0;
    padding-left: 1.2em;
    color: var(--text-2);
}

.dash-notes li { margin-bottom: 3px; }

/* ---------------- 常州属地地图（点区切换） ---------------- */

.map-wrap {
    /* ★ 地图**占满宽**，详情卡绝对定位浮在右上角（参考图就是这么做的）。
       第一版把详情卡放成兄弟列（1fr + 132px），地图只剩 240px 宽 ——
       常州六个区里天宁、钟楼挤成一条缝，边界根本看不清。 */
    position: relative;
    padding: 6px 8px 0;
}

.cz-map {
    display: block;
    width: 100%;
    height: auto;
    max-height: 250px;
}

/* `vector-effect: non-scaling-stroke` 让描边不随 viewBox 缩放而变粗 ——
   否则小屏上区界会糊成一片、大屏上又细得看不见。 */
.cz-area {
    stroke: rgba(122, 162, 224, .30);
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
    cursor: pointer;
    transition: fill .15s ease, stroke .15s ease;
}

.cz-area:hover { stroke: #9ec6ff; }

.cz-area.is-on {
    stroke: #9ec6ff;
    stroke-width: 2;
}

.cz-label {
    fill: rgba(233, 238, 251, .72);
    /* ★ 字号是 **viewBox 单位**，会被缩放：写 10.5 时在 380px 宽的图上
       只剩 7px。这里按 560 的 viewBox 给 20，实际约 13px。
       `paint-order: stroke` + 深色描边 —— 文字压在任何深浅的色块上都读得清。 */
    font-size: 20px;
    text-anchor: middle;
    paint-order: stroke;
    stroke: rgba(7, 13, 25, .72);
    stroke-width: 4px;
    stroke-linejoin: round;
    pointer-events: none;
    user-select: none;
}

.cz-label.is-on {
    fill: #ffffff;
    font-weight: 700;
    stroke: rgba(7, 13, 25, .85);
}

.map-detail {
    position: absolute;
    /* ★ 放**左下**而不是右上：右上正好压住新北区（常州北大门，数据量大），
       压住它等于把最该看的那块盖掉了。左下是溧阳市，面积最大、
       被压掉一块不影响辨认。 */
    left: 10px;
    bottom: 8px;
    width: 118px;
    padding: 8px 10px;
    border-radius: var(--r-sm);
    /* 半透明 + 模糊：压在图上但不挡死下面的区界 */
    background: rgba(7, 13, 25, .82);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
    border: 1px solid rgba(122, 162, 224, .28);
    box-shadow: 0 6px 18px rgba(0, 0, 0, .35);
}

.map-detail .md-name {
    font-size: 13px;
    font-weight: 700;
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.map-detail .md-kv {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 6px;
    margin-top: 6px;
    font-size: 11.5px;
    color: var(--text-3);
    white-space: nowrap;
}

.map-detail .md-kv b {
    color: var(--text);
    font-size: 13px;
    font-variant-numeric: tabular-nums;
}

.map-detail .md-bar {
    height: 5px;
    margin-top: 7px;
    border-radius: 3px;
    background: rgba(122, 162, 224, .14);
    overflow: hidden;
}

.map-detail .md-bar i {
    display: block;
    height: 100%;
    border-radius: 3px;
    background: linear-gradient(90deg, #4a9eff, rgba(74, 158, 255, .45));
}

.map-foot {
    padding: 0 10px 9px;
    font-size: 11px;
    line-height: 1.5;
    color: var(--text-3);
}

.map-foot b { color: var(--text-2); font-variant-numeric: tabular-nums; }

/* 面板标题图标：复用 shell.js 的 ICONS（不引图标库、不写第二份 path） */
.panel__ico {
    display: inline-flex;
    align-items: center;
    color: rgba(122, 162, 224, .85);
}

.panel__ico svg {
    width: 13px;
    height: 13px;
}

/* ---------------- 区级：街道 / 乡镇的树图（面积 = 人数） ----------------
   为什么不是地理边界图：公开的行政区划边界只到区县一级，街道级没有
   合规来源。树图不假装地理位置，只表达"谁多谁少"，同样可点选。
   这里只管样式，布局在 reports.html 的 `tmLayout()`。 */

.tm-wrap {
    position: relative;
    height: 196px;
    margin: 2px 6px 4px;
}

.tm-cell {
    position: absolute;
    padding: 2px;                 /* 用内边距当缝，比 margin 好算 —— 百分比定位下 margin 会把块挤出去 */
    min-width: 0;
    min-height: 0;
    cursor: pointer;
}

.tm-in {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 2px;
    text-align: left;
    height: 100%;
    min-width: 0;
    padding: 4px 6px;
    border-radius: 4px;
    border: 1px solid rgba(122, 162, 224, .22);
    overflow: hidden;
    transition: border-color .15s ease;
}

.tm-cell:hover .tm-in { border-color: rgba(158, 198, 255, .75); }

/* 选中态不只换边框色：小色块上一点色差根本看不出来，加一圈外发光才够 */
.tm-cell.is-on .tm-in {
    border-color: #9ec6ff;
    box-shadow: 0 0 0 1px rgba(158, 198, 255, .55), 0 0 12px rgba(74, 158, 255, .25);
}

.tm-in b {
    display: -webkit-box;
    /* ★ 允许折行并最多两行：「雕庄街道」在 40px 宽的细条里用省略号
       会变成「雕庄…」，而**街道名恰恰是这一块唯一的身份**。
       中文任意字间断行不产生歧义，比截断好。 */
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    font-size: 11.5px;
    font-weight: 600;
    line-height: 1.25;
    color: #eaf1ff;
    overflow: hidden;
    overflow-wrap: anywhere;
}

.tm-in i {
    display: block;
    font-style: normal;
    font-size: 10.5px;
    line-height: 1.3;
    color: rgba(233, 238, 251, .78);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-variant-numeric: tabular-nums;
}


/* ==========================================================================
   支付测试页（student/m/paytest.html）
   --------------------------------------------------------------------------
   10-29 第 47 轮：这一页原先用十几个内联 style 拼版式，同一个
   「12.5px 灰字」在五处各写一遍 —— 改一处漏四处，而且没有任何报错
   （见 tools/check_inline_styles.js 的来由）。这里把重复出现的组合收成语义类。

   ★ 前缀 `pt-` 只用于这一页，避免与全局类互相影响。
   ========================================================================== */
.pt-head { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
.pt-head__icon { color: var(--brand-600); display: inline-flex; }
.pt-head__title { font-size: 14px; font-weight: 600; color: var(--ink-800); }
.pt-head__badge { margin-left: auto; }
.pt-amount {
    font-size: 30px; font-weight: 700; color: var(--brand-700);
    line-height: 1.2; font-variant-numeric: tabular-nums;
}
.pt-sub { font-size: 12.5px; color: var(--ink-500); margin-top: 4px; }
.pt-sub--tight { margin-top: 2px; }
.pt-row { display: flex; gap: 8px; align-items: flex-start; margin-bottom: 8px; }
.pt-row__icon {
    flex: none; display: inline-flex; color: var(--brand-600); margin-top: 1px;
}
.pt-row__text { font-size: 13px; color: var(--ink-700); line-height: 1.55; }
.pt-qr-title {
    font-size: 13.5px; font-weight: 600; color: var(--ink-800);
    text-align: center; margin-bottom: 10px;
}
.pt-tip { font-size: 12px; color: var(--ink-500); text-align: center; margin-top: 8px; }
.pt-tip--foot { padding: 10px 16px 0; }
.pt-field { margin-top: 10px; }
.pt-url { font-size: 12px; }
.pt-hint { font-size: 12px; color: var(--ink-500); margin-top: 6px; }
.pt-result { display: flex; gap: 8px; align-items: flex-start; }
.pt-result__icon { flex: none; display: inline-flex; }
.pt-result__text { font-size: 13.5px; line-height: 1.6; font-weight: 600; }
.pt-result--ok .pt-result__icon,
.pt-result--ok .pt-result__text { color: var(--p-ok); }
.pt-result--warn .pt-result__icon,
.pt-result--warn .pt-result__text { color: var(--p-warn); }
.pt-result--err .pt-result__icon,
.pt-result--err .pt-result__text { color: var(--p-err); }

/* ==========================================================================
   机构工作台（`web/org/dashboard.html`）
   --------------------------------------------------------------------------
   这一页原来只有「四张指标卡 + 两张图 + 一张表」。本轮补三块内容，
   全部从**同一份** `GET /api/org/classes` 算出来，不新增接口：

     ① 欢迎条 `.org-hero` —— 机构是谁、能不能开班、四个常用入口
     ② 待办清单 `.todo`    —— 把「该处理但没人提醒」的事摆到首屏
     ③ 班次排期 `.schedule` —— 一天一行，列出当天有课的班次

   ★ 为什么不新增接口：每多一个接口就多一处口径，而口径迟早会漂
     （「看板上的开放中」与「班次页筛出来的条数」曾经就对不上）。
   ★ 配色只用既有令牌（--brand / --ink / --c1..c6 / 语义色），
     不引第二种强调色 —— 与全站「一屏之内只让蓝当主角」的调子一致。
   ★ 类名一律 `org-` / `todo` / `schedule` / `quick-nav` 前缀，
     不与既有组件撞名（`tools/check_chart.js` 按类名做 1:1 对照）。
   ========================================================================== */

/* ---- 页面纵向节奏 ----
   `.grid-2 > .card` 把卡片的底边距清掉了（两栏等高时不需要重影），
   于是连续两个网格行会**贴在一起**。这里只给本页补行距，不动全局。 */
.org-dash .grid-2 { margin-bottom: var(--sp-4); }

/* ---- 表格里的日期区间：禁止折行 ----
   折行后「起」和「止」被拆成两行，读起来像两个各自独立的日期。 */
.org-dash .date-range { white-space: nowrap; font-variant-numeric: tabular-nums; }

/* ---- 0 值条形真的画成 0 宽 ----
   `chart.js` 给 0 值写的是 `width:0`，但 `.chart__fill` 带 `min-width:3px`，
   于是 0 也在轨道上留一小段 —— 看着像"有一点人"，与"0 就是 0"的口径相反。
   只在本页放开最小宽度（`chart.js` 已给非零值兜了 0.8% 下限）。 */
.org-dash .chart__fill { min-width: 0; }
.org-dash > .card:last-child { margin-bottom: 0; }

/* ---- 顶部一行：4 张指标卡 + 班次状态构成 ----
   `.stat-grid` 默认是 auto-fill(190px)，放进定宽栅格后会自己算列数 ——
   这里钉死 4 列，卡片宽度才由"剩下的空间"决定，而不是由内容撑着。 */
.org-dash__top {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 320px;
    gap: var(--sp-4);
    margin-bottom: var(--sp-4);
    align-items: stretch;
}
.org-dash__top .stat-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    margin-bottom: 0;
}

/* 状态构成收成"第 5 块"：无 card__head（那是给大卡的），改用一条 14px 的标题带 */
.status-tile {
    display: flex;
    flex-direction: column;
    padding: 14px 16px;
    margin-bottom: 0;
}
.status-tile__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 8px;
}
.status-tile__title {
    font-size: 13px;
    font-weight: 600;
    color: var(--text-2);
    white-space: nowrap;
}
.status-tile__total {
    font-size: 12px;
    color: var(--text-3);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.status-tile__body {
    display: flex;
    align-items: center;
    gap: 14px;
    flex: 1 1 auto;
    min-width: 0;
}

/* 环图缩到 78px：整卡高度（≈132px）才会与指标卡齐平。
   不缩的话 170px 的环会把这张卡拉高，四张指标卡跟着被拉伸、卡内留白。 */
.status-tile .donut { flex: 0 0 78px; width: 78px; height: 78px; }
.status-tile .donut__center b { font-size: 17px; letter-spacing: -.3px; }
/* 图例并排（不是叠在环下面）：`.legend` 默认 max-width:380 + flex-basis 140，
   在这条 194px 的剩余宽度里要显式放开，否则它会被挤到换行。 */
.status-tile .legend {
    flex: 1 1 auto;
    gap: 4px;
    max-width: none;
    min-width: 0;
    font-size: 12px;
}
.status-tile .legend__val { font-size: 12px; }

/* ---- 待办（左）+ 各班次人数（右）同一行 ----
   1 : 1.15 —— 条形图靠长度说话，多给一点轨道宽度；待办是文本，窄一点不影响读。 */
.org-dash__pair {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
    gap: var(--sp-4);
    margin-bottom: var(--sp-4);
    align-items: stretch;
}
.org-dash__pair > .card { margin-bottom: 0; }

/* 中窄屏：状态卡落到指标卡下面，改成一整条"状态带" ——
   环 + 图例横向铺开，而不是让图例竖着挤在左边、右边留一大片空白。 */
@media (max-width: 1240px) {
    .org-dash__top { grid-template-columns: minmax(0, 1fr); }
    .status-tile { flex-direction: row; align-items: center; gap: 16px; padding: 12px 16px; }
    .status-tile__head { flex-direction: column; align-items: flex-start; gap: 2px; margin-bottom: 0; }
    .status-tile__body { flex: 1 1 auto; gap: 18px; }
    .status-tile .donut { flex: 0 0 62px; width: 62px; height: 62px; }
    .status-tile .legend { flex-direction: row; flex-wrap: wrap; gap: 6px 28px; }
}
@media (max-width: 1080px) {
    .org-dash__pair { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 820px) {
    .org-dash__top .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ------------------------------ 欢迎条 ------------------------------ */
/* 浅蓝径向光晕 + 线性渐变，压在卡片的柔阴影上。
   ★ 不用整块深蓝填充：这一页下面还有 5 张白卡，头图一旦压成深色，
     视线会先被它吸住，指标卡的层次就塌了。 */
.org-hero {
    position: relative;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-5);
    flex-wrap: wrap;
    padding: 20px 24px;
    margin-bottom: var(--sp-4);
    border: 1px solid var(--brand-100);
    border-radius: var(--r-lg);
    background:
        radial-gradient(560px 220px at 88% -40%, rgba(37, 99, 235, .10), transparent 70%),
        linear-gradient(120deg, #f2f7ff 0%, #f8fbff 54%, #ffffff 100%);
    box-shadow: var(--shadow-sm);
}

.org-hero__main { min-width: 0; flex: 1 1 320px; }

.org-hero__eyebrow {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 12px;
    letter-spacing: .4px;
    color: var(--brand-700);
}
/* 小圆点 + 一圈淡光环：与侧栏当前项同一套"在线"语汇 */
.org-hero__eyebrow i {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--brand-500);
    box-shadow: 0 0 0 4px rgba(37, 99, 235, .12);
    flex: 0 0 6px;
}

.org-hero__name {
    margin: 7px 0 0;
    font-size: 22px;
    font-weight: 700;
    letter-spacing: -.4px;
    line-height: 1.35;
    color: var(--text);
}

.org-hero__meta {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-top: 10px;
    font-size: 13px;
    color: var(--text-2);
}
.org-hero__meta .sep { color: var(--ink-300); }

.org-hero__side {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 9px;
    flex: 0 1 auto;
}
.org-hero__hint { font-size: 12px; color: var(--text-3); letter-spacing: .3px; }

/* 常用入口：胶囊按钮。
   ★ 用 `<a>` 而不是 button —— 它们全是"去另一页"，语义就是链接；
     写成 button 再 location.assign 会丢掉"新标签页打开"这个能力。 */
.quick-nav {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    justify-content: flex-end;
}
.quick-nav__item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 32px;
    padding: 0 13px;
    border: 1px solid var(--brand-100);
    border-radius: 999px;
    background: #fff;
    color: var(--brand-700);
    font-size: 12.5px;
    font-weight: 500;
    box-shadow: 0 1px 2px rgba(16, 24, 40, .04);
    transition: background var(--dur) var(--ease),
                border-color var(--dur) var(--ease),
                transform var(--dur) var(--ease);
}
.quick-nav__item:hover {
    background: var(--brand-50);
    border-color: var(--brand-500);
    color: var(--brand-700);
    text-decoration: none;
    transform: translateY(-1px);
}
.quick-nav__item svg { flex: 0 0 auto; opacity: .85; }

/* ------------------------------ 待办清单 ------------------------------ */
.todo {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 14px 0;
    border-bottom: 1px dashed var(--ink-100);
}
.todo:first-child { padding-top: 2px; }
.todo:last-child { padding-bottom: 2px; border-bottom: 0; }

/* 图标块：颜色只表达"这条要不要紧"，不承载信息 */
.todo__icon {
    flex: 0 0 30px;
    width: 30px;
    height: 30px;
    border-radius: 9px;
    display: grid;
    place-items: center;
    background: var(--warning-bg);
    color: var(--warning);
}
.todo__icon--info { background: var(--brand-50); color: var(--brand-600); }
.todo__icon--danger { background: var(--danger-bg); color: var(--danger); }
.todo__icon--ok { background: var(--success-bg); color: var(--success); }

.todo__main { flex: 1 1 auto; min-width: 0; }
.todo__title {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13.5px;
    font-weight: 600;
    color: var(--text);
}
/* 条数徽章：与标题同行，数字右对齐不跳动 */
.todo__num {
    padding: 0 7px;
    border-radius: 999px;
    background: var(--warning-bg);
    color: var(--warning);
    font-size: 11.5px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 18px;
}
.todo__num--info { background: var(--brand-50); color: var(--brand-700); }
.todo__num--danger { background: var(--danger-bg); color: var(--danger); }
.todo__hint {
    margin-top: 3px;
    font-size: 12px;
    line-height: 1.6;
    color: var(--text-3);
}
.todo__go {
    flex: 0 0 auto;
    align-self: center;
    font-size: 12.5px;
    color: var(--brand-700);
    white-space: nowrap;
}

/* ------------------------------ 班次排期（每天一格） ------------------------------
   布局沿用甘特图的读法：**一行一个班次，横轴是时间**；区别是横轴不再画
   三两个日期刻度，而是**每天一格** —— 有课的那几格上色，没课的是浅灰格子。
   这样"这个班占哪几天"不用换算，数格子就行。

   ★ 格子宽度用 `minmax(5px, 18px)` 而不是固定值：
     天数少时格子撑到 18px（好看），天数多时压到 5px 下限，
     再装不下就让整块横向滚动（`overflow-x`）—— 两种极端都不会破版。
   ★ 列模板由页面按天数内联（`grid-template-columns: 132px repeat(N, …) 区间`），
     所以这里只管单元格长相，不管列数。 */
.schedule { display: flex; flex-direction: column; }

.schedule__scroll {
    overflow-x: auto;
    overflow-y: hidden;
    padding-bottom: 2px;
}

.schedule__grid {
    display: grid;
    column-gap: 2px;
    row-gap: 4px;
    align-items: center;
}

/* 名称列：滚动时**钉在左边** —— 长历史的表格横向滚起来后，
   没有班次名的那一屏等于白看。 */
.schedule__name {
    position: sticky;
    left: 0;
    z-index: 1;
    padding-right: 8px;
    background: var(--surface);
    font-size: 12.5px;
    color: var(--text-2);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.schedule__name--head { font-size: 12px; color: var(--text-3); }

/* 刻度：每 7 天一个，宽度只有一格，文字允许溢出到邻格（邻格是空的） */
.schedule__tick {
    font-size: 11px;
    color: var(--text-3);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.schedule__tick.is-today { color: var(--warning); font-weight: 600; }

/* 一个格子 = 一天。没课也要画出来（浅灰），否则"空档"和"没这一列"分不清。 */
.schedule__cell {
    height: 15px;
    border-radius: 3px;
    background: var(--ink-100);
}
/* 有课：蓝=还在办，灰=已结束/已撤销。判据取服务端下发的 `effective_status`。 */
.schedule__cell.is-on { background: var(--c1); }
.schedule__cell.is-off { background: var(--ink-400); }
/* 今天那一列：整列套一圈琥珀色内描边，跨行连起来就是一条竖线，
   不必再单独画线（原本甘特图里那根 `gantt__today` 的替身）。 */
.schedule__cell.is-today { box-shadow: inset 0 0 0 1.5px var(--warning); }

.schedule__range {
    padding-left: 10px;
    font-size: 12px;
    color: var(--text-3);
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.schedule__days { margin-left: 4px; color: var(--ink-400); font-size: 11px; }

/* 悬停提示浮层：深底白字，贴在格子正上方（放不下就翻到下方）。
   挂在 body 上、fixed 定位 —— 卡片内部的横向滚动容器会裁掉 absolute 浮层。 */
.schedule__tip {
    position: fixed;
    left: 0;
    top: 0;
    z-index: 60;
    max-width: 280px;
    padding: 5px 9px;
    border-radius: 6px;
    background: var(--ink-900);
    color: #fff;
    font-size: 12px;
    line-height: 1.45;
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    transition: opacity .1s var(--ease);
    box-shadow: var(--shadow);
}
.schedule__tip.is-on { opacity: 1; }
.schedule__tip b { font-weight: 600; font-variant-numeric: tabular-nums; }
.schedule__tip span {
    display: block;
    margin-top: 1px;
    color: rgba(255, 255, 255, .76);
    font-size: 11.5px;
    overflow: hidden;
    text-overflow: ellipsis;
}

.schedule__legend {
    display: flex;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
    margin-top: var(--sp-3);
    padding-top: var(--sp-3);
    border-top: 1px dashed var(--ink-100);
    font-size: 12px;
    color: var(--text-3);
}
.schedule__legend i {
    display: inline-block;
    width: 11px;
    height: 8px;
    margin-right: 5px;
    border-radius: 2px;
    background: var(--c1);
    vertical-align: middle;
}
.schedule__legend i.is-off { background: var(--ink-300); }
.schedule__legend i.is-today { background: transparent; box-shadow: inset 0 0 0 1.5px var(--warning); }

.schedule__more {
    padding: 10px 0 2px;
    font-size: 12px;
    color: var(--text-3);
}

@media (max-width: 820px) {
    .schedule__grid { row-gap: 3px; }
    .schedule__cell { height: 14px; }
}

/* ------------------------------ 卡片头工具条 ------------------------------ */
/* 卡片头右侧放「搜索 + 两个按钮」：标题与工具条之间的间距交给 gap，
   ★ 不用 `margin-left:auto` 逐个推 —— 少一个元素就会错位。 */
.head-tools {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    flex-wrap: wrap;
}
.head-tools .input-search {
    width: 200px;
    height: 28px;
    padding: 0 10px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface);
    font-size: 12.5px;
    color: var(--text);
}
.head-tools .input-search::placeholder { color: var(--ink-400); }
.head-tools .input-search:focus {
    outline: none;
    border-color: var(--brand-500);
    box-shadow: 0 0 0 3px rgba(37, 99, 235, .12);
}

@media (max-width: 820px) {
    .org-hero__side { align-items: flex-start; }
    .quick-nav { justify-content: flex-start; }
    .head-tools .input-search { width: 100%; }
}

/* ---- 欢迎条里的数字（在办 / 在册）用正文色，与徽章区分开 ---- */
.org-hero__meta b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }

/* ---- 指标卡加载骨架 ----
   只画两根灰条（标签 + 数字），宽度靠 nth 固定 —— 不写内联 style，
   换版时不必逐页去改「这个 56% 是给谁的」。 */
.stat--skeleton { display: flex; flex-direction: column; gap: 10px; }
.stat--skeleton .skeleton:first-child { width: 56%; height: 12px; }
.stat--skeleton .skeleton:last-child { width: 44%; height: 26px; }
