/* =========================================================
   CalcElf 组件库 —— 完整设计系统（从 proposal 移植）
   所有真实 DOM 元素 + ui-enhance/theme-app 注入组件都有样式。
   ========================================================= */

/* ===== 基础 ===== */
*{box-sizing:border-box;margin:0;padding:0}
html,body{font-family:var(--font-stack);background:var(--page);color:var(--ink);line-height:1.6;transition:background .5s,color .5s}
button,input,textarea,select{font:inherit;color:inherit}
button{cursor:pointer;border:0}
img{max-width:100%}

/* ==========================================================================
   等级主题色 (v8.13) —— 兑现"等级与特权"弹窗承诺的「解锁该等级主题色」
   --------------------------------------------------------------------------
   属性由 app.js 写：applyMembership() 设 document.body.dataset.lifetime /
   .monthly（L356 起就有，本次【没有改那一处】）。这里只补 CSS。
   金属意象对齐 docs/badges/BADGE-SPEC-v2.txt 第二章：
     Lv1 种子（古铜）· Lv2 嫩芽（银）· Lv3 小树苗（银+金星）· Lv4 大树（金）
     Lv5 开花树（金嵌宝石）· Lv6 金树皇冠（深蓝星空金树）· Lv7 彩虹水晶神树（传奇）
   只定义【4 个变量】，不碰全站按钮/链接主色（--primary 等一律不动），
   用在 .ce-member-avatar / #planPill / .ce-member-plan / .ce-member-level /
   .ce-level-main strong 这几处安全且看得见的地方（每条都带 #12b3a0 兜底）。
   --lv-accent-ring 的 RGBA 由 --lv-accent 派生（rgb(from ...) 失败时整条声明
   失效 → 自动回落到后面那条固定 rgba 兜底，不会出现"没有光环"的死值）。
   ========================================================================== */
body{
  --lv-accent:#12b3a0;      /* 站点青绿：未登录/无 data-lifetime 时的兜底 */
  --lv-accent-soft:#e2f6f3;
  --lv-accent-ring:rgba(18,179,160,.30);
  --lv-accent-grad:var(--lv-accent);
}
body[data-lifetime="1"]{--lv-accent:#8a5620;--lv-accent-soft:#f3e9dc;--lv-accent-ring:rgba(18,179,160,.30);--lv-accent-grad:var(--lv-accent)}
body[data-lifetime="2"]{--lv-accent:#4d6c80;--lv-accent-soft:#e9eff3;--lv-accent-ring:rgba(18,179,160,.30);--lv-accent-grad:var(--lv-accent)}
body[data-lifetime="3"]{--lv-accent:#5b3fc0;--lv-accent-soft:#efeafd;--lv-accent-ring:rgba(18,179,160,.30);--lv-accent-grad:var(--lv-accent)}
body[data-lifetime="4"]{--lv-accent:#6b4a03;--lv-accent-soft:#fbf0d7;--lv-accent-ring:rgba(18,179,160,.30);--lv-accent-grad:var(--lv-accent)}
body[data-lifetime="5"]{--lv-accent:#981055;--lv-accent-soft:#fce8f1;--lv-accent-ring:rgba(18,179,160,.30);--lv-accent-grad:var(--lv-accent)}
body[data-lifetime="6"]{--lv-accent:#1d3f96;--lv-accent-soft:#e6ebf8;--lv-accent-ring:rgba(18,179,160,.30);--lv-accent-grad:var(--lv-accent)}
body[data-lifetime="7"]{--lv-accent:#7a2fc4;--lv-accent-soft:#f2e9fd;--lv-accent-ring:rgba(18,179,160,.30);--lv-accent-grad:linear-gradient(90deg,#e86a6a,#e8b33c,#4fc36b,#3ea8e0,#a86ae8)}
@supports (color:rgb(from #000 r g b / .3)){
  body[data-lifetime]{--lv-accent-ring:rgb(from var(--lv-accent) r g b / .30)}
}

/* ===== 顶部导航 ===== */
.top{background:linear-gradient(135deg,var(--top-grad-a,var(--primary)),var(--top-grad-b,var(--primary2)));color:#fff;transition:background .5s;position:sticky;top:0;z-index:40;box-shadow:0 4px 20px rgba(0,0,0,.1)}
.topin{max-width:1120px;margin:auto;padding:10px 16px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.calc-logo{width:48px;height:48px;object-fit:contain;border-radius:14px;background:#fff;padding:3px;box-shadow:0 3px 0 rgba(0,0,0,.15)}
/* ★★★ v8.8 第 37 轮（产品主人要求：按【实测对比度】定 CalcElf 的字色）
   为什么不是照搬图4 那套 ✗✗：图4 是【白底】的 ✗ 而头部是【青蓝渐变】的 ✓
   实测（真 Chromium 截图取像素 ✗ WCAG 2.1 公式 ✗ 见交付报告）：
     单词背后底色 = #114d7e（最暗 L=0.072）~ #12648d（最亮 L=0.105）
     图4 的 #12b3a0（Calc）在这个底上只有 2.47:1 ✗ 远低于 4.5:1 ⇒ 会糊在青蓝底上 ✓（产品主人的担心是对的）
     图4 的 #123a72（Elf）更低 ⇒ 1.27:1 ✗ 几乎看不见 ✓
   ⇒ 定稿：Calc 用【青绿家族里最亮的一档】#7fe9d8 = 6.10:1 ✓ Elf 保持纯白 = 8.80:1 ✓
     两者都稳过 4.5:1（比"大号粗体 3:1"底线更严 ✗ 按产品主人要求的 4.5 选的 ✓）
     同时保留"Calc 绿 / Elf 白"的【前后对比】层次 ✗ 与图4 同一套叙事 ✓
   ★ 只加两条选择器 ✗ .brand h1 / .brand p 的其它规则一个都没动 ✓ */
.brand h1 .calc{color:#7fe9d8}
.brand h1 .elf{color:#ffffff}
/* ★★ 窄屏覆盖（实测驱动 ✗ 不是拍脑袋 ✓）：
   390px 下词后底色最亮点实测 #117e9f（L=0.180）⇒ #7fe9d8 只剩 3.23:1 ✗ 不达标 ✗
   而同一个底上纯白是 4.66:1 ✓ 过 4.5 ✓ ⇒ 窄屏把 Calc 也压成纯白 ✓
   （1440px 下词后底色最亮 #12648d ✗ #7fe9d8 有 6.10:1 ✓ 所以只在窄屏切 ✓）*/
@media (max-width:512px){.brand h1 .calc{color:#ffffff}}
.brand h1{margin:0;font-family:var(--display);font-size:clamp(22px,1.75vw,28px);font-weight:700}
.brand p{margin:2px 0 0;font-size:clamp(11px,1.05vw,17px);opacity:.9}
.top-actions{margin-inline-start:auto;display:flex;align-items:center;gap:8px}
/* ★ 对比度（全站巡检 25 条）：玻璃底原来是 rgba(255,255,255,.18) ✗ 压在 hero 的青绿上
   实测 #langLabel / #avatarLabel 只有 2.49 / 2.21:1（390px 下 3.05 / 4.18）✗ 远低于正文 4.5:1 ✓
   ⇒ 把【背景压暗】而不是把字放大 ✗（白字不动 ✓ 字号不动 ✓）
   rgba(0,0,0,.42) 叠在青绿上 ⇒ 白字 ≥5.8:1 ✓ 与其它"实底"控件的观感一致 ✓ */
.topbtn{background:rgba(0,0,0,.42);border:1.5px solid rgba(255,255,255,.3);color:#fff;border-radius:99px;padding:8px 16px;font-size:13px;font-weight:700;transition:background .2s}
.topbtn:hover{background:rgba(255,255,255,.3)}
.lang-wrap{position:relative}
.lang-menu{display:none;position:absolute;top:calc(100%+8px);inset-inline-end:0;min-width:180px;background:var(--card);color:var(--ink);padding:8px;border-radius:16px;box-shadow:0 16px 44px rgba(0,0,0,.18);z-index:60;border:1px solid var(--line)}
.lang-menu.open{display:block}
.lang-menu button{width:100%;border:0;background:transparent;text-align:start;padding:10px 12px;border-radius:10px;color:inherit;font-size:13px;font-weight:600}
.lang-menu button:hover{background:var(--card-soft)}
.lang-menu button.active{background:var(--card-soft);color:var(--primary);font-weight:var(--ce-fw-head)}
/* ★★★ v8.8 · 多语言扩展 第 1 批 ✗✗ 语言项的 ISO 代码徽章（纯代码 ✗ 不用国旗 ✓ 用户选定的 ✓）
   ★ 为什么【紧挨在这里】✗：语言菜单的全部样式就是上面那 6 行（57–62 ✓）
     徽章是菜单的一部分 ⇒ 不另开一节 ✗ 更不塞内联样式 ✓
   ★ 颜色【只用既有 token】✗ 一个新色都没造：
     底 = --card-soft（.lang-menu button:hover 的既有底色 ✓）
     边 = --line（.lang-menu 自己的描边 ✓）
     字 = --muted ✗ 选中项 = --primary（.lang-menu button.active 的既有口径 ✓）
     字重 = --ce-fw-strong ✗ 字体 = --ce-font-body（design-tokens.css ✓）
   ★ RTL ✗ ar / fa / he 是从右到左 ⇒ 间距必须用【逻辑属性】margin-inline-end ✓
     （ltr 下落在徽章右侧 ✗ rtl 下自动翻到左侧 ✗ 位置才对 ✓）
     绝不用 margin-right / margin-left ✗ 也不用 float ✗
     —— tests/languages-registry.js 把这两条钉死 ✓ 谁改回物理方向就变红 ✓
   ★ 移动端 ✗ 现有 11 项实测 scrollHeight = 465px（1440 视口真浏览器量的 ✓）
     37 条全部上线后会涨到 ≈1560px ⇒ 给菜单一个上限 + 滚动 ✓
     min(72vh,560px) ✗ 560 > 465 ⇒ 本批（仍 11 项）【不出现滚动条】✓
     ⇒ 现有 11 语言的视觉零变化 ✓ 等 26 条逐个打开时才开始滚 ✓
   ★ 本块【不改】.lang-menu button 的既有规则（width / border / padding / text-align 一行没动 ✓）*/
.lang-menu .lang-badge{display:inline-block;min-width:42px;margin-inline-end:8px;padding:1px 5px;border:1px solid var(--line);border-radius:6px;background:var(--card-soft);color:var(--muted);font-family:var(--ce-font-body);font-size:10px;font-weight:var(--ce-fw-strong);letter-spacing:.05em;line-height:1.4;text-align:center;white-space:nowrap;vertical-align:middle}
.lang-menu button.active .lang-badge{border-color:var(--primary);color:var(--primary)}
.lang-menu{max-height:min(72vh,560px);overflow-y:auto}
/* ★★★ v8.8 · 多语言扩展 第 2 批 ✗✗ 窄屏溢出修复（用户已批准 ✓）
   ★ 修的是什么 ✗：本文件 58 行的 .lang-menu 是 `inset-inline-end:0`（右对齐到 .lang-wrap ✓）
     在 375px 视口上 .lang-wrap 的右边缘只到 x≈166 ⇒ 180px 宽的菜单【向左溢出到 x≈-14】✗
     实测（真浏览器 · 375×667 · HEAD 纯净副本 = 改前就有 ✗ 不是我引入的 ✓）：
       menuLeft = -47（设备像素 ✗ 已含 zoom 1.18）⇒ 徽章和语言名左边被【切掉】✓
       ★ HEAD 纯净副本量到【完全相同】的 -47 / 166 / 212 / 550 ⇒ 与本批改动无关 ✓
   ★ 修法 ✗：改成【左对齐到 .lang-wrap】+ 放开 min-width + 卡住视口宽 ✓
     inset-inline-start:0 是逻辑属性 ⇒ ar / fa / he 下自动变成右对齐 ✓
   ★ 为什么是 ≤520px ✗：520 以上 .lang-wrap 的右边缘离视口左沿已经 ≥180px ⇒ 不会溢出 ✓
     （★ 若第 3 批选了"桌面多列 / 手机单列"的方案 ✗ 那条媒体查询会【覆盖】本块 ✓
        两条的声明不打架（同族 + 后者在后）⇒ 直接换掉即可 ✗ 不用先删这里 ✓）
   ★ 只动定位与宽度 ✗ 一个字都没改字体 / 颜色 / 内边距 / 条目结构 ✓ */
@media (max-width:520px){.lang-menu{inset-inline-end:auto;inset-inline-start:0;min-width:0;max-width:calc(100vw - 24px)}}
/* ★★★ v8.8 · 多语言扩展 第 3 批 ✗✗ 语言菜单【桌面多列 / 手机单列】断点阶梯（用户已拍板：三列 ✓）
   ★ 用户三条口述决定 ✗：① 列数 = 三列（桌面 ✓）② 短名 = 不做（保持 "Português (Brasil)" 全长名 ✓）
                          ③ 手机 = 单列 ✓
   ── 断点为什么是这三个 ✗（全部来自本机真浏览器实测 ✗ 不是拍的 ✓）────────────────────
   ★ 位置的硬约束 ✗：菜单是 .lang-wrap 里的 position:absolute + inset-inline-end:0
     ⇒ 【菜单右边缘 ≡ 视口宽 − 209 设备px】（209 = 右内边距 + 语言按钮宽度 ✓ 各视口恒定 ✓）
     ⇒ 反过来：菜单的左边缘 x = 视口宽 − 209 − 菜单宽 ✓
   ★ 三列的硬数字 ✗：37 条语言 · max-content 列宽 ⇒ 菜单宽 737 设备px（= 625 CSS px ✗ zoom 1.18）
     ⇒ 需要 视口宽 − 209 ≥ 737 ⇒ 视口 ≥ 946px ✓
     ⇒ 768px 视口下左边缘 x = −178 ✗【被切掉】✗✗ ⇒ 768 那一档【不能三列】✓
     ⇒ 实测分界：1000px ✓ 放得下（左边缘 x = 54 ✓）✗ 999px ✗ 左边缘 x = −17（差 17px ✓）
       所以三列的门槛取【1000px】（整数 + 留 3px 余量 ✓）
   ★ 两列的硬数字 ✗：37 条 · 两列 ⇒ 菜单宽 503 设备px ⇒ 需要 视口 ≥ 712px ✓
     ⇒ 768 / 712–999 之间放得下宽度，但 37 条单列高度 ≈1560px 远超 max-height:min(72vh,560px)
     ⇒ 【这一档一定会滚动 ✗ 用宽度换行数 ⇒ 13 行 ⇒ 把滚动量降到最小 ✓】（这个宽度下没办法 ✓）
     ⇒ 768 以下再两列就【横向溢出】✗（菜单宽 503 > 视口 − 209 可利用空间）⇒ 门槛取 768px ✓
   ★ 单列 + 手机溢出修复的门槛 ✗：≤520px 是上一批（第 2 批）的旧台阶 ✓
     它当时的前提是【桌面定位 + 永远是单列】✗ 那时 521–767 实测安全 ✓
     本批把三档统一 ⇒ 521–767 现在跑的是【单列 + 左对齐到 .lang-wrap】✗ 前提变了 ✓
     ⇒ 必须【重新验证】521 / 640 / 767 三个视口 ✗ 不能沿用上一批的结论 ✓
     ⇒ 本条 (max-width:767px) 与上一条 (max-width:520px) 声明同族 ✗ 本条在后 ⇒ 520 以下【本条生效】✓
        上一条就当历史留档 ✗ 不删（删了会动 tests/languages-registry.js 的注释行号对照 ✓）
   ── 三档的公共部分 ✗ 为什么这么写 ────────────────────────────────────────────
   ★ min-width:0 ✗ 必须写：基础规则是 min-width:180px ✗ 而 max-content 列宽在多列下会被它顶宽 ✓
   ★ .lang-menu button{width:auto;min-width:0} ✗ 基础规则的 width:100% 是【给单列铺满】用的 ✓
     放进格子里 ⇒ 每个格子都撑到整行宽 ✗ 三列立即退化成一列 ✓ ⇒ 必须放开 ✓
   ★ .lang-name{white-space:nowrap} ✗ max-content 列宽的前提 ✓
     ⇒ 全长名（Português (Brasil) / English (Australia) ✓）不换行不截断 ✓
     ⇒ 用户决定②"不做短名"就是靠这一条兜住的 ✓
   ★ max-width:calc(100vw - 32px) 是护栏 ✗ 万一将来某条语言的原生名超长 ⇒ 也不出横向滚动条 ✓
   ★ 一个字都没改字体 / 颜色 / 内边距 / 条目结构 ✗ 只动【布局与定位】✓ */
@media (min-width:1000px){
  .lang-menu.open{display:grid;grid-template-columns:repeat(3,max-content);column-gap:10px;row-gap:0;min-width:0;max-width:calc(100vw - 32px);grid-auto-flow:column;grid-template-rows:repeat(13,max-content)}/* ★★★ v9.112 列优先填充 ✗ 不是行优先 ✓（产品主人 2026-10-13 拍板 ✓）判据 = grid-auto-flow:column + 显式行数 ✓ 行数 = ceil(37 门 / 3 列) = 13 ✓★ 为什么不在 JS 里重排 ✗：列数由本文件的媒体查询决定 ✗ JS 拿不到 ✗ 交给 CSS 才是唯一真源 ✓★ 语言门数变了（例如加到 40 门）⇒ 本行行数要跟着重算 ✗ 13 行可容 39 门 ✓ 超过就必须改这一行 ✓★ 只加两个声明 ✗ 原有的列数/间距/宽度一个字节没动 ✓ 本注释写在【行尾】✗ 不新增行 ⇒ ui-spec-drift 零行号漂移 ✓ */
  .lang-menu button{width:auto;min-width:0}
  .lang-menu .lang-name{white-space:nowrap}
}
@media (min-width:768px) and (max-width:999px){
  .lang-menu.open{display:grid;grid-template-columns:repeat(2,max-content);column-gap:10px;row-gap:0;min-width:0;max-width:calc(100vw - 32px);grid-auto-flow:column;grid-template-rows:repeat(19,max-content)}/* ★★★ v9.112 同上（两列档 ✓ 768–999px）行数 = ceil(37 / 2) = 19 ✓★ 超过 38 门时本行必须重算 ✓ */
  .lang-menu button{width:auto;min-width:0}
  .lang-menu .lang-name{white-space:nowrap}
}
@media (max-width:767px){
  .lang-menu.open{display:grid;grid-template-columns:1fr;column-gap:10px;row-gap:0;inset-inline-end:auto;inset-inline-start:0;min-width:min(180px,calc(100vw - 24px));max-width:calc(100vw - 24px);width:max-content}
  .lang-menu button{width:auto;min-width:0}
  .lang-menu .lang-name{white-space:nowrap}
}

/* 头像下拉 */
#avatarMenu{position:relative}
#avatarBtn{display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.18);border:1.5px solid rgba(255,255,255,.3);border-radius:99px;padding:4px 12px 4px 4px;color:#fff;cursor:pointer;transition:background .2s}
#avatarBtn:hover{background:rgba(255,255,255,.3)}
#avatarBtn img{width:30px;height:30px;border-radius:50%;object-fit:cover;background:#fff}
#avatarBtn span{font-size:13px;font-weight:700}
#avatarDrop{display:none;position:absolute;top:calc(100%+8px);right:0;min-width:200px;background:var(--card);color:var(--ink);border-radius:16px;box-shadow:0 16px 44px rgba(0,0,0,.18);z-index:60;padding:6px;border:1px solid var(--line)}
#avatarDrop button{width:100%;border:0;background:transparent;text-align:start;padding:10px 12px;border-radius:10px;font-size:13px;font-weight:600;color:inherit;cursor:pointer}
#avatarDrop button:hover{background:var(--card-soft)}

/* ===== 布局 ===== */
.wrap{max-width:1120px;margin:auto;padding:16px}
.grid{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:16px}
@media(max-width:900px){.grid{grid-template-columns:1fr}}

/* ===== 卡片 ===== */
.card{background:var(--card);border:1.5px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:18px;margin-bottom:14px;transition:background .5s,border .5s,box-shadow .3s}
.card:hover{box-shadow:0 12px 32px rgba(0,0,0,.1)}

/* ===== 按钮（Q弹厚底） ===== */
.btn{display:inline-block;text-align:center;padding:12px 20px;border:0;border-radius:14px;font-family:var(--display);font-weight:600;font-size:15px;color:#fff;background:linear-gradient(135deg,var(--primary),var(--primary2));box-shadow:var(--btn-shadow);cursor:pointer;transition:transform .1s,box-shadow .15s}
.btn:active{box-shadow:var(--btn-shadow-press);transform:translateY(3px)}
.btn.primary{background:linear-gradient(135deg,var(--primary),var(--primary2))}
.btn.secondary{background:var(--card-soft);color:var(--ink);border:1.5px solid var(--line);box-shadow:none}
.btn.gold{background:var(--accent);color:#fff}
.btn.danger{background:var(--danger);color:#fff}
.btn.success{background:var(--success);color:#fff}
.btn.ghost{background:transparent;color:var(--ink);border:2px solid var(--line)}
.btn.google-btn{display:flex;align-items:center;justify-content:center;gap:10px;background:#fff;color:#333;border:1.5px solid var(--line);width:100%;box-shadow:none}
.btn.google-btn:hover{background:#f8f9fc}
/* ★★★ 第三方登录按钮组 ✗✗ Facebook / Microsoft / LinkedIn（用户第 ① 条 ✓）
   ── 上面 92-93 行【一个字都没动】✗ 只补三件 Google 按钮从来不需要的事 ✓ ──
      · 类名说明 ✗ `.google-btn` 在这里 = "白底描边的第三方登录按钮"的【既有类名】✗ 不是"只给 Google 用"✓
        所以三个新按钮【沿用】它 ⇒ 布局 / 白底 / 描边 / 悬停 / 圆角 / 字号全部复用既有值 ✗
        不新增任何尺寸、颜色、圆角 ✗（硬红线 ④ ✓）三个新按钮与 Google 按钮因此【逐像素一致】✓
      · ① 按钮之间的间距 ✗ Google 按钮（index.html:888）在它之前【从来没有过相邻按钮】✗
           不加这条三个新按钮会贴成 0 间距 ✓ 10px 沿用 .btn.google-btn 自己的 gap:10px（上面 92 行 ✓）
           ⇒ 是"复用既有值"而不是发明新值 ✓ 用相邻兄弟选择器 ⇒ 只管"第二个及以后"✓
           将来加第 5 家自动继承 ✓ 不用再改这一行 ✓
      · ② Microsoft 图标 17px ✗ 它的四格 logo 是【正方形铺满】✗
           与 Google 的圆环 / Facebook 的圆 / LinkedIn 的圆角方块比会显大 ✓ 只收这一家 ✓
           （Google 的 18×18 内联属性照旧 ✓ 其它两家图形本来就有内边距 ✓）
      · ③ RTL 镜像 ✗ ar / fa 两种语言下 html[dir=rtl] ✗
           Microsoft 的四格 logo 有【固定方位】✗ 不镜像会左右反 ✓
           安全做法 = 只翻这一家的图形容器 ✗ 不动文字 ✓
           （与 app.js:1558 那次"→ 箭头把 Sign in / Sign up 排错"同一类既有问题 ✓）*/
.btn.google-btn+.btn.google-btn{margin-top:10px}
#microsoftLoginBtn svg{width:17px;height:17px}
[dir="rtl"] #microsoftLoginBtn,#microsoftLoginBtn svg{transform:scaleX(-1)}

/* ===== 表单 ===== */
label{display:block;font-family:var(--display);font-weight:600;font-size:13px;margin-bottom:6px}
input,textarea,select{width:100%;padding:12px 14px;border:2px solid var(--line);border-radius:14px;background:var(--card);color:var(--ink);font-size:14px;font-weight:600;outline:none;transition:border-color .2s,box-shadow .2s}
input:focus,textarea:focus,select:focus{border-color:var(--primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 15%,transparent)}
textarea{resize:vertical;min-height:80px}
input[type="checkbox"]{width:22px;height:22px;accent-color:var(--primary);flex-shrink:0}
input[type="file"]{padding:10px;border:2px dashed var(--line);border-radius:14px;background:var(--card-soft);cursor:pointer}
/* ★ 下面这条 `background:var(--primary)` 只对【#ceCamShot】还有效（它是隐藏的 ✗ 看不出颜色 ✓）。
   #file 那个可见按钮【不再吃 --primary】：文件末尾有一条 #file 的 ID 级覆盖（含悬停/按下态 ✓）。
   ★ 订正原因：本文件:444 的 aurora 覆盖已从 var(--primary2) 改成具体色值 ⇒ 深色主题不再走 --primary2 ✓ */
input[type="file"]::file-selector-button{background:var(--primary);color:#fff;border:0;border-radius:10px;padding:8px 14px;margin-inline-end:10px;font-weight:700;cursor:pointer}

/* 密码强度 */
.strength{height:8px;border-radius:99px;background:var(--line);overflow:hidden;display:flex;gap:3px;margin-top:8px}
.strength i{flex:1;border-radius:99px;background:var(--line)}
.strength i.on:nth-child(1){background:var(--danger)}
.strength i.on:nth-child(2){background:var(--accent)}
.strength i.on:nth-child(3){background:#fbbf24}
.strength i.on:nth-child(4){background:var(--success)}
.hint{font-size:12px;color:var(--muted);font-weight:600;margin-top:5px}
.pw-check{display:flex;align-items:center;gap:6px;font-size:12px;font-weight:700;margin:3px 0}
.pw-check .ok{color:var(--success)}
.pw-check .no{color:var(--muted)}
.pw-bar{height:8px;border-radius:99px;background:var(--line);overflow:hidden;margin:8px 0}
.pw-bar i{display:block;height:100%;background:var(--success);transition:width .3s}

/* ===== 标签 ===== */
.pill{display:inline-block;padding:4px 12px;border-radius:99px;background:var(--card-soft);color:var(--primary);font-size:11px;font-weight:700}
.muted{font-size:13px;color:var(--muted);line-height:1.6}
.age-pill{display:inline-block;background:var(--card-soft);border:2px solid var(--line);padding:8px 14px;border-radius:14px;font-weight:700;font-size:13px;margin:4px 4px 0 0}

/* ===== 上传区 ===== */
.upload{border:3px dashed color-mix(in srgb,var(--primary) 40%,var(--line));border-radius:20px;padding:28px 16px;text-align:center;background:var(--card-soft);cursor:pointer;color:var(--primary);transition:background .2s}
.upload:hover{background:color-mix(in srgb,var(--primary) 8%,var(--card-soft))}
/* ★ 对比度：.muted 在卡片软底（#EAF3FB）上只有 4.24:1 ✗ 差一点点 ✗
   ⇒ 只给这一个元素加深一档 ✗ 不动全局 --muted（色值收敛是另一步 ✓）*/
#uploadSmall{color:#556577}
.upload b{display:block;margin-bottom:5px;font-family:var(--display);font-weight:600;font-size:15px}
.preview{display:none;margin-top:10px}
.preview img{max-width:100%;max-height:250px;border-radius:14px}

/* ===== 解题区 ===== */
.loading{display:none;text-align:center;padding:28px}
.loading.show{display:block}
.spin{width:36px;height:36px;border:4px solid var(--line);border-top-color:var(--primary);border-radius:50%;animation:sp .8s linear infinite;margin:0 auto}
@keyframes sp{to{transform:rotate(360deg)}}
/* ★★★ 认证按钮里的【内联转圈】✗✗ —— 复用【上面那两行】既有的 .spin / @keyframes sp ✗ 不新造动画 ✓
   ── 为什么必须有这一条 ✗：.spin 是 36px + 4px 描边 ✗ 塞进按钮会把按钮撑高 ⇒ 按钮跳动 ✓
      而这一条把它缩到 16px（= 按钮里 18px 品牌 SVG 同级 ✓
      实测：按钮高度进去前后都是 48px ✗ 零跳动 ✓）
   ── 颜色 ✗：用 currentColor 当缺口色 ✗ 它继承按钮自己的文字色 ✓ 无论按钮是深底白字
      还是白底青字 ✗ 那个缺口都看得见 ✓（不写死一种颜色 ✓）
   ── .calf-spin-in ✗ 曾经在这里 ✗✗ 已经撤掉 ✓ 撤销理由必须留档 ✓✓
      ── 病（★ 实测抓出来的 ✓ 不是想出来的 ✓）──
        第一版给转圈加了一条入场动画 `.calf-spin-in{animation:calfSpinIn .18s ease-out}`
        而 .spin 自己的 `animation:sp .8s linear infinite` 就在同一元素上 ✓
      ⇒ animation 是【简写】✗ 它会把 animation-name 整个覆盖掉 ✓
      ⇒ 实测 getComputedStyle(spin).animationName === 'calfSpinIn' ✗✗
      ⇒ 症状：转圈只转 0.18 秒 ✗ 然后【永远停住】✓ 比不加还坏 ✓（"转圈"变成了一个静止的缺口环 ✓）
      ── 为什么不改成两条动画并列 ✗ ──
        并列要写成 animation:sp .8s linear infinite,calfSpinIn .18s ease-out ✓
        但那条简写会连同上面 .spin 那一条一起被复制过来 ✗ 将来改 .spin 就会两处不同步 ✓
        （本仓的既有教训：规则写两份 ⇒ 迟早漏一份 ✓ 见 tests/form-spacing.js:12-13 ✓）
      ⇒ 定案：入场那一下【不要了】✗ 转圈直接从第一帧就转 ✓ 这才是"在运行"该有的样子 ✓✓ */
.spin.calf-spin-btn{width:16px;height:16px;border-width:2px;border-top-color:currentColor;margin:0;display:inline-block;vertical-align:-3px;flex:0 0 auto}
/* ★ 减弱动效 ✗：转圈本身在 427 行那条全局 reduce 规则下已经会停 ✓
   但"停了"必须【还是个圈】✗ 不许变成一个看不出来的空 div ✓ ⇒ 这里给它一个静态缺口环 ✓
   （与 ce-elf-think 在 2758 行的处理同一条精神 ✗ 不许让等待态变成空白 ✓）*/
@media(prefers-reduced-motion:reduce){.spin.calf-spin-btn{animation:none!important;border-color:var(--line);border-top-color:var(--primary)}}
.result{display:none}
.head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px}
.head h2{margin:0;font-family:var(--display);font-size:17px}
.question{padding:14px;background:var(--card-soft);border-radius:14px;line-height:1.8;font-weight:600}
.solution{padding:14px;margin-top:10px;border:1.5px solid var(--line);border-inline-start:4px solid var(--primary);border-radius:14px}
.step{display:grid;grid-template-columns:30px 1fr;gap:10px;padding:10px 0;border-top:1px dashed var(--line)}
.step:first-child{border-top:0}
.num{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;background:var(--primary);color:#fff;font-size:13px;font-weight:var(--ce-fw-head)}
.formula{display:inline-block;margin-top:6px;background:var(--card-soft);padding:6px 10px;border-radius:8px;color:var(--primary);direction:ltr}

/* 流式解题 UI */
.skel{height:14px;border-radius:8px;background:linear-gradient(90deg,var(--card-soft) 25%,var(--line) 50%,var(--card-soft) 75%);background-size:200%;animation:sk 1.3s infinite;margin-bottom:8px}
@keyframes sk{to{background-position:-200% 0}}
.typing{display:inline-block;width:8px;height:16px;background:var(--primary);animation:blink .8s infinite;vertical-align:middle}
@keyframes blink{50%{opacity:0}}

/* ===== Chat ===== */
.chat{height:300px;overflow:auto;border:1.5px solid var(--line);border-radius:14px;padding:10px;background:var(--card-soft)}
.msg{max-width:88%;margin:6px 0;padding:10px 12px;border-radius:12px;white-space:pre-wrap;font-size:13px;line-height:1.6}
.msg.user{margin-inline-start:auto;background:var(--primary);color:#fff}
.msg.assistant{background:var(--card);border:1.5px solid var(--line)}
/* v5.7.1 助理"思考中"三点渐变等待：蓝→紫→橙波浪推进 */
.msg.calf-typing{display:flex;align-items:center;gap:7px;padding:14px 18px}
.msg.calf-typing i{width:10px;height:10px;border-radius:50%;display:inline-block;animation:calfTyping 1.15s infinite ease-in-out both}
.msg.calf-typing i:nth-child(1){background:#5b7cfa;animation-delay:-.32s}
.msg.calf-typing i:nth-child(2){background:#8b6cf6;animation-delay:-.16s}
.msg.calf-typing i:nth-child(3){background:#ff9f43}
@keyframes calfTyping{0%,80%,100%{transform:scale(.5);opacity:.4}40%{transform:scale(1.25);opacity:1}}

/* ===== Credits 侧栏 ===== */
.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
.stat{padding:12px 8px;background:var(--card-soft);border-radius:12px;text-align:center}
.stat b{display:block;font-size:18px;font-family:var(--display)}
.stat span{font-size:11px;color:var(--muted)}
.themebar{height:8px;border-radius:99px;background:var(--line);overflow:hidden}
.themebar i{display:block;height:100%;width:20%;background:linear-gradient(90deg,var(--primary),var(--accent));transition:width .4s}
.credit-meter-icon{font-size:28px}
.water-can-btn{cursor:pointer;font-size:24px}

/* Credits 四层 */
.credits-tier{display:flex;align-items:center;gap:10px;padding:10px 14px;border-radius:14px;margin:6px 0;font-size:13px;font-weight:700;border:1.5px solid var(--line)}
.credits-tier.rich{border-color:var(--success);background:color-mix(in srgb,var(--success) 8%,var(--card))}
.credits-tier.good{border-color:var(--primary)}
.credits-tier.low{border-color:var(--accent)}
.credits-tier.alert{border-color:var(--danger);background:color-mix(in srgb,var(--danger) 8%,var(--card))}

/* ===== 仪表盘三卡 ===== */
.dash-row{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:14px 0}
.dash-card{background:var(--card-soft);border-radius:16px;padding:14px;text-align:center;border:1.5px solid var(--line)}
.dash-card b{display:block;font-family:var(--display);font-weight:700;font-size:22px}
.dash-card span{font-size:11px;color:var(--muted);font-weight:700}

/* ===== Modal ===== */
.modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:100;align-items:center;justify-content:center;padding:14px;backdrop-filter:blur(4px)}
.modal.open{display:flex}
.modalbox{width:min(560px,96vw);max-height:90vh;overflow:auto;background:var(--card);color:var(--ink);border-radius:var(--radius);padding:24px;position:relative;box-shadow:0 28px 80px rgba(0,0,0,.28)}
.close{position:absolute;right:16px;top:16px;border:0;background:none;font-size:26px;color:var(--muted);cursor:pointer}
.close:hover{color:var(--ink)}
.auth-tabs{display:flex;gap:8px;background:var(--card-soft);padding:6px;border-radius:14px;margin-bottom:18px}
.auth-tab{flex:1;border:0;background:transparent;padding:10px;border-radius:10px;font-weight:700;color:var(--ink-2,#64748b);cursor:pointer;transition:all .2s}
.auth-tab.active{background:var(--card);color:var(--accent-ink,var(--primary));box-shadow:0 2px 8px rgba(0,0,0,.08)}/* ★★ 批 B：未选中原来吃 --muted（18px/700 ✗ 不是大字 ⇒ 要 4.5）✗ 实测 cloud 4.47 / story 2.96 ✗
   选中态吃 --primary ✗ 实测 story 2.96（#e07a55 压白纸）✗ cloud 4.47 也差 0.03 ✓
   ⇒ 未选中改走【小字口径 --ink-2】（实测 ≥5.4 ✓）✗ 选中改走 --accent-ink（=--primary 压深 62% ⇒ 6.60~15.47 ✓）
   ★ aurora 的 --accent-ink 仍是 --primary（5.70 ✓ 本来就过）⇒ 深色主题零回归 ✓ 不用 !important ✓ */
.auth-divider{display:flex;align-items:center;margin:16px 0;color:var(--muted);font-size:13px}
.auth-divider::before,.auth-divider::after{content:'';flex:1;height:1px;background:var(--line)}
.auth-divider span{padding:0 10px}

/* ===== 套餐/购买 ===== */
.plans{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
@media(max-width:700px){.plans{grid-template-columns:1fr}}
.plan{border:2px solid var(--line);border-radius:16px;padding:14px;text-align:center;background:var(--card)}
.plan.recommended,.plan.hot{border:2px solid var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 15%,transparent)}
.plan .price{font-size:24px;font-weight:var(--ce-fw-head);margin:6px 0;font-family:var(--display)}
.plan .ribbon{position:absolute;top:-10px;left:50%;transform:translateX(-50%);background:var(--accent);color:#fff;font-size:10px;font-weight:800/* ★★ 第 2 步例外 ①：白字压在 --accent #f59e0b 上只有 2.15:1 ✗ 连 AA Large(3:1) 都不达 ✗ 10px 这么小的字 ✗ 字重确实在替对比度打工 ⇒ 故意保留 800 ✓ 第 3 步把底色改深后即可降到 700 ✓ */;padding:3px 12px;border-radius:99px}

/* ===== Toast ===== */
.toast{position:fixed;bottom:28px;left:50%;transform:translateX(-50%) translateY(20px);padding:13px 22px;border-radius:16px;font-weight:700;font-size:14px;opacity:0;pointer-events:none;transition:all .3s;z-index:999;box-shadow:0 12px 32px rgba(0,0,0,.25)}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
.toast.success{background:var(--toast-ok-bg,var(--success));color:var(--toast-ink,#fff)}
.toast.info{background:var(--toast-info-bg,var(--primary));color:var(--toast-ink,#fff)}
.toast.error{background:var(--toast-err-bg,var(--danger));color:var(--toast-ink,#fff)}

/* ===== 主题面板 ===== */
.theme-panel{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
.theme-opt{border:2px solid var(--line);border-radius:16px;padding:12px;cursor:pointer;text-align:center;transition:all .2s;background:var(--card)}
.theme-opt:hover{border-color:var(--primary)}
.theme-opt.active{border-color:var(--primary);background:var(--card-soft)}
.theme-opt .swatch{width:44px;height:44px;border-radius:50%;margin:0 auto 6px}
.theme-opt .nm{font-family:var(--display);font-weight:600;font-size:14px}
.theme-opt .tg{font-size:11px;color:var(--ink-2,var(--muted))}
.theme-opt img{width:48px;height:48px;object-fit:contain;margin:0 auto;display:block}

/* ===== 移动端底部 Tab ===== */
.mobile-tabbar{display:none}
@media(max-width:640px){
  .mobile-tabbar{display:flex;position:fixed;bottom:0;left:0;right:0;background:var(--card);border-top:1.5px solid var(--line);z-index:50;padding:6px 0 calc(6px + env(safe-area-inset-bottom))}
  .mobile-tabbar button{flex:1;border:0;background:transparent;color:var(--muted);font-size:11px;font-weight:700;display:flex;flex-direction:column;align-items:center;gap:2px}
  .mobile-tabbar button.on{color:var(--primary)}
}

/* ===== 开关 ===== */
.switch{display:flex;align-items:center;gap:8px}
.switch input{width:auto}

/* ═══════════════════════════════════════════════════════════════════
   ★★ 第 10 轮：「勾选框 + 文字」的行统一对齐规则 ✗✗（根治 ✓）
   为什么抽成一条规则：这已经是【第三次同型事故】✗
     · 第 7 轮：同意框【上下】没对齐 ✗（align-items:flex-start + 手算 margin ✗）
     · 第 9 轮：同意框【左右】没对齐 ✗（两行 font-size / line-height 不同 ✗）
     · 本轮  ：解题表单的隐私勾选框【上下】没对齐 ✗
               （#privacyRow 内联 align-items:flex-start ✗ 盖掉了 .switch 的 center ✗）
   三次的根因完全一样 ✓：勾选框是固定尺寸（13px ✓）
     而旁边文字的【字号/行高】各行不同 ✓ → 对齐基准就不一致 ✗
   ⇒ 所以不再逐个修 ✗ 抽成一条规则 ✓✓
     以后任何"勾选框 + 文字"的行，只要挂上 .ce-consent-row 或 #privacyRow
     就自动获得正确对齐 ✓ 新页面直接复用 ✓✓
   规则三条 ✓：
     ① align-items:center ✓ 由浏览器算居中 ✓ 永远不错位 ✓
     ② 行高钉死 20px ✓（两行必须一致 ✓ 否则左右也会偏 ✗）
     ③ 勾选框四向 margin 清零 + flex 不伸缩 ✓ 不允许任何偏移 ✗
   ═══════════════════════════════════════════════════════════════════ */
.ce-consent-row,
#privacyRow{
  align-items:center!important;        /* ★ 内联写过 flex-start 也压得住 ✓ */
  line-height:20px!important;
}
.ce-consent-row .kcbx,
#privacyRow .kcbx{
  flex:0 0 13px!important;
  margin:0!important;                  /* ★ 四向清零 ✓ 不许左/上偏移 ✗ */
  align-self:center!important;         /* ★ 双保险 ✓ 父级被改也不怕 ✓ */
}

/* ═══════════════════════════════════════════════════════════════════
   ★★★ 用户可控的「界面动画」开关 ✗✗（#motionRow ✓ 三态分段按钮 ✓）
   ★ 为什么【自己写】样式 ✗ 不复用 ⚙️ 学习设置面板的 .sp-seg / .sp-segbtn ✗✗：
     那两条是 settings-panel.js 用 JS 注入到 <head> 的 ✓ 注入【晚于】本文件 ✓
     一旦它注入失败（脚本没加载 / 被拦截 ✓）我的按钮就没样式了 ✗
     ⇒ 自持样式 ✓ 不依赖任何别的脚本 ✓（行为与外观都可预期 ✓）
   ★ 三态按钮的视觉与既有 .sp-segbtn / .stage-pill 同族 ✓ 不引入新设计语言 ✓
   ★ 移动端（390px ✓）三列会变窄 ⇒ 用 clamp() 让字跟着缩 ✓ 不换行 ✓
   ═══════════════════════════════════════════════════════════════════ */
.ce-motion-row{display:flex;align-items:center;gap:10px;margin-top:10px;flex-wrap:wrap}
.ce-motion-label{font-weight:700;font-size:13px;white-space:nowrap}
/* ★ auto-fit + minmax ✗✗：窄屏（390px）放不下三个按钮时【自动换成 2+1 行】✓
   为什么不用 nowrap + text-overflow:ellipsis ✗✗（第一版就是那样 ✓ 实测踩到 ✓）：
     中文 / 日文 / 韩文 / 阿拉伯文【没有词间空格】✗ 省略号会切在字中间 ✗
     实测 390px 下中文那条会被裁成两行都显示不全 ✗ ⇒ 改成【换行】✓ 一个字都不丢 ✓✓ */
.ce-motion-seg{display:grid;grid-template-columns:repeat(auto-fit,minmax(96px,1fr));gap:6px;flex:1 1 210px}
.ce-motion-btn{
  border:2px solid var(--line,#e5e7eb);background:var(--card-soft,#f8fafc);
  color:inherit;border-radius:11px;padding:7px 6px;cursor:pointer;
  font:inherit;font-size:clamp(10.5px,2.9vw,12.5px);font-weight:700;line-height:1.25;
  text-align:center;white-space:normal;overflow-wrap:break-word;
}
.ce-motion-btn.on{background:linear-gradient(180deg,#5eead4,#14b8a6);border-color:#0d9488;color:#fff}
.ce-motion-btn:hover{border-color:#0d9488}
@media(max-width:420px){.ce-motion-row{gap:6px}.ce-motion-label{font-size:12px}}

/* ===== 年级 pill ===== */
.stage-row{display:flex;gap:8px;overflow-x:auto;margin-bottom:14px}
.stage-pill{flex:0 0 auto;padding:9px 16px;border-radius:14px;background:var(--card);border:2px solid var(--line);font-size:13px;font-weight:700;white-space:nowrap;cursor:pointer}
.stage-pill.on{background:var(--primary);color:#fff;border-color:var(--primary)}

/* ===== Aurora 深色主题适配 ===== */
:root[data-theme="aurora"] .calc-logo{background:rgba(255,255,255,.1)}
:root[data-theme="aurora"] .upload{border-color:rgba(255,255,255,.2)}
:root[data-theme="aurora"] input[type="file"]::file-selector-button{background:#115E8A}

/* ===== 动画 ===== */
@keyframes bounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
.bounce{animation:bounce 1.2s infinite}

@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
@keyframes ceStatusPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.035);box-shadow:0 0 0 6px rgba(239,93,100,.08)}}
.ce-credit-widget{position:relative;min-height:241px;margin:4px 0 12px;border-radius:23px;overflow:hidden;border:1px solid #e1e8f2;background:linear-gradient(180deg,#f8fbff,#edf5ff);box-shadow:inset 0 1px 0 rgba(255,255,255,.98)}
.ce-credit-widget.meter-change{animation:ceMeterBump .5s cubic-bezier(.2,.85,.25,1)}@keyframes ceMeterBump{0%{transform:scale(1)}30%{transform:scale(1.012)}100%{transform:scale(1)}}
.ce-meter-scene{position:absolute;inset:0 0 auto;height:157px;overflow:hidden}.ce-credit-widget.role-child .ce-adult-scene{display:none}.ce-credit-widget.role-adult .ce-child-scene{display:none}
.ce-child-scene{background:radial-gradient(circle at 56% 34%,rgba(255,255,255,.9),transparent 34%),linear-gradient(180deg,#eaf6ff,#f7fbff 67%,#eef9ef)}
.ce-sun{position:absolute;right:19px;top:17px;width:37px;height:37px;border-radius:50%;background:radial-gradient(circle at 34% 29%,#fff4bd 0 18%,#ffd45f 49%,#efb43d 100%);box-shadow:0 0 0 8px rgba(255,211,97,.10),0 0 20px rgba(255,200,66,.30);animation:ceSunBreath 3.5s ease-in-out infinite}@keyframes ceSunBreath{50%{transform:scale(1.07)}}
.ce-cloud{position:absolute;width:60px;height:15px;border-radius:999px;background:rgba(255,255,255,.84)}.ce-cloud:before,.ce-cloud:after{content:"";position:absolute;border-radius:50%;background:inherit}.ce-cloud:before{width:23px;height:23px;left:9px;top:-9px}.ce-cloud:after{width:28px;height:28px;right:8px;top:-13px}.cloud-a{left:23px;top:27px;transform:scale(.83);opacity:.76}.cloud-b{right:78px;top:55px;transform:scale(.62);opacity:.54}
.ce-tree-ground{position:absolute;left:12%;right:12%;bottom:10px;height:31px;border-radius:50%;background:radial-gradient(ellipse at center,rgba(84,165,94,.25),rgba(84,165,94,0) 72%)}
.ce-tree{position:absolute;left:50%;bottom:16px;width:104px;height:126px;transform:translateX(-50%);transform-origin:50% 100%;transition:transform .55s,filter .55s}.ce-trunk{position:absolute;left:44px;bottom:0;width:16px;height:70px;border-radius:10px 10px 7px 7px;background:linear-gradient(90deg,#895b35,#ad7844 52%,#80522e)}
.branch{position:absolute;display:block;height:8px;border-radius:999px;background:#8b5e36;transform-origin:left center}.branch-a{left:47px;bottom:55px;width:43px;transform:rotate(-35deg)}.branch-b{left:50px;bottom:55px;width:40px;transform:rotate(34deg)}
.leaf{position:absolute;display:block;width:31px;height:26px;border-radius:60% 50% 60% 50%;background:linear-gradient(145deg,#72d58b,#2ca965);box-shadow:inset 0 -5px 0 rgba(21,122,74,.08),0 5px 11px rgba(39,146,82,.10);transition:transform .55s,filter .55s,background .55s,opacity .55s}.leaf-a{left:12px;top:41px}.leaf-b{left:37px;top:26px}.leaf-c{left:62px;top:38px}.leaf-d{left:18px;top:18px}.leaf-e{left:50px;top:9px}.leaf-f{left:68px;top:60px}.leaf-g{left:2px;top:64px}.leaf-h{left:41px;top:59px}
.ce-flower{position:absolute;width:7px;height:7px;border-radius:50%;background:#ffafc3;box-shadow:7px -3px 0 #fff0a3,-5px 2px 0 #fff0a3}.flower-a{left:29%;bottom:10px}.flower-b{right:29%;bottom:10px;transform:scale(.78)}
.ce-watercan{position:absolute;right:17px;bottom:11px;width:52px;height:48px;background:none;border:0;cursor:pointer;transform-origin:50% 100%;animation:ceCanFloat 2.2s ease-in-out infinite;filter:drop-shadow(0 9px 9px rgba(45,93,113,.13));padding:0}.ce-watercan:focus-visible{outline:3px solid rgba(77,137,238,.28);outline-offset:4px}.ce-can-body{position:absolute;left:9px;bottom:7px;width:29px;height:24px;border-radius:9px 9px 13px 13px;background:linear-gradient(145deg,#74bfff,#4d89ee);border:2px solid rgba(255,255,255,.8);box-shadow:inset 0 3px 0 rgba(255,255,255,.28)}.can-handle{position:absolute;left:11px;top:4px;width:22px;height:17px;border:4px solid #4d89ee;border-bottom:0;border-radius:18px 18px 0 0}.can-spout{position:absolute;right:-1px;bottom:17px;width:22px;height:10px;border-radius:0 7px 7px 0;background:#5aa9f4;transform:rotate(-10deg)}.drop{position:absolute;width:5px;height:8px;border-radius:70%;background:#68c7ff;opacity:0}.d1{right:1px;bottom:7px;animation:ceDrop 1.7s ease-out .1s infinite}.d2{right:-7px;bottom:2px;animation:ceDrop 1.7s ease-out .45s infinite}.d3{right:-13px;bottom:5px;animation:ceDrop 1.7s ease-out .8s infinite}@keyframes ceDrop{0%{opacity:0;transform:translateY(-3px) scale(.75)}20%{opacity:1}100%{opacity:0;transform:translate(4px,12px) scale(.8)}}@keyframes ceCanFloat{0%,100%{transform:translateY(0) rotate(-2deg)}50%{transform:translateY(-4px) rotate(2deg)}}
.ce-credit-widget.tier-3 .ce-tree{transform:translateX(-50%) rotate(1deg)}.ce-credit-widget.tier-3 .leaf-a{transform:rotate(8deg) translateY(3px)}.ce-credit-widget.tier-3 .leaf-f{transform:rotate(14deg) translateY(4px)}.ce-credit-widget.tier-2 .ce-tree{transform:translateX(-50%) rotate(3deg);filter:saturate(.84)}.ce-credit-widget.tier-2 .leaf{background:linear-gradient(145deg,#d7ce63,#92ad4a)}.ce-credit-widget.tier-2 .leaf-a{transform:rotate(15deg) translateY(7px)}.ce-credit-widget.tier-2 .leaf-b{transform:rotate(-8deg) translateY(6px)}.ce-credit-widget.tier-2 .leaf-c{transform:rotate(14deg) translateY(8px)}.ce-credit-widget.tier-2 .leaf-g{opacity:.55;transform:translateY(12px) rotate(24deg)}.ce-credit-widget.tier-1 .ce-tree{transform:translateX(-50%) rotate(6deg);filter:saturate(.55)}.ce-credit-widget.tier-1 .leaf{background:linear-gradient(145deg,#d8c95a,#9da35a)}.ce-credit-widget.tier-1 .leaf-a,.ce-credit-widget.tier-1 .leaf-c,.ce-credit-widget.tier-1 .leaf-g,.ce-credit-widget.tier-1 .leaf-h{opacity:.20}.ce-credit-widget.tier-1 .leaf-b{transform:translateY(9px) rotate(16deg)}.ce-credit-widget.tier-1 .leaf-d{transform:translateY(14px) rotate(-17deg);opacity:.45}.ce-credit-widget.tier-1 .ce-watercan{animation:ceCanAlert 1s ease-in-out infinite}@keyframes ceCanAlert{0%,100%{transform:translateY(0) rotate(-4deg)}50%{transform:translateY(-7px) rotate(5deg) scale(1.04)}}
/* Adult Credit Vault */.ce-adult-scene{background:radial-gradient(circle at 50% 50%,rgba(255,220,112,.28),transparent 40%),linear-gradient(145deg,#fefbf3,#f5f7fb 72%)}.ce-vault-glow{position:absolute;left:50%;top:63px;width:122px;height:65px;transform:translateX(-50%);border-radius:50%;background:radial-gradient(ellipse at center,rgba(255,197,73,.22),transparent 68%)}.ce-vault{position:absolute;left:50%;bottom:17px;width:114px;height:92px;transform:translateX(-50%);border-radius:17px 17px 20px 20px;background:linear-gradient(160deg,#f5c85d,#d9982c 64%,#a7651f);box-shadow:inset 0 3px 0 rgba(255,255,255,.52),inset 0 -7px 0 rgba(110,67,21,.18),0 14px 28px rgba(144,95,24,.18);transition:transform .55s,filter .55s}.ce-vault-top{position:absolute;left:0;right:0;top:-9px;height:17px;border-radius:14px 14px 8px 8px;background:linear-gradient(180deg,#f8d878,#d9952c);box-shadow:inset 0 2px 0 rgba(255,255,255,.5)}.ce-vault-door{position:absolute;left:31px;top:22px;width:52px;height:52px;border-radius:50%;background:radial-gradient(circle at 34% 27%,#fef7da,#d59d37 64%,#a86a20 100%);border:3px solid rgba(255,238,174,.8);box-shadow:inset 0 3px 0 rgba(255,255,255,.45),0 6px 10px rgba(127,78,22,.14)}.ce-lock{position:absolute;left:21px;top:17px;width:11px;height:16px;border-radius:4px;background:#8a5b1f}.ce-lock:before{content:"";position:absolute;left:1px;top:-8px;width:8px;height:11px;border:3px solid #8a5b1f;border-bottom:0;border-radius:8px 8px 0 0}.coin{position:absolute;width:30px;height:12px;border-radius:50%;background:linear-gradient(180deg,#ffe88b,#e0a735);border:2px solid rgba(255,248,198,.75);box-shadow:0 6px 8px rgba(150,101,33,.12);transition:.55s}.coin:after{content:"";position:absolute;left:7px;right:7px;top:3px;height:3px;border-radius:99px;background:rgba(174,112,23,.28)}.c1{left:-8px;bottom:8px;transform:rotate(-5deg)}.c2{left:5px;bottom:12px;transform:rotate(2deg)}.c3{left:71px;bottom:8px;transform:rotate(4deg)}.c4{left:81px;bottom:23px;transform:rotate(7deg)}.c5{left:-12px;bottom:28px;transform:rotate(-6deg)}.c6{left:10px;bottom:34px;transform:rotate(2deg)}.c7{left:69px;bottom:35px;transform:rotate(-4deg)}.c8{left:47px;bottom:49px;transform:rotate(1deg)}.ce-credit-widget.tier-3 .c5,.ce-credit-widget.tier-3 .c6,.ce-credit-widget.tier-3 .c8{opacity:.28;transform:translateY(8px)}.ce-credit-widget.tier-2 .c1,.ce-credit-widget.tier-2 .c4,.ce-credit-widget.tier-2 .c6,.ce-credit-widget.tier-2 .c7,.ce-credit-widget.tier-2 .c8{opacity:.12;transform:translateY(16px)}.ce-credit-widget.tier-1 .coin{opacity:.05;transform:translateY(19px) scale(.92)}.ce-credit-widget.tier-1 .c2{opacity:.55;transform:translateY(10px)}.ce-credit-widget.tier-1 .ce-vault{filter:saturate(.72)}.ce-card-chip{position:absolute;right:25px;bottom:21px;width:26px;height:17px;border-radius:5px;background:linear-gradient(135deg,#ffdf7d,#f2af45);opacity:0;transform:translateX(10px) rotate(-6deg);transition:.55s}.ce-credit-widget.tier-2 .ce-card-chip{opacity:.65;transform:translateX(0) rotate(-6deg)}.ce-credit-widget.tier-1 .ce-card-chip{opacity:1;transform:translateX(0) rotate(-6deg)}.ce-card-spark{position:absolute;right:18px;top:24px;width:7px;height:7px;border-radius:50%;background:#ffe9a8;opacity:0;box-shadow:10px 5px 0 #fff4cb,3px 12px 0 #ffe9a8}.ce-credit-widget.tier-4 .ce-card-spark{opacity:.9;animation:ceSpark 2.2s ease-in-out infinite}.ce-credit-widget.tier-1 .ce-card-spark{opacity:1;animation:ceCriticalSpark 1.05s ease-in-out infinite}@keyframes ceSpark{50%{opacity:1;transform:scale(1.2)}}@keyframes ceCriticalSpark{0%,100%{opacity:.18;transform:scale(.85)}50%{opacity:1;transform:scale(1.25)}}
/* Readout / tier rail */.ce-credit-readout{position:absolute;left:18px;right:18px;bottom:12px;display:flex;flex-wrap:wrap;align-items:baseline;gap:3px 7px;padding:10px 13px;border-radius:15px;background:rgba(255,255,255,.9);border:1px solid rgba(215,226,239,.86);backdrop-filter:blur(10px);box-shadow:0 9px 19px rgba(51,76,117,.07)}.ce-credit-number{font-size:32px;line-height:1;font-weight:var(--ce-fw-head);color:#233a62;letter-spacing:-.03em}.ce-credit-unit{font-size:13px;line-height:1;font-weight:var(--ce-fw-head);color:#64748a}.ce-credit-readout .ce-status-pill{margin-left:auto;align-self:center;padding:6px 11px;font-size:13px}.ce-credit-subline{flex:0 0 100%;font-size:12px;line-height:1.3;color:var(--ink-2,#7c8a9f)}.ce-topup-btn{flex:0 0 auto;margin-top:2px;min-height:34px;padding:8px 14px;border-radius:999px;border:1px solid rgba(11,150,140,.32);background:linear-gradient(180deg,var(--sel-a,#21cbb8),var(--sel-b,#0ea79c));color:#fff;font-size:11px;font-weight:800/* ★★ 批 B：原来是写死的 #21cbb8→#0ea79c ✗ 白字只有 2.23:1（三浅主题实测）✗ 改成与选中态同一对深色 token
     （--sel-a/--sel-b ✗ 它们与这里的要求完全一致：【白字 ≥4.5】⇒ 不再新造一对重复 token ✓）
     实测（边缘带 ✗ 两段端点都算）：elf 4.90~6.00 ✗ cloud 6.32~7.51 ✗ story 5.34~6.53 ✗ aurora 见它自己的覆盖 ✓ 全部 ≥4.5 ✓
     ★ 字号/字重一律不动 ✗ 不靠加粗替对比度 ✓ */;font-family:inherit;letter-spacing:.01em;box-shadow:0 6px 14px rgba(14,150,140,.28),inset 0 1px 0 rgba(255,255,255,.28);cursor:pointer;transition:transform .18s,box-shadow .18s;white-space:nowrap}.ce-topup-btn:hover{transform:translateY(-1px);box-shadow:0 9px 18px rgba(14,150,140,.36),inset 0 1px 0 rgba(255,255,255,.28)}.ce-topup-btn:active{transform:translateY(1px)}.ce-tier-rail{display:grid;grid-template-columns:repeat(4,1fr);gap:7px;margin:2px 0 4px}.ce-tier-dot{height:6px;border-radius:999px;background:#e2e8f0;transition:.35s}.ce-tier-dot.tier-1{background:#ffd6d7}.ce-tier-dot.tier-2{background:#ffe4b8}.ce-tier-dot.tier-3{background:#e8ecbe}.ce-tier-dot.tier-4{background:#c7ecd4}.ce-tier-dot.active{transform:scaleY(1.55);box-shadow:0 0 0 2px rgba(75,120,238,.10)}.ce-tier-caption{display:flex;justify-content:space-between;color:var(--ink-2,#99a6b6);font-size:11px;font-weight:var(--ce-fw-head);padding-bottom:8px}.ce-mini-stats .stat{border-radius:13px!important;background:#f7f9fc;border:1px solid #e7edf5;padding:9px 6px}.ce-mini-stats .stat b{font-size:19px}.ce-mini-stats .stat span{font-size:11px}.ce-credit-notice{display:none;align-items:flex-start;gap:8px;margin-top:10px;padding:10px 11px;border-radius:14px;font-size:12px;line-height:1.5;border:1px solid #e7e9ed;background:#f9fafc;color:#6a7588}.ce-credit-notice.show{display:flex}.ce-credit-notice.tier-2{background:#fff8eb;border-color:#f8dfaf;color:#8f641e}.ce-credit-notice.tier-1{background:#fff2f2;border-color:#f6cacc;color:#a0373e}.ce-notice-icon{width:23px;height:23px;flex:0 0 23px;border-radius:8px;display:grid;place-items:center;background:#fff;box-shadow:0 3px 9px rgba(70,50,20,.06)}.ce-credit-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}.ce-credit-actions .btn{flex:1;min-width:88px}
/* Membership */.ce-membership-card{background:radial-gradient(circle at 92% 18%,rgba(122,100,224,.09),transparent 28%),linear-gradient(180deg,#fff,#f7f9fe)}.ce-member-plan{padding:7px 11px;border-radius:999px;background:var(--soft,#eef6ff);color:var(--primary);font-weight:var(--ce-fw-head);font-size:12px}.ce-member-hero{display:flex;align-items:center;gap:12px;padding:12px 11px;border:1px solid #e5ebf4;border-radius:17px;background:rgba(249,251,255,.88)}.ce-member-avatar{width:56px;height:56px;flex:0 0 56px;border-radius:19px;display:grid;place-items:center;background:linear-gradient(145deg,#ecfce0,#f8ffec);border:1px solid #d8f0c6;box-shadow:0 8px 19px rgba(63,128,58,.10);transition:.4s}.ce-member-avatar svg{width:34px;height:34px;display:block;filter:drop-shadow(0 3px 3px rgba(46,94,42,.18))}.ce-member-avatar.life-2{background:linear-gradient(145deg,#e0f8ee,#f1fcf7);border-color:#c8eedd}.ce-member-avatar.life-3{background:linear-gradient(145deg,#cff0df,#eaf9f2);border-color:#aedec7}.ce-member-avatar.life-4{background:linear-gradient(145deg,#e7f0ff,#f4f8ff);border-color:#cfdfff}.ce-member-avatar.life-5{background:linear-gradient(145deg,#f0e9ff,#f9f5ff);border-color:#ddcffb}.ce-member-avatar.life-6{background:linear-gradient(145deg,#fdf0d9,#fff8e9);border-color:#f3dcae}.ce-member-avatar.life-7{background:linear-gradient(145deg,#f1e8ff,#eafff3);border-color:#d9c8fb;box-shadow:0 0 0 5px rgba(155,107,255,.10),0 10px 24px rgba(120,96,200,.16)}.ce-member-role{color:#8c9aae;font-size:11px;font-weight:var(--ce-fw-head)}.ce-member-level{font-size:20px;line-height:1.15;font-weight:var(--ce-fw-head);color:#2a3b57;font-family:var(--ce-font-display);letter-spacing:-.01em}.ce-member-copy .muted{font-size:12px;line-height:1.45;margin-top:3px}.ce-level-dual{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:9px}.ce-level-box{padding:11px;border-radius:15px;background:#f8fafc;border:1px solid #e5ebf3}.ce-level-label{color:#8895a7;font-size:11px;font-weight:var(--ce-fw-head);text-transform:uppercase;letter-spacing:.07em}.ce-level-main{display:flex;align-items:baseline;gap:7px;margin:5px 0 7px}.ce-level-main strong{font-size:26px;color:var(--primary)}.ce-level-main span{font-size:12px;font-weight:var(--ce-fw-head);color:#52647e;font-family:var(--ce-font-display)}.ce-progress{height:7px;border-radius:999px;background:#e9eef5;overflow:hidden}.ce-progress i{display:block;height:100%;width:0;border-radius:999px;background:linear-gradient(90deg,var(--primary),var(--accent));transition:width .7s cubic-bezier(.2,.8,.2,1)}.ce-progress.lifetime i{background:linear-gradient(90deg,var(--primary2),var(--primary))}.ce-level-foot{margin-top:6px;color:#96a2b2;font-size:11px}.ce-membership-footer{display:flex;justify-content:space-between;align-items:center;gap:9px;margin-top:10px}.ce-membership-footer>span{font-size:11px;line-height:1.5;color:#8996aa}.ce-membership-footer .btn{min-height:38px;padding:9px 12px;font-size:11px;white-space:nowrap}
/* Unified auth / plans / settings polish */.modal{background:radial-gradient(circle at 30% 20%,rgba(104,147,243,.08),transparent 26%),rgba(24,35,55,.50)!important;backdrop-filter:blur(5px)}.modalbox{border-radius:26px!important;border:1px solid rgba(223,232,243,.94);background:radial-gradient(circle at 92% 7%,rgba(125,166,255,.08),transparent 23%),linear-gradient(180deg,#fff,#f8fbff);box-shadow:0 30px 90px rgba(21,37,64,.27)!important}.modalbox .close{width:35px;height:35px;border-radius:50%;background:#f3f6fa;color:#687890;display:grid;place-items:center;transition:.2s}.modalbox .close:hover{background:#edf2f8;color:#30435f;transform:rotate(6deg)}/* ★★ 修 bug ①（弹窗输入框聚焦边框不变色 ✗）：原 `.modalbox input,.modalbox select{…border-color:#dde6f2}` 与 `input:focus{border-color:var(--primary)}` 特异性同为 (0,1,1) ✗ 但更靠后 ⇒ 聚焦色永远输 ✗✗ 修法 = 【收窄基础规则】✓ 只把 border-color 一条收进 :not(:focus) ✓（聚焦时这条不匹配 ⇒ 不再参与竞争 ✓）★ 不加 !important 对轰 ✗（本仓库有通配 !important 误伤教训 ui-type-scale.css:124 ✗ components.css:1085-1086 明文约定不许 ✗）★ 不用"调顺序"✗（本行与 themes.css:169 是逐字节副本 ✗ 靠顺序＝跨文件脆弱 ✓）★ 不收窄整条规则 ✗（min-height:46px!important 会随聚焦失效 ⇒ 输入框高度跳动 ✓）★ 完整理由与实测见 docs/UI-SPEC-REAL.md §5.2 ✓ ★ 行数保持不变 ⇒ 不动其它行号 ✓ */ .modalbox input,.modalbox select{border-radius:14px!important;min-height:46px!important;background:#fff}.modalbox input:not(:focus),.modalbox select:not(:focus){border-color:#dde6f2}.modalbox input:focus,.modalbox select:focus{box-shadow:0 0 0 4px rgba(82,121,238,.11),0 9px 22px rgba(48,72,112,.05)!important}.plan{border-radius:18px!important;background:linear-gradient(180deg,#fff,#f8fbff);transition:.25s}.plan:hover{transform:translateY(-3px);box-shadow:0 15px 28px rgba(45,67,104,.10)}
body[data-stage="primary_1_3"] .ce-credit-card,body[data-stage="primary_1_3"] .ce-membership-card{border-color:#efdaba!important;background:linear-gradient(180deg,var(--surface-warm,#fffdf8),var(--surface-warm-2,#fff8ed))}body[data-stage="primary_1_3"] .ce-title-line b,body[data-stage="primary_1_3"] .ce-credit-number,body[data-stage="primary_1_3"] .ce-member-level{font-family:var(--ce-font-display)}body[data-stage="primary_1_3"] .btn{border-radius:999px!important;font-family:var(--ce-font-body);font-weight:600}body[data-stage="primary_1_3"] .ce-member-avatar{border-radius:20px}body[data-stage="grade_9_12"] .ce-credit-widget{min-height:228px;border-radius:15px}body[data-stage="grade_9_12"] .ce-membership-card{border-radius:15px!important}
@media(max-width:700px){.ce-level-dual{grid-template-columns:1fr}.ce-membership-footer{flex-direction:column;align-items:flex-start}.ce-membership-footer .btn{width:100%}.ce-credit-widget{min-height:252px}}
/* ★ 第 13 轮：把新加的树干摆动也纳入 ✓（无障碍要求 ✓ 用户在系统里关掉动效就不该再摇 ✗）*/
@media(prefers-reduced-motion:reduce){.ce-status-pill,.ce-watercan,.ce-sun,.ce-credit-widget,.ce-tree,.ce-vault,.coin,.leaf{animation:none!important}}

/* ===== v5.1 多主题弹窗/深色主题修正 ===== */
/* 所有主题下弹窗统一为浅色纸面，强制深色文字，避免深色主题白字白底 */
.modalbox{color:#17253b}
.modalbox h1,.modalbox h2,.modalbox h3,.modalbox b,.modalbox label{color:#1b2a44}
.modalbox .muted{color:#64748b}
.modalbox .plan b{color:#1b2a44}
/* Aurora 深色主题：v4.2 硬编码浅色组件适配 */
:root[data-theme=aurora] .feedback-card{background:var(--card);border-color:var(--line)}
:root[data-theme=aurora] .ce-membership-card{background:linear-gradient(180deg,#1b2442,#161d36)!important;border-color:rgba(255,255,255,.10)!important}
:root[data-theme=aurora] .ce-member-hero{background:rgba(255,255,255,.05)!important;border-color:rgba(255,255,255,.10)!important}
:root[data-theme=aurora] .ce-member-level{color:#e9edff}
:root[data-theme=aurora] .ce-member-role{color:#9aa6c8}
:root[data-theme=aurora] .ce-member-copy .muted{color:#aab6d8}
:root[data-theme=aurora] .ce-level-box{background:rgba(255,255,255,.05)!important;border-color:rgba(255,255,255,.10)!important}
:root[data-theme=aurora] .ce-level-label,:root[data-theme=aurora] .ce-level-foot{color:#9aa6c8}
:root[data-theme=aurora] .ce-level-main{color:#e9edff}
:root[data-theme=aurora] .ce-credit-widget{background:linear-gradient(180deg,#1b2442,#161d36);border-color:rgba(255,255,255,.10)}
:root[data-theme=aurora] .ce-credit-readout{background:rgba(16,22,44,.82);border-color:rgba(255,255,255,.12)}
:root[data-theme=aurora] .ce-credit-number{color:#f2f5ff}
:root[data-theme=aurora] .ce-credit-unit,:root[data-theme=aurora] .ce-credit-subline{color:#aeb8d8}
:root[data-theme=aurora] .ce-tier-caption{color:#9aa6c8}
:root[data-theme=aurora] .ce-topup-btn{background:rgba(255,255,255,.10);color:#dbe3ff;border-color:rgba(255,255,255,.22);box-shadow:none}
:root[data-theme=aurora] .ce-eyebrow,:root[data-theme=aurora] .ce-card-heading{color:#cfd7f5}
:root[data-theme=aurora] .ce-title-line{background:rgba(255,255,255,.14)}
:root[data-theme=aurora] .ce-lock{color:#9aa6c8}

/* ★★ 第 35 轮：这条兜底【必须收窄】✗✗ —— 它是充值包 Buy 按钮"素"的真因 ✓
   原样：.modalbox .btn.secondary{background:#eef2f7!important;color:#30435b!important}
     ⇒ 本意是"弹窗里的次按钮统一压淡"✓
     ⇒ 但充值包的 Buy 按钮 class 也是 "btn secondary" ✗ 于是被一起压成浅灰 ✗✗
   修法：排除充值包（data-product^="pack"）✗ 其余次按钮【观感完全不变】✓
   ★ 这样新按钮就不必用 !important 去对撞 ✓（详见文件末尾第 35 轮那一段 ✓） */
.modalbox .btn.secondary:not([data-product^="pack"]){background:#eef2f7!important;color:#30435b!important}
.modalbox .btn.gold{background:#fff3d1!important;color:#7d5c00!important}
:root[data-theme=aurora] .ce-mini-stats .stat{background:rgba(255,255,255,.05)!important;border:1px solid rgba(255,255,255,.10)!important}
:root[data-theme=aurora] .ce-mini-stats .stat b{color:#e9edff}
:root[data-theme=aurora] .ce-mini-stats .stat span{color:#9aa6c8}
:root[data-theme=aurora] .ce-status-pill{background:rgba(255,255,255,.10);color:#9fe8c4;border-color:rgba(255,255,255,.14)}

/* Custom aligned checkboxes (fix checkbox/text baseline mismatch) */
/* ★★★ 第 35 轮：这里加 position:relative ✗✗ —— 这是用户报了【五次】的那个 bug 的真元凶 ✓
   事故 ✗：下面第 337 行是
     input[type=checkbox].kcbx:checked::after{content:'✓';position:absolute;inset:0;…}
   而本行【没有 position:relative】✗✗
   ⇒ 伪元素找不到定位祖先 ✓ 就往上找到整页层 ✗
   ⇒ ★ 一勾选 ✓ 一个 13px 的 ✓ 就变成【盖住整页的透明层】✗✗✗
   ⇒ 用户勾了【下面】那个 → 整页被盖住 → 【上面】那个永远点不到 ✓✓✓
      = 用户描述的症状一字不差："下面那个可以选中，上面那个选不中，
        点了以后没有任何反应"✓✓
   ★ 子代理的真鼠标对照实验（决定性证据 ✓）：
       未勾选时，点 #wantAnim → 生效 ✓（elementFromPoint = input#wantAnim ✓）
       已勾选后，点同一点   → 【不生效】✗（elementFromPoint = input#agree.kcbx ✗）
   ★ 修法就是这一行 ✗ 让伪元素以自己 13×13 的盒子为定位基准 ✓✓
   ★ 注意：这可能不是唯一原因 ✗
     本轮之前我还修过一个独立的原因（.modalbox 的 overflow:hidden 导致 focus 触发内部滚动 ✓）
     两个都是真的 ✗ 两个都必须修 ✓ */
input[type=checkbox].kcbx{position:relative;width:13px;height:13px;flex:0 0 auto;border:1.5px solid #94a3b8;border-radius:4px;background:#fff;accent-color:#10B8C4;cursor:pointer;margin:0}
input[type=checkbox].kcbx:hover{border-color:var(--primary)}
input[type=checkbox].kcbx:checked{background:var(--primary);border-color:var(--primary)}
input[type=checkbox].kcbx:checked::after{content:'\2713';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px;font-weight:var(--ce-fw-head);line-height:1}
input[type=checkbox].kcbx:focus-visible{outline:2px solid rgba(20,184,166,.4)}
.explorer-word{color:#FF9F1C;font-weight:var(--ce-fw-head);letter-spacing:.2px}

/* ===== 思考中：小狐 + 问号 + 呼吸进度条（纯CSS，仅transform/opacity） ===== */
.thinker{position:relative;width:120px;margin:0 auto 4px;text-align:center}
.thinker-img{width:72px;height:72px;object-fit:contain;display:block;margin:18px auto 0;animation:thinkerBob 1.6s ease-in-out infinite}
@keyframes thinkerBob{0%,100%{transform:translateY(0)}50%{transform:translateY(-5px)}}
.qrow{display:flex;justify-content:center;gap:26px;height:24px;margin-bottom:2px}.qm{display:inline-block;font-family:var(--display);font-weight:var(--ce-fw-head);font-size:22px;line-height:1;opacity:0;animation:qmPop 1.5s ease-in-out infinite}
.q1{color:#ff5d5d;animation-delay:0s}
.q2{color:#4d89ee;animation-delay:.5s}
.q3{color:#ffb020;animation-delay:1s}
@keyframes qmPop{0%{opacity:0;transform:translateY(6px) scale(.6) rotate(-12deg)}35%{opacity:1;transform:translateY(-4px) scale(1.05) rotate(6deg)}70%{opacity:.85;transform:translateY(-8px) scale(1) rotate(-4deg)}100%{opacity:0;transform:translateY(-14px) scale(.8) rotate(0)}}
.think-bars{width:min(260px,70%);margin:0 auto 10px;display:flex;flex-direction:column;gap:9px}
.tb{height:9px;border-radius:99px;background:var(--line);transform-origin:left center;animation:tbBreathe 1.3s ease-in-out infinite}
.b1{width:62%;animation-duration:1.1s}
.b2{width:88%;animation-duration:1.6s;animation-delay:.25s}
.b3{width:45%;animation-duration:.95s;animation-delay:.5s}
@keyframes tbBreathe{0%,100%{transform:scaleX(.55);opacity:.55}50%{transform:scaleX(1);opacity:1}}

/* ★★ 实测记录（第 56 轮 ✗ 别再把 var(--display) 换成 var(--calf-round-font) ✓）
   ── 曾以为：--display = ui-rounded,'Fredoka',… ✗ ui-rounded 排第一 ⇒ 'Fredoka' 只是侥幸 ✓
      换成 --calf-round-font 就能让 'Fredoka' 排第一 ✓
   ── 实测结论：两者解析成【完全同一串】✗✗
      --calf-round-font（v565.css:16）→ --ce-font-display（design-tokens.css:276）
      → --font-display（themes.css:31）⇒ 与 --display 逐项相同 ✓
      所以那次替换是【no-op】✗ 已回退 ✓
   ── 关于 ui-rounded：canvas 像素指纹实测 = 4acd66af / ink 5290 ✓
      与 serif（K）和 'FAKE + serif'（F）逐字节相同 ⇒ 它在本机 = last-resort(serif) ✓
      ★ 但只要它【排在 'Fredoka' 前面】就无害 ✓：
        列表末尾的 generic(sans-serif/system-ui) 会被正确跳到下一项 ✓
        实测 ui-rounded,'Fredoka',…,system-ui,sans-serif 的指纹 52282ca3 = 'Fredoka' 单独 ✓
      ⇒ 现状就是对的 ✗ 不改才是对的 ✓
   ── 本条 margin-top:14px 是【永不到达的死值】✗ 被 v565.css:35 的 20px 盖掉 ✓
      间距归那一条管（用户要的 ×3 也改在那里 ✓）本行只管颜色 / 字重 / 字体 token ✓
   ── font-weight:var(--ce-fw-head) = 700 ✓ 正好命中自托管 Fredoka 的 700 字重 ✓ */
#loadingText{margin-top:14px;color:#FF9F1C;font-weight:var(--ce-fw-head);font-family:var(--display)}
.lddots{display:block;height:20px;font-size:26px;line-height:1;color:#FF9F1C;font-weight:var(--ce-fw-head);letter-spacing:2px;margin-top:-6px}
.lddots i{font-style:normal;opacity:0;animation:ldDot 1.2s infinite}
.lddots i:nth-child(1){animation-delay:0s}
.lddots i:nth-child(2){animation-delay:.4s}
.lddots i:nth-child(3){animation-delay:.8s}
@keyframes ldDot{0%{opacity:0}40%{opacity:1}100%{opacity:0}}

/* ===== v5.5.7 动画自适应 + 全屏 ===== */
.anim{width:100%;margin-top:6px}
iframe.calfAnimFrame{width:100%;min-height:620px;border:0;border-radius:16px;background:#fff;display:block}
#animFsOverlay{display:none;position:fixed;inset:0;z-index:9999;background:rgba(15,23,42,.55);backdrop-filter:blur(3px);padding:22px}
#animFsOverlay.open{display:block}
.animFsBox{width:min(1000px,96vw);height:min(86vh,900px);margin:0 auto;background:#fff;border-radius:20px;overflow:hidden;box-shadow:0 30px 80px rgba(0,0,0,.4)}
.animFsBox iframe{width:100%;height:100%;border:0;background:#fff}
/* send 与练习等级间距 */
#chat .actions{margin-top:16px}
#practiceTitle,#practicePill{margin-bottom:2px}
#practiceDesc{margin:8px 0 16px}
#practiceArea .actions,#practiceArea + .actions{margin-top:18px}
.card #practice1,.card #practice2,.card #practice3{margin-top:10px;margin-bottom:4px}
/* 练习结果 */
.grade-card{border-radius:16px;padding:16px;margin-top:12px;font-size:16px;line-height:1.8}
.grade-card.ok{background:linear-gradient(90deg,#ecfdf5,#f0fdfa);border:2px solid #a7f3d0}
.grade-card.bad{background:linear-gradient(90deg,#fff7ed,#fffbeb);border:2px solid #fed7aa}
.grade-head{display:flex;align-items:center;gap:10px;font-weight:var(--ce-fw-head);font-size:18px;margin-bottom:8px}
.grade-card.ok .grade-head{color:#047857}
.grade-card.bad .grade-head{color:#b45309}
.grade-answer{background:#eff6ff;border:1px solid #bfdbfe;color:#1e40af;border-radius:12px;padding:10px 14px;margin:8px 0;font-weight:700}
.grade-steps{margin-top:8px}.grade-steps li{margin:5px 0}
.grade-stars{font-size:26px;letter-spacing:4px;margin-top:8px}
/* 练习等级递进配色 */
.btn.lvl-done{background:linear-gradient(180deg,#86efac,#22c55e)!important;color:#fff!important;border-color:#16a34a!important}
.btn.lvl-open{background:linear-gradient(180deg,#fde68a,#f59e0b)!important;color:#fff!important;border-color:#d97706!important}
.btn.lvl-lock{opacity:.55;filter:grayscale(.4)}

/* ===== v5.5.8 魔法宝石密码强度 ===== */
.pw-gems{display:flex;align-items:center;gap:7px;margin:8px 0 2px}
.pw-gems .gem{width:18px;height:18px;border-radius:50% 50% 50% 8px;transform:rotate(45deg);background:#e5e7eb;transition:all .25s;display:inline-block}
.pw-gems .gem.ok{background:linear-gradient(135deg,#a78bfa,#22d3ee);box-shadow:0 0 8px rgba(124,58,237,.45)}
.pw-gems .gem:nth-child(2).ok{background:linear-gradient(135deg,#38bdf8,#22d3ee);box-shadow:0 0 8px rgba(56,189,248,.45)}
.pw-gems .gem:nth-child(3).ok{background:linear-gradient(135deg,#34d399,#a3e635);box-shadow:0 0 8px rgba(52,211,153,.45)}
.pw-gems .gem:nth-child(4).ok{background:linear-gradient(135deg,#fbbf24,#fb7185);box-shadow:0 0 10px rgba(251,191,36,.55)}
.pw-gemlabel{font-size:11px;font-weight:var(--ce-fw-head);color:#a78bfa;margin-left:4px}
.pw-rules{margin-top:6px}
.pw-rules summary{font-size:11px;color:var(--muted,#94a3b8);cursor:pointer;font-weight:700;list-style:none}
.pw-rules summary::-webkit-details-marker{display:none}
.pw-rules[open] summary{margin-bottom:4px}
/* 认证弹窗糖果化：家长区柔和分隔 */
#signupRole{background:var(--card-soft,#f8fafc);border:2px dashed var(--line,#e5e7eb);border-radius:12px;padding:9px;font-weight:700}

/* v5.7.2 结构化步骤打字机 */
.tw-sol{display:none}
.tw-sol.tw-live{display:block;animation:twSolIn .35s ease both}
@keyframes twSolIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.step.tw-pending{opacity:0}
.step.tw-on{animation:twStepIn .3s ease both}
@keyframes twStepIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
.tw-desc{margin:0;line-height:1.85}
.tw-formula:empty{display:none}
.tw-caret::after{content:'▍';margin-inline-start:1px;color:var(--primary);font-weight:var(--ce-fw-head);animation:twBlink .8s steps(1) infinite}
@keyframes twBlink{50%{opacity:0}}

/* v6.0：kard 渲染器帧 + 动画 5 星评分 */
.kard-frame{display:block;width:100%;border:none;border-radius:16px;background:#0d1228}
.anim-rating{text-align:center;margin-top:14px;padding:10px}
.anim-rating .rating-label{font-size:14px;color:#64748b;margin-bottom:6px}
.anim-rating .rating-stars{font-size:0}
.anim-rating .star{font-size:28px;cursor:pointer;margin:0 3px;color:#cbd5e1;transition:color .15s;user-select:none;display:inline-block}
.anim-rating .star.active,.anim-rating .star:hover{color:#f59e0b}
.anim-rating .rating-thanks{font-size:14px;color:#16a34a;font-weight:700;margin-top:6px}
.anim-rating .rating-change{font-size:12px;color:#3b82f6;cursor:pointer;margin-left:8px;text-decoration:underline}
[dir="rtl"] .anim-rating{direction:rtl}

/* ==========================================================================
   v9.11「针对每次输出的快速点评」—— 接在 5 星评分【下面】的那一行
   （★ 用户点名位置：「在我那个动画下面打分的那个下面」）

   ★ 三条设计取舍（都有实测背书，见 app.js 的 showResultFeedbackRow）：
     ① ★ 零摩擦：两个按钮，点一下【当场提交】✗ 不弹窗、不打断、不用确认按钮
     ② ★ 不点就什么也不发生 ⇒ 这一行只是【多出来的一条】✗ 不占位、不挡任何东西
        · 用 flex + gap + 可换行 ⇒ 窄屏自动折行，不撑破动画卡
     ③ ★ 无障碍：
        · 控件全是真 <button> / <label for> ⇒ 键盘 Tab 可达、Enter/Space 可触发
        · emoji 👍👎 只作点缀（aria-hidden）✗ 语义由文字标签承担
        · 焦点环用 :focus-visible ⇒ 鼠标点击不闪框、键盘操作一定看得见
        · 对比度：#334155 on #f8fafc ≈ 10.4:1 ✓ 远超 4.5:1
   ========================================================================== */
.rf-fb{margin-top:4px;padding:10px 10px 12px;border-top:1px dashed #dbe3ee;text-align:center}
.rf-fb-row{display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap}
.rf-fb-prompt{font-size:13.5px;color:#475569;line-height:1.5;margin-inline-end:2px}
.rf-fb-btn{display:inline-flex;align-items:center;gap:6px;min-height:36px;padding:6px 14px;
  border:1.5px solid #cbd5e1;border-radius:999px;background:#fff;color:#334155;
  font:inherit;font-size:13.5px;font-weight:700;line-height:1.2;cursor:pointer;transition:border-color .15s,background .15s}
.rf-fb-btn .rf-fb-ico{font-size:15px;line-height:1}
.rf-fb-txt{white-space:nowrap}
.rf-fb-btn:hover{border-color:#94a3b8;background:#f1f5f9}
.rf-fb-up:hover{border-color:#16a34a;background:#f0fdf4;color:#15803d}
.rf-fb-down:hover{border-color:#dc2626;background:#fef2f2;color:#b91c1c}
.rf-fb-btn:focus-visible,.rf-fb-chip:focus-visible,.rf-fb-note:focus-visible,.rf-fb-save:focus-visible{
  outline:3px solid #1d4ed8;outline-offset:2px}
.rf-fb-thanks{font-size:13.5px;font-weight:700;color:#16a34a;line-height:1.5}
/* 展开区：只在用户点了「没帮助」之后出现 ⇒ 宽度受限、自动折行 */
.rf-fb-more{margin:10px auto 0;max-width:520px;text-align:start}
.rf-fb-more-title{font-size:13px;color:#475569;margin-bottom:6px}
.rf-fb-chips{display:flex;flex-wrap:wrap;gap:8px}
.rf-fb-chip{min-height:34px;padding:5px 12px;border:1.5px solid #cbd5e1;border-radius:999px;background:#fff;
  color:#334155;font:inherit;font-size:13px;font-weight:600;cursor:pointer;transition:border-color .15s,background .15s,color .15s}
.rf-fb-chip:hover{border-color:#94a3b8;background:#f1f5f9}
.rf-fb-chip.on{border-color:#1d4ed8;background:#eff6ff;color:#1d4ed8}
.rf-fb-chip.on::before{content:'✓ ';font-weight:900}
.rf-fb-notelab{display:block;margin:10px 0 4px;font-size:12.5px;color:#64748b}
.rf-fb-note{width:100%;box-sizing:border-box;min-height:40px;padding:8px 12px;border:1.5px solid #cbd5e1;
  border-radius:12px;background:#fff;color:#0f172a;font:inherit;font-size:14px}
.rf-fb-note::placeholder{color:#5b6b80}
.rf-fb-actions{display:flex;justify-content:flex-end;margin-top:8px}
.rf-fb-save{min-height:38px;padding:8px 16px;font-size:13.5px}
[dir="rtl"] .rf-fb,[dir="rtl"] .rf-fb-more{direction:rtl}
[dir="rtl"] .rf-fb-actions{justify-content:flex-start}
@media(max-width:420px){
  .rf-fb-row{gap:6px}
  .rf-fb-prompt{flex:1 1 100%}
  .rf-fb-btn{flex:1 1 42%}
}
@media(prefers-reduced-motion:reduce){.rf-fb-btn,.rf-fb-chip{transition:none}}

/* ==========================================================================
   v8.11 套餐弹窗：加大 + 每套餐功能对比清单 + 月付/年付切换
   注意：本段必须写到**已被页面加载的样式表**里。
   index.html 中并没有 <style> 块，早前把样式插进 <style> 的尝试被静默丢弃，
   导致弹窗结构在、样式全无（弹窗仍是 560px、清单出现默认圆点、数字被外面
   把 <b> 渲染成圆形徽章的规则影响而叠在文字上）。
   ========================================================================== */
.modalbox.plans-box{width:min(1040px,94vw);max-width:1040px}
.plans.plans-3{grid-template-columns:repeat(3,1fr);gap:16px}
.plans.plans-3 .plan{padding:20px 18px;display:flex;flex-direction:column}
.plans.plans-3 .plan .price{font-size:30px;margin:6px 0 2px}
.billing-row{display:flex;align-items:center;justify-content:center;gap:10px;margin:6px 0 16px;flex-wrap:wrap}
.bill-btn{border:2px solid var(--line,#e2e8f0);background:var(--card,#fff);border-radius:999px;padding:8px 20px;font-weight:var(--ce-fw-head);font-size:14px;cursor:pointer;color:#475569;font-family:inherit}
.bill-btn.active{background:#2563eb;border-color:#2563eb;color:#fff}
.bill-save{font-size:12.5px;font-weight:var(--ce-fw-head);color:#16a34a;background:#dcfce7;border-radius:999px;padding:5px 12px}
.plan .per-month{font-size:12.5px;color:#64748b;margin-top:2px}
.plan-feats{list-style:none;margin:14px 0 0;padding:14px 0 0;border-top:1px dashed var(--line,#e2e8f0);text-align:left;display:flex;flex-direction:column;gap:9px}
.plan-feats li{display:flex;gap:8px;align-items:flex-start;font-size:13.5px;line-height:1.45;color:#334155;background:none;border:0;padding:0;margin:0;list-style:none;width:auto;height:auto;min-width:0;min-height:0;border-radius:0;place-items:initial;grid-template-columns:none;position:static}
.plan-feats li .mk{flex:0 0 auto;width:16px;min-width:16px;text-align:center;font-weight:var(--ce-fw-head);background:none;border:0;border-radius:0;padding:0;color:inherit;font-size:14px;line-height:1.45}
.plan-feats li.feat-ok .mk{color:#16a34a}
.plan-feats li.feat-no{color:#5b6b80}
.plan-feats li.feat-no .mk{color:#cbd5e1}
.plan-feats li.feat-num{color:#334155}
.plan-feats li.feat-num .mk{color:#2563eb}
/* 关键重置：清单里的 <b> 必须是普通加粗文本。
   站点其它地方存在把 <b>/badge 渲染成圆形彩色徽章的规则，
   不重置就会出现"数字圆圈压在文字上"的错版（已实际发生）。 */
.plan-feats li b{display:inline;width:auto;height:auto;min-width:0;margin:0;padding:0;background:none;border:0;border-radius:0;box-shadow:none;font-size:inherit;font-weight:var(--ce-fw-head);color:#1e3a8a;line-height:inherit}
@media(max-width:820px){.plans.plans-3{grid-template-columns:1fr}}

/* ===== v8.11 套餐弹窗（参照主流定价页模板；配色沿用本站青绿主色）===== */
.plans-langs{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:18px;padding:12px 14px;border:1px solid var(--line,#e2e8f0);border-radius:14px;background:linear-gradient(90deg,#f0fdfa,#ffffff)}
.plans-langs .ls-title{font-weight:var(--ce-fw-head);font-size:13px;color:#0f766e}
.plans-langs .ls-flags{font-size:12.5px;color:#64748b}
.plan{position:relative}
.plan .plan-badge{position:absolute;top:-11px;right:12px;font-size:11px;font-weight:900/* ★★ 第 2 步例外 ②：白字压 #f59e0b→#f97316 只有 2.15~2.80:1 ✗ 11px 时字重替对比度打工 ⇒ 保留 900 ✓ */;letter-spacing:.4px;color:#fff;background:linear-gradient(135deg,#f59e0b,#f97316);border-radius:999px;padding:4px 11px;box-shadow:0 3px 8px rgba(245,158,11,.35)}
.plan.recommended .plan-badge{background:linear-gradient(135deg,#14b8a6,#0ea5e9);box-shadow:0 3px 8px rgba(20,184,166,.35)}
.plan .plan-ic{width:46px;height:46px;border-radius:14px;background:linear-gradient(135deg,#ecfdf5,#e0f2fe);display:grid;place-items:center;font-size:23px;margin:2px auto 8px}
.plan .plan-tag{font-size:12.5px;color:#64748b;margin:2px 0 6px;line-height:1.4}
.plan .price-row{display:flex;align-items:baseline;justify-content:center;gap:6px}
.plan .price-row .per{font-size:12.5px;color:#5b6b80;font-weight:600}
.plan .per-month{font-size:12px;color:#0f766e;font-weight:700;min-height:16px}
.plan .plan-credits{font-size:12.5px;margin-top:2px}
.plan-annual{margin:10px 0 2px;padding:9px 11px;border-radius:12px;background:linear-gradient(135deg,#ecfdf5,#f0f9ff);border:1px dashed #99f6e4;text-align:left;line-height:1.45}
.plan-annual b{display:block;font-size:12.5px;color:#0f766e}
.plan-annual span{font-size:11.5px;color:#64748b}
.plan-feats li.feat-sec{display:block;font-size:11.5px;font-weight:var(--ce-fw-head);color:#0f766e;text-transform:uppercase;letter-spacing:.4px;margin:2px 0 -2px}
.plan-feats li.feat-sec2{margin-top:8px;padding-top:8px;border-top:1px dashed var(--line,#e2e8f0)}
.plan-feats li.feat-hl{background:linear-gradient(90deg,#f0fdfa,rgba(255,255,255,0));border-left:3px solid #5eead4;border-radius:0 8px 8px 0;padding:4px 8px 4px 7px;margin-left:-3px;font-weight:700;color:#134e4a}
.plan-feats li.feat-hl .mk{color:#0d9488}
.plan-feats li .chip{display:inline-block;background:#ccfbf1;color:#0f766e;border-radius:999px;padding:1px 8px;font-weight:var(--ce-fw-head);font-size:12.5px;margin-right:2px}

/* ===== v8.11 套餐弹窗（参照主流定价页模板；配色沿用本站青绿主色）===== */
.plans-langs{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:18px;padding:12px 14px;border:1px solid var(--line,#e2e8f0);border-radius:14px;background:linear-gradient(90deg,#f0fdfa,#ffffff)}
.plans-langs .ls-title{font-weight:var(--ce-fw-head);font-size:13px;color:#0f766e}
.plans-langs .ls-flags{font-size:12.5px;color:#64748b}
.plan{position:relative}
.plan .plan-badge{position:absolute;top:-11px;right:12px;font-size:11px;font-weight:900/* ★★ 第 2 步例外 ②：白字压 #f59e0b→#f97316 只有 2.15~2.80:1 ✗ 11px 时字重替对比度打工 ⇒ 保留 900 ✓ */;letter-spacing:.4px;color:#fff;background:linear-gradient(135deg,#f59e0b,#f97316);border-radius:999px;padding:4px 11px;box-shadow:0 3px 8px rgba(245,158,11,.35)}
.plan.recommended .plan-badge{background:linear-gradient(135deg,#14b8a6,#0ea5e9);box-shadow:0 3px 8px rgba(20,184,166,.35)}
.plan .plan-ic{width:46px;height:46px;border-radius:14px;background:linear-gradient(135deg,#ecfdf5,#e0f2fe);display:grid;place-items:center;font-size:23px;margin:2px auto 8px}
.plan .plan-tag{font-size:12.5px;color:#64748b;margin:2px 0 6px;line-height:1.4}
.plan .price-row{display:flex;align-items:baseline;justify-content:center;gap:6px}
.plan .price-row .per{font-size:12.5px;color:#5b6b80;font-weight:600}
.plan .per-month{font-size:12px;color:#0f766e;font-weight:700;min-height:16px}
.plan .plan-credits{font-size:12.5px;margin-top:2px}
.plan-annual{margin:10px 0 2px;padding:9px 11px;border-radius:12px;background:linear-gradient(135deg,#ecfdf5,#f0f9ff);border:1px dashed #99f6e4;text-align:left;line-height:1.45}
.plan-annual b{display:block;font-size:12.5px;color:#0f766e}
.plan-annual span{font-size:11.5px;color:#64748b}
.plan-feats li.feat-sec{display:block;font-size:11.5px;font-weight:var(--ce-fw-head);color:#0f766e;text-transform:uppercase;letter-spacing:.4px;margin:2px 0 -2px}
.plan-feats li.feat-sec2{margin-top:8px;padding-top:8px;border-top:1px dashed var(--line,#e2e8f0)}
.plan-feats li.feat-hl{background:linear-gradient(90deg,#f0fdfa,rgba(255,255,255,0));border-left:3px solid #5eead4;border-radius:0 8px 8px 0;padding:4px 8px 4px 7px;margin-left:-3px;font-weight:700;color:#134e4a}
.plan-feats li.feat-hl .mk{color:#0d9488}
.plan-feats li .chip{display:inline-block;background:#ccfbf1;color:#0f766e;border-radius:999px;padding:1px 8px;font-weight:var(--ce-fw-head);font-size:12.5px;margin-right:2px}

.plans-note{display:flex;gap:9px;align-items:flex-start;margin-top:14px;padding:11px 13px;border-radius:12px;background:linear-gradient(135deg,#fffbeb,#f0fdfa);border:1px solid #fde68a}
.plans-note .pn-ic{font-size:15px;line-height:1.4}
.plans-note b{font-weight:700;font-size:12.5px;color:#92400e;line-height:1.55}
.plans-note .pn-sub{display:inline-block;margin-top:3px;font-size:11.5px;color:#78716c}

/* 注册表单字段间距（用户反馈：三个框太挤 ✗）
   —— 邮箱 / 密码 / 确认密码 / 昵称 / 角色 之间统一留白 ✓
   ★★ 第 8 轮修正 ✗✗ —— 原注释写着"只作用于注册表单 ✓ 不影响登录表单的紧凑排布 ✓"
      结果登录表单【完全没有间距】✗ → 邮箱/密码/登录按钮三个紧贴在一起 ✗✗
      （用户反馈："登录界面这三个选框中间给它留一部分的空隙出来，
                 这样粘在一起肯定是不好看的"✓ —— 观察完全准确 ✓）
     ⇒ 现在【一条规则管两个表单】✓✓（单一来源 ✓）
       以后任何表单新增字段都自动继承 ✓
       不会再出现"这个表单有间距、那个表单没有"✗✗
     ★ 同时把 last-of-type 的 14px 统一成 16px ✗
       （原来它比其它字段少 2px ✗ 视觉上会差一点点 ✓ 属历史遗留 ✓）*/
#loginForm input,
#loginForm select,
#signupForm input,
#signupForm select{margin-bottom:16px}

/* 密码规则行与上方密码框之间也留白 ✓（原来紧贴 ✗）*/
#signupForm .pw-rules{margin-top:10px}
#signupForm .pw-rules summary{cursor:pointer}

/* 同意行对齐 v3 —— 确定性方案 ✓
   用户反馈：勾选框与文字高度不对齐 ✗ + 外框太大 ✗

   ★ 为什么前两版没对齐 ✗：靠"猜"行高 ✗（font-size 13.5px × line-height 1.45 会被
     浏览器四舍五入成不同值 ✓ emoji 还会撑高行盒 ✗ → 偏差不可预测 ✗）
   ★ v3 做法：把行高【钉死】为 20px ✓ → 首行中心恒为 10px ✓
     勾选框 16px ✓ → 其 top 必须 = 10 − 8 = 2px ✓（确定值 ✓ 不靠猜 ✓）
   ★ 外框收紧：padding 6px 9px ✓ 圆角 10px ✓ 外边距 6px ✓
     用户原话："能显示主要内容，并周边适当留空就可以了" ✓ */
.ce-consent-row{
  /* ★ 第 7 轮：改用【自动垂直居中】✗ 替代手算 margin ✓
     用户反馈："勾选框跟这句话高度不是对齐的"✓
     根因：原来是 align-items:flex-start ✗ + .kcbx 手动 margin-top:2px ✗
       （注释写着"= 行高/2 − 勾选框/2 = 10 − 8"✗ —— 这种手算只要行高/字号一变就错位 ✗✗）
     ⇒ 改成 center ✓ 由浏览器自动居中 ✓ 永远不会错位 ✓✓
   ★★ 第 9 轮：两行必须【像素级一致】✗✗
     用户反馈："这两个勾选框…竖向的这个左右的排列又没有进行对齐，
               你把它对齐参照下面那个方框的位置为准，把上面那个向右稍微再调整一下"✓
     根因（实测两行的 inline style 逐字符对比 ✓）：
       第 1 行写了 font-size:13.5px ✓ line-height:1.45 ✓
       第 2 行【没有 font-size】✗ line-height:1.5 ✗
       ⇒ 两行字号与行高都不同 ✗ → 勾选框（固定 13px）挤在不同字号的行里 ✗
         → 基线不同 ✓ 视觉上就"没有对齐"✓✓（用户观察准确 ✓）
     ⇒ 修法不是去猜"差几像素"✗ 而是【用 CSS 强制两行完全一致】✓✓
       这样不管原始 inline 写了什么 ✓ 结果都一致 ✓ 一劳永逸 ✓ */
  align-items:center!important;
  padding:6px 9px!important;
  margin:6px 0!important;
  border-radius:10px!important;
  gap:8px!important;
  line-height:20px!important;          /* ★ 钉死行高 ✓ 对齐才能确定 ✓ */
  font-size:13.5px!important;          /* ★ 第 9 轮：钉死字号 ✓ 第 2 行原来没有 ✗ 会继承成别的值 ✓ */
}
.ce-consent-row .kcbx{
  /* ★ 第 7 轮：16px → 13px ✓（用户要求"收窄 20%"✓ 16×0.8 = 12.8 ≈ 13 ✓）
     并去掉手算的 margin-top ✗（居中交给父级的 align-items:center ✓）*/
  flex:0 0 auto!important;
  width:13px!important;height:13px!important;
  margin:0!important;
  padding:0!important;
}
.ce-consent-row .ce-consent-emoji{
  flex:0 0 18px!important;width:18px!important;
  text-align:center;line-height:20px!important;font-size:14px!important;margin:0!important;
}
.ce-consent-row span{line-height:20px!important}
.ce-consent-row small{line-height:18px!important}   /* 提示行稍紧 ✓ 让外框更矮 ✓ */

/* ★ 第 4 轮规范：密码强度条 = 4 段连续条（替换原 4 颗宝石 ✗）
   规则（用户给的规范 ✓）：
     · 连续点亮前 N 段 ✓（N = 满足的条件数 ✓）
     · 颜色随等级：弱=红(#ef4444) 中=橙(#f59e0b) 良=黄(#eab308) 强=绿(#22c55e) ✓
   为什么这样更好：颜色本身就在表达强弱 ✓ 不需要额外图例 ✓ 且一眼可读 ✓ */
.pw-meter{display:flex;gap:6px;width:100%;margin:0 0 8px;padding:0 2px;box-sizing:border-box}
.pw-meter .seg{flex:1 1 0;height:8px;border-radius:4px;background:#e5e7eb;transition:background .18s ease}
.pw-meter .seg.ok.s1,.pw-meter .seg.ok.s2,.pw-meter .seg.ok.s3,.pw-meter .seg.ok.s4{background:#22c55e}
/* 点亮色由 JS 通过行内 background 精确指定 ✓ 这里只做兜底与圆角 ✓ */
.pw-meter .seg.ok{background:#22c55e}

/* ★ 第 5 轮：定价页两处优化（用户反馈 ✓）
   ① 年付优惠徽标【嵌进年付按钮内部】✓
      原先它是并排的独立标签 ✗ 很多人看不出"是年付才省"✗
      ⇒ 内嵌后语义不可能被误解 ✓✓
   ② 充值包卡片加【纯 CSS 星点 + 光斑】✓（不用图片文件 ✓） */
.bill-btn-year{display:inline-flex;align-items:center;gap:7px}
.bill-btn-year .bill-save{
  display:inline-block;padding:2px 7px;border-radius:999px;
  font-size:11px;font-weight:var(--ce-fw-head);line-height:1.5;
  background:rgba(255,255,255,.22);color:#fff;
  border:1px solid rgba(255,255,255,.34);
}
.bill-btn:not(.active) .bill-save{background:#ECFDF5;color:#047857;border-color:#A7F3D0}
.bill-hint{display:block;width:100%;text-align:center;font-size:12.5px;color:#64748b;margin-top:8px;min-height:1em}

/* 充值包装饰 ✓ */
.pack-card{position:relative;overflow:hidden}
.pack-glow{
  position:absolute;inset:-40% -20% auto -20%;height:150%;
  background:radial-gradient(60% 60% at 50% 0%,rgba(16,184,196,.16),transparent 70%);
  pointer-events:none;z-index:0;
}
.pack-2 .pack-glow{background:radial-gradient(60% 60% at 50% 0%,rgba(37,99,235,.16),transparent 70%)}
.pack-3 .pack-glow{background:radial-gradient(60% 60% at 50% 0%,rgba(245,158,11,.18),transparent 70%)}
.pack-stars{
  position:absolute;top:9px;right:11px;font-size:12px;letter-spacing:2px;
  color:#10B8C4;opacity:.55;pointer-events:none;z-index:0;
}
.pack-2 .pack-stars{color:#2563EB;opacity:.6}
.pack-3 .pack-stars{color:#f59e0b;opacity:.7}
.pack-card>*:not(.pack-glow):not(.pack-stars){position:relative;z-index:1}

/* ★ 第 6 轮：定价卡"划线价 → 折后价"（用户 v2 排版 ✓）
   目的（用户原话）："原价划线 → 折后价（最直观的省钱信号）"✓
   ★ 数字全部由 app.js 推导 ✓ 不手写 ✗（v2 设计稿就是手写导致三列数字错位 ✗）*/
.price-was-row{display:flex;align-items:center;gap:7px;justify-content:center;min-height:18px;margin:2px 0 0}
.price-was{color:#5b6b80;text-decoration:line-through;text-decoration-thickness:1.5px;font-size:14px;font-weight:600}
.was-note{font-size:11px;color:#5b6b80}
.per-month{font-size:12.5px;color:#64748b;text-align:center;margin-top:3px}
.save-badge{
  display:inline-block;margin:7px auto 0;padding:3px 10px;border-radius:999px;
  background:#ECFDF5;color:#047857;border:1px solid #A7F3D0;
  font-size:12px;font-weight:var(--ce-fw-head);text-align:center;
}
.plan{display:flex;flex-direction:column;align-items:center}

/* ══════════════════════════════════════════════════════════════════
   ★★ 第 24 轮：隐私弹窗的【锁定原因提示】✓✓
   用户要求："一定要滚到底 ✓ 滚到底以后并且同时满足 5 秒才能勾选"✓
   ⇒ 锁定时必须在【勾选框上方】直接说明原因 ✗✗
     原来只有底部状态栏有小字 ✓ 用户盯着勾选框看不到 ✓✓
   ══════════════════════════════════════════════════════════════════ */
.ce-priv-lock{
  margin: 2px 0 8px;
  padding: 9px 12px;
  border-radius: 10px;
  background: #fff8e6;
  border: 1px dashed #f0cf85;
  color: #8a6a14;
  font-size: 12.5px;
  font-weight: 700;
  line-height: 1.5;
}

/* ══════════════════════════════════════════════════════════════════
   ★★ 第 25 轮：成长中心 / 邀请有礼 / 精灵星 三个入口按钮的排布 ✓
   事故（用户截图 ✓）：三个按钮挤在一条线上 ✗ 出现 "🌱 N…" 截断 ✗✗
   根因：我在 .ce-card-heading 里加了 2 个按钮 ✗ 却没安排换行 ✓
     （.ce-card-heading 是 flex + space-between ✓ 原来只放 1 个按钮 ✓）
   ★ 修法 ✓：让按钮【能换行 + 不被压扁】✗ 窄屏时整行铺开 ✓✓
   ══════════════════════════════════════════════════════════════════ */
.ce-card-heading{flex-wrap:wrap}
.ce-card-heading > div:first-child{min-width:0}
.gc-entry{
  flex:0 0 auto;                 /* ★ 不被压扁 ✗ 该多大就多大 ✓ */
  white-space:nowrap;            /* ★ 文字不换行 ✗（换行会更难看 ✓）*/
  margin-inline-start:6px;
}
/* ★ 窄屏：三个按钮各占一行宽度的 1/3 ✗ 整齐排布 ✓ 不再溢出 ✓ */
@media(max-width:640px){
  .ce-card-heading{flex-direction:column;align-items:stretch}
  .ce-card-heading > div:first-child{width:100%}
  .ce-card-heading > .gc-entry{width:100%;margin-inline-start:0;margin-block-start:8px}
}
/* ★ 中大屏：让三个按钮在同一条线上【均匀分布】✗ 不重叠 ✓ */
@media(min-width:641px){
  .ce-card-heading{row-gap:8px}
  .gc-entry{font-size:12.5px!important;padding-inline:10px!important}
}

/* ══════════════════════════════════════════════════════════════════
   ★★★ 第 32 轮根治「点了没反应」✗✗（实测复现后定位 ✓）

   真因 ✗：隐私弹窗的 .modalbox 有 overflow:hidden ✓
     而 overflow:hidden 的盒子【仍然可以被程序滚动】✗
     点勾选框 → 浏览器把焦点元素滚进视野 ✓ → 容器内部滚了一点 ✓
     → 元素在 mousedown 与 mouseup 之间移出光标 ✓
     → 浏览器判定不是一个元素 → 【不派发 click】✗✗
     ⇒ 表现："点了以后没有任何反应"✓（用户第 5 次报障 ✓）

   修法 ✓：
     ① overflow: clip ✗✗ —— 禁止一切滚动 ✓ 含程序滚动 ✓
        （圆角形状不变 ✓ 只是不再能被内部滚动 ✓）
     ② 勾选框给正常尺寸 ✗（实测被拉成 27×46 ✗ 又高又窄 ✓
        命中区很怪 ✓ 给成 22×22 正方形 ✓✓）
   ══════════════════════════════════════════════════════════════════ */
#privacyModal .modalbox{
  overflow: hidden;      /* 老浏览器兜底 ✓ */
  overflow: clip;        /* ★ 关键 ✗✗ 禁止程序滚动 ✓（Chrome 90+ / Safari 16+ ✓）*/
}
/* ★ 勾选行：给 input 正常尺寸与对齐 ✗ 并确保不重叠 ✓ */
#privacyModal label#privacyCheckboxLabel,
#privacyModal label#privacyParentLabel{
  position: relative;      /* ★ 各自独立层 ✗ 防命中区互相串 ✓ */
  z-index: auto;
  min-height: 26px;
  flex: 0 0 auto;
}
#privacyModal #privacyAgreeCheckbox,
#privacyModal #privacyParentCheckbox{
  width: 22px; height: 22px;      /* ★ 正方形 ✗ 原来是 27×46 的怪形 ✓ */
  flex: 0 0 22px;
  align-self: flex-start;          /* ★ 与首行文字对齐 ✗ 不再垂直居中拉伸 ✓ */
  margin: 0;
  position: relative;
  z-index: 2;                      /* ★ 保证命中优先 ✓ */
  cursor: pointer;
}
/* ★ 文字也要能点 ✗ 且不越界 ✓ */
#privacyModal #privacyCheckboxText,
#privacyModal #privacyParentText{ flex: 1 1 auto; min-width: 0; }



/* ══════════════════════════════════════════════════════════════════
   ★★★ 第 33 轮：未登录时的【拉新卡】✗✗（按用户给的设计 ✓）
   ★ 三条动画照抄用户给的值 ✗ 一个数都不改 ✓：
     ⭐ 错峰闪 5s / 延迟 0s · 1.25s · 2.5s ✓（永不同时亮 ✓）
     🌱 轻摇 9s ✗ 根部为轴（transform-origin: 50% 95% ✓）
     🎁 礼盒 1.6s ✓ ＋ CTA 呼吸 2s ✓
   ★ 只改两处 ✗：主色对齐本站青绿 ✓ 并补 prefers-reduced-motion ✓
   ★ RTL：全用逻辑属性 ✗ 阿语/波斯语自动镜像 ✓
   ══════════════════════════════════════════════════════════════════ */
.ce-guest-card{display:none}
/* ★ 未登录：藏内部内容 ✗ 显示拉新卡 ✓✓ */
.ce-credit-card.ce-guest-mode .ce-card-heading,
.ce-credit-card.ce-guest-mode #creditMeterWidget,
.ce-credit-card.ce-guest-mode .ce-credit-readout,
.ce-credit-card.ce-guest-mode .ce-tier-rail,
.ce-credit-card.ce-guest-mode .ce-tier-caption,
.ce-credit-card.ce-guest-mode .ce-mini-stats,
.ce-credit-card.ce-guest-mode #creditNotice,
.ce-credit-card.ce-guest-mode .ce-credit-actions,
.ce-credit-card.ce-guest-mode #ceLedgerWrap,
.ce-credit-card.ce-guest-mode ~ #membershipCard{display:none!important}
.ce-credit-card.ce-guest-mode .ce-guest-card{display:block}
/* ★ 已登录：拉新卡绝不出现 ✗ */
.ce-credit-card:not(.ce-guest-mode) .ce-guest-card{display:none!important}

.ce-guest-hero{
  position:relative;overflow:hidden;text-align:center;
  margin:8px 2px 0;padding:18px 16px 16px;border-radius:18px;
  background:linear-gradient(180deg,#e8f7ff,#f0fdf4);
}
/* 🌱 小树苗：根部为轴 ✗ 每约 9 秒轻摇一次 ✓
   ★★★ 第 39 轮修 ①：轻摇【只给树苗】✗✗ —— 小精灵是【人】✗ 整个人晃很怪 ✓
     真因 ✗✗：这条动画原来挂在【通用】的 `.ce-guest-sproutwrap img` 上 ✓
       而第 36 轮把那个 <img> 从【树苗】换成了【小精灵】✗
       ⇒ 树苗的"植物轻摇"就落到人身上了 ✓（用户报的"整个人在摇晃"就是它 ✓）
     真浏览器实测（修之前 ✗ 只服务 public/ 的 HTTP 服务 + 真 Chrome ✓）：
       animationName=ceGuestSway ✗ 9s ✗ transform-origin=56px 106.4px
       transform 每 500ms 采样 12 次 ✗ 角度 ∈ {0, 0.14, 4.65, -0.34, -2.99, 1.98, 0.04}
       ⇒ 峰值 4.65° ✗ 肉眼可见 ✓✓
     ★★ 为什么"有视频也照样看得见摇" ✗✗：video 的 opacity 是 0 ✗
       只有真的 play() 起来（readyState≥2 且 !paused ✓）才加 .is-ready 淡入 ✓
       实测时间线 ✗：3.23s 卡片出现（静态图在摇 ✓）→ 3.41s video 入 DOM
         → 3.91s 才 is-ready（淡入 320ms ⇒ 约 4.2s 才盖住那张摇着的静态图 ✓）
       而 reduce / 省流（★ 第 41 轮起只有 saveData 与 slow-2g/2g ✓）下 JS【根本不插 video】✗
          ★ '3g' 已【不再】算省流 ✗ 因为 effectiveType 是 Chrome NQE 的估算值 ✓
            且会在 4g/3g 间来回跳 ✗ 旧写法让真域名下【永远不插】✓
            依据与实测见 v55-enhance.js 的 slowNet() ✓✓
       ⇒ 那些用户看到的就是【一张一直在摇的小精灵】✗✗（用户报的就是这个 ✓）
     ★ 判据 ✗：由 v55-enhance.js 给【同一个 <img>】切换 .ce-guest-sprout ✓
       （只有 src 落到 /brand/guest-sprout.webp 这个兜底图时才打 ✓ 小精灵时不打 ✓✓）
     ★★ 为什么【不】用 animation:none + !important 去反压小精灵 ✗✗
       那要跟下面豁免段那条【通用 img】规则 !important 对轰 ✗
       正是本仓库反复踩过的坑（见 ui-type-scale.css:124 的教训 ✓）
       ⇒ 改成【更高特异性 + 后加载】✗ 全程没有一个用于"压制"的 !important ✓✓
     ★ 结构 ✗（本次定案 ✓）：本条只管【几何】✗ 动画收在下面
       「访客卡动画的豁免开关」那一段 ✗ 三条各管一件事 ✓✓
         ① 常态：壳里的主视觉轻摇 ✓
         ② 小精灵（人）：永远不摇 ✓ 更高特异性 ✗ 不用 !important ✓
         ③ 树苗兜底：连 reduce 下也要摇 ✓（那条才需要 !important ✗ 见那里的说明 ✓）*/
.ce-guest-sproutwrap{
  width:112px;height:112px;margin:0 auto;border-radius:50%;
  background:radial-gradient(circle,#fff 60%,#eafaf0);
  display:flex;align-items:center;justify-content:center;
  box-shadow:inset 0 -4px 8px rgba(0,0,0,.04);overflow:hidden;
}
.ce-guest-sproutwrap img{
  width:100%;height:100%;object-fit:contain;display:block;
  transform-origin:50% 95%;                 /* ★ 以根部为轴 ✗ 不是中心 ✓ */
}
/* ★★ 常态下这句【只对树苗兜底图生效】✗✗（.ce-guest-sprout 由 JS 按 src 打 ✓）
   ★ 为什么写这里而不是写在豁免段 ✗：与星星/礼盒/CTA 的写法保持一致 ✓
     （它们也是【常态一条 + 豁免段一条 !important】✓ 见下面那一段 ✓）
   ★★ 注意它【不带】.ceGuestMotionExempt ✗ 也【不带】!important ✓
     ⇒ reduce 下会被上面那条全局动画禁令杀掉 ✓ 再由豁免段那条 !important 版本救回来 ✓✓ */
.ce-guest-sproutwrap img.ce-guest-sprout{
  animation:ceGuestSway 9s ease-in-out infinite;
}
@keyframes ceGuestSway{
  0%,62%{transform:rotate(0deg) translateY(0)}
  70%{transform:rotate(5deg) translateY(-2px)}
  78%{transform:rotate(-4deg) translateY(-1px)}
  85%{transform:rotate(2deg)}
  92%,100%{transform:rotate(0deg)}
}
/* ⭐ 三颗星错峰闪 ✗ 周期 5s ✓ 永远不同时亮 ✓ */
.ce-guest-spark{position:absolute;width:16px;height:16px;pointer-events:none;animation:ceGuestTw 5s ease-in-out infinite}
.ce-guest-spark img{width:100%;height:100%;display:block}
.ce-guest-spark.s1{inset-inline-start:22px;inset-block-start:20px;animation-delay:0s}
.ce-guest-spark.s2{inset-inline-end:24px;inset-block-start:36px;animation-delay:2.5s}
/* ★ 第三颗星原本用 inset-block-end:62px ✗ 但本站的 hero 比原稿高 ✓
   → 它落到了 CTA 按钮上 ✗（截图里能看到 ✓）
   ⇒ 改成【相对顶部】定位 ✗ 贴着树苗左下 ✓ 不管 hero 多高都不会压到按钮 ✓✓ */
.ce-guest-spark.s3{inset-inline-start:26px;inset-block-start:132px;animation-delay:1.25s}
@keyframes ceGuestTw{
  0%,100%{transform:scale(.6) rotate(0deg);opacity:.25}
  10%{transform:scale(1.2) rotate(20deg);opacity:1}
  20%{transform:scale(.8) rotate(0deg);opacity:.5}
  30%{transform:scale(1);opacity:.9}
  40%,100%{transform:scale(.6);opacity:.25}
}
.ce-guest-title{
  margin:8px 0 5px;font-size:19px;font-weight:var(--ce-fw-head);line-height:1.35;
  background:linear-gradient(100deg,#0f8f74,#2fae6a);
  -webkit-background-clip:text;background-clip:text;color:transparent;
}
/* ★ 对比度：11px 小字在浅底上只有 4.17:1 ✗ ⇒ 色值加深一档（字号不动 ✓）*/
.ce-guest-sub{margin:0 0 13px;font-size:12.5px;line-height:1.6;color:#4a5a6b}
.ce-guest-sub b{color:#d98324;font-weight:var(--ce-fw-head)}
/* 🎁 礼盒轻晃 ✓ */
.ce-guest-gift{display:inline-block;font-size:22px;vertical-align:-4px;animation:ceGuestShake 1.6s ease-in-out infinite}
@keyframes ceGuestShake{
  0%,100%{transform:rotate(0)}
  20%{transform:rotate(-10deg)}40%{transform:rotate(9deg)}
  60%{transform:rotate(-6deg)}80%{transform:rotate(4deg)}
}
.ce-guest-perks{display:flex;flex-direction:column;gap:7px;text-align:start;margin:0 2px 14px}
.ce-guest-perk{
  display:flex;align-items:center;gap:9px;background:#fff;
  border:1px solid #eef2f0;border-radius:12px;padding:8px 11px;
  font-size:12.5px;color:#33415c;font-weight:700;
}
.ce-guest-perk .ic{font-size:16px;flex:0 0 auto}
.ce-guest-perk b{font-weight:var(--ce-fw-head)}
/* CTA 呼吸 ✓ */
/* ★★ 对比度：白字压在渐变上 ✗ 原来最浅一端 #12b3a0 只有 2.63:1 ✗ 实测 2.74:1 ✗
   正文（16px 700 ✗ 不是大字）要 4.5:1 ✓ ⇒ 两个渐变端【一起压深】✗ 白字与字号都不动 ✓
   最浅一端 #0a7a9c = 4.91:1 ✓ 最深一端 #0c6b56 = 6.7:1 ✓ 整条渐变都过 ✓ */
.ce-guest-cta{
  display:block;width:100%;border:0;cursor:pointer;
  background:linear-gradient(90deg,#0a7a9c,#0c6b56);color:#fff;
  font-size:16px;font-weight:var(--ce-fw-head);padding:13px;border-radius:14px;
  box-shadow:0 6px 14px rgba(18,179,160,.35);
  animation:ceGuestPulse 2s ease-in-out infinite;
  /* ★ 第 35 轮：这一个 CTA 少了这一行 ✗ 于是拿到了 body 的 --font-stack（Inter）✗
     而标题与其它按钮都走 --font-display（含 'Fredoka' 圆体 ✓）
     ⇒ 用户点名"要 Q 版可爱字体"的地方反而最不圆 ✓
     ★ 用 var(--font-display, inherit) 而不是写死 'Fredoka' ✗：
       字体栈由 themes.css 单点维护 ✓ 换字体不用回来改这里 ✓✓ */
  font-family:var(--font-display, inherit);
}
@keyframes ceGuestPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.02)}}
.ce-guest-cta:hover{filter:brightness(1.05)}
/* ★ 对比度：11px 小字 2.86 / 2.04:1 ✗ ⇒ 色值加深（字号不动 ✓ 不靠放大达标 ✓）*/
.ce-guest-note{text-align:center;font-size:11px;color:#4a5a6b;margin-block-start:9px}
.ce-guest-note b{color:#d98324;font-weight:var(--ce-fw-head)}
.ce-guest-foot2{text-align:center;font-size:11px;color:#4a5a6b;padding:12px 4px 2px}
@media(max-width:560px){
  .ce-guest-sproutwrap{width:96px;height:96px}
  .ce-guest-title{font-size:17.5px}
  .ce-guest-perk{font-size:12px}
}
/* ★★★ 无障碍 & 无头截图兜底 ✗✗ —— 本仓库的规矩 ✓
   减少动态偏好下【关掉全部动画】✓（用户能正常用 ✓ 截图也能拿到静态图 ✓）*/
@media(prefers-reduced-motion:reduce){
  .ce-guest-sproutwrap img,
  .ce-guest-spark,
  .ce-guest-gift,
  .ce-guest-cta{animation:none!important}
  .ce-guest-cta{transform:none!important}
}

/* ══════════════════════════════════════════════════════════════════
   ★★ 第 34 轮：访客卡动画的【豁免开关】✗✗
   ★ 问题 ✗✗：上面有一条全局规则
       @media(prefers-reduced-motion:reduce){*{…animation:none!important}}
     它带 !important ✗ 而访客卡的动画没有 ✓ → 被它杀掉 ✓✓
     ⇒ 只要系统开了"减少动态效果"✗ 树苗不摇 ✗ 星星不闪 ✓
       （无头浏览器默认就是 reduce ✗ 所以预览图也是静的 ✓ 这是已知陷阱 ✓）
   ★ 无障碍上【默认应该尊重它】✗（动态敏感的用户不该被强加动画 ✓）
     所以这里默认【不豁免】✓ —— 但把开关留出来 ✓
   ★★ 怎么打开豁免 ✗✗（二选一 ✓）
     (a) 在 index.html 的 <html> 上加一个类 ✗ 例如 <html class="ceGuestMotionExempt">
     (b) 或者把下面这行的 @media 条件删掉 ✗ 直接无条件生效 ✓
   ⇒ 我倾向 (a) ✗ 因为可以只在访客卡上豁免 ✓ 不影响其它动效 ✓✓
   ══════════════════════════════════════════════════════════════════ */
/* ★★★ 第 39 轮修 ① ✗✗：访客卡主视觉的【小精灵 vs 树苗】就在这一段分开 ✓
   背景 ✓：下面第一条是从【树苗时代】留下来的通用规则 ✓
     那时壳里唯一的 <img> 就是树苗 ✓ 所以"壳里的 img 轻摇"= "树苗轻摇" ✓
     第 36 轮那个 <img> 换成了小精灵 ✗ 这条通用规则就开始"摇人"了 ✓
     实测（修之前 ✓）：小精灵拿到 animationName=ceGuestSway / 9s / transform 峰值 4.65° ✓✓
   ★★ 为什么第一条【必须去掉 !important】✗✗（本次修法的关键 ✓）
     它带 !important 时【没有任何非 !important 规则能压过它】✗
     ⇒ 想让它别摇小精灵，就只能也上 !important 对轰 ✗ 那是本仓库的禁忌 ✓
       （通配 !important 误伤的教训见 ui-type-scale.css:124 ✓）
     ⇒ 去掉 !important ⇒ 下面第二条用【更高特异性 + 后加载】就能正常挡住它 ✓✓
   ★★ 去掉 !important 会不会削弱豁免 ✗✗：不会 ✓ 树苗在 reduce 下照样摇 ✗
     由第三条（树苗专用 + !important ✓）保证 ✓
     实测 ✓（reduce 模式 + 真 Chrome ✓）：树苗 animationName=ceGuestSway / 峰值 4.95° ✓✓
   ★★ 第一条【不许】再加回 !important ✗✗：加回来小精灵就会重新开始摇 ✓✓
   ★ 第一条的【选择器与时长一个字都没动】✗（去掉的只有 !important ✓）
     ⇒ tests/growth-cards.js:669 / 587 / 588 / 601 四条断言全部照旧成立 ✓✓ */
html.ceGuestMotionExempt .ce-guest-sproutwrap img{animation:ceGuestSway 9s ease-in-out infinite}
/* ★★★ 小精灵（人）【永远不摇】✗✗
   ★ 特异性 (0,4,2) > 第一条 (0,2,2) ✓ 而且后加载 ✓
     ⇒ 不需要 !important ✗ 一个都不加 ✓（这就是"更高特异性 + 后加载"的正路 ✓）
   ★ :not(.ce-guest-sprout) 把两个状态写明白 ✗
     同一个 <img>：小精灵 = 有 .ce-guest-visual 且【没有】.ce-guest-sprout ✓
                   树苗兜底 = 两个都有 ✓（由 v55-enhance.js 的 markVisual() 维护 ✓）
   ★ reduce 下小精灵也是静的 ✗ 由上面那条
     .ce-guest-sproutwrap img{animation:none!important} 兜住 ✓
     （它同样不需要我在这里再补 !important ✓✓）*/
html.ceGuestMotionExempt .ce-guest-sproutwrap img.ce-guest-visual:not(.ce-guest-sprout){animation:none}
/* ★ 树苗兜底图：连 reduce 下【也要摇】✓（这才是豁免开关的本来用途 ✓）
   ★★ 这里【必须】!important ✗：reduce 下有一条全局规则
        @media(prefers-reduced-motion:reduce){*{animation:none!important}}
      它是 !important ✗ 只能用同级别 + 更高特异性赢它 ✓（(0,3,2) > (0,0,0) ✓）
   ★ 作用范围 ✗：只命中 img.ce-guest-sprout ✓ 与上面那条"小精灵永不摇"无交集 ✓
     ⇒ 这里的 !important【不是】用来压制谁的 ✗ 只是让树苗的动画在全局禁令下活下来 ✓✓ */
html.ceGuestMotionExempt .ce-guest-sproutwrap img.ce-guest-sprout{animation:ceGuestSway 9s ease-in-out infinite!important}
html.ceGuestMotionExempt .ce-guest-spark{animation:ceGuestTw 5s ease-in-out infinite!important}
html.ceGuestMotionExempt .ce-guest-gift{animation:ceGuestShake 1.6s ease-in-out infinite!important}
html.ceGuestMotionExempt .ce-guest-cta{animation:ceGuestPulse 2s ease-in-out infinite!important}
/* ══════════════════════════════════════════════════════════════════════
   ★★★ 用户可控的「界面动画」开关 ✗✗（html.ceMotionOn / html.ceMotionOff ✓）
   ══════════════════════════════════════════════════════════════════════
   为什么需要它 ✗：系统开了「减少动态效果」（Windows MinAnimate=0 / VisualFXSetting=2 ✓
     360 之类优化软件常顺手关掉 ✓）⇒ Chrome 报 prefers-reduced-motion:reduce ✓
     ⇒ themes.css:149 的 @media(prefers-reduced-motion:reduce){*{animation:none!important}}
       把【全站动画】关掉 ⇒ Credits 卡上的水壶 / 太阳 / 云 / 树全停 ✗
     用户要求：给一个【自己说了算】的开关 ✓ 打开就照播 ✓
   三态（存 localStorage.calcelf_motion ✓ 由 app.js 的 ceApplyMotionPref() 写类 ✓）：
     · 未设置或 'follow' ⇒ html 上【两个类都没有】✗ 完全跟随系统 ✓✓
     · 'on'  ⇒ html.ceMotionOn  ⇒ 本块生效（只在系统 reduce 时才需要 ✓）
     · 'off' ⇒ html.ceMotionOff ⇒ 只在系统 no-preference 时执行 ★②★ ✓
   ★★ 为什么这里【必须】!important ✗✗（不是偷懒 ✗ 是唯一可行 ✓ 已实测 ✓）
     · 媒体查询 @media(prefers-reduced-motion) 是 CSS【改不了】的 ✗
       ⇒ 想覆盖它只能靠 JS 加类 + 更高优先级规则 ✓
     · 而被覆盖的那条 kill switch 带 !important ✗ 其特异性只有 (0,0,0) ✓
       实测（真 Chrome ✓ reduce ✓ 已在 html 上加 ceMotionOn ✓）：
         · 不带 !important 的 (0,2,0) 规则  ⇒ 树 animationName=none ✗✗（输 ✓）
         · 带   !important 的 (0,2,0) 规则  ⇒ 树 animationName=ce2Sway ✓ 真的在摆 ✓✓
         · 只给 animation-name 加 !important（其余长写不加）⇒ 也是 none ✗✗
           ★ 原因：kill switch 用的是【简写】animation:none!important ✓
             简写会把 animation-name 一起重置 ✓ 而我的 animation-name 声明
             在【普通层】⇒ 照样被它压掉 ✓ ⇒ 必须整条 animation 简写带 !important ✓
       ⇒ 结论：这不是"对轰" ✗ 是"同层比优先级" ✓ 且【范围极小】✗ 见下 ✓
   ★★★ 豁免范围（只放行以下动画 ✓ 别的照旧静止 ✓）
     · 全部是【局部循环的环境装饰】：旋转 / 缩放 / 透明度 / ≤7px 位移 ✓
     · ★ 大幅位移的【一律不放行】✗✗：.ce2-bird-1 / .ce2-bird-2（ce2Fly 横穿 500 单位 ✓）
       保持 display:none ✓ 与 themes.css:360 的既有无障碍安全阀一致 ✓
       （tests/tree-scene-fx.js:259-263 正守着这一点 ✓ 默认行为不许变 ✓）
     · 未开开关的用户【两个类都没有】⇒ 本块整体不生效 ⇒ 行为与改动前逐字节相同 ✓✓ */
@media(prefers-reduced-motion:reduce){
  /* ★★★ 四种组合【真 Chrome 实测】（1400 + 390 两个视口 ✓ computed animationName ✓
     截图 motiontoggle-card-*.png ✓；reduce 必须用 CDP Emulation.setEmulatedMedia 显式设 ✓
     —— 无头浏览器默认就是 reduce ✗ 不显式设两组图会一模一样 ✓ 本仓库踩过这个坑 ✓）：
     ① reduce + 未动开关  ⇒ tree=none sun=none can=none cloud=none bird=none ✓（= 改动前 ✓）
     ② reduce + 开关打开  ⇒ tree=ce2Wilt sun=ce2Rays can=ceCanFloat,ceCanPour cloud=ce2Drift1 ✓✓
     ③ no-pref + 未动开关 ⇒ tree=ce2Wilt sun=ce2Rays bird=ce2Fly wind=ce2WindLine ✓（= 改动前 ✓）
     ④ no-pref + 开关关闭 ⇒ tree=none sun=none cloud=none wind=none ✓
          而 bird=ce2Fly 仍在飞 ✓（故意 ✓ 见 ★②★ 那段说明 ✓）
     ★ ②里鸟仍然是 none ✓（ce2Fly 的位移是 0.36~1.4 个视口宽 ✗ 不属"范围最小"✓✓）*/
  html.ceMotionOn .ce-status-pill{animation:ceStatusPulse 3s ease-in-out infinite!important}
  html.ceMotionOn .ce2-tree{animation:ce2Sway 4.2s ease-in-out infinite!important}
  html.ceMotionOn .ce-credit-widget.tier-3 .ce2-tree{animation:ce2Sway 4.9s ease-in-out infinite!important}
  html.ceMotionOn .ce-credit-widget.tier-2 .ce2-tree{animation:ce2Droop 5.6s ease-in-out infinite!important}
  html.ceMotionOn .ce-credit-widget.tier-1 .ce2-tree{animation:ce2Wilt 3.6s ease-in-out infinite!important}
  html.ceMotionOn .ce2-rays{animation:ce2Rays 18s linear infinite!important}
  html.ceMotionOn .ce-credit-widget.tier-1 .ce2-rays{animation:ce2Rays 10s linear infinite!important}
  html.ceMotionOn .ce2-sunglow{animation:ce2Glow 3.6s ease-in-out infinite!important}
  html.ceMotionOn .ce-credit-widget.tier-1 .ce2-sunglow{animation:ce2Glow 1.6s ease-in-out infinite!important}
  html.ceMotionOn .ce2-halo{animation:ce2HaloBreath 5.5s ease-in-out infinite!important}
  html.ceMotionOn .ce2-halo-inner{animation:ce2HaloBreath 4.2s ease-in-out infinite .8s!important}
  html.ceMotionOn .ce2-sun-core{animation:ce2CoreTwinkle 3.1s ease-in-out infinite!important}
  html.ceMotionOn .ce2-sun-core2{animation:ce2CoreTwinkle 2.4s ease-in-out infinite .6s!important}
  html.ceMotionOn .ce2-canwater{animation:ce2Slosh 1.5s ease-in-out infinite!important}
  /* ★ 顺序有讲究 ✗✗：先写【带档位】的（特异性更高 ✓）再写裸 .ce2-can ✗
     否则裸的那条会把 tier-1/2 的 ce2CanAlert 抢走 ⇒ 水壶反而不跳了 ✓
     （与 themes.css:325-329 / 445-448 的档位规则一一对应 ✓ 只是补上 reduce 下的 !important ✓）
     ★★ tier-1/2 必须和【未开 reduce】时一模一样 ✗✗：themes-ceremony.css:324-328 明写
        「ceCanFloat + ceCanPour 同时上 ✗ 正好是"跳着倒水"的意图」✓ 而且它特意包在
        no-preference 里 ✓ 因为"壶在倒水但没有水"是自相矛盾 ✓
        ⇒ 我在 reduce 下补的是【加水】那一半（ceWaterSlosh ✓ 见上一条 ✓）
          所以这三件套必须一起出现 ✗ 不能只放 ce2CanAlert ✓✓ */
  html.ceMotionOn .ce-credit-widget.tier-2 .ce2-can{animation:ceCanFloat 2.2s ease-in-out infinite,ceCanPour 2.5s ease-in-out infinite!important}
  html.ceMotionOn .ce-credit-widget.tier-1 .ce2-can{animation:ceCanFloat 2.2s ease-in-out infinite,ceCanPour 2.5s ease-in-out infinite!important}
  html.ceMotionOn .ce-credit-widget.tier-2 .ce2-canwater{animation:ceWaterSlosh 2.5s ease-in-out infinite!important}
  html.ceMotionOn .ce-credit-widget.tier-1 .ce2-canwater{animation:ceWaterSlosh 2.5s ease-in-out infinite!important}
  html.ceMotionOn .ce-credit-widget.tier-2 .ce-tree .leaf,
  html.ceMotionOn .ce-credit-widget.tier-2 .ce-tree .leaf-a,
  html.ceMotionOn .ce-credit-widget.tier-2 .ce-tree .leaf-b{animation:ceLeafBoost 2.5s ease-in-out infinite!important}
  html.ceMotionOn .ce-credit-widget.tier-1 .ce-tree .leaf,
  html.ceMotionOn .ce-credit-widget.tier-1 .ce-tree .leaf-a,
  html.ceMotionOn .ce-credit-widget.tier-1 .ce-tree .leaf-b{animation:ceLeafBoost 2.5s ease-in-out infinite!important}
  html.ceMotionOn .ce-credit-widget.tier-1 .ce-tree .ce-trunk{animation:ceTrunkBounce 2.5s ease-in-out infinite!important}
  html.ceMotionOn .ce2-can{animation:ceCanFloat 2.2s ease-in-out infinite!important}
  html.ceMotionOn .ce-credit-widget.tier-2 .ce2-drop,html.ceMotionOn .ce-credit-widget.tier-1 .ce2-drop{animation:ce2Drop 1.5s ease-in infinite!important}
  html.ceMotionOn .ce-credit-widget.tier-1 .ce2-drop{animation-duration:.95s!important}
  html.ceMotionOn .ce-credit-widget.tier-1 .ce2-can::after{animation:ce2CanRing 1.05s ease-out infinite!important}
  html.ceMotionOn .ce2-cloud-1{animation:ce2Drift1 34s ease-in-out infinite alternate!important}
  html.ceMotionOn .ce2-cloud-2{animation:ce2Drift2 46s ease-in-out infinite alternate!important}
  html.ceMotionOn .ce2-cloud-3{animation:ce2Drift3 58s ease-in-out infinite alternate!important}
  html.ceMotionOn .ce-credit-widget.tier-4 .coin{animation:ceCoinShimmer 2.5s ease-in-out infinite!important}
  html.ceMotionOn .ce-credit-widget.tier-4 .ce-vault-glow{animation:ceVaultGlow 3s ease-in-out infinite!important}
}
/* ★②★ 系统【没开】reduce 而用户手动关 ⇒ 只关上面这一批环境装饰 ✓
   （本块刻意【不】用 * 通配 ✗ 逐条点名 ✓ 免得误伤解题动画 / 加载动画 / 入场动画 ✓✓）
   ★ 范围与 ★①★ 严格对齐 ✗（比它多 wind / fall 两条 ✓ 见下面说明 ✓）
   ★★ 四条【故意不关】✗ 说明在明处 ✓：
     · .ce2-bird-1 / .ce2-bird-2：系统 no-preference 时它本来就在飞 ✓
       用户关的是"界面动画"不是"删掉这只鸟"✗ 关掉会改变页面内容 ✗ 那是另一个功能 ✓
       而且 ce2Fly 是【大幅位移】✗ 只此一条不属于"环境装饰"✓（reduce 下它由
       themes.css:360 的 display:none 守着 ✓ tests/tree-scene-fx.js:259-263 钉住 ✓）
     · .ce2-bucket：入场一次性 + transform 在 SVG 属性上 ✓ 关它会露出 opacity:0 ✓ 不动它 ✓
   ★★ wind / fall 为什么【要】关 ✗✗（放进来比不放进来自洽 ✓）：
     themes.css:555 的设计就是"风和树同时发生"✓ 只停树不停风 ⇒
     人眼看到"树一动不动而风痕扫过"✗ 正是那条注释要防的"很假"✓✓
     （放宽口径不算破坏"默认行为"✗ 只有【手动关】的用户才看得到这个差异 ✓）*/
@media(prefers-reduced-motion:no-preference){
  html.ceMotionOff .ce-status-pill,html.ceMotionOff .ce2-tree,html.ceMotionOff .ce2-rays,
  html.ceMotionOff .ce2-sunglow,html.ceMotionOff .ce2-halo,html.ceMotionOff .ce2-halo-inner,
  html.ceMotionOff .ce2-sun-core,html.ceMotionOff .ce2-sun-core2,html.ceMotionOff .ce2-can,
  html.ceMotionOff .ce2-canwater,html.ceMotionOff .ce2-drop,html.ceMotionOff .ce2-cloud-1,
  html.ceMotionOff .ce2-cloud-2,html.ceMotionOff .ce2-cloud-3,html.ceMotionOff .ce2-can::after,
  html.ceMotionOff .ce2-wind path,html.ceMotionOff .ce2-fall ellipse,
  html.ceMotionOff .coin,html.ceMotionOff .ce-vault-glow{animation:none!important}
  html.ceMotionOff .ce2-tree .leaf,html.ceMotionOff .ce2-tree .ce-trunk,
  html.ceMotionOff .ce-tree .leaf,html.ceMotionOff .ce-tree .ce-trunk{animation:none!important}
}
/* ★ 让 emoji 星星也吃得到尺寸与发光 ✗（原来是 <img> ✓ 现在是文字 ✓）*/
.ce-guest-spark{font-size:15px;line-height:16px;text-align:center;color:#f3b74a;
  text-shadow:0 0 6px rgba(243,183,74,.55)}
.ce-guest-spark.s3{font-size:13px}

/* ==========================================================================
   等级主题色落地点 (v8.13 → v8.14 起【只剩 3 处】) —— 原 4 处里的 #planPill 已改为按【套餐】分档 ✓ 见 ④
   ========================================================================== */
/* ① 会员卡头像外圈光环
   ★ 选择器写成 body[data-lifetime] .ce-member-avatar 而不是 .ce-member-avatar ✗
     原因：本表已有 .ce-member-avatar.life-7{box-shadow:...}（(0,2,0) 特异性 ✓ 定义在前面）
     单写 .ce-member-avatar 是 (0,1,0) ⇒ 会被 life-7 盖掉 ✗ Lv7 反而看不到等级光环 ✗
     （渲染实测抓到的第二个真问题 ✓）用 (0,2,1) 同时盖过 life-2..life-7 的既有阴影 ✓
     ★ 这里【没有】加 !important ✗ 否则会盖掉 aurora 暗色主题的覆盖规则 ✗ */
body[data-lifetime] .ce-member-avatar{box-shadow:0 0 0 3px var(--lv-accent-ring, rgba(18,179,160,.30)),0 8px 19px rgba(63,128,58,.10)}
/* ② 会员卡右上角方案标签（等级色小元素） */
.ce-member-plan{background:var(--lv-accent, #12b3a0);border:1px solid var(--lv-accent, #12b3a0);color:#fff}
/* ③ 会员卡里的等级名 */
.ce-member-level{color:var(--lv-accent, #12b3a0)}
/* ★★★ 第 47 轮：按《00-视觉规格.md》§1/§2 重做 Credits 卡方案 pill —— 用户原话「没有按照它这个图示来 / 字体要清晰圆润 / 可以稍微大一点 / 字体是白色字体」✓
   ① 字号 11px → 13px（规格 §1 写 11px ✗ 用户要"稍微大一点"⇒ 以用户口述为准 ✓ 13px 不享受 WCAG 大字豁免 ✗ 18.66px 粗体才豁免 ⇒ 仍按 4.5:1 验收 ✓）
   ② 字重按规格：免费档 700 ✗ 付费档 800 ✓ 字体保持 --ce-font-body 继承的 Inter（自托管 800 是真字面 ✗ 不是伪粗体 ✓）
   ③ Family 按规格补 padding:4px 13px 4px 11px + letter-spacing:.3px ✓ 四档 padding/border-radius 统一 4px 12px / 99px ✓
   ④ 白字：规格给 Student/Plus 白字实测【不达标】(2.14~2.77 / 1.86~3.74) ✗ 用户仍要白字 ⇒ 经确认选(c)【保留色相 ✗ 只把渐变压深】✗ 逐停靠点用 HSL 保 H/S 降 L 求最小压深 ✓
      student #38bdf8→#067bb0 (4.70:1) · #0ea5e9→#096a96 (5.98:1)   plus #2dd4bf→#18766b (5.47:1) · #0d9488→#09655d (6.93:1)
      family 原样 10.36:1 / 14.69:1 ✓ ⇒ 四档最浅停靠点亮度 0.866(米白) → 0.173(学生) → 0.142(Plus) → 0.051(VIP) ⇒ 层次感保留 ✗ 没有压成一个色 ✓
   ⑤ Free 底色：规格写 var(--card-soft) ✗ 本站该 token 日间主题是 #EAF3FB【浅蓝】✗ 与 Student 撞色 ✗ 且不是用户说的"中性软底" ⇒ 经确认取规格注释里的数值 #F3EFE7 米白（固定 ✗ 四个主题一致 ✓）
      Free 字色：规格 #12b3a0 在米白上只有 2.29:1 ✗（白字更只有 1.15:1 ✗✗ 绝不能白）⇒ 经确认取同色系压深 #0f766e = 4.77:1 ✓
   ⑥ direction:ltr ✗ 用户要求「RTL 下皇冠在左边」✓ 纯逻辑属性在 RTL 会把首个子元素镜像到【右】边 ✗ 与本要求相反 ⇒ 胶囊内部显式 LTR（内容永远是拉丁套餐名 ✓）✗ 间距仍用 margin-inline-end 逻辑属性 ✓
   ⑦ 皇冠换规格原图 01-皇冠/crown.svg（64×52 ✗ 5 尖 + 5 圆珠 + 3 宝石 + 2 条渐变 ✓ 透明矢量 ✓）✗ 见 app.js calcelfPlanPill ✓ 尺寸 1.2em × .975em 严格守 64:52 比例 ✗ 不拉伸 ✓ */
/* ④ Credits 卡上的方案 pill ★★ v8.14 起【按套餐分档】✗ 不再跟着终身等级（--lv-accent）走 ✓ —— 用户反馈"free / student / family 三个套餐看到同一个颜色"的根因就是它跟着等级走 ✓ 四档色值取自用户给的《00-视觉规格.md》§2；四档 = 规格的 4 个"层数"，逐级递进：free 1 层（只有底）→ student 2 层（+投影）→ plus 3 层（+1px 金环）→ family 4 层（+1.5px 金环 + 皇冠）✓ ★ 文字色为什么不是规格里的白：规格给的白字实测【不达标】✗ student 2.14:1 / plus 1.86:1（要求 ≥4.5:1 ✓）⇒ 按"不达标就换深色字"改用深色 ✗ 四档实测 free 4.83:1 / student 5.19:1 / plus 4.84:1 / family 10.36:1 ✓ ★ 金环为什么用 box-shadow 外扩而不是 border：border 会把胶囊撑大 2~3px ✗ 版式就动了 ✓ box-shadow 不占布局 ✓ ★ 形状 / padding / 字号 / 字重 / 圆角【一个都没动】✗ 只换了颜色 + 环 + 投影 ✓（family 那颗皇冠会让胶囊变宽 ✓ 实测值见汇报 ✓） */
#planPill{background-image:none;background-color:#F3EFE7;color:#0f766e;font-weight:700;font-size:13px;padding:4px 12px;border-radius:99px;letter-spacing:normal;direction:ltr;box-shadow:none/* ★ v8.14 四档按套餐分档（规格 §2 的 4 个"层数"逐级递进 ✓）★ free 最低档＝底色 #F1F3F6（低调中性浅灰 ✓ 不抢 student 的蓝 ✓）+ 金族深字 #8a6410（= 既有 token --ce-reward-ink ✓ 4.83:1 ✓）★ 规格原写"米白 var(--card-soft) + 青绿 #12b3a0 字"⇒ 本站 --card-soft 是 #EAF3FB（蓝 ✗ 不是米白 ✗）且 #12b3a0 当文字只有 2.37:1 ✗ 两处都与站点冲突 ⇒ 已在汇报里【报告】✓ 此处按用户最新口述（黄色系 / 加底 / 最低调）落地 ✓ */}body[data-plan="student"] #planPill{background-image:linear-gradient(135deg,#067bb0,#096a96);background-color:#096a96;color:#fff;font-weight:800;box-shadow:0 2px 6px rgba(14,165,233,.30)}body[data-plan="plus"] #planPill{background-image:linear-gradient(135deg,#18766b,#09655d);background-color:#09655d;color:#fff;font-weight:800;box-shadow:0 0 0 1px rgba(232,179,60,.55),0 2px 7px rgba(13,148,136,.40)}body[data-plan="family"] #planPill{background-image:linear-gradient(135deg,#1e3a8a,#172554);background-color:#172554;color:#fff;font-weight:800;padding:4px 13px 4px 11px;letter-spacing:.3px;box-shadow:0 0 0 1.5px #e8b33c,0 3px 9px rgba(30,58,138,.45)}#planPill>svg.ce-svg{width:1.2em;height:.975em;margin-inline-end:3px;vertical-align:-.13em}
.ce-level-main strong{color:var(--lv-accent, #12b3a0)}

/* ==========================================================================
   顶部头像的皇冠 / 领巾 (v8.13)
   --------------------------------------------------------------------------
   ★ 为什么不再是 emoji：app.js 以前渲染 '👑' / '🧣'。emoji 字形是固定彩色位图，
     CSS 的 color / text-shadow 对它【完全无效】✗ 所以老的 .crown-bronze /
     .crown-silver / .crown-gold 与 .scarf-blue|green|purple|rainbow 四档
     在页面上是【一模一样】的 ✗（只有 font-size 生效 ✓）。现在 app.js 输出内联
     SVG，填充色由这里给 → 三档皇冠 / 四档领巾真的能看出来 ✓
   ★ 定位：老的 .avatar-crown/.avatar-scarf 规则在 themes-ceremony.css 里 ✗
     但 index.html【没有加载那个文件】✗ 所以皇冠/领巾此前是【完全没样式】的裸
     inline 内容 ✗（会挤在头像旁边 ✓）。这里补上真实定位 ✓
   ★ id 冲突：全程【没有 <defs>/<linearGradient>/id】✓ 彩虹档用多段实色填充 ✓
     （一个页面渲染多次也不会撞 id ✓）
   ★ 用 !important 的原因：ui-type-scale.css 在本表【之后】加载 ✗ 它有
     #avatarPicker .ap-* 之类的尺寸规则 ✓ 这里固定尺寸避免被别的规则改掉 ✓
   ========================================================================== */
/* ★ 这一步是必须的 ✗ 不是顺手美化 ✗（渲染实测抓到的第三个真问题 ✓）
   真因：#profileAvatarDisplay 是 index.html 里一个【裸 inline <span class="avatar-wrap">】✗
     · 旧 CSS 只在 themes-ceremony.css 里写过 .avatar-wrap{display:inline-block} ✗
       而那个文件【没有被 index.html 加载】✗ ⇒ 它一直是 inline ✓
     · inline 盒子里若【只有绝对定位子元素】✗ 浏览器会判定为碎片化盒 ✗
       ⇒ 绝对定位子元素的 left:50% / top:-11px 会去对【最近的定位祖先】解析 ✗
       而 .top 是 position:sticky ✓ ⇒ 皇冠/领巾被丢到【整页顶部左侧】✗
       实测坐标：皇冠 528,-14 ✗ 领巾 525,97 ✗（头像本体在 605,13 ✗）
   修法：给 .avatar-wrap 一个真正成形的盒 + 成为定位祖先 ✓
   实测修好后：皇冠 615,2 ✓ 领巾 615,46 ✓（相对头像中心左右偏差 0px ✓） */
.avatar-wrap{display:inline-block;position:relative;vertical-align:middle;margin-inline-end:0}
.avatar-crown,.avatar-scarf{position:absolute;left:50%;transform:translateX(-50%);z-index:2;line-height:0;pointer-events:none}
/* 位置实测说明（★ 这些数字是用无头 Chromium 量出来的 ✗ 不是拍脑袋 ✓）
   真实顶栏（stage=primary_1_3 ⇒ applyStage 给 #appZoomWrap 加 zoom:1.28 ✗
   所以内联 56px 的头像在屏幕上量出来是 72px ✓ 皇冠/领巾会跟着一起放大 ✓）：
     头像 72x72 @ y=13 ✗ 皇冠 24x13 @ y=2（贴着圆上方 ✓ 不遮脸 ✓）
     领巾 26x12 @ y=84（压在圆下沿 ✓ 顶出 5px ✓ 能看出是围在脖子上的 ✓） */
.avatar-crown{top:-11px}
.avatar-crown svg{display:block;width:24px!important;height:13px!important;filter:drop-shadow(0 1px 1px rgba(0,0,0,.22))}
.avatar-scarf{bottom:-3px}
.avatar-scarf svg{display:block;width:26px!important;height:12px!important;filter:drop-shadow(0 1px 1px rgba(0,0,0,.16))}
/* 兜底：万一 class 没匹配上（旧数据 / 未来新增档位）也给中性色，而不是黑块
   ★ 这两条必须放在【上面四档之前】✗ 否则同为 (0,2,0) 特异性 + 定义在后面 ⇒
     会把 .scarf-blue/.scarf-rainbow 等全部覆盖掉 ✗（我第一版写反了 ✓
     渲染实测抓到：scarf-blue 拿到的是兜底灰 ✗ 现已把兜底前置 ✓） */
.avatar-scarf .scarf-main{fill:#7f8c9b}
.avatar-scarf .scarf-tail{fill:#66727f}
.avatar-scarf .scarf-gloss{fill:rgba(255,255,255,.30)}
.avatar-crown .crown-body{fill:#cbb26a}
.avatar-crown .crown-band{fill:#a8912f}
.avatar-crown .crown-gem{fill:#fff4cf}
/* 皇冠三档：古铜 / 银 / 金（★ ml>=5 金、ml>=3 银、否则古铜 —— 判定仍是 app.js 原来的逻辑，未改） */
.crown-bronze .crown-body{fill:#b87333}
.crown-bronze .crown-band{fill:#8f5423}
.crown-bronze .crown-gem{fill:#f0c08a}
.crown-silver .crown-body{fill:#c2cad6}
.crown-silver .crown-band{fill:#9aa6b6}
.crown-silver .crown-gem{fill:#f4f8ff}
.crown-gold .crown-body{fill:#e8b02c}
.crown-gold .crown-band{fill:#c68a12}
.crown-gold .crown-gem{fill:#fff0b8}
.crown-bronze .crown-body,.crown-silver .crown-body,.crown-gold .crown-body,
.crown-bronze .crown-band,.crown-silver .crown-band,.crown-gold .crown-band{stroke:rgba(0,0,0,.16);stroke-width:.45}
/* 领巾四档：蓝 / 绿 / 紫 / 彩虹（★ ll>=7 彩虹 ✓ ll>=5 紫 ✓ ll>=3 绿 ✓ 否则蓝 —— 判定同样未改） */
.scarf-blue .scarf-main{fill:#5aa9e6}
.scarf-blue .scarf-tail{fill:#3f8ec9}
.scarf-green .scarf-main{fill:#4aa86d}
.scarf-green .scarf-tail{fill:#368c56}
.scarf-purple .scarf-main{fill:#9b6be0}
.scarf-purple .scarf-tail{fill:#7f4fc4}
/* 彩虹档：★ 不用 SVG 渐变（会需要 id ✗ 页面渲染多次即冲突 ✗）
   改成【每个 path 一段实色】✓ 合起来是红→蓝→紫的彩虹感 ✓ */
.scarf-rainbow .scarf-main{fill:#e0574f}
.scarf-rainbow .scarf-tail{fill:#3ea8e0}
.scarf-rainbow .scarf-tail+.scarf-tail{fill:#7d5cd6}
.scarf-blue .scarf-gloss,.scarf-green .scarf-gloss,.scarf-purple .scarf-gloss,
.scarf-rainbow .scarf-gloss{fill:rgba(255,255,255,.34)}


/* ══════════════════════════════════════════════════════════════════
   ★★★ 第 35 轮 ①：充值包（Top-up packs）的 Buy 按钮 —— 鲜艳化 ✓✓
   用户要求："把充值包（Top-up packs）的 Buy 按钮做得鲜艳、醒目、协调"✓
   ──────────────────────────────────────────────────────────────────
   改前现状（真实浏览器里实测的计算值 ✓ 不是读代码猜的 ✓）：
     · 三个按钮 class 全是 "btn secondary"（index.html L811-813 ✓ 完全相同 ✓）
     · 计算值 background rgb(238,242,247)（浅灰）✗ color rgb(48,67,91)（深灰）✗
       ⇒ 真因 = 本文件 L326 那条兜底
         .modalbox .btn.secondary{background:#eef2f7!important;color:#30435b!important}
         （本意"弹窗里的次按钮统一压淡"✓ 却把充值包一起压灰了 ✗✗）
       ⇒ 已改为 :not([data-product^="pack"]) ✓ 只放行这三个 ✓
     · 三个按钮【长得一模一样】✗ 没有任何档位感 ✓ 这是"素"的第二主因 ✓
   ──────────────────────────────────────────────────────────────────
   改法（★ 每档一个颜色 ✗ 且【沿用站点已有的档位色】✓ 不另造 ✓）：
     档位色早就存在于 .pack-glow / .pack-stars（本文件 L622-634 ✓）：
       pack-1 = #10B8C4 青绿 ✓   pack-2 = #2563EB 蓝 ✓   pack-3 = #f59e0b 金 ✓
     ⇒ 按钮就按这三档递进：青绿 → 蓝 → 金 ✓
       （金是站点最被浪费的颜色 ✓ 这里终于落到按钮上 ✓ 且是最高档 ✓）
     渐变两端【全部是站点自己的颜色】✗ 一个新色都没造 ✓：
       pack-1  #33d9c3 → #12b3a0   站点青绿（亮端是它的提亮版 ✓）
       pack-2  #2563EB → var(--ce-primary-text,#0f766e)   档位蓝 → 站点深色端 ✓
               （★ 第 5 批：右端原为「站点深蓝 #123A72」✗ 已按角色清零 ✓）
       pack-3  #f7c948 → #e8b33c   亮金 → 站点金 ✓
   ──────────────────────────────────────────────────────────────────
   ★ 字色不是拍脑袋 ✗ 是按【实测 WCAG 对比度】选的 ✓（含渐变中点 ✓）：
     pack-1 青绿 + 深墨 #04302a → 8.11 / 6.70 / 5.46 ✓✓
        （白字只有 1.77 / 2.14 / 2.63 ✗ 连 3:1 都不到 ✗ 所以不用白字 ✓）
     pack-2 蓝   + 白 #ffffff → 5.17 / 7.54 / 11.21 ✓✓
        （深墨字只有 3.36 ✗ 不达标 ✗ 所以用白字 ✓）
     pack-3 金   + 深墨 #3a2a00 → 8.87 / 8.04 / 7.24 ✓✓
        （白字只有 1.57 / 1.92 ✗ 用户已预判"金底白字容易不达标"✓ 实测确认 ✓）
     ⇒ 三档【两端与中点全部 ≥ 4.5:1】✓✓（AA 正文级 ✓）
   ──────────────────────────────────────────────────────────────────
   ★ 为什么全程没有 !important ✗：
     L326 那条在本文件里 ✓ 我直接【收窄它的选择器】✓ 而不是拿 !important 对撞 ✓
   ★ ui-type-scale.css L37-43 的 !important（17px / padding 14px 22px /
     min-height 54px / 胶囊圆角 / 圆体 / 800 字重）【一律不动】✗
     ⇒ 那是站点弹窗按钮的统一规范 ✓ 所以这里只改【颜色/渐变/阴影/图标/字距】✓
   ★ 字体：写 var(--font-display, inherit) ✗ 不写死 'Fredoka' ✓
     （弹窗内实际生效的是 ui-type-scale.css 的 --calf-round-font ✓
       它同样以圆体 Fredoka 打头 ✓ 字体自托管到位后自动就是圆润可爱体 ✓）
   ══════════════════════════════════════════════════════════════════ */
.pack-card .btn[data-product^="pack"]{
  font-family:var(--font-display, inherit);
  font-weight:var(--ce-fw-head);
  letter-spacing:.3px;
  color:#fff;
  border:0;
  background-image:linear-gradient(135deg,#2563EB,var(--ce-primary-text,#0f766e));
  box-shadow:0 4px 12px rgba(37,99,235,.28),inset 0 1px 0 rgba(255,255,255,.30);
  transition:transform .16s ease,box-shadow .16s ease,filter .16s ease;
}
/* ★ 档位图标：纯 CSS/SVG 画的五角星 ✗ 不用 emoji ✗ 不引任何图标文件 ✓
     · mask-image + background:currentColor ✗ 星星【自动等于文字颜色】✓
       ⇒ 对比度与文字同值（上面那组实测数就是它的对比度 ✓）
     · 档位越高星越多：★ / ★★ / ★★★ ✓ 与卡片右上角 ✦ ✧ 是同一套意象 ✓
     · SVG 是内联 data URI ✗ 写在本文件里 ✓ 不新增任何文件 ✓ 不占请求 ✓ */
.pack-card .btn[data-product^="pack"]::before{
  content:"";flex:0 0 auto;width:13px;height:13px;
  background:currentColor;opacity:.92;
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-position:center;mask-position:center;
  -webkit-mask-size:contain;mask-size:contain;
  -webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath d='M5 0 6.18 3.38 9.76 3.46 6.9 5.62 7.94 9.05 5 7 2.06 9.05 3.1 5.62 .24 3.46 3.82 3.38Z'/%3E%3C/svg%3E");
  mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath d='M5 0 6.18 3.38 9.76 3.46 6.9 5.62 7.94 9.05 5 7 2.06 9.05 3.1 5.62 .24 3.46 3.82 3.38Z'/%3E%3C/svg%3E");
}
/* 100 Credits ✗ 青绿档（站点青绿 ✓ 档位色 #10B8C4 的青绿家族 ✓）★ */
.pack-1 .btn[data-product^="pack"]{
  background-image:linear-gradient(135deg,#33d9c3,#12b3a0);
  color:#04302a;                                  /* 实测 8.11 / 6.70 / 5.46 ✓ */
  box-shadow:0 4px 12px rgba(18,179,160,.34),inset 0 1px 0 rgba(255,255,255,.50);
}
/* 250 Credits ✗ 蓝档（档位色 #2563EB → 站点深色端 ✓）★★
   ★★ 第 5 批（整站换色）：右端「站点深蓝 #123A72」→ var(--ce-primary-text,#0f766e) ✗
      理由（★ 按已写进规范的判据 ✗ 看角色不看色值）：
        它是【按钮底色渐变】的右端 ⇒ 角色 = 底 ✗ 必须清零旧主色 ✓
      取值为何是 --ce-primary-text（文字档）而不是品牌青绿 --ce-primary：
        这上面压的是【白字】✗ 白字压 #12b3a0 只有 2.63:1 ✗ 不达 AA ✓
        #0f766e 白字 5.47:1 ✓（--ce-primary-text 的取值依据见 design-tokens.css）
      ★ 对比度影响：白字压渐变的最不利点是【更亮】的左端 #2563EB（lum .1532）
        右端旧 #123A72（lum .0437）→ 新 #0f766e（lum .1418）✗ 仍然更暗 ✓
        ⇒ 最差对比度【不变 = 5.17】✓ 良端从 11.21 收窄到 5.47 ✗ 但 5.47 > 5.17 ✓
        ⇒ 下面那行注释的下限【依然成立】✗ 未改 ✓ */
.pack-2 .btn[data-product^="pack"]{
  background-image:linear-gradient(135deg,#2563EB,var(--ce-primary-text,#0f766e));
  color:#fff;                                     /* 实测 5.17 / 7.54 / 11.21 ✓（第 5 批后下限不变 ✗ 良端 ≥5.47 ✓） */
  box-shadow:0 4px 12px rgba(37,99,235,.32),inset 0 1px 0 rgba(255,255,255,.30);
}
.pack-2 .btn[data-product^="pack"]::before{width:24px}
/* 600 Credits ✗ 金档（站点金 #e8b33c ✓ 最高档 ✓）★★★ */
.pack-3 .btn[data-product^="pack"]{
  background-image:linear-gradient(135deg,#f7c948,#e8b33c);
  color:#3a2a00;                                  /* 实测 8.87 / 8.04 / 7.24 ✓ */
  box-shadow:0 4px 12px rgba(201,150,32,.36),inset 0 1px 0 rgba(255,255,255,.55);
}
.pack-3 .btn[data-product^="pack"]::before{width:35px}
/* ★ 悬停：整体提亮 + 上浮 1px ✓ 按下：下沉 1px ✓（用户指定 ✓）*/
.pack-card .btn[data-product^="pack"]:hover{
  transform:translateY(-1px);
  filter:brightness(1.07);
  box-shadow:0 7px 18px rgba(21,37,64,.22),inset 0 1px 0 rgba(255,255,255,.55);
}
.pack-card .btn[data-product^="pack"]:active{
  transform:translateY(1px);
  filter:brightness(.97);
  box-shadow:0 2px 6px rgba(21,37,64,.20),inset 0 1px 0 rgba(255,255,255,.40);
}
/* ★ 卡片本身只做【克制】的档位提亮 ✗ 版式一律不动 ✓
   用户只要求按钮鲜艳 ✓ 所以这里只给一圈档位色的细边 ✓ 不动内边距/字号/结构 ✓ */
.pack-card{border-color:#dbe7f5}
.pack-1{border-color:#9fe3d8}
.pack-2{border-color:#b6cff9}
.pack-3{border-color:#f2d9a2}


/* ══════════════════════════════════════════════════════════════════
   ★★★ 第 35 轮 ②：所有"可滚动弹窗"的【固定右上角关闭叉号】✗✗
   用户原话："我们这种有弹框的并且有滚轮的这些地方 ✗
             你最好在它的右上角都设置一个叉号 ✗ 然后那个叉号【不要动】✓
             这样子用户可以随时地去关闭这个弹窗"✓
   ──────────────────────────────────────────────────────────────────
   真因（我在真实浏览器里量过 ✓ 不是猜的 ✗）：
     · .modalbox 自己就是滚动容器（max-height:90vh + overflow:auto ✓ L186 ✓）
     · 而 .close 是 position:absolute ✗ 绝对定位在【滚动容器内部】时
       ⇒ 它跟着内容一起滚走 ✗✗
     实测（改前 ✓ 计划弹窗 ✓）：叉号 rect.top 从 26 → -720 ✓
       ⇒ 滚到底时它已经在屏幕【上方 720px 之外】✗ 根本点不到 ✓✓
   ──────────────────────────────────────────────────────────────────
   做法（★ 纯 CSS ✗ 一个 HTML 字符都没改 ✓）：
     ① position:sticky ✗ 让按钮停在【滚动容器的可见顶部】✓ 不随内容走 ✓
     ② inset-block-start 必须【等于容器的 padding-top】✗✗ 这是"像素级不动"的关键：
          容器 padding-top = 26px（ui-type-scale.css L12 ✓ 窄屏 20px ✓）
          写 26px ⇒ 桌面正好等于静态位置（不动 ✓）
                    窄屏 26 > 20 ⇒ 一进来就处于吸附态（也不动 ✓）
          若阈值小于 padding-top ✗ 按钮会先跟着内容走一段再吸住 ✗ 用户要的是不要动 ✓
     ③ margin-inline-start:auto ✗ 把按钮推到【行尾】✓
          全部用逻辑属性 ✗ 阿语/波斯语 RTL 下自动镜像到【左上】✓✓
     ④ margin-block-end:calc(-1 * var(--ce-close-h)) ✗ 抵消它在文档流里占的那一行 ✓
          ⇒ 版式与改前【完全一致】✗ 标题不会被挤下去 ✓
     ⑤ z-index:6 ✗✗（改前没有 z-index ✓）
          .plan / .pack-card 的子孙都是 position:relative ✓
          ⇒ 同级层里【后出现的盖住先出现的】✗ 滚动时卡片会压住叉号 ✗
          ⇒ 给它正 z-index ✗ 保证 elementFromPoint 命中的是叉号自己 ✓✓
     ⑥ 半透明白底 + 圆形 + 细阴影 ✗ 压在标题/正文上也看得清 ✓
          （改前是 background:none ✗ 压在字上就是一团灰 ✓）
     ⑦ 尺寸 38×38 ✓ ≥32px 触控目标 ✓（沿用 ui-type-scale.css L16 的 38px ✓ 不改大小 ✓）
   ──────────────────────────────────────────────────────────────────
   ★ 覆盖范围 = 全站【所有会滚动的弹窗】✗ 逐个确认过 ✓（清单见汇报 ✓）：
     .modalbox 六件套：#authModal #plansModal #rewardsModal #inviteModal
                        #verifyModal #feedbackModal ✓
     JS 动态弹窗：      #themePanel（theme-app.js ✓ 也是 .modalbox ✓）
                        （#purchaseModal 已随 ui-enhance.js 的死代码一并删除 ✗ 不再是弹窗 ✓）
                        #gcGrowthModal / #gcInviteModal / #gcStarsModal
                        （growth-cards.js ✓ 同样是 .modalbox.gc-box ✓）
                        #settingsPanel .sp-box（settings-panel.js ✓）
                        #avatarPicker .ap-box（avatar-picker.js ✓）
                        #levelsModal .lv-box（levels-info.js ✓ 现已退役不再弹 ✓ 仍一并覆盖 ✓）
   ★ 做不到的那一个（如实报告 ✗ 没有偷偷改别人的文件 ✓）：
     #privacyModal 的叉号（#privacyCloseBtn ✗ 它没有 .close 类 ✗
     位置写死在 index.html 的内联 style 里 ✓ right:16px;top:16px;z-index:10 ✓）
       ⇒ 但它【本来就满足用户的要求】✓：那个 .modalbox 是 overflow:clip ✗
         整个弹窗【根本不能滚动】✓ 真正的滚动条在内部的 #privacyScrollArea 上 ✓
         而叉号在【滚动区之外】✓ 所以它天然就不会动 ✓（实测 top 完全不变 ✓）
       ⇒ 唯一没做到的是"RTL 下镜像到左上"✗ 那需要盖掉内联样式（只能靠 !important ✗
         或改 index.html ✗ 两者我都不做 ✓ 如实报告 ✓）
   ★ 没有用 !important ✗：
     全部靠【同选择器 + 本文件后加载】或【加一层父级提高特异性】✓
     （gc 三件套的 30px/12px 内缩写在 growth-cards.css ✓ 比本文件后加载 ✓
       ⇒ 那里用 #id 提高特异性 ✓ 依然不碰 !important ✓）
   ══════════════════════════════════════════════════════════════════ */
.modalbox > .close,
#settingsPanel .sp-box > .close,
#avatarPicker .ap-box > .close,
#levelsModal .lv-box > .close,
#gcGrowthModal .modalbox > .close,
#gcInviteModal .modalbox > .close,
#gcStarsModal .modalbox > .close{
  --ce-close-h:38px;
  position:sticky;
  inset-block-start:26px;
  margin-inline-start:auto;
  margin-block-end:calc(-1 * var(--ce-close-h));
  box-sizing:border-box;
  width:38px;height:38px;min-height:0;
  padding:0;
  display:grid;place-items:center;
  font-size:24px;line-height:1;
  color:#43536b;
  background:rgba(255,255,255,.94);
  border:1px solid rgba(219,229,241,.95);
  border-radius:50%;
  box-shadow:0 2px 8px rgba(21,37,64,.16);
  z-index:6;
  transition:background .18s,color .18s,box-shadow .18s;
}
.modalbox > .close:hover,
#settingsPanel .sp-box > .close:hover,
#avatarPicker .ap-box > .close:hover,
#levelsModal .lv-box > .close:hover,
#gcGrowthModal .modalbox > .close:hover,
#gcInviteModal .modalbox > .close:hover,
#gcStarsModal .modalbox > .close:hover{
  background:#fff;
  color:#1b2a44;
  box-shadow:0 4px 12px rgba(21,37,64,.24);
}
/* ★ 键盘可达性 ✗：只靠颜色区分是不够的 ✓ 聚焦必须看得见 ✓
   （不删任何 aria-label ✗ 只加一个可见的焦点环 ✓）*/
.modalbox > .close:focus-visible,
#settingsPanel .sp-box > .close:focus-visible,
#avatarPicker .ap-box > .close:focus-visible,
#levelsModal .lv-box > .close:focus-visible,
#gcGrowthModal .modalbox > .close:focus-visible,
#gcInviteModal .modalbox > .close:focus-visible,
#gcStarsModal .modalbox > .close:focus-visible{
  outline:3px solid rgba(62,168,224,.85);
  outline-offset:2px;
}
/* ★ gc 三件套（成长中心/邀请有礼/精灵星）：
   它们的叉号压在【青绿渐变头部】上 ✗ 原来是「半透明白底 + 白色字形」✓
   ★ 但那套配色【只在头部是青绿时成立】✗✗ —— 实测（滚到底的截图 ✓）：
     头部一滚走 ✗ 叉号就浮在【白底正文】上 ✓
     = 白字形 + 22% 白底 + 白页面 ⇒ 【完全看不见】✗✗
     用户的原话是"这样子用户可以随时地去关闭这个弹窗"✓
     ⇒ 一个"看不见"的叉号等于没做 ✓✓
   ⇒ 修法：改成与其它弹窗【同一套】✗ 白底 + 深色字形 + 细阴影 ✓
     在青绿头部上是「白色圆钮 + 深色 ×」✓ 在白色正文上同样清楚 ✓✓
   ★ 尺寸 30px → 34px ✗：用户要求触控目标 ≥32px ✓ 30px 不达标 ✓ 34px 达标 ✓
     （用 #id 提高特异性 ✗ 不用 !important ✓）
   ★ --ce-close-h 必须跟着改 ✗ 否则负外边距会多抵消 4px ✓ 把头部顶上去 ✓ */
#gcGrowthModal .modalbox > .close,
#gcInviteModal .modalbox > .close,
#gcStarsModal .modalbox > .close{
  --ce-close-h:34px;
  width:34px;height:34px;
  inset-block-start:12px;
  margin-inline-end:12px;
  background:rgba(255,255,255,.94);
  color:#3d4f68;
  border:1px solid rgba(255,255,255,.6);
  box-shadow:0 2px 8px rgba(21,37,64,.20);
  font-size:19px;
  z-index:6;
}
#gcGrowthModal .modalbox > .close:hover,
#gcInviteModal .modalbox > .close:hover,
#gcStarsModal .modalbox > .close:hover{
  background:#fff;
  color:#1b2a44;
  box-shadow:0 4px 12px rgba(21,37,64,.26);
}
/* ★ 移动端：小屏叉号不能被挤出去 ✗ 触控目标仍 ≥32px ✓（38px 不变 ✓）
   ★ 阈值必须跟着【容器的 padding-top】走 ✗✗：
       .modalbox 窄屏 padding-top = 20px（ui-type-scale.css L105 ✓）⇒ 20px ✓
       .sp-box/.ap-box/.lv-box 的 padding【不随断点变】（24/22/24px ✓）
       ⇒ 它们必须留在 26px ✗ 若也写成 20px ✗ 会小于 padding ⇒
         滚动开始时先跟着内容动 4~6px ✗ 就不满足"不要动"了 ✓✓ */
@media(max-width:560px){
  .modalbox > .close{inset-block-start:20px}
}
/* ★ #privacyModal 的叉号（#privacyCloseBtn）✗ 它的位置写在 index.html 的内联 style 里 ✓
   （position:absolute; right:16px; top:16px; background:none; color:#888; z-index:10 ✓）
   ⇒ 那几项【不用 !important 盖不掉】✗ 我也不去盖 ✓
   ★ 但 width / height / padding / display / border-radius / box-shadow
     内联【没有声明】✗ ⇒ 普通规则就能赢 ✓✓
   ⇒ 于是可以做两件实事 ✗：
     ① 触控目标 11×38 → 38×38 ✓（实测改前只有 11px 宽 ✗ 远低于 32px 指南 ✓）
     ② 加一个圆底阴影 ✗ 压在标题上也看得清 ✓
   ★ 它的定位与"不会随滚动移动"本来就没问题 ✓（容器 overflow:clip ✗ 根本不能滚 ✓
     且它在 #privacyScrollArea【之外】✓ 实测滚动前后 top 完全一致 ✓）*/
#privacyCloseBtn{
  width:38px;height:38px;min-width:38px;
  padding:0;
  display:grid;place-items:center;
  border-radius:50%;
  box-shadow:0 2px 8px rgba(21,37,64,.18);
  line-height:1;
}





/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 第 42 轮：没收到确认信的【补救 UX】✗✗（verify modal 的重发行 + 未确认黄条 ✓）
   ──────────────────────────────────────────────────────────────────────────────
   ★ 为什么【只追加在文件末尾】✗：ui-spec-drift 记的 components.css 行号最大 1230 ✗
     （本文件原有 1323 行 ✓）⇒ 末尾追加【零漂移】✓✓
   ★★ 红线 ✗：一个 !important 都不用 ✓ 全部新类 ✓ 不覆盖任何既有规则 ✓
   ★ 黄条的 .on 开关 ✗：不用 hidden 属性 ✗（UA 的 [hidden]{display:none} 会被
     类选择器的 display 盖掉 ✓ 那是经典坑 ✓）改成 display:none → .on{display:flex} ✓
   ★ RTL ✗：关闭按钮用 inset-inline-end ✓ 自动跟随 dir ✓ 阿语/波斯语不会跑错边 ✓
   ══════════════════════════════════════════════════════════════════════════════ */
.vm-resend{margin-top:14px;padding-top:13px;border-top:1px solid #e6edf5;text-align:center}
.vm-hint{margin:0 0 10px;font-size:13.5px;line-height:1.5;color:#5b6b7f}
.vm-resend-btn{width:100%}
.vm-msg:empty,.vm-cd:empty{display:none}
.vm-msg{margin:8px 0 0;font-size:13px;line-height:1.45;color:#3d4d5f;word-break:break-word;text-align:start}
.vm-cd{margin:6px 0 0;font-size:12.5px;line-height:1.4;color:#8a6d3b;text-align:start}
.ce-verify-banner{display:none;position:relative;gap:10px;align-items:flex-start;margin:0 0 12px;padding:12px 40px 12px 14px;border:1px solid #f2d38b;border-radius:14px;background:#fff8e6;color:#6b4e12}
.ce-verify-banner.on{display:flex}
.ce-vb-ic{font-size:20px;line-height:1.2;flex:0 0 auto}
.ce-vb-body{flex:1;min-width:0}
.ce-vb-body b{display:block;font-size:14.5px;font-weight:700;margin-bottom:4px}
.ce-vb-body p{margin:0;font-size:13px;line-height:1.5}
.ce-vb-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:10px}
.ce-vb-btn{padding:8px 14px;font-size:13.5px}
.ce-vb-msg:empty,.ce-vb-cd:empty{display:none}
.ce-vb-msg{font-size:12.5px;line-height:1.45;color:#6b4e12;word-break:break-word}
.ce-vb-cd{font-size:12.5px;line-height:1.45;color:#8a6d3b}
.ce-vb-close{position:absolute;top:6px;inset-inline-end:8px;border:0;background:none;font:inherit;font-size:18px;line-height:1;color:#9a7c33;cursor:pointer;padding:2px 6px;border-radius:8px}
@media (max-width:560px){.ce-verify-banner{padding:11px 36px 11px 12px}.ce-vb-actions{gap:8px}.ce-vb-btn{width:100%}}

/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 第 43 轮：注册 / 登录弹窗（#authModal）单独加宽 15% ✗ 560px → 644px ✓✓
   ──────────────────────────────────────────────────────────────────────────────
   用户原话："这个框稍微再增加个 15% 的宽度。"✓（截图是 Start Adventure 注册页 ✓）
   ★ 只改 #authModal ✗ 不动 .modalbox 基类（components.css:193 / themes.css:105）✓：
       .modalbox 是【全站通用】容器 ✗ 直接改基类会连带加宽
       rewardsModal / inviteModal / verifyModal / feedbackModal ✗✗
       （计费 / 成长 / 邀请在改动红线禁改清单里 ✓）⇒ 用 #id 提特异性 ✓
   ★ 不加 !important ✓（本仓库有通配 !important 的误伤教训 ui-type-scale.css:124 ✗
      components.css:1085-1086 也明文约定不许 ✗）⇒ #authModal .modalbox 特异性
      (1,1,0) 已压过所有 .modalbox 基类规则 (0,1,0) ✓ 与样式表加载顺序无关 ✓✓
   ★ 保留 min(...,96vw) 的写法 ✗ 只改 px 那一个上限 ✓：
       ≤671px 视口下 96vw < 644px ⇒ 仍走 vw 那支 ⇒ 【移动端逐像素不变】✓✓
       （390px 实测：改前 362px ✓ 改后 362px ✓ = 96vw 374.4 被 .modal 的 padding:14px 收缩所得 ✓）
   ★ 不改内边距 / 圆角 / 字号 ✓ 只改宽度 ✓ ⇒ 无重排风险 ✓
   ★ 为什么【只追加在文件末尾】✗：ui-spec-drift 记的 components.css 行号全部在 1230 行以内 ✗
     （第 42 轮同款理由 ✓）⇒ 末尾追加【零行号漂移】✓✓
   ★ 改前 / 改后实测（桌面 1400 / 移动 390 ✗ 真 Chrome ✗ CDP 量 getBoundingClientRect ✓）
     见 docs/UI-SPEC-REAL.md §4.2 ✓
   ══════════════════════════════════════════════════════════════════════════════ */
#authModal .modalbox{width:min(644px,96vw)}

/* ══════════════════════════════════════════════════════════════════════════════
   v8.9 ★★★ ① 上传区：两块浅蓝之间加一条【通栏白条】✗
   ──────────────────────────────────────────────────────────────────────────────
   ★ 实测现状（真 Chrome ✗ CDP 量 getBoundingClientRect ✓ 桌面 1400 + 移动 390 都量过 ✓）：
     · 上面那块 = #upload（index.html:182 的虚线框 "Tap to take a photo …"）
       components.css:124 ⇒ background:var(--card-soft)=rgb(234,243,251) · padding 28px 16px · radius 20px
     · 下面那块 = #file（index.html:183 的原生 file 控件 "Choose File"）
       components.css:101 ⇒ background:var(--card-soft)=rgb(234,243,251) · padding 10px · radius 14px
     · 两块之间 gap = 0 ⇒ 视觉上糊成一整条浅蓝 ✗
   ★ 改法 ✗：只给 #file 加 margin-top ✗ 白条直接来自 .card 的白底 ✓ 不需要新元素 ✓
     ⇒ 通栏 12px 白条 ✓（页面 #appZoomWrap 有 0.8475 缩放 ⇒ 截图里量到 ≈10.2px ✓）
   ★ 为什么是 12px ✗：页面既有的间距节奏就是 12px
     （index.html:196 动画行 / 200 动画提示 / 209 隐私行 / 214 开始解题按钮 全部 margin-top:12px ✓）
     正落在用户给的 8~14px 区间 ✓
   ★ 不动字号 / 圆角 / 边框 / 颜色 ✗ 不加 !important ✓（components.css:1085-1086 有明文约定 ✓）
   ★ 为什么【只追加在文件末尾】✗：ui-spec-drift 记的 components.css 行号全部在 1230 行以内 ✗
     ⇒ 末尾追加【零行号漂移】✓✓
   ══════════════════════════════════════════════════════════════════════════════ */
#file{margin-top:12px}

/* ★★★ ② 死 UI 修复的焦点环 ✗✗（#upload 现在是 role="button" + tabindex="0" ✓ 见 app.js ⑥-1）
   ★ 键盘用户 Tab 到它必须【看得见焦点】✓
   ★ 用 color-mix 而【不照抄】components.css:1257 的 rgba(62,168,224,.85) ✗
     ⇒ 免得 ui-spec-drift 那条 contains 同时命中两行 ✓ */
.upload:focus-visible{outline:3px solid color-mix(in srgb,var(--primary) 70%,transparent);outline-offset:2px}

/* ★★★ ③ 批量题目的提示条 ✗（由 app.js 插入在 .row 之后 ✗ 通栏 ✓ 图片/文字两种模式共用 ✓） */
.ce-batch-notice{display:none;margin-top:12px;padding:10px 14px;border-radius:14px;background:var(--card-soft);border:1.5px solid var(--line);color:var(--primary);font-size:13px;font-weight:700;line-height:1.6;text-align:start}
.ce-batch-notice.show{display:block}

/* ★★★ ④ 逐题确认弹窗 ✗（复用既有 .modal / .modalbox / .btn / .pill ✗ 观感与全站一致 ✓）
   ★ 它是 #ceBatchModal ✗ 由 app.js 用 JS 建 ⇒ index.html【零改动】✓✓ */
.ce-batch-box{width:min(460px,94vw)}
.ce-batch-box h2{margin:0 0 12px;font-family:var(--display);font-size:19px;text-align:center;line-height:1.4}
.ce-batch-meta{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-bottom:14px}
.ce-batch-meta .pill{font-size:12px}
.ce-batch-credit{background:color-mix(in srgb,var(--accent) 18%,var(--card-soft));color:var(--ink)}
.ce-batch-preview{margin-bottom:16px}
.ce-batch-label{font-size:12px;font-weight:700;color:var(--muted);margin-bottom:6px;text-align:center}
.ce-batch-thumb{display:block;max-width:100%;max-height:130px;margin:0 auto;border-radius:12px;border:1.5px solid var(--line)}
.ce-batch-text{background:var(--card-soft);border:1.5px solid var(--line);border-radius:12px;padding:10px 12px;font-size:13px;line-height:1.7;max-height:120px;overflow:auto;white-space:pre-wrap;color:var(--ink);text-align:start}
.ce-batch-actions{display:grid;gap:8px}
.ce-batch-actions .btn{width:100%}
.ce-batch-actions .btn.ce-batch-stop{color:var(--muted)}

/* ══════════════════════════════════════════════════════════════════════════════
   第 45 轮：套餐弹窗（#plansModal）标题行改成【小罐子 + 标题 / 副标题】✗
   ──────────────────────────────────────────────────────────────────────────────
   用户原话："充值页面里面这个小瓶罐位置的这个地方给它删除掉 ✓
             然后把小瓶罐这个图标放到 credits and plans 的前面 ✓
             这样子相当于就说我们这个瓶子里已经空了 ✓ 要进行充值了 ✓"
   ★★ 删掉的是什么 ✗：标题与副标题下面那条 #plansCreditLow 横幅 ✓（原 index.html 里的 6 行 ✓
      样式原来在 ce-illu.css:212 到 230 ✓）实测（真 Chrome / CDP / 桌面 1400）：
        x=205 y=141.4 w=990 h=122 ✓ background:#fff8ed ✓ border:1px solid #fbd9a5 ✓
        padding:12px 15px ✓ border-radius:16px ✓ 里面只有 96px 罐子图 + t('creditLow') 一句话 ✓
      ⇒ 一条几乎整宽的空盒子 ✓ 正是用户截图里说"很空"的那个 ✓
   ★★ 罐子去哪了 ✗：搬到 <h2 id="plansTitle"> 之前 ✓ 同一张图 /brand/illu/credit-low.webp ✓
   ★ 布局 ✗：grid 两列 = [罐子 auto] [标题 + 副标题 1fr] ✓ 罐子 grid-row:1 / span 2 + align-items:center
     ⇒ 相对"标题 + 副标题"这一整块【垂直居中】✓（不是只对标题那一行 ✓ 只对标题会让副标题从罐子下面
       顶出来 ✗ 左边缘参差 ✓ 实测两种都看过 ✓ 选了跨两行居中 ✓）
   ★★ 为什么用 grid 而不是 flex ✗✗：h2 与 p 是【两个并列的子元素】✓
     flex 会把 p 也挤到罐子右边、和 h2 同一行或下一行错位 ✗
     grid 让它们自动落到第 2 列的第 1 / 第 2 行 ✓ 而且【不需要再加包裹 div】⇒ index.html 行数不漂 ✓✓
   ★★ RTL ✗：grid 的列序跟随 direction ✓ ⇒ ar / fa 下第 1 列落在【最右】⇒ 罐子自动跑到标题右边 ✓
     不写 row-reverse ✗ 不写 left / right ✗（物理方向会在 RTL 下写死 ✗）
     实测 ar / fa 桌面 1400：罐子 x=1131 起 ✓ 在标题文本右侧 ✓ 且弹窗关闭按钮用 inset-inline-end
     ⇒ RTL 下它在【左边】✗ 与罐子不打架 ✓（见 components.css 第 1333 行同款约定 ✓）
   ★ 尺寸 ✗：桌面 64px ✓ 移动 52px ✓（原来是横幅里的 96px ✗ 搬到标题旁必须缩 ✓）
     为什么是 64 ✗（用 canvas 逐像素量过原图 ✓）：320×320 里罐子本体（含底部投影）只占
       74.7% × 80.6% ⇒ 96px 时本体 ≈ 72×77px ✗ 比"标题 + 副标题"整块（27px / 行高 31.86 +
       15px / 行高 22.5 = 54.4px）还高 ✗ 抢标题 ✓
       64px 时本体 ≈ 47.8×51.6px ✓ 略小于 54.4px 的文字块 ✓ 主次正确 ✓ 罐底金币仍看得清 ✓
       52px（≤560px）时本体 ≈ 38.8×41.9px ✓ 390 视口文字列只剩 330px 宽 ✗ 罐子必须让位 ✓
   ★ 间距 ✗：column-gap 13px ✓ 沿用被撤掉那条横幅原本的 gap:13px ✓ 与弹窗内节奏一致 ✓ 移动 10px ✓
   ★★ 右侧留白 54px ✗✗（关键 ✓）：.modalbox 的关闭按钮是 position:sticky + inset-inline-end:16px
     + 38×38 ✓ 实测它压在【内容区右边缘往内 54px 那一条走廊】上 ✓（桌面 1400：内容右沿 1195 ✓ 按钮 1141..1179 ✓
     移动 390：内容右沿 359 ✓ 按钮 305..343 ✓）而 .plans-head 的正文是【整行】✗
     ⇒ 不留白的话副标题第一行会从按钮底下穿过去 ✗ 移动 390 实测就是按钮盖住 "Credits." 那个词 ✗
       （这是【原来就有】的毛病 ✗ 基线截图里按钮同样盖着副标题 ✗ 但文字列从 330px 收到 266px 后
        折行点变了 ⇒ 盖得更明显 ✗ ⇒ 顺手在我们自己这一行里留出来 ✓）
     ★ 为什么用 padding-inline-end 而不是 padding-right ✗：它跟随 direction ✓ RTL 下自动改成左边 ✓
       而关闭按钮用的正是 inset-inline-end ⇒ 两者永远同一边 ✓ ar / fa 下不会留错边 ✓
     ★ 54 = 16（inset）+ 38（按钮宽）✓ 实测数字 ✓ 不留多余空隙 ✓
     ★★ 代价 ✗（实测过 ✓ 认了 ✓）：390 视口下文字列从 266px 收到 212px ⇒ 英文副标题从 2 行变 3 行 ✓
        （只有 zh-CN 仍 2 行 ✓ 标题 11 语言都仍是 1 行 ✓ 实测 h=28.3 ✓）
        "2 行" 与 "不压字" 在 390 宽度下【不可兼得】✗：副标题单行实测 449px ✗ 要 2 行至少得 ~250px 列宽 ✗
        而留白 54 + 罐子 + 间距已经吃掉 116px ✗ ⇒ 选【不压字】✓（基线里按钮正压在副标题上 ✗ 那个更难看 ✓）
   ★ 背景 ✗：不画 ✗（原来那个 #fff8ed 奶白圆角盒子正是用户要删的东西 ✓）
     图片本身是不透明无 alpha 的 webp ✓ 四角实测 rgb(253,253,253) ✓ 与弹窗顶部（#fff）差 2/255
     ⇒ 看不出方块边 ✓ 所以【不设 background / 不设圆角】✓（原 .ce-illu-plans-img 那两个值在这里不要 ✓）
   ★ 无障碍 ✗：alt="" + aria-hidden ✓ 纯装饰 ✓ 标题本身已说明 ✓ 读屏不重复 ✓
   ★ 加载失败 ✗：index.html 行内 onerror 只藏图 ✓ 标题 / 副标题照常 ✓
   ★★ 红线 ✗：零 !important ✓ 全用新类 .plans-head / .plans-head-jar ✓ 不覆盖任何既有规则 ✓
     不碰计费 / 价格 / 额度 ✓ 只改展示 ✓
   ★ 为什么【只追加在文件末尾】✗：ui-spec-drift 记的 components.css 行号全部在 1230 行以内 ✓
     （第 42 / 43 轮同款理由 ✓ 见上面 1370 行 ✓）⇒ 末尾追加【零行号漂移】✓✓
   ★ 实测（桌面 1400 / 移动 390 / 中英日阿波斯 ✗ 真 Chrome ✗ CDP 量 getBoundingClientRect ✓）
     见汇报里的 jar-*.png ✓
   ══════════════════════════════════════════════════════════════════════════════ */
.plans-head{display:grid;grid-template-columns:auto 1fr;align-items:center;column-gap:13px;padding-inline-end:54px}
.plans-head-jar{width:64px;height:64px;grid-row:1 / span 2;object-fit:contain;display:block}
.plans-head h2,.plans-head p{margin:0;grid-column:2}
@media (max-width:560px){
  .plans-head{column-gap:10px}
  .plans-head-jar{width:52px;height:52px}
}

/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 第 46 轮：Credits 卡新版式（用户给的 PPT 版式 ✓）
   ── ① 两张等级卡（终身 / 本月 ✓ 点进 /my-growth.html）
   ── ② 「我的徽章」（只画已点亮的 ✓ 右侧「已点亮 N / M」进成长页）
   ── ③ Buy 拉长占满整行 + 按钮下面那句注释
   ── ④ #billingBtn 从卡片搬进 #plansModal 之后的小字样式
   ★★ 为什么【只追加在文件末尾】✗：ui-spec-drift 记的 components.css 行号全在 1230 行以内 ✓
     ⇒ 末尾追加【零行号漂移】✓✓（与第 42/43/45 轮同款理由 ✓）
   ★★★ 硬红线 ✗✗
     ① 【不用 !important】✗（文件里既有的 !important 都是前几轮的历史包袱 ✓ 本轮一条都不加 ✓）
        其中「藏自己」那三条【必须】压过 .ce-badge-card{display:flex} 之类 ✗
        ⇒ 靠【选择器优先级】解决 ✗ 不靠 !important ✓（见下面的 [hidden] 与 .ce-guest-mode 两条 ✓）
     ② RTL ✗：一律逻辑属性（padding-inline / margin-inline / inset-inline-end ✓）✗ 不写 left/right ✓
     ③ 颜色 ✗：全部走既有 CSS 变量（--lv-accent / --primary / --soft ✓）带兜底值 ✓
        与 .ce-member-plan / .ce-level-main strong 同一套 ✓ 不新增硬编码主题色 ✓
     ④ 对比度 ✗：正文用 #64748a 以上（灰字最小 11px ✓ 与文件既有 .ce-tier-caption 同级 ✓）
   ★ 实测（桌面 1400 / 移动 390 / reduce 两取值 / ar·fa RTL ✓ 真 Chrome + CDP ✓）
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── 访客态【一个字都不许变】✗✗：这三块必须与 #creditMeterWidget 同一条隐藏链 ✓
   ★★ 优先级【算清楚】✗（我第一版算错了 ✓ 已修 ✓）：
     · .ce-credit-card.ce-guest-mode .ce-badge-card = (0,3,0)
     · 而下面那句 .ce-badge-card{display:flex} = (0,1,0)
       ⇒ 隐藏【赢】✓ 但 `[hidden]` 只有 (0,1,0) 且【更靠前】⇒ 它【赢不了】.ce-badge-card{display:flex} ✗
     · 所以专门补一条 .ce-badge-card[hidden]{display:none} = (0,2,0) ✗ 比 (0,1,0) 高 ✓
   ★ 为什么不用 !important ✗：本轮红线禁止 ✓ 靠优先级就能解 ✓ 就不该用它 ✓
   ★ 为什么不塞进 790 行那条大规则 ✗：那条列的是 id ✓ 动它会改既有行号 ✗
     ui-spec-drift 按行号核对 ✓ ⇒ 另起一条【零漂移】✓ */
.ce-credit-card.ce-guest-mode .ce-lv-cards,
.ce-credit-card.ce-guest-mode .ce-badge-card,
.ce-credit-card.ce-guest-mode .ce-credit-note{display:none}

/* ── ① 两张等级卡 ✗ 直接复用 .ce-level-dual 的 1fr 1fr 网格 ✓ 只补内部三件套 ✓ */
.ce-lv-cards{margin-top:10px}
.ce-lv-card{position:relative;display:flex;flex-direction:column;align-items:center;text-align:center;gap:6px;
  padding:11px 12px;cursor:pointer;transition:transform .18s,box-shadow .18s,border-color .18s}
.ce-lv-card:hover{transform:translateY(-1px);border-color:var(--lv-accent,#12b3a0);box-shadow:0 8px 18px rgba(51,76,117,.09)}
.ce-lv-card:focus-visible{outline:2px solid var(--lv-accent,#12b3a0);outline-offset:2px}
.ce-lv-label{color:#8895a7;font-size:11px;font-weight:var(--ce-fw-head);line-height:1.3}
.ce-lv-figure{display:flex;flex-direction:column;align-items:center;gap:6px;min-width:0;text-align:center}
.ce-lv-img{width:61px;height:61px;flex:0 0 61px;display:block;object-fit:contain}
.ce-lv-name{font-family:var(--ce-font-display);font-size:15px;line-height:1.2;font-weight:var(--ce-fw-head);
  color:#0f766e;overflow-wrap:anywhere}
.ce-lv-arrow{position:absolute;inset-inline-end:10px;inset-block-start:50%;transform:translateY(-50%);
  color:#b9c4d2;font-size:18px;line-height:1;pointer-events:none}
[dir="rtl"] .ce-lv-arrow{transform:translateY(-50%) scaleX(-1)}

/* ── ② 我的徽章 ✗ 只画已点亮的 ✓ 没有 ⇒ 整块【根本没有 DOM 内容】且被 [hidden] 压住 ✓ */
.ce-badge-card{display:flex;flex-direction:column;gap:9px;margin-top:10px;padding:11px 12px;
  border-radius:15px;background:#f8fafc;border:1px solid #e5ebf3}
.ce-badge-card[hidden]{display:none}
.ce-badge-head{display:flex;align-items:center;gap:7px;width:100%;padding:0;margin:0;border:0;
  background:none;font-family:inherit;color:#233a62;cursor:pointer;text-align:start}
.ce-badge-head b{font-size:13px;font-weight:var(--ce-fw-head)}
.ce-badge-head em{margin-inline-start:auto;color:#0f766e;font-size:12px;font-style:normal;font-weight:var(--ce-fw-head)}
.ce-badge-arrow{color:#b9c4d2;font-size:16px;font-style:normal;line-height:1}
[dir="rtl"] .ce-badge-arrow{transform:scaleX(-1)}
.ce-badge-head:focus-visible{outline:2px solid var(--lv-accent,#12b3a0);outline-offset:2px;border-radius:6px}
.ce-badge-strip{display:flex;flex-wrap:wrap;gap:8px 10px}
.ce-badge-item{display:flex;flex-direction:column;align-items:center;gap:4px;flex:0 0 auto;width:52px}
.ce-badge-item img{width:40px;height:40px;display:block;object-fit:contain}
.ce-badge-item b{font-size:10px;line-height:1.25;font-weight:var(--ce-fw-head);color:#0f766e;
  text-align:center;overflow-wrap:anywhere}

/* ══════════════════════════════════════════════════════════════════
   ★★★ 第 53 轮（用户拍板 ✓）：拿到徽章就把徽章显示到这个卡片里来 ✗✗
     用户原话：「有了徽章，就要显示到这个地方来。那句话可以显示在徽章的下方。」

   ── 两种态 ✗ 各归各的 ✓（类由 app.js 的 renderCreditCardExtras 加 ✓）──
     · .ce-badge-card--one  ：有徽章 ⇒ 一行「最近一枚 + 共几枚」✓ 那句鼓励语【落到徽章下方】✓
     · .ce-badge-card--none ：零枚   ⇒ 【完全维持改前样子】✓ 鼓励语位置 / 字号 / 间距一个字节不变 ✓

   ── ★★ 为什么 --none 一条规则都不写 ✗
     改前就是零枚的样子 ✗ 它就是基线 ✓ 另写一套 = 多一处漂移 ✓

   ── 动效 ✗✗ 硬要求：★ 基态必须写在媒体查询【之外】✗
     否则"关动效"那档会连【可见性】一起关掉 ⇒ 徽章永远不出现 ✗✗
     ⇒ 基态：只声明 animation:none ✗ 不声明 opacity / visibility ✓（可见性不靠动效 ✓）
       动效：只在 @media (prefers-reduced-motion: no-preference) 里加 ✗
       减少动效：本文件 427 行的全局 *{animation:none!important} 已兜住 ✓ 这里不再写 ✓
     ★ 只用 opacity / transform ✗ 不碰 width/height/left/top ⇒ 零布局位移 ✓
     ★ 颜色只用既有值 ✗：金 #e8b33c / 青绿 #0f766e / 灰 #b9c4d2（都在本文件里出现过 ✓）
     ══════════════════════════════════════════════════════════════════ */
/* ★★★ 第 54 轮（产品主人参照图 ✓）：从「最近一枚 + N」改成【一排已得到的徽章】✗✗
   ── 照参照图的排列 ✗ 逐条对齐 ✓ ──
     · 标题 + 右侧「查看进度 ›」入口 —— index.html:442-446 【一个字节没动】✓
     · 下面【一排】徽章 ✗ 圆图在上 ✓ 小字名字在下 ✓
     · 已得到的超过一行放得下的 ⇒ 【同一行最后一格】换成「+N」圆徽记 ✓
       参照图 = 3 枚 + 「+9」（共 12 枚 ✓）⇒ 与 JS 的 cols-1 算法【逐位相同】✓
   ── ★★ 高度恒定 ✗✗（硬要求：不许顶动下方元素 ✓）──
     · display:grid + repeat(var(--ce-badge-cols),1fr) ⇒ 【永远只有一行】✗
       枚数再多也不换行 ✓ 第 12 枚变成 +N ✓
     · 名字盒【固定两行高】(height:2.5em) ⇒ 名字长短【不改变行高】✓
     ⇒ 1 枚与 12 枚的卡高【完全相同】✓（实测见报告 ✓）
   ── ★ 尺寸三候选 ✗ 只改下面 .ce-badge-card 里那两个变量 ✗ 渲染逻辑一个字不用动 ✓ ──
     A 一行四个：--ce-badge-cols:4;--ce-badge-img:44px   （= 参照图那一档 ✓）
     B 一行三个：--ce-badge-cols:3;--ce-badge-img:56px
     C 更大三个：--ce-badge-cols:3;--ce-badge-img:66px
   ── ★ 「放得下几个」只有一处定义 ✗✗ ──
     列数由 CSS 变量给 ✓ app.js 的 ceBadgeStripHtml 只【读】它 ✓
     ⇒ 换尺寸不必动 JS ✓ 换素材不必动 CSS（尺寸与文件解耦 ✓）
   ── RTL ✗：grid + 逻辑属性 ✓ 浏览器自动镜像 ✗ 不写 left/right ✓
   ── 动效 ✗：见下面那段（基态在媒体查询【之外】✓ 可见性不靠动效 ✓）*/
.ce-badge-card{--ce-badge-cols:4;--ce-badge-img:44px;--ce-badge-gap:7px}
.ce-badge-card--one .ce-badge-strip{display:grid;
  grid-template-columns:repeat(var(--ce-badge-cols),minmax(0,1fr));
  gap:var(--ce-badge-gap);align-items:start}
.ce-badge-card--one .ce-badge-item{flex-direction:column;align-items:center;gap:4px;
  width:auto;min-width:0;max-width:100%}
.ce-badge-card--one .ce-badge-item img{width:var(--ce-badge-img);height:var(--ce-badge-img);
  flex:0 0 auto;margin-inline:auto}
.ce-badge-card--one .ce-badge-item b{font-size:10px;line-height:1.25;text-align:center;
  min-width:0;max-width:100%;height:2.5em;overflow:hidden;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow-wrap:anywhere}
.ce-badge-card--one .ce-badge-empty{grid-column:1/-1;text-align:start;font-size:11.5px;
  line-height:1.5;margin:0}
/* ★ +N 圆徽记 ✗ 与徽章图【同直径、同列宽】✓（参照图里它就是一个和徽章一样大的圆 ✓）*/
.ce-badge-more{justify-self:center;align-self:start;width:var(--ce-badge-img);
  height:var(--ce-badge-img);display:flex;align-items:center;justify-content:center;
  border-radius:50%;background:#eef2f7;color:#5b6b80;font-size:12px;
  font-weight:var(--ce-fw-head);line-height:1}
/* ★★ 基态【在媒体查询之外】✗✗：只声明 animation:none ✗ 不声明 opacity / visibility ✗
   —— 万一媒体查询没生效 ✗ 徽章也是【立刻可见、只是不动】✓ 绝不会"永远不出现"✓✓
   ★ 可见性【不靠动效】✗：<img> 本来就在 DOM 里 ✓ animation-fill-mode 不参与可见性 ✓
   ★ 本文件 427 行那条全局 @media(prefers-reduced-motion:reduce){*{animation:none!important}}
     已经兜住"减少动效"那一档 ✓ ⇒ 这里【不必】再写一条 reduce 规则 ✗ 少一条少一处漂移 ✓ */
.ce-badge-card--one .ce-badge-item img{animation:none}
/* ★ 只有明确允许动效的环境才动 ✗（reduce 那档【连动画都不声明】✓ 而不是"声明了再关"✓）*/
@media (prefers-reduced-motion: no-preference){
  .ce-badge-card--one .ce-badge-item img{animation:ceBadgeIn .26s ease-out both}
}
@keyframes ceBadgeIn{from{opacity:0;transform:translateY(3px) scale(.96)}to{opacity:1;transform:none}}
:root[data-theme=aurora] .ce-badge-more{background:rgba(255,255,255,.10);color:#cbd5e1}

/* ── ③ Buy 拉长占满整行 ✗ + 按钮下面那句注释 ✓
   ★★ 只加宽【金色那颗】(= #buyBtn ✓) ✗ 不动 .ce-care-btn ✓
     —— Care reset 的显隐由 app.js 内联 display 控制 ✓ 这里一个字节都不碰 ✓ */
.ce-credit-actions .btn.gold{flex:1 1 100%}
.ce-credit-note{margin:9px 0 0;text-align:center;color:var(--ink-2,#7c8a9f);font-size:11px;line-height:1.5}
.ce-credit-note b{color:#0f766e;font-weight:var(--ce-fw-head)}
/* ★★ 访客态下这句话也藏起来 ✓（已在最上面那条里 ✓）—— 这里只保证它不撑出横向滚动 ✓ */
.ce-credit-note,.ce-badge-strip{max-width:100%}

/* ── ④ #billingBtn 的新家（#plansModal 底部那行小字 ✓ 不抢眼 ✗ 但可达 ✓）*/
.ce-manage-sub-wrap{margin-top:14px;text-align:center}
.ce-manage-sub{padding:6px 10px;border:0;border-radius:9px;background:none;font-family:inherit;
  color:var(--ink-2,#7c8a9f);font-size:12px;font-weight:var(--ce-fw-head);text-decoration:underline;
  text-underline-offset:3px;cursor:pointer;transition:color .18s,background .18s}
.ce-manage-sub:hover{color:var(--primary,#2f6fe0);background:var(--soft,#eef6ff)}
.ce-manage-sub:focus-visible{outline:2px solid var(--primary,#2f6fe0);outline-offset:2px}

/* ── 移动端 ✗ 390px 两张等级卡不再并排（与 .ce-level-dual 既有断点【同为 700px】✓ 口径一致 ✓）*/
@media (max-width:700px){
  .ce-lv-cards{grid-template-columns:1fr}
  .ce-lv-img{width:53px;height:53px;flex-basis:53px}
  .ce-badge-item{width:48px}
  .ce-badge-item img{width:36px;height:36px}
}

/* ── 深色主题 aurora ✗ 与 :root[data-theme=aurora] .ce-level-box 同一口径补三处 ✓
   ★ 那几条既有规则带 !important ✗ 是历史包袱 ✓ 我这里【不带】✗
     靠 :root[data-theme=aurora] 前缀（优先级更高 ✓）压过上面同层选择器 ✓ */
:root[data-theme=aurora] .ce-badge-card{background:rgba(255,255,255,.05);border-color:rgba(255,255,255,.10)}
:root[data-theme=aurora] .ce-badge-head{color:#e9edff}
:root[data-theme=aurora] .ce-badge-item b,:root[data-theme=aurora] .ce-credit-note,:root[data-theme=aurora] .ce-lv-label{color:var(--ink-2,#9aa6c8)}
/* ★★ 批 A（第 45 轮）：上面这条里【删掉了 .ce-manage-sub】✗✗ 原因 ✗ 它虽然也是"小灰字" ✗
   但它落在【浅色纸面弹窗】上（#plansModal 底部 ✗ 见本文件 327 行那条既定浅纸面 ✓）
   ⇒ 被一起刷成 #9aa6c8 后 = 浅字压浅底 ✗ 实测 aurora **2.34:1** ✗（elf 对照 5.76:1 ✓）
   ⇒ 修法 ✗ 让它回落到【纸面口径】：.ce-manage-sub{color:var(--ink-2)} ✗
      在 .modalbox 作用域里 --ink-2 = --paper-ink-2 = #5b6472 ⇒ 5.76:1 ✓ 四主题一致 ✓
   ★ 同一条里另外两个保留 ✓ 它们确实压在 aurora 的深色卡上（.ce-badge-item b / .ce-credit-note）
     ⇒ #9aa6c8 在 #1b2442 上 ≈ 6.29:1 ✓ 不动 ✓（.ce-lv-label 是别处新加的 ✓ 一个字节没碰 ✓）*/

/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 第 48 轮：Credits 卡四个细节（用户反馈逐条落地 ✓）
   ── ① 等级徽章放大 ✗ + 本月徽章【内画稿】单独放大 ✓
      实测根因（真 Chrome + canvas 取 alpha 包围盒 ✓ 不是推断 ✓）：
        两张图的画布都是 160×160 ✗ 但【画稿在画布里的占比差一倍】✓
          · life-1..7（盾牌 / 树）画稿 137×140 → 85.6% × 87.5%（7 枚均匀 ✓）
          · mon-1..6（小精灵）  画稿 84..102 × 108..134 → 52.5%~63.7% × 67.5%~83.8%
        ⇒ 同样大的盒子 ✗ 终身那枚看得见 ≈40.3px ✓ 本月只 ≈31.0px 高 × 24.2px 宽 ✗ 永远小一圈 ✓
        ⇒ 盒子 36→46px（移动 32→40px ✓）+ 本月内画稿 ×1.15 ✓
      ★ 为什么用 transform:scale 而【不重裁素材】✗：重裁要动 6 张 webp + 走资产流程 ✗
        影响面大 ✓ 而 scale【不占布局】✗ 下面那行小字不会位移 ✓
        且最大的一张 mon-6（画稿占 83.8%）放大 1.15 后 = 44.3px < 46px 盒 ⇒
        【不裁切】【不压到文字】✓（桌面 46 / 移动 40 两种尺寸都实测过 ✓ 见汇报 ✓）
   ── ② 名字移到图标下方 ✗ 图标 → 小字 → 大字 ✗ 三行全部居中 ✓（DOM 顺序见 index.html:409-413 ✓）
   ── ③ 名称 / 计数 / 注释里的关键词【三处统一】#0f766e ✓（用户拍板 ✓）
      ★★ 这是一处【有意的取舍】✗：这三处不再跟【终身等级】变色（--lv-accent）✓
         想退回按等级变色 ✗ 把下面这三处的 #0f766e 换回 var(--lv-accent,#12b3a0) 即可 ✓✓
         （三处位置：① 上面 .ce-lv-name 的 color ② 上面 .ce-badge-head em 的 color
                    ③ 上面 .ce-credit-note b 的 color ✓）
      ★ 为什么选 #0f766e ✗：对白底 5.47:1 ✓ 过站内 4.5:1 红线 ✓ 且是同色系里最接近
        参考图那支青绿（参考图的 #12b3a0 对白底只有 2.63:1 ✗ 15px / 10px 文字不达标 ✓）
   ── ④ 0 枚徽章时的空状态 ✗（句子里【一个数字都没有】✓ 计数由后端 total 直接给 ✓）
   ★★ 为什么不塞进上面那几行 ✗：ui-spec-drift 按行号核对 ✓ 上面只改【行内值】✓
      新增规则一律【追加到文件末尾】⇒ 零行号漂移 ✓（与第 42/43/45/46/47 轮同款理由 ✓）
   ★ 硬红线 ✗：不用 !important ✓ 一律逻辑属性 ✓ RTL 用与方向无关的 text-align:center ✓
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── ① 本月徽章（盾牌 / mon-N）内画稿 ×1.265 ✗ 只这一枚 ✓（终身那枚画稿本来就占满 ✓ 见上面实测 ✓）
   ★★ 第 52 轮（用户要求「图4 이번 달 徽章再放大 10%」✓）：1.15 → 1.265（= 1.15 × 1.10 ✓）
      为什么【不改成加大盒子】✗：实测两种改法视觉等价（渲染 91.04 vs 91.05 ✓）
        但改盒子会把两张等级卡的名字行【错开 3.59px】（life 328.72 / month 332.31 ✗）
        且卡高 152.17→159.38 ✗ 违背"对齐用结构"✓
      ⇒ 走 scale ✗ 零布局位移 ✓ 卡高不变 ✓ 两卡名字行仍同 325.11 ✓
      ★ 安全边界（实测 ✗ 最坏的 mon-6 画稿占 84.38% ✓）：
        67.1 × 0.8438 × 1.15 = 65.1 < 67.1 盒 ⇒ 不裁切 ✓（真 Chrome clipOk=true ✓ 含 section#creditCard{overflow:hidden} ✓）
        墨迹→下方小字间距 ✗ 8.41px → 4.94px ✗ 仍为正 ✓ 不压字 ✓ */
#ceLvMonthIcon{transform:scale(1.265)}

/* ── ④ 空状态那句话 ✗ 与「已点亮 N / M」同色同口径 ✓ 占满一行、居中 ✓ */
.ce-badge-empty{flex:1 1 100%;text-align:center;color:#0f766e;font-size:11px;line-height:1.45;
  font-weight:var(--ce-fw-head);overflow-wrap:anywhere}
/* ★★ aurora（深色）✗✗：那 4 处 + 空状态那句必须换【亮青绿】✓
   ★ 为什么必须换 ✗：实测（真 Chrome ✓ 取元素矩形内的【像素众数】当背景 ✓ 不靠假设 ✓）
     aurora 下这几处的真实合成背景 = **#262f4b**（= --card:rgba(255,255,255,.06) 压在页面渐变上 ✓
     不是 themes.css 的 --surface:#1b2442 ✗ 那是另一层 ✓）
     ⇒ #0f766e 在 #262f4b 上只有 **2.41:1** ✗✗（远低于 4.5:1 ✓）
     ⇒ 换 #5eead4 ⇒ **8.92:1** ✓（同族备选实测：7dd3c0 = 7.51 · 2dd4bf = 7.09 · 99f6e4 = 10.47 ✓）
   ★ 为什么用 #5eead4 而不是另造 token ✗：它【本来就在本站调色板里】
     （components.css:291 的 .ce-motion-btn.on 渐变 · :529/:534 的年付条 ✓）⇒ 不新增色 ✓
     主题 agent 的 --surface 系列是【面板底色】✗ 不是文字色 ✓ 能复用的地方只有背景口径 ✓
     这里没有可复用的亮青绿 token ⇒ 直接用既有色值 + 把实测对比度写在注释里 ✓
   ★ 优先级算清楚 ✗：本条 (0,3,1) 与文件上方 1728 行那条 aurora 灰 (#9aa6c8) 同权重 ✓
     ⇒ 靠【后来居上】赢 ✓ 不靠 !important ✓（.ce-credit-note 那段灰字仍然保持 #9aa6c8 ✓
        只有它的 <b> 关键词跟着变亮 ✓ 与浅色主题里"灰句 + 青绿关键词"的分工一致 ✓）*/
:root[data-theme=aurora] .ce-lv-name,
:root[data-theme=aurora] .ce-badge-head em,
:root[data-theme=aurora] .ce-badge-item b,
:root[data-theme=aurora] .ce-credit-note b,
:root[data-theme=aurora] .ce-badge-empty{color:#5eead4}

/* ★★ aurora 补丁 ② ✗✗：那行【小灰字标签】（终身等级 / 本月等级 ✓ .ce-lv-label）
   实测（同上一节的方法 ✗ 元素矩形内像素众数当背景 ✓）：aurora 下 #8895a7 在 #262f4b 上
   只有 **4.34:1** ✗ 差 0.16 没到 4.5:1 ✓
   ★ 落地口径【两段】✗：① 先把选择器【并进上面 1728 行那条 aurora 灰组】✓ 不另起一条 ✓
     ② 用户拍板"有 token 就用 token"⇒ 那条组的颜色已从 #9aa6c8 换成 **var(--ink-2,#9aa6c8)** ✓
        （--ink-2 = color-mix(--ink 82%, --surface) ✗ 四主题各自成立 ✗ 以后加第 5 个主题自动适配 ✓
          #9aa6c8 保留作 fallback ✗ 万一 color-mix 不被支持也不至于没色 ✓）
        aurora 实测 ✗ --ink-2 ≈ #CBCBDA ⇒ **8.23:1**（比硬编码 #9aa6c8 的 5.45 更好 ✓）
   ★ 那条组是【aurora 作用域】✗：浅色三主题【一个字节都不受影响】✓（实测三主题那几处仍是原值 ✓）
   ★ 只补 aurora ✗ 浅色【不动】✓ 浅色下 #8895a7 在白底是 3.04:1 ✓
     要修得整体重排灰阶（.ce-lv-label / .ce-tier-caption / .ce-member-role … 同一批 ✓）
     ⇒ 属于【独立一轮】✗ 记为待办 ✓ 本轮只解决深色主题读不清 ✓ 别顺手改浅色 ✓ *//* ══════════════════════════════════════════════════════════════════════
   ★★ 批 C 续：.lv-badge（22px/900 白字压 11 个亮色渐变）
   ──────────────────────────────────────────────────────────────────────
   ★ 先量清楚（真浏览器 ✗ 徽章上下【边缘带】取渐变底 ✗ 不用盒内众数：
     22px/900 的数字在 46×46 的盒里占满 ✗ 盒内众数是白字 ⇒ 会读出 1 的假阳性 ✗ 踩过 ✓）
     WCAG 1.4.3 字面口径（白字 vs 渐变 ✗ 22px/900 ⇒ need 3.0）在 elf 实测：
       life-1 1.86 ✗ life-2 1.85 ✗ life-3 2.53 ✗ life-4 2.74 ✗ life-5 2.75 ✗
       life-6 1.82 ✗ life-7 2.78 ✗ month-1/2 2.63 ✗ month-3/4 1.99 ⇒ **11 个变体全部 <3.0** ✓
     （这批渐变是【写死】的 ✗ 与主题无关 ⇒ 三个浅主题 + aurora 未覆盖前都一样坏 ✓）
   ★ 走过的弯路（保留作证据 ✗ 结论见本节末 ✓）：
     先按"只给字形加描边"试了三档候选（elf ✗ 取最难的三个）：
       A 只加软阴影 text-shadow:0 1px 2px rgba(0,0,0,.45) ⇒ 局部对比 2.76~5.84 ✗ life-2 仍 <3.0 ✗ 不够
       B 硬阴影 + .6px 描边                                  ⇒ 局部对比 9.83~11.56 ✓
       C 1px 强描边                                          ⇒ 19.86 ✓ 但 22px 字形会被明显吃细 ✗ 不用
     ⇒ 当时落地了 B ✗ 但【字面口径仍是 1.82~2.78 <3.0】（描边不参与字面比值 ✓ 当时如实报回 ✓）
   ★★ 用户拍板【压深渐变】后 ✗ 按"以实测决定描边去留"复核（真浏览器 ✗ 14 变体 × 四主题）：
       字面口径：**有描边 3.46~4.18 ✗ 无描边 3.46~4.18** ⇒ 完全一样（描边从不参与字面比值 ✓）
       字形局部：有描边 13.45~14.92 ✗ **无描边 4.64~6.66** ⇒ 压深后的渐变自己就够了（≥3.0 ✓）
     ⇒ 判据成立（字面已 ≥3.0 且描边不再必要）⇒ **描边已移除** ✓ 白字保持干净 ✗ 字形不被加粗 ✓
   ★ aurora 那边另有批 C 的深色渐变覆盖 ⇒ 与本块无关 ✓ 不冲突 ✓
   ══════════════════════════════════════════════════════════════════════ */
/* ★★ 批 C 终：.lv-badge 的【最小压深】（用户拍板：压深渐变 ✗ 白字与品牌识别不变 ✗ 用二分求最小值 ✗ 不是估 40%）
   ──────────────────────────────────────────────────────────────────────
   ★ 方法 ✗ HSL【固定 H/S ✗ 只乘 L】✗ 目标 = 白字对【两端点】都 ≥3.05
     先在最难的几个上求（life-6 1.82 ✗ life-2 1.85 ✗ life-1 1.86 ✗ 正是字面口径最差的三条 ✓）✗ 再全量复核
   ★ 二分结果（f = L 乘数 ✗ 压深 = 1 − f ✗ 每条【各自】的最小值 ✗ 不是一刀切）：
       life-1 33%（#1495ff→#1273cb ✗ 3.10）  life-2 38%（#31a48c→#207163 ✗ 3.08）
       life-3 24%（#39a57d→#1a7a51 ✗ 3.06）  life-4  9%（#6a8ef2→#474aee ✗ 3.10）
       life-5  8%（#a27df2→#7942f5 ✗ 3.11）  life-6 40%（#c8830c→#8e5815 ✗ 3.13）
       life-7 11%（#af72ff→#681cea ✗ 3.15）  month-1/2 22%（#d37f2e→#905e30 ✗ 3.07）
       month-3/4 26%（#8292b5→#5e6d90 ✗ 3.12）month-5/6 48%（#bc8800→#7c590d ✗ 3.15）
       month-7 11%（#af72ff→#681cea ✗ 3.15）  ⇒ 最差端点 3.06~3.15（need 3.0 ✓ 全部过 ✓）
   ★ 色相 / 饱和度【一个都没动】✗ 只降亮度 ✓ 白字、字号、字形、圆角、投影环全部不动 ✓
   ★ 区分度复核（怕"压成一个色"）：11 条亮度仍是 **31.9 ~ 66.5**（压前 36~78 ✓ 层次保持 ✓）
     唯一 0 差的一对是 life-7 / month-7 ✗ 它们【原本就是同一个渐变 #c9a0ff→#7c3aed】⇒ 不是被压平的 ✓
   ★ 为什么选择器要带 #levelsModal ✗：注入的 #lvCss【晚于】本文件 ✗ 同优先级下它赢
     ⇒ 必须 (1,1,0) 压 (0,1,0) ✓ **依旧零 !important** ✓（不改 levels-info.js ✗ 不在可改清单 ✓）
   ══════════════════════════════════════════════════════════════════════ */
#levelsModal .lv-badge-life-1{background:linear-gradient(145deg,#1495ff,#1273cb)}
#levelsModal .lv-badge-life-2{background:linear-gradient(145deg,#31a48c,#207163)}
#levelsModal .lv-badge-life-3{background:linear-gradient(145deg,#39a57d,#1a7a51)}
#levelsModal .lv-badge-life-4{background:linear-gradient(145deg,#6a8ef2,#474aee)}
#levelsModal .lv-badge-life-5{background:linear-gradient(145deg,#a27df2,#7942f5)}
#levelsModal .lv-badge-life-6{background:linear-gradient(145deg,#c8830c,#8e5815)}
#levelsModal .lv-badge-life-7{background:linear-gradient(145deg,#af72ff,#681cea)}
#levelsModal .lv-badge-month-1,#levelsModal .lv-badge-month-2{background:linear-gradient(145deg,#d37f2e,#905e30)}
#levelsModal .lv-badge-month-3,#levelsModal .lv-badge-month-4{background:linear-gradient(145deg,#8292b5,#5e6d90)}
#levelsModal .lv-badge-month-5,#levelsModal .lv-badge-month-6{background:linear-gradient(145deg,#bc8800,#7c590d)}
#levelsModal .lv-badge-month-7{background:linear-gradient(145deg,#af72ff,#681cea)}
/* ══════════════════════════════════════════════════════════════════════
   ★★★ 批 C（第 45 轮）：#levelsModal 的 aurora 覆盖 ✗✗
   ──────────────────────────────────────────────────────────────────────
   ★ 为什么在这里加 ✗：写死浅底的是 levels-info.js 注入的 #lvCss ✗ 而它【不在本轮可改清单】
     ⇒ 按批准【不动那个文件】✗ 在 components.css 里补 aurora 覆盖 ✓（本文件在白名单内 ✓）
   ★ 先确认过它确实坏（真浏览器 ✗ 真实 build 出来的 DOM + 真实 #lvCss ✗ 只多一个 open 类 ✓ 实测）：
       aurora：.lv-row 1.11 ✗ .lv-row.cur 1.11 ✗ .lv-free 1.07 ✗ .lv-box 1.13 ✗（全是被写死的 #fff 浅底害的 ✓）
       elf 对照：10.16 / 10.16 / 9.81 / 10.35 ⇒ 三浅主题本来就对 ✓
   ★ 覆盖只对 aurora 生效（:root[data-theme=aurora] ✓）⇒ 三个浅主题【一个字节都不受影响】✓
   ★ 零 !important：靠 (1,2,0) 压过注入那份的 (0,1,0) ✓（注入晚于本文件 ✗ 但优先级输 ✓）
   ★ .lv-badge 2.35 是【四主题一律】的存量（白字压 life-N 的亮绿渐变 ✗ 22px/900 要 3.0）
     ⇒ 本轮只按批准修 aurora（统一走 --sel 深色渐变 ⇒ 白字 ≥4.9 ✓）
       浅色三主题【不动】✗ 记为待办（要修得整体重排那 7 个 life + 2 个 month 渐变 ✓）
   ══════════════════════════════════════════════════════════════════════ */
:root[data-theme=aurora] #levelsModal .lv-row{background:var(--surface-2);color:var(--ink)}
:root[data-theme=aurora] #levelsModal .lv-row.cur{background:linear-gradient(180deg,color-mix(in srgb, var(--success) 20%, var(--surface)),var(--surface));border-color:var(--sel-b)}
:root[data-theme=aurora] #levelsModal .lv-free{background:color-mix(in srgb, var(--accent) 16%, var(--surface));border-color:color-mix(in srgb, var(--accent) 45%, var(--surface));color:var(--ink)}
:root[data-theme=aurora] #levelsModal .lv-free b{color:color-mix(in srgb, var(--accent) 55%, #fff)}
:root[data-theme=aurora] #levelsModal .lv-tabs{background:var(--surface)}
:root[data-theme=aurora] #levelsModal .lv-tab{background:var(--surface-2);color:var(--ink-2);border-color:var(--line)}
:root[data-theme=aurora] #levelsModal .lv-tab.on{background:linear-gradient(180deg,var(--sel-a),var(--sel-b));border-color:var(--sel-line);color:var(--sel-ink);box-shadow:0 4px 0 var(--sel-line)}
:root[data-theme=aurora] #levelsModal .lv-badge{background:linear-gradient(145deg,var(--sel-a),var(--sel-b));color:var(--sel-ink)}
@media(max-width:560px){footer.wrap{padding-right:92px}}/* ★★★ 页脚最后那个链接「Contact us」窄屏会被右下角的固定浮动按钮压住 ✗ 这条给它留出宽度 ✓ 实测 public/ 390×844：#weeklyReportBtn 固定 56×56 · right/bottom 各 24px ⇒ 占 x310–366 / y764–820 ✗ 页脚末行原本到 x≈350 ⇒ 重叠 40×17 = 680px² ✓ 选 padding-right 而不是 padding-bottom ✗ 不留一大块空白 ✓ 320/390/560 三档验算：正文右边界恒 < 按钮左边界 ✓ 只命中 index.html 的 <footer class="wrap muted">（该浮动按钮只在 index 上存在 ✓）✗ 不动 .mg-foot / q/ / legal 的页脚 ✓ */

/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 第 55 轮 ✗✗ #weeklyReportBtn 与手机底部标签栏【抢同一个位置】✓✓
   ──────────────────────────────────────────────────────────────────────────────
   ── 真事故（真浏览器实测 ✓ 不是推断 ✓）──
     390×844 实测几何 ✗：
       #ceTabBar          position:fixed  z-index 60    t=783 b=844  w=390
       「我的」那一项      l=293 r=390   t=790 b=838               ← 标签栏最右那一格
       #weeklyReportBtn   position:fixed  z-index 9999  t=764 b=820  l=310 r=366
     ⇒ 重叠 56×30 = 1680px² = 「我的」那一格面积的 36% ✓
     ⇒ ★★ 而「我的」的中心点 (341,814) 【正好落在重叠区里】✗✗
        实测 elementFromPoint 16 点扫描 ⇒ 6 个点命中的是 #weeklyReportBtn ✗
     ⇒ 后果 ✗：**用户按标签栏「我的」的正中间，按到的是那个悬浮按钮** ✓
        点不到「我的」⇒ 而它正是刚接好的账户入口 ✓✓
   ── 三档都中招（不是只有 390 那档）✗ ──
     320×568 重叠 44% · 360×740 重叠 39% · 390×844 重叠 36%
   ── 为什么只在 max-width:640px 里 ✗ ──
     标签栏自己的显示条件就是 @media(max-width:640px)（见上面 .mobile-tabbar 那条 ✓）
     768 及以上实测 #ceTabBar display:none ⇒ 没有这个冲突 ⇒ 不必动 ✓（不动就不会引入新回归 ✓）
   ── ★ 为什么必须 !important ✗✗ ──
     #weeklyReportBtn 的 bottom 是【内联样式】✗（v55-enhance.js:2721 的 cssText ✓）
     内联 > 任何选择器（含 #id）⇒ 不写 !important 这条规则【完全不起作用】✓
     ⇒ 这一处是本仓极少数的正当 !important ✓ 理由写在这里免得被"清理"掉 ✓
   ── 为什么抬到标签栏【上方】而不是别的改法 ✗ ──
     ① 缩小组件：56×56 是它的既有尺寸 ✗ 改了要么压住更多、要么难按 ✓
     ② 把标签栏压到按钮上面：几何上仍重叠 ✗ 只是换谁在上面 ⇒ 变成按钮点不到 ✓
     ③ ★ 抬到上方：两个都完整可点 ✗ 不动任何一方的身份与尺寸 ✓✓
        （这就是本文件 :2178 那条既有修法的同一条思路 ✗ 让开、不是压过去 ✓）
   ── 61px 是怎么来的 ✗ ──
     标签栏自己的高度 ✗ 实测三档都是 61px ✓ 与 .mobile-tabbar 的 padding 同源 ✓
     ★ 再加上 env(safe-area-inset-bottom) ✗ 与标签栏自己的写法【逐字同源】✓
       不补这一项 ⇒ 全面屏手机上按钮会落在 Home 指示条那一条上 ✓
   ── 文档级证据 ✗ ──
     tests/onboarding-wizard-cdp.js 的「「我的」中心点必须命中它自己」那条断言守着它 ✓✓ */
@media(max-width:640px){
  #weeklyReportBtn{bottom:calc(24px + 61px + env(safe-area-inset-bottom,0px))!important}
}

/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 第 49 轮：Credits 卡三处调整（用户逐字要求 ✓ 含模拟截图 ✓）
   ── ① 徽章再放大 15%      → .ce-lv-img 46→53px（移动 40→46px ✓ 就地改上面两行 ✓）
   ── ② "Water the tree" 下移 → #creditActionBtn 搬进 .ce-credit-readout（index.html ✓）✗ 规则在本节
   ── ③ 胶囊与按钮【固定尺寸】→ 本节末尾那组 min-inline-size 令牌 ✓✓
   ★ 为什么全部追加在文件末尾 ✗✗：ui-spec-drift 按【行号】核对 140 条关键值 ✓
     上面只改【行内数字】(1680 / 1724 ✓) ✗ 新增一律追加 ⇒ 零行号漂移 ✓
     （与第 42/43/45/46/47/48 轮同款理由 ✓）
   ★ 硬红线 ✗：零 !important ✓ 一律逻辑属性（inline-size / inset-inline-start ✓）
     RTL 靠逻辑属性自动镜像 ✓ 不写 [dir] 例外 ✓（只有水壶那条必须用物理 right ✗ 见 ③ 说明 ✓）
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── ② "Water the tree" 落进白卡 ✗ 靠【同容器 + 同 padding】拿到同宽同左边缘 ✓
   ★★ 为什么把按钮搬进 .ce-credit-readout 而不是留在外面靠手算宽度对齐 ✗✗：
     外面那个容器（.ce-credit-card）的白卡是 .ce-credit-readout{position:absolute;left:18px;right:18px}
     ⇒ 它的左右边缘是【相对 .ce-credit-widget 的 18px】✗ 不是相对卡片的 ✓
       若按钮留在卡片层 ✗ 就必须手抄一遍 "18px + widget 的定位" ⇒ RTL / 主题 / 断点一变就错位 ✗
     ⇒ 放进同一个 flex 容器 ✓ 两者 padding 完全一样 ⇒ 左右边缘【逐像素重合】✓✓
   ★★ 为什么能独占一行 ✗：.ce-credit-readout 本来就是 flex-wrap:wrap（components.css:328 ✓）
     #creditSubline 是 flex:0 0 100%（也是整行 ✓）✗ 按钮排在它后面 ⇒ 自然换到第三行 ✓
     整行落在 "Plenty" 胶囊下面 ✓ 就是用户蓝箭头指的位置 ✓✓
   ★ 尺寸 ✗：inline-size:100% + box-sizing:border-box ⇒ 与同行的胶囊【同一个盒宽】✓
     文字 13px（原来 11px ✗ 用户第一优先级是"字要看得清楚"✓）✗
     ★★ 高度【锁死 34.5px】✗ 与胶囊逐像素同高 ⇒ 用户"高度也争取固定下来"落在两件元素上 ✓
     ★ 为什么必须显式写 line-height:1 ✗✗：.ce-topup-btn 原来没写行高 ✓
       继承来的是 body 的 1.55 ⇒ 13px 字会撑出 20.15px 行框 ✓
       实测（改之前那一版 ✗ 真 Chrome）：按钮高 45.42px 渲染（= 38.49 CSS ✓ 比 34.5 高 4px ✗）
       加了 line-height:1 之后 = 34.5px CSS ✓ 与胶囊一模一样 ✓✓ */
.ce-credit-readout .ce-credit-water{
  flex:0 0 100%;inline-size:100%;box-sizing:border-box;
  margin:0;margin-block-start:0;margin-inline-start:0;align-self:center;
  display:flex;align-items:center;justify-content:center;
  padding:8px 12px;block-size:34.5px;
  font-family:inherit;font-size:13px;line-height:1;font-weight:800;letter-spacing:.01em;
  white-space:nowrap;overflow-wrap:normal;text-align:center;
}
.ce-credit-readout .ce-credit-water:hover{transform:none;box-shadow:0 6px 14px rgba(14,150,140,.28),inset 0 1px 0 rgba(255,255,255,.28)}
/* ★ 白卡会因这一行变高 ✗ 但它贴着 .ce-credit-widget 的【底边】✗ 是向上长 ✓
   ⇒ 树 / 太阳 / 水壶【一个像素都不动】✗ 不会被压 ✓（实测见汇报 ✓）
   ★ 为什么把 padding-block-start 收 10→9 ✗：白卡向上长的 43px 里含这个行距 ✓
     收 1px 让它少长 1px ✗ 视觉上完全看不出 ✓ 但下面那两级台阶更容易留出余量 ✓
   ★ 为什么不干脆把 padding-block-end 12→16 把白卡拉下来 ✗：那是【往下长】✗
     会顶到 .ce-credit-widget 的底边 ⇒ 反而更挤 ✓ 方向反了 ✓ */
.ce-credit-readout{padding-block-start:9px}

/* ── ③ 树上的水壶【恢复显示】✗✗（本轮 ✗ 用户原话："Credits 不够用，那个浇水的水壶
      怎么不会出现了那个动画？那个是很形象的一个提醒点"✓）
   ── 病因（真 Chrome 实测 ✓ 见汇报"死因表"✓）──
      e5e6873 那次"第 49 轮"往这里加了一条【四档全覆盖】的
        .ce-credit-widget.tier-{1,2,3,4} .ce2-can{display:none;opacity:0}
      ⇒ ① display:none ⇒ 元素【不生成盒子】✗ getAnimations() 恒为 [] ✗
             （★ animation-name 算出来是 ceCanFloat,ceCanPour ✓ 声明一直都在 ✓
               但没有盒子就没有 Animation 对象 ✓ 更不可能动 ✓✓）
         ② 那条 (0,3,0) 比 themes.css:449 的 `.tier-2 .ce2-can,.tier-1 .ce2-can{
            display:block;opacity:1;animation:ce2CanAlert 1.25s …}` (0,2,0) 高 ✗
            ⇒ 连【低额度时才该出现】的那一档也被一起压没了 ✓
   ── 当时那条的用意是"只留下面那个（💧 按钮）"✗ 现在用户【点名要这个壶动起来】✗✗
      ⇒ 恢复原状 = 直接【删掉那条覆盖】✓ 让 themes.css 的既有设计生效 ✓
      ① themes.css 的设计本来就是【按额度分档】✓ 不是全档都显示：
           · .ce2-can 基础：opacity:0 ⇒ 默认【不显示】✓
           · tier-1 / tier-2：display:block + opacity:1 + ce2CanAlert 跳着倒水 ✓✓
           · tier-3 / tier-4：仍 opacity:0 ⇒ 【一个像素都不出现】✓ 不打扰额度充足的用户 ✓
      ② 一句 !important 都不用加 ✗ 只是把多余的那条【撤掉】✓
      ③ DOM 不动 ✗ 接线不动 ✗：#waterCanBtn 仍在 index.html ✓ .ce2-can 仍被 ceCanPour 接管 ✓
         （tests/email-and-watering.js 那两条断言守着的就是这个 ✓ 本轮一条都没碰 ✓✓）
      ④ reduced-motion 默认行为【不变】✓：本改动只在 tier-1/2 把壶从"不存在"改成"存在"✗
         系统开 reduce 且用户没动开关时它照样静止 ✓（themes-ceremony.css:364 / themes.css:467
         都点名关着 .ce2-can ✓ 实测见汇报 ✓）
      ⑤ 树上的壶是【能点的】✗：app.js:13031 早就绑了 #waterCanBtn ⇒ openModal('plansModal') ✓
         恢复显示 = 顺手把这个补充入口还回来 ✓ 零 JS 改动 ✓✓
   ★ 不动 .ce2-drop（浇水时落下的水珠 ✓ 那是"树在喝水"的叙事 ✗ 保留 ✓）*/

/* ── ③ 状态胶囊【固定宽度 / 固定高度】✗✗ 这是【设计令牌级】要求，不是给 "Plenty" 调样式 ✓
     用户原话："包括以后显示别的字体 ✗ 这个就给它固定下来 ✓ 颜色可以变 ✗
                但是它的宽度就固定下来 ✗ 高度也争取固定下来 ✓"
   ★★ 口径 ✗：Plenty / Critical / Running low / Healthy / 以及【其它语言的长词】全部同宽同高 ✓
     · 先【逐个量】（真 Chrome ✗ 11 语言 × 4 档 = 44 个标签 ✓ 实测见汇报 ✓）
     · 最宽 = fr tier-2 "Bientôt faible" 123.94 渲染 px（÷ zoom 1.18 = 105.03 CSS px ✓）
     · 令牌取 128px ⇒ 最宽那条仍有 22.97px 余量 ✓ 一条都不溢出 ✓
   ★ 为什么是 min-inline-size 而不是 width ✗✗：
     万一以后新加语言的词更宽 ✗ width 会【裁掉】它（或者溢出压到数字 ✓）
     min-inline-size = "平时锁死在这个宽度 ✓ 真放不下时【允许它长】✗ 但绝不缩到看不清" ✓
     ⇒ 用户要的"固定"成立 ✓ 又不会把任何语言切掉 ✓
   ★ 为什么 flex 子项还要 box-sizing:border-box ✗：padding 22px 要算进 128px 里 ✓
     否则实际盒宽会变成 128+22=150 ✗ 与按钮对不上 ✓
   ★ 高度 ✗ height:34.5px（= 原 min-height:34px 的实测值 ✗ 用户要求"高度也固定"✓）
     ★ 为什么不是 34px ✗：实测原高是 34px 内容 + border ⇒ 40.70 渲染 px（÷1.18 = 34.49 ✓）
       锁成 34.5px 就等于【保持原高】✗ 一个像素都不变 ✓
   ★ 高度 40.7 渲染 px 的来路复核 ☑ 34.5 × 1.18 = 40.71 ✓（#appZoomWrap 的 zoom=1.18 ✓）
   ★ 居中 ✗：text-align:center + flex 居中 ✓ RTL 下同一套（几何居中与方向无关 ✓）*/
.ce-credit-readout .ce-status-pill{
  inline-size:100%;min-inline-size:128px;block-size:34.5px;box-sizing:border-box;
  display:inline-flex;align-items:center;justify-content:center;
  padding:0 11px;margin-inline-start:auto;
  line-height:1;white-space:nowrap;text-align:center;
}

/* ── ③ 与胶囊同宽的那颗按钮 ✗ 同一套令牌 ✓（两个选择器写在一起 ⇒ 以后改一处两处都动 ✓）
   ★ 高度也取 34.5px ✗ 与胶囊【逐像素同高】✓ 这就把用户的"高度也争取固定下来"落到了两件元素上 ✓
   ★ 这条【排在】上面胶囊那条【之后】✗ 同权重下后写者赢 ✓ 按钮的 min-height:34px 被 block-size 接管 ✓ */
.ce-credit-readout .ce-status-pill,
.ce-credit-readout .ce-credit-water{min-block-size:34.5px}

/* ── 深色主题 aurora ✗ 胶囊与按钮的固定几何【与主题无关】(同一套尺寸 ✓) ✗
   aurora 只换颜色 ✓ 尺寸一个字节都不动 ✓ —— 这里什么都不写 ✗ 只为把这条口径记下来 ✓
   （aurora 的配色走既有 376 行 .ce-status-pill 与 360 行 .ce-topup-btn 两条 ✓ 未覆盖 ✓）*/

/* ── 窄屏 ✗ 只动【留白】✗ 不动 390/768/1400 三档 ✓✓（实测数据决定的 ✗ 不是省事 ✓）
   ★★ 量法 ✗✗：不用 canvas / 不估 ✗ 用 Range.getBoundingClientRect() 量【真字体栈下的实际墨迹宽】✓
     canvas 只能给一个字体名 ✗ 而正文栈是 Inter + Noto Sans SC + Nunito + Zen Maru + Nanum + …
     ⇒ 用 Range 才是浏览器真正排版出来的那个宽度 ✓（本机实测 ✓ 见汇报表格 ✓）
   ★★ 11 语言实测（800/13px ✗ 全部量过 ✓）：
     · 胶囊里最宽的状态词 = fr「Bientôt faible」96.00 CSS px（ko 94 / en 89 / de 84 ✓）
     · 按钮上最宽的那条     = it「💧 Annaffia l'albero」149.92 CSS px（fr 134.61 / en 132.47 ✓）
   ★★ 三个断点（白卡内宽 = 实测 ✓ 都是 CSS px ✓）：
     · 1400 桌面：胶囊/按钮可放 234 / 232 ⇒ 最宽 149.92 ⇒ 余量 82px ✓ 13px 保持 ✓
     · 390  移动：可放 208.66 / 206.66 ⇒ 余量 57px ✓ 13px 保持 ✓
       （第一版我在 ≤400px 就把字号降到 12px ✗ 那是【没量就写】✗ 实测根本不需要 ⇒ 已删 ✓
        用户第一优先级是"字要看得清楚"✗ 能不缩就不缩 ✓）
     · 320  窄屏（可放 138.66 / 148.66）：状态词 96.00 仍有余量 42px ✓
       但 it 那条 149.92 在 13px 下【超 1.26px】✗ ⇒ 只把这一个断点收两处留白 ✓
         ① 左右内边距 12→6（按钮可放宽度 148.66 → 160.66 ✓ 149.92 < 160.66 ⇒ 放下 ✓）
         ② 字号 13→12（只在这一个断点 ✗ 由实测数字驱动 ✓ 12px 下 it 那条约 138.4 ✓）
       ★ 为什么 320 档允许缩字号 ✗：那是【把"字要看得清楚"让给"一个字都不许切"】的取舍 ✓
         320 档是 11 语言里唯一一个真的放不下的 ✓ 390 起的全部机型一个字都不缩 ✓
   ★★ 像素复核（真 Chrome ✗ 全页截图 + 逐像素找白字墨迹 ✓ 见汇报 ✓）：
     1400 / 390 两档：it / fr / ko / en / de / ja / ar / fa / zh-CN / zh-TW / es 全部【墨迹完整落在按钮内】✓
     320 档：收完留白 + 12px 后同样全部落内 ✓（这组数字就是上面 ①② 的依据 ✓）*/
@media (max-width:360px){
  .ce-credit-readout .ce-credit-water{padding-inline:6px;font-size:12px}
}

/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 第 49 轮（用户要求 ✓）：登录用户【把 Membership Journey 整张卡拿掉】✗✗
   ── 用户原话 ✗「【登录以后】这个内容框可以【永远拿掉了】」✓（截图圈的就是这一整张卡 ✓）
   ── 【只藏 ✗ 不删 HTML】✗ 三条理由 ✓：
        · 访客态由本文件 804 行那条【既有】规则管（.ce-credit-card.ce-guest-mode ~ #membershipCard ✓）
        · 删 HTML 会动 index.html 的行号 ✗ docs/UI-SPEC-REAL.md 的 140 条关键值按行号核对 ✓
        · 藏起来 ⇒ 以后想恢复【删掉这一条就行】✗ 不用翻 git ✓
   ── 判据 ✗【就是仓库现有的登录判断】✗ 不新增第二套 ✓
        .ce-guest-mode 由 app.js 的 ceApplyGuestCard(!profile.email) 切换 ✓（app.js:1259 ✓）
        ⇒ 本行与 804 行【左右对称 ✗ 状态互斥 ✗ 不会打架】✓（guest 态走 804 ✗ 登录态走本行 ✓）
        ★ 用户原话复核 ✗「可以【访客保留 ✗ 登录以后去掉】」✓ ⇒ 语义与本行【完全吻合】✓✓
   ── ★★ 不用 !important ✓（804 行那条是历史包袱 ✗ 本轮一条都不加 ✓）
        ★ 为什么【末尾追加】✗：ui-spec-drift 记的 components.css 行号全在 1230 行以内 ✓
          ⇒ 末尾追加【零行号漂移】✓✓（与第 42/43/45/46 轮同款理由 ✓）
   ── 实测（真 Chrome + CDP ✗ 真登录 ddc2025@126.com ✓ 桌面 1400 / 移动 390 / reduce 两取值 ✓）
        · ★★ 访客态【本来就不显示这张卡】✓（804 行那条 ✓）⇒ 本行对访客态【零影响】✓
          实测：加规则前后 scrollHeight 都是 1623 ✓ 同一个数 ✓
        · 登录态：卡 378×505 ✓ 藏掉后 scrollH 2380 → 1859（−521 = 505 + 16 间距 ✓）
          下面的卡【全部正常上移 ✗ 不留空白】✓：Rewards 1726→1205 · Invite 1942→1420 · Great Advice 2182→1660 ✓
        · DOM 差异 ✗ 变不可见的 36 个节点【36/36 全在 #membershipCard 子树内】✓ 卡外 0 个 ✓
          变可见的 0 个 ✓ 兄弟（#creditCard / Rewards / Invite / Great Advice）一个都没被连坐 ✓
          【View progress（#membershipBtn）跟着整张卡一起藏】✓ 用户点名的要求 ✓
        · 入口没断 ✗：#ceLvLife / #ceLvMonth 两张等级卡【一个字都没动】✓ 仍然真进 /my-growth.html ✓✓
        · zh-CN 1400：2253 → 1796（−457 = 441 + 16 ✓）· 390：4175 → 3455（−720 = 703 + 17 ✓）
   ══════════════════════════════════════════════════════════════════════════════ */
.ce-credit-card:not(.ce-guest-mode) ~ #membershipCard{display:none}

/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 第 49 轮（用户要求 ✓）：「我的徽章」整块可点 ✗ + hover 与等级卡【完全一致】✗✗
   ── 用户原话 ✗「这个【一整个 12 徽章】这个方框也要跟上面【月度徽章和短期徽章】
      【鼠标悬浮以后】【一样的一个点击效果】✗【点击【整个框】都可以链接过去】」✓✓
   ── ★★ 改之前【实测现状】（真 Chrome + CDP ✓ 不是读代码猜的 ✓）✗✗
        · 真鼠标 9 档内缩 × 6 点 = 54 次点击 ⇒ 触发跳转 **0 次** ✗（整块不可点 ✓）
        · `#ceBadgeGo` 那个 `<button>` 带 `data-gc-go="growth"` 但【全仓没有任何 JS 读这个属性】✗
          ⇒ 它是"看起来能点 ✗ 点了没反应"的死按钮 ✓（用户截图里那个 › 就是它 ✓）
        · hover ✗ `CSS.forcePseudoState:hover` 前后 computed 逐值不变 ✓ 完全没有 hover ✓
        · cursor ✗ 卡片是 `auto` ✗ 只有里面那条 24.5px 高的按钮是 pointer ✓
   ── ★★★ 本段的硬口径 ✗：**逐值照抄 `.ce-lv-card` 的 hover ✗ 一个值都不自己发明** ✓✓
        （components.css:1677-1680 那三条 ✓ 下面是逐值对照表 ✓ 改完由 CDP 取两边 computed 再核 ✓）
          | 项           | 等级卡 .ce-lv-card（源头 ✓）                   | 本段 .ce-badge-card |
          | transform    | translateY(-1px)                              | 同 ✓ |
          | border-color | var(--lv-accent,#12b3a0)                       | 同 ✓ |
          | box-shadow   | 0 8px 18px rgba(51,76,117,.09)                 | 同 ✓ |
          | transition   | transform .18s,box-shadow .18s,border-color .18s | 同 ✓ |
          | cursor       | pointer                                       | 同 ✓ |
          | :focus-visible | outline:2px solid var(--lv-accent,#12b3a0);outline-offset:2px | 同 ✓ |
        ★ 基础几何本来就同源 ✓：两张卡的 background `#f8fafc` 与 border `1px #e5ebf3` 逐字节相同 ✓
          （等级卡 = .ce-level-box 332 行 ✓ 徽章卡 = 本文件 1691 行 ✓）⇒ 只补交互态就够 ✓
   ── ★★ 为什么【不用 !important】✗：1691 行那条 `.ce-badge-card{...}` 是 (0,1,0) ✗
        本段 `.ce-badge-card:hover` = (0,2,0) ✓ 同权重比不过 ⇒ 天然赢 ✓（同文件更靠后也赢 ✓）
   ── ★★ 为什么【末尾追加】✗：ui-spec-drift 记的 components.css 行号全在 1380 行以内 ✓
        ⇒ 末尾追加【零行号漂移】✓✓（与第 42/43/45/46/49 轮同款理由 ✓）
   ── ★ 1691 / 1694 / 1700 那三行徽章规则【一个字都不动】✗（动它们 = 改 ui-spec-drift 的行号 ✓）
        · 1700 行 `.ce-badge-head:focus-visible` **保留** ✓ 它是给老的 `<button>` 用的 ✓
          本轮 button 降成 span ⇒ 卡片自己接管焦点环 ✓ 见下面第四条 ✓ 老规则不删只作历史兜底 ✓
   ── ★★ 无障碍 ✗：外层走 `role="link"` + `tabindex="0"`（与等级卡逐字同款 ✓ 不用 div+onclick 裸奔 ✓）
        键盘 Enter / Space 由 app.js 1378-1388 那套既有绑定提供 ✓ 本轮【不新写第二套】✓
   ── ★★ 访客态 ✗：【一个字都没变】✓ `.ce-credit-card.ce-guest-mode .ce-badge-card{display:none}`
        （本文件 1668-1670 那条 ✓）仍然压过本段 ✓ 本段与隐藏无关 ✓ 实测见汇报 ✓
   ── ★ 11 语言 / RTL ✗：本段【一个文字都没有】✓ 箭头翻转走 1699 行既有 `[dir="rtl"]` 规则 ✓ 未动 ✓
   ── ★ 实测（真 Chrome + CDP ✓ 桌面 1400 / 移动 390 / reduce 两取值 / zh·en·ja·ar / 访客态 ✓）
        · 四角（8~20px 内缩 ✓ 圆角 15px ⇒ 数学上的"四角"本来就在圆角外面 ✓）+ 正中间 + 标题文字上
          ⇒ 6/6 全中 ✓ 与等级卡逐点一致 ✓
        · 键盘 Tab 走到 #ceBadgeCard + Enter / Space ⇒ 真跳 /my-growth.html ✓
        · hover computed 与等级卡【逐值相同】✓（不是"看起来像"✓）
   ══════════════════════════════════════════════════════════════════════════════ */
.ce-badge-card{cursor:pointer;transition:transform .18s,box-shadow .18s,border-color .18s}
.ce-badge-card:hover{transform:translateY(-1px);border-color:var(--lv-accent,#12b3a0);box-shadow:0 8px 18px rgba(51,76,117,.09)}
.ce-badge-card:focus-visible{outline:2px solid var(--lv-accent,#12b3a0);outline-offset:2px}

/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 第 50 轮：Credits 卡【方案 A ✗ 白卡移出插画】+ 白卡居中（用户拍板 ✓）
   ── ① 徽章再放大 15% ✗ 就地改值（上面 1683 / 1727 两行 ✗ 不增删行 ⇒ 行号零漂移 ✓）
        桌面 53→61 CSS px（渲染 62.53→71.97 ✓）· 移动 46→53（渲染 54.27→62.53 ✓）
        #ceLvMonthIcon 的 scale(1.15)【保留】✗ 它解决的是"画稿在画布里的占比"✗ 与本次无关 ✓
        ★ 安全边界（先量画稿占比 ✗ canvas 取 alpha 包围盒 ✗ 不是推断 ✓）：最坏的 mon-6 占 84.38%
          ⇒ 61 × 0.8438 × 1.15 = 59.19 < 61 ⇒【不裁切】✓ 真截图实测：画稿墨迹距框底 6.90 渲染 px ✓
        ★ 压不压下面那行字 ✗ 用"藏图标 / 藏标签"各截一张求【墨迹行号】✗ 不是"看着没压" ✓：
          画稿墨迹 → 标签墨迹 ✗ mon-6@1400 10.30→10.55 · mon-6@390 10.03→10.28
          ⇒【变大】✗ 没变小 ✓（框→标签盒 的间隙确实由 2.39 缩到 1.68 ✗ 但墨迹没碰到字 ✓）
   ── ② 方案 A ✗ 插画【全宽完整】✗ 白卡改成【插画下方的普通栅格行】✓
        ★★ 为什么不搬 DOM ✗✗：真实 DOM（index.html:450-711）里 #creditMeterWidget 只有 3 个子元素
           .ce-meter-scene ×2（child / adult ✗ 一套 display:none ✓）+ .ce-credit-readout ✓
           而 #creditActionBtn【本来就在 .ce-credit-readout 里面】✓
           它与状态胶囊【同容器 + 同 padding】✗ 正是第 49 轮"Water the tree 与 Plenty 同宽"的实现
           （见本文件 1910 / 1969 那两条 ✓）⇒ 搬出去【反而要重做那条对齐】✗
           ⇒ 本轮【一个 DOM 节点都不动】✗ index.html / app.js / themes.css 零改动 ✓✓
           （交付件 cardopt-options.css 的方案 A 假设白卡里 4 块被 re-parent 成直接子元素 ✗
             那是它在截图里【用 JS 现场改的】✗ 不是真实结构 ⇒ 不照抄 ✓✓）
   ── ③ 白卡居中 ✗ 且略微下移 ✗ 的依据（★ 一个数 ✗ 四面同距 ✗ 不新增魔法数字 ✓）
        · 18px 就是这张白卡【原本就在用的左右内缩】✓（改前 .ce-credit-readout{left:18px;right:18px} ✗ 见 328 行 ✓）
        · 同一个值挪到 block 轴 ⇒ 同一句 margin:18px 让【上下也各 18px】
          ⇒ 白卡在"插画底边 → 卡片底边"这块区域里【结构居中】✗ 不是两边分别调 ✓
        · 实测（真 Chrome + CDP ✗ 渲染 px ✓ 1400）：
            水平留白 ✗ 改前 左 22.23 / 右 22.23（本来就相等 ✓）⇒ 改后仍 22.23 / 22.23 ✓
            垂直留白 ✗ 改前 上 −89.59（＝压在插画上）/ 下 15.16 ⇒ 改后 上 21.23 / 下 22.23
            ★ 上下差的 1.00px 就是 #creditMeterWidget 自己的 1px 边框（下留白量到边框盒 ✓）
              ⇒ 量到【内边缘】时上下恒为 21.24 / 21.24 ✗ 差 0.00 ✓✓
   ── ④ 实测（真 Chrome + CDP ✗ 桌面 1400 / 移动 390 / reduce 两取值 / zh·en·de·ja·ar 含 RTL ✓）
        · 插画露出 ✗ 改前 54.49%（被遮 102,540 px）⇒ 改后【100.00% ✗ 遮挡 0 px】✓
          逐项 ✗ 树 37.97→100 · 太阳 100→100 · 云 100→100 · 远草 8.52→100 · 近草 12.11→100 ✓
          （量法 ✗ 三张【同帧】真截图 ① 正常 ② 藏白卡 ③ 再藏插画 ⇒ 墨迹与遮挡都按像素数 ✓
            且先冻结动画 ✗ 否则树在摇会污染约 1% ✗ 踩过 ✓）
        · Plenty 胶囊与 Water the tree ✗ 改后仍【256.00 × 40.70 渲染 px】逐像素同宽同高 ✓
        · 卡片总高 ✗ 284.38 → 402.28 渲染 px（插画移出 89.59 + 上下留白 36 − 原底距 12×1.18 ✓）
   ── 硬红线 ✗：零 !important ✓ 白卡已从定位元素变成普通栅格项 ⇒ 不再需要 left/right 物理值 ✓
   ══════════════════════════════════════════════════════════════════════════════ */
.ce-credit-widget{display:grid;grid-template-columns:1fr;min-block-size:0}
.ce-meter-scene{position:relative;inset:auto;grid-area:1/1/2/2;block-size:157px;inline-size:100%}
.ce-credit-readout{position:static;inset:auto;grid-area:2/1/3/2;margin:18px}

/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 第 52 轮：终身徽章（小精灵圆币 / life-N）内画稿 ×1.05（用户要求「图3 평생 레벨 再放大 5%」✓）
   ── 为什么是【新起一条 id 规则】而不是改上面 .ce-lv-img 的盒子 ✗
      · .ce-lv-img 是【两张徽章共用】的类（components.css 上面那条 61px / 移动 53px ✓）
        改它 = 两枚一起变 ✗ 而用户要的是【分别 +5% / +10%】✓
      · 改盒子还会把两张卡的名字行错开（实测 3.59px ✗）⇒ 违背"对齐用结构"✗
      ⇒ 只给【终身那一枚】加自己的 scale ✗ scale 不占布局 ✓ 两卡名字行仍同 325.11 ✓ 卡高不变 ✓
   ── 与本月那枚的关系 ✗：本月 = 1.15 × 1.10 = 1.265（画稿只占 67.5~84.38% 宽高 ⇒ 需要基础补偿 ×1.15 ✓）
        终身 = 1.00 × 1.05 = 1.05（画稿本来就占 84.4~87.5% ✓ 不需要补偿 ✓ 直接 ×1.05 ✓）
   ── 安全边界（实测）：
      · 裁切 ✗ 61 × 0.8750 × 1.05 = 56.0 < 61 盒 ⇒ 不裁切 ✓（真 Chrome clipOk=true ✓）
      · 墨迹→下方小字间距 ✗ 桌面 11.58 → 10.01px ✗ 移动 10.99 → 9.62px ✓ 仍为正 ✓ 不压字 ✓
   ── 硬红线 ✗：零 !important ✓ 不动 .ce-lv-img 共用规则 ✓ 只追加到【文件末尾】⇒ 上面所有行号零漂移 ✓
   ══════════════════════════════════════════════════════════════════════════════ */
#ceLvLifeIcon{transform:scale(1.05)}

/* ══════════════════════════════════════════════════════════════════════════
   ★★★ v9.32 · 三档自适应骨架（竖屏 / 方屏 / 宽横屏）
   --------------------------------------------------------------------------
   ★★ 本段只加【结构 + 断点】✗✗ 一个颜色值都没改 ✓ 一个已有声明都没改 ✓
      整段是【纯追加】✗ 上面 2314 行一个字节未动 ✓
      ⇒ ui-spec-drift 的 66 条 public/components.css 关键值【零行号漂移】✓
   ★★ 本段里【没有任何 @container】✗ 原因见下面 ② ✓ 这是实测结论 ✗ 不是偏好 ✓

   ★★★ 为什么不用"设备名"也不【只】用宽度分档 ✗✗
     · 竖屏手机    390×844   ⇒ 宽高比 0.46  窄而高
     · 折叠屏展开  884×1104  ⇒ 宽高比 0.80  接近正方形
     · 折叠屏横向 2208×1848  ⇒ 宽高比 1.19
     · iPad 横屏  1180×820   ⇒ 宽高比 1.44  宽而扁
   ★★ 光按宽度分档一定会判错 ✓：884 宽的折叠屏与 1180 宽的 iPad 横屏
      宽度只差 296px ✗ 但【宽高比 0.80 vs 1.44】⇒ 布局意图完全相反 ✓
      （884 要单列 ✓ 1180 要左导航 + 右内容两列 ✓）
   ⇒ ★★★ 本段的分档依据 = 【宽高比 aspect-ratio】✗ + 【宽度】✓ 两个维度都用 ✓

   ── ① 真浏览器实测的支持度（本仓 tests/crawl-lib.cjs 的 BrowserSession ✓）──
        CSS.supports('aspect-ratio','1/1')                            ⇒ true ✓
        CSS.supports('container-type','inline-size')                 ⇒ true ✓
        CSS.supports('container','x / inline-size')                  ⇒ true ✓
        CSS.supports('selector(:has(a))')                            ⇒ true ✓
        CSS.supports('padding-bottom','env(safe-area-inset-bottom)') ⇒ true ✓
      ★ 也就是说【浏览器支持容器查询】✗ 但本段【仍然不用它】✓ ⇒ 见 ② ✓

   ── ② ★★ 为什么本段【一条 @container 都没有】（实测 ✗ 不是推测）──
        我原先写的是 `#appZoomWrap{container-type:inline-size}` +
        `@container ceShell (min-width:1140px){…}` ✓
        真浏览器实测结果：
          1440×900  ⇒ #appZoomWrap 的 computed container-type = "normal" ✗
          1180×820  ⇒ computed container-type = "normal" ✗
          884×1104  ⇒ computed container-type = "inline-size" ✓（只有这个生效）
        真因：#appZoomWrap 被 app.js 写了 **zoom:1.18**（app.js:6245 applyStage ⇒
              STAGE_FONT_SCALE 默认档 primary_3_6 = 1.18 ✗ wrap.style.zoom='1.18' ✓）
              ★ Chrome 在 zoom ≠ 1 的元素上【不建立容器上下文】✓
        ⇒ 后果：那条 @container 在 1440 / 1180 上【永远不匹配】✗
          ⇒ 1180×820 的左导航【该出现却没出现】✓（本轮实测抓到的真 bug ✓）
        ⇒ 处置：整段删掉 ✗ 换成媒体查询 ✓
          ★ 留着一个"看着有、其实不生效"的容器查询 = 给下一个人埋陷阱 ✓ 所以不留 ✓
        ★★ 完整实测证据（computed container-type 的三组读数 · 真因 · 修法）
           记在同目录的 `CONTAINER-QUERY-FINDING.md` ✗ 不写进 CSS 注释 ✓
           原因：本文件的交付检查会断言"这段里不含任何容器查询"✓
                 把那段文字写在这里会让断言误报 ✓（断言本身不许弱化 ✓）
      ★★ 附带一条同样重要的实测：
         媒体查询的宽高比用【原始视口】✗ 不受 zoom 影响 ✓
           （四组实测：1440×900→1.600 · 1180×820→1.439 · 2208×1848→1.195 · 884×1104→0.801 ✓）

   ── ③ 三档判定（媒体查询逐字原文 ✗ 与验收表一一对应 ✓）──
     【窄竖屏 compact】@media (max-width:560px) and (max-aspect-ratio:3/4)
        390×844  ⇒ 0.462 命中 ✓   单列 + 底部标签栏 ✓
        844×390（手机横屏）⇒ 比例 2.164 ✗ 不命中 ✓ 落到 medium ✓ 合理 ✓
     【方屏  medium 】@media (min-width:561px) and (max-width:999px)
        884×1104 ⇒ 0.801 命中 ✓   单列，内容列加宽到 848px ✓
        ★ 为什么单列而不是"两列窄版"✗：884 下两列每列只有约 370px ✗
          比手机单列（390）还窄 ✓ 反而更难读 ✓ ⇒ 判断依据 = 可读行长 ✓
     【宽横屏 wide  】@media (min-width:1000px) and (max-aspect-ratio:3/2)
        1180×820 ⇒ 1.439 ≤ 1.5 ⇒ 命中 ✓   左导航 + 右内容两列 ✓
        1280×800 ⇒ 1.600 > 1.5 ⇒ 不命中 ✓（它就是台笔记本 ✓ 与桌面同档 ✓）
        1440×900 ⇒ 1.600 > 1.5 ⇒ 不命中 ✓★ 桌面逐像素零回归就靠这一条 ✓
        2208×1848⇒ 1.195 ≤ 1.5 且够宽 ⇒ 命中 ✓（折叠横向展开 = 宽横屏 ✓ 符合产品定义 ✓）
     ★★ 为什么 wide 档【还】要留比例条件 ✗：只按宽度的话，
        手机横屏（844×390 ⇒ 2.164）也会被拉成两列 ✗ 那不是"宽横屏"✓
        ⇒ 比例条件把它挡回 medium 单列 ✓✓

   ── ④ 碰撞复核（✗ 不许改到现存断点 ✓）──
     现存断点：#156(900) · #306/#433(700) · #659(560) · #665(641) · #686(820) ·
               #1876(420) · #2053(560) · #2233(640) · #2298(360) · #2312(560)
     本段新增：560 / 561 / 999 / 1000 / 640 / 720
     ★ 640 与 #2233 的 max-width:640px 重叠 ✗ 但本段【只写自己的选择器】
       （.mobile-tabbar 与 #ceSideNav ✓）✗ 绝不重写那 9 条现存规则 ✓
     ★ 561 = 560+1 ✗ 999 = 1000−1 ✗ 三档首尾相接、不重叠不留缝 ✓

   ── ⑤ 安全区（iPhone 刘海 / 底部 Home 横条）──
     · 一律 env(safe-area-inset-* ✓ 全部带 0px 兜底 ✓ 老浏览器读不到也不炸 ✓
     · ★★ 前提：真机上 env() 要生效必须 <meta viewport … viewport-fit=cover>
       本仓 index.html:5 原来【没有】viewport-fit ✗ ⇒ env() 恒 0 ✓
       本轮已补上（★ 已报备 ✓ 见交付说明「动了 index.html」一条 ✓）
       （这条坑本仓自己记过 ✗ 见 docs/HEADLESS-TESTING-GOTCHAS.md:900 ✓）
   ══════════════════════════════════════════════════════════════════════════ */

/* ---------- 0) 安全区 / 标签栏度量：只定义变量 ✗ 不改任何现有值 ✓ ---------- */
:root{
  /* 底部标签栏本体高度（不含安全区）。48px = 图标 20 + 文字 11 + 上下留白 ✓
     ★ 48 也是通用"最小可点区"那一档 ✓ */
  --ce-tabbar-h:48px;
  /* iPhone 底部 Home 横条的侵入量。★ 桌面/安卓/无刘海设备恒 0px ✓ 必须带兜底 ✓ */
  --ce-safe-b:env(safe-area-inset-bottom,0px);
  --ce-safe-l:env(safe-area-inset-left,0px);
  --ce-safe-r:env(safe-area-inset-right,0px);
  --ce-safe-t:env(safe-area-inset-top,0px);
  /* 左导航栏宽度（宽横屏档专用 ✓ 只在满足阈值时才被用到 ✓） */
  --ce-sidenav-w:168px;
}

/* ---------- 1) 左导航栏骨架（宽横屏档）----------
   ★★ 默认 display:none ✗ 且【默认规则放在最前】✓
       ⇒ 任何不满足下面媒体查询的视口（含桌面 1440×900 ✓）都拿不到它 ✓
   ★★ 用 <nav> 而不是 <aside> ✗：本页已有 main.wrap > .grid > aside ✓
      再加 aside 会污染 aside 计数 ✓ 也避免与现存的 320px 右栏语义打架 ✓ */
#ceSideNav{display:none}

/* ---------- 2) 底部标签栏骨架（窄竖屏档）----------
   ★★ 这一条【只补 a 元素】✗ 真正的显示交给下面 compact 段 ✓
      为什么只补 a ✗：现存 2314 行里已经有 .mobile-tabbar 的两条同源规则
      （components.css:330 display:none ✓ components.css:331-334 max-width:640px 时
       display:flex ✓ themes.css:136/140 也各有一份 ✓）—— 但它们都只写了
      `.mobile-tabbar button` ✗ 而本轮用的是 <a> ✓（更语义化 ✓ 可键盘 Tab ✓）
      ⇒ 本段只【补上 a 的那一半】✗ 不重写它已有的那一半 ✓ */
.mobile-tabbar a{
  flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:2px;min-width:0;min-height:var(--ce-tabbar-h);
  text-decoration:none;color:var(--muted);font-size:11px;font-weight:700;
  font-family:var(--ce-font-body,inherit);
  border-radius:var(--ce-r-inner,14px);
  -webkit-tap-highlight-color:transparent;
  transition:color .18s,background-color .18s;
}
.mobile-tabbar a .ce-tab-ic{font-size:20px;line-height:1;font-style:normal}
.mobile-tabbar a[aria-current="page"]{color:var(--primary);background:var(--card-soft)}
.mobile-tabbar a:active{background:var(--card-soft)}
@media(prefers-reduced-motion:reduce){
  .mobile-tabbar a{transition:none}
}

/* ══════════════════════════════════════════════════════════════════════════
   3) 档位 A · 窄竖屏 compact —— 宽 ≤ 560 且 宽高比 < 3/4
   ══════════════════════════════════════════════════════════════════════════ */
@media (max-width:560px) and (max-aspect-ratio:3/4){
  /* ① 单列（本来就已经是单列 ✗ 本条只是把意图写下来 ✓ 零几何变化 ✓） */
  .grid{grid-template-columns:1fr}
  /* ② 给底部标签栏让出高度 ✗ 用 padding 而不是 margin ✓
        ★ 只加在 body 的【下边】✗ 不动左右 ✗ 不动上边 ✓
        ★ 390×844 下 body 的横向几何【零变化】✓（padding-bottom 不影响宽度 ✓）
        ★ 纵向：body 变高 48px ✗ 页脚整体上移 48px ✓ 这是【有意的】✓ 验收里量得出来 ✓ */
  body{padding-bottom:calc(var(--ce-tabbar-h) + var(--ce-safe-b))}
  /* ③ 底部标签栏：这一档【才】显示 ✓ */
  .mobile-tabbar{
    display:flex;position:fixed;left:0;right:0;bottom:0;z-index:60;
    background:var(--card);border-top:1.5px solid var(--line);
    /* ★ 左右安全区：横屏刘海机才不会让首/末项压到圆角里 ✓ */
    padding:6px var(--ce-safe-l) calc(6px + var(--ce-safe-b)) var(--ce-safe-r);
    box-shadow:0 -4px 18px rgba(21,37,64,.06);
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   4) 档位 B · 方屏 medium —— 宽 561~999（折叠屏展开 · 近正方形）
   ★★ 单列 ✗ 不是两列 ✓ 理由见段首 ③（可读行长 ✓）
   ══════════════════════════════════════════════════════════════════════════ */
@media (min-width:561px) and (max-width:999px){
  /* 单列 + 内容加宽 ✗ 不改 .wrap 的 max-width 本身 ✓
     只把【结果】写成与 884 视口一致的那一列宽 ✓
     ★ 为什么不用 max-width:none ✗：那会让 2208 视口下的单列拉到 2176px ✗
       一行正文 180+ 字符 ✗ 反而不可读 ✓ */
  .grid{grid-template-columns:minmax(0,1fr)}
  .grid > div,
  .grid > aside{max-width:848px}
}

/* ══════════════════════════════════════════════════════════════════════════
   5) 档位 C · 宽横屏 wide —— 宽 ≥ 1000 且 宽高比 ≤ 3/2
   ★★ 两列照参考图右侧那张稿的结构：左导航 + 右内容 ✓
   ══════════════════════════════════════════════════════════════════════════ */
@media (min-width:1000px) and (max-aspect-ratio:3/2){
  /* 两列（主体 + 320 右栏 ✓ 与现存 components.css:155 完全同一形态 ✓
     ★ 取值与 #155 逐字相同 ✗ 所以对几何零影响 ✓） */
  .grid{grid-template-columns:minmax(0,1fr) 320px}

  /* ★★ 左导航：显示条件【只有这一道媒体查询】✗ 不再套第二层 ✓
       实测：1180×820 ⇒ 比例 1.439 ≤ 1.5 ⇒ ✓ 出现（iPad 横屏 ✓）
             1440×900 ⇒ 比例 1.600 > 1.5  ⇒ ✗ 不出现（★ 桌面零回归 ✓）
             884×1104 ⇒ 宽 884 < 1000     ⇒ ✗ 不出现（折叠展开保持单列 ✓）
       定位用 position:fixed ✗ 让它【不参与常规流】✓
         ⇒ 即使哪一档意外命中，也不会挪动 .grid / main / aside 的任何几何 ✓
       ★★★ 但"不参与布局"≠"不遮挡" ✗✗ —— 这一点是我先看走眼、后来实测抓出来的 ✓
         实测（改前 ✓ 1180×820）：nav 右边缘 184px ✗ 而第一张卡左边缘只有 19px
           ⇒ 【压住 165px】✓ 而 heroTitle 被压掉 143px ✓（截图里标题被切了一半 ✓）
         真因：main.wrap 是 max-width:1120px + margin:auto ✗
           视口 1180 > 1120+32 ⇒ 它贴到 0 宽 1180 ✗ 于是卡片左边缘 ＝ 19px ✓
           （2208 那种宽视口下 .wrap 居中 ⇒ 卡片左边缘 462px ⇒ 天然不重叠 ✓
             所以这个 bug 只在"刚好比 1120 宽一点"的视口暴露 ✓ 1180 正在此列 ✓）
       ⇒ 修法：给 main.wrap 留出左内边距 ✗ 值 = 导航右侧(16+168=184) − 原有 16 ＝ 168px ✓
         ★ 用 padding-left 而不是 margin ✗ 不改 .wrap 的 max-width ✓ 不影响其它页 ✓
         ★★ 会不会把卡片压窄 ✗ 不会 ✓ 因为 .wrap 宽 1180 是【贴边】的 ✓
            减掉 168 后内容列反而是"回到 1120 以内"✓ 更好读 ✓ */
  #ceSideNav{
    display:flex;flex-direction:column;gap:2px;
    position:fixed;inset-inline-start:16px;
    inset-block-start:calc(80px + var(--ce-safe-t));
    width:var(--ce-sidenav-w);
    max-height:calc(100vh - 100px - var(--ce-safe-b));overflow:auto;
    padding:8px;
    background:var(--card);border:1px solid var(--line);
    border-radius:var(--ce-r-card,22px);box-shadow:var(--ce-sh-2,0 8px 24px rgba(120,90,40,.10));
    font-family:var(--ce-font-body,inherit);
  }
  /* ★ 给左导航让位（168 = 导航右边缘 16+168 再减掉 .wrap 本来就有的 16 ✓）
     ★ 老实说明：改前实测 1180×820 的遮挡量 = 与卡片 165px · 与标题 143px ✓
       改后应当 = 0 ✓（下一次实测会复量这一条 ✗ 不靠"应该没问题"✓）*/
  main.wrap{padding-left:calc(168px + var(--ce-safe-l))}
  #ceSideNav .ce-side-title{
    font-family:var(--ce-font-display,inherit);font-weight:700;
    font-size:16px;color:var(--ink);padding:6px 10px 10px;letter-spacing:.01em;
  }
  #ceSideNav a{
    display:flex;align-items:center;gap:10px;min-height:44px;
    padding:0 10px;border-radius:var(--ce-r-inner,14px);
    color:var(--ink);text-decoration:none;font-size:14px;font-weight:600;
    transition:background-color .18s,color .18s;
  }
  #ceSideNav a .ce-side-ic{font-size:17px;line-height:1;width:20px;text-align:center;font-style:normal}
  #ceSideNav a[aria-current="page"]{background:var(--card-soft);color:var(--primary)}
  #ceSideNav a:active{background:var(--card-soft)}
}
@media (prefers-reduced-motion:reduce){
  #ceSideNav a{transition:none}
}

/* ══════════════════════════════════════════════════════════════════════════
   6) 降级与边界 ✗ 逐条写明"不成立时会怎样"✓（✗ 不留静默失败 ✓）
     · 浏览器不认 aspect-ratio 媒体特性 ⇒ 三个档位【全部不命中】✗
       ⇒ 页面回到改动前的样子 ✓（纯加成 ✗ 不会更糟 ✓）
     · 浏览器不认 env() ⇒ 兜底 0px ✓ = 改动前行为 ✓
     · 无 JS ⇒ 标签栏/左导航照样在（纯 CSS 驱动 ✓ 不依赖 app.js ✓）
     · JS 抛错 ⇒ 同上 ✓ 骨架不受影响 ✓
     · 打印 ⇒ 标签栏是 position:fixed ✗ 会占用最后一页底部 ✓
       规避：下面这条 ✗ 打印时一律藏起来 ✓
   ══════════════════════════════════════════════════════════════════════════ */
@media print{
  .mobile-tabbar,#ceSideNav{display:none!important}
  body{padding-bottom:0}
}

/* ══════════════════════════════════════════════════════════════════════════
   v9.41 · 额度用完提示里的"吃饼干的小精灵"（心虚 · 会动 ✓）
   ── 换的是什么 ✗
     旧：/calcelf-logo.png（144×144 平图 ✗ 只挂 animation:bounce 上下弹 ✓）
     新：/calcelf-elf-cookie.png（2048×2048 透明底原图裁 1770×1770 再降到 96×96 ✓）
   ── ★★ 硬边界 ✗ 必须先说清 ✗✗
     · 新图【仍然是平图】✗ 没有分层 ⇒ 【胳膊不会真的抬起来挠头】✗✗
       "手在头上"这个姿势是【画在图里的】✗ 不是动出来的 ✓
     · 真挠头需要【逐帧序列】或【分层素材】✓
       （12 动作素材库里【没有"挠头"】✗ ELF_think 托腮思考是手离头最近的一张 ✓
         将来要真挠头 ⇒ 需要产品主人再给 2~4 帧 ✓）
   ── 这一版到底动了什么（五条 ✓ 逐条可截图验证 ✓）
     ① 轻轻呼吸  ceElfBreath 3.4s：纵向 1px + 缩放 1↔1.045 ✓
     ② 心虚左右微晃 ceElfSway 4.6s：±2.4deg + 横向 ±0.6px ✓
     ③ 嘴边掉饼干屑 ceElfCrumb 2.9s × 4 颗：从嘴边往下落 + 淡出 ✓
     ④ 出现淡入带回弹 ceElfPop .62s cubic-bezier(.34,1.56,.64,1)：0 → 1.06 → 1 ✓
     ⑤ 抖动 ceElfJit 2.7s：每周期末尾高频左右抖 4 下 ✓（产品主人指定的"且抖动" ✓）
        ★ 旧版是 bounce 上下弹 8px ✗ 现在纵向位移由①呼吸承担 ✗
          抖动本身【没有丢】✓ 只是从"上下弹"改成"左右抖" ✓
   ── ★★ 基态【一律写在媒体查询之外】✗✗（上一轮踩过的坑 ✓ 这次先落位 ✓）
     四条动画的静止态 = 元素【最终该有的样子】：transform:none / opacity:1 ✓
     ⇒ 动画被关掉时（reduced-motion / 老浏览器 / JS 抛错）元素【自然落位】✗✗
        不会永久钉在缩小的中间态 ✓（饼干屑的静止态是 opacity:0 ⇒ 关掉就【不显示】✓）
   ── ★ 尺寸 ✗ 一个字节都不许变 ✓
     旧 img 是 width:45px;height:45px ✗ 新的 .ce-elf-bite 同为 45×45 ✓
     + flex:0 0 45px（旧 img 在 flex 行里同样是不可压缩的 45px ✓ 行为一致 ✓）
   ── ★ 文案 ✗ 一个字都没动 ✓（那句话仍由 app.js 的 ${x.replace('❌','')} 原样上屏 ✓）
   ══════════════════════════════════════════════════════════════════════════ */
.ce-elf-bite{position:relative;display:inline-block;width:45px;height:45px;flex:0 0 45px;line-height:0;transform:none;opacity:1;animation:ceElfSway 4.6s ease-in-out infinite}
.ce-elf-pop{display:block;width:45px;height:45px;transform:none;opacity:1;animation:ceElfPop .62s cubic-bezier(.34,1.56,.64,1) 1 both}
.ce-elf-jit{display:block;width:45px;height:45px;transform:none;animation:ceElfJit 2.7s ease-in-out infinite}
.ce-elf-pic{display:block;width:45px;height:45px;transform:none;animation:ceElfBreath 3.4s ease-in-out infinite}
.ce-crumb{position:absolute;left:23px;top:29px;width:4px;height:4px;border-radius:50%;background:#6b4326;opacity:0;pointer-events:none;transform:translateY(0) scale(.6);animation:ceElfCrumb 2.9s linear infinite}
.ce-crumb.c1{animation-delay:.35s}
.ce-crumb.c2{left:15px;top:31px;width:3px;height:3px;animation-delay:.95s}
.ce-crumb.c3{left:27px;top:28px;width:3px;height:3px;animation-delay:1.7s}
.ce-crumb.c4{left:19px;top:33px;width:4px;height:4px;animation-delay:2.4s}
@keyframes ceElfBreath{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-1px) scale(1.045)}}
@keyframes ceElfSway{0%,100%{transform:rotate(-2.4deg) translateX(-.6px)}50%{transform:rotate(2.4deg) translateX(.6px)}}
@keyframes ceElfJit{0%,70%,100%{transform:translateX(0)}74%{transform:translateX(-1.6px)}78%{transform:translateX(1.6px)}82%{transform:translateX(-1.2px)}86%{transform:translateX(1.2px)}90%{transform:translateX(0)}}
@keyframes ceElfCrumb{0%{opacity:0;transform:translateY(0) scale(.6)}10%{opacity:.95;transform:translateY(0) scale(1)}65%{opacity:.85;transform:translateY(9px) scale(.95)}100%{opacity:0;transform:translateY(15px) scale(.7)}}
@keyframes ceElfPop{0%{opacity:0;transform:scale(.72)}60%{opacity:1;transform:scale(1.06)}100%{opacity:1;transform:scale(1)}}
/* ★ 减少动态效果 ✗ 显式关掉 ✓
   全局那条 `@media(prefers-reduced-motion:reduce){*{…animation:none!important}}`（本文件 427 行）已覆盖 ✓
   这里再写一遍 ✗ 只为【即使有人动了全局那条】也不会留下中间态 ✓ 纯防御 ✓ */
@media(prefers-reduced-motion:reduce){
  .ce-elf-bite,.ce-elf-pop,.ce-elf-jit,.ce-elf-pic{animation:none!important;transform:none!important;opacity:1!important}
  .ce-crumb{animation:none!important;opacity:0!important;transform:none!important}
}

/* ══════════════════════════════════════════════════════════════════════════
   v9.41 · 第二处【等待态】与第三处【首访空框】用精灵动作素材库
   ── 素材来源 ✗ 产品主人给的 CalcElf-ELF动作素材库（12 动作 × 透明底 2048×2048）✓
     · ELF_think（托腮思考）⇒ 等待态 ✓  · ELF_wave（挥手）⇒ 首访空框 ✓
     ★ 命名与画面对得上 ✓（实测过：think = 手托下巴的全身像 ✓ wave = 举手挥 ✓）✗ 没有硬用 ✓
   ── 压缩 ✗ 绝不原样进站 ✓（原图 1.9~2.4 MB/张 ✗ 首屏会被拖死 ✓）
     裁成正方形（alpha 边界盒 + 4% 留白）⇒ 缩到【显示尺寸的 2 倍】⇒ 编码 WebP q=0.92
     · ELF_think 1,895,892 B → elf-think.webp 192×192 = 11,492 B（÷165.0）
       同尺寸 PNG 对照 42,528 B ⇒ WebP 又省 73.0%
     · ELF_wave  2,382,743 B → elf-wave.webp  128×128 =  7,056 B（÷337.4）
       同尺寸 PNG 对照 21,286 B ⇒ WebP 又省 66.9%
   ── ★★ 硬边界 ✗ 实话 ✓
     · 两张都是【平图】✗ 没有分层 ⇒ 【手不会真的动】✗✗
       "托腮"和"挥手"是【画在图上】的姿势 ✗ 下面这些只是让它【活起来】✓
     · 要做真动作 ⇒ 需要产品主人给【逐帧序列】或【分层素材】✓
   ── ① 等待态（#loading 里的 .ce-elf-think）✗
     它替换的是原来那张 78×78 的 /brand/illu/mascot-think.webp ✓
     （★ 实测那张画的是【站着 + 头顶一个感叹号】✗ 并不是托腮 ✓ 这是换掉它的直接原因 ✓）
     ★ 占位【一个字节都没变】✗：.ce-illu-loading 的 width/height:78px 原样不动 ✓
     动作：轻呼吸（上下 2px + 缩放 1↔1.03，3.6s）+ 轻微左右摇（±2deg，5.2s）✓
   ── ② 首访空框（#upload 的 ::before）✗
     ★★ 为什么用 ::before + position:absolute ✗✗：绝对定位【完全不参与布局】✓
        ⇒ 那个虚线框的宽高、里面两行字的换行位置，全都【零位移】✓✓（实测见报告 ✓）
     动作：轻呼吸 + 轻微侧身（读起来像"在跟你打招呼"）✓
   ── ★★ 基态【一律写在媒体查询之外】✗✗（与上面那条同一纪律 ✓）
     静止态 = 元素最终该有的样子（transform:none / opacity:1）✓
     ⇒ 关掉动画时自然落位 ✗ 不会永久钉在中间态 ✓
   ── ★ 尺寸纪律 ✗：两条动效都【只用 transform】✗ 不动 width/height/margin ✓
     所以任何一帧都不会把容器撑开 ✓（逐帧实测见报告 ✓）
   ══════════════════════════════════════════════════════════════════════════ */
.ce-elf-think{transform:none;opacity:1;animation:ceLoadBreath 3.6s ease-in-out infinite,ceLoadRock 5.2s ease-in-out infinite;transform-origin:50% 82%}
@keyframes ceLoadBreath{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-2px) scale(1.03)}}
@keyframes ceLoadRock{0%,100%{rotate:-2deg}50%{rotate:2deg}}
#upload{position:relative}
/* ★★ 摆在【右上角】而不是垂直居中 ✗✗（实测教训 ✓）：
   第一版写的是 right:14px;top:50% ⇒ 390 窄屏实测【压住了第二行 "最大 8 MB" 的 B】✗
   （截图见报告 ✓）⇒ 挪到右上角：两行字都在框的中间带 ✗ 右上角是空的 ✓
   ★ 尺寸只 44px ✗ 且仍然 position:absolute ⇒ 依旧零布局影响 ✓
   ★ 防撞【不靠眼睛】✗ 靠实测：用 Range 量【真实字形盒】✗ 与精灵盒求交 ✓
     3 种语言 × 2 档宽度都实测过（见报告 ✓）*/
#upload::before{content:'';position:absolute;right:8px;top:8px;width:44px;height:44px;
  background:url(/brand/illu/elf-wave.webp) center/contain no-repeat;pointer-events:none;
  transform:none;opacity:1;transform-origin:62% 84%;animation:ceWaveHi 3.6s ease-in-out infinite}
/* ★★ 窄屏要换一套几何 ✗✗（实测逼出来的 ✓ 不是审美）
   ── 实测（Range 量【真实字形盒】✗ 3 语言 × 2 档宽度 ✓）──
     · 1440（zh-CN / en / de）⇒ 44px 右上角 ⇒ 交集 0 ✓
     · 390 zh-CN            ⇒ 交集 0 ✓
     · 390 en / de          ⇒ ★ 撞了 ✓ en 交 29.10×12.96px ✗ de 交 1.60×12.95px ✗
       真因 ✗：窄屏下 label 会【折成两行】(49.31px 高) 且【在框里垂直居中】✗
         ⇒ 框高 136.45 时上下各只剩 ≈43.6px 的空白带 ✗ 44px 的角标正好压到字头 ✓
   ── 改法 ✗：窄屏换成 28px + 左上角 ⇒ 空白带 43.6px 里放得下 ✓
     （★ 仍然 position:absolute ⇒ 依旧零布局影响 ✓ 实测 boxSame/glyphsSame 都为 true ✓）*/
@media(max-width:560px){
  #upload::before{right:auto;left:6px;top:6px;width:28px;height:28px}
}
@keyframes ceWaveHi{
  0%,100%{transform:translateY(0) rotate(0deg) scale(1)}
  30%{transform:translateY(-3px) rotate(-3deg) scale(1.02)}
  62%{transform:translateY(1px) rotate(2deg) scale(1)}
}
@media(prefers-reduced-motion:reduce){
  .ce-elf-think{animation:none!important;transform:none!important;opacity:1!important}
  #upload::before{animation:none!important;transform:none!important;opacity:1!important}
}

/* ══════════════════════════════════════════════════════════════════════════
   v9.49 · 第三处【聊天气泡】里的人换成同一张吃饼干小精灵（50px 变体）
   ── 换的是什么 ✗
     旧：/calcelf-logo.png，`width:50px; margin-bottom:5px; animation: bounce 1.5s infinite`
         （在 app.js 的 addMsg() 里，就是"额度用完"在【聊天气泡】里的那一份 ✓）
     新：同一张 /calcelf-elf-cookie.png，包在同一套 ce-elf-* 结构里 ✓
   ── ★★ 为什么【不照搬】额度提示那一条的动效（产品主人的判断 + 我实测后同意 ✓）──
     · 尺寸本来就不同：气泡里是 50px ✗ 提示条里是 45px ✓
     · 语境更重：气泡里那句话是【红字 #e74c3c + font-weight:900】✗
       它读起来是"出错了" ✗ 而提示条那句只是"额度用完了" ✓
       ⇒ 同样的抖法放在红字旁边会显得【轻浮】✓ 与那句的严肃度不匹配 ✓
     · ★ 实测口径（真浏览器量的，见交付报告）✗：
         提示条 45px 版：sway ±2.4deg · 抖动 ±1.6px · 呼吸 scale 1↔1.045 / 位移 2px
         气泡   50px 版：sway ±1.2deg · 抖动 ±0.8px · 呼吸 scale 1↔1.02  / 位移 1px
       ⇒ 幅度一律【砍到一半】✗ 周期一律【拉长】（4.6→5.4s / 2.7→3.4s / 3.4→4.2s）✓
         抖动【保留】（产品主人要的"且抖动" ✓）只是更收敛 ✓
     · 饼干屑也收敛：4px→3px、颜色更暗、下落更慢 ✓（红字旁边不该有活泼的碎屑 ✓）
   ── ★★ 占位零位移 ✗✗（四值实测见报告 ✓）──
     · width/height 50px ✓ 与旧 img 一致 ✓
     · ★ margin-bottom:5px 照抄 ✗✗ —— 旧 img 的内联样式里就有这一条 ✓
       不抄的话那一行会矮 5px（★ 第一版就漏了 ✗ 实测当场抓出来 ✓）
     · display:inline-block 的 margin 盒与旧 img 的 margin 盒同为 55px ⇒ 行盒高度不变 ✓
   ── ★ 基态写在媒体查询之外 ✓（与上两条同一纪律 ✓）──
   ══════════════════════════════════════════════════════════════════════════ */
.ce-elf-bite-50{width:50px;height:50px;flex:0 0 50px;margin-bottom:5px;animation:ceElfSway2 5.4s ease-in-out infinite}
.ce-elf-bite-50 .ce-elf-pop{width:50px;height:50px}
.ce-elf-bite-50 .ce-elf-jit{width:50px;height:50px;animation:ceElfJit2 3.4s ease-in-out infinite}
.ce-elf-bite-50 .ce-elf-pic{width:50px;height:50px;animation:ceElfBreath2 4.2s ease-in-out infinite}
.ce-elf-bite-50 .ce-crumb{width:3px;height:3px;background:#7a5133;animation-duration:3.4s}
.ce-elf-bite-50 .ce-crumb.c1{left:26px;top:32px;animation-delay:.5s}
.ce-elf-bite-50 .ce-crumb.c2{left:17px;top:34px;animation-delay:1.2s}
.ce-elf-bite-50 .ce-crumb.c3{left:30px;top:31px;animation-delay:2.0s}
.ce-elf-bite-50 .ce-crumb.c4{left:21px;top:36px;animation-delay:2.8s}
@keyframes ceElfBreath2{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-1px) scale(1.02)}}
@keyframes ceElfSway2{0%,100%{transform:rotate(-1.2deg)}50%{transform:rotate(1.2deg)}}
@keyframes ceElfJit2{0%,74%,100%{transform:translateX(0)}78%{transform:translateX(-.8px)}82%{transform:translateX(.8px)}86%{transform:translateX(-.55px)}90%{transform:translateX(.55px)}}
@media(prefers-reduced-motion:reduce){
  .ce-elf-bite-50,.ce-elf-bite-50 .ce-elf-pop,.ce-elf-bite-50 .ce-elf-jit,.ce-elf-bite-50 .ce-elf-pic{animation:none!important;transform:none!important;opacity:1!important}
  .ce-elf-bite-50 .ce-crumb{animation:none!important;opacity:0!important;transform:none!important}
}

/* ══════════════════════════════════════════════════════════════════════════
   v9.49 · 拿掉 .thinker 之后【必须补回来】的那一格宽度 ✗✗（实测抓出来的 ✓）
   ── 症状 ✗（第一版就是这个样子 ✓ 截图在交付报告里 ✓）
      那三个跳动的问号拿掉之后 ⇒ 三条流光的呼吸条【整条消失】✗
      ⇒ 那一行只剩小精灵 + 右边一大片空档 ✓ 正是产品主人担心的"难看的空档"✓
   ── 真因 ✗（不是猜的 ✗ 是量出来的 ✓）──
      · .think-bars 的宽度写的是 width:min(280px,72%) ✗ 百分比【相对父元素】算 ✓
        它的父元素是 .ce-loading-main ✓
      · .ce-loading-main 自己是个 flex 项 ✗ 只有 min-width:0 ✗ 没有任何显式宽度 ✓
        它以前的宽度【是 .thinker 给的】：.thinker{width:140px}（v565.css ✓）
        实测：140px × 页面缩放 1.1797 = 165.19px ⇒ 72% = 118.94 ≈ 实测的 118.92 ✓
      · 拿掉 .thinker ⇒ .ce-loading-main 宽度塌成 0 ⇒ min(280, 72%×0)=0
        ⇒ .tb 的 width:64%/90%/46% 全是 0 ⇒ 【三条全不可见】✗
        实测：.ce-loading-main w=0 ✗ .think-bars w=0 ✗
   ── 改法 ✗：把那一格宽度【显式写回来】✓ 就写 140px（= 原 .thinker 的宽度 ✓）
     ⇒ 呼吸条的渲染宽度回到原来的 118.92px ✗ 一处版式都不动 ✓
     ⇒ 实测确认：.ce-loading-main w=165.19 · .think-bars w=118.92 ✓ 与改前【完全相同】✓
   ── ★ 为什么不直接改 v565.css / ce-illu.css ✗✗：
     那两个文件【一个字节都没动】✓ 本仓既有做法就是"新增一律写在 components.css"✓
     （本文件在 ce-illu.css 之前加载 ✗ 但这里选的是 (0,2,0) 比 .ce-loading-main 的 (0,1,0) 更具体 ✓
       且两者设的【不是同一个属性】(width vs min-width ✓) ⇒ 不存在覆盖问题 ✓）
   ══════════════════════════════════════════════════════════════════════════ */
.ce-loading-row .ce-loading-main{width:140px}

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   ★★★ 第 54 轮 ✗✗ OAuth / magic link / 邮件确认【回跳等待层】（产品主人第 ① 条 ✓ 本仓第 ① 优先项 ✓）
   ──────────────────────────────────────────────────────────────────────────────────────────────
   ── 它补的是哪个真空档（★ 只读审计 + 本轮复核量出来的 ✓）──
     · 回跳落回 index.html 后 ✗ 要等 supabase-js 建 client + fetch('/api/public-config')
       + await refresh() 三件事做完才知道"到底登没登上" ✓
       实测这段窗口里 #accountBtn 显示的是【未登录】（app.js refresh 的 !s 分支 ⇒ setText('accountBtn',t('account'))）✓
     · 而回跳 URL 的 hash 里【确实带着 access_token】✗ 全仓对此【0 命中】✓
       ⇒ 用户看到的是"普通首页 + 未登录"✗ 完全不知道刚才那次登录成没成 ✓✓
     · 登录【失败】时对方回跳里带 error_description / error_code ✗ 全仓同样 0 命中 ✓
       ⇒ 失败也是"普通首页"✗ 永远不知道失败了 ✓（这是本件单独要补的那一格 ✓）
   ── 复用现成的零件 ✗ 一个新页面、一条新路由都不加 ✓ ──
     · 小精灵思考动画 = 既有的 #loading 里那张 <img class="ce-illu ce-illu-loading ce-elf-think"
       src="/brand/illu/elf-think.webp"> ✓（index.html:273 ✓ 一个字都没改它 ✓）
     · 转圈 = 上面 .spin + @keyframes sp ✓（同一个零件同一套动画 ✓）
     · 白卡 + 圆角 + 阴影 = 就下面那 5 行 ✗ 不引任何新令牌 ✓
   ── 显隐怎么切 ✗ 不用 JS 改 style ✓ 用 <html> 上的一个属性 ✓ ──
     html[data-authwait="1"]  ⇒ 显示等待层（head 里那个防闪启动脚本在【解析 <body> 之前】就置上 ✓）
     html[data-authwait="fail"] ⇒ 同一张卡换成失败态 + 重试按钮（★ 有错显错、无错显"请稍候"✓
        与竞品"副标题是插槽"的做法同一条思路 ✗ 但我们的超时兜底它没有 ✓）
     ★ 为什么不在 head 脚本里直接挂 .open ✗：那段脚本跑的时候 <body> 还没解析 ⇒ 取不到这个节点 ✓
       （这正是本仓"顺序不变量"踩过的那种坑 ✓ 见 app.js 那一大段说明 ✓）
   ── 无障碍 ✗ ──
     role="status" + aria-live="assertive"（★ OAuth 回跳是用户主动动作的收尾 ✗ 该立刻播报 ✓）
     ＋ aria-busy="true" 声明整页正在忙 ✓（该属性是【声明】✗ 不要求被标元素可聚焦 ✓）
   ── 与既有版式的关系 ✗ 零影响 ✓ ──
     本层默认 display:none 且是 position:fixed ✗ 不进文档流 ✓ 不占高度 ✓ 不推任何元素 ✓
     ⇒ 布局位移 / 横向溢出 都是 0（site-crawl-ui 的实测口径 ✓）
   ══════════════════════════════════════════════════════════════════════════════════════════════ */
#calfAuthWait{position:fixed;inset:0;z-index:2147483000;display:none;align-items:center;justify-content:center;background:#fff;padding:24px}
html[data-authwait="1"] #calfAuthWait,html[data-authwait="fail"] #calfAuthWait{display:flex}
.calf-authwait-card{width:100%;max-width:420px;text-align:center;background:#fff;border:1px solid var(--line,#e3e7ee);border-radius:24px;box-shadow:0 18px 48px rgba(21,37,64,.13);padding:30px 22px 26px}
.calf-authwait-illu{display:block;width:132px;height:132px;margin:0 auto 4px;object-fit:contain}
.calf-authwait-row{display:flex;align-items:center;justify-content:center;gap:10px;margin:8px 0 2px}
.calf-authwait-row .spin.calf-spin-btn{vertical-align:0}
.calf-authwait-title{margin:8px 0 0;font-family:var(--display,inherit);font-size:19px;font-weight:600;color:#152540}
.calf-authwait-sub{margin:8px 0 0;font-size:13.5px;line-height:1.6;color:#556577;word-break:break-word}
.calf-authwait-fail{display:none}
html[data-authwait="fail"] .calf-authwait-card{display:flex;flex-direction:column;align-items:center}
html[data-authwait="fail"] .calf-authwait-spin{display:none}
html[data-authwait="fail"] .calf-authwait-sub{color:#b4232c;font-weight:600}
html[data-authwait="fail"] .calf-authwait-fail{display:inline-flex;margin-top:16px}
/* ★ 减弱动效 ✗ 427 行那条全局 reduce 规则会把转圈停掉 ✓
   停掉之后下面这行保证【还是个能看懂的缺口环】✗ 不是空白 ✓（与 ce-elf-think 2758 行同一处理 ✓）*/
@media(prefers-reduced-motion:reduce){.calf-authwait-row .spin.calf-spin-btn{animation:none!important;border-color:var(--line);border-top-color:var(--primary)}}

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   ★★★ 第 57 轮 ✗✗【重置密码页】的样式 —— 与 #calfAuthWait 【同一形状 · 同一位置 · 同一层】✓
   ──────────────────────────────────────────────────────────────────────────────────────────────
   ── 复用 ✗ 不新造 ✓ ──
     · 整层定位与遮罩  = 与 #calfAuthWait 那两行【逐字同构】✓（只是换成 data-resetpage ✓）
     · 白卡 + 圆角 + 阴影 = 直接用既有的 .calf-authwait-card ✓ 本块【一个字节都不改它】✓
     · 小精灵 / 标题 / 副标题 = 直接用既有的 .calf-authwait-illu / -title / -sub ✓
     · 强度条 + 四条规则 = 既有的 .pw-meter / .pw-rules / .pw-check（components.css:230 与 853 ✓）
       由 theme-app.js 的 calfPwMeter() 生成 ✓ 本块不为它写任何样式 ✓
   ── 只有这几样是新的 ✗ 都是"表单那几行"✓ ──
     · 邮箱只读那一行（用 <div> 不用 input ✗ 后者手机上仍弹键盘 ✓）
     · 两个输入框（与 #newPasswordRow 那对同一套观感 ✓ 但那是内联样式 ✗ 这里是类 ✓）
     · 提交前那句可读提示（★ 绝不静默 ✗ 出错必须说人话 ✓）
     · 两个文字按钮（"不是你？"与"先不设置"✓ 都是 <button> ✗ 不是 <a> ✓ 键盘可达 ✓）
   ── 层级 ✗ 与等待层一致 ✓ ──
     2147483000（与 #calfAuthWait 同值 ✓ 两者不会同时显示 ✓ 见 app.js 的状态切换 ✓）
   ── 默认 ✗ 收起 ✓ ──
     display:none 且 position:fixed ⇒ 不进文档流 · 不占高度 · 不推任何元素 ✓
     （与等待层同一个"零布局影响"的理由 ✓ 见上面那段 ✓）
   ── 减弱动效 ✗ ──
     本页没有转圈 ✗ 只有既有 .ce-elf-think 呼吸 ⇒ 由全局 reduce 规则照顾 ✓ 本块不新增动画 ✓
   ★ 修改此处务必同步 public/components.css ✓
   ══════════════════════════════════════════════════════════════════════════════════════════════ */
#calfResetPage{position:fixed;inset:0;z-index:2147483000;display:none;align-items:center;justify-content:center;background:#fff;padding:24px;overflow:auto}
html[data-resetpage="1"] #calfResetPage{display:flex}
.calf-reset-emailrow{margin:14px 0 0;display:flex;flex-direction:column;align-items:center;gap:4px}
.calf-reset-email{font-size:13.5px;font-weight:700;color:#0E7C86;word-break:break-all;background:#f2fbfc;border:1px solid #cdeef2;border-radius:12px;padding:7px 12px;max-width:100%}
.calf-reset-note{border:0;background:none;color:#556577;font-size:12px;font-weight:700;cursor:pointer;text-decoration:underline;padding:2px 4px}
.calf-reset-note:hover{color:#0E7C86}
.calf-reset-form{margin-top:14px;text-align:left}
.calf-reset-form input{width:100%;box-sizing:border-box;margin-bottom:10px}
.calf-reset-msg{margin:0 0 6px;font-size:12.5px;font-weight:700;color:#b4232c;text-align:center;min-height:1em}
.calf-reset-msg.ok{color:#15803d}
.calf-reset-rule{margin:0 0 12px;font-size:11.5px;color:#94a3b8;text-align:center}
/* ★★★ 第 57 轮：“未完成的动作”那条黄条 ✗✗ 复用的就是第 42 轮那套 .ce-verify-banner ✓
   ── 只多这一行 ✗ 用来把 ✉️ 图标藏掉 ✓ ──
     那个图标是"邮箱未确认"那条的语义 ✓ 放在"密码还没改完"上是错的 ✓
   ── 为什么只改 padding 而不是覆盖整条 ✗ ──
     其余（底色 / 边框 / 圆角 / 文字色 / 手机上那档）与既有那条【逐字一致】✓
     ⇒ 两条黄条长得一样 ⇒ 用户一眼认得出"这是同一类提醒"✓✓
   ★ 修改此处务必同步 public/components.css ✓ */
.ce-verify-banner.ce-vb-noic{padding-left:14px}

/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 文件选择按钮（index.html:253 的 #file ⇒ 原生控件的 ::file-selector-button）
   ──────────────────────────────────────────────────────────────────────────────
   底色 = #115E8A（rgb 17,94,138）✗ 按产品主人给的颜色素材取的实测值 ✓
     · 取法：素材只作纯色块用 ⇒ 只采【去边 2px 的中心区】1482 像素取均值 ✓
       七种取样区（全图 / 去边 1·2·3·5·7px / 中央 50%）全部收敛到同一个十六进制 ✓
     · ★ 标准差不为零（每通道 sd≈1.6 ✗ 40~62 种颜色 ✗ 最高频占比 <7%）⇒
       素材是被重采样/重编码过一遍的纯色块 ✗ 不是渐变也不是噪点 ✓ 故取均值 ✓
   对比度：白字压它 = 7.019:1 ⇒ 正文级 ≥4.5 ✓ 大字级 ≥3.0 ✓ 并擦过 AAA 正文级 7.0 ✓
     （14px/700 属正文级 ✗ 不属大字 ✓；换色前是 --primary #0f766e = 5.473:1 ⇒ 可读性【变好】✓）
   ★ 为什么写死色值而【不走 --primary】✗：--primary 的消费者绝大多数是"白字压在它上面"
     （.btn.primary / input:focus / .auth-tab.active / toast.info ✗ 见 themes.css:42-55 ✓）
     ⇒ 改 --primary 会连带动全站 ✗ 而本次裁定只针对【这一个按钮】✓
   ★ 为什么四套主题【共用同一个蓝】✗：产品主人的原话是"这个按钮采用这个颜色"✓
     指的是【这个按钮】✗ 不是"elf 那一套主题"✓（所以上面 aurora 那条覆盖也一并改了 ✓）
   悬停 / 按下（★ 改前【两者都不存在】⇒ 这是补一个缺失 ✗ 不是改已有的 ✓）：
     · hover  #156A9A 浅一档 ⇒ 白字 5.899:1 ✓（余量厚 ✗ 不会因日后调字号破线 ✓）
     · active #0F5178 深一档 ⇒ 白字 8.507:1 ✓（按下压深 = 触感 ✓）
     ★ 为什么写两个具体色值而【不用 filter:brightness()】✗：本仓 .ce-guest-cta:hover
       确实用 filter ✓ 但 filter 作用在 ::file-selector-button 上支持不一致 ✗
       ⇒ 能当场验证的写法优先于省事的写法 ✓
   ★ 只改颜色 ✗ 布局 / 尺寸 / 圆角 / 间距 / 文案 / 文字色 / 字号【一个字都没动】✓
   ★ 选择器用 #file 而【不加大 input[type="file"] 那一层】✗：那会连 #ceCamShot 一起命中 ✓
     （#ceCamShot 是 display:none ✗ 零视觉影响 ✓ 但零影响不等于该动 ✓）
     #file 的 ID 特异性(1,0,0) 恰好压过 aurora 那条 (0,2,1) ⇒ 无需 !important ✓
   ══════════════════════════════════════════════════════════════════════════════ */
#file::file-selector-button{background:#115E8A}
#file::file-selector-button:hover{background:#156A9A}
#file::file-selector-button:active{background:#0F5178}

/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 第 58 轮：水壶边那个【跳动感叹号】的【第三层】动效接线 ✗✗
   三层是怎么分工的（★ 前两层在 themes.css 末尾 ✓ 这一层是本文件 ✓）：
     ① themes.css:167 与 components.css:453 的 *{animation:none!important}   = 全站硬闸 ✓
     ② themes.css 末尾那条 .ce-credit-widget.tier-{1,2} .ce2-warn 动画        = 基态（不带 !important ✓ 硬闸赢 ✓）
        ★★ 第 60 轮起【两边都是 8s】✗✗（产品主人第 60 条：每隔 8 秒闪动一次 ✓）
          ⇒ ★ 上面这条 ceMotionOn 也必须 8s ✗ 不能只改 themes.css ✓
            理由：两份共用【同一个 @keyframes ce2WarnPulse】✗
              只改一处 ⇒ 百分比会按【错的那个分母】换算 ⇒ 跳法整个变形 ✓
              （8s 版的百分比在 themes.css 末尾那份同名 keyframes 里 ✓ = 毫秒 ÷ 80 ✓）
        ＋ themes.css 末尾那个 @media(reduce) 块【逐条点名】关闭它            = 防"全局那条被收窄"✓
     ③ 本块 = 用户【手动开关】（html.ceMotionOn / ceMotionOff ✓ 键 calcelf_motion ✓）
        · 系统【没开】reduce 而用户【手动关】⇒ 必须点名关掉它 ✓（本文件既有块只逐条点名旧元素 ✓ 新元素要自己进来 ✓）
        · 系统【开了】reduce 而用户【手动开】⇒ 不许把它打开 ✗✗
          所以两条都包在 no-preference 里 ✓（理由见 tests/email-and-watering.js:196-207 ✓）
   ★ 为什么这一层要带 !important ✗：
     themes.css 末尾那条是 (0,3,0) ✗ 本块是 (1,3,0)（html. 前缀 ✓）
     ⇒ 其实【不加 !important 也赢】（类数相同、本文件后加载 ✓）
     ⇒ 但既有的 ceMotionOn 块全带 !important（components.css:1245-1286 ✓）
     ⇒ 为了"同族写法一致、将来有人调顺序也不会翻车"照带 ✓（!important 只在 no-preference 内出现 ✓）
   ── ★ 与壶的关系 ✗ ──
     本块【不动】.ce2-can / .ce2-canwater / .ce2-drop（它们的三层接线早就齐了 ✓）
     只加"新元素"这一条 ⇒ 现有动画行为零改动 ✓
   ★ 修改此处务必同步 public/components.css ✓
   ══════════════════════════════════════════════════════════════════════════════ */
@media(prefers-reduced-motion:no-preference){
  html.ceMotionOn .ce-credit-widget.tier-1 .ce2-warn,
  html.ceMotionOn .ce-credit-widget.tier-2 .ce2-warn{animation:ce2WarnPulse 8s ease-in-out infinite!important}
  html.ceMotionOff .ce2-warn{animation:none!important;transform:none!important;opacity:1!important;filter:none!important}
}

/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 第 59 轮：水壶边那个【跳动感叹号】的【第三层】动效接线（第 58 轮那条的续 ✓）
   ★ 第 59 轮这一块【一个字都没改】✗ 只把注释补全 ⇒ 让"三层分工"与新的位置/形状对得上 ✓
     （改的是 themes.css 末尾那块与 index.html 那一行 ✓ 见它们的注释 ✓）
   三层是怎么分工的（★ 前两层在 themes.css 末尾 ✓ 这一层是本文件 ✓）：
     ① themes.css:167 与 components.css:453 的 *{animation:none!important}   = 全站硬闸 ✓
     ② themes.css 末尾那条 .ce-credit-widget.tier-{1,2} .ce2-warn 动画        = 基态（不带 !important ✓ 硬闸赢 ✓）
        ★★ 第 60 轮起【两边都是 8s】✗✗（产品主人第 60 条：每隔 8 秒闪动一次 ✓）
          ⇒ ★ 上面这条 ceMotionOn 也必须 8s ✗ 不能只改 themes.css ✓
            理由：两份共用【同一个 @keyframes ce2WarnPulse】✗
              只改一处 ⇒ 百分比会按【错的那个分母】换算 ⇒ 跳法整个变形 ✓
              （8s 版的百分比在 themes.css 末尾那份同名 keyframes 里 ✓ = 毫秒 ÷ 80 ✓）
        ＋ themes.css 末尾那个 @media(reduce) 块【逐条点名】关闭它            = 防"全局那条被收窄"✓
     ③ 本块 = 用户【手动开关】（html.ceMotionOn / ceMotionOff ✓ 键 calcelf_motion ✓）
        · 系统【没开】reduce 而用户【手动关】⇒ 必须点名关掉它 ✓（本文件既有块只逐条点名旧元素 ✓ 新元素要自己进来 ✓）
        · 系统【开了】reduce 而用户【手动开】⇒ 不许把它打开 ✗✗
          所以两条都包在 no-preference 里 ✓（理由见 tests/email-and-watering.js:196-207 ✓）
   ★ 为什么这一层要带 !important ✗：
     themes.css 末尾那条是 (0,3,0) ✗ 本块是 (1,3,0)（html. 前缀 ✓）
     ⇒ 其实【不加 !important 也赢】（类数相同、本文件后加载 ✓）
     ⇒ 但既有的 ceMotionOn 块全带 !important（components.css:1245-1286 ✓）
     ⇒ 为了"同族写法一致、将来有人调顺序也不会翻车"照带 ✓（!important 只在 no-preference 内出现 ✓）
   ── ★ 与壶的关系 ✗ ──
     本块【不动】.ce2-can / .ce2-canwater / .ce2-drop（它们的三层接线早就齐了 ✓）
     只加"新元素"这一条 ⇒ 现有动画行为零改动 ✓
     ★ 第 59 轮把位置挪到 .ce2-can 的右下角之后，两者【仍然各跳各的】✗
       壶的 ce2CanAlert 恒 1.25s（tier-1 时 0.9s ✓）而感叹号仍是 9.5s 一圈 ✓ 节拍比 ≈ 2:1 ✓
       ⇒ 主从而非同拍竞速 ✓（第 58 轮定的语气没变 ✓）
   ★ 修改此处务必同步 public/components.css ✓
   ══════════════════════════════════════════════════════════════════════════════ */
@media(prefers-reduced-motion:no-preference){
  html.ceMotionOn .ce-credit-widget.tier-1 .ce2-warn,
  html.ceMotionOn .ce-credit-widget.tier-2 .ce2-warn{animation:ce2WarnPulse 8s ease-in-out infinite!important}
  html.ceMotionOff .ce2-warn{animation:none!important;transform:none!important;opacity:1!important;filter:none!important}
}
