/* ═══════════════════════════════════════════════════════════════════════════
   🎬 web/motion.css —— 鸽邮「动效层」（8 条基本动作的规则全在这里）
   ───────────────────────────────────────────────────────────────────────────
   时长 / 缓动 **全部**取自 web/theme.css 的 token（--dur-* / --ease-* / --stagger /
   --press-scale / --motion-scale）。这里一个时间数字都不许硬编码 —— 改 theme.css 一处，
   正式游戏 + 动效样张一起变，甲方现场调强度也走同一根旋钮。

   谁用：web/index.html（正式游戏本体，由 motion.js 挂类名）
        · web/motion-demo.html（样张，直接手写类名演示）
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─────────────────────────────────────────────────────────────────────────
   ① 按下即时反馈（Press feedback）—— 100ms 内手感
   触发：pointerdown / :active（手指按下的那一瞬，不是松手）
   时长：--dur-instant(80ms) × --motion-scale   缓动：--ease-out
   不该有：键盘触发的动作（回车发信、快捷键）不加缩放 —— 一天几百次，会变拖。
   ───────────────────────────────────────────────────────────────────────── */
:root:not([data-motion="off"]) :is(
  .btn, .mini, .chip-btn, .bf-chip, .fam-tab, .boxtab, .hudbtn, .tab, .me-chip,
  .bag-chip, .seed, .fp, .bag-row, .store-row, .wg-card, .sr-buy, .mo-press
){
  transition:transform calc(var(--dur-fast) * var(--motion-scale)) var(--ease-out),
             box-shadow calc(var(--dur-fast) * var(--motion-scale)) var(--ease-out),
             background-color calc(var(--dur-fast) * var(--motion-scale)) var(--ease-out);
}
:root:not([data-motion="off"]) :is(
  .btn, .mini, .chip-btn, .bf-chip, .fam-tab, .boxtab, .hudbtn, .tab, .me-chip,
  .bag-chip, .seed, .fp, .bag-row, .store-row, .wg-card, .sr-buy, .mo-press
):active{
  transform:scale(var(--press-scale));
  box-shadow:var(--shadow-press);
  /* 按下要「立刻」(--dur-instant 只是给回弹用的，按下这段几乎为 0) */
  transition-duration:calc(var(--dur-instant) * var(--motion-scale));
}
/* 松手弹回：位移用弹簧曲线（有极轻过冲），不是线性回落 */
:root:not([data-motion="off"]) .mo-release{
  animation:mo-release calc(var(--dur-panel) * var(--motion-scale)) var(--ease-spring-soft);
}
@keyframes mo-release{ from{transform:scale(var(--press-scale))} to{transform:scale(1)} }

/* ─────────────────────────────────────────────────────────────────────────
   ② 数字滚动（Number ticker）—— 金币 / 心情
   触发：数值**变化时**（到账、扣费、收获）；页面首次渲染不滚（直接给最终值）
   时长：--dur-slow(420ms) × scale   缓动：--ease-out
   纪律：数字用等宽字形（tabular-nums），滚动时版面不许抖。
   不该有：打开页面就滚（玩家没做任何事，数字却动 = 噪音）。
   ───────────────────────────────────────────────────────────────────────── */
.mo-roll{
  display:inline-flex;align-items:baseline;font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1;overflow:hidden;line-height:1;vertical-align:-1px;
}
.mo-roll .d{position:relative;display:inline-block;height:1em;overflow:hidden;width:1ch;text-align:center}
.mo-roll .d i{
  display:block;font-style:normal;height:1em;line-height:1;white-space:pre;
  transition:transform calc(var(--dur-slow) * var(--motion-scale)) var(--ease-out);
}
.mo-roll.wobble .d i{transition-timing-function:var(--ease-spring-soft)}

