/* ============================================================================
   pages-learn.css —— 原 pages.css 的第 6/6 段（2026-09-19 拆分）

   原文件 3,696 行，把六种板块的样式装在了一起（《模型档位与代码质量_操作准则》
   §5 点名的技术债）。现按文件自己的 ===== 横幅拆成六段，index.html 按
   原顺序引入 —— 层叠顺序与拆分前完全一致（拆分脚本做过逐行相等校验）：

     1 pages-login.css → 2 pages-store.css → 3 pages-practice.css
     → 4 pages-identity.css → 5 pages-practice-fx.css → 6 pages-learn.css

   本段：学习模式（learn.js）：单词卡 + 小游戏
   ★ 2026-09-19 起还包括**挑战模式的打地鼠板**（views/practice_mole.js）——
     地鼠板只有一份样式，两个入口共用。理由见文件末尾那一节。
   ============================================================================ */

/* ==========================================================================
   学习模式（learn.js）—— 单词卡 + 小游戏

   这一屏跑在独立窗口里（chrome('bare')），所以它是**整页**的版式，
   不是嵌在平台页面里的一块。布局主轴线：
     顶栏（词本 / 页签 / 结束） → 卡片或游戏板 → 底部操作

   ★ 2026-09-19 老师第一次报版式：
     「现在的学习模式里几个游戏都只用了非常小的空间，整个页面很大部分
      都是空余的，优化一下游戏界面的大小。」
     根因是这些高度全写成了**固定值** —— `.ln-body{min-height:420px}`、
     `.ln-board{min-height:320px}`、`.ln-mole-grid{max-width:760px}`。
     它们跟窗口多大没关系：学生把窗口拉到全屏，板子还是那 320px 高、
     760px 宽，剩下全是空底色。
     → 当轮改法：整页撑满 + 板子吃掉剩余高度 + 格面 `1fr` 平分。
       ★ 但 `1fr` 平分**只解决了「空」，没解决「形」**。

   ★★ 2026-09-20 老师第二次报（就是上面那次的副作用）：
     「几个小游戏又过于巨大了，而且为了填满整个窗口，几个框体都变形了。
      配对游戏每个单词框的尺寸可以参考相机 3:2 或者 16:9；
      随堂测验和完成句子也把四个选项放到一排，用类似 16:9 的尺寸；
      打地鼠……适当留出上下左右的空余。」

     教训一句话：**撑满 ≠ 好看**。`grid-auto-rows: …1fr` 让行高去追窗口，
     窗口一高，格面就被拉成长条 —— 配对牌、选项框、地鼠洞全被压扁/拉长。

     现在的正确口径（**每一格锁定宽高比，整块板按「窗口剩下多高」反算自己该多宽**）：

       格宽 = 板宽 ÷ n
       格高 = 格宽 ÷ ratio          （ratio：配对 1.5 = 3:2；选项与地鼠洞 16/9）
       板高 = n × 格高 + (n−1) × g   （g = gap）
     ⇒ **板宽 = ratio × 板高 − (ratio − 1) × (n − 1) × g**

     高度看得见、宽度反算得出来 —— 于是窗口矮的时候**整块板连带宽一起缩**，
     格子永远是那个比例，绝不会被压扁。公式就写在各自的 `max-width` 里
     （`--board-h` 是「板子实际能用多高」，定义在 `.ln-window` / `.pm-wrap` 上）。
     再配一条 `.ln-board{overflow:auto}` 当安全阀：万一字体撑破了，
     滚动即可，不会挤成一团。

     ⚠️ 别退回 `grid-auto-rows: …1fr` 那种写法，也别退回固定 px 高度 ——
     两种都试过，一种被拉变形、一种留大片空白。
   ========================================================================== */

.ln-window {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-height: 100vh;      /* ← 撑满整页：板子才有空间可分 */
  max-width: 1280px;
  margin: 0 auto;
  padding: 16px 24px 32px;
}

/* ★★ 「板子实际能用多高」—— 所有按比例走的格子都从这一个数反算宽度
   （公式见文件顶部）。减掉的是顶栏 / 页签 / 游戏条 / 题面 / 上下内边距
   这些抢不到的高度 —— 估一个偏保守的值没关系：估多了板子只小一点，
   估少了格子也不会变形（下面那条 overflow:auto 会兜住）。
   `max(320px, …)` 是兜底：窗口压得很扁时宁可让它滚，也不要算出个负宽度。
   挑战那一屏（.pm-wrap）与学习窗口同一个口径。 */
.ln-window, .pm-wrap { --board-h: max(320px, calc(100vh - 330px)); }

/* 独立窗口的 .wrap 在 pages-login.css 里被压到 1080px —— 板子的宽也被它卡住。
   :has() 认不出来的老浏览器**直接忽略这一条**，退回 1080px 照样能用，
   所以这里不做任何 polyfill，也不把它当唯一依赖。 */
body.is-bare .wrap:has(.ln-window) { max-width: 1280px; }

.ln-top {
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
  flex: 0 0 auto;         /* 顶栏固定，长高的只能是下面的板 */
}

.ln-win-title {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-right: auto;
}

.ln-win-title .n {
  font-size: var(--fs-xl);
  color: var(--t-bright);
  font-weight: 600;
}

.ln-win-title .s { font-size: var(--fs-sm); color: var(--t-muted); }

.ln-tabs {
  display: flex;
  gap: 6px;
  background: var(--c-input);
  border-radius: var(--r-md);
  padding: 4px;
}

.ln-tab {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 38px;
  padding: 0 16px;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  color: var(--t-muted);
  font-size: var(--fs-md);
  cursor: pointer;
}

.ln-tab:hover { color: var(--t-primary); background: var(--c-hover); }

.ln-tab.is-on {
  background: var(--c-card-hi);
  color: var(--t-bright);
}

.ln-close { white-space: nowrap; }

/* ★ 原来是 min-height:420px —— 固定值，窗口再高它也不长。
   现在改成「把剩下的高度全吃掉」，再交给 .ln-board 往下分。 */
.ln-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* ---- 牌组切换（中文卡 / 英英卡） ---- */
.ln-decks {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 14px;
  flex: 0 0 auto;
}

.ln-deck-chip {
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: flex-start;
  padding: 9px 14px;
  background: var(--c-input);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
  cursor: pointer;
  text-align: left;
}

.ln-deck-chip .t { color: var(--t-primary); font-size: var(--fs-md); }
.ln-deck-chip .h { color: var(--t-dim); font-size: var(--fs-xs); }

/* 选中只由绿框 + 淡绿底表达，不塞「已选」两个字（老师 2026-09-19） */
.ln-deck-chip.is-on {
  border-color: var(--c-green-a);
  box-shadow: inset 0 0 0 1px var(--c-green-a),
    0 0 0 3px rgba(117, 176, 34, 0.12);
}

/* ---- 进度 ---- */
.ln-prog {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
  flex: 0 0 auto;
}

.ln-prog-track {
  flex: 1;
  height: 6px;
  background: var(--c-input);
  border-radius: 3px;
  overflow: hidden;
}

.ln-prog-fill {
  height: 100%;
  width: 0;
  background: linear-gradient(to right, var(--c-green-b), var(--c-green-a));
  transition: width 0.22s ease;
}

