/* ============================================================
   033 陪伴角色「票票」（内部代号 ip-mascot）· APP 黑白手绘线稿版动画
   ------------------------------------------------------------
   前身是 030 的心情兔；老板 2026-10-17 拍板「换掉了，你去搞一下然后替换掉」，
   造型按 IP形象交付包_票票线稿_20261017/ 的 B 定稿逐点量取重绘（母本在 index.html）。
   文件名与 `.mq` 根类保留不动：sw PRECACHE、index.html 的 link、皮肤与守闸都认这个名字，
   换角色不该顺带欠一笔"重接链路"的账。

   纪律（守闸见 specs/030-mood-mascot/spec.md §5 与 specs/033-ip-mascot/spec.md §11）：
   · 本文件**零硬编码色值**：颜色只允许 var(--*) 或由 var() 组成的 color-mix。
     理由不是洁癖 —— 我们有 11 档皮肤 × light/dark 双色彩，写死一个色值
     就等于给某档皮肤埋一笔「亮档颜色进暗档反噬」的账（024/027 踩过两次）。
   · 动画只用 transform / opacity；滤镜属性一律静态。
   · 每个循环周期 ≤14s，或振幅 ≥16px，或 scale ≥1.15（027 定的三选一硬口径）。
   · prefers-reduced-motion 下关掉循环与位移，但**保留静态表情差异** ——
     情绪是信息不是装饰，不能连表情一起关。
   · 三件识别物（邮差帽 / 金印章 / 罗盘）任何档位都不隐藏（交接文档红线 §9.1）。

   ⚠ 两处与交接文档的口径冲突，都按「红线 / 现有纪律」优先处理并记账：
   1) §6 要 tired 档配「小 z 圈」，但 §9.2 明禁图内出现文字/字母/数字 —— z 是字母。
      → 改成三个渐大空心泡（呼噜泡），语义保留、字母不出现。
   2) §2/§5 说罗盘指针是「红蓝双色」，但品牌 token 里**没有蓝**（--vermillion 朱砂 /
      --pine 墨绿 / --gold 金 / --ink 墨），而本文件禁硬编码色值。
      → 指针两半走 --vermillion 与 --ink（读起来仍是"双色细针"），不为一个点缀色破零硬编码的规矩。
   ============================================================ */

/* ---------- 容器 ---------- */
/* 票票是竖构图（viewBox 240×330），用 aspect-ratio 锁比例：
   写死 width+height 会在换 viewBox 时悄悄拉扁角色。 */
.mq-field {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3, 12px);
}
.mq-field .seg { width: 100%; }

/* 节点就是 <svg class="mq ip"> 本身（从 index.html 的 <template> 克隆而来，
   不经内联标记注入 —— security 守闸硬禁注入面）。
   132px 宽 = 高约 181px，比原兔子（168×162）只多 19px 高，
   校准屏的单屏预算（025）由真浏览器逐视口量，不靠这里保证。 */
.mq {
  display: block;
  width: 132px;
  height: auto;
  aspect-ratio: 240 / 330;
  margin: 0 auto;
  overflow: visible;          /* 滤镜作用域与伴随元素不许被 viewBox 裁掉 */
  /* 装饰层不许挡操作（025 R7 那笔账：挂错层会被卡片和 sticky dock 挡在可视区外） */
  pointer-events: none;
  position: relative;
  z-index: 0;
}

/* ---------- 线稿与填充：只从 token 取色 ---------- */
.ip-ink {
  fill: none;
  stroke: var(--ink);
  stroke-width: 3.1;
  stroke-linecap: round;      /* 圆头 = 马克笔起收笔，稚拙感的一半在这里 */
  stroke-linejoin: round;     /* 手绘风格必开 */
}
/* 内部细节细线：交接文档 §4「外轮廓粗重、内部细节细线」的"细"就在这一条 */
.ip-thin { stroke-width: 1.5; }
.ip-fill { fill: var(--ink); stroke: none; }
/* 眼内高光与留白件走纸色 —— 暗档自动翻深，不写死白（红线 §9.6 的正向半边） */
.ip-hl   { fill: var(--paper); stroke: none; }
.ip-dial { fill: var(--paper); }
/* 仅两处点缀色：印章底座浅金、罗盘指针红/墨（见文件头冲突记账） */
.ip-gold-ring  { stroke: var(--gold); }
.ip-gold-fill  { fill: var(--gold); stroke: var(--ink); stroke-width: 2.2; }
.ip-needle-red { fill: var(--vermillion); stroke: none; }
.ip-needle-blue { fill: var(--ink); stroke: none; }

