/* ==========================================================
   delight.css · 惊喜补丁（愉悦体验增强层）
   ------------------------------------------------------------
   设计原则：
   1) 纯增强——只新增类名，不覆盖 styles.css 任何既有规则。
      整文件删除 + 去掉 index.html 里两行引用即可完整回滚。
   2) 惊喜必须「服务功能」：每条反馈都带信息量（差几个 / 连对几题 / 连续几天），
      不是单纯加特效。
   3) 无障碍：全部动效包在 prefers-reduced-motion 保护下，
      系统开启「减弱动态效果」时自动降级为静态文案反馈。
   ========================================================== */

/* ---------- 飘字 / 粒子层（独立容器，不复用 confetti-layer，避免互相清空） ---------- */
#dm-layer {
  position: fixed; inset: 0; z-index: 60;
  pointer-events: none; overflow: hidden;
}

.dm-msg {
  position: absolute; left: 0; right: 0; top: 42%;
  text-align: center; font-weight: 900;
  font-size: clamp(24px, 6.4vw, 40px);
  color: var(--orange-d, #EE6A33);
  text-shadow: 0 3px 0 #fff, 0 8px 16px rgba(0, 0, 0, .16);
  animation: dm-pop 1.8s cubic-bezier(.2, 1.4, .4, 1) forwards;
}
.dm-msg small {
  display: block; margin-top: 6px;
  font-size: .46em; font-weight: 800;
  color: var(--ink-soft, #6E5E52);
  text-shadow: 0 2px 0 #fff;
}
.dm-msg.is-near { color: #E8932A; }
.dm-msg.is-fire { color: #FF5A2B; }
.dm-msg.is-big  { color: #C98A00; font-size: clamp(26px, 7vw, 44px); }

@keyframes dm-pop {
  0%   { opacity: 0; transform: scale(.5) translateY(18px); }
  16%  { opacity: 1; transform: scale(1.14) translateY(0); }
  28%  { transform: scale(1); }
  74%  { opacity: 1; transform: scale(1) translateY(-8px); }
  100% { opacity: 0; transform: scale(.94) translateY(-34px); }
}

/* 径向迸发的小粒子（连对 / 里程碑用，区别于从上往下落的 confetti） */
.dm-bit {
  position: absolute; left: 50%; top: 44%;
  animation: dm-fly 1.5s ease-out forwards;
  will-change: transform, opacity;
}
@keyframes dm-fly {
  0%   { opacity: 1; transform: translate(-50%, -50%) scale(.5) rotate(0deg); }
  100% { opacity: 0; transform: translate(calc(-50% + var(--dx, 0px)), calc(-50% + var(--dy, 0px))) scale(1.2) rotate(var(--rot, 180deg)); }
}

/* ---------- 小熊高光：连对里程碑时"被点亮"一下 ---------- */
.bear { position: relative; }          /* 仅为 ::after 定位，styles.css 未声明 position，无冲突 */
.bear.dm-halo::after {
  content: ''; position: absolute; inset: -16%;
  border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle, rgba(255, 201, 60, .6) 0%, rgba(255, 201, 60, 0) 68%);
  animation: dm-halo 1.3s ease-out forwards;
}
@keyframes dm-halo {
  0%   { opacity: 0; transform: scale(.6); }
  30%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.25); }
}

/* ---------- 首页「连续 N 天」徽章 ---------- */
.dm-chip {
  display: inline-flex; align-items: center; gap: 5px;
  background: linear-gradient(180deg, #FFE9B8, #FFD27A);
  border: 2px solid #F5B942; color: #8A5A00;
  border-radius: 999px;
  padding: clamp(6px, .8vw, 8px) clamp(13px, 1.7vw, 19px);
  font-size: clamp(14px, 1.5vw, 17px); font-weight: 800;
  animation: dm-chip-in .5s cubic-bezier(.34, 1.56, .64, 1) backwards;
}
@keyframes dm-chip-in {
  from { opacity: 0; transform: scale(.6) translateY(6px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}

/* ---------- 答题气泡：差一点点的两行版式 ---------- */
.dm-near-line {
  display: block; font-size: .92em; color: #E8932A; font-weight: 800;
  margin-bottom: 2px;
}
.dm-near-ans { display: block; font-weight: 700; }

/* ---------- 当前进度点：加一圈柔光（只动 box-shadow，不碰已有 transform） ---------- */
#progress .dot.now {
  box-shadow: 0 0 0 4px rgba(255, 138, 91, .22);
}

/* ---------- 转盘悬念：旋转时外圈转出一圈虚线光环 ---------- */
.wheel-frame.is-ticking::after {
  content: ''; position: absolute; inset: -12px;
  border-radius: 50%; pointer-events: none;
  border: 4px dashed rgba(255, 255, 255, .75);
  animation: dm-spin 2.4s linear infinite;
}
@keyframes dm-spin { to { transform: rotate(360deg); } }

/* ---------- 新用户引导：闲置 6 秒后主按钮轻轻呼吸一下 ---------- */
.dm-breathe { animation: dm-breathe 2.4s ease-in-out 3; }
@keyframes dm-breathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.035); }
}

/* ---------- 空状态插画：让"什么都没有"也像有人在跟你说话 ---------- */
.dm-empty-hero {
  font-size: clamp(38px, 5vw, 52px); line-height: 1;
  display: block; margin-bottom: 8px;
  animation: dm-bob 2.8s ease-in-out infinite;
}
@keyframes dm-bob {
  0%, 100% { transform: translateY(0) rotate(-3deg); }
  50%      { transform: translateY(-6px) rotate(3deg); }
}

/* ==========================================================
   第二批 · 治「学起来枯燥」：给学习循环加节奏变化
   ========================================================== */

/* ---------- D8 彩虹幸运题：已移除（用户要求去掉彩虹） ----------
   关末庆祝改为「全屏宝箱」（见文件末尾「第三批」区块）。 */

/* ---------- D9 小熊每日帽子：今天戴什么？ ----------
   每天换一顶，给"今天再打开一次"一个理由。
   JS 只往 body 上挂 dm-hat-周几，帽子全部由这里的 content 渲染。 */
.bear::before {
  position: absolute; top: -14px; right: -10px;
  z-index: 2; pointer-events: none;
  animation: dm-hat-in .6s cubic-bezier(.34, 1.56, .64, 1) backwards;
  filter: drop-shadow(0 3px 2px rgba(0, 0, 0, .18));
}
.bear-wrap .bear::before { font-size: clamp(36px, 4.6vw, 52px); }
.small-bear::before { font-size: clamp(26px, 3.2vw, 36px); top: -10px; right: -6px; }

body.dm-hat-0 .bear::before { content: '👑'; }   /* 周日：皇冠 */
body.dm-hat-1 .bear::before { content: '🎩'; }   /* 周一：魔术帽 */
body.dm-hat-2 .bear::before { content: '🧢'; }   /* 周二：棒球帽 */
body.dm-hat-3 .bear::before { content: '🎀'; }   /* 周三：蝴蝶结 */
body.dm-hat-4 .bear::before { content: '🎓'; }   /* 周四：学位帽 */
body.dm-hat-5 .bear::before { content: '⛑️'; }   /* 周五：小头盔 */
body.dm-hat-6 .bear::before { content: '🎃'; }   /* 周六：小南瓜 */

@keyframes dm-hat-in {
  from { opacity: 0; transform: translateY(-16px) rotate(14deg); }
  60%  { transform: translateY(2px) rotate(-4deg); }
  to   { opacity: 1; transform: translateY(0) rotate(0); }
}

/* ---------- D10 爪印小路：进度点变成小熊脚印，一次踩一个、左右脚交替 ----------
   10 个黄色小方块太"仪表盘"了。答对 / 答错 = 小熊向前踩出一个脚印。
   ① 单枚脚印：原来是 🐾（这个 emoji 自带【两只】爪印），一次踩出两个不合脚；
   ② 朝着右走：🐾 的趾尖天生朝上，光靠 rotate 怎么转都不像"往前走" —— 换成自己画的
      内联 SVG（掌垫在左、4 个趾垫在右），方向由几何决定，且各平台渲染完全一致；
   ③ 先左后右：奇数步 = 左脚（落在小路中线上方），偶数步 = 右脚（沿行进轴镜像
      scaleY(-1)，落在中线下方）—— 两步恰好互为镜像，读起来就是真的在走；
   ④ 对错都留印：答错的那一格（.wrong，粉色踏脚石）同样踩出脚印 —— 孩子走过的每一步
      都该在小路上留痕。对错由踏脚石底色说明（黄 = 对 / 粉 = 错），脚印只管"走过"；
      不给错的那一脚单换一种颜色：一条路上冒出两种脚印，反而把"一段完整的脚印"读碎了。 */
#progress .dot { position: relative; }
#progress .dot.right::after,
#progress .dot.wrong::after {
  content: '';                                   /* 关键：不用文字，杜绝"一次两个脚印" */
  position: absolute; left: 50%;
  /* 尺寸：盒高 55% × 38px（踏脚石内高）= 20.9px → 印子实测高约 21px。
     旧的 62% / 68% 实测 25.8px，在 38px 高的踏脚石里几乎顶满、把底色挤没了；
     收到 21px 后底色露得出来，"走过这里"和"这一步对/错"两个信息才分得开。
     （仍比最早的 🐾 大：emoji 整只 16px，其中单只爪仅约 8px。） */
  width: 50%; height: 55%;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='10.5 19.8 54.4 60.4'%3E%3Cg fill='%238E5A2B'%3E%3Cellipse cx='28' cy='50' rx='17.5' ry='19'/%3E%3Ccircle cx='47.8' cy='28.8' r='9'/%3E%3Ccircle cx='55.9' cy='42' r='9'/%3E%3Ccircle cx='55.9' cy='58' r='9'/%3E%3Ccircle cx='47.8' cy='71.2' r='9'/%3E%3C/g%3E%3C/svg%3E") center / contain no-repeat;
  z-index: 2;
}
/* 左脚（第 1、3、5… 步）：中线上方 + 趾尖微微上扬（外撇） */
#progress .dot.right:nth-child(odd)::after,
#progress .dot.wrong:nth-child(odd)::after {
  top: 36%;
  transform: translate(-50%, -50%) rotate(-7deg);
  animation: dm-paw-step-l .34s cubic-bezier(.34, 1.56, .64, 1);
}
/* 右脚（第 2、4、6… 步）：中线下方 + 趾尖微微下压；rotate(7deg) scaleY(-1) = 左脚的镜像 */
#progress .dot.right:nth-child(even)::after,
#progress .dot.wrong:nth-child(even)::after {
  top: 64%;
  transform: translate(-50%, -50%) rotate(7deg) scaleY(-1);
  animation: dm-paw-step-r .34s cubic-bezier(.34, 1.56, .64, 1);
}
/* 入场关键帧必须【各自写全 transform】：动画末帧的 transform 会直接接管元素，
   所以左右各一套。（旧的实现用 font-size 生长来回避这一点，换成 SVG 背景后不再适用。） */
@keyframes dm-paw-step-l {
  from { opacity: 0; transform: translate(-50%, -50%) rotate(-7deg) scale(.45); }
  to   { opacity: 1; transform: translate(-50%, -50%) rotate(-7deg) scale(1); }
}
@keyframes dm-paw-step-r {
  from { opacity: 0; transform: translate(-50%, -50%) rotate(7deg) scaleY(-1) scale(.45); }
  to   { opacity: 1; transform: translate(-50%, -50%) rotate(7deg) scaleY(-1) scale(1); }
}