.ln-prog-n {
  color: var(--t-muted);
  font-size: var(--fs-sm);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* ---- 卡片本体：两个面，靠 rotateY 翻 ---- */
.ln-card-stage {
  perspective: 1400px;
  display: flex;
  justify-content: center;
  /* ★★★ 2026-10-13（老师报的 bug）：「现在**学习模式的单词卡**，在**有些设备**里
     被**挤压成一根长条**（左右很短但上下很长，和默认的完全不沾边）。」

     这一层是下面 `.ln-card-wrap` 的 min-height 的**量尺**：
     卡片的高度下限要跟"卡片自己拿到多宽"挂钩，不能跟视口挂钩 ——
     理由见 `.ln-card-wrap` 那段长注释。
     ★ 用容器查询单位（cqw）而不是 vw：vw 跟的是**视口**，而卡片外面还套着
       两级内边距（`.wrap` 32px + `.ln-window` 48px）。窄屏上我们要把这些
       内边距收掉（见文件末尾 620px 那段），用 vw 就会**算多**、
       下限又跑到比例上面去 —— 那正是这个 bug 的复发路径。cqw 跟的是
       卡片真正拿到的那点宽度，内边距怎么变都不会算错。 */
  container-type: inline-size;
}

/* ============================================================================
   ★★★ 2026-10-12（老师报的 bug）：
     「打字卡里面，键帽的位置大小还是错误的。而且还穿插到了文字的下方。」
   根因：打字卡被塞进了**翻卡片的壳** `.ln-card-wrap` —— 它是"卡背翻转"用的，
        定死 `width: min(680px,100%)` + `aspect-ratio: 16/9` + `min-height:300px`：
        · 键盘最宽 1040px，被 680px 一挤 ⇒ 键帽变小、间距错乱（"位置大小错误"）
        · 卡片总高被 16:9 锁在 ~382px，而打字卡内容（大字 + 字母格 + 键盘）约 600px
          ⇒ 内容**溢出壳体**、压在下面的提示文字与底栏上（"穿插到文字下方"）
   ⇒ 打字卡加 `.is-type`：**占满宽度、高度自适应**，不再是"一张卡"。
   ★ 只影响打字卡；其它卡片的 16:9 翻转壳一个字没动。 */
.ln-card-wrap.is-type {
  width: 100%;
  max-width: 1120px;
  aspect-ratio: auto;
  min-height: 0;
  height: auto;
  perspective: none;
}

.ln-card-wrap {
  position: relative;   /* 翻面提示按它定位（见 .ln-flip-hint） */
  /* ★ flex: none —— 这一层**不许被 flex 压缩**。
     这条是照 `.pc-slot` / `.kbd-cap` 那两次事故写的（见 pages-practice.css
     里 `.pc-slot` 那段老师原话）：安卓 Chrome / 三星浏览器在 flex 协商上
     和桌面 Chromium 不一致，"按内容收缩"能把一个盒子压到看不下去。
     形态这种事不能交给协商，要显式禁止。 */
  flex: none;
  width: min(680px, 100%);
  /* ★★ 2026-09-25 老师定：卡片（含卡背）显示比例固定 16:9。
     宽 680px 时高 = 382px，与原来的 380px 几乎一致；窄屏才真正体现比例，
     所以顺带把下面窄屏那条写死的 320px 也去掉了。 */
  aspect-ratio: 16 / 9;
  /* ★★★ 2026-10-13（老师报的 bug）：「学习模式的单词卡，在有些设备里被
     挤压成一根长条（左右很短但上下很长，和默认的完全不沾边）。」

     ▍根因（真 Chrome 逐档量出来的，见 `_work/_selftest_cards_layout.mjs`）
       这里原来写的是 `min-height: 300px` —— 一个**与宽度毫无关系**的数。
       而宽度是 `min(680px, 100%)`，跟着视口一路缩。两条规则一打架：
         宽度 ≥ 533px（视口 ≥ 613px）时，16:9 算出来的高度本来就 ≥ 300，
           `min-height` 是多余的；
         宽度 < 533px 时，`min-height` **反过来压过 `aspect-ratio`**：
           宽度继续随视口缩，高度被钉死在 300px ⇒ 比例从 16:9 翻成竖版，
           而且宽度越窄翻得越狠：
             视口 390 → 卡片 310×300（1 : 0.97，已经竖过来了）
             视口 360 → 卡片 280×300（1 : 1.07）
             视口 320 → 卡片 240×300（1 : 1.25）
             视口 280 → 卡片 200×300（1 : 1.50）
             视口 240 → 卡片 160×300（1 : 1.88）← 这就是老师看到的那根长条
       所以它**不是"某档特判"能修的**：坏的是"高度下限与宽度脱钩"这件事本身，
       而这条规则在每一个宽度 < 533px 的设备上都在坏。

     ▍修法：把下限**绑在卡片自己的宽度上**。
       `cqw` = 这块卡片所在容器的 1% inline-size（量尺见 `.ln-card-stage`），
       16:9 的高度恰好 = 宽度 × 9/16 = 56.25cqw。于是
         `min(300px, 56.25cqw)` **在数学上永远不可能超过比例高度** ——
       形态成了任何视口下的不变量；而宽度够大时（≥533px）它仍然退回 300px，
       也就是原来那条"内容别挤没了"的安全网一个字没丢。
       ★ 为什么不干脆删掉 min-height：它是"卡背的正文别挤没了"的兜底
         （见下面 `.ln-face` 的 padding），要留着；要治的是它**越权**。
       ★ 为什么不用 vw：见 `.ln-card-stage` 那段。
       ★ 也用不着 `max-height`/`vh`：高度完全由 16:9 + 宽度决定，
         窗口矮就让整页滚（`.ln-window` 的 min-height:100vh 本来就允许）——
         `vh` 直接当高度那种脆做法在这里一次都不出现。 */
  min-height: min(300px, 56.25cqw);
  height: auto;
}

.ln-card {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform 0.45s cubic-bezier(0.4, 0.1, 0.2, 1);
  cursor: pointer;
}

.ln-card.is-flip { transform: rotateY(180deg); }

/* 底部留 62px 固定地带给翻面提示 —— 两个面都留，
   长释义滚起来也不会压到提示上（老师 2026-09-19）。 */
.ln-face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 26px 34px 56px;
  background: var(--c-card);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
  backface-visibility: hidden;
  overflow-y: auto;
}

/* ★★ 2026-09-25 老师定：卡背的**主要内容一律置中**（原来贴着第一排、像一行小字），
   并且字号整体放大 —— 「字体也很小，基本看不到」。
   做法：背面本身居中，释义改成「标签在上、内容在下」的竖排（原来标签占左边 76px，
   把释义挤到右边一小条），字号三档全部上调。 */
.ln-back {
  transform: rotateY(180deg);
  justify-content: center;
  align-items: center;
  text-align: center;
}

.ln-front {
  align-items: center;
  justify-content: center;
  background: linear-gradient(160deg, var(--c-card-hi), var(--c-card));
}

.ln-word {
  font-size: clamp(38px, 7vw, 68px);
  font-weight: 700;
  color: var(--t-bright);
  letter-spacing: 0.01em;
  text-align: center;
  word-break: break-word;
}

/* 词性跟着单词一起放大（原来 15px，跟 68px 的单词摆一起像脚注） */
.ln-word-pos { color: var(--t-muted); font-size: clamp(22px, 3.4vw, 32px); }

/* 唯一的翻面提示：固定在卡片下方 16px 处，不在会翻的两个面里
   —— 翻面前后都在同一处、文案也不变。
   pointer-events:none 让点击穿透到卡片上（点提示本身也能翻面）。 */
.ln-flip-hint {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 16px;
  text-align: center;
  color: var(--t-dim);
  font-size: var(--fs-sm);
  pointer-events: none;
}

.ln-def {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  width: 100%;
}

.ln-def .k {
  flex: none;
  color: var(--t-dim);
  font-size: var(--fs-sm);
  letter-spacing: 0.06em;
  padding-top: 0;
}

/* ---- 背面释义的字号：三档，按字数在 defRow() 里挂类名 ----
   短的（「季节」这种）跟正面单词同量级，一眼看清；长的自动降档，
   保证不出 380px 的卡片（中文释义最长 52 字、英文释义更长）。 */
.ln-def .v {
  flex: none;
  width: 100%;
  color: var(--t-bright);
  font-weight: 600;
  font-size: var(--fs-lg);
  line-height: 1.35;
  text-align: center;
}

/* 三档字号（2026-09-25 全面上调：原来 26/20/17 起，短释义在 iPad 上只有一小撮） */
.ln-def .v.is-big  { font-size: clamp(32px, 5.6vw, 50px); line-height: 1.25; }
.ln-def .v.is-long { font-size: clamp(26px, 4.2vw, 38px); line-height: 1.35; }
.ln-def .v.is-xl   { font-size: clamp(21px, 3.2vw, 30px); line-height: 1.5; }
/* 英英卡「暂无」：这词没有英文释义（2026-09-30 改口径，全部词都进这套牌）。
   刻意压暗 —— 它是「这里没有内容」的交代，不是一条释义。 */
.ln-def .v.is-none { color: var(--t-dim); font-size: clamp(24px, 3.6vw, 34px); }

/* 例句容器：和 .ln-def .v 长得一样，但例句**不跟释义一起放大** ——
   它比释义小一档、行距放宽到 1.8，长句子才读得下去。
   （原来还挂着「出处」一行；老师 2026-09-19 定：背面例句不标出处。） */
.ln-ex { flex: 1; }

.ln-ex-en {
  color: var(--t-primary);
  font-size: clamp(21px, 3.2vw, 30px);
  line-height: 1.75;
  text-align: center;
}

.ln-cards-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 20px;
}

.ln-nav-btn { white-space: nowrap; }

.ln-sound { min-width: 130px; }

/* ---- 小游戏目录 ----
   ★ 2026-09-20：原来是 `repeat(auto-fit, minmax(280px,1fr))` + 内容高度，
     在 1440 宽下实测 **290×120（2.42:1）** —— 四条窄扁条，跟别的格子不是一个格调。
     现在 4 个固定一排、每张锁 **16:9**（和选项框、地鼠洞同一个比），
     宽度照样按公式从 `--board-h` 反算。 */
.ln-games {
  --board-h: max(320px, calc(100vh - 250px));   /* 这一屏没有题面块 */
  /* ★★ 2026-10-12 / 老师：「把这个游戏模式做到学习模式里，放到小游戏里。」
     加了第 5 个玩法「单词连连看」之后，原来写死的 `repeat(4, …)` 会把 5 张卡片
     挤成 5 列（1440 下每张只有 ≈222px 宽，比另外四个时代窄了一圈），手机上那张
     更是没法看。⇒ 换成 auto-fit + 一个下限：一张卡片永远不小于 240px，
     大屏上能排几张排几张（1080px 上限内 4 张，1280 窗口下 5 张），
     窄屏自然掉到 2 张（下面 @media 620 那条也就不用再指定列数了）。
     ★ 16:9 的比例、从 --board-h 反算宽度的口径都没动 —— 卡片还是那个比例。 */
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 14px;
  width: 100%;
  max-width: min(1080px, calc(16 / 9 * var(--board-h) - 33px));
  margin: auto;                        /* 在板子里居中 */
}

.ln-game-card {
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: flex-start;
  justify-content: center;
  aspect-ratio: 16 / 9;
  min-height: 0;
  padding: 20px;
  text-align: left;
  background: var(--c-card);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: border-color 0.15s ease, transform 0.15s ease;
}

.ln-game-card:hover {
  border-color: var(--c-accent-dim);
  transform: translateY(-2px);
}

.ln-game-name {
  color: var(--t-bright);
  font-size: var(--fs-lg);
  font-weight: 600;
}

.ln-game-hint {
  color: var(--t-muted);
  font-size: var(--fs-sm);
  line-height: 1.6;
}

/* ---- 游戏通用外壳 ---- */
.ln-gbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 16px;
  flex: 0 0 auto;
}

.ln-gbar-l, .ln-gbar-r {
  display: flex;
  align-items: center;
  gap: 10px;
}

.ln-gstat {
  color: var(--t-primary);
  font-size: var(--fs-md);
  font-variant-numeric: tabular-nums;
}

