/* motion.css -- MiniApp 动效层（WEB-THEME 系列，2026-10-11）。
 *
 * 这个文件是什么：
 *   把 POS 剧情终端那套动效翻成本仓库的令牌化实现。八组关键帧一一对应，
 *   数值取自对 posapp/index.html 的 <style> 块实测：
 *
 *     fin       内容进场     opacity 0→1 + translateY(4px)→none，.35s ease-out
 *     tabin     切页闪一下   opacity .2→.95→.55→1，.26s ease-out
 *     blink     光标闪烁     50%{opacity:0}，steps(1)，1.1s
 *     flick     亮度抖动     .5~.62 之间跳，steps(1)，6s
 *     grain     颗粒位移     translate 三档跳变，steps(3)，.9s
 *     roll      扫描滚条     translateY(-30vh)→130vh，10s linear
 *     donepulse 完成脉冲     box-shadow 0→28px→6px，1.8s
 *     wpulse    节点呼吸     scale .6→1.9 + opacity .9→0，1.8s
 *
 * 为什么要单开一个文件而不是塞进 tokens.css：
 *   两者是**不同的轴**，混在一起就没法单独关。tokens.css 管静态质感
 *   （配色、字体、圆角、扫描线/暗角这两层静态纹理）；这里管「会动的东西」。
 *   用户能否只关动效、保留废土配色？能 —— 这就是分开的理由。
 *
 * 关掉动效只有一条规则（`[data-motion="off"]` 与 prefers-reduced-motion 共用一个块）：
 *   所有 `--motion-*` 时长都是**时长，且 0 表示不动**。基础皮肤把十个全声明为 0，
 *   所以它不但配色是旧的，**观感也是「完全静止」的** —— 这才是「原样保留」。
 *
 * 文案策略：零用户可见文案（bebot/text_audit.py 把新增静态资源里的 CJK 字符串
 *   当硬编码文案）。注释里的中文一律用「」包，不要用 ASCII 引号。
 *
 * 资源版本：引用本文件必须带 ?v=版本号（Cloudflare 对 /static/*.css 缓存 4 小时）。
 */

/* ─────────────────────────────────────────────────────────── 关键帧 ─────── */

/* 内容进场：整个游戏用得最多的一条（对话气泡 / 选项 / 系统通知 / 终端页都用它）。
 * 位移量走令牌，所以皮肤可以决定「飘多远」，0px 就是纯淡入。
 *
 * ⚠️ 这里动的是**独立变换属性**（CSS 的 translate），不是 transform 属性，这是刻意的、也是必须的：
 *   SVG 用 transform="translate(x,y)" 定位，而 CSS 的 transform **属性**会整体覆盖它
 *   （同名的表现属性优先级更低）。图屏的节点正好是这么定位的 —— 若用 transform 写这条动画，
 *   进场期间所有节点会被拽到原点，而 both 填充还会把它留在那儿。
 *   独立属性（translate / scale）与 transform 是**相乘**关系，所以它在 HTML 上等价、
 *   在 SVG 上与既有定位叠加。同理见下面的 t-pop。
 *   注：注释里不要用反引号包 CJK —— bEbot/text_audit.py 把成对反引号当字符串定界符。 */
@keyframes t-enter {
  from {
    opacity: 0;
    translate: 0 var(--motion-enter-shift);
  }
  to {
    opacity: 1;
    translate: none;
  }
}

/* 切页：闪一下再稳定。刻意不是淡入淡出 —— 荧光屏换页是「啪」的一下。 */
@keyframes t-swap {
  0% { opacity: 0.2; }
  35% { opacity: 0.95; }
  50% { opacity: 0.55; }
  100% { opacity: 1; }
}

/* 光标 / 菜单箭头闪烁。steps(1) 让它硬切，不是渐变。 */
@keyframes t-blink {
  50% { opacity: 0; }
}

/* 荧光屏亮度抖动。乘法形式（基准 × 系数）是为了保住 --texture-scanline：
 * 直接写死 .55 就等于把皮肤的档位架空了，那台屏幕上换档位会失效。 */
@keyframes t-flicker {
  0%, 80% { opacity: calc(var(--texture-scanline) * 1.00); }
  47% { opacity: calc(var(--texture-scanline) * 1.13); }
  48% { opacity: calc(var(--texture-scanline) * 0.91); }
  49% { opacity: calc(var(--texture-scanline) * 1.05); }
  81% { opacity: calc(var(--texture-scanline) * 1.09); }
}

/* 颗粒位移：三档跳，不插值。线性移动看起来像「镜头在飘」，不是胶片。 */
@keyframes t-grain {
  0% { transform: translate(0, 0); }
  33% { transform: translate(-37px, 21px); }
  66% { transform: translate(23px, -29px); }
  100% { transform: translate(0, 0); }
}

/* 扫描滚条：整条从上方屏外滚到下方屏外，10s 一圈。 */
@keyframes t-roll {
  0% { transform: translateY(-30vh); }
  100% { transform: translateY(130vh); }
}

/* 完成脉冲：那一下「叮」的视觉对应物。起点/终点都用令牌给的辉光。 */
@keyframes t-pulse {
  0% { box-shadow: 0 0 0 transparent; }
  25% { box-shadow: var(--motion-pulse-glow); }
  100% { box-shadow: 0 0 6px color-mix(in srgb, var(--accent) 15%, transparent); }
}

