/* =========================================================
 * 坦克大战 · 样式表
 *
 * 【本文件用到的 CSS 特性与真实下限】
 * 本作的**源码语法下限**是 ES2017（Chrome 55 / Safari 11 一线，见 00-changelog.js），
 * 但那是**脚本**的口径；CSS 另有一套下限，本文件此前把两者混为一谈：
 *   CSS Grid          Chrome 57 / Safari 10.1  → Chrome 55 上完全没有 Grid
 *   flex gap          Chrome 84 / Safari 14.1  → 更早的浏览器上元素之间没有间距
 *   @container        Chrome 105 / Safari 16   → 窄屏那整块响应式修复在更早的浏览器上不生效
 *   inset 简写        Chrome 87 / Safari 14.1   → 用到的四处都写了四条长写兜底
 *   min()/max()       Chrome 79 / Safari 13.1   → 用到的地方都留了同选择器的长写兜底
 *   env()             Chrome 89 / Safari 15.4   → 用到的地方都写成 env(名字, 兜底值) 两参形式
 *   backdrop-filter   Chrome 76 / Safari 9      → 全项目只 #overlay 一处，带 -webkit- 前缀
 *   zoom              Chrome 55 / Safari 5      → **非标准**属性，但 Chrome / Safari 长期支持
 *                                                （Firefox 126 起才支持）。它是 v4.9.0 整套
 *                                                UI 缩放的支点：整体等比放大由它实现。
 *                                                ⚠ 认得 zoom 的浏览器**远多于**认得 min()
 *                                                的浏览器（见下一行的交集），而 #app 的
 *                                                宽度公式有两条（现代写法除以 --ui-scale、
 *                                                旧浏览器兜底行不除）—— 两者的前提必须
 *                                                对齐，见下面「#app 宽度公式」那段与
 *                                                04-dom-input.js 的 uiScaleAvailable()。
 *   container-type    Chrome 105 / Safari 16    → 与 @container 同档，整块容器查询不生效
 *   100dvh            Chrome 108 / Safari 15.4  → 不认则整条声明被忽略，沿用上一行的 100vh
 *   overscroll-behavior Chrome 63 / Safari 16   → 只是不再阻止滚动接力，不影响可用性
 *
 * 【两类「兜底」的失效方式完全不同 —— 混起来会写出假的安全感】
 *   ① **解析期失效**（min()/max()/inset 简写不认）：整条声明被**丢弃**，
 *      级联继续往下走，于是同规则里先写的那条长写兜底**照常生效**。
 *      —— 这就是本文件「长写在前、现代写法在后」这条规矩成立的前提。
 *   ② **计算值期失效**（env() 的变量名不认识）：声明**不被丢弃**，而是按
 *      CSS Cascading 3 变成「计算值期无效」→ 属性落回**初始值**（非继承属性
 *      即 initial，如 left/right/bottom → auto、padding → 0）。
 *      **同规则里先写的长写兜底在这一档完全不起作用** —— 级联已经选中了这条
 *      声明，只是它的计算值无效。
 *      Chrome 79~88 正好落在这一档：认得 min()/max()（79 起）与 env() 函数
 *      （69 起），却不认 safe-area-inset-* 变量（89 起才有）。
 *      所以 env() 的保底值**必须写在 env() 自己的第二个实参里**
 *      （env(safe-area-inset-top, 0px)），不能靠前一条长写兜底。
 *      v4.7.9 之前全项目 33 处 env() 都是单参写法：在 Chrome 79~88 上
 *      触屏控制层（摇杆 / 开火 / 暂停 / 装备 / 刹车）的 left/right/bottom
 *      全部变成 auto —— 五颗键一起堆到屏幕左上角，触屏档直接不可用。
 *      （门禁见 tools/tank-test.mjs 的「v4.7.9 CSS 兼容」。）
 * 也就是说：**Chrome 55 上布局会明显更挤**（模式卡纵排、HUD 标签可能竖排、
 * 间距全失），但功能仍可用；**Chrome 105 以前窄屏那套修复整块不生效**。
 * 为什么不给 @container 补 @media 等价档：那会把「同族栅格不许一半用视口轴、
 * 一半用容器轴」这条门禁打回原形，而两轴并不等价（844×390 的手机横屏：视口宽 844
 * 落「宽档」，而应用宽在 v4.6.5 紧凑布局下也只有 456、HUD 仍该走窄档）—— 按视口
 * 补一份等于把已经修好的比例适配重新弄坏。唯一的例外是手机整屏弹窗：它脱离 #app
 * 后菜单的可用宽度就是屏幕宽，因此弹窗在紧凑档自己当尺寸容器（见文件末尾）。
 * ========================================================= */
  :root{
    --bg:#f5f6f8;
    --panel:#ffffff;
    --line:#e7eaee;
    --line-strong:#d9dee5;
    /* 文本色阶按 WCAG AA 定：正文一律 ≥4.5:1（画布外最浅的页面底 #f5f6f8 上也得达标）。
     * --faint 原为 #98a2b3（白底 2.58:1 / 页面底 2.38:1），HUD 的「生命 / 装甲 / 得分」标签、
     * 页脚提示、图鉴小节名全是 11~12px 小字，实际读数连一半都不到；--dim 原为 #667085，
     * 让 faint 顶上来、dim 自己再降一档，两级层次保住、又都达标。
     * --gold-strong / --green-strong 只用于**文字**：--gold / --green 本身还是大标题
     * （h2.title 32px、.scoreBig 30px 属大字号，AA 只要求 3:1）和卡片装饰条的颜色，
     * 把它们一并压深会改掉装饰观感，而那不是这条标准的适用范围。
     * v4.9.10：--gold 由 #d97706 压到 #d37105（ΔRGB 6/6/1，肉眼看不出差别）。
     * 原值在**面板上真正出现的那层底色**上只有 2.94:1 —— 差一点点，达不到大字要求的 3:1。
     * 之前那句「现值 3.19 / 3.30 达标」是在**过关结算**那张卡上量的（底色 #fcfdfd），
     * 而强化三选一与回合结算两张卡的合成底色更深（#f5f6f7），同一枚 --gold 在那里就掉到
     * 2.94 —— 同一个颜色在不同面板上一过一不过，是「按最差的那个面板定值」的问题。
     * 压深后：#f5f6f7 上 3.16、#fcfdfd 上 3.36、页面底 #f5f6f8 上 3.17，三处都过 3:1。
     * --green 同样是擦线（#f5f6f7 上 3.05），暂未改动，但改不动之前先由门禁钉住。
     * v4.5.39：--faint 由 #667085 再压深到 #5f6b7d。原因是表面改成了半透明玻璃 ——
     * 文字压到的底色不再是纯白（面板 --glass .74 白 叠 遮罩 .62 浅灰 叠 战场底色，
     * 实测合成约 rgb(252,252,253)），#667085 在上面只剩 4.87:1，压深后回到 5.25:1，
     * 给「半透明」留出余量而不是刚好压线过。 */
    --text:#1b2432; --dim:#525f73; --faint:#5c6879;
    --gold:#d37105; --gold-strong:#92400e; --green-strong:#15803d;
    --blue:#2563eb; --red:#dc2626; --green:#16a34a; --purple:#7c3aed;
    /* 半透明同族色（芯片描边用）。原先是「var(--blue) 后面拼两位」—— 展开后是
     * #2563eb55 这种**八位十六进制**色值，要 Chrome 62+ 才认，在本作声明的解析
     * 下限（Chrome 55）上整条作废、描边退化成深灰。函数化 rgba 老浏览器一律认。 */
    --blue-a33:rgba(37,99,235,.33);
    /* --gold-a33 是 --gold 的 alpha 版手抄本（两者必须同步改：色一改，描边就与卡片对不上）。
       ⚠ CSS **没有行注释**：这里若写成 `// …`，解析器会把它当成一条非法声明，
       并把后面直到下一个分号的整条声明一起吞掉 —— 本项目真的踩过：
       `--gold-a33:…; // 说明` 吞掉了紧随其后的 `--accent:#2563eb`，
       于是 var(--accent) 变成未定义变量、background-color 在计算值阶段作废、
       .btn.primary 的底色当场变成透明（白字压在白底上）。 */
    --gold-a33:rgba(211,113,5,.33);
    --accent:#2563eb; --accent-weak:#eef4ff;
    /* --accent-line 原为 #d5e2fb（图标块的淡蓝描边）。玻璃化后图标块改用白色发丝
     * 描边 + 顶光，这个值全项目零引用 —— 留着就是一条「看起来还在用、其实早就断了」
     * 的承诺。删掉它比留着更诚实（门禁里有一条「零引用 token 即死 token」的断言）。 */
    /* ---------- 液态玻璃：表面 / 顶光 / 暗边 / 阴影（v4.5.39 引入） ----------
     * 全项目**只有 #overlay 一处**用 backdrop-filter（--glass-blur）；其余表面靠
     * 「半透明填充 + 顶光内阴影 + 底缘暗边」模拟折射。
     * 理由：backdrop-filter 每用在一处就多一个 backdrop root，每个都要回读并模糊
     * 其下内容 —— 把 6 张模式卡、4 张难度卡、3 张强化卡各糊一层，代价与掉帧风险
     * 都远大于那点折射感，而 #overlay 已经把战场磨成毛玻璃，卡片压在它上面本来
     * 就读得出「玻璃后面还有东西」。
     * --glass-sheen 是顶光渐变（不是颜色）：表面**底色**仍由 --glass 单独给，
     * 这样对比度门禁取到的 backgroundColor 仍是真实的不透明度，不会因为
     * 「background 简写把底色重置成 transparent」而量到空气。 */
    --glass:rgba(255,255,255,.74);        /* 面板 / 卡片表面 */
    --glass-solid:rgba(255,255,255,.88);  /* 胶囊 / 芯片这类小件：要压住底图 */
    --glass-sheen:linear-gradient(180deg, rgba(255,255,255,.62), rgba(255,255,255,0) 46%);
    --glass-line:rgba(255,255,255,.72);    /* 顶光描边 */
    --glass-hair:rgba(16,24,40,.09);       /* 底缘暗边 */
    --glass-shadow:0 1px 2px rgba(16,24,40,.05), 0 10px 26px rgba(16,24,40,.09);
    --glass-inset:inset 0 1px 0 rgba(255,255,255,.95), inset 0 -1px 0 rgba(16,24,40,.045);
    --glass-blur:12px;                     /* 全项目唯一的 backdrop-filter 半径 */
    /* 橙色文字：首领标签与触屏提示共用。
     * 原先是散在 11-ui.js 行内 style 里的 #ea580c —— 它压在浅底上只有 3.29~3.56:1
     * （AA 要 4.5:1），而那行文案**只在触屏设备上出现**，真浏览器门禁跑在桌面
     * Chrome 里根本量不到它。压深到 #9a3412 后是 6.8:1，与身份色同族但读得清。 */
    --orange-strong:#9a3412;
    /* ---------- 布局几何（v4.5.40 收敛成唯一来源） ----------
     * 这四个数是整套布局的骨架，v4.5.40 之前以字面量散在 #app 的注释与公式里。
     * 提成 token 后，改布局只改这一处；下面的 @container 档位与门禁的量法都以它为准。
     *   --app-max     显示宽上限。**运行时由 04-dom-input.js 的 publishAppMax() 按
     *                  「位图总分辨率预算 ÷ 设备 DPR」反推后写进 :root**（预算等效 DPR 3
     *                  → DPR1 时 2402px、DPR2 时 1202px、DPR3 时 802px），于是大屏上战场
     *                  能长大，而「位图 ≥ 显示设备像素」这条清晰度不变量仍由构造保证。
     *                  这里的 802px 是**无 JS 兜底值**（= 画布逻辑宽 800 + 舞台边框 2），
     *                  脚本没跑起来时行为与 v4.8.1 之前完全一致。
     *   --app-min     340px = HUD 内容的实测最小宽（实测 338px 刚好装满），再窄静音键出界。
     *   --app-reserve  112px = --hud-h-max(64) + --foot-h(30) + 18（舞台边框 2 + 入场位移 6
     *                  + 余量 10），**算**出来而不是写死 —— 改 HUD / 页脚高度时预留自动跟着走。
     *   --stage-ar    1.3333 = 4/3。 */
    --app-max:802px;
    --app-min:340px;
    --stage-ar:1.3333;
    /* UI 等比缩放（v4.9.0 第二步）。1 = 不缩放；>1 时 #app 整体 zoom，
     * 而宽度公式**除以 scale** —— 于是「渲染宽」不变，「设计宽」= 渲染宽 ÷ scale：
     *   · @container 阈值看到的是设计宽度（实测 zoom 不会缩放容器查询的判据），
     *     HUD / 面板档位不会因为整体放大而跳档；
     *   · getBoundingClientRect 返回渲染后的坐标，鼠标→画布映射照旧正确。
     * 绝对值由 04-dom-input.js 推导（自动档）或玩家在暂停面板选档（100/125/150%）。
     *
     * ⚠ 设计宽**不是常量 802**（这里曾经写错过，v4.9.3 更正）：宽度公式算的是
     *   渲染宽 = min(--app-max, 98vw, 高度约束)，设计宽 = 渲染宽 ÷ --ui-scale。
     * 而 --app-max 是**运行时**由 publishAppMax() 按位图预算反推写入的
     *   （DPR1 → 2402 / DPR2 → 1202 / DPR3 → 802 —— 802 只是无 JS 兜底值），
     *   98vw 与高度约束也随视口变化；三者取小再除以 scale，设计宽就随
     *   「视口 + 缩放档位 + 设备 DPR」三者浮动。
     * 真浏览器实测（1400×900 / DPR1）：100% 档 → 设计宽 1051（受高度约束，
     *   (900-112)×1.3333 所限）；150% 档 → 651；自动档 → 800。
     * 读代码或调栅格时别把 802 当成设计宽的恒定值。 */
    --ui-scale:1;
    /* HUD 与页脚的高度上限（实测三档：HUD 窄 56 / 中 60 / 宽 64；页脚恒 30）——
     * 布局预留从这几个数**算**出来，而不是写死一个 112：
     *   64 (HUD) + 30 (页脚) + 18 (舞台边框 2 + 开场位移 6 + 余量 10) = 112 */
    --hud-h:56px;
    --hud-h-max:64px;
    --foot-h:30px;
    --app-reserve:calc(var(--hud-h-max) + var(--foot-h) + 18px);
    /* 短横屏（手机横放 / 很矮的窗口）改写成 36 + 0 + 10 = 46~48px，
     * 战场高度利用率从 71% 提到 88% —— 见文件末尾的「手机横屏」媒体块。 */
  }
  *{box-sizing:border-box; margin:0; padding:0;}
  /* 只给读屏器的常驻播报节点（视觉隐藏，但不 display:none —— 那会退出无障碍树）。 */
  .sr-only{
    position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden;
    clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0;
  }
  html,body{height:100%;}
  body{
    /* touch-action: manipulation —— 允许页面滚动，但掐掉「双击缩放」。
       iOS 10 起浏览器忽略 viewport 的 user-scalable=no，放大只能靠这一条压住。
       画布另有一条 touch-action:none（见下），对局时手指拖动不会把页面滚走。 */
    touch-action:manipulation;
    background-color:var(--bg);
    /* 液态玻璃的「底」：三团极淡的色雾。静态渐变 —— 没有动画、没有滤镜、没有
     * background-attachment:fixed，绘制一次就固定下来，运行期零开销。
     * 玻璃要有东西可折射，页面底就是那东西：左上（蓝）/ 右上（琥珀）/ 下方（绿），
     * alpha 全部压在 13% 以内，不与界面文字抢注意力，也不改「浅色主题」的调子。
     * 固定背景（fixed）在这里是纯亏：body 是可滚动容器，fixed 会让渐变在滚动时
     * 每帧重新合成一次全屏。 */
    background-image:
      radial-gradient(1100px 620px at 8% -12%, rgba(37,99,235,.13), transparent 62%),
      radial-gradient(880px 520px at 98% 2%, rgba(217,119,6,.10), transparent 60%),
      radial-gradient(900px 640px at 46% 112%, rgba(16,163,74,.09), transparent 60%);
    color:var(--text);
    font-family:"PingFang SC","Microsoft YaHei","Noto Sans SC",system-ui,sans-serif;
    display:flex; align-items:center; justify-content:center;
    user-select:none; -webkit-user-select:none;
    /* 兜底可滚动：极端小屏（窄 + 矮）下内容仍可够到，而不是被裁掉 */
    overflow:auto;
  }
  /* 尺寸同时受宽度与高度约束。
   * 战场是固定 4:3（800×600），#hud / #foot 是固定像素高度，因此应用总高
   * ≈ 宽 × 0.75 + 100px。只按 98vw 定宽时，1280×720 / 1366×768 这类常见笔记本
   * （可视高约 600~680）会算出 690px 的应用高度 —— 配合原先的 body{overflow:hidden}
   * 结果是顶部 HUD 与底部页脚被直接裁掉、且无法滚动到（实测 1280×633：应用高 690、
   * 顶端 -28、竖向溢出 39px）。
   * 用 100vh 反推宽度上限后，整个应用恒好落在视口内。
   * 112px = HUD 上限(64) + 页脚(30) + 舞台边框(2) + 开场动画位移(6，见下方 appIn) + 10px 余量。
   * 这两个上限都是**结构性**的，不是"算得刚好"：#enemyIcons 用 nowrap（图标横向收缩
   * 而不是换行）、窄宽度下 HUD 整体收紧一档、#foot 用 nowrap + 省略号（见下方 #foot
   * 注释，它折行时实测能到 74px）。改这几处样式前请重跑
   * tank-battle/tools/tank-live-check.mjs 的「布局矩阵」一节（那里逐档量 HUD 实际高度）。
   * 1.3333 = 4/3，把「可用高度」换算成「可用的画布宽度」。
   * v4.9.0 起整条公式算的是**渲染宽度**（物理 px），再除以 --ui-scale 得到布局宽度：
   *   · --app-max 是渲染上限（JS 按位图预算写入）；
   *   · --app-min / --app-reserve 是设计尺寸，跟着整体缩放一起乘 scale ——
   *     否则 HUD / 页脚放大后会把预留吃掉，矮视口下被推出屏幕。
   * container-type 让下面的 @container 能按「设计宽度」切换栅格。 */
  #app{
    /* 下限 340px：min() 里没有下界时，(100vh - 112px) * 1.3333 在矮视口下会算出很小
       甚至为负的值（负值被钳成 0 = 整个应用凭空消失，body 的 overflow:auto 也救不回来，
       因为没有内容可滚）。340px 同时是 .stats 表（min-width:280px + 卡片内边距）不被
       .card 的 overflow:hidden 裁掉得分列所需的最低宽度。
       **这条下限防的是「内容被裁」，不是「出滚动条」**：横向永远不会滚，因为 98vw 是
       min() 里的独立一项（应用恒 ≤ 98vw）。下限的副作用是**竖向**溢出 —— 应用被顶到
       340 宽就意味着它至少 344 高，视口比这更矮时页面会出现竖向滚动条。
       这是结构性的、不是缺陷：340px 正是 HUD 内容的实测最小宽（实测 338px 装满，
       再窄 1px 静音键就被挤出右缘），降不下去。 */
    width:var(--app-max); max-width:98vw;
    /* 上一行是旧浏览器兜底（CSS min()/max() 不可用时仍得到一个可用的 802 宽应用；
       var() 自 Chrome 49 起可用，在本作声明的解析下限（Chrome 55）之内），
       下一行才是带视口高度约束的正式值。Safari 11 不支持 min()，若只留下一行，
       width 声明会整条失效、#app 退成 auto 宽，战场与 HUD 都会错位。
       注意兜底行**没有高度约束**（min()/max() 是把三个约束合并的唯一手段，没有它就
       写不出「同时满足宽度上限与高度上限」）—— 旧浏览器上矮视口会退化成竖向滚动，
       好在 body 的 overflow:auto 仍能滚到页脚，不会把内容裁没。

       ⚠ 兜底行**也没有除以 --ui-scale**，而下一行的 zoom 是无条件生效的 ——
       「布局宽 × zoom」于是就是渲染宽，兜底行与 zoom 放在一起就自相矛盾：
       认得 zoom 却不认 min() 的浏览器（Chrome 55~78 / Safari 11~13.0，**都在本作
       声明的支持区间内**）会把整个应用放大 scale 倍 —— 实测 1440×900 / DPR2 下
       自动档 1.264，兜底布局宽 = min(1202, 98vw) = 1202，渲染宽 = 1202 × 1.264
       = 1520 > 视口 1440：右侧一半界面在屏外、整页横向滚动。
       修法在 JS 侧：**认不得 min() 就不给缩放**（--ui-scale 恒 1，zoom=1，
       兜底行与 zoom 自洽，行为等于 v4.9.0 之前的形态）—— 见 04-dom-input.js 的
       uiScaleAvailable()。两条宽度公式与缩放闸必须同口径，改动任何一处都要同步。 */
    width:calc(min(var(--app-max), 98vw, max(calc(var(--app-min) * var(--ui-scale)), calc((100vh - var(--app-reserve) * var(--ui-scale)) * var(--stage-ar)))) / var(--ui-scale));
    /* 支持 dvh 的浏览器（Chrome 108+ / Safari 15.4+）改用**动态**视口高度：
       iOS Safari 的 100vh 是地址栏收起时的高度，竖屏下比可见区高出一截 ——
       应用被算宽后整体更高、页脚被推出屏幕。dvh 随地址栏实时收缩。
       不支持 dvh 的浏览器忽略这一整条声明，沿用上一行的 100vh（行为与改动前一致）。 */
    width:calc(min(var(--app-max), 98vw, max(calc(var(--app-min) * var(--ui-scale)), calc((100dvh - var(--app-reserve) * var(--ui-scale)) * var(--stage-ar)))) / var(--ui-scale));
    /* 整体等比缩放。放最后一行：上面两条 width 都是**布局宽度**，zoom 只负责渲染端乘倍数，
       两者合起来才等于「渲染宽 = min(--app-max, 98vw, 高度约束)」
       「设计宽 = 渲染宽 ÷ --ui-scale」—— 设计宽随视口与缩放档位浮动，不是常量。 */
    zoom:var(--ui-scale);
    container-type:inline-size;
    margin:auto; /* 与 body 的 overflow:auto 配合：溢出时对齐起点，内容不会被顶出可滚动区域 */
    animation:appIn .5s ease both;
  }
  /* 入场位移取 6px 而不是 10px：transform 造成的溢出同样计入可滚动区域，
     实测 1280×720 下 HUD 涨到 64 时，10px 位移会让页面在开场 0.5 秒里多出 2px
     竖向滚动（闪一下滚动条）。6px 时最坏情况仍有 10px 余量。 */
  @keyframes appIn{from{opacity:0; transform:translateY(6px);} to{opacity:1; transform:none;}}
  /* ---------- HUD ---------- */
  #hud{
    display:flex; align-items:center; justify-content:space-between;
    /* 玻璃表面：半透明白叠在页面底色雾上 → HUD 上能看见底下那点蓝 / 琥珀渐变。
     * 底色与顶光**分开写**（background-color / background-image），不合成 background
     * 简写 —— 简写会把底色重置成 transparent，对比度门禁取到的就是空气。 */
    background-color:var(--glass);
    background-image:var(--glass-sheen);
    border:1px solid var(--glass-hair); border-bottom:none;
    border-radius:14px 14px 0 0; padding:10px 16px; min-height:var(--hud-h);
    font-size:14px; letter-spacing:.5px;
    /* 顶光：上边缘一道 1px 高光。「玻璃」的读感主要来自这道高光 + 半透明，
     * 而不是模糊 —— 模糊要 backdrop-filter（贵），高光只要 inset box-shadow（便宜）。 */
    box-shadow:inset 0 1px 0 rgba(255,255,255,.9);
  }
  #hud .grp{display:flex; align-items:center; gap:12px; min-width:0; flex:none;}
  /* 窄屏 HUD 的组间分隔点：极值组合（99 命 / 20 装甲 / 6~8 位分数）会把
     三组之间的 4px 间隙压到几乎看不见，「分 999999第 1 关」读起来像一串。
     分隔点用绝对定位放在间隙里，不占布局宽度，也不改变任何 HUD 尺寸。 */
  #hudMid, #hudRight{position:relative;}
  @media (orientation:portrait){
    @container (max-width:500px){
      #hudMid::before, #hudRight::before{
        content:""; position:absolute; left:-3px; top:50%; width:2px; height:2px;
        margin-top:-1px; border-radius:50%; background:rgba(27,36,50,.38);
      }
    }
  }
  /* white-space:nowrap：**必须**有。HUD 里的标签都是 1~3 个字的短词（生命 / 装甲 / 得分 /
     第 / 波 / 围猎 / 坚守 / 首领 / 终极 / +N），逐字折断从来不是想要的效果。
     CJK 可以在任意字符间断行，所以只要标签盒比文字窄**一个像素**，「生命」就会变成
     竖着的「生」「命」两行 —— 而这不会撑高 HUD（行高不变）、不会横向溢出（盒宽不变），
     于是既有的三条断言（HUD 高度 / 内部不溢出 / 静音按钮宽度）全都量不出来。
     此前 nowrap 只写在 ≤360 那一档，实测漏掉了两段最常见的手机宽度：
       · 应用 382~392px（视口 390 / 393 / 400）：窄档收起了「敌军」标签但**没**收
         标签字号，标签盒 22~23px 装不下 23px 的「生命」→ 逐字竖排；
       · 应用 421~431px（视口 430 / 440）：跌出窄档后 letter-spacing 回到 1px，
         标签要 24px 而盒只有 23~24px → 同样逐字竖排。
     nowrap 放在**基线**上：flex 的 min-width:auto 会因此把标签盒撑到文字宽度，
     差的正是那 1px，不会挤压别处（最窄档 320 已实测：HUD 内部 scrollWidth 仍等于
     clientWidth，静音按钮 28px 完整落在应用内）。 */
  #hud .lbl{color:var(--faint); font-size:11px; letter-spacing:2px; white-space:nowrap;}
  /* nowrap 同样适用于数值：`❤ ×2` / `⬢ 2/2` 中间有一个空格，390px 视口下左栏只剩
   * 182px 时，flex 会把 b 压到 36px 并把两段折成上下两行（实测截图里生命值变成
   * 心形在上、×2 在下）。标签早已 nowrap，数值这一半当时漏了；两段都 nowrap 后
   * 实测 HUD 内部仍是 380/380，不产生横向溢出。改窄屏 HUD 前请重跑布局矩阵。 */
  #hud b{color:var(--gold-strong); font-size:17px; font-weight:800; font-variant-numeric:tabular-nums; white-space:nowrap;}
  #hud b.life{color:var(--red);}
  #hud b.armor{color:var(--green-strong); font-size:14px;}
  #enemyIcons{display:flex; flex-wrap:nowrap; gap:3px; max-width:180px;}
  #enemyIcons i{
    position:relative; width:12px; height:9px; display:inline-block;
    background:#ef4444; border-radius:2px;
    /* 不许收缩：图标宽度一旦被 flex 压到 7px 左右，坦克形状就退化成一根红色细条，
       又回到"读起来像血条而不是剩余敌军数"的老问题。放不下时改由 JS 少显示几个
       （enemyIconCap() 按 .hudMeter 的宽度预算反推），而不是把每个压扁。 */
    flex:none;
  }
  /* 炮管朝上居中。旧写法把炮管向右伸出 4px，而图标间距只有 3px —— 相邻图标的
     炮管正好把间隙填满，9 个图标在视觉上连成一根红条，读起来像血条而不是
     "剩余敌军数"。改成纵向伸出后不再侵占横向间隙，同时保留原有的 12px 图标 +
     3px 间距宽度预算（ENEMY_ICON_CAP 与 .hudMeter 的宽度就是按这个预算反推的）。 */
  #enemyIcons i::after{
    content:""; position:absolute; left:50%; top:-3px; margin-left:-1.5px;
    width:3px; height:5px; background:#b91c1c; border-radius:1px;
  }
  #muteBtn{
    background-color:rgba(255,255,255,.55); background-image:none;
    color:var(--dim);
    border:1px solid var(--glass-hair); border-radius:9px;
    /* flex:none 放在基线：HUD 内容在极值下变宽时，静音键绝不能被 flex 压扁
     * （430 视口曾实测被压到 18px；窄档已在 ≤500 再收到 28px）。 */
    flex:none;
    width:36px; height:32px; cursor:pointer; font-size:16px; line-height:1;
    display:grid; place-items:center;
    box-shadow:inset 0 1px 0 rgba(255,255,255,.85);
    /* 显式列举取代 transition:all：本按钮的宽高在 @container(≤500/≤360) 两档会变，
     * all 会把 width / height 也变成可动画项 —— 跨断点时按钮先缩一半再到位，
     * 而这两个属性根本不参与视觉过渡，只是白白触发每帧重排。 */
    transition:color .18s ease, border-color .18s ease, background-color .18s ease;
  }
  #muteBtn:hover{color:var(--text); border-color:var(--line-strong); background-color:rgba(255,255,255,.8);}
  /* ---------- 舞台 ---------- */
  #stage{
    position:relative; border:1px solid var(--glass-hair); border-radius:0 0 14px 14px;
    overflow:hidden; background:#eef1f5; line-height:0;
    /* 两层阴影：贴地的一层给出「板」的厚度，上面一层把整块战场托离页面。
     * 只改阴影不改盒模型 → #app 的高度预算（默认 112px，短横屏 48px）不受影响。 */
    box-shadow:0 1px 2px rgba(16,24,40,.05), 0 16px 40px rgba(16,24,40,.12);
  }
  canvas{
    display:block; width:100%; height:auto; cursor:crosshair;
    /* 画布上不让浏览器接管手势：手指按在战场上时既不能把页面滚走，也不能双击缩放。
     * 只在画布上掐掉 —— 菜单弹窗仍要能正常滚动。
     * **touch-action:none 还有一层作用**：按 Pointer Events 规范，touch-action 为 none
     * 时浏览器不再为这次触摸派发「兼容鼠标事件」（mousedown / mousemove / mouseup）。
     * v4.7.0 之后画布不再接收驾驶触摸（驾驶走屏幕摇杆），可这条兼容路径仍会把
     * 「手指按在战场上」变成一次鼠标按压 + 一次指针移动 —— 真浏览器实测确认过：
     * 画布有 touch-action:none 时只收到 touchstart / touchmove / touchend，
     * 没有任何 mousedown / mousemove。去掉这条规则，鼠标档在纯触屏设备上会被
     * 误触战场一路拖着走（手指抬起后指针坐标还冻在最后一点，坦克继续冲）。 */
    touch-action:none;
  }
  /* ---------- 遮罩层 ---------- */
  #overlay{
    position:absolute;
    /* inset 是 Chrome 87 / Safari 14.1 才支持的简写；本作声明的解析下限是
       Chrome 55 / Safari 11 一线。只写 inset 会让旧浏览器上遮罩层退成
       position:absolute + static 偏移（即留在文档流末尾、被 #stage 的 overflow:hidden 裁掉），
       表现为「只有战场、没有任何菜单」。四条长写在前，inset 只作现代浏览器的收尾。 */
    top:0; right:0; bottom:0; left:0; inset:0;
    display:none; line-height:normal;
    /* 全项目**唯一**的 backdrop-filter 站点（其余表面只靠半透明 + 顶光模拟折射）。
     * 遮罩底色从 .88 降到 .84：磨砂负责藏细节，透明度负责透出颜色，压太实就白磨了。
     * .84 是**量出来**的：菜单的散排文字坐在遮罩上，遮罩后面是真实战场 ——
     * 战场最深的实心色是钢墙描边 #6e7b8b，压过它时这层遮罩合成的底色约 L=0.86，
     * --faint 压在上面 4.99:1、--accent 4.55:1，都还在 AA 之上；而再压到 .80 时
     * --accent 就掉到 4.4x（贴着线）。半径走 token，别再写死 ——
     * 写死就会出现「注释说 12px、代码是 8px」这种假账。 */
    background-color:rgba(240,243,247,.84);
    backdrop-filter:blur(var(--glass-blur)); -webkit-backdrop-filter:blur(var(--glass-blur));
    flex-direction:column; align-items:center; justify-content:flex-start; text-align:center;
    padding:22px; overflow:auto;
    /* 弹窗滚到底后不要把滚动接力给 body：整屏弹窗背后的页面不该跟着动。 */
    overscroll-behavior:contain;
  }
  #overlay.show{display:flex; animation:ovIn .28s ease both;}
  @keyframes ovIn{from{opacity:0;} to{opacity:1;}}
  /* 主菜单 / 房间面板的玻璃底板。
   * 这一层不只是装饰，还是**对比度兜底**：菜单里有几段文字（难度说明 / 图鉴小节名 /
   * 最高分 / 提示）直接坐在遮罩上，而遮罩后面是**真实战场** —— 战场是浅色系，磨砂后
   * 仍偏亮，但压过浅橙色的砖墙时底色实测约 L=0.79，--faint 压在上面只有 4.3:1
   * （AA 要 4.5:1）。给整块面板垫一层 .55 的白板，最差情况回到 4.87:1；
   * 顺带把「菜单」收成一个视觉整体，而不是一堆浮在战场上的文字。
   * 底色与顶光**分开写**：background 简写会把底色重置成 transparent，
   * 对比度门禁取到的就成了空气。 */
  .ov-col{
    margin:auto; display:flex; flex-direction:column; align-items:center; width:100%;
    max-width:740px; padding:22px 26px 26px; border-radius:22px;
    background-color:rgba(255,255,255,.60);
    background-image:linear-gradient(180deg, rgba(255,255,255,.80), rgba(255,255,255,.48));
    border:1px solid rgba(255,255,255,.55);
    box-shadow:0 24px 60px rgba(16,24,40,.14), inset 0 1px 0 rgba(255,255,255,.9);
  }
  /* 面板不得被 flex 容器压缩。暂停 / 结算 / 房间面板都是 #overlay（flex column +
     overflow:auto）的直接子项，默认 flex-shrink:1 会在「弹窗比面板矮」时把面板压到
     容器高度；.card 又带 overflow:hidden，于是下半截（主菜单 / 静音 / 音量 /
     减弱动态效果）被裁掉且不可滚动 —— 实测 320×568 下 .card clientHeight 208 /
     scrollHeight 757，#overlay scrollHeight 也停在 234；375×667 同样裁掉设置行；
     900×560 裁掉最后 33px。flex-shrink:0 让面板保持自然高度，溢出的部分由
     #overlay 滚动（margin:auto 在空间足够时仍负责居中）。 */
  .card, .ov-col{flex-shrink:0;}
  /* 入场动画只在「换了一张界面」时播放：ov-enter 由 showOverlay 按面板标签是否变化
     决定。整块 innerHTML 每次重绘都会创建新 .card，若只按选择器挂动画，房间 / 强化
     面板在 200ms 节流的洪泛下每 200ms 重播一次淡入，画面永远停在半透明；暂停面板
     点一次音量也会整块重新淡入。同一张面板就地重绘应当即时完成。 */
  #overlay.show.ov-enter .card, #overlay.show.ov-enter .ov-col{animation:cardIn .34s cubic-bezier(.2,.9,.3,1.1) both;}
  /* 模式卡 / 继续挑战的入场错峰也必须受 ov-enter 约束。此前只给 .card 加了这道闸，
     而 .modes button / .btn.cont 的 animation 写在自身规则里、无条件命中：
     玩家点一次难度卡片（showMenu → showOverlay 同一张面板就地重绘），整排模式卡
     会重新从 opacity:0 往上飘，最后一张还要等 0.30s 延迟 —— 面板在 0.75s 内
     处于「按钮暂时看不见、但已经可点」的状态，与「同一面板重绘必须即时可见」
     的约定相反。选择器与 .card 同源，首次打开 / 换面板时动画与错峰照旧。 */
  #overlay.show.ov-enter .modes button, #overlay.show.ov-enter .btn.cont{animation:modeIn .45s ease both;}
  /* 强化卡（.upcard）与自定义难度面板（.customBox）的入场动画同样收进 ov-enter 闸。
     它们原先写在自身规则里、无条件命中：玩家点一次难度卡（showMenu → showOverlay
     同一张面板就地重绘），整块自定义区会重新从 opacity:0 淡入，半秒内控件看不见
     却仍可点 —— 与上面 .card / .modes button 修的是同一个 bug，只是漏了这两条选择器。
     首次打开 / 换面板时动画照旧（ov-enter 由 showOverlay 按面板标签是否变化决定）。 */
  #overlay.show.ov-enter .upcard{animation:cardIn .4s cubic-bezier(.2,.9,.3,1.1) both;}
  #overlay.show.ov-enter .customBox{animation:cardIn .3s ease both;}
  @keyframes cardIn{from{opacity:0; transform:scale(.97) translateY(8px);} to{opacity:1; transform:none;}}
  .card{
    margin:auto;
    /* 玻璃面板：半透明白压在磨砂遮罩上。底色与顶光分开写（background 简写会把底色
     * 重置成 transparent，对比度门禁取到的就成了空气）。 */
    background-color:var(--glass);
    background-image:var(--glass-sheen);
    border:1px solid var(--glass-line); border-radius:16px;
    padding:26px 36px;
    /* 两层外阴影（托起）+ 两层内阴影（顶光 / 底缘暗边）—— 玻璃的厚度感来自这四层，
     * 不来自模糊。position / overflow 原本写在下面「卡片顶部高光条」那一段，
     * 合并到此处，避免玻璃化改写时只改到其中一处。 */
    box-shadow:var(--glass-shadow), var(--glass-inset);
    position:relative; overflow:hidden;
  }
  /* ---------- 标题 ---------- */
  .logo{
    font-size:44px; font-weight:900; letter-spacing:14px; text-indent:14px;
    color:var(--text);
  }
  /* 房间号既可读也可点：大号数字本身就是复制按钮（见 09-net-save.js copyRoomCode）。
     用户选中文本被全局 user-select:none 禁掉，这里单独放开，复制失败时还能手动拖选。 */
  .roomCode{
    background:none; border:0; padding:0; font-family:inherit; color:inherit;
    cursor:copy; user-select:text; -webkit-user-select:text;
  }
  .roomCode:disabled{cursor:default;}
  .roomCode:focus-visible{outline:2.5px solid var(--accent); outline-offset:6px; border-radius:8px;}
  /* 房号输入框的可视焦点：原先把 outline 内联清掉又没有替代样式，Tab 回来时只剩光标。
   * 这里仍不写 border-color：输入框的 `border:1.5px solid var(--line-strong)` 是行内
   * 样式（09-net-save.js 拼 HTML 时带的），行内声明优先级更高，写了也不会生效。
   * 旧写法用 18% 透明蓝的 box-shadow，合成到浅底后对比只有约 1.3:1（远低于 1.4.11
   * 对焦点指示要求的 3:1），在低对比屏幕上几乎看不见。改用不透明 accent 的 2px 外环
   * （对白底约 4.8:1），与 .chip:focus-visible / .roomCode:focus-visible 同口径。 */
  #roomNo:focus{
    outline:2px solid var(--accent);
    outline-offset:2px;
  }
  .sub{color:var(--dim); letter-spacing:5px; margin:8px 0 16px; font-size:12px; display:flex; align-items:center; gap:14px; justify-content:center;}
  .sub::before,.sub::after{content:""; height:1px; width:64px; background:var(--line-strong);}
  /* ---------- 玻璃表面：模式卡 / 强化卡 / 难度卡 / 通用按钮 / 胶囊共用 ----------
   * 这五条规则原先各写一遍「底色 + 描边 + 圆角 + cursor + font-family + transition」，
   * 抄了 5 份。玻璃化改写要动 5 处 —— 漏一处就得到「一半玻璃面、一半旧底色」的
   * 半成品（这坑历史上真踩过：.hudBadge 的黑纱差点这么没掉）。
   * 收敛成一条共享规则：共用属性只写一遍；圆角 / 字号 / 内边距仍留在各自规则里
   * —— 那几项差异是设计决定，不该被「归一」掉。
   *
   * transition 改成**显式列举**：原先的 transition:all 会把 width / height / min-width /
   * padding 这些**会触发重排**的属性也变成可动画项 —— 拖窗口跨过断点时静音按钮的
   * 尺寸会被补间动画（按钮先缩一半再到位），而这些属性根本不参与视觉过渡。
   * box-shadow 也**不**进列表：阴影不能走 GPU 合成，动画它会每帧重绘阴影覆盖的
   * 整块区域；摘出后 hover 只剩 transform（合成器独占）+ 描边色（便宜）。 */
  .modes button, .upcard, .dcard, .btn, .cPill{
    background-color:var(--glass);
    background-image:var(--glass-sheen);
    border:1px solid var(--glass-line);
    color:var(--text);
    font-family:inherit; cursor:pointer;
    box-shadow:var(--glass-shadow), var(--glass-inset);
    transition:transform .16s ease, border-color .16s ease, background-color .16s ease, color .16s ease;
  }
  /* ---------- 按钮 ---------- */
  .modes{display:grid; grid-template-columns:repeat(2, minmax(210px, 1fr)); gap:12px; margin-bottom:12px; width:94%; max-width:580px; width:min(580px, 94%);}
  .modes button{
    border-radius:14px; padding:12px 13px; font-size:17px; font-weight:800; letter-spacing:2px;
    display:flex; align-items:center; gap:12px; text-align:left;
  }
  .modes button:hover{transform:translateY(-2px); border-color:var(--accent); box-shadow:0 14px 30px rgba(37,99,235,.16), var(--glass-inset);}
  .modes button:active{transform:translateY(0);}
  /* 错峰延迟必须与 animation 写在**同一条作用域**里，并且让 n 阶选择器的
     特异度高于上面那条 animation 简写：简写会把 animation-delay 重置为 0，
     只留 `.modes button:nth-child(n)` 的话会被 `#overlay.show.ov-enter …`
     压过去（实测六张卡读到 [0,0,0,0,0,0]，错峰形同删除）。 */
  #overlay.show.ov-enter .modes button:nth-child(2){animation-delay:.06s;}
  #overlay.show.ov-enter .modes button:nth-child(3){animation-delay:.12s;}
  #overlay.show.ov-enter .modes button:nth-child(4){animation-delay:.18s;}
  /* 菜单实为 6 张模式卡（11-ui.js 主菜单），原先只排到第 4 张：在线合作 / 在线对战
     与第 1 张同时入场，比第 4 张还早 0.18s，错峰在最后两行断掉。 */
  #overlay.show.ov-enter .modes button:nth-child(5){animation-delay:.24s;}
  #overlay.show.ov-enter .modes button:nth-child(6){animation-delay:.30s;}
  @keyframes modeIn{from{opacity:0; transform:translateY(10px);} to{opacity:1; transform:none;}}
  .modes .ico{
    flex:none; width:42px; height:42px; display:grid; place-items:center;
    border-radius:11px; font-size:22px; color:var(--accent);
    /* 图标块：淡蓝底 + 斜向顶光，是整套玻璃语言里最小的一颗「玻璃珠」 */
    background-color:#eef4ff;
    background-image:linear-gradient(160deg, rgba(255,255,255,.92), rgba(219,234,254,.30));
    border:1px solid rgba(255,255,255,.75);
    box-shadow:inset 0 1px 0 rgba(255,255,255,.95), 0 2px 6px rgba(16,24,40,.07);
    transition:border-color .18s ease;
  }
  .modes button:hover .ico{border-color:var(--accent);}
  .modes .txt{display:flex; flex-direction:column; min-width:0;}
  .modes .txt b{font-size:17px; font-weight:800; letter-spacing:2px;}
  /* 文案区实际只有约 200px：字号 12px + 字距 1.5px 时「保卫基地 · 首领关卡 · 肉鸽强化」
     需要约 210px，于是该卡副标题折成两行、四张卡视觉不齐。收紧字号与字距后
     四条副标题都能单行放下（改动前请用无头浏览器复核四张卡是否等高）。 */
  .modes .txt span{display:block; font-size:11.5px; font-weight:400; color:var(--dim); margin-top:5px; letter-spacing:.6px;}
  /* 操作方式选择器：键盘 / 鼠标 / 触屏三张卡，强制两列并整体居中。
     不随 @container(≤640px) 塌成单列 —— 那种竖排占满整行的形态正是它看起来
     「不居中」的原因（.modes.pick 特异度高于容器查询里的 .modes）。
     触屏那张占满一整行（grid-column:1/-1）：它是**另一种设备**而不是第三种键位，
     单独一行反而比挤成两列更好认。 */
  .modes.pick{grid-template-columns:repeat(2,minmax(0,1fr)); width:92%; max-width:520px; width:min(520px,92%); margin-inline:auto;}
  .modes.pick .touchCard{grid-column:1 / -1;}
  .modes.pick .txt span{white-space:normal;}
  /* ---------- 继续挑战 ---------- */
  /* 与 .modes 同宽的独立一行，而不是当第五张卡塞进栅格 —— .modes 是 repeat(2, …)，
     第五张卡会单独占半行、把四张卡的视觉齐整打破。
     摘要（模式 + 关卡 + 得分 + 难度）可能很长，所以 .meta 用省略号收尾而不是折行：
     折行会把按钮撑高，而 #app 的高度预算是按“每块内容高度可预期”算的（见 #app 注释）。
     选择器用 .btn.cont 而不是 .cont：与 `.btn .ic` 同特异度时取决于前后顺序，
     提高一级就不靠“恰好写在后面”了。 */
  .btn.cont{
    width:94%; max-width:580px; width:min(580px, 94%); margin-bottom:12px;
    display:flex; align-items:center; justify-content:center; gap:9px;
  }
  .btn.cont .ic{margin-right:0; font-size:18px;}
  .btn.cont .meta{
    min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
    font-weight:400; font-size:12px; letter-spacing:.5px; opacity:.82;
  }
  /* ---------- 居中可换行行：六个容器共用同一套 flex 口径 ----------
   * .diffRow / .rowbtns / .legend / .tstat / .tstatCol .tstatRow / .setRow
   * 原先各写一遍「display:flex + flex-wrap:wrap + justify-content:center」，
   * 抄了 6 份。合成一条后它们换行行为必然一致 —— 抄本漂移过一次就让
   * 「属性行从中间断行」这类问题出现过（见 .tstat 的注释）。 */
  .diffRow, .rowbtns, .legend, .tstat, .tstatCol .tstatRow, .setRow{
    display:flex; flex-wrap:wrap; justify-content:center;
  }
  /* ---------- 难度 ---------- */
  .diffRow{align-items:center; gap:8px; margin-bottom:6px;}
  .diffRow .lbl{color:var(--faint); font-size:12px; letter-spacing:3px;}
  /* 胶囊按钮本体是 .cPill（自定义难度 / 操控模式 / 坦克配置共用，见 cRow 那节）。 */
  .diffDesc{color:var(--dim); font-size:12px; letter-spacing:1px; margin-bottom:16px; min-height:18px;}
  .diffDesc b{color:var(--accent); letter-spacing:2px; margin-right:8px;}
  /* ---------- 通用按钮 ---------- */
  .btn{
    border-radius:11px; padding:12px 22px; font-size:16px; font-weight:700; letter-spacing:1px;
    min-width:132px;
  }
  /* hover 换成淡蓝底时要把顶光渐变摘掉：白色顶光叠在淡蓝上会让这块底偏亮，
   * 而 .btn.mini.on / .btn.off 也走同一条路，统一写成 background-image:none。 */
  .btn:hover{border-color:var(--accent); color:var(--accent); background-color:var(--accent-weak); background-image:none;}
  .btn:active{transform:translateY(1px);}
  /* 主按钮：实底 + 顶部一道内高光 + 品牌色外发光。
   * **不用渐变底**：白色 16px 粗体压在渐变最亮的一端只有 3.7:1（AA 要 4.5:1），
   * 而 background 简写一旦被谁改掉，底色就跟着回退、探针量到的还是 --accent ——
   * 实底 + 内高光既达标又稳。 */
  .btn.primary{
    background-color:var(--accent); background-image:none;
    border-color:var(--accent); color:#fff;
    box-shadow:0 6px 18px rgba(37,99,235,.28), inset 0 1px 0 rgba(255,255,255,.28);
  }
  .btn.primary:hover{background-color:#1d4ed8; border-color:#1d4ed8; color:#fff;}
  /* 不可点状态必须看得出来。房间面板用 disabled 属性 + .off 类标记「还没轮到」的主按钮
     （双方未就绪时的「开始对战」、客人未准备时的「准备」），但此前 .btn.primary 的
     background/color 是作者声明，优先级压过 UA 的灰化渲染，而 .btn:hover 仍命中 ——
     玩家看到一颗亮蓝、鼠标一放还变深、点下去毫无反应的按钮，只能靠猜。
     .off 类名一直是悬空的（CSS 里没有对应规则），这里一并补上类和属性两条选择器。 */
  .btn:disabled,.btn.off{opacity:.45;cursor:not-allowed;}
  .btn:disabled:hover,.btn.off:hover{background-color:var(--panel); background-image:none; border-color:var(--line-strong); color:var(--text);}
  .btn.primary:disabled:hover,.btn.primary.off:hover,.btn.primary.off{background-color:var(--accent); background-image:none; border-color:var(--accent); color:#fff;}
  .btn .ic{margin-right:7px; vertical-align:-.15em;}
  /* ---------- 统一图标 ---------- */
  .ic{
    display:inline-block; width:1em; height:1em; flex:none; vertical-align:-.125em;
    fill:none; stroke:currentColor; stroke-width:1.8; stroke-linecap:round; stroke-linejoin:round;
  }
  .ic.f{fill:currentColor; stroke:none;}
  button:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}
  /* #overlay 是常驻容器，showOverlay() 在没有具体控件可聚焦时（主菜单）把焦点落在它身上，
     好让键盘用户不必从页面头重新 Tab。但给一个 600px 高、内容可滚到 1095px 的滚动容器
     套上 UA 默认焦点环，看起来就像画坏了（实测整块战场被一个蓝框圈住）。
     容器本身不是可交互对象，这里抑制焦点环；用户一按 Tab 就落到第一张模式卡上，
     那时仍有上面那条 :focus-visible 环。 */
  #overlay:focus{outline:none;}
  /* 画布同样抑制 UA 默认焦点环：面板收起后焦点交还战场（见 11-ui.js 的 hideOverlay），
   * 若留着默认环就是「整块战场被一个蓝框圈住」—— 与上面 #overlay 同一条理由。 */
  #game:focus{outline:none;}
  #overlay::-webkit-scrollbar{width:8px;}
  #overlay::-webkit-scrollbar-thumb{background:rgba(27,36,50,.18); border-radius:99px;}
  /* ---------- 徽记 / 徽章 ---------- */
  .emblem{
    width:52px; height:52px; margin:0 auto 12px; display:grid; place-items:center;
    border-radius:17px; font-size:32px; color:var(--accent);
    /* 徽记 = 最大的一颗玻璃珠：淡蓝底 + 斜向顶光 + 一圈内高光 */
    background-color:#eef4ff;
    background-image:linear-gradient(160deg, rgba(255,255,255,.95), rgba(219,234,254,.35));
    border:1px solid rgba(255,255,255,.8);
    box-shadow:inset 0 1px 0 rgba(255,255,255,.95), 0 4px 12px rgba(16,24,40,.08);
  }
  .badge-new{
    display:inline-block; margin-left:8px; vertical-align:middle;
    background:#fef3c7;
    border:1px solid #fcd34d; color:#92400e;
    font-size:11px; font-weight:800; letter-spacing:2px;
    padding:2px 10px; border-radius:999px;
  }
  /* ---------- 帮助 / 信息 ---------- */
  .help{
    background-color:rgba(255,255,255,.55);
    background-image:linear-gradient(180deg, rgba(255,255,255,.5), rgba(255,255,255,.16));
    border:1px solid rgba(255,255,255,.6); border-radius:12px;
    box-shadow:inset 0 1px 0 rgba(255,255,255,.85);
    padding:14px 20px; font-size:13px; color:var(--dim); line-height:2.0; text-align:left;
    max-width:600px;
  }
  .help b{color:var(--text);}
  /* 紧凑版：暂停面板里放一份按模式区分的操作说明。暂停面板本来就在 #overlay 里，
     高度不够时可以滚动，但仍希望它尽量不滚 —— 收紧行高与内边距后 3 行约 70px。
     上边距放在这里（而不是 showPause 的 inline style）：短横屏的密度规则要能覆盖它。 */
  .help.compact{line-height:1.8; padding:10px 15px; font-size:12.5px; max-width:520px; margin-top:12px;}
  .help .k{
    display:inline-block; background:var(--panel);
    border:1px solid var(--line-strong); border-bottom-width:2px;
    border-radius:6px; padding:1px 8px; margin:0 3px; font-size:12px; color:var(--text); font-weight:700;
  }
  .high{margin-top:16px; color:var(--dim); font-size:13px; letter-spacing:3px;}
  .high b{color:var(--gold-strong);}
  .legendHead{margin-top:14px; color:var(--faint); font-size:11px; letter-spacing:4px;}
  .legend{margin-top:8px; gap:6px; max-width:620px;}
  .chip{
    display:inline-flex; align-items:center; gap:6px;
    /* 芯片是玻璃面板上最小的一层：半透明白 + 顶光。描边用**深色**发丝而不是白色 ——
     * 白色描边压在浅色玻璃板上等于没有边（1.1:1），芯片会糊成一团看不出是几颗。 */
    background-color:var(--glass-solid);
    background-image:linear-gradient(180deg, rgba(255,255,255,.55), rgba(255,255,255,0) 60%);
    border:1px solid rgba(16,24,40,.12); border-radius:999px;
    box-shadow:inset 0 1px 0 rgba(255,255,255,.9);
    padding:3px 11px 3px 8px; font-size:12px; color:var(--dim); letter-spacing:1px;
    transition:border-color .16s ease, background-color .16s ease, color .16s ease;
  }
  .chip:hover{border-color:var(--line-strong); background-color:rgba(255,255,255,.9); background-image:none; color:var(--text);}
  /* 图鉴条目带 tabindex，键盘 Tab 停上来时必须有可见落点（否则「焦点在哪儿」不可见）。
     与 hover 同款高亮 + 一圈 outline；:focus-visible 只在键盘聚焦时命中，鼠标用户零差别。 */
  /* 焦点环用不透明的 --accent（#2563eb 对白底 ≈5.1:1，满足非文本对比度 3:1）。
     原先是 55% 不透明度的同一主色，叠在浅色玻璃面板上约 2.3:1 —— 低于 1.4.11
     对焦点指示的要求，而按钮正是弹窗里最主要的 Tab 站点。`.chip` 早已改用不透明
     主色，这条与它、`#roomNo`、`.roomCode` 统一同一口径。 */
  .chip:focus-visible{border-color:var(--accent); background:#f8fafc; color:var(--text); outline:2.5px solid var(--accent); outline-offset:2px;}
  .chip .g{font-weight:800; font-size:13px;}
  .chip.equip{border-color:#fcd34d; background:#fffbeb;}
  /* 道具图鉴自定义悬浮提示：原生 title 的弹出延迟由浏览器/系统控制（约 1.5 秒、不可调），
     换成自绘浮层把等待时间固定为 1 秒（见 showChipTipLater / bindOverlay）。 */
  #chipTip{
    position:fixed; left:0; top:0; z-index:99; pointer-events:none;
    max-width:300px; padding:6px 11px; border-radius:8px;
    background:rgba(15,23,42,.92); color:#f8fafc; font-size:12px; letter-spacing:1px;
    box-shadow:0 8px 22px rgba(15,23,42,.25);
    opacity:0; transform:translateY(4px);
    transition:opacity .16s ease, transform .16s ease;
  }
  #chipTip.show{opacity:1; transform:translateY(0);}
  h2.title{font-size:32px; letter-spacing:8px; margin-bottom:12px; font-weight:900; color:var(--text);}
  h2.title::after{
    content:""; display:block; width:56px; height:2px; margin:10px auto 0;
    background:var(--line-strong); border-radius:1px;
  }
  h2.title.win{color:var(--green);}
  h2.title.lose{color:var(--red);}
  h2.title.gold{color:var(--gold);}
  .scoreBig{font-size:30px; font-weight:900; letter-spacing:4px; margin:6px 0 2px;}
  .scoreBig .b{color:var(--blue);}
  .scoreBig .y{color:var(--gold);}
  .stats{margin:14px auto 18px; border-collapse:collapse; font-size:14px; min-width:280px;}
  .stats td{padding:6px 20px; border-bottom:1px solid #eef0f4; color:var(--dim); letter-spacing:1px;}
  .stats tr:last-child td{border-bottom:none;}
  .stats td:last-child{color:var(--text); font-weight:800; text-align:right; font-variant-numeric:tabular-nums;}
  .tip{color:var(--faint); font-size:12px; margin-top:14px; letter-spacing:2px;}
  .rowbtns{gap:14px; margin-top:10px;}
  #foot{display:flex; justify-content:space-between; gap:10px; flex-wrap:nowrap; color:var(--faint); font-size:12px; padding:10px 8px 2px; letter-spacing:1.5px; border-top:1px solid #eef0f4; margin-top:2px;}
  /* 页脚恒定一行。#app 的宽度是按视口高度反推的（见上），页脚一旦折行就会把这十几
     像素算漏：实测对战那行文案在 1024×600 下折成 3 行（30 → 74px），把整个应用顶出
     视口 14px。改为不换行 + 左侧提示单行省略号收尾 —— 窄屏下宁可省略也不要把战场
     挤出屏幕。完整操作说明另有三处可查：开始菜单的「帮助」区、开局画布上的提示、
     以及**暂停面板底部的按模式说明**（controlHelp()，P / Esc 随时可开）。
     右侧版本号不参与收缩，保证始终完整可见。 */
  #foot span{min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
  /* 左侧版权 / 右侧版本号不参与收缩（始终完整可见），中间动态提示占满余量并单行省略。 */
  #footCopy{flex:none; letter-spacing:.5px; margin-right:14px;}
  #footL{flex:1 1 auto;}
  /* 右侧版本号不参与收缩（始终完整可见），所以它的宽度直接决定左侧提示能有多少空间。
     收紧字距省下约 15px —— 左侧提示在 1024×600 下正好卡在临界值，这 15px 是余量。 */
  #foot span:last-child{flex:none; letter-spacing:.5px;}
  /* ---------- 肉鸽强化卡 ---------- */
  .cards{display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:12px; margin:14px 0 6px; width:94%; max-width:660px; width:min(660px,94%);}
  .upcard{
    position:relative; display:flex; flex-direction:column; align-items:center; gap:7px;
    border-radius:14px;
    padding:16px 12px 13px;
  }
  /* 错峰延迟必须与 animation 同在 ov-enter 闸内，且特异度高于那条 animation 简写
     （简写会把 animation-delay 重置为 0）—— 与 .modes button:nth-child 同一口径。 */
  #overlay.show.ov-enter .upcard:nth-child(2){animation-delay:.07s;}
  #overlay.show.ov-enter .upcard:nth-child(3){animation-delay:.14s;}
  .upcard:hover{transform:translateY(-3px); border-color:var(--accent); box-shadow:0 14px 30px rgba(37,99,235,.16), var(--glass-inset);}
  .upcard:active{transform:translateY(-1px);}
  .uc-ic{
    width:46px; height:46px; font-size:24px; display:grid; place-items:center;
    border-radius:12px;
    background-color:var(--glass-solid);
    background-image:linear-gradient(160deg, rgba(255,255,255,.9), rgba(255,255,255,0) 70%);
    border:1px solid rgba(16,24,40,.10);
    box-shadow:inset 0 1px 0 rgba(255,255,255,.9);
  }
  .uc-name{font-weight:800; font-size:15px; letter-spacing:1px;}
  .uc-desc{font-size:12px; color:var(--dim); line-height:1.7; min-height:40px; letter-spacing:.5px;}
  .uc-rar{font-size:10px; letter-spacing:2px; padding:2px 10px; border-radius:999px; border:1px solid; opacity:.95;}
  .cardTitle{font-size:14px; letter-spacing:4px; color:var(--gold-strong); margin-top:6px;}
  /* ---------- 坦克档案 ---------- */
  .tstat{gap:8px; margin:12px 0 4px; max-width:520px;}
  .tstat .chip b{color:var(--text); margin-left:2px;}
  /* 「我的坦克」属性预览固定两行：武器一行 / 车体一行 —— 单排 flex-wrap 的断行位置
     随 chip 数量漂移（火箭炮多一个溅射芯片），末位常孤零零掉下去；固定分组后稳定。
     窄屏放不下时各行内部仍可自动换行兜底（.tstatRow 同样 flex-wrap）。 */
  .tstat.tstatCol{flex-direction:column; align-items:center; gap:6px;}
  .tstatCol .tstatRow{gap:8px; max-width:100%;}
  /* ---------- 难度卡片 ---------- */
  .diffGrid{display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:10px; width:94%; max-width:660px; width:min(660px,94%); margin-bottom:10px;}
  .dcard{
    position:relative; display:flex; flex-direction:column; gap:6px;
    border-radius:12px;
    padding:10px 11px; text-align:left;
  }
  .dcard:hover{transform:translateY(-2px); border-color:var(--dc); box-shadow:0 12px 26px rgba(16,24,40,.13), var(--glass-inset);}
  /* 选中态比普通态更实（.9 白底），让「当前难度」在一排玻璃卡里一眼可辨。
   * 注意同特异度下 .dcard.on 写在 .dcard:hover 之后 → **会盖掉 hover 的阴影**，
   * 实测「已选中的难度卡」hover 时阴影从不生效、也从不参与过渡。补一条
   * .dcard.on:hover 把两者合起来，别让选中态成为唯一没有 hover 反馈的卡。 */
  .dcard.on{
    border-color:var(--dc);
    background-color:var(--glass-solid); background-image:none;
    box-shadow:0 0 0 2px var(--dc) inset, var(--glass-shadow), var(--glass-inset);
  }
  .dcard.on:hover{transform:translateY(-2px); box-shadow:0 0 0 2px var(--dc) inset, 0 12px 26px rgba(16,24,40,.13), var(--glass-inset);}
  .dc-top{display:flex; align-items:baseline; justify-content:space-between; gap:6px;}
  .dc-top b{font-size:15px; font-weight:800; letter-spacing:1px;}
  /* 选中难度卡的名称用身份色（新手绿 / 标准蓝 / 困难红 / 自定义青），15px 粗体在白底上
     最低只有 3.21:1。压深 28% 后四档为 5.8 / 8.3 / 8.0 / 6.4，全部达标，而卡片里的
     能量条 .dc-bar i.on 与选中描边仍是原色 —— 身份色没变，只有**文字**变深。
     前一行是兜底：不支持 color-mix 的老浏览器直接用 --text（对比最高），
     后一行整条声明在不支持时被当作无效忽略掉，不会退回到 3.2:1 的原始色。
     难度说明行里的难度名用的是**同一套配方**（11-ui.js 只在内联里给 --dc，色值交给
     这里定），原先两处各写一遍 color-mix 兜底行 —— 合并成一条，配方以后只改一处。 */
  .dcard.on .dc-top b, .diffDesc b{color:var(--text);}
  .dcard.on .dc-top b, .diffDesc b{color:color-mix(in srgb, var(--dc) 72%, #000);}
  .dc-top em{font-style:normal; font-size:11px; font-weight:800; color:var(--dim); font-variant-numeric:tabular-nums;}
  .dc-bar{display:flex; gap:2px;}
  .dc-bar i{flex:1; height:4px; border-radius:2px; background:var(--line);}
  .dc-bar i.on{background:var(--dc);}
  .dc-mini{font-size:10px; color:var(--faint); letter-spacing:.4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
  .dcard .ck{
    position:absolute; top:-7px; right:-7px; width:20px; height:20px; border-radius:50%;
    background:var(--dc); color:#fff; font-size:12px; font-weight:900; display:grid; place-items:center;
    /* 同上一条：白 ✓ 压在四档身份色上最低 3.30:1，叠 30% 黑纱后 6.0 起（.hudBadge 同法）。 */
    background-image:linear-gradient(rgba(0,0,0,.3),rgba(0,0,0,.3));
    box-shadow:0 3px 8px rgba(16,24,40,.2);
  }
  /* 难度说明行里的难度名的身份色配方已并入上面的 .dcard.on .dc-top b 合并规则
     （12px 正文需要 4.5:1，理由见那里的 color-mix 两行式）。 */
  .diffRec{
    font-size:11px; color:var(--dim);
    background-color:var(--glass-solid);
    background-image:linear-gradient(180deg, rgba(255,255,255,.5), rgba(255,255,255,0) 60%);
    border:1px solid rgba(16,24,40,.10); border-radius:999px; padding:2px 9px; letter-spacing:.5px;
    box-shadow:inset 0 1px 0 rgba(255,255,255,.85);
  }
  .diffRec b{color:var(--accent); font-variant-numeric:tabular-nums;}
  /* ---------- 自定义难度面板 ---------- */
  .customBox{
    width:94%; max-width:660px; width:min(660px,94%); margin:0 auto 12px;
    background-color:rgba(255,255,255,.5);
    background-image:linear-gradient(180deg, rgba(255,255,255,.55), rgba(255,255,255,.22));
    border:1px solid rgba(255,255,255,.6); border-radius:12px;
    box-shadow:inset 0 1px 0 rgba(255,255,255,.85);
    padding:9px 14px; display:flex; flex-direction:column; gap:6px;
  }
  .cRow{display:flex; align-items:center; gap:10px;}
  .cRow .cl{flex:none; width:60px; text-align:left; font-size:11px; letter-spacing:2px; color:var(--faint);}
  .cOpts{display:flex; gap:6px; flex-wrap:wrap; flex:1;}
  .cPill{
    color:var(--dim);
    border-radius:8px; padding:3px 11px; font-size:12px; font-weight:700; letter-spacing:1px;
  }
  .cPill:hover{color:var(--text); border-color:var(--faint);}
  .cPill.on{background-color:var(--accent); background-image:none; border-color:var(--accent); color:#fff;}
  /* 不可用的胶囊（例如房间里尚未完成认证时的操控模式）：必须看得出来不可点，
     否则点击无反应会被当成功能坏了。与 .btn:disabled 同一口径。 */
  .cPill:disabled{opacity:.45;cursor:not-allowed;}
  .cPill:disabled:hover{color:var(--faint);border-color:var(--line-strong);}
  /* ---------- HUD：难度徽章 / 得分跳动 / 低生命脉冲 / 进度条 ---------- */
  /* 难度徽章是**跨界面复用**的：除了 HUD，过关 / 战役通关 / GAME OVER 结算面板
     也渲染同一个 .hudBadge（11-ui.js 四处）。原选择器写成 `#hud .hudBadge`，
     结算面板里于是只剩内联 background-color —— 白字、内边距、圆角、黑纱全部丢失，
     继承来的 --dim 深灰压在绿色底上实测 1.96:1（AA 要求 4.5:1），徽章上的难度名
     基本读不出来。改成全局类；HUD 窄屏隐藏仍由下面的容器查询单独负责。 */
  .hudBadge{
    display:inline-block; padding:2px 9px; border-radius:999px; font-size:11px; font-weight:800;
    letter-spacing:1px; color:#fff; background:var(--dim); vertical-align:1px;
    /* 难度徽章的底色由 JS 按 DIFFS 逐档内联下发（新手绿 / 标准蓝 / 困难红），白字压在
     * #16a34a 上只有 3.30:1。叠一层 30% 黑纱把三档一并压深（5.44 / 7.91 / 8.25:1），
     * 装饰观感只变深一档、不动 DIFFS 本体 —— 那个颜色同时是难度卡片与画布里的身份色。
     * 前提是内联只写 background-color（见 11-ui.js 的 hudBadge）：写成 background 简写
     * 会连同 background-image 一起重置掉，这层纱就没了。
     * 顶光走 inset box-shadow（不占 background-image）：万一这层纱被谁抹掉，
     * 对比度会掉回 3.30:1 —— 门禁里有一条专门盯这件事（见 tank-live-check.mjs）。 */
    background-image:linear-gradient(rgba(0,0,0,.3),rgba(0,0,0,.3));
    box-shadow:inset 0 1px 0 rgba(255,255,255,.22);
  }
  #hud b.pop{animation:scorePop .3s cubic-bezier(.2,.9,.3,1.4);}
  @keyframes scorePop{0%{transform:scale(1);} 40%{transform:scale(1.26);} 100%{transform:scale(1);}}
  #hud b.low{animation:lowPulse 1.05s ease-in-out infinite;}
  @keyframes lowPulse{0%,100%{opacity:1;} 50%{opacity:.4;}}
  .hudMeter{display:flex; flex-direction:column; align-items:flex-end; gap:4px; max-width:180px;}
  /* 轨道底色：--line(#e7eaee) 叠在白底 HUD 上对比度只有 1.06:1，实测等于看不见 ——
     进度低时玩家看到的是一根悬空的彩色短条，读不出它相对的是哪一段总长。
     这里改用与画布上「基地耐久格」空槽同款的半透明深色（1.4:1），既能读出总长，
     又不会在满进度时和彩色填充抢注意力。 */
  .mbar{width:100%; height:3px; border-radius:2px; background:rgba(27,36,50,0.16); overflow:hidden;}
  /* 进度条用 transform:scaleX 而不是 width —— 本文件自己写下的规矩（见 :329 附近）：
     width / height 参与过渡就等于每帧触发重排。生存模式的波次进度每 0.25s 换一次目标，
     而过渡时长 0.28s 比它还长 → 整波次期间进度条一直处在 width 过渡中，
     等于每帧重排整条 HUD（.mbar ⊂ .hudMeter ⊂ #hud 是 flex 行）。
     scaleX 由合成器独占，零重排；视觉上与 width 完全等价
     （<i> 的布局宽度恒为 100%，靠缩放表现进度，.mbar 的 overflow:hidden 负责裁切）。
     圆角会被横向压扁一点（3px 高的细条，肉眼不可见）。 */
  .mbar i{display:block; width:100%; height:100%; border-radius:2px; background:var(--accent);
    transform-origin:left center; transform:scaleX(0); transition:transform .28s ease;}
  /* ---------- 卡片顶部高光条 ---------- */
  /* .card 的盒模型与玻璃表面已并入上面那条 .card 规则 —— 原先这里还有第二处
     .card{position:relative; overflow:hidden;}，玻璃化改写时只改到其中一处
     就会得到「有 overflow:hidden 却没玻璃底」的半成品卡。 */
  .card::before{
    content:""; position:absolute; left:0; right:0; top:0; height:3px;
    background:linear-gradient(90deg, var(--accent), #22d3ee 55%, var(--gold));
  }
  .card.win::before{background:linear-gradient(90deg, var(--green), #34d399 55%, #a7f3d0);}
  .card.lose::before{background:linear-gradient(90deg, var(--red), #f97316 55%, #fcd34d);}
  .card.gold::before{background:linear-gradient(90deg, var(--gold), #fcd34d 55%, #fef3c7);}
  /* ---------- 标题质感 ---------- */
  @supports ((-webkit-background-clip:text) or (background-clip:text)){
    .logo{
      background:linear-gradient(180deg, #1b2432 0%, #44536b 100%);
      -webkit-background-clip:text; background-clip:text; color:transparent;
      filter:drop-shadow(0 2px 0 rgba(255,255,255,.7));
    }
  }
  @media (max-width:560px){
    .logo{font-size:36px; letter-spacing:10px; text-indent:10px;}
    .modes button{min-width:160px;}
    .modes.pick button{min-width:0; padding:10px;}
    .diffGrid{grid-template-columns:repeat(2, minmax(0,1fr)); width:96%;}
    .cRow{align-items:flex-start; flex-direction:column; gap:6px;}
  }
  /* HUD 的难度徽章在 641~700px 的应用宽上也要收掉。1024×600 这类矮桌面
   * （应用宽 651）正好落在 640 容器档之外：HUD 已按宽档字号渲染，而
   * 生命 99 + 装甲 20/20 + 得分 999999 时实测三组间隙被压到 0（scrollWidth
   * 仍等于 clientWidth，不至于横向滚动，但「得分 999999第 3 关」已经黏成一段）。
   * 徽章的难度名在暂停面板 / 结算面板都完整可见，HUD 这一枚是重复信息；
   * 宽档确实放得下时（>700）才保留它当快速标识。 */
  @container (max-width:700px){
    #hud .hudBadge{display:none;}
  }
  /* 容器查询：按「应用实际宽度」切换栅格。
     应用宽度现在同时受 100vh 约束（见 #app 注释），于是会出现「视口很宽但应用很窄」
     的情况（例如 1024×600 的矮屏笔记本：应用只有 ~535px）。此时视口媒体查询不会命中，
     两列模式卡的文字区只剩约 145px，「保卫基地 · 首领关卡 · 肉鸽强化」必然折行，
     同一行内两张卡与另一行视觉不齐。按容器宽度切换后，窄应用自动回到单列。
     不支持容器查询的浏览器会整块忽略，退回改动前的两列布局，不会更差。 */
  @container (max-width:640px){
    .modes{grid-template-columns:1fr;}
    /* .modes.pick（选择操作方式面板）特异度高于 .modes，必须与它**同轴同档**收成单列。
       漏了它，561~640 容器宽下主菜单已单列、选择面板仍走基线两列（.modes.pick 的
       repeat(2,…) 压过容器档的 .modes），同屏两套形态互相打架 —— 正是 v4.5.40
       「同族栅格同轴」不变量要堵的缝。视口轴的 @media(≤560) 只是老浏览器兜底，
       现代浏览器的主规则在容器轴，所以这一条必须补在容器轴。 */
    .modes.pick{grid-template-columns:1fr;}
    .modes button{min-width:0;}
    /* 强化三选一（.cards）原先只挂在 @media(≤560px) 的**视口**轴上，于是同一族栅格里
       它与 .modes / .diffGrid 用着两把尺子：应用宽 490 的矮宽视口（1920×480）拿三列
       强化卡（每张仅 ~155px，描述文字折 4~5 行、卡片不等高），而应用宽 549 的窄高
       视口（560×900）反而是一列。栅格该跟**容器**走 —— 舞台在哪，栅格就该在哪。 */
    .cards{grid-template-columns:1fr;}
    .diffGrid{grid-template-columns:repeat(2, minmax(0,1fr)); width:96%;}
    .cRow{align-items:flex-start; flex-direction:column; gap:6px;}
    /* 窄应用下 HUD 一行放不下时会开始折行（标签在 CJK 之间可以断行），实测 HUD 会从
       56 涨到 69，把为视口高度预留的余量吃掉。这里整体收紧一档，使 HUD 恒定一行
       （实测 1024×600 下 0 敌 / 60 敌均为一档）。难度徽章已在上方 ≤700 档收掉。 */
    #hud{padding:8px 10px; gap:6px;}
    #hud .grp{gap:8px;}
    #hud .lbl{letter-spacing:1px;}
    #hud b{font-size:15px;}
    .hudMeter{max-width:120px;}
    /* 与 .hudMeter 的预算保持一致：enemyIconCap() 就是按这两个值反推图标个数的 */
    #enemyIcons{max-width:120px;}
  }
  /* 窄视口（≤560）：主菜单与「选择操作方式」面板一律回到单列 —— 同一档里两者的
     列数必须一致，否则同屏两套形态会互相打架。
     「选择操作方式」面板（.modes.pick）原先**强制两列**：它的特异度高于容器查询里的
     .modes，于是 320~430 的竖屏上每列只剩 42~74px 的文案列 —— 标题「键盘驾驶」被拆成
     两行、副标题被拆成 4~6 行（每行 2~3 字），图标又因垂直居中浮在文字旁边脱开；
     430 档一度比 414 档更挤（71 vs 85px）：卡片内边距走容器轴（@container ≤400px）、
     模式卡内边距走视口轴，两轴打架，视口越宽反而越窄。
     改成单列后实测（真浏览器，320/360/390/414/430 竖屏）：文案列
     172/209/236/236/229px，标题一律 1 行、副标题 1~2 行；面板高
     574/557/509/492/557px，都在 568~932 的视口高之内（320 档超出 30px，可滚动到达）。
     判据用**视口宽**而不是容器宽，是因为这块面板在两处宽度上取值不同：
     竖屏手机上是「整屏弹窗」（宽度 == 视口宽），而矮桌面（1024×600）是
     「弹窗只盖战场」（卡片宽 605px，两列仍放得下 156px 的文案列、标题单行）；
     用容器宽会把矮桌面也压成单列、面板反而变高一截。
     .modes 那一条在现代浏览器里是**冗余**的（容器轴那档已经覆盖了全部 ≤560 的视口）：
     它的作用是当**老浏览器兜底** —— @container 整块在 Chrome 57~104 / Safari 11~15
     上不生效（style.css 顶部的特性下限表），那时只剩基线的
     repeat(2,minmax(210px,1fr))：210px 的列宽下限在 390 视口上会把主菜单撑到 480px、
     右列的模式卡被视口右缘切掉一半；选择面板更惨，文案列只剩 20px、汉字逐字竖排。
     @media 早于 @container 几百年就被所有浏览器认得，正好兜住那条路径
     （门禁里有一档专门仿真它：把 container-type 置为 normal）。
     本块刻意放在容器档**之后**：容器轴那档才是现代浏览器的主规则（v4.5.40 的
     「同族栅格不许一半在 @container、一半在 @media」按先出现的那个块判定），
     这里的视口档只是叠加一条同值的兜底，两者不会打架。 */
  @media (max-width:560px){
    .modes{grid-template-columns:1fr;}
    .modes.pick{grid-template-columns:1fr;}
  }
  /* 超窄应用（≤500px）：320~510 视口下 #app 只有 314~500px，HUD 左栏 + 中栏已占
     170~230px，右侧「敌军」标签又会单独占一行（.hudMeter 是块级 flex 容器），
     实测 320px 下整页横向滚动 11px、静音按钮被压到 18px 宽（CSS 写的是 36px）。
     四处一起收：隐藏重复的「敌军」标签（图标组自身带 aria-label），静音按钮
     禁止收缩并收到 28px（仍高于 24px 的可点击下限），HUD 内边距 / 间距再收一档，
     敌军图标从 12×9 / 间距 3 收到 9×7 / 间距 2。
     图标数量不变（enemyIconCap() 仍按 380/640 两条线退档）；缩小的是每枚图标的
     占位。390px 视口下应用 382px，生命 99 + 装甲 20/20 + 得分 999999 时，原尺寸
     需要 401px 内容宽、越过应用右缘 15px 并让整页横向滚动；图标收紧后连同 JS 侧
     的单字标签（hudLbl）一起把极值组合压回 380px 以内。
     档位从 420 放宽到 500：430/440 视口虽然 scrollWidth 不超，但 flex 会把静音键
     压到 18px —— 同一份极值数据在 430 上测出来的不是溢出，而是按钮被挤扁；
     450~470 视口则连完整标签都放不下（实测整页横向滚动 1~4px）。
     短横屏不受这一档的字号影响（后面的紧凑块会覆盖成 10/14），标签缩写由 JS
     的 hudShortLabels() 单独按「是否短横屏」豁免。 */
  @container (max-width:500px){
    #hud{padding:6px 6px;gap:4px;}
    #hud .grp{gap:4px;}
    /* 窄屏数值同步收 1~2px：390 视口在 99 命 / 20 装甲 / 8 位分数时，左组
     * 自然宽仍会超出 3 组可用宽度，溢出的字形会压到中组的「第 N 关」上
     * （分隔点被字形盖住）。15/14 → 14/13 后左组自然宽回到预算内，
     * 15px→14px 的观感差别很小，1500 万级别的分数则改用「万」压缩。 */
    #hud .lbl{font-size:10px;letter-spacing:0;}
    #hud b{font-size:14px;}
    #hud b.armor{font-size:13px;}
    .hudMeterLbl{display:none;}
    #muteBtn{flex:none; width:28px; height:28px;}
    #enemyIcons{gap:2px;}
    #enemyIcons i{width:9px; height:7px;}
    #enemyIcons i::after{width:2px; height:4px; margin-left:-1px; top:-2px;}
  }
  /* 最窄应用（≤360px）：320px 下左栏只剩约 158px，「生命 / 装甲 / 得分」会被逐字
     竖排（每个 CJK 标签断成两行），同时 500 档的 15px 数字仍嫌宽。这里把标签收到
     10px、数字收到 13px，并给标签加 nowrap（不再逐字折断）；配合
     enemyIconCap() 的 3 图标预算，320px 下 HUD 内部 scrollWidth 与 clientWidth
     相等、静音按钮完整落在应用内。28px 按钮仍高于 24px 的可点击下限。 */
  @container (max-width:360px){
    #hud{padding:6px 6px;gap:3px;}
    #hud .grp{gap:3px;}
    #hud .lbl{font-size:10px; letter-spacing:0; white-space:nowrap;}
    #hud b{font-size:13px;}
    #hud b.armor{font-size:12px;}
    #hudRightInfo{min-width:0;}
    #muteBtn{width:28px; height:28px;}
  }
  /* 超窄应用（手机竖屏 / 很窄的浏览器窗口）：结算卡的 min-content 宽度 =
     .stats 的 280px + 卡片左右内边距 72 + 弹窗内边距 44。375 视口下应用只有 367px，
     实测弹窗 clientWidth 366 / scrollWidth 398，卡片右缘越过弹窗 10px 并被 overflow
     裁掉。这里收紧两圈内边距并取消表格最小宽度；内容按 100% 宽重排，不再横向溢出。 */
  /* 弹窗自身的内边距不能写进上面的 @container：紧凑档 #overlay 自己就是查询容器，
     容器查询只能作用于**后代**，不会命中容器自己。这里用视口宽替代 —— 紧凑档弹窗
     整屏、两者同宽；桌面上应用宽 ≤ 98vw，视口 ≤400 时弹窗宽度也必然落在这一档。 */
  @media (max-width:400px){
    #overlay{padding:12px;}
  }
  @container (max-width:400px){
    .card{padding:18px 12px; max-width:100%;}
    /* 玻璃底板在窄应用上也收一档内边距：它比原来的裸排版多吃 52px 横向，
     * 而 320 视口下应用只有 ~340px —— 不收的话模式卡的文案区被压到 ~180px，
     * 副标题「保卫基地 · 首领关卡 · 肉鸽强化」会被迫折成两行。 */
    .ov-col{padding:14px 12px 18px; border-radius:18px;}
    .stats{min-width:0; width:100%;}
    .stats td{padding:6px 8px; letter-spacing:.5px;}
  }
  /* 无手动选择时跟随系统偏好；JS 启动后用 reduce-motion-override 标记明确的玩家选择，
     让手动关闭也能覆盖系统的 reduce 规则。reduce-motion 类统一控制 CSS 与游戏内特效。 */
  @media (prefers-reduced-motion:reduce){ html:not(.reduce-motion-override) *{animation:none !important; transition:none !important;} }
  html.reduce-motion *{animation:none !important; transition:none !important;}
  /* 暂停面板底部的设置行 */
  .setRow{align-items:center; gap:10px; margin-top:12px;}
  .setRow .setLbl{color:var(--faint); font-size:12px; letter-spacing:2px;}
  .btn.mini{padding:6px 14px; font-size:13px; letter-spacing:1px;}
  .btn.mini.on{border-color:var(--accent); color:var(--accent); background-color:var(--accent-weak); background-image:none;}

  /* ---------- 触屏控制层（v4.7.0：虚拟摇杆 + 开火键） ----------
   * 触屏驾驶从「手指拖拽即转向即开火」改成两区操作：
   *   左下 摇杆 = 方向 + 幅度（推多远开多快），右下 开火 = 按住连射。
   * 装备 / 刹车 / 暂停仍与键鼠同一入口；右手列从下到上为 开火 / 刹车 / 装备，
   * 拇指不用横跳。整层 position:fixed，pointer-events:none 只给容器、控件本身
   * auto —— 否则透明遮罩会把整块战场的触摸全吃掉（画面看得见、摸不动）。
   * 尺寸按 WCAG 2.5.5 定：开火 92px、摇杆 132px、副键 64×52、暂停 52×44；
   * 短横屏 / 超窄屏再各降一档，副键不低于 54×46、暂停不低于 44×38。
   * 位置先写不带 env() 的保底值，再用 max()/env() 覆盖（刘海 / 手势条安全区）。 */
  /* 四条长写在前，inset 只作现代浏览器的收尾 —— 与 #overlay 同一道规矩。
     inset 是 Chrome 87 / Safari 14.1 才支持的简写，只写它的话旧浏览器会把整层
     退成「四边 auto 的 fixed 容器」→ 容器退到文档流位置，里面的控件跟着错位。 */
  #touchPad{position:fixed; top:0; right:0; bottom:0; left:0; inset:0; z-index:40; pointer-events:none;}
  #touchPad[hidden]{display:none;}
  #touchPad .tpBtn{
    position:absolute; pointer-events:auto;
    min-width:64px; min-height:52px; padding:0 12px;
    border-radius:15px; font:700 14px/1 "PingFang SC","Microsoft YaHei",system-ui,sans-serif;
    letter-spacing:1px; color:var(--text);
    background-color:rgba(255,255,255,.82);
    background-image:var(--glass-sheen);
    border:1px solid var(--glass-line);
    box-shadow:var(--glass-shadow), var(--glass-inset);
    touch-action:none;              /* 按住刹车时页面不能跟着滚 */
    -webkit-tap-highlight-color:transparent;
    user-select:none; -webkit-user-select:none;
    cursor:pointer;
  }
  #touchPad .tpBtn:active{background-color:rgba(219,234,254,.92);}
  /* 摇杆：底盘负责触摸，旋钮只做视觉反馈。max()/env() 覆盖安全区。 */
  #touchPad .tpJoy{
    position:absolute; pointer-events:auto;
    left:20px; bottom:20px;
    left:max(18px, env(safe-area-inset-left, 0px));
    bottom:max(18px, env(safe-area-inset-bottom, 0px));
    width:132px; height:132px; border-radius:50%;
    touch-action:none;
    -webkit-tap-highlight-color:transparent;
    user-select:none; -webkit-user-select:none;
    background:radial-gradient(circle at 50% 36%, rgba(255,255,255,.92), rgba(255,255,255,.56) 60%, rgba(226,232,240,.58));
    border:1px solid var(--glass-line);
    box-shadow:var(--glass-shadow), var(--glass-inset);
  }
  #touchPad .tpJoy::before{
    /* 四条长写在前，inset 只作现代浏览器的收尾 —— 与 #touchPad 容器同一道规矩。
       inset 是 Chrome 87 / Safari 14.1 才有的简写；只写它的话旧浏览器会把这一圈
       虚线导引环整个丢掉（position:absolute + 四边 auto + 无尺寸 → 零尺寸盒子），
       摇杆就少了一圈「可推动范围」的视觉提示（触屏档的可用性提示，不是装饰）。 */
    content:""; position:absolute;
    top:26%; right:26%; bottom:26%; left:26%;
    inset:26%;
    border-radius:50%;
    border:1px dashed rgba(37,99,235,.28);
  }
  #touchPad .tpJoy::after{
    content:""; position:absolute; left:50%; top:50%; width:4px; height:4px;
    margin:-2px 0 0 -2px; border-radius:50%; background:rgba(37,99,235,.36);
  }
  #touchPad .tpJoy.on{
    background:radial-gradient(circle at 50% 36%, rgba(255,255,255,.97), rgba(219,234,254,.72) 60%, rgba(191,219,254,.64));
  }
  #touchPad .tpJoyKnob{
    position:absolute; left:50%; top:50%; width:56px; height:56px;
    margin:-28px 0 0 -28px; border-radius:50%;
    background:radial-gradient(circle at 38% 30%, #fff, #eaf1ff 48%, #c9dcff);
    border:1px solid rgba(37,99,235,.24);
    box-shadow:0 2px 6px rgba(16,24,40,.16), 0 10px 20px rgba(37,99,235,.18), inset 0 1px 0 #fff;
    transform:translate3d(0,0,0);
    transition:transform .1s ease-out;
    will-change:transform;
  }
  #touchPad .tpJoy.on .tpJoyKnob{transition:none;}
  /* 开火键：整个右下角最大的目标，按住连射；暖色与浅色战场形成明确主次。 */
  #touchPad .tpFire{
    position:absolute; pointer-events:auto;
    right:20px; bottom:20px;
    right:max(18px, env(safe-area-inset-right, 0px));
    bottom:max(18px, env(safe-area-inset-bottom, 0px));
    width:92px; height:92px; padding:0; border-radius:50%;
    display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2px;
    font:800 13.5px/1 "PingFang SC","Microsoft YaHei",system-ui,sans-serif;
    letter-spacing:2px; color:#fff;
    background-color:#b93b40;
    background-image:linear-gradient(165deg, #c93f47 0%, #b93b40 55%, #96242e 100%);
    border:1px solid rgba(255,255,255,.55);
    box-shadow:0 2px 4px rgba(127,29,29,.18), 0 12px 24px rgba(185,28,28,.24), inset 0 1px 0 rgba(255,255,255,.4);
    touch-action:none;
    -webkit-tap-highlight-color:transparent;
    user-select:none; -webkit-user-select:none;
    cursor:pointer;
  }
  #touchPad .tpFireIc{font-size:19px; line-height:1; opacity:.9;}
  #touchPad .tpFireTxt{font-size:13.5px; letter-spacing:2px;}
  #touchPad .tpFire:active, #touchPad .tpFire.on{
    transform:scale(.96);
    background-image:linear-gradient(165deg, #b93b40 0%, #a52c34 55%, #7f1d24 100%);
    box-shadow:0 1px 2px rgba(127,29,29,.22), 0 5px 12px rgba(185,28,28,.22), inset 0 1px 0 rgba(255,255,255,.32);
  }
  /* 右手副键：竖屏横排在开火键上方（减少纵向占用，避免低端机压到页脚）；
   * 短横屏再改回竖排（战场两侧的空隙只有 ~110px，横排放不下）。 */
  #touchPad .tpSub{
    bottom:118px;
    bottom:calc(max(18px, env(safe-area-inset-bottom, 0px)) + 100px);
    width:64px; min-width:64px; height:52px; min-height:52px; padding:0;
    border-radius:14px; font-size:13.5px;
  }
  #touchPad .tpSub[data-touch="equip"]{right:20px;}
  #touchPad .tpSub[data-touch="equip"]{right:max(18px, env(safe-area-inset-right, 0px));}
  #touchPad .tpSub[data-touch="brake"]{right:92px;}
  #touchPad .tpSub[data-touch="brake"]{right:calc(max(18px, env(safe-area-inset-right, 0px)) + 72px);}
  /* 暂停：摇杆上方，远离右手拇指的活动区，避免开火时误触。 */
  #touchPad .tpPause{
    left:20px; bottom:168px;
    left:max(18px, env(safe-area-inset-left, 0px));
    bottom:calc(max(18px, env(safe-area-inset-bottom, 0px)) + 148px);
    width:52px; min-width:52px; height:44px; min-height:44px; padding:0;
    border-radius:14px; font-size:12.5px; letter-spacing:1px;
  }
  /* 横屏建议：只在**竖屏**时出现 —— 触屏游玩横屏画面更大，这是本项目对触屏用户的
     正式建议。放在 HUD 下方，避免遮住生命 / 装甲读数。 */
  #touchPad .tpTip{
    position:absolute; left:50%; transform:translateX(-50%);
    top:76px; top:calc(max(10px, env(safe-area-inset-top, 0px)) + 66px);
    padding:6px 14px; border-radius:999px;
    font-size:12px; letter-spacing:1px; color:var(--dim);
    background-color:rgba(255,255,255,.86);
    border:1px solid var(--glass-line);
    box-shadow:inset 0 1px 0 rgba(255,255,255,.9);
    display:none;
    white-space:nowrap;
  }
  @media (orientation:portrait){ #touchPad .tpTip{display:block;} }
  /* 320~360px 窄竖屏：再缩一档，开火 78、摇杆 108、副键 54×46。
   * 左右两块加起来的宽度必须 ≤ 视口，否则副键会压到摇杆上。 */
  @media (max-width:360px){
    #touchPad .tpJoy{
      left:max(14px, env(safe-area-inset-left, 0px));
      bottom:max(14px, env(safe-area-inset-bottom, 0px));
      width:108px; height:108px;
    }
    #touchPad .tpJoyKnob{width:46px; height:46px; margin:-23px 0 0 -23px;}
    #touchPad .tpPause{
      left:max(14px, env(safe-area-inset-left, 0px));
      bottom:calc(max(14px, env(safe-area-inset-bottom, 0px)) + 122px);
      width:44px; min-width:44px; height:38px; min-height:38px; font-size:12px;
    }
    #touchPad .tpFire{
      right:max(14px, env(safe-area-inset-right, 0px));
      bottom:max(14px, env(safe-area-inset-bottom, 0px));
      width:78px; height:78px;
    }
    #touchPad .tpFireIc{font-size:16px;}
    #touchPad .tpFireTxt{font-size:12px;}
    #touchPad .tpSub{
      width:54px; min-width:54px; height:46px; min-height:46px; font-size:13px;
      bottom:calc(max(14px, env(safe-area-inset-bottom, 0px)) + 86px);
    }
    #touchPad .tpSub[data-touch="equip"]{
      right:max(14px, env(safe-area-inset-right, 0px));
    }
    #touchPad .tpSub[data-touch="brake"]{
      right:calc(max(14px, env(safe-area-inset-right, 0px)) + 60px);
    }
    #touchPad .tpTip{font-size:11px; padding:5px 11px; letter-spacing:.5px;}
  }

  /* 竖屏手机：应用顶端对齐。body 的 margin:auto 居中在 390×844 上留下约 180px
   * 顶部空白，而底部控件又紧贴页脚 —— 低端机（320×568）上「装备」会直接压住
   * 页脚文字。顶部对齐后 HUD 贴顶、提示条落在 HUD 下方，底部整片留给摇杆 / 开火。
   * 只作用于手机竖屏档，平板与桌面仍保持居中。 */
  @media (orientation:portrait) and (max-width:560px){
    #app{
      align-self:flex-start; /* body 的 align-items:center 仍在；auto 外边距不覆盖它 */
      /* 长写在先：不支持 max() 的浏览器（Chrome 55~78）会**整条丢掉**这条声明，
         于是 margin-top 落回基础规则 #app{margin:auto} —— 而 body 是 flex 行容器，
         交叉轴上的 auto 外边距会吃掉全部剩余空间并**压过 align-self**，
         整个应用被顶到视口下沿（正是本条规则要避免的「装备键压住页脚」那一版）。
         写成 10px 就没有 auto 外边距，align-self:flex-start 重新说了算。 */
      margin-top:10px;
      margin-top:max(10px, env(safe-area-inset-top, 0px));
      margin-bottom:0;
    }
  }

  /* ---------- 手机 / 短视口：弹窗整屏（v4.6.5） ----------
   * 390×844 竖屏时舞台只有 285px 高，而主菜单约 1800px、结算面板约 1000px ——
   * 弹窗被锁在战场内，玩家是在一条 285px 高的「窗缝」里滚动整个菜单。横屏更糟：
   * 844×390 下弹窗只有 276px 高、菜单单列 1875px，首屏只看得见标题和一张模式卡。
   * 竖屏宽 ≤560 / 横屏高 ≤560 这两个条件覆盖手机（含浏览器地址栏占用后的可视区），
   * 不碰平板与桌面。命中后弹窗改为整屏，菜单按**屏幕宽**（#overlay 自己的容器查询）
   * 排布：横屏双列、竖屏单列。
   *
   * 必须同时关掉 #app 的入场动画：appIn 用 animation-fill-mode:both 留住终帧的
   * identity transform，而 transform 会让 #app 成为 fixed 后代的包含块 —— 弹窗会先
   * 被锁在应用框里、动画结束后再跳到整屏。手机端不做这段位移，直接不播。 */
  @media (orientation:landscape) and (max-height:560px), (orientation:portrait) and (max-width:560px){
    #app{animation:none;}
    #overlay{
      position:fixed;
      top:0; right:0; bottom:0; left:0; inset:0;
      /* 固定定位 + top/bottom 在 iOS 上量的是布局视口；dvh 跟随地址栏收放，
         避免弹窗底部（设置行 / 结算按钮）藏在浏览器工具栏后面。
         旧浏览器不认识 dvh 时沿用上面的 top/bottom:0。

         **但 dvh 必须除以 --ui-scale**：#overlay 是 #app 的后代，而 #app 带
         zoom:var(--ui-scale)。视口单位（vh / dvh）是**未缩放**的绝对量，
         而 zoom 会把元素自身的布局尺寸乘一遍 —— 于是「100dvh」最终渲染成
         视口高 × zoom：390×844 手机上选 150%（钳到 112%）时，整屏弹窗渲染成
         844 × 1.124 = 949px，比视口高出 105px；844×390 横屏上更是高出 195px。
         后果不是「多出一点留白」而是**内容被裁在屏幕外**：弹窗按 overflow:auto
         滚到底，底部那几行（车体图标 / 「0」计数等）仍落在视口下沿之外，
         玩家既滚不到也点不着（实测 390×844 裁掉 6 处、844×390 裁掉 16 处）。
         除以 scale 后渲染高恰为视口高，与 zoom=1 时的行为逐字一致。
         --ui-scale 在不支持 zoom 的浏览器上恒为 1（见 04-dom-input.js 的
         zoomSupported 兜底），故这条对老浏览器无副作用。 */
      height:calc(100vh / var(--ui-scale)); height:calc(100dvh / var(--ui-scale));
      z-index:60; border-radius:0;
      padding:12px;
      padding:max(12px, env(safe-area-inset-top, 0px)) max(12px, env(safe-area-inset-right, 0px))
        max(12px, env(safe-area-inset-bottom, 0px)) max(12px, env(safe-area-inset-left, 0px));
      /* 整屏弹窗盖住的是 HUD + 战场，不再只是舞台：底色由 .84 提到 .96 ——
       * v4.6.5 的 .92 在 2× 手机截图里仍能看见顶部的「生命 / 装甲」读数透出来，
       * 叠加卡片文字后读起来像重影。.96 保留一点底图层次，但不和正文抢读。
       * 这个不透明度下 12px 模糊已经几乎看不出差别，手机端直接关掉
       * backdrop-filter：全屏滚动时它要为整块背景每帧重新模糊，是白付的合成成本。 */
      background-color:rgba(240,243,247,.96);
      backdrop-filter:none; -webkit-backdrop-filter:none;
      overscroll-behavior:contain;
      /* 弹窗脱离舞台后改由自己当尺寸查询容器：菜单栅格按**屏幕宽**切换
       * （844px 横屏 → 双列、390px 竖屏 → 单列）。不放进基线规则是为了让桌面
       * 继续用 #app 的宽度 —— 1024×600 这类矮桌面的应用宽 651、弹窗内容宽 605，
       * 若按弹窗内容宽判断会被误降成单列，桌面菜单无端变高一截。 */
      container-type:inline-size;
    }
    /* 整屏弹窗盖住的是**整块视口**：HUD 与页脚被压在它下面，只剩 4% 的残影 ——
     * 2× 手机截图里「生命 / 装甲 / 弹药」那一行与版权行会清清楚楚浮在面板上，
     * 叠在面板文字旁边读起来像重影。遮罩那段的注释写的是「盖住的是 HUD + 战场」，
     * 那就让它真的盖住：面板打开时 HUD 与页脚让位。
     * 战场仍然从玻璃面板后面透出来（那才是这层设计的意思）；HUD / 页脚是页面框架，
     * 不该隔着玻璃显形。
     * 用 visibility 而不是 display：布局不变（应用宽度由视口算出，不依赖 HUD 实高），
     * 面板收起时 HUD 原位出现，不会跳一下。
     * 桌面 / 平板走的是「弹窗只盖战场」那条路，HUD 与页脚在弹窗之外，本来就不重叠，
     * 因此这条规则只写在整屏弹窗这一档里。 */
    #app.panel-open #hud,
    #app.panel-open #foot{ visibility:hidden; }
  }

  /* ---------- 手机横屏：紧凑 HUD，把高度还给战场（v4.6.5） ----------
   * 之前 --app-reserve 恒为 112px（HUD 64 + 页脚 30 + 余量 18）。844×390 的横屏里
   * 它吃掉 29% 的高度：战场只有 371×278，两侧各空 237px，触屏键还被甩到屏幕角上。
   * 现在短横屏把 HUD 收到 36px、隐藏页脚、预留降到 48px —— 战场 456×342，
   * 高度利用率从 71% 提到 88%，且仍不横向 / 纵向滚动。
   * HUD 高度必须和三处预算同步：min-height、muteBtn 高度（28 + 上下 padding 8）、
   * --hud-h-max + 10。改任一处都要重跑 tank-live-check.mjs 的「布局矩阵」。 */
  @media (orientation:landscape) and (max-height:560px){
    :root{
      --hud-h:36px;
      --hud-h-max:38px;
      --foot-h:0px;
      --app-reserve:calc(var(--hud-h-max) + 10px);
    }
    #foot{display:none;}
    #hud{min-height:var(--hud-h); padding:3px 8px; gap:6px; font-size:12px; border-radius:10px 10px 0 0;}
    #hud .grp{gap:7px;}
    #hud .lbl{font-size:10px; letter-spacing:.5px;}
    #hud b{font-size:14px;}
    #hud b.armor{font-size:12px;}
    /* 难度徽章在窄应用档本就被隐藏；宽横屏（900×560 → 应用 683）也一并收掉：
     * 省下的宽度让「敌军」图标组在 36px 高的单行 HUD 里不挤。 */
    #hud .hudBadge{display:none;}
    /* 「敌军 / 在场 / 指挥官」标签与图标组自带的 aria-label 重复；HUD 只剩 36px 高时
     * 优先把横向空间留给图标和数字（与 ≤500 窄应用档同一取舍）。 */
    .hudMeterLbl{display:none;}
    #muteBtn{width:28px; height:28px; border-radius:8px;}
    #enemyIcons{gap:2px;}
    #enemyIcons i{width:10px; height:8px;}
    /* 短横屏：控制层整体缩一档，仍留在战场两侧的空白里。
     * 右列从下到上 开火（82）/ 刹车（48）/ 装备（48），不压住底部中央的基地。 */
    #touchPad .tpJoy{
      left:max(14px, env(safe-area-inset-left, 0px));
      bottom:max(14px, env(safe-area-inset-bottom, 0px));
      width:116px; height:116px;
    }
    #touchPad .tpJoyKnob{width:50px; height:50px; margin:-25px 0 0 -25px;}
    #touchPad .tpPause{
      left:max(14px, env(safe-area-inset-left, 0px));
      bottom:calc(max(14px, env(safe-area-inset-bottom, 0px)) + 130px);
      width:46px; min-width:46px; height:40px; min-height:40px;
    }
    #touchPad .tpFire{
      right:max(14px, env(safe-area-inset-right, 0px));
      bottom:max(14px, env(safe-area-inset-bottom, 0px));
      width:82px; height:82px;
    }
    #touchPad .tpFireIc{font-size:17px;}
    #touchPad .tpFireTxt{font-size:12.5px;}
    #touchPad .tpSub,
    #touchPad .tpSub[data-touch="equip"],
    #touchPad .tpSub[data-touch="brake"]{
      /* 长写在先，值 = 上面的表达式在安全区为 0 时的结果（14 + 13 = 27）。
         不支持 max() 的浏览器（Chrome 55~78）整条丢掉上面的声明后，
         这一行仍把三颗副键摆进短横屏的右列；没有它就退回竖屏那套
         「刹车在右、装备在左」的横排位置，短横屏里两键会贴着战场右侧边缘。 */
      right:27px;
      right:calc(max(14px, env(safe-area-inset-right, 0px)) + 13px);
      width:56px; min-width:56px; height:48px; min-height:48px;
    }
    #touchPad .tpSub[data-touch="brake"]{
      bottom:104px;
      bottom:calc(max(14px, env(safe-area-inset-bottom, 0px)) + 90px);
    }
    #touchPad .tpSub[data-touch="equip"]{
      bottom:158px;
      bottom:calc(max(14px, env(safe-area-inset-bottom, 0px)) + 144px);
    }
    /* 主菜单在 390px 高的横屏里要优先露出六个模式入口：桌面字号会把首屏压到
     * 只剩标题 + 一行卡片。这里只压缩品牌区与模式卡，难度 / 帮助仍可向下滚动。 */
    #overlay .ov-col{padding:12px 16px 14px; border-radius:16px; max-width:760px;}
    .emblem{width:38px; height:38px; margin:0 auto 6px; border-radius:12px; font-size:22px;}
    .logo{font-size:30px; letter-spacing:8px; text-indent:8px;}
    .sub{margin:3px 0 9px; font-size:10.5px; letter-spacing:3px; gap:10px;}
    .sub::before,.sub::after{width:40px;}
    .modes{gap:7px; margin-bottom:7px;}
    .modes button{padding:7px 10px; font-size:14px; gap:8px; border-radius:11px;}
    .modes .ico{width:32px; height:32px; font-size:17px; border-radius:9px;}
    .modes .txt b{font-size:14.5px; letter-spacing:1px;}
    .modes .txt span{font-size:10.5px; margin-top:2px; letter-spacing:.2px;}
    .btn.cont{padding:9px 16px; margin-bottom:7px;}
    .diffGrid{margin-bottom:8px;}
    .diffDesc{margin-bottom:10px;}
    h2.title{font-size:26px; letter-spacing:5px; margin-bottom:8px;}
    .scoreBig{font-size:24px;}
    .stats{margin:10px auto 12px;}
    .rowbtns{gap:10px; margin-top:6px;}
    .high{margin-top:10px;}
    .help{padding:10px 16px; line-height:1.75; font-size:12px;}
    .legend{margin-top:6px;}

    /* ---------- 短横屏弹窗面板密度（v4.6.6） ----------
     * 844×390 原来暂停面板 421px、强化面板 408px、过关面板 516px，首屏只剩
     * 标题 + 一半内容，底部设置 / 重抽 / 跳过要强化都得先滚。这里把「同一张卡片」
     * 的盒模型整体收紧：内边距 26/36 → 12/18、标题 24→22、统计行 6px→2px、
     * 按钮 12px 内边距→7px、帮助行高 1.75→1.5；强化卡图标 46→32、描述行高
     * 1.7→1.45、卡间距 12→8。实测 844×390 暂停 301、强化 273、过关 337，
     * 全部装进 390px 首屏；932×430 同样。640×360 只有过关面板还需约 4px 滚动；
     * 568×320 保留滚动（内容确实放不下），不再出现「桌面字号把面板撑到两倍高」。
     *
     * 这些 card 内控件里有 7 处 `style="margin-top:14px"`（历史写法），
     * 行内声明压过样式表 —— 只有 rowbtns 的 margin 需要 !important 才能收紧；
     * 其余内边距/字号没有行内冲突（标题的 inline font-size 用 !important 单独收）。 */
    #overlay .card{padding:11px 18px 10px; border-radius:14px;}
    /* 强化 / 过关 / 胜利面板的宽度不再由内容「撑」出来：.cards 的 94% 宽度在
     * fit-content 卡片里会解成 472px（844 横屏），三张强化卡只剩 130px 宽。
     * 给这两类面板一个明确的宽度上限，三张卡回到 196px（844）/ 176px（640）。 */
    #overlay .card.gold, #overlay .card.win{
      /* 长写在先：min() 要 Chrome 79 / Safari 13.1。更早的浏览器会整条丢掉这条声明，
         卡片退回 fit-content —— 短横屏下强化卡只剩 130px 宽（.cards 的 94% 宽度
         在 fit-content 卡片里解不出来）。width:100% + max-width:680px 与
         width:min(680px,100%) 的计算结果逐像素相同，两种浏览器都拿到同一宽度。 */
      width:100%; max-width:680px;
      width:min(680px,100%);
    }
    #overlay .card h2.title{font-size:22px !important; letter-spacing:4px !important; margin-bottom:5px;}
    #overlay .card h2.title::after{margin-top:4px; width:40px;}
    #overlay .card .sub{margin:1px 0 5px; font-size:11.5px;}
    #overlay .card .stats{margin:5px auto 6px; font-size:12.5px;}
    #overlay .card .stats td{padding:2px 12px;}
    #overlay .card .rowbtns{margin-top:6px !important; gap:8px;}
    #overlay .card .rowbtns .btn{padding:7px 14px; min-width:104px; font-size:13.5px;}
    #overlay .card .setRow{margin-top:5px; gap:7px;}
    /* v4.9.0：设置行多了「画面」一档，.btn 的 min-width:132px 会让四颗键折成两行，
     * 568×320 下面板正好被顶出首屏。短横屏本来就不该按 132 的点击宽度铺键。 */
    #overlay .card .setRow .btn.mini{padding:5px 12px; font-size:12.5px; min-height:32px; min-width:0;}
    #overlay .card .help.compact{margin-top:5px; padding:5px 10px; line-height:1.5; font-size:11px;}
    #overlay .card .tip{margin-top:5px; font-size:10.5px;}
    #overlay .card .tstat{gap:5px; margin:5px 0 2px;}
    #overlay .card .chip{padding:1px 8px; font-size:10.5px;}
    /* 自定义难度 / 坦克配置 / 房间操控档的参数胶囊在菜单和房间面板里（.ov-col），
     * 不受上面的 .card 规则覆盖 —— 短横屏整层一起抬到 32px，避免 25px 高的小键
     * 在拇指下互相擦碰。 */
    #overlay .cPill{min-height:32px; padding:4px 11px;}
    /* 强化三选一在短横屏固定三列：.cards 的容器查询按弹窗内容宽在 640px 以下
     * 会塌成单列，三张卡竖排后面板高达 785px（640×360 / 568×320 实测）。
     * 最低 460px 起才三列（480×320 这类小横屏也能让整张面板装进首屏），
     * 更窄（如 430×320）仍单列，避免每张卡被压到 110px 以下。 */
    @media (min-width:460px){
      #overlay .cards{margin:7px 0 3px; gap:8px; grid-template-columns:repeat(3,minmax(0,1fr));}
    }
    #overlay .upcard{padding:8px 7px 7px; gap:4px; border-radius:11px;}
    #overlay .uc-ic{width:32px; height:32px; font-size:17px; border-radius:9px;}
    #overlay .uc-name{font-size:13.5px;}
    #overlay .uc-desc{font-size:11px; line-height:1.45; min-height:0; letter-spacing:.2px;}
    #overlay .uc-rar{font-size:9px; padding:1px 8px;}
    #overlay .cardTitle{font-size:11.5px; letter-spacing:2px; margin-top:1px;}
  }

  /* 640×360 这类「够宽、很矮」的横屏：整屏弹窗的内容宽约 616px，仍落在
   * @container (max-width:640px) 的单列档，但压缩后的模式卡每张只需 270px，
   * 双列能让六个入口在 360px 高的首屏全部露出。按**屏幕宽** ≥620 纠回双列；
   * 568 及更窄仍走单列，避免正文区被压到 200px 以下。 */
  @media (orientation:landscape) and (max-height:560px) and (min-width:620px){
    .modes{grid-template-columns:repeat(2,minmax(0,1fr));}
    /* 与容器档补的 .modes.pick 配对：620~640 横屏把主菜单纠回双列时，选择面板也要
       一起纠回双列，否则出现**反向**不一致（主菜单两列 / 选择面板单列）。两条同族
       栅格永远同进同退。 */
    .modes.pick{grid-template-columns:repeat(2,minmax(0,1fr));}
  }

  /* 竖屏触屏：自定义难度 / 坦克配置的参数胶囊原高约 25px、暂停面板的设置键 32px，
   * 都只是贴着 24px 的 WCAG 最低线，拇指点选很容易擦到相邻项。竖屏有充足的纵向
   * 空间（菜单本来就可滚动），把这两类小控件抬到 36px；横屏短视口已经在紧凑档
   * 收到 32px，不再重复抬高，避免暂停面板为了 4px 重新出现滚动。 */
  @media (orientation:portrait) and (pointer:coarse), (orientation:portrait) and (max-width:560px){
    .cPill{min-height:36px; padding:6px 13px;}
    .btn.mini{min-height:36px; padding:7px 14px;}
  }