/* 板子照样吃掉 .ln-body 剩下的高度（「居中」要有参照系才能成立），
   但**不再把高度分给格面** —— 格面自己锁了宽高比，靠 margin:auto 在板里居中。
   `overflow:auto` 是安全阀：万一有个词特别长把格子撑破了，滚动而不是挤压。 */
.ln-board {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 300px;      /* 窗口很矮时的兜底，别让它压成一条 */
  overflow: auto;         /* 内容比板高时滚动，不压格面 */
}

/* ---- 配对游戏 ----
   每张牌锁 **3:2**（老师 2026-09-20：「每个单词框的尺寸可以参考相机 3:2 或者 16:9」）。
   列数 `--n` 由 JS 按牌数算好（16 张 → 4×4）。
   宽度按文件顶部那个公式从 `--board-h` 反算 → 窗口矮时整块板**连带宽一起缩**，
   牌永远是 3:2，不会被拉成长条（上一版就是被 `grid-auto-rows:…1fr` 拉变形的）。

   ★★★ 2026-10-13（老师报的 bug，与上一轮"单词卡长条"同一族）：
     「**安卓平板**里的 **chrome 浏览器**里，**配对游戏**的框也被**压缩成条**了，
       根本看不到具体的信息。」（设备：三星 Tab S7 / 11 吋 / DPR ≈ 2
       ⇒ 横屏 CSS 视口约 1280×720～800）

     ▍根因（真 Chrome + 移动仿真逐项量出来的，工装见
        `_work/_selftest_games_layout.mjs`）：
       牌的高**只剩 `aspect-ratio` 一条规则**，而下面 `.ln-memo-card` 当时写着
       `min-height: 0` —— 那是把"自动最小尺寸"这条安全网**主动让掉**：
         · `aspect-ratio` 一旦没给出高度（引擎 / 时序 / 被别的规则覆盖），
           牌的高只剩 padding = **22px**，192px 宽的牌就成了一根**横条**；
           同一套工装量到的数：牌 192.75×**22**（3:2 该有 128.5）= 该有的 **17%**。
         · 格阵又是 `.ln-board` 这个 column flex 容器的子项，默认 `flex-shrink:1`
           ⇒ 板子腾不出高度时格阵先被压，行高跟着塌，`aspect-ratio` 形同虚设。
       ★ 为什么上一轮修过的**单词卡**没事、偏偏这两处坏：`.ln-card-wrap` 上一轮
         已经拿到了**绑在宽度上的高度下限** `min-height: min(300px, 56.25cqw)` ——
         就算 `aspect-ratio` 不生效，它也能把 16:9 撑出来。老师这轮报的两处
         （配对牌 / 地鼠洞）正是**仅剩的两处 `min-height: 0`** —— 这一点自洽。

     ▍修法（照上一轮那一套，不另发明）：
       ① 高度下限**绑在牌自己的宽度上**：`min-height: min(88px, 比例高)`，
          比例高用容器查询单位 `cqw` 从格阵宽度算 —— `min()` 保证它
          **数学上永远不超过比例高**，形态成了任何视口下的不变量；
       ② `flex: none`：格阵**不许被 flex 压缩**（与 `.ln-card-wrap` 那条同一个理由：
          形态这种事不能交给协商）。板子腾不出高就让 `.ln-board` 的 `overflow:auto`
          滚 —— 允许整页滚动，绝不把格面压掉；
       ③ 列数随宽度自适应：`auto-fit + minmax(max(132px, 每列宽), 1fr)` ——
          宽了正好 4 列（与 `--n` 一致），窄了自动降到 3 / 2 列，
          每张牌仍 ≥ 132×88。
       ★ 下限数字的依据（132 / 88）：
         · 132px 宽 = 本词表（app-junior-1600）里最长的一条释义在牌上
           至少要放得下两行的宽度；卡片左右内边距 2×10 + 牌面 2×8 = 36px，
           剩 96px 给字，按牌面字号 13px 算一行能放 14 个字符
           （词表最长的英文词是 transportation，14 字符）；
         · 88px 高 = 132 ÷ 1.5（3:2 该有的高），是触摸目标 44px 的两倍
           （`config.json` 的 `touch_target_px` 就是 44），
           也放得下两行 13px×1.4 的字。 */
.ln-memo-grid {
  /* 配对这一屏没有题面块，比别的游戏多出一截高度可用 */
  --board-h: max(320px, calc(100vh - 250px));
  /* 间隙写成变量：下面的"比例高"公式要用它（间隙也算进列宽里） */
  --gap: 12px;
  --cell-min: 132px;      /* 每列（每张牌）的宽度下限，依据见上 */
  --cell-floor: 88px;     /* = 132 ÷ 1.5 —— 3:2 的高度下限 */
  /* ★ 量尺：下面 `.ln-memo-card` 的 `cqw` 指的就是这个格阵的内容宽 */
  container-type: inline-size;
  display: grid;
  /* 兜底：认不出下面那条 `max()` 的引擎照旧 4 列均分（与改动前的行为一致） */
  grid-template-columns: repeat(var(--n, 4), minmax(0, 1fr));
  /* 正身：列宽至少 `--cell-min`，但不超过"n 列均分"的宽 ——
     于是宽的时候正好 n 列，窄的时候自动掉列，每张牌都守得住下限。 */
  grid-template-columns: repeat(auto-fit, minmax(max(var(--cell-min),
    calc((100% - (var(--n, 4) - 1) * var(--gap)) / var(--n, 4))), 1fr));
  grid-auto-rows: auto;                 /* 行高由牌自己的 3:2 决定，不再平分板高 */
  gap: var(--gap);
  width: 100%;
  max-width: min(1080px,
    calc(1.5 * var(--board-h) - (var(--gap) / 2) * (var(--n, 4) - 1)));
  /* ★ 不许被 flex 压缩：见上面 ▍修法 ② */
  flex: none;
  margin: auto;                         /* 在板子里居中，不再撑满 */
}

.ln-memo-card {
  position: relative;
  aspect-ratio: 3 / 2;
  /* ★ 写两行同一个属性：认得出 `cqw` 就用后一行（数学上永不越权），
     认不出（老引擎）就退回前一行那个"到哪都认"的硬下限。
     ★ 为什么两个数配套才安全：`--cell-floor` = `--cell-min` ÷ 1.5，
       而上面的列宽**被 minmax 保证** ≥ `--cell-min` ⇒ 3:2 的高必 ≥ `--cell-floor`
       ⇒ 这条下限**永远不会**超过比例高、把牌压成竖条。 */
  min-height: var(--cell-floor);
  min-height: min(var(--cell-floor),
    calc((100cqw - (var(--n, 4) - 1) * var(--gap)) / var(--n, 4) / 1.5));
  padding: 10px;
  background: var(--c-card);
  border: 1px solid var(--c-line);
  border-radius: var(--r-sm);
  cursor: pointer;
  overflow: hidden;
}

.ln-memo-card:hover { border-color: var(--c-accent-dim); }

/* 未翻开时只显示「?」—— 两张牌叠在一起，翻开的把面翻上来。
   ★ 2026-10-13：字号从**视口**单位（vw）改成**容器**单位（cqw）——
     牌拿到多宽 = 格阵宽 ÷ 列数，与视口宽差着"两级内边距 + 列数"两层，
     用 vw 会在窄档**算多**（牌变小了字没变小，字就被 `overflow:hidden` 裁掉：
     实测 1280×660 那档有 2 张中文卡的最后一行被切掉）。
     这是文件顶部那条结论的又一次应用：小屏给显式小版式，别用 clamp(…, vw, …)。
     系数 0.1 / 0.092 是照 1280×800 那一档（牌 192.75 宽）反推的：
     原来 `1.5vw` = 19.2px ⇒ 19.2 ÷ 192.75 ≈ 0.1，参考分辨率的观感一个字没变。 */
.ln-memo-face,
.ln-memo-word {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8px;
  text-align: center;
  font-size: clamp(15px, 1.5vw, 20px);          /* 兜底：认不出 cqw 的引擎 */
  font-size: clamp(13px,
    calc((100cqw - (var(--n, 4) - 1) * var(--gap, 12px)) / var(--n, 4) * 0.1), 20px);
  line-height: 1.4;
  backface-visibility: hidden;
  transition: transform 0.32s ease;
}

.ln-memo-face {
  background: var(--c-card-hi);
  color: var(--t-dim);
  font-size: 34px;
  font-weight: 700;
  transform: rotateY(0deg);
}

.ln-memo-word {
  background: var(--c-input);
  color: var(--t-bright);
  transform: rotateY(180deg);
}

.ln-memo-word.is-cn {
  font-size: clamp(14px, 1.4vw, 18px);          /* 兜底：认不出 cqw 的引擎 */
  font-size: clamp(13px,
    calc((100cqw - (var(--n, 4) - 1) * var(--gap, 12px)) / var(--n, 4) * 0.092), 18px);
  color: var(--t-primary);
}

.ln-memo-card.is-open .ln-memo-face { transform: rotateY(-180deg); }
.ln-memo-card.is-open .ln-memo-word { transform: rotateY(0deg); }

.ln-memo-card.is-done {
  border-color: var(--c-green-a);
  box-shadow: inset 0 0 0 1px var(--c-green-a);
  cursor: default;
}

.ln-memo-card.is-done .ln-memo-word { color: var(--c-green-a); }

/* ---- 题面（测验 / 完成句子 / 打地鼠共用） ---- */
.ln-prompt {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 26px 24px;
  margin-bottom: 18px;
  background: var(--c-card);
  border-radius: var(--r-md);
  text-align: center;
  flex: 0 0 auto;         /* 题面固定高度，长高的是下面那块板 */
}