/* 腮红：交接文档红线 §9.5 明写「不删除腮红与微笑这两个亲和来源」。
   上一版沿用 030 的 `--ink 20%` 淡染 —— 在米纸底上实测完全看不出来，
   那等于把红线里点名的亲和来源做成了一个隐形件（「看不见的等于没有」）。
   改成走品牌自己的暖色 --vermillion 淡染：11 档皮肤都有这个 token（:root 定底、皮肤只覆写），
   仍然零硬编码色值，但真的看得出来是腮红。皮肤想换口味继续覆写 --mq-blush。 */
.ip-blush ellipse {
  fill: var(--mq-blush, color-mix(in srgb, var(--vermillion) 34%, transparent));
  stroke: none;
  opacity: 0;
  transition: opacity .32s ease;
}

/* ---------- 状态切换：哪套眼 / 嘴 / 伴随元素显形 ---------- */
.ip-eye, .ip-mouth, .ip-prop, .ip-brow { opacity: 0; transition: opacity .22s ease; }

/* 十档（§6 表情集：先对齐现五档，再补 think/cheer/sorry/night） */
.mq-idle  .ip-eye-open,  .mq-idle  .ip-mouth-slight { opacity: 1; }
.mq-great .ip-eye-happy, .mq-great .ip-mouth-big    { opacity: 1; }
.mq-ok    .ip-eye-open,  .mq-ok    .ip-mouth-slight { opacity: 1; }
.mq-meh   .ip-eye-half,  .mq-meh   .ip-mouth-flat   { opacity: 1; }
.mq-tired .ip-eye-droopy, .mq-tired .ip-mouth-down  { opacity: 1; }
.mq-bad   .ip-eye-wide,  .mq-bad   .ip-mouth-down   { opacity: 1; }
.mq-think .ip-eye-half,  .mq-think .ip-mouth-small  { opacity: 1; }
.mq-cheer .ip-eye-wide,  .mq-cheer .ip-mouth-open   { opacity: 1; }
.mq-sorry .ip-eye-droopy, .mq-sorry .ip-mouth-down  { opacity: 1; }
.mq-night .ip-eye-closed, .mq-night .ip-mouth-small { opacity: 1; }

/* 挑眉毛只属于 think（半阖眼组被 meh 共用，嵌套 opacity 相乘，所以能单独关） */
.mq-think .ip-brow { opacity: 1; }

/* 腮红浓度：亲和来源之一，红线不许关。
   idle 也给淡腮红：定稿图的中立姿态本来就带两团，而 idle 是用户看到这个角色的第一眼，
   不能因为「还没选心情」就把红线点名的亲和来源关掉。 */
.mq-idle  .ip-blush ellipse { opacity: .35; }
.mq-great .ip-blush ellipse { opacity: .95; }
.mq-cheer .ip-blush ellipse { opacity: .8;  }
.mq-ok    .ip-blush ellipse { opacity: .5;  }
.mq-think .ip-blush ellipse { opacity: .3;  }
.mq-sorry .ip-blush ellipse { opacity: .3;  }
.mq-tired .ip-blush ellipse { opacity: .38; }
.mq-night .ip-blush ellipse { opacity: .25; }

/* 伴随元素按档对位 */
.mq-great .ip-prop-stars,
.mq-think .ip-prop-bubble,
.mq-tired .ip-prop-zzz,
.mq-sorry .ip-prop-sweat,
.mq-night .ip-prop-moon,
.mq-bad   .ip-prop-rain { opacity: 1; }

/* ---------- 手臂角度：肩为轴（外层管心情，内层管待机摆动） ---------- */
/* 两个 transform 不能挤在同一元素上（CSS 不能把两个 transform 相加）——
   030 的耳朵就是这么拆成外/内两层的，手臂沿用。 */
.ip-arm { transform-box: view-box; }
.ip-arm-l { transform-origin: 79px 188px; }   /* 举章臂：肩在衣领左下 */
.ip-arm-r { transform-origin: 203px 188px; }  /* 下垂臂：肩在衣领右下（随手臂外移同步改了轴心） */

