/* =========================================================
   四六级智学平台 · 设计系统（学生向 / 温和耐看）
   配色：低饱和靛蓝单色系；暖砂中性底；青/琥珀克制点缀
   动效：仅 transform / opacity，时长 0.2s，克制
   ------------------------------------------------------------
   2026-10-11 柔和化调整（用户反馈"页面太晃眼睛"）：
   原 --grad 用 #6366f1→#8b5cf6 的高饱和靛紫对撞，铺在吸顶导航上
   长时间盯屏易疲劳、且与白卡片撞出强烈色差。改为同色系深浅过渡，
   饱和度整体下调，底色改暖砂调，长时间阅读更舒适。
   ========================================================= */
:root {
  /* 主色阶（较原版降低饱和度，色相收窄到靛蓝 220°~250°） */
  --primary-700: #3d4a8a;
  --primary-600: #4a5aa8;   /* 主色（按钮/强调，白字对比度 ≥ 6:1）*/
  --primary-500: #6473bd;
  --primary-400: #8d99cf;
  --primary-100: #e4e8f6;
  --primary-light: #eff1f9; /* 浅底 */
  /* 渐变：同色系深→浅，色相跨度从 53° 收窄到约 12°，不再撞色 */
  --grad: linear-gradient(135deg, #4a5aa8 0%, #6473bd 100%);
  --grad-soft: linear-gradient(135deg, #eff1f9 0%, #f5f3fa 100%);
  /* 点缀色（同步降饱和） */
  --secondary: #4a9aa8;     /* 青 */
  --accent: #c98a3c;        /* 琥珀（提示/高频）*/
  --accent-soft: #fbf5ec;
  --pink: #c473a0;
  /* 中性：暖砂底替代冷蓝，长时间阅读更柔和 */
  --bg: #f6f5f2;
  --bg-grad:
    radial-gradient(1100px 560px at 100% -12%, #e8ebf6 0%, rgba(232,235,246,0) 60%),
    radial-gradient(820px 460px at -8% 0%, #efeaf6 0%, rgba(239,234,246,0) 55%),
    #f6f5f2;
  --card: #ffffff;
  --text: #2b3038;
  --text-soft: #5c6472;
  --text-mute: #98a0ae;
  --border: #e9e7e2;
  /* 语义色：--x 用于图标/边框/填充，--x-ink 用于文字（深色版，保证 4.5:1 对比度） */
  --ok: #2f9e78;
  --ok-bg: #eaf6f1;
  --ok-ink: #0f7355;      /* 对 --ok-bg ≥ 5:1 */
  --err: #d95757;
  --err-bg: #fbeded;
  --err-ink: #a93232;     /* 对 --err-bg ≥ 6:1 */
  --warn: #c98a3c;
  --warn-bg: #fbf5ec;
  --warn-ink: #8a5a18;    /* 对 --warn-bg ≥ 6:1 */
  --vip-yellow: #b8860b; /* 导航赞助标识黄：白底 ≥3:1；文字含「赞助」二字，颜色仅强调 */
  /* 阴影 / 圆角：阴影去掉靛蓝染色，改中性灰，减弱"发光感" */
  --shadow-sm: 0 1px 3px rgba(43,48,56,.05), 0 1px 2px rgba(43,48,56,.035);
  --shadow: 0 6px 20px rgba(43,48,56,.07), 0 2px 6px rgba(43,48,56,.04);
  --shadow-lg: 0 18px 44px rgba(79,70,229,.16);
  --radius: 18px;
  --radius-sm: 12px;
  --radius-pill: 999px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0; padding: 0;
  background: var(--bg-grad);
  background-attachment: fixed;
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
    "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
  font-size: 15px; line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}
a { color: inherit; }

/* 焦点可见（无障碍） */
:focus-visible { outline: 3px solid var(--primary-400); outline-offset: 2px; border-radius: 6px; }
.hidden { display: none !important; }

/* ---------- 顶部导航（毛玻璃） ---------- */
.topnav {
  position: sticky; top: 0; z-index: 50;
  background: rgba(255,255,255,.78);
  backdrop-filter: saturate(180%) blur(14px);
  -webkit-backdrop-filter: saturate(180%) blur(14px);
  border-bottom: 1px solid var(--border);
}
.nav-inner {
  max-width: 1120px; margin: 0 auto; padding: 0 18px;
  height: 62px; display: flex; align-items: center; gap: 14px;
}
.brand { display: flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; }
.brand-mark {
  background: var(--grad); color: #fff; font-weight: 800; font-size: 14px;
  padding: 6px 10px; border-radius: 10px; letter-spacing: .5px;
  box-shadow: 0 4px 12px rgba(99,102,241,.35);
}
.brand-text {
  font-weight: 800; font-size: 18px;
  background: var(--grad); -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}
/* 导航项区。
   ⚠️ min-width: 0 是关键，缺了它移动端菜单就会撑破屏幕。
   flex 子项的默认 min-width 是 auto（即 min-content），
   意味着**它不肯缩到比内容还窄** —— 于是 overflow-x: auto 永远不会触发，
   菜单会直接把 .nav-inner 顶宽、连带整页出现横向滚动条。
   设 min-width: 0 允许它收缩，横向滚动才真正生效。 */
.nav-links {
  display: flex; gap: 4px;
  flex: 1 1 0; min-width: 0;
  flex-wrap: nowrap;
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none;
  /* 滚动时留出一点内边距，避免最后一项贴边看着像被裁掉 */
  padding: 2px 0;
  -webkit-overflow-scrolling: touch;
  /* ⚠️ 这里原来有 scroll-behavior: smooth，**必须去掉**。
     实测（隔离复现）：带 smooth 时，给 scrollLeft 赋值会被当成一次
     平滑动画来处理，动画在无头环境/某些情况下不推进，
     结果 `el.scrollLeft = 372` 之后读回来**恒为 0**，
     而 `scrollBy({behavior:'instant'})` 却立刻生效（443）。

     后果是：导航项多到需要滚动时，**代码无法把用户名滚进可视区** ——
     用户横向滑一下能看到，但页面在切页/登录后不会自动定位，
     于是"最后一个选项点不到"。

     平滑滚动对导航没有价值（用户是主动横向滑的，不需要动画），
     去掉它换回可靠的即时滚动。 */
}
.nav-links::-webkit-scrollbar { display: none; }
.nav-links a {
  text-decoration: none; color: var(--text-soft);
  padding: 8px 14px; border-radius: 10px; font-size: 14px; font-weight: 600;
  white-space: nowrap; transition: background .15s, color .15s;
}
.nav-links a:hover { background: var(--primary-light); color: var(--primary-600); }
.nav-links a.active { background: var(--grad); color: #fff; box-shadow: 0 4px 12px rgba(99,102,241,.3); }
.nav-user { display: flex; align-items: center; gap: 10px; flex: none; }

/* ---------- 布局 ---------- */
/* .app 的底部留白先后踩过两次坑，这里把两次根因并排记下来，避免第三次重犯：

   ① 封面留白叠加（已修）
      .app 的 padding-bottom 是给普通页面的底部留白，但封面 .cover 是
      **整屏 hero**、自带 padding-bottom，它又渲染在 .app 里面 ——
      两段底部留白叠加，实测页尾灰条 115.6px，正好等于 cover 的 56 + app 的 60。
      修法：.app:has(> .cover) 把内边距整体撤掉。用 :has() 而不是给 #app
      加状态类，是为了避免"加了类忘了删"的残留 —— 判据直接来自真实 DOM。

   ② 60~70px 本身就不必要（本轮）
      剩下的那 60/70px 是按"底部有悬浮条、得给它让位"设计的，
      但全站**没有任何 position:fixed 的底部元素**（fixed 只有 toast 与遮罩，
      sticky 只有顶部导航）。没有东西需要让位，这段留白就成了
      "内容不满一屏时页尾一大片空白"的直接来源，也是本轮要修的那个现象。
      排查顺序值得记：先确认 bottom 方向有没有遮挡物，再决定留白该留多少，
      而不是照着"看起来差不多"拍一个值。 */
.app:has(> .cover) { padding: 0; }
.app { max-width: 1120px; margin: 0 auto; padding: 26px 18px 18px; }
.page-title { font-size: 24px; font-weight: 800; margin: 2px 0 6px; letter-spacing: -.3px; }
.page-sub { color: var(--text-soft); margin: 0 0 20px; font-size: 14px; }

.grid { display: grid; gap: 18px; }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 900px) { .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } }

/* 个人资料页「账号四卡」容器（赞助支持 / 邀请加成 / 学习计划 / 练习统计）。
   它原来是个普通块级 div：
     · 赞助↔邀请 有 18px 行距 —— 来自里面 .grid-2 自己的 gap；
     · 但紧随其后的「学习计划」「练习统计」是块级兄弟，**外边距为 0**，
       三张卡上下贴死，看起来像同一张卡被切成了三块（用户 2026-10-15 报）。
   改成单列 grid + 18px gap，行距与页面其它 .grid 完全同源；
   上方的 16px 也并进 18px，使「账号信息 → 赞助支持」与卡之间保持一致。 */
.profile-accounts { display: grid; gap: 18px; margin-top: 18px; }

/* ---------- 卡片 ---------- */
.card {
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 20px; box-shadow: var(--shadow);
  transition: transform .2s, box-shadow .2s;
}
.card.hover-lift:hover { transform: translateY(-3px); box-shadow: var(--shadow-lg); }
.card h3 { margin: 0 0 12px; font-size: 16px; display: flex; align-items: center; gap: 8px; font-weight: 700; }
.card .card-tag {
  font-size: 12px; font-weight: 700; color: var(--primary-600);
  background: var(--primary-light); padding: 3px 10px; border-radius: var(--radius-pill);
}
/* ---------- 卡片标题行：标题左、动作按钮右 ---------- */
/* 用户要求把「升级VIP / 复制邀请码 / 创建计划」从卡片底部提到标题行右上角，
   与「VIP 状态 / 邀请进度 / 学习计划」同行。原来在底部的 btn-row 已删除，
   同一功能只保留一个入口，避免用户疑心是两个不同的按钮。 */
.card-head-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin-bottom: 12px;
}
/* h3 自带 margin:0 0 12px，在 flex 行里会把整行撑高，必须清零 */
.card-head-row .card-head-title { margin: 0; min-width: 0; }
/* 按钮不参与伸缩：长标题（"VIP 状态 生效中"）挤压时先缩标题，
   绝不允许把按钮压扁或挤出视口——它是这一行唯一的功能入口。 */
.card-head-row .btn { flex: none; white-space: nowrap; }
/* 320px 窄屏：按钮再小一号，标题字号略降，保证同行不折行 */
@media (max-width: 360px) {
  .card-head-row { gap: 6px; }
  .card-head-row .card-head-title { font-size: 15px; }
  .card-head-row .btn { padding: 6px 10px; font-size: 12px; }
}
/* 卡片内的灰色说明文字：原先是内联 style 到处复制，收成一个类统一管 */
.card-note { color: var(--text-soft); font-size: 13px; margin-top: 10px; }
.stat-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px dashed var(--border); }
.stat-row:last-child { border-bottom: none; }
.stat-row .k { color: var(--text-soft); }
.stat-row .v { font-weight: 700; font-variant-numeric: tabular-nums; }
/* 目标日期：标签（目标 / 日期，两行）与 年/月/日 三个下拉**同一行**
   （用户 2026-10-14 手绘图：整体和下行的三个选项框放在同一行）。
   标签 flex:none 只占两字宽，三个下拉均分剩余宽度；
   下拉自身收紧（13px 字号、6px 横向内边距）——基础 select 是
   14px + 13px 内边距，三个排开在 320px 下会把「2026 年」裁掉。
   align-items: center 让两行文字与单行控件视觉重心对齐。 */
.p-date-row { display: flex; align-items: center; gap: 8px; }
.p-date-label {
  flex: none; font-size: 12px; line-height: 1.15; color: var(--text-soft);
  text-align: center;
}
.p-date-row select {
  flex: 1 1 0; min-width: 0; padding: 8px 6px; font-size: 13px;
}
@media (max-width: 359px) {
  .p-date-row { gap: 6px; }
  .p-date-row select { padding: 7px 5px; font-size: 12px; }
}
/* 学习计划表单：两列紧凑网格（用户 2026-10-14：创建表单占空间）。
   原来级别/日期/时长三个字段各占一行、各自带 label，移动端约 170px；
   现在 级别+时长 并排一行、日期三个下拉并排一行，label 降为 12px 小字，
   总高约 110px。320px 也不折行——两列各约 140px，
   「英语四级」四个字 + select 箭头放得下。 */
.p-form { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 12px; }
.p-field { min-width: 0; }
.p-field label {
  display: block; font-size: 12px; color: var(--text-soft); margin-bottom: 4px;
}
.p-field-full { grid-column: 1 / -1; }
/* 计划输入框（plans.note）：与其它字段同宽，但高度给到 2 行——
   备考计划通常是一句话的清单，1 行会逼着用户横向滚动看自己写的东西。 */
#pNote { resize: vertical; min-height: 0; height: 58px; line-height: 1.5; }
/* 表单底部提示：讲清"日期只能选今天及以后""按天任务是系统自动排的"。
   做成虚线分隔 + 小字，读起来像字段说明而不是警告条——它说的是
   正常行为，不是出错。 */
.p-form-hint {
  grid-column: 1 / -1;
  font-size: 12px; line-height: 1.6; color: var(--text-soft);
  margin: 10px 0 0; padding-top: 10px;
  border-top: 1px dashed var(--border);
}
/* 计划卡片里展示用户写的计划原文。
   预留：色块本身没有标签时，视觉上像页面副标题，用户会以为"我写的东西
   跑到表头去了"（2026-10-15 反馈）。所以外面套 .plan-note-block 加一个
   「我的计划」小标，把它锚成一条字段。
   保留换行：textarea 里的分行若被折成一行，"每天背词 / 周末真题"
   这种清单就糊成一坨。 */
.plan-note-block { margin-bottom: 12px; }
.plan-note-head {
  font-size: 11.5px; font-weight: 800; letter-spacing: .04em;
  color: var(--primary-700); margin-bottom: 4px;
}
.plan-note {
  background: var(--primary-light); border-radius: var(--radius-sm);
  border-left: 3px solid var(--primary-500);
  padding: 9px 11px; font-size: 13px; line-height: 1.6; color: var(--text);
  margin-bottom: 10px; white-space: pre-wrap; word-break: break-word;
}
.plan-note-block .plan-note { margin-bottom: 0; }
/* 「按天任务」区头：标题 + 「系统自动生成」标注分列两端。
   标注不是装饰 —— 它是这块内容里最容易被误解的地方：日程是后端按
   级别/时长/目标日期排出来的模板，用户一个字都没填过，不标注就会被
   当成"系统自己乱加任务"。 */
.plan-task-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 8px; margin: 16px 0 4px; padding-top: 12px;
  border-top: 1px solid var(--border);
}
.plan-task-head-title { font-size: 13.5px; font-weight: 800; color: var(--text); }
/* 用 --text-soft 而非 --text-mute：#98a0ae 在白底上只有 2.7:1，而
   「系统自动生成 · 今天起 2 天」是**必须读到的信息**（它解释这些任务
   从哪来），正文级文字需 ≥4.5:1（WCAG 2.2 AA）。--text-soft 是 5.9:1。
   层次感靠字号 11.5px 表达，不靠牺牲对比度。 */
.plan-task-head-meta { font-size: 11.5px; color: var(--text-soft); }
.plan-task-hint {
  font-size: 12px; line-height: 1.6; color: var(--text-soft);
  margin: 0 0 6px;
}
/* 按天任务：每条上下两行，别挤在一行。
   原来复用 .stat-row（flex + space-between 的"标签—数值"一行式），
   长任务文字在窄屏把左右两栏挤到互相换行，分类标签被顶歪、与文字错位，
   看起来像数据串行（用户 2026-10-14 截图圈出）。
   现在：上行 = 日期 + 分类徽标（flex、不换行），下行 = 任务文字整宽，
   长句自然折行也不会再吃掉左侧的日期/分类。 */
.plan-task {
  padding: 9px 0;
  border-bottom: 1px dashed var(--border);
}
.plan-task:last-child { border-bottom: none; }
.plan-task-top {
  display: flex; align-items: center; gap: 8px;
  flex-wrap: wrap;            /* 极窄屏：日期与徽标顶不下时整组换行，仍对齐 */
  margin-bottom: 3px;
}
.plan-task-date {
  font-size: 12.5px; color: var(--text-soft);
  font-variant-numeric: tabular-nums;
}
.plan-task-tag {
  flex: none;
  font-size: 11px; font-weight: 700; color: var(--primary-700);
  background: var(--primary-light); border-radius: 6px;
  padding: 1px 7px; line-height: 1.5;
}
.plan-task-text {
  font-size: 13.5px; line-height: 1.55; color: var(--text);
  word-break: break-word;     /* 超长无空格串也不撑破卡片 */
}
/* 统计分组（中文标签） */
.stat-group { margin-bottom: 14px; }
.stat-group:last-child { margin-bottom: 0; }
.stat-group-title {
  font-size: 12.5px; font-weight: 800; color: var(--primary-700);
  background: var(--primary-light); border-radius: 8px;
  padding: 4px 10px; margin: 6px 0 2px;
}
.stat-sub-table { margin: 8px 0 2px; font-size: 13px; }
.stat-sub-table th, .stat-sub-table td { padding: 6px 10px; }
.stat-sub-empty { font-size: 13px; color: var(--text-mute); padding: 6px 2px; }

/* ---------- 表单 ---------- */
label { display: block; font-size: 13px; color: var(--text-soft); margin: 12px 0 6px; font-weight: 600; }
input, select, textarea {
  width: 100%; padding: 11px 13px; border: 1.5px solid var(--border);
  border-radius: var(--radius-sm); font-size: 14px; font-family: inherit;
  background: #fff; color: var(--text); outline: none;
  transition: border .15s, box-shadow .15s;
}
input:focus, select:focus, textarea:focus {
  border-color: var(--primary-500);
  box-shadow: 0 0 0 4px rgba(99,102,241,.14);
}
textarea { resize: vertical; min-height: 96px; }

/* 写作 / 翻译的作答框：默认 textarea 只有 96px（min-height），
   约 4 行，写一篇 120 词的作文要来回滚动，而且滚动时看不到自己前面写了什么 ——
   这正是"不好输入、不好浏览答案"的来源。
   这里给到 14 行左右的可视高度：一屏内能看完自己写的整段。
   选择器带 textarea 前缀是为了压过上面那条 `textarea { min-height: 96px }`
   （后者权重 0,0,1；这里 0,1,1），否则 min-height 会被它盖回去。
   仍保留 resize: vertical，长文想再拉高可以拖。 */
textarea.ans-long {
  min-height: 340px;
  line-height: 1.8;      /* 中文长段落行距太挤会串行，行高给足 */
  font-size: 14.5px;
  padding: 12px 14px;
}
.ans-long-tip {
  margin: 0 0 8px; font-size: 12.5px; color: var(--text-mute);
}
/* 小屏再收一档：340px 在 640px 高的屏上会吃掉过半视野，
   缩到 260px 仍能容下十余行，同时让题干和提交按钮留在同一屏内。 */
@media (max-width: 600px) {
  textarea.ans-long { min-height: 260px; font-size: 14px; }
}

/* ---------- 按钮（微交互） ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  background: var(--grad); color: #fff; border: none;
  padding: 11px 20px; border-radius: var(--radius-sm); font-size: 14px; font-weight: 700;
  cursor: pointer; transition: transform .2s, box-shadow .2s, opacity .2s;
  box-shadow: 0 6px 16px rgba(99,102,241,.3); text-decoration: none;
}
.btn:hover { transform: translateY(-2px) scale(1.02); box-shadow: 0 10px 22px rgba(99,102,241,.38); }
.btn:active { transform: translateY(0) scale(.97); }
.btn:disabled { opacity: .55; cursor: not-allowed; transform: none; box-shadow: none; }
.btn-sm { padding: 7px 14px; font-size: 13px; }
.btn-ghost { background: #fff; color: var(--text-soft); border: 1.5px solid var(--border); box-shadow: none; }
.btn-ghost:hover { background: var(--primary-light); color: var(--primary-600); transform: translateY(-1px); }
.btn-outline { background: #fff; color: var(--primary-600); border: 1.5px solid var(--primary-500); box-shadow: none; }
.btn-outline:hover { background: var(--primary-light); }
.btn-block { width: 100%; }
.btn-row { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 14px; }
/* 居中变体：个人资料的「保存资料 / 修改密码」放在表单底部，
   靠左会让人以为是上一行输入框的一部分，居中更像一个明确的收尾动作组。 */
.btn-row-center { justify-content: center; }
.btn-row-center .btn { min-width: 128px; }

/* ---------- 认证页（渐变英雄区） ---------- */
/* 认证页顶部留白：原来 7vh，在 844px 高的手机上约 59px，
   加上导航条与安全区，首屏要滑一下才看到登录框。
   减到 3.5vh（约 30px）—— 仍然与导航条保持呼吸，但不再需要"再滑一下"。
   不能减到 0：那会让表单紧贴顶部导航，看起来像是被挤下去的。 */