/* ==========================================================
   第三批 · 关末全屏宝箱 + 每关动作钩子
   ========================================================== */

/* ---------- 关末全屏宝箱（替代"直接亮贴纸"） ----------
   结算页出现贴纸 → 盖住直接展示 → 全屏宝箱 → 孩子点击开箱
   → 开箱动效 + 音效 → 贴纸按"性质"做专属入场（鲸鱼转/独角兽蹦/火箭发射…）+ 音效。 */
#dm-chest-overlay {
  position: fixed; inset: 0; z-index: 300;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  /* 故意【不设背景】：宝箱是"浮在结算页上"的一个箱子，不再独占一屏（原来是暖色径向渐变盖满整屏）。
     整层仍然捕获点击 —— 透明只影响视觉；捕获是为了别让孩子在看奖励时误触结算页的"下一关"。 */
  /* 显隐靠 opacity + visibility：display 无法过渡，切 display 就没有淡入淡出。
     默认隐藏；overlay 只在 is-show 期间挂在 DOM 上，所以 display:flex 常开是安全的。 */
  opacity: 0; visibility: hidden;
  transition: opacity .28s ease, visibility 0s linear .28s;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
/* ⚠️ 这条必须有：JS 在 buildChestOverlay() 里 add('is-show')。
   历史 bug（2026-09-15 线上炸过）：整体重做宝箱 CSS 时漏掉了 .is-show，
   于是 overlay 永远 display:none —— 而 buildChestOverlay 又把 #sticker-prize
   设成 visibility:hidden，结果孩子【既看不到宝箱、也看不到贴纸】，结算页一片空白。
   教训：JS 里 add/remove 的每个状态类，CSS 里都必须有对应规则，两端要成对检查。 */
#dm-chest-overlay.is-show {
  opacity: 1; visibility: visible;
  transition: opacity .28s ease, visibility 0s;
}
/* ---------- 宝箱造型：AI 出图（木质箱体 + 银色金属包角 / 拱形绑带 / 挂锁 / 侧提环） ----------
   为什么不用纯 CSS 画：弧形木纹、铆钉、金属高光 CSS 画不出来，上一版画出来像"文件夹"。
   现在换成两张【同构图、同尺寸(565x460)】的 AI 渲染图叠放：关闭图 + 开启图，
   开箱瞬间用「全屏白光爆闪」盖住切换帧，视觉上就是"一闪 → 盖子开了"。
   两张图尺寸完全一致，所以叠放天然对齐（见 kids-math-masters/preview/_cmp_overlay.png 的对位验证）。 */
#dm-chest-overlay .chest-stage {
  position: relative;
  width: min(62vw, 320px);
  aspect-ratio: 565 / 460;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
}

/* 宝箱本体：两张图叠放。drop-shadow 给整体一个落地投影。 */
.dm-chest {
  position: relative; width: 100%; height: 100%;
  transform-origin: 50% 100%;
  filter: drop-shadow(0 16px 20px rgba(120, 60, 10, .34));
}
.dm-chest .chest-img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: contain; display: block;
  pointer-events: none;
  -webkit-user-select: none; user-select: none; -webkit-user-drag: none;
}
.dm-chest .chest-img--closed { z-index: 3; opacity: 1; transition: opacity .07s linear; }
.dm-chest .chest-img--open   { z-index: 4; opacity: 0; transform: scale(.94); transform-origin: 50% 100%; }
#dm-chest-overlay.is-open .dm-chest .chest-img--closed { opacity: 0; }
#dm-chest-overlay.is-open .dm-chest .chest-img--open { animation: dm-chest-pop .5s cubic-bezier(.2, 1.5, .42, 1) forwards; }
/* 开盖的"弹一下"：0.94 → 1.05 → 1，和爆闪同步，峰值落在切换帧上 */
@keyframes dm-chest-pop {
  0%   { opacity: 0; transform: scale(.94); }
  22%  { opacity: 1; transform: scale(1.05); }
  55%  { transform: scale(.985); }
  100% { opacity: 1; transform: scale(1); }
}

/* 金色光晕：平时轻轻呼吸（"这里有好东西"），开箱时爆开 */
.dm-chest .chest-glow {
  position: absolute; left: 50%; top: 50%; width: 158%; height: 158%;
  transform: translate(-50%, -50%); border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 240, 175, .62) 0%, rgba(255, 212, 120, .3) 38%, rgba(255, 190, 90, 0) 70%);
  opacity: 0; z-index: 0; pointer-events: none;
}
#dm-chest-overlay:not(.is-open) .dm-chest .chest-glow { animation: dm-glow-idle 2.6s ease-in-out infinite; }
@keyframes dm-glow-idle { 0%, 100% { opacity: .34; transform: translate(-50%, -50%) scale(.92); } 50% { opacity: .74; transform: translate(-50%, -50%) scale(1.06); } }

/* 光芒射线：开箱瞬间从箱底向外放射。
   做法 = repeating-conic-gradient 画放射条纹 + 径向 mask 把边缘淡出成圆形光晕。 */
.dm-chest .chest-rays {
  position: absolute; left: 50%; top: 52%; width: 340%; height: 340%;
  transform: translate(-50%, -50%) scale(.22) rotate(0deg);
  background: repeating-conic-gradient(from 0deg, rgba(255, 249, 214, .92) 0deg 3.2deg, rgba(255, 249, 214, 0) 3.2deg 16deg);
  -webkit-mask-image: radial-gradient(circle, rgba(0, 0, 0, .95) 0%, rgba(0, 0, 0, .6) 30%, rgba(0, 0, 0, 0) 60%);
  mask-image: radial-gradient(circle, rgba(0, 0, 0, .95) 0%, rgba(0, 0, 0, .6) 30%, rgba(0, 0, 0, 0) 60%);
  opacity: 0; z-index: 1; pointer-events: none;
}
#dm-chest-overlay.is-open .dm-chest .chest-rays { animation: dm-rays 1.1s ease-out forwards; }
@keyframes dm-rays {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(.22) rotate(0deg); }
  20%  { opacity: .95; }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1.45) rotate(90deg); }
}

/* 冲击波圆环：一圈金色能量环向外扩 */
.dm-chest .chest-shock {
  position: absolute; left: 50%; top: 52%; width: 46%; aspect-ratio: 1;
  transform: translate(-50%, -50%) scale(.2); border-radius: 50%;
  border: 6px solid rgba(255, 238, 172, .92);
  box-shadow: 0 0 24px rgba(255, 220, 130, .85), inset 0 0 24px rgba(255, 220, 130, .6);
  opacity: 0; z-index: 6; pointer-events: none;
}
#dm-chest-overlay.is-open .dm-chest .chest-shock { animation: dm-shock .8s cubic-bezier(.2, .7, .3, 1) forwards; }
@keyframes dm-shock {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(.2); }
  16%  { opacity: .95; }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(2.5); }
}

/* 迸发星星：12 颗从箱口向外飞散。方向由每颗自己的 --tx / --ty 决定（椭圆分布，横向铺得更开）。 */
.dm-chest .chest-sparks { position: absolute; left: 50%; top: 44%; width: 0; height: 0; z-index: 7; pointer-events: none; }
.dm-chest .chest-sparks i {
  position: absolute; left: 0; top: 0; width: 15px; height: 15px; margin: -7.5px 0 0 -7.5px;
  background: radial-gradient(circle, #fff 0%, #ffe9a8 45%, rgba(255, 233, 168, 0) 72%);
  clip-path: polygon(50% 0, 61% 39%, 100% 50%, 61% 61%, 50% 100%, 39% 61%, 0 50%, 39% 39%);
  opacity: 0; transform: translate(0, 0) scale(.2);
}
#dm-chest-overlay.is-open .dm-chest .chest-sparks i { animation: dm-spark-fly .95s cubic-bezier(.15, .85, .25, 1) forwards; }
@keyframes dm-spark-fly {
  0%   { opacity: 0; transform: translate(0, 0) scale(.2) rotate(0deg); }
  14%  { opacity: 1; transform: translate(calc(var(--tx) * .2), calc(var(--ty) * .2)) scale(1.15) rotate(45deg); }
  100% { opacity: 0; transform: translate(var(--tx), var(--ty)) scale(.35) rotate(230deg); }
}
.dm-chest .chest-sparks i:nth-child(1)  { --tx: 132px; --ty: 0px;    animation-delay: 0s; }
.dm-chest .chest-sparks i:nth-child(2)  { --tx: 114px; --ty: 50px;   animation-delay: .03s; }
.dm-chest .chest-sparks i:nth-child(3)  { --tx: 66px;  --ty: 87px;   animation-delay: .06s; }
.dm-chest .chest-sparks i:nth-child(4)  { --tx: 0px;   --ty: 100px;  animation-delay: .09s; }
.dm-chest .chest-sparks i:nth-child(5)  { --tx: -66px; --ty: 87px;   animation-delay: .12s; }
.dm-chest .chest-sparks i:nth-child(6)  { --tx: -114px; --ty: 50px;  animation-delay: .15s; }
.dm-chest .chest-sparks i:nth-child(7)  { --tx: -132px; --ty: 0px;   animation-delay: .18s; }
.dm-chest .chest-sparks i:nth-child(8)  { --tx: -114px; --ty: -50px; animation-delay: .21s; }
.dm-chest .chest-sparks i:nth-child(9)  { --tx: -66px; --ty: -87px;  animation-delay: .24s; }
.dm-chest .chest-sparks i:nth-child(10) { --tx: 0px;   --ty: -100px; animation-delay: .27s; }
.dm-chest .chest-sparks i:nth-child(11) { --tx: 66px;  --ty: -87px;  animation-delay: .3s; }
.dm-chest .chest-sparks i:nth-child(12) { --tx: 114px; --ty: -50px;  animation-delay: .33s; }

/* 平时"闪闪发光"①：箱体周围 6 颗小星错峰闪烁 */
.dm-chest .chest-sparkles { position: absolute; inset: -4%; z-index: 8; pointer-events: none; }
.dm-chest .chest-sparkles i {
  position: absolute; width: 13px; height: 13px;
  background: radial-gradient(circle, #fff 0%, #ffe9a8 45%, rgba(255, 233, 168, 0) 72%);
  clip-path: polygon(50% 0, 61% 39%, 100% 50%, 61% 61%, 50% 100%, 39% 61%, 0 50%, 39% 39%);
  opacity: 0; animation: dm-sparkle 2.2s ease-in-out infinite;
}
.dm-chest .chest-sparkles i:nth-child(1) { left: 2%;  top: 10%; width: 13px; height: 13px; animation-delay: 0s; }
.dm-chest .chest-sparkles i:nth-child(2) { left: 88%; top: 16%; width: 10px; height: 10px; animation-delay: .5s; }
.dm-chest .chest-sparkles i:nth-child(3) { left: 8%;  top: 62%; width: 15px; height: 15px; animation-delay: 1s; }
.dm-chest .chest-sparkles i:nth-child(4) { left: 82%; top: 66%; width: 9px;  height: 9px;  animation-delay: 1.5s; }
.dm-chest .chest-sparkles i:nth-child(5) { left: 24%; top: 1%;  width: 11px; height: 11px; animation-delay: .85s; }
.dm-chest .chest-sparkles i:nth-child(6) { left: 66%; top: 4%;  width: 12px; height: 12px; animation-delay: 1.3s; }
@keyframes dm-sparkle { 0%, 100% { opacity: 0; transform: scale(.4) rotate(0deg); } 50% { opacity: 1; transform: scale(1) rotate(45deg); } }

/* 平时"闪闪发光"②：一道高光扫过箱体。
   关键：拿宝箱自己的 PNG 当 mask，高光才会【只落在箱子身上】而不是糊一条白带在空气里。
   mask 用 @supports 兜底 —— 万一浏览器不支持图片遮罩，这层干脆不显示（宁可不做，不能出错）。 */
@supports (mask-image: linear-gradient(#000, #000)) {
  .dm-chest .chest-sheen {
    position: absolute; inset: 0; z-index: 5; pointer-events: none;
    background: linear-gradient(102deg, rgba(255, 255, 255, 0) 42%, rgba(255, 255, 255, .6) 50%, rgba(255, 255, 255, 0) 58%);
    background-size: 260% 100%; background-repeat: no-repeat;
    -webkit-mask-image: url("assets/chest/chest_closed.png");
    mask-image: url("assets/chest/chest_closed.png");
    -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
    -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
    animation: dm-sheen 3.4s ease-in-out infinite;
  }
  #dm-chest-overlay.is-open .dm-chest .chest-sheen {
    -webkit-mask-image: url("assets/chest/chest_open.png");
    mask-image: url("assets/chest/chest_open.png");
  }
}
@keyframes dm-sheen {
  0%        { background-position: 140% 0; opacity: .9; }
  55%       { background-position: -60% 0; opacity: .9; }
  56%, 100% { background-position: -60% 0; opacity: 0; }
}

/* 待开：整体轻轻呼吸，提示"点我" */
#dm-chest-overlay:not(.is-open) .chest-stage { animation: dm-chest-breathe 1.9s ease-in-out infinite; }
@keyframes dm-chest-breathe { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-9px) scale(1.025); } }