.mq-idle  .ip-arm-l { transform: rotate(0deg); }
/* 开心：举章臂上扬，印章跟着亮起来（配合下面一次性盖章） */
.mq-great .ip-arm-l { transform: rotate(-9deg); }
/* 鼓励：下垂臂抬起来挥手 */
.mq-cheer .ip-arm-r { transform: rotate(-34deg); }
/* 累/抱歉：两只手都往下坠一点 */
.mq-tired .ip-arm-l, .mq-sorry .ip-arm-l { transform: rotate(7deg); }
.mq-tired .ip-arm-r, .mq-sorry .ip-arm-r { transform: rotate(6deg); }
/* 晚安：手臂收着 */
.mq-night .ip-arm-l { transform: rotate(4deg); }

.ip-arm, .ip-eye, .ip-mouth, .ip-blush ellipse, .ip-prop, .ip-brow {
  transition: transform .34s cubic-bezier(.34, 1.42, .5, 1), opacity .22s ease;
}

/* ---------- idle 循环：轻，但必须真在动 ---------- */
/* 呼吸：3.6s ≤ 14s ✓。挂 .ip-figure（主体层），阴影反向。 */
@keyframes ip-breathe {
  from { transform: translateY(0); }
  to   { transform: translateY(-5px); }
}
.ip-figure {
  transform-box: view-box;
  animation: ip-breathe 3.6s ease-in-out infinite alternate;
}
/* 身体上浮时阴影缩小变淡（交接文档 §7 最后一条） */
@keyframes ip-shadow {
  from { transform: scale(1);    opacity: .9; }
  to   { transform: scale(.9);   opacity: .62; }
}
.ip-shadow {
  transform-box: view-box;
  transform-origin: 126px 318px;
  animation: ip-shadow 3.6s ease-in-out infinite alternate;
}

/* 双臂待机轻摆：左右异周期 (5.4s / 6.3s) 自然错峰，摆的是内层 .ip-arm-sway */
@keyframes ip-sway-a {
  from { transform: rotate(-2.6deg); }
  to   { transform: rotate(2.6deg);  }
}
@keyframes ip-sway-b {
  from { transform: rotate(3deg);  }
  to   { transform: rotate(-3deg); }
}
.ip-arm-l .ip-arm-sway {
  transform-box: view-box;
  transform-origin: 79px 188px;
  animation: ip-sway-a 5.4s ease-in-out infinite alternate;
}
.ip-arm-r .ip-arm-sway {
  transform-box: view-box;
  transform-origin: 203px 188px;
  animation: ip-sway-b 6.3s ease-in-out infinite alternate;
}

/* 眨眼：4.4s 一次、闭合约 0.15s（周期 ≤14s ✓）。只眨实心眼，弧线眼不眨。 */
@keyframes ip-blink {
  0%, 90%, 100% { transform: scaleY(1); }
  93%, 96%      { transform: scaleY(0.08); }
}
.ip-eye-open, .ip-eye-wide, .ip-eye-half, .ip-eye-droopy {
  transform-box: view-box;
  transform-origin: 126px 139px;
  animation: ip-blink 4.4s steps(1, end) infinite;
}

/* 罗盘指针：待机极慢摆（12.5s ≤14s ✓）；think 时明显转一圈找方向 */
@keyframes ip-needle-idle {
  from { transform: rotate(-16deg); }
  to   { transform: rotate(13deg);  }
}
.ip-needle {
  transform-box: view-box;
  transform-origin: 100px 213px;
  animation: ip-needle-idle 12.5s ease-in-out infinite alternate;
}
@keyframes ip-needle-scan {
  0%   { transform: rotate(0deg); }
  55%  { transform: rotate(300deg); }
  78%  { transform: rotate(344deg); }
  100% { transform: rotate(352deg); }
}
.mq-think .ip-needle { animation: ip-needle-scan 2.8s cubic-bezier(.2,.9,.2,1) infinite; }

/* 星星闪：opacity + 轻缩放，2.6s */
@keyframes ip-twinkle {
  0%, 100% { opacity: .35; transform: scale(.82) rotate(0deg); }
  50%      { opacity: 1;   transform: scale(1.18) rotate(18deg); }
}
.mq-great .ip-prop-stars path {
  transform-box: view-box;
  animation: ip-twinkle 2.6s ease-in-out infinite;
}
.mq-great .ip-prop-stars path:nth-child(2) { animation-duration: 3.1s; animation-delay: -.9s; }

