/* ══════════════════════════════════════════════════════════════════════
   ce-illu.css —— 第 36 轮：12 张插画的接线样式 ✗✗
   ══════════════════════════════════════════════════════════════════════
   ★ 为什么单独一个文件 ✗✗（本仓库规矩 ✓）
     · 不动 components.css ✗ —— 尤其【等级主题色】那一段（--lv-accent 相关 ✓ 刚做完 ✓）
     · 不动 ce-stars.css  ✗ —— 星星随机化刚做完 ✓
     · 不动 my-growth.css ✗ —— 刚提交的 ✓
   ★ 本文件【只加新选择器】✗ 不覆盖、不重写任何既有规则 ✓
     例外只有一条：.ce-guest-visual 的 object-fit 与连续框 —— 它是新元素 ✓ 不与既有规则冲突 ✓
     ★ 访客卡的摇摆动画【故意不在这里重写】✗✗
       它仍由 components.css 的 `.ce-guest-sproutwrap img` 提供 ✓
       （含 transform-origin:50% 95% ✓ 与 9s ✓ 与 reduced-motion 兜底 ✓）
       ⇒ tests/growth-cards.js 那 4 条动画断言依旧成立 ✓ 一处都不动 ✓✓
   ★ 无障碍 ✗：所有插画都 alt="" + aria-hidden ✓（旁边都有文字 ✓ 读屏不重复 ✓）
   ★ RTL ✗：本文件只用【逻辑属性】与【居中】✗ 不用 left/right ✗ 不镜像反 ✓
              （小精灵是"挥手打招呼"✗ 镜像后仍可读 ✓ 但我们【不做任何镜像变换】✓）
   ★ 移动端 ✗：全部走 max-width:100% + object-fit:contain ✓ 390px 不溢出 ✓
   ★ 动效 ✗：本文件【不添加任何 animation】✗ 静态图 ✓ 与 reduced-motion 永不冲突 ✓✓
   ══════════════════════════════════════════════════════════════════════ */

/* ── ① 访客卡主视觉：小精灵（mascot-hello）✓✓ ───────────────────────
   ★ 位置 ✗：放进原有的白色圆形光晕 .ce-guest-sproutwrap 里 ✓
     该光晕是 112px（<=560px 时 96px ✓）flex 居中 ✓ overflow:hidden ✓
   ★ 尺寸 ✗：宽高 100% + object-fit:contain ✓
     方形图（2048×2048 → 320×320 ✓）在方形容器里正好铺满 ✗ 不变形 ✓
   ★ 注意 ✗：这里【故意不写 animation / transform-origin】✓
     否则会盖掉 components.css 的那两条 ✓ 断言就红了 ✓ */
.ce-guest-sproutwrap img.ce-guest-visual{
  width:100%;height:100%;object-fit:contain;display:block;
}
/* ★ 兜底 ✗✗：小精灵与备选树苗都加载失败时 ✗
   用 onerror 把元素设成 visibility:hidden ✓
   ★ 为什么不是 display:none ✗✗：
     v55-enhance.js 的星星避让会量 .ce-guest-sproutwrap 的矩形 ✓
     它的尺寸【由 CSS 的 width/height 决定】✗ 不由子元素撑开 ✓
     ⇒ 但为稳妥仍保留元素在布局里 ✓ 圆形光晕与布局零位移 ✓
     ⇒ 绝不出现裂图 ✓ 也绝不出现位移 ✓✓ */
.ce-guest-visual[data-broken="1"]{visibility:hidden}