/* 开箱：光晕爆开 */
#dm-chest-overlay.is-open .dm-chest .chest-glow { animation: dm-glow 1.05s ease-out forwards; }
@keyframes dm-glow {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(.2); }
  26%  { opacity: 1; transform: translate(-50%, -50%) scale(1.05); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(7); }
}

/* 白光爆闪：作用仍是"切换帧遮罩"（挡住关闭图→开启图那一帧），但已从【整屏】改成【局部】——
   它现在挂在 .chest-stage 里、以宝箱为中心爆开。整屏刷白会把结算页闪没，与"宝箱浮在结算页上"冲突。 */
#dm-chest-overlay .chest-flash {
  position: absolute; left: 50%; top: 50%; width: 235%; aspect-ratio: 1;
  border-radius: 50%; z-index: 30; pointer-events: none; opacity: 0;
  transform: translate(-50%, -50%) scale(.4);
  background: radial-gradient(circle, rgba(255, 255, 255, .98) 0%, rgba(255, 248, 216, .86) 24%, rgba(255, 226, 150, .34) 46%, rgba(255, 215, 120, 0) 68%);
}
#dm-chest-overlay.is-open .chest-flash { animation: dm-flash .72s ease-out forwards; }
@keyframes dm-flash {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(.4); }
  12%  { opacity: 1; transform: translate(-50%, -50%) scale(.92); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1.4); }
}


#dm-chest-overlay .chest-hint {
  margin-top: 5vh; font-size: clamp(18px, 2.6vw, 26px); font-weight: 800; color: #7a4a16;
  text-shadow: 0 2px 0 #fff; animation: dm-pop 2.2s ease-in-out infinite;
}
/* 开箱后飞出的贴纸卡片 */
#dm-chest-overlay .chest-sticker {
  position: absolute; left: 50%; top: 42%;
  transform: translate(-50%, -50%) scale(0); opacity: 0; z-index: 5;
  width: clamp(200px, 46vw, 320px); height: clamp(200px, 46vw, 320px);
  font-size: 0;
  /* 弹入用 transition（不是 keyframes）：这样"飞回收纳位"能另起一套 animation，两者不打架 */
  transition: opacity .3s ease, transform .46s cubic-bezier(.34, 1.56, .64, 1);
}
#dm-chest-overlay .chest-name {
  position: absolute; left: 0; right: 0; top: 74%; text-align: center;
  font-size: clamp(18px, 2.6vw, 26px); font-weight: 800; color: #7a4a16;
  text-shadow: 0 2px 0 #fff; opacity: 0;
}
#dm-chest-overlay.is-revealed .chest-sticker { opacity: 1; transform: translate(-50%, -50%) scale(1); }
#dm-chest-overlay.is-revealed .chest-name { animation: dm-name-in .5s ease .2s forwards; }
@keyframes dm-name-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }

/* ---------- 卡片出现 → 宝箱"功成身退" ----------
   用户要求：点开、卡片出来后宝箱就该消失，只留卡片停几秒。 */
.dm-chest { transition: opacity .26s ease, transform .32s ease; }
#dm-chest-overlay.is-revealed .dm-chest { opacity: 0; transform: scale(.72); }
/* 已经点开了，"点一下宝箱"的提示就收掉（dm-pop 在动 opacity，所以要连 animation 一起关） */
#dm-chest-overlay.is-open .chest-hint { animation: none; opacity: 0; }

/* ---------- 卡片"收进"结算页的贴纸位 ----------
   位移与缩放差值由 JS 用 getBoundingClientRect() 实测后写进 --fly-x / --fly-y / --fly-scale，
   这里只负责把这段路走出"被吸进去"的手感：末段才淡出，落点上正好与贴纸位重叠交接。 */
@keyframes dm-card-fly {
  0%   { opacity: 1; transform: translate(-50%, -50%) translate(0, 0) scale(1); }
  70%  { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -50%) translate(var(--fly-x, 0px), var(--fly-y, 0px)) scale(var(--fly-scale, .3)); }
}
#dm-chest-overlay.is-flying .chest-sticker {
  animation: dm-card-fly var(--fly-dur, .68s) cubic-bezier(.46, .03, .3, 1) forwards;
}
/* 名字不跟着飞，起飞前先淡掉 */
#dm-chest-overlay.is-flying .chest-name { animation: none; opacity: 0; transition: opacity .18s ease; }

/* 落地：结算页真正的贴纸位"收下"它，弹一下（落点感） */
.sticker-prize.dm-landed { animation: dm-slot-pop .52s cubic-bezier(.34, 1.56, .64, 1); }
@keyframes dm-slot-pop { 0% { transform: scale(.8); } 58% { transform: scale(1.07); } 100% { transform: scale(1); } }

/* ==========================================================
   贴纸 = AI 生成的黏土立体插画（透明底 PNG）+ CSS「招牌动作」
   ------------------------------------------------------------
   结构：
     .chest-sticker
       └ .sticker-wrap.cx-<名>.m-<动作>     ← 入场动画 + 脚底阴影同步
           └ img.sticker-img.m-<动作>        ← 持续「招牌动作」

   为什么必须拆两层：同一个元素同一时刻只能跑一组 transform 动画，
   而"入场"和"持续动作"都要改 transform，只能父子各管一层。

   全部只动 transform / filter（走合成器，不触发重排）。
   旧版"canvas 逐帧重绘 16 个角色"的 rAF 循环已整体移除——
   现在动作零 JS 开销，美术质量由 AI 出图保证。
   贴图：assets/stickers/out_<名>.png（512×512 透明底，清单见 _meta.json）
   ========================================================== */
#dm-chest-overlay .chest-sticker .sticker-wrap {
  position: relative; width: 100%; height: 100%;
  transform-origin: 50% 100%;
  animation: dm-char-pop .6s cubic-bezier(.34,1.56,.64,1) both;
}
#dm-chest-overlay .chest-sticker .sticker-img {
  width: 100%; height: 100%; display: block; object-fit: contain;
  /* ⚠️ transform-origin 绝对不能写在这里：
     本规则含 #id，(1,2,0) 会盖掉下方 .sticker-img.m-twinkle 的 (0,2,0) 中心覆盖，
     导致自转类绕底边转 → 飞出画面。统一放到下面「变换原点」区。 */
  will-change: transform;
  animation: m-idle 3.6s ease-in-out infinite;
}
/* 脚底接触阴影：让"跳/跑/蹦"有落点感。飞行类角色在下面 display:none */
.sticker-wrap::after {
  content: ''; position: absolute; left: 50%; bottom: 3%;
  width: 52%; height: 7%; margin-left: -26%; border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(ellipse at center, rgba(60,40,80,.3), rgba(60,40,80,0) 70%);
}
.cx-whale::after, .cx-octopus::after, .cx-balloon::after, .cx-rocket::after,
.cx-butterfly::after, .cx-rainbow::after, .cx-star::after { display: none; }

@keyframes dm-char-pop { 0% { opacity:0; transform: scale(.3); } 60% { opacity:1; transform: scale(1.08); } 100% { opacity:1; transform: scale(1); } }

/* ---------- 入场：各角色"来"的方式不同 ---------- */
.cx-whale, .cx-octopus       { animation: dm-swim-in 1.1s cubic-bezier(.2,1,.4,1) both; }
.cx-unicorn, .cx-fox, .cx-tiger { animation: dm-run-in 1s cubic-bezier(.2,1,.4,1) both; }
.cx-penguin                  { animation: dm-slide-in 1s cubic-bezier(.2,1,.4,1) both; }
.cx-rocket, .cx-balloon      { animation: dm-rise-in 1s cubic-bezier(.3,1,.4,1) both; }
.cx-panda                    { animation: dm-roll-in 1s cubic-bezier(.2,1.4,.4,1) both; }
.cx-frog                     { animation: dm-hop-in .9s cubic-bezier(.3,1.3,.5,1) both; }
.cx-butterfly                { animation: dm-flutter-in 1.1s cubic-bezier(.3,1,.4,1) both; }
/* 恐龙：三只都是"重物踩下来"的入场 */
.cx-trex, .cx-triceratops, .cx-pachy { animation: dm-stomp-in 1s cubic-bezier(.3,1.25,.45,1) both; }
.cx-strawberry, .cx-trophy, .cx-star { animation: dm-drop-in .9s cubic-bezier(.3,1.4,.5,1) both; }
.cx-koala, .cx-lollipop, .cx-rainbow { animation: dm-char-pop .7s cubic-bezier(.34,1.56,.64,1) both; }

