/* 门户品牌动态标识 · 入场版（2026-08-13 接入门户登录页）
 *
 * 真源：docs/design/icon_motion_20260811/approved/motion_intro.css（拍板只读区）
 * 本文件由该文件程序化转换而来 —— 仅两处改动：
 *   1. id 选择器（#mark-root / #spark-big）改 class（.p2m-root / .p2m-spark），
 *      因为同一页可能嵌多份（设计预览的亮/暗双栏），id 重复只命中第一份；
 *   2. 文件末尾补 prefers-reduced-motion 与容器尺寸（原文件刻意不写 media
 *      query，见其行内注释：由 showcase 脚本外层包裹）。
 * **几何与关键帧数值不得在此改动** —— 要改先改 approved 源再重新生成。
 * 注：下方注释里出现的 #mark-root 是散文引用，钩子已全部 class 化。
 *
 * 摆幅安全区（approved/README.md 逐帧打磨结论，越过即露出轮廓）：
 *   前臂绕肘 −16°~+6.5° · 后前臂绕肘 −7.5°~+18° · 小腿绕膝 −12°~+1°
 */

/* TR WheelChair 图标动效 · motion.css
 *
 * 几何来源：approved/logo_motion_geom.svg（已拍板，不可改）
 * 关节坐标：approved/joints.json —— transform-origin 必须取这些值，
 *           改动即破坏「共关节圆 ⇒ 摆动隐形」的前提。
 *
 * 可动件只有三个：
 *   .p-arm-front  绕 sh_front (145.80, 66.53)
 *   .p-arm-back   绕 sh_back  (173.45, 82.25)
 *   .p-shank      绕 knee_j   (201.00, 165.00)
 * 其余（躯干 / R 字 / L型靠腿 / 大轮 / 速度线 / 椅架 / 头）随 #mark-root 整体推进。
 *
 * 人格三词：精准 · 迅捷 · 稳健 —— 位移走直线不走弧线，收尾过冲 ≤1%，不做弹跳。
 * 时间结构 20 : 50 : 30（anticipation : action : follow-through），总时长 1500ms。
 *
 * ⚠ 关键帧内的 timing-function 必须写**字面** cubic-bezier。
 *   写 var(--token) 在 Chromium 里会被静默丢弃、退化成 linear，
 *   于是各段之间出现速度断崖 —— casual 播放和均匀帧条都看不出来，
 *   只有用 probe 读计算值才会暴露。tokens 仅用于 animation 简写与文档。
 */

:root {
  --p2m-duration: 1500ms;
  /* 缓动令牌（仅供 animation 简写与阅读；关键帧内一律写字面值） */
  --p2m-ease-gather: cubic-bezier(0.42, 0, 0.92, 0.58);  /* 蓄势：慢起加速 */
  --p2m-ease-drive:  cubic-bezier(0.16, 1, 0.30, 1);     /* 爆发：强 ease-out */
  --p2m-ease-settle: cubic-bezier(0.33, 0, 0.20, 1);     /* 落定：柔和收敛 */
}

/* 所有 transform 以 viewBox 坐标为准，关节坐标才能直接用 */
.p2m-root, .p2m-root * { transform-box: view-box; }

/* ── 底盘整体推进 ──────────────────────────────────────────────
 * 从左侧远处蓄势推近 → 爆发冲入 → 过冲 2.2px 后收敛。
 * 起始 opacity 0.06 而非 0：保证第一帧就可见，避免"凭空出现"。 */
.p2m-root {
  transform-origin: 128px 150px;
  animation: p2m-root var(--p2m-duration) both;
}
@keyframes p2m-root {
  0%   { opacity: 0.06; transform: translateX(-58px) scale(0.93);
         animation-timing-function: cubic-bezier(0.42, 0, 0.92, 0.58); }  /* gather */
  20%  { opacity: 0.55; transform: translateX(-44px) scale(0.952);
         animation-timing-function: cubic-bezier(0.16, 1, 0.30, 1); }     /* drive */
  62%  { opacity: 1;    transform: translateX(2.2px) scale(1.008);
         animation-timing-function: cubic-bezier(0.33, 0, 0.20, 1); }     /* settle */
  78%, 100% { opacity: 1; transform: translateX(0) scale(1); }
}