/* 注意力呼吸：节点 / 徽标在「这里有东西」时的扩散圈。
 * 同样用独立属性 `scale`（理由见 t-enter）：图屏的节点有 transform 定位，
 * 用 `transform: scale()` 会把定位一起抹掉。 */
@keyframes t-pop {
  0% { scale: 0.6; opacity: 0.9; }
  100% { scale: 1.9; opacity: 0; }
}

/* ───────────────────────────────────────────────────────── 工具类 ─────── */

/* 图标按钮：选中就反白，像 Pip-Boy 的选中态。用 keyframes 而不是 transition 的
 * 元素（如 .t-done）走下面各自的类。 */
:where(button, a, [role="button"], .ui-nav-item, .card, input, select, textarea) {
  transition: background-color 0.1s, color 0.1s, border-color 0.1s, box-shadow 0.1s;
}

/* 进场：默认给新出现的内容块。motion.js 会自动挂这个类并加错峰延迟。 */
.t-enter {
  animation: t-enter var(--motion-enter-duration) ease-out both;
}

/* 切页整块闪一下。 */
.t-swap {
  animation: t-swap var(--motion-swap-duration) ease-out both;
}

/* 完成脉冲：一次性的，不循环。 */
.t-done {
  animation: t-enter var(--motion-enter-duration) ease-out both,
             t-pulse var(--motion-pulse-duration) ease-out both;
}

/* 光标：放在文字后面当下划线光标用（内容由页面给，这里不给字符，
 * 否则等于往样式表里写文案）。 */
.t-cursor::after {
  content: "";
  display: inline-block;
  width: 0.55em;
  height: 1em;
  margin-left: 2px;
  vertical-align: -0.12em;
  background: currentColor;
  animation: t-blink var(--motion-blink) steps(1) infinite;
}

/* 注意力呼吸圈：需要一个定位父元素。 */
.t-pop {
  position: relative;
}

.t-pop::after {
  content: "";
  position: absolute;
  inset: -4px;
  border: 1px solid var(--accent);
  border-radius: inherit;
  pointer-events: none;
  animation: t-pop var(--motion-pulse-duration) ease-out infinite;
}

/* ─────────────────────────────────────────────── CRT 动效层（grain + roll）──
 * tokens.css 已经占了 html 的两个伪元素做**静态**扫描线与暗角；这里再要两层
 * 「会动」的，所以由 motion.js 挂一个 .t-crt 容器（对应游戏里的 #crt）。
 * z-index 比静态层低一位，于是扫描线仍然压在最上面 —— 与游戏一致。
 * 动效关闭时 motion.js 不挂这个容器，连节点都不存在。 */
.t-crt {
  position: fixed;
  inset: 0;
  z-index: 2147482999;
  pointer-events: none;
  overflow: hidden;
}

/* 颗粒：内联 SVG 噪声，零外部请求（与 ui-shared.css 的 .ui-grain 同源做法）。
 * 不用现成的 .ui-grain 是因为那个层的挂载与档位归 ui-shared.js 管，
 * 动效层要能独立开关。 */
.t-crt::before {
  content: "";
  position: absolute;
  inset: -60px;
  opacity: var(--texture-grain);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='140' height='140' filter='url(%23n)'/></svg>");
  background-size: 140px 140px;
  animation: t-grain var(--motion-crt-grain) steps(3) infinite;
}

/* 滚条：一条很扁的亮带扫过全屏。 */
.t-crt::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 18vh;
  background: linear-gradient(
    to bottom,
    transparent,
    color-mix(in srgb, var(--accent) 7%, transparent) 45%,
    color-mix(in srgb, var(--accent) 12%, transparent) 55%,
    transparent
  );
  animation: t-roll var(--motion-crt-roll) linear infinite;
}

/* 亮度抖动：加在**静态扫描线层**上（它就是游戏里的 body::before）。 */
html::after {
  animation: t-flicker var(--motion-crt-flicker) steps(1) infinite;
}

/* ─────────────────────────────────────────── 关掉「终端质感」（CRT off）─────
 * 与「关动效」是**两件事**，刻意分开（参考游戏里也是两个开关：`.nocrt` 与
 * reduced-motion）：
 *   · data-motion="off" → 画面静止，但静态扫描线/暗角还在（还是那台机器，只是不闪）
 *   · data-crt="off"    → 连静态纹理一起关掉（变成一台普通显示器）
 * 为什么值得单独留一个：扫描线在手机小屏上对某些人是真的累眼，而他们未必想
 * 放弃整屏配色。给一个「只是别加纹理」的选项，比逼人在「全都要」与「全不要」之间选要诚实。
 * 关掉时同样把 CRT 节点移除（motion.js 负责），不只是把 opacity 归零。 */
[data-crt="off"] {
  --texture-scanline: 0;
  --texture-vignette: 0;
  --texture-grain: 0;
  --texture-glow: 0;
}

[data-crt="off"] .t-crt {
  display: none;
}

/* ─────────────────────────────────────────────── 关掉动效（唯一开关）─────
 * 两个触发条件各写一遍（@media 不能进选择器列表），但语义完全相同：
 *   ① 用户在开关里关掉 → motion.js 给根元素写 data-motion="off"
 *   ② 系统 prefers-reduced-motion → 同一套规则，无需 JS 参与（首帧即生效）
 * 口径与游戏一致：整个页面一律不动，而不是「变快一点」。 */
[data-motion="off"] *,
[data-motion="off"] *::before,
[data-motion="off"] *::after {
  animation: none !important;
  transition: none !important;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
  }
}

@media print {
  .t-crt {
    display: none;
  }
}