.auth-wrap { max-width: 430px; margin: 3.5vh auto 0; padding: 0 16px; }
.auth-hero {
  text-align: center; margin-bottom: 18px;
}
.auth-hero .ah-logo {
  display: inline-flex; align-items: center; gap: 10px; margin-bottom: 12px;
}
.auth-hero .ah-mark {
  background: var(--grad); color: #fff; font-weight: 800; font-size: 16px;
  padding: 8px 12px; border-radius: 12px; box-shadow: 0 6px 16px rgba(99,102,241,.4);
}
.auth-hero .ah-title {
  font-size: 26px; font-weight: 800;
  background: var(--grad); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}
.auth-hero .ah-sub { color: var(--text-soft); font-size: 14px; margin-top: 6px; }
.auth-card { padding: 26px; border-radius: var(--radius); }

/* 成功提示条。核对身份通过、密码重置成功这类"步骤已推进"的提示，
   不能只用 toast —— toast 会自动消失，但用户此刻还需要它来确认
   "我已经进入下一步了"。这里给一个常驻的行内提示。 */
.ok-tip {
  margin: 0 0 14px;
  padding: 10px 12px;
  border-radius: 8px;
  background: var(--ok-bg, #ecfdf5);
  border: 1px solid #a7f3d0;
  color: #065f46;
  font-size: 13.5px;
  line-height: 1.6;
}

/* 「忘记密码？」这类文字按钮。
   用 button 而不是 a：它触发的不是页面跳转，而是 SPA 内部路由切换。
   但它长得必须像链接 —— 否则用户不会把它当作可点的出口，
   而这恰恰是原来"找不到入口"的一环。 */
button.link {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: var(--primary-600);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
button.link:hover { color: var(--primary-700); }
button.link:focus-visible { outline: 2px solid var(--primary-600); outline-offset: 2px; border-radius: 2px; }
.auth-tabs { display: flex; gap: 8px; margin-bottom: 6px; background: var(--primary-light); padding: 5px; border-radius: 12px; }
.auth-tabs button {
  flex: 1; padding: 10px; border: none; background: transparent;
  color: var(--primary-600); border-radius: 9px; font-weight: 700; cursor: pointer; font-size: 14px;
  transition: background .15s, color .15s;
}
.auth-tabs button.active { background: #fff; color: var(--primary-700); box-shadow: var(--shadow-sm); }

/* ---------- 题目 ---------- */
.q-passage {
  background: var(--primary-light); border-left: 4px solid var(--primary-500);
  padding: 13px 15px; border-radius: 10px; margin-bottom: 14px; font-size: 14px; color: #374151;
}

/* listening 的 passage 自带 \n 分隔对话轮次，.q-passage 加 raw-lines
   启用 pre-wrap 原样保留换行（对话轮次不连成一片）；
   reading 的材料无换行，加上无副作用。
   （2026-10-15 起材料只有一个总喇叭，不再逐句渲染 .sp-line，
   换行的显示完全靠这里兜住。） */
.q-passage.raw-lines { white-space: pre-wrap; }

.q-content { font-size: 16px; font-weight: 700; margin-bottom: 14px; }
.q-type-badge {
  display: inline-block; font-size: 12px; color: #fff; background: var(--text-mute);
  padding: 2px 10px; border-radius: var(--radius-pill); margin-left: 8px; font-weight: 600;
}
.options { display: flex; flex-direction: column; gap: 10px; margin: 8px 0; }
.option {
  display: flex; align-items: flex-start; gap: 10px; padding: 12px 15px;
  border: 1.5px solid var(--border); border-radius: 12px; cursor: pointer; transition: transform .15s, border .15s, background .15s;
}
.option:hover { border-color: var(--primary-400); background: var(--primary-light); transform: translateX(2px); }
.option.selected { border-color: var(--primary-500); background: var(--primary-light); }
.option .opt-key {
  flex: none; width: 26px; height: 26px; border-radius: 50%; background: #fff;
  border: 1.5px solid var(--border); display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 13px; color: var(--primary-600);
}
.option.correct { border-color: var(--ok); background: var(--ok-bg); }
.option.correct .opt-key { background: var(--ok); color: #fff; border-color: var(--ok); }
.option.wrong { border-color: var(--err); background: var(--err-bg); }
.option.wrong .opt-key { background: var(--err); color: #fff; border-color: var(--err); }

/* ---------- 反馈 ---------- */
.feedback { margin-top: 16px; padding: 14px 16px; border-radius: 12px; font-size: 14px; }
.feedback.ok { background: var(--ok-bg); border: 1px solid #bbf7d0; }
.feedback.err { background: var(--err-bg); border: 1px solid #fecaca; }
/* 部分分（翻译等连续评分题型）：琥珀色表示「答了但没答好」，比二元的绿/红更准确 */
.feedback.warn { background: var(--warn-bg); border: 1px solid #fde68a; }
.feedback .fb-title { font-weight: 700; margin-bottom: 6px; }
.feedback.ok .fb-title { color: var(--ok-ink); }
.feedback.err .fb-title { color: var(--err-ink); }
.feedback.warn .fb-title { color: var(--warn-ink); }
.feedback .ans { font-weight: 700; }
.feedback .analysis { color: var(--text-soft); margin-top: 6px; }

/* ---------- 进度 ---------- */
.progress-bar { height: 9px; background: var(--border); border-radius: var(--radius-pill); overflow: hidden; margin: 10px 0; }
.progress-fill { height: 100%; background: var(--grad); border-radius: var(--radius-pill); transition: width .4s; }
.progress-text { font-size: 13px; color: var(--text-soft); }

/* 进度环（仪表盘统计） */
.ring { --p: 0; --size: 92px; width: var(--size); height: var(--size); border-radius: 50%;
  background: conic-gradient(var(--primary-500) calc(var(--p) * 1%), #e8ebf4 0);
  display: flex; align-items: center; justify-content: center; position: relative; }
.ring::before { content: ""; position: absolute; inset: 9px; background: #fff; border-radius: 50%; }
.ring > span { position: relative; font-weight: 800; font-size: 18px; color: var(--primary-700); }

/* ---------- 计时（自校准 + 三级状态 + 进度条） ---------- */
.exam-timer {
  position: sticky; top: 62px; z-index: 40; background: rgba(255,255,255,.92);
  backdrop-filter: blur(8px); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 12px 16px; margin-bottom: 16px;
  box-shadow: var(--shadow); display: flex; flex-direction: column; gap: 8px;
}
.exam-timer .timer-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.exam-timer .timer-state { font-size: 13px; font-weight: 700; color: var(--text-soft); line-height: 1.4; }
/* 数字用 tabular-nums：秒数跳动时宽度不变，不会左右抖 */
.exam-timer .time { font-size: 22px; font-weight: 800; font-variant-numeric: tabular-nums; color: var(--primary-700); line-height: 1.2; }
.exam-timer.warn { border-color: #e2c79b; background: linear-gradient(180deg, var(--warn-bg), rgba(255,255,255,.94)); }
.exam-timer.warn .time, .exam-timer.warn .timer-state { color: var(--warn-ink); }
.exam-timer.danger { border-color: #fca5a5; background: linear-gradient(180deg, var(--err-bg), rgba(255,255,255,.94)); }
.exam-timer.danger .time, .exam-timer.danger .timer-state { color: var(--err-ink); }
/* 呼吸动画只动 opacity / transform，不触发重排 */
.exam-timer.danger .time { animation: timerPulse 1.4s ease-in-out infinite; }
@keyframes timerPulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: .58; transform: scale(1.05); }
}
@media (prefers-reduced-motion: reduce) {
  .exam-timer.danger .time { animation: none; }
}
.exam-progress { height: 7px; background: var(--border); border-radius: var(--radius-pill); overflow: hidden; }
/* 只动画 transform（合成层），宽度恒定 100% 靠 scaleX 收缩 */
.exam-progress > i {
  display: block; height: 100%; width: 100%;
  transform-origin: left center; transform: scaleX(1);
  background: var(--grad);
  transition: transform .25s linear, background .25s;
}
.exam-timer.warn .exam-progress > i { background: linear-gradient(135deg, #c98a3c 0%, #d9a95f 100%); }
.exam-timer.danger .exam-progress > i { background: linear-gradient(135deg, #ef4444 0%, #f87171 100%); }
.exam-timer .timer-sub { font-size: 12.5px; color: var(--text-soft); font-variant-numeric: tabular-nums; }
/* ≤576px 收字号并改单列；320px 起不换行、不横向溢出 */
@media (max-width: 576px) {
  .exam-timer { padding: 10px 12px; }
  .exam-timer .time { font-size: 19px; }
  .exam-timer .timer-state { font-size: 12px; }
  .exam-timer .timer-sub { font-size: 12px; }
  .exam-timer .timer-row { flex-direction: column; align-items: flex-start; gap: 2px; }
}

/* ---------- 表格 ---------- */
.table { width: 100%; border-collapse: collapse; font-size: 14px; }
/* 单元格内容不换行：会员列表里用户名、手机号、学校、备注都可能偏长，
   换行会让某一行高出一截，扫视时对不齐，也难横向对比同一列。
   外层 .table-scroll 已有 overflow-x:auto，加 min-width 后整体横向滚动即可。 */
.table th, .table td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--border); white-space: nowrap; }
.table th { color: var(--text-soft); font-weight: 700; background: #fafbff; }
.table tr:hover td { background: #fafbff; }
/* 允许换行的例外：地区、联系方式这类可能较长的列。
   表格主体已设为 nowrap（保证每列一排、扫视对齐），
   这些列用 ta-wrap 显式恢复折行，避免把整张表撑得过宽。 */
.table .ta-wrap, .ta-wrap { white-space: normal; max-width: 260px; }

/* ---------- 徽章 ---------- */
.badge { display: inline-block; font-size: 12px; padding: 3px 10px; border-radius: var(--radius-pill); font-weight: 700; white-space: nowrap; }
/* 长文案徽章（赞助解锁说明、邀请加成）加 .badge-wrap 允许折行，
   否则 nowrap 会把它撑成 470px+，连带整页横向出屏（用户 2026-10-15 截图）。 */
.badge-wrap { white-space: normal; }
/* 用 ink 深色版做文字色：原 --ok/--err/--warn 在对应浅底上只有 2~3.4:1，不满足 WCAG AA 4.5:1 */
.badge.ok { background: var(--ok-bg); color: var(--ok-ink); }
.badge.err { background: var(--err-bg); color: var(--err-ink); }
.badge.warn { background: var(--warn-bg); color: var(--warn-ink); }
.badge.vip { background: #fbf1de; color: #8a5a18; }
.badge.admin { background: var(--primary-light); color: var(--primary-700); }

/* ---------- 筛选 chips ---------- */
/* （原 .filter-row-right「少 chips 靠右」工具类已随口语页级别行重排版删除——
    2026-10-14 起口语的 四级/六级 移入题号行 .sp-toprow，不再单独成行。） */
.filter-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
.chip {
  padding: 8px 15px; border-radius: var(--radius-pill); border: 1.5px solid var(--border);
  background: #fff; cursor: pointer; font-size: 13px; color: var(--text-soft); font-weight: 600;
  transition: transform .15s, border .15s, background .15s, color .15s;
}
.chip:hover { transform: translateY(-1px); border-color: var(--primary-400); color: var(--primary-600); }
.chip.active { background: var(--grad); color: #fff; border-color: transparent; box-shadow: 0 4px 12px rgba(99,102,241,.3); }

/* ---------- 空状态 / 错误 ---------- */
.empty {
  text-align: center; color: var(--text-soft); padding: 44px 16px;
  border: 1.5px dashed var(--border); border-radius: var(--radius); background: #fff;
  font-size: 14px;
}
.empty .em-emoji { font-size: 38px; display: block; margin-bottom: 10px; }
.err-box {
  background: var(--err-bg); border: 1px solid #fecaca; color: var(--err);
  padding: 14px 16px; border-radius: 12px; margin: 10px 0;
}
.link { color: var(--primary-600); cursor: pointer; text-decoration: none; font-weight: 600; }
.link:hover { text-decoration: underline; }
/* 表格里用按钮承载「继续考试」：保持链接观感，但语义是 button，键盘可达 */
.link-btn {
  background: none; border: 0; padding: 0; font: inherit;
  color: var(--primary-600); cursor: pointer; font-weight: 600;
}
.link-btn:hover { text-decoration: underline; }
.link-btn:disabled { opacity: .55; cursor: not-allowed; }
.link-btn:focus-visible { outline: 2px solid var(--primary-600); outline-offset: 2px; border-radius: 4px; }
/* 危险次级操作：用颜色 + 文案共同传达，不只靠颜色 */
.btn-danger-text { color: var(--err-ink); border-color: #fecaca; }
.btn-danger-text:hover { background: var(--err-bg); }

/* 后台会员管理的语义按钮。
   封禁是破坏性操作 → 红底；解封是恢复操作 → 绿底。
   必须与 .btn-outline（白底描边）区分：同屏出现时管理员不能靠位置记忆
   判断哪个是"危险"的。 */
.btn-warn {
  background: #fff; color: #b91c1c; border: 1.5px solid #fca5a5;
  box-shadow: none;
}
.btn-warn:hover { background: #fef2f2; border-color: #ef4444; }
.btn-ok {
  background: #fff; color: #15803d; border: 1.5px solid #86efac;
  box-shadow: none;
}
.btn-ok:hover { background: #f0fdf4; border-color: #22c55e; }
.btn-danger {
  background: #dc2626; color: #fff; border: 1.5px solid #dc2626;
  box-shadow: 0 6px 16px rgba(220,38,38,.28);
}
.btn-danger:hover { background: #b91c1c; border-color: #b91c1c; }

/* ---------- 断考恢复卡片 ---------- */
.exam-resume {
  border: 1.5px solid #fde68a;
  background: linear-gradient(180deg, var(--warn-bg), #fff);
  margin-bottom: 16px;
}
.exam-resume .resume-head { display: flex; gap: 14px; align-items: flex-start; }
.exam-resume .resume-icon { font-size: 26px; line-height: 1.2; }
.exam-resume h3 { margin: 0 0 4px; font-size: 16px; color: var(--text); }
.exam-resume .resume-meta { margin: 0; font-size: 13.5px; color: var(--text-soft); line-height: 1.7; }
.exam-resume .resume-meta b { color: var(--warn-ink); }
.exam-resume .btn-row { margin-top: 14px; }

/* 作答自动保存提示 */
.exam-save-tag {
  font-size: 12.5px; color: var(--text-mute); margin: -6px 0 14px;
  display: flex; align-items: center; gap: 6px;
}
.exam-save-tag::before { content: '💾'; }

/* 空状态卡片（未交卷时查看成绩的出口） */
.empty-state { text-align: center; padding: 40px 20px; }
.empty-state .es-icon { font-size: 40px; margin-bottom: 12px; }
.empty-state h3 { margin: 0 0 8px; font-size: 17px; }
.empty-state p { margin: 0; color: var(--text-soft); font-size: 14px; }

.form-hint {
  font-size: 13px; color: var(--text-soft); margin: 0 0 12px;
  background: var(--primary-light); border-radius: 10px; padding: 10px 12px;
}

/* ---------- Toast ---------- */
.toast-wrap { position: fixed; top: 16px; left: 50%; transform: translateX(-50%); z-index: 200; display: flex; flex-direction: column; gap: 8px; align-items: center; }
.toast {
  background: #1f2937; color: #fff; padding: 11px 20px; border-radius: 12px;
  font-size: 14px; box-shadow: 0 8px 24px rgba(0,0,0,.22); animation: fade .2s; max-width: 90vw;
}
.toast.ok { background: var(--ok); }
.toast.err { background: var(--err); }
/* 琥珀底 + 深棕字：白字在 #f59e0b 上只有约 2:1，不满足 AA */
.toast.warn { background: var(--warn-bg); color: var(--warn-ink); border: 1px solid #fde68a; }
@keyframes fade { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; } }

/* ---------- Modal ---------- */
.modal-mask {
  position: fixed; inset: 0; background: rgba(15,23,42,.45); z-index: 150;
  display: flex; align-items: center; justify-content: center; padding: 16px;
}
.modal { background: #fff; border-radius: var(--radius); max-width: 540px; width: 100%; overflow: hidden; box-shadow: var(--shadow-lg); }
.modal-head {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 12px 12px 22px; font-weight: 800;
  border-bottom: 1px solid var(--border); font-size: 16px;
}
/* 标题本身用 h2（弹窗是个 dialog，标题该是标题元素，屏幕阅读器能按标题跳转），
   默认 margin 必须清掉；长用户名要能换行而不是把 × 挤出屏幕。 */
.modal-head-title {
  flex: 1 1 auto; min-width: 0; margin: 0;
  font-size: 16px; font-weight: 800; line-height: 1.4;
  overflow-wrap: anywhere;
}
/* 标题栏右侧关闭：32px 圆形点击区（WCAG 2.2 目标尺寸下限 24px），
   触屏与鼠标都够点；hover/active 只动 background 与 transform。 */
.modal-x {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0; border: none; background: transparent;
  border-radius: 50%; color: var(--text-mute); font-size: 22px; line-height: 1;
  cursor: pointer; transition: background .2s, color .2s, transform .2s;
}
.modal-x:hover { background: var(--primary-light); color: var(--primary-600); }
.modal-x:active { transform: scale(.95); }
.modal-x:focus-visible { outline: 2px solid var(--primary-600); outline-offset: 2px; border-radius: 50%; }
.modal-body { padding: 22px; max-height: 70vh; overflow: auto; }
.modal-foot {
  padding: 15px 22px; border-top: 1px solid var(--border);
  display: flex; flex-wrap: wrap; gap: 10px; justify-content: flex-end;
}
/* 按钮不许被压窄成"一字一行"：flex 的 min-width:auto 只保证单字宽 + 内边距，
   窄屏上长文案（如「调整 AI 次数」）会被挤成竖排文字（用户 2026-10-15 实报）。
   min-width:max-content 让按钮保持整句一行，放不下就整行换行。 */
.modal-foot .btn { min-width: max-content; }
/* 动作多、文案长的弹窗（会员详情的 7 个动作）走等宽矩阵：
   手机 2 列、≥600px 3 列，每个按钮同宽同行高，视觉上是一块整齐的操作区。
   由 JS 通过 modal(title, body, foot, 'is-grid') 显式开启，普通确认弹窗不受影响。 */
.modal-foot.is-grid { display: grid; grid-template-columns: repeat(2, 1fr); }
.modal-foot.is-grid .ud-close { grid-column: 1 / -1; }
@media (min-width: 600px) {
  .modal-foot.is-grid { grid-template-columns: repeat(3, 1fr); }
}

/* ---------- 打印 ---------- */
.print-area { padding: 24px; background: #fff; }
@media print {
  body * { visibility: hidden; }
  #printArea, #printArea * { visibility: visible; }
  #printArea { position: absolute; left: 0; top: 0; width: 100%; }
}

/* =========================================================
   熟词僻义中心（差异化亮点模块）
   ========================================================= */
/* ---------- 首屏控制面板（第 31 轮整体重排） ----------
   用户反馈「整体重新排版的好看点，现在太占位置了」（圈出首屏整块）。
   实测 390px 下旧版控件区约 473px（近一屏的 70%），成因是**五块各自带边框
   的独立卡片纵向堆叠**：① 渐变 Intro 卡 ② 浏览模式卡 ③ 自测模式卡
   ④ 筛选框 ⑤ 搜索行，外加 ⑥ 进度胶囊。每块都有 border + padding + margin，
   盒子套盒子，纵向开销翻倍。

   新结构：**一块面板**（.qk-panel），内部分区靠分隔线而不是边框，
   控件按「主 → 次 → 工具 → 状态」重排（HTML 顺序见 renderQuirks）。
   390px 实测约 292px（−38%），省下的全部还给词条列表。 */
.qk-panel {
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 12px 14px 10px; margin-bottom: 14px;
}
/* 标题行：图标 / 标题 / 动作三者**垂直居中**同一行。
   旧版把灯泡与标题做顶部对齐，是因为标题下挂着两行说明文字；
   现在说明挪到分段控件下方（.qm-hint），标题行只剩单行，居中才对。 */
.quirk-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
/* 图标收进浅色圆角块：比裸 emoji 更像"设计过的徽标"，
   16px 标题与它同高（26px），整行因此只有 26px。 */
.qh-icon {
  flex: none; width: 26px; height: 26px; border-radius: 9px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 15px; line-height: 1; background: var(--primary-light);
}
/* min-width:0 让标题可收缩 —— 否则窄屏上按钮会被内容顶出面板。 */
.quirk-head h2 {
  flex: 1 1 auto; min-width: 0; margin: 0;
  font-size: 16px; font-weight: 800; color: var(--primary-700); line-height: 1.25;
}
/* 「重新学习」：低频 + 破坏性动作，做成标题行右上角的幽灵按钮。
   文字色 --text-soft 对白底 6.3:1（≥4.5:1，WCAG 2.2 AA 正文级）。 */
.qh-act {
  flex: none; display: inline-flex; align-items: center; gap: 3px;
  font-family: inherit; font-size: 12px; font-weight: 600; color: var(--text-soft);
  background: transparent; border: 1px solid var(--border);
  border-radius: var(--radius-pill); padding: 4px 9px; min-height: 25px;
  cursor: pointer; white-space: nowrap;
  transition: color .18s, border-color .18s, background-color .18s;
}
.qh-act:hover { color: var(--primary-700); border-color: var(--primary-500); background: var(--primary-light); }
.qh-act:focus-visible { outline: 2px solid var(--primary-600); outline-offset: 2px; }

/* ---------- 分段控件（浏览模式 / 自测模式） ----------
   旧版这两张卡是 grid 1fr 1fr，≤600px 被改成单列 —— 每张 ~70px，
   叠起来 150px，是首屏最大的一笔开销。改成分段控件后：
   ① 两段**永远并排**（不再有堆叠断点）；
   ② 说明文字移出按钮（见 .qm-hint），按钮只剩图标 + 4 字标题，34px 高；
   ③ 选中态 = 白底 + 阴影 + 深色字，不只靠颜色（灰度/弱视下同样可辨）。 */
.quirk-modes {
  display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
  padding: 4px; margin: 0 0 8px;
  background: var(--primary-100); border-radius: 12px;
}
.qm-btn {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  min-height: 34px; padding: 0 6px;
  background: transparent; border: 0; border-radius: 9px;
  font-family: inherit; font-size: 13.5px; font-weight: 700; color: var(--text-soft);
  cursor: pointer; white-space: nowrap;
  transition: color .18s, background-color .18s, box-shadow .18s;
}
/* 未选中段：--text-soft 对 --primary-100 轨道底 ≈5.6:1，达正文级标准 */
.qm-btn:hover { color: var(--primary-700); }
.qm-btn:focus-visible { outline: 2px solid var(--primary-600); outline-offset: 2px; }
.qm-btn.active { background: #fff; color: var(--primary-700); box-shadow: 0 1px 4px rgba(43, 48, 56, .12); }
.qm-ico { font-size: 15px; line-height: 1; flex: none; }
/* 当前模式的一句话说明，随模式切换更新（repaintMode 同步 textContent）。
   单行 + 省略号兜底：文案再长也不许折成两行把首屏撑高。 */
.qm-hint {
  margin: 0 2px 10px; font-size: 12px; line-height: 1.45; color: var(--text-soft);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ---------- 平台口令归因条 ----------
   内容平台引流兑换入口。做成面板内一条浅色行：不抢主控件视线，
   但要让「从抖音/小红书搜过来」的用户一眼看到自己有福利没领。
   配色走项目现有的 --accent-soft / --primary-light，不引新色。 */
.promo-bar { margin: 16px 2px 12px; }
.promo-row {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 12px; border-radius: var(--radius-pill);
  background: var(--accent-soft);
  border: 1px dashed var(--accent);
}
.promo-done {
  background: var(--primary-light);
  border-style: solid;
  border-color: var(--primary);
}
.promo-ico { font-size: 15px; line-height: 1; flex: 0 0 auto; }
.promo-txt {
  flex: 1 1 auto; min-width: 0; font-size: 12.5px; line-height: 1.4;
  color: #111; overflow: hidden; text-overflow: ellipsis;
}
.promo-txt b { color: var(--text); }
.promo-input {
  flex: 0 1 150px; min-width: 0; height: 30px; box-sizing: border-box;
  padding: 4px 12px; font-size: 12.5px; border-radius: var(--radius-pill);
  border: 1px solid var(--border); background: var(--card);
  color: #111;
}
.promo-input::placeholder { color: #888; }
.promo-btn {
  flex: 0 0 auto; height: 30px; padding: 0 14px;
  font-size: 12.5px; font-weight: 600; border: none; cursor: pointer;
  border-radius: var(--radius-pill);
  background: var(--primary); color: #111;
}
.promo-btn:disabled { opacity: .6; cursor: default; }
/* 四平台口令公示行：小字、不抢视线，但对「从哪个平台来的」这个
   真实困惑给出答案 —— 转化率比口令神秘感值钱。
   必须 1 行放下（用户 2026-10-16 截图反馈：390px 手机上折成两行）：
   字号压到 10px + nowrap，超宽走省略号兜底，绝不换行撑高。 */
.promo-codes {
  margin: 6px 2px 0; font-size: 10px; line-height: 1.5;
  color: var(--text-mute); text-align: center;
  letter-spacing: .02em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* 窄屏：说明文字让位，输入框 + 按钮保持可点 */
@media (max-width: 480px) {
  .promo-txt { display: none; }
  .promo-input { flex: 1 1 auto; }
}
/* Admin 归因看板：手机宽度下 5 列不出框（用户 2026-10-16 反馈表格
   错位跑出卡片）—— 基础 .table 是 14px + 11px 13px 内边距 + nowrap，
   这里整体收紧字号与间距。 */
.promo-table { font-size: 12px; }
.promo-table th, .promo-table td { padding: 6px 6px; }
/* 会员管理列表：字号整体收小，与上方平台口令表格一致；
   外层 .table-scroll 已支持横向滑动看学校等后续列。 */
.admin-users-sm { font-size: 12px; }
.admin-users-sm th, .admin-users-sm td { padding: 7px 9px; }
/* 启停 = 纯文字点按（用户明确「不用做按钮」）：启用深色 / 关闭灰，
   点划线只作可点暗示。 */
.promo-state {
  cursor: pointer; font-weight: 600; color: #111;
  border-bottom: 1px dotted var(--border);
}
.promo-state.off { color: var(--text-mute); font-weight: 400; }
/* 平台名可点选筛选最近兑换：默认主色 + 点划线暗示可点；
   选中态高亮（背景填充 + 去下划线）。 */
.promo-plat {
  cursor: pointer; color: var(--primary);
  border-bottom: 1px dotted var(--border);
}
.promo-plat.active {
  color: #111; background: var(--primary-light);
  border-radius: 4px; padding: 0 4px; border-bottom: none;
}
/* 最近兑换的日期：小一号、与左侧用户名同为灰色、不加粗
   （覆盖 .stat-row .v 的 700 加粗）。 */
.promo-date { font-size: 12px; font-weight: 400; color: var(--text-mute); }

/* ---------- 工具行：搜索框独占整行 ---------- */
.quirk-tools { display: flex; align-items: center; gap: 8px; }
.quirk-search { position: relative; flex: 1 1 auto; }
.quirk-search input {
  width: 100%; height: 34px; box-sizing: border-box;
  padding: 6px 14px 6px 34px; font-size: 13.5px; line-height: 1.2;
  border-radius: var(--radius-pill);
}
/* 放大镜是纯装饰（aria-hidden），故用低对比的 --text-mute：
   它不承载信息，不适用正文 4.5:1 的要求。 */
.quirk-search .qs-ico {
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  font-size: 13px; line-height: 1; color: var(--text-mute); pointer-events: none;
}
@media (max-width: 360px) {
  /* 极窄屏：标题行的动作按钮再收一档。
     320px 实测 26+8+96+8+70 = 208 < 256 可用宽，本就放得下，
     这里只是给更大字体/更长品牌名留的余量。 */
  .qh-act { padding: 4px 7px; font-size: 11.5px; }
}

.quirk-grid { display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }

/* 浏览模式配色对齐自测模式（quiz-card / quiz-word）：
   此前两套各自为政——自测是渐变底+28px 大字+主色，浏览是纯白底+22px 小字，
   用户在同一页里来回切换会明显感觉「像两个产品」。统一到自测那套视觉语汇。 */
.quirk-card {
  background: linear-gradient(160deg, #fff 0%, #f5f3fa 100%);
  border: 1.5px solid var(--primary-100); border-radius: var(--radius);
  padding: 22px; box-shadow: var(--shadow); display: flex; flex-direction: column; gap: 12px;
  transition: transform .2s, box-shadow .2s;
}
.quirk-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-lg); }
.quirk-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.quirk-word { font-size: 26px; font-weight: 800; color: var(--primary-700); line-height: 1.25; }
.quirk-phon { font-size: 13px; color: var(--text-mute); font-weight: 500; }
.quirk-cats { display: flex; gap: 6px; flex-wrap: wrap; margin: 2px 0 4px; }
.quirk-lv { font-size: 11px; font-weight: 700; padding: 2px 9px; border-radius: var(--radius-pill); }
.quirk-lv.cet4 { background: #e2e8f2; color: #3d4a8a; }
.quirk-lv.cet6 { background: #f4e6ee; color: #8f4f6d; }
.quirk-lv.both { background: var(--primary-light); color: var(--primary-700); }
.quirk-lv.qtag { background: var(--accent-soft); color: #8a5a18; }
.quirk-mean-row { font-size: 14px; }
.quirk-mean-row .lab { color: var(--text-mute); font-weight: 600; margin-right: 6px; }
.quirk-common { color: var(--text-soft); }
.quirk-quirk { color: var(--text); font-weight: 700; }
.quirk-hidden { filter: blur(6px); user-select: none; transition: filter .2s; }
.quirk-hidden.revealed { filter: none; }
/* 例句同样对齐自测模式：白底 + 细描边，而不是浅蓝底 + 左侧色条 */
.quirk-example {
  background: #fff; border: 1px solid var(--border); border-radius: 12px; padding: 14px;
  font-size: 13.5px; color: var(--text); line-height: 1.7;
}
.quirk-example .ex-en { line-height: 1.6; }
.quirk-example .ex-zh { color: var(--text-soft); margin-top: 4px; }
.quirk-tip { font-size: 13px; color: var(--accent); background: var(--accent-soft); border-radius: 10px; padding: 9px 12px; }
.quirk-actions { display: flex; gap: 8px; margin-top: 4px; }

/* ---------- 浏览进度（断点续看） ----------
   一条细进度条 + 「只看没看过的」开关，横向排布。
   不做成卡片：它不是内容，是"你读到哪了"的状态条，
   给它同样的卡片待遇会和下面的词条抢注意力。 */
/* 进度条 + 「只看没看过的」开关，压成**一行**，且不再自画边框。
   第 31 轮把首屏控件合并进 .qk-panel 后，它从"独立胶囊"降级为面板里的
   最后一段：再套一层 border 就是面板里套盒子，视觉上多一层杂音。
   现在只有一条细进度条 + 计数 + 开关，nowrap 保证 320px 也不折行。 */
.qk-prog {
  display: flex; align-items: center; gap: 8px;
  flex-wrap: nowrap;
  padding: 8px 0 0; margin: 0;
  /* 显式 box-sizing：不写的话 padding 会加在 height 之外，
     而某些全局规则（如 * { box-sizing: border-box } 若被覆盖）
     会让实测高度比预期大 18px。锁死它，尺寸可预测。 */
  box-sizing: border-box;
}
.qk-bar {
  flex: 1 1 44px; min-width: 44px; height: 5px;
  background: var(--primary-100); border-radius: var(--radius-pill); overflow: hidden;
}
.qk-bar > i { display: block; height: 100%; background: var(--grad); border-radius: var(--radius-pill); transition: width .25s ease; }
.qk-txt {
  font-size: 11.5px; color: var(--text-soft); white-space: nowrap;
  flex: none; font-variant-numeric: tabular-nums;
  /* line-height 必须显式给 1。
     实测：不给时这一项行盒高 19px（11.5px 字却被撑到 19px），
     容器因此从 31px 涨到 57px —— 折行的不是它，是它把别人挤下去的。
     b/span 有自己的默认行高继承链，父级不锁死就会被撑开。 */
  line-height: 1;
}
.qk-txt b { color: var(--primary-700); font-variant-numeric: tabular-nums; }
.qk-sw {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 11.5px; color: var(--text-soft); cursor: pointer; user-select: none;
  /* 去边框去底色：它现在是面板内的一段，不再自成一个胶囊。
     min-height 24px 是为 WCAG 2.2 §2.5.8「目标尺寸 ≥24px」——
     label 整体可点，13px 的 checkbox 本身不达标，靠 label 撑够。 */
  padding: 3px 0; min-height: 24px; box-sizing: border-box;
  white-space: nowrap; flex: none; line-height: 1.3;
  /* ⚠️ margin 必须显式归零 —— 这是一个查了很久的**真根因**，别再删：
     全局有 `label { margin: 12px 0 6px }`（给表单字段标签用的，见「表单」一节），
     而 .qk-sw 是个**当行内控件用的 <label>**，于是它默默吃到了那 12px/6px。
     后果有两层，都很难从截图上看出原因：
       ① flex 行的行高按子项的**边距盒**计算 → 24+18=42px，
          整行从 32px 涨到 50px，白吃 18px 纵向空间（进度条看起来"悬空"）；
       ② 上 12 / 下 6 不对称 → 开关相对进度条**下移 3px**（(12−6)/2），
          和左右的文字不在一条视觉中线上。
     box-sizing:border-box 只约束 border 盒，管不到 margin —— 所以 min-height
     写了也没用。实测：不归零 50px，归零后 32px。 */
  margin: 0;
}
/* checkbox 是 replaced 元素，默认有自己的盒高（约 13px），
   会把 label 撑到比文字高一倍。锁死它，父级的 line-height 才说了算。 */
.qk-sw input { margin: 0; width: 13px; height: 13px; flex: none;
  cursor: pointer; accent-color: var(--primary-600); }
/* 开关用 accent-color 一行上色，不自绘控件 —— 自绘要多花十几行
   还要处理各种浏览器差异，收益远小于维护成本。 */
.qk-sw input { margin: 0; cursor: pointer; accent-color: var(--primary-600); }

/* 「继续上次」横幅：同样压成一行。
   原来是「文字 + 继续 + 重新从头」flex-wrap 混排，
   在 390px 下按钮被挤到第二行，整块变成三行高。
   现在文字左、按钮右，中间弹性留白。 */
.qk-resume {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; flex-wrap: nowrap;
  padding: 7px 12px; margin-bottom: 8px; font-size: 12.5px;
  background: var(--accent-soft); border-radius: var(--radius-pill);
  color: #8a5a18;
}
.qk-resume-txt { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.qk-resume-acts { display: flex; gap: 6px; flex: none; }
/* 按钮压小一档：横幅里的操作是辅助功能，不该和主操作一样重 */
.qk-resume-acts .btn { padding: 4px 11px; font-size: 12.5px; }
/* 已看过的卡片：整卡降一档透明度，一眼区分「读过的」与「还没读的」。
   只用颜色区分是不够的，所以另外还有 ✓ 标记（见 .qseen）。 */
.quirk-card.is-seen { opacity: .62; }
.quirk-card.is-seen:hover { opacity: 1; }
.qseen {
  display: inline-block; margin-left: 6px; vertical-align: middle;
  font-size: 12px; color: var(--ok-ink); font-weight: 700;
}
.fav-btn {
  border: 1.5px solid var(--border); background: #fff; border-radius: var(--radius-pill);
  padding: 6px 14px; font-size: 13px; font-weight: 600; color: var(--text-soft); cursor: pointer;
  transition: transform .15s, border .15s, color .15s, background .15s;
}
.fav-btn:hover { transform: scale(1.04); border-color: var(--primary-400); color: var(--primary-600); }
.fav-btn.on { background: var(--accent-soft); border-color: #e2c79b; color: #8a5a18; }

/* 自测模式 */
.quiz-card {
  background: linear-gradient(160deg, #fff 0%, #f5f3fa 100%);
  border: 1.5px solid var(--primary-100); border-radius: var(--radius);
  padding: 22px; box-shadow: var(--shadow); min-height: 220px;
  display: flex; flex-direction: column; gap: 14px; justify-content: center;
}
/* 自测模式：单词是大字号标题。
   喇叭尺寸不在这里定 —— 统一走 .vc-speak 的 sm 档（26px），
   与熟词僻义浏览模式一致。
   注意这一行和后面喇叭那节都出现过 .quiz-word 的定义：早先这里
   写的是 flex 布局 + 38px 喇叭，喇叭那节又写了一次同样的大尺寸，
   两处尺寸还不一样（38 vs 32）。同一个类散落两处定义、
   各改各的，是这类样式漂移最常见的成因 —— 现在尺寸只剩一处。 */
.quiz-word { font-size: 28px; font-weight: 800; color: var(--primary-700); }
.quiz-prompt { color: var(--text-soft); font-size: 14px; }
.quiz-example { background: #fff; border: 1px solid var(--border); border-radius: 12px; padding: 14px; font-size: 15px; line-height: 1.7; }
.quiz-answer { background: var(--ok-bg); border: 1px solid #bbf7d0; border-radius: 12px; padding: 14px; font-size: 15px; }
.quiz-answer b { color: var(--ok); }

/* 统计小卡（仪表盘用） */
.mini-stat {
  background: #fff; border: 1px solid var(--border); border-radius: var(--radius);
  padding: 16px; box-shadow: var(--shadow);
}
.mini-stat .ms-num { font-size: 26px; font-weight: 800; color: var(--primary-700); }
.mini-stat .ms-lab { font-size: 13px; color: var(--text-soft); margin-top: 2px; }

/* 用户名导航项：与其它功能项**完全同款**（同 padding / 圆角 / 字号 / 过渡）。
   它是导航项之一，不是一个"特别的东西" —— 早前它是导航栏外的一行文字，
   视觉上像标签而不像入口，用户不知道能点。
   用 --nav-x 变量与 .nav-links a 共享同一组值，
   以后调整导航样式只改一处，不会两处漂移。 */
.nav-me {
  text-decoration: none;
  color: var(--text-soft);
  padding: 8px 14px; border-radius: 10px;
  font-size: 14px; font-weight: 600;
  white-space: nowrap;
  transition: background .15s, color .15s;
  /* 与前面拉开一点距离：它是身份标识，不是第N 个功能入口 */
  margin-left: 10px;
  position: relative;
  /* ⚠️ 必须 flex: none（= flex-grow:0; flex-shrink:0; flex-basis:auto）。
     根因不是它被右侧「退出」按钮压住 —— 实测两者间距 6px，中心点也命中它，
     真正的问题是**它被压扁了**：内容要 128px，窄屏上只拿到 22px，
     剩下 82.8% 被 overflow:hidden 裁掉，看上去就像被退出按钮"吃掉"了一截。

     为什么会被压扁：flex 子项的 min-width:auto 会取"自动最小尺寸"，
     而按 CSS Flexbox 规范，**当 overflow 不为 visible 时该值算作 0**
     （因为一旦允许溢出，就不存在"必须完整显示"的最小尺寸）。
     早前窄屏规则给 .nav-me 加了 overflow:hidden 做省略号，
     min-width 随之变成 0，于是 flex-shrink:1 就能把它一路压到只剩 padding。

     写法选 flex:none 而非单独写 min-width，是为了同时关掉 shrink：
     容器本来就 overflow-x:auto，多出来的宽度交给横向滚动，
     不该由"把某一项压扁"来消化。
     ⚠️ 此后 overflow/ellipsis 一律不再加回来（见 @media 600px 内的注释）：
     省略号 + flex:none 是自相矛盾的两套策略。 */
  flex: none;
}
/* 分隔竖线：不靠颜色区分身份项，形状本身就说明了"这是另一类东西" */
.nav-me::before {
  content: '';
  position: absolute;
  left: 0; top: 50%;
  width: 1px; height: 16px;
  margin-top: -8px;
  background: var(--border);
}
.nav-me:hover { background: var(--primary-light); color: var(--primary-600); }
.nav-me.active { background: var(--grad); color: #fff; box-shadow: 0 4px 12px rgba(99,102,241,.3); }
.nav-me.active::before { background: transparent; }
.nav-me:focus-visible { outline: 2px solid var(--primary-600); outline-offset: 2px; }
/* 赞助会员：导航用户名后「（赞助）」黄色标示（用户 2026-10-16）。
   角色文字本身已写明身份，黄色仅为强调，不违反 WCAG「颜色非唯一辨识通道」。 */
.nav-me .nav-role { font-size: 12px; font-weight: 600; margin-left: 3px; opacity: .9; }
.nav-me.is-vip .nav-role { color: var(--vip-yellow); }
/* 激活态（白字 + 靛蓝渐变底）改用亮黄，保证可读 */
.nav-me.is-vip.active .nav-role { color: #ffe066; }

/* 账号信息卡里的管理员入口。
   用分隔线与上方只读字段区隔开：那一片是"看"，这里是"做"，
   混在一起会让人以为整张卡都不能点。 */
.admin-entry {
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

/* 题库导出卡片。与 .admin-entry 同为"只读信息下方的动作区"，
   所以沿用同一套分隔线语言，保证两张卡片看起来是一组。 */
.export-entry {
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}
/* 头部行：标题左、权益标签紧随。原来把 6px 下边距挂在 h3 上，
   而 .ee-head 是 align-items:center 的 flex 行 —— h3 的底边距会把整行高度
   撑大并让标题相对标签上移，看起来"两个元素没对齐"。改为 h3 归零、
   间距交给容器自己的 margin-bottom（用户 2026-10-15：「排版做整齐一点」）。 */
.ee-head {
  display: flex; align-items: center; gap: 10px;
  flex-wrap: wrap; margin-bottom: 8px;
}
.ee-head h3 { margin: 0; font-size: 15px; }
/* 赞助支持卡内：解锁徽章与「导出题库」控件之间的实线发丝分隔。
   用实线（非虚线），与全站「普通会员界面去虚线」口径一致（2026-10-16）。 */
.ee-sep { border: none; border-top: 1px solid var(--border); margin: 14px 0 2px; }
/* 标签不用纯绿色底 + 白字：那是对比度最差的组合之一。
   用浅底 + 深字，文字对比度 >= 4.5:1。 */
.ee-tag {
  font-size: 12px; font-weight: 700;
  padding: 3px 9px; border-radius: 999px;
  background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0;
  white-space: nowrap;
}
.ee-row { display: flex; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
/* 导出动作组：按钮均分整行宽度。
   旧样式复用 .btn-row-center → 按钮被压到 min-width:128px 居中，
   卡片里两侧各留一大片空白（用户 2026-10-15：「空间利用起来」）。
   单个按钮（未赞助态）时 flex:1 铺满整行，两个按钮（赞助态）左右等宽，
   两种状态视觉语言一致。
   ⚠️ min-width 必须是 max-content 而不是 0：0 允许 flex 把按钮压到比文字还窄，
   320px 下「下载 Markdown」会被压成两行（实测）；max-content 让按钮的
   最小宽度 = 文字本身，两个放不下时依靠 .ee-row 的 flex-wrap 整块换行
   （各自独占一行、依然铺满），而不是把字拆开。 */
.ee-acts { margin-top: 14px; }
.ee-acts .btn { flex: 1 1 0; min-width: max-content; }
/* 320~359px：并排放不下两个按钮（「下载 Markdown」固有宽 141px + 「直接打印」
   118px + 10px 间距 = 269px > 该区间可用宽度），若硬挤会因为 min-width 钳制
   变成一大一小（实测 141/99，占总宽 56%/40%），失衡且难看。
   改为上下两块各自铺满整行：两个按钮等宽、文字都不断行。 */
@media (max-width: 359px) {
  .ee-acts { flex-direction: column; }
}
/* 导出续打：避免每次都重复同一页最前面的题，可从上次结束处接着打 */
/* 同一行：左勾选框、右「重置进度」靠右（justify-content:space-between + margin-left:auto 双保险） */
.ee-resume-row { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.ee-resume {
  display: flex; align-items: center; gap: 6px;
  font-size: 13px; color: var(--text); cursor: pointer;
}
.ee-resume input { width: auto; margin: 0; }
.ee-hint { color: var(--text-soft); font-size: 12px; }
.ee-reset {
  margin-left: auto; font-size: 12px; white-space: nowrap;
  color: var(--primary); text-decoration: underline;
}
.ee-field { flex: 1 1 140px; min-width: 130px; }
.ee-field label {
  display: block; margin-bottom: 5px;
  font-size: 13px; color: var(--text-soft); font-weight: 600;
}
.ee-field select {
  width: 100%; padding: 8px 10px;
  border: 1px solid var(--border); border-radius: 8px;
  background: #fff; font: inherit; font-size: 14px; color: var(--text);
}
.ee-field select:focus-visible {
  outline: 2px solid var(--primary-600); outline-offset: 1px;
}

/* ============ AI 额度显示 + 额度用尽友好卡 ============
   额度上限是**浮动的**（每邀 1 人 +3 次，封顶 13 人 → 20~59 次），
   所以必须让用户随时看见"今日已用 12/26"，否则一旦超出就是
   「界面说还剩 5 次、点下去说用完」—— 最伤信任的一种体验分裂。 */
[data-ai-quota] {
  display: block; font-size: 12.5px; color: var(--text-soft);
  margin-top: 6px;
}
[data-ai-quota].is-low { color: var(--warn, #b7791f); font-weight: 600; }
/* [data-ai-bonus] 的样式已删除：它渲染的「邀请加成：每天 +N 次（已邀 X 人…）」
   与邀请卡里的绿徽章重复（用户 2026-10-15 确认后移除元素）。 */
.ai-cache-tip {
  font-size: 12px; color: var(--primary-700); background: var(--primary-light);
  border-radius: 8px; padding: 4px 9px; margin-bottom: 8px;
}
/* 额度用尽：明确定位成「今天的免费额度用完了」，不是「系统故障」。
   黄色而非红色 —— 它不是错误，只是需要用户做一个选择。 */
.ai-limit-card {
  border: 1px solid var(--border); border-left: 4px solid var(--primary-500);
  background: var(--primary-light); border-radius: var(--radius-sm);
  padding: 12px 14px; margin-top: 10px;
}
.ai-limit-title { font-size: 14.5px; font-weight: 800; color: var(--primary-700); }
.ai-limit-meta { font-size: 12.5px; color: var(--text-soft); margin-top: 4px; }
.ai-limit-note { font-size: 13px; color: var(--text); margin-top: 6px; line-height: 1.6; }
.ai-limit-wx { font-size: 12px; color: var(--text-soft); margin-top: 8px; text-align: center; }

/* ============ 落地页（# /home）============
   定位：登录后第一屏 —— 把「亮点」与「免费项目」摆出来让用户选。
   关键设计：亮点卡**每张都是真入口**（button + data-go），不是纯展示，
   这是解决「AI 批注藏在导航深处没人发现」的核心手段。

   配色说明（2026-10-14 用户要求「漂亮大气、颜色鲜艳，适合手机屏宽高度」）：
   渐变取 靛蓝 → 紫 → 洋红，三档实测与白字对比度分别为 7.4 / 6.6 / 6.0，
   全部高于 WCAG AA 的 4.5:1 —— 鲜艳与可读不冲突，别为了"好看"用浅粉配白字。 */
.home-hero {
  position: relative; overflow: hidden;
  background: linear-gradient(135deg, #4338ca 0%, #7c3aed 48%, #be185d 100%);
  color: #fff; border-radius: 18px; padding: 20px 16px 16px; margin-bottom: 16px;
  box-shadow: 0 10px 24px rgba(67, 56, 202, .26);
}
/* 右上角柔光斑：纯装饰，用 ::after 免得多一个 DOM 节点 */
.home-hero::after {
  content: ''; position: absolute; right: -40px; top: -50px;
  width: 160px; height: 160px; border-radius: 50%;
  background: rgba(255, 255, 255, .14); pointer-events: none;
}
.home-hero-badge {
  display: inline-block; font-size: 11.5px; font-weight: 700;
  padding: 4px 10px; border-radius: 999px; margin-bottom: 10px;
  background: rgba(255, 255, 255, .18); border: 1px solid rgba(255, 255, 255, .34);
}
.home-hero-title { font-size: 22px; font-weight: 900; line-height: 1.28; }
.home-hero-sub { font-size: 13px; opacity: .95; margin-top: 6px; line-height: 1.55; }
.home-hero-stats {
  position: relative; display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 8px; margin-top: 14px;
}
.hhs {
  background: rgba(255, 255, 255, .15); border: 1px solid rgba(255, 255, 255, .28);
  border-radius: 12px; padding: 8px 4px; text-align: center;
}
.hhs b { display: block; font-size: 18px; font-weight: 900; line-height: 1.2; }
.hhs span { display: block; font-size: 11px; opacity: .92; margin-top: 2px; }
.home-sec-title {
  font-size: 13px; font-weight: 800; color: var(--primary-700);
  margin: 16px 0 8px;
}
/* 手机两列、最后一张通栏：三张卡在 390px 上排 3 列每张只剩 ~110px，
   标题都要折行，反而更挤；2 列 + 末张通栏既整齐又不折字。 */
/* 2×2 等宽四格（亮点功能）。删除旧版 .home-card:last-child 通栏规则，
   否则第四格会独占整行、右下角留空洞（用户 2026-10-15 截图圈出的红圈）。 */
.home-cards { display: grid; gap: 10px; grid-template-columns: repeat(2, 1fr); }
.home-card {
  position: relative; overflow: hidden;
  text-align: left; background: var(--card); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 14px 12px; cursor: pointer;
  box-shadow: var(--shadow); transition: transform .2s, box-shadow .2s;
  display: block; width: 100%; font-family: inherit;
}
.home-card::before {
  content: ''; position: absolute; left: 0; right: 0; top: 0; height: 3px;
  background: var(--primary-500);
}
.home-card.hc-1::before { background: linear-gradient(90deg, #f59e0b, #ef4444); }
.home-card.hc-2::before { background: linear-gradient(90deg, #8b5cf6, #6366f1); }
.home-card.hc-3::before { background: linear-gradient(90deg, #10b981, #0ea5e9); }
.home-card.hc-4::before { background: linear-gradient(90deg, #ef4444, #f97316); }
.home-card:hover { transform: translateY(-2px); box-shadow: 0 8px 18px rgba(43,48,56,.12); }
.home-card-ico {
  width: 40px; height: 40px; border-radius: 12px; font-size: 20px;
  display: flex; align-items: center; justify-content: center;
  background: var(--primary-light); margin-bottom: 8px;
}
.home-card.hc-1 .home-card-ico { background: #fff1e6; }
.home-card.hc-2 .home-card-ico { background: #ece8ff; }
.home-card.hc-3 .home-card-ico { background: #e3f8ef; }
.home-card.hc-4 .home-card-ico { background: #ffeaea; }
.home-card-title { font-size: 15px; font-weight: 800; color: var(--text); }
.home-card-desc { font-size: 12.5px; color: var(--text-soft); margin-top: 4px; line-height: 1.5; }
.home-card-go { font-size: 12.5px; font-weight: 700; color: var(--primary-700); margin-top: 8px; }
.home-invite { border-left: 4px solid var(--primary-500); }
/* 复制邀请码紧跟在「我的邀请码」行下方、右对齐（用户 2026-10-15 要求）：
   右对齐让按钮落在邀请码**值的正下方**，从属关系一眼可见；
   原先它是卡片最底部居中的大按钮，跟邀请码隔了两行，「复制什么」要靠猜。 */
/* 邀请卡底部一行：左「已邀请 N 人」+ 右「复制邀请码」按钮（用户 2026-10-15 三轮：
   两者并成同一行）。按钮 flex:none + min-width:max-content —— flex 的
   min-width:auto 只保证单字宽，320px 下会把「复制邀请码」压成竖排文字
   （与 .modal-foot 同一类坑）。 */
.invite-foot { align-items: center; gap: 10px; }
.invite-foot .k { flex: 1 1 auto; min-width: 0; }
.invite-foot .if-num { color: var(--text); font-weight: 700; font-variant-numeric: tabular-nums; }
.invite-foot .btn { flex: none; min-width: max-content; padding: 6px 12px; }
.home-invite-line { font-size: 13.5px; color: var(--text); margin-bottom: 8px; line-height: 1.6; }
.home-practice { display: grid; gap: 8px; }
.home-pb {
  text-align: left; background: var(--primary-light); border: 1px solid transparent;
  border-radius: var(--radius-sm); padding: 12px 14px; cursor: pointer;
  font-family: inherit; transition: border-color .2s, transform .2s;
}
.home-pb:hover { border-color: var(--primary-500); transform: translateX(2px); }
.home-pb-title { font-size: 14px; font-weight: 800; color: var(--primary-700); }
.home-pb-meta { font-size: 12px; color: var(--text-soft); margin-top: 3px; }
/* 赞助状态卡：生效中给一条金色描边 + 天数徽标，与普通卡拉开层级 */
.home-vip { border-left: 4px solid var(--border); }
.home-vip.is-on { border-left-color: #f59e0b; background: #fffaf0; }
/* 用户 2026-10-15：练习块的末项「选词填空」与赞助卡原先是两块紧贴的色块，
   读起来像同一个列表的延续（"赞助支持"会被当成第 3 个练习项）。
   加一点间距把这组练习和下面这张状态卡分开。用相邻兄弟选择器限定，
   只在紧跟练习块时生效，不牵连别处的 .home-vip。 */
.home-practice + .home-vip { margin-top: 14px; }
/* 赞助卡最下一行：AI 额度状态。用户 2026-10-15 要求把原本挂在卡外的
   「🌟 赞助有效期内 · AI 批改每天 200 次」收进卡内并居中。
   上方补一条虚线 —— 直接挨着排会被读成上面那段解锁说明的续句。 */
.home-vip .vip-quota {
  text-align: center;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px dashed var(--border);
}
.vip-days {
  font-size: 12px; font-weight: 800; color: #92400e;
  background: #fef3c7; border-radius: 999px; padding: 3px 10px;
}
/* 普通会员界面去虚线（用户 2026-10-16）：首页邀请卡 / 赞助卡、个人资料账号卡
   的虚线分隔改为干净的实线发丝线，去掉「虚线」观感、保持整齐。
   仅作用于这几处，不动考试 / 错题 / 弹窗里的 .stat-row。 */
.home-invite .stat-row,
.home-vip .stat-row,
.profile-side .stat-row,
.profile-account-grid .stat-row { border-bottom: 1px solid var(--border); }
.home-invite .stat-row:last-child,
.home-vip .stat-row:last-child,
.profile-side .stat-row:last-child,
.profile-account-grid .stat-row:last-child { border-bottom: none; }
.home-vip .vip-quota { border-top: 1px solid var(--border); }

/* 首页：桌面/平板四格并排成一行（与手机 2×2 构成同一组亮点，只是更宽）。
   手机两列是刻意的（见 .home-cards 注释），别在窄屏断点里又改回单列 ——
   曾经为省高度把分段控件改回单列，结果整轮重排白做。 */
@media (min-width: 768px) {
  .home-cards { grid-template-columns: repeat(4, 1fr); }
  .home-hero { padding: 26px 22px 20px; }
  .home-hero-title { font-size: 26px; }
}
@media (max-width: 374px) {
  .home-hero-title { font-size: 20px; }
  .hhs b { font-size: 16px; }
}

/* 「这个地方会花钱」的就地提示 —— 用户 2026-10-14 要求在使用 API 的地方
   提示剩余次数。刻意做成小字 + 淡底，只在该提示时提示，不做成警告条：
   额度没用完就弹警告，等于把正常功能说成有问题。 */
[data-ai-quota-hint] {
  font-size: 12px; color: var(--text-soft); margin: 0 0 8px;
}
[data-ai-quota-hint].is-low {
  color: #92400e; background: #fef3c7;
  border-radius: 8px; padding: 4px 9px; font-weight: 600;
}
/* 已提交后答案仍可改的提示：告诉用户"改完能重批"，否则可编辑的输入框
   看起来像 bug（明明提交过了怎么还能改）。 */
.ans-edit-tip {
  font-size: 12px; color: var(--text-soft); margin: 6px 0 0; line-height: 1.5;
}
/* 会员列表「剩余 AI」列 / 详情弹窗里的额度单元格。
   数字旁一律带文字（"次" / "不限次"），不靠颜色单通道传达状态。 */
.ai-cell { white-space: nowrap; }
.ai-left { font-weight: 700; color: var(--text); }
.ai-left.is-low { color: #92400e; }
.ai-unlimited { font-weight: 700; color: var(--primary-700); }

/* ============ 练习板块设置（级别 / 题量 chips）============
   原来是一个 <select> 里塞 4 个平铺选项，用户看不出该选哪个；
   现在改成 chips 并默认「全部」级别（零成本：后端 level 不传即不筛）。 */
.level-row { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
.level-label {
  flex: none; width: 62px; font-size: 13px; font-weight: 600;
  color: var(--text-soft);
}
.level-chips { flex: 1; display: flex; gap: 6px; flex-wrap: wrap; }
.level-chips .chip {
  cursor: pointer; padding: 7px 13px; font-size: 13px; border-radius: 999px;
  border: 1px solid var(--border); background: #fff; color: var(--text-soft);
  min-height: 32px; transition: border-color .15s, color .15s, background .15s;
}
.level-chips .chip.active {
  border-color: var(--primary-500); color: var(--primary-700);
  background: var(--primary-light); font-weight: 700;
}
.block-hint { font-size: 13px; color: var(--text-soft); margin: 0 0 4px; line-height: 1.6; }

/* 赞助锁定的导出卡：入口**可见但置灰**，不隐藏。
   隐藏等于"藏功能"，用户找不到入口时只会觉得平台小气；
   置灰 + 说清后果（用户自己判断值不值）才是诚实做法。 */
.export-entry.is-locked {
  opacity: .82; border-left: 3px solid var(--border);
}
.export-entry.is-locked .ee-head h3 { color: var(--text-soft); }

/* ============ 打印排版（仅 .print-area 生效）============
   导出卷子的排版规则：
   1) 题目与答案**分开两段**，答案区强制分页 —— 打印试卷时可删掉答案页；
   2) 单题 break-inside:avoid —— 避免一道题被切成两半跨页；
   3) 正文用衬线字体，打印观感比无衬线好。 */
.pe-h2 {
  text-align: center; color: #2563eb;
  font-size: 20px; margin: 0 0 6px;
}
.pe-sub {
  text-align: center; color: #6b7280;
  font-size: 13px; margin: 0 0 16px;
  padding-bottom: 12px; border-bottom: 1px solid #e5e9f2;
}
.pe-q {
  border-bottom: 1px solid #eef1f7;
  padding: 12px 0;
  break-inside: avoid;
  page-break-inside: avoid;
}
.pe-qh { font-size: 12.5px; color: #6b7280; margin-bottom: 6px; }
.pe-passage, .pe-body { margin: 6px 0; line-height: 1.75; }
.pe-opts { margin: 6px 0 0 1.2em; line-height: 1.9; }
/* 答案区：强制另起一页，这是"能当试卷用"的关键 */
.pe-answers {
  break-before: page;
  page-break-before: always;
  padding-top: 8px;
}
.pe-answers h3 { font-size: 16px; margin: 0 0 4px; }
.pe-answers .pe-sub { border-bottom: 1px solid #e5e9f2; }
.pe-ans-list { padding-left: 1.6em; }
.pe-ans-list li {
  margin-bottom: 12px;
  line-height: 1.7;
  break-inside: avoid;
  page-break-inside: avoid;
}
.pe-an { color: #4b5563; font-size: 13.5px; margin-top: 4px; }
/* 导出/打印的选择题选项：文字标记（你当时选·错误 / 正确答案）确保灰度
   打印与色弱用户也能读；颜色仅作辅助通道，不单独承担信息传达。 */
.pe-opt { margin: 2px 0; line-height: 1.9; }
.pe-opt-correct { font-weight: 600; }
.pe-opt-correct .pe-mark { color: #2f9e78; }
.pe-opt-wrong .pe-mark { color: #d95757; }
.pe-mark { font-style: normal; font-size: 12px; margin-left: 6px; }

/* 窄屏适配。
   用户名（.nav-me）**不能藏** —— 它是个人资料的唯一入口，
   藏了就等于窄屏用户进不去设置页。只限制宽度让它可省略。 */
@media (max-width: 600px) {
  .nav-inner { gap: 8px; padding: 0 12px; }
  .brand-text { display: none; }
  .brand-mark { font-size: 13px; padding: 5px 8px; }
  /* 品牌区也要能收缩，否则固定宽度会挤掉导航项的可用空间 */
  .brand { flex: none; }
  /* 底部 14px：与 .site-footer 的 margin-top:12px + 页脚内上边距 12px
     叠加出「内容 → 分隔线 26px → 文字 12px」。三项是同向叠加关系，
     改任何一项都要把另两项算进来（第 26 轮用户反馈原来三层共 68px 过量）。 */
  .app { padding: 18px 14px 14px; }
  .page-title { font-size: 21px; }
  /* 用户名不再截断，只收一点内边距。
     根因：窄屏规则曾给 .nav-me 加 max-width:84px + overflow:hidden +
     text-overflow:ellipsis，结果「admin（管理员）」被压成「admin...」。
     省略号在这里是有害的 —— 用户名是**身份信息**，"我是谁"被截成前缀就等于丢了，
     而且它是个人资料的入口，用户认不出自己点的是哪个。
     正确做法：横向不够就滚动。.nav-links 本身就是 overflow-x:auto 的滚动容器，
     多出来的宽度交给它，而不是把某一项压扁 —— 压扁会让最后一项
     挤到"退出"按钮底下，看着像被盖住；滚动则每一项都是完整可点的。 */
  .nav-me { margin-left: 6px; padding: 8px 10px; }
  .nav-me::before { display: none; }
  /* 导航项字号略收，一屏能多放一两项 */
  .nav-links a { padding: 8px 11px; font-size: 13.5px; }
}

/* 更窄的屏（iPhone SE 一类）：连品牌标记都让位给导航项。
   首字母品牌只是装饰，藏掉不影响任何功能。
   这里原先还有一条 .nav-me { max-width: 68px }，已随上一处的省略号一并删除 ——
   390px 与 360px 同属这一档，两个断点各给一个更窄的上限，
   用户在不同机型上会看到同一个用户名被截成不同长度，纯属自相矛盾。 */
@media (max-width: 420px) {
  .nav-inner { padding: 0 10px; gap: 6px; }
  .brand-mark { display: none; }
  .nav-user .btn-sm { padding: 6px 10px; font-size: 13px; }
}

/* 宽屏（≥1300px）：页头这一行放宽到 1340px，让 11 个导航项**一次性全部显示**。
   用户 2026-10-15 报「电脑端左侧菜单不能全部展示」。实测根因有两层：
     ① 导航项固有宽度合计 958px（其中「admin（管理员）」一项就 138px），
        而页头 max-width 只有 1120px —— 扣掉品牌 219 + 退出按钮 83 + 两处
        14px 间距后，导航容器只剩 853px，**差 105px**。这个缺口靠压
        导航项内边距补不平（全压到 11px 还差几像素），所以必须给它更多横向空间。
     ② 切页时的"active 项居中"算法用错了坐标系（见 app.js renderNav 注释），
        宽屏下把导航整体往右滚，**最左的「首页」被滚没了** —— 那才是用户
        在 1920 宽屏上看到"左边两项不见了"的直接原因。
   主体内容仍是 1120px 不动（卡片宽度不变），只放宽页头这一行；
   内边距收到 12px 是为在 1300px 视口下也留出余量（此时容器还吃不满 1340）。 */
@media (min-width: 1300px) {
  .topnav .nav-inner { max-width: 1340px; }
  .nav-links a { padding: 8px 12px; }
}

/* ============ 僻义中心：分页 ============ */
.quirk-count {
  font-size: 13px;
  color: var(--text-soft);
  margin: 2px 0 12px;
}
.quirk-count b { color: var(--primary-600); font-size: 15px; }

.load-more-wrap {
  display: flex;
  justify-content: center;
  padding: 20px 0 8px;
}
.load-more-wrap .btn { min-width: 220px; }

/* =========================================================
   AI 深度批改（写作 / 翻译 / 口语主观题）
   ========================================================= */
.ai-grade-zone { margin-top: 16px; }

/* 成绩页主观题 AI 批注：原生 <details> 折叠卡片，键盘可达 */
.ai-result-zone { margin-top: 12px; border-top: 1px dashed var(--border); padding-top: 10px; }
.ai-result-zone > summary {
  cursor: pointer; list-style: none;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  font-size: 14px; font-weight: 700; color: var(--primary-700);
  padding: 4px 2px; border-radius: 8px;
}
.ai-result-zone > summary::-webkit-details-marker { display: none; }
.ai-result-zone > summary::before {
  content: '▸'; display: inline-block; font-size: 12px;
  transition: transform .2s;
}
.ai-result-zone[open] > summary::before { transform: rotate(90deg); }
.ai-result-zone > summary:focus-visible { outline: 3px solid var(--primary-400); outline-offset: 2px; }
.ai-result-hint { font-size: 12px; font-weight: 500; color: var(--text-mute); }
/* 限流是预期降级：用中性信息条，不用红色错误条 */
.ai-limited {
  display: flex; gap: 10px; align-items: flex-start;
  background: var(--primary-light); border: 1px solid var(--primary-100);
  border-radius: var(--radius-sm); padding: 12px 14px; font-size: 13.5px; color: var(--text);
}
.ai-limited p { margin: 4px 0 0; color: var(--text-soft); }
.ai-retry {
  font-size: 13.5px; color: var(--warn-ink);
  background: var(--warn-bg); border: 1px solid #fde68a;
  border-radius: var(--radius-sm); padding: 10px 12px;
}

.ai-grade-bar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.ai-grade-btn {
  background: linear-gradient(135deg, #4a5aa8 0%, #6473bd 100%);
  box-shadow: 0 8px 20px rgba(139,92,246,.32);
}
.ai-grade-btn.is-loading { opacity: .7; transform: none; }
/* 「重新批改」是次要动作：白底描边，与实心渐变的「AI 智能批改」分出主次。
   ⚠️ 必须写在 .ai-grade-btn 之后并加 .btn-outline 提高权重 ——
   否则 .ai-grade-btn 的渐变背景会把它也染成实心，两个按钮长得一模一样。 */
.ai-grade-bar .btn-outline.ai-grade-btn {
  background: #fff; color: var(--primary-700); box-shadow: none;
  border: 1px solid var(--primary-500); white-space: nowrap;
}
.ai-grade-bar .ai-grade-btn { white-space: nowrap; }
.ai-grade-note { font-size: 12.5px; color: var(--text-mute); }
.ai-grade-result:empty { display: none; }

.ai-grade-card {
  margin-top: 14px; padding: 18px;
  border-radius: var(--radius); border: 1px solid var(--primary-100);
  background: linear-gradient(160deg, #ffffff 0%, #f7f5ff 100%);
  box-shadow: var(--shadow);
}
.ai-grade-card.is-heuristic {
  border-color: #fde68a;
  background: linear-gradient(160deg, #ffffff 0%, #fbf5ec 100%);
}

/* 分数头部 */
.ai-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.ai-head-left { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.ai-engine {
  font-size: 13px; font-weight: 700; padding: 4px 12px;
  border-radius: var(--radius-pill); white-space: nowrap;
}
.ai-engine.ai { background: var(--grad); color: #fff; box-shadow: 0 4px 12px rgba(99,102,241,.3); }
.ai-engine.rule { background: var(--accent-soft); color: #8a5a18; border: 1px solid #e2c79b; }
.ai-score { font-size: 26px; font-weight: 800; color: var(--primary-700); line-height: 1.1; }
.ai-score i { font-style: normal; font-size: 14px; font-weight: 600; color: var(--text-mute); margin-left: 4px; }
.ai-pct { font-size: 13px; font-weight: 700; color: var(--primary-600); }
.ai-bar { height: 10px; background: #e8ebf4; border-radius: var(--radius-pill); overflow: hidden; margin: 12px 0 2px; }
.ai-bar-fill { height: 100%; border-radius: var(--radius-pill); background: var(--grad); transition: width .5s; }
.ai-grade-card.is-heuristic .ai-bar-fill { background: linear-gradient(135deg, #c98a3c 0%, #d9a95f 100%); }

/* 降级提示条 */
.ai-tip {
  display: flex; gap: 8px; align-items: flex-start; margin-top: 12px;
  background: var(--accent-soft); border: 1px solid #fde68a; color: #92400e;
  border-radius: var(--radius-sm); padding: 10px 13px; font-size: 13px; line-height: 1.6;
}

/* 区块 */
.ai-sec { margin-top: 16px; }
.ai-sec-title {
  display: flex; align-items: center; gap: 8px; margin-bottom: 10px;
  font-size: 14px; font-weight: 800; color: var(--primary-700);
}
.ai-copy {
  margin-left: auto; border: 1.5px solid var(--border); background: #fff;
  color: var(--primary-600); font-size: 12px; font-weight: 700; font-family: inherit;
  padding: 4px 12px; border-radius: var(--radius-pill); cursor: pointer;
  transition: transform .15s, background .15s;
}
.ai-copy:hover { background: var(--primary-light); transform: translateY(-1px); }

/* 分项评分 */
.ai-dim { padding: 10px 0; border-bottom: 1px dashed var(--border); }
.ai-dim:last-child { border-bottom: none; padding-bottom: 0; }
.ai-dim-top { display: flex; justify-content: space-between; align-items: center; gap: 10px; font-size: 13.5px; }
.ai-dim-name { font-weight: 700; color: var(--text); }
.ai-dim-score { font-weight: 700; color: var(--primary-600); font-variant-numeric: tabular-nums; white-space: nowrap; }
.ai-dim-bar { height: 8px; background: #eef1f8; border-radius: var(--radius-pill); overflow: hidden; margin: 7px 0 4px; }
.ai-dim-fill {
  height: 100%; border-radius: var(--radius-pill);
  background: linear-gradient(90deg, #4a5aa8 0%, #6473bd 100%);
  transition: width .5s;
}
.ai-dim-cmt { font-size: 13px; color: var(--text-soft); line-height: 1.6; }

/* 亮点 */
.ai-list { margin: 0; padding-left: 20px; font-size: 13.5px; color: #374151; }
.ai-list li { margin: 5px 0; }
.ai-list.hl li::marker { color: var(--ok); }

/* 问题清单 */
.ai-issue {
  background: #fff; border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 12px 14px; margin-bottom: 10px; box-shadow: var(--shadow-sm);
}
.ai-issue:last-child { margin-bottom: 0; }
.ai-issue-orig {
  font-size: 13.5px; color: var(--err); word-break: break-word;
  text-decoration: line-through; text-decoration-color: rgba(239,68,68,.55);
}
.ai-issue-arrow { font-size: 12px; color: var(--text-mute); margin: 5px 0; }
.ai-issue-sug {
  font-size: 13.5px; font-weight: 700; color: #047857; background: var(--ok-bg);
  border-radius: 8px; padding: 6px 10px; word-break: break-word; line-height: 1.6;
}
.ai-issue-reason { font-size: 13px; color: var(--text-soft); margin-top: 6px; line-height: 1.6; }

/* 总体评语 */
.ai-comment {
  background: #fff; border: 1px solid var(--border); border-left: 4px solid var(--primary-500);
  border-radius: 10px; padding: 12px 14px; font-size: 14px; color: #374151;
  white-space: pre-wrap; line-height: 1.75;
}

/* 润色范文 */
.ai-polished {
  margin: 0; background: var(--primary-light); border: 1px dashed var(--primary-400);
  border-radius: var(--radius-sm); padding: 14px 16px; font-family: inherit;
  font-size: 14px; line-height: 1.85; white-space: pre-wrap; word-break: break-word;
  max-height: 340px; overflow: auto;
}

@media (max-width: 600px) {
  .ai-grade-card { padding: 15px; }
  .ai-grade-btn { width: 100%; }
  /* 批改区出现第二个按钮后要**平分一行**（用户 2026-10-14 要求「一行排列」），
     而上面的 width:100% 会把它们各占一行、堆成两排。
     只剩一个按钮时仍保持满宽 —— 单一主操作在手机上就该是大按钮。 */
  .ai-grade-bar .ai-grade-btn { width: auto; flex: 1 1 0; min-width: 0; padding-left: 10px; padding-right: 10px; }
  .ai-grade-note { width: 100%; }
  .ai-score { font-size: 22px; }
  .ai-polished { max-height: 260px; }
}
@media (max-width: 576px) {
  .ai-result-zone > summary { font-size: 13.5px; }
  .ai-result-hint { width: 100%; }
  .ai-limited { flex-direction: column; gap: 6px; }
}

/* ============================================================
   阶段 3：词汇打卡（单词库 / 生词本 / 打卡）
   配色沿用 :root 的温和低饱和 token，不引入新色相。
   ============================================================ */
/* 同 .quirk-head 的两个问题：
   1) 图标要与标题顶部对齐，而不是各自垂直居中
      （两行文字对单行图标做居中，图标会浮在中间，像另起了一行）
   2) 不允许 wrap —— 文字块按内容宽度撑开会差几像素触发换行，
      把图标甩到上一行。这里给文字块 min-width:0 让它可收缩。 */
/* 头部紧凑化（用户反馈圈出的区域占地太多，390px 下原约 220px）：
   标题区降一档字号、副标题 12px 单行、外边距 14→10。
   副标题保留——它承担「三档自评机制」的页面内说明，删掉后用户
   不知道「不认识会自动进生词本」，但那句话在封面亮点里也出现过，
   哪天要再省 20px，这里是第一个该动的。 */
.vocab-head {
  display: flex; gap: 12px; align-items: flex-start;
  flex-wrap: nowrap; margin-bottom: 10px;
}
.vocab-head .vh-icon { font-size: 22px; line-height: 1; flex: none; }
.vocab-head > div:last-child { min-width: 0; flex: 1 1 auto; }
.vocab-head h2 { margin: 0; font-size: 17px; font-weight: 800; color: var(--primary-700); }
.vocab-head p { margin: 2px 0 0; color: var(--text-soft); font-size: 12px; }

/* 统计条：紧凑版（替代原 6 个 .vstat 方块）。
   原方案在 390px 宽下是 2×3 网格，约 220px 高，占掉近四分之一屏。
   现在压成两行：进度条一行（主信息）+ 三档小标签一行（次信息）。 */
/* 词汇打卡：主导航 + 级别筛选合并行（renderVocab 里两组合并到 #vocabTabs）。
   .chip 是全站通用类（padding 8px 15px / 13px 字 ≈ 35px 高），
   直接复用两行会占掉近六分之一屏；这里只覆盖尺寸，颜色/圆角/交互
   仍与全站 chips 保持一支，避免"同一颗按钮在两个页面不一样大"。
   层级：主导航 6px 11px，级别筛选同档但更短（全部/四级/六级）——
   为在 390px 单行放下，主导航去掉了 emoji 图标（见 renderVocab 注释）。
   竖线 .vtabs-sep 分隔两组；折行只发生在 320px 极小屏，
   此时竖线会孤零零留在行首，所以 ≤374px 隐藏，靠间距分组。 */
.vtabs { margin-bottom: 8px; gap: 6px; align-items: center; }
.vtabs .chip { padding: 6px 11px; font-size: 12px; }
.vtabs .chip[data-level] { padding: 5px 10px; font-size: 12px; border-width: 1px; }
.vtabs-sep { flex: none; width: 1px; height: 16px; background: var(--border); margin: 0 3px; }
@media (max-width: 374px) {
  .vtabs-sep { display: none; }
}

.vstat-row { margin-bottom: 10px; }
.vq-bar-wrap { display: flex; align-items: center; gap: 10px; }
.vq-bar {
  flex: 1; height: 9px; border-radius: var(--radius-pill);
  background: var(--primary-100); overflow: hidden; display: flex;
}
.vq-seg { display: block; height: 100%; }
/* 三档之间留 1px 白缝：颜色相近时（尤其色弱用户）靠边界也能数清段数 */
.vq-seg + .vq-seg { border-left: 1px solid #fff; }
.vq-seg.ok { background: var(--ok-ink); }
.vq-seg.warn { background: var(--warn-ink); }
.vq-seg.err { background: var(--err-ink); }
.vq-main { font-size: 13px; color: var(--text-soft); white-space: nowrap; }
.vq-main b, .vq-pct { font-variant-numeric: tabular-nums; }
.vq-learned {
  border: 0; background: none; padding: 0; cursor: pointer;
  font-size: 15px; font-weight: 800; color: var(--primary-700);
  font-variant-numeric: tabular-nums;
}
.vq-learned:hover { text-decoration: underline; }
.vq-pct {
  margin-left: 6px; padding: 1px 6px; border-radius: var(--radius-pill);
  background: var(--primary-light); color: var(--primary-700); font-size: 11px; font-weight: 700;
}
/* 自评定级行：认识 / 模糊 / 不认识 + 打卡按钮 + 连续打卡火焰，必须在**一行**里。
   上一版靠 .vq-gap 把火焰推到行尾、打卡放在最后，390px 下宽度超了，
   flex-wrap 把打卡折到第二行（用户截图反馈"打卡后变成 2 行"）。
   现在三层保障：
   ① 顺序改为 三档 → 打卡 → 火焰（用户指定的动线）；
   ② flex-wrap: nowrap —— 结构上就不可能折行；
   ③ 实测收紧内边距与间距（间距 8→4px、chip 内边距 9→7px），
      360px 下也放得下（实测：收紧前 360px 溢出 10px）。
   最极端的情况（三档都到四位数 + 连续一年）内容约 350px，
   手机宽度放不下——此时行内**横向滑动**而不是折行，永远保持一行；
   上下留 4px padding，保证按钮的焦点环不被滚动容器裁掉（无障碍）。 */
.vq-row {
  display: flex; align-items: center; gap: 3px; flex-wrap: nowrap;
  padding: 4px 0; margin-top: 2px;
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none; -ms-overflow-style: none;
}
.vq-row::-webkit-scrollbar { display: none; }
.vq {
  display: inline-flex; align-items: center; gap: 3px;
  border: 1px solid var(--border); background: #fff; cursor: pointer;
  border-radius: var(--radius-pill); padding: 3px 7px;
  font-size: 12px; color: var(--text-soft); white-space: nowrap;
}
.vq:hover { border-color: var(--primary-400); }
.vq b { color: var(--text); font-variant-numeric: tabular-nums; }
.vq-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
.vq-dot.ok { background: var(--ok-ink); }
.vq-dot.warn { background: var(--warn-ink); }
.vq-dot.err { background: var(--err-ink); }
.vq-streak {
  flex: none; white-space: nowrap;
  font-size: 12px; color: var(--accent); font-weight: 700;
  background: var(--accent-soft); border-radius: var(--radius-pill); padding: 3px 7px;
}
/* 打卡入口：紧跟「不认识」之后（用户 2026-10-14 截图圈定位置）。
   它是这一行里唯一的"动作"，三档 chip 是"查看"、火焰是"状态"，
   用 primary 淡底实心按钮把三者分清；点击**当场打卡**（不是跳转）。
   与 .vq-pct 同一套 primary-light/primary-700 配色，对比度全站已校验。 */
.vq-checkin {
  flex: none; white-space: nowrap; border: 1px solid var(--primary-100);
  background: var(--primary-light); color: var(--primary-700);
  cursor: pointer; border-radius: var(--radius-pill);
  padding: 3px 9px; font-size: 12px; font-weight: 700; font-family: inherit;
}
.vq-checkin:hover { background: var(--primary-100); }
.vq-checkin:focus-visible { outline: 2px solid var(--primary-600); outline-offset: 2px; }
/* 今日已打卡：换成功色，语义从"去打卡"变为"已完成"。
   不只靠颜色区分——文案本身就变成了「已打卡」，色弱用户也读得懂。 */
.vq-checkin.done {
  background: var(--ok-bg); border-color: var(--ok-bg); color: var(--ok-ink);
}
.vq-checkin.done:hover { background: #e0f0e9; }
/* 小屏（≤374px，覆盖 360 / 375 等常见手机宽度）：整行再收紧一档
   （字号 11px、内边距 2px 5px、间距 2px），并把火焰的"天"字隐藏——
   「🔥 30」配上 title="累计打卡 N 天"语义不减，但省下的余量正好让
   "学了 800 / 200 / 300、连续 30 天"这类几周后的真实重度数据在 360px
   也能整行放得下、不触发行内横向滚动。320px 一并受益。 */
@media (max-width: 374px) {
  .vq-row { gap: 2px; }
  .vq { padding: 2px 5px; font-size: 11px; }
  .vq-streak { padding: 2px 5px; font-size: 11px; }
  .vq-checkin { padding: 2px 6px; font-size: 11px; }
  .vq-streak-d { display: none; }
}
/* 取词依据提示：告诉用户这批词为什么同根同尾，否则"怎么老是一串相似词"
   会被当成 bug。 */
.vhint {
  font-size: 12px; color: var(--text-soft); margin-bottom: 10px;
  background: var(--primary-light); border-radius: 10px; padding: 7px 10px;
}
.vhint b { color: var(--primary-700); }
/* 放在内容末尾的取词说明（.vhint-foot）。
   .vhint 默认带 margin-bottom:10px，那是为"夹在两块内容之间"写的；
   到了页尾它下面已经没有内容了，这 10px 只会让整块往上飘、
   与上方卡片贴得过近。所以这里把外边距整体对调成 margin-top。 */
.vhint-foot { margin: 18px 0 0; }

/* 学习卡 */
.vprog { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.vp-bar { flex: 1; height: 6px; border-radius: var(--radius-pill); background: var(--primary-100); overflow: hidden; }
.vp-bar > i { display: block; height: 100%; width: 0; background: var(--grad); border-radius: var(--radius-pill); transition: width .28s ease; }
.vp-text { font-size: 13px; color: var(--text-soft); font-variant-numeric: tabular-nums; }

/* 学习卡。
   padding 28px→20px、词 30px→26px：不是"为了塞下更多"，而是——
   点开「显示释义」后，卡片会多出 释义 + 三档按钮 + 例句 两块内容，
   原来那圈 padding 与大字号会把它们顶到屏外，用户必须滑屏才看得到。
   收紧的是**空白与字号的冗余量**，不是信息量。 */
.vcard { text-align: center; padding: 20px 16px; }
.vc-top { display: flex; flex-direction: column; gap: 3px; align-items: center; }
.vc-word { font-size: 26px; font-weight: 800; color: var(--text); letter-spacing: .5px; }
.vc-ph { font-size: 13px; color: var(--text-mute); font-family: ui-monospace, "SF Mono", Consolas, monospace; }
.vc-pos { margin-top: 6px; font-size: 12px; color: var(--text-mute); }
.vc-meaning { margin-top: 12px; font-size: 17px; font-weight: 700; color: var(--primary-700); }
/* 揭示动效：只动 transform/opacity，200-280ms，不做花哨过渡 */
.vc-meaning[hidden] { display: none; }
.vc-meaning { animation: vcFade .24s ease; }
@keyframes vcFade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

#vPeek { margin-top: 14px; }

/* ============ 单词朗读喇叭 ============
   全站只有**两档**尺寸，都在这里定，别处只准选档、不准写死数值。
   写死的后果是真实发生过的：同一颗喇叭在六个场景里是
   22 / 26 / 28 / 30 / 32 / 38px 六种大小，用户在页面之间来回切时会
   明显感到"按钮在呼吸"，而这种差异没有任何功能上的理由。

   档位取自熟词僻义浏览模式 —— 那里的喇叭是**嵌在文字流里**的
   （紧跟在单词/句子后面），视觉上最不打扰阅读，因此以它为基准：

     sm=26px 贴在单词后：单词本身已经是大字（28~30px），
                 喇叭再大就头重脚轻，像给标题配了个大按钮。
     md=22px 贴在整句/段落后：句子是长文本，喇叭必须更轻，
                 且平时不显形（透明），只在hover 时浮出。

   底边距用 vertical-align 而不是父容器的 flex 对齐：
   浏览模式的喇叭就在文字中间，改成 inline 才能和它视觉一致；
   flex 容器（align-items:center）会把喇叭按整行高度居中，
   在 22px 高的行里会明显偏低。 */
.vc-speak {
  display: inline-flex; align-items: center; justify-content: center;
  flex: none;
  /* sm 档（默认）。md 档在下面按场景覆盖这几个变量。 */
  --spk-box: 26px;
  --spk-icon: 15px;
  --spk-valign: 6%;
  --spk-gap: 8px;
  width: var(--spk-box); height: var(--spk-box); padding: 0;
  border: 1.2px solid var(--border); border-radius: 50%;
  background: #fff; color: var(--primary-600);
  vertical-align: var(--spk-valign);
  margin-left: var(--spk-gap);
  cursor: pointer;
  transition: background-color .2s ease, transform .2s ease, border-color .2s ease;
}
.vc-speak:hover { background: var(--primary-light); border-color: var(--primary-500); transform: scale(1.06); }
.vc-speak:active { transform: scale(.94); }
.vc-speak:focus-visible { outline: 2px solid var(--primary-600); outline-offset: 2px; }
.vc-speak svg { width: var(--spk-icon); height: var(--spk-icon); fill: currentColor; }

/* md 档：整句 / 段落后的喇叭。
   这些容器**全部**是"文字里插一个喇叭"的结构（例句、材料、题干、
   passage），和浏览模式同构，所以统一在这里覆盖变量，不再逐个场景写尺寸。
   平时透明是为了让长文本区域保持干净 —— 一段材料旁边杵着五个实心圆按钮，
   视觉噪声比功能本身还大。 */
.quiz-example .vc-speak,
.vc-en-line .vc-speak,
.q-passage .vc-speak,
.q-content .vc-speak,
.sp-q-t .vc-speak,
/* .sp-q 与 .sp-q-t 是同一套样式的两个类名：
   .sp-q-t 用于口试单题页，.sp-q 用于模考内的口语题。
   两处都是「题干文字里插一个喇叭」，所以必须一起进 md 档 ——
   漏掉 .sp-q 会让模考里的喇叭退回 sm 档(26px)，
   贴在 14.5px 的题干旁边显得又大又重，和口试页不一致。 */
.sp-q .vc-speak,
.sp-material .vc-speak,
.quirk-example .vc-speak {
  --spk-box: 22px;
  --spk-valign: middle;
  --spk-gap: 6px;
  border-color: transparent;
  background: transparent;
}
/* md 档的 hover / 朗读中：平时隐去，交互时才显形。
   这条必须单独写，不能靠 .vc-speak:hover —— 上面把 background/border
   设成 transparent 的选择器权重更高（多了一个类），
   通用 hover 规则会被它盖掉，结果就是"hover 也没反应"。 */
.quiz-example .vc-speak:hover,
.vc-en-line .vc-speak:hover,
.q-passage .vc-speak:hover,
.q-content .vc-speak:hover,
.sp-q-t .vc-speak:hover,
.sp-q .vc-speak:hover,
.sp-material .vc-speak:hover,
.quirk-example .vc-speak:hover {
  border-color: var(--border); background: var(--primary-light);
}
.quiz-example .vc-speak.is-speaking,
.vc-en-line .vc-speak.is-speaking,
.q-passage .vc-speak.is-speaking,
.q-content .vc-speak.is-speaking,
.sp-q-t .vc-speak.is-speaking,
.sp-q .vc-speak.is-speaking,
.sp-material .vc-speak.is-speaking,
.quirk-example .vc-speak.is-speaking {
  border-color: var(--primary-600); background: var(--primary-600);
}

/* 文字 + 喇叭同行的容器。
   原来是 flex 两列布局（文字占满、喇叭贴右端），现在统一改成
   **普通块级 + 内联喇叭**，与浏览模式一致。
   改的原因不是审美，而是 flex 在这里解决了一个不存在的问题：
   长段落本来就会自然换行，flex 的"左文字右喇叭"两列反而会让喇叭
   固定在首行右上、与后续文字行不对齐；而且 align-items:center
   会把喇叭按整块高度居中，在多行文字里出现明显偏移。
   inline 布局下喇叭跟着文字走，行高、基线都自然对齐。
   align-items 换成 vertical-align 之后，
   .vc-speak 上的 --spk-valign 才能真正起作用。 */
.q-wordline {
  display: block;
}
.q-wordline .q-type-badge { vertical-align: middle; }

/* 正在朗读：喇叭轻微脉动，给出「点了生效」的即时反馈。
   纯 transform 动画（GPU 友好），且尊重 prefers-reduced-motion。 */
.vc-speak.is-speaking { background: var(--primary-600); border-color: var(--primary-600); color: #fff; }
.vc-speak.is-speaking svg { animation: spPulse .9s ease-in-out infinite; }
@keyframes spPulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.14); }
}
@media (prefers-reduced-motion: reduce) {
  .vc-speak.is-speaking svg { animation: none; }
  .vc-speak:hover, .vc-speak:active { transform: none; }
}


/* 词 + 喇叭同行：喇叭贴词右侧，与词在同一视线焦点内。
   从 flex 改成普通块级 + 内联喇叭：喇叭紧跟在词之后，
   和浏览模式（.quirk-word）结构一致，尺寸也直接走 .vc-speak 的
   sm 档默认值，不再单独写 32px —— 这个 32px 曾是六种尺寸之一。 */
.vc-wordline { display: block; }
/* .vc-word 必须是 inline —— 它的父容器已经不再是 flex，
   而它本身是 <div>（块级），若保持块级就会占满整行宽度、
   把喇叭挤到词的下一行。这正是原来这个容器要用 flex 的原因：
   用 flex 容器"包住"块级子元素来强行并排。
   改成 inline 布局后，正确做法是让词自己变inline，
   词和喇叭才能真正在同一行里自然流动 —— 与浏览模式一致。 */
.vc-word { display: inline; }

/* 熟词僻义页的喇叭 —— 全站喇叭的**基准**，上面 sm/md 两档就是照它定的。
   这里只补一条：.quirk-word 是一整块标题行，词本身就很大，
   如果喇叭固定 34px，贴在 22px 的大词旁边会显得头重脚轻。
   而 .quirk-word 默认是 block，inline 元素会另起一行 —— 那样喇叭
   会被挤到词的下一行，正是「看起来像没加上」的原因。
   所以靠 inline-flex + vertical-align 让它贴在词后。 */
.quirk-word .vc-speak {
  --spk-box: 26px;
  --spk-icon: 15px;
  --spk-valign: 6%;
  --spk-gap: 8px;
}
.quirk-word .vc-speak svg { width: var(--spk-icon); height: var(--spk-icon); }

/* 例句行：喇叭跟在整句后面（同 md 档，尺寸在上面统一定过了）。
   同样从 flex 改成块级 —— 句子会自然换行，flex 两列反而让喇叭
   和第二行文字对不齐。 */
.vc-en-line { display: block; }
/* 同 .vc-word：原为块级 div，靠 flex 父容器强行并排；
   改成 inline 布局后必须让它变inline，否则句子与喇叭会分两行。 */
.vc-en-line .vc-en { display: inline; }

/* 生词本里的喇叭：不与「标记掌握」按钮抢视线。
   sm 档（26px）与浏览模式一致，原来这里是 26px 但 svg 只有 14px、
   用负 vertical-align 往上顶 —— 负对齐在多行文字里会把喇叭
   顶进上一行的行间空隙，改为与基准一致的 6% 正对齐。 */
.vf-w .vc-speak-fav {
  --spk-box: 26px;
  --spk-icon: 15px;
  --spk-valign: 6%;
  --spk-gap: 8px;
}
.vf-w .vc-speak-fav svg { width: var(--spk-icon); height: var(--spk-icon); }
.vc-rating { margin-top: 12px; display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.vc-rating[hidden] { display: none; }
.vr { min-width: 96px; }
.vr-ok { background: var(--ok); border-color: transparent; color: #fff; }
.vr-warn { background: var(--warn); border-color: transparent; color: #fff; }
.vr-err { background: var(--err); border-color: transparent; color: #fff; }
.vr:disabled { opacity: .5; cursor: not-allowed; transform: none; }

.vc-ex { margin-top: 14px; padding-top: 12px; border-top: 1px dashed var(--border); text-align: left; }
.vc-ex[hidden] { display: none; }
.vc-en { font-size: 14px; color: var(--text); line-height: 1.6; }
.vc-cn { margin-top: 6px; font-size: 13px; color: var(--text-soft); line-height: 1.6; }

/* 生词本 */
.vf-count { font-size: 13px; color: var(--text-soft); }
.vf-list { display: grid; gap: 10px; }
.vf-item { display: flex; gap: 14px; align-items: center; justify-content: space-between; padding: 14px 16px; box-shadow: var(--shadow-sm); }
.vf-l { min-width: 0; flex: 1; }
.vf-w { font-size: 17px; font-weight: 700; color: var(--text); }
.vf-ph { margin-left: 8px; font-size: 12px; font-weight: 400; color: var(--text-mute); font-family: ui-monospace, Consolas, monospace; }
.vf-m { margin-top: 3px; font-size: 14px; color: var(--primary-700); }
.vf-e { margin-top: 5px; font-size: 12px; color: var(--text-mute); line-height: 1.5; }
.vf-r { display: flex; gap: 8px; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }

/* 打卡 */
.vci { padding: 20px; }
.vci-top { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.vci-streak { font-size: 32px; font-weight: 800; color: var(--accent); line-height: 1.1; }
.vci-streak > span { margin-left: 6px; font-size: 13px; font-weight: 400; color: var(--text-soft); }
.vci-sub { margin-top: 4px; font-size: 13px; color: var(--text-mute); }
.vci-grid {
  margin-top: 20px;
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 8px;
}
.vci-day { display: flex; flex-direction: column; align-items: center; gap: 5px; padding: 8px 2px; border-radius: var(--radius-sm); background: var(--bg); }
/* 空心=未打卡 / 实心=已打卡：不靠颜色单独传达状态 */
.vci-day > i { width: 9px; height: 9px; border-radius: 50%; border: 1.5px solid var(--border); background: transparent; }
.vci-day.on { background: var(--accent-soft); }
.vci-day.on > i { background: var(--accent); border-color: var(--accent); }
.vci-day > span { font-size: 11px; color: var(--text-mute); font-variant-numeric: tabular-nums; }
.vci-foot { margin-top: 14px; font-size: 12px; color: var(--text-mute); }

/* 紧凑统计条不再需要按断点改列数（它已经不是网格了），
   原先的 .vstat-row grid-template-columns 三条规则已随之删除。
   992px 断点里只留下真正与列数无关的生词本/打卡卡内边距调整。 */
@media (min-width: 992px) {
  .vf-item { padding: 16px 20px; }
  .vci { padding: 26px; }
}
@media (prefers-reduced-motion: reduce) {
  .vc-meaning { animation: none; }
  .vp-bar > i { transition: none; }
}

/* ============================================================
   启动封面：先讲差异化价值，再进登录
   ============================================================ */
.cover {
  min-height: 100vh;
  display: flex; align-items: center; justify-content: center;
  /* 底部 56px 是配合"外面还有 .app 的 60px"定的，实际叠加出 116px 灰条。
     现在 .app:has(> .cover) 已把外层留白撤掉，这里给一个够用的收尾间距即可。 */
  padding: 40px 20px 32px;
  background:
    radial-gradient(1100px 520px at 12% -8%, var(--primary-light) 0%, transparent 62%),
    radial-gradient(900px 460px at 92% 4%, var(--accent-soft) 0%, transparent 58%),
    var(--bg);
}
.cover-inner { width: 100%; max-width: 1080px; text-align: center; }
.cover-badge {
  display: inline-block; padding: 6px 16px; border-radius: var(--radius-pill);
  background: var(--card); border: 1px solid var(--border);
  color: var(--primary-700); font-size: 13px; font-weight: 600;
  box-shadow: var(--shadow-sm);
}
.cover-title {
  margin: 22px 0 0; font-size: 40px; line-height: 1.32;
  font-weight: 800; color: var(--text); letter-spacing: .3px;
}
.cover-title .cv-hl {
  background: var(--grad-soft); color: var(--primary-600);
  padding: 2px 10px; border-radius: var(--radius-sm);
  box-decoration-break: clone;
}
.cover-sub { margin: 18px auto 0; max-width: 620px; font-size: 16px; line-height: 1.85; color: var(--text-soft); }
.cover-cta { margin-top: 30px; display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
.btn-lg { padding: 13px 30px; font-size: 15px; border-radius: var(--radius-sm); }

.cover-grid {
  margin-top: 48px;
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  text-align: left;
}
.cv-item {
  background: var(--card); border-radius: var(--radius);
  padding: 20px; box-shadow: var(--shadow-sm);
  /* 列式 flex + 徽标 margin-top:auto（见 .cv-tag）：
     六张卡的文案长短不一，同行卡片被 grid 拉成等高后，
     徽标一律落到卡片底部同一条基线，网格才显得齐（2026-10-15）。 */
  display: flex; flex-direction: column;
  transition: transform .22s ease, box-shadow .22s ease;
}
.cv-item:hover { transform: translateY(-3px); box-shadow: var(--shadow); }
.cv-icon { font-size: 24px; line-height: 1; }
.cv-h { margin: 10px 0 0; font-size: 16px; font-weight: 700; color: var(--text); }
.cv-d { margin: 8px 0 12px; font-size: 13.5px; line-height: 1.75; color: var(--text-soft); }
.cv-tag {
  display: inline-block; align-self: flex-start; margin-top: auto; padding: 3px 10px;
  border-radius: var(--radius-pill);
  background: var(--primary-light); color: var(--primary-700);
  font-size: 12px; font-weight: 600;
}
/* 付费/权益类徽标（题库导出）换语义色，和"免费能力"在视觉上分开。
   用 --warn-bg / --warn-ink 这一对已验证过对比度的令牌（≥6:1），不新造颜色；
   而且颜色只是辅助——"赞助支持解锁"这几个字本身就说清了性质。 */
.cv-tag.is-paid { background: var(--warn-bg); color: var(--warn-ink); }

.cover-stats {
  margin-top: 40px; display: grid; gap: 12px;
  grid-template-columns: repeat(2, 1fr);
}
.cover-stats > div {
  background: var(--card); border-radius: var(--radius);
  padding: 18px 12px; box-shadow: var(--shadow-sm);
}
.cover-stats b { display: block; font-size: 26px; font-weight: 800; color: var(--primary-700); }
.cover-stats span { display: block; margin-top: 4px; font-size: 12.5px; color: var(--text-mute); }
.cover-foot { margin-top: 32px; font-size: 12.5px; color: var(--text-mute); }

/* ---------- 全站联系页脚（登录后每个页面底部 + 封面页底部） ----------
   版式与登录/注册页的联系栏完全一致（同一个 authContactHtml）：
   两行居中文字 + 虚线圆角微信号按钮（.auth-contact / .auth-contact-hint /
   .auth-wx 见下方「登录页联系栏」一节，不再为页脚单写一套）。
   容器只负责：顶部分隔线、最大宽度、整体居中、上下留白。

   ⚠️ 间距铁律（第 26 轮用户实测反馈「卡片到页脚留白太高太空」）：
   内容底部 → 页脚文字的间距 = .app 的 padding-bottom + 本规则 margin-top
   + .site-footer-inner 的 padding-top，**三层叠加**。改小任何一层前先想清楚
   另两层是多少——早先 24 + 28 + 16 = 68px（微信里实测约 90px），
   用户明确反馈过量。现在的目标是「内容 → 分隔线 24px 左右、线 → 文字 12px」，
   即移动端总计约 36-40px，别再凭感觉各自加大。 */
.site-footer {
  margin-top: 12px;
  border-top: 1px solid var(--border);
  background: rgba(255,255,255,.6);
}
.site-footer-inner {
  max-width: 720px; margin: 0 auto; padding: 10px 18px 16px;
  text-align: center;
}
/* .auth-contact 的 14px 上边距是为**登录表单下方**的语境定的
   （要和表单拉开距离）。页脚里分隔线已经做了视觉分隔，
   再叠加 14px 就是"线下面还空一大截"——第 26 轮用户反馈的正是这个。
   在页脚语境里归零，由 .site-footer-inner 的 padding-top 统一控制。 */
.site-footer .auth-contact { margin-top: 0; }
/* 封面页底部也用它（用户要求"整个系统统一起来"）：
   封面容器自带左右 padding，页脚那套 18px 内边距在这里会缩窄内容，
   所以左右归零、只留上方一点呼吸；auth-contact 的 14px 上边距
   同样归零——封面里它上面是 cover-foot，不是表单。 */
.cover .site-footer-inner { padding: 8px 0 0; }
.cover .auth-contact { margin-top: 0; }

/* ---------- 工信部备案号常驻条：全站底部，不随登录态隐藏 ---------- */
.beian-bar {
  text-align: center;            /* 水平居中 */
  padding: 10px 0;               /* 上下对称 → 条内垂直居中 */
  font-size: 12px;
  line-height: 1.6;
  color: #999;
  background: #fafafa;
  border-top: 1px solid var(--border);
}
.beian-bar a { color: #999; text-decoration: none; }
.beian-bar a:hover { color: #666; text-decoration: underline; }

/* ---------- 升级 VIP 弹窗：微信号一键复制 ---------- */
/* 用户要求：「点开升级VIP按钮，显示联系微信智学备考 mycet46 咨询，名字带自动复制功能」。
   名字做成大号可点按钮，点一下即复制。网页侧无法直接调起微信加好友
   （微信没开放这个能力给网页），"复制后到添加朋友搜号码"是唯一可靠路径，
   所以复制动作必须做得显眼、点完立刻有反馈。 */
.vip-wx {
  margin-top: 14px; padding: 14px;
  background: var(--primary-light);
  border: 1px dashed var(--primary);
  border-radius: var(--radius-sm);
}
.vip-wx-label { font-size: 12.5px; color: var(--text-mute); font-weight: 600; }
.vip-wx-name {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; margin-top: 8px; padding: 10px 14px; cursor: pointer;
  background: var(--bg); border: 1px solid var(--primary);
  border-radius: var(--radius-pill); font: inherit; text-align: left;
  transition: transform .2s ease, box-shadow .2s ease;
}
.vip-wx-name:hover { transform: scale(1.02); box-shadow: var(--shadow-sm, 0 2px 8px rgba(0,0,0,.08)); }
.vip-wx-name:active { transform: scale(.98); }
.vip-wx-name:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.vip-wx-text { font-size: 17px; font-weight: 800; color: var(--primary-700); letter-spacing: .5px; }
.vip-wx-act { font-size: 12px; color: var(--primary); white-space: nowrap; }
.vip-wx-note { margin: 8px 0 0; font-size: 12.5px; color: var(--text-soft); line-height: 1.6; }

/* 登录/注册页里那条轻量联系方式（.auth-contact / .auth-wx）。
   封面页、登录后每个页面的页脚用的也是这一份（contactFooterHtml）：
   用户 2026-10-14 拿截图对比后要求"整个系统统一起来"，
   原来封面那张左对齐大卡片（.contact-card 系列）已整体删除。

   微信号做成 <button> 而非 <span>：span 不可聚焦、键盘用户 tab 不到，
   也点不动（只有鼠标能用），等于对一部分人隐藏了这条出口。
   外观则刻意贴近正文（同样的字号与灰色），只在悬停/聚焦时给出主色，
   做到"平时不抢眼，交互时明确"。 */
.auth-contact {
  text-align: center; color: var(--text-soft);
  font-size: 13px; margin: 14px 0 0;
}
/* 「友情提醒：点微信号自动复制」——单独一行、居中、比正文更弱。
   这行不能省：微信号的样式（虚线框 + 主色）已经像可点了，
   但对没耐心的人，"像按钮"不等于"知道点了会发生什么"。
   放在下一行做解释，比在微信号上挂 title 可靠 ——
   手机没有鼠标悬停，title 对绝大多数用户等于不存在。 */
.auth-contact-hint {
  text-align: center; color: var(--text-mute);
  font-size: 12px; margin: 6px 0 0; line-height: 1.5;
}
.auth-wx {
  display: inline-block; cursor: pointer;
  padding: 2px 8px; margin-left: 2px;
  background: transparent; color: var(--primary);
  border: 1px dashed var(--primary); border-radius: var(--radius-pill);
  font: inherit; font-size: 13px; font-weight: 600;
  transition: background .15s, color .15s;
}
.auth-wx code {
  font-size: 12.5px; background: transparent; padding: 0;
  letter-spacing: .2px;
}
.auth-wx:hover { background: var(--primary-light); }
.auth-wx:focus-visible { outline: 2px solid var(--primary-600); outline-offset: 2px; }

/* 窄屏：联系方式保持一排，靠缩小字号/间距把它塞进一行，
   而不是靠换行或滚动。用户要的是"一排"，能塞下就别让用户横向拖。 */

@media (min-width: 768px) {
  .cover-title { font-size: 52px; }
  .cover-sub { font-size: 17px; }
  .cover-stats { grid-template-columns: repeat(4, 1fr); }
}

/* ============================================================
   题库导出面板（免费开放）
   ============================================================
   这一整块原本服务于管理后台侧栏的 .export-card 面板，
   该面板已随 renderAdmin 的侧栏一并删除（与个人资料页的导出题库重复），
   因此下面所有 .export-card 规则现在都没有对应元素，已整体清掉。

   ⚠️ 清理时保留的信息：.ex-field 这个类**仍在用** ——
   笔记页的搜索框（renderNotes 的 noteQ）就是 .ex-field。
   原来给它的样式全部挂在 .export-card 之下，属于"挂错了父级"：
   笔记页那个 .ex-field 从来没拿到过这里的 flex 列布局，
   纯属同名巧合。真正的笔记搜索框样式是 .note-mgr 之外的默认表单样式，
   删除本块不影响笔记页外观。 */

/* 紧凑统计条里可下钻的小标签：原生 button，需重置按钮外观并保留键盘焦点可见。
   原先的 .vstat-act（大方块 + 右上角 › 箭头）已随 6 方块方案一起删除：
   新形态是行内小标签，箭头指示物挤在 12px 的标签里只会显得脏。 */
.vq:focus-visible, .vq-learned:focus-visible {
  outline: 2px solid var(--primary-600); outline-offset: 2px;
}
.vstat-act:hover .vs-go { color: var(--primary-600); }

/* ============================================================
   口试（CET-SET）：录音 + 转写 + AI 批注
   ============================================================ */
.sp-q-t { font-size: 14.5px; line-height: 1.75; color: var(--text); }
.sp-ref { margin-top: 12px; }
.sp-ref > summary {
  cursor: pointer; font-size: 13px; color: var(--primary-600);
  padding: 6px 0; list-style: none; user-select: none;
}
.sp-ref > summary::-webkit-details-marker { display: none; }
.sp-ref > summary::before { content: '▸ '; }
.sp-ref[open] > summary::before { content: '▾ '; }
.sp-ref-b {
  margin-top: 8px; padding: 14px; border-radius: var(--radius-sm);
  background: var(--primary-light); font-size: 13.5px; line-height: 1.8;
  color: var(--text); border-left: 3px solid var(--primary-400);
}
.sp-tip-bar {
  margin-top: 14px; padding: 12px 14px; border-radius: var(--radius-sm);
  background: var(--accent-soft); color: var(--warn-ink);
  font-size: 13px; line-height: 1.7;
}

.sp-back { margin-bottom: 14px; }
.sp-card { margin-top: 4px; }
/* 录音行整体居中：按钮 + 计时器作为一组居中排布（用户要求"行内居中"）。
   模考内的口语题（examSpeakingHtml）复用同一行样式，一并居中保持一致。 */
.sp-rec-row { display: flex; align-items: center; justify-content: center; gap: 14px; flex-wrap: wrap; }
/* 录音中：按钮转警示色 + 提示文字，不只靠颜色区分状态 */
#spRecBtn.recording { background: var(--err); border-color: transparent; color: #fff; }
.sp-timer {
  font-size: 22px; font-weight: 700; color: var(--text);
  font-variant-numeric: tabular-nums; letter-spacing: .5px;
}

/* 声波：纯装饰，录制中才动 */
.sp-wave {
  display: flex; align-items: flex-end; justify-content: center; gap: 4px;
  height: 40px; margin: 16px 0 6px;
}
.sp-wave > i {
  width: 4px; height: 8px; border-radius: 2px; background: var(--primary-100);
}
.sp-wave.on > i { background: var(--primary-500); animation: spWave 1s ease-in-out infinite; }
.sp-wave.on > i:nth-child(2) { animation-delay: .12s; }
.sp-wave.on > i:nth-child(3) { animation-delay: .24s; }
.sp-wave.on > i:nth-child(4) { animation-delay: .36s; }
.sp-wave.on > i:nth-child(5) { animation-delay: .48s; }
.sp-wave.on > i:nth-child(6) { animation-delay: .6s; }
.sp-wave.on > i:nth-child(7) { animation-delay: .72s; }
.sp-wave.on > i:nth-child(8) { animation-delay: .84s; }
.sp-wave.on > i:nth-child(9) { animation-delay: .96s; }
@keyframes spWave { 0%,100% { height: 8px; } 50% { height: 32px; } }

.sp-transcript {
  margin: 12px 0; padding: 12px 14px; border-radius: var(--radius-sm);
  background: var(--primary-light); border-left: 3px solid var(--primary-500);
}
.sp-transcript.hidden { display: none; }
.sp-tx { font-size: 14px; line-height: 1.75; color: var(--text); }
.sp-tip { margin-top: 6px; font-size: 12px; color: var(--text-mute); }

.sp-label { display: block; margin: 14px 0 6px; font-size: 13px; color: var(--text-soft); }
.sp-hint { margin-left: 8px; font-size: 12px; color: var(--text-mute); font-weight: 400; }
#spText { width: 100%; min-height: 120px; font-size: 14px; line-height: 1.75; resize: vertical; }
.sp-meta { margin: 8px 0 0; font-size: 12.5px; color: var(--text-mute); }
#spGrade:not(:empty) { margin-top: 18px; }

@media (prefers-reduced-motion: reduce) {
  .sp-wave.on > i { animation: none; height: 20px; }
}

/* 模考内的口语题：录音作答区复用口试样式，仅补题干与标签 */
.sp-q { font-size: 14.5px; line-height: 1.75; color: var(--text); }
.sp-material {
  margin: 10px 0 14px; padding: 12px 14px;
  background: var(--primary-light); border: 1px solid var(--border);
  border-radius: var(--radius); font-size: 14px; color: var(--text-soft);
}
.sp-lb {
  display: block; margin: 12px 0 6px;
  font-size: 13px; font-weight: 600; color: var(--text-soft);
}

/* 口试单题流水线：进度条 / 下一题 / 完成页 */
/* 题号 + 级别筛选同一行（用户 2026-10-14：原来 chips 单独一行"太空"）。
   左题号右 chips，space-between；320px 下题号约 90px + chips 约 110px，
   合计 200px < 288px 可用宽，不换行也不溢出。
   原 .sp-progress-textonly 整行（含被隐藏的轨道）已被本组合取代，
   但 .sp-progress* 规则保留——完成页/兜底渲染仍可能引用。 */
.sp-toprow {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin: 4px 0 10px;
}
.sp-toprow .sp-progress-text { margin-bottom: 0; }
.sp-lv { display: flex; gap: 6px; flex: none; }
.sp-lv .chip { padding: 5px 12px; font-size: 12px; }
.sp-progress { margin: 16px 0 14px; }
/* 只留题号的变体：去掉灰色轨道，标题与题目卡片之间的呼吸感由 margin 提供 */
.sp-progress-textonly { margin: 4px 0 10px; }
.sp-progress-text { font-size: 13px; color: var(--text-soft); margin-bottom: 8px; }
.sp-progress-textonly .sp-progress-text { margin-bottom: 0; }
.sp-progress-text b { color: var(--primary-700); font-size: 15px; }
.sp-progress-track {
  height: 8px; border-radius: var(--radius-pill);
  background: var(--primary-light); overflow: hidden;
}
.sp-progress-fill {
  height: 100%; border-radius: var(--radius-pill);
  background: var(--grad); transition: width .3s ease;
}
.sp-next-row { margin-top: 18px; }
.sp-done { text-align: center; padding: 40px 24px; }
.sp-done .em-emoji { font-size: 44px; }
.sp-done h3 { margin: 12px 0 6px; }
.sp-done p { color: var(--text-soft); margin-bottom: 18px; }
.sp-done .btn-row { justify-content: center; }
@media (prefers-reduced-motion: reduce) {
  .sp-progress-fill { transition: none; }
}

/* ============================================================
   用户信息体系：注册资料 / 个人资料页 / 管理后台用户编辑
   ============================================================ */
/* 表单字段单元：label + 控件 + 错误提示算**一个**语义单元。
   放进栅格时列数 == 字段数，避免 label 与自己的控件被拆到不同格子。 */
.profile-form .pf-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; margin-top: 12px; }
.profile-form .pf-field:first-child { margin-top: 0; }
.profile-form .pf-field label {
  display: block; margin: 0; font-size: 13px;
  font-weight: 600; color: var(--text-soft);
}
/* 必填星号：用 aria-hidden 隐藏，读屏由 label 文本本身承担语义 */
.pf-req { color: var(--err); margin-left: 3px; }
.profile-form .pf-field input,
.profile-form .pf-field select,
.profile-form .pf-field textarea { width: 100%; margin: 0; }
/* aria-invalid 时给红框；:focus-visible 保留焦点环，不用 outline:none */
.profile-form [aria-invalid="true"] { border-color: var(--err); background: var(--err-bg); }
.profile-form [aria-invalid="true"]:focus-visible { outline: 2px solid var(--err-ink); outline-offset: 2px; }

/* 错误提示：aria-live="polite" 的播报节点，为空时不占位 */
.form-error {
  margin: 0; min-height: 0; font-size: 12.5px; line-height: 1.5;
  color: var(--err-ink); font-weight: 600;
}
.form-error:empty { display: none; }

/* 字段下方的灰色补充说明（非错误） */
.field-hint {
  margin: 6px 0 0; font-size: 12.5px; line-height: 1.6;
  color: var(--text-mute);
}
/* 提示紧贴它所属控件（负 margin），避免看起来像下一个字段的标题。
   提示现在是 .pf-field 的内部子元素，故直接用相邻选择器即可。 */
.field-hint:empty { display: none; }
.pf-field > .field-hint { margin-top: 5px; margin-bottom: 0; }

/* 强制补全模式下，省市校等必填项的红色星号 = .pf-req
   （规则见上方 profile-form 区块，与 fieldHtml 的必填星号同一个类）。
   仅作视觉提示，真正的必填校验在 bindProfileForm 的 forced 分支里做，
   不依赖颜色单点传达（符合 WCAG：星号 + 文案 + 提交拦截三重提示）。 */

/* 城市/学校联动下拉：下拉与「其他」手填框同属一个 .pf-field，
   .is-hidden 用 display:none 真正移出布局流，不留空隙。 */
.pf-select { width: 100%; margin: 0; }
.pf-other { width: 100%; margin: 6px 0 0; }
.pf-other.is-hidden { display: none; }
/* 下拉显示占位/请选择时压暗，与真实取值区分 */
.pf-select:invalid, .pf-select option[value=""] { color: var(--text-soft); }

/* ============ hidden 属性的全局兜底 ============
   HTML 的 hidden 属性本质是 UA 样式表里的 `[hidden] { display: none }`，
   但**任何作者样式里的 display 声明都会覆盖它**（同优先级、后来居上）。
   本项目大量元素带 .btn / .chip / .row 等类，而这些类都设了
   `display: inline-flex` / `flex` —— 于是 `el.hidden = true` 完全失效：
   元素在 DOM 里仍然显示。

   真实症状：生词卡点「显示释义」后，按钮本该消失却仍留在原地；
   熟词僻义自测的答案框同理。

   这里用 `!important` 全局兜底，而不是给每个元素补 [hidden] 规则——
   后者属于「逐个打补丁」，漏一个就再犯一次。所有元素统一走这一条。 */
[hidden] { display: none !important; }
.profile-form .pf-grid {
  display: grid; gap: 14px; grid-template-columns: 1fr;
}
.profile-form .pf-grid > .pf-field { margin-top: 12px; }
@media (min-width: 640px) {
  .profile-form .pf-grid { grid-template-columns: repeat(2, 1fr); }
}
/* 需要整行宽度的字段（如备考目标 textarea） */
.pf-wide { grid-column: 1 / -1; }

/* ---------- 省份 / 城市 / 学校 / 专业 四级地区字段版式 ----------
   产品要求的版式（用户原话：「学校换行和专业放在一行」）：

     行 1：省份 │ 城市
     行 2：学校 │ 专业

   实现要点：
   1. 用 CSS Grid 的显式列定位，而不是靠元素顺序 + span 猜测。
      学校列内容天然比省份长（院校名普遍较长），给学校 1.35fr、城市 1fr，
      避免学校名被截断成「浙江...」而省份却空着一大截。
   2. 窄屏（<640px）退化为单列纵向堆叠：320px 屏上两列并排会导致
      每个控件只剩 ~140px，院校名几乎无法阅读。
   3. 学校保留「其他」手填框时，下拉与输入框会并排占据同一格宽度。 */
.profile-form .pf-grid.pf-region {
  display: grid;
  gap: 14px;
  grid-template-columns: 1fr;
}
@media (min-width: 640px) {
  .profile-form .pf-grid.pf-region {
    /* 第 1 列（省份 / 学校）给 1.35fr，第 2 列（城市 / 专业）给 1fr。
       为什么第 1 列更宽：省份名短（「浙江省」3 字），但**学校名长**
       （「哈尔滨工业大学（威海）」12 字），两者共用第 1 列。
       若按直觉给省份列更多宽度，结果是学校列被压到最窄 ——
       恰恰是最需要空间的字段被挤掉，实测院校名会被截断。 */
    grid-template-columns: 1.35fr 1fr;
  }
  /* 行 1：省份 | 城市 */
  .profile-form .pf-region > .pf-field:nth-child(1) { grid-column: 1; grid-row: 1; }
  .profile-form .pf-region > .pf-field:nth-child(2) { grid-column: 2; grid-row: 1; }
  /* 行 2：学校 | 专业 —— 学校换行后与专业同行 */
  .profile-form .pf-region > .pf-field-school { grid-column: 1; grid-row: 2; }
  .profile-form .pf-region > .pf-field-major { grid-column: 2; grid-row: 2; }
}
/* 后台编辑弹窗宽度较窄，学校列压缩为 1:1，避免弹窗内出现横向滚动 */
@media (min-width: 640px) {
  .modal .pf-grid.pf-region { grid-template-columns: 1fr 1fr; }
}

/* 名单外老数据提示：值不能静默丢失，也不能让用户以为没保存过 */
.profile-form .field-stale {
  margin: 2px 0 0; font-size: 12px; line-height: 1.6; color: var(--warn, #b45309);
  background: var(--warn-bg, #fffbeb); border: 1px solid var(--warn-bd, #fde68a);
  border-radius: 8px; padding: 6px 8px;
}
.profile-form .field-stale b { color: inherit; }
/* 清除按钮：够大到可点（≥32px），且有明确 hover/focus 反馈 */
.stale-clear {
  margin-left: 6px; padding: 2px 8px; min-height: 28px;
  border: 1px solid currentColor; border-radius: 6px;
  background: transparent; color: inherit; font-size: 12px; cursor: pointer;
  transition: background-color .2s ease, transform .2s ease;
}
.stale-clear:hover { background: rgba(180, 83, 9, .12); }
.stale-clear:active { transform: scale(.96); }
.stale-clear:focus-visible { outline: 2px solid var(--primary, #2563eb); outline-offset: 2px; }

/* 省份选好前，后两级是 disabled，压暗并禁止 hover 反馈，
   否则用户会以为下拉坏了 */
.pf-select:disabled {
  background: var(--bg); color: var(--text-mute);
  cursor: not-allowed;
}

.card-sub {
  margin: 0 0 16px; font-size: 13px; line-height: 1.75; color: var(--text-soft);
}
.profile-card { align-self: start; }
.profile-side { align-self: start; }
.profile-side code {
  background: var(--primary-light); color: var(--primary-600);
  padding: 2px 8px; border-radius: 6px; font-weight: 700;
}

/* 管理后台用户列表：列变多了，窄屏横向滚动而不是把卡片撑破 */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.table-scroll .table { min-width: 720px; }
.table .ta-c { text-align: center; }
.table .ta-num {
  text-align: right; white-space: nowrap;
  font-variant-numeric: tabular-nums;   /* 时长列数字等宽，纵向对齐更好读 */
}
/* 省份/城市/学校可能很长（尤其手填值），允许换行而不是把表格撑到失控 */
.table .ta-wrap { min-width: 140px; max-width: 220px; word-break: break-word; }
/* 后台用户表：2026-10-14 精简为三列（会员名 / 总时长 / 学校）后
   不再需要 1080px 最小宽——窄屏本就放得下，强行 min-width 只会
   逼出横向滚动。保留 .table-scroll 容器兜底未来加列的情况。 */
.table-scroll .table.admin-users { min-width: 0; }
/* 会员名红字标管理员：权限身份一眼可辨。
   用 --err-ink（#a93232，白底 6.6:1）而不是 --err（#d95757，白底仅 3.9:1）——
   红字是**正文信息**不是装饰，必须过 WCAG AA 4.5:1；
   且详情弹窗里另有「角色：管理员」文字，不单靠颜色传达。 */
.admin-users .au-name-admin { color: var(--err-ink, #a93232); }
.admin-users .au-name-btn:hover .au-name-admin { color: var(--err-ink, #a93232); text-decoration: underline; }
/* 赞助会员的会员名用琥珀色（用户 2026-10-15：名字用颜色区分身份）。
   与普通用户（正文色 #2b3038）对比明显，且 --warn-ink 在白底上 ≥6:1。 */
.admin-users .au-name-vip { color: var(--warn-ink, #8a5a18); }
.admin-users .au-name-btn:hover .au-name-vip { color: var(--warn-ink, #8a5a18); text-decoration: underline; }
/* 身份标记（⚙ 管理员 / ★ 赞助会员）：颜色之外的**文字通道**，
   WCAG 1.4.1 不允许只用颜色传达信息；11px 单字符，几乎不占横向空间。 */
.au-mark { margin-left: 4px; font-size: 11px; line-height: 1; vertical-align: 1px; }
.au-mark-admin { color: var(--err-ink, #a93232); }
.au-mark-vip { color: var(--warn, #c98a3c); }

/* 操作列两个按钮并排；窄屏纵向堆叠，避免挤成一条缝 */
.row-acts { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; }

/* 密码不可查看的说明条：放在用户表正上方，别让管理员以为漏了一列 */
.admin-pwd-note {
  margin: 0 0 12px; padding: 10px 12px;
  background: var(--warn-bg); color: var(--warn-ink);
  border: 1px solid rgba(201,138,60,.3); border-radius: 8px;
  font-size: 13px; line-height: 1.7;
}
/* 临时密码展示：大字 + 等宽 + 居中，方便一眼抄下来 */
.tmp-pwd {
  margin: 10px 0 12px; padding: 16px;
  background: var(--primary-light); color: var(--primary-700);
  border: 2px dashed var(--primary-400); border-radius: 10px;
  text-align: center; font-size: 28px; font-weight: 800;
  letter-spacing: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  word-break: break-all; user-select: all;
}
.tmp-note { margin: 0; font-weight: 700; }
.user-edit-btn { white-space: nowrap; }
.user-edit-btn:focus-visible { outline: 2px solid var(--primary-600); outline-offset: 2px; }

/* ============ 修改密码页 ============ */
/* 强制改密横幅：role="alert"，插入时读屏会立即播报，
   颜色用警告语义而非纯红——这不是"你错了"，是"你需要处理一件事"，
   用红色会让人误以为系统故障。 */
.notice {
  margin: 0 0 16px; padding: 12px 14px; border-radius: 10px;
  font-size: 14px; line-height: 1.8;
}
.notice-warn {
  background: var(--warn-bg); color: var(--warn-ink);
  border: 1px solid rgba(201,138,60,.35);
}
.notice-sub { font-size: 12px; opacity: .8; }

/* 密码强度条：纯提示，不参与校验。
   颜色 + 文字双通道传达（不能只靠颜色，色觉障碍用户读不出来），
   高度 6px 配圆角，视觉上足够轻，不与密码输入框抢注意力。 */
.pwd-meter {
  display: flex; align-items: center; gap: 10px;
  margin-top: 10px; font-size: 12px; color: var(--text-soft);
}
.pwd-bar {
  flex: 1; height: 6px; border-radius: 3px;
  background: var(--border, #e2e8f0); overflow: hidden;
}
.pwd-bar > i { display: block; height: 100%; width: 0; border-radius: 3px; transition: width .25s ease; }
.pwd-bar > i.lv0, .pwd-bar > i.lv1 { background: #dc2626; }
.pwd-bar > i.lv2 { background: #d97706; }
.pwd-bar > i.lv3 { background: #16a34a; }
.pwd-bar > i.lv4 { background: #15803d; }
.pwd-tip { min-width: 3.2em; }  /* 文案长度不同时避免宽度跳动 */

/* 密码建议清单 */
.tips-list {
  margin: 0 0 14px; padding-left: 20px;
  font-size: 13px; line-height: 1.9; color: var(--text-soft);
}
.tips-list b { color: var(--text); }

/* ============ 笔记：随行工具化改造 ============ */
/* 原实现把样式写成行内 style 塞在考后成绩页里，无法下沉复用。
   抽成 .note-box 后，错题本/练习页/成绩页共用一套外观。 */
.note-box {
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px dashed var(--border);
}
/* margin 显式写出（与全局 `label { margin: 12px 0 6px }` 的取值一致）：
   它是一个**块级表单标签**，本来就该吃那套间距，写出来是为了让"依赖隐式继承"
   变成"写明意图" —— 免得哪天全局 label 规则一改，这里静默漂移。
   （反例见 .qk-sw：那个 label 当行内控件用，已被全局 margin 污染过一次。） */
.note-box-label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--p);
  margin: 12px 0 6px;
}
.note-box .noteInput {
  width: 100%;
  min-height: 52px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 14px;
  resize: vertical;
  background: var(--bg);
  color: inherit;
}
.note-box .noteInput:focus-visible {
  outline: 2px solid var(--primary-light);
  outline-offset: 1px;
  border-color: var(--primary);
}
.note-box-ops {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
  flex-wrap: wrap;
}
.note-box .noteHint {
  color: var(--ok);
  font-size: 13px;
}
/* 仅供读屏的标签：不显示但可被辅助技术读取 */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* 笔记管理页 */
.note-mgr { margin-bottom: 16px; }
.note-mgr-tip {
  font-size: 13px;
  color: var(--p);
  margin-bottom: 12px;
  line-height: 1.6;
}
.note-item { margin-bottom: 12px; }
.note-item-head {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 6px;
}
.note-item-time { font-size: 12px; color: var(--p); }
.note-item-qid {
  font-size: 12px;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  background: var(--primary-light);
  color: var(--primary);
}
.note-item-ops { margin-left: auto; display: flex; gap: 8px; }
.note-item-q {
  font-size: 13px;
  color: var(--p);
  background: var(--bg);
  border-left: 3px solid var(--border);
  padding: 6px 10px;
  margin-bottom: 6px;
  line-height: 1.5;
}
.note-item-body {
  white-space: pre-wrap;
  line-height: 1.7;
  word-break: break-word;
}
/* 对错标记：不能只靠颜色区分（WCAG 1.4.1），文案本身就说清了"答对/答错" */
.note-mark {
  font-size: 11.5px; font-weight: 700; padding: 1px 8px;
  border-radius: var(--radius-pill); white-space: nowrap;
}
.note-mark.ok { color: var(--ok); background: var(--ok-bg); }
.note-mark.bad { color: var(--err-ink); background: var(--err-bg); }

/* 原题与作答上下文：默认折叠，点开才占纵向空间 */
.note-item-ctx { margin-top: 10px; }
.note-ctx summary {
  cursor: pointer; font-size: 12.5px; color: var(--p);
  padding: 4px 0; user-select: none;
}
.note-ctx summary:focus-visible { outline: 2px solid var(--p); outline-offset: 2px; border-radius: 4px; }
.note-ctx-body {
  margin-top: 8px; padding: 10px 12px;
  background: var(--bg); border-left: 3px solid var(--border);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
/* 键值两列：左列定宽，长题干与长答案都在右列自动换行，不会互相挤压 */
.note-ctx-row {
  display: grid; grid-template-columns: 72px minmax(0, 1fr);
  gap: 10px; padding: 4px 0;
}
.note-ctx-row + .note-ctx-row { border-top: 1px dashed var(--border); }
.note-ctx-k { font-size: 12.5px; color: var(--text-soft); font-weight: 600; }
.note-ctx-v {
  font-size: 13.5px; line-height: 1.65; word-break: break-word;
  white-space: pre-wrap;
}
@media (max-width: 480px) {
  .note-ctx-row { grid-template-columns: 1fr; gap: 2px; }
}

/* 账户信息里的次级说明（如"奖励进度 1/2"） */
.stat-sub { color: var(--text-soft); font-size: 12.5px; margin-left: 4px; }
.note-item-edit textarea {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 14px;
  background: var(--bg);
  color: inherit;
}

/* 参考范文：提交批注后才出现的对照区 */
.sp-ref-hint {
  margin-top: 10px;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  background: var(--primary-light);
  color: var(--p);
  font-size: 13px;
  line-height: 1.6;
}
.sp-ref-after {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px dashed var(--border);
}
.sp-ref-after > summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 14px;
  padding: 4px 0;
}
.sp-ref-after > summary:focus-visible {
  outline: 2px solid var(--primary-light);
  outline-offset: 2px;
}
.sp-ref-b {
  margin-top: 8px;
  padding: 12px 14px;
  background: var(--bg);
  border-radius: var(--radius-sm);
  line-height: 1.8;
  white-space: pre-wrap;
}
.sp-ref-tip {
  margin-top: 8px;
  font-size: 13px;
  color: var(--p);
  line-height: 1.6;
}

/* ============ 管理后台布局 ============
   原先是 1.55fr / 1fr 的两列栅格（左：统计+会员，右：导出题库面板）。
   导出面板删除后右列恒为空，若保留栅格会把左栏压到 60% 宽、
   底部留出大片空白 —— 空列比没有列更糟。
   现在后台是单栏结构，.admin-layout / .admin-side 两类已随之删除，
   两张卡直接回到 .app 的常规单列流里。
   （更早一版用 grid-2 强制左右等高，那会把高瘦的统计列表拉长、底部留白；
     这个坑记在这里，因为"改回两列"是很自然的下一步动作。） */
.admin-main { min-width: 0; }
.admin-main > .card + .card { margin-top: 18px; }
.admin-users { overflow: hidden; }
.admin-users-head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
.admin-users-head h3 { margin: 0; }
.admin-users-tip {
  font-size: 12px;
  color: var(--p);
}

/* ============ 会员管理：筛选条 / 状态徽章 / 分页 ============
   窄屏（<720px）把搜索框与两个下拉改为整行堆叠：三者并排时
   每格只剩约 90px，「搜索用户名/昵称/手机号」这类 placeholder 会被压没，
   用户根本不知道能搜什么。 */
.admin-filters {
  margin: 0 0 12px; padding: 12px;
  background: var(--bg, #f8fafc);
  border: 1px solid var(--border); border-radius: 10px;
}
.af-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.af-row + .af-row { margin-top: 10px; }
.af-search { flex: 1 1 260px; min-width: 0; }
.af-search input {
  width: 100%; padding: 9px 12px; font-size: 14px;
  border: 1.5px solid var(--border); border-radius: 8px; background: #fff;
}
.af-search input:focus-visible {
  outline: none; border-color: var(--primary-500, #6366f1);
  box-shadow: 0 0 0 3px rgba(99,102,241,.15);
}
.af-select {
  padding: 9px 10px; font-size: 13px; min-width: 108px;
  border: 1.5px solid var(--border); border-radius: 8px; background: #fff;
}
/* 会员管理两排筛选 chip：统一尺寸的等分网格。
   痛点（用户实拍反馈"太乱了"）：chip 宽度原来跟着文字长短变
   （"全部"窄、"VIP 有效"宽），两排长短不齐、边也对不齐，像一团乱麻。
   改为 grid 等分列后所有 chip 同宽同高；状态排 4 个刚好占满 4 列，
   角色排 3 个占前 3 列、右侧留一个空位 —— 两排纵向严格对齐，
   大小统一、边界清晰。手机上 4 等分仍成立（360px 视口每格约 73px，
   12px 字号 + 4px 内边距放得下最宽的"VIP 有效"）；
   320px 极窄屏再降一档字号，防止文字溢出 chip。 */
.af-chips {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 6px;
}
.af-chips .chip {
  width: 100%;
  min-width: 0;
  height: 34px;
  padding: 0 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  font-size: 12px;
}
@media (max-width: 360px) {
  .af-chips .chip { font-size: 11px; padding: 0 2px; }
}
.af-div { width: 1px; height: 20px; background: var(--border); margin: 0 4px; }
/* 角色筛选行：**与状态行完全同款**（用户反馈"把第二行下面的底色去掉，
   和上一行样式一致，靠左排列"）。
   此前这行多了一层浅紫底 + 内边距，本想用底色暗示"这是另一组筛选"，
   实际效果是两行粗细不一、像贴了块补丁。现在：
   ① 底色/内边距全部移除，只保留与状态行相同的 10px 行距；
   ② 4 列网格下 3 个 chip 天然靠左站前 3 列，与上一行纵向对齐。 */
.af-row.af-chips-role {
  margin-top: 10px;
}
.admin-filters .field-hint { margin: 8px 0 0; }

@media (max-width: 720px) {
  .af-search { flex: 1 1 100%; }
  .af-select { flex: 1 1 calc(50% - 4px); min-width: 0; }
}

/* 状态徽章：一律「文字 + 颜色」双通道。
   只用颜色区分 VIP / 封禁 / 普通，色觉障碍用户读不出来，
   这是 WCAG 2.2 里 1.4.1（不依赖颜色传达信息）的直接要求。 */
.badge.banned { background: #fee2e2; color: #991b1b; border: 1px solid #fecaca; }
.badge.vip { background: #fef3c7; color: #92400e; border: 1px solid #fde68a; }
.badge.plain { background: #f1f5f9; color: var(--text-soft); border: 1px solid var(--border); }
.vip-on { color: #92400e; font-weight: 700; }

/* 封禁行整体压暗，让管理员一眼扫出异常账号。
   同时保留文字徽章，不依赖这一层视觉。 */
tr.row-banned td { background: #fef7f7; }
tr.row-banned td:first-child { box-shadow: inset 3px 0 0 #dc2626; }

.ta-sub { display: block; font-size: 11px; color: var(--text-mute); margin-top: 2px; }
.ta-sub.warn { color: #b45309; font-weight: 600; }
.au-name { font-weight: 600; }
/* 用户名按钮：看起来还是 plain text，但可点击、有键盘焦点环。
   去掉 button 的默认边框/背景，只保留 hover 变色 + focus-visible 焦点框
   （键盘用户 Tab 到它时必须看得见焦点，这是 WCAG 2.4.7 的硬要求）。 */
.au-name-btn {
  display: block; max-width: 100%; padding: 0; border: none;
  background: none; cursor: pointer; text-align: left;
  font: inherit; color: inherit; border-radius: 6px;
}
.au-name-btn:hover .au-name { color: var(--primary-600, #6366f1); text-decoration: underline; }
.au-name-btn:focus-visible {
  outline: 2px solid var(--primary-500, #6366f1);
  outline-offset: 2px;
}
/* .au-nick 已随第 26 轮「行内只显示会员名」删除（昵称只出现在详情弹窗），
   样式一并撤掉，避免留一条永远匹配不到元素的死规则。 */

.admin-pager {
  display: flex; gap: 10px; align-items: center; justify-content: center;
  margin-top: 14px;
}
.ap-info { font-size: 13px; color: var(--text-soft); font-variant-numeric: tabular-nums; }

/* 错题本："你当时选 / 正确答案"显式文字行。
   选项变色只是辅助通道；这行文字才是完整信息——
   色弱、灰度打印、夜间模式下都能分清对错。 */
.wb-ans { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.wb-ans em { font-style: normal; font-weight: 700; padding: 1px 8px; border-radius: 6px; }
.wb-ans .wb-mine { color: var(--err-ink, #a93232); background: var(--err-bg, #fef2f2); }
.wb-ans .wb-right { color: var(--ok, #16a34a); background: var(--ok-bg, #f0fdf4); }

/* 会员详情弹窗：分区小标题 + 次级说明 + 待办警示 */
.ud-h {
  margin: 16px 0 6px; font-size: 13px; font-weight: 700;
  color: var(--text-soft, #64748b); letter-spacing: .05em;
}
.ud-h:first-child { margin-top: 0; }
.ud-sub { display: block; font-size: 12px; font-weight: 400; color: var(--text-mute, #94a3b8); margin-top: 2px; }
.ud-warn { color: #b45309; font-weight: 600; }
.modal-body .stat-row { padding: 7px 0; }
/* 用户详情弹窗紧凑版（.ud-tight，对齐平台口令表格观感）：
   12px 字号 + 行距/分区间距收紧。 */
.ud-tight { font-size: 12px; }
.ud-tight .stat-row { padding: 4px 0; }
.ud-tight .ud-h { margin: 10px 0 3px; font-size: 12px; }
.ud-tight .ud-sub { font-size: 11px; }

/* 续费弹窗的快捷天数 */
.vip-quick { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 14px; }
.vip-quick .chip { cursor: pointer; }
.vip-quick .chip.active { background: var(--primary-light); color: var(--primary-700); }

/* .q-wordline 改成 inline 布局后，文字/喇叭"被挤成两半"的问题
   不复存在 —— 它们本来就该在同一行里排。
   这三条原本是 flex 两列布局的补丁（补宽度、压badge 边距、强制块级），
   布局前提已经变了，补丁就成了无用甚至有害的残留：
   尤其 margin-left:0 会让题型标签紧贴喇叭，读起来像喇叭的一部分。 */
.q-wordline { width: 100%; }

/* ============ 熟词僻义：筛选区 ============
   第 31 轮起，级别 / 类型两排是 .qk-panel 内部的**一段**（顶部一条分隔线），
   不再是独立的边框卡片 —— 那层 border + padding + margin 正是首屏虚高的来源之一。
   两排各带一个 11.5px 前缀标签（级别 / 类型）：用户反馈"「全部」两排都有，
   看不出各自管哪个维度"，标签是那一条的解法。
   ⚠️ 分段控件（.quirk-modes）的样式已上移到熟词僻义页头部那一节，
      这里不再重复定义 —— 同一套类名在两处定义是样式漂移的温床。 */
.quirk-filters {
  /* 两排 chip 共用的排版参数。改动只改这里，两排一起变。 */
  --qf-fs: 12.5px;
  --qf-pad: 6px 11px;
  --qf-gap: 6px;
  display: flex; flex-direction: column; gap: 7px;
  padding-top: 10px; margin-bottom: 10px;
  border-top: 1px solid var(--border);
}
.qf-line { display: flex; align-items: center; gap: 8px; min-width: 0; }
/* 前缀标签用 --text-soft 而不是 --text-mute：
   #98a0ae 在白底上只有 2.7:1，而这是正文级文字，需 ≥4.5:1（WCAG 2.2 AA）。
   层次感靠字号（11.5px）+ 字重（700）+ 前缀位置来表达，不靠牺牲对比度。 */
.qf-label {
  flex: none; width: 26px; font-size: 11.5px; font-weight: 700;
  color: var(--text-soft); letter-spacing: .5px;
}
/* ⚠️ 每排必须各自在一行内，不能因为字数多而换行：
   1) flex-wrap: nowrap —— .filter-row 默认是 wrap，而"级别/类型"是**两个维度**，
      一旦某排折成两行，"两排"就变成了"三排四排"，用户对不上哪几个是同一维度。
   2) overflow-x: auto —— nowrap 的前提是**必须有兜底**。若容器比这一排还窄，
      nowrap 就会让内容溢出到卡片外、把整页撑出横向滚动条（正是本任务禁止的）。
      交给本行自己滚动即可：溢出量被关在 .quirk-filters 内部，屏幕不宽。
   3) .chip 上的 flex:none —— 不加的话 flex 子项会被压窄，
      文字虽然有 nowrap 却仍被裁掉，变成另一种形式的"看不到"，
      等于把换行问题换成了截断问题。 */
.qf-chips {
  margin: 0;
  flex-wrap: nowrap;
  gap: var(--qf-gap);
  min-width: 0; max-width: 100%;
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none;           /* 与 .nav-links 一致：能滚但不画滚动条 */
  -webkit-overflow-scrolling: touch;
}
.qf-chips::-webkit-scrollbar { display: none; }
.qf-chips .chip {
  flex: none;                      /* 不参与压缩，宽度恒等于内容 */
  font-size: var(--qf-fs);
  padding: var(--qf-pad);
}

@media (max-width: 600px) {
  /* ⚠️ 这里**故意不再**把 .quirk-modes 改成单列。
     旧版是两张 70px 的大卡，手机上必须堆叠；现在它是 34px 的分段控件，
     并排才符合"分段"的语义，堆叠反而会白吃 40px。 */
  /* 手机上字号与内边距一起收一档，让最宽的一排（四个四字 chip）
     尽量不触发横向滚动。实测 390px 下可用宽约 304px，
     加上左前缀标签（26+8）后该排约 276px，放得下；
     360px 下刚好贴边，320px 会切到行内滚动 —— 由 .qf-chips 的
     overflow-x:auto 兜底，绝不换行、也绝不溢出屏幕。
     注意这仍然只改 .quirk-filters 上的变量 —— 两排仍然同字号，
     收档不会让其中一排单独变小。 */
  .quirk-filters {
    --qf-fs: 12px;
    --qf-pad: 5px 10px;
    --qf-gap: 5px;
  }
}

/* ============================================================
   下拉刷新指示器 + 薄弱点报告 + 错题/笔记工具栏（A1/B5/B7/B8）
   ============================================================ */

/* 下拉刷新：固定在顶部、默认藏在视口上方的细条。
   touchmove 时 JS 直接写 inline transform 把它拉下来；松手若未过阈值
   JS 清掉 inline transform → 回到此处 translateY(-56px) 隐藏；
   若触发刷新则加 .active → 显形并转圈。
   pointer-events:none：纯展示，不能挡住下方点击。
   位移动画属"非必要动效"，prefers-reduced-motion 下 JS 已不写位移，
   这里也撤掉过渡，转圈同样停掉（见下方媒体查询）。 */
.ptr-indicator {
  position: fixed; top: 0; left: 0; right: 0;
  height: 48px; z-index: 60;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  background: var(--card); color: var(--text-soft);
  transform: translateY(-56px); opacity: 0;
  pointer-events: none;
  box-shadow: 0 2px 8px rgba(0,0,0,.06);
}
.ptr-indicator.active { transform: translateY(0); opacity: 1; }
.ptr-spinner {
  width: 20px; height: 20px;
  border: 2.5px solid var(--border);
  border-top-color: var(--primary-600);
  border-radius: 50%;
}
.ptr-arrow { display: inline-block; font-size: 16px; line-height: 1; transition: transform .2s ease; }
.ptr-label { font-size: 13px; font-weight: 600; color: var(--text); }
/* ready：箭头翻转（↑）+ 文字变「松手刷新」+ spinner 转绿（绿色仅为增强，状态主通道是文字与箭头，符合 WCAG 1.4.1） */
.ptr-indicator.ready .ptr-spinner { border-top-color: var(--ok); }
.ptr-indicator.ready .ptr-arrow { transform: rotate(180deg); }
.ptr-indicator.active .ptr-arrow { display: none; }
.ptr-indicator.active .ptr-spinner { animation: ptr-spin .7s linear infinite; }
@keyframes ptr-spin { to { transform: rotate(360deg); } }

/* 首次下拉刷新引导（触摸设备，一次性） */
.ptr-hint {
  position: fixed; left: 50%; top: 58px; transform: translateX(-50%) translateY(-8px);
  z-index: 70; display: flex; align-items: center; gap: 6px;
  padding: 6px 14px; border-radius: 999px;
  background: var(--card); color: var(--text);
  box-shadow: 0 2px 10px rgba(0,0,0,.12);
  font-size: 13px; font-weight: 600; white-space: nowrap;
  opacity: 0; pointer-events: none;
  transition: opacity .3s ease, transform .3s ease;
}
.ptr-hint.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.ptr-hint-arrow { display: inline-block; animation: ptr-hint-bounce 1.2s ease-in-out infinite; }
@keyframes ptr-hint-bounce { 0%,100% { transform: translateY(0); } 50% { transform: translateY(3px); } }
@media (prefers-reduced-motion: reduce) {
  .ptr-arrow { transition: none; }
  .ptr-hint-arrow { animation: none; }
}

/* 错题本 / 笔记 工具栏：一排小按钮，可换行，绝不挤成竖排
   （按钮加 min-width:max-content，避免窄屏被压成"一字一行"）。 */
.wb-toolbar { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0; align-items: center; }
.wb-toolbar .btn { min-width: max-content; }

/* 薄弱点报告：按板块错题库的横条。
   .bc-bar.weak 标红提示错得最多的板块，但长度本就最长，不单靠颜色传达。 */
.barchart-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; }
.bc-label { flex: none; width: 60px; font-size: 13px; color: var(--text-soft); }
.bc-track {
  flex: 1 1 auto; min-width: 0; height: 10px;
  border-radius: var(--radius-pill); background: var(--bg); overflow: hidden;
}
.bc-bar {
  display: block; height: 100%; min-width: 2px;
  background: var(--primary-600); border-radius: var(--radius-pill);
  transition: width .4s ease;
}
.bc-bar.weak { background: var(--err); }
.bc-val {
  flex: none; min-width: 28px; text-align: right;
  font-weight: 700; font-variant-numeric: tabular-nums; font-size: 13px;
}

/* 词汇掌握度甜甜圈（SVG）。数值同时写在 aria-label 与图例中，不依赖颜色。 */
.donut-wrap { display: flex; flex-wrap: wrap; align-items: center; gap: 18px; }
.donut { width: 140px; height: 140px; flex: none; }
.donut-seg { transition: stroke-dasharray .5s ease; }
.donut-seg.known { stroke: var(--ok); }
.donut-seg.fuzzy { stroke: var(--warn); }
.donut-seg.unknown { stroke: var(--err); }
.donut-num { font-size: 26px; font-weight: 800; fill: var(--text); text-anchor: middle; }
.donut-cap { font-size: 12px; fill: var(--text-soft); text-anchor: middle; }
.donut-legend { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; font-size: 13px; }
.donut-legend li { display: flex; align-items: center; gap: 8px; }
.dot { width: 10px; height: 10px; border-radius: 50%; flex: none; display: inline-block; }
.dot.known { background: var(--ok); }
.dot.fuzzy { background: var(--warn); }
.dot.unknown { background: var(--err); }

/* 复习建议列表 */
.advice-list { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 8px; }
.advice-list li { font-size: 14px; line-height: 1.7; color: var(--text); }

/* 管理员后台 AI 能力状态卡（A1）：状态后的次级说明弱一档 */
.admin-ai .ai-sub { color: var(--text-mute); font-size: 12px; font-weight: 500; }

/* 降级动效：尊重系统"减少动态效果"偏好 */
@media (prefers-reduced-motion: reduce) {
  .bc-bar, .donut-seg { transition: none; }
  .ptr-indicator.active .ptr-spinner { animation: none; }
}

/* 打印：工具栏与下拉指示条不进纸；图表保留（数值已在文字里可读） */
@media print {
  .wb-toolbar, .ptr-indicator, .ptr-hint { display: none !important; }
}