/* ── 可动件：只旋转，绝不自带位移 ──────────────────────────────
 *
 * ⚠ 铁律 1：肢体的平移**必须**完全由父级 #mark-root 带动，
 *   关键帧里只能出现 rotate。
 *
 * 为什么（2026-08-12 返工）：初版给手臂写了 translateX(-27px)、
 * 小腿写了 -18px，而 #mark-root 是 -58px。CSS transform 会叠加，
 * 于是起始帧手臂比躯干多偏 31px、小腿多偏 40px —— 肢体整段平移脱离身体，
 * 看起来像「断手拼接」。用户当场指出这一点。
 *
 * ⚠ 铁律 2：**肩不转，摆幅全部交给肘**（2026-08-13 逐帧打磨）。
 *
 * 逐帧扫描发现：凸起只发生在**肩关节区** —— 手臂上缘与躯干斜边的夹角处，
 * 臂身摆动时会扫出躯干轮廓。这是刚体绕轴旋转的几何必然（真人靠皮肤形变
 * 化解，刚体不行）。单段手臂 ±16°/±18° 时越界达 26/36px。
 *
 * 扫描肩肘摆幅分配的结果很干脆 —— 肩角越小越界越少，肩角 0° 时降到 0~1px：
 *   肩 16.0° 肘  0.0° → 26px      肩  4.0° 肘 12.0° →  4px
 *   肩  8.0° 肘  8.0° → 13px      肩  0.0° 肘 16.0° →  0px  ← 采用
 * 动画上也成立：冲刺时肩部本就相对固定，摆动主要发生在肘。
 *
 * 故 .p-arm-front / .p-arm-back（上臂）保持静止，
 * 只有 .p-arm-*-fore（前臂）绕肘摆动。上臂仍需存在 —— 它是前臂与躯干之间
 * 的连接段，且其肩端圆盘是「共圆心」的锚点。
 */

/* 前臂（前伸侧）：绕 elbow_front 摆动，承担全部摆幅 */
.p-arm-front-fore {
  transform-origin: 123px 55.9px;
  animation: p2m-fore-front var(--p2m-duration) both;
}
@keyframes p2m-fore-front {
  0%   { transform: rotate(-16deg);
         animation-timing-function: cubic-bezier(0.42, 0, 0.92, 0.58); }
  22%  { transform: rotate(-13.5deg);
         animation-timing-function: cubic-bezier(0.16, 1, 0.30, 1); }
  70%  { transform: rotate(6.5deg);
         animation-timing-function: cubic-bezier(0.33, 0, 0.20, 1); }
  84%, 100% { transform: rotate(0deg); }
}

/* 前臂（后摆侧）：绕 elbow_back，与前伸侧反向 —— 双臂对摆才像跑动 */
.p-arm-back-fore {
  transform-origin: 206.7px 99.12px;
  animation: p2m-fore-back var(--p2m-duration) both;
}
@keyframes p2m-fore-back {
  0%   { transform: rotate(18deg);
         animation-timing-function: cubic-bezier(0.42, 0, 0.92, 0.58); }
  22%  { transform: rotate(15deg);
         animation-timing-function: cubic-bezier(0.16, 1, 0.30, 1); }
  68%  { transform: rotate(-7.5deg);
         animation-timing-function: cubic-bezier(0.33, 0, 0.20, 1); }
  82%, 100% { transform: rotate(0deg); }
}

