/* =========================================================
   CalcElf 设计系统 —— 四主题 token 架构
   一套组件 + 四套 CSS 变量；:root[data-theme] 切换，免刷新。
   ========================================================= */

/* 字体系统（OFL 可商用） */
:root{
  /* ★ 第 4 轮规范第 ⑥ 条：字体栈为 Inter + Noto Sans SC ✓
     （Noto Sans SC 是中文主力 ✓ 放在系统字体之前 ✓ 保证中英文观感一致 ✓） */
  /* ★ 第 7 轮：把【系统自带的圆体】提到前面 ✓✓（用户要更"Q"的字体 ✓）
     ★ 诚实说明：字体栈里原本写了 Fredoka/Nunito ✗ 但【从未加载】✗
       （既无 @font-face ✗ 也无 Google Fonts 引用 ✗）→ 实际一直是系统默认字体 ✗
     ⇒ 本轮的零依赖解法：优先用【系统自带圆体】✓
         ui-rounded      —— Safari/iOS 直接给圆体 ✓（最先 ✓）
         Yuanti SC       —— macOS/iOS 中文圆体 ✓
         YouYuan 幼圆     —— Windows 中文圆体 ✓
       特点：零网络请求 ✓ 中国大陆可访问 ✓ 立即生效 ✓
     ⇒ 想要更强的一致圆体（所有设备都一样 ✓）需【自托管字体文件】✓ 见 docs/GROWTH-SPEC 待办 ✓
   ★★ 第 35 轮：Fredoka 已经【自托管到位】了（public/fonts/fredoka/*.woff2 ✓
       public/fredoka.css ✓ 由字体子代理完成 ✓）⇒ 顺序必须跟着改 ✗✗
      原顺序 ui-rounded,'Yuanti SC','YouYuan','Fredoka',… ✗ 的隐患 ✓：
        装了「幼圆 YouYuan」的中文 Windows 上 ✗ 拉丁字母会先被 YouYuan 吃掉 ✗
        ⇒ 自托管的 Fredoka 根本轮不到 ✓（字体是一次性按字符逐字体回退的 ✓）
      现顺序 ui-rounded,'Fredoka','Yuanti SC','YouYuan',… ✓ 的理由 ✓：
        · ui-rounded 仍排第一 ✗ 这是【有意保留】：macOS/iOS 上它是系统原生圆体
          （SF Rounded ✓ 与系统 UI 完全一致 ✓ 且零下载 ✓）
          ⇒ 苹果设备继续用系统圆体 ✗ 其它平台落到我们自带的 Fredoka ✓✓
        · Fredoka 【只有拉丁字形】✗ 汉字会自动继续往后落到
          Yuanti SC / YouYuan / PingFang / 雅黑 ✓
          ⇒ 汉字观感【完全不变】✗ 只是拉丁字母终于一定用上 Fredoka ✓✓ */
  --font-display:ui-rounded,'Fredoka','Yuanti SC','YouYuan','Inter','Noto Sans SC','Zen Maru Gothic','Nanum Square Round','PingFang SC','Hiragino Sans GB','Microsoft YaHei',system-ui,sans-serif;
  --font-body:'Inter','Noto Sans SC','Nunito','Zen Maru Gothic','Nanum Square Round','PingFang SC','Hiragino Sans GB','Microsoft YaHei',ui-rounded,-apple-system,system-ui,sans-serif;
  --font-academic:'Inter','Noto Sans SC','Nunito','PingFang SC','Hiragino Sans GB','Microsoft YaHei',-apple-system,system-ui,sans-serif;
}