/* ── ② 加载中：小精灵思考（mascot-think）✓ ─────────────────────────
   ★ 位置 ✗：与 .thinker 的【三个跳动问号 + 呼吸条】并排 ✓
     ★ 为什么不上下堆叠 ✗✗：第一版做成堆叠 ✓ 实测【两个角色一上一下】✗
       观感像"两个吉祥物"✓ 我出图时自己发现了 ✓
     ★ 第二版改成并排后 ✗ 实测【还是两个角色】✗✗ —— 真因 ✗：
       `.thinker-img` 引用的 calcelf-logo.png 【本身就是那只动漫小精灵】✓
       （不是抽象 logo ✗）⇒ 并排 = 小精灵 vs 小精灵 ✓ 更怪 ✓
     ⇒ 定案 ✗：在这一行里【隐藏 .thinker-img】✓ 由 mascot-think 顶上它原来的位置 ✓
       ★ 只隐藏这一行内的 ✗ 页面别处若再用 .thinker-img 不受影响 ✓
       ★ .qrow（三个问号）/ .think-bars（呼吸条）/ #loadingText / .lddots 全部保留 ✓
         它们的动画（qmPop / tbBreathe / ldDot）照常跑 ✓ 一个类都没改 ✓ */
.ce-illu{max-width:100%;height:auto}
.ce-loading-row{
  display:flex;align-items:center;justify-content:center;
  gap:18px;flex-wrap:wrap;
}
.ce-loading-main{min-width:0}
/* ★ 这一行里由 mascot-think 代表角色 ✗ 隐藏重复的那只 ✓ */
.ce-loading-row .thinker-img{display:none}
/* ★ 但问号行原来靠 .thinker-img 的 margin-top 撑高 ✗ 隐藏后要补回同样的占位 ✓
   20px ≈ 原 margin:18px + 视觉留白 ✓ 保证三个问号仍在角色上方 ✓ */
.ce-loading-row .thinker{padding-top:20px}
/* ══════════════════════════════════════════════════════════════════════════
   v9.49 · 去掉等待态小精灵外面那块【白底板】✗✗（产品主人："能不能把这个小人从图里面抠出来"）
   ── 先查根因 ✗ 不猜 ✓（两种可能都验了 ✓）──
     ✗ 可能性一【图片本身带白底】⇒ 【不成立】✓ 实测读像素：
         /brand/illu/elf-think.webp 四角 alpha = 0,0,0,0 · 四边中点 alpha 也全 0
         alpha 边界盒 104×177（画布 192×192）⇒ 人物之外真的是透明的 ✓
         /brand/illu/elf-wave.webp   四角 alpha = 0,0,0,0 ✓
         /calcelf-elf-cookie.png     四角 alpha = 0,0,0,0 ✓
     ★ 可能性二【我们自己给它加了一层白底衬】⇒ 【成立】✓ 就是下面这一条 ✓
         原来写的是：border-radius:20px;background:#fff;padding:3px;
                     box-shadow:0 6px 18px rgba(37,99,235,.12);
     ── ★★ 这块底板【当初是必需的】✗ 现在不是了 ✓（这才是真因 ✓）──
         实测旧图 /brand/illu/mascot-think.webp 四角 alpha = **255,255,255,255**
         ⇒ 旧图【本身就是不透明的白底方图】✓ 所以必须给它配一块圆角白底板 + 阴影，
           看起来才像一个"图标"✓（否则就是一张白方块贴在页面上 ✓）
         换成透明底精灵之后 ✗ 底板失去了理由 ✗ 反而把人物框成了一个 App 图标 ✗
         ⇒ 与右边那三条直接浮在底色上的呼吸条【完全不搭】✓ 正是产品主人看到的 ✓
   ── 改法 ✗ ──
     · 拿掉 background / border-radius / box-shadow（= 那块白底板 ✓）
     · ★ width:78px;height:78px;padding:3px 一个字节都不动 ✓⇒ 占位零位移 ✓
     · 换成【跟着人物轮廓走】的淡阴影 filter:drop-shadow(…) ✓
       它不是一块板 ✗ 是沿 alpha 边缘描的一圈柔光 ✓ 浅底不发灰 ✗ 深底也有边界 ✓
       （产品主人原话给的选项："只加很淡的阴影，不要整块白底" ✓）
   ── ★ 浅底 / 深底可见度（实测口径：不透明像素里"与底色亮度差 < 0.08"的占比 ✓）──
     浅 #f4f6f9：think 1.11% · wave 1.51% · cookie 2.86%   ⇒ 都清楚 ✓
     白 #ffffff：0.09% / 0.51% / 1.22%                      ⇒ 更清楚 ✓
     深 #0f172a：4.49% / 5.11% / 6.08%                      ⇒ 可以 ✓
     深 #12303f（本站头部那种深青）：10.44% / 9.76% / 9.44%  ⇒ ★ 最差的一档 ✓
       主要丢的是深蓝鞋 / 深蓝腰带 / 描边那一小片 ✓
       ⇒ 结论：三处现在的落点【全是浅底】⇒ 去底板安全 ✓
         将来若真要放到深青底上 ⇒ 先加 drop-shadow（本行已加 ✓）或加深色半透明药丸底 ✓
         绝不回到整块白底 ✓
   ══════════════════════════════════════════════════════════════════════════ */