/* 小腿：绕 knee_j（膝以下才动，R 字整体不动）
 * 单关节，无分摊余地 —— 半径已压至内切（×0.93）消除静态凸起。
 *
 * 摆幅**不对称**：逐度扫描得安全区间 −14° ~ +1°（越界 ≤1px）。
 * 正向一过 +1° 就在膝内侧（x≈196–199, y≈168–175）顶破 R 字轮廓，
 * 负向到 −14° 都只有 1px。这与动画语义正好吻合 ——
 * 起始蓄势时小腿后收（负向），落定回到 0，本就不需要正向过冲。
 * 故取 −12° 起、收尾走到 +1° 即止。最晚落定（88%），关节链末端拖尾。 */
.p-shank {
  transform-origin: 201px 165px;
  animation: p2m-shank var(--p2m-duration) both;
}
@keyframes p2m-shank {
  0%   { transform: rotate(-12deg);
         animation-timing-function: cubic-bezier(0.42, 0, 0.92, 0.58); }
  24%  { transform: rotate(-9.8deg);
         animation-timing-function: cubic-bezier(0.16, 1, 0.30, 1); }
  72%  { transform: rotate(1deg);
         animation-timing-function: cubic-bezier(0.33, 0, 0.20, 1); }
  88%, 100% { transform: rotate(0deg); }
}

/* ── 速度线：依次剥离（secondary action）────────────────────────
 * 延后到 24–33% 才入场，不与主体抢注意力（staging）。
 * 三条错峰 60ms，越靠下越晚 —— 制造"被甩在后面"的速度感。 */
.p-speed-top, .p-speed-mid, .p-speed-bot {
  animation-duration: var(--p2m-duration);
  animation-fill-mode: both;
}
.p-speed-top { animation-name: p2m-speed-a; }
.p-speed-mid { animation-name: p2m-speed-b; }
.p-speed-bot { animation-name: p2m-speed-c; }

@keyframes p2m-speed-a {
  0%, 24%  { opacity: 0; transform: translateX(-34px) scaleX(0.55);
             animation-timing-function: cubic-bezier(0.16, 1, 0.30, 1); }
  66%      { opacity: 1; transform: translateX(1.8px) scaleX(1.02);
             animation-timing-function: cubic-bezier(0.33, 0, 0.20, 1); }
  76%, 100% { opacity: 1; transform: translateX(0) scaleX(1); }
}
@keyframes p2m-speed-b {
  0%, 28%  { opacity: 0; transform: translateX(-40px) scaleX(0.5);
             animation-timing-function: cubic-bezier(0.16, 1, 0.30, 1); }
  70%      { opacity: 1; transform: translateX(2.1px) scaleX(1.025);
             animation-timing-function: cubic-bezier(0.33, 0, 0.20, 1); }
  80%, 100% { opacity: 1; transform: translateX(0) scaleX(1); }
}
@keyframes p2m-speed-c {
  0%, 33%  { opacity: 0; transform: translateX(-46px) scaleX(0.45);
             animation-timing-function: cubic-bezier(0.16, 1, 0.30, 1); }
  74%      { opacity: 1; transform: translateX(2.4px) scaleX(1.03);
             animation-timing-function: cubic-bezier(0.33, 0, 0.20, 1); }
  84%, 100% { opacity: 1; transform: translateX(0) scaleX(1); }
}

/* ── 轮圈高光：表达转动 ─────────────────────────────────────────
 * 为什么不直接 rotate 轮子：大轮是「同心圆环 + 圆心毂」，没有辐条，
 * 严格旋转对称 —— rotate 任意角度渲染结果逐像素相同，转动完全不可见。
 * 故用一段沿轮圈跑的高光弧表达。
 *
 * 基础 opacity=0 写在 SVG 属性上，76% 后归零：
 * 这是 Final Frame Contract 能逐像素相等的前提，
 * 也让减弱动效模式下高光弧自动不可见。 */