/* ─────────────────────────────────────────────────────────────────────────
   ③ 面板从触发点生长（Origin-aware panel）
   触发：点「展示盲盒内容 / 小卖部 / 出海」这类带来源的按钮
   时长：--dur-panel(280ms) × scale   缓动：--ease-spring（轻微过冲）
   transform-origin = 按钮位置（--mo-ox/--mo-oy 由 JS 算好写进 style）
   不该有：全局「回主菜单」这类没有来源的跳转 —— 那种用淡入更自然。
   ───────────────────────────────────────────────────────────────────────── */
:root:not([data-motion="off"]) .mo-panel-in{
  transform-origin:var(--mo-ox,50%) var(--mo-oy,50%);
  animation:mo-panel-grow calc(var(--dur-panel) * var(--motion-scale)) var(--ease-spring) both;
}
@keyframes mo-panel-grow{
  from{opacity:0;transform:scale(.92) translateY(6px)}
  to  {opacity:1;transform:scale(1) translateY(0)}
}
/* 关闭：原路缩回同一个触发点（进出一致，Apple「空间一致性」） */
:root:not([data-motion="off"]) .mo-panel-out{
  transform-origin:var(--mo-ox,50%) var(--mo-oy,50%);
  animation:mo-panel-shrink calc(var(--dur-ui) * var(--motion-scale)) var(--ease-out) both;
  pointer-events:none;
}
@keyframes mo-panel-shrink{
  from{opacity:1;transform:scale(1)}
  to  {opacity:0;transform:scale(.94)}
}
/* 遮罩只做淡入 —— 它是背景，不该有位移 */
:root:not([data-motion="off"]) .mo-scrim-in{animation:mo-fade calc(var(--dur-fast) * var(--motion-scale)) var(--ease-out) both}
.mo-scrim-in{animation:mo-fade var(--dur-fast) var(--ease-out) both}
@keyframes mo-fade{from{opacity:0}to{opacity:1}}

/* ─────────────────────────────────────────────────────────────────────────
   ④ 列表交错入场（Stagger）
   触发：列表**整体重画**（打开背包/商店/面板）——每个项 --i 从 0 递增
   单条时长：--dur-ui(180ms) × scale   每项延迟：--i × --stagger(32ms)
   上限：延迟总和封顶 8 项（再多就整批一起进）—— 不然长列表最后一项要等好几百 ms。
   不该有：定时刷新的列表（世界频道）逐条重播 —— 那种只让**新消息**上滑（见 ⑦）。
   ───────────────────────────────────────────────────────────────────────── */
:root:not([data-motion="off"]) .mo-stagger > *{
  animation:mo-item-in calc(var(--dur-ui) * var(--motion-scale)) var(--ease-out) both;
  animation-delay:calc(min(var(--i,0), 8) * var(--stagger) * var(--motion-scale));
}
@keyframes mo-item-in{
  from{opacity:0;transform:translateY(10px)}
  to  {opacity:1;transform:translateY(0)}
}
/* 交错是**装饰**：播放期间不许挡住点击 */
:root:not([data-motion="off"]) .mo-stagger{pointer-events:auto}

/* ─────────────────────────────────────────────────────────────────────────
   ⑤ 弹簧与过冲（Overshoot）
   触发：卡片 / 面板入场、开箱出物 —— 需要「活」的时刻
   时长：--dur-panel × scale   缓动：--ease-spring（≈ damping .8 / response .35）
   纪律：UI 常规动作 150~300ms（--dur-ui / --dur-panel 就卡在这个区间）；过冲只用在
        有动量感的地方。菜单淡入不该过冲。
   ───────────────────────────────────────────────────────────────────────── */
:root:not([data-motion="off"]) .mo-pop-in{
  animation:mo-pop-in calc(var(--dur-panel) * var(--motion-scale)) var(--ease-spring) both;
}
@keyframes mo-pop-in{
  0%  {opacity:0;transform:scale(.90)}
  100%{opacity:1;transform:scale(1)}
}