@keyframes dm-swim-in    { 0% { opacity:0; transform: translateX(-120px) scale(.7); } 60% { opacity:1; transform: translateX(8px) scale(1.05); } 100% { opacity:1; transform: translateX(0) scale(1); } }
@keyframes dm-run-in     { 0% { opacity:0; transform: translateX(-140px) translateY(-6px) rotate(-8deg) scale(.8); } 55% { opacity:1; transform: translateX(10px) rotate(3deg) scale(1.04); } 100% { opacity:1; transform: translateX(0) rotate(0) scale(1); } }
@keyframes dm-slide-in   { 0% { opacity:0; transform: translateX(120px) rotate(8deg); } 100% { opacity:1; transform: translateX(0) rotate(0); } }
@keyframes dm-rise-in    { 0% { opacity:0; transform: translateY(90px) scale(.8); } 100% { opacity:1; transform: translateY(0) scale(1); } }
@keyframes dm-roll-in    { 0% { opacity:0; transform: rotate(-380deg) scale(.5); } 100% { opacity:1; transform: rotate(0) scale(1); } }
@keyframes dm-hop-in     { 0% { opacity:0; transform: translateY(70px) scale(1.14,.86); } 45% { opacity:1; transform: translateY(-22px) scale(.95,1.07); } 75% { transform: translateY(0) scale(1.06,.94); } 100% { opacity:1; transform: translateY(0) scale(1); } }
@keyframes dm-flutter-in { 0% { opacity:0; transform: translate(-70px,-40px) rotate(-12deg) scale(.7); } 70% { opacity:1; transform: translate(6px,4px) rotate(4deg) scale(1.03); } 100% { opacity:1; transform: translate(0,0) rotate(0) scale(1); } }
/* 恐龙入场：从高处砸下来 + 落地那一瞬压扁 */
@keyframes dm-stomp-in   { 0% { opacity:0; transform: translateY(-140px) scale(.85); } 55% { opacity:1; transform: translateY(0) scale(1.12,.86); } 72% { transform: translateY(-16px) scale(.97,1.04); } 88% { transform: translateY(0) scale(1.05,.95); } 100% { opacity:1; transform: translateY(0) scale(1); } }
@keyframes dm-drop-in    { 0% { opacity:0; transform: translateY(-120px) scale(.6); } 55% { opacity:1; transform: translateY(10px) scale(1.06); } 75% { transform: translateY(-8px) scale(1); } 100% { opacity:1; transform: translateY(0) scale(1); } }

/* 仍被别处引用（.dm-friend / 跷跷板 .vs），必须保留 */
@keyframes dm-bob { 0%,100% { transform: translateY(0) rotate(0); } 50% { transform: translateY(-7px) rotate(-2deg); } }

/* ---------- 招牌动作关键帧（每个角色一个） ---------- */
@keyframes m-idle    { 0%,100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-5px) scale(1.015); } }
/* 阴影跟随"离地"变淡变小，与身体同周期 → 自动同步 */
@keyframes m-shadow  { 0%,100% { transform: scale(1); opacity:.9; } 35% { transform: scale(.7); opacity:.5; } 60% { transform: scale(1.05); opacity:.95; } }

/* 鲸鱼 / 章鱼：游 —— 左右摆 + 上下浮 */
@keyframes m-swim   { 0%,100% { transform: translate(-6px,2px) rotate(-3deg); } 25% { transform: translate(-1px,-7px) rotate(0); } 50% { transform: translate(6px,2px) rotate(3deg); } 75% { transform: translate(-1px,-4px) rotate(0); } }
/* 考拉：轻摇 */
@keyframes m-sway   { 0%,100% { transform: rotate(-5deg) translateY(0); } 50% { transform: rotate(5deg) translateY(-4px); } }
/* 熊猫：圆滚滚左右倒 */
@keyframes m-wobble { 0%,100% { transform: rotate(-6deg) translateX(-4px); } 25% { transform: rotate(0) translateY(-5px); } 50% { transform: rotate(6deg) translateX(4px); } 75% { transform: rotate(0) translateY(-5px); } }
/* 狐狸 / 老虎：小跑 —— 前后颠 + 微移 */
@keyframes m-trot   { 0%,100% { transform: translate(-7px,0) rotate(-2deg); } 25% { transform: translate(-2px,-8px) rotate(0) scale(1.01,1.03); } 50% { transform: translate(7px,0) rotate(2deg); } 75% { transform: translate(2px,-8px) rotate(0) scale(1.01,1.03); } }
/* 青蛙 / 棒棒糖：弹跳（挤压拉伸） */
@keyframes m-hop    { 0%,100% { transform: translateY(0) scale(1,1); } 10% { transform: translateY(0) scale(1.12,.88); } 32% { transform: translateY(-34px) scale(.94,1.09); } 52% { transform: translateY(0) scale(1.1,.9); } 64% { transform: translateY(-13px) scale(.98,1.03); } 78% { transform: translateY(0) scale(1.04,.96); } }
/* 企鹅：摇摇摆摆走 */
@keyframes m-waddle { 0%,100% { transform: rotate(0) translateX(-6px); } 25% { transform: rotate(-9deg) translateY(-4px); } 50% { transform: rotate(0) translateX(6px); } 75% { transform: rotate(9deg) translateY(-4px); } }
/* 彩虹：微闪 + 提亮呼吸 */
@keyframes m-shimmer { 0%,100% { transform: scale(1); filter: brightness(1) saturate(1); } 50% { transform: scale(1.035); filter: brightness(1.16) saturate(1.22); } }
/* 火箭：蓄力抖动 → 猛窜 → 回落 */
@keyframes m-launch { 0% { transform: translateY(0) scale(1,1); } 10% { transform: translateY(3px) scale(1.05,.95); } 22% { transform: translateY(-30px) scale(.96,1.06); } 32% { transform: translateY(-10px) scale(1.04,.96); } 46% { transform: translateY(-20px); } 68% { transform: translateY(-8px); } 100% { transform: translateY(0) scale(1,1); } }
/* 气球：上下浮 + 左右轻摆 */
@keyframes m-float  { 0%,100% { transform: translateY(0) rotate(-3deg); } 50% { transform: translateY(-18px) rotate(3deg); } }
/* 草莓：连续弹 */
@keyframes m-bounce { 0%,100% { transform: translateY(0) scale(1,1); } 20% { transform: translateY(-20px) scale(.96,1.05); } 40% { transform: translateY(0) scale(1.08,.92); } 60% { transform: translateY(-9px) scale(.99,1.02); } 80% { transform: translateY(0) scale(1.04,.96); } }
/* 独角兽：飞奔 */
@keyframes m-run    { 0%,100% { transform: translate(-10px,0) rotate(-3deg); } 25% { transform: translate(-3px,-11px) rotate(0); } 50% { transform: translate(10px,0) rotate(3deg); } 75% { transform: translate(3px,-11px) rotate(0); } }
/* 蝴蝶：扁平贴图用"水平压缩"模拟双翅开合 + 轻飘 */
@keyframes m-flap   { 0%,100% { transform: translateY(0) scaleX(1) scaleY(1); } 25% { transform: translateY(-9px) scaleX(.7) scaleY(1.07); } 50% { transform: translateY(-2px) scaleX(1.06) scaleY(.95); } 75% { transform: translateY(-10px) scaleX(.76) scaleY(1.06); } }
/* 霸王龙：沉重双踏 —— 抬起 → 砸地压扁，节奏比"小跑"慢一倍 */
@keyframes m-stomp  { 0%,100% { transform: translateY(0) scale(1,1); } 14% { transform: translateY(0) scale(1.1,.88); } 34% { transform: translateY(-22px) scale(.95,1.07) rotate(-1deg); } 50% { transform: translateY(0) scale(1.14,.84); } 66% { transform: translateY(-13px) scale(.98,1.04) rotate(1deg); } 82% { transform: translateY(0) scale(1.07,.93); } }
/* 三角龙：低头冲锋 —— 先后坐蓄力，再整个向前顶出去 */
@keyframes m-charge { 0%,100% { transform: translate(0,0) rotate(0); } 18% { transform: translate(-8px,3px) rotate(3deg) scale(1.03,.97); } 44% { transform: translate(16px,-4px) rotate(-6deg) scale(.97,1.04); } 62% { transform: translate(6px,0) rotate(-2deg); } 78% { transform: translate(12px,-2px) rotate(-4deg); } }
/* 肿头龙：顶头槌 —— 后仰蓄势 → 猛地向前一顶 → 反弹回位 */
@keyframes m-bash   { 0%,100% { transform: translate(0,0) rotate(0) scale(1,1); } 16% { transform: translate(-10px,0) rotate(4deg) scale(1.03,.97); } 34% { transform: translate(14px,-3px) rotate(-7deg) scale(.95,1.05); } 46% { transform: translate(2px,1px) rotate(2deg) scale(1.05,.95); } 64% { transform: translate(-3px,-5px) rotate(0) scale(1,1); } 82% { transform: translate(1px,0) rotate(0); } }
/* 奖杯：高光闪 */
@keyframes m-shine  { 0%,100% { transform: scale(1) rotate(0); filter: brightness(1); } 50% { transform: scale(1.06) rotate(-2deg); filter: brightness(1.25) drop-shadow(0 0 12px rgba(255,214,102,.9)); } }
/* 星星（彩蛋）：一闪一闪 */
@keyframes m-twinkle { 0%,100% { transform: scale(.94) rotate(-7deg); opacity:.92; } 50% { transform: scale(1.09) rotate(7deg); opacity:1; } }
/* 通用庆祝（备用：结算/彩蛋大赏） */
@keyframes m-cheer  { 0%,100% { transform: translateY(0) scale(1) rotate(0); } 25% { transform: translateY(-24px) scale(1.06) rotate(-5deg); } 50% { transform: translateY(0) scale(1,.94); } 75% { transform: translateY(-15px) scale(1.04) rotate(5deg); } }

/* ---------- 动作 ↔ 角色绑定 ----------
   同时给"身体"和"脚底阴影"定同一节奏（时长/缓动一致 → 起跳时阴影同步变淡） */
.sticker-img.m-idle    { animation: m-idle    3.6s ease-in-out infinite; }
.sticker-img.m-swim    { animation: m-swim    3.2s ease-in-out infinite; }
.sticker-img.m-sway    { animation: m-sway    2.6s ease-in-out infinite; }
.sticker-wrap.m-sway::after    { animation: m-shadow 2.6s ease-in-out infinite; }
.sticker-img.m-wobble  { animation: m-wobble  3s   ease-in-out infinite; }
.sticker-wrap.m-wobble::after  { animation: m-shadow 3s   ease-in-out infinite; }
.sticker-img.m-trot    { animation: m-trot    1.5s ease-in-out infinite; }
.sticker-wrap.m-trot::after    { animation: m-shadow 1.5s ease-in-out infinite; }
.sticker-img.m-hop     { animation: m-hop     1.15s cubic-bezier(.3,1.05,.45,1) infinite; }
.sticker-wrap.m-hop::after     { animation: m-shadow 1.15s cubic-bezier(.3,1.05,.45,1) infinite; }
.sticker-img.m-waddle  { animation: m-waddle  1.9s ease-in-out infinite; }
.sticker-wrap.m-waddle::after  { animation: m-shadow 1.9s ease-in-out infinite; }
.sticker-img.m-shimmer { animation: m-shimmer 3s   ease-in-out infinite; }
.sticker-img.m-launch  { animation: m-launch  2.4s ease-in-out infinite; }
.sticker-img.m-float   { animation: m-float   3.4s ease-in-out infinite; }
.sticker-img.m-bounce  { animation: m-bounce  1.3s ease-in-out infinite; }
.sticker-wrap.m-bounce::after  { animation: m-shadow 1.3s ease-in-out infinite; }
.sticker-img.m-run     { animation: m-run     1.1s ease-in-out infinite; }
.sticker-wrap.m-run::after     { animation: m-shadow 1.1s ease-in-out infinite; }
.sticker-img.m-flap    { animation: m-flap    1.6s ease-in-out infinite; }
.sticker-img.m-stomp   { animation: m-stomp   1.8s ease-in-out infinite; }
.sticker-wrap.m-stomp::after   { animation: m-shadow 1.8s ease-in-out infinite; }
.sticker-img.m-charge  { animation: m-charge  2.1s ease-in-out infinite; }
.sticker-wrap.m-charge::after  { animation: m-shadow 2.1s ease-in-out infinite; }
.sticker-img.m-bash    { animation: m-bash    1.6s ease-in-out infinite; }
.sticker-wrap.m-bash::after    { animation: m-shadow 1.6s ease-in-out infinite; }
.sticker-img.m-shine   { animation: m-shine   2.2s ease-in-out infinite; }
.sticker-img.m-twinkle { animation: m-twinkle 1.6s ease-in-out infinite; }
.sticker-img.m-cheer   { animation: m-cheer   1.4s ease-in-out infinite; }
.sticker-wrap.m-cheer::after   { animation: m-shadow 1.4s ease-in-out infinite; }