.ln-prompt-cn {
  font-size: clamp(26px, 3.2vw, 40px);
  color: var(--t-bright);
  font-weight: 600;
  line-height: 1.5;
}

.ln-prompt-hint { color: var(--t-dim); font-size: var(--fs-sm); }

/* 完成句子的那个句子。★ 2026-09-30（老师）：「字体比较小」→
   20 / 2.2vw / 28 提到 26 / 3vw / 40（比随堂测验的中文题面 26~40 同档），
   行高从 1.7 收到 1.6 —— 句子变大了，行距不收会显得松；
   宽度上限 900 → 1000px，长句少折一行。 */
.ln-quiz-sent {
  font-size: clamp(26px, 3vw, 40px);
  color: var(--t-bright);
  line-height: 1.6;
  max-width: 1000px;
}

/* ★★★ 2026-09-30（老师）：「学习模式里，完成句子这个游戏，句子所在的位置太靠上了，
   而且字体比较小，可以下移到单词选项的顶部上面，把字体放大一些。」
   —— 现场：`.ln-prompt` 是 `flex:0 0 auto` 钉在板子顶上，
      `.ln-opts` 却用 `margin:auto` 在**题面之下剩下的**空地里居中
      ⇒ 被撑大的是「句子」和「选项」之间那一段，
      看上去就是「句子孤零零挂在顶上、选项落在下半截」。
   改法：把板子里的空白**让给上下两端** —— 题面吃 `margin-top:auto`、
      选项吃 `margin-bottom:auto`（左右仍 auto 保持居中）。
      flex 里多个 auto margin 会**平分**剩余空间，于是「题面 + 选项」
      作为一个整体垂直居中，句子就正好落在选项的顶上。
   ★ 为什么不用 `justify-content:center`：这块板是 `overflow:auto` 的，
     居中对齐在内容比板高时会把**顶部裁掉而且滚不上去**（经典 flexbox 坑）；
     auto margin 在空间不够时会自动归零，永远能从第一行滚到最后一行。
   ★ 只挂 `.ln-board.is-sent`（`gameBlank` 里加的）—— 随堂测验 / 打地鼠 / 练习页
     用的还是老排法，一个字没动。 */
.ln-board.is-sent .ln-prompt { margin-top: auto; }
.ln-board.is-sent .ln-opts { margin: 0 auto auto; }

/* ---- 选项（随堂测验 / 完成句子共用）----
   ★ 老师 2026-09-20：「把四个选项放到一排，用类似 16:9 的尺寸」。
     原来写的是 `repeat(auto-fit, minmax(260px,1fr))` —— 在 1000px 宽里
     只排得下 **3 个**（4×260 + 3×12 = 1076 > 1000），第 4 个一直被挤到第二行。
   现在改用 **flex**：`flex: 1 1 calc(25% − 9px)` → 4 个正好一排；
   万一某题只出 3 个或 2 个选项，它们会自动摊满一整行，
   不像固定 4 列那样在右边留两个空位。
   宽度同样按上面的公式从 `--board-h` 反算（ratio = 16/9、n = 4、gap = 12
   → 减去 (16/9 − 1) × 3 × 12 ≈ 28px），于是窗口矮的时候选项变窄，
   但**永远是 16:9**，不会变成 4 个细长条。 */
.ln-opts {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  width: 100%;
  max-width: min(1080px, calc(16 / 9 * var(--board-h) - 28px));
  margin: auto;                        /* 在题面下面那块空地里居中 */
}

.ln-opt {
  flex: 1 1 calc(25% - 9px);           /* 4 个一排；2/3 个时自动摊满整行 */
  min-width: 0;
  aspect-ratio: 16 / 9;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 4px;
  padding: 14px 20px;
  text-align: left;
  background: var(--c-card);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
  color: var(--t-primary);
  font-size: clamp(17px, 1.7vw, 22px);
  cursor: pointer;
  transition: border-color 0.12s ease, background 0.12s ease;
}

.ln-opt:hover { border-color: var(--c-accent-dim); background: var(--c-hover); }

.ln-opt.is-ok {
  border-color: var(--c-green-a);
  background: rgba(117, 176, 34, 0.16);
  color: var(--t-bright);
}

.ln-opt.is-bad {
  border-color: var(--c-red-a);
  background: rgba(192, 57, 43, 0.18);
  color: var(--t-bright);
}

/* ---- 打地鼠 ----
   ★ 这一组是**共用板**：学习模式的打地鼠（learn.js 的 gameMole）
   和挑战模式的打地鼠（practice_mole.js）用的是同一份样式 ——
   同一个玩法在两个入口长得不一样，是最容易让人怀疑「是不是坏了」的那类问题。
   板子只管「长什么样」；「露几只、扣不扣命、怎么计分」全在各自的 JS 里。 */
/* ★ 老师 2026-09-20：「打地鼠的词尺寸差不多占据满了整个页面，
   适当留出上下左右的空余」—— 上一版 3×3 的洞被 `…1fr` 拉满整板高度，
   又高又大。现在每个洞锁 **16:9**，整块板按公式从 `--board-h` 反算宽度
   （ratio = 16/9、n = 3、gap = 14 → 减 (16/9 − 1) × 2 × 14 ≈ 22px），
   板子 `margin:auto` 居中，四周自然留出空余。

   ★★★ 2026-10-13（老师报的 bug，与配对牌同一处病）：
     「**打地鼠**也**看不到地鼠**，只能看到中文提示和它的听发音按钮。」
     根因与配对牌**同一条**：洞的高只剩 `aspect-ratio`（洞没有内容 ——
     里面的地鼠是 `position:absolute`），而 `.ln-mole-hole` 写着 `min-height: 0`
     ⇒ `aspect-ratio` 一旦没给出高度，洞就塌成 **0px 高**；
     地鼠是 `bottom:0` + `translateY(105%)` 从洞里升起来的，
     洞的 `overflow:hidden` 把 0px 以外的部分**整个裁掉** ⇒
     洞里可见高度实测 **0**，而题面（中文提示 + 听发音按钮）是 `.ln-prompt{flex:0 0 auto}`、
     压不到也不裁 ⇒ 屏幕上就只剩它俩 —— 与老师那句话**逐字吻合**。
   ▍修法与配对牌完全同构（下限绑宽度 + `flex:none` + 列数自适应）：
     洞宽下限 144px、洞高下限 81px（= 144 ÷ 16/9）。
     144 的依据：地鼠本身有 2×16px 上下内边距 + 一行的字，
     实测地鼠高 55～66px；洞高 81px 时它 0.94×81 = 76px ≥ 66px，
     地鼠**整个**露得出来（`translateY(6%)` 那一下只吃掉洞高的 6%）；
     81px 也是触摸目标 44px 的 1.8 倍。 */
.ln-mole-grid {
  /* ★ 打地鼠的题面块（中文释义 + 提示 + 发音按钮）比其他游戏高一截，
     实测（真 Chrome，1440×900 / 1024×768 两档）：题面块占 183px、它和洞阵之间还有约 50px。
     通用口径 −330px 会算出「板比实际腾出的空间高 38px」→ 底排被切、出滚动条；
     −400px 只剩 16px 底边距，底排贴着视口下沿、看着像被切掉。
     **−450px** 让洞阵上下的空余几乎相等（实测 1440 上 50 / 下 51；1024 上 50 / 下 56）。 */
  --board-h: max(300px, calc(100vh - 450px));
  --gap: 14px;
  --cols: 3;             /* 列数上限：9 个洞排 3×3（列数由 `--cols` 定，别再写死 3） */
  --cell-min: 144px;     /* 每个洞的宽度下限，依据见上 */
  --cell-floor: 81px;    /* = 144 ÷ (16/9) —— 洞的高度下限 */
  /* ★ 量尺：下面洞与地鼠的 `cqw` 指的就是这个格阵的内容宽 */
  container-type: inline-size;
  display: grid;
  /* 兜底：认不出下面那条 `max()` 的引擎照旧 3 列均分（与改动前的行为一致） */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* 正身：列宽至少 `--cell-min`，但不超过"3 列均分"的宽 ——
     窄档自动掉到 2 列 / 1 列，每个洞仍守得住下限。 */
  grid-template-columns: repeat(auto-fit, minmax(max(var(--cell-min),
    calc((100% - (var(--cols) - 1) * var(--gap)) / var(--cols))), 1fr));
  grid-auto-rows: auto;                /* 洞高由它自己的 16:9 决定，不再平分板高 */
  gap: var(--gap);
  width: 100%;
  max-width: min(1080px, calc(16 / 9 * var(--board-h) - 22px));
  /* ★ 不许被 flex 压缩（同配对牌）：形态不能交给协商 */
  flex: none;
  margin: auto;                        /* 在板子里居中 */
}

.ln-mole-hole {
  position: relative;
  aspect-ratio: 16 / 9;
  /* ★ 与配对牌同一个口径：`min(下限, 比例高)` —— 数学上永不越权。
     这一条就是老师那句「看不到地鼠」的解药：洞有了 81px 的下限，
     地鼠就再也不会被挤到 0 高的洞里、被 `overflow:hidden` 整个裁掉。 */
  min-height: var(--cell-floor);
  min-height: min(var(--cell-floor),
    calc((100cqw - (var(--cols, 3) - 1) * var(--gap, 14px)) / var(--cols, 3) / 1.7778));
  background: var(--c-input);
  border-radius: 50% 50% var(--r-md) var(--r-md);
  box-shadow: inset 0 6px 12px rgba(0, 0, 0, 0.45);
  overflow: hidden;
}

