/* ============================================================
   英语听写 · 移动端设计系统
   目标：在手机上有原生 App 的质感
   ============================================================ */
:root {
  /* v2.36 · 对齐设计稿：背景极淡 + 玻璃近白微透 + 深蓝粗字 */
  --bg: #E8F1FB;
  --bg-2: rgba(255,255,255,.55);
  /* v2.40 · 白度整体提上来一档 —— 抹掉 backdrop-filter 之后，
     这里的透明度就是「最终不透度」，太透会让字压不住背景。 */
  --card: linear-gradient(152deg, rgba(255,255,255,.94) 0%, rgba(255,255,255,.86) 46%, rgba(255,255,255,.92) 100%);
  /* 遗留变量：老 WebView 的 @supports 回退分支还在引用它。不要再挂到会滚动的东西上。 */
  --glass-blur: blur(22px) saturate(180%);
  --text: #1B2B52;
  --text-2: #5E6C8F;
  --text-3: #97A2BB;
  --line: rgba(150,178,220,.28);
  --line-2: rgba(150,178,220,.14);

  --primary: #3E7BFA;
  --primary-2: #8A63FF;
  /* 设计稿按钮是「左青右紫」的横向渐变 */
  --grad: linear-gradient(90deg, #3FC3FF 0%, #5E7BFF 52%, #8B6BFF 100%);
  /* v2.34 磨砂风格：淡底改成半透明冰蓝，和玻璃卡片同一套语言 */
  --grad-soft: rgba(216,235,255,.60);
  --success: #0FB58C;
  --success-soft: rgba(64,222,180,.16);
  --danger: #EF5A6F;
  --danger-soft: rgba(244,120,140,.14);
  --warn: #E8890C;
  --warn-soft: rgba(248,182,88,.16);

  --r-xl: 28px;
  --r-lg: 22px;
  --r-md: 16px;
  --r-sm: 12px;

  --sh-1: 0 1px 1px rgba(255,255,255,.65) inset, 0 2px 8px rgba(88,120,180,.07), 0 10px 26px rgba(88,120,180,.10);
  --sh-2: 0 4px 14px rgba(88,120,180,.10), 0 18px 40px rgba(88,120,180,.12);
  --sh-btn: 0 8px 20px rgba(78,123,255,.34), 0 3px 8px rgba(78,123,255,.22);
  --sh-up: 0 -1px 0 var(--line), 0 -8px 24px rgba(88,120,180,.06);

  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-t: env(safe-area-inset-top, 0px);
  --tab-h: 58px;

  /* v2.39 · 全站唯一的横向栅格。
     ⚠️ 这里必须是**唯一**的一份：顶栏、内容区、悬浮 dock 栏三处的左右留白
        只要差 2px，在手机上就是肉眼可见的「边缘对不齐」（学生反馈原话）。
        以前是 18 / 16 / 14 三个值各写各的 —— 全部收到这一个变量上。 */
  --gutter: 16px;
  /* 内容区最大宽度（含左右留白）—— 大屏上 dock 栏要跟内容同宽并一起居中 */
  --content-max: 560px;

  --ease: cubic-bezier(.32,.72,0,1);
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { margin: 0; padding: 0; height: 100%; }
html { -webkit-text-size-adjust: 100%; }
body {
  background: #E3EEFB;
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, sans-serif;
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: none;
}
/* v2.34 · 全局冰川极光背景：薄荷 + 冰蓝 + 淡紫 + 樱粉的光斑，
   固定住不跟页面滚（background-attachment: fixed 在 iOS 上是坏的，用伪元素） */
body::before {
  content: ""; position: fixed; inset: 0; z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(560px 440px at 94% -8%, rgba(146,232,200,.52), transparent 60%),
    radial-gradient(540px 480px at -12% 6%, rgba(168,212,255,.42), transparent 64%),
    radial-gradient(620px 540px at 92% 58%, rgba(196,178,250,.30), transparent 70%),
    radial-gradient(520px 420px at 30% 96%, rgba(178,214,255,.26), transparent 68%),
    linear-gradient(170deg, #EAF2FB 0%, #F0F6FD 44%, #F7FAFE 100%);
}
/* 冰晶噪点：极细的磨砂颗粒，去掉塑料平面的光滑感（iOS 毛玻璃的那层"霜"）。
   ⚠️⚠️ 这里**绝不能加 mix-blend-mode: overlay**。
   它是 position:fixed + 整屏大小，叠加混合意味着「页面每滚一帧，浏览器都要把
   整块固定层和下面整篇滚动内容重新混一次」—— 中低端安卓上直接掉帧（学生反馈
   「滑动卡顿、不流畅」的最大一笔就是这个）。去掉混合、只留一点点透明度，
   肉眼几乎看不出差别，但滚动回到纯合成，一帧都不用重混。 */
body::after {
  content: ""; position: fixed; inset: 0; z-index: -1;
  pointer-events: none; opacity: .045;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='.6'/%3E%3C/svg%3E");
}
button, input, textarea, select { font: inherit; color: inherit; }
button { border: none; background: none; cursor: pointer; padding: 0; }
a { color: var(--primary); text-decoration: none; }
img { max-width: 100%; display: block; }
h1,h2,h3,h4 { margin: 0; font-weight: 600; }

/* ---------- 应用外壳 ---------- */
#app { min-height: 100%; }

/* ---------- 顶部栏（v2.40：从「一条横带」改成「一条不占地方的细线」） ----------
   ⚠️ v2.39 及以前这里是 54px 高 + 上下各 10px 的大标题带，一进页面先吃掉近百像素，
      老师手机上一屏就那么高，反馈原话是「每页最上方的横条全部删除，太占空间了」。
   v2.40 的处理：
     ① 主 Tab 页（听写 / 作文 / 我的 / 书架）**整条不要了** —— 底部 dock 已经写着
        你在哪一栏，页面自己有 hero / 资料卡做标题，这条纯属重复。见 app.js 的 topbar() 调用点。
     ② 子页还留着返回键（不然退不出去），但压到 44px、标题降到 17px。
     ③ 默认**完全透明**，和页面背景连成一片（所以顶上不再有那道「横条」）；
        只有往下滚了才由 JS 加 .stuck，浮出一层和背景同色的底 —— 否则标题会跟
        从下面滚上来的卡片文字叠在一起。
     ④ 去掉 backdrop-filter：它是 sticky 的，底下的内容每滚一帧都要重算一次模糊，
        是最贵的那种用法（见文件底部 v2.40 的性能说明）。
   ---- 左边缘仍然跟内容卡对齐（手机上就是 --gutter，宽屏按同一个公式跟过去）。 */
.topbar {
  position: sticky; top: 0; z-index: 30;
  padding-top: calc(var(--safe-t) + 6px);
  padding-bottom: 6px;
  padding-left: max(var(--gutter), calc((100% - var(--content-max)) / 2 + var(--gutter)));
  padding-right: max(var(--gutter), calc((100% - var(--content-max)) / 2 + var(--gutter)));
  background: transparent;
  transition: background .2s var(--ease), box-shadow .2s var(--ease);
  display: flex; align-items: center; gap: 10px;
  min-height: 44px;
}
.topbar.stuck {
  background: rgba(233,242,252,.94);
  box-shadow: 0 1px 0 rgba(150,178,220,.22);
}
.topbar h1 { font-size: 19px; font-weight: 700; letter-spacing: -.3px; flex: 1; }
.topbar h1.sm { font-size: 17px; letter-spacing: -.2px; }
.topbar .sub { font-size: 12.5px; color: var(--text-3); font-weight: 400; margin-left: 6px; }

/* v2.34 统一的返回按钮：磨砂玻璃圆钮 + 一道冰蓝描边，全站子页都用它 */
.back-btn {
  width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center; flex: none;
  background: rgba(255,255,255,.78); border: 0.5px solid rgba(255,255,255,.9);
  box-shadow: 0 2px 8px rgba(88,120,180,.12);
  color: var(--text); transition: transform .18s var(--ease);
}
.back-btn:active { transform: scale(.9); }
.back-btn svg { width: 18px; height: 18px; }

.screen {
  padding: 4px var(--gutter) calc(var(--tab-h) + var(--safe-b) + 48px);
  max-width: var(--content-max); margin: 0 auto;
  animation: screen-in .28s var(--ease);
}
.screen.no-tab { padding-bottom: calc(var(--safe-b) + 96px); }
/* v2.40 · 没有顶栏的页面，安全区顶部内边距必须由内容区自己补。
   ⚠️ 以前这段留白是 .topbar 的 padding-top 出的（calc(--safe-t + 10px)）。
      主 Tab 页把顶栏删掉之后，如果这里不补，第一张卡会直接钻到刘海 / 状态栏底下。
      mount() 会在检测不到 .topbar 时给 #app 打上 .no-tb —— 用 :has() 也能写，
      但老安卓 WebView 不支持，所以交给 JS 判。 */
#app.no-tb .screen { padding-top: calc(var(--safe-t) + 10px); }
#app.no-tb .screen.no-tab { padding-top: calc(var(--safe-t) + 14px); }

/* 表单标签放进左右布局（标签 + 小按钮）时，样式保持一致 */
.field .row-between > label {
  display: block; font-size: 13.5px; font-weight: 550; color: var(--text-2); margin: 0 2px;
}
/* 小号文字按钮（「改预设」这种，不抢视线） */
.link-mini {
  border: 0; background: transparent; padding: 2px 2px; margin-bottom: 7px;
  font-family: inherit; font-size: 13px; font-weight: 600; color: var(--primary);
  cursor: pointer; border-radius: 6px;
}
.link-mini:active { opacity: .55; }
@keyframes screen-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* ---------- 底部 Tab（v2.34：悬浮磨砂玻璃胶囊 + 活动指示条） ---------- */
.tabbar {
  position: fixed; bottom: calc(var(--safe-b) + 12px); z-index: 40;
  /* v2.39 · 左右边缘 = 内容卡的左右边缘（同一个 --gutter）。
     实测以前是 .screen 16 / .tabbar 14 / .topbar 18 —— 三个值，所以「边缘对不齐」。
     宽屏上再用 max-width + auto 外边距跟内容区一起居中，不会左窄右宽。 */
  left: var(--gutter); right: var(--gutter);
  max-width: calc(var(--content-max) - var(--gutter) * 2);
  margin-left: auto; margin-right: auto;
  display: flex;
  padding-bottom: 0;
  border-radius: 28px;
  /* v2.40 · 去掉 backdrop-filter。它固定在屏幕底部、下面永远压着正在滚动的列表，
     等于每一帧都要重新模糊一次「身后那块内容」—— 手机上掉帧最狠的一处。
     底下的 #dock-scrim 本来就是一层和背景同色的渐隐，两者一起看几乎一样。 */
  background: linear-gradient(160deg, rgba(255,255,255,.97), rgba(252,254,255,.93));
  box-shadow:
    0 12px 34px rgba(96,130,190,.16),
    inset 0 1px 0 rgba(255,255,255,.95);
}
.tab {
  position: relative;
  flex: 1; height: var(--tab-h);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  color: var(--text-3); font-size: 12.5px; font-weight: 550;
  transition: color .2s var(--ease);
}
/* v2.39 · dock 栏文字放大到 12.5px；行高压到 1.1，否则 58px 装不下会撞到下面的指示条 */
.tab span { line-height: 1.1; letter-spacing: .2px; white-space: nowrap; }
.tab svg { width: 23px; height: 23px; transition: transform .24s var(--ease); }
.tab.on { color: var(--primary); font-weight: 600; }
.tab.on svg { transform: translateY(-1px) scale(1.06); }
.tab:active svg { transform: scale(.92); }
/* 活动指示条：跟设计稿一致，落在文字下面 */
.tab.on::after {
  content: ""; position: absolute; bottom: 5px; left: 50%;
  transform: translateX(-50%);
  width: 16px; height: 3px; border-radius: 3px;
  background: var(--grad);
}

/* ---------- 底部渐隐遮罩（v2.39） ----------   悬浮 dock 栏是「浮」在内容上的，栏底到屏幕底之间那一条（12px + 安全区）以前是裸的 ——
   学生往下一拉，卡片就从 dock 栏底下透出来（截图反馈的就是这个）。
   这里补一层固定的渐隐玻璃：紧贴屏幕底那一段几乎不透明、往上化开到完全透明，
   滚到下面的内容就是「化掉」而不是「被切一刀」，跟 iOS 内容滚进 tab bar 底下的观感一致。 */
.dock-scrim {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 35;
  height: calc(var(--safe-b) + 82px);
  pointer-events: none;
  background: linear-gradient(to top,
    rgba(232,241,252,.995) 0%,
    rgba(232,241,252,.985) 24%,
    rgba(233,242,252,.92) 44%,
    rgba(235,243,253,.62) 64%,
    rgba(237,245,254,.22) 82%,
    rgba(238,246,254,0) 100%);
  /* v2.40 · 这里原来还有一层 backdrop-filter: blur(10px)。
     它横跨整个屏幕宽度、固定在底部、身后是持续滚动的内容 —— 一帧一帧地重算模糊，
     是「滑动卡顿」里第二贵的一笔。渐变本身已经把内容「化掉」了，模糊是多余的，去掉。 */
}

/* ---------- 卡片（v2.35：冰透玻璃 —— 压低白度 + 高饱和透光 + 冰面反光） ---------- */
.card {
  position: relative; z-index: 0;
  background: var(--card);
  border-radius: var(--r-lg);
  border: 1px solid rgba(255,255,255,.92);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.95),
    inset 0 -10px 22px -16px rgba(110,168,255,.28),
    0 6px 20px rgba(96,130,190,.10);
  /* v2.40 · 去掉 backdrop-filter（见文件底部「滑动性能」那一节）。
     卡片天生就在滚动流里，一页十几张、每张都开模糊 —— 是掉帧的主因。
     --card 的渐变本来是 .78–.90 的近白，抹掉模糊后只把起始白度提到 .94，观感几乎不变。 */
  padding: 16px;
  margin-bottom: 12px;
}
/* 斜向冰面反光：z-index:-1 让它待在内容之下、卡片底色之上 */
.card::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit; pointer-events: none;
  background: linear-gradient(116deg,
    rgba(255,255,255,.85) 0%, rgba(255,255,255,.35) 24%,
    rgba(255,255,255,.10) 50%, rgba(255,255,255,.50) 100%);
}
.card.pad-0 { padding: 0; overflow: hidden; }
.card.tight { padding: 14px; }

.section-title {
  font-size: 15px; font-weight: 700; color: var(--text);
  letter-spacing: -.2px; margin: 20px 4px 8px;
}
.section-title:first-child { margin-top: 8px; }

/* ---------- 头图（v2.36：近白玻璃大卡，内容居中，统计数字青/紫点缀） ---------- */
.hero {
  position: relative; z-index: 0;
  border-radius: var(--r-xl);
  background: linear-gradient(152deg, rgba(255,255,255,.94) 0%, rgba(255,255,255,.86) 48%, rgba(255,255,255,.91) 100%);
  border: 1px solid rgba(255,255,255,.92);
  color: var(--text); padding: 24px 20px;
  margin-bottom: 14px;
  text-align: center;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.95),
    inset 0 -14px 26px -18px rgba(110,168,255,.26),
    0 10px 30px rgba(96,130,190,.12);
  overflow: hidden;
}
.hero::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  pointer-events: none;
  background: linear-gradient(116deg,
    rgba(255,255,255,.85) 0%, rgba(255,255,255,.30) 26%,
    rgba(255,255,255,.06) 52%, rgba(255,255,255,.45) 100%);
}
/* 右上一枚冰川柔光 —— 替代原来的蓝色竖条 */
.hero::after {
  content: ""; position: absolute; right: -70px; top: -90px;
  width: 230px; height: 230px; border-radius: 50%;
  background: radial-gradient(circle, rgba(142,192,255,.30), rgba(151,232,196,.14) 55%, transparent 72%);
}
.hero h2 { font-size: 22px; font-weight: 700; letter-spacing: -.4px; margin-bottom: 6px; position: relative; padding-left: 0; }
.hero p { margin: 0; font-size: 13.5px; color: var(--text-2); position: relative; padding-left: 0; }
.hero .hero-row { display: flex; gap: 26px; justify-content: center; margin-top: 16px; position: relative; padding-left: 0; }
.hero .hero-stat b {
  font-size: 26px; font-weight: 700; letter-spacing: -.5px;
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}
/* 第一个数字 = 青绿，第二个 = 紫（对齐设计稿） */
.hero .hero-stat:nth-child(1) b { background-image: linear-gradient(120deg, #2BC9B4, #38B6FF); }
.hero .hero-stat:nth-child(2) b { background-image: linear-gradient(120deg, #6E8CFF, #9B6BFF); }
.hero .hero-stat:nth-child(3) b { background-image: linear-gradient(120deg, #38B6FF, #8B6BFF); }
.hero .hero-stat span { font-size: 12px; color: var(--text-3); display: block; }

/* ---------- 列表项 ---------- */
.item {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--line-2);
  transition: background .16s;
  text-align: left; width: 100%;
}
.item:last-child { border-bottom: none; }
.item:active { background: var(--bg-2); }
.item-main { flex: 1; min-width: 0; }
.item-title { font-size: 15.5px; font-weight: 550; letter-spacing: -.1px; }
.item-sub { font-size: 13px; color: var(--text-3); margin-top: 3px; }
.item-arrow { color: #C7CCDA; flex: none; }
.item-arrow svg { width: 18px; height: 18px; }

.avatar {
  width: 42px; height: 42px; border-radius: 13px; flex: none;
  display: grid; place-items: center; font-size: 18px; font-weight: 600;
  background: var(--grad-soft); color: var(--primary);
}
.avatar.round { border-radius: 50%; }

/* ---------- 按钮（v2.34：主按钮 = 冰川渐变胶囊） ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  height: 46px; padding: 0 20px;
  border-radius: var(--r-md);
  background: rgba(255,255,255,.72);
  border: 0.5px solid rgba(255,255,255,.85);
  box-shadow: 0 2px 8px rgba(88,120,180,.08);
  color: var(--text);
  font-size: 15.5px; font-weight: 550; letter-spacing: -.1px;
  transition: transform .16s var(--ease), opacity .16s, box-shadow .2s;
  white-space: nowrap;
}
.btn:active { transform: scale(.97); }
.btn[disabled] { opacity: .5; pointer-events: none; }
.btn svg { width: 18px; height: 18px; }

.btn-primary { background: var(--grad); border-color: transparent; color: #fff; box-shadow: var(--sh-btn); border-radius: 999px; }
.btn-success { background: linear-gradient(135deg, #2FD6A8, #0FB58C); border-color: transparent; color: #fff; box-shadow: 0 8px 20px rgba(15,181,140,.30); border-radius: 999px; }
.btn-danger  { background: var(--danger-soft); color: var(--danger); border-color: transparent; }
.btn-ghost   { background: rgba(255,255,255,.72); border: 0.5px solid rgba(255,255,255,.85); box-shadow: 0 2px 8px rgba(88,120,180,.08); }
.btn-block   { display: flex; width: 100%; }
.btn-lg      { height: 54px; font-size: 17px; border-radius: 999px; }
.btn-sm      { height: 36px; padding: 0 14px; font-size: 14px; border-radius: var(--r-sm); }
.btn-round   { border-radius: 999px; }

.btn-row { display: flex; gap: 10px; }
.btn-row > .btn { flex: 1; }

/* ---------- 表单 ---------- */
.field { margin-bottom: 14px; }
.field > label {
  display: block; font-size: 13.5px; font-weight: 550; color: var(--text-2);
  margin: 0 2px 7px;
}
.input, textarea.input, select.input {
  width: 100%; height: 50px; padding: 0 15px;
  background: rgba(255,255,255,.75); border: 1.5px solid rgba(150,178,220,.35);
  border-radius: var(--r-md);
  font-size: 16px; /* 16px 防止 iOS 自动放大 */
  transition: border-color .18s, box-shadow .18s;
  appearance: none; outline: none;
}
textarea.input { height: auto; padding: 13px 15px; line-height: 1.65; resize: vertical; }
.input:focus, textarea.input:focus, select.input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3.5px rgba(78,123,255,.15);
}
select.input {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%239096A8' stroke-width='1.8' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 15px center;
  padding-right: 38px;
}
.input-hint { font-size: 12.5px; color: var(--text-3); margin: 6px 2px 0; line-height: 1.6; }

/* ---------- 药丸 / 标签 ---------- */
.pill {
  display: inline-flex; align-items: center; gap: 4px;
  height: 24px; padding: 0 10px; border-radius: 999px;
  font-size: 12px; font-weight: 550;
  background: rgba(255,255,255,.65); color: var(--text-2);
  border: 0.5px solid rgba(255,255,255,.85);
}
.pill-blue { background: rgba(78,123,255,.12); color: var(--primary); }
.pill-green { background: var(--success-soft); color: var(--success); }
.pill-red { background: var(--danger-soft); color: var(--danger); }
.pill-warn { background: var(--warn-soft); color: var(--warn); }
.pill-grad { background: rgba(255,255,255,.22); color: #fff; }

.chips { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
  height: 34px; padding: 0 14px; border-radius: 999px;
  background: rgba(255,255,255,.70); border: 0.5px solid rgba(255,255,255,.85);
  box-shadow: 0 2px 8px rgba(88,120,180,.08);
  font-size: 13.5px; font-weight: 500; color: var(--text-2);
  display: inline-flex; align-items: center; gap: 5px;
  transition: all .18s var(--ease);
}
.chip:active { transform: scale(.95); }
.chip.on { background: var(--grad); color: #fff; border-color: transparent; box-shadow: var(--sh-btn); }

/* ---------- 进度条 ---------- */
.bar { height: 7px; border-radius: 999px; background: var(--bg-2); overflow: hidden; }
.bar > i { display: block; height: 100%; border-radius: 999px; background: var(--grad); transition: width .5s var(--ease); }
.bar.thin { height: 5px; }
.bar.green > i { background: linear-gradient(135deg, #12B886, #0EA372); }

/* ---------- 空状态 ---------- */
.empty {
  text-align: center; padding: 52px 24px; color: var(--text-3);
}
.empty .ico {
  width: 68px; height: 68px; margin: 0 auto 14px; border-radius: 22px;
  background: var(--grad-soft); display: grid; place-items: center;
}
.empty .ico svg { width: 30px; height: 30px; color: var(--primary); opacity: .75; }
.empty h4 { font-size: 15.5px; color: var(--text-2); margin-bottom: 6px; font-weight: 550; }
.empty p { font-size: 13.5px; margin: 0; line-height: 1.7; }

/* ---------- 加载 ---------- */
.spin {
  width: 18px; height: 18px; border-radius: 50%; flex: none;
  border: 2px solid rgba(255,255,255,.35); border-top-color: #fff;
  animation: spin .7s linear infinite;
}
.spin.dark { border-color: rgba(59,111,246,.22); border-top-color: var(--primary); }
@keyframes spin { to { transform: rotate(360deg); } }

.loading-wrap {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  padding: 70px 0; color: var(--text-3); font-size: 14px;
}

/* ---- 顶部细进度条 ----
   以前每切一页/刷新一次，都先把整屏擦成「加载中…」。那一下白屏才是"卡"的
   主要来源 —— 内容已经在手上了却又被抹掉。现在改成：内容留在原地不动，
   只在顶部走一条细线告诉用户"正在取新数据"。
   只有真的没有任何内容可显示时（第一次进这个页面）才用全屏 loading。 */
.pbar {
  position: fixed; top: 0; left: 0; right: 0; height: 2.5px;
  z-index: 999; pointer-events: none;
  opacity: 0; transition: opacity .18s ease;
}
.pbar.on { opacity: 1; }
.pbar i {
  display: block; height: 100%; width: 30%;
  background: linear-gradient(90deg, rgba(59,111,246,0), var(--primary));
  border-radius: 0 2px 2px 0;
  animation: pbar-run 1.15s ease-in-out infinite;
}
@keyframes pbar-run {
  0%   { transform: translateX(-100%); width: 28%; }
  55%  { width: 58%; }
  100% { transform: translateX(320%); width: 28%; }
}

/* 内容没变时不重播入场动画 —— 否则「先用缓存画、再按新数据画」看起来像闪了一下 */
#app.no-anim .screen { animation: none; }

.skel { background: linear-gradient(90deg, #EEF1F7 25%, #F6F8FC 50%, #EEF1F7 75%); background-size: 200% 100%; animation: skel 1.3s ease-in-out infinite; border-radius: var(--r-md); }
@keyframes skel { from { background-position: 200% 0; } to { background-position: -200% 0; } }

/* ---------- 登录页 ---------- */
.login-wrap {
  min-height: 100vh; min-height: 100dvh;
  display: flex; flex-direction: column;
  padding: calc(var(--safe-t) + 40px) 22px calc(var(--safe-b) + 30px);
  max-width: 460px; margin: 0 auto;
}
.login-logo {
  width: 76px; height: 76px; border-radius: 24px;
  background: var(--grad); box-shadow: 0 12px 28px rgba(59,111,246,.32);
  display: grid; place-items: center; margin-bottom: 22px;
}
.login-logo svg { width: 38px; height: 38px; color: #fff; }
.login-wrap h1 { font-size: 27px; letter-spacing: -.6px; margin-bottom: 8px; }
.login-wrap .lead { color: var(--text-3); font-size: 14.5px; margin-bottom: 30px; }
.seg {
  display: flex; gap: 4px; padding: 4px;
  background: rgba(255,255,255,.65);
  border: 0.5px solid rgba(255,255,255,.85);
  border-radius: 14px; margin-bottom: 20px;
}
.seg button {
  flex: 1; height: 42px; border-radius: 11px; font-size: 15px; font-weight: 550;
  color: var(--text-2); transition: all .22s var(--ease);
}
.seg button.on { background: rgba(255,255,255,.95); color: var(--primary); box-shadow: var(--sh-1); }

/* ---------- 底部弹层 ---------- */
.sheet-wrap { position: fixed; inset: 0; z-index: 100; display: flex; align-items: flex-end; }
.sheet-mask {
  position: absolute; inset: 0; background: rgba(16,20,32,.42);
  backdrop-filter: blur(2px); animation: fade .22s ease;
}
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.sheet {
  position: relative; width: 100%; max-height: 90vh;
  background: rgba(255,255,255,.90);
  backdrop-filter: saturate(170%) blur(30px);
  -webkit-backdrop-filter: saturate(170%) blur(30px);
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  display: flex; flex-direction: column;
  animation: sheet-up .34s var(--ease);
  max-width: 560px; margin: 0 auto;
}
@keyframes sheet-up { from { transform: translateY(100%); } to { transform: none; } }
.sheet-head {
  display: flex; align-items: center; gap: 10px;
  padding: 18px 18px 12px; flex: none;
}
.sheet-head h3 { flex: 1; font-size: 18px; letter-spacing: -.3px; }
.sheet-close {
  width: 32px; height: 32px; border-radius: 50%; flex: none;
  background: var(--bg-2); color: var(--text-2);
  display: grid; place-items: center; transition: transform .16s var(--ease);
}
.sheet-close:active { transform: scale(.9); }
.sheet-close svg { width: 16px; height: 16px; }
.sheet-body {
  padding: 0 18px calc(var(--safe-b) + 20px);
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}

/* ---------- Toast ---------- */
.toast {
  position: fixed; left: 50%; bottom: calc(var(--tab-h) + var(--safe-b) + 46px);
  transform: translate(-50%, 14px);
  background: rgba(28,43,77,.88); color: #fff;
  padding: 11px 18px; border-radius: 999px;
  font-size: 14px; max-width: 84vw; text-align: center;
  opacity: 0; pointer-events: none; z-index: 200;
  transition: opacity .24s, transform .28s var(--ease);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 10px 30px rgba(28,43,77,.25);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }

/* ============================================================
   报听写 · 沉浸式播放页
   ============================================================ */
.play-wrap {
  min-height: 100vh; min-height: 100dvh;
  background: linear-gradient(178deg, #16264D 0%, #101C3E 60%, #0B1330 100%);
  color: #fff;
  display: flex; flex-direction: column;
  padding: calc(var(--safe-t) + 14px) 22px calc(var(--safe-b) + 22px);
  max-width: 560px; margin: 0 auto;
}
.play-head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
.play-head .t { flex: 1; font-size: 15px; opacity: .8; text-align: center; }
.play-close {
  width: 36px; height: 36px; border-radius: 50%; flex: none;
  background: rgba(255,255,255,.10); color: #fff;
  display: grid; place-items: center;
}
.play-close svg { width: 18px; height: 18px; }

.play-progress { margin: 12px 0 6px; }
.play-progress .bar { background: rgba(255,255,255,.14); height: 5px; }
.play-progress .bar > i { background: linear-gradient(90deg, #7BA0FF, #B08CFF); }
.play-counter {
  display: flex; justify-content: space-between;
  font-size: 13px; opacity: .62; margin-top: 9px;
}

.play-stage { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 10px 0; }
.play-ring {
  position: relative; width: 240px; height: 240px;
  display: grid; place-items: center;
}
.play-ring .ring-svg { position: absolute; inset: 0; transform: rotate(-90deg); }
.play-ring .ring-svg circle { fill: none; stroke-width: 4; }
.play-ring .track { stroke: rgba(255,255,255,.10); }
.play-ring .prog { stroke: url(#ringGrad); stroke-linecap: round; transition: stroke-dashoffset .3s linear; }
.play-core {
  width: 176px; height: 176px; border-radius: 50%;
  background: radial-gradient(circle at 50% 34%, rgba(123,160,255,.28), rgba(108,76,255,.12) 62%, rgba(255,255,255,.02));
  border: 1px solid rgba(255,255,255,.10);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
  backdrop-filter: blur(6px);
  transition: transform .3s var(--ease);
}
.play-core.speaking { transform: scale(1.035); }

/* 跟读卡（v2.52.0 重做）—— 跟读那几秒盖在圆环上。
   ⚠️ 必须在 .play-ring 里面（它是 position:relative），这里只管自己铺满。
   老师原话「跟读的界面太丑了」。原来丑在四件事上，一条一条改掉：
     ① emoji 当图标（🎤🔴✅🔇🔉🔊）→ 换成单色线性 SVG，颜色跟着状态走。
        iOS 上 emoji 是彩色大图案，和整个 app 的线性图标不是一路；
     ② 深色块 + `backdrop-filter: blur(10px)` 压在渐变环上 → 糊成一坨，
        改成实色渐变面 + 细描边（顺带省掉一次模糊，便宜手机不卡）；
     ③ 只有一根 4px 细条，录音时没有任何「正在听」的动静
        → 加一层扩散脉冲环 + 7 根跳动的音量柱；
     ④ 倒数塞在副标题里（「… · 3」）→ 单独一个大数字压在图标上。 */
.ra-card {
  position: absolute; inset: -6px; z-index: 6;
  display: none; flex-direction: column; align-items: center; justify-content: center;
  padding: 14px 18px 18px; border-radius: 30px; text-align: center; color: #fff;
  /* ⚠️ 必须**不透明**（v2.52.0）：原来 .97 的透明度让底下那个 176px 的亮圆盘透上来，
      卡片里会出现几圈若隐若现的同心圆，看着像渲染没擦干净。 */
  background: linear-gradient(180deg, #1B2138 0%, #0E1120 100%);
  border: 1px solid rgba(255,255,255,.09);
  box-shadow: 0 16px 44px rgba(0,0,0,.42);
}
.ra-card.on { display: flex; }

/* 顶行：左边状态小签，右边「第几轮」 */
.ra-hd { width: 100%; display: flex; align-items: center; justify-content: space-between; height: 20px; }
.ra-tag {
  font-size: 10.5px; font-weight: 700; letter-spacing: .06em;
  padding: 3px 9px; border-radius: 999px;
  background: rgba(123,160,255,.16); color: #B9C7FF;
}
.ra-round { font-size: 11px; color: rgba(255,255,255,.42); font-variant-numeric: tabular-nums; }

/* 中间那颗「头」：圆底 + 图标（倒数时换成大数字） */
.ra-vis { position: relative; width: 82px; height: 82px; margin: 8px 0 0; display: grid; place-items: center; }
.ra-vis::before {
  content: ''; position: absolute; inset: 0; border-radius: 50%;
  background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.10);
}
.ra-pulse {
  position: absolute; inset: 0; border-radius: 50%; pointer-events: none;
  border: 1.5px solid currentColor; opacity: 0;
}
.ra-ico { position: relative; z-index: 1; width: 34px; height: 34px; display: grid; place-items: center; color: #9FB4FF; }
.ra-ico svg { width: 100%; height: 100%; display: block; }
.ra-count {
  position: absolute; z-index: 2; display: none; line-height: 1;
  font-size: 40px; font-weight: 700; font-variant-numeric: tabular-nums; color: #fff;
}
.has-count .ra-count { display: block; }
/* ⚠️ 倒数那三秒**把麦克风图标整块藏掉**（v2.55.1）。
   老师原话：「录音前的倒计时和背景的话筒图案重合了，把录音前倒计时的背景喇叭删掉」。
   以前是 opacity:.16 淡出 —— 大数字压在淡麦克风上，两个形状叠在一起就是重影。
   现在 display:none，圆底里只留一个干净的大数字（字号同步从 34 提到 40）。 */
.has-count .ra-ico { display: none; }

/* 音量柱。⚠️ 不录音时**透明但占位**（不是 display:none）——
   否则卡片的标题会随状态上下跳一格，看着像界面在抖。
   原来留着一排灰点：截图里读起来像一串省略号，是多余的装饰。 */
.ra-bars { display: flex; align-items: center; justify-content: center; gap: 3px; height: 14px; margin-top: 9px; }
.ra-bars i { width: 3px; height: 4px; border-radius: 2px; background: #FF9B9B; opacity: 0; }
.st-rec .ra-bars i { opacity: 1; animation: raBar .62s ease-in-out infinite alternate; }
.st-rec .ra-bars i:nth-child(1) { animation-delay: -.36s; }
.st-rec .ra-bars i:nth-child(2) { animation-delay: -.18s; }
.st-rec .ra-bars i:nth-child(3) { animation-delay: -.05s; }
.st-rec .ra-bars i:nth-child(4) { animation-delay: -.42s; }
.st-rec .ra-bars i:nth-child(5) { animation-delay: -.12s; }
.st-rec .ra-bars i:nth-child(6) { animation-delay: -.28s; }
.st-rec .ra-bars i:nth-child(7) { animation-delay: -.08s; }
@keyframes raBar { from { height: 3px; } to { height: 14px; } }

/* 状态配色。⚠️ 只有这四种颜色，别再加 —— 一多就分不清「在听」和「没读好」 */
.st-count .ra-ico, .st-ready .ra-ico, .st-replay .ra-ico { color: #9FB4FF; }
.st-rec .ra-ico { color: #FF7F7F; }
.st-ok .ra-ico { color: #5BE0A0; }
.st-warn .ra-ico { color: #FFC46B; }
.st-rec .ra-pulse { color: rgba(255,127,127,.55); animation: raPulse 1.6s ease-out infinite; }
.ra-card.st-rec { border-color: rgba(255,127,127,.30); }
.ra-card.st-ok { border-color: rgba(91,224,160,.30); }
.ra-card.st-warn { border-color: rgba(255,196,107,.30); }
/* 状态小签跟着变色 —— 只改图标不改签，会出现「红麦克风 + 蓝底标签」的割裂感 */
.st-rec .ra-tag { background: rgba(255,127,127,.16); color: #FFB3B3; }
.st-ok .ra-tag { background: rgba(91,224,160,.16); color: #93EDC4; }
.st-warn .ra-tag { background: rgba(255,196,107,.16); color: #FFD79A; }
@keyframes raPulse {
  0% { transform: scale(1); opacity: .7; }
  100% { transform: scale(1.45); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .st-rec .ra-pulse, .st-rec .ra-bars i { animation: none; }
}

/* 大号参考分（v2.54.0）：默认收起，`raPaint(..., { score })` 才出现。
   老师原话「分数要放大点，现在太小了」—— 以前塞在 .ra-s 那行 12.5px 的小字里。
   ⚠️ 旁边那个「参考分」是**标签**，不是句子：口径（这是参考、不是成绩）没变，
      只是不再占一整行；完整那句话留在结果页的「跟读小结」里。 */
.ra-score {
  display: none; margin: 0; flex-direction: column;
  align-items: center; gap: 3px;
}
.ra-score.on { display: flex; }
.ra-score b {
  font-size: 54px; font-weight: 700; line-height: .92; letter-spacing: -1.5px;
  color: #7FE7BE; font-variant-numeric: tabular-nums;
}
.ra-score span { font-size: 11px; color: rgba(255,255,255,.5); letter-spacing: .14em; }
/* 读成别的词用暖色，别用红色 —— 这不是「错题」，只是「听起来不一样」 */
.ra-score.is-low b { color: #FFC46B; }
/* 让位：有分数时把中间那个图标圆和音量柱收起来，卡片才装得下 54px 的号 */
.ra-card.is-score .ra-vis, .ra-card.is-score .ra-bars { display: none; }
/* ⚠️ 副标题和进度条也要一起收掉：它们即使空着 / opacity:0 仍占 19+15px，
   会把「读到了 + 92」这一块整体顶到卡片上半部，下面空一大片（看图才发现）。 */
.ra-card.is-score .ra-s, .ra-card.is-score .ra-bar { display: none; }
/* 顶行（状态签 + 第几轮）改成绝对定位：它一脱离文档流，卡片里就只剩那枚大数字，
   justify-content:center 才能真正把它放在正中间。 */
.ra-card.is-score .ra-hd { position: absolute; top: 12px; left: 18px; right: 18px; width: auto; }
.ra-card.is-score .ra-t { margin-top: 6px; }
/* keep-sub（v2.55.1）：分数低、要求重录那一屏要把「再读一遍，读清楚一点」留下。
   ⚠️ 只在 opt.keepSub 时才放出来 —— 平时「读到了 + 92」不需要说明，
      留着那 19px 会把大数字顶高、下面空一片。 */
.ra-card.is-score.keep-sub .ra-s { display: block; }

/* 撒花（v2.55.1）：分数过关时飘一阵彩纸。老师说「分数高通过的话给个撒花的动效」。
   ⚠️ 只撒在**跟读卡内部**（inset:0 + overflow:hidden + border-radius:inherit）：
      铺满全屏会挡住「下一条」按钮，孩子这会儿还在做听写。
   ⚠️ 位移走 CSS 变量 + 一个 @keyframes，不逐帧写 JS（26 片也不卡）。 */
.ra-fx {
  position: absolute; inset: 0; z-index: 1; overflow: hidden;
  border-radius: inherit; pointer-events: none;
}
/* ⚠️ 分数 / 说明要压在碎片**上面**（v2.55.1 看图才发现的）：
   碎片是 z-index:1 的定位层，会盖在普通文字上 —— 一片彩纸正好落在「参考分」的
   「参」字上，那个字就没了。给文字抬到 2，碎片从数字和文字**背后**过。
   ⚠️ 只抬有分数的这一屏：没分数时那几行文字本来就不和碎片重叠。 */
.ra-card.is-score .ra-score,
.ra-card.is-score .ra-t,
.ra-card.is-score .ra-s { position: relative; z-index: 2; }
.ra-fx > i {
  position: absolute; display: block; opacity: 0;
  animation-name: raFall; animation-timing-function: cubic-bezier(.22,.7,.3,1);
  animation-fill-mode: forwards;
}
@keyframes raFall {
  0%   { opacity: 1; transform: translate3d(0, 0, 0) rotate(0deg); }
  70%  { opacity: 1; }
  100% { opacity: 0; transform: translate3d(var(--dx, 0px), 230px, 0) rotate(var(--rot, 180deg)); }
}
/* 系统开了「减少动态效果」就一片都不放（JS 里也挡了一道） */
@media (prefers-reduced-motion: reduce) {
  .ra-fx { display: none; }
}

.ra-t { margin-top: 12px; font-size: 15px; font-weight: 600; line-height: 1.35; }
/* ⚠️ 这里装的是**一整句说明**，不是数字 —— 别写回 21px 粗体（一句长话会被撑爆）。 */
.ra-s {
  margin-top: 5px; min-height: 19px; font-size: 12.5px; font-weight: 400;
  line-height: 1.5; color: rgba(255,255,255,.58); font-variant-numeric: tabular-nums;
}

/* 跟读：麦克风用不了时的**常驻**说明条（v2.51.1）。
   以前只闪一句 toast，家长根本看不见，只会说「录音没成功」。
   ⚠️ 用暖色，和「出错」区分开 —— 这不是错误，是「这次不录，成绩照算」。 */
.ra-warn {
  margin: 0 16px 10px; padding: 10px 12px; border-radius: 12px;
  background: rgba(255, 190, 90, .13); border: 1px solid rgba(255, 190, 90, .32);
  color: #FFD8A8; font-size: 12.5px; line-height: 1.6;
}
/* ⚠️ 只认**直接子元素**的那个 `<b>` 当标题。
   一开始写的是 `.ra-warn b`，结果说明正文里的 `<b>https</b>` 也被变成块级 ——
   一句话被拆成三段，截图里看得清清楚楚。正文要强调请用 `<em>`。 */
.ra-warn > b { display: block; margin-bottom: 2px; font-size: 13px; color: #FFE3BC; }
.ra-warn em { font-style: normal; font-weight: 700; color: #FFF1DC; }
.ra-warn span { opacity: .92; }

/* ---- 麦克风被拒绝时的说明条（v2.55.2）----
   老师原话「学生点了拒绝，加一个提醒」。这一版把提醒从「一句话」变成「一句话 + 一个按钮」：
   被拒绝之后浏览器**再也不会弹框**了，孩子唯一能做的就是去设置里改，
   所以按钮必须留着 —— 改完回来按它一下就能接上。
   ⚠️ 拒绝用**红**、其他情况（被占用）继续用暖黄：这两件事的严重程度不一样，
      一个是「你得去改设置」，一个只是「等一下就好」。 */
.ra-warn.is-deny {
  background: rgba(255, 110, 110, .13); border-color: rgba(255, 110, 110, .34);
  color: #FFD3D3;
}
.ra-warn.is-deny > b { color: #FFC0C0; }
.ra-warn.is-deny em { color: #FFE4E4; }
/* 「还没要过权限」那一档（v2.55.3）：用蓝，不用红 —— 这时候什么都没坏，
   只是「点了这颗按钮手机才会问你」。红色会让孩子以为出错了。 */
.ra-warn.is-ask {
  background: rgba(90, 150, 255, .14); border-color: rgba(90, 150, 255, .34);
  color: #D6E4FF;
}
.ra-warn.is-ask > b { color: #E4EEFF; }
.ra-warn.is-ask em { color: #fff; }

/* 分步指引（v2.55.3）：老师原话「有的学生可能不会更改权限」。
   ⚠️ 三档环境（iPhone Safari / iPhone 微信 / 安卓）各给各的步骤，
      所以这里必须是**有序列表**，不能压成一句话。
   ⚠️ 序号自己画（counter），别用 <ol> 默认的 1. 2. 3. —— 字号对不齐会很难看。 */
.ra-warn-sub { display: block; margin-bottom: 8px; opacity: .92; }
.ra-art {
  display: block; width: 100%; max-width: 260px; height: auto;
  margin: 0 0 8px; color: #FFD3D3;
}
.ra-steps {
  list-style: none; counter-reset: ras; margin: 0 0 8px; padding: 0;
}
.ra-steps li {
  counter-increment: ras; position: relative;
  padding-left: 22px; margin-bottom: 5px; line-height: 1.5;
}
.ra-steps li::before {
  content: counter(ras);
  position: absolute; left: 0; top: 1px;
  width: 16px; height: 16px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 10.5px; font-weight: 700;
  background: currentColor; color: #2A1420;
  opacity: .85;
}
.ra-steps em { font-style: normal; font-weight: 700; }
.ra-warn-tail { display: block; opacity: .85; font-size: 12px; }
/* 按钮 + 「正在等麦克风…」，两条并排；窄屏自动换行 */
.ra-warn-act {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 8px;
}
.ra-warn-btn {
  flex: none; height: 30px; padding: 0 12px; border-radius: 999px;
  border: 1px solid rgba(255,255,255,.22); background: rgba(255,255,255,.10);
  color: #fff; font-size: 12.5px; font-weight: 600; line-height: 28px;
}
.ra-warn-btn:active { background: rgba(255,255,255,.18); }
.ra-warn-btn:disabled { opacity: .55; }
/* 「打开麦克风」是这一屏唯一该点的东西 —— 给它实心蓝，别和次要按钮一个样 */
.ra-warn-btn.is-primary {
  border-color: transparent; background: #3E7BFA; color: #fff;
  box-shadow: 0 3px 10px rgba(62, 123, 250, .38);
}
.ra-warn-btn.is-primary:active { background: #2F6AE8; }
/* ⚠️ 用 `<i>` 不用 `<span>`：上面 `.ra-warn span { opacity: .92 }` 会一起吃掉它，
      而且正文里那个 span 是块级的，混在一起排版会乱。 */
.ra-warn-dot {
  font-style: normal; font-size: 11.5px; opacity: .8;
  display: inline-flex; align-items: center; gap: 6px;
}
.ra-warn-dot::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; opacity: .85;
  animation: raWaitBlink 1.1s ease-in-out infinite;
}
@keyframes raWaitBlink { 0%, 100% { opacity: .18; } 50% { opacity: .95; } }
/* 按了「再试一次」还是不行 → 抖一下，让他知道**这一下真的试过了**。
   ⚠️ 光换文案不够：孩子根本不会重读那句话。 */
.ra-warn.shake { animation: raShake .34s ease-in-out; }
@keyframes raShake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-4px); }
  50% { transform: translateX(4px); }
  80% { transform: translateX(-2px); }
}
@media (prefers-reduced-motion: reduce) {
  .ra-warn-dot::before { animation: none; }
  .ra-warn.shake { animation: none; }
}
/* 底部细条。⚠️ 必须**内缩 + 圆头**（v2.52.0）：原来 100% 宽、贴着卡片下沿，
   满格时看着就是卡片底下多了一条蓝线，像边框没画完。 */
.ra-bar { margin-top: 12px; width: 78%; max-width: 180px; height: 3px; border-radius: 3px;
  background: rgba(255,255,255,.10); overflow: hidden; }
.ra-bar > i { display: block; height: 100%; width: 0; border-radius: 3px;
  background: linear-gradient(90deg, #7BA0FF, #B08CFF); transition: width .1s linear; }
/* ⚠️ 「读到了 / 没读好 / 再听一遍」这三态**不画进度条**（v2.52.0）：
   满格的一根彩色线横在标题下面就成了一条多余的横线。
   只留「准备倒数 / 正在录」两态 —— 那才是真在走进度。 */
.ra-card.st-ok .ra-bar, .ra-card.st-warn .ra-bar, .ra-card.st-replay .ra-bar { opacity: 0; }
.ra-card.st-ok .ra-bar > i { background: linear-gradient(90deg, #4FD8A0, #7FE7BE); }
.ra-card.st-warn .ra-bar > i { background: linear-gradient(90deg, #FFB05C, #FFD08A); }

/* 跟读卡亮着的时候，把那排「播放波形」藏掉 —— 它是给「正在播语音」用的，
   跟读时留在卡片下面只会像多出来一个元素。 */
.play-stage .wave { transition: opacity .2s ease; }
body.ra-on .play-stage .wave { opacity: 0; }
.play-core .hint { font-size: 12.5px; letter-spacing: 3px; opacity: .55; text-transform: uppercase; }
.play-core .big { font-size: 46px; font-weight: 300; letter-spacing: -1px; line-height: 1; }
.play-core .sub { font-size: 13.5px; opacity: .6; }

/* 波形 */
.wave { display: flex; align-items: center; gap: 4px; height: 30px; margin-top: 20px; }
.wave i {
  width: 4px; border-radius: 3px; background: linear-gradient(180deg, #9DB6FF, #7C5CFF);
  animation: wave 1.05s ease-in-out infinite; opacity: .35;
}
.wave.on i { opacity: 1; }
.wave i:nth-child(1){height:10px;animation-delay:0s}
.wave i:nth-child(2){height:20px;animation-delay:.09s}
.wave i:nth-child(3){height:28px;animation-delay:.18s}
.wave i:nth-child(4){height:16px;animation-delay:.27s}
.wave i:nth-child(5){height:24px;animation-delay:.36s}
.wave i:nth-child(6){height:12px;animation-delay:.45s}
.wave i:nth-child(7){height:20px;animation-delay:.54s}
@keyframes wave { 0%,100% { transform: scaleY(.42); } 50% { transform: scaleY(1); } }
.wave:not(.on) i { animation-play-state: paused; transform: scaleY(.42); }

.play-tip {
  margin-top: 26px; font-size: 13.5px; opacity: .5; text-align: center; line-height: 1.7;
  min-height: 44px; padding: 0 10px;
}

.play-controls { display: flex; align-items: center; justify-content: center; gap: 16px; margin-top: 8px; }
.pc {
  width: 56px; height: 56px; border-radius: 50%;
  background: rgba(255,255,255,.10); color: #fff;
  display: grid; place-items: center;
  transition: transform .16s var(--ease), background .2s;
}
.pc:active { transform: scale(.9); }
.pc svg { width: 22px; height: 22px; }
.pc.big { width: 68px; height: 68px; background: rgba(255,255,255,.16); }
.pc.big svg { width: 27px; height: 27px; }
/* ⚠️ I.play / I.pause 是**蓝色渐变填充**的图标，落在蓝色主控钮上会糊成一团看不见。
   主控钮上的图标必须统一刷成白色（presentation attribute 的 fill 优先级低于 CSS，这里能盖住）。 */
.pc.primary svg, .pc.primary svg * { fill: #fff; }
.pc.primary { background: linear-gradient(135deg, #4667D6, #6349C9); box-shadow: inset 0 1px 0 rgba(255,255,255,.20), 0 5px 14px rgba(20,32,80,.40); }
.pc.ghost { background: transparent; opacity: .55; }

.play-bottom { margin-top: 16px; }

/* ---------- 对答案 ---------- */
.check-row {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 15px; border-radius: var(--r-md);
  background: var(--card); box-shadow: var(--sh-1); margin-bottom: 9px;
}
.check-row.right { background: var(--success-soft); box-shadow: none; }
.check-row.wrong { background: var(--danger-soft); box-shadow: none; }
.check-mark {
  width: 30px; height: 30px; border-radius: 50%; flex: none;
  display: grid; place-items: center; background: var(--bg-2); color: var(--text-3);
  font-size: 15px; font-weight: 700;
}
.check-row.right .check-mark { background: var(--success); color: #fff; }
.check-row.wrong .check-mark { background: var(--danger); color: #fff; }
/* ⚠️ I.check 这类图标**没有自带尺寸**，裸放会撑满整个容器 —— 必须在这里定死。 */
.check-mark svg { width: 16px; height: 16px; }
.check-main { flex: 1; min-width: 0; }
.check-en { font-size: 17px; font-weight: 600; letter-spacing: -.2px; font-family: ui-monospace, "SF Mono", Menlo, monospace; }
.check-zh { font-size: 13px; color: var(--text-3); margin-top: 1px; }
.check-btns { display: flex; gap: 6px; flex: none; }
.tick {
  width: 40px; height: 40px; border-radius: 12px; flex: none;
  display: grid; place-items: center; background: var(--bg-2); color: var(--text-3);
  transition: all .18s var(--ease);
}
.tick:active { transform: scale(.9); }
.tick.yes.on { background: var(--success); color: #fff; }
.tick.no.on { background: var(--danger); color: #fff; }
.tick svg { width: 20px; height: 20px; }

/* ---------- 结果页 ---------- */
.score-hero {
  border-radius: var(--r-xl); padding: 24px 20px;
  background: var(--grad); color: #fff; text-align: center;
  box-shadow: 0 12px 30px rgba(59,111,246,.28); margin-bottom: 14px;
  position: relative; overflow: hidden;
}
.score-hero::after {
  content: ""; position: absolute; right: -50px; top: -60px;
  width: 180px; height: 180px; border-radius: 50%; background: rgba(255,255,255,.12);
}
.score-num { font-size: 54px; font-weight: 700; line-height: 1; letter-spacing: -2px; position: relative; }
.score-num small { font-size: 19px; font-weight: 500; opacity: .7; letter-spacing: 0; }
.score-label { font-size: 13.5px; opacity: .85; margin-top: 6px; position: relative; }

/* ---- 积分提示卡（听写结果页 / 作文批改结果页）----
   固定样式的一份小卡：左边一个圆底图标，右边两行字。
   ⚠️ 图标尺寸由 CSS 定死 —— I.trophy / I.spark 这类图标没有自带尺寸，
      裸放进 flex 里会撑满整行（这个坑在留言板那块踩过一次）。 */
.pt-card { display: flex; align-items: center; gap: 12px; padding: 14px 16px; }
.pt-card .pt-ico {
  flex: 0 0 auto; width: 38px; height: 38px; border-radius: 12px;
  display: flex; align-items: center; justify-content: center;
  background: var(--grad-soft); color: var(--primary);
}
.pt-card .pt-ico svg { width: 20px; height: 20px; }
.pt-card .pt-body { flex: 1 1 auto; min-width: 0; }
.pt-card.ok { background: var(--success-soft); box-shadow: none; }
.pt-card.ok .pt-ico { background: rgba(255,255,255,.65); color: var(--success); }
.pt-card.ok .pt-body .f15 { color: var(--success); }

.dim { margin-bottom: 15px; }
.dim:last-child { margin-bottom: 0; }
.dim-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 7px; }
.dim-head .n { font-size: 14.5px; font-weight: 550; }
.dim-head .s { font-size: 13px; color: var(--text-3); font-variant-numeric: tabular-nums; }
.dim-comment { font-size: 13px; color: var(--text-3); margin-top: 6px; line-height: 1.65; }

.err-item {
  padding: 13px 0; border-bottom: 1px solid var(--line-2);
}
.err-item:last-child { border-bottom: none; padding-bottom: 0; }
.err-item:first-child { padding-top: 0; }
.err-type { font-size: 11.5px; }
.err-orig {
  font-size: 14.5px; color: var(--danger); text-decoration: line-through;
  text-decoration-color: rgba(239,68,68,.5); font-family: ui-monospace, Menlo, monospace;
  word-break: break-word;
}
.err-corr {
  font-size: 15px; color: var(--success); font-weight: 550;
  font-family: ui-monospace, Menlo, monospace; margin-top: 3px; word-break: break-word;
}
.err-why { font-size: 13.5px; color: var(--text-2); margin-top: 8px; line-height: 1.7; }

/* ---------- 写作页：拍照 → 识别 → 提交（v2.50.0 三步一个框） ---------- */
.ew-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.ew-tip { font-size: 13px; color: var(--text-3); line-height: 1.7; margin-top: 6px; }
.ew-step {
  display: flex; gap: 11px; align-items: flex-start;
  margin-top: 14px; padding-top: 14px;
  border-top: 1px dashed var(--line-2);
  opacity: .45; transition: opacity .18s var(--ease-app);
}
.ew-step:first-of-type { border-top: none; }
.ew-step.on, .ew-step.done { opacity: 1; }
.ew-no {
  flex: none; width: 24px; height: 24px; border-radius: 50%;
  display: grid; place-items: center; font-size: 13px; font-weight: 700;
  background: var(--bg-2); color: var(--text-3);
}
.ew-step.on .ew-no { background: var(--primary); color: #fff; }
.ew-step.done .ew-no { background: var(--success-soft); }
.ew-step.done .ew-no svg { width: 14px; height: 14px; }
.ew-body { flex: 1; min-width: 0; }
.ew-t { font-size: 14.5px; font-weight: 600; line-height: 1.4; }
.ew-pick {
  margin-top: 10px; padding: 22px 14px; text-align: center; cursor: pointer;
  border: 1.5px dashed var(--line); border-radius: 14px; background: var(--bg-2);
  transition: background .15s var(--ease-app);
}
.ew-pick:active { background: var(--bg-3, var(--bg-2)); }
.ew-pick-ico { width: 46px; height: 46px; margin: 0 auto 8px; }
.ew-pick-ico svg { width: 100%; height: 100%; }

.essay-box {
  font-size: 14.5px; line-height: 1.9; white-space: pre-wrap; word-break: break-word;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
}
.essay-box.revised { color: #0B7A57; }
.essay-box.upgraded { color: var(--text); }
/* 拔高版里被换过句型的地方 */
.essay-box .hl {
  color: var(--primary); font-weight: 650;
  background: color-mix(in srgb, var(--primary) 13%, transparent);
  border-radius: 5px; padding: 0 3px;
}

/* ---------- 结果页：拔高示范（原句 → 进阶句型） ---------- */
.upg-top { display: flex; gap: 6px; flex-wrap: wrap; }
.upg-orig {
  font-size: 14px; line-height: 1.65; color: var(--text-3);
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  word-break: break-word;
}

.li { display: flex; gap: 9px; font-size: 14.5px; line-height: 1.75; margin-bottom: 9px; }
.li:last-child { margin-bottom: 0; }
.li .dot { flex: none; color: var(--primary); font-weight: 700; }
.li.ok .dot { color: var(--success); }

/* ---------- 选词 ---------- */
.pick-grid { display: flex; flex-direction: column; gap: 8px; }
.pick {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 14px; border-radius: var(--r-md);
  background: var(--card); box-shadow: var(--sh-1);
  /* ⚠️ 别写 `all`：一个单元可能上千条 .pick，`transition: all` 会让每次重画
     都去给上千个元素登记全部属性的过渡（v2.50.1 修卡顿时改掉的）。 */
  transition: background .16s var(--ease), color .16s var(--ease),
              box-shadow .16s var(--ease), transform .12s var(--ease);
  text-align: left; width: 100%;
  /* ★ 屏外的那几百条浏览器先不渲染 —— 滚动/点击都轻很多（不支持的浏览器自动忽略这一行） */
  content-visibility: auto;
  contain-intrinsic-size: auto 56px;
}
.pick:active { transform: scale(.985); }
.pick.on { background: var(--grad); color: #fff; box-shadow: var(--sh-btn); }
.pick .cb {
  width: 24px; height: 24px; border-radius: 8px; flex: none;
  border: 2px solid #D8DDEA; display: grid; place-items: center;
  transition: all .18s var(--ease);
}
.pick.on .cb { background: rgba(255,255,255,.28); border-color: transparent; }
.pick .cb svg { width: 14px; height: 14px; color: #fff; opacity: 0; transition: opacity .18s; }
.pick.on .cb svg { opacity: 1; }
.pick .en { font-size: 16px; font-weight: 600; letter-spacing: -.1px; font-family: ui-monospace, Menlo, monospace; }
.pick .zh { font-size: 12.5px; color: var(--text-3); margin-top: 1px; }
.pick.on .zh { color: rgba(255,255,255,.82); }
/* ⚠️ 词性小字（pron./adj.）在选中态必须一起变白：全局有条 `.pos { color: 灰 }`，
   不改的话蓝底上那截灰字几乎看不见（v2.49.0 截图里抓到的）。 */
.pick.on .pos { color: rgba(255,255,255,.72); opacity: 1; }
.pick .pos { font-size: 11px; opacity: .7; margin-left: 5px; font-family: inherit; }
/* 「还有 N 条 · 显示更多」（v2.50.1：选词表分块渲染，首屏只画 120 条） */
.pick-more { margin: 0 14px 14px; width: calc(100% - 28px); }

/* ---------- 通用工具类 ---------- */
.row { display: flex; align-items: center; gap: 10px; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.wrap { flex-wrap: wrap; }
.grow { flex: 1; min-width: 0; }
.mt4 { margin-top: 4px; } .mt8 { margin-top: 8px; } .mt12 { margin-top: 12px; }
.mt16 { margin-top: 16px; } .mt20 { margin-top: 20px; } .mt24 { margin-top: 24px; }
/* ⚠️ mb4 / mb6 / mb10 / mb14 / mb18 是 v2.45.1 补上的：
   页面里早就写了这五个类（19 处），但这里一直**没定义** —— 也就是说那些间距
   一直是**静默失效**的：标题紧贴着下面那行说明，看着像漏了样式。
   补上之后是「按作者原本的意图」生效，不是新加了一个设计。 */
.mb0 { margin-bottom: 0; } .mb4 { margin-bottom: 4px; } .mb6 { margin-bottom: 6px; }
.mb8 { margin-bottom: 8px; } .mb10 { margin-bottom: 10px; } .mb12 { margin-bottom: 12px; }
.mb14 { margin-bottom: 14px; } .mb18 { margin-bottom: 18px; }
.t2 { color: var(--text-2); } .t3 { color: var(--text-3); }
.f13 { font-size: 13px; } .f14 { font-size: 14px; } .f15 { font-size: 15px; }
.bold { font-weight: 600; }
.center { text-align: center; }
.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; }
.divider { height: 1px; background: var(--line-2); margin: 14px 0; }
.hide { display: none !important; }

/* ============================================================
   复习页 · 对答案页 · 底部操作条
   ============================================================ */

/* ---------- 底部固定操作条（v2.34：悬浮磨砂胶囊） ---------- */
.bottom-bar {
  position: fixed; bottom: calc(var(--safe-b) + 12px); z-index: 45;
  /* 与 .tabbar 完全同一套对齐规则（见上面的注释） */
  left: var(--gutter); right: var(--gutter);
  max-width: calc(var(--content-max) - var(--gutter) * 2);
  margin-left: auto; margin-right: auto;
  border-radius: 24px;
  padding: 12px var(--gutter) 14px;
  /* v2.40 · 同 .tabbar：固定条上的 backdrop-filter 每帧都要重算，去掉。 */
  background: linear-gradient(160deg, rgba(255,255,255,.97), rgba(252,254,255,.93));
  box-shadow:
    0 12px 34px rgba(96,130,190,.16),
    inset 0 1px 0 rgba(255,255,255,.95);
}
/* 条本身已经被 max-width 收窄并跟内容一起居中了，里面的按钮撑满即可。
   （原来写死 528px，宽屏上比条的内宽还窄，会缩在中间对不齐。） */
.bottom-bar > * { max-width: 100%; margin-left: auto; margin-right: auto; }
.bb-progress { font-size: 12.5px; color: var(--text-3); text-align: center; margin-bottom: 9px; }
.bb-row { display: flex; gap: 8px; align-items: center; }
.screen.with-bar { padding-bottom: calc(var(--safe-b) + 150px); }

/* ---------- 内容类型标签 ---------- */
.lv {
  display: inline-block; font-size: 11.5px; font-weight: 600;
  padding: 3px 9px; border-radius: 999px; letter-spacing: .2px;
}
.lv-word     { background: rgba(78,123,255,.12); color: var(--primary); }
.lv-phrase   { background: #F3EFFF; color: #6C4CFF; }
.lv-sentence { background: #E7F7F1; color: #0EA372; }
/* 「背单词」这枚小标（v2.48.0）：挂在任务标题后面，让学生一眼看出
   「这条是刷词的，不用写字、不用拍本子」。和上面三种内容类型不是一回事，所以单开一格。 */
.lv-quiz     { background: #FFF3E2; color: #C77A16; }

/* 词性小标（n. / v. / adj.）—— 通用一套。
   ⚠️ 不写这一条的话，跨单元卡片、我的记录页这些**新地方**的 <span class="pos">
   会按父元素的字号显示，跟在单词后面连成 "bottlen." 这种看不清的东西。
   .pick .pos / .rv-en .pos / .mk-en .pos 各自有更具体的规则，会盖掉这里。 */
.pos { font-size: 11px; color: var(--text-3); font-weight: 500; margin-left: 5px; }

/* 音标（v2.59.0）—— 紧跟在单词后面的一小截 IPA。短语和句子不带（服务端直接给空串）。
   ⚠️ 字体**不能继承父元素**：`.pick .en` 是等宽字体，而 IPA 里的 ʃ ʒ ɹ ɚ θ ð
      在不少等宽字体里根本没字形，会变成一排方框。 */
.ph {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  font-size: 12px; font-weight: 500; color: var(--text-3);
  margin-left: 6px; letter-spacing: 0; white-space: nowrap;
}
.pick.on .ph { color: rgba(255, 255, 255, .8); }

/* 发布页每条的「用过 N 次」（v2.59.0）—— 只标**用过**的。
   一个单元动辄几百条，每行再挂一个「没用过」会把单词本身盖掉；
   老师在这里真正想找的是「哪些已经考过了」。 */
.use-mini {
  flex: none; font-size: 11px; font-weight: 600; line-height: 1.6;
  padding: 0 6px; border-radius: 999px; letter-spacing: 0;
  background: var(--bg-2); color: var(--text-3);
}
.pick.on .use-mini { background: rgba(255, 255, 255, .22); color: #fff; }

/* ---------- 复习页 ---------- */
.rv-hero {
  border-radius: var(--r-xl); padding: 20px;
  background: var(--grad); color: #fff;
  box-shadow: 0 12px 30px rgba(59,111,246,.26);
  margin-bottom: 14px; position: relative; overflow: hidden;
}
.rv-hero::after {
  content: ""; position: absolute; right: -40px; top: -70px;
  width: 170px; height: 170px; border-radius: 50%; background: rgba(255,255,255,.13);
}
.rv-title { font-size: 20px; font-weight: 700; letter-spacing: -.3px; position: relative; }
.rv-meta  { font-size: 13px; opacity: .82; margin-top: 6px; position: relative; }
.rv-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 14px; position: relative; }
.rv-chips .lv { background: rgba(255,255,255,.22); color: #fff; }
.rv-hint {
  padding: 11px 15px; font-size: 12.5px; color: var(--text-3);
  background: var(--bg-2); border-bottom: 1px solid var(--line-2);
}
.rv-row { padding: 12px 15px; border-bottom: 1px solid var(--line-2); }
.rv-row:last-child { border-bottom: none; }
.rv-en {
  font-size: 16px; font-weight: 600; letter-spacing: -.1px;
  line-height: 1.55; word-break: break-word;
}
.rv-en .pos { font-size: 11px; color: var(--text-3); font-weight: 500; margin-left: 6px; }
.rv-zh { font-size: 13px; color: var(--text-3); margin-top: 3px; line-height: 1.65; }
.rv-foot { height: 8px; }

/* ---------- 小提示卡 / 说明卡 ---------- */
.tip-card { display: flex; gap: 12px; align-items: flex-start; }
.tip-ico {
  width: 38px; height: 38px; border-radius: 12px; flex: none;
  background: var(--grad-soft); color: var(--primary);
  display: grid; place-items: center;
}
.tip-ico svg { width: 20px; height: 20px; }
.tip-body { flex: 1; min-width: 0; line-height: 1.7; }
.note-card { display: flex; gap: 10px; align-items: flex-start; background: #FFFCF3; font-size: 14px; line-height: 1.7; }
.note-card svg { width: 18px; height: 18px; flex: none; margin-top: 2px; color: var(--warn); }

/* ---------- 对答案页 ---------- */
.ans-row {
  background: var(--card); border-radius: var(--r-lg); box-shadow: var(--sh-1);
  padding: 14px 15px 12px; margin-bottom: 10px;
  transition: background .2s var(--ease), box-shadow .2s var(--ease);
}
.ans-row.right { background: var(--success-soft); box-shadow: none; }
.ans-row.wrong { background: var(--danger-soft); box-shadow: none; }
.ans-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.ans-flag { font-size: 12px; font-weight: 600; color: var(--text-3); }
.ans-flag.ok { color: var(--success); }
.ans-flag.no { color: var(--danger); }
.ans-en {
  font-size: 17px; font-weight: 600; letter-spacing: -.2px;
  line-height: 1.5; word-break: break-word;
}
.ans-zh { font-size: 13px; color: var(--text-3); margin-top: 4px; line-height: 1.65; }
.ans-btns { display: flex; gap: 8px; margin-top: 12px; }
/* 注意：这里不要用带 .ans-row.right 前缀的选择器，否则会盖掉「已选中」的高亮色 */
.ans-btns .tick { flex: 1; width: auto; height: 44px; border-radius: 13px; background: rgba(23,26,36,.055); }
.ans-btns .tick:active { transform: scale(.97); }
.ans-btns .tick svg { width: 21px; height: 21px; }

/* 结果页里的答案行，长句子不要用等宽字体 */
.check-en { font-family: inherit; font-size: 16.5px; line-height: 1.5; word-break: break-word; }

/* ---------- 发布页：三类内容分节 ---------- */
.section-title {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; flex-wrap: wrap;
}
.sec-head { cursor: pointer; -webkit-tap-highlight-color: transparent; }
.sec-left { display: inline-flex; align-items: center; gap: 4px; }
.sec-left .caret {
  display: inline-grid; place-items: center;
  width: 16px; height: 16px; color: var(--text-3);
  transform: rotate(90deg); transition: transform .2s var(--ease);
}
.sec-left .caret.on { transform: rotate(-90deg); }
.sec-left .caret svg { width: 13px; height: 13px; }
.sec-actions { display: flex; gap: 6px; }
.sec-actions .btn { padding: 4px 10px; font-size: 12px; }
/* 二级小标题：单词 / 短语 / 句子（在某个 Section 里面）
   缩进 + 左侧细线，和一级的「Section A」拉开层级，一眼看出从属关系 */
.sec-sub {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; flex-wrap: wrap;
  margin-left: 4px; padding-left: 11px;
  border-left: 2px solid var(--line);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.sec-sub .sec-left { color: var(--text-2); }
.sec-sub .caret {
  display: inline-grid; place-items: center;
  width: 15px; height: 15px; color: var(--text-3);
  transform: rotate(90deg); transition: transform .2s var(--ease);
}
.sec-sub .caret.on { transform: rotate(-90deg); }
.sec-sub .caret svg { width: 12px; height: 12px; }
.sec-sub + .card { margin-left: 4px; }

/* 选学生：可勾选的名单（一行一个，不用卡片阴影，看起来像一张长列表） */
.pk-list { margin-top: 4px; }
.pk-list .pk-pick { border-radius: 0; box-shadow: none; background: transparent; border-bottom: 1px solid var(--line); }
.pk-list .card:last-child .pk-pick:last-child { border-bottom: none; }
/* 选中态要盖过上面那条「透明背景」，否则勾了看不出 */
.pk-pick.on { background: var(--grad); box-shadow: none; }
.pk-pick:active { background: var(--bg-2); transform: none; }
.pk-pick.on:active { background: var(--grad); }
.sec-pick { padding: 4px 10px 12px; }
/* 一行一条，紧凑排版 */
.sec-pick .pick { padding: 10px 12px; gap: 10px; }
.sec-pick .pick .cb { width: 22px; height: 22px; border-radius: 7px; }
.sec-pick .pick .en { font-size: 15px; }
.pick-line { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.pick-line .en { flex: none; }
.pick-line .zh {
  flex: 1; min-width: 0; margin-top: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-align: right;
}

/* ---------- 播放页：中文题 + 类型标签 ---------- */
.play-tag { font-size: 11.5px; padding: 3px 9px; border-radius: 999px; font-weight: 600; }
.play-tag.is-word { background: rgba(123,160,255,.20); color: #A9C0FF; }
.play-tag.is-zh   { background: rgba(176,140,255,.20); color: #C9B2FF; }
.play-zh {
  width: 100%; max-width: 380px; margin-bottom: 18px;
  background: rgba(255,255,255,.07);
  border: 1px solid rgba(255,255,255,.13);
  border-radius: var(--r-lg); padding: 16px 18px; text-align: center;
  animation: screen-in .3s var(--ease);
}
.play-zh-label { font-size: 12px; letter-spacing: 2px; opacity: .45; margin-bottom: 12px; }
.play-zh-text { font-size: 28px; font-weight: 700; line-height: 1.55; letter-spacing: .3px; word-break: break-word; }
.play-left { margin-top: 14px; font-size: 13px; opacity: .5; font-variant-numeric: tabular-nums; }
.play-mix { text-align: center; font-size: 12.5px; opacity: .36; margin-top: 4px; }

/* ---------- 复习页 / 书架：点一下听发音 ---------- */
.rv-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 15px; border-bottom: 1px solid var(--line-2);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  transition: background .18s var(--ease);
}
.rv-row:last-child { border-bottom: none; }
.rv-row:active { background: var(--bg-2); }
.rv-main { flex: 1; min-width: 0; }
.rv-play {
  width: 34px; height: 34px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  background: rgba(78,123,255,.10); color: var(--primary);
  transition: all .18s var(--ease);
}
.rv-play svg { width: 17px; height: 17px; }
.rv-row.playing { background: rgba(78,123,255,.10); }
.rv-row.playing .rv-play { background: var(--primary); color: #fff; transform: scale(1.06); }

/* ---------- 书架的 Section 分层与批量选择 ---------- */
.rv-sub {
  font-size: 13px; font-weight: 600; color: var(--text-2);
  margin: 12px 2px 6px;
}
.rv-row.picking { padding-left: 12px; gap: 10px; }
.rv-row.picking .rv-play { display: none; }
.rv-row.sel { background: rgba(78,123,255,.10); }
.rv-row.sel:active { background: rgba(78,123,255,.17); }
.pick-box {
  width: 22px; height: 22px; flex: none; border-radius: 50%;
  border: 1.6px solid var(--line); background: #fff;
  display: grid; place-items: center; color: #fff;
  transition: all .16s var(--ease-app);
}
.pick-box svg { width: 13px; height: 13px; }
.rv-row.sel .pick-box { background: var(--primary); border-color: var(--primary); }

/* ---------- 播放页：汉译英的大中文 + 倒计时条 ---------- */
.play-zh-bar {
  height: 3px; border-radius: 3px; background: rgba(255,255,255,.14);
  margin-top: 14px; overflow: hidden;
}
.play-zh-bar > i {
  display: block; height: 100%; width: 0%;
  background: linear-gradient(90deg, #7BA0FF, #B08CFF);
  transition: width .3s linear;
}

/* ---------- 任务内部的排行 ---------- */
.rk-row {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 0;
  border-top: 1px solid var(--line-2);
}
.rk-row:first-of-type { border-top: none; }
.rk-badge {
  width: 26px; height: 26px; flex: none;
  display: grid; place-items: center;
  border-radius: 50%; font-size: 13px; font-weight: 700;
  background: var(--bg-2); color: var(--text-3);
}
.rk-badge.r1 { background: #FFF3C4; color: #B8860B; }
.rk-badge.r2 { background: #EEF1F6; color: #6B7280; }
.rk-badge.r3 { background: #FBEEE0; color: #B45309; }
/* v2.50.0：任务内排行也带头像 + 头像框（和全局榜保持同一套画法） */
.rk-face { flex: none; display: flex; align-items: center; }
.rk-main { flex: 1; min-width: 0; }
.rk-name { font-size: 14.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rk-sub { font-size: 12.5px; color: var(--text-3); margin-top: 2px; }
.rk-val {
  flex: none; font-size: 15px; font-weight: 700; color: var(--primary);
  font-variant-numeric: tabular-nums;
}
.rk-val.is-time { font-size: 13px; font-weight: 600; color: var(--text-2); }
.rk-mini {
  width: 22px; height: 22px; flex: none;
  display: grid; place-items: center;
  border-radius: 8px; font-size: 12px; font-weight: 700;
  background: var(--bg-2); color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.rk-mini.r1 { background: #FFF3C4; color: #B8860B; }
.rk-mini.r2 { background: #EEF1F6; color: #6B7280; }
.rk-mini.r3 { background: #FBEEE0; color: #B45309; }

/* 做完听写后的名次横幅 */
.rank-banner {
  display: flex; align-items: center; gap: 12px;
  background: var(--grad-soft);
  border-radius: var(--r-lg);
  padding: 14px 16px; margin-bottom: 12px;
  transition: transform .16s var(--ease);
}
.rank-banner:active { transform: scale(.985); }
.rank-banner-badge {
  width: 44px; height: 44px; flex: none; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--grad); color: #fff;
  font-size: 19px; font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.rank-banner-title { font-size: 15px; font-weight: 650; letter-spacing: -.1px; }
.rank-banner-sub { font-size: 13px; color: var(--text-2); margin-top: 2px; }
.rank-banner-go { margin-left: auto; flex: none; font-size: 13px; font-weight: 600; color: var(--primary); }

/* ---------- 交「听写本」照片 ---------- */
/* 三列网格：手机上一张约 1/3 宽，缩略图够学生自己确认「拍的是不是这一页」。 */
.hw-thumbs {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
  margin-top: 2px;
}
.hw-thumb {
  position: relative;
  aspect-ratio: 3 / 4;
  border-radius: var(--r-sm);
  overflow: hidden;
  background: var(--bg-2);
  border: 1px solid var(--line);
}
.hw-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* ✕ 压在图右上角。⚠️ 图片本身不能是按钮，所以这里单独放一个 <button>。 */
.hw-del {
  position: absolute; top: 4px; right: 4px;
  width: 22px; height: 22px; padding: 0;
  border: none; border-radius: 50%;
  display: grid; place-items: center;
  background: rgba(17, 22, 36, .62); color: #fff;
  font-size: 12px; line-height: 1; cursor: pointer;
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
}
.hw-del:disabled { opacity: .45; }
.hw-del:active { background: var(--danger); }

/* ---- v2.39 · 正在上传的那几张（随拍随上传） ----
   学生选完照片**立刻**看到这一格（本地 blob 预览，不走网络），
   上面盖一层转圈 + 一条进度扫光；传完了这一格才换成服务端那张大图。
   目的是把「拍完要等」变成「拍完就在那儿了」。 */
.hw-thumb.uploading { border-color: rgba(94,123,255,.45); }
.hw-thumb.uploading img { filter: saturate(.85) brightness(.94); }
.hw-thumb .hw-spin {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  background: rgba(17,22,36,.44);
}
.hw-thumb .hw-spin::before {
  content: ""; width: 24px; height: 24px; border-radius: 50%;
  border: 3px solid rgba(255,255,255,.30);
  border-top-color: #fff;
  box-shadow: 0 1px 6px rgba(0,0,0,.25);
  animation: spin .8s linear infinite;
}
/* 一条自下而上扫过的光，让「在传」这件事有方向感（不是只有一个转圈） */
.hw-thumb.uploading::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 100%;
  background: linear-gradient(to top, rgba(94,123,255,.68), rgba(94,123,255,.24) 45%, rgba(94,123,255,0) 78%);
  animation: hw-sweep 1.4s var(--ease) infinite;
  pointer-events: none;
}
@keyframes hw-sweep {
  0%   { transform: translateY(100%); }
  100% { transform: translateY(0); }
}
/* 传失败的那张：红边 + 一个「重试」角标（点了重传） */
.hw-thumb.failed { border-color: rgba(239,90,111,.75); box-shadow: inset 0 0 0 2px rgba(239,90,111,.42); }
.hw-thumb.failed::after {
  content: "!";
  position: absolute; inset: 0;
  display: grid; place-items: center;
  background: rgba(206,48,74,.50);
  color: #fff; font-size: 24px; font-weight: 800;
  text-shadow: 0 1px 3px rgba(0,0,0,.35);
}
.hw-empty {
  grid-column: 1 / -1;
  padding: 22px 12px;
  text-align: center;
  font-size: 13px; color: var(--text-3);
  border: 1px dashed var(--line);
  border-radius: var(--r-sm);
  background: var(--bg-2);
}

/* 学生端首页的榜单入口 */
.lb-entry-sub { font-size: 13px; color: var(--text-3); margin-top: 3px; }
.lb-entry-sub b { color: var(--primary); font-weight: 700; }

/* ---------- 排行榜 ---------- */
.lb-row {
  display: flex; align-items: center; gap: 10px;
  padding: 11px 15px; border-bottom: 1px solid var(--line-2);
}
.lb-row:last-child { border-bottom: none; }
.lb-row.lb-me { background: rgba(78,123,255,.08); }
.lb-row.lb-top { background: linear-gradient(90deg, #FFFDF5, #fff); }
.lb-rank {
  width: 30px; height: 30px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  background: var(--bg-2); color: var(--text-3);
  font-size: 13px; font-weight: 700;
}
.lb-rank.r1 { background: #FFF3C4; color: #B8860B; }
.lb-rank.r2 { background: #EEF1F6; color: #6B7280; }
.lb-rank.r3 { background: #FBEEE0; color: #B45309; }
.lb-who { flex: 1; min-width: 0; }
.lb-name { font-size: 15px; font-weight: 600; display: flex; align-items: center; }
.lb-sub { font-size: 12.5px; color: var(--text-3); margin-top: 2px; }
.lb-val { font-size: 17px; font-weight: 700; color: var(--primary); flex: none; font-variant-numeric: tabular-nums; }
.lb-val.is-time { font-size: 13px; font-weight: 600; color: var(--text-2); background: var(--bg-2); padding: 3px 9px; border-radius: 9px; }

.podium { display: flex; align-items: flex-end; gap: 8px; margin-bottom: 6px; }
.podium-col { flex: 1; text-align: center; }
.podium-avatar {
  width: 46px; height: 46px; border-radius: 50%; margin: 0 auto 6px;
  display: grid; place-items: center; font-weight: 700; font-size: 15px;
  background: var(--grad); color: #fff;
}
.podium-col.p2 .podium-avatar, .podium-col.p3 .podium-avatar { width: 40px; height: 40px; font-size: 13px; }
.podium-name { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.podium-val { font-size: 13px; color: var(--primary); font-weight: 700; }
.podium-bar {
  margin-top: 6px; border-radius: 10px 10px 0 0;
  background: var(--grad-soft); color: var(--text-2);
  font-size: 18px; font-weight: 700; padding: 8px 0;
}
.podium-col.p1 .podium-bar { height: 68px; line-height: 52px; }
.podium-col.p2 .podium-bar { height: 52px; line-height: 36px; }
.podium-col.p3 .podium-bar { height: 44px; line-height: 28px; }

/* ---------- 开关 ---------- */
.switch {
  width: 50px; height: 30px; border-radius: 999px; flex: none;
  background: #D8DDEA; position: relative; transition: background .22s var(--ease);
}
.switch > i {
  position: absolute; top: 3px; left: 3px; width: 24px; height: 24px;
  border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.18);
  transition: transform .22s var(--ease);
}
.switch.on { background: var(--primary); }
.switch.on > i { transform: translateX(20px); }

/* ---------- 易错点分析 ---------- */
.mk-row {
  display: flex; align-items: center; gap: 11px;
  padding: 13px 15px; border-bottom: 1px solid var(--line-2);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  position: relative; transition: background .18s var(--ease);
}
.mk-row:last-child { border-bottom: none; }
.mk-row:active { background: var(--bg-2); }
.mk-row.playing { background: rgba(78,123,255,.10); }
.mk-row::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: transparent;
}
.mk-row.hot::before { background: var(--danger); }
.mk-row.warm::before { background: var(--warn); }
.mk-row.cool::before { background: #9BB0D8; }
/* 选中（v2.30.0）：左侧色条改成主色，底色是淡淡的主色 —— 不整行刷成蓝色，
   因为这一页每一行本来就带着红/橙的错误率语义色，铺满会互相打架。
   ⚠️ 必须写在上面三条 hot/warm/cool 之后：同类选择器后面的赢。 */
.mk-row.on { background: var(--grad-soft); }
.mk-row.on::before { background: var(--primary); }

/* 行首的勾选圈。点整行 = 选中 / 取消（和发布页那些 .pick 一个交互，别另发明一套）。 */
.mk-cb {
  width: 24px; height: 24px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  background: #fff; border: 1.5px solid var(--line);
  color: transparent; transition: all .16s var(--ease);
}
.mk-cb svg { width: 13px; height: 13px; }
.mk-row.on .mk-cb { background: var(--primary); border-color: var(--primary); color: #fff; }

.mk-rank {
  width: 26px; height: 26px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  background: var(--bg-2); color: var(--text-3);
  font-size: 12px; font-weight: 700;
}
.mk-row.hot .mk-rank { background: var(--danger-soft); color: var(--danger); }
.mk-row.warm .mk-rank { background: var(--warn-soft); color: var(--warn); }
.mk-main { flex: 1; min-width: 0; }
.mk-en {
  font-size: 16px; font-weight: 600; letter-spacing: -.1px;
  line-height: 1.45; word-break: break-word;
}
.mk-en .pos { font-size: 11px; color: var(--text-3); font-weight: 500; margin-left: 5px; }
.mk-zh { font-size: 13px; color: var(--text-3); margin-top: 2px; line-height: 1.55; }
.mk-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 7px; }
.mk-unit { font-size: 11.5px; color: var(--text-3); }
.mk-cnt { font-size: 11.5px; color: var(--text-3); font-variant-numeric: tabular-nums; }
.mk-right { flex: none; text-align: right; }
.mk-pct {
  font-size: 20px; font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.mk-pct small { font-size: 12px; font-weight: 600; margin-left: 1px; }
.mk-row.hot .mk-pct { color: var(--danger); }
.mk-row.warm .mk-pct { color: var(--warn); }
.mk-row.cool .mk-pct { color: var(--primary); }
.mk-row.zero .mk-pct { color: var(--text-3); }
.mk-play {
  width: 26px; height: 26px; border-radius: 50%;
  display: grid; place-items: center; margin: 7px 0 0 auto;
  background: var(--bg-2); color: var(--text-3);
}
.mk-play svg { width: 14px; height: 14px; }
.mk-row.playing .mk-play { background: var(--primary); color: #fff; }
.mk-row.playing, .check-row.playing { background: rgba(78,123,255,.10); }

/* 宽屏（平板 / 桌面浏览器）：内容区放宽一点。
   ⚠️⚠️ 这里**只改栅格变量**，绝不再单独去动 `.screen` 的 padding。
      原来这一条写的是 `.screen { padding-left:20px; padding-right:20px }`，
      而 dock 栏还固定在 16px —— 于是宽屏上「内容左边缘」和「dock 栏左边缘」
      永远差 4px。学生反馈的「边缘对不齐」在手机上是我之前统一栅格修掉的，
      这一条是同一个病在宽屏上的复发点。 */
@media (min-width: 620px) {
  :root { --gutter: 20px; --content-max: 680px; }
}

/* ============================================================
   动效升级 v2.3 · 彩色主调 + 丝滑转场
   ============================================================ */
:root {
  /* v2.36 · 冰川蓝主渐变（覆盖旧蓝紫）· 左青右紫横向 */
  --grad: linear-gradient(90deg, #3FC3FF 0%, #5E7BFF 52%, #8B6BFF 100%);
  --grad-3: linear-gradient(120deg, #38B6FF 0%, #5E7BFF 55%, #8B6BFF 118%);
  --accent: #8A63FF;
  /* 主流 App 的标准缓动：iOS / Material 都用这条 */
  --ease-app: cubic-bezier(.32, .72, 0, 1);
  --ease-out: cubic-bezier(.32, .72, 0, 1);
  --ease-spring: cubic-bezier(.32, .72, 0, 1);
}

/* ---- 页面切换：整页淡入 0.3 秒，不做逐项延迟 ----
   （逐条飞入是"网页感"，主流 App 都是整页一起进来） */
.screen { animation: page-in .3s var(--ease-app); }
@keyframes page-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
@keyframes rise-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* ---- 按钮：流光 + 涟漪 + 按压回弹 ---- */
.btn { position: relative; overflow: hidden; }
.ripple {
  position: absolute; border-radius: 50%;
  background: rgba(255,255,255,.5);
  transform: scale(0); opacity: .9;
  pointer-events: none;
  animation: ripple .34s var(--ease-app) forwards;
}
.ripple.dark { background: rgba(47,66,120,.16); }
@keyframes ripple { to { transform: scale(2.4); opacity: 0; } }
.btn-primary:active, .btn-success:active { transform: scale(.955); }

/* ---- 输入框：聚焦冰蓝发光 + 标签上浮 ---- */
.input:focus, textarea.input:focus, select.input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 4px rgba(78,123,255,.15), 0 6px 18px rgba(78,123,255,.10);
}
.field > label { transition: color .18s var(--ease), transform .18s var(--ease); }
.field:focus-within > label { color: var(--primary); transform: translateY(-1px); }

/* ---- Toast 弹性入场 ---- */
.toast { transition: opacity .18s ease, transform .22s var(--ease-app); }

/* ---- 登录页：彩色极光背景 + 逐项浮现 ---- */
.lg-aurora { position: fixed; inset: 0; overflow: hidden; z-index: 0; pointer-events: none; }
.lg-aurora i {
  position: absolute; border-radius: 50%;
  filter: blur(64px); opacity: .5;
  animation: lg-drift 20s var(--ease) infinite alternate;
  will-change: transform;
}
.lg-aurora i:nth-child(1) { width: 330px; height: 330px; background: #58C0FF; left: -100px; top: -70px; }
.lg-aurora i:nth-child(2) { width: 300px; height: 300px; background: #9F8CFF; right: -90px; top: 110px; animation-duration: 24s; animation-delay: -7s; }
.lg-aurora i:nth-child(3) { width: 280px; height: 280px; background: #7FE3C8; left: 22%; bottom: -130px; animation-duration: 27s; animation-delay: -13s; }
.lg-aurora i:nth-child(4) { width: 220px; height: 220px; background: #FF9BCD; right: 16%; bottom: -70px; animation-duration: 31s; animation-delay: -4s; opacity: .32; }
@keyframes lg-drift {
  from { transform: translate3d(0,0,0) scale(1); }
  to   { transform: translate3d(46px,-34px,0) scale(1.18); }
}
.login-wrap { position: relative; z-index: 1; }
.login-card {
  background: rgba(255,255,255,.78);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  border: 1px solid rgba(255,255,255,.7);
  border-radius: 26px;
  padding: 22px 20px 20px;
  box-shadow: 0 18px 50px rgba(31,41,90,.14);
}
.login-wrap > * { animation: page-in .3s var(--ease-app) both; }
.login-card { animation: page-in .34s var(--ease-app) .04s both; }
@keyframes pop-in {
  from { opacity: 0; transform: scale(.9) translateY(10px); }
  to   { opacity: 1; transform: none; }
}

/* ---- 登录成功：全屏彩色转场 ---- */
.lg-burst {
  position: fixed; z-index: 999; left: 50%; top: 58%;
  width: 72px; height: 72px; margin: -36px 0 0 -36px;
  border-radius: 50%;
  background: var(--grad-3);
  transform: scale(0); opacity: .96;
  pointer-events: none;
}
.lg-burst.go { animation: lg-burst .52s var(--ease-app) forwards; }
@keyframes lg-burst {
  0%   { transform: scale(0); opacity: .96; }
  100% { transform: scale(26); opacity: 1; }
}
.lg-burst.fade { animation: lg-burst-fade .3s ease forwards; }
@keyframes lg-burst-fade { to { opacity: 0; } }

/* ---- 书架：教材分册折叠 ---- */
.book-block { border-bottom: 1px solid var(--line-2); }
.book-block:last-child { border-bottom: none; }
.book-head { padding: 15px 16px; border-bottom: 1px solid var(--line); }
.book-caret { transition: transform .22s var(--ease-app); }
.book-caret.open { transform: rotate(90deg); }
.book-units {
  background: var(--card);          /* 展开的单元跟上面一样是白底，不再压一层灰 */
  padding: 0;
  animation: page-in .2s var(--ease-app);
}
.book-units .unit-row {
  padding: 13px 16px 13px 22px;
  border-bottom: 1px solid var(--line);   /* 单元之间的分界线，看得清 */
}
.book-units .unit-row:active { background: var(--bg-2); }
.book-units .unit-row:last-child { border-bottom: none; }
.book-units .unit-main { flex: 1; min-width: 0; }
.book-units .unit-row .item-title { font-size: 15px; }

/* ---- 书架：单条内容的「改 / 删」小按钮 ---- */
.rv-acts { display: flex; gap: 4px; flex: none; margin-right: 2px; }
.mini-btn {
  width: 34px; height: 34px; border-radius: 11px;
  display: grid; place-items: center;
  background: var(--bg-2); color: var(--text-2);
  transition: background .18s var(--ease), color .18s var(--ease), transform .18s var(--ease);
}
.mini-btn:active { transform: scale(.94); }
.mini-btn.danger { color: var(--danger); background: var(--danger-soft); }
.mini-btn svg { width: 17px; height: 17px; }
.mini-btn[disabled] { opacity: .28; }
.mini-btn[data-top], .mini-btn[data-up], .mini-btn[data-down] { width: 32px; height: 32px; border-radius: 10px; }
.mini-btn[data-top] { color: var(--primary); background: color-mix(in srgb, var(--primary) 14%, transparent); }

/* ---- 书架：调整顺序模式（拖拽） ---- */
.ord-no {
  flex: none; width: 20px; text-align: center;
  font-size: 12px; font-weight: 700; color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
/* 拖拽把手：touch-action:none 让手指按住拖动时页面不跟着滚 */
.drag-handle {
  width: 40px; height: 34px; border-radius: 10px;
  display: grid; place-items: center;
  color: var(--primary);
  background: color-mix(in srgb, var(--primary) 13%, transparent);
  touch-action: none; cursor: grab;
  -webkit-tap-highlight-color: transparent;
}
.drag-handle svg { width: 18px; height: 18px; }
.drag-handle:active { cursor: grabbing; transform: scale(.95); }
/* 被拖的那一行：原位变成浅色占位块，让人看清会落到哪 */
.rv-row.drag-src {
  background: color-mix(in srgb, var(--primary) 9%, transparent);
  border-radius: 12px;
}
.rv-row.drag-src > .rv-main,
.rv-row.drag-src > .rv-play,
.rv-row.drag-src > .ord-no { opacity: .16; }
.rv-row.drag-src .drag-handle { background: transparent; color: var(--text-3); }
/* 跟手的那一块 */
.drag-ghost {
  position: fixed; z-index: 9999; pointer-events: none;
  background: var(--card); border-radius: 14px;
  box-shadow: 0 14px 34px rgba(16, 24, 40, .22), 0 2px 6px rgba(16, 24, 40, .1);
  transform: scale(1.015);
}
.drag-ghost .drag-handle { color: var(--primary); }
.drag-ghost .rv-play { visibility: hidden; }
.dragging-row { user-select: none; -webkit-user-select: none; }
.card.dragging-card { box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 34%, transparent); }
.card.ord-saving { opacity: .72; }
.ord-tip {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  margin: 10px 2px 12px; padding: 9px 12px;
  font-size: 12.5px; font-weight: 550; color: var(--primary);
  background: color-mix(in srgb, var(--primary) 9%, transparent);
  border-radius: 12px;
}
.ord-tip svg { width: 15px; height: 15px; flex: none; }

/* ---- 书架：正在生成发音时的提示 ---- */
.rv-row.rv-loading .rv-play { color: var(--primary); animation: spin 1s linear infinite; }

/* ---- 书架：老师新加的内容标记 ---- */
.tag-new {
  display: inline-block; margin-left: 6px; padding: 1px 7px;
  border-radius: 999px; font-size: 11px; font-weight: 600;
  background: var(--danger); color: #fff; vertical-align: 1px;
}
.dot-new {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  background: var(--danger); vertical-align: 2px; margin-left: 5px;
}

/* ---- 底部 Tab：书架有新内容时的小红点 ---- */
.tab[data-tab="units"].has-new { position: relative; }
.tab[data-tab="units"].has-new::after {
  content: ""; position: absolute; top: 9px; left: 50%; margin-left: 7px;
  width: 8px; height: 8px; border-radius: 50%; background: var(--danger);
}

/* ---- 尊重"减少动态效果"设置 ---- */
@media (prefers-reduced-motion: reduce) {
  .lg-aurora i { animation: none !important; }
  .screen, .login-wrap > *, .login-card {
    animation-duration: .01ms !important; animation-delay: 0ms !important;
  }
  /* 进度条不跑了，但还得看得见 —— 直接铺满一条实线 */
  .pbar i { animation: none !important; width: 100% !important; transform: none !important; }
}

/* ---- 口令输入兼容：普通文本框 + CSS 掩码 ---- */
/* 部分安卓大屏/老 WebView 的输入法对 type="password" 不弹键盘， */
/* 换成文本框用 CSS 打码，外观不变但能正常唤起输入法。 */
.pwd-wrap { position: relative; }
.input.masked { -webkit-text-security: disc; }
.input.masked.show { -webkit-text-security: none; }
.input.has-eye { padding-right: 50px; }
.pwd-eye {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  width: 38px; height: 38px; display: grid; place-items: center;
  color: var(--text-3); border-radius: 11px;
  transition: color .18s var(--ease), background .18s var(--ease);
}
.pwd-eye:active { background: var(--bg-2); }
.pwd-eye.on { color: var(--primary); }
.pwd-eye svg { width: 20px; height: 20px; }

/* ============================================================
   学习时长（老师看学生）
   ============================================================ */
/* 整行是 div（里面要放信封按钮，button 套 button 是非法 HTML）——
   补上 cursor，桌面预览时才像可点的 */
.st-row { cursor: pointer; }
.st-row .st-big {
  flex: none; font-size: 19px; font-weight: 700; letter-spacing: -.3px;
  color: var(--primary); font-variant-numeric: tabular-nums;
}
.st-row .st-big span { font-size: 12px; font-weight: 600; color: var(--text-3); margin-left: 2px; }
/* v2.40 · 「不许拆行」的小工具类。中文默认逐字换行，「2 小时 40 分」会在
   「小 / 时」之间断开变成两行，看着像排版坏了；包一层 .nb 就锁成整块。 */
.nb { white-space: nowrap; }
/* 右侧那一列：大数字 + 「在学」小字。
   ⚠️ 光摆一个「28 分」，老师会理解成「他打开了软件 28 分钟」——
      那个数字其实是**在学**时长（页面在前台 且 一分钟内还有操作），
      跟「打开时长」不是一回事。标签必须写死在数字下面，别再让人猜。 */
.st-row .st-num-col {
  flex: none; display: flex; flex-direction: column;
  align-items: flex-end; justify-content: center;
}
.st-row .st-big-cap {
  font-size: 10.5px; font-weight: 600; letter-spacing: .3px;
  color: var(--text-3); line-height: 1.3;
}
.st-row.dim .st-big { color: var(--text-3); }
.st-row.dim .avatar { filter: grayscale(1); opacity: .55; }
.item-sub.odd { color: var(--warn); font-weight: 550; }

/* 学习时长行上的小信封：点一下就直接给他发条消息。
   单独一个按钮、吃掉自己的点击 —— 整行仍然是「点进详情」。 */
.st-msg {
  flex: none; width: 34px; height: 34px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  color: var(--primary); background: var(--grad-soft);
}
.st-msg svg { width: 17px; height: 17px; }
.st-msg:active { opacity: .6; }

/* 首页「教师留言板」入口上的未读数 */
.bm-new {
  flex: none; font-size: 11.5px; font-weight: 700; color: #fff;
  background: var(--danger); border-radius: 999px; padding: 2px 8px;
}

/* 学生端「老师给你留了话」的弹窗。
   ⚠️ 这里的 svg **必须给死宽高**：I.pen 本身没有尺寸，
   在 .avatar / .btn 里是被容器约束住的，裸放会撑满整屏（实测就是这么炸的）。 */
.bm-cap {
  display: flex; align-items: center; gap: 7px;
  font-size: 13px; font-weight: 600; color: var(--primary); margin-bottom: 12px;
}
.bm-cap svg { width: 15px; height: 15px; flex: none; }
/* 一次弹好几条时别把抽屉顶穿，超过就自己滚 */
.bm-list { max-height: 46vh; overflow-y: auto; }

/* 「正在用」的绿点：外圈用浅绿，手机上比单独一个小点更容易看见 */
.st-live {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  background: var(--success); margin-left: 6px; vertical-align: middle;
  box-shadow: 0 0 0 3px var(--success-soft);
}

.st-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px 10px; }
.st-cell { min-width: 0; }
.st-num {
  font-size: 20px; font-weight: 700; letter-spacing: -.4px;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.st-num.main { color: var(--primary); }
.st-num i { font-size: 12.5px; font-weight: 600; color: var(--text-3); font-style: normal; }
.st-cap { font-size: 12.5px; color: var(--text-3); margin-top: 3px; }

.st-legend {
  margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 7px;
}
.st-legend span { font-size: 12.5px; color: var(--text-3); display: flex; align-items: center; gap: 7px; }
.st-legend .k { width: 8px; height: 8px; border-radius: 3px; flex: none; }
.st-legend .k.act { background: var(--primary); }
.st-legend .k.idle { background: #C9CFDE; }
.st-legend .k.away { background: var(--warn); }
/* v2.40 · 「打开时长 = 在学 + 挂机」那一行的色块，用主色渐变表示它是前面三项的和 */
.st-legend .k.sum { background: linear-gradient(90deg, var(--primary), #8A63FF); }

.st-best {
  margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line);
  font-size: 13px; color: var(--text-2);
}
.st-best b { color: var(--primary); }

/* 纯 CSS 柱状图：一根柱子 = 一天，下段在学、上段挂机 */
.st-chart { display: flex; align-items: flex-end; gap: 3px; padding-top: 4px; }
.st-col { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: center; }
.st-stack {
  width: 100%; max-width: 18px; border-radius: 5px; overflow: hidden;
  display: flex; flex-direction: column-reverse; justify-content: flex-start;
  /* 故意不给底色：一给底色，「这天没打开」看起来就像「整根灰柱＝全程挂机」，
     而图例里灰色正好是挂机 —— 会看错。没数据的日子只留底部一条细线。 */
}
.st-seg.act { background: var(--grad); }
.st-seg.idle { background: #DCE1EC; }
.st-seg.none { height: 3px; background: var(--line); border-radius: 2px; }
.st-day { font-size: 9.5px; color: var(--text-3); margin-top: 5px; font-variant-numeric: tabular-nums; }
.st-awdot { display: block; width: 5px; height: 5px; border-radius: 50%; margin-top: 3px; background: transparent; }
.st-awdot.on { background: var(--warn); }

.st-aw-row { align-items: flex-start; }
.st-aw-t {
  flex: none; width: 48px; font-size: 14.5px; font-weight: 650;
  color: var(--warn); font-variant-numeric: tabular-nums; padding-top: 1px;
}
.st-aw-row .item-sub { margin-top: 2px; }

/* 分时段：一根柱子 = 一小时，下段在学、上段挂机。
   日总量只能说「今天学了 40 分钟」，这条才能看出「几点在学、哪一段是挂着凑的」。 */
.hr-chart { display: flex; align-items: flex-end; gap: 2px; padding-top: 4px; }
.hr-col { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: center; }
.hr-stack {
  width: 100%; border-radius: 2px; overflow: hidden;
  display: flex; flex-direction: column-reverse; justify-content: flex-start;
}
.hr-seg.act { background: var(--grad); }
.hr-seg.idle { background: #DCE1EC; }
.hr-seg.none { height: 3px; background: var(--line); border-radius: 2px; }
/* 24 根柱子放不下 24 个标签，只每隔几根标一个；高度留死，免得空字符串把行高挤掉 */
.hr-h {
  font-size: 8.5px; color: var(--text-3); margin-top: 5px; height: 12px;
  font-variant-numeric: tabular-nums;
}
.hr-h.on { color: var(--primary); font-weight: 700; }
.hr-note { font-size: 12.5px; color: var(--text-3); margin-top: 12px; line-height: 1.6; }
.hr-note b { color: var(--primary); }

/* 学生端「我的」页：今天专注多久（低调，不打分不排名） */
.st-me { display: flex; align-items: center; gap: 12px; }

/* 我的薄弱考点（学生端「我的」页）—— 一行一个考点：名字 + 进度条 + 对的/做过的。
   进度条留 44% 给名字：考点名可能是「一般将来时·will 与 be going to 的区别」这种长句。 */
.st-kp-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 2px; }
.kpr { display: flex; align-items: center; gap: 10px; margin-top: 9px; }
.kpr-n {
  flex: 0 1 auto; max-width: 46%; font-size: 13.5px; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.kpr-bar { flex: 1 1 auto; height: 6px; border-radius: 99px; background: var(--line-2); overflow: hidden; }
.kpr-bar i { display: block; height: 100%; border-radius: 99px; }
.kpr-p { flex: 0 0 auto; font-size: 12.5px; color: var(--text-3); font-variant-numeric: tabular-nums; }

/* 书架每条的「用过 N 次」（老师端）—— 用来一眼看出哪些内容还没考过 */
.use-badge {
  display: inline-block; margin-left: 7px; padding: 1px 7px; vertical-align: 1px;
  font-size: 11.5px; font-weight: 600; border-radius: 999px;
  background: var(--success-soft); color: var(--success);
}
.use-badge.off { background: var(--bg-2); color: var(--text-3); font-weight: 500; }

/* ---- 班级 / 班组 ---- */
.grp-row .avatar { font-size: 13px; font-weight: 600; }
/* 自建班组那一行有**三个**按钮（成员 / 改名 / 删除）+ 一个可能很长的名字。
   窄屏上不加这两条，按钮会被压扁、名字会折成两行把按钮挤下去。
   名字长度在弹层里已经限到 12 字（maxlength）。 */
.grp-row .item-main { flex: 1 1 auto; min-width: 0; }
.grp-row .item-title { line-height: 1.4; word-break: break-word; }
.grp-row > .btn { flex: 0 0 auto; white-space: nowrap; padding: 0 10px; }
.grp-tag {
  display: inline-block; margin-left: 6px; padding: 1px 7px; vertical-align: 1px;
  font-size: 11px; font-weight: 600; border-radius: 999px;
  background: var(--grad-soft); color: var(--primary-2);
}
.grp-locked { font-size: 12px; color: var(--text-3); flex: none; }

/* ============================================================
   v2.20 · A 风格：学生首页
   ------------------------------------------------------------
   首页第一条是「教师留言板」，且老师的话要大字号 —— 这是学生一进来
   唯一必须看见的东西，其余（待完成）都排在它后面。
   ============================================================ */

/* 顶部身份条：头像 + 姓名 + 班级 + 今日时长 */
.home-head {
  display: flex; align-items: center; gap: 12px;
  background: var(--card); border: 0.5px solid var(--line);
  border-radius: var(--r-lg); box-shadow: var(--sh-1);
  padding: 14px 16px; margin-bottom: 12px;
}
.home-head .hh-avatar {
  width: 46px; height: 46px; border-radius: 50%; flex: none;
  background: var(--grad-soft); color: var(--primary);
  font-size: 17px; font-weight: 600;
  display: grid; place-items: center;
}
.home-head .hh-name { font-size: 17px; font-weight: 600; letter-spacing: -.2px; }
.home-head .hh-sub { font-size: 12.5px; color: var(--text-3); margin-top: 2px; }
.home-head .hh-time {
  flex: none; text-align: right; font-size: 12px; color: var(--text-3);
}
.home-head .hh-time b { display: block; font-size: 15px; color: var(--primary); font-weight: 600; }

/* 教师留言板（首页主体） */
.bm-card {
  position: relative; overflow: hidden;
  /* ⚠️ button 默认 display:inline-block —— 不写 width:100% 这张首页主卡会「按内容收缩」，
     留言短的时候缩成一小块、只占左边三分之一，跟下面的卡片对不齐。
     （以前看不出来是因为正文带着空状态样式的 24px 横向内边距，恰好把它撑到了满宽。） */
  display: block; width: 100%;
  /* ⚠️ button 默认 text-align:center —— 留言正文必须左对齐，忘了这条整个卡都是居中的 */
  text-align: left;
  background: var(--card); border: 0.5px solid var(--line);
  border-radius: var(--r-lg); box-shadow: var(--sh-1);
  padding: 16px 18px 15px 20px; margin-bottom: 14px;
}
.bm-card::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0;
  width: 5px; background: var(--primary);
}
.bm-card:active { background: var(--bg-2); }
.bm-head { display: flex; align-items: center; gap: 9px; }
.bm-ico {
  width: 30px; height: 30px; border-radius: 50%; flex: none;
  background: var(--grad-soft); color: var(--primary);
  display: grid; place-items: center;
}
.bm-ico svg { width: 16px; height: 16px; }
.bm-title { font-size: 15px; font-weight: 600; color: var(--text); }
.bm-count { font-size: 12px; color: var(--text-3); }
.bm-more { margin-left: auto; font-size: 13px; color: var(--primary); flex: none; }
/* 老师的话 —— 首页最大号的字，就是要它压过一切。
   v2.40：老师反馈「字稍微显眼点」，所以字重 500 → 600、字号 20 → 21px、
   字距再收紧一点，颜色用最深的 --text。
   另外「如果字很多就滚动显示」：留言超过 5 行时不再无限往下撑高
   （以前一条长留言会把下面的「待完成」整个顶出第一屏），改成卡内滚动，
   底部留一条渐隐，暗示「下面还有」。 */
.bm-say {
  margin-top: 13px; font-size: 21px; line-height: 1.6;
  font-weight: 600; color: var(--text); letter-spacing: -.35px;
  word-break: break-word;
}

/* ---- v2.41 · 留言板：两行窗口 + 跑马灯 ----
   老师要的是「只显示两行文字，剩下的像跑马灯一样滚动显示就行，不需要全都显示出来」。
   做法：
     · `.bm-say-win` 是**两行高的窗口**，超出裁掉；
     · 内容由 JS 量出真实高度，真超了两行才加 `.is-run` 并把时长写进 `--bm-dur`；
     · 滚动靠 `translateY`：顶上停一下 → 慢慢挪到底 → 再停一下 → 回位重来。
   为什么时长是 JS 算的：留言有长有短，写死 10s 会让短留言快得看不清、
   长留言慢得看不到头。26px/s 是 21px 中文读着舒服的速度。
   ⚠️⚠️ 窗口高度和位移都**必须用同一对变量算**（`--bm-fs` × `--bm-lh`）。
   第一版写的是 `max-height: 3.2em`，而 `em` 在 `.bm-say-win` 上按**它自己的**
   字号（16px，从 body 继承）解析 → 窗口只有 51px，而正文行高是 33.6px × 2 = 67px：
   第二行被**拦腰裁掉**，位移量也算成 3.2em（在 `.bm-say` 上是 21px→67px）——
   去程比窗口多走 16px。窗口高和终点位移**必须是同一个数**，所以统一成 `--bm-2l`。
   ⚠️ 用 transform 而不是改 scrollTop —— transform 走合成层，不触发重排，
      正好符合 v2.40 定下的「滚动流里不许有逐帧重绘的东西」这条红线。 */
.bm-say-win {
  --bm-fs: 21px;          /* 正文字号，和 .bm-say 保持一致 */
  --bm-lh: 1.6;           /* 正文行高 */
  --bm-2l: calc(var(--bm-fs) * var(--bm-lh) * 2);   /* 正好两行 */
  margin-top: 13px;
  max-height: var(--bm-2l);
  overflow: hidden;
  position: relative;
}
.bm-say-win.is-empty { --bm-fs: 14.5px; }   /* 「老师的话会出现在这里」是占位小字 */
.bm-say-win .bm-say { margin-top: 0; font-size: var(--bm-fs); line-height: var(--bm-lh); }
.bm-say-win.is-run .bm-say {
  animation: bm-marquee var(--bm-dur, 10s) linear infinite;
  will-change: transform;
}
/* 两头各留 14% 的时间停住 —— 一上来就滚的话，第一句还没读完就走了 */
@keyframes bm-marquee {
  0%, 14%   { transform: translateY(0); }
  86%, 100% { transform: translateY(calc(-100% + var(--bm-2l))); }
}
/* 系统开了「减弱动态效果」：不滚，改回手指上下滑（留言板详情页本来就能看全） */
@media (prefers-reduced-motion: reduce) {
  .bm-say-win { overflow-y: auto; -webkit-overflow-scrolling: touch; touch-action: pan-y; overscroll-behavior: contain; }
  .bm-say-win.is-run .bm-say { animation: none; }
}

/* ⚠️⚠️ 这个修饰类**绝不能叫 `.empty`**。
   全站的空状态块是 `.empty { text-align:center; padding:52px 24px }`（见 388 行），
   两者会撞上：留言板没有留言时，正文被顶成居中 + 上下各 52px 内边距，
   卡片凭空高出 100 多像素，看起来就是「字没居中、空了一大片」（学生截图反馈的就是这个）。
   v2.39 改名为 is-empty。 */
.bm-say.is-empty { font-size: 14.5px; font-weight: 400; color: var(--text-3); line-height: 1.6; }
.bm-foot { margin-top: 11px; font-size: 12px; color: var(--text-3); }

/* 首页「待完成」：听写和作文混排，做完的不出现 */
.todo-row { display: flex; align-items: center; gap: 12px; padding: 13px 16px; border-bottom: 1px solid var(--line-2); width: 100%; text-align: left; }
.todo-row:last-child { border-bottom: none; }
.todo-row:active { background: var(--bg-2); }
.todo-row .tr-ico { width: 46px; height: 46px; border-radius: 14.5px; flex: none; display: grid; place-items: center; }
.todo-row .tr-ico svg { width: 24px; height: 24px; }
/* v2.38：essay/done 的旧语义底色删除 —— 新图标自带橙/绿渐变，统一白玻璃托 */

/* ============================================================
   v2.22.0 · 批次 3：分数段分布 / 成绩趋势 / 高分范文 / 英语报
   ============================================================ */

/* ---- 分数段分布（只给老师看） ---- */
.dist-row { display: flex; align-items: center; gap: 10px; padding: 5px 0; }
.dist-label { flex: 0 0 66px; font-size: 13px; color: var(--text-2); font-variant-numeric: tabular-nums; }
.dist-bar { flex: 1; height: 10px; border-radius: 6px; background: var(--bg-2); overflow: hidden; }
.dist-bar i { display: block; height: 100%; border-radius: 6px; background: var(--primary); transition: width .45s var(--ease); }
/* 高分段绿、低分段红 —— 老师扫一眼就知道这一班偏在哪，不用一个个读数字 */
.dist-bar i.b-ok { background: var(--success); }
.dist-bar i.b-mid { background: var(--primary); }
.dist-bar i.b-low { background: var(--warn); }
.dist-bar i.b-bad { background: var(--danger); }
.dist-n { flex: 0 0 34px; text-align: right; font-size: 14px; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
.dist-n small { font-size: 11px; font-weight: 500; color: var(--text-3); margin-left: 1px; }

/* ---- 成绩趋势折线（学生端） ---- */
.trend-wrap { position: relative; }
.trend-svg { display: block; width: 100%; height: auto; overflow: visible; }
.tr-grid { stroke: var(--line); stroke-width: 1; stroke-dasharray: 3 4; }
.tr-axis { font-size: 10px; fill: var(--text-3); font-variant-numeric: tabular-nums; }
.tr-line { fill: none; stroke: var(--primary); stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; }
.tr-dot { fill: #fff; stroke: var(--primary); stroke-width: 2; }
.tr-dot.is-last { fill: var(--primary); stroke: #fff; stroke-width: 2.5; }
.tr-halo { fill: var(--primary); opacity: .15; }
.trend-last { text-align: center; font-size: 12.5px; color: var(--text-3); margin-top: 2px; }
.trend-last b { color: var(--primary); font-size: 14px; }
.trend-say {
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line-2);
  font-size: 14px; color: var(--text-2); line-height: 1.7;
}
.trend-say b.up { color: var(--success); }
.trend-say b.down { color: var(--danger); }
.trend-empty {
  padding: 26px 12px; text-align: center; font-size: 13.5px; color: var(--text-3);
  background: var(--bg-2); border-radius: var(--r-md);
}
/* 趋势逐次明细左侧那个短日期（不能用 .rk-mini —— 那个只有 22px，装不下 10/07） */
.tdate { flex: none; width: 46px; font-size: 12px; font-weight: 700; color: var(--text-3); font-variant-numeric: tabular-nums; }

/* ---- 高分范文（每次作文任务内部，排行榜上方） ---- */
.md-piece { border-top: 1px solid var(--line-2); padding: 14px 0 4px; }
.md-piece:first-of-type { border-top: none; padding-top: 6px; }
.md-head { display: flex; align-items: center; gap: 10px; margin-bottom: 9px; }
.md-medal {
  width: 28px; height: 28px; flex: none; display: grid; place-items: center;
  border-radius: 10px; font-size: 14px; background: var(--bg-2); color: var(--text-3);
}
.md-medal.r1 { background: #FFF3C4; }
.md-medal.r2 { background: #EEF1F6; }
.md-medal.r3 { background: #FBEEE0; }
.md-who { flex: 1; min-width: 0; }
.md-name { font-size: 14.5px; font-weight: 700; }
.md-sub { font-size: 12.5px; color: var(--text-3); margin-top: 2px; }
.md-body {
  font-size: 14.5px; line-height: 1.85; color: var(--text);
  background: var(--bg-2); border-radius: var(--r-md); padding: 12px 14px;
  white-space: pre-wrap; word-break: break-word;
}
.md-good { margin-top: 8px; font-size: 12.5px; color: var(--success); line-height: 1.7; }

/* ---- 英语报（老师端 A4 打印视图） ---- */
.np-screen { background: #E9ECF3; }
.np-bar { padding: 12px 2px 14px; }
/* 210×297mm 是 A4 的真实尺寸：屏幕上就是一张纸，打印出来一比一。
   max-width:100% 是给手机留的活路 —— 窄屏上它退回普通单栏，不会横向溢出。 */
.np-sheet {
  width: 210mm; max-width: 100%; min-height: 297mm; margin: 0 auto;
  background: #fff; color: #171A24;
  padding: 15mm 14mm;
  box-shadow: 0 4px 22px rgba(16,24,40,.14);
  font-family: Georgia, "Times New Roman", "Songti SC", "STSong", serif;
}
.np-head { text-align: center; }
.np-mast {
  font-size: 40px; font-weight: 800; letter-spacing: 1.5px; line-height: 1.1;
  color: #16233F; font-family: Georgia, "Times New Roman", serif;
}
.np-sub { font-size: 12.5px; letter-spacing: 3px; color: #7A839A; margin-top: 5px; text-transform: uppercase; }
.np-rule { height: 3px; background: #16233F; margin: 12px 0 8px; }
.np-meta { display: flex; justify-content: space-between; font-size: 11.5px; color: #7A839A; }
.np-title-band { margin: 18px 0 6px; padding: 12px 14px; background: #F4F6FB; border-left: 4px solid #16233F; }
.np-topic { font-size: 19px; font-weight: 700; color: #16233F; line-height: 1.35; }
.np-prompt { font-size: 12.5px; color: #5A6172; margin-top: 6px; line-height: 1.7; }
.np-stats { font-size: 11.5px; color: #7A839A; margin-top: 8px; }
.np-pieces { margin-top: 12px; }
.np-piece { padding: 13px 0; border-bottom: 1px dashed #D8DEEA; break-inside: avoid; page-break-inside: avoid; }
.np-piece:last-child { border-bottom: none; }
.np-piece-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 7px; }
.np-rank {
  flex: none; min-width: 22px; height: 22px; padding: 0 6px;
  display: inline-grid; place-items: center; border-radius: 6px;
  background: #EEF1F6; color: #6B7280; font-size: 12px; font-weight: 700;
  font-family: system-ui, -apple-system, sans-serif;
}
.np-rank.r1 { background: #FFF3C4; color: #B8860B; }
.np-rank.r2 { background: #EEF1F6; color: #6B7280; }
.np-rank.r3 { background: #FBEEE0; color: #B45309; }
.np-name { font-size: 15px; font-weight: 700; color: #16233F; }
.np-class { font-size: 11.5px; color: #7A839A; }
/* 三块金牌不写「并列」，看着像编号坏了 —— 旁边挂一个小标签就说清楚了 */
.np-tie {
  padding: 1px 6px; border-radius: 5px; background: #FFF3C4; color: #B8860B;
  font-size: 10.5px; font-weight: 700;
  font-family: system-ui, -apple-system, sans-serif;
}
.np-score { margin-left: auto; font-size: 12.5px; font-weight: 700; color: #3B6FF6; }
.np-body { font-size: 13.5px; line-height: 1.95; color: #262B38; text-align: justify; }
.np-body p { margin: 0 0 7px; }
.np-good {
  margin-top: 8px; padding: 8px 10px; background: #F0FAF6;
  font-size: 11.5px; color: #0EA372; line-height: 1.7;
  font-family: system-ui, -apple-system, sans-serif;
}
.np-good span { font-weight: 700; }
.np-foot {
  display: flex; justify-content: space-between;
  margin-top: 20px; padding-top: 10px; border-top: 1px solid #D8DEEA;
  font-size: 11.5px; color: #7A839A;
}

/* 打印：只留那张纸。
   ⚠️ .np-sheet 的 min-height 必须清零，否则浏览器会**多吐一张空白页**。 */
@page { size: A4; margin: 0; }
@media print {
  html, body { background: #fff !important; }
  .topbar, .tabbar, .np-bar { display: none !important; }
  .screen { padding: 0 !important; animation: none !important; }
  .np-screen { background: #fff !important; }
  .np-sheet {
    width: 210mm; min-height: 0; margin: 0; padding: 14mm 13mm;
    box-shadow: none; border-radius: 0;
  }
  .np-piece { break-inside: avoid; page-break-inside: avoid; }
  .np-head, .np-title-band { break-after: avoid; page-break-after: avoid; }
}

/* ============================================================
   听写「导出 / 打印」（v2.29.0）
   ------------------------------------------------------------
   给没有手机的住校生：第 1 张词表页（中英文对照，背用），
   第 2 张默写页（只有编号和空行，凭记忆写）。
   ⚠️ 两张纸必须是**两个独立的 A4 块**，靠 break-after: page 分页。
      绝不能拿 `min-height: 297mm` 去撑高度 —— 浏览器会多吐一张空白页
      （英语报那边 .np-sheet 的注释里踩过一次）。
   ============================================================ */
.de-screen { background: var(--bg-2); }
/* ⚠️ 导出页**不能**套 .screen 的 --content-max（宽屏 680）：
   A4 纸本身要 210mm ≈ 794px，被夹到 680 之后只能缩成 640 宽 ——
   这就是老师说的「在希沃大屏上字小得看不清」的真正原因（屏幕再宽，纸也只有 640px）。
   这里只放开**宽度上限**，左右留白仍旧走 --gutter，别的地方一个字都没动。 */
.screen.de-screen { max-width: none; }
/* 说明条 + 按钮：跟着 .de-screen 一起放开宽度限制之后会拉成一整条横幅，
   所以单独给它一个居中的宽度上限（手机上本来就是满宽，不受影响）。 */
.de-bar { padding: 12px 14px; max-width: 760px; margin: 0 auto; }
/* ---- 工具条（v2.49.5 重排）----
   老师原话：「这些图标能不能排列整齐点」。
   以前三行各用各的类（chip / btn-sm / chip），高度 30 / 34 / 30 参差，左边缘也不在一条线上。
   现在统一成一套：**每颗控件都 36px 高、同一套圆角、同一个 8px 间距、左边缘对齐**。 */
.de-tools { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; margin-top: 12px; }
.de-trow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* 两栏 / 单栏：做成真正的分段控件（一个白底槽 + 里面两半），比两颗独立 chip 整齐 */
.de-seg {
  display: inline-flex; align-items: center; gap: 2px;
  height: 36px; padding: 3px; box-sizing: border-box;
  background: #fff; border: 1px solid var(--line); border-radius: 12px;
}
.de-seg button {
  height: 28px; padding: 0 16px; border: 0; border-radius: 9px;
  background: transparent; color: var(--text-2);
  font-size: 13px; font-weight: 600; cursor: pointer;
}
.de-seg button.is-on { background: var(--grad); color: #fff; }
/* v2.49.6：分段控件从 2 颗变 4 颗（单 / 两 / 三 / 四栏），左右留白收窄一档 ——
   4 颗并排约 214px，390 宽的手机一行也装得下，不会把「大屏显示」挤下去。 */
#deCols button { padding: 0 12px; }

/* 单颗按钮 / 图标按钮：和分段控件同高、同圆角、同一行 */
.de-tbtn {
  height: 36px; padding: 0 14px; box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center; gap: 5px;
  border: 1px solid var(--line); border-radius: 12px; background: #fff;
  color: var(--text); font-size: 13px; font-weight: 600; cursor: pointer;
}
.de-tbtn svg { width: 15px; height: 15px; }
.de-tbtn.is-on { background: var(--grad); color: #fff; border-color: transparent; }
.de-tbtn:active, .de-ibtn:active { transform: scale(.96); }
.de-ibtn {
  width: 36px; height: 36px; flex: none; box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); border-radius: 12px; background: #fff;
  color: var(--text); font-size: 17px; line-height: 1; font-weight: 600; cursor: pointer;
}
.de-zlab { font-size: 12.5px; color: var(--text-3); margin-right: 2px; }
/* 数字要定宽：不然 234% → 99% 一变，＋/－ 两颗按钮会左右乱跳 */
.de-zval {
  min-width: 54px; height: 36px; line-height: 36px; text-align: center;
  font-size: 12.5px; color: var(--text-2); font-variant-numeric: tabular-nums;
}


/* 缩放行只在 .is-big 时出现：平时看打印效果就该是 100%，放大键摆在那儿只会让人误点 */
.de-trow#deZoom { display: none; }
.de-screen.is-big .de-trow#deZoom { display: flex; }

/* 两颗下载按钮：等宽、同高，别一个胖一个瘦 */
.de-trow .de-act { flex: 1 1 0; min-width: 0; height: 44px; padding: 0 10px; }

/* ⚠️ 这段必须放在上面那条 `.de-act` 之后：媒体查询不提高优先级，
   同权重的规则永远是**后写的赢** —— 放前面的话 flex-basis 会被 1 0 盖掉，手机上任旧并排溢出。 */
@media (max-width: 759px) {
  .de-trow #deBig { display: none; }
  .de-trow .de-act { flex: 1 1 100%; }
}

.de-zhint { font-size: 12px; color: var(--text-3); margin-top: 8px; line-height: 1.5; }
/* 大屏模式是「投给学生看」的，说明文字占掉一两百像素太浪费 —— 整页显示时正是最缺高度的时候 */
.de-screen.is-big .de-tip { display: none; }
/* ★ v2.49.5：大屏模式下**纸头整块不显示**（老师：「上面这些就不要显示了，直接隐藏」）——
   投在教室大屏上是给全班看词表的，班级 / 布置日期 / 截止 / 「要求：」这些是给老师看的，
   白占掉一屏开头，还把字号能放大的余量吃掉了。⚠️ 只在 .is-big 下藏，打印和 PDF 一个字不变。 */
.de-screen.is-big .de-title,
.de-screen.is-big .de-meta,
.de-screen.is-big .de-note { display: none; }
.de-page {
  width: 210mm; max-width: 100%;
  margin: 0 auto 16px; padding: 9mm 10mm;
  background: #fff; color: #1B2333;
  border-radius: 10px; box-shadow: 0 2px 10px rgba(20, 30, 60, .08);
  /* 打印稿用衬线体更像卷子；中文优先宋体，英文走 Times */
  font-family: "Songti SC", SimSun, "Times New Roman", serif;
}
.de-title {
  font-size: 24px; font-weight: 700; text-align: center;
  letter-spacing: .5px; word-break: break-word;
}
.de-title.sm { font-size: 19px; }
/* v2.49.3：字号整体上抬一档 —— 12.5px 在教室那块希沃大屏上，后排孩子是真的看不清 */
.de-meta { font-size: 13px; color: #5B6478; text-align: center; margin-top: 4px; line-height: 1.55; }
.de-meta.left { text-align: left; margin: 6px 0 6px; }
.de-note { font-size: 13.5px; color: #444B5C; margin-top: 6px; line-height: 1.55; }
.de-h2 { font-size: 16px; font-weight: 700; margin: 9px 0 6px; }

/* ---- 两栏（v2.49.3）----
   左栏 1…N/2、右栏 N/2+1…N，竖着排（像课本），不是横着一行行切。
   ⚠️ 用两张并排的小表，不用 CSS 多栏（columns）—— 表格拆进 columns 会从中间断，
      序号列还可能被甩到上一栏末尾。 */
.de-cols { display: flex; align-items: flex-start; gap: 7mm; }
.de-cols.n1 { display: block; }
.de-col { flex: 1 1 0; min-width: 0; }
.de-cols.n1 .de-col { flex: none; width: 100%; }

.de-tbl { width: 100%; border-collapse: collapse; font-size: 15px; line-height: 1.34; }
.de-tbl th, .de-tbl td { padding: 4px 6px; text-align: left; border-bottom: 1px solid #D7DBE4; }
.de-tbl thead th {
  background: #F2F4F7; font-weight: 600; font-size: 13px;
  border-top: 1.5px solid #9AA1B1; border-bottom: 1.5px solid #9AA1B1;
}
/* ⚠️ 30px 装不下「序号」两个字（13.5px 时会被折成两行，看着像排版坏了）——
   给到 38px 并禁止折行。 */
.de-tbl .de-n { width: 38px; text-align: center; color: #7A839A; font-size: 13px; white-space: nowrap; }
.de-tbl .de-en {
  width: 40%; word-break: break-word;
  /* v2.49.4（老师：「英文字体再大点，加粗，和旁边的中文可以一样高」）——
     Times 的 x-height 天生比汉字小一截，同样 15px 看着就是矮一截、也更淡。
     所以英文单独放大到 17px 并加粗，行高和中文字对齐，两边看起来才一样高。 */
  font-size: 17px; font-weight: 700; letter-spacing: .1px; line-height: 1.3;
}
.de-tbl .de-zh { color: #2A3245; font-size: 15.5px; line-height: 1.3; }
/* 句子行：占满整栏，英文在上、中文小一号跟在下面（v2.49.4） */
.de-tbl .de-s { line-height: 1.35; }
.de-tbl .de-s-en { display: block; font-size: 16.5px; font-weight: 700; }
.de-tbl .de-s-zh { display: block; font-size: 13.5px; color: #5B6478; margin-top: 1px; }

/* ---- 三栏 / 四栏（v2.49.6）----
   老师原话：「不行增加可以自定义栏数」—— 从「两栏 / 单栏」二选一，扩成 1~4 栏。
   ⚠️ 栏数一多每栏就窄：四栏时一栏只剩 160px 左右，17px 的英文一个词能折三行。
      所以字号、栏距、序号列宽按栏数**成档**往下收 —— 这不是"越选越难看"，
      是四栏本来就拿小字换内容量（老师要的正是"一页塞更多"）。
   ⚠️ 用 [data-n] 属性选择器，属性由 fillCols() 写在 .de-cols 元素上，
      和数据无关的 .n1 类并行存在（单栏仍走 .n1）。 */
.de-cols[data-n="3"] { gap: 5mm; }
.de-cols[data-n="4"] { gap: 4mm; }
.de-cols[data-n="3"] .de-tbl { font-size: 13.5px; }
.de-cols[data-n="3"] .de-tbl .de-en { width: 44%; font-size: 15px; }
.de-cols[data-n="3"] .de-tbl .de-zh { font-size: 13.5px; }
.de-cols[data-n="3"] .de-tbl .de-n { width: 30px; font-size: 11px; }
.de-cols[data-n="3"] .de-tbl .de-s-en { font-size: 14.5px; }
.de-cols[data-n="3"] .de-tbl .de-s-zh { font-size: 12px; }
.de-cols[data-n="3"] .de-w { margin-bottom: 12px; }
.de-cols[data-n="3"] .de-w.is-long { margin-bottom: 22px; }

.de-cols[data-n="4"] .de-tbl { font-size: 12px; }
.de-cols[data-n="4"] .de-tbl th, .de-cols[data-n="4"] .de-tbl td { padding: 4px 4px; }
.de-cols[data-n="4"] .de-tbl .de-en { width: 46%; font-size: 13.5px; }
.de-cols[data-n="4"] .de-tbl .de-zh { font-size: 12px; }
.de-cols[data-n="4"] .de-tbl .de-n { width: 26px; padding: 4px 3px; font-size: 10px; }
.de-cols[data-n="4"] .de-tbl .de-s-en { font-size: 13px; }
.de-cols[data-n="4"] .de-tbl .de-s-zh { font-size: 11px; }
.de-cols[data-n="4"] .de-w { margin-bottom: 10px; }
.de-cols[data-n="4"] .de-w.is-long { margin-bottom: 18px; }
.de-cols[data-n="4"] .de-wn { font-size: 12.5px; min-width: 20px; }

/* 默写空行：用带下边框的元素，而不是一串 `_____` ——
   后者在窄屏上**不会折行**，一条长下划线会直接把排版撑破。 */
.de-writes { margin-top: 10px; }
.de-cols .de-writes { margin-top: 0; }
/* ⚠️ v2.49.6：17/34px 改成 16/30px。41 条（其中 10 条句子）在两栏下，默写页量出来
   1163px > A4 的 1123px —— 多出来的 40px 会把最后两行顶到第 3 张纸上，
   老师拿到手就是「后面那张几乎空白」。收 1+4px 后是 1082px，正好一张。
   动这两个数**必须**用 page.pdf() 数页数，别只看屏幕。 */
.de-w { display: flex; align-items: flex-end; gap: 8px; margin-bottom: 16px; }
.de-w.is-long { margin-bottom: 30px; }     /* 句子一行写不下，留高一点 */
.de-wn { font-size: 14.5px; color: #5B6478; flex: none; min-width: 24px; }
.de-line { flex: 1; height: 0; border-bottom: 1px solid #A9AFBE; }

/* ---- 大屏显示（v2.49.3）----
   希沃 / 一体机是横屏，A4 纸只占中间一条；点「大屏显示」把整张纸等比放大到屏幕宽度。
   ⚠️ 放大倍数用行内 `element.style.zoom` 算（见 app.js），这里只把宽度限制放开 ——
      留着 max-width:100% 的话，放大后会被父级再夹一次，等于没放大。 */
.de-screen.is-big .de-page { max-width: none; margin-bottom: 22px; }
/* ★ v2.49.6（老师：「全屏的时候下面的 dock栏隐藏」）
   dock 栏是 position:fixed 的，永远横在屏幕最底下 —— 大屏 / 全屏时会压住纸的最后几行
   （尤其「整页显示」时纸刚好铺满一屏，正好被它盖掉一截），而且投给全班看词表
   也没有切 Tab 的道理。
   · 大屏模式：JS 写在 tabbar 的 inline 上（见 app.js 的 dockOff），退出大屏会还原。
   · 真·浏览器全屏（F11 / ⌃⌘F）：走这里 —— 用 !important 才压得住 inline。
     两条路**互不覆盖**：退出全屏后，大屏模式写在 inline 上的决定还在。 */
html.is-fs .tabbar,
html.is-fs #dock-scrim { display: none !important; }
/* dock 收掉之后，.screen.no-tab 那 96px 的底部留白就没用了，让出来给纸 */
.de-screen.is-big { padding-bottom: calc(var(--safe-b) + 24px); }

@media print {
  .de-screen { background: #fff !important; }
  .de-bar { display: none !important; }
  /* ⚠️⚠️ 在**大屏模式下**直接按 Ctrl+P / 点「打印」时，必须把大屏那套全部还原 ——
     否则 ① 屏幕上藏掉的纸头（标题/班级/要求）在纸上也没了，发下去的纸不知道是哪次听写；
     ② 行内 style.zoom（2.3 倍）会被带进排版，整张纸放大出 A4。
     行内样式优先级高于普通规则，所以这里必须 !important。 */
  .de-screen.is-big .de-title,
  .de-screen.is-big .de-meta,
  .de-screen.is-big .de-note { display: block; }
  .de-screen .de-page { zoom: 1 !important; }
  .de-page {
    width: 210mm; max-width: none; margin: 0; padding: 9mm 10mm;
    box-shadow: none; border-radius: 0;
    break-after: page; page-break-after: always;
  }
  .de-page:last-child { break-after: auto; page-break-after: auto; }
  .de-tbl tr, .de-w { break-inside: avoid; page-break-inside: avoid; }
  /* 词表跨页时表头重复印一遍，学生翻页不用回头找哪列是英文 */
  .de-tbl thead { display: table-header-group; }
}

/* ============================================================
   错词复习段（v2.23.0）
   这一段**不计分**，所以界面上必须一眼看得出来「换段了」——
   否则学生会以为自己在做同一份卷子，还纳闷怎么分数没变。
   ============================================================ */
.play-tag.is-review { background: var(--success-soft); color: var(--success); }
.ans-row.is-review { background: var(--success-soft); }
.ans-row.is-review .ans-head .lv { background: rgba(0, 0, 0, .06); color: var(--success); }

/* ============================================================
   书架：三层结构（分组 > 分册 > 单元）+ 每一层的「改名」小笔（v2.28.0）
   ============================================================ */
/* 书名/分组名后面那支小笔。不能用 .mini-btn —— 那是 34×34，塞进标题行会把字挤走。 */
.nm-btn {
  width: 26px; height: 26px; border-radius: 9px;
  display: inline-grid; place-items: center; vertical-align: -5px;
  margin-left: 6px; flex: none;
  background: var(--bg-2); color: var(--text-3);
  transition: background .18s var(--ease), color .18s var(--ease), transform .18s var(--ease);
}
.nm-btn:active { transform: scale(.92); background: var(--bg-3, var(--bg-2)); color: var(--text-2); }
.nm-btn svg { width: 14px; height: 14px; }

/* 上层分组头（「中考词汇」）：压一层浅灰，一眼能看出它比分册高一级 */
.grp-head { background: var(--bg-2); }
.grp-head .item-title { font-weight: 600; }
/* 分组展开后，里面的分册整体往右缩一点，层级看得出来 */
.book-sub { animation: page-in .2s var(--ease-app); }
.book-sub .book-head { padding-left: 26px; }
.book-sub .book-units .unit-row { padding-left: 32px; }

/* ============================================================
   英语报「选范文」弹层（v2.28.0）
   ------------------------------------------------------------
   ⚠️ 这一行的内容是**范文正文预览**，老师要看着内容挑哪几篇上报纸。
     通用的 .pick-line .zh 是 `nowrap + ellipsis`（给单词表用的，一行放得下），
     套在这里会把「11.5 / 12（96%）」和正文全挤没 —— 所以另起一套可换行的行内结构。
   ============================================================ */
.npk-line { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; min-width: 0; text-align: left; }
.npk-top { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 16px; font-weight: 600; }
.npk-meta { font-size: 12.5px; color: var(--text-3); }
.npk-text {
  font-size: 12.5px; color: var(--text-3); line-height: 1.5;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.npk-pick.on .npk-meta, .npk-pick.on .npk-text { color: rgba(255,255,255,.82); }

/* ============================================================
   选词填空 · 学生作答页（v2.32.0）
   ------------------------------------------------------------
   ⚠️⚠️ 老师点名要的两件事：**词盒固定在上方不动、短文自己滚**。
   实现方式**不是** position:sticky（有些手机闪，而且「到底动没动」没法断言），
   而是「整页锁成一屏的 flex 列」：
     .cz-page  高度 = 一屏、overflow:hidden  → 页面本身永远不滚
     .cz-top   词盒 + 标题，flex:none        → 它是兄弟节点，压根不在滚动区里
     .cz-body  flex:1 + overflow-y:auto      → 只有短文这块滚
   ⚠️ .cz-body 一定要写 `min-height: 0` —— flex 子项默认 min-height:auto，
      内容一长就把父容器顶开，滚的会变成整个页面（词盒就跟着跑了）。
   ============================================================ */
.cz-page {
  height: 100vh; height: 100dvh;
  display: flex; flex-direction: column;
  max-width: 560px; margin: 0 auto;
  background: var(--bg);
  overflow: hidden;
}
.cz-top {
  flex: none; z-index: 2;
  background: var(--card);
  border-bottom: 1px solid var(--line);
  box-shadow: var(--sh-1);
  padding: calc(var(--safe-t) + 10px) 14px 0;
}
.cz-nav { display: flex; align-items: center; gap: 10px; }
.cz-back {
  width: 34px; height: 34px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  background: var(--bg-2); color: var(--text-2);
  transition: transform .18s var(--ease);
}
.cz-back:active { transform: scale(.9); }
.cz-back svg { width: 18px; height: 18px; }
.cz-nav-title {
  flex: 1; min-width: 0; font-size: 16px; font-weight: 600; letter-spacing: -.2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cz-prog { flex: none; font-size: 12.5px; color: var(--text-3); font-variant-numeric: tabular-nums; }

/* 词盒：换行铺开；词多到放不下时它自己滚，但绝不跟着短文滚 */
.cz-words {
  display: flex; flex-wrap: wrap; gap: 8px;
  padding: 12px 0 4px;
  max-height: 30vh; overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.cz-chip {
  padding: 7px 14px; border-radius: 999px;
  background: #fff; color: var(--text);
  border: 1px solid var(--line);
  box-shadow: 0 1px 2px rgba(16,24,40,.04);
  font-size: 15px; font-weight: 550; line-height: 1.4;
  transition: transform .14s var(--ease), background .16s var(--ease), color .16s var(--ease);
}
.cz-chip:active { transform: scale(.93); }
/* 已经填进短文的词变淡（命中判断按词形，不区分大小写）。
   ⚠️ 不用 line-through —— 万一同一词要填两处，一道删除线会让学生以为「这词不能用了」。
   真填错了：点短文里那个方框就能换。 */
.cz-chip.used { background: var(--bg-2); color: #C3C9D6; border-color: var(--line-2); box-shadow: none; }

.cz-hint { font-size: 12.5px; color: var(--text-3); padding: 6px 0 9px; }
.cz-hint:empty { display: none; }

/* 短文区：唯一能滚的地方 */
.cz-body {
  flex: 1; min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  padding: 16px 18px calc(var(--safe-b) + 26px);
}
.cz-lead {
  font-size: 13px; color: var(--primary); line-height: 1.65;
  background: var(--grad-soft); border-radius: var(--r-md);
  padding: 9px 12px; margin-bottom: 15px;
}
.cz-passage {
  font-size: 17px; line-height: 2.05; letter-spacing: .1px;
  color: var(--text); word-break: break-word;
}
/* 结果页里的短文塞在白卡里，字号收一档，省得挤 */
.cz-passage.res { font-size: 15.5px; line-height: 1.95; }

/* 空：一条下划线 + 浅蓝底；填了 = 实线 + 深色字；选中 = 一圈高亮 */
.cz-blank {
  display: inline-block; vertical-align: baseline;
  min-width: 78px; padding: 0 8px; margin: 0 3px;
  border-radius: 8px; border-bottom: 2px solid rgba(59,111,246,.30);
  background: rgba(59,111,246,.05);
  text-align: center; font-weight: 600; color: var(--primary);
  cursor: pointer;
  transition: background .16s var(--ease), box-shadow .16s var(--ease), border-color .16s var(--ease);
}
.cz-blank:empty::after { content: "\00a0"; }   /* 空着也占住宽度，别塌成一小截 */
.cz-blank.filled { border-bottom-color: var(--primary); background: rgba(59,111,246,.12); }
.cz-blank.sel {
  border-bottom-color: var(--primary); background: #fff;
  box-shadow: 0 0 0 2px var(--primary), 0 3px 10px rgba(59,111,246,.22);
}
/* 结果态：不再可点，对错各一套底色 */
.cz-blank.res { cursor: default; border-bottom-color: transparent; box-shadow: none; }
.cz-blank.res.ok { background: var(--success-soft); color: var(--success); }
.cz-blank.res.no { background: var(--danger-soft); color: var(--danger); text-decoration: line-through; text-decoration-color: rgba(239,68,68,.45); }
.cz-blank.res.no.noans { text-decoration: none; }

.cz-foot { margin-top: 28px; }
.cz-foot-hint { font-size: 12.5px; color: var(--text-3); text-align: center; margin-top: 10px; line-height: 1.7; }

/* 听写结果页上的「再来一关」入口卡片 */
.card.cz-next { background: var(--grad-soft); border-color: #DCE5FF; box-shadow: none; }

/* 结果页「这几个空再记一遍」：你填的（划掉）/ 应该是 */
.cz-fix { padding: 9px 0; border-bottom: 1px solid var(--line-2); }
.cz-fix:last-child { border-bottom: none; padding-bottom: 0; }
.cz-fix:first-of-type { padding-top: 0; }
.cz-fix-line { display: flex; align-items: baseline; gap: 8px; line-height: 1.75; min-width: 0; }
.cz-fix-line + .cz-fix-line { margin-top: 2px; }
.cz-fix-line .t3 { flex: none; }
.wrong-txt {
  color: var(--danger); text-decoration: line-through; text-decoration-color: rgba(239,68,68,.5);
  font-weight: 600; word-break: break-word;
}
.ok-txt { color: var(--success); font-weight: 600; word-break: break-word; }

/* 能点开的榜单行（v2.33.0：点姓名看积分记录） */
.lb-row.lb-click { cursor: pointer; }
.lb-row.lb-click:active { background: var(--bg-2); }

/* 积分记录弹层（v2.33.0） */
.pr-row { display: flex; align-items: center; gap: 10px; padding: 11px 14px; border-bottom: 1px solid var(--line-2); }
.pr-row:last-child { border-bottom: none; }
.pr-main { flex: 1; min-width: 0; }
.pr-title { font-size: 14.5px; font-weight: 600; word-break: break-word; }
.pr-sub { font-size: 12.5px; color: var(--text-3); margin-top: 2px; }
.pr-pt { flex: none; font-size: 15px; font-weight: 700; color: var(--primary); font-variant-numeric: tabular-nums; }

/* ============================================================
   v2.34 · 冰川蓝磨砂玻璃 · iOS 丝滑转场
   ============================================================ */

/* ---- 方向感知转场：push 从右滑入 / pop 从左滑入 / 切 Tab 淡入上浮 ----
   方向由 app.js 里的 NAVDIR 写到 #app[data-nav] 上（只动标记，不动业务）。 */
@keyframes page-push {
  from { opacity: 0; transform: translateX(32px); }
  to   { opacity: 1; transform: none; }
}
@keyframes page-pop {
  from { opacity: 0; transform: translateX(-32px); }
  to   { opacity: 1; transform: none; }
}
@keyframes page-tab {
  from { opacity: 0; transform: translateY(12px) scale(.985); }
  to   { opacity: 1; transform: none; }
}
#app[data-nav="push"] .screen { animation: page-push .36s var(--ease-app); }
#app[data-nav="pop"]  .screen { animation: page-pop .36s var(--ease-app); }
#app[data-nav="tab"]  .screen { animation: page-tab .30s var(--ease-app); }
/* 二次绘制（先缓存后对齐）仍然绝不重播动画 */
#app.no-anim .screen { animation: none !important; }

/* ---- v2.40：这几块原来是「自带底色 + 开模糊」，现在只留底色 ---- */
.home-head, .bm-card {
  border: 0.5px solid rgba(255,255,255,.80);
}
.bm-card::before {
  width: 4px; border-radius: 0 4px 4px 0;
  background: var(--grad);
}
.cz-page { background: transparent; }
.cz-top {
  background: rgba(255,255,255,.92);
  border-bottom: 0.5px solid rgba(255,255,255,.85);
}
/* 选词填空的词盒 chip / 结果提示卡跟上玻璃风 */
.cz-chip { background: rgba(255,255,255,.80); border-color: rgba(255,255,255,.9); }
.cz-next { background: rgba(255,255,255,.55) !important; border-color: rgba(255,255,255,.85) !important; }

/* ---- 播放页(沉浸深色)里的主控钮 ----
   ⚠️ 这里原来直接用 --grad（#3FC3FF→#8B6BFF 青到紫的高饱和渐变）+ 0.42 的蓝紫外发光，
      在近黑的底上亮得刺眼（学生反馈「播放按钮颜色太亮了」）。
      现在压暗一档、去掉外发光，只留一枚很浅的内高光，
      亮度落在环形进度条（#7BA0FF / #B08CFF）之下，主次才对。 */
.pc.primary {
  background: linear-gradient(135deg, #4667D6 0%, #6349C9 100%);
  color: #EEF2FF;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.20),
    0 5px 14px rgba(20,32,80,.40);
}
.play-progress .bar > i, .play-zh-bar > i { background: linear-gradient(90deg, #38B6FF, #8A63FF); }

/* ---- 打印：极光背景不许上纸 ---- */
@media print {
  body::before { display: none !important; }
}

/* ---- 减少动态效果：转场一并关掉 ---- */
@media (prefers-reduced-motion: reduce) {
  #app[data-nav] .screen { animation-duration: .01ms !important; }
}

/* ============================================================
   v2.35 · 冰透玻璃升级
   ------------------------------------------------------------
   上一版是「白卡 + 模糊」，看着还是白的 —— 因为白度太高、背景太淡，
   玻璃后面没有颜色可以透。这一版按 iOS 毛玻璃的三条规律重做：
     ① 背景必须够彩（body::before 已加强）→ 透光才有内容
     ② 玻璃白度压低（.50 → .24 渐变）+ saturate 拉到 200% → 透出背后色相
     ③ 高光棱边 + 下沿冰蓝 + 内层雾感 → 玻璃的"厚度"
   ============================================================ */

/* ---- 底部弹层：更透的冰玻璃 ---- */
.sheet {
  background: linear-gradient(168deg, rgba(255,255,255,.62), rgba(255,255,255,.44));
  backdrop-filter: blur(40px) saturate(200%);
  -webkit-backdrop-filter: blur(40px) saturate(200%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.95),
    0 -12px 44px rgba(72,112,178,.22);
}

/* ---- 输入框：冰面质感 ---- */
.input, textarea.input, select.input {
  background: linear-gradient(160deg, rgba(255,255,255,.86), rgba(255,255,255,.72));
  border-color: rgba(255,255,255,.75);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.9), 0 1px 3px rgba(72,112,178,.06);
}
.input:focus, textarea.input:focus, select.input:focus { border-color: var(--primary); }
/* 积分设置里「这一档填错了」的输入框 —— 报错要指到具体那一格，光弹一句 toast 老师找不到人 */
.input.pp-bad {
  border-color: var(--danger) !important;
  background: var(--danger-soft) !important;
  box-shadow: 0 0 0 3px rgba(244,120,140,.18) !important;
}

/* ---- 小片玻璃：勾选行 / 答案行 / 名单行 / 词盒 chip / 标签 ----
   v2.40 · 这一组是**滚动流里数量最多**的元件（一页动辄几十个），
   每个都挂 backdrop-filter 就是「滑动不流畅」的元凶。全部去掉，只留底色和阴影。 */
.check-row, .ans-row, .pick, .cz-chip, .rank-banner, .home-head, .bm-card, .md-body, .drag-ghost, .cz-next, .note-card {
  border: 1px solid rgba(255,255,255,.60);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.85),
    inset 0 -8px 18px -12px rgba(110,168,255,.55),
    0 6px 22px rgba(72,112,178,.12) !important;
}
.cz-chip {
  background: linear-gradient(158deg, rgba(255,255,255,.88), rgba(255,255,255,.74));
}
.check-row.right, .check-row.wrong, .ans-row.right, .ans-row.wrong { box-shadow: none !important; }

.chip, .pill, .lv, .seg, .cz-blank:not(.res) { background-clip: padding-box; }
.chip { background: linear-gradient(158deg, rgba(255,255,255,.88), rgba(255,255,255,.70)); }
.seg {
  background: linear-gradient(160deg, rgba(255,255,255,.50), rgba(255,255,255,.32));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.85), inset 0 0 0 1px rgba(255,255,255,.40);
}
.seg button.on {
  background: linear-gradient(160deg, rgba(255,255,255,.92), rgba(255,255,255,.72));
  box-shadow: 0 3px 12px rgba(72,112,178,.14), inset 0 1px 0 #fff;
}

/* ---- 圆形小玻璃钮：返回 / 关闭 / 工具钮 ---- */
.back-btn, .sheet-close, .mini-btn, .nm-btn, .cz-back, .bm-ico, .rev-play, .st-msg {
  background: linear-gradient(158deg, rgba(255,255,255,.90), rgba(255,255,255,.72));
  border: 0.5px solid rgba(255,255,255,.80);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.9), 0 3px 10px rgba(72,112,178,.12);
}
.sheet-close { border: 0.5px solid rgba(255,255,255,.80); }

/* ---- 列表 / 书架里的整块白底也跟上（否则展开区一白就"破功"） ---- */
.book-units { background: linear-gradient(168deg, rgba(255,255,255,.80), rgba(255,255,255,.66)); }

/* ---- 登录卡 ---- */
.login-card {
  background: linear-gradient(158deg, rgba(255,255,255,.86), rgba(255,255,255,.70));
  border: 1px solid rgba(255,255,255,.62);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.95), 0 18px 50px rgba(31,41,90,.16);
}

/* ---- 打印：颗粒层不上纸 ---- */
@media print { body::after { display: none !important; } }

/* ---- 玻璃上的按压反馈：白 40% 在冰面上几乎看不见，换成冰蓝按下色 ---- */
.item:active, .todo-row:active, .rv-row:active, .mk-row:active, .lb-row.lb-click:active, .book-units .unit-row:active {
  background: rgba(120,180,255,.16) !important;
}

/* ============================================================
   v2.36 · 对齐设计稿的字号层级与元件提白
   ============================================================ */

/* ---- 顶栏标题收小（设计稿是 17px 的小顶栏，大字留给页面内容） ---- */
/* v2.40 · 顶栏标题统一 17px（理由见文件开头 .topbar 那一段）。 */
.topbar h1, .topbar h1.sm { font-size: 17px; font-weight: 700; letter-spacing: -.2px; }

/* ---- Section 标题放大加粗（设计稿「全部听写」≈ 20px 粗体深蓝） ---- */
.section-title { font-size: 18px; font-weight: 700; letter-spacing: -.3px; margin: 22px 2px 10px; }

/* ---- 列表卡：更高、图标更大，留白向设计稿看齐 ---- */
.item { padding: 16px 18px; gap: 14px; }
.item-title { font-size: 16.5px; font-weight: 700; color: var(--text); }
.item-sub { font-size: 13px; color: var(--text-3); }
.avatar { width: 46px; height: 46px; border-radius: 15px; }

/* ---- v2.35 的小玻璃件整体提白（透色收敛到「微微透」） ---- */
.sheet {
  background: linear-gradient(168deg, rgba(255,255,255,.90), rgba(255,255,255,.80));
  backdrop-filter: blur(32px) saturate(190%);
  -webkit-backdrop-filter: blur(32px) saturate(190%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.95), 0 -10px 40px rgba(96,130,190,.18);
}
.input, textarea.input, select.input {
  background: linear-gradient(160deg, rgba(255,255,255,.85), rgba(255,255,255,.70));
  border-color: rgba(255,255,255,.95);
}
.check-row, .ans-row, .pick, .rank-banner, .home-head, .bm-card, .md-body, .drag-ghost, .cz-next, .note-card {
  border: 1px solid rgba(255,255,255,.90);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.92),
    inset 0 -8px 16px -14px rgba(110,168,255,.24),
    0 5px 16px rgba(96,130,190,.09) !important;
}
.chip { background: linear-gradient(158deg, rgba(255,255,255,.88), rgba(255,255,255,.72)); }
.back-btn, .sheet-close, .mini-btn, .nm-btn, .cz-back, .bm-ico, .rev-play, .st-msg {
  background: linear-gradient(158deg, rgba(255,255,255,.88), rgba(255,255,255,.66));
  border: 0.5px solid rgba(255,255,255,.95);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.95), 0 2px 8px rgba(96,130,190,.10);
}
.seg { background: linear-gradient(160deg, rgba(255,255,255,.80), rgba(255,255,255,.62)); }
.cz-top { background: linear-gradient(160deg, rgba(255,255,255,.88), rgba(255,255,255,.76)); }
.book-units { background: linear-gradient(168deg, rgba(255,255,255,.86), rgba(255,255,255,.74)); }
.login-card {
  background: linear-gradient(158deg, rgba(255,255,255,.88), rgba(255,255,255,.74));
}
/* 按钮主色阴影柔一点 */
.btn-primary { box-shadow: 0 10px 24px rgba(94,123,255,.32), inset 0 1px 0 rgba(255,255,255,.35); }

/* ============================================================
   v2.36b · hero 统计行对齐设计稿：「12 个听写任务 · 8 人已完成」
   数字与文字同行（基线对齐），两个 stat 之间补一枚「·」分隔点。
   内容零改动，纯排版还原。
   ============================================================ */
.hero .hero-row { align-items: baseline; gap: 0; }
.hero .hero-stat { display: flex; align-items: baseline; gap: 6px; }
.hero .hero-stat b { font-size: 24px; }
.hero .hero-stat span { font-size: 16px; font-weight: 650; color: var(--text); }
.hero .hero-stat + .hero-stat { margin-left: 10px; }
.hero .hero-stat + .hero-stat::before {
  content: "·";
  font-size: 22px; font-weight: 700; color: var(--text-2);
  margin-right: 12px; align-self: center;
}
/* v2.39 · 三个统计（书架：分册 / 单元 / 内容）在 390px 上排不进一行，
   一折行那个「·」就跑到行首变成「· 4951 条内容」——很难看。
   三个的时候改成三栏「数字在上、说明在下」，等宽居中：多少位数字都不会挤。 */
.hero .hero-row.cols-3 { flex-wrap: nowrap; gap: 0; }
.hero .hero-row.cols-3 .hero-stat {
  flex: 1 1 0; min-width: 0;
  flex-direction: column; align-items: center; gap: 0;
}
.hero .hero-row.cols-3 .hero-stat + .hero-stat { margin-left: 0; }
.hero .hero-row.cols-3 .hero-stat + .hero-stat::before { display: none; }
.hero .hero-row.cols-3 .hero-stat b { font-size: 23px; line-height: 1.15; }
.hero .hero-row.cols-3 .hero-stat span { font-size: 13.5px; font-weight: 500; color: var(--text-2); }

/* ============================================================
   v2.37 · 按《移动应用 UI 核心原则》(Kole Jain) 校准
   ------------------------------------------------------------
   导航/尺度/内容/一页一任务/手势/动态感/空状态
   本应用已符合大半，这一版补齐缺口：
   ①尺度：root 页 iOS Large Title（28px/800/行高1.15/字距收紧）
   ②内容减法：箭头减淡、标题行高收紧
   ③空状态：放大图标底 + 层级
   ④手势反馈：下拉刷新指示器（JS 在 app.js）
   ============================================================ */

/* ---- 尺度：大标题只给 root Tab 页（无返回键的 topbar），子页保持 17px ---- */
/* v2.40 · 顶栏不再用 28px 大字 —— 那是「一条横带」时代的写法，
   现在顶栏只是返回键旁边一行小字，大字留给页面内容。 */
.topbar h1, .topbar h1.sm { font-size: 17px; font-weight: 700; letter-spacing: -.2px; line-height: 1.3; }
/* 标题一律收紧行高（教程：大字 110-120% 才专业） */
h2, h3 { line-height: 1.2; }
.hero h2 { letter-spacing: -.6px; }

/* ---- 内容减法：能进详情的行才保留箭头，但让它退到视线最后 ---- */
.item-arrow { color: #C9D3E6; }
.item-arrow svg { width: 16px; height: 16px; }

/* ---- 尺度：section 标题升到 20px/800，扫一眼就知道分节 ---- */
.section-title { font-size: 20px; font-weight: 800; letter-spacing: -.4px; margin: 24px 2px 10px; }
.section-title:first-child { margin-top: 10px; }

/* ---- 空状态：图标底放大 + 三层信息分明 + 柔光，绝不白屏 ---- */
.empty { padding: 56px 24px; }
.empty .ico {
  width: 88px; height: 88px; border-radius: 28px; margin-bottom: 18px;
  background: linear-gradient(150deg, rgba(255,255,255,.95), rgba(255,255,255,.72));
  border: 1px solid rgba(255,255,255,.9);
  box-shadow:
    inset 0 1px 0 #fff,
    inset 0 -10px 18px -14px rgba(110,168,255,.35),
    0 12px 30px rgba(96,130,190,.14);
  position: relative;
}
.empty .ico::after {
  content: ""; position: absolute; inset: -14px; border-radius: 36px; z-index: -1;
  background: radial-gradient(circle, rgba(150,200,255,.28), transparent 70%);
}
.empty .ico svg { width: 40px; height: 40px; }
.empty h4 { font-size: 16.5px; font-weight: 700; color: var(--text); margin-bottom: 7px; }
.empty p { font-size: 14px; line-height: 1.7; }

/* ---- 手势反馈：下拉刷新指示器（跟随手指透明度，释放触发刷新） ---- */
#pull-hint {
  position: fixed; top: calc(var(--safe-t) + 10px); left: 50%;
  transform: translateX(-50%) translateY(-60px);
  z-index: 80; height: 36px; padding: 0 16px; border-radius: 999px;
  background: rgba(255,255,255,.92);
  box-shadow: 0 8px 24px rgba(96,130,190,.20), inset 0 1px 0 #fff;
  display: flex; align-items: center; gap: 8px;
  font-size: 12.5px; font-weight: 650; color: var(--text-2);
  opacity: 0; pointer-events: none;
  transition: opacity .2s ease, transform .28s var(--ease);
  backdrop-filter: blur(18px) saturate(180%);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
}
#pull-hint.show { opacity: 1; transform: translateX(-50%) translateY(0); }
#pull-hint svg { width: 16px; height: 16px; color: var(--primary); transition: transform .08s linear; }
#pull-hint.loading svg { animation: spin 1s linear infinite; }
#pull-hint.loading { color: var(--primary); }

/* ---- 动态感：所有可点卡片统一按压反馈（缺的补上） ---- */
.card.item:active, button.card:active { transform: scale(.985); transition: transform .14s var(--ease); }

/* ============================================================
   v2.39 · 「有新版本」提示条（见 app.js 的 VERSION_WATCH）
   ------------------------------------------------------------
   悬在 dock 栏上方（72 = dock 底 12 + 高 58 + 2 的余量），不挡住任何操作。
   故意做成「要点一下才刷新」—— 自动刷新会把学生正在写的字冲掉。
   ============================================================ */
.new-ver-bar {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(var(--safe-b) + 84px);
  z-index: 90;
  display: flex; align-items: center; gap: 8px;
  padding: 11px 17px; border-radius: 999px;
  background: linear-gradient(160deg, rgba(255,255,255,.97), rgba(255,255,255,.88));
  border: 1px solid rgba(255,255,255,.95);
  box-shadow: 0 12px 30px rgba(96,130,190,.24), inset 0 1px 0 #fff;
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  font-size: 13.5px; font-weight: 650; color: var(--primary);
  white-space: nowrap;
  animation: ver-in .34s var(--ease);
  transition: transform .16s var(--ease);
}
.new-ver-bar:active { transform: translateX(-50%) scale(.96); }
.new-ver-bar[disabled] { opacity: .7; }
.new-ver-bar .nv-dot {
  width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: var(--danger);
  box-shadow: 0 0 0 3px rgba(239,90,111,.18);
}
@keyframes ver-in {
  from { opacity: 0; transform: translateX(-50%) translateY(14px) scale(.94); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
}

/* ============================================================
   v2.38 · SF Symbols 图标系统落地（对齐 design-ios-spec.html）
   ------------------------------------------------------------
   squircle 白玻璃托 + 每功能专属渐变色相：
   听写=冰蓝 / 作文=暖橙 / 单元=薄荷绿 / 我的=紫 / 排行=金
   ============================================================ */

/* ---- Tab 图标：每个 .tab 两个 svg（渐变版在前、线性版在后） ---- */
.tab svg + svg { display: none; }          /* 默认显示第一个（渐变版）？不 —— 默认未选中该显示线性版 */
.tab:not(.on) svg:first-child { display: none; }   /* 未选中：隐藏渐变版 → 显示线性版 */
.tab.on svg + svg { display: none; }       /* 选中：只显示渐变版 */

/* ---- squircle 白玻璃托（列表头像/图标底座统一换新） ---- */
.avatar {
  width: 46px; height: 46px; border-radius: 14.5px; flex: none;
  display: grid; place-items: center;
  background: linear-gradient(160deg, rgba(255,255,255,.96), rgba(255,255,255,.62));
  border: 1px solid rgba(255,255,255,.95);
  box-shadow: inset 0 1px 0 #fff, inset 0 -7px 12px -10px rgba(110,168,255,.45), 0 4px 12px rgba(96,130,190,.14);
  /* 旧代码里 avatar 用 color:var(--primary) 传给 stroke 图标 —— 新渐变填充图标自带颜色 */
}
.avatar svg { width: 25px; height: 25px; }
.avatar.round { border-radius: 50%; }
/* avatar 里如果还残留纯色文字（姓名缩写），保持主色 */
.avatar { color: var(--primary); font-weight: 700; }

/* ---- 提示卡/小图标底座跟上 ---- */
.tip-ico, .pt-card .pt-ico, .tr-ico, .bm-ico, .st-msg {
  background: linear-gradient(160deg, rgba(255,255,255,.96), rgba(255,255,255,.62)) !important;
  border: 1px solid rgba(255,255,255,.95);
  box-shadow: inset 0 1px 0 #fff, inset 0 -7px 12px -10px rgba(110,168,255,.45), 0 4px 12px rgba(96,130,190,.14);
  color: var(--primary);
}
.tip-ico svg, .pt-card .pt-ico svg, .tr-ico svg { width: 22px; height: 22px; }

/* ---- 登录 logo：换成新渐变图标本体（I.speaker 现在自带渐变） ---- */
.login-logo svg { width: 40px; height: 40px; }

/* v2.38b · hero 三统计时不许把文字挤折行：允许整组换行，stat 内部禁止折行 */
.hero .hero-row { flex-wrap: wrap; row-gap: 8px; }
.hero .hero-stat span { white-space: nowrap; }

/* ============================================================
   v2.40 · 滑动性能（重要 —— 改样式之前先读这一段）
   ------------------------------------------------------------
   学生反馈「界面滑动的时候卡顿，不流畅」。
   手机上掉帧的两个大头，都出在「跟着页面滚的层」上：

   ① backdrop-filter（毛玻璃）
      .card / .chip / .check-row / .ans-row / .pick / .home-head / .bm-card /
      .input … 这些**在滚动流里、一页几十个**的元件以前全都挂着 blur(22px)。
      浏览器得为它们各自单独合成一层，滚一帧就重算一次身后那块内容的模糊。
      —— 现在全部改成「近白底 + 阴影」，不用 blur。观感几乎没差（底色本来就是
      .78–.90 的白），但滚动回到纯合成。
   ② mix-blend-mode: overlay（body::after 的噪点层）
      它是一整屏大小、position: fixed 的层。叠加混合意味着每滚一帧都要把
      整块固定层和下面整篇内容重新混一次 —— 已改成普通叠放 + opacity:.045。

   ⚠️ 仍然保留 backdrop-filter 的地方，只有**偶尔出现、面积小或无滚动背景**的
      覆盖层：.sheet（底部弹层）、.sheet-mask、.toast、.play-core、登录卡。
      给「常驻 + 压着滚动内容」的元件（.tabbar / .bottom-bar / .dock-scrim /
      .topbar）加 backdrop-filter 是禁区 —— 那正是最贵的一种用法。

   ⚠️ 还有一条硬约束：主 Tab 页没有顶栏，安全区顶部留白靠 #app.no-tb .screen 补。
      谁要是把这条规则删了/改了，内容就会钻到刘海底下。
   ============================================================ */

/* ============================================================
   v2.42 · 头像 / 层级树 / 积分商店
   ------------------------------------------------------------
   ① 头像 `.avv` —— 三种来源画成同一个形状：内置（CSS 渐变 + 表情）、
      上传的照片（<img>，走鉴权接口）、没设（姓名末两字）。
      形状统一是**必须**的：榜单里一行里可能三种都有，形状不一样会看着像列表错位。
      ⚠️ 这里**不放 backdrop-filter**（见上面 v2.40 那段）：头像会出现在滚动流里，
         而且一页几十个（榜单 / 名单 / 商店）。
   ② 层级树 `.tn-*` —— 深度靠 `--d` 这个 CSS 变量一层层传下去
      （父容器写 `--d: n+1`，行自己读 `calc()`）。不用「每层写死 padding-left」是因为
      老师一「换个层级」，整棵子树的深度全变，写死就得重算一整棵树。
   ============================================================ */

/* ---- 头像 ---- */
.avv {
  display: grid; place-items: center; flex: none;
  width: 46px; height: 46px; border-radius: 15px;
  overflow: hidden; font-size: 20px; line-height: 1;
  font-weight: 700; color: #fff;
  /* ⚠️ 头像框 `.avv-ring` 是**绝对定位在里面**的一层环（见下面那段）：
     靠的就是这个 position:relative。删了它环会跑到页面左上角去。 */
  position: relative;
}
.avv img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* 没设过头像的人（v2.45 起是**所有人**的默认样子）：就画姓名末两个字。
   ⚠️ 底色必须够实、够彩。原来是 `--grad-soft`（近白冰蓝）——
      在白色卡片上，头像本身看着就像一块空白，戴上头像框以后
      「框和头像之间」更像隔了一圈空隙（老师原话：「四边有空隙，难看」）。
      换成品牌蓝渐变 + 白字：框贴着它有对比，看着就是「一个头像」而不是「一个框」。 */
.avv-txt {
  background: linear-gradient(150deg, #7FB4FF 0%, #5E7BFF 55%, #6B5CF0 100%);
  color: #fff;
  font-size: 17px; letter-spacing: -.5px;
  text-shadow: 0 1px 2px rgba(24,48,110,.28);
}
.av-wrap { display: flex; align-items: center; flex: none; }
.av-off { filter: grayscale(1); opacity: .5; }   /* 已停用的学生：头像也灰掉 */

/* 榜单行里那一格头像 */
.lb-face { flex: none; display: flex; align-items: center; }
/* 领奖台上的头像：原来是渐变圆形里写姓名首字，现在换成同一个 .avv（没设头像的人仍走老样子） */
.podium-av2 { margin: 0 auto 6px; display: flex; justify-content: center; }
.podium-av2 .avv { box-shadow: 0 6px 16px rgba(72,112,178,.20); }

/* ============================================================
   头像框（v2.42.0 追加）
   ------------------------------------------------------------
   老师要的：「增加头像框……头像整体保持一致」。
   所以框**不是**「头像外面再包一层 div」—— 那就又多出一个形状，
   榜单 / 名单 / 领奖台 / 商店四处各写一份，迟早长得不一样。
   做法：环是 `.avv` **里面**绝对定位的一层（inset:0，圆角 inherit），
   `avatarHTML()` 一个函数把三种头像 + 任意框一次画完，全站同一个形状。

   ⚠️ 环的渐变用 `mask` 挖空中间（业界标准写法）：
      没挖空的 `background` 会把整块头像盖住。@supports 不认时退回 pure `border` 的纯色环，
      最差也是一圈实色框，不会变成「一片色块糊住脸」。
   ⚠️ `pointer-events:none`：环只是为了看，别把「点头像换头像」的手势抢走。
   ⚠️ 环是 46px 的小块静态元素，不在滚动重绘热点上，也没有 blur —— 合规 v2.40 那条红线。
   ============================================================ */
.avv-ring {
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
  border-radius: inherit;
  border: var(--fw, 3px) solid var(--fc, #FFB300);
}
@supports ((mask-composite: exclude) or (-webkit-mask-composite: xor)) {
  .avv-ring {
    border-color: transparent;
    background-image: var(--fg);
    /* ⚠️⚠️ 这两行不能省（v2.45.1 修）：
       `background-origin` 默认是 **padding-box**，而渐变是画到 border-box 的 —— 于是
       渐变只铺满「边框里面那一块」，外面那圈 3~5px 的 band 拿到的是**重复/延伸出来的另一段颜色**。
       肉眼看到的就是「角上和边上不是同一个色」的那块（老师截图圈出来的正是这里）。
       铺满整个盒子 + 不重复，这一圈才是同一条渐变的边。 */
    background-origin: border-box;
    background-repeat: no-repeat;
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    mask-composite: exclude;
  }
}
/* 上传的照片压在环下面（`<img>` 不带定位，环带 z-index:2 本来就压得住，
   这里再钉一遍是防将来有人给 img 加了 transform 之类，层叠顺序会被改掉）。 */
.avv > img { position: relative; z-index: 1; }

/* ============================================================
   画出来的限定框（v2.46.2，运动会）
   ------------------------------------------------------------
   金环/银环那几档是「一圈渐变」，一条 CSS 就够。运动会这档不是环：
   跑道要压住头像四边、彩旗伸到右上角外面、金牌挂在左下角外面 ——
   **元素故意画到 100×100 的画布外面**，所以这里只做三件事：
     ① 放行溢出：`.avv` 平时是 `overflow:hidden`（照片要靠它裁圆角），
        对这一档必须打开，否则旗子和金牌会被切掉一半；
     ② 溢出以后 `<img>` 没人裁了 → 自己带 `border-radius: inherit`，
        不加就是「圆头像 + 四角露白边」；
     ③ SVG 铺满整格 + 自己也不裁（`overflow:visible` 在根 `<svg>` 上
        默认是 hidden，只在 CSS 里写得开）。
   ⚠️ 这一层是 `pointer-events:none`：点头像换头像的手势不能被它抢走。
   ⚠️ 不用 `backdrop-filter`、不新增动画 —— 榜单一页几十个头像，
      违反 v2.40 那条红线就是学生反馈过的「滑动卡顿」。 */
.avv-fx { overflow: visible; }
.avv-fx > img { border-radius: inherit; }
.avv-art {
  position: absolute; inset: 0; z-index: 3; pointer-events: none;
  border-radius: inherit;
}
.avv-art svg {
  width: 100%; height: 100%; display: block;
  overflow: visible;          /* 旗子 / 金牌伸到盒子外面靠它 */
}

/* 「点自己的头像换头像」——头像本身就是按钮，不再单开一行。
   铅笔角标是**唯一**的可见提示：不点它也能点，但没有它学生根本猜不到头像能点。 */
.av-btn {
  position: relative; flex: none; padding: 0; border: 0; background: none;
  -webkit-tap-highlight-color: transparent;
}
.av-btn:active .avv { transform: scale(.95); }
.av-btn .avv { transition: transform .12s var(--ease-app); }
.av-edit {
  position: absolute; right: -4px; bottom: -4px; z-index: 3;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--primary); color: #fff;
  display: grid; place-items: center;
  box-shadow: 0 2px 6px rgba(46,107,247,.35), 0 0 0 2px #fff;
}
.av-edit svg { width: 12px; height: 12px; }

/* 框的九宫格：每个格子就是一个「真头像 + 那个框」的小样（还是 avatarHTML 画的），
   所见即所得，选之前不用想象自己戴上好不好看。 */
.av-fcell {
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  padding: 8px 0 6px; border: 0; border-radius: 14px; background: none;
  -webkit-tap-highlight-color: transparent;
}
.av-fcell:active { transform: scale(.93); }
/* ⚠️ 选中态**不要**在头像上加蓝色外环！那会和学生真正戴的框抢眼睛 ——
   一眼看过去分不清「这个是我选的」还是「这个框是蓝的」。
   选中就画在**格子**上（淡蓝底 + 一圈淡蓝边），框只由头像自己那一圈表示。 */
.av-fcell.on { background: rgba(94,155,255,.14); box-shadow: inset 0 0 0 1.5px rgba(94,155,255,.5); }
.av-fname { font-size: 11.5px; color: var(--text-3); line-height: 1; white-space: nowrap; }
.av-fcell.on .av-fname { color: var(--primary); font-weight: 600; }

/* ---- 层级树 ---- */
.tn-node { border-bottom: 1px solid var(--line-2); }
.tn-node:last-child { border-bottom: none; }
.tn-row {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 16px 14px calc(16px + var(--d, 0) * 15px);
  border-bottom: 1px solid var(--line);
  transition: background .16s var(--ease);
}
.tn-row:last-child { border-bottom: none; }
.tn-row:active { background: var(--bg-2); }
.tn-body { flex: 1; min-width: 0; }
.tn-body .item-title { font-size: 15.5px; line-height: 1.35; word-break: break-word; }
.tn-body .item-sub { font-size: 12.5px; }
.tn-fico { width: 38px; height: 38px; border-radius: 12px; }
.tn-fico svg { width: 21px; height: 21px; }
.tn-row .mini-btn { flex: none; }
.tn-row .book-caret { flex: none; }
.tn-kids { animation: page-in .2s var(--ease-app); }
/* 「往这一层里加一层」——只在老师的整理模式下出现 */
.tn-addrow {
  display: flex; align-items: center; gap: 6px; width: 100%;
  padding: 12px 16px 12px calc(16px + var(--d, 0) * 15px);
  color: var(--primary); font-size: 14px; font-weight: 550;
  border-bottom: 1px solid var(--line);
  background: rgba(94,155,255,.06);
}
.tn-addrow svg { width: 16px; height: 16px; }
.tn-addrow:active { background: rgba(94,155,255,.14); }

/* ---- 「换到别的层级」的候选列表 ---- */
.mv-item {
  display: block; width: 100%; text-align: left;
  padding: 14px 16px; margin-bottom: 8px;
  border-radius: var(--r-md);
  background: linear-gradient(158deg, rgba(255,255,255,.88), rgba(255,255,255,.72));
  border: 1px solid rgba(255,255,255,.90);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.95), 0 2px 8px rgba(96,130,190,.10);
  transition: transform .16s var(--ease);
}
.mv-item:active { transform: scale(.99); }
.mv-item.on { opacity: .55; }
.mv-name { font-size: 15px; font-weight: 550; color: var(--text); line-height: 1.5; word-break: break-word; }
.mv-tip { font-size: 12.5px; color: var(--text-3); margin-top: 3px; }

/* ---- 换头像弹层 ---- */
.av-me { display: flex; align-items: center; gap: 14px; }
/* 框的九宫格。一行 6 个（含「不加框」）。
   ⚠️ v2.45.1 之前这里还并排着 `.av-cell` / `.av-lock`（12 个内置表情头像 + 右下角价签）——
      内置头像整块下架，那两个类**删掉了**，别看着眼熟又搬回来。 */
/* 四列（v2.46.2 由 6 列改成 4 列）：框从 6 个变成 7 个，6 列会变成「一排 6 个 + 光秃秃 1 个」；
   4 列刚好 4+3，再出一个限定框就是 4+4。格子里那个 44px 的小样也是 avatarHTML 画的。 */
.av-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px 10px; }

/* ---- 积分商店 ---- */
.shop-row {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 16px; border-bottom: 1px solid var(--line);
}
.shop-row:last-child { border-bottom: none; }
.shop-body { flex: 1; min-width: 0; }
.shop-name { font-size: 15.5px; font-weight: 600; line-height: 1.4; word-break: break-word; }
.shop-note { font-size: 12.5px; color: var(--text-3); margin-top: 3px; line-height: 1.55; }
.shop-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 7px; }
.shop-tag {
  font-size: 11.5px; padding: 2px 8px; border-radius: 999px;
  background: var(--bg-2); color: var(--text-2); white-space: nowrap;
}
.shop-tag.once { background: rgba(255,138,0,.15); color: #B4620A; }
.shop-tag.never { background: rgba(11,191,149,.15); color: #067A60; }
.shop-tag.no { background: var(--danger-soft); color: var(--danger); }
/* 老师赠送（v2.55.5）：和「学生自己兑的」用同一个色系但更淡 —— 它是记账不是奖励 */
.shop-tag.gift { background: rgba(123,160,255,.16); color: #5B7FE0; }
.shop-buy { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 7px; }
.shop-cost { font-size: 16px; font-weight: 750; color: var(--primary); white-space: nowrap; font-variant-numeric: tabular-nums; }
.shop-cost i { font-size: 11px; font-style: normal; font-weight: 600; }

/* 商品图标选择用的方片（.chip 本体是文字胶囊，这是它的图标版） */
.chips .chip svg { width: 19px; height: 19px; }


/* ============================================================
   v2.43 · 任务提醒（站内那一半）
   ------------------------------------------------------------
   ⚠️ 这一块只在「老师发了新任务」这一刻出现，4 秒自己收走，
      所以可以带一点模糊；**别把它做成常驻**，常驻就等于没有提醒。
   ⚠️ tab 上的数字和桌面上 App 图标上的数字是**同一个数**（都是待完成项），
      别在这里另算一套 —— 两处对不上，孩子会以为系统坏了。
   ============================================================ */

/* dock 栏「听写」图标右上角的数字 */
.tab-badge {
  position: absolute; top: 3px; left: 50%; margin-left: 3px;
  min-width: 17px; height: 17px; padding: 0 4px;
  border-radius: 9px; background: var(--danger); color: #fff;
  font-size: 11px; font-weight: 700; line-height: 17px; text-align: center;
  font-style: normal; letter-spacing: 0;
  box-shadow: 0 0 0 2px #fff, 0 2px 6px rgba(239,90,111,.4);
  pointer-events: none;
}

/* 顶部滑入的那条 */
.todo-hint {
  position: fixed; z-index: 70;
  left: var(--gutter); right: var(--gutter);
  top: calc(env(safe-area-inset-top, 0px) + 8px);
  max-width: var(--content-max); margin: 0 auto;
  display: flex; align-items: center; gap: 11px;
  padding: 10px 10px 10px 12px;
  border-radius: var(--r-md);
  background: rgba(255,255,255,.97);
  box-shadow: 0 10px 30px rgba(24,43,84,.18), 0 1px 3px rgba(24,43,84,.08);
  transform: translateY(calc(-100% - env(safe-area-inset-top, 0px) - 20px));
  opacity: 0; pointer-events: none;
  transition: transform .34s cubic-bezier(.32,.72,.24,1), opacity .22s var(--ease, ease);
}
.todo-hint.on { transform: translateY(0); opacity: 1; pointer-events: auto; }

.th-ico {
  flex: none; width: 32px; height: 32px; border-radius: 50%;
  display: grid; place-items: center; color: #fff;
  background: linear-gradient(140deg, #FF8B8B, #F0486A);
}
.th-ico svg { width: 17px; height: 17px; }
.th-main { flex: 1; min-width: 0; }
.th-t { font-size: 14px; font-weight: 650; color: var(--text); line-height: 1.35; }
.th-s {
  font-size: 12.5px; color: var(--text-2); margin-top: 2px; line-height: 1.35;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.th-x {
  flex: none; width: 28px; height: 28px; border: 0; border-radius: 50%;
  background: rgba(150,178,220,.16); color: var(--text-2);
  font-size: 12px; line-height: 1;
}
.th-x:active { background: rgba(150,178,220,.3); }

/* 「催一下」弹层里那段可直接粘到群里的文字 */
.nudge-ta {
  width: 100%; min-height: 132px; resize: vertical;
  padding: 12px 13px; border-radius: var(--r-sm);
  border: 1px solid var(--line); background: #fff;
  font-size: 14px; line-height: 1.7; color: var(--text);
  font-family: inherit;
}

/* ============================================================
   v2.44 · 学生积分窗口（老师）
   ------------------------------------------------------------
   为什么要有这一屏：积分算在 NAS 这边，班级宠物在 ourteacher 那边，
   两边没有 API 可通。所以这里做的是「把该加的分算好 → 写成对面认识的文本
   → 老师粘一次 → 看着分数真加上去了 → 回来记一笔账（记账只为去重）」。
   ⚠️ 行元件**不许加 backdrop-filter** —— 一屏几十行，每帧重算模糊必掉帧；
      底色本来就是近白，近白底 + 阴影观感几乎不变（见 v2.40 那条红线）。
   ⚠️ 修饰类一律带 `pet-` 前缀：`.on` / `.empty` 这些通用名是全站共用的。
   ============================================================ */

.pet-empty {
  padding: 34px 16px; text-align: center;
  font-size: 14px; line-height: 1.8; color: var(--text-3);
}

/* 班号胶囊右边的人数小方块 */
.pet-cnt {
  display: inline-block; margin-left: 5px; padding: 0 5px;
  border-radius: 7px; background: rgba(150,178,220,.18);
  font-size: 11px; font-weight: 700; color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
.chip.on .pet-cnt { background: rgba(255,255,255,.32); color: #fff; }

/* 「全选 / 全不选」—— 两段文字，不占按钮的位置 */
.pet-quick { display: flex; gap: 14px; }
.pet-quick button {
  border: 0; background: none; padding: 2px 0;
  font: inherit; font-size: 13px; font-weight: 600; color: var(--primary);
}
.pet-quick button:active { opacity: .5; }

.pet-list {
  border-radius: var(--r-md); overflow: hidden;
  background: rgba(255,255,255,.74);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.9), 0 2px 10px rgba(96,130,190,.07);
}
.pet-row {
  display: flex; align-items: center; gap: 9px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--line-2);
  transition: background .16s;
}
.pet-row:last-child { border-bottom: none; }
.pet-row.on { background: rgba(62,123,250,.06); }
.pet-row .avv { flex: none; }

/* 勾选圈：没勾是空心圈，勾上是蓝底白勾 */
.pet-ck {
  flex: none; width: 22px; height: 22px; padding: 0;
  border-radius: 50%; border: 1.6px solid rgba(150,178,220,.55);
  background: #fff; display: grid; place-items: center;
  transition: background .14s, border-color .14s, transform .12s var(--ease);
}
.pet-ck svg { width: 13px; height: 13px; }
.pet-ck.on { background: var(--primary); border-color: var(--primary); }
.pet-ck:active { transform: scale(.88); }

.pet-main { flex: 1; min-width: 0; }
.pet-nm {
  font-size: 15px; font-weight: 650; color: var(--text); line-height: 1.3;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pet-tag {
  display: inline-block; margin-left: 6px; padding: 1px 6px;
  border-radius: 6px; background: rgba(150,178,220,.2);
  font-size: 11px; font-weight: 600; color: var(--text-2); vertical-align: 1px;
}
/* 「该加 / 累计 / 已同步」这三个数一行放不下（姓名长一点就被截掉最要紧的「该加」），
   所以允许两行 —— 截断成「该加…」等于没写。 */
.pet-sub {
  font-size: 12px; color: var(--text-3); margin-top: 2px; line-height: 1.4;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; word-break: break-word;
}

/* −  数字  ＋ */
.pet-step {
  flex: none; display: flex; align-items: center; gap: 1px;
  padding: 2px; border-radius: 10px;
  background: rgba(233,242,255,.92);
}
.pet-step button {
  width: 27px; height: 30px; border: 0; background: none;
  color: var(--primary); font-size: 19px; font-weight: 600;
  line-height: 1; display: grid; place-items: center;
}
.pet-step button:active { opacity: .45; }
.pet-num {
  width: 42px; height: 30px; border: 0; border-radius: 8px;
  background: #fff; text-align: center;
  font: inherit; font-size: 15px; font-weight: 750; color: var(--text);
  font-variant-numeric: tabular-nums;
  appearance: textfield; -moz-appearance: textfield;
}
.pet-num::-webkit-inner-spin-button,
.pet-num::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.pet-num:disabled { background: rgba(255,255,255,.45); color: var(--text-3); }
.pet-num:focus { outline: 2px solid rgba(62,123,250,.3); outline-offset: 0; }

/* 贴底的「已选 N 人 · 合计 M 分 / 生成」那条。
   卡片已经自带模糊（弹层允许），这里只用近白渐变，不再叠一层 blur。 */
.pet-bar {
  position: sticky; bottom: 0; z-index: 1;
  margin: 14px -18px calc(-1 * (var(--safe-b) + 20px));
  padding: 14px 18px calc(var(--safe-b) + 14px);
  background: linear-gradient(180deg, rgba(252,253,255,0) 0%, rgba(252,253,255,.97) 30%);
}
.pet-bar-t { font-size: 13px; color: var(--text-2); font-variant-numeric: tabular-nums; }
.pet-bar-t b { color: var(--text); font-weight: 750; }

/* 第二步：可直接粘到 ourteacher 的那段纯文本 */
.pet-ta {
  width: 100%; min-height: 104px; resize: vertical;
  padding: 12px 13px; border-radius: var(--r-sm);
  border: 1px solid var(--line); background: #fff;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 14px; line-height: 1.9; color: var(--text);
  letter-spacing: .2px;
}

/* ①②③ 的小标题 */
.pet-step-t {
  display: flex; align-items: center; gap: 8px;
  font-size: 14px; font-weight: 700; color: var(--text);
  margin: 15px 0 8px;
}
.pet-step-t i {
  flex: none; width: 20px; height: 20px; border-radius: 50%;
  background: var(--grad); color: #fff;
  font-size: 12px; font-style: normal; font-weight: 700;
  display: grid; place-items: center;
}

/* 贴底那条里的「回去改数字」—— 它是退路，不该和主按钮一样大，
   占掉 56px 就会把上面第 ③ 步的说明挤出屏幕。 */
.pet-bar-link {
  display: block; width: 100%; margin-top: 9px; padding: 4px 0;
  border: 0; background: none; text-align: center;
  font: inherit; font-size: 13px; font-weight: 600; color: var(--text-2);
}
.pet-bar-link:active { opacity: .5; }

/* ============================================================
   v2.46.0 · 已购标记 / 用过的照片 / 一键匹配 / 积分折算
   ------------------------------------------------------------
   ⚠️ 这一整段都**不加 backdrop-filter**（见文件顶部 v2.40 那条红线）：
     头像框九宫格、照片历史、可疑条目列表，全都在滚动流里且一页几十个。
   ============================================================ */

/* ---- 空状态卡：**宽度和下面「已完成」的卡一模一样**，只是矮一点（v2.46.0） ----
   老师先说过「待完成的框太宽了」，改窄之后又指着截图说
   「待完成和下面的已完成等宽，高度是自适应，现在的结构不对」——
   ⚠️ 所以**宽度不能跟着文字走**（fit-content 会让它缩成中间一小块），
      要在同一条 .card 栅格上占满，只有**高度**随内容走。
   原来难看的原因不是宽度，是高度：`.empty` 自带 56px 上下留白 + 88px 图标 ≈ 260px，
   一屏里就它最抢眼，而它其实只是在说「没事干」。
   ⚠️ 只给 `.card.empty-card` 用，别改 `.empty` 本身 ——
      那个是全站空状态（书架空、名单空、列表空），那些地方要占满才像「这一块是空的」。 */
.card.empty-card {
  padding: 0;            /* 内边距交给 .empty，卡片本身不叠一层 */
}
.card.empty-card .empty { padding: 22px 20px 20px; }
.card.empty-card .empty .ico {
  width: 54px; height: 54px; border-radius: 18px; margin-bottom: 10px;
}
.card.empty-card .empty .ico svg { width: 25px; height: 25px; }
.card.empty-card .empty h4 { font-size: 15px; margin-bottom: 4px; }
.card.empty-card .empty p { font-size: 12.5px; }

/* ---- 头像框格子上的「已购 / 5 分」小签 ---- */
.av-ftag {
  font-size: 9.5px; line-height: 1; font-weight: 700;
  color: #067A60; background: rgba(11,191,149,.14);
  padding: 2px 5px; border-radius: 999px; white-space: nowrap;
}
.av-ftag.buy { color: var(--text-3); background: var(--bg-2); font-weight: 600; }

/* ---- 限定头像框（v2.46.2：运动会）----
   格子上的「限定」小签 + 下面是「差在哪」那张小卡。
   ⚠️ 小签**不能**写成「没达成 = 灰掉」：灰掉看着像「这个框不存在 / 坏了」。
      限定是**特色**，所以用运动会那身红金，和普通档位的灰签一眼分开。 */
.av-ftag.lock {
  color: #fff; font-weight: 700;
  background: linear-gradient(135deg, #FF7A45, #E8402F 70%, #B31F16);
}
/* 限定框那格给一层很淡的红底（不是 1px 红框：细边框在小屏上看着像「表格线」，
   和下面那个「限定」小签是重复表达）。`.on` 的蓝底优先级更高，别被盖掉。 */
.av-fcell.lim { background: rgba(232,64,47,.055); }
.av-fcell.lim.on { background: rgba(94,155,255,.14); box-shadow: inset 0 0 0 1.5px rgba(94,155,255,.5); }

.av-rule {
  margin-top: 10px; padding: 11px 12px 12px;
  border-radius: 14px;
  background: linear-gradient(180deg, rgba(255,122,69,.10), rgba(232,64,47,.055));
  box-shadow: inset 0 0 0 1px rgba(232,64,47,.16);
}
.av-rule-h { font-size: 12.5px; font-weight: 700; color: #B31F16; margin-bottom: 7px; }
.av-conds { display: flex; flex-direction: column; gap: 5px; }
.av-cond { font-size: 12px; line-height: 1.5; color: var(--text-3); }
.av-cond b { color: var(--text-1); font-weight: 700; }
.av-cond.ok { color: #0B7A5A; }
.av-cond.ok b { color: #0B7A5A; }
.av-rule-why { font-size: 12px; color: var(--text-3); margin-top: 7px; line-height: 1.6; }
.av-rule-why.ok { color: #0B7A5A; font-weight: 700; }

/* ---- 老师端「头像框兑换条件」（v2.46.2）---- */
.fr-card { padding: 12px; border-radius: 16px; background: var(--bg-2); margin-bottom: 10px; }
.fr-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.fr-del {
  flex: none; border: 0; border-radius: 999px; padding: 6px 10px;
  font-size: 12px; font-weight: 700; color: var(--danger);
  background: #fff; box-shadow: var(--sh-1);
  -webkit-tap-highlight-color: transparent;
}
.fr-del:active { transform: scale(.95); }
.fr-fields { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.fr-fields label {
  position: relative; display: block;
  font-size: 11.5px; color: var(--text-3); line-height: 1.3;
}
/* 单位（天 / % / 分）绝对定位在输入框右下角 —— 单独一行会把三列撑高一行 */
.fr-fields label b {
  position: absolute; right: 9px; bottom: 9px;
  font-size: 10.5px; font-weight: 600; color: var(--text-3);
  pointer-events: none;
}
.fr-fields input {
  width: 100%; margin-top: 5px; border: 0; border-radius: 10px;
  background: #fff; padding: 8px 24px 8px 8px;
  font-size: 15px; font-weight: 700; color: var(--text-1); text-align: center;
  -moz-appearance: textfield;
}
.fr-fields input::-webkit-outer-spin-button,
.fr-fields input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.fr-none {
  font-size: 12.5px; color: var(--text-3); line-height: 1.7;
  text-align: center; padding: 18px 10px; border-radius: 14px; background: var(--bg-2);
}
.fr-add { font-size: 12.5px; color: var(--text-3); }
.fr-chip {
  display: inline-block; margin: 7px 7px 0 0; padding: 6px 11px;
  border: 0; border-radius: 999px; background: #fff; box-shadow: var(--sh-1);
  font-size: 12.5px; font-weight: 600; color: var(--text-1);
  -webkit-tap-highlight-color: transparent;
}
.fr-chip:active { transform: scale(.95); }

/* ============================================================
   v2.46.3 · 按分组看任务 / 转发弹层 / 免听写卡
   ⚠️ 这些都在滚动流里，**一律不加 backdrop-filter**（v2.40 那条红线）。
   ============================================================ */
/* 分组那一排小胶囊：横向滚，不换行（换行会把下面的学生列表顶下去） */
.gt-chips {
  display: flex; gap: 8px; overflow-x: auto; padding: 2px 0 10px;
  -webkit-overflow-scrolling: touch; scrollbar-width: none;
}
.gt-chips::-webkit-scrollbar { display: none; }
.gt-chip {
  flex: none; border: 0; border-radius: 999px; padding: 8px 13px;
  background: #fff; box-shadow: var(--sh-1);
  font-size: 13px; font-weight: 600; color: var(--text-2);
  display: inline-flex; align-items: center; gap: 6px;
  -webkit-tap-highlight-color: transparent;
}
.gt-chip i {
  font-style: normal; font-size: 11px; font-weight: 700;
  color: var(--text-3); background: var(--bg-2);
  padding: 1px 6px; border-radius: 999px;
}
.gt-chip.on { background: var(--primary); color: #fff; }
.gt-chip.on i { background: rgba(255,255,255,.24); color: #fff; }
/* 自建班组（提高班 / 补差班）和主班要在 chip 上分得出来 —— 都是「801」这种短码，
   只靠名字看不出哪个是名单里的班、哪个是老师自己分的组。 */
.gt-chip em {
  font-style: normal; font-size: 10px; font-weight: 700;
  color: var(--primary); background: var(--primary-soft, #E8F0FE);
  padding: 1px 5px; border-radius: 6px;
}
.gt-chip.on em { color: #fff; background: rgba(255,255,255,.24); }
.gt-chip:active { transform: scale(.96); }

/* 「把这条任务再发给别人（加人）」—— 一行小字链接。
   ⚠️ 故意**不是**白底按钮：它下面/上面已经各有一颗白底大按钮，
      再摞一颗的话三颗的阴影会连成一片（老师原话「两个件的间隔不对」）。 */
/* 「再发给别人（加人）」—— 看起来是一张白底卡片，里面只有一行字。
   ⚠️ 三件事别改：
      ① padding 归零、由里面那颗按钮撑满 —— 否则卡片上下留白处点不动，看着能点其实点不到；
      ② 里面那颗按钮**不许**再长成大按钮（46px + 阴影）：它上面就是「催一下」，
         两颗白按钮摞一起阴影连成一片，老师说的「两个件的间隔不对」就是那个；
      ③ 半径 / 阴影 / 描边全部继承 `.card`，别在这儿另写一套 —— 「和上面一样的底框」就是这么来的。 */
.add-stu-card { padding: 0; }
.add-stu-link {
  display: flex; align-items: center; justify-content: center; gap: 5px;
  width: 100%; padding: 15px 16px;
  border: 0; background: none;
  color: var(--primary); font-size: 14px; font-weight: 600;
  font-family: inherit; -webkit-tap-highlight-color: transparent;
}
.add-stu-link:active { background: rgba(120,150,200,.07); border-radius: inherit; }
.add-stu-plus { font-size: 16px; line-height: 1; font-weight: 400; opacity: .85; }

/* 选人弹层（「再布置给其他学生」）。
   ⚠️ v2.46.3 第二版：原来这里还有一段 `.fw-text`（生成一段话发到群里的那个框），
      老师看清了之后说「不是你现在的样子」—— 转发＝在已发布的任务里加人。整块删了，
      只留选人。别再把「复制文案」那套搬回来。 */
.fw-groups { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
.fw-chip {
  border: 0; border-radius: 999px; padding: 7px 12px;
  background: #fff; box-shadow: var(--sh-1);
  font-size: 12.5px; font-weight: 600; color: var(--text-1);
  display: inline-flex; align-items: center; gap: 5px;
}
.fw-chip i { font-style: normal; font-size: 11px; color: var(--text-3); font-weight: 700; }
/* 自建班组和主班要在 chip 上分得出来（和分组页同一个记号） */
.fw-chip em {
  font-style: normal; font-size: 10px; font-weight: 700;
  color: var(--primary); background: var(--primary-soft, #E8F0FE);
  padding: 1px 5px; border-radius: 6px;
}
.fw-chip:active { transform: scale(.95); }

/* ★ 选人弹层顶上那一行「已建分组」（v2.48.0）——
   老师原话：「按分组显示的时候最上面显示所有已经建立的分组名，我点击组名选择所有成员，
   不用像现在这样划到最下面选择不同的组，太麻烦了。」
   点一下＝整组全选，再点一下＝整组取消；整组都在里面时 chip 变实心（.on）。
   ⚠️ 这行**不跟着搜索框过滤** —— 它是开关，不是结果列表。 */
.pk-groups { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
.fw-chip.on {
  background: var(--grad);
  color: #fff;
  box-shadow: 0 4px 12px rgba(120, 150, 255, .28);
}
.fw-chip.on i { color: rgba(255, 255, 255, .88); }
.fw-chip.on em { background: rgba(255, 255, 255, .24); color: #fff; }

/* 挑学生弹层的「确定」（v2.49.0）——
   老师原话：「底部的确认按钮改为界面的右边悬浮按钮，不用选择后拉到最下方点击。」
   sticky 挂在滚动容器（.sheet-body）底部：一直贴在右下角，滚到底就落回原位。
   ⚠️ 外面这条 pointer-events:none 是必须的 —— 否则它会挡住列表最后一行的点击。 */
.pk-ok-bar {
  position: sticky; bottom: calc(var(--safe-b) + 8px);
  display: flex; justify-content: flex-end;
  margin-top: 12px; z-index: 6;
  pointer-events: none;
}
.pk-ok-bar .btn {
  pointer-events: auto;
  width: auto; min-width: 132px; height: 46px;
  padding: 0 24px; border-radius: 999px;
  box-shadow: 0 10px 26px rgba(90, 110, 255, .34);
}

/* 「自己练单词」里的空单元（v2.49.0）——
   教材骨架里那些还没导入内容的单元**留着但置灰**：直接不显示的话，
   学生会以为「明明课本有这一单元，怎么列表里没有」。
   点它会给一句说明（app.js 里绑 data-empty）。 */
.sw-empty { opacity: .45; }
.sw-empty .item-arrow { display: none; }

/* 选人列表：限高自己滚，弹层不要长到顶天 */
.fw-list {
  max-height: 232px; overflow-y: auto;
  background: var(--bg-2); border-radius: 14px; padding: 6px;
  -webkit-overflow-scrolling: touch;
}
.fw-stu {
  width: 100%; border: 0; background: none; border-radius: 10px;
  display: flex; align-items: center; gap: 9px;
  padding: 9px 10px; text-align: left;
  -webkit-tap-highlight-color: transparent;
}
.fw-stu + .fw-stu { border-top: 1px solid rgba(0,0,0,.045); }
.fw-stu .fw-box {
  flex: none; width: 17px; height: 17px; border-radius: 6px;
  border: 1.5px solid rgba(94,155,255,.45); background: #fff;
  font-size: 9px; color: #fff; line-height: 15px; text-align: center;
}
.fw-stu .fw-nm { flex: 1; min-width: 0; font-size: 14px; color: var(--text-1); font-weight: 600; }
.fw-stu .fw-cls { font-size: 11.5px; color: var(--text-3); }
.fw-stu.on { background: rgba(94,155,255,.12); }
.fw-stu.on .fw-box { background: var(--primary); border-color: var(--primary); }
.fw-stu.has { opacity: .42; }
.fw-stu.has .fw-box {
  width: auto; padding: 0 5px; border-radius: 999px; border-color: transparent;
  background: var(--bg-2); color: var(--text-3); font-size: 9.5px; font-weight: 700;
}

/* 商店里的「免听写卡」小签（和「一次性道具」区分开，用蓝的） */
.shop-tag.card { color: #1B63D6; background: rgba(46,107,247,.12); }

/* ---- 「我用过的照片」 ---- */
.av-photos { display: flex; flex-wrap: wrap; gap: 9px; }
.av-photo {
  position: relative; width: 52px;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  border: 0; background: none; padding: 0;
  -webkit-tap-highlight-color: transparent;
}
.av-photo .avv { width: 46px; height: 46px; border-radius: 15px; }
.av-photo .avv img { width: 100%; height: 100%; object-fit: cover; }
.av-photo:active .avv { transform: scale(.94); }
.av-photo.on .avv { box-shadow: 0 0 0 2.5px var(--primary); }
/* 右上角那个小 × ：**只在不是正在用的那张上出现**。
   正在用的那张删掉 = 头像当场变碎图，服务端也会拒，界面上干脆不给这个键。 */
.av-photo .av-del {
  position: absolute; top: -3px; right: 1px; z-index: 3;
  width: 17px; height: 17px; border-radius: 50%;
  background: rgba(38,48,66,.62); color: #fff;
  font-size: 11px; line-height: 17px; text-align: center; font-weight: 700;
}
.av-photo .av-ptag { font-size: 9.5px; color: var(--text-3); line-height: 1; }

/* ---- 书架「一键匹配」 ---- */
.af-prog {
  height: 6px; border-radius: 999px; background: var(--bg-2); overflow: hidden; margin: 10px 0 6px;
}
.af-prog i { display: block; height: 100%; border-radius: 999px; background: var(--grad); transition: width .35s var(--ease-app); }
.af-stat { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 12.5px; color: var(--text-3); }
.af-stat b { color: var(--text); font-variant-numeric: tabular-nums; }
.af-issue {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 12px 0; border-bottom: 1px solid var(--line);
}
.af-issue:last-child { border-bottom: none; }
.af-issue .pet-ck { margin-top: 1px; }
.af-issue-main { flex: 1; min-width: 0; }
.af-issue-en { font-size: 15px; font-weight: 600; color: var(--text); word-break: break-word; }
.af-issue-en s { color: var(--danger); font-weight: 500; }
.af-issue-val { font-size: 14px; color: #067A60; font-weight: 600; word-break: break-word; }
.af-issue-why { font-size: 12px; color: var(--text-3); margin-top: 3px; line-height: 1.5; }
.af-kind {
  flex: none; font-size: 10.5px; font-weight: 700; padding: 2px 7px; border-radius: 999px;
  background: rgba(255,138,0,.14); color: #B4620A; margin-top: 2px;
}
.af-kind.zh { background: rgba(94,155,255,.14); color: var(--primary); }

/* ---- 学生积分：折算面板 ---- */
.pet-conv {
  margin: 10px 0 4px; padding: 12px;
  border-radius: var(--r-md); background: var(--bg-2);
  border: 1px solid var(--line-2);
}
.pet-conv-ops { display: flex; gap: 6px; margin-bottom: 8px; }
.pet-conv-ops button {
  flex: 1; height: 34px; border-radius: 10px; border: 1px solid var(--line-2);
  background: #fff; font: inherit; font-size: 15px; font-weight: 600; color: var(--text-2);
}
.pet-conv-ops button.on { background: var(--primary); border-color: var(--primary); color: #fff; }
.pet-conv-row { display: flex; align-items: center; gap: 8px; }
.pet-conv-row .input { flex: 1; }
.pet-conv-pv { font-size: 12.5px; color: var(--text-3); line-height: 1.7; margin-top: 8px; }
.pet-conv-pv b { color: var(--text); font-variant-numeric: tabular-nums; }
.pet-conv-pv .up { color: var(--danger); }

/* ============================================================
   v2.46.1 · 书架层级树可以拖着重新排列
   ------------------------------------------------------------
   老师原话：「书架允许我移动每次进行重新排列，和单词整理的逻辑一样」。
   复用 `.drag-handle` 那套（幽灵跟手 + 原位变淡），这里只补树行自己的尺寸和状态。
   ⚠️ 树行本来就挤（图标 + 两行字 + 整理图标 + 箭头），把手只能做小一号（30×30）。
   ============================================================ */
.tn-grip {
  width: 30px; height: 30px; border-radius: 9px;
  color: var(--primary);
  background: color-mix(in srgb, var(--primary) 13%, transparent);
}
.tn-grip svg { width: 16px; height: 16px; }
.tn-row .tn-grip { flex: none; }
/* 被拖的那一层：原位只留一条淡痕，让人看清会落到哪 */
.tn-node.drag-src > .tn-row { background: color-mix(in srgb, var(--primary) 8%, transparent); }
.tn-node.drag-src > .tn-row > * { opacity: .18; }
/* 跟手的那一块：克隆的是整棵 .tn-node，只带这一行走（子孙展开着也不能一起飞） */
.tn-ghost { overflow: hidden; }
.tn-ghost .tn-kids { display: none; }
.tn-ghost .tn-node { border-bottom: none; }
/* 正在拖的这一层给个圈：提醒「只能在同一层里换」，跨层要回去点「换个层级」 */
.tn-holder-on {
  border-radius: 10px;
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--primary) 26%, transparent);
}

/* ============================================================
   背单词（v2.48.0）—— 英文 → 认识 / 不认识 → 不认识的重做选择题
   ------------------------------------------------------------
   老师定的流程：「显示出英文发音，最下面显示认识和不认识按钮，学生点击认识
   显示中文释义，中文释义在页面中间显示，显示后学生也可以再次点击不认识，
   不认识的词最后会在所有的词走完之后出选择题让学生选择再认识。」
   ⚠️ 外壳沿用播放页那一层深色底（.play-wrap / .play-head / .play-progress），
      学生点进来和听写是同一个感觉；这一页只加自己那几块。
   ⚠️ 这是沉浸式深色页，mount 时**必须关 #dock-scrim**（见 mount 里的判断）。
   ============================================================ */
.qz-body { flex: 1; display: flex; flex-direction: column; min-height: 0; }

.qz-stage {
  flex: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  padding: 12px 0 6px; text-align: center;
}

/* 英文（主角）。长单词 / 短语要能换行，别撑破屏 */
.qz-word {
  font-size: 40px; font-weight: 700; line-height: 1.24;
  letter-spacing: -.6px; word-break: break-word; max-width: 100%;
}
.qz-word.mid { font-size: 33px; }

.qz-pos { font-size: 13.5px; opacity: .55; margin-top: 6px; letter-spacing: .2px; }

/* 「再听一遍」—— 小圆角按钮，压在英文下面 */
.qz-audio {
  margin-top: 20px;
  display: inline-flex; align-items: center; gap: 7px;
  padding: 9px 18px; border: 0; border-radius: 999px;
  background: rgba(255, 255, 255, .10); color: #fff;
  font-size: 13.5px; font-weight: 600; font-family: inherit;
  -webkit-tap-highlight-color: transparent;
}
.qz-audio svg { width: 16px; height: 16px; }
.qz-audio:active { transform: scale(.96); }

/* ★ 中文释义：点了「认识」才出现，**在页面中间**（老师原话「中文释义在页面中间显示」） */
.qz-zh {
  margin-top: 26px; min-height: 76px;
  display: flex; align-items: center; justify-content: center;
  font-size: 25px; font-weight: 700; line-height: 1.5;
  padding: 0 10px; word-break: break-word;
  animation: qzZhIn .22s var(--ease) both;
}
/* 还没亮的时候这里只占位（保持版面不跳），别写成一个大问号 */
.qz-zh.is-empty { font-size: 14px; font-weight: 400; opacity: .26; animation: none; }
@keyframes qzZhIn { from { opacity: 0; transform: translateY(7px); } to { opacity: 1; transform: none; } }

/* ★ 最下面那一排：不认识 / 认识（亮出中文后右边变「下一个」） */
.qz-actions { display: flex; gap: 12px; margin-top: 8px; padding-bottom: 2px; }
.qz-actions > * { flex: 1; }
.qz-btn {
  min-height: 56px; border: 0; border-radius: 18px;
  display: grid; place-items: center;
  font-size: 17px; font-weight: 700; color: #fff;
  font-family: inherit; -webkit-tap-highlight-color: transparent;
  transition: background .16s var(--ease), transform .12s var(--ease);
}
.qz-btn:active { transform: scale(.97); }
.qz-btn.no { background: rgba(255, 255, 255, .12); }
.qz-btn.yes {
  background: linear-gradient(135deg, #7BA0FF, #8C6BFF);
  box-shadow: 0 8px 22px rgba(110, 120, 255, .32);
}
/* 「已记为不认识」——按下去之后的终态，不能再点 */
.qz-btn.no.ok { background: rgba(255, 255, 255, .07); color: rgba(255, 255, 255, .45); }

/* 补认关：4 个选项两列排 */
.qz-opts {
  width: 100%; display: grid; grid-template-columns: 1fr 1fr;
  gap: 11px; margin-top: 26px;
}
.qz-opt {
  min-height: 64px; padding: 12px 10px;
  border: 0; border-radius: 16px;
  background: rgba(255, 255, 255, .09); color: #fff;
  font-size: 15.5px; font-weight: 600; line-height: 1.42;
  font-family: inherit; word-break: break-word;
  -webkit-tap-highlight-color: transparent;
  transition: background .16s var(--ease), transform .12s var(--ease);
}
.qz-opt:active { transform: scale(.97); }
.qz-opt.is-ok { background: rgba(48, 209, 88, .24); color: #8BEFA6; }
.qz-opt.is-bad { background: rgba(255, 69, 58, .24); color: #FF9A93; }
.qz-opt[disabled] { opacity: .95; }

/* 结果页：一个大数字 + 一句话 */
.qz-big-num { font-size: 54px; font-weight: 300; letter-spacing: -2px; line-height: 1; }
.qz-big-num span { font-size: 22px; opacity: .35; font-weight: 400; letter-spacing: 0; }
.qz-pts {
  margin-top: 14px; font-size: 15px; font-weight: 700;
  color: #8BEFA6;
}
.qz-wrong {
  margin-top: 18px; max-width: 330px;
  font-size: 13px; line-height: 1.8; opacity: .6;
  word-break: break-word;
}

/* ---------- 老师端「听跟读录音」弹层（v2.51.0）---------- */
.ra-stu { border-bottom: 1px solid var(--line-2); }
.ra-stu:last-child { border-bottom: 0; }
.ra-stu-head {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 13px 2px; background: none; text-align: left; font-size: 14.5px;
}
.ra-stu-head .grow { min-width: 0; }

/* ---- 每人的跟读均分（v2.55.2）----
   老师原话「学生的录音评分，教师端显示每个学生此次的均分」。
   以前分数是塞在 `12px 灰色小字`里的一个附注（「2/6 · 参考 87 分」），手机上一眼扫不到。
   现在单独一枚 pill：紫蓝色（和分数大数字同一个色系），一眼能在一列名字里挑出来。
   ⚠️ 数字用 tabular-nums：一列分数上下对齐，老师才好横着比。
   ⚠️ 别用红色/绿色：这不是「对错」，只是「读得像不像」—— 红绿会让人当成成绩。 */
.pill-ra {
  background: rgba(123,160,255,.20); color: #5B7FE0;
  font-weight: 700; font-variant-numeric: tabular-nums;
}
/* 弹层顶部那条「全班均分」：大数字 + 标签 + 一行小字说明。
   ⚠️ 它上面不能再套卡片 —— 弹层本身已经是白底了，再套一层会像嵌进另一个界面。 */
.ra-avg {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  margin: 2px 0 10px; padding: 10px 14px; border-radius: 14px;
  background: rgba(123,160,255,.10); border: 1px solid rgba(123,160,255,.20);
}
.ra-avg b {
  font-size: 26px; font-weight: 700; line-height: 1; color: #4E7BFF;
  font-variant-numeric: tabular-nums;
}
.ra-avg span { font-size: 12.5px; font-weight: 600; color: var(--text-2); }
.ra-avg em {
  font-style: normal; margin-left: auto; font-size: 11.5px; color: var(--text-3);
}
.ra-avg.is-off { background: rgba(150,178,220,.10); border-color: var(--line-2); }
.ra-avg.is-off b { color: var(--text-3); }
.ra-stu-body { padding: 2px 0 14px; }
.ra-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.ra-i {
  width: 34px; height: 34px; border-radius: 10px; font-size: 13px;
  display: grid; place-items: center; font-variant-numeric: tabular-nums;
  border: 1px solid var(--line); transition: transform .12s var(--ease);
}
.ra-i.ok { background: var(--success-soft); color: #137A4B; border-color: transparent; }
.ra-i.weak { background: #FFF4E0; color: #9A6A00; border-color: transparent; }
/* v2.53.0：识别说「读成别的词」—— 橙，和「太轻（黄）/ 没声音（红）」分开 */
.ra-i.off { background: #FFEEDE; color: #A8531A; border-color: transparent; }
.ra-i.no { background: #FDECEC; color: #B03030; border-color: transparent; }
.ra-i.none { color: var(--text-3); background: var(--bg-2); border-color: transparent; }
.ra-i.playing { transform: scale(1.1); box-shadow: inset 0 0 0 2px var(--primary); }

/* ============================================================
   名单里当场听录音（v2.55.4）
   ------------------------------------------------------------
   老师原话：「把录音做到学生名单每个学生里面，不像现在这样（最后那张
   「跟读录音 · 听一听」的卡片）」。所以录音从一张独立卡片搬到了每个学生行下。
   三条必须守住：
     ① **整行还是「点进学生弹层」**，只有「跟读 xx」那枚 pill 吃自己的点击（stopPropagation）；
     ② 展开的行要和上面那行**连成一体**（同底色、无分隔线），否则看起来像另一个人；
     ③ 编号格沿用弹层里那套 `.ra-i` 颜色，老师不用重新认一次颜色。
   ============================================================ */
/* 名单标题下那一行小字：左边统计、右边「全班表 ›」 */
.ra-line { display: flex; align-items: baseline; gap: 8px; margin-top: 5px; }
.ra-line #raLine { flex: 1; min-width: 0; }
.ra-more {
  flex: none; font-size: 12.5px; font-weight: 600; color: var(--primary);
  background: none; border: 0; padding: 2px 0;
}
/* 一个学生的外壳：分隔线挪到外壳上，里面那行 `.item` 不再画线（否则展开后多一道） */
.stu-row { border-bottom: 1px solid var(--line-2); }
.stu-row:last-child { border-bottom: 0; }
.stu-row .item { border-bottom: 0; }
/* 「跟读 92 ▾」这枚 pill 是按钮 —— pill 本身是 span 的观感，按钮要抹掉默认样式 */
.ra-pill { border: 1px solid transparent; font: inherit; font-size: 12px; cursor: pointer; }
.ra-pill.on { background: rgba(123,160,255,.30); color: #4A73D8; }
/* 展开的那一块：浅蓝底 + 和上面那一行贴住 */
.ra-inline {
  padding: 2px 16px 12px;
  background: rgba(123,160,255,.07);
  box-shadow: inset 0 1px 0 rgba(123,160,255,.16);
  animation: raIn .18s var(--ease);
}
@keyframes raIn { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }
.ra-inline-top {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  font-size: 12.5px; color: var(--text-3); padding: 8px 0 8px;
}
.ra-inline-sc { font-size: 15px; font-weight: 700; color: #6C8FF0; font-variant-numeric: tabular-nums; }
.ra-inline-sc.is-off { font-size: 12.5px; color: var(--text-3); font-weight: 600; }
.ra-inline-flag {
  padding: 1px 7px; border-radius: 999px; font-size: 11.5px;
  background: rgba(255,190,90,.18); color: #9A6A00;
}
.ra-inline-flag.off { background: rgba(255,150,80,.18); color: #A8531A; }
.ra-inline-tip { margin-left: auto; font-size: 11.5px; opacity: .8; }
.ra-inline-empty { padding: 10px 0 6px; font-size: 13px; color: var(--text-3); }

/* ============================================================
   断线重连（v2.55.0）
   ------------------------------------------------------------
   老师反馈「人多的时候被挤出去」。根因不在账号，在启动那一步：
   任何一次网络失败都被当成「没登录」丢回登录页。
   现在改成「正在重连 → 重试」，就得有这两个东西的样子：
     .net-tip      轻提示条（不换页，页面还在原地）
     .offline-wrap 重连失败后的整页兜底（一个能点的重试）

   ⚠️ 别把它做成红色警告：孩子看到红色会以为「我账号被封了」。
      用中性的蓝灰，主按钮仍然是主色。
   ============================================================ */
.net-tip {
  position: fixed; left: 50%; top: 14px; transform: translateX(-50%) translateY(-14px);
  z-index: 4000; display: none; align-items: center; gap: 8px;
  padding: 9px 16px; border-radius: 999px;
  font-size: 13px; font-weight: 600; color: var(--text);
  background: rgba(255,255,255,.96); border: 1px solid var(--line);
  box-shadow: var(--sh-2);
  opacity: 0; transition: opacity .18s ease, transform .18s ease;
  max-width: calc(100vw - 32px); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.net-tip.on { display: flex; opacity: 1; transform: translateX(-50%) translateY(0); }
.net-tip .spin {
  width: 13px; height: 13px; border-radius: 50%;
  border: 2px solid rgba(62,123,250,.25); border-top-color: var(--primary);
  animation: netSpin .8s linear infinite;
}
@keyframes netSpin { to { transform: rotate(360deg); } }

.offline-wrap {
  min-height: 62vh; display: flex; flex-direction: column;
  align-items: center; justify-content: center; text-align: center;
  padding: 28px 26px; gap: 6px;
}
.offline-ico { width: 68px; height: 68px; color: var(--text-3); margin-bottom: 6px; }
.offline-ico svg { width: 100%; height: 100%; }
.offline-t { font-size: 19px; font-weight: 700; color: var(--text); }
.offline-s {
  font-size: 13.5px; line-height: 1.75; color: var(--text-2);
  margin: 4px 0 18px; max-width: 300px;
}
.offline-wrap .btn { min-width: 168px; }
.offline-e {
  margin-top: 14px; font-size: 11.5px; color: var(--text-3);
  word-break: break-all; max-width: 300px;
}

/* ============================================================
   听写抽查（v2.58.0）
   ------------------------------------------------------------
   一屏 3~5 条输入框，要能在小屏上**不用滚动就看全** ——
   孩子一滚动就会看串行，把第 2 条的答案填到第 3 条框里。
   ============================================================ */
.sp-row { padding: 14px 14px 12px; }
.sp-row.sp-ok { border-color: color-mix(in srgb, var(--success) 42%, transparent); }
.sp-row.sp-no { border-color: color-mix(in srgb, var(--danger) 42%, transparent); }
.sp-sum { margin-bottom: 12px; }
/* 逐格填字母（v2.60.0）—— 替掉 v2.59.0 的「横线 + 下面一个文本框」。
   老师原话：「只显示上方的横线，然后依次跳动每个横线引导学生打上对应的字母，
   可以在横线前面加上拼写引导」。

   ⚠️ 一个格子就是一个真 <input maxlength=1>，不是画出来的假光标：
      Android 各输入法对「清空 value 自己接管」处理不一，真 input 最不容易出岔子。
   ⚠️ 字号必须 ≥16px（这里 18）：低于 16px 时 iOS 会在聚焦那一刻把整页放大，
      学生一碰格子页面就跳一下。
   ⚠️ 底线用 border-bottom 而不是背景虚线条：聚焦时直接把颜色换成主色，
      不用再叠一层伪元素，也没有重绘抖动。 */
.sp-line { display: flex; align-items: center; gap: 7px; margin-top: 6px; flex-wrap: wrap; }
.sp-line .pos { font-size: 11px; color: var(--text-3); font-weight: 500; margin: 0; flex: none; }
/* 「拼写」这一小标就是老师说的「横线前面的引导」：告诉孩子这几条线是拿来敲字的 */
.sp-guide {
  flex: none; font-size: 11px; font-weight: 600; letter-spacing: .4px;
  color: var(--text-3); opacity: .85;
}
.sp-blank { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px 0; min-width: 0; }
/* 一个词一段；词与词之间靠 padding-right 拉开，比 gap 好控制（换行时不会多出边距） */
.sp-w { display: inline-flex; flex-wrap: wrap; gap: 3px; padding-right: 12px; }
.sp-w:last-child { padding-right: 0; }
.sp-c {
  width: 19px; height: 32px; padding: 0; margin: 0;
  border: 0; border-bottom: 2px solid color-mix(in srgb, var(--text-3) 60%, transparent);
  border-radius: 0; background: transparent;
  font-family: inherit; font-size: 18px; font-weight: 700; line-height: 32px;
  text-align: center; color: var(--text-1);
  caret-color: var(--primary);
  -webkit-appearance: none; appearance: none;
  outline: none; box-shadow: none;
  transition: border-color .12s, background .12s;
}
.sp-c:focus {
  border-bottom-color: var(--primary);
  background: color-mix(in srgb, var(--primary) 8%, transparent);
}
/* 交卷后：按对错给底色，一眼看出哪一条没写对 */
.sp-row.sp-ok .sp-c { border-bottom-color: color-mix(in srgb, var(--success) 65%, transparent); }
.sp-row.sp-no .sp-c { border-bottom-color: color-mix(in srgb, var(--danger) 65%, transparent); }
.sp-c[disabled] { opacity: 1; -webkit-text-fill-color: var(--text-1); }

/* ---- 讲评卡（v2.58.0）：这节课该讲哪几个词 ---- */
.ww-row { display: flex; align-items: center; gap: 10px; padding: 9px 0; border-top: 1px solid var(--line); }
.ww-row:first-of-type { border-top: none; }
.ww-n {
  flex: 0 0 22px; height: 22px; border-radius: 11px; background: var(--danger-soft);
  color: var(--danger); font-size: 12px; font-weight: 700;
  display: flex; align-items: center; justify-content: center;
}
.ww-main { flex: 1; min-width: 0; }
.ww-en { font-size: 15px; font-weight: 650; letter-spacing: .2px; word-break: break-word; }
.ww-cnt { flex: 0 0 auto; font-size: 12.5px; color: var(--text-3); }
.ww-cnt b { color: var(--danger); font-size: 15px; }

/* ---------- 挑练习题（v2.60） ----------
   发布听写时的第三关。列表项要能在一屏里看清「题干 + 选项 + 考点 + 有没有答案」，
   所以横向铺开、竖向紧凑：勾选框独占一列，其余信息右对齐撑满。 */
.qz-item {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 12px 13px; margin-bottom: 9px;
  background: rgba(255,255,255,.72);
  border: .5px solid rgba(255,255,255,.9);
  border-radius: 14px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.9);
  cursor: pointer;
  transition: background .18s var(--ease), border-color .18s var(--ease);
}
.qz-item.on { background: rgba(219,238,255,.92); border-color: rgba(110,170,255,.75); }
.qz-item.is-miss { opacity: .7; cursor: default; }
.qz-item input {
  margin: 3px 0 0; width: 18px; height: 18px; flex: none;
  accent-color: var(--primary);
}
.qz-body { flex: 1; min-width: 0; }
.qz-stem { font-size: 14px; line-height: 1.55; color: var(--text); word-break: break-word; }
.qz-opts {
  display: flex; flex-wrap: wrap; gap: 4px 14px;
  margin-top: 5px; font-size: 12.5px; color: var(--text-2);
}
.qz-opts b { color: var(--primary); font-weight: 650; }
.qz-meta { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 7px; }
.qz-miss-card {
  border-color: rgba(255,168,60,.5);
  background: rgba(255,249,238,.92);
  margin-bottom: 12px;
}

/* ---------- 学生做练习题（v2.60） ----------
   一题一屏。选项做成整块可点的横条而不是原生 radio：
   孩子在手机上点 20px 的小圆点经常点不中，整条都能点才踏实。 */
.q-stem {
  font-size: 16px; line-height: 1.7; color: var(--text);
  word-break: break-word;
}
.q-mat {
  background: rgba(255,255,255,.62);
  border: .5px solid rgba(255,255,255,.9);
  border-radius: 14px;
  padding: 10px 14px;
  margin-bottom: 12px;
}
.q-mat > summary {
  font-size: 13.5px; font-weight: 600; color: var(--primary);
  cursor: pointer; list-style: none;
}
.q-mat > summary::-webkit-details-marker { display: none; }
.q-mat > summary::after { content: " ▾"; font-weight: 400; }
.q-mat[open] > summary::after { content: " ▴"; }
.q-mat-body {
  margin-top: 9px; font-size: 14px; line-height: 1.85; color: var(--text-2);
  max-height: 46vh; overflow-y: auto;
}

.q-opt {
  display: flex; align-items: flex-start; gap: 11px;
  width: 100%; text-align: left;
  padding: 13px 15px; margin-bottom: 10px;
  background: rgba(255,255,255,.72);
  border: 1px solid rgba(255,255,255,.92);
  border-radius: 16px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.9);
  transition: background .18s var(--ease), border-color .18s var(--ease), transform .12s var(--ease);
}
.q-opt:active { transform: scale(.985); }
.q-opt.on {
  background: rgba(214,235,255,.95);
  border-color: rgba(94,155,255,.85);
  box-shadow: 0 4px 14px rgba(78,123,255,.14), inset 0 1px 0 rgba(255,255,255,.9);
}
.q-opt-k {
  flex: none; width: 26px; height: 26px; border-radius: 9px;
  display: flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 700;
  background: rgba(78,123,255,.1); color: var(--primary);
}
.q-opt.on .q-opt-k { background: var(--grad); color: #fff; }
.q-opt-t { flex: 1; font-size: 15.5px; line-height: 1.55; color: var(--text); padding-top: 2px; word-break: break-word; }

/* 结果页逐题回顾 */
.qr-row {
  padding: 12px 0;
  border-bottom: .5px solid var(--line-2);
}
.qr-row:last-child { border-bottom: 0; }
.qr-hd { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
.qr-no {
  width: 22px; height: 22px; border-radius: 7px; flex: none;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700;
  background: rgba(120,140,175,.12); color: var(--text-2);
}
.qr-tag { font-size: 11.5px; font-weight: 600; padding: 1px 7px; border-radius: 7px; }
.qr-row.ok .qr-tag { background: var(--success-soft); color: var(--success); }
.qr-row.no .qr-tag { background: var(--danger-soft); color: var(--danger); }
.qr-ans { font-size: 12.5px; color: var(--text-2); }
.qr-ans b { color: var(--success); }
.qr-row.no .qr-ans b { color: var(--primary); }
.qr-stem { font-size: 14px; line-height: 1.6; color: var(--text); word-break: break-word; }
/* 解析是要**读**的一句话，不是备注：12.5px + --text-3 在浅底上几乎看不见
   （学生在手机上会直接跳过）。字号提到 13、颜色提到 --text-2，左边加一道竖线。 */
.qr-an {
  margin-top: 6px; font-size: 13px; line-height: 1.7; color: var(--text-2);
  background: rgba(120,140,175,.07); border-radius: 9px; padding: 8px 11px;
  border-left: 3px solid rgba(150,178,220,.5);
}

/* ============================================================
   v2.62.0 · 单元作文（看中文默写英文）
   ------------------------------------------------------------
   三块界面：单元页底部那张入口卡 / 老师编辑器 / 学生默写页。
   ⚠️ 所有输入框都写 16px 以上：低于 16px 时 iOS 聚焦会整页放大，
      一节课放大十几次，学生根本没法写。
   ============================================================ */

/* --text-1 在 v2.60.0 的格子样式里被引用过，但从来没定义过。
   不补这一条的话 `color: var(--text-1)` 是无效值，靠继承兜着 ——
   看着没坏，换个父元素颜色就跟着变。这里补成和正文同色。 */
:root { --text-1: #1B2B52; }

/* ---- 单元页底部那张入口卡 ---- */
.essay-card {
  margin-top: 14px;
  padding: 15px 16px;
  border-radius: var(--r-lg);
  background: linear-gradient(150deg, rgba(255,255,255,.95), rgba(238,246,255,.9));
  border: 1px solid rgba(150,178,220,.32);
  box-shadow: 0 6px 18px rgba(88,120,180,.08);
}
.essay-head { display: flex; gap: 12px; align-items: flex-start; }
.essay-ico {
  width: 38px; height: 38px; border-radius: 12px; flex: none;
  background: var(--grad-soft); color: var(--primary);
  display: grid; place-items: center;
}
.essay-ico svg { width: 20px; height: 20px; }
.essay-ttl { font-size: 14.5px; font-weight: 650; color: var(--text); letter-spacing: -.1px; }
.essay-sub { font-size: 12.5px; color: var(--text-3); margin-top: 4px; line-height: 1.5; }

/* 「默写」小标（和「背单词」并列） */
.lv-recite { background: rgba(139,107,255,.14); color: #6B4FD8; }

/* ---- 老师编辑器 ---- */
.ep {
  background: rgba(255,255,255,.82);
  border: 1px solid rgba(150,178,220,.3);
  border-radius: var(--r-md);
  padding: 12px;
  margin-bottom: 12px;
}
.ep-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.ep-no { font-size: 13px; font-weight: 650; color: var(--text); }
.ep-cnt { font-size: 11.5px; color: var(--text-3); flex: 1; }
.ep-empty { font-size: 12.5px; color: var(--text-3); padding: 6px 2px 10px; }
.ep-add {
  display: flex; align-items: center; justify-content: center; gap: 5px;
  width: 100%; height: 34px; margin-top: 8px;
  border-radius: 10px; border: 1px dashed rgba(150,178,220,.55);
  background: rgba(255,255,255,.5);
  font-size: 12.5px; font-weight: 600; color: var(--primary);
}
.ep-add svg { width: 14px; height: 14px; }

.eb {
  border-top: 1px solid var(--line-2);
  padding: 10px 0 4px;
}
.eb:first-of-type { border-top: 0; }
.eb-zh {
  width: 100%; height: 40px; padding: 0 11px;
  border-radius: 10px; border: 1.5px solid rgba(150,178,220,.35);
  background: rgba(233,243,255,.6);
  font-size: 16px; color: var(--text); -webkit-appearance: none; appearance: none;
  outline: none;
}
.eb-zh:focus { border-color: var(--primary); background: #fff; }
.eb-en {
  width: 100%; margin-top: 7px; padding: 9px 11px;
  border-radius: 10px; border: 1.5px solid rgba(150,178,220,.35);
  background: #fff;
  font-size: 16px; line-height: 1.6; color: var(--text);
  resize: vertical; outline: none; -webkit-appearance: none; appearance: none;
}
.eb-en:focus { border-color: var(--primary); }
.eb-acts { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 7px; }
.eb-a {
  display: inline-flex; align-items: center; gap: 4px;
  height: 27px; padding: 0 10px; border-radius: 8px;
  background: rgba(78,123,255,.09); color: #2B58C4;
  font-size: 12px; font-weight: 600;
}
.eb-a svg { width: 13px; height: 13px; }
.eb-a.danger { background: rgba(244,120,140,.13); color: var(--danger); }
.eb-a:active { transform: scale(.96); }

/* ---- 学生默写页 ---- */
.rc-hero {
  border-radius: var(--r-lg);
  padding: 16px;
  background: linear-gradient(150deg, rgba(255,255,255,.96), rgba(236,245,255,.9));
  border: 1px solid rgba(150,178,220,.3);
  box-shadow: 0 6px 18px rgba(88,120,180,.08);
  margin-bottom: 12px;
}
.rc-ttl { font-size: 17px; font-weight: 700; color: var(--text); letter-spacing: -.2px; }
.rc-meta { font-size: 12.5px; color: var(--text-3); margin-top: 5px; }
.rc-prog { height: 6px; border-radius: 99px; background: rgba(120,140,175,.16); overflow: hidden; margin-top: 12px; }
.rc-prog > i { display: block; height: 100%; width: 0; border-radius: 99px; background: var(--grad); transition: width .25s ease; }
.rc-count { font-size: 12px; color: var(--text-3); margin-top: 7px; }
.rc-last {
  font-size: 12.5px; line-height: 1.6; color: #8A5B00;
  background: rgba(248,182,88,.14); border-radius: 10px;
  padding: 9px 12px; margin-bottom: 12px;
}
.rc-para { margin-bottom: 14px; }
.rc-ptitle {
  font-size: 12.5px; font-weight: 650; color: var(--text-2);
  margin: 0 3px 8px;
}
.rc-block {
  background: rgba(255,255,255,.9);
  border: 1px solid rgba(150,178,220,.3);
  border-radius: var(--r-md);
  padding: 12px;
  margin-bottom: 10px;
}
.rc-zh {
  font-size: 15.5px; line-height: 1.65; color: var(--text);
  font-weight: 500; letter-spacing: .1px;
}
.rc-en {
  width: 100%; margin-top: 10px; padding: 10px 11px;
  border-radius: 10px; border: 1.5px solid rgba(150,178,220,.4);
  background: #fff;
  font-size: 16px; line-height: 1.6; color: var(--text);
  resize: vertical; outline: none; -webkit-appearance: none; appearance: none;
}
.rc-en:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(78,123,255,.12); }
.rc-en::placeholder { color: var(--text-3); font-size: 14px; }

/* 结果页 */
.rc-hero.done { text-align: center; }
.rc-big { font-size: 40px; font-weight: 700; line-height: 1.1; color: var(--text); letter-spacing: -1px; }
.rc-big span { font-size: 18px; font-weight: 500; color: var(--text-3); }
.rc-pts { font-size: 13px; font-weight: 650; color: var(--success); margin-top: 8px; }
.rc-pts.dim { color: var(--text-3); font-weight: 500; }
.rc-wrong { background: rgba(255,255,255,.94); }
.rw-zh { font-size: 13px; color: var(--text-3); line-height: 1.5; }
.rw-ans { font-size: 16px; font-weight: 600; color: var(--text); margin-top: 6px; line-height: 1.55; }
.rw-mine { font-size: 13px; color: var(--danger); margin-top: 7px; line-height: 1.5; }

/* ============ v2.65.0 · 作文默写改成纸笔 + 拍照 ============ */

/* 书架单元页：作文全文 */
.essay-body { margin-top: 10px; }
.eb-para { margin-bottom: 12px; }
.eb-para:last-child { margin-bottom: 0; }
.eb-pt {
  font-size: 12px; font-weight: 650; color: var(--text-3);
  letter-spacing: .4px; margin-bottom: 6px;
}
.eb-blk {
  background: rgba(255,255,255,.9);
  border: 1px solid rgba(150,178,220,.28);
  border-radius: 10px;
  padding: 9px 11px;
  margin-bottom: 7px;
}
.eb-en { font-size: 14.5px; line-height: 1.6; color: var(--text); font-weight: 500; }
.eb-zh { font-size: 13px; line-height: 1.6; color: var(--text-3); margin-top: 3px; }

/* 发布弹层：逐块勾选 */
.pk-head { margin: 14px 0 8px; }
.pk-tools { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
.pk-count { margin-left: auto; font-size: 12.5px; color: var(--text-3); font-weight: 600; }
.pk-list {
  max-height: 46vh; overflow-y: auto; -webkit-overflow-scrolling: touch;
  border: 1px solid rgba(150,178,220,.3); border-radius: 12px;
  padding: 8px 10px; background: rgba(255,255,255,.6);
}
.pk-para { margin-bottom: 10px; }
.pk-para:last-child { margin-bottom: 0; }
.pk-pt { font-size: 12px; font-weight: 650; color: var(--text-3); margin-bottom: 4px; }
.pk-row {
  display: flex; gap: 9px; align-items: flex-start;
  padding: 8px 6px; border-radius: 9px;
}
.pk-row:active { background: rgba(78,123,255,.06); }
.pk-row.off { opacity: .62; }
.pk-row input { margin-top: 3px; width: 17px; height: 17px; flex: 0 0 auto; accent-color: var(--primary); }
.pk-txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.pk-en { font-size: 14px; line-height: 1.55; color: var(--text); }
.pk-zh { font-size: 12.5px; line-height: 1.55; color: var(--text-3); }
.pk-tag {
  flex: 0 0 auto; font-style: normal; font-size: 10.5px; font-weight: 650;
  color: var(--text-3); background: rgba(150,178,220,.18);
  border-radius: 999px; padding: 2px 7px; white-space: nowrap;
}

/* 默写 ① 写：只有中文 */
.rc-len { font-size: 12px; color: var(--text-3); margin-top: 6px; }

/* 默写 ② 核对：答案 + 对/错 */
.rc-ans {
  margin-top: 9px; padding: 9px 11px;
  border-radius: 10px;
  background: rgba(34,197,94,.08);
  border: 1px solid rgba(34,197,94,.28);
  font-size: 15.5px; line-height: 1.6; color: var(--text);
  word-break: break-word;
}
.rc-mk { display: flex; gap: 8px; margin-top: 9px; }
.rc-b {
  flex: 1; padding: 9px 0; border-radius: 10px;
  border: 1.5px solid rgba(150,178,220,.45);
  background: #fff; color: var(--text-2);
  font-size: 14.5px; font-weight: 650;
  -webkit-appearance: none; appearance: none;
}
.rc-b.on.ok { background: rgba(34,197,94,.14); border-color: rgba(34,197,94,.6); color: #15803d; }
.rc-b.on.no { background: rgba(239,68,68,.12); border-color: rgba(239,68,68,.55); color: #b91c1c; }

/* 默写 ② 核对：拍照 */
.rc-shots { display: flex; gap: 8px; flex-wrap: wrap; }
.rc-shot { position: relative; width: 78px; height: 78px; }
.rc-shot img {
  width: 100%; height: 100%; object-fit: cover;
  border-radius: 10px; border: 1px solid rgba(150,178,220,.35);
}
.rc-del {
  position: absolute; top: -6px; right: -6px;
  width: 22px; height: 22px; border-radius: 50%;
  border: none; background: rgba(30,41,59,.82); color: #fff;
  font-size: 12px; line-height: 22px; text-align: center; padding: 0;
}