/* ---------- 变换原点（必须留在「低特异性」选择器上） ----------
   默认「贴地」：绕脚底转，跳 / 跑 / 摇才有蹬地感。
   自转 / 悬浮类必须显式改回几何中心，否则会像被甩出去的轮子。
   ⚠️ 踩过一次坑（用户截图「向日葵转的不对」暴露的）：当时 m-spin 的
      (0,2,0) 中心覆盖被上面 #id 规则的 transform-origin 盖掉，整朵花绕底边
      转 → 180° 时被甩到盒子下方、飞出卡片。前车之鉴，别再往里加 origin。
   这里的选择器是 (0,1,0)/(0,2,0)，务必不要合并进 #dm-chest-overlay 的规则里。 */
.sticker-img { transform-origin: 50% 100%; }
.sticker-img.m-shine, .sticker-img.m-twinkle,
.sticker-img.m-swim, .sticker-img.m-float, .sticker-img.m-flap,
.sticker-img.m-shimmer, .sticker-img.m-launch, .sticker-img.m-cheer {
  transform-origin: 50% 50%;
}

/* ==========================================================
   图鉴 / 最近获得：把 emoji 换成插画
   —— 静态陈列（十几格同时动会吵、也费电），点一下才播放它的招牌动作
   ========================================================== */
.album-img {
  width: 86%; height: 86%; object-fit: contain; display: block;
  pointer-events: none;                 /* 点击事件交给 .album-cell */
}
.album-cell.got { cursor: pointer; -webkit-tap-highlight-color: transparent; }
.album-cell.got:active { transform: scale(.93); }
/* "下一张"剪影：直接用插画做暗色剪影，比 emoji 剪影更能勾起"想要"
   （filter 见 styles.css 的 .album-cell .album-silhouette） */
.album-cell .album-silhouette { width: 88%; height: 88%; object-fit: contain; }

/* 结算页贴纸位里的插画：结构与开箱卡片一致，尺寸由 styles.css 的 .sticker-new 给 */
#sticker-prize .sticker-wrap { position: relative; width: 100%; height: 100%; }
#sticker-prize .sticker-img { width: 100%; height: 100%; display: block; object-fit: contain; }

.sticker-board .sticker-img-sm {
  width: 86%; height: 86%; object-fit: contain; display: block;
  animation: m-idle 3.6s ease-in-out infinite;
}

/* ---------- 每关一个动作钩子 ---------- */
/* 加法 合体：+ 号脉动，提示"两组数要合成一个" */
#shapes.dm-mode-add .shape-sep { animation: dm-sep-pulse 1.4s ease-in-out infinite; color: #FF7A3C; font-weight: 900; }
@keyframes dm-sep-pulse { 0%,100% { transform: scale(1); opacity: .7; } 50% { transform: scale(1.5); opacity: 1; } }

/* 减法 吃掉：被吃豆人吃掉的数"缩小上飘消失"。
   ★ 开局不预告：5 个苹果长得一模一样 —— 不置灰、不划线、不加虚线框。
     styles.css 里 .shape.taken 自带 "opacity:.3 + grayscale(1) + 斜划线"，
     那是"这题会吃掉哪几个"的剧透。孩子该做的是先把 5-2 算出来，
     答对后看着吃豆人把其中 2 个吃掉，用"变少"这个动作去验证答案 ——
     苹果还没被吃就先灰掉，等于把答案的形状提前摆在眼前了。
     所以这一层把 taken 的外观抹平（纯覆盖，没动 styles.css / app.js）。
     .dm-eat-now 的 dm-eat 动画照旧生效：动画优先级高于普通声明，
     吃起来仍然是缩小上飘消失。 */
#shapes.dm-mode-sub .shape.taken {
  opacity: 1; filter: none;
  outline: none; outline-offset: 0;    /* 旧版挂的"预告"虚线框 */
}
#shapes.dm-mode-sub .shape.taken::after { content: none; }   /* 旧版挂的"预告"斜划线 */
#shapes.dm-mode-sub .shape.taken.dm-eat-now { animation: dm-eat .9s ease-out forwards; }
@keyframes dm-eat { 0% { opacity: 1; transform: translateY(0) scale(1); } 45% { opacity: 1; transform: translateY(-8px) scale(1.12); } 100% { opacity: 0; transform: translateY(-30px) scale(.15); } }

/* 数一数 陪数：点一下图形就跳一下 */
#shapes.dm-mode-count { position: relative; }
#shapes.dm-mode-count .shape { cursor: pointer; }
#shapes.dm-mode-count .shape.dm-tap { animation: dm-shape-tap .4s ease; }
@keyframes dm-shape-tap { 0% { transform: scale(1); } 40% { transform: scale(1.35) translateY(-6px); } 100% { transform: scale(1); } }

/* 分一分 分享：出现一个"朋友"（挂在右筐上；出场时机见下面的 .dm-mascot-in） */
.dm-friend {
  position: absolute; right: -26px; top: -9px; font-size: clamp(28px, 4.2vw, 42px);
  pointer-events: none; opacity: 0; z-index: 6;
  filter: drop-shadow(0 3px 3px rgba(0, 0, 0, .2));
}

/* 比多少 跷跷板：开局水平（dm-see-flat），答对后重的一边压下去、轻的一边翘起 */
.dm-seesaw .dm-beam {
  transform-origin: 50% 50%;
  transition: transform .55s cubic-bezier(.34, 1.45, .55, 1);
}
#shapes.dm-see-flat  .dm-seesaw .dm-beam { transform: none; }          /* 开局水平 */
#shapes.dm-see-left  .dm-seesaw .dm-beam { transform: rotate(-4.5deg); }  /* 左端下沉 */
#shapes.dm-see-right .dm-seesaw .dm-beam { transform: rotate(4.5deg); }   /* 右端下沉 */
#shapes.dm-see-left  .side.left  { transform: translateY(12px) rotate(-4deg); }
#shapes.dm-see-right .side.right { transform: translateY(12px) rotate(4deg); }
#shapes.dm-see-left  .side.right { transform: translateY(-8px); }      /* 轻的一边翘起 */
#shapes.dm-see-right .side.left  { transform: translateY(-8px); }
#shapes.dm-see-left .side, #shapes.dm-see-right .side { transition: transform .5s cubic-bezier(.34, 1.45, .55, 1); transform-origin: 50% 88%; }
#shapes.dm-see-left  .vs, #shapes.dm-see-right .vs { animation: dm-bob 2s ease-in-out infinite; }

/* ==========================================================
   第四批 · 六个玩法 aha 钩子强化（加法合堆 / 减法小熊吃 /
   数一数星座 / 分一分分享 / 比多少跷跷板 / 凑十BOSS弱点+💎）
   ========================================================== */

/* 需要定位的 #shapes 模式：自己插入的小熊/朋友/跷跷板都要相对它定位 */
#shapes.dm-mode-sub, #shapes.dm-mode-split,
#shapes.dm-see-left, #shapes.dm-see-right, #shapes.dm-see-flat { position: relative; }

/* —— 算式题：开局就把图形显示出来 ——
   app.js v68 会把加/减题的图形先置 is-pending（opacity:0）藏起来，答完才"长出来"，
   本意是不让孩子直接数图形出答案。但这样一来：
     · 加法的"两组滑到一起合成一堆"没法演 —— 框里是空的，合并了什么也看不见；
     · 减法看不到"5 个里被吃掉 2 个"。
   所以在愉悦层把它们直接显示出来（纯 CSS 覆盖，没动 app.js）。
   想恢复 v68 的藏法：删掉下面这条规则即可，其余都不用动。 */
#shapes.dm-mode-add .shape.is-pending,
#shapes.dm-mode-sub .shape.is-pending { opacity: 1; transform: none; }
/* 减法：开局 5 个苹果一视同仁，不做任何"哪几个会被吃"的标记（见上面 taken 的说明） */

/* —— 加法：左右两个框 → 答对后撞到一起合并成一个框 —— */
/* gap 要参与"合拢"的过渡：#shapes 的 flex gap 在 sep 两侧各占一份，合拢时这份占位也得收掉。
   为什么收 gap 而不是给 sep 加负 margin：gap 是被媒体查询改过的（窄屏 6/8px、宽屏 12px），
   写死 -12px 会在 gap 小的屏上过度收缩、把两框压得重叠（实测中缝 -2px）。收 gap 则天然适配。 */
#shapes.dm-mode-add {
  position: relative;
  transition: gap .32s cubic-bezier(.34, .9, .2, 1) .15s;
}

/* 两个框：左边暖色 = 第一组，右边冷色 = 第二组（答对后两者会统一成一个框的配色） */
.dm-add-group {
  position: relative;
  display: inline-flex; flex-wrap: nowrap; align-items: center; justify-content: center;
  flex: 0 0 auto;
  gap: 6px; padding: 8px 10px;
  border: 3px solid #FFD9A8; border-radius: 16px;
  background: #FFFBF0;
  will-change: transform;
  transition: background-color .28s ease, border-color .28s ease, border-radius .28s ease;
}
#shapes.dm-mode-add .dm-add-right { border-color: #CFE8F5; background: #F5FBFF; }
#shapes.dm-mode-add .shape-sep {
  display: inline-block;
  transition: transform .4s ease, opacity .35s ease,
              width .32s cubic-bezier(.34, .9, .2, 1) .15s,
              margin-left .32s cubic-bezier(.34, .9, .2, 1) .15s,
              margin-right .32s cubic-bezier(.34, .9, .2, 1) .15s;
}

/* 撞上去：各先后撤 7px"蓄力"，再冲向对方；时长 470ms —— delight.js 按这个时长在撞上那一帧爆特效。
   ★ 这里有个关键教训：光靠 transform 平移是**合不拢的**。
     平移最多消掉两框之间 .shape-sep 的占位（宽 14 + 两侧 flex gap 12 = 38px），
     两框【内侧】还各自留着 padding 10 + border 3 = 13px，合完中间必然剩 26px 的缝
     （真机实测：间距 [6,6,26,6]），5 个苹果排不成均匀的一排 —— 就是用户截图里那道缝。
     所以把内侧 chrome 一起收进同一组关键帧：末帧 = 内侧贴齐、只留框内常规的 6px 间隙。
     #shapes 是 justify-content:center，两框收多少就从两侧各收一半，天然对称。 */
#shapes.dm-add-armed-h .dm-add-left  { animation: dm-slam-h-left .47s cubic-bezier(.34, .9, .2, 1) forwards; }
#shapes.dm-add-armed-h .dm-add-right { animation: dm-slam-h-right .47s cubic-bezier(.34, .9, .2, 1) forwards; }
#shapes.dm-add-armed-v .dm-add-left  { animation: dm-slam-v-left .47s cubic-bezier(.34, .9, .2, 1) forwards; }
#shapes.dm-add-armed-v .dm-add-right { animation: dm-slam-v-right .47s cubic-bezier(.34, .9, .2, 1) forwards; }