.ce-illu-loading{
  width:78px;height:78px;object-fit:contain;
  display:block;flex:0 0 auto;
  padding:3px;
  filter:drop-shadow(0 2px 4px rgba(15,40,70,.28));
}
@media(max-width:560px){
  .ce-loading-row{flex-direction:column;gap:6px}
  .ce-loading-row .thinker{padding-top:8px}
  .ce-illu-loading{width:56px;height:56px;padding:2px}
}

/* ── ③ 空状态插画（credits 流水为空 / 周报为空）✓✓ ──────────────────
   ★ 这两个位置是【代码里真实存在的空状态】✗ 不是新造页面 ✓：
     · v55-enhance.js 的 #ceLedgerList："还没有记录，去解第一道题吧！"（ledger_empty ✓）
     · v55-enhance.js 的 weeklyBodyHTML()："本周还没有学习记录…"（weekly_empty ✓）
   ★ 尺寸 ✗：64px 居中 ✓ 空状态本来就是"留白 + 一句话"✗ 图不能比字还抢眼 ✓
   ★ 用 margin-inline:auto ✗ 不用 margin-left/right ✗ RTL 下同样居中 ✓ */
.ce-illu-empty{
  width:64px;height:64px;object-fit:contain;
  display:block;margin:0 auto 8px;
}
@media(max-width:560px){
  .ce-illu-empty{width:56px;height:56px}
}
/* ★ 空状态的包裹层 ✗：原先是纯 inline style 的一行文字 ✓
   加图后包一层 ✓ 保持居中与原有字号/颜色 ✓ 不改既有观感 ✓ */