/* 雨滴：位移 19px ≥ 16px ✓（走「振幅」分支） */
@keyframes ip-fall {
  0%   { opacity: 0; transform: translateY(0); }
  18%  { opacity: 1; }
  100% { opacity: 0; transform: translateY(19px); }
}
.mq-bad .ip-drop-1 { transform-box: view-box; animation: ip-fall 1.8s ease-in infinite; }
.mq-bad .ip-drop-2 { transform-box: view-box; animation: ip-fall 1.8s ease-in infinite; animation-delay: -.85s; }

/* 呼噜泡上飘：3.6s，逐枚错相 */
@keyframes ip-drift {
  0%   { opacity: 0;   transform: translate(0, 6px) scale(.7); }
  30%  { opacity: .95; }
  100% { opacity: 0;   transform: translate(7px, -18px) scale(1.12); }
}
.mq-tired .ip-prop-zzz circle {
  transform-box: view-box;
  animation: ip-drift 3.6s ease-in-out infinite;
}
.mq-tired .ip-prop-zzz circle:nth-child(2) { animation-delay: -1.2s; }
.mq-tired .ip-prop-zzz circle:nth-child(3) { animation-delay: -2.4s; }

/* 思考泡漂浮 */
@keyframes ip-float {
  0%, 100% { transform: translate(0, 0); }
  50%      { transform: translate(-3px, -7px); }
}
.mq-think .ip-prop-bubble {
  transform-box: view-box;
  animation: ip-float 4.4s ease-in-out infinite;
}

/* 汗滴：往下坠 17px ≥16px ✓ */
@keyframes ip-drip {
  0%   { opacity: 0; transform: translateY(0); }
  25%  { opacity: 1; }
  100% { opacity: 0; transform: translateY(17px); }
}
.mq-sorry .ip-prop-sweat path {
  transform-box: view-box;
  animation: ip-drip 2.4s ease-in infinite;
}

/* 乌云慢慢横移 */
@keyframes ip-cloud {
  0%, 100% { transform: translateX(-4px); }
  50%      { transform: translateX(5px); }
}
.mq-bad .ip-prop-rain path:first-child {
  transform-box: view-box;
  animation: ip-cloud 7.2s ease-in-out infinite;
}

/* 挥手：cheer 档在抬起的基础上小幅摆（肩为轴，外层已经 rotate 过，这里摆内层） */
@keyframes ip-wave {
  from { transform: rotate(-13deg); }
  to   { transform: rotate(13deg);  }
}
.mq-cheer .ip-arm-r .ip-arm-sway {
  animation: ip-wave .72s ease-in-out infinite alternate;
}

/* ---------- 盖章：出票根那一下（一次性 pop，交接文档 §6/§7） ---------- */
/* 印章随举章臂向下轻落 + 回弹。挂在 great 上 = 「抽到好支线 / 完成出票根」那一刻；
   不做成"等着将来有人调"的死动画（禁过度工程）。 */
@keyframes ip-stamp {
  0%   { transform: rotate(-9deg); }
  32%  { transform: rotate(15deg); }
  58%  { transform: rotate(-3deg); }
  78%  { transform: rotate(9deg);  }
  100% { transform: rotate(-9deg); }
}
.mq-great .ip-arm-l {
  animation: ip-stamp .62s cubic-bezier(.3, 1.4, .4, 1) 1;
}
@keyframes ip-stamp-press {
  0%, 100% { transform: translateY(0); }
  34%      { transform: translateY(7px); }
}
.mq-great .ip-stamp {
  transform-box: view-box;
  animation: ip-stamp-press .62s ease-out 1;
}

/* ---------- 换档一次性弹一下：scale 1.16 ≥ 1.15 ✓ ---------- */
@keyframes mq-pop {
  0%   { transform: scale(1); }
  38%  { transform: scale(1.16); }
  70%  { transform: scale(.972); }
  100% { transform: scale(1); }
}
.mq.mq-pop .mq-stage {
  transform-box: view-box;
  transform-origin: 126px 318px;   /* 从脚下长起来，不是原地胀大 */
  animation: mq-pop .42s cubic-bezier(.34, 1.56, .5, 1) 1;
}

/* ---------- 入场描边生长：pathLength=1 归一，纯 CSS 免 JS 量长度 ---------- */
@keyframes mq-draw { to { stroke-dashoffset: 0; } }
.mq-draw {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: mq-draw .58s ease-out forwards;
}
.ip-head { animation-delay: .04s; }
.ip-arm .mq-draw { animation-delay: 0s; }

