/* 后浪学习平台 · 样式
 * 约定：
 *   - 颜色一律走 CSS 变量，深色主题只覆盖变量，不写第二套规则
 *   - 字号三档由 html[data-font] 控制，全部尺寸用 rem/em 跟随
 *   - 不引入任何外部字体/图标库，图标用内联 SVG 或字符
 */

/* ================= 变量 ================= */
:root {
  --bg: #F6F7FB;
  --panel: #FFFFFF;
  --panel-2: #F1F3F9;
  --panel-3: #E9EDF6;
  --line: #E3E7F0;
  --line-2: #D5DBE8;
  --txt: #1B2030;
  /* ★ dim/dim2 的取值由 impeccable detect 实测 + WCAG AA 验算决定，不再凭眼睛调。
     改之前实测：--dim2 #9AA3B5 on #fff 只有 2.54:1（远低于 4.5），
     --dim #6B7488 on --bg 只有 4.38:1（差一点点）。两族都压深后仍保留一档层次。 */
  --dim: #4E5768;    /* 6.79:1 on --bg ｜ 6.56:1 on --panel-2 */
  --dim2: #5C6576;   /* 5.87:1 on #fff ｜ 5.29:1 on --panel-2 */
  --pri: #2563EB;
  --pri-2: #1D4ED8;
  /* ★ 按钮专用渐变：白字/白图标压在它上面，所以**不能**直接用 --pri-2 ——
     dark 主题的 --pri-2 是浅蓝 #7BA4FF，白字压上去比原来更看不清。
     两套主题各给一组，各自都实测过白字对比度（light 5.93:1 / dark 4.86:1）。 */
  --btn-from: #1D4ED8;
  --btn-to: #0369A1;
  --pri-soft: #E8EFFF;
  --ok: #16A34A;
  --ok-soft: #E6F6EC;
  --warn: #D97706;
  --warn-soft: #FDF3E3;
  --bad: #DC2626;
  --bad-soft: #FDECEC;
  --shadow: 0 1px 2px rgba(16, 24, 40, .04), 0 10px 26px -14px rgba(16, 24, 40, .22);
  --shadow-lg: 0 2px 4px rgba(16, 24, 40, .05), 0 24px 60px -22px rgba(16, 24, 40, .34);
  --r: 14px;
  /* 批次109：比 --r 大一档的圆角（16）。实体键按钮与提示卡三分层共用这一档 ——
     聪聪学堂那两处量出来就是 16，而且"要被按下去的键"圆角太小会显得钝。 */
  --r-lg: 16px;
  --r-sm: 10px;
  --r-xs: 7px;
  /* 侧栏宽度可拖拽调整（批次2），存 localStorage；收起时走 .side-min */
  --side-w: 268px;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
  /* ★ 学习岛屿（批次101）专用配色。
     刻意**不从 --pri 派生**：岛是"自然景物"（海/沙/草/树），品牌换肤不该把海换成紫色。
     深色主题只覆盖这一组变量，规则一条都不重写。 */
  --lp-sea: #CFE7F6;      /* 海面 */
  --lp-sea-2: #B6D8EE;    /* 浪纹 */
  --lp-sand: #EFDCB2;     /* 沙滩（岛面） */
  --lp-cliff: #C79A6B;    /* 岛壁亮面 */
  --lp-cliff-lo: #9E7048; /* 岛壁暗面 / 地块左崖 */
  --lp-grass: #8FD08A;    /* 已掌握的地 */
  --lp-open: #F2C14E;     /* 这一关（暖金，故意离开绿族才跳得出来） */
  --lp-open-2: #C99A2E;
  --lp-ground: #DFD2B6;   /* 还没打开的地（"没清理的地面"，不是灰石头） */
  --lp-ground-2: #C2AF8C;
  --lp-lock-face: #F3EAD8;
  --lp-lock-ink: #7A6444;
  --lp-path: #C9B183;     /* 路 */
  --lp-tree: #2F7A46;
  --lp-tree-2: #3E9B58;
  --lp-trunk: #8A5A32;
  /* ★ 鱼缸（批次105）专用配色。与学习岛屿（批次101）**同一条理由**：
     水是"自然景物"，品牌换肤不该把海换成紫色。深色主题只覆盖这一组变量，
     绘制规则一条都不重写。 */
  --tk-water: #DCEBF5;      /* 水（浅处） */
  --tk-water-2: #B9D7EA;    /* 水（近底，压暗一档） */
  --tk-glass: #93B7CF;      /* 缸壁 */
  --tk-surface: #EAF4FB;    /* 水面那条亮线 */
  --tk-wave: #FFFFFF;       /* 浪纹 */
  --tk-ray: #FFFFFF;        /* 光柱 */
  --tk-body: #3E8E7E;       /* 鱼身 */
  --tk-fin: #2F7A6B;        /* 鳍 / 尾 */
  --tk-tail-whip: #9AA9B4;  /* 鳐那根细鞭尾 */
  --tk-band: #FFFFFF;       /* 横纹（小丑鱼 / 鲸鲨） */
  --tk-eye: #FFFFFF;
  --tk-pupil: #12303A;
  --tk-spout: #B7D6EA;      /* 鲸的喷水 */
  --tk-spray: #DCEDF8;      /* 喷水溅起的小水珠 */
  --tk-belly: #CFE7E0;      /* 鲸的浅色腹部 */
  --tk-spot: #EAF4FB;       /* 鲸鲨的斑点 */
  --tk-lateral: #2F7A6B;    /* 侧线 */
  --tk-mouth: #1F5A50;      /* 嘴线 */
  /* ★★ 批次141：神话线（蛟 / 龙 / 苍龙）的部件色。
     为什么单独一组而不是复用 --tk-fin：龙的角、爪、翼、云气在**浅色背景**上
     必须比鱼鳍更亮更暖，否则会糊成一片深绿；深色主题下又要压暗一档否则刺眼。
     两套主题各自给值，JS 里一个 fill 都不写死。 */
  --tk-horn: #C9A227;       /* 角（暖金，神话感的第一信号） */
  --tk-claw: #B8912A;       /* 爪 */
  --tk-leg: #7A6A3A;        /* 腿 */
  --tk-dscale: #2F7A6B;     /* 龙鳞（斜线） */
  --tk-dfin: #2A6E62;       /* 蛟的背鳍 */
  --tk-dmane: #3C8C6E;      /* 龙鬃 */
  --tk-wing: #A8C8B8;       /* 苍龙的翼 */
  --tk-whisk: #8FA9B8;      /* 龙须 */
  --tk-brow: #1F5A50;       /* 眉 */
  --tk-cloud: #E6EFF6;      /* 头顶云气 */
  --tk-weed: #4E9A5C;       /* 水草 */
  --tk-weed-deco: #4E9A5C;  /* 缸景·水草丛 */
  --tk-coral: #E08A6B;      /* 珊瑚 */
  --tk-bubble: #FFFFFF;
  --tk-school: #7FA9C4;     /* 远处的鱼群 */
  --tk-lamp-glow: #FFD98A;  /* 石灯的灯芯 */
  /* ★ 动效时长（批次106）：全站只允许这四档。
     改之前实测：本文件里散着 **14 个不同时长**，其中 95 处挤在 .08~.18s 却分成
     .08/.13/.15/.16/.18 五个数字 —— 人眼分不出 130ms 与 150ms 的差别，
     但同一类交互在不同组件上各写各的数字，累积起来就是"说不上来哪里别扭"。
     现在按**交互类型**归并（不再按组件各写各的）：
       --dur-1 微反馈   悬停 / 按下 / 颜色与边框变化（原 .08/.12/.13/.15/.16s）
       --dur-2 状态切换  展开收起 / 开关 / 进度条（原 .18/.2s）
       --dur-3 浮层进出  菜单 / 面板 / 抽屉（原 .22/.24/.25/.26/.3s）
       --dur-4 大幅位移  岛屿移动 / 页面级进场（原 .35/.45/.5/.55/.6s）
     ★ 缓动只留两条，不再有第三种：
       --ease-out 减速（进场 / 悬停 —— 快出慢入，收尾不突兀）
       --ease-io  标准（双向位移 —— 起止对称）
     ★ 无限循环的呼吸 / 闪烁（.lp-halo / .mic / .skel / .caret）**不并入**：
       它们是"环境音"而不是"交互反馈"，压到同一档反而让节奏变单调。 */
  --dur-1: .15s;
  --dur-2: .18s;
  --dur-3: .26s;
  --dur-4: .45s;
  /* ★ 批次309：水中滑行是**第五条**，刻意不进四档 —— 它不是"交互反馈"，
     而是鱼的移动手感常量（批次306 用户实测「角色跟随鼠标移动太快」，
     把 .5s 调到 .9s 才有"游过去"的手感）。硬塞进 --dur-4 会让鱼"啪"地弹过去，
     正好丢掉用户要的那口气。`.sea-mine`（自己的鱼跟鼠标）与
     `.sea-bait`（撒鱼食下沉）共用它 —— 两处都是同一件事：水里慢慢滑过去。 */
  --dur-glide: .9s;
  --ease-out: cubic-bezier(.22, .8, .3, 1);
  --ease-io: cubic-bezier(.4, 0, .2, 1);
}
[data-theme="dark"] {
  --bg: #0E1117;
  --panel: #161A22;
  --panel-2: #1C212B;
  --panel-3: #232935;
  --line: #272D3A;
  --line-2: #333A4A;
  --txt: #E8ECF4;
  --dim: #9BA5B8;   /* 7.62:1 on --bg */
  /* ★ 深色底上「越亮越清楚」，所以辅助色要往亮调，不是往暗调。
     改之前 --dim2 #6C7688 on --panel 只有 3.8:1（深色模式同样不过 AA）。 */
  --dim2: #8A94A8;  /* 6.19:1 on --bg ｜ 5.29:1 on --panel-2 */
  --pri: #5B8CFF;
  --pri-2: #7BA4FF;
  /* 深色底上按钮要"亮起来"才显眼，但亮到能让白字读清是有上限的：
     #7BA4FF 太浅（白字几乎看不见），这里退到 blue-600 档，白字 4.86:1。 */
  --btn-from: #2563EB;
  --btn-to: #1D4ED8;
  --pri-soft: #1B2740;
  --ok: #34D399;
  --ok-soft: #14301F;
  --warn: #FBBF24;
  --warn-soft: #33270E;
  --bad: #F87171;
  --bad-soft: #351A1A;
  /* 批次206：红笔批改色 —— 深色底上往亮调（同 --bad 的道理：红笔要"跳"出来） */
  --pen: #FF6B6B;
  --pen-soft: #3A1D1D;
  --shadow: 0 1px 2px rgba(0, 0, 0, .45), 0 12px 30px -16px rgba(0, 0, 0, .75);
  --shadow-lg: 0 2px 6px rgba(0, 0, 0, .5), 0 28px 70px -26px rgba(0, 0, 0, .9);
  /* 学习岛屿（批次101）：深色下海更深、沙更暗、草更沉 —— 但**关系不变**
     （已掌握最亮、这一关最跳、没打开最闷），否则学生换主题就认不出自己的进度。 */
  --lp-sea: #0E1E2C;
  --lp-sea-2: #17303F;
  --lp-sand: #6E5F45;
  --lp-cliff: #4A3B2E;
  --lp-cliff-lo: #382C22;
  --lp-grass: #2E6B49;
  --lp-open: #B8860B;
  --lp-open-2: #8A6410;
  --lp-ground: #6A5B41;
  --lp-ground-2: #57492F;
  --lp-lock-face: #2A2419;
  --lp-lock-ink: #C0A87E;
  --lp-path: #3E3626;
  --lp-tree: #2A6B45;
  --lp-tree-2: #357D51;
  --lp-trunk: #6B4526;
  /* 鱼缸（批次105）：深色下海更深、鱼更亮 —— 但**关系不变**
     （鱼永远比水亮、水草永远比水暗一档），否则学生换主题就看不见自己的鱼了。 */
  --tk-water: #0F2433;
  --tk-water-2: #07161F;
  --tk-glass: #2C4A5E;
  --tk-surface: #3A6480;
  --tk-wave: #4C7C9B;
  --tk-ray: #2A4A61;
  --tk-body: #3FB39B;
  --tk-fin: #2E8A79;
  --tk-tail-whip: #5B7285;
  --tk-band: #CFE4F2;
  --tk-eye: #F2F8FC;
  --tk-pupil: #0A1E26;
  --tk-spout: #4E7E9C;
  --tk-spray: #7FB4D2;
  --tk-belly: #2A6A5E;
  --tk-spot: #CFE4F2;
  --tk-lateral: #2E8A79;
  --tk-mouth: #1B4A42;
  /* ★★ 批次141：神话线部件（深色主题）。整体压暗一档 + 提饱和，
     否则在深蓝底上会「发光发白」—— 这是深色主题里最常见的一眼假。 */
  --tk-horn: #D9B94A;
  --tk-claw: #C6A33A;
  --tk-leg: #6E6040;
  --tk-dscale: #2E8A79;
  --tk-dfin: #27675C;
  --tk-dmane: #347F62;
  --tk-wing: #3E6B5E;
  --tk-whisk: #5B7285;
  --tk-brow: #0F3B35;
  --tk-cloud: #33556B;
  --tk-weed: #2F7A4A;
  --tk-weed-deco: #3A8F58;
  --tk-coral: #B4644A;
  --tk-bubble: #6FA3C0;
  --tk-school: #3C6A88;
  --tk-lamp-glow: #FFC96B;
}

/* 字号三档 */
html { font-size: 16px; }
html[data-font="sm"] { font-size: 15px; }
html[data-font="md"] { font-size: 16.5px; }
html[data-font="lg"] { font-size: 18px; }

/* ================= 键盘焦点（批次106）=================
   ★ 改之前实测：全站只有 4 条 :focus-visible（.sc-main / .sc-sw / .int-btn /
   .iv-guide-*），而 .btn / .ct / .nav-i / .pm-i / .pm-f-i 一概没有 —— 键盘与
   外接键鼠用户根本看不见焦点在哪，只能靠"猜现在按下去会触发哪个"。
   ★ 用 :focus-visible 而不是 :focus：鼠标点击**不留**环，只有键盘导航才显示，
   这样补可达性不会把鼠标用户的界面弄脏。
   ★ 放在这里（而不是文件末尾）是有意的：本规则特异性只有 (0,1,0)，
   后面更具体的 `.inp:focus` / `.d-ans:focus` 等仍能覆盖它，各自的
   自定义环（box-shadow）不受影响。 */
:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }
/* ★ 侧栏整块是 `overflow: hidden`（.side），导航项贴着内边距，
   外扩 2px 的环会被裁掉半圈 ⇒ 这一处改用**内缩环**（跟随圆角，一定看得见）。 */
.nav-i:focus-visible { outline-offset: -2px; }

/* ================= 商店配色 / 背景（批次95）=================
   露珠买到的两样东西，落地方式都只是**覆盖 CSS 变量**：
     · data-skin    → 品牌色（--pri / --pri-2 / --btn-from / --btn-to / --pri-soft）
     · data-skin-bg → 页面底色（--bg）
   ★ 为什么不给每件商品写一套选择器：那会让"按钮 / 链接 / 聚焦环 / 导航高亮"
   每一处都要改一遍，漏一处就是"半件衣服"。只换变量 ⇒ 全站自动跟着变。
   ★ 颜色的**唯一真源就在这个文件**；`server/economy.js` 里的 `swatch` 只是
   给商店格子画小色块用的近似值，不参与渲染。
   ★ 浅色 / 深色各给一组：深色主题下 --pri 是**当文字用**的（要浅），
   而 --btn-from/--btn-to 上有白字（要深）—— 两者方向相反，不能共用一套。
   ★ 对比度：全部取调色板的 600/800 档，浅色主题下 --pri on #fff ≥ 4.5:1，
   白字 on --btn-from ≥ 4.5:1；深色主题下 --pri on --pri-soft ≥ 7:1。 */
html[data-skin="skin-bamboo"] { --pri: #0F6E56; --pri-2: #0B5743; --btn-from: #0B5743; --btn-to: #0F6E56; --pri-soft: #E1F5EE; }
html[data-skin="skin-sun"]    { --pri: #854F0B; --pri-2: #633806; --btn-from: #633806; --btn-to: #854F0B; --pri-soft: #FAEEDA; }
html[data-skin="skin-night"]  { --pri: #185FA5; --pri-2: #0C447C; --btn-from: #0C447C; --btn-to: #185FA5; --pri-soft: #E6F1FB; }
html[data-skin="skin-lotus"]  { --pri: #993556; --pri-2: #72243E; --btn-from: #72243E; --btn-to: #993556; --pri-soft: #FBEAF0; }
html[data-skin="skin-bamboo"][data-theme="dark"] { --pri: #5DCAA5; --pri-2: #9FE1CB; --pri-soft: #0B2A22; --btn-from: #0F6E56; --btn-to: #0B5743; }
html[data-skin="skin-sun"][data-theme="dark"]    { --pri: #EF9F27; --pri-2: #FAC775; --pri-soft: #33270E; --btn-from: #854F0B; --btn-to: #633806; }
html[data-skin="skin-night"][data-theme="dark"]  { --pri: #85B7EB; --pri-2: #B5D4F4; --pri-soft: #0C1B2E; --btn-from: #185FA5; --btn-to: #0C447C; }
html[data-skin="skin-lotus"][data-theme="dark"]  { --pri: #ED93B1; --pri-2: #F4C0D1; --pri-soft: #2E1520; --btn-from: #993556; --btn-to: #72243E; }
/* 背景：只动 --bg。★ 刻意**不动** --panel / --panel-2 ——
   那两个变量上的文字对比度是按白底算过的（见 :root 里那段 WCAG 注释），
   挪一下就可能掉到 4.5 以下。只换最外层底色，风险最小、也够看得出来。 */
html[data-skin-bg="bg-dawn"] { --bg: #FBF3E4; }
html[data-skin-bg="bg-rain"] { --bg: #EAF2FB; }
html[data-skin-bg="bg-pine"] { --bg: #EDF6EA; }
html[data-skin-bg="bg-sea"]  { --bg: #E7F7F4; }
html[data-skin-bg="bg-dawn"][data-theme="dark"] { --bg: #16120B; }
html[data-skin-bg="bg-rain"][data-theme="dark"] { --bg: #0B1119; }
html[data-skin-bg="bg-pine"][data-theme="dark"] { --bg: #0C120B; }
html[data-skin-bg="bg-sea"][data-theme="dark"]  { --bg: #0A1413; }

/* ---------- 商店（批次95）----------
   全部是**静态布局**，没有一处依赖 @media —— 用 auto-fill 自适应列数，
   这样窄屏不需要额外回退（也就不会把"第一个 @media 的位置"往前挪）。 */
/* 批次109：商店页的滚动区（.kb-sub 约定：顶栏钉住、滚动交给内部容器）。
   不要再写第二条 .shop-wrap 规则 —— 同 specificity 下后者覆盖前者，会把修复整条吃掉。 */
.shop-wrap {
  flex: 1; min-height: 0; overflow-y: auto; padding: 0 0 24px;
  display: grid; grid-template-columns: minmax(0, 1fr) 340px;
  gap: 22px; align-items: start;
}
.shop-main { min-width: 0; }
.shop-wallet {
  display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 10px;
  padding: 14px 16px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r);
}
.shop-wallet > div { flex: 1 1 96px; min-width: 96px; }
.shop-wallet span { display: block; font-size: .72rem; color: var(--dim); }
.shop-wallet b { font-size: 1.35rem; font-variant-numeric: tabular-nums; }
.shop-src, .shop-note { font-size: .78rem; color: var(--dim); line-height: 1.7; margin: 0 0 16px; }
.shop-note { margin: 16px 0 0; padding: .7em .9em; background: var(--panel-2); border-radius: var(--r-sm); }
/* 商店右侧的「我的海」：与商品列并排，宽屏固定可见；失败时虚线框展示文字状态（批次130）。 */
.shop-tank {
  position: sticky; top: 0;
  padding: 12px 12px 14px; background: var(--panel);
  border: 1px solid var(--line); border-radius: var(--r);
}
.shop-tank-h { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
.shop-tank-h b { font-size: .9rem; }
.shop-tank-sc { border-radius: 14px; overflow: hidden; }
.shop-tank-cap { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; font-size: .8rem; color: var(--dim); margin-top: 8px; }
.shop-tank-cap b { color: var(--txt); font-size: .88rem; }
.shop-tank-note { font-size: .74rem; line-height: 1.6; margin-top: 6px; }
.shop-tank-off { border-style: dashed; }
.shop-group { margin-bottom: 22px; }
.shop-group h3 { font-size: .86rem; font-weight: 600; margin: 0 0 10px; color: var(--dim); }
.shop-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(146px, 1fr)); gap: 12px; }
.shop-cell {
  font: inherit; text-align: left; cursor: pointer; display: flex; flex-direction: column; gap: 2px;
  padding: 12px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r);
  transition: border-color var(--dur-1), transform var(--dur-1), box-shadow var(--dur-1);
}
.shop-cell:hover:not(:disabled) { border-color: var(--pri); transform: translateY(-2px); box-shadow: var(--shadow); }
.shop-cell:disabled { cursor: default; }
.shop-cell.on { border-color: var(--pri); box-shadow: 0 0 0 2px var(--pri-soft) inset; }
.shop-cell.poor { opacity: .55; }
.shop-sw { display: block; height: 42px; border-radius: var(--r-sm); border: 1px solid var(--line-2); margin-bottom: 8px; }
.shop-sw.sw-default { background: linear-gradient(135deg, var(--panel), var(--panel-3)); }
.shop-cell b { font-size: .86rem; font-weight: 500; }
.shop-cell small { font-size: .72rem; color: var(--dim); line-height: 1.5; }
.shop-tag { margin-top: 8px; font-size: .72rem; color: var(--pri); font-weight: 500; }
.shop-cell.poor .shop-tag { color: var(--dim); }
.shop-cell.on .shop-tag { color: var(--dim); }

/* 露珠：跟成长值同一行，所以做得跟成长条一样轻。
   水滴是 CSS 画的（圆 + 一个直角），不引图片也不用 emoji ——
   和 petIcon 走 SVG 是同一条线：需要被识别的图形不靠字符。 */
.pet-dew {
  display: inline-flex; align-items: center; gap: 4px; margin-left: 8px;
  font-size: .72rem; color: var(--pri); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.pet-dew i { width: 9px; height: 9px; border-radius: 50% 50% 50% 0; background: currentColor; transform: rotate(-45deg); flex: none; }

/* 宝箱反馈：只在"这张卡第一次记牢"那一刻出现一次（见 app.js 的 chestHTML）。 */
.mf-chest {
  display: flex; align-items: center; gap: 10px; margin-top: 10px; padding: 10px 12px;
  background: var(--warn-soft); border-radius: var(--r-sm); border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent);
}
.mf-chest b { display: block; font-size: .84rem; font-weight: 500; }
.mf-chest small { display: block; font-size: .74rem; color: var(--dim); }
.mfc-box { position: relative; width: 24px; height: 20px; border-radius: 4px; background: var(--panel); border: 1.5px solid var(--warn); flex: none; }
.mfc-box::before { content: ''; position: absolute; left: -1.5px; right: -1.5px; top: -1.5px; height: 8px; border-radius: 4px 4px 1px 1px; background: var(--warn); }
.mfc-box::after { content: ''; position: absolute; left: 50%; top: 5px; width: 5px; height: 6px; margin-left: -2.5px; border-radius: 1px; background: var(--warn); }

/* ================= 基础 ================= */
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--txt);
  font-family: system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }
[hidden] { display: none !important; }
.dim { color: var(--dim); }
.sp { flex: 1; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 8px; border: 2px solid transparent; background-clip: content-box; }
::-webkit-scrollbar-thumb:hover { background: var(--dim2); background-clip: content-box; }
::-webkit-scrollbar-track { background: transparent; }

/* ================= 通用控件 ================= */
.btn {
  border: 1px solid var(--line-2);
  background: var(--panel);
  color: var(--txt);
  border-radius: var(--r-sm);
  padding: .42em .95em;
  font-size: .9rem;
  font-weight: 500;
  transition: background var(--dur-1), border-color var(--dur-1), transform var(--dur-1), color var(--dur-1);
  white-space: nowrap;
}
.btn:hover { background: var(--panel-2); border-color: var(--dim2); }
.btn:active { transform: translateY(1px); }
.btn.primary { background: var(--pri); border-color: var(--pri); color: #fff; }
.btn.primary:hover { background: var(--pri-2); border-color: var(--pri-2); }
.btn.ghost { background: transparent; border-color: transparent; color: var(--dim); }
.btn.ghost:hover { background: var(--panel-2); color: var(--txt); }
.btn.sm { padding: .3em .7em; font-size: .82rem; }
.btn.danger { color: var(--bad); border-color: var(--line-2); }
.btn.danger:hover { background: var(--bad-soft); border-color: var(--bad); }
.btn[disabled] { opacity: .5; pointer-events: none; }

/* ================= 批次109（2026-10-12）：借聪聪学堂两处"手感" =================
   学的是它阅读页那两样做得好的东西（做法见 _g5sys.cjs / _g5card.cjs 的实测数字）：
     ① .btn.chunky      主要 CTA 的"实体键"——4px 同色深边 + 按下压 3px
     ② .note 三分层      记住这个（绿）/ 小贴士（黄）/ 容易搞错（红）
   两处都**只加普通规则、不新增 @media**：_p25check 断言「.skill-c .sc-ico 在第一个
   响应式 @media 之前」、_mapcheck 断言「文件最后一个 @media 是地图那条 680px 窄屏
   回退」，中部插媒体查询会把这两条一起顶红。窄屏下按钮靠 flex-wrap、卡片靠
   max-width 自适应，本来就不需要单独回退。 */

/* ---- ① 实体键按钮 ----
   ★ 底边色为什么取 --btn-from 而不是 --pri-2：--pri-2 在深色主题是**浅**一档
     （#7BA4FF），一道浅色底边会让"被按下去的键"看起来是往上凸的，正好
     跟要传达的手感相反。而 --btn-from 正是本项目既有的"按钮族里压暗一档、白字
     压得住"那个 token —— :root 与 6 套 data-skin × 深浅主题各自都给过值，且都
     实测过白字对比度（见 :root 第 24~28 行那段注释）⇒ 换肤、换主题都不需要为
     底边再配一套颜色，也不会出现"浅色主题有底边、深色主题没有"的分裂。
   ★ 底边为什么用 box-shadow 而不是 border-bottom：transform 不吃布局。按下时
     只是"键体往下走 5px、底边从 6px 剩到 1px"，同一行的兄弟按钮不会跟着抖；
     用 border-bottom 的话元素高度会随按下变化 5px，flex 行高当场一跳。
   ★ :hover 为什么显式把背景钉回 --pri：.btn.primary:hover 用的是 --pri-2，
     深色主题下那是 #7BA4FF，白字压上去比不悬停时更糊（:root 那段注释记着这件事）。
     这里把悬停改成"键体抬 1px、底边从 6px 长到 7px"，颜色一个字节都不动，
     对比度零风险，手感反而更像一颗真键。
   ★ min-height 用 em 不用 px：字号三档（html[data-font] 15/16.5/18px）下一起缩放，
     这是文件头"全部尺寸用 rem/em 跟随"的约定。3.4em 在中间档 ≈ 54px。

   ★★★ 批次111（2026-10-12）：把实体键**放大到一眼可辨**。
     批次109 那版是"底边 4px / 按下 3px / min-height 3.3em"，数值取自聪聪学堂，
     但上线后用户实测反馈「整站都翻了，就是没变化」—— 孤立看时那 4px 底边会被
     大脑直接归到"还是那个按钮"；只有拿同一台浏览器、只切新旧两版 app.css 并排
     才看得出差异。⇒ **照抄对标的数值 ≠ 手感传达得到**，验收口径必须是"用户看得出"。
     本批把三个量一起抬：底边 4→6px、按下 3→5px、min-height 3.3→3.4em，
     并**保持"位移 == 底边-1"的固定关系**（按下后底边剩 1px = 真的压到底）。
     同时补两档落点（见下面 ①b / ①c）。 */
.btn.chunky {
  min-height: 3.4em; padding: .5em 1.5em;
  border-radius: var(--r-lg); font-size: .98rem; font-weight: 800;
  box-shadow: 0 6px 0 0 var(--line-2);
  transition: background var(--dur-1), border-color var(--dur-1), transform var(--dur-1),
    color var(--dur-1), box-shadow var(--dur-1);
}
.btn.chunky:hover { background: var(--panel-2); border-color: var(--dim2); }
.btn.chunky:active { transform: translateY(5px); box-shadow: 0 1px 0 0 var(--line-2); }
.btn.chunky.primary { background: var(--pri); border-color: var(--pri); color: #fff;
  box-shadow: 0 6px 0 0 var(--btn-from); }
.btn.chunky.primary:hover { background: var(--pri); border-color: var(--pri);
  transform: translateY(-1px); box-shadow: 0 7px 0 0 var(--btn-from); }
.btn.chunky.primary:active { transform: translateY(5px); box-shadow: 0 1px 0 0 var(--btn-from); }

/* ---- ①b 紧凑档：分区工具栏里的主按钮（.btn.chunky.sm）----
   ★ 为什么单开一档：.sm 那些按钮长在工具栏 / 分区标题行里，3.4em ≈ 54px 会当场把
     行高撑破（旁边是 .ct 那种 26px 的胶囊按钮）。紧凑档只保留"硬底边 + 按下去"
     两件事，尺寸退回 2.5em（≈40px）、圆角退回 --r。
   ★ 数值同样守"位移 == 底边-1"：底边 5px、按下 4px、按下后底边剩 1px。
   ★ 权重：.btn.chunky.sm（3 类）> .btn.chunky（2 类），所以这几条排在原组之后一定赢；
     排在一起是为了读起来是一条线。 */
.btn.chunky.sm { min-height: 2.5em; padding: .4em 1.05em; font-size: .84rem; border-radius: var(--r); }
.btn.chunky.sm:active { transform: translateY(4px); box-shadow: 0 1px 0 0 var(--line-2); }
.btn.chunky.primary.sm { box-shadow: 0 5px 0 0 var(--btn-from); }
.btn.chunky.primary.sm:hover { transform: translateY(-1px); box-shadow: 0 6px 0 0 var(--btn-from); }
.btn.chunky.primary.sm:active { transform: translateY(4px); box-shadow: 0 1px 0 0 var(--btn-from); }

/* ---- ①c 登录 / 注册那几颗全宽按钮（.land-btn.chunky）----
   ★ 为什么单独写：.land-btn 不属于 .btn 家族（它是全宽渐变按钮，定义在下方的
     .land 区块），直接挂 .chunky 拿不到任何样式。这里给它同一套"实体键"。
   ★ 底边同样用 --btn-from：渐变的深端就是它（白字 5.93:1，见 .land 区块那条注释）
     ⇒ 底边与键面是同一条色阶，不会出现"底边和键面不是一家人"。
   ★ 必须把 .land-btn:hover 那条浮起投影（0 10px 28px）一起压掉 —— 它和"实体键"
     的硬边互相打架。本组权重更高（两个类 > 一个类），写在前面也能赢。 */
.land-btn.chunky {
  border-radius: var(--r-lg); font-weight: 700;
  box-shadow: 0 6px 0 0 var(--btn-from);
  transition: filter var(--dur-1), transform var(--dur-1), box-shadow var(--dur-1);
}
.land-btn.chunky:hover { filter: brightness(1.05); box-shadow: 0 7px 0 0 var(--btn-from); }
.land-btn.chunky:active { transform: translateY(5px); box-shadow: 0 1px 0 0 var(--btn-from); }

/* ---- ② 提示卡三分层 ----
   几何与配色全部实测自聪聪学堂阅读页那三张卡（_g5card.cjs，2026-10-12 复测）：
     宽 512 / 圆角 16 / 内边距 16 / 2px 同色 20~25% 边框 / 5~10% 同色底 / 零阴影。
   ★ 标题文字用 --txt 而不用同色全饱和色：--ok #16A34A 与 --warn #D97706 压在
     浅色底上只有 3.2:1，小字号过不了 WCAG AA —— 本项目 :root 里 dim/dim2 那段
     注释就是为同一件事重挑的值，不能再开一个口子。分层改由**边框 + 底色 + 标题
     前那颗同色圆点**承担：圆点是图形不是文字，WCAG 1.4.11 只要 3:1，三色全过。
   ★ 底色直接用既有的 --ok-soft / --warn-soft / --bad-soft —— 它们本来就是"同色
     淡底"那一档，且深浅主题各自验过（实测换算下来分别约 10% / 8~11% / 8%，
     正落在实测区间里）。自己再手搓一遍百分比只会多一处将来会漂的色值。
   ★ 只有**边框**需要 20~25% 那一档（既有 token 里没有），走 color-mix 现算；
     照 .land-card 的老规矩**在上一档留一条实心同色的兜底**：不支持 color-mix 的
     浏览器在解析期就丢掉 color-mix 那条，于是落回"2px 同色实心边框"，三层仍然
     分得开 —— 而不是整条 border 变成 unset（border-style:none ⇒ 边框直接没了）。
     （注意兜底必须写在**属性**上、不能写成 `--nt-line` 的两条重复声明：
     自定义属性不做语法校验，后一条永远赢，兜底那行等于没写。）
   ★ max-width 而不是 width：512 是"阅读页正文列"的宽度，卡片在窄容器里应当收窄，
     不该把容器撑破。 */
.note {
  max-width: 512px; margin: 10px 0 0; padding: 16px;
  border: 2px solid var(--nt-line); border-radius: var(--r-lg);
  background: var(--nt-bg); box-shadow: none;
  font-size: .86rem; line-height: 1.7; color: var(--txt);
}
.note > b { display: flex; align-items: center; gap: 7px; margin: 0 0 3px;
  font-size: .82rem; font-weight: 800; color: var(--txt); }
.note > b::before { content: ""; flex: none; width: 8px; height: 8px;
  border-radius: 50%; background: var(--nt-ink); }
.note-mem { --nt-ink: var(--ok); --nt-line: var(--ok); --nt-bg: var(--ok-soft); }
.note-tip { --nt-ink: var(--warn); --nt-line: var(--warn); --nt-bg: var(--warn-soft); }
.note-warn { --nt-ink: var(--bad); --nt-line: var(--bad); --nt-bg: var(--bad-soft); }
.note-mem { border-color: color-mix(in srgb, var(--ok) 25%, transparent); }
.note-tip { border-color: color-mix(in srgb, var(--warn) 25%, transparent); }
.note-warn { border-color: color-mix(in srgb, var(--bad) 20%, transparent); }

.inp, .sel, textarea {
  border: 1px solid var(--line-2);
  background: var(--panel);
  border-radius: var(--r-sm);
  padding: .42em .7em;
  font-size: .9rem;
  outline: none;
  transition: border-color var(--dur-1), box-shadow var(--dur-1);
}
.inp:focus, .sel:focus, textarea:focus { border-color: var(--pri); box-shadow: 0 0 0 3px var(--pri-soft); }
.sel { cursor: pointer; padding-right: 1.6em; appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--dim) 50%), linear-gradient(135deg, var(--dim) 50%, transparent 50%);
  background-position: calc(100% - 14px) 55%, calc(100% - 9px) 55%;
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat;
}

.sw { display: inline-flex; align-items: center; gap: .45em; font-size: .88rem; color: var(--dim); cursor: pointer; user-select: none; }
.sw input { width: 1.05em; height: 1.05em; accent-color: var(--pri); cursor: pointer; }

.seg { display: inline-flex; background: var(--panel-2); border-radius: var(--r-sm); padding: 3px; gap: 2px; }
.seg button {
  border: 0; background: transparent; color: var(--dim);
  padding: .3em .85em; border-radius: 7px; font-size: .86rem; font-weight: 500;
  transition: background var(--dur-1), color var(--dur-1);
}
.seg button.on { background: var(--panel); color: var(--txt); box-shadow: var(--shadow); }

/* ================= 空间门 ================= */
/* ★ align-items 不能用 center：
   内容一旦高过视口，居中会把顶部推到 overflow 区域**且滚不回去**（flex 经典坑）。
   改成 flex-start + 子元素 margin:auto 0 —— 有空间时上下自动均分（视觉上仍是居中），
   没空间时两个 auto 归零、从顶部开始，滚得到。 */
.land {
  position: fixed; inset: 0; z-index: 50;
  display: flex; align-items: flex-start; justify-content: center;
  padding: 24px; overflow: auto;
  background: var(--bg);
}
.land-bg { position: absolute; inset: 0; overflow: hidden; }
.land-bg canvas { width: 100%; height: 100%; display: block; }
.land-bg::after {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(60% 55% at 50% 30%, transparent 0%, var(--bg) 78%);
}
/* 抬头与卡片同宽、同一条中轴 —— 宽度只写在这里，卡片用 100% 继承。
   两处各写一次 min(440px,100%) 迟早会漂移。 */
.land-col { position: relative; z-index: 2; width: min(440px, 100%); margin: auto 0; }
.land-head { text-align: center; margin-bottom: 18px; animation: rise var(--dur-4) var(--ease-out) both; }
.land-stance {
  margin: 10px 0 0; font-size: .84rem; line-height: 1.6; color: var(--dim);
}
.land-card {
  position: relative;
  width: 100%;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 20px;
  padding: 26px 30px 20px;
  box-shadow: var(--shadow-lg);
  animation: rise var(--dur-4) var(--ease-out) both;
}
@keyframes rise { from { opacity: 0; transform: translateY(14px) scale(.99); } to { opacity: 1; transform: none; } }
.land-logo {
  display: flex; align-items: center; gap: 12px;
}
/* ★ 品牌图形：浪形 + 浪尖一点光。宽度收着点，别抢了「后浪奔涌」四个字的视线。 */
.land-logo-img { width: 68px; height: auto; flex: none; }
.land-logo-word {
  font-size: 1.85rem; font-weight: 700; letter-spacing: .04em;
  background: linear-gradient(120deg, var(--pri), #0EA5E9 70%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.land-sub { color: var(--dim); font-size: .85rem; margin: 4px 0 0; }
.land-stale {
  background: var(--warn-soft); color: var(--warn); border: 1px solid var(--warn);
  border-radius: var(--r-sm); padding: 8px 12px; font-size: .84rem; margin-bottom: 14px;
}
.land-tabs { display: flex; gap: 4px; background: var(--panel-2); border-radius: var(--r-sm); padding: 4px; margin-bottom: 20px; }
.land-tab {
  flex: 1; border: 0; background: transparent; color: var(--dim);
  padding: .5em; border-radius: 8px; font-size: .88rem; font-weight: 500;
  transition: background var(--dur-1), color var(--dur-1);
}
.land-tab.on { background: var(--panel); color: var(--txt); box-shadow: var(--shadow); }
.field { margin-bottom: 14px; }
.field label { display: block; font-size: .8rem; color: var(--dim); margin-bottom: 5px; font-weight: 500; }
.field input {
  width: 100%; border: 1px solid var(--line-2); background: var(--panel);
  border-radius: var(--r-sm); padding: .58em .8em; font-size: .95rem; outline: none;
  transition: border-color var(--dur-1), box-shadow var(--dur-1);
}
.field input:focus { border-color: var(--pri); box-shadow: 0 0 0 3px var(--pri-soft); }
.land-namehint { font-size: .78rem; min-height: 1.2em; margin: -8px 0 12px; color: var(--dim2); transition: color var(--dur-1); }
.land-namehint.ok { color: var(--ok); }
.land-namehint.bad { color: var(--warn); }
.land-btn {
  width: 100%; border: 0; border-radius: var(--r-sm);
  background: linear-gradient(120deg, var(--pri), #0EA5E9);
  color: #fff; font-size: 1rem; font-weight: 600; padding: .72em;
  transition: filter var(--dur-1), transform var(--dur-1);
}
.land-btn:hover { filter: brightness(1.06); }
.land-btn:active { transform: translateY(1px); }
.land-btn[disabled] { opacity: .6; pointer-events: none; }
.land-err { color: var(--bad); font-size: .84rem; min-height: 1.3em; margin-top: 12px; }
/* ★ 合规区的字号单独提一档（0.76 → 0.8125rem，12.16px → 13px）。
   impeccable 报 tiny-text 时点到过这里，但**没有全量改 0.68–0.78rem 那 120 多处** ——
   那些是标签/徽章/时间戳，改了会改变整个界面的密度与布局，属于另一个工程。
   这里必须改，因为它承载的是法律提示：「不满 14 周岁，请由监护人阅读并同意后再使用」
   是一条不能因为字号小就看不清的信息。 */
.land-foot { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--line); color: var(--dim2); font-size: .8125rem; text-align: center; }
/* 用行内排版而不是 flex —— flex 换行会把「·」分隔符甩到行首。
   nowrap：链接标签绝不能折在词中间（320px 下会把「儿童个人信息保护规则」断成「…保护规/则」）。 */
.land-legal { margin-bottom: 7px; line-height: 1.9; }
.land-legal a { color: var(--dim); text-decoration: underline; text-underline-offset: 3px; white-space: nowrap; }
.land-legal a:hover { color: var(--pri); }
.land-sep { color: var(--line-2); margin: 0 5px; }
.land-note { color: var(--dim2); margin-bottom: 7px; line-height: 1.55; }
.land-meta { color: var(--dim2); line-height: 1.6; }
.land-link { border: 0; background: transparent; color: var(--dim); font-size: .8125rem; text-decoration: underline; text-underline-offset: 3px; padding: 0; }
.land-link:hover { color: var(--pri); }

/* ══════════════════════════════════════════════════════════════════
   空间门视觉升级（2026-10-04）：把「登录」重写成「把想不明白的地方解开」
   ──────────────────────────────────────────────────────────────────
   两条纪律，是这一整块能安全加在生产页面上的前提：

   1. **只追加覆盖，不改原规则。** 上面的 .land* 每一条都有断言守着
      （抬头在卡外、立场句存在、合规入口可见、字段顺序固定…）。
      改原规则等于和断言对赌；追加覆盖只在后面生效，断言读到的 DOM 一字未动。
   2. **一律加 `.land` 作用域。** `.field` / `.btn` / `.acct-*` 是
      主界面与登录页**共用**的类，动它们会连带改掉学生正在用的界面。

   DOM 只加了一个 <h1 class="land-h1">，其余元素 / id / 顺序 / 注释全部原样。
   ══════════════════════════════════════════════════════════════════ */

/* 两栏：左标题 / 右卡片。
   `display:contents` 是关键 —— 它让 .land-head 与 .land-card **跳过父容器**
   直接成为 .land 的 grid item，于是两栏布局一行 DOM 都不用改。
   原 .land-col 的 width/margin 随之失效（这正是我们想要的：它本来只为单列服务）。 */
.land {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(372px, 428px);
  align-items: center;
  gap: clamp(28px, 5vw, 88px);
  padding: clamp(28px, 6vh, 72px) clamp(24px, 7vw, 108px);
  justify-content: center;
}
.land-col { display: contents; }
.land-head { margin-bottom: 0; text-align: left; max-width: 34em; }
.land-logo { font-size: 1.5rem; letter-spacing: .08em; }
.land-sub { font-size: .8rem; letter-spacing: .04em; }
.land-stance { margin-top: 18px; max-width: 31em; font-size: .95rem; line-height: 1.85; }

/* 主标题：把平台的灵魂句放大成第一眼的视觉锤 */
.land-h1 {
  margin: .34em 0 0;
  font-size: clamp(38px, 4.7vw, 62px);
  line-height: 1.08; font-weight: 800; letter-spacing: -.025em;
  color: var(--txt);
}
.land-h1 em { font-style: normal; color: var(--pri); }

/* 卡片：半透明毛玻璃。结在背后隐约透出来，但文字层始终压在最上面 ——
   可读性靠「不透明的内容层」保证，不靠「祈祷线别太亮」。 */
.land-card {
  width: 100%;
  background: var(--panel);                                  /* 老浏览器兜底 */
  background: color-mix(in srgb, var(--panel) 74%, transparent);
  -webkit-backdrop-filter: blur(18px) saturate(1.25);
  backdrop-filter: blur(18px) saturate(1.25);
  border-radius: 22px; padding: 26px 28px 20px;
  box-shadow: 0 24px 68px rgba(15, 23, 42, .26);
  animation: rise var(--dur-4) var(--ease-out) both;
}
.land-bg::after {
  background:
    radial-gradient(58% 52% at 72% 42%, transparent 0%, transparent 42%, var(--bg) 96%),
    linear-gradient(180deg, transparent 60%, var(--bg) 100%);
}

/* 页签：滑动胶囊（.on 是既有状态类，JS 只切这个，不能改） */
.land-tabs {
  position: relative; gap: 2px; border-radius: 999px;
  background: var(--panel-2); padding: 5px; margin-bottom: 20px;
}
/* ★★ 2026-10-05 用户反馈「页签字体看不清楚」。
   先量后定（真 Chromium 实测）：
     · 选中项 color = rgb(255,255,255) 压蓝渐变 ⇒ 5.93:1，**过 AA**；
     · 未选中项 color = rgb(78,87,104) 压 --panel-2 ⇒ 6.56:1，**过 AA**。
   ⇒ **不是颜色问题**。真因是 **font-size 只有 14.19px 且字重 400** ——
     汉字在低分辨率 / 缩放下笔画细就发糊，而页签又是全站第一眼要看的东西。
   对策：**加粗到 600**（比加深颜色更能提升汉字可读性）+ 未选中项改用 --dim2
   （#5C6576，比 --dim 深一档）拉开与选中项白字的**明度层次**，
   让人一眼看出哪个是当前项。 */
.land-tab {
  position: relative; z-index: 1; border-radius: 999px;
  padding: .52em .3em; font-size: .86rem; font-weight: 600;
  line-height: 1.25; transition: color var(--dur-3);
}
.land-tab.on { background: transparent; color: #fff; box-shadow: none; }
/* 胶囊位移：由 JS 写 --pill-x（app.js 的 moveLandPill）。
   ★★ 2026-10-05 修：此前**没有任何地方设置过 transform** ⇒ 胶囊恒在第 1 格。
   选中第 2/3 格时，胶囊压住的是已经变灰的第 1 格，实测对比度 1.14:1（灰字压蓝），
   看起来就是「一选中就看不见了」。
   为什么用 translateX(var(--pill-x)) 而不是 translateX(calc(100% * var(--i)))：
   三格之间有 gap、容器有 padding，100%/3 与实际格宽差几像素，累积到第三格会明显偏。
   JS 直接量出真实左边界差更准，也顺带把窗口 resize 一起管了。
   兜底：--pill-x 未设置时回落到第 1 格（旧行为），不会突然乱跳。 */
.land-tabs::before {
  content: ""; position: absolute; z-index: 0; top: 5px; left: 5px;
  width: calc((100% - 10px) / 3); height: calc(100% - 10px);
  border-radius: 999px; background: linear-gradient(120deg, var(--btn-from), var(--btn-to));
  box-shadow: 0 4px 14px rgba(37, 99, 235, .3);
  transform: translateX(var(--pill-x, 0px));
  transition: transform var(--dur-4) var(--ease-io);
}
/* ★ 非选中项压深一档（--dim2 而非 --dim）：小字号下更清楚，也和选中项的白字拉开层次。
   ★ JS 只切 .on，胶囊位移仍由既有的 transform 规则（下方媒体查询/J S 侧）负责，未改动。 */
.land-tab:not(.on) { color: var(--dim2); }

/* 输入框：底线式。聚焦时底线铺满并发光，而不是套一个框 */
.land .field { position: relative; margin-bottom: 18px; }
.land .field label { font-size: .8rem; margin-bottom: 2px; }
.land .field input {
  border: 0; border-bottom: 1.5px solid var(--line-2);
  background: transparent; border-radius: 0;
  padding: .5em 0 .55em; font-size: 1.02rem;
  transition: border-color var(--dur-3);
}
.land .field input:focus {
  border-color: transparent; box-shadow: none;
  border-bottom-color: var(--pri);
}
.land .field::after {
  /* ★ 用 transform: scaleX 而不是 width —— width 是**布局属性**，
     动画它每帧都触发重排/reflow（impeccable 的 layout-transition 规则点到了这一行，
     而且这是我自己写的那处）。scaleX 走合成层，不触发布局。
     顺带把 box-shadow 也去掉了：同一条规则还命中 dark-glow，
     而输入框底线本来就不需要发光。 */
  content: ""; position: absolute; left: 0; bottom: 0; height: 1.5px; width: 100%;
  transform: scaleX(0); transform-origin: left center;
  background: linear-gradient(90deg, var(--pri), #0EA5E9);
  transition: transform var(--dur-4) var(--ease-out); pointer-events: none;
}
.land .field:focus-within::after { transform: scaleX(1); }
.land .field input::placeholder { color: var(--dim2); }

/* 主按钮：保留全宽（不能改成「悬停才展开」的圆钮 —— 家长用户看不到文字会不敢点） */
.land-btn {
  border-radius: 12px; padding: .8em; font-size: .98rem; font-weight: 600;
  /* ★ 白字压在渐变上，末端色必须够深：#0EA5E9 只有 2.77:1（impeccable 实测 6 处命中）。
     改用 --btn-from/--btn-to（见 :root）⇒ 白字 5.93:1，视觉仍是「蓝到青」的家族色。
     只改**有白字/白图标压在上面**的渐变；logo 渐变字、底线等装饰性的保持原样。 */
  background: linear-gradient(120deg, var(--btn-from), var(--btn-to));
  box-shadow: 0 8px 22px rgba(37, 99, 235, .26);
  transition: filter var(--dur-2), transform var(--dur-1), box-shadow var(--dur-2);
}
.land-btn:hover { filter: brightness(1.05); box-shadow: 0 10px 28px rgba(37, 99, 235, .34); }

/* 合规文字是信息性内容，颜色不能淡到读不出来（尤其 14 岁监护人条款） */
.land-foot { color: var(--dim); }
.land-note, .land-meta { color: var(--dim); }

/* ★ 空间门的响应式规则**故意放在文件末尾**（见文末「空间门窄屏」块）。
   原因：app.css 里有两条断言是「位置型」的 ——
     _p25check  : .skill-c .sc-ico 必须排在**第一个** @media 之前
     _parity7check: .side-mask{display:none} 必须排在**第一个** @media(max-width) 之前
   它们的用意（媒体查询会盖掉同权重的基础规则）是对的，但比较基准是
   「文件里第一个 @media 的位置」。在文件中部插一个 @media 会把基准提前，
   让这两条**假红**（实际行为毫无变化 —— 我们的 media 里只有 .land* 规则）。
   把 media 挪到末尾 = 不动任何既有规则的相对顺序，两条断言都恢复原状。 */

/* ================= 主框架 ================= */
.app { display: flex; height: 100vh; overflow: hidden; }

.side {
  width: var(--side-w); flex: 0 0 var(--side-w);
  background: var(--panel);
  border-right: 1px solid var(--line);
  display: flex; flex-direction: column;
  overflow: hidden;
  transition: width var(--dur-2) ease, flex-basis var(--dur-2) ease;
}
/* 侧栏顶部只留品牌名 —— 空间卡 / 成长条 / 公告已收进右上角「我的」菜单
   （2026-10-04）。原来那块占 190px 竖向空间，而项目与对话列表才是学生天天在用的。 */
/* ★ 品牌图形与文字**并排**（2026-10-04 起）：原来是竖排（图形独占一行 + 名 + 副标），
   268px 宽的侧栏里白吃掉一行高度。并排后顶部省下 26px，刚好让给项目列表。
   文字仍分两行（名在上、副标在下），靠 .brand-tx 这个竖向 flex 容器收住。 */
.brand { display: flex; align-items: center; gap: 9px; padding: 14px 16px 12px; border-bottom: 1px solid var(--line); }
/* ★ 侧栏品牌图形：高度压到 26px —— 这里寸土寸金（268px 宽的侧栏，
   顶部每一像素都要让给项目/对话列表）。图形只做识别，不做主角。 */
.brand-logo { display: block; width: auto; height: 26px; flex: none; }
.brand-tx { display: flex; flex-direction: column; min-width: 0; }
.brand-tx > b { font-size: 1.05rem; font-weight: 700; letter-spacing: .02em; line-height: 1.2; }
.brand-tx > small { color: var(--dim2); font-size: .72rem; margin-top: 1px; }
/* .pet-line 的基准样式保留：成长条现在渲染在「我的」菜单里，用的是同一个类。 */
.pet-line { font-size: .76rem; color: var(--dim); display: flex; align-items: center; gap: 6px; cursor: default; }
/* 批次114d：`.pet-ico` 从手绘 SVG 换成 AI 插画（<img>）。
   ★ 尺寸原来来自 SVG 自己的 width/height 属性，<img> 没有这套 ⇒ 必须在这里定死。
     基准 18px（成长条里那一枚），其余调用点各自覆盖：
       .mf-pet 30px（练习记录）/ .rw-ico 28px（奖励浮层）/ .av-cell 48px（换形态弹窗）。
   🔴 漏掉任何一条，那个位置的图会按原图 512px 渲染、把布局撑爆。
   ★★ 批次117 整体放大（用户原话「插画太小」）：换形态格 34→48、头像格 34→44、
     .mf-pet 25→30（连盒子 27→32）、.rw-ico 22→28、基准 15→18。**每条都要同步断言**。 */
.pet-ico { display: block; width: 18px; height: 18px; object-fit: contain; }
.pet-line .pet-ico { font-size: .95rem; line-height: 1; }
.pet-line .pet-bar { flex: 1; height: 4px; background: var(--panel-3); border-radius: 3px; overflow: hidden; }
.pet-line .pet-bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--ok), #86EFAC); border-radius: 3px; transition: width var(--dur-4) var(--ease-out); }

/* ---- 鱼缸（批次105）----
   ★ 上色**一律走 CSS 变量**（见 :root / [data-theme="dark"] 的 --tk-*），
     和「学习岛屿」同一条规矩：JS 里写死色值就会出现"浅色下好看、深色下刺眼"。
   ★ 整张图**不做任何动画** —— 气泡、光柱都是静态的。
     刻意的：这是"看看自己养成了什么样"的一页，不是游戏画面；
     动效越多，越容易让人以为"在这里待着能涨成长值"（而成长值只来自掌握）。
     顺带也就自动满足了 prefers-reduced-motion。 */
.tk-svg { display: block; width: 100%; height: auto; border-radius: 14px; }
.tk-water { fill: var(--tk-water); }
.tk-deep { fill: var(--tk-water-2); }
.tk-glass { fill: none; stroke: var(--tk-glass); stroke-width: 3; }
.tk-surface { fill: none; stroke: var(--tk-surface); stroke-width: 2.5; stroke-linecap: round; }
.tk-wave { fill: none; stroke: var(--tk-wave); stroke-width: 2; stroke-linecap: round; opacity: .45; }
.tk-ray { fill: var(--tk-ray); opacity: .16; }
.tk-weed { fill: none; stroke: var(--tk-weed); stroke-width: 3.4; stroke-linecap: round; }
.tk-weed-deco { fill: none; stroke: var(--tk-weed-deco); stroke-width: 4; stroke-linecap: round; }
.tk-coral { fill: var(--tk-coral); }
.tk-lamp-glow { fill: var(--tk-lamp-glow); }
.tk-bubble { fill: var(--tk-bubble); opacity: .5; }
.tk-school { color: var(--tk-school); }
.tk-body { fill: var(--tk-body); }
.tk-fin { fill: var(--tk-fin); }
.tk-tail-whip { fill: var(--tk-tail-whip); }
.tk-band { fill: var(--tk-band); opacity: .8; }
.tk-eye { fill: var(--tk-eye); }
.tk-pupil { fill: var(--tk-pupil); }

/* ★★ 批次141：神话线（蛟 stage6 / 龙 stage7 / 苍龙 stage8）的部件样式。
   全部只用 CSS 变量上色 —— 深色主题跟着 [data-theme] 自动切，
   在 JS 里写死色值就会出现「浅色下好看、深色下刺眼」（同 tk-* 那套规矩）。 */
.tk-horn   { fill: none; stroke: var(--tk-horn); stroke-width: 2.2; stroke-linecap: round; }
.tk-horn2  { opacity: .62; }
.tk-claw   { fill: none; stroke: var(--tk-claw); stroke-width: 1.8; stroke-linecap: round; }
.tk-leg    { fill: none; stroke: var(--tk-leg); stroke-width: 3; stroke-linecap: round; }
.tk-dscale { fill: none; stroke: var(--tk-dscale); stroke-width: 1.2; opacity: .55; }
.tk-dfin   { fill: var(--tk-dfin); }
.tk-dmane  { fill: var(--tk-dmane); }
.tk-wing   { fill: var(--tk-wing); opacity: .82; }
.tk-wing2  { opacity: .6; }
.tk-whisk  { fill: none; stroke: var(--tk-whisk); stroke-width: 1.4; stroke-linecap: round; opacity: .85; }
.tk-brow   { fill: none; stroke: var(--tk-brow); stroke-width: 2; stroke-linecap: round; }
.tk-cloud  { fill: var(--tk-cloud); opacity: .7; }

/* ★ 喷水是**填充的羽状水花**，不是描边线条 —— 线条太细会读成"须"（见 whaleArt 的注释）。 */
.tk-spout { fill: var(--tk-spout); opacity: .82; }
.tk-spray { fill: var(--tk-spray); opacity: .9; }
.tk-belly { fill: var(--tk-belly); opacity: .55; }
.tk-spot { fill: var(--tk-spot); opacity: .7; }
.tk-lateral { fill: none; stroke: var(--tk-lateral); stroke-width: 3; stroke-linecap: round; opacity: .6; }
.tk-mouth { fill: none; stroke: var(--tk-mouth); stroke-width: 3; stroke-linecap: round; opacity: .75; }

/* 批次130：只让鱼和配件一起缓慢游动，水体、光柱、气泡与缸底保持静止。 */
.tk-swim {
  transform-box: view-box;
  transform-origin: 236px 186px;
  animation: tk-swim 11s ease-in-out infinite;
  will-change: transform;
}
@keyframes tk-swim {
  0%   { transform: translate(0, 0) rotate(0deg); }
  20%  { transform: translate(66px, -24px) rotate(-6deg); }
  42%  { transform: translate(134px, 6px) rotate(0deg); }
  62%  { transform: translate(186px, -20px) rotate(5deg); }
  82%  { transform: translate(96px, 22px) rotate(-3deg); }
  100% { transform: translate(0, 0) rotate(0deg); }
}

/* ===== 批次299：全班大海（班级里"看见全班角色"的全屏视图） =====
   用户原话：「我想在班级里边增加一个可以看见全班角色的大海，支持全屏展示」。

   ★ 配色一律走 `--tk-*`（鱼缸那一组）：海是"自然景物"，品牌换肤不该把海换成紫色。
     深色主题只覆盖变量，绘制规则一条都不重写 —— 与批次105/101 同一条理由。
   ★ 留白（viewBox 之外）用 `--tk-water-2`：`preserveAspectRatio="xMidYMid meet"`
     在宽屏上左右会留白，用"更深的水"接上，看上去是水更深，不是黑边。 */
.sea-wrap {
  position: fixed; inset: 0; z-index: 9000;
  display: flex; flex-direction: column;
  background: var(--tk-water-2);
}
.sea-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 16px; background: var(--bg); color: var(--txt);
  border-bottom: 1px solid var(--line);
}
.sea-bar b { font-size: 1rem; }
.sea-cnt { font-size: .8rem; color: var(--dim); }
/* 批次309：海里实时在线人数 —— 让"同步通没通"一眼可见，不用靠感觉猜。 */
.sea-live { font-size: .8rem; color: var(--dim); white-space: nowrap; }
.sea-live::before { content: ''; display: inline-block; width: 6px; height: 6px;
  border-radius: 50%; background: #35c76e; margin-right: 4px; vertical-align: 1px; }
.sea-bar .sp { flex: 1; }
.sea-stage { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.sea-svg { display: block; width: 100%; height: 100%; }

/* ===== 批次304：全班大海的三个互动（点鱼看名片 / 我在这 / 撒鱼食）=====
   ★ 三样都不挂任何奖励：不给成长值、不给露珠、不写服务端状态。
     它是"我碰一下，世界有反应"，不是又一个刷分口子。 */
.sea-fish { cursor: pointer; }
/* 撒鱼食的位移只动这一层：外层 `sea-fish` 的 transform 被游动动画占着，
   两个 transform 写在同一元素上必然互相覆盖（批次301 的教训）。 */
.sea-bait { transition: transform var(--dur-glide) var(--ease-io); }
/* 选中的鱼 / 被"我在这"点到的鱼：用 drop-shadow 打光，不改几何
   （画光圈要自己算坐标，鱼还在游，光圈会飘） */
.sea-on { filter: drop-shadow(0 0 7px rgba(255, 255, 255, .95)) drop-shadow(0 0 3px var(--pri)); }
.sea-hi { filter: drop-shadow(0 0 10px #fff) drop-shadow(0 0 6px var(--pri)); }
.sea-ping {
  fill: none; stroke: var(--pri); stroke-width: 3; pointer-events: none;
  transform-box: fill-box; transform-origin: center;
  animation: seaPing 1.8s ease-out both;
}
@keyframes seaPing {
  0%   { transform: scale(.35); opacity: .95; stroke-width: 4; }
  70%  { transform: scale(1.6);  opacity: .30; stroke-width: 2; }
  100% { transform: scale(2.1);  opacity: 0;   stroke-width: 1; }
}
.sea-food { pointer-events: none; }
.sea-food circle { fill: #FFD98A; animation: seaFood 2.6s ease-out both; }
@keyframes seaFood {
  0%   { opacity: 0;   transform: translateY(6px); }
  15%  { opacity: .95; transform: translateY(0); }
  75%  { opacity: .90; }
  100% { opacity: 0;   transform: translateY(-8px); }
}
.sea-hint {
  position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%);
  padding: 5px 12px; border-radius: 999px; font-size: .74rem; white-space: nowrap;
  color: var(--dim); background: rgba(255, 255, 255, .72); pointer-events: none;
}
.sea-card {
  position: absolute; z-index: 3; min-width: 148px; max-width: 240px;
  padding: 10px 12px; border-radius: 12px; background: var(--panel);
  border: 1px solid var(--line); box-shadow: 0 8px 26px rgba(15, 25, 45, .18);
  font-size: .82rem; line-height: 1.45;
}
.sea-card[hidden] { display: none; }
.sea-card-hd { display: flex; align-items: center; gap: 6px; }
.sea-card-hd b { font-size: .92rem; color: var(--txt); }
.sea-card-me {
  font-style: normal; font-size: .68rem; padding: 1px 5px; border-radius: 4px;
  color: var(--pri); background: var(--pri-soft);
}
.sea-card-on { margin-left: auto; font-size: .7rem; color: var(--dim); white-space: nowrap; }
.sea-card-on::before {
  content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%;
  background: var(--dim); margin-right: 4px; vertical-align: 1px; opacity: .5;
}
.sea-card-on.on { color: var(--ok); }
.sea-card-on.on::before { background: var(--ok); opacity: 1; }
.sea-card-meta { margin-top: 4px; color: var(--dim); font-size: .76rem; }
.sea-card-badges { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
.sea-card-badges i {
  font-style: normal; font-size: .68rem; padding: 1px 6px; border-radius: 999px;
  color: var(--dim); background: var(--bg); border: 1px solid var(--line);
}
.sea-card-none { color: var(--dim); font-size: .72rem; border: 0; background: none; }

/* ★ 批次305：打招呼。鱼身亮一下 + 头顶浮起 👋（1.6s 自消）。
   ★ 光晕用 filter 不用 transform —— `.sea-fish` 的 transform 被游动动画占着，
     再叠一个 transform 动画会互相覆盖（同「我在这」那个环的坑）。
   ★ 👋 插进那条鱼里（跟鱼一起游动），不自己算绝对坐标。 */
.sea-wave { filter: drop-shadow(0 0 8px #FFD98A) drop-shadow(0 0 4px #fff); }
.sea-wave-emoji {
  font-size: 34px; text-anchor: middle; pointer-events: none;
  transform-box: fill-box; transform-origin: center bottom;
  animation: seaWaveUp 1.5s ease-out both;
}
@keyframes seaWaveUp {
  0% { opacity: 0; transform: translateY(6px) scale(.7); }
  20% { opacity: 1; transform: translateY(0) scale(1); }
  100% { opacity: 0; transform: translateY(-26px) scale(1.05); }
}
.sea-greet-btn {
  display: block; width: 100%; margin-top: 8px; padding: 6px 10px;
  border: 1px solid var(--pri); border-radius: 9px; background: var(--pri-soft);
  color: var(--pri); font: inherit; font-size: .78rem; cursor: pointer;
  transition: background var(--dur-1), transform var(--dur-1);
}
.sea-greet-btn:hover { background: var(--pri); color: #fff; }
.sea-greet-btn:active { transform: scale(.97); }

/* ═══ 批次315：全班大海 · 聊天 ═══════════════════════════════════
   用户原话：「班级新增聊天功能，允许在大海里聊天」
   拍板：全班公开（不做私聊）+ 完整聊天室全部留档。

   ★ 为什么是**并排**而不是把面板浮在海面上：
     浮层会盖住一部分鱼 —— "点鱼看名片"点不到，而且窄屏上半个班看不见。
     用 flex 让海真的让出宽度，SVG 靠 `preserveAspectRatio=meet` 自己等比缩。
   ★ 配色全走主题变量：这里有一条 `.sea-wrap` 的水色底，写死深色/浅色都会
     在另一套主题下变成"看不见的字"（批次73 在这上面栽过）。
   ★ 面板里的所有正文都用 `white-space: pre-wrap` 处理换行 ——
     不拼 `<br>`，因为正文是 `HL.esc` 过的纯文本，拼标签等于给自己开一个注入口子。 */
.sea-body { flex: 1; min-height: 0; display: flex; }
.sea-chat {
  /* ★ 批次316：举报/清空/@ 三个浮层都绝对定位在面板内部 ⇒ 这里要当定位上下文。 */
  position: relative;
  flex: 0 0 340px; min-width: 0; display: flex; flex-direction: column;
  background: var(--bg); border-left: 1px solid var(--line);
}
.sea-chat[hidden] { display: none; }
.sea-chat-hd {
  display: flex; align-items: center; gap: 8px; flex: none;
  padding: 9px 12px; border-bottom: 1px solid var(--line);
}
.sea-chat-hd b { font-size: .9rem; color: var(--txt); }
.sea-chat-sub { font-size: .7rem; color: var(--dim); }
.sea-chat-hd .sp { flex: 1; }
.sea-chat-x {
  width: 24px; height: 24px; flex: none; padding: 0; line-height: 22px;
  border: 0; border-radius: 6px; background: transparent; color: var(--dim);
  font-size: 16px; cursor: pointer;
}
.sea-chat-x:hover { background: var(--line); color: var(--txt); }
.sea-chat-log {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column; gap: 8px; padding: 10px 12px;
}
.sea-msg { max-width: 92%; }
/* 自己说的话靠右 —— 与"全班公告板"不冲突：左右只区分说话人，不区分可见范围。 */
.sea-msg.me { align-self: flex-end; }
.sea-msg-hd { display: flex; align-items: center; gap: 6px; font-size: .68rem; color: var(--dim); }
.sea-msg-hd b { font-size: .72rem; color: var(--txt); font-weight: 600; }
.sea-msg.me .sea-msg-hd { flex-direction: row-reverse; }
.sea-msg-tx {
  margin-top: 2px; padding: 6px 10px; border-radius: 10px;
  background: var(--line); color: var(--txt);
  font-size: .82rem; line-height: 1.5;
  white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere;
}
.sea-msg.me .sea-msg-tx { background: var(--pri-soft); color: var(--txt); }
/* 删除按钮只在 hover 时显形：老师看得到、学生根本看不到（isOwner 才渲染）。 */
.sea-msg-del {
  width: 18px; height: 18px; padding: 0; line-height: 16px; flex: none;
  border: 0; border-radius: 5px; background: transparent; color: var(--dim);
  font-size: 13px; cursor: pointer; opacity: 0; transition: opacity var(--dur-1);
}
.sea-msg:hover .sea-msg-del { opacity: 1; }
.sea-msg-del:hover { background: var(--bad-soft, #FDECEC); color: var(--bad, #C0392B); }
/* 被老师移除的那条：留一个占位，不整条消失 ——
   整条消失会让"翻历史"突然少一段，看起来像消息丢了。 */
.sea-msg.del { align-self: center; max-width: 100%; }
.sea-msg-sys { font-size: .72rem; color: var(--dim); font-style: italic; }
.sea-chat-more { flex: none; padding: 0 12px; }
.sea-chat-more button {
  width: 100%; margin-bottom: 4px; padding: 4px 0; border: 0; border-radius: 7px;
  background: transparent; color: var(--dim); font: inherit; font-size: .74rem; cursor: pointer;
}
.sea-chat-more button:hover { background: var(--line); color: var(--txt); }
.sea-chat-more button[hidden] { display: none; }
.sea-chat-in {
  flex: none; display: flex; align-items: flex-end; gap: 8px;
  padding: 8px 10px; border-top: 1px solid var(--line);
}
.sea-chat-in textarea {
  flex: 1; min-width: 0; max-height: 96px; padding: 7px 10px; resize: none;
  border: 1px solid var(--line); border-radius: 9px; background: var(--bg);
  color: var(--txt); font: inherit; font-size: .82rem; line-height: 1.45;
}
.sea-chat-in textarea:focus { outline: none; border-color: var(--pri); }
.sea-chat-tip { flex: none; padding: 0 12px 8px; font-size: .72rem; color: var(--dim); min-height: 0; }
.sea-chat-tip.on { min-height: 18px; }
.sea-chat-tip.err { color: var(--bad, #C0392B); }
/* 未读角标：只在面板收起时出现，关掉海面就清零（不落服务端状态）。 */
.sea-chat-n {
  display: inline-block; min-width: 16px; height: 16px; margin-left: 5px;
  padding: 0 4px; border-radius: 999px; background: var(--bad, #C0392B);
  color: #fff; font-size: .64rem; font-style: normal; line-height: 16px; text-align: center;
}
.sea-chat-n[hidden] { display: none; }
/* 批次32x：海聊内嵌答题赛 */
.sea-quiz { margin: 6px 0 2px; }
.sea-quiz-q { border: 1px solid var(--pri, #2B5CFF); border-radius: 12px; background: var(--pri-soft, #EEF2FF); padding: 10px 12px; }
.sea-quiz-tag { font-size: .72rem; color: var(--pri, #2B5CFF); font-weight: 700; margin-bottom: 4px; }
.sea-quiz-prompt { font-weight: 600; color: var(--txt); margin-bottom: 8px; line-height: 1.5; }
.sea-quiz-opts { display: flex; flex-wrap: wrap; gap: 6px; }
.sea-quiz-opt {
  border: 1px solid var(--line); background: #fff; color: var(--txt);
  border-radius: 9px; padding: 6px 10px; font: inherit; font-size: .82rem; cursor: pointer;
  text-align: left; max-width: 100%;
}
.sea-quiz-opt:hover:not(:disabled) { border-color: var(--pri, #2B5CFF); }
.sea-quiz-opt:disabled { opacity: .5; cursor: not-allowed; }
.sea-quiz-hint { margin-top: 6px; font-size: .7rem; color: var(--dim); }
.sea-quiz-s { border-radius: 12px; padding: 8px 12px; font-size: .82rem; }
.sea-quiz-s.ok { background: #E7F8ED; color: #1F8A4C; }
.sea-quiz-s.no { background: #FDECEC; color: #C0392B; }
.sea-quiz-an { margin-top: 4px; color: var(--txt); opacity: .85; }
.sea-quiz-sum { border-radius: 12px; padding: 10px 12px; background: var(--pri-soft, #EEF2FF); color: var(--pri, #2B5CFF); font-weight: 600; }
/* 批次32y-4：秒选（点得太快、这题不算分）—— 中性色，不能画成"答错"的红。 */
.sea-quiz-s.tip { background: #FFF7E0; color: #8A6100; }
/* 批次32y-4：扣露珠的提示。用 warn 色，与正误色区分开（红是"答错"，黄是"代价"）。 */
.sea-quiz-warn { margin-top: 4px; font-size: .75rem; color: var(--warn, #D97706); font-weight: 600; }
/* 批次32y-4：组队 PK 本轮排行榜（名次 / 名字 / 答对几题 / 成长值）。 */
.sea-quiz-rk { list-style: none; margin: 8px 0 0; padding: 0; }
.sea-quiz-rk li {
  display: flex; align-items: center; gap: 8px;
  padding: 4px 6px; border-radius: 8px; font-size: .8rem; font-weight: 600;
}
.sea-quiz-rk li + li { margin-top: 2px; }
.sea-quiz-rk li.top { background: #FFF7E0; color: #8A6100; }
.sea-quiz-rk li i {
  width: 18px; height: 18px; flex: none; border-radius: 50%;
  background: var(--line); color: var(--dim); font-style: normal;
  font-size: .7rem; line-height: 18px; text-align: center;
}
.sea-quiz-rk li.top i { background: #F5B301; color: #fff; }
.sea-quiz-rk li b { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sea-quiz-rk li span { color: var(--dim); font-weight: 500; white-space: nowrap; }
.sea-quiz-rk li em { font-style: normal; color: #1F8A4C; white-space: nowrap; }
/* 批次32y-5：组队 PK 邀请卡 —— 卡片上直接给「加入队伍」，不用再手抄队伍号。
   ★ 加在既有的 .sea-quiz-* 块里，**不新增媒体查询块**（多套件靠 lastIndexOf 定位窄屏回退）。 */
.sea-quiz-inv { display: flex; flex-direction: column; gap: 6px; }
.sea-quiz-join {
  align-self: flex-start; margin-top: 2px; padding: 6px 16px;
  border: 0; border-radius: 999px; background: #2F6BFF; color: #fff;
  font-size: .82rem; font-weight: 700; cursor: pointer;
}
.sea-quiz-join:hover { background: #245AD8; }
.sea-quiz-join:disabled { background: #E3E7EF; color: #8A94A6; cursor: default; }
/* 批次32y-6：组队 PK「本题结果」卡 —— 谁答对、谁答错、谁没答。
   ★ 同样加在既有的 .sea-quiz-* 块里，**不新增媒体查询块**。
   ★ 这几种状态色沿用结算卡那套（对=绿 / 错=红 / 没答=灰），
     孩子已经认得，不必再发明一套新颜色。 */
.sea-quiz-mk { border: 1px solid var(--line-2, #E3E7EF); border-radius: 12px; background: #F7F9FF; padding: 8px 12px; }
.sea-quiz-mkl { list-style: none; margin: 6px 0 8px; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.sea-quiz-mkl li {
  display: inline-flex; align-items: baseline; gap: 6px;
  padding: 3px 10px; border-radius: 999px; font-size: .8rem;
  background: #EFF1F5; color: var(--dim, #5C6576);
}
.sea-quiz-mkl li b { font-weight: 700; color: var(--txt, #1F2430); }
.sea-quiz-mkl li.mk-ok { background: #E7F8ED; color: #1F8A4C; }
.sea-quiz-mkl li.mk-no { background: #FDECEC; color: #C0392B; }
.sea-quiz-mkl li.mk-none { background: #EFF1F5; color: #8A94A6; }
.sea-quiz-key { font-size: .82rem; color: var(--txt, #1F2430); }
.sea-quiz-key b { color: var(--pri, #2B5CFF); }
/* 已作答的整组选项一起灰掉 —— 只灰被点的那一个，会让人以为还能改答案。 */
.sea-quiz-opt.done { opacity: .45; }
#seaQuizBtn { white-space: nowrap; }
/* ★ 批次316：有人 @ 你时角标换成提醒色 —— "有人在叫你"和"又有几句闲话"
   是两件事，用同一个红色会让孩子分不清该先看哪个。 */
.sea-chat-n.at { background: var(--warn, #D97706); }

/* ============ 批次316：@ 同学 / 举报 / 一键清空 ============ */

/* 头部两个老师专用小按钮（学生那边带 hidden，压根不占位）。 */
.sea-chat-op {
  flex: none; padding: 2px 7px; border: 1px solid var(--line); border-radius: 999px;
  background: transparent; color: var(--dim); font: inherit; font-size: .68rem;
  line-height: 16px; cursor: pointer;
}
.sea-chat-op[hidden] { display: none; }
.sea-chat-op:hover { background: var(--line); color: var(--txt); }
.sea-chat-op .sea-chat-n { margin-left: 4px; }

/* 正文里被 @ 到的名字。只加粗+品牌色，不加底色（气泡本身已经有底色了）。 */
.sea-at { color: var(--pri); font-weight: 600; }

/* 每条别人消息上的举报按钮：与删除按钮同一套"hover 才显形"。
   ★ 已举报的（.on）常驻显示 —— 否则用户下次又点一遍，还以为没提交成功。 */
.sea-msg-flag {
  margin-left: auto; height: 18px; padding: 0 5px; flex: none;
  border: 0; border-radius: 5px; background: transparent; color: var(--dim);
  font: inherit; font-size: .64rem; line-height: 18px; cursor: pointer;
  opacity: 0; transition: opacity var(--dur-1);
}
.sea-msg:hover .sea-msg-flag { opacity: 1; }
.sea-msg-flag.on { opacity: 1; color: var(--bad, #C0392B); }
.sea-msg-flag:hover { background: var(--bad-soft, #FDECEC); color: var(--bad, #C0392B); }

/* 被举报次数：只有建班人拿得到这个字段，学生那边压根不渲染。 */
.sea-msg-bad { font-size: .64rem; color: var(--bad, #C0392B); }

/* @ 选人浮层：贴在输入框正上方，最多 8 个人、超出滚动。 */
.sea-at-pop {
  flex: none; max-height: 168px; overflow-y: auto;
  margin: 0 10px 2px; padding: 4px; border: 1px solid var(--line); border-radius: 9px;
  background: var(--bg); box-shadow: 0 -6px 16px rgba(0, 0, 0, .08);
}
.sea-at-pop[hidden] { display: none; }
.sea-at-item {
  display: block; width: 100%; padding: 6px 8px; border: 0; border-radius: 7px;
  background: transparent; color: var(--txt); font: inherit; font-size: .8rem;
  text-align: left; cursor: pointer;
}
.sea-at-item.on, .sea-at-item:hover { background: var(--pri-soft); color: var(--pri); }

/* 举报原因 / 举报列表：绝对定位在面板内部（不新开全局 modal ——
   考试作答中 openModal 会把卷子顶掉，而且这块本来就该跟着海面一起关）。 */
.sea-chat-pop {
  position: absolute; left: 10px; right: 10px; bottom: 64px; z-index: 6;
  max-height: 68%; overflow-y: auto;
  border: 1px solid var(--line); border-radius: 12px; background: var(--bg);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .18);
  animation: seaPopIn var(--dur-2) ease-out both;
}
.sea-chat-pop[hidden] { display: none; }
@keyframes seaPopIn {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

/* ============ 批次32x · P1：答题赛扩展弹层（组队PK / 班级榜 / 题报错） ============ */
/* 这些 .qv-* 都是 #seaChatPop 内部的卡片内容；容器定位/底色已由 .sea-chat-pop 提供，
   这里只管内部排版与间距。统一走浅色极简（米白 + 品牌蓝），不堆黑色色块。 */
.qv-menu, .qv-team, .qv-lb, .qv-rep { padding: 14px 14px 12px; display: flex; flex-direction: column; gap: 9px; }
.qv-title { font-weight: 700; color: var(--txt); font-size: .95rem; line-height: 1.4; }
.qv-row { display: flex; align-items: center; gap: 8px; color: var(--txt); font-size: .85rem; }
.qv-row.qv-join { gap: 6px; }
.qv-row input[type="text"], .qv-row input:not([type]), #qvTeamId {
  flex: 1; min-width: 0; padding: 6px 9px; border: 1px solid var(--line); border-radius: 8px;
  background: var(--bg); color: var(--txt); font: inherit; font-size: .85rem;
}
.qv-teamid { font-size: .82rem; color: var(--dim); }
.qv-teamid b { color: var(--pri); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .5px; }
/* 班级榜 / 举报列表行：左文案 + 右操作，分隔线区分。 */
.qv-lb-row, .qv-rep-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 8px 2px; border-top: 1px solid var(--line); font-size: .85rem; color: var(--txt);
}
.qv-lb-row:first-of-type, .qv-rep-row:first-of-type { border-top: none; }
.qv-lb-row > span:last-child { color: var(--dim); font-size: .8rem; white-space: nowrap; }
.qv-rep-row > div { flex: 1; min-width: 0; }
.qv-rep-row [data-acc] { flex: none; }
/* 题卡 / 结算卡里的「⚠ 报错」链接：hover 才显形，常驻会让孩子以为题出错了。 */
.sea-quiz-rep {
  display: inline-block; margin-top: 6px; font-size: .7rem; color: var(--dim);
  cursor: pointer; text-decoration: none; user-select: none;
}
.sea-quiz-rep:hover { color: var(--warn, #D97706); }
.sea-quiz-rep:disabled { opacity: .5; cursor: default; }
.sea-quiz-rep:disabled:hover { color: var(--dim); }
.sea-pop-hd {
  display: flex; align-items: center; gap: 8px; padding: 8px 10px;
  border-bottom: 1px solid var(--line);
}
.sea-pop-hd b { flex: 1; font-size: .84rem; color: var(--txt); }
.sea-pop-q {
  margin: 8px 10px; padding: 6px 9px; border-radius: 8px;
  background: var(--line); color: var(--dim); font-size: .74rem; line-height: 1.5;
  word-break: break-word;
}
.sea-pop-opt {
  display: block; width: calc(100% - 20px); margin: 6px 10px; padding: 8px 10px;
  border: 1px solid var(--line); border-radius: 9px; background: var(--bg);
  color: var(--txt); font: inherit; font-size: .8rem; text-align: left; cursor: pointer;
}
.sea-pop-opt:hover { border-color: var(--pri); background: var(--pri-soft); color: var(--pri); }
.sea-pop-ft { padding: 8px 10px 10px; font-size: .68rem; color: var(--dim); text-align: center; }
.sea-pop-list { padding: 4px 0; }
.sea-pop-item { padding: 8px 10px; border-bottom: 1px solid var(--line); }
.sea-pop-item:last-child { border-bottom: 0; }
.sea-pop-item.new { background: var(--pri-soft); }
.sea-pop-meta { font-size: .68rem; color: var(--dim); }
.sea-pop-meta b { color: var(--txt); }
.sea-pop-tx { margin-top: 3px; font-size: .78rem; color: var(--txt); line-height: 1.45; word-break: break-word; }
.sea-pop-note { margin-top: 3px; font-size: .72rem; color: var(--dim); }
.sea-pop-empty { padding: 22px 10px; text-align: center; font-size: .78rem; color: var(--dim); }
/* ===== 批次321：班级管理浮层（禁言 / 任命管理员）=====
   ★ 一屏要放下几十个成员，所以每行压成**一条**：名字 + 标签 + 状态 + 动作。
     做成"一人一张卡"的话，40 人的班要滚三屏才能找到刷屏的那一个。 */
.sea-pop-hint { padding: 7px 10px; font-size: .68rem; line-height: 1.5; color: var(--dim); background: var(--line); }
.sea-mg-row {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  padding: 7px 10px; border-bottom: 1px solid var(--line);
}
.sea-mg-row:last-child { border-bottom: 0; }
.sea-mg-row b { font-size: .78rem; color: var(--txt); font-weight: 600; }
/* 被禁言那一行整条压暗：管理员一眼能看出"这个人已经处理过了"。 */
.sea-mg-row.muted { background: var(--line); }
.sea-mg-row.muted b { color: var(--dim); }
.sea-mg-sub { flex: 1 1 100%; font-size: .66rem; color: var(--dim); }
.sea-mg-act { margin-left: auto; display: flex; gap: 4px; flex-wrap: wrap; justify-content: flex-end; }
.sea-mg-b {
  padding: 3px 7px; border: 1px solid var(--line); border-radius: 7px;
  background: var(--bg); color: var(--txt); font: inherit; font-size: .68rem; cursor: pointer;
}
.sea-mg-b:hover { border-color: var(--pri); background: var(--pri-soft); color: var(--pri); }
/* 角色标签：老师 / 管理员。★ 颜色分开 —— "谁有权限"必须一眼可辨，
   否则学生会以为老师也是管理员（或者反过来）。 */
.sea-tag {
  padding: 1px 5px; border-radius: 5px; font-size: .62rem; font-style: normal;
  font-weight: 600; white-space: nowrap;
}
.sea-tag.own { background: var(--pri-soft); color: var(--pri); }
.sea-tag.adm { background: var(--line); color: var(--txt); }
/* 工具条上的「聊天」按钮在面板展开时高亮，一眼能看出"它现在开着"。 */
.sea-bar .btn.on { background: var(--pri-soft); color: var(--pri); border-color: var(--pri); }

/* 鱼嘴里冒的气泡：只显示 4.2 秒，同时最多 3 个（多了在海面糊成一片）。
   ★ `pointer-events:none` —— 气泡飘在鱼头上，不能挡"点鱼看名片"。
   ★ 定位靠 JS 写 left/top（鱼的屏幕坐标），这里只负责"以底边中点为锚点往上长"。 */
.sea-bubble {
  position: absolute; transform: translate(-50%, -100%);
  max-width: 200px; padding: 6px 10px; border-radius: 10px;
  background: #fff; color: #1F2430;
  border: 1px solid rgba(0, 0, 0, .08); box-shadow: 0 6px 18px rgba(0, 0, 0, .16);
  font-size: .78rem; line-height: 1.4; pointer-events: none;
  white-space: pre-wrap; word-break: break-word;
  animation: seaBubbleIn var(--dur-2) ease-out both;
}
/* 小尾巴：让气泡看得出是从这条鱼嘴里出来的 */
.sea-bubble::after {
  content: ''; position: absolute; left: 50%; bottom: -5px; width: 8px; height: 8px;
  margin-left: -4px; background: #fff; transform: rotate(45deg);
  border-right: 1px solid rgba(0, 0, 0, .08); border-bottom: 1px solid rgba(0, 0, 0, .08);
}
@keyframes seaBubbleIn {
  from { opacity: 0; transform: translate(-50%, -92%) scale(.86); }
  to { opacity: 1; transform: translate(-50%, -100%) scale(1); }
}
/* ★ 批次316：被 @ 的那条气泡。换成品牌色描边 + 加粗 ——
   和普通聊天气泡要一眼能区分，否则"有人在叫你"混在一堆"在吗"里就看不见了。
   ★ 尾巴的颜色也要跟着换，不然气泡是蓝的、尾巴是白的，像贴错了一张纸。 */
.sea-bubble.at {
  background: var(--pri-soft); color: var(--txt);
  border-color: var(--pri); font-weight: 600;
}
.sea-bubble.at::after { background: var(--pri-soft); border-color: var(--pri); }


/* ★★ 批次308：班级页「全班大海」**小窗预览**（批次303 首做 / 307 撤 / 308 重做）。
   用户原话（308）：「307 重做」——上一版撤掉是因为「总是调不好比例」。
   ★ 307 的病根不是 CSS，是 JS：viewBox 高度写死 400，人数一多行距被压扁 ⇒
     鱼被迫缩小 / 挤成一团。308 改成**高度随行数自适应**（见 classSeaMiniHTML），
     CSS 这边只负责「等比缩放、不留白、不变形」：
   ★ `height:auto` 是必须的：高度由 viewBox 比例算出来，任何卡宽都不会上下留白。
     写死像素高度时 `meet` 会按宽缩、上下各留一条纯水色（"海里没水"）。
   ★ 整块可点（`cursor:pointer` + hover 抬一点）：小窗本身就是入口。 */
.sea-mini {
  display: block; width: 100%; max-width: 640px; padding: 0; text-align: left; font: inherit;
  margin: 12px auto 14px; border: 1px solid var(--line); border-radius: 12px;
  overflow: hidden; background: var(--tk-water-2); cursor: pointer;
  transition: box-shadow var(--dur-2), transform var(--dur-2);
}
.sea-mini:hover { box-shadow: 0 6px 20px rgba(20, 30, 60, .14); transform: translateY(-1px); }
.sea-mini:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }
.sea-mini-hd {
  display: flex; align-items: baseline; gap: 8px;
  padding: 9px 12px; background: var(--bg); border-bottom: 1px solid var(--line);
}
.sea-mini-hd b { font-size: .92rem; color: var(--txt); }
.sea-mini-hd span { font-size: .78rem; color: var(--dim); }
.sea-mini-hd .sp { flex: 1; }
.sea-mini-more { color: var(--pri); font-size: .78rem; }
.sea-mini-stage { display: block; overflow: hidden; }
.sea-mini-svg { display: block; width: 100%; height: auto; }
.sea-mini-empty {
  display: flex; align-items: center; justify-content: center; min-height: 96px;
  padding: 16px; color: var(--dim); font-size: .82rem; text-align: center; cursor: default;
}

/* ★ 只动 translateY：鱼的位置和体量已经由 `fishWearSVG` 的 transform 算好了，
   这里再动 scale / rotate 会与它打架（而且配件会跟着转，帽子会歪）。 */
.sea-fish { animation: sea-bob 6.4s ease-in-out infinite alternate; }
/* ★★ 批次307：实时游动位移（自己跟鼠标 + 同学的位置同步）统一打在这一层。
   为什么不打外层：`sea-bob` / `seaSw{i}` 动画占着 `sea-fish` 的 transform，
   而 **CSS 动画优先级高于行内 style** —— 行内 translate 会被动画整个吞掉，
   线上表现就是"自己看着在动，同学那条一动不动"（本次用户报障的真因之一）。 */
.sea-move { will-change: transform; }
@keyframes sea-bob {
  from { transform: translateY(0); }
  to   { transform: translateY(-9px); }
}

/* 名字压在水面上要看得清 ⇒ `paint-order: stroke` 让描边落在字**底下**，
   描边色用页面底色（等于给字加一圈"挖空"），深浅主题都读得出来。 */
.sea-nm {
  fill: var(--txt); font-size: 15px; font-weight: 600; text-anchor: middle;
  paint-order: stroke; stroke: var(--bg); stroke-width: 3.5px; stroke-linejoin: round;
}
.sea-nm.me { fill: var(--pri); }

/* 自己那圈光：**只描边不填充** —— 填充会把鱼整个盖住（批次298 同一个教训） */
.sea-me { fill: none; stroke: var(--pri); stroke-width: 2.5; opacity: .5; }

/* ★★ 批次301：自己的鱼跟鼠标。**必须关掉浮动动画** ——
   两套 transform 打同一个元素，后写的赢，表现成"鼠标一抖鱼就弹回原位"。
   批次306：用户实测「角色跟随鼠标移动太快」⇒ 0.5s 放到 0.9s，游过去的手感更明显，
   不再是"贴着光标抖"。 */ 
.sea-mine {
  animation: none;
  /* 位置由 JS 逐帧 lerp 接管（缓慢游动），此处不再加 transition —— 否则两重平滑叠加会抖 */
  will-change: transform;
}

/* 在线点：绿 = 在线，灰 = 离线。判定在服务端（`sessions.last_seen_at` 5 分钟内），
   前端只认 `m.online` 这个布尔 —— 别在这里自己算时间。 */
.sea-dot { fill: #9AA4B5; opacity: .7; }
.sea-dot.on { fill: #22C55E; opacity: 1; animation: sea-pulse 2.6s ease-in-out infinite; }
@keyframes sea-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: .45; }
}
/* ★★ 批次302 订正：这里**不许**写 `@media`。
   批次299 为了窄屏缩小名字字号，在这条后面直接插了一个
   窄屏媒体查询（把 .sea-nm 字号压到 12px）——
   后果是「第一个响应式块」从 3503 行被提前到 935 行，
   于是 `_gearcheck` G4（gear 基准规则必须在第一个 @media 之前）
   和 `_p25check` K13（.cls-wrap 同理）**同时假红**。
   窄屏回退统一放文件末尾（见「全班大海 · 窄屏回退」那段）。 */

/* 鱼缸面板的排版 */
.tk-wrap { display: flex; flex-direction: column; gap: 10px; }
.tk-cap { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; font-size: .82rem; color: var(--dim); }
.tk-cap b { color: var(--txt); font-size: .95rem; }
.tk-cap .tk-w { color: var(--dim2); }
.tk-stats { display: flex; gap: 8px; flex-wrap: wrap; }
.tk-stat {
  flex: 1 1 92px; min-width: 92px; padding: 8px 10px; border-radius: 10px;
  background: var(--panel-2); border: 1px solid var(--line);
}
.tk-stat > b { display: block; font-size: .95rem; color: var(--txt); }
.tk-stat > span { font-size: .74rem; color: var(--dim2); }
/* 缸景商店：学生在自己的缸里就能添置，不用绕去商店。
   买不起的压暗但**不隐藏** —— 看不见的目标不是目标（同 openLooks 的口径）。 */
.tk-shop { margin-top: 2px; }
.tk-shop > b { font-size: .86rem; }
.tk-shop > .dim { font-size: .76rem; margin-bottom: 6px; }
.tk-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); gap: 8px; }
.tk-item {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 8px 6px; border-radius: 10px; cursor: pointer; text-align: center;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--txt);
  font: inherit; font-size: .78rem;
}
.tk-item:hover { border-color: var(--line-2); background: var(--panel-3); }
.tk-item.on { border-color: var(--ok); background: var(--ok-soft); }
.tk-item.no { opacity: .5; cursor: default; }
.tk-item.no:hover { border-color: var(--line); background: var(--panel-2); }
/* 批次117：缸景格子里原来是 **纯色块**（`.tk-sw`，22px 圆点 = 服务端的 swatch）。
   批次114d 只把缸底的装饰换成 AI 插画、没换格子 ⇒ 学生看不到"买了长什么样"。
   现在引同一张 deco-*.png，格子里的样子 = 缸里的样子。 */
.tk-item .tk-art { width: 44px; height: 44px; object-fit: contain; display: block; flex: none; margin-bottom: 2px; }
.tk-item > span { color: var(--dim2); font-size: .72rem; }

/* ── 穿戴配件（批次117）────────────────────────────────────────────
   五件全画在鱼身上（鱼缸 `heroFish` 之后 / 商店预览里），位置全部由 app.js 的
   `fishBox` 算 —— 这里只管颜色。**没有引入任何新图片资源**，全是 SVG。
   ★ 为什么不用 `--tk-*` 变量：那一组是"水与鱼"的环境色，深浅主题各一份；
     配件是**物体固有色**（草帽就是黄的、领结就是红的），深浅主题下都该是同一个色。
     写死之前逐件在两种主题的水面上比过：五件都是中高明度 + 深描边，
     在深色水面和浅色水面上都立得住。
   ★ 新类必须留在**第一个 @media 之前**（鱼缸那一批的同一条规矩）。
   ★ `stroke-width` 用的是 viewBox 用户单位（鱼缸 640×360 / 预览 100×68），
     两处的实际像素粗细接近，不用分开调。 */
.gear-brim { fill: #D9A23C; stroke: #8A5A1C; stroke-width: 1.6; }
.gear-hat { fill: #E8BE5A; stroke: #8A5A1C; stroke-width: 1.6; }
.gear-band { fill: none; stroke: #B5772B; stroke-width: 2.2; stroke-linecap: round; }
.gear-lens { fill: #CFE8F5; fill-opacity: .5; stroke: #3F5165; stroke-width: 2; }
.gear-strap { fill: none; stroke: #3F5165; stroke-width: 2.2; stroke-linecap: round; }
.gear-bow { fill: #E0574F; stroke: #8E2F2B; stroke-width: 1.4; }
.gear-knot { fill: #B93F39; }
.gear-vine { fill: none; stroke: #4E8C48; stroke-width: 2; }
.gear-petal { fill: #F2A8C0; stroke: #B45C7E; stroke-width: 1.2; }
.gear-chain { fill: none; stroke: #E7C87A; stroke-width: 2; stroke-linecap: round; }
.gear-shell { fill: #FBEFD4; stroke: #B7925A; stroke-width: 1.2; }
.gear-shell-big { fill: #FBEFD4; stroke: #B7925A; stroke-width: 1.4; }
.gear-shell-line { fill: none; stroke: #B7925A; stroke-width: 1.2; stroke-linecap: round; }
/* 商店里的「你的鱼戴着它」预览：鱼和配件画在同一个 <svg> 里。
   ★★ 纵横比**必须** `height: auto` 交给 viewBox 管 —— 批次148 起这里画的是位图鱼
      （`<svg viewBox="…" width="100" height="68">`，100:68 横向）。
      曾经（批次122）误把它改成 `aspect-ratio: 1` + `position: relative`，那是给
      「HTML 盒子 + 两张 img」的另一种方案准备的 —— 套到 SVG 上会把预览压成
      正方形容器，图缩小、上下留白，「装饰像没生效」。
      🔴 红线：`.gear-pv` 不得出现 `aspect-ratio` / `position`（纵横比归 viewBox）。 */
.gear-pv { display: block; width: 100%; max-width: 100px; height: auto; margin: 0 auto 6px; }
/* ★★ 批次148：穿戴预览换成**位图鱼**之后的两条。
   ① SVG 里的 `<image>` 默认按"平滑插值"缩放 —— 128px 的 AI 图放进 100px 的格子
      会糊成一团；`image-rendering` 不许它插值，边界还是清楚的。
      （鱼缸大画面那边不这么干：那里是 640×360、图反而被放大，平滑更好看。）
   ② 位图没有配件层，而龙系那 5 种只给了「喉位」—— 帽子/眼镜/花环**锚不到位置上**。
      这种格子必须是「看得见、但明显不可买」：灰度 + 降透明度 + 划掉说明，
      **不能 display:none** —— 学生需要知道"不是我少买了什么，是这条形态戴不了"。 */
.gear-pv image { image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges; }
.shop-cell.unfit { opacity: .62; }
.shop-cell.unfit .gear-pv { filter: grayscale(1); }
.shop-unfit { display: block; font-size: .68rem; font-style: normal; color: var(--dim); margin-top: 2px; }
/* 商店「缸景」组的**迷你鱼缸**（批次136：把纯色块换成实际图片 —— 用户原话
   「缸景用实际图片代替，不要使方框啊」）。SVG 由 app.js 的 `tankPv` 现画：
   小缸 + 这一件 deco-*.png 压在缸底 + 一条你的鱼（同一张图、同一套 .tk-* 上色）。
   ★ 和 .gear-pv 同一条规矩：**纵横比必须由 CSS 定死**（SVG 只给 viewBox）——
     不给尺寸会按 viewBox 的 148px 宽渲染，格子立刻被撑爆。
   ★ max-width 148px = viewBox 宽：再放大只是把 128px 的原图拉糊。 */
.tank-pv {
  display: block; width: 100%; max-width: 148px; aspect-ratio: 148 / 92;
  margin: 0 auto 6px; border: 1px solid var(--line-2); border-radius: var(--r-sm);
}

/* 连胜条：和成长条并排住在「我的」菜单里（见 app.js 的 streakLineHTML）。
   视觉主角是 🔥 和天数 —— 连胜**没有"进度"**，所以这里刻意不画进度条：
   画了就会让人以为"再攒点就升级"，而连胜唯一的正事是"今天推进一次"。
   护盾用一排小图标，灭掉的压暗（不是消失）—— 让人知道上限是几颗。 */
.streak-line { font-size: .76rem; color: var(--dim); display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.streak-line .st-flame { font-size: .95rem; line-height: 1; }
.streak-line .st-num b { color: var(--txt); font-size: .86rem; }
.streak-line .st-shields { display: inline-flex; gap: 1px; }
.streak-line .st-sh { font-style: normal; font-size: .72rem; }
.streak-line .st-sh.off { opacity: .2; filter: grayscale(1); }
.streak-line .st-tail { color: var(--dim2); }
.streak-line .st-fix { margin-left: auto; }

.nav { padding: 8px; display: flex; flex-direction: column; gap: 2px; }
.nav-i {
  border: 0; background: transparent; color: var(--dim);
  display: flex; align-items: center; gap: 10px;
  padding: .52em .7em; border-radius: var(--r-sm);
  font-size: .9rem; font-weight: 500; text-align: left;
  transition: background var(--dur-1), color var(--dur-1);
}
.nav-i:hover { background: var(--panel-2); color: var(--txt); }
.nav-i.on { background: var(--pri-soft); color: var(--pri); font-weight: 600; }
.ni-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: .45; flex: 0 0 6px; }
.nav-i.on .ni-dot { opacity: 1; }
.badge {
  margin-left: auto; background: var(--bad); color: #fff;
  font-size: .68rem; font-weight: 700; border-radius: 9px; padding: 0 6px; min-width: 18px; text-align: center;
  line-height: 18px; height: 18px;
}
.side-h {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 16px 6px; color: var(--dim2); font-size: .74rem; font-weight: 600; letter-spacing: .05em;
}
.proj-list { flex: 1; overflow-y: auto; padding: 0 8px 16px; }

.conv-i {
  display: flex; align-items: center; gap: 8px;
  padding: .5em .7em; border-radius: var(--r-sm); cursor: pointer;
  font-size: .86rem; color: var(--dim); transition: background var(--dur-1), color var(--dur-1);
}
.conv-i:hover { background: var(--panel-2); color: var(--txt); }
.conv-i.on { background: var(--panel-2); color: var(--txt); font-weight: 600; }
.conv-i .ci-t { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.conv-i .ci-x { opacity: 0; border: 0; background: transparent; color: var(--dim2); font-size: .95rem; line-height: 1; padding: 2px 4px; border-radius: 5px; }
.conv-i:hover .ci-x { opacity: 1; }
.conv-i .ci-x:hover { background: var(--bad-soft); color: var(--bad); }

.main { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }

/* 抽屉遮罩：桌面端侧栏是常驻的，遮罩不存在。
 * ★ 这条必须写在下面所有媒体查询之前 —— 同权重下后写的赢，
 *   放到媒体查询后面会把窄屏的 display:block 直接盖掉。
 *   （测试 _parity7check.cjs J 组会守这条顺序） */
.side-mask { display: none; }
.view { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
.topbar {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 18px; border-bottom: 1px solid var(--line);
  background: var(--panel); flex: 0 0 auto;
}
.tb-txt { min-width: 0; }
.tb-txt .t { font-weight: 600; font-size: .98rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 副标题一律单行省略。
 * 原来这里是 flex + flex-wrap:wrap —— 容器一旦被挤窄，文字就会**逐字换行**，
 * 变成一条竖排的"乱码"并撑高顶栏（390px 下实测 h=201px / 7 行）。
 * 单行省略在任何宽度下都是可控的。 */
.tb-txt .s { color: var(--dim2); font-size: .74rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* 对话页的副标题里还塞了一个项目下拉，这两个得排成一行 */
#convSub { display: flex; align-items: center; gap: 8px; }
#convSubText { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#convSub .proj-sel { flex: 0 0 auto; }
.proj-sel { font-size: .72rem; padding: 2px 6px; max-width: 140px; background: var(--panel-2); border-color: var(--line); }
.hamb { display: none; font-size: 1.1rem; padding: .2em .5em; }
/* 顶栏次级控件（模式 / 模型 / 智能体 / 分享 / 更多）整组包一层。
 * 桌面端 display:contents —— 子元素直接参与 .topbar 的 flex 布局，排版与拆分前逐像素一致；
 * 窄屏下整组切成一条独立行，见下面的 @media。 */
.tb-tools { display: contents; }
.tb-chk { font-size: .78rem; color: var(--dim); display: inline-flex; align-items: center; gap: 4px; cursor: pointer; white-space: nowrap; }
.tb-chk input { cursor: pointer; }
#kbSearch { max-width: 200px; }

/* ================= 学习页「模式切换 + 学科下拉」那一排（批次176）=================
 * .learn-mode 自己带 margin: 10px 16px 0 和 align-self: flex-start（在 star.css 里）——
 * 进了这一排就交给父级管，否则会双重缩进。
 * ★ 选择器权重 2 类 > 1 类，所以不用管 app.css / star.css 的先后顺序，一定赢。
 * ★★ 为什么不把学科下拉放进 .topbar：390px 实测（本地 Chromium 量过基线）——
 *   顶栏再塞一个 96px 的下拉会把标题从 114px 压到 10px，"学习"两个字直接看不见。
 *   这一排只有两个胶囊按钮、空得很，放这儿既省地方语义又顺。 */
.learn-bar {
  display: flex; align-items: center; gap: 10px;
  margin: 10px 16px 0; align-self: flex-start; flex: none;
  max-width: calc(100% - 32px);
}
.learn-bar .learn-mode { margin: 0; align-self: center; }

/* ================= 对话流 =================
 * 版式（批次12）：**通栏**，不再居中 840px。
 *   之前 840px 居中在 1440+ 的屏幕上左右各空 300px 以上，
 *   学生的话和最右边的自己的话都缩在中间一小块里，很空、很散。
 *   现在 AI 永远贴左、用户永远贴右，中间那点空地正好把两边拉开。
 *   —— 只靠 32px 左右内边距撑出安全区，宽屏上也不会顶到窗口边。 */
.stream { flex: 1; overflow-y: auto; padding: 26px 32px 14px; scroll-behavior: smooth; }
.stream-in { max-width: none; margin: 0; }

/* ================= 空状态（批次8 · 第一印象层 P1）=================
 * 打开产品的前 5 秒。原来只有一个 60px 淡图标 + 两行小字，
 * 学生第一次进来不知道能问什么 —— 四张快捷卡就是来解决这件事的。 */
.empty { text-align: center; color: var(--dim); padding: 7vh 20px 4vh; }
/* 批次114d：插画从手绘 SVG 换成 AI 图（336×336 正方形）⇒ 用 contain 收进原来的框，
   不裁剪也不拉伸（拉伸会把鱼压扁）。 */
.empty-art { display: block; width: 168px; height: 132px; margin: 0 auto 10px; object-fit: contain; }
.empty-hi { font-size: 1.55rem; font-weight: 700; color: var(--txt); margin: 0 0 6px; letter-spacing: .01em; }
.empty-ask { font-size: 1.02rem; font-weight: 500; color: var(--txt); margin: 0 0 10px; }
/* 立场句：桌面单行、窄屏自然折行。26em 时会在桌面折出「剩下的几步。」这种孤字行，
   所以放宽到 34em —— 中文没有词边界，宁可整句一行，也不要最后一行只剩几个字。 */
.empty-stance { font-size: .86rem; color: var(--dim2); line-height: 1.65; max-width: 34em; margin: 0 auto; }
.empty-cards {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px;
  max-width: 720px; margin: 26px auto 0;
}
.ec {
  display: block; text-align: left; width: 100%;
  border: 1px solid var(--line); background: var(--panel);
  border-radius: var(--r-sm); padding: 12px 13px 13px; color: var(--txt);
  transition: border-color var(--dur-1), background var(--dur-1), transform var(--dur-1), box-shadow var(--dur-1);
}
.ec:hover { border-color: var(--pri); background: var(--pri-soft); transform: translateY(-2px); box-shadow: var(--shadow); }
.ec:active { transform: translateY(0); }
.ec-i { display: block; width: 26px; height: 26px; border-radius: 8px; background: var(--pri-soft); color: var(--pri); }
.ec-i svg { display: block; width: 16px; height: 16px; margin: 5px; }
.ec b { display: block; font-size: .9rem; font-weight: 600; margin: 9px 0 3px; }
.ec .ec-d { display: block; font-size: .76rem; color: var(--dim2); line-height: 1.5; }
.ec:hover .ec-d { color: var(--dim); }
.empty-tips { margin: 20px auto 0; padding: .6em 1em; background: var(--pri-soft); border-radius: 8px; font-size: .8rem; color: var(--pri-2); max-width: 34em; line-height: 1.5; }
.learn-next-wrap { width: min(100%, 38em); margin: 18px auto 0; text-align: left; }
.learn-next { display: grid; gap: 12px; padding: 16px 18px; border: 1px solid var(--line); border-radius: 14px; background: var(--panel); box-shadow: 0 8px 24px rgba(25,45,75,.06); }
.learn-next-kicker { color: var(--pri-2); font-size: .76rem; font-weight: 700; letter-spacing: .04em; }
.learn-next h2 { margin: 4px 0 3px; color: var(--txt); font-size: 1.02rem; }
.learn-next p { margin: 0; color: var(--dim); font-size: .84rem; line-height: 1.55; }
.learn-next-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.learn-next-detail { color: var(--dim); font-size: .82rem; }
.learn-next small { color: var(--dim2); font-size: .72rem; }
.learn-intent { display: grid; gap: 7px; padding: 11px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel-2); }
.learn-intent label { color: var(--txt); font-size: .8rem; font-weight: 650; }
.learn-intent .inp { width: 100%; }
.learn-intent-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.learn-intent-actions small { color: var(--dim2); font-size: .7rem; line-height: 1.45; }
.learn-intent-actions .btn { flex: none; }
/* ===== 批次114：首页「学习起点」三块 + 「全部工具」次级入口 =====
   ★ 基准规则一律排在文件里**第一个响应式 @media 之前**（_p25check / _parity7check
     等多套件钉着"基准样式在第一个 @media 前"）；窄屏回退**并进文件末尾**那条既有块
     （_mapcheck 断言它是最后一个 @media）—— 这里**不新增任何 @media**。
   ★ 时长一律走 `--dur-*` token：`_ui106check` 断言"亚秒级时长一个裸字面量都不剩"。 */
.learn-next-h { margin: 0; color: var(--txt); font-size: 1.06rem; }
/* 三块共用一张"卡里的小卡"：底部一条深一档的边，跟实体键同一套语言，
   但**不做按下位移** —— 它是容器不是按钮，会动的只有里面那颗按钮。 */
.hs { display: grid; gap: 8px; padding: 13px 14px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel-2); }
.hs-kicker { color: var(--pri-2); font-size: .74rem; font-weight: 700; letter-spacing: .04em; }
.hs-title { color: var(--txt); font-size: .96rem; font-weight: 650; line-height: 1.4; }
.hs-act { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.hs-note { color: var(--dim2); font-size: .72rem; }
/* 到期清单：**内容**而不是数字。等宽排布让"知识名 / 到期状态"两栏对齐，扫一眼就能读。 */
.hs-due { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }
.hs-due-i { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; padding: 7px 10px; border: 1px solid var(--line); border-radius: 9px; background: var(--panel); }
.hs-due-i b { color: var(--txt); font-size: .86rem; font-weight: 600; line-height: 1.45; }
.hs-due-s { flex: none; color: var(--dim2); font-size: .74rem; }
.hs-due-hot { color: var(--warn); font-style: normal; font-weight: 650; }
.hs-due-skel, .hs-due-more { color: var(--dim2); font-size: .76rem; line-height: 1.5; }
/* 「全部工具」——原生 `<details>`：零 JS、键盘可达、窄屏天然自适应 */
.home-more { width: min(100%, 38em); margin: 16px auto 0; text-align: left; border: 1px solid var(--line); border-radius: 12px; background: var(--panel); }
.hm-sum { display: flex; align-items: baseline; gap: 8px; padding: 11px 14px; cursor: pointer; list-style: none; }
.hm-sum::-webkit-details-marker { display: none; }
.hm-sum b { color: var(--txt); font-size: .88rem; font-weight: 650; }
.hm-sum small { color: var(--dim2); font-size: .74rem; }
.hm-sum::after { content: '▾'; margin-left: auto; color: var(--dim2); font-size: .8rem; transition: transform var(--dur-2); }
.home-more[open] .hm-sum::after { transform: rotate(180deg); }
.hm-body { display: grid; gap: 14px; padding: 0 14px 14px; }
.hm-sec { display: grid; gap: 8px; }
.hm-h { margin: 0; color: var(--dim); font-size: .76rem; font-weight: 700; letter-spacing: .04em; }
.hm-tools { display: flex; flex-wrap: wrap; gap: 7px; }
.hm-t { padding: .42em .8em; border: 1px solid var(--line); border-radius: 999px; background: var(--panel-2); color: var(--txt); font-size: .8rem; font-family: inherit; cursor: pointer; }
.hm-t:hover { border-color: var(--pri); color: var(--pri); }

/* 学习首页：让一个真实关卡成为视觉中心，其他学习入口保持可见。 */
.empty:has(.home-focus:not([hidden])) { padding-top: 2vh; }
.empty:has(.home-focus:not([hidden])) .empty-art,
.empty:has(.home-focus:not([hidden])) .empty-hi,
.empty:has(.home-focus:not([hidden])) .empty-ask,
.empty:has(.home-focus:not([hidden])) .empty-stance,
.empty:has(.home-focus:not([hidden])) .empty-tips { display: none; }
.empty:has(.home-focus:not([hidden])) .learn-next-wrap { width: min(100%, 58em); margin-top: 0; }
.empty:has(.home-focus:not([hidden])) .home-more { width: min(100%, 58em); }
.home-focus { display: grid; grid-template-columns: minmax(0, 1fr) minmax(220px, 32%); gap: 28px; align-items: center; padding: 30px 28px; border-radius: var(--r-lg); background: var(--panel); }
.home-focus[hidden] { display: none; }
.learn-next .hs[hidden] { display: none; }
.home-focus-kicker, .lp-focus-kicker { color: var(--pri-2); font-size: .77rem; font-weight: 650; letter-spacing: .04em; }
.home-focus h2 { margin: 18px 0 12px; color: var(--txt); font-size: clamp(2rem, 4vw, 3.3rem); line-height: 1.24; letter-spacing: -.035em; font-weight: 650; }
.home-focus h2 em { color: var(--pri-2); font-style: normal; }
.home-focus p { max-width: 29em; color: var(--dim); font-size: .94rem; line-height: 1.8; margin: 0 0 24px; }
.home-focus-route { display: grid; gap: 13px; align-content: center; padding: 23px; border: 1px solid var(--line); border-radius: var(--r); background: var(--panel-2); }
.home-focus-route-title { color: var(--txt); font-size: .84rem; font-weight: 700; margin-bottom: 3px; }
.home-focus-step { color: var(--dim); font-size: .82rem; padding-left: 15px; border-left: 2px solid var(--line-2); line-height: 1.5; }
.home-focus-step.done { color: var(--ok); border-color: var(--ok); }
.home-focus-step.current { color: var(--txt); border-color: var(--pri); font-weight: 700; }
.home-focus-map { justify-self: start; margin-top: 7px; padding: 0; border: 0; background: transparent; color: var(--pri-2); font: inherit; font-size: .82rem; cursor: pointer; }
.empty:has(.home-focus:not([hidden])) .learn-next { gap: 15px; padding: 0; border: 0; background: transparent; box-shadow: none; }
.empty:has(.home-focus:not([hidden])) .hs-review { display: flex; align-items: center; justify-content: space-between; gap: 16px; background: var(--panel); padding: 16px 20px; }
.empty:has(.home-focus:not([hidden])) .hs-review .hs-due { flex: 1; }
.empty:has(.home-focus:not([hidden])) .hs-review .hs-due-i { border: 0; background: transparent; padding: 0; }
.empty:has(.home-focus:not([hidden])) .learn-intent { background: var(--panel); }
.lp-focus { padding: 25px 27px; margin-bottom: 18px; border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel); }
.lp-focus h2 { margin: 8px 0 5px; color: var(--txt); font-size: clamp(1.65rem, 3vw, 2.3rem); line-height: 1.3; }
.lp-focus p { margin: 0 0 17px; color: var(--dim); font-size: .88rem; line-height: 1.6; }
.lp-focus .btn { margin-top: 2px; }

/* ===== 学段适配（批次113）=====
   ★ 首页空状态按**学生资料里的学段/年级**换开场文案与快捷入口的疏密：
     小学（primary）= 文案更具体、入口更宽松（2 列、卡更高、字更大）；
     高中（senior）= 信息更紧凑（4 列更窄、内边距更小、提示更小）；
     初中（middle）/ 未填写 = 沿用原有节奏（不加任何规则）。
   ★ 判据全在 `data-learn-stage` 这一个属性上（由 app.js 的 renderStreamEmpty 写），
     本文件不做任何正则/取值判断 —— 分档只有一份，改档位只改 JS。
   ★ 只加普通规则、**不新增 @media**（首个响应式 @media 之前，别把它的位置往后推）。 */
.empty[data-learn-stage="primary"] .empty-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); max-width: 560px; }
.empty[data-learn-stage="primary"] .ec { min-height: 116px; padding: 15px 16px; }
.empty[data-learn-stage="primary"] .ec b { font-size: .96rem; }
.empty[data-learn-stage="primary"] .ec .ec-d { font-size: .82rem; }
.empty[data-learn-stage="senior"] .learn-next-wrap { max-width: 46em; }
.empty[data-learn-stage="senior"] .empty-cards { max-width: 820px; gap: 8px; margin-top: 18px; }
.empty[data-learn-stage="senior"] .ec { padding: 10px 11px; }
.empty[data-learn-stage="senior"] .empty-tips { font-size: .74rem; }
.mem-source { border: 0; padding: 0; background: transparent; color: var(--pri-2); font: inherit; cursor: pointer; text-align: left; }
.mem-source:hover { text-decoration: underline; }
/* ★ .learn-next 的窄屏回退已移到文件末尾 —— app.css 有「第一个 @media 位置」型断言，
   在中部插 @media 会让 _p25check / _parity22fe / _growthcheck / _encheck / _leaderboardcheck
   等一整批静态套件假红（批次70 真踩过：原先插在第 543 行）。 */

/* 对齐规则（批次11 · 参考竞品的对话版式）：
 *   · AI 左、用户右，共用同一条内容列与基线；
 *   · AI **不套气泡**——长回答套气泡会把正文挤成窄栏，读起来比纯文档累；
 *   · 用户套浅色气泡，右缘贴齐内容列右缘；头像在最右，与首行文字对齐。 */
.msg { display: flex; gap: 12px; margin-bottom: 28px; align-items: flex-start; animation: msgIn var(--dur-3) ease both; }
@keyframes msgIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.msg .av {
  flex: 0 0 32px; width: 32px; height: 32px; border-radius: 10px;
  display: flex; align-items: center; justify-content: center;
  font-size: .78rem; font-weight: 700; user-select: none;
  box-shadow: 0 1px 2px rgba(16, 24, 40, .07);
}
/* 批次R4：小涌用形象图做头像（public/ai/img/xiaoyong-128.png，已抠透明底）；
   渐变底与投影撤掉，图片撑满圆角块（contain 保证鲸鱼不被裁）。 */
.msg.ai .av { background: transparent; box-shadow: none; overflow: hidden; }
.msg.ai .av img { width: 100%; height: 100%; object-fit: contain; display: block; }
.msg.user .av { background: var(--panel-3); color: var(--dim); }
.msg .body { flex: 1; min-width: 0; }
/* 用户消息：整块靠右。用 row-reverse + flex-start 让头像落在最右侧，
   而不是靠 margin-left:auto 把气泡推过去——那会在头像和气泡之间留一大段空白。 */
.msg.user { flex-direction: row-reverse; justify-content: flex-start; }
.msg.user .body {
  flex: 0 1 auto;
  background: var(--pri-soft);
  border: 1px solid color-mix(in srgb, var(--pri) 15%, transparent);
  border-radius: 16px 16px 5px 16px;
  padding: 11px 15px; max-width: 78%;
  box-shadow: 0 1px 2px rgba(16, 24, 40, .04);
}
.msg.user .md p:first-child { margin-top: 0; }
.msg.user .md p:last-child { margin-bottom: 0; }
/* AI 落款行：谁在说话 + 什么时候说的（竞品同款） */
.msg .meta {
  font-size: .72rem; color: var(--dim2); margin: 0 0 7px;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.msg .meta .who { font-weight: 600; color: var(--dim); }
.msg.user .meta { display: none; }
/* 操作条：始终可见（用户反馈找不到删除按钮）。hover 时按钮本身变实。 */
.msg .acts { display: flex; gap: 5px; margin-top: 10px; opacity: 1; flex-wrap: wrap; }
.msg .acts .btn { opacity: .65; transition: opacity var(--dur-2); }
.msg:hover .acts .btn, .msg .acts:hover .btn { opacity: 1; }
.msg .acts .btn { font-size: .72rem; padding: .2em .58em; border-radius: 8px; }

/* ── 追问建议（批次47，2026-10-06）────────────────────────────────
   用户诉求「AI 交互互动性和趣味性不够」。
   位置：渲染在 .acts **之前**（追问是"继续对话"，比"对这条回复的操作"更靠前）。
   ★ 它是**建议**不是考试：所以做成"一点就把这句话发出去"的快捷方式，
     而不是要学生作答的题。学生想无视就无视，想自己打字就自己打字。
   ★ 视觉上必须比 .acts 显眼（.acts 是 .65 透明度的次要操作），
     否则学生根本注意不到 —— 那这个功能等于没做。
     用 --pri-soft 底 + --pri 字，跟互动卡的"想一想"同一族色，
     让人一眼认出"这是可以点的、和普通文字不一样"。 */
.asks { display: flex; gap: 6px; margin-top: 12px; flex-wrap: wrap; align-items: center; }
.asks-lead { font-size: .72rem; color: var(--dim); margin-right: 2px; flex: none; }
.ask-chip {
  font: inherit; font-size: .78rem; line-height: 1.4;
  padding: .32em .72em; border-radius: 999px; cursor: pointer;
  background: var(--pri-soft); color: var(--pri);
  border: 1px solid transparent;
  transition: background var(--dur-1), border-color var(--dur-1), transform var(--dur-1);
  text-align: left;
}
.ask-chip:hover { border-color: var(--pri); transform: translateY(-1px); }
.ask-chip:active { transform: translateY(0); }

/* ============ 公共教材（批次96）============
   ★ 基准规则**必须排在文件里第一个 @media 之前**（_p25check / _parity7check 等
      多套件断言「基准样式在第一个响应式 @media 之前」）；窄屏回退放文件末尾那条
      既有的窄屏块里（_mapcheck 断言它是最后一个 @media）。
   ⚠️ 这段注释里**不许写出那条媒体查询的完整选择器**：`_unitcheck.cjs` 是用**裸
      `indexOf`/`lastIndexOf`** 数「全程只有一个窄屏块」的，**不剥注释** ⇒ 注释里写全
      就会被数成 2 个、断言直接变红（本项目已踩过多次：注释里别写完整选择器）。 */
.tb-note { font-size: .82rem; }
.tb-card {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px; margin-bottom: 8px;
  border: 1px solid var(--line); border-radius: 12px;
  background: var(--panel);
}
.tb-cov {
  flex: 0 0 auto; width: 54px; height: 72px;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; border-radius: 6px;
  background: var(--panel-2); border: 1px solid var(--line);
}
/* 封面是 80 DPI 渲染的整页图，按高度铺满即可 —— 不裁不缩放比例失真 */
.tb-cov img { height: 100%; width: auto; display: block; }
.tb-info { flex: 1 1 auto; min-width: 0; }
.tb-info > b { display: block; font-size: .95rem; }
.tb-info .dim { font-size: .78rem; }
/* 目录预览：一行截断。它只用来确认"是这本"，不是替代目录页 */
.tb-toc {
  font-size: .76rem; color: var(--dim2); margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* 批次163：教材「分门别类」—— 学科分组壳 + 组内年级小标题。
   ★ 纯壳规则：不新增 @media、不动 .tb-card 本体（多套件断言它基准样式的位置），
     所以整段紧贴 .tb-toc、仍排在第一个 @media 之前。
   ★ 上色走 CSS 变量（深色主题跟着 [data-theme] 切），与上面几条同一规矩。 */
.tb-group { margin: 0 0 2px; }
.tb-group-h {
  display: flex; align-items: baseline; gap: 8px;
  font-size: .95rem; font-weight: 500;
  margin: 12px 2px 8px;
}
.tb-group-h .dim { font-size: .78rem; font-weight: 400; }
.tb-grade-h { font-size: .8rem; color: var(--dim2); margin: 8px 2px 4px; }
/* ============ 学习通关路径（批次98）============
   ★ 基准规则**必须排在文件里第一个 @media 之前**（同上面那段：多套件断言
     「基准样式在第一个响应式 @media 之前」）。
   ★ 这一段**刻意不新增任何 @media**：关卡路径是一个 viewBox 整体缩放的 SVG，
     宽窄屏靠 `width:100%` 自己适配，不需要窄屏回退 ——
     也就不会把「文件里最后一个 @media 是地图那条窄屏回退」的断言顶掉。
   ★ 上色一律走 CSS 变量（不写死 fill）：深色主题跟着 [data-theme] 自动切。 */
.learn-wrap { flex: 1; min-height: 0; overflow-y: auto; padding: 14px 16px 40px; }
.learn-note { padding: 10px 2px; font-size: .84rem; }

/* ---- 选一册：书卡（与公共书库的卡片同族，但多一条进度）---- */
.learn-books { display: flex; flex-direction: column; gap: 10px; max-width: 780px; }
.lb-card {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px; border: 1px solid var(--line); border-radius: 14px;
  background: var(--panel);
}
.lb-card.lb-nopath { opacity: .7; }
.lb-cov {
  flex: 0 0 auto; width: 54px; height: 72px;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; border-radius: 6px;
  background: var(--panel-2); border: 1px solid var(--line);
}
.lb-cov img { height: 100%; width: auto; display: block; }
.lb-info { flex: 1 1 auto; min-width: 0; }
.lb-info > b { display: block; font-size: .95rem; }
.lb-info .dim { font-size: .78rem; }
.lb-prog { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
.lb-pct { font-size: .78rem; color: var(--dim); white-space: nowrap; }
.lb-bar {
  flex: 1 1 auto; max-width: 220px; height: 7px;
  border-radius: 99px; background: var(--panel-2); overflow: hidden;
}
.lb-bar > i { display: block; height: 100%; border-radius: 99px; background: var(--ok); }
.lb-soon { margin-top: 4px; font-size: .76rem; }
.lb-act { flex: 0 0 auto; }

/* ---- 一册的关卡路径 = 2.5D 等距岛屿（批次101）----
   ★ 换掉了原来的"蜿蜒圆点路"：一册教材本来就是**一片地**，不是一个列表。
     圆点排成一条线只看得见"第几步"；铺成岛之后"走到哪儿、还剩多少"是一眼的事。
   ★ 这一段**刻意不新增任何 @media**（理由见文件末尾那条说明）。 */
.learn-path { max-width: 780px; margin: 0 auto; }
.lp-head { display: flex; flex-direction: column; gap: 2px; margin: 2px 4px 8px; }
.lp-head > b { font-size: 1rem; }
.lp-head .dim { font-size: .8rem; }
/* ★ 等距投影下**画布宽高比恒为 2:1**（跟网格是方是扁无关）。
   窄屏上如果一味缩到 100% 宽，地块上的名字会缩到 4px（实测：375px 视口下正好 4.2px）
   ⇒ 宁可让它**横向滑**，也不让字变成糊的。min-width 兜住可读性下限。 */
.lp-map { overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; }
.lp-svg { display: block; width: 100%; height: auto; }
/* ★ 画布宽度 --lp-w 由 JS 按内容算好写在 svg 的 style 上（见 learningPathView 结尾）。
   两头都要卡：窄册不许被放大超过 1.25 倍（10px 的名字会变成 20px），
   宽册在窄屏不许缩到 620px 以下（再小名字就糊了 ⇒ 宁可横向滑）。 */
.lp-map > .lp-svg {
  margin: 0 auto;
  max-width: calc(var(--lp-w) * 1.25);
  min-width: min(620px, var(--lp-w));
}
/* 海 */
.lp-sea { fill: var(--lp-sea); }
.lp-wave { fill: none; stroke: var(--lp-sea-2); stroke-width: 2; stroke-linecap: round; opacity: .85; }
/* 岛面：滩 + 崖（崖整体下移 ⇒ 2.5D 侧壁） */
.lp-shore { fill: var(--lp-sand); }
.lp-cliff { fill: var(--lp-cliff); }
/* 路：一条虚线的土径；已走过的那几段（上一块地已掌握）染绿 */
.lp-seg { stroke: var(--lp-path); stroke-width: 7; stroke-linecap: round; stroke-dasharray: 1 12; }
.lp-seg.seg-done { stroke: var(--ok); stroke-dasharray: none; opacity: .55; }
/* 地块：顶面 + 左崖 + 右崖（三块拼出一个 2.5D 台地） */
.lp-top { fill: var(--lp-grass); }
.lp-fl { fill: var(--lp-cliff-lo); }
.lp-fr { fill: var(--lp-cliff); }
.lp-node { cursor: pointer; }
.lp-node.st-lock { cursor: default; }
.lp-node.st-open .lp-top { fill: var(--lp-open); stroke: var(--lp-open-2); stroke-width: 1.5; }
.lp-node.st-open .lp-fl, .lp-node.st-open .lp-fr { fill: var(--lp-open-2); }
.lp-node.st-lock .lp-top { fill: var(--lp-ground); }
.lp-node.st-lock .lp-fl, .lp-node.st-lock .lp-fr { fill: var(--lp-ground-2); }
/* 当前这一关的呼吸环：它只是"点我"的提示、不是信息 ⇒ reduced-motion 下停掉 */
.lp-halo { fill: none; stroke: var(--lp-open-2); stroke-width: 3.5; opacity: .6; animation: lpPulse 2s ease-in-out infinite; }
@keyframes lpPulse { 0%, 100% { opacity: .2; } 50% { opacity: .6; } }
/* 状态圆（勾 / 编号 / 锁）—— ★ 必须是 <circle> 且带 cx：浏览器套件按它读 x 分布 */
.lp-c { fill: #fff; stroke: var(--line); stroke-width: 1.5; }
.lp-node.st-done .lp-c { fill: var(--ok); stroke: var(--ok); }
.lp-node.st-open .lp-c { fill: var(--pri); stroke: var(--pri); }
.lp-node.st-lock .lp-c { fill: var(--lp-lock-face); stroke: var(--lp-ground-2); }
.lp-ic { fill: none; stroke: #fff; stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; }
.lp-n { font-size: 13px; font-weight: 700; fill: #fff; }
.lp-lock rect, .lp-lock path { fill: none; stroke: var(--lp-lock-ink); stroke-width: 2; }
/* 考试节点（批次164）：状态圈里画一张卷子（夹子 + 三行字）。
   三种状态共用一个字形，过没过看圈色：done=实色圈⇒白描，lock=浅圈⇒墨色描边。 */
.lp-exam rect, .lp-exam line { fill: none; stroke: var(--lp-lock-ink); stroke-width: 2; stroke-linecap: round; }
.lp-exam .lp-exam-clip { fill: var(--lp-lock-ink); stroke: none; }
.lp-node.st-done .lp-exam rect, .lp-node.st-done .lp-exam line,
.lp-node.st-open .lp-exam rect, .lp-node.st-open .lp-exam line { stroke: #fff; }
.lp-node.st-done .lp-exam .lp-exam-clip,
.lp-node.st-open .lp-exam .lp-exam-clip { fill: #fff; }
/* 掌握一块地 ⇒ 长一棵树（纯装饰，确定性按序号摆） */
.lp-tree { fill: var(--lp-tree); }
.lp-tree-2 { fill: var(--lp-tree-2); }
.lp-trunk { fill: var(--lp-trunk); }
/* ★ 文字描一圈底色再画字（paint-order: stroke）：名字落在地块之间的空档里，
   总会压到一点沙面或水面，没有这圈底就会糊掉。描边色用 --panel ⇒ 深色主题自动跟着变。 */
.lp-t {
  font-size: 10px; fill: var(--txt);
  paint-order: stroke; stroke: var(--panel); stroke-width: 3px; stroke-linejoin: round;
}
.lp-node.st-lock .lp-t { fill: var(--dim2); }
/* 单元里程碑：一枚编号小圆，骑在该单元第一块地的北顶点上 */
.lp-band-bg { fill: var(--panel); stroke: var(--warn); stroke-width: 2; }
.lp-band-l { font-size: 9px; font-weight: 700; fill: var(--warn); }
.lp-legend {
  display: flex; gap: 14px; justify-content: center; flex-wrap: wrap;
  margin-top: 12px; font-size: .78rem; color: var(--dim);
}
.lp-legend .lg::before {
  content: ''; display: inline-block; width: 9px; height: 9px; border-radius: 50%;
  margin-right: 5px; vertical-align: middle;
}
.lp-legend .lg-done::before { background: var(--lp-grass); }
.lp-legend .lg-open::before { background: var(--lp-open); }
.lp-legend .lg-lock::before { background: var(--lp-ground); border: 1px solid var(--lp-ground-2); box-sizing: border-box; }
/* 单元对照：地图上的里程碑只放得下编号，单元全名在这里给（当前所在单元点亮） */
.lp-units { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-top: 12px; justify-content: center; }
.lp-units .lu { display: inline-flex; align-items: baseline; gap: 5px; font-size: .76rem; color: var(--dim); }
.lp-units .lu > i { font-style: normal; font-size: .68rem; font-weight: 700; color: var(--warn); }
.lp-units .lu-cur { color: var(--txt); font-weight: 600; }

/* 批次125：提示词优化大师。
   ① .polish-bar = 输入区下面的成品对比栏（学生点了「优化提问」才出现）；
   ② .msg-polish = 学生自己那条消息下面的「已优化提问」条（自动/手动两种模式都显示）。
   ★ 全部走既有变量，不引入第二种颜色；窄屏回退并进文件末尾 680px 块（不另起媒体查询）。
   ★ 字号一律 ≥ .8rem —— 项目里 .74rem（≈11.8px）已被列为待修项，不再新增。 */
.polish-bar {
  margin: 0 12px 8px; padding: 10px 12px; border-radius: var(--r-sm);
  background: var(--panel); border: 1px solid var(--line);
}
.polish-bar .pb-head { display: flex; align-items: center; gap: 6px; font-size: .82rem; color: var(--dim); }
.polish-bar .pb-head b { color: var(--pri); }
.polish-bar .pb-why { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.polish-bar .pb-x {
  margin-left: 4px; border: 0; background: none; color: var(--dim2); cursor: pointer;
  font-size: 1rem; line-height: 1; padding: 2px 4px; border-radius: var(--r-xs);
}
.polish-bar .pb-x:hover { color: var(--txt); background: var(--panel-2); }
.polish-bar .pb-body {
  margin: 7px 0 9px; font-size: .92rem; line-height: 1.55; color: var(--txt);
  white-space: pre-wrap; word-break: break-word;
}
.polish-bar .pb-acts { display: flex; flex-wrap: wrap; gap: 8px; }
.msg-polish { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 8px; font-size: .8rem; color: var(--dim); }
.msg-polish .mp-ic { color: var(--pri); }
.msg-polish .mp-tx { color: var(--pri); font-weight: 600; }
.msg-polish .mp-why { color: var(--dim2); }
.msg-polish .mp-btn {
  border: 1px solid var(--line); background: var(--panel-2); color: var(--dim);
  border-radius: var(--r-xs); padding: .12em .55em; font-size: .8rem; cursor: pointer;
}
.msg-polish .mp-btn:hover { color: var(--pri); border-color: var(--pri); }
.msg-polish .mp-orig, .msg-polish .mp-new {
  flex-basis: 100%; margin-top: 3px; padding: .5em .7em; border-radius: var(--r-xs);
  background: var(--panel-2); color: var(--txt); font-size: .86rem; line-height: 1.5;
  white-space: pre-wrap; word-break: break-word;
}
/* 「看改过的」展开的那段：与原文同一只盒子，用主色描个边 ——
   一眼分得清"这是我写的"和"这是实际问出去的"（两段可能同时出现在同一条消息下）。 */
.msg-polish .mp-new { border: 1px solid color-mix(in srgb, var(--pri) 32%, transparent); }

/* 触屏没有 hover：给一点常驻的下划线暗示可点 */
@media (hover: none) {
  .ask-chip { background: var(--pri-soft); border-color: color-mix(in srgb, var(--pri) 28%, transparent); }
}

/* ================= 回复末尾的「飘带」：鼓励语 + 彩蛋（批次63） =================
   ★ 为什么单独一条、且比"接着问"更轻：
     鼓励是"对刚做完这件事的回应"（往回看），追问是"接下来能做什么"（往前看）——
     两件事混一行会既分不清、又都不想点。所以飘带用**更浅**的底、更小的字号，
     像一句附言，不抢正文和追问的注意力。
   ★ 为什么用 --ok（绿）而不是 --pri：学校场景里蓝"确定/可点"已经给了追问与按钮，
     鼓励换一族色（绿=肯定）才不会和"接着问"的 chip 混成一堆。
   ★ 彩蛋（--warn 琥珀）常驻一个可点样式，但**多数轮次根本不会出现**（模型只在
     真出现值得纪念的时刻才写）—— 稀缺感就是它全部的价值。 */
.tail-ribbon {
  display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; align-items: baseline;
}
.tail-cheer {
  display: inline-flex; align-items: baseline; gap: .4em;
  font-size: .8rem; line-height: 1.6; color: var(--dim);
  padding: .18em .1em .18em 0;
}
.tail-cheer .tail-ico { color: var(--ok); font-size: .78em; flex: none; }
.tail-egg {
  font: inherit; font-size: .78rem; line-height: 1.6;
  display: inline-flex; align-items: baseline; gap: .4em;
  padding: .26em .7em; border-radius: 999px; cursor: pointer;
  background: var(--warn-soft); color: var(--warn);
  border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent);
  transition: transform var(--dur-1), border-color var(--dur-1);
  text-align: left;
}
.tail-egg:hover { border-color: var(--warn); transform: translateY(-1px); }
.tail-egg:active { transform: translateY(0); }
.tail-egg .tail-ico { font-size: .95em; flex: none; }
/* 点一下"挖到了"的小动画：纯装饰。reduced-motion 下不动，尊重系统设置。
   ★ 批次106：这是**全站唯一保留的回弹缓动**，是刻意的例外 —— 它是"挖到彩蛋"
   那一下的惊喜感，一年也碰不到几次；把它换成 --ease-out 会让这个瞬间变得平淡。
   其余 19 处缓动已全部归并到 --ease-out / --ease-io，不再有第三种。 */
.tail-egg.pop { animation: eggPop var(--dur-4) cubic-bezier(.34,1.56,.64,1); }
@keyframes eggPop {
  0% { transform: scale(1) rotate(0); }
  35% { transform: scale(1.06) rotate(-2deg); }
  65% { transform: scale(.99) rotate(1.5deg); }
  100% { transform: scale(1) rotate(0); }
}
@media (prefers-reduced-motion: reduce) {
  .tail-egg.pop { animation: none; }
  .tail-egg:hover { transform: none; }
  /* 批次98：关卡路径上"这一关"的呼吸圈也停掉（它只是提示，不是信息） */
  .lp-node.st-open .lp-halo { animation: none; opacity: .35; }
}
/* 选中文字朗读浮动按钮 */
.sel-tts-btn { animation: popIn var(--dur-2) ease-out; }
/* 批次92：选中文字后浮出的**按钮排**（朗读选中 + 问小老师）。
   ★ 定位由 JS 按整排宽度算（`placeSelTts`）—— 原来那两个魔数是按**单个**按钮算的。 */
.sel-bar { display: flex; gap: 6px; animation: popIn var(--dur-2) ease-out; }
@keyframes popIn { from { transform: scale(.85); opacity: 0; } to { transform: scale(1); opacity: 1; } }

.caret { display: inline-block; width: 2px; height: 1.05em; background: var(--pri); vertical-align: text-bottom; animation: blink 1s step-end infinite; margin-left: 1px; }
@keyframes blink { 50% { opacity: 0; } }

.think { color: var(--dim2); font-size: .84rem; font-style: italic; margin-bottom: 6px; }

/* ================= Markdown 渲染 =================
 * 目标：让一段回答「条理分明、重点清晰」——
 *   · 加粗 = 荧光笔（不是单纯变粗），重点词一眼扫得到；
 *   · 有序列表 = 圆形序号徽章，步骤感强、顺序一眼看清；
 *   · 小标题 = 左侧色条，段落层次不用读也能分出来。 */
.md { font-size: .965rem; line-height: 1.82; word-wrap: break-word; overflow-wrap: anywhere; }
.md > *:first-child { margin-top: 0; }
.md > *:last-child { margin-bottom: 0; }
.md p { margin: .62em 0; }

/* 重点：加粗 + 主题色 + 底部荧光带 */
.md strong {
  font-weight: 700; color: var(--pri-2);
  background: linear-gradient(transparent 58%, color-mix(in srgb, var(--pri) 16%, transparent) 58%);
  padding: 0 .08em; border-radius: 3px;
}
/* 用户气泡底色本身就是浅蓝，荧光带叠上去会糊——那里只保留颜色 */
.msg.user .md strong { background: none; }

/* 标题：左侧色条，层次分明 */
.md h1, .md h2, .md h3, .md h4 { margin: 1.3em 0 .55em; line-height: 1.42; font-weight: 700; }
.md h1 { font-size: 1.28rem; }
.md h2 { font-size: 1.14rem; padding-left: .62em; border-left: 4px solid var(--pri); border-radius: 2px; }
.md h3 { font-size: 1.02rem; padding-left: .6em; border-left: 3px solid color-mix(in srgb, var(--pri) 42%, transparent); }
.md h4 { font-size: .95rem; color: var(--dim); }

/* 列表：有序 = 序号徽章，无序 = 主题色圆点 */
.md ul, .md ol { margin: .7em 0; }
.md ul { list-style: none; padding-left: 1.5em; }
.md ol { list-style: none; padding-left: 2.1em; counter-reset: md-ol; }
.md li { margin: .46em 0; }
.md ol > li { counter-increment: md-ol; position: relative; }
.md ol > li::before {
  content: counter(md-ol);
  position: absolute; left: -2.1em; top: .2em;
  width: 1.62em; height: 1.62em; line-height: 1.62em;
  border-radius: 50%; background: var(--pri-soft); color: var(--pri-2);
  font-size: .78em; font-weight: 700; text-align: center;
  font-variant-numeric: tabular-nums;
}
.md ul > li { position: relative; }
.md ul > li::before {
  content: ""; position: absolute; left: -1.1em; top: .78em;
  width: 6px; height: 6px; border-radius: 50%;
  background: color-mix(in srgb, var(--pri) 55%, transparent);
}
/* 嵌套一层换个颜色，避免满屏都是同一颗点 */
.md ul ul > li::before { background: var(--dim2); width: 5px; height: 5px; left: -1.05em; }
.md ul ul, .md ul ol, .md ol ul, .md ol ol { margin: .35em 0 .1em; }

.md blockquote {
  margin: .9em 0; padding: .68em 1.05em; border-left: 3px solid var(--pri);
  background: var(--panel-2); border-radius: 0 10px 10px 0; color: var(--dim);
}
.md blockquote p { margin: .28em 0; }
.md a { color: var(--pri); text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--pri) 35%, transparent); }
.md a:hover { border-bottom-color: var(--pri); }
.md hr { border: 0; border-top: 1px solid var(--line); margin: 1.3em 0; }
.md code.inl {
  font-family: var(--mono); font-size: .87em;
  background: var(--panel-3); color: var(--pri-2);
  padding: .12em .42em; border-radius: 6px;
}
[data-theme="dark"] .md code.inl { color: var(--pri-2); }
.md table { border-collapse: collapse; width: 100%; margin: .95em 0; font-size: .88rem; display: block; overflow-x: auto; }
.md th, .md td { border: 1px solid var(--line); padding: .5em .75em; text-align: left; }
.md th { background: var(--panel-2); font-weight: 650; }
.md tbody tr:nth-child(even) { background: color-mix(in srgb, var(--panel-2) 55%, transparent); }
/* 正文里的图片（模型用 markdown 插图时） */
.md img {
  display: block; max-width: 100%; height: auto; margin: .8em 0;
  border-radius: 12px; border: 1px solid var(--line); box-shadow: var(--shadow);
}
.md .katex { font-family: var(--mono); }

/* ── 批次212：结构化呈现（模型没给结构时由前端兜底拆出来的块） ──
   目标：让"一坨自然语言"变成眼睛能扫的层次 —— 要点列表、例子卡、结论块。
   ★ 只改呈现不改语义：拆出来的每一段都能在原文里逐字找到。 */

/* 要点列表：引导句在上，条目成列 */
.md-pts { margin: .72em 0; }
.md-pts-h { color: var(--dim); margin-bottom: .34em; }
.md-pts ul { list-style: none; padding-left: 0; margin: 0; }
.md-pts > ul > li {
  position: relative; padding: .34em 0 .34em 1.5em; margin: .18em 0;
  border-bottom: 1px dashed color-mix(in srgb, var(--line) 70%, transparent);
  line-height: 1.62;
}
.md-pts > ul > li:last-child { border-bottom: 0; }
.md-pts > ul > li::before {
  content: ""; position: absolute; left: .32em; top: .82em;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--pri); opacity: .75;
}
/* 分隔符（顿号/分号）：**原文里就在那儿，一个字都没删**（红线①），
   视觉上弱化到几乎看不见 —— 列表本身就承担了"分隔"的语义。 */
.md .md-sep { opacity: .3; margin-left: .1em; }

/* 破折号后的解释段：原文的破折号**一个字都没删**（红线①：只改呈现不改语义），
   视觉上的"孤零零起头"用悬挂缩进化解 —— 破折号悬在左侧缩进位，不占正文行首。 */
.md-pts-r {
  color: var(--dim); margin-top: .42em;
  padding-left: 1.1em; text-indent: -1.1em;
}

/* 例子卡：「比如…」引导的一组并列例子 */
.md-egs { margin: .72em 0; }
.md-egs-h { color: var(--dim); margin-bottom: .38em; }
/* 引导词（"比如"）：做成小标签，与下面的例子卡自然衔接 */
.md-egs-lead {
  display: inline-block; margin-bottom: .4em; padding: .1em .6em;
  font-size: .8em; color: var(--pri); background: color-mix(in srgb, var(--pri) 10%, transparent);
  border-radius: 999px; letter-spacing: .02em;
}
.md-egs ul { list-style: none; padding-left: 0; margin: 0; }
.md-egs > ul > li {
  position: relative; padding: .44em .8em .44em 1.5em; margin: .26em 0;
  background: var(--panel-2); border-radius: 9px; line-height: 1.62;
}
.md-egs > ul > li::before {
  content: ""; position: absolute; left: .62em; top: .92em;
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--pri); opacity: .55;
}

/* 结论块：一眼看到"重点在这" */
.md blockquote.md-key {
  border-left-width: 4px;
  background: color-mix(in srgb, var(--pri) 9%, var(--panel-2));
  color: var(--text);
}
.md blockquote.md-key p { color: inherit; }

/* 步骤序列 / 对比表（批次214） */
.md-steps { list-style: none; padding-left: 0; margin: .7em 0; }
.md-steps > li {
  position: relative; padding: .32em 0 .32em 1.6em; margin-left: .35em;
  border-left: 2px solid var(--line); line-height: 1.75;
}
.md-steps > li::before {
  content: ''; position: absolute; left: -.55em; top: .85em;
  width: .5em; height: .5em; border-radius: 50%; background: var(--pri);
}
.md-steps-h { color: var(--dim); margin-bottom: .3em; }
/* 小标题块（批次217）：讲解里「标签：内容」的句子，把标签搬到行首当小标题。
   为什么不用 h2/h3：这里的小标题是**从原句里搬出来的半句**（"拿一个具体例子看："），
   拿 h2 的体量去承载它会头重脚轻；一行主色小字 + 一道短线，扫读时定位得到、又不抢正题的重量。 */
.md-sec { margin: .82em 0; }
.md-sec-h {
  font-weight: 700; color: var(--pri); font-size: .9rem;
  letter-spacing: .01em; margin-bottom: .16em;
}
.md-sec-h::after {
  content: ""; display: block; width: 1.6em; height: 2px; margin-top: .3em;
  background: color-mix(in srgb, var(--pri) 45%, transparent); border-radius: 2px;
}
.md-sec > p { margin: .2em 0 0; }
.md-cmp { border-collapse: collapse; width: 100%; margin: .7em 0; font-size: .86rem; }
.md-cmp th, .md-cmp td { border: 1px solid var(--line); padding: .42em .7em; text-align: left; vertical-align: top; }
.md-cmp th { background: var(--panel-2); color: var(--dim); font-weight: 600; white-space: nowrap; width: 1%; }

/* 代码块 */
.code-wrap { position: relative; margin: .9em 0; border-radius: var(--r-sm); overflow: hidden; border: 1px solid var(--line); background: var(--panel-2); }
.code-h {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 10px; background: var(--panel-3); border-bottom: 1px solid var(--line);
  font-size: .72rem; color: var(--dim); font-family: var(--mono);
}
.code-h .sp { flex: 1; }
.code-copy { border: 0; background: transparent; color: var(--dim); font-size: .72rem; padding: 2px 6px; border-radius: 5px; }
.code-copy:hover { background: var(--panel); color: var(--pri); }
.code-wrap pre { margin: 0; padding: 12px 14px; overflow-x: auto; }
.code-wrap code { font-family: var(--mono); font-size: .82rem; line-height: 1.7; white-space: pre; }
.tk-k { color: #C026D3; } .tk-s { color: #16A34A; } .tk-c { color: var(--dim2); font-style: italic; }
.tk-n { color: #EA580C; } .tk-f { color: #2563EB; } .tk-p { color: var(--dim); }
[data-theme="dark"] .tk-k { color: #F0ABFC; } [data-theme="dark"] .tk-s { color: #86EFAC; }
[data-theme="dark"] .tk-n { color: #FDBA74; } [data-theme="dark"] .tk-f { color: #93C5FD; }

/* 结构化图（AI 产出） */
/* 图示卡：宽度封顶 560px —— 不封顶时 SVG 会撑满 840px 的内容列，
   一张三角形就能占掉整个视口，把上下文全挤出屏幕。560 宽足够看清 A/B/C 和"高"。 */
.art { margin: 1em 0; border: 1px solid var(--line); border-radius: var(--r); background: var(--panel-2); padding: 12px; overflow: hidden; max-width: 560px; }
.art-h { font-size: .8rem; color: var(--dim); margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
.art-h b { color: var(--txt); font-weight: 600; }
.art svg { display: block; width: 100%; height: auto; }
.art .art-bad { font-size: .8rem; color: var(--warn); }

/* 数学（自研轻量渲染） */
.math { font-family: "Times New Roman", var(--mono), serif; font-style: italic; font-size: 1.02em; padding: 0 .1em; }
.math-block { display: block; text-align: center; margin: 1em 0; font-size: 1.12em; }
.frac { display: inline-flex; flex-direction: column; vertical-align: middle; text-align: center; margin: 0 .18em; font-style: normal; }
.frac > .num { border-bottom: 1px solid currentColor; padding: 0 .3em; font-size: .92em; }
.frac > .den { padding: 0 .3em; font-size: .92em; }
.sqrt { display: inline-block; font-style: normal; }
.sqrt > .rad { border-top: 1px solid currentColor; padding: 0 .18em; margin-left: -.06em; }

/* 环境（方程组 / 矩阵 / 对齐）—— \begin{cases} 这类整块排版。
   全部用主题变量，深浅色都跟着走。 */
.math-env { display: inline-flex; align-items: center; gap: .12em; font-style: normal; vertical-align: middle; }
.math-env table { border-collapse: collapse; font-style: italic; }
.math-env td { padding: .12em .45em; text-align: left; white-space: nowrap; }
/* cases：左侧大括号。用缩放的字形做，不引 SVG 字体 */
.cases-d { font-size: 2.7em; font-style: normal; font-weight: 300; line-height: 1; color: var(--txt); transform: scaleX(.62); }
.cases-d.left { margin-right: -.18em; }
.cases-d.right { margin-left: -.18em; }
.math-cases td.cond { font-style: normal; color: var(--dim); font-size: .92em; padding-left: .9em; }
/* 矩阵两侧的圆括号/方括号 */
.math-matrix-wrap > .mtx-d { font-size: 2.2em; font-style: normal; line-height: 1; color: var(--txt); transform: scaleX(.62); }
.math-matrix td { text-align: center; padding: .1em .5em; }
/* aligned：& 前的列右对齐、& 后的列左对齐，等号才能排成一条线 */
.math-aligned td { padding: .1em .2em; }
.math-aligned td.al-r { text-align: right; padding-right: .1em; }
.math-aligned td.al-l { text-align: left; padding-left: .1em; }
.math-block .math-env { display: block; }
.math-block .math-cases-wrap,
.math-block .math-matrix-wrap { display: inline-flex; }

/* ================= 输入区 ================= */
.composer {
  flex: 0 0 auto; display: flex; align-items: flex-end; gap: 8px;
  padding: 12px 18px 14px; border-top: 1px solid var(--line); background: var(--panel);
}
.composer textarea {
  flex: 1; resize: none; min-height: 2.6em; max-height: 9em;
  padding: .6em .85em; line-height: 1.6; border-radius: var(--r);
}
/* ---- 批次205：麦克风挪到文本框右下角 ----
   ★ 用户原话「将语音输入按钮挪到文本框右下角」。以前 #mic 是 textarea 的
     flex 兄弟（横排在左），学生输入时长文本被它挤窄，且和发送按钮并列容易误点。
   ★ 做法与 autoVoice 给其它输入框挂的 .mic-float **完全同构**：文本框外包一层
     position:relative，麦克风 absolute 贴 right/bottom 内角；文本框 padding-right
     让位，文字不会钻到按钮底下。停止/发送按钮留在 flex 行里，位置不变。
   ★ 基准规则写在所有 @media 之前（本项目踩过"基准写在媒体查询之后被盖掉、零报错"的坑）。 */
.composer .input-wrap { position: relative; flex: 1; min-width: 0; display: flex; }
.composer .input-wrap textarea { flex: 1; padding-right: 2.9em; }
.composer .input-wrap .mic {
  position: absolute; right: 6px; bottom: 6px; z-index: 2;
  width: 2em; height: 2em; font-size: .92rem;
}
.mic {
  flex: 0 0 auto; width: 2.4em; height: 2.4em; border-radius: 50%;
  border: 1px solid var(--line-2); background: var(--panel); color: var(--dim);
  display: flex; align-items: center; justify-content: center; font-size: 1rem;
  transition: background var(--dur-1), color var(--dur-1), box-shadow var(--dur-1);
}
.mic:hover { color: var(--pri); border-color: var(--pri); }
.mic.on { background: var(--bad); border-color: var(--bad); color: #fff; animation: pulse 1.4s ease-in-out infinite; }
@keyframes pulse { 0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--bad) 45%, transparent); } 50% { box-shadow: 0 0 0 8px transparent; } }
.voice-hint { padding: 0 18px 8px; font-size: .74rem; color: var(--dim2); min-height: 1.1em; }
/* ================= 学习模式说明卡（批次87）=================
   ★ 用户诉求：「这三种模式切换后，应该有模式使用说明？给几个建议提问？」
   位置：输入框**正上方**（`.composer` 之前）。它是"刚刚切了模式"的一次性回应，
   所以只在 change 事件里弹，刷新页面 / 换对话时收起（见 app.js 的 renderModeCard / setMode）。
   ★ 建议提问是**按钮**不是链接：点一下把原话填进输入框，学生可以改两个字再发。 */
.mode-card {
  flex: 0 0 auto; margin: 0 18px 10px; padding: 11px 13px 12px;
  border: 1px solid var(--pri); border-radius: var(--r);
  background: var(--pri-soft); font-size: .8rem; line-height: 1.6;
}
.mode-card .mc-h { display: flex; align-items: center; gap: 7px; margin-bottom: 5px; }
.mode-card .mc-h b { font-size: .88rem; }
.mode-card .mc-tag {
  font-size: .68rem; padding: .1em .5em; border-radius: 999px;
  background: var(--pri); color: #fff; opacity: .85;
}
.mode-card .mc-desc { margin: 0 0 8px; color: var(--txt); }
.mode-card .mc-asks { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.mode-card .mc-lab { color: var(--dim2); font-size: .74rem; }
.mode-card .mc-ask {
  text-align: left; cursor: pointer; font: inherit; font-size: .76rem;
  padding: .34em .7em; border-radius: 999px;
  border: 1px dashed var(--pri); background: var(--panel); color: var(--pri);
}
.mode-card .mc-ask:hover { border-style: solid; }
/* ================= 课件状态条（批次286）=================
   学生在右边课件里拖滑块 / 改参数时，输入框上方出现这一条，
   告诉他「老师看得见你现在的这个状态」，并给一个 × 让他这次不带。
   ★ 用虚线边框 + 淡底，与 mode-card（实线、pri 色）区分开：
     那条是"教学模式"的常驻说明，这条是"此刻的动态状态"，不该抢它的视觉重量。 */
.ivt-obs {
  flex: 0 0 auto; margin: 0 18px 8px; padding: 7px 10px;
  display: flex; align-items: center; gap: 7px;
  border: 1px dashed var(--line-2); border-radius: var(--r);
  background: var(--panel-2); font-size: .76rem; line-height: 1.5;
  color: var(--dim);
}
.ivt-obs-ico { color: var(--pri); font-size: .9rem; line-height: 1; }
.ivt-obs-t { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ivt-obs-from { flex: 0 0 auto; color: var(--dim2); opacity: .75; font-size: .72rem; }
.ivt-obs-x {
  flex: 0 0 auto; cursor: pointer; font: inherit; font-size: .9rem; line-height: 1;
  width: 20px; height: 20px; border-radius: 50%;
  border: 1px solid var(--line-2); background: transparent; color: var(--dim2);
  display: flex; align-items: center; justify-content: center;
}
.ivt-obs-x:hover { border-color: var(--pri); color: var(--pri); }
/* ================= 通用语音输入按钮（批次83）=================
   ★ 用户要「输入需要超过10个字的地方，都加上语音输入选项」。
   app.js 的 autoVoice() 会给页面上**所有文字输入框**（textarea + 文字类 input，
   但排除聊天主输入框 —— 它有 #mic；也排除登录/注册/重置那几个身份凭证面板）
   自动插入一个 <button class="mic-float">。这里定义它悬浮在输入框右下角的样子。
   ★ 基准规则写在所有**响应式** @media（max-width）之前：本项目踩过
     "基准写在媒体查询之后被直接盖掉、零报错"的坑。 */
.voice-host { position: relative; }
.mic-float {
  position: absolute; right: 8px; bottom: 8px; z-index: 3;
  width: 1.9em; height: 1.9em; border-radius: 50%; padding: 0;
  display: flex; align-items: center; justify-content: center; font-size: .9rem;
  border: 1px solid var(--line-2); background: var(--panel); color: var(--dim);
  opacity: .85; transition: background var(--dur-1), color var(--dur-1), border-color var(--dur-1), opacity var(--dur-1);
}
.mic-float:hover { color: var(--pri); border-color: var(--pri); opacity: 1; }
.mic-float.on { background: var(--bad); border-color: var(--bad); color: #fff; opacity: 1; animation: pulse 1.4s ease-in-out infinite; }
/* ================= 画板（画图回答）=================
   独立覆盖层，不复用 .modal —— 画板要的是一块尽量大的作画区，
   .modal 的 width:min(560px,…) 会把它压成一条。
   ★ 基准规则全部写在所有 @media 之前（本项目踩过"基准写在媒体查询之后
     被直接盖掉、零报错"的坑）。 */
.bd-mask { padding: 16px; }
.bd-panel {
  background: var(--panel); border: 1px solid var(--line); border-radius: 18px;
  width: min(1000px, 100%); max-height: 100%; display: flex; flex-direction: column;
  box-shadow: var(--shadow-lg); animation: rise var(--dur-3) var(--ease-out) both;
  overflow: hidden;
  /* ★★ 必须给 height，不能只给 max-height。
     只给 max-height 时 panel 会**按内容收缩**（实测 900px 视口下只有 320px 高），
     而 .bd-stage 的 flex:1 只能在 panel 已有的高度里分配 —— 结果作画区只剩 148px，
     一条横杠，根本画不了东西。给了 height:100% 才有"剩余空间"可分。 */
  height: 100%;
}
.bd-head {
  display: flex; align-items: center; gap: 10px; padding: 13px 16px;
  border-bottom: 1px solid var(--line); flex: 0 0 auto;
}
.bd-head b { font-size: 1rem; }
.bd-tip { font-size: .75rem; color: var(--dim2); }

.bd-bar {
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  padding: 9px 16px; border-bottom: 1px solid var(--line);
  flex: 0 0 auto; background: var(--panel-2);
}
.bd-grp { display: flex; align-items: center; gap: 5px; }
.bd-sep { width: 1px; height: 20px; background: var(--line-2); }
.bd-t {
  width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--line-2);
  background: var(--panel); color: var(--dim); display: flex; align-items: center;
  justify-content: center; padding: 0;
}
.bd-t:hover { color: var(--pri); border-color: var(--pri); }
/* 选中态：工具按钮与颜色/粗细共用一套"选中"外观，学生一眼看出当前拿的是什么 */
.bd-t[aria-pressed="true"], .bd-dot[aria-pressed="true"], .bd-sz[aria-pressed="true"] {
  border-color: var(--pri); box-shadow: 0 0 0 2px var(--pri-soft);
}
.bd-t[aria-pressed="true"] { color: var(--pri); background: var(--pri-soft); }
.bd-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.bd-dot {
  width: 24px; height: 24px; border-radius: 50%; border: 1px solid var(--line-2);
  padding: 0; position: relative;
}
/* 颜色按钮内部再画一个小圆：border 用来表示选中，内圆才是真颜色 */
.bd-dot::after {
  content: ""; position: absolute; inset: 3px; border-radius: 50%; background: var(--c, #000);
}
.bd-sz {
  width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--line-2);
  background: var(--panel); display: flex; align-items: center; justify-content: center; padding: 0;
}
.bd-sz::after { content: ""; border-radius: 50%; background: var(--dim); width: var(--d, 6px); height: var(--d, 6px); }
.bd-sz-er::after { display: none; }
.bd-sz-ic { display: block; }

/* 作画区：白底 —— 与导出的 PNG 一致，学生看到什么发出去就是什么。
   深色主题下也不改：手写答案就是写在白纸上的，这里保持"纸"的隐喻。
   min-height 给的是**下限**（极矮视口上别把作画区压成一条），
   正常情况由 panel 的 height:100% + flex:1 决定它有多高。 */
.bd-stage {
  flex: 1 1 auto; min-height: 200px; position: relative;
  background: #FFFFFF; overflow: hidden; touch-action: none;
}
.bd-canvas { display: block; cursor: crosshair; touch-action: none; }
.bd-foot {
  display: flex; align-items: center; gap: 10px; padding: 11px 16px;
  border-top: 1px solid var(--line); flex: 0 0 auto;
}
.bd-empty { font-size: .78rem; color: var(--dim2); }


/* ================= 知识卡 ================= */
.card-stats { display: flex; gap: 8px; padding: 14px 18px 4px; flex-wrap: wrap; flex: 0 0 auto; }
.st {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 8px 14px; min-width: 78px; cursor: pointer; transition: border-color var(--dur-1), transform var(--dur-1);
}
.st:hover { border-color: var(--dim2); }
.st:active { transform: translateY(1px); }
.st.on { border-color: var(--pri); background: var(--pri-soft); }
.st b { display: block; font-size: 1.22rem; font-weight: 700; line-height: 1.25; }
.st span { font-size: .72rem; color: var(--dim); }
.st.hot b { color: var(--bad); }
.st.done b { color: var(--ok); }

.card-bar { display: flex; gap: 8px; padding: 10px 18px; align-items: center; flex-wrap: wrap; flex: 0 0 auto; }
.card-bar .inp { flex: 1; min-width: 140px; }
.card-list { flex: 1; overflow-y: auto; padding: 4px 18px 20px; display: flex; flex-direction: column; gap: 9px; }

.kcard {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r);
  padding: 13px 15px; transition: border-color var(--dur-1), box-shadow var(--dur-1);
}
.kcard:hover { border-color: var(--line-2); box-shadow: var(--shadow); }
.kcard.due { border-left: 3px solid var(--warn); }
.kc-top { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; flex-wrap: wrap; }
.kc-k { font-weight: 650; font-size: .95rem; flex: 1; min-width: 0; }
.chip { font-size: .68rem; padding: .12em .55em; border-radius: 20px; font-weight: 600; white-space: nowrap; }
.chip.learning { background: var(--pri-soft); color: var(--pri); }
.chip.almost { background: var(--warn-soft); color: var(--warn); }
.chip.mastered { background: var(--ok-soft); color: var(--ok); }
.chip.retired { background: var(--panel-3); color: var(--dim); }
/* 学科 chip（批次246）：知识卡列表里标出这张卡属于哪个学科 */
.chip.kc-subj { background: var(--panel-3); color: var(--dim); font-weight: 500; }
.chip.ok { background: var(--ok-soft); color: var(--ok); }
.chip.t-choice { background: var(--panel-3); color: var(--dim); }
.chip.t-spelling { background: var(--panel-3); color: var(--dim); }
.chip.t-understanding { background: var(--panel-3); color: var(--dim); }
.kc-q { color: var(--dim); font-size: .86rem; margin: 2px 0 8px; }
.kc-foot { display: flex; align-items: center; gap: 10px; font-size: .74rem; color: var(--dim2); flex-wrap: wrap; }
.kc-foot .sp { flex: 1; }
/* ★ 批次122：「再连对 N 天到『快记住了』」—— 给一个比邻位稍亮的色，
   因为它是这段脚注里唯一**朝向未来**的信息（其余都在说过去/现在）。 */
.kc-foot .kc-next { color: var(--dim); }
.kc-foot .kc-next b { color: var(--pri); font-weight: 700; }
.pager { display: flex; justify-content: center; gap: 6px; padding: 6px 18px 18px; flex: 0 0 auto; }
.pager button { border: 1px solid var(--line-2); background: var(--panel); border-radius: 7px; padding: .25em .7em; font-size: .82rem; color: var(--dim); }
.pager button.on { background: var(--pri); border-color: var(--pri); color: #fff; }
.pager button[disabled] { opacity: .4; pointer-events: none; }

/* 练习弹窗 */
.practice { max-width: 620px; }
.pc-k { font-size: .78rem; color: var(--dim); margin-bottom: 4px; }
.pc-q { font-size: 1.02rem; font-weight: 550; margin-bottom: 16px; line-height: 1.7; }
.pc-opts { display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px; }
.pc-opt {
  display: flex; align-items: flex-start; gap: 10px; text-align: left;
  border: 1px solid var(--line-2); background: var(--panel); border-radius: var(--r-sm);
  padding: .65em .9em; font-size: .93rem; transition: border-color var(--dur-1), background var(--dur-1);
}
.pc-opt:hover { border-color: var(--pri); background: var(--pri-soft); }
.pc-opt .oi {
  flex: 0 0 22px; width: 22px; height: 22px; border-radius: 50%;
  background: var(--panel-3); color: var(--dim); font-size: .76rem; font-weight: 700;
  display: flex; align-items: center; justify-content: center;
}

.pc-opt.on { border-color: var(--pri); background: var(--pri-soft); }
.pc-opt.on .oi { background: var(--pri); color: #fff; }
.pc-opt.right { border-color: var(--ok); background: var(--ok-soft); }
.pc-opt.right .oi { background: var(--ok); color: #fff; }
.pc-opt.wrong { border-color: var(--bad); background: var(--bad-soft); }
.pc-opt.wrong .oi { background: var(--bad); color: #fff; }
.pc-opt[disabled] { pointer-events: none; }

.pc-input { width: 100%; min-height: 5em; border: 1px solid var(--line-2); background: var(--panel); border-radius: var(--r-sm); padding: .7em .9em; resize: vertical; outline: none; }
.pc-input:focus { border-color: var(--pri); box-shadow: 0 0 0 3px var(--pri-soft); }
/* 批次208：练习弹窗的语音按钮不再压「我已经会了」。
   autoVoice 把 .mic-float 插到输入框的父元素、锚在右下角；练习弹窗里输入框的父元素
   原本是整个 .practice（底部还有 .pc-acts 一排按钮），麦克风正好落在「我已经会了」上。
   给输入框单独套一层紧贴的相对定位容器，让麦克风锚到输入框自己的右下角，不再压按钮。 */
.pc-input-wrap { position: relative; }
.pc-input-wrap > .pc-input,
.pc-input-wrap > .inp { display: block; }
.pc-fb { border-radius: var(--r-sm); padding: 10px 13px; font-size: .88rem; margin: 12px 0; line-height: 1.7; }
.pc-fb.ok { background: var(--ok-soft); color: var(--ok); }
.pc-fb.bad { background: var(--bad-soft); color: var(--bad); }
.pc-fb.mid { background: var(--warn-soft); color: var(--warn); }
.pc-fb.neutral { background: var(--panel-2); color: var(--dim); }
/* 批次116：「这次比上次强在哪」（`.pc-vs`）—— 刻意**不复用 `.pc-fb` 的配色**：
   `.pc-fb` 是"这次对了/错了"的**判定**（一张底色卡），这一块是"往前挪了哪几格"的**清单**。
   两者一起出现时，用同一套底色会让人以为它们是同一条结论的两个说法。
   左侧那条 `--ok` 竖线是唯一的"好"的信号 —— 而且这块**只在真有进步时才渲染**。 */
.pc-vs { margin: 12px 0; padding: 10px 13px; border: 1px solid var(--line); border-left: 3px solid var(--ok); background: var(--panel-2); border-radius: var(--r-sm); }
.pc-vs > b { display: block; font-size: .8rem; color: var(--txt); margin-bottom: 6px; }
.pc-vs-i { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; padding: 3px 0; }
.pc-vs-l { font-size: .83rem; font-weight: 700; color: var(--txt); }
.pc-vs-w { font-size: .78rem; color: var(--dim); line-height: 1.5; }
.pc-acts { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 14px; }
.pc-acts .sp { flex: 1; }
/* ================= 上传「放哪儿」弹窗（批次88）=================
   ★ 用户拍板：上传时预选**本子**与**项目**，别让新资料一律落「未归类」+ 不挂项目。
   项目是多选（一份资料可以同时挂在几个项目上）⇒ 用一排可勾的小胶囊，不用下拉多选
   （原生 multiple select 在手机上几乎没法用）。 */
.up-sel { width: 100%; }
.up-pjs { display: flex; flex-wrap: wrap; gap: 6px; }
.up-pj {
  display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
  padding: .3em .7em; border-radius: 999px; font-size: .8rem;
  border: 1px solid var(--line-2); background: var(--panel); color: var(--dim);
}
.up-pj:has(input:checked) { border-color: var(--pri); background: var(--pri-soft); color: var(--pri); font-weight: 600; }
.up-pj input { accent-color: var(--pri); margin: 0; }
.pc-verdict { display: flex; gap: 6px; margin: 8px 0 2px; flex-wrap: wrap; }
.pc-verdict .chip { font-size: .72rem; padding: .25em .7em; }

/* 自由练习开关 / 规则 / AI 收卡建议 */
.pc-free { display: inline-flex; align-items: center; gap: 6px; font-size: .8rem; color: var(--dim); margin: 4px 0 2px; cursor: pointer; user-select: none; }
.pc-free input { width: 15px; height: 15px; accent-color: var(--pri); }
.card-quota { font-size: .72rem; color: var(--dim); border: 1px solid var(--line); border-radius: 999px; padding: 3px 10px; white-space: nowrap; }
.card-quota.low { color: var(--warn); border-color: var(--warn); }
.card-ai { display: flex; align-items: center; gap: 10px; padding: 10px 18px; background: var(--pri-soft); border-radius: var(--r); margin: 4px 18px 0; flex-wrap: wrap; }
.card-ai-ic { color: var(--pri); font-weight: 700; }
.card-ai-txt { font-size: .82rem; color: var(--dim); flex: 1; min-width: 160px; }
.rules { margin: 4px 2px 2px; padding-left: 20px; line-height: 1.9; font-size: .92rem; }
.rules li { margin: 2px 0; }
.sg-note { font-size: .84rem; color: var(--dim); line-height: 1.7; margin-bottom: 12px; }
.sg-row { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--r-sm); margin-bottom: 8px; cursor: pointer; }
.sg-row:hover { border-color: var(--pri); }
.sg-chk { width: 16px; height: 16px; accent-color: var(--pri); flex: 0 0 auto; }
.sg-k { font-weight: 600; font-size: .92rem; flex: 0 0 auto; max-width: 46%; }
.sg-q { font-size: .8rem; color: var(--dim); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }


/* 通用卡片行 */
.row-card {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r);
  padding: 13px 16px; display: flex; align-items: center; gap: 12px;
}
.row-card > div:first-child { flex: 1; min-width: 0; }
.row-card b { font-size: .93rem; }
.row-card .dim { font-size: .78rem; }

/* 项目按学科分组（批次32y-9 D）：原生 <details>，零 JS、键盘可达、窄屏天然自适应。
   ★ 纯非响应式规则，放在所有宽度断点之前 —— 不碰「断点总数」与末尾断点那两条守卫。 */
.proj-group { border: 1px solid var(--line); border-radius: var(--r); background: var(--panel-2); }
.proj-group > summary { cursor: pointer; padding: 10px 14px; font-size: .86rem; color: var(--txt); list-style: none; display: flex; align-items: center; gap: 8px; }
.proj-group > summary::-webkit-details-marker { display: none; }
.proj-group > summary::before { content: "▸"; color: var(--dim2); font-size: .8rem; }
.proj-group[open] > summary::before { content: "▾"; }
.proj-group > summary b { font-weight: 700; }
.proj-group-n { color: var(--dim2); font-size: .78rem; font-weight: 600; }
.proj-group-b { display: flex; flex-direction: column; gap: 9px; padding: 0 10px 10px; }

/* ---- 管理后台：异地运维用得到，要能一眼看清"这是谁、能不能登" ---- */
.adm-tabs { display: flex; gap: 8px; margin-bottom: 12px; }
.adm-tab {
  padding: .35em .85em; border-radius: 20px; border: 1px solid var(--line);
  background: var(--panel); color: var(--dim); font-size: .84rem; cursor: pointer;
}
.adm-tab.on { background: var(--pri-soft); color: var(--pri); border-color: var(--pri); font-weight: 600; }

/* 空间行的操作区（改口令）。窄屏换行，不要挤成一条。 */
.adm-sp-ops { display: flex; gap: 8px; align-items: center; flex: 0 0 auto; flex-wrap: wrap; }
/* "无口令"是个真问题，不能跟"有口令"一样是灰字 —— 那正是本次要修的东西。 */
.adm-nopw { color: var(--bad); font-weight: 600; }
.adm-user {
  display: block; margin-bottom: 10px; align-items: unset;
  border-left: 3px solid var(--line);
}
.adm-user:has(.chip.almost) { border-left-color: var(--warn); }
.adm-u-h { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 4px; }
.adm-u-m { font-size: .76rem; line-height: 1.7; word-break: break-all; }
.adm-u-act { display: flex; gap: 8px; margin-top: 9px; flex-wrap: wrap; }
/* ★ 批次144b：姓名是主标题 —— 管理员在这一屏上做决定，认人靠的就是它。
   字号比原来的 <b> 大一号、字重更实，用户名降到下面的 .adm-u-m 灰字里。 */
.adm-u-name { font-size: .95rem; font-weight: 700; line-height: 1.35; }
/* 姓名未填：给个视觉标记，让人一眼扫出"这一屏里谁还缺姓名"。
   ★ 用 warn 色而不是 danger 色 —— 缺姓名是"待补"，不是"出错"。 */
.adm-u-name--none { color: var(--warn); font-weight: 600; }
.adm-user--noname { border-left-color: var(--warn); }

/* ---- 批次271：用户列表分页（用户原话「太长了，默认每页20个」）----
   ★ 翻页条贴在列表**底部**、右对齐，和 .ad-pager（看板那几张表）保持同一套视觉，
     管理员不用学第二种翻页样式。 */
.adm-pager {
  display: flex; gap: 8px; align-items: center; justify-content: flex-end;
  margin: 4px 0 2px; font-size: .78rem;
}
/* 列表顶部的"共 N 人 · 每页 20 个"：让管理员一眼知道后面还有几页，
   不然翻到底才发现只看了三分之一。 */
.adm-u-head { font-size: .76rem; margin-bottom: 8px; }

/* ---- 批次197：管理后台「公告」tab（草稿 → 人工审核 → 发布） ---- */
.adm-ann { max-width: 760px; }
/* 审核门的说明条：这块必须显眼 —— 管理员得知道"存草稿"不等于"已发布"。 */
.adm-ann-note {
  padding: 10px 12px; margin-bottom: 12px; border-radius: 10px;
  background: var(--pri-soft); color: var(--pri);
  font-size: .84rem; line-height: 1.75;
}
.adm-ann-note b { font-weight: 700; }
.adm-ann-form { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }
.adm-ann-in, .adm-ann-ta {
  width: 100%; box-sizing: border-box; padding: .55em .7em;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--panel); color: var(--fg); font-size: .9rem; font-family: inherit;
}
.adm-ann-ta { min-height: 7em; resize: vertical; line-height: 1.7; }
.adm-ann-in:focus, .adm-ann-ta:focus { outline: none; border-color: var(--pri); }
.adm-ann-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.adm-ann-lv { font-size: .84rem; color: var(--dim); display: flex; align-items: center; gap: 5px; }
.adm-ann-sec { margin: 14px 0 6px; font-size: .88rem; }
/* 待审草稿用左侧警示边 —— 跟"已发布"一眼区分开，不会看错。 */
.adm-ann-item.is-draft { border-left: 3px solid var(--warn); padding-left: 10px; }
.adm-ann-acts { display: flex; gap: 8px; margin-top: 8px; }
/* xs：比 .btn.sm 再小一号，用于塞进标题行、不把那一行撑高 */
.btn.xs { padding: 2px 8px; font-size: .72rem; line-height: 1.5; }

/* ---- 管理后台：创建空间的授权码（批次59，2026-10-06）----
   它是"能不能建空间"的闸门，固定在空间列表上方。视觉与「入班码」同一套：
   码要大到一眼能抄走 —— 管理员的工作就是把它念给 / 发给要建空间的人。 */
.adm-grant { display: block; align-items: unset; margin-bottom: 12px; border-left: 3px solid var(--warn); }
.adm-grant-code {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 8px 0 2px; padding: .5em .7em;
  border: 1px dashed var(--line-2); border-radius: var(--r-sm); background: var(--panel-2);
}
.adm-grant-code b { font-family: var(--mono); font-size: 1.5rem; letter-spacing: .22em; color: var(--pri); }

/* ---- 管理后台：班级（批次58）----
   班级从"学生自己建"改成"管理员建 + 管理员主动拉人"。这块要装下：
   一个班一行、入班码（要能一键复制）、以及一个可展开的「拉人 / 成员」面板。 */
.adm-cls { display: block; margin-bottom: 10px; align-items: unset; border-left: 3px solid var(--pri); }
.adm-cls-code {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 7px; padding: .42em .65em;
  border: 1px dashed var(--line-2); border-radius: var(--r-sm); background: var(--panel-2);
}
.adm-cls-code span { font-size: .74rem; color: var(--dim2); }
.adm-cls-code b { font-family: var(--mono); font-size: 1.08rem; letter-spacing: .14em; color: var(--pri); }
.adm-cls-code i { font-style: normal; font-size: .72rem; color: var(--dim2); }
/* 展开区：拉人面板与成员名单都落在这里，跟行之间拉一条分隔线 */
.adm-cls-pane {
  margin-top: 10px; padding-top: 10px;
  border-top: 1px solid var(--line); 
}
.adm-pick-h { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
.adm-pick-q {
  flex: 1 1 200px; min-width: 0; padding: .42em .7em;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--panel); color: var(--txt); font-size: .84rem;
}
.adm-pick-list { max-height: 260px; overflow: auto; border: 1px solid var(--line); border-radius: var(--r-sm); }
.adm-pick-i {
  display: flex; align-items: center; gap: 8px; padding: .45em .65em;
  border-bottom: 1px solid var(--line); cursor: pointer; font-size: .84rem;
}
.adm-pick-i:last-child { border-bottom: 0; }
.adm-pick-i:hover { background: var(--panel-2); }
/* 已经在班里的人：不可勾（灰掉），一眼能和"可选"分开 */
.adm-pick-i.in { background: var(--panel-3); cursor: default; }
.adm-pick-i.in .adm-pick-n { color: var(--dim); }
.adm-pick-n { font-weight: 600; }
.adm-pick-m { font-size: .74rem; }
.adm-pick-note { font-size: .74rem; line-height: 1.7; margin-top: 8px; }
.adm-cls-memlist { border: 1px solid var(--line); border-radius: var(--r-sm); }
.adm-cls-mem {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: .45em .65em; border-bottom: 1px solid var(--line); font-size: .82rem;
}
.adm-cls-mem:last-child { border-bottom: 0; }
.adm-cls-mn { font-weight: 600; }

/* ================= 硅基世界护照卡（批次144c）================= */
/* ★ 图片当底图（1672×941 设计稿），动态的两块（编号/日期）用同色块盖掉再叠真值。
   为什么不用半透明：设计稿底下有印刷底纹，半透明会透出假编号的轮廓。
   位置/字号/颜色 = 三重视觉验证实测（_passport_cmp{,_2,_3}.png）：
     ①只盖不填⇒假值消失无接缝；②填相同串⇒与原图重合；③填真值⇒位置字号自然。
   字号用 cqw 随卡宽等比缩（换显示尺寸不露馅），详见 .pass-img-no 注释。
   ★ 类名 pass-img-* 独立命名（**不能复用** 护照页的 .pass-card）：同名类在
     同一张样式表里两级互相覆盖 —— 2026-10-20 实测撞名把护照页金卡的渐变/
     金边/外发光和封面编号的字号/颜色全改错了。 */
.pass-img { position: relative; width: 100%; max-width: 640px; margin: 0 auto;
  border-radius: 16px; overflow: hidden; box-shadow: var(--shadow);
  border: 1px solid var(--line); background: #FDF9EE; container-type: inline-size; }
.pass-img-bg { display: block; width: 100%; height: auto; user-select: none; -webkit-user-drag: none; }
/* 编号：三重视觉验证实测（位置/宽高 + 字号字重颜色，出处见 CSS 注释头） */
.pass-img-no { position: absolute; left: 29.7%; top: 44.4%; width: 44.5%; height: 8.2%;
  display: flex; align-items: center; background: #FDF9EE;
  font-size: 5.2cqw; font-weight: 700; letter-spacing: 0; color: #0A1440;
  white-space: nowrap; font-variant-numeric: tabular-nums; }
/* 日期：同上 */
.pass-img-date { position: absolute; left: 29.7%; top: 62.0%; width: 22.5%; height: 6.6%;
  display: flex; align-items: center; background: #FDF9EE;
  font-size: 3.6cqw; font-weight: 600; letter-spacing: .01em; color: #0A1440;
  white-space: nowrap; font-variant-numeric: tabular-nums; }
/* 字号为什么用 cqw 不是 rem/vmin：卡是等比缩放的图，字必须跟着卡宽一起缩 ——
   rem 在手机上字比图大、桌面上字比图小，会露出被盖的区域；vmin 跟视口不跟卡宽。
   cqw = 容器宽度百分比（.pass-img 已设 container-type: inline-size），永远同步。 */

/* 首次领证弹窗（z-index 92：盖得住 .mask(80)，盖不住引导层 .gp-layer(95)）*/
.pass-dlg-mask { position: fixed; inset: 0; z-index: 92; background: rgba(12,16,26,.62);
  display: flex; align-items: center; justify-content: center; padding: 20px;
  backdrop-filter: blur(3px); animation: fade var(--dur-2) ease both; }
.pass-dlg { background: var(--panel); border: 1px solid var(--line); border-radius: 20px;
  width: min(680px, 100%); max-height: 92vh; overflow-y: auto; padding: 26px 24px 22px;
  box-shadow: var(--shadow-lg); text-align: center;
  animation: rise var(--dur-3) var(--ease-out) both; }
.pass-dlg-crest { font-size: 2.2rem; line-height: 1; margin-bottom: 6px; }
.pass-dlg-title { margin: 0 0 8px; font-size: 1.28rem; letter-spacing: .01em; }
.pass-dlg-sub { margin: 0 0 18px; font-size: .9rem; line-height: 1.7; color: var(--dim); }
.pass-dlg-sub strong { color: var(--txt); }
.pass-dlg-ok { margin-top: 18px; min-width: 132px; }

/* ================= 弹窗 / 提示 ================= */
.mask {
  position: fixed; inset: 0; z-index: 80; background: rgba(12, 16, 26, .5);
  display: flex; align-items: center; justify-content: center; padding: 20px;
  backdrop-filter: blur(2px); animation: fade var(--dur-2) ease both;
}
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.modal {
  background: var(--panel); border: 1px solid var(--line); border-radius: 18px;
  width: min(560px, 100%); max-height: 86vh; display: flex; flex-direction: column;
  box-shadow: var(--shadow-lg); animation: rise var(--dur-3) var(--ease-out) both;
}
.modal-h { display: flex; align-items: center; gap: 10px; padding: 15px 18px; border-bottom: 1px solid var(--line); flex: 0 0 auto; }
.modal-h b { font-size: 1rem; }
.modal-b { padding: 18px; overflow-y: auto; }

/* 密码 / 口令浮层（2026-10-06）
   ★ z-index 必须高于 .mask(80) —— 管理后台弹窗里也会叠着弹它。
   输入框默认 type="password"（遮蔽）；只有"展示新口令给你抄"那次才临时切 text。 */
.pw-mask { z-index: 90; }
.pw-modal { width: min(420px, 100%); }
.pw-hint { margin: 0 0 10px; font-size: .84rem; line-height: 1.6; }
.pw-input { width: 100%; box-sizing: border-box; letter-spacing: .06em; }
.pw-err { min-height: 1.2em; margin-top: 6px; font-size: .8rem; color: var(--bad); }
/* 批次332：家长只读链接二维码。★ 二维码必须留在纯白底上（暗色主题也不改背景），
   否则黑码压在深色面板上扫不出来；静区由后端 SVG 自带的 4 模块留白。 */
.pw-qr { margin-top: 12px; text-align: center; }
.pw-qr-h { font-size: .84rem; color: var(--dim); margin-bottom: 6px; }
.pw-qr-a { display: inline-block; line-height: 0; border-radius: 10px; }
.pw-qr-img { display: block; width: 190px; height: 190px; background: #fff; border-radius: 10px; }
.pw-qr-tip { margin-top: 6px; font-size: .76rem; color: var(--dim); }
@media (max-width: 420px) { .pw-qr-img { width: 180px; height: 180px; } }

/* 从错题本导入（wn = wrong-notebook）
   ★ 只用主题变量，别写死颜色 —— 亮/暗两套主题都要能看。 */
.wn-p { margin: 0 0 12px; line-height: 1.6; font-size: .9rem; }
.wn-hint { margin: 8px 0 0; font-size: .82rem; color: var(--dim); line-height: 1.55; }
.wn-err { color: var(--bad); font-size: .86rem; }
.wn-row { margin: 10px 0 4px; }
.wn-row .inp { width: 100%; }
.wn-prev { margin: 6px 0 14px; font-size: .88rem; line-height: 1.6; }
.wn-sample { margin: 8px 0 0; padding-left: 18px; color: var(--dim); }
.wn-sample li { margin: 3px 0; font-size: .84rem; }
.wn-acts { display: flex; align-items: center; gap: 10px; }
.wn-acts .sp { flex: 1 1 auto; }
/* 目标项目下拉 + 说明 */
.wn-top { margin: 0 0 10px; font-size: .88rem; }
.wn-top .inp { margin-left: 6px; min-width: 180px; }
.wn-top .wn-hint { margin-top: 4px; }
/* 错题勾选列表：撑高但封顶，超出的滚动 —— 一次列几十题也不至于把弹窗顶出屏幕 */
.wn-list {
  max-height: 46vh; overflow: auto;
  border: 1px solid var(--line); border-radius: 8px;
  padding: 4px; margin: 0 0 12px;
}
.wn-list .wn-all {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 8px; border-bottom: 1px solid var(--line);
  font-size: .84rem;
  position: sticky; top: 0;
  background: var(--panel); z-index: 1;
}
.wn-list .wn-all label { display: flex; align-items: center; gap: 6px; cursor: pointer; }
.wn-list .wn-all .dim { margin-left: auto; }
.wn-li {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 7px 8px; border-radius: 6px; cursor: pointer; line-height: 1.5;
}
.wn-li:hover { background: var(--panel-2); }
.wn-li input { margin-top: 3px; flex: 0 0 auto; }
.wn-li-t { flex: 1 1 auto; font-size: .87rem; word-break: break-word; }
.wn-li-m { flex: 0 0 auto; font-size: .78rem; color: var(--dim); white-space: nowrap; }

/* 错题本分区（SSO 免密内嵌）。
   ★ 为什么要 flex:1 + min-height:0：分区容器本身是 flex 列，iframe 要"撑满剩余高度"。
     只写 height:100% 在 flex 子项里算不出基准高度，会塌成 0 —— min-height:0 才让
     flex 子项允许被压缩、从而正确分配。这是 iframe 嵌进 flex 布局最常见的坑。 */
.wn-frame-wrap { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.wn-frame-tip { padding: 18px 4px; font-size: .9rem; line-height: 1.7; max-width: 520px; }
.wn-frame {
  flex: 1 1 auto; width: 100%; min-height: 0;
  border: 0; border-radius: 10px; background: var(--panel);
  /* 让内嵌页面在深色主题下不至于刺眼：与自身主题一致时无副作用 */
  color-scheme: normal;
}

/* ★ 批次310（用户 2026-10-29：「错题本没有看到成长值规则」）。
   ★ 为什么压成一行 + flex-wrap：它是"看一眼就过"的说明，不该占掉 iframe 的高度
     （错题本自己还要滚动）；窄屏自然折行，**不另起媒体查询** ——
     本文件的媒体查询总数是钉死的（见 `_padfloatcheck` F4）。
   ★ `[hidden]` 要单独写 display:none：`display:flex` 会盖掉 hidden 属性的默认样式，
     不写这一条，服务端没给规则时会留下一个空的白条。 */
.wn-rule {
  flex: 0 0 auto; display: flex; flex-wrap: wrap; align-items: baseline;
  margin: 0 0 10px; padding: 8px 12px; border-radius: 10px;
  background: var(--bg); border: 1px solid var(--line);
  font-size: .8rem; line-height: 1.6; color: var(--dim);
}
.wn-rule[hidden] { display: none; }
.wn-rule b { color: var(--txt); font-weight: 600; }
.wn-rule-t {
  flex: 0 0 auto; margin-right: 8px; padding: 1px 7px; border-radius: 999px;
  background: var(--pri-soft); color: var(--pri); font-weight: 600; font-size: .74rem;
}
.wn-rule-sep { color: var(--line); margin: 0 8px; }
.toast {
  position: fixed; left: 50%; bottom: 32px; transform: translateX(-50%);
  background: var(--txt); color: var(--bg); padding: .55em 1.1em; border-radius: 22px;
  font-size: .86rem; z-index: 9500; box-shadow: var(--shadow-lg); max-width: 84vw; text-align: center;
  animation: toastIn var(--dur-3) var(--ease-out) both;
}
@keyframes toastIn { from { opacity: 0; transform: translate(-50%, 10px); } to { opacity: 1; transform: translate(-50%, 0); } }

/* 骨架屏 */
.skel { background: var(--panel-2); border-radius: var(--r-sm); height: 1em; animation: shimmer 1.4s ease-in-out infinite; }
@keyframes shimmer { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }

/* 图表（原生 SVG） */
.chart { margin: 4px 0 2px; }
.chart-t { font-size: .82rem; color: var(--dim); margin-bottom: 2px; }
.chart-lg { display: flex; gap: 14px; flex-wrap: wrap; font-size: .74rem; color: var(--dim); margin-bottom: 4px; }
.chart-lg span { display: inline-flex; align-items: center; gap: 5px; }
.chart-lg i { width: 9px; height: 9px; border-radius: 3px; display: inline-block; }
.chart svg { display: block; width: 100%; height: auto; }

/* 看板 */
.dash-grid { display: flex; gap: 8px; flex-wrap: wrap; padding: 14px 18px 4px; }
.dash-kpi {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 9px 15px; min-width: 92px;
}
.dash-kpi b { display: block; font-size: 1.35rem; font-weight: 700; line-height: 1.2; }
.dash-kpi span { font-size: .72rem; color: var(--dim); }
.dash-kpi.up b { color: var(--ok); }
.dash-kpi.down b { color: var(--warn); }
.dash-panel {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r);
  padding: 14px 16px; margin-bottom: 10px;
}
.dash-panel > h3 { margin: 0 0 10px; font-size: .88rem; font-weight: 600; display: flex; align-items: center; gap: 8px; }
.dash-panel > h3 span { font-weight: 400; color: var(--dim2); font-size: .74rem; }
.report-lines { margin: 0; padding-left: 1.1em; font-size: .88rem; line-height: 1.85; color: var(--txt); }
.report-lines li { margin: .15em 0; }
.report-lines li::marker { color: var(--pri); }
.weak-i { display: flex; align-items: center; gap: 10px; padding: .5em 0; border-bottom: 1px solid var(--line); font-size: .86rem; }
.weak-i:last-child { border-bottom: 0; }
.weak-i .w-t { flex: 1; min-width: 0; }
.weak-i .w-n { font-family: var(--mono); font-size: .78rem; color: var(--bad); white-space: nowrap; }

/* ================= 学习日报（批次10）================= */
/* #dashBody 是 .card-list（flex 列 + overflow-y:auto），日报与看板是它里面两个普通块。
   ★ 都写 flex:none —— 默认 flex-shrink:1 时，两块内容一高就会被压缩，
     又变回"各自滚动、两块都看不全"。 */
#dailyBox, #dashPanels { flex: none; }

/* 周报（批次16 + 增强） */
#weeklyBox { margin-bottom: 10px; padding: 14px; background: var(--panel); border-radius: 12px; border: 1px solid var(--line); }

/* 头部：标题 + 日期选择器。
   ★ 这里不用 flex-wrap —— 中文/日期混排时 wrap 会让「–」分隔符单独掉到下一行。 */
.w-header { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; flex-wrap: wrap; }
.w-header h3 { margin: 0; font-size: .92rem; font-weight: 600; color: var(--txt); white-space: nowrap; }
.w-cur { font-size: .68rem; font-weight: 500; color: var(--pri); background: var(--pri-soft); border-radius: 999px; padding: 1px 7px; margin-left: 4px; vertical-align: 1px; }
.w-dates { font-size: .74rem; color: var(--dim); }
.w-pick { display: flex; align-items: center; gap: 5px; }
/* 批次251：日期框前面的「数据范围」小标签。
   ★ 加它的原因不是好看：用户把这两个日期当成了**家长链接的有效期**来提需求
     （「有效期设置为永久有效」）。写清楚这是"看哪几天的数据"，
     比在别处补一句解释管用 —— 误解就发生在这一行的位置上。 */
.w-pick-lb { font-size: .7rem; color: var(--dim); white-space: nowrap; }
.w-date {
  font-size: .74rem; padding: 4px 7px; border: 1px solid var(--line-2); border-radius: var(--r-xs);
  background: var(--panel); color: var(--txt); min-width: 0;
}
/* ★ 批次106：原来只有 border-color 变化，键盘用户几乎看不见 —— 补上与 .inp 同款的外环。
   （这里保留 outline: none：环用 box-shadow 画，避免和 border-radius 打架。） */
.w-date:focus { outline: none; border-color: var(--pri); box-shadow: 0 0 0 3px var(--pri-soft); }
.w-tilde { color: var(--dim2); font-size: .74rem; }

/* 快捷区间 */
.w-quicks { display: flex; align-items: center; gap: 6px; margin-bottom: 12px; flex-wrap: wrap; }
.w-q {
  font-size: .72rem; padding: 3px 10px; border: 1px solid var(--line-2); border-radius: 999px;
  background: var(--panel); color: var(--dim); white-space: nowrap;
}
.w-q:hover { border-color: var(--pri); color: var(--pri); }
.w-q.on { background: var(--pri-soft); border-color: var(--pri); color: var(--pri); font-weight: 600; }
.w-limit { font-size: .68rem; color: var(--dim2); margin-left: 2px; }

.w-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; align-items: stretch; }
/* ★ 卡片必须等高：#weeklyBox 被截图/导出时是按元素高度裁的，
   高度参差会让"算不出来的那张"只露出半截说明字。 */
.w-kpi {
  display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
  text-align: center; padding: 10px 6px; background: var(--bg); border-radius: 8px; border: 1px solid var(--line);
}
.w-kpi b { display: block; font-size: 1.25rem; font-weight: 700; color: var(--pri); margin-bottom: 2px; line-height: 1.2; }
/* ★ 算不出来的数字用灰色 + 比正文还小的说明字，视觉上就"低一档" ——
   补一个 0 会被误读成"这周一次都没对"，那是完全不同的一件事。 */
.w-kpi b.w-na { color: var(--dim2); font-weight: 600; }
.w-kpi span { font-size: .72rem; color: var(--dim); line-height: 1.3; }
.w-kpi .w-why { display: block; font-style: normal; font-size: .64rem; color: var(--dim2); line-height: 1.35; margin-top: 3px; }
.w-kpi .w-sub { display: block; font-style: normal; font-size: .66rem; color: var(--dim2); margin-top: 3px; }

/* ---- 数字溯源（批次22 前端接线）----
   基调：让"能点的数字"看起来**像数字本身可点**，而不是旁边多挂一个按钮。
   点击后明细展开在 .w-grid 下方（#weeklyEv），而不是浮层 —— 浮层在窄屏会盖住数字本身。
   ★ 布局要点：按钮是 inline，下划线只画在**数字那一行**，不画到「明细」上。
     若把 <u>明细</u> 也纳入下划线范围，它会跟数字挤在一行里（实测：数字和「明细」重叠）。 */
.w-kpi b .w-ev-btn {
  display: inline-block; padding: 0; border: none; background: none; cursor: pointer;
  font: inherit; color: inherit; line-height: inherit; font-weight: inherit;
  text-align: center;
}
.w-kpi b .w-ev-btn .w-ev-v {
  display: block; border-bottom: 1px dashed currentColor;  /* 虚线 = "这里可以再往下看一层" */
}
.w-kpi b .w-ev-btn:hover { color: var(--pri); }
.w-kpi b .w-ev-i {
  display: block; margin-top: 2px; font-size: .6rem; font-weight: 500;
  color: var(--dim2); text-decoration: none; letter-spacing: .04em; border-bottom: none;
}
.w-kpi b .w-ev-btn:hover .w-ev-i { color: var(--pri); }

.w-ev-sec { margin-top: 10px; }
/* 空容器在没内容时不占位：#weeklyEv 是全页常驻节点，
   不去掉它的 margin/padding 会在数字和「这几天」之间多出一块空白。 */
.w-ev-sec:empty { display: none; }
.w-ev-sec[data-open="0"] { display: none; }
.w-ev-hint { margin: 6px 0 0; font-size: .68rem; }
.w-ev-h {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding-bottom: 6px; border-bottom: 1px solid var(--line); margin-bottom: 8px;
}
.w-ev-h b { font-size: .8rem; color: var(--txt); }
.w-ev-range { font-size: .68rem; color: var(--dim2); font-family: var(--mono); }
.w-ev-x {
  margin-left: auto; flex: none; width: 22px; height: 22px; padding: 0;
  border: 1px solid var(--line); border-radius: 6px; background: var(--panel);
  color: var(--dim); font-size: .9rem; line-height: 1; cursor: pointer;
}
.w-ev-x:hover { border-color: var(--pri); color: var(--pri); }
.w-ev-list { max-height: 260px; overflow-y: auto; }
.w-ev-g { margin-bottom: 10px; }
.w-ev-g i { display: block; font-style: normal; font-size: .66rem; color: var(--dim2); margin-bottom: 4px; font-family: var(--mono); }
.w-ev-r { display: flex; align-items: baseline; gap: 8px; padding: 4px 0; border-bottom: 1px solid var(--line); }
.w-ev-r:last-child { border-bottom: none; }
.w-ev-t { flex: 1; min-width: 0; font-size: .78rem; color: var(--txt); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.w-ev-m { flex: none; font-size: .68rem; color: var(--dim2); font-family: var(--mono); white-space: nowrap; }
.w-ev-none { font-size: .78rem; color: var(--dim); line-height: 1.6; }
.w-ev-more { margin: 6px 0 0; font-size: .68rem; color: var(--dim2); }

/* ---- 与上一段对比（批次22-②）----
   ★★ 刻意**不用红绿**（这是本项目一条明确的产品主张）：
     记录多不等于学得好，用红/绿等于把"多"判成"好"。
     所以 up / down 用同一族的**中性强调色**，只在形态上区分（↑↓ 不画，改文字"多/少"）。
   ★ "没法比"必须是独立的一档样式：它既不是"一样"也不是"0"，不能混进 flat。 */
.w-cmp-row {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 6px 0; border-bottom: 1px solid var(--line);
}
.w-cmp-row:last-of-type { border-bottom: none; }
.w-cmp-n { flex: none; width: 6.4em; font-size: .78rem; color: var(--txt); }
.w-cmp-v { flex: none; min-width: 3.4em; font-size: .86rem; font-weight: 700; color: var(--txt); font-family: var(--mono); }
.w-cmp-l { flex: 1; min-width: 0; font-size: .7rem; color: var(--dim2); font-family: var(--mono); }
.w-cmp-row em { flex: none; font-style: normal; font-size: .7rem; white-space: nowrap; }
/* up / down 同色 —— 只表达"变多了 / 变少了"，不表达"好 / 坏" */
.w-cmp-up, .w-cmp-down { color: var(--pri); font-family: var(--mono); }
.w-cmp-flat { color: var(--dim2); }
.w-cmp-na { color: var(--dim2); }   /* 「没法比」与「一样」视觉上须区分：na 更淡且带说明 */

/* ---- 7 阶段成长画像（批次22-③）----
   ★ 设计基调：这是一张"形状图"，不是成绩单。
     所以全页**不出现分数、百分比大字、排名**；雷达只表达"哪块凹下去"，
     维度用文字说清"这个数是从哪来的"。
   ★ 算不出来的维度：整块降低对比 + 明确写「还看不出来」，绝不显示 0%。 */
.g-wrap {
  margin-bottom: 12px; padding: 12px 14px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 10px;
}
.g-err { font-size: .82rem; color: var(--dim); padding: 6px 0; }
.g-head h4 { margin: 0 0 6px; font-size: .86rem; font-weight: 600; color: var(--txt); }
.g-head h4 span { font-weight: 400; font-size: .7rem; color: var(--dim2); margin-left: 6px; }
.g-next { margin: 0 0 10px; font-size: .78rem; color: var(--dim); line-height: 1.65; }
.g-next b { color: var(--pri); }
.g-practice { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 0 0 12px; padding: 9px 10px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; }
.g-practice span { font-size: .72rem; color: var(--dim); line-height: 1.5; }
.g-practice .btn { flex: none; }

/* 雷达 + 维度列表：窄屏竖排，宽屏左右并排 */
.g-body { display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap; }
.g-radar { flex: none; width: 300px; max-width: 100%; height: auto; }
.g-dims { flex: 1; min-width: 200px; }
.g-dim { padding: 6px 0; border-bottom: 1px solid var(--line); }
.g-dim:last-child { border-bottom: none; }
.g-dim-h { display: flex; align-items: baseline; gap: 8px; }
.g-dim-h b { flex: 1; font-size: .78rem; color: var(--txt); }
.g-dim-v { flex: none; font-size: .84rem; font-weight: 700; color: var(--pri); font-family: var(--mono); }
.g-dim-v i { font-style: normal; font-size: .62rem; font-weight: 400; color: var(--dim2); margin-left: 1px; }
/* 「还看不出来」与「0%」必须是两种样子 —— 这就是规矩②在图上的体现 */
.g-na {
  flex: none; font-style: normal; font-size: .66rem; color: var(--dim2);
  padding: 1px 6px; border: 1px dashed var(--line-2); border-radius: 999px;
}
.g-dim.na b { color: var(--dim); }
.g-dim p { margin: 3px 0 0; font-size: .68rem; color: var(--dim2); line-height: 1.5; }

/* 阶梯：走到 = 实心，当前下一步 = 描边高亮，没到 = 灰空心。
   ★ 不用"等级名"、不给星星，只标"已走到"。 */
.g-lad-h { margin: 14px 0 8px; font-size: .82rem; font-weight: 600; color: var(--txt); }
.g-ladder { display: flex; flex-direction: column; gap: 2px; }
.g-step { display: flex; gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--line); }
.g-step:last-child { border-bottom: none; }
.g-dot {
  flex: none; width: 22px; height: 22px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-style: normal; font-size: .68rem; font-family: var(--mono);
  background: var(--panel); border: 1px solid var(--line-2); color: var(--dim2);
}
.g-step.on .g-dot { background: var(--pri); border-color: var(--pri); color: #fff; font-weight: 700; }
.g-step.next .g-dot { border-color: var(--pri); color: var(--pri); border-width: 2px; }
.g-step-b { flex: 1; min-width: 0; }
.g-step-b b { display: block; font-size: .8rem; color: var(--txt); }
.g-step.on .g-step-b b { color: var(--pri); }
.g-ok {
  font-size: .62rem; font-weight: 500; color: var(--pri); margin-left: 6px;
  padding: 1px 5px; border: 1px solid var(--pri); border-radius: 999px;
}
.g-desc { display: block; font-size: .68rem; color: var(--dim); margin-top: 1px; }
.g-step-b p { margin: 2px 0 0; font-size: .68rem; color: var(--dim2); line-height: 1.5; }

/* 分区小节 */
.w-sec { margin-top: 16px; }
.w-sec h4 { margin: 0 0 8px; font-size: .82rem; font-weight: 600; color: var(--txt); }
.w-sec h4 span { font-weight: 400; font-size: .7rem; color: var(--dim2); margin-left: 6px; }
.w-none { font-size: .82rem; }
.w-note { margin: 8px 0 0; font-size: .72rem; color: var(--dim2); line-height: 1.6; }
.w-cover { margin: 0; font-size: .82rem; color: var(--dim); line-height: 1.7; }
.w-cover b { color: var(--txt); }

/* 逐天条：有记录=实心，没记录=灰色空心。
   ★ 「空」和「0」在视觉上必须不同 —— 这正是周报规矩②在界面上的体现。 */
.w-days { display: flex; gap: 6px; flex-wrap: wrap; }
.w-day {
  flex: 1 1 0; min-width: 58px; padding: 8px 4px; text-align: center;
  background: var(--panel-2); border: 1px dashed var(--line-2); border-radius: 8px;
}
.w-day.has { background: var(--pri-soft); border-style: solid; border-color: var(--pri); }
.w-day i { display: block; font-style: normal; font-size: .66rem; color: var(--dim); margin-bottom: 3px; white-space: nowrap; }
.w-day.has i { color: var(--pri); }
.w-day b { display: block; font-size: 1rem; font-weight: 700; color: var(--txt); line-height: 1.2; }
/* 「空」不是「算不出来的 —」：那天只是没发生任何事。同族样式、不同类名。 */
.w-day b.w-blank { color: var(--dim2); font-size: .78rem; font-weight: 500; }
.w-day em { display: block; font-style: normal; font-size: .66rem; color: var(--dim2); margin-top: 2px; }

/* 按科目：条形只表达"对/错的构成"，不给科目评分 */
.w-sub-row { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
.w-sub-n { flex: none; width: 5.2em; font-size: .78rem; color: var(--txt); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.w-bar { flex: 1; height: 9px; border-radius: 999px; background: var(--bad-soft); overflow: hidden; }
.w-bar-ok { display: block; height: 100%; background: var(--ok); border-radius: 999px; }
.w-sub-v { flex: none; font-size: .7rem; color: var(--dim); white-space: nowrap; font-family: var(--mono); }

/* 卡住的卡 */
.w-stuck { display: flex; align-items: baseline; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--line); }
.w-stuck:last-child { border-bottom: none; }
.w-stuck-t { flex: 1; min-width: 0; font-size: .82rem; color: var(--txt); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.w-stuck-n { flex: none; font-size: .72rem; color: var(--bad); font-family: var(--mono); white-space: nowrap; }

.w-empty { font-size: .85rem; color: var(--dim); padding: 8px 0; }

/* ---- 历史周报（批次22）：只存"人写的那段" ----
   设计基调：这一块是全页**唯一写入**的东西，所以给它一个和纯事实区不同的边框，
   让人一眼看出"下面这两句是我写的、会被保存"。
   但**不给它更强的强调色** —— 写感想不该比看事实更显眼。 */
.w-note-sec {
  padding: 12px; border: 1px solid var(--line-2); border-radius: 10px;
  background: var(--panel-2);
}
.w-note-sec h4 { margin: 0 0 6px; }
.w-note-l { display: block; margin-top: 10px; }
.w-note-l b { display: block; font-size: .78rem; font-weight: 600; color: var(--txt); margin-bottom: 5px; }
.w-note-l input {
  width: 100%; box-sizing: border-box; padding: 8px 10px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
  color: var(--txt); font-size: .85rem; font-family: inherit;
}
/* ★ 批次106：同上，只有边框变色 = 键盘看不见焦点，补外环。 */
.w-note-l input:focus { outline: none; border-color: var(--pri); box-shadow: 0 0 0 3px var(--pri-soft); }
.w-note-act { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
.w-rd { margin-top: 10px; }
.w-rd b { display: block; font-size: .78rem; font-weight: 600; color: var(--txt); margin-bottom: 3px; }
.w-rd p { margin: 0; font-size: .85rem; color: var(--dim); line-height: 1.7; white-space: pre-wrap; word-break: break-word; }
.w-final-tag {
  font-size: .66rem; font-weight: 500; color: var(--pri);
  background: var(--pri-soft); padding: 2px 7px; border-radius: 999px; margin-left: 6px;
}

/* 历史列表：每一条是一个按钮（点开=跳到那一段区间） */
.w-hist {
  display: block; width: 100%; text-align: left; box-sizing: border-box;
  padding: 10px; margin-bottom: 8px; cursor: pointer;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 10px;
  font-family: inherit; color: inherit;
}
.w-hist:hover { border-color: var(--pri); }
.w-hist-h { display: flex; align-items: center; gap: 8px; }
.w-hist-h b { font-size: .8rem; color: var(--txt); font-family: var(--mono); }
.w-hist-st { font-size: .64rem; font-style: normal; color: var(--dim); border: 1px solid var(--line-2); border-radius: 999px; padding: 1px 6px; white-space: nowrap; }
.w-hist-st.final { color: var(--pri); border-color: var(--pri); }
.w-hist-n { margin-top: 4px; font-size: .7rem; color: var(--dim2); font-family: var(--mono); }
.w-hist-t { margin-top: 5px; font-size: .78rem; color: var(--dim); line-height: 1.6; overflow: hidden; text-overflow: ellipsis; }

/* 阶段进度推进条（看板）：回答"整批卡走到哪一步了" */
.pg-bar { display: flex; height: 22px; border-radius: 999px; overflow: hidden; background: var(--panel-2); border: 1px solid var(--line); }
.pg-bar i { display: block; height: 100%; }
.pg-bar i:first-child { border-top-left-radius: 999px; border-bottom-left-radius: 999px; }
.pg-bar i:last-child { border-top-right-radius: 999px; border-bottom-right-radius: 999px; }
.pg-lg { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 9px; }
.pg-lg span { font-size: .74rem; color: var(--dim); white-space: nowrap; }
.pg-lg span i { display: inline-block; width: 9px; height: 9px; border-radius: 2px; margin-right: 4px; vertical-align: -1px; }
.pg-lg span b { color: var(--txt); font-family: var(--mono); margin-left: 2px; }
.d-daily { padding-bottom: 16px; }
.d-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; }
.d-head h3 { margin: 0; font-size: .92rem; font-weight: 600; }
.d-head h3 span { font-weight: 400; color: var(--dim2); font-size: .74rem; margin-left: 6px; }
.d-nav { margin-left: auto; display: flex; gap: 6px; }
.d-headline { margin: 2px 0 12px; font-size: .9rem; color: var(--txt); line-height: 1.6; }

/* 数字：每个都可点开看依据 */
.d-nums { display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); gap: 8px; }
.d-num {
  display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
  text-align: left; border: 1px solid var(--line); background: var(--panel-2);
  border-radius: var(--r-sm); padding: .6em .7em; cursor: pointer;
  transition: border-color var(--dur-1), background var(--dur-1), transform var(--dur-1);
}
.d-num:hover { border-color: var(--pri); background: var(--pri-soft); }
.d-num:active { transform: translateY(1px); }
.d-num b { font-size: 1.2rem; font-weight: 700; line-height: 1.25; color: var(--txt); }
.d-num span { font-size: .74rem; color: var(--dim); line-height: 1.4; }
.d-num i { font-style: normal; font-size: .68rem; color: var(--pri); margin-top: 3px; }
/* ★ 算不出来的数字：灰掉、值显示 —，并把"为什么算不出来"直接写在卡片上，
     而不是藏在 tooltip 里 —— 数据诚实要看得见才算数。 */
.d-num.na { cursor: default; background: var(--panel); }
.d-num.na:hover { border-color: var(--line); background: var(--panel); }
.d-num.na b { color: var(--dim2); }
.d-num.na i { color: var(--dim2); line-height: 1.45; }
.d-hint { margin: 8px 0 0; font-size: .74rem; color: var(--dim2); line-height: 1.6; }

/* 四问 */
.d-qs { margin-top: 18px; display: flex; flex-direction: column; gap: 14px; }
.d-q { border-left: 2px solid var(--line-2); padding-left: 12px; }
.d-q-h { display: flex; gap: 8px; align-items: baseline; }
.d-q-h i {
  font-style: normal; flex: none; width: 18px; height: 18px; border-radius: 50%;
  background: var(--pri-soft); color: var(--pri); font-size: .68rem; font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center;
}
.d-q-h span { font-size: .88rem; font-weight: 600; color: var(--txt); line-height: 1.5; }
.d-f { margin: 7px 0 0; padding-left: 1.05em; font-size: .84rem; color: var(--dim); line-height: 1.8; }
.d-f li::marker { color: var(--pri); }
.d-f li.d-un { color: var(--dim2); font-style: italic; }
.d-f li.d-un::marker { color: var(--dim2); }
.d-ev {
  border: 0; background: transparent; color: var(--pri); font-size: .74rem;
  text-decoration: underline; text-underline-offset: 3px; padding: 0 2px; cursor: pointer;
}
.d-ev:hover { color: var(--pri-2); }
.d-ans {
  display: block; width: 100%; margin-top: 8px; resize: vertical;
  border: 1px solid var(--line-2); background: var(--panel); color: var(--txt);
  border-radius: var(--r-sm); padding: .5em .7em; font: inherit; font-size: .86rem;
  line-height: 1.7; outline: none; transition: border-color var(--dur-1), box-shadow var(--dur-1);
}
.d-ans:focus { border-color: var(--pri); box-shadow: 0 0 0 3px var(--pri-soft); }
.d-ans[readonly] { background: var(--panel-2); color: var(--dim); }

.d-savebar { display: flex; align-items: center; gap: 10px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); }
.d-savebar > span { flex: 1; min-width: 0; font-size: .76rem; color: var(--dim2); }

/* 折叠块：不评分的原因 / 这份日报不能证明什么 */
.d-fold { margin-top: 10px; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--panel-2); }
.d-fold > summary { cursor: pointer; padding: .55em .8em; font-size: .82rem; font-weight: 600; color: var(--txt); list-style: none; }
.d-fold > summary::-webkit-details-marker { display: none; }
.d-fold > summary::before { content: "▸ "; color: var(--dim2); }
.d-fold[open] > summary::before { content: "▾ "; }
.d-ns { padding: 0 .8em .5em; }
.d-ns-i { padding: .5em 0; border-top: 1px solid var(--line); }
.d-ns-i:first-child { border-top: 0; }
.d-ns-i b { display: block; font-size: .8rem; color: var(--txt); margin-bottom: 2px; }
.d-ns-i p { margin: 0; font-size: .78rem; color: var(--dim); line-height: 1.75; }
.d-lim { margin: 0; padding: .1em .8em .7em 2em; font-size: .78rem; color: var(--dim); line-height: 1.8; }
.d-lim li::marker { color: var(--dim2); }

.d-hwrap { margin-top: 16px; }
.d-ht { font-size: .76rem; color: var(--dim2); margin-bottom: 6px; }
.d-hist { display: flex; flex-wrap: wrap; gap: 6px; }
.d-hb {
  display: flex; flex-direction: column; align-items: flex-start;
  border: 1px solid var(--line); background: var(--panel); color: var(--txt);
  border-radius: var(--r-sm); padding: .35em .6em; font-size: .78rem; cursor: pointer;
}
.d-hb:hover { border-color: var(--pri); }
.d-hb i { font-style: normal; font-size: .66rem; color: var(--dim2); }

/* 溯源弹窗 */
.ev-list { display: flex; flex-direction: column; gap: 8px; }
.ev-i { border: 1px solid var(--line); border-radius: var(--r-sm); padding: .55em .7em; background: var(--panel-2); }
.ev-h { display: flex; align-items: baseline; gap: 8px; }
.ev-h b { flex: 1; min-width: 0; font-size: .86rem; }
.ev-h span { font-family: var(--mono); font-size: .74rem; color: var(--dim2); }
.ev-i p { margin: 4px 0 0; font-size: .8rem; color: var(--dim); line-height: 1.7; }
.ev-v { margin-top: 4px; font-size: .74rem; color: var(--dim2); }

/* ================= 家长视角（批次20）=================
   设计基调：**比看板更安静**。
   家长这一页的读者是"不坐在电脑前的大人"，他会在手机上、在晚上翻一下。
   所以：更大的字号、更少的颜色、更多的解释文字。
   颜色只用在**一句话总结**那一块（三种语气），其余一律走中性色 ——
   颜色本身在暗示"好/坏"，而这一页刻意不做评判。 */
.p-head {
  border: 1px solid var(--line); border-left: 3px solid var(--pri);
  border-radius: var(--r-sm); background: var(--panel-2);
  padding: .8em .95em; margin-bottom: 14px;
}
.p-head-t { font-size: 1rem; font-weight: 700; color: var(--txt); line-height: 1.6; margin-bottom: 6px; }
.p-head-p { margin: 0 0 8px; font-size: .82rem; color: var(--dim); line-height: 1.85; }
.p-head-a {
  font-size: .82rem; color: var(--txt); line-height: 1.7;
  border-top: 1px dashed var(--line); padding-top: 8px;
}
.p-head-a > span {
  display: inline-block; font-size: .7rem; font-weight: 700; color: var(--pri);
  margin-right: 6px; white-space: nowrap;
}
/* 三种语气：只在左侧色条 + 标题色上区分，不做整块底色（底色太重会像"警告"） */
.p-tone-quiet { border-left-color: var(--dim2); }
.p-tone-quiet .p-head-t { color: var(--dim); }
.p-tone-gap { border-left-color: #e0a33b; }
.p-tone-gap .p-head-a > span { color: #b8791f; }
.p-tone-active { border-left-color: var(--pri); }

/* 有记录的天：一天一个小方块，颜色深浅不表达好坏，只表达"有没有" */
.p-days { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.p-day {
  display: flex; flex-direction: column; align-items: center;
  border: 1px solid var(--line); background: var(--panel); border-radius: var(--r-sm);
  padding: .3em .55em; min-width: 44px;
}
.p-day i { font-style: normal; font-size: .64rem; color: var(--dim2); }
.p-day b { font-size: .86rem; color: var(--txt); }

/* 「算不出来」说明框：和 KPI 卡上的「—」配套，语气要平，不能像报错 */
.p-unknown-box {
  margin-top: 10px; padding: .6em .8em;
  border: 1px dashed var(--line); border-radius: var(--r-sm); background: var(--panel-2);
}
.p-unknown-box b { display: block; font-size: .8rem; color: var(--txt); margin-bottom: 3px; }
.p-unknown-box p { margin: 0; font-size: .78rem; color: var(--dim); line-height: 1.75; }

/* 可以帮上忙的地方：一条一条，不并排（并排会被读成"待办清单"） */
.p-help {
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--panel); padding: .6em .8em; margin-bottom: 8px;
}
.p-help-h { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
.p-help-a { flex: 1; min-width: 0; font-size: .86rem; font-weight: 600; color: var(--txt); }
.p-help-n { font-size: .72rem; color: var(--dim2); white-space: nowrap; }
.p-help-t { margin: 0; font-size: .8rem; color: var(--dim); line-height: 1.75; }

/* 他自己写的那段 */
.p-note {
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--panel); padding: .6em .8em; margin-bottom: 8px;
}
.p-note-d { font-size: .7rem; color: var(--dim2); margin-bottom: 5px; }
.p-note-l { margin: 0 0 4px; font-size: .82rem; color: var(--txt); line-height: 1.8; }
.p-note-l:last-child { margin-bottom: 0; }
.p-note-l > span {
  display: inline-block; font-size: .68rem; font-weight: 700; color: var(--dim2);
  margin-right: 6px; white-space: nowrap;
}
.p-note-l > span::after { content: "："; }

/* 现在有什么（存量，只摆不评）*/
.p-now { display: flex; flex-wrap: wrap; gap: 8px; }
.p-now-i {
  display: flex; align-items: baseline; gap: 4px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--panel); padding: .3em .6em;
}
.p-now-i b { font-size: .9rem; color: var(--txt); }
.p-now-i i { font-style: normal; font-size: .7rem; color: var(--dim2); }

/* 能力中心 */
/* ★★ 网格密度（2026-10-05 用户要求"一屏看到更多卡片"）：
   minmax 从 240px 收到 190px ⇒ 1440 视口从 3 列变 5 列；卡片内边距同步收紧。
   放宽列的代价是「描述」一行放不下更多字 —— 所以描述改成最多 2 行省略号，
   把"看全说明"这件事交给点击后的**详情弹窗**（.sk-mask），而不是挤在卡片里。 */
.skill-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 9px; }
.skill-c {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r);
  padding: 10px 12px; transition: border-color var(--dur-1), background var(--dur-1), transform var(--dur-1);
  display: flex; flex-direction: column; gap: 4px;
}
.skill-c:hover { border-color: var(--dim2); }
.skill-c.on { border-color: var(--pri); background: var(--pri-soft); }
/* 卡片主体 = "打开详情"的可点区。它必须自己是个整块可点元素，
   不能靠外层 .skill-c 承担点击 —— 否则开关也会连带触发详情。 */
.skill-c .sc-main { display: flex; flex-direction: column; gap: 4px; cursor: pointer; border-radius: var(--r-sm); }
.skill-c .sc-main:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }
.skill-c .sc-top { display: flex; align-items: center; gap: 7px; }
.skill-c .sc-n { font-weight: 600; font-size: .88rem; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.skill-c .sc-d {
  font-size: .76rem; color: var(--dim); line-height: 1.5;
  /* 最多两行 + 省略号：190px 宽的列里，三行以上会把卡片拉得参差不齐。
     想看全就走详情弹窗（.sc-main 点开）。 */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.skill-c .sc-b { font-size: .68rem; color: var(--dim2); display: flex; gap: 8px; align-items: center; }
.skill-c .sc-more { margin-left: auto; color: var(--pri); opacity: 0; transition: opacity var(--dur-1); }
.skill-c:hover .sc-more { opacity: 1; }
/* 技能图标（batch25）：57 条技能各一个手写原生 SVG。
   描边用 currentColor ⇒ 深浅主题、hover、选中态自动跟随，不写死颜色。
   尺寸固定在标题行里，不随字号放大缩小，保证 57 个形状观感一致。 */
.skill-c .sc-ico { flex: 0 0 20px; width: 20px; height: 20px; color: var(--dim); display: flex; align-items: center; justify-content: center; }
.skill-c:hover .sc-ico { color: var(--fg); }
.skill-c.on .sc-ico { color: var(--pri); }
.ski { width: 20px; height: 20px; display: block; }
/* 「用它开一段对话」：卡片内的次级动作。不做成实心主按钮 ——
   卡片整体点一下是"开详情"，这个按钮是"直接开始用"，两者权重必须分开。 */
/* ---- 批次107：AI 生成的图标 ----------------------------------------------------
   图标不再是手写 SVG，而是 AI 出图后切好的 PNG。★ 关键做法：**PNG 当 alpha 蒙版用**，
   而不是直接 <img>。形状和颜色因此是分开的两件事 —— 深浅主题、hover、选中态照旧
   由 `currentColor` 决定，和原来 SVG 的观感完全一致；直接放 <img> 就只能吃 PNG 里
   那层死掉的蓝色，深色主题下会瞎。
   ★ 尺寸**沿用各容器原来的口径，一个都不改** —— 换图标不该顺手动布局。 */
.ai-i {
  display: inline-block; flex: none;
  width: 16px; height: 16px;
  background-color: currentColor;
  -webkit-mask: var(--ai) center / contain no-repeat;
          mask: var(--ai) center / contain no-repeat;
}
.ski.ai-i { width: 20px; height: 20px; }
.ec-i .ai-i { width: 16px; height: 16px; margin: 5px; }
.pm-ic .ai-i { display: block; width: 17px; height: 17px; }
.bd-t .ai-i { width: 16px; height: 16px; }
.ct .ai-i { width: 15px; height: 15px; }
.skill-c .sc-go {
  margin-top: 3px; align-self: flex-start;
  font: inherit; font-size: .72rem; color: var(--pri);
  background: none; border: 1px solid var(--line-2); border-radius: 999px;
  padding: 2px 9px; cursor: pointer; transition: border-color var(--dur-1), background var(--dur-1);
}
.skill-c .sc-go:hover { border-color: var(--pri); background: var(--pri-soft); }
.skill-c .sc-go:active { transform: translateY(1px); }
/* 技能开关（.sc-sw 是"启用/取消"的可点区域，独立于卡片主体）。
   ★ 2026-10-05：从"点整卡切换"改成"只点这个开关才切换" ——
     学生想看说明时不再误开技能。触摸目标靠 padding 撑到 28px 以上。 */
.skill-c .sc-sw {
  flex: 0 0 auto; display: inline-flex; align-items: center;
  padding: 4px 4px; margin: -4px -2px -4px 0; border-radius: 999px; cursor: pointer;
}
.skill-c .sc-sw:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }
.sc-sw .tgl { width: 30px; height: 18px; border-radius: 11px; background: var(--panel-3); position: relative; flex: 0 0 30px; transition: background var(--dur-2); }
.sc-sw .tgl::after { content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: var(--panel); box-shadow: var(--shadow); transition: transform var(--dur-2); }
.sc-sw.on .tgl { background: var(--pri); }
.sc-sw.on .tgl::after { transform: translateX(12px); }
/* 备考教练专区（2026-10-04 新增；2026-10-05 明确排在学科卡**之后**）：
   与学科卡网格同一套 .skill-c，单独成区、不进学科筛选。
   用一条虚线顶边 + 小标题把它和上方学科卡区分开。 */
.skill-zone { margin-top: 16px; padding-top: 12px; border-top: 1px dashed var(--line-2); }
.skill-zone .sz-head {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: .8rem; font-weight: 700; color: var(--pri);
  margin-bottom: 9px; padding-left: 9px; border-left: 3px solid var(--pri);
}
.coach-zone .skill-c { border-left: 3px solid var(--pri); }

/* ================= 技能详情弹窗（2026-10-05）=================
   不复用 .modal（560px 塞不下"分条方法清单"）⇒ 独立覆盖层，宽到 920px。
   ★ 基准规则一律写在所有 @media 之前。 */
.sk-mask { padding: 18px; }
.sk-panel {
  background: var(--panel); border: 1px solid var(--line); border-radius: 18px;
  width: min(920px, 100%); height: min(86vh, 760px); max-height: 100%;
  display: flex; flex-direction: column;
  box-shadow: var(--shadow-lg); animation: rise var(--dur-3) var(--ease-out) both;
  overflow: hidden;
}
.sk-head {
  display: flex; align-items: center; gap: 10px; padding: 14px 18px;
  border-bottom: 1px solid var(--line); flex: 0 0 auto;
}
.sk-head b { font-size: 1.02rem; }
.sk-body { flex: 1 1 auto; overflow-y: auto; padding: 18px 20px; }
.sk-foot {
  display: flex; align-items: center; gap: 10px; padding: 12px 18px;
  border-top: 1px solid var(--line); flex: 0 0 auto; background: var(--panel-2);
}
.sk-hero { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.sk-ico { flex: 0 0 44px; width: 44px; height: 44px; border-radius: 12px; background: var(--pri-soft); color: var(--pri); display: flex; align-items: center; justify-content: center; }
.sk-ico .ski { width: 26px; height: 26px; }
.sk-name { font-size: 1.1rem; font-weight: 700; }
.sk-tags { display: flex; gap: 6px; margin-top: 5px; flex-wrap: wrap; }
.sk-tag { font-size: .7rem; color: var(--dim); background: var(--panel-2); border: 1px solid var(--line); border-radius: 999px; padding: 2px 9px; }
.sk-tag.on { color: var(--pri); border-color: var(--pri); background: var(--pri-soft); }
.sk-desc { font-size: .9rem; color: var(--txt); line-height: 1.65; margin: 0 0 16px; }
.sk-sec-h {
  font-size: .82rem; font-weight: 700; color: var(--dim); margin: 0 0 10px;
  padding-left: 9px; border-left: 3px solid var(--pri);
}
/* 方法清单：宽屏两栏，窄屏一栏（见文件末尾的窄屏回退）。
   用 CSS 多列而不是 grid —— 条目高度不一，多列能自动平衡高度。 */
.sk-steps { margin: 0; padding-left: 0; list-style: none; columns: 2; column-gap: 26px; }
.sk-steps li {
  font-size: .84rem; line-height: 1.7; color: var(--txt);
  padding-left: 20px; position: relative; margin: 0 0 9px;
  break-inside: avoid;
}
.sk-steps li::before {
  content: ""; position: absolute; left: 6px; top: .62em;
  width: 5px; height: 5px; border-radius: 50%; background: var(--pri);
}
.sk-none { font-size: .86rem; color: var(--dim); }
.sk-note { font-size: .78rem; color: var(--dim2); line-height: 1.6; margin-top: 14px; padding-top: 12px; border-top: 1px dashed var(--line-2); }
/* 详情浮层打开时锁背景滚动（否则手机上能滚到底下的列表） */
body.sk-open { overflow: hidden; }


/* 知识库 */
.kb-drop {
  border: 1.5px dashed var(--line-2); border-radius: var(--r); background: var(--panel-2);
  padding: 22px 16px; text-align: center; color: var(--dim); font-size: .86rem;
  cursor: pointer; transition: border-color var(--dur-1), background var(--dur-1); margin: 0 18px 12px;
}
.kb-drop:hover, .kb-drop.over { border-color: var(--pri); background: var(--pri-soft); color: var(--pri); }
.kb-drop b { color: var(--txt); }
.kb-drop .dim { font-size: .76rem; margin-top: 5px; }
.doc-i {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r);
  padding: 12px 15px; display: flex; align-items: center; gap: 12px;
}
.doc-i > div:first-child { flex: 1; min-width: 0; }
.doc-i b { font-size: .92rem; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.doc-i .dim { font-size: .75rem; margin-top: 2px; }
.doc-i.bad { border-color: var(--warn); background: var(--warn-soft); }
.doc-i .doc-err { font-size: .76rem; color: var(--warn); margin-top: 4px; line-height: 1.6; }
/* 批次119：识别失败时的「怎么办」引导。
   ★ 与 .doc-err（只说哪里错了）分工：这一块说**下一步做什么**。
   ★ 只加普通规则、不新增 @media（首个响应式 @media 之前，别把它的位置往后推）。
   ★ 复用既有 token（--panel/--line/--dim2/--fg），不手搓颜色。 */
.doc-i .doc-fix {
  margin-top: 6px; padding: 9px 11px; font-size: .76rem; line-height: 1.7; color: var(--dim2);
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-sm);
}
.doc-i .doc-fix b { color: var(--fg); font-weight: 600; }
.doc-i .doc-fix .land-link { display: inline-block; margin-top: 7px; }
/* 扫描件识别进度：条要细、提示要小 —— 它是"在动"的证据，不是主角 */
.doc-i .doc-prog {
  margin-top: 6px; height: 4px; border-radius: 999px;
  background: var(--pri-soft); overflow: hidden; max-width: 420px;
}
.doc-i .doc-prog i {
  display: block; height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, var(--pri), #38BDF8);
  transition: width var(--dur-4) ease;
}
.doc-i .doc-prog-t { font-size: .74rem; color: var(--dim2); margin-top: 4px; }

/* ---------- 教材 / 资料查看器（批次34「看原页」；批次127 加 AI 版 + 跳页 + 目录） ---------- */
/* 教材小字多，能放大是必须的————放不大看不清就等于没给。 */
.pgv { display: flex; flex-direction: column; gap: 10px; }
/* ★★ 批次155：教材查看器全屏（AI 版 / 原版共用）。目标 = #mask 整层：全屏后
   .mask 的 flex 居中照常工作，.modal 铺满、.modal-b→.pgv→.pgv-stage 逐级吃掉
   剩余高度（覆盖 stage 的 58vh 定高）。退出后全部自然回落，无需 JS 恢复样式。 */
.mask:fullscreen { background: #10131a; padding: 12px; }
.mask:fullscreen .modal { width: min(1400px, 100%); max-height: none; height: 100%; }
.mask:fullscreen .modal-b { flex: 1; min-height: 0; padding: 12px 14px; }
.mask:fullscreen .pgv { flex: 1; min-height: 0; }
.mask:fullscreen .pgv-stage { height: auto; flex: 1; min-height: 0; }
.mask:-webkit-full-screen { background: #10131a; padding: 12px; }
.mask:-webkit-full-screen .modal { width: min(1400px, 100%); max-height: none; height: 100%; }
/* ★ 批次127：AI 版 / 原版 双页签。放在工具栏**上方**——先选"看哪种"，
   再决定翻到哪页，顺序跟学生的思路一致。 */
.pgv-tabs { display: flex; gap: 6px; border-bottom: 1px solid var(--line); padding-bottom: 8px; }
.pgv-tab {
  background: none; border: 1px solid transparent; border-radius: var(--r-sm, 6px);
  color: var(--dim); font: inherit; font-size: .86rem; padding: 5px 14px; cursor: pointer;
  transition: color var(--dur-2) ease, background var(--dur-2) ease, border-color var(--dur-2) ease;
}
.pgv-tab:hover { color: var(--txt); }
.pgv-tab.on { color: var(--pri); border-color: var(--pri); background: var(--pri-weak, rgba(0,0,0,.04)); font-weight: 600; }
.pgv-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.pgv-bar .sp { flex: 1; }
/* 页码输入框：原来 `1 / 209` 里的 `1` 只是个 <b>，现在换成能输入的框。
   宽度按 4 位数留（1000+ 页的教材也有），右对齐让数字跟后面的 `/ 209` 贴齐。 */
.pgv-jump {
  width: 64px; padding: 4px 6px; text-align: right; font: inherit; font-size: .9rem;
  color: var(--txt); background: var(--bg-2, #f4f6fa);
  border: 1px solid var(--line); border-radius: var(--r-sm, 6px);
}
.pgv-jump:focus { outline: none; border-color: var(--pri); }
/* 数字框的加减箭头在窄宽下会挤掉数字，去掉（学生直接输更快）。 */
.pgv-jump::-webkit-outer-spin-button, .pgv-jump::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.pgv-jump { -moz-appearance: textfield; }
.pgv-no { font-size: .84rem; color: var(--dim); }
.pgv-no b { color: var(--txt); font-size: .95rem; }
.pgv-z { font-size: .78rem; color: var(--dim2); min-width: 44px; text-align: center; }
/* 舞台有固定高度且可滚动：放大后图会超出，要能拖着看。 */
.pgv-stage {
  background: var(--bg-2, #f4f6fa); border: 1px solid var(--line); border-radius: var(--r);
  height: 58vh; overflow: auto; padding: 12px; text-align: center;
}
.pgv-img { display: block; margin: 0 auto; width: 100%; height: auto; border-radius: 4px;
  box-shadow: 0 2px 12px rgba(0,0,0,.10); background: #fff; }
/* ★ 批次127：AI 正文。用 <pre> 是为了保住服务端提取时的换行（数学公式一行一条），
   但 **必须 `white-space: pre-wrap`** —— 正文行长短不一，不换行会横向拉出滚动条，
   学生每读一行都要左右拖。等宽字体也会让中文变丑，所以用正文字体。 */
.pgv-ai {
  display: block; margin: 0; padding: 4px 6px; text-align: left;
  font-family: inherit; font-size: .92rem; line-height: 1.75; color: var(--txt);
  white-space: pre-wrap; word-break: break-word;
}
/* ★★ 批次149：教材 AI 版**图文混排**。
   用户原话：「图1是竞品的PDF教材解析，图文并茂；图2是你的PDF解析，纯文字展示，我需要图1的」。
   正文走 `.pgv-ai-doc`（逐段 <p>），插图走 `.pgv-fig` 插在引用它的那段后面。
   ★ `.pgv-ai`（<pre>）**保留不动** —— 这一页没有可用插图时仍走它，批次127 的 C6
     断言（长行不横向拉出滚动条）守着那一支；两条支路并存是有意的，不是遗留。
   ★ 不新增 @media（`_p25check`/`_mapcheck` 守着数量与位置）。
   ★★ 注意：本段注释里**不许出现** `.pgv-ai` 那条规则的属性词（重复了就会把批次127
     反证⑱ 的改动兜住 —— 把那条属性删掉后 C6 仍能在注释里读到它，实测漏网过一次）。
   ★★ 另外「变」+「异」这两个字**也不许出现在 CSS 里** —— `_combonegative.cjs` 的 I 组
     红线逐字扫 app.css，注释里写了同样算污染（本批实测撞了一次）。 */
.pgv-ai-doc { padding: 4px 6px; text-align: left; font-size: .92rem; line-height: 1.75; color: var(--txt); }
.pgv-ai-doc .pgv-ai-p { margin: 0 0 6px; word-break: break-word; }
.pgv-fig {
  margin: 8px 0 12px; padding: 0; display: block; text-align: center;
}
.pgv-fig-img {
  display: block; margin: 0 auto; max-width: 100%; height: auto;
  border-radius: 6px; border: 1px solid var(--line); background: #fff;
}
.pgv-fig-cap { margin-top: 4px; font-size: .76rem; color: var(--dim); }
/* 没能在正文里定位到的图（无图号 / 正文没引用）收在页尾，**不丢** */
.pgv-figs { margin-top: 14px; padding-top: 10px; border-top: 1px dashed var(--line); }
.pgv-figs-h { font-size: .78rem; color: var(--dim); margin-bottom: 6px; }
.pgv-tip { font-size: .74rem; text-align: center; }
/* ★ 批次127：目录。默认 hidden（点「目录」才展开），条数可能几十条 ⇒ 自身可滚动。 */
.pgv-toc {
  max-height: 34vh; overflow: auto; border: 1px solid var(--line); border-radius: var(--r);
  background: var(--bg-2, #f4f6fa); padding: 6px;
}
.pgv-toc-h { font-size: .78rem; padding: 8px 6px; }
.pgv-toc-i {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  background: none; border: none; border-radius: var(--r-sm, 6px);
  color: var(--txt); font: inherit; font-size: .84rem; padding: 6px 8px; cursor: pointer;
}
.pgv-toc-i:hover { background: var(--pri-weak, rgba(0,0,0,.05)); }
.pgv-toc-i.unit { font-weight: 600; margin-top: 4px; }
.pgv-toc-i .pgv-toc-t { flex: 1; }
.pgv-toc-i .pgv-toc-p { color: var(--dim2); font-size: .76rem; }
/* 定位不到真实 PDF 页的条目：**不可点**（拿书页号硬跳会跳错页），灰掉并禁掉指针。 */
.pgv-toc-i.na { color: var(--dim2); cursor: default; }
.pgv-toc-i.na:hover { background: none; }
/* 「看全文」图文混排（批次37）：文字流里按位置插入教材插图。
   图自成一块、居中、宽度受限 —— 教材插图多为横向，铺满会太大、也会挤掉文字。 */
.kb-text-with-figs { }
.kb-page-no { margin: 1.4em 0 .4em; font-size: .78rem; font-weight: 600; color: var(--dim2);
  letter-spacing: .04em; border-bottom: 1px dashed var(--line); padding-bottom: 3px; }
.kb-text-with-figs .kb-page-no:first-child { margin-top: 0; }
.kb-fig { margin: 1em auto; text-align: center; max-width: 92%; }
.kb-fig a { display: inline-block; line-height: 0; }
.kb-fig img { max-width: 100%; height: auto; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: #fff; box-shadow: 0 1px 6px rgba(0,0,0,.06); transition: box-shadow var(--dur-1) ease; }
.kb-fig a:hover img { box-shadow: 0 3px 14px rgba(0,0,0,.14); }
.kb-fig figcaption { margin-top: .35em; font-size: .76rem; color: var(--fg); line-height: 1.5; }
/* 长描述（没有短标签时的回落）—— 更小更灰，因为它本来只是给检索用的说明 */
.kb-fig-desc { font-size: .72rem; color: var(--dim); font-weight: 400; }
.kb-fig-h { margin: 1.6em 0 .6em; font-size: .82rem; color: var(--dim2); }
.hit-i { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 11px 14px; font-size: .84rem; }
.hit-i .h-f { font-size: .74rem; color: var(--pri); margin-bottom: 4px; font-weight: 600; }
.hit-i mark { background: color-mix(in srgb, var(--warn) 30%, transparent); color: inherit; border-radius: 3px; padding: 0 2px; }
.cat-bar { display: flex; gap: 7px; padding: 0 18px 10px; flex-wrap: wrap; align-items: center; }
.cat-chip {
  border: 1px solid var(--line-2); background: var(--panel); border-radius: 18px;
  padding: .22em .8em; font-size: .8rem; color: var(--dim); cursor: pointer;
}
.cat-chip.on { background: var(--pri); border-color: var(--pri); color: #fff; }
/* 能力页有两排筛选（分类 / 学科）。第二排顶到第一排下面，只留一行的间距 —— 
   两排都带 padding-bottom 的话中间会空出 20px，看着像两块不相干的东西。 */
#skillSubjects { padding-top: 0; }
#skillSubjects .cat-chip { font-size: .76rem; padding: .16em .7em; }

/* ================= 知识库主工作区 =================
   左边"本子树"+ 右边二级分区。原来 11 根平级导航里除对话外的 9 个功能
   都收在右边的分区条上，记忆 / 设置 走右上角头像菜单。 */
.kb-wrap { flex: 1; min-height: 0; display: flex; }
.kb-tree {
  flex: 0 0 220px; border-right: 1px solid var(--line);
  display: flex; flex-direction: column; padding: 12px 10px 16px; overflow-y: auto;
}
.kb-tree-h {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 6px 8px; color: var(--dim2); font-size: .74rem; font-weight: 600; letter-spacing: .05em;
}
.kb-tree-new {
  border: 1px solid var(--line-2); background: var(--panel); color: var(--dim);
  width: 22px; height: 22px; line-height: 1; border-radius: 6px; font-size: .95rem;
}
.kb-tree-new:hover { background: var(--pri-soft); border-color: var(--pri); color: var(--pri); }
.kb-tree-list { display: flex; flex-direction: column; gap: 2px; }
/* ★ 批次88：左树分两组（本子 / 按项目）。分组标题要**明显比行轻**，
   不然会被当成一个可以点的格子；也不给 hover，它本来就不是选项。 */
.kb-tree-g {
  padding: 8px 6px 3px; font-size: .68rem; color: var(--dim2);
  letter-spacing: .04em; font-weight: 600;
}
.kb-tree-g:first-child { padding-top: 2px; }
.kb-tree-i {
  display: flex; align-items: center; gap: 6px; padding: .46em .6em;
  border-radius: var(--r-sm); color: var(--dim); font-size: .86rem; cursor: pointer;
}
.kb-tree-i:hover { background: var(--panel-2); color: var(--txt); }
.kb-tree-i.on { background: var(--pri-soft); color: var(--pri); font-weight: 600; }
.kb-tree-i .kt-n { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kb-tree-i .kt-c { font-size: .7rem; color: var(--dim2); font-variant-numeric: tabular-nums; white-space: nowrap; }
.kb-tree-i .kt-c.full { color: var(--warn); }
.kb-tree-i .kt-x {
  border: 1px solid var(--line-2); background: var(--panel); color: var(--warn);
  font-size: .68rem; border-radius: 5px; padding: 1px 5px; line-height: 1.5;
}
.kb-tree-i .kt-x:hover { background: var(--warn); border-color: var(--warn); color: #fff; }
.kb-tree-foot { margin-top: auto; padding: 10px 6px 0; font-size: .72rem; color: var(--dim2); line-height: 1.6; }

.kb-main { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
.kb-tabs { display: flex; gap: 4px; padding: 9px 18px 0; flex-wrap: wrap; flex: 0 0 auto; }
.kb-tab {
  border: 0; background: transparent; color: var(--dim);
  padding: .38em .8em; border-radius: 999px; font-size: .85rem; font-weight: 500;
  display: inline-flex; align-items: center; gap: 5px;
}
.kb-tab:hover { background: var(--panel-2); color: var(--txt); }
.kb-tab.on { background: var(--pri-soft); color: var(--pri); font-weight: 600; }
.kb-tab-b {
  background: var(--bad); color: #fff; font-size: .66rem; font-weight: 700;
  border-radius: 8px; min-width: 16px; text-align: center; line-height: 16px; height: 16px; padding: 0 4px;
}
.kb-sub { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
/* 分区里的顶栏就是"控制条"，不再重复画一条下边框 —— 上面已经有分区条了 */
.kb-sub > .topbar { border-bottom: 0; padding-bottom: 4px; }

/* ★ 设置页的滚动区（批次99）。
   .kb-sub 是 overflow:hidden + 纵向 flex，全站约定是"顶栏钉住、滚动交给内部的
   .card-list"。但设置页有**两个并列**的内容块 —— #profBox（个人资料）与 #setList
   （空间/口令/主题/字号/退出）—— 而 #profBox 只是个普通 div：它内部那个 .card-list
   的父级不是 flex 容器，于是 flex:1 / overflow-y:auto 双双失效，它按内容自然撑高。
   实测（1078×630）：内容总高 758 > 容器 630 ⇒ 被裁掉 128px；#profBox 671px 把
   #setList 挤到 24px（只剩上下 padding）、可见比例 0%。而 .kb-sub 是 hidden，
   整页没有滚动条 ⇒ 用户以为到底了，其实"空间/口令/主题/字号/退出"整块从没露过面。
   ⇒ 用一个 .set-scroll 把这两块兜成**唯一**的滚动区，顶栏照旧钉在顶上。 */
.set-scroll { flex: 1; min-height: 0; overflow-y: auto; }

.kb-subbar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 18px 4px; flex: 0 0 auto;
}
.kb-state { display: flex; gap: 4px; background: var(--panel-2); padding: 3px; border-radius: 999px; }
.kb-state-i {
  border: 0; background: transparent; color: var(--dim); border-radius: 999px;
  padding: .3em .85em; font-size: .82rem; display: inline-flex; align-items: center; gap: 5px;
}
.kb-state-i b { font-size: .74rem; font-weight: 700; opacity: .7; font-variant-numeric: tabular-nums; }
.kb-state-i:hover { color: var(--txt); }
.kb-state-i.on { background: var(--panel); color: var(--pri); font-weight: 600; box-shadow: var(--shadow); }
.kb-state-i.on b { opacity: 1; }

/* ================= 知识库使用引导 ================= */
.kb-guide { display: flex; align-items: center; gap: 10px; padding: 10px 18px; background: var(--warn-soft); border-bottom: 1px solid var(--line); font-size: .82rem; color: var(--warn); }
.kb-guide[hidden] { display: none !important; }
.kg-body { flex: 1; min-width: 0; }
.kg-body b { color: var(--txt); margin-right: 6px; }
.kg-step { display: inline; }
.kg-step + .kg-step::before { content: ' | '; opacity: .5; margin: 0 6px; }
.kg-close { background: transparent; border: 0; color: var(--dim2); cursor: pointer; font-size: 1rem; padding: 2px 6px; }
.kg-close:hover { color: var(--txt); }

/* ================= 右上角头像入口 ================= */
.me-chip {
  display: inline-flex; align-items: center; gap: 6px; max-width: 168px;
  border: 1px solid var(--line); background: var(--panel); border-radius: 999px;
  padding: 2px 8px 2px 2px; color: var(--txt); font-size: .82rem;
}
.me-chip:hover { border-color: var(--pri); background: var(--pri-soft); }
.me-chip .me-av { width: 24px; height: 24px; border-radius: 50%; overflow: hidden; background: var(--panel-2); display: block; flex: 0 0 24px; }
.me-chip .me-av svg, .me-chip .me-av img { display: block; width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.me-chip .me-nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.me-chip .me-caret { color: var(--dim2); font-size: .7rem; }

/* ================= 批次161 · 护照蓝星（名牌荣誉标识）=================
   ★ 用户原话：「通关后名字后边加上蓝v（类似 x），我们可以用蓝星表示」。
   ★ 为什么是「星」而不是「V」：蓝 V 是 X 的商标形态，抄过去像仿冒；
     星是通用的「成就」符号，且与本项目的「露珠 / 宝箱 / 徽章」同一套意象。
   ★ 颜色写死成品牌蓝而不是 var(--pri)：主题色可以换（商店里卖配色），
     但「护照」这个荣誉标识**不该跟着换色** —— 一换就变成了普通装饰。
   ★ 只做行内元素、不给 position/float：它要能塞进 .me-nm / .cls-nm / 菜单标题
     任何一处而不改变那三处既有的排版（那三处都有等宽/截断的断言）。 */
.pp-star {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.02em; height: 1.02em; margin-left: .18em; vertical-align: -.16em;
  color: #2f7fe0; flex: 0 0 auto; line-height: 1; cursor: help;
}
.pp-star svg { display: block; width: 100%; height: 100%; }
/* 名牌里那颗星不能把名字挤掉：.me-chip 的 max-width 只有 168px，
   .me-nm 已经 text-overflow:ellipsis ⇒ 星给固定宽度、不参与收缩。 */
.me-chip .pp-star { flex: 0 0 auto; }
/* 深色主题下 #2f7fe0 偏暗，提亮一档，否则在深底上糊成一团。 */
[data-theme="dark"] .pp-star { color: #6aa8ff; }

.me-head { display: flex; align-items: center; gap: 12px; padding: 4px 2px 14px; border-bottom: 1px solid var(--line); margin-bottom: 10px; }
.me-head .me-av.big { width: 42px; height: 42px; flex: 0 0 42px; }
.me-head .me-av.big svg, .me-head .me-av.big img { width: 100%; height: 100%; object-fit: cover; }
.me-menu { display: flex; flex-direction: column; gap: 4px; }
.me-mi {
  display: flex; flex-direction: column; gap: 2px; text-align: left;
  border: 1px solid var(--line); background: var(--panel); border-radius: var(--r-sm);
  padding: .6em .8em; color: var(--txt);
}
.me-mi:hover { border-color: var(--pri); background: var(--pri-soft); }
.me-mi b { font-size: .9rem; }
/* ★ 只作用于**直接子级**的说明行。批次258 之前写的是 `.me-mi span`（权重 0,1,1），
   比 `.badge`（0,1,0）高 ⇒ 公告徽章那个 <span class="badge"> 明明写了 color:#fff，
   数字仍被这条染成 var(--dim2) 灰 —— 线上表现就是「红底灰字」。
   收窄成 `>` 让徽章不再被说明行样式波及，也免得以后往 b 里塞任何 span 都踩一遍。 */
.me-mi > span { font-size: .76rem; color: var(--dim2); }
.me-mi.danger b { color: var(--bad); }
/* 「我的」菜单里的空间卡与成长条（原侧栏顶部那块，2026-10-04 挪过来）。
   .pet-line 的基准样式在上面，这里只补它在菜单里的间距 ——
   菜单里没有侧栏那条竖线的约束，左右留白跟 .me-mi 对齐即可。 */
.me-head .me-switch { margin-left: auto; flex: 0 0 auto; }
/* 批次216：「我的」头部那行「当前成长值 N」。
   原来这个位置是护照签发公告 —— 一条只在签发时成立一次的公告，
   挂着不走，还占着最该显示数字的位置。改成读数后只补两件事：
   ① 数字用主色加粗，跟下面成长条里的数字同一套观感；
   ② 等宽数字（tabular-nums），成长值涨到四位数时这一行不会左右抖。 */
.me-head .me-growth { font-variant-numeric: tabular-nums; }
.me-head .me-growth b { color: var(--pri); font-weight: 700; }
.me-pet {
  margin: 0 0 10px; padding: 8px 10px;
  background: var(--panel-2); border-radius: var(--r-sm);
}
/* 连胜条跟成长条同一套容器样式（.streak-line 的基准在文件上半部分）。 */
.me-streak {
  margin: -4px 0 10px; padding: 8px 10px;
  background: var(--panel-2); border-radius: var(--r-sm);
}
.me-mi b { display: flex; align-items: center; gap: 6px; }
.me-mi .me-badge { margin-left: 0; }
/* 徽章在任何容器里都必须「红底白字」，这是用户明确要求的口径（批次258）。
   显式再钉一遍 color/background：DOM 顺序或外层选择器再变时也不会被悄悄染灰。 */
.me-mi .me-badge { color: #fff; background: var(--bad); }

/* ================= 英语 · 单元四关 + 艾宾浩斯（批次24）=================
   ★ 基准样式一律写在所有 @media 之前：同权重后写的赢，
     要是把 .en4-bar.na 这类基准规则写在 @media 后面，会被媒体查询里的规则直接盖掉且零报错。 */
.en4-wrap { padding: 4px 18px 8px; flex: 0 0 auto; }
.en4-err { padding: 12px; color: var(--bad); font-size: .84rem; }
.en4-empty { border: 1px dashed var(--line-2); border-radius: var(--r); padding: 14px 16px; font-size: .86rem; color: var(--dim); }
.en4-empty b { color: var(--txt); }
.en4-empty-s { margin-top: 6px; font-size: .78rem; color: var(--dim2); line-height: 1.7; }

.en4-unit { border: 1px solid var(--line); border-radius: var(--r); background: var(--panel); padding: 10px 12px; margin-bottom: 9px; }
.en4-unit.done { border-color: var(--ok); }
.en4-u-h { display: flex; align-items: center; gap: 10px; margin-bottom: 9px; flex-wrap: wrap; }
.en4-u-h b { font-size: .92rem; }
.en4-u-n { font-size: .74rem; color: var(--dim2); white-space: nowrap; }
.en4-u-ok { font-size: .72rem; color: var(--ok); border: 1px solid var(--ok); border-radius: 999px; padding: 1px 9px; white-space: nowrap; }

.en4-gates { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.en4-gates-all { margin-top: 10px; }
.en4-gate {
  display: flex; flex-direction: column; align-items: flex-start; gap: 5px;
  padding: 9px 10px; border: 1px solid var(--line-2); border-radius: var(--r-sm);
  background: var(--panel-2); color: var(--txt); cursor: pointer; text-align: left;
  transition: border-color var(--dur-1), transform var(--dur-1);
}
.en4-gate:hover { border-color: var(--pri); }
.en4-gate:active { transform: translateY(1px); }
.en4-ic { font-size: .9rem; font-weight: 700; color: var(--pri); line-height: 1; }
.en4-nm { font-size: .8rem; font-weight: 600; white-space: nowrap; }
.en4-bar { width: 100%; height: 5px; border-radius: 999px; background: var(--panel-3); overflow: hidden; }
.en4-bar i { display: block; height: 100%; background: var(--pri); border-radius: 999px; transition: width var(--dur-3); }
/* ★ null（没有分母）与 0（一个都没过）必须长得不一样 */
.en4-bar.na { background: transparent; border: 1px dashed var(--line-2); }
.en4-pct { font-family: var(--mono); font-size: .72rem; color: var(--dim); }
.en4-pct.na { color: var(--dim2); font-family: inherit; font-size: .72rem; }
.en4-cnt { font-family: var(--mono); font-size: .68rem; color: var(--dim2); }

/* 复习队列 */
.en4-review { padding: 0 18px 6px; flex: 0 0 auto; }
.en4-rv { border: 1px solid var(--line); border-radius: var(--r); background: var(--panel); padding: 10px 12px; }
.en4-rv-h { display: flex; align-items: baseline; gap: 10px; margin-bottom: 7px; flex-wrap: wrap; }
.en4-rv-h b { font-size: .9rem; }
.en4-rv-n { font-size: .72rem; color: var(--dim2); }
.en4-rv-s { display: flex; gap: 14px; flex-wrap: wrap; font-size: .78rem; color: var(--dim); }
.en4-rv-s b { font-family: var(--mono); font-size: .92rem; color: var(--txt); }
.en4-na { color: var(--dim2); font-style: normal; }
.en4-rv-tip { margin-top: 6px; font-size: .74rem; color: var(--dim2); line-height: 1.6; }
.en4-rv-a { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
.en4-rv-d { font-size: .74rem; color: var(--dim2); font-family: var(--mono); }

/* 答题 */
.en4-wrap-q { max-height: 60vh; overflow: auto; }
.en4-q { border: 1px solid var(--line); border-radius: var(--r-sm); padding: .75em .85em; margin-bottom: 8px; background: var(--panel); }
.en4-q-h { display: flex; align-items: center; gap: 9px; margin-bottom: 7px; flex-wrap: wrap; }
.en4-q-n { font-family: var(--mono); font-size: .74rem; color: var(--dim2); }
.en4-q-w { font-size: 1rem; font-weight: 600; }
.en4-q-p { font-size: .8rem; color: var(--dim); }
.en4-q-l { font-family: var(--mono); font-size: .74rem; color: var(--dim2); letter-spacing: .16em; }
.en4-q-m { font-size: .82rem; color: var(--dim); margin-bottom: 7px; }
.en4-start { font-size: .74rem; color: var(--dim2); margin: 2px 0 7px; }
.en4-start code { background: var(--panel-3); border-radius: 4px; padding: 1px 5px; margin-right: 5px; font-size: .72rem; }
.en4-ch { display: flex; flex-wrap: wrap; gap: 7px; }
.en4-ch-i { border: 1px solid var(--line-2); background: var(--panel-2); color: var(--txt); border-radius: var(--r-sm); padding: 5px 12px; font-size: .84rem; cursor: pointer; }
.en4-ch-i:hover { border-color: var(--pri); }
.en4-ch-i.on { border-color: var(--pri); background: var(--pri-soft); }
.en4-ch-i.ok { border-color: var(--ok); background: var(--ok-soft); }
.en4-ch-i.bad { border-color: var(--bad); background: var(--bad-soft); text-decoration: line-through; }
.en4-q.ok { border-color: var(--ok); background: var(--ok-soft); }
.en4-q.bad { border-color: var(--bad); background: var(--bad-soft); }
.en4-q.used { border-color: var(--pri); }
.en4-q.noused { border-color: var(--line-2); }
/* ★ 造句的 unknown（想不起来）用中性色，不用红 —— 它不是错 */
.en4-q.unk { border-color: var(--line-2); background: var(--panel-2); }
.en4-fb { font-size: .82rem; margin-top: 6px; line-height: 1.6; }
.en4-fb.ok { color: var(--ok); }
.en4-fb.bad { color: var(--bad); }
.en4-fb.mid { color: var(--dim); }
.en4-fb-x { color: var(--dim); margin-top: 3px; }
.en4-sum { display: flex; gap: 14px; align-items: baseline; padding: .6em 0 .5em; }
.en4-sum b { font-size: 1.5rem; font-family: var(--mono); }
.en4-rv-od { font-size: .7rem; color: var(--bad); border: 1px solid var(--bad); border-radius: 999px; padding: 1px 7px; white-space: nowrap; }
.en4-rv-c { font-size: .7rem; color: var(--dim2); font-family: var(--mono); white-space: nowrap; }
.en4-rv-q .inp { margin-bottom: 6px; }

/* ================= 班级 + 班级排行榜（批次28）=================
 * 全站唯一"把人排序"的地方。两条视觉纪律：
 *   ① 0 分 / null 的人**不给名次**，显示「—」—— 不画"第 37 名"这种羞辱。
 *   ② 只有「我」那一行描出来，别的行不强调 —— 榜单是给自己看进度的，不是给别人看的。
 */
/* 批次109：班级页的滚动区（同 .shop-wrap，不要再写第二条定义）。 */
.cls-wrap { flex: 1; min-height: 0; overflow-y: auto; display: flex; gap: 14px; align-items: flex-start; }
.cls-list { width: 190px; flex: none; }
.cls-list-h { font-size: .74rem; color: var(--dim2); padding: 0 4px 6px; }
.cls-i { display: block; width: 100%; text-align: left; border: 1px solid var(--line); background: var(--panel); border-radius: var(--r-sm); padding: .55em .7em; margin-bottom: 6px; cursor: pointer; font: inherit; }
.cls-i:hover { border-color: var(--line-2); }
.cls-i.on { border-color: var(--pri); background: var(--pri-soft); }
.cls-i-n { display: block; font-size: .88rem; color: var(--txt); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cls-i-m { display: block; font-size: .72rem; color: var(--dim2); margin-top: 2px; }
.cls-main { flex: 1; min-width: 0; }

.cls-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.cls-head-l b { font-size: 1.02rem; }
.cls-head-l span { display: block; font-size: .76rem; color: var(--dim2); margin-top: 2px; }
.cls-code { display: flex; align-items: center; gap: 8px; border: 1px dashed var(--line-2); border-radius: var(--r-sm); padding: .5em .7em; margin-bottom: 12px; background: var(--panel-2); }
.cls-code span { font-size: .74rem; color: var(--dim2); }
.cls-code b { font-family: var(--mono); font-size: 1.15rem; letter-spacing: .14em; color: var(--pri); }
.cls-code i { font-style: normal; font-size: .72rem; color: var(--dim2); }
/* 被管理员直接拉进班的告知条（批次58）：不是报错、不是警告，是一条"这事怎么发生的"说明。 */
.cls-notice {
  border-left: 3px solid var(--pri); border-radius: var(--r-sm);
  background: var(--pri-soft); color: var(--pri);
  padding: .5em .75em; margin-bottom: 12px; font-size: .8rem; line-height: 1.7;
}

.cls-tabs { display: flex; gap: 6px; margin-bottom: 8px; }
.cls-tab { border: 1px solid var(--line); background: var(--panel); border-radius: 999px; padding: .32em .85em; font: inherit; font-size: .8rem; color: var(--dim); cursor: pointer; }
.cls-tab:hover { border-color: var(--line-2); color: var(--txt); }
.cls-tab.on { border-color: var(--pri); background: var(--pri-soft); color: var(--pri); }
.cls-hint { font-size: .74rem; color: var(--dim2); margin-bottom: 10px; }

.cls-board { border: 1px solid var(--line); border-radius: var(--r-sm); overflow: hidden; background: var(--panel); }
.cls-row { display: flex; align-items: center; gap: 10px; padding: .55em .8em; border-bottom: 1px solid var(--line); }
.cls-row:last-child { border-bottom: 0; }
.cls-row.me { background: var(--pri-soft); }
.cls-row.off { opacity: .62; }
.cls-rk { width: 2.1em; flex: none; text-align: right; font-family: var(--mono); font-size: .95rem; color: var(--dim); }
.cls-row.me .cls-rk { color: var(--pri); }
.cls-nm { flex: 1; min-width: 0; font-size: .88rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cls-nm i { font-style: normal; font-size: .68rem; color: var(--pri); border: 1px solid var(--pri); border-radius: 999px; padding: 0 5px; margin-left: 6px; }
.cls-vl { font-family: var(--mono); font-size: .95rem; color: var(--txt); }
.cls-na { font-family: inherit; font-size: .76rem; color: var(--dim2); }
.cls-none, .cls-note, .cls-err { padding: .8em; font-size: .78rem; color: var(--dim2); }
.cls-note { padding: .5em .8em 0; }
.cls-err { color: var(--bad); }

.cls-members { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.cls-m-h { font-size: .74rem; color: var(--dim2); margin-right: 2px; }
.cls-m { font-size: .78rem; color: var(--dim); border: 1px solid var(--line); border-radius: 999px; padding: .15em .6em; }
.cls-m.me { border-color: var(--pri); color: var(--pri); }

.cls-empty { border: 1px dashed var(--line-2); border-radius: var(--r); padding: 2.2em 1.4em; text-align: center; background: var(--panel); }
.cls-empty b { display: block; font-size: 1rem; margin-bottom: 6px; }
.cls-empty span { display: block; font-size: .8rem; color: var(--dim2); max-width: 34em; margin: 0 auto; line-height: 1.7; }
.cls-empty-btns { display: flex; gap: 10px; justify-content: center; margin-top: 14px; }
/* ★ 批次66：没登录账号时的提示里，"你已经有账号了，账号名是 XXX" 那句要**跳出来** ——
 * 它才是学生真正需要的那条信息（后半句"密码就是你的访问口令"他能立刻试）。
 * 与上面那条 span 的区别靠**颜色 + 间距**，不靠加粗（加粗在暗色下几乎看不出来）。 */
.cls-na-hint { margin-top: 10px !important; color: var(--pri) !important; }
.cls-na-hint b { display: inline; font-weight: 600; }

/* ================= 上下文折叠条（批次53） =================
 * 服务端只把最近 20 条消息发给模型，更早的会被压成摘要（见 server/core.js）。
 * ★ 摘要以前是**纯后端行为**、界面上看不见 —— 学生只觉得"聊久了它就忘了"。
 * 这条把"更早的 N 条已压成摘要"摆出来，可展开看原文。
 * 视觉上要**弱于消息本身**：它是一段被折叠的记忆，不是一条新消息。 */
.ctx-fold { margin: 6px 0 14px; }
.cf-head {
  display: flex; align-items: center; gap: 8px;
  padding: .55em .85em; border-radius: var(--r-sm);
  background: var(--panel-2); border: 1px dashed var(--line-2);
}
.cf-ico { color: var(--dim2); font-size: .9rem; line-height: 1; flex: none; }
.cf-t { flex: 1; min-width: 0; font-size: .78rem; color: var(--dim); }
.cf-btn {
  font: inherit; font-size: .74rem; flex: none; cursor: pointer;
  padding: .2em .6em; border-radius: 999px;
  background: transparent; border: 1px solid var(--line-2); color: var(--dim);
}
.cf-btn:hover { border-color: var(--pri); color: var(--pri); }
.cf-body {
  margin-top: 8px; padding: .8em .95em; border-radius: var(--r-sm);
  background: var(--panel); border: 1px solid var(--line);
  font-size: .8rem; line-height: 1.75; color: var(--dim2);
  /* 摘要是模型写的多段自由文本，用 pre-wrap 保住它自己的换行 */
  white-space: pre-wrap; word-break: break-word;
}
/* 摘要生成时间（批次55a）—— 比正文还弱，只回答"这段记忆有多新" */
.cf-at { flex: none; font-size: .72rem; color: var(--dim2); }

/* 记忆面板：把「AI 自动总结」的条目和手写的区分开（批次53） */
.mem-note {
  margin: 0 18px 8px; padding: .5em .8em; border-radius: var(--r-xs);
  background: var(--pri-soft); color: var(--pri-2);
  font-size: .75rem; line-height: 1.6;
}
/* 记忆行内编辑（批次55b）：输入框直接顶替原来的文字，保存/取消在右侧 */
.mrow-txt { display: block; }
.mem-edit-input {
  flex: 1; min-width: 0; font: inherit; font-size: .9rem;
  padding: .42em .62em; border-radius: var(--r-xs);
  background: var(--panel-2); border: 1px solid var(--pri); color: var(--txt);
}
/* ★ 批次106：同上，补外环（原来只有边框变色）。 */
.mem-edit-input:focus { outline: none; border-color: var(--pri-2); background: var(--panel); box-shadow: 0 0 0 3px var(--pri-soft); }
.chip.mem-auto { background: var(--pri-soft); color: var(--pri); }

/* 重置码面板（批次60）：没有重置码时给的"找谁要"提示，要比普通 dim 更容易被看见 ——
   这条信息的**唯一用途**就是告诉一个进不来的人下一步该找谁。 */
.rs-rc-hint {
  margin: .1em 0 .9em; padding: .6em .75em; border-radius: var(--r-xs);
  background: var(--panel-2); border-left: 3px solid var(--pri);
  line-height: 1.55;
}

/* 创建空间成功卡片（批次61）。
   三行"账号/口令/空间ID"的键值表：值用等宽字 + 可点击复制，
   因为这几串是要被**抄下来**的（学生下次靠账号名回来）。 */
.sp-done { display: flex; flex-direction: column; gap: 8px; margin: .4em 0 1.1em; }
.sp-done-row {
  display: flex; align-items: center; gap: 10px;
  background: var(--panel-2); border-radius: var(--r-xs); padding: 9px 12px;
}
.sp-done-row .k { color: var(--dim); font-size: .84rem; flex: 0 0 5.2em; }
.sp-done-row .v {
  font-family: var(--mono, monospace); font-size: .95rem; font-weight: 500;
  color: var(--txt); overflow-wrap: anywhere; flex: 1;
}
.sp-done-row .c { flex: 0 0 auto; }
.sp-done-row.hi { background: var(--pri-soft); }
.sp-done-row.hi .v { color: var(--pri); }

/* 「进入已有空间」的重名候选列表（批次61）。
   空间名允许重名（suggestName 会自动加数字），所以按名字进可能命中多个 ——
   这里把候选摆出来让人**点选**，而不是替他猜一个（猜错就进了别人的空间）。 */
.sp-picks { display: flex; flex-direction: column; gap: 6px; margin: 0 0 14px; }
.sp-pick {
  display: flex; align-items: center; gap: 10px; text-align: left;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--txt);
  border-radius: var(--r-xs); padding: 9px 12px; font-size: .9rem; cursor: pointer;
}
.sp-pick:hover { border-color: var(--pri); background: var(--pri-soft); }
.sp-pick .id { font-family: var(--mono, monospace); color: var(--dim); font-size: .84rem; }

/* ================= 顶栏响应式 =================
 * 窗口不够宽时让顶栏**换行**，而不是把标题压成竖排。
 * 单开 900px 而不是并进侧栏的 820px —— 侧栏停靠时（>820）主区反而更窄，
 * 顶栏需要比侧栏更早换行。820 以下侧栏变浮层，主区反而变宽了。 */
@media (max-width: 900px) {
  .topbar { flex-wrap: wrap; row-gap: 8px; }
  /* 换行布局里 .sp 会把标题挤扁，去掉它，让标题自己长满第一行 */
  .topbar > .sp { display: none; }
  /* flex-basis 用 0（不是 auto）：假设主尺寸为 0 才一定放得下第一行，
   * 否则标题会带着自己的整段文字宽度去抢位置，换行位置就不可控了。 */
  .topbar > .tb-txt { flex: 1 1 0; min-width: 0; }
  .topbar > .tb-tools {
    display: flex; flex: 1 0 100%; order: 1;
    align-items: center; gap: 8px;
    overflow-x: auto; overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .topbar > .tb-tools::-webkit-scrollbar { display: none; }
  .topbar > .tb-tools > * { flex: 0 0 auto; }
}

@media (max-width: 820px) {
  /* 手机上本子树占了半屏就没法看文档了 —— 收成一条横向滚动的条 */
  .kb-wrap { flex-direction: column; }
  .kb-tree {
    flex: 0 0 auto; border-right: 0; border-bottom: 1px solid var(--line);
    flex-direction: row; align-items: center; gap: 8px; padding: 8px 12px; overflow-x: auto;
  }
  /* ★ 批次88：分组标题（本子 / 按项目）在横向条里也一起收掉 ——
     横向条上没法体现分组，留着就是两个孤零零的小字挤在格子中间。 */
  .kb-tree-h, .kb-tree-foot, .kb-tree-g { display: none; }
  .kb-tree-list { flex-direction: row; flex: 1; gap: 6px; }
  .kb-tree-i { flex: 0 0 auto; border: 1px solid var(--line); padding: .3em .7em; }
  .kb-tabs { padding: 8px 12px 0; }
  .kb-subbar { padding: 8px 12px 4px; }
  /* 班级：手机上"班级列表"横过来收成一条，别把榜单挤没 */
  .cls-wrap { flex-direction: column; }
  .cls-list { width: 100%; display: flex; gap: 6px; overflow-x: auto; }
  .cls-list-h { display: none; }
  .cls-i { flex: 0 0 auto; width: auto; margin-bottom: 0; }
  .me-chip .me-nm { display: none; }
}

/* ================= 移动端 ================= */
@media (max-width: 820px) {
  .side {
    position: fixed; top: 0; bottom: 0; left: 0; z-index: 60;
    transform: translateX(-100%); transition: transform var(--dur-3) var(--ease-out);
    box-shadow: var(--shadow-lg); width: 280px; flex-basis: 280px;
  }
  .side.open { transform: none; }
  /* 侧栏打开后 280px 的浮层正好盖住汉堡 —— 不给遮罩就"点得到开、点不到关"。
   * 遮罩压在侧栏下面（55 < 60），点它即关。关闭时 pointer-events:none，
   * 不会挡住任何点击。 */
  .side-mask {
    display: block; position: fixed; inset: 0; z-index: 55;
    background: rgba(16, 24, 40, .38);
    opacity: 0; pointer-events: none;
    transition: opacity var(--dur-3) ease;
  }
  .side.open ~ .side-mask { opacity: 1; pointer-events: auto; }
  /* 浮层抽屉没有"拖宽"这回事 */
  .side-resizer { display: none; }
  .hamb { display: block; }
  .stream { padding: 16px 12px 4px; }
  .composer { padding: 10px 12px 12px; }
  .card-stats, .card-bar, .card-list { padding-left: 12px; padding-right: 12px; }
  /* 四关在窄屏改两列：四列会把「认得出」这类中文标签挤成竖排 */
  .en4-wrap, .en4-review { padding-left: 12px; padding-right: 12px; }
  .en4-gates { grid-template-columns: repeat(2, 1fr); }
  /* ★ 单词行必须换行：一行里塞了 词(96) + 音标(84) + 意思 + 计数 + 两个按钮 + 删，
     390px 宽下放不下 —— 实测 speak-btn 与删除按钮被推出右边界。
     换行后第一行给词/音标/意思，第二行放计数与按钮。 */
  .en-i { flex-wrap: wrap; row-gap: 6px; gap: 8px; }
  .en-i .en-w { min-width: 0; }
  .en-i .en-p { min-width: 0; }
  .en-i .en-m { flex: 1 1 100%; order: 3; }
  .en-i .en-n { order: 4; }
  .msg.user .body { max-width: 92%; }
  .land-card { padding: 24px 20px 18px; }
  /* 落地页抬头：手机上压缩，别让"我们是谁"把表单顶出首屏 */
  .land { padding: 18px 14px; }
  .land-head { margin-bottom: 14px; }
  .land-logo { gap: 9px; }
  .land-logo-img { width: 48px; }
  .land-logo-word { font-size: 1.5rem; }
  .land-stance { margin-top: 8px; font-size: .8rem; }
  .topbar { padding: 10px 12px; gap: 8px; }
  .topbar .sel { max-width: 92px; }
  /* 批次176：学习页这一排的学科下拉别把整排撑爆（模式胶囊 + 下拉在 390px 下要排得开） */
  .learn-bar #learnSubjSel { max-width: 112px; }
  /* 手机上「收起成图标」没意义（侧栏本来就是浮层），让位给汉堡 */
  #sideToggle { display: none; }
  /* 搜索框独占一行，别和标题抢。
   * order:1 让它排在头像之后 —— 否则它会把头像挤到第三行。 */
  .topbar > #kbSearch { flex: 1 0 100%; max-width: none; order: 1; }
  /* 项目下拉在窄屏再收一点，给副标题留出省略的空间 */
  #projectSel { max-width: 104px; }
  /* 空状态：手机上四张卡排 2×2，视觉主体收小，别把首屏占满 */
  .empty { padding: 4vh 16px 3vh; }
  .empty-art { width: 132px; height: 104px; margin-bottom: 6px; }
  .empty-hi { font-size: 1.28rem; }
  .empty-ask { font-size: .96rem; margin-bottom: 8px; }
  .empty-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 18px; }
  .ec { padding: 10px 11px 11px; }
  .ec b { margin: 7px 0 2px; }
  .empty-tips { margin-top: 14px; }
  /* 日报：窄屏下数字排 2 列，日期导航换到下一行 */
  .d-nums { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .d-head h3 { width: 100%; }
  .d-nav { margin-left: 0; width: 100%; }
  .d-nav .btn { flex: 1; }
}
@media (max-width: 480px) {
  .msg .av { display: none; }
  .msg.user .body { max-width: 100%; }
  /* 落地页三个 tab 在 320px 下会折成两行（"创建学习 / 空间"），
     胶囊高 62px 且三格高度不齐，很难看。
     中文没有词边界、也不会缩字，只能整体压字号 + 压内边距把它塞回一行。
     算过：3 格共 16 个汉字，容器 252px ⇒ 字号必须 ≤ 12.6px（.79rem）。取 .76rem 留余量。 */
  .land { padding: 14px 12px; }
  .land-card { padding: 20px 16px 16px; }
  .land-tabs { padding: 3px; gap: 3px; margin-bottom: 16px; }
  /* 标签强制单行（不折成两行胶囊）。.76rem 在 252px 容器下差 1–2px 会折行，
     降到 .72rem 并压横向内边距，保证"创建学习空间"六个字一行塞下；高度随之回到 ≤44px。 */
  .land-tab { font-size: .72rem; padding: .5em .14em; white-space: nowrap; }
  /* 三份文件横排在 320px 放不下。藏掉「·」，改用外边距分隔 ——
     这样换行点只会落在两个链接之间，不会留下一个孤零零的分隔符。 */
  .land-legal { font-size: .72rem; line-height: 2.1; }
  .land-legal a { margin: 0 6px; }
  .land-sep { display: none; }
}

/* 打印 / 分享页 */
.share-body { max-width: 720px; margin: 0 auto; padding: 28px 18px 60px; }
.share-head { border-bottom: 1px solid var(--line); padding-bottom: 14px; margin-bottom: 22px; }
.share-head h1 { font-size: 1.3rem; margin: 0 0 4px; }

/* ================= 英语（P10）================= */
.en-i { display: flex; align-items: center; gap: 12px; padding: .62em .8em; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--panel); margin-bottom: 7px; }
.en-i .en-w { font-weight: 600; font-size: .95rem; min-width: 96px; }
.en-i .en-p { font-family: var(--mono); font-size: .78rem; color: var(--dim2); min-width: 84px; }
.en-i .en-m { flex: 1; min-width: 0; font-size: .86rem; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.en-i .en-n { font-family: var(--mono); font-size: .72rem; color: var(--dim2); white-space: nowrap; }
.en-i .en-n.hot { color: var(--bad); }
.en-i .en-x { opacity: 0; transition: opacity var(--dur-1); }
.en-i:hover .en-x { opacity: 1; }

/* 听写 */
.dict-wrap { max-height: 62vh; overflow: auto; }
.dict-i { padding: .8em .9em; border: 1px solid var(--line); border-radius: var(--r-sm); margin-bottom: 8px; background: var(--panel); }
.dict-i .dict-h { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; }
.dict-i .dict-n { font-family: var(--mono); font-size: .76rem; color: var(--dim2); }
.dict-i .dict-p { font-size: 1rem; font-weight: 600; flex: 1; }
.dict-i .dict-l { font-family: var(--mono); font-size: .76rem; color: var(--dim2); letter-spacing: .18em; }
.dict-i.ok { border-color: var(--ok); background: var(--ok-soft); }
.dict-i.bad { border-color: var(--bad); background: var(--bad-soft); }
.dict-i .dict-fb { font-size: .82rem; margin-top: 6px; }
.dict-i .dict-fb.ok { color: var(--ok); }
.dict-i .dict-fb.bad { color: var(--bad); }
.dict-score { display: flex; gap: 14px; align-items: baseline; padding: .6em 0 1em; }
.dict-score b { font-size: 1.6rem; font-family: var(--mono); }
.speak-btn { border: 1px solid var(--line-2); background: var(--panel-2); color: var(--txt); border-radius: 999px; width: 30px; height: 30px; cursor: pointer; font-size: .9rem; line-height: 1; flex: 0 0 30px; }
.speak-btn:hover { border-color: var(--pri); color: var(--pri); }
/* ================= 测评（P6）================= */
.ex-i { display: flex; align-items: center; gap: 12px; padding: .7em .85em; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--panel); margin-bottom: 8px; }
.ex-i > div:first-child { flex: 1; min-width: 0; }
.ex-i b { font-size: .93rem; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ex-i .dim { font-size: .75rem; margin-top: 3px; }
.ex-pill { font-size: .72rem; padding: 2px 9px; border-radius: 999px; background: var(--panel-3); color: var(--dim); white-space: nowrap; }
.ex-pill.open { background: var(--pri-soft); color: var(--pri); }
.ex-pill.done { background: var(--ok-soft); color: var(--ok); }
.ex-q { border: 1px solid var(--line); border-radius: var(--r-sm); padding: .8em .9em; margin-bottom: 9px; background: var(--panel); }
.ex-q .eq-h { display: flex; align-items: center; gap: 8px; font-size: .74rem; color: var(--dim2); margin-bottom: 6px; }
.ex-q .eq-t { font-size: .95rem; font-weight: 600; line-height: 1.6; margin-bottom: 9px; }
.ex-q.ok { border-color: var(--ok); }
.ex-q.bad { border-color: var(--bad); }
.ex-q.unknown { border-color: var(--line-2); }
.ex-q .eq-fb { font-size: .82rem; margin-top: 7px; padding-top: 7px; border-top: 1px dashed var(--line); }
.ex-q .eq-fb .k { color: var(--dim2); }
.ex-res { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; }
.ex-res .r { background: var(--panel-2); border-radius: var(--r-sm); padding: .5em .8em; font-size: .78rem; color: var(--dim); }
.ex-res .r b { display: block; font-size: 1.15rem; font-family: var(--mono); color: var(--txt); }
/* ===== 批次341 · P1-5 知识点抽屉「怎么学」四入口 / P1-4 把握度自评 ===== */
.pd-sub { font-size: .92rem; font-weight: 700; margin-bottom: 10px; line-height: 1.6; }
.pd-ways { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.pd-way { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; text-align: left;
  padding: 12px 14px; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--panel-2); cursor: pointer; }
.pd-way:hover { border-color: var(--accent); }
.pd-way b { font-size: .95rem; }
.pd-way span { font-size: .74rem; color: var(--dim2); line-height: 1.5; }
.pc-conf { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
@media (max-width: 640px) { .pd-ways { grid-template-columns: 1fr; } }

.ex-flow { font-size: .76rem; color: var(--dim2); line-height: 1.7; }

/* ===== 批次341 · 结果页知识点分区 + 逐题追问 + 下一步动作条（StudyFetch P0-1/2/3）=====
   ★ 分区只表达「这次作答覆盖到的题目里对了几个」，不做百分比/星级——一次测评不够谈"掌握度"。 */
.ex-topics { border: 1px solid var(--line); border-radius: var(--r-sm); padding: .7em .9em; margin-bottom: 12px; background: var(--panel-2); }
.ex-tp-h { font-size: .78rem; font-weight: 700; margin-bottom: 8px; }
.ex-tp { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 7px 0; border-top: 1px dashed var(--line); }
.ex-tp:first-of-type { border-top: 0; }
.ex-tp.bad { border-left: 3px solid var(--bad); padding-left: 8px; }
.ex-tp-n { flex: 1 1 46%; min-width: 180px; font-size: .86rem; font-weight: 600; line-height: 1.5; }
.ex-tp-c { font-size: .78rem; color: var(--dim); display: flex; gap: 6px; flex-wrap: wrap; }
.ex-tp .okc, .okc { color: var(--ok); }
.ex-tp .okc, .ex-tp .badc { font-weight: 700; }
.ex-tp .badc, .badc { color: var(--bad); }
.ex-tp .ex-tp-ask { margin-left: auto; }
.ex-next { border-top: 1px solid var(--line); padding-top: 10px; margin-top: 10px; }
.ex-next-note { font-size: .76rem; color: var(--dim2); line-height: 1.7; margin-bottom: 9px; }
.ex-next-reason { margin-top: 8px; font-size: .78rem; color: var(--bad); background: var(--bad-soft, #fdecea); border-radius: var(--r-sm); padding: .5em .7em; }
.ex-q .exq-ask { margin-top: 8px; }
@media (max-width: 640px) {
  .ex-tp-n { flex: 1 1 100%; min-width: 0; }
  .ex-tp .ex-tp-ask { margin-left: 0; }
  .ex-next .pc-acts { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  .ex-next .pc-acts .btn { width: 100%; }
}

/* ================= 精准学习测评（批次104c）=================
 * ★ 全部是新类名（`as-*`），**不复用也不覆盖** exam 那批（`ex-*` / `eq-*` / `pc-*`）——
 *   覆盖别人的选择器会让"改了看不出来"，而反证套件锚的就是那些类名。
 * ★ 只用 var()，不写死颜色 ⇒ 深色主题跟着 `[data-theme]` 自动切。 */

/* —— 册列表上的入口 —— */
.as-entry { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 8px 0; padding: .6em .8em; border: 1px dashed var(--line-2); border-radius: var(--r-sm); background: var(--panel-2); }
.as-entry-t { flex: 1; min-width: 12em; }
.as-entry-t b { display: block; font-size: .88rem; }
.as-entry-t .dim { font-size: .74rem; line-height: 1.5; }

/* —— 答题进度 —— */
.as-prog { font-size: .76rem; color: var(--dim2); padding: 6px 0 2px; }

/* —— 分数卡 —— */
.as-score { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; margin-bottom: 12px; padding: .8em .9em; border: 1px solid var(--line); border-radius: var(--r); background: var(--panel-2); }
.as-big b { font-size: 2.4rem; font-family: var(--mono); line-height: 1; color: var(--txt); }
.as-big span { font-size: .78rem; color: var(--dim); margin-left: 3px; }
.as-nums { display: flex; gap: 12px; flex-wrap: wrap; }
.as-nums .r { font-size: .76rem; color: var(--dim); }
.as-nums .r b { display: block; font-size: 1.15rem; font-family: var(--mono); color: var(--txt); }
.as-caveat { flex: 1 0 100%; font-size: .74rem; color: var(--dim2); line-height: 1.6; padding-top: 6px; border-top: 1px dashed var(--line); }

/* —— 摘要（先补这些 / 要从头学 / 没测到 / 没抽到）—— */
.as-sum-wrap { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.as-sum { border-left: 3px solid var(--line-2); padding: .5em .8em; background: var(--panel-2); border-radius: 0 var(--r-sm) var(--r-sm) 0; }
.as-sum b { font-size: .84rem; margin-right: 6px; }
.as-sum > span { font-size: .74rem; color: var(--dim2); }
.as-sum-u { font-size: .8rem; margin-top: 3px; color: var(--txt); }
.as-sum.tone-warn { border-left-color: var(--warn); }
.as-sum.tone-bad { border-left-color: var(--bad); }
.as-sum.tone-dim { border-left-color: var(--line); }

/* —— 起点建议 —— */
.as-start { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; padding: .7em .9em; border: 1px solid var(--pri); border-radius: var(--r-sm); background: var(--pri-soft); }
.as-start b { font-size: .86rem; }
.as-start > span { font-size: .84rem; }
.as-start.ok { border-color: var(--ok); background: var(--ok-soft); }

/* —— 逐单元画像 —— */
.as-pt-h { font-size: .82rem; font-weight: 700; margin: 14px 0 8px; }
.as-units { display: flex; flex-direction: column; gap: 7px; }
.as-u { border: 1px solid var(--line); border-radius: var(--r-sm); padding: .55em .8em; background: var(--panel); }
.as-u-h { display: flex; align-items: center; gap: 8px; }
.as-u-h b { font-size: .88rem; flex: 1; min-width: 0; }
.as-u-t { font-size: .76rem; }
.as-u-s { font-size: .74rem; color: var(--dim2); margin-top: 2px; }
.as-u.tone-ok { border-left: 3px solid var(--ok); }
.as-u.tone-warn { border-left: 3px solid var(--warn); }
.as-u.tone-bad { border-left: 3px solid var(--bad); }
.as-u.tone-dim { border-left: 3px solid var(--line-2); }
.as-pill { font-size: .7rem; padding: 2px 8px; border-radius: 999px; white-space: nowrap; background: var(--panel-3); color: var(--dim); }
.as-pill.ok { background: var(--ok-soft); color: var(--ok); }
.as-pill.warn { background: var(--warn-soft); color: var(--warn); }
.as-pill.bad { background: var(--bad-soft); color: var(--bad); }
.as-dots { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 5px; }
.as-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--line-2); }
.as-dot.ok { background: var(--ok); }
.as-dot.bad { background: var(--bad); }
.as-dot.dim { background: var(--line-2); }

/* —— 逐题折叠 —— */
.as-detail { margin-top: 12px; }
.as-detail > summary { font-size: .82rem; cursor: pointer; color: var(--pri); padding: 6px 0; }

/* —— 这一册做过的测评（册页底部）—— */
.as-hist { margin-top: 14px; border-top: 1px dashed var(--line); padding-top: 10px; }
.as-hist-h { font-size: .8rem; font-weight: 700; color: var(--dim2); margin-bottom: 6px; }
.as-hist-i { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; cursor: pointer; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--panel); padding: .5em .8em; margin-bottom: 6px; font: inherit; color: inherit; }
.as-hist-i:hover { border-color: var(--pri); }
.as-hist-i b { font-size: .84rem; flex: 1; }
.as-hist-sc { font-size: .88rem; font-family: var(--mono); color: var(--txt); }
.as-hist-i .dim { font-size: .72rem; }
/* 考试结果页（批次164）：大分数 + 逐题解析。样式尽量复用 as-*/pc-*，只补这两块。 */
.mx-grow {
  display: inline-block;
  margin-left: 10px;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(200, 165, 74, .16);
  color: #8a6a1f;
  font-size: 12px;
  font-weight: 600;
}
.mx-grow.dim { background: transparent; color: inherit; opacity: .6; font-weight: 400; }
/* 批次210-D：错题去向（「N 道错题已送去错题本」/「已记下，接入后收录」）。
   ★ 独立前缀 mx-wn：成绩单页同时有 mx-grow / mx-score / mxp-*，新类不抢它们的规则。 */
.mx-wn {
  display: inline-block;
  margin-left: 10px;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(74, 144, 226, .14);
  color: #2c5f9e;
  font-size: 12px;
  font-weight: 600;
}
.mx-wn.dim { background: transparent; color: inherit; opacity: .6; font-weight: 400; }
.mx-score { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin: 6px 0 12px; font-size: .84rem; }
.mx-score b { font-size: 1.6rem; font-family: var(--mono); line-height: 1; }
.mx-score.ok b { color: var(--ok); }
.mx-score.bad b { color: var(--bad); }
.mx-ans { margin-top: 8px; font-size: .84rem; }
.mx-ans .dim { font-size: .72rem; margin-top: 6px; }

/* 考试卷面（批次168·100 分制）：卷头 / 题型区块 / 每题分值 / 逐题得失。
   ★ 独立类名、只用 var()，不覆盖别处选择器（深色主题跟着 data-theme 自动切）。 */
.mx-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; padding: 8px 0 2px; font-size: .84rem; }
.mx-head b { font-size: .95rem; }
.mx-sec { margin: 4px 0 8px; }
.mx-sec-h { font-size: .84rem; font-weight: 700; margin: 10px 0 6px; padding-bottom: 4px; border-bottom: 1px solid var(--line); }
.mx-sec-h .dim { font-weight: 400; margin-left: 6px; }
.mx-pts { font-size: .72rem; color: var(--dim2); white-space: nowrap; }
.mx-get { font-size: .72rem; color: var(--ok); white-space: nowrap; }
.mx-lost { font-size: .72rem; color: var(--bad); white-space: nowrap; }

/* 标准考试卷（批次171 A+B）：标准卷头 / 题组说明行 / 填空横线 / 小问作答区 /
   项目式表格 / 装订线 / 打印卷。
   ★ 独立类名、只用 var()，不覆盖别处选择器（深色主题跟着 data-theme 自动切）。 */
.mxp { display: flex; gap: 12px; align-items: stretch; }
/* 装订线：竖排「密封线内不要答题」+ 虚线（标准卷的仪式感，也是"别越过"的提示） */
.mxp-seam { flex: none; width: 20px; writing-mode: vertical-rl; text-align: center;
  font-size: .62rem; color: var(--dim2); letter-spacing: .32em; padding: 4px 0;
  border-right: 1px dashed var(--line-2); user-select: none; }
.mxp-body { flex: 1; min-width: 0; }
/* 标准卷头：学年学期 / 考试名 / 满分用时 / 密封栏（班级姓名考号） */
.mxp-head { text-align: center; padding: 2px 0 8px; border-bottom: 2px solid var(--txt); }
.mxp-term { font-size: .76rem; color: var(--dim); letter-spacing: .06em; }
.mxp-title { font-size: 1.02rem; font-weight: 700; margin: 3px 0; }
.mxp-meta { font-size: .72rem; color: var(--dim); }
.mxp-seal { display: flex; justify-content: center; gap: 22px; margin-top: 8px; font-size: .76rem; color: var(--dim); }
.mxp-seal label { display: inline-flex; align-items: center; gap: 6px; }
.mxp-seal-l { display: inline-block; width: 5.5em; border-bottom: 1px solid var(--line-2); height: 1.2em; }
/* ---- 批次279：卷面成长值说明（用户 2026-10-27 诉求「让学生知道完成后能得多少分，
   言简意赅即可」）—— 独立类名，不给既有卷头版式带变化；深色主题跟着 var() 自动切。 */
.mxp-grow { font-size: .72rem; color: var(--dim); margin-top: 6px; letter-spacing: .02em; }
.mxp-sh-top .mxp-grow { color: #444; font-size: 10.5px; margin-top: 3px; }
/* 题组（区块头带标准说明行：「共N小题，每小题X分，共Y分」） */
.mxp-sec { margin: 12px 0 4px; }
.mxp-sec-h { font-size: .86rem; font-weight: 700; margin: 8px 0 6px; padding-bottom: 4px; border-bottom: 1px solid var(--line); }
.mxp-sec-note { font-weight: 400; font-size: .72rem; color: var(--dim2); margin-left: 8px; }
/* 一道题 */
.mxp-item { margin: 10px 0; }
.mxp-q { display: flex; align-items: baseline; gap: 6px; font-size: .88rem; }
.mxp-no { font-weight: 700; flex: none; }
.mxp-qt { flex: 1; min-width: 0; }
.mxp-p { flex: none; font-size: .72rem; color: var(--dim2); white-space: nowrap; }
.mxp-foot { margin-top: 14px; padding-top: 8px; border-top: 1px dashed var(--line);
  font-size: .68rem; color: var(--dim2); text-align: center; }
/* 填空题：题干里的 ＿＿＿ 就地换成横线输入框 */
.fb-line { font-size: .88rem; line-height: 2.1; }
.fb-in { display: inline-block; width: 6.5em; border: none; border-bottom: 1px solid var(--line-2);
  background: transparent; color: var(--txt); font: inherit; text-align: center;
  padding: 0 4px; margin: 0 2px; outline: none; vertical-align: baseline; }
.fb-in:focus { border-bottom-color: var(--pri); background: var(--pri-soft); }
/* 解答题：小问各自留作答区 */
.sv-sub { margin: 6px 0 8px; }
.sv-q { font-size: .84rem; display: flex; align-items: baseline; gap: 6px; }
.sv-ta { margin-top: 4px; }
/* 项目式表格题：作答格 = 绑到某个小问的输入格 */
.sv-tab { margin: 8px 0 10px; overflow-x: auto; }
.sv-tab table { border-collapse: collapse; font-size: .8rem; min-width: 60%; }
.sv-tab th, .sv-tab td { border: 1px solid var(--line-2); padding: 6px 10px; text-align: center; }
.sv-tab th { background: var(--panel-3); color: var(--dim); font-weight: 600; }
.sv-cell { width: 100%; min-width: 4.5em; border: none; background: transparent; color: var(--txt);
  font: inherit; text-align: center; padding: 2px; outline: none; }
.sv-cell:focus { background: var(--pri-soft); }
/* 结果页：逐小问明细 + 部分分 */
.sv-row { margin: 6px 0; padding: 6px 8px; border: 1px solid var(--line); border-radius: var(--r-sm); font-size: .82rem; }
.sv-row .dim { font-size: .72rem; margin-top: 4px; }
.sv-row-h { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; font-weight: 600; }
.sv-row-h .mxp-p { font-weight: 400; }
.sv-verdict { font-size: .72rem; white-space: nowrap; }
.sv-verdict.ok { color: var(--ok); }
.sv-verdict.bad { color: var(--bad); }

/* ---- 批次172b（C 图形题）：figure 配图（作答页 + 打印卷共用，原生 SVG 零依赖）---- */
.mxp-fig { margin: 8px 0 6px; }
.mxp-fig svg { display: block; max-width: 100%; height: auto; }
.mxp-fig-cap { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
.mxp-sh-q .mxp-fig { margin: 6px 0 8px 1.6em; }
body.printing .mxp-fig { break-inside: avoid; page-break-inside: avoid; }

/* ---- 批次172a：打印卷头居中 + 弹窗头部动作区（用户 10-21 报障）---- */
.mxp-sh-top { text-align: center; border-bottom: 2px solid #111; padding-bottom: 8px; }
.mxp-sh-top .ex-sh-fill { justify-content: center; margin-top: 10px; }
.modal-acts { display: inline-flex; gap: 8px; align-items: center; }
.modal-acts[hidden] { display: none; }

/* ---- 批次210-C：打印卷头二维码（用户诉求④：家长拍照扫码交卷）----
   ★ 右对齐贴第一页最上方：不挤标题行居中版式（172a 立的约定，不给它带变化）。
   ★ 尺寸写 mm 不写 px：打印是物理尺寸 —— 22mm 在 A4 上约 2.2cm，手机隔 15cm 可扫；
     写 px 会随屏幕 DPI 漂，同一张卷在不同电脑上印出来大小不一样。
   ★ 不加 @media（app.css 的媒体查询总数钉死 21，谁都不能追加），
     用 body.printing 这个既有钩子做打印期断页保护。 */
.mxp-sh-qr { display: flex; align-items: center; justify-content: flex-end; gap: 3mm; margin-bottom: 2mm; }
.mxp-sh-qr-tip { font-size: 10.5px; line-height: 1.35; color: #444; text-align: right; }
.mxp-sh-qr-code { line-height: 0; }
.mxp-sh-qr-code svg { width: 22mm; height: 22mm; display: block; }
body.printing .mxp-sh-qr { break-inside: avoid; page-break-inside: avoid; }

/* ---- 批次206：红笔批改（老师批卷式呈现，用户 10-25 拍板「批改直接在卷子上红笔
   批注，总写在卷子顶部，按照老师批改卷面的方式呈现」）----
   ★ 红笔色 --pen 比 --bad 更亮：老师用的是签字笔红，不是警告红；深色主题下
     提亮一档（--bad 在 dark 里偏暗，红笔要"跳"出来）。手写感用楷体栈，
     系统没楷体时回落 cursive，绝不因此报错或变默认黑体。 */
:root { --pen: #d9363e; --pen-soft: #fdecec; }
.mxp-graded { }
/* 卷首总分：密封栏正下方、右对齐 —— 老师批卷往这儿一挥笔的位置 */
.mxp-mark-total { display: flex; align-items: baseline; justify-content: flex-end;
  flex-wrap: wrap; gap: 4px 10px; margin-top: 10px; text-align: right; }
.mxp-mark-total-l { font-size: .8rem; color: var(--dim); }
.mxp-mark-num { font-size: 2.1rem; line-height: 1; font-weight: 700; color: var(--pen);
  font-family: 'KaiTi','STKaiti','Kaiti SC','楷体',cursive;
  border: 2px solid var(--pen); border-radius: 48% / 62%; padding: 2px 16px;
  transform: rotate(-2deg); }
.mxp-mark-full { font-size: .82rem; color: var(--dim); }
.mxp-mark-sub { font-size: .72rem; color: var(--dim2); }
.mxp-mark-verdict { font-size: .78rem; font-weight: 700; color: var(--pri); }
/* 每题判分：红笔 ✓/✗ + 得分（题干行右侧） */
.mxp-mark { margin-left: auto; flex: none; font-size: .78rem; font-weight: 700;
  white-space: nowrap; color: var(--pen);
  font-family: 'KaiTi','STKaiti','Kaiti SC','楷体',cursive; }
/* ★ 批次224：绿=对 / 红=错 / 橙=部分对 / 黄=没写。
   原来三个都用红笔色 --pen，家长分不出"哪题要订正"（用户 2026-10-26 提的）。 */
.mxp-mark.ok { color: var(--ok); }
.mxp-mark.part { color: var(--warn); }
.mxp-mark.bad { color: var(--bad); }
.mxp-mark.sm { font-size: .72rem; font-weight: 600; }
.mxp-mark.dim { color: var(--warn); font-weight: 400; }   /* ★ 批次224：没写=黄 */
/* 错题题号：红笔圈出来（老师习惯性地把错题号圈起来） */
.mxp-no-bad { color: var(--pen); border: 1.5px solid var(--pen); border-radius: 50%;
  padding: 0 5px; }
/* 选项：正确项描红打✓；学生错选项划掉打✗（划掉的是"选错的"这个动作） */
.mxp-graded .pc-opt { cursor: default; align-items: center; }
.mxp-graded .pc-opt.right { border-color: var(--pen); background: var(--pen-soft); color: var(--pen); font-weight: 600; }
.mxp-graded .pc-opt.wrong { border-color: var(--line-2); color: var(--dim2);
  text-decoration: line-through; text-decoration-color: var(--pen); text-decoration-thickness: 1.5px; }
.mxp-tick, .mxp-cross, .mxp-qmark { font-style: normal; margin-left: 8px; font-weight: 700; flex: none; }
.mxp-tick { color: var(--pen); }
.mxp-cross { color: var(--pen); }
.mxp-qmark { color: var(--dim2); }
/* 填空：学生答案就地在空里显示（下划线 = 写在横线上）+ 每空判分 */
.fb-ans { display: inline-block; min-width: 3em; padding: 0 6px; text-align: center;
  border-bottom: 1px solid var(--line-2); color: var(--txt); }
/* 解答题小问的作答展示（只读，长得像写在卷上的答案区） */
.sv-ans { margin-top: 4px; padding: 6px 10px; border: 1px solid var(--line);
  border-radius: var(--r-sm); font-size: .84rem; white-space: pre-wrap; background: var(--panel-2); }
.sv-cell-v { display: inline-block; min-width: 2.5em; }
/* 红笔"应为 …"（错处的正确答案）与"批注："（判定理由）—— 老师卷边口吻 */
.mxp-fix { margin-top: 3px; font-size: .76rem; color: var(--pen);
  font-family: 'KaiTi','STKaiti','Kaiti SC','楷体',cursive; }
.mxp-note { margin-top: 3px; font-size: .74rem; color: var(--pen); opacity: .92;
  font-family: 'KaiTi','STKaiti','Kaiti SC','楷体',cursive; }
.mxp-graded .sv-tab td { vertical-align: top; }

/* ---- 考试记录（批次210：查看之前的提交和批改记录）----
   ★ 独立前缀 mxh-（不与 mxp-* 撞名 —— CSS 同名类互相覆盖是真踩过的坑）。
   红笔判分复用 .mxp-mark.ok/part/bad 与 .mxp-note（同一套「老师批卷」视觉）。 */
.mxh { max-width: 760px; margin: 0 auto; }
.mxh-tip { font-size: .76rem; padding: 2px 2px 10px; }
/* 列表行：考次 · 得分 · 得分率 · 通过与否 · 时间（整行可点） */
.mxh-row { display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  padding: 11px 14px; margin-bottom: 8px; border: 1px solid var(--line);
  border-radius: var(--r-sm); background: var(--panel); cursor: pointer; }
.mxh-row:hover { border-color: var(--pri); }
.mxh-row.pass { border-left: 3px solid var(--pri); }
.mxh-no { font-weight: 700; font-size: .86rem; min-width: 4.5em; }
.mxh-score b { font-size: 1.15rem; color: var(--txt); }
.mxh-score .dim, .mxh-acc { font-size: .74rem; }
.mxh-pass { margin-left: auto; font-size: .76rem; font-weight: 700; color: var(--dim2); }
.mxh-row.pass .mxh-pass { color: var(--pri); }
.mxh-at { font-size: .72rem; min-width: 5.5em; text-align: right; }
/* 明细头：这一次的得分 +  verdict */
.mxh-detail-h { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  padding: 10px 2px 4px; border-bottom: 2px solid var(--pen); }
.mxh-detail-h b { font-size: .95rem; }
.mxh-detail-score { margin-left: auto; font-size: .8rem; color: var(--dim); }
.mxh-detail-score b { font-size: 1.5rem; color: var(--pen); font-weight: 700; }
.mxh-detail-score.pass b { color: var(--pri); }
.mxh-detail-verdict { font-size: .8rem; font-weight: 700; color: var(--dim2); }
.mxh-detail-sub { font-size: .74rem; padding: 6px 2px 10px; }
/* 明细里的一道题（题面行 + 判分 + 答案区） */
.mxh-item { border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 10px 12px; margin-bottom: 10px; background: var(--panel); }
.mxh-item-h { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.mxh-no { font-weight: 700; color: var(--dim2); }
.mxh-q { font-size: .88rem; flex: 1; min-width: 12em; }
.mxh-pts { font-size: .74rem; color: var(--dim2); }
.mxh-sub { margin-top: 6px; padding-top: 6px; border-top: 1px dashed var(--line); font-size: .82rem; }
.mxh-ans { margin-top: 4px; font-size: .84rem; white-space: pre-wrap; }
.mxh-ans .dim { display: block; font-size: .7rem; margin-bottom: 1px; }
/* 选择题选项（批次237：交卷明细里补带选项，不再只剩「学生答：B」）。
   ★ E7 铁律：JS 造的 class 必须在 css 有规则 —— 只画 DOM 不给样式，
     选项就成了几行没有任何分隔的裸文本，看着像串行了。
   ★ 颜色一律走变量（--line / --panel-2），深色模式下不能扎眼。 */
.mxh-opts { margin-top: 6px; display: flex; flex-direction: column; gap: 4px; }
.mxh-opt { font-size: .82rem; padding: 4px 8px; border: 1px solid var(--line);
  border-radius: var(--r-sm); background: var(--panel-2); }
/* 手写原图缩略（批次211：明细里并排展示「手写原图 + 识别文字 + 参考答案」）。
   ★ 样式归位到 CSS、不写内联 —— E7 铁律：JS 造的 class 必须在 css 有规则；
   边框用 var(--line) 跟随主题（内联写死 #ddd 在深色模式会扎眼）。 */
.mxh-hand { max-width: 140px; max-height: 90px; border: 1px solid var(--line);
  border-radius: 6px; vertical-align: middle; }

/* ---- 手写画板（批次210-B：解答题支持画板手写，交卷后识别手写答案）----
   ★ 独立前缀 pad-*/sv-hand*（不撞 mxp-*/mxh-*）。canvas 白底在 JS 里填，
   CSS 只给边框与触控手势（touch-action:none 让 pointermove 不被滚动抢走）。 */
.sv-hand { display: flex; align-items: center; gap: 8px; margin-top: 6px; flex-wrap: wrap; }
.sv-hand-thumb { position: relative; display: inline-block; line-height: 0;
  border: 1px solid var(--line); border-radius: var(--r-sm); overflow: hidden; background: #fff; }
.sv-hand-thumb img { display: block; max-height: 56px; max-width: 160px; width: auto; height: auto; }
.sv-hand-x { position: absolute; top: 0; right: 0; width: 18px; height: 18px; line-height: 16px;
  padding: 0; border: 0; border-radius: 0 0 0 6px; background: rgba(0,0,0,.45);
  color: #fff; font-size: 12px; cursor: pointer; }
.sv-hand-x:hover { background: var(--pen); }
.pad-wrap { max-width: 900px; margin: 0 auto; }
.pad-tip { font-size: .78rem; padding: 2px 2px 8px; line-height: 1.5; }
.pad-canvas { display: block; margin: 0 auto; border: 1px solid var(--line-2);
  border-radius: var(--r-sm); background: #fff; touch-action: none; cursor: crosshair; }
.pad-acts { display: flex; align-items: center; gap: 8px; padding: 10px 2px 2px; }
.pad-acts .sp { flex: 1; }
.pad-wait { text-align: center; padding: 8vh 16px; }
.pad-wait p { margin: 6px 0; }
.pad-spin { width: 34px; height: 34px; margin: 0 auto 12px; border-radius: 50%;
  border: 3px solid var(--line-2); border-top-color: var(--pri);
  animation: padspin 0.9s linear infinite; }
@keyframes padspin { to { transform: rotate(360deg); } }

/* ---- 标准考试卷：打印卷（复用 #examSheet + body.printing，不新增媒体查询）---- */
.mxp-sh { max-width: 760px; margin: 0 auto; color: #111; line-height: 1.5; }
.mxp-sh-head { display: flex; justify-content: space-between; align-items: flex-end;
  border-bottom: 2px solid #111; padding-bottom: 8px; gap: 16px; flex-wrap: wrap; }
.mxp-sh-term { font-size: 13px; color: #444; }
.mxp-sh-meta { font-size: 13px; color: #444; margin-top: 2px; }
/* ★ 批次189：打印卷页脚 —— 纸卷怎么交回来（原来一个字都没有） */
.mxp-sh-foot {
  margin-top: 16px;
  padding-top: 8px;
  border-top: 1px dashed #8a8a8a;
  font-size: 11px;
  line-height: 1.65;
  page-break-inside: avoid;
}
.mxp-sh-foot-h { font-weight: 700; font-size: 11.5px; margin-bottom: 3px; }
.mxp-sh-foot p { margin: 2px 0; }
.mxp-sh-sec { margin: 14px 0 6px; }
.mxp-sh-sec-h { font-size: 15px; font-weight: 700; }
.mxp-sh-sec-note { font-weight: 400; font-size: 13px; color: #444; margin-left: 6px; }
.mxp-sh-q { margin: 12px 0; page-break-inside: avoid; break-inside: avoid; font-size: 14px; }
.mxp-sh-no { font-weight: 700; margin-right: 4px; }
.mxp-sh-opts { display: flex; flex-wrap: wrap; gap: 8px 28px; margin: 4px 0 0 1.6em; }
.mxp-sh-opts > span { flex: 0 0 auto; min-width: 6em; }
.mxp-sh-blanks { margin: 4px 0 0 1.6em; }
.mxp-sh-blank { display: inline-block; min-width: 7em; border-bottom: 1px solid #111; margin-right: 16px; height: 1.7em; }
.mxp-sh-sub { margin: 6px 0 0 1.6em; font-size: 14px; }
.mxp-sh-tab { border-collapse: collapse; margin: 6px 0 0 1.6em; font-size: 13px; }
.mxp-sh-tab th, .mxp-sh-tab td { border: 1px solid #666; padding: 4px 12px; min-width: 3em; height: 1.9em; }
.mxp-sh-tab th { background: #eee; }
body.printing .mxp-sh { max-width: 100%; }
body.printing .mxp-sh-ans { -webkit-print-color-adjust: exact; print-color-adjust: exact; }

/* —— 岛屿上的只读 overlay（测评建议，不是进度）—— */
.as-ov { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; padding: .6em .85em; border: 1px dashed var(--pri); border-radius: var(--r-sm); background: var(--panel-2); }
.as-ov-tag { font-size: .7rem; padding: 2px 8px; border-radius: 999px; background: var(--pri-soft); color: var(--pri); white-space: nowrap; }
.as-ov-t { font-size: .84rem; flex: 1; min-width: 10em; }
.as-ov-w { font-size: .72rem; flex: 1 0 100%; line-height: 1.6; }
/* ★ 里程碑上那枚"快过 / 从这里"角标：**独立类名**，不复刻 .lp-band-l 的语义。
 *   它是 overlay（测评建议），与关卡状态无关 —— 混用会让"改了看不出来"。 */
.lp-band-ov { font-size: 8px; font-weight: 700; fill: var(--pri); }
.lp-svg .lp-band-ov { pointer-events: none; }
.lg-sug::before { background: var(--pri); }

/* ================= 公共资料池（P3）================= */
.pool-i { border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--panel); padding: .75em .85em; margin-bottom: 8px; }
.pool-i.mine { border-color: var(--pri); }
.pool-i.hidden-i { opacity: .6; }
.pool-top { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
.pool-top b { font-size: .93rem; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pool-tag { font-size: .7rem; padding: 2px 8px; border-radius: 999px; background: var(--panel-3); color: var(--dim); white-space: nowrap; }
.pool-tag.k-cards { background: var(--pri-soft); color: var(--pri); }
.pool-tag.k-words { background: var(--ok-soft); color: var(--ok); }
.pool-tag.k-exam { background: var(--warn-soft); color: var(--warn); }
.pool-tag.gone { background: var(--bad-soft); color: var(--bad); }
.pool-sum { font-size: .82rem; color: var(--dim); line-height: 1.6; }
.pool-prev { font-size: .78rem; color: var(--dim2); font-family: var(--mono); margin-top: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pool-bot { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.pool-bot .by { font-size: .74rem; color: var(--dim2); flex: 1; }
.pool-ban { padding: .7em .9em; border-radius: var(--r-sm); background: var(--warn-soft); color: var(--warn); font-size: .82rem; margin-bottom: 10px; line-height: 1.6; }
.pool-note { font-size: .78rem; color: var(--dim2); padding: 0 0 10px; line-height: 1.6; }

/* 表单标签（弹窗内用，与空间门的 .field label 视觉一致但独立） */
.lb { display: block; font-size: .78rem; color: var(--dim); margin-bottom: 5px; font-weight: 500; }
.lb-wrap { margin-bottom: 10px; }
.card-stats .st { cursor: default; }

/* ================= 动效（P8）=================
 * 三条原则，写在这里免得后来被加歪：
 *   1. 只做"状态变化"的动效，不做无意义的持续炫耀。没有撒花、没有抽奖、没有音效 ——
 *      那是短视频的刺激打法，不是学习工具的活。
 *   2. 时长一律 ≤ 320ms（图表生长稍长，但只放一次）。动效是让"变化被看见"，
 *      不是让人盯着看。任何需要等它演完的动效都是坏动效。
 *   3. 全部在 prefers-reduced-motion: reduce 下关闭，见文件末尾的兜底块。
 */

/* 视图切换：轻微上浮，让"换页了"这件事被感知到。
   知识库里的二级分区（.kb-sub）原本是顶层 .view，改版后降了一级，
   这里补上同一条动画，切分区才有和切顶层视图一致的手感。 */
.view:not([hidden]), .kb-sub:not([hidden]) { animation: viewIn var(--dur-3) var(--ease-out) both; }
@keyframes viewIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: none; } }

/* 导航当前项：小圆点弹一下（一次，不循环） */
.nav-i.on .ni-dot { animation: dotPop var(--dur-3) var(--ease-out) both; }
@keyframes dotPop { 0% { transform: scale(.4); } 60% { transform: scale(1.5); } 100% { transform: scale(1); } }

/* 知识卡列表入场：极轻的错峰，避免整屏同时"啪"地出现 */
.kcard { animation: kcIn var(--dur-3) var(--ease-out) both; }
@keyframes kcIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.kcard:nth-child(1) { animation-delay: 0ms; }
.kcard:nth-child(2) { animation-delay: 22ms; }
.kcard:nth-child(3) { animation-delay: 44ms; }
.kcard:nth-child(4) { animation-delay: 66ms; }
.kcard:nth-child(5) { animation-delay: 88ms; }
.kcard:nth-child(n+6) { animation-delay: 100ms; }

/* 状态刚变化的那张卡：状态 chip 呼吸一次，指明"是这里变了" */
.chip.just { animation: chipFlash var(--dur-4) var(--ease-out) 1; }
@keyframes chipFlash {
  0% { transform: scale(1); box-shadow: 0 0 0 0 color-mix(in srgb, var(--pri) 50%, transparent); }
  35% { transform: scale(1.14); box-shadow: 0 0 0 5px transparent; }
  100% { transform: scale(1); box-shadow: 0 0 0 0 transparent; }
}

/* 宠物成长值涨了：整条轻轻一亮，不放大不弹跳（成长是慢事） */
.pet-line.up { animation: petUp var(--dur-4) ease-out 1; }
@keyframes petUp {
  0% { filter: none; }
  30% { filter: drop-shadow(0 0 6px color-mix(in srgb, var(--ok) 60%, transparent)); }
  100% { filter: none; }
}

/* 图表：柱子从底部长起来、折线从左往右画出来。
 * 折线用 pathLength="1" 把路径长度归一，这样任意长度的折线都能用同一套
 * dasharray 动画，不需要在 JS 里调 getTotalLength()（那要等布局完成，会闪）。 */
.chart .ch-bar { transform-box: fill-box; transform-origin: bottom; animation: barGrow var(--dur-4) var(--ease-out) both; }
@keyframes barGrow { from { transform: scaleY(.02); } to { transform: scaleY(1); } }
.chart .ch-line { stroke-dasharray: 1; stroke-dashoffset: 1; animation: chDraw var(--dur-4) var(--ease-out) both; }
@keyframes chDraw { to { stroke-dashoffset: 0; } }
.chart .ch-area { animation: fade var(--dur-4) ease both; }
.chart .ch-dot { animation: dotIn var(--dur-3) var(--ease-out) both; }
@keyframes dotIn { from { opacity: 0; } to { opacity: 1; } }

/* 图示生成中：一条流动的虚线轮廓，说明"还在画"，而不是死等 */
.art-pending { border: 1px dashed var(--line-2); border-radius: var(--r-sm); padding: 14px; }
.art-pending .art-h { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.art-pending .art-h b { font-size: .84rem; color: var(--dim); }
.art-pending .dotline { flex: 1; height: 1px; background: repeating-linear-gradient(90deg, var(--line-2) 0 6px, transparent 6px 12px); background-size: 200% 100%; animation: flow 1.2s linear infinite; }
@keyframes flow { from { background-position: 0 0; } to { background-position: -24px 0; } }

/* 流式光标：给正在吐字的位置一点柔光，让"这里还在写"比一根硬线更容易被注意到。
 * 用 step-end 闪烁（不是渐隐）—— 渐隐在余光里像"卡住了"，闪烁才像"在打字"。 */
.caret { box-shadow: 0 0 7px color-mix(in srgb, var(--pri) 55%, transparent); border-radius: 1px; }

/* ================= 账号体系（批次1）=================
 * 登录页的账号面板、协议勾选、公告入口、个人资料。
 * 全部走已有 CSS 变量，深色主题零额外规则。 */

/* --- 登录页：账号面板 --- */
.acct-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 2px 0 12px; }
.acct-row.center { justify-content: center; margin-top: 12px; }
.acct-code { display: flex; gap: 8px; }
.acct-code input { flex: 1; min-width: 0; }
.acct-code .btn { flex: 0 0 auto; white-space: nowrap; }
.acct-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.acct-agree {
  display: flex; align-items: flex-start; gap: 8px;
  font-size: .82rem; line-height: 1.55; color: var(--dim);
  margin: 4px 0 12px; cursor: pointer;
}
.acct-agree input { margin-top: .22em; flex: 0 0 auto; }
.acct-agree a { color: var(--pri); text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--pri) 35%, transparent); }
.acct-agree a:hover { border-bottom-color: var(--pri); }
.land-link {
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--pri); font: inherit; font-size: .85rem;
}
.land-link:hover { text-decoration: underline; }
.land-link[disabled] { color: var(--dim2); cursor: default; text-decoration: none; }

/* 公告入口原来在侧栏（.ann-line），现在收进「我的」菜单，样式不再需要 */
.ann-item { padding: 12px 0; border-bottom: 1px solid var(--line); }
.ann-item:last-child { border-bottom: 0; }
.ann-item .ah { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.ann-item .ah b { font-weight: 500; }
.ann-item .ab { color: var(--dim); font-size: .88rem; line-height: 1.65; white-space: pre-wrap; }
.ann-item .at { color: var(--dim2); font-size: .78rem; margin-top: 6px; }
.ann-tag {
  font-size: .7rem; padding: 1px 6px; border-radius: 999px;
  background: var(--bad-soft); color: var(--bad); border: 1px solid color-mix(in srgb, var(--bad) 30%, transparent);
}
.ann-unread { width: 6px; height: 6px; border-radius: 50%; background: var(--pri); flex: 0 0 auto; }

/* --- 协议正文 --- */
.doc-body h4 { font-size: .95rem; font-weight: 500; margin: 18px 0 6px; }
.doc-body h4:first-child { margin-top: 4px; }
.doc-body p { color: var(--dim); font-size: .9rem; line-height: 1.75; margin: 0 0 4px; }
.doc-meta { color: var(--dim2); font-size: .78rem; margin-bottom: 10px; }

/* --- 个人资料 --- */
.prof-head { display: flex; align-items: center; gap: 14px; }
.prof-av {
  width: 52px; height: 52px; border-radius: 50%; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  background: var(--panel-3); border: 1px solid var(--line);
  overflow: hidden;
}
.prof-av svg, .prof-av img { width: 100%; height: 100%; display: block; object-fit: cover; }

/* --- 批次130：设置页里那枚"实时头像" ---
   ★ 为什么不是 9 个格子而是一枚大图 + 两个按钮：头像真源换成了装扮后的宠物
     （见 app.js 的 meAvatarHTML），"选一个图标"这个动作本身不存在了。
     留给学生的是**看**（这条鱼现在长这样）+ **改外观的两条真路**（换形态 / 商店戴配件）。
   ★ `.av-live-av` 是正圆不是方的：和右上角 `.me-av`、菜单 `.me-av.big` 同一个形状，
     三处必须看起来是"同一个头像"，否则学生会以为是三个不同的东西。
   ★ 尺寸 76px：比右上角（24）和资料卡（52）都大一号 —— 这一页的用途就是看它。 */
.av-live { display: flex; align-items: center; gap: 14px; margin-top: 12px; flex-wrap: wrap; }
.av-live-av {
  width: 76px; height: 76px; border-radius: 50%; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  background: var(--panel-3); border: 1px solid var(--line); overflow: hidden;
}
.av-live-av svg { width: 100%; height: 100%; display: block; }
.av-live-side { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.av-live-side > b { font-size: .95rem; }
.av-live-side > span { font-size: .78rem; }
.av-live-side .card-bar { margin-top: 6px; flex-wrap: wrap; }
/* ★ 头像里那条鱼自己不上色（fill 全走 .tk-* / .gear-* 变量），所以这里不写死色值 */
.av-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(76px, 1fr)); gap: 10px; margin-top: 12px; }
.av-cell {
  border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--panel-2);
  padding: 6px; cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 4px;
}
.av-cell:hover { border-color: var(--line-2); }
.av-cell.on { border-color: var(--pri); background: var(--pri-soft); }
.av-cell svg { width: 44px; height: 44px; display: block; }
/* ★ 形态格里的鱼是彩色 AI 插画（批次114d）：上面那条管不到它，单独给一条。
   头像格（avatarSvg）仍是 SVG，继续走上面那条 —— 两处共用 .av-cell，别合并。
   ★ 批次117：34→48（用户报「插画太小」）。同时 .av-grid 的 minmax 56→76，
     否则格子装不下 —— 改尺寸必须连带改格子宽。 */
.av-cell .pet-ico { width: 48px; height: 48px; }
.av-cell span { font-size: .72rem; color: var(--dim); }
.av-cell.on span { color: var(--pri); }
.chip-wrap { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.chip-btn {
  border: 1px solid var(--line); background: var(--panel-2); color: var(--dim);
  border-radius: 999px; padding: 5px 13px; font: inherit; font-size: .84rem; cursor: pointer;
}
.chip-btn:hover { border-color: var(--line-2); }
.chip-btn.on { background: var(--pri-soft); border-color: var(--pri); color: var(--pri); }
.goal-list { margin-top: 10px; }
.goal-item {
  display: flex; align-items: center; gap: 10px; padding: 8px 0;
  border-bottom: 1px solid var(--line); font-size: .9rem;
}
.goal-item:last-child { border-bottom: 0; }
.goal-item .gt { flex: 1; min-width: 0; }
.goal-empty { color: var(--dim2); font-size: .88rem; padding: 6px 0; }

/* ================= 批次2：Chat 富功能 ================= */

/* ---- 侧栏收起（只留图标）---- */
.app.side-min .side { width: 66px; flex-basis: 66px; }
.app.side-min .brand-tx > small,
.app.side-min .nav-txt,
.app.side-min .side-h span,
.app.side-min .conv-filter,
.app.side-min .conv-i .ci-t,
.app.side-min .conv-i .ci-p,
.app.side-min .conv-i .ci-x,
.app.side-min .side-h #newConv { display: none; }
/* 收起态：66px 宽塞不下并排 —— 退回竖排居中，图形在名之上。 */
.app.side-min .brand { flex-direction: column; gap: 4px; padding: 16px 8px 10px; text-align: center; }
.app.side-min .brand-tx { align-items: center; }
/* ★ 66px 的栏里 1.05rem「后浪奔涌」会折成两行 —— 收到 .82rem 仍不够，必须禁折行。
   这是收起态**唯一**会破版的地方（不锁死就会出现「后浪奔 / 涌」这种断字）。 */
.app.side-min .brand-tx > b { font-size: .78rem; letter-spacing: 0; white-space: nowrap; }
.app.side-min .brand-logo { height: 30px; }
.app.side-min .nav { padding: 8px 6px; }
.app.side-min .nav-i { justify-content: center; padding: .6em 0; font-size: 1rem; }
.app.side-min .conv-i { justify-content: center; }
.app.side-min .conv-i .ci-fav { display: none; }
.app.side-min .side-h { justify-content: center; padding: 10px 4px 4px; }
.app.side-min .side-h::after { content: "···"; color: var(--dim2); letter-spacing: .1em; }
.app.side-min .proj-list { padding: 0 4px 16px; }

/* ---- 目录宽度拖拽 ---- */
.side-resizer {
  flex: 0 0 5px; width: 5px; cursor: col-resize;
  background: transparent; position: relative; z-index: 3;
  margin-left: -3px;
}
.side-resizer::after {
  content: ""; position: absolute; top: 0; bottom: 0; left: 2px; width: 1px;
  background: transparent; transition: background var(--dur-1);
}
.side-resizer:hover::after, body.resizing .side-resizer::after { background: var(--pri); }
body.resizing { cursor: col-resize; user-select: none; }
body.resizing * { cursor: col-resize !important; }

/* ---- 侧栏筛选 ---- */
.conv-filter { display: flex; gap: 6px; padding: 6px 10px 8px; align-items: center; }
.conv-filter .inp { flex: 1; min-width: 0; font-size: .82rem; padding: .4em .6em; }
.conv-filter .btn.on { background: var(--pri-soft); border-color: var(--pri); color: var(--pri); }
/* ★ 收藏二级面板（2026-10-10）：从 ★ 右边伸出的小面板，
   上半「收藏的对话」、下半「收藏的回答」—— 两层收藏一次看全。 */
.fav-wrap { position: relative; flex: 0 0 auto; }
.fav-fly {
  /* ★★ 批次92：必须 fixed，**不能** absolute。
     面板要伸出侧栏右边缘，而 .side 是 overflow:hidden ⇒ absolute 的面板会被**整个裁掉**：
     getBoundingClientRect 照样给出正确矩形、几何断言全绿，**用户却什么都看不见**（实测
     elementFromPoint 四个角全部命中 .stream，面板可见面积 0）。fixed 的包含块是视口，
     不受任何祖先裁剪。坐标不写死，由 placeFavFly() 按 #favBtn 的 rect 现算。 */
  position: fixed; left: 0; top: 0; z-index: 62;
  /* width 用 min()：窄屏下侧栏是抽屉，面板不许顶出屏幕 */
  width: min(19rem, calc(100vw - 24px)); max-height: 22rem; overflow-y: auto;
  padding: 6px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r); box-shadow: 0 12px 32px rgba(15, 23, 42, .18);
  animation: pmIn var(--dur-1) ease-out;
}
.fav-fly[hidden] { display: none; }
/* 翻到按钮左侧时挂的标记类 —— 左右坐标由 placeFavFly() 算（fixed 下不能用 right:100%） */
.fav-fly.l { transform-origin: right top; }
.fav-hd { font-size: .72rem; font-weight: 700; letter-spacing: .06em; color: var(--dim2); padding: 4px 8px 2px; }
.fav-sec {
  display: flex; align-items: center; gap: 6px;
  font-size: .68rem; font-weight: 700; letter-spacing: .06em; color: var(--dim);
  padding: 7px 8px 3px;
}
.fav-sec span { font-weight: 600; color: var(--dim2); }
.fav-i {
  display: block; width: 100%; text-align: left; cursor: pointer;
  border: 0; background: transparent; color: var(--txt);
  border-radius: var(--r-sm); padding: .42em .55em; font-size: .8rem;
}
.fav-i:hover { background: var(--panel-2); }
.fav-i .fi-t { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fav-i .fi-s {
  display: block; font-size: .68rem; color: var(--dim2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fav-sep { height: 1px; background: var(--line); margin: 6px 4px; }
.fav-empty { padding: 4px 8px 8px; font-size: .74rem; color: var(--dim2); line-height: 1.6; }
/* 从收藏面板跳到某条回答后让它闪一下 —— 否则用户不知道"跳过来了"是跳到哪一条 */
.msg.hit .body { animation: favHit 1.6s ease-out 1; border-radius: var(--r); }
@keyframes favHit {
  0%, 100% { background: transparent; }
  18% { background: var(--pri-soft); }
}
.conv-i .ci-fav { color: var(--warn); font-size: .72rem; flex: 0 0 auto; }
.conv-i .ci-p {
  font-size: .62rem; color: var(--pri); background: var(--pri-soft);
  border-radius: 4px; padding: 1px 4px; flex: 0 0 auto;
}

/* ---- 批次7：左栏项目树（项目是主轴，对话挂在项目下）---- */
.side-empty { padding: 14px 10px; }
.side-empty p { margin: 0 0 6px; font-size: .86rem; color: var(--txt); }
.side-empty .dim { color: var(--dim); font-size: .78rem; line-height: 1.7; }
.side-empty .se-acts { display: flex; gap: 6px; margin-top: 12px; flex-wrap: wrap; }

.side-p { margin: 1px 0; }
.sp-h {
  display: flex; align-items: center; gap: 5px;
  padding: 5px 7px; border-radius: var(--r-sm); cursor: pointer;
  transition: background var(--dur-1);
}
.sp-h:hover { background: var(--panel-2); }
.sp-caret { width: 11px; flex: 0 0 auto; font-size: .68rem; color: var(--dim2); }
.sp-n {
  flex: 1; min-width: 0; font-size: .84rem; font-weight: 600; color: var(--txt);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sp-c { flex: 0 0 auto; font-size: .7rem; color: var(--dim2); }
.sp-add, .sp-x {
  flex: 0 0 auto; opacity: 0; border: 0; background: transparent;
  color: var(--dim2); cursor: pointer; font-size: .82rem; line-height: 1;
  padding: 2px 4px; border-radius: 5px; transition: opacity var(--dur-1), background var(--dur-1);
}
.sp-h:hover .sp-add, .sp-h:hover .sp-x { opacity: 1; }
.sp-add:hover { background: var(--pri-soft); color: var(--pri); }
.sp-x:hover { background: var(--panel-3); color: var(--txt); }
.side-p .conv-i { padding-left: 24px; }

/* 建议"后面再做"的分区（批次57：门控从"墙"改成"建议"）——淡一点，但仍然点得进去 */
.kb-tab.locked { opacity: .55; cursor: pointer; }
.kb-tab.locked:hover { background: var(--hover, rgba(127,127,127,.08)); }
.kb-tab-l { display: inline-flex; align-items: center; margin-left: 3px; color: var(--dim2); }
/* 门控"改成建议"之后弹的选择框（批次57） */
.unlock-lead { margin: 0 0 8px; line-height: 1.6; }
.unlock-acts { display: flex; gap: 10px; justify-content: flex-end; margin-top: 14px; }
.me-mi.locked { opacity: .5; cursor: not-allowed; }
.btn.ghost.on { background: var(--pri-soft); border-color: var(--pri); color: var(--pri); }
/* 「已收成卡」（批次65）：复用 on 的"已激活"观感，但它是**终态**（disabled），
   所以要用 not-allowed 之外的中性提示 —— 学生要能看出"这事办好了"而不是"坏了"。
   opacity 不能压太低，否则像被禁用出错。 */
.btn.ghost.done { background: var(--pri-soft); border-color: var(--pri); color: var(--pri); opacity: 1; cursor: default; }
.btn.ghost.done:hover { background: var(--pri-soft); }

/* ---- 快速滚动 ---- */
.jump-btns {
  position: absolute; right: 26px; bottom: 168px; z-index: 6;
  display: flex; flex-direction: column; gap: 6px;
}
.view#view-chat { position: relative; }
.jump {
  width: 34px; height: 34px; border-radius: 50%;
  border: 1px solid var(--line-2); background: var(--panel); color: var(--dim);
  box-shadow: var(--shadow); font-size: .95rem; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  transition: color var(--dur-1), border-color var(--dur-1), opacity var(--dur-1);
}
.jump:hover:not(:disabled) { color: var(--pri); border-color: var(--pri); }
.jump:disabled { opacity: .35; cursor: default; }

/* ---- 输入区扩展 ---- */
.composer-wrap {
  flex: 0 0 auto; border-top: 1px solid var(--line); background: var(--panel);
}
.composer-tools {
  display: flex; align-items: center; gap: 6px;
  padding: 8px 18px 0; flex-wrap: wrap;
}
.composer-tools .grow { flex: 1; }
.ct {
  border: 1px solid var(--line); background: var(--panel); color: var(--dim);
  border-radius: 999px; padding: .26em .8em; font-size: .76rem; font-weight: 500;
  transition: background var(--dur-1), color var(--dur-1), border-color var(--dur-1);
}
.ct:hover { color: var(--txt); border-color: var(--line-2); background: var(--panel-2); }
.ct.on { background: var(--pri-soft); border-color: var(--pri); color: var(--pri); font-weight: 600; }
.ct-hint { font-size: .72rem; color: var(--dim2); }
.composer { border-top: 0; padding-top: 8px; }

/* 输入区里的模型下拉：跟 .ct 按钮一样高、一样圆，不像表单控件 */
.composer-tools .ct-sel { font-size: .76rem; padding: .26em 1.6em .26em .8em; max-width: 11.5em; }
.composer-tools .ct-chk { display: inline-flex; align-items: center; gap: .35em; font-size: .76rem; }

/* ---- 当前技能标签（输入区左下角，2026-10-05）----
   ★ 为什么放在 composer-wrap 里、排在 composer-tools **之前**：
     它在 DOM 上先出现就自然落在左下角（composer-wrap 是普通块级流），
     而且与「＋」菜单同一个父容器 ⇒ 换技能时能就近对齐、不用跨区定位。
   ★ 只在选了技能时显示（`hidden`）——「全部技能」是默认态，默认态挂标签是噪音。 */
.ag-chip {
  /* ★ 靠左靠的是 `width: fit-content`，**不是** align-self ——
     .composer-wrap 是普通块级容器（不是 flex），align-self 在这里是空操作。
     写成 flex + fit-content 就够了；加 align-self 只会误导下一个人。 */
  display: flex; align-items: stretch; margin: 8px 18px 0;
  width: fit-content;
  border: 1px solid var(--pri); background: var(--pri-soft);
  border-radius: 999px; overflow: hidden;
}
.ag-chip[hidden] { display: none; }   /* 别让 flex 覆盖掉 hidden */
.ag-chip-b {
  display: inline-flex; align-items: center; gap: .4em;
  border: 0; background: transparent; color: var(--pri);
  padding: .28em .55em .28em .8em; font-size: .76rem; font-weight: 600;
  max-width: 16em;
}
.ag-chip-b:hover { background: rgba(37, 99, 235, .09); }
.ag-chip-k { font-weight: 500; color: var(--dim); font-size: .72rem; }
.ag-chip-k::after { content: '·'; margin-left: .4em; }
.ag-chip-n { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ag-chip-x {
  border: 0; background: transparent; color: var(--pri);
  padding: 0 .7em 0 .1em; font-size: .95rem; line-height: 1;
  border-left: 1px solid rgba(37, 99, 235, .22);
}
.ag-chip-x:hover { background: rgba(37, 99, 235, .14); color: var(--bad); }

/* ---- 「要不要我直接讲最关键的那一步？」（批次57）----
   位置与 .ag-chip 相同（输入区上方、靠左），但语义不同：那是"当前状态"（选了哪个技能），
   这是"给你的一个选项"。所以用**虚线**边框 + 中性文字色，视觉上后退一步 ——
   它是建议，不是提示或警告，不该抢眼（抢眼了就变成系统在推他，与自主感相冲）。
   ★ 靠左靠的是 `width: fit-content`（.composer-wrap 是普通块级容器，align-self 在此是空操作）。 */
.ask-key {
  display: flex; align-items: stretch; margin: 8px 18px 0;
  width: fit-content; max-width: calc(100% - 36px);
  border: 1px dashed var(--line); background: var(--panel);
  border-radius: 999px; overflow: hidden;
}
.ask-key[hidden] { display: none; }   /* 别让 flex 覆盖掉 hidden */
/* ★★ 批次115：三颗台阶按钮**各自**也用 hidden 控制显隐 —— 必须显式压掉下面那条 display，
   否则"作者样式 > UA 样式"会让 [hidden] 失效、三颗一起露出来（同 .ask-key 踩过的那个坑）。 */
.ask-key-b[hidden] { display: none; }
.ask-key-b {
  display: inline-flex; align-items: center;
  border: 0; background: transparent; color: var(--txt);
  padding: .28em .5em .28em .8em; font-size: .76rem; font-weight: 500;
  cursor: pointer; font-family: inherit;
}
.ask-key-b:hover { background: var(--pri-soft); color: var(--pri); }
.ask-key-x {
  border: 0; background: transparent; color: var(--dim);
  padding: 0 .7em 0 .1em; font-size: .95rem; line-height: 1;
  border-left: 1px dashed var(--line); cursor: pointer; font-family: inherit;
}
.ask-key-x:hover { color: var(--bad); }

/* ---- 「＋」动作菜单 ---- */
.plus-wrap { position: relative; display: inline-flex; }
.ct-plus { font-size: .95rem; line-height: 1; padding: .1em .55em .2em; font-weight: 500; }
.ct-plus.on { background: var(--pri-soft); border-color: var(--pri); color: var(--pri); }
.plus-menu {
  position: absolute; bottom: calc(100% + 8px); left: 0; z-index: 60;
  min-width: 15.5rem; padding: 6px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r); box-shadow: 0 12px 32px rgba(15, 23, 42, .16);
  animation: pmIn var(--dur-1) ease-out;
}
.plus-menu.r { left: auto; right: 0; }
@keyframes pmIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.pm-i {
  display: flex; align-items: center; gap: 10px; width: 100%;
  border: 0; background: transparent; color: var(--txt); cursor: pointer;
  border-radius: var(--r-sm); padding: 9px 10px; font-size: .86rem; text-align: left;
}
.pm-i:hover { background: var(--panel-2); }
.pm-i.on { color: var(--pri); font-weight: 600; }
.pm-i.on .pm-ic { color: var(--pri); }
.pm-ic {
  width: 1.5em; flex: none; display: inline-flex; align-items: center; justify-content: center;
  color: var(--dim);
}
.pm-ic svg { display: block; }
.pm-tx { flex: 1; }
.pm-ar { color: var(--dim2); font-size: 1rem; }
.pm-sep { height: 1px; background: var(--line); margin: 5px 8px; }

/* ---- 「技能」的二级面板（悬停展开，2026-10-05）----
   从 ＋ 菜单右侧伸出，贴着「技能」那一项。
   ★ 定位是相对 `.pm-wrap`（不是 .plus-menu）—— 只有这样才能对齐到那一项本身；
     挂到 .plus-menu 上的话，面板顶部会对到菜单顶，看着像脱离了那一行的从属关系。
   ★ `max-height` + 滚动：技能有 11 个，加上分组标题会很高，
     不封顶的话面板会顶出屏幕上方（它是从底往上长的）。 */
.pm-wrap { position: relative; }
.pm-fly {
  position: absolute; left: calc(100% + 4px); bottom: -6px; z-index: 61;
  /* 批次48：技能从 11 条涨到 33 条（4 个分类），原来 17rem 一次只露 9 条，
     滚到底很费劲。抬到 21rem；再往上会顶到视口上沿。 */
  min-width: 12.5rem; max-height: 21rem; overflow-y: auto;
  padding: 5px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r); box-shadow: 0 12px 32px rgba(15, 23, 42, .18);
  animation: pmIn var(--dur-1) ease-out;
}
.pm-fly[hidden] { display: none; }
.pm-fly.l { left: auto; right: calc(100% + 4px); }   /* 右边放不下时翻到左侧 */
.pm-f-cat {
  font-size: .68rem; font-weight: 700; letter-spacing: .06em; color: var(--dim);
  padding: 6px 8px 3px;
}
.pm-f-i {
  display: block; width: 100%; text-align: left;
  border: 0; background: transparent; color: var(--txt);
  border-radius: var(--r-sm); padding: .38em .55em; font-size: .8rem;
}
.pm-f-i:hover { background: var(--panel-2); }
.pm-f-i.on { background: var(--pri-soft); color: var(--pri); font-weight: 600; }
.pm-f-i b { font-weight: inherit; }
.pm-f-i span { display: block; font-size: .7rem; color: var(--dim); font-weight: 400; }
.pm-f-sep { height: 1px; background: var(--line); margin: 5px 6px; }
.pm-f-all {
  display: block; width: 100%; text-align: left;
  border: 0; background: transparent; color: var(--pri);
  border-radius: var(--r-sm); padding: .38em .55em; font-size: .76rem; font-weight: 600;
}
.pm-f-all:hover { background: var(--pri-soft); }
/* ★ 另外三项共用的二级面板（2026-10-10）：纵向由 JS 按悬停项摆正，所以只锁横向。 */
.pm-fly-share { bottom: auto; min-width: 14rem; }
.pm-f-empty { padding: 6px 8px 8px; font-size: .74rem; color: var(--dim2); line-height: 1.6; }

/* ---- 待发送附件 ---- */
.attach-preview { display: flex; flex-wrap: wrap; gap: 8px; padding: 10px 18px 0; }
.att-chip {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: 4px 6px 4px 4px; font-size: .78rem;
}
.att-chip img { width: 34px; height: 34px; object-fit: cover; border-radius: 6px; }
.att-chip b { max-width: 12em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.att-chip i { color: var(--dim2); font-style: normal; font-size: .7rem; }
.att-chip button {
  border: 0; background: transparent; color: var(--dim2); font-size: .95rem;
  line-height: 1; padding: 0 3px; border-radius: 4px;
}
.att-chip button:hover { background: var(--bad-soft); color: var(--bad); }

/* ---- 对话级临时资料 ---- */
.temp-docs { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; padding: 10px 18px 0; }
.td-label { font-size: .72rem; color: var(--dim2); font-weight: 600; letter-spacing: .04em; }
.td-chip {
  display: inline-flex; align-items: center; gap: 5px;
  background: var(--pri-soft); color: var(--pri); border: 1px solid transparent;
  border-radius: 999px; padding: .2em .4em .2em .7em; font-size: .75rem; max-width: 20em;
}
.td-chip.bad { background: var(--bad-soft); color: var(--bad); }
.td-chip button { border: 0; background: transparent; color: inherit; opacity: .6; font-size: .9rem; line-height: 1; padding: 0 3px; }
.td-chip button:hover { opacity: 1; }
.td-note { font-size: .7rem; color: var(--dim2); }

/* ---- 消息内扩展 ---- */
/* 配图区：正文之前放用户上传的图/文件，正文之后放 AI 配的图与互动课堂。
   图片宽度跟着版式走：批次12 对话列从 840px 改成通栏后，
   420px 在宽屏上又变回"缩略图"，所以提到 520px 一档。 */
.msg-atts { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }
.msg-atts:last-child { margin-bottom: 0; }
.msg-att-img {
  display: block; border-radius: 12px; overflow: hidden; border: 1px solid var(--line);
  line-height: 0; box-shadow: var(--shadow); transition: transform var(--dur-1), box-shadow var(--dur-1);
}
.msg-att-img:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); }
.msg-att-img img { display: block; max-width: 520px; max-height: 520px; object-fit: cover; }
.msg-att-file {
  display: inline-block; background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--r-xs); padding: .2em .6em; font-size: .76rem; color: var(--dim);
}
/* AI 自动配的插画：独占一行，跟随主题边框。
   · svg 模式（几何/物理示意图）= 模型画的结构化矢量图，620px 够看清；
   · raster 模式（AI 生图，规划/总结/计划的场景插画）= 760px，值得看清细节。 */
.msg-att-illu {
  flex: 1 1 100%; width: 100%; max-width: 620px; margin: 4px 0 2px;
  border: 1px solid var(--line); border-radius: 12px; overflow: hidden; line-height: 0;
  box-shadow: var(--shadow); background: var(--panel-2);
}
.msg-att-illu.art-ai { max-width: 760px; }
/* 「AI 海报」（批次80）：整张 AI 生图，**竖版 2:3**（1696x2528）——
   铺满 760px 宽会变成 1140px 高，在聊天里太霸道，所以**限高、宽度自适应**：
   想看清细节就点「下载」拿原图。★ 只加普通规则、**不新增 @media**
   （_mapcheck 断言"文件里最后一个 @media 是地图那条窄屏回退"）。 */
.msg-att-illu.art-aiposter img { width: auto; max-width: 100%; max-height: 640px; margin: 0 auto; }
.msg-att-illu.art-aiposter .poster-dl {
  margin-left: .5em; color: var(--pri); text-decoration: none;
  border-bottom: 1px solid currentColor; white-space: nowrap;
}
.msg-att-illu svg, .msg-att-illu img { display: block; width: 100%; height: auto; }
.msg-att-illu .illu-cap {
  display: block; line-height: 1.5; font-size: .72rem; color: var(--dim2);
  padding: .45em .8em; background: var(--panel); border-top: 1px solid var(--line);
}
/* 计划卡（学习计划 / 阶段安排）：是"信息图"，不是插画。
   · 列数不定（2~8 列），用 auto-fit 让浏览器自己决定一行放几列 ——
     窄屏自动折成两行，全程不需要 JS 参与；
   · 每条要点由浏览器自动折行，中英混排也不会算错（这正是当初不画 SVG 的原因）；
   · 宽一点（1040px）：竞品那种 7 列计划表在 420px 里是排不下的。 */
.msg-att-illu.art-plan-wrap { max-width: 1040px; line-height: 1.5; background: var(--panel); }
.msg-att-illu.art-plan-wrap .art {
  margin: 0; padding: 0; max-width: none; width: 100%;
  border: 0; border-radius: 0; background: transparent; box-shadow: none;
}
.art-plan .art-h { padding: 12px 14px 9px; margin-bottom: 0; border-bottom: 1px solid var(--line); }
.art-plan .art-h > span {
  font-size: .68rem; color: var(--pri); background: var(--pri-soft);
  border-radius: 999px; padding: .12em .55em;
}
/* 「存成图片」：计划卡右上角，导出 1080 宽竖版海报（见 public/ai/js/poster.js） */
.art-plan .art-h .plan-save {
  margin-left: auto; flex: none; font-family: inherit; line-height: 1.4;
  padding: .24em .72em; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--line); background: var(--panel); color: var(--dim);
  font-size: .68rem; transition: color var(--dur-1), border-color var(--dur-1);
}
.art-plan .art-h .plan-save:hover { color: var(--pri); border-color: var(--pri); }
.art-plan .art-h .plan-save:disabled { opacity: .6; cursor: default; }
.plan-sub { font-size: .78rem; color: var(--dim); padding: 9px 14px 0; }

.art-map-wrap { max-width: 1040px; }
/* ===== 学习地图卡（批次73 改版）：与 Canvas 海报共用同一套视觉语言 =====
   暖色标题带 + 红色主线缎带 + 序号药丸 + 彩色描边组卡 + 底部主线条。
   ★ 为什么必须和海报逐项对齐：聊天里看到的、和点「下载海报」拿到的那张，
     必须是**同一个东西**。否则"在聊天里觉得好看、下载下来是另一张图"是最伤的体验。
   ★ 卡片自带浅色底（#FFFBF0），所以里面的文字一律写**固定深色**、不跟主题变量走
     —— 否则深色主题下会变成浅字配浅底，整张卡看不见。 */
.art-lmap { overflow: hidden; border-radius: 20px; background: #FFFBF0; border: 1px solid #EFE3CE; }
.art-lmap .lmap-h { display:flex; align-items:center; gap:10px; padding:14px 16px; }
.art-lmap .lmap-h b { font-size:16px; color:#7A3E0A; }
.art-lmap .lmap-h span { color:#A8834A; font-size:12px; }
.art-lmap .map-save { margin-left:auto; border:1px solid #E7D6B4; border-radius:9px; background:#fff; color:#7A3E0A; padding:7px 11px; cursor:pointer; }
.art-lmap .map-save:hover { border-color:#BA7517; }
.art-lmap .map-save:disabled { opacity:.6; cursor:default; }

.lmap-ribbon { margin:0 16px; padding:9px 14px; border-radius:999px; background:#E24B4A; color:#fff; font-size:13px; font-weight:700; text-align:center; overflow-wrap:anywhere; }
/* ★ 批次78：序号药丸退场 → **卡片内编号徽章**（与海报左上角那枚同款：
   圆角方块、主题色实底、白色粗体数字）。悬空药丸是一排数字孤零零飘在缎带下面，
   没有连线、也看不出是什么；收进卡片左上角之后它才真的读得出"这是第几步"。
   ★ 圆角取 9px（不是 50%）：海报那枚是 roundRect(46,46,r15)，比例 0.326，
     缩到 26px 就是 9px —— 两处必须同一形状，否则聊天里圆、下载下来方。 */
.lmap-g-no { flex:0 0 auto; width:26px; height:26px; border-radius:9px; color:#fff; font-style:normal; font-size:14px; font-weight:700; display:flex; align-items:center; justify-content:center; }

.lmap-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:14px; padding:14px 16px 16px; }
/* ★ 批次78：卡底/描边改成**从主题色派生**（与海报的 mixWhite(color,0.90/0.60) 同一套比例）。
   先写死色再写 color-mix：老浏览器不认 color-mix 时整条声明会被丢弃，必须留前一行兜底
   —— 直接只写 color-mix 的话，不支持的环境里卡底会掉成透明。 */
.lmap-g { min-width:0; background:#fff; background:color-mix(in srgb, var(--lm-color) 10%, #fff); border:2px solid #F0E7D6; border-color:color-mix(in srgb, var(--lm-color) 42%, #fff); border-radius:16px; padding:12px; display:flex; flex-direction:column; }
.lmap-g-head { display:flex; align-items:center; gap:10px; margin-bottom:10px; flex:0 0 auto; }
.lmap-g-txt { min-width:0; flex:1 1 auto; display:flex; flex-direction:column; gap:3px; }
.lmap-g-txt span { color:var(--lm-color); font-size:12px; font-weight:700; }
.lmap-g-txt b { color:#172554; font-size:15px; overflow-wrap:anywhere; }
.lmap-g-art { flex:0 0 auto; width:88px; height:66px; border-radius:12px; overflow:hidden; background:#F3F6FC; background:color-mix(in srgb, var(--lm-color) 20%, #fff); display:flex; align-items:center; justify-content:center; }
.lmap-g-art img { width:100%; height:100%; object-fit:cover; display:block; }
.lmap-g-art .lmap-ic { width:34px; height:34px; color:var(--lm-color); }

/* ★ 同排两张组卡被 grid 拉到等高 ⇒ 内容少的那张底部会空一大块。
   ★ 批次78：改成**条目整摞垂直居中**（justify-content:center），与海报的 `extra / 2` 同一招。
     旧版是让每个条目自己 `flex:1 1 auto` 摊开 —— 那是"高盒子装少内容、字全挤在顶部"，
     条目一少就变成三块大留白，比空在底部更难看。 */
.lmap-items { display:flex; flex-direction:column; gap:9px; flex:1 1 auto; justify-content:center; }
/* ★ 条目 = 白底 + 左侧主题色竖条（海报 lmDrawItem 的画法：卡底是浅色、条目才是白块，
   层次靠"浅色托白"而不是"白托浅灰"。旧版 #F7F9FD 是一块冷灰蓝，跟暖色卡底不是一套色系）。 */
.lmap-item { min-width:0; background:#fff; border-left:3px solid var(--lm-color); border-radius:12px; padding:10px 12px; flex:0 0 auto; }
.lmap-item h4 { display:flex; align-items:center; gap:7px; margin:0 0 6px; color:#1E293B; font-size:14px; overflow-wrap:anywhere; }
.lmap-item h4 .lmap-ic { width:16px; height:16px; flex:0 0 auto; color:var(--lm-color); }
/* ★ 这里**不能**跟主题变量走：条目底是固定白，深色主题的 --dim(#9BA5B8) 压上去只有 ~2.5:1。
   卡片自带浅色底 ⇒ 里面的文字一律写固定深色（这条本来就在文件里写着，旧代码漏了）。 */
.lmap-item p { margin:4px 0 0; color:#475569; font-size:12px; line-height:1.55; overflow-wrap:anywhere; }
.lmap-item p.lmap-texts { color:#64748B; }
.lmap-item p i { display:inline-block; min-width:32px; margin-right:6px; color:var(--lm-color); font-style:normal; font-weight:700; }
.lmap-item p.lmap-pit i { color:#C2410C; }

.lmap-sum { display:grid; gap:7px; margin:0 16px 16px; padding:13px 15px; border-radius:14px; background:#EAF1FE; }
.lmap-sum b { color:#1D4ED8; font-size:13px; }
.lmap-sum span { display:flex; align-items:baseline; gap:8px; color:#334155; font-size:13px; }
.lmap-sum span i { flex:0 0 auto; width:7px; height:7px; border-radius:50%; }

/* ── 单元一览卡（批次76）：专属"知识板"版式 ──────────────────────────
   ★ 与学习地图卡共用同一套暖色视觉语言（标题带 / 缎带 / 序号药丸 / 主题色描边 / 底部总结），
     但"格子里装什么"完全不同：地图格 = 若干篇目小卡，单元格 = 通栏插画 + 要点清单 + 任务/留心。
   ★ 基准规则全部写在所有 @media 之前（窄屏回退在文件末尾）。 */
.art-ub { overflow: hidden; border-radius: 20px; background: #FFFBF0; border: 1px solid #EFE3CE; }
.art-ub .ub-h { display:flex; align-items:center; gap:10px; padding:14px 16px; }
.art-ub .ub-h b { font-size:16px; color:#7A3E0A; }
.art-ub .ub-h span { color:#A8834A; font-size:12px; }
.art-ub .ub-save { margin-left:auto; border:1px solid #E7D6B4; border-radius:9px; background:#fff; color:#7A3E0A; padding:7px 11px; cursor:pointer; }
.art-ub .ub-save:hover { border-color:#BA7517; }
.art-ub .ub-save:disabled { opacity:.6; cursor:default; }

.ub-ribbon { margin:0 16px; padding:9px 14px; border-radius:999px; background:#E24B4A; color:#fff; font-size:13px; font-weight:700; text-align:center; overflow-wrap:anywhere; }
.ub-pills { display:flex; justify-content:center; flex-wrap:wrap; gap:14px; padding:14px 16px 2px; }
.ub-pills i { width:26px; height:26px; border-radius:50%; color:#fff; font-style:normal; font-size:13px; font-weight:700; display:flex; align-items:center; justify-content:center; }

.ub-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:14px; padding:14px 16px 16px; }
/* ★ 批次82：只有一个单元时通栏（学生学单个单元点「单元一览」的场景）。
   放在**基准样式区**（第一个响应式 @media 之前），且**不新增 @media** ——
   _p25check/_parity7check 断言基准样式在首个 @media 前、_mapcheck 断言末尾 @media 是地图那条。 */
.ub-grid.is-one { grid-template-columns:1fr; }
.ub-u { min-width:0; background:#fff; border:1px solid #F0E7D6; border-top:5px solid var(--ub-color); border-radius:16px; padding:12px; display:flex; flex-direction:column; }
.ub-u-art { width:100%; height:96px; border-radius:12px; overflow:hidden; background:#F3F6FC; display:flex; align-items:center; justify-content:center; margin-bottom:10px; }
.ub-u-art img { width:100%; height:100%; object-fit:cover; object-position:center 75%; display:block; }
.ub-u-art .lmap-ic { width:48px; height:48px; color:var(--ub-color); }
.ub-u-head { display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; margin-bottom:6px; }
.ub-u-head i { color:var(--ub-color); font-style:normal; font-size:12px; font-weight:700; }
.ub-u-head b { color:#172554; font-size:15px; overflow-wrap:anywhere; }
.ub-words { margin:0 0 8px; color:var(--muted); font-size:12px; line-height:1.55; overflow-wrap:anywhere; }
.ub-pts { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:6px; }
.ub-pts li { position:relative; padding-left:14px; color:#334155; font-size:13px; line-height:1.5; overflow-wrap:anywhere; }
.ub-pts li::before { content:''; position:absolute; left:0; top:7px; width:6px; height:6px; border-radius:50%; background:var(--ub-color); }
.ub-task, .ub-pit { margin:8px 0 0; color:var(--dim); font-size:12px; line-height:1.55; overflow-wrap:anywhere; }
.ub-task i, .ub-pit i { display:inline-block; min-width:32px; margin-right:6px; color:var(--ub-color); font-style:normal; font-weight:700; }
.ub-pit i { color:#C2410C; }

.ub-sum { display:grid; gap:7px; margin:0 16px 16px; padding:13px 15px; border-radius:14px; background:#EAF1FE; }
.ub-sum b { color:#1D4ED8; font-size:13px; }
.ub-sum span { display:flex; align-items:baseline; gap:8px; color:#334155; font-size:13px; }
.ub-sum span i { flex:0 0 auto; width:7px; height:7px; border-radius:50%; }

.plan-grid {
  list-style: none; margin: 0; padding: 12px 14px 6px; display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.plan-col {
  position: relative; background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 10px; padding: 11px 10px 10px; overflow: hidden;
}
.plan-col::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 3px; background: var(--c); }
.plan-day { font-size: .72rem; font-weight: 700; color: var(--c); letter-spacing: .02em; }
.plan-theme { font-size: .86rem; font-weight: 650; color: var(--txt); margin: 3px 0 6px; }
.plan-pts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
.plan-pts li { position: relative; padding-left: 11px; font-size: .78rem; color: var(--dim); }
.plan-pts li::before {
  content: ""; position: absolute; left: 1px; top: .52em;
  width: 4px; height: 4px; border-radius: 50%; background: var(--c); opacity: .8;
}
.plan-focus {
  margin-top: 8px; padding: 5px 8px; border-radius: 6px; font-size: .74rem;
  color: var(--txt); background: var(--panel); border-left: 3px solid var(--c);
}
.plan-tips {
  margin: 6px 14px 13px; padding: 9px 11px; border-radius: 9px;
  background: var(--panel-2); border: 1px dashed var(--line-2);
}
.plan-tips > b { display: block; font-size: .74rem; color: var(--dim); font-weight: 600; margin-bottom: 4px; }
.plan-tips ol { margin: 0; padding-left: 1.2em; }
.plan-tips li { font-size: .78rem; color: var(--dim); margin: 2px 0; }

/* ---- 互动卡（步骤条 / 选择题 / 翻卡）----
 * 三样共用 .art-int 外框。设计约束（见 render.js 的 interactive() 注释）：
 *   · **无 JS 也读得懂**：步骤全展开、选项与解析都在、翻卡正面朝上 →
 *     所以这里所有"隐藏"都必须是 `.on`/`.open` 这类**增量**类，
 *     不能在 CSS 里默认 display:none 掉正文（打印/复制时正文会一起消失）。
 *   · 与主题变量绑定（浅色/深色都对），不写死颜色。
 *   · 窄屏（≤480）按钮不换行、字号不缩到看不清。
 */
.art-int { padding: 0; overflow: hidden; }
.art-int .art-h { padding: 11px 14px 9px; margin-bottom: 0; border-bottom: 1px solid var(--line); }
.art-int .art-h > span {
  font-size: .68rem; color: var(--pri); background: var(--pri-soft);
  border-radius: 999px; padding: .12em .55em; flex: none;
}
.int { padding: 12px 14px 14px; }
.int-btn {
  font-family: inherit; font-size: .76rem; line-height: 1.4; cursor: pointer;
  padding: .34em .9em; border-radius: 999px; border: 1px solid var(--line);
  background: var(--panel); color: var(--dim); transition: color var(--dur-1), border-color var(--dur-1), background var(--dur-1);
}
.int-btn:hover { color: var(--pri); border-color: var(--pri); }
.int-btn:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }
.int-foot { display: flex; gap: 7px; align-items: center; margin-top: 11px; flex-wrap: wrap; }
.int-foot .int-prev, .int-foot .int-next { color: var(--pri); border-color: var(--pri-soft); background: var(--pri-soft); }
.int-foot .int-prev:disabled, .int-foot .int-next:disabled { opacity: .4; cursor: default; color: var(--dim); border-color: var(--line); background: var(--panel); }
.int-foot .int-all { margin-left: auto; }

/* —— 步骤条：默认全部展开（无 JS 可读），点了才变成"一步一步" —— */
.int-steps-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
.int-step { border-radius: var(--r-sm); border: 1px solid transparent; transition: background var(--dur-1), border-color var(--dur-1); }
.int-step.open { background: var(--panel-2); border-color: var(--line); }
.int-step-h {
  display: flex; align-items: center; gap: 9px; width: 100%; text-align: left;
  font-family: inherit; font-size: .84rem; font-weight: 600; color: var(--txt);
  background: none; border: 0; cursor: pointer; padding: 8px 8px; border-radius: var(--r-sm);
}
.int-step-n {
  flex: none; width: 22px; height: 22px; border-radius: 50%; font-size: .72rem; font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--panel-3); color: var(--dim);
}
.int-step.done .int-step-n { background: var(--ok); color: #fff; }
.int-step.cur .int-step-n { background: var(--pri); color: #fff; }
.int-step-t { flex: 1; min-width: 0; }
.int-step-x {
  flex: none; width: 8px; height: 8px; margin-right: 3px;
  border-right: 1.6px solid var(--dim2); border-bottom: 1.6px solid var(--dim2);
  transform: rotate(45deg); transition: transform var(--dur-2);
}
.int-step.open .int-step-x { transform: rotate(-135deg); }
.int-step-b { font-size: .82rem; color: var(--dim); line-height: 1.65; padding: 0 8px 9px 39px; }
/* JS 接管后才折叠 —— 用一个类标记，保证"没跑 JS 时正文可见" */
.int-steps.armed .int-step:not(.open) .int-step-b { display: none; }
.int-steps.armed .int-step:not(.open) .int-step-h { color: var(--dim); font-weight: 500; }

/* —— 选择题：点一下给对错 + 解析，不记分 —— */
.int-q { font-size: .88rem; font-weight: 600; color: var(--txt); line-height: 1.6; margin-bottom: 10px; }
.int-opts { display: flex; flex-direction: column; gap: 6px; }
.int-opt {
  display: flex; align-items: flex-start; gap: 9px; width: 100%; text-align: left;
  font-family: inherit; font-size: .84rem; color: var(--txt); line-height: 1.55;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 9px 11px; cursor: pointer; transition: border-color var(--dur-1), background var(--dur-1);
}
.int-opt:hover:not(:disabled) { border-color: var(--pri); }
.int-opt:disabled { cursor: default; }
.int-opt-k {
  flex: none; width: 20px; height: 20px; border-radius: 6px; font-size: .72rem; font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--panel); border: 1px solid var(--line-2); color: var(--dim);
}
.int-opt-t { flex: 1; min-width: 0; }
.int-opt.right { border-color: var(--ok); background: var(--ok-soft); }
.int-opt.right .int-opt-k { background: var(--ok); border-color: var(--ok); color: #fff; }
.int-opt.wrong { border-color: var(--bad); background: var(--bad-soft); }
.int-opt.wrong .int-opt-k { background: var(--bad); border-color: var(--bad); color: #fff; }
/* 没标标准答案时（data-noanswer）：点中的那个只做"你看的是这条"的标记，
   一律中性色 —— 不判分就不该出现对/错的绿红两色。 */
.int-opt.picked { border-color: var(--pri); background: var(--pri-soft); }
/* 揭晓后：正确答案始终有标记（选错的人也要看得到对的是哪个） */
.int-quiz.revealed .int-opt[data-ok="1"] { border-color: var(--ok); }
.int-quiz.revealed .int-opt[data-ok="1"] .int-opt-k { background: var(--ok); border-color: var(--ok); color: #fff; }
.int-why { font-size: .78rem; color: var(--dim); line-height: 1.6; padding: 5px 2px 0 29px; }
.int-quiz:not(.revealed) .int-why { display: none; }
.int-res {
  margin-top: 10px; padding: 8px 11px; border-radius: var(--r-sm);
  font-size: .82rem; font-weight: 600; color: var(--txt); background: var(--panel-2); border-left: 3px solid var(--pri);
}
.int-res.ok { background: var(--ok-soft); border-left-color: var(--ok); color: var(--ok); }
.int-res.no { background: var(--warn-soft); border-left-color: var(--warn); color: var(--warn); }

/* —— 翻卡：正面朝上（无 JS 时就是一张卡片列表），点了翻 —— */
.int-flip-grid { display: grid; gap: 8px; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); }
.int-card {
  position: relative; min-height: 78px; text-align: center; font-family: inherit; cursor: pointer;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 12px 10px; display: flex; align-items: center; justify-content: center;
  transition: border-color var(--dur-1), background var(--dur-1);
}
.int-card:hover { border-color: var(--pri); }
.int-card[aria-pressed="true"] { background: var(--pri-soft); border-color: var(--pri); }
.int-card-in { display: flex; flex-direction: column; gap: 5px; align-items: center; }
.int-card-f { font-size: .86rem; font-weight: 650; color: var(--txt); line-height: 1.5; }
.int-card-b { font-size: .78rem; color: var(--dim); line-height: 1.55; }
/* 未翻开时背面藏起来 —— 但只在 armed（JS 已接管）之后，否则复制/打印会丢背面 */
.int-flip.armed .int-card[aria-pressed="false"] .int-card-b { display: none; }
.int-flip.armed .int-card[aria-pressed="false"] .int-card-f::after {
  content: "点开看答案"; display: block; font-size: .68rem; font-weight: 400;
  color: var(--dim2); margin-top: 4px;
}
@media (max-width: 480px) {
  .int-foot { gap: 6px; }
  .int-foot .int-all { margin-left: 0; width: 100%; }
  .int-flip-grid { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); }
}

/* 打印/导出：把互动卡的"未展开"状态也摊平 —— 纸上没有可点的东西 */
@media print {
  .int-foot { display: none; }
  .int-steps.armed .int-step .int-step-b { display: block !important; }
  .int-flip.armed .int-card .int-card-b { display: block !important; }
  .int-quiz .int-why { display: block !important; }
}
.msg-net { font-size: .74rem; border-radius: var(--r-xs); padding: .4em .7em; margin-top: 8px; }
.msg-net.on { background: var(--ok-soft); color: var(--ok); }
.msg-net.off { background: var(--warn-soft); color: var(--warn); }
.msg-net .net-list { display: flex; flex-direction: column; gap: 2px; margin-top: 5px; }
.msg-net .net-list a { color: inherit; text-decoration: underline; font-size: .72rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 骨架条下面的一行等待说明（如"正在打开你发的链接…"）：干等几秒最容易被当成卡死 */
.wait-hint { font-size: .76rem; color: var(--dim2); margin-top: 6px; }
.msg-src { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; font-size: .72rem; color: var(--dim2); margin-top: 8px; }
.src-chip { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-xs); padding: .1em .5em; color: var(--dim); }
.msg-tr {
  margin-top: 8px; border-left: 3px solid var(--pri); background: var(--pri-soft);
  border-radius: 0 var(--r-xs) var(--r-xs) 0; padding: .5em .8em; font-size: .88rem;
}
.msg-tr > b { display: block; font-size: .7rem; color: var(--pri); margin-bottom: 3px; font-weight: 600; letter-spacing: .03em; }
.msg-tr .md > p:first-child { margin-top: 0; }
.msg-tr .md > p:last-child { margin-bottom: 0; }
.msg-flag { font-size: .74rem; color: var(--dim2); margin-top: 8px; }
.msg-flag.bad { color: var(--bad); }
.msg .acts { flex-wrap: wrap; }

/* ---- AI 配图 ---- */
.img-holder { margin: 0 0 22px; }
.imgload { font-size: .8rem; color: var(--dim); display: flex; align-items: center; gap: 8px; }
.imgload::before {
  content: ""; width: 12px; height: 12px; border-radius: 50%;
  border: 2px solid var(--line-2); border-top-color: var(--pri);
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.imgfail {
  font-size: .8rem; color: var(--bad); background: var(--bad-soft);
  border-radius: var(--r-sm); padding: .6em .8em;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.art-img { display: block; width: 100%; max-width: 560px; border-radius: var(--r-sm); }

/* ---- 弹窗里的列表（对话菜单 / 智能体 / 移动项目）---- */
.cm-list, .ag-list { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; max-height: 52vh; overflow-y: auto; }
.cm-i, .ag-i {
  text-align: left; border: 1px solid var(--line); background: var(--panel);
  border-radius: var(--r-sm); padding: .6em .8em; display: flex; flex-direction: column; gap: 2px;
  transition: border-color var(--dur-1), background var(--dur-1);
}
.cm-i:hover, .ag-i:hover { border-color: var(--pri); background: var(--pri-soft); }
.cm-i.on, .ag-i.on { border-color: var(--pri); background: var(--pri-soft); }
.cm-i b, .ag-i b { font-size: .88rem; font-weight: 600; }
.cm-i span, .ag-i span { font-size: .74rem; color: var(--dim); }
.cm-i.danger b { color: var(--bad); }
.cm-i.danger:hover { border-color: var(--bad); background: var(--bad-soft); }
.ag-i b i { font-style: normal; font-weight: 400; font-size: .7rem; }
/* 已选中/已挂上：右上角一个小勾。
   只有浅蓝底 + 蓝描边时，用户分不清"这是被选中的"还是"这类条目就长这样"——
   挂资料的弹窗是多选，点过没点过必须一眼可辨（纯 CSS 画，不用图标字体）。 */
.ag-i.on { position: relative; }
.ag-i.on::after {
  content: ''; position: absolute; top: .62em; right: .75em;
  width: 5px; height: 10px; border: solid var(--pri);
  border-width: 0 2px 2px 0; transform: rotate(45deg);
}
/* ---- 技能弹窗的分类小标题（2026-10-05）----
   改前是一整条平铺列表，学科卡与方法卡交错（语文/费曼讲解/数学/错题复盘…），
   用户看不出哪些是学科。加组名把「学科」「备考教练」分开。
   ★ 用 sticky 让它滚动时钉住 —— 列表 max-height:52vh 会滚，
     组名跟着滚走的话，用户滑到中间就不知道自己在哪一组了。 */
.ag-cat {
  display: flex; align-items: center; gap: .45em;
  margin: 6px 0 -2px; font-size: .72rem; font-weight: 700;
  letter-spacing: .06em; color: var(--dim);
  position: sticky; top: 0; z-index: 1;
  background: var(--panel); padding: 5px 2px 4px;
}
.ag-cat::after { content: ''; flex: 1; height: 1px; background: var(--line); }
.ag-cat-n { font-weight: 500; color: var(--dim2); letter-spacing: 0; }
.ag-cat-n::after { content: ' 项'; font-size: .68rem; }
/* 第一个组名贴着搜索框，去掉多余上边距 */
#agList .ag-cat:first-of-type { margin-top: 0; }

/* ---- 技能选择弹窗：加宽 + 分组网格（批次48，2026-10-06）----
   改前是 `width: min(560px)` 的**单列长列表**，33 条技能要滚很久才看得到
   「备考教练」；用户原话：「窗口要横向大一点，尽可能展示更多技能，
   技能要分门别类」。现在弹窗 960px、每个分类各自一个自适应网格。 */
.modal.wide { width: min(960px, 100%); }
/* 弹窗变宽后，纵向反而更该给足 —— 否则宽出来的空间全浪费在下拉上 */
.modal.wide .ag-list { max-height: 66vh; }

/* 「全部技能」横跨整行：它不属于任何分类，是"不指定"，不该混进网格 */
.ag-all { width: 100%; }

/* 一个分类 = 标题 + 它自己的网格。
   ★ 为什么不是"一个大网格 + 标题当格子"：那样标题要 `grid-column: 1/-1` 硬撑，
     列数一变就得跟着改；而且 sticky 标题会被同一行的卡片挤住。
     分组各自成网格后，列数由容器宽度自适应，与标题互不干扰。 */
.ag-sec + .ag-sec { margin-top: 13px; }
.ag-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(212px, 1fr));
  gap: 6px;
}
/* 网格里卡片更紧凑：宽度由列宽决定，标题和描述都可能折行 */
.ag-grid .ag-i { padding: .5em .68em; }
.ag-grid .ag-i b { font-size: .84rem; }
/* 描述压到两行 —— 高度不齐的卡片会让网格出现"锯齿"，截断比参差好 */
.ag-grid .ag-i span {
  font-size: .71rem; line-height: 1.45;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* 窄屏退回单列：212px 在手机上两列会挤成豆腐块 */
@media (max-width: 560px) {
  .ag-grid { grid-template-columns: 1fr; }
  .modal.wide .ag-list { max-height: 60vh; }
}

/* 档位锁住的助手（2026-10-03）：灰掉 + 写明"需要哪一档"。
   必须和"没全局启用"区分开 —— 后者选中就能用，前者选了也不会生效。
   原来两者都画成「未开启」，学生会选中一个锁住的助手，看到"已选「XX」"，
   然后发现 AI 毫无变化（服务端 promptsFor 会把它静默滤掉）。 */
.ag-lock { font-style: normal; font-weight: 400; font-size: .7rem; color: var(--warn); }
/* 弹窗顶部那段"怎么用"的说明：默认 .dim 太小，这里是关键信息，给足行距 */
.ag-tip { margin: 8px 0 0; line-height: 1.6; }

/* ---- 拖入文件 ---- */
.stream.drop { outline: 2px dashed var(--pri); outline-offset: -10px; background: var(--pri-soft); }

/* 触屏没有 hover：操作条必须常显，否则手机上根本点不到"朗读/翻译/删除" */
@media (hover: none) {
  .msg .acts { opacity: 1; }
  .conv-i .ci-x { opacity: 1; }
}


/* 无障碍兜底：系统里关了动效就全关。
 * 放在最后，靠层叠盖住上面所有 animation —— 比逐个加 media 包裹可靠。 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .caret, .mic.on { animation: none !important; }
  .tk-swim { animation: none !important; }
  .chart .ch-line { stroke-dashoffset: 0 !important; }
  .chart .ch-bar { transform: none !important; }
}

/* ================= 互动课堂（批次4）================= */
.msg-att-ivt {
  display: flex; align-items: center; gap: 10px; width: 100%; max-width: 420px;
  border: 1px solid var(--line-2); border-radius: var(--r-sm); background: var(--panel-2);
  padding: 10px 12px; margin-bottom: 8px;
}
.ivt-ic { font-size: 1.3rem; color: var(--pri); line-height: 1; }
.ivt-txt { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.ivt-txt b { font-weight: 600; }
.ivt-txt .dim { font-size: .78rem; }

/* 右侧面板（默认收起在右侧，打开后滑出） */
.iv-panel {
  position: fixed; top: 0; right: 0; bottom: 0; width: 440px; max-width: 92vw; z-index: 60;
  background: var(--panel); border-left: 1px solid var(--line-2);
  box-shadow: var(--shadow-lg); display: flex; flex-direction: column;
  animation: ivIn var(--dur-3) var(--ease-out) both;
}
.iv-panel[hidden] { display: none; }
.iv-panel.full { width: auto; left: 0; max-width: none; }
/* ★ 批次295：全屏时**仍然要能拖** —— 用户报障就是要"任意调整大小"，
   把分隔条藏起来等于把唯一的调节入口也砍了。全屏时把它贴到屏幕最左边，
   往右拖就是"从全屏拖出来"（见 interactive.js 的 fromEdge）。 */
.iv-panel.full .iv-resizer { left: 0; width: 16px; }
/* ★ 批次294：全屏后让课件吃掉整个高度 —— .ivt-html 平时封顶 min(72vh,680px)，
   1080p 屏上只用到 2/3，「全屏」就名不副实。只作用于面板内（消息里的内联卡片不受影响）。 */
.iv-panel.full .ivt-html { height: calc(100vh - 96px); min-height: 360px; }
/* 全屏按钮的"已全屏"态（ghost 按钮本身没有这个视觉） */
#ivFull.on { color: var(--pri); border-color: var(--pri); }
@keyframes ivIn { from { transform: translateX(24px); opacity: 0; } to { transform: none; opacity: 1; } }
/* ★★ 批次295：分隔条原来只有 6px 宽、还压在面板边缘上，学生**根本点不中**
   （报障原话：「页面大小应该可以任意调整，当前这样就不能调了」）。
   现在命中区加宽到 14px，视觉上仍是 1px 细线 + 悬停时冒出一个抓手。 */
.iv-resizer {
  position: absolute; left: -7px; top: 0; bottom: 0; width: 14px; cursor: col-resize; z-index: 2;
}
.iv-resizer::after {
  content: ""; position: absolute; top: 0; bottom: 0; left: 7px; width: 1px; background: transparent; transition: background var(--dur-1);
}
.iv-resizer:hover::after, body.resizing .iv-resizer::after { background: var(--pri); }
/* 抓手：悬停/拖动时出现在面板边缘正中，告诉学生"这儿能拖" */
.iv-resizer::before {
  content: ""; position: absolute; top: 50%; left: 4px; width: 7px; height: 46px; margin-top: -23px;
  border-radius: 4px; background: var(--line-2); opacity: 0;
  transition: opacity var(--dur-1), background var(--dur-1);
}
.iv-resizer:hover::before, body.resizing .iv-resizer::before { opacity: 1; background: var(--pri); }
/* ★★★ 批次295（关键）：课件在 iframe 里，指针一移进 iframe，父页面就收不到
   pointermove ⇒ 拖拽当场冻住，看着就是"拖不动"。拖动期间把 iframe 让路。 */
body.resizing iframe { pointer-events: none !important; }
.iv-h {
  display: flex; align-items: center; gap: 8px; padding: 10px 12px;
  border-bottom: 1px solid var(--line); font-weight: 600;
}
.iv-h .sp { flex: 1; }
.iv-body { flex: 1; min-height: 0; overflow: auto; padding: 14px; }

/* ---------- 小老师细读面板（批次92）----------
   ★ 复用 `.iv-panel` 那一整套（右侧滑出 / 拖拽调宽 / 全屏 / 动画），这里只补它特有的三块：
     顶部引用块、消息区、底部追问行。**不改 .iv-panel 的任何规则** —— 那是互动课堂的。 */
.tutor-panel { width: 360px; }
.tutor-quote {
  margin: 10px 12px 0; padding: 8px 10px; max-height: 30vh; overflow: auto;
  background: var(--panel-2); border: 1px solid var(--line); border-left: 3px solid var(--pri);
  border-radius: var(--r-sm); color: var(--dim); font-size: .84rem; line-height: 1.55;
  white-space: pre-wrap; word-wrap: break-word; overflow-wrap: anywhere;
}
.tutor-quote[hidden] { display: none; }
.tutor-quote .tq-h { display: block; color: var(--pri); font-weight: 600; margin-bottom: 4px; }
.tutor-body .tm { margin: 0 0 14px; }
.tutor-body .tm-h { color: var(--dim); font-size: .78rem; margin-bottom: 4px; }
.tutor-body .tm.me .tm-md { background: var(--panel-2); border-radius: var(--r-sm); padding: 8px 10px; }
.tutor-loading, .tutor-err { padding: 14px; color: var(--dim); }
.tutor-err { color: var(--bad); }
.tutor-foot {
  border-top: 1px solid var(--line); padding: 8px 12px 10px;
  display: flex; flex-direction: column; gap: 6px;
}
.tutor-note { color: var(--dim); font-size: .76rem; }
.tutor-ask { display: flex; gap: 6px; align-items: center; }
.tutor-ask .btn { flex: 0 0 auto; }
.tutor-inp { flex: 1; min-width: 0; display: flex; }
.tutor-inp .inp { flex: 1; min-width: 0; }
/* ★ 批次92：「语音输入」浮标（批次83 自动挂在每个文字输入框上）是按
   **多行 textarea 的右下角**设计的（`bottom: 8px`）。这条追问框是**单行**的，
   照那个位置摆会顶出框外 2px，而且父元素若是那条 flex 行就正好压住「问」按钮。
   这里把它摆到输入框右端的**垂直居中**处 —— 与站内其它单行输入框同一个观感。 */
.tutor-ask .mic-float { bottom: auto; top: 50%; transform: translateY(-50%); right: 8px; }

/* 批次92：知识点卡上的第二个按钮 —— 「让小老师讲」。
   ★ 与「学这一步」**并列但语义不同**（一个要留痕、要考；一个只是"这段再说一遍"），
     所以配色刻意分开：学习是暖色、小老师是中性描边。 */
.lmap-tutor {
  border: 1px solid var(--line); border-radius: 9px; background: transparent;
  color: var(--dim); padding: 5px 10px; font-size: 12px; cursor: pointer;
}
.lmap-tutor:hover { border-color: var(--pri); color: var(--pri); }
.iv-fig { display: flex; flex-direction: column; gap: 12px; }
.iv-guide { display: flex; flex-direction: column; gap: 8px; padding: 12px; background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-sm); }
.iv-guide-title { color: var(--pri); font-size: .95rem; }
.iv-guide-goal, .iv-guide-prompt, .iv-guide-status, .iv-guide-reflection p { margin: 0; font-size: .86rem; line-height: 1.55; }
.iv-guide-prompt { color: var(--txt); font-weight: 600; }
.iv-guide-input { width: 100%; resize: vertical; padding: 8px 10px; color: var(--txt); background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-sm); font: inherit; }
.iv-guide-input:focus-visible, .iv-guide-action:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }
.iv-guide-status { color: var(--dim); }
.iv-guide-reflection { display: flex; flex-direction: column; gap: 5px; padding-top: 8px; border-top: 1px solid var(--line); }
.iv-guide-reflection[hidden] { display: none; }
.iv-svg { width: 100%; height: auto; background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-sm); touch-action: none; }
.iv-ctrl { display: flex; flex-direction: column; gap: 10px; }
.iv-read { font-size: .9rem; line-height: 1.6; background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 10px 12px; }
.iv-read b { color: var(--pri); }
.iv-read .dim { color: var(--dim); font-size: .82rem; }
.iv-slider { display: flex; flex-direction: column; gap: 4px; font-size: .85rem; }
.iv-slider label { color: var(--dim); }
.iv-slider .iv-pv { color: var(--txt); }
.iv-slider input[type=range] { width: 100%; accent-color: var(--pri); }
.iv-err { padding: 14px; color: var(--bad); background: var(--bad-soft); border-radius: var(--r-sm); }
.iv-share-fig { width: 100%; max-width: 640px; margin: 0 auto; }
.iv-share-fig .iv-svg { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r); }
.share-head h1 { font-size: 1.3rem; margin: 0 0 4px; }
.share-head .dim { margin-bottom: 14px; }


/* ================= 思考过程（reasoning_content） =================
 * 模型在正文前先流出的推理。默认折叠，正在生成时展开跟随。
 * 用 .live 让圆点脉动 —— 这是"它还在想"的唯一可见证据，
 * 也是学生不再以为"没响应"的关键。
 */
.rsn { margin: 2px 0 6px; }
.rsn-hd {
  display: inline-flex; align-items: center; gap: 6px;
  background: none; border: 0; padding: 2px 6px 2px 2px;
  cursor: pointer; font: inherit; font-size: .78rem; color: var(--dim);
  border-radius: var(--r-xs);
}
.rsn-hd:hover { color: var(--txt); background: var(--panel-2); }
.rsn-dot { width: .45rem; height: .45rem; border-radius: 50%; background: var(--pri); opacity: .45; flex: none; }
.rsn.live .rsn-dot { opacity: 1; animation: thinkPulse 1.1s ease-in-out infinite; }
.rsn-ar { font-size: .62rem; line-height: 1; transition: transform var(--dur-2) ease; }
.rsn.open .rsn-ar { transform: rotate(180deg); }
.rsn-bd {
  display: none;
  margin: 4px 0 4px 2px; padding: 9px 11px;
  border-left: 2px solid var(--line-2);
  border-radius: 0 var(--r-xs) var(--r-xs) 0;
  background: var(--panel-2);
  font-size: .78rem; line-height: 1.7; color: var(--dim);
  white-space: pre-wrap; word-break: break-word;
  max-height: 20rem; overflow-y: auto;
}
.rsn.open .rsn-bd { display: block; }
@keyframes thinkPulse { 0%, 100% { opacity: .3; transform: scale(.85); } 50% { opacity: 1; transform: scale(1); } }

/* 裸写公式兜底出来的上下标（render.js 的 2b 分支）。
   浏览器默认的 sup/sub 会把行高顶开，这里压住，并跟正文协调。 */
.msg sup, .msg sub { font-size: .74em; line-height: 0; position: relative; vertical-align: baseline; }
.msg sup { top: -.5em; }
.msg sub { bottom: -.25em; }

/* ══════════════════════════════════════════════════════════════════
   空间门 · 窄屏回退（2026-10-04）
   放在文件**末尾**是刻意的 —— 理由见上面 .land-note 附近那段注释：
   app.css 里有两条「第一个 @media 位置」型断言，在中部插 @media 会让它们假红。
   display:contents 的两栏在 320px 上会把卡片挤没，所以这里必须回退单栏。
   ══════════════════════════════════════════════════════════════════ */
@media (max-width: 1000px) {
  .land {
    grid-template-columns: minmax(0, 1fr);
    align-content: center; gap: clamp(18px, 3.4vh, 30px);
    padding: 84px 20px 84px;
  }
  .land-head { text-align: center; margin: 0 auto; }
  .land-logo { font-size: 1.3rem; }
  .land-h1 { font-size: clamp(30px, 8.6vw, 44px); }
  .land-stance { margin: 14px auto 0; font-size: .88rem; }
  .land-card { padding: 22px 20px 18px; }
  .land-bg::after { background: linear-gradient(180deg, transparent 30%, var(--bg) 92%); }
}
/* 极窄屏：标题别再往下压，卡片才是主角 */
@media (max-width: 380px) {
  .land-h1 { font-size: 27px; }
  .land-stance { font-size: .84rem; }
  .land-tabs { margin-bottom: 16px; }
}
@media (prefers-reduced-motion: reduce) {
  .land-head, .land-card { animation: none; }
  .land-tabs::before { transition: none; }
}

/* ── 画板 · 窄屏回退 ──────────────────────────────────────────────
   ★ 同样放文件末尾：理由与上面那段相同 —— app.css 里有「第一个 @media
     位置」型断言，在中部插 @media 会让它们假红。
   手机上作画区本来就小，工具条再横排会挤成两行把画布顶没，
   所以窄屏把提示文字收掉、按钮收窄，优先保证画布高度。 */
@media (max-width: 640px) {
  .bd-mask { padding: 0; }
  .bd-panel { width: 100%; max-height: 100%; border-radius: 0; border: 0; }
  .bd-tip { display: none; }
  .bd-bar { gap: 5px; padding: 8px 10px; }
  .bd-head { padding: 11px 12px; }
  .bd-foot { padding: 9px 12px; }
  /* 工具按钮收到 26px：仍满足触摸目标下限（约 26–28px 视具体 DPR），
     但能让「工具 + 5 色 + 3 粗细 + 撤销清空」在一行内排得下 */
  .bd-t, .bd-sz { width: 27px; height: 27px; }
  .bd-dot { width: 22px; height: 22px; }
  .bd-empty { font-size: .72rem; }
}

/* ── 技能详情 · 窄屏回退 ────────────────────────────────────────────
   ★ 同样放文件末尾：app.css 里有「第一个 @media 位置」型断言，
      在中部插 @media 会让它们假红。
   手机上 920px 用不上、两栏方法清单会挤成每栏半行，所以：
   面板铺满、清单改单栏、图标缩小。 */
@media (max-width: 640px) {
  .sk-mask { padding: 0; }
  .sk-panel { width: 100%; height: 100%; max-height: 100%; border-radius: 0; border: 0; }
  .sk-head { padding: 12px 14px; }
  .sk-body { padding: 14px; }
  .sk-foot { padding: 10px 14px; }
  .sk-steps { columns: 1; }
  .sk-ico { flex: 0 0 36px; width: 36px; height: 36px; }
  .sk-ico .ski { width: 22px; height: 22px; }
  .sk-name { font-size: 1rem; }
  .sk-foot .btn { flex: 1 1 auto; }
}
/* 中等宽度（手机上横屏、小平板）：两栏会太挤，提前收成一栏 */
/* ── 首页「今天的下一步」· 窄屏回退 ──────────────────────────────
   ★ 放文件末尾：app.css 里有「第一个 @media 位置」型断言，在中部插 @media 会让
     _p25check / _parity22fe / _growthcheck / _encheck / _leaderboardcheck 等假红。
   手机上按钮横排会挤成两行，所以整组竖排、按钮占满一行。 */
@media (max-width: 560px) { .learn-next { padding: 14px; } .learn-next-actions { align-items: stretch; flex-direction: column; } .learn-next-actions .btn { width: 100%; } }
/* ★ .g-practice 的窄屏回退与 .learn-next 同处文件末尾 —— 中部插 @media 会顶掉「第一个响应式 @media」断言（_p25check/_encheck/_leaderboardcheck）。 */
@media (max-width: 560px) { .g-practice { align-items: flex-start; flex-direction: column; } }

@media (max-width: 860px) {
  .sk-steps { columns: 1; }
}

/* ── 沿地图学习 + 掌握检查（批次75）────────────────────────────────
   地图不再只是海报：每个知识点带一个「学这一步」入口 + 一个掌握状态徽章；
   点进去之后，讲解下面会挂一张「掌握检查」卡（1~2 道小题）。
   ★ 配色沿用地图卡的暖色并**写死**（不跟主题变量）—— 深色主题下浅字配浅底会看不见，
     批次73 已经在这上面踩过一次。
   ★ 徽章为空时不占位（`.lmap-st:empty`）：还没学的知识点上不该出现一个空药丸。 */
.lmap-foot { display:flex; align-items:center; gap:8px; margin-top:9px; }
.lmap-st { font-size:11px; font-weight:700; border-radius:999px; padding:2px 8px; }
.lmap-st:empty { display:none; }
.lmap-st.st-learning { color:#7A3E0A; background:#FAEEDA; }
.lmap-st.st-mastered { color:#166534; background:#DCFCE7; }
.lmap-learn { margin-left:auto; border:1px solid #E7D6B4; border-radius:9px; background:#fff; color:#7A3E0A; padding:5px 10px; font-size:12px; cursor:pointer; }
.lmap-learn:hover { border-color:#BA7517; background:#FFF7E6; }
.lmap-learn:disabled { opacity:.6; cursor:default; }

/* ★★ 掌握卡是**文字卡**，但它的容器 `.msg-att-illu` 为图片/SVG 设了 `line-height:0`
   （见文件里 `.msg-att-illu` 那条）⇒ 卡里的标题 `.mst-h b` 与副标题 `.mst-where`
   会继承 0 行高、两行叠在一起（线上真踩：标题被"第1单元·…"压住，批次103 修）。
   同 `.art-plan-wrap` 的思路 —— 文字卡必须把行高重置回来；写在卡**自己**身上
   （而不是只写在外层容器上），换个容器渲染也不会再塌。 */
.art-mastery { border-radius:18px; background:#FFFBF0; border:1px solid #EFE3CE; padding:14px 16px; line-height:1.5; }
.mst-h { display:flex; align-items:center; gap:9px; flex-wrap:wrap; }
.mst-tag { font-size:11px; font-weight:700; color:#fff; background:#BA7517; border-radius:999px; padding:2px 9px; }
.mst-h b { color:#7A3E0A; font-size:15px; overflow-wrap:anywhere; }
.mst-where { margin-top:5px; color:#A8834A; font-size:12px; }
.mst-intro { margin:8px 0 0; color:#4B5563; font-size:13px; line-height:1.6; }
.mst-qs { list-style:none; margin:12px 0 0; padding:0; display:grid; gap:12px; }
.mst-q { background:#fff; border:1px solid #F0E7D6; border-radius:13px; padding:11px 13px; }
.mst-q-t { margin:0; color:#1E293B; font-size:14px; line-height:1.6; overflow-wrap:anywhere; }
.mst-opts { display:grid; gap:7px; margin-top:9px; }
.mst-opt { display:flex; align-items:flex-start; gap:8px; padding:8px 10px; border:1px solid #EDE4D3; border-radius:10px; background:#FFFDF8; cursor:pointer; font-size:13px; color:#334155; line-height:1.5; }
.mst-opt:hover { border-color:#D9A94A; }
.mst-opt input { margin-top:2px; flex:0 0 auto; }
.mst-ta { width:100%; margin-top:9px; box-sizing:border-box; border:1px solid #EDE4D3; border-radius:10px; padding:8px 10px; font:inherit; font-size:13px; line-height:1.55; background:#FFFDF8; color:#334155; resize:vertical; }
.mst-res { margin-top:9px; display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; }
.mst-res:empty { display:none; }
.mst-mark { font-size:11px; font-weight:700; border-radius:999px; padding:2px 9px; flex:0 0 auto; }
.mst-mark.ok { color:#166534; background:#DCFCE7; }
/* 批次170：掌握检查的「错」与全站口径对齐 —— 练习卷 .pc-opt.wrong / .pc-fb.bad
   一贯走 --bad 红；这里原来写死成橙色，且不随暗色主题变（令牌自动适配）。 */
.mst-mark.no { color: var(--bad); background: var(--bad-soft); }
.mst-why { color:var(--dim); font-size:12px; line-height:1.55; }
.mst-hint, .mst-ex { margin:8px 0 0; font-size:12.5px; line-height:1.6; color:#4B5563; background:#FDF6E8; border-left:3px solid #EF9F27; border-radius:0 9px 9px 0; padding:8px 11px; }
.mst-ex { background:#F2F7EE; border-left-color:#97C459; }
.mst-hint[hidden], .mst-ex[hidden] { display:none; }
.mst-hint i, .mst-ex i { display:inline-block; margin-right:7px; font-style:normal; font-weight:700; color:#B45309; }
.mst-ex i { color:#3B6D11; }
.mst-foot { display:flex; align-items:center; gap:11px; flex-wrap:wrap; margin-top:13px; }
.mst-submit { border:0; border-radius:10px; background:#BA7517; color:#fff; padding:8px 18px; font-size:13px; font-weight:700; cursor:pointer; }
.mst-submit:hover { background:#9A6212; }
.mst-submit:disabled { opacity:.65; cursor:default; }
.mst-note { color:#A8834A; font-size:12px; line-height:1.5; flex:1 1 200px; }


/* ================= 学习成就与现场激励 ================= */
.ach-wrap { flex: 1; min-height: 0; overflow: auto; padding: 12px 18px 24px; }
.ach-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr)); gap: 10px; margin-bottom: 14px; }
.ach-summary > div { display: flex; flex-direction: column; gap: 5px; padding: 14px 16px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); }
.ach-summary span, .ach-card small { color: var(--dim2); font-size: .78rem; }
.ach-summary b { color: var(--txt); font-size: 1.05rem; }
/* 成就按五类分组（里程碑 / 掌握 / 复习 / 习惯 / 经济）—— 分组标题带"已得/总数"。 */
.ach-group { margin-bottom: 16px; }
.ach-group-title { display: flex; align-items: baseline; gap: 8px; margin: 0 0 9px; color: var(--txt); font-size: .92rem; }
.ach-group-title > span { color: var(--dim2); font-size: .78rem; font-weight: 400; }
.ach-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 270px), 1fr)); gap: 10px; }
.ach-card { display: flex; align-items: flex-start; gap: 12px; min-width: 0; padding: 14px; border: 1px solid var(--line); background: var(--panel); border-radius: var(--r); }
.ach-card.earned { border-color: color-mix(in srgb, var(--ok) 34%, var(--line)); background: color-mix(in srgb, var(--ok-soft) 40%, var(--panel)); }
.ach-card.pending { opacity: .82; }
.ach-icon { display: grid; place-items: center; flex: 0 0 40px; width: 40px; height: 40px; border-radius: 12px; background: var(--panel-2); font-size: 1.35rem; }
.ach-info { min-width: 0; flex: 1; }
.ach-title { display: flex; align-items: center; gap: 7px; color: var(--txt); font-weight: 700; }
.ach-title > span { padding: 2px 6px; border-radius: 999px; background: var(--ok-soft); color: var(--ok); font-size: .68rem; font-weight: 600; }
/* 成就奖励（露珠）。★ 用 `.ach-title .ach-reward`（两个类）压过 `.ach-title > span`（类+元素），
   否则会被"已获得"那枚绿胶囊的样式盖住 —— 奖励与状态要一眼分得开。 */
.ach-title .ach-reward { padding: 2px 7px; border-radius: 999px; background: var(--pri-soft); color: var(--pri-2); font-size: .68rem; font-weight: 600; }
.ach-info p { margin: 5px 0 8px; color: var(--dim); font-size: .82rem; line-height: 1.5; }
.ach-info small { display: block; margin-top: 5px; }
.ach-progress, .mf-progress { height: 5px; overflow: hidden; border-radius: 999px; background: var(--panel-3); }
.ach-progress i, .mf-progress i { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--pri), var(--ok)); transition: width var(--dur-4) ease; }
.ach-pilot { margin-top: 16px; padding: 14px 16px; background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r); }
.ach-pilot h3 { margin: 0 0 9px; color: var(--txt); font-size: .92rem; }
.ach-pulse, .ach-pulse-done { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ach-pulse > div { flex: 1 1 240px; display: flex; flex-direction: column; gap: 4px; }
.ach-pulse b { color: var(--txt); font-size: .86rem; }
.ach-pulse small, .ach-pulse-done span { color: var(--dim2); font-size: .77rem; }
.ach-pulse-done { color: var(--txt); font-size: .84rem; }
.motivation-feedback { margin: 9px 0; padding: 10px 12px; border: 1px solid var(--line); background: var(--panel-2); border-radius: var(--r-sm); color: var(--txt); }
.motivation-feedback.evolved { border-color: color-mix(in srgb, var(--ok) 55%, var(--line)); background: var(--ok-soft); }
.mf-main { display: flex; align-items: center; gap: 9px; }
.mf-pet { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; flex: 0 0 32px; }
.mf-pet svg { width: 30px; height: 30px; }
.mf-pet .pet-ico { width: 30px; height: 30px; }
.mf-main b { display: block; font-size: .85rem; }
.mf-main small { display: block; margin-top: 2px; color: var(--dim2); font-size: .73rem; }
.mf-evolved { margin-left: auto; padding: 3px 8px; border-radius: 999px; background: var(--ok); color: #fff; font-size: .72rem; animation: mfPop var(--dur-4) ease-out both; }
.mf-progress { margin: 8px 0 0 36px; }
.mf-badges { display: flex; flex-wrap: wrap; gap: 5px; margin: 8px 0 0 36px; }
.mf-badge { padding: 3px 7px; border-radius: 999px; background: var(--pri-soft); color: var(--pri-2); font-size: .73rem; }
/* 新徽章带出的露珠奖励（`+N 露珠`）—— 用琥珀色与徽章名分开，一眼看出"这枚给了多少"。 */
.mf-badge-dew { margin-left: 6px; font-style: normal; font-weight: 700; color: #BA7517; }
.mf-tip { margin: 7px 0 0 36px; color: var(--dim); font-size: .77rem; line-height: 1.45; }
/* 批次318：「今天的知识卡成长值拿满了」—— 这条**必须比普通 tip 显眼**。
   普通 tip 是灰小字（"练习已记录"这类顺带说明），而这一条是学生这次"答对了却没加分"
   的唯一解释；画成灰小字，他会当成又一句废话，然后继续刷十张卡。
   只用颜色/边框抬一级，不另起媒体查询（原因见 .mf-session 上面那段关于媒体查询数量的说明）。 */
.mf-tip-cap { color: var(--pri-2); background: var(--pri-soft); border-left: 3px solid var(--pri); border-radius: 0 6px 6px 0; padding: 6px 9px; font-weight: 600; }
/* 连击 + 星级（批次93）：一场练习之内的即时反馈。基准规则写在所有响应式 @media 之前，
   窄屏也不另起媒体查询 —— 它只有一行短文字，flex-wrap 就够（另起 @media 会顶掉
   _p25check / _parity7check 的「第一个 @media 位置」断言，见文件顶部那段说明）。 */
.mf-session { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; margin: 8px 0 0 36px; }
.mf-session .ms-main { padding: 3px 9px; border-radius: 999px; background: var(--pri-soft); color: var(--pri-2); font-size: .76rem; font-weight: 700; letter-spacing: .02em; }
.mf-session small { color: var(--dim2); font-size: .73rem; }
.motivation-feedback.compact { padding: 7px 9px; }
.motivation-feedback.compact .mf-tip { margin-left: 0; }
.motivation-feedback.compact .mf-session { margin-left: 0; }
.motivation-feedback.compact .mf-progress { display: none; }
@keyframes mfPop { 0% { transform: scale(.86); opacity: .4; } 70% { transform: scale(1.06); } 100% { transform: scale(1); opacity: 1; } }

/* ===== 奖励弹窗（批次112）=====
   起因：学生掌握一张卡 / 交一次卷之后，奖励（成长值、露珠、宝箱、徽章）只在
   原地留一行字 —— 该看见的那一下没看见，之后又一直占着位置。
   现在统一成"浮层弹一下、自己走"：**即时看得见，又不留痕**。
   ★ 三处共用同一个组件：掌握卡片 / 选择题答对 / 测评交卷（app.js 的 rewardPop）。
   ★ 位置钉在视口顶部居中：不盖住正在读的内容，也不随滚动跑。
   ★ 基准规则写在第一个响应式 @media **之后**、最后一个 @media **之前**，
     并且**全程不新增 @media** —— _p25check 守"第一个 @media 的位置"、
     _mapcheck 守"最后一个 @media 是地图那条 680px 回退"，中部插一块会同时顶红。
   ★ 颜色一个都不新造，全部走既有 token（--panel / --line-2 / --r-lg /
     --pri-soft / --shadow-lg），换肤换深浅主题自动跟上。 */
.rw-host {
  position: fixed; top: 14px; left: 50%; transform: translateX(-50%);
  z-index: 9600;                    /* 高过 .toast 的 9500：奖励提示不该被普通提示盖住。
                                       ★ 批次32x：.toast 为了盖住 .sea-wrap(9000) 从 120 提到 9500，
                                         这里必须跟着上抬，否则奖励弹窗会被普通提示压住（_b112check E9）。
                                         仍低于 .shot-stage(9999)。 */
  display: flex; flex-direction: column; gap: 8px;
  width: max-content; max-width: min(92vw, 420px);
  pointer-events: none;             /* 空的时候不许挡住底下的点击 */
}
.rw-pop {
  pointer-events: auto;             /* 但弹窗自己要点得到（那个 ✕） */
  position: relative; overflow: hidden;
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; border: 1px solid var(--line-2); border-radius: var(--r-lg);
  background: var(--panel); color: var(--txt); box-shadow: var(--shadow-lg);
  animation: rwIn var(--dur-3) var(--ease-out) both;
}
.rw-pop.good { border-color: color-mix(in srgb, var(--ok) 45%, var(--line-2)); }
.rw-pop.out { animation: rwOut var(--dur-3) var(--ease-in) both; }
.rw-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; flex: 0 0 30px; border-radius: 50%;
  background: var(--pri-soft); color: var(--pri-2); font-size: .95rem; font-weight: 800;
}
.rw-ico svg { width: 28px; height: 28px; }
.rw-ico .pet-ico { width: 28px; height: 28px; }
.rw-body { flex: 1 1 auto; min-width: 0; }
.rw-body b { display: block; font-size: .86rem; font-weight: 800; }
.rw-body span { display: block; margin-top: 2px; color: var(--dim2); font-size: .75rem; line-height: 1.45; }
.rw-x {
  flex: 0 0 auto; width: 26px; height: 26px; padding: 0; border: 0; border-radius: 50%;
  background: transparent; color: var(--dim2); font-size: 1.05rem; line-height: 1; cursor: pointer;
}
.rw-x:hover { background: var(--panel-2); color: var(--txt); }
/* 剩余时间条：贴着底边、随圆角裁掉。宽度从 100% 收到 0，和 JS 那个定时器同长
   （JS 只改 animation-duration，其余在 CSS 里 —— 一处改不动两处）。 */
.rw-bar { position: absolute; left: 0; bottom: 0; height: 2px; background: var(--pri);
  animation: rwBar 4200ms linear both; }
@keyframes rwIn { from { opacity: 0; transform: translateY(-10px) scale(.97); } to { opacity: 1; transform: none; } }
@keyframes rwOut { to { opacity: 0; transform: translateY(-8px) scale(.98); } }
@keyframes rwBar { from { width: 100%; } to { width: 0; } }

/* 班内只展示明确允许公开的奖励摘要；不传成长值、错题和答题详情。 */
.cls-optin, .cls-optin-info { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 10px 0; padding: 12px 14px; background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-sm); }
.cls-optin > div { display: flex; flex-direction: column; gap: 3px; }
.cls-optin b { color: var(--txt); font-size: .84rem; }
.cls-optin span, .cls-optin-info { color: var(--dim2); font-size: .77rem; }
.cls-reward-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 245px), 1fr)); gap: 8px; margin-top: 8px; }
.cls-reward-member { min-width: 0; padding: 10px 12px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-sm); }
.cls-reward-member.me { border-color: var(--pri); }
/* ★★ 批次136：头像 + 名字并排的一行（原来是名字和阶段文字两头对齐）。
   ★ 头像尺寸必须在这里覆盖 —— `petIcon` 出的是 `<img class="pet-ico">`，
     不覆盖就按 app.css 的基准 18px 渲染（基准那条是给「我的」菜单的成长条用的）。
     写法与 `.av-cell .pet-ico` / `.mf-pet .pet-ico` 完全一致（父类 + 后代选择器）。
     44px：既认得出是哪一条鱼，又不把 245px 宽的卡片撑爆。 */
.cls-reward-top { display: flex; align-items: center; gap: 9px; min-width: 0; }
.cls-reward-top .pet-ico { width: 44px; height: 44px; flex: none; }
/* ★★ 批次138：班级格改用 `petAvatarSVG`（鱼 + 穿戴合成 SVG）后的尺寸。
   与上面那条 `.pet-ico` 同一尺寸（44px）—— 换实现不该换视觉重量。
   ⚠️ 这条**不能省**：`petAvatarSVG` 出的是 `<svg width="100%" height="100%">`，
   父容器不给尺寸时会退化成按 viewBox 原始尺寸（几百 px）渲染，直接撑爆格子。
   这与 `.av-cell svg { width: 44px; height: 44px }` 是同一条规矩。
   ★ `flex: none` 同 `.pet-ico`：`.cls-reward-top` 是 flex 行，
   不加它头像会被长名字挤扁。 */
.cls-reward-top .pet-av-svg { width: 44px; height: 44px; flex: none; display: block; }
/* 批次142：petAvatarBitmap 出 <img class="pet-av-bitmap">。下列容器原本只给 svg 定尺寸，<img> 不命中 ⇒ 逐一镜像；漏掉任一条，那个位置的图会按原图 512px 渲染、把布局撑爆。.me-chip/.me-head/.prof-av 的 img 规则已同时命中 svg 与 img，无需单独加。 */
.pet-av-bitmap { display: block; width: 44px; height: 44px; object-fit: contain; }
.av-cell .pet-av-bitmap { width: 44px; height: 44px; }
.mf-pet .pet-av-bitmap { width: 30px; height: 30px; }
.rw-ico .pet-av-bitmap { width: 28px; height: 28px; }
.cls-reward-top .pet-av-bitmap { width: 44px; height: 44px; flex: none; }
.pet-line .pet-av-bitmap { width: 18px; height: 18px; flex: 0 0 18px; }
  /* ★★ 批次143：缸景里的位图鱼（fishScene / tankPv 用 fishBitmapSVG 画）。
     尺寸由 <image> 的 width/height 属性 + fishBox 实算结果给定，
     CSS 这里只保一条规则在（项目纪律：JS 用的 class 必须在 CSS 里有规则）。
     配件不走这条线 —— 位图本身没配件，gearArt 走的是 .tk-gear 那条矢量支线。 */
  .tk-fish-bm { /* noop，尺寸按元素属性 */ }
.cls-reward-name { display: flex; flex-direction: column; justify-content: center; gap: 1px; min-width: 0; }
.cls-reward-name b { overflow: hidden; color: var(--txt); text-overflow: ellipsis; white-space: nowrap; font-size: .83rem; }
.cls-reward-name > span { color: var(--dim2); white-space: nowrap; font-size: .72rem; }
.cls-awards { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 7px; }
.cls-award { padding: 3px 7px; border-radius: 999px; background: var(--pri-soft); color: var(--pri-2); font-size: .7rem; }
.cls-award-empty { color: var(--dim2); font-size: .72rem; }
/* 批次93：分组（联赛）说明条。基准规则写在所有响应式 @media 之前，
   窄屏回退并进文件末尾那个窄屏块（另起媒体查询会顶掉 _mapcheck 的
   「最后一个 @media」断言）。
   ⚠️ 上面这句刻意**不写**窄屏块的完整选择器 —— _unitcheck.cjs 用裸字面量
   统计「全程只有一个窄屏块」，注释里写全了会被算成两块（本项目第 N 次踩）。 */
.cls-league { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; margin: 0 0 10px; padding: 10px 12px; background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-sm); }
.cls-league > b { color: var(--txt); font-size: .82rem; }
.cls-league > span { flex: 1 1 260px; color: var(--dim); font-size: .77rem; line-height: 1.5; }
.cls-league > span b { color: var(--txt); }

.cls-members { display: block; }
.cls-members > .cls-m-h { display: block; margin: 0; }

@media (prefers-reduced-motion: reduce) {
  .mf-evolved, .ach-progress i, .mf-progress i { animation: none; transition: none; }
  /* 批次112：奖励弹窗的进场 / 出场 / 剩余时间条一起关掉。
     ★ 关掉之后**不会有 animationend 事件**，所以 JS 那边收尾必须另有一个定时器
       兜底（见 rewardPop 里的 setTimeout(bye, 400)）—— 只靠 animationend 的话，
       开了"减少动态效果"的同学屏幕上会永远留着一个关不掉的浮层。 */
  .rw-pop, .rw-pop.out, .rw-bar { animation: none; }
  /* 批次R3：消息头朗读开关的音波也一起关掉。
     ★ 并进本块，**不另起媒体查询** —— 本文件有一族断言要求媒体查询总数恒为 21
       （_passcovercheck C4 / _b172acheck C / _unitcheck 等，横跨 10 个套件）。
       批次R3 最初另起了一块，把这族全部顶红；批次217 把整块挪到断点之前只修好了
       lastIndexOf 那部分，总数仍多 1 ⇒ 这里改成并进来。 */
  .tts-toggle .eq i { animation: none; }
  /* 批次289：自动课件「正在生成」的转圈（.iv-pend-dot，批次285 加的）也一起关掉。
     ★ 并进本块、**不另起媒体查询** —— 本文件媒体查询总数钉死 21、末条必须是 680px
       窄屏回退（_textbookscheck K5 / _mapexamcheck E8 一代代在守）。
     ★ 本块位置在 .iv-pend-dot 的基础规则**之前**，同特异性会被后面那句
       `animation: spin .8s linear infinite` 盖回去 ⇒ 双写选择器抬一级特异性，
       与书写顺序无关。 */
  .iv-pend-dot.iv-pend-dot { animation: none; }
}

/* ★ 学习地图卡（批次73）、单元一览卡（批次76）与掌握检查卡（批次75）的窄屏回退
   都放在文件末尾 —— 中部插 @media 会顶掉「第一个响应式 @media」断言。
   ⚠️ 三套回退**并进同一个 @media 块**（而不是各写一个）：_mapcheck.cjs 断言
   「地图那条窄屏回退是文件里**最后一个** @media」，另起一块会把它顶掉。 */
/* ================= 硅基世界护照 · 高亮聚焦软引导（guide.js） =================
   ★ z-index 层级：.mask(80) < .pw-mask(90) < .gp-layer(95)
     引导层要在最上面 —— 它出现时整个界面都该被压暗，包括弹窗。
   ★ 挖洞不用"四块遮罩"，而用打在目标元素上的巨大 box-shadow：
     目标位置随窗口/滚动自适应，零重算（见 guide.js 的 paint()）。
   ★ 只用主题变量，亮/暗两套都要能看。 */
.gp-layer { position: fixed; inset: 0; z-index: 95; pointer-events: none; }
.gp-layer[hidden] { display: none !important; }

/* 挖洞：本身透明，只靠两层 box-shadow —— 外层铺满全屏压暗，内层描一圈高亮边。
   ★ 关键：本元素 **pointer-events: auto** 且铺满全屏（inset:0 的 fill 兄弟）。
     之前它只有 box-shadow 压暗、没有实体拦截层 ⇒ 「其余区域变灰」只是**视觉**上的，
     鼠标照样能点到下面的真按钮（学生乱点就跑题了）。见 .gp-fill。 */
.gp-hole {
  position: fixed; border-radius: 12px;
  box-shadow: 0 0 0 9999px rgba(10, 14, 22, .58), 0 0 0 2px var(--pri), 0 0 0 5px rgba(37, 99, 235, .28);
  transition: left var(--dur-2) var(--ease-out),
              top var(--dur-2) var(--ease-out),
              width var(--dur-2) var(--ease-out),
              height var(--dur-2) var(--ease-out);
  pointer-events: none;
}
.gp-hole[hidden] { display: none !important; }

/* 拦截层：铺满全屏、吃掉所有点击（这正是"其他区域不可点"的实体保证）。
   ★ 为什么不让 .gp-hole 直接吃点击：box-shadow 不产生可点区域，
     挖洞本身必须保持 pointer-events:none，否则它会把**目标元素**也一起挡住。
   ★ 目标元素的点击由 guide.js 的 document 捕获监听兜住（见该文件说明）。 */
.gp-fill { position: fixed; inset: 0; pointer-events: auto; }
.gp-fill[hidden] { display: none !important; }

/* ★ 降级态（guide.js 的 dimShades() 挂 .gp-layer--dim）：找不到目标时
   整屏**只压暗、不吃点击**。为什么必须有这条：
   找不到目标 = 我们没把学生带过去（比如他在对话页想打字，而目标在「学习」视图），
   此时再让 .gp-shade 吃点击（pointer-events:auto）就等于**把他困在原地** ——
   输入框点不动、字也打不了，只能点气泡上的「暂时跳过」（2026-10-15 用户截图报的 bug）。
   压暗只是视觉提示，鼠标必须穿透到下面的真控件。
   ★ 正常高亮（paint()）时**不挂**这个类 ⇒ .gp-shade 仍是 auto，
     「非洞口区域不可点」的实体保证不受影响。 */
.gp-layer--dim .gp-shade { pointer-events: none; }

/* ★ 四块遮罩：上/下/左/右 围出洞口。批次124 的「一块全屏 .gp-fill 把洞口也盖住
   ⇒ 学生看得见高亮框、点下去没反应」在 guide.js 侧已改成拆四块，但**当时漏了这组 CSS**
   —— 没有 position，内联 left/top/width/height 会被忽略，遮罩塌成静态空 div，
   于是「非洞口区域不可点」的保证失效（2026-10-15 补，与 guide.js 的 paintShades() 配套）。
   ★ 为什么 pointer-events 必须 auto：这就是"吃掉非洞口点击"的实体保证；
     洞口内**不放任何元素** ⇒ 目标自己的点击天然穿透到真按钮。 */
.gp-shade { position: fixed; pointer-events: auto; }
.gp-shade[hidden] { display: none !important; }

/* 气泡：引导卡。位置由 JS 算（自动避让目标），只负责长相。 */
.gp-bubble {
  position: fixed; width: min(320px, calc(100vw - 24px)); box-sizing: border-box;
  background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
  padding: 14px 16px; box-shadow: var(--shadow-lg);
  animation: rise var(--dur-3) var(--ease-out) both;
  pointer-events: auto;
}
.gp-bubble[hidden] { display: none !important; }
.gp-tag {
  display: inline-block; margin-bottom: 8px; padding: 2px 8px; border-radius: 999px;
  font-size: .72rem; font-weight: 600; letter-spacing: .02em;
  background: var(--pri-soft); color: var(--pri-2);
}
.gp-title { font-size: .98rem; font-weight: 600; color: var(--txt); margin-bottom: 6px; }
/* ★ 批次151：明确「高亮的那一块就是要点的地方」的样式。
   位置必须紧跟在 .gp-title 之后、.gp-goal 之前——
   读起来是「这关做什么 → 点哪儿 → 目标是什么」。
   别挪到 .gp-goal 之后：那会变成先讲目标再讲点哪。 */
.gp-do {
  margin-bottom: 8px; padding: 7px 10px; border-radius: 8px;
  font-size: .84rem; line-height: 1.55; color: var(--pri-2);
  background: var(--pri-soft); border: 1px solid var(--pri-3);
}
.gp-goal { font-size: .86rem; line-height: 1.6; color: var(--dim); }
.gp-hint { margin-top: 10px; font-size: .8rem; color: var(--dim2); }
.gp-hint summary { cursor: pointer; color: var(--pri-2); }
.gp-hint > div { margin-top: 6px; line-height: 1.55; }
/* ★ 半完成进度行：让学生看见"过关进度"，避免"传完资料引导没前进"的困惑（批次132）。 */
.gp-progress { margin-top: 10px; font-size: .82rem; line-height: 1.6; padding: 8px 10px; border-radius: 8px;
  background: color-mix(in srgb, var(--pri) 10%, transparent); border: 1px solid color-mix(in srgb, var(--pri) 22%, transparent); }
.gp-done { color: var(--ok, #1f9d55); font-weight: 600; }
.gp-miss { color: var(--warn, #c9821a); font-weight: 600; }
.gp-acts { display: flex; justify-content: flex-end; margin-top: 12px; }
.gp-skip {
  background: transparent; border: 1px solid var(--line-2); color: var(--dim2);
  border-radius: 8px; padding: 5px 12px; font-size: .8rem; cursor: pointer;
}
.gp-skip:hover { color: var(--txt); border-color: var(--dim2); }

/* 入门自学线小圆点（挂在「学习」导航上，有未完成的关才亮）。 */
.pass-dot {
  display: inline-block; width: 7px; height: 7px; margin-left: 4px;
  border-radius: 50%; background: var(--pri); cursor: pointer;
  box-shadow: 0 0 0 3px rgba(37, 99, 235, .18);
  vertical-align: middle;
}
.pass-dot[hidden] { display: none !important; }

/* ★ 批次161：硅基世界护照的**一次性路标卡片**（用户报障原话：
   「当时设计了一个硅基世界护照，为什么没有用上？」）。
   ── 为什么是常驻卡片，而不是又一层引导 / 又一个弹窗 ──────────────
   批次161 已经为这个吃过教训：自动开的引导层（`navigate:true` 会把视图切走，
   外加 4 块吃点击的遮罩）把正在点「知识库」的学生一把拽回「学习」，
   报障原话是「这里出卷也做不了 / 测评这里的题目还是填写不了」。
   卡片的约定因此是**三不**：不抢焦点、不吃点击（没有遮罩、没有 position:fixed）、
   不拦路 —— 它就停在「学习」页顶部，学生往下滚照样挑册、点关卡。
   ── 什么时候消失 ──────────────
   ① 点「以后再说」⇒ 记住（localStorage `hl_pass_nudge`），不再出现；
   ② 护照走完（progress.complete）⇒ 自动消失，不留垃圾。
   走完之后顶级「护照」导航会收起来，但「我的」→ 护照 仍在（meTabs 第一条）——
   这就是"引导一次"里的那"一次"：之后随时能回去看，只是不再天天杵着。 */
.pass-nudge {
  display: flex; align-items: center; gap: 10px 14px; flex-wrap: wrap;
  margin: 0 0 14px; padding: 12px 14px;
  border: 1px solid var(--line-2); border-radius: 12px;
  background: linear-gradient(180deg, rgba(37, 99, 235, .07), rgba(37, 99, 235, .02));
}
.pass-nudge[hidden] { display: none !important; }
.pass-nudge-t { font-weight: 650; font-size: .92rem; }
.pass-nudge-s { color: var(--dim2); font-size: .82rem; flex: 1 1 160px; }
.pass-nudge-b { display: flex; gap: 8px; margin-left: auto; }

/* ================= 硅基世界护照 · 关卡页（批次120）=================
   ★ 视觉口径：这一页要让人觉得"这是一件正经的事"，不是又一个设置页 ——
     所以主视觉给得比一般页面重（大数字 + 进度环 + 拿到后的护照卡）。 */
/* ★ 滚动三件套（`flex:1` + `min-height:0` + `overflow-y:auto`）缺一不可：
   父级 `.view` 是 `overflow:hidden` 的 flex 列容器，这一层不滚就**整页滚不动**
   —— 7 关清单在 768px 高的窗口里第 6、7 关直接被截掉（2026-10-15 用户报的
   "这个页面又不能下滑"）。与 `.learn-wrap` / `.shop-wrap` 同一套写法。
   ⚠️ `min-height:0` 不能省：flex 子项默认 `min-height:auto`，会被内容撑高，
   于是 `overflow-y:auto` 形同虚设（见本文件 2728 行记过的同款坑）。 */
.pass-wrap { flex: 1; min-height: 0; overflow-y: auto; padding: 18px 20px 40px; max-width: 860px; }
.pass-loading, .pass-empty { padding: 40px 4px; color: var(--dim2); font-size: .9rem; }

.pass-head { margin-bottom: 18px; }
/* —— 未拿到：进度环 —— */
.pass-ring {
  padding: 20px 22px; border-radius: 16px;
  background: var(--panel); border: 1px solid var(--line);
}
.pass-ring-num { font-size: 2.1rem; font-weight: 700; color: var(--txt); letter-spacing: -.02em; line-height: 1; }
.pass-ring-num span { font-size: 1.05rem; font-weight: 500; color: var(--dim2); margin-left: 4px; }
.pass-ring-bar { margin: 12px 0 8px; height: 8px; border-radius: 999px; background: var(--panel-2); overflow: hidden; }
.pass-ring-bar i { display: block; height: 100%; border-radius: 999px; background: var(--pri); transition: width var(--dur-3) var(--ease-out); }
.pass-ring-s { font-size: .82rem; color: var(--dim2); }

/* —— 已拿到：护照卡（金色描边，一眼看出这是"证"）—— */
.pass-card {
  padding: 22px 24px; border-radius: 16px; position: relative; overflow: hidden;
  background: linear-gradient(135deg, var(--panel) 0%, var(--panel-2) 100%);
  border: 1px solid var(--gold, #c8a54a);
  box-shadow: 0 0 0 4px rgba(200, 165, 74, .12);
}
.pass-card-h { font-size: .78rem; letter-spacing: .16em; color: var(--gold, #c8a54a); font-weight: 600; }
.pass-card-no { margin: 8px 0 6px; font-size: 1.5rem; font-weight: 700; color: var(--txt); letter-spacing: .04em; font-variant-numeric: tabular-nums; }
.pass-card-s { font-size: .82rem; color: var(--dim2); }

/* —— 护照封面（批次135）：放进「我的成就」最前面那个分区。
   与 `.pass-card` 是同一件东西的两种摆法：护照页那张给"刚拿到"看，
   ★ 批次144c：封面「已签发」态已改为图片卡 .pass-img-*（见上方「硅基世界护照卡」块），
     这里的 .pass-cover 只剩未签发（pending）态在用；.pass-cover.issued 等金分支规则
     已无 HTML 引用，留着不动（pending 态同族规则仍在用，别整个删）。
   封面这张给"成就墙"看 —— 所以更宽、更平（横版证件卡），
   暗金底纹 + 纹章 + 编号 + 双线印章一眼认得出是"证"。
   ★ 未签发（pending）用中性灰：让学生看见"差多少"，不是一块空壳。 */
.pass-cover {
  position: relative; overflow: hidden;
  display: flex; align-items: center; gap: 16px;
  padding: 18px 20px; border-radius: 16px;
  background: var(--panel); border: 1px solid var(--line);
}
.pass-cover.issued {
  background: linear-gradient(120deg, color-mix(in srgb, var(--gold, #c8a54a) 12%, var(--panel)) 0%, var(--panel) 58%, color-mix(in srgb, var(--gold, #c8a54a) 8%, var(--panel)) 100%);
  border-color: var(--gold, #c8a54a);
  box-shadow: 0 0 0 4px rgba(200, 165, 74, .12);
}
/* 暗金底纹：斜向细纹，不抢文字（pointer-events 关掉，纯装饰）。 */
.pass-cover.issued::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background-image: repeating-linear-gradient(135deg, rgba(200, 165, 74, .10) 0 2px, transparent 2px 9px);
}
.pass-cover-crest {
  flex: none; width: 52px; height: 52px; border-radius: 14px;
  display: grid; place-items: center; font-size: 1.7rem;
  background: var(--panel-2); border: 1px solid var(--line-2);
}
.pass-cover.issued .pass-cover-crest { background: color-mix(in srgb, var(--gold, #c8a54a) 16%, var(--panel-2)); border-color: var(--gold, #c8a54a); }
.pass-cover-body { flex: 1; min-width: 0; position: relative; }
.pass-cover-h { font-size: .74rem; letter-spacing: .16em; font-weight: 600; color: var(--dim2); }
.pass-cover.issued .pass-cover-h { color: var(--gold, #c8a54a); }
.pass-cover-no { margin: 6px 0 5px; font-size: 1.32rem; font-weight: 700; color: var(--txt); letter-spacing: .05em; font-variant-numeric: tabular-nums; }
.pass-cover.pending .pass-cover-no { color: var(--dim2); letter-spacing: .18em; }
.pass-cover-s { font-size: .79rem; color: var(--dim2); }
.pass-cover-bar { margin: 10px 0 0; height: 6px; border-radius: 999px; background: var(--panel-3); overflow: hidden; max-width: 320px; }
.pass-cover-bar i { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--pri), var(--pri-2)); transition: width var(--dur-3) var(--ease-out); }
/* 双线印章：模式A / 模式B 两条线都走通才算数 —— 摆成两枚小印。 */
.pass-cover-seal { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; margin-top: 9px; font-size: .72rem; color: var(--dim2); }
.pass-cover-seal span {
  padding: 2px 8px; border-radius: 999px; font-weight: 600; font-variant-numeric: tabular-nums;
  background: var(--panel-2); color: var(--dim); border: 1px solid var(--line-2);
}
.pass-cover.issued .pass-cover-seal span { border-color: color-mix(in srgb, var(--gold, #c8a54a) 55%, var(--line)); color: color-mix(in srgb, var(--gold, #c8a54a) 70%, var(--txt)); }
/* 已通过的水印章：只在签发后出现，压在右下角（纯装饰）。 */
.pass-cover-stamp {
  position: absolute; right: 18px; bottom: 10px; pointer-events: none;
  font-size: 1.5rem; font-weight: 800; letter-spacing: .12em;
  color: color-mix(in srgb, var(--gold, #c8a54a) 30%, transparent);
  transform: rotate(-9deg); border: 2px solid color-mix(in srgb, var(--gold, #c8a54a) 30%, transparent);
  border-radius: 8px; padding: 1px 10px;
}

/* —— 双模式对照（这一页最想让学生看懂的东西）—— */
/* ⚠️ 窄屏回退**不在这里另写媒体查询** —— 见文件末尾那个共用块的说明
   （`_mapcheck.cjs` 断言"地图那条窄屏回退是文件里最后一个媒体查询"，
     且它是按字面串数的，注释里连写这两个字都会算进去）。
   本条的窄屏规则已并进末尾那个共用块。 */
.pass-modes { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 12px; }
.pass-mode { padding: 12px 14px; border-radius: 12px; background: var(--panel); border: 1px solid var(--line); }
.pass-mode.on { border-color: var(--pri); background: var(--pri-soft); }
.pass-mode-n { font-size: .84rem; font-weight: 600; color: var(--txt); }
.pass-mode-v { margin: 4px 0 4px; font-size: 1.15rem; font-weight: 700; color: var(--pri-2); font-variant-numeric: tabular-nums; }
.pass-mode-h { font-size: .76rem; line-height: 1.5; color: var(--dim2); }

/* —— 关卡清单 —— */
.pass-list { display: flex; flex-direction: column; gap: 8px; }
.pass-row {
  display: flex; align-items: flex-start; gap: 12px; padding: 13px 15px;
  border-radius: 12px; background: var(--panel); border: 1px solid var(--line);
}
.pass-row.done { border-color: var(--ok, #22a06b); background: var(--panel); }
.pass-row-i {
  flex: none; width: 24px; height: 24px; border-radius: 50%; margin-top: 1px;
  display: flex; align-items: center; justify-content: center;
  font-size: .8rem; font-weight: 700; color: var(--dim2);
  background: var(--panel-2); border: 1px solid var(--line-2);
}
.pass-row.done .pass-row-i { background: var(--ok, #22a06b); border-color: var(--ok, #22a06b); color: #fff; }
.pass-row.unknown .pass-row-i { border-style: dashed; }
.pass-row-b { flex: 1; min-width: 0; }
.pass-row-t { font-size: .9rem; font-weight: 600; color: var(--txt); }
.pass-row-g { margin-top: 3px; font-size: .82rem; line-height: 1.55; color: var(--dim); }
.pass-row-c { margin-top: 4px; font-size: .78rem; line-height: 1.5; color: var(--dim2); font-style: italic; }
.pass-row-w { margin-top: 5px; font-size: .78rem; line-height: 1.5; color: var(--dim2); }
.pass-row.done .pass-row-w { color: var(--ok, #22a06b); }
.pass-row-m { margin-top: 6px; font-size: .72rem; color: var(--dim2); letter-spacing: .02em; }
.pass-row-go { flex: none; align-self: center; }

/* 挖洞已由 .gp-fill 兜住点击；这里只是让"鼠标移到挖洞上"仍是指针感（视觉提示可点）。 */
.gp-hole { cursor: pointer; }


/* ---- 测评答题卷（可打印 / 可手填，报障⑤）---- */
#examSheet { display: none; }
.ex-sh { max-width: 760px; margin: 0 auto; color: #111; font-family: var(--sans, sans-serif); line-height: 1.5; }
.ex-sh-head { display: flex; justify-content: space-between; align-items: flex-end; border-bottom: 2px solid #111; padding-bottom: 8px; gap: 16px; flex-wrap: wrap; }
.ex-sh-title { font-size: 18px; font-weight: 700; }
.ex-sh-fill { display: flex; gap: 18px; font-size: 14px; }
.ex-sh-fill label { display: inline-flex; align-items: center; gap: 4px; }
.ex-sh-fill .ex-blank { display: inline-block; width: 92px; border-bottom: 1px solid #111; }
.ex-sh-tip { font-size: 13px; color: #444; margin: 10px 0 4px; }
.ex-sh-q { margin: 16px 0; page-break-inside: avoid; break-inside: avoid; }
.ex-sh-h { font-size: 15px; margin-bottom: 4px; }
.ex-sh-h .ex-sh-no { font-weight: 700; margin-right: 6px; }
.ex-sh-h .ex-sh-meta { font-weight: 400; color: #555; font-size: 13px; }
.ex-sh-t { font-size: 14px; }
.ex-sh-ans { margin-top: 8px; min-height: 88px; border: 1px solid #aaa; border-radius: 4px; background: repeating-linear-gradient(#fff, #fff 27px, #cdd 28px); }
/* ★ 批次322：选择题的作答格只要写一个字母，不用一整块 88px 的空白（印出来纯浪费纸）。
   ★ 刻意**不另起媒体查询**（app.css 的媒体查询总数钉死 21、最后一个是 680px 窄屏回退，
     追加一个会把「最后一个媒体查询」顶掉 ⇒ 几十条老断言集体假红）。 */
.ex-sh-pick { min-height: 34px; background: none; border-style: dashed; }
/* ---- 测评答题卷：打印时只留这一张（报障⑤）----
   ★★ 刻意不用 print 型媒体查询：app.css 的媒体查询**总数钉死 21**、最后一个是 680px
      窄屏回退（_mapcheck / _passcovercheck / _unitcheck … 一代代套件都在守），追加一个
      print 型查询会把「最后一个媒体查询」顶掉 ⇒ 几十条老断言集体假红。
      改为打印前由 printExamSheet() 给 <body> 挂 `.printing`，afterprint 摘下。
      （顺带比媒体查询 print 更可控：打印预览取消/失败也能摘干净。） */
body.printing #examSheet { display: block !important; }
body.printing > *:not(#examSheet) { display: none !important; }
body.printing .ex-sh { max-width: 100%; }
body.printing .ex-sh-ans { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
/* ★ 批次217 搬家说明：下面这一整块（消息头朗读开关）原来是**追加在文件最末尾**的，
   其中最后一行是 `@media (prefers-reduced-motion: reduce)`。
   而本项目有一条被十个套件依赖的约定：**文件末尾只放响应式断点** ——
   `_mapcheck` / `_maplearn` / `_b109` / `_b116` / `_b122` / `_b206` / `_home114` /
   `_kbproj88` / `_aiposter` / `_assessui` 都用 `css.lastIndexOf("@media")` 定位
   「地图那条窄屏回退」并断言它含 `.lmap-grid`。
   把非响应式的 `prefers-reduced-motion` 挂在最末，等于把那 10 条断言全部顶红
   （实测各红 1 条，与地图代码毫无关系）。
   ⇒ 整块平移到响应式断点**之前**。块内顺序一个字没动，计算样式不变。 */
/* 批次R3：消息头朗读开关（「小涌」旁的喇叭）+ 朗读中的音波动画 */
.tts-toggle { display:inline-flex; align-items:center; gap:2px; background:none; border:0;
  padding:2px 5px; margin-left:5px; border-radius:8px; cursor:pointer;
  color:var(--dim2,#8a9099); vertical-align:middle; line-height:0; }
.tts-toggle:hover { background:var(--panel-2,#f2f4f7); color:var(--text,#25292e); }
.tts-toggle svg { display:block; }
.tts-toggle .ic-off { display:none; }
body.hl-mute .tts-toggle { color:var(--dim3,#b8bec6); }
body.hl-mute .tts-toggle .ic-on { display:none; }
body.hl-mute .tts-toggle .ic-off { display:block; }
.tts-toggle .eq { display:none; align-items:flex-end; gap:2px; height:13px; }
.tts-toggle .eq i { width:2.5px; height:5px; background:currentColor; border-radius:2px;
  animation:hl-eq .9s ease-in-out infinite; }
.tts-toggle .eq i:nth-child(2) { animation-delay:.15s; }
.tts-toggle .eq i:nth-child(3) { animation-delay:.3s; }
@keyframes hl-eq { 0%,100% { height:4px; } 50% { height:13px; } }
body.hl-speaking .tts-toggle .ic-on { display:none; }
body.hl-speaking:not(.hl-mute) .tts-toggle .eq { display:inline-flex; }
/* 朗读音波的「减少动态效果」降级已并进前面那个既有的同款块（约 5427 行），
   这里不再另起媒体查询 —— 另起会让本文件的媒体查询总数变成 22，顶掉那族断言。 */

/* 批次R5：截图区域框选层（getDisplayMedia 只给整屏/窗口/标签页，区域框选在这一层做） */
.shot-stage { position: fixed; inset: 0; z-index: 9999; background: rgba(10, 14, 22, .78);
  cursor: crosshair; touch-action: none; user-select: none; }
.shot-img { position: absolute; display: block; border-radius: 4px;
  box-shadow: 0 10px 48px rgba(0, 0, 0, .55); -webkit-user-drag: none; }
/* 选区：边框内就是裁出来的部分，外面用超大 box-shadow 压暗（挖洞效果，不额外叠遮罩） */
.shot-sel { position: absolute; border: 2px solid #2b5cff; border-radius: 3px;
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, .45); pointer-events: none; }
.shot-dim { position: absolute; right: 4px; bottom: 4px; font: 12px/1.5 ui-monospace, Menlo, Consolas, monospace;
  color: #fff; background: rgba(0, 0, 0, .72); padding: 1px 6px; border-radius: 5px; white-space: nowrap; }
.shot-tip { position: absolute; left: 50%; top: 18px; transform: translateX(-50%);
  color: #fff; font-size: 13px; background: rgba(0, 0, 0, .55); padding: 6px 14px;
  border-radius: 999px; white-space: nowrap; }
.shot-bar { position: absolute; left: 50%; bottom: 22px; transform: translateX(-50%);
  display: flex; gap: 8px; }

/* ---- 批次227：手写画板改成**浮窗**（用户：「挡住题目了，而且关闭画板就退出卷子」）----
   ★ 为什么插在这里、而且**一条媒体查询都不加**：本文件的媒体查询条数与
     "最后一个媒体查询块"被多套钉死（`_mapcheck` 断言"地图那条窄屏回退是文件里
     最后一个"），往末尾追加带媒体查询的块会一次顶红一片。窄屏靠
     `max-width: 96vw` + `min()` 自适应即可，不需要媒体查询。
   ★ 浮层是 `#mask` 的后代（见 app.js `padEnsure`）⇒ 全屏卷面下也不会消失；
     `position: fixed` 的包含块是 .mask 的 padding box（它带 backdrop-filter），
     与视口基本重合，所以拖动的夹边按视口算就够。 */
.pad-float {
  position: fixed; z-index: 20; left: 0; top: 0;
  width: 460px; max-width: 96vw; max-height: 88vh;
  display: flex; flex-direction: column;
  background: var(--panel); border: 1px solid var(--line-2); border-radius: var(--r-sm);
  box-shadow: 0 18px 48px rgba(8, 16, 30, .3); overflow: hidden;
}
.pad-float[hidden] { display: none; }
.pad-float.dragging { box-shadow: 0 22px 60px rgba(8, 16, 30, .4); }
.pad-float-h {
  display: flex; align-items: center; gap: 8px; flex: 0 0 auto;
  padding: 7px 8px 7px 10px; background: var(--panel-2);
  border-bottom: 1px solid var(--line); cursor: grab;
  touch-action: none; user-select: none;
}
.pad-float.dragging .pad-float-h { cursor: grabbing; }
.pad-float-h b { font-size: .82rem; color: var(--txt); }
.pad-float-h .sp { flex: 1; }
/* 拖动把手（三个小点）：告诉学生"这条能拖"，不然没人会去试 */
.pad-float-dots {
  width: 10px; height: 16px; flex: none; color: var(--dim); opacity: .7;
  background-image: radial-gradient(currentColor 1px, transparent 1.2px);
  background-size: 5px 5px; background-position: 0 1px;
}
.pad-float-btn {
  width: 22px; height: 22px; flex: none; padding: 0; line-height: 20px;
  border: 0; border-radius: 5px; background: transparent;
  color: var(--dim); font-size: 14px; cursor: pointer;
}
.pad-float-btn:hover { background: var(--line); color: var(--txt); }
.pad-float-b { padding: 8px 10px 10px; overflow: auto; min-height: 0; }
.pad-float.min .pad-float-b { display: none; }
/* 题干提示最多占三行多，再长就自己滚 —— 浮层不能被题目撑高 */
.pad-float .pad-tip { max-height: 76px; overflow: auto; }
.pad-float .pad-acts { padding: 8px 24px 0 0; flex-wrap: wrap; }

/* ---- 批次335：手写浮层尺寸（预设 + 右下角拖拽手柄）----
   ★ 同样**不加媒体查询**（见上方批次227 的注释：本文件断点条数/最后一个断点块被套件钉死），
     窄屏靠 `padClampW()` 在 JS 里钳到视口内。 */
.pad-sizes { display: flex; align-items: center; gap: 6px; padding: 0 2px 6px; flex-wrap: wrap; }
.pad-chip {
  padding: 3px 10px; border: 1px solid var(--line-2); border-radius: 999px;
  background: transparent; color: var(--dim); font-size: .72rem; line-height: 1.5;
  cursor: pointer; touch-action: manipulation;
}
.pad-chip:hover { color: var(--txt); border-color: var(--pri); }
/* 右下角缩放手柄：touch-action:none 保证触屏拖它只缩放、不滚页、不触发布局缩放 */
.pad-resize {
  position: absolute; right: 0; bottom: 0; width: 26px; height: 26px; z-index: 3;
  cursor: nwse-resize; touch-action: none;
  border-bottom-right-radius: var(--r-sm);
  background-image:
    linear-gradient(135deg, transparent 46%, var(--line-2) 46%, var(--line-2) 54%, transparent 54%),
    linear-gradient(135deg, transparent 70%, var(--line-2) 70%, var(--line-2) 76%, transparent 76%);
  background-size: 100% 100%, 100% 100%;
  background-repeat: no-repeat;
}
.pad-resize:hover { background-color: var(--line); }
.pad-float.resizing { box-shadow: 0 22px 60px rgba(8, 16, 30, .4); }
.pad-float.resizing .pad-resize { background-color: var(--line); }

@media (max-width: 680px) {
  .lmap-grid { grid-template-columns: 1fr; padding: 12px; gap: 10px; }
  /* ★ 批次302：全班大海的名字在窄屏收小一号（鱼排得密，不收会互相压住）。
     ★★ 为什么并进这一块、而不是自己另起一个断点块：
     ① 文件里所有基准规则必须排在**第一个**断点块之前（`_gearcheck` G4 /
        `_p25check` K13）—— 批次299 就是在这里栽的：为了这条规则在 `.sea-dot`
        后面直接插了一个断点块，把「第一个响应式块」从 3503 行提前到 935 行，
        G4 与 K13 同时假红。
     ② 这一块必须是文件里**最后一个**断点块（`_b111check` C2 / `_b112check` E4 /
        `_b116check` / `_b122check` 都断言「最后一个仍是地图那条 680px」）
        —— 追加到文件末尾同样会把它顶掉。
     ③ ★ 注释里也**不许**出现 `@` + `media` 这个字面量：`_kbproj88` / `_aiposter` /
        `_assessui` 是用 `lastIndexOf` 找最后一个断点块的，注释里提一句就会把锚点
        顶到注释上（批次302 第一版就是这么假红的）。
     ⇒ 三条约束同时成立的位置只有一个：**并进来**（与批次104c 的 `.as-score` 同法）。
     断点因此从 640 变成 680：大海是全屏浮层，680 以下名字本来就该缩，无害。 */
  .sea-nm { font-size: 12px; }
  /* ★ 批次104c：学情测评的窄屏回退 —— **必须并进上面这个 680px 块**
     （_mapcheck 断言「地图那条窄屏回退是文件里最后一个」，另起一块会把它顶掉）。 */
  .as-score { flex-direction: column; align-items: flex-start; gap: 10px; }
  .art-lmap .lmap-h { flex-wrap: wrap; }
  .art-lmap .lmap-h b { flex: 1 1 60%; }
  .art-lmap .map-save { margin-left: 0; }
  .lmap-g-no { width: 22px; height: 22px; border-radius: 8px; font-size: 12px; }
  .ub-grid { grid-template-columns: 1fr; padding: 12px; gap: 10px; }
  .art-ub .ub-h { flex-wrap: wrap; }
  .art-ub .ub-h b { flex: 1 1 60%; }
  .art-ub .ub-save { margin-left: 0; }
  .ub-pills { gap: 10px; }
  /* 批次75：窄屏下「状态 + 学这一步」换行、交卷按钮占满一行 */
  .lmap-foot { flex-wrap: wrap; }
  .lmap-learn { margin-left: 0; }
  .mst-foot { flex-direction: column; align-items: stretch; }
  .mst-submit { width: 100%; }
  /* 批次78：成就页 / 班级奖励的窄屏回退 —— 并进本块（另起媒体查询会顶掉 _mapcheck 断言） */
  .ach-wrap { padding: 10px 12px 18px; }
  .ach-summary { grid-template-columns: 1fr; gap: 7px; }
  .ach-summary > div { padding: 10px 12px; }
  .cls-optin { align-items: flex-start; flex-direction: column; }
  .cls-optin .btn { width: 100%; }
  .cls-reward-name { align-items: flex-start; flex-direction: column; gap: 2px; }
  /* 批次93：分组说明条在窄屏改成竖排（并进本块；另起媒体查询会顶掉 _mapcheck 断言） */
  .cls-league { flex-direction: column; align-items: stretch; }
  .cls-league .btn { width: 100%; }
  /* 批次87：模式说明卡的窄屏回退 —— 建议提问改成整行堆叠（并进本块；另起媒体查询会顶掉 _mapcheck 断言） */
  .mode-card { margin: 0 12px 8px; }
  .mode-card .mc-asks { flex-direction: column; align-items: stretch; }
  .mode-card .mc-ask { width: 100%; }
  /* 批次92：小老师面板的窄屏回退 —— 并进本块（另起媒体查询会顶掉 _mapcheck 断言）。
     窄屏下 360px 的侧栏挤得没法读，直接铺满（与互动课堂的「全屏」同一个思路）。 */
  .tutor-panel { width: auto; left: 0; max-width: none; }
  .tutor-quote { max-height: 22vh; }
  .lmap-tutor { margin-left: 0; }
  /* 批次93（连胜）：补卡按钮在窄屏占满一行（并进本块；另起媒体查询会顶掉 _mapcheck 断言） */
  .streak-line .st-fix { margin-left: 0; width: 100%; }
  /* 批次96：公共教材卡片在窄屏改成"封面 + 文字"两行、按钮整行（并进本块；
     另起媒体查询会顶掉 _mapcheck 断言）。 */
  .tb-card { flex-wrap: wrap; }
  .tb-info { flex: 1 1 60%; }
  .tb-card .btn { width: 100%; }
  .tb-note { display: block; width: 100%; }
  /* 批次114：首页「学习起点」的窄屏回退 —— 并进本块（另起媒体查询会顶掉 _mapcheck 断言）。
     ★ 到期清单改成两行：知识名可能很长，与"到期状态"并排会把状态挤没（学生就看不到"该练了"）。
     ★ 主按钮占满一行 —— 手指点的东西不能只有半个拇指宽（用户第 6 条）。 */
  .hs-due-i { flex-direction: column; align-items: flex-start; gap: 3px; }
  .hs-act .btn { width: 100%; }
  .hm-tools { gap: 6px; }
  .hm-t { font-size: .78rem; }
  /* 批次125：优化对比栏窄屏回退 —— 并进本块（另起媒体查询会顶掉 _mapcheck 断言） */
  .polish-bar { margin: 0 8px 8px; }
  .polish-bar .pb-acts .btn { flex: 1; }
  /* 批次120（硅基世界护照）：双模式对照卡在窄屏改竖排（并进本块；
     另起媒体查询会顶掉地图那条"最后一个媒体查询"的断言 —— 见本块开头说明）。 */
  .pass-modes { grid-template-columns: 1fr; }
  /* 批次135：护照封面在窄屏改成竖排、纹章与文字分两行（并进本块；
     另起媒体查询会顶掉地图那条"最后一个媒体查询"的断言 —— 见本块开头说明）。 */
  .pass-cover { flex-direction: column; align-items: flex-start; gap: 10px; }
  .pass-cover-stamp { display: none; }
  /* 批次130：商店右列退到商品下方；窄屏取消 sticky，避免遮挡。 */
  .shop-wrap { grid-template-columns: 1fr; gap: 16px; }
  .shop-tank { position: static; }
  .home-focus { grid-template-columns: 1fr; gap: 20px; padding: 23px 20px; }
  .home-focus h2 { margin: 12px 0 8px; }
  .home-focus p { margin-bottom: 18px; }
  .home-focus-copy > .btn { width: 100%; }
  .home-focus-route { padding: 16px; gap: 9px; }
  .empty:has(.home-focus:not([hidden])) .hs-review { display: grid; }
  .empty:has(.home-focus:not([hidden])) .hs-review .hs-act { width: 100%; }
  .lp-focus { padding: 20px; }
  .lp-focus .btn { width: 100%; }
  /* 批次315：大海聊天面板的窄屏回退 —— 并进本块（另起断点会顶掉地图那条"最后一个"的断言）。
     ★ 手机上左右并排会让海只剩一条缝（鱼挤成一列、名字互相压住），所以改成
       **上下分**：海在上、聊天在下。聊天给 46vh —— 再高就把海压没了，
       再低就只能看见两条消息（聊天的可用性靠"能看见上下文"，不是靠"能看见一行"）。 */
  .sea-body { flex-direction: column; }
  .sea-stage { min-height: 32vh; }
  .sea-chat { flex: 0 0 auto; height: 46vh; border-left: 0; border-top: 1px solid var(--line); }
  .sea-msg { max-width: 96%; }
}

/* ---------- 互动课件（HTML 沙箱，批次283）----------
   只补这一轨特有的四块：标签条、错误横幅、加载态、iframe 舞台。
   **不动 .iv-panel / .iv-body 的任何规则** —— 那是互动课堂（DSL 轨）共用的。 */
.ivt-html { display: flex; flex-direction: column; gap: 8px; height: min(72vh, 680px); min-height: 420px; }
.ivt-html-bar { display: flex; align-items: center; gap: 8px; flex: none; }
.ivt-html-bar .sp { flex: 1; }
.ivt-html-tag { font-size: .78rem; padding: 2px 8px; border-radius: 999px; background: var(--panel-2); border: 1px solid var(--line); color: var(--pri); }
.ivt-html-err { flex: none; padding: 8px 10px; border-radius: var(--r-sm); background: #FEF2F2; border: 1px solid #FCA5A5; color: var(--bad); font-size: .84rem; line-height: 1.5; }
.ivt-html-stage { position: relative; flex: 1; min-height: 0; border: 1px solid var(--line); border-radius: var(--r-sm); overflow: hidden; background: #fff; }
.ivt-html-loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--dim); font-size: .9rem; background: var(--panel-2); z-index: 2; }
/* ★ iframe 本身不带 allow-same-origin（见 ivtframe.js）；这里只管版式 */
.ivt-frame { display: block; width: 100%; height: 100%; border: 0; background: #fff; }

/* 批次284：互动课件的类型下拉。用**原生 select** —— 自己写弹出菜单要处理点外部关闭、
   滚动定位、键盘可达，必出 bug；原生控件在移动端还是系统选择器，更稳。 */
.ivt-kind {
  height: 26px; padding: 0 4px; font-size: .78rem; line-height: 26px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--panel); color: var(--dim); cursor: pointer;
}
.ivt-kind:focus-visible { outline: 2px solid var(--pri); outline-offset: 1px; }

/* ---------- 「正在生成」占位（批次285）----------
   自动课件要 20–40 秒，学生读完答案那一刻右边就得有个东西在长，否则屏幕半分钟
   毫无变化，他会以为没做。样式独立（不铺 .iv-body 的规则），尺寸也内联兜底。 */
.iv-pend {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; min-height: 240px; height: 100%; padding: 18px; text-align: center;
}
.iv-pend-dot {
  width: 32px; height: 32px; border-radius: 50%;
  border: 3px solid var(--line-2, #E3E7EF); border-top-color: var(--pri, #2B5CFF);
  animation: spin .8s linear infinite;
}
.iv-pend-t { font-size: .92rem; color: var(--txt, #1F2430); line-height: 1.6; }
.iv-pend-s { font-size: .8rem; color: var(--dim, #5C6576); line-height: 1.6; }