/* 前 32% 只做"后撤蓄力"，之后才真正合拢（内侧 padding / 边框宽度收到位）。
   transform 末帧回到 0 —— 合拢是布局收的，不是平移过去的。 */
@keyframes dm-slam-h-left {
  0%   { transform: translateX(0);                          padding-right: 10px; border-right-width: 3px; }
  24%  { transform: translateX(var(--dm-back));             padding-right: 10px; border-right-width: 3px; }
  32%  { transform: translateX(var(--dm-back)) scaleX(.96); padding-right: 10px; border-right-width: 3px; }
  100% { transform: translateX(0);                          padding-right: 0;    border-right-width: 0; }
}
@keyframes dm-slam-h-right {
  0%   { transform: translateX(0);                          padding-left: 10px; border-left-width: 3px; }
  24%  { transform: translateX(var(--dm-back));             padding-left: 10px; border-left-width: 3px; }
  32%  { transform: translateX(var(--dm-back)) scaleX(.96); padding-left: 10px; border-left-width: 3px; }
  100% { transform: translateX(0);                          padding-left: 6px;  border-left-width: 0; }
}
@keyframes dm-slam-v-left {
  0%   { transform: translateY(0);                          padding-bottom: 8px; border-bottom-width: 3px; }
  24%  { transform: translateY(var(--dm-back));             padding-bottom: 8px; border-bottom-width: 3px; }
  32%  { transform: translateY(var(--dm-back)) scaleY(.96); padding-bottom: 8px; border-bottom-width: 3px; }
  100% { transform: translateY(0);                          padding-bottom: 0;   border-bottom-width: 0; }
}
@keyframes dm-slam-v-right {
  0%   { transform: translateY(0);                          padding-top: 8px; border-top-width: 3px; }
  24%  { transform: translateY(var(--dm-back));             padding-top: 8px; border-top-width: 3px; }
  32%  { transform: translateY(var(--dm-back)) scaleY(.96); padding-top: 8px; border-top-width: 3px; }
  100% { transform: translateY(0);                          padding-top: 6px; border-top-width: 0; }
}

/* "+"号一起缩没。注意这里只动 transform / opacity，不动宽度 —— 与下面横排那条的区别很关键。 */
#shapes.dm-add-armed-h .shape-sep,
#shapes.dm-add-armed-v .shape-sep { transform: scale(0) rotate(180deg); opacity: 0; }

/* 横排合拢：sep 自身的宽（14）归零，两侧由 column-gap 收掉 → sep 这一段占位从 (gap+14+gap) 变成 0。 */
#shapes.dm-add-armed-h .shape-sep { width: 0; }
#shapes.dm-add-armed-h { column-gap: 0; }

/* 竖排合拢（窄屏两框换行）：只收 row-gap，**宽度一分都不动**。
   这一条踩过坑：如果连 sep 宽度和 column-gap 一起收，两框的总宽会缩到装得下同一行，
   于是布局重排成"并排"，可关键帧收的是【竖向】的 padding/边框 —— 结果框少了下边/上边，
   看起来缺一条边。合拢轴必须和关键帧收的轴一致，所以竖向合拢时宽度保持原样。 */
#shapes.dm-add-armed-v { row-gap: 0; }

/* 合并：内侧圆角归零 + 底色/边框色统一 → 两个框焊成一个框；内侧 chrome 消隐（不必再靠透明边框撑）。
   内侧 padding/边框的收尾值在这里再声明一遍做兜底：关键帧的 forwards 只在 armed 类还在时生效，
   而这三个类应当自洽 —— 两处同值，不会互相打架。 */
#shapes.dm-add-fused .dm-add-group { background: #FFFDF4; border-color: #FFC978; }
#shapes.dm-add-fused.dm-add-h .dm-add-left  { padding-right: 0; border-right-width: 0; border-top-right-radius: 0; border-bottom-right-radius: 0; }
#shapes.dm-add-fused.dm-add-h .dm-add-right { padding-left: 6px; border-left-width: 0;  border-top-left-radius: 0;  border-bottom-left-radius: 0; }
#shapes.dm-add-fused.dm-add-v .dm-add-left  { padding-bottom: 0; border-bottom-width: 0; border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
#shapes.dm-add-fused.dm-add-v .dm-add-right { padding-top: 6px; border-top-width: 0;    border-top-left-radius: 0;    border-top-right-radius: 0; }

/* 撞击瞬间：图形区整体震一下（碎片的冲击感靠这一下坐实） */
#shapes.dm-add-impact { animation: dm-quake .34s cubic-bezier(.36, .07, .19, .97); }
@keyframes dm-quake {
  0%   { transform: translate(0, 0) rotate(0); }
  18%  { transform: translate(-3px, 2px) rotate(-.6deg); }
  36%  { transform: translate(4px, -2px) rotate(.6deg); }
  54%  { transform: translate(-3px, 1px) rotate(-.3deg); }
  72%  { transform: translate(2px, 0) rotate(.2deg); }
  100% { transform: translate(0, 0) rotate(0); }
}

/* 撞击点特效容器：以撞击点为原点（0×0），子元素各自 translate(-50%,-50%) 居中。
   加法的"撞合"和分一分的"裂开"共用同一套形状 —— 容器名不同，装饰件完全一样。 */
.dm-add-fx, .dm-split-fx { position: absolute; left: 0; top: 0; width: 0; height: 0; z-index: 8; pointer-events: none; }
.dm-add-fx .dm-flash, .dm-split-fx .dm-flash {
  position: absolute; left: 0; top: 0; width: 110px; height: 110px; margin: -55px 0 0 -55px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255,255,255,.98) 0%, rgba(255,238,180,.88) 36%, rgba(255,214,120,0) 70%);
  animation: dm-flash .3s ease-out forwards;
}
@keyframes dm-flash { 0% { opacity: 0; transform: scale(.18); } 34% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(1.5); } }
.dm-add-fx .dm-shock, .dm-split-fx .dm-shock {
  position: absolute; left: 0; top: 0; width: 22px; height: 22px; margin: -11px 0 0 -11px;
  border: 5px solid #FFD166; border-radius: 50%;
  box-shadow: 0 0 14px rgba(255, 200, 90, .85), inset 0 0 10px rgba(255, 255, 255, .9);
  animation: dm-shock-out .52s cubic-bezier(.2, .8, .3, 1) forwards;
}
@keyframes dm-shock-out { 0% { opacity: 1; transform: scale(.32); } 100% { opacity: 0; transform: scale(5.4); } }

/* 碰撞碎片：三角形的碎块从撞击点朝外飞，边飞边转边淡出 */
.dm-add-fx .dm-shard, .dm-split-fx .dm-shard {
  position: absolute; left: 0; top: 0; display: block; border-radius: 1px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .18);
  animation-name: dm-shard-fly;
  animation-timing-function: cubic-bezier(.2, .62, .36, 1);
  animation-fill-mode: forwards;
}
@keyframes dm-shard-fly {
  0% {
    opacity: 1;
    transform: translate(-50%, -50%) rotate(0deg) scale(.5);
  }
  18% {
    opacity: 1;
    transform: translate(calc(-50% + var(--dx) * .3), calc(-50% + var(--dy) * .3)) rotate(calc(var(--rot) * .3)) scale(1.18);
  }
  100% {
    opacity: 0;
    transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) rotate(var(--rot)) scale(.45);
  }
}

/* —— 减法：吃豆人从右边开进来吃被拿走的图形 ——
   一开始不出现（JS 里答对才创建），出现后靠 transform 位移一站一站往左吃。
   注意：位置和翻转都写在最外层的 transform 上，所以"咬合"和"吃饱晃动"
   只能动内层 img 的 transform —— 否则会把位移覆盖掉，吃豆人会瞬移回原点。 */
.dm-pac {
  position: absolute; left: 0; top: 0;
  width: clamp(34px, 5.4vw, 54px); height: clamp(34px, 5.4vw, 54px);
  z-index: 7; pointer-events: none;
  transition-property: transform;
  transition-timing-function: cubic-bezier(.34, .9, .28, 1);
  transition-duration: 0ms;
  filter: drop-shadow(0 3px 3px rgba(0, 0, 0, .22));
  will-change: transform;
}
.dm-pac img { width: 100%; height: 100%; display: block; -webkit-user-drag: none; }
.dm-pac.chomp img { animation: dm-pac-chomp .13s ease-in-out 2; }
@keyframes dm-pac-chomp {
  0%, 100% { transform: scaleX(1) scaleY(1); }
  50%      { transform: scaleX(.84) scaleY(1.08); }
}
.dm-pac.is-full img { animation: dm-pac-happy .62s ease-in-out; }
@keyframes dm-pac-happy {
  0%, 100% { transform: translateY(0) rotate(0) scale(1); }
  25%      { transform: translateY(-7px) rotate(-7deg) scale(1.06); }
  60%      { transform: translateY(-3px) rotate(7deg) scale(1.03); }
}
.dm-pac.is-gone { opacity: 0; transition: opacity .45s ease; }

/* —— 分一分：一整箱 → 答对后从中间裂开，变成两个筐 ——
   布局要点：两个筐【零间距】贴着，接缝由各自 3px 的内侧 padding 补成 6px，
   正好等于筐内部的 gap —— 所以开局那 5 个图形是间距均匀的一整排。
   ★ 框为什么不用 border 而用 box-shadow 画：border 会实打实占掉 6px（左右各 3），
     接缝立刻从 6px 变成 12px —— 一排里冒出一道缝，正是加法那边踩过的坑。
     box-shadow 不参与布局，0 宽度的 ring 可以原地淡入，接缝就能一直保持均匀。 */
#shapes.dm-mode-split {
  position: relative;
  column-gap: 0;
  transition: column-gap .5s cubic-bezier(.34, 1.1, .4, 1) .2s;
}
.dm-split-l, .dm-split-r {
  position: relative;
  display: inline-flex; flex-wrap: nowrap; align-items: center; justify-content: center;
  flex: 0 0 auto;
  gap: 6px; padding: 6px 3px;
  border-radius: 16px;
  background: transparent;
  box-shadow: 0 0 0 3px rgba(255, 217, 168, 0);
  transition: background-color .34s ease .2s, box-shadow .34s ease .2s;
}
#shapes.dm-split-open { column-gap: 16px; }
#shapes.dm-split-open .dm-split-l,
#shapes.dm-split-open .dm-split-r { padding: 8px 10px; }
#shapes.dm-split-open .dm-split-l { box-shadow: 0 0 0 3px #FFD9A8; background: #FFFBF0; }
#shapes.dm-split-open .dm-split-r { box-shadow: 0 0 0 3px #CFE8F5; background: #F5FBFF; }

/* "裂开"的两拍：前 34% 憋住并向里挤一下（像被从中间顶开），之后两半才真正撑开成筐。
   撑开是【布局】做的（padding 变大 + column-gap 0→16），不是平移 —— 平移只会让两堆
   叠在一起，框的厚度照样留在外面。 */