.ln-mole {
  position: absolute;
  left: 10%;
  right: 10%;
  bottom: 0;
  transform: translateY(105%);
  padding: 16px 8px;
  background: linear-gradient(180deg, var(--c-hover), var(--c-accent-deep));
  border: 1px solid var(--c-accent-dim);
  border-radius: var(--r-sm);
  color: var(--t-bright);
  /* ★ 字号同样从 vw 改成 cqw（见配对牌那段）：地鼠给出来的词是**英文**，
     词表里最长的是 transportation（14 字符）。
     字号与洞宽挂钩之后，"最长那个词也放得下一行"这件事跟视口无关：
     地鼠一行高 = 32px(padding) + 1.55×字号，洞高下限 81px 容得下 ≈ 31px 的字号，
     而下面这个 clamp 在任何一档都给不到 31px。 */
  font-size: clamp(16px, 1.6vw, 22px);          /* 兜底：认不出 cqw 的引擎 */
  font-size: clamp(14px,
    calc((100cqw - (var(--cols, 3) - 1) * var(--gap, 14px)) / var(--cols, 3) * 0.104), 20px);
  cursor: pointer;
  transition: transform 0.18s ease;
}

.ln-mole.is-up { transform: translateY(6%); }

.ln-mole.is-hit {
  background: linear-gradient(180deg, var(--c-green-a), var(--c-green-b));
  border-color: var(--c-green-a);
}

/* 打错的那一只：红一下再沉下去，和「打中」的绿对称。
   挑战模式的打地鼠有「命」，学生必须看清自己刚点的是哪个词，
   不然「打错了」只是一句空话（这个标记由 practice_mole.js 挂，
   并且和 is-hit 一样在每一窝露头前整体清掉）。 */
.ln-mole.is-miss {
  background: linear-gradient(180deg, var(--c-red-a), var(--c-red-b));
  border-color: var(--c-red-a);
}

.ln-mole-word { display: block; word-break: break-word; }

/* ---- 结束面板 ---- */
.ln-over { text-align: center; }

.ln-over-big {
  font-size: var(--fs-2xl);
  color: var(--t-bright);
  font-weight: 700;
}

.ln-over-sub {
  margin-top: 8px;
  color: var(--t-muted);
  font-size: var(--fs-md);
  line-height: 1.7;
}

.ln-over-stats {
  display: flex;
  justify-content: center;
  gap: 26px;
  flex-wrap: wrap;
  margin: 20px 0 6px;
}

.ln-stat { display: flex; flex-direction: column; gap: 2px; }

.ln-stat .n {
  font-size: var(--fs-xl);
  color: var(--t-bright);
  font-variant-numeric: tabular-nums;
}

.ln-stat .l { font-size: var(--fs-xs); color: var(--t-dim); }

/* ==========================================================================
   联机连连看 · 结算成绩单（views/practice_link.js 的 paintSettle）
   —— 2026-10-13 老师定的结算规则要"分开展示"三件事：
      每个玩家的 **过程得分 / 名次奖励 / 总分**，以及名次。
   ★ 挂在既有 .ln-over 里（既有结算屏的下半段），所以这几种字号都跟着
     既有面板走，不新起一套视觉。
   ★ 一行三个人（名次 / 名字 / 三个数），列宽固定 —— 名字长短不会把
     数字那一列推来推去（tabular-nums + nowrap），看着才像一张表。
   ========================================================================== */

.ln-over-settle {
  margin: 14px auto 4px;
  max-width: 520px;
  padding: 10px 12px;
  background: var(--c-input);
  border: 1px solid var(--c-line-soft);
  border-radius: 12px;
  text-align: left;
}

.ln-settle-head {
  color: var(--t-dim);
  font-size: var(--fs-xs);
  margin-bottom: 6px;
}

.ln-settle-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: var(--fs-sm);
  line-height: 1.9;
}