/* ─────────────────────────────────────────────────────────────────────────
   ⑥ 获得反馈（Gain feedback）—— 开箱 pop + 光晕 / 宠物弹跳 / 图标放大
   触发：真拿到东西的那一刻（开箱出物、喂食成功、拾取入包）
   时长：pop --dur-panel × scale（过冲）· 光晕 --dur-slow × scale
   纪律：只在「赚到」时给；每次失败也弹 = 反馈贬值。
   ───────────────────────────────────────────────────────────────────────── */
:root:not([data-motion="off"]) .mo-gain{animation:mo-gain-pop calc(var(--dur-panel) * var(--motion-scale)) var(--ease-spring) both}
@keyframes mo-gain-pop{
  0%  {opacity:0;transform:scale(.86)}
  60% {opacity:1;transform:scale(1.06)}
  100%{opacity:1;transform:scale(1)}
}
/* 光晕：物品底下炸开一圈柔光，然后收干净 */
.mo-glow{position:relative}
:root:not([data-motion="off"]) .mo-glow::after{
  content:"";position:absolute;inset:-18%;border-radius:inherit;pointer-events:none;
  background:radial-gradient(closest-side,color-mix(in srgb,var(--accent) 55%,transparent),transparent 72%);
  animation:mo-glow-out calc(var(--dur-slow) * var(--motion-scale)) var(--ease-out) both;
}
@keyframes mo-glow-out{from{opacity:.9;transform:scale(.7)}to{opacity:0;transform:scale(1.35)}}
/* 宠物喂食成功：弹跳一下（squash & stretch，落回原状） */
:root:not([data-motion="off"]) .mo-bounce{animation:mo-bounce calc(var(--dur-panel) * var(--motion-scale)) var(--ease-out) both}
@keyframes mo-bounce{
  0%{transform:translateY(0) scale(1,1)}
  28%{transform:translateY(-9px) scale(.94,1.08)}
  56%{transform:translateY(0) scale(1.06,.94)}
  78%{transform:translateY(-3px) scale(1,1)}
  100%{transform:translateY(0) scale(1,1)}
}
/* 拾取物品：图标从小放大（不是 scale(0) —— 现实里东西不会凭空冒出来） */
:root:not([data-motion="off"]) .mo-scale-in{animation:mo-scale-in calc(var(--dur-ui) * var(--motion-scale)) var(--ease-spring) both}
@keyframes mo-scale-in{from{opacity:0;transform:scale(.72)}to{opacity:1;transform:scale(1)}}

/* ─────────────────────────────────────────────────────────────────────────
   ⑦ 世界频道新消息（Live feed append）
   触发：**新的一条**追加进世界频道（不是整列表重画）
   时长：--dur-ui(180ms) × scale   缓动：--ease-out
   纪律：轻微上滑 + 淡入，**不打断阅读** —— 不抢焦点、不滚动跳位、不闪背景色。
   ───────────────────────────────────────────────────────────────────────── */