.ce-empty-box{
  text-align:center;
  padding:14px;
  color:#5b6b80;
  font-size:13px;
  line-height:1.7;
}
.ce-empty-box.ce-empty-weekly{color:#64748b;font-size:14px;padding:30px 10px}

/* ══════════════════════════════════════════════════════════════════════
   第 37 轮：剩下 7 张插画接线 ✗✗
   ══════════════════════════════════════════════════════════════════════
   ★ 本段【只追加新选择器】✗ 上面 ①②③ 三段一个字都没动 ✓
   ★ 动效 ✗✗（用户点名要求 ✓）
     components.css:276 有一条
       @media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
     它带 !important ✗ 特异性 (0,0,0) 但有 !important ⇒ 谁也压不过它 ✓
     ⇒ 本文件【不写 !important】✗ 【不加豁免类】✗
        只在 reduce 之外加一条淡入 + 12px 上浮 ✓
        reduce 下被那条全局规则杀掉 = 【期望行为】✓✓ 不是我漏了 ✓
     ★ 所以关闭逻辑【不能挂在 animationend 上】✗✗
        见 v55-enhance.js 的 showLevelUpCard()：用 setTimeout ✓ 与动画无关 ✓✓
   ★ RTL ✗：全部逻辑属性（margin-inline / inset-inline / margin-block）✗
             不用 row-reverse ✗ 不镜像 ✓
   ★ 移动端 ✗：全部 max-width:100% + object-fit:contain ✓ 390px 不横向溢出 ✓
   ══════════════════════════════════════════════════════════════════════ */

/* ── ④ ★★ 升级庆祝卡片（celebrate-levelup ✓ 用户点名价值最高 ✓）──────
   ★ 定位 ✗：fixed 居中 ✓ 不插进文档流 ✓
     ⇒ 绝不把下面的内容顶下去 ✓【不打断阅读与操作】✓✓
   ★ 不打断操作 ✗✗：外层 pointer-events:none ✓
     只有卡片本体与按钮收事件 ✓ 卡片之外的页面照常可点 ✓✓
   ★ z-index ✗：60 ✓ 高于普通卡片与状态条 ✓
     低于全屏动画覆盖层（#animFsOverlay ✓）与弹窗层 ✓ 不抢它们的层级 ✓
   ★ 淡入 + 12px 上浮 ✗：reduce 下由全局规则杀掉 ✓ 只剩终点态 ✓ */
.ce-illu-levelup{
  position:fixed;z-index:60;pointer-events:none;
  inset-inline:0;inset-block-start:clamp(62px,11vh,116px);
  display:flex;justify-content:center;
  padding-inline:14px;
  animation:ceIlluRise .42s ease-out both;
}
@keyframes ceIlluRise{
  from{opacity:0;transform:translate3d(0,12px,0)}
  to{opacity:1;transform:none}
}
.ce-illu-lv-card{
  pointer-events:auto;
  display:flex;align-items:center;gap:14px;
  max-width:min(430px,94vw);
  background:#fff;
  border:1px solid #dbe7f5;
  border-radius:20px;
  padding:14px 16px;
  box-shadow:0 20px 48px rgba(21,37,64,.20);
}
/* ★ 插画尺寸 ✗：96px ✓（用户给的是 80~110 ✓）
   320×320 纯白底方图 ✓ contain 不变形 ✓ 圆角白底与卡片融为一体 ✓
   ★ 【绝不裁切】✗✗：图里的小树 + 上升箭头主体贴边 ✓
     所以只缩不裁 ✓ 禁用任何 object-fit:cover 与 overflow:hidden ✓✓ */
.ce-illu-lv-img{
  width:96px;height:96px;flex:0 0 auto;
  object-fit:contain;
  border-radius:16px;background:#fff;
}
.ce-illu-lv-body{min-width:0;display:flex;flex-direction:column;gap:2px}
.ce-illu-lv-title{font-size:16px;font-weight:var(--ce-fw-head);color:#0f766e;line-height:1.4}
.ce-illu-lv-row{display:flex;align-items:baseline;gap:6px;flex-wrap:wrap;font-size:14px;line-height:1.5}
.ce-illu-lv-lab{color:#64748b;font-size:12.5px}
.ce-illu-lv-name{color:#1b2a44;font-weight:var(--ce-fw-head)}
.ce-illu-lv-cheer{color:#5c7a94;font-size:12.5px;line-height:1.55}
.ce-illu-lv-ok{
  flex:0 0 auto;margin-inline-start:6px;
  border:0;cursor:pointer;
  border-radius:999px;
  background:#14B8A6;color:#fff;
  font-size:13px;font-weight:800/* ★★ 第 2 步例外 ⑨：白字压在 #14B8A6 上只有 2.49:1 ✗ 13px 替对比度打工 ⇒ 保留 800 ✓ */;
  padding:9px 15px;
  font-family:inherit;
}
.ce-illu-lv-ok:hover{background:#0F766E}
/* ★ 窄屏 ✗：竖排 ✓ 图小一点 ✓ 按钮不再挤在右边 ✓ */
@media(max-width:560px){
  .ce-illu-levelup{inset-block-start:56px;padding-inline:10px}
  .ce-illu-lv-card{flex-wrap:wrap;justify-content:center;text-align:center;gap:8px;padding:12px 14px}
  .ce-illu-lv-img{width:78px;height:78px}
  .ce-illu-lv-row{justify-content:center}
  .ce-illu-lv-ok{margin-inline-start:0;width:100%}
}

/* ── ⑤ 徽章墙空状态（empty-nobadge ✓ 用户点名"新造"✓）──────────────
   ★ 位置 ✗：徽章墙【上方】多一行引导 ✓ 那 6 张卡【照常显示】✗✗
     ⇒ 所以本规则【只作用于新加的 .gc-badge-empty 那一行】✓
       一个既有 .gc-badge 规则都没碰 ✓✓
   ★ 尺寸 ✗：88px ✓（用户给的 80~100 ✓）移动 76px ✓
   ★ 只在 earnedCount === 0 时渲染 ✓（见 growth-cards.js ✓）
     已点亮 ≥1 枚 ⇒ 这一行【根本不存在】✓ 不是 display:none ✓ */
.gc-badge-empty{
  display:flex;align-items:center;gap:12px;
  margin:0 0 12px;
  padding:12px 14px;
  background:#fffdf5;
  border:1px dashed #e8d9a8;
  border-radius:14px;
}
.gc-badge-empty-img{
  width:88px;height:88px;flex:0 0 auto;
  object-fit:contain;
  border-radius:12px;background:#fff;
}
.gc-badge-empty-tx{min-width:0;color:#7a6320;font-size:13.5px;line-height:1.7}
@media(max-width:560px){
  .gc-badge-empty{gap:9px;padding:10px 11px}
  .gc-badge-empty-img{width:76px;height:76px}
  .gc-badge-empty-tx{font-size:12.5px}
}

/* ── ⑥ Credits 不足横幅（credit-low ✓）—— ★★ 规则【已删除】✗ 墓碑 ✗ ────────
   ★★ 删了什么 ✗：原来这里 25 行（206-230）的三条规则 + 560px 媒体查询全部删除 ✓
     #plansCreditLow ✓ .ce-illu-plans-img ✓ .ce-illu-plans-tx ✓（值见 docs/UI-SPEC-REAL.md §7.1）
   ★★ 依据（可复算 ✓）：全仓【已无任何元素】用它们 ✗
     · index.html:804 只剩"原内容：<div id=plansCreditLow …>"那句注释 ✗ 元素没了 ✓
     · app.js:3094 只读一次性标记 + 防御性 row.hidden=true ✗ 从不渲染它 ✓
     · tests/ 里 ce-illu-plans / plansCreditLow 零命中 ✓
   ★★ 罐子【没被删】✓：同一张 /brand/illu/credit-low.webp 缩到 64px（移动 52px）
     挪到套餐弹窗标题 <h2> 之前 ⇒ class 换成 .plans-head-jar ✓ 样式在 components.css 末尾 ✓
     ⇒ .ce-illu-plans-img 这个 class 名已无人使用 ✓ 可以删 ✓
   ★★ 实测：删前 348 行 ⇒ 删后 338 行（净 -10 ✓ 全是本节 ✗ 后面 117 行逐行相同 ✓）
     ⇒ 本节【之后】的行号整体 -10 ✗ 引用 ce-illu.css 行号的文档要跟着改 ✓
       （§7.1 那四条 plans.banner-* 校验已按新现实删除 ✓ 见本次同步 ✓）*/

/* ── ⑦ 判分结果：答对 cheering / 答错 oops（mascot-cheer / mascot-oops ✓）
   ★ 位置 ✗：.grade-head 里那行文字的【旁边】✓ 那行文字一个字没改 ✓✓
   ★ 尺寸 ✗：80px ✓（用户给的 72~88 ✓）克制 ✓ 不抢主内容 ✓
   ★ 为什么插在文字之前 ✗：RTL 下 flex 会自动镜像 ✓ 不用 row-reverse ✓ */
.grade-head .ce-illu-grade{
  width:80px;height:80px;flex:0 0 auto;
  object-fit:contain;
  border-radius:16px;background:#fff;
}
.grade-head{flex-wrap:wrap}
@media(max-width:560px){
  .grade-head .ce-illu-grade{width:64px;height:64px}
}

/* ── ⑧ 练习区未出题时的提示（mascot-reading ✓）────────────────────
   ★ 位置 ✗：#practiceArea 的【初始占位】✓
     实测：那个 div 在 index.html 里是空的 ✗ 点 Level 1/2/3 才会被填 ✓
     ⇒ 把插画放在【三个 Level 按钮之下、答案区之上】✓
       旁边那行文字（#practiceDesc ✓）一个字都没动 ✓✓
   ★ 尺寸 ✗：80px ✓ 居中 ✓ 绝不撑高版式 ✓ */
.ce-illu-practice{
  display:flex;align-items:center;justify-content:center;gap:10px;
  margin-block-start:6px;
  text-align:center;
}
.ce-illu-practice .ce-illu-practice-img{
  width:80px;height:80px;flex:0 0 auto;
  object-fit:contain;
  border-radius:16px;background:#fff;
}
.ce-illu-practice-tx{color:#5b6b80;font-size:12.5px;line-height:1.6}
@media(max-width:560px){
  .ce-illu-practice .ce-illu-practice-img{width:64px;height:64px}
}

/* ── ⑨ 解题讲解区（mascot-teacher ✓）────────────────────────────
   ★ 位置 ✗：#solutionTitle 所在的 .head 行【右边空档】✓
     ★ 为什么不塞进 h2 里 ✗：那个 h2 的文字由 app.js 的 setText('solutionTitle') 维护 ✓
       塞进去会被 t() 的赋值冲掉 ✗✗ ⇒ 单独一个 span ✓ 兄弟节点 ✓ 互不干扰 ✓✓
   ★ 尺寸 ✗：80px ✓（用户给的 72~88 ✓）
   ★ .head 本来是 space-between ✗ 这一行是【第 3 个子元素】✓
     用 margin-inline-start:auto 把它推到最右 ✓ 不动 .head 自身的规则 ✓✓ */
.ce-illu-sol{
  flex:0 0 auto;margin-inline-start:auto;
  width:80px;height:80px;
  object-fit:contain;
  border-radius:16px;background:#fff;
}
@media(max-width:560px){
  .ce-illu-sol{width:64px;height:64px}
}

/* ── ⑩ 访客卡文案排版（用户第 37 轮追加要求 ✓）────────────────────
   ★ 真因 ✗：整句当一个字符串流式排版 ✓ "…50 Credits 🎁" 断行位置难看 ✗
     而且最后那段没居中 ✗
   ★ 改法 ✗：拆成三个【块级】span ✓ 最后一块独立居中 + 略大 ✓
   ★ 只加新类 ✓ .ce-guest-sub / .ce-guest-sub b / .ce-guest-gift 一个都没改 ✓✓
     ⇒ 🎁 的 ceGuestShake 1.6s 动画照常跑 ✓ 那条动画在 components.css ✓ 我不碰 ✓
   ★ 逻辑属性 ✗：margin-block-start ✓ RTL 下自动镜像 ✓ */
.ce-guest-sub-line{display:block}
.ce-guest-bonus{
  display:block;
  margin-block-start:6px;
  font-size:1.06em;
  font-weight:var(--ce-fw-head);
}

/* ── ⑪ 访客卡主视觉：挥手动画（第 38 轮 ✓）─────────────────────────
   ★ 结构 ✗（由 v55-enhance.js 的 ceGuestWaveVideo 插入 ✓ 幂等 ✓）：
       .ce-guest-sproutwrap             ← components.css 的白色圆形光晕 ✓ 我不碰 ✓
         └ .ce-guest-sproutstack       ← 新增 ✓ position:relative ✓ 只当 video 的包含块 ✓
             ├ img#guestSprout         ← 原静态图【原地保留】✗ 永远是兜底 ✓
             └ video.ce-guest-wave     ← 新动画 ✓ 盖在静态图上 ✓
   ★★ 为什么不给 .ce-guest-sproutwrap 加 position:relative ✗✗
     三颗星 .ce-guest-spark 是 position:absolute ✓ 包含块 = .ce-guest-hero ✓
     光晕一旦成为定位元素 ✗ 星星坐标系整体改变 ✗ 上一轮的避让全部作废 ✓
     ⇒ 所以【由新壳自己定位】✗ 光晕与 img 的矩形一个像素都不动 ✓（实测见汇报 ✓）
   ★ 尺寸 ✗：新壳 100%×100% ✓ 与光晕同宽高 ✓ 圆形裁切仍由光晕的 overflow:hidden 提供 ✓
     video 再补一层 border-radius:50% ✗ 两层一致 ✓ 视觉零差 ✓
   ★ 填满 ✗：object-fit:cover ✓ 视频是方形 ⇒ 方形容器里正好铺满 ✗ 不裁不变形 ✓
   ★ 过渡 ✗：只用 opacity ✓ 320ms ✓ ≤400ms ✓ 初始 opacity:0 ✓
     只有 JS 加上 .is-ready（= 真的开始播了 ✓）才淡入 ✓
   ★ 绝不裂图/空白 ✗：加载失败 / 解码失败 / 不支持 / autoplay 被拦
     ⇒ JS 把 video 整个删掉 ✓ 露出的就是下面那张静态图 ✓
   ★★ 不吃点击 ✗✗：pointer-events:none ✓（实测 elementFromPoint ✓）
   ★ RTL ✗：只用 inset:0 与百分比 ✗ 无 left/right ✗ 不镜像反 ✓
   ★ reduced-motion ✗：本文件不写 !important ✗ 不与 components.css 顶部那条
     @media(prefers-reduced-motion:reduce){*{transition:none!important;…}} 对撞 ✓
   ★★ 第 40 轮更新 ✗✗：reduce 下【不再一律不插 video】✓（★ 这条注释原来写错了 ✓）
     判据换成 motionAllowed() = "没开 reduce ✓ 或 开了但有 ceGuestMotionExempt ✓" ✓
     —— 这张卡本来就靠那个类豁免 reduce（星星在闪 / 礼盒在晃 / CTA 在呼吸 ✓）
       主角小精灵当然该享受【同一套豁免】✓
       （否则就是"周围的都在动 ✗ 只有主角不动"比"都不动"更怪 ✓ 用户原话 ✓）
     ⇒ 下面那条 @media(reduce) 只是"把话说全"✗ 不再是主路径 ✓
       有豁免类时：320ms 淡入照旧（.is-ready 由 JS 打 ✗ 与 reduce 无关 ✓）
       没有豁免类时：JS 根本不插 video ✓ 这条也就没人命中 ✓✓ */
.ce-guest-sproutstack{
  position:relative;
  width:100%;height:100%;
  display:block;
}
.ce-guest-wave{
  position:absolute;inset:0;
  width:100%;height:100%;
  object-fit:cover;object-position:50% 50%;
  border-radius:50%;
  display:block;
  opacity:0;
  transition:opacity 320ms ease;
  pointer-events:none;                 /* ★ 任何情况下都不吃点击 ✓ */
  background:transparent;
}
.ce-guest-wave.is-ready{opacity:1}
@media(prefers-reduced-motion:reduce){
  .ce-guest-wave{transition:none}
}