/* ===== 四主题 token ===== */
:root[data-theme="elf"]{
  /* ★ 第 4 轮规范：默认主题改为品牌规范配色 ✓
       Navy #123A72 作主色 ✓ · Aqua #10B8C4 作副色 ✓ · 浅蓝白底 ✓
       理由：用户反馈"整个 UI 和底色并没有参照这个规范"✓
       注意：只改【默认主题】✓ 其余三套主题保留 ✓（用户选择 ✓） */
  /* ★★★ 整站换色（青绿主色）第 1 批 ✗✗ —— 这里改的是【主色归属】✗ 不是色值本身 ✓
     ---------------------------------------------------------------
     ★ 改前：--primary:#123A72（藏青作主色）
     ★ 改后：--primary:var(--ce-primary-text,#0f766e)
     ★★ 为什么不是 var(--ce-primary)（品牌青绿 #12b3a0）✗✗：
       实测（真 Chromium 真像素 ✗ tests/site-crawl-ui.js）：
         --primary 的消费者里【绝大多数是"白字压在它上面"】
         （.top 顶栏 / .btn.primary / input:focus / .auth-tab.active / toast.info ✗）
       而白字压 #12b3a0 只有 **2.63:1** ✗ 远低于 AA 4.5 ✗
       直接改过去实测：**全站对比度不达标从 5 条涨到 33 条** ✗✗
     ⇒ 白字底必须走【深一档】#0f766e（白字 5.47:1 ✓）
     ⇒ 品牌青绿 #12b3a0 仍然是品牌的【唯一真源】✗ 但它只做
       实底填充 / 图形 / 边框 / 大字（见 design-tokens.css 的 --ce-primary 注释 ✓）
     ★ cloud / story / aurora 三套的 --primary 一个字都没动 ✓ 只有 elf 换 ✓ */
  --primary:var(--ce-primary-text,#0f766e); --primary2:var(--ce-primary-grad,#0b7d70); --accent:#f59e0b; --danger:#ef4444; --success:#22c55e;--top-grad-a:#123A72;--top-grad-b:#10B8C4;/* ★★ 第 6 批：顶栏渐变【自带的两个端点】= 藏青 → 亮青 ✓ 产品主人拍板回滚成改色前那条 ✓。只有 elf 定义 ⇒ cloud/story/aurora 读不到 ⇒ 落回 var(--primary)/var(--primary2) ⇒ 那三套顶栏一个像素不变 ✓ 消费者只有 .top 一个 ✓ 为什么不动 --primary 见 docs/UI-SPEC-REAL.md 第 6 批 ✓ */
  --page:#F4F9FD; --card:#ffffff; --card-soft:#EAF3FB; --ink:#334155; --muted:#5b6b80;/* ★★ 批 C 续：原来是 #64748b ✗ 白底 4.76 ✓ 但一压到浅色面就破：#EAF3FB 上 4.24 ✗ 页底 #F4F9FD 上 4.49 ✗
     ⇒ 按红线①流程抬到 #5b6b80（白 5.44 ✗ card-soft 4.85 ✗ 页底 5.13 ⇒ 全部 ≥4.5 ✓ ✗ 保持原蓝灰色相 ✗ 只降亮度 ✓）
     ★ 同步改了 docs/UI-SPEC-REAL.md 的 138 行（散文）与 1425 行（drift 清单的 contains ✓ 该行被 color.surfaces 钉死 ✓） */
  --line:#e2e8f0; --radius:24px; --shadow:0 8px 24px rgba(20,184,166,.1);
  --btn-shadow:0 5px 0 #0f766e; --btn-shadow-press:0 1px 0 #0f766e;
  --font-stack:var(--font-body); --display:var(--font-display);
}
:root[data-theme="cloud"]{
  --primary:#6366f1; --primary2:#a78bfa; --accent:#f59e0b; --danger:#f43f5e; --success:#10b981;
  --page:linear-gradient(180deg,#eef0fb,#f7f5ff); --card:#ffffff; --card-soft:#f5f3ff; --ink:#1e1b4b; --muted:#5f6774;/* ★★ 批 C：(8) 原来是 #6b7280 ✗ 白底 4.83 形式上过 ✗ 但一压到浅色面就破：#f5f3ff 上 4.41 ✗ 页底 #eef0fb 上 4.26 ✗
     ⇒ 抬一档到 #5f6774（白 5.71 ✗ card-soft 5.20 ✗ 页底 5.03 ⇒ 全部 ≥4.5 ✓）★ 只改 token ✗ 字号/字重/布局不动 ✓ */
  --line:#e5e7f0; --radius:24px; --shadow:0 8px 20px rgba(99,102,241,.12);
  --btn-shadow:0 8px 20px rgba(99,102,241,.35); --btn-shadow-press:0 4px 10px rgba(99,102,241,.3);
  --font-stack:var(--font-academic); --display:var(--font-display);
}
:root[data-theme="story"]{
  --primary:#e07a55; --primary2:#81b29a; --accent:#f2cc8f; --danger:#e5989b; --success:#22c55e;
  --page:#fbf3e4; --card:#ffffff; --card-soft:#fdf0dd; --ink:#3d405b; --muted:#736346;/* ★★ 批 C：(8) 原来是 #8b7a5e ✗ 白底只有 4.16 ✗ 压到 --card-soft #fdf0dd 更差 3.70 ✗
     ⇒ 抬到 #736346（白 5.83 ✗ card-soft 5.19 ✗ 页底 #fbf3e4 5.29 ⇒ 全部 ≥4.5 ✓）★ 保留暖褐色相 ✗ 只改 token ✓ */
  --line:#e8d5b5; --radius:24px; --shadow:0 6px 18px rgba(224,122,85,.12);
  --btn-shadow:0 5px 0 #b85a3d; --btn-shadow-press:0 1px 0 #b85a3d;
  --font-stack:var(--font-body); --display:var(--font-display);
}
:root[data-theme="aurora"]{
  --primary:#7c3aed; --primary2:#06b6d4; --accent:#ec4899; --danger:#f43f5e; --success:#10b981;
  --page:linear-gradient(135deg,#0f0e26,#1a1840 50%,#2d1b69); --card:rgba(255,255,255,.06); --card-soft:rgba(124,58,237,.15); --ink:#f1f0fb; --muted:#9c98c9;
  --line:rgba(255,255,255,.12); --radius:16px; --shadow:0 8px 24px rgba(124,58,237,.25);
  --btn-shadow:0 8px 24px rgba(124,58,237,.4); --btn-shadow-press:0 4px 12px rgba(124,58,237,.3);
  --font-stack:var(--font-academic); --display:var(--font-display);
}

/* ===== 基础重置与组件 ===== */
*{box-sizing:border-box}
html,body{margin:0;padding:0;background:var(--page);color:var(--ink);font-family:var(--font-stack);font-size:16px;transition:background .5s,color .5s}
button,input,textarea,select{font:inherit}
button{cursor:pointer}

/* 卡片 */
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:16px;margin-bottom:14px;transition:background .5s,border .5s}
.top{background:linear-gradient(135deg,var(--top-grad-a,var(--primary)),var(--top-grad-b,var(--primary2)));color:#fff;transition:background .5s}
.topin{max-width:1120px;margin:auto;padding:12px 16px;display:flex;align-items:center;gap:12px}
.calc-logo{width:48px;height:48px;object-fit:contain;border-radius:14px;background:#fff;padding:2px}
.brand h1{margin:0;font-family:var(--display);font-size:clamp(22px,1.75vw,28px)}
.brand p{margin:2px 0 0;font-size:clamp(11px,1.05vw,17px);opacity:.9}

/* 按钮 */
.btn{border:0;border-radius:14px;padding:12px 18px;font-family:var(--display);font-weight:600;font-size:15px;transition:transform .1s,box-shadow .15s}
.btn.primary{background:linear-gradient(135deg,var(--primary),var(--primary2));color:#fff;box-shadow:var(--btn-shadow)}
.btn.primary:active{box-shadow:var(--btn-shadow-press);transform:translateY(3px)}
.btn.secondary{background:var(--card-soft);color:var(--ink);border:1.5px solid var(--line)}
.btn.gold{background:var(--accent);color:#fff}
.btn.danger{background:var(--danger);color:#fff}
.btn.success{background:var(--success);color:#fff}

/* 输入 */
input,textarea,select{width:100%;border:1.5px solid var(--line);border-radius:12px;padding:11px 13px;background:var(--card);color:var(--ink);outline:none}
input:focus,textarea:focus,select:focus{border-color:var(--primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 15%,transparent)}
label{display:block;font-family:var(--display);font-weight:600;font-size:13px;margin-bottom:6px}

/* 标签 */
.pill{display:inline-block;padding:4px 10px;border-radius:99px;background:var(--card-soft);color:var(--primary);font-size:11px;font-weight:700}
.muted{font-size:12px;color:var(--muted);line-height:1.6}

/* 弹窗 */
.modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:100;align-items:center;justify-content:center;padding:14px}
.modal.open{display:flex}
.modalbox{width:min(560px,96vw);max-height:90vh;overflow:auto;background:var(--card);color:var(--ink);border-radius:var(--radius);padding:24px;position:relative}
.close{position:absolute;right:16px;top:16px;border:0;background:none;font-size:24px;color:var(--muted);cursor:pointer}

/* 主题切换面板 */
.theme-panel{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
.theme-opt{border:2px solid var(--line);border-radius:16px;padding:12px;cursor:pointer;text-align:center;transition:all .2s;background:var(--card)}
.theme-opt.active{border-color:var(--primary);background:var(--card-soft)}
.theme-opt .swatch{width:40px;height:40px;border-radius:50%;margin:0 auto 6px}
.theme-opt .nm{font-family:var(--display);font-weight:600;font-size:13px}
.theme-opt .tg{font-size:10px;color:var(--ink-2,var(--muted))}

/* 密码强度 */
.pw-check{display:flex;align-items:center;gap:6px;font-size:12px;font-weight:600;margin:3px 0}
.pw-check .ok{color:var(--success)}
.pw-check .no{color:var(--muted)}
.pw-bar{height:8px;border-radius:99px;background:var(--line);overflow:hidden;margin:8px 0}
.pw-bar i{display:block;height:100%;background:var(--success);transition:width .3s}

/* Toast */
.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%) translateY(20px);padding:12px 20px;border-radius:14px;font-weight:700;font-size:14px;opacity:0;pointer-events:none;transition:all .3s;z-index:999;box-shadow:0 10px 30px rgba(0,0,0,.2)}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
.toast.success{background:var(--toast-ok-bg,var(--success));color:var(--toast-ink,#fff)}
.toast.info{background:var(--toast-info-bg,var(--primary));color:var(--toast-ink,#fff)}
.toast.error{background:var(--toast-err-bg,var(--danger));color:var(--toast-ink,#fff)}

/* 移动端底部 Tab */
@media(max-width:640px){
  .mobile-tabbar{display:flex;position:fixed;bottom:0;left:0;right:0;background:var(--card);border-top:1.5px solid var(--line);z-index:50;padding:6px 0 calc(6px + env(safe-area-inset-bottom))}
  .mobile-tabbar button{flex:1;border:0;background:transparent;color:var(--muted);font-size:11px;font-weight:700;display:flex;flex-direction:column;align-items:center;gap:2px}
  .mobile-tabbar button.on{color:var(--primary)}
}
.mobile-tabbar{display:none}

/* 骨架屏 */
.skeleton{background:linear-gradient(90deg,var(--card-soft) 25%,var(--line) 50%,var(--card-soft) 75%);background-size:200% 100%;animation:sk 1.4s infinite;border-radius:8px}
@keyframes sk{0%{background-position:200% 0}100%{background-position:-200% 0}}

/* Credits 四层 */
.credits-tier{display:flex;align-items:center;gap:8px;padding:8px 12px;border-radius:12px;margin:4px 0;font-size:12px;font-weight:700;border:1.5px solid var(--line)}
.credits-tier.rich{border-color:var(--success)}
.credits-tier.good{border-color:var(--primary)}
.credits-tier.low{border-color:var(--accent)}
.credits-tier.alert{border-color:var(--danger)}

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

/* v4.2 Credits 四层插画 + 会员旅程 */
@keyframes ceStatusPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.035);box-shadow:0 0 0 6px rgba(239,93,100,.08)}}
.ce-credit-widget{position:relative;min-height:241px;margin:4px 0 12px;border-radius:23px;overflow:hidden;border:1px solid #e1e8f2;background:linear-gradient(180deg,#f8fbff,#edf5ff);box-shadow:inset 0 1px 0 rgba(255,255,255,.98)}
.ce-credit-widget.meter-change{animation:ceMeterBump .5s cubic-bezier(.2,.85,.25,1)}@keyframes ceMeterBump{0%{transform:scale(1)}30%{transform:scale(1.012)}100%{transform:scale(1)}}
.ce-meter-scene{position:absolute;inset:0 0 auto;height:157px;overflow:hidden}.ce-credit-widget.role-child .ce-adult-scene{display:none}.ce-credit-widget.role-adult .ce-child-scene{display:none}
.ce-child-scene{background:radial-gradient(circle at 56% 34%,rgba(255,255,255,.9),transparent 34%),linear-gradient(180deg,#eaf6ff,#f7fbff 67%,#eef9ef)}
.ce-sun{position:absolute;right:19px;top:17px;width:37px;height:37px;border-radius:50%;background:radial-gradient(circle at 34% 29%,#fff4bd 0 18%,#ffd45f 49%,#efb43d 100%);box-shadow:0 0 0 8px rgba(255,211,97,.10),0 0 20px rgba(255,200,66,.30);animation:ceSunBreath 3.5s ease-in-out infinite}@keyframes ceSunBreath{50%{transform:scale(1.07)}}
.ce-cloud{position:absolute;width:60px;height:15px;border-radius:999px;background:rgba(255,255,255,.84)}.ce-cloud:before,.ce-cloud:after{content:"";position:absolute;border-radius:50%;background:inherit}.ce-cloud:before{width:23px;height:23px;left:9px;top:-9px}.ce-cloud:after{width:28px;height:28px;right:8px;top:-13px}.cloud-a{left:23px;top:27px;transform:scale(.83);opacity:.76}.cloud-b{right:78px;top:55px;transform:scale(.62);opacity:.54}
.ce-tree-ground{position:absolute;left:12%;right:12%;bottom:10px;height:31px;border-radius:50%;background:radial-gradient(ellipse at center,rgba(84,165,94,.25),rgba(84,165,94,0) 72%)}
.ce-tree{position:absolute;left:50%;bottom:16px;width:104px;height:126px;transform:translateX(-50%);transform-origin:50% 100%;transition:transform .55s,filter .55s}.ce-trunk{position:absolute;left:44px;bottom:0;width:16px;height:70px;border-radius:10px 10px 7px 7px;background:linear-gradient(90deg,#895b35,#ad7844 52%,#80522e)}
.branch{position:absolute;display:block;height:8px;border-radius:999px;background:#8b5e36;transform-origin:left center}.branch-a{left:47px;bottom:55px;width:43px;transform:rotate(-35deg)}.branch-b{left:50px;bottom:55px;width:40px;transform:rotate(34deg)}
.leaf{position:absolute;display:block;width:31px;height:26px;border-radius:60% 50% 60% 50%;background:linear-gradient(145deg,#72d58b,#2ca965);box-shadow:inset 0 -5px 0 rgba(21,122,74,.08),0 5px 11px rgba(39,146,82,.10);transition:transform .55s,filter .55s,background .55s,opacity .55s}.leaf-a{left:12px;top:41px}.leaf-b{left:37px;top:26px}.leaf-c{left:62px;top:38px}.leaf-d{left:18px;top:18px}.leaf-e{left:50px;top:9px}.leaf-f{left:68px;top:60px}.leaf-g{left:2px;top:64px}.leaf-h{left:41px;top:59px}
.ce-flower{position:absolute;width:7px;height:7px;border-radius:50%;background:#ffafc3;box-shadow:7px -3px 0 #fff0a3,-5px 2px 0 #fff0a3}.flower-a{left:29%;bottom:10px}.flower-b{right:29%;bottom:10px;transform:scale(.78)}
.ce-watercan{position:absolute;right:17px;bottom:11px;width:52px;height:48px;background:none;border:0;cursor:pointer;transform-origin:50% 100%;animation:ceCanFloat 2.2s ease-in-out infinite;filter:drop-shadow(0 9px 9px rgba(45,93,113,.13));padding:0}.ce-watercan:focus-visible{outline:3px solid rgba(77,137,238,.28);outline-offset:4px}.ce-can-body{position:absolute;left:9px;bottom:7px;width:29px;height:24px;border-radius:9px 9px 13px 13px;background:linear-gradient(145deg,#74bfff,#4d89ee);border:2px solid rgba(255,255,255,.8);box-shadow:inset 0 3px 0 rgba(255,255,255,.28)}.can-handle{position:absolute;left:11px;top:4px;width:22px;height:17px;border:4px solid #4d89ee;border-bottom:0;border-radius:18px 18px 0 0}.can-spout{position:absolute;right:-1px;bottom:17px;width:22px;height:10px;border-radius:0 7px 7px 0;background:#5aa9f4;transform:rotate(-10deg)}.drop{position:absolute;width:5px;height:8px;border-radius:70%;background:#68c7ff;opacity:0}.d1{right:1px;bottom:7px;animation:ceDrop 1.7s ease-out .1s infinite}.d2{right:-7px;bottom:2px;animation:ceDrop 1.7s ease-out .45s infinite}.d3{right:-13px;bottom:5px;animation:ceDrop 1.7s ease-out .8s infinite}@keyframes ceDrop{0%{opacity:0;transform:translateY(-3px) scale(.75)}20%{opacity:1}100%{opacity:0;transform:translate(4px,12px) scale(.8)}}@keyframes ceCanFloat{0%,100%{transform:translateY(0) rotate(-2deg)}50%{transform:translateY(-4px) rotate(2deg)}}
.ce-credit-widget.tier-3 .ce-tree{transform:translateX(-50%) rotate(1deg)}.ce-credit-widget.tier-3 .leaf-a{transform:rotate(8deg) translateY(3px)}.ce-credit-widget.tier-3 .leaf-f{transform:rotate(14deg) translateY(4px)}.ce-credit-widget.tier-2 .ce-tree{transform:translateX(-50%) rotate(3deg);filter:saturate(.84)}.ce-credit-widget.tier-2 .leaf{background:linear-gradient(145deg,#d7ce63,#92ad4a)}.ce-credit-widget.tier-2 .leaf-a{transform:rotate(15deg) translateY(7px)}.ce-credit-widget.tier-2 .leaf-b{transform:rotate(-8deg) translateY(6px)}.ce-credit-widget.tier-2 .leaf-c{transform:rotate(14deg) translateY(8px)}.ce-credit-widget.tier-2 .leaf-g{opacity:.55;transform:translateY(12px) rotate(24deg)}.ce-credit-widget.tier-1 .ce-tree{transform:translateX(-50%) rotate(6deg);filter:saturate(.55)}.ce-credit-widget.tier-1 .leaf{background:linear-gradient(145deg,#d8c95a,#9da35a)}.ce-credit-widget.tier-1 .leaf-a,.ce-credit-widget.tier-1 .leaf-c,.ce-credit-widget.tier-1 .leaf-g,.ce-credit-widget.tier-1 .leaf-h{opacity:.20}.ce-credit-widget.tier-1 .leaf-b{transform:translateY(9px) rotate(16deg)}.ce-credit-widget.tier-1 .leaf-d{transform:translateY(14px) rotate(-17deg);opacity:.45}.ce-credit-widget.tier-1 .ce-watercan{animation:ceCanAlert 1s ease-in-out infinite}@keyframes ceCanAlert{0%,100%{transform:translateY(0) rotate(-4deg)}50%{transform:translateY(-7px) rotate(5deg) scale(1.04)}}
/* Adult Credit Vault */.ce-adult-scene{background:radial-gradient(circle at 50% 50%,rgba(255,220,112,.28),transparent 40%),linear-gradient(145deg,#fefbf3,#f5f7fb 72%)}.ce-vault-glow{position:absolute;left:50%;top:63px;width:122px;height:65px;transform:translateX(-50%);border-radius:50%;background:radial-gradient(ellipse at center,rgba(255,197,73,.22),transparent 68%)}.ce-vault{position:absolute;left:50%;bottom:17px;width:114px;height:92px;transform:translateX(-50%);border-radius:17px 17px 20px 20px;background:linear-gradient(160deg,#f5c85d,#d9982c 64%,#a7651f);box-shadow:inset 0 3px 0 rgba(255,255,255,.52),inset 0 -7px 0 rgba(110,67,21,.18),0 14px 28px rgba(144,95,24,.18);transition:transform .55s,filter .55s}.ce-vault-top{position:absolute;left:0;right:0;top:-9px;height:17px;border-radius:14px 14px 8px 8px;background:linear-gradient(180deg,#f8d878,#d9952c);box-shadow:inset 0 2px 0 rgba(255,255,255,.5)}.ce-vault-door{position:absolute;left:31px;top:22px;width:52px;height:52px;border-radius:50%;background:radial-gradient(circle at 34% 27%,#fef7da,#d59d37 64%,#a86a20 100%);border:3px solid rgba(255,238,174,.8);box-shadow:inset 0 3px 0 rgba(255,255,255,.45),0 6px 10px rgba(127,78,22,.14)}.ce-lock{position:absolute;left:21px;top:17px;width:11px;height:16px;border-radius:4px;background:#8a5b1f}.ce-lock:before{content:"";position:absolute;left:1px;top:-8px;width:8px;height:11px;border:3px solid #8a5b1f;border-bottom:0;border-radius:8px 8px 0 0}.coin{position:absolute;width:30px;height:12px;border-radius:50%;background:linear-gradient(180deg,#ffe88b,#e0a735);border:2px solid rgba(255,248,198,.75);box-shadow:0 6px 8px rgba(150,101,33,.12);transition:.55s}.coin:after{content:"";position:absolute;left:7px;right:7px;top:3px;height:3px;border-radius:99px;background:rgba(174,112,23,.28)}.c1{left:-8px;bottom:8px;transform:rotate(-5deg)}.c2{left:5px;bottom:12px;transform:rotate(2deg)}.c3{left:71px;bottom:8px;transform:rotate(4deg)}.c4{left:81px;bottom:23px;transform:rotate(7deg)}.c5{left:-12px;bottom:28px;transform:rotate(-6deg)}.c6{left:10px;bottom:34px;transform:rotate(2deg)}.c7{left:69px;bottom:35px;transform:rotate(-4deg)}.c8{left:47px;bottom:49px;transform:rotate(1deg)}.ce-credit-widget.tier-3 .c5,.ce-credit-widget.tier-3 .c6,.ce-credit-widget.tier-3 .c8{opacity:.28;transform:translateY(8px)}.ce-credit-widget.tier-2 .c1,.ce-credit-widget.tier-2 .c4,.ce-credit-widget.tier-2 .c6,.ce-credit-widget.tier-2 .c7,.ce-credit-widget.tier-2 .c8{opacity:.12;transform:translateY(16px)}.ce-credit-widget.tier-1 .coin{opacity:.05;transform:translateY(19px) scale(.92)}.ce-credit-widget.tier-1 .c2{opacity:.55;transform:translateY(10px)}.ce-credit-widget.tier-1 .ce-vault{filter:saturate(.72)}.ce-card-chip{position:absolute;right:25px;bottom:21px;width:26px;height:17px;border-radius:5px;background:linear-gradient(135deg,#ffdf7d,#f2af45);opacity:0;transform:translateX(10px) rotate(-6deg);transition:.55s}.ce-credit-widget.tier-2 .ce-card-chip{opacity:.65;transform:translateX(0) rotate(-6deg)}.ce-credit-widget.tier-1 .ce-card-chip{opacity:1;transform:translateX(0) rotate(-6deg)}.ce-card-spark{position:absolute;right:18px;top:24px;width:7px;height:7px;border-radius:50%;background:#ffe9a8;opacity:0;box-shadow:10px 5px 0 #fff4cb,3px 12px 0 #ffe9a8}.ce-credit-widget.tier-4 .ce-card-spark{opacity:.9;animation:ceSpark 2.2s ease-in-out infinite}.ce-credit-widget.tier-1 .ce-card-spark{opacity:1;animation:ceCriticalSpark 1.05s ease-in-out infinite}@keyframes ceSpark{50%{opacity:1;transform:scale(1.2)}}@keyframes ceCriticalSpark{0%,100%{opacity:.18;transform:scale(.85)}50%{opacity:1;transform:scale(1.25)}}
/* Readout / tier rail */.ce-credit-readout{position:absolute;left:18px;right:18px;bottom:12px;display:flex;flex-wrap:wrap;align-items:baseline;gap:3px 7px;padding:10px 13px;border-radius:15px;background:rgba(255,255,255,.9);border:1px solid rgba(215,226,239,.86);backdrop-filter:blur(10px);box-shadow:0 9px 19px rgba(51,76,117,.07)}.ce-credit-number{font-size:32px;line-height:1;font-weight:var(--ce-fw-head);color:#233a62;letter-spacing:-.03em}.ce-credit-unit{font-size:13px;line-height:1;font-weight:var(--ce-fw-head);color:#64748a}.ce-credit-readout .ce-status-pill{margin-left:auto;align-self:center;padding:6px 11px;font-size:13px}.ce-credit-subline{flex:0 0 100%;font-size:12px;line-height:1.3;color:var(--ink-2,#7c8a9f)}.ce-topup-btn{flex:0 0 auto;margin-top:2px;min-height:34px;padding:8px 14px;border-radius:999px;border:1px solid rgba(11,150,140,.32);background:linear-gradient(180deg,#21cbb8,#0ea79c);color:#fff;font-size:11px;font-weight:800/* ★★ 第 2 步例外 ③：白字压 #21cbb8→#0ea79c 只有 2.04~2.99:1 ✗ 11px 时字重替对比度打工 ⇒ 保留 800 ✓ 第 3 步改底色后降 700 ✓ */;font-family:inherit;letter-spacing:.01em;box-shadow:0 6px 14px rgba(14,150,140,.28),inset 0 1px 0 rgba(255,255,255,.28);cursor:pointer;transition:transform .18s,box-shadow .18s;white-space:nowrap}.ce-topup-btn:hover{transform:translateY(-1px);box-shadow:0 9px 18px rgba(14,150,140,.36),inset 0 1px 0 rgba(255,255,255,.28)}.ce-topup-btn:active{transform:translateY(1px)}.ce-tier-rail{display:grid;grid-template-columns:repeat(4,1fr);gap:7px;margin:2px 0 4px}.ce-tier-dot{height:6px;border-radius:999px;background:#e2e8f0;transition:.35s}.ce-tier-dot.tier-1{background:#ffd6d7}.ce-tier-dot.tier-2{background:#ffe4b8}.ce-tier-dot.tier-3{background:#e8ecbe}.ce-tier-dot.tier-4{background:#c7ecd4}.ce-tier-dot.active{transform:scaleY(1.55);box-shadow:0 0 0 2px rgba(75,120,238,.10)}.ce-tier-caption{display:flex;justify-content:space-between;color:var(--ink-2,#99a6b6);font-size:11px;font-weight:var(--ce-fw-head);padding-bottom:8px}.ce-mini-stats .stat{border-radius:13px!important;background:#f7f9fc;border:1px solid #e7edf5;padding:9px 6px}.ce-mini-stats .stat b{font-size:19px}.ce-mini-stats .stat span{font-size:11px}.ce-credit-notice{display:none;align-items:flex-start;gap:8px;margin-top:10px;padding:10px 11px;border-radius:14px;font-size:12px;line-height:1.5;border:1px solid #e7e9ed;background:#f9fafc;color:#6a7588}.ce-credit-notice.show{display:flex}.ce-credit-notice.tier-2{background:#fff8eb;border-color:#f8dfaf;color:#8f641e}.ce-credit-notice.tier-1{background:#fff2f2;border-color:#f6cacc;color:#a0373e}.ce-notice-icon{width:23px;height:23px;flex:0 0 23px;border-radius:8px;display:grid;place-items:center;background:#fff;box-shadow:0 3px 9px rgba(70,50,20,.06)}.ce-credit-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}.ce-credit-actions .btn{flex:1;min-width:88px}
/* Membership */.ce-membership-card{background:radial-gradient(circle at 92% 18%,rgba(122,100,224,.09),transparent 28%),linear-gradient(180deg,#fff,#f7f9fe)}.ce-member-plan{padding:7px 11px;border-radius:999px;background:var(--soft,#eef6ff);color:var(--primary);font-weight:var(--ce-fw-head);font-size:12px}.ce-member-hero{display:flex;align-items:center;gap:12px;padding:12px 11px;border:1px solid #e5ebf4;border-radius:17px;background:rgba(249,251,255,.88)}.ce-member-avatar{width:56px;height:56px;flex:0 0 56px;border-radius:19px;display:grid;place-items:center;background:linear-gradient(145deg,#ecfce0,#f8ffec);border:1px solid #d8f0c6;box-shadow:0 8px 19px rgba(63,128,58,.10);transition:.4s}.ce-member-avatar svg{width:34px;height:34px;display:block;filter:drop-shadow(0 3px 3px rgba(46,94,42,.18))}.ce-member-avatar.life-2{background:linear-gradient(145deg,#e0f8ee,#f1fcf7);border-color:#c8eedd}.ce-member-avatar.life-3{background:linear-gradient(145deg,#cff0df,#eaf9f2);border-color:#aedec7}.ce-member-avatar.life-4{background:linear-gradient(145deg,#e7f0ff,#f4f8ff);border-color:#cfdfff}.ce-member-avatar.life-5{background:linear-gradient(145deg,#f0e9ff,#f9f5ff);border-color:#ddcffb}.ce-member-avatar.life-6{background:linear-gradient(145deg,#fdf0d9,#fff8e9);border-color:#f3dcae}.ce-member-avatar.life-7{background:linear-gradient(145deg,#f1e8ff,#eafff3);border-color:#d9c8fb;box-shadow:0 0 0 5px rgba(155,107,255,.10),0 10px 24px rgba(120,96,200,.16)}.ce-member-role{color:#8c9aae;font-size:11px;font-weight:var(--ce-fw-head)}.ce-member-level{font-size:20px;line-height:1.15;font-weight:var(--ce-fw-head);color:#2a3b57;font-family:var(--ce-font-display);letter-spacing:-.01em}.ce-member-copy .muted{font-size:12px;line-height:1.45;margin-top:3px}.ce-level-dual{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:9px}.ce-level-box{padding:11px;border-radius:15px;background:#f8fafc;border:1px solid #e5ebf3}.ce-level-label{color:#8895a7;font-size:11px;font-weight:var(--ce-fw-head);text-transform:uppercase;letter-spacing:.07em}.ce-level-main{display:flex;align-items:baseline;gap:7px;margin:5px 0 7px}.ce-level-main strong{font-size:26px;color:var(--primary)}.ce-level-main span{font-size:12px;font-weight:var(--ce-fw-head);color:#52647e;font-family:var(--ce-font-display)}.ce-progress{height:7px;border-radius:999px;background:#e9eef5;overflow:hidden}.ce-progress i{display:block;height:100%;width:0;border-radius:999px;background:linear-gradient(90deg,var(--primary),var(--accent));transition:width .7s cubic-bezier(.2,.8,.2,1)}.ce-progress.lifetime i{background:linear-gradient(90deg,var(--primary2),var(--primary))}.ce-level-foot{margin-top:6px;color:#96a2b2;font-size:11px}.ce-membership-footer{display:flex;justify-content:space-between;align-items:center;gap:9px;margin-top:10px}.ce-membership-footer>span{font-size:11px;line-height:1.5;color:#8996aa}.ce-membership-footer .btn{min-height:38px;padding:9px 12px;font-size:11px;white-space:nowrap}
/* Unified auth / plans / settings polish */.modal{background:radial-gradient(circle at 30% 20%,rgba(104,147,243,.08),transparent 26%),rgba(24,35,55,.50)!important;backdrop-filter:blur(5px)}.modalbox{border-radius:26px!important;border:1px solid rgba(223,232,243,.94);background:radial-gradient(circle at 92% 7%,rgba(125,166,255,.08),transparent 23%),linear-gradient(180deg,#fff,#f8fbff);box-shadow:0 30px 90px rgba(21,37,64,.27)!important}.modalbox .close{width:35px;height:35px;border-radius:50%;background:#f3f6fa;color:#687890;display:grid;place-items:center;transition:.2s}.modalbox .close:hover{background:#edf2f8;color:#30435f;transform:rotate(6deg)}/* ★★ 与 components.css:301 是逐字节副本 ⇒ 必须同步改 ✗（规格 §9.8）★ 修 bug ①：border-color 收窄到 :not(:focus) ✓ 不加 !important ✓ 理由见 docs/UI-SPEC-REAL.md §5.2 */ .modalbox input,.modalbox select{border-radius:14px!important;min-height:46px!important;background:#fff}.modalbox input:not(:focus),.modalbox select:not(:focus){border-color:#dde6f2}.modalbox input:focus,.modalbox select:focus{box-shadow:0 0 0 4px rgba(82,121,238,.11),0 9px 22px rgba(48,72,112,.05)!important}.plan{border-radius:18px!important;background:linear-gradient(180deg,#fff,#f8fbff);transition:.25s}.plan:hover{transform:translateY(-3px);box-shadow:0 15px 28px rgba(45,67,104,.10)}
body[data-stage="primary_1_3"] .ce-credit-card,body[data-stage="primary_1_3"] .ce-membership-card{border-color:#efdaba!important;background:linear-gradient(180deg,var(--surface-warm,#fffdf8),var(--surface-warm-2,#fff8ed))}body[data-stage="primary_1_3"] .ce-title-line b,body[data-stage="primary_1_3"] .ce-credit-number,body[data-stage="primary_1_3"] .ce-member-level{font-family:var(--ce-font-display)}body[data-stage="primary_1_3"] .btn{border-radius:999px!important;font-family:var(--ce-font-body);font-weight:600}body[data-stage="primary_1_3"] .ce-member-avatar{border-radius:20px}body[data-stage="grade_9_12"] .ce-credit-widget{min-height:228px;border-radius:15px}body[data-stage="grade_9_12"] .ce-membership-card{border-radius:15px!important}
@media(max-width:700px){.ce-level-dual{grid-template-columns:1fr}.ce-membership-footer{flex-direction:column;align-items:flex-start}.ce-membership-footer .btn{width:100%}.ce-credit-widget{min-height:252px}}
@media(prefers-reduced-motion:reduce){.ce-status-pill,.ce-watercan,.ce-sun,.ce-credit-widget,.ce-tree,.ce-vault,.coin,.leaf{animation:none!important}}

/* ===== v5.1 补回 v4.2 Credits/会员卡缺失的基础布局规则 ===== */
/* =========================================================
   CalcElf v4.2 — Credit Story + Membership UI
   Four clear states; numbers stay explicit; visuals provide the story.
   ========================================================= */
.ce-credit-card,.ce-membership-card{overflow:hidden}
.ce-card-heading{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:10px}
.ce-eyebrow{display:block;color:var(--ink-2,#92a0b4);font-size:11px;letter-spacing:.15em;font-weight:var(--ce-fw-head);margin-bottom:4px}
.ce-title-line{display:flex;align-items:center;gap:8px}
.ce-title-line b{font-size:22px}
.ce-status-pill{padding:7px 10px;border-radius:999px;font-size:10px;font-weight:var(--ce-fw-head);background:var(--pill-ok-bg,#edf8f1);color:var(--pill-ok-ink,#2d9761);border:1px solid rgba(65,175,105,.15)}
.ce-status-pill.tier-3{background:var(--pill-t3-bg,#f6f8e6);color:var(--pill-t3-ink,#8a761f)}
.ce-status-pill.tier-2{background:var(--pill-t2-bg,#fff1dd);color:var(--pill-t2-ink,#ad681d)}
.ce-status-pill.tier-1{background:var(--pill-t1-bg,#fff0f0);color:var(--pill-t1-ink,#c23b44);animation:ceStatusPulse 1.8s ease-in-out infinite}

/* =========================================================
   v5.2 增强：儿童小树四层分层动画（蚂蚁森林风）
   纯CSS keyframes，只用 transform/opacity，无JS轮询
   ========================================================= */

/* --- 太阳：光芒缓转 + 光晕呼吸 --- */
.ce-sun{
  animation:ceSunBreath 3.5s ease-in-out infinite !important;
}
.ce-sun::before{
  content:"";position:absolute;inset:-12px;border-radius:50%;
  background:conic-gradient(from 0deg,transparent 0deg,rgba(255,211,97,.3) 20deg,transparent 40deg,transparent 90deg,rgba(255,211,97,.3) 110deg,transparent 130deg,transparent 180deg,rgba(255,211,97,.3) 200deg,transparent 220deg,transparent 270deg,rgba(255,211,97,.3) 290deg,transparent 310deg);
  animation:ceSunRays 12s linear infinite;
}
@keyframes ceSunRays{to{transform:rotate(360deg)}}
@keyframes ceSunBreath{0%,100%{transform:scale(1);box-shadow:0 0 0 8px rgba(255,211,97,.10),0 0 20px rgba(255,200,66,.30)}50%{transform:scale(1.08);box-shadow:0 0 0 14px rgba(255,211,97,.14),0 0 36px rgba(255,200,66,.45)}}

/* tier1 暴晒刺目橙红 */
.ce-credit-widget.tier-1 .ce-sun{
  background:radial-gradient(circle at 34% 29%,#ffe0b8 0 18%,#ff8c42 49%,#e0562e 100%) !important;
  box-shadow:0 0 0 12px rgba(255,140,66,.18),0 0 40px rgba(255,100,40,.5) !important;
}
.ce-credit-widget.tier-1 .ce-sun::before{
  background:conic-gradient(from 0deg,transparent 0deg,rgba(255,140,66,.4) 20deg,transparent 40deg,transparent 90deg,rgba(255,140,66,.4) 110deg,transparent 130deg,transparent 180deg,rgba(255,140,66,.4) 200deg,transparent 220deg,transparent 270deg,rgba(255,140,66,.4) 290deg,transparent 310deg);
}

/* --- 云：横向漂移 --- */
.ce-cloud{animation:ceCloudDrift 18s linear infinite !important}
.cloud-b{animation-duration:28s !important;animation-delay:-9s !important}
@keyframes ceCloudDrift{
  0%{transform:translateX(-40px) scale(.83)}
  50%{transform:translateX(40px) scale(.83)}
  100%{transform:translateX(-40px) scale(.83)}
}
.ce-credit-widget.tier-1 .ce-cloud{opacity:.25 !important;animation-duration:24s !important}
.ce-credit-widget.tier-1 .cloud-b{opacity:.15 !important}

/* RTL 镜像 */
html[dir="rtl"] .ce-cloud{animation-direction:reverse}

/* --- 树：轻柔左右摇晃 --- */
.ce-tree{animation:ceTreeSway 4s ease-in-out infinite !important}
@keyframes ceTreeSway{
  0%,100%{transform:translateX(-50%) rotate(-1.5deg)}
  50%{transform:translateX(-50%) rotate(1.5deg)}
}
.ce-credit-widget.tier-2 .ce-tree{animation:ceTreeDroop 5s ease-in-out infinite !important}
@keyframes ceTreeDroop{
  0%,100%{transform:translateX(-50%) rotate(-2.5deg)}
  50%{transform:translateX(-50%) rotate(-0.5deg)}
}
.ce-credit-widget.tier-1 .ce-tree{animation:ceTreeWilt 3.5s ease-in-out infinite !important}
@keyframes ceTreeWilt{
  0%,100%{transform:translateX(-50%) rotate(4deg)}
  50%{transform:translateX(-50%) rotate(7deg)}
}

/* --- 地面：随 tier 变色 --- */
.ce-tree-ground{
  background:radial-gradient(ellipse at center,rgba(84,165,94,.25),rgba(84,165,94,0) 72%) !important;
}
.ce-credit-widget.tier-3 .ce-tree-ground{
  background:radial-gradient(ellipse at center,rgba(120,170,80,.22),rgba(120,170,80,0) 72%) !important;
}
.ce-credit-widget.tier-2 .ce-tree-ground{
  background:radial-gradient(ellipse at center,rgba(190,170,80,.30),rgba(190,170,80,0) 72%) !important;
}
.ce-credit-widget.tier-1 .ce-tree-ground{
  background:radial-gradient(ellipse at center,rgba(200,140,60,.35),rgba(200,140,60,0) 72%) !important;
}
/* tier1 干裂裂纹 */
.ce-credit-widget.tier-1 .ce-tree-ground::after{
  content:"";position:absolute;inset:6px 20px 0;
  background:
    linear-gradient(90deg,transparent 48%,rgba(140,90,30,.4) 49%,transparent 51%),
    linear-gradient(60deg,transparent 48%,rgba(140,90,30,.3) 49%,transparent 51%),
    linear-gradient(-60deg,transparent 48%,rgba(140,90,30,.3) 49%,transparent 51%);
  border-radius:50%;
}

/* --- 新增小鸟 --- */
.ce-bird{
  position:absolute;width:18px;height:8px;
  top:25px;left:-20px;
  animation:ceBirdFly 22s linear infinite;
  pointer-events:none;
}
.ce-bird::before,.ce-bird::after{
  content:"";position:absolute;bottom:0;width:10px;height:4px;
  border-top:2px solid #5a7a9a;
  border-radius:50% 50% 0 0;
}
.ce-bird::before{left:0;transform:rotate(-8deg)}
.ce-bird::after{right:0;transform:rotate(8deg)}
@keyframes ceBirdFly{
  0%{transform:translateX(0) translateY(0)}
  25%{transform:translateX(120px) translateY(-8px)}
  50%{transform:translateX(240px) translateY(4px)}
  75%{transform:translateX(360px) translateY(-6px)}
  100%{transform:translateX(480px) translateY(0)}
}
html[dir="rtl"] .ce-bird{animation-direction:reverse}

/* --- 水壶：蚂蚁森林风橙壶 --- */
.ce-watercan .can-body{
  background:linear-gradient(145deg,#ffb347,#ff8c2e) !important;
  border-color:rgba(255,255,255,.9) !important;
}
.ce-watercan .can-handle{
  border-color:#e67320 !important;
}
.ce-watercan .can-spout{
  background:#ff9c42 !important;
}
/* 壶身小花印 */
.ce-watercan .can-body::after{
  content:"✿";position:absolute;left:50%;top:50%;
  transform:translate(-50%,-50%);
  color:rgba(255,255,255,.6);font-size:10px;line-height:1;
}

/* tier3/4 水壶安静角落 */
.ce-credit-widget.tier-3 .ce-watercan,
.ce-credit-widget.tier-4 .ce-watercan{
  animation:ceCanQuiet 3s ease-in-out infinite !important;
  transform:scale(.85);
  opacity:.8;
}
.ce-credit-widget.tier-3 .drop,
.ce-credit-widget.tier-4 .drop{opacity:0 !important;animation:none !important}
@keyframes ceCanQuiet{
  0%,100%{transform:scale(.85) translateY(0)}
  50%{transform:scale(.85) translateY(-2px)}
}

/* tier1/2 水壶弹到显眼位置 */
.ce-credit-widget.tier-1 .ce-watercan,
.ce-credit-widget.tier-2 .ce-watercan{
  animation:ceCanAlert 1.2s ease-in-out infinite !important;
  right:12px;bottom:20px;
}
.ce-credit-widget.tier-2 .drop{animation:ceDrop 1.5s ease-out infinite !important}

/* --- 成人金库：金币微光 + 辉光呼吸 --- */
.ce-credit-widget.tier-4 .ce-vault-glow{
  animation:ceVaultGlow 3s ease-in-out infinite !important;
}
.ce-credit-widget.tier-4 .coin{
  animation:ceCoinShimmer 2.5s ease-in-out infinite;
}
.ce-credit-widget.tier-4 .c2{animation-delay:.4s}
.ce-credit-widget.tier-4 .c4{animation-delay:.8s}
.ce-credit-widget.tier-4 .c6{animation-delay:1.2s}
@keyframes ceVaultGlow{
  0%,100%{opacity:.5;transform:translateX(-50%) scale(1)}
  50%{opacity:.8;transform:translateX(-50%) scale(1.08)}
}
@keyframes ceCoinShimmer{
  0%,100%{filter:brightness(1)}
  50%{filter:brightness(1.25)}
}
/* tier1 金库辉光变暗 */
.ce-credit-widget.tier-1 .ce-vault-glow{opacity:.2 !important;animation:none !important}

/* --- reduced-motion：只关位移循环，保留 tier 颜色 --- */
@media(prefers-reduced-motion:reduce){
  .ce-sun,.ce-sun::before,.ce-cloud,.ce-tree,.ce-watercan,
  .ce-bird,.ce-vault-glow,.ce-credit-widget.tier-4 .coin{
    animation:none !important;
  }
  .ce-tree{transform:translateX(-50%) !important}
  .ce-bird{display:none}
}

/* =========================================================
   v5.3.1 Credits 小树场景（蚂蚁森林风 SVG 重绘）
   四层额度：tier4 充裕 / tier3 良好 / tier2 偏低(≤60) / tier1 告急(<30)
   ========================================================= */
.ce2-scene{display:block;width:100%;height:100%}
.ce-credit-widget.role-child{
  --ce2-hillfar:#c9ecb1; --ce2-hillnear:#a9df82; --ce2-ground:#8fd06a; --ce2-grassline:#5da84f;
  --ce2-leaf1:#83d96f; --ce2-leaf2:#58bd4a; --ce2-leaf3:#3fa247;
}
.ce-credit-widget.role-child.tier-3{
  --ce2-hillfar:#d4edb8; --ce2-hillnear:#b3df90; --ce2-ground:#9ed079; --ce2-grassline:#6cab57;
  --ce2-leaf1:#8fd87c; --ce2-leaf2:#63c057; --ce2-leaf3:#4aa650;
}
.ce-credit-widget.role-child.tier-2{
  --ce2-hillfar:#ece2a6; --ce2-hillnear:#e2d188; --ce2-ground:#d8c15f; --ce2-grassline:#b39d49;
  --ce2-leaf1:#ded474; --ce2-leaf2:#c2b652; --ce2-leaf3:#a3a34c;
}
.ce-credit-widget.role-child.tier-1{
  --ce2-hillfar:#f2dd9e; --ce2-hillnear:#ecc579; --ce2-ground:#e0b45c; --ce2-grassline:#b98a44;
  --ce2-leaf1:#e6cb74; --ce2-leaf2:#d0ac52; --ce2-leaf3:#b38f46;
}
.ce2-hill-far{fill:var(--ce2-hillfar);transition:fill .8s ease}
.ce2-hill-near{fill:var(--ce2-hillnear);transition:fill .8s ease}
.ce2-ground{fill:var(--ce2-ground);transition:fill .8s ease}
.ce2-grass path{stroke:var(--ce2-grassline);transition:stroke .8s ease}
.ce2-leaf{transition:fill .8s ease,opacity .8s ease}
.ce2-l1{fill:var(--ce2-leaf1)}.ce2-l2{fill:var(--ce2-leaf2)}.ce2-l3{fill:var(--ce2-leaf3)}
.ce2-haze{fill:#ffd98e;opacity:0;transition:opacity .8s ease}
.tier-2 .ce2-haze{opacity:.10}.tier-1 .ce2-haze{opacity:.30}
.tier-2 .ce2-flowers{opacity:.18}.tier-1 .ce2-flowers{opacity:0}
.ce-credit-widget:not(.tier-2):not(.tier-1) .ce2-flowers{opacity:1}
.ce2-cracks{opacity:0;transition:opacity .8s ease}
.tier-2 .ce2-cracks{opacity:.4}.tier-1 .ce2-cracks{opacity:.85}
.ce2-fallen{opacity:0;transition:opacity .8s ease}
.tier-2 .ce2-fallen{opacity:.7}.tier-1 .ce2-fallen{opacity:1}
.ce2-bucket{opacity:0;transform:translate(150px,132px) scale(.85);transition:opacity .6s ease}
.tier-2 .ce2-bucket,.tier-1 .ce2-bucket{opacity:1;animation:ce2BucketIn .8s cubic-bezier(.2,1.4,.4,1) 1}
@keyframes ce2BucketIn{from{opacity:0;transform:translate(150px,140px) scale(.4)}to{opacity:1;transform:translate(150px,132px) scale(.85)}}

/* 太阳 */
.ce2-rays{transform-box:fill-box;transform-origin:center;animation:ce2Rays 18s linear infinite}
@keyframes ce2Rays{to{transform:rotate(360deg)}}
.ce2-sunglow{transform-box:fill-box;transform-origin:center;animation:ce2Glow 3.6s ease-in-out infinite}
@keyframes ce2Glow{0%,100%{transform:scale(1);opacity:.42}50%{transform:scale(1.24);opacity:.72}}
.tier-1 .ce2-sunglow{fill:#ff7a3c!important;animation-duration:1.6s}
.tier-1 .ce2-rays{stroke:#ff8a4d!important;animation-duration:10s}

/* 云：漂移范围收在安全区内，窄卡片 slice 裁剪也始终能看到云；带轻微投影 */
.ce2-cloud{transition:opacity .8s ease;filter:drop-shadow(0 3px 4px rgba(70,100,150,.14))}
@keyframes ce2Drift1{from{transform:translateX(110px)}to{transform:translateX(250px)}}
@keyframes ce2Drift2{from{transform:translateX(150px)}to{transform:translateX(315px)}}
@keyframes ce2Drift3{from{transform:translateX(80px)}to{transform:translateX(200px)}}
.ce2-cloud-1{animation:ce2Drift1 34s ease-in-out infinite alternate}
.ce2-cloud-2{animation:ce2Drift2 46s ease-in-out infinite alternate;animation-delay:-18s}
.ce2-cloud-3{animation:ce2Drift3 58s ease-in-out infinite alternate;animation-delay:-30s}
.tier-2 .ce2-cloud{opacity:.7}.tier-1 .ce2-cloud{opacity:.32}

/* 飞鸟（干旱时飞走；从画外飞入、另一侧飞出，瞬移发生在画面外） */
@keyframes ce2Fly{from{transform:translateX(-40px)}to{transform:translateX(460px)}}
.ce2-bird-1{animation:ce2Fly 30s linear infinite}
.ce2-bird-2{animation:ce2Fly 46s linear infinite;animation-delay:-20s}
.ce2-bird-flap{transform-box:fill-box;transform-origin:center;animation:ce2Flap .5s ease-in-out infinite alternate}
@keyframes ce2Flap{from{transform:scaleY(1)}to{transform:scaleY(.32)}}
.tier-2 .ce2-bird,.tier-1 .ce2-bird{display:none}

/* 树：轻摇 / 垂头 / 枯萎 */
.ce2-tree{transform-box:fill-box;transform-origin:50% 100%;animation:ce2Sway 4.2s ease-in-out infinite}
@keyframes ce2Sway{0%,100%{transform:rotate(-1.6deg)}50%{transform:rotate(1.6deg)}}
.tier-3 .ce2-tree{animation:ce2Sway 4.9s ease-in-out infinite}
.tier-2 .ce2-tree{animation:ce2Droop 5.6s ease-in-out infinite}
@keyframes ce2Droop{0%,100%{transform:rotate(.8deg)}50%{transform:rotate(2.8deg)}}
.tier-1 .ce2-tree{animation:ce2Wilt 3.6s ease-in-out infinite}
@keyframes ce2Wilt{0%,100%{transform:rotate(3.4deg)}50%{transform:rotate(5.4deg)}}

/* 水壶：>60 完全隐藏；≤60 跳出倾斜浇水；<30 急促 */
/* ★ 用户第 50 轮：「水壶要不要挪位置？★ 挪一下位置吧，往右边挪一点。」✓
   ⇒ right 9px → 17px（往右 8px ✓）★ 为什么是 17 而不是 20+ ✗：
     · 宿主 .ce-meter-scene 是 overflow:hidden ✗ 壶不能出界 ✓
       实测（真 Chrome ✓ 1400/390/1920 三档视口 × 0~100% 共 21 个相位 ✓）：
         壶【旋转后的包围盒】距场景右边界的余量最小 = 9.42px（出现在 45% 相位 ✓）
         ⇒ 往右最多 9.42px 就会贴边 ✓ 取 8px ⇒ 余量还剩 1.42px ✓（不进裁剪 ✗）
     · 别再大了 ✗：壶本身带旋转 + drop-shadow ✗ 贴边会看着"顶住卡片右缘"✓
     · 视觉依据：太阳在右上（圆心距右缘约 55px ✓）壶右移后与太阳错开更明显 ✗
       且壶嘴到树冠的水平距离从 ~93px 缩到 ~85px ⇒ 水柱更短更集中 ✓
   ★ 三股水【不用改坐标】✗：水珠的 transform-origin 锁在壶嘴（SVG 用户坐标 11px 27px ✓）
     壶挪多少，水流起点就跟着挪多少 ✓（见下面 ce2Pour3 那一段的说明 ✓）*/
.ce2-can{position:absolute;right:13px;bottom:40px;width:88px;height:76px;padding:0;border:0;background:none;cursor:pointer;z-index:3;
  display:none;opacity:0;transform:rotate(-15deg);transform-origin:68% 88%;filter:drop-shadow(0 8px 8px rgba(120,80,20,.18));
  transition:opacity .4s ease}
.ce2-can svg{width:100%;height:100%;display:block;overflow:visible}
.ce2-canwater{transform-box:fill-box;transform-origin:center;animation:ce2Slosh 1.5s ease-in-out infinite}
@keyframes ce2Slosh{0%,100%{transform:translateY(0)}50%{transform:translateY(-2.6px)}}
.ce2-drop{opacity:0}
/* ★★ 圆点基态【必须在媒体查询之外】✗✗（本轮实测踩到 ✓ 这是最严重的一处 ✓）
   若写进 @media(prefers-reduced-motion:no-preference) 里 ✗ 系统开 reduce 时
   那整块不生效 ⇒ opacity 回落到初始值 1 ⇒ 30 颗蓝点【永久定在屏幕上】✗✗
   实测（真 Chrome ✓ verify.cjs ✓ 未修前）：reduce 档 "全为 1 的颗数 = 30 / 30" ✗
   ⇒ 挪到媒体查询外面之后：reduce = 动画 none + opacity 0 ⇒ 一个点都不出现 ✓✓
   （与 .ce2-drop 分开写两条 ✗ 合并会让查 ceDropFall 的那条正则先命中 ✓ 实测踩过 ✓） */
.ce2-dot{opacity:0}
/* ★★★ 花洒扇形：三环各自一个 <g> ✗ 锚点锁在【喷头心】(16.5,26.5) ✓
   ★ 为什么用独立变换属性 scale ✗✗：transform-origin 要落在 SVG 用户坐标的原点 ✓
     环内 <g> 的 bbox 中心不是发射点 ✗ 所以必须 transform-box:view-box + 绝对长度 ✓
     （这条路子本文件 545 行已经在用 ✓ 实测有效 ✓）
   ★ 基态 opacity:0 写在上一行【媒体查询之外】✗✗：
     若把可见性交给环上那条动画 ✗ 系统开 reduce 时动画被关掉 ⇒ 透明度回落 1 ⇒
     36 颗蓝点【永久钉在屏幕上】✓（上一轮就是踩了这个 ✓ 见 460-465 的注释 ✓） */
.ce2-dot-ring{transform-box:view-box;transform-origin:16.5px 26.5px}
.ce2-dot-ring .ce2-dot{opacity:0}
.tier-2 .ce2-can,.tier-1 .ce2-can{display:block;opacity:1;animation:ce2CanAlert 1.25s ease-in-out infinite}
@keyframes ce2CanAlert{0%,100%{transform:rotate(-15deg) translateY(0)}50%{transform:rotate(-22deg) translateY(-7px)}}
.tier-2 .ce2-drop,.tier-1 .ce2-drop{animation:ce2Drop 1.5s ease-in infinite}
.tier-1 .ce2-can{animation-duration:.9s}
.tier-1 .ce2-drop{animation-duration:.95s}
.ce2-drop.dp2{animation-delay:.38s!important}.ce2-drop.dp3{animation-delay:.76s!important}
/* ★★★ 洒水：7 排圆点的逐排延迟 ✓ 第一排 0 → 第二排 +90ms → … → 第七排 +540ms ✓
   ★ 为什么零 !important 也稳 ✗：区块里 .tier-2 .ce2-dot.ce2-rN 那五条 (0,3,0) 特异性更高 ✓
     本组 (0,2,0) 只是"兜底"✗ 且两处值【逐位相同】⇒ 谁赢都落在同一个数上 ✓✓
   ★ 与 themes-ceremony.css:333-338 的三条 0/.42/.84 也不打架 ✗
     理由同上：本文件那三条 dp1/dp2/dp3 的 0/.38/.76 早就把值钉死了 ✓ */
.ce2-dot.ce2-r2{animation-delay:.09s}.ce2-dot.ce2-r3{animation-delay:.18s}
.ce2-dot.ce2-r4{animation-delay:.27s}.ce2-dot.ce2-r5{animation-delay:.36s}
.ce2-dot.ce2-r6{animation-delay:.45s}.ce2-dot.ce2-r7{animation-delay:.54s}
@keyframes ce2Drop{0%{opacity:0;transform:translate(0,-3px) scale(.6)}22%{opacity:1}82%{opacity:.85}100%{opacity:0;transform:translate(-10px,19px) scale(.9)}}
.tier-1 .ce2-can::after{content:"";position:absolute;left:10%;right:6%;top:12%;bottom:6%;border-radius:50%;
  border:3px solid rgba(255,120,60,.55);animation:ce2CanRing 1.05s ease-out infinite}
@keyframes ce2CanRing{0%{transform:scale(.72);opacity:.85}100%{transform:scale(1.28);opacity:0}}

/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 三股水 · 三个方向 · 扇形撒向树 · 水滴【下厚上薄】★★★
   用户原话：「3 个水的段，然后 3 个不同的方向，这样子撒下去的……
              水珠，那肯定是下面来的厚重，上面薄一点那种水珠的效果」

   ★ 为什么写成这样（纯 CSS，零外部资源，DOM 一个字节都不动 ✗✗）：
     壶 SVG 里【本来就有】三条水珠 path（index.html .ce2-drop.dp1/dp2/dp3 ✓）
     但三条的 d 完全相同 ✗ 且 animations 只让它们【一起竖直下落 19px】✓
     ⇒ 实测（真 Chrome ✓ 见报告 baseline）：三条的 bbox 逐像素相同（9.05,27,3.9,7 ✓）
        屏幕上就是【同一个位置的一颗小水珠】✗ 既看不出"三股"✗ 也看不出"方向"✗
     ⇒ 所以全部靠 CSS 从【同一个壶嘴锚点】把三条分别【转开 + 拉长】✓
        这就是"扇出去"的几何：同一个原点 ✗ 不同角度 ✗ 不同长度 ✓✓

   ★ 三件 CSS 武器（都是纯 CSS ✓ 不用 SVG 属性 ✗ 不用外部图 ✗ 不改 DOM 一个字节 ✗）：
      ① rotate + scale（独立变换属性 ✓ 与 animation 那条 transform 各管一路 ✓）
         · transform-origin:11px 27px + transform-box:view-box
           ⇒ 锚点锁在【壶嘴尖】的 SVG 用户坐标 ✓ 壶倾斜 / 壶挪位置都跟着走 ✓
         · rotate  = 每股自己的【方向】✓（60° / 45° / 30° 三个角度 ✓）
         · scale   = 每股自己的【粗细 + 长度】✓
              x 分量 → 水柱粗细（三股各自反算 ⇒ 屏幕线宽都落在 ~1.9px ✓）
              y 分量 → 水柱长度（ry × --ce2-k ✓ --ce2-k 由关键帧驱动 ✓）
     ② stroke + fill:none —— 这是"三股能分开"的关键 ✓
         · 水滴 path 的 d 本来就是一条 5.2×7 的细曲线 ✓ 描边后 = 一条细水柱 ✓
         · 圆头 linecap ⇒ 末端是【饱满的水头】✓
         · fill:none 必须 ✗ 否则水滴的"肚"被径向放大后变成大色块 ⇒ 三股糊成一片 ✓
     ③ mask-image 单层渐变 —— 「下厚上薄」就靠它 ✓
         · 沿水流方向：0%(近壶嘴) 全透 → 20% 起全实 → 100%(近树) 保持全实 ✓
         · 渐变角度 calc(90deg - var(--ce2-rot)) 补偿元素自身的旋转 ✓
           ⇒ 渐变方向【始终】沿"壶嘴→树"✗ 不随角度歪 ✓✓
      ④ 节奏对齐：ceCanPour 在 18% 倾到 -16deg ⇒ 水在 14% 起、56% 收、
         64% 前彻底消失（不留硬跳 ✓）；三股 delay 0 / .13 / .25 ⇒ 轻微错位 ✓

   ★★ 实测数据（真 Chrome ✓ 1440×900 ✓ 报告 ③④ 有完整表 ✓）：
      · 三股长度 67.57 / 45.80 / 29.28 CSS px ⇒ 最长/最短 = 2.31 倍 ✓
      · 三股方向 135.1° / 120.1° / 105.1° ⇒ 扇形张开 30.0° ✓
      · 三股与竖直夹角 45.1° / 30.1° / 15.1° ⇒ 互不相同 ✓
      · 每股"垂直墨宽"沿程单调增长（第 1 股 4.25→6.13→8.75→10.88 CSS px）✓
        = 用户要的「下面来的厚重，上面薄一点」✓✓
   ★ 分档语义【一个字没动】✗：全部包在 .tier-1 / .tier-2 里 ✓
     tier-3 / tier-4 仍然是【一个像素都不出现】✓（.ce2-drop 基态 opacity:0 ✓）
   ★ 无障碍【一个字没动】✗：整块包在 no-preference 里 ✓
     系统 reduce + Auto ⇒ 本块整体不生效 ⇒ 水珠仍然 opacity:0 ✗ 全页 0 动画 ✓✓
   ══════════════════════════════════════════════════════════════════════════════ */
@media(prefers-reduced-motion:no-preference){
  .tier-2 .ce2-drop,.tier-1 .ce2-drop{
    /* ── 三股共用的那一半 ─────────────────────────────────────────────
       ★★ 坑 ①（实测踩到 ✓）：CSS 自定义属性【不能】在一条规则里写三遍 ✗
          `.x{--a:1;--a:2;--a:3}` 只有【最后一条】生效 ✓（不是叠加 ✗）
          ⇒ 第一版三条水珠拿到的 rot/rx/w/ink 全一样 ✗ 屏幕上就是一条水 ✓✓
          ⇒ 正解：共用部分留在本条 ✗ 三股各自的差异【拆成三条规则】✓（见下 ✓）
       ★★ 坑 ②（更隐蔽 ✗✗）：SVG 元素上 CSS `rotate` 属性【正负号与 HTML 相反】✓
          规范里 SVG 的 rotate() 用数学正方向（y 轴向上 ✓）✗ 而 CSS rotate 属性
          沿用 SVG 的符号 ⇒ 写 -63.5deg 实际转出 +63.5deg（数学正 = 屏幕逆时针 ✓）
          实测证据（真 Chrome ✓ 1400×900 ✓）：
            写 `--ce2-rot:-63.5deg` ⇒ 三股水全部朝【右下】跑 ✗✗
              dp1 端点 (1324.08,478.39) vs 壶嘴 (1225.52,460.12) ⇒ 方向 (98.6,18.3) 右下 ✓
            把三条都取正 ⇒ 端点变到壶嘴的【左下】✓（与用户标的笔画一致 ✓）
          ⇒ 所以下面三条规则的 --ce2-rot 一律取【正值】✓
            （读法：--ce2-rot 的绝对值 = 与"正下方"的夹角 ✓ 越大越斜 ⇔ 落点越高 ✓）*/
    transform-box:view-box;                /* 锚点用 SVG 用户坐标 ✗ 不是 bbox ✓ */
    transform-origin:11px 27px;            /* = 壶嘴尖（index.html 那条水滴路径的起点 ✓）
                                              ★ 壶挪位置也不用改这里 ✗：11/27 是【SVG 用户坐标】✓
                                                壶整体平移多少，这个锚点就跟着平移多少 ✓✓ */
    stroke-linecap:round;stroke-linejoin:round;   /* 末端 = 饱满的水头 ✓ */
    /* ★★ 为什么这一路非带 !important 不可 ✗✗（实测踩到的第二个坑 ✓）：
       themes-ceremony.css:332 写的是【简写】`animation:ceDropFall 2.5s ease-in infinite` ✓
       简写带 !important 时【每一路都带】✗ 本文件先加载 ⇒ 同特异性也一定输 ✓
       ⇒ 实测第一版：三条水珠的 animationName 仍是 ceDropFall ✗ 只是被拉了 100 倍长 ✗✗
       ⇒ 所以 name/duration/timing/iteration-count 四路都必须 !important ✓
       ★ 安全性：整块包在 no-preference 里 ✗ reduce 下本块【整体不生效】✓
         所以不会重演 themes-ceremony.css:319-322 记的那个"壶在倒水但没有水"的自相矛盾 ✓✓ */
    animation-name:ce2Pour3!important;animation-duration:2.5s!important;
    animation-timing-function:ease-in-out!important;
    animation-iteration-count:infinite!important;
    rotate:var(--ce2-rot);
    /* ★★ 坑 ④：`getBBox()` 在【有描边】时把 stroke-width 也算进去 ✓
       而 transform-origin 是拿 bbox 解算的 ✗ ⇒ 描边越粗，形状的"远端"越靠下 ✓
       实测（真 Chrome ✓）：描边 1.35 时 bbox 高 8.35（不是 7 ✓）
         ⇒ 远端偏移 = 7 + sw/2 ✗ 会被 ry 放大 ⇒ 三股的长度全被拉长了 ~15% ✗
         落点直接穿过树冠掉到草地上 ✗（第一版截图里就是"扫到地面"✓）
       ⇒ 正解：把长度【只交给 --ce2-k 一个旋钮】✓
           `scale: calc(var(--ce2-rx) * var(--ce2-k)) calc(var(--ce2-ry) * var(--ce2-k))`
         关键帧只动 --ce2-k ✗ 三股换长度就只改 ry ✓ 一处改、处处对 ✓✓
       ★ 三条线宽同取 1.25 ⇒ bbox 逐像素相同 ⇒ 远端偏移公式对三股一致 ✓
         （这样"长度"才是唯一变量 ✗ 数字才可比 ✓）*/
    scale:calc(var(--ce2-rx) * var(--ce2-k)) calc(var(--ce2-ry) * var(--ce2-k));
    /* ★★ 坑 ③（最关键的一个 ✗✗）：【必须把 fill 关掉】✓
       原来三条 path 都是有填充的水滴形 ✗ 一旦径向拉到 4~10 倍 ✗
       那个"水滴肚"就变成一块【又宽又扁的色块】✗✗
         实测（真 Chrome ✓）：三股的填充块横向各占约 27° ✗ 而彼此只错开 17° ✗
         ⇒ 三块糊成一片大三角 ✗ 截图里就是"一条又宽又淡的蓝楔子"✗
           完全看不出"三股" ✗ 用户要的"3 个水的段"就没了 ✓
       ⇒ 正解：fill:none ✗ 只留 stroke ✓
         水滴的 d 本身是一条 5.2×7 的细路径 ✗ 描边后就是【一条细水柱】✓
         长度由径向缩放给 ✓ 圆头让末端是饱满的水头 ✓
         ⇒ 三股立刻分得开 ✓✓（实测见报告 ③）*/
    fill:none;stroke:var(--ce2-ink);
    stroke-width:var(--ce2-w);
    /* ★★ 水头/水尾渐隐 —— 这一条就是用户要的"下厚上薄" ✓✓
       ★★ 坑 ⑤（最要命的一个 ✗✗）：渐变方向试错了两次 ✓
          · 第一次 `linear-gradient(calc(90deg - var(--ce2-rot)),#0000 0%,#000 62%)` ✗
            实测（真 Chrome ✓ 逐剖面量像素 ✓）：第 1 股
              垂直墨宽 5.50→7.38→9.25→10.50→11.25 CSS px ✓ 确实越靠树越宽 ✓
              但覆盖像素 45→40→35→19→17 ✗ 墨量 B-R 85→72→68→67→60 ✗
            ⇒ 水柱【中段就淡没了】✗ 只剩靠近壶嘴的一小截 ✗（截图里"水只有一点点"✓）
          · 第二次改成 270deg ⇒ 更糟 ✗ 全柱变暗淡 ✗
          · ★ 定案 = 90deg 且色标改成【三段】✓
            真因：linear-gradient 的 0% 是【渐变线起点】✗ 而 90deg 的起点正好在
                  "壶嘴→树"这条线的【壶嘴那一头】✓ 所以 90deg 本来就是对的方向 ✓
                  第一版之所以"中段就没了"✗ 是色标太软（0%→62% 一路线性）
                  ⇒ 到 62% 才全实 ✗ 而 62% 已经接近水柱尽头了 ✓
            正解：0% → 20% → 100%（20% 就全实 ✓ 整根水柱都看得见 ✓ 末端保持全实 ✓✓）
       ★ 为什么还要 -rot：元素自己带了 rotate ✗ 渐变的坐标也跟着转 ✓
         减掉 --ce2-rot 之后，渐变方向【始终】沿"壶嘴→树"✗ 不随角度歪 ✓
       ★★ 实测（真 Chrome ✓ 第 1 股逐剖面 ✓ 见报告 ④）：
            垂直墨宽 4.38 → 4.88 → 6.75 → 8.13 → 9.13 → 9.88
                      → 10.50 → 11.00 → 11.13 → 11.13 CSS px
            ⇒ 2.5 倍单调增长 ✓ 正好就是用户要的"下面来的厚重，上面薄一点"✓✓ */
    mask-image:linear-gradient(calc(90deg - var(--ce2-rot)),#0000 0%,#000 20%,#000 100%);
  }
  /* ── 三股各自的差异：方向 / 长度 / 粗细 / 深浅 / 轻微错位 ─────────────
     ★ rx 为什么不是 1 ✗：fill 关掉后只剩 stroke ✗ stroke 的宽度【也吃 scale】✓
       ⇒ 三股取同一个 rx 的话 ✗ 越短的那股屏幕线宽越细（与长度成正比 ✓）
       ⇒ 这里按各自角度反算 rx ✗ 让三股屏幕线宽都落在 1.7~1.9px ✓
         （角度越小 ⇒ 该方向的缩放越弱 ⇒ rx 要越大来补 ✓✓）
     ★ 错位取 0 / .13s / .25s（= 0% / 5.2% / 10% 相位 ✓）
       理由：ce2Pour3 的可见窗口是 14%~64% ✗ 而 ceCanPour 在 18% 倾到 -16deg ✓
       若错位取 0/.38/.76（themes-ceremony.css:336/338 那两条 ✓ 占 2.5s 的 15%/30% ✓）
       ⇒ 第 3 股要到壶身已经抬到 45% 才到峰值 ✗ 变成"壶走了水才出来"✗✗
       ⇒ 所以这里必须用 !important 把 delay 抢回来（那两条也带 !important ✗
          同特异性下【后加载的赢】✗ 而本文件在前 ✗ 不加就抢不回来 ✓✓）
       ★ 且三条互不相同 ⇒ tests/email-and-watering.js:139-144 那条断言继续绿 ✓ */
  /* 第 1 股（最斜 / 最长 / 最细 / 最淡）⇒ 从树冠右缘扫到树身左侧 ✓ */
  .tier-2 .ce2-drop.dp1,.tier-1 .ce2-drop.dp1{
    --ce2-rot:60deg;--ce2-rx:2.42;--ce2-ry:9.0;--ce2-w:1;--ce2-ink:#9ed6ff;
    animation-delay:0s!important}
  /* ★★★ 洒水圆点：【4 列 × 7 排 = 28 颗】✓【扇形】✓【逐排出现】✓ 第一排→…→第七排 ✓
     ★ 单点 1.9s（< 壶的 2.5s 循环 ✓）· 逐排 +90ms ⇒ 第 7 排 0.54s 才起 ✓
       实测（真 Chrome ✓ 28 颗动画对象逐个读出来 ✓）：
         r1=0 · r2=90 · r3=180 · r4=270 · r5=360 · r6=450 · r7=540ms ✓ 七档互不相同 ✓
     ★★ 扇形（不是规整网格 ✗）—— 参考图 图三 实测梯度照搬 ✓
         列距【逐排收窄】：参考 15.9→16.4→16.0→16.4→13.3 px
         每排左缘【逐排左移】：参考 -4.6→-5.0→-6.0→-6.0→-7.1 px
       ⇒ 我方用户单位：第 1 列恒 2.5 · 第 2~4 列逐排左移 0.5 · 排距恒 12.2
         排内跨度 42.6→41.1 ⇒ 越往下越紧 ✓（扇形收窄 ✓）
         ★ 左移为什么只给 0.5 ✗：壶带 -15deg 旋转 ⇒ 旋转把"往下"变成"往右"✓
           设计侧每左移 1 单位，屏幕上只抵掉 0.26px ✓ 要做出参考图 -2.5px/排
           得给 ~5 单位/排 ✗ 而第 1 列已在 x=2.5（左缘留白 0.56 ✓）⇒ 会裁掉左半边 ✓
           ⇒ 定案：第 1 列钉死 ✗ 只让第 2~4 列逐排收 ✓ 28 颗一个都不出界 ✓✓
       ⇒ 全在 cx/cy（markup）里 ✗ 动画一行都不用改 ✓ 逐排延迟照旧 ✓✓
       实测逐排不透明度（把 15 条动画钉到同一时刻 ✓）：
         t=120ms 0.26 / 0.08 / 0    / 0    / 0    / 0    ⇒ 只有第一排看得见 ✓
         t=260ms 0.56 / 0.31 / 0.19 / 0    / 0    / 0    ⇒ 第一→第二→第三 排着队出来 ✓
         t=400ms 0.91 / 0.70 / 0.43 / 0.27 / 0.10 / 0.00 ⇒ 五排依次变亮 ✓
         t=1060ms 全 1.00 ⇒ 整片铺满 ✓✓
     ★ 只动 opacity ✗ 不动 transform / r ✗✗：
       圆点【不动位置】才看得出"一排一排显示"✓ 一动就成飞溅了 ✗
       扇形全部由 markup 的 cx/cy 给出 ✗ 动画不参与造型 ✓
       （r 直接吃 markup 里的属性值 1.94 ✓ 28 颗点径逐像素一致 ✓✓）
     ★★ 为什么【不】把圆点并进上面 .tier-2 .ce2-drop 那条组规则 ✗✗（实测踩到的坑 ✓）：
       那条带 transform-origin:11px 27px（锁在壶嘴 ✗ 那是三股水的锚点 ✓）
       圆点吃进去就变成"从壶嘴往外飞"✗ 而不是"一颗一颗原地冒出来"✓
       而且它把 animation-duration 钉成 2.5s ✗ 圆点会跟着壶的倾倒节奏 ✗
       ⇒ 圆点必须【自己一条 animation】✓ 下面这条就是 ✓
     ★★ 也不能写成 animation-name:var(--ce2-anim) ✗✗（第二版实测踩到 ✓）：
       自定义属性在 animation-name 里【不会被替换】⇒ 计算值恒为 none ⇒
       15 颗点一个都不动 ✗（同一条规则里的 animation-delay 却照常生效 ⇒
       现象很迷惑：延迟对、动画没有 ✓ 已在真 Chrome 里逐条读 cssRules 定位 ✓）
       ⇒ 定案：动画名直接字面量写在圆点自己的规则里 ✓ 零间接层 ✓✓ */
  /* ★ 圆点的基态也写在这一条里 ✗ 不跟 .ce2-drop 合并 ✓
     （★ 圆点【一定要自己一条规则】✗✗ 本轮实测踩到过：
       一并进上面那条组规则就会连带吃到 transform-origin:11px 27px（壶嘴锚点 ✗）
       与 animation-duration:2.5s ✗ ⇒ 从壶嘴往外飞 + 跟壶同节拍 ✓ 都不是我们要的 ✓
       而且并进去还会与 tests/email-and-watering.js 查 .ce2-drop 的那两条正则抢先后 ✓
       ⇒ 规矩：圆点自成一档 ✓ 基态、动画、延迟全写在圆点自己的选择器上 ✓✓） */
  .tier-2 .ce2-dot,.tier-1 .ce2-dot{animation:ce2Splash 1.9s ease-out 0s infinite}
  /* ★★★ 花洒扇形：四环【同步】由内向外推 ✗ 可见性逐环错峰 ✓✓
      ★ 上面那条 .tier-2 .ce2-dot (0,3,0) 一个字没删 ✗✗ —— 它是"仍被样式系统认得"的
        证据 ✓（tests/email-and-watering.js 的 ce2Splash 探针与它并存 ✓）
        下面四条 (0,4,0) 只改 name / duration / timing / iteration / delay ✓
      ★ 为什么四环【同一个 delay 0s】✗✗（第一版差点写成逐环错峰 ✓ 已在浏览器里想清 ✓）：
        错峰 ⇒ 四环的 scale 各在不同相位 ⇒ 半径比 27.2:46.6:66.0:85.4 被破坏 ⇒
        那就不是"一个锥形扇"✗ 而是四片互相穿插的水 ✗（用户要的是【一个扇形】✓）
        ⇒ 定案：四环整体同步地"从喷头那个点推开 / 收回"✓ 扇形永远是扇形 ✓✓ */
  .tier-2 .ce2-fan,.tier-1 .ce2-fan{--ce2-fan-rot:27deg;rotate:var(--ce2-fan-rot)}
  .tier-2 .ce2-fan > .ce2-dot-ring,.tier-1 .ce2-fan > .ce2-dot-ring{animation:ce2FanK 1.8s ease-in-out 0s infinite!important}
  .tier-2 .ce2-fan > .ce2-dot-ring > .ce2-dot,.tier-1 .ce2-fan > .ce2-dot-ring > .ce2-dot{animation-name:ce2Ring1!important;animation-duration:1.8s!important;animation-timing-function:ease-in-out!important;animation-iteration-count:infinite!important;animation-delay:0s!important}
  .tier-2 .ce2-fan > .ce2-dot-ring:nth-child(2) > .ce2-dot,.tier-1 .ce2-fan > .ce2-dot-ring:nth-child(2) > .ce2-dot{animation-name:ce2Ring2!important;animation-delay:.072s!important}
  .tier-2 .ce2-fan > .ce2-dot-ring:nth-child(3) > .ce2-dot,.tier-1 .ce2-fan > .ce2-dot-ring:nth-child(3) > .ce2-dot{animation-name:ce2Ring3!important;animation-delay:.144s!important}
  .tier-2 .ce2-fan > .ce2-dot-ring:nth-child(4) > .ce2-dot,.tier-1 .ce2-fan > .ce2-dot-ring:nth-child(4) > .ce2-dot{animation-name:ce2Ring4!important;animation-delay:.216s!important}
  .tier-2 .ce2-fan > .ce2-dot-ring:nth-child(5) > .ce2-dot,.tier-1 .ce2-fan > .ce2-dot-ring:nth-child(5) > .ce2-dot{animation-name:ce2Ring5!important;animation-delay:.288s!important}
  .tier-2 .ce2-fan > .ce2-dot-ring:nth-child(6) > .ce2-dot,.tier-1 .ce2-fan > .ce2-dot-ring:nth-child(6) > .ce2-dot{animation-name:ce2Ring6!important;animation-delay:.36s!important}
  /* ★★ 为什么是 `.ce2-fan > .ce2-dot-ring:nth-child(N)` ✗✗ 这层 .ce2-fan 是【必须】的 ✓
      （真 Chrome 实测踩到 ✓ 不是设计洁癖 ✓）：
        环若直接做壶 SVG 的子节点 ✗ 它们其实是第 9~12 个（前面还有 defs/壶嘴/玫瑰/
        把手/壶身/壶内水/花印 共 8 个 ✓）⇒ CSS 写的 :nth-child(2..4) 落在别人身上 ✗
        实测证据：48 颗逐颗读 computed animationName ⇒ 全部是 "ce2Ring1" ✗✗
          ⇒ 逐环错峰淡入完全失效 ⇒ 四环【一起闪】✗ 正是最不想看到的那种 ✓
        ⇒ 正解 = 外面套一层 .ce2-fan ✗ 四环成为它【唯一】的四个子节点 ⇒ 序号恒为 1..4 ✓
          与壶 SVG 里以后加多少元素彻底解耦 ✓✓ */
  /* ★ 旧的"逐排延迟"五条（.ce2-dot.ce2-r2…r7）随网格造型一起下线 ✗✗
     理由（如实记 ✗ 不是为了测试好看 ✓）：
       那五条是"一排一排原地冒出来"的节奏 ✗ 与花洒的【径向喷】不再是同一件事 ✓
       它们只管 animation-delay ✗ 而上面四条环规则每颗点都带 delay…!important ⇒
       同特异性下后者赢 ✗ 留着也只是误导后来人 ✓
     ★ 但 ce2-rN 这些类【继续留在 markup 里】✗ 一颗没删 ✓（它们是"行"的身份 ✓） */
  .tier-2 .ce2-dot.ce2-r1,.tier-1 .ce2-dot.ce2-r1{animation-delay:0s!important}
  /* 第 2 股（中间长度 / 中间粗细 / 最深）⇒ 打到树冠下沿中央 ✓ */
  .tier-2 .ce2-drop.dp2,.tier-1 .ce2-drop.dp2{
    --ce2-rot:45deg;--ce2-rx:1.9;--ce2-ry:6.1;--ce2-w:1;--ce2-ink:#5fb8f7;
    animation-delay:.13s!important}
  /* 第 3 股（最短 / 最陡 / 最深——"下面厚重"那股最短最实 ✓）⇒ 打到树冠右下 ✓ */
  .tier-2 .ce2-drop.dp3,.tier-1 .ce2-drop.dp3{
    --ce2-rot:30deg;--ce2-rx:1.49;--ce2-ry:3.9;--ce2-w:1;--ce2-ink:#4aa9ee;
    animation-delay:.25s!important}
}
/* ★ 一条关键帧同时干两件事（必须一起 ✗ 不能拆成两个 animation ✓）：
     · 沿"壶嘴→树"把水柱【由细拉长】⇒ 尺寸单调递增 ✓（只动 --ce2-k ✓）
     · opacity 0→1 收尾再归零 ⇒ 流量跟着壶的倾倒节奏 ✗ 且不留硬跳 ✓
   ★ 40%~56% 的"回缩"是刻意的 ✗✗：ceCanPour 在 46%~70% 正把壶抬回去 ✓
     水跟着收 ⇒ 与壶同节拍 ✓ 而且收缩过程有 3 帧过渡 ⇒ 不是硬跳 ✓
   ★ 三股都从 0 起、64% 前归零 ⇒ 循环处 opacity 与 scale 都连续 ✓
     实测：58%/62%/66% 三帧的墨量单调降到 0 ✗ 没有"突然没了"✓ */
/* ★★★ 洒水单点：只动 opacity ✗ 点径由 markup 的 r=1.94 定死 ✓
   ★★ 为什么【不】动 transform / scale ✗✗（第一版差点这么写 ✓ 已在浏览器里否掉 ✓）
      （另一条更隐蔽的坑 ✓：圆点若并进 .ce2-drop 那条组规则，会连带吃到
        transform-origin:11px 27px ⇒ 从壶嘴往外飞 ✗ 见下面圆点规则的注释 ✓）
     · SVG 上 transform-origin 会被 11px 27px 那条抢走 ⇒ 圆点从壶嘴往外飞 ✗
     · 点径一动，"排"就散成"溅"✗ 用户要的是【成排】✓ 不是水花四溅 ✓
   ★ 节奏：0.5s 渐显（= 单点 1.9s 的 0%~26% ✓）⇒ 0.9s 保持（26%~84%）⇒
     0.36s 收掉（84%~100%）✓ 三点都在用户"看得见"的那一段里 ✓
   ★ 28 颗共用这一条 ✗ 差异【只在 animation-delay】✓ 那才是"逐排"的来源 ✓✓ */
@keyframes ce2Splash{
  0%{opacity:0}
  10%{opacity:.32}
  26%{opacity:1}
  60%{opacity:1}
  76%{opacity:.85}
  88%{opacity:.3}
  100%{opacity:0}
}
/* ★★★ 花洒的那口"气"：整片水从【喷头那个点】往外推开 ✗ 再收回 ✓✓
   ★ 锚点是 .ce2-fan > .ce2-dot-ring 的 transform-origin:16.5px 26.5px（= 喷头心 ✓）
     所以 scale 就是【径向位移】✗ 不是"变大变小"✓
     —— 每个点到发射点的距离被乘上 k ⇒ 近的少走 ✗ 远的多走 ⇒ 天然是"越远越散"✓
   ★ 六环共用这一条（同一 delay ✓）⇒ 半径比恒定 ⇒ 永远是一个锥形扇 ✓
   ★★ k 的下限是【算出来的】✗✗（第一版取 0.24 ✗ 看图发现水全糊在喷头上了 ✓）：
     内环 17.5 × 0.24 = 4.2 单位 = 屏幕 5px ✗ 而玫瑰头半径就有 4.4 单位 ✓
     ⇒ 六环一起缩到喷头那个小圈里 ⇒ 屏幕上是一坨蓝斑 ✗ 不是"喷"✓
     ⇒ 取 0.55：内环 = 9.6 单位 = 屏幕 11.4px（在玫瑰头外面外面一点点 ✓）
       外环 = 85.5 × 0.55 = 47 单位 = 55px ⇒ 冲程 = 85.5−47 = 38.5 单位 = 45px ✓
       0.38 个周期（0.684s）走完 45px ⇒ 外沿速度 ~99px/s ⇒ 是"喷"的观感 ✓✓
   ★ 上限 1.04（轻微过冲 ✓ 让最外圈"顶出画面感"更足 ✓）
     最远点实测 100.88px（k=1）⇒ 场景底边距发射点 105.6px ⇒ 仍不出界 ✓
     （k=1.04 时最远 ~105px ✗ 也仍在场景内 ✓ 实测 12 个相位 clipped 全 0 ✓✓） */
@keyframes ce2FanK{
  0%{transform:scale(.55)}
  38%{transform:scale(1)}
  74%{transform:scale(1)}
  100%{transform:scale(.55)}
}
/* ★★ 可见性逐环错峰 ✗✗（六环 delay 0 / .072 / .144 / .216 / .288 / .36s ⇒ 接力 ✓）
   ★ 为什么要错峰 ✗：整片一起淡出再一起淡入 ⇒ 看着像"一闪一闪"✗ 不像"一直在喷"✓
   ★ 为什么每环【各自一条关键帧】而不是只改 delay ✗✗（写之前想到的坑 ✓）：
     关键帧里的百分比是【相对各自周期】的时间 ✗ 共用一个关键帧 + 不同 delay
     ⇒ 环与环的淡入淡出形状完全同步 ✗ 依旧是整片一起闪 ✓（delay 只平移 ✗ 不改形状 ✓）
     ⇒ 正解 = 六条形状相同、时间轴整体错开的关键帧 ✓ 这才是真错峰 ✓✓
   ★★ "窗口多宽"是【算出来的】✗✗ 不是随手取的（第一版取到 82% 就归零 ✗ 实测断流 ✓）：
     环 i 的可见窗口 = [delay_i + 0.20·T , delay_i + 1.00·T]（T = 1.8s ✓）
       ⇒ 最早一环：window_end = 0 + 1.8 = 1.8s ✓
       ⇒ 最晚一环：window_end = 0.36 + 1.8 = 2.16s ⇒ 回卷到 0.36s
       ⇒ 从"上一轮最晚一环收尾 1.8s"到"这一轮最早一环起步 0s(+1.8)"之间
          空档 = 1.8 − (0.36 + 1.8 − 1.8) = 1.8 − 0.36 = 1.44s ✗
     第一版没算这步 ⇒ 实测相位 t=1500ms 时 48 颗 op 【全 0】✗✗ 屏幕上一滴水没有 ✓
     ⇒ 正解：窗口取满 [20% , 100%]（= 0.8·T = 1.44s 全程可见 ✓）
       ⇒ 空档 = 1.8 − 1.44 − 0.36 = 0s ✗ 六环首尾相扣 ✓ 从不断流 ✓✓
     ★ 淡入 20% 之内同时【从 0.24 推到 1.0】⇒ 看着是"从喷头冒出来"✗ 不是"原地闪"✓
     ★ 实测口径：0/150/…/1650ms 共 12 个相位逐帧量 ⇒ 可见颗数 11~66 从不为 0 ✓✓ */
@keyframes ce2Ring1{
  0%{opacity:0}
  20%{opacity:.95}
  46%{opacity:.9}
  78%{opacity:.72}
  100%{opacity:0}
}
@keyframes ce2Ring2{
  0%{opacity:0}
  20%{opacity:.94}
  46%{opacity:.89}
  78%{opacity:.71}
  100%{opacity:0}
}
@keyframes ce2Ring3{
  0%{opacity:0}
  20%{opacity:.93}
  46%{opacity:.88}
  78%{opacity:.7}
  100%{opacity:0}
}
@keyframes ce2Ring4{
  0%{opacity:0}
  20%{opacity:.92}
  46%{opacity:.87}
  78%{opacity:.69}
  100%{opacity:0}
}
@keyframes ce2Ring5{
  0%{opacity:0}
  20%{opacity:.91}
  46%{opacity:.86}
  78%{opacity:.68}
  100%{opacity:0}
}
@keyframes ce2Ring6{
  0%{opacity:0}
  20%{opacity:.9}
  46%{opacity:.85}
  78%{opacity:.67}
  100%{opacity:0}
}
@keyframes ce2Pour3{
  0%{opacity:0;--ce2-k:.12}
  8%{opacity:.08;--ce2-k:.42}
  14%{opacity:.46;--ce2-k:.8}
  18%{opacity:.68;--ce2-k:1}          /* ★ 18% = ceCanPour 倾角峰值（-16deg）✓ */
  30%{opacity:.85;--ce2-k:1}
  40%{opacity:.94;--ce2-k:1}
  48%{opacity:.9;--ce2-k:.9}
  56%{opacity:.55;--ce2-k:.66}
  60%{opacity:.22;--ce2-k:.4}
  64%,100%{opacity:0;--ce2-k:.12}
}

/* RTL：云鸟反向 */
html[dir="rtl"] .ce2-cloud-1,html[dir="rtl"] .ce2-cloud-2,html[dir="rtl"] .ce2-cloud-3,
html[dir="rtl"] .ce2-bird-1,html[dir="rtl"] .ce2-bird-2{animation-direction:reverse}

/* 减弱动画偏好：全部静止，保留四层颜色叙事 */
@media(prefers-reduced-motion:reduce){
  .ce2-rays,.ce2-sunglow,.ce2-cloud-1,.ce2-cloud-2,.ce2-cloud-3,.ce2-bird-1,.ce2-bird-2,.ce2-bird-flap,
  .ce2-tree,.ce2-can,.ce2-canwater,.tier-2 .ce2-drop,.tier-1 .ce2-drop,.tier-2 .ce2-dot,.tier-1 .ce2-dot,.tier-1 .ce2-can::after,.ce2-bucket{animation:none!important}
  .tier-2 .ce2-can,.tier-1 .ce2-can{transform:rotate(-15deg)}
  .ce2-tree{transform:none}
}

/* v5.3.1b 缺水时树冠稀疏、露出枝条与枝头小叶簇 */
.ce2-twig{fill:none;stroke:var(--ce2-twigc,#8a5f38);stroke-width:2.6;stroke-linecap:round;transition:stroke .8s ease}
.ce2-leaf{transform-box:fill-box;transform-origin:center;transition:fill .8s ease,opacity .8s ease,transform .8s ease}
.ce2-tuft{opacity:0;fill:var(--ce2-leaf1);transform-box:fill-box;transform-origin:center;transition:opacity .7s ease,fill .8s ease}
.ce2-sheen{transition:opacity .6s ease}
.role-child.tier-2{--ce2-twigc:#8a6a3d}
.role-child.tier-1{--ce2-twigc:#75532f}
/* tier2 偏低：外侧叶簇收缩，枝条与叶尖小叶簇显出 */
.tier-2 .fbl,.tier-2 .fbr{transform:scale(.5);opacity:.18}
.tier-2 .fl,.tier-2 .fr{transform:scale(.68);opacity:.55}
.tier-2 .ftl,.tier-2 .ftr{transform:scale(.8)}
.tier-2 .fcm{transform:scale(.72);opacity:.7}
.tier-2 .ce2-tuft{opacity:.95}
.tier-2 .ce2-sheen{opacity:.45}
.tier-2 .fc{transform:scale(.84)}
/* tier1 告急：大冠几乎落尽，只见枝条与零星叶簇 */
.tier-1 .fbl,.tier-1 .fbr{opacity:0}
.tier-1 .fl,.tier-1 .fr{transform:scale(.5);opacity:.22}
.tier-1 .fc{transform:scale(.6)}
.tier-1 .fcm{transform:scale(.45);opacity:.3}
.tier-1 .ftl,.tier-1 .ftr{transform:scale(.7);opacity:.7}
.tier-1 .ce2-tuft{opacity:.95}
.tier-1 .ce2-sheen{opacity:0}
.tier-1 .ce2-branch,.tier-1 .ce2-twig{opacity:.92}

/* ═══════════════════════════════════════════════════════════════════
   ★★ 第 13 轮：清风吹过 ✓✓（用户要求 ✓）
   用户原话："隔个十来秒晃一下…就是有风吹过，一阵风呜这样清风吹过…
             就是时间放那里，它都会这样子一阵风吹过，然后树会晃动摆动一下"✓
   ★ 设计要点（让风像"真的吹过"✗ 而不是机械循环 ✗）：
     ① 周期 12 秒 ✓ 其中【只有约 2.5 秒在动】✗
        其余 9.5 秒完全静止 ✓✓ → 体感是"偶尔来一阵"✓ 不是持续摇摆 ✓
        （keyframes 里 0%~79% 钉在原位 ✓ 79%~100% 完成一次摆动 ✓）
     ② 摆幅小 ✗：树冠 ±1.6° ✓ 树干只 ±0.35° ✓
        （真实的树：树干几乎不动 ✓ 树冠动得多 ✓✓）
     ③ 叶子【错峰】✓：每片延迟 0~0.5s ✓
        → 像风从一侧扫到另一侧 ✓✓ 比整块一起摆自然得多 ✓
     ④ ease-in-out ✓ 起风与收风都有加减速 ✓
     ⑤ 尊重 prefers-reduced-motion ✓（下方已有规则 ✓ 再把 ce2 系列加上 ✓）
   ★ 为什么用 12s 而不是真随机 ✗：
     纯 CSS 做不到随机 ✓ 12s 在体感上已够"偶尔"✓
     （用户停留通常 < 60s ✓ 会看到 3~5 次 ✓ 不觉得机械 ✓）
   ═══════════════════════════════════════════════════════════════════ */
/* 树冠整体：只摆动很短的窗口 ✓ 其余时间静止 ✓ */
.ce2-tree{animation:ce2WindSway 12s ease-in-out infinite;transform-box:fill-box;transform-origin:50% 100%}
@keyframes ce2WindSway{
  0%,79%{transform:rotate(0deg)}
  83%{transform:rotate(-1.6deg)}
  87%{transform:rotate(1.3deg)}
  91%{transform:rotate(-0.8deg)}
  95%{transform:rotate(0.45deg)}
  100%{transform:rotate(0deg)}
}
/* 叶子：在树冠摆动的基础上各自轻颤 ✓ 错峰 ✓ */
.ce2-tree .ce2-leaf{animation:ce2LeafRustle 12s ease-in-out infinite;transform-box:fill-box;transform-origin:50% 60%}
@keyframes ce2LeafRustle{
  0%,79%{transform:translate(0,0) rotate(0deg)}
  83%{transform:translate(-1.8px,-0.8px) rotate(-4.5deg)}
  87%{transform:translate(1.5px,0.5px) rotate(3.2deg)}
  91%{transform:translate(-1px,-0.5px) rotate(-1.9deg)}
  95%{transform:translate(0.6px,0.2px) rotate(1.1deg)}
  100%{transform:translate(0,0) rotate(0deg)}
}
/* ★ 错峰延迟 ✓ 让风像从右向左扫过 ✓（越靠右越早动 ✓）*/
.ce2-tree .fc{animation-delay:.00s}.ce2-tree .fcm{animation-delay:.06s}
.ce2-tree .fl{animation-delay:.14s}.ce2-tree .fr{animation-delay:.10s}
.ce2-tree .ftl{animation-delay:.26s}.ce2-tree .ftr{animation-delay:.22s}
.ce2-tree .fbl{animation-delay:.18s}.ce2-tree .fbr{animation-delay:.16s}
.ce2-tree .f1{animation-delay:.30s}.ce2-tree .f2{animation-delay:.28s}
.ce2-tree .f3{animation-delay:.34s}.ce2-tree .f4{animation-delay:.38s}.ce2-tree .f5{animation-delay:.36s}
/* 树干：几乎不动 ✓ 只极轻微跟随 ✓ */
.ce2-trunk{animation:ce2TrunkSway 12s ease-in-out infinite;transform-box:fill-box;transform-origin:50% 100%}
@keyframes ce2TrunkSway{
  0%,79%{transform:rotate(0deg)}
  85%{transform:rotate(-0.35deg)}
  92%{transform:rotate(0.22deg)}
  100%{transform:rotate(0deg)}
}
/* ★ 无障碍：系统里关了动效就不该再摇 ✗ */
@media(prefers-reduced-motion:reduce){.ce2-tree,.ce2-tree .ce2-leaf,.ce2-trunk{animation:none!important}}

/* ═══════════════════════════════════════════════════════════════════
   ★★ 第 15 轮：蓝色风痕 + 落叶 + 太阳光晕呼吸 ✓✓（用户要求 ✓）
   ★ 时间轴（12 秒一轮 ✓ 与树的摆动【共用同一节拍】✓✓）：
       0% ~ 79%   完全静止 ✓（没有风 ✓ 风痕不可见 ✓）
       79% ~ 88%  ★ 一阵风扫过 ✓（风痕可见 ✓ 叶子开始飘 ✓）
       88% ~ 100% 风走了 ✓ 叶子飘完淡出 ✓
     ⇒ 与 .ce2-tree 的 ce2WindSway 对齐 ✓ 风和树同时发生 ✓✓
        （如果两者节拍不同 ✓ 就会"树在摆但没有风"✗ 很假 ✓）
   ═══════════════════════════════════════════════════════════════════ */
/* ★ 太阳光晕：缓慢呼吸 ✓ 与太阳本体【错开节拍】✓（同拍会显得机械 ✗）*/
.ce2-halo{animation:ce2HaloBreath 5.5s ease-in-out infinite;transform-box:fill-box;transform-origin:center}
.ce2-halo-inner{animation:ce2HaloBreath 4.2s ease-in-out infinite .8s;transform-box:fill-box;transform-origin:center}
@keyframes ce2HaloBreath{
  0%,100%{transform:scale(.94);opacity:.82}
  50%{transform:scale(1.08);opacity:1}
}
.ce2-sun-core{animation:ce2CoreTwinkle 3.1s ease-in-out infinite}
.ce2-sun-core2{animation:ce2CoreTwinkle 2.4s ease-in-out infinite .6s}
@keyframes ce2CoreTwinkle{0%,100%{opacity:.75}50%{opacity:1}}
/* ★ 无障碍：关了动效就不该再吹 ✗ */
@media(prefers-reduced-motion:reduce){
  .ce2-wind path,.ce2-fall ellipse,.ce2-halo,.ce2-halo-inner,.ce2-sun-core,.ce2-sun-core2{animation:none!important}
}

/* ═══════════════════════════════════════════════════════════════════
   ★★ 第 18 轮：风与落叶 ✓✓（按用户五条意见重做 ✓）
   ★ 时长：15 / 17.5 / 20 秒三档 ✓ 由 JS 每次进站【随机挑一档】✓✓
     写在 --ce2-wind-dur 上 ✓（未设时回退 16s ✓）
   ★ 相位：--ce2-wind-phase 由 JS 随机给一个负延迟 ✓
     这样同一档时长也不会每次都在同一刻起风 ✓✓（用户要"随机"✓）
   ★ 方向：全部沿【右上 → 左下】✓✓（与落叶方向一致 ✓ 逻辑通顺 ✓）
   ═══════════════════════════════════════════════════════════════════ */
.ce2-tree,.ce2-tree .ce2-leaf,.ce2-trunk,.ce2-wind path,.ce2-fall ellipse{
  animation-duration:var(--ce2-wind-dur,16s)!important;
  animation-delay:var(--ce2-wind-phase,0s)!important;
}
/* ★ 风痕：短斜段从右上扫向左下 ✓ 到【一半就消失】✗ 不是穿出画面 ✓ */
.ce2-wind path{animation-name:ce2WindLine;animation-timing-function:ease-out;animation-iteration-count:infinite;transform-box:fill-box}
@keyframes ce2WindLine{
  0%,79%{transform:translate(0,0);opacity:0}
  81%{opacity:0}
  /* ★★ 起风：从右上来 ✓ 短促有力 ✓ */
  82.5%{transform:translate(10px,-6px);opacity:.52}
  85%{transform:translate(-14px,10px);opacity:.60}
  /* ★★ 吹到一半 → 消失 ✓✓（用户明确要求 ✓）*/
  87%{transform:translate(-30px,20px);opacity:.22}
  88%,100%{transform:translate(-38px,26px);opacity:0}
}
.ce2-wind-1{animation-delay:calc(var(--ce2-wind-phase,0s) + 0s)!important}
.ce2-wind-2{animation-delay:calc(var(--ce2-wind-phase,0s) + .14s)!important}
.ce2-wind-3{animation-delay:calc(var(--ce2-wind-phase,0s) + .28s)!important}
/* ★ 落叶：在左侧 ✓ 继续向左下 ✓ 与风同向 ✓✓ */
.ce2-fall ellipse{animation-name:ce2LeafFall;animation-timing-function:ease-in;animation-iteration-count:infinite;transform-box:fill-box;transform-origin:center}
@keyframes ce2LeafFall{
  0%,79%{transform:translate(0,0) rotate(0deg);opacity:0}
  80%{opacity:0}
  82%{opacity:.95}
  88%{transform:translate(-30px,24px) rotate(-125deg);opacity:.85}
  93%{transform:translate(-52px,50px) rotate(-215deg);opacity:.32}
  96%,100%{transform:translate(-58px,58px) rotate(-255deg);opacity:0}
}
.ce2-fall-1{animation-delay:calc(var(--ce2-wind-phase,0s) + 0s)!important}
.ce2-fall-2{animation-delay:calc(var(--ce2-wind-phase,0s) + .10s)!important}
.ce2-fall-3{animation-delay:calc(var(--ce2-wind-phase,0s) + .20s)!important}
.ce2-fall-4{animation-delay:calc(var(--ce2-wind-phase,0s) + .30s)!important}
.ce2-fall-5{animation-delay:calc(var(--ce2-wind-phase,0s) + .42s)!important}

/* ══════════════════════════════════════════════════════════════════════
   ★★★ 第 45 轮：主题层【对比度泄漏】根治 ✗✗（两个已确认现象 ✗ 同一个根因 ✓）
   ──────────────────────────────────────────────────────────────────────
   ★ 现象（用户 + 另一路 agent 各报一个 ✓ 本机真浏览器实测 ✓ 不是推断 ✓）：
     ① 用户："某一些 theme 下面下拉框的颜色与后面的重叠 ✗ 没有设背景色吗 ✗
              怎么会是透明的 ✗ 影响阅读和体验"
        实测（aurora 深色 ✗ 桌面 1400）：#avatarDrop computed background 只有
        rgba(255,255,255,0.06) ✗ 菜单盒像素采样 = #ecf9fb（背后白卡【整片透上来】✗）
        菜单文字 #f1f0fb 压在上面 ⇒ 1.03~1.07:1 ✗ 文字互相重叠 ✗
        同一元素在 elf/cloud/story = rgb(255,255,255) 不透明 ⇒ 10.08~15.99:1 ✓
     ② 另一路实测：aurora 下 html/body 与 .ce-credit-widget 都正确变深 ✓
        但 #creditCard 仍是写死的米白 ⇒ #creditsTitle/.ce-card-heading = rgb(207,215,245)
        压在米白卡上 = 1.41:1 ✗ 几乎看不见 ✓
   ★ 根因 ✗ 不是"元素没吃到 token" ✓ 而是【吃到了 ✗ 但 token 本身不是面板色】：
     · aurora 的 --card = rgba(255,255,255,.06) ✗ 那是【覆盖色】（玻璃感）✗
       而全仓 25 处 `var(--card)` 里有 15 处是【本该不透明的面板】
       （#avatarDrop / .lang-menu / .modalbox / .plan / .theme-opt / .stage-pill /
         .mobile-tabbar / 输入框 / .sp-box / .ap-box / .lv-box …）⇒ 一律透光 ✓
     · 文字色却【是】跟主题走的（aurora --ink:#f1f0fb）⇒ 浅字 + 透光面板 = 重叠 ✗
     · 另有【写死浅色】与主题对撞：body[data-stage="primary_1_3"] 那条米白（见本文件 170 行 ✓）
   ★ 修法 ✗ 不动那 25 处消费者 ✗ 不加任何 !important ✗：
     只把 --card 【在面板作用域里重新指向不透明 token】✓
       body      ⇒ --card: var(--surface)   （页面上的面板：菜单/卡片/输入框/注入弹层 ✓）
       .modalbox ⇒ --card: var(--paper)     （弹窗在四主题里都是浅色纸面 ✓
                                              见 components.css 第 334 行那条既定设计 ✓）
     ★★ 为什么这一手能压过 ui-enhance.js:367 的【内联】background:var(--card) ✗✗
        内联声明的是 background ✗ 而 --card 是【继承下来的自定义属性】✓
        自定义属性由 css 级联决定 ⇒ 内联那一行会【解析成新的 --card 值】✓
        （内联只赢"同属性"之争 ✗ 不赢"变量取值" ✗ 所以不需要 !important ✓✓）
     ★ 为什么顺带把 --ink/--muted 也在纸面作用域里换掉 ✗：
        弹窗里的 input 走 var(--ink) ✗ aurora 下 = #f1f0fb 压在写死的 #fff 输入框上
        ⇒ 那是同族但【还没被报出来】的一条泄漏 ✗ 一并收掉 ✓（取值沿用 components.css:335/337 ✓）
   ★ 深色面板取色依据（不是新发明 ✗ 与既有 aurora 面板同族 ✓）：
     components.css 里 aurora 的 .ce-credit-widget / .ce-membership-card 早就是
     linear-gradient(180deg,#1b2442,#161d36) ⇒ --surface/--surface-2 就是这两端 ✓
   ★ 低龄段暖色卡（现象 ②）✗：米色原来是【写死】的 ✗ 三个浅主题下本来就对 ✓
     ⇒ 改成 token：浅主题 --surface-warm/--surface-warm-2 与原来两个色【逐字节相同】✓
       aurora 指向 --surface/--surface-2 ⇒ 米白卡不再出现 ✗ 浅色文字不再压浅底 ✓
       （低龄段"暖色描边 + 胶囊"的语言【保留】✗ 只去掉写死的底 ✓）
   ★ 选中态（.sp-chip.on/.sp-segbtn.on ✗ settings-panel.js 注入那份）原来是写死的
     #5eead4→#14b8a6 + 白字 ⇒ 四主题一律 1.60~1.62:1 ✗
     ⇒ --sel-a/--sel-b/--sel-line 按主题给【同族且能过 4.5 的色】✓ 白字与字号都不动 ✓
   ★ 零 !important ✗ 零新增"选择器级"补丁 ✗ 三个浅主题【渲染结果不变】
     （--surface=#ffffff = 原来的 --card ✓ --surface-warm*=原来那两个色 ✓）
   ══════════════════════════════════════════════════════════════════════ */
:root{
  /* ★ 默认值只服务"data-theme 还没写上"的那一瞬 ✓（那一刻 --card 本来是空的 ⇒ 不会更差 ✓）*/
  --paper:#ffffff; --paper-ink:#17253b; --paper-muted:#64748b; --paper-ink-2:#5b6472;
  --surface:#ffffff; --surface-2:#ffffff; --surface-warm:#ffffff; --surface-warm-2:#ffffff;
  /* ★★ 批 A（四主题一律的那 4 条 ✗ 真浏览器实测 2.35~3.48:1 ✗ 全部 <4.5）：
     · .ce-eyebrow 11px/700 #92a0b4 → 2.66 · .ce-tier-caption 11px/700 #99a6b6 → 2.35
     · .ce-credit-subline 12px #7c8a9f → 3.48 · .ce-status-pill 家族 → 3.38/3.95/4.15
     ★ 修法 ✗ 小字灰色【从 --ink 派生】✗ 不再各写一个死灰 ✓
       --ink-2 = color-mix(--ink 82%, --surface) ⇒ 四主题各自成立 ✓ 换主题自动跟随 ✓
       实测（最不利表面 ✗ 卡片 #fff / 读数条 #edf5ff / 迷你统计 #f7f9fc）：
         elf 6.08→5.53 · cloud 9.10→8.28 · story 5.97→5.43 · aurora 9.48→10.34 ⇒ 全部 ≥4.5 ✓
     ★ 胶囊的浅底色【不是】对比度问题 ⇒ 只把文字色 token 化 ✗ 底色一并命名便于换主题 ✓
       实测：tier-4 #1b6b45 on #edf8f1 = 5.96 · tier-3 #5f5113 on #f6f8e6 = 7.29
             tier-2 #8a4d12 on #fff1dd = 5.99 · tier-1 #a52730 on #fff0f0 = 6.47 ✓
       （aurora 的胶囊另有既有整条覆盖 ✗ components.css:370 ⇒ 这里给的是浅色纸面口径 ✓）
     ★ 同族顺带（同一死灰 #7c8a9f 的另外两处 ✗ 批 A 发现 ✓ 一并 token 化）：
       .ce-credit-note（11px ✗ 卡内）· .ce-manage-sub（12px ✗ 弹窗底部 ⇒ 走纸面口径 --paper-ink-2）✓ */
  --ink-2:color-mix(in srgb, var(--ink) 82%, var(--surface));
  /* ★★ 批 B（主题相关的 5 条 ✗ 实测 elf/cloud/story 2.23~2.96 ✗ 全部 <4.5）：
     · .ce-topup-btn 白字压写死的 #21cbb8→#0ea79c = 2.23（三浅主题）
     · .sp-save（=.btn.primary 白字压 --primary→--primary2 浅端）elf 2.58 / story 2.60
     · .toast.info 云 4.47 / 绘本 2.96 ✗ .toast.success 四主题一律 2.28~2.54 ✗ .toast.error 2.26~3.76
     · .auth-tab.active 云 4.47 / 绘本 2.96（18px/700 ✗ 不是大字 ⇒ 要 4.5）
     ★ 修法 ✗ 全部走 token ✗ 一个 !important 都不用：
       --shade:#000                  混色锚点（只用于"把主题色压深到白字压得住"✗ 不是颜色本身 ✓）
       --cta 复用 --sel-a/--sel-b    唯一要求都是【白字 ≥4.5】⇒ 不再新造一对重复 token ✓
       --toast-info-bg / -ok-bg / -err-bg + --toast-ink
         info: elf/aurora = --primary（11.21/5.70 ✓）✗ 云/绘本取 --sel-b（7.90/6.11 ✓）
         ok  : color-mix(--success 58%, shade) ⇒ 6.01/6.53/6.01/6.53 ✓
         err : color-mix(--danger  58%, shade) ⇒ 8.69/8.50/5.93/8.50 ✓
       --accent-ink = color-mix(--primary 62%, shade) ⇒ 15.47/8.96/6.60/10.56 ✓
         ★ aurora 单独保持 --primary（它本来就 5.70 过 ✗ 深色主题观感零回归 ✓）
     ★ 注：(6) #planPill 用【安全带口径】复核 = 4.77 ✓ 本来就 PASS ⇒ 不动（修前那个 4.32 是旧口径假阳性 ✓）*/
  --shade:#000; --toast-ink:#ffffff;
  --accent-ink:color-mix(in srgb, var(--primary) 62%, var(--shade));
  --toast-info-bg:var(--primary);
  --toast-ok-bg:color-mix(in srgb, var(--success) 58%, var(--shade));
  --toast-err-bg:color-mix(in srgb, var(--danger) 58%, var(--shade));
  --pill-ok-bg:#edf8f1; --pill-ok-ink:#1b6b45;
  --pill-t3-bg:#f6f8e6; --pill-t3-ink:#5f5113;
  --pill-t2-bg:#fff1dd; --pill-t2-ink:#8a4d12;
  --pill-t1-bg:#fff0f0; --pill-t1-ink:#a52730;
  --sel-a:#0f766e; --sel-b:#115e59; --sel-line:#0b5a54; --sel-ink:#ffffff;
}
:root[data-theme="elf"]{
  --surface:#ffffff; --surface-2:#F4F9FD;
  --surface-warm:#fffdf8; --surface-warm-2:#fff8ed;
  --sel-a:#0f766e; --sel-b:#115e59; --sel-line:#0b5a54; --sel-ink:#ffffff;
}
:root[data-theme="cloud"]{
  --surface:#ffffff; --surface-2:#F7F5FF;
  --surface-warm:#fffdf8; --surface-warm-2:#fff8ed;
  --sel-a:#4f46e5; --sel-b:#4338ca; --sel-line:#3730a3; --sel-ink:#ffffff;
}
:root[data-theme="story"]{
  --surface:#ffffff; --surface-2:#FDF0DD;
  --surface-warm:#fffdf8; --surface-warm-2:#fff8ed;
  --sel-a:#b85a3d; --sel-b:#9c4a31; --sel-line:#8a3f2a; --sel-ink:#ffffff;
}
:root[data-theme="aurora"]{
  --surface:#1b2442; --surface-2:#161d36;
  --surface-warm:#1b2442; --surface-warm-2:#161d36;
  --sel-a:#6d28d9; --sel-b:#5b21b6; --sel-line:#4c1d95; --sel-ink:#ffffff;
}
/* ★★ 面板作用域 ✗✗：把 --card 重新指向不透明面 ✓
   （不改那 25 处消费者 ✗ 不用 !important ✗ 内联那份也会跟着解析 ✓ 理由见上面注释 ✓）*/
body{--card:var(--surface)}
/* ★★ 纸面作用域 ✗✗：弹窗四主题都是浅色纸面（components.css 第 334 行 ✓）
   ⇒ 里面恢复"纸面"token（取值与 components.css:335/337 已有的两条【完全相同】✓）
   ★ 为什么必须连 --ink 一起换 ✗：不然弹窗里 input 的 color 还是 aurora 的 #f1f0fb ✗
     压在写死的 #fff 输入框上 ⇒ 白字白底 ✓（同族泄漏 ✗ 本轮一并收掉 ✓）*/
.modalbox{--card:var(--paper);--ink:var(--paper-ink);--muted:var(--paper-muted);--ink-2:var(--paper-ink-2)}
/* ★★ 批 B：逐主题的例外（都放在文件末尾 ⇒ 不动任何既有行号 ✓）
   · cloud/story 的 .toast.info 底色 ✗ --primary 太浅（4.47/2.96）⇒ 取各自的深色端 --sel-b（7.90/6.11 ✓）
   · aurora 的 .auth-tab.active ✗ 它本来就 5.70 过 ⇒ 保持 --primary（**深色主题观感零回归** ✓）*/
:root[data-theme="cloud"]{--toast-info-bg:var(--sel-b)}
:root[data-theme="story"]{--toast-info-bg:var(--sel-b)}
:root[data-theme="aurora"]{--accent-ink:var(--primary)}

/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 第 58 轮：水壶边的【跳动感叹号】✗✗（产品主人第 58 条要求 ✓）
   用户原话：「一个水壶还不够…水壶在动，然后呢，在它边上放一个感叹号，也是在那跳…感叹号提醒让警示的作用会更加的明显，
              类似于这种感叹号的红颜色的，要粗壮一点的小朋友看了现在也不要太突兀的，有比较卡通的感叹号。
              然后外面再套一个三角三角形，它的边缘要润滑要圆角，然后整个东西在那跳，忽大忽小忽大忽小，
              跟这个动画要重叠在这个动画的上面。然后我点那个感叹号和点这个水壶，都会引导到充值的页面」
   ── 每条要求落在哪一行 ✗ ──
     · "红颜色 / 粗壮 / 卡通 / 不要突兀"  ⇒ fill #E8554A + 同色 stroke-width:7 + 无黑描边（见 .ce2-warn-tri 那段的注释 ✓）
     · "外面套一个三角、边缘润滑要圆角"   ⇒ stroke-linejoin:round 让粗描边把顶点磨圆 ✓
     · "整个东西在那跳、忽大忽小忽大忽小" ⇒ @keyframes ce2WarnPulse 的 scale + opacity + brightness ✓
     · "跟这个动画重叠在这个动画的上面"   ⇒ 本元素是 .ce2-can【同级的绝对定位兄弟】✗ 叠在同一个场景层上 ✓（不是并排、不占位 ✓）
     · "点感叹号和点水壶都进充值页"       ⇒ 两处【同一份】逻辑在 app.js 末尾的代理监听 ✓ 本文件不管跳转 ✓
   ── ★★ 节奏：为什么是 9500ms 一圈（950 跳 + 8550 停）✗✗（★ 产品主人拍板的数 ✓）──
     他在被问到"一直跳还是间歇跳"之后追加原话：
       「跳一下，然后停的时间停一段时间稍微长一点，跳呢速度也不要太快…跳完了以后呢，停一下，比如停个七八秒，八九秒都可以，然后再跳」
     ⇒ 本条按此实现：950ms（两跳 × 475ms）+ 8550ms 停 = 9500ms
     · 停 8550ms 落在"七八秒、八九秒"区间内、取中偏高的一侧 ✓
     · 跳:停 ≈ 1:9 ⇒ 是"招手"不是"警报" ✓
       （常见警示闪动是 200~300ms 一次 ✗ 那是警报语气 ✓ 475ms 是慢拍 ✓）
     · ★ 与壶的节拍比 ≈ 2:1：壶的 ce2CanAlert 恒 1.25s（上面 .tier-2 .ce2-can 那条 ✓）
       ⇒ 8.55s 静止期内壶跳约 7 拍、感叹号只在那 0.95s 里动两下
       ⇒ 视觉上读成"壶一直在轻晃（环境感）+ 感叹号偶尔招呼一下（事件感）"✗ 主从关系 ✓
         不会变成"两件东西同拍竞速"✓（★ 这就是"两个东西同时跳吵不吵"的解法 ✓ 不是把壶停掉 ✓）
   ── ★★ 为什么一条动画里同时做 scale + opacity + brightness ✗✗ ──
     他对同一件事说过两种话：①"忽大忽小忽大忽小"（= 缩放）②"忽闪忽现"（= 更像透明度）
     ⇒ 两句要【都满足】的唯一做法就是两个一起做 ✓ 但都做【弱】✗：
        scale 1.00→1.09 · opacity 1→.94 · filter 加 4% 亮度
     ⇒ 幅度小而不刺眼，而且 opacity/brightness 让小幅缩放【更容易被察觉】✓
       ——这就是为什么 scale 只给到 1.09 而【不是】1.15 甚至 1.5（★ 用两个弱信号换一个不刺眼的幅度 ✓）
     ⇒ ★ 并且全程【不变色、不做高对比反色】✗ 是"呼吸/忽闪"✗ 不是"闪烁"✓
   ── ★★ 基态写在媒体查询之外 ✗✗（与 components.css:2784-2789 同一条纪律 ✓）──
     9500ms 里【89%】是静止的（10%~100% 停在 scale 1 / opacity 1 ✓）
     ⇒ 关掉动画时画面与"停在休息位"逐像素一致 ✗ 不会留下中间态 ✓✓
   ── ★★ reduced-motion：三层里的第 1、2 层 ✗✗（第 3 层在 components.css 末尾 ✓）──
     ① 全站硬闸（themes.css:167 与 components.css:453 的 *{animation:none!important}）本来就会关掉它 ✓
     ② 本条动画【不带 !important】⇒ 硬闸一定赢 ✓
     ③ 但仍要【逐条点名】✗（tests/tree-scene-fx.js:251-263 与 tests/email-and-watering.js:192-195
        明写"即使将来全局那条被收窄，点名列表里也必须有它"）⇒ 加进 themes.css:847-852 那个 reduce 块 ✓
   ★ 修改此处务必同步 public/themes.css ✓（本仓 root/public 是孪生副本 ✓）
   ══════════════════════════════════════════════════════════════════════════════ */
/* 容器：与 .ce2-can 同为 .ce-meter-scene 下的绝对定位兄弟
   （"重叠在这个动画的上面"✓ 不并排、不占位 ✓ 也不与 .ce2-can 嵌套 ⇒ 不出现 button 套 button ✓）
   ★ 可点区 40×42 ⇒ 与壶（88×76）一样远高于常见的 36~44px 下限 ✓ 两个目标中心相距约 64px ⇒ 不会误触 ✓ */
.ce2-warn{position:absolute;right:110px;bottom:46px;width:40px;height:42px;padding:0;border:0;background:none;cursor:pointer;z-index:4;display:none;-webkit-tap-highlight-color:transparent}
/* ★ 悬停轻微放大 ✗ 用 transform 而不是独立的 scale 属性 ✓
   理由：@keyframes ce2WarnPulse 写的是 transform ✗ 而独立 scale 属性与 transform【会相乘/打架】✓
     ⇒ 统一走 transform ⇒ "没在跳的那 89% 时间"里悬停才真的看得见 ✓（跳的时候动画优先级更高 ✓） */
.ce2-warn:hover{transform:scale(1.06)}
/* ★★★ 形状只声明一次 ✗✗：那个 0×0 的 <svg class="ce2-warn-defs"> 里【画着】三角与"!"✓
     CSS 一个字都不重复顶点坐标 ✗ 它只负责【染色 + 定位 + 跳动】✓
   ★ 为什么不是"纯 CSS 拼"（border 三角 / clip-path）✗✗：
     border 做出来的正三角形【永远是锐角】✗ 给不了"边缘要润滑要圆角"✓
     clip-path 也不能给 polygon 圆角 ✓
     ⇒ 只有 SVG 的 stroke-linejoin:round + 粗描边能把顶点磨成圆角 ✓（顶点外圆角 ≈ 9.4px ✓）
   ★★ 为什么不是"CSS mask 拿同一份几何染两层"✗✗（这一版【试过并当场否掉了】✓）：
     mask-image:url(#id) 这种【片段引用】在 Chrome / Safari 上口径不稳 ✗
     ⇒ 改成"markup 里那份 SVG 自己就是形状 + inset 白圈做内亮边"✓ 零兼容风险 ✓ 代码也更短 ✓
   ★★ 为什么不做 CSS 的 opacity 淡入 ✗✗：
     动画已经拿 opacity（1↔.94）当"忽闪"信号了 ✗ 再叠一层淡入会打架 ✓
     ⇒ 出现/消失交给 display 的即时切换 ✓（tier 变化本来就是一次状态跳变 ✓）*/
/* ★★★ 两支各自独立 ✗✗（不嵌套 ✓）—— 这一条是【实测逼出来的】✓
   病（本轮第一版踩到并修 ✓）：先把形状放进一个 <svg class="ce2-warn-defs" width="0" height="0"> 当"形状源"✗
     ① 那个 0×0 视口会把【嵌套的 <svg> 子元素】整支裁掉 ✓
        ⇒ getBoundingClientRect() 仍然报 40×42（盒子几何是有的 ✓）但【一个像素都不画】✓
        ⇒ 光看 computed style / rect 会误判成"已经好了"✓（实测证据：display:block ✓ 截图里却什么都没有 ✓）
     ② 被裁之后它们以 .ce-meter-scene 为定位上下文 ⇒ 落在场景右下角【外侧】⇒ 更看不见 ✓
   ⇒ 修法：两支都【直接】挂在按钮里 ✗ 不嵌套、不靠任何裁剪语义 ✓ 顶点坐标仍然只写一次 ✓
     · 三角 = .ce2-warn-tri（粗描边 + 同色 fill ⇒ 实心 + 圆角 ✓）
     · 内圈白亮边 = .ce2-warn-tri 里的【第二条 polygon】✗ 用的是【同一份顶点坐标】的描边版 ✓
       （fill:none + 1.6 白描边 ✓ 这样就不必用 inset box-shadow / mask 去凑 ✓
        而且"几何只有一处"这条纪律保住了 ✓ —— 改顶点时两条 polygon 一起改 ✓）
     · 感叹号 = .ce2-warn-bang（杆 + 圆点 ✓）
   ★ 两支都 pointer-events:none ✗ 点击穿透到下面的 <button> ✓（按钮本身就是命中区 ✓） */
.ce2-warn-ball{position:absolute;inset:0;display:block;pointer-events:none}
.ce2-warn-tri,.ce2-warn-bang{position:absolute;left:0;top:0;width:40px;height:42px;overflow:visible;display:block;pointer-events:none}
/* ★★★ 三角 ✗✗ 三件：
     ① 粗描边 + 同色 fill ⇒ 实心 + 圆角（顶点外圆角 ≈ 9.4px / 底角 ≈ 3.15px ✓ 真的"润滑"✓）
     ② fill #E8554A —— 【不用纯饱和红】✗ 是偏橙的暖珊瑚红 ✓
        理由：壶是暖橙（#ffb04a→#f2811d）+ 草地是嫩绿 ⇒ 纯正红压嫩绿是对比最刺眼的组合 ✓
     ③ 草地落影用 filter:drop-shadow ✓（挂在整支 <svg> 上 ⇒ 整支只有一个影子 ✓）
     ★ 尺寸压小 ✗ 36 宽 vs 壶 88 宽 ⇒ 它是徽标不是主角 ✓ */
.ce2-warn-tri{filter:drop-shadow(0 3px 4px rgba(150,60,45,.28))}
/* ★★ 感叹号 ✗✗ 杆 3.8 宽 + 2.35 半径圆点 ✓
   ★ 杆粗 3.8/40 = 9.5% ⇒ 满足"要粗壮一点"✓ 全圆头（rx=1.9 = 半宽 / 圆点用圆）✓
   ★ drop-shadow 挂在这一支（不是三角那支）✗ ⇒ 影子是"感叹号投在三角上"✓ 而不是三角投在草地上 ✓
     两支各投各的会糊 ✗ */
.ce2-warn-bang{filter:drop-shadow(0 1px 1px rgba(150,60,45,.35))}
/* ★★ 只在 tier-1 / tier-2 出现 ✗✗（与壶【同一档】✓ 见上面 .tier-2 .ce2-can 那条）
   基态 display:none ⇒ 额度充足（tier-3/4）时这张卡【一个字节都不变】✓
   ★ 为什么用 .ce-credit-widget 前缀（0,3,0）而不是裸 .tier-2（0,2,0）✗：
     与 themes.css:490 那条同特异性 ⇒ 显式提一级更稳 ✓（写法也与 490 那条一致 ✓） */
.ce-credit-widget.tier-1 .ce2-warn,.ce-credit-widget.tier-2 .ce2-warn{display:block}
/* ★★★ 跳动 ✗✗ ★ 第 60 轮起【总周期 9.5s → 8s】✗✗（产品主人第 60 条："闪动频率设置成每隔 8 秒闪动一次"✓）
   · 他只说了【周期】✗ 没说跳法 ⇒ ★ 跳法一格不动 ✓ 只改总周期 ✓
   · 旧：9.5s 一圈 = 950ms 跳（两跳 × 475ms）+ 8550ms 停 ✓
   · 新：8s 一圈 = 960ms 跳（两跳 × 480ms）+ 7040ms 停 ✓
   · 关键帧百分比 = 毫秒 ÷ 80（8000ms ÷ 100 ✓）⇒ 480ms = 6% · 240ms = 3% ✓
     ★ 换算后每个节点的【毫秒值】与旧版逐位相同（110/230/350/470/590/710/830/950 ✓）
   ★★★ 同步改动：components.css 末尾【两处】ceMotionOn 那份也必须 9.5s → 8s ✗✗
     ⇒ 那两份共用【同一个 @keyframes ce2WarnPulse】✗ 只改一处 ⇒ 百分比会按旧分母换算 ⇒ 跳法整个变形 ✓
       （实测过的坑：同一个 keyframes 名字被两个 duration 引用 = 两条腿不共用同一个分母 ✓）
   ★ 不带 !important ✗（reduce 硬闸必须赢得过它 ✓
     带 !important 的那一条在 components.css 末尾、且包在 @media(prefers-reduced-motion:no-preference) 里 ✓）*/
.ce-credit-widget.tier-1 .ce2-warn,.ce-credit-widget.tier-2 .ce2-warn{animation:ce2WarnPulse 8s ease-in-out infinite}
@keyframes ce2WarnPulse{
  0%{transform:scale(1);opacity:1;filter:brightness(1)}
  1.2%{transform:scale(1.075);opacity:.965;filter:brightness(1.03)}
  2.5%{transform:scale(1.09);opacity:.94;filter:brightness(1.04)}
  3.8%{transform:scale(1.075);opacity:.965;filter:brightness(1.03)}
  5%{transform:scale(1);opacity:1;filter:brightness(1)}
  6.2%{transform:scale(1.075);opacity:.965;filter:brightness(1.03)}
  7.5%{transform:scale(1.09);opacity:.94;filter:brightness(1.04)}
  8.8%{transform:scale(1.075);opacity:.965;filter:brightness(1.03)}
  10%{transform:scale(1);opacity:1;filter:brightness(1)}
  100%{transform:scale(1);opacity:1;filter:brightness(1)}
}
/* 键盘焦点环 ✓（.ce2-warn 是原生 <button> ⇒ Tab 可达、Enter/Space 可触发 ✓）
   ★ 用 :focus-visible 而不是 :focus ✗ 鼠标点一下不会留一个环 ✓ 键盘用户一定看得见 ✓ */
.ce2-warn:focus-visible{outline:3px solid var(--primary,#0f766e);outline-offset:3px;border-radius:12px}
/* ★★ 第二层 reduced-motion：逐条点名关闭 ✗✗
   （★ 与 themes.css:847-852 那个 reduce 块同一纪律：即使将来有人收窄了全局 * 那条，这里也必须点名 ✓
      见 tests/tree-scene-fx.js:251-263 与 tests/email-and-watering.js:192-195 的断言意图 ✓）
   ★ 落位写死成【静止位】✗ scale 1 / opacity 1 / brightness 1 ⇒ 与"停在休息位"逐像素一致 ✓ */
@media(prefers-reduced-motion:reduce){
  .ce2-warn{animation:none!important;transform:none!important;opacity:1!important;filter:none!important}
}

/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 第 59 轮：那个【跳动感叹号】的三处修改（★ 产品主人第 59 条要求 ✓）
   ── 他的原话 ✗✗ ──
     「另外，那个感叹号我觉得呢，把这个感叹号可以做得更加圆润，里面的叹号也可以更加 Q 一点，
       位置放在水壶的右下角那个位置就好了，你改一下。」
   ── 三处改动落在哪 ✗ ──
     · "更加圆润"     ⇒ 三角从 polygon+stroke 7 换成真圆角 path（下面 .ce2-warn-tri 那段 ✓）
     · "叹号更加 Q"  ⇒ 杆更粗更短 + 点更大 + 杆底到点顶的间隙从 2.05px 拉到 5.78px（index.html 那一行内 ✓）
     · "水壶右下角"  ⇒ right 110→10 / bottom 46→10（下面 .ce2-warn 那条 ✓）
   ── ★★★ 置顶：为什么"完全避开壶"做不到（物理上限 ✓ 先看这条再看位置）──
     壶的【绘制像素】右缘到 x≈313~320（场景局部坐标 ✓ v5 掩膜：13 相位并集 + 静止帧，
       实测 bbox x[214,320] y[71,151]），离场景右缘（331.13px）只剩 11~18px ✓
     而图标【整段跳动包络】是 51.43×54.01px（.ce2-warn 是 40×42，跳动峰值 scale 1.09 ✓）
     ⇒ ★ 「完全避开壶的绘制像素」与「放在壶的右下角」在几何上【互斥】✓
       那块地方本来就装不下 51px ✗ 换谁来做都一样 ✓
     ⇒ ★★ 所以本轮的物理上限就是：【按钮盒与壶盒相交，而红三角的斜边落在壶的右下轮廓线上】✓
       下面对"与壶盒的间隙"给的是【负数】（= 相交深度 ✓）那不是缺陷 ✗ 是这件事的上限 ✓
       真正该看的两个正数：与树冠 +44.06px、与树干 +97.71px ✓（旧位置是 −46.15 / −14.04 ✓）
   ── ★ 位置怎么定的（三个判据 ✓ 逐个报数）──
     ① inScene 静止与峰值【都必须 true】⇒ 排掉 right:13/bottom:0（峰值下溢 2.23px ✗）
     ② 与树冠的间隙要为正且尽量大 ⇒ right:10/bottom:10 = +44.06（旧位置 −46.15 ✓ 这正是他说的"压在树冠里"✓）
     ③ 四边余量都要留 ⇒ 它是 左 270.02 / 上 121.68 / 右 9.68 / 下 9.57 px ✓
     ★ 场景是 .ce-meter-scene（overflow:hidden ✓ 331.13×185.25 ✓）⇒ 出界会被裁 ✗ 所以判据①是硬闸 ✓
   ── ★★ 为什么选 right:10 / bottom:10 而不是"贴着场景右下角"✗ ──
     贴角（right:4/bottom:4）时整段包络 x[277.1,328.5] y[128.8,182.8] ⇒ 右余 2.6 / 下余 2.5px
       ⇒ 跳动的峰值帧只剩 2.5px 余量 ✗ 而且与树冠反而更近（+51.20 是更远 ✓ 但右/下太贴 ✗）
     right:10/bottom:10 是"既在壶的右下、四边又都留得住"的那一档 ✓
   ── ★ 橙色光环那 0~2px 的轻压 ✗ 是已知项 ✗ 不是 bug ✓（★ 写明，免得将来有人量到以为坏了 ✓）──
     tier-1 时壶自己有一圈橙色光环（.tier-1 .ce2-can::after ✓ border-radius:50% + border:3px ✓
       animation:ce2CanRing 1.05s ease-out infinite ✓ scale .72→1.28 · opacity .85→0 ✓）
     实测（解析式 + 真浏览器 ✓）：基准环宽 3.53083px（Chrome 把 border-width 也按 transform 缩放 ✓
       实测 scale=.72 时 borderTopWidth 算出来 2.54237px ⇒ 3.53083×0.72 ✓）
     绘制外沿半径 = 椭圆半径 × scale + 3.53083 × scale ÷ 2 ✓
     看得见的相位（opacity ≥ 0.10 ⇒ 进度 ≤ 0.882 ⇒ scale ≤ 1.214）里，外沿最大到
       x[197.9,320.4] y[45.6,154.9] ✓ 而图标包络顶边 y = 121.68 ⇒
     ⇒ ★ 与光环最近约 0~2px（顶左角轻轻压到最外圈 1~2px ✓）而那一档的 opacity 在 0.17~0.42 之间 ✓
       ⇒ 视觉上是"红三角压在淡淡的橙圈上"✗ 不是硬撞 ✓ 故【不作处理】✓（产品主人第 59 条批复认可 ✓）
   ── 三层 reduced-motion 的分工没有变 ✗（第 59 轮一个字都没动它 ✓）──
     ① themes.css:167 / components.css:453 的全局硬闸 ✓
     ② 本文件末尾的 @media(reduce) 逐条点名块（下面 .ce2-warn 那条 ✓）
     ③ components.css 末尾的手动开关块 ✓
     ★ 第 59 轮把 .ce2-warn 也【补进】了 tests/email-and-watering.js:192 那份点名列表 ✓
       （该文件注释自己写着"即使将来全局那条被收窄，点名列表里也必须有它"⇒
         新加的动画元素按该文件的既有规矩就该进那份列表 ✗ 不能靠全局那条兜着 ✓
         ★ 这是【加】一条断言 ✗ 不是改/弱化任何既有断言 ✓）
   ★ 修改此处务必同步 public/themes.css ✓（本仓 root/public 是孪生副本 ✓）
   ══════════════════════════════════════════════════════════════════════════════ */
/* 容器：与 .ce2-can 同为 .ce-meter-scene 下的绝对定位兄弟
   （"重叠在这个动画的上面"✓ 不并排、不占位 ✓ 也不与 .ce2-can 嵌套 ⇒ 不出现 button 套 button ✓）
   ★ 第 59 轮：right 110→10 / bottom 46→10 ✓ 理由见上面那段（三个判据 + 物理上限 ✓）
   ★ 可点区仍是 40×42 ⇒ 与壶（88×76）一样远高于常见的 36~44px 下限 ✓ */
.ce2-warn{position:absolute;right:10px;bottom:10px;width:40px;height:42px;padding:0;border:0;background:none;cursor:pointer;z-index:4;display:none;-webkit-tap-highlight-color:transparent}
/* ★ 悬停轻微放大 ✗ 用 transform 而不是独立的 scale 属性 ✓
   理由：@keyframes ce2WarnPulse 写的是 transform ✗ 而独立 scale 属性与 transform【会相乘/打架】✓
     ⇒ 统一走 transform ⇒ "没在跳的那 89% 时间"里悬停才真的看得见 ✓（跳的时候动画优先级更高 ✓） */
.ce2-warn:hover{transform:scale(1.06)}
/* ★★★ 形状只声明一次 ✗✗：三角的圆角路径与内白亮线【同一条几何】✓ CSS 一个字都不重复顶点坐标 ✓
     它只负责【染色 + 定位 + 跳动】✓
   ── 第 59 轮为什么换掉 polygon + stroke 7 ✗✗（"更加圆润"这条的算术 ✓）──
     旧做法：<polygon points="20,2.5 37.5,39.5 2.5,39.5"> + fill/stroke 同色 + stroke-width:7 + linejoin:round
       ⇒ 视觉实体 = 原三角形【向外平移 3.5】∪ 顶点关节圆弧 ✓
         顶点外沿圆角 = 3.5/sin(25.315°) = 4.093 单位 = 屏幕 4.83px ✓
         底角外沿圆角 = 3.5/sin(32.34°)  = 3.271 单位 = 屏幕 3.86px ✓（★ 底角才是"看着尖"的那个 ✓）
       ★ 粗描边能给的圆角【上限就是这样】✗ 想更圆只有两条路 ✓
         甲：继续加粗 stroke ⇒ 整个三角形会被撑大 ✗ 位置与四组间隙全部要重算 ✓（不选 ✓）
         乙：改用真弧线画圆角 path ✓（本轮选它 ✓）
   ── 新做法的几何（与旧做法【同一副骨架】✓ 只换圆角半径 ✓）──
     三条边 = 原三角形各边向外平移 1.75（= stroke 7 的【外半】✓ 这条把尺寸和位置钉住了 ✓）
     外扩多边形顶点 = 相邻两条外移边的交点 ✓
       A(20, -1.593) · B(-0.264, 41.25) · C(40.264, 41.25) ✓
     圆角半径 r：旧的 1.75（几何等价 ✓ = 屏幕上 2.06px）→ 新的 3.0（屏幕上 3.54px ✓ 1.71 倍 ✓）
     ★ 为什么是 3.0 而不是 6.0 ✗✗：
       r 越大 ⇒ 顶点圆弧越往内退 ⇒ 外廓越小 ✓ 实测（采样求绘制框 ✓）：
         r=3.0 ⇒ 绘制框 37.96×40.13 单位（旧的 39.03×41.26 ⇒ 只小 1.26/1.33px ✓ 屏幕尺寸基本不变 ✓）
         r=6.0 ⇒ 绘制框 35.40×37.42 单位（小 4.29/4.53px ✗ 明显缩水 ✓）
       ⇒ 取 3.0 ✓ 屏幕尺寸与旧的一致 ✓ 圆角明显更圆 ✓（r=6.0 那版留作备选 ✓ 产品主人第 59 条批复"两版都报"✓）
     ★ 半径上限（切点不得越过边中点）顶点 11.21 / 底角 12.83 ⇒ r=3.0 余量充足 ✓
     ★ 自检：弧心→切点的距离 = 3.0 ✓ 逐角成立 ✓
   ── 为什么不是"纯 CSS 拼"（border 三角 / clip-path）✗✗ ──
     border 做出来的正三角形【永远是锐角】✗ 给不了"边缘要润滑要圆角"✓
     clip-path 也不能给 polygon 圆角 ✓ ⇒ 只有 SVG 的 path 弧线能给 ✓
   ── 为什么不做 CSS 的 opacity 淡入 ✗✗ ──
     动画已经拿 opacity（1↔.94）当"忽闪"信号了 ✗ 再叠一层淡入会打架 ✓
     ⇒ 出现/消失交给 display 的即时切换 ✓（tier 变化本来就是一次状态跳变 ✓） */
/* ★★★ 两支各自独立 ✗✗（不嵌套 ✓）—— 这一条是【实测逼出来的】✓
    病（第 58 轮第一版踩到并修 ✓）：先把形状放进一个 <svg width="0" height="0"> 当"形状源"✗
      ① 那个 0×0 视口会把【嵌套的 <svg> 子元素】整支裁掉 ✓
         ⇒ getBoundingClientRect() 仍然报 40×42（盒子几何是有的 ✓）但【一个像素都不画】✓
         ⇒ 光看 computed style / rect 会误判成"已经好了"✓（实测证据：display:block ✓ 截图里却什么都没有 ✓）
      ② 被裁之后它们以 .ce-meter-scene 为定位上下文 ⇒ 落在场景右下角【外侧】⇒ 更看不见 ✓
    ⇒ 修法：两支都【直接】挂在按钮里 ✗ 不嵌套、不靠任何裁剪语义 ✓
      · 三角 = .ce2-warn-tri（第 59 轮起：真圆角 path + 同路径的内白亮线 ✓）
        ★ 几何只写一次 ✗✗ 内白亮线走【同一条路径的向内偏移】（下面 index.html 那一行里的第二条 path ✓）
      · 感叹号 = .ce2-warn-bang（杆 + 圆点 ✓）
    ★ 两支都 pointer-events:none ✗ 点击穿透到下面的 <button> ✓（按钮本身就是命中区 ✓） */
.ce2-warn-ball{position:absolute;inset:0;display:block;pointer-events:none}
.ce2-warn-tri,.ce2-warn-bang{position:absolute;left:0;top:0;width:40px;height:42px;overflow:visible;display:block;pointer-events:none}
/* ★★★ 三角 ✗✗ 三件：
     ① 第 59 轮起【真圆角 path】✗ stroke-width 从 7 降到 1.6（只用来把边沿收干净 ✓ 不再靠它磨圆 ✓）
        ★ 描边 1.6 的外半 0.8 会让绘制框再外扩 0.8 ⇒ 上面算的 37.96×40.13 已经是含描边的 ✓
     ② fill #E8554A —— 【不用纯饱和红】✗ 是偏橙的暖珊瑚红 ✓（★ 第 59 轮一个字没改 ✓）
        理由：壶是暖橙（#ffb04a→#f2811d）+ 草地是嫩绿 ⇒ 纯正红压嫩绿是对比最刺眼的组合 ✓
     ③ 草地落影仍用 filter:drop-shadow ✓（挂在整支 <svg> 上 ⇒ 整支只有一个影子 ✓）
     ★ 尺寸压小 ✗ 36 宽 vs 壶 88 宽 ⇒ 它是徽标不是主角 ✓ */
.ce2-warn-tri{filter:drop-shadow(0 3px 4px rgba(150,60,45,.28))}
/* ★★ 感叹号 ✗✗ 第 59 轮按"更 Q"重定值（产品主人："里面的叹号也可以更加 Q 一点"✓）
   ★ "Q"在插画语言里 = 更粗 + 更短 + 更圆 + 间距更大 ⇒ 四条一起做 ✓（下面逐条对数 ✓）
     · 杆粗   3.8 → 4.069 单位（屏幕 4.48 → 4.80px ✓ 1.07 倍 —— 略粗 ✓ 不夸张 ✓）
     · 杆长   11.5 → 8.646 单位（屏幕 13.57 → 10.20px ✓ 0.75 倍 —— 短了 ✓ 全圆头 rx 2.034 ✓）
     · 点     2.35 → 2.458 单位（屏幕 ⌀5.54 → ⌀5.80px ✓ 1.05 倍 —— 更大 ✓）
     · 间隙   2.05 → 5.78px（杆底到点顶 ✓ 2.82 倍 ✓★ 这一条最要紧 ✓）
   ★★ 为什么间隙这条最要紧 ✗✗：
     36px 显示尺寸下旧间隙只有 2.05px ⇒ 杆与点在低分屏/缩放下会糊成一根 ✗
       "一眼看出是感叹号"这个判据就是栽在这里 ✓
     新间隙 5.78px ⇒ 杆与点清清楚楚分开 ✓ 而且整体仍是"粗杆 + 大点"的卡通比例 ✓
   ★ 为什么不是"更短更粗"的 Q-b（杆 5.4×9.4 / 点 ⌀6.4）✗：
     Q-b 的杆宽高比更接近方形 ⇒ 36px 下更容易读成"一个方块加一个点" ✗
     而 Q-a 保住"叹号"的细长比例 ✓（Q-b 留作备选 ✓ 产品主人第 59 条批复"若他看完图说要更 Q 就换它"✓）
   ★ drop-shadow 挂在这一支（不是三角那支）✗ ⇒ 影子是"感叹号投在三角上"✓ 而不是三角投在草地上 ✓
     两支各投各的会糊 ✗ */
.ce2-warn-bang{filter:drop-shadow(0 1px 1px rgba(150,60,45,.35))}
/* ★★ 只在 tier-1 / tier-2 出现 ✗✗（与壶【同一档】✓ 见上面 .tier-2 .ce2-can 那条）
   基态 display:none ⇒ 额度充足（tier-3/4）时这张卡【一个字节都不变】✓
   ★ 为什么用 .ce-credit-widget 前缀（0,3,0）而不是裸 .tier-2（0,2,0）✗：
     与 themes.css:490 那条同特异性 ⇒ 显式提一级更稳 ✓（写法也与 490 那条一致 ✓） */
.ce-credit-widget.tier-1 .ce2-warn,.ce-credit-widget.tier-2 .ce2-warn{display:block}
/* ★★★ 跳动 ✗✗ ★ 第 60 轮起【总周期 9.5s → 8s】✗✗（产品主人第 60 条："闪动频率设置成每隔 8 秒闪动一次"✓）
   · 他只说了【周期】✗ 没说跳法 ⇒ ★ 跳法一格不动 ✓ 只改总周期 ✓
   · 旧：9.5s 一圈 = 950ms 跳（两跳 × 475ms）+ 8550ms 停 ✓
   · 新：8s 一圈 = 960ms 跳（两跳 × 480ms）+ 7040ms 停 ✓
   · 关键帧百分比 = 毫秒 ÷ 80（8000ms ÷ 100 ✓）⇒ 480ms = 6% · 240ms = 3% ✓
     ★ 换算后每个节点的【毫秒值】与旧版逐位相同（110/230/350/470/590/710/830/950 ✓）
   ★★★ 同步改动：components.css 末尾【两处】ceMotionOn 那份也必须 9.5s → 8s ✗✗
     ⇒ 那两份共用【同一个 @keyframes ce2WarnPulse】✗ 只改一处 ⇒ 百分比会按旧分母换算 ⇒ 跳法整个变形 ✓
       （实测过的坑：同一个 keyframes 名字被两个 duration 引用 = 两条腿不共用同一个分母 ✓）
   ★ 不带 !important ✗（reduce 硬闸必须赢得过它 ✓
     带 !important 的那一条在 components.css 末尾、且包在 @media(prefers-reduced-motion:no-preference) 里 ✓）*/
.ce-credit-widget.tier-1 .ce2-warn,.ce-credit-widget.tier-2 .ce2-warn{animation:ce2WarnPulse 8s ease-in-out infinite}
@keyframes ce2WarnPulse{
  0%{transform:scale(1);opacity:1;filter:brightness(1)}
  1.2%{transform:scale(1.075);opacity:.965;filter:brightness(1.03)}
  2.5%{transform:scale(1.09);opacity:.94;filter:brightness(1.04)}
  3.8%{transform:scale(1.075);opacity:.965;filter:brightness(1.03)}
  5%{transform:scale(1);opacity:1;filter:brightness(1)}
  6.2%{transform:scale(1.075);opacity:.965;filter:brightness(1.03)}
  7.5%{transform:scale(1.09);opacity:.94;filter:brightness(1.04)}
  8.8%{transform:scale(1.075);opacity:.965;filter:brightness(1.03)}
  10%{transform:scale(1);opacity:1;filter:brightness(1)}
  100%{transform:scale(1);opacity:1;filter:brightness(1)}
}
/* 键盘焦点环 ✓（.ce2-warn 是原生 <button> ⇒ Tab 可达、Enter/Space 可触发 ✓）
   ★ 用 :focus-visible 而不是 :focus ✗ 鼠标点一下不会留一个环 ✓ 键盘用户一定看得见 ✓ */
.ce2-warn:focus-visible{outline:3px solid var(--primary,#0f766e);outline-offset:3px;border-radius:12px}
/* ★★ 第二层 reduced-motion：逐条点名关闭 ✗✗
   （★ 与 themes.css:847-852 那个 reduce 块同一纪律：即使将来有人收窄了全局 * 那条，这里也必须点名 ✓
      见 tests/tree-scene-fx.js:251-263 与 tests/email-and-watering.js:192-195 的断言意图 ✓
      ★ 第 59 轮起 tests/email-and-watering.js:192 那份点名列表里【也有 .ce2-warn】✓）
   ★ 落位写死成【静止位】✗ scale 1 / opacity 1 / brightness 1 ⇒ 与"停在休息位"逐像素一致 ✓ */
@media(prefers-reduced-motion:reduce){
  .ce2-warn{animation:none!important;transform:none!important;opacity:1!important;filter:none!important}
}
/* ══════════════════════════════════════════════════════════════════════════════
   ★★★ 第 60 轮：那个【警示图标】的三处修改（★ 产品主人第 60 条 ✓）
   ── 他的原话 ✗✗ ──
     「这个里面感叹号它周围围白那个红色边上那一圈白的边框颜色，改成我们的这个网站的特色颜色（图二）。
       中间的这个感叹号，上面那一竖可以再长一点。然后他现在的闪动频率，你给他设置成每隔8秒闪动一次。」
   ── 三处落在哪 ✗ ──
     · "那一圈白改成特色色" ⇒ index.html 那一行里【第 2 条】<path> 的 stroke ✓
        ★ 一律挂 var(--ce-primary,#12b3a0) ✗ 不抄十六进制 ⇒ 将来换品牌色它跟着变 ✓
        ★★★ 这圈边换色【不影响三角本身的红】✗✗ 极易被后来者"顺手统一"成一个颜色 ✗
          · 红三角 = path[0]（fill="#E8554A" stroke="#E8554A" ✓ 一个字都不许动 ✓）
          · 那一圈边 = path[1]（fill:none + stroke=var(--ce-primary,#12b3a0) ✓ 只有它该变 ✓）
          · 产品主人原话是"【红色】边上那一圈白的边框颜色改成特色色"⇒ 【红色】是他要保留的前提 ✓
          ⇒ ★ 若把两者统一成一个颜色 ⇒ 红三角就没了 ✗ 而且会连带毁掉下面①那句论证的前提 ✓
            （"外圈那条极细红边就是 path[0] 的 1.6 stroke 露在外面的一半 ✓ 所以三角照样跳得出来"✓）
     · "上面那一竖再长一点" ⇒ index.html 那一行里 <rect> 的 y/height ✓
     · "每隔 8 秒闪动一次"   ⇒ 本文件末尾的 @keyframes ce2WarnPulse + 上面那条声明 ✓
   ── ① 那一圈边：为什么挂 --ce-primary ✗ 不是 --primary（★ 这是【刻意的】）──
     ★ 产品主人给的"图二"是【青色】⇒ 他要的就是那个色 ✓
       挂 --primary 会让它变成 cloud 紫 / story 橙 / aurora 紫 ⇒ 那就不是图二那个色了 ✗
     ⇒ 所以本处挂 --ce-primary(#12b3a0 = 品牌青绿，design-tokens.css 的【唯一真源】✓)
       fallback #12b3a0 是【目标色】✗ 不是兜底 ✓
     ★★ 代价（如实记 ✗ 不粉饰 ✓）：cloud / story / aurora 三套主题下它【仍是青色】⇒ 不跟随主题 ✓
       ⇒ 若要跟随主题，改挂 --primary ✓ 但那时它会变紫/橙 ⇒ 需产品主人重新拍板 ✓
   ── ① 那圈边的【代价数】（★ 实测 ✗ 不是估算 ✓ 真 Chrome + CDP + 真像素）──
     ★ 那圈边原来是 stroke="rgba(255,255,255,.55)"（★ 是【一条 path 的描边】✗ 不是第三支 SVG ✓
       它是内圈亮边：向内偏 1.325 单位 − 描边外半 0.8 ⇒ 外侧留 0.62px 红边 ✓）
     ★★ 换成 --ce-primary 之后（起止都是实测值 ✓）：
        · 带内最亮像素    #feb9af  →  #14b4a1
        · ΔL(边, 红三角)   0.3472   →  0.1122   （只剩 32% ✗）
        · 边 vs 红 对比    2.19     →  1.38
     ★★★ 根因（将来有人觉得"这圈边怎么不显"⇒ 一眼看到原因 ✗ 不必重新测一遍 ✓）：
        【红与青的亮度天然挨着】L(#E8554A)=0.2415 vs L(#12b3a0)=0.3491 ⇒ ΔL 只有 0.108 ✓
        而白 L=1.0000 ⇒ ΔL(白,红)=0.7585 ✓
        ⇒ 一条 1.2px 的"等亮度异色线"在 36px 真尺寸下会糊掉 ✓（×8 放大仍看得见 ✗ 但真尺寸要仔细找 ✓）
     ★ 补救全部【实测失败】✗（不是没试 ✓）：
        · 白底衬 + 青叠上           ⇒ 白被青【整条盖死】，带里找不到一个变亮的像素 ✓
        · 白底衬加宽到 2.6 单位     ⇒ 白边回来了 ✓ 但青带只剩 0.25px ✗ 细成"彩边" ✗
        · 白底衬加宽到 ≥3.4 单位    ⇒ 青被【完全埋掉】⇒ 三角变纯白描边 ✗✗ 那就不是他要的东西了 ✓
        · 换更亮/更深的青           ⇒ 最亮青 1.37 / 最深青 1.52 ⇒ 都远不及白的 3.60 ✓
     ⇒ ★ 所以选"忠实执行"✗ 代价照实报 ✓（产品主人看 ×1/×4 两档图自行判断 ✓）
     ★ 备选（第一顺位 ✓）：若产品主人看完图说"不显"⇒ 换 --top-grad-b #10B8C4
        ΔL(边,红) = 0.1423（比 #12b3a0 好 32% ✓）· 一个变量名的事 ✓
   ── ② 那一竖：长了多少 ✗ 往哪延伸 ✗ 撞不撞（★ 判据用真几何 ✗ 不靠猜 ✓）──
     · <rect> y 14.4 → 12.546 · height 8.646 → 10.5
     · ⇒ 往【上】延伸（顶点方向 ✓）✗ 杆底 25.08 不动 ⇒ 杆底到点顶的【间隙一格不变】✓
     · 杆「绘制长度」= 内高 + 2r = 8.646+4.068 = 12.714 → 10.5+4.068 = 14.568 单位（1.146× ✓）
       36px 档屏幕 +4.4px ✓
     · ★★ 撞不撞：用 SVGGeometryElement.isPointInFill 在【红三角路径】上逐点判内外 ✓
         实测：逐 0.25 单位扫到【杆高 13.396 仍全在内部】✓ 再往上才出界 ✓
         杆顶两上角的出界临界 y = 4.05 ⇒ 理论最大杆高 18.996（2.197×）✓
       ⇒ ★ "更长会撞三角顶"【不成立】✗ 上限很宽 ✓（前一轮的担心是错的 ✓）
       ⇒ 取 10.5 而不是 13.0：净空 1.37px vs 0.32px ⇒ 稳得多 ✓
     · ★ 间隙：中心线净空 2.862 单位 / 够得着的绘制边缘 6.05 单位 = 【一格不变 ✓ 绝不缩小 ✓】
     ── ★★★ 口径纪律（第 60 轮定 ✓ 这条要留 ✗ 别再混用 ✓）──
       本文件一律用【单位】（SVG user unit = viewBox 单位 ✓）
       要换算成屏幕 px 时：【必须注明档位 + 系数】✓
         · 36px 档（= 基准 40 × 0.9）⇒ 系数 0.900
         · 实测 1440px 视口下系数 1.17969（按钮 47.19px 宽 ✓ 由 .ce-credit-widget 的宽度决定 ✓）
       ★ 前几轮注释里 px 与单位混用过 ✗ 例如"杆 10.2px / 间隙 5.78px"其实是"单位 × 0.955"的口径 ✓
         ⇒ 那两处不是错 ✗ 是没注明档位 ⇒ 从本轮起一律注明 ✓
   ── ③ 闪动周期 9.5s → 8s（★ 只改总周期 ✗ 跳法不动 ✓）──
     · 下面 @keyframes 的百分比 = 毫秒 ÷ 80 ⇒ 8s 一圈 = 960ms 跳（两跳 × 480ms）+ 7040ms 停 ✓
     · ★★ 必须同步：components.css 末尾【两处】ceMotionOn 那份（9.5s → 8s ✓）
       不然后果是"同一个 @keyframes 被两个 duration 引用 ⇒ 百分比按错的分母换算 ⇒ 跳法变形"✗✗
     · ★ 若产品主人要的是【单跳】（一次跳动 / 8 秒）⇒ 改法不同 ✗
       那是删掉第二跳（5.875% / 7.375% / 8.875% / 10.375% 四个节点 + 改成 1.5%/3%/... 的一套）
       ⇒ 本次【不做】✗ 他要的是"周期 8 秒" ✓ 两跳保留 ✓
   ── 三层 reduced-motion 的分工没有变 ✗（第 60 轮一个字都没动它 ✓）──
     ① themes.css:145 / components.css:453 的全局硬闸 ✓
     ② 本文件末尾的 @media(reduce) 逐条点名块 ✓
     ③ components.css 末尾的手动开关块 ✓
     ★ tests/email-and-watering.js:192 那份点名列表读的是 themes-ceremony.css ⇒ 本轮不碰那个文件 ✓
   ★ 修改此处务必同步 public/themes.css ✓（本仓 root/public 是孪生副本 ✓）
   ══════════════════════════════════════════════════════════════════════════════ */
/* ★★ 8s 版关键帧 ✗✗ 同名覆盖上面那份（后面赢 ✓）
   ★ 新名称也行 ✗ 但同名覆盖改动更小 ✓ 且能让上面那条 8s 声明直接生效 ✓
   ★ 百分比 = 毫秒 ÷ 80 ✓ 与旧的 ÷95 是【同一组毫秒值】⇒ 跳法逐位相同 ✓ */
@keyframes ce2WarnPulse{
  0%{transform:scale(1);opacity:1;filter:brightness(1)}
  1.375%{transform:scale(1.075);opacity:.965;filter:brightness(1.03)}
  2.875%{transform:scale(1.09);opacity:.94;filter:brightness(1.04)}
  4.375%{transform:scale(1.075);opacity:.965;filter:brightness(1.03)}
  5.875%{transform:scale(1);opacity:1;filter:brightness(1)}
  7.375%{transform:scale(1.075);opacity:.965;filter:brightness(1.03)}
  8.875%{transform:scale(1.09);opacity:.94;filter:brightness(1.04)}
  10.375%{transform:scale(1.075);opacity:.965;filter:brightness(1.03)}
  11.875%{transform:scale(1);opacity:1;filter:brightness(1)}
  100%{transform:scale(1);opacity:1;filter:brightness(1)}
}