#shapes.dm-split-open .dm-split-l { animation: dm-split-open-l .76s cubic-bezier(.34, 1.16, .42, 1) forwards; }
#shapes.dm-split-open .dm-split-r { animation: dm-split-open-r .76s cubic-bezier(.34, 1.16, .42, 1) forwards; }
@keyframes dm-split-open-l {
  0%   { padding: 6px 3px; transform: translateX(0) rotate(0); }
  34%  { padding: 6px 3px; transform: translateX(3px) rotate(.7deg); }
  52%  { padding: 6px 3px; transform: translateX(-2px) rotate(-.4deg); }
  100% { padding: 8px 10px; transform: translateX(0) rotate(0); }
}
@keyframes dm-split-open-r {
  0%   { padding: 6px 3px; transform: translateX(0) rotate(0); }
  34%  { padding: 6px 3px; transform: translateX(-3px) rotate(-.7deg); }
  52%  { padding: 6px 3px; transform: translateX(2px) rotate(.4deg); }
  100% { padding: 8px 10px; transform: translateX(0) rotate(0); }
}

/* 分隔符与问号框的"暗位"：留在 DOM 里（app.js 答错时要往问号框前一个一个数图形），
   但一点视觉都不占 —— 问号框摆在那儿等于提前告诉孩子"还差几个"，开局应当是"一整箱"。 */
.dm-split-park { display: none; }

/* 裂开点特效：一道白光裂缝先亮（0s，对应"咔"），半拍后白光 + 冲击环 + 碎块炸开
   （.2s，对应"啵"）。两拍分开才像"裂开"，一次性全放就像普通碰撞了。 */
.dm-split-fx .dm-crack {
  position: absolute; left: 0; top: 0; width: 6px; margin-left: -3px;
  border-radius: 3px;
  background: linear-gradient(180deg,
    rgba(255, 255, 255, 0) 0%, #fff 10%, #FFE9A8 46%, #FFC96B 54%, #fff 90%, rgba(255, 255, 255, 0) 100%);
  box-shadow: 0 0 14px rgba(255, 216, 120, .95);
  animation: dm-crack-flash .42s ease-out forwards;
}
@keyframes dm-crack-flash {
  0%   { opacity: 0; transform: scaleY(.06) scaleX(.5); }
  26%  { opacity: 1; transform: scaleY(1) scaleX(1); }
  62%  { opacity: 1; transform: scaleY(1) scaleX(1.7); }
  100% { opacity: 0; transform: scaleY(1.08) scaleX(.3); }
}
/* 白光/冲击环/碎块整体推迟到第二拍。★ 必须同时给 opacity:0 + fill-mode:both：
   否则延迟期间它们会以"未播动画的初始样式"（opacity 1）裸露在接缝上。 */
.dm-split-fx .dm-flash,
.dm-split-fx .dm-shock,
.dm-split-fx .dm-shard { opacity: 0; animation-delay: .2s; animation-fill-mode: both; }

/* 🐻 / 🐰 各扶一只筐，各贴自己那只筐的左上 / 右上角站稳。
   开局【两只都不出现】—— "一整箱"的时候还不存在"谁的一堆"，谈归属是多余的；
   裂开那一刻它们才跳出来接管各自的筐。和吃豆人同一条规矩：角色只在该出场时出场。
   x 方向特意推到筐外 26px（≈角色自身的宽度）：贴着框线站，而不是压在第一个图形上 ——
   一开始写 -4px，实测把第一个苹果的左上角盖掉了两成，看着像"挡住了"。 */
.dm-share-bear {
  position: absolute; left: -26px; top: -9px;
  font-size: clamp(28px, 4.2vw, 42px); line-height: 1; pointer-events: none; z-index: 6;
  filter: drop-shadow(0 3px 3px rgba(0, 0, 0, .2));
  opacity: 0;
}
#shapes.dm-split-open .dm-share-bear {
  animation: dm-mascot-in .5s cubic-bezier(.34, 1.56, .64, 1) .2s both,
             dm-bob 2.4s ease-in-out .9s infinite;
}
#shapes.dm-split-open .dm-friend {
  animation: dm-mascot-in .5s cubic-bezier(.34, 1.56, .64, 1) .27s both,
             dm-bob 2.4s ease-in-out .97s infinite;
}
@keyframes dm-mascot-in {
  from { opacity: 0; transform: translateY(-12px) scale(.35); }
  to   { opacity: 1; transform: none; }
}

