/* ============ 小狐狸阅读 · 高保真原型样式 ============ */
:root {
  --bg: #FBF5EA;
  --card: #FFFFFF;
  --ink: #4A3B2A;
  --ink-soft: #8A7A63;
  --primary: #E8833A;
  --primary-deep: #D06B24;
  --blue: #5B9BD5;
  --green: #6FBF63;
  --red: #E06A5A;
  --gold: #F5B940;
  --radius: 24px;
  --shadow: 0 6px 24px rgba(120, 85, 40, .12);
}
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { height: 100%; }
body {
  font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
  background: var(--bg); color: var(--ink);
  overflow: hidden; touch-action: manipulation;
  -webkit-user-select: none; user-select: none;
}
button { font-family: inherit; border: none; cursor: pointer; color: inherit; background: none; touch-action: manipulation; }
[hidden] { display: none !important; }

/* ---- 视图切换 ---- */
.view { position: absolute; inset: 0; display: none; flex-direction: column; }
.view.active { display: flex; animation: fadein .25s ease; }
@keyframes fadein { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* ---- 书架（选书） ---- */
/* 顶栏：左边问候，右边数据（不再居中占半屏，把空间留给书） */
.shelf-top {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  /* 统计条加了中文标签后会变宽，窄屏允许换行，别把标题挤掉 */
  flex-wrap: wrap;
  padding: calc(3vh + env(safe-area-inset-top)) 5vw 1.2vh;
}
/* 左边问候语占剩余空间、可收缩：右边现在是「三块统计条 + 账号胶囊」，
   窄屏挤不下时让标题先让步换行，而不是把账号名字压没 */
.shelf-brand { flex: 1 1 auto; min-width: 0; }
.shelf-hi { font-size: clamp(24px, 2.6vw, 34px); font-weight: 600; letter-spacing: 1px; }
.shelf-sub { margin-top: 4px; font-size: 16px; color: var(--ink-soft); }
.shelf-stats { display: flex; gap: 10px; flex: none; flex-wrap: wrap; justify-content: flex-end; }
.mini-stat {
  display: flex; align-items: baseline; gap: 4px; background: var(--card);
  border-radius: 20px; padding: 8px 14px; box-shadow: var(--shadow); white-space: nowrap;
}
.mini-stat .mi { font-size: 16px; }
/* 中文小标签（打卡 / 读过 / 读完）：只有 emoji + 数字 + 单位家长看不懂 */
.mini-stat .mk { font-size: 13px; color: var(--ink-soft); }
.mini-stat b { font-size: 22px; font-weight: 600; color: var(--primary-deep); }
.mini-stat .ml { font-size: 13px; color: var(--ink-soft); }

/* 「继续上次」入口：一枚小胶囊按钮，跟「🔥打卡 / 🎯读过 / 📚读完」同一行，
   排在打卡左边，不再单独占一行、不再占顶部。
   演进：74px 封面大卡片 → 44px 紧凑横条 → 顶部胶囊 → 现在的统计条内胶囊。
   书架的目标是让娃一眼看到更多书，入口够点就行，进度信息挪到长按/悬停提示里。
   它是 .shelf-stats 的 flex 子项，宽度由内容撑开；max-width 只是给超长书名兜底。 */
.continue-card {
  display: inline-flex; align-items: center; gap: 6px;
  width: auto; max-width: 30vw;
  margin: 0;
  padding: 5px 12px 5px 5px;
  background: linear-gradient(120deg, #FFF3E2, #FFE7CC);
  border: 2px solid #FFD9AE; border-radius: 999px;
  box-shadow: 0 2px 6px rgba(120, 80, 30, .12);
  font-size: 13px; font-weight: 600; color: var(--primary-deep);
  -webkit-tap-highlight-color: transparent;
}
.continue-card:active { transform: scale(.96); }
.continue-cover {
  width: 28px; height: 28px; flex: none; border-radius: 50%; object-fit: cover;
  background: #F0E7D6; display: flex; align-items: center; justify-content: center; font-size: 15px;
}
.continue-title {
  font-size: 13px; font-weight: 600; min-width: 0; max-width: 26vw;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.continue-go { flex: none; font-size: 11px; color: var(--primary); }

.shelf-grid {
  flex: 1; display: flex; flex-wrap: wrap; gap: 2.4vw; justify-content: flex-start;
  align-content: flex-start; padding: 1vh 5vw calc(4vh + env(safe-area-inset-bottom));
  /* 书本多了要能滚动，不能把内容裁掉 */
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  /* webkit 让滚动条可见（iPad Safari 仍按系统默认，仅桌面/安卓 webkit 起效） */
  scrollbar-color: var(--primary) #F0E7D6; scrollbar-width: thin;
  /* 滚动边界阴影提示：iPad 系统滚动条不可见，用这个告诉用户「这里能往上滑」 */
  background:
    linear-gradient(var(--bg) 30%, transparent),
    linear-gradient(transparent, var(--bg) 70%) 0 100%,
    radial-gradient(farthest-side at 50% 0, rgba(180,140,80,.18), transparent),
    radial-gradient(farthest-side at 50% 100%, rgba(180,140,80,.18), transparent) 0 100%;
  background-repeat: no-repeat;
  background-size: 100% 24px, 100% 24px, 100% 12px, 100% 12px;
  background-attachment: local, local, scroll, scroll;
}
.shelf-grid::-webkit-scrollbar { width: 8px; }
.shelf-grid::-webkit-scrollbar-track { background: #F0E7D6; border-radius: 4px; }
.shelf-grid::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 4px; }

/* ---- 书架分类 tab：只有难度分级（小号实心胶囊） ----
   ⚠️ 2026-09-16：「原创故事 / 经典童话」一级 tab 的样式已删 —— 原创书整体下架，
   书库里只剩经典，一级 tab 只剩一个选项，是纯噪音。
   ⚠️ 2026-09-14：档次从 3 档改成最多 6 档（L1 启蒙 … L6 挑战），
   标签由 js/app.js 的 renderLevelTabs() 按「真有书的级别」动态生成，
   所以这里必须能横向滚动，不能换行（换行会把书架内容往下顶）。 */
#view-shelf .level-tabs {
  justify-content: flex-start; gap: 8px;
  margin: 1.6vh 5vw 0; width: fit-content; max-width: 90vw;
  overflow-x: auto; flex-wrap: nowrap;
  scrollbar-width: none; -ms-overflow-style: none;
  position: sticky; top: 0; z-index: 5;
}
#view-shelf .level-tabs::-webkit-scrollbar { display: none; }
#view-shelf .level-tabs .level-tab { font-size: 15px; padding: 8px 18px; }
/* 难度 tab 独立样式（原来蹭 .mod-tab，会 syncedModTabs 误重置，已解耦） */
.level-tab {
  font-size: 16px; padding: 9px 18px; border-radius: 999px;
  color: var(--ink-soft); font-weight: 500; white-space: nowrap;
  background: rgba(255, 255, 255, .72);
  box-shadow: 0 2px 8px rgba(120, 85, 40, .08);
}
.level-tab.active {
  background: linear-gradient(135deg, #FF9A56, var(--primary-deep));
  color: #fff; font-weight: 600; box-shadow: 0 4px 12px rgba(232, 131, 58, .32);
}
.level-tab .l-badge {
  font-size: 12px; font-weight: 700; line-height: 1;
  padding: 3px 8px; border-radius: 999px; margin-left: 5px;
  background: rgba(120, 85, 40, .12); color: var(--ink-soft);
}
.level-tab.active .l-badge { background: rgba(255, 255, 255, .28); color: #fff; }
/* 某一档暂时没书时的空态提示 */
.shelf-empty {
  width: 100%; padding: 8vh 0; text-align: center;
  color: var(--ink-soft); font-size: 17px;
}

/* 每本书的难度角标（配色按六级，L1 启蒙 → L6 挑战 由冷到暖递进） */
.book-card { position: relative; }
.book-lv {
  position: absolute; top: 10px; left: 10px; z-index: 2;
  font-size: 12px; font-weight: 700; letter-spacing: .5px;
  color: #fff; padding: 2px 10px; border-radius: 20px;
  background: linear-gradient(135deg, #F5A623, #ED8B00);  /* 兜底 = L2 起步：橙 */
  box-shadow: 0 2px 6px rgba(0,0,0,.15);
}
.book-card.level-1 .book-lv { background: linear-gradient(135deg, #7DD48A, #35A34C); }  /* L1 启蒙：绿 */
.book-card.level-2 .book-lv { background: linear-gradient(135deg, #F5A623, #ED8B00); }  /* L2 起步：橙 */
.book-card.level-3 .book-lv { background: linear-gradient(135deg, #7EB6FF, #3E7BEF); }  /* L3 入门：蓝 */
.book-card.level-4 .book-lv { background: linear-gradient(135deg, #B98CFF, #7B3FF2); }  /* L4 进阶：紫 */
.book-card.level-5 .book-lv { background: linear-gradient(135deg, #4DD0E1, #0097A7); }  /* L5 熟练：青 */
.book-card.level-6 .book-lv { background: linear-gradient(135deg, #FF8A80, #D32F2F); }  /* L6 挑战：红 */
/* 注：书卡右上角原有「综合 N」角标（.book-score），2026-09-11 按用户要求撤掉。
   口径函数 bookScore() 仍在 js/app.js 里，只是不再显示。 */
.book-card {
  width: min(24vw, 280px); background: var(--card); border-radius: 22px;
  box-shadow: var(--shadow); padding: 14px; display: flex; flex-direction: column;
  gap: 8px; text-align: left; transition: transform .12s;
}
.book-card:active { transform: scale(.97); }
.book-cover {
  /* ⚠️ `height:auto` 不能省：app.js 里给 <img> 写了 width="640" height="427"，
     那是**表现属性**，会当成 height:427px 生效，把下面的 aspect-ratio 顶掉 ——
     结果封面被拉成竖条、只露中间一条（2026-09-11 线上真踩过）。
     写死 height:auto 后，aspect-ratio 生效，宽高比提示仍来自那两个属性。 */
  width: 100%; height: auto; aspect-ratio: 3/2; object-fit: cover; border-radius: 14px; background: #F0E7D6;
  display: flex; align-items: center; justify-content: center; font-size: 40px;
}
/* ⚠️ 2026-09-21 用户反馈「书本上面的进度条有的高有的低，好丑」——
   病根在这里：原来 desc 只有 `min-height: 38px` + 自然换行，实测各书渲染
   **2~6 行**，卡片高度差最多 85px，于是每张卡里的进度条距卡片顶部 254~356px 不等。
   书名与简介都改成**固定行数**（超出省略号）：书名 2 行、简介 3 行。
   `min-height` 用 em（行高倍数）而不是像素——字号本身是 clamp 的，写死像素会错位。
   文案长度差异被胶囊吃掉，卡片等高 → 进度条落在同一条水平线上。
   ⚠️ 别把这行当「想截文案」——书卡在桌子宽屏下每行能放 ~20 字，40~60 字的简介
   基本都在 3 行内；只有 390px 手机（书卡仅 ~145px 宽）才会真截。 */
.book-title {
  font-size: clamp(18px, 1.9vw, 22px); font-weight: 600; line-height: 1.25;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden; min-height: 2.5em;   /* 2 行 × 1.25 */
}
.book-desc {
  font-size: 14px; color: var(--ink-soft); line-height: 1.45;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
  overflow: hidden; min-height: 4.35em;   /* 3 行 × 1.45 */
}
/* 万一某张卡被拉得更高（宽屏同一行里还有更高的卡），meta + 进度条整体贴底，
   进度条就不会浮在半空中。
   ⚠️ 2026-09-21 第二轮：原来这里是 `flex-wrap: wrap` 的一行流式布局，
   但 tag（出处）长度在 2~16 字之间（实测 318 本里 80 个 ≥9 字），
   窄卡里长的 tag 自己就换行 → 不同卡片 meta 差一行（65px vs 48px），
   进度条还是差 17px。改成 grid：**tag 独占第一行、字数与完成度分列第二行两端** ——
   meta 恒为两行，且 tag 过长时省略号收尾（只有窄屏才会真截）。 */
.book-meta {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center;
  gap: 6px 8px; margin-top: auto;
}
.book-tag {
  font-size: 13px; background: #F3EBDA; color: var(--ink-soft); padding: 3px 10px; border-radius: 20px;
  grid-column: 1 / -1; justify-self: start; min-width: 0; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.book-meta > .book-chars { grid-column: 1; }
.book-meta > .book-prog-text { grid-column: 3; }
/* 总字数（2026-09-16 用户要求）：家长挑书时最直观的一个量。
   刻意做成**纯文字**而不是第三个胶囊 —— 三个胶囊并排会把卡片撑爆，
   手机窄屏下书卡只有 ~90px 宽。 */
.book-chars { font-size: 13px; color: var(--ink-soft); }
.book-bar { height: 10px; border-radius: 20px; background: #EFE7D6; overflow: hidden; }
.book-bar i { display: block; height: 100%; background: var(--primary); border-radius: 20px; }
.book-prog-text { font-size: 14px; color: var(--ink-soft); }
.book-card.locked { opacity: .62; }
.book-card.locked .book-cover { font-size: 40px; }
/* 没有封面图的书（原创童话集）用 emoji 当封面 */
.book-cover.emoji-cover {
  background: linear-gradient(160deg, #FFF6E2 0%, #F1E7CE 100%);
  font-size: 56px;
}

/* ---- 首页（书卡 + 三阶段）----------------------------------------
   2026-09-17 重做。改造前是「大标题 / 打卡胶囊 / 独立引言卡 / 三张横条卡」
   四段各说各话（用户评价「太丑」），现在收成两块：一张讲清「这是本什么书」
   （.book-hero），一组讲清「怎么读」（.home-modules 的三个步骤行）。

   ⚠️ .home-scroll 的 `min-height: 0` 是**必须的**，别删：
   .view 是 position:absolute; inset:0 的**定高** flex 容器，flex 子项默认
   min-height:auto（不肯收缩到内容高度以下）。少了这一行，内容不会在这里滚动，
   而是把 .view 整个撑出屏幕，再被 body{overflow:hidden} 裁掉 ——
   这正是「内容展示不全、还没有滚动条」的根因。 */
.home-scroll {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  display: flex; flex-direction: column;
  padding: 2px 0 28px;
}

/* 书卡：封面 + 书名 + 级别/页数/字数 + 引言，长在同一张卡里 */
.book-hero {
  margin: 6px 32px 0; padding: 26px 26px;
  background: var(--card); border-radius: 28px; box-shadow: var(--shadow);
  display: flex; flex-direction: column; gap: 22px;
}
.hero-head { display: flex; align-items: center; gap: 20px; }
.hero-cover {
  flex: none; width: 100px; height: 100px; border-radius: 24px; overflow: hidden;
  background: linear-gradient(160deg, #FFF6E2 0%, #F1E7CE 100%);
  border: 3px solid #fff;
  /* 一圈白边 + 浅投影：让封面看着像一张小绘本封面，而不是一块灰色占位方块。
     （没有插图的书只有 emoji，光靠 emoji 撑不起视觉重心） */
  box-shadow: 0 4px 14px rgba(150, 110, 60, .16);
  display: flex; align-items: center; justify-content: center;
  font-size: 52px; line-height: 1;
}
.hero-cover img { width: 100%; height: 100%; object-fit: cover; }
.hero-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 10px; }
.hero-title { font-size: 28px; font-weight: 700; line-height: 1.25; overflow-wrap: anywhere; }
.hero-tags { display: flex; flex-wrap: wrap; gap: 8px; }
.hero-tag {
  font-size: 14px; color: var(--ink-soft); white-space: nowrap;
  background: #F6EEDD; border-radius: 12px; padding: 5px 12px;
}
.hero-tag.lv { color: var(--primary-deep); font-weight: 600; }

/* ---- 「关于这本书」引言（2026-09-16 新增，09-17 两轮改）----
   内容是给孩子**自己读**的（句长守本级上限，门禁 tools/check-books.js 守着）。
   虚线分隔而不是另起一张浮空卡片 —— 它就是这张书卡的一部分（「书的前言」）。
   📌 2026-09-17 第二轮：数据里仍是**句子数组**（生成器与门禁按句校验句长），
   但显示上是**一整段**。原先一句一个米色小方块，四句摞起来像四行待办项，
   又碎又吵（用户原话「不要分成那么多句子」）—— 现在整段只挂一个朗读按钮。 */
.home-intro {
  border-top: 1px dashed #EADFC8; padding-top: 16px;
  display: flex; flex-direction: column; gap: 11px;
}
/* [hidden] 必须显式压回 display:none —— 上面写了 display:flex 就会盖掉 hidden */
.home-intro[hidden] { display: none; }
.intro-cap {
  display: flex; align-items: center; gap: 6px;
  font-size: 14px; color: var(--ink-soft); letter-spacing: 1px;
}
.intro-ico { font-size: 16px; }
.intro-lines { display: flex; flex-direction: column; }

/* 整段 = 一个按钮：左边一枚圆形播放键（::before 画的），右边是连贯的一整段话。
   点一次读整段，再点一次停。
   ⚠️ 图标必须是 ::before 的 content —— 伪元素不进 textContent，
   所以 .intro-block 的 textContent 就是**纯段落文字**，e2e 拿它跟
   BOOK.intro.join("") 逐字比对。别改成「<span>▶</span> + 文字」。 */
.intro-block {
  width: 100%; display: flex; align-items: flex-start; gap: 12px;
  background: #FBF4E6; border-radius: 18px;
  padding: 13px 15px;
  font-size: 17.5px; line-height: 1.68; color: var(--ink);
  text-align: left; overflow-wrap: anywhere;
  transition: background .13s, transform .1s;
}
.intro-block::before {
  content: "▶";
  flex: none; width: 36px; height: 36px; border-radius: 50%;
  margin-top: -3px;                 /* 与第一行文字的视觉中线对齐 */
  display: flex; align-items: center; justify-content: center;
  padding-left: 3px;                /* ▶ 的重心偏左，补一点才视觉居中 */
  font-size: 14px; color: #fff;
  background: linear-gradient(150deg, #FFB067, var(--primary));
  box-shadow: 0 3px 9px rgba(232, 131, 58, .3);
  transition: box-shadow .13s, background .13s;
}
.intro-block:active { transform: scale(.99); }
/* 正在读：底色变暖 + 播放键换成「暂停」并外扩一圈光晕（孩子一眼看出在播） */
.intro-block.playing { background: #FFEFD9; }
.intro-block.playing::before {
  content: "❚❚"; padding-left: 0; font-size: 11px;
  background: linear-gradient(150deg, var(--primary), var(--primary-deep));
  box-shadow: 0 0 0 5px rgba(232, 131, 58, .15);
}

/* 三个功能：序号圆标 + 标题 + 进度条 + 进度文字 + 右箭头 */
/* 2026-09-18 第四轮改版（用户：「卡片里三行信息太挤」+「底部一大片空白，上重下轻」）：
   ① 卡片内部整体松绑（padding / 行距 / 标题字号都放大一档），三行信息不再挤在 88px 里；
   ② 模块列改成**底部锚定** —— 用 margin-top:auto 吃掉垂直富余。
      这样书卡贴着顶栏、功能卡落在屏幕下半，空白被分成「卡间距」而不是
      「底部一大片」。顺带把三个可点区域推到拇指够得着的地方（单手拿手机时
      屏幕下方最好点，这是移动端的常识）。
      ⚠️ margin-top:auto 在**内容超出**时会退化成 0（不会挤压、也不会裁掉内容），
      所以必须配 padding-top 兜住「书卡与功能卡至少隔开 22px」；
      flex 的 auto margin 与 justify-content:center 不同，它在溢出时是安全的
      —— 后者会把顶部内容顶出可视区并且滚不回去，正是「内容展示不全」那个老坑。 */
.home-modules {
  margin: 20px 32px 0;
  display: flex; flex-direction: column; gap: 16px;
  /* 弹性填满 .home-scroll 的剩余高度（见竖屏块里的详细说明） */
  flex: 1 0 auto; justify-content: flex-end;
}
.mod-card {
  position: relative;
  display: flex; align-items: center; gap: 16px;
  background: var(--card); border-radius: 22px; box-shadow: var(--shadow);
  padding: 18px 20px; text-align: left;
  transition: transform .12s;
  flex: 1 0 auto; max-height: 168px;
}
.mod-card:active { transform: scale(.985); }
.mod-no {
  flex: none; width: 44px; height: 44px; border-radius: 50%;
  background: var(--primary); color: #fff;
  font-size: 20px; font-weight: 700;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 3px 0 var(--primary-deep);
}
.mod-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 10px; }
.mod-title { font-size: 22px; font-weight: 600; line-height: 1.2; }
/* 副标题与进度文字**同一行**、左右各占一头，进度条于是能占满整行（2026-09-17）。
   原先进度文字单独占一行，三张卡凭空多出三行，首屏被撑满还得往下滚。
   ⚠️ 也别把进度文字摆到进度条右侧：每本书的文案长短不同（「还没开始」 vs
   「先完成「听故事」」），进度条会被挤成三种宽度，三张卡左右对不齐就散了。 */
.mod-meta { display: flex; align-items: baseline; gap: 10px; }
.mod-sub {
  flex: 1; min-width: 0; font-size: 13.5px; color: var(--ink-soft);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mod-bar { height: 8px; border-radius: 20px; background: #EFE7D6; overflow: hidden; }
.mod-bar i {
  display: block; height: 100%; width: 0;
  background: var(--primary); border-radius: 20px;
  transition: width .3s ease;
}
.mod-prog {
  flex: none; white-space: nowrap;
  font-size: 13.5px; font-weight: 600; color: var(--primary-deep);
}
/* 右箭头做成一枚圆形按钮（2026-09-17）：原先是一根裸着的橙色小三角，
   弱到看不出「这里可以点」。 */
.mod-go {
  flex: none; width: 38px; height: 38px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  padding-left: 2px;
  font-size: 14px; color: #fff;
  background: linear-gradient(150deg, #FFB067, var(--primary));
  box-shadow: 0 3px 9px rgba(232, 131, 58, .3);
}

/* ---------- 模块顺序锁（2026-09-14，09-17 改成整行灰化） ----------
   1 听故事 → 2 读句子 → 3 闯关练习。未解锁的整行灰掉、右箭头变锁，
   锁定理由由 renderModLocks() 直接写进 .mod-prog（原来只挂在 title 属性上，
   而手机上根本没有 hover —— 孩子点不动也不知道为什么）。 */
.mod-card.locked { background: #F4EEE2; box-shadow: none; }
.mod-card.locked:active { transform: none; }
.mod-card.locked .mod-no { background: #D5CAB6; box-shadow: 0 3px 0 #C2B7A2; }
.mod-card.locked .mod-title { color: var(--ink-soft); }
.mod-card.locked .mod-bar i { background: #D5CAB6; }
.mod-card.locked .mod-prog { color: var(--ink-soft); font-weight: 500; }
/* 锁：灰底灰锁。🔒 是彩色 emoji，直接摆上去比整行都跳，所以压成灰阶 */
.mod-card.locked .mod-go {
  background: #E7DFD0; box-shadow: none;
  padding-left: 0; font-size: 15px;
  filter: grayscale(1); opacity: .7;
}

/* 宽屏：书卡与三个步骤左右分栏。不然一块窄条被拉到 1200px 宽，像张纸条。 */
@media (min-width: 900px) and (min-aspect-ratio: 1/1) {
  .home-scroll {
    display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
    align-content: start; gap: 0 28px; padding: 8px 5vw 28px;
  }
  .book-hero { margin: 0; }
  /* 两栏布局里不需要弹性填满，限高也收掉（否则卡片长高会跟左栏不齐） */
  .home-modules { margin: 0; padding-top: 0; gap: 14px; flex: none; justify-content: flex-start; }
  .mod-card { flex: none; max-height: none; }
}
.words-head { display: flex; justify-content: center; padding: 4px 24px 0; }

/* ---- 通用按钮 ---- */
.btn-big {
  font-size: 26px; font-weight: 600; padding: 20px 64px; border-radius: 60px;
  letter-spacing: 4px; transition: transform .1s;
}
.btn-big:active { transform: scale(.96); }
.btn-primary { background: var(--primary); color: #fff; box-shadow: 0 6px 0 var(--primary-deep); }
.btn-primary:active { box-shadow: 0 2px 0 var(--primary-deep); transform: translateY(4px); }
.btn-icon {
  width: 52px; height: 52px; border-radius: 50%; background: var(--card);
  font-size: 24px; box-shadow: var(--shadow); flex-shrink: 0;
}
.btn-pill {
  font-size: 17px; padding: 10px 20px; border-radius: 40px;
  background: var(--card); box-shadow: var(--shadow); color: var(--ink);
}
.btn-pill.on { background: var(--primary); color: #fff; }
.btn-mid {
  font-size: 20px; padding: 16px 30px; border-radius: 40px;
  background: var(--card); box-shadow: var(--shadow); color: var(--ink); font-weight: 500;
}
.btn-mid:disabled { opacity: .45; }
.btn-nav {
  width: 64px; height: 64px; border-radius: 50%; background: var(--card);
  font-size: 22px; box-shadow: var(--shadow); flex-shrink: 0;
}
/* 末页/末句时右箭头换成文字行动按钮（「去读句子」/「去闯关」）——
   ▶ 的圆形放不下三个字，改成同高的胶囊；padding 刻意压小（0 14px），
   因为它是和「上一句 ◀ / 上一页 ◀」并排占一行，撑宽了会把中间的句子卡挤窄。
   高度与 .btn-nav 完全一致，翻页切换时按钮不会跳。 */
.btn-nav.wide {
  width: auto; min-width: 64px; height: 64px; border-radius: 32px;
  padding: 0 14px; font-size: 16px; font-weight: 600;
  line-height: 1.15; white-space: nowrap;
}
/* 打上主色当行动召唤：读到最后一刻这个按钮就是「下一步该干嘛」 */
.btn-nav.cta {
  background: linear-gradient(135deg, #FF9A56, var(--primary-deep));
  color: #fff; box-shadow: 0 4px 14px rgba(232, 131, 58, .35);
}
.btn-close {
  font-size: 19px; padding: 12px 34px; border-radius: 40px;
  background: var(--primary); color: #fff; margin-top: 6px;
}

/* ---- 顶栏 ---- */
.topbar {
  display: flex; align-items: center; gap: 16px;
  padding: calc(12px + env(safe-area-inset-top)) 24px 12px;
}
/* 书名标题固定宽度：三页顶栏的书名宽度一致，tabs 紧跟其后就不会因书名长短而漂移 */
.topbar-title { font-size: 21px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 0 0 auto; min-width: 8em; }
.topbar-actions { display: flex; gap: 10px; margin-left: auto; }
/* 模块 tab（听故事/读句子/闯关）三页位置一致：
   书名 title 用 flex:0 不占满，tabs 紧跟其后固定在书名右侧的同一位置，
   右侧 actions 各自靠右，不影响 tabs 的横向位置 */
.topbar .mod-tabs { flex: none; }

/* ---- 🔴 窄屏（手机竖屏）顶栏一行放不下：实测 390px 需要 703px ----
   组成：返回 52 + 书名 168（min-width:8em，桌面端用来固定 tabs 位置）+
        模块 tab 281（flex:none，不能压缩）+ 右侧动作 130 + 间距/内边距。
   而 body 是 `overflow-x:hidden` —— 溢出部分不是「能滑过去」，是**直接裁掉**：
   孩子看不到也点不到「📦 录音箱」「拼音开关」和模块 tab。
   （2026-09-14 用 CDP 390px 设备模拟量出来的，见 tools/e2e/kid-nav-test.mjs）
   处理：模块 tab 独占第二行（一个功能都不删），书名放开固定宽度、允许省略号收缩。
   只动这三个模块页——书架顶栏（品牌 + 账号胶囊）本来就放得下。 */
@media (max-width: 560px) {
  #view-story .topbar, #view-words .topbar, #view-quiz .topbar {
    flex-wrap: wrap; row-gap: 8px; padding-left: 16px; padding-right: 16px;
  }
  #view-story .topbar .mod-tabs,
  #view-words .topbar .mod-tabs,
  #view-quiz .topbar .mod-tabs { order: 3; width: 100%; justify-content: center; }
  #view-story .topbar-actions,
  #view-words .topbar-actions,
  #view-quiz .topbar-actions { order: 2; }
  /* flex 从 0 0 auto 放开成可收缩：书名太长时省略号，而不是把顶栏顶宽 */
  #view-story .topbar-title,
  #view-words .topbar-title,
  #view-quiz .topbar-title { min-width: 0; flex: 0 1 auto; }
}

/* ---- 听故事 ---- */
.story-body { flex: 1; display: flex; gap: 20px; padding: 0 24px; min-height: 0; }
.story-text-panel {
  flex: 1.9; background: var(--card); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 4vh 2vw;
  display: flex; flex-direction: column; gap: 3.2vh;
  overflow-y: auto;
  /* ⚠️ 不能用 justify-content:center——flex 容器「居中 + 滚动」时溢出量被均分到两端，
     顶部内容会被裁掉且永远滚不回来（点开《迷路的小邮差》第一句被挡就是这个原因）。
     改用子元素 margin:auto 实现同样的居中效果，内容超高时 margin 归零、可滚到顶。 */
}
/* --fit 由 JS 动态调整（fitStoryText）：整本书所有页装不下时统一缩一档字号，
   保证文字区尽量不出滚动条；句子少、装得下的书是 1。

   --fs / --sgap / --py / --lh 是四个版式旋钮，**整本书共用同一组值**：

   🔴 2026-09-18 第五轮：**句间距改由 --fs 派生**，不再有第二套值。
     之前是 `.story-text{gap:2.4vh}` + `.tight{gap:1.2vh}`，手机竖屏又被
     `@media (max-width:560px)` 覆盖成 `gap:12px` —— 于是「不 tight 的书 12px、
     tight 的书 1.2vh(=10.1px)」，不同的书间隔真的不一样。
     现在 gap = max(10px, .55 × 句子字号)：字号大间距跟着大（正常的行间节奏），
     但整库只有一个公式，任何两页、两本书之间都不会冒出第三种间距。

   🔴 同时删掉了 `.sentence` 的 `min-height: var(--smin)`（按页统一句高）。
     它是「同一页里短句上下凭空多出整行空白」和「翻页格式不一样」的直接来源，
     详见 js/app.js 的 computeBookFit() 顶部注释。

     · --py 拼音字号：**必须带绝对下限**。0.7 档时 .52em 只剩 9px 出头 ——
       孩子根本看不清，拼音就白标了。下限只兜底，正常情况下仍跟着字号缩放。
     · --lh 汉字行高：原先是 2.1 —— 26px 的字撑出 54.6px 的行盒，垂直空间
       全浪费在字之间，自适应只好一路把字号压到最小档（这才是「拼音小」的根因：
       拼音是 em 单位、跟着字号一起被压）。收到 1.35 后同一页能容纳的字号大
       一档多。行距不再靠 line-height 兜，而是「拼音带 + 汉字行」两块自己构成。 */
.story-text {
  --fs: calc(clamp(26px, 2.7vw, 36px) * var(--fit));
  --sgap: max(10px, calc(var(--fs) * .55));
  --py: max(11px, .52em);
  --lh: 1.35;
  --fit: 1;
  display: flex; flex-direction: column; gap: var(--sgap); margin: auto 0;
}
/* 兜底：缩到最小档 70% 仍装不下时，再收紧行距（JS 加 tight）
   ⚠️ 这一档**只收行距、绝不动间距** —— 间距一旦分两套值，
   就会重演用户报的「有的书间隔大、有的书间隔小」。 */
.story-text.tight { --lh: 1.2; }
.story-text.tight .zi i { margin-bottom: 1px; }
.sentence {
  font-size: var(--fs); line-height: var(--lh); color: var(--ink);
  transition: color .2s, background .2s; border-radius: 14px; padding: 4px 10px;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-start;
  /* 整句就是热区：点它 = 从这句往下重听（2026-09-18 第三轮）
     ⚠️ 这里**没有 min-height**：句子高度 = 它自己的自然高度（1 行就是 1 行，
     2 行就是 2 行）。句子之间的距离由 .story-text 的统一 gap 负责。 */
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.sentence.speaking { background: #FFE8CF; color: var(--primary-deep); }
.sentence.speaking .zi i { color: var(--primary-deep); }
.sentence.done { color: var(--ink); }
/* 点句重听的落点反馈：闪一下底色就退（动画结束自己回到常态，不用 JS 收尾） */
.sentence.hit { animation: sentHit .45s ease-out; }
@keyframes sentHit { from { background: #FFD9B0; } to { background: transparent; } }
/* 单字：拼音在上、汉字在下，严格居中 */
.zi {
  display: inline-flex; flex-direction: column; align-items: center;
  margin: 0 3px; vertical-align: bottom;
}
.zi i {
  /* 拼音比汉字宽，是句子折行的主因 —— 这份宽度靠下面 .zi 的 margin 收敛，
     不再靠把字号收到看不见。
     ⚠️ min-height 不能删：**没有拼音的字（标点）也要占同样一条拼音带**。
     少了它，标点所在的 .zi 比汉字矮一截，会被 .sentence 的 align-items:center
     往上顶半条拼音带（实测 4~7px），看起来像「标点浮在半空」。 */
  font-style: normal; font-size: var(--py); line-height: 1.15; min-height: 1.15em;
  color: var(--ink-soft); margin-bottom: 2px; white-space: nowrap;
}
.zi b { font-weight: inherit; }
/* 2026-09-18 第四轮：`.zi.hw`（热词高亮 + 可点）已删 —— 用户要求把
   「系统默认挑出的词语可点击弹小窗看读音+解释」整套去掉，且连正文里的热词标记
   一起删。正文现在是纯文字，全句只有「整句点击重听」一个交互。 */
.no-pinyin .zi i { display: none; }

/* ---------- 段落式正文（2026-09-22，L3 及以上，由 JS 加 .para）----------
   用户原话：「听故事的页面，是不是没必要一个句子一行？除了打油诗等对格式有要求的，
   正常的故事是不是可以弄成段落的形式。这样排列也整齐，也省空间，可以把字体弄大点，
   小朋友看拼音也清楚。」

   为什么值得改（可量化）：句子独占一行时，**每句行尾的残余宽度全部浪费**。
   L5/L6 一句 17~22 字、一行只放得下 8~9 字 → 一句占 2~3 行，末尾必剩半行；
   一页 4 句实测浪费约 3 行（25%）。段内连排后这些空间被下一句填满 ——
   同样的内容占更少的行，自适应（computeBookFit）就能把 --fit 调大一档。

   为什么**不是**「把多句合并成一个 <p>」：一句话必须仍是独立元素 ——
   · 点句重听（renderStoryPage 给每个 .sentence 挂 onclick）
   · 跳转条 / 逐句朗读高亮 / keepSentenceInView / 整页进度
   所以这里**只改 display，不动 DOM 结构**。

   ⚠️ .sentence 的 `padding: 4px 10px` 必须归零：连排时每句左右各多 10px，
      段落的左右边界会参差不齐（同一段首尾各缩 10px）。
   ⚠️ 行距：inline 流里行盒高度由最高的行内盒决定 —— .zi（拼音带+汉字）约 1.7em，
      本来就比 --lh(1.35) 高，所以行距由 .zi 顶着走、天然够。
      因此**不要在这里放大 line-height**：那只会把行盒从 .zi 的高度顶到 strut 的高度，
      白涨高度（与「省空间换字号」正好相反）。反过来**也别缩到 .zi 以下** ——
      缩了不生效，行盒仍由 .zi 顶住。
      同理 .tight 的 `--lh: 1.2` 对段落式几乎无感（它真正起作用的是「一句一行」的书）。 */
.story-text.para { display: block; text-indent: 2em; }
.story-text.para .sentence { display: inline; padding: 0; border-radius: 0; }
/* 🔴 必须给 .zi 定宽（= 一个汉字宽），否则段落式会比一句一行**宽得多**：
   .zi 是 inline-flex —— 在 .sentence（flex 容器）里会被 flex-shrink 压回汉字宽，
   但一旦 .sentence 变成 inline、它掉进普通行内流，就按 max-content 取宽
   （= 拼音的宽度）。实测同一个字：一句一行 18px、段落式 50px，
   每行只放得下 5.5 字（本该 12 字），正文高度从 249px 涨到 464px。
   定宽后拼音仍然照常溢出显示（.zi i 是 white-space:nowrap），
   与一句一行时的观感一致。量法见 tmp/rz-work/para-diag2.mjs。 */
/* 🔴 段落式的**字格宽度**必须按拼音的实际宽度给 —— 否则相邻两格的拼音会首尾相接，
   对拼音还不熟的孩子分不清哪串属于哪个字（用户 2026-09-24 报的「睡着」）。
   成因：`--py` 拼音字号下，一个 4 字符带调音节（shuì / zháo）实测宽 ≈ 1.04em
   —— **比 1em 的字格还宽**。

   为什么**不**统一按最长音节（chuāng ≈ 1.84em @--py）定宽、也不 width:auto：
     · 统一放宽 → 93% 的短音节格子白空 70%，每行字数从 12 掉到 8，
       computeBookFit 被迫缩档；
     · width:auto（让内容撑）→ **实测更糟**：段落式掉进普通行内流后按 max-content
       取宽 = 拼音宽，L2 的 54 本 --fit 从 1 集体掉到 0.7（就是上面那段注释说的 5.5 字/行）。

   所以按**音节各自的实际宽度**给（不是按字母数分档 —— 试过，不行）：
     · 第一版按「拼音几个字母」分三档（≤4 保持 1em / 5 档 3.3×--py / ≥6 档 3.95×），
       全库仍有 2186 处相邻拼音叠在一起 —— 因为字母数不是宽度的好代理
       （4 字母的 máng / bāng / cōng 与 5 字母的 shàng 一样宽）；
     · 现在由 JS 逐个实测（canvas measureText，见 js/app.js 的 applyParaZiWidth）。
   ⚠️ 宽度这段**不能删**（见上一段注释）：删掉 width 就是回到 1em 的相接；
      它同时是 JS 没跑起来（或拼音被整页撤掉）时的兜底值。
   ⚠️ 只动段落式：一句一行时 .zi 由 flex-shrink 压回汉字宽，是另一套机制，别一起改。
   ⚠️ 字格变宽会让**行数**变多 → 正文变高 → computeBookFit 会缩档。
      所以量版式的离屏探针（renderIntoProbe）必须走**同一套**宽度逻辑，
      否则量出来的 --fit 偏乐观、真面板溢出。 */
.story-text.para .zi { width: 1em; margin: 0 4px; }
/* 🔴 margin 的 4px 是 JS 侧 gap 计算的输入（PARA_ZI_GAP=4）：
   两侧共 8px，字格取 `拼音宽 − 4` 后，两串拼音的墨迹间隙仍剩 4px。
   改这里必须同步改 js/app.js 的 PARA_ZI_GAP。 */
/* 连排时两句之间只有标点自身的宽度，再补一点点，免得「。下一句」挤成一团 */
.story-text.para .sentence + .sentence { margin-left: .12em; }

/* 首次访问引导：非 secure context 时浮在内容之上，半透明遮罩 */
#trust-banner {
  position: fixed; inset: 0; z-index: 9999;
  background: rgba(20, 20, 20, .55);
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
}
#trust-banner[hidden] { display: none; }
.trust-card {
  background: var(--card); color: var(--ink);
  border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,.35);
  max-width: 460px; padding: 24px 26px;
  font-size: 16px; line-height: 1.6;
}
.trust-card h2 { margin: 0 0 10px; font-size: 22px; }
.trust-card p  { margin: 8px 0; }
.trust-card code { background: #f0e8da; padding: 1px 6px; border-radius: 4px; font-size: 14px; }
.trust-card .btn { margin-top: 14px; padding: 10px 18px; }
.trust-card .btn-link { display: block; margin-top: 8px; background: none; border: none; color: var(--ink-soft); cursor: pointer; text-decoration: underline; font-size: 13px; }
/* 页码挪到页脚中央（和翻页圆点在一起），不再挤在文字面板顶部——
   之前「《书名》第 X / N 页」贴在文字框里，书名顶栏已有，位置很别扭 */
.story-foot-mid { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.story-page-no { font-size: 15px; color: var(--ink-soft); text-align: center; }
.story-footer { display: flex; align-items: center; justify-content: space-between; padding: 10px 24px calc(10px + env(safe-area-inset-bottom)); }
.dots { display: flex; gap: 8px; }
.dot { width: 10px; height: 10px; border-radius: 50%; background: #E3D9C6; }
.dot.cur { background: var(--primary); transform: scale(1.3); }

/* 页码 / 词语索引 跳转按钮 */
.page-jump, .word-jump { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
/* margin-bottom:auto + 句子卡的 margin-top:auto = 把「卡片 + 跳转格」当成一整组
   在 .word-col 里居中（2026-09-18 第六轮）。用 auto 外边距而不是 justify-content:center：
   列是 overflow-y:auto 的滚动容器，居中 justify 在内容超高时会把顶部推到够不到的地方，
   auto 外边距在没有余量时会塌成 0，不会出这个问题。 */
.word-jump { margin-top: 10px; margin-bottom: auto; }
.jump-btn {
  width: 40px; height: 40px; border-radius: 12px; font-size: 17px;
  background: var(--card); color: var(--ink-soft); box-shadow: var(--shadow);
  font-weight: 600;
}
.jump-btn.cur { background: var(--primary); color: #fff; }
/* 听故事页的页码：绿色 = 这一页每一句都完整听完了（2026-09-16 用户要求）。
   .cur = 孩子正停在这一页。两者同时成立时用「橙底 + 绿圈」——
   否则 .cur 会把绿色盖掉，孩子刚听完这一页却看不到任何变化。 */
.jump-btn.done { background: #DFF0D6; color: #3E7D2C; }
.jump-btn.done.cur { background: var(--primary); color: #fff; box-shadow: 0 0 0 3px #7DD48A; }

/* 词语索引卡片：左上是序号、中央大号分数，干净分开 */
.word-jump .jump-btn {
  width: 60px; height: 60px; padding: 0;
  display: grid; grid-template-rows: 16px 1fr; align-items: center; justify-items: center;
  position: relative;
}
.word-jump .jump-btn b.no {
  align-self: start; justify-self: start;
  font-size: 11px; font-weight: 500; opacity: .55; margin: 5px 0 0 7px;
}
.word-jump .jump-btn em.sc {
  font-size: 22px; line-height: 1; font-style: normal; font-weight: 700; color: inherit;
  margin-top: -2px;
}
.word-jump .jump-btn.unread { background: #F4EDDF; color: #B0A48B; }
.word-jump .jump-btn.unread em.sc { opacity: .55; font-weight: 500; font-size: 18px; }
.word-jump .jump-btn.unread.cur { background: var(--primary); color: #fff; }

/* 分数五档配色（90+/75/60/40/<40） */
.word-jump .jump-btn.t-best { background: #D9EFCC; color: #2E6A1E; }
.word-jump .jump-btn.t-good { background: #E8F2CF; color: #5F7E2F; }
.word-jump .jump-btn.t-okay { background: #FFF0B8; color: #8A6A1E; }
.word-jump .jump-btn.t-low  { background: #FFDDB6; color: #A85A24; }
.word-jump .jump-btn.t-bad  { background: #FBD7D1; color: #A23A2E; }
.word-jump .jump-btn.t-best.cur, .word-jump .jump-btn.t-good.cur,
.word-jump .jump-btn.t-okay.cur, .word-jump .jump-btn.t-low.cur,
.word-jump .jump-btn.t-bad.cur { background: var(--primary); color: #fff; }

/* 词语大分数：100 分制 */
.word-score-num { font-size: 64px; font-weight: 700; color: var(--primary-deep); line-height: 1; display: flex; align-items: baseline; justify-content: center; gap: 2px; }
.word-score-num i { font-size: 22px; font-weight: 500; color: var(--ink-soft); font-style: normal; }
.word-score-num.pop { animation: scorePop .45s ease-out; }
@keyframes scorePop { 0%{transform:scale(.5);opacity:0} 60%{transform:scale(1.15);opacity:1} 100%{transform:scale(1);opacity:1} }

/* 闯关题目索引条 */
.quiz-footer {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 10px 16px calc(12px + env(safe-area-inset-bottom));
}
.quiz-jump { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.jump-btn.right { background: #DFF0D6; color: #3E7D2C; }
.jump-btn.wrong { background: #FBE0DC; color: #B3483A; }
.jump-btn.right.cur, .jump-btn.wrong.cur { background: var(--primary); color: #fff; }
#btn-quiz-done { background: var(--green); color: #fff; font-weight: 600; }

/* 顶部模块切换 tab */
.mod-tabs { display: flex; gap: 6px; background: #F0E7D6; padding: 5px; border-radius: 40px; }
.mod-tab {
  font-size: 16px; padding: 9px 18px; border-radius: 30px;
  color: var(--ink-soft); font-weight: 500; white-space: nowrap;
}
.mod-tab.active { background: var(--card); color: var(--primary-deep); font-weight: 600; box-shadow: var(--shadow); }
/* 解锁态（2026-09-18 第六轮）。
   用户报「听故事时页面上方的 听故事、读句子、闯关 没有展示出是否已解锁的样式」——
   首页那三张卡片一直有锁态（灰掉 + 🔒 + 「先完成『听故事』」），顶栏这份只有高亮，
   同一份状态两套口径。现在补上：未解锁 → 灰一档 + 🔒；已完成 → ✓。
   图标一律走 ::after 伪元素（**不进 textContent**，e2e 逐字比对 tab 文案），
   桌面再靠 title 说明原因。
   ⚠️ .locked 写在 .active 之后：锁定态的颜色要能压住高亮态。
   ⚠️ 不禁用点击：点得动、点了 toast 告诉孩子为什么（launchMod 里拦），
      比灰成不可点更好 —— 否则孩子会以为按钮坏了。 */
.mod-tab.locked { color: #B4A98F; }
.mod-tab.locked::after { content: "🔒"; font-size: 12px; margin-left: 4px; }
.mod-tab.done::after { content: "✓"; font-size: 13px; font-weight: 700; margin-left: 4px; color: var(--green); }
.mod-tab.done.active::after { color: var(--primary); }

/* ---- 读句子 ---- */
.tabs { display: flex; gap: 8px; flex: 1; }
.tab {
  font-size: 17px; padding: 10px 22px; border-radius: 40px;
  background: var(--card); color: var(--ink-soft); box-shadow: var(--shadow);
}
.tab.active { background: var(--primary); color: #fff; font-weight: 600; }
.words-body { flex: 1; display: flex; align-items: center; gap: 18px; padding: 0 18px; min-height: 0; }
.word-col { flex: 1; display: flex; flex-direction: column; align-items: center; min-width: 0; }
.word-card {
  width: 100%; min-height: 34vh; max-height: 54vh; background: var(--card);
  border-radius: var(--radius); box-shadow: var(--shadow);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1.6vh;
  /* 下内边距专门留给「落款」（.word-example，绝对定位贴下沿），卡片再高也压不到它 */
  padding: 24px 24px 56px; position: relative;
}
.word-main { font-size: clamp(44px, 5.4vw, 68px); font-weight: 600; letter-spacing: 8px; text-align: center; }
/* pun = 长句排版：字号收一档、允许折行，避免整句溢出卡片 */
.word-main.pun {
  font-size: clamp(26px, 3vw, 40px);
  letter-spacing: 2px; line-height: 1.4;
  max-width: 100%; overflow-wrap: anywhere;
}
/* 读句子页逐字结构：拼音在上、汉字在下，每个字一格严格居中。
   .zi 默认横向 margin 4px 是听故事页节奏，句子页收窄些；拼音字号同听故事页
   一样带**绝对下限**（2026-09-18 第三轮「拼音弄大点」），长句缩到 22px 时
   也不会掉到 11px 以下。min-height 的用处见听故事页 `.zi i` 的注释。
   ⚠️ 拼音必须自己压掉 letter-spacing：`.word-main` 是 8px（短词）/ .pun 是 2px，
     那是给**汉字**拉开字距用的，但 letter-spacing 会继承 ——
     2px 摊到拼音的每个字母上，5 个字母的「shàng」白白多出 10px，
     拼音被拉得又宽又散（这也是它看起来比汉字抢眼的原因之一）。 */
.word-main .zi { margin: 0 3px; line-height: 1.25; }
.word-main .zi i {
  font-size: max(11px, .46em); line-height: 1.15; min-height: 1.15em;
  letter-spacing: .5px;
  margin-bottom: 3px; color: var(--blue);
}
.word-main .zi b { font-size: 1em; }
.word-main.no-pinyin .zi i { display: none; }

/* ---------- 逐字发音批注（读句子页，2026-09-14）----------
   读完之后每个字按 SOE 逐字精准度着色；点字可单独听发音。
   档位 = 数据上的 data-sc（great/ok/bad/miss），由 Score.charState 判定，
   与 js/score.js 的阈值同源，别在这里另立标准。
   注：这里按「学习反馈」用绿=好、红=要重读，与股市红涨绿跌无关。 */
.word-main .zi.tap { cursor: pointer; }
.word-main .zi.tap:active b { transform: translateY(1px); }
/* ---------- 逐字动态撤拼音的视觉提示（2026-09-22）----------
   认得的字不再给拼音（见 js/charprofile.js），但这些字必须**看起来像能点** ——
   孩子得知道「这里可以点开看拼音」，否则他以为字本来就长这样、根本不会去求助，
   而这套机制最值钱的那条信号（孩子主动点开拼音 = 自报不认识）就一条也采不到。
   一条虚线下划线就够：足够显眼，又不至于让句子看起来像满篇错字。
   点开之后撤销提示（.py-open，拼音已经在眼前，不再需要引导）。 */
.word-main .zi.py-cut b {
  border-bottom: 2px dashed rgba(91, 155, 213, .5);
  padding-bottom: 1px;
}
.word-main .zi.py-cut:active b { background: rgba(91, 155, 213, .12); }
/* ---------- 声调提示点（2026-09-22，SOE 声调评测）----------
   字读对了、但声调没读准 → 字底下加一个小橙点，title 写「声调再听一听」。
   ⚠️ **刻意不改主色、不给数字**（调研报告 P0-1 的建议）：
      声调偏了 ≠ 这个字不认识。整格变红会让孩子以为「我完全不认识这个字」，
      打击面远大于实际。一个小点够提醒，家长端另有声调统计。
   橙点也与「要重读」的红、与「多读/漏读」的波浪线都区分得开。 */
.word-main .zi[data-tone="bad"] b { position: relative; }
.word-main .zi[data-tone="bad"] b::after {
  content: "";
  position: absolute; left: 50%; bottom: -5px;
  width: 5px; height: 5px; margin-left: -2.5px;
  border-radius: 50%; background: #D98324;
}
/* ---------- 「先自己读」引导（2026-09-22）----------
   置灰的「听一听」要明确表达「现在还不能听，但读完就能听」，而不是「坏了」：
   保留一点底色 + 虚线边，比单纯 opacity 更容易被孩子读成「待解锁」。 */
.btn-mid.btn-locked {
  opacity: .5;
  background: transparent;
  box-shadow: none;
  border: 2px dashed rgba(232, 131, 58, .5);
  color: var(--primary-deep);
}
/* 卡片上的引导语（位置原本是分数卡，读完就被分数替换，不需要手动清） */
.word-result .read-first {
  font-size: 19px; font-weight: 500; color: var(--primary-deep);
  text-align: center; line-height: 1.6; padding: 6px 0;
}
.word-main .zi b { border-radius: 8px; transition: color .18s, background-color .18s, box-shadow .18s; }
/* padding/margin 互相抵消，着色不改变字宽，句子不会因为上色而重排 */
.word-main .zi[data-sc] b { padding: 0 3px; margin: 0 -3px; }
.word-main .zi[data-sc="great"] b { color: #33803A; background: rgba(111, 191, 99, .18); }
.word-main .zi[data-sc="ok"]    b { color: #9C6B12; background: rgba(245, 185, 64, .22); }
.word-main .zi[data-sc="bad"]   b { color: #B8402F; background: rgba(224, 106, 90, .20); }
.word-main .zi[data-sc="miss"]  b {
  color: #B8402F; background: rgba(224, 106, 90, .20);
  text-decoration: underline wavy #B8402F; text-underline-offset: 8px;
}
/* 边读边亮：SOE 流式中间结果先把「已读到的字」圈出来，读完再由上面的评价色接管 */
.word-main .zi[data-live="1"] b { box-shadow: 0 0 0 4px rgba(91, 155, 213, .28); border-radius: 12px; }

/* 结果区颜色图例：只在有逐字批注时出现，帮孩子/家长看懂颜色 */
.word-result-legend {
  display: flex; gap: 12px; flex-wrap: wrap; justify-content: center;
  font-size: 12px; color: var(--ink-soft); line-height: 1.6;
}
.word-result-legend span { display: inline-flex; align-items: center; gap: 4px; }
.word-result-legend i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
.word-result-legend i[data-sc="great"] { background: var(--green); }
.word-result-legend i[data-sc="ok"]    { background: var(--gold); }
.word-result-legend i[data-sc="bad"]   { background: var(--red); }
.word-result-legend i[data-sc="miss"]  { background: var(--red); border-radius: 50%; }
.word-result-legend .hint { color: var(--blue); }
/* 🔴 2026-09-18 第六轮（用户报「句子卡里『第 x 页 · 第 x 句』怎么离下边框那么远」）：
   两个原因叠加 ——
     ① `.word-result` 空着的时候也占着 min-height:64px（那是给分数预留的），
        它排在落款下面，等于在落款和下边框之间凭空塞了 64px；
     ② 卡片被 `.word-col > .word-card { flex:1 0 auto }` 撑满整列后内容整体居中，
        短句书的句子只有几十像素高，剩下的高度全摊成上下留白
        （实测落款离下边框：L1 mouse-lamp 152px、L6 鸿门宴 86px）。
   处理：空结果区不再占位；落款改成**钉在卡片下沿的落款**（绝对定位），
   它到下边框的距离从此与卡片高度无关，永远是下面那 18px（竖屏 14px）。
   卡片的下内边距同步留出落款的位置。 */
.word-example {
  position: absolute; left: 0; right: 0; bottom: 18px;
  text-align: center;
  font-size: clamp(15px, 1.5vw, 20px); color: var(--ink-soft);
}
.word-example:empty { display: none; }
.word-result { display: flex; flex-direction: column; align-items: center; gap: 8px; min-height: 64px; }
/* 没出分之前结果区是空气，别留高度 —— 留着的 64px 只会把落款顶得老高 */
.word-result:empty { min-height: 0; }
/* 评分卡片三段式：大分数字 + 鼓励语 + 评分源标签（云端/本地） */
.word-result-score { display: flex; align-items: baseline; gap: 4px; line-height: 1; }
.word-result-score .score-num { font-size: 44px; font-weight: 800; color: var(--primary-deep); }
.word-result-score .score-unit { font-size: 16px; color: var(--ink-soft); }
.word-result-msg { font-size: 17px; color: var(--ink-soft); text-align: center; }
.word-result-src {
  font-size: 12px; padding: 2px 10px; border-radius: 10px;
  background: #f5efe6; color: #8a6a45; letter-spacing: .5px;
}
.word-stars { font-size: 40px; letter-spacing: 6px; }
.word-stars .off { opacity: .25; filter: grayscale(1); }
.word-score-msg { font-size: 20px; color: var(--primary-deep); font-weight: 500; }
.pop { animation: pop .4s ease; }
@keyframes pop { 0% { transform: scale(.5); opacity: 0; } 70% { transform: scale(1.15); } 100% { transform: scale(1); } }
.words-footer {
  display: flex; align-items: center; justify-content: center; gap: 5vw;
  padding: 14px 24px calc(16px + env(safe-area-inset-bottom));
}
.btn-mic {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  width: 110px; height: 110px; border-radius: 50%;
  background: var(--primary); color: #fff; box-shadow: 0 6px 0 var(--primary-deep);
  gap: 2px;
}
.btn-mic .mic-icon { font-size: 34px; }
.btn-mic .mic-label { font-size: 17px; font-weight: 600; }
.btn-mic.rec { background: var(--red); box-shadow: 0 6px 0 #B84B3C; animation: pulse 1s infinite; }
@keyframes pulse { 50% { transform: scale(1.06); } }
.btn-mic:active { transform: translateY(4px); box-shadow: 0 2px 0 var(--primary-deep); }

/* ---- 练习 ---- */
.quiz-progress { font-size: 18px; color: var(--ink-soft); margin-left: auto; font-weight: 500; }
.quiz-body {
  flex: 1; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 4.5vh; padding: 0 8vw 6vh;
  /* ⚠️ 这两行是安全网（2026-09-23 选项带拼音之后加）：.view 是定高 flex、
     body 又是 overflow:hidden，内容一超高就被 justify-content:center 从两头裁掉。
     长题干 + 长选项（「小老鼠求它，它觉得好笑」那种）在窄屏上正好会超。
     min-height:0 是让 flex 子项真的能滚（少它 overflow 不生效）—— 同正文页的滚动铁律。 */
  overflow-y: auto; min-height: 0;
}
.quiz-q { font-size: clamp(24px, 2.6vw, 34px); font-weight: 500; text-align: center; line-height: 1.7; }
.btn-listen {
  font-size: 22px; padding: 16px 44px; border-radius: 50px;
  background: var(--blue); color: #fff; box-shadow: 0 5px 0 #4781B4; font-weight: 500;
}
.quiz-options { display: flex; gap: 3vw; flex-wrap: wrap; justify-content: center; }
.opt-btn {
  min-width: 26vw; max-width: 300px; padding: 4vh 2vw;
  font-size: clamp(26px, 2.8vw, 36px); font-weight: 600;
  background: var(--card); border-radius: 22px; box-shadow: var(--shadow);
  color: var(--ink); border: 4px solid transparent; transition: transform .1s;
}
.opt-btn:active { transform: scale(.97); }
.opt-btn.picked { border-color: var(--blue); }
.opt-btn.right { background: #EAF6E4; border-color: var(--green); color: #3E7D2C; }
.opt-btn.wrong { background: #FBEAE7; border-color: var(--red); color: var(--red); animation: shake .4s; }
/* ---- 选项带拼音（2026-09-23）----
   用户提：「有些练习题里面，选项没有拼音，会影响小朋友选出正确答案，因为不认识选项写的是什么。」
   确实如此：L1 是 3–6 岁，题干还有朗读兜底，选项此前什么都没有。
   结构复用正文的 .zi（拼音在上、汉字在下、逐字对齐），拼音**常显**、不参与撤拼音抽检（题面≠阅读材料）。
   ⚠️ 字号取 .42em 而不是正文的 --py(.52em)：选项本身字号大（clamp 26–36px），
      .52em 会把按钮顶得过高，窄屏上三个选项就装不下了。 */
.opt-btn .zi { margin: 0 1px; }
.opt-btn .zi i { font-size: max(10px, .42em); font-weight: 500; }
/* 带拼音的选项多了一条拼音带 —— 靠收 padding + 略降字号把高度换回来 */
.opt-btn.has-py { padding: 2.6vh 1.5vw; font-size: clamp(21px, 2.4vw, 31px); }
/* 长选项（≥8 字，由 JS 加 .opt-long）**独占一行**：不然它在窄屏上折行，
   末尾会剩一两个字单挂一行（实测「小老鼠求它，它觉得好笑」11 字就差一个字）。
   独占一行后宽度从 70vw 放到接近满宽，11 字也放得下。 */
.opt-btn.opt-long { flex-basis: 100%; max-width: none; }
/* 判对/判错时拼音跟着按钮变色，别让它留在灰字上、显得这块没反馈 */
.opt-btn.right .zi i, .opt-btn.wrong .zi i { color: currentColor; opacity: .7; }
@keyframes shake { 20%, 60% { transform: translateX(-8px); } 40%, 80% { transform: translateX(8px); } }
.quiz-feedback { font-size: clamp(22px, 2.2vw, 28px); font-weight: 600; min-height: 1.6em; text-align: center; }
.quiz-feedback.good { color: #3E7D2C; }
.quiz-feedback.bad { color: var(--red); }

/* ---- 完成页 ---- */
.done-wrap {
  flex: 1; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 2.6vh; padding: 4vh 8vw;
}
.done-stars { font-size: 64px; letter-spacing: 10px; animation: pop .6s ease; }
.done-title { font-size: 34px; font-weight: 600; }
.done-sub { font-size: 20px; color: var(--ink-soft); }
.done-record {
  background: var(--card); border-radius: 18px; box-shadow: var(--shadow);
  padding: 14px 26px; font-size: 16px; color: var(--ink-soft); max-width: 560px;
  max-height: 24vh; overflow-y: auto;
}
.done-record div { padding: 4px 0; }

/* ---- 弹卡 ---- */
.modal {
  position: fixed; inset: 0; background: rgba(60, 40, 15, .35);
  display: flex; align-items: center; justify-content: center; z-index: 50;
}
.modal-card {
  background: var(--card); border-radius: 28px; box-shadow: var(--shadow);
  padding: 34px 40px; display: flex; flex-direction: column; align-items: center;
  gap: 14px; max-width: 86vw; max-height: 80vh; animation: pop .3s ease;
}
.modal-wide { width: min(640px, 90vw); }
.mw-pinyin { font-size: 20px; color: var(--blue); letter-spacing: 4px; }
.mw-text { font-size: 38px; font-weight: 600; letter-spacing: 3px; }
.mw-explain { font-size: 20px; color: var(--ink-soft); text-align: center; line-height: 1.7; }
.box-list { width: 100%; max-height: 46vh; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; }
.box-item {
  display: flex; align-items: center; gap: 14px; background: var(--bg);
  border-radius: 14px; padding: 12px 16px;
}
.box-word { font-size: 20px; font-weight: 600; flex: 1; }
.box-time { font-size: 13px; color: var(--ink-soft); }
.box-stars { font-size: 17px; }
.box-play {
  width: 44px; height: 44px; border-radius: 50%; background: var(--primary);
  color: #fff; font-size: 17px;
}
.box-empty { text-align: center; color: var(--ink-soft); padding: 26px 0; font-size: 17px; }
/* 录音箱：同一句读了很多次 → 按句分组，组内一行一次 */
.box-group { display: flex; flex-direction: column; gap: 6px; }
.box-group-hd {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding: 0 4px 2px;
}
.box-group-word {
  font-size: 18px; font-weight: 600; flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.box-group-meta { flex: none; font-size: 13px; color: var(--ink-soft); }
.box-take {
  flex: 1; font-size: 15px; font-weight: 600; color: var(--primary-deep);
}
.box-play:disabled { background: #E3D8C4; cursor: default; }

/* ---- toast ---- */
.toast {
  position: fixed; left: 50%; bottom: 12vh; transform: translateX(-50%);
  background: rgba(60,40,15,.85); color: #fff; font-size: 18px;
  padding: 14px 30px; border-radius: 40px; z-index: 60; animation: fadein .2s;
}

/* ---- 又宽又矮的窗口（笔记本上把浏览器窗口压扁时很常见）----
   默认左右分栏在矮窗口里会把文字框挤成一条，句子被迫折行、高矮不一。
   这里把插图区收窄、文字区加宽，让一句能排在一行里：句子高度自然一致，
   也不用把字号缩到看不清。 */
@media (max-height: 620px) and (min-aspect-ratio: 1/1) {
  .story-body { gap: 14px; padding: 0 16px; }

  .story-text-panel { flex: 2.4; padding: 2.4vh 1.6vw; }
  .story-footer { padding: 6px 24px calc(6px + env(safe-area-inset-bottom)); }
  .story-foot-mid { gap: 3px; }
  .story-page-no { font-size: 13px; }
  .btn-nav { padding: 8px 14px; }
  .story-emoji { min-height: 0; }
}

/* ---- 竖屏适配 ---- */
@media (max-aspect-ratio: 1/1) {
  /* 竖屏（手机）：书卡和步骤行都收窄边距、降一档字号。
     390px 下左右各 16px，正文仍有 350px 左右，够读。
     ⚠️ 这里不再给 .home-modules 写 overflow —— 滚动统一交给 .home-scroll，
        两处都写会出现「内层滚完外层还能滚」的双层滚动条。 */
  /* 高度是量过的：844px 视口下要一屏看全，这几处共省约 40px
     （实测改前先到 874px，第三张卡底边被切掉 30px）。别随手加回来。
     ⚠️ 2026-09-18 第四轮修正：这段「为了塞进一屏」的压缩**压过头了**。
     实测 390×844 下书卡 + 三步卡只占 619px，下面空出 143px（无引言的书
     空出 313px）—— 于是用户看到的是「卡片里三行挤在一起，下面一大片空」。
     现在：① 卡片内边距/字号回涨一档（仍有富余，不会溢出）；
     ② `.book-hero` 给一个保底高度并在内部垂直居中 —— 没写 intro 的书
        （83 本里还有 80 本）书卡本来只有 124px，上方一块小卡 + 下方三张卡
        中间会拉开一大片空；保底 168px 把它压到合理范围；
     ③ `.home-modules` 改成**弹性填满剩余高度**（flex:1 0 auto），卡片
        `flex:1 0 auto` 跟着长高、并用 `max-height:144px` 限高，
        多出来的空间用 `justify-content:flex-end` 推到书卡与功能卡之间。
        效果：引言长的书（花木兰）卡片自然长到 ~116px、零空白；
        没引言的书卡片长到 144px 封顶，空白也只有 ~108px 而不是 313px。
     ⚠️ 卡片 `flex-shrink` 必须是 0：矮屏（如 667px）上若有 shrink，
        三张卡会被压扁而不是让 .home-scroll 去滚。 */
  .home-scroll { padding-bottom: 16px; }
  .book-hero {
    margin: 4px 16px 0; padding: 20px 18px; border-radius: 22px; gap: 16px;
    min-height: 168px; justify-content: center;
  }
  .hero-head { gap: 14px; }
  .hero-cover { width: 84px; height: 84px; font-size: 44px; border-radius: 20px; }
  .hero-title { font-size: 23px; }
  .hero-tag { font-size: 13px; padding: 4px 10px; }
  .intro-block {
    font-size: 16px; line-height: 1.62;
    padding: 11px 13px; border-radius: 15px; gap: 10px;
  }
  .intro-block::before {
    width: 32px; height: 32px; margin-top: -1px;
    padding-left: 3px; font-size: 12px;
  }
  .intro-block.playing::before { padding-left: 0; font-size: 10px; }
  .home-modules {
    margin: 14px 16px 0; padding-top: 20px; gap: 13px;
    flex: 1 0 auto; justify-content: flex-end;
  }
  .mod-card {
    padding: 16px 16px; gap: 13px; border-radius: 20px;
    flex: 1 0 auto; max-height: 144px;
  }
  .mod-no { width: 42px; height: 42px; font-size: 19px; }
  .mod-title { font-size: 21px; }
  .mod-go { width: 36px; height: 36px; font-size: 13px; }
  .mod-card.locked .mod-go { font-size: 15px; }
  .story-body { flex-direction: column; }

  .story-text-panel { flex: 1; padding: 12px 18px; gap: 10px; }
  /* ⚠️ 这里原来有一行 `.story-text { gap: 12px }`，已删（2026-09-18 第五轮）：
     它会让竖屏的间距（12px）和平板的 `2.4vh`/`1.2vh` 变成两套值，
     跨设备、跨书都对不上。间距现在统一由 .story-text 的 --sgap 派生。 */
  .tabs .tab { padding: 8px 14px; font-size: 15px; }
  .opt-btn { min-width: 70vw; }
  .mod-tab { padding: 8px 12px; font-size: 14px; }
  .jump-btn { width: 34px; height: 34px; font-size: 15px; }
  /* 书架：竖屏两列，顶栏改为上下两行 */
  .shelf-top { flex-direction: column; align-items: flex-start; gap: 10px; }
  .shelf-hi { font-size: 24px; }
  .mini-stat { padding: 6px 12px; }
  .mini-stat b { font-size: 18px; }
  .book-card { width: min(44vw, 300px); }
  .continue-card { padding: 3px 10px 3px 3px; gap: 5px; }
  .continue-cover { width: 22px; height: 22px; font-size: 12px; }
  .continue-title { font-size: 12px; max-width: 24vw; }
  /* 书架难度 tab：窄屏收字号、缩内边距，避免胶囊挤到换行 */
  .level-tabs { gap: 6px; }
  .level-tab { font-size: 14px; padding: 8px 13px; }
  .level-tab .l-badge { font-size: 11px; padding: 2px 6px; margin-left: 4px; }
}

/* ---------- 读句子页 · 手机竖屏（2026-09-16 修）----------
   实测 390×844：`.words-body` 只剩 525px，而 `.word-col`（句子卡 min-height:34vh
   + 一排 60px 的句子跳转格子，18 句要折 6 行）量出来 704px。
   溢出的部分被 `body{overflow-x:hidden}` 之外的 `overflow:hidden` 直接裁掉，
   表现就是用户报的「卡片盖住顶栏下半 + 跳转格子底下几行点不到」。
   处理两条：
     ① 整列改成**可滚动**（宁可滑一下，也不能被裁掉点不到）；
     ② 箭头和跳转格子**收一号**，让常见的十几句仍然能一屏看完。
   注意：`.word-jump .jump-btn` 有 (0,2,0) 的特异性，上面那条 `.jump-btn { width:34px }`
   压不住它 —— 所以下面必须写成 `.word-jump .jump-btn` 才生效。 */
@media (max-aspect-ratio: 1/1) {
  .words-body { align-items: stretch; gap: 8px; padding: 0 10px; }
  .word-col { overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; padding: 2px 0 8px; }
  .word-col > * { flex: none; }
  /* 🔴 2026-09-18 第五轮 + 第六轮的结论合起来看：
     第五轮现象（用户报的「下面留了那么多空白」）：.word-col 被 .words-body 的
     align-items:stretch 拉到 525px，而内容是「卡片 + 跳转格子」自然高，
     短句书只有 ~340px → 底部凭空空出 140~190px（实测 mouse-lamp 140、lion-mouse 186）。
     当时让卡片 flex-grow 把这块空间全吃掉 —— 结果空间从「跳转格子下面」搬到了
     「卡片里面」，第六轮用户立刻报了另一头：「第 x 页 · 第 x 句 怎么离下边框那么远」。
     现在改成**两头都不留死空**：卡片只按内容高（min 34vh，长句撑高时不压缩，
     flex-shrink:0），余量交给 auto 外边距在「卡片上方」和「跳转格子下方」平分
     （卡片 margin-top:auto + .word-jump margin-bottom:auto）。
     这样既没有 140px 的死空白，卡片内部也不再有虚高。
     ⚠️ 必须写 `.word-col > .word-card`：上面那条 `.word-col > *` 与本选择器同特异性，
     靠书写顺序取胜太脆，索性把选择器写实。 */
  .word-col > .word-card { flex: 0 0 auto; margin-top: auto; }
  .word-card { min-height: 34vh; max-height: none; padding: 14px 12px 46px; gap: 1vh; }
  .word-example { bottom: 14px; }
  .word-main { letter-spacing: 4px; }
  .word-main.pun { font-size: clamp(22px, 6.2vw, 34px); line-height: 1.35; }
  .word-result-score .score-num { font-size: 34px; }
  .words-body .btn-nav { width: 46px; height: 46px; font-size: 18px; align-self: center; }
  .words-body .btn-nav.wide { width: auto; min-width: 46px; height: 46px; padding: 0 10px; font-size: 13px; border-radius: 23px; }
  .word-jump { gap: 6px; margin-top: 10px; }
  .word-jump .jump-btn { width: 46px; height: 46px; grid-template-rows: 13px 1fr; }
  .word-jump .jump-btn b.no { font-size: 10px; margin: 4px 0 0 5px; }
  .word-jump .jump-btn em.sc { font-size: 17px; }
  .word-jump .jump-btn.unread em.sc { font-size: 15px; }
}

/* ---------- 长书的两级句子跳转（2026-09-16 起，2026-09-18 第六轮重做）----------
   L5/L6 单本 37–45 句，平铺一排几十个格子既找不到「我读到哪儿」，手机上还会折十几行。
   超过 12 句时改成两级：第一级选「页」，第二级选这一页里的句子。

   🔴 上一版的毛病（用户 2026-09-18 报「看了好多次都没意识到是页码和句子顺序，
   离得太远，关系也不明显」）：两级**长得一模一样** —— 同尺寸方块、都只写数字、
   都没有文字说明，连高亮色都一样，于是屏幕上就是两个同样的橙块（「页 1」和「句 1」）。
   现在靠三件事区分：
     ① 每级行首一个文字标签「页」/「句」（.jump-tag，不可点）；
     ② 形状不同：页 = 矮胶囊（32px 高、只放数字），句 = 方块（序号 + 得分两行）；
     ③ 页胶囊收窄，12 页从三行降到两行，两级不再隔着大段空白。
   标签列宽固定，两级的格子左边缘对齐 —— 一眼看出是同一个选择器的上下两级。 */
.word-jump-group { display: flex; flex-direction: column; gap: 8px; align-items: stretch; width: 100%; }
.jump-seg { display: flex; align-items: flex-start; gap: 6px; width: 100%; }
/* 两级之间只用一条虚线分开，标签已经说明层级，不再需要别的分隔物 */
.jump-seg + .jump-seg { padding-top: 8px; border-top: 1px dashed #E3D9C6; }
/* 行首标签：只说「这一行选的是页还是句」，不可点。
   高度对齐**本行按钮**（页 32px / 句 46px 竖屏、60px 桌面）——
   试过 align-self:stretch 让它铺满整行：多行网格下会变成一条长竖条
   （L1 的句子网格铺满时高 150px），像渲染残留，反而更乱。
   固定成「一个格子的高度」时它读起来就是个贴在行首的分类签。 */
.jump-tag {
  flex: none; width: 22px; height: 32px;
  display: flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 600; color: #A8977A;
  background: #F4EDDF; border-radius: 9px;
}
.jump-seg-sent .jump-tag { height: 60px; }
.jump-grid { flex: 1; min-width: 0; display: flex; gap: 5px; flex-wrap: wrap; align-content: flex-start; }
/* 页号胶囊：只有数字、没有分数，刻意做矮做圆，和下面的「句子方块」不同形。
   34px 是算出来的：标签占掉 22+6 之后还剩 234px，6 个 34px 胶囊 + 5 个 5px 间距
   = 229px ≤ 234 —— 12 页正好落在两排（32px 只能放 6 个，也够，但没有余量）。 */
.word-jump .jump-btn.page-btn {
  display: flex; align-items: center; justify-content: center;
  width: auto; min-width: 34px; height: 32px; padding: 0 8px;
  border-radius: 16px;
  background: #F0E7D6; color: var(--ink-soft); font-size: 14px; font-weight: 600;
}
.word-jump .jump-btn.page-btn.cur { background: var(--primary); color: #fff; }
/* 竖屏下句子格子收成 46px，行首的「句」标签跟着一起收（写在这里才压得住上面那条 60px） */
@media (max-aspect-ratio: 1/1) {
  .jump-seg-sent .jump-tag { height: 46px; }
}

/* ============ 账号：登录 / 注册 ============ */
.auth-wrap {
  flex: 1; display: flex; justify-content: center; align-items: flex-start;
  padding: calc(4vh + env(safe-area-inset-top)) 5vw calc(4vh + env(safe-area-inset-bottom));
  overflow-y: auto;
}
.auth-card {
  /* margin:auto 上下 —— 内容矮时垂直居中，内容高时从顶部开始（不会被裁掉脑袋） */
  margin: auto 0;
  width: min(460px, 100%); background: var(--card); border-radius: 28px;
  box-shadow: var(--shadow); padding: 34px 30px 26px; text-align: center;
}
.auth-logo { font-size: 56px; line-height: 1; }
.auth-title { margin-top: 10px; font-size: 26px; font-weight: 600; letter-spacing: 1px; }
.auth-sub { margin-top: 6px; font-size: 15px; color: var(--ink-soft); }

.auth-tabs {
  display: flex; gap: 4px; margin: 22px 0 18px; padding: 4px;
  background: #F1E7D8; border-radius: 16px;
}
.auth-tab {
  flex: 1; padding: 11px 0; border-radius: 13px; font-size: 16px; color: var(--ink-soft);
  transition: background .18s, color .18s;
}
.auth-tab.active {
  background: linear-gradient(135deg, var(--primary), var(--primary-deep));
  color: #fff; font-weight: 600; box-shadow: 0 4px 12px rgba(232, 131, 58, .3);
}

.auth-field { text-align: left; margin-bottom: 14px; }
.auth-field label { display: block; margin: 0 0 6px 4px; font-size: 14px; color: var(--ink-soft); }
.auth-field input {
  width: 100%; padding: 14px 16px; font-size: 16px;   /* 16px 是 iOS 不自动放大的下限 */
  color: var(--ink); background: #FBF7F0; border: 2px solid #EFE4D4; border-radius: 14px;
  outline: none; font-family: inherit; transition: border-color .18s, background .18s;
  -webkit-user-select: text; user-select: text;
}
.auth-field input:focus { border-color: var(--primary); background: #fff; }
.auth-field input::placeholder { color: #C4B7A4; }

.auth-agree {
  display: flex; align-items: flex-start; gap: 10px; text-align: left;
  margin: 4px 0 16px; font-size: 13.5px; line-height: 1.5; color: var(--ink-soft);
}
.auth-agree input { width: 20px; height: 20px; flex: none; margin-top: 1px; accent-color: var(--primary); }

.auth-err {
  margin: 0 0 12px; padding: 10px 14px; border-radius: 12px;
  background: #FDECE9; color: #B4372A; font-size: 14px; text-align: left;
}

.btn.btn-primary {
  background: linear-gradient(135deg, var(--primary), var(--primary-deep));
  color: #fff; font-weight: 600; box-shadow: 0 6px 16px rgba(232, 131, 58, .32);
}
.btn.btn-primary:disabled { opacity: .6; box-shadow: none; }
.auth-submit { width: 100%; padding: 15px 0; font-size: 17px; border-radius: 16px; }
.auth-offline { margin-top: 14px; }
.btn-link { font-size: 14px; color: var(--ink-soft); text-decoration: underline; padding: 6px; }

/* ---- 选孩子 ---- */
.child-list { display: flex; flex-direction: column; gap: 10px; margin: 4px 0 14px; }
.child-card {
  display: flex; align-items: center; gap: 14px; text-align: left;
  padding: 14px 16px; background: #FBF7F0; border: 2px solid #EFE4D4; border-radius: 18px;
  transition: transform .16s, border-color .16s;
}
.child-card:active { transform: scale(.985); border-color: var(--primary); }
.cc-emoji { font-size: 34px; flex: none; }
.cc-body { flex: 1; min-width: 0; }
.cc-name { display: block; font-size: 18px; font-weight: 600; }
.cc-sub { display: block; margin-top: 3px; font-size: 13px; color: var(--ink-soft); }
.cc-go { flex: none; font-size: 14px; color: var(--primary-deep); font-weight: 600; }
.child-empty { padding: 18px; font-size: 15px; color: var(--ink-soft); }
.child-add-btn { width: 100%; padding: 13px 0; border-radius: 16px; background: #F1E7D8; font-size: 16px; }
.child-add-form { margin-top: 14px; display: flex; flex-direction: column; gap: 12px; }
.child-add-form input {
  width: 100%; padding: 14px 16px; font-size: 16px; color: var(--ink); font-family: inherit;
  background: #FBF7F0; border: 2px solid #EFE4D4; border-radius: 14px; outline: none;
}
.emoji-pick { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.emoji-cell {
  width: 46px; height: 46px; font-size: 24px; border-radius: 14px;
  background: #FBF7F0; border: 2px solid #EFE4D4; transition: transform .15s, border-color .15s, background .15s;
}
.emoji-cell.on { border-color: var(--primary); background: #FFF1E2; transform: scale(1.06); }
.child-logout { margin-top: 18px; }

/* ⚠️ 注意：本项目没有 `.btn` 基础类（只有 .btn-big/.btn-primary 这些变体），
   所以凡是 `class="btn btn-primary"` 的元素必须自带 padding/圆角/字号，
   否则会被浏览器默认样式压成一条细橙条（2026-09-10 选孩子页踩到） */
.child-add-form .btn-primary {
  padding: 14px 0; border-radius: 16px; font-size: 16px;
  background: linear-gradient(135deg, var(--primary), var(--primary-deep));
  color: #fff; font-weight: 600; box-shadow: 0 6px 16px rgba(232, 131, 58, .32);
}
.child-add-form .btn-primary:active { transform: translateY(2px); box-shadow: 0 2px 8px rgba(232, 131, 58, .3); }
.child-add-form .btn-primary:disabled { opacity: .6; box-shadow: none; }

/* ---- 顶栏账号入口 + 下拉菜单 ---- */
.account-btn {
  /* 从「圆头像」改成「头像 + 名字」胶囊：家里两个孩子时一眼看得出在读谁的进度。
     宽度自适应名字长度，名字过长时省略号收尾，不会把统计条挤下去。 */
  flex: none; display: inline-flex; align-items: center; gap: 6px;
  min-height: 44px; max-width: 34vw; margin-left: 10px; padding: 0 14px;
  border-radius: 999px; background: var(--card); box-shadow: var(--shadow);
  opacity: .72; transition: opacity .18s;
}
.account-btn .ab-emoji { font-size: 22px; line-height: 1; flex: none; }
.account-btn .ab-name {
  font-size: 16px; font-weight: 600; color: var(--primary-deep);
  max-width: 22vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.account-btn:active, .account-btn:hover { opacity: 1; }
.account-menu {
  position: absolute; right: 5vw; top: calc(3vh + env(safe-area-inset-top) + 56px);
  z-index: 50; min-width: 190px; padding: 8px;
  background: var(--card); border-radius: 18px; box-shadow: 0 10px 30px rgba(120, 85, 40, .2);
  animation: fadein .16s ease;
}
.am-head { padding: 10px 12px 8px; font-size: 15px; font-weight: 600; border-bottom: 1px solid #F1E7D8; }
.am-item { display: block; width: 100%; text-align: left; padding: 12px; border-radius: 12px; font-size: 15px; }
.am-item:active { background: #FBF7F0; }

/* 竖屏：卡片收紧、账号菜单改从底部弹出（手指更够得着） */
@media (orientation: portrait) {
  .auth-card { padding: 28px 22px 22px; border-radius: 24px; }
  .auth-logo { font-size: 48px; }
  .account-menu { top: auto; bottom: calc(4vh + env(safe-area-inset-bottom)); right: 6vw; }
  /* 竖屏（含手机）：三块统计条整体收一号，保证「🔥打卡 0天」能一行放下 */
  .shelf-stats { gap: 8px; }
  .mini-stat { padding: 7px 11px; }
  .mini-stat .mi { font-size: 15px; }
  .mini-stat b { font-size: 20px; }
  .mini-stat .mk, .mini-stat .ml { font-size: 12px; }
  /* 竖屏/手机：账号胶囊整体收一号，别把「🔥打卡」挤到第二行 */
  .account-btn { padding: 0 11px; gap: 5px; max-width: 40vw; }
  .account-btn .ab-emoji { font-size: 19px; }
  .account-btn .ab-name { font-size: 14px; max-width: 26vw; }
  /* 书卡在竖屏下只有 44vw 宽（375px 屏约 165px），左上「⭐⭐⭐挑战」要收一号 */
  .book-lv { font-size: 11px; padding: 2px 7px; letter-spacing: 0; }
}

/* ---- 诊断面板 / 轻提示（js/diag.js）----
   出问题时家长点「诊断信息」就能把浏览器侧的日志复制出来。
   平时不出现，不打扰孩子阅读。 */
#diag-panel {
  position: fixed; inset: auto 0 0 0; max-height: 62vh; z-index: 9999;
  display: flex; flex-direction: column;
  background: var(--card); color: var(--ink);
  border-radius: 18px 18px 0 0; box-shadow: 0 -8px 30px rgba(120, 85, 40, .22);
  -webkit-user-select: text; user-select: text;
}
.diag-hd {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 16px 10px; font-size: 16px; border-bottom: 1px solid #F1E7D8;
}
.diag-x { color: var(--ink-soft); font-size: 18px; padding: 0 6px; cursor: pointer; }
.diag-body {
  flex: 1; overflow: auto; margin: 0; padding: 12px 16px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11.5px; line-height: 1.7; white-space: pre-wrap; word-break: break-all;
  color: var(--ink); background: #FFFDF9;
}
.diag-ft { display: flex; gap: 8px; padding: 10px 16px calc(14px + env(safe-area-inset-bottom)); border-top: 1px solid #F1E7D8; }
.diag-btn {
  flex: 1; padding: 11px 0; border-radius: 14px; font-size: 14px;
  background: #F4EDE2; color: var(--ink);
}
.diag-btn.primary { background: var(--primary); color: #fff; font-weight: 600; }
.diag-btn:active { opacity: .82; }

#diag-toast {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(5vh + env(safe-area-inset-bottom)); z-index: 9998;
  display: flex; align-items: center; gap: 10px;
  max-width: 92vw; padding: 11px 14px; border-radius: 16px;
  background: rgba(74, 59, 42, .94); color: #FBF5EA; font-size: 13.5px;
  box-shadow: 0 6px 22px rgba(0, 0, 0, .25);
}
#diag-toast button {
  flex: none; padding: 6px 10px; border-radius: 10px;
  background: var(--gold); color: #4A3B2A; font-size: 12.5px; font-weight: 600;
}

/* ============================================================
 * 账号安全相关：注销（2026-09-10）
 * 沿用现有配色（--primary 暖橙、--red 警示），不引入新色系
 * 📌 2026-09-11：找回码 / 导出学习记录 入口已下线，对应的
 *    .auth-hint / .auth-forgot / .rc-code 样式一并删掉了
 * ============================================================ */

/* ---- 账号菜单：注销那一条用警示色，跟普通项区分开 ---- */
.am-item.am-danger { color: var(--red); }
.am-item.am-danger:active { background: #FDECE9; }

/* ---- 弹窗里的按钮排一行 ---- */
.modal-row { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-top: 4px; }
.btn-danger {
  background: linear-gradient(135deg, var(--red), #C9503F);
  color: #fff; font-weight: 600;
}
.btn-danger:disabled { opacity: .45; }

.dg-tip { font-size: 14px; color: var(--ink-soft); }
.modal-card .auth-field input { font-size: 16px; }

/* 注销弹窗内容多，小屏要能滚（原有弹卡没设 overflow，长内容会被截掉） */
#modal-danger .modal-card { overflow-y: auto; }

/* 弹窗里的按钮成对出现，把「主操作」和「取消」拉开层次：
   危险操作弹窗里，取消用中性灰（.btn-ghost），破坏性动作用红色（.btn-danger），
   免得橙色主按钮把注意力从真正的后果上引开。 */
.btn-mid.btn-primary {
  background: linear-gradient(135deg, var(--primary), var(--primary-deep));
  color: #fff; font-weight: 600; box-shadow: 0 6px 16px rgba(232, 131, 58, .32);
}
.btn-ghost {
  font-size: 20px; padding: 16px 30px; border-radius: 40px;
  background: #FBF7F0; color: var(--ink-soft); border: 2px solid #EFE4D4; font-weight: 500;
}

/* ===== 意见反馈 / 联系我们（2026-09-27） =====
   与注销弹窗共用 .modal/.modal-card/.modal-row，只补两份专属样式。 */
.fb-text {
  width: 100%; box-sizing: border-box;
  border: 1px solid #e3e3e6; border-radius: 12px;
  padding: 12px 14px; margin: 4px 0 0;
  font-size: 16px; font-family: inherit; line-height: 1.6;
  resize: vertical; min-height: 110px; color: var(--ink, #333);
}
.fb-text:focus { outline: none; border-color: var(--primary, #e8833a); }
.ct-wx {
  display: flex; align-items: center; gap: 10px;
  background: #faf7f2; border: 1px solid #efe6d9; border-radius: 12px;
  padding: 12px 14px; margin: 6px 0 0; width: 100%; box-sizing: border-box;
}
.ct-wx-k { font-size: 13px; color: #9a8f80; }
/* user-select:all —— 长按整串选中，复制按钮失灵时手也能选 */
.ct-wx-v { flex: 1; font-size: 20px; font-weight: 700; letter-spacing: .5px; color: #5a4a35; user-select: all; }
.ct-copy {
  border: none; background: var(--primary, #e8833a); color: #fff;
  border-radius: 999px; padding: 8px 16px; font-size: 14px; cursor: pointer;
  white-space: nowrap;
}
.ct-copy:active { transform: translateY(1px); }

/* ============================================================
 * 手机端适配（2026-09-29 全面重做）
 * ------------------------------------------------------------
 * 起因：用户「给手机显示做适配」。
 * 做法：先量、再改。`tools/e2e/mobile-probe.mjs` 在 5 个真实手机视口
 *   （320×568 / 360×800 / 390×844 / 414×896 / 430×932）把五个屏全部量了一遍：
 *   横向溢出 / 内层滚动高度 / 可点区尺寸 / 关键字号。
 *   下面每一条都对应一个**量出来的数字**，没有一条是凭手感调的。
 *
 * 量出来的四个硬问题（改前）：
 *
 *  ① 顶栏膨胀成三行。@390×844 听故事页顶栏 189px、读句子页 229px
 *     （占屏高 22% / 27%）。组成是「返回 + 书名 + 动作胶囊 + 模块 tab」竖着摞。
 *     对照：@430×932 同样四个元素只占一行 139px —— **只差 40px 宽就多出两行**，
 *     说明它不是「手机放不下」，是尺寸没跟着屏宽降档。
 *  ② 听故事页正文必须内滚。正文面板只有 237px，而内容要 350px
 *     （`.story-text-panel 视237/内350`），首行拼音被顶部裁掉、看着像坏了。
 *     根因：插图区 `flex:1.1` 抢走 234px，正文区只有 `flex:1` ——
 *     一张 emoji 插图和整页正文一样高，对分级阅读是主次颠倒。
 *  ③ 读句子页底部三个按钮在窄屏竖排变形（「读一读」被压成竖椭圆、
 *     「听一听」折成三行）。原因：`.words-footer` 固定 `gap:5vw` +
 *     110px 圆钮，320px 下每个按钮只剩 65px 宽。
 *  ④ 书架每行只放得下 1 本书、右边空一半。算式：
 *     `44vw 卡宽 ×2 + 2.4vw 间距 + 5vw×2 内边距 = 100.4vw > 100vw`
 *     —— 数学上就装不下两张，所以永远折行。
 *
 * 两处「不是 bug 但必须点名」的取舍：
 *  · 页/句跳转格子保持 34px（< 44px 的通用推荐命中区）。它是**精确点选**，
 *    放大到 44px 会让 12 页的格子从 2 行涨到 3 行、footer 从 105px 涨到 164px，
 *    吃掉正文空间。这里选择保住正文。`nav-legibility-test.mjs` 守着下限 ≥30px。
 *  · 顶栏动作（拼音开 / 再听一遍 / 显示拼音 / 录音箱）只缩尺寸、**不**收纳进
 *    「更多」菜单：一是它们都是高频操作，二是收纳要改 DOM，而这些元素上有
 *    e2e 契约（kid-nav-test 会点它们）。所以只降档，不搬家。
 *
 * 🔴 **命中区判据（本轮实测定下来的）**：不要求每个元素都 44×44，而是
 *    「**最小边 ≥44px，或面积 ≥ 44×44 = 1936px²**」—— 因为横条形的页签/胶囊
 *    宽度远大于 44px，实际比一个 44×44 的方块更好点。按这个判据复核 @390：
 *    `.mod-tab` 91×37=3367 ✓、`.tab` 58×38=2204 ✓、`.level-tab` 121×38=4598 ✓、
 *    动作胶囊与账号胶囊统一到高 ≥44 ✓；**唯一例外是 `.jump-btn` 34×32=1088**，
 *    理由见上（放大就破「页级 ≤2 行」）。
 * ⚠️ 改完必须用 `mobile-probe.mjs` 把**同一元素改前改后**的宽高对比一遍 ——
 *    2026-09-29 第一版就是把动作胶囊从 42 高/96 宽压到 40 高/65 宽，
 *    而「省高度」的理由并不成立（那一行高度由返回键决定，胶囊不撑高）。
 *
 * 🔴 三条不能碰的线（碰了会让已有 e2e 变红）：
 *  · `.btn-nav` 高度**必须保持 64px** —— kid-nav-test 断言
 *    「末页按钮高度和普通圆按钮一致（不跳）」是 `h === 64`。
 *  · `--fs` 下限（`.story-text` 的 clamp 26px）**不要动** —— 改版式前要先跑
 *    `typo-probe` D 段。这一轮所有问题都靠布局解决，所以没碰它。
 *  · `.word-jump` 的两级格子尺寸不动 —— nav-legibility 断言「页级 ≤2 行」「≥30×30」。
 *
 * 🔴 这个块放在**文件最末尾**：与前面的 @media 块同特异性，靠书写顺序生效。
 *    以后加手机规则继续往这里加，别插到前面去（插前面会被这里的旧值盖掉）。
 * ============================================================ */
@media (max-width: 600px) {

  /* ---------- 1. 三个模块页顶栏：压回一行 ---------- */
  #view-story .topbar, #view-words .topbar, #view-quiz .topbar {
    flex-wrap: wrap; row-gap: 6px;
    padding: 8px 12px;
    gap: 8px;
  }
  /* 返回键 52 → 44（44 是 iOS 的推荐命中区下限，不再往下压） */
  #view-story .topbar > .btn-icon,
  #view-words .topbar > .btn-icon,
  #view-quiz  .topbar > .btn-icon { width: 44px; height: 44px; font-size: 19px; }
  /* 书名放开固定宽度（那是桌面端用来钉住 tab 位置的），窄屏让它省略号收缩 */
  #view-story .topbar-title,
  #view-words .topbar-title,
  #view-quiz  .topbar-title { font-size: 17px; min-width: 0; flex: 0 1 auto; }
  /* 动作胶囊：**只缩字号**（16 → 13）。这是「顶栏能不能一行放下」的关键：
     @390 三个动作胶囊改前要 238px + 书名，差 31px 才放得下。
     ⚠️ 高度**留在 44px**：这一行的高度由 44px 的返回键决定，胶囊长到 44px
     不占额外高度（实测顶栏仍是 121px），所以完全没有理由牺牲命中区。
     🔴 2026-09-29 第一版压到 40px，探针立刻量出 `#btn-pinyin 96×42 → 65×40`、
     `#btn-replay-page 136×42 → 96×40` —— **命中区被我自己改小了**。已修回 44px
     并补回内边距（7px 11px → 8px 14px）。教训：改窄屏时「顺手压小」很容易，
     所以每次都要用 `mobile-probe.mjs` 把**改前改后同一元素**的宽高摆出来比。 */
  #view-story .topbar .btn-pill,
  #view-words .topbar .btn-pill,
  #view-quiz  .topbar .btn-pill { font-size: 13px; padding: 8px 14px; min-height: 44px; }
  #view-story .topbar-actions,
  #view-words .topbar-actions,
  #view-quiz  .topbar-actions { gap: 6px; }
  #view-quiz .quiz-progress { font-size: 15px; }
  /* 模块 tab 仍独占第二行（一个功能都不删），但压扁一档、高度保住 36px 好点 */
  #view-story .topbar .mod-tabs,
  #view-words .topbar .mod-tabs,
  #view-quiz  .topbar .mod-tabs {
    order: 3; width: 100%; justify-content: center;
    flex: 0 0 auto; padding: 4px;
  }
  #view-story .topbar .mod-tab,
  #view-words .topbar .mod-tab,
  #view-quiz  .topbar .mod-tab { font-size: 14px; padding: 9px 13px; min-height: 36px; }

  /* ---------- 2. 听故事页：插图与正文重新分账 ----------
     `flex: 1.1 / 1` → `0.9 / 2.1`。
     为什么不是更极端的 0.62/2.4：那是我第一版的值，实测正文面板能到 486px、
     完全不滚，但插图只剩 121px 高 —— 对绘本页来说太小了（`object-fit:contain`
     在 366×121 的框里只画出 ~128px 宽）。而 390×844 下正文内容只要 285px，
     486px 的框有一半是空的，等于把插图白白压扁。
     0.9/2.1 是量出来的平衡点：@360 插图 172px / 正文框 401px（>内容 390px，不滚），
     @390 插图 185px / 正文框 432px（>285px），@430 插图 212px。
     参考实现见 `@media (max-height:620px) and (min-aspect-ratio:1/1)` 的 0.62/2.4
     —— 那是「又宽又矮」的场景，横向空间富余、必须优先保文字高度，与本场景不同。 */
  .story-body { padding: 0 12px; gap: 10px; }

  .story-text-panel { flex: 2.1; padding: 10px 12px; }
  .story-emoji { font-size: min(14vh, 96px); min-height: 0; }

  /* ---------- 3. 听故事页 footer：给页码格子腾出整行宽 ----------
     只收内边距，**不动 `.btn-nav` 的 64px**（见上面的红线）。
     实测：内边距 24→10 后中段可用宽度 214 → 242px，
     12 个 34px 格子从「每行 5 个共 3 行」变成「每行 6 个共 2 行」，footer 164 → 105px。 */
  .story-footer { padding: 6px 10px calc(6px + env(safe-area-inset-bottom)); }
  .story-page-no { font-size: 13px; }
  .story-foot-mid { gap: 4px; }
  .story-footer .page-jump { gap: 5px; }

  /* ---------- 4. 读句子页：底部三按钮别再竖排变形 ---------- */
  .words-head { padding: 2px 12px 0; }
  .words-head .tabs { flex: 0 1 auto; gap: 6px; }
  .tab { font-size: 15px; padding: 9px 20px; min-height: 38px; }
  .words-body { padding: 0 8px; gap: 6px; }
  .words-footer { gap: 10px; padding: 10px 12px calc(14px + env(safe-area-inset-bottom)); }
  /* ⚠️ 只改底部这一排里的 .btn-mid —— 弹窗里（`.btn-mid.btn-primary`）也用这个类，
     一起改会把弹窗按钮压小。所以选择器必须写 `.words-footer .btn-mid`。
     `flex: 1 1 0` + `white-space: nowrap` 是治「竖排」的方子：
     改前按钮宽度由内容决定、溢出后文字自己换行，现在等宽分掉剩余空间且不折行。 */
  .words-footer .btn-mid {
    flex: 1 1 0; min-width: 0;
    font-size: 15px; padding: 13px 8px; white-space: nowrap;
  }
  .btn-mic { width: 84px; height: 84px; box-shadow: 0 5px 0 var(--primary-deep); }
  .btn-mic .mic-icon { font-size: 26px; }
  .btn-mic .mic-label { font-size: 14px; }

  /* ---------- 5. 书架：每行两本 + 顶部压扁 ----------
     顶部改「两行网格」而不是竖排三行：品牌与账号同行、统计条独占第二行。
     改前 `.shelf-top` 244px（占屏高 29%）。 */
  .shelf-top {
    display: grid; grid-template-columns: minmax(0, 1fr) auto;
    align-items: start; gap: 6px 8px;
    padding: calc(1.4vh + env(safe-area-inset-top)) 4vw 6px;
  }
  .shelf-brand { grid-column: 1; grid-row: 1; }
  /* 🔴 高度留在 44px（= 基础样式的值），**不要往下压**：
     这一行的高度由品牌块（21+2+13.5 ≈ 48px）决定，账号胶囊凑到 44px 不额外占高，
     但压到 40px 就会掉到通用命中区（44px）以下 —— 2026-09-29 第一版压成 40px，
     探针立刻把 `.account-btn 86×40` 算进「可点区偏小」，已修回。 */
  .account-btn {
    grid-column: 2; grid-row: 1; margin-left: 0;
    min-height: 44px; padding: 0 10px; gap: 4px;
  }
  .account-btn .ab-emoji { font-size: 17px; }
  .account-btn .ab-name { font-size: 13px; max-width: 30vw; }
  .shelf-stats { grid-column: 1 / -1; grid-row: 2; gap: 6px; justify-content: flex-start; }
  .shelf-hi { font-size: 21px; }
  .shelf-sub { font-size: 13.5px; margin-top: 2px; }
  .mini-stat { padding: 4px 8px; border-radius: 16px; }
  .mini-stat .mi { font-size: 13px; }
  .mini-stat .mk, .mini-stat .ml { font-size: 11px; }
  .mini-stat b { font-size: 17px; }
  .continue-card { padding: 3px 9px 3px 3px; gap: 4px; max-width: 34vw; }
  .continue-cover { width: 22px; height: 22px; font-size: 12px; }
  .continue-title { font-size: 11.5px; max-width: 26vw; }
  /* 难度 tab：横向滚动保留（六个级别一行放不下是常态），只是把高度提到 38 好点 */
  #view-shelf .level-tabs { margin: 6px 4vw 0; max-width: 92vw; }
  .level-tab { font-size: 14px; padding: 9px 14px; min-height: 38px; }
  /* 🔴 两列的关键：宽度必须让「2 卡 + 1 间距」正好等于内容宽。
     原来写 `min(44vw, 300px)` + `gap:2.4vw` + `padding:5vw` = 100.4vw，
     恒大于 100vw → 永远折成一列。现在用 `calc((100% - 12px)/2)` 反解，
     与 gap 同源，不会再算错。 */
  .shelf-grid { padding: 8px 4vw calc(3vh + env(safe-area-inset-bottom)); gap: 12px; }
  .book-card { width: calc((100% - 12px) / 2); padding: 10px; gap: 6px; border-radius: 18px; }

  /* ---------- 6. 闯关页 ---------- */
  .quiz-body { gap: 3vh; padding: 0 5vw 3vh; }
  .quiz-q { font-size: 20px; line-height: 1.62; }
  .quiz-options { gap: 10px; }
  .opt-btn { padding: 2.6vh 4vw; font-size: 22px; border-radius: 18px; }
  .opt-btn.has-py { padding: 2vh 3vw; font-size: 20px; }
  .quiz-footer { padding: 6px 12px calc(8px + env(safe-area-inset-bottom)); gap: 6px; }
}


/* 2026-10-15: keep Chinese closing punctuation with its preceding character.
   Text may wrap naturally; never shrink the established child-readable font. */
.story-text .zi-pair { display: inline-flex; align-items: baseline; }
.story-text .zi.punct { min-width: 0; width: .45em; margin-left: 0; margin-right: 0; }

/* Two actions remain after removing sentence demonstration playback. */
.words-footer { justify-content: center; }
.words-footer #btn-myplay { flex: 0 1 160px; }

/* Whole-page reading: preserve readable text size and scroll the middle column. */
#view-words .words-body { align-items: stretch; }
#view-words .word-col { min-height:0; overflow-y:auto; padding:8px 2px 14px; }
#view-words .word-col > .word-card {
  flex:0 0 auto; min-height:34vh; max-height:none; margin-top:0; justify-content:flex-start;
}
#view-words .word-main { text-align:left; width:100%; }
#view-words .word-jump { flex-shrink:0; margin-bottom:0; }
#view-words button:disabled { opacity:.45; cursor:default; }


/* 2026-10-19 纯文本阅读：取消插图列，使用居中、顶端起排的阅读纸面。
   面板与离屏测量探针共用内边距，保留整本统一字号及溢出时的滚动。 */
.story-body {
  width: 100%; max-width: 1040px; margin: 0 auto;
  padding: 8px 28px 0; gap: 0; flex-direction: column;
  min-height: 0; min-width: 0;
}
.story-text-panel {
  flex: 1; min-height: 0; min-width: 0;
  padding: clamp(24px, 4vh, 44px) clamp(24px, 4vw, 56px);
  gap: 0; border: 1px solid #eadfce;
  background: #fffdf8; box-shadow: 0 4px 20px #63452808;
  scrollbar-gutter: stable;
}
.story-text {
  margin: 0; width: 100%; min-width: 0;
}
.story-footer {
  width: 100%; max-width: 1040px; margin: 0 auto;
  padding-left: 28px; padding-right: 28px;
}
@media (max-width: 560px) {
  .story-body { padding: 4px 12px 0; }
  .story-text-panel { padding: 24px 16px; border-radius: 20px; }
  .story-footer { padding-left: 12px; padding-right: 12px; }
}
@media (max-height: 500px) and (orientation: landscape) {
  .story-body { padding-top: 0; }
  .story-text-panel { padding-top: 16px; padding-bottom: 16px; }
}

/* 首行缩进只属于段落，不能继承到拼音与汉字的小字格内部。 */
.story-text .zi, .story-text .zi-pair { text-indent: 0; }

/* 自动翻页开关保持44px触控区；窄屏三个动作共享独立工具栏。 */
#btn-story-auto { min-height: 44px; white-space: nowrap; }
#btn-story-auto.is-on { background: #e6f2e4; color: #32683a; box-shadow: inset 0 0 0 1px #9cc497; }
@media (max-width: 560px) {
  #view-story .topbar-actions {
    width: 100%; flex: 0 0 100%; justify-content: flex-end; flex-wrap: wrap;
  }
  #view-story .topbar-actions .btn-pill { padding-left: 10px; padding-right: 10px; }
}

#view-story .topbar-title { min-width: 0; flex: 1 1 auto; }
@media (max-width: 1100px) {
  #view-story .topbar { flex-wrap: wrap; }
  #view-story .topbar-actions { order: 2; }
  #view-story .topbar .mod-tabs { order: 3; width: 100%; justify-content: center; }
}