.p2m-spark {
  animation: p2m-spark var(--p2m-duration) both;
}
@keyframes p2m-spark {
  0%, 26% { opacity: 0; stroke-dashoffset: 0.55;
            animation-timing-function: cubic-bezier(0.16, 1, 0.30, 1); }
  34%     { opacity: 0.85; }
  62%     { opacity: 0.7;  stroke-dashoffset: -0.42;
            animation-timing-function: cubic-bezier(0.33, 0, 0.20, 1); }
  76%, 100% { opacity: 0; stroke-dashoffset: -0.62; }
}

/* ── 无障碍 ────────────────────────────────────────────────────
 * 这里**不写** prefers-reduced-motion —— animate_svg_showcase.py 会把整个
 * 文件包进它自己的 media query 里，重复写会嵌套失效（脚本会直接告警）。
 * 减弱动效下的静态呈现由该脚本保证。 */


/* ── 容器 ──────────────────────────────────────────────────────
 * 尺寸由外层 .auth-mark 给（portal.css），此处只保证 SVG 撑满且不裁切。
 * overflow:visible —— 入场第一帧整体在左侧 −58px 处，裁切会切掉推进过程。
 * 不加圆角方框底板：本 SVG 自带白色 shell 轮廓层（sticker 描边），亮/暗
 * 主题下都有对比，故无须 .logo-plate 那块白底 —— 那是给无 shell 的扁平
 * PNG 垫的（见 portal.css「品牌底板」注释）。 */
.p2m-svg { display: block; width: 100%; height: 100%; overflow: visible; }

/* ── 静态终帧档（.p2m-static）+ 减弱动效 ────────────────────────
 * 两者要的是同一件事：直接停在终态（= 拍板静态姿态）—— 可动件回 0°、
 * 速度线 opacity 1、轮圈高光 0。故共用一套规则，避免两处各写一遍而漂移。
 *
 * .p2m-static 的用途（2026-08-13）：登录页之外的认证页（验证码 / 锁定 /
 * 注册 / 找回密码 / 注册完成 / 重发验证信）也换成无底板标识，但**不播动画** ——
 * 每翻一页重播一次 1.5s 入场会吵，尤其 MFA 那步用户本就着急；而样式统一后
 * 「登录 → 验证码」不再出现标识忽大忽小、忽有框忽无框的跳变。
 *
 * 用 animation:none 而非 duration:0.01ms —— 后者仍会跑 fill-mode，
 * 慢机器上可能闪一帧起始姿态（整体在左侧 −58px、几乎全透明）。
 *
 * 终态值与 approved/logo.svg 静止态逐像素一致（Final Frame Contract）。 */
.p2m-static,
.p2m-static .p-arm-front-fore,
.p2m-static .p-arm-back-fore,
.p2m-static .p-shank,
.p2m-static .p-speed-top,
.p2m-static .p-speed-mid,
.p2m-static .p-speed-bot,
.p2m-static .p2m-spark { animation: none !important; }
.p2m-static { opacity: 1; transform: none; }
.p2m-static .p-arm-front-fore,
.p2m-static .p-arm-back-fore,
.p2m-static .p-shank { transform: rotate(0deg); }
.p2m-static .p-speed-top,
.p2m-static .p-speed-mid,
.p2m-static .p-speed-bot { opacity: 1; transform: none; }
.p2m-static .p2m-spark { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .p2m-root,
  .p2m-root .p-arm-front-fore,
  .p2m-root .p-arm-back-fore,
  .p2m-root .p-shank,
  .p2m-root .p-speed-top,
  .p2m-root .p-speed-mid,
  .p2m-root .p-speed-bot,
  .p2m-root .p2m-spark { animation: none !important; }
  .p2m-root { opacity: 1; transform: none; }
  .p2m-root .p-arm-front-fore,
  .p2m-root .p-arm-back-fore,
  .p2m-root .p-shank { transform: rotate(0deg); }
  .p2m-root .p-speed-top,
  .p2m-root .p-speed-mid,
  .p2m-root .p-speed-bot { opacity: 1; transform: none; }
  .p2m-root .p2m-spark { opacity: 0; }
}