/* —— 比多少：天平跷跷板 —— */
.dm-seesaw { position: absolute; left: 6%; right: 6%; bottom: 6px; height: 0; pointer-events: none; z-index: 4; }
.dm-seesaw .dm-beam {
  position: absolute; left: 0; right: 0; bottom: 0; height: 7px; border-radius: 7px;
  background: linear-gradient(180deg, #D7A05A, #9A6630);
  box-shadow: 0 2px 5px rgba(0, 0, 0, .22);
}
.dm-seesaw .dm-fulcrum {
  position: absolute; left: 50%; bottom: 2px; transform: translateX(-50%);
  color: #8A5A2B; font-size: 22px; line-height: 1;
}
.dm-vs-hide { opacity: 0 !important; }

/* —— 数一数：数完整堆闪成星座 —— */
#shapes.dm-count-done .shape { animation: dm-star-twinkle 1.4s ease; }
@keyframes dm-star-twinkle {
  0%, 100% { filter: none; }
  50% { filter: drop-shadow(0 0 8px rgba(255, 220, 120, .95)) brightness(1.12); }
}

/* —— 凑十：2×5 填格子 —— 答对后剩余空格一个个"落进去"的回弹 */
#shapes.dm-mode-make10 .slot.dm-ten-fill { animation: dm-ten-pop .5s cubic-bezier(.34, 1.56, .64, 1); }
@keyframes dm-ten-pop {
  0%   { transform: scale(.6); opacity: .2; }
  60%  { transform: scale(1.12); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}

/* ---------- D11/D12 皮皮专属：名字 + 点小熊互动 ---------- */
.dm-name-row { display:flex; align-items:center; gap:10px; margin:10px 0; }
.dm-name-label { font-size:15px; font-weight:800; color:var(--ink-soft,#6E5E52); white-space:nowrap; }
.dm-name-input {
  flex:1; min-width:0; padding:8px 12px; font-size:16px; font-weight:800;
  border:2px solid #FFD9A8; border-radius:12px; background:#FFFBF0; color:#7a4a16; outline:none;
}
.dm-name-input:focus { border-color:#FFB347; box-shadow:0 0 0 3px rgba(255,179,71,.25); }
.dm-name-hint { font-size:13px; color:#9a8c80; margin:0 0 4px; }
/* D13 唱口诀：看大数，数小数（加法正着数，减法倒着数）
   ★ v97：平时**完全不出现**（已删掉「不会算？」胶囊）；只有答错时才盖到算式栏上，
     把「6+1=？」那一行遮住，就地讲一遍怎么数。绝对定位 → 不改变任何版面高度。 */
.quiz-card { position: relative; }          /* 覆盖层的定位参照（原本是 static） */
.dm-chant { display: none; }
.dm-chant.is-on {
  display: flex; flex-direction: column; justify-content: center; gap: 3px;
  position: absolute; z-index: 6;
  transform: translateY(-50%);              /* top 给的是算式行的垂直中心 */
  pointer-events: none;                     /* 只做讲解，不挡下面的选项按钮 */
  animation: dm-chant-in .26s cubic-bezier(.2,1.3,.5,1) both;
}
@keyframes dm-chant-in {
  from { opacity: 0; transform: translateY(-50%) scale(.94); }
  to   { opacity: 1; transform: translateY(-50%) scale(1); }
}
.dm-chant-rail {
  display: flex; margin: 0; padding: 10px 6px; position: relative;
  background: #FFFDF7; border: 2px solid #FFE3B8; border-radius: 14px;
  box-shadow: 0 6px 16px rgba(150,110,40,.16);
  gap: 2px; align-items: center; justify-content: space-between;
  overflow: visible;                        /* 弧线要飞出轨外，不能被裁 */
}
/* 跳跃弧线层：底边贴着导轨内框，**向上多长 30px** —— "箭头跳出框外"就发生在这段留白里。
   两个要点：
   ① 不要靠 `svg { overflow: visible }` 去赌浏览器肯不肯画到视口外（能不能画因内核而异），
      直接把盒子做大最稳；
   ② 因此坐标系也跟着变了：JS 里改用 **这层 SVG 自己的 getBoundingClientRect()** 当原点
      （`railArc()`），所以 top/height 再怎么调都不用改数字。
   导轨自身 padding **上下等宽**（10px 6px）→ 数字真正居中；
   弧线空间不再从导轨 padding 里抠。 */
.dm-rail-svg {
  position: absolute; left: 0; top: -30px; width: 100%; height: calc(100% + 30px);
  overflow: visible; pointer-events: none;
}
@keyframes dm-arc-draw { to { stroke-dashoffset: 0; } }
.dm-tick {
  flex: 1 1 0; min-width: 0; height: 30px; max-width: 32px;
  display: flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 800; color: #C9C4BC;
  border-radius: 50%; background: transparent;
  transition: transform .18s cubic-bezier(.34,1.56,.64,1), background .18s, color .18s;
}
.dm-tick.is-base { background: #EFE6FF; color: #7B55D6; transform: scale(1.18); }
.dm-tick.is-step { background: #DCEEFF; color: #1F6FB2; transform: scale(1.10); }
/* 预演：语音还在讲大小数时，先把待会儿要走的格子淡淡圈出来（避免画面空等七八秒） */
.dm-tick.is-preview { background: #FFF4DC; color: #C08A2A; box-shadow: inset 0 0 0 2px #FFD79A; }
.dm-tick.is-ans  { background: #1F9DFF; color: #fff; transform: scale(1.24); box-shadow: 0 0 0 3px rgba(31,157,255,.22); }
.dm-chant-say { min-height: 16px; margin-top: 4px; text-align: center; font-size: 13px; font-weight: 800; color: #B4741A; }
.dm-toggle-btn {
  padding: 6px 16px; font: inherit; font-size: 15px; font-weight: 800;
  border: 2px solid #FFD9A8; border-radius: 12px; background: #FFFBF0; color: #9a8c80; cursor: pointer;
}
.dm-toggle-btn.is-on { background: #FFF0D6; border-color: #FFB347; color: #B4741A; }


/* 点小熊：开心跳一下 */
.bear.dm-bear-hi { animation: dm-bear-hi .9s cubic-bezier(.34,1.56,.64,1); }
@keyframes dm-bear-hi {
  0%   { transform: translateY(0) rotate(0); }
  25%  { transform: translateY(-14px) rotate(-6deg) scale(1.06); }
  50%  { transform: translateY(2px) rotate(5deg) scale(1.02); }
  75%  { transform: translateY(-7px) rotate(-3deg); }
  100% { transform: translateY(0) rotate(0); }
}
/* 小熊说话气泡（点它时冒出来的俏皮话） */
.dm-bear-talk {
  position:absolute; left:50%; bottom:100%;
  transform:translate(-50%,-8px);
  background:#fff; color:#7a4a16; font-weight:800; font-size:15px;
  padding:8px 12px; border-radius:14px; border:2px solid #FFD9A8;
  box-shadow:0 6px 16px rgba(0,0,0,.14); white-space:nowrap; z-index:50;
  pointer-events:none;
  animation: dm-bear-talk-in .3s ease, dm-bear-talk-out .4s ease 1.7s forwards;
}
.dm-bear-talk::after {
  content:""; position:absolute; left:50%; bottom:-9px; transform:translateX(-50%);
  border:8px solid transparent; border-top-color:#FFD9A8;
}
@keyframes dm-bear-talk-in  { from { opacity:0; transform:translate(-50%,-2px) scale(.8); } to { opacity:1; transform:translate(-50%,-8px) scale(1); } }
@keyframes dm-bear-talk-out { to   { opacity:0; transform:translate(-50%,-18px) scale(.9); } }

/* ---------- 无障碍：系统开启「减弱动态效果」时全部降级 ---------- */
@media (prefers-reduced-motion: reduce) {
  .dm-msg { animation: none; opacity: 1; }
  .dm-bit,
  .bear.dm-halo::after,
  .wheel-frame.is-ticking::after { display: none; }
  .dm-breathe, .dm-empty-hero, .dm-chip { animation: none; }
  .bear::before { animation: none; }
  #progress .dot.right::after,
  #progress .dot.wrong::after { animation: none; }

  /* 宝箱 / 贴纸专属动画：降级为静态出现，不丢"开箱拿到奖励"的语义 */
  #dm-chest-overlay .chest-stage,
  #dm-chest-overlay .dm-chest .chest-glow,
  #dm-chest-overlay .dm-chest .chest-sparkles i,
  #dm-chest-overlay .dm-chest .chest-sheen,
  #dm-chest-overlay .dm-chest .chest-rays,
  #dm-chest-overlay .dm-chest .chest-shock,
  #dm-chest-overlay .dm-chest .chest-sparks i,
  #dm-chest-overlay .chest-flash { animation: none; }
  #dm-chest-overlay { transition: none; }
  /* 宝箱直接定格成"已打开"姿态（换开启图），不播爆闪/射线/迸发星，但语义不丢 */
  #dm-chest-overlay .dm-chest .chest-img--open { opacity: 1; transform: none; transition: none; }
  #dm-chest-overlay .dm-chest .chest-img--closed { display: none; }
  #dm-chest-overlay .chest-sticker,
  #dm-chest-overlay .chest-name { animation: none !important; opacity: 1 !important; transform: translate(-50%, -50%) scale(1) !important; }
  .dm-chest { transition: none; }
  /* 减弱动效下不飞：JS 会直接露出贴纸位并关闭（见 flyToSlot 的 REDUCED 分支） */
  .sticker-prize.dm-landed { animation: none; }
  /* 贴纸动画降级为静态定格：保留插画和脚底阴影（不丢"拿到了奖励"的语义），
     只是不游/不跳/不转。图片本来就是静帧，直接关掉动画即可。 */
  #dm-chest-overlay .sticker-wrap,
  #dm-chest-overlay .sticker-img,
  #sticker-prize .sticker-wrap,
  #sticker-prize .sticker-img,
  .sticker-wrap::after,
  .sticker-board .sticker-img-sm { animation: none !important; opacity: 1 !important; transform: none !important; }
  /* 每关钩子：去掉跳动/倾斜/脉动，保留静态视觉提示（如划掉线、朋友头像、弱点徽章）。
     ★ 这里不再列 .dm-friend —— 它和 .dm-share-bear 的出场动画关掉后基础态是 opacity:0，
       必须在下面"裂开"的降级里显式点亮，不能只靠 animation:none。 */
  #shapes.dm-mode-add .shape-sep,
  #shapes.dm-see-left .vs, #shapes.dm-see-right .vs,
  .dm-pac.chomp img, .dm-pac.is-full img,
  #shapes.dm-mode-make10 .slot.dm-ten-fill, #shapes.dm-count-done .shape { animation: none !important; }
  /* 跷跷板倾斜是"答案演示"，减弱动效下仍要静态呈现（重的一边压低），只是不播过渡/回弹 */
  #shapes.dm-see-left .dm-seesaw .dm-beam { transform: rotate(-4.5deg); transition: none !important; }
  #shapes.dm-see-right .dm-seesaw .dm-beam { transform: rotate(4.5deg); transition: none !important; }
  #shapes.dm-see-left .side.left { transform: translateY(12px) rotate(-4deg); transition: none !important; }
  #shapes.dm-see-right .side.right { transform: translateY(12px) rotate(4deg); transition: none !important; }
  #shapes.dm-see-left .side.right, #shapes.dm-see-right .side.left { transform: translateY(-8px); transition: none !important; }
  #shapes.dm-mode-count .shape.dm-tap,
  #shapes.dm-mode-sub .shape.taken.dm-eat-now { animation: none; }
  /* 减法"被吃掉"降级：动画关掉后 dm-eat 的终态不会应用，这里直接把终态写成 opacity:0，
     所以"少了两个"的语义还在，只是不做缩小上飘。吃豆人也不做位移（JS 里会静态定位）。 */
  #shapes.dm-mode-sub .shape.taken.dm-eat-now { opacity: 0; }
  .dm-pac { transition: none; }
  /* 加法"合并"降级：不蓄力、不对撞、不震动、没有碎片 —— 两框瞬间贴合（"变成一个框"照旧表达，只是不动）。
     ★ animation:none 会让关键帧的末帧失效，所以内侧 chrome 的收尾值必须在这里显式写死：
       漏了它，布局不收，中缝就又回来了（"两框并拢了但中间一道缝"在减弱动效下重现）。 */
  .dm-add-group { transition: none; }
  #shapes.dm-add-armed-h .dm-add-left  { animation: none; transform: none; padding-right: 0; border-right-width: 0; }
  #shapes.dm-add-armed-h .dm-add-right { animation: none; transform: none; padding-left: 6px;  border-left-width: 0; }
  #shapes.dm-add-armed-v .dm-add-left  { animation: none; transform: none; padding-bottom: 0; border-bottom-width: 0; }
  #shapes.dm-add-armed-v .dm-add-right { animation: none; transform: none; padding-top: 6px; border-top-width: 0; }
  #shapes.dm-add-armed-h .shape-sep,
  #shapes.dm-add-armed-v .shape-sep { visibility: hidden; opacity: 0; transform: none; }
  #shapes.dm-add-armed-h .shape-sep { width: 0; }
  #shapes.dm-add-armed-h { column-gap: 0; }
  #shapes.dm-add-armed-v { row-gap: 0; }
  #shapes.dm-add-impact { animation: none; }
  /* 减弱动态时 JS 不再建 .dm-add-fx（没碎片/冲击波要放），所以这里也不必有它的降级规则 */

  /* 分一分"裂开"降级：不憋、不抖、不爆碎片 —— 两个筐直接成型（"分成两堆"照旧表达）。
     ★ 和加法同一个坑：animation:none 会让关键帧末帧失效，两个筐的 padding 收尾值
       （8px 10px）必须在这里显式写死；漏了它，减弱动态下两个筐会一直"憋"在 6px 3px。 */
  .dm-split-l, .dm-split-r { transition: none; }
  #shapes.dm-split-open { column-gap: 16px; transition: none; }
  #shapes.dm-split-open .dm-split-l,
  #shapes.dm-split-open .dm-split-r { animation: none; transform: none; padding: 8px 10px; }
  /* 🐻 / 🐰 的基础态是 opacity:0（开局不出现），出场动画一关就得把"裂开后"显式点亮，
     否则减弱动态下两个孩子永远看不见 —— 这是上一版减法"吃豆人只剩静态图"的同型问题。 */
  #shapes.dm-split-open .dm-share-bear,
  #shapes.dm-split-open .dm-friend { animation: none !important; opacity: 1; transform: none; }
  .bear.dm-bear-hi { animation: none !important; }
  .dm-bear-talk { animation: none !important; }
  /* D13 唱口诀降级：不做进出场缩放，也不一级级跳，直接把路线点亮（JS 侧也走 REDUCED 分支） */
  .dm-chant.is-on { animation: none; }
  .dm-tick { transition: none; }
  .dm-arc { animation: none !important; stroke-dashoffset: 0 !important; }
}
/* ==========================================================
   D14 首页小鲸鱼（20 帧精灵序列 · 纯装饰，不参与判题）
   ----------------------------------------------------------
   素材：assets/whale/whale-sheet.webp
        横向 20 帧单行精灵表，每帧 288×254（源 384×338 画框 × 0.75）
        这版鲸鱼几乎占满画框宽（中位数 87%），所以显示宽度要给得偏小，
        视觉权重才和当年的小图一致（占 57% 的那版曾给到 168px）。
   播放：第 1–17 帧 @ 9fps（用户定）。素材实测是 4 组 × 5 帧、
        组间有姿势/机位跳变，1–17 是挑出来最顺的连续区间。
   ★ 三层 DOM 必须分开（本项目踩过：transform 动画会互相覆盖）：
       定位层 .dm-whale        left/top 跟随视口，不带动画
       浮动层 .dm-whale-float   上下呼吸
       精灵层 .dm-whale-inner   翻帧 + 点击缩放
   ★ 帧高 ≠ 帧宽：容器高度必须乘 --wh-ar，垂直锚点也要按真实高度算。
   ★ 只在首页出现：其它页由 JS 加 .is-away 淡出。
     不能挂进 #page-home —— .page.is-active 带 pop 动画（transform），
     会让 fixed 子元素改以 .page 为包含块，定位与裁剪都会跑偏。
   ========================================================== */
.dm-whale {
  position: fixed;
  z-index: 20;
  --wh-fw: 138px;                 /* 单帧显示宽度（JS 按屏宽写到元素上） */
  --wh-ar: .881944;               /* 帧高 / 帧宽 = 254 / 288 */
  --wh-n: 20;                     /* 帧数，换素材时同步 */
  width: var(--wh-fw);
  height: calc(var(--wh-fw) * var(--wh-ar));
  pointer-events: none;           /* 默认不挡点击：这是装饰，孩子点哪儿都别失手 */
  -webkit-tap-highlight-color: transparent;
  transition: opacity .3s ease;
}
.dm-whale.is-live { pointer-events: auto; cursor: pointer; }
.dm-whale.is-away { opacity: 0; pointer-events: none; }

.dm-whale-float,
.dm-whale-inner { width: 100%; height: 100%; }

.dm-whale-inner {
  background-image: url("assets/whale/whale-sheet.webp");
  background-repeat: no-repeat;
  background-size: calc(var(--wh-fw) * var(--wh-n)) 100%;
  background-position-x: calc(var(--wh-fw) * -1 * var(--wh-f, 0));
  filter: drop-shadow(0 8px 12px rgba(30, 111, 176, .28));
  transform-origin: 50% 76%;      /* 缩放锚点压低：像踩在水面上 duang 一下 */
  transition: transform .16s ease;
}
.dm-whale.is-live:active .dm-whale-inner { transform: scale(.92); }
.dm-whale.is-tap .dm-whale-inner { animation: dm-whale-tap .34s ease; }
@keyframes dm-whale-tap {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.10) rotate(-3deg); }
  100% { transform: scale(1); }
}

.dm-whale-float { animation: dm-whale-float 5.4s ease-in-out infinite; }
@keyframes dm-whale-float {
  0%, 100% { transform: translateY(-5px) rotate(-1.5deg); }
  50%      { transform: translateY(9px) rotate(1.5deg); }
}

/* 注：试过在脚下加"水面波纹"（白色径向渐变椭圆），在浅色天空里读起来像一块白污渍，
   而且会和素材自带的喷水/气泡打架 —— 去掉，浮空感交给 drop-shadow。 */

/* 减弱动态：不翻帧（定格第 1 帧）、不呼吸、点击也不弹 */
@media (prefers-reduced-motion: reduce) {
  .dm-whale-float { animation: none; }
  .dm-whale-inner { transition: none; }
  .dm-whale.is-tap .dm-whale-inner { animation: none; }
}