:root:not([data-motion="off"]) .fd.mo-new{
  animation:mo-feed-in calc(var(--dur-ui) * var(--motion-scale)) var(--ease-out) both;
}
@keyframes mo-feed-in{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
:root:not([data-motion="off"]) .fd.mo-new::after{
  content:"";position:absolute;left:0;top:0;bottom:0;width:3px;
  background:var(--accent);opacity:0;animation:mo-new-mark calc(var(--dur-slow)*2 * var(--motion-scale)) var(--ease-out) .1s both;
}
.fd{position:relative}
@keyframes mo-new-mark{0%{opacity:.9}100%{opacity:0}}

/* ─────────────────────────────────────────────────────────────────────────
   ⑧ 滑动切换（Drag + snap）—— 主题切换 / 分页
   跟手：拖的时候 1:1（无过渡）；松手：--dur-panel × scale + --ease-drawer 吸附
   纪律：跟手阶段**不能**有 transition（有就"粘滞"）；松手才上曲线。
   ───────────────────────────────────────────────────────────────────────── */
.mo-track{display:flex;will-change:transform}
.mo-track.dragging{transition:none;cursor:grabbing}
.mo-track.snapping{transition:transform calc(var(--dur-panel) * var(--motion-scale)) var(--ease-drawer)}
:root[data-motion="off"] .mo-track{transition:none!important}

/* ── 横向分页器（web/swipe.js 用）：背包分类 / 商店分类 ──
   · overflow:hidden → 面板在容器里横向滑动，**不会**顶出横向滚动条
   · touch-action:pan-y → 竖着划仍然滚页面（浏览器管），横着划才归我们
   · align-items:flex-start → 每页各自量自己的高度（syncHeight 要读 offsetHeight）
   · 面板一律 flex:0 0 100% + min-width:0（min-width:0 是防 flex 子项撑破的关键） */
.mo-pager{overflow:hidden;touch-action:pan-y}
.mo-pager > .mo-track{align-items:flex-start}
.mo-pager > .mo-track > *{flex:0 0 100%;width:100%;min-width:0}
:root[data-motion="off"] .mo-pager > .mo-track{transition:none!important}
.mo-dots{display:flex;gap:6px;justify-content:center;margin-top:10px}
.mo-dots i{width:7px;height:7px;border-radius:50%;background:var(--line2);transition:background var(--dur-fast) var(--ease-out),transform var(--dur-fast) var(--ease-out)}
.mo-dots i.on{background:var(--accent);transform:scale(1.25)}
.mo-swipe-hint{font-size:11px;color:var(--dim2);text-align:center;margin-top:6px}

/* ─────────────────────────────────────────────────────────────────────────
   ③b 关闭弹窗（Scrim fade-out + 面板收回）
   ⚠️ 为什么必须有这一段（甲方 2026-10-03 报的「点边缘关信件，窗口闪一下」）：
      以前关闭 = closeModal() 里一句 classList.add('hidden') → display:none，
      是**一帧硬切**：半透明遮罩 + backdrop-filter blur 整层在同一帧消失，
      浅色主题下页面亮度瞬间跳一大截（实测顶部带均值 171 → 247，跳 +76），
      看上去就是"窗口闪一下"。按 token 淡出后，这个台阶被摊到 --dur-panel(280ms)，
      单帧亮度跳变小到看不见；关闭动画反而比原来更完整。
      时长/缓动**全部取自 theme.css token**，不在这里硬编码时间。
   ───────────────────────────────────────────────────────────────────────── */
.mo-scrim-out{animation:mo-scrim-out calc(var(--dur-panel) * var(--motion-scale)) var(--ease-out) both}
.mo-panel-out{animation:mo-panel-out calc(var(--dur-panel) * var(--motion-scale)) var(--ease-drawer) both}
@keyframes mo-scrim-out{from{opacity:1}to{opacity:0}}
@keyframes mo-panel-out{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.96)}}

/* ── 无障碍 / 强度=关：一律静态直出（保留可读性，去掉位移与过冲） ── */
@media (prefers-reduced-motion: reduce){
  .mo-stagger > *,
  .mo-pop-in, .mo-gain, .mo-scale-in, .mo-bounce, .fd.mo-new, .mo-panel-in, .mo-panel-out,
  .mo-scrim-out{
    animation:none !important;
  }
  .mo-roll .d i{transition:none !important}
  .mo-track{transition:none !important}
}
:root[data-motion="off"] .mo-stagger > *,
:root[data-motion="off"] .mo-pop-in,
:root[data-motion="off"] .mo-gain,
:root[data-motion="off"] .mo-scale-in,
:root[data-motion="off"] .mo-bounce,
:root[data-motion="off"] .fd.mo-new,
:root[data-motion="off"] .mo-panel-in,
:root[data-motion="off"] .mo-scrim-out,
:root[data-motion="off"] .mo-panel-out{animation:none !important}
