* { box-sizing: border-box; margin: 0; padding: 0; }
  body {
    min-height: 100vh;
    /* ★ PC 端固定布局：窗口窄于 1360px 时出现横向滚动条，
       而不是把三栏结构挤成单列（避免布局随窗口大小变形） */
    min-width: 1360px;
    background: var(--surface-3);
    font-family: system-ui, -apple-system, "Microsoft YaHei", sans-serif;
    color: var(--text-1);
  }
  /* ★★ 这一条是「字体不对」的根因所在（用户反馈：这个字体没有显示全部）★★
     `<input>` / `<textarea>` / `<button>` 这些表单控件**默认不继承**父级字体 ✗
     —— 浏览器给它们一套自己的默认字体，和页面上的字不是一套。

     原来自个儿在 .fd-input / .gb-input / .goal-input 等处零散补过 inherit ✓，
     但**全局这条没有** ✗ → 谁忘了补，谁就用错字体 ✗
     （登录框、顶栏搜索框就是这么漏掉的 ✓）

     ★ 在【全局】补比在每处补好：以后新增输入框不会再漏。
     ★ line-height 没在这里统一 —— 有些输入框（如顶栏那个细搜索框）
       需要更紧的行高，统一设会打乱它们的排版；
       只在需要的中文输入框上单独给 1.5 即可（见 .ipt input）。 */
  input, textarea, select, button {
    font-family: inherit;
  }
  input { width: 100%; height: 42px; padding: 0 12px; border: 1px solid var(--border-2); border-radius: var(--r-sm); font-size: 16px; outline: none; transition: border-color .15s; }
  input:focus { border-color: var(--brand); }
  .loading { text-align: center; padding: 60px; color: var(--text-4); font-size: 16px; }
  .notice-box { background: var(--warning-note-bg); border: 1px solid var(--warning-note-bd); border-radius: var(--r-sm); padding: 14px 18px; font-size: 15px; color: var(--warning-note-tx); margin-bottom: 16px; line-height: 1.7; }
  .empty { text-align: center; padding: 60px 20px; color: var(--text-4); font-size: 16px; }
  .placeholder { background: var(--surface-1); border-radius: var(--r-md); padding: 60px; text-align: center; color: var(--text-4); font-size: 16px; box-shadow: 0 2px 12px rgba(0,0,0,.04); }
  .sec-title { font-size: 18px; font-weight: 600; margin: 26px 0 14px; display: flex; align-items: center; gap: 8px; }
  .sec-title .badge { font-size: 14px; font-weight: 400; color: var(--text-4); }

  /* ══════ 全局提示条 ══════
     ★ 固定在视口顶部居中，任何页面都能看到 ✓
     ★ white-space: pre-line —— 好几条提示里带了换行（比如网络自检的说明）✓
       不设它的话会挤成一行，读起来很费劲 ✗
     ★ 点击可关（见模板的 @click），也会自动消失（见 app.js 的 tip）✓
     ★ z-index 只在定位元素上生效 ✓ 这里是 fixed，符合条件 ✓ */
  .g-toast {
    position: fixed; top: 22px; left: 50%; transform: translateX(-50%);
    z-index: 9999;
    max-width: min(560px, calc(100vw - 48px));
    padding: 12px 20px; border-radius: 12px;
    background: rgba(35, 42, 55, .95); color: #fff;
    font-size: 14px; line-height: 1.7; white-space: pre-line;
    box-shadow: 0 10px 30px rgba(0, 0, 0, .2);
    cursor: pointer; user-select: none;
  }
  /* ★★★ 这里原来写的是 `.g-toast.err` ✗ 踩了个坑 ★★★
       `.err` 是【全站早就有的通用类】（第 390 行：`.err { background: danger-bg; color: danger }`）✓
       而 `.g-toast` 和 `.err` 的【特异性相同】（都是 0,1,0）✗
       → 后出现的 `.err` 赢了 ✓ → 提示条的字被染成**红色** ✗
       → 红字压红底 = **文字看不见** ✗✗（用户截图就是这个）
       ★ 教训：**不要借用通用类名当自己的修饰符** ✗
         它属于谁、带什么样式，你并不知道 ✓
       ★ 现在改用专属名 `g-toast-err` ✓ 特异性 0,2,0 ✓ 谁也盖不住 ✓
         并且**把字色也显式写上** ✓ —— 不依赖"继承来的白色" ✓ */
  .g-toast.g-toast-err {
    background: #B5403C;
    color: #fff;
  }
  .g-toast-enter-active, .g-toast-leave-active { transition: opacity .2s, transform .2s; }
  .g-toast-enter-from, .g-toast-leave-to { opacity: 0; transform: translateX(-50%) translateY(-8px); }

  /* ========== 登录（按 Figma 设计图重做）========== */
  .login-wrap {
    min-height: 100vh;
    background: linear-gradient(180deg, var(--surface-grad-top) 0%, var(--surface-grad-mid) 38%, var(--surface-1) 72%);
    display: flex; align-items: center; justify-content: center;
    padding: 40px 20px;
    position: relative; overflow: hidden;
  }

  /* ================= 登录页入场动效（移植自 App 启动页 splash）=================
     ------------------------------------------------------------------
     动效分五拍，节奏对齐启动页：
       ① 0.00s  呼吸光圈启动（3.4s 循环）
       ② 0.50s  品牌标题逐字升起（每字 +0.12s）
       ③ 0.85s  分隔线从中间向两侧展开
       ④ 1.05s  标语淡入上浮
       ⑤ 1.25s+ 表单控件逐个错峰淡入
     ------------------------------------------------------------------

     ★★★ 设计原则（踩坑后的修正，务必遵守）★★★
     【动画只能锦上添花，绝不能决定内容可不可见】

     错误写法（曾导致登录页整块白屏）：
       .brand h1 { opacity: 0; animation: rise .9s backwards; }
       —— 把「隐藏」写在元素基础样式里。一旦动画因任何原因没跑
          （浏览器禁动画、扩展拦截、动画名笔误、被后面的同名规则覆盖），
          元素就【永久停在 opacity: 0】，内容凭空消失且不报错。

     正确写法（本文件采用）：
       元素基础样式保持【完全可见】（不写 opacity/transform）；
       隐藏态只存在于 @keyframes 的 0% 帧里，由 animation 负责装扮。
       配合 fill-mode: both —— 动画开始前用 0% 帧、结束后用 100% 帧。
       这样即使动画整体失效，元素仍是「本来的样子」（可见），页面不崩。

     ★ 另一处约束：动画的 100% 帧必须回到元素本来样式（无残留 transform）。
       若结束后仍保留 transform，元素会永久成为层叠上下文，
       内部弹层的 z-index 出不来（历史踩坑，见「下拉遮挡」条目）。 */

  /* ① 呼吸光圈：铺在品牌区背后的柔光，模拟启动页的 .halo */
  .brand { position: relative; }
  .brand-halo {
    position: absolute; left: 50%; top: 30%;
    width: 340px; height: 340px; margin: -170px 0 0 -170px;
    border-radius: 50%; pointer-events: none;
    background: radial-gradient(circle, rgba(var(--brand-rgb), .16) 0%, transparent 68%);
    /* 光圈是纯装饰，用 both 让它从第一帧就处于 0% 状态，不闪 */
    animation: brandBreathe 3.4s ease-in-out infinite both;
  }
  @keyframes brandBreathe {
    0%, 100% { transform: scale(.82); opacity: .55; }
    50%      { transform: scale(1.06); opacity: 1; }
  }

  /* ② 品牌标题：作为整体先抬起，内部逐字再依次弹出 */
  .brand h1 {
    /* ★ 基础样式【不写 opacity / transform】—— 保持可见，动画负责入场 */
    font-size: 28px; font-weight: 700; color: var(--text-1);
    display: flex; align-items: center; gap: 10px; letter-spacing: -.2px;
    position: relative;                       /* 压在光圈之上 */
    animation: brandRise .95s cubic-bezier(.22,.9,.28,1) .15s both;
  }
  /* ★ 逐字容器：必须独立一层。
     原因：.brand h1 是 display: flex + gap: 10px，
     若把每个字直接作为 h1 的子节点，每个字都会成为一个 flex item，
     被 gap 拉开 10px —— 字距会散架。
     所以 JS 先把「欢迎使用」整体包进 <span class="tt">，再在它内部逐字拆，
     这样 gap 只作用于「文字块 ↔ 标签胶囊」之间，字与字之间是正常行内排布。 */
  .brand h1 .tt { display: inline-flex; }      /* inline-flex 不产生额外空隙 */
  /* 逐字：JS 把标题拆成 .ch 后再补 animationDelay。
     ★ 同样不写基础 opacity: 0。
       JS 拆分是同步的、必然发生；万一没发生，文字就保持可见（不会消失）。 */
  .brand h1 .ch {
    display: inline-block;
    animation: brandPop .7s cubic-bezier(.2,.95,.3,1) both;
    /* 行楷/楷体等字体下，逐字折叠可能让字贴近，补一点横向呼吸 */
    margin-right: .02em;
  }
  /* 关键帧里写全 from/to —— 隐藏态只在这里出现 */
  @keyframes brandPop {
    from { opacity: 0; transform: translateY(18px) rotate(6deg); }
    to   { opacity: 1; transform: none; }
  }
  @keyframes brandRise {
    from { opacity: 0; transform: translateY(14px) scale(.94); }
    to   { opacity: 1; transform: none; }
  }
  /* 表单区用的轻量版：位移更小，适合控件 */
  @keyframes brandFadeUp {
    from { opacity: 0; transform: translateY(12px); }
    to   { opacity: 1; transform: none; }
  }

  /* ③ 分隔线：从中间向两侧展开 */
  .brand-rule {
    width: 64px; height: 3px; border-radius: 2px; margin-top: 16px;
    background: linear-gradient(90deg, transparent, var(--brand), transparent);
    position: relative;
    transform-origin: center;
    animation: brandGrow .8s ease .85s both;
  }
  @keyframes brandGrow {
    from { transform: scaleX(0); opacity: .4; }
    to   { transform: scaleX(1); opacity: 1; }
  }

  /* ④ 标语淡入上浮 */
  .brand .slogan {
    /* ★ 14 → 17：标语与上面那条分隔线之间稍稍拉开，品牌区三段（标题 / 线 / 标语）
       的节奏更舒展。★ 排版属性只写在这一处，别处不重复声明（见 .login-card 上方警告）。 */
    margin-top: 17px; font-size: 16px; color: var(--text-4);
    position: relative;
    animation: brandFadeUp .85s ease 1.05s both;
  }

  /* ⑤ 表单控件错峰淡入
     ------------------------------------------------------------------
     ★ 选择器用 :nth-child(n)（按「第几个子元素」计数），
       不要用 :nth-of-type —— 后者按【元素类型】计数，
       .ipt 都是 div，而 .tabs 也是 div，
       混在一起会导致 nth-of-type 的序号算错、延迟错位。
       这里直接用 nth-child 显式列出每个位置。 */
  .login-card > *:not(.brand) {
    animation: brandFadeUp .6s cubic-bezier(.22,.9,.28,1) both;
  }
  .login-card > .tabs          { animation-delay: 1.25s; }
  /* ★ 2026-10-20：输入框外面包了一层 .form-area（固定容器）
     ★ 入场动画挂在这一层 → 只跑一次 ✓
       里面 .ipt 换节点（验证码↔密码）时不再重跑 1.3s 延迟的入场动画 ✗
       （那正是"切登录方式时卡片闪一下"的根因）*/
  .login-card > .form-area     { animation-delay: 1.33s; }
  .login-card > .btn-login     { animation-delay: 1.47s; }
  .login-card > .hint          { animation-delay: 1.53s; }
  /* 微信授权登录已移除，此处原有两个延迟档（1.59s / 1.64s）一并取消。
     .agree / .msg 从 1.70s 提到 1.60s，补上那两个空档，
     让入场瀑布仍然「一档接一档」，末尾不会出现停顿。 */
  .login-card > .agree         { animation-delay: 1.60s; }
  .login-card > .msg           { animation-delay: 1.60s; }

  /* ★★ 兜底：动画失效时强制显示全部内容 ★★
     JS 会给 .login-card 加 .anim-off（见 app.js 的 guardLoginAnim），
     触发条件是「动画被禁用 / 元素在动画结束后仍不可见」。
     这类问题一旦发生就是【整页白屏】，必须有自动兜底，不能靠人工发现。 */
  .login-card.anim-off > *,
  .login-card.anim-off .brand h1,
  .login-card.anim-off .brand h1 .ch,
  .login-card.anim-off .brand-rule,
  .login-card.anim-off .brand .tag,
  .login-card.anim-off .brand .tag::before,
  .login-card.anim-off .brand .slogan {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  /* ★ 蓝底是 ::before 在做动画 —— 兜底必须【同时】作用到它，
     否则动画一停，蓝底就永远停在 opacity:0，胶囊会变成"没有底的字" ✗
     （这比空胶囊更糟：文字直接贴在页面上，认不出这是个标签）✓ */
  .login-card.anim-off .brand .tag::before { opacity: 1 !important; }

  /* 尊重系统「减少动态效果」偏好：关掉全部动画，直接显示终态 */
  @media (prefers-reduced-motion: reduce) {
    .brand-halo { animation: none; }
    .login-card > *:not(.brand),
    .brand h1, .brand h1 .ch, .brand-rule, .brand .tag, .brand .slogan,
    .brand .tag::before {
      animation: none !important;
      opacity: 1 !important;
      transform: none !important;
    }
  }

  .login-card {
    /* ★ 2026-10-23 视觉优化：主色渐变收进一个局部变量，三处共用
       （按钮 / Tab 选中 / 品牌胶囊），以后调只改这一行。
       ★ 为什么把亮端从 --brand-light(#4A9BF5) 换掉：
         实测白字在 #4A9BF5 上的对比度只有 2.88，#3482EB 也才 3.79，
         都低于 WCAG AA 的 4.5 —— 这是"按钮看着发飘、字不够实"的真正原因。
         新渐变 #1A5FD0 → #2E80EB 覆盖区间的对比度 4.6~5.9，全部达标。 */
    --btn-grad: linear-gradient(100deg, #1A5FD0 0%, #2E80EB 100%);

    width: 460px; background: var(--surface-1); border-radius: var(--r-xl);
    /* ★ 底部留白从 34 提到 40：原来 46/34 是「上面留得多、下面被挤」，
       看着头重脚轻。40 与 46 更接近，四边观感均衡。 */
    padding: 48px 42px 40px;
    /* ★ 2026-10-23：单层大阴影 → 三层（贴地 + 中景 + 远景）。
       单层阴影只有"糊"没有"浮"；三层叠起来卡片才有厚度。 */
    box-shadow:
      0 1px 2px rgba(16, 29, 54, .05),
      0 12px 32px rgba(var(--brand-rgb), .10),
      0 36px 80px rgba(var(--brand-rgb), .10);
    position: relative;                       /* 光圈定位基准之一 */
    overflow: hidden;                         /* 让呼吸光圈不溢出圆角 */
  }
  /* ★★ 致命坑（已踩过两次，务必记住）★★
     下面这些选择器在「动效区块」（本文件上方）里已有【完整定义】，
     其中包含 opacity / transform / animation。
     如果在这里再写一遍「只含排版属性」的同名规则，CSS 会按属性逐个覆盖 ——
     opacity 与 animation 被清掉，元素就永久停在不可见状态，
     表现为【整块内容凭空消失、页面只剩白卡片】，浏览器不报任何错，极难查。

     所以：.brand h1 / .brand .tag / .brand .slogan 的排版属性
     一律写在「动效区块」那一处，这里【绝不重复声明】。
     —— 历史事故：.brand h1 与 .brand .slogan 都曾在此被覆盖，已合并。 */

  /* ══════ 胶囊：文字先到、蓝底后到 ══════
     ★★ 之前两次修都不对 ★★
       第一次只对齐了延迟、漏了时长 ✗ → 胶囊比字早 0.2 秒到位 ✗
       第二次把延迟/时长/缓动全对齐了 ✗ → 胶囊与第一个字【同时】出现 ✓
       但用户还是说「还是太快」 ✗
       ★ 原因是我一直把胶囊当成「一个整体」来淡入 ✗
         而它的**底色是包在文字外面的** ✗
         —— 胶囊一淡入，文字就被"装进"蓝底里了 ✗
         用户看到的是「先出现一个蓝框，文字才往里填」✗
         那不是他要的「字到了之后它才出现」✗

       ★★ 现在的做法：把【底色】和【文字】拆成两层动画 ★★
         文字（.ch）→ 照常用 brandPop，不变 ✓
         蓝底（::before）→ **延后到文字基本到位之后再淡入** ✓
       → 于是顺序变成：
           0.83s  「乐答题库」逐字浮现（底下是透明的）
           1.35s  蓝底从透明淡入，文字已经在上面了 ✓
       这才是「等字到了，它才出现」✓

       ★ 延迟用 CSS 变量 --tagDelay，由 JS 按「第一个字的延迟」算好注入
         （见 app.js 的 splitLoginTitle）—— 伪元素没法直接写 inline style，
         所以走变量 ✓ */
  .brand .tag {
    position: relative;
    font-size: 15px; font-weight: 500; color: var(--surface-1);
    padding: 5px 13px; border-radius: 999px;
    isolation: isolate;          /* ★ 让 ::before 的 z-index 不跑到父级背后去 */
  }
  /* 胶囊内部逐字后，每个字是 inline-block，需要靠 vertical-align 对齐基线，
     否则中英混排时会偏。这里统一按 middle 对齐，视觉最稳。 */
  .brand .tag .ch { vertical-align: middle; }

  /* 蓝底：绝对定位铺满，文字压在上面 */
  .brand .tag::before {
    content: '';
    position: absolute; inset: 0;
    /* ★★ 2026-10-23 修：必须 -1，否则蓝底把白字盖死 ★★
       现象：胶囊是蓝的、里面没有字（用户截图反馈「白色字体消失」）。
       根因：::before 是【定位元素】、文字（.ch）是【非定位内容】，
            同一个层叠上下文里定位元素绘制在非定位内容之后 ⇒ 蓝底压字。
       .tag 上的 isolation:isolate 保证了 -1 只会沉到【胶囊自己】的底部，
       不会跑到父级背景后面去（这正是当初加 isolation 的原因）。
       为什么「欢迎使用」四个字没事：它们不在 .tag 里，不受这一层影响。 */
    z-index: -1;
    border-radius: 999px;
    background: var(--btn-grad);
    /* ★ 延迟 = --tagDelay（JS 注入）+ 一点点余量，让文字先站好 ✓
       0.42s 的淡入足够快，不会显得拖沓 ✓ */
    animation: tagBg .42s cubic-bezier(.3,.7,.4,1) var(--tagDelay, 1.35s) both;
  }
  @keyframes tagBg {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  /* ★ 更正一条过时备注（第 86 轮）★
     这里原来写着「.brand .tag 已随品牌区改版而移除，登录页标题现在是纯文字」——
     【这条是错的】：index.html 里的登录卡片至今仍是
       <h1>欢迎使用 <span class="tag">乐答题库</span></h1>
     胶囊一直在用（就是标题右侧那个蓝色圆角标）。
     ★ 过时的注释比没有注释更糟：它会让人以为这个类没有使用者。
     已在下面补上它真正需要的入场动画。 */

  .tabs {
    display: flex; gap: 8px; background: var(--surface-2);
    padding: 5px; border-radius: 999px;
    /* ★ 原来 30 / 24 偏紧；品牌区与表单之间需要一点停顿，
       否则「标题 → 切换器 → 输入框」会挤成一坨。 */
    margin: 34px 0 28px;
  }
  .tabs button {
    flex: 1; height: 44px; border: 0; border-radius: 999px;
    background: transparent; color: var(--text-4); font-size: 16px;
    cursor: pointer; font-family: inherit;
    /* ★ 点击动效（2026-10-20 用户要求）：按下轻微缩小、松开回弹 ✓
       只过渡这几个属性，不用 transition: all（会把 font-weight 也做插值）*/
    transition: background .2s, color .2s, box-shadow .2s, transform .12s ease;
  }
  /* 未选中项悬停：给一点底和更深的字，鼠标扫过去能看出"这个能点" */
  .tabs button:not(.on):hover {
    background: rgba(var(--brand-rgb), .07);
    color: var(--text-3);
  }
  .tabs button:active { transform: scale(.955); }
  /* 未选中那个按下时给一点点底色，手感更实 ✓ */
  .tabs button:not(.on):active { background: rgba(var(--brand-rgb), .10); }
  .tabs button.on {
    background: var(--btn-grad);
    color: var(--surface-1); font-weight: 500;
    /* ★ 阴影 .30 → .20：渐变本身已经比原来深了，阴影再重就糊成一团 */
    box-shadow: 0 4px 12px rgba(var(--brand-rgb), .20);
  }

  .ipt {
    display: flex; align-items: center; gap: 12px;
    /* ★ 高度 42 → 46：42px 在 460px 宽的卡片里显得单薄，
       46px 与 999px 胶囊弧度更配，触控区也更接近 WCAG 的 44px 建议。 */
    height: 46px; background: var(--surface-2); border-radius: 999px;
    padding: 0 22px; margin-bottom: 16px;
    /* ★ 2026-10-23：原来 border 是 transparent（完全无边），
       浅灰底在浅白卡片上边界模糊，像"浮着的一块灰"。
       换成极淡的品牌色描边（不是灰色），明暗主题下都协调。 */
    border: 1.5px solid rgba(var(--brand-rgb), .055);
    transition: background .2s, border-color .2s;
  }
  /* 悬停时轻微回应，但别抢 focus 的戏 */
  .ipt:hover:not(:focus-within) {
    background: var(--surface-1);
    border-color: rgba(var(--brand-rgb), .13);
  }
  .ipt:focus-within {
    background: var(--surface-1);
    border-color: var(--brand);
    /* 聚焦时给一圈很淡的外光，视线更容易落到正在输入的框上 */
    box-shadow: 0 0 0 4px rgba(var(--brand-rgb), .08);
  }
  /* ★ 切换登录方式时，新出现的那个输入框轻微淡入（0 延迟、0.18s）
     既给出"确实换了"的反馈，又不会让卡片空一瞬 ✓ */
  .form-area .ipt { animation: iptSwitch .18s ease both; }
  @keyframes iptSwitch {
    from { opacity: .35; transform: translateY(3px); }
    to   { opacity: 1; transform: none; }
  }
  .ipt svg { width: 20px; height: 20px; flex: none; color: var(--text-4); }
  .ipt input {
    flex: 1; min-width: 0; height: auto; border: 0; background: transparent;
    font-size: 16px; color: var(--text-1); outline: none; padding: 0;
    /* ══════ 补两处（用户反馈：这个字体没有显示全部）══════
       ① **font-family: inherit** ——
          `<input>` 默认【不继承】父级字体 ✗，它用浏览器的默认字体。
          站内其它输入框（.fd-input / .gb-input / .goal-input / 按钮）
          都显式写了 inherit ✓，**只有登录这两处漏了** ✗
          → 结果登录框的字形和站内其它地方不是一套。
          ★ 这属于"漏写"而不是"写错"，光看截图很难判断，
            要横着比一遍同类控件才能发现。

       ② **line-height: 1.5** ——
          input 的高度是按【行盒】算的；不写 line-height 时用字体自身的 normal，
          而某些中文字体的 ascent / descent 加起来超过这个值 ✗
          → 字形的上下边缘被切掉，看起来就是"字体没有显示全部" ✗
          ★ 给一个明确的 1.5，让行盒比字形高，从根上杜绝裁切。 */
    font-family: inherit;
    line-height: 1.5;
  }
  .ipt input::placeholder { color: var(--text-placeholder); }
  .ipt .getcode {
    font-size: 16px; color: var(--brand); cursor: pointer;
    white-space: nowrap; flex: none;
  }
  .ipt .getcode.dis { color: var(--decor-1); cursor: not-allowed; }

  .btn-login {
    /* ★ 两处改动，都是「扁」的主要来源：
         ① margin-top 8 → 18：原来主按钮几乎贴着输入框，没有呼吸；
         ② height 42 → 48：主按钮是这张卡里最重要的一步，
            比输入框高一点才压得住（原来是同高，视觉上没有主次）。 */
    width: 100%; height: 48px; margin-top: 18px;
    border: 0; border-radius: 999px;
    background: var(--btn-grad);
    color: var(--surface-1); font-size: 18px; font-weight: 600; font-family: inherit;
    cursor: pointer; transition: .2s;
    /* ★ 双层阴影：近距离的"落地感" + 远距离的"光晕" */
    box-shadow: 0 2px 6px rgba(var(--brand-rgb), .18), 0 10px 24px rgba(var(--brand-rgb), .22);
  }
  .btn-login:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 12px 28px rgba(var(--brand-rgb),.34); }
  .btn-login:disabled { opacity: .55; cursor: not-allowed; transform: none; box-shadow: none; }

  .hint {
    text-align: center; font-size: 14px; color: var(--text-4); margin-top: 20px;
    /* ★ 两种登录方式的提示语一个一行、一个两行 → 卡片高度差 18px 会"跳" ✗
       固定成两行高（实测两行 = 36px），切换时卡片高度不变 ✓ */
    min-height: 36px;
  }

  /* ══════ 微信扫码面板（2026-10-23 新增，当前为模拟占位）══════
     结构见 index.html：.wx-qr-wrap（白底卡片 + 二维码 + 「模拟」角标）→ 两行提示。
     接真实登录时，.wx-qr（img）换成 iframe，尺寸沿用 144×144 即可。 */
  .wx-panel {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    /* 与「两个输入框(124) + 主按钮(66)」接近，切 Tab 时卡片少跳 */
    min-height: 190px;
    padding: 2px 0 0;
  }
  .wx-qr-wrap {
    position: relative;
    padding: 12px;
    /* ★ 固定白色，不跟随主题 —— 二维码底色要是白的才扫得动 */
    background: #fff;
    border-radius: 16px;
    border: 1.5px solid rgba(var(--brand-rgb), .10);
    box-shadow: 0 4px 16px rgba(var(--brand-rgb), .10);
  }
  .wx-qr { display: block; width: 144px; height: 144px; border-radius: 8px; }
  .wx-qr-badge {
    position: absolute; top: -9px; right: -9px;
    font-size: 11px; line-height: 1; padding: 4px 8px; border-radius: 999px;
    background: var(--warning-bg); color: var(--warning); font-weight: 500;
    border: 1px solid rgba(255, 255, 255, .9);
  }
  .wx-tip { margin-top: 16px; font-size: 15px; color: var(--text-2); text-align: center; }
  .wx-tip-sub { margin-top: 5px; font-size: 13px; color: var(--text-4); text-align: center; }

  /* ★ 微信授权登录的样式已整体移除（.wx-divider / .wx-icon 及其子元素）。
     原因：网页版不做微信登录，避免与 APP 端账号体系打架。
     一并取消的还有入场动效里给它们排的两个延迟档（1.59s / 1.64s）。

     移除后 .hint 与 .agree 直接相邻，中间间距由 .agree 的 margin-top: 30px 提供
     （原来是 .wx-divider 的 38px 上边距在撑）。若日后要恢复微信登录：
       ① HTML 加回 .wx-divider（分隔线 + 文字）与 .wx-icon（绿色圆标）；
       ② 这里补回样式：.wx-icon 用 tokens.css 的 --wechat 令牌；
       ③ 入场动效的延迟档按「hint 之后、agree 之前」插两个（每档 +60ms）。 */

  .agree {
    display: flex; align-items: center; justify-content: center; gap: 8px;
    /* ★ 30 → 34：协议行是整张卡的收尾，与提示语之间需要比其它间距更大一点的区隔
       （两者都是小号灰字，间距不够会连成一片）。 */
    margin-top: 34px; font-size: 14px; color: var(--text-4);
    cursor: pointer; user-select: none;
  }
  .agree input { width: 15px; height: 15px; accent-color: var(--brand); cursor: pointer; flex: none; }
  .agree a { color: var(--brand); text-decoration: none; }

  .msg { margin-top: 16px; padding: 12px 16px; border-radius: var(--r-md); font-size: 15px; line-height: 1.7; white-space: pre-wrap; word-break: break-all; }
  .err { background: var(--danger-bg); color: var(--danger); }
  .ok  { background: var(--success-bg); color: var(--success); }

  /* 顶栏 */
  /* 顶栏：半透明白 + 毛玻璃（glassmorphism）。
     底为浅色，故前景用深色系。 */
  .topbar {
    height: 60px;
    background: rgba(255, 255, 255, .72);
    backdrop-filter: saturate(180%) blur(20px);
    -webkit-backdrop-filter: saturate(180%) blur(20px);   /* Safari */
    border-bottom: 1px solid rgba(255, 255, 255, .65);
    display: flex; align-items: center; padding: 0;
    position: sticky; top: 0; z-index: var(--z-topbar);
    box-shadow: 0 1px 12px rgba(22, 35, 58, .06);
  }
  /* 品牌区 + 导航 + 搜索：整块居中
     ★ 顶栏去掉左右 padding —— 否则绝对定位的 left:50% 会以「padding box」为基准，
       导致居中偏移（上次踩过：菜单实际偏右约 100px）。 */
  .topbar-center {
    position: absolute; left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    display: flex; align-items: center; gap: 34px;
    white-space: nowrap;
  }

  /* 品牌区：图标 + 文字
     ★ 参照粉笔 —— logo 要"小而精致"，不抢导航的视觉比重。
       图标 26px / 文字 20px，与导航（18-20px）同级，靠字重区分主次。 */
  .logo {
    display: flex; align-items: center; gap: 8px;
    font-size: 20px; font-weight: 700; color: var(--brand); letter-spacing: .2px;
    flex: none; cursor: pointer; transition: var(--dur-fast);
  }
  .logo:hover { opacity: .8; }
  .logo-img {
    width: 26px; height: 26px; border-radius: 7px;
    display: block; object-fit: contain; flex: none;
  }

  /* 顶栏全局搜索：填满导航右侧的留白区 */
  .topbar-search {
    display: flex; align-items: center; gap: 9px;
    width: 300px; height: 38px; padding: 0 15px;
    background: var(--surface-2);
    border: 1px solid transparent;
    border-radius: var(--r-full);
    transition: var(--dur-fast);
    flex: none;
  }
  /* 聚焦时：白底 + 品牌描边 + 光晕 */
  .topbar-search:focus-within {
    background: var(--surface-1);
    border-color: var(--brand);
    box-shadow: 0 0 0 3px rgba(var(--brand-rgb), .12);
  }
  .topbar-search .ico {
    width: 17px; height: 17px; color: var(--text-4);
    flex: none; cursor: pointer; transition: var(--dur-fast);
  }
  .topbar-search .ico:hover { color: var(--brand); }
  .topbar-search input {
    flex: 1; min-width: 0;
    /* ★ 必须显式覆盖全局 input 的取值（第 11 行 input{border:1px solid;
       border-radius:10px;height:42px}）：
         - height: 42px 会超出容器 38px 的内容高，让输入区上下溢出；
         - border 会画出「内层第二个框」，与外层胶囊叠成双边框。
       只写 border:0 不够稳 —— 焦点态那条 input:focus 规则同特异性且在后面，
       颜色仍可能被重新赋上；这里把 width/style/radius/height 全部显式重置。

       ★★ 踩坑记录（第 95 轮埋的雷，那一轮自己也没发现）：
         当时给这一处补 line-height，用「插到第一条右花括号之前」定位 ——
         而【那条右花括号属于注释，不属于规则】✗
         结果这一行被写进了注释内部，还顺带把注释结构弄坏：
         中间的注释结束符把注释提前关掉，后面几行跑到注释外面成了废字符 ✗
       ★ 症状：搜索框里那行灰字「搜索题库、网课、考试类型」上下被切 ✓
         —— 和 .ipt input 一模一样的毛病，但它一直没被修好 ✗
       ★ 教训：「CSS 写坏了浏览器会静默跳过」这件事非常危险：
         不会报错、不会在控制台留痕，只是那几条声明悄悄不起作用 ✓ */
    border: 0 none;
    border-radius: 0;
    height: 100%;
    padding: 0;                 /* 清掉全局 input 的 padding: 0 12px */
    appearance: none; -webkit-appearance: none;
    background: none; outline: none; box-shadow: none;
    font-size: 15px; font-family: inherit; color: var(--text-1);
    line-height: 1.5;   /* ★ 真正生效的这一行 —— 以前有一行同名的被误写进了上面的注释里 ✗ */
  }
  /* 焦点态也保持无边框（外层的 :focus-within 负责描边） */
  .topbar-search input:focus { border: 0 none; outline: none; box-shadow: none; }
  .topbar-search input::placeholder { color: var(--text-placeholder); }

  /* 导航：项间距用 padding 控制 */
  .menu { display: flex; gap: 2px; }
  .menu span {
    position: relative; overflow: hidden;      /* 承载涟漪层 */
    padding: 9px 20px; font-size: 20px;        /* 无背景块，靠下划线标记 */
    color: var(--text-3); cursor: pointer;
    /* ★ 不对称过渡：文字色变化走常规速度；
       transform（按压回弹）退场走 --dur-slow，否则松手瞬间「弹回来」也算一次闪。 */
    transition: color var(--dur-fast), transform var(--dur-slow) var(--ease-out);
  }
  .menu span:hover { color: var(--brand); }

  /* ① 按压反馈
     ★ 用 transition-duration 只覆盖「进场」那一档（90ms），
       松手后 :active 失效，自动回落到基础态的 --dur-slow 慢慢弹回。
       scale 由 .96 放宽到 .97：按下瞬间的剧烈缩放同样会加剧「闪」的观感。 */
  .menu span:active {
    transform: scale(.97);
    transition-duration: var(--dur-fast), var(--dur-press);
  }

  /* ② 涟漪 / 高亮底色
     ★ border-radius 必须给：这一层是 inset:0 铺满整个 span 的色块，
       不给圆角就是「四方正正」的直角矩形，与全站其它控件的圆角语言不一致。
       10px 与 .ipt(胶囊)、.hcs-card(--r-sm) 观感统一。

     ★★ 时长必须不对称（进场快 / 退场慢），别改回同一条 ★★
       点一次只有 60~120ms。原先进出都是 150ms，按下还没亮完就松手，
       过渡被中途反向 → 亮点一闪而过，用户反馈「闪眼睛」。
       现在：进场 90/170ms（跟手）→ 退场 360ms（柔和淡出，不刺眼）。
       写法：基础态写「退场」时长；:active 里用 transition-duration 覆盖成「进场」。
       —— 因为 :active 的声明只在按住期间生效，松手即回落到退场的时长。 */
  .menu span::after {
    content: ''; position: absolute; inset: 0;
    /* 峰值由 .10 降到 .08：淡出虽已放缓，压一点峰值可再降刺激感，肉眼几乎无差 */
    background: rgba(var(--brand-rgb), .08);
    border-radius: var(--r-sm);                 /* ★ 圆角，别改成直角 */
    opacity: 0; transform: scale(.55);
    transition: opacity var(--dur-slow) var(--ease-out),
                transform var(--dur-slow) var(--ease-out);   /* ← 退场：慢 */
    pointer-events: none;
  }
  .menu span:active::after {
    opacity: 1; transform: scale(1);
    transition-duration: var(--dur-press), 170ms;            /* ← 进场：快 */
    transition-timing-function: var(--ease);
  }

  /* 系统「减少动态效果」偏好：
     关掉缩放 / 位移（那才是「运动」），但保留淡入淡出 ——
     纯透明度变化不属于运动，全部关掉反而会让高亮变成瞬间开关、更刺眼。 */
  @media (prefers-reduced-motion: reduce) {
    .menu span { transition: color var(--dur-fast); }
    .menu span:active { transform: none; }
    .logout { transition: background var(--dur-fast); }
    .logout:active { transform: none; }
    .menu span::after {
      transform: none;
      transition: opacity var(--dur-base) var(--ease-out);
    }
    .menu span:active::after { transform: none; }
  }

  /* ③ 当前项：底部蓝色下划线（参照粉笔），从左往右展开 */
  .menu span::before {
    content: ''; position: absolute;
    left: 20px; right: 20px; bottom: 3px; height: 3px;
    background: var(--brand); border-radius: 2px;
    transform: scaleX(0); transform-origin: left center;
    transition: transform var(--dur-base) cubic-bezier(.4, 0, .2, 1);
  }
  .menu span.active::before { transform: scaleX(1); }
  .menu span.active { color: var(--brand); font-weight: 500; }

  /* ★ 注意：圆角只加在 ::after（高亮层）上，【不要】给 .menu span 本身加。
     原因：
       ① .menu span 是透明无底的，给它加 border-radius 看不出任何效果；
       ② 它带着 overflow: hidden，父级一旦有圆角 + overflow，
          会按【父级圆角】去裁子元素 —— 子层 ::after 的圆角反而被切掉，
          表现为「圆角外套着直角」，比不加还难看。
     所以：让高亮层自己圆角即可，父级保持直角 + overflow 裁切边界。 */

  /* 用户区：现在并入 .topbar-center，作为居中组的一员
     —— 去掉原来的 margin-left:auto（那是为了单独贴右）与 margin-right。 */
  /* ══════ 顶栏右侧用户区：整体压到和搜索框/导航同一档 ══════
     ★★ 用户反馈：「这两个有点太大了，这一栏有点不匹配」★★
     ★ 原来这行的尺寸是：
         顶栏高 60 ✓  导航字 20 ✓  搜索框高 38 ✓
         **头像 40×40** ✗   **退出按钮 42 高 + 18 号字** ✗
       → 40 和 42 是这一行里【最大的两个】✗ 一眼就"出头"✓
       → 而且 42/60 = 70%，在 60px 的栏里几乎顶满 ✓ 看着很挤 ✓
     ★ 现在统一到 34 这一档 ✓ —— 比搜索框(38)略小 ✓
       因为搜索框是输入控件，它需要更大的可点面积 ✓
       头像和按钮是"展示 + 次要动作" ✓ 34 足够 ✓ */
  .user { display: flex; align-items: center; gap: 12px; flex: none; }
  /* ★ 头像 + 昵称包成一组，整组可点 ✓ 打开「我的」面板
       间距放在组内 ✓ 组和「退出登录」之间仍由 .user 的 gap 管 ✓ */
  .user-me {
    display: flex; align-items: center; gap: 12px;
    cursor: pointer; user-select: none;
    padding: 4px 8px; margin: -4px -8px;      /* ★ 扩大可点区域但不改变布局 ✓ */
    border-radius: var(--r-sm);
    transition: background var(--dur-fast);
  }
  .user-me:hover { background: var(--surface-2); }
  .avatar {
    width: 34px; height: 34px; border-radius: 50%;
    overflow: hidden;                 /* ★ 图片超出圆形要裁掉 ✓ */
    flex: none;
    background: var(--brand); color: #fff;
    display: flex; align-items: center; justify-content: center;
    font-size: 14px;            /* 首字，34px 圆里 14 号正好 ✓ */
    flex: none;
  }
  .uname {
    font-size: 16px; color: var(--text-1);
    /* ★ 名字太长时截断，别把退出按钮挤出去 ✓ */
    max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

  /* 右侧按钮：黑色实心（参照粉笔的「登录」按钮）
     ★ 2026-10-19 这条尺寸被调过两轮，最终定在 32 / 14：
       第①轮 用户「太大了，有点突出」
         真正的元凶不是这里的 height，而是 tokens.css 第 242 行的
         「触控目标最小尺寸」兜底：.logout { min-height: var(--tap-min) } = 44px，
         它把 height: 34px 直接顶成 44px ✗ 所以按钮一直比头像(34)还高。
       第②轮 先压到 28 / 13 → 用户「现在太小了」✗ 偏小于头像，读起来像标签
       → 现在 32 高 / 14 号字 / 左右 16：约 88×32 ✓
         比头像(34)矮 2px、比昵称(16 号字)略轻，和搜索框(38)同一档但更收敛 ✓
       ★ min-height 必须显式写：不写就被 tokens.css 的 44px 兜底顶回去（只改 height 无效）。
         32 仍略低于 WCAG 的 44 建议值 —— PC 鼠标场景的取舍，注释保留以备回查。 */
  .logout {
    height: 32px; min-height: 32px; padding: 0 16px; border-radius: var(--r-sm);
    border: 0;
    background: var(--text-1); color: #fff;
    font-size: 14px; font-weight: var(--fw-medium);
    font-family: inherit; cursor: pointer;
    /* 与导航同一套不对称过渡：退场慢，避免松手时「弹回来」也跟着闪 */
    transition: background var(--dur-fast), transform var(--dur-slow) var(--ease-out);
  }
  .logout:hover { background: #000; }
  .logout:active { transform: scale(.97); transition-duration: var(--dur-fast), var(--dur-press); }
  .content { max-width: 1440px; margin: 0 auto; padding: 22px 40px 56px; }

  /* ---- 页面切换动效 ----
     切换导航时 Vue 会重建页面根元素，animation 因此重新播放。
     注意：这里用了 transform，会为 fixed 后代创建包含块 ——
     所以 .content 内部【不能有 position: fixed 元素】，
     .dock（右下悬浮）与 .modal-mask（调整目标弹窗）已移出到 </main> 之后。 */
  @keyframes pageIn {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  .content > * {
    /* ★ fill-mode 用 backwards 而不是 both：
       both 会在动画结束后【保留】to 的 `transform: translateY(0)`，
       而 transform（哪怕值是 0）会创建层叠上下文 ——
       于是 .content 的每个直接子区块各自成层叠上下文，
       区块内的弹出层（.fd-menu / .fr-menu / .hhc-pop）的 z-index
       被关在各自区块里出不来，会被下一个区块盖住（下拉被分类卡遮挡的成因）。
       backwards 只在动画【开始前】应用 from，结束后回归元素本身样式（无 transform），
       入场效果不变，层叠上下文也不再残留。 */
    animation: pageIn var(--dur-base) cubic-bezier(.4, 0, .2, 1) backwards;
  }
  /* 同一页内的多个区块依次浮现（错峰 40ms），比整块一起出现更有层次 */
  .content > *:nth-child(1) { animation-delay: 0ms; }
  .content > *:nth-child(2) { animation-delay: 40ms; }
  .content > *:nth-child(3) { animation-delay: 80ms; }
  .content > *:nth-child(4) { animation-delay: 120ms; }
  .content > *:nth-child(n+5) { animation-delay: 160ms; }
  .dev-bar { background: var(--debug-bg); color: var(--warning-note-tx); font-size: 15px; padding: 9px 32px; text-align: center; border-bottom: 1px solid var(--warning-note-bd); }

  /* ===== 题库页：左侧章节树 + 右侧小节列表（PC 化）===== */
  .ch-wrap {
    display: grid;
    grid-template-columns: 260px minmax(0, 1fr);
    gap: 20px; align-items: start;
  }

  .ch-tree {
    background: var(--surface-1); border-radius: var(--r-md);
    padding: 8px 6px; box-shadow: var(--sh-1);
    max-height: calc(100vh - var(--topbar-h) - 40px);
    overflow-y: auto;
  }
  .ct-head { font-size: 14px; color: var(--text-4); font-weight: var(--fw-medium); padding: 10px 14px 8px; }
  .ct-item {
    display: flex; align-items: center; gap: 10px;
    padding: 11px 14px; border-radius: var(--r-sm);
    cursor: pointer; transition: var(--dur-fast);
  }
  .ct-item:hover { background: var(--brand-50); }
  .ct-item.on { background: var(--brand-50); box-shadow: inset 3px 0 0 var(--brand); }
  .ct-no {
    width: 20px; height: 20px; flex: none; border-radius: var(--r-xs);
    background: var(--surface-2); color: var(--text-4);
    display: flex; align-items: center; justify-content: center;
    font-size: 14px; font-variant-numeric: tabular-nums;
  }
  .ct-item.on .ct-no { background: var(--brand); color: var(--surface-1); }
  .ct-nm {
    flex: 1; min-width: 0; font-size: 15px; color: var(--text-2);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .ct-item.on .ct-nm { color: var(--brand); font-weight: var(--fw-medium); }
  .ct-arw { color: var(--decor-1); font-size: 16px; flex: none; }
  .ct-item.on .ct-arw { color: var(--brand); }

  .ch-body {
    background: var(--surface-1); border-radius: var(--r-md);
    padding: 20px 24px 24px; box-shadow: var(--sh-1);
    min-height: 424px;
  }
  .cb-head {
    display: flex; align-items: center; gap: 12px;
    padding-bottom: 16px; margin-bottom: 18px;
    border-bottom: 1px solid var(--border-1);
  }
  .cb-t { font-size: 16px; font-weight: var(--fw-bold); color: var(--text-1); }
  .cb-c { font-size: 14px; color: var(--text-4); }

  /* ---------- 小节列表（单列行）----------
     ★★ 由「卡片网格」改成「单列行」，三个理由 ★★
       ① 原先是 auto-fill minmax(240px,1fr) 的网格，在 904px 内宽下正好 3 列 ——
          4 个小节就排成 3 + 1，末排只填 33%、空掉两格。改成单列后无论几节都整齐。
       ② 每行仅 54px，8 节也才 432px，比卡片（132px 起）省一半以上纵向空间。
       ③ **进度条拉通整行** —— 各节的推进程度可以横向直接比较，
          哪一节落后一眼就能看出来，这一点卡片网格做不到。

     ★ 题数徽标复用首页学习动态卡在用的 .hc-tag.ok，不再另造一个同款绿色徽标。
     ★ .hc-bar 没有复用：它自带 margin-top: 12px，是给竖向卡片用的；
       横向行里需要的是「拉通剩余宽度」的条，所以新写 .sl-bar。 */
  .sl-list { display: flex; flex-direction: column; gap: 8px; }

  .sl-row {
    display: flex; align-items: center; gap: 14px;
    padding: 12px 16px; min-height: 54px;
    border: 1px solid var(--border-1); border-radius: var(--r-sm);
    cursor: pointer; transition: var(--dur-fast);
  }
  .sl-row:hover { border-color: var(--brand-200); background: var(--brand-50); }

  /* 节号：悬停时点亮，暗示「这一行可点」 */
  .sl-no {
    width: 26px; height: 26px; flex: none; border-radius: var(--r-xs);
    background: var(--surface-2); color: var(--text-4);
    display: flex; align-items: center; justify-content: center;
    font-size: 13px; font-weight: var(--fw-bold); font-variant-numeric: tabular-nums;
    transition: var(--dur-fast);
  }
  .sl-row:hover .sl-no { background: var(--brand); color: #fff; }

  /* ★ 名称列用【固定宽度】而不是 max-width ——
     这样不论章名长短，「题数 / 进度条 / 百分比 / 已完成」都落在同一竖列上，
     各节的进度条才是真正可横向比较的（max-width 会让长名把后面的元素推右）。
     过长的名字走省略号。 */
  .sl-nm {
    flex: none; width: 210px;
    font-size: 15px; font-weight: var(--fw-medium); color: var(--text-1);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

  /* ★ flex: 1 —— 进度条吃掉所有剩余宽度，于是各行的条等长、可横向比较 */
  .sl-bar {
    flex: 1; min-width: 80px; height: 6px; border-radius: var(--r-full);
    background: var(--surface-2); overflow: hidden;
  }
  .sl-bar i {
    display: block; height: 100%; border-radius: var(--r-full);
    background: linear-gradient(90deg, var(--brand-light), var(--brand));
  }

  .sl-pct {
    flex: none; width: 46px; text-align: right;
    font-size: 14px; font-weight: var(--fw-bold); color: var(--brand);
    font-variant-numeric: tabular-nums;
  }
  .sl-done {
    flex: none; width: 118px; text-align: right;
    font-size: 13px; color: var(--text-4); font-variant-numeric: tabular-nums;
  }
  /* 行动入口做成描边小按钮而不是纯文字链接 —— 一行里它是最该被点的地方 */
  .sl-go {
    flex: none; font-size: 13px; color: var(--brand);
    padding: 5px 12px; border-radius: var(--r-sm);
    border: 1px solid var(--brand-200); white-space: nowrap;
    transition: var(--dur-fast);
  }
  .sl-row:hover .sl-go { background: var(--brand); border-color: var(--brand); color: #fff; }

  /* ---------- 三层结构：节 → 小节 ---------- */
  /* 展开了小节的节：整行轻微着色 + 序号点亮，明确「这一行是展开状态」 */
  .sl-row.open { border-color: var(--brand-200); background: var(--brand-50); }
  .sl-row.open .sl-no { background: var(--brand); color: #fff; }

  /* 行末的「N 个小节 ▾」开关（复用 .sl-go 的描边按钮外观） */
  .sl-toggle { display: inline-flex; align-items: center; gap: 5px; }
  .sl-toggle svg { flex: none; transition: transform var(--dur-base) var(--ease); }

  /* ★ 小节行：缩进一级 + 左侧竖线，把「归属关系」画出来。
     ★ 缩进量取 40px = 上级序号徽标宽 26 + 行内 gap 14 ——
       即「让开一个序号位」，所以小节的名称比上级的名称再缩进一点，
       层级一眼可辨；而【同级之间】仍严格对齐，仍然可以横向比进度。
     ★ 序号位放宽到 38px：小节编号形如「1.1 / 3.2」有三位字符，
       沿用上级的 26px 会挤。 */
  .sl-sub {
    margin-left: 40px;
    border-left: 2px solid var(--border-2);
    border-top-left-radius: 0; border-bottom-left-radius: 0;
    background: var(--surface-2);
  }
  .sl-sub:hover { border-left-color: var(--brand-200); background: var(--brand-50); }
  .sl-sub .sl-no { width: 38px; font-size: 12px; }
  /* ★ 158px 是算出来的，不是随手写的：
       上级的「题数」起点 = 16(padding) + 26(序号) + 14(gap) + 210(名称) + 14(gap) = 280
       小节的同一位置   = 40(缩进) + 16 + 38 + 14 + X + 14
       令两者相等 → X = 158
       所以小节的进度条与上级【落在同一条竖线上】，两级之间的推进度也能直接比。 */
  .sl-sub .sl-nm { font-size: 14px; width: 158px; }
  .sl-sub .hc-tag { font-size: 13px; }

  /* ★ 已移除：.cb-grid / .sec-card / .sc-top / .sc-nm / .sc-meta
     这五个类是小节卡片网格专用的，改成单列行后全部失去使用者。 */

  /* ===== 悬浮弹窗（Modal）===== */
  .modal-mask {
    position: fixed; inset: 0; z-index: var(--z-mask);
    background: rgba(22, 35, 58, .45);
    display: flex; align-items: center; justify-content: center;
    padding: 40px 24px;
    animation: maskIn var(--dur-fast) ease both;
  }
  @keyframes maskIn { from { opacity: 0; } to { opacity: 1; } }

  .modal {
    background: var(--surface-1);
    border-radius: var(--r-lg);
    box-shadow: var(--sh-modal);
    width: 100%; max-width: 660px;
    max-height: calc(100vh - 80px);
    display: flex; flex-direction: column;
    animation: modalIn var(--dur-base) cubic-bezier(.34, 1.4, .64, 1) both;
  }
  @keyframes modalIn {
    from { opacity: 0; transform: translateY(18px) scale(.99); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
  }

  .modal-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: 18px 26px; border-bottom: 1px solid var(--border-1); flex: none;
  }
  .mh-t { font-size: 18px; font-weight: var(--fw-bold); color: var(--text-1); }
  .mh-x {
    width: 30px; height: 30px; border-radius: var(--r-sm);
    display: flex; align-items: center; justify-content: center;
    font-size: 22px; line-height: 1; color: var(--text-4);
    cursor: pointer; transition: var(--dur-fast);
  }
  .mh-x:hover { background: var(--surface-2); color: var(--text-1); }

  .modal-body { padding: 18px 22px; overflow-y: auto; flex: 1; }
  .modal-foot {
    display: flex; justify-content: flex-end; gap: 12px;
    padding: 16px 26px; border-top: 1px solid var(--border-1); flex: none;
  }
  .modal-foot .qbtn { height: 40px; padding: 0 26px; font-size: 16px; }

  /* 弹窗内的卡片：去掉独立阴影（已在弹窗容器内），改用描边区分 */
  .goal-modal .goal-card { box-shadow: none; border: 1px solid var(--border-1); }
  .goal-modal .goal-hero { margin-bottom: 12px; }

  /* ===== 首页：推荐网课 ===== */
  .home-course {
    background: var(--surface-1); border-radius: var(--r-md);
    padding: 20px 24px; box-shadow: var(--sh-1);
    margin-bottom: 18px;
  }
  .hcs-head { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
  .hcs-t { font-size: 20px; font-weight: var(--fw-bold); color: var(--text-1); flex: none; }
  .hcs-d { font-size: 16px; color: var(--text-4); flex: 1; min-width: 0; }
  .hcs-more { font-size: 16px; color: var(--brand); cursor: pointer; flex: none; }
  .hcs-more:hover { text-decoration: underline; }

  .hcs-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
  .hcs-card {
    border: 1px solid var(--border-1); border-radius: var(--r-sm);
    overflow: hidden; cursor: pointer; transition: var(--dur-fast);
    display: flex; flex-direction: column;
  }
  .hcs-card:hover { border-color: var(--brand-200); box-shadow: 0 4px 14px rgba(var(--brand-rgb), .10); }
  .hcs-cover {
    height: 98px; flex: none;
    background: linear-gradient(135deg, var(--brand-light), var(--brand));
    display: flex; align-items: center; justify-content: center;
    color: #fff; font-size: 20px; font-weight: var(--fw-medium);
    overflow: hidden;
  }
  .hcs-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .hcs-body { padding: 12px 14px 14px; display: flex; flex-direction: column; flex: 1; }
  .hcs-nm {
    font-size: 18px; font-weight: var(--fw-medium); color: var(--text-1);
    line-height: 1.5; margin-bottom: 8px;
    /* 标题固定两行高度：卡片高度整齐，不受标题长短影响 */
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden; min-height: 39px;
  }
  .hcs-meta { font-size: 16px; color: var(--text-4); }
  .hcs-foot {
    display: flex; align-items: center; justify-content: space-between;
    margin-top: auto; padding-top: 12px;
  }
  .hcs-price { font-size: 20px; font-weight: var(--fw-bold); color: var(--danger); }
  .hcs-go { font-size: 16px; color: var(--brand); }

  /* ===== 通栏功能区：快速检索（题库 + 网课）===== */
  .finder {
    background: linear-gradient(180deg, var(--brand-50) 0%, #F7FBFF 100%);
    border: 1px solid var(--brand-100);
    border-radius: var(--r-lg);
    /* ★ 左右 padding 改为 24px，与分类页其余区块（.cat-l4 也是 24px）一致
       —— 原 28px 比下方面板多缩进 4~6px，两块内容左边缘对不齐。 */
    padding: 24px; margin-bottom: 18px;
    /* ★ position + z-index 必须给：.content > * 上有 pageIn 动画，
       animation-fill-mode: both 会保留 to 的 transform: translateY(0)，
       导致 .finder 与 .cat-wrap 各自成为独立层叠上下文（transform 会创建）。
       两个上下文在父级都是 z-index:auto → 文档流靠后的 .cat-wrap 盖住 .finder，
       .fd-menu 的 z-index:20 被关在 .finder 内部，出不来 → 下拉被分类卡遮住。
       给 .finder 整体抬高一层即可（它内含弹出的下拉，需要盖过下方区块）。 */
    position: relative;
    z-index: var(--z-content);
  }
  .fd-head { display: flex; align-items: center; gap: 9px; margin-bottom: 18px; color: var(--brand); }
  .fd-t { font-size: 18px; font-weight: var(--fw-bold); color: var(--text-1); }
  .fd-d { font-size: 14px; color: var(--text-4); }
  /* 4 列：地区 / 岗位类型 / 关键词 / 按钮。
     ★ 前两列用 minmax(0, 1fr) 而不是 1fr —— 1fr 的默认 min-width 是 auto，
       grid 子项被内容撑宽后会把整行挤变形；minmax(0, …) 才能让省略号生效。 */
  .fd-form {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.5fr) auto;
    gap: 12px; margin-bottom: 16px;
  }
  .fd-input {
    height: 42px; padding: 0 14px; font-family: inherit; font-size: 15px;
    border: 1px solid var(--border-2); border-radius: var(--r-sm);
    background: var(--surface-1); color: var(--text-1); outline: none;
    transition: var(--dur-fast); width: 100%; min-width: 0; box-sizing: border-box;
  }
  .fd-input:focus { border-color: var(--brand); }
  .fd-input::placeholder { color: var(--text-placeholder); }

  /* 自定义下拉（原生 select 的展开面板由系统渲染：直角 + 系统蓝高亮，
     与本设计的圆角 / 配色不搭，故与 .fr-sel 同模式自绘） */
  .fd-sel {
    position: relative;
    height: 42px; padding: 0 38px 0 14px;
    display: flex; align-items: center;
    border: 1px solid var(--border-2); border-radius: var(--r-sm);
    background: var(--surface-1); color: var(--text-1);
    font-size: 15px; cursor: pointer;
    transition: var(--dur-fast); user-select: none;
    width: 100%; min-width: 0; box-sizing: border-box;
  }
  .fd-sel:hover { border-color: var(--brand-300); }
  .fd-sel.open { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-50); }
  .fd-sel > span {
    flex: 1; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .fd-sel > span.ph { color: var(--text-placeholder); }
  .fd-sel .caret {
    position: absolute; right: 14px; width: 12px; height: 12px;
    color: var(--text-4);
    transition: transform var(--dur-base) cubic-bezier(.4, 0, .2, 1);
  }
  .fd-sel:hover .caret { color: var(--brand); }
  .fd-sel.open .caret { transform: rotate(180deg); color: var(--brand); }

  .fd-menu {
    position: absolute; top: calc(100% + 6px); left: 0;
    min-width: 100%; max-height: 288px; overflow-y: auto;
    z-index: var(--z-content);
    background: var(--surface-1);
    border: 1px solid var(--border-1);
    border-radius: var(--r-sm);
    box-shadow: var(--sh-3);
    padding: 6px;
    transform-origin: top left;
    animation: dropIn var(--dur-fast) cubic-bezier(.34, 1.4, .64, 1) both;
  }
  .fd-opt {
    display: flex; align-items: center; gap: 8px;
    padding: 9px 12px; border-radius: var(--r-xs);
    font-size: 15px; color: var(--text-2); cursor: pointer;
    transition: var(--dur-fast); white-space: nowrap;
  }
  .fd-opt:hover { background: var(--brand-50); color: var(--brand); }
  .fd-opt.on { background: var(--brand-50); color: var(--brand); font-weight: var(--fw-medium); }
  .fd-opt em {
    margin-left: auto; padding-left: 10px;
    font-size: 14px; font-style: normal; color: var(--text-4);
  }
  .fd-opt.on em { color: var(--brand); }

  .fd-btn {
    height: 42px; padding: 0 26px; border: 0; border-radius: var(--r-sm);
    background: linear-gradient(90deg, var(--brand-light), var(--brand));
    color: var(--surface-1); font-size: 16px; font-weight: var(--fw-medium);
    font-family: inherit; cursor: pointer; display: inline-flex;
    align-items: center; justify-content: center; gap: 7px;
    transition: var(--dur-fast); white-space: nowrap;
  }
  .fd-btn:hover { opacity: .92; }
  .fd-tags { display: flex; flex-wrap: wrap; gap: 10px; }

  /* 窄屏：检索表单塌成两行（下拉 / 输入框并排，按钮独占一行） */
  @media (max-width: 900px) {
    .fd-form { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
    .fd-btn { grid-column: 1 / -1; }
  }

  /* ★ 首页 Hero 无需窄屏断点：body 已设 min-width: 1360px（见文件头），
     窗口再窄也只出横向滚动条，不会压缩布局 —— 所以右栏恒 1060px、
     轮播图恒 ≈464px、左栏恒需 8 条分类，与窗口宽度无关。 */

  /* ===== 内容区筛选行 ===== */
  .filter-row {
    display: flex; align-items: center; gap: 8px;
    margin-top: 14px; padding-bottom: 14px;
    border-bottom: 1px solid var(--border-1);
  }
  .fr-item {
    padding: 6px 14px; border-radius: var(--r-full); font-size: 14px;
    color: var(--text-3); cursor: pointer; transition: var(--dur-fast);
  }
  .fr-item:hover { background: var(--brand-50); color: var(--brand); }
  .fr-item.on { background: var(--brand-50); color: var(--brand); font-weight: var(--fw-medium); }
  .fr-sp { flex: 1; }
  /* 自定义下拉：原生 select 的下拉面板由系统渲染（直角 + 系统蓝高亮），
     与本设计的圆角 / 配色完全不搭，故改为自定义实现 */
  .fr-sel {
    position: relative;
    height: 34px; padding: 0 32px 0 14px;
    display: inline-flex; align-items: center;
    border: 1px solid var(--border-2); border-radius: var(--r-sm);
    background: var(--surface-1); color: var(--text-3);
    font-size: 14px; cursor: pointer;
    transition: var(--dur-fast); user-select: none;
  }
  .fr-sel:hover { border-color: var(--brand-300); color: var(--brand); }
  .fr-sel.open { border-color: var(--brand); color: var(--brand); }
  .fr-sel .caret {
    position: absolute; right: 11px; width: 12px; height: 12px;
    transition: transform var(--dur-base) cubic-bezier(.4, 0, .2, 1);
  }
  .fr-sel.open .caret { transform: rotate(180deg); }

  .fr-menu {
    position: absolute; top: calc(100% + 6px); right: 0;
    min-width: 124px; z-index: var(--z-content);
    background: var(--surface-1);
    border: 1px solid var(--border-1);
    border-radius: var(--r-sm);
    box-shadow: var(--sh-3);
    padding: 6px;
    transform-origin: top right;
    animation: dropIn var(--dur-fast) cubic-bezier(.34, 1.4, .64, 1) both;
  }
  .fr-opt {
    padding: 8px 12px; border-radius: var(--r-xs);
    font-size: 14px; color: var(--text-2); cursor: pointer;
    transition: var(--dur-fast); white-space: nowrap;
  }
  .fr-opt:hover { background: var(--brand-50); color: var(--brand); }
  .fr-opt.on { background: var(--brand-50); color: var(--brand); font-weight: var(--fw-medium); }

  /* 快捷入口（图标改用 APP 官方彩色图，故不再用 color 控制颜色） */
  .hbs-item .ico {
    width: 22px; height: 22px; flex: none;
    object-fit: contain;
    transition: transform var(--dur-fast);
  }
  .hbs-sep { height: 1px; background: var(--border-1); margin: 8px 0; }

  /* ===== 右下悬浮工具 ===== */
  .dock {
    position: fixed; right: 24px; bottom: 32px; z-index: var(--z-content);
    display: flex; flex-direction: column; gap: 8px;
  }
  .dock-i {
    width: 52px; height: 52px; border-radius: var(--r-md);
    background: var(--surface-1); border: 1px solid var(--border-1);
    box-shadow: var(--sh-2);
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 3px; cursor: pointer; transition: var(--dur-fast);
    color: var(--text-3); font-size: 14px;
  }
  .dock-i svg { width: 18px; height: 18px; }
  .dock-i:hover { border-color: var(--brand); color: var(--brand); box-shadow: var(--sh-3); }

  /* ========== 首页 · 混合栅格（参照粉笔首页：三栏 Hero + 通栏 + 网格+侧栏）========== */
  .home-hero {
    display: grid;
    /* 学习动态已并入下方「学习进度」通栏，故 Hero 改为两栏 */
    /* ★ minmax(0, 1fr) 而不是 minmax(360px, 1fr)：
       grid item 的 min-width 默认是 auto（= min-content），
       当右栏里放了图片这类替换元素时，min-content = 图片固有宽度，
       1fr 会被「顶开」而不再等于剩余空间 → 右栏比 left+gap 之外的应有宽度更宽，
       整个 grid 溢出 .content（实测 .hero-card 溢出父容器 81px、右边缘跑到 1600）。
       min 值写成 0 关掉这条隐含最小值，1fr 才真正等于「剩余空间」。
       360px 那个下限由 min-height / 窄屏横向滚动条兜底，PC 固定布局下不需要。 */
    grid-template-columns: 280px minmax(0, 1fr);
    gap: 20px; margin-bottom: 18px;
    /* ★ align-items: stretch（默认）—— 两栏强制等高。
       右栏 .hero-card 用 aspect-ratio: var(--banner-w)/var(--banner-h) 定高，
       容器比例来自 tokens.css 的令牌（当前 1100:481）。
       内容区宽 1360px 时：右栏 = 1360-280-20 = 1060px → 高 ≈ 464px。
       左栏分类面板由 stretch 被动跟随，无论令牌怎么改都自动等高，
       不再依赖「8 条分类刚好 464px」这个巧合。 */
    align-items: stretch;
  }

  /* 左：考试分类导航 */
  .hh-cats {
    background: var(--surface-1); border-radius: var(--r-md);
    padding: 8px 6px; box-shadow: var(--sh-1);
    /* ★ 与右侧 Banner 等高（.home-hero 是 stretch，这里不要再覆盖 align-self）。
       Banner 高度 = 图片高度（自适应），所以这里的高度是「被动跟随」的：
       stretch 会把左栏拉到与右栏同高，无论那批接口图是什么比例都自动对齐。
       —— 不再依赖「8 条分类 ≈ 464px」这个写死的巧合。

       列表条数由 app.js 的 homeCats 决定（当前取前 8 个一级分类）。
       若某批图很矮、8 条放不下，.hhc-list 内部会压缩（见下），
       保证 head 与底部「查看全部分类」始终可见，不会顶出面板。 */
    display: flex; flex-direction: column;
    min-height: 0;               /* 关键：允许被压缩，否则会反过来顶高整行 */
    overflow: hidden;            /* 图片偏矮、分类放不下时兜底裁切，防止条目顶出面板
                                    ★ 注意：它【会】裁掉绝对定位的子孙。
                                      .hhc-pop 之所以能不受影响，是因为它改用了
                                      position: fixed（见 .hhc-pop 注释）——
                                      当年写「不裁 .hhc-pop」是错的，正是这个错误
                                      导致悬浮窗整整消失了一段时间。 */
    position: relative;          /* 悬浮面板的定位基准 */
    z-index: var(--z-content);
  }
  .hhc-head { font-size: 16px; color: var(--text-3); padding: 12px 14px 8px; font-weight: var(--fw-medium); flex: none; }
  /* 列表区：撑满 head 与 more 之间的空间。
     ★ 不能加 overflow-y: auto —— 会裁掉挂在 .hhc-item 内部、向右侧弹出的
       .hhc-pop 悬浮面板（overflow 会裁剪绝对定位子元素）。
     ★ 也不设 flex: none —— 轮播图改成「图片比例自适应」后高度不再恒定，
       若某批接口图偏矮，8 条分类会放不下。
       这里保持 flex: 1 + min-height: 0，让列表区可被压缩；
       真正的兜底在 .hh-cats 的 overflow: hidden（防顶出面板）。 */
  .hhc-list {
    flex: 1; min-height: 0;
    display: flex; flex-direction: column;
    /* 列表区：撑满 head 与 more 之间的空间。
       ★ 这里没有设 overflow：
         面板 .hhc-pop 现在用的是 position: fixed（不受祖先 overflow 裁剪），
         所以理论上这里可以安全地加 overflow-y: auto 让条目滚动 ——
         但当前仍沿用「由 .hh-cats 的 overflow: hidden 在更外层兜底」的做法
         （banner 图偏矮时会裁掉末尾条目，而不是出现滚动条）。
         若日后想改成滚动，直接给这里加 overflow-y: auto 即可，不会再影响面板。 */
  }
  .hhc-item {
    display: flex; align-items: center; gap: 9px;
    padding: 13px 14px; border-radius: var(--r-sm);
    cursor: pointer; transition: var(--dur-fast);
    position: relative;          /* ★ 面板的定位基准 = 当前这一项 */
    flex: none;                  /* 条目高度不被压缩，压缩体现在「显示几条」 */
  }
  .hhc-item:hover { background: var(--brand-50); }
  .hhc-item .nm { font-size: 16px; color: var(--text-1); flex: none; }
  .hhc-item .ds {
    font-size: 15px; color: var(--text-4); flex: 1; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .hhc-item .arw { color: var(--decor-1); font-size: 18px; flex: none; }
  .hhc-item:hover .arw { color: var(--brand); }
  .hhc-item.on { background: var(--brand-50); }
  .hhc-item.on .arw { color: var(--brand); }

  /* 悬浮面板：显示该分类下的子分类 */
  .hhc-pop {
    /* ★★ 必须用 position: fixed，不能用 absolute ★★
       .hh-cats 上有 overflow: hidden（用途是「banner 图偏矮时，8 条分类不顶出面板」），
       而 overflow 会裁剪绝对定位的子孙 —— 本面板是 left: 100% 向右弹出的，
       整个位于 .hh-cats 盒子【外面】，用 absolute 会被 100% 裁掉，
       表现就是「悬浮窗不见了」（浏览器不报任何错，很难查）。

       fixed 的包含块是视口，【不受任何祖先 overflow 裁剪】；
       同时它在 DOM 里仍是 .hhc-item 的子孙 ——
       所以鼠标从条目移进面板时，.hh-cats 的 mouseleave 不会触发，悬停交互照旧。

       代价：坐标不能再用 left: 100% / top: 0 自动算，必须由 JS 按被悬停条目的
       getBoundingClientRect() 写进 style（见 app.js 的 showCatPop / hoverPos）。
       面板 left 上限 = 分类栏宽 280 + 6 ≈ 286，加 max-width 400 仍远在视口内。 */
    position: fixed;
    /* 宽度：由「最宽的那一行」决定（通常是底部「查看全部」那行），
       上限 400px。
       ★ 不能用 width:max-content —— 那是「收缩宽度」，子元素的 width:100%
         无法回推父宽，导致标签行按自身内容排版、右侧留空。
       fit-content 保留了「贴合内容」的特性，同时允许子元素撑满。 */
    width: fit-content;
    min-width: 240px;
    max-width: 400px;
    z-index: var(--z-content);
    background: var(--surface-1);
    border: 1px solid var(--border-1);
    border-radius: var(--r-md);
    box-shadow: var(--sh-3);
    padding: 18px 20px;
    /* 弹出动效：从左侧淡入 + 位移。
       注意：这里【不用 scale】—— 缩放会在动画期间改变面板内部可用宽度，
       导致文字折行、内容瞬时变高，从而触发 .pop-grid 的滚动条一闪而过。 */
    transform-origin: left center;
    animation: popIn var(--dur-base) cubic-bezier(.34, 1.4, .64, 1) both;
  }
  @keyframes popIn {
    from { opacity: 0; transform: translateX(-10px); }
    to   { opacity: 1; transform: translateX(0); }
  }
  /* 下拉菜单从上方 6px 处落下（popIn 是横向位移，供侧滑面板用，不适用于此） */
  @keyframes dropIn {
    from { opacity: 0; transform: translateY(-6px) scale(.98); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
  }
  /* 面板内的子项依次淡入（错峰 12ms，最多 12 项） */
  .hhc-pop .pop-item {
    animation: popItem 220ms cubic-bezier(.4, 0, .2, 1) both;
  }
  .hhc-pop .pop-item:nth-child(1)  { animation-delay: 30ms; }
  .hhc-pop .pop-item:nth-child(2)  { animation-delay: 42ms; }
  .hhc-pop .pop-item:nth-child(3)  { animation-delay: 54ms; }
  .hhc-pop .pop-item:nth-child(4)  { animation-delay: 66ms; }
  .hhc-pop .pop-item:nth-child(5)  { animation-delay: 78ms; }
  .hhc-pop .pop-item:nth-child(6)  { animation-delay: 90ms; }
  .hhc-pop .pop-item:nth-child(n+7){ animation-delay: 102ms; }
  @keyframes popItem {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  .pop-head {
    display: flex; align-items: center; gap: 10px;
    padding-bottom: 12px; margin-bottom: 14px;
    border-bottom: 1px solid var(--border-1);
    font-size: 16px; font-weight: var(--fw-bold); color: var(--text-1);
  }
  .pop-sub { font-size: 14px; color: var(--text-4); font-weight: var(--fw-normal); }
  /* 分类面板：子项用流式标签布局。
     ★ 宽度对齐：面板宽度由最宽子块（通常是 .pop-foot）决定，
       标签行若只按内容宽度排，右侧就会空出一块。
       因此这里给 .pop-grid 设 width:100%，标签用 flex:1 1 auto 均分整行，
       行数不变、右侧不留空。 */
  .pop-grid {
    display: flex; flex-wrap: wrap; gap: 8px;
    width: 100%;
  }
  .pop-item {
    flex: 1 1 auto;
    min-width: 84px;
    max-width: 190px;          /* 单个标签不被拉成一条长条 */
    padding: 9px 15px;
    background: var(--surface-2);
    border-radius: var(--r-sm);
    font-size: 16px; color: var(--text-2);
    cursor: pointer; transition: var(--dur-fast);
    white-space: nowrap;
    text-align: center;
  }
  .pop-item:hover { background: var(--brand-50); color: var(--brand); }
  .pop-empty { font-size: 14px; color: var(--text-4); padding: 12px 0; }
  .pop-foot {
    margin-top: 14px; padding-top: 12px;
    border-top: 1px solid var(--border-1);
    font-size: 14px; color: var(--brand); cursor: pointer;
    display: flex; align-items: center; gap: 4px;
  }
  .pop-foot:hover { text-decoration: underline; }
  .pop-foot .arw { color: var(--brand); }

  .hhc-more {
    display: flex; align-items: center; justify-content: center; gap: 4px;
    margin: 6px 8px 4px; padding: 10px;
    flex: none;                 /* 不参与伸缩，恒定贴在列表区下方 */
    border-top: 1px solid var(--border-1);
    font-size: 16px; color: var(--brand); cursor: pointer;
    transition: var(--dur-fast); border-radius: var(--r-sm);
  }
  .hhc-more:hover { background: var(--brand-50); }
  .hhc-more .arw { color: var(--brand); }
  /* 面板右上角的可见状态条 ——
     ★ 目的是【把猜测换成事实】：复现页已证明请求层没问题，
       那就只能看「有没有被调用」和「结果有没有落到 state 上」✗
     ★ 平时它是安静的灰色；一旦分类一个都没进来，就变红并提示点一下 ✗ */
  .hhc-diag {
    font-size: 11px; font-weight: 500; color: var(--text-4);
    cursor: pointer; padding: 1px 7px; border-radius: var(--r-full);
    background: var(--surface-2);
  }
  .hhc-diag.bad { color: var(--danger); background: var(--danger-bg); }
  .hhc-diag:hover { color: var(--brand); }
  .hhc-empty { padding: 32px 14px; text-align: center; font-size: 14px; color: var(--text-4); }  .hhc-empty { padding: 32px 14px; text-align: center; font-size: 14px; color: var(--text-4); }
  /* 失败态：与「加载中」在视觉上必须能区分 ——
     加载中是灰的、安静的；失败要能看出「出事了」并且给一个可点的动作。 */
  .hhc-empty.hhc-err { color: var(--danger); }
  .hhc-retry {
    color: var(--brand); cursor: pointer; text-decoration: underline;
    text-underline-offset: 2px; font-weight: 600;
  }
  .hhc-retry:hover { opacity: .75; }        /* 站内没有 --brand-dark，别硬编不存在的令牌 */

  /* 中：品牌卡 / Banner 轮播 */
  .hero-card.has-img {
    padding: 0;
    /* ★★ 比例锁死 + cover 裁切（上线方案）★★
       ------------------------------------------------------------------
       为什么锁死比例：后台可配多张轮播图，若容器高度跟着每张图比例变，
       4 秒切一张时整个 Hero 区（含左侧分类面板）会上下跳动，观感很差。
       锁死后切图高度纹丝不动。

       代价：比例与容器不符的图，会被 object-fit: cover 裁掉超出的边缘。
       所以【上传 banner 时请按容器比例出图】（比例见 tokens.css 的
       --banner-w / --banner-h），这样既不跳动也不裁切。

       为什么不用 contain：contain 会为了完整显示图片而留白 ——
       那正是「上线后轮播图有多余空白」的成因。 */
    aspect-ratio: var(--banner-w) / var(--banner-h);
    min-height: 0;
    display: block;
    overflow: hidden;        /* 圆角裁切 + 兜住 cover 溢出的部分 */
  }

  /* ★★ 轮播图：绝对定位铺满 + cover 裁切 ★★
     ------------------------------------------------------------------
     · absolute + inset: 0 铺满容器，图片不参与容器尺寸计算，
       容器比例完全由上面的 aspect-ratio 决定 → 切图时高度恒定。
     · cover：图片按「填满容器」缩放，长边超出部分裁掉，
       永远不留白、也不变形（短边必定被裁）。
     · object-position: center 让裁切从两侧均分，主体居中不易被切掉。
       若某张图主体偏左/偏右，可单独给它配 object-position —— 但更推荐重新出图。 */
  .hero-img {
    position: absolute; inset: 0;
    display: block;
    width: 100%; height: 100%;
    object-fit: cover;       /* ★ 由 contain 改 cover：铺满不裁留白 */
    object-position: center;
    border-radius: inherit;  /* 跟随容器圆角，避免四角露出直角 */
  }
  .banner-dots {
    position: absolute; left: 0; right: 0; bottom: 14px;
    display: flex; justify-content: center; gap: 8px; z-index: 3;
  }
  .banner-dots i {
    width: 7px; height: 7px; border-radius: var(--r-full);
    background: rgba(255, 255, 255, .55);
    cursor: pointer; transition: var(--dur-base);
  }
  .banner-dots i.on { background: #fff; width: 18px; border-radius: var(--r-xs); }

  .hero-btn {
    margin-top: 22px; align-self: flex-start;
    height: 38px; padding: 0 22px;
    border: 1px solid rgba(255, 255, 255, .5);
    border-radius: var(--r-full);
    background: rgba(255, 255, 255, .16); color: #fff;
    font-size: 15px; font-family: inherit; cursor: pointer;
    transition: var(--dur-fast); position: relative; z-index: 2;
  }
  .hero-btn:hover { background: rgba(255, 255, 255, .28); }

  /* 右：学习动态 */
  .hh-side {
    background: var(--surface-1); border-radius: var(--r-md);
    padding: 18px 20px; box-shadow: var(--sh-1); align-self: start;
  }
  .hhs-head { font-size: 15px; font-weight: var(--fw-bold); color: var(--text-1); margin-bottom: 14px; }
  /* ---- 可点击行：统一的「可点」信号 ----
     ① 默认就有 › 箭头（静态提示"这里能进"）
     ② hover 整行浅蓝底（动态确认"鼠标在这")
     ③ hover 时箭头右移 3px（轻微位移，是「可进入」最直观的语言） */
  .hhs-row {
    display: flex; align-items: center; gap: 8px;
    padding: 10px 12px; margin: 0 -12px;
    border-radius: var(--r-sm);
    font-size: 15px; cursor: pointer; transition: var(--dur-fast);
  }
  .hhs-row .k { color: var(--text-3); flex: 1; min-width: 0; }
  .hhs-row .v { color: var(--text-1); font-weight: var(--fw-medium); flex: none; }
  .hhs-row .v.warn { color: var(--warning); }
  .hhs-row:hover { background: var(--brand-50); }
  .hhs-row:hover .k { color: var(--brand); }
  .hhs-row .arw {
    color: var(--decor-1); font-size: 16px; flex: none;
    transition: var(--dur-fast);
  }
  .hhs-row:hover .arw { color: var(--brand); transform: translateX(3px); }
  .hhs-bar { height: 6px; background: var(--surface-2); border-radius: var(--r-xs); overflow: hidden; margin: 10px 0 6px; }
  .hhs-bar i { display: block; height: 100%; border-radius: var(--r-xs); background: linear-gradient(90deg, var(--brand-light), var(--brand)); }
  .hhs-tip { font-size: 14px; color: var(--text-4); }
  .hhs-divider { height: 1px; background: var(--border-1); margin: 16px 0; }
  .hhs-item {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
    padding: 10px 12px; margin: 0 -12px;
    border-radius: var(--r-sm);
    font-size: 15px; color: var(--text-2);
    cursor: pointer; transition: var(--dur-fast);
  }
  .hhs-item:hover { background: var(--brand-50); color: var(--brand); }
  .hhs-item .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
  .hhs-item .arw {
    color: var(--decor-1); flex: none; font-size: 16px;
    transition: var(--dur-fast);
  }
  .hhs-item:hover .arw { color: var(--brand); transform: translateX(3px); }

  /* ---- 通栏：学习进度（左主指标 + 右辅指标，建立视觉层级）---- */
  .prog-banner {
    background: var(--surface-1); border-radius: var(--r-md);
    /* ★ 左右 padding 统一为 24px，与 .home-course / .hb-main / .hb-side 一致
       —— 否则各区块内容左边缘差 2~6px，纵向排列时看起来「不齐」。 */
    padding: 22px 24px; box-shadow: var(--sh-1); margin-bottom: 18px;
  }
  .pb-top { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 22px; }
  .pb-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 5px; }

  /* ★★ 题库名 —— 整张卡里最跳的一行 ★★
     （原来是 14px 浅灰的副标题，被「我的学习进度」压着，用户要求提到突出位置）

     ★ 为什么用「绝对定位的伪元素色条」而不是 flex + ::before：
       .pb-bank 需要 overflow + text-overflow: ellipsis 来截断过长的题库名，
       而【flex 容器上的 text-overflow 不会作用于匿名文本项】——
       改成 display: flex 后省略号会失效，长名字会把标题撑破。
       所以这里保持块级 + 绝对定位的色条，两者都能满足。 */
  .pb-bank {
    position: relative;
    display: block;
    padding-left: 14px;
    font-size: 20px; font-weight: var(--fw-bold); color: var(--text-1);
    letter-spacing: -.2px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .pb-bank::before {
    content: ''; position: absolute; left: 0; top: 50%;
    transform: translateY(-50%);
    width: 4px; height: 18px; border-radius: 2px; background: var(--brand);
  }

  /* 「我的学习进度」降为说明小字。
     ★ padding-left 与色条对齐（色条 4px + 间隙 10px），
       让两行文字的左边缘落在同一条竖线上。 */
  .pb-t {
    padding-left: 14px;
    font-size: 13px; font-weight: var(--fw-normal); color: var(--text-4);
  }
  .pb-acts { display: flex; gap: 10px; flex: none; }
  .pb-acts .qbtn { height: 38px; padding: 0 22px; font-size: 15px; }

  .pb-main {
    display: grid;
    /* 左（完成度 + 四项统计）主区略宽，右（学习动态）次之 */
    grid-template-columns: minmax(0, 1.08fr) minmax(0, .92fr);
    gap: 28px; align-items: stretch;
  }

  /* 左：主指标 —— 标题行 / 大号百分比 / 进度条 / 四统计 */
  .pb-hero { display: flex; flex-direction: column; }

  .pbh-row {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 12px; margin-bottom: 4px;
  }
  .pbh-label { font-size: 14px; color: var(--text-4); }
  .pbh-frac { font-size: 14px; color: var(--text-4); font-variant-numeric: tabular-nums; }
  .pbh-frac b { color: var(--text-1); font-weight: var(--fw-bold); }

  .pbh-num { display: flex; align-items: baseline; gap: 1px; line-height: 1; margin-bottom: 14px; }
  .pbh-num b { font-size: 40px; font-weight: var(--fw-bold); color: var(--brand); letter-spacing: -1.5px; }
  .pbh-num span { font-size: 20px; font-weight: var(--fw-bold); color: var(--brand); }

  .pbh-bar {
    height: 12px; background: var(--surface-2);
    border-radius: var(--r-full); overflow: hidden; margin-bottom: 20px;
  }
  .pbh-bar i {
    display: block; height: 100%; border-radius: var(--r-full);
    background: linear-gradient(90deg, var(--brand-light), var(--brand));
    transition: width var(--dur-base);
  }
  /* 今日计划：用绿色区分于"总进度"的蓝色 */
  .pbh-bar.is-today i { background: var(--success); }

  /* 分区线：把"总进度"与"今日计划"分开 */
  .pbh-sep { height: 1px; background: var(--border-1); margin: 18px 0; }

  .pbh-go {
    font-size: 14px; color: var(--brand); cursor: pointer;
    margin-left: 8px; font-weight: var(--fw-normal);
  }
  .pbh-go:hover { text-decoration: underline; }
  .pbh-tip { font-size: 14px; color: var(--text-4); margin-top: 8px; }

  /* 四项统计：margin-top:auto 让它沉到卡片底部，与右栏对齐 */
  .pbs-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: auto; }
  .pbs { text-align: center; padding: 11px 0; background: var(--surface-2); border-radius: var(--r-sm); }
  .pbs b { display: block; font-size: 20px; font-weight: var(--fw-bold); color: var(--text-1); line-height: 1.25; }
  .pbs span { font-size: 14px; color: var(--text-4); }

  /* 右：学习动态（浅灰底与左侧主区区分） */
  .pb-side {
    background: var(--surface-2);
    border-radius: var(--r-md);
    padding: 16px 18px;
    display: flex; flex-direction: column;
  }
  .pb-side .hhs-head { font-size: 15px; font-weight: var(--fw-bold); color: var(--text-1); margin-bottom: 10px; }
  .pbs-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
  .pbs { text-align: center; padding: 14px 0; background: var(--surface-2); border-radius: var(--r-sm); }
  .pbs b { display: block; font-size: 22px; font-weight: var(--fw-bold); color: var(--text-1); line-height: 1.2; }
  .pbs span { font-size: 14px; color: var(--text-4); }

  /* 学习动态并入后，行 hover 底色改用白色（父容器已是浅灰底） */
  .pb-side .hhs-row:hover,
  .pb-side .hhs-item:hover { background: var(--surface-1); }

  /* 今日目标（原独立行，已并入学习动态，样式保留备用） */
  .pb-today {
    display: flex; align-items: center; gap: 12px;
    padding: 12px 16px;
    border: 1px solid var(--border-1); border-radius: var(--r-sm);
    cursor: pointer; transition: var(--dur-fast);
  }
  .pb-today:hover { border-color: var(--brand-200); background: var(--surface-hover); }
  .pbt-label { font-size: 14px; color: var(--text-3); flex: none; }
  .pbt-bar {
    flex: 1; min-width: 60px; height: 6px; background: var(--surface-2);
    border-radius: var(--r-full); overflow: hidden;
  }
  .pbt-bar i { display: block; height: 100%; border-radius: var(--r-full); background: var(--success); }
  .pbt-val { font-size: 14px; color: var(--text-2); font-variant-numeric: tabular-nums; flex: none; }
  .pbt-tag { font-size: 14px; padding: 2px 8px; border-radius: var(--r-xs); flex: none; }
  .pbt-tag.ok { background: var(--success-bg); color: var(--success); }

  /* 下半部：网格 + 侧栏（两栏等高，底部对齐） */
  .home-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 300px;
    gap: 20px;
    align-items: stretch;        /* 左主区拉伸到与右栏等高 */
  }
  .hb-main {
    background: var(--surface-1); border-radius: var(--r-md);
    /* ★ 左右 padding 与 .home-course / .prog-banner 统一为 24px（原 22px 偏小，
       导致「我的题库」内容比上方区块更靠左，看起来没对齐）。 */
    padding: 20px 24px 22px; box-shadow: var(--sh-1);
    min-height: 100%;            /* 内容少时也撑满整行高度 */
  }
  .hb-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 14px; margin-top: 16px;
    /* Grid 默认 align-items: stretch —— 同一行的卡片自动等高，
       不要加 grid-auto-rows: 1fr（容器高度为 auto 时它会退化成按内容计算，
       反而把等高效果破坏掉） */
    /* ★ 容器高度按「每页 9 条 = 3 行卡片 + 2 个行间距」锁定：
       最后一页不足 9 条时也保持同样高度，翻页 / 切换页签不会引起页面高度跳动。
       —— 注意这是「容器」高度，不是「卡片」高度 */
    min-height: calc(132px * 3 + 14px * 2);   /* = 424px */
    align-content: start;   /* ★ 行按自身内容高度排列，不被容器高度拉伸
                                （否则只有 1 行时卡片会被撑成 3 行那么高） */
  }
  .hb-card {
    border: 1px solid var(--border-1); border-radius: var(--r-sm);
    padding: 16px 18px; cursor: pointer; transition: var(--dur-fast);
    min-height: 132px;                       /* 与上面 3 行高度的基数一致 */
    display: flex; flex-direction: column;   /* 底部操作行沉底对齐 */
  }
  .hb-card:hover { border-color: var(--brand-200); box-shadow: 0 4px 14px rgba(var(--brand-rgb), .10); }
  .hc-top { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 8px; }
  .hc-nm { font-size: 16px; font-weight: var(--fw-medium); color: var(--text-1); flex: 1; min-width: 0; }
  .hc-tag { font-size: 14px; padding: 2px 8px; border-radius: var(--r-xs); flex: none; }
  .hc-tag.trial { background: var(--warning-bg); color: var(--warning); }
  .hc-tag.ok { background: var(--success-bg); color: var(--success); }
  .hc-meta { font-size: 14px; color: var(--text-4); }
  .hc-bar { height: 5px; background: var(--surface-2); border-radius: var(--r-xs); overflow: hidden; margin-top: 12px; }
  .hc-bar i { display: block; height: 100%; border-radius: var(--r-xs); background: linear-gradient(90deg, var(--brand-light), var(--brand)); }
  .hc-foot { display: flex; align-items: center; justify-content: space-between; margin-top: auto; padding-top: 12px; }
  .hc-pct { font-size: 14px; color: var(--text-4); }
  .hc-go { font-size: 14px; color: var(--brand); }

  /* 空状态 */
  .hb-empty {
    display: flex; flex-direction: column;
    align-items: center; justify-content: center; gap: 14px;
    padding: 62px 20px; color: var(--text-4); font-size: 15px;
    grid-column: 1 / -1;
  }
  /* 空状态插图（改用 APP 官方图，尺寸比原 SVG 大一些更醒目） */
  .hb-empty .em { width: 108px; height: auto; flex: none; object-fit: contain; }

  /* 翻页器：内容多时按页显示，避免页面无限往下长 */
  .pager {
    display: flex; align-items: center; justify-content: center;
    gap: 6px; margin-top: 22px; flex-wrap: wrap;
  }
  .pg {
    min-width: 34px; height: 34px; padding: 0 10px;
    display: inline-flex; align-items: center; justify-content: center;
    border: 1px solid var(--border-2); border-radius: var(--r-sm);
    background: var(--surface-1); color: var(--text-3);
    font-size: 15px; cursor: pointer; transition: var(--dur-fast);
    user-select: none;
  }
  .pg:hover:not(.dis):not(.on):not(.dots) { border-color: var(--brand); color: var(--brand); }
  .pg.on { background: var(--brand); border-color: var(--brand); color: var(--surface-1); font-weight: var(--fw-medium); }
  .pg.dis { color: var(--decor-1); cursor: not-allowed; }
  .pg.dots { border: 0; background: transparent; cursor: default; color: var(--text-4); }
  .pg-info { font-size: 14px; color: var(--text-4); margin-left: 12px; }

  .hb-side {
    background: var(--surface-1); border-radius: var(--r-md);
    /* ★ 左右 padding 与其他首页区块统一为 24px（原 20px 偏小）。
       上下 20px 与 .hb-main 保持一致 —— 两者并排，首行标题才对齐。 */
    padding: 20px 24px; box-shadow: var(--sh-1);
    /* ★ 与左主区等高平齐：左栏内容多时右栏一起长高，底部永远对齐。
       （原先用 align-self:start 保 sticky，会导致两栏不等高，故取消） */
    display: flex; flex-direction: column;
  }
  .hbs-head { font-size: 15px; font-weight: var(--fw-bold); color: var(--text-1); margin-bottom: 12px; }
  .hbs-item {
    display: flex; align-items: center; gap: 10px;
    padding: 11px 12px; margin: 0 -12px; border-radius: var(--r-sm);
    font-size: 16px; color: var(--text-2); cursor: pointer; transition: var(--dur-fast);
  }
  .hbs-item .nm {
    flex: 1; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .hbs-item:hover { background: var(--brand-50); color: var(--brand); }
  .hbs-item:hover .ico { transform: scale(1.12); }   /* 彩色图标：放大代替变色 */
  .hbs-item:hover .arw { color: var(--brand); transform: translateX(3px); }
  .hbs-item .tag { font-size: 14px; padding: 1px 8px; border-radius: var(--r-xs); background: var(--warning-bg); color: var(--warning); flex: none; }
  .hbs-item .arw { color: var(--decor-1); flex: none; font-size: 18px; transition: var(--dur-fast); }
  .hbs-item:last-of-type { margin-bottom: 18px; }   /* 与推广卡的最小间距 */

  .hbs-promo {
    margin-top: auto;              /* ★ 沉底：右栏被拉高时推广卡贴底，不留半截空白 */
    padding: 18px; border-radius: var(--r-md);
    background: linear-gradient(135deg, var(--brand-light), var(--brand));
    color: #fff; cursor: pointer;
  }
  .hp-t { font-size: 16px; font-weight: var(--fw-bold); }
  .hp-d { font-size: 14px; opacity: .92; margin-top: 6px; }
  .hp-btn {
    margin-top: 14px; display: inline-block; padding: 7px 18px;
    border-radius: var(--r-full); background: rgba(255, 255, 255, .2);
    font-size: 14px; border: 1px solid rgba(255, 255, 255, .35);
  }
  .hbs-promo:hover .hp-btn { background: rgba(255, 255, 255, .32); }

  /* 响应式降级 */
  
  

  /* ========== 首页（按 Figma 设计图重做）========== */
  /* 公告条 */
  .notice-bar {
    display: flex; align-items: center; gap: 10px;
    background: var(--brand-50); border-radius: 999px;
    padding: 12px 20px; margin-bottom: 18px;
    font-size: 15px; color: var(--brand); cursor: pointer; transition: .2s;
  }
  .notice-bar:hover { background: var(--brand-100); }
  .notice-bar .ico { width: 18px; height: 18px; flex: none; }

  /* 公告文字容器：负责裁切 + 承载切换动效 */
  .ann-wrap {
    flex: 1; min-width: 0; overflow: hidden;
    display: flex; align-items: center;
  }
  .notice-bar .txt {
    display: inline-block; white-space: nowrap;
    color: var(--brand);
  }

  /* ---- 公告切换动效：上一条上滑淡出，下一条下方淡入 ---- */
  .ann-enter-active,
  .ann-leave-active {
    transition: opacity var(--dur-base) cubic-bezier(.4, 0, .2, 1),
                transform var(--dur-base) cubic-bezier(.4, 0, .2, 1);
  }
  .ann-enter-from { opacity: 0; transform: translateY(12px); }
  .ann-leave-to   { opacity: 0; transform: translateY(-12px); }

  /* ---- 文字超长时：跑马灯横向滚动 ----
     由 JS 测量 scrollWidth > clientWidth 后加上 .marquee */
  .notice-bar .txt.marquee {
    animation: annMarquee 18s linear infinite;
    padding-right: 90px;              /* 滚完留白，避免文字首尾相接 */
  }
  @keyframes annMarquee {
    0%, 8%    { transform: translateX(0); }
    92%, 100% { transform: translateX(calc(-100% + 60px)); }
  }

  .notice-bar .close { color: var(--brand-200); font-size: 20px; line-height: 1; padding: 0 2px; }
  .notice-bar .close:hover { color: var(--brand); }
  .ann-idx { font-size: 14px; color: var(--brand); opacity: .72; flex: none; font-variant-numeric: tabular-nums; }

  .home-top { display: grid; grid-template-columns: 1fr 1.18fr; gap: 20px; margin-bottom: 20px; }

  /* 品牌卡 */
  .hero-card {
    border-radius: var(--r-md); padding: 0;
    background: linear-gradient(135deg, var(--brand-light) 0%, var(--brand) 100%);
    color: var(--surface-1); position: relative; overflow: hidden;
    /* ★ width: 100% + min-width: 0 —— 双保险：
       作为 grid item，它必须严格等于所在列宽，不能被内部图片的固有尺寸顶开。
       （容器侧已把列改成 minmax(0, 1fr)，这里是 item 侧的对称加固。） */
    width: 100%;
    min-width: 0;
    /* ★ 比例统一走令牌（tokens.css 的 --banner-w / --banner-h），
       全站只此一处定义，换接口图时改令牌即可，不必翻 CSS。
       （.hero-card.has-img 里会再引用一次，两者始终一致。）

       历史坑：这里曾写死 686/300，是按内置图 1100×481 换算的；
       上线换成后台接口图后，只要图片比例 ≠ 686:300，contain 就会留白，
       表现为「轮播图左右有多余空白」。故改为令牌 + cover 方案。 */
    aspect-ratio: var(--banner-w) / var(--banner-h);
    min-height: 200px;
    display: flex; flex-direction: column; justify-content: center;
  }
  .hero-card .t { font-size: 32px; font-weight: 700; letter-spacing: 2px; position: relative; z-index: 2; }

  /* 品牌卡插图：贴右侧居中，纯装饰不拦截点击 */
  .hero-art {
    position: absolute; right: 34px; top: 50%;
    transform: translateY(-50%);
    width: 300px; height: auto;
    max-width: 46%;              /* 窄屏按比例缩，不挤占文字区 */
    z-index: 1; pointer-events: none;
  }

  /* 文字区：永远浮在插图之上 */
  .hero-copy {
    position: relative; z-index: 2;
    display: flex; flex-direction: column; align-items: flex-start;
  }
  .hero-card .s { margin-top: 10px; font-size: 16px; opacity: .92; position: relative; z-index: 2; }
  .hero-card .deco  { position: absolute; right: -40px; bottom: -60px; width: 220px; height: 220px; border-radius: 50%; background: rgba(255,255,255,.10); }
  .hero-card .deco2 { position: absolute; right: 50px; top: -60px; width: 140px; height: 140px; border-radius: 50%; background: rgba(255,255,255,.08); }
  .hero-card .dot1 { position: absolute; right: 76px; bottom: 46px; width: 14px; height: 14px; border-radius: 50%; background: rgba(255,255,255,.45); }
  .hero-card .dot2 { position: absolute; right: 110px; bottom: 82px; width: 9px; height: 9px; border-radius: 50%; background: rgba(255,255,255,.55); }
  .hero-card .dot3 { position: absolute; right: 44px; bottom: 110px; width: 7px; height: 7px; border-radius: 50%; background: rgba(255,255,255,.5); }

  /* 累计学习进度卡 */
  .prog-card {
    background: var(--surface-1); border-radius: var(--r-md); padding: 26px 28px;
    box-shadow: 0 4px 22px rgba(var(--brand-rgb),.08);
    display: flex; flex-direction: column;
  }
  .prog-head { display: flex; align-items: center; margin-bottom: 8px; }
  .prog-head .t { font-size: 16px; font-weight: 600; }
  .prog-head .adj { margin-left: auto; font-size: 15px; color: var(--brand); cursor: pointer; }
  .prog-head .adj:hover { text-decoration: underline; }
  .prog-bank {
    font-size: 15px; color: var(--text-4); margin-bottom: 16px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .prog-bar { height: 8px; background: var(--surface-2); border-radius: var(--r-xs); overflow: hidden; margin-bottom: 8px; }
  .prog-bar i { display: block; height: 100%; border-radius: var(--r-xs); background: linear-gradient(90deg, var(--brand-light), var(--brand)); transition: width .5s; }
  .prog-num { font-size: 14px; color: var(--text-4); margin-bottom: 20px; }
  .prog-num b { color: var(--brand); font-size: 16px; }

  .stat-row { display: flex; gap: 12px; margin-bottom: 20px; }
  .stat-item { flex: 1; min-width: 0; }
  .stat-item .ic {
    width: 32px; height: 32px; border-radius: var(--r-sm);
    display: flex; align-items: center; justify-content: center; margin-bottom: 9px;
  }
  .stat-item .ic svg { width: 17px; height: 17px; }
  .stat-item .n { font-size: 24px; font-weight: 700; color: var(--text-1); line-height: 1.15; }
  .stat-item .l { font-size: 14px; color: var(--text-4); margin-top: 4px; }

  .btn-start {
    width: 100%; height: 42px; margin-top: auto;
    border: 0; border-radius: 999px;
    background: linear-gradient(90deg, var(--brand-light), var(--brand));
    color: var(--surface-1); font-size: 16px; font-weight: 500; font-family: inherit;
    cursor: pointer; transition: .2s;
    box-shadow: 0 8px 20px rgba(var(--brand-rgb),.26);
  }
  .btn-start:hover { transform: translateY(-1px); box-shadow: 0 10px 24px rgba(var(--brand-rgb),.32); }

  /* 下方三卡 */
  .home-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
  .hc {
    background: var(--surface-1); border-radius: var(--r-md); padding: 22px 24px;
    box-shadow: 0 4px 22px rgba(var(--brand-rgb),.08);
  }
  .hc .t {
    font-size: 16px; font-weight: 600; margin-bottom: 18px;
    display: flex; align-items: center; gap: 8px;
  }
  .hc .t .tag {
    font-size: 14px; padding: 2px 8px; border-radius: var(--r-xs);
    background: var(--brand-50); color: var(--brand); font-weight: 400;
  }
  .hc .t .tag.warn { background: var(--warning-bg); color: var(--warning); }
  .hc-nums { display: flex; gap: 7px; align-items: baseline; }
  .hc-nums .n { font-size: 28px; font-weight: 700; color: var(--text-1); }
  .hc-nums .u { font-size: 14px; color: var(--text-4); }
  .hc-row { display: flex; gap: 18px; }
  .hc-row .kv { display: flex; flex-direction: column; align-items: center; gap: 3px; }
  .hc-row .kv b { font-size: 18px; color: var(--text-1); }
  .hc-row .kv span { font-size: 14px; color: var(--text-4); }
  .mini-bar { height: 6px; background: var(--surface-2); border-radius: var(--r-xs); overflow: hidden; margin-top: 14px; }
  .mini-bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--brand-light), var(--brand)); border-radius: var(--r-xs); }
  .hc .sub { font-size: 14px; color: var(--text-4); margin-top: 10px; }

  /* 轮播（保留，后台配了才显示） */
  .banner { position: relative; border-radius: var(--r-md); overflow: hidden; background: var(--surface-2); aspect-ratio: 1200/300; margin-bottom: 20px; }
  .banner img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .banner .dots { position: absolute; left: 0; right: 0; bottom: 14px; display: flex; justify-content: center; gap: 8px; }
  .banner .dots i { width: 8px; height: 8px; border-radius: 50%; background: rgba(255,255,255,.55); cursor: pointer; transition: .2s; }
  .banner .dots i.on { background: var(--surface-1); width: 20px; border-radius: var(--r-xs); }

  .goal { background: var(--surface-1); border-radius: var(--r-md); padding: 26px 28px; box-shadow: 0 2px 12px rgba(0,0,0,.04); margin-bottom: 24px; }
  .goal .gtop { display: flex; align-items: baseline; gap: 14px; margin-bottom: 20px; }
  .goal .gtitle { font-size: 20px; font-weight: 600; }
  .goal .gdate { font-size: 15px; color: var(--text-4); }
  .goal .gdays { margin-left: auto; font-size: 15px; color: var(--warning); }
  .goal .gdays b { font-size: 24px; }
  .goal .stats { display: flex; gap: 40px; margin-bottom: 20px; }
  .goal .stat .n { font-size: 24px; font-weight: 600; color: var(--brand); }
  .goal .stat .l { font-size: 14px; color: var(--text-4); margin-top: 4px; }
  .goal .bar { height: 8px; background: var(--border-1); border-radius: var(--r-xs); overflow: hidden; }
  .goal .bar i { display: block; height: 100%; background: var(--brand); border-radius: var(--r-xs); }
  .overview { display: grid; grid-template-columns: repeat(3,1fr); gap: 20px; }
  .ov-card { background: var(--surface-1); border-radius: var(--r-md); padding: 24px; box-shadow: 0 2px 12px rgba(0,0,0,.04); }
  .ov-card .t { font-size: 16px; font-weight: 500; margin-bottom: 8px; }
  .ov-card .d { font-size: 15px; color: var(--text-4); }

  /* 分类三栏 */
  /* ===== 分类页：三列等宽等高卡片 =====
     ★ 三张卡片等宽；配合 .cat-wrap 的 align-items:stretch 自动等高。
       每张卡内部 flex 列布局，列表区 flex:1 —— 内容少的列靠
       .tile-foot 的 margin-top:auto 把「最近浏览 / 热门岗位」推到底，
       不留空白。 */
  .cat-wrap {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    /* ★ 行高直接由容器定死（grid-template-rows 才能约束行）：
       max-height 加在 grid 容器上【不会】限制行高，长列表会把行顶开、
       撑破 max-height，卡片就盖到下方 L4 面包屑上了（截图那次事故）。
       改用 grid-template-rows: 470px —— 行高硬约束，配合卡片
       overflow:hidden + 内层 .tile-body 滚动，长列表在卡内滚。 */
    grid-template-rows: 470px;
    gap: 14px;
    align-items: stretch;
    margin-bottom: 18px;
  }

  /* --- 三张卡片共用外壳 --- */
  .cat-l1, .cat-l2, .cat-l3 {
    background: var(--surface-1);
    border: 1px solid var(--border-1);
    border-radius: var(--r-md);
    box-shadow: var(--sh-1);
    display: flex; flex-direction: column;
    min-height: 0;                 /* 允许子项收缩，否则 flex 子项撑破容器 */
    height: 100%;                  /* 跟随行高（470px），不被内容顶开 */
    overflow: hidden;              /* 内层超出的滚动交给 .tile-body */
  }

  /* 卡片标题行（1/2/3 步骤序号 + 标题 + 计数） */
  .tile-head {
    flex: none;
    display: flex; align-items: center; gap: 9px;
    padding: 15px 16px 13px;
    border-bottom: 1px solid var(--border-1);
  }
  .tile-head .step {
    flex: none; width: 20px; height: 20px; border-radius: var(--r-full);
    display: flex; align-items: center; justify-content: center;
    background: var(--brand-50); color: var(--brand);
    font-size: 14px; font-weight: var(--fw-bold); line-height: 1;
  }
  .tile-head .tt { font-size: 16px; font-weight: var(--fw-bold); color: var(--text-1); }
  .tile-head .cnt { margin-left: auto; font-size: 14px; color: var(--text-4); }

  /* 列表区：可滚动 + 占满剩余高度 */
  .tile-body {
    flex: 1; min-height: 0;
    padding: 10px;
    overflow-y: auto;
    display: flex; flex-direction: column;
  }

  /* --- 三列共用行 --- */
  .cat-l1 .item, .cat-l2 .item, .cat-l3 .item {
    display: flex; align-items: center; gap: 10px;
    padding: 10px 12px; border-radius: var(--r-sm);
    cursor: pointer; transition: var(--dur-fast);
  }
  .cat-l1 .item + .item, .cat-l2 .item + .item, .cat-l3 .item + .item { margin-top: 2px; }

  /* ★★ ② / ③ 两列内容的入场动效 ★★
     ────────────────────────────────────────────────────────────────
     场景：点左侧「选择地区」后，中间「选择城市」与右侧「选择岗位」
           两张卡的内容会整批换掉 —— 加一段错峰入场，让「内容刷新」
           这件事被看见，而不是数字无声地跳一下。

     ★ 为什么只给 L2 / L3、不给 L1：
       左列是「源头」（你点的就是它），保持不动才有稳定的参照；
       右两列是「结果」，它们动起来才能表达出「由左往右推演」的层级感。

     ★ 为什么动画能自动重放（关键）：
       这两列的 v-for 绑的是 :key="c2._id" / :key="b._id"，
       换地区后 id 全变 → Vue 销毁旧节点、创建新节点 →
       新节点上的 CSS 动画自然从头播。
       反过来，点城市只改 .active 这个 class，节点被复用 →
       动画【不会】重播，正是想要的（列表内容没变就不该重播）。

     ★★ 用 fill-mode: backwards 而不是 both ★★
       backwards：只在「延迟期间」套用 from 帧，动画结束后回到元素
                  自身的基础样式（无 opacity、无 transform）。
       both：会把 to 帧永久留在元素上 —— 而 to 帧含 transform，
             会让元素【永久】成为层叠上下文，内部 z-index 再也出不去（本项目踩过）。
       这里 to 与基础样式等价，所以 backwards 视觉完全一样，但更安全。

     ★ 基础样式【不写】opacity / transform，隐藏态只存在于 from 帧里 ——
       动画一旦因任何原因失效，元素仍是本来的可见样子，不会白屏。 */

  .cat-l2 .item, .cat-l3 .item {
    animation: catItemIn .32s cubic-bezier(.22,.9,.28,1) backwards;
  }
  @keyframes catItemIn {
    from { opacity: 0; transform: translateX(-8px); }   /* 从左滑入：呼应「由左往右推演」 */
    to   { opacity: 1; transform: none; }
  }

  /* 逐条错峰：每条 +35ms，表末封顶在 280ms。
     ★ 必须封顶：不封顶的话 20 条的城市列最后一条要等 700ms，
       用户会以为卡住了 —— 动效反而变成负担。 */
  .cat-l2 .item:nth-child(1),   .cat-l3 .item:nth-child(1)   { animation-delay: 0ms; }
  .cat-l2 .item:nth-child(2),   .cat-l3 .item:nth-child(2)   { animation-delay: 35ms; }
  .cat-l2 .item:nth-child(3),   .cat-l3 .item:nth-child(3)   { animation-delay: 70ms; }
  .cat-l2 .item:nth-child(4),   .cat-l3 .item:nth-child(4)   { animation-delay: 105ms; }
  .cat-l2 .item:nth-child(5),   .cat-l3 .item:nth-child(5)   { animation-delay: 140ms; }
  .cat-l2 .item:nth-child(6),   .cat-l3 .item:nth-child(6)   { animation-delay: 175ms; }
  .cat-l2 .item:nth-child(7),   .cat-l3 .item:nth-child(7)   { animation-delay: 210ms; }
  .cat-l2 .item:nth-child(8),   .cat-l3 .item:nth-child(8)   { animation-delay: 245ms; }
  .cat-l2 .item:nth-child(n+9), .cat-l3 .item:nth-child(n+9) { animation-delay: 280ms; }

  /* 系统「减少动态效果」：直接不播。
     因为隐藏态只写在 from 帧，animation: none 后元素就是基础样式 = 完全可见。 */
  @media (prefers-reduced-motion: reduce) {
    .cat-l2 .item, .cat-l3 .item { animation: none; }
  }
  .cat-l1 .item:hover, .cat-l2 .item:hover, .cat-l3 .item:hover { background: var(--brand-50); }
  .cat-l1 .item .nm, .cat-l2 .item .nm, .cat-l3 .item .nm {
    flex: 1; min-width: 0;
    font-size: 16px; color: var(--text-2);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .cat-l1 .item .ar { flex: none; width: 14px; height: 14px; color: var(--decor-1); }
  .cat-l1 .item:hover .ar { color: var(--brand); }

  /* 选中态：实心品牌渐变 + 白字（替掉原来的 inset 3px 竖线，层级更清晰） */
  .cat-l1 .item.active, .cat-l2 .item.active, .cat-l3 .item.active {
    background: linear-gradient(96deg, var(--brand) 0%, var(--brand-light) 100%);
    box-shadow: var(--sh-brand);
  }
  .cat-l1 .item.active .nm, .cat-l2 .item.active .nm, .cat-l3 .item.active .nm { color: var(--surface-1); font-weight: var(--fw-medium); }
  .cat-l1 .item.active .ar { color: var(--surface-1); }

  /* L1 行内的城市预览（不点进去也知道里面有什么） */
  .cat-l1 .item .city-pv {
    flex: none; max-width: 150px; font-size: 14px; color: var(--text-4);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .cat-l1 .item.active .city-pv { color: rgba(255,255,255,.85); }

  /* L2 / L3 行内的数量胶囊 */
  .cat-l2 .item .pill, .cat-l3 .item .pill {
    flex: none; padding: 2px 9px; border-radius: var(--r-full);
    font-size: 14px; background: var(--brand-50); color: var(--brand);
  }
  .cat-l2 .item.active .pill, .cat-l3 .item.active .pill {
    background: rgba(255,255,255,.22); color: var(--surface-1);
  }

  /* 空态指引（比一行「暂无」更有指向性） */
  .cat-l2 .slot, .cat-l3 .slot {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 10px; padding: 30px 16px; text-align: center;
    font-size: 15px; color: var(--text-4);
  }
  .cat-l2 .slot svg, .cat-l3 .slot svg { color: var(--decor-1); }

  /* 空白列的填充块：margin-top:auto 推到底，补满卡片 */
  .tile-foot {
    margin-top: auto;
    padding: 14px 4px 4px;
    border-top: 1px dashed var(--border-1);
  }
  .tile-foot-t { font-size: 14px; color: var(--text-4); padding: 0 8px 7px; }
  .tile-foot .mini {
    display: flex; align-items: center;
    padding: 7px 8px; border-radius: var(--r-xs);
    font-size: 15px; color: var(--text-3);
    cursor: pointer; transition: var(--dur-fast);
  }
  .tile-foot .mini:hover { background: var(--brand-50); color: var(--brand); }
  .tile-foot .mini.on { color: var(--brand); font-weight: var(--fw-medium); }
  .tile-foot .mini .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  /* --- L4 卡片区（通栏，三列下方） --- */
  /* 题库套数卡上的「免费」标记 —— 与价格同一位置、同样大小 ✓ */
  .cat-l4 .vp .free { color: var(--success); font-weight: var(--fw-bold); }
  .cat-l4 {
    background: var(--surface-1);
    border: 1px solid var(--border-1);
    border-radius: var(--r-md);
    box-shadow: var(--sh-1);
    /* ★ 左右 padding 与 .finder 统一为 24px（原 22px）。 */
    padding: 20px 24px;
  }
  .cat-l4 .crumb { font-size: 15px; color: var(--text-4); margin-bottom: 18px; display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
  .cat-l4 .crumb .n { color: var(--brand); background: var(--brand-50); padding: 2px 11px; border-radius: var(--r-full); font-size: 14px; }
  .cat-l4 .hint { font-size: 15px; color: var(--text-4); }

  /* ★★ 防止点「选择岗位」时下方内容区高度跳动 ★★
     ────────────────────────────────────────────────────────────────
     问题：selectL3() 的时序是「先 catL4List = [] 并置 catL4Loading，
           等接口回来再赋值」。于是内容区会先塌成一行「正在读取…」，
           数据到了再撑成 N 行 —— 每点一次岗位就「塌一下再撑开」，
           观感就是闪一下。

     修法：给内容区一个【不小于一行卡片】的最小高度（.ver-item 实测约 73px），
           让「加载态 / 空态 / 一行结果」三者高度一致，切换时不再跳。
           两行及以上仍会自然变高 —— 那是真实的内容差异，不是抖动。

     ★ 为什么提示行也要给最小高并做成 flex 居中：
       提示行本身只有一行文字（约 21px），不给高度就还是会把容器压塌；
       加 flex + align-items: center 是为了让文字在这个高度里垂直居中，
       否则会贴着顶部、显得像没对齐。

     ★ 73px 这个数字的来源：.ver-item 内容高 47px（.vt 里 .nm 16px×1.45 ≈ 23 +
       .sub 的 5px 间距 + 18.8 ≈ 47）
       + 上下 padding 24px（12×2）+ 上下边框 2px（1×2）= 73px。
       若日后改了 .ver-item 的内边距或字号，这里要跟着调。 */
  /* ★★ 套数 / 网课的内容区固定为【恰好一行】★★
     ────────────────────────────────────────────────────────────────
     用户要求：「题库套数和配套网课不要跟随内容自适应，改成固定的」。

     之前的做法是 min-height 73px + 折叠时 max-height 156px ——
     高度其实仍在 73 / 156 之间变（视套数而定），页面还是会有位移。

     现在直接给【固定高】：
       · 73px = 恰好一行（.ver-item 73px）
       · 超过一行（本宽度下网格正好 3 列，即 >3 项）就折叠，
         点「展开全部」才恢复自适应高度
     ★ 折叠态用固定 height 而不是 max-height：
       max-height 只封上限，内容少的时候块还是短的 —— 那正是「自适应」。
     ★ 加载骨架也遵守同一高度（它复用 .ver-item，且外层就是这个 73px 的网格），
       所以「读取中 → 出结果」也不会有任何位移。
     ★ 展开后高度变化是【用户主动触发】的，属于预期，不算抖动。 */
  .cat-block .ver-grid { height: 73px; overflow: hidden; align-content: start; }
  .cat-block .hint { height: 73px; display: flex; align-items: center; }
  /* 展开态：恢复自适应高度 */
  .cat-block .ver-grid.full { height: auto; overflow: visible; }

  /* ★★ 骨架卡片（加载态占位）★★
     ────────────────────────────────────────────────────────────────
     为什么不再用「正在读取…」这行文字：
       它本质上只有一行字（约 21px），和卡片（73px）**天生不可能等高** ——
       上一版靠手算一个 73px 去凑，估错几像素就还是跳。
       改成骨架卡片后，它复用 .ver-item 这个类，
       高度由【同一个 min-height】决定 —— 加载态与结果态等高是「构造上成立」的，
       不依赖任何手算数字。

     ★ 骨架为什么放 3 个：网格是 auto-fill minmax(320px,1fr)，
       .cat-l4 内宽约 1232px → 正好 3 列，3 个骨架恰好占满一行，
       与常见结果（3 套）行数一致。
     ★ 微光动画只动 background-position，不动尺寸 —— 不会引起任何布局变化。 */
  .ver-item.sk { pointer-events: none; cursor: default; }
  .ver-item.sk:hover { border-color: var(--border-1); background: var(--surface-1); transform: none; }
  .ver-item.sk .sk-bar {
    background: linear-gradient(90deg, var(--surface-2) 25%, var(--surface-3) 37%, var(--surface-2) 63%);
    background-size: 400% 100%;
    border-radius: var(--r-xs);
    animation: skShine 1.4s ease infinite;
  }
  /* ★ 骨架的左侧方块（.sk-b）已随真实卡片的徽标一起删除 ——
     骨架必须与真实卡片同构，否则「加载态 → 出结果」内部结构会变。
     现在骨架只有两行文字条，高度由父级 .ver-item 的 min-height: 73px 兜住，
     与真实卡片仍然相等。 */
  .ver-item.sk .sk-l1 { height: 20px; width: 68%; }
  .ver-item.sk .sk-l2 { height: 14px; width: 46%; margin-top: 8px; }
  @keyframes skShine {
    from { background-position: 100% 50%; }
    to   { background-position: 0 50%; }
  }
  @media (prefers-reduced-motion: reduce) {
    .ver-item.sk .sk-bar { animation: none; }
  }
  .cat-l3 .hint { font-size: 14px; color: var(--text-4); padding: 12px 16px; }

  /* L4 内的分区（题库版本 / 配套网课） */
  .cat-block + .cat-block { margin-top: 26px; padding-top: 22px; border-top: 1px dashed var(--border-1); }
  .cat-block-head { display: flex; align-items: center; gap: 9px; margin-bottom: 14px; }
  .cat-block-head .tt { font-size: 18px; font-weight: 600; color: var(--text-1); }
  .cat-block-head .tt::before { content: ''; display: inline-block; width: 4px; height: 16px; margin-right: 8px; vertical-align: -2px; border-radius: 2px; background: var(--brand); }
  .cat-block-head .cnt { font-size: 14px; color: var(--text-4); }
  .cat-block-head .tip { font-size: 14px; color: var(--text-4); }
  /* 区块标题行的右侧入口（当前「配套网课」已移除该入口，
     保留此规则以便后续区块复用；无引用时不影响任何渲染）。 */
  .cat-block-head .more { margin-left: auto; font-size: 15px; color: var(--text-3); cursor: pointer; }
  .cat-block-head .more:hover { color: var(--brand); }

  /* 版本 / 网课列表：横向紧凑行卡，列数自适应。
     数量 20 个时自动排成多列，1~2 个时也不留大片空白。 */
  .ver-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 10px; }
  .ver-grid.few { grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); }

  /* ★ 已移除 .ver-grid.capped 的 max-height:156px 规则 ——
     改成「固定一行 + 展开」后（见上方 .cat-block .ver-grid 的 height: 73px
     与 .ver-grid.full），折叠不再需要单独的封顶规则。 */

  /* 折叠开关（套数与网课共用） */
  .ver-more {
    display: flex; align-items: center; justify-content: center; gap: 6px;
    margin-top: 12px; padding: 9px 0;
    font-size: var(--fs-sm); color: var(--brand); cursor: pointer;
    border: 1px dashed var(--brand-200); border-radius: var(--r-sm);
    background: var(--brand-50); transition: var(--dur-fast);
    user-select: none;
  }
  /* ★ 悬停只改边框虚实与文字色，不改底色 —— 项目里没有 --brand-100 这个令牌，
     而 var(--brand-100, var(--brand-50)) 这种写法会掩盖「令牌写错」的问题。 */
  .ver-more:hover { border-style: solid; color: var(--brand-hover); }
  .ver-more svg { flex: none; transition: transform var(--dur-base) var(--ease); }

  .ver-item { display: flex; align-items: center; gap: 13px; padding: 12px 14px; background: var(--surface-1);
              border: 1px solid var(--border-1); border-radius: var(--r-sm); cursor: pointer; transition: .18s;
              /* ★ 固定最小高度（实测自然高 73px）：
                 ① 同一排卡片等高，不会因某个标题多一行而参差；
                 ② 让「骨架卡片」与真实卡片高度一致 —— 见下方 .ver-item.sk。 */
              min-height: 73px; }
  .ver-item:hover { border-color: var(--brand); background: var(--brand-50); transform: translateX(2px); }

  /* ★ 已移除 .ver-item .badge-y / .badge-y.old / .badge-y.cv
     （套数卡片的年份徽标 + 网课卡的分类徽标）—— 用户要求删掉左侧方块的样式。
     ★ 年份没有随之丢失：改由 l4Title() 拼回卡片标题（见 app.js）。
       不能只删徽标了事 —— shortName() 本来就故意去掉了年份，
       徽标一没，2026 / 2025 / 2024 三张卡会显示成完全一样。 */

  .ver-item .vt { flex: 1; min-width: 0; }
  /* ★ 标题收敛成【单行 + 省略号】，不再允许折到第二行。
     原因：取消徽标后标题里多了年份（如「2026年上海市工会工作者」），
     折行风险变大；而 .ver-grid 已被固定为 73px 高（正好一行卡片），
     第二行会被容器直接裁掉 —— 那还不如给个省略号来得诚实。
     实测 3 列布局下标题可用宽约 250px，13 字的标题（≈208px）放得下。 */
  .ver-item .nm { font-size: 16px; color: var(--text-1); line-height: 1.45;
                  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* 「往年旧套」弱化：原来是给徽标加 .old，徽标删掉后改挂在标题上 */
  .ver-item .nm.old { color: var(--text-4); font-weight: var(--fw-normal); }
  .ver-item .sub { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 5px; }
  /* ★ 已移除 .ver-item .tf（「行政编」这类标签的样式）——
     模板里那一处 span 已按用户要求删除，这个类已无使用者。
     .tf-q 保留：它还在被 6 处文字说明使用（含 N 个题库科目 / 讲师 / 课时…）。 */
  .ver-item .tf-q { font-size: 14px; color: var(--text-4); }

  .ver-item .vp { flex: none; text-align: right; }
  .ver-item .vp b { display: block; font-size: 18px; color: var(--danger); }
  .ver-item .vp s { font-size: 14px; color: var(--text-4); }

  .ver-item .ar { flex: none; color: var(--text-4); transition: .18s; }
  .ver-item:hover .ar { color: var(--brand); transform: translateX(2px); }

  .ver-item.is-course .nm { font-weight: 500; }

  /* ★★ L4 通栏区（题库套数 / 配套网课）的入场动效 ★★
     ────────────────────────────────────────────────────────────────
     场景：点右侧「选择岗位」后，下方整块内容（面包屑 + 套数 + 网课）会换掉 ——
           加一段入场让「内容刷新了」这件事被看见，而不是数字无声地跳一下。

     ★ 方向为什么是「从下往上」：这块位于三列卡片的下方，
       从下方浮起呼应「内容被推出来」的物理直觉（用户说的「弹出」）。
       —— 与 L2/L3 那两列不同：它们在左侧、用的是从左滑入（呼应由左往右推演）。

     ★ 为什么能自动重放：两个 v-for 的 key 绑的都是 _id（L4 的 _id 形如
       `level3Id + '-y' + year`），换岗位后 id 全变 → Vue 重建节点 → 动画从头播。
       点城市、点地区不重建节点 → 不会重播，正是想要的。

     ★★ 用 fill-mode: backwards，不用 both ★★
       backwards 只在「延迟期间」套用 from 帧，动画结束后回到元素自身样式。
       both 会把 to 帧永久留下 —— 而 to 帧含 transform，
       会让元素永久成为层叠上下文，内部 z-index 再也出不去（本项目踩过）。
       这里 to 与基础样式等价，所以 backwards 视觉一样、但更安全。
       另外 .ver-item:hover 自带 translateX(2px)，用 backwards 才不会把 hover 效果压掉。

     ★ 基础样式【不写】opacity / transform，隐藏态只存在于 from 帧里 ——
       动画一旦因任何原因失效，元素仍是本来的可见样子，不会白屏。 */
  @keyframes verItemIn {
    from { opacity: 0; transform: translateY(12px) scale(.98); }
    to   { opacity: 1; transform: none; }
  }
  .ver-grid .ver-item {
    animation: verItemIn .34s cubic-bezier(.22,.9,.28,1) backwards;
  }

  /* 逐条错峰 +45ms，第 6 条起封顶在 225ms。
     ★ 用 calc(var(--ver-base) + …) 而不是写死延迟：
       「配套网课」那一块整块晚 110ms 出场（形成「套数先来、网课随后」的层次），
       只需给第二个 .cat-block 设一个 --ver-base，不必把 6 条规则再抄一遍。 */
  .ver-grid .ver-item:nth-child(1)    { animation-delay: calc(var(--ver-base, 0ms) + 0ms); }
  .ver-grid .ver-item:nth-child(2)    { animation-delay: calc(var(--ver-base, 0ms) + 45ms); }
  .ver-grid .ver-item:nth-child(3)    { animation-delay: calc(var(--ver-base, 0ms) + 90ms); }
  .ver-grid .ver-item:nth-child(4)    { animation-delay: calc(var(--ver-base, 0ms) + 135ms); }
  .ver-grid .ver-item:nth-child(5)    { animation-delay: calc(var(--ver-base, 0ms) + 180ms); }
  .ver-grid .ver-item:nth-child(n+6)  { animation-delay: calc(var(--ver-base, 0ms) + 225ms); }

  /* 第二个区块（配套网课）整块后移一拍 */
  .cat-block + .cat-block { --ver-base: 110ms; }

  /* 系统「减少动态效果」：直接不播。
     因为隐藏态只写在 from 帧，animation: none 后元素就是基础样式 = 完全可见。 */
  @media (prefers-reduced-motion: reduce) {
    .ver-grid .ver-item { animation: none; }
  }

  .cat-l4 .crumb .n-course { color: var(--success); background: var(--success-bg); }

  /* 题库详情 */
  /* ★ 已移除：.bd-back（「← 返回分类」返回链接）
     题库详情改为弹窗后，返回动作由弹窗头部的 × 与点击遮罩承担，
     这个链接已无任何使用者。 */
  .bank-hero { background: var(--surface-1); border-radius: var(--r-md); padding: 24px 28px; box-shadow: 0 2px 12px rgba(0,0,0,.04); display: flex; gap: 24px; margin-bottom: 22px; }
  .bank-hero .cover { width: 180px; height: 120px; flex: none; border-radius: var(--r-sm); overflow: hidden; background: var(--surface-3); display: flex; align-items: center; justify-content: center; color: var(--text-4); font-size: 14px; }
  .bank-hero .cover img { width: 100%; height: 100%; object-fit: cover; }
  .bank-hero .meta { flex: 1; }

  /* ---------- 网课详情弹窗（与题库详情弹窗同构）----------
     用户口径：「分类里面点击四级类目的网课，模仿一下四级类目的题库」。
     外壳直接复用 .bank-modal / .bank-hero / .modal-head / .bank-modal-foot，
     这里只补网课特有的几块：播放占位封面、课程标签、目录 / 讲义 / 介绍。 */
  /* 封面位：暂无真实封面，做成「播放」占位（可点，后续接视频源） */
  .bank-hero .cover.cm-cover { background: var(--brand-50); color: var(--brand); cursor: pointer; }
  .bank-hero .cover.cm-cover .ph { display: flex; align-items: center; justify-content: center;
                                   width: 46px; height: 46px; border-radius: 50%; background: var(--brand); color: #fff; }
  .bank-hero .cover.cm-cover svg { width: 20px; height: 20px; margin-left: 2px; }
  .bank-hero .cover.cm-cover:hover .ph { filter: brightness(1.06); }

  /* 课程标签：贴在标题下面的说明行里，与课时 / 讲师同一行 */
  .bank-hero .bn-sub .cm-tag {
    display: inline-block; font-size: 12px; padding: 1px 7px; border-radius: var(--r-xs);
    background: var(--brand-50); color: var(--brand); margin-right: 6px;
  }

  /* 目录：一章一行，点开看小节 */
  .cm-chap { border: 1px solid var(--border-1); border-radius: var(--r-sm); overflow: hidden; }
  .cm-chap + .cm-chap { margin-top: 10px; }
  .cm-chap-head {
    display: flex; align-items: center; gap: 12px;
    padding: 13px 16px; cursor: pointer; background: var(--surface-1); transition: var(--dur-fast);
  }
  .cm-chap-head:hover { background: var(--brand-50); }
  .cm-chap-head .no {
    width: 24px; height: 24px; flex: none; border-radius: var(--r-xs);
    background: var(--surface-2); color: var(--text-4);
    display: flex; align-items: center; justify-content: center;
    font-size: 12px; font-weight: var(--fw-bold); font-variant-numeric: tabular-nums;
  }
  .cm-chap-head .nm { flex: 1; min-width: 0; font-size: 15px; color: var(--text-1);
                      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .cm-chap-head .cnt { font-size: 13px; color: var(--text-4); flex: none; }
  .cm-chap-head .ar { flex: none; color: var(--text-4); display: flex;
                      transition: transform var(--dur-base) var(--ease); }
  .cm-chap-head .ar.open { transform: rotate(180deg); }
  .cm-chap-head .ar svg { width: 15px; height: 15px; }

  .cm-lessons { background: var(--surface-2); padding: 4px 0; border-top: 1px solid var(--border-1); }
  .cm-lesson {
    display: flex; align-items: center; gap: 12px;
    padding: 10px 16px 10px 26px; cursor: pointer; font-size: 14px; color: var(--text-2);
    transition: var(--dur-fast);
  }
  .cm-lesson:hover { background: var(--surface-1); color: var(--brand); }
  .cm-lesson .idx { flex: none; color: var(--text-4); font-variant-numeric: tabular-nums; font-size: 13px; }
  .cm-lesson .nm { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .cm-lesson .dur { flex: none; color: var(--text-4); font-size: 13px; font-variant-numeric: tabular-nums; }

  .cm-intro { font-size: 15px; line-height: 2; color: var(--text-2); white-space: pre-wrap; }

  .cm-docs { display: flex; flex-direction: column; gap: 8px; }
  .cm-doc {
    display: flex; align-items: center; gap: 12px;
    padding: 12px 16px; border: 1px solid var(--border-1); border-radius: var(--r-sm);
    font-size: 15px; color: var(--text-2); cursor: pointer; transition: var(--dur-fast);
  }
  .cm-doc:hover { border-color: var(--brand-200); background: var(--brand-50); }
  .cm-doc .ic { flex: none; color: var(--brand); display: flex; }
  .cm-doc .ic svg { width: 17px; height: 17px; }
  .cm-doc .nm { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .cm-doc .tag { flex: none; font-size: 12px; padding: 1px 7px; border-radius: var(--r-xs);
                 background: var(--surface-2); color: var(--text-4); }
  .bank-hero .bn { font-size: 24px; font-weight: 600; margin-bottom: 6px; }
  /* ★ 「岗位名」降为标题下面的小字说明。
     删掉「题库套数」列表后，标题改挂当前套名（currentL4.name），
     岗位名就退到这里当范围说明 —— 与「我的学习进度」卡同一套主次原则：
     具体的那一个当标题，范围当说明。 */
  .bank-hero .bn-sub { font-size: var(--fs-sm); color: var(--text-4); margin-bottom: 12px; }
  .bank-hero .tg { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
  .bank-hero .tg span { font-size: 14px; padding: 3px 9px; border-radius: var(--r-xs); background: var(--brand-50); color: var(--brand); }
  .bank-hero .pr { display: flex; align-items: baseline; gap: 10px; margin-bottom: 16px; }
  .bank-hero .pr b { font-size: 28px; color: var(--danger); }
  .bank-hero .pr s { font-size: 16px; color: var(--text-4); }
  .hero-actions { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
  .act-btn { height: 40px; padding: 0 22px; border-radius: var(--r-sm); border: 1px solid var(--brand); background: var(--surface-1); color: var(--brand); font-size: 16px; cursor: pointer; }
  .act-btn:hover:not(:disabled) { background: var(--brand-50); }
  .act-btn:disabled { opacity: .5; cursor: not-allowed; }
  .act-btn.solid { background: var(--brand); color: var(--surface-1); }
  .act-btn.solid:hover:not(:disabled) { opacity: .9; background: var(--brand); }
  .owned-tag { font-size: 15px; padding: 5px 12px; border-radius: var(--r-xs); background: var(--success-bg); color: var(--success); }
  /* ★ 已移除 .lv4-list / .lv4-item / .lv4-item:hover / .lv4-item.active
     这四个类是题库详情弹窗里「题库套数」列表专用的。
     用户要求把那个列表整块删掉（点进来的就已经是某一套了，
     再把整套 L4 列一遍是重复），四个类随之失去全部使用者。 */
  /* ---------- 章节目录（题库详情弹窗内）----------
     ★★ 去掉图标的理由 ★★
       原来每条左侧有个 40×40 的图标块。但章节的 icon 字段基本是空的
       （mock 里全是空串），于是那块永远显示章名的首字 ——
       既不是真图标、又占掉 40px 宽 + 14px 间距，让本来能放两列的卡片变成一列半。
       去掉后改成「序号徽标 + 章名」：
       序号本来就比「首字图标」提供了更多信息（第几章一眼可见），
       且省下的宽度让弹窗（内容宽 712px）正好排两列。

     ★ 同时把卡片本身压扁：padding 16→12、去掉阴影改描边 ——
       这一层是「辅助信息」（选章），不该和「包含科目 / 题库套数」抢视觉重量。 */
  .chapter-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .chapter-item {
    display: flex; align-items: center; gap: 12px;
    padding: 12px 14px; border-radius: var(--r-sm);
    border: 1px solid var(--border-1);
    cursor: pointer; transition: var(--dur-fast);
  }
  .chapter-item:hover { border-color: var(--brand-200); background: var(--brand-50); }
  .chapter-item.active { border-color: var(--brand); background: var(--brand-50); }
  .chapter-item .no {
    width: 26px; height: 26px; flex: none; border-radius: var(--r-xs);
    background: var(--surface-2); color: var(--text-4);
    display: flex; align-items: center; justify-content: center;
    font-size: 13px; font-weight: var(--fw-bold); font-variant-numeric: tabular-nums;
    transition: var(--dur-fast);
  }
  .chapter-item:hover .no { background: var(--brand); color: #fff; }
  .chapter-item.active .no { background: var(--brand); color: #fff; }
  .chapter-item .nm {
    flex: 1; min-width: 0; font-size: 15px; color: var(--text-1);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* ---------- 章 → 小节：独立小弹窗 ----------
     ★★ 用户口径 ★★
       点章之后小节要【弹窗弹出】，而且【不显示题目进度】——
       题库详情是介绍题库的地方，不是刷题入口，也就不展示个人做题进度。
     ★ 为什么不再内嵌在详情弹窗里：详情本身已有 hero + 章节目录，
       再接一长条小节列表会让整页变成「目录树」，与「介绍」的定位不符。

     ★ 叠层：z-index 取 --z-mask + 10，压在题库详情弹窗（--z-mask）之上。
     ★ 关自己的时候不会连带关掉详情弹窗，靠的是模态遮罩上的 `.self` 判定
       （点到内层遮罩时，冒泡到外层遮罩的 target 仍是内层，`.self` 不成立）。 */
  .sec-mask { z-index: calc(var(--z-mask) + 10); }
  .sec-modal {
    background: var(--surface-1); border-radius: var(--r-lg);
    box-shadow: var(--sh-modal);
    width: 100%; max-width: 480px;
    max-height: calc(100vh - 160px);
    display: flex; flex-direction: column;
  }
  .sec-modal-body { padding: 4px 10px 14px; overflow-y: auto; }

  /* ══════ 小节的层级与锁定 ══════
     ★ 层级：子节点行本来就有 .sl-sub 的缩进 ✓ 但以前永远不显示 ✗
       （subSectionsOf 只认 s.children 而接口给的是扁平表 ✓ 已修 ✓）
     ★ 锁定：只压暗 + 锁图标 ✓ **不改字色** ✗
       —— 前面连着栽过两次"字色撞底色"，这次只用透明度 ✓ */
  .sl-row.locked { opacity: .5; }
  .sl-row.locked:hover { background: transparent; }
  .sl-go.sl-lock { color: var(--text-4); font-weight: var(--fw-medium); }


  /* ★ 只展示、不可点：没有 cursor: pointer、没有 :hover ——
       这里的小节是「这套题库里有哪些内容」的介绍，不是入口。 */
  .sec-row {
    display: flex; align-items: center; gap: 12px;
    padding: 12px 14px; border-radius: var(--r-sm);
    font-size: 15px; color: var(--text-1);
  }
  .sec-row + .sec-row { border-top: 1px dashed var(--border-1); }
  .sec-row .no {
    width: 24px; height: 24px; flex: none; border-radius: var(--r-xs);
    background: var(--surface-2); color: var(--text-4);
    display: flex; align-items: center; justify-content: center;
    font-size: 12px; font-weight: var(--fw-bold); font-variant-numeric: tabular-nums;
  }
  .sec-row .nm {
    flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

  /* ★ 已移除 .section-list / .section-item / .sn / .sp / .sbar：
     这套类是原来内嵌在题库详情弹窗里的「小节列表」专用的，
     那块已按用户要求改成上面的独立弹窗，这几个类随之失去全部使用者。
     ★ 顺带说明：进度（.sp / .sbar）也随之不再需要 ——
       新弹窗按用户要求不展示题目进度。 */

  /* 刷题主界面（题库菜单） */
  /* ★ 这组 .practice-head 规则【已不再被模板使用】——
     题库页页头已改成 .tp-head（见文件下方「题库页·参考站布局」区块）。
     保留是为了防止旧缓存 HTML 拿到无样式页头；新代码不要再引用。 */
  .practice-head {
    background: var(--surface-1); border-radius: var(--r-md); padding: 16px 22px;
    display: flex; align-items: center; gap: 14px; margin-bottom: 20px;
    box-shadow: 0 2px 12px rgba(0,0,0,.04);
  }
  .practice-head .ph-label { font-size: 16px; color: var(--text-4); }
  .practice-head .ph-select {
    height: 38px; padding: 0 12px; border: 1px solid var(--border-2); border-radius: var(--r-sm);
    font-size: 16px; color: var(--text-1); background: var(--surface-1); outline: none;
    min-width: 260px; cursor: pointer;
  }
  .practice-head .ph-select:focus { border-color: var(--brand); }
  .practice-head .ph-meta { font-size: 15px; color: var(--text-4); }
  .practice-head .mid { flex: 1; }

  /* ============================================================
     题库页 · 参考站布局（.tp-* 前缀，全部为本页私有命名空间）
     ------------------------------------------------------------
     几何来源：参考站截图实测后折算到本站「内容区 1360px」
       （1440 max-width − 40×2 padding = 1360，与截图折算一致）
       左栏 892px（66%） / 右栏 449px（33%） / 栏间距 18px
       三色功能卡：各 273px，间距 18px → 273×3 + 18×2 = 855 ≈ 左栏内宽
     设计原则：
       ① 两栏用 minmax(0, 1fr) 而非 1fr —— grid item 的 min-width 默认
          auto(=min-content)，内部有长文本/图片时会把 1fr 顶开（轮播图踩过）。
       ② 圆角一律加在「有背景色/描边」的那一层，父级保持直角 + overflow 裁切，
          否则会按父级圆角去裁子元素，出现「圆角外套直角」。
       ③ 不写任何 opacity:0 到基础样式 —— 动画只能锦上添花。
     ============================================================ */

  /* ---------- ① 页头：图标 + 题库名 + 状态 + 切换下拉 ---------- */
  .tp-head {
    display: flex; align-items: center; gap: 10px;
    margin-bottom: 16px;
  }
  .tp-ic {
    width: 26px; height: 26px; flex: none;
    display: inline-flex; align-items: center; justify-content: center;
    color: var(--brand);
  }
  .tp-ic svg { width: 20px; height: 20px; }
  .tp-nm {
    font-size: var(--fs-md); font-weight: var(--fw-bold); color: var(--text-1);
    max-width: 420px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .tp-tag {
    font-size: var(--fs-xs); flex: none;
    padding: 2px 9px; border-radius: var(--r-full);
  }
  .tp-tag.ok    { background: var(--success-bg); color: var(--success); }
  .tp-tag.trial { background: var(--warning-bg); color: var(--warning); }
  .tp-tag.exp   { background: var(--surface-2);  color: var(--text-4); }
  .tp-head .mid { flex: 1; }
  .tp-meta { font-size: var(--fs-sm); color: var(--text-4); flex: none; }

  /* 切换题库：自定义下拉 */
  .tp-sel {
    position: relative; flex: none;
    display: inline-flex; align-items: center; gap: 6px;
    height: 34px; padding: 0 12px; margin-left: 4px;
    border: 1px solid var(--border-2); border-radius: var(--r-sm);
    background: var(--surface-1); color: var(--text-3);
    font-size: var(--fs-sm); cursor: pointer;
    transition: border-color var(--dur-fast), background var(--dur-fast);
  }
  .tp-sel:hover { border-color: var(--brand-200); background: var(--brand-50); }
  .tp-sel.open { border-color: var(--brand); }
  .tp-sel .caret { width: 14px; height: 14px; transition: transform var(--dur-base); }
  .tp-sel.open .caret { transform: rotate(180deg); }

  .tp-menu {
    position: absolute; left: 0; top: calc(100% + 6px); z-index: var(--z-content);
    min-width: 300px; max-height: 340px; overflow-y: auto;
    background: var(--surface-1); border: 1px solid var(--border-2);
    border-radius: var(--r-sm); box-shadow: var(--sh-3);
    padding: 6px;
  }
  .tp-opt {
    display: flex; align-items: center; gap: 10px;
    padding: 10px 12px; border-radius: var(--r-xs);
    font-size: var(--fs-sm); color: var(--text-2); cursor: pointer;
    transition: background var(--dur-fast);
  }
  .tp-opt:hover { background: var(--brand-50); }
  .tp-opt.on { background: var(--brand-50); color: var(--brand); font-weight: var(--fw-medium); }
  .tp-opt .on-nm { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .tp-opt .on-tag { font-size: var(--fs-xs); color: var(--text-4); flex: none; }
  .tp-opt.on .on-tag { color: var(--brand); }

  /* ---------- ② 两栏骨架 ---------- */
  .tp-wrap {
    display: grid;
    grid-template-columns: minmax(0, 892fr) minmax(0, 449fr);
    gap: 18px;
    align-items: start;
  }
  .tp-main { min-width: 0; display: flex; flex-direction: column; gap: 16px; }
  .tp-aside { min-width: 0; display: flex; flex-direction: column; gap: 16px; }

  /* ---------- ③ 开始一组练习（主栏顶部 CTA）----------
     ★ 它取代了原来的三张功能卡（我的错题 / 我的收藏 / 随机练习）。
       一张卡说清「方式 + 题量 + 开练」，比三张卡各说一件更省纵向空间。

     ★ 三种方式与取材范围（用户口径）：
         错题 → 取【当前整章】里错题本出现过的
         收藏 → 取【当前整章】里收藏过的
         随机 → 取【整套题库】随机抽
       「章节」已删除 —— 章节目录就在卡片正下方，再放一个入口是重复的。

     ★ 宽度是实测出来的（卡片内容宽 841px = 893 − 26×2，1440 视口实测）：
         标题块 180.6 + 16 + 方式组 221.6 + 16 + 分隔 1 + 16
       + 题量组 242.8 + 16 + 按钮 131 = 841px（严丝合缝，余量全给标题块）
       标题块设成 flex: 1（伸缩），其余全部 flex: none（不被压缩）——
       万一以后加字，先挤标题块，不会把右侧的控件挤变形。
     ★★ 为什么 gap 22→16、胶囊内边距各收 2px ★★
       旧版固定件合计 720px，标题块只剩 120.6px，而「开始一组练习」6 字 × 20px
       正好要 120px —— 只有 0.6px 余量 ✗
       于是选中「100题」（3 位数，加粗后比「50题」宽 0.4px）时，
       固定件涨 0.4px → 标题块被压到 120 以下 → 标题折成「开始一组练 / 习」✗✗
       收窄后标题块 = 126px（1360 视口）/ 180px（1440 视口），
       四档题量怎么点，这一行都不动 ✓
     ★ 蓝底渐变 + 白字：这是本页【唯一的主动作】，给足视觉重量。 */
  .qz-start {
    display: flex; align-items: center; gap: 16px;
    /* ★ 允许折行：窄屏时「控件区」整体落到第二行 ✓
       原来不折行 → 只有文案块能收缩 → 被压到 89px（实测），
       副标题断成几行、最后一行只剩一个「练」字 ✗ */
    flex-wrap: wrap;
    row-gap: 14px;
    padding: 20px 26px;
    border-radius: var(--r-md);
    background: linear-gradient(120deg, var(--brand-light) 0%, var(--brand) 100%);
    box-shadow: 0 10px 28px rgba(var(--brand-rgb), .24);
    color: #fff;
  }
  /* ★ 唯一可伸缩的一块：多出来的宽度给它，控件位置就不受影响。
     ★★ 但它【不能被压到标题以下】★★ —— 见下方 .qs-t 的 nowrap 说明
     ★ 给一个最小宽度：标题 6 个字需要 ~126px，低于它就只能靠 nowrap 硬撑 ✗
       160px 起步能保证副标题至少排成两行、且不会出现"单字成行" ✓ */
  .qz-start .qs-tx { flex: 1 1 auto; min-width: 132px; }
  /* ★ 控件区整体一块：宽屏时靠右排开，窄屏时整体换行（不会被切散）*/
  .qz-start .qs-ctl {
    display: flex; align-items: center; gap: 12px;
    flex: 0 1 auto; margin-left: auto;
  }
  /* ★★ 标题永不换行 ★★
     这一行是 flex 里唯一会收缩的孩子，一旦被压到 < 20px×6 字，标题就会折成
     「开始一组练 / 习」两行（用户截图就是这个）。
     nowrap 让它宁可右边缘多占几像素（右侧还有 16px 间隙兜底），也不折行。 */
  .qz-start .qs-t { font-size: var(--fs-lg); font-weight: var(--fw-bold); white-space: nowrap; }
  .qz-start .qs-d {
    font-size: var(--fs-xs); opacity: .82; margin-top: 5px;
    /* ★ 两行时尽量均分，避免"末行只剩一个字"（Chrome/Edge 114+ 支持）✓
       不支持的浏览器会忽略这一行，退回普通换行 —— 不报错、不破版 ✓ */
    text-wrap: balance;
  }

  .qz-start .qs-modes { flex: none; display: flex; gap: 8px; }
  .qz-start .qs-modes span {
    font-size: var(--fs-sm); cursor: pointer; user-select: none;
    padding: 8px 14px; border-radius: var(--r-full);
    background: rgba(255,255,255,.16); color: #fff;
    border: 1px solid rgba(255,255,255,.22);
    /* ★ 过渡只写「会变颜色/阴影」的三项，不再用 transition: var(--dur-fast)——
       那句等价于 transition: all 150ms，会把 font-weight 也做插值 ✗
       点「100题」时胶囊在 150ms 内由 400 渐变成 600，宽度一路变大，
       整行随之抖动（栅格重排）。限定属性后，选中态是【一步到位】✓ */
    transition: background var(--dur-fast), border-color var(--dur-fast), box-shadow var(--dur-fast);
    white-space: nowrap;
  }
  /* ★ 题源后面的条数徽标 ——
     让人一眼看出「这个来源到底有没有货」，不用点下去才知道 ✓ */
  .qz-start .qs-cnt {
    font-size: 10px; font-weight: 700; color: #fff;
    background: rgba(255,255,255,.3); border-radius: var(--r-full);
    padding: 0 5px; margin-left: 5px; line-height: 14px;
    font-variant-numeric: tabular-nums;
  }
  /* ★★ 选中态的徽标原来是"白字 + 半透明白底" ✗
       而选中态胶囊本身是【纯白底】✓
       → 白字压白底 = **那个数字直接消失了** ✗✗
       用户说的是「选中之后就不显示了」✓ 就是这个 ✓
       ★ 修法：选中态改用品牌色字 + 浅品牌底（深字浅底，对比度足够 ✓）
         而不是在半透明白上再叠一层白 ✗ */
  /* ══════ 整卡锁定态（试用中的题库）══════
     ★ 用户口径：「这个卡片所有功能都是付费功能」
       → 三个题源、题量、立即开始，整体压暗 ✓ 一眼看出"这一整块现在还不行" ✓
     ★ 但【不禁用点击】—— 点了会说明原因并指出可行路径（按章节练）✓
       灰掉又不说为什么，比"能点但告诉你为什么"更让人困惑 ✗
     ★ 字色仍然保持白色 ✓ 只是整体降透明度 ✓
       —— 不靠"改字色"来表达禁用，避免又踩"红压红 / 白压白" ✗ */
  /* ★★★ 锁定态现在是【另一套布局】★★★
     不是"旧布局压暗" ✗ —— 旧布局里有一块是 flex:1 1 auto + min-width:0 ✓
     塞一句长文案进去就会被压到 0 宽 ✗ 文字竖着挤成一列 ✗ 整卡塌掉 ✗✗
     ★ 现在锁定时只渲染：锁 + 标题 + 一句短说明 + 动作 ✓
       三个胶囊、题量、分隔线都不进 DOM ✓ 没有可挤压的东西 ✓ */
  .qz-start.locked { gap: 16px; }
  .qz-start .qs-lock { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 12px; }
  .qz-start .qs-lock .lk { font-size: 18px; line-height: 1; flex: none; }
  .qz-start .qs-lock .tx { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
  .qz-start .qs-lock .tx b { font-size: var(--fs-lg); font-weight: var(--fw-bold); color: #fff; }
  .qz-start .qs-lock .tx em {
    font-style: normal; font-size: var(--fs-xs); color: rgba(255,255,255,.86);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /* ★ 锁定态的按钮：白底蓝字 —— 在渐变上对比最强 ✓ 而且一看就是"可点的动作" ✓
       不做成灰的 ✗ 灰按钮会让人以为"不能点" ✗ 而它其实点了会解释 ✓ */
  .qz-start.locked .lock-go {
    flex: none;
    background: #fff; color: var(--brand);
    box-shadow: 0 6px 16px rgba(0,0,0,.12);
  }
  .qz-start.locked .lock-go:hover { transform: translateY(-1px); box-shadow: 0 8px 20px rgba(0,0,0,.16); }

  .qz-start .qs-modes span.on .qs-cnt {
    color: var(--brand);
    background: var(--brand-50);
  }

  .qz-start .qs-modes span:hover { background: rgba(255,255,255,.28); }
  .qz-start .qs-modes span.on {
    /* 选中态：白底 + 品牌蓝字（对比 5.01:1）+ 微投影 ——
       加粗仍保留，但宽度变化已被下方的 .qs-tx 余量吸收 ✓ */
    background: #fff; color: var(--brand); font-weight: var(--fw-bold); border-color: #fff;
    box-shadow: 0 3px 10px rgba(0,0,0,.14);
  }

  /* 题量组：比方式组更紧凑 —— 它多了「题」字且是 4 个，
     沿用方式组的 padding 会把整行撑到 940px 以外。 */
  .qz-start .qs-counts span { font-size: var(--fs-xs); padding: 8px 10px; }

  /* 方式 / 题量两组之间的竖分隔线（半透明白，不切断蓝渐变） */
  .qz-start .qs-sep {
    flex: none; width: 1px; height: 22px;
    background: rgba(255,255,255,.28);
  }

  .qz-start .qs-go {
    flex: none; height: 46px; padding: 0 22px;
    border: 0; border-radius: var(--r-full);
    background: #fff; color: var(--brand);
    font-size: var(--fs-base); font-weight: var(--fw-bold); font-family: inherit;
    cursor: pointer; transition: var(--dur-fast);
    display: inline-flex; align-items: center; gap: 7px;
    white-space: nowrap;
  }
  .qz-start .qs-go:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 8px 20px rgba(0,0,0,.18); }
  .qz-start .qs-go:active:not(:disabled) { transform: translateY(0); box-shadow: none; }
  /* 「随机 / 整套题库」要拉几十个请求，必须给加载态，否则用户会连点 */
  .qz-start .qs-go:disabled { opacity: .72; cursor: not-allowed; }

  /* ★ 已移除：.tp-fns / .tfn / .tfn-ic / .tfn-tx / .tfn-t / .tfn-d（三张功能卡）
     已被上面的 .qz-start 一张卡取代，这几个类现无任何使用者。
     用到的 assets/icons 下的 .webp 不受影响 —— 首页快捷入口仍在用。 */

  /* ---------- ④ 章节标题行 ---------- */
  .tp-sec {
    display: flex; align-items: center; gap: 10px;
    padding: 2px 2px 0;
    scroll-margin-top: calc(var(--topbar-h) + 16px);   /* 锚点跳转让开顶栏 */
  }
  .tp-sec .ts-t { font-size: var(--fs-md); font-weight: var(--fw-bold); color: var(--text-1); }
  .tp-sec .ts-c { font-size: var(--fs-xs); color: var(--text-4); }

  /* ---------- ⑤ 列表区块（整块白卡：章条 + 小节网格）---------- */
  .tp-list {
    background: var(--surface-1); border-radius: var(--r-md);
    box-shadow: var(--sh-1); overflow: hidden;
  }

  /* 章条：横向排列，当前章高亮
     ★★ 由「自动宽度 + flex 换行」改为「等宽网格」★★
        原来的 .tl-chs 是 flex-wrap，章条宽度跟着章名长短变，
        每排能塞几个全看运气 —— 8 个章分成 6 + 2，
        第一排浪费 13%、第二排浪费 71%，末排永远对不齐。
        改成网格后每排都填满，章数从 4 到 16 都不破版。

     ★ 列数由 JS 算好写在 --chs-cols 上（见 app.js 的 chapterCols）：
         行数 = ceil(章数 / 4)，列数 = ceil(章数 / 行数)
       8 章 → 4 列 × 2 行（满）；6 章 → 3×2（满）；12 章 → 4×3（满）。
       默认值 4 是兜底，万一变量没设上也不会塌成 1 列。 */
  .tl-chs {
    display: grid;
    grid-template-columns: repeat(var(--chs-cols, 4), minmax(0, 1fr));
    gap: 8px;
    padding: 14px 18px;
    border-bottom: 1px solid var(--border-1);
    background: var(--surface-2);
  }
  .tl-ch {
    display: flex; align-items: center; gap: 9px;
    padding: 9px 14px; border-radius: var(--r-sm);
    background: var(--surface-1); border: 1px solid var(--border-2);
    cursor: pointer; transition: var(--dur-fast);
    min-width: 0;                        /* ★ 允许被压缩，长章名走省略号 */
  }
  .tl-ch:hover { border-color: var(--brand-200); background: var(--brand-50); }
  .tl-ch.on { background: var(--brand); border-color: var(--brand); }
  .tl-ch .ch-no {
    width: 20px; height: 20px; flex: none; border-radius: var(--r-full);
    background: var(--surface-2); color: var(--text-4);
    display: flex; align-items: center; justify-content: center;
    font-size: 12px; font-weight: var(--fw-bold); font-variant-numeric: tabular-nums;
  }
  .tl-ch.on .ch-no { background: rgba(255,255,255,.24); color: var(--text-on-brand); }
  .tl-ch .ch-nm {
    flex: 1; min-width: 0;
    font-size: var(--fs-sm); color: var(--text-2);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .tl-ch.on .ch-nm { color: var(--text-on-brand); font-weight: var(--fw-medium); }
  /* ★ .ch-arw（章条右侧的 › 箭头）已删除：
       点它并不会跳页，只是切换下方的小节列表 —— 箭头会误导成「进入新页面」，
       而且占掉约 15px 宽度。去掉后格子能完整放下 5 字章名。 */

  /* 小节区 */
  .tl-bd { padding: 18px 18px 20px; }
  .tl-bd-head {
    display: flex; align-items: center; gap: 10px;
    padding-bottom: 14px; margin-bottom: 16px;
    border-bottom: 1px solid var(--border-1);
  }
  .tl-bd-head .bh-t { font-size: var(--fs-base); font-weight: var(--fw-bold); color: var(--text-1); }
  .tl-bd-head .bh-c { font-size: var(--fs-xs); color: var(--text-4); }

  /* ---------- ⑥ 副栏卡片 ---------- */
  .tpa-card {
    background: var(--surface-1); border-radius: var(--r-md);
    box-shadow: var(--sh-1); overflow: hidden;
  }
  .tpa-head {
    display: flex; align-items: center; gap: 10px;
    padding: 14px 16px; border-bottom: 1px solid var(--border-1);
  }
  .tpa-head .ah-t { font-size: var(--fs-base); font-weight: var(--fw-bold); color: var(--text-1); }
  .tpa-head .ah-more {
    margin-left: auto; font-size: var(--fs-xs); color: var(--brand); cursor: pointer;
  }
  .tpa-head .ah-more:hover { text-decoration: underline; }

  /* ---------- ⑦ 知识速记卡 / 我的数据卡（副栏）----------
     视觉语言参照 APP 的「数据概览」：浅色底 + 同色系数字。
     ★★ 空态是主场景，不是例外 ★★
        知识速记的真实数据现在是 0/0（教研尚未发布资料），
        所以「资料分类 / 资料总数 都是 0」这一屏才是用户最常看到的，
        设计时必须保证它好看、有话可说，而不是只设计满数据的版本。

     四色全部取自现有令牌，本区块【不新造任何颜色】：
       蓝 --brand-50 / --brand       绿 --success-bg / --success
       红 --danger-bg / --danger      橙 --warning-bg / --warning
     ------------------------------------------------------------ */

  .ks-body { padding: 14px 16px 16px; }

  /* 数字卡：底座（竖向 —— 数字在上、标签在下） */
  .ks-tiles { display: grid; gap: 12px; }
  .ks-tiles.two  { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* ★ minmax(0,1fr) 而非 1fr：4 联卡很窄，数字长了不会被顶开 */
  .ks-tiles.four { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }

  .ks-tile {
    border-radius: var(--r-sm); padding: 13px 10px 12px;
    display: flex; flex-direction: column; align-items: center; gap: 5px;
    text-align: center; cursor: pointer;
    transition: transform var(--dur-fast), box-shadow var(--dur-fast);
  }
  /* ★ 不可点击的静态卡（用户要求）——
     这两张是「资料分类 / 资料总数」的**统计展示**，
     知识速记本身还没有资料（接口未接），点了进去是一个空页面，
     是个死胡同 → 去掉手型光标与悬浮反馈，让它一眼就是"不可点"。
     ★ 注意不能直接给 .ks-tile 去 cursor:pointer —— 同一套样式
       还被「我的错题 / 我的收藏」那些真能点的卡复用着 ✗ */
  .ks-tile.is-static { cursor: default; }
  .ks-tile.is-static:hover { transform: none; box-shadow: none; }
  .ks-tile:hover { transform: translateY(-1px); box-shadow: var(--sh-1); }
  .ks-tile .ic { width: 24px; height: 24px; display: block; flex: none; }
  .ks-tile .ic svg { width: 100%; height: 100%; display: block; }
  .ks-tile .num {
    font-size: 26px; font-weight: var(--fw-bold); line-height: 1.1;
    font-variant-numeric: tabular-nums; letter-spacing: -.5px;
  }
  .ks-tile .lb { font-size: var(--fs-xs); color: var(--text-4); white-space: nowrap; }

  /* 四联紧凑版（我的数据）：数字小一档，不做 hover 位移（4 张一起跳太吵）
     ★ .lb 加省略号兜底：4 联卡很窄（≈97px），万一以后换成长标签，
       宁可截断也不要溢出卡片 —— 溢出会盖到隔壁卡上，很难看。 */
  .ks-tiles.four .ks-tile { padding: 12px 6px 11px; gap: 3px; }
  .ks-tiles.four .ks-tile .num { font-size: 21px; }
  .ks-tiles.four .ks-tile .lb {
    font-size: 12px;
    max-width: 100%;
    overflow: hidden; text-overflow: ellipsis;
  }
  .ks-tiles.four .ks-tile:hover { transform: none; box-shadow: none; }
  /* ★ 刷新中：数字淡淡变虚（切题库 / 切回标签时用）
     目的不是"好看"，而是**避免数字连跳好几次**：
     有了这个态，用户看到的是「旧数字变虚 → 新数字一次到位」，
     而不是「旧值 → 空 → 5 → 3 → 14051」一路闪 ✗ */
  .ks-tiles .ks-tile .num { transition: opacity .18s ease; }
  .ks-tiles.is-loading .ks-tile .num { opacity: .35; }
  .ks-tiles.is-loading .ks-tile { transition: background-color .18s ease; }

  /* 横向版（知识速记）：图标在左，数字 + 标签在右
     高度从 96px 压到 56px，省下的纵向空间留给分类清单 */
  .ks-tiles.row .ks-tile {
    flex-direction: row; align-items: center; text-align: left;
    gap: 10px; padding: 11px 14px;
  }
  .ks-tiles.row .ks-tile .txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
  .ks-tiles.row .ks-tile .num { font-size: 22px; }
  .ks-tiles.row .ks-tile .lb { font-size: var(--fs-xs); }

  /* 四色（全部取自令牌） */
  .ks-tile.blue  { background: var(--brand-50); }
  .ks-tile.blue   .ic, .ks-tile.blue   .num { color: var(--brand); }
  .ks-tile.green { background: var(--success-bg); }
  .ks-tile.green  .ic, .ks-tile.green  .num { color: var(--success); }
  .ks-tile.red   { background: var(--danger-bg); }
  .ks-tile.red    .ic, .ks-tile.red    .num { color: var(--danger); }
  .ks-tile.amber { background: var(--warning-bg); }
  .ks-tile.amber  .ic, .ks-tile.amber  .num { color: var(--warning); }

  /* 分类清单 */
  .ks-list { margin-top: 12px; display: flex; flex-direction: column; }
  .ks-row {
    display: flex; align-items: center; gap: 10px;
    padding: 9px 8px; border-radius: var(--r-xs);
    font-size: var(--fs-sm); color: var(--text-2);
    cursor: pointer; transition: background var(--dur-fast);
  }
  .ks-row:hover { background: var(--brand-50); }
  .ks-row .dot { width: 6px; height: 6px; border-radius: var(--r-full); flex: none; background: var(--brand-300); }
  .ks-row .nm { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .ks-row .ct { font-size: var(--fs-xs); color: var(--text-4); flex: none; font-variant-numeric: tabular-nums; }

  /* 空态：虚线框比实线框更「待人填充」，不会显得是破版 */
  .ks-empty {
    margin-top: 12px; padding: 18px 16px;
    border: 1px dashed var(--border-2); border-radius: var(--r-sm);
    text-align: center; background: var(--surface-hover);
  }
  .ks-empty .art { width: 88px; height: 80px; margin: 0 auto 6px; display: block; }
  .ks-empty .t1 { font-size: var(--fs-sm); color: var(--text-3); font-weight: var(--fw-medium); }
  .ks-empty .t2 { font-size: var(--fs-xs); color: var(--text-4); margin-top: 5px; line-height: 1.7; }

  /* ★ 「学习记录」列表行（.tpa-list / .tpa-row / .ar-nm / .ar-mt）
     与「我的数据」旧四行（.tpa-stats / .st-*）已整体移除 ——
     前者被上面的知识速记卡取代，后者改成了 .ks-tiles.four 四联数字卡。
     这两组类名现无任何使用者，故不保留定义（避免死代码）。 */

  .acc { display: flex; flex-direction: column; gap: 12px; }
  .acc-item { background: var(--surface-1); border-radius: var(--r-md); overflow: hidden; box-shadow: 0 2px 12px rgba(0,0,0,.04); }
  .acc-head { display: flex; align-items: center; gap: 14px; padding: 16px 22px; cursor: pointer; transition: .15s; }
  .acc-head:hover { background: var(--brand-50); }
  .acc-head.open { background: var(--brand-50); border-bottom: 1px solid var(--brand-50); }
  .acc-head .ci { width: 38px; height: 38px; flex: none; border-radius: var(--r-sm); background: var(--brand-50); overflow: hidden; display: flex; align-items: center; justify-content: center; font-size: 16px; color: var(--brand); }
  .acc-head .ci img { width: 100%; height: 100%; object-fit: cover; }
  .acc-head .cn { font-size: 18px; flex: 1; }
  .acc-head .arrow { font-size: 15px; color: var(--text-4); }
  .acc-body { padding: 8px 14px 14px 74px; }
  .sec-row {
    display: flex; align-items: center; gap: 16px;
    padding: 13px 16px; border-radius: var(--r-sm); cursor: pointer; transition: .15s;
    font-size: 16px;
  }
  .sec-row:hover { background: var(--brand-50); }
  .sec-row .sn { flex: 1; }
  .sec-row .sp { font-size: 15px; color: var(--text-4); }
  .sec-row .go { font-size: 15px; color: var(--brand); }

  /* 我的题库 */
  .mybank-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px,1fr)); gap: 16px; }
  .mybank-item { background: var(--surface-1); border-radius: var(--r-md); padding: 20px 22px; box-shadow: 0 2px 12px rgba(0,0,0,.04); }
  .mybank-item .t { font-size: 18px; font-weight: 500; margin-bottom: 10px; }
  .mybank-item .meta { font-size: 15px; color: var(--text-4); display: flex; gap: 14px; flex-wrap: wrap; }
  .tag-trial { font-size: 14px; padding: 2px 9px; border-radius: var(--r-xs); background: var(--warning-note-bg); color: var(--warning); }
  .tag-ok { font-size: 14px; padding: 2px 9px; border-radius: var(--r-xs); background: var(--success-bg); color: var(--success); }
  .tag-exp { font-size: 14px; padding: 2px 9px; border-radius: var(--r-xs); background: var(--danger-bg); color: var(--danger); }

  /* 答题器 */
  /* 答题区：PC 端放开宽度（原来卡在 880px，宽屏下只占 46%） */
  .quiz {
    max-width: 1360px; margin: 0 auto;
    /* ★★ 刷题页固定高度（用户要求：两栏要固定一个高度）★★
       ────────────────────────────────────────────────────────────
       原来两栏是「内容多高就多高」：解析一长、题目一多，
       整块刷题区就往下长，页面越来越长，右侧答题卡也跟着拉长。

       ★★ 为什么不写 calc(100vh - 具体像素) ★★
         .qz-top 和 .qz-foot 的高度【不是定值】—— 它会随
         字号设置（阅读设置里可切大/中/小）、模式标签换行、
         以后增删元素而变化。把它们估成 62 / 68 写进 calc，
         一改就错位，而且错位后很难看出是哪个数不对。
       ★★ 做法：把 .quiz 变成一个「视口高度的纵向 flex 列」，
         顶栏与底栏 flex: none（按自身高度占位），
         中间 .qz-split 用 flex: 1 吃掉剩下的全部高度。
         这样只有两个数需要写死，而它们【本来就写死在样式表里】、
         不随内容变化：顶栏 60px、.content 上下内边距 22/56px。 */
    height: calc(100vh - 60px - 22px - 56px);
    display: flex; flex-direction: column;
  }

  /* ---------- 答题页页头：返回 + 标题 + 进度条 + 统计 + 模式，合并为一条 ----------
     ★ 原来这里是两块：.quiz-bar（返回/标题/题号统计）与 .quiz-modes（模式切换，
       孤零零飘在卡片外面）。合并后省掉整整一行，并且把「第 3 / 12 题」这种
       纯文字换成【看得见的进度条】—— 长卷子做到一半时，进度感比数字更直观。 */
  .qz-top {
    background: var(--surface-1); border-radius: var(--r-md);
    padding: 14px 20px; box-shadow: var(--sh-1);
    display: flex; align-items: center; gap: 18px;
    margin-bottom: 16px;
    /* ★ 在 .quiz 那个「视口纵向 flex 列」里，顶栏按自身高度占位。
       默认的 flex: 0 1 auto 允许收缩 —— 高度紧张时它会被压扁，
       所以必须显式 flex: none。底栏同理。 */
    flex: none;
  }
  .qz-top .bk {
    font-size: 15px; color: var(--text-3); cursor: pointer; flex: none;
    display: inline-flex; align-items: center; gap: 4px;
    padding: 6px 12px; border-radius: var(--r-sm);
    transition: var(--dur-fast);
  }
  .qz-top .bk:hover { background: var(--brand-50); color: var(--brand); }
  .qz-top .tt {
    font-size: 16px; font-weight: var(--fw-bold); color: var(--text-1);
    flex: none; max-width: 340px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

  /* ★ 云端记录状态条：小圆角标签，只在作答页顶部出现
       ok = 云端会话生效；bad = 退回本机；无类 = 快速练习（本机记录） */
  .v2chip {
    flex: none; font-size: 12px; line-height: 1;
    padding: 5px 9px; border-radius: var(--r-full);
    background: var(--surface-2); color: var(--text-3); border: 1px solid var(--border-1);
    white-space: nowrap; cursor: default;
    /* 失败时这里会带后端原话，可能很长 → 截断，完整内容看 title */
    max-width: 260px; overflow: hidden; text-overflow: ellipsis;
  }
  .v2chip.ok  { background: var(--brand-50); color: var(--brand); border-color: var(--brand-200); }
  .v2chip.bad { background: #FFF4E8; color: #B4681C; border-color: #F2D3AC; }

  .qz-top .mid { flex: 1; }

  /* 进度条：宽度由模板按 (qIndex+1)/questions.length 算 */
  .qz-prog { flex: 1; min-width: 0; display: flex; align-items: center; gap: 12px; }
  .qz-prog .track {
    flex: 1; min-width: 60px; height: 6px; border-radius: var(--r-full);
    background: var(--surface-2); overflow: hidden;
  }
  .qz-prog .track i {
    display: block; height: 100%; border-radius: var(--r-full);
    background: linear-gradient(90deg, var(--brand-light), var(--brand));
    transition: width var(--dur-base) var(--ease);
  }
  .qz-prog .num { font-size: 14px; color: var(--text-4); font-variant-numeric: tabular-nums; flex: none; }
  .qz-prog .num b { color: var(--text-1); font-size: 15px; }

  /* ✓ / ✗ 统计（原来是裸文字，现在做成带底色的胶囊，扫一眼就能读数） */
  .qz-stats { display: flex; align-items: center; gap: 8px; flex: none; }
  .qz-stats .s { font-size: 13px; padding: 4px 10px; border-radius: var(--r-full); font-variant-numeric: tabular-nums; }
  .qz-stats .s.ok { background: var(--success-bg); color: var(--success); }
  .qz-stats .s.no { background: var(--danger-bg); color: var(--danger); }
  /* ★ 考试模式用【品牌色中性胶囊】而不是绿/红 ——
     考试全程不判对错，绿红会暗示「已经判过了」。 */
  .qz-stats .s.done { background: var(--brand-50); color: var(--brand); }

  /* 模式切换：从卡片外并进页头 */
  .qz-modes {
    display: flex; gap: 3px; flex: none;
    background: var(--surface-2); padding: 4px; border-radius: var(--r-full);
  }
  .qz-modes span {
    font-size: 14px; color: var(--text-4); cursor: pointer;
    padding: 7px 16px; border-radius: var(--r-full);
    transition: var(--dur-base); white-space: nowrap;
  }
  .qz-modes span.on {
    background: var(--surface-1); color: var(--brand); font-weight: var(--fw-medium);
    box-shadow: 0 2px 8px rgba(var(--brand-rgb), .16);
  }

  .fav-btn { font-size: 22px; cursor: pointer; color: var(--decor-1); transition: .15s; line-height: 1; user-select: none; }
  .fav-btn:hover { color: var(--gold); }
  .fav-btn.on { color: var(--gold); }

  .q-card { background: var(--surface-1); border-radius: var(--r-md); padding: 32px 36px; box-shadow: 0 2px 12px rgba(0,0,0,.04); }

  /* 题号区：一行说清「什么题型、第几题」，收藏从纯★图标改成带文字的胶囊
     （原来只有一个 ★，用户不确定它是不是收藏按钮） */
  .qz-qhead { display: flex; align-items: center; gap: 10px; margin-bottom: 20px; }
  .qz-qhead .ty { font-size: 13px; font-weight: var(--fw-bold); padding: 4px 11px; border-radius: var(--r-xs); background: var(--brand-50); color: var(--brand); }
  .qz-qhead .ty.multi { background: var(--warning-bg); color: var(--warning); }
  .qz-qhead .ty.judge { background: var(--success-bg); color: var(--success); }
  .qz-qhead .idx { font-size: 14px; color: var(--text-4); font-variant-numeric: tabular-nums; }
  .qz-qhead .sp { flex: 1; }
  /* ── 收藏开关（题头）────────────────────────────────────────
     ★★ 用户反馈：「收藏没动效，而且一刷题就闪烁」★★
     ★ 两个根因都在这块：
       ① 模板原本写的是 class="qz-tool"，而 .qz-tool 的样式只写在
          .qz-foot 作用域里 → 题头这个按钮【一条样式都没匹配上】✗
          于是没有 border / 没有 transition / .on 也不生效，
          连 svg 都没尺寸（默认按 300×150 参与布局 → 一颗巨大的空心星）✗
       ② 它在 <div class="qz-body" :key="curQ.id"> 内部 →
          切题时被重建，并跟着 qInNext 的 opacity 0→1 淡入一次 → 每题闪一下 ✗
     ★ 现在：类名归位 + 尺寸写死 + 已经把它搬到 qz-body 外面（见模板注释） */
  .qz-qhead .fav {
    font-size: 13px; color: var(--text-4); cursor: pointer;
    display: inline-flex; align-items: center; gap: 5px;
    padding: 5px 12px; border-radius: var(--r-full);
    border: 1px solid var(--border-2);
    background: var(--surface-1);
    /* 只写过属性名，不写 all —— 避免将来加 padding/width 之类被一起补间 */
    transition: color var(--dur-fast), background-color var(--dur-fast),
                border-color var(--dur-fast), box-shadow var(--dur-fast);
    user-select: none;
  }
  /* ★ svg 必须给尺寸：不给就按默认视口撑开 */
  .qz-qhead .fav svg { width: 15px; height: 15px; flex: none; display: block; }
  /* ★ 文字槽定宽 3em（"已收藏"三个字）——
       文案在「收藏 / 已收藏」之间切换时按钮宽度不变，
       否则旁边的题号会被推一下，看着同样像闪 ✓ */
  .qz-qhead .fav .lb { width: 3em; line-height: 1; }
  .qz-qhead .fav:hover { border-color: var(--gold); color: var(--gold-text); box-shadow: 0 0 0 3px rgba(var(--gold-rgb), .10); }
  /* 状态：已收藏 —— 只改颜色与填充，不播动画（动画归 .pop 管） */
  .qz-qhead .fav.on {
    background: var(--gold-bg); border-color: var(--gold-border);
    color: var(--gold-text); font-weight: var(--fw-medium);
  }
  .qz-qhead .fav.on svg { fill: currentColor; }
  /* 收藏练习里的只读位：说明「这一组本来就是收藏」，不能点 */
  .qz-qhead .fav.ro { cursor: default; color: var(--gold-text); }
  .qz-qhead .fav.ro:hover { border-color: var(--border-2); box-shadow: none; color: var(--gold-text); }
  .qz-qhead .fav.ro svg { fill: currentColor; }

  /* ── 收藏的「按一下」反馈（pop）────────────────────────────
     ★ 只由【点击】打上的 .pop 触发，420ms 后由 app.js 摘掉。
     ★ 不把动画挂在 .on 上：切到一道本来就收藏过的题时 .on 已经存在，
       而 qz-body 会按 :key 重建 → 动画跟着重播一次 → 每题闪一下 ✗
       现在 .on 只负责状态色，.pop 只负责动作，两件事彻底分开 ✓
     ★ 星形绕正中转会显得歪，transform-origin 略下移一档。 */
  @keyframes favPop {
    0%   { transform: scale(1)    rotate(0deg); }
    35%  { transform: scale(1.35) rotate(-10deg); }
    65%  { transform: scale(.90)  rotate(5deg); }
    100% { transform: scale(1)    rotate(0deg); }
  }
  @keyframes favRing {
    0%   { box-shadow: 0 0 0 0 rgba(var(--gold-rgb), .45); }
    100% { box-shadow: 0 0 0 9px rgba(var(--gold-rgb), 0); }
  }
  .fav.pop, .qz-tool.pop { animation: favRing .42s cubic-bezier(.22,.9,.3,1); }
  .fav.pop svg, .qz-tool.pop svg {
    transform-origin: 50% 55%;
    animation: favPop .4s cubic-bezier(.34,1.56,.64,1);
  }
  @media (prefers-reduced-motion: reduce) {
    .fav.pop, .qz-tool.pop, .fav.pop svg, .qz-tool.pop svg { animation: none !important; }
  }

  .qz-stem { font-size: 20px; line-height: 1.9; color: var(--text-1); margin-bottom: 22px; }
  .qz-stem .num { color: var(--text-4); margin-right: 6px; font-variant-numeric: tabular-nums; }
  .qz-opts { display: flex; flex-direction: column; gap: 10px; }
  /* 题目配图：限制最大高度，超长图不撑高页面；contain 保证图完整不被裁 */
  .q-img {
    max-width: 100%; max-height: 320px; width: auto; height: auto;
    object-fit: contain;
    border-radius: var(--r-sm); margin-bottom: 22px; display: block;
  }

  /* ---------- 选项 ----------
     ★ margin-bottom 已去掉，间距改由父级 .qz-opts 的 gap 统一控制
       （原来选项自带 margin-bottom，与容器 gap 混用会互相打架）。
     ★ 字母徽标 26px → 30px 并加粗、描边 1px → 1.5px：
       原来「选中」只靠一圈细描边表示，扫视时很难一眼看出选了哪个。 */
  .opt {
    border: 1.5px solid var(--border-2); border-radius: var(--r-sm);
    padding: 14px 18px; cursor: pointer;
    display: flex; gap: 14px; align-items: flex-start;
    transition: var(--dur-fast); font-size: 17px; line-height: 1.7;
  }
  /* ⚠️ 选项文字必须占满剩余宽度：flex 容器里不设 flex:1 会被压缩到换行 */
  .opt > span:last-child { flex: 1; min-width: 0; word-break: break-word; }

  /* 字母徽标：三态分别是「默认灰底 / hover 稍亮 / 选中实心品牌色」 */
  .opt .k {
    width: 30px; height: 30px; flex: none; border-radius: 50%;
    background: var(--surface-2); color: var(--text-3);
    display: flex; align-items: center; justify-content: center;
    font-size: 15px; font-weight: var(--fw-bold);
    transition: var(--dur-fast);
  }

  /* ★★ hover 与「已选中」必须一眼分得开 ★★
     ────────────────────────────────────────────────────────────
     原来这两条用的是【完全相同】的样式（边框 --brand、背景 --brand-50）：
       .opt:hover  { border-color: var(--brand); background: var(--brand-50); }
       .opt.picked { border-color: var(--brand); background: var(--brand-50); }
     于是考试模式「选完自动跳下一题」之后 —— 鼠标仍停在原来的屏幕位置，
     那一格正好落在新题的 A 选项上 → hover 效果看着就像「A 已被默认选中」。
     （用户报的正是这个现象。它不是状态错了，是两种状态长得一样。）

     ★ 分工重新划清：
       hover  —— 只提示「这里可以点」：中性浅底 + 浅品牌边框，弱
       picked —— 才是真的选上了：品牌底 + 深品牌边框 + 实心字母徽标，强
     ★ :not(.picked) 是必要的 —— 选中的选项在被 hover 时也该保持「已选中」的样子。 */
  .opt:hover:not(.locked):not(.picked) {
    border-color: var(--brand-200);
    background: var(--surface-2);
  }
  .opt:hover:not(.locked):not(.picked) .k { background: var(--border-1); }

  .opt.picked { border-color: var(--brand); background: var(--brand-50); }
  /* 选中的徽标再加一圈描边，与 hover 的浅灰徽标拉开更大差距 */
  .opt.picked .k { background: var(--brand); color: var(--surface-1); box-shadow: 0 0 0 3px var(--brand-100); }

  /* ★★ 「已选」文字标记：只出现在【答案还没揭晓】的选中项上 ★★
     ────────────────────────────────────────────────────────────
     用户反馈：「考试模式回看时，看不出自己到底选没选」—— 之前选中态只有
     一圈品牌色边框 + 实心字母徽标，而 hover 也有一圈浅品牌边框，
     回看时（尤其鼠标还在选项上）两种情况很难一眼分开。

     ★ 适用范围：picked 且【没被锁定】。这一组合只在
       「考试模式未交卷」和「练习模式多选题还没点确认」时出现 ——
       都是答案尚未揭晓、确实需要告诉用户"你选了什么"的场合。
     ★ 已判定 / 背题模式都会带 locked（optClass 里 done → locked），
       所以这条不会和 .opt.right / .opt.wrong 的「正确答案 / 你的选择」并存，
       也不会出现一行三个标记。 */
  .opt.picked:not(.locked)::after {
    content: '✓ 已选'; flex: none; align-self: center;
    font-size: 13px; font-weight: var(--fw-bold); color: var(--brand);
    padding-left: 10px; white-space: nowrap;
  }
  .opt.right { border-color: var(--success); background: var(--success-bg); }
  .opt.right .k { background: var(--success); color: var(--surface-1); }
  .opt.wrong { border-color: var(--danger); background: var(--danger-bg); }
  .opt.wrong .k { background: var(--danger); color: var(--surface-1); }
  .opt.locked { cursor: default; }
  /* ★ 已移除 .opt.locked:hover / .locked.picked:hover / .locked.right:hover / .locked.wrong:hover
     四条规则。它们原本是「已判定 / 背题锁定态悬停时把颜色复位」的补丁，
     因为当时的 hover 是一条无差别的 .opt:hover。
     现在 hover 自带 :not(.locked):not(.picked) 守卫，锁定态根本命中不到它，
     这四条补丁就失去作用了 —— 留着反而是「两套机制在管同一件事」。 */

  /* ★ 对 / 错标记：纯 CSS 伪元素实现，不需要改 JS。
       optClass() 已经把语义算好了 —— right = 正确选项、wrong = 选了但错，
       所以这里直接按类挂标记即可。
       ::after 是伪元素，不影响 :last-child 的判定，选项文字仍是最后一个真实子元素。
       加 nowrap 但配 flex:none：窄屏时压缩的是文字（flex:1 min-width:0），标记不换行。 */
  .opt.right::after {
    content: '✓ 正确答案'; flex: none; align-self: center;
    font-size: 13px; font-weight: var(--fw-bold); color: var(--success);
    padding-left: 10px; white-space: nowrap;
  }
  .opt.wrong::after {
    content: '✗ 你的选择'; flex: none; align-self: center;
    font-size: 13px; font-weight: var(--fw-bold); color: var(--danger);
    padding-left: 10px; white-space: nowrap;
  }

  .judge-box { margin-top: 22px; border-radius: var(--r-md); padding: 20px 24px; font-size: 16px; line-height: 1.85; }
  .judge-box.ok { background: var(--success-bg); border: 1px solid var(--success-border); }
  .judge-box.no { background: var(--danger-bg); border: 1px solid var(--danger-border); }
  /* ★ 背题模式：没有对错可判，所以用【中性色】——
     沿用 ok/no 的绿红会让人以为「这题我答对了/答错了」，
     而背题模式根本没有作答这一步。这里用品牌浅底，读起来是「讲解」而不是「批改」。 */
  .judge-box.recite { background: var(--brand-50); border: 1px solid var(--brand-200); }
  .judge-box.recite .res { color: var(--brand); font-size: 16px; }
  .judge-box .res { font-size: 18px; font-weight: 600; margin-bottom: 10px; }
  .judge-box.ok .res { color: var(--success); }
  .judge-box.no .res { color: var(--danger); }
  .judge-box .ans { font-size: 16px; margin-bottom: 12px; color: var(--text-3); }
  .judge-box .ans b { color: var(--text-1); font-size: 18px; }
  .judge-box .an-t { font-size: 15px; color: var(--text-4); margin-bottom: 6px; }
  /* ★★ 解析正文：限高 + 内部滚动 ★★
     （用户反馈：解析太多了就不好看了）

     ★ 为什么【只限正文、不限整个 .judge-box】：
       上面那几行（回答正确 / 正确答案是 X / 答题技巧）是固定几行的判定结论，
       必须始终可见；真正会失控的只有下面这段法规正文。
       所以只给正文封顶，结论区保持常驻 —— 一进来就能看到对不对、答案是什么。

     ★ 上限 240px ≈ 8 行（16px × 1.9 行高 ≈ 30px/行）——
       再多就滚。一条几百字的法规解析不会再把整页拉到很长。

     ★ overscroll-behavior: contain：解析里滚到底不要带动整页 ——
       用户想继续读解析，结果页面被滚跑了是很恼人的。
     ★ 细滚动条 + 右侧留白：正文区很宽，系统粗滚动条在这里格外扎眼，
       还会压到最后一行文字。 */
  .judge-box .an-c {
    font-size: 16px; color: var(--text-2); line-height: 1.9; white-space: pre-wrap;
    max-height: 240px;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    padding-right: 4px;
  }
  .judge-box .an-c::-webkit-scrollbar { width: 6px; }
  .judge-box .an-c::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 3px; }
  .judge-box .an-c::-webkit-scrollbar-track { background: transparent; }
  .sync-tip { font-size: 14px; color: var(--text-4); margin-top: 12px; }

  /* 背题模式的引导条：说明自评的默认规则（不点就翻页 = 默认记住了） */
  .recite-hint {
    margin-top: 18px; padding: 11px 16px;
    border-radius: var(--r-sm);
    background: var(--brand-50); border: 1px dashed var(--brand-200);
    font-size: 14px; color: var(--brand); line-height: 1.7;
  }
  .recite-hint b { color: var(--brand); }

  .quiz-foot { display: flex; gap: 14px; margin-top: 24px; align-items: center; }
  .qbtn { height: 42px; padding: 0 28px; border-radius: var(--r-sm); border: 1px solid var(--border-2); background: var(--surface-1); color: var(--text-2); font-size: 16px; cursor: pointer; transition: .15s; }
  .qbtn:hover:not(:disabled) { border-color: var(--brand); color: var(--brand); }
  .qbtn:disabled { opacity: .45; cursor: not-allowed; }
  .qbtn.primary { background: var(--brand); border-color: var(--brand); color: var(--surface-1); flex: 1; }
  .qbtn.primary:hover:not(:disabled) { opacity: .9; color: var(--surface-1); }

  /* ★ 简答题的提交行：按钮右边要放「✓ 已提交」提示，
     所以这一处不撑满（.qbtn.primary 默认 flex:1） */
  .sh-foot .qbtn.primary { flex: none; }
  .sh-done { font-size: 14px; font-weight: var(--fw-bold); color: var(--success); }

  .result-card { background: var(--surface-1); border-radius: var(--r-md); padding: 48px; text-align: center; box-shadow: 0 2px 12px rgba(0,0,0,.04); }
  .result-card h2 { font-size: 28px; margin-bottom: 24px; }
  .result-card .big { font-size: 40px; font-weight: 600; color: var(--success); margin-bottom: 8px; }
  .result-card .sub { font-size: 16px; color: var(--text-4); margin-bottom: 28px; }
  .result-card .nums { display: flex; justify-content: center; gap: 60px; margin-bottom: 32px; }
  .result-card .nums .n { font-size: 28px; font-weight: 600; }
  .result-card .nums .l { font-size: 15px; color: var(--text-4); margin-top: 4px; }

  /* ========== 通用：页面标题栏 ========== */
  .page-head { display: flex; align-items: center; gap: 14px; margin-bottom: 22px; }
  .page-head .bk { font-size: 16px; color: var(--text-3); cursor: pointer; }
  .page-head .bk:hover { color: var(--brand); }
  .page-head .sp { flex: 1; }
  .page-head .more { font-size: 20px; color: var(--text-4); cursor: pointer; letter-spacing: 2px; }

  /* ========== 公告详情 ========== */
  .notice-page { background: var(--surface-1); border-radius: var(--r-md); padding: 34px 40px; box-shadow: 0 4px 22px rgba(var(--brand-rgb),.08); }
  .notice-page h1 { font-size: 24px; font-weight: 600; margin-bottom: 10px; }
  .notice-page .time { font-size: 15px; color: var(--text-4); padding-bottom: 20px; margin-bottom: 22px; border-bottom: 1px solid var(--surface-2); }

  /* 公告配图：按原图宽度上限显示（不放大，避免发虚） */
  .notice-img {
    display: block;
    width: 100%; max-width: 686px; height: auto;
    margin: 0 0 26px;
    border-radius: var(--r-md);
  }

  .notice-page .body { font-size: 16px; line-height: 2.1; color: var(--text-2); white-space: pre-wrap; }

  /* ========== 调整目标 ========== */
  /* ---- 调整目标弹窗：紧凑表单（不用卡片堆叠，省一半高度）---- */
  .goal-modal .modal-body { padding: 16px 22px; }

  .gb-row {
    display: flex; align-items: center; gap: 14px;
    min-height: 48px; padding: 2px 0;
  }
  .gb-k { width: 86px; flex: none; font-size: 15px; color: var(--text-3); }
  .gb-k i { color: var(--warning); font-style: normal; margin-left: 2px; }
  .gb-v {
    font-size: 15px; color: var(--text-1); flex: 1; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

  .gb-input {
    flex: 1; min-width: 0; height: 36px; padding: 0 12px;
    border: 1px solid var(--border-2); border-radius: var(--r-sm);
    background: var(--surface-1); color: var(--text-1);
    font-family: inherit; font-size: 15px; outline: none;
    transition: var(--dur-fast);
  }
  .gb-input:focus { border-color: var(--brand); }
  .gb-input::placeholder { color: var(--text-placeholder); }

  .gb-bank {
    background: var(--surface-2); border-radius: var(--r-sm);
    padding: 0 14px; min-height: 44px; cursor: pointer;
    transition: var(--dur-fast);
  }
  .gb-bank:hover { background: var(--brand-50); }
  .gb-bank .gb-k { width: auto; color: var(--text-4); }
  .gb-sw { color: var(--brand); font-size: 16px; flex: none; }

  .gb-line { height: 1px; background: var(--border-1); margin: 12px 0; }

  /* 总题量：可直接输入的框（原来只是纯文本，看不出能改） */
  .gb-num {
    width: 88px; height: 34px; padding: 0; text-align: center;
    border: 1px solid var(--border-2); border-radius: var(--r-sm);
    background: var(--surface-1); color: var(--text-1);
    font-family: inherit; font-size: 16px; font-weight: var(--fw-bold);
    font-variant-numeric: tabular-nums;
    outline: none; transition: var(--dur-fast);
    -moz-appearance: textfield;
  }
  .gb-num::-webkit-outer-spin-button,
  .gb-num::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
  .gb-num:hover { border-color: var(--brand-300); }
  .gb-num:focus { border-color: var(--brand); background: var(--brand-50); }

  /* 考试日期：带日历图标的可点按钮（原来只是纯文本） */
  .gb-date {
    display: inline-flex; align-items: center; gap: 7px;
    height: 34px; padding: 0 13px;
    border: 1px solid var(--border-2); border-radius: var(--r-sm);
    background: var(--surface-1); color: var(--text-1);
    font-size: 15px; cursor: pointer; transition: var(--dur-fast);
  }
  .gb-date svg { width: 14px; height: 14px; color: var(--text-4); flex: none; }
  .gb-date:hover { border-color: var(--brand-300); color: var(--brand); }
  .gb-date:hover svg { color: var(--brand); }
  .gb-date.muted { color: var(--text-placeholder); }

  .gb-note { font-size: 14px; color: var(--text-4); line-height: 1.6; margin-top: 4px; }

  .gb-est {
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    font-size: 15px; color: var(--text-3);
  }
  .gb-est-t { font-size: 14px; color: var(--text-4); margin-right: 6px; }
  .ge-item b { color: var(--brand); font-weight: var(--fw-bold); }
  .ge-dot { color: var(--decor-1); }

  /* 弹窗内紧凑化：原整页版本的大留白在弹窗里太占地方 */
  .goal-hero {
    background: var(--surface-1); border: 1px solid var(--border-1);
    border-radius: var(--r-md); padding: 16px 20px;
    display: flex; align-items: center; margin-bottom: 12px;
  }
  .goal-hero .big { font-size: 28px; font-weight: var(--fw-bold); color: var(--text-1); line-height: 1.1; }
  .goal-hero .lb { font-size: 14px; color: var(--text-4); margin-top: 3px; }
  .goal-hero .ico { margin-left: auto; width: 44px; height: 44px; flex: none; }

  /* 考试名称：可编辑输入框 */
  .goal-input {
    flex: 1; min-width: 0; height: 36px; padding: 0 12px;
    border: 1px solid var(--border-2); border-radius: var(--r-sm);
    background: var(--surface-1); color: var(--text-1);
    font-family: inherit; font-size: 15px; outline: none;
    transition: var(--dur-fast);
  }
  .goal-input:focus { border-color: var(--brand); }
  .goal-input::placeholder { color: var(--text-placeholder); }

  .goal-card {
    background: var(--surface-1); border-radius: var(--r-md); padding: 20px 24px;
    box-shadow: 0 4px 22px rgba(var(--brand-rgb),.08); margin-bottom: 16px;
  }
  .goal-card .card-t { font-size: 16px; font-weight: 600; margin-bottom: 12px; }

  .bank-pick-row {
    display: flex; align-items: center; gap: 12px;
    padding: 14px 18px; border-radius: var(--r-md); background: var(--surface-2);
    font-size: 16px; cursor: pointer; transition: .15s; margin-bottom: 10px;
  }
  .bank-pick-row:hover { background: var(--brand-50); }
  .bank-pick-row .nm { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-1); }
  .bank-pick-row .sw { color: var(--brand); font-size: 18px; }

  .goal-row {
    display: flex; align-items: center; gap: 16px;
    padding: 17px 0; border-top: 1px solid var(--surface-2); font-size: 16px;
  }
  .goal-row .k { width: 130px; color: var(--text-4); flex: none; }
  .goal-row .v { flex: 1; color: var(--text-1); }
  .goal-row .v.muted { color: var(--text-placeholder); cursor: pointer; }
  .goal-row .v.muted:hover { color: var(--brand); }

  .goal-tips { font-size: 14px; color: var(--text-4); line-height: 1.95; }
  .goal-tips .t { display: flex; gap: 7px; }
  .goal-tips .t i { font-style: normal; color: var(--decor-1); flex: none; }

  .est-row { display: flex; gap: 14px; margin-top: 14px; }
  .est {
    flex: 1; border: 1px solid var(--border-2); border-radius: var(--r-md);
    padding: 20px 12px; text-align: center; background: var(--surface-2);
  }
  .est .n { font-size: 28px; font-weight: 700; color: var(--brand); }
  .est .l { font-size: 14px; color: var(--text-4); margin-top: 7px; }
  .est:nth-child(2) .n { color: var(--warning); }
  .est:nth-child(3) .n { color: var(--success); }

  /* 数字步进器 */
  .stepper { display: flex; align-items: center; gap: 10px; }
  .stepper .sbtn {
    width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--border-2);
    background: var(--surface-1); color: var(--text-3); font-size: 18px; line-height: 1;
    cursor: pointer; display: flex; align-items: center; justify-content: center;
    user-select: none; transition: .15s;
  }
  .stepper .sbtn:hover { border-color: var(--brand); color: var(--brand); background: var(--brand-50); }
  .stepper .sval {
    min-width: 80px; text-align: center; height: 36px; line-height: 36px;
    border: 1px solid var(--border-2); border-radius: var(--r-sm); font-size: 16px; font-weight: 500;
  }

  /* ========== 弹层（.mask + .sheet：题库选择器等）========== */
  .mask {
    position: fixed; inset: 0; background: rgba(var(--text-1-rgb),.35);
    display: flex; align-items: center; justify-content: center;
    z-index: var(--z-mask); padding: 24px;
    animation: maskIn var(--dur-fast) ease both;    /* 遮罩淡入 */
  }
  .sheet {
    width: 520px; max-height: 86vh; overflow-y: auto;
    background: var(--surface-1); border-radius: var(--r-lg); padding: 26px 30px 28px;
    box-shadow: var(--sh-modal);
    /* 与 .modal 统一的入场动效：从下浮起 + 轻微回弹 */
    animation: modalIn var(--dur-base) cubic-bezier(.34, 1.4, .64, 1) both;
  }
  .sheet-head { display: flex; align-items: center; margin-bottom: 22px; }
  .sheet-head .t { font-size: 18px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
  .sheet-head .t svg { flex: none; }
  .sheet-head .x { margin-left: auto; font-size: 24px; color: var(--text-4); cursor: pointer; line-height: 1; }
  .sheet-head .x:hover { color: var(--text-3); }

  /* ---------- 题量不足确认弹窗（重设计）----------
     ★★ 旧版为什么「看着不舒服」★★
       ① 关键数字埋在一句话里：「一共只有 5 道题，少于你选择的 20 题」——
          要读完整句才能比出差距，扫不出来；
       ② 又套了一层 line-height: 2，一段文字变成一堵墙；
       ③ 复用了 .goal-tips（那是别处的提示条样式）再靠内联样式去改，
          而 .sheet 自己已经有 padding 26/30/28 与 width 520 ——
          三套样式互相打架，所以怎么调都别扭。

     ★★ 新版怎么排 ★★
       把「可刷 / 你选的数量」两个数字并排成两块，中间用「<」连接 ——
       一眼看出差多少，不必再读句子。
       · 左侧「可刷」用品牌色强调 —— 它是【实际会发生什么】
       · 右侧「你选的数量」用中性灰 —— 它是【原本想要什么】，不是结果
         两侧字号相同、靠颜色分主次，高度也就自然齐平。
       ★ 按钮文案直接写出结果（「按 4 道开始」），点之前就知道会发生什么。 */
  .shortage-lead {
    text-align: center; font-size: 14px; color: var(--text-4);
    margin-bottom: 18px;
  }
  .shortage-lead b { color: var(--text-2); font-weight: var(--fw-medium); }

  .shortage-cmp {
    display: flex; align-items: center; justify-content: center; gap: 14px;
    margin-bottom: 20px;
  }
  .shortage-cell {
    flex: 1; max-width: 152px; padding: 15px 12px 13px;
    border-radius: var(--r-md); text-align: center;
  }
  .shortage-cell .n {
    font-size: 32px; font-weight: var(--fw-bold); line-height: 1.1;
    font-variant-numeric: tabular-nums; letter-spacing: -.5px;
  }
  .shortage-cell .u { font-size: 12px; margin-top: 5px; }
  /* 可刷的量：品牌色 —— 这是用户真正拿得到的东西 */
  .shortage-cell.have { background: var(--brand-50); border: 1px solid var(--brand-200); }
  .shortage-cell.have .n, .shortage-cell.have .u { color: var(--brand); }
  /* 所选数量：中性灰 —— 它只是「原本想要」 */
  .shortage-cell.want { background: var(--surface-2); border: 1px solid var(--border-1); }
  .shortage-cell.want .n, .shortage-cell.want .u { color: var(--text-4); }

  /* 中间的关系符：明确是「少于」而不是并列 */
  .shortage-op { flex: none; font-size: 22px; font-weight: var(--fw-bold); color: var(--text-4); }

  .shortage-ask {
    text-align: center; font-size: 15px; color: var(--text-2); line-height: 1.6;
  }
  .shortage-ask b { color: var(--brand); font-variant-numeric: tabular-nums; }

  .shortage-acts { display: flex; gap: 12px; margin-top: 22px; }
  .shortage-acts .qbtn { flex: 1; height: 44px; font-size: 15px; }

  /* ---------- 检索没结果的弹窗 ----------
     用户要求：如果没有搜到给个弹窗。
     ★ 三段式，信息优先级从高到低：
       ① .miss-lead  —— 一句话结论（没匹配到什么），字最大最重
       ② .miss-scope —— 【已检索范围】，这是这个弹窗存在的理由：
          把「范围限制」如实摊开，用户才知道「不是全站没有，只是这里没有」
       ③ .miss-hint  —— 下一步怎么办
     ★ 范围那块用浅底 + 等宽数字，与上下两段文字区分开 ——
       它是「事实」，其余两段是「话」。 */
  .miss-lead { font-size: 15px; color: var(--text-2); line-height: 1.7; margin-bottom: 16px; }
  .miss-lead b { color: var(--text-1); font-weight: var(--fw-bold); }

  .miss-scope {
    background: var(--surface-2); border: 1px solid var(--border-1);
    border-radius: var(--r-sm); padding: 11px 14px; margin-bottom: 14px;
  }
  .miss-scope .k { font-size: 12px; color: var(--text-4); margin-bottom: 4px; }
  .miss-scope .v { font-size: 14px; color: var(--text-1); font-weight: var(--fw-medium);
                   line-height: 1.7; font-variant-numeric: tabular-nums; }

  .miss-hint { font-size: 13px; color: var(--text-4); line-height: 1.95; }

  /* 交卷 / 退出的二次确认：正文用警示色，一眼看出是「有后果」的操作 */
  .quiz-ask-body {
    font-size: 14px; line-height: 1.95; color: var(--text-2);
    /* ★ 用 note 那一对（琥珀底 + 琥珀边）—— 与标题上的警示图标同色系，
       也和「题量不足」弹窗的提示块保持一致。
       站内没有 --warning-border，别硬编一个不存在的令牌。 */
    background: var(--warning-note-bg); border: 1px solid var(--warning-note-bd);
    border-radius: var(--r-sm); padding: 12px 14px;
  }
  .quiz-ask-acts { display: flex; gap: 12px; margin-top: 22px; }
  /* ★ nowrap + 左右收窄：这个弹窗现在可能是【三档】（考试模式没交卷就走），
     460px 宽的框里三个按钮平分，字号 15px 时「交卷并离开」会被折成三行 ✗ */
  .quiz-ask-acts .qbtn { flex: 1; height: 44px; font-size: 15px; white-space: nowrap; padding: 0 10px; }

  .miss-acts { display: flex; gap: 12px; margin-top: 22px; }
  .miss-acts .qbtn { flex: 1; height: 44px; font-size: 15px; }

  .btn-save {
    width: 100%; height: 44px; margin-top: 24px;
    border: 0; border-radius: 999px;
    background: linear-gradient(90deg, var(--brand-light), var(--brand));
    color: var(--surface-1); font-size: 16px; font-weight: 500; font-family: inherit;
    cursor: pointer; transition: .2s;
    box-shadow: 0 8px 20px rgba(var(--brand-rgb),.26);
  }
  .btn-save:hover { transform: translateY(-1px); }

  /* 日历 */
  .cal-head { display: flex; align-items: center; justify-content: center; gap: 20px; margin: 6px 0 14px; }
  .cal-head .m { font-size: 16px; font-weight: 600; min-width: 130px; text-align: center; }
  .cal-head .nav { font-size: 18px; color: var(--text-4); cursor: pointer; padding: 2px 6px; user-select: none; }
  .cal-head .nav:hover { color: var(--brand); }
  .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
  .cal-grid .wd { text-align: center; font-size: 14px; color: var(--text-4); padding: 8px 0; }
  .cal-cell {
    height: 40px; display: flex; align-items: center; justify-content: center;
    font-size: 16px; border-radius: var(--r-sm); cursor: pointer; transition: .12s; color: var(--text-2);
  }
  .cal-cell:hover { background: var(--brand-50); }
  .cal-cell.empty { cursor: default; }
  .cal-cell.empty:hover { background: transparent; }
  .cal-cell.today { color: var(--brand); font-weight: 600; }
  .cal-cell.on { background: linear-gradient(90deg, var(--brand-light), var(--brand)); color: var(--surface-1); font-weight: 500; }
  .cal-cell.on:hover { background: linear-gradient(90deg, var(--brand-light), var(--brand)); }

  /* 题库选择列表 */
  .bank-pick-item {
    display: flex; align-items: center; gap: 12px;
    padding: 16px 18px; border-radius: var(--r-md); cursor: pointer;
    font-size: 16px; transition: .15s; margin-bottom: 6px;
    border: 1px solid transparent; color: var(--text-2);
  }
  .bank-pick-item:hover { background: var(--surface-2); }
  .bank-pick-item.on { background: var(--brand-50); border-color: var(--brand-200); color: var(--brand); font-weight: 500; }
  .pick-collapse { text-align: center; font-size: 15px; color: var(--text-4); margin-top: 16px; cursor: pointer; }
  .pick-collapse:hover { color: var(--brand); }

  /* 时长标签 */
  .dur-tags { display: flex; gap: 10px; flex-wrap: wrap; }
  .dur-tags span {
    padding: 9px 18px; border-radius: 999px; font-size: 15px;
    background: var(--surface-2); color: var(--text-3); cursor: pointer; transition: .15s;
    border: 1px solid transparent;
  }
  .dur-tags span:hover { background: var(--brand-50); color: var(--brand); }
  .dur-tags span.on { background: var(--brand-50); color: var(--brand); font-weight: 500; border-color: var(--brand-200); }

  /* 首页小节标题（平台公告） */
  .sec-label {
    display: flex; align-items: center; gap: 7px;
    font-size: 15px; color: var(--brand); font-weight: 500; margin-bottom: 10px;
  }
  .sec-label svg { width: 15px; height: 15px; }
  /* ══════ 通用小按钮（.adj-btn）══════
     ★★ 这里原来【只写了 `.prog-head .adj-btn`】✗
       那是"某个容器里的后代"选择器 ✓ 只有在 .prog-head 里面才生效 ✓
       而 .adj-btn 在别处也用了：
         · 我的错题页「开始练习」✓
         · 我的收藏页「开始练习」✓
         · 邀请页「开始分享」✓
         · **激活码兑换页「兑换」** ✓ ← 用户截图就是这个
       → 那些地方完全没有样式 ✓ 显示成浏览器默认的灰方块 ✓
         用户看到的"按钮风格不对"就是这个 ✓

     ★ 做法：补一条【不依赖容器】的基础样式 ✓
       让它在任何地方都长得像站内的按钮 ✓
       原来 `.prog-head .adj-btn` 那条保留 ✓ —— 它会在特定容器里覆盖细节 ✓
     ★ 用品牌蓝实心 —— 这几个都是该页的主要动作 ✓
       和「保存信息」「确认保存」那些主按钮同一套语言 ✓ */
  .adj-btn {
    height: 40px; padding: 0 22px;
    border: 1px solid var(--brand);
    border-radius: var(--r-sm);
    background: var(--brand); color: #fff;
    font-size: 14px; font-weight: var(--fw-medium);
    font-family: inherit;
    cursor: pointer;
    transition: var(--dur-fast);
    white-space: nowrap;
  }
  /* 锁定的 .adj-btn：不用灰字 ✗（会让人以为"不能点"）
     改成"浅底 + 品牌色边框"✓ —— 一看就知道是"要开通才能用"，而且点了会说原因 ✓ */
  .adj-btn.locked {
    background: var(--surface-1);
    border-color: var(--brand);
    color: var(--brand);
  }
  .adj-btn.locked:hover:not(:disabled) { background: var(--brand-50); opacity: 1; }

  .adj-btn:hover:not(:disabled) { opacity: .92; }
  .adj-btn:active:not(:disabled) { transform: translateY(1px); }
  .adj-btn:disabled { opacity: .45; cursor: not-allowed; }

  .prog-head .adj-btn {
    margin-left: auto; height: 30px; padding: 0 16px;
    border: 0; border-radius: 999px; font-size: 15px; font-family: inherit;
    background: linear-gradient(90deg, var(--brand-light), var(--brand)); color: var(--surface-1); cursor: pointer;
    box-shadow: 0 4px 12px rgba(var(--brand-rgb),.24);
  }
  .prog-head .adj-btn:hover { opacity: .92; }
  .prog-sub { font-size: 14px; color: var(--text-4); margin-top: 6px; margin-bottom: 18px; display: flex; gap: 16px; }
  .prog-sub b { color: var(--brand); }

  /* ========== 错题本 / 练习设置（按设计图）========== */
  .stat-two { display: flex; gap: 64px; margin-top: 10px; }
  .stat-two .it .n { font-size: 32px; font-weight: 700; color: var(--brand); line-height: 1.1; }
  .stat-two .it .l { font-size: 14px; color: var(--text-4); margin-top: 7px; }

  .seg-tabs { display: flex; gap: 28px; border-bottom: 1px solid var(--surface-2); margin-bottom: 6px; }
  .seg-tabs span { padding: 13px 2px; font-size: 16px; color: var(--text-4); cursor: pointer; position: relative; }
  .seg-tabs span.on { color: var(--brand); font-weight: 600; }
  .seg-tabs span.on::after {
    content: ''; position: absolute; left: 0; right: 0; bottom: -1px;
    height: 2px; border-radius: var(--r-xs); background: linear-gradient(90deg, var(--brand-light), var(--brand));
  }

  /* ---------- 错题本：题库列表卡 ----------
     ★ 用户反馈：原来这几行是「裸行」直接铺在页面底色上 ✗
       行间只有一条极淡的分隔线，整块显得空，也看不出哪一段能点 ✗
     ★ 现在收进一张白卡：圆角 + 轻投影 + 行间分隔 ✓
       并且【整行都是点击区】（hover 变浅蓝底 + 题库名转品牌色）✓
       —— 点击热区从「去查看」那 3 个字扩大到整行 ✓
     ★ 空态 / 加载态也画在卡里（.list-empty），不出现「一片空白」✓ */
  .list-card {
    background: var(--surface-1);
    border-radius: var(--r-md);
    box-shadow: 0 4px 22px rgba(var(--brand-rgb), .08);
    overflow: hidden;          /* 让 hover 底色不越出圆角 */
    margin-bottom: 16px;
  }
  .list-row-s {
    display: flex; align-items: center; gap: 16px;
    padding: 19px 24px;
    border-bottom: 1px solid var(--border-1);
    font-size: 16px; cursor: pointer;
    transition: background var(--dur-fast);
  }
  .list-row-s:last-child { border-bottom: 0; }
  .list-row-s:hover { background: var(--brand-50); }
  .list-row-s .nm {
    flex: 1; min-width: 0; color: var(--text-1);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    transition: color var(--dur-fast);
  }
  .list-row-s:hover .nm { color: var(--brand); }
  .list-row-s .sub { font-size: 14px; color: var(--text-4); flex: none; }
  .list-row-s .sub.wait { color: var(--warning); }
  .list-row-s .go {
    font-size: 15px; color: var(--brand); flex: none;
    padding: 7px 18px; border-radius: 999px; background: var(--brand-50);
    transition: background var(--dur-fast);
  }
  /* 整行 hover 时把按钮也一起加深 —— 视觉上「行」和「动作」是一件事 */
  .list-row-s:hover .go { background: var(--brand-100); }
  /* 卡内的空态 / 加载态：与列表同样的左右内边距，不破坏卡片轮廓 */
  .list-empty { padding: 36px 24px; text-align: center; font-size: 15px; color: var(--text-4); }

  .review-card {
    background: linear-gradient(135deg, var(--brand-light), var(--brand)); color: var(--surface-1);
    border-radius: var(--r-md); padding: 26px 30px; margin-bottom: 16px;
    box-shadow: 0 10px 26px rgba(var(--brand-rgb),.22);
  }
  .review-card .t { font-size: 20px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
  .review-card .t .tg { font-size: 14px; background: rgba(255,255,255,.22); padding: 3px 11px; border-radius: 999px; font-weight: 400; }
  .review-card .row { font-size: 15px; opacity: .94; margin-top: 14px; }
  .review-card .row b { font-weight: 500; }

  .wq-card {
    background: var(--surface-1); border-radius: var(--r-md); padding: 20px 24px;
    box-shadow: 0 3px 18px rgba(var(--brand-rgb),.07); margin-bottom: 14px;
  }
  .wq-head { display: flex; align-items: center; gap: 9px; margin-bottom: 13px; font-size: 14px; }
  .wq-head .tag { background: var(--brand-50); color: var(--brand); padding: 3px 10px; border-radius: var(--r-xs); }
  .wq-head .tag.ok { background: var(--success-bg); color: var(--success); }
  .wq-head .tag.wait { background: var(--warning-bg); color: var(--warning); }
  .wq-head .meta { color: var(--text-4); }
  .wq-head .sp { flex: 1; }
  .wq-head .out { color: var(--text-4); cursor: pointer; }
  .wq-head .out:hover { color: var(--danger); }
  .wq-stem { font-size: 16px; line-height: 1.85; color: var(--text-1); margin-bottom: 13px; }
  .wq-ans { font-size: 15px; color: var(--text-4); }
  .wq-ans b { color: var(--success); }
  .wq-ans s { color: var(--danger); text-decoration: none; }

  /* 练习设置 */
  .pick-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  .pick-box {
    border: 1.5px solid var(--border-2); border-radius: var(--r-md); padding: 16px 18px;
    cursor: pointer; transition: .15s; display: flex; align-items: center; gap: 13px; background: var(--surface-1);
  }
  .pick-box:hover { border-color: var(--brand-200); background: var(--surface-2); }
  .pick-box.on { border-color: var(--brand); background: var(--brand-50); }
  .pick-box .ic { width: 38px; height: 38px; border-radius: var(--r-sm); display: flex; align-items: center; justify-content: center; flex: none; }
  .pick-box .ic svg { width: 19px; height: 19px; }
  .pick-box .nm { font-size: 16px; font-weight: 500; color: var(--text-1); }
  .pick-box .ds { font-size: 14px; color: var(--text-4); margin-top: 4px; }
  .pick-box .ck { margin-left: auto; color: var(--brand); font-size: 18px; flex: none; }

  .pick-list { display: flex; flex-direction: column; gap: 10px; }
  .pick-line {
    border: 1.5px solid var(--border-2); border-radius: var(--r-md); padding: 16px 18px;
    cursor: pointer; transition: .15s; display: flex; align-items: center; gap: 14px; background: var(--surface-1);
  }
  .pick-line:hover { border-color: var(--brand-200); background: var(--surface-2); }
  .pick-line.on { border-color: var(--brand); background: var(--brand-50); }
  .pick-line .ic { width: 38px; height: 38px; border-radius: var(--r-sm); display: flex; align-items: center; justify-content: center; flex: none; }
  .pick-line .ic svg { width: 19px; height: 19px; }
  .pick-line .nm { font-size: 16px; font-weight: 500; color: var(--text-1); }
  .pick-line .ds { font-size: 14px; color: var(--text-4); margin-top: 4px; }
  .pick-line .ck { margin-left: auto; color: var(--brand); font-size: 18px; flex: none; }

  .note-bar { display: flex; align-items: center; gap: 8px; font-size: 14px; color: var(--text-4); margin: 22px 0 16px; }
  .note-bar b { color: var(--brand); font-weight: 500; }

  /* 学习趋势柱状图 */
  .chart { display: flex; align-items: flex-end; gap: 16px; height: 168px; padding: 18px 6px 0; }
  .chart .bar { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; height: 100%; }
  .chart .cnt { font-size: 14px; color: var(--text-4); margin-bottom: 7px; }
  .chart .bar.top .cnt { color: var(--brand); font-weight: 600; }
  .chart .col {
    width: 100%; max-width: 36px; border-radius: var(--r-sm) 8px 3px 3px;
    background: var(--brand-100); transition: height .45s;
  }
  .chart .col.top { background: linear-gradient(180deg, var(--brand-light), var(--brand)); }
  .chart .lb { font-size: 14px; color: var(--text-4); margin-top: 11px; }
  .chart .bar.top .lb { color: var(--brand); font-weight: 500; }

  /* 记录行（左标题右按钮） */
  .rec-row { display: flex; align-items: center; gap: 16px; padding: 18px 4px; border-bottom: 1px solid var(--surface-2); }
  .rec-row:last-child { border-bottom: 0; }
  .rec-row .nm { font-size: 16px; color: var(--text-1); }
  .rec-row .sub { font-size: 14px; color: var(--text-4); margin-top: 7px; }
  .rec-row .go {
    flex: none; font-size: 15px; color: var(--brand); cursor: pointer;
    padding: 7px 18px; border-radius: 999px; background: var(--brand-50); transition: .15s;
  }
  .rec-row .go:hover { background: var(--brand-100); }
  .rec-row .go.plain { background: transparent; color: var(--text-4); }
  .rec-row .go.plain:hover { color: var(--brand); }

  .bank-switch-row {
    display: flex; align-items: center; gap: 12px;
    padding: 14px 18px; border-radius: var(--r-md); background: var(--surface-2);
    font-size: 16px; cursor: pointer; transition: .15s;
  }
  .bank-switch-row:hover { background: var(--brand-50); }
  .bank-switch-row .nm { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-1); }
  .bank-switch-row .sw { color: var(--brand); font-size: 18px; }

  /* ========== 搜索 / 我的题库（按设计图）========== */
  .search-box {
    display: flex; align-items: center; gap: 11px;
    height: 44px; background: var(--surface-2); border-radius: 999px;
    padding: 0 22px; margin-bottom: 22px;
    border: 1.5px solid transparent; transition: .2s;
  }
  .search-box:focus-within { background: var(--surface-1); border-color: var(--brand); box-shadow: 0 4px 16px rgba(var(--brand-rgb),.08); }
  .search-box svg { width: 18px; height: 18px; color: var(--text-4); flex: none; }
  /* ★ 同样补上：这个也是漏了 font-family: inherit 的一处（见 .ipt input 的说明） */
  .search-box input { flex: 1; min-width: 0; border: 0; background: transparent; font-size: 16px; outline: none; color: var(--text-1); font-family: inherit; line-height: 1.5; }
  .search-box input::placeholder { color: var(--text-placeholder); }

  .scope-label { font-size: 15px; color: var(--text-4); margin-bottom: 13px; }
  .chip-row { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 30px; }
  .chip {
    padding: 8px 17px; border-radius: 999px; font-size: 15px;
    background: var(--surface-2); color: var(--text-3); cursor: pointer; transition: .15s;
    border: 1px solid transparent;
  }
  .chip:hover { background: var(--brand-50); color: var(--brand); }
  .chip.on { background: var(--brand-50); color: var(--brand); border-color: var(--brand-200); font-weight: 500; }

  .hot-head { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
  .hot-head .t { font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 7px; }
  .hot-head .chg {
    margin-left: auto; font-size: 14px; color: var(--text-4); cursor: pointer;
    display: flex; align-items: center; gap: 5px;
  }
  .hot-head .chg:hover { color: var(--brand); }

  .hot-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2px 44px; }
  .hot-item {
    padding: 14px 4px; font-size: 16px; color: var(--text-2);
    cursor: pointer; transition: .15s;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .hot-item:hover { color: var(--brand); }

  .empty-state { text-align: center; padding: 70px 20px 90px; }
  .empty-state .art { width: 130px; height: 118px; margin: 0 auto 24px; }
  .empty-state .txt { font-size: 16px; color: var(--text-4); }

  /* 我的题库页 */
  .mb-head {
    background: var(--surface-1); border-radius: var(--r-md); padding: 26px 30px;
    box-shadow: 0 4px 22px rgba(var(--brand-rgb),.08); margin-bottom: 18px;
  }
  .mb-title { font-size: 18px; font-weight: 600; display: flex; align-items: center; gap: 9px; margin-bottom: 7px; }
  .mb-sub { font-size: 14px; color: var(--text-4); }
  .mb-tabs { display: flex; gap: 28px; border-bottom: 1px solid var(--surface-2); margin: 20px 0 18px; }
  .mb-tabs span { padding: 0 2px 13px; font-size: 16px; color: var(--text-4); cursor: pointer; position: relative; }
  .mb-tabs span.on { color: var(--brand); font-weight: 600; }
  .mb-tabs span.on::after {
    content: ''; position: absolute; left: 0; right: 0; bottom: -1px;
    height: 2px; border-radius: var(--r-xs); background: linear-gradient(90deg, var(--brand-light), var(--brand));
  }

  /* 分类页顶部条 */
  .cat-toolbar {
    display: flex; align-items: center; gap: 14px;
    background: var(--surface-1); border-radius: var(--r-md); padding: 18px 24px;
    box-shadow: 0 4px 22px rgba(var(--brand-rgb),.08); margin-bottom: 16px;
  }
  .cat-toolbar .ct-title { font-size: 18px; font-weight: 600; display: flex; align-items: center; gap: 8px; flex: none; }
  .cat-toolbar .ct-filters { display: flex; gap: 8px; flex-wrap: wrap; margin-left: auto; }

  /* ★ 已移除：.quiz-modes（模式切换）、.quiz-tools / .tool（底部工具栏）
     —— 刷题页改版后，模式切换并进了页头的 .qz-modes，
        底部工具栏换成了「图标 + 文字」的 .qz-tools / .qz-tool。
        这两个类现无任何使用者，故不保留定义。 */

  /* ★ 已移除：.card-legend / .card-legend i / .card-grid
     这三个类原本只服务于弹层「答题记录」，该弹层已随「目录」按钮一并删除
     （原因见 index.html 末尾的说明）。侧栏答题卡用的是 .qz-cells / .qz-legend，
     与它们无关，故不受影响。 */

  /* .card-cell 仍在用 —— 侧栏答题卡的 .qz-cells 复用它。
     ★ 尺寸不再由 .qz-cells 固定给：现在按题量切 .d-md / .d-lg / .d-xl 三档，
       见下方 .qz-cells 那一段（一节可能有几百道题，固定 6×42 会撑爆侧栏）。 */
  .card-cell {
    height: 38px; display: flex; align-items: center; justify-content: center;
    border-radius: var(--r-sm); font-size: 15px; cursor: pointer; transition: .15s;
    background: var(--surface-2); color: var(--text-3);
  }
  .card-cell:hover { background: var(--brand-50); }
  .card-cell.right { background: var(--success-bg); color: var(--success); font-weight: 500; }
  .card-cell.wrong { background: var(--danger-bg); color: var(--danger); font-weight: 500; }
  /* ★ 考试模式的「已答」：中性色。
     考试全程不判对错，答题卡就【只显示进度】——
     这里用品牌浅底表示「答过了」，与绿/红区分开，
     免得被误读成「答对了」或者「答错了」。 */
  .card-cell.done { background: var(--brand-50); color: var(--brand); font-weight: 500; }

  .ta {
    width: 100%; border: 1px solid var(--border-2); border-radius: var(--r-md);
    padding: 14px 16px; font-size: 16px; font-family: inherit; outline: none;
    min-height: 130px; resize: vertical; color: var(--text-1); line-height: 1.7;
    box-sizing: border-box;
  }
  .ta:focus { border-color: var(--brand); }
  .ta::placeholder { color: var(--text-placeholder); }
  .ta.one { min-height: 42px; height: 42px; }

  /* ========== 悬浮球面板：「我的」==========
     ★★ 用户要求：删掉「我的」界面，把里面的功能全部放进悬浮球 ★★

     ★ 定位：固定在悬浮球【左侧】。
       right = 悬浮球右边距 24 + 球宽 52 + 间距 12 = 88px。
       不跟着球跑、也不做小三角箭头 —— 面板接近两屏高，
       做成气泡会把箭头拉得离球很远，反而看不出是从哪弹出来的。
     ★ 布局从原「我的」页的「横向铺满」改成「纵向列表」：
       面板只有 320px 宽，原来那种一行 5 个图标卡在这么窄的地方会挤成一团。
     ★ 高度上限 + 内部滚动：条目共 11 项，小屏上会顶出视口。 */
  .me-mask {
    position: fixed; inset: 0; z-index: var(--z-mask);
    /* ★ 不压暗：它是「抽屉」不是「模态」。压暗会让人以为整页被锁住了，
       而这里其实只是从右侧滑出的一列账号入口。 */
    background: transparent;
  }
  /* ══════ 「我的」面板顶部：窄标题栏 + ✕ ══════
     ★ 用户反馈「这个界面就无法退出了」✗
       原来面板里没有 ✕ ✓ 唯一的关闭方式是"退出登录" ✗
     ★ 只加 28px 高 ✓ 不占多少空间 ✓ 但它是**最明确的出口** ✓
       比"点外面"和"按 Esc"都好 —— 因为那两个都要用户先知道 ✓ */
  .me-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: 0 2px 10px;
  }
  .me-head .t { font-size: 13px; color: var(--text-4); font-weight: var(--fw-medium); letter-spacing: .3px; }
  .me-x {
    width: 26px; height: 26px; padding: 0;
    display: flex; align-items: center; justify-content: center;
    border: 0; border-radius: 50%; background: transparent;
    color: var(--text-4); font-size: 19px; line-height: 1;
    font-family: inherit; cursor: pointer; transition: var(--dur-fast);
  }
  .me-x:hover { background: var(--surface-2); color: var(--text-1); }

  /* ★ 从顶栏头像打开时，贴右上角 ✓ 而不是从右下角冒出来 ✗ */
  .me-panel.from-head {
    right: 20px; top: 72px; bottom: auto;
    animation: mePanelInHead var(--dur-base) var(--ease) both;
  }
  @keyframes mePanelInHead {
    from { opacity: 0; transform: translateY(-8px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  .me-panel {
    position: absolute; right: 88px; bottom: 32px;
    width: 320px; max-height: calc(100vh - 120px);
    overflow-y: auto; overscroll-behavior: contain;
    background: var(--surface-1); border: 1px solid var(--border-1);
    border-radius: var(--r-md); box-shadow: var(--sh-modal);
    padding: 14px;
    animation: mePanelIn var(--dur-base) var(--ease) both;
  }
  @keyframes mePanelIn {
    from { opacity: 0; transform: translateX(10px); }
    to   { opacity: 1; transform: translateX(0); }
  }

  /* ① 账号卡：面板里改用浅底，而不是原来的蓝色渐变 ——
     蓝渐变在 320px 宽的窄条里很吵，会压过下面那一片功能入口。 */
  .me-card {
    display: flex; align-items: center; gap: 12px;
    background: var(--surface-2); border-radius: var(--r-sm);
    padding: 12px 14px; margin-bottom: 12px;
  }
  .me-card .av {
    width: 42px; height: 42px; border-radius: 50%; flex: none;
    background: var(--brand); color: #fff;
    display: flex; align-items: center; justify-content: center;
    font-size: 19px; font-weight: var(--fw-bold);
  }
  .me-card .id { min-width: 0; flex: 1; }
  .me-card .nm {
    font-size: 16px; font-weight: var(--fw-bold); color: var(--text-1);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .me-card .sb { font-size: 12px; color: var(--text-4); margin-top: 4px; cursor: pointer; }
  .me-card .sb:hover { color: var(--brand); }

  /* ② 三格数据 */
  .me-stats {
    display: flex; margin-bottom: 14px;
    border: 1px solid var(--border-1); border-radius: var(--r-sm); overflow: hidden;
  }
  .me-stats .st {
    flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px;
    padding: 10px 4px; border-right: 1px solid var(--border-1);
  }
  .me-stats .st:last-child { border-right: 0; }
  .me-stats .st .n {
    font-size: 18px; font-weight: var(--fw-bold); color: var(--text-1);
    line-height: 1; font-variant-numeric: tabular-nums;
  }
  .me-stats .st .l { font-size: 11px; color: var(--text-4); white-space: nowrap; }

  /* ③ 分组标题 */
  .me-sec { font-size: 12px; color: var(--text-4); padding: 4px 4px 8px; }

  /* ④ 列表 */
  /* 「已记住」标签：绿色小徽章 —— 让"登录态是好的"这件事一眼可见 ✓ */
  .me-row .tag.ok { color: var(--success); background: var(--success-bg); }
  .me-list { margin-bottom: 12px; }
  .me-list .me-row {
    display: flex; align-items: center; gap: 10px;
    padding: 10px 8px; border-radius: var(--r-sm);
    font-size: 14px; color: var(--text-2); cursor: pointer; transition: var(--dur-fast);
  }
  .me-list .me-row:hover { background: var(--brand-50); color: var(--brand); }
  .me-list .me-row .nm { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .me-list .me-row .nm.danger { color: var(--danger); }
  .me-list .me-row .ic {
    width: 28px; height: 28px; border-radius: var(--r-xs); flex: none;
    display: flex; align-items: center; justify-content: center;
  }
  .me-list .me-row .ic svg { width: 16px; height: 16px; }
  /* 五个入口各给一个浅底色 —— 纯图标一排会显得太素，也分不清是哪一项 */
  .me-list .me-row .ic.b1 { background: var(--brand-50); color: var(--brand); }
  .me-list .me-row .ic.b2 { background: var(--success-bg); color: var(--success); }
  .me-list .me-row .ic.b3 { background: var(--surface-2); color: var(--text-3); }
  .me-list .me-row .bd {
    flex: none; min-width: 18px; height: 18px; padding: 0 5px;
    border-radius: var(--r-full); background: var(--danger); color: #fff;
    font-size: 11px; font-weight: var(--fw-bold); line-height: 18px; text-align: center;
    font-variant-numeric: tabular-nums;
  }
  .me-list .me-row .tag { flex: none; font-size: 11px; color: var(--text-4); }
  .me-list .me-row .arw { flex: none; color: var(--text-4); font-size: 15px; }
  /* 退出登录单独一块、与上面留白隔开 —— 手一滑就点到的危险操作 */
  .me-list.me-quit { margin-top: 4px; padding-top: 8px; border-top: 1px solid var(--border-1); margin-bottom: 0; }

  /* ★ 已移除 .dock-av（悬浮球「我的」的头像首字图标）——
     用户要求改成和「反馈」一样的固定小人线条图标，那个类失去使用者。
     三个按钮现在共用 .dock-i svg 的规格（18px / stroke-width 1.8），
     摆在一起才是整齐的一列。 */
  .dock-i.on { border-color: var(--brand); color: var(--brand); box-shadow: var(--sh-3); }

  .cell {
    display: flex; align-items: center; gap: 12px;
    padding: 17px 4px; border-bottom: 1px solid var(--surface-2);
    font-size: 16px; cursor: pointer;
  }
  .cell:last-child { border-bottom: 0; }
  .cell .nm { flex: 1; color: var(--text-2); }
  .cell .tag { font-size: 14px; color: var(--text-4); }
  .cell .arw { color: var(--decor-1); font-size: 16px; }

  /* FAQ 折叠 */
  .faq-item { border-bottom: 1px solid var(--surface-2); }
  .faq-q {
    display: flex; align-items: center; gap: 12px;
    padding: 18px 4px; font-size: 16px; color: var(--text-1); cursor: pointer; transition: .15s;
  }
  .faq-q:hover { color: var(--brand); }
  .faq-q .no { color: var(--text-4); flex: none; min-width: 22px; }
  .faq-q .ar { margin-left: auto; color: var(--decor-1); transition: transform .2s; flex: none; }
  .faq-item.open .faq-q { color: var(--brand); font-weight: 500; }
  .faq-item.open .faq-q .ar { transform: rotate(180deg); }
  .faq-a {
    padding: 0 4px 20px 34px; font-size: 15px; color: var(--text-3); line-height: 2;
    /* ★ 答案里有多段（「（有）…/（没有）…」这种）→ 保留换行 ✓ */
    white-space: pre-wrap;
  }

  /* 关于我们 */
  .about-wrap { text-align: center; padding: 26px 0 10px; }
  /* ★ 现在是一张图（官方 LD 标识），不是"渐变底 + 文字" ✗
     · display:block + margin:auto 让它居中 ✓
     · 图本身已经是圆角 + 透明底，所以这里【不要】再加圆角或背景 ✗
     · 阴影保留：让图标从白色卡片上"浮"起来一点 ✓ */
  .about-logo {
    display: block;
    width: 84px; height: 84px;
    margin: 0 auto 18px;
    box-shadow: 0 10px 24px rgba(var(--brand-rgb), .26);
    border-radius: 20px;          /* 与图片自带圆角对齐，避免阴影露出方角 */
  }
  .about-body { font-size: 16px; line-height: 2.1; color: var(--text-2); margin-top: 26px; text-align: left; }

  /* 兑换码 */
  .act-code-row { display: flex; gap: 12px; }
  .act-code-row input {
    flex: 1; min-width: 0; height: 44px; border: 1px solid var(--border-2); border-radius: var(--r-md);
    padding: 0 18px; font-size: 16px; outline: none; font-family: inherit; color: var(--text-1);
  }
  .act-code-row input:focus { border-color: var(--brand); }
  .act-step { display: flex; align-items: center; gap: 14px; padding: 15px 0; }
  .act-step .n {
    width: 30px; height: 30px; border-radius: 50%; flex: none;
    background: linear-gradient(135deg, var(--brand-light), var(--brand)); color: var(--surface-1);
    display: flex; align-items: center; justify-content: center; font-size: 16px;
  }
  .act-step .tx { font-size: 15px; color: var(--text-2); }

  /* ========== PC 端答题分栏：题干 + 选项同栏，答题卡独立侧栏 ==========
     ★★ 由「三栏」改为「双栏」的核心理由 ★★
       答题的自然顺序是「读题干 → 看选项 → 看解析」，是一条【从上往下】的线。
       原先把题干放左栏、选项放右栏（.82fr / 1.18fr），眼睛要横向来回跳，
       长题干还会被栏宽逼着折行、容易漏看后半句。
       合并成一栏后视线只需一路向下；右侧整列让给答题卡 ——
       它也从 248px 涨到 400px（原来格子只有 44px，又小又难点）。

     ★ 侧栏 400px 与「我的题库」页副栏（449px）量级一致，全站侧栏观感统一。
     ★ 侧栏 sticky + 内滚：题目多时答题卡不会跟着页面滚走。 */
  .q-card { padding: 28px 34px; }

  /* ★★ 两栏【等高等齐】★★
     align-items 从 start 改成 stretch（grid 的默认值）——
     两栏被拉到同一行高，顶边与底边都对齐。
     ★ 之所以能这么做：原来侧栏是 sticky + 内部滚动（为了让答题卡「跟着页面走」），
       但那个方案下侧栏高度由内容决定，与主栏参差；
       用户明确要「等高等齐」，所以改成由主栏定高、侧栏跟随。
     ★ 答题卡随之铺满整列（见下面的 .qz-side .qz-sheet:first-child），
       不然卡片只有内容高、下方会留一大截空白，反而不像「等高」。 */
  /* ★★ 答题页要给右侧悬浮球留出位置 ★★
     （把自己当学员走一遍发现的）
     ★ 为什么只有答题页需要：其它页面的内容是【滚动】的，
       被悬浮球压住的部分滚一下就出来了；而答题页在上一轮改成了
       【固定高度 + 内部滚动】—— 被压住的那块就【永远点不到】了。
     ★ 什么时候会压住（算出来的，不是感觉）：
         内容区最右 = (vw − 1440) / 2 + 1440 − 40（.content 上限 1440 + 右内边距 40）
         悬浮球占最右 24 + 52 = 76px
         解得 vw < 1512 时必然重叠 —— 1366 / 1440 这两种最常见的笔记本宽度都在里面。
     ★ 所以只在 ≤1512 时留位；再宽就不必留，免得白丢一块版面。
       留 80px（球宽 52 + 边距 24 + 余量）够用。 */
  @media (max-width: 1512px) {
    .quiz { padding-right: 80px; }
  }

  .qz-split {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 400px;   /* ★ minmax(0,·) 而非 1fr：防被内容顶开 */
    gap: 20px;
    align-items: stretch;
    /* ★ 吃掉 .quiz 里上下两条之外的剩余高度（见 .quiz 那段）。
       min-height: 0 是关键：flex 子项默认 min-height: auto，
       不加它就不会收缩到内容高度以下，整列会被内容顶开、height 形同虚设。
       ★ 这两个属性和 grid 声明写在【同一条规则】里 ——
         同一个选择器分两处写，改一处不生效的时候很难找。 */
    flex: 1; min-height: 0;
  }
  /* ══════ 切题动效（用户要求：考试模式点选后自动跳下一题要有动效）══════
     ★ 机制：题目内容那层是 `<div class="qz-body" :key="curQ.id">` ——
       key 一变 Vue 就【重建这个元素】，下面的入场动画因此自动重播。
       不需要「加类 → 等动画结束 → 删类」那一套，也不会只播第一次。

     ★ 方向跟着阅读方向走：
         next → 从右侧滑入（下一题）   prev → 从左侧滑入（上一题）
         jump → 轻轻上浮（答题卡跳题，没有"前后"可言）
       ★ 位移取 24px：够看出方向，又不至于让眼睛跟着甩。
       ★ 时长 0.24s：考试模式是"点完就走"，动效必须比手速快 ——
         慢了会变成每答一题都要等它一下，反而烦。
         （自动跳题本身有 260ms 延迟，加起来约半秒，衔接刚好。）

     ★ 用 `both` 填充：动画开始前就停在 from（透明），
       避免元素先闪一下原样再开始动。 */
  @keyframes qInNext { from { opacity: 0; transform: translateX(24px); }  to { opacity: 1; transform: none; } }
  @keyframes qInPrev { from { opacity: 0; transform: translateX(-24px); } to { opacity: 1; transform: none; } }
  @keyframes qInJump { from { opacity: 0; transform: translateY(12px); }  to { opacity: 1; transform: none; } }
  .qz-body.q-in-next { animation: qInNext .24s cubic-bezier(.22,.9,.3,1) both; }
  .qz-body.q-in-prev { animation: qInPrev .24s cubic-bezier(.22,.9,.3,1) both; }
  .qz-body.q-in-jump { animation: qInJump .3s  cubic-bezier(.22,.9,.3,1) both; }

  /* 尊重系统「减少动态效果」：关掉切题动画，直接显示终态 */
  /* ★ 自评条：切题时只做【透明度】淡入 ——
     它在 .qz-body 外面（要吸底），不跟着题身一起滑就会显得两半不同步。
     但【绝不能加 transform】：position: sticky 的元素一旦在被重建时发生位移，
     吸底位置会明显抖一下。所以这里只动 opacity。 */
  @keyframes rjFade { from { opacity: 0; } to { opacity: 1; } }
  /* ★ 已标记后的引导条：换成"确认态" ✓ 不改变高度 ✓
       只是把颜色从"提示"变成"已确认" —— 避免用户以为那一条没了 ✗ */
  .recite-hint.done { background: var(--success-bg); color: var(--success); }
  .recite-hint.done b { color: var(--success); }

  .recite-judge { animation: rjFade .3s ease both; }

  @media (prefers-reduced-motion: reduce) {
    .qz-body.q-in-next, .qz-body.q-in-prev, .qz-body.q-in-jump { animation: none !important; }
  }
  @media (prefers-reduced-motion: reduce) {
    .recite-judge { animation: none !important; }
  }

  /* ★★ 主栏自己滚，而不是把整页撑长 ★★
     （用户要求：两栏固定一个高度）
     ★ 高度由 .qz-split 的固定高度给出（见 .quiz 那段），
       所以这里只需要 overflow-y: auto —— 内容超出就在主栏内部滚。
     ★ overscroll-behavior: contain：主栏滚到底不要带动整页 ——
       刷题时最忌讳「想继续往下看解析，结果整页被滚跑了」。
     ★ .qz-main 就是 .q-card（有 padding），内边距会一起滚走。
       这是可接受的：内容区本身就是整张卡片。 */
  /* ★★ 主栏外层：只负责给自绘滚动条当定位参照 ★★
     display:flex + 子项 flex:1 —— 让 .qz-main 撑满这一列，
     与它原来直接当 grid 子项时的尺寸完全一致（不多 1px）✓ */
  .qz-main-box {
    position: relative;
    min-width: 0;
    min-height: 0;
    max-height: 100%;
    display: flex;
  }
  .qz-main {
    flex: 1;
    min-width: 0;
    max-height: 100%;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* ★★ 用户口径：「不要占掉内容宽度，压住内容边缘就行」★★
       ★ 浏览器给不了「overlay + 常驻」：
         · ::-webkit-scrollbar 自定义 → classic 滚动条，必然占 10px 布局宽 ✗
         · scrollbar-width: thin  → 变 overlay，但【不滚动时自动淡出】✗
       → 所以原生条整个隐藏（none 也会盖掉 tokens.css 那句 * { thin }），
         改用下面 .qz-sb 那根自绘的：绝对定位、不参与布局、常驻可见 ✓
       ★ 注意 scrollbar-width: none 在 Chrome/Firefox 都生效且不占位 ✓ */
    scrollbar-width: none;
    scrollbar-color: auto;
  }
  .qz-main::-webkit-scrollbar { width: 0; height: 0; }

  /* ★ 自绘滚动条：贴在主栏右边缘内侧 6px，浮在内容之上
        · 宽度 10px 是【轨道】的宽度，滑块本身 6px（左右各留 2px）——
          细一点不抢视线，也不至于细到看不见
        · 上下各留 8px：滑块跑到头时不会顶在卡片圆角上
        · 颜色用 --text-4（白卡 4.97:1 / 夜间 5.49:1），
          不用 --border-2 —— 那个在这个白底上只有 1.1:1，等于没画 ✗ */
  .qz-sb {
    position: absolute;
    top: 8px; bottom: 8px; right: 6px;
    width: 10px;
    opacity: 0;
    transition: opacity .18s var(--ease-out, ease);
    pointer-events: none;
  }
  .qz-sb.on { opacity: 1; }
  .qz-sb i {
    position: absolute;
    left: 2px; top: 0;
    width: 6px;
    border-radius: 3px;
    background: var(--text-4);
    /* transform 由 JS 给：滚动时只合成、不重排 ✓ */
    will-change: transform;
  }
  .qz-side {
    min-width: 0; display: flex; flex-direction: column; gap: 16px;
    max-height: 100%;
    /* 侧栏作为【安全网】也可滚：答题卡本身已有三层限高（外层 42vh +
       每张卡片 4 行 + 卡内滚动），正常不会溢出；
       但类别特别多时仍可能超出，给一条兜底滚动比裁掉内容好。 */
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
  }
  /* 答题卡铺满侧栏整列，与主栏底部对齐 */
  .qz-side .qz-sheet:first-child { flex: 1; display: flex; flex-direction: column; }
  .qz-side .qz-sheet:first-child .qz-sheet-body { flex: 1; }

  /* 侧栏卡片外壳（答题卡 / 快捷工具共用） */
  .qz-sheet { background: var(--surface-1); border-radius: var(--r-md); box-shadow: var(--sh-1); overflow: hidden; }
  .qz-sheet-head {
    display: flex; align-items: center; gap: 10px;
    padding: 15px 18px; border-bottom: 1px solid var(--border-1);
  }
  .qz-sheet-head .t { font-size: 16px; font-weight: var(--fw-bold); color: var(--text-1); }
  .qz-sheet-head .sp { flex: 1; }
  .qz-sheet-head .mini { font-size: 13px; color: var(--text-4); font-variant-numeric: tabular-nums; }
  .qz-sheet-body { padding: 16px 18px 18px; }

  /* 三项统计：从【竖排 3 行】改成【横排 3 格】
     原来在 248px 里挤成三行小字，既占高度又难扫读 */
  .qz-sstat { display: flex; gap: 8px; margin-bottom: 16px; }
  .qz-sstat .c { flex: 1; text-align: center; padding: 10px 6px; border-radius: var(--r-sm); }
  .qz-sstat .c .n { font-size: 20px; font-weight: var(--fw-bold); font-variant-numeric: tabular-nums; line-height: 1.2; }
  .qz-sstat .c .l { font-size: 12px; margin-top: 3px; }
  .qz-sstat .c.ok { background: var(--success-bg); }
  .qz-sstat .c.ok .n, .qz-sstat .c.ok .l { color: var(--success); }
  .qz-sstat .c.no { background: var(--danger-bg); }
  .qz-sstat .c.no .n, .qz-sstat .c.no .l { color: var(--danger); }
  .qz-sstat .c.bk { background: var(--surface-2); }
  .qz-sstat .c.bk .n { color: var(--text-2); }
  .qz-sstat .c.bk .l { color: var(--text-4); }
  /* 考试模式的「已答」格：中性品牌色，与绿/红区分开 */
  .qz-sstat .c.done { background: var(--brand-50); }
  .qz-sstat .c.done .n, .qz-sstat .c.done .l { color: var(--brand); }

  /* 答题卡格子：5 列 → 6 列，格子 44px → 62px（侧栏变宽带来的直接收益）
     ★ .cur（当前题）的高亮原来挂在 .qa-grid 上，.qa-grid 已移除，这里补回来。
       cardState() 现在返回四种状态：blank / right / wrong / done
       （done = 考试模式下的「已答」，中性色，见上），
       四种底色分别由 .card-cell 的通用规则负责。 */
  /* ★★ 答题卡网格：限高 + 内部滚动（用户口径：一节可能有几百道题）★★
     原来固定 6 列 × 42px：300 题会排出约 50 行 ≈ 2400px，
     400px 宽的侧栏直接被撑爆、整个页面被拉得极长。

     ★ max-height 用 vh 而不是固定 px：侧栏是「与主栏等高等齐」的弹性布局，
       固定像素在不同屏高下会打架；42vh 在各种屏高下都留得住进度与图例。
     ★ overscroll-behavior: contain —— 网格滚到底时不要带着整页一起滚，
       否则用户想在答题卡里继续找题，结果整页被滚跑了。
     ★ 自定义细滚动条：侧栏本来就窄，系统粗滚动条会压掉一列格子。 */
  .qz-cells {
    max-height: 42vh;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* ★ 右侧多留一点：组变成带边框的卡片后，滚动条会贴上最后一列的卡片边框 */
    padding-right: 6px;
    scrollbar-width: thin;
    /* ★ 从「网格」改成「纵向分组容器」（用户口径：答题卡按题型分段）——
       分组标题要独立成条，每组就必须是独立的块，
       所以外层不能再是 grid，改成 flex 纵向排布。 */
    display: flex; flex-direction: column; gap: 10px;
  }
  .qz-cells::-webkit-scrollbar { width: 6px; }
  .qz-cells::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 3px; }
  .qz-cells::-webkit-scrollbar-track { background: transparent; }

  /* ---------- 题型分组 ----------
     ★★ 为什么改成「一组一个框」（用户反馈：标题和序号撞在一起）★★
       之前每组只是「标题行 + 下面一坨格子」，两者之间没有任何边界，
       而标题又带一条 box-shadow 画的 1px 分隔线 ——
       那条线在与格子贴合、以及滚动时压到别的内容上时，
       看起来就像「标题和序号撞在一起」。

     ★★ 去掉 position: sticky ★★
       改成按题型全局归并后，一组里全是同一种题型，
       而且一组本来就不长（多数只有一两行格子）——
       标题吸顶带来的「知道在看哪一类」的好处很小，
       但它在滚动时压到相邻内容的副作用很显眼。两相权衡，去掉更干净。

     ★ 现在的结构：一个带边框的浅色卡片，标题是卡片顶部的灰条，
       格子在其下方独立的内边距区域里 —— 谁是谁一目了然。 */
  .qz-group {
    border: 1px solid var(--border-1);
    border-radius: var(--r-sm);
    background: var(--surface-1);
    /* ★ 这里刻意【不做裁切】来收圆角 ——
       一裁切就会打断格子当前态那圈 focus 光圈（它会溢出到卡片外沿）。
       （本注释不写那条属性的原文：写了会让属性扫描出现假命中。） */
  }
  .qz-group-head {
    display: flex; align-items: center; gap: 8px;
    padding: 9px 12px;
    background: var(--surface-2);          /* 浅灰条：与白色格子区明确分开 */
    border-bottom: 1px solid var(--border-1);
    border-radius: var(--r-sm) var(--r-sm) 0 0;
    /* ★ 可点：按题型折叠 / 展开（用户要求） */
    cursor: pointer; user-select: none;
    transition: var(--dur-fast);
  }
  .qz-group-head:hover { background: var(--brand-50); }
  /* 折叠后只剩标题一条，下方不再有内容 —— 去掉那条分隔线，四角都收圆 */
  .qz-group.collapsed .qz-group-head { border-bottom-color: transparent; border-radius: var(--r-sm); }

  /* 折叠箭头：展开时朝下，收起时朝右 —— 与文件树的惯例一致 */
  .qz-group-head .ar { flex: none; display: flex; color: var(--text-4); transition: transform var(--dur-base) var(--ease); }
  .qz-group-head .ar svg { width: 14px; height: 14px; }
  .qz-group-head .ar:not(.open) { transform: rotate(-90deg); }

  .qz-group-head .ty { font-size: 13px; font-weight: var(--fw-bold); color: var(--text-1); }
  /* ★ 已移除 .rg（题号区间）—— 分组改成按题型【全局归并】后，
     一组里的题号本来就可能不连续（单选题组是 1、2、6、7），
     写「1–7 题」会误导。组标题现在只有「题型 + 题数」两项，更干净。 */
  .qz-group-head .ct { margin-left: auto; font-size: 12px; color: var(--text-4); font-variant-numeric: tabular-nums; }
  /* 折叠后右侧换成了「已答 N / M」，是更该被看到的信息，稍微提一点权重 */
  .qz-group.collapsed .qz-group-head .ct { color: var(--text-3); font-weight: var(--fw-medium); }

  /* 格子区：独立内边距 —— 让格子与上方标题条彻底分开，不再有「贴在一起」的观感 */
  .qz-group-grid { display: grid; gap: 8px; padding: 11px 12px 13px; }

  /* ★★ 每张卡片自己的高度上限 ★★
     （用户要求：卡片要有一个默认的大小长度，不然题目一多就太长）

     ★ 为什么不能只靠外层 .qz-cells 的 42vh：
       那是【整块答题卡】的上限 —— 一旦某一类题特别多（比如单选题 200 道），
       展开后它会把同一卡片里的其它类【完全挤出视野】，
       用户得先滚过 200 个格子才能看到「多选题」的标题。
     ★ 现在每张卡片的格子区各自限高、各自内部滚动：
       所有类别的标题始终都在，展开哪一类就在哪一类里滚。

     ★★ 上限按「4 行格子」给，三档密度各自算 ★★
        d-md 每行 42 + 8 = 50 → 4 行 200px
        d-lg 每行 36 + 6 = 42 → 4 行 168px
        d-xl 每行 30 + 5 = 35 → 4 行 140px
      用「行数 × 行高」而不是拍一个固定数，三档密度下看到的行数才一致。
      即使某档的字号/行高以后调整，也只需同步改这几个数，不会出现
      「小档能看 4 行、大档只能看 3 行」这种不齐。

     ★ overscroll-behavior: contain —— 卡内滚到底不要带着外层（乃至整页）一起滚，
       否则用户想在某个类别里继续找题，结果整页被滚跑了。 */
  .qz-cells.d-md .qz-group-grid { max-height: 200px; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; }
  .qz-cells.d-lg .qz-group-grid { max-height: 168px; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; }
  .qz-cells.d-xl .qz-group-grid { max-height: 140px; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; }
  .qz-group-grid::-webkit-scrollbar { width: 6px; }
  .qz-group-grid::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 3px; }
  .qz-group-grid::-webkit-scrollbar-track { background: transparent; }

  /* 密度档位：题越多，格子越小、列数越多（档位由 app.js 的 cardDensity 给）。
     ★ 网格现在挂在 .qz-group-grid 上，所以密度类要作用到它。 */
  .qz-cells.d-md .qz-group-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .qz-cells.d-md .card-cell { height: 42px; font-size: 14px; }
  .qz-cells.d-lg .qz-group-grid { grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 6px; }
  .qz-cells.d-lg .card-cell { height: 36px; font-size: 13px; }
  .qz-cells.d-xl .qz-group-grid { grid-template-columns: repeat(10, minmax(0, 1fr)); gap: 5px; }
  .qz-cells.d-xl .card-cell { height: 30px; font-size: 12px; }

  /* 跳号：题量上百以后，靠拖动网格找第 217 题并不现实 */
  .qz-jump { display: flex; align-items: center; gap: 6px; margin-top: 12px; font-size: 13px; color: var(--text-4); }
  .qz-jump .lb { flex: none; }
  .qz-jump .no {
    width: 64px; height: 30px; padding: 0 8px;
    border: 1px solid var(--border-2); border-radius: var(--r-sm);
    background: var(--surface-1); color: var(--text-1);
    font-size: 13px; font-family: inherit; text-align: center;
    font-variant-numeric: tabular-nums;
    -moz-appearance: textfield; appearance: textfield;
    transition: var(--dur-fast);
  }
  /* 去掉数字输入框的上下箭头 —— 侧栏窄，那两个箭头既挤又没用 */
  .qz-jump .no::-webkit-outer-spin-button,
  .qz-jump .no::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
  .qz-jump .no:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-50); }
  .qz-jump .go {
    margin-left: auto; flex: none;
    height: 30px; padding: 0 14px; border-radius: var(--r-sm);
    border: 1px solid var(--border-2); background: var(--surface-1);
    color: var(--text-2); font-size: 13px; font-family: inherit; cursor: pointer;
    transition: var(--dur-fast);
  }
  .qz-jump .go:hover { border-color: var(--brand); color: var(--brand); background: var(--brand-50); }
  /* ★ 这里原来有一条固定格子尺寸的规则（6 列 42px），已删除 ——
     现在尺寸由上方 .d-md / .d-lg / .d-xl 三档密度按题量给。
     留着会让「格子多大」有两处定义，改一处不生效还找不到原因。
     （本注释不写那条规则的原文：写了会让「规则扫描」出现假命中。） */
  .qz-cells .card-cell.cur {
    box-shadow: 0 0 0 2px var(--brand);
    background: var(--brand-50); color: var(--brand); font-weight: var(--fw-bold);
  }

  .qz-legend { display: flex; gap: 16px; margin-top: 14px; font-size: 12px; color: var(--text-4); }
  .qz-legend i { display: inline-block; width: 9px; height: 9px; border-radius: 3px; margin-right: 5px; vertical-align: middle; }
  .qz-legend i.r { background: var(--success); }
  .qz-legend i.w { background: var(--danger); }
  .qz-legend i.b { background: var(--decor-2); }
  /* 考试模式的「已答」圆点：中性品牌色 */
  .qz-legend i.d { background: var(--brand); }

  /* ★★ 背题模式的自评条 ★★
     看完答案后出现「我记住了 / 不会」，决定这一题在答题卡上显示为对还是错。

     ★★ 为什么改成吸底常驻（用户要求「放在明显的位置」）★★
       原来它排在解析【最下面】—— 解析一长（背题模式的解析往往很长），
       它就被挤到屏幕外，用户读完还得往下找，很容易漏掉、直接翻页走了。
       ★ 刷题页是【整页滚动】（没有内部滚动容器），所以 sticky 可用：
         · 解析短 → 它就在原位，不碍事
         · 解析长 → 它钉在视口底部，读的过程中一直看得见
         · 滚到底 → 回到自然位置，不会挡住末尾内容
       ★ bottom: 16px 留一点空隙，让它读起来是「浮在内容上方」而不是贴着屏幕边。
       ★ z-index: 5 只压过题目内容，不越过答题卡侧栏与顶栏。 */
  .recite-judge {
    position: sticky; bottom: 16px; z-index: 5;
    /* ★ 改成【纵向两级】：主行（提问句 + 按钮）+ 状态行（同步结果）——
       原来两者挤在同一条 flex 行里，结果文字一出现就把 flex:1 的提问句压缩，
       整行跟着重排；再叠加「网络回来才渲染」的跳变，就是用户看到的那一闪。 */
    display: flex; flex-direction: column;
    margin-top: 18px; padding: 14px 18px 12px;
    border-radius: var(--r-md);
    background: var(--surface-1); border: 1px solid var(--border-1);
    /* 双层阴影：让它在内容滚过时明确「浮起来」，避免与正文糊在一起 */
    box-shadow: 0 -2px 10px rgba(22, 35, 58, .06), 0 8px 24px rgba(22, 35, 58, .10);
    animation: reciteIn var(--dur-base) var(--ease) both;
  }
  @keyframes reciteIn {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: translateY(0); }
  }

  .recite-judge .rj-main { display: flex; align-items: center; gap: 12px; }

  /* 提问句加重：原来 --text-3 太轻，与解析正文糊在一起分不出来 */
  .recite-judge .lb {
    flex: 1; font-size: 15px; color: var(--text-1); font-weight: var(--fw-medium);
    display: flex; align-items: center; gap: 8px;
  }
  /* 左侧来一个小色点，进一步把它与解析区分开 */
  .recite-judge .lb::before {
    content: ''; flex: none; width: 4px; height: 16px; border-radius: 2px;
    background: var(--brand);
  }

  .recite-judge .qbtn { height: 40px; padding: 0 22px; font-size: 15px; flex: none; }

  /* ★★ 状态行：高度常驻 ★★
     不论有没有文字都占着 18px —— 所以「正在加入…」出现、
     换成「✓ 已加入错题本」、或用户改点「我记住了」后消失，
     都不会让整条吸底栏忽高忽低。
     padding-left 与提问句的文字左边缘对齐（色条 4px + 间隙 8px = 12px）。 */
  .recite-judge .rj-status {
    min-height: 18px; margin-top: 5px; padding-left: 12px;
    display: flex; align-items: center;
  }
  .recite-judge .rj-tip { font-size: 13px; color: var(--text-4); }

  /* 交叉淡入：让「正在加入错题本…」→「✓ 已加入错题本」是渐变而不是硬切。
     ★ 硬切会让文字长度突变、看起来又抖一下 —— 这是「闪烁」的第二个来源。 */
  .rjfade-enter-active, .rjfade-leave-active { transition: opacity var(--dur-fast) var(--ease); }
  .rjfade-enter-from, .rjfade-leave-to { opacity: 0; }

  /* ★ 两个按钮的【默认态】就带上各自语义的淡色 ——
     原来两个都是灰描边，看着像两个无关的次要按钮；
     现在一眼能看出「一个偏绿、一个偏红」是一组对立的选择。
     ★ 选中态再把底色坐实，形成「未选 → 已选」的明显落差。 */
  .recite-judge .qbtn:nth-of-type(1) {
    color: var(--success); border-color: var(--success-border); background: var(--success-bg);
  }
  .recite-judge .qbtn:nth-of-type(2) {
    color: var(--danger); border-color: var(--danger-border); background: var(--danger-bg);
  }
  .recite-judge .qbtn:nth-of-type(1).on {
    background: var(--success); border-color: var(--success); color: #fff; font-weight: var(--fw-bold);
  }
  .recite-judge .qbtn:nth-of-type(2).on {
    background: var(--danger); border-color: var(--danger); color: #fff; font-weight: var(--fw-bold);
  }
  .recite-judge .qbtn:hover:not(.on) { filter: brightness(.97); }

  /* 「不会」→ 加入错题本的结果反馈。
     ★ 放在自评条内部、按钮之后，而不是复用解析区那个 .sync-tip ——
       那个在背题模式下不显示（它挂在 judged 条件下）。 */
  /* ★ 状态提示的样式已统一到上方 .recite-judge .rj-status / .rj-tip 一处。
     这里原来还有一条 `.rj-tip { flex: none; margin-left: 4px }` ——
     那是「挤在 flex 主行里」时代的定位方式，与现在的独立状态行冲突，已删。
     （同一个类只留一条规则，避免两处打架 —— 第 52 轮弹窗那次就是被这种叠加坑过。） */

  .qz-submit {
    width: 100%; height: 44px; margin-top: 16px;
    border: 0; border-radius: var(--r-sm);
    background: linear-gradient(90deg, var(--brand-light), var(--brand));
    color: var(--surface-1); font-size: 16px; font-weight: var(--fw-bold);
    font-family: inherit; cursor: pointer; transition: var(--dur-fast);
    box-shadow: var(--sh-brand);
  }
  .qz-submit:hover { opacity: .93; }

  /* 快捷工具（收藏 / 反馈 / 字号）
     ★★ 从侧栏卡片挪到底部翻页条 ★★
       侧栏为配合「与答题区等高等齐」把整列都给了答题卡，工具不必再占一张卡；
       挪进翻页条后，它们与「上一题 / 下一题」同处一条操作带 ——
       都是「做题过程中会随手点一下」的动作，放一起更顺手。
     ★ 形态从「图标 + 文字」的宽按钮改成 38×38 图标按钮：
       侧栏有 420px 宽放得下文字，翻页条没有；含义改由 title 提示承担。
     ★ 三个按钮都是 38×38，满足 44px 触控建议的接近值（桌面上鼠标操作，
       且它们不是高频主操作，38 足够；主操作按钮仍是 44px 的 qbtn）。 */
  .qz-foot .qz-tools { display: flex; gap: 6px; flex: none; }
  /* ★★★ 从「38×38 纯图标」改成「图标 + 文字」★★★
     ★ 用户反馈：「这个反馈错题的标签现在看不出来是反馈错题」✗
     ★ 原注释自己就写着「含义靠 title 提示」✗ ——
       而 **tooltip 要悬停才出现** ✗ 扫一眼根本不知道是什么 ✓
     ★ 一个图标（那个气泡）在 17px 下更像"书签"或"文件" ✗
       再用 title 兜底 = 默认用户会去悬停 ✗ 这是设计上的偷懒 ✓
     ★ 现在：**图标 + 两个字** ✓ 一眼可读 ✓
       38px 固定宽改不了文字，所以改成"高度固定、宽度自适应" ✓ */
  .qz-foot .qz-tool {
    height: 38px; flex: none;
    padding: 0 12px;
    display: inline-flex; align-items: center; justify-content: center; gap: 6px;
    border: 1px solid var(--border-2); border-radius: var(--r-sm);
    background: var(--surface-1); color: var(--text-3);
    font-size: var(--fs-xs);
    cursor: pointer; transition: var(--dur-fast);
    white-space: nowrap;
  }
  .qz-foot .qz-tool .lb { line-height: 1; }
  /* ★ 收藏这一格：文案在「收藏 / 已收藏 / 收藏题」之间切换，
       文字槽定宽 3em 后按钮宽度不变 —— 不然点一下，旁边的「反馈 / 字号」
       会跟着横移一截，看着像闪 ✓ */
  .qz-foot .fav-tool .lb { width: 3em; }
  /* 只读的工具位：不能点 ✓ 但要说明"为什么不能点" ✓
     ★ 不压暗到看不出 ✗ —— 它是**信息**（这一组是收藏题）✓ 不是"不可用" ✗ */
  .qz-foot .qz-tool.ro { cursor: default; color: var(--gold-text); }
  .qz-foot .qz-tool.ro:hover { border-color: var(--border-2); background: var(--surface-1); color: var(--gold-text); }
  .qz-foot .qz-tool:hover { border-color: var(--brand-200); background: var(--brand-50); color: var(--brand); }
  /* ★ 底色走令牌：原来写死 #FDF6E3，夜间模式下是浅米底 + 亮金字，
     对比度只有 1.7:1 左右，几乎看不清 ✗ */
  .qz-foot .qz-tool.on { border-color: var(--gold-border); background: var(--gold-bg); color: var(--gold-text); }
  .qz-foot .qz-tool.on:hover { border-color: var(--gold); }
  .qz-foot .qz-tool svg { width: 17px; height: 17px; }
  /* 收藏选中时把星形填实，光靠颜色差异在 17px 图标上不够明显 */
  .qz-foot .qz-tool.on svg { fill: currentColor; }
  /* ★ 工具从 38px 变成"图标+文字"后更宽了 ✓
     窄屏时先让中间的快捷键提示让位 —— 它是次要信息 ✓
     工具是功能入口，不能让位 ✗ */
  @media (max-width: 1280px) {
    .qz-foot .hint { display: none; }
  }

  /* 底部翻页条：把「上一题 / 下一题」放进一条白卡，
     快捷键提示挪到中间（原来挤在工具栏最右边，几乎看不见） */
  .qz-foot {
    display: flex; align-items: center; gap: 14px;
    background: var(--surface-1); border-radius: var(--r-md);
    padding: 14px 20px; box-shadow: var(--sh-1); margin-top: 16px;
    flex: none;   /* ★ 同 .qz-top：底栏也按自身高度占位，不许被压缩 */
  }
  .qz-foot .sp { flex: 1; }
  .qz-foot .hint { font-size: 13px; color: var(--text-4); display: inline-flex; align-items: center; gap: 5px; }
  .qz-foot .hint kbd {
    font-family: inherit; background: var(--surface-2); border: 1px solid var(--border-2);
    border-radius: 4px; padding: 1px 6px; font-size: 12px; color: var(--text-3);
  }

  /* ★ 已移除：.tool-hint —— 快捷键提示改挂在 .qz-foot .hint 上，
     位置也从「工具栏最右边」挪到翻页条中间（原来贴在角落几乎看不见）。 */

  /* ========== 网课 ========== */
  .course-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 18px; }
  .course-card {
    display: flex; gap: 16px; background: var(--surface-1); border-radius: var(--r-md);
    padding: 16px; cursor: pointer; transition: .18s;
    box-shadow: 0 3px 16px rgba(var(--brand-rgb),.06); border: 1px solid transparent;
  }
  .course-card:hover { transform: translateY(-2px); box-shadow: 0 10px 26px rgba(var(--brand-rgb),.13); border-color: var(--brand-100); }
  .course-card .cv {
    width: 108px; height: 78px; flex: none; border-radius: var(--r-sm);
    background: linear-gradient(135deg, var(--brand-300), var(--brand-light)); color: var(--surface-1);
    display: flex; align-items: center; justify-content: center;
    font-size: 14px; overflow: hidden;
  }
  .course-card .cv img { width: 100%; height: 100%; object-fit: cover; }
  .course-card .cb { flex: 1; min-width: 0; display: flex; flex-direction: column; }
  .course-card .nm {
    font-size: 16px; font-weight: 500; color: var(--text-1); line-height: 1.55;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }
  .course-card .meta { font-size: 14px; color: var(--text-4); margin-top: 7px; }
  .course-card .pr { margin-top: auto; font-size: 18px; font-weight: 600; color: var(--danger); }
  .course-card .pr em { font-size: 14px; color: var(--decor-1); font-weight: 400; font-style: normal; margin-left: 8px; }

  /* ---------- 网课页：粉笔式课程列表（用户要求模仿该界面）----------
     ★ 只借【结构】，不借品牌：配色与圆角仍走站内令牌。
     ★ 这套结构解决的是同一批问题：
       ① 两签先把「能买的」和「已买的」分开，不用在一堆课里找自己买过的；
       ② 分类标签墙十几个平铺，比下拉框少一次点击、还能一眼看全都有什么；
       ③ 3 列【定高】卡片 —— 一排卡片底部对齐，价格横向可比，扫一列就够。 */
  .cs-tabs {
    display: flex; gap: 32px; margin-bottom: 18px;
    border-bottom: 1px solid var(--border-1);
  }
  .cs-tabs span {
    font-size: 18px; color: var(--text-4); cursor: pointer;
    padding: 0 2px 12px; position: relative; transition: var(--dur-fast);
  }
  .cs-tabs span:hover { color: var(--text-2); }
  .cs-tabs span.on { color: var(--text-1); font-weight: var(--fw-bold); }
  .cs-tabs span.on::after {
    content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 3px;
    border-radius: 2px; background: var(--brand);
  }

  /* ★ .cs-cats / .cs-chip（分类标签墙）已删除（用户要求撤掉那面墙）。
     本注释不写被删规则的原文，避免样式扫描出现假命中。 */

  /* ★ .cs-region（地区行）已删除（用户要求）。
     本注释不写被删规则的原文，避免样式扫描出现假命中。 */

  /* 3 列定高卡片 */
  .cs-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
  .cs-card {
    position: relative;
    display: flex; flex-direction: column;
    background: var(--surface-1); border: 1px solid var(--border-1);
    border-radius: var(--r-md); padding: 16px 16px 14px;
    /* ★ 定高：一排卡片底部对齐，价格横向可比 ——
       不定高的话长标题会把整排带歪，正是用户之前抱怨过的那类问题。 */
    min-height: 172px;
    cursor: pointer; transition: var(--dur-fast);
  }
  .cs-card:hover {
    border-color: var(--brand-200); box-shadow: var(--sh-2);
    transform: translateY(-1px);
  }

  .cs-top { display: flex; gap: 8px; margin-bottom: 8px; }
  .cs-name {
    flex: 1; min-width: 0; font-size: 15px; font-weight: var(--fw-bold);
    color: var(--text-1); line-height: 1.5;
    /* ★ 标题最多两行、超出省略 —— 不定行数的话卡片高度会被长标题带跑 */
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .cs-arw { flex: none; color: var(--text-4); font-size: 16px; line-height: 1.3; }

  .cs-date {
    font-size: 12px; color: var(--text-4); margin-bottom: 10px;
    font-variant-numeric: tabular-nums;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

  .cs-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
  .cs-tag {
    font-size: 11px; padding: 2px 8px; border-radius: var(--r-xs);
    background: var(--brand-50); color: var(--brand); border: 1px solid var(--brand-200);
    white-space: nowrap;
  }

  /* 底部：头像组靠左、价格靠右，用 margin-top:auto 顶到卡片底 ——
     这样不同高度的内容也能让「价格」落在同一条基线上。 */
  .cs-bottom { margin-top: auto; display: flex; align-items: flex-end; gap: 10px; }
  .cs-avatars { display: flex; flex: none; }
  .cs-avatars i {
    width: 22px; height: 22px; border-radius: 50%;
    border: 2px solid var(--surface-1); margin-left: -8px;
  }
  .cs-avatars i:first-child { margin-left: 0; }
  .cs-avatars i:nth-child(1) { background: var(--brand-100); }
  .cs-avatars i:nth-child(2) { background: var(--brand-200); }
  .cs-avatars i:nth-child(3) { background: var(--border-1); }

  .cs-price { margin-left: auto; text-align: right; }
  .cs-price b {
    font-size: 17px; font-weight: var(--fw-bold); color: var(--danger);
    font-variant-numeric: tabular-nums; letter-spacing: -.2px;
  }
  .cs-price em { display: block; font-size: 11px; color: var(--text-4); font-style: normal; margin-top: 2px; }

  /* 已购角标：与「我的课程」页签呼应，扫一眼就知道哪些已经买过 */
  .cs-owned {
    position: absolute; top: 0; right: 0;
    font-size: 11px; padding: 3px 9px; font-weight: var(--fw-bold);
    border-radius: 0 var(--r-md) 0 var(--r-sm);
    background: var(--success-bg); color: var(--success);
  }

  /* ---------- 翻页（用户要求：购买课程显示全部课程，记得加翻页）----------
     ★ 居中排布：页数不多时它是一条居中的细控件，不抢卡片流的注意力。
     ★ 「共 N 门 · 第 x / y 页」放在右侧而不是中间 ——
       它是【读】的信息，不是【点】的目标，跟页码混在一起会误点。 */
  .cs-pager {
    display: flex; align-items: center; justify-content: center;
    gap: 8px; margin-top: 26px; padding-top: 20px;
    border-top: 1px solid var(--border-1);
    /* ★ 加 relative 是为了给右侧那行说明文字做定位参照 ——
       写在【同一条规则】里，不另开一条；同一个选择器分两处写，
       改一处不生效的时候很难找（这个坑踩过不止一次）。 */
    position: relative;
  }
  .cs-pager .pg {
    min-width: 36px; height: 36px; padding: 0 12px;
    border: 1px solid var(--border-2); border-radius: var(--r-sm);
    background: var(--surface-1); color: var(--text-2);
    font-size: 14px; font-family: inherit; cursor: pointer;
    font-variant-numeric: tabular-nums;
    transition: var(--dur-fast);
  }
  .cs-pager .pg:hover:not(:disabled):not(.on) { border-color: var(--brand); color: var(--brand); background: var(--brand-50); }
  .cs-pager .pg.on {
    border-color: var(--brand); background: var(--brand); color: #fff;
    font-weight: var(--fw-bold); cursor: default;
  }
  .cs-pager .pg:disabled { opacity: .45; cursor: not-allowed; }
  .cs-pager .pg-dot { color: var(--text-4); padding: 0 2px; user-select: none; }
  /* ★ 说明文字用绝对定位挂到右边：它是【读】的信息，不是【点】的目标。
     放进 flex 流里会把居中的页码挤偏，混在页码里还容易误点。 */
  .cs-pager .pg-info {
    position: absolute; right: 0;
    font-size: 13px; color: var(--text-4); font-variant-numeric: tabular-nums;
  }

  .course-hero {
    height: 268px; border-radius: var(--r-md); margin-bottom: 18px;
    background: linear-gradient(135deg, var(--brand-light), var(--brand));
    display: flex; align-items: center; justify-content: center; cursor: pointer;
    box-shadow: 0 10px 26px rgba(var(--brand-rgb),.22);
  }
  .course-hero .play {
    width: 70px; height: 70px; border-radius: 50%;
    background: rgba(255, 255, 255, .92);
    display: flex; align-items: center; justify-content: center;
    transition: .2s;
  }
  .course-hero:hover .play { transform: scale(1.08); }
  .course-hero .play svg { width: 24px; height: 24px; color: var(--brand); margin-left: 4px; }

  .lesson-row {
    display: flex; align-items: center; gap: 12px;
    padding: 13px 14px; border-radius: var(--r-sm); cursor: pointer; transition: .15s;
    font-size: 16px;
  }
  .lesson-row:hover { background: var(--surface-2); }
  .lesson-row .idx { color: var(--text-4); font-size: 14px; flex: none; min-width: 32px; }
  .lesson-row .nm { flex: 1; min-width: 0; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .lesson-row .dur { font-size: 14px; color: var(--text-4); flex: none; }
  .lesson-row .play-btn { color: var(--brand); font-size: 14px; flex: none; }

  /* ========== 简答题 / 共享题干 / 详情底栏 ========== */
  .shared-stem {
    background: var(--surface-2); border-radius: var(--r-md); padding: 18px 22px;
    margin-bottom: 22px; border-left: 3px solid var(--brand);
  }
  .shared-stem .ss-t { font-size: 14px; color: var(--brand); font-weight: 500; margin-bottom: 10px; }
  /* ★★ 材料题正文：同样限高 + 内部滚动 ★★
     这与上面 .an-c 是【同一类问题】：材料题的一段背景材料可能上千字，
     不封顶的话会把选项和解析一起顶到屏幕外 —— 页面被一条材料拉得极长。
     ★ 额度比解析大（320px ≈ 10 行）：材料是【必须先读完才能作答】的内容，
       封得太狠会逼用户在一个小框里反复滚，影响做题。
       「标题行常驻 + 正文可滚」这个结构不变。
     ★ overscroll-behavior: contain 与细滚动条的处理同 .an-c。 */
  .shared-stem .ss-c {
    font-size: 16px; line-height: 1.95; color: var(--text-2); white-space: pre-wrap;
    max-height: 320px;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    padding-right: 4px;
  }
  .shared-stem .ss-c::-webkit-scrollbar { width: 6px; }
  .shared-stem .ss-c::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 3px; }
  .shared-stem .ss-c::-webkit-scrollbar-track { background: transparent; }

  /* ══════ 长内容「展开全文」按钮（材料 / 解析共用）══════
     ★ 用户口径：材料与答题技巧都很长，加个眼睛按钮点一下直接看全部
     ★ 收起态沿用原来的限高 + 内部滚动（.ss-c 320px / .an-c 240px）✓
     ★ 展开态解除限高、去掉内滚动条，让整段自然撑开 ✓
     ★ 颜色只用令牌 → 日间 / 夜间 / 护眼三套主题自动跟随 ✓ */
  .eye-btn {
    display: inline-flex; align-items: center; gap: 6px;
    height: 28px; padding: 0 12px; border-radius: 999px;
    border: 1px solid var(--border-2); background: var(--surface-1);
    color: var(--text-3); font-size: 13px; font-weight: 500;
    cursor: pointer; flex: none; transition: .15s;
  }
  .eye-btn:hover { border-color: var(--brand); color: var(--brand); }
  .eye-btn.on { border-color: var(--brand); background: var(--brand-50); color: var(--brand); }
  .eye-btn svg { flex: none; }
  /* 标题行改成两端对齐：左边标签、右边按钮 */
  .shared-stem .ss-t,
  .judge-box .an-t { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
  /* 展开态：解除限高与内滚动 */
  .shared-stem .ss-c.is-open,
  .judge-box .an-c.is-open { max-height: none; overflow: visible; padding-right: 0; }

  /* ---------- 题库详情弹窗 ----------
     ★ 复用项目已有的三样东西，只补「弹窗本体 + 滚动区 + 常驻页脚」：
         .modal-mask  —— 遮罩 + 淡入
         .modal-head / .mh-t / .mh-x —— 头部（标题 + 关闭）
         @keyframes modalIn —— 从下浮起 + 轻微回弹的入场

     ★ 宽度取 760px：详情里有「章节目录」的卡片网格（.chapter-list 是
       auto-fill minmax(240px,1fr)）。内容宽 712px 时正好排 2 列；
       若沿用别的弹窗的 660px 只有 612px，卡片会显得局促。

     ★ 高度用 max-height 而不是固定高：
       题库内容少（无套数 / 无章节）时弹窗自动缩矮，不留一片空白；
       内容多时封顶到视口，由中间区滚动。 */
  .bank-modal {
    background: var(--surface-1); border-radius: var(--r-lg);
    box-shadow: var(--sh-modal);
    width: 100%; max-width: 760px;
    max-height: calc(100vh - 80px);
    display: flex; flex-direction: column;
    overflow: hidden;
    animation: modalIn var(--dur-base) cubic-bezier(.34, 1.4, .64, 1) both;
  }
  .bank-modal-body { flex: 1; overflow-y: auto; padding: 20px 24px 4px; }
  .bank-modal-foot {
    flex: none; display: flex; gap: 12px; align-items: center;
    padding: 14px 24px; border-top: 1px solid var(--border-1);
  }
  .bank-modal-foot .qbtn { padding: 0 22px; flex: none; }
  .bank-modal-foot .qbtn.primary { flex: 1; }

  /* 弹窗内的 hero：底色改浅灰 ——
     它原来是「白卡 + 投影」，放进同样是白的弹窗里会糊成一片、看不出卡片边界。 */
  .bank-modal-body .bank-hero {
    background: var(--surface-2); box-shadow: none;
    padding: 20px 22px; margin-bottom: 20px;
  }

  /* ★ 已移除：.detail-bar（题库详情底部操作条）
     详情改为弹窗后，那组按钮变成了常驻页脚 .bank-modal-foot，
     .detail-bar 已无任何使用者。 */

  .trial-ok { text-align: center; padding: 12px 6px 4px; }
  .trial-ok .em { font-size: 40px; margin-bottom: 16px; line-height: 1; }
  .trial-ok .t1 { font-size: 20px; font-weight: 600; color: var(--text-1); margin-bottom: 10px; }
  .trial-ok .t2 { font-size: 15px; color: var(--text-4); line-height: 2; }

  /* 我的 */
  .panel { background: var(--surface-1); border-radius: var(--r-md); padding: 32px; box-shadow: 0 2px 12px rgba(0,0,0,.04); }
  .panel h2 { font-size: 22px; margin-bottom: 20px; }
  .info-row { display: flex; padding: 14px 0; border-bottom: 1px solid var(--surface-3); font-size: 16px; }
  .info-row .k { width: 110px; color: var(--text-4); flex: none; }
  .info-row .v { word-break: break-all; }

/* ★ 头像图片：铺满圆形 ✓ 三处共用（顶栏 / 我的面板 / 个人信息） */
.avatar img, .av img {
  width: 100%; height: 100%; object-fit: cover; display: block; border-radius: 50%;
}

/* ★ 「我的数据」云端统计失败提示（只在出错时出现，不占版面） */
.ks-warn {
  margin-top: 10px;
  font-size: 12px; line-height: 1.5;
  color: #B4681C; background: #FFF4E8;
  border: 1px solid #F2D3AC; border-radius: 8px;
  padding: 7px 10px; word-break: break-all;
}


/* ══════════════════════════════════════════════════════════════════════════
   移动端 / 平板自适应  2026-10-24
   ══════════════════════════════════════════════════════════════════════════

   ★★★ 铁律：这一整段【只覆盖、不修改】上方任何规则 ★★★
     上面 4900 多行是 PC 端布局，一行都不动。
     所有适配都写在 @media 里 —— 宽于断点时这些规则根本不参与计算，
     桌面端的渲染结果与改造前【逐像素一致】。
     要回滚：删掉从这里到文件末尾即可，不需要动别的任何地方。

   ★ 断点三档
     ≤1024  平板：栏宽收窄、三列→两列
     ≤768   手机：全部单列、顶栏改两行
     ≤420   小屏：字号与间距再收一档

   ★ 为什么要三档而不是一档
     768~1024 这个区间（iPad 竖屏、小笔记本半屏）如果直接给单列，
     空间浪费很多；给两列更合理。所以要一档专门处理它。

   ★ 三个已知的移动端坑（下面各有对应处理）
     ① 100vh 不可靠：地址栏伸缩时 100vh 大于可视高度 → 底部被切
        → 用 100dvh；拿不准就直接放弃定高（刷题页就是这么做的）
     ② 底部安全区：iPhone Home Indicator 占 34px，贴底元素要留出来
        → padding-bottom: env(safe-area-inset-bottom)
     ③ iOS 输入框聚焦缩放：font-size < 16px 时 Safari 会自动放大页面
        → 输入框字号一律 ≥16px（全局 input 本来就是 16px，别降）
   ══════════════════════════════════════════════════════════════════════════ */


/* ═══════════════ ① 平板 / 窄笔记本：< 1360px ═══════════════
   ★ 为什么断点不是 1024 而是 1359：
     原设计的 body{min-width:1360px} 是「窄于 1360 就出横向滚动条」。
     → 1280 宽的笔记本（13 寸 MacBook、125%/100% 缩放的 Windows 本）会被判进
       这个区间，右侧直接多出一条横向滚动条 ✗ 这是改造前就存在的问题。
     既然做了响应式，就顺手把这段填上：1025~1359 走「两列紧凑」而不是滚动条。
     1360 及以上【完全不受影响】，桌面原设计的分毫不变。 */
@media (max-width: 1359px) {

  /* ★ 全站解锁的第一步。不解这条，下面写什么都没用 ——
     body 仍然 1360 宽，浏览器照样把整页缩小显示 */
  body { min-width: 0; }

  .content { max-width: 100%; padding: 18px 20px 48px; }

  /* ── 首页：侧栏收窄，三列→两列 ── */
  .home-hero  { grid-template-columns: 240px minmax(0, 1fr); }
  .home-body  { grid-template-columns: minmax(0, 1fr) 260px; }
  .home-top   { grid-template-columns: 1fr; }
  .home-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .overview   { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* ── 题库：章节树收窄 ── */
  .ch-wrap    { grid-template-columns: 220px minmax(0, 1fr); }
  .hcs-grid   { grid-template-columns: repeat(3, 1fr); }

  /* ── 刷题：右栏 400 → 320，把主内容让出来 ── */
  .qz-split   { grid-template-columns: minmax(0, 1fr) 320px; }

  /* ── 列表类：三列→两列 ── */
  .cs-grid    { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .pbs-row    { grid-template-columns: repeat(2, 1fr); }

  /* ── 两栏等比 ── */
  .tp-wrap    { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }

  /* ── 顶栏：间距收紧，搜索框变短 ── */
  .topbar-center  { gap: 18px; }
  .topbar-search  { width: 210px; }
  .menu span      { font-size: 18px; padding: 9px 14px; }
}


/* ═══════════════ ② 手机：< 768px ═══════════════ */
@media (max-width: 768px) {

  body { min-width: 0; }
  /* ★ 底部留 76px：右下浮条是 fixed 的（约 44px 高 + 10px 边距），
     不留出来最后一块内容永远被它压住 */
  .content { max-width: 100%; padding: 12px 12px 76px; }

  /* ★ 兜底：任何图片都不许撑破容器。
     移动端最容易出的问题就是某个固定宽度的插图把整页顶出横向滚动条 */
  img, svg { max-width: 100%; }


  /* ══════ 顶栏：单行 → 两行 ══════

     桌面结构（一行）：
       logo · [首页 分类 题库] 搜索框300px · 头像 昵称 退出
     390px 放不下，而且 .topbar-center 是 position:absolute 绝对居中的，
     窄屏下它会直接压在 logo 和头像上。

     改成两行：
       第一行：logo ······················ 头像 + 退出
       第二行：首页 分类 题库 ········ [搜索框]

     ★ 为什么用纯 CSS 而不是汉堡菜单：汉堡要改模板 + 加 JS 状态 + 处理层级，
       成本翻倍且引入回归风险；两行式一行模板都不用动。 */
  .topbar { height: auto; }

  .topbar-center {
    position: static; transform: none;      /* ← 取消绝对居中，回到文档流 */
    width: 100%; height: auto;
    flex-wrap: wrap; row-gap: 6px;
    padding: 10px 12px 8px;
    gap: 0;
  }

  /* 排布：第一行 品牌 · 导航 · 头像/退出    第二行 搜索框（独占一行）
     ★ 这里踩过一个坑：原打算用 .user{margin-left:auto} 让导航换行 —— 不行。
       flex 换行【分线阶段会把 auto 外边距当作 0】，所以导航照样挤在第一行，
       auto 只在分完线之后才去吃剩余空间。
       → 真正能强制换行的只有 flex-basis：给搜索框 100% 的基准宽度，
         它必然独占一行；前三块按自然顺序排在第一行。 */
  .logo           { order: 1; flex: 0 0 auto; }
  .menu           { order: 2; flex: 0 0 auto; }
  .user           { order: 3; flex: 0 0 auto; margin-left: auto; }
  .topbar-search  { order: 4; flex: 1 1 100%; min-width: 0; }

  .logo-txt { display: none; }              /* 只留图标，省横向空间 */
  .uname    { display: none; }              /* 昵称省掉，头像够识别 */

  .menu span { font-size: 15px; padding: 11px 12px; }   /* 高度做到 ~40px */

  .topbar-search { width: auto; height: 34px; }
  /* ★ 16px：低于 16px 时 iOS Safari 聚焦会自动放大整页，体验很糟 */
  .topbar-search input { font-size: 16px; }

  .avatar { width: 36px; height: 36px; font-size: 14px; }
  .logout { min-height: 38px; padding: 0 12px; font-size: 13px; }

  .dev-bar { padding: 8px 12px; font-size: 13px; }


  /* ══════ 登录页 ══════

     ★ 这是改造前最严重的一页：390px 下 .login-card 在 x=450 处，
       完全在屏幕右边外面 ⇒ 手机上打开登录页是【一片纯白】。
       根因就是 .login-card 写死了 width: 460px。 */
  .login-wrap {
    min-height: 100dvh;                     /* 100vh → 100dvh */
    align-items: flex-start;
    padding: 24px 16px 32px;
    /* 纵向允许滚（卡片比屏幕高时能看全），横向继续裁掉光晕 */
    overflow-x: hidden; overflow-y: auto;
  }
  .login-card {
    width: 100%; max-width: 460px;
    padding: 30px 20px 26px;
    border-radius: 18px;
  }
  .brand .title, .brand .cn { font-size: 26px; }
  .tabs button { font-size: 14px; padding: 9px 10px; }
  .ipt { height: 46px; }                    /* 保持 ≥44 触控高度 */
  .btn-login { height: 48px; }
  .wx-qr { width: 180px; height: 180px; }


  /* ══════ 首页：三栏全部塌成单列 ══════ */
  .home-hero  { grid-template-columns: 1fr; }
  .home-body  { grid-template-columns: 1fr; }
  .home-top   { grid-template-columns: 1fr; }
  .home-cards { grid-template-columns: 1fr; }
  .overview   { grid-template-columns: 1fr; }


  /* ══════ 分类 / 题库页 ══════ */
  .ch-wrap { grid-template-columns: 1fr; }
  /* ★ 单列后章节树会很长，限高让它自己滚，否则要划很久才看到题目
     （:first-child 就是左侧那棵章节树） */
  .ch-wrap > *:first-child { max-height: 42vh; overflow-y: auto; overscroll-behavior: contain; }

  /* ★ 「地区 / 城市 / 岗位」三栏：桌面是三列并排，而且行高被写死 470px。
     手机上三列一起挤会把标题压成竖排（「选 择 地 区」）并裁掉内容 ✗
     → 改单列，同时必须把 grid-template-rows 的 470px 放开，
       否则每张卡还是固定 470 高，三张叠起来 1400 多像素 */
  .cat-wrap {
    grid-template-columns: 1fr;
    grid-template-rows: auto;
  }
  .cat-wrap > * { max-height: 56vh; overflow-y: auto; overscroll-behavior: contain; }

  .hcs-grid     { grid-template-columns: repeat(2, 1fr); }
  .chapter-list { grid-template-columns: 1fr; }
  .ver-grid, .ver-grid.few { grid-template-columns: 1fr; }
  .pb-main      { grid-template-columns: 1fr; }
  .pbs-row      { grid-template-columns: repeat(2, 1fr); }
  .tl-chs       { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .tp-wrap      { grid-template-columns: 1fr; }
  .mybank-grid  { grid-template-columns: 1fr; }
  .course-grid  { grid-template-columns: 1fr; }
  .cs-grid      { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .pick-grid    { grid-template-columns: 1fr; }
  .hot-grid     { grid-template-columns: 1fr; }
  .fd-form      { grid-template-columns: 1fr; }
  .ks-tiles.two  { grid-template-columns: 1fr; }
  .ks-tiles.four { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* ★ 日历必须保持 7 列（拆了就失去日历语义），只能缩小格子与间距 */
  .cal-grid     { gap: 2px; }


  /* ══════ 刷题页：两栏 → 单列 ══════

     ★ .qz-split 原为 grid-template-columns: 1fr 400px
       右栏固定 400px —— 单这一条就超过 390px 整屏，是刷题页在手机上
       必然溢出的直接原因。
     ★ .quiz 原来的 height: calc(100vh - 138px) 也放弃：
       移动端 100vh 随地址栏变化而不准，定高只会把内容顶出屏幕。 */
  .quiz {
    max-width: 100%;
    height: auto;
    min-height: 0;
  }
  .qz-split { grid-template-columns: 1fr; }

  /* 原来内部滚动（PC 定高布局的产物），移动端改页面自然滚动 */
  .qz-main     { overflow: visible; max-height: none; }
  .qz-main-box { max-height: none; }

  /* 答题卡从右侧栏挪到题目下方 */
  .qz-side     { max-height: none; overflow: visible; }

  /* ★ 答题卡格子：8 / 10 列在手机上每格不到 30px，点不中。
     统一压到 6 列，保证单格 ≥44px 的可点面积 */
  .qz-cells.d-md .qz-group-grid,
  .qz-cells.d-lg .qz-group-grid,
  .qz-cells.d-xl .qz-group-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }

  /* ★ 底栏吸底：上一题 / 下一题 不用每次划到最底 */
  .qz-foot {
    position: sticky; bottom: 0; z-index: 30;
    margin-top: 12px;
    padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
    box-shadow: 0 -2px 12px rgba(22, 35, 58, .08);
  }
  .qz-foot .hint, .qz-foot kbd { display: none; }   /* ← → 键盘提示在手机上没有意义 */
  .qz-tool .lb { display: none; }                   /* 工具按钮只留图标 */


  /* ══════ 右下浮动工具（我的 / 反馈 / 客服 / 阅读 / 技巧）══════
     ★ 竖排 5 个 52px 会占掉右侧 292px 高度，手机上太挡内容
       → 改横排 44px，保证触控面积的同时只占一条 */
  .dock {
    flex-direction: row;
    right: 8px; bottom: 10px;
    gap: 6px;
  }
  .dock-i { width: 44px; height: 44px; font-size: 11px; gap: 1px; }
  /* 刷题页有吸底操作条，把浮条抬到它上面，避免压住「下一题」 */
  body:has(.qz-foot) .dock { bottom: calc(72px + env(safe-area-inset-bottom)); }


  /* ══════ 弹窗 / 面板 ══════ */
  .modal-mask { padding: 14px; align-items: flex-end; }
  .modal {
    max-width: 100%;
    max-height: calc(100dvh - 28px);
    border-radius: 16px 16px 0 0;      /* 手机上从底部弹起 */
  }
  .panel    { padding: 20px 16px; }
  .info-row { font-size: 15px; }
  .info-row .k { width: 84px; }
}


/* ═══════════════ ③ 小屏：< 420px ═══════════════ */
@media (max-width: 420px) {

  .content { padding: 10px 10px 28px; }

  .topbar-center { padding: 8px 10px 6px; }
  /* ★ 别再往下压 padding：导航项一压就掉到 30px 高，手机上容易点错 */
  .menu span     { font-size: 14px; padding: 10px 10px; }
  .logout        { font-size: 12px; padding: 0 8px; }

  .login-card { padding: 26px 16px 22px; }
  .brand .title, .brand .cn { font-size: 23px; }
  .tabs button { font-size: 13px; padding: 8px 6px; }
  .wx-qr { width: 160px; height: 160px; }

  .cs-grid  { grid-template-columns: 1fr; }
  .hcs-grid { grid-template-columns: repeat(2, 1fr); }

  .dock { right: 6px; bottom: 8px; gap: 5px; }
  /* ★ 触控目标不再往下缩：44px 是下限，缩到 42 只省 2px 却更容易点错 */
  .dock-i { width: 44px; height: 44px; font-size: 10px; }

  .qz-cells.d-md .qz-group-grid,
  .qz-cells.d-lg .qz-group-grid,
  .qz-cells.d-xl .qz-group-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}


/* ══════════════════════════════════════════════════════════════════════════
   ④ 平板 / iPad 专项修补（2026-10-24 第二轮 · 用户 iPad 实测反馈）

   用户报四条，全部在这一段解决。仍然【只覆盖、不修改】上方任何规则 ——
   要回滚：删掉从这里到文件末尾即可。

   ① 右侧被截断
      .qz-top 是一行【不换行】的 flex：
        ‹返回 64 + 标题 117 + 进度条 + ✓✗ 90 + 模式 278，再加 40 内边距，
        最窄要 640px 才装得下。
      竖屏 390px 时不够宽 → .qz-modes 被顶到 x=651（视口才 390），
      整页横向溢出 261px：进度条、模式切换、右侧答题卡全被推出屏幕外。
      → 手机档让页头换行，模式独占一行、三段等分。

   ② 宽屏窄页
      .qz-split 的右栏是【写死的 400px】：
        1366 屏主栏只剩 786（占 57%），820 竖屏更只剩 360 —— 右边却还
        压着一张 320 宽的答题卡，题目被挤成一条窄缝。
      → ≤1439 改成 clamp(280px, 26vw, 340px) 按比例走；
        ≤1100 且【竖屏】时直接单列，答题卡移到题目下方，主栏吃满整宽。

   ③ 顶栏拥挤
      820/1024 宽时顶栏内容共 785px，两边余量只剩 35px；搜索框 210px
      装不下 12 个字的占位符，文字被切在框里 —— 看起来就像「搜索框被截断」。
      → ≤1359 搜索框收成 40×40 放大镜。
        点它仍然走 topSearch()：切到搜索页，搜索页里有完整输入框，
        所以【不丢功能】✓（已核 index.html 第 2265 行，不是猜的）。
        真正的「整行搜索框」留给手机档（≤768 本来就是 flex-basis:100%）。

   ④ 触控区偏小
      .opt 内边距 14/18、高度 60px，是鼠标时代的尺寸。平板是手指点，
      按压区按 48 起算。
      → ≤1439 加到 19/20（高度约 70）；答题卡格子 38 → 42。
   ══════════════════════════════════════════════════════════════════════════ */


/* ───────── ④-1 平板顶栏：搜索框收成放大镜（769 ~ 1359） ───────── */
@media (min-width: 769px) and (max-width: 1359px) {

  .topbar-search {
    width: 40px; height: 40px; padding: 0;
    justify-content: center;
    cursor: pointer;
  }
  /* ★ 只藏 input，不藏整个框：外面的胶囊底、边框、hover 都保留，
     点哪儿都还是那个 topSearch()（svg 上的 @click 仍在） */
  .topbar-search input { display: none; }
  .topbar-search .ico { width: 19px; height: 19px; }
  .topbar-search:hover .ico { color: var(--brand); }

  /* 导航字号再收一档：原来 18px/14px 内边距，四项加起来 244px，
     在 820 这种窄平板上和 logo、用户区挤成一片 */
  .menu span { font-size: 17px; padding: 9px 12px; }
}


/* ───────── ④-2 平板主栏比例 + 触控尺寸（≤ 1439） ─────────
   ★ 为什么上界取 1439 而不是 1359：
     iPad Pro 13 寸横向是 1366×1024（M4 为 1376×1032）——
     它【刚好卡在 1360 之上】，所以第一轮的 1359 断点完全没覆盖到它，
     平板横屏拿到的还是桌面那套 400px 固定右栏。 */
@media (max-width: 1439px) {

  .qz-split { grid-template-columns: minmax(0, 1fr) clamp(280px, 26vw, 340px); }

  /* 选项：14/18 → 19/20，高度 60 → 约 70；顺带把 A/B/C/D 圆标和文字间距加大 */
  .opt { padding: 19px 20px; }

  /* 答题卡格子：38 高对触屏偏小；宽度由 6 等分决定，不用管 */
  .card-cell { height: 42px; }
}


/* ───────── ④-3 平板（≤ 1100）：浮条收小，答题页收起 ─────────
   ★ 曾经试过把它改成横排（240×46 的一条贴底），实测更糟：
     首页 1024×768 上它正好压住「开始刷题」主按钮 ——
     横排只是把 5 个按钮从竖条变成横条，覆盖面积反而更大 ✗
   → 改回竖排，尺寸从 52 收到 44，与桌面端同一套行为：
     只压住卡片右边距，不压控件。
   ★ 原来 .quiz 在 ≤1512 时有 80px 右内边距；这里取消，
     因为答题页干脆不显示浮条了，不需要让位。 */
@media (max-width: 1100px) {

  .quiz { padding-right: 0; }

  .dock { flex-direction: column; right: 8px; bottom: 16px; gap: 6px; }
  .dock-i { width: 44px; height: 44px; font-size: 11px; gap: 1px; }

  /* ★ 键盘快捷键提示（← → 翻题 · A~D 选答案）在平板上没有意义，
     却占着 220px —— 去掉它，「上一题」才不会被压成两行竖排 */
  .qz-foot .hint, .qz-foot kbd { display: none; }
  .qz-foot .qbtn { flex: none; white-space: nowrap; }
  /* ★ 刷题页直接收起浮条 —— 不是"抬到操作条上方"：
     操作条高度是【变的】（考试模式多一个「交卷」、末题位文案更长），
     用固定 bottom 值去抬，总有一天会被它压住。
     而浮条这 5 项在答题页本来就近乎重复：
       我的/客服/顶部 走顶部导航，
       收藏/反馈/字号 底栏里全有（见 .qz-tools）✓
     → 只在答题页收起，其它页面照常显示。 */
  body:has(.qz-foot) .dock { display: none; }
}


/* ───────── ④-4 平板竖屏：答题卡移到题目下方，主栏吃满整宽 ─────────
   ★ 用 orientation 而不是固定宽度：同一台 iPad 横过来是 1024/1366，
     两栏是舒服的；竖起来只有 820，两栏就变成两条窄缝。 */
@media (max-width: 1100px) and (orientation: portrait) {

  .quiz { height: auto; }
  .qz-split { grid-template-columns: 1fr; }

  /* 单列后不再有「固定高度 + 内部滚动」，改回整页滚 */
  .qz-main-box { max-height: none; }
  .qz-main     { max-height: none; overflow: visible; }
  .qz-side     { max-height: none; overflow: visible; }
  .qz-sb       { display: none; }        /* 自绘滚动条只服务于内部滚动，这里没有 */

  /* 底部操作条改吸底：页面变长了，「下一题」不能跟着滚走 */
  .qz-foot {
    position: sticky; bottom: 0; z-index: 30;
    margin-top: 12px;
    padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
    box-shadow: 0 -2px 12px rgba(22, 35, 58, .08);
  }
}


/* ───────── ④-5 手机：答题页页头换行（≤ 768） ─────────
   ★ 覆盖掉上方 ② 里那套「一行塞满」的假设；值全部靠 order 重排，
     没有一条依赖 DOM 顺序，所以 questions 为空 / 没有 v2chip 时也不会错位。 */
@media (max-width: 768px) {

  .qz-top {
    flex-wrap: wrap;
    gap: 10px 12px;
    padding: 12px 14px;
  }

  .qz-top .bk  { order: 1; padding: 10px 8px; }          /* 触控高度 44 */
  .qz-top .tt  { order: 2; flex: 1 1 auto; min-width: 0; max-width: none; }
  .qz-top .v2chip { order: 3; max-width: 100%; }
  .qz-top .mid    { order: 4; flex: 1 1 100%; }

  /* 进度条独占第二行：basis 120px 让它一定换行，又不会宽到把 ✓✗ 挤掉 */
  .qz-prog   { order: 4; flex: 1 1 120px; }
  .qz-prog .track { min-width: 40px; }
  .qz-stats  { order: 5; flex: none; }
  .qz-stats .s { font-size: 12px; padding: 4px 8px; }

  /* 模式切换整行三等分：原来 278px 宽的一整块，手机上就是溢出元凶 */
  .qz-modes { order: 6; flex: 1 1 100%; }
  .qz-modes span { flex: 1; text-align: center; padding: 9px 4px; }

  /* 题型胶囊不许被压成竖排（单 / 选 / 题 三行） */
  .qz-qhead { flex-wrap: wrap; row-gap: 8px; }
  .qz-qhead .ty { flex: none; white-space: nowrap; }

  /* 卡片左右内边距 36 → 18：366 的屏上省出 36px 给选项 */
  .q-card { padding: 22px 18px; }

  .opt { padding: 18px 16px; gap: 12px; }

  /* 顶栏搜索框 34 → 40：手机上它是整行宽，34 高偏薄 */
  .topbar-search { height: 40px; }

  /* ★ 底栏按钮不许缩成「上 一 题」三行：
       底栏一行要塞 上一题 + 4 个工具 + 下一题，390 屏上只有 346px，
       flex 默认允许收缩 → 文字被压成竖排 ✗
     → 按钮 flex:none + nowrap，工具区略缩，实在放不下时由 420 档换行 */
  .qz-foot { gap: 8px; }
  .qz-foot .sp { display: none; }
  .qz-foot .qbtn {
    flex: none; white-space: nowrap;
    padding: 0 12px; font-size: 14px; height: 42px;
  }
  .qz-foot .qz-tools { gap: 4px; }
  .qz-foot .qz-tool { width: 40px; height: 40px; }
}


/* ───────── ④-6 手机小屏：底栏换两行（≤ 420） ─────────
   390 屏：上一题 78 + 下一题 78 + 工具 172 + 间距 = 349 > 346。
   ★ 用 order 把「上一题」「下一题」提到第一行、工具整行放第二行 ——
     不能靠 DOM 顺序，DOM 里工具是夹在两个按钮中间的。 */
@media (max-width: 420px) {

  .qz-foot { flex-wrap: wrap; row-gap: 8px; padding: 10px 10px calc(10px + env(safe-area-inset-bottom)); }
  .qz-foot > .qbtn:first-child { order: 1; flex: 1 1 0; }
  .qz-foot > .qbtn:last-child  { order: 2; flex: 1 1 0; }
  .qz-foot .qz-tools { order: 3; flex: 1 1 100%; justify-content: center; }
}


/* ───────── ④-7 触控目标保底（≤ 1439，平板 / 手机） ─────────
   ★ 依据：tokens.css --tap-min: 44px（WCAG 2.5.5）。
     PC 上 .logout 故意压到 32 高（鼠标场景，注释见 style.css 第 709 行），
     但平板是手指，必须回到 44。
   ★ 只给【交互控件】加，不给布局容器加，避免把版式撑开。
   ★ 上界取 1439 而不是 1100：iPad Pro 13 横向是 1366×1024，
     它比 1100 宽 —— 用 1100 会漏掉用户手上这台机器。 */
@media (max-width: 1439px) {

  .logout { height: 44px; min-height: 44px; }

  /* 模式切换：加 min-height 而不是加 padding —— padding 会把整条加宽 */
  .qz-modes span {
    min-height: 40px;
    display: inline-flex; align-items: center; justify-content: center;
  }
  .qz-top .bk { min-height: 40px; }
  .qz-foot .qz-tool { min-height: 42px; }
  .qz-tool { min-height: 42px; }
}


/* ───────── ④-8 两栏布局下的答题卡密度（平板横屏） ─────────
   ★ 侧栏从 400 收到 280~340 后，原来 6 列会把格子挤到 29px 宽（实测），
     手指根本点不准。降到 4 列：280 侧栏下约 45px，340 侧栏下约 62px。
   ★ 只在【横屏】生效：竖屏是单列，答题卡铺满整宽，6 列反而更好。 */
@media (min-width: 769px) and (max-width: 1439px) and (orientation: landscape) {

  .qz-cells.d-md .qz-group-grid,
  .qz-cells.d-lg .qz-group-grid,
  .qz-cells.d-xl .qz-group-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}


/* ══════════════════════════════════════════════════════════════════════════
   ⑤ 题库详情页：两栏比例 / 章条列数 / 小节行对齐（2026-10-24 第三轮）

   这一轮修的是上一轮【自己改坏】的地方 + 两处一直存在的对齐缺陷。
   仍然只覆盖、不修改上方规则；回滚就删到文件末尾。

   ① 两栏比例被我压成了 1:1
      桌面端 .tp-wrap 是 892 : 449（≈2:1），主栏才是"主"。
      上一轮在 ≤1359 写了「两栏等比 1fr 1fr」，1024 下主栏从 ~660 掉到 482：
        · 4 个章条挤进 482 → 每个 106px，8 个章名【全部】被省略号截断
        · 小节行只剩 447，而 7 个格子最少要 683 → 子项整体溢出行框
      实测（改前）：1024 下 .sl-row 宽 447，「开始做题」的右边缘到 754 —— 越界 307px。

   ② 小节行用 flex + 固定宽度，缺一项就整体左移
      行内是「序号 26 / 名称 210 / 题数 / 进度条 flex / 百分比 46 / 已完成 118 / 按钮」。
      · 题数标签 .hc-tag 是 auto 宽，"46 题"和"132 题"不一样长 → 右边全跟着挪
      · total=0 的行没有 .sl-tag 也没有 .sl-bar（模板里 v-if）→ 整行再挪一次
      实测「已完成」的左边缘：1024 下是 [379, 538, 546] 三个值，1440 下是 [399, 671] 两个值
      → 这就是用户看到的「第 2 项没和上面对齐」。
      修法：改成 Grid【固定轨道 + 显式 grid-column】。
      缺元素时轨道不会塌陷（grid 里没放东西的格子照样占位），
      所以不管哪一行缺什么，右边四列的竖线永远对齐 ✓
   ══════════════════════════════════════════════════════════════════════════ */


/* ───────── ⑤-1 两栏恢复 2:1（≤ 1439） ─────────
   ★ 用 fr 而不是固定 px：1439 以下宽度是连续的，
     固定 260px 会在 1440 断点前后跳一下。 */
@media (max-width: 1439px) {
  .tp-wrap { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
}

/* ★ 上一条会盖掉 ≤768 那条「单列」—— 所以这里必须再收一次。
   900 而不是 768：820 竖屏（iPad Air）用 2fr/1fr 时主栏只有 508，
   章条一格 150 出头、小节行还要拆两行，不如直接单列。 */
@media (max-width: 900px) {
  .tp-wrap { grid-template-columns: 1fr; }
}


/* ───────── ⑤-2 章节小节行：grid 固定轨道 ─────────
   ★ 桌面（>1439）也一起改 —— 那里本来就有两处缺失元素导致的对齐偏差。
   列宽沿用原来的设计意图（名称 210 / 百分比 46 / 已完成 118），
   只是把「谁在哪个格子」写死，不再靠 DOM 顺序。 */
.sl-row {
  display: grid;
  grid-template-columns:
    26px                    /* 1 序号 */
    minmax(0, 210px)        /* 2 名称（过长省略） */
    minmax(0, 66px)         /* 3 题数标签 */
    minmax(70px, 1fr)       /* 4 进度条（吃掉剩余） */
    46px                    /* 5 百分比 */
    118px                   /* 6 已完成 */
    auto;                   /* 7 行动按钮 */
  align-items: center;
  gap: 14px;
}
.sl-row > .sl-no   { grid-column: 1; }
.sl-row > .sl-nm   { grid-column: 2; width: auto; min-width: 0; }   /* 宽度交给轨道 */
.sl-row > .hc-tag  { grid-column: 3; justify-self: start; white-space: nowrap; }
.sl-row > .sl-bar  { grid-column: 4; width: 100%; min-width: 0; }   /* 原来的 flex:1 / min-width:80 在这里无意义 */
.sl-row > .sl-pct  { grid-column: 5; }
.sl-row > .sl-done { grid-column: 6; }
.sl-row > .sl-go   { grid-column: 7; justify-self: end; }

/* 小节行：序号位 38、名称 158（缩进一级后仍与上级同一条竖线，见上方原注释） */
.sl-row.sl-sub {
  grid-template-columns: 38px minmax(0, 158px) minmax(0, 62px) minmax(70px, 1fr) 46px 118px auto;
}


/* ───────── ⑤-3 章条：名称允许折两行（全尺寸） ─────────
   ★ 原规则是 nowrap + 省略号，4 列 208px 时「第一章 国内时政热点」被截成
     「第一章 国内时政…」——用户的原话是「被强制截断成省略号，空间利用极不合理」。
   ★ 折行 + 两行截断（-webkit-line-clamp）比省略号可读，
     而且不会让格子忽宽忽窄（行高由 grid 统一拉伸）。
   ★ .ch-no 贴着第一行顶部对齐，避免两行时序号飘到中间。 */
.tl-ch  { align-items: flex-start; }
.tl-ch .ch-no { margin-top: 1px; }
.tl-ch .ch-nm {
  white-space: normal;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  line-height: 1.45;
}


/* ───────── ⑤-3 平板 / 窄屏：轨道收窄 ─────────
   ★ 2026-10-25 修正：原来这里写 42px / 104px，比元素自身的 width 窄
     （.sl-pct 46px、.sl-done 118px，都是原规则里写死的 flex:none 宽度）。
     格子比元素窄时元素不会缩 —— 它只会溢出：
       实测 1180/1280/1366 三档，「已完成」的右边缘都比格子右移 14px，
       正好压进「开始做题」左边框，两者间距 −4px（视觉上贴在一起/被盖住）。
     改成与元素等宽（46 / 118），差额交给 minmax(56px,1fr) 的进度条吸收；
     1101 下进度条仍有 225px，离 56px 的下限很远。
   ★ 固定轨道的目的没变：右四列永远是同一条竖线。 */
@media (max-width: 1439px) {

  .sl-row { gap: 10px;
    grid-template-columns: 26px minmax(0, 30%) minmax(0, 58px) minmax(56px, 1fr) 46px 118px auto; }
  .sl-row.sl-sub {
    grid-template-columns: 38px minmax(0, 24%) minmax(0, 58px) minmax(56px, 1fr) 46px 118px auto; }

  /* 章条：不再写死列数，按「一格至少 180px」自动排
     ★ 180 = 序号 20 + 间距 7 + 内边距 20 + 9 个汉字 133 —— 少于它就会出省略号。
       实测 160px 时 8 个章名【全部】被截断，180 时 0 个。
     ★ 这一条会盖掉模板里 :style 给的 --chs-cols（内联只设变量，不设属性） */
  .tl-chs { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
  .tl-ch  { padding: 9px 10px; gap: 7px; }
  /* ★ 180px 仍装不下「第四章 社区治理与服务」（10 字 ≈ 130px + 序号 + 内边距）：
     与其继续加宽、把 3 列变 2 列，不如允许它折两行 ——
     用户的原话是「被强制截断成省略号，空间利用极不合理」，
     折行比省略号可读，也不会让格子忽宽忽窄。 */
  .tl-ch .ch-nm {
    white-space: normal;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    line-height: 1.45;
  }
}


/* ───────── ⑤-4 平板竖屏 / 手机：小节行拆两行 ─────────
   ★ 7 个格子在一行里最少要 683px，1100 以下不可能一行装下。
     拆成两行而不是继续压字号：
       第 1 行  序号 | 名称（弹性）            | 开始做题 →
       第 2 行       | 进度条 | 百分比 | 已完成
   ★ 「N 题」标签在这里隐掉：已完成那格写的是「已完成 12 / 46」，
     总数已经在里面了，留着只是重复占宽。 */
@media (max-width: 1100px) {

  .sl-row {
    grid-template-columns: 26px minmax(0, 1fr) 44px 110px auto;
    grid-template-rows: auto auto;
    row-gap: 8px; column-gap: 10px;
    padding: 12px 14px;
  }
  .sl-row > .sl-no   { grid-area: 1 / 1 / 3 / 2; }        /* 序号跨两行居中 */
  .sl-row > .sl-nm   { grid-area: 1 / 2 / 2 / 5; }
  .sl-row > .sl-go   { grid-area: 1 / 5 / 2 / 6; }
  .sl-row > .hc-tag  { display: none; }
  .sl-row > .sl-bar  { grid-area: 2 / 2 / 3 / 3; }
  .sl-row > .sl-pct  { grid-area: 2 / 3 / 3 / 4; width: auto; text-align: left; }
  .sl-row > .sl-done { grid-area: 2 / 4 / 3 / 6; width: auto; text-align: right; }

  .sl-row.sl-sub { grid-template-columns: 38px minmax(0, 1fr) 44px 110px auto; }

  /* ★ 章条列数不在这里再写一遍：180px 的下限在窄屏上自然就排出 3 列 / 2 列 / 1 列，
     多写一条只会互相打架（曾经写成 140px，结果 1024 挤成 4 列全截断）。 */
}


/* ───────── ⑤-5 手机：小节行去掉进度条，右列只剩「% + 已完成」 ─────────
   ★ 390 屏行内可用宽只有 306px，而 26+44+110+按钮 92+间距 30 = 302 —— 刚好卡死。
     把进度条拿掉（百分比还在），右列腾出 60px，也不会再横向溢出 253px。 */
@media (max-width: 560px) {

  .sl-row { grid-template-columns: 26px minmax(0, 1fr) auto auto; }
  .sl-row > .sl-nm   { grid-area: 1 / 2 / 2 / 3; }
  .sl-row > .sl-go   { grid-area: 1 / 3 / 2 / 5; }
  .sl-row > .sl-bar  { display: none; }
  .sl-row > .sl-pct  { grid-area: 2 / 2 / 3 / 3; }
  .sl-row > .sl-done { grid-area: 2 / 3 / 3 / 5; }

  .sl-row.sl-sub { grid-template-columns: 38px minmax(0, 1fr) auto auto; }
}


/* ───────── ⑤-6 手机：开练控制条换行（≤ 768） ─────────
   ★ 实测 390 宽时 .qs-ctl 有 607px：三个方式(错题/收藏/随机) + 分隔线 +
     四个题量(20/40/50/100) + 「立即开始」全在一行，而且子项全是 flex:none
     → 既不能缩也不能换行，整页横向溢出 253px（三轮测量里唯一还在溢出的东西）。
   ★ 换行后：方式一行三等分、题量一行四等分、按钮独占一行（触控更好点）。 */
@media (max-width: 768px) {

  .qz-start .qs-ctl { flex: 1 1 100%; margin-left: 0; flex-wrap: wrap; row-gap: 10px; }
  .qz-start .qs-sep { display: none; }
  .qz-start .qs-modes { flex: 1 1 100%; }
  .qz-start .qs-modes span { flex: 1; text-align: center; padding: 9px 6px; }
  .qz-start .qs-go { flex: 1 1 100%; justify-content: center; }

  /* ★ 浮条在手机档必须回到「横排贴底」：
     ④-3（≤1100）为了平板把它改成了竖排，而那条在文件里更靠后，
     会把 ②（≤768）的横排覆盖掉 —— 结果手机上又变回右侧竖条，
     正好压住章节列表右边（390 实测截图里能直接看到）。这里补回来。 */
  .dock { flex-direction: row; right: 8px; bottom: 10px; gap: 6px; }
}

/* ═══════════════════════════════════════════════════════════════════════
   ⑥ 答题页第二轮专项优化（2026-10-24 夜）
   ───────────────────────────────────────────────────────────────────────
   上一轮（②③④）解决的是「装不下」，这一轮解决的是「装下了但用起来别扭」。
   下面每条都对应探针 .tmp/diag-quiz.js 量出来的数字，不是看图猜的。

   A. 820×1180 横向溢出 36px、844×390 溢出 12px —— 元凶只有【同一个】：
      .qz-modes 宽 278，右边缘顶到 856。
      页头那一行要塞：bk 64 + 标题 311 + 进度条 + stats 90 + modes 278
      + 4 个 18px 间隙 ≈ 855，再加左右内距 40 —— 超过 820 就顶出去。
      更隐蔽的是：空间不够时浏览器先压 .qz-prog（它 min-width:0），
      实测被压到【0 宽】—— 进度条直接消失，比溢出更难发现。
      ★ 为什么 ④ 没管到：页头换行规则只写在 ≤768，
        而 820 / 844 / 1024 正好落在 769~1023 这段没人管的区间。

   B. 844×390 手机横屏：一道题都看不见。
      .quiz 被写死 height: calc(100vh - 138px) = 252，
      页头 76 + 底栏 70 之后主栏只剩 74px 可视，内容却有 500 ——
      要在栏内滚 426px 才能读完四个选项，而首屏【0 个选项】。
      根因不是"适配不够"，是布局假设失效：「两栏固定高度」只在大屏成立。
      390 高的视口里页头+底栏就吃掉 146px，再怎么挤也没有空间，
      所以这一档必须放弃固定高度，改成【内容自然高度 + 整页滚动 + 底栏吸底】。

   C. 选项高度存在两套值：≥1440 是 60px（padding 14），
      ≤1439 被 ④-2 抬到 70px（padding 19）。
      多出的 10px 并不是为触控 —— 整行都可点，60px 早已远超 44 的底线，
      它纯粹白吃竖向空间，1024×768 因此只能看到 3 个选项里的 3 个（第 4 个差 8px）。
      → 收回到 15px，与桌面同高。

   D. 顺带修一个生产环境才暴露的隐患（这台测试夹具里 v2chip 不出现）：
      .qz-top 的标题是 flex: none，云端会话标签 .v2chip 最宽 260px ——
      两者同时出现时，1024 下总宽 1144，照样溢出。
      → 标题改成可收缩（超长走省略号），页头整体允许折行。
   ═══════════════════════════════════════════════════════════════════════ */

/* ⑥-1 页头：从「顶出视口」改成「先截标题 → 再折行」
   ────────────────────────────────────────────────────────────────────
   ★ flex-wrap 全局打开是安全的：内容放得下时它一次都不会换行，
     1440 / 1920 的渲染结果逐像素不变（.tmp/regress-pc.js 每次都要复核）。
   ★ 标题 flex 从 none 改成 0 1 auto + min-width: 0：
     挤的时候第一个让位的是标题（省略号），而不是进度条被压成 0 宽。
     —— 进度条是"做到哪儿了"的唯一可视依据，它比完整标题更该留下。 */
.qz-top { flex-wrap: wrap; row-gap: 10px; }
.qz-top .tt { flex: 0 1 auto; min-width: 0; }

/* ⑥-2 769~1023：模式标签独占一行、三等分
   ────────────────────────────────────────────────────────────────────
   这一档折行后如果只是让 modes 落在第二行左侧（它固定 278 宽），
   右边会空出一大块，看着像没排完。整行三等分反而更像"标签栏"。
   ★ ≤768 那套 order 重排（页头的 4 行布局）在此之上仍然生效，
     这里只补「占满一行 + 均分」，不动 order，两者不打架。 */
@media (max-width: 1023px) {
  .qz-modes { flex: 1 1 100%; }
  .qz-modes span { flex: 1; min-width: 0; text-align: center; }
}

/* ⑥-3 选项高度统一：769~1439 回到 60（覆盖 ④-2 的 padding: 19px 20px）
   ────────────────────────────────────────────────────────────────────
   ★ 为什么下限卡在 769：≤768 手机那档用的是 18px/16px（拇指够得着，
     竖屏也不缺高度），不要去改它。
   ★ 效果：1024×768 主栏内容 500 → 460，4 个选项刚好露出全。
     60px 的行高对触控依然宽裕（底线 44px）。 */
@media (min-width: 769px) and (max-width: 1439px) {
  .opt { padding: 14px 20px; }
  /* ★ 进度条给一条底线：页面一挤，浏览器先压它（它 min-width:0），
     「云端已同步」标签一出现就能把它压到 30px。宁可让模式标签折到第二行，
     也不要让"做到第几题"的唯一可视依据消失。 */
  .qz-prog { min-width: 96px; }
}

/* ⑥-4 平板横屏（769~1100）：主栏再省 14px，把第 4 个选项拉进首屏
   ────────────────────────────────────────────────────────────────────
   ⑥-3 之后 1024×768 还差 8px（460 vs 452）。这里从卡片内距（28→24）
   与题头下间距（20→14）各拿一点 —— 都是纯留白，不影响任何可点元素。
   ★ 为什么加 min-width: 769：≤768 那档的 .q-card 内距是 22/18（为窄屏定的），
     不挡住它就会被这条覆盖掉。 */
@media (min-width: 769px) and (max-width: 1100px) {
  .qz-main { padding: 24px 28px; }
  .qz-qhead { margin-bottom: 14px; }
}

/* ⑥-5 手机横屏（高 ≤520）：放弃固定高度，整页滚动 + 底栏吸底
   ────────────────────────────────────────────────────────────────────
   ★ 为什么这一档要单独立一条、而不是并进 ④-4（≤1100 竖屏）：
     ④-4 的判据是 orientation: portrait，横屏进不去；
     而 iPad 横屏是 1024×768 / 1180×820 / 1366×1024，高度都 >520，
     也进不来 —— 这条只会命中真手机（844×390 / 932×430 / 667×375）。
   ★ 为什么是单栏而不是两栏：横屏宽 844 只比竖屏 390 宽，
     侧栏 280 会吃掉 1/3 宽，题干被迫折成 4~5 行，整题反而更高。
     单栏 764 宽时同一道题的总高度更矮 —— 竖屏那档（④-4）同此判断。
   ★ 底栏 sticky 而不是固定：整页在滚，翻题按钮必须一直在手边，
     否则读到选项末尾还得滚回去找"下一题"。 */
@media (orientation: landscape) and (max-height: 520px) {
  .quiz { height: auto; }
  .qz-split { grid-template-columns: 1fr; gap: 14px; }
  .qz-main-box { max-height: none; }
  .qz-main { max-height: none; overflow: visible; padding: 16px 18px; }
  .qz-sb { display: none; }
  .qz-side { max-height: none; overflow: visible; }

  /* 页头压成一行：标题最多 140px（超了走省略号），其余全部瘦身 */
  .qz-top { flex-wrap: nowrap; gap: 8px 10px; padding: 8px 12px; margin-bottom: 12px; }
  .qz-top .bk { flex: none; padding: 6px 6px; font-size: 14px; }
  .qz-top .tt { flex: 0 1 auto; min-width: 0; max-width: 220px; }
  .qz-top .v2chip { display: none; }        /* 设备有一整屏，不用这个标签解释 */
  .qz-prog { flex: 1 1 80px; gap: 8px; min-width: 88px; }
  .qz-prog .track { min-width: 40px; }
  .qz-stats { flex: none; gap: 6px; }
  .qz-stats .s { padding: 3px 7px; font-size: 12px; }
  .qz-modes { flex: none; padding: 3px; gap: 2px; }
  .qz-modes span { flex: none; padding: 7px 10px; font-size: 13px; }

  .qz-qhead { margin-bottom: 12px; }
  .opt { padding: 11px 14px; font-size: 16px; line-height: 1.6; gap: 12px; }
  .opt .k { width: 26px; height: 26px; font-size: 14px; }

  .qz-foot { position: sticky; bottom: 0; z-index: 30; margin-top: 12px;
             padding: 8px 12px calc(8px + env(safe-area-inset-bottom)); gap: 8px; }
}

/* ⑥-6 【已放弃，留档说明】≤768 想「返回 + 标题」合并成一行、省 40px 高度
   ────────────────────────────────────────────────────────────────────
   做法是 `.tt{flex:1 1 0}`（basis 0 才会收缩而不是折行）——
   实测 390 下标题被压到 **26px**（只剩一个省略号）：因为同一行里
   .qz-prog（order 4 / basis 120 / grow 1）跟它平分剩余空间，
   而生产的 .v2chip（「✓ 云端记录 · 已存 N 题」，最宽 260）还会再抢一次。
   ★ 结论：390 宽下页头有 6 个元素（返回/标题/云标签/进度/统计/模式），
     4 行是诚实的结果。用「题库名看不见」换 40px 高度 —— 不划算，故不采用。
     真嫌高就去砍元素（例如手机档隐藏云标签），而不是抢标题的宽度。 */

/* ═══════════════════════════════════════════════════════════════════════
   ⑦ 背题自评条：并进底栏（2026-10-25 重写，原「三段停靠」方案作废）
   ───────────────────────────────────────────────────────────────────────
   历史轨迹，留着以免再走一遍：
     ① 最初它是 sticky 吸底 → 必然遮挡（sticky 靠脱离文档流贴底，内容从下面穿过去
        是定义，改 bottom / 做矮都只是换一屏被盖）。实测 1024×768 压住 C 整条。
     ② 改成 `.qz-main:has(> .recite-judge)` 纵向三段（题头/题身滚/自评条停靠）→
        遮挡解决了，但代价是背题模式滚动容器换成 .qz-body，自绘滚动条 .qz-sb 失效。
     ③ 现在：整条挪进 .qz-foot，和「上一题 / 下一题」同一行。
        · 省掉独占的 76px —— 768 高的屏上，选项区从 225px 变回 300+
        · .qz-main 恢复为唯一滚动容器 → .qz-sb 在三种模式下都准确，②的代价消失
        · 不再需要 :has(> .recite-judge)，也就没有「只在背题模式生效」这种脆弱条件
   ★ 底栏本来就有 .sp{flex:1} 弹簧：状态行「正在加入… / ✓ 已加入」出现或消失时，
     挤压的是空白，不是别的按钮 —— 原来那个闪烁问题不会复发。
   ═══════════════════════════════════════════════════════════════════════ */
.recite-judge.in-foot {
  position: static; bottom: auto; z-index: auto;
  flex: 0 1 auto; min-width: 0;
  display: flex; align-items: center; gap: 10px;
  margin: 0; padding: 0;
  background: transparent; border: 0; border-radius: 0; box-shadow: none;
}
.recite-judge.in-foot .rj-main { display: flex; align-items: center; gap: 8px; min-width: 0; }
.recite-judge.in-foot .lb {
  flex: none; font-size: 14px; font-weight: var(--fw-medium); color: var(--text-3);
  white-space: nowrap;
}
.recite-judge.in-foot .lb::before { height: 14px; }
/* 按钮比原来的 40 矮一点：底栏的「上一题/下一题」也是 38~40，视觉上要齐 */
.recite-judge.in-foot .qbtn { height: 36px; padding: 0 16px; font-size: 14px; flex: none; }
.recite-judge.in-foot .rj-status {
  min-height: 0; margin: 0; padding: 0;
  flex: 0 1 auto; min-width: 0; overflow: hidden;
}
.recite-judge.in-foot .rj-tip { font-size: 12px; white-space: nowrap; }

/* 有自评条时，底栏中间那条「← → 翻题 · A~D 选答案」提示让路 —— 它只是提示，不是功能 */
.qz-foot:has(.recite-judge.in-foot) .hint { display: none; }

/* ≤1023：提问句隐掉，只留两个按钮 —— 一行里塞不下 8 个元素 */
@media (max-width: 1023px) {
  .recite-judge.in-foot .lb { display: none; }
  .qz-foot:has(.recite-judge.in-foot) .rj-status { max-width: 96px; }
}

/* ≤768：底栏本来就已经折成几行了，自评条插进去就变第三行（390 实测 175px，占屏 21%）。
   改成：提问句隐掉、两个按钮收窄，去和「收藏 / 反馈 / 字号」共用一行；
   放不下时浏览器会自己折 —— 不用写死 100% 强制占满。order:10 保证它在最后。 */
@media (max-width: 768px) {
  .qz-foot:has(.recite-judge.in-foot) { flex-wrap: wrap; row-gap: 8px; }
  .qz-foot:has(.recite-judge.in-foot) > .recite-judge.in-foot {
    order: 10; flex: 0 1 auto;
    margin-left: auto;
  }
  .qz-foot:has(.recite-judge.in-foot) > .recite-judge.in-foot .lb { display: none; }
  .qz-foot:has(.recite-judge.in-foot) > .recite-judge.in-foot .qbtn { padding: 0 13px; }
}

/* ≤560：既然是独占一行，就让两个按钮各占一半 —— 拇指更好按，也不浪费左边那片空白 */
@media (max-width: 560px) {
  .qz-foot:has(.recite-judge.in-foot) > .recite-judge.in-foot {
    flex: 1 1 100%; margin-left: 0;
  }
  .qz-foot:has(.recite-judge.in-foot) > .recite-judge.in-foot .rj-main { flex: 1; gap: 8px; }
  .qz-foot:has(.recite-judge.in-foot) > .recite-judge.in-foot .qbtn { flex: 1; padding: 0 8px; }
}

/* ═══════════════════════════════════════════════════════════════════════
   ⑧ 全局提示条不再压顶栏（用户反馈：提示框盖住了导航的「题库」）
   ───────────────────────────────────────────────────────────────────────
   实测：`.topbar` 是 position:sticky，高 **60**（≥820）/ **99**（390）/ **103**（768）
        `.g-toast` 是 position:fixed、`top: 22px` → 22~66 这一条正好压在导航文字上。

   ★ 为什么按断点分开写、而不是一个固定值：
     顶栏在 ≤768 会换行（我加的响应式层让它能折两行），高度从 60 涨到 99~103。
     一个 72px 在手机上仍然会压住第二行。
   ★ 为什么用 body:has(.topbar) 限定：登录页没有顶栏，
     提示条保持 top:22 才对（那里上方就是空的），不能一并下移。
   ═══════════════════════════════════════════════════════════════════════ */
body:has(.topbar) .g-toast { top: 72px; }
@media (max-width: 768px) {
  body:has(.topbar) .g-toast { top: 112px; }
}

/* ⑨ 登录卡片切 Tab 不再跳高度（用户反馈：切换 tab 时高度会跳一下）
   ───────────────────────────────────────────────────────────────────────
   实测（.tmp/sweep-login.js，16 档宽度）：
     验证码 ↔ 密码：**0px** —— 10-20 那次把两个输入框收进稳定的 .form-area 已经解决
     切到微信扫码　：仍然跳，且差值随宽度变化
       360~390 → +54 ｜ 430~768 → +74 ｜ ≥800 → +38

   ★ 差值为什么随宽度变：**二维码图片本身有三档尺寸**
       （base 144 / ≤768 的 180 / ≤420 的 160，见上方 .wx-qr 那三条规则），
       面板高度 = 二维码 + 24（白底内距）+ 提示区 42 → 228 / 264 / 244。
       面板高度不是定值，差值就跟着漂（+38 / +74 / +54）。
   → 做法：**按二维码的三档，把表单区垫到对应高度**（断点与 .wx-qr 完全对齐，
     以后改二维码尺寸时，这两处要一起改）：
        ≥769  → min-height 162（对应 144 的二维码）
        ≤768  → 198（对应 180）
        ≤420  → 178（对应 160）
     多出来的空间用 flex 居中平分到上下，读起来是"这一栏留白更松"，
     而不是"按钮前面突然空一块"。
   ★ 为什么不用一个固定值：二维码在 ≤768 被特意放大到 180（手机上更好扫），
     一个值必然在某个区间留下跳动 —— 那是把一个 bug 换成另一个。 */
.form-area {
  display: flex; flex-direction: column; justify-content: center;
  /* ★ 2026-10-27：162 → 234。微信 Tab 从「144 假图」换成「真二维码 iframe」后
     面板高了 72px（实测三 Tab 631/631/**703**），这里同步垫高，三 Tab 才重新同高。
     ★ 这个值只对 ≥769 有意义 —— 手机端「微信扫码」整条隐藏，⑪ 里已把 min-height 归 0。 */
  min-height: 234px;
}
/* ★ 2026-10-27：原来这里还有两条按宽度设的 198 / 178 —— 那是给旧「180/160 二维码」算的。
   现在二维码面板固定 242（不随宽度变），这两条已经对不上，删除。
   触屏设备不看这张卡片（⑪ 里把 min-height 归 0），非触屏统一 234。 */

/* ═══════════════════════════════════════════════════════════════════════
   ⑩ 高度维度 + 流体化（2026-10-25，用户问「为什么不能按设备比例自动适配」）
   ───────────────────────────────────────────────────────────────────────
   根因不是「宽度不够」，而是【适配只做了宽度这一维】。
   实测（.tmp/probe-fit.js）1024×768（iPad Mini，4:3 矮屏）：
     练习模式  4 个选项全露（宽度早就够）
     背题模式  选项区只剩 225px → 4 个选项只露 2 个

   为什么宽度断点看不见这件事：
     1024×768（iPad Mini，4:3）和 1024×1366（iPad Pro 竖着）落在同一档，
     但前者要压高度、后者要拉长 —— 宽度断点分辨不出来。

   ★ 三条措施，按「收益 ÷ 风险」排序：

   ① 背题自评条并进底栏（改的是模板，不在这一块）→ 直接省掉独占的 76px
   ② 纵向尺寸改成 clamp() 跟随视口高度【连续】变化，消掉「1439 上下两套值」
      那种台阶；不再是「到点突跳」，中间尺寸也不会崩
   ③ 背题模式在矮屏上把页头的「记住了 N / 不会 N」让出来 ——
      实测它比练习模式的「✓ 0 ✗ 0」宽 46px，正好把 .qz-modes 挤到第二行，
      而 .qz-prog 是 flex:1 会把剩余空间吃满（379px），让折行更早发生。
      这两个数字在右侧【答题卡】里本来就有（记住了 / 不会 / 未看），是重复信息。

   ★ 为什么用 .quiz:has(.recite-judge.in-foot) 判定「背题模式」：
     自评条只在 quizMode === 'recite' 且 shown(curQ) 时渲染，
     而 shown() 在背题模式【恒为 true】（见 app.js 3335 行），
     所以「自评条在」等价于「现在就是背题模式」，比另加一个 class 更省事，
     也不会在课堂练习 / 考试模式误伤。
   ═══════════════════════════════════════════════════════════════════════ */

/* ───────── ⑩-1 【已放弃，留档】把 .quiz 高度换成 100dvh ─────────
   原本想解决「手机地址栏收起/展开时 100vh 不跟着变」。
   实测（390×844）直接把滚动区从 512 压成 **44**：
     5383 行有 `@media (max-width:1100px) and (orientation: portrait) { .quiz { height: auto } }`
     —— 手机竖屏是【整页自然滚动】，没有内部滚动容器；
     我又给 .quiz 写回固定高度，就把那个开关盖掉了，.qz-main 被挤成一条。
   ★ 结论：dvh 只在「固定高度布局」里有意义，而手机上这个布局已经不存在了，
     所以这条不写 —— 少一个覆盖、少一个坑。 */

/* ───────── ⑩-2 纵向尺寸流体化（只作用于 769~1439 宽、高度 ≤900 这一带） ─────────
   ★★ 为什么卡这么死（两版都是实测抓出来的错）★★
     第 1 版全尺寸写 clamp，1440×900 把 .qz-main 从 28 压到 18.9、.qz-qhead 从 20 压到 13.5
       —— 已经调好的桌面版被"流体化"顺手改掉了。
     第 2 版收成一条带，但 1360×900 仍然差 2.5%：
       因为 1101~1439 的基线是 main **28** / qhead **20**（来自基础规则），
       而 ≤1100 的基线是 ⑥-4 给的 **24 / 14** —— 一条 clamp 不可能同时对上两个基线。
   → 所以按宽度拆成两条，**各自封顶到本档的基线值**：
       769~1100 ：封顶 24 / 14，斜率 2.75vh / 1.56vh
       1101~1439：封顶 28 / 20，斜率 3.2vh / 2.3vh
     ★ 斜率取「基线 ÷ 9」再往上留一点余量（2.67→2.75、3.11→3.2、2.22→2.3）：
       严格相等会算出 27.99 这种亚像素，900 高处封不到 28 —— 实测 1360×900 因此
       还残留 1163 像素差异（y203~227 一条）。加余量后归零。
     斜率都用「基线 ÷ 9」，于是到 900 高时刚好封顶回到基线
     → 900 及以上的像素一处不动，只有更矮的屏才开始收。
   ★ 用 padding-block 不用 padding：横向内距留给各档断点（⑥-3/⑥-4/⑥-5），少一个"两套值"来源。 */
@media (min-width: 769px) and (max-width: 1100px) and (max-height: 900px) {
  .qz-top, .qz-foot { padding-block: clamp(9px, 1.56vh, 14px); }
  .qz-main  { padding-block: clamp(14px, 2.75vh, 24px); }
  .qz-qhead { margin-bottom: clamp(9px, 1.56vh, 14px); }
  .opt      { padding-block: clamp(10px, 1.56vh, 14px); }
}
@media (min-width: 1101px) and (max-width: 1439px) and (max-height: 900px) {
  .qz-top, .qz-foot { padding-block: clamp(9px, 1.56vh, 14px); }
  .qz-main  { padding-block: clamp(16px, 3.2vh, 28px); }
  .qz-qhead { margin-bottom: clamp(11px, 2.3vh, 20px); }
  .opt      { padding-block: clamp(10px, 1.56vh, 14px); }
}

/* ───────── ⑩-3 矮屏（≤820）：背题模式让出重复统计 ─────────
   只在背题模式生效，练习/考试模式的「✓ 0 ✗ 0」「已答 3 / 12」照旧保留。
   实测这一条值 50px：它比练习模式那两个短标签宽 46px，正好把 .qz-modes 挤到第二行。
   ★ 加 min-height: 521 是为了不和 ⑥-5 的手机横屏档打架。 */
@media (max-height: 820px) and (min-height: 521px) {
  .quiz:has(.recite-judge.in-foot) .qz-stats { display: none; }
}

/* ───────── ⑩-4 极矮屏（≤680）：再硬收一档 ─────────
   1280×720、老笔记本 1024×600 这类，clamp 到 680 以下已经贴地板，补一档硬收。 */
@media (min-width: 769px) and (max-width: 1439px) and (max-height: 680px) {
  .qz-top { row-gap: 6px; }
  .qz-main { padding-block: 12px; }
  .qz-foot { padding-block: 7px; margin-top: 8px; }
  .opt { padding-block: 9px; }
  .qz-qhead { margin-bottom: 8px; }
}

/* ═══════════════════════════════════════════════════════════════════════
   ⑪ 微信扫码登录（2026-10-27 接真）
   ───────────────────────────────────────────────────────────────────────
   原来这里是 <img src="wx-qr-mock.png"> + 「模拟」角标，现在是 iframe 直嵌微信页面。

   ★★ 这一块改了三版，前两版都扫不出来，原因记在这里免得再走 ★★
   实测（用 jsQR 解码截图来判定「能不能扫」，比肉眼可靠）：
     微信 qrconnect 页面在 336×336 的 iframe 里，**二维码黑块区固定 244×244，位置 (46,66)**；
     页面不响应式，且与 devicePixelRatio 无关（DSF 1/2/3 量出来都是 244，已验）。
     码外围只有微信自己给的边距（左 46 / 上 66），**码图本身不带静区**。

     第 1 版：窗口 170 + 负边距裁到二维码 → 四周只剩 5px（< 1 个模块）→ **jsQR 解不出，手机扫不出**
     第 2 版：窗口 200 + 负边距 → 还是解不出（244 的码被 200 的窗切掉一角）
     第 3 版（当前）：**整页等比缩放**，不裁切 —— 336 × .72 = 242 的窗口，二维码 176px，
       微信自带的边距按比例保留（左 33 / 上 47），静区完整 → jsQR 解得出来 ✓

   ★ 为什么不再试着裁到最小：
     给足 4 个模块的静区，窗口至少要 244 + 2×26 = 296，和整页 336 已经差不多，
     裁切省不下高度，却多一个「微信改版就错位」的风险。

   ★ 代价与补偿：
     面板比老结构（144 二维码）高 42px（242 vs 200），`.form-area` 的 min-height 已同步 +42，
     三个 Tab 依然同高，切 Tab 不跳。
   ▶ 若哪天微信改版：把 iframe 临时调成 336×336 截图，用 `.tmp/qrcheck/decode.js` 解码，
     能解出 `open.weixin.qq.com/connect/confirm?uuid=` 就说明静区够。
   ═══════════════════════════════════════════════════════════════════════ */
.wx-qr-frame {
  padding: 0; overflow: hidden;
  width: 242px; height: 242px;
  background: #fff;
  border-radius: 12px;
}
.wx-frame {
  display: block; border: 0;
  width: 336px; height: 336px;
  transform: scale(.72);
  transform-origin: 0 0;
}
/* 取二维码中 / 失败时的占位：把后端原话完整显示出来，不许自己编原因 */
.wx-qr-state {
  width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
  padding: 14px; text-align: center;
  font-size: 12px; line-height: 1.65; color: var(--text-3);
  word-break: break-word;
}
.wx-retry {
  margin-top: 10px; padding: 7px 16px;
  font-size: 13px; font-family: inherit; cursor: pointer;
  border-radius: var(--r-sm);
  border: 1px solid var(--brand-200);
  background: var(--surface-1); color: var(--brand);
  transition: var(--dur-fast);
}
.wx-retry:hover { background: var(--brand-50); }

/* 手机/平板（触屏）隐藏「微信扫码」：同一台设备扫不了自己屏幕上的码。
   ★★ 2026-10-27 修正：原来写的是 `@media (max-width: 768px)`，把「窗口拉窄的电脑」
      也一起藏了 —— 用户电脑窗口落在 768 以下，直接看不到这个 Tab（实测 768 隐藏 / 769 显示）。
      宽度判断的是**窗口大小**，跟"能不能扫码"毫无关系。
      改成判断输入设备：只有主输入是触摸（pointer: coarse）才隐藏。
      桌面窄窗口照样显示，二维码 242px 在 390 宽里也放得下。 */
@media (pointer: coarse) {
  .tabs .tab-wx { display: none; }
  /* 连带：⑨ 的垫高是为了对齐二维码面板，触屏上没有这个 Tab 了，放回自然高度 */
  .form-area { min-height: 0; }
}