/* 名次那一列：定宽 + 等宽数字 —— "第 1 名"与"第 12 名"不许把后面推走 */
.ln-settle-rk {
  flex: 0 0 62px;
  color: var(--t-dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.ln-settle-nm {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--t-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ln-settle-num {
  flex: 0 0 auto;
  color: var(--t-bright);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.ln-settle-row.is-me .ln-settle-nm { color: var(--c-green-a); font-weight: 600; }
/* 没交卷的人：淡一档，但这一行**留着**（名单不因为人没做完而变短） */
.ln-settle-row.is-gone { opacity: 0.5; }

.ln-settle-note {
  margin-top: 8px;
  color: var(--t-dim);
  font-size: var(--fs-xs);
  line-height: 1.6;
}

/* ==========================================================================
   单词连连看（views/practice_link.js）
   —— 学习模式小游戏的第 5 个玩法（老师 2026-10-12）。

   ★ 这一段是**从零写的**，没有搬源包 02_分层/link.css：那一份是浅色纸面调
     （#F7F6F3 底 + 三国城池贴图 + 砖墙渐变），照搬进来就是「深色站里贴了一张
     浅色纸」——平台所有页面共用 theme.css 的一套令牌，这里也只用 --c-* / --t-*。
   ★ 搬过来的是**结构**：城墙（不可穿越，只画不占格）→ 巡逻道（一圈，永远可走）
     → 牌区。内核 canReach 认那一圈巡逻道，所以贴墙的牌可以沿城内绕一圈连上。
   ★ 词块尺寸全部走 JS 算出来的 --cw / --ch（见 practice_link.js 的 tileSize）：
     同一盘面上大小必须一致，长短内容只缩字号 —— 这是源包 2026-10-10 老师定的
     「大小必须一致，不能跟着内容长短变」，别退回 min-height + padding 那套。
   ========================================================================== */

.ln-link-note {
  flex: 0 0 auto;
  margin-bottom: 10px;
  color: var(--t-muted);
  font-size: var(--fs-sm);
  line-height: 1.7;
}

/* ---- 本局词数档位（老师 2026-10-12：「单词连连看 现在无法选单词数」）----
   ★ 挂在 .ln-link-note **里面**（棋盘正上方）：说明条与档位是同一类东西
     （「这一局怎么打」），而且这样 .ln-board 仍然只是「说明条 + 牌区」两件、
     .ln-link-wrap 仍然只是「装牌 + 底部按钮行」两件 —— 那两条结构有断言在守，
     新功能不该去动旧结构（见 practice_link.js 里 start() ① 那段）。
   ★ 只补布局与尺寸：颜色/底色/悬停态一律走 components.css 的 .btn，
     选中态走平台上已经在用的 `.is-on`（.ln-tab / .ln-deck-chip 同款），
     不另造一套按钮皮肤。 */
.ln-link-note-txt { display: block; }

.ln-link-count {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 6px;
}

.ln-link-count-lab { color: var(--t-dim); font-size: var(--fs-sm); }

/* 比 .btn 小一号：这一排是「顺手的档位」，不该抢走牌区的注意力 */
.ln-link-count-btn {
  min-height: 26px;
  padding: 0 10px;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}

/* 当前这一档 —— 与 .ln-tab.is-on / .ln-deck-chip.is-on 同一个语义（绿框 + 淡绿底） */
.ln-link-count-btn.is-on {
  background: var(--c-card-hi);
  box-shadow: inset 0 0 0 1px var(--c-green-a),
    0 0 0 3px rgba(117, 176, 34, 0.12);
}

/* ★★ 本机放不下的档位（手机竖屏一屏只有 13 对 ⇒ 16 那一档）。
   画出来、但点不了：老师要的是「能选」，藏起来等于又变回「没有这个功能」；
   而「16 对（本机最多 13 对）」这个看得见的答复，正好回答
   「选了 16 却只给 13」那件事 —— 不静默降级，也不假装支持。
   disabled 的透明度由 .btn:disabled 给，这里只把字压暗一档。 */
.ln-link-count-btn.is-full {
  color: var(--t-dim);
  text-decoration: line-through;
}

/* 灰按钮收不到 click（disabled 的元素不派发事件）⇒ 外面这层透明壳接点击，
   点一下把「为什么点不了」写到提示行上。它不占位、不改布局。 */
.ln-link-count-dis { display: inline-flex; }

.ln-link-count-tip { color: var(--t-dim); font-size: var(--fs-xs); }

.ln-link-wrap {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* 城墙 + 城内地面。牌区网格 .ln-link-grid 在里面居中。
   ★ --pad 是这一圈内边距的**唯一来源**：连线 SVG 的定位也读它（见下一条）。
     窄屏由 @media 620 收到 5px —— 390px 的手机上这 6px 也是要省的。 */
.ln-link-board {
  --pad: 8px;
  position: relative;              /* 连线 SVG 的定位参照 */
  width: fit-content;              /* ★ 贴着自己的格子，不留一大片空城内地面 */
  max-width: 100%;
  margin: 0 auto;
  padding: var(--pad);
  background: var(--c-input);
  border: 3px solid var(--c-line);
  border-radius: var(--r-md);
  box-shadow: inset 0 0 0 1px var(--c-line-soft),
    0 3px 10px rgba(0, 0, 0, 0.35);
}

/* 连线：覆盖在牌区之上，纯装饰，绝不挡点击。
   颜色取 currentColor（.ln-link-board 继承的正文色）—— 不另造一个"连线专用色"。 */
.ln-link-svg {
  position: absolute;
  left: var(--pad);
  top: var(--pad);
  pointer-events: none;
  color: var(--c-gold);
  opacity: 0.9;
}

.ln-link-grid {
  display: grid;
  gap: 6px;                        /* ★ 必须与 practice_link.js 的 GAP 一致 */
  justify-content: center;
}

/* 词块。★ 颜色**不在这里写死**，由下面 .t0/.t1/.t2 按词号给：
   同一个词的英文格与中文格同色（源包 2026-10-10 老师：三色定位更快）。 */
.ln-link-tile {
  width: var(--cw);
  height: var(--ch);
  min-width: 0;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3px;
  overflow: hidden;
  border: 1px solid var(--c-line);
  border-radius: var(--r-sm);
  background: var(--c-card);
  color: var(--t-bright);
  font-family: inherit;
  line-height: 1.35;
  text-align: center;
  cursor: pointer;
  user-select: none;
  transition: transform 0.08s ease, border-color 0.15s ease;
}

.ln-link-tile.is-cn { line-height: 1.2; }

.ln-link-tile:hover { border-color: var(--c-accent-dim); }
.ln-link-tile:active { transform: scale(0.97); }

/* 字体自适应（fitFont）算出来的字号写在里面的 span 上 */
.ln-link-word {
  display: block;
  word-break: break-word;
  overflow-wrap: anywhere;
}

/* 选中：实心金框 + 一道外发光 —— 源包用 outline，这边用 border-color + shadow，
   因为词块的 border 本来就在（改色比描边更省一层绘制）。 */
.ln-link-tile.is-sel {
  border-color: var(--c-gold);
  box-shadow: inset 0 0 0 2px var(--c-gold),
    0 0 0 3px rgba(212, 175, 55, 0.18);
}

/* 消掉的那一刻闪一下绿；提示闪一下金（虚框）。
   两个都是**瞬时**的：paint_link.js 在下一次 render 时就不会再把它们挂上去，
   所以绿色绝不会留到下一对（源包第 23 轮为这个「绿色留在洞上」修过一次）。 */
.ln-link-tile.is-ok {
  border-color: var(--c-green-a);
  box-shadow: inset 0 0 0 2px var(--c-green-a);
}

.ln-link-tile.is-hint { border-color: var(--c-gold); }

/* 已消 / 开局留白：留位置但不可见、不可点。★ 不带 is-en/is-cn 兜底类 ——
   带了就会让「这一盘有多少个英文格」这种选择器统计凭空多一倍（源包坑清单第 5 条）。 */
.ln-link-tile.is-gone {
  visibility: hidden;
  pointer-events: none;
}

/* 城墙内侧的巡逻道（永远可走，内核认它）。
   ★★ 宽度由 JS 写进来（--walk）：桌面 14px、窄屏 8px。
      它是**纯视觉**，但在 390px 的手机上两条道要吃掉 28px，
      而这 28px 正好是「6 列放得下」和「放不下、牌区要横滚」的差别
      —— 所以它必须跟着格尺寸一起算，不能写死（真 Chrome 量出来的）。 */
.ln-link-walk {
  width: var(--walk, 14px);
  min-width: 0;
  min-height: 0;
  pointer-events: none;
  border: 1px dashed var(--c-line-soft);
  border-radius: var(--r-sm);
  background: repeating-linear-gradient(45deg,
    rgba(255, 255, 255, 0.045) 0 5px, transparent 5px 10px);
}

/* 形状自带的洞：占位保住网格对齐，完全透明、不响应点击 */
.ln-link-hole {
  min-width: 0;
  min-height: 0;
  pointer-events: none;
}

/* 三色（同色 ≠ 同一对 —— 这一句写在页面的提示条里，别只给颜色不解释）：
   英文格 = 同色实心深底；中文格 = 同色浅底 + 同色粗边 ⇒ 类型仍一眼可分。
   ★ 这里的深底三色是**调过的平台版**：源包那三个色（#B03A2E / #1E7B4F / #1B5FA8）
   是给白底纸面配的，直接用在深色卡片上对比度不够，字会糊。 */
.ln-link-tile.t0 { --tc: #d16455; background: #7d2b20; border-color: #d16455; }
.ln-link-tile.t1 { --tc: #62bd8f; background: #1d5c3d; border-color: #62bd8f; }
.ln-link-tile.t2 { --tc: #7ab4f0; background: #1d4a7a; border-color: #7ab4f0; }
.ln-link-tile.t0.is-cn,
.ln-link-tile.t1.is-cn,
.ln-link-tile.t2.is-cn {
  background: var(--c-card-hi);
  border: 2px solid var(--tc);
  color: var(--tc);
}

/* ---- 底部：提示 / 重排 / 发音 + 限时连击条 ---- */
.ln-link-foot {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 14px;
}

/* 一句话提示。★ min-height 写死：没有它的话，每出一句提示整块板会往上跳一格
   （源包那行 #msg 有 min-height:22px，照抄）。 */
.ln-link-msg {
  flex: 1 1 240px;
  min-height: 22px;
  color: var(--c-green-a);
  font-size: var(--fs-md);
}

.ln-link-msg.is-bad { color: #ff8b7d; }

.ln-link-chain { flex: 1 1 100%; }

.ln-link-chain-lab {
  color: var(--t-dim);
  font-size: var(--fs-xs);
}

.ln-link-chain-lab b {
  color: var(--t-primary);
  font-size: var(--fs-md);
  font-variant-numeric: tabular-nums;
}

.ln-link-chain-track {
  height: 8px;
  margin-top: 3px;
  background: var(--c-input);
  border: 1px solid var(--c-line-soft);
  border-radius: 99px;
  overflow: hidden;
}

.ln-link-chain-fill {
  height: 100%;
  width: 0;
  background: var(--c-line);
}

/* 链在跑（还没断）的时候才点亮 —— 断了就退回一条灰底，一眼看出「连不上了」 */
.ln-link-chain-fill.is-hot {
  background: linear-gradient(90deg, var(--c-red-a), var(--c-gold) 45%, var(--c-green-a));
}

/* ==========================================================================
   联机连连看 · 对手进度面板（views/practice_link.js 的 roomPanel / roomPaint）
   —— 老师 2026-10-13：「能让每个玩家隔几秒看到其他对手的大概进度」。

   ★★★ **零位移**是这一块最要紧的约束（这个项目反复踩过）：
     它 `position: fixed` 挂在**棋盘之外**（document.body 上），
     所以它出现、变高、多十个玩家，都**不会让棋盘挪一格** ——
     正在点格子的学生不会因为面板刷新而点到别的格子。
     ⇒ 所以这一块里**不许**出现任何影响文档流的写法（float / inline-block 撑宽 /
       position:static 之类）。要改先想清楚"它会不会推棋盘"。

   ★ 宽度写 `min(280px, 46vw)`：窄屏不会盖住半块棋盘，宽屏也不会随内容伸缩
     （文字长短变 → 面板宽度变 → 视觉上"抖"）。
   ★ 面板内部滚动（max-height + overflow），30 个学生也不会把屏幕撑爆。
   ========================================================================== */

.ln-room-panel {
  position: fixed;
  right: 12px;
  top: 64px;                 /* 让开 .ln-gbar 那一行外壳 */
  z-index: 40;
  width: min(280px, 46vw);
  max-height: 48vh;
  overflow: auto;
  padding: 8px 10px;
  background: var(--c-panel, rgba(12, 16, 24, 0.86));
  border: 1px solid var(--c-line-soft);
  border-radius: 12px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.32);
  font-size: var(--fs-xs);
}

/* 还没拿到数据（第一拍之前）先藏起来 —— 它是 fixed，藏/显都不动棋盘 */
.ln-room-panel.is-off { display: none; }

.ln-room-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 6px;
}

.ln-room-title { color: var(--t-dim); font-size: var(--fs-xs); }

.ln-room-list { display: flex; flex-direction: column; gap: 7px; }

.ln-room-row { display: flex; flex-direction: column; gap: 3px; }

.ln-room-nm { display: flex; align-items: baseline; justify-content: space-between; gap: 6px; }

/* 名字可能很长（中文姓名 4 字 + 班号）：让它可以省略，**不许**把面板撑宽 */
.ln-room-name {
  color: var(--t-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ln-room-pct {
  color: var(--t-dim);
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
}

/* ★★ 2026-10-13（老师：「面板要显示百分比 + 大概还剩多少对的简单示意」）：
   百分比在 .ln-room-pct、剩余对数在这一格。**它只是一个数量** ——
   面板里永远不出现"剩的是哪几个词"（零答案泄露那条纪律）。
   ★ `flex: 0 0 auto` + `nowrap`：这一格出现/文字变长都不许把名字挤走
     （名字那一格是 flex:1 + 省略号，位移全被它吃掉）。 */
.ln-room-rest {
  color: var(--t-dim);
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
  white-space: nowrap;
}

.ln-room-row.is-me .ln-room-name { color: var(--c-green-a); font-weight: 600; }
.ln-room-row.is-me .ln-room-pct { color: var(--c-green-a); }
.ln-room-row.is-gone { opacity: 0.42; }

.ln-room-track {
  height: 6px;
  background: var(--c-input);
  border: 1px solid var(--c-line-soft);
  border-radius: 99px;
  overflow: hidden;
}

/* 宽度由 JS 给**自定义属性** `--ln-pct`（0–100），这里算成百分比 ——
   JS 一侧不写 style.width（见 practice_link.js 里那段注释的两条理由）。 */
.ln-room-fill {
  height: 100%;
  width: calc(var(--ln-pct, 0) * 1%);
  background: var(--c-line);
  transition: width 0.3s ease;
}

.ln-room-fill.is-done {
  background: linear-gradient(90deg, var(--c-green-a), var(--c-gold));
}

/* ★★ 2026-10-13：进度条下面那两行**必须**各自一行、不许折行 ——
   `sub`（消了几对 / 分数 / 连击）与 `bonus`（第 N 名 · 过程 + 奖励 = 总分）
   都是"文字长短会变"的内容，一旦允许折行，行数就在几秒一拍之间变，
   下面几行跟着上下动。`nowrap` + `ellipsis`：高度恒定，多余的字省略。
   ★ 这也是"面板内部零位移"的落点（棋盘那边靠 fixed 定位保证）。 */
.ln-room-sub {
  color: var(--t-dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 结算那一行（"为什么他赢了"）：名次 + 过程 + 奖励 = 总分。
   ★ 永远占位（节点在 row 建好时就有），没交卷时写"还没交卷" ——
     行高因此恒定。 */
.ln-room-bonus {
  color: var(--t-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 窄屏：面板收得更小、更靠边（棋盘本来就窄） */
@media (max-width: 620px) {
  .ln-room-panel {
    width: 46vw;
    right: 6px;
    top: 52px;
    max-height: 34vh;
    padding: 6px 8px;
    border-radius: 10px;
  }
}

/* ==========================================================================
   挑战模式 · 打地鼠（views/practice_mole.js）
   —— 与学习模式共用上面那块地鼠板（.ln-mole-*），这里只补挑战特有的外壳：
     整页撑满 + 连击提示。
   ========================================================================== */

.pm-wrap {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-height: 100vh;
  max-width: 1280px;
  margin: 0 auto;
  padding: 16px 24px 32px;
}

/* 独立窗口的 .wrap 会把它压到 1080px —— 和学习窗口同样的处理（见文件顶部） */
body.is-bare .wrap:has(.pm-wrap) { max-width: 1280px; }

/* 连击提示：答对时从题面里浮起一个「+N」再淡掉。
   它是纯装饰，pointer-events:none —— 绝不挡住底下的地鼠。 */
.pm-combo {
  position: absolute;
  left: 50%;
  bottom: 12px;
  transform: translateX(-50%);
  pointer-events: none;
  color: var(--c-green-a);
  font-size: var(--fs-xl);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  animation: pm-combo-up 0.9s ease-out forwards;
}

@keyframes pm-combo-up {
  0%   { opacity: 0; transform: translate(-50%, 6px) scale(0.9); }
  25%  { opacity: 1; transform: translate(-50%, 0) scale(1.06); }
  100% { opacity: 0; transform: translate(-50%, -26px) scale(1); }
}

/* 题面要能托住那个绝对定位的连击提示 */
.pm-prompt { position: relative; }

/* 开局倒计时在这一屏要占满整块板 —— 这会儿题目还没出来。
   （卷面式那边它塞在 .pc-stage 里，靠别的内容撑高度，这里没有。） */
.pm-wrap .pc-countdown { flex: 1; }

@media (max-width: 620px) {
  .ln-card-wrap { aspect-ratio: 16 / 9; height: auto; }
  /* ★★★ 2026-10-13（跟"长条"同一个 bug 的第二半）：
     窄屏上左右两级内边距一共吃掉 80px（`.wrap` 16×2 + `.ln-window` 24×2）——
     在 320px 的机器上那是 **1/4 个屏幕**。卡片本来就该把那点宽度拿回来：
     宽度多 20px，16:9 的高度就多 11px，正文的可读面积是真的差了一档。
     ★ `:has()` 认不出来的老浏览器**直接忽略这一条**（退回 16px），
       不影响任何形态 —— 形态由 `.ln-card-wrap` 的 16:9 保证，
       这里只调"卡片能拿到多宽"，丢了也只是小一点。（与上面 body.is-bare
       那条同一个口径：可选项，不当唯一依赖。） */
  .ln-window { padding: 12px 10px 24px; }
  body.is-bare .wrap:has(.ln-window) { padding-left: 8px; padding-right: 8px; }
  /* 卡面内边距跟着收：26/34/56 是按 680×382 的卡定的，240×135 的卡上
     光上下内边距就吃掉 82px、左右再吃 68px —— 正文只剩一条缝。 */
  .ln-face { padding: 14px 12px 40px; gap: 10px; }
  /* `.ln-word` 的 clamp 下限 38px 是按大卡定的；小卡上 38px 的
     "fourteenth" 一行要 197px，240px 宽的卡里会折成两行。
     ★ 只降字号、不动任何盒子的尺寸 —— 布局不受字号连累。 */
  .ln-word { font-size: clamp(26px, 8.4vw, 44px); }
  .ln-body { min-height: 0; }
  .ln-board { min-height: 260px; }
  /* 窄屏只收「间隙」和「板子可用高度」—— **绝不去改行高**：
     行高一动，格子就不是 3:2 / 16:9 了，正是这次要修的毛病。
     比例交给上面那些 aspect-ratio，窗口矮了整块板自己会缩。 */
  .ln-window, .pm-wrap { --board-h: max(280px, calc(100vh - 300px)); }
  /* ★ 2026-10-13：窄屏只改**间隙变量**（`--gap`），不要再直接写 `gap: 8px` ——
     两个格阵的比例高公式、最大宽度公式都是拿 `--gap` 算的，
     直接改 `gap` 会让"下限"和"实际列宽"两套数各说各话（这类脱钩正是这次的根因）。 */
  .ln-memo-grid { --gap: 8px; --board-h: max(280px, calc(100vh - 230px)); }
  .ln-mole-grid { --gap: 8px; }
  /* 目录 4 个一排太挤 → 2 个一排（仍 16:9）。
     ★ 2026-10-12：列数不用在这里改了 —— 上面那条 `.ln-games` 已经换成
       auto-fit + minmax(240px, 1fr)，窄屏自己就掉到 2 张（甚至 1 张）。
       这里只留「间隙和板子高度」这一档，别把列数写回来。 */
  .ln-games {
    gap: 10px;
    --board-h: max(280px, calc(100vh - 230px));
  }
  /* 连连看：窄屏把城墙和巡逻道收窄 —— 一圈巡逻道是 14px×2，加上 padding
     在 360px 的手机上要吃掉近 60px 的牌区宽度，收窄之后牌区能多出一格。
     ★ 巡逻道的**实际宽度**由 JS 的 --walk 定（见 .ln-link-walk），
       这里只改城墙内边距和圆角；两处必须同口径 —— JS 那边用的是同一个 620 断点。 */
  .ln-link-board { --pad: 5px; border-width: 2px; }
  .ln-link-tile { border-radius: 2px; }
  /* 手机上 4 个一排太挤 → 2 个一排，仍保持 16:9 */
  .ln-opts { gap: 8px; }
  .ln-opt { flex-basis: calc(50% - 4px); padding: 12px 14px; }
  .pm-wrap { padding: 12px 14px 24px; }
}

/* ★★ 2026-10-13：`--board-h` 改用 `dvh`（动态视口高度）—— 安卓 Chrome 的
   `100vh` 是**大视口**（地址栏收起时才达到的高度），比真实看得见的高度大
   50~60px。而这两块板子的**宽度**是从 `--board-h`（一个高度）反算出来的：
   高度估大了，板子就会比实际腾得出的空间大 —— 出滚动条还是轻的，
   老毛病是格面被压。`dvh` 跟着真实可视区走（`.modal` 的 max-height
   在 components.css 里已经为同一个原因改过一次，见那段"iPad 卡死"的注释）。

   ★ 为什么这里必须用 `@supports` 包一层，而 `.modal` 那边是写两行：
     `--board-h` 是**自定义属性**，它的值不在这里解析。写两行的话，
     不认 `dvh` 的引擎不会"退回上一行"，而是在**用到它的地方**整条失效
     （`max-width` 变成 none ⇒ 板子反而撑满）。普通属性才适用"写两行"那套。 */
@supports (height: 100dvh) {
  .ln-memo-grid { --board-h: max(320px, calc(100dvh - 250px)); }
  .ln-mole-grid { --board-h: max(300px, calc(100dvh - 450px)); }

  @media (max-width: 620px) {
    .ln-memo-grid { --board-h: max(280px, calc(100dvh - 230px)); }
  }
}

/* ---------------- 打字卡（2026-09-25 新增，第 5 种卡） ----------------
   同一个词反复打：打对 → 细绿勾闪一下 → 输入框清空 → 还是这个词。
   输入框与单词等长，字号与卡片正面的单词同量级。 */

.ln-type {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: 22px 14px;
}

.ln-type-row {
  display: flex;
  align-items: center;
  gap: 10px;
}

.ln-type-boxes {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
}

/* 一个字母一个格子：打对变绿、打错红一下（整词随即从头再来） */
.ln-type-box {
  /* ★ 2026-09-25 老师选 L 档：56×74，字母 40px；字体要 **bold**（原来 mono 细体看不清） */
  width: 56px;
  height: 74px;
  background: var(--c-input);
  border: 1px solid var(--c-line);
  border-radius: 8px;
  color: var(--t-bright);
  font-size: 40px;
  font-weight: 700;
  text-align: center;
  padding: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

.ln-type-box:focus {
  border-color: var(--c-accent);
  outline: none;
}

.ln-type-box.is-ok {
  border-color: var(--c-green-a);
  color: var(--t-bright);
}

.ln-type-box.is-err {
  border-color: var(--c-red-a);
  animation: ln-shake 0.35s ease-out;
}

@keyframes ln-shake {
  0% { transform: translateX(0); }
  25% { transform: translateX(-3px); }
  50% { transform: translateX(3px); }
  75% { transform: translateX(-2px); }
  100% { transform: translateX(0); }
}

/* 细绿勾：默认不可见，打对时闪一下（老师要的「轻描淡写」） */
.ln-type-mark {
  color: var(--c-green-a);
  font-size: 30px;
  opacity: 0;
}

.ln-type-mark.is-pop {
  animation: ln-tick 0.8s ease-out;
}

@keyframes ln-tick {
  0% { opacity: 1; transform: scale(0.7); }
  55% { opacity: 1; transform: scale(1.15); }
  100% { opacity: 0; transform: scale(1); }
}

.ln-type-rep {
  color: var(--t-dim);
  font-size: var(--fs-sm);
}


/* ---------- 长释义的「详情」（老师 2026-09-26） ----------
   卡片上只给前 150 字，剩下的点这颗按钮弹完整释义。
   ★ 它是按钮不是链接：手机上要点得到，所以给足 44px 的触控高度。 */
.ln-def .ln-more {
  flex: none;
  margin-top: 4px;
  min-height: 34px;
  padding: 0 16px;
  border-radius: var(--r-md);
  border: 1px solid var(--c-line);
  background: var(--c-card-hi);
  color: var(--c-accent);
  font-size: var(--fs-sm);
  font-family: var(--font);
  cursor: pointer;
}
.ln-def .ln-more:hover { border-color: var(--c-accent); background: var(--c-hover); }
.ln-def .v.is-clipped { text-align: left; }


/* ============================================================================
   打字卡 · 深色版（老师 2026-10-12 给的设想画面）
   ----------------------------------------------------------------------------
   老师原话：「打字的 UI 应该要做成这样。修改一下，整个页面甚至不需要多余的
             文字。这个界面已经足够一目了然了。」
   版式（自上而下）：深蓝底 → 大号单词 → 字母格 → 浅色键盘卡（键盘 + 十指图）
   配色取自设想画面（已用取色器量过）：
       页面底色 #1B2939 ｜ 字母格 #0F1621 ｜ 当前格描边 #7EA2FF ｜ 键盘卡 #F3F5F9
   ★ 只作用于 `.ln-type.is-dark`，其它卡片与旧版式完全不受影响。
   ========================================================================== */
.ln-type.is-dark {
  background: #1b2939;
  border-radius: 18px;
  padding: 34px 24px 26px;
  gap: 20px;
}

/* ---- 单词：整屏主角 ---- */
.ln-type.is-dark .ln-word {
  color: #fff;
  font-size: clamp(40px, 6.2vw, 74px);
  font-weight: 800;
  letter-spacing: .5px;
  line-height: 1.15;
  text-align: center;
  margin: 0;
}

/* ---- 字母格：深色方块，当前格亮蓝描边 ---- */
.ln-type.is-dark .ln-type-boxes { gap: 10px; }
.ln-type.is-dark .ln-type-box {
  width: 64px;
  height: 84px;
  background: #0f1621;
  border: 1.5px solid #26364b;
  border-radius: 12px;
  color: #fff;
  font-size: 42px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05);
}
.ln-type.is-dark .ln-type-box:focus {
  border-color: #7ea2ff;
  box-shadow: 0 0 0 3px rgba(126, 162, 255, .22), inset 0 1px 0 rgba(255, 255, 255, .05);
}
.ln-type.is-dark .ln-type-box.is-ok { border-color: #5fd08a; color: #eafff2; }
.ln-type.is-dark .ln-type-box.is-err { border-color: #ff7a6b; background: #3a1b1b; }
.ln-type.is-dark .ln-type-mark { color: #5fd08a; font-size: 30px; }

/* ---- 多余文字一律不显示（老师：整个页面不需要多余的文字） ----
   ★ 2026-10-12 二次：老师点名要去掉的三类字，这里统统不显示：
       · 「按下一个键，键盘上对应的键就会亮」（键盘右侧那行，`.kbd-bar`）
       · 「默认浅灰」（皮肤选择器按钮上的名字 → 只留三格色块，名字进 title）
       · 「错一个字母就从头再来…」（已在 JS 里不再进 DOM，见 paintType） */
.ln-type.is-dark .kbd-bar { display: none; }
.ln-type.is-dark .kbd-skins-lab { display: none; }
.ln-type.is-dark .kbd-skin span { display: none; }
.ln-type.is-dark .kbd-skin { padding: 4px 7px; }
.ln-type.is-dark .kbd-skin i b { width: 13px; height: 18px; }

/* ---- 键盘放进一张浅色卡片（与设想画面一致） ----
   ★ `width:100%` 是必须的：`.ln-type.is-dark` 是 `align-items:center` 的纵向 flex，
     不写的话键盘会"按内容收缩"（`flex:1 1 0` 的键帽基准宽度为 0）⇒ 键帽被压扁、错位。
     这正是老师看到的"键帽位置大小错误"。（2026-10-12） */
.ln-type.is-dark .kbd {
  width: 100%;
  align-self: stretch;
  background: #f3f5f9;
  border-color: #e2e6ec;
  box-shadow: 0 12px 34px rgba(0, 0, 0, .28);
}

/* ---- 皮肤选择器：低调一行（没有可选项时自然消失） ---- */
.ln-type.is-dark .kbd-skins { justify-content: center; margin-top: 12px; }
.ln-type.is-dark .kbd-skin {
  background: #24384f;
  border-color: #3a5474;
  color: #d6e2f0;
}
.ln-type.is-dark .kbd-skin:hover { border-color: #7ea2ff; }
.ln-type.is-dark .kbd-skin.is-on { border-color: #7ea2ff; box-shadow: 0 0 0 2px rgba(126, 162, 255, .25); }

/* ============================================================================
   ★★★ 2026-10-13（老师报的 bug，**这是那根"长条"的第二个来源、也是最狠的一个**）
     老师原话：「现在**学习模式的单词卡**，在**有些设备**里被**挤压成一根长条**
               （左右很短但上下很长，和默认的完全不沾边）。」

   ▍根因（真 Chrome 量出来的，见 `_work/_selftest_cards_layout.mjs`）
     打字卡这一屏的内容是「大号单词 → 字母格 → 软键盘（+ 十指图）」，
     三样在宽屏上是横着排的，可它们的尺寸**全是写死的、没有一个会跟宽度缩**：
       · `.ln-type-box`  64×84 + `flex-wrap: wrap`
       · `.kbd-cap`      高 52、每行 14 个键（`.kbd` max-width 1040）
       · `.kbd-hands`    十指图，一个人就占 130px 高
     宽度不够时它们只会**各自折行**，于是卡片高度随行数线性上涨：
       320×568  → 卡片 240 × **1268**（宽高比 0.19 —— 一根竖着的长条）
       280×653  → 卡片 200 × **1324**
       240×400  → 卡片 160 × **1784**（宽高比 0.09）
     而默认（≥1024 宽）是 1120×885。这就是老师说的"和默认的完全不沾边"。

   ▍修法：窄屏给这一屏一个**显式的、写死的**小尺寸版式。
     ★ 为什么"写死"而不是 `clamp(…, vw, …)`：`.pc-slot` / `.pc-kb-slot`
       那两次事故已经写过结论（见 pages-practice.css 里 `.pc-slot` 那段）——
       安卓 Chrome 上 `vw` 参与 `clamp` 之后会被算小、元素贴着下限甚至被
       flex 压缩，同一份 CSS 在两个 Chromium 上表现不同。老师点名过两次，
       这条线不再赌：窄屏用**显式媒体查询**，任何浏览器上数值都一样。
     ★ 断点为什么是 **700px** 而不是文件里惯用的 620px（这一条是**量出来的**，
       不是估的 —— 扫描脚本 `_work/_probe_type_sweep.mjs`）：
         视口 620 → 打字卡 584×351（宽高比 1.67，桌面版式在小屏上还成立）
         视口 621 → 打字卡 541×**940**（宽高比 **0.576**）← 断点一过就掉下去
         视口 660 → 0.616 ｜ 700 → 0.657 ｜ 740 → 0.697 ｜ 767 → 0.724
       也就是 **621～~690 这一段**：桌面版式（52px 键帽 + 十指图）放不下，
       620 以下那套小版式又够不着，卡片被拉成近 1.7 倍高的竖条。
       断点挪到 700 之后，桌面侧最差的一档是 719 → 0.676，仍在红线之上。
       （620 是 `.ln-*` 全族给"游戏板 / 间隙"定的档；打字卡这一屏内容更密，
        需要更宽的一档 —— 所以它**单独一档**，别的 `.ln-*` 一个字没动。）
     ★ 十指图（`.hd-pair`，svg 190×215、实测占 229px 高）窄屏**收掉**：
       它是**装饰** —— "该用哪根手指"这个可操作的信息，`.kbd-cap.is-target`
       的橙色光圈已经给出来了（那是老师点名要做的那一半）。
       229px 在小屏上换掉最划算，而**皮肤选择器留着** ——
       那是老师点名要的功能（"换了以后才能在自己的学习模式里使用"），
       功能与装饰撞车时让装饰退。
   ========================================================================== */
@media (max-width: 700px) {
  .ln-type.is-dark { padding: 16px 10px 14px; gap: 12px; border-radius: 12px; }
  .ln-type.is-dark .ln-word { font-size: 30px; }
  /* 字母格：64×84 → 38×50（字号跟着降到 24px）。
     ★ 一起降是因为它和 `.pc-slot` 一样是"输入目标"：只缩盒子不缩字，
       字母会顶到框边。 */
  .ln-type.is-dark .ln-type-boxes { gap: 6px; }
  .ln-type.is-dark .ln-type-box {
    width: 38px;
    height: 50px;
    font-size: 24px;
    border-radius: 9px;
  }
  /* 键盘：键帽 52 → 26 高、间距 7 → 4、内边距减半 —— 五行键帽省下约 130px。 */
  .ln-type.is-dark .kbd { --kbd-gap: 4px; padding: 8px 8px 10px; border-radius: 10px; }
  .ln-type.is-dark .kbd-cap { height: 26px; font-size: 10px; border-radius: 5px; }
  .ln-type.is-dark .kbd-wide { font-size: 9px; letter-spacing: 0; }
  /* 十指图两条实现都收掉：优先走 core/hands.js 的 `.hd-pair`（多皮肤 SVG），
     拿不到时才退回 kbd.js 自带的抽象指条 `.kbd-hands` —— 所以两条都要写。 */
  .ln-type.is-dark .hd-pair { display: none; }
  .ln-type.is-dark .kbd-hands { display: none; }
  .ln-type.is-dark .kbd-skins { margin-top: 6px; gap: 6px; }
}