/* 画完的稳定终态。注意这里**不能靠摘掉 .mq-draw 实现**：
   摘类会让 stroke-dasharray 从 1 变成无，位移滤镜会重新光栅化，
   边缘抗锯齿跳一格 —— reduced-motion 验收会把它误报成「动画未归零」（030 实测 0.1479%）。
   给一个显式终态，两边字节一致，才不会把「滤镜重光栅」误报成「还在动」。 */
.mq-drawn {
  animation: none !important;
  stroke-dasharray: none;
  stroke-dashoffset: 0;
}

/* ---------- specs/034 S2：四处扩点的挂载尺寸与位置 ----------
   只写尺寸/位置，**不新增任何 @keyframes 或 animation**：
   think 的指针、great 的盖章、cheer 的挥手全部复用上面已有动效，
   于是下面 prefers-reduced-motion 那一段天然盖到这三处（守闸 L6 钉“新块里不许出现 animation:”）。
   颜色一个都没写 —— 角度的事归线稿层，线稿只从 token 取色（M1）。 */

/* 等待态（home.js）：原来那块位置坐的是 160×160 罗盘，票票换成 108px 宽（高约 148），
   不拿角色去撑屏，也不让它比“正在为你寻找”那行字还抢镜。 */
.mq.ip-slot-wait { width: 108px; }

/* 完成盖章（completion-fx.js）：挂在 .cfx 那个 260×260 舞台上。
   注：anim-completion.css:23 定过 `.cfx span, .cfx div { position:absolute }`，
   而票票的根节点是 <svg> —— 它拿不到那条规则，必须在这里自己摆位，
   否则会当成普通流元素掉到印章右上角去。 */
.mq.ip-slot-stamp {
  position: absolute;
  left: 50%;
  bottom: -30px;
  width: 88px;
  transform: translateX(-50%);
  z-index: 1;
}

/* 空状态（ticket.js / library.js）：取代原来那个字符角标（❏ ◐ ❐），88px 不抢文案。 */
.mq.ip-slot-empty { width: 88px; margin: 0 auto 6px; }

/* 夜间小结（diary.js）：票票 `night` 档在回看页上岗（S2 当年记账的那第四处）。
   72px 且 `flex: 0 0 auto`：这一块的主角是那两句数字，角色是陪伴位，
   不许在 flex 行里被文案挤扁（不新增动效，night 的眼/嘴/月亮全复用上面已有的那套）。 */
.mq.ip-slot-night { width: 72px; flex: 0 0 auto; }

/* ---------- 降级：停循环、去位移，但表情照常换 ---------- */
@media (prefers-reduced-motion: reduce) {
  .ip-figure,
  .ip-shadow,
  .ip-arm-l .ip-arm-sway,
  .ip-arm-r .ip-arm-sway,
  .ip-eye-open, .ip-eye-wide, .ip-eye-half, .ip-eye-droopy,
  .ip-needle,
  .mq-think .ip-needle,
  .mq-great .ip-prop-stars path,
  .mq-bad .ip-drop-1, .mq-bad .ip-drop-2,
  .mq-tired .ip-prop-zzz circle,
  .mq-think .ip-prop-bubble,
  .mq-sorry .ip-prop-sweat path,
  .mq-bad .ip-prop-rain path:first-child,
  .mq-great .ip-arm-l,
  .mq-great .ip-stamp,
  .mq.mq-pop .mq-stage {
    /* 用 0.01ms 而不是 animation:none —— 后者会连带掐掉别的功能性过渡 */
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
  }
  /* 一次性入场描边直接给终态，别留半截线；dasharray 也要一并清掉，
     否则与 .mq-drawn 的终态不一致，会被逐帧差量成「还在动」。 */
  .mq-draw { animation-duration: .01ms !important; stroke-dashoffset: 0 !important; stroke-dasharray: none !important; }
  /* 雨滴/汗滴/呼噜泡停在可见位置，不要停在 opacity:0 的 keyframe 上 */
  .mq-bad .ip-drop-1, .mq-bad .ip-drop-2,
  .mq-sorry .ip-prop-sweat path { opacity: 1 !important; }
  .mq-tired .ip-prop-zzz circle { opacity: .9 !important; }
  /* 手臂/眼/嘴/腮红的切换 transition 也压掉，改成瞬时换 */
  .ip-arm, .ip-eye, .ip-mouth, .ip-blush ellipse, .ip-prop, .ip-brow {
    transition-duration: .01ms !important;
  }
}
