Преглед на файлове

feat: V2 重制 9 个演示 GIF + 新增英文版 + hero 升级到 v10

亮点:
- 统一 VI:全部对齐 hero-v10 色板 (#000000 + #D97757) 和 Brand Reveal 米色面板 signature
- 文字克制:每个 demo 控制在 20-48 字 / 15-36 词,从解释性文字转为锚点
- 中英平行:英文版不是字幕翻译,画面元素重新设计
- 高清 GIF:1280×720 · 25fps · full palette · sierra2_4a dither
- Concept-driven:每个 demo 都有清晰的核心传达隐喻,哑剧测试可过

文件变更:
- 9 个能力/流程 demo 全部 V2 重做(c1-c6, w1-w3),中英双版
- hero animation 从 v9 升级到 v10(中英双版)
- README.md 引用更新 v9 → v10
- README.en.md 所有 GIF 加 -en 后缀 + note 更新
- 移除冗余:20 个 -60fps.mp4(README 未引用)+ 18 个 demo MP4

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
alchain преди 2 месеца
родител
ревизия
fed775c447

+ 15 - 15
README.en.md

@@ -27,22 +27,22 @@ npx skills add alchaincyf/huashu-design
 
 [See it work](#demo-gallery) · [Install](#install) · [What it does](#what-it-does) · [How it works](#core-mechanics) · [vs. Claude Design](#vs-claude-design)
 
-> 📖 **Note for English readers**: this skill is built by a Chinese-speaking developer for a Chinese-speaking audience. The skill's agent prompts (`SKILL.md`, `references/*.md`) and all demo assets below are in **Chinese** — they work with English tasks just fine (the agent is bilingual), but the visible text in screenshots/GIFs is Chinese. The core design principles translate universally.
+> 📖 **Note for English readers**: this skill is built by a Chinese-speaking developer. The skill's agent prompts (`SKILL.md`, `references/*.md`) are in Chinese but the agent is bilingual — works fine with English tasks. The demos below are the English parallel versions; the Chinese ones are in the default-named files (see the Chinese [README.md](README.md)).
 
 </div>
 
 ---
 
 <p align="center">
-  <video src="demos/hero-animation-v9.mp4" autoplay muted loop playsinline width="100%">
-    Your browser doesn't support inline video. <a href="demos/hero-animation-v9.mp4">Download MP4</a>.
+  <video src="demos/hero-animation-v10-en.mp4" autoplay muted loop playsinline width="100%">
+    Your browser doesn't support inline video. <a href="demos/hero-animation-v10-en.mp4">Download MP4</a>.
   </video>
 </p>
 
-<p align="center"><sub>▲ 10-second hero animation showing what huashu-design does (<a href="demos/hero-animation-v9.mp4">download MP4</a> if autoplay doesn't work)</sub></p>
+<p align="center"><sub>▲ 10-second hero animation showing what huashu-design does (<a href="demos/hero-animation-v10-en.mp4">download MP4</a> if autoplay doesn't work)</sub></p>
 
 <p align="center">
-  <img src="demos/w3-fallback-advisor.gif" alt="Fallback design advisor · 3 differentiated directions from 20 design philosophies" width="100%">
+  <img src="demos/w3-fallback-advisor-en.gif" alt="Fallback design advisor · 3 differentiated directions from 20 design philosophies" width="100%">
 </p>
 
 ---
@@ -82,61 +82,61 @@ No buttons, no panels, no Figma plugin. Agent-agnostic — drops into Claude Cod
 
 ## Demo Gallery
 
-> The demos below use Chinese text in UI but the mechanics apply to any language.
+> English parallel versions of the demos. Chinese versions live at the default filenames (see the Chinese README).
 
 ### Design Direction Advisor
 
 The fallback for vague briefs: pick 3 differentiated directions from 5 schools × 20 philosophies, generate all 3 demos in parallel, let the user choose.
 
-<p align="center"><img src="demos/w3-fallback-advisor.gif" width="100%"></p>
+<p align="center"><img src="demos/w3-fallback-advisor-en.gif" width="100%"></p>
 
 ### iOS App Prototype
 
 Pixel-accurate iPhone 15 Pro body (Dynamic Island / status bar / Home Indicator) · state-driven multi-screen navigation · real images pulled from Wikimedia/Met/Unsplash · Playwright click tests before delivery.
 
-<p align="center"><img src="demos/c1-ios-prototype.gif" width="100%"></p>
+<p align="center"><img src="demos/c1-ios-prototype-en.gif" width="100%"></p>
 
 ### Motion Design Engine
 
 Stage + Sprite time-slice model · `useTime` / `useSprite` / `interpolate` / `Easing` — four APIs cover every animation need · one command exports MP4 / GIF / 60fps-interpolated / BGM-scored finals.
 
-<p align="center"><img src="demos/c3-motion-design.gif" width="100%"></p>
+<p align="center"><img src="demos/c3-motion-design-en.gif" width="100%"></p>
 
 ### HTML Slides → Editable PPTX
 
 HTML decks for browser presentation · `html2pptx.js` reads DOM computed styles and translates each element into real PowerPoint objects · exports are **actual text frames**, not image-bed fakes.
 
-<p align="center"><img src="demos/c2-slides-pptx.gif" width="100%"></p>
+<p align="center"><img src="demos/c2-slides-pptx-en.gif" width="100%"></p>
 
 ### Tweaks · Live Variation Switching
 
 Colors / typography / information density parameterized · side panel toggle · pure-frontend + `localStorage` persistence · survives reload.
 
-<p align="center"><img src="demos/c4-tweaks.gif" width="100%"></p>
+<p align="center"><img src="demos/c4-tweaks-en.gif" width="100%"></p>
 
 ### Infographic / Data Viz
 
 Magazine-grade typography · precise CSS Grid columns · `text-wrap: pretty` typographic details · driven by real data · exports to vector PDF / 300dpi PNG / SVG.
 
-<p align="center"><img src="demos/c5-infographic.gif" width="100%"></p>
+<p align="center"><img src="demos/c5-infographic-en.gif" width="100%"></p>
 
 ### 5-Dimension Expert Critique
 
 Philosophical coherence · visual hierarchy · execution craft · functionality · innovation — each scored 0–10 · radar-chart visualization · outputs Keep / Fix / Quick Wins punch list.
 
-<p align="center"><img src="demos/c6-expert-review.gif" width="100%"></p>
+<p align="center"><img src="demos/c6-expert-review-en.gif" width="100%"></p>
 
 ### Junior Designer Workflow
 
 No heroic one-shot attempts: start with assumptions + placeholders + reasoning, show it to the user early, then iterate. Fixing a misunderstanding early is 100× cheaper than fixing it late.
 
-<p align="center"><img src="demos/w2-junior-designer.gif" width="100%"></p>
+<p align="center"><img src="demos/w2-junior-designer-en.gif" width="100%"></p>
 
 ### Core Asset Protocol · 5-step hard process
 
 Mandatory whenever the task involves a specific brand: ask → search → download (three fallback paths) → verify + extract → write `brand-spec.md` covering **logo, product shots, UI screenshots, colors, fonts** — all required assets, not just colors.
 
-<p align="center"><img src="demos/w1-brand-protocol.gif" width="100%"></p>
+<p align="center"><img src="demos/w1-brand-protocol-en.gif" width="100%"></p>
 
 ---
 

+ 2 - 2
README.md

@@ -34,13 +34,13 @@ npx skills add alchaincyf/huashu-design
 ---
 
 <p align="center">
-  <img src="demos/hero-animation-v9.gif" alt="huashu-design Hero · 打字 → 选方向 → 画廊展开 → 聚焦 → 品牌显形" width="100%">
+  <img src="demos/hero-animation-v10.gif" alt="huashu-design Hero · 打字 → 选方向 → 画廊展开 → 聚焦 → 品牌显形" width="100%">
 </p>
 
 <p align="center"><sub>
   ▲ 25 秒 · Terminal → 4 方向 → Gallery ripple → 4 次 Focus → Brand reveal<br>
   👉 <a href="https://www.huasheng.ai/huashu-design-hero/">访问带音效的 HTML 互动版</a> ·
-  <a href="demos/hero-animation-v9.mp4">下载 MP4(含 BGM+SFX · 10MB)</a>
+  <a href="demos/hero-animation-v10.mp4">下载 MP4(含 BGM+SFX · 10MB)</a>
 </sub></p>
 
 <p align="center">

+ 1145 - 0
demos/c1-ios-prototype-en.html

@@ -0,0 +1,1145 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8" />
+<title>huashu-design V2 · c1-ios-prototype · EN</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<style>
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --dim: rgba(255,255,255,0.18);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;
+    --accent-deep: #B85D3D;
+    --cd-bg: #F5F4F0;
+    --cd-ink: #1A1918;
+    --cd-dim: #8B867E;
+    --cd-green: #2D4A3A;
+
+    --serif-en: "Source Serif 4", Georgia, serif;
+    --serif-cn: "Noto Serif SC", "Songti SC", serif;
+    --sans: "Inter", -apple-system, "PingFang SC", sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
+    -webkit-font-smoothing: antialiased;
+  }
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+  }
+
+  /* Film grain */
+  .stage::after {
+    content: '';
+    position: absolute; inset: 0;
+    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='200' height='200' filter='url(%23n)' opacity='0.4'/></svg>");
+    opacity: 0.02;
+    pointer-events: none;
+    mix-blend-mode: overlay;
+    z-index: 200;
+  }
+
+  /* Watermark — always on top, adapts in brand reveal (handled by JS) */
+  .watermark {
+    position: absolute;
+    top: 36px; left: 48px;
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,0.16);
+    text-transform: uppercase;
+    z-index: 400;
+    pointer-events: none;
+    transition: color 0.4s;
+  }
+  .watermark.on-light { color: rgba(26,25,24,0.22); }
+
+  /* ============ Terminal (left) ============ */
+  .terminal {
+    position: absolute;
+    top: 50%;
+    left: 120px;
+    transform: translateY(-50%);
+    width: 620px;
+    background: rgba(18, 18, 18, 1);
+    border: 1px solid var(--hairline);
+    border-radius: 14px;
+    overflow: hidden;
+    opacity: 0;
+    will-change: opacity, transform;
+    box-shadow:
+      0 0 0 1px rgba(255,255,255,0.02),
+      0 40px 80px -20px rgba(217,119,87,0.12);
+  }
+  .tty-head {
+    display: flex; align-items: center; gap: 8px;
+    padding: 14px 18px;
+    border-bottom: 1px solid var(--hairline);
+    background: rgba(255,255,255,0.02);
+  }
+  .tty-head .d { width: 11px; height: 11px; border-radius: 50%; background: rgba(255,255,255,0.1); }
+  .tty-head .d.r { background: #5a2a2a; }
+  .tty-head .d.y { background: #5a4a2a; }
+  .tty-head .d.g { background: #2a5a35; }
+  .tty-head .title {
+    margin-left: 14px;
+    font-family: var(--mono);
+    font-size: 12px;
+    color: var(--muted);
+    letter-spacing: 0.04em;
+  }
+  .tty-body {
+    padding: 32px 28px;
+    font-family: var(--mono);
+    font-size: 20px;
+    line-height: 1.7;
+    color: rgba(255,255,255,0.88);
+    min-height: 220px;
+  }
+  .prompt { color: var(--accent); margin-right: 10px; }
+  .comment { color: var(--ink-60); font-size: 16px; margin-bottom: 10px; }
+  .typed { white-space: pre; }
+  .cursor {
+    display: inline-block;
+    width: 10px; height: 24px;
+    background: var(--accent);
+    vertical-align: -4px;
+    margin-left: 2px;
+    animation: blink 1s steps(2) infinite;
+  }
+  @keyframes blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }
+
+  /* Arrow connector terminal → iPhone */
+  .connector {
+    position: absolute;
+    top: 50%;
+    left: 740px;
+    width: 160px;
+    height: 2px;
+    transform: translateY(-50%);
+    opacity: 0;
+    background: linear-gradient(90deg, var(--accent) 0%, rgba(217,119,87,0) 100%);
+    transform-origin: left center;
+    will-change: opacity, transform;
+  }
+
+  /* ============ iPhone ============ */
+  .phone-wrap {
+    position: absolute;
+    top: 50%;
+    left: 1020px;
+    transform: translateY(-50%);
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+  .phone {
+    width: 440px;
+    height: 900px;
+    background: #0e0e10;
+    border-radius: 58px;
+    padding: 12px;
+    position: relative;
+    box-shadow:
+      0 0 0 1.5px rgba(255,255,255,0.14),
+      0 0 0 8px rgba(30,30,32,1),
+      0 80px 160px -20px rgba(0,0,0,0.85),
+      0 30px 70px -20px rgba(217,119,87,0.1);
+  }
+  .phone::before {
+    /* subtle metallic ring */
+    content: '';
+    position: absolute;
+    inset: -4px;
+    border-radius: 62px;
+    background: linear-gradient(135deg, rgba(255,255,255,0.12), rgba(255,255,255,0) 40%, rgba(217,119,87,0.05) 80%, rgba(255,255,255,0.08));
+    z-index: -1;
+  }
+  .screen {
+    width: 416px;
+    height: 876px;
+    border-radius: 46px;
+    overflow: hidden;
+    position: relative;
+    background: #F5F4F0;  /* default: claude mist */
+  }
+  .screen.dark { background: #0a0a0a; }
+
+  /* Dynamic island */
+  .island {
+    position: absolute;
+    top: 14px;
+    left: 50%;
+    transform: translateX(-50%);
+    width: 120px;
+    height: 34px;
+    background: #000;
+    border-radius: 999px;
+    z-index: 30;
+  }
+  /* Status bar */
+  .status-bar {
+    position: absolute;
+    top: 0; left: 0; right: 0;
+    height: 54px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 18px 34px 0 34px;
+    font-family: -apple-system, "SF Pro Text", sans-serif;
+    font-size: 15px;
+    font-weight: 600;
+    z-index: 20;
+    pointer-events: none;
+    color: inherit;
+  }
+  .status-bar .icons {
+    display: flex; align-items: center; gap: 6px;
+  }
+  .status-bar .icons .bars {
+    display: flex; align-items: flex-end; gap: 2px; height: 11px;
+  }
+  .status-bar .icons .bars div {
+    width: 3px; background: currentColor; border-radius: 1px;
+  }
+  .status-bar .icons .bat {
+    width: 26px; height: 12px;
+    border: 1.2px solid currentColor; border-radius: 3px; padding: 1px;
+    position: relative;
+    opacity: 0.9;
+  }
+  .status-bar .icons .bat::after {
+    content: ''; position: absolute; top: 3px; right: -3px; width: 2px; height: 6px;
+    background: currentColor; border-radius: 0 1px 1px 0;
+  }
+  .status-bar .icons .bat .fill {
+    width: 84%; height: 100%; background: currentColor; border-radius: 1px;
+  }
+  .home-indicator {
+    position: absolute;
+    bottom: 10px;
+    left: 50%;
+    transform: translateX(-50%);
+    width: 140px;
+    height: 5px;
+    background: rgba(0,0,0,0.3);
+    border-radius: 999px;
+    z-index: 10;
+  }
+  .screen.dark .home-indicator { background: rgba(255,255,255,0.5); }
+
+  /* Content area (below status bar) */
+  .content {
+    position: absolute;
+    top: 64px; left: 0; right: 0; bottom: 30px;
+    overflow: hidden;
+    z-index: 5;
+  }
+
+  /* Screen views */
+  .screen-view {
+    position: absolute;
+    inset: 0;
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+
+  /* 1. Wireframe (ghost) */
+  .wire {
+    padding: 40px 28px;
+  }
+  .wire .ghost {
+    background: rgba(26, 25, 24, 0.08);
+    border-radius: 10px;
+    margin-bottom: 14px;
+  }
+  .wire .g1 { height: 36px; width: 60%; }
+  .wire .g2 { height: 180px; }
+  .wire .g3 { height: 20px; width: 80%; }
+  .wire .g4 { height: 20px; width: 50%; }
+  .wire .g5 { height: 52px; margin-top: 24px; }
+
+  /* 2. Home screen — 主屏 · pomodoro */
+  .home-screen { padding: 40px 28px; color: var(--cd-ink); }
+  .home-screen .kicker {
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.22em;
+    color: var(--cd-dim);
+    text-transform: uppercase;
+  }
+  .home-screen .title {
+    font-family: var(--serif-en);
+    font-size: 48px;
+    font-weight: 300;
+    line-height: 1.02;
+    margin-top: 14px;
+    letter-spacing: -0.035em;
+    font-style: italic;
+  }
+  .home-screen .time-big {
+    margin-top: 50px;
+    font-family: var(--serif-en);
+    font-size: 168px;
+    font-weight: 200;
+    line-height: 0.95;
+    letter-spacing: -0.04em;
+    color: var(--cd-ink);
+  }
+  .home-screen .time-big .sep { color: var(--accent); }
+  .home-screen .sub {
+    font-family: var(--sans);
+    font-size: 15px;
+    color: var(--cd-dim);
+    margin-top: 18px;
+    letter-spacing: 0.02em;
+  }
+  .home-screen .cta {
+    margin-top: 64px;
+    height: 62px;
+    background: var(--cd-ink);
+    color: #fff;
+    border-radius: 999px;
+    display: flex; align-items: center; justify-content: center;
+    font-family: var(--sans);
+    font-size: 17px;
+    font-weight: 500;
+    letter-spacing: 0.04em;
+    position: relative;
+  }
+  .home-screen .cta::before {
+    content: '';
+    width: 0; height: 0;
+    border-left: 10px solid #fff;
+    border-top: 7px solid transparent;
+    border-bottom: 7px solid transparent;
+    margin-right: 10px;
+  }
+
+  /* 3. Timer · 计时 · ring */
+  .timer-screen {
+    padding: 40px 28px;
+    color: var(--cd-ink);
+    text-align: center;
+  }
+  .timer-screen .phase {
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.24em;
+    color: var(--accent);
+    text-transform: uppercase;
+    text-align: left;
+  }
+  .ring-wrap {
+    margin: 80px auto 0;
+    width: 320px; height: 320px;
+    position: relative;
+  }
+  .ring-wrap svg {
+    width: 100%; height: 100%;
+    transform: rotate(-90deg);
+  }
+  .ring-wrap .bg-ring {
+    fill: none; stroke: rgba(26,25,24,0.08); stroke-width: 14;
+  }
+  .ring-wrap .fg-ring {
+    fill: none; stroke: #D97757; stroke-width: 14; stroke-linecap: round;
+    stroke-dasharray: 880;
+    stroke-dashoffset: 880;
+  }
+  .ring-wrap .ring-label {
+    position: absolute;
+    top: 50%; left: 50%;
+    transform: translate(-50%, -50%);
+    text-align: center;
+  }
+  .ring-wrap .rl-time {
+    font-family: var(--serif-en);
+    font-size: 86px;
+    font-weight: 200;
+    line-height: 1;
+    letter-spacing: -0.03em;
+    color: var(--cd-ink);
+  }
+  .ring-wrap .rl-tag {
+    margin-top: 10px;
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.2em;
+    color: var(--cd-dim);
+    text-transform: uppercase;
+  }
+  .timer-screen .actions {
+    margin-top: 60px;
+    display: flex; gap: 14px; justify-content: center;
+  }
+  .timer-screen .act-btn {
+    padding: 14px 32px;
+    border-radius: 999px;
+    background: rgba(26,25,24,0.05);
+    font-family: var(--sans);
+    font-size: 14px;
+    font-weight: 500;
+    color: var(--cd-ink);
+    letter-spacing: 0.04em;
+    border: 1px solid rgba(26,25,24,0.08);
+  }
+  .timer-screen .act-btn.primary {
+    background: var(--cd-ink);
+    color: #fff;
+    border-color: transparent;
+  }
+
+  /* 4. Stats · 统计 · bar chart */
+  .stats-screen { padding: 40px 28px; color: var(--cd-ink); }
+  .stats-screen .stats-label {
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.24em;
+    color: var(--cd-dim);
+    text-transform: uppercase;
+  }
+  .stats-screen .stats-hero {
+    font-family: var(--serif-en);
+    font-size: 120px;
+    font-weight: 200;
+    line-height: 1;
+    letter-spacing: -0.04em;
+    margin-top: 10px;
+  }
+  .stats-screen .stats-hero .unit {
+    font-size: 28px;
+    color: var(--cd-dim);
+    margin-left: 8px;
+    font-weight: 300;
+  }
+  .stats-screen .stats-sub {
+    font-family: var(--sans);
+    font-size: 14px;
+    color: var(--cd-dim);
+    margin-top: 6px;
+    letter-spacing: 0.02em;
+  }
+  .chart {
+    margin-top: 52px;
+    display: flex;
+    gap: 10px;
+    align-items: flex-end;
+    height: 200px;
+    padding: 0 4px;
+  }
+  .chart .bar {
+    flex: 1;
+    background: var(--accent);
+    border-radius: 6px 6px 0 0;
+    opacity: 0.85;
+    transform-origin: bottom;
+    will-change: transform;
+  }
+  .chart .bar.dim { background: rgba(26,25,24,0.15); }
+  .chart-x {
+    display: flex;
+    justify-content: space-between;
+    margin-top: 12px;
+    font-family: var(--mono);
+    font-size: 10px;
+    color: var(--cd-dim);
+    letter-spacing: 0.08em;
+    padding: 0 4px;
+  }
+
+  /* 5. Settings · 设置 · list */
+  .settings-screen { padding: 40px 28px; color: var(--cd-ink); }
+  .settings-screen .title-row {
+    font-family: var(--serif-en);
+    font-size: 48px;
+    font-weight: 300;
+    letter-spacing: -0.035em;
+    font-style: italic;
+  }
+  .settings-screen .list {
+    margin-top: 40px;
+    background: #FFFFFF;
+    border-radius: 14px;
+    overflow: hidden;
+    border: 1px solid rgba(26,25,24,0.06);
+  }
+  .settings-screen .row {
+    padding: 22px 24px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    border-bottom: 1px solid rgba(26,25,24,0.06);
+  }
+  .settings-screen .row:last-child { border-bottom: none; }
+  .settings-screen .row .k {
+    font-family: var(--sans);
+    font-size: 16px;
+    color: var(--cd-ink);
+  }
+  .settings-screen .row .v {
+    font-family: var(--mono);
+    font-size: 13px;
+    color: var(--cd-dim);
+    letter-spacing: 0.04em;
+  }
+  .toggle {
+    width: 48px; height: 28px;
+    border-radius: 999px;
+    background: var(--cd-green);
+    position: relative;
+  }
+  .toggle::after {
+    content: ''; position: absolute;
+    top: 3px; right: 3px;
+    width: 22px; height: 22px;
+    background: #fff;
+    border-radius: 50%;
+    box-shadow: 0 1px 2px rgba(0,0,0,0.15);
+  }
+  .toggle.off { background: rgba(26,25,24,0.15); }
+  .toggle.off::after { left: 3px; right: auto; }
+
+  /* Tab bar (bottom of home-like screens) */
+  .tab-bar {
+    position: absolute;
+    bottom: 30px; left: 28px; right: 28px;
+    height: 58px;
+    background: #FFFFFF;
+    border-radius: 999px;
+    border: 1px solid rgba(26,25,24,0.08);
+    display: flex;
+    justify-content: space-around;
+    align-items: center;
+    padding: 0 14px;
+    box-shadow: 0 10px 28px -10px rgba(0,0,0,0.15);
+  }
+  .tab-bar .tab {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 2px;
+    font-family: var(--mono);
+    font-size: 10px;
+    color: var(--cd-dim);
+    letter-spacing: 0.1em;
+    text-transform: uppercase;
+    padding: 8px 14px;
+    border-radius: 999px;
+  }
+  .tab-bar .tab.active {
+    background: var(--cd-ink);
+    color: #fff;
+  }
+  .tab-bar .tab .ico {
+    width: 18px; height: 18px;
+    border-radius: 4px;
+    background: currentColor;
+    opacity: 0.9;
+    margin-bottom: 3px;
+  }
+
+  /* Finger / tap */
+  .tap {
+    position: absolute;
+    z-index: 40;
+    width: 64px; height: 64px;
+    pointer-events: none;
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+  .tap .core {
+    position: absolute;
+    inset: 18px;
+    background: rgba(217, 119, 87, 0.85);
+    border-radius: 50%;
+    box-shadow: 0 0 0 2px rgba(255,255,255,0.5), 0 0 24px rgba(217,119,87,0.5);
+  }
+  .tap .ring {
+    position: absolute;
+    inset: 0;
+    border: 2px solid rgba(217,119,87,0.6);
+    border-radius: 50%;
+    animation: tapring 0.6s ease-out;
+  }
+  @keyframes tapring {
+    0% { transform: scale(0.4); opacity: 1; }
+    100% { transform: scale(1.3); opacity: 0; }
+  }
+
+  /* ============ Brand Reveal ============ */
+  .brand-wall {
+    position: absolute;
+    inset: 0;
+    background: var(--cd-bg);
+    z-index: 300;
+    opacity: 0;
+    transform: translateY(100%);
+    will-change: transform, opacity;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+  }
+  .brand-wordmark {
+    font-family: var(--serif-en);
+    font-size: 132px;
+    font-weight: 200;
+    color: var(--cd-ink);
+    letter-spacing: -0.04em;
+    line-height: 1;
+    opacity: 0;
+    transform: scale(0.92);
+    will-change: opacity, transform;
+  }
+  .brand-wordmark .dot { color: var(--accent); padding: 0 10px; font-weight: 300; }
+  .brand-underline {
+    margin-top: 28px;
+    height: 2px;
+    width: 0;
+    background: var(--accent);
+    will-change: width;
+  }
+  .brand-cn {
+    margin-top: 30px;
+    font-family: var(--serif-en);
+    font-style: italic;
+    font-size: 22px;
+    font-weight: 300;
+    color: var(--cd-dim);
+    letter-spacing: 0.12em;
+    opacity: 0;
+    will-change: opacity;
+  }
+</style>
+</head>
+<body>
+
+<div class="stage" id="stage">
+
+  <div class="watermark">HUASHU · DESIGN</div>
+
+  <!-- Terminal -->
+  <div class="terminal" id="terminal">
+    <div class="tty-head">
+      <div class="d r"></div>
+      <div class="d y"></div>
+      <div class="d g"></div>
+      <div class="title">~/projects</div>
+    </div>
+    <div class="tty-body">
+      <div class="comment" id="comment" style="opacity:0">&gt; Type a sentence, get a clickable app.</div>
+      <div style="margin-top:6px">
+        <span class="prompt">$</span><span class="typed" id="typed"></span><span class="cursor" id="ttyCursor"></span>
+      </div>
+    </div>
+  </div>
+
+  <div class="connector" id="connector"></div>
+
+  <!-- Phone -->
+  <div class="phone-wrap" id="phoneWrap">
+    <div class="phone">
+      <div class="screen" id="screen">
+
+        <!-- Status bar -->
+        <div class="status-bar" id="statusBar" style="color:#1A1918">
+          <span>9:41</span>
+          <div class="icons">
+            <div class="bars">
+              <div style="height:4px"></div>
+              <div style="height:6px"></div>
+              <div style="height:8px"></div>
+              <div style="height:10px"></div>
+            </div>
+            <div class="bat"><div class="fill"></div></div>
+          </div>
+        </div>
+
+        <div class="island"></div>
+
+        <div class="content">
+
+          <!-- 1. Wireframe -->
+          <div class="screen-view" id="view-wire">
+            <div class="wire">
+              <div class="ghost g1"></div>
+              <div class="ghost g2"></div>
+              <div class="ghost g3"></div>
+              <div class="ghost g4"></div>
+              <div class="ghost g5"></div>
+            </div>
+          </div>
+
+          <!-- 2. Home -->
+          <div class="screen-view" id="view-home">
+            <div class="home-screen">
+              <div class="kicker">POMODORO</div>
+              <div class="title">Next up.</div>
+              <div class="time-big">25<span class="sep">:</span>00</div>
+              <div class="sub">Write one section. Rest five minutes.</div>
+              <div class="cta">Focus now</div>
+            </div>
+          </div>
+
+          <!-- 3. Timer -->
+          <div class="screen-view" id="view-timer">
+            <div class="timer-screen">
+              <div class="phase">FOCUS · ROUND 1</div>
+              <div class="ring-wrap">
+                <svg viewBox="0 0 320 320">
+                  <circle class="bg-ring" cx="160" cy="160" r="140"/>
+                  <circle class="fg-ring" id="fgRing" cx="160" cy="160" r="140"/>
+                </svg>
+                <div class="ring-label">
+                  <div class="rl-time" id="ringTime">24:12</div>
+                  <div class="rl-tag">REMAINING</div>
+                </div>
+              </div>
+              <div class="actions">
+                <div class="act-btn">Pause</div>
+                <div class="act-btn primary">Skip</div>
+              </div>
+            </div>
+          </div>
+
+          <!-- 4. Stats -->
+          <div class="screen-view" id="view-stats">
+            <div class="stats-screen">
+              <div class="stats-label">THIS WEEK</div>
+              <div class="stats-hero">23<span class="unit">rounds</span></div>
+              <div class="stats-sub">+5 from last week</div>
+              <div class="chart" id="chart">
+                <div class="bar dim" style="height:30%"></div>
+                <div class="bar" style="height:52%"></div>
+                <div class="bar" style="height:70%"></div>
+                <div class="bar" style="height:42%"></div>
+                <div class="bar" style="height:86%"></div>
+                <div class="bar" style="height:95%"></div>
+                <div class="bar" style="height:64%"></div>
+              </div>
+              <div class="chart-x">
+                <span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span><span>S</span>
+              </div>
+            </div>
+          </div>
+
+          <!-- 5. Settings -->
+          <div class="screen-view" id="view-settings">
+            <div class="settings-screen">
+              <div class="title-row">Settings</div>
+              <div class="list">
+                <div class="row">
+                  <span class="k">Focus length</span>
+                  <span class="v">25 MIN</span>
+                </div>
+                <div class="row">
+                  <span class="k">White noise</span>
+                  <div class="toggle"></div>
+                </div>
+                <div class="row">
+                  <span class="k">Ring alert</span>
+                  <div class="toggle off"></div>
+                </div>
+                <div class="row">
+                  <span class="k">Theme</span>
+                  <span class="v">CLAUDE MIST</span>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- Tab bar (shared, appears on home/stats/settings) -->
+          <div class="tab-bar" id="tabBar" style="display:none">
+            <div class="tab active" data-tab="home">
+              <div class="ico"></div>
+              <span>HOME</span>
+            </div>
+            <div class="tab" data-tab="timer">
+              <div class="ico"></div>
+              <span>TIMER</span>
+            </div>
+            <div class="tab" data-tab="stats">
+              <div class="ico"></div>
+              <span>STATS</span>
+            </div>
+            <div class="tab" data-tab="settings">
+              <div class="ico"></div>
+              <span>SET</span>
+            </div>
+          </div>
+
+        </div>
+
+        <div class="home-indicator"></div>
+
+        <!-- Tap overlay (inside screen so z-index > content) -->
+        <div class="tap" id="tap">
+          <div class="ring"></div>
+          <div class="core"></div>
+        </div>
+
+      </div>
+    </div>
+  </div>
+
+  <!-- Brand reveal -->
+  <div class="brand-wall" id="brandWall">
+    <div class="brand-wordmark" id="brandWord">huashu<span class="dot">·</span>design</div>
+    <div class="brand-underline" id="brandLine"></div>
+    <div class="brand-cn" id="brandCn">Say it. Get an app.</div>
+  </div>
+
+</div>
+
+<script>
+(() => {
+  // ── Scale to viewport (1920×1080 canvas) ─────────────────────────
+  function fit() {
+    const stage = document.getElementById('stage');
+    const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  fit();
+  window.addEventListener('resize', fit);
+
+  // ── Easing ───────────────────────────────────────────────────────
+  const expoOut = t => (t <= 0 ? 0 : t >= 1 ? 1 : 1 - Math.pow(2, -10 * t));
+  const expoIn  = t => (t <= 0 ? 0 : t >= 1 ? 1 : Math.pow(2, 10 * (t - 1)));
+  const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;
+  const cubicOut = t => 1 - Math.pow(1 - t, 3);
+  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
+  const lerp = (a, b, t) => a + (b - a) * t;
+
+  // Animate a value by requestAnimationFrame between timeline markers
+  function seg(t, start, end) {
+    return clamp((t - start) / (end - start), 0, 1);
+  }
+
+  // ── Elements ─────────────────────────────────────────────────────
+  const el = (id) => document.getElementById(id);
+  const terminal = el('terminal');
+  const comment  = el('comment');
+  const typed    = el('typed');
+  const ttyCursor = el('ttyCursor');
+  const connector = el('connector');
+  const phoneWrap = el('phoneWrap');
+  const views = {
+    wire: el('view-wire'),
+    home: el('view-home'),
+    timer: el('view-timer'),
+    stats: el('view-stats'),
+    settings: el('view-settings'),
+  };
+  const tap = el('tap');
+  const tabBar = el('tabBar');
+  const fgRing = el('fgRing');
+  const ringTime = el('ringTime');
+  const brandWall = el('brandWall');
+  const brandWord = el('brandWord');
+  const brandLine = el('brandLine');
+  const brandCn = el('brandCn');
+
+  // Typing text
+  const typeStr = 'make pomodoro app';
+  function setTyping(progress) {
+    const n = Math.floor(typeStr.length * progress);
+    typed.textContent = typeStr.slice(0, n);
+  }
+
+  // Show/hide views — hard swap (no cross-fade overlap)
+  function showView(name) {
+    Object.keys(views).forEach(k => {
+      const isActive = (k === name);
+      views[k].style.opacity = isActive ? '1' : '0';
+      views[k].style.visibility = isActive ? 'visible' : 'hidden';
+      views[k].style.transform = isActive ? 'translateY(0)' : 'translateY(0)';
+      views[k].style.transition = isActive ? 'opacity 0.22s ease-out' : 'none';
+    });
+  }
+
+  // Active tab
+  function setActiveTab(name) {
+    document.querySelectorAll('.tab-bar .tab').forEach(t => {
+      t.classList.toggle('active', t.dataset.tab === name);
+    });
+  }
+
+  // Play tap at screen coords (relative to .screen: 416×876)
+  function playTap(x, y) {
+    tap.style.left = (x - 32) + 'px';
+    tap.style.top = (y - 32) + 'px';
+    tap.style.opacity = '1';
+    // restart keyframe animation
+    const ring = tap.querySelector('.ring');
+    ring.style.animation = 'none';
+    ring.offsetHeight; // reflow
+    ring.style.animation = '';
+    // fade out
+    setTimeout(() => { tap.style.opacity = '0'; }, 550);
+  }
+
+  // ── SFX via WebAudio ─────────────────────────────────────────────
+  let audioCtx = null;
+  function ac() {
+    if (!audioCtx) {
+      try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e){}
+    }
+    return audioCtx;
+  }
+  function sfxClick(vol = 0.16) {
+    const c = ac(); if (!c) return;
+    const o = c.createOscillator();
+    const g = c.createGain();
+    o.type = 'square';
+    o.frequency.setValueAtTime(1200, c.currentTime);
+    o.frequency.exponentialRampToValueAtTime(500, c.currentTime + 0.04);
+    g.gain.setValueAtTime(vol, c.currentTime);
+    g.gain.exponentialRampToValueAtTime(0.001, c.currentTime + 0.05);
+    o.connect(g); g.connect(c.destination);
+    o.start(); o.stop(c.currentTime + 0.06);
+  }
+  function sfxEnter() {
+    const c = ac(); if (!c) return;
+    const o = c.createOscillator();
+    const g = c.createGain();
+    o.type = 'sine';
+    o.frequency.setValueAtTime(180, c.currentTime);
+    o.frequency.exponentialRampToValueAtTime(440, c.currentTime + 0.25);
+    g.gain.setValueAtTime(0.22, c.currentTime);
+    g.gain.exponentialRampToValueAtTime(0.001, c.currentTime + 0.3);
+    o.connect(g); g.connect(c.destination);
+    o.start(); o.stop(c.currentTime + 0.32);
+  }
+  function sfxChime() {
+    const c = ac(); if (!c) return;
+    [523.25, 783.99].forEach((f, i) => {
+      const o = c.createOscillator();
+      const g = c.createGain();
+      o.type = 'sine';
+      o.frequency.value = f;
+      g.gain.setValueAtTime(0, c.currentTime + i * 0.08);
+      g.gain.linearRampToValueAtTime(0.18, c.currentTime + i * 0.08 + 0.04);
+      g.gain.exponentialRampToValueAtTime(0.001, c.currentTime + i * 0.08 + 1.2);
+      o.connect(g); g.connect(c.destination);
+      o.start(c.currentTime + i * 0.08);
+      o.stop(c.currentTime + i * 0.08 + 1.25);
+    });
+  }
+
+  // ── Timeline ─────────────────────────────────────────────────────
+  const DURATION = 10.0;
+
+  const sfxFired = new Set();
+  function fireOnce(id, fn) {
+    if (sfxFired.has(id)) return;
+    sfxFired.add(id);
+    fn();
+  }
+
+  // Screen switch schedule (within Beat 2, 2.0s → 8.0s)
+  // Tap coords are relative to the 416×876 .screen
+  const schedule = [
+    { t: 2.0, view: 'wire',     tabIco: null,       tap: null },
+    { t: 3.1, view: 'home',     tabIco: 'home',     tap: null },                 // home materializes (no tap — it's the fill moment)
+    { t: 4.4, view: 'timer',    tabIco: 'timer',    tap: {x: 208, y: 624} },     // tap "开始专注" CTA
+    { t: 6.3, view: 'stats',    tabIco: 'stats',    tap: {x: 300, y: 810} },     // tap stats tab
+    { t: 7.5, view: 'settings', tabIco: 'settings', tap: {x: 370, y: 810} },     // tap settings tab
+  ];
+  let scheduleIdx = 0;
+
+  let startTime = null;
+  let raf = null;
+
+  function tick(now) {
+    if (!startTime) startTime = now;
+    const t = (now - startTime) / 1000;
+
+    // ── Beat 1: 0-2s ─────────────────────────────────────────
+    // Terminal fade in (0 → 0.4s)
+    {
+      const k = expoOut(seg(t, 0.0, 0.4));
+      terminal.style.opacity = k;
+      terminal.style.transform = `translateY(-50%) translateX(${lerp(-30, 0, k)}px)`;
+    }
+    // iPhone fade in (0.2 → 0.9s)
+    {
+      const k = expoOut(seg(t, 0.2, 0.9));
+      phoneWrap.style.opacity = k;
+      phoneWrap.style.transform = `translateY(-50%) translateX(${lerp(60, 0, k)}px) scale(${lerp(0.96, 1, k)})`;
+      if (t > 0.25) fireOnce('enter', sfxEnter);
+    }
+    // Connector fade
+    {
+      const k = expoOut(seg(t, 0.7, 1.2));
+      connector.style.opacity = k;
+      connector.style.transform = `translateY(-50%) scaleX(${k})`;
+    }
+    // Comment
+    {
+      const k = expoOut(seg(t, 0.8, 1.2));
+      comment.style.opacity = k * 0.82;
+    }
+    // Typing (0.6 → 1.9s)
+    {
+      const k = cubicInOut(seg(t, 0.6, 1.9));
+      setTyping(k);
+      // key click SFX at certain progress points
+      if (t > 0.8 && t < 1.85) {
+        const charsShown = Math.floor(typeStr.length * k);
+        const key = 'typ' + charsShown;
+        if (!sfxFired.has(key) && charsShown > 0 && charsShown % 3 === 0) {
+          fireOnce(key, () => sfxClick(0.08));
+        }
+      }
+    }
+    // Hide cursor when typing done
+    ttyCursor.style.opacity = t > 1.85 ? '0' : '1';
+
+    // ── Beat 2: 2-8s ─────────────────────────────────────────
+    // Execute scheduled screen transitions
+    while (scheduleIdx < schedule.length && t >= schedule[scheduleIdx].t) {
+      const s = schedule[scheduleIdx];
+      showView(s.view);
+      // status bar color: dark-text on light screens, but wire also light, keep dark
+      if (s.view === 'wire') {
+        tabBar.style.display = 'none';
+      } else {
+        tabBar.style.display = 'flex';
+        setActiveTab(s.tabIco);
+      }
+      if (s.tap) {
+        // small delay so tap appears at moment of switch
+        setTimeout(() => playTap(s.tap.x, s.tap.y), 120);
+        if (s.view !== 'wire') fireOnce('click_' + s.view, () => sfxClick(0.18));
+      }
+      scheduleIdx++;
+    }
+
+    // Timer ring animation: once timer appears (4.4s), animate ring from empty → 42% filled
+    if (t >= 4.4 && t < 6.3) {
+      const ringT = clamp((t - 4.5) / 1.2, 0, 1);
+      const fillPct = expoOut(ringT) * 0.42;
+      const offset = 880 * (1 - fillPct);
+      // Set as both style AND attr so neither overrides the other
+      fgRing.style.strokeDashoffset = offset;
+      fgRing.setAttribute('stroke-dashoffset', offset);
+      // Count down visually: 24:12 → 14:03
+      const mins = Math.floor(lerp(24, 14, expoOut(ringT)));
+      const secs = Math.floor(lerp(12, 3, expoOut(ringT)));
+      ringTime.textContent = String(mins).padStart(2,'0') + ':' + String(secs).padStart(2,'0');
+    }
+
+    // ── Beat 3: 8-10s ────────────────────────────────────────
+    // Phone + terminal fade out fast (7.5 → 7.9) so wall doesn't guillotine
+    if (t >= 7.5) {
+      const k = cubicOut(seg(t, 7.5, 7.9));
+      phoneWrap.style.opacity = String(1 - k);
+      phoneWrap.style.transform = `translateY(-50%) scale(${lerp(1, 0.94, k)})`;
+      terminal.style.opacity = String(1 - k);
+      terminal.style.transform = `translateY(-50%) scale(${lerp(1, 0.96, k)})`;
+      connector.style.opacity = String(1 - k);
+    }
+    // Brand wall slides up (7.9 → 8.6) — starts AFTER phone is gone
+    {
+      const k = expoOut(seg(t, 7.9, 8.6));
+      brandWall.style.transform = `translateY(${lerp(100, 0, k)}%)`;
+      brandWall.style.opacity = k > 0 ? '1' : '0';
+      const watermark = document.querySelector('.watermark');
+      if (k > 0.6) watermark.classList.add('on-light');
+      else watermark.classList.remove('on-light');
+    }
+    // Wordmark appears
+    {
+      const k = expoOut(seg(t, 8.5, 9.2));
+      brandWord.style.opacity = k;
+      brandWord.style.transform = `scale(${lerp(0.92, 1, k)})`;
+      if (t > 8.55) fireOnce('chime', sfxChime);
+    }
+    // Underline
+    {
+      const k = expoOut(seg(t, 9.0, 9.6));
+      brandLine.style.width = (280 * k) + 'px';
+    }
+    // CN label
+    {
+      const k = cubicOut(seg(t, 9.3, 9.9));
+      brandCn.style.opacity = k * 0.9;
+    }
+
+    if (t < DURATION) {
+      raf = requestAnimationFrame(tick);
+    } else {
+      // Hold final frame
+      if (!window.__recording) {
+        // loop for preview
+        setTimeout(() => {
+          startTime = null;
+          scheduleIdx = 0;
+          sfxFired.clear();
+          // Reset views
+          showView('wire');
+          tabBar.style.display = 'none';
+          fgRing.style.strokeDashoffset = 880;
+          fgRing.setAttribute('stroke-dashoffset', 880);
+          ringTime.textContent = '24:12';
+          // Reset brand
+          brandWall.style.transform = 'translateY(100%)';
+          brandWall.style.opacity = '0';
+          brandWord.style.opacity = '0';
+          brandWord.style.transform = 'scale(0.92)';
+          brandLine.style.width = '0';
+          brandCn.style.opacity = '0';
+          // Reset terminal typing
+          typed.textContent = '';
+          ttyCursor.style.opacity = '1';
+          comment.style.opacity = '0';
+          terminal.style.opacity = '0';
+          phoneWrap.style.opacity = '0';
+          connector.style.opacity = '0';
+          document.querySelector('.watermark').classList.remove('on-light');
+          raf = requestAnimationFrame(tick);
+        }, 600);
+      }
+    }
+  }
+
+  // seek(0) helper for render-video.js
+  window.__seek = function(s) {
+    startTime = performance.now() - s * 1000;
+  };
+
+  // Initial state
+  showView('wire');
+  tabBar.style.display = 'none';
+
+  // Wait for fonts, then start animation
+  (document.fonts ? document.fonts.ready : Promise.resolve()).then(() => {
+    requestAnimationFrame((now) => {
+      startTime = now;
+      window.__ready = true;
+      raf = requestAnimationFrame(tick);
+    });
+  });
+})();
+</script>
+
+</body>
+</html>

+ 1101 - 680
demos/c1-ios-prototype.html

@@ -1,721 +1,1142 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
+<!doctype html>
+<html lang="zh-Hans">
 <head>
-<meta charset="UTF-8">
-<title>Huashu-Design · iOS App Prototype</title>
-<script crossorigin src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
-<script crossorigin src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
-<script src="https://unpkg.com/@babel/standalone@7.25.6/babel.min.js"></script>
+<meta charset="utf-8" />
+<title>huashu-design V2 · c1-ios-prototype · 中文版</title>
 <link rel="preconnect" href="https://fonts.googleapis.com">
 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,300;1,6..72,400&family=Noto+Serif+SC:wght@400;500;600&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
 <style>
-  * { box-sizing: border-box; margin: 0; padding: 0; }
-  html, body { width: 100%; height: 100%; overflow: hidden; }
-  body { background: #0c0c0c; font-family: 'Newsreader', 'Noto Serif SC', Georgia, serif; color: #1a1a1a; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --dim: rgba(255,255,255,0.18);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;
+    --accent-deep: #B85D3D;
+    --cd-bg: #F5F4F0;
+    --cd-ink: #1A1918;
+    --cd-dim: #8B867E;
+    --cd-green: #2D4A3A;
+
+    --serif-en: "Source Serif 4", Georgia, serif;
+    --serif-cn: "Noto Serif SC", "Songti SC", serif;
+    --sans: "Inter", -apple-system, "PingFang SC", sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
+    -webkit-font-smoothing: antialiased;
+  }
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+  }
+
+  /* Film grain */
+  .stage::after {
+    content: '';
+    position: absolute; inset: 0;
+    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='200' height='200' filter='url(%23n)' opacity='0.4'/></svg>");
+    opacity: 0.02;
+    pointer-events: none;
+    mix-blend-mode: overlay;
+    z-index: 200;
+  }
+
+  /* Watermark — always on top, adapts in brand reveal (handled by JS) */
+  .watermark {
+    position: absolute;
+    top: 36px; left: 48px;
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,0.16);
+    text-transform: uppercase;
+    z-index: 400;
+    pointer-events: none;
+    transition: color 0.4s;
+  }
+  .watermark.on-light { color: rgba(26,25,24,0.22); }
+
+  /* ============ Terminal (left) ============ */
+  .terminal {
+    position: absolute;
+    top: 50%;
+    left: 120px;
+    transform: translateY(-50%);
+    width: 620px;
+    background: rgba(18, 18, 18, 1);
+    border: 1px solid var(--hairline);
+    border-radius: 14px;
+    overflow: hidden;
+    opacity: 0;
+    will-change: opacity, transform;
+    box-shadow:
+      0 0 0 1px rgba(255,255,255,0.02),
+      0 40px 80px -20px rgba(217,119,87,0.12);
+  }
+  .tty-head {
+    display: flex; align-items: center; gap: 8px;
+    padding: 14px 18px;
+    border-bottom: 1px solid var(--hairline);
+    background: rgba(255,255,255,0.02);
+  }
+  .tty-head .d { width: 11px; height: 11px; border-radius: 50%; background: rgba(255,255,255,0.1); }
+  .tty-head .d.r { background: #5a2a2a; }
+  .tty-head .d.y { background: #5a4a2a; }
+  .tty-head .d.g { background: #2a5a35; }
+  .tty-head .title {
+    margin-left: 14px;
+    font-family: var(--mono);
+    font-size: 12px;
+    color: var(--muted);
+    letter-spacing: 0.04em;
+  }
+  .tty-body {
+    padding: 32px 28px;
+    font-family: var(--mono);
+    font-size: 20px;
+    line-height: 1.7;
+    color: rgba(255,255,255,0.88);
+    min-height: 220px;
+  }
+  .prompt { color: var(--accent); margin-right: 10px; }
+  .comment { color: var(--ink-60); font-size: 16px; margin-bottom: 10px; }
+  .typed { white-space: pre; }
+  .cursor {
+    display: inline-block;
+    width: 10px; height: 24px;
+    background: var(--accent);
+    vertical-align: -4px;
+    margin-left: 2px;
+    animation: blink 1s steps(2) infinite;
+  }
+  @keyframes blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }
+
+  /* Arrow connector terminal → iPhone */
+  .connector {
+    position: absolute;
+    top: 50%;
+    left: 740px;
+    width: 160px;
+    height: 2px;
+    transform: translateY(-50%);
+    opacity: 0;
+    background: linear-gradient(90deg, var(--accent) 0%, rgba(217,119,87,0) 100%);
+    transform-origin: left center;
+    will-change: opacity, transform;
+  }
+
+  /* ============ iPhone ============ */
+  .phone-wrap {
+    position: absolute;
+    top: 50%;
+    left: 1020px;
+    transform: translateY(-50%);
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+  .phone {
+    width: 440px;
+    height: 900px;
+    background: #0e0e10;
+    border-radius: 58px;
+    padding: 12px;
+    position: relative;
+    box-shadow:
+      0 0 0 1.5px rgba(255,255,255,0.14),
+      0 0 0 8px rgba(30,30,32,1),
+      0 80px 160px -20px rgba(0,0,0,0.85),
+      0 30px 70px -20px rgba(217,119,87,0.1);
+  }
+  .phone::before {
+    /* subtle metallic ring */
+    content: '';
+    position: absolute;
+    inset: -4px;
+    border-radius: 62px;
+    background: linear-gradient(135deg, rgba(255,255,255,0.12), rgba(255,255,255,0) 40%, rgba(217,119,87,0.05) 80%, rgba(255,255,255,0.08));
+    z-index: -1;
+  }
+  .screen {
+    width: 416px;
+    height: 876px;
+    border-radius: 46px;
+    overflow: hidden;
+    position: relative;
+    background: #F5F4F0;  /* default: claude mist */
+  }
+  .screen.dark { background: #0a0a0a; }
+
+  /* Dynamic island */
+  .island {
+    position: absolute;
+    top: 14px;
+    left: 50%;
+    transform: translateX(-50%);
+    width: 120px;
+    height: 34px;
+    background: #000;
+    border-radius: 999px;
+    z-index: 30;
+  }
+  /* Status bar */
+  .status-bar {
+    position: absolute;
+    top: 0; left: 0; right: 0;
+    height: 54px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 18px 34px 0 34px;
+    font-family: -apple-system, "SF Pro Text", sans-serif;
+    font-size: 15px;
+    font-weight: 600;
+    z-index: 20;
+    pointer-events: none;
+    color: inherit;
+  }
+  .status-bar .icons {
+    display: flex; align-items: center; gap: 6px;
+  }
+  .status-bar .icons .bars {
+    display: flex; align-items: flex-end; gap: 2px; height: 11px;
+  }
+  .status-bar .icons .bars div {
+    width: 3px; background: currentColor; border-radius: 1px;
+  }
+  .status-bar .icons .bat {
+    width: 26px; height: 12px;
+    border: 1.2px solid currentColor; border-radius: 3px; padding: 1px;
+    position: relative;
+    opacity: 0.9;
+  }
+  .status-bar .icons .bat::after {
+    content: ''; position: absolute; top: 3px; right: -3px; width: 2px; height: 6px;
+    background: currentColor; border-radius: 0 1px 1px 0;
+  }
+  .status-bar .icons .bat .fill {
+    width: 84%; height: 100%; background: currentColor; border-radius: 1px;
+  }
+  .home-indicator {
+    position: absolute;
+    bottom: 10px;
+    left: 50%;
+    transform: translateX(-50%);
+    width: 140px;
+    height: 5px;
+    background: rgba(0,0,0,0.3);
+    border-radius: 999px;
+    z-index: 10;
+  }
+  .screen.dark .home-indicator { background: rgba(255,255,255,0.5); }
+
+  /* Content area (below status bar) */
+  .content {
+    position: absolute;
+    top: 64px; left: 0; right: 0; bottom: 30px;
+    overflow: hidden;
+    z-index: 5;
+  }
+
+  /* Screen views */
+  .screen-view {
+    position: absolute;
+    inset: 0;
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+
+  /* 1. Wireframe (ghost) */
+  .wire {
+    padding: 40px 28px;
+  }
+  .wire .ghost {
+    background: rgba(26, 25, 24, 0.08);
+    border-radius: 10px;
+    margin-bottom: 14px;
+  }
+  .wire .g1 { height: 36px; width: 60%; }
+  .wire .g2 { height: 180px; }
+  .wire .g3 { height: 20px; width: 80%; }
+  .wire .g4 { height: 20px; width: 50%; }
+  .wire .g5 { height: 52px; margin-top: 24px; }
+
+  /* 2. Home screen — 主屏 · pomodoro */
+  .home-screen { padding: 40px 28px; color: var(--cd-ink); }
+  .home-screen .kicker {
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.22em;
+    color: var(--cd-dim);
+    text-transform: uppercase;
+  }
+  .home-screen .title {
+    font-family: var(--serif-cn);
+    font-size: 40px;
+    font-weight: 500;
+    line-height: 1.15;
+    margin-top: 10px;
+    letter-spacing: -0.01em;
+  }
+  .home-screen .time-big {
+    margin-top: 50px;
+    font-family: var(--serif-en);
+    font-size: 168px;
+    font-weight: 200;
+    line-height: 0.95;
+    letter-spacing: -0.04em;
+    color: var(--cd-ink);
+  }
+  .home-screen .time-big .sep { color: var(--accent); }
+  .home-screen .sub {
+    font-family: var(--sans);
+    font-size: 15px;
+    color: var(--cd-dim);
+    margin-top: 18px;
+    letter-spacing: 0.02em;
+  }
+  .home-screen .cta {
+    margin-top: 64px;
+    height: 62px;
+    background: var(--cd-ink);
+    color: #fff;
+    border-radius: 999px;
+    display: flex; align-items: center; justify-content: center;
+    font-family: var(--sans);
+    font-size: 17px;
+    font-weight: 500;
+    letter-spacing: 0.04em;
+    position: relative;
+  }
+  .home-screen .cta::before {
+    content: '';
+    width: 0; height: 0;
+    border-left: 10px solid #fff;
+    border-top: 7px solid transparent;
+    border-bottom: 7px solid transparent;
+    margin-right: 10px;
+  }
+
+  /* 3. Timer · 计时 · ring */
+  .timer-screen {
+    padding: 40px 28px;
+    color: var(--cd-ink);
+    text-align: center;
+  }
+  .timer-screen .phase {
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.24em;
+    color: var(--accent);
+    text-transform: uppercase;
+    text-align: left;
+  }
+  .ring-wrap {
+    margin: 80px auto 0;
+    width: 320px; height: 320px;
+    position: relative;
+  }
+  .ring-wrap svg {
+    width: 100%; height: 100%;
+    transform: rotate(-90deg);
+  }
+  .ring-wrap .bg-ring {
+    fill: none; stroke: rgba(26,25,24,0.08); stroke-width: 14;
+  }
+  .ring-wrap .fg-ring {
+    fill: none; stroke: #D97757; stroke-width: 14; stroke-linecap: round;
+    stroke-dasharray: 880;
+    stroke-dashoffset: 880;
+  }
+  .ring-wrap .ring-label {
+    position: absolute;
+    top: 50%; left: 50%;
+    transform: translate(-50%, -50%);
+    text-align: center;
+  }
+  .ring-wrap .rl-time {
+    font-family: var(--serif-en);
+    font-size: 86px;
+    font-weight: 200;
+    line-height: 1;
+    letter-spacing: -0.03em;
+    color: var(--cd-ink);
+  }
+  .ring-wrap .rl-tag {
+    margin-top: 10px;
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.2em;
+    color: var(--cd-dim);
+    text-transform: uppercase;
+  }
+  .timer-screen .actions {
+    margin-top: 60px;
+    display: flex; gap: 14px; justify-content: center;
+  }
+  .timer-screen .act-btn {
+    padding: 14px 32px;
+    border-radius: 999px;
+    background: rgba(26,25,24,0.05);
+    font-family: var(--sans);
+    font-size: 14px;
+    font-weight: 500;
+    color: var(--cd-ink);
+    letter-spacing: 0.04em;
+    border: 1px solid rgba(26,25,24,0.08);
+  }
+  .timer-screen .act-btn.primary {
+    background: var(--cd-ink);
+    color: #fff;
+    border-color: transparent;
+  }
+
+  /* 4. Stats · 统计 · bar chart */
+  .stats-screen { padding: 40px 28px; color: var(--cd-ink); }
+  .stats-screen .stats-label {
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.24em;
+    color: var(--cd-dim);
+    text-transform: uppercase;
+  }
+  .stats-screen .stats-hero {
+    font-family: var(--serif-en);
+    font-size: 120px;
+    font-weight: 200;
+    line-height: 1;
+    letter-spacing: -0.04em;
+    margin-top: 10px;
+  }
+  .stats-screen .stats-hero .unit {
+    font-size: 28px;
+    color: var(--cd-dim);
+    margin-left: 8px;
+    font-weight: 300;
+  }
+  .stats-screen .stats-sub {
+    font-family: var(--sans);
+    font-size: 14px;
+    color: var(--cd-dim);
+    margin-top: 6px;
+    letter-spacing: 0.02em;
+  }
+  .chart {
+    margin-top: 52px;
+    display: flex;
+    gap: 10px;
+    align-items: flex-end;
+    height: 200px;
+    padding: 0 4px;
+  }
+  .chart .bar {
+    flex: 1;
+    background: var(--accent);
+    border-radius: 6px 6px 0 0;
+    opacity: 0.85;
+    transform-origin: bottom;
+    will-change: transform;
+  }
+  .chart .bar.dim { background: rgba(26,25,24,0.15); }
+  .chart-x {
+    display: flex;
+    justify-content: space-between;
+    margin-top: 12px;
+    font-family: var(--mono);
+    font-size: 10px;
+    color: var(--cd-dim);
+    letter-spacing: 0.08em;
+    padding: 0 4px;
+  }
+
+  /* 5. Settings · 设置 · list */
+  .settings-screen { padding: 40px 28px; color: var(--cd-ink); }
+  .settings-screen .title-row {
+    font-family: var(--serif-cn);
+    font-size: 40px;
+    font-weight: 500;
+    letter-spacing: -0.01em;
+  }
+  .settings-screen .list {
+    margin-top: 40px;
+    background: #FFFFFF;
+    border-radius: 14px;
+    overflow: hidden;
+    border: 1px solid rgba(26,25,24,0.06);
+  }
+  .settings-screen .row {
+    padding: 22px 24px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    border-bottom: 1px solid rgba(26,25,24,0.06);
+  }
+  .settings-screen .row:last-child { border-bottom: none; }
+  .settings-screen .row .k {
+    font-family: var(--sans);
+    font-size: 16px;
+    color: var(--cd-ink);
+  }
+  .settings-screen .row .v {
+    font-family: var(--mono);
+    font-size: 13px;
+    color: var(--cd-dim);
+    letter-spacing: 0.04em;
+  }
+  .toggle {
+    width: 48px; height: 28px;
+    border-radius: 999px;
+    background: var(--cd-green);
+    position: relative;
+  }
+  .toggle::after {
+    content: ''; position: absolute;
+    top: 3px; right: 3px;
+    width: 22px; height: 22px;
+    background: #fff;
+    border-radius: 50%;
+    box-shadow: 0 1px 2px rgba(0,0,0,0.15);
+  }
+  .toggle.off { background: rgba(26,25,24,0.15); }
+  .toggle.off::after { left: 3px; right: auto; }
+
+  /* Tab bar (bottom of home-like screens) */
+  .tab-bar {
+    position: absolute;
+    bottom: 30px; left: 28px; right: 28px;
+    height: 58px;
+    background: #FFFFFF;
+    border-radius: 999px;
+    border: 1px solid rgba(26,25,24,0.08);
+    display: flex;
+    justify-content: space-around;
+    align-items: center;
+    padding: 0 14px;
+    box-shadow: 0 10px 28px -10px rgba(0,0,0,0.15);
+  }
+  .tab-bar .tab {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 2px;
+    font-family: var(--mono);
+    font-size: 10px;
+    color: var(--cd-dim);
+    letter-spacing: 0.1em;
+    text-transform: uppercase;
+    padding: 8px 14px;
+    border-radius: 999px;
+  }
+  .tab-bar .tab.active {
+    background: var(--cd-ink);
+    color: #fff;
+  }
+  .tab-bar .tab .ico {
+    width: 18px; height: 18px;
+    border-radius: 4px;
+    background: currentColor;
+    opacity: 0.9;
+    margin-bottom: 3px;
+  }
+
+  /* Finger / tap */
+  .tap {
+    position: absolute;
+    z-index: 40;
+    width: 64px; height: 64px;
+    pointer-events: none;
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+  .tap .core {
+    position: absolute;
+    inset: 18px;
+    background: rgba(217, 119, 87, 0.85);
+    border-radius: 50%;
+    box-shadow: 0 0 0 2px rgba(255,255,255,0.5), 0 0 24px rgba(217,119,87,0.5);
+  }
+  .tap .ring {
+    position: absolute;
+    inset: 0;
+    border: 2px solid rgba(217,119,87,0.6);
+    border-radius: 50%;
+    animation: tapring 0.6s ease-out;
+  }
+  @keyframes tapring {
+    0% { transform: scale(0.4); opacity: 1; }
+    100% { transform: scale(1.3); opacity: 0; }
+  }
+
+  /* ============ Brand Reveal ============ */
+  .brand-wall {
+    position: absolute;
+    inset: 0;
+    background: var(--cd-bg);
+    z-index: 300;
+    opacity: 0;
+    transform: translateY(100%);
+    will-change: transform, opacity;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+  }
+  .brand-wordmark {
+    font-family: var(--serif-en);
+    font-size: 132px;
+    font-weight: 200;
+    color: var(--cd-ink);
+    letter-spacing: -0.04em;
+    line-height: 1;
+    opacity: 0;
+    transform: scale(0.92);
+    will-change: opacity, transform;
+  }
+  .brand-wordmark .dot { color: var(--accent); padding: 0 10px; font-weight: 300; }
+  .brand-underline {
+    margin-top: 28px;
+    height: 2px;
+    width: 0;
+    background: var(--accent);
+    will-change: width;
+  }
+  .brand-cn {
+    margin-top: 30px;
+    font-family: var(--serif-cn);
+    font-size: 18px;
+    font-weight: 300;
+    color: var(--cd-dim);
+    letter-spacing: 0.4em;
+    opacity: 0;
+    will-change: opacity;
+  }
 </style>
 </head>
 <body>
-<div id="root"></div>
-
-<!-- animations.jsx inlined -->
-<script type="text/babel">
-(function() {
-  const { createContext, useContext, useState, useEffect, useRef } = React;
-  const TimeContext = createContext({ time: 0, duration: 10, playing: false });
-  const SpriteContext = createContext(null);
-  const Easing = {
-    linear: t => t,
-    easeIn: t => t * t,
-    easeOut: t => 1 - (1 - t) * (1 - t),
-    easeInOut: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
-    spring: t => {
-      const c = (2 * Math.PI) / 3;
-      return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c) + 1;
-    },
-  };
-  function interpolate(t, input, output, easing) {
-    const [a, b] = input, [x, y] = output;
-    if (t <= a) return x; if (t >= b) return y;
-    let p = (t - a) / (b - a); if (easing) p = easing(p);
-    return x + (y - x) * p;
-  }
-  function useTime() { return useContext(TimeContext).time; }
-  function useSprite() { return useContext(SpriteContext) || { t: 0, elapsed: 0, duration: 0 }; }
-  function Stage({ duration = 10, width = 1920, height = 1080, loop = true, children, bgColor = '#fff' }) {
-    const [time, setTime] = useState(0);
-    const [playing, setPlaying] = useState(true);
-    const [scale, setScale] = useState(1);
-    const rafRef = useRef(null);
-    const effectiveLoop = (typeof window !== 'undefined' && window.__recording) ? false : loop;
-    useEffect(() => {
-      const update = () => {
-        const s = Math.min(window.innerWidth / width, (window.innerHeight - 56) / height);
-        setScale(s);
-      };
-      update(); window.addEventListener('resize', update);
-      return () => window.removeEventListener('resize', update);
-    }, [width, height]);
-    useEffect(() => {
-      if (!playing) return;
-      let cancelled = false, last = null;
-      function tick(now) {
-        if (cancelled) return;
-        if (last === null) { last = now; if (typeof window !== 'undefined') window.__ready = true; }
-        const delta = (now - last) / 1000; last = now;
-        setTime(prev => {
-          const next = prev + delta;
-          if (next >= duration) return effectiveLoop ? 0 : duration - 0.001;
-          return next;
-        });
-        rafRef.current = requestAnimationFrame(tick);
-      }
-      const start = () => { if (!cancelled) rafRef.current = requestAnimationFrame(tick); };
-      if (document.fonts && document.fonts.ready) document.fonts.ready.then(start); else start();
-      return () => { cancelled = true; cancelAnimationFrame(rafRef.current); };
-    }, [playing, duration, effectiveLoop]);
-    const progress = time / duration;
-    const ctx = { time, duration, playing, setPlaying, setTime };
-    return (
-      <TimeContext.Provider value={ctx}>
-        <div style={{position:'fixed', inset:0, background:'#0c0c0c', display:'flex', flexDirection:'column'}}>
-          <div style={{flex:1, position:'relative', overflow:'hidden'}}>
-            <div style={{position:'absolute', top:'50%', left:'50%', transformOrigin:'center center', width, height, background: bgColor, overflow:'hidden', transform:`translate(-50%, -50%) scale(${scale})`}}>
-              {children}
-            </div>
-          </div>
-          <div className="no-record" style={{position:'fixed', bottom:0, left:0, right:0, background:'rgba(0,0,0,0.8)', padding:'12px 20px', display:'flex', alignItems:'center', gap:16, color:'#fff', fontSize:12, zIndex:100}}>
-            <button onClick={()=>setPlaying(p=>!p)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>{playing?'⏸ 暂停':'▶ 播放'}</button>
-            <button onClick={()=>setTime(0)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>⏮ 开始</button>
-            <div style={{fontFamily:'ui-monospace, monospace', fontVariantNumeric:'tabular-nums', minWidth:90}}>{time.toFixed(2)}s / {duration.toFixed(2)}s</div>
-            <div style={{flex:1, height:4, background:'rgba(255,255,255,0.2)', borderRadius:2, position:'relative'}}>
-              <div style={{position:'absolute', top:0, left:0, height:'100%', width:`${progress*100}%`, background:'#fff', borderRadius:2}} />
-            </div>
-          </div>
-        </div>
-      </TimeContext.Provider>
-    );
-  }
-  function Sprite({ start = 0, end, children, style }) {
-    const { time } = useContext(TimeContext);
-    const actualEnd = end == null ? Infinity : end;
-    if (time < start || time >= actualEnd) return null;
-    const duration = actualEnd - start;
-    const elapsed = time - start;
-    const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
-    return (
-      <SpriteContext.Provider value={{ t, elapsed, duration, start, end: actualEnd }}>
-        <div style={{position:'absolute', inset:0, ...style}}>{children}</div>
-      </SpriteContext.Provider>
-    );
-  }
-  window.Animations = { Stage, Sprite, useTime, useSprite, Easing, interpolate };
-})();
-</script>
 
-<!-- Demo scene -->
-<script type="text/babel">
-const { Stage, Sprite, useTime, useSprite, Easing, interpolate } = window.Animations;
-
-// ── Design tokens ─────────────────────────────────────────
-const CREAM = '#FAF6EF';
-const PAPER = '#FDFBF5';
-const INK = '#1a1a1a';
-const TERRA = '#C04A1A';
-const OLIVE = '#6a6b4e';
-const ASH = '#6b6b6b';
-const LINE = '#e5ddcd';
-const LINE2 = '#d9d2c5';
-
-const serif = "'Newsreader', 'Noto Serif SC', Georgia, serif";
-const sans = "'Inter', -apple-system, sans-serif";
-const mono = "'JetBrains Mono', ui-monospace, monospace";
-
-// ── Art image: CSS-rendered "oil painting" hero ──────────
-function ArtBlock({ mood = 'warm', height = 200 }) {
-  // Three curated palettes for variety (no Unsplash dep, stable offline)
-  const palettes = {
-    warm: ['#8b4a2b', '#c67b4a', '#e3a876', '#f2d4a7'], // Turner sunset
-    quiet: ['#3d4a3a', '#6a8066', '#a8b89c', '#e0d8b8'], // Corot pastoral
-    study: ['#2a3552', '#5e6b8a', '#8b98b5', '#d4c9a5'], // Vermeer indoor
-  };
-  const p = palettes[mood];
-  return (
-    <div style={{
-      width: '100%', height, position: 'relative', overflow: 'hidden',
-      background: `linear-gradient(135deg, ${p[0]} 0%, ${p[1]} 35%, ${p[2]} 70%, ${p[3]} 100%)`,
-    }}>
-      {/* Impressionist brush texture */}
-      <div style={{
-        position: 'absolute', inset: 0,
-        background: `
-          radial-gradient(ellipse 80px 30px at 30% 40%, ${p[3]}44, transparent 70%),
-          radial-gradient(ellipse 60px 20px at 70% 60%, ${p[0]}33, transparent 70%),
-          radial-gradient(ellipse 100px 40px at 50% 80%, ${p[2]}44, transparent 70%),
-          radial-gradient(ellipse 50px 25px at 20% 70%, ${p[1]}55, transparent 70%)
-        `,
-        filter: 'blur(1px)',
-      }} />
-      {/* Subtle scratch noise */}
-      <svg width="100%" height="100%" style={{position:'absolute', inset:0, opacity: 0.18}}>
-        <filter id="paint-noise">
-          <feTurbulence baseFrequency="0.9" numOctaves="2" />
-          <feColorMatrix values="0 0 0 0 0.3   0 0 0 0 0.2   0 0 0 0 0.1   0 0 0 1 0" />
-        </filter>
-        <rect width="100%" height="100%" filter="url(#paint-noise)" />
-      </svg>
+<div class="stage" id="stage">
+
+  <div class="watermark">HUASHU · DESIGN</div>
+
+  <!-- Terminal -->
+  <div class="terminal" id="terminal">
+    <div class="tty-head">
+      <div class="d r"></div>
+      <div class="d y"></div>
+      <div class="d g"></div>
+      <div class="title">~/projects</div>
     </div>
-  );
-}
-
-// ── iOS Frame (simplified from ios_frame.jsx, positioned for demo) ──
-function IosFrame({ children, time = '9:41', scale = 1, style = {} }) {
-  const W = 420, H = 900;
-  return (
-    <div style={{
-      display: 'inline-block',
-      padding: 13,
-      background: '#0a0a0a',
-      borderRadius: 62,
-      boxShadow: '0 0 0 2px #2a2a2a, 0 30px 80px rgba(0,0,0,0.35), 0 10px 30px rgba(0,0,0,0.2)',
-      position: 'relative',
-      transform: `scale(${scale})`,
-      transformOrigin: 'center center',
-      ...style,
-    }}>
-      <div style={{
-        position: 'relative', width: W, height: H,
-        borderRadius: 50, overflow: 'hidden', background: PAPER,
-      }}>
-        {/* Status bar */}
-        <div style={{
-          position: 'absolute', top: 0, left: 0, right: 0, height: 54,
-          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
-          padding: '0 34px', fontSize: 17, fontWeight: 600,
-          fontFamily: '-apple-system, "SF Pro Text", sans-serif',
-          color: '#000', zIndex: 20, pointerEvents: 'none',
-        }}>
-          <span>{time}</span>
-          <div style={{display:'flex', alignItems:'center', gap: 6}}>
-            <div style={{display:'flex', alignItems:'flex-end', gap: 2, height: 12}}>
-              {[4, 6, 9, 11].map((h, i) => <div key={i} style={{width:3, height:h, background:'#000', borderRadius:1}} />)}
-            </div>
-            <svg width="16" height="12" viewBox="0 0 16 12">
-              <path d="M8 11.5a1 1 0 100-2 1 1 0 000 2z" fill="#000" />
-              <path d="M3 7.5a7 7 0 0110 0" stroke="#000" strokeWidth="1.3" fill="none" strokeLinecap="round" />
-              <path d="M1 4.5a11 11 0 0114 0" stroke="#000" strokeWidth="1.3" fill="none" strokeLinecap="round" opacity="0.7" />
-            </svg>
-            <div style={{width:26, height:12, border:'1.5px solid #000', borderRadius:3, padding:1, position:'relative'}}>
-              <div style={{width:'85%', height:'100%', background:'#000', borderRadius:1}} />
-              <div style={{position:'absolute', top:3, right:-3, width:2, height:6, background:'#000', borderRadius:'0 1px 1px 0'}} />
-            </div>
-          </div>
-        </div>
-        {/* Dynamic island */}
-        <div style={{
-          position: 'absolute', top: 12, left: '50%',
-          transform: 'translateX(-50%)', width: 124, height: 36,
-          background: '#000', borderRadius: 999, zIndex: 30,
-        }} />
-        {/* Content */}
-        <div style={{position:'absolute', top: 54, left: 0, right: 0, bottom: 34, overflow: 'hidden'}}>
-          {children}
-        </div>
-        {/* Home indicator */}
-        <div style={{
-          position: 'absolute', bottom: 10, left: '50%',
-          transform: 'translateX(-50%)', width: 140, height: 5,
-          background: 'rgba(0,0,0,0.28)', borderRadius: 999, zIndex: 10,
-        }} />
+    <div class="tty-body">
+      <div class="comment" id="comment" style="opacity:0">&gt; 说一句话,拿回一个能点的 App</div>
+      <div style="margin-top:6px">
+        <span class="prompt">$</span><span class="typed" id="typed"></span><span class="cursor" id="ttyCursor"></span>
       </div>
     </div>
-  );
-}
-
-// ── Screen: Today ────────────────────────────────────────
-function TodayScreen({ animateT = 1 }) {
-  const headerOp = interpolate(animateT, [0, 0.25], [0, 1]);
-  const headerY = interpolate(animateT, [0, 0.35], [20, 0], Easing.easeOut);
-  const heroOp = interpolate(animateT, [0.15, 0.5], [0, 1]);
-  const heroY = interpolate(animateT, [0.15, 0.5], [30, 0], Easing.easeOut);
-  const memoriesOp = interpolate(animateT, [0.4, 0.8], [0, 1]);
-
-  return (
-    <div style={{padding: '24px 22px 0', height: '100%', display:'flex', flexDirection:'column', background: PAPER}}>
-      {/* Header */}
-      <div style={{opacity: headerOp, transform: `translateY(${headerY}px)`, marginBottom: 18}}>
-        <div style={{fontFamily: mono, fontSize: 10, color: ASH, letterSpacing:'0.25em', marginBottom: 4}}>
-          周二 · 4月20日
-        </div>
-        <div style={{fontFamily: serif, fontSize: 34, fontWeight: 500, color: INK, lineHeight: 1, letterSpacing:'-0.01em'}}>
-          今日
-        </div>
-      </div>
+  </div>
 
-      {/* Hero card */}
-      <div style={{opacity: heroOp, transform: `translateY(${heroY}px)`, border: `1px solid ${LINE}`, background:'#fff', marginBottom: 14}}>
-        <ArtBlock mood="warm" height={180} />
-        <div style={{padding: '14px 16px 16px'}}>
-          <div style={{fontFamily: mono, fontSize: 9, color: TERRA, letterSpacing:'0.2em', marginBottom: 6}}>
-            继续阅读 · 剩 12 分钟
-          </div>
-          <div style={{fontFamily: serif, fontSize: 20, fontWeight: 500, color: INK, lineHeight: 1.2, marginBottom: 4, letterSpacing:'-0.005em'}}>
-            《<span style={{fontStyle:'italic'}}>沉思录</span>》
-          </div>
-          <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 13, color: ASH}}>
-            马可·奥勒留 · 第四卷
-          </div>
-          {/* AI insight */}
-          <div style={{marginTop: 14, paddingTop: 12, borderTop: `1px solid ${LINE}`,
-            display:'flex', gap: 10, alignItems: 'flex-start'}}>
-            <div style={{width: 6, height: 6, borderRadius:'50%', background: TERRA, marginTop: 6, flexShrink: 0}} />
-            <div>
-              <div style={{fontFamily: mono, fontSize: 9, color: TERRA, letterSpacing:'0.2em', marginBottom: 2}}>
-                流明 · 已关联
-              </div>
-              <div style={{fontFamily: serif, fontSize: 13, color: INK, lineHeight: 1.4}}>
-                呼应你 3 周前读的《<span style={{fontStyle:'italic'}}>塞涅卡书简·28</span>》——同在谈<span style={{fontStyle:'italic'}}>内心堡垒</span>。
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
+  <div class="connector" id="connector"></div>
 
-      {/* Memory bubbles list */}
-      <div style={{opacity: memoriesOp, flex: 1}}>
-        <div style={{fontFamily: mono, fontSize: 9, color: ASH, letterSpacing:'0.25em', marginBottom: 10}}>
-          来自你的记忆
-        </div>
-        {[
-          { title: '"Amor fati"——一个说法', sub: '尼采 · 2 个月前', dot: OLIVE },
-          { title: '论「注意力即爱」', sub: '薇依 · 5 个月前', dot: TERRA },
-        ].map((m, i) => (
-          <div key={i} style={{padding: '11px 0', borderTop: i === 0 ? `1px solid ${LINE}` : 'none', borderBottom: `1px solid ${LINE}`, display:'flex', alignItems:'center', gap: 12}}>
-            <div style={{width: 8, height: 8, borderRadius: '50%', background: m.dot}} />
-            <div style={{flex: 1}}>
-              <div style={{fontFamily: serif, fontSize: 14, color: INK, lineHeight: 1.3}}>{m.title}</div>
-              <div style={{fontFamily: mono, fontSize: 9, color: ASH, marginTop: 2, letterSpacing:'0.1em'}}>{m.sub}</div>
+  <!-- Phone -->
+  <div class="phone-wrap" id="phoneWrap">
+    <div class="phone">
+      <div class="screen" id="screen">
+
+        <!-- Status bar -->
+        <div class="status-bar" id="statusBar" style="color:#1A1918">
+          <span>9:41</span>
+          <div class="icons">
+            <div class="bars">
+              <div style="height:4px"></div>
+              <div style="height:6px"></div>
+              <div style="height:8px"></div>
+              <div style="height:10px"></div>
             </div>
-            <div style={{fontFamily: serif, fontSize: 18, color: ASH, fontStyle:'italic'}}>→</div>
+            <div class="bat"><div class="fill"></div></div>
           </div>
-        ))}
-      </div>
-    </div>
-  );
-}
-
-// ── Screen: Memory (graph view) ───────────────────────────
-function MemoryScreen({ animateT = 1 }) {
-  const headerOp = interpolate(animateT, [0, 0.3], [0, 1]);
-  const graphOp = interpolate(animateT, [0.15, 0.6], [0, 1]);
-  const listOp = interpolate(animateT, [0.5, 0.9], [0, 1]);
-
-  // Nodes for graph
-  const nodes = [
-    { x: 210, y: 100, r: 22, label: '斯多葛', emph: true },
-    { x: 110, y: 180, r: 14, label: '伦理' },
-    { x: 310, y: 170, r: 16, label: '美德', emph: true },
-    { x: 90, y: 260, r: 10, label: '' },
-    { x: 200, y: 240, r: 12, label: '' },
-    { x: 320, y: 270, r: 18, label: '自我' },
-    { x: 150, y: 330, r: 11, label: '' },
-    { x: 280, y: 340, r: 13, label: '心流' },
-  ];
-  const edges = [
-    [0, 1], [0, 2], [0, 4], [1, 3], [2, 5], [4, 5], [4, 6], [5, 7], [6, 7], [1, 4],
-  ];
-
-  return (
-    <div style={{padding: '24px 22px 0', height: '100%', background: PAPER, display:'flex', flexDirection:'column'}}>
-      <div style={{opacity: headerOp, marginBottom: 14}}>
-        <div style={{fontFamily: mono, fontSize: 10, color: ASH, letterSpacing:'0.25em', marginBottom: 4}}>
-          287 条 · 4 个聚类
-        </div>
-        <div style={{fontFamily: serif, fontSize: 34, fontWeight: 500, color: INK, lineHeight: 1, letterSpacing:'-0.01em'}}>
-          记忆
         </div>
-      </div>
 
-      {/* Graph visualization */}
-      <div style={{
-        opacity: graphOp, border:`1px solid ${LINE}`, background:'#fff',
-        height: 400, position:'relative', overflow:'hidden', marginBottom: 14,
-      }}>
-        <svg viewBox="0 0 420 400" width="100%" height="100%" style={{display:'block'}}>
-          {/* edges */}
-          {edges.map(([a, b], i) => {
-            const na = nodes[a], nb = nodes[b];
-            return <line key={i} x1={na.x} y1={na.y} x2={nb.x} y2={nb.y}
-              stroke="#c8beb0" strokeWidth={0.8} opacity={0.7} />;
-          })}
-          {/* nodes */}
-          {nodes.map((n, i) => {
-            const appear = interpolate(animateT, [0.2 + i * 0.04, 0.4 + i * 0.04], [0, 1], Easing.easeOut);
-            return (
-              <g key={i} opacity={appear}>
-                <circle cx={n.x} cy={n.y} r={n.r}
-                  fill={n.emph ? TERRA : '#ede5d3'}
-                  stroke={n.emph ? TERRA : '#b8ac94'}
-                  strokeWidth={1} />
-                {n.label && (
-                  <text x={n.x} y={n.y + n.r + 14} textAnchor="middle"
-                    fontFamily={serif} fontStyle="italic" fontSize={11}
-                    fill={n.emph ? TERRA : '#666'}>
-                    {n.label}
-                  </text>
-                )}
-              </g>
-            );
-          })}
-        </svg>
-        {/* corner label */}
-        <div style={{position:'absolute', top: 12, left: 14,
-          fontFamily: mono, fontSize: 9, color: ASH, letterSpacing:'0.2em'}}>
-          · 图谱视图
-        </div>
-        <div style={{position:'absolute', bottom: 12, right: 14,
-          fontFamily: mono, fontSize: 9, color: TERRA, letterSpacing:'0.2em'}}>
-          斯多葛派 · 47 条
-        </div>
-      </div>
+        <div class="island"></div>
 
-      {/* Top clusters */}
-      <div style={{opacity: listOp}}>
-        <div style={{fontFamily: mono, fontSize: 9, color: ASH, letterSpacing:'0.25em', marginBottom: 8}}>
-          主要聚类
-        </div>
-        {[
-          { name: '斯多葛', count: 47, swatch: TERRA },
-          { name: '注意力', count: 32, swatch: OLIVE },
-        ].map((c, i) => (
-          <div key={i} style={{padding: '9px 0', borderTop: i === 0 ? `1px solid ${LINE}` : 'none', borderBottom: `1px solid ${LINE}`, display:'flex', alignItems:'center', gap: 10}}>
-            <div style={{width: 14, height: 14, background: c.swatch, borderRadius: 2}} />
-            <div style={{flex: 1, fontFamily: serif, fontSize: 14, color: INK}}>{c.name}</div>
-            <div style={{fontFamily: mono, fontSize: 10, color: ASH}}>{c.count}</div>
+        <div class="content">
+
+          <!-- 1. Wireframe -->
+          <div class="screen-view" id="view-wire">
+            <div class="wire">
+              <div class="ghost g1"></div>
+              <div class="ghost g2"></div>
+              <div class="ghost g3"></div>
+              <div class="ghost g4"></div>
+              <div class="ghost g5"></div>
+            </div>
           </div>
-        ))}
-      </div>
-    </div>
-  );
-}
-
-// ── Screen: Chat ─────────────────────────────────────────
-function ChatScreen({ animateT = 1 }) {
-  const headerOp = interpolate(animateT, [0, 0.2], [0, 1]);
-  const msg1Op = interpolate(animateT, [0.15, 0.35], [0, 1]);
-  const msg2Op = interpolate(animateT, [0.4, 0.65], [0, 1]);
-  const ctxCardOp = interpolate(animateT, [0.55, 0.75], [0, 1]);
-  const msg3Op = interpolate(animateT, [0.7, 0.92], [0, 1]);
-  const inputOp = interpolate(animateT, [0.5, 0.8], [0, 1]);
-
-  // Typewriter for AI reply (msg2)
-  const aiText = '两处呼应——马可谈的「内心堡垒」和塞涅卡第 28 封信中的独处。';
-  const charCount = Math.floor(interpolate(animateT, [0.4, 0.7], [0, aiText.length]));
-  const typed = aiText.slice(0, charCount);
-
-  return (
-    <div style={{padding: '24px 22px 0', height: '100%', background: PAPER, display:'flex', flexDirection:'column'}}>
-      <div style={{opacity: headerOp, marginBottom: 16}}>
-        <div style={{fontFamily: mono, fontSize: 10, color: ASH, letterSpacing:'0.25em', marginBottom: 4}}>
-          流明 · 已关联
-        </div>
-        <div style={{fontFamily: serif, fontSize: 30, fontWeight: 500, color: INK, lineHeight: 1, letterSpacing:'-0.01em'}}>
-          问你的记忆
-        </div>
-      </div>
 
-      <div style={{flex: 1, display:'flex', flexDirection:'column', gap: 12}}>
-        {/* User msg */}
-        <div style={{opacity: msg1Op, alignSelf:'flex-end', maxWidth: '85%',
-          background:'#f0e8d5', padding: '10px 14px', borderRadius: '18px 18px 4px 18px'}}>
-          <div style={{fontFamily: serif, fontSize: 14, color: INK, lineHeight: 1.4}}>
-            我最近关于<span style={{fontStyle:'italic'}}>「独处」</span>在想什么?
+          <!-- 2. Home -->
+          <div class="screen-view" id="view-home">
+            <div class="home-screen">
+              <div class="kicker">POMODORO · 专注</div>
+              <div class="title">下一件要做的事</div>
+              <div class="time-big">25<span class="sep">:</span>00</div>
+              <div class="sub">写完这一节,休息 5 分钟</div>
+              <div class="cta">开始专注</div>
+            </div>
           </div>
-        </div>
 
-        {/* AI reply (typewriter) */}
-        <div style={{opacity: msg2Op, alignSelf:'flex-start', maxWidth: '90%',
-          paddingLeft: 14, borderLeft: `2px solid ${TERRA}`}}>
-          <div style={{fontFamily: mono, fontSize: 9, color: TERRA, letterSpacing:'0.2em', marginBottom: 4}}>
-            流明
+          <!-- 3. Timer -->
+          <div class="screen-view" id="view-timer">
+            <div class="timer-screen">
+              <div class="phase">FOCUS · 第 1 轮</div>
+              <div class="ring-wrap">
+                <svg viewBox="0 0 320 320">
+                  <circle class="bg-ring" cx="160" cy="160" r="140"/>
+                  <circle class="fg-ring" id="fgRing" cx="160" cy="160" r="140"/>
+                </svg>
+                <div class="ring-label">
+                  <div class="rl-time" id="ringTime">24:12</div>
+                  <div class="rl-tag">剩余</div>
+                </div>
+              </div>
+              <div class="actions">
+                <div class="act-btn">暂停</div>
+                <div class="act-btn primary">跳过</div>
+              </div>
+            </div>
           </div>
-          <div style={{fontFamily: serif, fontSize: 14, color: INK, lineHeight: 1.45, minHeight: 60}}>
-            {typed}
-            {charCount < aiText.length && charCount > 0 && (
-              <span style={{color: TERRA, marginLeft: 2}}>|</span>
-            )}
+
+          <!-- 4. Stats -->
+          <div class="screen-view" id="view-stats">
+            <div class="stats-screen">
+              <div class="stats-label">本周 · 统计</div>
+              <div class="stats-hero">23<span class="unit">轮</span></div>
+              <div class="stats-sub">比上周多出 5 轮</div>
+              <div class="chart" id="chart">
+                <div class="bar dim" style="height:30%"></div>
+                <div class="bar" style="height:52%"></div>
+                <div class="bar" style="height:70%"></div>
+                <div class="bar" style="height:42%"></div>
+                <div class="bar" style="height:86%"></div>
+                <div class="bar" style="height:95%"></div>
+                <div class="bar" style="height:64%"></div>
+              </div>
+              <div class="chart-x">
+                <span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span><span>S</span>
+              </div>
+            </div>
           </div>
-        </div>
 
-        {/* Context card */}
-        <div style={{opacity: ctxCardOp, alignSelf:'flex-start', maxWidth: '88%',
-          background:'#fff', border: `1px solid ${LINE}`, padding: '10px 12px',
-          marginLeft: 14, display:'flex', gap: 10, alignItems:'center'}}>
-          <div style={{width: 40, height: 40, flexShrink: 0, overflow:'hidden'}}>
-            <ArtBlock mood="study" height={40} />
+          <!-- 5. Settings -->
+          <div class="screen-view" id="view-settings">
+            <div class="settings-screen">
+              <div class="title-row">设置</div>
+              <div class="list">
+                <div class="row">
+                  <span class="k">专注时长</span>
+                  <span class="v">25 MIN</span>
+                </div>
+                <div class="row">
+                  <span class="k">白噪音</span>
+                  <div class="toggle"></div>
+                </div>
+                <div class="row">
+                  <span class="k">提醒铃声</span>
+                  <div class="toggle off"></div>
+                </div>
+                <div class="row">
+                  <span class="k">主题</span>
+                  <span class="v">CLAUDE MIST</span>
+                </div>
+              </div>
+            </div>
           </div>
-          <div style={{flex: 1}}>
-            <div style={{fontFamily: serif, fontSize: 12, fontWeight: 500, color: INK, lineHeight: 1.2}}>
-              塞涅卡 · 第 28 封信
+
+          <!-- Tab bar (shared, appears on home/stats/settings) -->
+          <div class="tab-bar" id="tabBar" style="display:none">
+            <div class="tab active" data-tab="home">
+              <div class="ico"></div>
+              <span>HOME</span>
+            </div>
+            <div class="tab" data-tab="timer">
+              <div class="ico"></div>
+              <span>TIMER</span>
             </div>
-            <div style={{fontFamily: mono, fontSize: 8, color: ASH, marginTop: 2, letterSpacing:'0.15em'}}>
-              3 周前阅读 · 4 分钟
+            <div class="tab" data-tab="stats">
+              <div class="ico"></div>
+              <span>STATS</span>
+            </div>
+            <div class="tab" data-tab="settings">
+              <div class="ico"></div>
+              <span>SET</span>
             </div>
           </div>
-          <div style={{fontFamily: serif, fontSize: 16, color: ASH, fontStyle:'italic'}}>↗</div>
-        </div>
 
-        {/* User follow-up */}
-        <div style={{opacity: msg3Op, alignSelf:'flex-end', maxWidth: '70%',
-          background:'#f0e8d5', padding: '10px 14px', borderRadius: '18px 18px 4px 18px'}}>
-          <div style={{fontFamily: serif, fontSize: 14, color: INK}}>
-            给我看原文段落。
-          </div>
         </div>
-      </div>
 
-      {/* Input bar */}
-      <div style={{opacity: inputOp, padding: '10px 0 16px',
-        borderTop: `1px solid ${LINE}`, marginTop: 12,
-        display:'flex', alignItems:'center', gap: 10}}>
-        <div style={{flex: 1, fontFamily: serif, fontStyle:'italic',
-          fontSize: 13, color: ASH}}>
-          从你的阅读里问我任何事…
-        </div>
-        <div style={{width: 28, height: 28, background: TERRA, borderRadius: '50%',
-          display:'flex', alignItems:'center', justifyContent:'center',
-          color:'#fff', fontFamily: sans, fontSize: 16}}>↑</div>
-      </div>
-    </div>
-  );
-}
-
-// ── Tab bar ───────────────────────────────────────────────
-function TabBar({ active = 'today', tapping = null }) {
-  const tabs = [
-    { id: 'today', label: '今日' },
-    { id: 'memory', label: '记忆' },
-    { id: 'chat', label: '对话' },
-  ];
-  return (
-    <div style={{
-      position: 'absolute', bottom: 0, left: 0, right: 0,
-      height: 72, background: 'rgba(253,251,245,0.95)',
-      backdropFilter: 'blur(12px)',
-      borderTop: `1px solid ${LINE}`,
-      display: 'flex', alignItems: 'center',
-      fontFamily: serif,
-    }}>
-      {tabs.map((t) => {
-        const isActive = active === t.id;
-        const isTapping = tapping === t.id;
-        return (
-          <div key={t.id} style={{
-            flex: 1, textAlign:'center', position:'relative',
-            padding: '12px 0 18px',
-          }}>
-            {/* Ripple */}
-            {isTapping !== null && isTapping > 0 && isTapping < 1 && (
-              <div style={{
-                position:'absolute', top:'50%', left:'50%',
-                transform: `translate(-50%, -50%) scale(${1 + isTapping * 2})`,
-                width: 44, height: 44, borderRadius:'50%',
-                background: TERRA, opacity: 0.25 * (1 - isTapping),
-                pointerEvents:'none',
-              }} />
-            )}
-            <div style={{
-              fontSize: 15, fontWeight: isActive ? 600 : 400,
-              fontStyle: isActive ? 'normal' : 'italic',
-              color: isActive ? TERRA : ASH,
-              letterSpacing: '0.02em',
-            }}>
-              {t.label}
-            </div>
-            {isActive && (
-              <div style={{
-                position:'absolute', bottom: 8, left:'50%',
-                transform: 'translateX(-50%)',
-                width: 18, height: 2, background: TERRA,
-              }} />
-            )}
-          </div>
-        );
-      })}
-    </div>
-  );
-}
-
-// ── Scene composition ─────────────────────────────────────
-// Timeline:
-//   0.0 – 1.8  iPhone fade+bounce in
-//   1.8 – 7.5  Today screen (fills in)
-//   7.5 – 8.5  Tap on Memory tab (ripple)
-//   8.5 – 13.5 Memory screen
-//   13.5 – 14.5 Tap on Chat tab
-//   14.5 – 19.5 Chat screen
-//   19.5 – 21.5 Pan: phone shrinks + capability labels appear
-//   21.5 – 24.0 Hold final frame with labels
-function App() {
-  return (
-    <Stage duration={24} width={1920} height={1080} bgColor={CREAM}>
-      <MainComposition />
-    </Stage>
-  );
-}
-
-function MainComposition() {
-  const time = useTime();
-
-  // Phone entrance
-  const entranceT = Math.min(1, Math.max(0, time / 1.8));
-  const phoneOp = interpolate(entranceT, [0, 0.5], [0, 1]);
-  const phoneScale = interpolate(entranceT, [0, 1], [0.82, 0.88], Easing.spring);
-
-  // Pan-out in final scene (19.5 – 21.5)
-  const panT = Math.min(1, Math.max(0, (time - 19.5) / 2));
-  const finalScale = interpolate(panT, [0, 1], [0.88, 0.68], Easing.easeInOut);
-  const finalX = interpolate(panT, [0, 1], [0, -200], Easing.easeInOut);
-
-  const currentScale = panT > 0 ? finalScale : phoneScale;
-  const currentX = panT > 0 ? finalX : 0;
-
-  // Screen determination
-  let activeScreen = 'today';
-  let tapping = null; // { id: 'memory', t: 0..1 }
-  let screenAnimateT = 1;
-  let transitionProgress = 0;
-
-  if (time < 7.5) {
-    activeScreen = 'today';
-    screenAnimateT = Math.min(1, Math.max(0, (time - 1.8) / 2.5));
-  } else if (time < 8.5) {
-    activeScreen = 'today';
-    tapping = { id: 'memory', t: (time - 7.5) / 1.0 };
-    transitionProgress = (time - 8.0) / 0.5; // slide starts at 8.0
-  } else if (time < 13.5) {
-    activeScreen = 'memory';
-    screenAnimateT = Math.min(1, Math.max(0, (time - 8.5) / 2.5));
-  } else if (time < 14.5) {
-    activeScreen = 'memory';
-    tapping = { id: 'chat', t: (time - 13.5) / 1.0 };
-    transitionProgress = (time - 14.0) / 0.5;
-  } else if (time < 19.5) {
-    activeScreen = 'chat';
-    screenAnimateT = Math.min(1, Math.max(0, (time - 14.5) / 2.5));
-  } else {
-    activeScreen = 'chat';
-    screenAnimateT = 1;
-  }
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM}}>
-      {/* Phone */}
-      <div style={{
-        position: 'absolute', top: '50%', left: '50%',
-        transform: `translate(calc(-50% + ${currentX}px), -50%) scale(${currentScale})`,
-        opacity: phoneOp, transformOrigin: 'center center',
-      }}>
-        <IosFrame>
-          <div style={{position:'relative', width:'100%', height:'100%'}}>
-            {activeScreen === 'today' && <TodayScreen animateT={screenAnimateT} />}
-            {activeScreen === 'memory' && <MemoryScreen animateT={screenAnimateT} />}
-            {activeScreen === 'chat' && <ChatScreen animateT={screenAnimateT} />}
-            <TabBar active={activeScreen} tapping={tapping ? tapping.t : null} />
-          </div>
-        </IosFrame>
-      </div>
+        <div class="home-indicator"></div>
 
-      {/* Capability labels (appear during pan-out 19.5+) */}
-      {panT > 0.05 && <CapabilityLabels t={panT} />}
-
-      {/* Masthead (tucked corner, always visible from ~2s) */}
-      {time > 2 && time < 19.5 && (
-        <div style={{
-          position: 'absolute', top: 60, left: 80,
-          opacity: Math.min(1, (time - 2) / 0.6),
-          maxWidth: 420,
-        }}>
-          <div style={{fontFamily: mono, fontSize: 12, color: TERRA, letterSpacing:'0.3em', marginBottom: 10}}>
-            iOS APP 原型
-          </div>
-          <div style={{fontFamily: serif, fontSize: 70, fontWeight: 500, color: INK, lineHeight: 1.05, letterSpacing:'-0.015em'}}>
-            真机。<br/>
-            <span style={{fontStyle:'italic', color: TERRA}}>真</span>交互。
-          </div>
-          <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 20, color: ASH, marginTop: 22, lineHeight: 1.55}}>
-            iPhone 15 Pro 机身 · 灵动岛 · 状态驱动多屏<br/>
-            AI 密度信息 · CSS 艺术 · Playwright 点击测试
-          </div>
+        <!-- Tap overlay (inside screen so z-index > content) -->
+        <div class="tap" id="tap">
+          <div class="ring"></div>
+          <div class="core"></div>
         </div>
-      )}
-
-      {/* Screen label (bottom) */}
-      {time > 2 && time < 19.5 && (
-        <ScreenLabel active={activeScreen} time={time} />
-      )}
-
-      {/* Watermark */}
-      <div style={{position:'absolute', bottom: 24, right: 32,
-        fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
-        fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
-        Created by Huashu-Design
+
       </div>
     </div>
-  );
-}
-
-function ScreenLabel({ active, time }) {
-  const label = { today: '屏幕 1 · 今日', memory: '屏幕 2 · 记忆', chat: '屏幕 3 · 对话' }[active];
-  const idx = { today: 1, memory: 2, chat: 3 }[active];
-  return (
-    <div style={{
-      position: 'absolute', bottom: 80, left: 80,
-      fontFamily: mono, fontSize: 11, color: ASH, letterSpacing:'0.25em',
-      opacity: 0.9,
-    }}>
-      <span style={{color: TERRA, marginRight: 12}}>0{idx}</span>
-      <span>{label.toUpperCase()}</span>
-    </div>
-  );
-}
-
-function CapabilityLabels({ t }) {
-  const labels = [
-    { text: '真图 · Wikimedia / Met / Unsplash', y: 220, delay: 0.0 },
-    { text: 'Inline React · 双击就开', y: 380, delay: 0.15 },
-    { text: 'AppPhone · 状态驱动多屏切换', y: 540, delay: 0.30 },
-    { text: '信息密度型 · 每屏 ≥ 3 处差异化', y: 700, delay: 0.45 },
-    { text: 'Playwright · 交付前点击测试', y: 860, delay: 0.60 },
+  </div>
+
+  <!-- Brand reveal -->
+  <div class="brand-wall" id="brandWall">
+    <div class="brand-wordmark" id="brandWord">huashu<span class="dot">·</span>design</div>
+    <div class="brand-underline" id="brandLine"></div>
+    <div class="brand-cn" id="brandCn">说一句话 · 拿回一个 App</div>
+  </div>
+
+</div>
+
+<script>
+(() => {
+  // ── Scale to viewport (1920×1080 canvas) ─────────────────────────
+  function fit() {
+    const stage = document.getElementById('stage');
+    const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  fit();
+  window.addEventListener('resize', fit);
+
+  // ── Easing ───────────────────────────────────────────────────────
+  const expoOut = t => (t <= 0 ? 0 : t >= 1 ? 1 : 1 - Math.pow(2, -10 * t));
+  const expoIn  = t => (t <= 0 ? 0 : t >= 1 ? 1 : Math.pow(2, 10 * (t - 1)));
+  const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;
+  const cubicOut = t => 1 - Math.pow(1 - t, 3);
+  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
+  const lerp = (a, b, t) => a + (b - a) * t;
+
+  // Animate a value by requestAnimationFrame between timeline markers
+  function seg(t, start, end) {
+    return clamp((t - start) / (end - start), 0, 1);
+  }
+
+  // ── Elements ─────────────────────────────────────────────────────
+  const el = (id) => document.getElementById(id);
+  const terminal = el('terminal');
+  const comment  = el('comment');
+  const typed    = el('typed');
+  const ttyCursor = el('ttyCursor');
+  const connector = el('connector');
+  const phoneWrap = el('phoneWrap');
+  const views = {
+    wire: el('view-wire'),
+    home: el('view-home'),
+    timer: el('view-timer'),
+    stats: el('view-stats'),
+    settings: el('view-settings'),
+  };
+  const tap = el('tap');
+  const tabBar = el('tabBar');
+  const fgRing = el('fgRing');
+  const ringTime = el('ringTime');
+  const brandWall = el('brandWall');
+  const brandWord = el('brandWord');
+  const brandLine = el('brandLine');
+  const brandCn = el('brandCn');
+
+  // Typing text
+  const typeStr = 'make a pomodoro app';
+  function setTyping(progress) {
+    const n = Math.floor(typeStr.length * progress);
+    typed.textContent = typeStr.slice(0, n);
+  }
+
+  // Show/hide views — hard swap (no cross-fade overlap)
+  function showView(name) {
+    Object.keys(views).forEach(k => {
+      const isActive = (k === name);
+      views[k].style.opacity = isActive ? '1' : '0';
+      views[k].style.visibility = isActive ? 'visible' : 'hidden';
+      views[k].style.transform = isActive ? 'translateY(0)' : 'translateY(0)';
+      views[k].style.transition = isActive ? 'opacity 0.22s ease-out' : 'none';
+    });
+  }
+
+  // Active tab
+  function setActiveTab(name) {
+    document.querySelectorAll('.tab-bar .tab').forEach(t => {
+      t.classList.toggle('active', t.dataset.tab === name);
+    });
+  }
+
+  // Play tap at screen coords (relative to .screen: 416×876)
+  function playTap(x, y) {
+    tap.style.left = (x - 32) + 'px';
+    tap.style.top = (y - 32) + 'px';
+    tap.style.opacity = '1';
+    // restart keyframe animation
+    const ring = tap.querySelector('.ring');
+    ring.style.animation = 'none';
+    ring.offsetHeight; // reflow
+    ring.style.animation = '';
+    // fade out
+    setTimeout(() => { tap.style.opacity = '0'; }, 550);
+  }
+
+  // ── SFX via WebAudio ─────────────────────────────────────────────
+  let audioCtx = null;
+  function ac() {
+    if (!audioCtx) {
+      try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e){}
+    }
+    return audioCtx;
+  }
+  function sfxClick(vol = 0.16) {
+    const c = ac(); if (!c) return;
+    const o = c.createOscillator();
+    const g = c.createGain();
+    o.type = 'square';
+    o.frequency.setValueAtTime(1200, c.currentTime);
+    o.frequency.exponentialRampToValueAtTime(500, c.currentTime + 0.04);
+    g.gain.setValueAtTime(vol, c.currentTime);
+    g.gain.exponentialRampToValueAtTime(0.001, c.currentTime + 0.05);
+    o.connect(g); g.connect(c.destination);
+    o.start(); o.stop(c.currentTime + 0.06);
+  }
+  function sfxEnter() {
+    const c = ac(); if (!c) return;
+    const o = c.createOscillator();
+    const g = c.createGain();
+    o.type = 'sine';
+    o.frequency.setValueAtTime(180, c.currentTime);
+    o.frequency.exponentialRampToValueAtTime(440, c.currentTime + 0.25);
+    g.gain.setValueAtTime(0.22, c.currentTime);
+    g.gain.exponentialRampToValueAtTime(0.001, c.currentTime + 0.3);
+    o.connect(g); g.connect(c.destination);
+    o.start(); o.stop(c.currentTime + 0.32);
+  }
+  function sfxChime() {
+    const c = ac(); if (!c) return;
+    [523.25, 783.99].forEach((f, i) => {
+      const o = c.createOscillator();
+      const g = c.createGain();
+      o.type = 'sine';
+      o.frequency.value = f;
+      g.gain.setValueAtTime(0, c.currentTime + i * 0.08);
+      g.gain.linearRampToValueAtTime(0.18, c.currentTime + i * 0.08 + 0.04);
+      g.gain.exponentialRampToValueAtTime(0.001, c.currentTime + i * 0.08 + 1.2);
+      o.connect(g); g.connect(c.destination);
+      o.start(c.currentTime + i * 0.08);
+      o.stop(c.currentTime + i * 0.08 + 1.25);
+    });
+  }
+
+  // ── Timeline ─────────────────────────────────────────────────────
+  const DURATION = 10.0;
+
+  const sfxFired = new Set();
+  function fireOnce(id, fn) {
+    if (sfxFired.has(id)) return;
+    sfxFired.add(id);
+    fn();
+  }
+
+  // Screen switch schedule (within Beat 2, 2.0s → 8.0s)
+  // Tap coords are relative to the 416×876 .screen
+  const schedule = [
+    { t: 2.0, view: 'wire',     tabIco: null,       tap: null },
+    { t: 3.1, view: 'home',     tabIco: 'home',     tap: null },                 // home materializes (no tap — it's the fill moment)
+    { t: 4.4, view: 'timer',    tabIco: 'timer',    tap: {x: 208, y: 624} },     // tap "开始专注" CTA
+    { t: 6.3, view: 'stats',    tabIco: 'stats',    tap: {x: 300, y: 810} },     // tap stats tab
+    { t: 7.5, view: 'settings', tabIco: 'settings', tap: {x: 370, y: 810} },     // tap settings tab
   ];
-  return (
-    <>
-      {labels.map((l, i) => {
-        const localT = Math.max(0, Math.min(1, (t - l.delay) / 0.35));
-        const op = localT;
-        const x = interpolate(localT, [0, 1], [1400, 1280], Easing.easeOut);
-        return (
-          <div key={i} style={{
-            position: 'absolute', left: x, top: l.y,
-            opacity: op, display:'flex', alignItems:'center', gap: 14,
-          }}>
-            <div style={{width: 60, height: 1, background: TERRA}} />
-            <div>
-              <div style={{fontFamily: mono, fontSize: 10, color: TERRA, letterSpacing:'0.25em', marginBottom: 3}}>
-                0{i + 1}
-              </div>
-              <div style={{fontFamily: serif, fontSize: 20, color: INK, lineHeight: 1.25, letterSpacing:'-0.005em'}}>
-                {l.text}
-              </div>
-            </div>
-          </div>
-        );
-      })}
-    </>
-  );
-}
+  let scheduleIdx = 0;
+
+  let startTime = null;
+  let raf = null;
+
+  function tick(now) {
+    if (!startTime) startTime = now;
+    const t = (now - startTime) / 1000;
+
+    // ── Beat 1: 0-2s ─────────────────────────────────────────
+    // Terminal fade in (0 → 0.4s)
+    {
+      const k = expoOut(seg(t, 0.0, 0.4));
+      terminal.style.opacity = k;
+      terminal.style.transform = `translateY(-50%) translateX(${lerp(-30, 0, k)}px)`;
+    }
+    // iPhone fade in (0.2 → 0.9s)
+    {
+      const k = expoOut(seg(t, 0.2, 0.9));
+      phoneWrap.style.opacity = k;
+      phoneWrap.style.transform = `translateY(-50%) translateX(${lerp(60, 0, k)}px) scale(${lerp(0.96, 1, k)})`;
+      if (t > 0.25) fireOnce('enter', sfxEnter);
+    }
+    // Connector fade
+    {
+      const k = expoOut(seg(t, 0.7, 1.2));
+      connector.style.opacity = k;
+      connector.style.transform = `translateY(-50%) scaleX(${k})`;
+    }
+    // Comment
+    {
+      const k = expoOut(seg(t, 0.8, 1.2));
+      comment.style.opacity = k * 0.82;
+    }
+    // Typing (0.6 → 1.9s)
+    {
+      const k = cubicInOut(seg(t, 0.6, 1.9));
+      setTyping(k);
+      // key click SFX at certain progress points
+      if (t > 0.8 && t < 1.85) {
+        const charsShown = Math.floor(typeStr.length * k);
+        const key = 'typ' + charsShown;
+        if (!sfxFired.has(key) && charsShown > 0 && charsShown % 3 === 0) {
+          fireOnce(key, () => sfxClick(0.08));
+        }
+      }
+    }
+    // Hide cursor when typing done
+    ttyCursor.style.opacity = t > 1.85 ? '0' : '1';
 
-ReactDOM.createRoot(document.getElementById('root')).render(<App />);
+    // ── Beat 2: 2-8s ─────────────────────────────────────────
+    // Execute scheduled screen transitions
+    while (scheduleIdx < schedule.length && t >= schedule[scheduleIdx].t) {
+      const s = schedule[scheduleIdx];
+      showView(s.view);
+      // status bar color: dark-text on light screens, but wire also light, keep dark
+      if (s.view === 'wire') {
+        tabBar.style.display = 'none';
+      } else {
+        tabBar.style.display = 'flex';
+        setActiveTab(s.tabIco);
+      }
+      if (s.tap) {
+        // small delay so tap appears at moment of switch
+        setTimeout(() => playTap(s.tap.x, s.tap.y), 120);
+        if (s.view !== 'wire') fireOnce('click_' + s.view, () => sfxClick(0.18));
+      }
+      scheduleIdx++;
+    }
+
+    // Timer ring animation: once timer appears (4.4s), animate ring from empty → 42% filled
+    if (t >= 4.4 && t < 6.3) {
+      const ringT = clamp((t - 4.5) / 1.2, 0, 1);
+      const fillPct = expoOut(ringT) * 0.42;
+      const offset = 880 * (1 - fillPct);
+      // Set as both style AND attr so neither overrides the other
+      fgRing.style.strokeDashoffset = offset;
+      fgRing.setAttribute('stroke-dashoffset', offset);
+      // Count down visually: 24:12 → 14:03
+      const mins = Math.floor(lerp(24, 14, expoOut(ringT)));
+      const secs = Math.floor(lerp(12, 3, expoOut(ringT)));
+      ringTime.textContent = String(mins).padStart(2,'0') + ':' + String(secs).padStart(2,'0');
+    }
+
+    // ── Beat 3: 8-10s ────────────────────────────────────────
+    // Phone + terminal fade out fast (7.5 → 7.9) so wall doesn't guillotine
+    if (t >= 7.5) {
+      const k = cubicOut(seg(t, 7.5, 7.9));
+      phoneWrap.style.opacity = String(1 - k);
+      phoneWrap.style.transform = `translateY(-50%) scale(${lerp(1, 0.94, k)})`;
+      terminal.style.opacity = String(1 - k);
+      terminal.style.transform = `translateY(-50%) scale(${lerp(1, 0.96, k)})`;
+      connector.style.opacity = String(1 - k);
+    }
+    // Brand wall slides up (7.9 → 8.6) — starts AFTER phone is gone
+    {
+      const k = expoOut(seg(t, 7.9, 8.6));
+      brandWall.style.transform = `translateY(${lerp(100, 0, k)}%)`;
+      brandWall.style.opacity = k > 0 ? '1' : '0';
+      const watermark = document.querySelector('.watermark');
+      if (k > 0.6) watermark.classList.add('on-light');
+      else watermark.classList.remove('on-light');
+    }
+    // Wordmark appears
+    {
+      const k = expoOut(seg(t, 8.5, 9.2));
+      brandWord.style.opacity = k;
+      brandWord.style.transform = `scale(${lerp(0.92, 1, k)})`;
+      if (t > 8.55) fireOnce('chime', sfxChime);
+    }
+    // Underline
+    {
+      const k = expoOut(seg(t, 9.0, 9.6));
+      brandLine.style.width = (280 * k) + 'px';
+    }
+    // CN label
+    {
+      const k = cubicOut(seg(t, 9.3, 9.9));
+      brandCn.style.opacity = k * 0.9;
+    }
+
+    if (t < DURATION) {
+      raf = requestAnimationFrame(tick);
+    } else {
+      // Hold final frame
+      if (!window.__recording) {
+        // loop for preview
+        setTimeout(() => {
+          startTime = null;
+          scheduleIdx = 0;
+          sfxFired.clear();
+          // Reset views
+          showView('wire');
+          tabBar.style.display = 'none';
+          fgRing.style.strokeDashoffset = 880;
+          fgRing.setAttribute('stroke-dashoffset', 880);
+          ringTime.textContent = '24:12';
+          // Reset brand
+          brandWall.style.transform = 'translateY(100%)';
+          brandWall.style.opacity = '0';
+          brandWord.style.opacity = '0';
+          brandWord.style.transform = 'scale(0.92)';
+          brandLine.style.width = '0';
+          brandCn.style.opacity = '0';
+          // Reset terminal typing
+          typed.textContent = '';
+          ttyCursor.style.opacity = '1';
+          comment.style.opacity = '0';
+          terminal.style.opacity = '0';
+          phoneWrap.style.opacity = '0';
+          connector.style.opacity = '0';
+          document.querySelector('.watermark').classList.remove('on-light');
+          raf = requestAnimationFrame(tick);
+        }, 600);
+      }
+    }
+  }
+
+  // seek(0) helper for render-video.js
+  window.__seek = function(s) {
+    startTime = performance.now() - s * 1000;
+  };
+
+  // Initial state
+  showView('wire');
+  tabBar.style.display = 'none';
+
+  // Wait for fonts, then start animation
+  (document.fonts ? document.fonts.ready : Promise.resolve()).then(() => {
+    requestAnimationFrame((now) => {
+      startTime = now;
+      window.__ready = true;
+      raf = requestAnimationFrame(tick);
+    });
+  });
+})();
 </script>
+
 </body>
 </html>

+ 1055 - 0
demos/c2-slides-pptx-en.html

@@ -0,0 +1,1055 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8" />
+<title>c2-slides-pptx · English · v2</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<style>
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --dim: rgba(255,255,255,0.18);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;
+    --accent-deep: #B85D3D;
+    --cd-bg: #F5F4F0;
+    --cd-panel: #FFFFFF;
+    --cd-ink: #1A1918;
+    --cd-dim: #8B867E;
+    --cd-hair: rgba(0,0,0,0.08);
+
+    --serif-cn: "Source Serif 4", Georgia, serif;
+    --serif-en: "Source Serif 4", Georgia, serif;
+    --sans: "Inter", -apple-system, system-ui, sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
+    -webkit-font-smoothing: antialiased;
+  }
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+  }
+
+  /* Film grain (2% opacity) */
+  .stage::after {
+    content: '';
+    position: absolute; inset: 0;
+    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.5 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
+    opacity: 0.025;
+    pointer-events: none;
+    mix-blend-mode: overlay;
+    z-index: 200;
+  }
+
+  .watermark-tl {
+    position: absolute;
+    top: 40px; left: 56px;
+    font-family: var(--mono);
+    font-size: 14px;
+    letter-spacing: 0.2em;
+    text-transform: uppercase;
+    color: rgba(255,255,255,0.16);
+    z-index: 180;
+    pointer-events: none;
+  }
+
+  /* ====== Beat 1: browser-fullscreen deck ====== */
+  .beat1 {
+    position: absolute; inset: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    opacity: 1;
+  }
+
+  .deck-window {
+    width: 1400px;
+    height: 788px;
+    border-radius: 14px;
+    background: #101010;
+    border: 1px solid var(--hairline);
+    box-shadow: 0 40px 120px -30px rgba(217,119,87,0.18),
+                0 0 0 1px rgba(255,255,255,0.03);
+    position: relative;
+    will-change: transform, opacity;
+  }
+  .deck-window .deck-body-wrap {
+    position: absolute;
+    top: 44px; left: 0; right: 0; bottom: 0;
+    border-radius: 0 0 14px 14px;
+    overflow: hidden;
+    background: #0A0A0A;
+  }
+  .deck-chrome {
+    height: 44px;
+    background: #161616;
+    border-bottom: 1px solid var(--hairline);
+    display: flex;
+    align-items: center;
+    padding: 0 18px;
+    gap: 14px;
+  }
+  .deck-chrome .traffic {
+    display: flex; gap: 8px;
+  }
+  .deck-chrome .traffic .d {
+    width: 11px; height: 11px; border-radius: 50%;
+    background: var(--hairline);
+  }
+  .deck-chrome .url {
+    flex: 1;
+    text-align: center;
+    font-family: var(--mono);
+    font-size: 12px;
+    color: var(--muted);
+    letter-spacing: 0.02em;
+  }
+  .deck-chrome .page-count {
+    font-family: var(--mono);
+    font-size: 13px;
+    color: var(--accent);
+    letter-spacing: 0.08em;
+    min-width: 60px;
+    text-align: right;
+  }
+
+  .deck-slide {
+    position: absolute;
+    top: 0; left: 0;
+    width: 100%;
+    height: 100%;
+    background: #0A0A0A;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    padding: 96px 120px;
+    will-change: transform, opacity;
+  }
+  .deck-slide .eyebrow {
+    font-family: var(--mono);
+    font-size: 14px;
+    color: var(--accent);
+    letter-spacing: 0.24em;
+    text-transform: uppercase;
+    margin-bottom: 24px;
+  }
+  .deck-slide h1 {
+    font-family: var(--serif-cn);
+    font-size: 92px;
+    font-weight: 500;
+    line-height: 1.08;
+    color: var(--ink);
+    margin: 0 0 28px 0;
+    letter-spacing: -0.01em;
+  }
+  .deck-slide .sub {
+    font-family: var(--sans);
+    font-size: 22px;
+    color: var(--ink-60);
+    line-height: 1.5;
+    max-width: 780px;
+  }
+  .deck-slide .hairline {
+    margin-top: 48px;
+    width: 80px;
+    height: 2px;
+    background: var(--accent);
+  }
+
+  /* Key press indicator — sits below the window */
+  .key-hint {
+    position: absolute;
+    top: calc(50% + 440px);
+    left: 50%;
+    transform: translateX(-50%);
+    display: flex;
+    align-items: center;
+    gap: 14px;
+    font-family: var(--mono);
+    font-size: 13px;
+    color: var(--muted);
+    letter-spacing: 0.14em;
+    opacity: 0;
+    will-change: opacity;
+    z-index: 30;
+  }
+  .key-hint .kbd {
+    display: inline-flex;
+    align-items: center; justify-content: center;
+    width: 36px; height: 36px;
+    border: 1px solid var(--hairline);
+    border-radius: 6px;
+    background: rgba(255,255,255,0.04);
+    color: var(--ink-80);
+    font-size: 14px;
+    will-change: background, color, transform;
+  }
+
+  /* ====== Beat 2: split screen — HTML left, PowerPoint right ====== */
+  .beat2 {
+    position: absolute; inset: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 56px;
+    opacity: 0;
+    padding: 0 96px;
+    will-change: opacity;
+  }
+
+  .split-window {
+    width: 820px;
+    height: 580px;
+    border-radius: 12px;
+    overflow: hidden;
+    position: relative;
+    will-change: transform, opacity;
+  }
+
+  /* Left: HTML deck shrunk */
+  .split-left {
+    background: #0A0A0A;
+    border: 1px solid var(--hairline);
+    box-shadow: 0 30px 80px -30px rgba(0,0,0,0.6);
+  }
+  .split-left .mini-chrome {
+    height: 30px;
+    background: #161616;
+    border-bottom: 1px solid var(--hairline);
+    display: flex;
+    align-items: center;
+    padding: 0 12px;
+    gap: 8px;
+  }
+  .split-left .mini-chrome .d {
+    width: 8px; height: 8px; border-radius: 50%;
+    background: var(--hairline);
+  }
+  .split-left .mini-chrome .label {
+    margin-left: 10px;
+    font-family: var(--mono);
+    font-size: 11px;
+    color: var(--muted);
+    letter-spacing: 0.08em;
+  }
+  .split-left .mini-slide {
+    padding: 56px 64px;
+    height: calc(100% - 30px);
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+  }
+  .split-left .mini-eye {
+    font-family: var(--mono);
+    font-size: 11px;
+    color: var(--accent);
+    letter-spacing: 0.22em;
+    text-transform: uppercase;
+    margin-bottom: 16px;
+  }
+  .split-left .mini-title {
+    font-family: var(--serif-cn);
+    font-size: 54px;
+    font-weight: 500;
+    line-height: 1.1;
+    color: var(--ink);
+    letter-spacing: -0.01em;
+  }
+  .split-left .mini-sub {
+    margin-top: 20px;
+    font-family: var(--sans);
+    font-size: 15px;
+    color: var(--ink-60);
+    line-height: 1.5;
+  }
+  .split-left .mini-hair {
+    margin-top: 28px;
+    width: 52px; height: 2px;
+    background: var(--accent);
+  }
+
+  /* Right: PowerPoint chrome */
+  .split-right {
+    background: #F3F2EE;
+    border: 1px solid rgba(0,0,0,0.2);
+    box-shadow: 0 30px 80px -30px rgba(0,0,0,0.6);
+  }
+  .ppt-titlebar {
+    height: 32px;
+    background: #C44A36;
+    display: flex;
+    align-items: center;
+    padding: 0 14px;
+    gap: 10px;
+    color: #fff;
+    font-family: var(--sans);
+    font-size: 12px;
+    font-weight: 500;
+    letter-spacing: 0.02em;
+  }
+  .ppt-titlebar .pp-logo {
+    width: 18px; height: 18px;
+    background: #fff;
+    border-radius: 2px;
+    display: inline-flex;
+    align-items: center; justify-content: center;
+    color: #C44A36;
+    font-weight: 700;
+    font-size: 11px;
+    font-family: var(--sans);
+  }
+  .ppt-titlebar .title-text { opacity: 0.92; }
+  .ppt-titlebar .win-dots {
+    margin-left: auto;
+    display: flex; gap: 10px;
+    opacity: 0.7;
+  }
+  .ppt-titlebar .win-dots span {
+    width: 10px; height: 10px; border: 1px solid rgba(255,255,255,0.7);
+    border-radius: 1px;
+  }
+
+  .ppt-toolbar {
+    height: 40px;
+    background: #EAE8E3;
+    border-bottom: 1px solid rgba(0,0,0,0.08);
+    display: flex;
+    align-items: center;
+    padding: 0 14px;
+    gap: 14px;
+    font-family: var(--sans);
+    font-size: 12px;
+    color: #4A4843;
+  }
+  .ppt-toolbar .tool {
+    display: flex; align-items: center; gap: 6px;
+    padding: 4px 10px;
+    border-radius: 4px;
+  }
+  .ppt-toolbar .tool.active {
+    background: #fff;
+    border: 1px solid rgba(0,0,0,0.08);
+    color: var(--cd-ink);
+  }
+  .ppt-toolbar .tool .ico {
+    width: 14px; height: 14px;
+    border: 1px solid currentColor;
+    border-radius: 2px;
+    opacity: 0.7;
+  }
+  .ppt-toolbar .font-name {
+    padding: 4px 10px;
+    background: #fff;
+    border: 1px solid rgba(0,0,0,0.12);
+    border-radius: 3px;
+    min-width: 140px;
+    font-size: 12px;
+    color: var(--cd-ink);
+    display: flex; align-items: center; justify-content: space-between;
+  }
+  .ppt-toolbar .divider {
+    width: 1px; height: 20px;
+    background: rgba(0,0,0,0.08);
+  }
+
+  /* PPT canvas (the actual slide) */
+  .ppt-canvas {
+    height: calc(100% - 32px - 40px);
+    background: #D8D4CB;
+    padding: 24px;
+    position: relative;
+    overflow: hidden;
+  }
+  .ppt-slide {
+    background: #0A0A0A;
+    border-radius: 3px;
+    width: 100%;
+    height: 100%;
+    padding: 56px 64px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    position: relative;
+    box-shadow: 0 4px 16px rgba(0,0,0,0.18);
+  }
+  .ppt-slide .ppt-eye {
+    font-family: var(--mono);
+    font-size: 11px;
+    color: var(--accent);
+    letter-spacing: 0.22em;
+    text-transform: uppercase;
+    margin-bottom: 16px;
+  }
+  .ppt-slide .ppt-title-frame {
+    position: relative;
+    display: inline-block;
+    padding: 6px 10px;
+    margin: -6px -10px;
+    border-radius: 2px;
+    transition: box-shadow 0.12s ease;
+    align-self: flex-start;
+    max-width: fit-content;
+    min-width: 160px;
+  }
+  .ppt-slide .ppt-title-frame.selected {
+    box-shadow:
+      0 0 0 1px rgba(217,119,87,0.0),
+      inset 0 0 0 0 rgba(217,119,87,0.0);
+  }
+  .ppt-slide .ppt-title-frame.editing {
+    box-shadow:
+      0 0 0 1.5px var(--accent),
+      0 0 0 3px rgba(217,119,87,0.2);
+  }
+  .ppt-slide .ppt-title {
+    font-family: var(--serif-cn);
+    font-size: 54px;
+    font-weight: 500;
+    line-height: 1.1;
+    color: var(--ink);
+    letter-spacing: -0.01em;
+    display: inline;
+    position: relative;
+  }
+  .ppt-slide .edit-caret {
+    display: inline-block;
+    width: 2px;
+    height: 52px;
+    background: var(--accent);
+    vertical-align: -8px;
+    margin: 0 2px;
+    opacity: 0;
+  }
+  .ppt-slide .ppt-sub {
+    margin-top: 20px;
+    font-family: var(--sans);
+    font-size: 15px;
+    color: var(--ink-60);
+    line-height: 1.5;
+  }
+  .ppt-slide .ppt-hair {
+    margin-top: 28px;
+    width: 52px; height: 2px;
+    background: var(--accent);
+  }
+  /* Selection handles (corners) */
+  .ppt-slide .ppt-title-frame .handle {
+    position: absolute;
+    width: 8px; height: 8px;
+    background: var(--accent);
+    border: 1.5px solid #fff;
+    border-radius: 1px;
+    opacity: 0;
+    pointer-events: none;
+  }
+  .ppt-slide .ppt-title-frame .handle.tl { top: -4px; left: -4px; }
+  .ppt-slide .ppt-title-frame .handle.tr { top: -4px; right: -4px; }
+  .ppt-slide .ppt-title-frame .handle.bl { bottom: -4px; left: -4px; }
+  .ppt-slide .ppt-title-frame .handle.br { bottom: -4px; right: -4px; }
+  .ppt-slide .ppt-title-frame.selected .handle { opacity: 1; }
+  .ppt-slide .ppt-title-frame.editing .handle { opacity: 0; }
+
+  /* Mouse cursor */
+  .cursor {
+    position: absolute;
+    top: 0; left: 0;
+    width: 22px; height: 30px;
+    pointer-events: none;
+    z-index: 50;
+    opacity: 0;
+    will-change: transform, opacity;
+    filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
+  }
+  .cursor svg { width: 100%; height: 100%; }
+
+  /* Double-click ripple */
+  .dblclick-ripple {
+    position: absolute;
+    top: 0; left: 0;
+    width: 20px; height: 20px;
+    border: 2px solid var(--accent);
+    border-radius: 50%;
+    pointer-events: none;
+    z-index: 45;
+    opacity: 0;
+    will-change: transform, opacity;
+  }
+
+  /* Connection line between two windows */
+  .connector {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    width: 56px;
+    height: 120px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    opacity: 0;
+    will-change: opacity;
+    z-index: 10;
+  }
+  .connector svg { width: 100%; height: 100%; }
+  .connector-label {
+    position: absolute;
+    top: calc(50% + 72px);
+    left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--mono);
+    font-size: 12px;
+    color: var(--accent);
+    letter-spacing: 0.12em;
+    white-space: nowrap;
+    opacity: 0;
+    will-change: opacity;
+  }
+
+  /* Stage labels above windows */
+  .split-label {
+    position: absolute;
+    top: -48px;
+    left: 0;
+    font-family: var(--mono);
+    font-size: 16px;
+    color: var(--ink-60);
+    letter-spacing: 0.18em;
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity;
+    white-space: nowrap;
+  }
+  .split-label .em { color: var(--accent); }
+
+  /* ====== Brand Reveal (米色面板 · hero-v10 系列 signature) ====== */
+  .brand-panel {
+    position: absolute;
+    inset: 0;
+    background: var(--cd-bg);
+    transform: translateY(100%);
+    will-change: transform;
+    z-index: 80;
+  }
+  .brand-reveal {
+    position: absolute;
+    inset: 0;
+    z-index: 81;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    opacity: 0;
+    pointer-events: none;
+    will-change: opacity;
+  }
+  .brand-reveal .brand-wordmark {
+    font-family: var(--serif-en);
+    font-size: 72px;
+    font-weight: 100;
+    font-variation-settings: "wght" 100;
+    letter-spacing: -0.01em;
+    color: var(--cd-ink);
+    line-height: 1;
+    opacity: 0;
+    will-change: opacity, transform, font-variation-settings;
+  }
+  .brand-reveal .brand-wordmark .accent {
+    color: var(--accent);
+    font-weight: inherit;
+  }
+  .brand-reveal .brand-line {
+    width: 0;
+    height: 2px;
+    background: var(--accent);
+    margin-top: 60px;
+    will-change: width;
+  }
+
+</style>
+</head>
+<body>
+
+<div class="stage" id="stage">
+  <div class="watermark-tl">HUASHU · DESIGN</div>
+
+  <!-- ====== Beat 1 ====== -->
+  <div class="beat1" id="beat1">
+    <div class="deck-window" id="deckWindow">
+      <div class="deck-chrome">
+        <div class="traffic"><span class="d"></span><span class="d"></span><span class="d"></span></div>
+        <div class="url">localhost:8080 / deck · presenting</div>
+        <div class="page-count" id="pageCount">3 / 12</div>
+      </div>
+
+      <div class="deck-body-wrap">
+        <div class="deck-slide" id="slideA">
+          <div class="eyebrow">AI PSYCHOLOGY · 03</div>
+          <h1>The Mind<br/>is Plastic</h1>
+          <div class="sub">Agents aren't tools. They have preferences.</div>
+          <div class="hairline"></div>
+        </div>
+
+        <div class="deck-slide" id="slideB" style="opacity:0; transform: translateX(60px);">
+          <div class="eyebrow">AI PSYCHOLOGY · 04</div>
+          <h1>Injection<br/>&amp; Steering</h1>
+          <div class="sub">A world hides in the parameters.</div>
+          <div class="hairline"></div>
+        </div>
+      </div>
+    </div>
+
+    <div class="key-hint" id="keyHint">
+      <span>PRESS</span>
+      <span class="kbd" id="kbdKey">→</span>
+    </div>
+  </div>
+
+  <!-- ====== Beat 2: Split Screen ====== -->
+  <div class="beat2" id="beat2">
+    <!-- LEFT: HTML deck -->
+    <div class="split-col" style="position: relative;">
+      <div class="split-label" id="labelLeft">HTML · <span class="em">READ-ONLY</span></div>
+      <div class="split-window split-left" id="splitLeft">
+      <div class="mini-chrome">
+        <span class="d"></span><span class="d"></span><span class="d"></span>
+        <span class="label">localhost:8080/deck</span>
+      </div>
+      <div class="mini-slide">
+        <div class="mini-eye">AI PSYCHOLOGY · 03</div>
+        <div class="mini-title">The Mind<br/>is Plastic</div>
+        <div class="mini-sub">Agents aren't tools. They have preferences.</div>
+        <div class="mini-hair"></div>
+      </div>
+      </div>
+    </div>
+
+    <!-- Connector -->
+    <div class="connector" id="connector">
+      <svg viewBox="0 0 56 120" fill="none">
+        <line x1="4" y1="60" x2="52" y2="60" stroke="#D97757" stroke-width="1.5" stroke-dasharray="4 4"/>
+        <polygon points="44,54 54,60 44,66" fill="#D97757"/>
+      </svg>
+    </div>
+    <div class="connector-label" id="connectorLabel">html2pptx.js</div>
+
+    <!-- RIGHT: PowerPoint -->
+    <div class="split-col" style="position: relative;">
+      <div class="split-label" id="labelRight">PowerPoint · <span class="em">EDITABLE TEXT</span></div>
+      <div class="split-window split-right" id="splitRight">
+      <div class="ppt-titlebar">
+        <div class="pp-logo">P</div>
+        <div class="title-text">ai-psychology-talk.pptx - PowerPoint</div>
+        <div class="win-dots"><span></span><span></span><span></span></div>
+      </div>
+      <div class="ppt-toolbar">
+        <div class="tool">
+          <span class="ico"></span>
+          <span class="font-name"><span id="fontName">Source Serif 4</span><span style="opacity:0.5">▾</span></span>
+        </div>
+        <div class="divider"></div>
+        <div class="tool"><span style="font-weight:700">B</span></div>
+        <div class="tool" style="font-style:italic">I</div>
+        <div class="tool" style="text-decoration:underline">U</div>
+        <div class="divider"></div>
+        <div class="tool active"><span class="ico" style="background:#D97757;border-color:#D97757"></span></div>
+      </div>
+
+      <div class="ppt-canvas">
+        <div class="ppt-slide">
+          <div class="ppt-eye">AI PSYCHOLOGY · 03</div>
+          <div class="ppt-title-frame" id="titleFrame">
+            <span class="handle tl"></span>
+            <span class="handle tr"></span>
+            <span class="handle bl"></span>
+            <span class="handle br"></span>
+            <span class="ppt-title" id="titleText">The Mind is Plastic</span><span class="edit-caret" id="caret"></span>
+          </div>
+          <div class="ppt-sub">Agents aren't tools. They have preferences.</div>
+          <div class="ppt-hair"></div>
+        </div>
+
+        <!-- Cursor arrow -->
+        <div class="cursor" id="cursor">
+          <svg viewBox="0 0 22 30" fill="none">
+            <path d="M2 2 L2 22 L8 17 L12 26 L16 24 L12 15 L20 14 Z"
+                  fill="#1A1918" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/>
+          </svg>
+        </div>
+        <!-- Double-click ripple -->
+        <div class="dblclick-ripple" id="ripple"></div>
+      </div>
+      </div>
+    </div>
+  </div>
+
+  <!-- ====== Brand Reveal (米色面板 · hero-v10 signature) ====== -->
+  <div class="brand-panel" id="brandPanel"></div>
+  <div class="brand-reveal" id="brandReveal">
+    <div class="brand-wordmark" id="wordmark">huashu<span class="accent">-</span>design</div>
+    <div class="brand-line" id="brandLine"></div>
+  </div>
+
+</div>
+
+<script>
+(function() {
+  // ---------- Fit stage ----------
+  const stage = document.getElementById('stage');
+  function rescale() {
+    const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  rescale();
+  window.addEventListener('resize', rescale);
+
+  // ---------- Easings ----------
+  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
+  const expoOut = t => (t <= 0) ? 0 : (t >= 1) ? 1 : 1 - Math.pow(2, -10 * t);
+  const expoIn = t => (t <= 0) ? 0 : (t >= 1) ? 1 : Math.pow(2, 10 * (t - 1));
+  const easeOut = t => 1 - Math.pow(1 - t, 3);
+  const easeInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;
+  function lerp(time, start, end, fromV, toV, ease) {
+    if (time <= start) return fromV;
+    if (time >= end) return toV;
+    let p = (time - start) / (end - start);
+    if (ease) p = ease(p);
+    return fromV + (toV - fromV) * p;
+  }
+  function clampLerp(time, start, end) {
+    if (time <= start) return 0;
+    if (time >= end) return 1;
+    return (time - start) / (end - start);
+  }
+
+  // ---------- Timeline (10s total) ----------
+  const T = {
+    DURATION: 10.0,
+
+    // Beat 1: 0 - 2s
+    deckIn:       [0.15, 0.9],       // browser fade+rise
+    keyHintIn:    [0.6, 1.1],
+    keyPress:     [1.25, 1.4],       // arrow key highlight
+    slideFlip:    [1.3, 1.9],        // slide A→B
+    beat1Out:     [2.0, 2.4],
+
+    // Beat 2: split screen: 2.2 - 8.0s
+    beat2In:      [2.3, 2.9],
+    labelsIn:     [3.0, 3.5],
+
+    cursorIn:     [3.1, 3.4],        // cursor arrives on right side
+    cursorMove1:  [3.4, 4.1],        // cursor moves to title
+    dblclick:     [4.1, 4.3],        // double click
+    frameSelect:  [4.15, 4.35],      // frame shows handles
+    frameEdit:    [4.4, 4.55],       // frame enters edit mode
+    caretShowStart: 4.5,
+    textDelete:   [4.6, 5.4],        // delete original text char by char
+    textRetype:   [5.5, 7.2],        // type new text char by char
+    commitEdit:   [7.3, 7.5],        // exit edit mode
+
+    connectorIn:  [3.3, 3.9],
+    beat2Out:     [8.0, 8.3],        // main scene fades to 0 (0.3s)
+
+    // Brand Reveal (米色面板 · hero-v10 signature): 8.3 - 10s
+    // panelRise 与 beat2Out 微重叠 0.05s,避免黑屏间隙
+    panelRise:    [8.25, 8.7],       // 米色面板 translateY 100%→0 (expoOut)
+    wordmarkIn:   [8.7, 9.3],        // wordmark opacity 0→1 + translateY 20→0 + weight 100→500 (0.6s, expoOut)
+    brandLineIn:  [9.3, 9.7],        // brand-line expand 0→280px (0.4s, cubicOut)
+    brandHold:    [9.7, 10.0],       // hold (0.3s)
+  };
+
+  // ---------- Elements ----------
+  const beat1 = document.getElementById('beat1');
+  const beat2 = document.getElementById('beat2');
+  const brandReveal = document.getElementById('brandReveal');
+  const deckWindow = document.getElementById('deckWindow');
+  const pageCount = document.getElementById('pageCount');
+  const slideA = document.getElementById('slideA');
+  const slideB = document.getElementById('slideB');
+  const keyHint = document.getElementById('keyHint');
+  const kbdKey = document.getElementById('kbdKey');
+  const splitLeft = document.getElementById('splitLeft');
+  const splitRight = document.getElementById('splitRight');
+  const labelLeft = document.getElementById('labelLeft');
+  const labelRight = document.getElementById('labelRight');
+  const connector = document.getElementById('connector');
+  const connectorLabel = document.getElementById('connectorLabel');
+  const cursor = document.getElementById('cursor');
+  const ripple = document.getElementById('ripple');
+  const titleFrame = document.getElementById('titleFrame');
+  const titleText = document.getElementById('titleText');
+  const caret = document.getElementById('caret');
+  const panel = document.getElementById('brandPanel');
+  const wordmark = document.getElementById('wordmark');
+  const brandLine = document.getElementById('brandLine');
+
+  // Text to animate
+  const ORIG_TEXT = 'The Mind is Plastic';
+  const NEW_TEXT  = 'Mind · Plastic';
+
+  // ---------- Render ----------
+  function render(t) {
+
+    /* ======= Beat 1 ======= */
+    let beat1Op;
+    if (t < T.beat1Out[0]) {
+      beat1Op = lerp(t, T.deckIn[0], T.deckIn[1], 0, 1, expoOut);
+    } else {
+      beat1Op = 1 - clampLerp(t, T.beat1Out[0], T.beat1Out[1]);
+    }
+    beat1.style.opacity = beat1Op;
+    beat1.style.visibility = beat1Op > 0.01 ? 'visible' : 'hidden';
+
+    // Deck window rise
+    const deckRise = lerp(t, T.deckIn[0], T.deckIn[1], 24, 0, expoOut);
+    deckWindow.style.transform = `translate3d(0, ${deckRise}px, 0)`;
+
+    // Key hint appear
+    const khOp = clampLerp(t, T.keyHintIn[0], T.keyHintIn[1]);
+    keyHint.style.opacity = khOp;
+
+    // Key press flash
+    const kpActive = t >= T.keyPress[0] && t < T.keyPress[1] + 0.2;
+    if (kpActive) {
+      const kp = clampLerp(t, T.keyPress[0], T.keyPress[1]);
+      kbdKey.style.background = `rgba(217,119,87,${0.9 * (1 - kp * 0.4)})`;
+      kbdKey.style.color = '#fff';
+      kbdKey.style.transform = `scale(${1 - 0.08 * kp})`;
+    } else {
+      kbdKey.style.background = '';
+      kbdKey.style.color = '';
+      kbdKey.style.transform = '';
+    }
+
+    // Slide flip A→B
+    if (t >= T.slideFlip[0] && t < T.slideFlip[1] + 0.2) {
+      const sp = clampLerp(t, T.slideFlip[0], T.slideFlip[1]);
+      const eased = expoOut(sp);
+      slideA.style.opacity = 1 - eased;
+      slideA.style.transform = `translateX(${-60 * eased}px)`;
+      slideB.style.opacity = eased;
+      slideB.style.transform = `translateX(${60 * (1 - eased)}px)`;
+      // Update page count at midway
+      if (sp > 0.5) pageCount.textContent = '4 / 12';
+      else pageCount.textContent = '3 / 12';
+    } else if (t >= T.slideFlip[1]) {
+      slideA.style.opacity = 0;
+      slideB.style.opacity = 1;
+      slideB.style.transform = 'translateX(0)';
+      pageCount.textContent = '4 / 12';
+    } else {
+      slideA.style.opacity = 1;
+      slideA.style.transform = 'translateX(0)';
+      slideB.style.opacity = 0;
+      pageCount.textContent = '3 / 12';
+    }
+
+    /* ======= Beat 2 ======= */
+    let beat2Op = 0;
+    if (t >= T.beat2In[0] && t < T.beat2Out[1]) {
+      if (t < T.beat2In[1]) beat2Op = clampLerp(t, T.beat2In[0], T.beat2In[1]);
+      else if (t < T.beat2Out[0]) beat2Op = 1;
+      else beat2Op = 1 - clampLerp(t, T.beat2Out[0], T.beat2Out[1]);
+    }
+    beat2.style.opacity = beat2Op;
+    beat2.style.visibility = beat2Op > 0.01 ? 'visible' : 'hidden';
+
+    // Windows rise in
+    const splitInP = clampLerp(t, T.beat2In[0], T.beat2In[1]);
+    const splitRise = lerp(t, T.beat2In[0], T.beat2In[1], 28, 0, expoOut);
+    splitLeft.style.transform = `translate3d(${-8 * (1 - expoOut(splitInP))}px, ${splitRise}px, 0)`;
+    splitRight.style.transform = `translate3d(${8 * (1 - expoOut(splitInP))}px, ${splitRise}px, 0)`;
+
+    // Labels
+    const labelOp = clampLerp(t, T.labelsIn[0], T.labelsIn[1]);
+    labelLeft.style.opacity = labelOp * 0.7;
+    labelRight.style.opacity = labelOp * 0.85;
+
+    // Connector
+    const connOp = clampLerp(t, T.connectorIn[0], T.connectorIn[1]);
+    connector.style.opacity = connOp;
+    connectorLabel.style.opacity = connOp * 0.9;
+
+    /* === Cursor movement === */
+    // Cursor positions (relative to .ppt-canvas, which is inside split-right)
+    // Canvas starts at (0,0), size ~820 × 508 (580 - 32 - 40)
+    // Title sits around x=84 y=110 (inside .ppt-slide padding 56/64)
+    // We'll place cursor with absolute positioning inside .ppt-canvas.
+
+    // Entry point: off to the right bottom of canvas
+    const P_ENTER = { x: 720, y: 420 };
+    const P_TITLE = { x: 250, y: 170 }; // on the title
+
+    let cursorOp = 0;
+    let cx = P_ENTER.x, cy = P_ENTER.y;
+
+    if (t >= T.cursorIn[0] && t < T.beat2Out[0]) {
+      cursorOp = 1;
+      // Phase 1: appear (pop in with slight scale)
+      const inP = clampLerp(t, T.cursorIn[0], T.cursorIn[1]);
+      cursorOp = expoOut(inP);
+
+      // Phase 2: move to title
+      if (t >= T.cursorMove1[0]) {
+        const mp = clampLerp(t, T.cursorMove1[0], T.cursorMove1[1]);
+        const e = easeInOut(mp);
+        cx = P_ENTER.x + (P_TITLE.x - P_ENTER.x) * e;
+        cy = P_ENTER.y + (P_TITLE.y - P_ENTER.y) * e;
+      } else {
+        cx = P_ENTER.x;
+        cy = P_ENTER.y;
+      }
+
+      // After double-click, slight jitter toward caret position during typing
+      if (t >= T.textRetype[0] && t < T.textRetype[1]) {
+        cx = P_TITLE.x + 6;
+        cy = P_TITLE.y - 2;
+      }
+    } else if (t >= T.beat2Out[0]) {
+      cursorOp = 1 - clampLerp(t, T.beat2Out[0], T.beat2Out[1]);
+    }
+    cursor.style.opacity = cursorOp;
+    cursor.style.transform = `translate(${cx}px, ${cy}px)`;
+
+    /* === Double-click ripple === */
+    // Ripple pulses twice at T.dblclick start
+    let rippleVisible = false;
+    if (t >= T.dblclick[0] && t < T.dblclick[0] + 0.7) {
+      const dt = t - T.dblclick[0];
+      // Two rapid pulses
+      const pulse1 = clamp(dt / 0.25, 0, 1);
+      const pulse2 = clamp((dt - 0.15) / 0.25, 0, 1);
+      const scale1 = 0.4 + pulse1 * 1.4;
+      const scale2 = 0.4 + pulse2 * 1.4;
+      const op1 = 1 - pulse1;
+      const op2 = dt > 0.15 ? (1 - pulse2) : 0;
+      // Render as single element: use larger of the two
+      const scale = Math.max(scale1, scale2);
+      const op = Math.max(op1, op2);
+      ripple.style.opacity = op;
+      ripple.style.transform = `translate(-50%, -50%) translate(${P_TITLE.x + 6}px, ${P_TITLE.y + 26}px) scale(${scale})`;
+      rippleVisible = true;
+    }
+    if (!rippleVisible) ripple.style.opacity = 0;
+
+    /* === Frame states: selected → editing === */
+    titleFrame.classList.remove('selected', 'editing');
+    if (t >= T.frameSelect[0] && t < T.frameEdit[0]) {
+      titleFrame.classList.add('selected');
+    } else if (t >= T.frameEdit[0] && t < T.commitEdit[1]) {
+      titleFrame.classList.add('editing');
+    }
+
+    /* === Text animation: delete → retype === */
+    let displayedText = ORIG_TEXT;
+    let caretOp = 0;
+
+    if (t < T.textDelete[0]) {
+      displayedText = ORIG_TEXT;
+      caretOp = t >= T.caretShowStart ? 1 : 0;
+    } else if (t < T.textDelete[1]) {
+      // Delete: remove chars from end
+      const dp = clampLerp(t, T.textDelete[0], T.textDelete[1]);
+      const charsToRemove = Math.floor(dp * ORIG_TEXT.length);
+      displayedText = ORIG_TEXT.slice(0, ORIG_TEXT.length - charsToRemove);
+      caretOp = 1;
+    } else if (t < T.textRetype[0]) {
+      displayedText = '';
+      caretOp = 1;
+    } else if (t < T.textRetype[1]) {
+      // Retype new text
+      const rp = clampLerp(t, T.textRetype[0], T.textRetype[1]);
+      const charsToShow = Math.floor(rp * NEW_TEXT.length);
+      displayedText = NEW_TEXT.slice(0, charsToShow);
+      caretOp = 1;
+    } else if (t < T.commitEdit[1]) {
+      displayedText = NEW_TEXT;
+      // Caret blinks while still in edit mode
+      caretOp = (Math.floor(t * 2) % 2 === 0) ? 1 : 0.3;
+    } else {
+      displayedText = NEW_TEXT;
+      caretOp = 0;
+    }
+
+    // Blinking during idle-in-edit phases (when not actively typing/deleting)
+    if (t >= T.caretShowStart && t < T.textDelete[0]) {
+      caretOp = (Math.floor((t - T.caretShowStart) * 3) % 2 === 0) ? 1 : 0.35;
+    }
+
+    titleText.textContent = displayedText;
+    caret.style.opacity = caretOp;
+
+    /* ======= Brand Reveal (米色面板 · hero-v10 signature) ======= */
+    // Panel rises from bottom (米色面板 #F5F4F0)
+    const panelP = clampLerp(t, T.panelRise[0], T.panelRise[1]);
+    panel.style.transform = `translateY(${(1 - expoOut(panelP)) * 100}%)`;
+
+    // brand-reveal container visible once panel starts rising
+    brandReveal.style.opacity = panelP > 0.01 ? 1 : 0;
+
+    // Wordmark: opacity 0→1 + translateY 20→0 + weight 100→500 (expoOut)
+    const wmP = clampLerp(t, T.wordmarkIn[0], T.wordmarkIn[1]);
+    const wmEased = expoOut(wmP);
+    wordmark.style.opacity = wmEased;
+    const wmRise = (1 - wmEased) * 20;
+    wordmark.style.transform = `translate3d(0, ${wmRise}px, 0)`;
+    const w = 100 + (500 - 100) * wmEased;
+    wordmark.style.fontVariationSettings = `"wght" ${w.toFixed(0)}`;
+    wordmark.style.fontWeight = Math.round(w);
+
+    // Brand line expand 0→280px (cubicOut)
+    const lineP = clampLerp(t, T.brandLineIn[0], T.brandLineIn[1]);
+    const cubicOut = x => 1 - Math.pow(1 - x, 3);
+    brandLine.style.width = (280 * cubicOut(lineP)) + 'px';
+  }
+
+  // ---------- Driver ----------
+  let manualT = null;
+  let startMs = null;
+  let hasFinished = false;
+  function tick(now) {
+    if (manualT != null) render(manualT);
+    else {
+      if (startMs == null) startMs = now;
+      const elapsed = (now - startMs) / 1000;
+      const recording = window.__recording === true;
+      let t;
+      if (recording) {
+        t = Math.min(elapsed, T.DURATION - 0.001);
+        if (elapsed >= T.DURATION) hasFinished = true;
+      } else {
+        t = elapsed % T.DURATION;
+      }
+      render(t);
+    }
+    requestAnimationFrame(tick);
+  }
+  // Force first-frame render synchronously, THEN set ready
+  render(0);
+  requestAnimationFrame(tick);
+
+  window.__setTime = function(t) { manualT = t; render(t); };
+  window.__resume = function() { manualT = null; startMs = null; };
+  window.__duration = T.DURATION;
+  window.__render = render;
+  window.__ready = true;
+})();
+</script>
+</body>
+</html>

+ 1015 - 938
demos/c2-slides-pptx.html

@@ -1,978 +1,1055 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
+<!doctype html>
+<html lang="zh-Hans">
 <head>
-<meta charset="UTF-8">
-<title>Huashu-Design · Slides → PPTX</title>
-<script crossorigin src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
-<script crossorigin src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
-<script src="https://unpkg.com/@babel/standalone@7.25.6/babel.min.js"></script>
+<meta charset="utf-8" />
+<title>c2-slides-pptx · 中文版 · v2</title>
 <link rel="preconnect" href="https://fonts.googleapis.com">
 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,300;1,6..72,400;1,6..72,500&family=Noto+Serif+SC:wght@300;400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
 <style>
-  * { box-sizing: border-box; margin: 0; padding: 0; }
-  html, body { width: 100%; height: 100%; overflow: hidden; }
-  body {
-    background: #0c0c0c;
-    font-family: 'Newsreader', 'Noto Serif SC', Georgia, serif;
-    color: #1a1a1a;
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --dim: rgba(255,255,255,0.18);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;
+    --accent-deep: #B85D3D;
+    --cd-bg: #F5F4F0;
+    --cd-panel: #FFFFFF;
+    --cd-ink: #1A1918;
+    --cd-dim: #8B867E;
+    --cd-hair: rgba(0,0,0,0.08);
+
+    --serif-cn: "Noto Serif SC", "Source Han Serif SC", serif;
+    --serif-en: "Source Serif 4", Georgia, serif;
+    --sans: "Inter", -apple-system, "PingFang SC", system-ui, sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
     -webkit-font-smoothing: antialiased;
-    text-rendering: optimizeLegibility;
   }
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+  }
+
+  /* Film grain (2% opacity) */
+  .stage::after {
+    content: '';
+    position: absolute; inset: 0;
+    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.5 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
+    opacity: 0.025;
+    pointer-events: none;
+    mix-blend-mode: overlay;
+    z-index: 200;
+  }
+
+  .watermark-tl {
+    position: absolute;
+    top: 40px; left: 56px;
+    font-family: var(--mono);
+    font-size: 14px;
+    letter-spacing: 0.2em;
+    text-transform: uppercase;
+    color: rgba(255,255,255,0.16);
+    z-index: 180;
+    pointer-events: none;
+  }
+
+  /* ====== Beat 1: browser-fullscreen deck ====== */
+  .beat1 {
+    position: absolute; inset: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    opacity: 1;
+  }
+
+  .deck-window {
+    width: 1400px;
+    height: 788px;
+    border-radius: 14px;
+    background: #101010;
+    border: 1px solid var(--hairline);
+    box-shadow: 0 40px 120px -30px rgba(217,119,87,0.18),
+                0 0 0 1px rgba(255,255,255,0.03);
+    position: relative;
+    will-change: transform, opacity;
+  }
+  .deck-window .deck-body-wrap {
+    position: absolute;
+    top: 44px; left: 0; right: 0; bottom: 0;
+    border-radius: 0 0 14px 14px;
+    overflow: hidden;
+    background: #0A0A0A;
+  }
+  .deck-chrome {
+    height: 44px;
+    background: #161616;
+    border-bottom: 1px solid var(--hairline);
+    display: flex;
+    align-items: center;
+    padding: 0 18px;
+    gap: 14px;
+  }
+  .deck-chrome .traffic {
+    display: flex; gap: 8px;
+  }
+  .deck-chrome .traffic .d {
+    width: 11px; height: 11px; border-radius: 50%;
+    background: var(--hairline);
+  }
+  .deck-chrome .url {
+    flex: 1;
+    text-align: center;
+    font-family: var(--mono);
+    font-size: 12px;
+    color: var(--muted);
+    letter-spacing: 0.02em;
+  }
+  .deck-chrome .page-count {
+    font-family: var(--mono);
+    font-size: 13px;
+    color: var(--accent);
+    letter-spacing: 0.08em;
+    min-width: 60px;
+    text-align: right;
+  }
+
+  .deck-slide {
+    position: absolute;
+    top: 0; left: 0;
+    width: 100%;
+    height: 100%;
+    background: #0A0A0A;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    padding: 96px 120px;
+    will-change: transform, opacity;
+  }
+  .deck-slide .eyebrow {
+    font-family: var(--mono);
+    font-size: 14px;
+    color: var(--accent);
+    letter-spacing: 0.24em;
+    text-transform: uppercase;
+    margin-bottom: 24px;
+  }
+  .deck-slide h1 {
+    font-family: var(--serif-cn);
+    font-size: 92px;
+    font-weight: 500;
+    line-height: 1.08;
+    color: var(--ink);
+    margin: 0 0 28px 0;
+    letter-spacing: -0.01em;
+  }
+  .deck-slide .sub {
+    font-family: var(--sans);
+    font-size: 22px;
+    color: var(--ink-60);
+    line-height: 1.5;
+    max-width: 780px;
+  }
+  .deck-slide .hairline {
+    margin-top: 48px;
+    width: 80px;
+    height: 2px;
+    background: var(--accent);
+  }
+
+  /* Key press indicator — sits below the window */
+  .key-hint {
+    position: absolute;
+    top: calc(50% + 440px);
+    left: 50%;
+    transform: translateX(-50%);
+    display: flex;
+    align-items: center;
+    gap: 14px;
+    font-family: var(--mono);
+    font-size: 13px;
+    color: var(--muted);
+    letter-spacing: 0.14em;
+    opacity: 0;
+    will-change: opacity;
+    z-index: 30;
+  }
+  .key-hint .kbd {
+    display: inline-flex;
+    align-items: center; justify-content: center;
+    width: 36px; height: 36px;
+    border: 1px solid var(--hairline);
+    border-radius: 6px;
+    background: rgba(255,255,255,0.04);
+    color: var(--ink-80);
+    font-size: 14px;
+    will-change: background, color, transform;
+  }
+
+  /* ====== Beat 2: split screen — HTML left, PowerPoint right ====== */
+  .beat2 {
+    position: absolute; inset: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 56px;
+    opacity: 0;
+    padding: 0 96px;
+    will-change: opacity;
+  }
+
+  .split-window {
+    width: 820px;
+    height: 580px;
+    border-radius: 12px;
+    overflow: hidden;
+    position: relative;
+    will-change: transform, opacity;
+  }
+
+  /* Left: HTML deck shrunk */
+  .split-left {
+    background: #0A0A0A;
+    border: 1px solid var(--hairline);
+    box-shadow: 0 30px 80px -30px rgba(0,0,0,0.6);
+  }
+  .split-left .mini-chrome {
+    height: 30px;
+    background: #161616;
+    border-bottom: 1px solid var(--hairline);
+    display: flex;
+    align-items: center;
+    padding: 0 12px;
+    gap: 8px;
+  }
+  .split-left .mini-chrome .d {
+    width: 8px; height: 8px; border-radius: 50%;
+    background: var(--hairline);
+  }
+  .split-left .mini-chrome .label {
+    margin-left: 10px;
+    font-family: var(--mono);
+    font-size: 11px;
+    color: var(--muted);
+    letter-spacing: 0.08em;
+  }
+  .split-left .mini-slide {
+    padding: 56px 64px;
+    height: calc(100% - 30px);
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+  }
+  .split-left .mini-eye {
+    font-family: var(--mono);
+    font-size: 11px;
+    color: var(--accent);
+    letter-spacing: 0.22em;
+    text-transform: uppercase;
+    margin-bottom: 16px;
+  }
+  .split-left .mini-title {
+    font-family: var(--serif-cn);
+    font-size: 54px;
+    font-weight: 500;
+    line-height: 1.1;
+    color: var(--ink);
+    letter-spacing: -0.01em;
+  }
+  .split-left .mini-sub {
+    margin-top: 20px;
+    font-family: var(--sans);
+    font-size: 15px;
+    color: var(--ink-60);
+    line-height: 1.5;
+  }
+  .split-left .mini-hair {
+    margin-top: 28px;
+    width: 52px; height: 2px;
+    background: var(--accent);
+  }
+
+  /* Right: PowerPoint chrome */
+  .split-right {
+    background: #F3F2EE;
+    border: 1px solid rgba(0,0,0,0.2);
+    box-shadow: 0 30px 80px -30px rgba(0,0,0,0.6);
+  }
+  .ppt-titlebar {
+    height: 32px;
+    background: #C44A36;
+    display: flex;
+    align-items: center;
+    padding: 0 14px;
+    gap: 10px;
+    color: #fff;
+    font-family: var(--sans);
+    font-size: 12px;
+    font-weight: 500;
+    letter-spacing: 0.02em;
+  }
+  .ppt-titlebar .pp-logo {
+    width: 18px; height: 18px;
+    background: #fff;
+    border-radius: 2px;
+    display: inline-flex;
+    align-items: center; justify-content: center;
+    color: #C44A36;
+    font-weight: 700;
+    font-size: 11px;
+    font-family: var(--sans);
+  }
+  .ppt-titlebar .title-text { opacity: 0.92; }
+  .ppt-titlebar .win-dots {
+    margin-left: auto;
+    display: flex; gap: 10px;
+    opacity: 0.7;
+  }
+  .ppt-titlebar .win-dots span {
+    width: 10px; height: 10px; border: 1px solid rgba(255,255,255,0.7);
+    border-radius: 1px;
+  }
+
+  .ppt-toolbar {
+    height: 40px;
+    background: #EAE8E3;
+    border-bottom: 1px solid rgba(0,0,0,0.08);
+    display: flex;
+    align-items: center;
+    padding: 0 14px;
+    gap: 14px;
+    font-family: var(--sans);
+    font-size: 12px;
+    color: #4A4843;
+  }
+  .ppt-toolbar .tool {
+    display: flex; align-items: center; gap: 6px;
+    padding: 4px 10px;
+    border-radius: 4px;
+  }
+  .ppt-toolbar .tool.active {
+    background: #fff;
+    border: 1px solid rgba(0,0,0,0.08);
+    color: var(--cd-ink);
+  }
+  .ppt-toolbar .tool .ico {
+    width: 14px; height: 14px;
+    border: 1px solid currentColor;
+    border-radius: 2px;
+    opacity: 0.7;
+  }
+  .ppt-toolbar .font-name {
+    padding: 4px 10px;
+    background: #fff;
+    border: 1px solid rgba(0,0,0,0.12);
+    border-radius: 3px;
+    min-width: 140px;
+    font-size: 12px;
+    color: var(--cd-ink);
+    display: flex; align-items: center; justify-content: space-between;
+  }
+  .ppt-toolbar .divider {
+    width: 1px; height: 20px;
+    background: rgba(0,0,0,0.08);
+  }
+
+  /* PPT canvas (the actual slide) */
+  .ppt-canvas {
+    height: calc(100% - 32px - 40px);
+    background: #D8D4CB;
+    padding: 24px;
+    position: relative;
+    overflow: hidden;
+  }
+  .ppt-slide {
+    background: #0A0A0A;
+    border-radius: 3px;
+    width: 100%;
+    height: 100%;
+    padding: 56px 64px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    position: relative;
+    box-shadow: 0 4px 16px rgba(0,0,0,0.18);
+  }
+  .ppt-slide .ppt-eye {
+    font-family: var(--mono);
+    font-size: 11px;
+    color: var(--accent);
+    letter-spacing: 0.22em;
+    text-transform: uppercase;
+    margin-bottom: 16px;
+  }
+  .ppt-slide .ppt-title-frame {
+    position: relative;
+    display: inline-block;
+    padding: 6px 10px;
+    margin: -6px -10px;
+    border-radius: 2px;
+    transition: box-shadow 0.12s ease;
+    align-self: flex-start;
+    max-width: fit-content;
+    min-width: 160px;
+  }
+  .ppt-slide .ppt-title-frame.selected {
+    box-shadow:
+      0 0 0 1px rgba(217,119,87,0.0),
+      inset 0 0 0 0 rgba(217,119,87,0.0);
+  }
+  .ppt-slide .ppt-title-frame.editing {
+    box-shadow:
+      0 0 0 1.5px var(--accent),
+      0 0 0 3px rgba(217,119,87,0.2);
+  }
+  .ppt-slide .ppt-title {
+    font-family: var(--serif-cn);
+    font-size: 54px;
+    font-weight: 500;
+    line-height: 1.1;
+    color: var(--ink);
+    letter-spacing: -0.01em;
+    display: inline;
+    position: relative;
+  }
+  .ppt-slide .edit-caret {
+    display: inline-block;
+    width: 2px;
+    height: 52px;
+    background: var(--accent);
+    vertical-align: -8px;
+    margin: 0 2px;
+    opacity: 0;
+  }
+  .ppt-slide .ppt-sub {
+    margin-top: 20px;
+    font-family: var(--sans);
+    font-size: 15px;
+    color: var(--ink-60);
+    line-height: 1.5;
+  }
+  .ppt-slide .ppt-hair {
+    margin-top: 28px;
+    width: 52px; height: 2px;
+    background: var(--accent);
+  }
+  /* Selection handles (corners) */
+  .ppt-slide .ppt-title-frame .handle {
+    position: absolute;
+    width: 8px; height: 8px;
+    background: var(--accent);
+    border: 1.5px solid #fff;
+    border-radius: 1px;
+    opacity: 0;
+    pointer-events: none;
+  }
+  .ppt-slide .ppt-title-frame .handle.tl { top: -4px; left: -4px; }
+  .ppt-slide .ppt-title-frame .handle.tr { top: -4px; right: -4px; }
+  .ppt-slide .ppt-title-frame .handle.bl { bottom: -4px; left: -4px; }
+  .ppt-slide .ppt-title-frame .handle.br { bottom: -4px; right: -4px; }
+  .ppt-slide .ppt-title-frame.selected .handle { opacity: 1; }
+  .ppt-slide .ppt-title-frame.editing .handle { opacity: 0; }
+
+  /* Mouse cursor */
+  .cursor {
+    position: absolute;
+    top: 0; left: 0;
+    width: 22px; height: 30px;
+    pointer-events: none;
+    z-index: 50;
+    opacity: 0;
+    will-change: transform, opacity;
+    filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
+  }
+  .cursor svg { width: 100%; height: 100%; }
+
+  /* Double-click ripple */
+  .dblclick-ripple {
+    position: absolute;
+    top: 0; left: 0;
+    width: 20px; height: 20px;
+    border: 2px solid var(--accent);
+    border-radius: 50%;
+    pointer-events: none;
+    z-index: 45;
+    opacity: 0;
+    will-change: transform, opacity;
+  }
+
+  /* Connection line between two windows */
+  .connector {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    width: 56px;
+    height: 120px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    opacity: 0;
+    will-change: opacity;
+    z-index: 10;
+  }
+  .connector svg { width: 100%; height: 100%; }
+  .connector-label {
+    position: absolute;
+    top: calc(50% + 72px);
+    left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--mono);
+    font-size: 12px;
+    color: var(--accent);
+    letter-spacing: 0.12em;
+    white-space: nowrap;
+    opacity: 0;
+    will-change: opacity;
+  }
+
+  /* Stage labels above windows */
+  .split-label {
+    position: absolute;
+    top: -48px;
+    left: 0;
+    font-family: var(--mono);
+    font-size: 16px;
+    color: var(--ink-60);
+    letter-spacing: 0.18em;
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity;
+    white-space: nowrap;
+  }
+  .split-label .em { color: var(--accent); }
+
+  /* ====== Brand Reveal (米色面板 · hero-v10 系列 signature) ====== */
+  .brand-panel {
+    position: absolute;
+    inset: 0;
+    background: var(--cd-bg);
+    transform: translateY(100%);
+    will-change: transform;
+    z-index: 80;
+  }
+  .brand-reveal {
+    position: absolute;
+    inset: 0;
+    z-index: 81;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    opacity: 0;
+    pointer-events: none;
+    will-change: opacity;
+  }
+  .brand-reveal .brand-wordmark {
+    font-family: var(--serif-en);
+    font-size: 72px;
+    font-weight: 100;
+    font-variation-settings: "wght" 100;
+    letter-spacing: -0.01em;
+    color: var(--cd-ink);
+    line-height: 1;
+    opacity: 0;
+    will-change: opacity, transform, font-variation-settings;
+  }
+  .brand-reveal .brand-wordmark .accent {
+    color: var(--accent);
+    font-weight: inherit;
+  }
+  .brand-reveal .brand-line {
+    width: 0;
+    height: 2px;
+    background: var(--accent);
+    margin-top: 60px;
+    will-change: width;
+  }
+
 </style>
 </head>
 <body>
-<div id="root"></div>
 
-<!-- animations.jsx inlined -->
-<script type="text/babel">
-(function() {
-  const { createContext, useContext, useState, useEffect, useRef, useCallback } = React;
-  const TimeContext = createContext({ time: 0, duration: 10, playing: false });
-  const SpriteContext = createContext(null);
-
-  const Easing = {
-    linear: t => t,
-    easeIn: t => t * t,
-    easeOut: t => 1 - (1 - t) * (1 - t),
-    easeInOut: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
-    spring: t => {
-      const c = (2 * Math.PI) / 3;
-      return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c) + 1;
-    },
-  };
-
-  function interpolate(t, input, output, easing) {
-    const [inStart, inEnd] = input;
-    const [outStart, outEnd] = output;
-    if (t <= inStart) return outStart;
-    if (t >= inEnd) return outEnd;
-    let progress = (t - inStart) / (inEnd - inStart);
-    if (easing) progress = easing(progress);
-    return outStart + (outEnd - outStart) * progress;
-  }
-
-  function useTime() { return useContext(TimeContext).time; }
-  function useSprite() {
-    const sprite = useContext(SpriteContext);
-    return sprite || { t: 0, elapsed: 0, duration: 0 };
-  }
-
-  function Stage({ duration = 10, width = 1920, height = 1080, loop = true, children, bgColor = '#fff' }) {
-    const [time, setTime] = useState(0);
-    const [playing, setPlaying] = useState(true);
-    const [scale, setScale] = useState(1);
-    const rafRef = useRef(null);
-    const effectiveLoop = (typeof window !== 'undefined' && window.__recording) ? false : loop;
-
-    useEffect(() => {
-      function updateScale() {
-        const vw = window.innerWidth;
-        const vh = window.innerHeight - 56;
-        const s = Math.min(vw / width, vh / height);
-        setScale(s);
-      }
-      updateScale();
-      window.addEventListener('resize', updateScale);
-      return () => window.removeEventListener('resize', updateScale);
-    }, [width, height]);
-
-    useEffect(() => {
-      if (!playing) return;
-      let cancelled = false;
-      let last = null;
-      function tick(now) {
-        if (cancelled) return;
-        if (last === null) {
-          last = now;
-          if (typeof window !== 'undefined') window.__ready = true;
-        }
-        const delta = (now - last) / 1000;
-        last = now;
-        setTime(prev => {
-          const next = prev + delta;
-          if (next >= duration) return effectiveLoop ? 0 : duration - 0.001;
-          return next;
-        });
-        rafRef.current = requestAnimationFrame(tick);
-      }
-      const start = () => { if (!cancelled) rafRef.current = requestAnimationFrame(tick); };
-      if (document.fonts && document.fonts.ready) document.fonts.ready.then(start); else start();
-      return () => { cancelled = true; cancelAnimationFrame(rafRef.current); };
-    }, [playing, duration, effectiveLoop]);
-
-    const progress = time / duration;
-    const ctx = { time, duration, playing, setPlaying, setTime };
-
-    const canvasStyle = {
-      position: 'absolute',
-      top: '50%',
-      left: '50%',
-      transformOrigin: 'center center',
-      width,
-      height,
-      background: bgColor,
-      overflow: 'hidden',
-      transform: `translate(-50%, -50%) scale(${scale})`,
-    };
-
-    return (
-      <TimeContext.Provider value={ctx}>
-        <div style={{position:'fixed', inset:0, background:'#0c0c0c', display:'flex', flexDirection:'column'}}>
-          <div style={{flex:1, position:'relative', overflow:'hidden'}}>
-            <div style={canvasStyle}>{children}</div>
-          </div>
-          <div className="no-record" style={{position:'fixed', bottom:0, left:0, right:0, background:'rgba(0,0,0,0.8)', backdropFilter:'blur(10px)', padding:'12px 20px', display:'flex', alignItems:'center', gap:16, color:'#fff', fontSize:12, zIndex:100}}>
-            <button onClick={()=>setPlaying(p=>!p)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>{playing?'⏸ 暂停':'▶ 播放'}</button>
-            <button onClick={()=>setTime(0)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>⏮ 开始</button>
-            <div style={{fontFamily:'ui-monospace, monospace', fontVariantNumeric:'tabular-nums', minWidth:90}}>{time.toFixed(2)}s / {duration.toFixed(2)}s</div>
-            <div style={{flex:1, height:4, background:'rgba(255,255,255,0.2)', borderRadius:2, position:'relative'}}>
-              <div style={{position:'absolute', top:0, left:0, height:'100%', width:`${progress*100}%`, background:'#fff', borderRadius:2}} />
-            </div>
-          </div>
-        </div>
-      </TimeContext.Provider>
-    );
-  }
-
-  function Sprite({ start = 0, end, children, style }) {
-    const { time } = useContext(TimeContext);
-    const actualEnd = end == null ? Infinity : end;
-    if (time < start || time >= actualEnd) return null;
-    const duration = actualEnd - start;
-    const elapsed = time - start;
-    const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
-    const spriteValue = { t, elapsed, duration, start, end: actualEnd };
-    return (
-      <SpriteContext.Provider value={spriteValue}>
-        <div style={{position:'absolute', inset:0, ...style}}>{children}</div>
-      </SpriteContext.Provider>
-    );
-  }
-
-  window.Animations = { Stage, Sprite, useTime, useSprite, Easing, interpolate };
-})();
-</script>
+<div class="stage" id="stage">
+  <div class="watermark-tl">HUASHU · DESIGN</div>
 
-<!-- Demo scene -->
-<script type="text/babel">
-const { Stage, Sprite, useTime, useSprite, Easing, interpolate } = window.Animations;
-
-// ── Design tokens ─────────────────────────────────────────
-const CREAM = '#FAF6EF';
-const INK = '#1a1a1a';
-const TERRA = '#C04A1A';
-const ASH = '#6b6b6b';
-const LINE = '#d9d2c5';
-const OLIVE = '#6a6b4e';
-const DEEP_BLUE = '#2a3552';
-
-const serif = "'Newsreader', 'Noto Serif SC', Georgia, serif";
-const sans = "'Inter', -apple-system, sans-serif";
-const mono = "'JetBrains Mono', ui-monospace, monospace";
-
-// ══════════════════════════════════════════════════════════
-// Scene 1 (0 – 3s) · 开题
-// ══════════════════════════════════════════════════════════
-function Scene1_Title() {
-  const { elapsed } = useSprite();
-  const tagOp = interpolate(elapsed, [0, 0.6], [0, 1]);
-  const mainOp = interpolate(elapsed, [0.4, 1.2], [0, 1]);
-  const mainY = interpolate(elapsed, [0.4, 1.2], [40, 0], Easing.easeOut);
-  const terraOp = interpolate(elapsed, [1.1, 1.8], [0, 1]);
-  const lineW = interpolate(elapsed, [1.6, 2.2], [0, 640]);
-  const subOp = interpolate(elapsed, [1.9, 2.5], [0, 1]);
-  const fadeOut = interpolate(elapsed, [2.7, 3.0], [1, 0]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
-      display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
-      <div style={{position:'absolute', top: 72, left: 88,
-        fontFamily: mono, fontSize: 12, letterSpacing:'0.3em',
-        color: ASH, opacity: tagOp}}>
-        <span style={{color: TERRA}}>●</span>  幻灯片能力 · HTML + PPTX
-      </div>
-      <div style={{fontFamily: serif, fontSize: 130, fontWeight: 500,
-        color: INK, lineHeight: 1.0, letterSpacing:'-0.015em',
-        opacity: mainOp, transform: `translateY(${mainY}px)`,
-        textAlign: 'center'}}>
-        <span style={{fontStyle:'italic'}}>播放</span>用 HTML,<br/>
-        <span style={{fontStyle:'italic', color: TERRA, opacity: terraOp}}>编辑</span>用 PPTX
-      </div>
-      <div style={{height: 1, background: INK, width: lineW, marginTop: 40}} />
-      <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 24,
-        color: ASH, marginTop: 24, opacity: subOp, letterSpacing:'0.02em'}}>
-        一个源文件,两种交付形态
-      </div>
-    </div>
-  );
-}
-
-// ══════════════════════════════════════════════════════════
-// Scene 2 (3 – 9s) · HTML Deck 翻页
-// ══════════════════════════════════════════════════════════
-function Scene2_DeckFlip() {
-  const { elapsed } = useSprite();
-  const frameOp = interpolate(elapsed, [0, 0.6], [0, 1]);
-  const frameScale = interpolate(elapsed, [0, 0.8], [0.94, 1], Easing.easeOut);
-
-  // Three pages, each ~1.5s. Stagger timings inside deck.
-  // Page 1: 0.6 – 2.2 | Page 2: 2.2 – 3.8 | Page 3: 3.8 – 5.6
-  const pageIndex = elapsed < 2.2 ? 0 : elapsed < 3.8 ? 1 : 2;
-  const pageNum = pageIndex + 1;
-
-  const fadeOut = interpolate(elapsed, [5.6, 6.0], [1, 0]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
-      display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
-      <div style={{position:'absolute', top: 48, left: 88,
-        fontFamily: mono, fontSize: 11, letterSpacing:'0.3em', color: ASH}}>
-        <span style={{color: TERRA}}>●</span>  SCENE 02 · HTML DECK
-      </div>
-      <div style={{position:'absolute', top: 48, right: 88,
-        fontFamily: serif, fontStyle:'italic', fontSize: 20, color: ASH}}>
-        浏览器里直接演讲
+  <!-- ====== Beat 1 ====== -->
+  <div class="beat1" id="beat1">
+    <div class="deck-window" id="deckWindow">
+      <div class="deck-chrome">
+        <div class="traffic"><span class="d"></span><span class="d"></span><span class="d"></span></div>
+        <div class="url">localhost:8080 / deck · 全屏演讲</div>
+        <div class="page-count" id="pageCount">3 / 12</div>
       </div>
 
-      <div style={{opacity: frameOp, transform: `scale(${frameScale})`,
-        transformOrigin:'center center'}}>
-        <BrowserFrame url="file:///Users/huashu/decks/annual-2026/deck.html">
-          <DeckSlide pageIndex={pageIndex} localElapsed={elapsed} />
-          {/* Footer inside deck */}
-          <div style={{position:'absolute', bottom: 18, left: 28, right: 28,
-            display:'flex', justifyContent:'space-between', alignItems:'center',
-            zIndex: 5}}>
-            <div style={{fontFamily: mono, fontSize: 11, color: ASH,
-              letterSpacing:'0.15em'}}>
-              {String(pageNum).padStart(2,'0')} / 12
-            </div>
-            <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-              letterSpacing:'0.2em'}}>
-              HUASHU · DESIGN
-            </div>
-          </div>
-          {/* TERRA progress bar */}
-          <div style={{position:'absolute', bottom: 0, left: 0, right: 0,
-            height: 3, background: '#eee', zIndex: 5}}>
-            <div style={{height:'100%', width: `${(pageNum/12)*100}%`,
-              background: TERRA}} />
-          </div>
-        </BrowserFrame>
-      </div>
+      <div class="deck-body-wrap">
+        <div class="deck-slide" id="slideA">
+          <div class="eyebrow">AI 心理学 · 第 3 节</div>
+          <h1>心智的<br/>可塑性</h1>
+          <div class="sub">Agent 不是工具,它有自己的偏好。</div>
+          <div class="hairline"></div>
+        </div>
 
-      <div style={{marginTop: 28, fontFamily: mono, fontSize: 11, color: ASH,
-        letterSpacing:'0.25em'}}>
-        <span style={{color: pageIndex === 0 ? TERRA : LINE}}>●</span>
-        <span style={{margin:'0 10px', color: pageIndex === 1 ? TERRA : LINE}}>●</span>
-        <span style={{color: pageIndex === 2 ? TERRA : LINE}}>●</span>
-      </div>
-    </div>
-  );
-}
-
-// Browser chrome container (chrome style, 1600×900 deck 16:9)
-function BrowserFrame({ url, children }) {
-  const W = 1400, H = 788;  // 16:9 ratio
-  return (
-    <div style={{
-      display:'inline-block',
-      background:'#e8e4dc',
-      borderRadius: 12,
-      boxShadow:'0 30px 70px rgba(0,0,0,0.18), 0 10px 24px rgba(0,0,0,0.12)',
-      padding: 0,
-      overflow:'hidden',
-      border:`1px solid ${LINE}`,
-    }}>
-      {/* Title bar */}
-      <div style={{height: 42, display:'flex', alignItems:'center',
-        background:'#e8e4dc', padding:'0 16px', gap: 8,
-        borderBottom:`1px solid ${LINE}`}}>
-        <div style={{width:12, height:12, borderRadius:'50%', background:'#ff5f57'}} />
-        <div style={{width:12, height:12, borderRadius:'50%', background:'#febc2e'}} />
-        <div style={{width:12, height:12, borderRadius:'50%', background:'#28c840'}} />
-        <div style={{flex: 1, height: 26, background:'#faf6ef', border:`1px solid ${LINE}`,
-          borderRadius: 6, marginLeft: 16, padding:'0 14px',
-          display:'flex', alignItems:'center', gap: 8,
-          fontFamily: mono, fontSize: 11, color: ASH, letterSpacing:'0.02em',
-          overflow:'hidden', whiteSpace:'nowrap'}}>
-          <svg width="10" height="12" viewBox="0 0 10 12" style={{flexShrink: 0}}>
-            <path d="M2 5 V3.5 a3 3 0 016 0 V5" stroke={OLIVE} strokeWidth="1.2" fill="none"/>
-            <rect x="1" y="5" width="8" height="6" fill={OLIVE} opacity="0.85"/>
-          </svg>
-          <span style={{color: INK, opacity: 0.7}}>{url}</span>
+        <div class="deck-slide" id="slideB" style="opacity:0; transform: translateX(60px);">
+          <div class="eyebrow">AI 心理学 · 第 4 节</div>
+          <h1>注入与引导</h1>
+          <div class="sub">参数里藏着一个世界。</div>
+          <div class="hairline"></div>
         </div>
-        <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-          letterSpacing:'0.15em'}}>DECK MODE</div>
       </div>
-      {/* Deck area */}
-      <div style={{width: W, height: H, background:'#fff', position:'relative',
-        overflow:'hidden'}}>
-        {children}
-      </div>
-    </div>
-  );
-}
-
-// Three deck pages
-function DeckSlide({ pageIndex, localElapsed }) {
-  // Slide-in entrance each time pageIndex changes
-  const pageStart = pageIndex === 0 ? 0.6 : pageIndex === 1 ? 2.2 : 3.8;
-  const sinceStart = localElapsed - pageStart;
-  const slideX = interpolate(sinceStart, [0, 0.5], [140, 0], Easing.easeOut);
-  const fadeIn = interpolate(sinceStart, [0, 0.4], [0, 1]);
-
-  return (
-    <div key={pageIndex} style={{position:'absolute', inset:0,
-      opacity: fadeIn, transform: `translateX(${slideX}px)`}}>
-      {pageIndex === 0 && <CoverPage />}
-      {pageIndex === 1 && <DataPage />}
-      {pageIndex === 2 && <QuotePage />}
     </div>
-  );
-}
-
-function CoverPage() {
-  return (
-    <div style={{padding: '80px 80px 60px', height:'100%', background:'#fff',
-      display:'flex', flexDirection:'column'}}>
-      <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.3em',
-        color: TERRA, marginBottom: 14}}>
-        VOL.01 · ANNUAL REPORT
-      </div>
-      <div style={{flex: 1, display:'flex', flexDirection:'column',
-        justifyContent:'center'}}>
-        <div style={{fontFamily: serif, fontSize: 132, fontWeight: 500,
-          color: INK, lineHeight: 1.02, letterSpacing:'-0.02em'}}>
-          2026<br/>
-          <span style={{fontStyle:'italic'}}>设计年度</span>报告
-        </div>
-        <div style={{height: 1, background: INK, width: 380, marginTop: 36,
-          marginBottom: 28}} />
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 22,
-          color: ASH, letterSpacing:'0.02em'}}>
-          The shape of digital craft, from typography to motion.
-        </div>
-      </div>
+
+    <div class="key-hint" id="keyHint">
+      <span>键盘翻页</span>
+      <span class="kbd" id="kbdKey">→</span>
     </div>
-  );
-}
-
-function DataPage() {
-  const numbers = [
-    { big: '428', label: '项目交付', unit: 'projects' },
-    { big: '92%', label: '客户续约', unit: 'retention' },
-    { big: '3.1x', label: '交付提速', unit: 'vs 2025' },
-  ];
-  const bars = [
-    { h: 0.45, label: 'Q1' },
-    { h: 0.62, label: 'Q2' },
-    { h: 0.78, label: 'Q3' },
-    { h: 1.00, label: 'Q4', hi: true },
-  ];
-  return (
-    <div style={{padding: '60px 80px 56px', height:'100%', background:'#fff',
-      display:'flex', flexDirection:'column'}}>
-      <div style={{fontFamily: mono, fontSize: 11, letterSpacing:'0.3em',
-        color: TERRA, marginBottom: 10}}>SECTION 02 · NUMBERS</div>
-      <div style={{fontFamily: serif, fontSize: 56, fontWeight: 500, color: INK,
-        letterSpacing:'-0.015em', marginBottom: 36}}>
-        今年的三个关键数字
+  </div>
+
+  <!-- ====== Beat 2: Split Screen ====== -->
+  <div class="beat2" id="beat2">
+    <!-- LEFT: HTML deck -->
+    <div class="split-col" style="position: relative;">
+      <div class="split-label" id="labelLeft">HTML · <span class="em">只读演示</span></div>
+      <div class="split-window split-left" id="splitLeft">
+      <div class="mini-chrome">
+        <span class="d"></span><span class="d"></span><span class="d"></span>
+        <span class="label">localhost:8080/deck</span>
       </div>
-      <div style={{display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap: 48,
-        marginBottom: 40}}>
-        {numbers.map((n, i) => (
-          <div key={i}>
-            <div style={{fontFamily: serif, fontSize: 112, fontWeight: 400,
-              color: i === 2 ? TERRA : INK, lineHeight: 1, letterSpacing:'-0.02em'}}>
-              {n.big}
-            </div>
-            <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 22,
-              color: INK, marginTop: 10}}>
-              {n.label}
-            </div>
-            <div style={{fontFamily: mono, fontSize: 11, color: ASH,
-              letterSpacing:'0.2em', marginTop: 4}}>
-              {n.unit}
-            </div>
-          </div>
-        ))}
+      <div class="mini-slide">
+        <div class="mini-eye">AI 心理学 · 第 3 节</div>
+        <div class="mini-title">心智的<br/>可塑性</div>
+        <div class="mini-sub">Agent 不是工具,它有自己的偏好。</div>
+        <div class="mini-hair"></div>
       </div>
-      <div style={{flex: 1, display:'flex', alignItems:'flex-end', gap: 20,
-        paddingLeft: 4, borderTop:`1px solid ${LINE}`, paddingTop: 24}}>
-        {bars.map((b, i) => (
-          <div key={i} style={{flex: 1, display:'flex', flexDirection:'column',
-            alignItems:'center'}}>
-            <div style={{width:'78%', height: `${b.h * 180}px`,
-              background: b.hi ? TERRA : INK, marginBottom: 10}} />
-            <div style={{fontFamily: mono, fontSize: 11, color: ASH,
-              letterSpacing:'0.2em'}}>{b.label}</div>
-          </div>
-        ))}
       </div>
     </div>
-  );
-}
-
-function QuotePage() {
-  return (
-    <div style={{padding: '80px', height:'100%', background:'#faf6ef',
-      display:'flex', flexDirection:'column', justifyContent:'center',
-      alignItems:'center', position:'relative'}}>
-      <div style={{position:'absolute', top: 64, left: 80,
-        fontFamily: mono, fontSize: 11, letterSpacing:'0.3em', color: TERRA}}>
-        EPIGRAPH · III
-      </div>
-      <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 104,
-        fontWeight: 400, color: INK, lineHeight: 1.15, letterSpacing:'-0.015em',
-        textAlign:'center', maxWidth: 1100}}>
-        "Less,<br/>but <span style={{color: TERRA}}>better</span>."
-      </div>
-      <div style={{height: 1, background: INK, width: 140, marginTop: 44,
-        marginBottom: 20}} />
-      <div style={{fontFamily: serif, fontSize: 22, color: ASH,
-        letterSpacing:'0.08em'}}>
-        — Dieter Rams
-      </div>
+
+    <!-- Connector -->
+    <div class="connector" id="connector">
+      <svg viewBox="0 0 56 120" fill="none">
+        <line x1="4" y1="60" x2="52" y2="60" stroke="#D97757" stroke-width="1.5" stroke-dasharray="4 4"/>
+        <polygon points="44,54 54,60 44,66" fill="#D97757"/>
+      </svg>
     </div>
-  );
-}
-
-// ══════════════════════════════════════════════════════════
-// Scene 3 (9 – 15s) · 导出流水线
-// ══════════════════════════════════════════════════════════
-function Scene3_Pipeline() {
-  const { elapsed } = useSprite();
-  const titleOp = interpolate(elapsed, [0, 0.6], [0, 1]);
-  const fadeOut = interpolate(elapsed, [5.6, 6.0], [1, 0]);
-
-  const nodes = [
-    { title: 'HTML Deck', sub: 'source of truth', icon: 'code', delay: 0.4 },
-    { title: 'html2pptx.js', sub: 'read computedStyle', icon: 'scan', delay: 1.1, hi: true },
-    { title: 'pptxgenjs', sub: 'assemble objects', icon: 'compose', delay: 1.8 },
-    { title: 'deck.pptx', sub: 'editable output', icon: 'doc', delay: 2.5 },
-  ];
-
-  const cmdOp = interpolate(elapsed, [3.8, 4.4], [0, 1]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
-      padding: '72px 96px 56px', display:'flex', flexDirection:'column'}}>
-      <div style={{display:'flex', justifyContent:'space-between',
-        alignItems:'baseline', opacity: titleOp, marginBottom: 12}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11, letterSpacing:'0.3em',
-            color: TERRA, marginBottom: 10}}>
-            <span>●</span>  SCENE 03 · EXPORT PIPELINE
-          </div>
-          <div style={{fontFamily: mono, fontSize: 64, fontWeight: 500,
-            color: INK, letterSpacing:'0.04em'}}>
-            导出流水线
-          </div>
-        </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 20,
-          color: ASH, textAlign:'right', maxWidth: 380, lineHeight: 1.5}}>
-          把 DOM 翻译成<br/>
-          PowerPoint 对象图
-        </div>
-      </div>
+    <div class="connector-label" id="connectorLabel">html2pptx.js</div>
 
-      <div style={{height: 1, background: INK, width: '100%', opacity: titleOp,
-        marginTop: 28, marginBottom: 48}} />
-
-      {/* Pipeline nodes */}
-      <div style={{display:'flex', alignItems:'stretch', gap: 0, flex: 1,
-        position:'relative'}}>
-        {nodes.map((n, i) => {
-          const op = interpolate(elapsed, [n.delay, n.delay + 0.5], [0, 1]);
-          const ty = interpolate(elapsed, [n.delay, n.delay + 0.5], [28, 0], Easing.easeOut);
-          return (
-            <React.Fragment key={i}>
-              <div style={{flex: 1, opacity: op, transform: `translateY(${ty}px)`,
-                background: n.hi ? TERRA : '#fff',
-                border: `1px solid ${n.hi ? TERRA : LINE}`,
-                padding:'28px 24px', display:'flex', flexDirection:'column',
-                color: n.hi ? '#fff' : INK}}>
-                <div style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.25em',
-                  opacity: n.hi ? 0.85 : 0.5, marginBottom: 18}}>
-                  STEP {String(i+1).padStart(2, '0')}
-                </div>
-                <NodeIcon kind={n.icon} hi={n.hi} />
-                <div style={{fontFamily: mono, fontSize: 20, fontWeight: 500,
-                  marginTop: 20, letterSpacing:'0.01em'}}>
-                  {n.title}
-                </div>
-                <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 15,
-                  opacity: n.hi ? 0.85 : 0.6, marginTop: 6}}>
-                  {n.sub}
-                </div>
-              </div>
-              {i < nodes.length - 1 && (
-                <ArrowBetween elapsed={elapsed} startTime={n.delay + 0.4} />
-              )}
-            </React.Fragment>
-          );
-        })}
+    <!-- RIGHT: PowerPoint -->
+    <div class="split-col" style="position: relative;">
+      <div class="split-label" id="labelRight">PowerPoint · <span class="em">真文本框可改</span></div>
+      <div class="split-window split-right" id="splitRight">
+      <div class="ppt-titlebar">
+        <div class="pp-logo">P</div>
+        <div class="title-text">AI-心理学-演讲.pptx - PowerPoint</div>
+        <div class="win-dots"><span></span><span></span><span></span></div>
       </div>
-
-      {/* Data flow caption */}
-      <div style={{marginTop: 36, display:'flex', alignItems:'center', gap: 24,
-        opacity: interpolate(elapsed, [3.2, 3.8], [0, 1])}}>
-        <div style={{fontFamily: mono, fontSize: 13, color: ASH,
-          letterSpacing:'0.05em', flex: 1}}>
-          <span style={{color: OLIVE}}>DOM node</span> <span style={{color: TERRA}}>→</span>{' '}
-          <span style={{color: INK}}>{'{ type, text, font, color, x, y }'}</span>
-          <span style={{color: ASH, margin:'0 14px'}}>·</span>
-          <span style={{color: TERRA}}>→</span> <span style={{color: INK}}>slide.addText(...) / slide.addShape(...)</span>
+      <div class="ppt-toolbar">
+        <div class="tool">
+          <span class="ico"></span>
+          <span class="font-name"><span id="fontName">Noto Serif SC</span><span style="opacity:0.5">▾</span></span>
         </div>
+        <div class="divider"></div>
+        <div class="tool"><span style="font-weight:700">B</span></div>
+        <div class="tool" style="font-style:italic">I</div>
+        <div class="tool" style="text-decoration:underline">U</div>
+        <div class="divider"></div>
+        <div class="tool active"><span class="ico" style="background:#D97757;border-color:#D97757"></span></div>
       </div>
 
-      {/* Command subtitle */}
-      <div style={{marginTop: 22, opacity: cmdOp,
-        background:'#1a1a1a', padding:'16px 24px',
-        borderLeft: `3px solid ${TERRA}`,
-        display:'flex', alignItems:'center', gap: 16}}>
-        <div style={{fontFamily: mono, fontSize: 12, color: TERRA,
-          letterSpacing:'0.2em'}}>$</div>
-        <div style={{fontFamily: mono, fontSize: 15, color: '#f5f0e6',
-          letterSpacing:'0.02em'}}>
-          node export_deck_pptx.mjs deck.html <span style={{color: '#8ca577'}}>--mode editable</span>
-        </div>
-      </div>
-    </div>
-  );
-}
-
-function NodeIcon({ kind, hi }) {
-  const fg = hi ? '#fff' : INK;
-  const bg = hi ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.04)';
-  if (kind === 'code') {
-    return (
-      <div style={{width: 72, height: 72, background: bg,
-        display:'flex', alignItems:'center', justifyContent:'center'}}>
-        <svg width="34" height="34" viewBox="0 0 34 34" fill="none">
-          <path d="M12 10 L5 17 L12 24" stroke={fg} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
-          <path d="M22 10 L29 17 L22 24" stroke={fg} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
-          <path d="M19 7 L15 27" stroke={fg} strokeWidth="2" strokeLinecap="round"/>
-        </svg>
-      </div>
-    );
-  }
-  if (kind === 'scan') {
-    return (
-      <div style={{width: 72, height: 72, background: bg,
-        display:'flex', alignItems:'center', justifyContent:'center'}}>
-        <svg width="38" height="38" viewBox="0 0 38 38" fill="none">
-          <rect x="6" y="6" width="26" height="26" stroke={fg} strokeWidth="2"/>
-          <line x1="6" y1="15" x2="32" y2="15" stroke={fg} strokeWidth="1.5"/>
-          <line x1="6" y1="23" x2="32" y2="23" stroke={fg} strokeWidth="1.5"/>
-          <line x1="15" y1="6" x2="15" y2="32" stroke={fg} strokeWidth="1.5"/>
-          <line x1="23" y1="6" x2="23" y2="32" stroke={fg} strokeWidth="1.5"/>
-          <circle cx="19" cy="19" r="3" fill={fg}/>
-        </svg>
-      </div>
-    );
-  }
-  if (kind === 'compose') {
-    return (
-      <div style={{width: 72, height: 72, background: bg,
-        display:'flex', alignItems:'center', justifyContent:'center'}}>
-        <svg width="38" height="38" viewBox="0 0 38 38" fill="none">
-          <rect x="4" y="4" width="16" height="12" stroke={fg} strokeWidth="2"/>
-          <rect x="22" y="4" width="12" height="12" stroke={fg} strokeWidth="2"/>
-          <rect x="4" y="20" width="12" height="14" stroke={fg} strokeWidth="2"/>
-          <rect x="18" y="20" width="16" height="14" stroke={fg} strokeWidth="2"/>
-        </svg>
-      </div>
-    );
-  }
-  // doc
-  return (
-    <div style={{width: 72, height: 72, background: bg,
-      display:'flex', alignItems:'center', justifyContent:'center'}}>
-      <svg width="34" height="38" viewBox="0 0 34 38" fill="none">
-        <path d="M6 4 H22 L28 10 V34 H6 Z" stroke={fg} strokeWidth="2" strokeLinejoin="round"/>
-        <path d="M22 4 V10 H28" stroke={fg} strokeWidth="2" strokeLinejoin="round"/>
-        <line x1="11" y1="17" x2="23" y2="17" stroke={fg} strokeWidth="1.5"/>
-        <line x1="11" y1="22" x2="23" y2="22" stroke={fg} strokeWidth="1.5"/>
-        <line x1="11" y1="27" x2="19" y2="27" stroke={fg} strokeWidth="1.5"/>
-      </svg>
-    </div>
-  );
-}
-
-function ArrowBetween({ elapsed, startTime }) {
-  const reveal = interpolate(elapsed, [startTime, startTime + 0.3], [0, 1]);
-  return (
-    <div style={{width: 48, display:'flex', alignItems:'center',
-      justifyContent:'center', position:'relative'}}>
-      <svg width="48" height="24" viewBox="0 0 48 24" style={{opacity: reveal}}>
-        <line x1="0" y1="12" x2={34 * reveal + 8} y2="12" stroke={TERRA} strokeWidth="1.5"/>
-        {reveal > 0.6 && (
-          <path d="M38 6 L44 12 L38 18" stroke={TERRA} strokeWidth="1.5" fill="none"
-            strokeLinecap="round" strokeLinejoin="round"/>
-        )}
-      </svg>
-    </div>
-  );
-}
-
-// ══════════════════════════════════════════════════════════
-// Scene 4 (15 – 20s) · 产物:可编辑文本框
-// ══════════════════════════════════════════════════════════
-function Scene4_PPTEdit() {
-  const { elapsed } = useSprite();
-  const fadeIn = interpolate(elapsed, [0, 0.5], [0, 1]);
-  const pptScale = interpolate(elapsed, [0, 0.8], [0.94, 1], Easing.easeOut);
-
-  // Selection bounding box appears at 0.8s, handles animate in staggered
-  const selectOp = interpolate(elapsed, [0.9, 1.3], [0, 1]);
-
-  // Format panel slides in from right at 1.5s
-  const panelX = interpolate(elapsed, [1.6, 2.4], [80, 0], Easing.easeOut);
-  const panelOp = interpolate(elapsed, [1.6, 2.4], [0, 1]);
-
-  // Caption fades in 2.4s
-  const captionOp = interpolate(elapsed, [2.4, 3.0], [0, 1]);
-
-  // Checkboxes tick in sequentially
-  const chk1 = elapsed > 3.2 ? 1 : 0;
-  const chk2 = elapsed > 3.7 ? 1 : 0;
-  const chk3 = elapsed > 4.2 ? 1 : 0;
-
-  const fadeOut = interpolate(elapsed, [4.6, 5.0], [1, 0]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM,
-      opacity: fadeIn * fadeOut,
-      display:'flex', flexDirection:'column', alignItems:'center',
-      padding:'60px 60px 40px'}}>
-      <div style={{width:'100%', display:'flex', justifyContent:'space-between',
-        alignItems:'baseline', marginBottom: 20}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11, letterSpacing:'0.3em',
-            color: TERRA, marginBottom: 8}}>
-            <span>●</span>  SCENE 04 · THE ARTIFACT
+      <div class="ppt-canvas">
+        <div class="ppt-slide">
+          <div class="ppt-eye">AI 心理学 · 第 3 节</div>
+          <div class="ppt-title-frame" id="titleFrame">
+            <span class="handle tl"></span>
+            <span class="handle tr"></span>
+            <span class="handle bl"></span>
+            <span class="handle br"></span>
+            <span class="ppt-title" id="titleText">心智的可塑性</span><span class="edit-caret" id="caret"></span>
           </div>
-          <div style={{fontFamily: serif, fontSize: 52, fontWeight: 500, color: INK,
-            letterSpacing:'-0.01em'}}>
-            产物:可编辑文本框
-          </div>
-        </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18, color: ASH,
-          textAlign:'right', maxWidth: 340, lineHeight: 1.5}}>
-          在 PowerPoint 里<br/>
-          像素级复现,字还是字
+          <div class="ppt-sub">Agent 不是工具,它有自己的偏好。</div>
+          <div class="ppt-hair"></div>
         </div>
-      </div>
 
-      <div style={{position:'relative', transform: `scale(${pptScale})`,
-        transformOrigin:'center center'}}>
-        <PPTMockup selectOp={selectOp} />
-
-        {/* Format panel */}
-        <div style={{position:'absolute', top: 94, right: -296,
-          width: 272, background:'#f5f2ed', border:`1px solid ${LINE}`,
-          boxShadow:'0 12px 30px rgba(0,0,0,0.08)',
-          transform: `translateX(${panelX}px)`, opacity: panelOp,
-          padding: 0}}>
-          <FormatPanel />
-        </div>
-      </div>
-
-      <div style={{marginTop: 28, display:'flex', alignItems:'center', gap: 48,
-        opacity: captionOp}}>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 24,
-          color: TERRA, letterSpacing:'0.01em'}}>
-          原生 PowerPoint 文本框 · 不是图片
-        </div>
-        <div style={{display:'flex', gap: 28, fontFamily: mono, fontSize: 13}}>
-          <CheckRow label="文字可编辑" on={chk1} />
-          <CheckRow label="字体保留" on={chk2} />
-          <CheckRow label="位置/颜色精确" on={chk3} />
-        </div>
-      </div>
-    </div>
-  );
-}
-
-function CheckRow({ label, on }) {
-  return (
-    <div style={{display:'flex', alignItems:'center', gap: 8}}>
-      <div style={{width: 18, height: 18, border:`1.5px solid ${on ? TERRA : LINE}`,
-        background: on ? TERRA : 'transparent',
-        display:'flex', alignItems:'center', justifyContent:'center',
-        transition:'none'}}>
-        {on ? (
-          <svg width="12" height="12" viewBox="0 0 12 12">
-            <path d="M2 6 L5 9 L10 3" stroke="#fff" strokeWidth="2" fill="none"
-              strokeLinecap="round" strokeLinejoin="round"/>
+        <!-- Cursor arrow -->
+        <div class="cursor" id="cursor">
+          <svg viewBox="0 0 22 30" fill="none">
+            <path d="M2 2 L2 22 L8 17 L12 26 L16 24 L12 15 L20 14 Z"
+                  fill="#1A1918" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/>
           </svg>
-        ) : null}
-      </div>
-      <span style={{color: on ? INK : ASH}}>{label}</span>
-    </div>
-  );
-}
-
-function PPTMockup({ selectOp }) {
-  const W = 1100, H = 620;
-  return (
-    <div style={{width: W, height: H, background:'#f4f1ec',
-      border:`1px solid ${LINE}`, boxShadow:'0 22px 50px rgba(0,0,0,0.14)',
-      display:'flex', flexDirection:'column'}}>
-      {/* PPT ribbon (title bar + tabs) */}
-      <div style={{height: 32, background:'#dcd7cd', display:'flex',
-        alignItems:'center', padding:'0 14px', gap: 8,
-        borderBottom:`1px solid ${LINE}`}}>
-        <div style={{width:10, height:10, borderRadius:'50%', background:'#ff5f57'}} />
-        <div style={{width:10, height:10, borderRadius:'50%', background:'#febc2e'}} />
-        <div style={{width:10, height:10, borderRadius:'50%', background:'#28c840'}} />
-        <div style={{flex: 1, textAlign:'center', fontFamily: sans, fontSize: 11,
-          color: ASH, letterSpacing:'0.02em'}}>
-          deck.pptx — PowerPoint
         </div>
+        <!-- Double-click ripple -->
+        <div class="dblclick-ripple" id="ripple"></div>
       </div>
-      <div style={{height: 34, background:'#ebe7de', display:'flex',
-        alignItems:'center', padding:'0 18px', gap: 22,
-        fontFamily: sans, fontSize: 11, color: INK,
-        borderBottom:`1px solid ${LINE}`}}>
-        <span style={{color: TERRA, fontWeight: 600,
-          borderBottom: `2px solid ${TERRA}`, paddingBottom: 6,
-          marginBottom: -7}}>Home</span>
-        <span style={{opacity: 0.55}}>Insert</span>
-        <span style={{opacity: 0.55}}>Design</span>
-        <span style={{opacity: 0.55}}>Transitions</span>
-        <span style={{opacity: 0.55}}>Animations</span>
-        <span style={{opacity: 0.55}}>Slide Show</span>
-        <span style={{opacity: 0.55}}>Review</span>
-        <span style={{opacity: 0.55}}>View</span>
       </div>
+    </div>
+  </div>
 
-      {/* Body: slide panel (left) + slide canvas (main) */}
-      <div style={{flex: 1, display:'flex'}}>
-        {/* Thumbnails */}
-        <div style={{width: 160, background:'#eae5db',
-          borderRight:`1px solid ${LINE}`, padding:'12px 12px',
-          display:'flex', flexDirection:'column', gap: 8}}>
-          {[0,1,2,3].map(i => (
-            <div key={i} style={{
-              background:'#fff',
-              border: i === 2 ? `2px solid ${TERRA}` : `1px solid ${LINE}`,
-              aspectRatio:'16/9', position:'relative',
-              padding: 8, display:'flex', alignItems:'center',
-              justifyContent:'center'}}>
-              <div style={{position:'absolute', top: 4, left: 4,
-                fontFamily: mono, fontSize: 8, color: ASH}}>{i+1}</div>
-              {i === 2 && (
-                <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 12,
-                  color: INK}}>"Less..."</div>
-              )}
-              {i !== 2 && (
-                <div style={{width:'70%', height: 3, background: LINE}} />
-              )}
-            </div>
-          ))}
-        </div>
+  <!-- ====== Brand Reveal (米色面板 · hero-v10 signature) ====== -->
+  <div class="brand-panel" id="brandPanel"></div>
+  <div class="brand-reveal" id="brandReveal">
+    <div class="brand-wordmark" id="wordmark">huashu<span class="accent">-</span>design</div>
+    <div class="brand-line" id="brandLine"></div>
+  </div>
 
-        {/* Slide canvas */}
-        <div style={{flex: 1, background:'#e8e4dc', display:'flex',
-          alignItems:'center', justifyContent:'center', padding: 32,
-          position:'relative'}}>
-          <div style={{width: 720, height: 405, background:'#faf6ef',
-            boxShadow:'0 8px 24px rgba(0,0,0,0.1)',
-            border:`1px solid ${LINE}`, position:'relative'}}>
-            {/* The editable text box */}
-            <div style={{position:'absolute', top:'50%', left:'50%',
-              transform:'translate(-50%, -50%)', textAlign:'center',
-              padding:'18px 40px'}}>
-              <div style={{fontFamily: serif, fontStyle:'italic',
-                fontSize: 72, color: INK, lineHeight: 1.1,
-                letterSpacing:'-0.01em'}}>
-                "Less, but <span style={{color: TERRA}}>better</span>."
-              </div>
-              <div style={{fontFamily: serif, fontSize: 14, color: ASH,
-                marginTop: 14, letterSpacing:'0.1em'}}>
-                — Dieter Rams
-              </div>
-            </div>
-            {/* Selection bounding box + 8 handles */}
-            {selectOp > 0 && <SelectionBox opacity={selectOp} />}
-
-            {/* slide number */}
-            <div style={{position:'absolute', bottom: 10, right: 14,
-              fontFamily: sans, fontSize: 9, color: ASH}}>3</div>
-          </div>
-        </div>
-      </div>
-    </div>
-  );
-}
-
-function SelectionBox({ opacity }) {
-  // Box centered around the textbox (~ 520×160)
-  const BW = 560, BH = 170;
-  const color = '#4a9eff';
-  const handles = [
-    { x: 0, y: 0 }, { x: 0.5, y: 0 }, { x: 1, y: 0 },
-    { x: 0, y: 0.5 }, { x: 1, y: 0.5 },
-    { x: 0, y: 1 }, { x: 0.5, y: 1 }, { x: 1, y: 1 },
-  ];
-  return (
-    <div style={{position:'absolute', top:'50%', left:'50%',
-      width: BW, height: BH, transform:'translate(-50%, -50%)',
-      border: `1.5px solid ${color}`, opacity,
-      boxShadow:`0 0 0 1px rgba(255,255,255,0.6)`, pointerEvents:'none'}}>
-      {handles.map((h, i) => (
-        <div key={i} style={{position:'absolute',
-          left: `${h.x * 100}%`, top: `${h.y * 100}%`,
-          transform:'translate(-50%, -50%)',
-          width: 10, height: 10, background:'#fff',
-          border: `1.5px solid ${color}`, borderRadius: 2}} />
-      ))}
-      {/* Rotate handle */}
-      <div style={{position:'absolute', left:'50%', top: -26,
-        transform:'translateX(-50%)',
-        width: 10, height: 10, background:'#fff',
-        border: `1.5px solid ${color}`, borderRadius: '50%'}} />
-      <div style={{position:'absolute', left:'50%', top: -17,
-        transform:'translateX(-50%)', width: 1, height: 9, background: color}} />
-      {/* Label: Text Box */}
-      <div style={{position:'absolute', top: -26, left: 0,
-        fontFamily: mono, fontSize: 10, color: color,
-        background:'rgba(255,255,255,0.9)', padding:'2px 6px',
-        letterSpacing:'0.1em'}}>
-        TEXT BOX · shape #1
-      </div>
-    </div>
-  );
-}
-
-function FormatPanel() {
-  return (
-    <div>
-      <div style={{padding:'12px 16px', borderBottom:`1px solid ${LINE}`,
-        display:'flex', justifyContent:'space-between', alignItems:'center',
-        background:'#ebe7de'}}>
-        <div style={{fontFamily: sans, fontSize: 12, color: INK,
-          fontWeight: 600}}>Format Text</div>
-        <div style={{fontFamily: mono, fontSize: 10, color: ASH}}>✕</div>
-      </div>
-      <div style={{padding:'16px'}}>
-        <div style={{fontFamily: sans, fontSize: 10, color: ASH,
-          letterSpacing:'0.15em', marginBottom: 8}}>FONT</div>
-        <div style={{background:'#fff', border:`1px solid ${LINE}`,
-          padding:'8px 10px', marginBottom: 14,
-          display:'flex', justifyContent:'space-between', alignItems:'center',
-          fontFamily: sans, fontSize: 12, color: INK}}>
-          <span style={{fontFamily: serif, fontStyle:'italic'}}>Newsreader</span>
-          <span style={{color: ASH, fontSize: 10}}>▾</span>
-        </div>
-        <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap: 10,
-          marginBottom: 14}}>
-          <div>
-            <div style={{fontFamily: sans, fontSize: 10, color: ASH,
-              letterSpacing:'0.15em', marginBottom: 6}}>SIZE</div>
-            <div style={{background:'#fff', border:`1px solid ${LINE}`,
-              padding:'6px 10px', fontFamily: sans, fontSize: 12}}>72 pt</div>
-          </div>
-          <div>
-            <div style={{fontFamily: sans, fontSize: 10, color: ASH,
-              letterSpacing:'0.15em', marginBottom: 6}}>WEIGHT</div>
-            <div style={{background:'#fff', border:`1px solid ${LINE}`,
-              padding:'6px 10px', fontFamily: sans, fontSize: 12}}>400 · italic</div>
-          </div>
-        </div>
-        <div style={{fontFamily: sans, fontSize: 10, color: ASH,
-          letterSpacing:'0.15em', marginBottom: 6}}>COLOR</div>
-        <div style={{display:'flex', gap: 8, marginBottom: 14}}>
-          <div style={{width: 28, height: 28, background: INK, border:`2px solid ${INK}`}} />
-          <div style={{width: 28, height: 28, background: TERRA,
-            outline:`2px solid ${TERRA}`, outlineOffset: 1}} />
-          <div style={{width: 28, height: 28, background: OLIVE}} />
-          <div style={{width: 28, height: 28, background: DEEP_BLUE}} />
-          <div style={{width: 28, height: 28, background:'#fff', border:`1px solid ${LINE}`}} />
-        </div>
-        <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-          letterSpacing:'0.1em', lineHeight: 1.6,
-          paddingTop: 10, borderTop:`1px solid ${LINE}`}}>
-          x: 2.4in · y: 2.1in<br/>
-          w: 5.8in · h: 1.7in
-        </div>
-      </div>
-    </div>
-  );
-}
-
-// ══════════════════════════════════════════════════════════
-// Scene 5 (20 – 24s) · 收尾
-// ══════════════════════════════════════════════════════════
-function Scene5_Final() {
-  const { elapsed } = useSprite();
-  const tagOp = interpolate(elapsed, [0, 0.5], [0, 1]);
-  const mainY = interpolate(elapsed, [0.2, 1.2], [50, 0], Easing.easeOut);
-  const mainOp = interpolate(elapsed, [0.2, 1.0], [0, 1]);
-  const lineW = interpolate(elapsed, [1.1, 1.8], [0, 540]);
-  const subOp = interpolate(elapsed, [1.5, 2.2], [0, 1]);
-  const monoOp = interpolate(elapsed, [2.2, 2.8], [0, 1]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM,
-      display:'flex', alignItems:'center', justifyContent:'center',
-      flexDirection:'column'}}>
-      <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
-        color: TERRA, marginBottom: 32, opacity: tagOp}}>
-        ONE SOURCE · TWO STATES
-      </div>
-      <div style={{fontFamily: serif, fontSize: 220, fontWeight: 500,
-        color: INK, lineHeight: 0.98, letterSpacing:'-0.03em',
-        opacity: mainOp, transform: `translateY(${mainY}px)`}}>
-        一<span style={{color: ASH, fontStyle:'italic'}}>源</span>
-        <span style={{color: TERRA, margin:'0 28px'}}>·</span>
-        双<span style={{color: ASH, fontStyle:'italic'}}>态</span>
-      </div>
-      <div style={{height: 1, background: INK, width: lineW, marginTop: 46}} />
-      <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 28,
-        color: ASH, marginTop: 28, opacity: subOp, letterSpacing:'0.02em'}}>
-        浏览器里演讲  ·  PowerPoint 里二次编辑
-      </div>
-      <div style={{fontFamily: mono, fontSize: 18, color: INK, marginTop: 34,
-        opacity: monoOp, letterSpacing:'0.1em',
-        padding:'12px 28px', background:'#fff', border:`1px solid ${LINE}`}}>
-        <span style={{color: OLIVE}}>deck.html</span>
-        <span style={{color: TERRA, margin:'0 14px'}}>⇌</span>
-        <span style={{color: DEEP_BLUE}}>deck.pptx</span>
-      </div>
-    </div>
-  );
-}
-
-// ── Watermark ─────────────────────────────────────────────
-function Watermark() {
-  return (
-    <div style={{position:'absolute', bottom: 24, right: 32,
-      fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
-      fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
-      Created by Huashu-Design
-    </div>
-  );
-}
-
-// ── Main composition ──────────────────────────────────────
-function App() {
-  return (
-    <Stage duration={24} width={1920} height={1080} bgColor={CREAM}>
-      <Sprite start={0} end={3}><Scene1_Title /></Sprite>
-      <Sprite start={3} end={9}><Scene2_DeckFlip /></Sprite>
-      <Sprite start={9} end={15}><Scene3_Pipeline /></Sprite>
-      <Sprite start={15} end={20}><Scene4_PPTEdit /></Sprite>
-      <Sprite start={20} end={24}><Scene5_Final /></Sprite>
-      <Watermark />
-    </Stage>
-  );
-}
-
-ReactDOM.createRoot(document.getElementById('root')).render(<App />);
+</div>
+
+<script>
+(function() {
+  // ---------- Fit stage ----------
+  const stage = document.getElementById('stage');
+  function rescale() {
+    const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  rescale();
+  window.addEventListener('resize', rescale);
+
+  // ---------- Easings ----------
+  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
+  const expoOut = t => (t <= 0) ? 0 : (t >= 1) ? 1 : 1 - Math.pow(2, -10 * t);
+  const expoIn = t => (t <= 0) ? 0 : (t >= 1) ? 1 : Math.pow(2, 10 * (t - 1));
+  const easeOut = t => 1 - Math.pow(1 - t, 3);
+  const easeInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;
+  function lerp(time, start, end, fromV, toV, ease) {
+    if (time <= start) return fromV;
+    if (time >= end) return toV;
+    let p = (time - start) / (end - start);
+    if (ease) p = ease(p);
+    return fromV + (toV - fromV) * p;
+  }
+  function clampLerp(time, start, end) {
+    if (time <= start) return 0;
+    if (time >= end) return 1;
+    return (time - start) / (end - start);
+  }
+
+  // ---------- Timeline (10s total) ----------
+  const T = {
+    DURATION: 10.0,
+
+    // Beat 1: 0 - 2s
+    deckIn:       [0.15, 0.9],       // browser fade+rise
+    keyHintIn:    [0.6, 1.1],
+    keyPress:     [1.25, 1.4],       // arrow key highlight
+    slideFlip:    [1.3, 1.9],        // slide A→B
+    beat1Out:     [2.0, 2.4],
+
+    // Beat 2: split screen: 2.2 - 8.0s
+    beat2In:      [2.3, 2.9],
+    labelsIn:     [3.0, 3.5],
+
+    cursorIn:     [3.1, 3.4],        // cursor arrives on right side
+    cursorMove1:  [3.4, 4.1],        // cursor moves to title
+    dblclick:     [4.1, 4.3],        // double click
+    frameSelect:  [4.15, 4.35],      // frame shows handles
+    frameEdit:    [4.4, 4.55],       // frame enters edit mode
+    caretShowStart: 4.5,
+    textDelete:   [4.6, 5.4],        // delete original text char by char
+    textRetype:   [5.5, 7.2],        // type new text char by char
+    commitEdit:   [7.3, 7.5],        // exit edit mode
+
+    connectorIn:  [3.3, 3.9],
+    beat2Out:     [8.0, 8.3],        // main scene fades to 0 (0.3s)
+
+    // Brand Reveal (米色面板 · hero-v10 signature): 8.3 - 10s
+    // panelRise 与 beat2Out 微重叠 0.05s,避免黑屏间隙
+    panelRise:    [8.25, 8.7],       // 米色面板 translateY 100%→0 (expoOut)
+    wordmarkIn:   [8.7, 9.3],        // wordmark opacity 0→1 + translateY 20→0 + weight 100→500 (0.6s, expoOut)
+    brandLineIn:  [9.3, 9.7],        // brand-line expand 0→280px (0.4s, cubicOut)
+    brandHold:    [9.7, 10.0],       // hold (0.3s)
+  };
+
+  // ---------- Elements ----------
+  const beat1 = document.getElementById('beat1');
+  const beat2 = document.getElementById('beat2');
+  const brandReveal = document.getElementById('brandReveal');
+  const deckWindow = document.getElementById('deckWindow');
+  const pageCount = document.getElementById('pageCount');
+  const slideA = document.getElementById('slideA');
+  const slideB = document.getElementById('slideB');
+  const keyHint = document.getElementById('keyHint');
+  const kbdKey = document.getElementById('kbdKey');
+  const splitLeft = document.getElementById('splitLeft');
+  const splitRight = document.getElementById('splitRight');
+  const labelLeft = document.getElementById('labelLeft');
+  const labelRight = document.getElementById('labelRight');
+  const connector = document.getElementById('connector');
+  const connectorLabel = document.getElementById('connectorLabel');
+  const cursor = document.getElementById('cursor');
+  const ripple = document.getElementById('ripple');
+  const titleFrame = document.getElementById('titleFrame');
+  const titleText = document.getElementById('titleText');
+  const caret = document.getElementById('caret');
+  const panel = document.getElementById('brandPanel');
+  const wordmark = document.getElementById('wordmark');
+  const brandLine = document.getElementById('brandLine');
+
+  // Text to animate
+  const ORIG_TEXT = '心智的可塑性';
+  const NEW_TEXT  = '心智 · 可塑性';
+
+  // ---------- Render ----------
+  function render(t) {
+
+    /* ======= Beat 1 ======= */
+    let beat1Op;
+    if (t < T.beat1Out[0]) {
+      beat1Op = lerp(t, T.deckIn[0], T.deckIn[1], 0, 1, expoOut);
+    } else {
+      beat1Op = 1 - clampLerp(t, T.beat1Out[0], T.beat1Out[1]);
+    }
+    beat1.style.opacity = beat1Op;
+    beat1.style.visibility = beat1Op > 0.01 ? 'visible' : 'hidden';
+
+    // Deck window rise
+    const deckRise = lerp(t, T.deckIn[0], T.deckIn[1], 24, 0, expoOut);
+    deckWindow.style.transform = `translate3d(0, ${deckRise}px, 0)`;
+
+    // Key hint appear
+    const khOp = clampLerp(t, T.keyHintIn[0], T.keyHintIn[1]);
+    keyHint.style.opacity = khOp;
+
+    // Key press flash
+    const kpActive = t >= T.keyPress[0] && t < T.keyPress[1] + 0.2;
+    if (kpActive) {
+      const kp = clampLerp(t, T.keyPress[0], T.keyPress[1]);
+      kbdKey.style.background = `rgba(217,119,87,${0.9 * (1 - kp * 0.4)})`;
+      kbdKey.style.color = '#fff';
+      kbdKey.style.transform = `scale(${1 - 0.08 * kp})`;
+    } else {
+      kbdKey.style.background = '';
+      kbdKey.style.color = '';
+      kbdKey.style.transform = '';
+    }
+
+    // Slide flip A→B
+    if (t >= T.slideFlip[0] && t < T.slideFlip[1] + 0.2) {
+      const sp = clampLerp(t, T.slideFlip[0], T.slideFlip[1]);
+      const eased = expoOut(sp);
+      slideA.style.opacity = 1 - eased;
+      slideA.style.transform = `translateX(${-60 * eased}px)`;
+      slideB.style.opacity = eased;
+      slideB.style.transform = `translateX(${60 * (1 - eased)}px)`;
+      // Update page count at midway
+      if (sp > 0.5) pageCount.textContent = '4 / 12';
+      else pageCount.textContent = '3 / 12';
+    } else if (t >= T.slideFlip[1]) {
+      slideA.style.opacity = 0;
+      slideB.style.opacity = 1;
+      slideB.style.transform = 'translateX(0)';
+      pageCount.textContent = '4 / 12';
+    } else {
+      slideA.style.opacity = 1;
+      slideA.style.transform = 'translateX(0)';
+      slideB.style.opacity = 0;
+      pageCount.textContent = '3 / 12';
+    }
+
+    /* ======= Beat 2 ======= */
+    let beat2Op = 0;
+    if (t >= T.beat2In[0] && t < T.beat2Out[1]) {
+      if (t < T.beat2In[1]) beat2Op = clampLerp(t, T.beat2In[0], T.beat2In[1]);
+      else if (t < T.beat2Out[0]) beat2Op = 1;
+      else beat2Op = 1 - clampLerp(t, T.beat2Out[0], T.beat2Out[1]);
+    }
+    beat2.style.opacity = beat2Op;
+    beat2.style.visibility = beat2Op > 0.01 ? 'visible' : 'hidden';
+
+    // Windows rise in
+    const splitInP = clampLerp(t, T.beat2In[0], T.beat2In[1]);
+    const splitRise = lerp(t, T.beat2In[0], T.beat2In[1], 28, 0, expoOut);
+    splitLeft.style.transform = `translate3d(${-8 * (1 - expoOut(splitInP))}px, ${splitRise}px, 0)`;
+    splitRight.style.transform = `translate3d(${8 * (1 - expoOut(splitInP))}px, ${splitRise}px, 0)`;
+
+    // Labels
+    const labelOp = clampLerp(t, T.labelsIn[0], T.labelsIn[1]);
+    labelLeft.style.opacity = labelOp * 0.7;
+    labelRight.style.opacity = labelOp * 0.85;
+
+    // Connector
+    const connOp = clampLerp(t, T.connectorIn[0], T.connectorIn[1]);
+    connector.style.opacity = connOp;
+    connectorLabel.style.opacity = connOp * 0.9;
+
+    /* === Cursor movement === */
+    // Cursor positions (relative to .ppt-canvas, which is inside split-right)
+    // Canvas starts at (0,0), size ~820 × 508 (580 - 32 - 40)
+    // Title sits around x=84 y=110 (inside .ppt-slide padding 56/64)
+    // We'll place cursor with absolute positioning inside .ppt-canvas.
+
+    // Entry point: off to the right bottom of canvas
+    const P_ENTER = { x: 720, y: 420 };
+    const P_TITLE = { x: 250, y: 170 }; // on the title
+
+    let cursorOp = 0;
+    let cx = P_ENTER.x, cy = P_ENTER.y;
+
+    if (t >= T.cursorIn[0] && t < T.beat2Out[0]) {
+      cursorOp = 1;
+      // Phase 1: appear (pop in with slight scale)
+      const inP = clampLerp(t, T.cursorIn[0], T.cursorIn[1]);
+      cursorOp = expoOut(inP);
+
+      // Phase 2: move to title
+      if (t >= T.cursorMove1[0]) {
+        const mp = clampLerp(t, T.cursorMove1[0], T.cursorMove1[1]);
+        const e = easeInOut(mp);
+        cx = P_ENTER.x + (P_TITLE.x - P_ENTER.x) * e;
+        cy = P_ENTER.y + (P_TITLE.y - P_ENTER.y) * e;
+      } else {
+        cx = P_ENTER.x;
+        cy = P_ENTER.y;
+      }
+
+      // After double-click, slight jitter toward caret position during typing
+      if (t >= T.textRetype[0] && t < T.textRetype[1]) {
+        cx = P_TITLE.x + 6;
+        cy = P_TITLE.y - 2;
+      }
+    } else if (t >= T.beat2Out[0]) {
+      cursorOp = 1 - clampLerp(t, T.beat2Out[0], T.beat2Out[1]);
+    }
+    cursor.style.opacity = cursorOp;
+    cursor.style.transform = `translate(${cx}px, ${cy}px)`;
+
+    /* === Double-click ripple === */
+    // Ripple pulses twice at T.dblclick start
+    let rippleVisible = false;
+    if (t >= T.dblclick[0] && t < T.dblclick[0] + 0.7) {
+      const dt = t - T.dblclick[0];
+      // Two rapid pulses
+      const pulse1 = clamp(dt / 0.25, 0, 1);
+      const pulse2 = clamp((dt - 0.15) / 0.25, 0, 1);
+      const scale1 = 0.4 + pulse1 * 1.4;
+      const scale2 = 0.4 + pulse2 * 1.4;
+      const op1 = 1 - pulse1;
+      const op2 = dt > 0.15 ? (1 - pulse2) : 0;
+      // Render as single element: use larger of the two
+      const scale = Math.max(scale1, scale2);
+      const op = Math.max(op1, op2);
+      ripple.style.opacity = op;
+      ripple.style.transform = `translate(-50%, -50%) translate(${P_TITLE.x + 6}px, ${P_TITLE.y + 26}px) scale(${scale})`;
+      rippleVisible = true;
+    }
+    if (!rippleVisible) ripple.style.opacity = 0;
+
+    /* === Frame states: selected → editing === */
+    titleFrame.classList.remove('selected', 'editing');
+    if (t >= T.frameSelect[0] && t < T.frameEdit[0]) {
+      titleFrame.classList.add('selected');
+    } else if (t >= T.frameEdit[0] && t < T.commitEdit[1]) {
+      titleFrame.classList.add('editing');
+    }
+
+    /* === Text animation: delete → retype === */
+    let displayedText = ORIG_TEXT;
+    let caretOp = 0;
+
+    if (t < T.textDelete[0]) {
+      displayedText = ORIG_TEXT;
+      caretOp = t >= T.caretShowStart ? 1 : 0;
+    } else if (t < T.textDelete[1]) {
+      // Delete: remove chars from end
+      const dp = clampLerp(t, T.textDelete[0], T.textDelete[1]);
+      const charsToRemove = Math.floor(dp * ORIG_TEXT.length);
+      displayedText = ORIG_TEXT.slice(0, ORIG_TEXT.length - charsToRemove);
+      caretOp = 1;
+    } else if (t < T.textRetype[0]) {
+      displayedText = '';
+      caretOp = 1;
+    } else if (t < T.textRetype[1]) {
+      // Retype new text
+      const rp = clampLerp(t, T.textRetype[0], T.textRetype[1]);
+      const charsToShow = Math.floor(rp * NEW_TEXT.length);
+      displayedText = NEW_TEXT.slice(0, charsToShow);
+      caretOp = 1;
+    } else if (t < T.commitEdit[1]) {
+      displayedText = NEW_TEXT;
+      // Caret blinks while still in edit mode
+      caretOp = (Math.floor(t * 2) % 2 === 0) ? 1 : 0.3;
+    } else {
+      displayedText = NEW_TEXT;
+      caretOp = 0;
+    }
+
+    // Blinking during idle-in-edit phases (when not actively typing/deleting)
+    if (t >= T.caretShowStart && t < T.textDelete[0]) {
+      caretOp = (Math.floor((t - T.caretShowStart) * 3) % 2 === 0) ? 1 : 0.35;
+    }
+
+    titleText.textContent = displayedText;
+    caret.style.opacity = caretOp;
+
+    /* ======= Brand Reveal (米色面板 · hero-v10 signature) ======= */
+    // Panel rises from bottom (米色面板 #F5F4F0)
+    const panelP = clampLerp(t, T.panelRise[0], T.panelRise[1]);
+    panel.style.transform = `translateY(${(1 - expoOut(panelP)) * 100}%)`;
+
+    // brand-reveal container visible once panel starts rising
+    brandReveal.style.opacity = panelP > 0.01 ? 1 : 0;
+
+    // Wordmark: opacity 0→1 + translateY 20→0 + weight 100→500 (expoOut)
+    const wmP = clampLerp(t, T.wordmarkIn[0], T.wordmarkIn[1]);
+    const wmEased = expoOut(wmP);
+    wordmark.style.opacity = wmEased;
+    const wmRise = (1 - wmEased) * 20;
+    wordmark.style.transform = `translate3d(0, ${wmRise}px, 0)`;
+    const w = 100 + (500 - 100) * wmEased;
+    wordmark.style.fontVariationSettings = `"wght" ${w.toFixed(0)}`;
+    wordmark.style.fontWeight = Math.round(w);
+
+    // Brand line expand 0→280px (cubicOut)
+    const lineP = clampLerp(t, T.brandLineIn[0], T.brandLineIn[1]);
+    const cubicOut = x => 1 - Math.pow(1 - x, 3);
+    brandLine.style.width = (280 * cubicOut(lineP)) + 'px';
+  }
+
+  // ---------- Driver ----------
+  let manualT = null;
+  let startMs = null;
+  let hasFinished = false;
+  function tick(now) {
+    if (manualT != null) render(manualT);
+    else {
+      if (startMs == null) startMs = now;
+      const elapsed = (now - startMs) / 1000;
+      const recording = window.__recording === true;
+      let t;
+      if (recording) {
+        t = Math.min(elapsed, T.DURATION - 0.001);
+        if (elapsed >= T.DURATION) hasFinished = true;
+      } else {
+        t = elapsed % T.DURATION;
+      }
+      render(t);
+    }
+    requestAnimationFrame(tick);
+  }
+  // Force first-frame render synchronously, THEN set ready
+  render(0);
+  requestAnimationFrame(tick);
+
+  window.__setTime = function(t) { manualT = t; render(t); };
+  window.__resume = function() { manualT = null; startMs = null; };
+  window.__duration = T.DURATION;
+  window.__render = render;
+  window.__ready = true;
+})();
 </script>
 </body>
 </html>

+ 1134 - 0
demos/c3-motion-design-en.html

@@ -0,0 +1,1134 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8" />
+<title>huashu-design · c3 motion design (EN)</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600;700&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
+<style>
+:root {
+  --bg: #000000;
+  --ink: #FFFFFF;
+  --ink-80: rgba(255,255,255,0.82);
+  --ink-60: rgba(255,255,255,0.58);
+  --muted: rgba(255,255,255,0.40);
+  --dim: rgba(255,255,255,0.18);
+  --hairline: rgba(255,255,255,0.12);
+  --hair-strong: rgba(255,255,255,0.22);
+  --accent: #D97757;
+  --accent-deep: #B85D3D;
+  --accent-dim: rgba(217,119,87,0.25);
+  --serif-cn: "Noto Serif SC", "Songti SC", "STSong", serif;
+  --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
+  --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
+  --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+}
+
+html, body {
+  margin: 0; padding: 0;
+  background: #000;
+  overflow: hidden;
+  font-family: var(--sans);
+  color: var(--ink);
+  -webkit-font-smoothing: antialiased;
+}
+* { box-sizing: border-box; }
+
+.stage {
+  position: fixed;
+  top: 50%; left: 50%;
+  width: 1920px; height: 1080px;
+  transform-origin: center center;
+  background: var(--bg);
+  overflow: hidden;
+}
+
+/* Subtle film grain overlay, 2% */
+.stage::after {
+  content: '';
+  position: absolute; inset: 0;
+  pointer-events: none;
+  opacity: 0.025;
+  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
+  mix-blend-mode: overlay;
+  z-index: 200;
+}
+
+/* Watermark */
+.watermark-tl {
+  position: absolute;
+  top: 40px; left: 56px;
+  font-family: var(--mono);
+  font-size: 14px;
+  letter-spacing: 0.2em;
+  color: rgba(255,255,255,0.16);
+  z-index: 50;
+  text-transform: none;
+  font-weight: 500;
+}
+.watermark-br {
+  position: absolute;
+  bottom: 32px; right: 48px;
+  font-family: var(--mono);
+  font-size: 10px;
+  letter-spacing: 0.24em;
+  color: rgba(255,255,255,0.22);
+  z-index: 100;
+  text-transform: uppercase;
+  opacity: 0;
+  transition: opacity 0.6s;
+}
+.watermark-br.visible { opacity: 1; }
+
+/* Scene container */
+.scene {
+  position: absolute; inset: 0;
+  opacity: 0;
+  visibility: hidden;
+  will-change: opacity;
+}
+.scene.visible { visibility: visible; }
+
+/* ============ Split layout ============ */
+.split {
+  position: absolute; inset: 0;
+}
+.split-top {
+  position: absolute;
+  top: 0; left: 0;
+  width: 100%; height: 48%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.split-bottom {
+  position: absolute;
+  bottom: 0; left: 0;
+  width: 100%; height: 52%;
+}
+
+/* Horizontal divider hairline */
+.split-divider {
+  position: absolute;
+  left: 160px; right: 160px;
+  top: 48%;
+  height: 1px;
+  background: var(--hairline);
+  z-index: 5;
+}
+
+/* Section label (top-left of each half) */
+.panel-label {
+  position: absolute;
+  top: 32px;
+  left: 160px;
+  font-family: var(--mono);
+  font-size: 12px;
+  letter-spacing: 0.3em;
+  color: var(--muted);
+  text-transform: uppercase;
+}
+.split-bottom .panel-label { top: 32px; }
+.panel-label .accent { color: var(--accent); font-weight: 500; }
+
+/* ============ Top: Timeline ============ */
+.timeline-wrap {
+  width: 1600px;
+  position: relative;
+  margin-top: 40px;
+}
+.timeline-track {
+  position: relative;
+  height: 2px;
+  background: var(--hairline);
+  width: 100%;
+}
+.timeline-track .fill {
+  position: absolute;
+  top: 0; left: 0;
+  height: 100%;
+  background: linear-gradient(90deg, var(--accent) 0%, rgba(217,119,87,0.4) 100%);
+  width: 0%;
+  will-change: width;
+}
+
+/* Tick marks */
+.tick {
+  position: absolute;
+  width: 1px;
+  height: 10px;
+  background: var(--muted);
+  top: -4px;
+  transform: translateX(-0.5px);
+}
+.tick.major { height: 14px; top: -6px; background: var(--ink-60); }
+.tick-label {
+  position: absolute;
+  top: 18px;
+  font-family: var(--mono);
+  font-size: 11px;
+  color: var(--muted);
+  letter-spacing: 0.1em;
+  transform: translateX(-50%);
+}
+
+/* Playhead */
+.playhead {
+  position: absolute;
+  top: -28px;
+  left: 0;
+  width: 2px;
+  height: 58px;
+  background: var(--accent);
+  transform: translateX(-1px);
+  will-change: transform;
+  z-index: 10;
+  box-shadow: 0 0 20px rgba(217,119,87,0.5);
+}
+.playhead::before {
+  content: '';
+  position: absolute;
+  top: -8px;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 14px; height: 14px;
+  background: var(--accent);
+  border-radius: 50%;
+  box-shadow: 0 0 16px rgba(217,119,87,0.6);
+}
+.playhead::after {
+  content: '';
+  position: absolute;
+  top: -6px;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 6px; height: 6px;
+  background: var(--bg);
+  border-radius: 50%;
+  z-index: 2;
+}
+
+/* API capsules on timeline */
+.api-capsule {
+  position: absolute;
+  top: -92px;
+  transform: translateX(-50%);
+  padding: 10px 20px;
+  border: 1px solid var(--hairline);
+  border-radius: 999px;
+  background: rgba(0,0,0,0.6);
+  backdrop-filter: blur(8px);
+  font-family: var(--mono);
+  font-size: 18px;
+  font-weight: 500;
+  color: var(--ink-60);
+  letter-spacing: 0.02em;
+  transition: none;
+  will-change: color, border-color, transform, box-shadow;
+  white-space: nowrap;
+}
+.api-capsule.lit {
+  color: var(--accent);
+  border-color: var(--accent);
+  box-shadow: 0 0 30px rgba(217,119,87,0.35);
+}
+.api-capsule .tiny {
+  font-size: 10px;
+  color: var(--muted);
+  letter-spacing: 0.2em;
+  margin-right: 10px;
+  display: inline-block;
+  vertical-align: middle;
+  opacity: 0.7;
+}
+.api-capsule.lit .tiny { color: var(--accent); opacity: 0.9; }
+
+/* Tick connector (short vertical line from capsule to timeline) */
+.capsule-stem {
+  position: absolute;
+  top: -48px;
+  width: 1px;
+  height: 44px;
+  background: var(--hairline);
+  transform: translateX(-0.5px);
+  z-index: 1;
+}
+.capsule-stem.lit { background: var(--accent); }
+
+/* ============ Bottom: Driven stage ============ */
+.driven-stage {
+  position: absolute;
+  top: 0; left: 0;
+  width: 100%; height: 100%;
+}
+
+.viz {
+  position: absolute;
+  top: 46%; left: 50%;
+  transform: translate(-50%, -50%);
+  width: 1000px; height: 400px;
+  opacity: 0;
+  will-change: opacity;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* viz 1: useTime — clock */
+.viz-clock {
+  position: relative;
+  width: 280px; height: 280px;
+  border: 1.5px solid var(--hair-strong);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.viz-clock .tickmark {
+  position: absolute;
+  width: 1px;
+  height: 8px;
+  background: var(--muted);
+  top: 10px;
+  left: 50%;
+  transform-origin: 50% 130px;
+}
+.viz-clock .tickmark.q {
+  width: 2px;
+  height: 14px;
+  background: var(--ink-60);
+}
+.viz-clock .hand-h {
+  position: absolute;
+  width: 3px; height: 80px;
+  background: var(--ink);
+  left: 50%;
+  bottom: 50%;
+  transform-origin: 50% 100%;
+  transform: translateX(-50%) rotate(30deg);
+  border-radius: 2px;
+  will-change: transform;
+}
+.viz-clock .hand-m {
+  position: absolute;
+  width: 2px; height: 110px;
+  background: var(--ink-80);
+  left: 50%;
+  bottom: 50%;
+  transform-origin: 50% 100%;
+  transform: translateX(-50%) rotate(120deg);
+  border-radius: 2px;
+  will-change: transform;
+}
+.viz-clock .hand-s {
+  position: absolute;
+  width: 1.5px; height: 120px;
+  background: var(--accent);
+  left: 50%;
+  bottom: 50%;
+  transform-origin: 50% 100%;
+  transform: translateX(-50%) rotate(0deg);
+  border-radius: 2px;
+  will-change: transform;
+  box-shadow: 0 0 10px rgba(217,119,87,0.4);
+}
+.viz-clock .center-dot {
+  width: 12px; height: 12px;
+  border-radius: 50%;
+  background: var(--accent);
+  z-index: 5;
+  box-shadow: 0 0 10px rgba(217,119,87,0.6);
+}
+.viz-clock-label {
+  position: absolute;
+  bottom: -48px;
+  left: 50%;
+  transform: translateX(-50%);
+  font-family: var(--mono);
+  font-size: 13px;
+  color: var(--muted);
+  letter-spacing: 0.12em;
+  white-space: nowrap;
+}
+.viz-clock-label .val {
+  color: var(--accent);
+  font-variant-numeric: tabular-nums;
+}
+
+/* viz 2: interpolate — morph box */
+.viz-morph {
+  display: flex;
+  gap: 80px;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+}
+.morph-box {
+  width: 260px; height: 260px;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.morph-rect {
+  background: var(--accent);
+  border-radius: 4px;
+  will-change: width, height, background, border-radius, transform;
+  box-shadow: 0 0 40px rgba(217,119,87,0.25);
+}
+.morph-label {
+  position: absolute;
+  bottom: -48px;
+  left: 50%;
+  transform: translateX(-50%);
+  font-family: var(--mono);
+  font-size: 12px;
+  color: var(--muted);
+  letter-spacing: 0.12em;
+  white-space: nowrap;
+}
+.morph-label .val { color: var(--accent); font-variant-numeric: tabular-nums; }
+.morph-arrow {
+  font-family: var(--mono);
+  font-size: 28px;
+  color: var(--muted);
+  letter-spacing: 0.2em;
+}
+
+/* viz 3: Easing — curves */
+.viz-curves {
+  position: relative;
+  width: 720px; height: 320px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.curves-svg {
+  width: 100%; height: 100%;
+}
+.curve-label {
+  position: absolute;
+  font-family: var(--mono);
+  font-size: 12px;
+  color: var(--muted);
+  letter-spacing: 0.08em;
+  white-space: nowrap;
+}
+/* viewBox 720x320 → right edge ≈ 680 of 720 → 94%. Vertical:
+   y=40 is visual top (output value 1), y=260 is bottom (value 0).
+   Labels go at right side, vertically aligned with where each curve
+   approaches its asymptote at t≈0.7.
+   expoOut at t=0.7 ~ 0.99 (≈ y=42)
+   cubicOut at t=0.7 ~ 0.973 (≈ y=46)
+   linear at t=0.7 ~ 0.7 (≈ y=106)
+   So spatial order top→bottom: expoOut, cubicOut, linear
+*/
+.curve-label.l-expo   { top:  6%; right: 4%; color: var(--accent); }
+.curve-label.l-cubic  { top: 16%; right: 4%; color: rgba(255,255,255,0.78); }
+.curve-label.l-linear { top: 36%; right: 4%; color: rgba(255,255,255,0.42); }
+
+.curve-dot {
+  position: absolute;
+  width: 10px; height: 10px;
+  border-radius: 50%;
+  background: var(--accent);
+  transform: translate(-50%, -50%);
+  box-shadow: 0 0 14px rgba(217,119,87,0.6);
+  will-change: left, top;
+}
+
+/* viz 4: useSprite — choreographed grid */
+.viz-sprites {
+  display: grid;
+  grid-template-columns: repeat(6, 60px);
+  grid-template-rows: repeat(4, 60px);
+  gap: 18px;
+  justify-content: center;
+  align-content: center;
+  padding: 40px 0;
+}
+.sprite {
+  width: 60px; height: 60px;
+  background: var(--hairline);
+  border: 1px solid var(--dim);
+  will-change: transform, opacity, background;
+  opacity: 0;
+  border-radius: 2px;
+}
+
+.sprite-label {
+  position: absolute;
+  bottom: -6px;
+  left: 50%;
+  transform: translateX(-50%);
+  font-family: var(--mono);
+  font-size: 12px;
+  color: var(--muted);
+  letter-spacing: 0.12em;
+  white-space: nowrap;
+}
+.sprite-label .val { color: var(--accent); font-variant-numeric: tabular-nums; }
+
+/* ============ Scene 0: Opening title ============ */
+.scene-intro {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+.scene-intro .title {
+  font-family: var(--serif-en);
+  font-size: 128px;
+  font-weight: 300;
+  letter-spacing: -0.025em;
+  color: var(--ink);
+  line-height: 1.02;
+  will-change: opacity, transform, font-weight;
+}
+.scene-intro .title .accent { color: var(--accent); }
+.scene-intro .sub {
+  margin-top: 28px;
+  font-family: var(--mono);
+  font-size: 16px;
+  color: var(--muted);
+  letter-spacing: 0.3em;
+}
+
+/* ============ Scene 2: Brand reveal (米色面板标准动作) ============ */
+.scene-brand {
+  background: transparent;
+  pointer-events: none;
+  z-index: 150;
+}
+.brand-panel {
+  position: absolute;
+  inset: 0;
+  background: #F5F4F0;
+  transform: translateY(100%);
+  will-change: transform;
+}
+.brand-wordmark {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, calc(-50% + 20px));
+  font-family: "Source Serif 4", Georgia, serif;
+  font-size: 72px;
+  font-weight: 100;
+  font-variation-settings: "wght" 100;
+  letter-spacing: -0.01em;
+  color: #1A1918;
+  text-align: center;
+  line-height: 1;
+  opacity: 0;
+  white-space: nowrap;
+  will-change: opacity, transform, font-weight, font-variation-settings;
+}
+.brand-wordmark .accent { color: #D97757; font-weight: inherit; }
+.brand-line {
+  position: absolute;
+  top: calc(50% + 60px);
+  left: 50%;
+  transform: translateX(-50%);
+  height: 2px;
+  width: 0px;
+  background: #D97757;
+  will-change: width;
+}
+
+/* ============ Replay button (hidden during record) ============ */
+.replay-btn {
+  position: absolute;
+  bottom: 40px;
+  left: 50%;
+  transform: translateX(-50%);
+  padding: 12px 32px;
+  border: 1px solid var(--hair-strong);
+  border-radius: 999px;
+  background: transparent;
+  color: var(--ink-60);
+  font-family: var(--mono);
+  font-size: 13px;
+  letter-spacing: 0.2em;
+  cursor: pointer;
+  opacity: 0;
+  pointer-events: none;
+  transition: opacity 0.4s;
+  z-index: 300;
+}
+.replay-btn.visible {
+  opacity: 1;
+  pointer-events: auto;
+}
+</style>
+</head>
+<body>
+
+<div class="stage" id="stage">
+
+  <!-- Top-left watermark (always on) -->
+  <div class="watermark-tl">HUASHU · DESIGN</div>
+
+  <!-- ============ Scene 0: Intro (0 → 1.6s) ============ -->
+  <div class="scene scene-intro" id="scene-intro">
+    <div class="title" id="introTitle">Timeline <span class="accent">=</span> Code</div>
+    <div class="sub" id="introSub">MOTION · ENGINE · ANIMATED</div>
+  </div>
+
+  <!-- ============ Scene 1: Split view (1.6 → 8.2s) ============ -->
+  <div class="scene" id="scene-main">
+    <div class="split">
+
+      <!-- TOP: Timeline -->
+      <div class="split-top">
+        <div class="panel-label">TIMELINE · <span class="accent">PLAYHEAD</span></div>
+        <div class="timeline-wrap">
+          <div class="timeline-track">
+            <div class="fill" id="timelineFill"></div>
+
+            <!-- Tick marks (10 ticks for 10s) -->
+            <div class="tick" style="left: 0%;"></div>
+            <div class="tick major" style="left: 0%;"></div>
+            <div class="tick" style="left: 10%;"></div>
+            <div class="tick major" style="left: 20%;"></div>
+            <div class="tick" style="left: 30%;"></div>
+            <div class="tick major" style="left: 40%;"></div>
+            <div class="tick" style="left: 50%;"></div>
+            <div class="tick major" style="left: 60%;"></div>
+            <div class="tick" style="left: 70%;"></div>
+            <div class="tick major" style="left: 80%;"></div>
+            <div class="tick" style="left: 90%;"></div>
+            <div class="tick major" style="left: 100%;"></div>
+
+            <div class="tick-label" style="left: 0%;">0s</div>
+            <div class="tick-label" style="left: 20%;">2s</div>
+            <div class="tick-label" style="left: 40%;">4s</div>
+            <div class="tick-label" style="left: 60%;">6s</div>
+            <div class="tick-label" style="left: 80%;">8s</div>
+            <div class="tick-label" style="left: 100%;">10s</div>
+
+            <!-- API capsules anchored at their trigger points -->
+            <!-- Scene-main spans 1.6→8.2; timeline maps 0→10s globally for clarity.
+                 cap positions here mirror when each API is "active" on the lower viz. -->
+            <!-- useTime: global t = 1.8 → 3.3 → center ~2.5s → 25% -->
+            <div class="capsule-stem" id="stem-time" style="left: 18%;"></div>
+            <div class="api-capsule" id="cap-time" style="left: 18%;">
+              <span class="tiny">01</span>useTime
+            </div>
+
+            <!-- interpolate: 3.5 → 5s → center 4.2s → 42% -->
+            <div class="capsule-stem" id="stem-interp" style="left: 38%;"></div>
+            <div class="api-capsule" id="cap-interp" style="left: 38%;">
+              <span class="tiny">02</span>interpolate
+            </div>
+
+            <!-- Easing: 5 → 6.5s → center 5.7s → 57% -->
+            <div class="capsule-stem" id="stem-easing" style="left: 58%;"></div>
+            <div class="api-capsule" id="cap-easing" style="left: 58%;">
+              <span class="tiny">03</span>Easing
+            </div>
+
+            <!-- useSprite: 6.5 → 8s → center 7.2s → 72% -->
+            <div class="capsule-stem" id="stem-sprite" style="left: 80%;"></div>
+            <div class="api-capsule" id="cap-sprite" style="left: 80%;">
+              <span class="tiny">04</span>useSprite
+            </div>
+
+            <!-- Playhead -->
+            <div class="playhead" id="playhead"></div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Divider -->
+      <div class="split-divider"></div>
+
+      <!-- BOTTOM: Driven stage -->
+      <div class="split-bottom">
+        <div class="panel-label">DRIVEN · <span class="accent">STAGE</span></div>
+        <div class="driven-stage">
+
+          <!-- viz 1: useTime — clock -->
+          <div class="viz" id="viz-time">
+            <div class="viz-clock" id="clockRoot">
+              <!-- 12 tick marks -->
+              <div class="tickmark q" style="transform: translate(-50%, 0) rotate(0deg);"></div>
+              <div class="tickmark" style="transform: translate(-50%, 0) rotate(30deg);"></div>
+              <div class="tickmark" style="transform: translate(-50%, 0) rotate(60deg);"></div>
+              <div class="tickmark q" style="transform: translate(-50%, 0) rotate(90deg);"></div>
+              <div class="tickmark" style="transform: translate(-50%, 0) rotate(120deg);"></div>
+              <div class="tickmark" style="transform: translate(-50%, 0) rotate(150deg);"></div>
+              <div class="tickmark q" style="transform: translate(-50%, 0) rotate(180deg);"></div>
+              <div class="tickmark" style="transform: translate(-50%, 0) rotate(210deg);"></div>
+              <div class="tickmark" style="transform: translate(-50%, 0) rotate(240deg);"></div>
+              <div class="tickmark q" style="transform: translate(-50%, 0) rotate(270deg);"></div>
+              <div class="tickmark" style="transform: translate(-50%, 0) rotate(300deg);"></div>
+              <div class="tickmark" style="transform: translate(-50%, 0) rotate(330deg);"></div>
+
+              <div class="hand-h" id="handH"></div>
+              <div class="hand-m" id="handM"></div>
+              <div class="hand-s" id="handS"></div>
+              <div class="center-dot"></div>
+
+              <div class="viz-clock-label">
+                t = <span class="val" id="timeVal">0.00s</span>
+              </div>
+            </div>
+          </div>
+
+          <!-- viz 2: interpolate — morph -->
+          <div class="viz" id="viz-interp">
+            <div class="viz-morph">
+              <div class="morph-box">
+                <div class="morph-rect" id="morphFrom" style="width: 80px; height: 80px; background: var(--hair-strong); border-radius: 2px;"></div>
+                <div class="morph-label">FROM · <span class="val">0 → 100</span></div>
+              </div>
+              <div class="morph-arrow">──────→</div>
+              <div class="morph-box">
+                <div class="morph-rect" id="morphTo"></div>
+                <div class="morph-label">INTERPOLATE · <span class="val" id="interpVal">0.00</span></div>
+              </div>
+            </div>
+          </div>
+
+          <!-- viz 3: Easing — 3 curves drawn in parallel -->
+          <div class="viz" id="viz-easing">
+            <div class="viz-curves">
+              <svg class="curves-svg" viewBox="0 0 720 320" preserveAspectRatio="none">
+                <!-- Grid -->
+                <line x1="60" y1="260" x2="680" y2="260" stroke="rgba(255,255,255,0.18)" stroke-width="1"/>
+                <line x1="60" y1="260" x2="60" y2="40" stroke="rgba(255,255,255,0.18)" stroke-width="1"/>
+
+                <!-- Axis labels -->
+                <text x="50" y="266" text-anchor="end" fill="rgba(255,255,255,0.4)" font-family="JetBrains Mono, monospace" font-size="11">0</text>
+                <text x="50" y="48" text-anchor="end" fill="rgba(255,255,255,0.4)" font-family="JetBrains Mono, monospace" font-size="11">1</text>
+                <text x="680" y="282" text-anchor="end" fill="rgba(255,255,255,0.4)" font-family="JetBrains Mono, monospace" font-size="11">t</text>
+
+                <!-- Curves -->
+                <path id="pathLinear" d="M 60 260 L 60 260" stroke="rgba(255,255,255,0.42)" stroke-width="1.5" fill="none" stroke-linecap="round"/>
+                <path id="pathCubic" d="M 60 260 L 60 260" stroke="rgba(255,255,255,0.75)" stroke-width="1.8" fill="none" stroke-linecap="round"/>
+                <path id="pathExpo" d="M 60 260 L 60 260" stroke="#D97757" stroke-width="2.2" fill="none" stroke-linecap="round"/>
+              </svg>
+              <div class="curve-label l-linear">linear</div>
+              <div class="curve-label l-cubic">cubicOut</div>
+              <div class="curve-label l-expo">expoOut</div>
+            </div>
+          </div>
+
+          <!-- viz 4: useSprite — 24 sprites -->
+          <div class="viz" id="viz-sprite">
+            <div class="viz-sprites" id="spriteGrid">
+              <!-- 24 sprites (6x4), filled by JS -->
+            </div>
+          </div>
+        </div>
+      </div>
+
+    </div>
+  </div>
+
+  <!-- ============ Scene 2: Brand reveal (米色面板, 8.0 → 10s) ============ -->
+  <div class="scene scene-brand" id="scene-brand">
+    <div class="brand-panel" id="brandPanel"></div>
+    <div class="brand-wordmark" id="wordmark">huashu<span class="accent">-</span>design</div>
+    <div class="brand-line" id="brandLine"></div>
+  </div>
+
+  <!-- Bottom-right watermark -->
+  <div class="watermark-br" id="watermarkBR">V2 · 2026</div>
+
+  <!-- Replay button (hidden during recording) -->
+  <button class="replay-btn no-record" id="replayBtn">REPLAY</button>
+
+</div>
+
+<script>
+(function() {
+  // =============== Timing ===============
+  const T = {
+    DURATION: 10.0,
+
+    // Scene 0: intro
+    intro_in:  [0.0, 0.5],
+    intro_out: [1.3, 1.6],
+
+    // Scene 1: main (timeline + driven stage)
+    main_in:   [1.5, 1.9],     // fade in
+    // Playhead sweeps from 0% (at t=1.6) to 100% (at t=8.2).
+    // API activations use GLOBAL time. Their capsule position is placed so
+    // that playhead passes under the capsule right when the API peaks.
+    main_t0:   1.6,
+    main_t_end: 8.2,
+    main_out:  [8.0, 8.4],
+
+    // API activations (GLOBAL time)
+    // Each API: [activate_start, peak, deactivate_end]
+    // Capsule x% = (peak - 1.6) / (8.2 - 1.6) * 100
+    useTime:     [2.0, 2.8, 3.6],   // capsule @ ~18%
+    interpolate: [3.6, 4.1, 4.8],   // capsule @ ~38%
+    Easing:      [4.8, 5.4, 6.2],   // capsule @ ~58%
+    useSprite:   [6.2, 6.9, 7.9],   // capsule @ ~80%
+
+    // Scene 2: Brand reveal (米色面板 standard, last 2s of T=10)
+    // [T-2.0 → T-1.7]: main fade 1→0 (already handled by main_out 8.0-8.4)
+    // [T-1.7 → T-1.3]: beige panel translateY 100%→0, expoOut
+    // [T-1.3 → T-0.7]: wordmark weight 100→500 + y:20→0 + opacity 0→1, expoOut
+    // [T-0.7 → T-0.3]: orange line width 0→280px, cubicOut
+    // [T-0.3 → T]: hold
+    brand_panel:  [8.3, 8.7],
+    brand_word:   [8.7, 9.3],
+    brand_line:   [9.3, 9.7],
+  };
+
+  // =============== Easings ===============
+  const expoOut = t => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t));
+  const expoIn  = t => (t <= 0 ? 0 : Math.pow(2, 10 * (t - 1)));
+  const cubicOut = t => 1 - Math.pow(1 - t, 3);
+  const cubicIn  = t => t * t * t;
+  const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
+  const easeInOut  = cubicInOut;
+  const linear = t => t;
+
+  // =============== Utils ===============
+  const clamp = (v, lo = 0, hi = 1) => Math.max(lo, Math.min(hi, v));
+  const clampLerp = (t, t0, t1) => clamp((t - t0) / (t1 - t0));
+  function lerp(t, t0, t1, v0, v1, easing = linear) {
+    const p = clampLerp(t, t0, t1);
+    return v0 + (v1 - v0) * easing(p);
+  }
+
+  // =============== DOM refs ===============
+  const scenes = {
+    intro: document.getElementById('scene-intro'),
+    main: document.getElementById('scene-main'),
+    brand: document.getElementById('scene-brand'),
+  };
+  const introTitle = document.getElementById('introTitle');
+  const introSub = document.getElementById('introSub');
+
+  const timelineFill = document.getElementById('timelineFill');
+  const playhead = document.getElementById('playhead');
+
+  const capTime = document.getElementById('cap-time');
+  const capInterp = document.getElementById('cap-interp');
+  const capEasing = document.getElementById('cap-easing');
+  const capSprite = document.getElementById('cap-sprite');
+
+  const stemTime = document.getElementById('stem-time');
+  const stemInterp = document.getElementById('stem-interp');
+  const stemEasing = document.getElementById('stem-easing');
+  const stemSprite = document.getElementById('stem-sprite');
+
+  const vizTime = document.getElementById('viz-time');
+  const vizInterp = document.getElementById('viz-interp');
+  const vizEasing = document.getElementById('viz-easing');
+  const vizSprite = document.getElementById('viz-sprite');
+
+  const handS = document.getElementById('handS');
+  const handM = document.getElementById('handM');
+  const handH = document.getElementById('handH');
+  const timeVal = document.getElementById('timeVal');
+
+  const morphTo = document.getElementById('morphTo');
+  const interpVal = document.getElementById('interpVal');
+
+  const pathLinear = document.getElementById('pathLinear');
+  const pathCubic = document.getElementById('pathCubic');
+  const pathExpo = document.getElementById('pathExpo');
+
+  const spriteGrid = document.getElementById('spriteGrid');
+  const wordmark = document.getElementById('wordmark');
+  const brandLine = document.getElementById('brandLine');
+  const brandPanel = document.getElementById('brandPanel');
+  const watermarkBR = document.getElementById('watermarkBR');
+  const replayBtn = document.getElementById('replayBtn');
+
+  // Build 24 sprites (6x4 grid)
+  const SPRITE_COLS = 6, SPRITE_ROWS = 4;
+  const spriteEls = [];
+  for (let r = 0; r < SPRITE_ROWS; r++) {
+    for (let c = 0; c < SPRITE_COLS; c++) {
+      const el = document.createElement('div');
+      el.className = 'sprite';
+      // center distance for ripple
+      const dc = c - (SPRITE_COLS - 1) / 2;
+      const dr = r - (SPRITE_ROWS - 1) / 2;
+      const dist = Math.sqrt(dc * dc + dr * dr);
+      const maxDist = Math.sqrt(((SPRITE_COLS - 1) / 2) ** 2 + ((SPRITE_ROWS - 1) / 2) ** 2);
+      el.dataset.delay = (dist / maxDist).toFixed(3);
+      spriteGrid.appendChild(el);
+      spriteEls.push(el);
+    }
+  }
+
+  // =============== Scene helpers ===============
+  function showScene(el, opacity) {
+    if (opacity > 0.001) el.classList.add('visible');
+    else el.classList.remove('visible');
+    el.style.opacity = opacity;
+  }
+
+  // =============== API activation logic ===============
+  function apiState(t_local, api) {
+    // Returns { on: bool, strength: 0-1 }
+    const [a, peak, b] = T[api];
+    if (t_local < a || t_local > b) return { on: false, strength: 0 };
+    if (t_local < peak) {
+      return { on: true, strength: expoOut(clampLerp(t_local, a, peak)) };
+    } else {
+      return { on: true, strength: 1 - cubicIn(clampLerp(t_local, peak, b)) };
+    }
+  }
+
+  // =============== Draw easing curves progressively ===============
+  function easingPath(easingFn, progress) {
+    // progress 0-1 draws the curve from left to right
+    // x range: 60 → 680, y range: 260 (0) → 40 (1)
+    const X0 = 60, X1 = 680, Y0 = 260, Y1 = 40;
+    const steps = Math.max(2, Math.floor(progress * 80));
+    let d = `M ${X0} ${Y0}`;
+    for (let i = 1; i <= steps; i++) {
+      const t = (i / 80) * progress;
+      const x = X0 + (X1 - X0) * t;
+      const y = Y0 + (Y1 - Y0) * easingFn(t);
+      d += ` L ${x.toFixed(2)} ${y.toFixed(2)}`;
+    }
+    return d;
+  }
+
+  // =============== Render ===============
+  function render(t) {
+    // ============ Scene 0: Intro ============
+    if (t < T.main_in[1]) {
+      let op = 0;
+      if (t < T.intro_in[1]) op = clampLerp(t, T.intro_in[0], T.intro_in[1]);
+      else if (t < T.intro_out[0]) op = 1;
+      else op = 1 - clampLerp(t, T.intro_out[0], T.intro_out[1]);
+      showScene(scenes.intro, op);
+
+      // weight morph + rise
+      const morphP = expoOut(clampLerp(t, T.intro_in[0], T.intro_in[1] + 0.3));
+      const w = 150 + (400 - 150) * morphP;
+      introTitle.style.fontWeight = Math.round(w);
+      const rise = lerp(t, T.intro_in[0], T.intro_in[1], 16, 0, expoOut);
+      introTitle.style.transform = `translate3d(0, ${rise}px, 0)`;
+      introSub.style.opacity = clampLerp(t, T.intro_in[1], T.intro_in[1] + 0.4);
+    } else {
+      showScene(scenes.intro, 0);
+    }
+
+    // ============ Scene 1: Main (split view) ============
+    if (t >= T.main_in[0] - 0.1 && t < T.main_out[1]) {
+      let op;
+      if (t < T.main_in[1]) op = clampLerp(t, T.main_in[0], T.main_in[1]);
+      else if (t < T.main_out[0]) op = 1;
+      else op = 1 - clampLerp(t, T.main_out[0], T.main_out[1]);
+      showScene(scenes.main, op);
+
+      // Playhead sweeps 0% → 100% across the window [main_t0, main_t_end]
+      const phP = clampLerp(t, T.main_t0, T.main_t_end);
+      const phPct = phP * 100;
+      playhead.style.left = phPct + '%';
+      // Keep: use t directly for API state
+      const t_local_clamped = t;
+
+      // Timeline fill
+      timelineFill.style.width = phPct + '%';
+
+      // API capsules: lit state driven by apiState
+      const stTime = apiState(t_local_clamped, 'useTime');
+      const stInterp = apiState(t_local_clamped, 'interpolate');
+      const stEasing = apiState(t_local_clamped, 'Easing');
+      const stSprite = apiState(t_local_clamped, 'useSprite');
+
+      setLit(capTime, stemTime, stTime);
+      setLit(capInterp, stemInterp, stInterp);
+      setLit(capEasing, stemEasing, stEasing);
+      setLit(capSprite, stemSprite, stSprite);
+
+      // Viz opacities — each viz only visible during its API's window
+      vizTime.style.opacity = stTime.on ? stTime.strength : 0;
+      vizInterp.style.opacity = stInterp.on ? stInterp.strength : 0;
+      vizEasing.style.opacity = stEasing.on ? stEasing.strength : 0;
+      vizSprite.style.opacity = stSprite.on ? stSprite.strength : 0;
+
+      // ========= viz 1: clock =========
+      // Continuous rotation (not just when active) so transition looks natural
+      // But only animate hands when api is near-active, to avoid wasted cpu
+      {
+        const [a, _peak, b] = T.useTime;
+        // Second hand: one revolution over the active window
+        const localP = clampLerp(t_local_clamped, a, b);
+        // Multi-revolution: 1.5 turns over the window
+        const sDeg = localP * 540;
+        const mDeg = localP * 180 + 120;
+        const hDeg = localP * 60 + 30;
+        handS.style.transform = `translateX(-50%) rotate(${sDeg}deg)`;
+        handM.style.transform = `translateX(-50%) rotate(${mDeg}deg)`;
+        handH.style.transform = `translateX(-50%) rotate(${hDeg}deg)`;
+
+        // Display value as t in seconds mapping 0→1.50
+        const displayVal = (localP * 1.5).toFixed(2);
+        timeVal.textContent = displayVal + 's';
+      }
+
+      // ========= viz 2: interpolate =========
+      {
+        const [a, _peak, b] = T.interpolate;
+        const localP = clampLerp(t_local_clamped, a, b);
+        const eased = easeInOut(localP);
+        // morph from 80×80 black → 220×160 orange, rounded
+        const W = 80 + (240 - 80) * eased;
+        const H = 80 + (160 - 80) * eased;
+        const bright = Math.round(30 + (217 - 30) * eased);
+        const brightG = Math.round(30 + (119 - 30) * eased);
+        const brightB = Math.round(30 + (87 - 30) * eased);
+        const rad = 2 + (20 - 2) * eased;
+        morphTo.style.width = W + 'px';
+        morphTo.style.height = H + 'px';
+        morphTo.style.background = `rgb(${bright}, ${brightG}, ${brightB})`;
+        morphTo.style.borderRadius = rad + 'px';
+        interpVal.textContent = eased.toFixed(2);
+      }
+
+      // ========= viz 3: easing curves =========
+      {
+        const [a, _peak, b] = T.Easing;
+        const localP = clampLerp(t_local_clamped, a, b);
+        pathLinear.setAttribute('d', easingPath(linear, localP));
+        pathCubic.setAttribute('d', easingPath(cubicOut, localP));
+        pathExpo.setAttribute('d', easingPath(expoOut, localP));
+      }
+
+      // ========= viz 4: sprites =========
+      {
+        const [a, _peak, b] = T.useSprite;
+        const localP = clampLerp(t_local_clamped, a, b);
+        for (const el of spriteEls) {
+          const delay = parseFloat(el.dataset.delay);
+          const spriteLocalT = clamp((localP - delay * 0.5) / 0.5, 0, 1);
+          const op = expoOut(spriteLocalT);
+          el.style.opacity = op;
+          const scale = 0.5 + 0.5 * op;
+          const y = (1 - op) * 14;
+          el.style.transform = `translateY(${y}px) scale(${scale})`;
+          el.style.background = op > 0.85 ? 'var(--accent)' : 'var(--hairline)';
+        }
+      }
+    } else {
+      showScene(scenes.main, 0);
+    }
+
+    // ============ Scene 2: Brand reveal (米色面板标准动作) ============
+    if (t >= T.brand_panel[0] - 0.1) {
+      showScene(scenes.brand, 1);
+
+      // [T-1.7 → T-1.3]: beige panel slides up, expoOut
+      const panelP = expoOut(clampLerp(t, T.brand_panel[0], T.brand_panel[1]));
+      brandPanel.style.transform = `translateY(${(1 - panelP) * 100}%)`;
+
+      // [T-1.3 → T-0.7]: wordmark weight 100→500 + y:20→0 + opacity:0→1, expoOut
+      const wordP = expoOut(clampLerp(t, T.brand_word[0], T.brand_word[1]));
+      const w = 100 + (500 - 100) * wordP;
+      wordmark.style.fontVariationSettings = `"wght" ${w.toFixed(0)}`;
+      wordmark.style.fontWeight = Math.round(w);
+      wordmark.style.opacity = wordP;
+      const wRise = (1 - wordP) * 20;
+      wordmark.style.transform = `translate(-50%, calc(-50% + ${wRise}px))`;
+
+      // [T-0.7 → T-0.3]: orange line expands 0→280px, cubicOut
+      const lineP = cubicOut(clampLerp(t, T.brand_line[0], T.brand_line[1]));
+      brandLine.style.width = (lineP * 280) + 'px';
+    } else {
+      showScene(scenes.brand, 0);
+      brandPanel.style.transform = 'translateY(100%)';
+      wordmark.style.opacity = 0;
+      brandLine.style.width = '0px';
+    }
+
+    // Watermark visible from start of main until end
+    if (t >= T.main_in[0] && t < T.DURATION - 0.15) {
+      watermarkBR.classList.add('visible');
+    } else {
+      watermarkBR.classList.remove('visible');
+    }
+  }
+
+  function setLit(capsule, stem, state) {
+    if (state.on && state.strength > 0.15) {
+      capsule.classList.add('lit');
+      stem.classList.add('lit');
+      // Subtle scale pulse centered on peak (simplistic)
+      const scale = 1.0 + state.strength * 0.06;
+      capsule.style.transform = `translateX(-50%) scale(${scale})`;
+    } else {
+      capsule.classList.remove('lit');
+      stem.classList.remove('lit');
+      capsule.style.transform = 'translateX(-50%)';
+    }
+  }
+
+  // =============== Driver ===============
+  let manualT = null;
+  let startMs = null;
+  let hasFinishedOnce = false;
+
+  function tick(now) {
+    if (manualT != null) {
+      render(manualT);
+    } else {
+      if (startMs == null) startMs = now;
+      const elapsed = (now - startMs) / 1000;
+      const recording = window.__recording === true;
+      let t;
+      if (recording) {
+        t = Math.min(elapsed, T.DURATION - 0.001);
+        if (elapsed >= T.DURATION && !hasFinishedOnce) hasFinishedOnce = true;
+      } else {
+        t = elapsed % T.DURATION;
+        // Show replay button when we've played at least once
+        if (elapsed >= T.DURATION) {
+          replayBtn.classList.add('visible');
+        }
+      }
+      render(t);
+    }
+    requestAnimationFrame(tick);
+  }
+
+  // First paint signal for renderer
+  document.fonts.ready.then(() => {
+    render(0);
+    requestAnimationFrame(() => {
+      window.__ready = true;
+      requestAnimationFrame(tick);
+    });
+  });
+
+  // ========= Stage scaling (fit viewport) =========
+  function fitStage() {
+    const stage = document.getElementById('stage');
+    const scaleX = window.innerWidth / 1920;
+    const scaleY = window.innerHeight / 1080;
+    const scale = Math.min(scaleX, scaleY);
+    stage.style.transform = `translate(-50%, -50%) scale(${scale})`;
+  }
+  fitStage();
+  window.addEventListener('resize', fitStage);
+
+  // Replay
+  replayBtn.addEventListener('click', () => {
+    startMs = null;
+    replayBtn.classList.remove('visible');
+  });
+
+  // =============== Expose for frame-accurate rendering ===============
+  window.__setTime = (t) => { manualT = t; render(t); };
+  window.__resume = () => { manualT = null; startMs = null; };
+  window.__duration = T.DURATION;
+  window.__render = render;
+})();
+</script>
+</body>
+</html>

+ 1099 - 521
demos/c3-motion-design.html

@@ -1,556 +1,1134 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
+<!doctype html>
+<html lang="zh-Hans">
 <head>
-<meta charset="UTF-8">
-<title>Huashu-Design · Motion Design</title>
-<script crossorigin src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
-<script crossorigin src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
-<script src="https://unpkg.com/@babel/standalone@7.25.6/babel.min.js"></script>
+<meta charset="utf-8" />
+<title>huashu-design · c3 motion design(中文版)</title>
 <link rel="preconnect" href="https://fonts.googleapis.com">
 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400;1,6..72,500&family=Noto+Serif+SC:wght@400;500;600&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600;700&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
 <style>
-  * { box-sizing: border-box; margin: 0; padding: 0; }
-  html, body { width: 100%; height: 100%; overflow: hidden; }
-  body { background: #0c0c0c; font-family: 'Newsreader', 'Noto Serif SC', Georgia, serif; color: #1a1a1a; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
+:root {
+  --bg: #000000;
+  --ink: #FFFFFF;
+  --ink-80: rgba(255,255,255,0.82);
+  --ink-60: rgba(255,255,255,0.58);
+  --muted: rgba(255,255,255,0.40);
+  --dim: rgba(255,255,255,0.18);
+  --hairline: rgba(255,255,255,0.12);
+  --hair-strong: rgba(255,255,255,0.22);
+  --accent: #D97757;
+  --accent-deep: #B85D3D;
+  --accent-dim: rgba(217,119,87,0.25);
+  --serif-cn: "Noto Serif SC", "Songti SC", "STSong", serif;
+  --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
+  --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
+  --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+}
+
+html, body {
+  margin: 0; padding: 0;
+  background: #000;
+  overflow: hidden;
+  font-family: var(--sans);
+  color: var(--ink);
+  -webkit-font-smoothing: antialiased;
+}
+* { box-sizing: border-box; }
+
+.stage {
+  position: fixed;
+  top: 50%; left: 50%;
+  width: 1920px; height: 1080px;
+  transform-origin: center center;
+  background: var(--bg);
+  overflow: hidden;
+}
+
+/* Subtle film grain overlay, 2% */
+.stage::after {
+  content: '';
+  position: absolute; inset: 0;
+  pointer-events: none;
+  opacity: 0.025;
+  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
+  mix-blend-mode: overlay;
+  z-index: 200;
+}
+
+/* Watermark */
+.watermark-tl {
+  position: absolute;
+  top: 40px; left: 56px;
+  font-family: var(--mono);
+  font-size: 14px;
+  letter-spacing: 0.2em;
+  color: rgba(255,255,255,0.16);
+  z-index: 50;
+  text-transform: none;
+  font-weight: 500;
+}
+.watermark-br {
+  position: absolute;
+  bottom: 32px; right: 48px;
+  font-family: var(--mono);
+  font-size: 10px;
+  letter-spacing: 0.24em;
+  color: rgba(255,255,255,0.22);
+  z-index: 100;
+  text-transform: uppercase;
+  opacity: 0;
+  transition: opacity 0.6s;
+}
+.watermark-br.visible { opacity: 1; }
+
+/* Scene container */
+.scene {
+  position: absolute; inset: 0;
+  opacity: 0;
+  visibility: hidden;
+  will-change: opacity;
+}
+.scene.visible { visibility: visible; }
+
+/* ============ Split layout ============ */
+.split {
+  position: absolute; inset: 0;
+}
+.split-top {
+  position: absolute;
+  top: 0; left: 0;
+  width: 100%; height: 48%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.split-bottom {
+  position: absolute;
+  bottom: 0; left: 0;
+  width: 100%; height: 52%;
+}
+
+/* Horizontal divider hairline */
+.split-divider {
+  position: absolute;
+  left: 160px; right: 160px;
+  top: 48%;
+  height: 1px;
+  background: var(--hairline);
+  z-index: 5;
+}
+
+/* Section label (top-left of each half) */
+.panel-label {
+  position: absolute;
+  top: 32px;
+  left: 160px;
+  font-family: var(--mono);
+  font-size: 12px;
+  letter-spacing: 0.3em;
+  color: var(--muted);
+  text-transform: uppercase;
+}
+.split-bottom .panel-label { top: 32px; }
+.panel-label .accent { color: var(--accent); font-weight: 500; }
+
+/* ============ Top: Timeline ============ */
+.timeline-wrap {
+  width: 1600px;
+  position: relative;
+  margin-top: 40px;
+}
+.timeline-track {
+  position: relative;
+  height: 2px;
+  background: var(--hairline);
+  width: 100%;
+}
+.timeline-track .fill {
+  position: absolute;
+  top: 0; left: 0;
+  height: 100%;
+  background: linear-gradient(90deg, var(--accent) 0%, rgba(217,119,87,0.4) 100%);
+  width: 0%;
+  will-change: width;
+}
+
+/* Tick marks */
+.tick {
+  position: absolute;
+  width: 1px;
+  height: 10px;
+  background: var(--muted);
+  top: -4px;
+  transform: translateX(-0.5px);
+}
+.tick.major { height: 14px; top: -6px; background: var(--ink-60); }
+.tick-label {
+  position: absolute;
+  top: 18px;
+  font-family: var(--mono);
+  font-size: 11px;
+  color: var(--muted);
+  letter-spacing: 0.1em;
+  transform: translateX(-50%);
+}
+
+/* Playhead */
+.playhead {
+  position: absolute;
+  top: -28px;
+  left: 0;
+  width: 2px;
+  height: 58px;
+  background: var(--accent);
+  transform: translateX(-1px);
+  will-change: transform;
+  z-index: 10;
+  box-shadow: 0 0 20px rgba(217,119,87,0.5);
+}
+.playhead::before {
+  content: '';
+  position: absolute;
+  top: -8px;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 14px; height: 14px;
+  background: var(--accent);
+  border-radius: 50%;
+  box-shadow: 0 0 16px rgba(217,119,87,0.6);
+}
+.playhead::after {
+  content: '';
+  position: absolute;
+  top: -6px;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 6px; height: 6px;
+  background: var(--bg);
+  border-radius: 50%;
+  z-index: 2;
+}
+
+/* API capsules on timeline */
+.api-capsule {
+  position: absolute;
+  top: -92px;
+  transform: translateX(-50%);
+  padding: 10px 20px;
+  border: 1px solid var(--hairline);
+  border-radius: 999px;
+  background: rgba(0,0,0,0.6);
+  backdrop-filter: blur(8px);
+  font-family: var(--mono);
+  font-size: 18px;
+  font-weight: 500;
+  color: var(--ink-60);
+  letter-spacing: 0.02em;
+  transition: none;
+  will-change: color, border-color, transform, box-shadow;
+  white-space: nowrap;
+}
+.api-capsule.lit {
+  color: var(--accent);
+  border-color: var(--accent);
+  box-shadow: 0 0 30px rgba(217,119,87,0.35);
+}
+.api-capsule .tiny {
+  font-size: 10px;
+  color: var(--muted);
+  letter-spacing: 0.2em;
+  margin-right: 10px;
+  display: inline-block;
+  vertical-align: middle;
+  opacity: 0.7;
+}
+.api-capsule.lit .tiny { color: var(--accent); opacity: 0.9; }
+
+/* Tick connector (short vertical line from capsule to timeline) */
+.capsule-stem {
+  position: absolute;
+  top: -48px;
+  width: 1px;
+  height: 44px;
+  background: var(--hairline);
+  transform: translateX(-0.5px);
+  z-index: 1;
+}
+.capsule-stem.lit { background: var(--accent); }
+
+/* ============ Bottom: Driven stage ============ */
+.driven-stage {
+  position: absolute;
+  top: 0; left: 0;
+  width: 100%; height: 100%;
+}
+
+.viz {
+  position: absolute;
+  top: 46%; left: 50%;
+  transform: translate(-50%, -50%);
+  width: 1000px; height: 400px;
+  opacity: 0;
+  will-change: opacity;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* viz 1: useTime — clock */
+.viz-clock {
+  position: relative;
+  width: 280px; height: 280px;
+  border: 1.5px solid var(--hair-strong);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.viz-clock .tickmark {
+  position: absolute;
+  width: 1px;
+  height: 8px;
+  background: var(--muted);
+  top: 10px;
+  left: 50%;
+  transform-origin: 50% 130px;
+}
+.viz-clock .tickmark.q {
+  width: 2px;
+  height: 14px;
+  background: var(--ink-60);
+}
+.viz-clock .hand-h {
+  position: absolute;
+  width: 3px; height: 80px;
+  background: var(--ink);
+  left: 50%;
+  bottom: 50%;
+  transform-origin: 50% 100%;
+  transform: translateX(-50%) rotate(30deg);
+  border-radius: 2px;
+  will-change: transform;
+}
+.viz-clock .hand-m {
+  position: absolute;
+  width: 2px; height: 110px;
+  background: var(--ink-80);
+  left: 50%;
+  bottom: 50%;
+  transform-origin: 50% 100%;
+  transform: translateX(-50%) rotate(120deg);
+  border-radius: 2px;
+  will-change: transform;
+}
+.viz-clock .hand-s {
+  position: absolute;
+  width: 1.5px; height: 120px;
+  background: var(--accent);
+  left: 50%;
+  bottom: 50%;
+  transform-origin: 50% 100%;
+  transform: translateX(-50%) rotate(0deg);
+  border-radius: 2px;
+  will-change: transform;
+  box-shadow: 0 0 10px rgba(217,119,87,0.4);
+}
+.viz-clock .center-dot {
+  width: 12px; height: 12px;
+  border-radius: 50%;
+  background: var(--accent);
+  z-index: 5;
+  box-shadow: 0 0 10px rgba(217,119,87,0.6);
+}
+.viz-clock-label {
+  position: absolute;
+  bottom: -48px;
+  left: 50%;
+  transform: translateX(-50%);
+  font-family: var(--mono);
+  font-size: 13px;
+  color: var(--muted);
+  letter-spacing: 0.12em;
+  white-space: nowrap;
+}
+.viz-clock-label .val {
+  color: var(--accent);
+  font-variant-numeric: tabular-nums;
+}
+
+/* viz 2: interpolate — morph box */
+.viz-morph {
+  display: flex;
+  gap: 80px;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+}
+.morph-box {
+  width: 260px; height: 260px;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.morph-rect {
+  background: var(--accent);
+  border-radius: 4px;
+  will-change: width, height, background, border-radius, transform;
+  box-shadow: 0 0 40px rgba(217,119,87,0.25);
+}
+.morph-label {
+  position: absolute;
+  bottom: -48px;
+  left: 50%;
+  transform: translateX(-50%);
+  font-family: var(--mono);
+  font-size: 12px;
+  color: var(--muted);
+  letter-spacing: 0.12em;
+  white-space: nowrap;
+}
+.morph-label .val { color: var(--accent); font-variant-numeric: tabular-nums; }
+.morph-arrow {
+  font-family: var(--mono);
+  font-size: 28px;
+  color: var(--muted);
+  letter-spacing: 0.2em;
+}
+
+/* viz 3: Easing — curves */
+.viz-curves {
+  position: relative;
+  width: 720px; height: 320px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.curves-svg {
+  width: 100%; height: 100%;
+}
+.curve-label {
+  position: absolute;
+  font-family: var(--mono);
+  font-size: 12px;
+  color: var(--muted);
+  letter-spacing: 0.08em;
+  white-space: nowrap;
+}
+/* viewBox 720x320 → right edge ≈ 680 of 720 → 94%. Vertical:
+   y=40 is visual top (output value 1), y=260 is bottom (value 0).
+   Labels go at right side, vertically aligned with where each curve
+   approaches its asymptote at t≈0.7.
+   expoOut at t=0.7 ~ 0.99 (≈ y=42)
+   cubicOut at t=0.7 ~ 0.973 (≈ y=46)
+   linear at t=0.7 ~ 0.7 (≈ y=106)
+   So spatial order top→bottom: expoOut, cubicOut, linear
+*/
+.curve-label.l-expo   { top:  6%; right: 4%; color: var(--accent); }
+.curve-label.l-cubic  { top: 16%; right: 4%; color: rgba(255,255,255,0.78); }
+.curve-label.l-linear { top: 36%; right: 4%; color: rgba(255,255,255,0.42); }
+
+.curve-dot {
+  position: absolute;
+  width: 10px; height: 10px;
+  border-radius: 50%;
+  background: var(--accent);
+  transform: translate(-50%, -50%);
+  box-shadow: 0 0 14px rgba(217,119,87,0.6);
+  will-change: left, top;
+}
+
+/* viz 4: useSprite — choreographed grid */
+.viz-sprites {
+  display: grid;
+  grid-template-columns: repeat(6, 60px);
+  grid-template-rows: repeat(4, 60px);
+  gap: 18px;
+  justify-content: center;
+  align-content: center;
+  padding: 40px 0;
+}
+.sprite {
+  width: 60px; height: 60px;
+  background: var(--hairline);
+  border: 1px solid var(--dim);
+  will-change: transform, opacity, background;
+  opacity: 0;
+  border-radius: 2px;
+}
+
+.sprite-label {
+  position: absolute;
+  bottom: -6px;
+  left: 50%;
+  transform: translateX(-50%);
+  font-family: var(--mono);
+  font-size: 12px;
+  color: var(--muted);
+  letter-spacing: 0.12em;
+  white-space: nowrap;
+}
+.sprite-label .val { color: var(--accent); font-variant-numeric: tabular-nums; }
+
+/* ============ Scene 0: Opening title ============ */
+.scene-intro {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+.scene-intro .title {
+  font-family: var(--serif-cn);
+  font-size: 108px;
+  font-weight: 300;
+  letter-spacing: -0.02em;
+  color: var(--ink);
+  line-height: 1.05;
+  will-change: opacity, transform, font-weight;
+}
+.scene-intro .title .accent { color: var(--accent); }
+.scene-intro .sub {
+  margin-top: 28px;
+  font-family: var(--mono);
+  font-size: 16px;
+  color: var(--muted);
+  letter-spacing: 0.3em;
+}
+
+/* ============ Scene 2: Brand reveal (米色面板标准动作) ============ */
+.scene-brand {
+  background: transparent;
+  pointer-events: none;
+  z-index: 150;
+}
+.brand-panel {
+  position: absolute;
+  inset: 0;
+  background: #F5F4F0;
+  transform: translateY(100%);
+  will-change: transform;
+}
+.brand-wordmark {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, calc(-50% + 20px));
+  font-family: "Source Serif 4", Georgia, serif;
+  font-size: 72px;
+  font-weight: 100;
+  font-variation-settings: "wght" 100;
+  letter-spacing: -0.01em;
+  color: #1A1918;
+  text-align: center;
+  line-height: 1;
+  opacity: 0;
+  white-space: nowrap;
+  will-change: opacity, transform, font-weight, font-variation-settings;
+}
+.brand-wordmark .accent { color: #D97757; font-weight: inherit; }
+.brand-line {
+  position: absolute;
+  top: calc(50% + 60px);
+  left: 50%;
+  transform: translateX(-50%);
+  height: 2px;
+  width: 0px;
+  background: #D97757;
+  will-change: width;
+}
+
+/* ============ Replay button (hidden during record) ============ */
+.replay-btn {
+  position: absolute;
+  bottom: 40px;
+  left: 50%;
+  transform: translateX(-50%);
+  padding: 12px 32px;
+  border: 1px solid var(--hair-strong);
+  border-radius: 999px;
+  background: transparent;
+  color: var(--ink-60);
+  font-family: var(--mono);
+  font-size: 13px;
+  letter-spacing: 0.2em;
+  cursor: pointer;
+  opacity: 0;
+  pointer-events: none;
+  transition: opacity 0.4s;
+  z-index: 300;
+}
+.replay-btn.visible {
+  opacity: 1;
+  pointer-events: auto;
+}
 </style>
 </head>
 <body>
-<div id="root"></div>
 
-<!-- animations.jsx inlined -->
-<script type="text/babel">
-(function() {
-  const { createContext, useContext, useState, useEffect, useRef } = React;
-  const TimeContext = createContext({ time: 0, duration: 10, playing: false });
-  const SpriteContext = createContext(null);
-  const Easing = {
-    linear: t => t,
-    easeIn: t => t * t,
-    easeOut: t => 1 - (1 - t) * (1 - t),
-    easeInOut: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
-    spring: t => {
-      const c = (2 * Math.PI) / 3;
-      return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c) + 1;
-    },
-  };
-  function interpolate(t, input, output, easing) {
-    const [a, b] = input, [x, y] = output;
-    if (t <= a) return x; if (t >= b) return y;
-    let p = (t - a) / (b - a); if (easing) p = easing(p);
-    return x + (y - x) * p;
-  }
-  function useTime() { return useContext(TimeContext).time; }
-  function useSprite() { return useContext(SpriteContext) || { t: 0, elapsed: 0, duration: 0 }; }
-  function Stage({ duration = 10, width = 1920, height = 1080, loop = true, children, bgColor = '#fff' }) {
-    const [time, setTime] = useState(0);
-    const [playing, setPlaying] = useState(true);
-    const [scale, setScale] = useState(1);
-    const rafRef = useRef(null);
-    const effectiveLoop = (typeof window !== 'undefined' && window.__recording) ? false : loop;
-    useEffect(() => {
-      const update = () => {
-        const s = Math.min(window.innerWidth / width, (window.innerHeight - 56) / height);
-        setScale(s);
-      };
-      update(); window.addEventListener('resize', update);
-      return () => window.removeEventListener('resize', update);
-    }, [width, height]);
-    useEffect(() => {
-      if (!playing) return;
-      let cancelled = false, last = null;
-      function tick(now) {
-        if (cancelled) return;
-        if (last === null) { last = now; if (typeof window !== 'undefined') window.__ready = true; }
-        const delta = (now - last) / 1000; last = now;
-        setTime(prev => {
-          const next = prev + delta;
-          if (next >= duration) return effectiveLoop ? 0 : duration - 0.001;
-          return next;
-        });
-        rafRef.current = requestAnimationFrame(tick);
-      }
-      const start = () => { if (!cancelled) rafRef.current = requestAnimationFrame(tick); };
-      if (document.fonts && document.fonts.ready) document.fonts.ready.then(start); else start();
-      return () => { cancelled = true; cancelAnimationFrame(rafRef.current); };
-    }, [playing, duration, effectiveLoop]);
-    const progress = time / duration;
-    const ctx = { time, duration, playing, setPlaying, setTime };
-    return (
-      <TimeContext.Provider value={ctx}>
-        <div style={{position:'fixed', inset:0, background:'#0c0c0c', display:'flex', flexDirection:'column'}}>
-          <div style={{flex:1, position:'relative', overflow:'hidden'}}>
-            <div style={{position:'absolute', top:'50%', left:'50%', transformOrigin:'center center', width, height, background: bgColor, overflow:'hidden', transform:`translate(-50%, -50%) scale(${scale})`}}>
-              {children}
+<div class="stage" id="stage">
+
+  <!-- Top-left watermark (always on) -->
+  <div class="watermark-tl">HUASHU · DESIGN</div>
+
+  <!-- ============ Scene 0: Intro (0 → 1.6s) ============ -->
+  <div class="scene scene-intro" id="scene-intro">
+    <div class="title" id="introTitle">时间轴 <span class="accent">=</span> 代码</div>
+    <div class="sub" id="introSub">TIMELINE · MOTION · ENGINE</div>
+  </div>
+
+  <!-- ============ Scene 1: Split view (1.6 → 8.2s) ============ -->
+  <div class="scene" id="scene-main">
+    <div class="split">
+
+      <!-- TOP: Timeline -->
+      <div class="split-top">
+        <div class="panel-label">TIMELINE · <span class="accent">PLAYHEAD</span></div>
+        <div class="timeline-wrap">
+          <div class="timeline-track">
+            <div class="fill" id="timelineFill"></div>
+
+            <!-- Tick marks (10 ticks for 10s) -->
+            <div class="tick" style="left: 0%;"></div>
+            <div class="tick major" style="left: 0%;"></div>
+            <div class="tick" style="left: 10%;"></div>
+            <div class="tick major" style="left: 20%;"></div>
+            <div class="tick" style="left: 30%;"></div>
+            <div class="tick major" style="left: 40%;"></div>
+            <div class="tick" style="left: 50%;"></div>
+            <div class="tick major" style="left: 60%;"></div>
+            <div class="tick" style="left: 70%;"></div>
+            <div class="tick major" style="left: 80%;"></div>
+            <div class="tick" style="left: 90%;"></div>
+            <div class="tick major" style="left: 100%;"></div>
+
+            <div class="tick-label" style="left: 0%;">0s</div>
+            <div class="tick-label" style="left: 20%;">2s</div>
+            <div class="tick-label" style="left: 40%;">4s</div>
+            <div class="tick-label" style="left: 60%;">6s</div>
+            <div class="tick-label" style="left: 80%;">8s</div>
+            <div class="tick-label" style="left: 100%;">10s</div>
+
+            <!-- API capsules anchored at their trigger points -->
+            <!-- Scene-main spans 1.6→8.2; timeline maps 0→10s globally for clarity.
+                 cap positions here mirror when each API is "active" on the lower viz. -->
+            <!-- useTime: global t = 1.8 → 3.3 → center ~2.5s → 25% -->
+            <div class="capsule-stem" id="stem-time" style="left: 18%;"></div>
+            <div class="api-capsule" id="cap-time" style="left: 18%;">
+              <span class="tiny">01</span>useTime
             </div>
-          </div>
-          <div className="no-record" style={{position:'fixed', bottom:0, left:0, right:0, background:'rgba(0,0,0,0.8)', padding:'12px 20px', display:'flex', alignItems:'center', gap:16, color:'#fff', fontSize:12, zIndex:100}}>
-            <button onClick={()=>setPlaying(p=>!p)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>{playing?'⏸ 暂停':'▶ 播放'}</button>
-            <button onClick={()=>setTime(0)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>⏮ 开始</button>
-            <div style={{fontFamily:'ui-monospace, monospace', fontVariantNumeric:'tabular-nums', minWidth:90}}>{time.toFixed(2)}s / {duration.toFixed(2)}s</div>
-            <div style={{flex:1, height:4, background:'rgba(255,255,255,0.2)', borderRadius:2, position:'relative'}}>
-              <div style={{position:'absolute', top:0, left:0, height:'100%', width:`${progress*100}%`, background:'#fff', borderRadius:2}} />
+
+            <!-- interpolate: 3.5 → 5s → center 4.2s → 42% -->
+            <div class="capsule-stem" id="stem-interp" style="left: 38%;"></div>
+            <div class="api-capsule" id="cap-interp" style="left: 38%;">
+              <span class="tiny">02</span>interpolate
             </div>
-          </div>
-        </div>
-      </TimeContext.Provider>
-    );
-  }
-  function Sprite({ start = 0, end, children, style }) {
-    const { time } = useContext(TimeContext);
-    const actualEnd = end == null ? Infinity : end;
-    if (time < start || time >= actualEnd) return null;
-    const duration = actualEnd - start;
-    const elapsed = time - start;
-    const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
-    return (
-      <SpriteContext.Provider value={{ t, elapsed, duration, start, end: actualEnd }}>
-        <div style={{position:'absolute', inset:0, ...style}}>{children}</div>
-      </SpriteContext.Provider>
-    );
-  }
-  window.Animations = { Stage, Sprite, useTime, useSprite, Easing, interpolate };
-})();
-</script>
 
-<!-- Demo scene -->
-<script type="text/babel">
-const { Stage, Sprite, useTime, useSprite, Easing, interpolate } = window.Animations;
-
-const CREAM = '#FAF6EF';
-const INK = '#1a1a1a';
-const TERRA = '#C04A1A';
-const OLIVE = '#6a6b4e';
-const DEEP_BLUE = '#2a3552';
-const ASH = '#6b6b6b';
-const LINE = '#d9d2c5';
-
-const serif = "'Newsreader', 'Noto Serif SC', Georgia, serif";
-const sans = "'Inter', -apple-system, sans-serif";
-const mono = "'JetBrains Mono', ui-monospace, monospace";
-
-// ── Scene 1: Title (0 – 3s) ────────────────────────────
-function Scene1_Title() {
-  const { elapsed } = useSprite();
-  const titleY = interpolate(elapsed, [0, 1.2], [60, 0], Easing.easeOut);
-  const titleOp = interpolate(elapsed, [0, 0.8], [0, 1]);
-  const subOp = interpolate(elapsed, [0.6, 1.4], [0, 1]);
-  const lineW = interpolate(elapsed, [0.9, 1.6], [0, 520]);
-  const apiOp = interpolate(elapsed, [1.4, 2.2], [0, 1]);
-  const fadeOut = interpolate(elapsed, [2.6, 3.0], [1, 0]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
-      display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
-      <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
-        color: TERRA, marginBottom: 24, opacity: titleOp}}>
-        动画引擎 · Stage + Sprite
-      </div>
-      <div style={{fontFamily: serif, fontSize: 148, fontWeight: 500, color: INK,
-        lineHeight: 1, letterSpacing:'-0.02em', opacity: titleOp,
-        transform: `translateY(${titleY}px)`}}>
-        <span style={{fontStyle:'italic', color: TERRA}}>Motion</span> Design
-      </div>
-      <div style={{height: 1, background: INK, width: lineW, marginTop: 36}} />
-      <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 24, color: ASH,
-        marginTop: 28, opacity: subOp}}>
-        时间驱动 · 可编排 · 60fps 导出
-      </div>
-      <div style={{fontFamily: mono, fontSize: 14, color: ASH,
-        marginTop: 40, opacity: apiOp, letterSpacing:'0.1em'}}>
-        &lt;Stage&gt; · &lt;Sprite&gt; · useTime() · useSprite() · interpolate() · Easing
-      </div>
-    </div>
-  );
-}
-
-// ── Scene 2: Easing functions comparison (3 – 8s) ────────
-function Scene2_Easing() {
-  const { elapsed } = useSprite();
-  const titleOp = interpolate(elapsed, [0, 0.4], [0, 1]);
-  const fadeOut = interpolate(elapsed, [4.6, 5.0], [1, 0]);
-
-  // Lane sweep cycle every 2s
-  const cycle = (elapsed % 2.2) / 2.0;
-  const sweepT = Math.min(1, Math.max(0, cycle));
-
-  const curves = [
-    { name: 'linear', label: 'linear', fn: Easing.linear, color: ASH },
-    { name: 'easeOut', label: 'easeOut', fn: Easing.easeOut, color: OLIVE },
-    { name: 'spring', label: 'spring', fn: Easing.spring, color: TERRA },
-    { name: 'easeInOut', label: 'easeInOut', fn: Easing.easeInOut, color: DEEP_BLUE },
-  ];
-
-  const trackLeft = 320;
-  const trackRight = 1480;
-  const trackLen = trackRight - trackLeft;
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
-      padding: '80px 100px', display:'flex', flexDirection:'column'}}>
-      <div style={{opacity: titleOp, marginBottom: 50,
-        display:'flex', justifyContent:'space-between', alignItems:'baseline'}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-            letterSpacing:'0.3em', marginBottom: 6}}>场景 1 · EASING</div>
-          <div style={{fontFamily: serif, fontSize: 56, fontWeight: 500, color: INK,
-            letterSpacing:'-0.01em'}}>
-            四种缓动曲线同跑
-          </div>
-        </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18, color: ASH,
-          textAlign:'right'}}>
-          同样的 2 秒,<br/>
-          走出四种不同的「节奏感」
-        </div>
-      </div>
+            <!-- Easing: 5 → 6.5s → center 5.7s → 57% -->
+            <div class="capsule-stem" id="stem-easing" style="left: 58%;"></div>
+            <div class="api-capsule" id="cap-easing" style="left: 58%;">
+              <span class="tiny">03</span>Easing
+            </div>
 
-      <div style={{flex: 1, position:'relative'}}>
-        {curves.map((c, i) => {
-          const y = 80 + i * 140;
-          const t = c.fn(sweepT);
-          const x = trackLeft + trackLen * t;
-          // Draw the curve as a mini sparkline right of track
-          const sparkW = 160, sparkH = 50;
-          const sparkPts = Array.from({length: 30}, (_, k) => {
-            const tx = k / 29;
-            const ty = 1 - c.fn(tx);
-            return `${tx * sparkW},${ty * sparkH}`;
-          }).join(' ');
-          return (
-            <div key={i}>
-              {/* Label (left) */}
-              <div style={{position:'absolute', left: 0, top: y - 22, width: 280,
-                fontFamily: mono, fontSize: 14, color: INK, letterSpacing:'0.05em'}}>
-                <span style={{color: c.color, marginRight: 12}}>●</span>
-                Easing.<span style={{color: c.color}}>{c.label}</span>
-              </div>
-              {/* Track */}
-              <div style={{position:'absolute', left: trackLeft, top: y,
-                width: trackLen, height: 2, background: LINE}} />
-              {/* Dot */}
-              <div style={{position:'absolute', left: x - 14, top: y - 14,
-                width: 28, height: 28, borderRadius: '50%',
-                background: c.color,
-                boxShadow: `0 4px 12px ${c.color}55`}} />
-              {/* Sparkline */}
-              <svg style={{position:'absolute', left: trackRight + 60, top: y - sparkH/2 - 5,
-                width: sparkW, height: sparkH}}>
-                <polyline points={sparkPts} stroke={c.color} strokeWidth="1.5" fill="none" />
-                <circle cx={sweepT * sparkW} cy={(1 - c.fn(sweepT)) * sparkH}
-                  r="3.5" fill={c.color} />
-              </svg>
+            <!-- useSprite: 6.5 → 8s → center 7.2s → 72% -->
+            <div class="capsule-stem" id="stem-sprite" style="left: 80%;"></div>
+            <div class="api-capsule" id="cap-sprite" style="left: 80%;">
+              <span class="tiny">04</span>useSprite
             </div>
-          );
-        })}
-      </div>
 
-      {/* Timeline at bottom */}
-      <div style={{marginTop: 10, position:'relative', height: 30}}>
-        <div style={{fontFamily: mono, fontSize: 10, color: ASH, letterSpacing:'0.15em'}}>
-          t = <span style={{color: INK}}>{sweepT.toFixed(2)}</span> &nbsp; · &nbsp; 周期 2.0s
-        </div>
-      </div>
-    </div>
-  );
-}
-
-// ── Scene 3: interpolate() function demo (8 – 14s) ───────
-function Scene3_Interpolate() {
-  const { elapsed } = useSprite();
-  const titleOp = interpolate(elapsed, [0, 0.4], [0, 1]);
-  const fadeOut = interpolate(elapsed, [5.6, 6.0], [1, 0]);
-
-  // Animated t value: 0→1 cycle over ~3s
-  const cycle = (elapsed % 3.2) / 3.0;
-  const t = Math.min(1, Math.max(0, cycle));
-
-  // Three mapped outputs from same t:
-  const opacity = interpolate(t, [0, 1], [0, 1]);
-  const scale = interpolate(t, [0, 1], [0.4, 1.2], Easing.spring);
-  const rotation = interpolate(t, [0, 1], [-30, 30], Easing.easeInOut);
-  const translateX = interpolate(t, [0, 0.5, 1], [-80, 40, 0]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
-      padding: '80px 100px', display:'flex', flexDirection:'column'}}>
-      <div style={{opacity: titleOp, marginBottom: 40,
-        display:'flex', justifyContent:'space-between', alignItems:'baseline'}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-            letterSpacing:'0.3em', marginBottom: 6}}>场景 2 · INTERPOLATE</div>
-          <div style={{fontFamily: serif, fontSize: 56, fontWeight: 500, color: INK,
-            letterSpacing:'-0.01em'}}>
-            一个 <span style={{fontStyle:'italic', color: TERRA}}>t</span>,四种变化
+            <!-- Playhead -->
+            <div class="playhead" id="playhead"></div>
           </div>
         </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18, color: ASH,
-          textAlign:'right'}}>
-          用同一条时间轴,<br/>
-          映射出透明度、尺寸、旋转、位移
-        </div>
       </div>
 
-      {/* t value progress bar */}
-      <div style={{background:'#fff', border: `1px solid ${LINE}`,
-        padding: '20px 32px', marginBottom: 30}}>
-        <div style={{display:'flex', justifyContent:'space-between',
-          alignItems:'baseline', marginBottom: 14}}>
-          <div style={{fontFamily: mono, fontSize: 13, color: INK}}>
-            <span style={{color: ASH}}>const</span> t = <span style={{color: TERRA}}>{t.toFixed(3)}</span>
-          </div>
-          <div style={{fontFamily: mono, fontSize: 11, color: ASH, letterSpacing:'0.15em'}}>
-            时间 → 0 到 1
-          </div>
-        </div>
-        <div style={{height: 4, background: LINE, position:'relative'}}>
-          <div style={{position:'absolute', top:0, left:0, height:'100%',
-            width: `${t * 100}%`, background: TERRA}} />
-        </div>
-      </div>
+      <!-- Divider -->
+      <div class="split-divider"></div>
 
-      {/* Four demos */}
-      <div style={{flex: 1, display:'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 24}}>
-        {[
-          { name: 'opacity', code: 'interpolate(t, [0,1], [0,1])', val: opacity, render:
-            <div style={{width: 120, height: 120, background: TERRA, opacity}} /> },
-          { name: 'scale + spring', code: 'interpolate(t, [0,1], [0.4,1.2], spring)', val: scale, render:
-            <div style={{width: 120, height: 120, background: OLIVE,
-              transform: `scale(${scale})`}} /> },
-          { name: 'rotate', code: 'interpolate(t, [0,1], [-30,30], easeInOut)', val: rotation, render:
-            <div style={{width: 120, height: 120, background: DEEP_BLUE,
-              transform: `rotate(${rotation}deg)`}} /> },
-          { name: 'translateX (3 stops)', code: 'interpolate(t, [0,.5,1], [-80,40,0])', val: translateX, render:
-            <div style={{width: 120, height: 120, background: INK,
-              transform: `translateX(${translateX}px)`}} /> },
-        ].map((d, i) => (
-          <div key={i} style={{background:'#fff', border:`1px solid ${LINE}`,
-            padding: '18px 18px 14px', display:'flex', flexDirection:'column'}}>
-            <div style={{fontFamily: mono, fontSize: 9, color: TERRA,
-              letterSpacing:'0.2em', marginBottom: 6}}>0{i+1}</div>
-            <div style={{fontFamily: serif, fontSize: 18, fontWeight: 500, color: INK,
-              marginBottom: 6}}>{d.name}</div>
-            <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-              marginBottom: 20, minHeight: 32, wordBreak:'break-all', lineHeight: 1.5}}>
-              {d.code}
-            </div>
-            <div style={{flex: 1, display:'flex', alignItems:'center',
-              justifyContent:'center', overflow:'hidden'}}>
-              {d.render}
-            </div>
-            <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-              marginTop: 12, textAlign:'right'}}>
-              = <span style={{color: TERRA}}>{d.val.toFixed(2)}</span>
+      <!-- BOTTOM: Driven stage -->
+      <div class="split-bottom">
+        <div class="panel-label">DRIVEN · <span class="accent">STAGE</span></div>
+        <div class="driven-stage">
+
+          <!-- viz 1: useTime — clock -->
+          <div class="viz" id="viz-time">
+            <div class="viz-clock" id="clockRoot">
+              <!-- 12 tick marks -->
+              <div class="tickmark q" style="transform: translate(-50%, 0) rotate(0deg);"></div>
+              <div class="tickmark" style="transform: translate(-50%, 0) rotate(30deg);"></div>
+              <div class="tickmark" style="transform: translate(-50%, 0) rotate(60deg);"></div>
+              <div class="tickmark q" style="transform: translate(-50%, 0) rotate(90deg);"></div>
+              <div class="tickmark" style="transform: translate(-50%, 0) rotate(120deg);"></div>
+              <div class="tickmark" style="transform: translate(-50%, 0) rotate(150deg);"></div>
+              <div class="tickmark q" style="transform: translate(-50%, 0) rotate(180deg);"></div>
+              <div class="tickmark" style="transform: translate(-50%, 0) rotate(210deg);"></div>
+              <div class="tickmark" style="transform: translate(-50%, 0) rotate(240deg);"></div>
+              <div class="tickmark q" style="transform: translate(-50%, 0) rotate(270deg);"></div>
+              <div class="tickmark" style="transform: translate(-50%, 0) rotate(300deg);"></div>
+              <div class="tickmark" style="transform: translate(-50%, 0) rotate(330deg);"></div>
+
+              <div class="hand-h" id="handH"></div>
+              <div class="hand-m" id="handM"></div>
+              <div class="hand-s" id="handS"></div>
+              <div class="center-dot"></div>
+
+              <div class="viz-clock-label">
+                t = <span class="val" id="timeVal">0.00s</span>
+              </div>
             </div>
           </div>
-        ))}
-      </div>
-    </div>
-  );
-}
-
-// ── Scene 4: Sprite sequencing on timeline (14 – 20s) ───
-function Scene4_Sprite() {
-  const { elapsed } = useSprite();
-  const titleOp = interpolate(elapsed, [0, 0.4], [0, 1]);
-  const fadeOut = interpolate(elapsed, [5.6, 6.0], [1, 0]);
-
-  // Timeline plays 6s
-  const localTime = Math.min(elapsed, 5.6);
-
-  const sprites = [
-    { name: 'Title',  start: 0.0, end: 2.5, color: TERRA,   y: 0, label: '标题' },
-    { name: 'Image',  start: 0.8, end: 3.5, color: OLIVE,   y: 1, label: '图像淡入' },
-    { name: 'Text',   start: 1.8, end: 4.5, color: DEEP_BLUE, y: 2, label: '正文' },
-    { name: 'Outro',  start: 4.0, end: 5.5, color: '#8b4a2b', y: 3, label: '结尾' },
-  ];
-
-  const timelineLeft = 100;
-  const timelineRight = 1820;
-  const timelineW = timelineRight - timelineLeft;
-  const totalDur = 5.6;
-  const cursorX = timelineLeft + (localTime / totalDur) * timelineW;
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
-      padding: '80px 100px 60px', display:'flex', flexDirection:'column'}}>
-      <div style={{opacity: titleOp, marginBottom: 30,
-        display:'flex', justifyContent:'space-between', alignItems:'baseline'}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-            letterSpacing:'0.3em', marginBottom: 6}}>场景 3 · SPRITE 编排</div>
-          <div style={{fontFamily: serif, fontSize: 56, fontWeight: 500, color: INK,
-            letterSpacing:'-0.01em'}}>
-            时间片段 · <span style={{fontStyle:'italic'}}>同台起舞</span>
-          </div>
-        </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18, color: ASH,
-          textAlign:'right'}}>
-          每个 &lt;Sprite start=... end=...&gt;<br/>
-          在自己的时间窗口出场、退场
-        </div>
-      </div>
 
-      {/* Live visualization area */}
-      <div style={{background:'#fff', border:`1px solid ${LINE}`, flex: 1,
-        position:'relative', overflow:'hidden', marginBottom: 30}}>
-        {sprites.map((s, i) => {
-          const active = localTime >= s.start && localTime < s.end;
-          if (!active) return null;
-          const localT = (localTime - s.start) / (s.end - s.start);
-          const op = interpolate(localT, [0, 0.15, 0.85, 1], [0, 1, 1, 0]);
-          const ty = interpolate(localT, [0, 0.2], [30, 0], Easing.easeOut);
-
-          if (s.name === 'Title') {
-            return (
-              <div key={i} style={{position:'absolute', top: 60, left: 80, right: 80,
-                opacity: op, transform: `translateY(${ty}px)`}}>
-                <div style={{fontFamily: mono, fontSize: 10, color: s.color,
-                  letterSpacing:'0.3em', marginBottom: 10}}>CHAPTER 01</div>
-                <div style={{fontFamily: serif, fontSize: 64, fontWeight: 500,
-                  color: INK, lineHeight: 1.05, letterSpacing:'-0.01em'}}>
-                  如何让动画 <span style={{fontStyle:'italic', color: s.color}}>好看</span>
-                </div>
-              </div>
-            );
-          }
-          if (s.name === 'Image') {
-            return (
-              <div key={i} style={{position:'absolute', top: 60, right: 80,
-                width: 380, height: 240, opacity: op,
-                transform: `translateY(${ty}px)`,
-                background: `linear-gradient(135deg, ${s.color}, ${s.color}88 50%, ${s.color}33)`,
-                overflow: 'hidden'}}>
-                <div style={{position:'absolute', inset: 0,
-                  background: `radial-gradient(circle at 30% 30%, ${s.color}aa, transparent 50%)`}} />
-                <div style={{position:'absolute', bottom: 14, left: 16,
-                  fontFamily: mono, fontSize: 9, color: '#fff',
-                  letterSpacing:'0.2em', opacity: 0.8}}>
-                  IMAGE · FADE-IN
-                </div>
+          <!-- viz 2: interpolate — morph -->
+          <div class="viz" id="viz-interp">
+            <div class="viz-morph">
+              <div class="morph-box">
+                <div class="morph-rect" id="morphFrom" style="width: 80px; height: 80px; background: var(--hair-strong); border-radius: 2px;"></div>
+                <div class="morph-label">FROM · <span class="val">0 → 100</span></div>
               </div>
-            );
-          }
-          if (s.name === 'Text') {
-            return (
-              <div key={i} style={{position:'absolute', bottom: 80, left: 80, right: 80,
-                opacity: op, transform: `translateY(${ty}px)`}}>
-                <div style={{fontFamily: mono, fontSize: 10, color: s.color,
-                  letterSpacing:'0.3em', marginBottom: 10}}>BODY</div>
-                <div style={{fontFamily: serif, fontSize: 20, color: INK,
-                  lineHeight: 1.55, maxWidth: 720}}>
-                  好的 motion 不是每个元素都在抢戏——是<span style={{fontStyle:'italic'}}>一个
-                  </span>进、<span style={{fontStyle:'italic'}}>一个</span>退、留足呼吸,
-                  最后合奏收尾。
-                </div>
+              <div class="morph-arrow">──────→</div>
+              <div class="morph-box">
+                <div class="morph-rect" id="morphTo"></div>
+                <div class="morph-label">INTERPOLATE · <span class="val" id="interpVal">0.00</span></div>
               </div>
-            );
-          }
-          if (s.name === 'Outro') {
-            return (
-              <div key={i} style={{position:'absolute', inset: 0,
-                opacity: op, display:'flex', alignItems:'center',
-                justifyContent:'center', flexDirection:'column'}}>
-                <div style={{fontFamily: serif, fontSize: 88, fontWeight: 500,
-                  color: s.color, fontStyle:'italic', letterSpacing:'-0.01em'}}>
-                  — fin —
-                </div>
-                <div style={{fontFamily: mono, fontSize: 11, color: ASH,
-                  letterSpacing:'0.3em', marginTop: 14}}>
-                  4 SPRITES · 5.5 SECONDS · 1 STAGE
-                </div>
-              </div>
-            );
-          }
-        })}
-      </div>
+            </div>
+          </div>
+
+          <!-- viz 3: Easing — 3 curves drawn in parallel -->
+          <div class="viz" id="viz-easing">
+            <div class="viz-curves">
+              <svg class="curves-svg" viewBox="0 0 720 320" preserveAspectRatio="none">
+                <!-- Grid -->
+                <line x1="60" y1="260" x2="680" y2="260" stroke="rgba(255,255,255,0.18)" stroke-width="1"/>
+                <line x1="60" y1="260" x2="60" y2="40" stroke="rgba(255,255,255,0.18)" stroke-width="1"/>
+
+                <!-- Axis labels -->
+                <text x="50" y="266" text-anchor="end" fill="rgba(255,255,255,0.4)" font-family="JetBrains Mono, monospace" font-size="11">0</text>
+                <text x="50" y="48" text-anchor="end" fill="rgba(255,255,255,0.4)" font-family="JetBrains Mono, monospace" font-size="11">1</text>
+                <text x="680" y="282" text-anchor="end" fill="rgba(255,255,255,0.4)" font-family="JetBrains Mono, monospace" font-size="11">t</text>
+
+                <!-- Curves -->
+                <path id="pathLinear" d="M 60 260 L 60 260" stroke="rgba(255,255,255,0.42)" stroke-width="1.5" fill="none" stroke-linecap="round"/>
+                <path id="pathCubic" d="M 60 260 L 60 260" stroke="rgba(255,255,255,0.75)" stroke-width="1.8" fill="none" stroke-linecap="round"/>
+                <path id="pathExpo" d="M 60 260 L 60 260" stroke="#D97757" stroke-width="2.2" fill="none" stroke-linecap="round"/>
+              </svg>
+              <div class="curve-label l-linear">linear</div>
+              <div class="curve-label l-cubic">cubicOut</div>
+              <div class="curve-label l-expo">expoOut</div>
+            </div>
+          </div>
 
-      {/* Timeline viz (showing sprite spans) */}
-      <div style={{position:'relative', height: 110}}>
-        {/* Labels */}
-        {sprites.map((s, i) => (
-          <div key={i} style={{position:'absolute', left: 0, top: i * 22,
-            fontFamily: mono, fontSize: 10, color: ASH, letterSpacing:'0.05em'}}>
-            <span style={{color: s.color, marginRight: 8}}>●</span>{s.label}
+          <!-- viz 4: useSprite — 24 sprites -->
+          <div class="viz" id="viz-sprite">
+            <div class="viz-sprites" id="spriteGrid">
+              <!-- 24 sprites (6x4), filled by JS -->
+            </div>
           </div>
-        ))}
-        {/* Tracks */}
-        {sprites.map((s, i) => {
-          const x0 = timelineLeft + (s.start / totalDur) * timelineW;
-          const x1 = timelineLeft + (s.end / totalDur) * timelineW;
-          const active = localTime >= s.start && localTime < s.end;
-          return (
-            <div key={i} style={{position:'absolute',
-              left: x0, top: i * 22 - 2, width: x1 - x0, height: 16,
-              background: active ? s.color : `${s.color}55`,
-              borderLeft: `2px solid ${s.color}`}} />
-          );
-        })}
-        {/* Playhead cursor */}
-        <div style={{position:'absolute', left: cursorX - 1, top: -6,
-          width: 2, height: 110, background: INK, zIndex: 5}} />
-        <div style={{position:'absolute', left: cursorX - 20, top: -20,
-          fontFamily: mono, fontSize: 10, color: INK,
-          letterSpacing:'0.1em'}}>
-          {localTime.toFixed(2)}s
         </div>
       </div>
-    </div>
-  );
-}
-
-// ── Scene 5: Outro (20 – 22s) ─────────────────────────
-function Scene5_Outro() {
-  const { elapsed } = useSprite();
-  const fadeIn = interpolate(elapsed, [0, 0.6], [0, 1], Easing.easeOut);
-  const lineW = interpolate(elapsed, [0.5, 1.3], [0, 620]);
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeIn,
-      display:'flex', alignItems:'center', justifyContent:'center',
-      flexDirection:'column'}}>
-      <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
-        color: TERRA, marginBottom: 20}}>
-        导出 · MP4 / GIF / 60FPS / BGM
-      </div>
-      <div style={{fontFamily: serif, fontSize: 108, fontWeight: 500,
-        color: INK, lineHeight: 1, letterSpacing:'-0.015em'}}>
-        从 <span style={{fontStyle:'italic', color: TERRA}}>时间</span>,到成片
-      </div>
-      <div style={{height: 1, background: INK, width: lineW, marginTop: 36}} />
-      <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 22, color: ASH,
-        marginTop: 26, maxWidth: 800, textAlign:'center', lineHeight: 1.55}}>
-        render-video.js · convert-formats.sh · add-music.sh<br/>
-        一条命令跑完,产出社交媒体可直接用的素材
-      </div>
-    </div>
-  );
-}
 
-// ── Watermark ──────────────────────────────────────────
-function Watermark() {
-  return (
-    <div style={{position:'absolute', bottom: 24, right: 32,
-      fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
-      fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
-      Created by Huashu-Design
     </div>
-  );
-}
+  </div>
 
-function App() {
-  return (
-    <Stage duration={22} width={1920} height={1080} bgColor={CREAM}>
-      <Sprite start={0} end={3}><Scene1_Title /></Sprite>
-      <Sprite start={3} end={8}><Scene2_Easing /></Sprite>
-      <Sprite start={8} end={14}><Scene3_Interpolate /></Sprite>
-      <Sprite start={14} end={20}><Scene4_Sprite /></Sprite>
-      <Sprite start={20} end={22}><Scene5_Outro /></Sprite>
-      <Watermark />
-    </Stage>
-  );
-}
+  <!-- ============ Scene 2: Brand reveal (米色面板, 8.0 → 10s) ============ -->
+  <div class="scene scene-brand" id="scene-brand">
+    <div class="brand-panel" id="brandPanel"></div>
+    <div class="brand-wordmark" id="wordmark">huashu<span class="accent">-</span>design</div>
+    <div class="brand-line" id="brandLine"></div>
+  </div>
+
+  <!-- Bottom-right watermark -->
+  <div class="watermark-br" id="watermarkBR">V2 · 2026</div>
+
+  <!-- Replay button (hidden during recording) -->
+  <button class="replay-btn no-record" id="replayBtn">REPLAY</button>
+
+</div>
+
+<script>
+(function() {
+  // =============== Timing ===============
+  const T = {
+    DURATION: 10.0,
+
+    // Scene 0: intro
+    intro_in:  [0.0, 0.5],
+    intro_out: [1.3, 1.6],
+
+    // Scene 1: main (timeline + driven stage)
+    main_in:   [1.5, 1.9],     // fade in
+    // Playhead sweeps from 0% (at t=1.6) to 100% (at t=8.2).
+    // API activations use GLOBAL time. Their capsule position is placed so
+    // that playhead passes under the capsule right when the API peaks.
+    main_t0:   1.6,
+    main_t_end: 8.2,
+    main_out:  [8.0, 8.4],
+
+    // API activations (GLOBAL time)
+    // Each API: [activate_start, peak, deactivate_end]
+    // Capsule x% = (peak - 1.6) / (8.2 - 1.6) * 100
+    useTime:     [2.0, 2.8, 3.6],   // capsule @ ~18%
+    interpolate: [3.6, 4.1, 4.8],   // capsule @ ~38%
+    Easing:      [4.8, 5.4, 6.2],   // capsule @ ~58%
+    useSprite:   [6.2, 6.9, 7.9],   // capsule @ ~80%
+
+    // Scene 2: Brand reveal (米色面板 standard, last 2s of T=10)
+    // [T-2.0 → T-1.7]: main fade 1→0 (already handled by main_out 8.0-8.4)
+    // [T-1.7 → T-1.3]: beige panel translateY 100%→0, expoOut
+    // [T-1.3 → T-0.7]: wordmark weight 100→500 + y:20→0 + opacity 0→1, expoOut
+    // [T-0.7 → T-0.3]: orange line width 0→280px, cubicOut
+    // [T-0.3 → T]: hold
+    brand_panel:  [8.3, 8.7],
+    brand_word:   [8.7, 9.3],
+    brand_line:   [9.3, 9.7],
+  };
+
+  // =============== Easings ===============
+  const expoOut = t => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t));
+  const expoIn  = t => (t <= 0 ? 0 : Math.pow(2, 10 * (t - 1)));
+  const cubicOut = t => 1 - Math.pow(1 - t, 3);
+  const cubicIn  = t => t * t * t;
+  const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
+  const easeInOut  = cubicInOut;
+  const linear = t => t;
 
-ReactDOM.createRoot(document.getElementById('root')).render(<App />);
+  // =============== Utils ===============
+  const clamp = (v, lo = 0, hi = 1) => Math.max(lo, Math.min(hi, v));
+  const clampLerp = (t, t0, t1) => clamp((t - t0) / (t1 - t0));
+  function lerp(t, t0, t1, v0, v1, easing = linear) {
+    const p = clampLerp(t, t0, t1);
+    return v0 + (v1 - v0) * easing(p);
+  }
+
+  // =============== DOM refs ===============
+  const scenes = {
+    intro: document.getElementById('scene-intro'),
+    main: document.getElementById('scene-main'),
+    brand: document.getElementById('scene-brand'),
+  };
+  const introTitle = document.getElementById('introTitle');
+  const introSub = document.getElementById('introSub');
+
+  const timelineFill = document.getElementById('timelineFill');
+  const playhead = document.getElementById('playhead');
+
+  const capTime = document.getElementById('cap-time');
+  const capInterp = document.getElementById('cap-interp');
+  const capEasing = document.getElementById('cap-easing');
+  const capSprite = document.getElementById('cap-sprite');
+
+  const stemTime = document.getElementById('stem-time');
+  const stemInterp = document.getElementById('stem-interp');
+  const stemEasing = document.getElementById('stem-easing');
+  const stemSprite = document.getElementById('stem-sprite');
+
+  const vizTime = document.getElementById('viz-time');
+  const vizInterp = document.getElementById('viz-interp');
+  const vizEasing = document.getElementById('viz-easing');
+  const vizSprite = document.getElementById('viz-sprite');
+
+  const handS = document.getElementById('handS');
+  const handM = document.getElementById('handM');
+  const handH = document.getElementById('handH');
+  const timeVal = document.getElementById('timeVal');
+
+  const morphTo = document.getElementById('morphTo');
+  const interpVal = document.getElementById('interpVal');
+
+  const pathLinear = document.getElementById('pathLinear');
+  const pathCubic = document.getElementById('pathCubic');
+  const pathExpo = document.getElementById('pathExpo');
+
+  const spriteGrid = document.getElementById('spriteGrid');
+  const wordmark = document.getElementById('wordmark');
+  const brandLine = document.getElementById('brandLine');
+  const brandPanel = document.getElementById('brandPanel');
+  const watermarkBR = document.getElementById('watermarkBR');
+  const replayBtn = document.getElementById('replayBtn');
+
+  // Build 24 sprites (6x4 grid)
+  const SPRITE_COLS = 6, SPRITE_ROWS = 4;
+  const spriteEls = [];
+  for (let r = 0; r < SPRITE_ROWS; r++) {
+    for (let c = 0; c < SPRITE_COLS; c++) {
+      const el = document.createElement('div');
+      el.className = 'sprite';
+      // center distance for ripple
+      const dc = c - (SPRITE_COLS - 1) / 2;
+      const dr = r - (SPRITE_ROWS - 1) / 2;
+      const dist = Math.sqrt(dc * dc + dr * dr);
+      const maxDist = Math.sqrt(((SPRITE_COLS - 1) / 2) ** 2 + ((SPRITE_ROWS - 1) / 2) ** 2);
+      el.dataset.delay = (dist / maxDist).toFixed(3);
+      spriteGrid.appendChild(el);
+      spriteEls.push(el);
+    }
+  }
+
+  // =============== Scene helpers ===============
+  function showScene(el, opacity) {
+    if (opacity > 0.001) el.classList.add('visible');
+    else el.classList.remove('visible');
+    el.style.opacity = opacity;
+  }
+
+  // =============== API activation logic ===============
+  function apiState(t_local, api) {
+    // Returns { on: bool, strength: 0-1 }
+    const [a, peak, b] = T[api];
+    if (t_local < a || t_local > b) return { on: false, strength: 0 };
+    if (t_local < peak) {
+      return { on: true, strength: expoOut(clampLerp(t_local, a, peak)) };
+    } else {
+      return { on: true, strength: 1 - cubicIn(clampLerp(t_local, peak, b)) };
+    }
+  }
+
+  // =============== Draw easing curves progressively ===============
+  function easingPath(easingFn, progress) {
+    // progress 0-1 draws the curve from left to right
+    // x range: 60 → 680, y range: 260 (0) → 40 (1)
+    const X0 = 60, X1 = 680, Y0 = 260, Y1 = 40;
+    const steps = Math.max(2, Math.floor(progress * 80));
+    let d = `M ${X0} ${Y0}`;
+    for (let i = 1; i <= steps; i++) {
+      const t = (i / 80) * progress;
+      const x = X0 + (X1 - X0) * t;
+      const y = Y0 + (Y1 - Y0) * easingFn(t);
+      d += ` L ${x.toFixed(2)} ${y.toFixed(2)}`;
+    }
+    return d;
+  }
+
+  // =============== Render ===============
+  function render(t) {
+    // ============ Scene 0: Intro ============
+    if (t < T.main_in[1]) {
+      let op = 0;
+      if (t < T.intro_in[1]) op = clampLerp(t, T.intro_in[0], T.intro_in[1]);
+      else if (t < T.intro_out[0]) op = 1;
+      else op = 1 - clampLerp(t, T.intro_out[0], T.intro_out[1]);
+      showScene(scenes.intro, op);
+
+      // weight morph + rise
+      const morphP = expoOut(clampLerp(t, T.intro_in[0], T.intro_in[1] + 0.3));
+      const w = 150 + (400 - 150) * morphP;
+      introTitle.style.fontWeight = Math.round(w);
+      const rise = lerp(t, T.intro_in[0], T.intro_in[1], 16, 0, expoOut);
+      introTitle.style.transform = `translate3d(0, ${rise}px, 0)`;
+      introSub.style.opacity = clampLerp(t, T.intro_in[1], T.intro_in[1] + 0.4);
+    } else {
+      showScene(scenes.intro, 0);
+    }
+
+    // ============ Scene 1: Main (split view) ============
+    if (t >= T.main_in[0] - 0.1 && t < T.main_out[1]) {
+      let op;
+      if (t < T.main_in[1]) op = clampLerp(t, T.main_in[0], T.main_in[1]);
+      else if (t < T.main_out[0]) op = 1;
+      else op = 1 - clampLerp(t, T.main_out[0], T.main_out[1]);
+      showScene(scenes.main, op);
+
+      // Playhead sweeps 0% → 100% across the window [main_t0, main_t_end]
+      const phP = clampLerp(t, T.main_t0, T.main_t_end);
+      const phPct = phP * 100;
+      playhead.style.left = phPct + '%';
+      // Keep: use t directly for API state
+      const t_local_clamped = t;
+
+      // Timeline fill
+      timelineFill.style.width = phPct + '%';
+
+      // API capsules: lit state driven by apiState
+      const stTime = apiState(t_local_clamped, 'useTime');
+      const stInterp = apiState(t_local_clamped, 'interpolate');
+      const stEasing = apiState(t_local_clamped, 'Easing');
+      const stSprite = apiState(t_local_clamped, 'useSprite');
+
+      setLit(capTime, stemTime, stTime);
+      setLit(capInterp, stemInterp, stInterp);
+      setLit(capEasing, stemEasing, stEasing);
+      setLit(capSprite, stemSprite, stSprite);
+
+      // Viz opacities — each viz only visible during its API's window
+      vizTime.style.opacity = stTime.on ? stTime.strength : 0;
+      vizInterp.style.opacity = stInterp.on ? stInterp.strength : 0;
+      vizEasing.style.opacity = stEasing.on ? stEasing.strength : 0;
+      vizSprite.style.opacity = stSprite.on ? stSprite.strength : 0;
+
+      // ========= viz 1: clock =========
+      // Continuous rotation (not just when active) so transition looks natural
+      // But only animate hands when api is near-active, to avoid wasted cpu
+      {
+        const [a, _peak, b] = T.useTime;
+        // Second hand: one revolution over the active window
+        const localP = clampLerp(t_local_clamped, a, b);
+        // Multi-revolution: 1.5 turns over the window
+        const sDeg = localP * 540;
+        const mDeg = localP * 180 + 120;
+        const hDeg = localP * 60 + 30;
+        handS.style.transform = `translateX(-50%) rotate(${sDeg}deg)`;
+        handM.style.transform = `translateX(-50%) rotate(${mDeg}deg)`;
+        handH.style.transform = `translateX(-50%) rotate(${hDeg}deg)`;
+
+        // Display value as t in seconds mapping 0→1.50
+        const displayVal = (localP * 1.5).toFixed(2);
+        timeVal.textContent = displayVal + 's';
+      }
+
+      // ========= viz 2: interpolate =========
+      {
+        const [a, _peak, b] = T.interpolate;
+        const localP = clampLerp(t_local_clamped, a, b);
+        const eased = easeInOut(localP);
+        // morph from 80×80 black → 220×160 orange, rounded
+        const W = 80 + (240 - 80) * eased;
+        const H = 80 + (160 - 80) * eased;
+        const bright = Math.round(30 + (217 - 30) * eased);
+        const brightG = Math.round(30 + (119 - 30) * eased);
+        const brightB = Math.round(30 + (87 - 30) * eased);
+        const rad = 2 + (20 - 2) * eased;
+        morphTo.style.width = W + 'px';
+        morphTo.style.height = H + 'px';
+        morphTo.style.background = `rgb(${bright}, ${brightG}, ${brightB})`;
+        morphTo.style.borderRadius = rad + 'px';
+        interpVal.textContent = eased.toFixed(2);
+      }
+
+      // ========= viz 3: easing curves =========
+      {
+        const [a, _peak, b] = T.Easing;
+        const localP = clampLerp(t_local_clamped, a, b);
+        pathLinear.setAttribute('d', easingPath(linear, localP));
+        pathCubic.setAttribute('d', easingPath(cubicOut, localP));
+        pathExpo.setAttribute('d', easingPath(expoOut, localP));
+      }
+
+      // ========= viz 4: sprites =========
+      {
+        const [a, _peak, b] = T.useSprite;
+        const localP = clampLerp(t_local_clamped, a, b);
+        for (const el of spriteEls) {
+          const delay = parseFloat(el.dataset.delay);
+          const spriteLocalT = clamp((localP - delay * 0.5) / 0.5, 0, 1);
+          const op = expoOut(spriteLocalT);
+          el.style.opacity = op;
+          const scale = 0.5 + 0.5 * op;
+          const y = (1 - op) * 14;
+          el.style.transform = `translateY(${y}px) scale(${scale})`;
+          el.style.background = op > 0.85 ? 'var(--accent)' : 'var(--hairline)';
+        }
+      }
+    } else {
+      showScene(scenes.main, 0);
+    }
+
+    // ============ Scene 2: Brand reveal (米色面板标准动作) ============
+    if (t >= T.brand_panel[0] - 0.1) {
+      showScene(scenes.brand, 1);
+
+      // [T-1.7 → T-1.3]: beige panel slides up, expoOut
+      const panelP = expoOut(clampLerp(t, T.brand_panel[0], T.brand_panel[1]));
+      brandPanel.style.transform = `translateY(${(1 - panelP) * 100}%)`;
+
+      // [T-1.3 → T-0.7]: wordmark weight 100→500 + y:20→0 + opacity:0→1, expoOut
+      const wordP = expoOut(clampLerp(t, T.brand_word[0], T.brand_word[1]));
+      const w = 100 + (500 - 100) * wordP;
+      wordmark.style.fontVariationSettings = `"wght" ${w.toFixed(0)}`;
+      wordmark.style.fontWeight = Math.round(w);
+      wordmark.style.opacity = wordP;
+      const wRise = (1 - wordP) * 20;
+      wordmark.style.transform = `translate(-50%, calc(-50% + ${wRise}px))`;
+
+      // [T-0.7 → T-0.3]: orange line expands 0→280px, cubicOut
+      const lineP = cubicOut(clampLerp(t, T.brand_line[0], T.brand_line[1]));
+      brandLine.style.width = (lineP * 280) + 'px';
+    } else {
+      showScene(scenes.brand, 0);
+      brandPanel.style.transform = 'translateY(100%)';
+      wordmark.style.opacity = 0;
+      brandLine.style.width = '0px';
+    }
+
+    // Watermark visible from start of main until end
+    if (t >= T.main_in[0] && t < T.DURATION - 0.15) {
+      watermarkBR.classList.add('visible');
+    } else {
+      watermarkBR.classList.remove('visible');
+    }
+  }
+
+  function setLit(capsule, stem, state) {
+    if (state.on && state.strength > 0.15) {
+      capsule.classList.add('lit');
+      stem.classList.add('lit');
+      // Subtle scale pulse centered on peak (simplistic)
+      const scale = 1.0 + state.strength * 0.06;
+      capsule.style.transform = `translateX(-50%) scale(${scale})`;
+    } else {
+      capsule.classList.remove('lit');
+      stem.classList.remove('lit');
+      capsule.style.transform = 'translateX(-50%)';
+    }
+  }
+
+  // =============== Driver ===============
+  let manualT = null;
+  let startMs = null;
+  let hasFinishedOnce = false;
+
+  function tick(now) {
+    if (manualT != null) {
+      render(manualT);
+    } else {
+      if (startMs == null) startMs = now;
+      const elapsed = (now - startMs) / 1000;
+      const recording = window.__recording === true;
+      let t;
+      if (recording) {
+        t = Math.min(elapsed, T.DURATION - 0.001);
+        if (elapsed >= T.DURATION && !hasFinishedOnce) hasFinishedOnce = true;
+      } else {
+        t = elapsed % T.DURATION;
+        // Show replay button when we've played at least once
+        if (elapsed >= T.DURATION) {
+          replayBtn.classList.add('visible');
+        }
+      }
+      render(t);
+    }
+    requestAnimationFrame(tick);
+  }
+
+  // First paint signal for renderer
+  document.fonts.ready.then(() => {
+    render(0);
+    requestAnimationFrame(() => {
+      window.__ready = true;
+      requestAnimationFrame(tick);
+    });
+  });
+
+  // ========= Stage scaling (fit viewport) =========
+  function fitStage() {
+    const stage = document.getElementById('stage');
+    const scaleX = window.innerWidth / 1920;
+    const scaleY = window.innerHeight / 1080;
+    const scale = Math.min(scaleX, scaleY);
+    stage.style.transform = `translate(-50%, -50%) scale(${scale})`;
+  }
+  fitStage();
+  window.addEventListener('resize', fitStage);
+
+  // Replay
+  replayBtn.addEventListener('click', () => {
+    startMs = null;
+    replayBtn.classList.remove('visible');
+  });
+
+  // =============== Expose for frame-accurate rendering ===============
+  window.__setTime = (t) => { manualT = t; render(t); };
+  window.__resume = () => { manualT = null; startMs = null; };
+  window.__duration = T.DURATION;
+  window.__render = render;
+})();
 </script>
 </body>
 </html>

+ 989 - 0
demos/c4-tweaks-en.html

@@ -0,0 +1,989 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8" />
+<title>c4-tweaks · Slide. See it morph. (English)</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600;700&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<style>
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;
+    --accent-deep: #B85D3D;
+
+    /* Mock landing page · warm variant (initial state) */
+    --warm-bg: #F6EFE6;
+    --warm-panel: #FFFFFF;
+    --warm-ink: #1A1918;
+    --warm-dim: #8B867E;
+    --warm-hair: rgba(0,0,0,0.08);
+    --warm-accent: #D97757;
+
+    /* Mock landing page · cool variant (after slider 1) */
+    --cool-bg: #0E1620;
+    --cool-panel: #17222E;
+    --cool-ink: #E8EEF5;
+    --cool-dim: #7A8A9B;
+    --cool-hair: rgba(255,255,255,0.08);
+    --cool-accent: #5A8CB8;
+
+    --serif-en: "Source Serif 4", Georgia, serif;
+    --serif-cn: "Noto Serif SC", "Source Serif 4", Georgia, serif;
+    --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
+    -webkit-font-smoothing: antialiased;
+  }
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform: translate(-50%, -50%);
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+  }
+
+  /* Film grain */
+  .grain {
+    position: absolute; inset: 0;
+    background-image:
+      radial-gradient(rgba(255,255,255,0.02) 1px, transparent 1px);
+    background-size: 3px 3px;
+    opacity: 0.4;
+    pointer-events: none;
+    z-index: 2;
+  }
+
+  /* Watermark */
+  .watermark {
+    position: absolute;
+    top: 44px; left: 56px;
+    font-family: var(--mono);
+    font-size: 14px;
+    font-weight: 500;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,0.16);
+    z-index: 10;
+  }
+
+  .version-mark {
+    position: absolute;
+    bottom: 44px; right: 56px;
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,0.12);
+    z-index: 10;
+  }
+
+  /* ============ Main composition ============ */
+  .composition {
+    position: absolute;
+    inset: 0;
+    display: grid;
+    grid-template-columns: 1080px 500px;
+    gap: 80px;
+    padding: 130px 120px 140px 140px;
+    align-items: center;
+    perspective: 2400px;
+  }
+
+  /* ---- Design preview (left) ---- */
+  .preview-frame {
+    position: relative;
+    width: 1080px;
+    height: 800px;
+    border-radius: 18px;
+    overflow: hidden;
+    transform-style: preserve-3d;
+    transform: rotateX(6deg) rotateY(-4deg);
+    box-shadow:
+      0 50px 120px rgba(0,0,0,0.6),
+      0 0 0 1px rgba(255,255,255,0.06);
+    opacity: 0;
+    will-change: opacity, transform, background;
+    transition: background 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .preview-frame.warm {
+    background: var(--warm-bg);
+  }
+  .preview-frame.cool {
+    background: var(--cool-bg);
+  }
+
+  /* Browser chrome top bar */
+  .browser-chrome {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    padding: 16px 22px;
+    border-bottom: 1px solid var(--warm-hair);
+    background: var(--warm-panel);
+    transition: all 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .browser-chrome {
+    background: var(--cool-panel);
+    border-bottom-color: var(--cool-hair);
+  }
+  .dot {
+    width: 11px; height: 11px; border-radius: 50%;
+    background: rgba(0,0,0,0.14);
+  }
+  .cool .dot { background: rgba(255,255,255,0.14); }
+  .url-bar {
+    flex: 1;
+    margin-left: 14px;
+    padding: 6px 14px;
+    border-radius: 6px;
+    background: rgba(0,0,0,0.04);
+    font-family: var(--mono);
+    font-size: 12px;
+    color: var(--warm-dim);
+    letter-spacing: 0.05em;
+    transition: all 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .url-bar {
+    background: rgba(255,255,255,0.04);
+    color: var(--cool-dim);
+  }
+
+  /* Hero content */
+  .preview-body {
+    padding: 54px 72px 60px 72px;
+    color: var(--warm-ink);
+    transition: color 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .preview-body { color: var(--cool-ink); }
+
+  .preview-eyebrow {
+    font-family: var(--mono);
+    font-size: 11px;
+    font-weight: 500;
+    letter-spacing: 0.24em;
+    text-transform: uppercase;
+    color: var(--warm-accent);
+    transition: color 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .preview-eyebrow { color: var(--cool-accent); }
+
+  .preview-title {
+    margin-top: 16px;
+    font-family: var(--serif-en);
+    font-weight: 400;
+    font-size: 86px;
+    line-height: 1.02;
+    letter-spacing: -0.02em;
+    transition: font-family 240ms cubic-bezier(.2,.8,.2,1),
+                font-weight 240ms cubic-bezier(.2,.8,.2,1),
+                letter-spacing 240ms cubic-bezier(.2,.8,.2,1);
+  }
+  .preview-title .em {
+    color: var(--warm-accent);
+    font-style: italic;
+    transition: color 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .preview-title .em { color: var(--cool-accent); }
+
+  .preview-frame.sans .preview-title {
+    font-family: var(--sans);
+    font-weight: 200;
+    letter-spacing: -0.045em;
+  }
+  .preview-frame.sans .preview-title .em {
+    font-style: normal;
+  }
+
+  .preview-sub {
+    margin-top: 24px;
+    font-family: var(--serif-en);
+    font-size: 20px;
+    font-weight: 300;
+    line-height: 1.6;
+    max-width: 720px;
+    color: var(--warm-dim);
+    transition: color 280ms cubic-bezier(.2,.8,.2,1),
+                font-family 240ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .preview-sub { color: var(--cool-dim); }
+  .preview-frame.sans .preview-sub {
+    font-family: var(--sans);
+  }
+
+  /* Density cards grid */
+  .card-grid {
+    margin-top: 54px;
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    gap: 18px;
+    transition: grid-template-columns 280ms cubic-bezier(.2,.8,.2,1),
+                gap 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .preview-frame.dense .card-grid {
+    grid-template-columns: repeat(3, 1fr);
+    grid-auto-rows: minmax(72px, auto);
+    gap: 10px;
+  }
+
+  .card {
+    padding: 22px 22px 24px 22px;
+    border-radius: 10px;
+    background: rgba(0,0,0,0.035);
+    border: 1px solid var(--warm-hair);
+    transition: all 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .card {
+    background: rgba(255,255,255,0.03);
+    border-color: var(--cool-hair);
+  }
+  .preview-frame.dense .card {
+    padding: 12px 14px;
+  }
+
+  .card-icon {
+    width: 28px; height: 28px;
+    border-radius: 6px;
+    background: var(--warm-accent);
+    opacity: 0.16;
+    margin-bottom: 14px;
+    transition: all 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .card-icon { background: var(--cool-accent); }
+  .preview-frame.dense .card-icon {
+    width: 18px; height: 18px;
+    margin-bottom: 8px;
+  }
+
+  .card-title {
+    font-family: var(--serif-en);
+    font-size: 18px;
+    font-weight: 500;
+    color: var(--warm-ink);
+    letter-spacing: -0.005em;
+    transition: color 280ms cubic-bezier(.2,.8,.2,1),
+                font-family 240ms cubic-bezier(.2,.8,.2,1),
+                font-size 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .card-title { color: var(--cool-ink); }
+  .preview-frame.sans .card-title {
+    font-family: var(--sans);
+    font-weight: 500;
+  }
+  .preview-frame.dense .card-title {
+    font-size: 13px;
+  }
+
+  .card-text {
+    margin-top: 6px;
+    font-family: var(--serif-en);
+    font-size: 13px;
+    line-height: 1.45;
+    color: var(--warm-dim);
+    transition: all 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .card-text { color: var(--cool-dim); }
+  .preview-frame.sans .card-text { font-family: var(--sans); }
+  .preview-frame.dense .card-text {
+    font-size: 11px;
+    line-height: 1.3;
+    opacity: 0.85;
+  }
+
+  /* Extra cards (hidden in sparse mode) */
+  .card.extra {
+    opacity: 0;
+    transform: scale(0.92);
+    transition: opacity 240ms cubic-bezier(.2,.8,.2,1),
+                transform 240ms cubic-bezier(.2,.8,.2,1),
+                background 280ms cubic-bezier(.2,.8,.2,1),
+                border-color 280ms cubic-bezier(.2,.8,.2,1);
+    pointer-events: none;
+    max-height: 0;
+    padding: 0;
+    overflow: hidden;
+  }
+  .preview-frame.dense .card.extra {
+    opacity: 1;
+    transform: scale(1);
+    max-height: 120px;
+    padding: 12px 14px;
+  }
+
+  /* ---- Slider panel (right) ---- */
+  .slider-panel {
+    position: relative;
+    width: 500px;
+    opacity: 0;
+    will-change: opacity, transform;
+    display: flex;
+    flex-direction: column;
+    gap: 64px;
+  }
+
+  .anchor-line {
+    position: absolute;
+    top: -80px;
+    left: 8px;
+    font-family: var(--serif-en);
+    font-weight: 400;
+    font-size: 26px;
+    letter-spacing: 0.02em;
+    color: var(--ink-80);
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+  .anchor-line .em {
+    color: var(--accent);
+    font-weight: 500;
+  }
+
+  .slider-item {
+    display: flex;
+    flex-direction: column;
+    gap: 18px;
+  }
+
+  .slider-label {
+    display: flex;
+    align-items: baseline;
+    justify-content: space-between;
+  }
+  .slider-name {
+    font-family: var(--mono);
+    font-size: 14px;
+    font-weight: 500;
+    letter-spacing: 0.18em;
+    color: var(--ink-80);
+    text-transform: uppercase;
+  }
+  .slider-value {
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.14em;
+    color: var(--muted);
+  }
+
+  /* Track */
+  .track {
+    position: relative;
+    width: 100%;
+    height: 2px;
+    background: var(--hairline);
+  }
+  .track-fill {
+    position: absolute;
+    top: 0; left: 0;
+    height: 100%;
+    width: 10%;
+    background: var(--accent);
+    will-change: width;
+  }
+
+  /* Tick marks */
+  .ticks {
+    position: absolute;
+    inset: -4px 0 -4px 0;
+    display: flex;
+    justify-content: space-between;
+    pointer-events: none;
+  }
+  .tick {
+    width: 1px;
+    height: 10px;
+    background: rgba(255,255,255,0.14);
+  }
+
+  /* Knob */
+  .knob {
+    position: absolute;
+    top: 50%;
+    left: 10%;
+    width: 26px; height: 26px;
+    border-radius: 50%;
+    background: var(--ink);
+    transform: translate(-50%, -50%);
+    box-shadow: 0 0 0 1px rgba(0,0,0,0.6),
+                0 8px 24px rgba(0,0,0,0.5);
+    will-change: left, transform, box-shadow;
+  }
+  .knob.active {
+    box-shadow: 0 0 0 2px var(--accent),
+                0 0 30px rgba(217,119,87,0.45),
+                0 8px 24px rgba(0,0,0,0.5);
+  }
+
+  /* Cursor */
+  .cursor {
+    position: absolute;
+    width: 20px; height: 20px;
+    pointer-events: none;
+    will-change: left, top, opacity;
+    opacity: 0;
+    z-index: 20;
+  }
+  .cursor svg { width: 100%; height: 100%; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.8)); }
+
+  /* ---- Brand reveal ---- */
+  /* Stage dimmer: fades the composition out just before the panel slides in */
+  .stage-dimmer {
+    position: absolute;
+    inset: 0;
+    background: #000000;
+    opacity: 0;
+    z-index: 40;
+    pointer-events: none;
+    will-change: opacity;
+  }
+  .brand-panel {
+    position: absolute;
+    inset: 0;
+    background: #F5F4F0;
+    transform: translateY(100%);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    z-index: 50;
+    will-change: transform;
+  }
+  .brand-wordmark {
+    font-family: var(--serif-en);
+    font-size: 72px;
+    font-weight: 100;
+    font-variation-settings: "wght" 100;
+    letter-spacing: -0.02em;
+    color: #1A1918;
+    text-align: center;
+    line-height: 1;
+    opacity: 0;
+    transform: translateY(20px);
+    will-change: opacity, transform, font-variation-settings, font-weight;
+  }
+  .brand-wordmark .accent { color: #D97757; font-weight: inherit; }
+
+  .brand-line {
+    /* Flex-centered, 60px below wordmark (line-height 1 @ 72px → descender + 24 gap) */
+    margin-top: 60px;
+    height: 2px;
+    width: 0;
+    background: #D97757;
+    align-self: center;
+    will-change: width;
+  }
+
+</style>
+</head>
+<body>
+
+<div class="stage" id="stage">
+
+  <div class="grain"></div>
+  <div class="watermark">HUASHU · DESIGN</div>
+  <div class="version-mark">V2 · 2026</div>
+
+  <div class="composition">
+
+    <!-- LEFT: design preview -->
+    <div class="preview-frame warm" id="preview">
+      <div class="browser-chrome">
+        <span class="dot"></span><span class="dot"></span><span class="dot"></span>
+        <div class="url-bar">yourbrand.design</div>
+      </div>
+      <div class="preview-body">
+        <div class="preview-eyebrow">Agent Studio</div>
+        <div class="preview-title">Built for <span class="em">them</span>.<br/>Who never sleep.</div>
+        <div class="preview-sub">A design system that ships while you rest — ready before you open the file.</div>
+
+        <div class="card-grid" id="cardGrid">
+          <div class="card">
+            <div class="card-icon"></div>
+            <div class="card-title">Brand Assets</div>
+            <div class="card-text">Logos, palettes, type — one source of truth.</div>
+          </div>
+          <div class="card">
+            <div class="card-icon"></div>
+            <div class="card-title">Prototype</div>
+            <div class="card-text">One sentence in, a clickable app out.</div>
+          </div>
+          <div class="card">
+            <div class="card-icon"></div>
+            <div class="card-title">Motion</div>
+            <div class="card-text">Timeline is code. Swap 25 for 60 fps.</div>
+          </div>
+          <div class="card extra">
+            <div class="card-icon"></div>
+            <div class="card-title">Slides</div>
+            <div class="card-text">HTML is PPTX.</div>
+          </div>
+          <div class="card extra">
+            <div class="card-icon"></div>
+            <div class="card-title">Infographic</div>
+            <div class="card-text">Data in, magazine out.</div>
+          </div>
+          <div class="card extra">
+            <div class="card-icon"></div>
+            <div class="card-title">Review</div>
+            <div class="card-text">Five axes. Honest punch list.</div>
+          </div>
+          <div class="card extra">
+            <div class="card-icon"></div>
+            <div class="card-title">Advisor</div>
+            <div class="card-text">Three roads. You pick.</div>
+          </div>
+          <div class="card extra">
+            <div class="card-icon"></div>
+            <div class="card-title">Junior</div>
+            <div class="card-text">Show first. Polish later.</div>
+          </div>
+          <div class="card extra">
+            <div class="card-icon"></div>
+            <div class="card-title">Protocol</div>
+            <div class="card-text">Five steps. No skip.</div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- RIGHT: slider panel -->
+    <div class="slider-panel" id="panel">
+
+      <div class="anchor-line" id="anchor">
+        Slide. <span class="em">See it morph.</span>
+      </div>
+
+      <!-- Slider 1 · palette -->
+      <div class="slider-item">
+        <div class="slider-label">
+          <span class="slider-name">Palette</span>
+          <span class="slider-value" id="val1">warm</span>
+        </div>
+        <div class="track">
+          <div class="ticks">
+            <span class="tick"></span><span class="tick"></span><span class="tick"></span>
+            <span class="tick"></span><span class="tick"></span>
+          </div>
+          <div class="track-fill" id="fill1"></div>
+          <div class="knob" id="knob1"></div>
+        </div>
+      </div>
+
+      <!-- Slider 2 · type -->
+      <div class="slider-item">
+        <div class="slider-label">
+          <span class="slider-name">Type</span>
+          <span class="slider-value" id="val2">serif</span>
+        </div>
+        <div class="track">
+          <div class="ticks">
+            <span class="tick"></span><span class="tick"></span><span class="tick"></span>
+            <span class="tick"></span><span class="tick"></span>
+          </div>
+          <div class="track-fill" id="fill2"></div>
+          <div class="knob" id="knob2"></div>
+        </div>
+      </div>
+
+      <!-- Slider 3 · density -->
+      <div class="slider-item">
+        <div class="slider-label">
+          <span class="slider-name">Density</span>
+          <span class="slider-value" id="val3">sparse</span>
+        </div>
+        <div class="track">
+          <div class="ticks">
+            <span class="tick"></span><span class="tick"></span><span class="tick"></span>
+            <span class="tick"></span><span class="tick"></span>
+          </div>
+          <div class="track-fill" id="fill3"></div>
+          <div class="knob" id="knob3"></div>
+        </div>
+      </div>
+
+    </div>
+
+    <!-- Cursor -->
+    <div class="cursor" id="cursor">
+      <svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
+        <path d="M2 2 L2 16 L6 12 L9 18 L11 17 L8 11 L14 11 Z"
+              fill="white" stroke="#000" stroke-width="1.2" stroke-linejoin="round"/>
+      </svg>
+    </div>
+  </div>
+
+  <!-- Stage dimmer (fades scene to black before panel sweeps in) -->
+  <div class="stage-dimmer" id="stageDimmer"></div>
+
+  <!-- Brand reveal layer -->
+  <div class="brand-panel" id="brandPanel">
+    <div class="brand-wordmark" id="brandMark">huashu<span class="accent">-</span>design</div>
+    <div class="brand-line" id="brandLine"></div>
+  </div>
+
+</div>
+
+<script>
+(function() {
+  // ---------- Fit stage ----------
+  const stage = document.getElementById('stage');
+  function rescale() {
+    const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  rescale();
+  window.addEventListener('resize', rescale);
+
+  // ---------- Animation ----------
+  const DURATION = 10.0; // seconds
+
+  const preview   = document.getElementById('preview');
+  const panel     = document.getElementById('panel');
+  const anchor    = document.getElementById('anchor');
+  const cursor    = document.getElementById('cursor');
+
+  const knob1 = document.getElementById('knob1');
+  const knob2 = document.getElementById('knob2');
+  const knob3 = document.getElementById('knob3');
+  const fill1 = document.getElementById('fill1');
+  const fill2 = document.getElementById('fill2');
+  const fill3 = document.getElementById('fill3');
+  const val1  = document.getElementById('val1');
+  const val2  = document.getElementById('val2');
+  const val3  = document.getElementById('val3');
+
+  const stageDimmer = document.getElementById('stageDimmer');
+  const brandPanel = document.getElementById('brandPanel');
+  const brandMark  = document.getElementById('brandMark');
+  const brandLine  = document.getElementById('brandLine');
+
+  // Easings
+  const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
+  const expoIn  = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
+  const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2;
+  const cubicOut = t => 1 - Math.pow(1 - t, 3);
+
+  function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
+  function lerp(t, t0, t1, v0, v1, ease) {
+    if (t <= t0) return v0;
+    if (t >= t1) return v1;
+    const k = (t - t0) / (t1 - t0);
+    return v0 + (v1 - v0) * (ease ? ease(k) : k);
+  }
+  function clampLerp(t, t0, t1) {
+    if (t <= t0) return 0;
+    if (t >= t1) return 1;
+    return (t - t0) / (t1 - t0);
+  }
+
+  // Knob motion — drag feel: first 70% is a cubic ease (hand moving),
+  // final 15% is overshoot + snap to target (magnetic arrival).
+  function knobMotion(t, t0, t1, fromPct, toPct) {
+    if (t <= t0) return fromPct;
+    if (t >= t1) return toPct;
+    const k = (t - t0) / (t1 - t0);
+    const direction = toPct > fromPct ? 1 : -1;
+    const range = Math.abs(toPct - fromPct);
+
+    if (k < 0.72) {
+      // Main drag: cubic easeInOut feels like a hand moving
+      const e = cubicInOut(k / 0.72);
+      return fromPct + (toPct - fromPct) * e;
+    } else if (k < 0.85) {
+      // Overshoot past target by ~2%
+      const overK = (k - 0.72) / 0.13;
+      const overshoot = 2.2;
+      return toPct + direction * overshoot * Math.sin(overK * Math.PI);
+    } else {
+      // Settled at target
+      return toPct;
+    }
+  }
+
+  // Timeline (seconds, 10s total)
+  const T = {
+    stage_in:     [0.0, 1.0],       // frame + panel appear
+    anchor_in:    [0.8, 1.4],
+
+    // Slider 1 · palette: warm → cool (1.2s → 3.2s) — arrive at 3.0s
+    s1_cursor_to: [1.3, 1.9],
+    s1_drag:      [1.9, 2.9],
+    s1_settle:    [2.9, 3.1],
+
+    // Slider 2 · type: serif → sans
+    s2_cursor_to: [3.2, 3.7],
+    s2_drag:      [3.7, 4.7],
+    s2_settle:    [4.7, 4.9],
+
+    // Slider 3 · density: sparse → dense
+    s3_cursor_to: [5.0, 5.5],
+    s3_drag:      [5.5, 6.5],
+    s3_settle:    [6.5, 6.7],
+
+    hold:         [6.7, 8.0],
+
+    // Brand reveal (cream walloff · aligned with hero-v10 signature)
+    scene_out:    [8.0, 8.3],   // main composition fade to black (0.3s)
+    brand_panel:  [8.3, 8.7],   // cream panel sweeps up from bottom, expoOut (0.4s)
+    brand_mark:   [8.7, 9.3],   // wordmark: wght 100→500 + y 20→0 + opacity 0→1 (0.6s)
+    brand_line:   [9.3, 9.7],   // orange line expands 0→280 from center (0.4s)
+    brand_hold:   [9.7, 10.0],  // hold final frame
+  };
+
+  // Slider-to-state logic. Value-changes happen at settle start.
+  let state = { palette: 'warm', type: 'serif', density: 'sparse' };
+  let lastStateHash = '';
+  function updatePreview() {
+    preview.classList.remove('warm', 'cool', 'sans', 'dense');
+    if (state.palette === 'warm') preview.classList.add('warm');
+    else preview.classList.add('cool');
+    if (state.type === 'sans') preview.classList.add('sans');
+    if (state.density === 'dense') preview.classList.add('dense');
+  }
+  updatePreview();
+
+  function setKnobState(knob, active) {
+    if (active) knob.classList.add('active');
+    else knob.classList.remove('active');
+  }
+
+  function setValueLabel(el, text) {
+    if (el.textContent !== text) el.textContent = text;
+  }
+
+  // ---------- Cursor path (in composition coords) ----------
+  // Composition uses grid: left column 1220 + 60 gap, panel is at right.
+  // We'll position cursor using .composition-relative absolute positioning.
+  // Cursor is child of .composition, whose padding is 130/100/140/140.
+  // So coords relative to .composition padding-box.
+  // Simpler: cursor is absolute in .stage coords since parent composition
+  // covers full stage. Use inline style left/top in px.
+
+  // Anchor positions (rough — will fine-tune):
+  const CURSOR_PARK   = { x: 1900, y: 1080 }; // off-screen bottom-right
+  // Slider tracks: panel starts around x≈1420, width 520. Each track spans that width.
+  // We'll measure actual rect at first tick.
+  let sliderRects = null;
+  function measureRects() {
+    const stageRect = stage.getBoundingClientRect();
+    const scale = stageRect.width / 1920;
+    const getTrackBox = (id) => {
+      const el = document.getElementById(id).parentElement; // .track
+      const r = el.getBoundingClientRect();
+      return {
+        left: (r.left - stageRect.left) / scale,
+        top:  (r.top  - stageRect.top)  / scale,
+        width:  r.width / scale,
+        height: r.height / scale,
+      };
+    };
+    sliderRects = {
+      s1: getTrackBox('knob1'),
+      s2: getTrackBox('knob2'),
+      s3: getTrackBox('knob3'),
+    };
+  }
+
+  function positionCursor(x, y, opacity) {
+    cursor.style.left = x + 'px';
+    cursor.style.top  = y + 'px';
+    cursor.style.opacity = opacity;
+  }
+
+  function knobLeft(id, pct) {
+    const el = document.getElementById(id);
+    el.style.left = pct + '%';
+  }
+  function fillWidth(id, pct) {
+    const el = document.getElementById(id);
+    el.style.width = pct + '%';
+  }
+
+  // Tick / render
+  let startTs = null;
+  let frameCount = 0;
+
+  function tick(ts) {
+    if (!startTs) startTs = ts;
+    const t = (ts - startTs) / 1000;
+
+    // Measure rects once
+    if (!sliderRects && frameCount > 1) {
+      measureRects();
+    }
+
+    // --- Stage in ---
+    const stageK = clampLerp(t, T.stage_in[0], T.stage_in[1]);
+    const stageOp = cubicOut(stageK);
+    preview.style.opacity = stageOp;
+    preview.style.transform = `rotateX(${lerp(t, T.stage_in[0], T.stage_in[1], 10, 6, cubicOut)}deg) rotateY(-4deg) translateY(${lerp(t, T.stage_in[0], T.stage_in[1], 20, 0, expoOut)}px)`;
+    panel.style.opacity = stageOp;
+    panel.style.transform = `translateX(${lerp(t, T.stage_in[0], T.stage_in[1], 30, 0, expoOut)}px)`;
+
+    // Anchor
+    const aK = clampLerp(t, T.anchor_in[0], T.anchor_in[1]);
+    anchor.style.opacity = cubicOut(aK);
+    anchor.style.transform = `translateY(${lerp(t, T.anchor_in[0], T.anchor_in[1], 10, 0, expoOut)}px)`;
+
+    // Snap point: when knob reaches target (72% of drag duration)
+    const s1SnapT = T.s1_drag[0] + (T.s1_drag[1] - T.s1_drag[0]) * 0.72;
+    const s2SnapT = T.s2_drag[0] + (T.s2_drag[1] - T.s2_drag[0]) * 0.72;
+    const s3SnapT = T.s3_drag[0] + (T.s3_drag[1] - T.s3_drag[0]) * 0.72;
+
+    // --- Slider 1: palette ---
+    // Knob 10% → 90%
+    const k1pct = knobMotion(t, T.s1_drag[0], T.s1_drag[1], 10, 90);
+    knobLeft('knob1', k1pct); fillWidth('fill1', k1pct);
+    setKnobState(knob1, t >= T.s1_cursor_to[0] && t < T.s1_settle[1] + 0.2);
+    if (t >= s1SnapT && state.palette !== 'cool') {
+      state.palette = 'cool'; updatePreview(); setValueLabel(val1, 'cool');
+    }
+
+    // --- Slider 2: type ---
+    const k2pct = knobMotion(t, T.s2_drag[0], T.s2_drag[1], 10, 90);
+    knobLeft('knob2', k2pct); fillWidth('fill2', k2pct);
+    setKnobState(knob2, t >= T.s2_cursor_to[0] && t < T.s2_settle[1] + 0.2);
+    if (t >= s2SnapT && state.type !== 'sans') {
+      state.type = 'sans'; updatePreview(); setValueLabel(val2, 'sans');
+    }
+
+    // --- Slider 3: density ---
+    const k3pct = knobMotion(t, T.s3_drag[0], T.s3_drag[1], 10, 90);
+    knobLeft('knob3', k3pct); fillWidth('fill3', k3pct);
+    setKnobState(knob3, t >= T.s3_cursor_to[0] && t < T.s3_settle[1] + 0.2);
+    if (t >= s3SnapT && state.density !== 'dense') {
+      state.density = 'dense'; updatePreview(); setValueLabel(val3, 'dense');
+    }
+
+    // --- Cursor choreography ---
+    if (sliderRects) {
+      const r1 = sliderRects.s1, r2 = sliderRects.s2, r3 = sliderRects.s3;
+      // Positions of knob at 10% and 90%
+      const k1Start = { x: r1.left + r1.width * 0.10, y: r1.top + r1.height/2 };
+      const k1End   = { x: r1.left + r1.width * 0.90, y: r1.top + r1.height/2 };
+      const k2Start = { x: r2.left + r2.width * 0.10, y: r2.top + r2.height/2 };
+      const k2End   = { x: r2.left + r2.width * 0.90, y: r2.top + r2.height/2 };
+      const k3Start = { x: r3.left + r3.width * 0.10, y: r3.top + r3.height/2 };
+      const k3End   = { x: r3.left + r3.width * 0.90, y: r3.top + r3.height/2 };
+
+      let cx = CURSOR_PARK.x, cy = CURSOR_PARK.y, co = 0;
+
+      if (t < T.s1_cursor_to[0]) {
+        // still off-screen (or just appeared)
+        cx = CURSOR_PARK.x; cy = CURSOR_PARK.y; co = 0;
+      } else if (t < T.s1_cursor_to[1]) {
+        // cursor flies to s1 knob start
+        const k = clampLerp(t, T.s1_cursor_to[0], T.s1_cursor_to[1]);
+        const e = cubicOut(k);
+        cx = lerp(t, T.s1_cursor_to[0], T.s1_cursor_to[1], CURSOR_PARK.x, k1Start.x, cubicOut);
+        cy = lerp(t, T.s1_cursor_to[0], T.s1_cursor_to[1], CURSOR_PARK.y, k1Start.y, cubicOut);
+        co = e;
+      } else if (t < T.s1_drag[1]) {
+        // dragging s1
+        cx = r1.left + (r1.width * k1pct / 100);
+        cy = r1.top + r1.height/2;
+        co = 1;
+      } else if (t < T.s2_cursor_to[0]) {
+        cx = k1End.x; cy = k1End.y; co = 1;
+      } else if (t < T.s2_cursor_to[1]) {
+        cx = lerp(t, T.s2_cursor_to[0], T.s2_cursor_to[1], k1End.x, k2Start.x, cubicInOut);
+        cy = lerp(t, T.s2_cursor_to[0], T.s2_cursor_to[1], k1End.y, k2Start.y, cubicInOut);
+        co = 1;
+      } else if (t < T.s2_drag[1]) {
+        cx = r2.left + (r2.width * k2pct / 100);
+        cy = r2.top + r2.height/2;
+        co = 1;
+      } else if (t < T.s3_cursor_to[0]) {
+        cx = k2End.x; cy = k2End.y; co = 1;
+      } else if (t < T.s3_cursor_to[1]) {
+        cx = lerp(t, T.s3_cursor_to[0], T.s3_cursor_to[1], k2End.x, k3Start.x, cubicInOut);
+        cy = lerp(t, T.s3_cursor_to[0], T.s3_cursor_to[1], k2End.y, k3Start.y, cubicInOut);
+        co = 1;
+      } else if (t < T.s3_drag[1]) {
+        cx = r3.left + (r3.width * k3pct / 100);
+        cy = r3.top + r3.height/2;
+        co = 1;
+      } else if (t < T.hold[1]) {
+        // fade out cursor
+        cx = k3End.x; cy = k3End.y;
+        co = lerp(t, T.s3_drag[1], T.hold[1], 1, 0, cubicOut);
+      }
+
+      positionCursor(cx, cy, co);
+    }
+
+    // --- Brand reveal (cream walloff · aligned with hero-v10 signature) ---
+    // 1) Scene dimmer: composition fades to black (0.3s)
+    const soK = clampLerp(t, T.scene_out[0], T.scene_out[1]);
+    stageDimmer.style.opacity = cubicOut(soK);
+
+    // 2) Cream panel sweeps up from bottom, expoOut (0.4s)
+    const bpK = clampLerp(t, T.brand_panel[0], T.brand_panel[1]);
+    const panelY = lerp(t, T.brand_panel[0], T.brand_panel[1], 100, 0, expoOut);
+    brandPanel.style.transform = `translateY(${panelY}%)`;
+
+    // 3) Wordmark: font-weight 100→500 + y 20→0 + opacity 0→1, expoOut (0.6s)
+    const bmK = clampLerp(t, T.brand_mark[0], T.brand_mark[1]);
+    const bmE = expoOut(bmK);
+    const wght = 100 + (500 - 100) * bmE;
+    brandMark.style.opacity = bmE;
+    brandMark.style.transform = `translateY(${20 * (1 - bmE)}px)`;
+    brandMark.style.fontWeight = Math.round(wght);
+    brandMark.style.fontVariationSettings = `"wght" ${wght.toFixed(0)}`;
+
+    // 4) Orange line: width 0→280 from center, cubicOut (0.4s)
+    const blK = clampLerp(t, T.brand_line[0], T.brand_line[1]);
+    brandLine.style.width = (280 * cubicOut(blK)) + 'px';
+
+    frameCount++;
+
+    // Loop or stop
+    if (t < DURATION) {
+      requestAnimationFrame(tick);
+    } else {
+      if (window.__recording === true) {
+        // recording mode: hold last frame
+        return;
+      }
+      // Restart after 1s pause (for manual viewing)
+      setTimeout(() => {
+        startTs = null;
+        state = { palette: 'warm', type: 'serif', density: 'sparse' };
+        updatePreview();
+        setValueLabel(val1, 'warm'); setValueLabel(val2, 'serif'); setValueLabel(val3, 'sparse');
+        requestAnimationFrame(tick);
+      }, 900);
+    }
+  }
+
+  // Start animation after fonts ready
+  const startAnim = () => {
+    requestAnimationFrame((ts) => {
+      startTs = ts;
+      window.__ready = true; // signal for render-video.js
+      requestAnimationFrame(tick);
+    });
+  };
+
+  if (document.fonts && document.fonts.ready) {
+    document.fonts.ready.then(startAnim);
+  } else {
+    setTimeout(startAnim, 500);
+  }
+})();
+</script>
+
+</body>
+</html>

+ 947 - 720
demos/c4-tweaks.html

@@ -1,762 +1,989 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
+<!doctype html>
+<html lang="zh-Hans">
 <head>
-<meta charset="UTF-8">
-<title>Huashu-Design · Tweaks 实时变体</title>
-<script crossorigin src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
-<script crossorigin src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
-<script src="https://unpkg.com/@babel/standalone@7.25.6/babel.min.js"></script>
+<meta charset="utf-8" />
+<title>c4-tweaks · 拨动即所得(中文版)</title>
 <link rel="preconnect" href="https://fonts.googleapis.com">
 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,300;1,6..72,400;1,6..72,500&family=Noto+Serif+SC:wght@300;400;500;600;700&family=Noto+Sans+SC:wght@400;500;600;700&family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600;700&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
 <style>
-  * { box-sizing: border-box; margin: 0; padding: 0; }
-  html, body { width: 100%; height: 100%; overflow: hidden; }
-  body {
-    background: #0c0c0c;
-    font-family: 'Newsreader', 'Noto Serif SC', Georgia, serif;
-    color: #1a1a1a;
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;
+    --accent-deep: #B85D3D;
+
+    /* Mock landing page · warm variant (initial state) */
+    --warm-bg: #F6EFE6;
+    --warm-panel: #FFFFFF;
+    --warm-ink: #1A1918;
+    --warm-dim: #8B867E;
+    --warm-hair: rgba(0,0,0,0.08);
+    --warm-accent: #D97757;
+
+    /* Mock landing page · cool variant (after slider 1) */
+    --cool-bg: #0E1620;
+    --cool-panel: #17222E;
+    --cool-ink: #E8EEF5;
+    --cool-dim: #7A8A9B;
+    --cool-hair: rgba(255,255,255,0.08);
+    --cool-accent: #5A8CB8;
+
+    --serif-en: "Source Serif 4", Georgia, serif;
+    --serif-cn: "Noto Serif SC", "Source Serif 4", Georgia, serif;
+    --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
     -webkit-font-smoothing: antialiased;
-    text-rendering: optimizeLegibility;
   }
-</style>
-</head>
-<body>
-<div id="root"></div>
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform: translate(-50%, -50%);
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+  }
 
-<!-- animations.jsx inlined -->
-<script type="text/babel">
-(function() {
-  const { createContext, useContext, useState, useEffect, useRef, useCallback } = React;
-  const TimeContext = createContext({ time: 0, duration: 10, playing: false });
-  const SpriteContext = createContext(null);
-
-  const Easing = {
-    linear: t => t,
-    easeIn: t => t * t,
-    easeOut: t => 1 - (1 - t) * (1 - t),
-    easeInOut: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
-    spring: t => {
-      const c = (2 * Math.PI) / 3;
-      return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c) + 1;
-    },
-  };
+  /* Film grain */
+  .grain {
+    position: absolute; inset: 0;
+    background-image:
+      radial-gradient(rgba(255,255,255,0.02) 1px, transparent 1px);
+    background-size: 3px 3px;
+    opacity: 0.4;
+    pointer-events: none;
+    z-index: 2;
+  }
 
-  function interpolate(t, input, output, easing) {
-    const [inStart, inEnd] = input;
-    const [outStart, outEnd] = output;
-    if (t <= inStart) return outStart;
-    if (t >= inEnd) return outEnd;
-    let progress = (t - inStart) / (inEnd - inStart);
-    if (easing) progress = easing(progress);
-    return outStart + (outEnd - outStart) * progress;
-  }
-
-  function useTime() { return useContext(TimeContext).time; }
-  function useSprite() {
-    const sprite = useContext(SpriteContext);
-    return sprite || { t: 0, elapsed: 0, duration: 0 };
-  }
-
-  function Stage({ duration = 10, width = 1920, height = 1080, loop = true, children, bgColor = '#fff' }) {
-    const [time, setTime] = useState(0);
-    const [playing, setPlaying] = useState(true);
-    const [scale, setScale] = useState(1);
-    const rafRef = useRef(null);
-    const effectiveLoop = (typeof window !== 'undefined' && window.__recording) ? false : loop;
-
-    useEffect(() => {
-      function updateScale() {
-        const vw = window.innerWidth;
-        const vh = window.innerHeight - 56;
-        const s = Math.min(vw / width, vh / height);
-        setScale(s);
-      }
-      updateScale();
-      window.addEventListener('resize', updateScale);
-      return () => window.removeEventListener('resize', updateScale);
-    }, [width, height]);
-
-    useEffect(() => {
-      if (!playing) return;
-      let cancelled = false;
-      let last = null;
-      function tick(now) {
-        if (cancelled) return;
-        if (last === null) {
-          last = now;
-          if (typeof window !== 'undefined') window.__ready = true;
-        }
-        const delta = (now - last) / 1000;
-        last = now;
-        setTime(prev => {
-          const next = prev + delta;
-          if (next >= duration) return effectiveLoop ? 0 : duration - 0.001;
-          return next;
-        });
-        rafRef.current = requestAnimationFrame(tick);
-      }
-      const start = () => { if (!cancelled) rafRef.current = requestAnimationFrame(tick); };
-      if (document.fonts && document.fonts.ready) document.fonts.ready.then(start); else start();
-      return () => { cancelled = true; cancelAnimationFrame(rafRef.current); };
-    }, [playing, duration, effectiveLoop]);
-
-    const progress = time / duration;
-    const ctx = { time, duration, playing, setPlaying, setTime };
-
-    const canvasStyle = {
-      position: 'absolute',
-      top: '50%',
-      left: '50%',
-      transformOrigin: 'center center',
-      width,
-      height,
-      background: bgColor,
-      overflow: 'hidden',
-      transform: `translate(-50%, -50%) scale(${scale})`,
-    };
+  /* Watermark */
+  .watermark {
+    position: absolute;
+    top: 44px; left: 56px;
+    font-family: var(--mono);
+    font-size: 14px;
+    font-weight: 500;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,0.16);
+    z-index: 10;
+  }
 
-    return (
-      <TimeContext.Provider value={ctx}>
-        <div style={{position:'fixed', inset:0, background:'#0c0c0c', display:'flex', flexDirection:'column'}}>
-          <div style={{flex:1, position:'relative', overflow:'hidden'}}>
-            <div style={canvasStyle}>{children}</div>
-          </div>
-          <div className="no-record" style={{position:'fixed', bottom:0, left:0, right:0, background:'rgba(0,0,0,0.8)', backdropFilter:'blur(10px)', padding:'12px 20px', display:'flex', alignItems:'center', gap:16, color:'#fff', fontSize:12, zIndex:100}}>
-            <button onClick={()=>setPlaying(p=>!p)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>{playing?'⏸ 暂停':'▶ 播放'}</button>
-            <button onClick={()=>setTime(0)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>⏮ 开始</button>
-            <div style={{fontFamily:'ui-monospace, monospace', fontVariantNumeric:'tabular-nums', minWidth:90}}>{time.toFixed(2)}s / {duration.toFixed(2)}s</div>
-            <div style={{flex:1, height:4, background:'rgba(255,255,255,0.2)', borderRadius:2, position:'relative'}}>
-              <div style={{position:'absolute', top:0, left:0, height:'100%', width:`${progress*100}%`, background:'#fff', borderRadius:2}} />
-            </div>
-          </div>
-        </div>
-      </TimeContext.Provider>
-    );
-  }
-
-  function Sprite({ start = 0, end, children, style }) {
-    const { time } = useContext(TimeContext);
-    const actualEnd = end == null ? Infinity : end;
-    if (time < start || time >= actualEnd) return null;
-    const duration = actualEnd - start;
-    const elapsed = time - start;
-    const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
-    const spriteValue = { t, elapsed, duration, start, end: actualEnd };
-    return (
-      <SpriteContext.Provider value={spriteValue}>
-        <div style={{position:'absolute', inset:0, ...style}}>{children}</div>
-      </SpriteContext.Provider>
-    );
-  }
-
-  window.Animations = { Stage, Sprite, useTime, useSprite, Easing, interpolate };
-})();
-</script>
+  .version-mark {
+    position: absolute;
+    bottom: 44px; right: 56px;
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,0.12);
+    z-index: 10;
+  }
 
-<!-- Demo scene -->
-<script type="text/babel">
-const { Stage, Sprite, useTime, useSprite, Easing, interpolate } = window.Animations;
-
-// ── Design tokens ─────────────────────────────────────────
-const CREAM = '#FAF6EF';
-const INK = '#1a1a1a';
-const TERRA = '#C04A1A';
-const ASH = '#6b6b6b';
-const LINE = '#d9d2c5';
-const OLIVE = '#6a6b4e';
-const DEEP_BLUE = '#2a3552';
-
-const serif = "'Newsreader', 'Noto Serif SC', Georgia, serif";
-const sansCN = "'Noto Sans SC', -apple-system, sans-serif";
-const playfair = "'Playfair Display', Georgia, serif";
-const sans = "'Inter', -apple-system, sans-serif";
-const mono = "'JetBrains Mono', ui-monospace, monospace";
-
-// Palettes — the actual Tweak presets
-const PALETTES = {
-  warm:  { bg: CREAM,     accent: TERRA,       text: INK,    sub: ASH,    line: LINE,    name: '暖米 + 赤陶',  enName: 'WARM · TERRA' },
-  olive: { bg: '#f2efdf', accent: OLIVE,       text: '#2a2a1e', sub: '#7a7a5e', line: '#d4d1b8', name: '墨绿 + 鹅黄',  enName: 'OLIVE · CITRON' },
-  deep:  { bg: '#f4efe6', accent: DEEP_BLUE,   text: INK,    sub: '#5a6478', line: '#c9c3b3', name: '深蓝 + 沙金',  enName: 'DEEP · SAND' },
-};
-
-const FONTS = {
-  serif: { ui: serif, display: serif, name: 'Newsreader(衬线)' },
-  sans:  { ui: sansCN, display: sansCN, name: '思源黑体' },
-  play:  { ui: playfair, display: playfair, name: 'Playfair Display' },
-};
-
-// ── Scene 1: Title (0 – 3s) ────────────────────────────────
-function Scene1_Title() {
-  const { elapsed } = useSprite();
-  const labelOp = interpolate(elapsed, [0.2, 0.8], [0, 1]);
-  const mainY = interpolate(elapsed, [0.3, 1.3], [40, 0], Easing.easeOut);
-  const mainOp = interpolate(elapsed, [0.3, 1.1], [0, 1]);
-  const lineW = interpolate(elapsed, [1.1, 1.9], [0, 460]);
-  const subOp = interpolate(elapsed, [1.4, 2.1], [0, 1]);
-  const fadeOut = interpolate(elapsed, [2.6, 3.0], [1, 0], Easing.easeIn);
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
-      display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
-      <div style={{fontFamily: mono, fontSize: 14, letterSpacing:'0.35em',
-        color: TERRA, marginBottom: 30, opacity: labelOp}}>
-        实时变体 · TWEAKS
-      </div>
-      <div style={{fontFamily: serif, fontSize: 120, fontWeight: 500, color: INK,
-        lineHeight: 1.05, letterSpacing: '-0.01em',
-        opacity: mainOp, transform: `translateY(${mainY}px)`}}>
-        一个 HTML · <span style={{fontStyle:'italic', color: TERRA}}>多种</span>设计
-      </div>
-      <div style={{height: 1, background: INK, width: lineW, marginTop: 40}} />
-      <div style={{fontFamily: serif, fontStyle: 'italic', fontSize: 22,
-        color: ASH, marginTop: 26, opacity: subOp, letterSpacing:'0.02em'}}>
-        不需要重新生成代码 · 只切参数
-      </div>
-    </div>
-  );
-}
-
-// ── Scene 2: Main stage — control panel + live card (3 – 12s) ──
-function Scene2_MainStage() {
-  const { elapsed } = useSprite();
-
-  // Decide current Tweaks state based on elapsed time inside the scene.
-  // Scene2 elapsed: 0 – 9s
-  //   0 – 4s : warm + serif + 40
-  //   4 – 7s : olive + serif + 40   (palette change @ 4s)
-  //   7 – 9s : olive + sans + 40    (font change @ 7s)
-  let palette = 'warm';
-  let font = 'serif';
-  const density = 40;
-
-  if (elapsed >= 4) palette = 'olive';
-  if (elapsed >= 7) font = 'sans';
-
-  // Ripple trigger times
-  const rippleTimes = [4, 7];
-  const ripples = rippleTimes.map(t => {
-    const e = elapsed - t;
-    if (e < 0 || e > 1.2) return null;
-    return { t, progress: e / 1.2 };
-  }).filter(Boolean);
-
-  // Fade-in intro
-  const introOp = interpolate(elapsed, [0, 0.4], [0, 1]);
-  const fadeOut = interpolate(elapsed, [8.6, 9.0], [1, 0]);
-
-  const pal = PALETTES[palette];
-  const fnt = FONTS[font];
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM,
-      display:'flex', opacity: introOp * fadeOut}}>
-      {/* Left: Control Panel (30%) */}
-      <ControlPanel palette={palette} font={font} density={density} elapsed={elapsed} />
-
-      {/* Right: Live Card (70%) */}
-      <div style={{flex: 1, position:'relative', padding: '60px 80px',
-        display:'flex', alignItems:'center', justifyContent:'center',
-        transition: 'background 600ms ease-in-out',
-        background: pal.bg}}>
-        <LiveCard palette={palette} font={font} density={density} />
-
-        {/* Ripples */}
-        {ripples.map((r, i) => (
-          <Ripple key={r.t} progress={r.progress}
-            x={r.t === 4 ? 180 : 180}
-            y={r.t === 4 ? 340 : 490} />
-        ))}
-      </div>
-    </div>
-  );
-}
-
-function ControlPanel({ palette, font, density, elapsed }) {
-  return (
-    <div style={{width: '30%', background: '#f2ece0',
-      borderRight: `1px solid ${LINE}`, padding: '60px 44px 40px',
-      display:'flex', flexDirection:'column', gap: 38,
-      fontFamily: sans}}>
-      <div>
-        <div style={{fontFamily: mono, fontSize: 11, letterSpacing:'0.35em',
-          color: TERRA, marginBottom: 6}}>
-          TWEAKS
-        </div>
-        <div style={{fontFamily: serif, fontSize: 28, fontWeight: 500, color: INK,
-          letterSpacing:'-0.01em'}}>
-          设计调参面板
-        </div>
-      </div>
+  /* ============ Main composition ============ */
+  .composition {
+    position: absolute;
+    inset: 0;
+    display: grid;
+    grid-template-columns: 1080px 500px;
+    gap: 80px;
+    padding: 130px 120px 140px 140px;
+    align-items: center;
+    perspective: 2400px;
+  }
 
-      {/* Group 1: palette */}
-      <ControlGroup label="01 · 配色方案" en="PALETTE">
-        <Radio checked={palette==='warm'}  label="暖米 + 赤陶"   swatches={[CREAM, TERRA]} />
-        <Radio checked={palette==='olive'} label="墨绿 + 鹅黄"   swatches={['#f2efdf', OLIVE]} />
-        <Radio checked={palette==='deep'}  label="深蓝 + 沙金"   swatches={['#f4efe6', DEEP_BLUE]} />
-      </ControlGroup>
-
-      {/* Group 2: font */}
-      <ControlGroup label="02 · 字型" en="TYPEFACE">
-        <Radio checked={font==='serif'} label="Newsreader(衬线)" fontFamily={serif} />
-        <Radio checked={font==='sans'}  label="思源黑体"         fontFamily={sansCN} />
-        <Radio checked={font==='play'}  label="Playfair Display" fontFamily={playfair} />
-      </ControlGroup>
-
-      {/* Group 3: density */}
-      <ControlGroup label="03 · 信息密度" en="DENSITY">
-        <div style={{position:'relative', height: 4, background:'#e0dbcc',
-          marginTop: 16, marginBottom: 10}}>
-          <div style={{position:'absolute', left: 0, top: 0, height:'100%',
-            width: `${density}%`, background: TERRA}} />
-          <div style={{position:'absolute', left: `${density}%`, top: -6,
-            transform:'translateX(-50%)', width: 16, height: 16,
-            borderRadius:'50%', background: TERRA,
-            boxShadow:'0 2px 6px rgba(0,0,0,0.15)'}} />
-        </div>
-        <div style={{display:'flex', justifyContent:'space-between',
-          fontFamily: mono, fontSize: 9, letterSpacing:'0.2em', color: ASH}}>
-          <span>克制</span><span style={{color: TERRA}}>标准</span><span>密集</span>
-        </div>
-      </ControlGroup>
+  /* ---- Design preview (left) ---- */
+  .preview-frame {
+    position: relative;
+    width: 1080px;
+    height: 800px;
+    border-radius: 18px;
+    overflow: hidden;
+    transform-style: preserve-3d;
+    transform: rotateX(6deg) rotateY(-4deg);
+    box-shadow:
+      0 50px 120px rgba(0,0,0,0.6),
+      0 0 0 1px rgba(255,255,255,0.06);
+    opacity: 0;
+    will-change: opacity, transform, background;
+    transition: background 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .preview-frame.warm {
+    background: var(--warm-bg);
+  }
+  .preview-frame.cool {
+    background: var(--cool-bg);
+  }
 
-      <div style={{flex: 1}} />
+  /* Browser chrome top bar */
+  .browser-chrome {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    padding: 16px 22px;
+    border-bottom: 1px solid var(--warm-hair);
+    background: var(--warm-panel);
+    transition: all 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .browser-chrome {
+    background: var(--cool-panel);
+    border-bottom-color: var(--cool-hair);
+  }
+  .dot {
+    width: 11px; height: 11px; border-radius: 50%;
+    background: rgba(0,0,0,0.14);
+  }
+  .cool .dot { background: rgba(255,255,255,0.14); }
+  .url-bar {
+    flex: 1;
+    margin-left: 14px;
+    padding: 6px 14px;
+    border-radius: 6px;
+    background: rgba(0,0,0,0.04);
+    font-family: var(--mono);
+    font-size: 12px;
+    color: var(--warm-dim);
+    letter-spacing: 0.05em;
+    transition: all 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .url-bar {
+    background: rgba(255,255,255,0.04);
+    color: var(--cool-dim);
+  }
 
-      <div style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.12em',
-        color: ASH, lineHeight: 1.6, borderTop: `1px solid ${LINE}`,
-        paddingTop: 16}}>
-        localStorage 持久化<br/>
-        <span style={{color: TERRA}}>→</span> 刷新不丢
-      </div>
-    </div>
-  );
-}
-
-function ControlGroup({ label, en, children }) {
-  return (
-    <div>
-      <div style={{display:'flex', justifyContent:'space-between',
-        alignItems:'baseline', marginBottom: 14}}>
-        <div style={{fontFamily: serif, fontSize: 15, fontWeight: 500, color: INK}}>
-          {label}
-        </div>
-        <div style={{fontFamily: mono, fontSize: 9, letterSpacing:'0.25em',
-          color: ASH}}>{en}</div>
-      </div>
-      <div style={{display:'flex', flexDirection:'column', gap: 8}}>{children}</div>
-    </div>
-  );
-}
-
-function Radio({ checked, label, swatches, fontFamily }) {
-  return (
-    <div style={{display:'flex', alignItems:'center', gap: 12,
-      padding:'9px 12px', background: checked ? '#fff' : 'transparent',
-      border: `1px solid ${checked ? TERRA : 'transparent'}`,
-      transition:'all 240ms ease-out'}}>
-      <div style={{width: 14, height: 14, borderRadius:'50%',
-        border: `1.5px solid ${checked ? TERRA : '#b0a898'}`,
-        display:'flex', alignItems:'center', justifyContent:'center',
-        flexShrink: 0}}>
-        {checked && <div style={{width: 7, height: 7, borderRadius:'50%',
-          background: TERRA}} />}
-      </div>
-      <div style={{flex: 1, fontFamily: fontFamily || sans, fontSize: 13,
-        color: checked ? INK : '#4a4a4a'}}>{label}</div>
-      {swatches && (
-        <div style={{display:'flex', gap: 3}}>
-          {swatches.map((c, i) => (
-            <div key={i} style={{width: 12, height: 12, background: c,
-              border:'1px solid rgba(0,0,0,0.06)'}} />
-          ))}
-        </div>
-      )}
-    </div>
-  );
-}
-
-function Ripple({ progress, x, y }) {
-  const size = progress * 420;
-  const op = 1 - progress;
-  return (
-    <div style={{position:'absolute', left: x, top: y,
-      width: size, height: size, borderRadius:'50%',
-      border: `2px solid ${TERRA}`, opacity: op,
-      transform: 'translate(-50%, -50%)',
-      pointerEvents:'none'}} />
-  );
-}
-
-function LiveCard({ palette, font, density }) {
-  const pal = PALETTES[palette];
-  const fnt = FONTS[font];
-
-  return (
-    <div style={{width: '100%', maxWidth: 880, background: '#fff',
-      border: `1px solid ${pal.line}`,
-      transition:'border-color 600ms ease-in-out',
-      position:'relative'}}>
-      {/* Header bar */}
-      <div style={{padding:'18px 32px', borderBottom:`1px solid ${pal.line}`,
-        display:'flex', justifyContent:'space-between', alignItems:'center',
-        transition:'border-color 600ms ease-in-out'}}>
-        <div style={{fontFamily: mono, fontSize: 11, letterSpacing:'0.3em',
-          color: pal.accent, transition:'color 600ms ease-in-out'}}>
-          LUMINA · v3.2
-        </div>
-        <div style={{fontFamily: mono, fontSize: 10, color: pal.sub,
-          letterSpacing:'0.15em', transition:'color 600ms ease-in-out'}}>
-          PALETTE · {pal.enName}
-        </div>
-      </div>
+  /* Hero content */
+  .preview-body {
+    padding: 54px 72px 60px 72px;
+    color: var(--warm-ink);
+    transition: color 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .preview-body { color: var(--cool-ink); }
+
+  .preview-eyebrow {
+    font-family: var(--mono);
+    font-size: 11px;
+    font-weight: 500;
+    letter-spacing: 0.24em;
+    text-transform: uppercase;
+    color: var(--warm-accent);
+    transition: color 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .preview-eyebrow { color: var(--cool-accent); }
+
+  .preview-title {
+    margin-top: 16px;
+    font-family: var(--serif-cn);
+    font-weight: 400;
+    font-size: 86px;
+    line-height: 1.02;
+    letter-spacing: -0.02em;
+    transition: font-family 240ms cubic-bezier(.2,.8,.2,1),
+                font-weight 240ms cubic-bezier(.2,.8,.2,1),
+                letter-spacing 240ms cubic-bezier(.2,.8,.2,1);
+  }
+  .preview-title .em {
+    color: var(--warm-accent);
+    font-style: italic;
+    transition: color 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .preview-title .em { color: var(--cool-accent); }
+
+  .preview-frame.sans .preview-title {
+    font-family: var(--sans);
+    font-weight: 200;
+    letter-spacing: -0.045em;
+  }
+  .preview-frame.sans .preview-title .em {
+    font-style: normal;
+  }
+
+  .preview-sub {
+    margin-top: 24px;
+    font-family: var(--serif-cn);
+    font-size: 20px;
+    font-weight: 300;
+    line-height: 1.6;
+    max-width: 720px;
+    color: var(--warm-dim);
+    transition: color 280ms cubic-bezier(.2,.8,.2,1),
+                font-family 240ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .preview-sub { color: var(--cool-dim); }
+  .preview-frame.sans .preview-sub {
+    font-family: var(--sans);
+  }
+
+  /* Density cards grid */
+  .card-grid {
+    margin-top: 54px;
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    gap: 18px;
+    transition: grid-template-columns 280ms cubic-bezier(.2,.8,.2,1),
+                gap 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .preview-frame.dense .card-grid {
+    grid-template-columns: repeat(3, 1fr);
+    grid-auto-rows: minmax(72px, auto);
+    gap: 10px;
+  }
+
+  .card {
+    padding: 22px 22px 24px 22px;
+    border-radius: 10px;
+    background: rgba(0,0,0,0.035);
+    border: 1px solid var(--warm-hair);
+    transition: all 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .card {
+    background: rgba(255,255,255,0.03);
+    border-color: var(--cool-hair);
+  }
+  .preview-frame.dense .card {
+    padding: 12px 14px;
+  }
 
-      {/* Hero content */}
-      <div style={{padding:'56px 60px 48px', display:'grid',
-        gridTemplateColumns:'1.4fr 1fr', gap: 48}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11,
-            color: pal.accent, letterSpacing:'0.25em', marginBottom: 16,
-            transition:'color 600ms ease-in-out'}}>
-            READING · MEMORY
+  .card-icon {
+    width: 28px; height: 28px;
+    border-radius: 6px;
+    background: var(--warm-accent);
+    opacity: 0.16;
+    margin-bottom: 14px;
+    transition: all 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .card-icon { background: var(--cool-accent); }
+  .preview-frame.dense .card-icon {
+    width: 18px; height: 18px;
+    margin-bottom: 8px;
+  }
+
+  .card-title {
+    font-family: var(--serif-cn);
+    font-size: 18px;
+    font-weight: 500;
+    color: var(--warm-ink);
+    letter-spacing: -0.005em;
+    transition: color 280ms cubic-bezier(.2,.8,.2,1),
+                font-family 240ms cubic-bezier(.2,.8,.2,1),
+                font-size 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .card-title { color: var(--cool-ink); }
+  .preview-frame.sans .card-title {
+    font-family: var(--sans);
+    font-weight: 500;
+  }
+  .preview-frame.dense .card-title {
+    font-size: 13px;
+  }
+
+  .card-text {
+    margin-top: 6px;
+    font-family: var(--serif-cn);
+    font-size: 13px;
+    line-height: 1.45;
+    color: var(--warm-dim);
+    transition: all 280ms cubic-bezier(.2,.8,.2,1);
+  }
+  .cool .card-text { color: var(--cool-dim); }
+  .preview-frame.sans .card-text { font-family: var(--sans); }
+  .preview-frame.dense .card-text {
+    font-size: 11px;
+    line-height: 1.3;
+    opacity: 0.85;
+  }
+
+  /* Extra cards (hidden in sparse mode) */
+  .card.extra {
+    opacity: 0;
+    transform: scale(0.92);
+    transition: opacity 240ms cubic-bezier(.2,.8,.2,1),
+                transform 240ms cubic-bezier(.2,.8,.2,1),
+                background 280ms cubic-bezier(.2,.8,.2,1),
+                border-color 280ms cubic-bezier(.2,.8,.2,1);
+    pointer-events: none;
+    max-height: 0;
+    padding: 0;
+    overflow: hidden;
+  }
+  .preview-frame.dense .card.extra {
+    opacity: 1;
+    transform: scale(1);
+    max-height: 120px;
+    padding: 12px 14px;
+  }
+
+  /* ---- Slider panel (right) ---- */
+  .slider-panel {
+    position: relative;
+    width: 500px;
+    opacity: 0;
+    will-change: opacity, transform;
+    display: flex;
+    flex-direction: column;
+    gap: 64px;
+  }
+
+  .anchor-line {
+    position: absolute;
+    top: -80px;
+    left: 8px;
+    font-family: var(--serif-cn);
+    font-weight: 400;
+    font-size: 26px;
+    letter-spacing: 0.02em;
+    color: var(--ink-80);
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+  .anchor-line .em {
+    color: var(--accent);
+    font-weight: 500;
+  }
+
+  .slider-item {
+    display: flex;
+    flex-direction: column;
+    gap: 18px;
+  }
+
+  .slider-label {
+    display: flex;
+    align-items: baseline;
+    justify-content: space-between;
+  }
+  .slider-name {
+    font-family: var(--mono);
+    font-size: 14px;
+    font-weight: 500;
+    letter-spacing: 0.18em;
+    color: var(--ink-80);
+    text-transform: uppercase;
+  }
+  .slider-value {
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.14em;
+    color: var(--muted);
+  }
+
+  /* Track */
+  .track {
+    position: relative;
+    width: 100%;
+    height: 2px;
+    background: var(--hairline);
+  }
+  .track-fill {
+    position: absolute;
+    top: 0; left: 0;
+    height: 100%;
+    width: 10%;
+    background: var(--accent);
+    will-change: width;
+  }
+
+  /* Tick marks */
+  .ticks {
+    position: absolute;
+    inset: -4px 0 -4px 0;
+    display: flex;
+    justify-content: space-between;
+    pointer-events: none;
+  }
+  .tick {
+    width: 1px;
+    height: 10px;
+    background: rgba(255,255,255,0.14);
+  }
+
+  /* Knob */
+  .knob {
+    position: absolute;
+    top: 50%;
+    left: 10%;
+    width: 26px; height: 26px;
+    border-radius: 50%;
+    background: var(--ink);
+    transform: translate(-50%, -50%);
+    box-shadow: 0 0 0 1px rgba(0,0,0,0.6),
+                0 8px 24px rgba(0,0,0,0.5);
+    will-change: left, transform, box-shadow;
+  }
+  .knob.active {
+    box-shadow: 0 0 0 2px var(--accent),
+                0 0 30px rgba(217,119,87,0.45),
+                0 8px 24px rgba(0,0,0,0.5);
+  }
+
+  /* Cursor */
+  .cursor {
+    position: absolute;
+    width: 20px; height: 20px;
+    pointer-events: none;
+    will-change: left, top, opacity;
+    opacity: 0;
+    z-index: 20;
+  }
+  .cursor svg { width: 100%; height: 100%; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.8)); }
+
+  /* ---- Brand reveal ---- */
+  /* Stage dimmer: fades the composition out just before the panel slides in */
+  .stage-dimmer {
+    position: absolute;
+    inset: 0;
+    background: #000000;
+    opacity: 0;
+    z-index: 40;
+    pointer-events: none;
+    will-change: opacity;
+  }
+  .brand-panel {
+    position: absolute;
+    inset: 0;
+    background: #F5F4F0;
+    transform: translateY(100%);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    z-index: 50;
+    will-change: transform;
+  }
+  .brand-wordmark {
+    font-family: var(--serif-en);
+    font-size: 72px;
+    font-weight: 100;
+    font-variation-settings: "wght" 100;
+    letter-spacing: -0.02em;
+    color: #1A1918;
+    text-align: center;
+    line-height: 1;
+    opacity: 0;
+    transform: translateY(20px);
+    will-change: opacity, transform, font-variation-settings, font-weight;
+  }
+  .brand-wordmark .accent { color: #D97757; font-weight: inherit; }
+
+  .brand-line {
+    /* Flex-centered, 60px below wordmark (line-height 1 @ 72px → descender + 24 gap) */
+    margin-top: 60px;
+    height: 2px;
+    width: 0;
+    background: #D97757;
+    align-self: center;
+    will-change: width;
+  }
+
+</style>
+</head>
+<body>
+
+<div class="stage" id="stage">
+
+  <div class="grain"></div>
+  <div class="watermark">HUASHU · DESIGN</div>
+  <div class="version-mark">V2 · 2026</div>
+
+  <div class="composition">
+
+    <!-- LEFT: design preview -->
+    <div class="preview-frame warm" id="preview">
+      <div class="browser-chrome">
+        <span class="dot"></span><span class="dot"></span><span class="dot"></span>
+        <div class="url-bar">yourbrand.design</div>
+      </div>
+      <div class="preview-body">
+        <div class="preview-eyebrow">Agent Studio</div>
+        <div class="preview-title">为<span class="em">他们</span>造好<br/>工作的场所。</div>
+        <div class="preview-sub">一个设计系统,不等你打开;它在你睡觉时,已经把草稿交出来了。</div>
+
+        <div class="card-grid" id="cardGrid">
+          <div class="card">
+            <div class="card-icon"></div>
+            <div class="card-title">品牌资产</div>
+            <div class="card-text">Logo / 色板 / 字型的单一事实源。</div>
           </div>
-          <div style={{fontFamily: fnt.display, fontSize: 68,
-            fontWeight: font === 'sans' ? 700 : 500, color: pal.text,
-            lineHeight: 1.05, letterSpacing:'-0.02em',
-            transition:'color 600ms ease-in-out',
-            marginBottom: 14}}>
-            Lumina
+          <div class="card">
+            <div class="card-icon"></div>
+            <div class="card-title">原型工场</div>
+            <div class="card-text">写一句话,得到一个能点的 App。</div>
           </div>
-          <div style={{fontFamily: fnt.ui,
-            fontStyle: font === 'play' ? 'italic' : 'normal',
-            fontSize: 22, color: pal.sub, lineHeight: 1.4,
-            letterSpacing: font === 'sans' ? 0 : '0.01em',
-            transition:'color 600ms ease-in-out',
-            marginBottom: 28}}>
-            阅读记忆 · 让每一次阅读被看见
+          <div class="card">
+            <div class="card-icon"></div>
+            <div class="card-title">动效引擎</div>
+            <div class="card-text">时间轴即代码,25 到 60 帧随意切。</div>
           </div>
-          <div style={{fontFamily: fnt.ui, fontSize: 14, color: pal.text,
-            lineHeight: 1.7, opacity: 0.78, marginBottom: 32,
-            transition:'color 600ms ease-in-out'}}>
-            把你读过的每一行、标注过的每一段,<br/>
-            汇成一条属于你的阅读河流。
+          <div class="card extra">
+            <div class="card-icon"></div>
+            <div class="card-title">文档工坊</div>
+            <div class="card-text">HTML 即 PPTX。</div>
           </div>
-          <div style={{display:'flex', gap: 12, alignItems:'center'}}>
-            <div style={{padding:'12px 26px', background: pal.accent,
-              color:'#fff', fontFamily: fnt.ui, fontSize: 13,
-              letterSpacing: font === 'sans' ? '0.05em' : '0.12em',
-              transition:'background 600ms ease-in-out'}}>
-              {font === 'sans' ? '开始使用' : 'Start Reading'}
-            </div>
-            <div style={{fontFamily: mono, fontSize: 11, color: pal.sub,
-              letterSpacing:'0.2em',
-              transition:'color 600ms ease-in-out'}}>
-              FREE · BETA
-            </div>
+          <div class="card extra">
+            <div class="card-icon"></div>
+            <div class="card-title">信息图</div>
+            <div class="card-text">数据进,杂志出。</div>
           </div>
-        </div>
-
-        {/* Right image block */}
-        <div style={{background: pal.bg,
-          border: `1px solid ${pal.line}`,
-          transition:'all 600ms ease-in-out',
-          aspectRatio: '3 / 4', position:'relative', overflow:'hidden'}}>
-          {/* Abstract book spine illustration */}
-          <svg width="100%" height="100%" viewBox="0 0 300 400"
-            preserveAspectRatio="xMidYMid slice">
-            {[0,1,2,3,4,5].map(i => (
-              <rect key={i} x={40 + i * 35} y={60 + (i % 2) * 20}
-                width={26} height={280 - (i % 3) * 30}
-                fill="none" stroke={pal.accent}
-                strokeWidth={i === 2 ? 2 : 1}
-                opacity={0.55 + (i === 2 ? 0.4 : 0)}
-                style={{transition:'stroke 600ms ease-in-out'}} />
-            ))}
-            <circle cx={150} cy={200} r={58} fill="none"
-              stroke={pal.accent} strokeWidth={1.5} opacity={0.5}
-              style={{transition:'stroke 600ms ease-in-out'}} />
-            <line x1={40} y1={350} x2={260} y2={350}
-              stroke={pal.text} strokeWidth={0.8} opacity={0.5}
-              style={{transition:'stroke 600ms ease-in-out'}} />
-          </svg>
-          <div style={{position:'absolute', bottom: 16, left: 18,
-            fontFamily: mono, fontSize: 9, letterSpacing:'0.2em',
-            color: pal.sub,
-            transition:'color 600ms ease-in-out'}}>
-            FIG. 01 — SHELF
+          <div class="card extra">
+            <div class="card-icon"></div>
+            <div class="card-title">专家评审</div>
+            <div class="card-text">五维打分,诚实的体检。</div>
+          </div>
+          <div class="card extra">
+            <div class="card-icon"></div>
+            <div class="card-title">方向顾问</div>
+            <div class="card-text">给你三条路选。</div>
+          </div>
+          <div class="card extra">
+            <div class="card-icon"></div>
+            <div class="card-title">Junior 模式</div>
+            <div class="card-text">先 show,再精修。</div>
+          </div>
+          <div class="card extra">
+            <div class="card-icon"></div>
+            <div class="card-title">品牌协议</div>
+            <div class="card-text">五步,不能跳。</div>
           </div>
         </div>
       </div>
+    </div>
 
-      {/* Footer meta */}
-      <div style={{padding:'14px 32px', borderTop:`1px solid ${pal.line}`,
-        display:'flex', justifyContent:'space-between',
-        fontFamily: mono, fontSize: 10, letterSpacing:'0.2em',
-        color: pal.sub,
-        transition:'all 600ms ease-in-out'}}>
-        <span>DENSITY · {density}</span>
-        <span>FONT · {font.toUpperCase()}</span>
-        <span>TWEAK ID · #{palette}-{font}-{density}</span>
+    <!-- RIGHT: slider panel -->
+    <div class="slider-panel" id="panel">
+
+      <div class="anchor-line" id="anchor">
+        拨动<span class="em">即所得</span>
       </div>
-    </div>
-  );
-}
-
-// ── Scene 3: Code view (12 – 17s) ─────────────────────────
-function Scene3_CodeView() {
-  const { elapsed } = useSprite();
-  const introOp = interpolate(elapsed, [0, 0.5], [0, 1]);
-  const fadeOut = interpolate(elapsed, [4.6, 5.0], [1, 0]);
-
-  // Code typing effect
-  const fullCode = `// Tweaks via localStorage + CSS vars
-const tweaks = {
-  palette: 'warm',   // ← user 选
-  font:    'serif',
-  density: 40,
-};
-
-document.documentElement.style
-  .setProperty(
-    '--accent',
-    PALETTES[tweaks.palette].accent
-  );
-
-localStorage.setItem(
-  'tweaks', JSON.stringify(tweaks)
-);`;
-
-  const typeProgress = Math.max(0, Math.min(1, (elapsed - 0.6) / 2.4));
-  const visibleChars = Math.floor(fullCode.length * typeProgress);
-  const visibleCode = fullCode.slice(0, visibleChars);
-  const cursorBlink = Math.floor(elapsed * 2.5) % 2 === 0 && typeProgress < 1;
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM,
-      display:'flex', flexDirection:'column', opacity: introOp * fadeOut,
-      padding:'60px 80px'}}>
-      <div style={{display:'flex', justifyContent:'space-between',
-        alignItems:'baseline', marginBottom: 36}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.35em',
-            color: TERRA, marginBottom: 6}}>
-            UNDER THE HOOD
-          </div>
-          <div style={{fontFamily: serif, fontSize: 52, fontWeight: 500,
-            color: INK, letterSpacing:'-0.01em'}}>
-            原理 · <span style={{fontStyle:'italic', color: TERRA}}>一行配置</span>,无限变体
-          </div>
+
+      <!-- Slider 1 · 调色 -->
+      <div class="slider-item">
+        <div class="slider-label">
+          <span class="slider-name">调色</span>
+          <span class="slider-value" id="val1">warm</span>
         </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
-          color: ASH, textAlign:'right', lineHeight: 1.5}}>
-          纯前端 · 无后端依赖<br/>
-          <span style={{fontSize: 14}}>刷新保留状态</span>
+        <div class="track">
+          <div class="ticks">
+            <span class="tick"></span><span class="tick"></span><span class="tick"></span>
+            <span class="tick"></span><span class="tick"></span>
+          </div>
+          <div class="track-fill" id="fill1"></div>
+          <div class="knob" id="knob1"></div>
         </div>
       </div>
 
-      <div style={{display:'grid', gridTemplateColumns:'1fr 1.4fr', gap: 40,
-        flex: 1}}>
-        {/* Left: simplified tweak visualization */}
-        <div style={{background:'#fff', border:`1px solid ${LINE}`,
-          padding: 36, display:'flex', flexDirection:'column', gap: 28}}>
-          <div style={{fontFamily: mono, fontSize: 11, letterSpacing:'0.3em',
-            color: TERRA}}>TWEAK · STATE</div>
-
-          <MiniRow label="palette" value="warm" swatch={TERRA} />
-          <MiniRow label="font"    value="serif" />
-          <MiniRow label="density" value="40" />
-
-          <div style={{height: 1, background: LINE, margin:'8px 0'}} />
-
-          <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 16,
-            color: ASH, lineHeight: 1.55}}>
-            三个参数的组合空间:<br/>
-            <span style={{color: TERRA, fontFamily: mono, fontStyle:'normal',
-              fontSize: 14}}>3 × 3 × ∞ = 无限</span>
-          </div>
-
-          <div style={{flex: 1}} />
-
-          <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-            letterSpacing:'0.15em', lineHeight: 1.7}}>
-            → 改代码:不必要<br/>
-            → 重新生成:不必要<br/>
-            → 只改变量:30ms 生效
+      <!-- Slider 2 · 字型 -->
+      <div class="slider-item">
+        <div class="slider-label">
+          <span class="slider-name">字型</span>
+          <span class="slider-value" id="val2">serif</span>
+        </div>
+        <div class="track">
+          <div class="ticks">
+            <span class="tick"></span><span class="tick"></span><span class="tick"></span>
+            <span class="tick"></span><span class="tick"></span>
           </div>
+          <div class="track-fill" id="fill2"></div>
+          <div class="knob" id="knob2"></div>
         </div>
+      </div>
 
-        {/* Right: code block */}
-        <div style={{background:'#0e1016', padding:'28px 32px',
-          fontFamily: mono, fontSize: 15, color:'#d4c9b5',
-          lineHeight: 1.7, position:'relative', overflow:'hidden'}}>
-          <div style={{display:'flex', gap: 8, marginBottom: 20}}>
-            <div style={{width: 10, height: 10, borderRadius:'50%',
-              background:'#ff5f57'}} />
-            <div style={{width: 10, height: 10, borderRadius:'50%',
-              background:'#febc2e'}} />
-            <div style={{width: 10, height: 10, borderRadius:'50%',
-              background:'#28c840'}} />
-            <div style={{marginLeft: 14, fontSize: 10, color:'#888',
-              letterSpacing:'0.15em'}}>tweaks.js</div>
+      <!-- Slider 3 · 密度 -->
+      <div class="slider-item">
+        <div class="slider-label">
+          <span class="slider-name">密度</span>
+          <span class="slider-value" id="val3">sparse</span>
+        </div>
+        <div class="track">
+          <div class="ticks">
+            <span class="tick"></span><span class="tick"></span><span class="tick"></span>
+            <span class="tick"></span><span class="tick"></span>
           </div>
-          <pre style={{whiteSpace:'pre-wrap', margin: 0,
-            fontFamily: mono, fontSize: 15, lineHeight: 1.65}}>
-            <CodeColorize text={visibleCode} />
-            {cursorBlink && <span style={{color:'#ff6a3d'}}>▌</span>}
-          </pre>
+          <div class="track-fill" id="fill3"></div>
+          <div class="knob" id="knob3"></div>
         </div>
       </div>
+
     </div>
-  );
-}
-
-function MiniRow({ label, value, swatch }) {
-  return (
-    <div style={{display:'flex', alignItems:'center', gap: 14}}>
-      <div style={{fontFamily: mono, fontSize: 11, color: ASH,
-        letterSpacing:'0.2em', width: 80}}>{label}</div>
-      <div style={{flex: 1, fontFamily: mono, fontSize: 14, color: INK}}>
-        {value}
-      </div>
-      {swatch && (
-        <div style={{width: 14, height: 14, background: swatch}} />
-      )}
+
+    <!-- Cursor -->
+    <div class="cursor" id="cursor">
+      <svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
+        <path d="M2 2 L2 16 L6 12 L9 18 L11 17 L8 11 L14 11 Z"
+              fill="white" stroke="#000" stroke-width="1.2" stroke-linejoin="round"/>
+      </svg>
     </div>
-  );
-}
-
-// Very light syntax coloring
-function CodeColorize({ text }) {
-  const lines = text.split('\n');
-  return (
-    <>
-      {lines.map((line, i) => (
-        <span key={i}>
-          {colorizeLine(line)}
-          {'\n'}
-        </span>
-      ))}
-    </>
-  );
-}
-
-function colorizeLine(line) {
-  const parts = [];
-  let rest = line;
-
-  // comment
-  const cIdx = rest.indexOf('//');
-  if (cIdx >= 0) {
-    const before = rest.slice(0, cIdx);
-    const comment = rest.slice(cIdx);
-    return (
-      <>
-        {tokenize(before)}
-        <span style={{color:'#6a7d6a'}}>{comment}</span>
-      </>
-    );
-  }
-  return tokenize(line);
-}
-
-function tokenize(s) {
-  // keywords + strings
-  const kw = ['const', 'let', 'var', 'function', 'return'];
-  const words = s.split(/(\s+|[{}();,=.:'])/);
-  return words.map((w, i) => {
-    if (kw.includes(w)) return <span key={i} style={{color:'#c79cff'}}>{w}</span>;
-    if (/^'[^']*'$/.test(w)) return <span key={i} style={{color:'#ffb86c'}}>{w}</span>;
-    if (/^[0-9]+$/.test(w)) return <span key={i} style={{color:'#ff6a3d'}}>{w}</span>;
-    if (['palette', 'font', 'density', 'tweaks', 'PALETTES', 'accent'].includes(w)) {
-      return <span key={i} style={{color:'#8be9fd'}}>{w}</span>;
+  </div>
+
+  <!-- Stage dimmer (fades scene to black before panel sweeps in) -->
+  <div class="stage-dimmer" id="stageDimmer"></div>
+
+  <!-- Brand reveal layer -->
+  <div class="brand-panel" id="brandPanel">
+    <div class="brand-wordmark" id="brandMark">huashu<span class="accent">-</span>design</div>
+    <div class="brand-line" id="brandLine"></div>
+  </div>
+
+</div>
+
+<script>
+(function() {
+  // ---------- Fit stage ----------
+  const stage = document.getElementById('stage');
+  function rescale() {
+    const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  rescale();
+  window.addEventListener('resize', rescale);
+
+  // ---------- Animation ----------
+  const DURATION = 10.0; // seconds
+
+  const preview   = document.getElementById('preview');
+  const panel     = document.getElementById('panel');
+  const anchor    = document.getElementById('anchor');
+  const cursor    = document.getElementById('cursor');
+
+  const knob1 = document.getElementById('knob1');
+  const knob2 = document.getElementById('knob2');
+  const knob3 = document.getElementById('knob3');
+  const fill1 = document.getElementById('fill1');
+  const fill2 = document.getElementById('fill2');
+  const fill3 = document.getElementById('fill3');
+  const val1  = document.getElementById('val1');
+  const val2  = document.getElementById('val2');
+  const val3  = document.getElementById('val3');
+
+  const stageDimmer = document.getElementById('stageDimmer');
+  const brandPanel = document.getElementById('brandPanel');
+  const brandMark  = document.getElementById('brandMark');
+  const brandLine  = document.getElementById('brandLine');
+
+  // Easings
+  const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
+  const expoIn  = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
+  const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2;
+  const cubicOut = t => 1 - Math.pow(1 - t, 3);
+
+  function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
+  function lerp(t, t0, t1, v0, v1, ease) {
+    if (t <= t0) return v0;
+    if (t >= t1) return v1;
+    const k = (t - t0) / (t1 - t0);
+    return v0 + (v1 - v0) * (ease ? ease(k) : k);
+  }
+  function clampLerp(t, t0, t1) {
+    if (t <= t0) return 0;
+    if (t >= t1) return 1;
+    return (t - t0) / (t1 - t0);
+  }
+
+  // Knob motion — drag feel: first 70% is a cubic ease (hand moving),
+  // final 15% is overshoot + snap to target (magnetic arrival).
+  function knobMotion(t, t0, t1, fromPct, toPct) {
+    if (t <= t0) return fromPct;
+    if (t >= t1) return toPct;
+    const k = (t - t0) / (t1 - t0);
+    const direction = toPct > fromPct ? 1 : -1;
+    const range = Math.abs(toPct - fromPct);
+
+    if (k < 0.72) {
+      // Main drag: cubic easeInOut feels like a hand moving
+      const e = cubicInOut(k / 0.72);
+      return fromPct + (toPct - fromPct) * e;
+    } else if (k < 0.85) {
+      // Overshoot past target by ~2%
+      const overK = (k - 0.72) / 0.13;
+      const overshoot = 2.2;
+      return toPct + direction * overshoot * Math.sin(overK * Math.PI);
+    } else {
+      // Settled at target
+      return toPct;
     }
-    if (['document', 'localStorage'].includes(w)) {
-      return <span key={i} style={{color:'#ff79c6'}}>{w}</span>;
+  }
+
+  // Timeline (seconds, 10s total)
+  const T = {
+    stage_in:     [0.0, 1.0],       // frame + panel appear
+    anchor_in:    [0.8, 1.4],
+
+    // Slider 1 · palette: warm → cool (1.2s → 3.2s) — arrive at 3.0s
+    s1_cursor_to: [1.3, 1.9],
+    s1_drag:      [1.9, 2.9],
+    s1_settle:    [2.9, 3.1],
+
+    // Slider 2 · type: serif → sans
+    s2_cursor_to: [3.2, 3.7],
+    s2_drag:      [3.7, 4.7],
+    s2_settle:    [4.7, 4.9],
+
+    // Slider 3 · density: sparse → dense
+    s3_cursor_to: [5.0, 5.5],
+    s3_drag:      [5.5, 6.5],
+    s3_settle:    [6.5, 6.7],
+
+    hold:         [6.7, 8.0],
+
+    // Brand reveal (米色 walloff · 2s total)
+    scene_out:    [8.0, 8.3],   // main composition fade to black (0.3s)
+    brand_panel:  [8.3, 8.7],   // cream panel sweeps up from bottom, expoOut (0.4s)
+    brand_mark:   [8.7, 9.3],   // wordmark: wght 100→500 + y 20→0 + opacity 0→1 (0.6s)
+    brand_line:   [9.3, 9.7],   // orange line expands 0→280 from center (0.4s)
+    brand_hold:   [9.7, 10.0],  // hold final frame
+  };
+
+  // Slider-to-state logic. Value-changes happen at settle start.
+  let state = { palette: 'warm', type: 'serif', density: 'sparse' };
+  let lastStateHash = '';
+  function updatePreview() {
+    preview.classList.remove('warm', 'cool', 'sans', 'dense');
+    if (state.palette === 'warm') preview.classList.add('warm');
+    else preview.classList.add('cool');
+    if (state.type === 'sans') preview.classList.add('sans');
+    if (state.density === 'dense') preview.classList.add('dense');
+  }
+  updatePreview();
+
+  function setKnobState(knob, active) {
+    if (active) knob.classList.add('active');
+    else knob.classList.remove('active');
+  }
+
+  function setValueLabel(el, text) {
+    if (el.textContent !== text) el.textContent = text;
+  }
+
+  // ---------- Cursor path (in composition coords) ----------
+  // Composition uses grid: left column 1220 + 60 gap, panel is at right.
+  // We'll position cursor using .composition-relative absolute positioning.
+  // Cursor is child of .composition, whose padding is 130/100/140/140.
+  // So coords relative to .composition padding-box.
+  // Simpler: cursor is absolute in .stage coords since parent composition
+  // covers full stage. Use inline style left/top in px.
+
+  // Anchor positions (rough — will fine-tune):
+  const CURSOR_PARK   = { x: 1900, y: 1080 }; // off-screen bottom-right
+  // Slider tracks: panel starts around x≈1420, width 520. Each track spans that width.
+  // We'll measure actual rect at first tick.
+  let sliderRects = null;
+  function measureRects() {
+    const stageRect = stage.getBoundingClientRect();
+    const scale = stageRect.width / 1920;
+    const getTrackBox = (id) => {
+      const el = document.getElementById(id).parentElement; // .track
+      const r = el.getBoundingClientRect();
+      return {
+        left: (r.left - stageRect.left) / scale,
+        top:  (r.top  - stageRect.top)  / scale,
+        width:  r.width / scale,
+        height: r.height / scale,
+      };
+    };
+    sliderRects = {
+      s1: getTrackBox('knob1'),
+      s2: getTrackBox('knob2'),
+      s3: getTrackBox('knob3'),
+    };
+  }
+
+  function positionCursor(x, y, opacity) {
+    cursor.style.left = x + 'px';
+    cursor.style.top  = y + 'px';
+    cursor.style.opacity = opacity;
+  }
+
+  function knobLeft(id, pct) {
+    const el = document.getElementById(id);
+    el.style.left = pct + '%';
+  }
+  function fillWidth(id, pct) {
+    const el = document.getElementById(id);
+    el.style.width = pct + '%';
+  }
+
+  // Tick / render
+  let startTs = null;
+  let frameCount = 0;
+
+  function tick(ts) {
+    if (!startTs) startTs = ts;
+    const t = (ts - startTs) / 1000;
+
+    // Measure rects once
+    if (!sliderRects && frameCount > 1) {
+      measureRects();
     }
-    return <span key={i}>{w}</span>;
-  });
-}
-
-// ── Scene 4: Finale (17 – 20s) ────────────────────────────
-function Scene4_Final() {
-  const { elapsed } = useSprite();
-  const labelOp = interpolate(elapsed, [0.1, 0.7], [0, 1]);
-  const mainY = interpolate(elapsed, [0.2, 1.2], [30, 0], Easing.easeOut);
-  const mainOp = interpolate(elapsed, [0.2, 1.0], [0, 1]);
-  const lineW = interpolate(elapsed, [1.0, 1.8], [0, 540]);
-  const dimsOp = interpolate(elapsed, [1.3, 2.1], [0, 1]);
-
-  const dimensions = ['配色', '字型', '密度', '布局', '动画速度'];
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM,
-      display:'flex', alignItems:'center', justifyContent:'center',
-      flexDirection:'column'}}>
-      <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
-        color: TERRA, marginBottom: 26, opacity: labelOp}}>
-        TWEAKS · EVERYTHING IS A VARIABLE
-      </div>
-      <div style={{fontFamily: serif, fontSize: 108, fontWeight: 500,
-        color: INK, lineHeight: 1.05, letterSpacing:'-0.01em',
-        opacity: mainOp, transform: `translateY(${mainY}px)`,
-        textAlign:'center'}}>
-        一个源文件 · <span style={{fontStyle:'italic', color: TERRA}}>无限</span>变体
-      </div>
-      <div style={{height: 1, background: INK, width: lineW, marginTop: 38}} />
-
-      <div style={{marginTop: 36, display:'flex', gap: 10,
-        opacity: dimsOp, alignItems:'center'}}>
-        {dimensions.map((d, i) => (
-          <React.Fragment key={i}>
-            {i > 0 && (
-              <span style={{fontFamily: mono, fontSize: 14,
-                color: LINE, margin:'0 2px'}}>·</span>
-            )}
-            <span style={{fontFamily: mono, fontSize: 14,
-              letterSpacing:'0.2em',
-              color: i === 0 ? TERRA : ASH,
-              padding:'6px 14px',
-              border: `1px solid ${i === 0 ? TERRA : LINE}`}}>
-              {d}
-            </span>
-          </React.Fragment>
-        ))}
-      </div>
 
-      <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 20,
-        color: ASH, marginTop: 36, opacity: dimsOp,
-        maxWidth: 720, textAlign:'center', lineHeight: 1.5}}>
-        设计不是一次性的结果 ——<br/>
-        而是一组可以随时拨动的旋钮
-      </div>
-    </div>
-  );
-}
-
-// ── Watermark ─────────────────────────────────────────────
-function Watermark() {
-  return (
-    <div style={{position:'absolute', bottom: 24, right: 32,
-      fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
-      fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
-      Created by Huashu-Design
-    </div>
-  );
-}
-
-// ── Main composition ──────────────────────────────────────
-function App() {
-  return (
-    <Stage duration={20} width={1920} height={1080} bgColor={CREAM}>
-      <Sprite start={0}  end={3}><Scene1_Title /></Sprite>
-      <Sprite start={3}  end={12}><Scene2_MainStage /></Sprite>
-      <Sprite start={12} end={17}><Scene3_CodeView /></Sprite>
-      <Sprite start={17} end={20}><Scene4_Final /></Sprite>
-      <Watermark />
-    </Stage>
-  );
-}
-
-ReactDOM.createRoot(document.getElementById('root')).render(<App />);
+    // --- Stage in ---
+    const stageK = clampLerp(t, T.stage_in[0], T.stage_in[1]);
+    const stageOp = cubicOut(stageK);
+    preview.style.opacity = stageOp;
+    preview.style.transform = `rotateX(${lerp(t, T.stage_in[0], T.stage_in[1], 10, 6, cubicOut)}deg) rotateY(-4deg) translateY(${lerp(t, T.stage_in[0], T.stage_in[1], 20, 0, expoOut)}px)`;
+    panel.style.opacity = stageOp;
+    panel.style.transform = `translateX(${lerp(t, T.stage_in[0], T.stage_in[1], 30, 0, expoOut)}px)`;
+
+    // Anchor
+    const aK = clampLerp(t, T.anchor_in[0], T.anchor_in[1]);
+    anchor.style.opacity = cubicOut(aK);
+    anchor.style.transform = `translateY(${lerp(t, T.anchor_in[0], T.anchor_in[1], 10, 0, expoOut)}px)`;
+
+    // Snap point: when knob reaches target (72% of drag duration)
+    const s1SnapT = T.s1_drag[0] + (T.s1_drag[1] - T.s1_drag[0]) * 0.72;
+    const s2SnapT = T.s2_drag[0] + (T.s2_drag[1] - T.s2_drag[0]) * 0.72;
+    const s3SnapT = T.s3_drag[0] + (T.s3_drag[1] - T.s3_drag[0]) * 0.72;
+
+    // --- Slider 1: palette ---
+    // Knob 10% → 90%
+    const k1pct = knobMotion(t, T.s1_drag[0], T.s1_drag[1], 10, 90);
+    knobLeft('knob1', k1pct); fillWidth('fill1', k1pct);
+    setKnobState(knob1, t >= T.s1_cursor_to[0] && t < T.s1_settle[1] + 0.2);
+    if (t >= s1SnapT && state.palette !== 'cool') {
+      state.palette = 'cool'; updatePreview(); setValueLabel(val1, 'cool');
+    }
+
+    // --- Slider 2: type ---
+    const k2pct = knobMotion(t, T.s2_drag[0], T.s2_drag[1], 10, 90);
+    knobLeft('knob2', k2pct); fillWidth('fill2', k2pct);
+    setKnobState(knob2, t >= T.s2_cursor_to[0] && t < T.s2_settle[1] + 0.2);
+    if (t >= s2SnapT && state.type !== 'sans') {
+      state.type = 'sans'; updatePreview(); setValueLabel(val2, 'sans');
+    }
+
+    // --- Slider 3: density ---
+    const k3pct = knobMotion(t, T.s3_drag[0], T.s3_drag[1], 10, 90);
+    knobLeft('knob3', k3pct); fillWidth('fill3', k3pct);
+    setKnobState(knob3, t >= T.s3_cursor_to[0] && t < T.s3_settle[1] + 0.2);
+    if (t >= s3SnapT && state.density !== 'dense') {
+      state.density = 'dense'; updatePreview(); setValueLabel(val3, 'dense');
+    }
+
+    // --- Cursor choreography ---
+    if (sliderRects) {
+      const r1 = sliderRects.s1, r2 = sliderRects.s2, r3 = sliderRects.s3;
+      // Positions of knob at 10% and 90%
+      const k1Start = { x: r1.left + r1.width * 0.10, y: r1.top + r1.height/2 };
+      const k1End   = { x: r1.left + r1.width * 0.90, y: r1.top + r1.height/2 };
+      const k2Start = { x: r2.left + r2.width * 0.10, y: r2.top + r2.height/2 };
+      const k2End   = { x: r2.left + r2.width * 0.90, y: r2.top + r2.height/2 };
+      const k3Start = { x: r3.left + r3.width * 0.10, y: r3.top + r3.height/2 };
+      const k3End   = { x: r3.left + r3.width * 0.90, y: r3.top + r3.height/2 };
+
+      let cx = CURSOR_PARK.x, cy = CURSOR_PARK.y, co = 0;
+
+      if (t < T.s1_cursor_to[0]) {
+        // still off-screen (or just appeared)
+        cx = CURSOR_PARK.x; cy = CURSOR_PARK.y; co = 0;
+      } else if (t < T.s1_cursor_to[1]) {
+        // cursor flies to s1 knob start
+        const k = clampLerp(t, T.s1_cursor_to[0], T.s1_cursor_to[1]);
+        const e = cubicOut(k);
+        cx = lerp(t, T.s1_cursor_to[0], T.s1_cursor_to[1], CURSOR_PARK.x, k1Start.x, cubicOut);
+        cy = lerp(t, T.s1_cursor_to[0], T.s1_cursor_to[1], CURSOR_PARK.y, k1Start.y, cubicOut);
+        co = e;
+      } else if (t < T.s1_drag[1]) {
+        // dragging s1
+        cx = r1.left + (r1.width * k1pct / 100);
+        cy = r1.top + r1.height/2;
+        co = 1;
+      } else if (t < T.s2_cursor_to[0]) {
+        cx = k1End.x; cy = k1End.y; co = 1;
+      } else if (t < T.s2_cursor_to[1]) {
+        cx = lerp(t, T.s2_cursor_to[0], T.s2_cursor_to[1], k1End.x, k2Start.x, cubicInOut);
+        cy = lerp(t, T.s2_cursor_to[0], T.s2_cursor_to[1], k1End.y, k2Start.y, cubicInOut);
+        co = 1;
+      } else if (t < T.s2_drag[1]) {
+        cx = r2.left + (r2.width * k2pct / 100);
+        cy = r2.top + r2.height/2;
+        co = 1;
+      } else if (t < T.s3_cursor_to[0]) {
+        cx = k2End.x; cy = k2End.y; co = 1;
+      } else if (t < T.s3_cursor_to[1]) {
+        cx = lerp(t, T.s3_cursor_to[0], T.s3_cursor_to[1], k2End.x, k3Start.x, cubicInOut);
+        cy = lerp(t, T.s3_cursor_to[0], T.s3_cursor_to[1], k2End.y, k3Start.y, cubicInOut);
+        co = 1;
+      } else if (t < T.s3_drag[1]) {
+        cx = r3.left + (r3.width * k3pct / 100);
+        cy = r3.top + r3.height/2;
+        co = 1;
+      } else if (t < T.hold[1]) {
+        // fade out cursor
+        cx = k3End.x; cy = k3End.y;
+        co = lerp(t, T.s3_drag[1], T.hold[1], 1, 0, cubicOut);
+      }
+
+      positionCursor(cx, cy, co);
+    }
+
+    // --- Brand reveal (米色 walloff · aligned with hero-v10 signature) ---
+    // 1) Scene dimmer: composition fades to black (0.3s)
+    const soK = clampLerp(t, T.scene_out[0], T.scene_out[1]);
+    stageDimmer.style.opacity = cubicOut(soK);
+
+    // 2) Cream panel sweeps up from bottom, expoOut (0.4s)
+    const bpK = clampLerp(t, T.brand_panel[0], T.brand_panel[1]);
+    const panelY = lerp(t, T.brand_panel[0], T.brand_panel[1], 100, 0, expoOut);
+    brandPanel.style.transform = `translateY(${panelY}%)`;
+
+    // 3) Wordmark: font-weight 100→500 + y 20→0 + opacity 0→1, expoOut (0.6s)
+    const bmK = clampLerp(t, T.brand_mark[0], T.brand_mark[1]);
+    const bmE = expoOut(bmK);
+    const wght = 100 + (500 - 100) * bmE;
+    brandMark.style.opacity = bmE;
+    brandMark.style.transform = `translateY(${20 * (1 - bmE)}px)`;
+    brandMark.style.fontWeight = Math.round(wght);
+    brandMark.style.fontVariationSettings = `"wght" ${wght.toFixed(0)}`;
+
+    // 4) Orange line: width 0→280 from center, cubicOut (0.4s)
+    const blK = clampLerp(t, T.brand_line[0], T.brand_line[1]);
+    brandLine.style.width = (280 * cubicOut(blK)) + 'px';
+
+    frameCount++;
+
+    // Loop or stop
+    if (t < DURATION) {
+      requestAnimationFrame(tick);
+    } else {
+      if (window.__recording === true) {
+        // recording mode: hold last frame
+        return;
+      }
+      // Restart after 1s pause (for manual viewing)
+      setTimeout(() => {
+        startTs = null;
+        state = { palette: 'warm', type: 'serif', density: 'sparse' };
+        updatePreview();
+        setValueLabel(val1, 'warm'); setValueLabel(val2, 'serif'); setValueLabel(val3, 'sparse');
+        requestAnimationFrame(tick);
+      }, 900);
+    }
+  }
+
+  // Start animation after fonts ready
+  const startAnim = () => {
+    requestAnimationFrame((ts) => {
+      startTs = ts;
+      window.__ready = true; // signal for render-video.js
+      requestAnimationFrame(tick);
+    });
+  };
+
+  if (document.fonts && document.fonts.ready) {
+    document.fonts.ready.then(startAnim);
+  } else {
+    setTimeout(startAnim, 500);
+  }
+})();
 </script>
+
 </body>
 </html>

+ 816 - 0
demos/c5-infographic-en.html

@@ -0,0 +1,816 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8" />
+<title>c5-infographic · Data → Typography (EN)</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<style>
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --dim: rgba(255,255,255,0.18);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;
+    --accent-deep: #B85D3D;
+
+    /* Brand Reveal */
+    --cd-bg: #F5F4F0;
+    --cd-panel: #FFFFFF;
+    --cd-ink: #1A1918;
+    --cd-dim: #8B867E;
+
+    --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
+    --serif-cn: "Noto Serif SC", "Songti SC", "Source Han Serif SC", serif;
+    --sans: "Inter", -apple-system, "PingFang SC", system-ui, sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+    font-feature-settings: "kern" 1, "liga" 1, "calt" 1;
+  }
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+    /* Subtle film grain via SVG — 2% opacity */
+    background-image:
+      radial-gradient(ellipse at 20% 30%, rgba(217,119,87,0.025), transparent 50%),
+      radial-gradient(ellipse at 80% 70%, rgba(217,119,87,0.018), transparent 55%);
+  }
+
+  .watermark {
+    position: absolute;
+    top: 40px; left: 48px;
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.2em;
+    color: var(--ink);
+    opacity: 0.16;
+    text-transform: uppercase;
+    z-index: 400;
+    transition: color 0.3s ease;
+  }
+  .watermark.on-light { color: var(--cd-ink); opacity: 0.35; }
+
+  .v2-mark {
+    position: absolute;
+    bottom: 40px; right: 48px;
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.2em;
+    color: var(--ink);
+    opacity: 0.16;
+    z-index: 400;
+  }
+
+  /* ============ Split layout ============ */
+  .split-left {
+    position: absolute;
+    left: 120px; top: 50%;
+    transform: translateY(-50%);
+    width: 440px;
+    will-change: opacity, transform;
+  }
+
+  .json-block {
+    font-family: var(--mono);
+    font-size: 15px;
+    line-height: 1.75;
+    color: var(--ink-60);
+    letter-spacing: 0.01em;
+    white-space: pre;
+  }
+  .json-block .k { color: var(--ink-80); }
+  .json-block .s { color: var(--accent); }
+  .json-block .n { color: var(--ink); font-weight: 500; }
+  .json-block .p { color: var(--muted); }
+
+  .json-label {
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.28em;
+    color: var(--muted);
+    text-transform: uppercase;
+    margin-bottom: 22px;
+  }
+
+  /* Pipe arrow from JSON → infographic */
+  .pipe {
+    position: absolute;
+    left: 580px; top: 50%;
+    transform: translateY(-50%);
+    width: 90px; height: 2px;
+    background: linear-gradient(to right, var(--hairline), var(--accent), var(--hairline));
+    opacity: 0;
+    will-change: opacity;
+  }
+  .pipe::after {
+    content: '';
+    position: absolute;
+    right: -4px; top: 50%;
+    transform: translateY(-50%) rotate(45deg);
+    width: 8px; height: 8px;
+    border-right: 2px solid var(--accent);
+    border-top: 2px solid var(--accent);
+  }
+
+  /* ============ Infographic (right side) ============ */
+  .infographic {
+    position: absolute;
+    right: 100px; top: 72px;
+    width: 1120px; height: 936px;
+    background: #0A0A0A;
+    border: 1px solid var(--hairline);
+    padding: 56px 64px;
+    opacity: 0;
+    transform: translateY(18px);
+    will-change: opacity, transform;
+    overflow: hidden;
+  }
+
+  .ig-masthead {
+    display: flex;
+    justify-content: space-between;
+    align-items: baseline;
+    border-bottom: 1px solid var(--hairline);
+    padding-bottom: 20px;
+    margin-bottom: 36px;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .ig-masthead .issue {
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.3em;
+    color: var(--muted);
+    text-transform: uppercase;
+  }
+  .ig-masthead .issue .orange { color: var(--accent); }
+  .ig-masthead .dept {
+    font-family: var(--mono);
+    font-weight: 400;
+    font-size: 10px;
+    letter-spacing: 0.3em;
+    color: var(--ink-60);
+    text-transform: uppercase;
+  }
+
+  .ig-display {
+    font-family: var(--serif-en);
+    font-weight: 300;
+    font-size: 96px;
+    line-height: 1.0;
+    letter-spacing: -0.025em;
+    color: var(--ink);
+    margin-bottom: 6px;
+    opacity: 0;
+    will-change: opacity, transform;
+    text-wrap: pretty;
+    font-feature-settings: "liga" 1, "dlig" 1, "kern" 1;
+  }
+  .ig-display .en {
+    font-family: var(--serif-en);
+    font-style: italic;
+    font-weight: 300;
+    color: var(--accent);
+    font-feature-settings: "liga" 1, "dlig" 1, "swsh" 1;
+  }
+
+  .ig-deck {
+    font-family: var(--serif-en);
+    font-style: italic;
+    font-weight: 300;
+    font-size: 22px;
+    color: var(--ink-60);
+    letter-spacing: 0.01em;
+    margin-bottom: 44px;
+    opacity: 0;
+    will-change: opacity;
+    font-feature-settings: "liga" 1, "dlig" 1;
+  }
+
+  /* Grid of 5 stats */
+  .ig-grid {
+    display: grid;
+    grid-template-columns: 1.3fr 1fr 1fr 1fr;
+    gap: 32px;
+    margin-bottom: 44px;
+  }
+  .ig-cell {
+    opacity: 0;
+    will-change: opacity, transform;
+    border-top: 2px solid var(--ink);
+    padding-top: 14px;
+  }
+  .ig-cell.accent { border-top-color: var(--accent); }
+  .ig-cell .label {
+    font-family: var(--mono);
+    font-size: 10px;
+    font-weight: 400;
+    color: var(--muted);
+    letter-spacing: 0.26em;
+    margin-bottom: 14px;
+    text-transform: uppercase;
+  }
+  .ig-cell .label .en {
+    font-family: var(--mono);
+    text-transform: uppercase;
+    letter-spacing: 0.26em;
+  }
+  .ig-cell .big {
+    font-family: var(--serif-en);
+    font-weight: 300;
+    font-size: 72px;
+    line-height: 0.92;
+    color: var(--ink);
+    letter-spacing: -0.03em;
+    font-variant-numeric: oldstyle-nums proportional-nums;
+    font-feature-settings: "onum" 1, "pnum" 1, "kern" 1;
+  }
+  .ig-cell.accent .big { color: var(--accent); }
+  .ig-cell .big .unit {
+    font-size: 28px;
+    color: var(--ink-60);
+    letter-spacing: 0;
+  }
+  .ig-cell .sub {
+    margin-top: 12px;
+    font-family: var(--serif-en);
+    font-style: italic;
+    font-size: 14px;
+    color: var(--ink-60);
+    line-height: 1.4;
+    font-feature-settings: "liga" 1, "dlig" 1;
+    letter-spacing: 0.005em;
+  }
+
+  /* Comparison bars */
+  .ig-bars {
+    display: grid;
+    grid-template-columns: 140px 1fr 80px;
+    gap: 18px 24px;
+    row-gap: 18px;
+    border-top: 1px solid var(--hairline);
+    padding-top: 28px;
+    align-items: center;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .ig-bars .row-label {
+    font-family: var(--serif-en);
+    font-size: 16px;
+    font-weight: 400;
+    color: var(--ink-80);
+    letter-spacing: 0.005em;
+  }
+  .ig-bars .row-label.highlight { color: var(--accent); font-weight: 500; }
+  .ig-bars .row-bar {
+    height: 6px;
+    background: var(--hairline);
+    position: relative;
+    overflow: hidden;
+  }
+  .ig-bars .row-bar .fill {
+    position: absolute;
+    left: 0; top: 0; bottom: 0;
+    background: var(--ink-80);
+    width: 0%;
+    will-change: width;
+  }
+  .ig-bars .row-bar .fill.accent { background: var(--accent); }
+  .ig-bars .row-val {
+    font-family: var(--serif-en);
+    font-size: 16px;
+    color: var(--ink);
+    text-align: right;
+    font-variant-numeric: oldstyle-nums tabular-nums;
+    font-feature-settings: "onum" 1, "tnum" 1;
+    letter-spacing: 0.01em;
+  }
+
+  .ig-footer {
+    position: absolute;
+    bottom: 40px; left: 64px; right: 64px;
+    display: flex; justify-content: space-between; align-items: baseline;
+    border-top: 1px solid var(--hairline);
+    padding-top: 16px;
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.24em;
+    color: var(--muted);
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .ig-footer .folio { color: var(--ink-60); letter-spacing: 0.32em; }
+
+  /* ============ Typography detail zoom ============ */
+  .detail-zoom {
+    position: absolute;
+    inset: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    opacity: 0;
+    will-change: opacity;
+    background: radial-gradient(ellipse at center, #0A0A0A, #000000);
+    z-index: 250;
+  }
+  .detail-word {
+    font-family: var(--serif-en);
+    font-weight: 300;
+    font-style: italic;
+    font-size: 320px;
+    line-height: 0.9;
+    letter-spacing: -0.01em;
+    color: var(--ink);
+    /* Enable OpenType ligatures, discretionary ligatures, swashes */
+    font-feature-settings: "liga" 1, "dlig" 1, "swsh" 1, "salt" 1, "calt" 1;
+    text-rendering: optimizeLegibility;
+    will-change: transform, opacity;
+  }
+  .detail-word .fi {
+    /* fi ligature is default with "liga" */
+    color: var(--accent);
+  }
+  .detail-annotation {
+    position: absolute;
+    top: calc(50% + 170px); left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.28em;
+    color: var(--muted);
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity;
+    white-space: nowrap;
+  }
+  .detail-annotation .dot {
+    color: var(--accent);
+    padding: 0 8px;
+  }
+
+  /* Callout lines pointing to ligature */
+  .callout {
+    position: absolute;
+    left: 50%; top: 50%;
+    transform: translate(-50%, -50%);
+    pointer-events: none;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .callout svg { overflow: visible; display: block; }
+
+  /* ============ Brand Reveal ============ */
+  .brand-wall {
+    position: absolute;
+    inset: 0;
+    background: var(--cd-bg);
+    z-index: 300;
+    opacity: 0;
+    transform: translateY(100%);
+    will-change: transform, opacity;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+  }
+  .brand-wordmark {
+    font-family: var(--serif-en);
+    font-size: 132px;
+    font-weight: 200;
+    color: var(--cd-ink);
+    letter-spacing: -0.04em;
+    line-height: 1;
+    opacity: 0;
+    transform: scale(0.92);
+    will-change: opacity, transform;
+    font-feature-settings: "liga" 1, "dlig" 1;
+  }
+  .brand-wordmark .dot { color: var(--accent); padding: 0 10px; font-weight: 300; }
+  .brand-underline {
+    margin-top: 28px;
+    height: 2px;
+    width: 0;
+    background: var(--accent);
+    will-change: width;
+  }
+  .brand-cn {
+    margin-top: 30px;
+    font-family: var(--serif-cn);
+    font-size: 18px;
+    font-weight: 300;
+    color: var(--cd-dim);
+    letter-spacing: 0.4em;
+    opacity: 0;
+    will-change: opacity;
+  }
+
+</style>
+</head>
+<body>
+
+<div class="stage" id="stage">
+
+  <div class="watermark" id="watermark">HUASHU · DESIGN</div>
+  <div class="v2-mark">V2 · 2026</div>
+
+  <!-- Left: JSON data -->
+  <div class="split-left" id="splitLeft" style="opacity:0">
+    <div class="json-label" id="jsonLabel">DATA &#8594; benchmarks.json</div>
+    <pre class="json-block" id="jsonBlock"></pre>
+  </div>
+
+  <!-- Pipe arrow -->
+  <div class="pipe" id="pipe"></div>
+
+  <!-- Right: Infographic -->
+  <div class="infographic" id="infographic">
+
+    <div class="ig-masthead" id="igMasthead">
+      <div class="issue">Issue &#8470; 05 <span class="orange">&#183; AI Benchmarks</span> &#183; Q2 2026</div>
+      <div class="dept">FRONTIER REPORT</div>
+    </div>
+
+    <h1 class="ig-display" id="igDisplay">
+      The Age of<br>
+      <span class="en">benchmarks</span>.
+    </h1>
+
+    <p class="ig-deck" id="igDeck">
+      Five frontier models, five numbers, one uncomfortable truth.
+    </p>
+
+    <div class="ig-grid" id="igGrid">
+      <div class="ig-cell accent" data-cell="0">
+        <div class="label">Leader <span class="en">&#183; Q2</span></div>
+        <div class="big">Claude 4.7</div>
+        <div class="sub">Sonnet, 1M ctx &#183; Anthropic</div>
+      </div>
+      <div class="ig-cell" data-cell="1">
+        <div class="label"><span class="en">SWE-bench</span></div>
+        <div class="big">77<span class="unit">.2%</span></div>
+        <div class="sub">coding, verified split</div>
+      </div>
+      <div class="ig-cell" data-cell="2">
+        <div class="label"><span class="en">GPQA</span></div>
+        <div class="big">84<span class="unit">.5</span></div>
+        <div class="sub">diamond, graduate science</div>
+      </div>
+      <div class="ig-cell" data-cell="3">
+        <div class="label">Price <span class="en">&#183; input</span></div>
+        <div class="big">$3<span class="unit">/M</span></div>
+        <div class="sub">per million tokens, typical</div>
+      </div>
+    </div>
+
+    <div class="ig-bars" id="igBars">
+      <div class="row-label highlight">Claude 4.7 Sonnet</div>
+      <div class="row-bar"><div class="fill accent" data-w="77.2"></div></div>
+      <div class="row-val">77.2</div>
+
+      <div class="row-label">GPT-5 Turbo</div>
+      <div class="row-bar"><div class="fill" data-w="74.8"></div></div>
+      <div class="row-val">74.8</div>
+
+      <div class="row-label">Gemini 3 Pro</div>
+      <div class="row-bar"><div class="fill" data-w="71.3"></div></div>
+      <div class="row-val">71.3</div>
+
+      <div class="row-label">GLM-5</div>
+      <div class="row-bar"><div class="fill" data-w="68.9"></div></div>
+      <div class="row-val">68.9</div>
+
+      <div class="row-label">Kimi k3</div>
+      <div class="row-bar"><div class="fill" data-w="66.4"></div></div>
+      <div class="row-val">66.4</div>
+    </div>
+
+    <div class="ig-footer" id="igFooter">
+      <span>Set in Source Serif 4 &amp; JetBrains Mono</span>
+      <span class="folio">P. 05</span>
+      <span>Data &#183; 2026 Q2, public benchmarks</span>
+    </div>
+
+  </div>
+
+  <!-- Detail zoom: Typography ligature -->
+  <div class="detail-zoom" id="detailZoom">
+    <div class="detail-word" id="detailWord">bench<span class="fi">ma</span>rks</div>
+    <div class="callout" id="callout" style="display:none"></div>
+    <div class="detail-annotation" id="detailAnnotation">
+      SOURCE SERIF 4 <span class="dot">·</span> ITALIC <span class="dot">·</span> OLDSTYLE FIGURES
+    </div>
+  </div>
+
+  <!-- Brand Reveal -->
+  <div class="brand-wall" id="brandWall">
+    <div class="brand-wordmark" id="brandWord">huashu<span class="dot">·</span>design</div>
+    <div class="brand-underline" id="brandLine"></div>
+    <div class="brand-cn" id="brandCn">D A T A &#183; T Y P O G R A P H Y</div>
+  </div>
+
+</div>
+
+<script>
+(() => {
+  'use strict';
+
+  // ---------- Scale stage to viewport ----------
+  const stage = document.getElementById('stage');
+  function fitStage() {
+    const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  fitStage();
+  window.addEventListener('resize', fitStage);
+
+  // ---------- Easing ----------
+  const expoOut = t => t >= 1 ? 1 : 1 - Math.pow(2, -10 * t);
+  const expoIn = t => t <= 0 ? 0 : Math.pow(2, 10 * (t - 1));
+  const cubicOut = t => 1 - Math.pow(1 - t, 3);
+  const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;
+  const lerp = (t, a, b, c, d, ease=x=>x) => {
+    if (b === a) return c;
+    const k = Math.max(0, Math.min(1, (t - a) / (b - a)));
+    return c + (d - c) * ease(k);
+  };
+  const seg = (t, a, b) => Math.max(0, Math.min(1, (t - a) / (b - a)));
+
+  // ---------- Refs ----------
+  const splitLeft = document.getElementById('splitLeft');
+  const jsonLabel = document.getElementById('jsonLabel');
+  const jsonBlock = document.getElementById('jsonBlock');
+  const pipe = document.getElementById('pipe');
+  const infographic = document.getElementById('infographic');
+  const igMasthead = document.getElementById('igMasthead');
+  const igDisplay = document.getElementById('igDisplay');
+  const igDeck = document.getElementById('igDeck');
+  const igGrid = document.getElementById('igGrid');
+  const igCells = igGrid.querySelectorAll('.ig-cell');
+  const igBars = document.getElementById('igBars');
+  const igBarFills = igBars.querySelectorAll('.fill');
+  const igFooter = document.getElementById('igFooter');
+  const detailZoom = document.getElementById('detailZoom');
+  const detailWord = document.getElementById('detailWord');
+  const detailAnnotation = document.getElementById('detailAnnotation');
+  const callout = document.getElementById('callout');
+  const brandWall = document.getElementById('brandWall');
+  const brandWord = document.getElementById('brandWord');
+  const brandLine = document.getElementById('brandLine');
+  const brandCn = document.getElementById('brandCn');
+  const watermark = document.getElementById('watermark');
+
+  // ---------- JSON content (for progressive reveal) ----------
+  const jsonRaw = [
+    '{',
+    '  "issue": "2026-Q2",',
+    '  "leader": "Claude 4.7",',
+    '  "models": [',
+    '    { "name": "Claude 4.7",   "swe": 77.2 },',
+    '    { "name": "GPT-5 Turbo",  "swe": 74.8 },',
+    '    { "name": "Gemini 3 Pro", "swe": 71.3 },',
+    '    { "name": "GLM-5",        "swe": 68.9 },',
+    '    { "name": "Kimi k3",      "swe": 66.4 }',
+    '  ],',
+    '  "gpqa_top": 84.5,',
+    '  "price_per_M": 3',
+    '}'
+  ];
+
+  function formatJson(lines) {
+    return lines.map(line => {
+      return line
+        .replace(/"([a-zA-Z_]+)":/g, '<span class="k">"$1"</span>:')
+        .replace(/: "([^"]+)"/g, ': <span class="s">"$1"</span>')
+        .replace(/: ([0-9.]+)/g, ': <span class="n">$1</span>')
+        .replace(/([{}\[\],])/g, '<span class="p">$1</span>');
+    }).join('\n');
+  }
+
+  // ---------- Timeline ----------
+  const DURATION = 10.0;
+
+  // SFX cue points (played back in ffmpeg post-processing, not browser):
+  //   t=0.35  → keyboard/type-fast.mp3  (data entering)
+  //   t=2.15  → container/card-snap.mp3 (infographic settles)
+  //   t=6.75  → transition/whoosh-fast.mp3 (zoom-in to typography)
+  //   t=8.70  → impact/logo-reveal.mp3   (brand reveal chime)
+  const sfxFired = new Set();
+  function fireOnce(key) {
+    if (sfxFired.has(key)) return;
+    sfxFired.add(key);
+    // cue emitted for post-processing; no in-browser playback
+  }
+
+  let startTime = null;
+  let raf;
+
+  function tick(now) {
+    if (startTime == null) startTime = now;
+    const t = (now - startTime) / 1000;
+
+    // ── Beat 1: 0-2s · JSON data appears, types in ─────────
+    // JSON label fade in
+    {
+      const k = cubicOut(seg(t, 0.15, 0.55));
+      jsonLabel.style.opacity = k;
+      splitLeft.style.opacity = '1';
+    }
+    // Progressive type-reveal: reveal N lines of JSON by time
+    {
+      const totalLines = jsonRaw.length;
+      const k = seg(t, 0.3, 1.9);
+      const linesShown = Math.floor(k * totalLines);
+      const shown = jsonRaw.slice(0, Math.max(0, linesShown));
+      jsonBlock.innerHTML = formatJson(shown);
+      if (linesShown >= 3 && t < 1.9) fireOnce('datain');
+    }
+
+    // ── Pipe arrow (1.8 → 2.2) ─────────────────────────────
+    {
+      const k = cubicOut(seg(t, 1.8, 2.2));
+      pipe.style.opacity = k;
+    }
+
+    // ── Beat 2a: 2.0-3.2s · Infographic canvas arrives ─────
+    {
+      const k = expoOut(seg(t, 2.0, 2.8));
+      infographic.style.opacity = k;
+      infographic.style.transform = `translateY(${lerp(t, 2.0, 2.8, 18, 0, expoOut)}px)`;
+      if (t > 2.1) fireOnce('settle');
+    }
+    // Masthead
+    {
+      const k = cubicOut(seg(t, 2.6, 3.1));
+      igMasthead.style.opacity = k;
+    }
+
+    // ── Beat 2b: 3.0-4.2s · Display headline appears ──────
+    {
+      const k = expoOut(seg(t, 3.0, 3.8));
+      igDisplay.style.opacity = k;
+      igDisplay.style.transform = `translateY(${lerp(t, 3.0, 3.8, 16, 0, expoOut)}px)`;
+    }
+    // Deck line (italic)
+    {
+      const k = cubicOut(seg(t, 3.6, 4.2));
+      igDeck.style.opacity = k;
+    }
+
+    // ── Beat 2c: 4.0-5.2s · Grid cells (ripple, 4 cells) ──
+    igCells.forEach((cell, i) => {
+      const start = 4.0 + i * 0.12;
+      const end = start + 0.5;
+      const k = expoOut(seg(t, start, end));
+      cell.style.opacity = k;
+      cell.style.transform = `translateY(${lerp(t, start, end, 14, 0, expoOut)}px)`;
+    });
+
+    // ── Beat 2d: 5.2-6.4s · Comparison bars grow ─────────
+    {
+      const k = cubicOut(seg(t, 5.1, 5.4));
+      igBars.style.opacity = k;
+    }
+    igBarFills.forEach((fill, i) => {
+      const start = 5.3 + i * 0.08;
+      const end = start + 0.7;
+      const w = parseFloat(fill.getAttribute('data-w'));
+      const pct = lerp(t, start, end, 0, w, expoOut);
+      fill.style.width = pct + '%';
+    });
+    // Footer
+    {
+      const k = cubicOut(seg(t, 6.0, 6.6));
+      igFooter.style.opacity = k * 0.9;
+    }
+
+    // ── Beat 2e: 6.6-8.2s · Zoom to typography detail ────
+    if (t >= 6.6 && t < 8.3) {
+      const k = expoOut(seg(t, 6.6, 7.4));
+      // Infographic scales up and fades — simulate push-in
+      const scale = lerp(t, 6.6, 7.4, 1, 3.4, expoOut);
+      const ty = lerp(t, 6.6, 7.4, 0, -140, expoOut);
+      infographic.style.transform = `translateY(${ty}px) scale(${scale})`;
+      infographic.style.opacity = String(1 - k * 0.85);
+      splitLeft.style.opacity = String(1 - k);
+      pipe.style.opacity = String(1 - k);
+
+      // Detail zoom fades in
+      const k2 = expoOut(seg(t, 7.0, 7.7));
+      detailZoom.style.opacity = k2;
+      // Word subtle scale-in (starts from 0.96)
+      detailWord.style.transform = `scale(${lerp(t, 7.0, 7.9, 0.96, 1.0, expoOut)})`;
+
+      // SFX at 6.7
+      if (t > 6.7) fireOnce('zoom');
+
+      // Callout + annotation (7.5 → 8.1)
+      const k3 = cubicOut(seg(t, 7.6, 8.1));
+      callout.style.opacity = k3;
+      detailAnnotation.style.opacity = k3;
+    }
+
+    // ── Beat 3: 8.2-10s · Brand reveal ───────────────────
+    // Detail zoom fades under brand wall
+    if (t >= 8.1) {
+      const k = cubicOut(seg(t, 8.1, 8.5));
+      detailZoom.style.opacity = String(Math.max(0, 1 - k));
+    }
+    // Brand wall slides up from bottom
+    {
+      const k = expoOut(seg(t, 8.1, 8.7));
+      brandWall.style.transform = `translateY(${lerp(t, 8.1, 8.7, 100, 0, expoOut)}%)`;
+      brandWall.style.opacity = k > 0 ? '1' : '0';
+      if (k > 0.55) watermark.classList.add('on-light');
+      else watermark.classList.remove('on-light');
+    }
+    // Wordmark
+    {
+      const k = expoOut(seg(t, 8.6, 9.2));
+      brandWord.style.opacity = k;
+      brandWord.style.transform = `scale(${lerp(t, 8.6, 9.2, 0.92, 1.0, expoOut)})`;
+      if (t > 8.65) fireOnce('chime');
+    }
+    // Underline
+    {
+      const k = expoOut(seg(t, 9.0, 9.6));
+      brandLine.style.width = (280 * k) + 'px';
+    }
+    // CN tagline
+    {
+      const k = cubicOut(seg(t, 9.3, 9.9));
+      brandCn.style.opacity = k * 0.9;
+    }
+
+    // Loop / hold
+    if (t < DURATION) {
+      raf = requestAnimationFrame(tick);
+    } else {
+      if (!window.__recording) {
+        setTimeout(() => {
+          // Reset
+          startTime = null;
+          sfxFired.clear();
+          jsonBlock.innerHTML = '';
+          splitLeft.style.opacity = '0';
+          pipe.style.opacity = '0';
+          infographic.style.opacity = '0';
+          infographic.style.transform = 'translateY(18px) scale(1)';
+          igMasthead.style.opacity = '0';
+          igDisplay.style.opacity = '0';
+          igDeck.style.opacity = '0';
+          igBars.style.opacity = '0';
+          igFooter.style.opacity = '0';
+          igCells.forEach(c => { c.style.opacity = '0'; });
+          igBarFills.forEach(f => { f.style.width = '0%'; });
+          detailZoom.style.opacity = '0';
+          callout.style.opacity = '0';
+          detailAnnotation.style.opacity = '0';
+          brandWall.style.transform = 'translateY(100%)';
+          brandWall.style.opacity = '0';
+          brandWord.style.opacity = '0';
+          brandLine.style.width = '0';
+          brandCn.style.opacity = '0';
+          watermark.classList.remove('on-light');
+          raf = requestAnimationFrame(tick);
+        }, 800);
+      }
+    }
+  }
+
+  window.__seek = function(s) {
+    startTime = performance.now() - s * 1000;
+  };
+
+  // Wait for fonts, then start
+  (document.fonts ? document.fonts.ready : Promise.resolve()).then(() => {
+    requestAnimationFrame((now) => {
+      startTime = now;
+      window.__ready = true;
+      raf = requestAnimationFrame(tick);
+    });
+  });
+})();
+</script>
+
+</body>
+</html>

+ 781 - 735
demos/c5-infographic.html

@@ -1,767 +1,813 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
+<!doctype html>
+<html lang="zh-Hans">
 <head>
-<meta charset="UTF-8">
-<title>Huashu-Design · Infographic Demo</title>
-<script crossorigin src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
-<script crossorigin src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
-<script src="https://unpkg.com/@babel/standalone@7.25.6/babel.min.js"></script>
+<meta charset="utf-8" />
+<title>c5-infographic · 数据 → 印刷级排版(中文版)</title>
 <link rel="preconnect" href="https://fonts.googleapis.com">
 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,300;1,6..72,400;1,6..72,500&family=Noto+Serif+SC:wght@300;400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
 <style>
-  * { box-sizing: border-box; margin: 0; padding: 0; }
-  html, body { width: 100%; height: 100%; overflow: hidden; }
-  body {
-    background: #0c0c0c;
-    font-family: 'Newsreader', 'Noto Serif SC', Georgia, serif;
-    color: #1a1a1a;
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --dim: rgba(255,255,255,0.18);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;
+    --accent-deep: #B85D3D;
+
+    /* Brand Reveal */
+    --cd-bg: #F5F4F0;
+    --cd-panel: #FFFFFF;
+    --cd-ink: #1A1918;
+    --cd-dim: #8B867E;
+
+    --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
+    --serif-cn: "Noto Serif SC", "Songti SC", "Source Han Serif SC", serif;
+    --sans: "Inter", -apple-system, "PingFang SC", system-ui, sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
     -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+    font-feature-settings: "kern" 1, "liga" 1, "calt" 1;
+  }
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+    /* Subtle film grain via SVG — 2% opacity */
+    background-image:
+      radial-gradient(ellipse at 20% 30%, rgba(217,119,87,0.025), transparent 50%),
+      radial-gradient(ellipse at 80% 70%, rgba(217,119,87,0.018), transparent 55%);
+  }
+
+  .watermark {
+    position: absolute;
+    top: 40px; left: 48px;
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.2em;
+    color: var(--ink);
+    opacity: 0.16;
+    text-transform: uppercase;
+    z-index: 400;
+    transition: color 0.3s ease;
+  }
+  .watermark.on-light { color: var(--cd-ink); opacity: 0.35; }
+
+  .v2-mark {
+    position: absolute;
+    bottom: 40px; right: 48px;
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.2em;
+    color: var(--ink);
+    opacity: 0.16;
+    z-index: 400;
+  }
+
+  /* ============ Split layout ============ */
+  .split-left {
+    position: absolute;
+    left: 120px; top: 50%;
+    transform: translateY(-50%);
+    width: 440px;
+    will-change: opacity, transform;
+  }
+
+  .json-block {
+    font-family: var(--mono);
+    font-size: 15px;
+    line-height: 1.75;
+    color: var(--ink-60);
+    letter-spacing: 0.01em;
+    white-space: pre;
+  }
+  .json-block .k { color: var(--ink-80); }
+  .json-block .s { color: var(--accent); }
+  .json-block .n { color: var(--ink); font-weight: 500; }
+  .json-block .p { color: var(--muted); }
+
+  .json-label {
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.28em;
+    color: var(--muted);
+    text-transform: uppercase;
+    margin-bottom: 22px;
+  }
+
+  /* Pipe arrow from JSON → infographic */
+  .pipe {
+    position: absolute;
+    left: 580px; top: 50%;
+    transform: translateY(-50%);
+    width: 90px; height: 2px;
+    background: linear-gradient(to right, var(--hairline), var(--accent), var(--hairline));
+    opacity: 0;
+    will-change: opacity;
+  }
+  .pipe::after {
+    content: '';
+    position: absolute;
+    right: -4px; top: 50%;
+    transform: translateY(-50%) rotate(45deg);
+    width: 8px; height: 8px;
+    border-right: 2px solid var(--accent);
+    border-top: 2px solid var(--accent);
+  }
+
+  /* ============ Infographic (right side) ============ */
+  .infographic {
+    position: absolute;
+    right: 100px; top: 72px;
+    width: 1120px; height: 936px;
+    background: #0A0A0A;
+    border: 1px solid var(--hairline);
+    padding: 56px 64px;
+    opacity: 0;
+    transform: translateY(18px);
+    will-change: opacity, transform;
+    overflow: hidden;
+  }
+
+  .ig-masthead {
+    display: flex;
+    justify-content: space-between;
+    align-items: baseline;
+    border-bottom: 1px solid var(--hairline);
+    padding-bottom: 20px;
+    margin-bottom: 36px;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .ig-masthead .issue {
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.3em;
+    color: var(--muted);
+    text-transform: uppercase;
+  }
+  .ig-masthead .issue .orange { color: var(--accent); }
+  .ig-masthead .dept {
+    font-family: var(--serif-cn);
+    font-weight: 300;
+    font-size: 14px;
+    letter-spacing: 0.35em;
+    color: var(--ink-60);
+  }
+
+  .ig-display {
+    font-family: var(--serif-cn);
+    font-weight: 400;
+    font-size: 84px;
+    line-height: 1.02;
+    letter-spacing: -0.01em;
+    color: var(--ink);
+    margin-bottom: 6px;
+    opacity: 0;
+    will-change: opacity, transform;
+    text-wrap: pretty;
+  }
+  .ig-display .en {
+    font-family: var(--serif-en);
+    font-style: italic;
+    font-weight: 300;
+    color: var(--accent);
+    font-feature-settings: "liga" 1, "dlig" 1, "swsh" 1;
+  }
+
+  .ig-deck {
+    font-family: var(--serif-en);
+    font-style: italic;
+    font-weight: 300;
+    font-size: 22px;
+    color: var(--ink-60);
+    letter-spacing: 0.01em;
+    margin-bottom: 44px;
+    opacity: 0;
+    will-change: opacity;
+    font-feature-settings: "liga" 1, "dlig" 1;
+  }
+
+  /* Grid of 5 stats */
+  .ig-grid {
+    display: grid;
+    grid-template-columns: 1.3fr 1fr 1fr 1fr;
+    gap: 32px;
+    margin-bottom: 44px;
+  }
+  .ig-cell {
+    opacity: 0;
+    will-change: opacity, transform;
+    border-top: 2px solid var(--ink);
+    padding-top: 14px;
+  }
+  .ig-cell.accent { border-top-color: var(--accent); }
+  .ig-cell .label {
+    font-family: var(--serif-cn);
+    font-size: 12px;
+    font-weight: 300;
+    color: var(--muted);
+    letter-spacing: 0.22em;
+    margin-bottom: 14px;
+  }
+  .ig-cell .label .en {
+    font-family: var(--mono);
+    text-transform: uppercase;
+    letter-spacing: 0.26em;
+  }
+  .ig-cell .big {
+    font-family: var(--serif-en);
+    font-weight: 300;
+    font-size: 72px;
+    line-height: 0.92;
+    color: var(--ink);
+    letter-spacing: -0.03em;
+    font-variant-numeric: oldstyle-nums proportional-nums;
+    font-feature-settings: "onum" 1, "pnum" 1, "kern" 1;
+  }
+  .ig-cell.accent .big { color: var(--accent); }
+  .ig-cell .big .unit {
+    font-size: 28px;
+    color: var(--ink-60);
+    letter-spacing: 0;
+  }
+  .ig-cell .sub {
+    margin-top: 12px;
+    font-family: var(--serif-en);
+    font-style: italic;
+    font-size: 14px;
+    color: var(--ink-60);
+    line-height: 1.4;
+    font-feature-settings: "liga" 1, "dlig" 1;
+    letter-spacing: 0.005em;
+  }
+
+  /* Comparison bars */
+  .ig-bars {
+    display: grid;
+    grid-template-columns: 140px 1fr 80px;
+    gap: 18px 24px;
+    row-gap: 18px;
+    border-top: 1px solid var(--hairline);
+    padding-top: 28px;
+    align-items: center;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .ig-bars .row-label {
+    font-family: var(--serif-cn);
+    font-size: 15px;
+    font-weight: 400;
+    color: var(--ink-80);
+    letter-spacing: 0.02em;
+  }
+  .ig-bars .row-label.highlight { color: var(--accent); font-weight: 500; }
+  .ig-bars .row-bar {
+    height: 6px;
+    background: var(--hairline);
+    position: relative;
+    overflow: hidden;
+  }
+  .ig-bars .row-bar .fill {
+    position: absolute;
+    left: 0; top: 0; bottom: 0;
+    background: var(--ink-80);
+    width: 0%;
+    will-change: width;
+  }
+  .ig-bars .row-bar .fill.accent { background: var(--accent); }
+  .ig-bars .row-val {
+    font-family: var(--serif-en);
+    font-size: 16px;
+    color: var(--ink);
+    text-align: right;
+    font-variant-numeric: oldstyle-nums tabular-nums;
+    font-feature-settings: "onum" 1, "tnum" 1;
+    letter-spacing: 0.01em;
+  }
+
+  .ig-footer {
+    position: absolute;
+    bottom: 40px; left: 64px; right: 64px;
+    display: flex; justify-content: space-between; align-items: baseline;
+    border-top: 1px solid var(--hairline);
+    padding-top: 16px;
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.24em;
+    color: var(--muted);
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .ig-footer .folio { color: var(--ink-60); letter-spacing: 0.32em; }
+
+  /* ============ Typography detail zoom ============ */
+  .detail-zoom {
+    position: absolute;
+    inset: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    opacity: 0;
+    will-change: opacity;
+    background: radial-gradient(ellipse at center, #0A0A0A, #000000);
+    z-index: 250;
+  }
+  .detail-word {
+    font-family: var(--serif-en);
+    font-weight: 300;
+    font-style: italic;
+    font-size: 320px;
+    line-height: 0.9;
+    letter-spacing: -0.01em;
+    color: var(--ink);
+    /* Enable OpenType ligatures, discretionary ligatures, swashes */
+    font-feature-settings: "liga" 1, "dlig" 1, "swsh" 1, "salt" 1, "calt" 1;
     text-rendering: optimizeLegibility;
+    will-change: transform, opacity;
+  }
+  .detail-word .fi {
+    /* fi ligature is default with "liga" */
+    color: var(--accent);
+  }
+  .detail-annotation {
+    position: absolute;
+    top: calc(50% + 170px); left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.28em;
+    color: var(--muted);
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity;
+    white-space: nowrap;
+  }
+  .detail-annotation .dot {
+    color: var(--accent);
+    padding: 0 8px;
+  }
+
+  /* Callout lines pointing to ligature */
+  .callout {
+    position: absolute;
+    left: 50%; top: 50%;
+    transform: translate(-50%, -50%);
+    pointer-events: none;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .callout svg { overflow: visible; display: block; }
+
+  /* ============ Brand Reveal ============ */
+  .brand-wall {
+    position: absolute;
+    inset: 0;
+    background: var(--cd-bg);
+    z-index: 300;
+    opacity: 0;
+    transform: translateY(100%);
+    will-change: transform, opacity;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
   }
+  .brand-wordmark {
+    font-family: var(--serif-en);
+    font-size: 132px;
+    font-weight: 200;
+    color: var(--cd-ink);
+    letter-spacing: -0.04em;
+    line-height: 1;
+    opacity: 0;
+    transform: scale(0.92);
+    will-change: opacity, transform;
+    font-feature-settings: "liga" 1, "dlig" 1;
+  }
+  .brand-wordmark .dot { color: var(--accent); padding: 0 10px; font-weight: 300; }
+  .brand-underline {
+    margin-top: 28px;
+    height: 2px;
+    width: 0;
+    background: var(--accent);
+    will-change: width;
+  }
+  .brand-cn {
+    margin-top: 30px;
+    font-family: var(--serif-cn);
+    font-size: 18px;
+    font-weight: 300;
+    color: var(--cd-dim);
+    letter-spacing: 0.4em;
+    opacity: 0;
+    will-change: opacity;
+  }
+
 </style>
 </head>
 <body>
-<div id="root"></div>
-
-<!-- animations.jsx inlined -->
-<script type="text/babel">
-(function() {
-  const { createContext, useContext, useState, useEffect, useRef } = React;
-  const TimeContext = createContext({ time: 0, duration: 10, playing: false });
-  const SpriteContext = createContext(null);
-  const Easing = {
-    linear: t => t,
-    easeIn: t => t * t,
-    easeOut: t => 1 - (1 - t) * (1 - t),
-    easeInOut: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
-    spring: t => {
-      const c = (2 * Math.PI) / 3;
-      return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c) + 1;
-    },
-  };
-  function interpolate(t, input, output, easing) {
-    const [a, b] = input, [x, y] = output;
-    if (t <= a) return x; if (t >= b) return y;
-    let p = (t - a) / (b - a); if (easing) p = easing(p);
-    return x + (y - x) * p;
-  }
-  function useTime() { return useContext(TimeContext).time; }
-  function useSprite() { return useContext(SpriteContext) || { t: 0, elapsed: 0, duration: 0 }; }
-  function Stage({ duration = 10, width = 1920, height = 1080, loop = true, children, bgColor = '#fff' }) {
-    const [time, setTime] = useState(0);
-    const [playing, setPlaying] = useState(true);
-    const [scale, setScale] = useState(1);
-    const rafRef = useRef(null);
-    const effectiveLoop = (typeof window !== 'undefined' && window.__recording) ? false : loop;
-    useEffect(() => {
-      const update = () => {
-        const s = Math.min(window.innerWidth / width, (window.innerHeight - 56) / height);
-        setScale(s);
-      };
-      update(); window.addEventListener('resize', update);
-      return () => window.removeEventListener('resize', update);
-    }, [width, height]);
-    useEffect(() => {
-      if (!playing) return;
-      let cancelled = false, last = null;
-      function tick(now) {
-        if (cancelled) return;
-        if (last === null) { last = now; if (typeof window !== 'undefined') window.__ready = true; }
-        const delta = (now - last) / 1000; last = now;
-        setTime(prev => {
-          const next = prev + delta;
-          if (next >= duration) return effectiveLoop ? 0 : duration - 0.001;
-          return next;
-        });
-        rafRef.current = requestAnimationFrame(tick);
-      }
-      const start = () => { if (!cancelled) rafRef.current = requestAnimationFrame(tick); };
-      if (document.fonts && document.fonts.ready) document.fonts.ready.then(start); else start();
-      return () => { cancelled = true; cancelAnimationFrame(rafRef.current); };
-    }, [playing, duration, effectiveLoop]);
-    const progress = time / duration;
-    const ctx = { time, duration, playing, setPlaying, setTime };
-    return (
-      <TimeContext.Provider value={ctx}>
-        <div style={{position:'fixed', inset:0, background:'#0c0c0c', display:'flex', flexDirection:'column'}}>
-          <div style={{flex:1, position:'relative', overflow:'hidden'}}>
-            <div style={{position:'absolute', top:'50%', left:'50%', transformOrigin:'center center', width, height, background: bgColor, overflow:'hidden', transform:`translate(-50%, -50%) scale(${scale})`}}>
-              {children}
-            </div>
-          </div>
-          <div className="no-record" style={{position:'fixed', bottom:0, left:0, right:0, background:'rgba(0,0,0,0.8)', padding:'12px 20px', display:'flex', alignItems:'center', gap:16, color:'#fff', fontSize:12, zIndex:100}}>
-            <button onClick={()=>setPlaying(p=>!p)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>{playing?'⏸ 暂停':'▶ 播放'}</button>
-            <button onClick={()=>setTime(0)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>⏮ 开始</button>
-            <div style={{fontFamily:'ui-monospace, monospace', fontVariantNumeric:'tabular-nums', minWidth:90}}>{time.toFixed(2)}s / {duration.toFixed(2)}s</div>
-            <div style={{flex:1, height:4, background:'rgba(255,255,255,0.2)', borderRadius:2, position:'relative'}}>
-              <div style={{position:'absolute', top:0, left:0, height:'100%', width:`${progress*100}%`, background:'#fff', borderRadius:2}} />
-            </div>
-          </div>
-        </div>
-      </TimeContext.Provider>
-    );
-  }
-  function Sprite({ start = 0, end, children, style }) {
-    const { time } = useContext(TimeContext);
-    const actualEnd = end == null ? Infinity : end;
-    if (time < start || time >= actualEnd) return null;
-    const duration = actualEnd - start;
-    const elapsed = time - start;
-    const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
-    return (
-      <SpriteContext.Provider value={{ t, elapsed, duration, start, end: actualEnd }}>
-        <div style={{position:'absolute', inset:0, ...style}}>{children}</div>
-      </SpriteContext.Provider>
-    );
-  }
-  window.Animations = { Stage, Sprite, useTime, useSprite, Easing, interpolate };
-})();
-</script>
 
-<!-- Demo scene -->
-<script type="text/babel">
-const { Stage, Sprite, useTime, useSprite, Easing, interpolate } = window.Animations;
-
-// ── Design tokens ─────────────────────────────────────────
-const CREAM = '#FAF6EF';
-const INK = '#1a1a1a';
-const TERRA = '#C04A1A';
-const ASH = '#6b6b6b';
-const LINE = '#d9d2c5';
-const OLIVE = '#6a6b4e';
-const DEEP_BLUE = '#2a3552';
-
-const serif = "'Newsreader', 'Noto Serif SC', Georgia, serif";
-const sans = "'Inter', -apple-system, sans-serif";
-const mono = "'JetBrains Mono', ui-monospace, monospace";
-
-// ── Scene 1: Title (0 – 3s) ───────────────────────────────
-function Scene1_Title() {
-  const { elapsed } = useSprite();
-  const topOp = interpolate(elapsed, [0.0, 0.6], [0, 1]);
-  const topLineW = interpolate(elapsed, [0.3, 1.0], [0, 220]);
-  const mainOp = interpolate(elapsed, [0.5, 1.2], [0, 1]);
-  const mainY = interpolate(elapsed, [0.5, 1.3], [32, 0], Easing.easeOut);
-  const italicOp = interpolate(elapsed, [1.0, 1.6], [0, 1]);
-  const subOp = interpolate(elapsed, [1.5, 2.1], [0, 1]);
-  const fadeOut = interpolate(elapsed, [2.6, 3.0], [1, 0], Easing.easeIn);
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
-      display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
-      <div style={{display:'flex', alignItems:'center', gap: 18, opacity: topOp, marginBottom: 48}}>
-        <div style={{height: 1, background: TERRA, width: topLineW}}/>
-        <div style={{fontFamily: mono, fontSize: 12, color: TERRA,
-          letterSpacing:'0.35em'}}>
-          信息图 · 数据驱动 · 印刷级
-        </div>
-        <div style={{height: 1, background: TERRA, width: topLineW}}/>
-      </div>
-      <div style={{fontFamily: serif, fontSize: 160, fontWeight: 500,
-        color: INK, lineHeight: 1, letterSpacing: '-0.02em',
-        opacity: mainOp, transform: `translateY(${mainY}px)`}}>
-        让数据<span style={{fontStyle:'italic', color: TERRA, opacity: italicOp}}>说话</span>
-      </div>
-      <div style={{fontFamily: serif, fontStyle: 'italic', fontSize: 24,
-        color: ASH, marginTop: 44, opacity: subOp, letterSpacing: '0.02em'}}>
-        精确排版 · 一眼看懂 · 可印刷
-      </div>
-    </div>
-  );
-}
-
-// ── Scene 2: Full infographic layout (3 – 10s) ────────────
-// Uses a magazine spread style: headline, three columns (big numbers / bars / pie), trend line footer
-function Scene2_Spread() {
-  const { elapsed } = useSprite();
-  const headerOp = interpolate(elapsed, [0, 0.5], [0, 1]);
-  const ruleW = interpolate(elapsed, [0.3, 1.0], [0, 1800]);
-  const colDelay = [0.6, 1.0, 1.4];
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM,
-      padding: '60px 80px 50px', display:'flex', flexDirection:'column'}}>
-      {/* Masthead */}
-      <div style={{opacity: headerOp, display:'flex', justifyContent:'space-between',
-        alignItems:'baseline', marginBottom: 14, fontFamily: mono, fontSize: 11,
-        letterSpacing: '0.3em', color: ASH}}>
-        <span>HUASHU · INFOGRAPHIC REPORT</span>
-        <span>VOL. 01 · 2026.04</span>
-      </div>
-      {/* Headline */}
-      <div style={{opacity: headerOp, fontFamily: serif, fontSize: 72,
-        fontWeight: 500, color: INK, lineHeight: 1.05, letterSpacing: '-0.01em',
-        marginBottom: 8}}>
-        2026 AI 写作工具
-        <span style={{fontStyle:'italic', color: TERRA, marginLeft: 20}}>年度观察</span>
-      </div>
-      <div style={{opacity: headerOp, fontFamily: serif, fontStyle:'italic',
-        fontSize: 20, color: ASH, marginBottom: 22}}>
-        156 位创作者匿名问卷 · 3 月 15 日 – 4 月 10 日
-      </div>
-      {/* Top rule */}
-      <div style={{width: ruleW, height: 1, background: INK, marginBottom: 28}}/>
-
-      {/* Three-column grid */}
-      <div style={{display:'grid', gridTemplateColumns:'1fr 1px 1.15fr 1px 0.95fr',
-        gap: 36, flex: 1}}>
-        <ColumnLeft elapsed={elapsed - colDelay[0]} />
-        <div style={{background: LINE}}/>
-        <ColumnMid elapsed={elapsed - colDelay[1]} />
-        <div style={{background: LINE}}/>
-        <ColumnRight elapsed={elapsed - colDelay[2]} />
-      </div>
+<div class="stage" id="stage">
 
-      {/* Footer trend line */}
-      <FooterTrend elapsed={elapsed - 3.5} />
-    </div>
-  );
-}
-
-function ColumnLeft({ elapsed }) {
-  const e = Math.max(0, elapsed);
-  const labelOp = interpolate(e, [0, 0.4], [0, 1]);
-  // 87%
-  const n1 = Math.round(interpolate(e, [0.3, 1.8], [0, 87], Easing.easeOut));
-  const bar1 = interpolate(e, [0.3, 1.8], [0, 87], Easing.easeOut);
-  // 3.2x
-  const n2 = interpolate(e, [1.3, 2.8], [1.0, 3.2], Easing.easeOut);
-  const bar2 = interpolate(e, [1.3, 2.8], [0, 3.2/5*100], Easing.easeOut);
-  // 156
-  const n3 = Math.round(interpolate(e, [2.3, 3.6], [0, 156], Easing.easeOut));
-  const bar3 = interpolate(e, [2.3, 3.6], [0, 100], Easing.easeOut);
-
-  return (
-    <div style={{display:'flex', flexDirection:'column', gap: 30}}>
-      <div style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.3em',
-        color: TERRA, opacity: labelOp}}>
-        COLUMN / 01 · 核心指标
-      </div>
-      <MetricRow
-        value={`${n1}%`}
-        width={`${bar1}%`}
-        label="用户每周使用 AI 辅助写作"
-        note="¹ 每周 ≥ 3 次"
-        color={TERRA}
-      />
-      <MetricRow
-        value={`${n2.toFixed(1)}×`}
-        width={`${bar2}%`}
-        label="平均产出效率提升"
-        note="² 自述周稿字数"
-        color={OLIVE}
-      />
-      <MetricRow
-        value={String(n3)}
-        width={`${bar3}%`}
-        label="有效样本数"
-        note="³ 剔除 AI 默认答卷"
-        color={DEEP_BLUE}
-      />
-    </div>
-  );
-}
-
-function MetricRow({ value, width, label, note, color }) {
-  return (
-    <div>
-      <div style={{display:'flex', alignItems:'baseline', gap: 10, marginBottom: 8}}>
-        <div style={{fontFamily: serif, fontSize: 72, fontWeight: 500,
-          color: INK, lineHeight: 0.95, letterSpacing:'-0.02em',
-          fontVariantNumeric:'tabular-nums'}}>
-          {value}
-        </div>
-      </div>
-      <div style={{height: 6, background: '#eee7d7', width:'100%',
-        marginBottom: 10, position:'relative'}}>
-        <div style={{position:'absolute', top:0, left:0, height:'100%',
-          width, background: color}}/>
-      </div>
-      <div style={{fontFamily: serif, fontSize: 15, color: INK, lineHeight: 1.4}}>
-        {label}
-        <span style={{fontFamily: mono, fontSize: 9, color: ASH,
-          verticalAlign:'super', marginLeft: 4}}>{note}</span>
-      </div>
+  <div class="watermark" id="watermark">HUASHU · DESIGN</div>
+  <div class="v2-mark">V2 · 2026</div>
+
+  <!-- Left: JSON data -->
+  <div class="split-left" id="splitLeft" style="opacity:0">
+    <div class="json-label" id="jsonLabel">DATA · benchmarks.json</div>
+    <pre class="json-block" id="jsonBlock"></pre>
+  </div>
+
+  <!-- Pipe arrow -->
+  <div class="pipe" id="pipe"></div>
+
+  <!-- Right: Infographic -->
+  <div class="infographic" id="infographic">
+
+    <div class="ig-masthead" id="igMasthead">
+      <div class="issue">Issue № 05 · <span class="orange">AI Benchmarks</span> · Q2 2026</div>
+      <div class="dept">性 能 报 告</div>
     </div>
-  );
-}
-
-function ColumnMid({ elapsed }) {
-  const e = Math.max(0, elapsed);
-  const labelOp = interpolate(e, [0, 0.4], [0, 1]);
-  // 5 bars, staggered 0.15s
-  const bars = [
-    { name:'长文创作', pct: 78, color: TERRA },
-    { name:'短内容', pct: 64, color: OLIVE },
-    { name:'标题/文案', pct: 52, color: DEEP_BLUE },
-    { name:'润色校对', pct: 41, color: ASH },
-    { name:'翻译', pct: 29, color: ASH },
-  ];
-  const chartH = 320;
-  const maxPct = 100;
-
-  return (
-    <div style={{display:'flex', flexDirection:'column', gap: 18}}>
-      <div style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.3em',
-        color: TERRA, opacity: labelOp}}>
-        COLUMN / 02 · 用途分布
-      </div>
-      <div style={{fontFamily: serif, fontSize: 28, fontWeight: 500,
-        color: INK, lineHeight: 1.2, opacity: labelOp,
-        letterSpacing: '-0.01em'}}>
-        你最常用 AI 做什么?
+
+    <h1 class="ig-display" id="igDisplay">
+      大模型<br>
+      <span class="en">benchmarks</span> 之年
+    </h1>
+
+    <p class="ig-deck" id="igDeck">
+      Five frontier models, five numbers, one uncomfortable truth.
+    </p>
+
+    <div class="ig-grid" id="igGrid">
+      <div class="ig-cell accent" data-cell="0">
+        <div class="label">领跑模型 <span class="en">· leader</span></div>
+        <div class="big">Claude 4.7</div>
+        <div class="sub">Sonnet, 1M ctx · Anthropic</div>
       </div>
-      <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 13,
-        color: ASH, opacity: labelOp}}>
-        多选题 · 百分比占全样本
+      <div class="ig-cell" data-cell="1">
+        <div class="label"><span class="en">SWE-bench</span></div>
+        <div class="big">77<span class="unit">.2%</span></div>
+        <div class="sub">coding, verified split</div>
       </div>
-      {/* chart */}
-      <div style={{position:'relative', height: chartH, display:'flex',
-        alignItems:'flex-end', gap: 18, padding: '0 8px', marginTop: 4,
-        borderBottom:`1px solid ${INK}`}}>
-        {/* y-axis gridlines */}
-        {[25, 50, 75, 100].map(v => (
-          <div key={v} style={{position:'absolute', left: 0, right: 0,
-            bottom: (v/maxPct)*chartH, height: 1,
-            borderTop:`1px dashed ${LINE}`, pointerEvents:'none'}}>
-            <div style={{position:'absolute', left: -36, top: -8,
-              fontFamily: mono, fontSize: 9, color: ASH,
-              letterSpacing:'0.05em'}}>
-              {v}%
-            </div>
-          </div>
-        ))}
-        {bars.map((b, i) => {
-          const delay = 0.8 + i * 0.15;
-          const growT = Math.max(0, Math.min(1, (e - delay) / 0.55));
-          const h = growT * (b.pct / maxPct) * chartH;
-          const labelOpB = Math.max(0, Math.min(1, (e - delay - 0.35) / 0.3));
-          return (
-            <div key={i} style={{flex: 1, position:'relative',
-              display:'flex', flexDirection:'column', alignItems:'center',
-              justifyContent:'flex-end', height:'100%'}}>
-              <div style={{position:'absolute', top: -22,
-                fontFamily: mono, fontSize: 11, color: INK,
-                letterSpacing:'0.02em', fontVariantNumeric:'tabular-nums',
-                opacity: labelOpB}}>
-                {b.pct}%
-              </div>
-              <div style={{width: '100%', height: h, background: b.color,
-                transition:'none'}}/>
-            </div>
-          );
-        })}
+      <div class="ig-cell" data-cell="2">
+        <div class="label"><span class="en">GPQA</span></div>
+        <div class="big">84<span class="unit">.5</span></div>
+        <div class="sub">diamond, graduate science</div>
       </div>
-      {/* x-axis labels */}
-      <div style={{display:'flex', gap: 18, padding: '0 8px', marginTop: -8}}>
-        {bars.map((b, i) => (
-          <div key={i} style={{flex: 1, textAlign:'center',
-            fontFamily: serif, fontSize: 12, color: INK,
-            letterSpacing:'0.02em', opacity: labelOp}}>
-            {b.name}
-          </div>
-        ))}
+      <div class="ig-cell" data-cell="3">
+        <div class="label">价差 <span class="en">· price</span></div>
+        <div class="big">$3<span class="unit">/M</span></div>
+        <div class="sub">input token, typical</div>
       </div>
     </div>
-  );
-}
-
-function ColumnRight({ elapsed }) {
-  const e = Math.max(0, elapsed);
-  const labelOp = interpolate(e, [0, 0.4], [0, 1]);
-  // Three pie slices sweep in
-  const slices = [
-    { label:'Claude', pct: 46, color: TERRA },
-    { label:'GPT', pct: 31, color: DEEP_BLUE },
-    { label:'GLM/国产', pct: 23, color: OLIVE },
-  ];
-  const cx = 130, cy = 130, r = 104;
-  const C = 2 * Math.PI * r;
-
-  // cumulative pct as fractions
-  let acc = 0;
-  const slicesCalc = slices.map((s, i) => {
-    const delay = 0.6 + i * 0.45;
-    const sweepT = Math.max(0, Math.min(1, (e - delay) / 0.7));
-    const start = acc;
-    const end = acc + s.pct / 100;
-    acc = end;
-    return { ...s, start, end, sweepT, delay };
-  });
 
-  return (
-    <div style={{display:'flex', flexDirection:'column', gap: 14}}>
-      <div style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.3em',
-        color: TERRA, opacity: labelOp}}>
-        COLUMN / 03 · 模型占有率
-      </div>
-      <div style={{fontFamily: serif, fontSize: 24, fontWeight: 500,
-        color: INK, lineHeight: 1.2, opacity: labelOp,
-        letterSpacing:'-0.01em'}}>
-        主力模型分布
-      </div>
-      <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 12,
-        color: ASH, opacity: labelOp, marginBottom: 6}}>
-        单选题 · 日常首选
-      </div>
+    <div class="ig-bars" id="igBars">
+      <div class="row-label highlight">Claude 4.7 Sonnet</div>
+      <div class="row-bar"><div class="fill accent" data-w="77.2"></div></div>
+      <div class="row-val">77.2</div>
 
-      <div style={{display:'flex', alignItems:'center', gap: 18}}>
-        <svg width="260" height="260" viewBox="0 0 260 260">
-          {/* Background ring */}
-          <circle cx={cx} cy={cy} r={r} fill="none"
-            stroke={LINE} strokeWidth={1}/>
-          {slicesCalc.map((s, i) => {
-            // Draw partial arc with stroke-dasharray
-            const sweepLen = (s.end - s.start) * s.sweepT;
-            const dash = sweepLen * C;
-            const gap = C - dash;
-            const rot = s.start * 360 - 90;
-            return (
-              <circle key={i} cx={cx} cy={cy} r={r}
-                fill="none" stroke={s.color} strokeWidth={28}
-                strokeDasharray={`${dash} ${gap}`}
-                strokeDashoffset={0}
-                transform={`rotate(${rot} ${cx} ${cy})`}
-                opacity={0.95}/>
-            );
-          })}
-          {/* Inner text */}
-          <text x={cx} y={cy - 4} textAnchor="middle"
-            fontFamily={serif} fontSize={34} fill={INK}
-            fontWeight={500} letterSpacing="-0.5">
-            n=156
-          </text>
-          <text x={cx} y={cy + 22} textAnchor="middle"
-            fontFamily={mono} fontSize={10} fill={ASH}
-            letterSpacing="0.2em">
-            TOTAL
-          </text>
-        </svg>
-        <div style={{display:'flex', flexDirection:'column', gap: 14, flex: 1}}>
-          {slicesCalc.map((s, i) => {
-            const txtOp = Math.max(0, Math.min(1, (e - s.delay - 0.3) / 0.4));
-            return (
-              <div key={i} style={{display:'flex', alignItems:'baseline',
-                gap: 10, opacity: txtOp}}>
-                <div style={{width: 10, height: 10, background: s.color,
-                  marginTop: 4, flexShrink: 0}}/>
-                <div style={{flex: 1}}>
-                  <div style={{fontFamily: serif, fontSize: 18,
-                    fontWeight: 500, color: INK, letterSpacing:'0.01em'}}>
-                    {s.label}
-                  </div>
-                </div>
-                <div style={{fontFamily: serif, fontSize: 22,
-                  fontWeight: 500, color: INK,
-                  fontVariantNumeric:'tabular-nums'}}>
-                  {s.pct}%
-                </div>
-              </div>
-            );
-          })}
-        </div>
-      </div>
+      <div class="row-label">GPT-5 Turbo</div>
+      <div class="row-bar"><div class="fill" data-w="74.8"></div></div>
+      <div class="row-val">74.8</div>
+
+      <div class="row-label">Gemini 3 Pro</div>
+      <div class="row-bar"><div class="fill" data-w="71.3"></div></div>
+      <div class="row-val">71.3</div>
+
+      <div class="row-label">GLM-5</div>
+      <div class="row-bar"><div class="fill" data-w="68.9"></div></div>
+      <div class="row-val">68.9</div>
+
+      <div class="row-label">Kimi k3</div>
+      <div class="row-bar"><div class="fill" data-w="66.4"></div></div>
+      <div class="row-val">66.4</div>
     </div>
-  );
-}
-
-function FooterTrend({ elapsed }) {
-  const e = Math.max(0, elapsed);
-  const op = interpolate(e, [0, 0.5], [0, 1]);
-  const data = [12, 18, 24, 31, 38, 48, 57, 64, 71, 78, 84, 87];
-  const months = ['05','06','07','08','09','10','11','12','01','02','03','04'];
-  const W = 1760, H = 86, PAD = 8;
-  const maxV = 100;
-  // progressive reveal of line
-  const revealT = Math.max(0, Math.min(1, (e - 0.3) / 1.4));
-  const nPoints = Math.max(1, Math.floor(revealT * data.length));
-  const pts = [];
-  for (let i = 0; i < data.length; i++) {
-    const x = (i / (data.length - 1)) * W;
-    const y = H - (data[i] / maxV) * (H - PAD * 2) - PAD;
-    pts.push([x, y]);
-  }
-  const visiblePts = pts.slice(0, nPoints);
-  const d = visiblePts.map((p, i) => (i === 0 ? 'M' : 'L') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' ');
-  const area = visiblePts.length > 1
-    ? d + ` L ${visiblePts[visiblePts.length-1][0].toFixed(1)} ${H} L 0 ${H} Z`
-    : '';
-
-  return (
-    <div style={{marginTop: 26, opacity: op}}>
-      <div style={{display:'flex', justifyContent:'space-between',
-        alignItems:'baseline', marginBottom: 8}}>
-        <div style={{fontFamily: mono, fontSize: 10, color: TERRA,
-          letterSpacing:'0.3em'}}>TREND · 过去 12 个月 AI 周使用率(%)</div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 13,
-          color: ASH}}>
-          从 12% 到 87% · 增长 7.25×
-        </div>
-      </div>
-      <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`}
-        style={{display:'block', width:'100%', height: H}}>
-        {area && <path d={area} fill={TERRA} opacity={0.08}/>}
-        {d && <path d={d} fill="none" stroke={TERRA} strokeWidth={1.6}/>}
-        {visiblePts.map((p, i) => (
-          <circle key={i} cx={p[0]} cy={p[1]} r={2.4} fill={TERRA}/>
-        ))}
-        {/* Axis labels */}
-        {pts.map((p, i) => (
-          <text key={i} x={p[0]} y={H - 0}
-            textAnchor="middle" fontFamily={mono} fontSize={9} fill={ASH}
-            opacity={0.6}>
-            {months[i]}
-          </text>
-        ))}
-      </svg>
+
+    <div class="ig-footer" id="igFooter">
+      <span>Set in Noto Serif SC &amp; Source Serif 4</span>
+      <span class="folio">P. 05</span>
+      <span>Data · 2026 Q2, public benchmarks</span>
     </div>
-  );
-}
-
-// ── Scene 3: Typography close-up (10 – 17s) ───────────────
-function Scene3_Typography() {
-  const { elapsed } = useSprite();
-  const fadeIn = interpolate(elapsed, [0, 0.6], [0, 1]);
-  const fadeOut = interpolate(elapsed, [6.5, 7.0], [1, 0], Easing.easeIn);
-  const opacity = Math.min(fadeIn, fadeOut);
-  const labelOp = interpolate(elapsed, [0.2, 0.8], [0, 1]);
-  const leftOp = interpolate(elapsed, [0.4, 1.2], [0, 1]);
-  const compareOp = interpolate(elapsed, [1.8, 2.6], [0, 1]);
-  const captionOp = interpolate(elapsed, [3.6, 4.4], [0, 1]);
-
-  // Pulsing scale on "87"
-  const pulse = 1 + Math.sin(elapsed * 1.6) * 0.008;
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM, opacity,
-      padding: '60px 80px', display:'flex', flexDirection:'column'}}>
-      {/* Top label */}
-      <div style={{display:'flex', alignItems:'baseline',
-        justifyContent:'space-between', marginBottom: 32, opacity: labelOp}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-            letterSpacing:'0.3em', marginBottom: 6}}>DETAIL · ZOOM 1.5×</div>
-          <div style={{fontFamily: serif, fontSize: 46, fontWeight: 500,
-            color: INK, letterSpacing:'-0.01em'}}>
-            排版细节:<span style={{fontStyle:'italic', color: TERRA}}>品味税</span>
-          </div>
-        </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
-          color: ASH, textAlign:'right', maxWidth: 400, lineHeight: 1.5}}>
-          "AI 能写中文,但分不清什么是好的中文排版"
-        </div>
-      </div>
 
-      <div style={{display:'grid', gridTemplateColumns:'1.25fr 1fr',
-        gap: 56, flex: 1}}>
-        {/* Left: Number gradient showcase */}
-        <div style={{opacity: leftOp, display:'flex', flexDirection:'column',
-          gap: 28}}>
-          <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-            letterSpacing:'0.3em'}}>01 · 字号梯度 · HIERARCHY</div>
-          <div style={{display:'flex', alignItems:'baseline', gap: 36,
-            borderBottom:`1px solid ${LINE}`, paddingBottom: 28}}>
-            <div style={{fontFamily: serif, fontSize: 220, fontWeight: 500,
-              color: INK, lineHeight: 0.88, letterSpacing:'-0.04em',
-              fontVariantNumeric:'tabular-nums', display:'inline-block',
-              transform:`scale(${pulse})`, transformOrigin:'left bottom'}}>
-              87<span style={{fontSize: 80, color: TERRA,
-                verticalAlign:'super', marginLeft: 4, fontStyle:'italic'}}>%</span>
-            </div>
-            <div style={{fontFamily: serif, fontSize: 110, fontWeight: 400,
-              color: OLIVE, lineHeight: 0.88, letterSpacing:'-0.02em',
-              fontVariantNumeric:'tabular-nums'}}>
-              3.2<span style={{fontSize: 44, fontStyle:'italic',
-                color: ASH, marginLeft: 2}}>×</span>
-            </div>
-            <div style={{fontFamily: serif, fontSize: 56, fontWeight: 400,
-              color: DEEP_BLUE, lineHeight: 0.88,
-              fontVariantNumeric:'tabular-nums'}}>
-              156
-            </div>
-          </div>
-          <div style={{fontFamily: serif, fontSize: 15, color: ASH,
-            lineHeight: 1.55, maxWidth: 580}}>
-            主数据 <span style={{color: INK, fontWeight: 500}}>220pt</span>、
-            次级 <span style={{color: INK, fontWeight: 500}}>110pt</span>、
-            辅助 <span style={{color: INK, fontWeight: 500}}>56pt</span>——
-            梯度 2× 不是工程师拍脑袋,是几百年印刷品的视觉惯性。
-          </div>
-
-          <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-            letterSpacing:'0.3em', marginTop: 10}}>02 · 换行 · TEXT-WRAP: PRETTY</div>
-          <div style={{fontFamily: serif, fontSize: 34, fontWeight: 500,
-            color: INK, lineHeight: 1.25, letterSpacing:'-0.01em',
-            maxWidth: 620, textWrap:'pretty'}}>
-            标题在该断的地方断开<br/>
-            避免孤字和单字成行
-          </div>
-
-          <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-            letterSpacing:'0.3em', marginTop: 4}}>03 · 上标辅注 · MONO FOOTNOTE</div>
-          <div style={{fontFamily: serif, fontSize: 20, color: INK,
-            lineHeight: 1.6, maxWidth: 620}}>
-            87%
-            <span style={{fontFamily: mono, fontSize: 11, color: TERRA,
-              verticalAlign:'super', marginLeft: 4}}>¹</span>
-            用户每周用 AI 辅助写作
-            <div style={{fontFamily: mono, fontSize: 11, color: ASH,
-              marginTop: 10, letterSpacing:'0.05em'}}>
-              ¹ 基于 156 位创作者调研,每周 ≥ 3 次
-            </div>
-          </div>
-        </div>
-
-        {/* Right: AI slop vs 精致 */}
-        <div style={{opacity: compareOp, display:'flex', flexDirection:'column',
-          gap: 20}}>
-          <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-            letterSpacing:'0.3em'}}>04 · AI SLOP vs 精致版</div>
-          {/* Slop version */}
-          <div style={{position:'relative', border: `1.5px dashed #c06060`,
-            padding: '22px 22px', borderRadius: 16,
-            background: 'linear-gradient(135deg, #6a47d4 0%, #3a1a7a 100%)'}}>
-            <div style={{position:'absolute', top: -10, left: 14,
-              background: '#c06060', color:'#fff', fontFamily: mono,
-              fontSize: 9, padding:'2px 10px', letterSpacing:'0.2em'}}>
-              ✕ 反例 · 不要这样做
-            </div>
-            <div style={{fontFamily: sans, fontSize: 28, fontWeight: 700,
-              color:'#fff', marginBottom: 6, letterSpacing:'-0.01em'}}>
-              🚀 AI 写作工具爆发增长!
-            </div>
-            <div style={{fontFamily: sans, fontSize: 13, color:'rgba(255,255,255,0.85)',
-              lineHeight: 1.5}}>
-              ✨ 87% 用户都在用!💡 效率提升 3.2 倍!🎯 赶紧加入!
-            </div>
-            <div style={{marginTop: 14, display:'flex', gap: 8}}>
-              <div style={{background:'rgba(255,255,255,0.2)',
-                padding:'6px 12px', borderRadius: 999,
-                fontFamily: sans, fontSize: 11, color:'#fff'}}>
-                #AI写作
-              </div>
-              <div style={{background:'rgba(255,255,255,0.2)',
-                padding:'6px 12px', borderRadius: 999,
-                fontFamily: sans, fontSize: 11, color:'#fff'}}>
-                #爆款
-              </div>
-            </div>
-          </div>
-
-          {/* Good version */}
-          <div style={{position:'relative', background:'#fff',
-            border: `1px solid ${LINE}`, padding: '22px 22px'}}>
-            <div style={{position:'absolute', top: -10, left: 14,
-              background: TERRA, color:'#fff', fontFamily: mono,
-              fontSize: 9, padding:'2px 10px', letterSpacing:'0.2em'}}>
-              ✓ 精致版 · DO THIS
-            </div>
-            <div style={{fontFamily: mono, fontSize: 9, color: TERRA,
-              letterSpacing:'0.3em', marginBottom: 4}}>ESSAY · 2026.04</div>
-            <div style={{fontFamily: serif, fontSize: 26, fontWeight: 500,
-              color: INK, lineHeight: 1.2, letterSpacing:'-0.01em',
-              marginBottom: 8}}>
-              AI 写作<br/>
-              <span style={{fontStyle:'italic'}}>悄然</span>改变创作者
-            </div>
-            <div style={{height: 1, background: INK, width: 70, marginBottom: 10}}/>
-            <div style={{fontFamily: serif, fontSize: 13, color:'#444',
-              lineHeight: 1.6}}>
-              87% 的创作者已经把 AI 纳入日常工作流;
-              效率提升 3.2×,但人味不减反增——
-              工具不定义内容,品味才定义。
-            </div>
-          </div>
-        </div>
-      </div>
+  </div>
 
-      {/* Caption */}
-      <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 20,
-        color: ASH, textAlign:'center', marginTop: 22, opacity: captionOp,
-        letterSpacing:'0.02em'}}>
-        排版细节是 AI 分不清的 <span style={{color: TERRA, fontWeight: 500,
-          fontStyle:'normal'}}>品味税</span>
-      </div>
+  <!-- Detail zoom: Typography ligature -->
+  <div class="detail-zoom" id="detailZoom">
+    <div class="detail-word" id="detailWord">bench<span class="fi">ma</span>rks</div>
+    <div class="callout" id="callout" style="display:none"></div>
+    <div class="detail-annotation" id="detailAnnotation">
+      SOURCE SERIF 4 <span class="dot">·</span> ITALIC <span class="dot">·</span> OLDSTYLE FIGURES
     </div>
-  );
-}
-
-// ── Scene 4: Outro (17 – 22s) ─────────────────────────────
-function Scene4_Outro() {
-  const { elapsed } = useSprite();
-  const fadeIn = interpolate(elapsed, [0, 0.6], [0, 1]);
-  const mainY = interpolate(elapsed, [0, 1.2], [28, 0], Easing.easeOut);
-  const italicOp = interpolate(elapsed, [0.8, 1.4], [0, 1]);
-  const lineW = interpolate(elapsed, [1.0, 1.8], [0, 680]);
-  const subOp = interpolate(elapsed, [1.6, 2.2], [0, 1]);
-  const monoOp = interpolate(elapsed, [2.4, 3.2], [0, 1]);
-  const monoLineW = interpolate(elapsed, [2.8, 3.8], [0, 520]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeIn,
-      display:'flex', alignItems:'center', justifyContent:'center',
-      flexDirection:'column'}}>
-      <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
-        color: TERRA, marginBottom: 32, opacity: fadeIn}}>
-        HUASHU-DESIGN · INFOGRAPHIC CAPABILITY
-      </div>
+  </div>
+
+  <!-- Brand Reveal -->
+  <div class="brand-wall" id="brandWall">
+    <div class="brand-wordmark" id="brandWord">huashu<span class="dot">·</span>design</div>
+    <div class="brand-underline" id="brandLine"></div>
+    <div class="brand-cn" id="brandCn">数 据 · 印 刷 级 排 版</div>
+  </div>
+
+</div>
+
+<script>
+(() => {
+  'use strict';
+
+  // ---------- Scale stage to viewport ----------
+  const stage = document.getElementById('stage');
+  function fitStage() {
+    const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  fitStage();
+  window.addEventListener('resize', fitStage);
+
+  // ---------- Easing ----------
+  const expoOut = t => t >= 1 ? 1 : 1 - Math.pow(2, -10 * t);
+  const expoIn = t => t <= 0 ? 0 : Math.pow(2, 10 * (t - 1));
+  const cubicOut = t => 1 - Math.pow(1 - t, 3);
+  const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;
+  const lerp = (t, a, b, c, d, ease=x=>x) => {
+    if (b === a) return c;
+    const k = Math.max(0, Math.min(1, (t - a) / (b - a)));
+    return c + (d - c) * ease(k);
+  };
+  const seg = (t, a, b) => Math.max(0, Math.min(1, (t - a) / (b - a)));
+
+  // ---------- Refs ----------
+  const splitLeft = document.getElementById('splitLeft');
+  const jsonLabel = document.getElementById('jsonLabel');
+  const jsonBlock = document.getElementById('jsonBlock');
+  const pipe = document.getElementById('pipe');
+  const infographic = document.getElementById('infographic');
+  const igMasthead = document.getElementById('igMasthead');
+  const igDisplay = document.getElementById('igDisplay');
+  const igDeck = document.getElementById('igDeck');
+  const igGrid = document.getElementById('igGrid');
+  const igCells = igGrid.querySelectorAll('.ig-cell');
+  const igBars = document.getElementById('igBars');
+  const igBarFills = igBars.querySelectorAll('.fill');
+  const igFooter = document.getElementById('igFooter');
+  const detailZoom = document.getElementById('detailZoom');
+  const detailWord = document.getElementById('detailWord');
+  const detailAnnotation = document.getElementById('detailAnnotation');
+  const callout = document.getElementById('callout');
+  const brandWall = document.getElementById('brandWall');
+  const brandWord = document.getElementById('brandWord');
+  const brandLine = document.getElementById('brandLine');
+  const brandCn = document.getElementById('brandCn');
+  const watermark = document.getElementById('watermark');
+
+  // ---------- JSON content (for progressive reveal) ----------
+  const jsonRaw = [
+    '{',
+    '  "issue": "2026-Q2",',
+    '  "leader": "Claude 4.7",',
+    '  "models": [',
+    '    { "name": "Claude 4.7",   "swe": 77.2 },',
+    '    { "name": "GPT-5 Turbo",  "swe": 74.8 },',
+    '    { "name": "Gemini 3 Pro", "swe": 71.3 },',
+    '    { "name": "GLM-5",        "swe": 68.9 },',
+    '    { "name": "Kimi k3",      "swe": 66.4 }',
+    '  ],',
+    '  "gpqa_top": 84.5,',
+    '  "price_per_M": 3',
+    '}'
+  ];
 
-      <div style={{fontFamily: serif, fontSize: 148, fontWeight: 500,
-        color: INK, lineHeight: 1, letterSpacing:'-0.02em',
-        transform:`translateY(${mainY}px)`}}>
-        <span style={{fontStyle:'italic', opacity: italicOp}}>数据</span>
-        <span style={{opacity: fadeIn}}> 配得上 </span>
-        <span style={{color: TERRA, opacity: italicOp}}>好看</span>
-      </div>
+  function formatJson(lines) {
+    return lines.map(line => {
+      return line
+        .replace(/"([a-zA-Z_]+)":/g, '<span class="k">"$1"</span>:')
+        .replace(/: "([^"]+)"/g, ': <span class="s">"$1"</span>')
+        .replace(/: ([0-9.]+)/g, ': <span class="n">$1</span>')
+        .replace(/([{}\[\],])/g, '<span class="p">$1</span>');
+    }).join('\n');
+  }
 
-      <div style={{height: 1, background: INK, width: lineW, marginTop: 44}}/>
+  // ---------- Timeline ----------
+  const DURATION = 10.0;
+
+  // SFX cue points (played back in ffmpeg post-processing, not browser):
+  //   t=0.35  → keyboard/type-fast.mp3  (data entering)
+  //   t=2.15  → container/card-snap.mp3 (infographic settles)
+  //   t=6.75  → transition/whoosh-fast.mp3 (zoom-in to typography)
+  //   t=8.70  → impact/logo-reveal.mp3   (brand reveal chime)
+  const sfxFired = new Set();
+  function fireOnce(key) {
+    if (sfxFired.has(key)) return;
+    sfxFired.add(key);
+    // cue emitted for post-processing; no in-browser playback
+  }
 
-      <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 24,
-        color: ASH, marginTop: 26, opacity: subOp, letterSpacing:'0.02em'}}>
-        印刷级 · 不因为缩放而失真
-      </div>
+  let startTime = null;
+  let raf;
+
+  function tick(now) {
+    if (startTime == null) startTime = now;
+    const t = (now - startTime) / 1000;
+
+    // ── Beat 1: 0-2s · JSON data appears, types in ─────────
+    // JSON label fade in
+    {
+      const k = cubicOut(seg(t, 0.15, 0.55));
+      jsonLabel.style.opacity = k;
+      splitLeft.style.opacity = '1';
+    }
+    // Progressive type-reveal: reveal N lines of JSON by time
+    {
+      const totalLines = jsonRaw.length;
+      const k = seg(t, 0.3, 1.9);
+      const linesShown = Math.floor(k * totalLines);
+      const shown = jsonRaw.slice(0, Math.max(0, linesShown));
+      jsonBlock.innerHTML = formatJson(shown);
+      if (linesShown >= 3 && t < 1.9) fireOnce('datain');
+    }
+
+    // ── Pipe arrow (1.8 → 2.2) ─────────────────────────────
+    {
+      const k = cubicOut(seg(t, 1.8, 2.2));
+      pipe.style.opacity = k;
+    }
+
+    // ── Beat 2a: 2.0-3.2s · Infographic canvas arrives ─────
+    {
+      const k = expoOut(seg(t, 2.0, 2.8));
+      infographic.style.opacity = k;
+      infographic.style.transform = `translateY(${lerp(t, 2.0, 2.8, 18, 0, expoOut)}px)`;
+      if (t > 2.1) fireOnce('settle');
+    }
+    // Masthead
+    {
+      const k = cubicOut(seg(t, 2.6, 3.1));
+      igMasthead.style.opacity = k;
+    }
+
+    // ── Beat 2b: 3.0-4.2s · Display headline appears ──────
+    {
+      const k = expoOut(seg(t, 3.0, 3.8));
+      igDisplay.style.opacity = k;
+      igDisplay.style.transform = `translateY(${lerp(t, 3.0, 3.8, 16, 0, expoOut)}px)`;
+    }
+    // Deck line (italic)
+    {
+      const k = cubicOut(seg(t, 3.6, 4.2));
+      igDeck.style.opacity = k;
+    }
+
+    // ── Beat 2c: 4.0-5.2s · Grid cells (ripple, 4 cells) ──
+    igCells.forEach((cell, i) => {
+      const start = 4.0 + i * 0.12;
+      const end = start + 0.5;
+      const k = expoOut(seg(t, start, end));
+      cell.style.opacity = k;
+      cell.style.transform = `translateY(${lerp(t, start, end, 14, 0, expoOut)}px)`;
+    });
+
+    // ── Beat 2d: 5.2-6.4s · Comparison bars grow ─────────
+    {
+      const k = cubicOut(seg(t, 5.1, 5.4));
+      igBars.style.opacity = k;
+    }
+    igBarFills.forEach((fill, i) => {
+      const start = 5.3 + i * 0.08;
+      const end = start + 0.7;
+      const w = parseFloat(fill.getAttribute('data-w'));
+      const pct = lerp(t, start, end, 0, w, expoOut);
+      fill.style.width = pct + '%';
+    });
+    // Footer
+    {
+      const k = cubicOut(seg(t, 6.0, 6.6));
+      igFooter.style.opacity = k * 0.9;
+    }
+
+    // ── Beat 2e: 6.6-8.2s · Zoom to typography detail ────
+    if (t >= 6.6 && t < 8.3) {
+      const k = expoOut(seg(t, 6.6, 7.4));
+      // Infographic scales up and fades — simulate push-in
+      const scale = lerp(t, 6.6, 7.4, 1, 3.4, expoOut);
+      const ty = lerp(t, 6.6, 7.4, 0, -140, expoOut);
+      infographic.style.transform = `translateY(${ty}px) scale(${scale})`;
+      infographic.style.opacity = String(1 - k * 0.85);
+      splitLeft.style.opacity = String(1 - k);
+      pipe.style.opacity = String(1 - k);
+
+      // Detail zoom fades in
+      const k2 = expoOut(seg(t, 7.0, 7.7));
+      detailZoom.style.opacity = k2;
+      // Word subtle scale-in (starts from 0.96)
+      detailWord.style.transform = `scale(${lerp(t, 7.0, 7.9, 0.96, 1.0, expoOut)})`;
+
+      // SFX at 6.7
+      if (t > 6.7) fireOnce('zoom');
+
+      // Callout + annotation (7.5 → 8.1)
+      const k3 = cubicOut(seg(t, 7.6, 8.1));
+      callout.style.opacity = k3;
+      detailAnnotation.style.opacity = k3;
+    }
+
+    // ── Beat 3: 8.2-10s · Brand reveal ───────────────────
+    // Detail zoom fades under brand wall
+    if (t >= 8.1) {
+      const k = cubicOut(seg(t, 8.1, 8.5));
+      detailZoom.style.opacity = String(Math.max(0, 1 - k));
+    }
+    // Brand wall slides up from bottom
+    {
+      const k = expoOut(seg(t, 8.1, 8.7));
+      brandWall.style.transform = `translateY(${lerp(t, 8.1, 8.7, 100, 0, expoOut)}%)`;
+      brandWall.style.opacity = k > 0 ? '1' : '0';
+      if (k > 0.55) watermark.classList.add('on-light');
+      else watermark.classList.remove('on-light');
+    }
+    // Wordmark
+    {
+      const k = expoOut(seg(t, 8.6, 9.2));
+      brandWord.style.opacity = k;
+      brandWord.style.transform = `scale(${lerp(t, 8.6, 9.2, 0.92, 1.0, expoOut)})`;
+      if (t > 8.65) fireOnce('chime');
+    }
+    // Underline
+    {
+      const k = expoOut(seg(t, 9.0, 9.6));
+      brandLine.style.width = (280 * k) + 'px';
+    }
+    // CN tagline
+    {
+      const k = cubicOut(seg(t, 9.3, 9.9));
+      brandCn.style.opacity = k * 0.9;
+    }
+
+    // Loop / hold
+    if (t < DURATION) {
+      raf = requestAnimationFrame(tick);
+    } else {
+      if (!window.__recording) {
+        setTimeout(() => {
+          // Reset
+          startTime = null;
+          sfxFired.clear();
+          jsonBlock.innerHTML = '';
+          splitLeft.style.opacity = '0';
+          pipe.style.opacity = '0';
+          infographic.style.opacity = '0';
+          infographic.style.transform = 'translateY(18px) scale(1)';
+          igMasthead.style.opacity = '0';
+          igDisplay.style.opacity = '0';
+          igDeck.style.opacity = '0';
+          igBars.style.opacity = '0';
+          igFooter.style.opacity = '0';
+          igCells.forEach(c => { c.style.opacity = '0'; });
+          igBarFills.forEach(f => { f.style.width = '0%'; });
+          detailZoom.style.opacity = '0';
+          callout.style.opacity = '0';
+          detailAnnotation.style.opacity = '0';
+          brandWall.style.transform = 'translateY(100%)';
+          brandWall.style.opacity = '0';
+          brandWord.style.opacity = '0';
+          brandLine.style.width = '0';
+          brandCn.style.opacity = '0';
+          watermark.classList.remove('on-light');
+          raf = requestAnimationFrame(tick);
+        }, 800);
+      }
+    }
+  }
 
-      <div style={{marginTop: 60, opacity: monoOp,
-        display:'flex', alignItems:'center', flexDirection:'column', gap: 14}}>
-        <div style={{height: 1, background: LINE, width: monoLineW}}/>
-        <div style={{fontFamily: mono, fontSize: 14, color: INK,
-          letterSpacing:'0.18em'}}>
-          export → <span style={{color: TERRA}}>PDF 矢量</span> /
-          <span style={{color: OLIVE}}> PNG 300dpi</span> /
-          <span style={{color: DEEP_BLUE}}> SVG 原生</span>
-        </div>
-        <div style={{height: 1, background: LINE, width: monoLineW}}/>
-      </div>
-    </div>
-  );
-}
-
-// ── Watermark ─────────────────────────────────────────────
-function Watermark() {
-  return (
-    <div style={{position:'absolute', bottom: 24, right: 32,
-      fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
-      fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
-      Created by Huashu-Design
-    </div>
-  );
-}
-
-// ── Composition ───────────────────────────────────────────
-function App() {
-  return (
-    <Stage duration={22} width={1920} height={1080} bgColor={CREAM}>
-      <Sprite start={0} end={3}><Scene1_Title /></Sprite>
-      <Sprite start={3} end={10}><Scene2_Spread /></Sprite>
-      <Sprite start={10} end={17}><Scene3_Typography /></Sprite>
-      <Sprite start={17} end={22}><Scene4_Outro /></Sprite>
-      <Watermark />
-    </Stage>
-  );
-}
-
-ReactDOM.createRoot(document.getElementById('root')).render(<App />);
+  window.__seek = function(s) {
+    startTime = performance.now() - s * 1000;
+  };
+
+  // Wait for fonts, then start
+  (document.fonts ? document.fonts.ready : Promise.resolve()).then(() => {
+    requestAnimationFrame((now) => {
+      startTime = now;
+      window.__ready = true;
+      raf = requestAnimationFrame(tick);
+    });
+  });
+})();
 </script>
+
 </body>
 </html>

+ 885 - 0
demos/c6-expert-review-en.html

@@ -0,0 +1,885 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8" />
+<title>c6 · Five Axes · One Punch List</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@200;300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<style>
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --dim: rgba(255,255,255,0.18);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;
+    --accent-deep: #B85D3D;
+    --cd-bg: #F5F4F0;
+    --cd-panel: #FFFFFF;
+    --cd-ink: #1A1918;
+
+    --serif-zh: "Noto Serif SC", "Songti SC", serif;
+    --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
+    --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
+    -webkit-font-smoothing: antialiased;
+  }
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+  }
+
+  /* Film grain */
+  .stage::before {
+    content: '';
+    position: absolute;
+    inset: 0;
+    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
+    opacity: 0.02;
+    pointer-events: none;
+    z-index: 100;
+  }
+
+  /* Chrome */
+  .mark {
+    position: absolute;
+    top: 48px; left: 64px;
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,1);
+    opacity: 0.16;
+    pointer-events: none;
+    z-index: 50;
+  }
+  .mark-right {
+    position: absolute;
+    top: 48px; right: 64px;
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,1);
+    opacity: 0.16;
+    pointer-events: none;
+    z-index: 50;
+  }
+
+  /* Title */
+  .title-line {
+    position: absolute;
+    top: 108px;
+    left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.28em;
+    color: var(--muted);
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+
+  /* Main composition: camera wrapper for push-in at Beat 3 */
+  .camera {
+    position: absolute;
+    inset: 0;
+    transform-origin: 1000px 940px; /* center of Fix first-row */
+    will-change: transform;
+  }
+
+  /* ============ LEFT: under-review artwork ============ */
+  .subject {
+    position: absolute;
+    left: 150px;
+    top: 310px;
+    width: 640px;
+    height: 460px;
+    background: #0B0B0B;
+    border: 1px solid var(--hairline);
+    border-radius: 8px;
+    overflow: hidden;
+    opacity: 0;
+    will-change: opacity, transform, filter;
+    transform: translateY(12px);
+  }
+  .subject::after {
+    /* subtle inner vignette */
+    content: '';
+    position: absolute;
+    inset: 0;
+    box-shadow: inset 0 0 120px rgba(0,0,0,0.6);
+    pointer-events: none;
+  }
+  .subject-label {
+    position: absolute;
+    left: 20px;
+    top: 18px;
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.25em;
+    color: var(--muted);
+    z-index: 3;
+  }
+  .subject-dot {
+    position: absolute;
+    right: 20px;
+    top: 18px;
+    width: 6px;
+    height: 6px;
+    background: var(--accent);
+    border-radius: 50%;
+    z-index: 3;
+    box-shadow: 0 0 10px rgba(217,119,87,0.6);
+  }
+  /* Subject wireframe: abstract design mockup */
+  .subject-canvas {
+    position: absolute;
+    inset: 50px 36px 36px;
+  }
+  .wf-h1 {
+    width: 62%;
+    height: 18px;
+    background: rgba(255,255,255,0.28);
+    border-radius: 2px;
+    margin-bottom: 10px;
+  }
+  .wf-h2 {
+    width: 38%;
+    height: 10px;
+    background: rgba(255,255,255,0.14);
+    border-radius: 2px;
+    margin-bottom: 28px;
+  }
+  .wf-row {
+    display: flex;
+    gap: 12px;
+    margin-bottom: 12px;
+  }
+  .wf-row .bar {
+    height: 8px;
+    background: rgba(255,255,255,0.10);
+    border-radius: 2px;
+  }
+  .wf-grid {
+    display: grid;
+    grid-template-columns: 1fr 1fr 1fr;
+    gap: 14px;
+    margin-top: 28px;
+  }
+  .wf-card {
+    height: 82px;
+    background: rgba(255,255,255,0.04);
+    border: 1px solid rgba(255,255,255,0.06);
+    border-radius: 6px;
+    position: relative;
+  }
+  .wf-card::before {
+    content: '';
+    position: absolute;
+    left: 12px; top: 14px;
+    width: 40%;
+    height: 6px;
+    background: rgba(255,255,255,0.22);
+    border-radius: 2px;
+  }
+  .wf-card::after {
+    content: '';
+    position: absolute;
+    left: 12px; bottom: 16px;
+    width: 64%;
+    height: 4px;
+    background: rgba(255,255,255,0.10);
+    border-radius: 2px;
+  }
+  .wf-card.accent { border-color: rgba(217,119,87,0.55); background: rgba(217,119,87,0.06); }
+  .wf-card.accent::before { background: var(--accent); }
+  .wf-foot {
+    position: absolute;
+    left: 0; right: 0;
+    bottom: 0;
+    height: 44px;
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    padding: 0 4px;
+  }
+  .wf-chip {
+    height: 22px;
+    padding: 0 10px;
+    background: rgba(255,255,255,0.05);
+    border: 1px solid rgba(255,255,255,0.08);
+    border-radius: 11px;
+    flex: 0 0 auto;
+    width: 68px;
+  }
+  .wf-chip.wide { width: 120px; }
+
+  /* ============ Light sweep ============ */
+  .sweep {
+    position: absolute;
+    left: 130px;
+    top: 250px;
+    width: 680px;
+    height: 140px;
+    background: linear-gradient(180deg,
+      rgba(217,119,87,0) 0%,
+      rgba(217,119,87,0.12) 20%,
+      rgba(255,220,200,0.62) 50%,
+      rgba(217,119,87,0.18) 80%,
+      rgba(217,119,87,0) 100%);
+    filter: blur(14px);
+    opacity: 0;
+    pointer-events: none;
+    z-index: 4;
+    mix-blend-mode: screen;
+    will-change: opacity, transform;
+  }
+  .sweep-line {
+    position: absolute;
+    left: 150px;
+    top: 310px;
+    width: 640px;
+    height: 1px;
+    background: linear-gradient(90deg,
+      transparent 0%,
+      rgba(255,220,200,0.2) 10%,
+      rgba(255,220,200,0.9) 50%,
+      rgba(255,220,200,0.2) 90%,
+      transparent 100%);
+    filter: blur(0.6px);
+    box-shadow: 0 0 14px rgba(217,119,87,0.8), 0 0 30px rgba(217,119,87,0.3);
+    opacity: 0;
+    pointer-events: none;
+    z-index: 6;
+    will-change: opacity, transform;
+  }
+
+  /* ============ RIGHT: radar chart ============ */
+  .radar-wrap {
+    position: absolute;
+    right: 280px;
+    top: 200px;
+    width: 520px;
+    height: 520px;
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+  .radar-wrap svg {
+    width: 100%;
+    height: 100%;
+    overflow: visible;
+  }
+  .radar-grid path {
+    fill: none;
+    stroke: rgba(255,255,255,0.10);
+    stroke-width: 1;
+  }
+  .radar-spoke {
+    stroke: rgba(255,255,255,0.08);
+    stroke-width: 1;
+  }
+  .radar-poly {
+    fill: rgba(217,119,87,0.16);
+    stroke: var(--accent);
+    stroke-width: 2;
+    stroke-linejoin: round;
+  }
+  .radar-point {
+    fill: var(--accent);
+    stroke: #1A1918;
+    stroke-width: 2;
+  }
+  .radar-label {
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.2em;
+    fill: var(--muted);
+    text-transform: uppercase;
+    opacity: 0;
+  }
+  .radar-label-zh {
+    font-family: var(--serif-en);
+    font-size: 22px;
+    font-weight: 400;
+    font-style: italic;
+    fill: var(--ink);
+    letter-spacing: 0.01em;
+  }
+  .radar-score {
+    font-family: var(--mono);
+    font-size: 13px;
+    fill: var(--accent);
+    letter-spacing: 0.08em;
+  }
+
+  .radar-title {
+    position: absolute;
+    right: 280px;
+    top: 160px;
+    width: 520px;
+    text-align: center;
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.28em;
+    color: var(--muted);
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .radar-score-total {
+    position: absolute;
+    left: 150px;
+    top: 170px;
+    width: 640px;
+    text-align: left;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .radar-score-total .score-row {
+    display: flex;
+    align-items: baseline;
+    gap: 24px;
+  }
+  .radar-score-total .score-label {
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.28em;
+    color: var(--muted);
+    text-transform: uppercase;
+  }
+  .radar-score-total .score-num {
+    font-family: var(--serif-en);
+    font-size: 72px;
+    font-weight: 300;
+    color: var(--ink);
+    letter-spacing: -0.02em;
+    line-height: 1;
+  }
+  .radar-score-total .score-num .accent { color: var(--accent); }
+  .radar-score-total .score-total {
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.28em;
+    color: var(--muted);
+    margin-top: 8px;
+    text-transform: uppercase;
+  }
+
+  /* ============ Single Fix row (Concept Card lean) ============ */
+  .fix-lane {
+    position: absolute;
+    left: 150px;
+    bottom: 120px;
+    width: 1620px;
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+  .fix-head {
+    display: flex;
+    align-items: baseline;
+    gap: 14px;
+    margin-bottom: 20px;
+    padding-bottom: 12px;
+    border-bottom: 1px solid var(--hairline);
+  }
+  .fix-mark {
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.28em;
+    color: var(--accent);
+    text-transform: uppercase;
+  }
+  .fix-zh {
+    font-family: var(--serif-en);
+    font-size: 28px;
+    font-weight: 400;
+    font-style: italic;
+    color: var(--ink);
+  }
+  .fix-count {
+    margin-left: auto;
+    font-family: var(--mono);
+    font-size: 11px;
+    color: var(--muted);
+    letter-spacing: 0.2em;
+  }
+
+  .fix-row {
+    position: relative;
+    font-family: var(--sans);
+    font-size: 28px;
+    font-weight: 300;
+    color: var(--ink);
+    line-height: 1.45;
+    padding: 12px 0;
+    display: flex;
+    gap: 20px;
+    align-items: center;
+  }
+  .fix-row .idx {
+    font-family: var(--mono);
+    font-size: 12px;
+    color: var(--muted);
+    letter-spacing: 0.2em;
+    flex: 0 0 40px;
+    padding-top: 2px;
+  }
+  .fix-row .mono {
+    font-family: var(--mono);
+    font-size: 26px;
+    letter-spacing: 0;
+    color: var(--accent);
+    font-weight: 400;
+  }
+  .fix-row .arrow {
+    color: var(--muted);
+    margin: 0 4px;
+  }
+
+  .fix-severity {
+    display: inline-block;
+    padding: 3px 10px;
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.22em;
+    color: var(--accent);
+    border: 1px solid rgba(217,119,87,0.5);
+    border-radius: 3px;
+    margin-right: 10px;
+    vertical-align: 3px;
+  }
+  .fix-pulse {
+    position: absolute;
+    inset: 4px -12px 4px -12px;
+    border: 1px solid var(--accent);
+    border-radius: 4px;
+    opacity: 0;
+    pointer-events: none;
+    will-change: opacity;
+    box-shadow: 0 0 24px rgba(217,119,87,0.35);
+  }
+
+  /* ============ Brand Reveal (hero-v10 signature) ============ */
+  .stage-dimmer {
+    position: absolute;
+    inset: 0;
+    background: #000000;
+    opacity: 0;
+    z-index: 40;
+    pointer-events: none;
+    will-change: opacity;
+  }
+  .brand-panel {
+    position: absolute;
+    inset: 0;
+    background: #F5F4F0;
+    transform: translateY(100%);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    z-index: 50;
+    will-change: transform;
+  }
+  .brand-wordmark {
+    font-family: var(--serif-en);
+    font-size: 72px;
+    font-weight: 100;
+    font-variation-settings: "wght" 100;
+    letter-spacing: -0.02em;
+    color: #1A1918;
+    text-align: center;
+    line-height: 1;
+    opacity: 0;
+    transform: translateY(20px);
+    will-change: opacity, transform, font-variation-settings, font-weight;
+  }
+  .brand-wordmark .accent { color: #D97757; font-weight: inherit; }
+  .brand-line {
+    margin-top: 60px;
+    height: 2px;
+    width: 0;
+    background: #D97757;
+    align-self: center;
+    will-change: width;
+  }
+</style>
+</head>
+<body>
+  <div class="stage" id="stage">
+    <div class="mark">HUASHU · DESIGN</div>
+    <div class="mark-right">V2 · 2026</div>
+
+    <div class="title-line" id="titleLine">c6 · Expert Review · Five Axes</div>
+
+    <div class="camera" id="camera">
+      <!-- Subject: design under review -->
+      <div class="subject" id="subject">
+        <div class="subject-label">SUBJECT · DRAFT_V3</div>
+        <div class="subject-dot"></div>
+        <div class="subject-canvas">
+          <div class="wf-h1"></div>
+          <div class="wf-h2"></div>
+          <div class="wf-row"><div class="bar" style="width:24%"></div><div class="bar" style="width:14%"></div><div class="bar" style="width:20%"></div></div>
+          <div class="wf-row"><div class="bar" style="width:30%"></div><div class="bar" style="width:10%"></div></div>
+          <div class="wf-grid">
+            <div class="wf-card"></div>
+            <div class="wf-card accent"></div>
+            <div class="wf-card"></div>
+          </div>
+          <div class="wf-foot">
+            <div class="wf-chip wide"></div>
+            <div class="wf-chip"></div>
+            <div class="wf-chip"></div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Scanning light -->
+      <div class="sweep" id="sweep"></div>
+      <div class="sweep-line" id="sweepLine"></div>
+
+      <!-- Radar chart (right) -->
+      <div class="radar-title" id="radarTitle">Five-Axis Diagnosis · Radar</div>
+      <div class="radar-wrap" id="radarWrap">
+        <svg viewBox="-270 -270 540 540" xmlns="http://www.w3.org/2000/svg">
+          <!-- Grid rings (5 levels) -->
+          <g class="radar-grid" id="radarGrid"></g>
+          <!-- Spokes to 5 axes -->
+          <g id="radarSpokes"></g>
+          <!-- Filled polygon -->
+          <polygon id="radarPoly" class="radar-poly" points="" />
+          <!-- Points -->
+          <g id="radarPoints"></g>
+          <!-- Axis labels -->
+          <g id="radarLabels"></g>
+        </svg>
+      </div>
+
+      <div class="radar-score-total" id="radarTotal">
+        <div class="score-row">
+          <div class="score-num"><span id="scoreNum">0</span><span class="accent">/50</span></div>
+          <div>
+            <div class="score-label">OVERALL · PASSED</div>
+            <div class="score-total">WEIGHTED · 7.4</div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Single Fix row: Concept Card lean -->
+      <div class="fix-lane" id="fixLane">
+        <div class="fix-head">
+          <span class="fix-mark">FIX</span>
+          <span class="fix-zh">Fix</span>
+          <span class="fix-count">01 / 01</span>
+        </div>
+        <div class="fix-row">
+          <span class="idx">01</span>
+          <span><span class="fix-severity">⚡</span>Tracking <span class="mono">0.02</span><span class="arrow"> → </span><span class="mono">0.04em</span></span>
+          <div class="fix-pulse" id="fixPulse"></div>
+        </div>
+      </div>
+    </div>
+
+    <!-- Brand Reveal (hero-v10 signature) -->
+    <div class="stage-dimmer" id="stageDimmer"></div>
+    <div class="brand-panel" id="brandPanel">
+      <div class="brand-wordmark" id="brandMark">huashu<span class="accent">-</span>design</div>
+      <div class="brand-line" id="brandLine"></div>
+    </div>
+  </div>
+
+<script>
+  // Auto-scale
+  function fitStage() {
+    const stage = document.getElementById('stage');
+    const sx = window.innerWidth / 1920;
+    const sy = window.innerHeight / 1080;
+    const s = Math.min(sx, sy);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  fitStage();
+  window.addEventListener('resize', fitStage);
+
+  // Easings
+  const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
+  const expoIn  = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
+  const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
+  const cubicOut = t => 1 - Math.pow(1 - t, 3);
+
+  function lerp(t, a, b, easing) {
+    if (t <= 0) return a;
+    if (t >= 1) return b;
+    const e = easing ? easing(t) : t;
+    return a + (b - a) * e;
+  }
+  function seg(time, start, end) {
+    if (time <= start) return 0;
+    if (time >= end) return 1;
+    return (time - start) / (end - start);
+  }
+
+  // ============ Build radar SVG ============
+  const RADIUS = 210;
+  const AXES = [
+    { zh: 'Philosophy', en: 'PHILOSOPHY', score: 8 },
+    { zh: 'Hierarchy',  en: 'HIERARCHY',  score: 6 },
+    { zh: 'Execution',  en: 'EXECUTION',  score: 8 },
+    { zh: 'Function',   en: 'FUNCTION',   score: 7 },
+    { zh: 'Innovation', en: 'INNOVATION', score: 8 },
+  ];
+  const N = AXES.length;
+
+  function axisPoint(i, r) {
+    // Start at top (-90deg), clockwise
+    const angle = -Math.PI / 2 + (2 * Math.PI * i) / N;
+    return [Math.cos(angle) * r, Math.sin(angle) * r];
+  }
+
+  // Grid rings (polygons at 5 levels)
+  const gridG = document.getElementById('radarGrid');
+  for (let level = 1; level <= 5; level++) {
+    const r = (RADIUS * level) / 5;
+    const pts = [];
+    for (let i = 0; i < N; i++) {
+      const [x, y] = axisPoint(i, r);
+      pts.push(`${x.toFixed(2)},${y.toFixed(2)}`);
+    }
+    const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
+    poly.setAttribute('points', pts.join(' '));
+    poly.setAttribute('fill', 'none');
+    poly.setAttribute('stroke', level === 5 ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.07)');
+    poly.setAttribute('stroke-width', '1');
+    gridG.appendChild(poly);
+  }
+
+  // Spokes
+  const spokesG = document.getElementById('radarSpokes');
+  for (let i = 0; i < N; i++) {
+    const [x, y] = axisPoint(i, RADIUS);
+    const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+    line.setAttribute('x1', 0);
+    line.setAttribute('y1', 0);
+    line.setAttribute('x2', x.toFixed(2));
+    line.setAttribute('y2', y.toFixed(2));
+    line.setAttribute('class', 'radar-spoke');
+    spokesG.appendChild(line);
+  }
+
+  // Labels (position outside). ZH sits at a base radial distance; EN stacks
+  // below it with a fixed vertical offset to avoid overlap on the side axes.
+  const labelsG = document.getElementById('radarLabels');
+  AXES.forEach((axis, i) => {
+    const angle = -Math.PI / 2 + (2 * Math.PI * i) / N;
+    const dirX = Math.cos(angle);
+    const dirY = Math.sin(angle);
+
+    // text-anchor based on horizontal direction
+    let anchor = 'middle';
+    if (dirX > 0.3) anchor = 'start';
+    else if (dirX < -0.3) anchor = 'end';
+
+    const baseRadial = RADIUS + 36;
+    const [bx, by] = axisPoint(i, baseRadial);
+
+    // Title Case serif italic label (only one per axis in EN)
+    const zhText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+    zhText.setAttribute('x', bx.toFixed(2));
+    zhText.setAttribute('y', by.toFixed(2));
+    zhText.setAttribute('text-anchor', anchor);
+    zhText.setAttribute('dominant-baseline', 'middle');
+    zhText.setAttribute('class', 'radar-label-zh');
+    zhText.textContent = axis.zh;
+    labelsG.appendChild(zhText);
+  });
+
+  // Points (initial: center)
+  const pointsG = document.getElementById('radarPoints');
+  const pointEls = AXES.map((axis, i) => {
+    const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+    circle.setAttribute('cx', 0);
+    circle.setAttribute('cy', 0);
+    circle.setAttribute('r', 5);
+    circle.setAttribute('class', 'radar-point');
+    circle.setAttribute('opacity', '0');
+    pointsG.appendChild(circle);
+    return circle;
+  });
+  const radarPoly = document.getElementById('radarPoly');
+
+  // ============ Timeline (10s) ============
+  //  Beat 1 (0-2s): title + subject enters
+  //  Beat 2 (2-8s):
+  //    2.0-3.8: light sweep top → bottom (1.8s)
+  //    3.2-4.8: radar grid fades in + polygon + points grow from center
+  //    4.8-5.2: score count up
+  //    5.0-6.0: Keep col ripple in
+  //    5.5-6.5: Fix col ripple in
+  //    6.0-7.0: Quick Wins col ripple in
+  //    7.0-8.0: hold
+  //  Beat 3 (8-10s): push-in camera to fix[0] + pulse (8-9), brand reveal (8.0-10.0)
+
+  const titleLine = document.getElementById('titleLine');
+  const subject = document.getElementById('subject');
+  const sweep = document.getElementById('sweep');
+  const sweepLine = document.getElementById('sweepLine');
+  const radarTitle = document.getElementById('radarTitle');
+  const radarWrap = document.getElementById('radarWrap');
+  const radarTotal = document.getElementById('radarTotal');
+  const scoreNum = document.getElementById('scoreNum');
+  const fixLane = document.getElementById('fixLane');
+  const fixPulse = document.getElementById('fixPulse');
+  const camera = document.getElementById('camera');
+  const stageDimmer = document.getElementById('stageDimmer');
+  const brandPanel = document.getElementById('brandPanel');
+  const brandMark = document.getElementById('brandMark');
+  const brandLine = document.getElementById('brandLine');
+
+  const DURATION = 10.0;
+  let startTime = null;
+  let loop = true;
+  if (window.__recording === true) loop = false;
+
+  function tick(now) {
+    if (startTime === null) startTime = now;
+    let t = (now - startTime) / 1000;
+
+    if (t >= DURATION) {
+      if (loop) { startTime = now; t = 0; }
+      else { t = DURATION; }
+    }
+
+    // Title fade in/out
+    const titleIn = seg(t, 0.2, 1.2);
+    const titleOut = seg(t, 7.6, 8.0);
+    titleLine.style.opacity = Math.min(cubicOut(titleIn), 1 - titleOut);
+    titleLine.style.transform = `translateX(-50%) translateY(${lerp(titleIn, -6, 0, cubicOut)}px)`;
+
+    // Subject appears Beat 1
+    const subjectIn = seg(t, 0.4, 1.8);
+    subject.style.opacity = expoOut(subjectIn);
+    subject.style.transform = `translateY(${lerp(subjectIn, 14, 0, expoOut)}px)`;
+
+    // Subject dims after sweep completes (during Beat 2 to keep focus right)
+    const subjectDim = seg(t, 4.4, 5.6);
+    const dimFactor = lerp(subjectDim, 1.0, 0.38, cubicInOut);
+    subject.style.filter = `saturate(${lerp(subjectDim, 1.0, 0.5, cubicInOut)}) brightness(${dimFactor})`;
+
+    // Light sweep: 2.0-3.8 top to bottom
+    const sweepProgress = seg(t, 2.0, 3.8);
+    const sweepOp = (t < 2.0 || t > 4.2) ? 0 :
+      (t < 2.2 ? seg(t, 2.0, 2.2) :
+       t < 3.7 ? 1 :
+       1 - seg(t, 3.7, 4.2));
+    sweep.style.opacity = sweepOp * 0.95;
+    sweepLine.style.opacity = sweepOp * 1.0;
+    // Move from y=250 to y=700 (subject top 310 to bottom 770)
+    const sweepY = lerp(sweepProgress, -70, 410, cubicInOut);
+    sweep.style.transform = `translateY(${sweepY}px)`;
+    sweepLine.style.transform = `translateY(${sweepY + 70}px)`;
+
+    // Radar title + wrap appear 3.2
+    const radarIn = seg(t, 3.2, 4.0);
+    radarTitle.style.opacity = cubicOut(radarIn);
+    radarWrap.style.opacity = cubicOut(radarIn);
+    radarWrap.style.transform = `scale(${lerp(radarIn, 0.92, 1.0, expoOut)})`;
+
+    // Radar grid strokes already visible once wrap fades; animate grid via stroke-dasharray trick would be overkill.
+    // Instead, grow polygon + points from center (3.6-4.8)
+    const polyGrow = seg(t, 3.6, 4.8);
+    const polyT = expoOut(polyGrow);
+    const polyPts = [];
+    AXES.forEach((axis, i) => {
+      const targetR = (axis.score / 10) * RADIUS;
+      const r = targetR * polyT;
+      const [x, y] = axisPoint(i, r);
+      polyPts.push(`${x.toFixed(2)},${y.toFixed(2)}`);
+      const pt = pointEls[i];
+      pt.setAttribute('cx', x.toFixed(2));
+      pt.setAttribute('cy', y.toFixed(2));
+      pt.setAttribute('opacity', polyT.toFixed(2));
+    });
+    radarPoly.setAttribute('points', polyPts.join(' '));
+
+    // EN labels fade in slightly later
+    const enLabelIn = seg(t, 4.2, 4.8);
+    document.querySelectorAll('[data-type="en-label"]').forEach(el => {
+      el.setAttribute('opacity', cubicOut(enLabelIn).toFixed(2));
+    });
+
+    // Score count up 4.6-5.4, target total = 37
+    const scoreT = seg(t, 4.6, 5.4);
+    const total = AXES.reduce((s, a) => s + a.score, 0); // 37
+    const shown = Math.round(lerp(scoreT, 0, total, cubicOut));
+    scoreNum.textContent = shown;
+    radarTotal.style.opacity = cubicOut(seg(t, 4.4, 5.0));
+
+    // Fix lane ripple in (5.3-6.1)
+    const fixRip = seg(t, 5.3, 6.1);
+    fixLane.style.opacity = expoOut(fixRip);
+    fixLane.style.transform = `translateY(${lerp(fixRip, 24, 0, expoOut)}px)`;
+
+    // Beat 3: Push-in camera to Fix row + pulse (7.4-8.0)
+    const pushT = seg(t, 7.4, 8.0);
+    const scale = lerp(pushT, 1.0, 1.18, cubicInOut);
+    camera.style.transform = `scale(${scale})`;
+
+    // Fix pulse border: blink 2 times between 7.6-8.0
+    const pulseOp = t < 7.6 ? 0 :
+      t < 8.0 ? (0.4 + 0.6 * Math.abs(Math.sin((t - 7.6) * Math.PI * 2.4))) :
+      0;
+    fixPulse.style.opacity = pulseOp;
+
+    // ============ Brand Reveal (hero-v10 signature, aligned) ============
+    // [T-2.0 → T-1.7s] i.e. 8.0-8.3: scene fade to black (0.3s)
+    const soK = seg(t, 8.0, 8.3);
+    stageDimmer.style.opacity = cubicOut(soK);
+    const sceneFade = seg(t, 8.0, 8.3);
+    camera.style.opacity = 1 - cubicOut(sceneFade);
+
+    // [T-1.7 → T-1.3s] i.e. 8.3-8.7: cream panel slides from bottom (0.4s, expoOut)
+    const panelT = seg(t, 8.3, 8.7);
+    const panelY = lerp(panelT, 100, 0, expoOut);
+    brandPanel.style.transform = `translateY(${panelY}%)`;
+
+    // [T-1.3 → T-0.7s] i.e. 8.7-9.3: wordmark wght 100→500 + y 20→0 + opacity 0→1 (0.6s)
+    const markT = seg(t, 8.7, 9.3);
+    const markE = expoOut(markT);
+    const wght = 100 + (500 - 100) * markE;
+    brandMark.style.opacity = markE;
+    brandMark.style.transform = `translateY(${20 * (1 - markE)}px)`;
+    brandMark.style.fontWeight = Math.round(wght);
+    brandMark.style.fontVariationSettings = `"wght" ${wght.toFixed(0)}`;
+
+    // [T-0.7 → T-0.3s] i.e. 9.3-9.7: orange line width 0→280 (0.4s, cubicOut)
+    const lineT = seg(t, 9.3, 9.7);
+    brandLine.style.width = `${lerp(lineT, 0, 280, cubicOut)}px`;
+
+    // [T-0.3 → T] hold
+
+    if (!window.__ready) window.__ready = true;
+
+    if (loop || t < DURATION) requestAnimationFrame(tick);
+  }
+
+  (document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve())
+    .then(() => requestAnimationFrame(tick));
+</script>
+</body>
+</html>

+ 864 - 622
demos/c6-expert-review.html

@@ -1,652 +1,894 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
+<!doctype html>
+<html lang="zh-Hans">
 <head>
-<meta charset="UTF-8">
-<title>Huashu-Design · Expert Review</title>
-<script crossorigin src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
-<script crossorigin src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
-<script src="https://unpkg.com/@babel/standalone@7.25.6/babel.min.js"></script>
+<meta charset="utf-8" />
+<title>c6 · 五个维度,给你一份手术单</title>
 <link rel="preconnect" href="https://fonts.googleapis.com">
 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400;1,6..72,500&family=Noto+Serif+SC:wght@300;400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@200;300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
 <style>
-  * { box-sizing: border-box; margin: 0; padding: 0; }
-  html, body { width: 100%; height: 100%; overflow: hidden; }
-  body { background: #0c0c0c; font-family: 'Newsreader', 'Noto Serif SC', Georgia, serif; color: #1a1a1a; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --dim: rgba(255,255,255,0.18);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;
+    --accent-deep: #B85D3D;
+    --cd-bg: #F5F4F0;
+    --cd-panel: #FFFFFF;
+    --cd-ink: #1A1918;
+
+    --serif-zh: "Noto Serif SC", "Songti SC", serif;
+    --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
+    --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
+    -webkit-font-smoothing: antialiased;
+  }
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+  }
+
+  /* Film grain */
+  .stage::before {
+    content: '';
+    position: absolute;
+    inset: 0;
+    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
+    opacity: 0.02;
+    pointer-events: none;
+    z-index: 100;
+  }
+
+  /* Chrome */
+  .mark {
+    position: absolute;
+    top: 48px; left: 64px;
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,1);
+    opacity: 0.16;
+    pointer-events: none;
+    z-index: 50;
+  }
+  .mark-right {
+    position: absolute;
+    top: 48px; right: 64px;
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,1);
+    opacity: 0.16;
+    pointer-events: none;
+    z-index: 50;
+  }
+
+  /* Title */
+  .title-line {
+    position: absolute;
+    top: 108px;
+    left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.28em;
+    color: var(--muted);
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+
+  /* Main composition: camera wrapper for push-in at Beat 3 */
+  .camera {
+    position: absolute;
+    inset: 0;
+    transform-origin: 1000px 940px; /* center of Fix first-row */
+    will-change: transform;
+  }
+
+  /* ============ LEFT: under-review artwork ============ */
+  .subject {
+    position: absolute;
+    left: 150px;
+    top: 310px;
+    width: 640px;
+    height: 460px;
+    background: #0B0B0B;
+    border: 1px solid var(--hairline);
+    border-radius: 8px;
+    overflow: hidden;
+    opacity: 0;
+    will-change: opacity, transform, filter;
+    transform: translateY(12px);
+  }
+  .subject::after {
+    /* subtle inner vignette */
+    content: '';
+    position: absolute;
+    inset: 0;
+    box-shadow: inset 0 0 120px rgba(0,0,0,0.6);
+    pointer-events: none;
+  }
+  .subject-label {
+    position: absolute;
+    left: 20px;
+    top: 18px;
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.25em;
+    color: var(--muted);
+    z-index: 3;
+  }
+  .subject-dot {
+    position: absolute;
+    right: 20px;
+    top: 18px;
+    width: 6px;
+    height: 6px;
+    background: var(--accent);
+    border-radius: 50%;
+    z-index: 3;
+    box-shadow: 0 0 10px rgba(217,119,87,0.6);
+  }
+  /* Subject wireframe: abstract design mockup */
+  .subject-canvas {
+    position: absolute;
+    inset: 50px 36px 36px;
+  }
+  .wf-h1 {
+    width: 62%;
+    height: 18px;
+    background: rgba(255,255,255,0.28);
+    border-radius: 2px;
+    margin-bottom: 10px;
+  }
+  .wf-h2 {
+    width: 38%;
+    height: 10px;
+    background: rgba(255,255,255,0.14);
+    border-radius: 2px;
+    margin-bottom: 28px;
+  }
+  .wf-row {
+    display: flex;
+    gap: 12px;
+    margin-bottom: 12px;
+  }
+  .wf-row .bar {
+    height: 8px;
+    background: rgba(255,255,255,0.10);
+    border-radius: 2px;
+  }
+  .wf-grid {
+    display: grid;
+    grid-template-columns: 1fr 1fr 1fr;
+    gap: 14px;
+    margin-top: 28px;
+  }
+  .wf-card {
+    height: 82px;
+    background: rgba(255,255,255,0.04);
+    border: 1px solid rgba(255,255,255,0.06);
+    border-radius: 6px;
+    position: relative;
+  }
+  .wf-card::before {
+    content: '';
+    position: absolute;
+    left: 12px; top: 14px;
+    width: 40%;
+    height: 6px;
+    background: rgba(255,255,255,0.22);
+    border-radius: 2px;
+  }
+  .wf-card::after {
+    content: '';
+    position: absolute;
+    left: 12px; bottom: 16px;
+    width: 64%;
+    height: 4px;
+    background: rgba(255,255,255,0.10);
+    border-radius: 2px;
+  }
+  .wf-card.accent { border-color: rgba(217,119,87,0.55); background: rgba(217,119,87,0.06); }
+  .wf-card.accent::before { background: var(--accent); }
+  .wf-foot {
+    position: absolute;
+    left: 0; right: 0;
+    bottom: 0;
+    height: 44px;
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    padding: 0 4px;
+  }
+  .wf-chip {
+    height: 22px;
+    padding: 0 10px;
+    background: rgba(255,255,255,0.05);
+    border: 1px solid rgba(255,255,255,0.08);
+    border-radius: 11px;
+    flex: 0 0 auto;
+    width: 68px;
+  }
+  .wf-chip.wide { width: 120px; }
+
+  /* ============ Light sweep ============ */
+  .sweep {
+    position: absolute;
+    left: 130px;
+    top: 250px;
+    width: 680px;
+    height: 140px;
+    background: linear-gradient(180deg,
+      rgba(217,119,87,0) 0%,
+      rgba(217,119,87,0.12) 20%,
+      rgba(255,220,200,0.62) 50%,
+      rgba(217,119,87,0.18) 80%,
+      rgba(217,119,87,0) 100%);
+    filter: blur(14px);
+    opacity: 0;
+    pointer-events: none;
+    z-index: 4;
+    mix-blend-mode: screen;
+    will-change: opacity, transform;
+  }
+  .sweep-line {
+    position: absolute;
+    left: 150px;
+    top: 310px;
+    width: 640px;
+    height: 1px;
+    background: linear-gradient(90deg,
+      transparent 0%,
+      rgba(255,220,200,0.2) 10%,
+      rgba(255,220,200,0.9) 50%,
+      rgba(255,220,200,0.2) 90%,
+      transparent 100%);
+    filter: blur(0.6px);
+    box-shadow: 0 0 14px rgba(217,119,87,0.8), 0 0 30px rgba(217,119,87,0.3);
+    opacity: 0;
+    pointer-events: none;
+    z-index: 6;
+    will-change: opacity, transform;
+  }
+
+  /* ============ RIGHT: radar chart ============ */
+  .radar-wrap {
+    position: absolute;
+    right: 280px;
+    top: 200px;
+    width: 520px;
+    height: 520px;
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+  .radar-wrap svg {
+    width: 100%;
+    height: 100%;
+    overflow: visible;
+  }
+  .radar-grid path {
+    fill: none;
+    stroke: rgba(255,255,255,0.10);
+    stroke-width: 1;
+  }
+  .radar-spoke {
+    stroke: rgba(255,255,255,0.08);
+    stroke-width: 1;
+  }
+  .radar-poly {
+    fill: rgba(217,119,87,0.16);
+    stroke: var(--accent);
+    stroke-width: 2;
+    stroke-linejoin: round;
+  }
+  .radar-point {
+    fill: var(--accent);
+    stroke: #1A1918;
+    stroke-width: 2;
+  }
+  .radar-label {
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.2em;
+    fill: var(--ink-80);
+    text-transform: uppercase;
+  }
+  .radar-label-zh {
+    font-family: var(--serif-zh);
+    font-size: 22px;
+    font-weight: 300;
+    fill: var(--ink);
+    letter-spacing: 0.05em;
+  }
+  .radar-score {
+    font-family: var(--mono);
+    font-size: 13px;
+    fill: var(--accent);
+    letter-spacing: 0.08em;
+  }
+
+  .radar-title {
+    position: absolute;
+    right: 280px;
+    top: 160px;
+    width: 520px;
+    text-align: center;
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.28em;
+    color: var(--muted);
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .radar-score-total {
+    position: absolute;
+    left: 150px;
+    top: 170px;
+    width: 640px;
+    text-align: left;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .radar-score-total .score-row {
+    display: flex;
+    align-items: baseline;
+    gap: 24px;
+  }
+  .radar-score-total .score-label {
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.28em;
+    color: var(--muted);
+    text-transform: uppercase;
+  }
+  .radar-score-total .score-num {
+    font-family: var(--serif-en);
+    font-size: 72px;
+    font-weight: 300;
+    color: var(--ink);
+    letter-spacing: -0.02em;
+    line-height: 1;
+  }
+  .radar-score-total .score-num .accent { color: var(--accent); }
+  .radar-score-total .score-total {
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.28em;
+    color: var(--muted);
+    margin-top: 8px;
+    text-transform: uppercase;
+  }
+
+  /* ============ Single Fix row (Concept Card lean) ============ */
+  .fix-lane {
+    position: absolute;
+    left: 150px;
+    bottom: 120px;
+    width: 1620px;
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+  .fix-head {
+    display: flex;
+    align-items: baseline;
+    gap: 14px;
+    margin-bottom: 20px;
+    padding-bottom: 12px;
+    border-bottom: 1px solid var(--hairline);
+  }
+  .fix-mark {
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.28em;
+    color: var(--accent);
+    text-transform: uppercase;
+  }
+  .fix-zh {
+    font-family: var(--serif-zh);
+    font-size: 28px;
+    font-weight: 400;
+    color: var(--ink);
+  }
+  .fix-count {
+    margin-left: auto;
+    font-family: var(--mono);
+    font-size: 11px;
+    color: var(--muted);
+    letter-spacing: 0.2em;
+  }
+
+  .fix-row {
+    position: relative;
+    font-family: var(--sans);
+    font-size: 28px;
+    font-weight: 300;
+    color: var(--ink);
+    line-height: 1.45;
+    padding: 12px 0;
+    display: flex;
+    gap: 20px;
+    align-items: center;
+  }
+  .fix-row .idx {
+    font-family: var(--mono);
+    font-size: 12px;
+    color: var(--muted);
+    letter-spacing: 0.2em;
+    flex: 0 0 40px;
+    padding-top: 2px;
+  }
+  .fix-row .mono {
+    font-family: var(--mono);
+    font-size: 26px;
+    letter-spacing: 0;
+    color: var(--accent);
+    font-weight: 400;
+  }
+  .fix-row .arrow {
+    color: var(--muted);
+    margin: 0 4px;
+  }
+
+  .fix-severity {
+    display: inline-block;
+    padding: 3px 10px;
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.22em;
+    color: var(--accent);
+    border: 1px solid rgba(217,119,87,0.5);
+    border-radius: 3px;
+    margin-right: 10px;
+    vertical-align: 3px;
+  }
+  .fix-pulse {
+    position: absolute;
+    inset: 4px -12px 4px -12px;
+    border: 1px solid var(--accent);
+    border-radius: 4px;
+    opacity: 0;
+    pointer-events: none;
+    will-change: opacity;
+    box-shadow: 0 0 24px rgba(217,119,87,0.35);
+  }
+
+  /* ============ Brand Reveal (hero-v10 signature) ============ */
+  .stage-dimmer {
+    position: absolute;
+    inset: 0;
+    background: #000000;
+    opacity: 0;
+    z-index: 40;
+    pointer-events: none;
+    will-change: opacity;
+  }
+  .brand-panel {
+    position: absolute;
+    inset: 0;
+    background: #F5F4F0;
+    transform: translateY(100%);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    z-index: 50;
+    will-change: transform;
+  }
+  .brand-wordmark {
+    font-family: var(--serif-en);
+    font-size: 72px;
+    font-weight: 100;
+    font-variation-settings: "wght" 100;
+    letter-spacing: -0.02em;
+    color: #1A1918;
+    text-align: center;
+    line-height: 1;
+    opacity: 0;
+    transform: translateY(20px);
+    will-change: opacity, transform, font-variation-settings, font-weight;
+  }
+  .brand-wordmark .accent { color: #D97757; font-weight: inherit; }
+  .brand-line {
+    margin-top: 60px;
+    height: 2px;
+    width: 0;
+    background: #D97757;
+    align-self: center;
+    will-change: width;
+  }
 </style>
 </head>
 <body>
-<div id="root"></div>
-
-<!-- animations.jsx inlined -->
-<script type="text/babel">
-(function() {
-  const { createContext, useContext, useState, useEffect, useRef } = React;
-  const TimeContext = createContext({ time: 0, duration: 10, playing: false });
-  const SpriteContext = createContext(null);
-  const Easing = {
-    linear: t => t,
-    easeIn: t => t * t,
-    easeOut: t => 1 - (1 - t) * (1 - t),
-    easeInOut: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
-    spring: t => {
-      const c = (2 * Math.PI) / 3;
-      return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c) + 1;
-    },
-  };
-  function interpolate(t, input, output, easing) {
-    const [a, b] = input, [x, y] = output;
-    if (t <= a) return x; if (t >= b) return y;
-    let p = (t - a) / (b - a); if (easing) p = easing(p);
-    return x + (y - x) * p;
-  }
-  function useTime() { return useContext(TimeContext).time; }
-  function useSprite() { return useContext(SpriteContext) || { t: 0, elapsed: 0, duration: 0 }; }
-  function Stage({ duration = 10, width = 1920, height = 1080, loop = true, children, bgColor = '#fff' }) {
-    const [time, setTime] = useState(0);
-    const [playing, setPlaying] = useState(true);
-    const [scale, setScale] = useState(1);
-    const rafRef = useRef(null);
-    const effectiveLoop = (typeof window !== 'undefined' && window.__recording) ? false : loop;
-    useEffect(() => {
-      const update = () => {
-        const s = Math.min(window.innerWidth / width, (window.innerHeight - 56) / height);
-        setScale(s);
-      };
-      update(); window.addEventListener('resize', update);
-      return () => window.removeEventListener('resize', update);
-    }, [width, height]);
-    useEffect(() => {
-      if (!playing) return;
-      let cancelled = false, last = null;
-      function tick(now) {
-        if (cancelled) return;
-        if (last === null) { last = now; if (typeof window !== 'undefined') window.__ready = true; }
-        const delta = (now - last) / 1000; last = now;
-        setTime(prev => {
-          const next = prev + delta;
-          if (next >= duration) return effectiveLoop ? 0 : duration - 0.001;
-          return next;
-        });
-        rafRef.current = requestAnimationFrame(tick);
-      }
-      const start = () => { if (!cancelled) rafRef.current = requestAnimationFrame(tick); };
-      if (document.fonts && document.fonts.ready) document.fonts.ready.then(start); else start();
-      return () => { cancelled = true; cancelAnimationFrame(rafRef.current); };
-    }, [playing, duration, effectiveLoop]);
-    const progress = time / duration;
-    const ctx = { time, duration, playing, setPlaying, setTime };
-    return (
-      <TimeContext.Provider value={ctx}>
-        <div style={{position:'fixed', inset:0, background:'#0c0c0c', display:'flex', flexDirection:'column'}}>
-          <div style={{flex:1, position:'relative', overflow:'hidden'}}>
-            <div style={{position:'absolute', top:'50%', left:'50%', transformOrigin:'center center', width, height, background: bgColor, overflow:'hidden', transform:`translate(-50%, -50%) scale(${scale})`}}>
-              {children}
-            </div>
-          </div>
-          <div className="no-record" style={{position:'fixed', bottom:0, left:0, right:0, background:'rgba(0,0,0,0.8)', padding:'12px 20px', display:'flex', alignItems:'center', gap:16, color:'#fff', fontSize:12, zIndex:100}}>
-            <button onClick={()=>setPlaying(p=>!p)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>{playing?'⏸ 暂停':'▶ 播放'}</button>
-            <button onClick={()=>setTime(0)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>⏮ 开始</button>
-            <div style={{fontFamily:'ui-monospace, monospace', fontVariantNumeric:'tabular-nums', minWidth:90}}>{time.toFixed(2)}s / {duration.toFixed(2)}s</div>
-            <div style={{flex:1, height:4, background:'rgba(255,255,255,0.2)', borderRadius:2, position:'relative'}}>
-              <div style={{position:'absolute', top:0, left:0, height:'100%', width:`${progress*100}%`, background:'#fff', borderRadius:2}} />
-            </div>
+  <div class="stage" id="stage">
+    <div class="mark">HUASHU · DESIGN</div>
+    <div class="mark-right">V2 · 2026</div>
+
+    <div class="title-line" id="titleLine">c6 · 专家评审 · 五个维度</div>
+
+    <div class="camera" id="camera">
+      <!-- Subject: design under review -->
+      <div class="subject" id="subject">
+        <div class="subject-label">SUBJECT · DRAFT_V3</div>
+        <div class="subject-dot"></div>
+        <div class="subject-canvas">
+          <div class="wf-h1"></div>
+          <div class="wf-h2"></div>
+          <div class="wf-row"><div class="bar" style="width:24%"></div><div class="bar" style="width:14%"></div><div class="bar" style="width:20%"></div></div>
+          <div class="wf-row"><div class="bar" style="width:30%"></div><div class="bar" style="width:10%"></div></div>
+          <div class="wf-grid">
+            <div class="wf-card"></div>
+            <div class="wf-card accent"></div>
+            <div class="wf-card"></div>
           </div>
-        </div>
-      </TimeContext.Provider>
-    );
-  }
-  function Sprite({ start = 0, end, children, style }) {
-    const { time } = useContext(TimeContext);
-    const actualEnd = end == null ? Infinity : end;
-    if (time < start || time >= actualEnd) return null;
-    const duration = actualEnd - start;
-    const elapsed = time - start;
-    const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
-    return (
-      <SpriteContext.Provider value={{ t, elapsed, duration, start, end: actualEnd }}>
-        <div style={{position:'absolute', inset:0, ...style}}>{children}</div>
-      </SpriteContext.Provider>
-    );
-  }
-  window.Animations = { Stage, Sprite, useTime, useSprite, Easing, interpolate };
-})();
-</script>
-
-<!-- Demo scene -->
-<script type="text/babel">
-const { Stage, Sprite, useTime, useSprite, Easing, interpolate } = window.Animations;
-
-// ── Design tokens ─────────────────────────────────────────
-const CREAM = '#FAF6EF';
-const INK = '#1a1a1a';
-const TERRA = '#C04A1A';
-const ASH = '#6b6b6b';
-const LINE = '#d9d2c5';
-const OLIVE = '#6a6b4e';
-const DEEP_BLUE = '#2a3552';
-
-const serif = "'Newsreader', 'Noto Serif SC', Georgia, serif";
-const sans = "'Inter', -apple-system, sans-serif";
-const mono = "'JetBrains Mono', ui-monospace, monospace";
-
-// ── 5 dimensions ──────────────────────────────────────────
-const DIMENSIONS = [
-  { no: '01', name: '哲学一致性', desc: '是否遵循既定的设计风格', score: 9 },
-  { no: '02', name: '视觉层级',   desc: '信息优先级是否一目了然', score: 8 },
-  { no: '03', name: '细节执行',   desc: '排版、间距、字重是否到位', score: 7 },
-  { no: '04', name: '功能性',     desc: '交互是否顺畅、可用',       score: 6 },
-  { no: '05', name: '创新性',     desc: '是否超出了平均水准',       score: 8 },
-];
-
-const COMMENTS = [
-  '赤陶橙贯穿,serif + 留白很 Kenya Hara',
-  'Hero 和 body 强度接近,主次需再拉开',
-  '行距、字号梯度还差一点点克制',
-  'CTA 可达但色值和主色冲突',
-  '版面节奏有想法,避开了模板感',
-];
-
-// ── Scene 1: Title (0 – 3s) ────────────────────────────
-function Scene1_Title() {
-  const { elapsed } = useSprite();
-  const topOp = interpolate(elapsed, [0, 0.5], [0, 1]);
-  const titleY = interpolate(elapsed, [0.2, 1.3], [50, 0], Easing.easeOut);
-  const titleOp = interpolate(elapsed, [0.2, 1.1], [0, 1]);
-  const lineW = interpolate(elapsed, [1.0, 1.8], [0, 540]);
-  const subOp = interpolate(elapsed, [1.4, 2.1], [0, 1]);
-  const fadeOut = interpolate(elapsed, [2.6, 3.0], [1, 0]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
-      display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
-      <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
-        color: TERRA, marginBottom: 28, opacity: topOp}}>
-        设计评审 · 5 维度评分
-      </div>
-      <div style={{fontFamily: serif, fontSize: 120, fontWeight: 500, color: INK,
-        lineHeight: 1, letterSpacing:'-0.02em', opacity: titleOp,
-        transform: `translateY(${titleY}px)`}}>
-        <span style={{fontStyle:'italic', color: TERRA}}>评</span>设计 · 不评设计师
-      </div>
-      <div style={{height: 1, background: INK, width: lineW, marginTop: 36}} />
-      <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 22, color: ASH,
-        marginTop: 28, opacity: subOp, letterSpacing:'0.02em'}}>
-        做完之后 · 用 5 个刻度看清楚
-      </div>
-    </div>
-  );
-}
-
-// ── Scene 2: 5 dimensions intro (3 – 8s) ───────────────
-function Scene2_Dimensions() {
-  const { elapsed } = useSprite();
-  const titleOp = interpolate(elapsed, [0, 0.4], [0, 1]);
-  const fadeOut = interpolate(elapsed, [4.5, 5.0], [1, 0]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
-      padding: '100px 100px 80px', display:'flex', flexDirection:'column'}}>
-      <div style={{opacity: titleOp, marginBottom: 60,
-        display:'flex', justifyContent:'space-between', alignItems:'baseline'}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-            letterSpacing:'0.3em', marginBottom: 6}}>步骤 1 / 3 · 评审维度</div>
-          <div style={{fontFamily: serif, fontSize: 60, fontWeight: 500, color: INK,
-            letterSpacing:'-0.01em'}}>
-            五把<span style={{fontStyle:'italic', color: TERRA}}>尺子</span>
+          <div class="wf-foot">
+            <div class="wf-chip wide"></div>
+            <div class="wf-chip"></div>
+            <div class="wf-chip"></div>
           </div>
         </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 19, color: ASH,
-          textAlign:'right', lineHeight: 1.5}}>
-          主观审美变不可辩论,<br/>
-          客观维度变可打分
-        </div>
       </div>
 
-      <div style={{flex: 1, display:'grid', gridTemplateColumns:'repeat(5, 1fr)',
-        gap: 22, alignItems:'stretch'}}>
-        {DIMENSIONS.map((d, i) => {
-          const appearStart = 0.6 + i * 0.4;
-          const appearEnd = appearStart + 0.7;
-          const op = interpolate(elapsed, [appearStart, appearEnd], [0, 1], Easing.easeOut);
-          const ty = interpolate(elapsed, [appearStart, appearEnd], [30, 0], Easing.easeOut);
-          return (
-            <div key={i} style={{
-              opacity: op,
-              transform: `translateY(${ty}px)`,
-              background: '#fff',
-              border: `1px solid ${LINE}`,
-              padding: '32px 26px 30px',
-              display:'flex', flexDirection:'column',
-              position:'relative',
-            }}>
-              <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 72,
-                fontWeight: 400, color: TERRA, lineHeight: 1,
-                letterSpacing:'-0.02em', marginBottom: 20}}>
-                {d.no}
-              </div>
-              <div style={{height: 1, background: INK, width: 40, marginBottom: 18}} />
-              <div style={{fontFamily: serif, fontSize: 26, fontWeight: 500,
-                color: INK, lineHeight: 1.15, marginBottom: 12}}>
-                {d.name}
-              </div>
-              <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 15,
-                color: ASH, lineHeight: 1.55, flex: 1}}>
-                {d.desc}
-              </div>
-              <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-                letterSpacing:'0.2em', marginTop: 22}}>
-                0 – 10 PT
-              </div>
-            </div>
-          );
-        })}
+      <!-- Scanning light -->
+      <div class="sweep" id="sweep"></div>
+      <div class="sweep-line" id="sweepLine"></div>
+
+      <!-- Radar chart (right) -->
+      <div class="radar-title" id="radarTitle">五维诊断 · RADAR</div>
+      <div class="radar-wrap" id="radarWrap">
+        <svg viewBox="-270 -270 540 540" xmlns="http://www.w3.org/2000/svg">
+          <!-- Grid rings (5 levels) -->
+          <g class="radar-grid" id="radarGrid"></g>
+          <!-- Spokes to 5 axes -->
+          <g id="radarSpokes"></g>
+          <!-- Filled polygon -->
+          <polygon id="radarPoly" class="radar-poly" points="" />
+          <!-- Points -->
+          <g id="radarPoints"></g>
+          <!-- Axis labels -->
+          <g id="radarLabels"></g>
+        </svg>
       </div>
-    </div>
-  );
-}
-
-// ── Scene 3: Radar + scoring (8 – 14s) ────────────────
-function Scene3_Radar() {
-  const { elapsed } = useSprite();
-  const headerOp = interpolate(elapsed, [0, 0.5], [0, 1]);
-  const fadeOut = interpolate(elapsed, [5.5, 6.0], [1, 0]);
-
-  // Radar reveal progress — polygon expands from center
-  const reveal = interpolate(elapsed, [0.8, 2.4], [0, 1], Easing.easeOut);
-
-  // Total score count-up
-  const totalT = interpolate(elapsed, [1.6, 2.8], [0, 1], Easing.easeOut);
-  const totalVal = Math.round(totalT * 38);
-
-  // Radar geometry
-  const cx = 340, cy = 440, R = 260;
-  const N = 5;
-  const maxScore = 10;
-  const angle = i => -Math.PI/2 + i * 2 * Math.PI / N;
-
-  // Axis endpoints
-  const axisPts = DIMENSIONS.map((_, i) => ({
-    x: cx + Math.cos(angle(i)) * R,
-    y: cy + Math.sin(angle(i)) * R,
-  }));
-
-  // Score polygon points (animated)
-  const scorePts = DIMENSIONS.map((d, i) => {
-    const r = (d.score / maxScore) * R * reveal;
-    return {
-      x: cx + Math.cos(angle(i)) * r,
-      y: cy + Math.sin(angle(i)) * r,
-    };
-  });
-  const scorePath = scorePts.map(p => `${p.x},${p.y}`).join(' ');
-
-  // Concentric rings
-  const rings = [2, 4, 6, 8, 10];
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
-      padding: '70px 90px 50px', display:'flex', flexDirection:'column'}}>
-      <div style={{opacity: headerOp, marginBottom: 24,
-        display:'flex', justifyContent:'space-between', alignItems:'baseline'}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-            letterSpacing:'0.3em', marginBottom: 6}}>步骤 2 / 3 · 打分</div>
-          <div style={{fontFamily: serif, fontSize: 54, fontWeight: 500, color: INK,
-            letterSpacing:'-0.01em'}}>
-            五边形 · <span style={{fontStyle:'italic'}}>照见</span>每一维
+
+      <div class="radar-score-total" id="radarTotal">
+        <div class="score-row">
+          <div class="score-num"><span id="scoreNum">0</span><span class="accent">/50</span></div>
+          <div>
+            <div class="score-label">总评 · PASSED</div>
+            <div class="score-total">五维加权 · 7.4</div>
           </div>
         </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18, color: ASH,
-          textAlign:'right', lineHeight: 1.5}}>
-          不是给个评价,<br/>
-          是把问题「可视化」出来
-        </div>
       </div>
 
-      <div style={{flex: 1, display:'grid', gridTemplateColumns: '720px 1fr', gap: 60}}>
-        {/* Radar */}
-        <div style={{position:'relative', background:'#fff', border:`1px solid ${LINE}`}}>
-          <svg viewBox="0 0 720 880" width="100%" height="100%" style={{display:'block'}}>
-            {/* Concentric rings */}
-            {rings.map((r, i) => {
-              const ringR = (r / maxScore) * R;
-              const pts = DIMENSIONS.map((_, k) => {
-                const x = cx + Math.cos(angle(k)) * ringR;
-                const y = cy + Math.sin(angle(k)) * ringR;
-                return `${x},${y}`;
-              }).join(' ');
-              return (
-                <g key={i}>
-                  <polygon points={pts} fill="none" stroke={LINE} strokeWidth="1" />
-                  <text x={cx + 6} y={cy - ringR + 4}
-                    fontFamily={mono} fontSize="10" fill={ASH}
-                    letterSpacing="0.1em">{r}</text>
-                </g>
-              );
-            })}
-            {/* Axes */}
-            {axisPts.map((p, i) => (
-              <line key={i} x1={cx} y1={cy} x2={p.x} y2={p.y}
-                stroke={LINE} strokeWidth="1" />
-            ))}
-            {/* Score polygon */}
-            <polygon points={scorePath}
-              fill={TERRA} fillOpacity="0.18"
-              stroke={TERRA} strokeWidth="2" />
-            {/* Score dots */}
-            {scorePts.map((p, i) => reveal > 0.6 && (
-              <circle key={i} cx={p.x} cy={p.y} r="5"
-                fill={TERRA} opacity={Math.min(1, (reveal - 0.6) / 0.4)} />
-            ))}
-            {/* Axis labels + score */}
-            {DIMENSIONS.map((d, i) => {
-              const labelR = R + 48;
-              const lx = cx + Math.cos(angle(i)) * labelR;
-              const ly = cy + Math.sin(angle(i)) * labelR;
-              const anchor = Math.abs(Math.cos(angle(i))) < 0.2 ? 'middle'
-                : Math.cos(angle(i)) > 0 ? 'start' : 'end';
-              const showScore = elapsed > 2.4 + i * 0.15;
-              return (
-                <g key={i}>
-                  <text x={lx} y={ly}
-                    fontFamily={mono} fontSize="13" fill={INK}
-                    fontWeight="500" textAnchor={anchor}
-                    letterSpacing="0.08em">
-                    {d.name}
-                  </text>
-                  {showScore && (
-                    <text x={lx} y={ly + 20}
-                      fontFamily={serif} fontSize="22" fill={TERRA}
-                      fontStyle="italic" fontWeight="500" textAnchor={anchor}>
-                      {d.score}
-                      <tspan fontSize="13" fill={ASH} fontStyle="normal"> / 10</tspan>
-                    </text>
-                  )}
-                </g>
-              );
-            })}
-            {/* Center total score */}
-            <text x={cx} y={750}
-              fontFamily={mono} fontSize="11" fill={ASH}
-              letterSpacing="0.3em" textAnchor="middle">
-              总分
-            </text>
-            <text x={cx} y={820}
-              fontFamily={serif} fontSize="72" fill={INK}
-              fontWeight="500" textAnchor="middle"
-              letterSpacing="-0.02em">
-              <tspan fontStyle="italic" fill={TERRA}>{totalVal}</tspan>
-              <tspan fontSize="34" fill={ASH} letterSpacing="0"> / 50</tspan>
-            </text>
-          </svg>
+      <!-- Single Fix row: Concept Card lean -->
+      <div class="fix-lane" id="fixLane">
+        <div class="fix-head">
+          <span class="fix-mark">FIX</span>
+          <span class="fix-zh">修复</span>
+          <span class="fix-count">01 / 01</span>
         </div>
-
-        {/* Right: breakdown list */}
-        <div style={{display:'flex', flexDirection:'column', gap: 18, paddingTop: 4}}>
-          <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-            letterSpacing:'0.25em', marginBottom: 4}}>
-            BREAKDOWN · 逐项
-          </div>
-          {DIMENSIONS.map((d, i) => {
-            const rowAppear = 2.2 + i * 0.25;
-            const op = interpolate(elapsed, [rowAppear, rowAppear + 0.6], [0, 1]);
-            const barT = interpolate(elapsed, [rowAppear + 0.2, rowAppear + 0.9],
-              [0, d.score / 10], Easing.easeOut);
-            const tx = interpolate(elapsed, [rowAppear, rowAppear + 0.5], [20, 0], Easing.easeOut);
-            return (
-              <div key={i} style={{opacity: op, transform:`translateX(${tx}px)`,
-                background:'#fff', border:`1px solid ${LINE}`, padding:'14px 20px'}}>
-                <div style={{display:'flex', justifyContent:'space-between',
-                  alignItems:'baseline', marginBottom: 8}}>
-                  <div style={{fontFamily: serif, fontSize: 22, fontWeight: 500, color: INK}}>
-                    <span style={{fontFamily: mono, fontSize: 11, color: TERRA,
-                      marginRight: 12, letterSpacing:'0.15em'}}>{d.no}</span>
-                    {d.name}
-                  </div>
-                  <div style={{fontFamily: serif, fontSize: 22, fontStyle:'italic',
-                    fontWeight: 500, color: TERRA}}>
-                    {d.score}<span style={{fontSize: 13, color: ASH, fontStyle:'normal'}}> / 10</span>
-                  </div>
-                </div>
-                {/* Progress bar */}
-                <div style={{height: 4, background: LINE, position:'relative', marginBottom: 8}}>
-                  <div style={{position:'absolute', top:0, left:0, height:'100%',
-                    width: `${barT * 100}%`, background: TERRA}} />
-                </div>
-                <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 14,
-                  color: ASH, lineHeight: 1.5}}>
-                  {COMMENTS[i]}
-                </div>
-              </div>
-            );
-          })}
+        <div class="fix-row">
+          <span class="idx">01</span>
+          <span><span class="fix-severity">⚡</span>字距 <span class="mono">0.02em</span><span class="arrow"> → </span><span class="mono">0.04em</span></span>
+          <div class="fix-pulse" id="fixPulse"></div>
         </div>
       </div>
     </div>
-  );
-}
-
-// ── Scene 4: Keep / Fix / Quick Wins (14 – 20s) ──────
-function Scene4_Actions() {
-  const { elapsed } = useSprite();
-  const headerOp = interpolate(elapsed, [0, 0.4], [0, 1]);
-  const fadeOut = interpolate(elapsed, [5.5, 6.0], [1, 0]);
-
-  const keeps = [
-    '赤陶橙 accent 贯穿全文',
-    'serif display 给了文学气质',
-    '留白足够 · 信息不挤',
-  ];
-  const fixes = [
-    { tag: '致命', sev: TERRA,   text: 'Hero 图和 body 抢焦点 · 降低 hero 字号' },
-    { tag: '重要', sev: OLIVE,   text: '侧边 CTA 色和品牌主色冲突' },
-    { tag: '优化', sev: ASH,     text: 'Footer 字号可以再小 2px' },
-  ];
-  const wins = [
-    'Hero 字号 96 → 72',
-    'CTA 改成 terra 主色',
-    'Footer 字号 14 → 12',
+
+    <!-- Brand Reveal (hero-v10 signature) -->
+    <div class="stage-dimmer" id="stageDimmer"></div>
+    <div class="brand-panel" id="brandPanel">
+      <div class="brand-wordmark" id="brandMark">huashu<span class="accent">-</span>design</div>
+      <div class="brand-line" id="brandLine"></div>
+    </div>
+  </div>
+
+<script>
+  // Auto-scale
+  function fitStage() {
+    const stage = document.getElementById('stage');
+    const sx = window.innerWidth / 1920;
+    const sy = window.innerHeight / 1080;
+    const s = Math.min(sx, sy);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  fitStage();
+  window.addEventListener('resize', fitStage);
+
+  // Easings
+  const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
+  const expoIn  = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
+  const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
+  const cubicOut = t => 1 - Math.pow(1 - t, 3);
+
+  function lerp(t, a, b, easing) {
+    if (t <= 0) return a;
+    if (t >= 1) return b;
+    const e = easing ? easing(t) : t;
+    return a + (b - a) * e;
+  }
+  function seg(time, start, end) {
+    if (time <= start) return 0;
+    if (time >= end) return 1;
+    return (time - start) / (end - start);
+  }
+
+  // ============ Build radar SVG ============
+  const RADIUS = 210;
+  const AXES = [
+    { zh: '哲学', en: 'PHILOSOPHY', score: 8 },
+    { zh: '层级', en: 'HIERARCHY',  score: 6 },
+    { zh: '执行', en: 'EXECUTION',  score: 8 },
+    { zh: '功能', en: 'FUNCTION',   score: 7 },
+    { zh: '创新', en: 'INNOVATION', score: 8 },
   ];
+  const N = AXES.length;
 
-  const col1T = interpolate(elapsed, [0.4, 1.2], [0, 1], Easing.easeOut);
-  const col2T = interpolate(elapsed, [0.8, 1.6], [0, 1], Easing.easeOut);
-  const col3T = interpolate(elapsed, [1.2, 2.0], [0, 1], Easing.easeOut);
-  const footerOp = interpolate(elapsed, [3.8, 4.6], [0, 1]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeOut,
-      padding: '70px 90px 60px', display:'flex', flexDirection:'column'}}>
-      <div style={{opacity: headerOp, marginBottom: 40,
-        display:'flex', justifyContent:'space-between', alignItems:'baseline'}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-            letterSpacing:'0.3em', marginBottom: 6}}>步骤 3 / 3 · 行动清单</div>
-          <div style={{fontFamily: serif, fontSize: 56, fontWeight: 500, color: INK,
-            letterSpacing:'-0.01em'}}>
-            Keep · Fix · <span style={{fontStyle:'italic', color: TERRA}}>Quick Wins</span>
-          </div>
-        </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18, color: ASH,
-          textAlign:'right', lineHeight: 1.5}}>
-          打完分 · 不是扔下报告,<br/>
-          是给一张可执行的「修复清单」
-        </div>
-      </div>
+  function axisPoint(i, r) {
+    // Start at top (-90deg), clockwise
+    const angle = -Math.PI / 2 + (2 * Math.PI * i) / N;
+    return [Math.cos(angle) * r, Math.sin(angle) * r];
+  }
 
-      <div style={{flex: 1, display:'grid', gridTemplateColumns:'1fr 1.15fr 1fr',
-        gap: 28}}>
-        {/* KEEP */}
-        <div style={{opacity: col1T, transform:`translateY(${(1-col1T)*20}px)`,
-          background:'#fff', border:`1px solid ${LINE}`,
-          borderTop: `4px solid ${OLIVE}`, padding: '30px 30px 28px',
-          display:'flex', flexDirection:'column'}}>
-          <div style={{display:'flex', justifyContent:'space-between',
-            alignItems:'baseline', marginBottom: 24}}>
-            <div>
-              <div style={{fontFamily: mono, fontSize: 11, color: OLIVE,
-                letterSpacing:'0.3em', marginBottom: 8}}>KEEP</div>
-              <div style={{fontFamily: serif, fontSize: 32, fontWeight: 500, color: INK}}>
-                保持这些
-              </div>
-            </div>
-            <div style={{fontFamily: serif, fontSize: 44, fontStyle:'italic',
-              fontWeight: 400, color: OLIVE, lineHeight: 1}}>
-              3
-            </div>
-          </div>
-          <div style={{display:'flex', flexDirection:'column', gap: 18, flex: 1}}>
-            {keeps.map((k, i) => (
-              <div key={i} style={{display:'flex', gap: 14, alignItems:'flex-start'}}>
-                <div style={{fontFamily: mono, fontSize: 16, color: OLIVE,
-                  fontWeight: 600, marginTop: 2}}>✓</div>
-                <div style={{fontFamily: serif, fontSize: 19, color: INK,
-                  lineHeight: 1.5, flex: 1}}>{k}</div>
-              </div>
-            ))}
-          </div>
-        </div>
+  // Grid rings (polygons at 5 levels)
+  const gridG = document.getElementById('radarGrid');
+  for (let level = 1; level <= 5; level++) {
+    const r = (RADIUS * level) / 5;
+    const pts = [];
+    for (let i = 0; i < N; i++) {
+      const [x, y] = axisPoint(i, r);
+      pts.push(`${x.toFixed(2)},${y.toFixed(2)}`);
+    }
+    const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
+    poly.setAttribute('points', pts.join(' '));
+    poly.setAttribute('fill', 'none');
+    poly.setAttribute('stroke', level === 5 ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.07)');
+    poly.setAttribute('stroke-width', '1');
+    gridG.appendChild(poly);
+  }
 
-        {/* FIX */}
-        <div style={{opacity: col2T, transform:`translateY(${(1-col2T)*20}px)`,
-          background:'#fff', border:`1px solid ${LINE}`,
-          borderTop: `4px solid ${TERRA}`, padding: '30px 30px 28px',
-          display:'flex', flexDirection:'column'}}>
-          <div style={{display:'flex', justifyContent:'space-between',
-            alignItems:'baseline', marginBottom: 24}}>
-            <div>
-              <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-                letterSpacing:'0.3em', marginBottom: 8}}>FIX</div>
-              <div style={{fontFamily: serif, fontSize: 32, fontWeight: 500, color: INK}}>
-                需修复 · 按严重度
-              </div>
-            </div>
-            <div style={{fontFamily: serif, fontSize: 44, fontStyle:'italic',
-              fontWeight: 400, color: TERRA, lineHeight: 1}}>
-              3
-            </div>
-          </div>
-          <div style={{display:'flex', flexDirection:'column', gap: 16, flex: 1}}>
-            {fixes.map((f, i) => (
-              <div key={i} style={{display:'flex', gap: 14, alignItems:'flex-start',
-                paddingBottom: 14, borderBottom: i < fixes.length - 1 ? `1px solid ${LINE}` : 'none'}}>
-                <div style={{
-                  background: f.sev, color: '#fff',
-                  fontFamily: mono, fontSize: 10, letterSpacing:'0.15em',
-                  padding: '4px 10px', marginTop: 4, minWidth: 58,
-                  textAlign: 'center', fontWeight: 600,
-                }}>
-                  {f.tag}
-                </div>
-                <div style={{fontFamily: serif, fontSize: 17, color: INK,
-                  lineHeight: 1.5, flex: 1}}>{f.text}</div>
-              </div>
-            ))}
-          </div>
-        </div>
+  // Spokes
+  const spokesG = document.getElementById('radarSpokes');
+  for (let i = 0; i < N; i++) {
+    const [x, y] = axisPoint(i, RADIUS);
+    const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+    line.setAttribute('x1', 0);
+    line.setAttribute('y1', 0);
+    line.setAttribute('x2', x.toFixed(2));
+    line.setAttribute('y2', y.toFixed(2));
+    line.setAttribute('class', 'radar-spoke');
+    spokesG.appendChild(line);
+  }
 
-        {/* QUICK WINS */}
-        <div style={{opacity: col3T, transform:`translateY(${(1-col3T)*20}px)`,
-          background:'#fff', border:`1px solid ${LINE}`,
-          borderTop: `4px solid ${DEEP_BLUE}`, padding: '30px 30px 28px',
-          display:'flex', flexDirection:'column'}}>
-          <div style={{display:'flex', justifyContent:'space-between',
-            alignItems:'baseline', marginBottom: 24}}>
-            <div>
-              <div style={{fontFamily: mono, fontSize: 11, color: DEEP_BLUE,
-                letterSpacing:'0.3em', marginBottom: 8}}>QUICK WINS</div>
-              <div style={{fontFamily: serif, fontSize: 32, fontWeight: 500, color: INK}}>
-                5 分钟能做的
-              </div>
-            </div>
-            <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-              letterSpacing:'0.2em', textAlign:'right', lineHeight: 1.6}}>
-              TOP<br/>3
-            </div>
-          </div>
-          <div style={{display:'flex', flexDirection:'column', gap: 18, flex: 1}}>
-            {wins.map((w, i) => (
-              <div key={i} style={{display:'flex', gap: 16, alignItems:'flex-start'}}>
-                <div style={{fontFamily: serif, fontSize: 32, fontStyle:'italic',
-                  color: DEEP_BLUE, fontWeight: 400, lineHeight: 1, minWidth: 32,
-                  marginTop: -4}}>
-                  {i+1}
-                </div>
-                <div style={{fontFamily: serif, fontSize: 19, color: INK,
-                  lineHeight: 1.5, flex: 1}}>{w}</div>
-              </div>
-            ))}
-          </div>
-        </div>
-      </div>
+  // Labels (position outside). ZH sits at a base radial distance; EN stacks
+  // below it with a fixed vertical offset to avoid overlap on the side axes.
+  const labelsG = document.getElementById('radarLabels');
+  AXES.forEach((axis, i) => {
+    const angle = -Math.PI / 2 + (2 * Math.PI * i) / N;
+    const dirX = Math.cos(angle);
+    const dirY = Math.sin(angle);
+
+    // text-anchor based on horizontal direction
+    let anchor = 'middle';
+    if (dirX > 0.3) anchor = 'start';
+    else if (dirX < -0.3) anchor = 'end';
+
+    const baseRadial = RADIUS + 36;
+    const [bx, by] = axisPoint(i, baseRadial);
+
+    // ZH label
+    const zhText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+    zhText.setAttribute('x', bx.toFixed(2));
+    zhText.setAttribute('y', by.toFixed(2));
+    zhText.setAttribute('text-anchor', anchor);
+    zhText.setAttribute('dominant-baseline', 'middle');
+    zhText.setAttribute('class', 'radar-label-zh');
+    zhText.textContent = axis.zh;
+    labelsG.appendChild(zhText);
+
+    // EN label stacks vertically below ZH (always +22px in y)
+    const enText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+    enText.setAttribute('x', bx.toFixed(2));
+    enText.setAttribute('y', (by + 22).toFixed(2));
+    enText.setAttribute('text-anchor', anchor);
+    enText.setAttribute('dominant-baseline', 'middle');
+    enText.setAttribute('class', 'radar-label');
+    enText.textContent = axis.en;
+    enText.setAttribute('opacity', '0');
+    enText.setAttribute('data-type', 'en-label');
+    labelsG.appendChild(enText);
+  });
 
-      {/* Footer slogan */}
-      <div style={{marginTop: 36, textAlign:'center', opacity: footerOp}}>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 26,
-          color: TERRA, letterSpacing:'0.02em'}}>
-          不是给个评价 · 是给个修复清单
-        </div>
-      </div>
-    </div>
-  );
-}
-
-// ── Scene 5: Outro (20 – 22s) ─────────────────────────
-function Scene5_Outro() {
-  const { elapsed } = useSprite();
-  const fadeIn = interpolate(elapsed, [0, 0.6], [0, 1], Easing.easeOut);
-  const titleY = interpolate(elapsed, [0, 1.0], [40, 0], Easing.easeOut);
-  const lineW = interpolate(elapsed, [0.5, 1.3], [0, 540]);
-  const subOp = interpolate(elapsed, [0.9, 1.6], [0, 1]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background: CREAM, opacity: fadeIn,
-      display:'flex', alignItems:'center', justifyContent:'center',
-      flexDirection:'column'}}>
-      <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
-        color: TERRA, marginBottom: 22}}>
-        5 维度 · 客观 · 可操作
-      </div>
-      <div style={{fontFamily: serif, fontSize: 108, fontWeight: 500,
-        color: INK, lineHeight: 1, letterSpacing:'-0.015em',
-        transform: `translateY(${titleY}px)`}}>
-        先打分 · 再<span style={{fontStyle:'italic', color: TERRA}}>修</span>
-      </div>
-      <div style={{height: 1, background: INK, width: lineW, marginTop: 36}} />
-      <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 22, color: ASH,
-        marginTop: 26, opacity: subOp, letterSpacing:'0.02em'}}>
-        Huashu-Design · Expert Review
-      </div>
-    </div>
-  );
-}
-
-// ── Watermark ──────────────────────────────────────────
-function Watermark() {
-  return (
-    <div style={{position:'absolute', bottom: 24, right: 32,
-      fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
-      fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
-      Created by Huashu-Design
-    </div>
-  );
-}
-
-function App() {
-  return (
-    <Stage duration={22} width={1920} height={1080} bgColor={CREAM}>
-      <Sprite start={0} end={3}><Scene1_Title /></Sprite>
-      <Sprite start={3} end={8}><Scene2_Dimensions /></Sprite>
-      <Sprite start={8} end={14}><Scene3_Radar /></Sprite>
-      <Sprite start={14} end={20}><Scene4_Actions /></Sprite>
-      <Sprite start={20} end={22}><Scene5_Outro /></Sprite>
-      <Watermark />
-    </Stage>
-  );
-}
-
-ReactDOM.createRoot(document.getElementById('root')).render(<App />);
+  // Points (initial: center)
+  const pointsG = document.getElementById('radarPoints');
+  const pointEls = AXES.map((axis, i) => {
+    const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+    circle.setAttribute('cx', 0);
+    circle.setAttribute('cy', 0);
+    circle.setAttribute('r', 5);
+    circle.setAttribute('class', 'radar-point');
+    circle.setAttribute('opacity', '0');
+    pointsG.appendChild(circle);
+    return circle;
+  });
+  const radarPoly = document.getElementById('radarPoly');
+
+  // ============ Timeline (10s) ============
+  //  Beat 1 (0-2s): title + subject enters
+  //  Beat 2 (2-8s):
+  //    2.0-3.8: light sweep top → bottom (1.8s)
+  //    3.2-4.8: radar grid fades in + polygon + points grow from center
+  //    4.8-5.2: score count up
+  //    5.0-6.0: Keep col ripple in
+  //    5.5-6.5: Fix col ripple in
+  //    6.0-7.0: Quick Wins col ripple in
+  //    7.0-8.0: hold
+  //  Beat 3 (8-10s): push-in camera to fix[0] + pulse (8-9), brand reveal (8.0-10.0)
+
+  const titleLine = document.getElementById('titleLine');
+  const subject = document.getElementById('subject');
+  const sweep = document.getElementById('sweep');
+  const sweepLine = document.getElementById('sweepLine');
+  const radarTitle = document.getElementById('radarTitle');
+  const radarWrap = document.getElementById('radarWrap');
+  const radarTotal = document.getElementById('radarTotal');
+  const scoreNum = document.getElementById('scoreNum');
+  const fixLane = document.getElementById('fixLane');
+  const fixPulse = document.getElementById('fixPulse');
+  const camera = document.getElementById('camera');
+  const stageDimmer = document.getElementById('stageDimmer');
+  const brandPanel = document.getElementById('brandPanel');
+  const brandMark = document.getElementById('brandMark');
+  const brandLine = document.getElementById('brandLine');
+
+  const DURATION = 10.0;
+  let startTime = null;
+  let loop = true;
+  if (window.__recording === true) loop = false;
+
+  function tick(now) {
+    if (startTime === null) startTime = now;
+    let t = (now - startTime) / 1000;
+
+    if (t >= DURATION) {
+      if (loop) { startTime = now; t = 0; }
+      else { t = DURATION; }
+    }
+
+    // Title fade in/out
+    const titleIn = seg(t, 0.2, 1.2);
+    const titleOut = seg(t, 7.6, 8.0);
+    titleLine.style.opacity = Math.min(cubicOut(titleIn), 1 - titleOut);
+    titleLine.style.transform = `translateX(-50%) translateY(${lerp(titleIn, -6, 0, cubicOut)}px)`;
+
+    // Subject appears Beat 1
+    const subjectIn = seg(t, 0.4, 1.8);
+    subject.style.opacity = expoOut(subjectIn);
+    subject.style.transform = `translateY(${lerp(subjectIn, 14, 0, expoOut)}px)`;
+
+    // Subject dims after sweep completes (during Beat 2 to keep focus right)
+    const subjectDim = seg(t, 4.4, 5.6);
+    const dimFactor = lerp(subjectDim, 1.0, 0.38, cubicInOut);
+    subject.style.filter = `saturate(${lerp(subjectDim, 1.0, 0.5, cubicInOut)}) brightness(${dimFactor})`;
+
+    // Light sweep: 2.0-3.8 top to bottom
+    const sweepProgress = seg(t, 2.0, 3.8);
+    const sweepOp = (t < 2.0 || t > 4.2) ? 0 :
+      (t < 2.2 ? seg(t, 2.0, 2.2) :
+       t < 3.7 ? 1 :
+       1 - seg(t, 3.7, 4.2));
+    sweep.style.opacity = sweepOp * 0.95;
+    sweepLine.style.opacity = sweepOp * 1.0;
+    // Move from y=250 to y=700 (subject top 310 to bottom 770)
+    const sweepY = lerp(sweepProgress, -70, 410, cubicInOut);
+    sweep.style.transform = `translateY(${sweepY}px)`;
+    sweepLine.style.transform = `translateY(${sweepY + 70}px)`;
+
+    // Radar title + wrap appear 3.2
+    const radarIn = seg(t, 3.2, 4.0);
+    radarTitle.style.opacity = cubicOut(radarIn);
+    radarWrap.style.opacity = cubicOut(radarIn);
+    radarWrap.style.transform = `scale(${lerp(radarIn, 0.92, 1.0, expoOut)})`;
+
+    // Radar grid strokes already visible once wrap fades; animate grid via stroke-dasharray trick would be overkill.
+    // Instead, grow polygon + points from center (3.6-4.8)
+    const polyGrow = seg(t, 3.6, 4.8);
+    const polyT = expoOut(polyGrow);
+    const polyPts = [];
+    AXES.forEach((axis, i) => {
+      const targetR = (axis.score / 10) * RADIUS;
+      const r = targetR * polyT;
+      const [x, y] = axisPoint(i, r);
+      polyPts.push(`${x.toFixed(2)},${y.toFixed(2)}`);
+      const pt = pointEls[i];
+      pt.setAttribute('cx', x.toFixed(2));
+      pt.setAttribute('cy', y.toFixed(2));
+      pt.setAttribute('opacity', polyT.toFixed(2));
+    });
+    radarPoly.setAttribute('points', polyPts.join(' '));
+
+    // EN labels fade in slightly later
+    const enLabelIn = seg(t, 4.2, 4.8);
+    document.querySelectorAll('[data-type="en-label"]').forEach(el => {
+      el.setAttribute('opacity', cubicOut(enLabelIn).toFixed(2));
+    });
+
+    // Score count up 4.6-5.4, target total = 37
+    const scoreT = seg(t, 4.6, 5.4);
+    const total = AXES.reduce((s, a) => s + a.score, 0); // 37
+    const shown = Math.round(lerp(scoreT, 0, total, cubicOut));
+    scoreNum.textContent = shown;
+    radarTotal.style.opacity = cubicOut(seg(t, 4.4, 5.0));
+
+    // Fix lane ripple in (5.3-6.1)
+    const fixRip = seg(t, 5.3, 6.1);
+    fixLane.style.opacity = expoOut(fixRip);
+    fixLane.style.transform = `translateY(${lerp(fixRip, 24, 0, expoOut)}px)`;
+
+    // Beat 3: Push-in camera to Fix row + pulse (7.4-8.0)
+    const pushT = seg(t, 7.4, 8.0);
+    const scale = lerp(pushT, 1.0, 1.18, cubicInOut);
+    camera.style.transform = `scale(${scale})`;
+
+    // Fix pulse border: blink 2 times between 7.6-8.0
+    const pulseOp = t < 7.6 ? 0 :
+      t < 8.0 ? (0.4 + 0.6 * Math.abs(Math.sin((t - 7.6) * Math.PI * 2.4))) :
+      0;
+    fixPulse.style.opacity = pulseOp;
+
+    // ============ Brand Reveal (hero-v10 signature, aligned) ============
+    // [T-2.0 → T-1.7s] i.e. 8.0-8.3: scene fade to black (0.3s)
+    const soK = seg(t, 8.0, 8.3);
+    stageDimmer.style.opacity = cubicOut(soK);
+    const sceneFade = seg(t, 8.0, 8.3);
+    camera.style.opacity = 1 - cubicOut(sceneFade);
+
+    // [T-1.7 → T-1.3s] i.e. 8.3-8.7: cream panel slides from bottom (0.4s, expoOut)
+    const panelT = seg(t, 8.3, 8.7);
+    const panelY = lerp(panelT, 100, 0, expoOut);
+    brandPanel.style.transform = `translateY(${panelY}%)`;
+
+    // [T-1.3 → T-0.7s] i.e. 8.7-9.3: wordmark wght 100→500 + y 20→0 + opacity 0→1 (0.6s)
+    const markT = seg(t, 8.7, 9.3);
+    const markE = expoOut(markT);
+    const wght = 100 + (500 - 100) * markE;
+    brandMark.style.opacity = markE;
+    brandMark.style.transform = `translateY(${20 * (1 - markE)}px)`;
+    brandMark.style.fontWeight = Math.round(wght);
+    brandMark.style.fontVariationSettings = `"wght" ${wght.toFixed(0)}`;
+
+    // [T-0.7 → T-0.3s] i.e. 9.3-9.7: orange line width 0→280 (0.4s, cubicOut)
+    const lineT = seg(t, 9.3, 9.7);
+    brandLine.style.width = `${lerp(lineT, 0, 280, cubicOut)}px`;
+
+    // [T-0.3 → T] hold
+
+    if (!window.__ready) window.__ready = true;
+
+    if (loop || t < DURATION) requestAnimationFrame(tick);
+  }
+
+  (document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve())
+    .then(() => requestAnimationFrame(tick));
 </script>
 </body>
 </html>

+ 1498 - 0
demos/hero-animation-v10-en.html

@@ -0,0 +1,1498 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8" />
+<title>Huashu Design · Here's to the Agents (v10)</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<style>
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --dim: rgba(255,255,255,0.18);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;          /* terracotta — 致敬 Anthropic 血统 */
+    --accent-deep: #B85D3D;
+
+    /* Claude Design palette — Act 0 专用 */
+    --cd-bg: #F5F4F0;
+    --cd-panel: #FFFFFF;
+    --cd-ink: #1A1918;
+    --cd-dim: #8B867E;
+    --cd-hair: rgba(0,0,0,0.08);
+    --cd-hair-strong: rgba(0,0,0,0.16);
+    --cd-green: #2D4A3A;
+    --cd-green-deep: #1E3428;
+    --cd-green-soft: #3F5E4D;
+
+    --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
+    --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
+    -webkit-font-smoothing: antialiased;
+  }
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+  }
+
+  .scene {
+    position: absolute; inset: 0;
+    display: flex; align-items: center; justify-content: center;
+    opacity: 0;
+    visibility: hidden;
+    will-change: opacity, transform;
+  }
+  .scene.visible { visibility: visible; }
+
+  /* ============ Act 1 ============ */
+  .act1 {
+    flex-direction: column;
+    gap: 40px;
+  }
+  .hero-line {
+    font-family: var(--sans);
+    font-size: 132px;
+    font-weight: 200;
+    letter-spacing: -0.045em;
+    color: var(--ink);
+    text-align: center;
+    line-height: 1.02;
+    will-change: transform, opacity, font-variation-settings;
+  }
+  .hero-line .accent { color: var(--accent); font-weight: inherit; }
+
+  .not-line {
+    font-family: var(--sans);
+    font-size: 96px;
+    font-weight: 200;
+    letter-spacing: -0.035em;
+    color: var(--ink);
+    text-align: center;
+    line-height: 1.08;
+  }
+  .not-line .strike {
+    color: var(--muted);
+    text-decoration: line-through;
+    text-decoration-thickness: 3px;
+    text-decoration-color: var(--accent);
+  }
+
+  /* ============ Abstract GUI icons (no real product screenshots) ============ */
+  .gui-glyph {
+    position: absolute;
+    opacity: 0;
+    will-change: opacity, transform, filter;
+  }
+  .gui-glyph.click {
+    /* Mouse cursor arrow */
+    width: 120px; height: 120px;
+    display: flex; align-items: center; justify-content: center;
+  }
+  .gui-glyph.click::before {
+    content: '';
+    width: 40px; height: 40px;
+    border: 2px solid var(--muted);
+    border-radius: 50%;
+    position: absolute;
+    animation: clickring 0.8s ease-out forwards;
+    animation-play-state: paused;
+  }
+  @keyframes clickring {
+    0%   { transform: scale(0.5); opacity: 0.8; }
+    100% { transform: scale(2.2); opacity: 0; }
+  }
+  .gui-glyph.click svg { width: 56px; height: 56px; position: relative; z-index: 2; }
+
+  .gui-glyph.drag {
+    /* Slider */
+    width: 400px; height: 48px;
+    display: flex; align-items: center;
+    gap: 0;
+  }
+  .gui-glyph.drag .track {
+    flex: 1;
+    height: 3px;
+    background: var(--hairline);
+    border-radius: 2px;
+    position: relative;
+  }
+  .gui-glyph.drag .fill {
+    position: absolute;
+    height: 100%;
+    background: var(--muted);
+    width: 30%;
+    border-radius: 2px;
+  }
+  .gui-glyph.drag .thumb {
+    position: absolute;
+    width: 24px; height: 24px;
+    background: var(--ink);
+    border: 1px solid var(--muted);
+    border-radius: 50%;
+    top: 50%;
+    left: 30%;
+    transform: translate(-50%, -50%);
+  }
+
+  .gui-glyph.folder {
+    /* Window frame w/ file list */
+    width: 420px; height: 260px;
+    background: rgba(255,255,255,0.02);
+    border: 1px solid var(--hairline);
+    border-radius: 10px;
+    overflow: hidden;
+  }
+  .gui-glyph.folder .head {
+    padding: 12px 16px;
+    border-bottom: 1px solid var(--hairline);
+    display: flex; gap: 8px;
+  }
+  .gui-glyph.folder .head .dot {
+    width: 9px; height: 9px; border-radius: 50%;
+    background: var(--hairline);
+  }
+  .gui-glyph.folder .row {
+    padding: 10px 16px;
+    font-family: var(--mono);
+    font-size: 13px;
+    color: var(--muted);
+    display: flex;
+    justify-content: space-between;
+    border-bottom: 1px solid var(--hairline);
+  }
+  .gui-glyph.folder .row:last-child { border-bottom: none; }
+  .gui-glyph.folder .row .meta {
+    color: var(--dim);
+  }
+
+  /* ============ Act 2 ============ */
+  .act2 {
+    flex-direction: column;
+  }
+
+  .terminal {
+    width: 1180px;
+    border-radius: 16px;
+    background: rgba(20, 20, 20, 1);
+    border: 1px solid var(--hairline);
+    overflow: hidden;
+    box-shadow:
+      0 0 0 1px rgba(255,255,255,0.02),
+      0 60px 120px -30px rgba(217,119,87,0.15);
+  }
+  .tty-head {
+    display: flex; align-items: center; gap: 9px;
+    padding: 18px 22px;
+    background: rgba(255,255,255,0.02);
+    border-bottom: 1px solid var(--hairline);
+  }
+  .tty-head .d {
+    width: 13px; height: 13px; border-radius: 50%;
+    background: var(--hairline);
+  }
+  .tty-head .d.red { background: #5a2a2a; }
+  .tty-head .d.yellow { background: #5a4a2a; }
+  .tty-head .d.green { background: #2a5a35; }
+  .tty-title {
+    margin-left: 16px;
+    color: var(--muted);
+    font-size: 14px;
+    font-family: var(--mono);
+    letter-spacing: 0.02em;
+  }
+  .tty-body {
+    padding: 44px 36px;
+    font-family: var(--mono);
+    font-size: 26px;
+    line-height: 1.6;
+    color: rgba(255,255,255,0.86);
+    min-height: 160px;
+  }
+  .prompt { color: var(--accent); margin-right: 12px; }
+  .typed { white-space: pre; }
+  .cursor {
+    display: inline-block;
+    width: 12px; height: 28px;
+    background: var(--accent);
+    vertical-align: -5px;
+    margin-left: 3px;
+  }
+
+  /* Gallery (v6 structure, dark theme) */
+  .gallery-viewport {
+    position: absolute;
+    inset: 0;
+    overflow: hidden;
+    perspective: 2400px;
+    perspective-origin: 50% 45%;
+  }
+  .gallery-canvas {
+    position: absolute;
+    top: 50%; left: 50%;
+    width: 4320px;
+    height: 2520px;
+    transform-origin: center center;
+    transform-style: preserve-3d;
+    will-change: transform;
+    display: grid;
+    grid-template-columns: repeat(8, 1fr);
+    gap: 40px;
+    padding: 60px;
+  }
+  .gallery-card {
+    background: #1a1a1a;
+    border-radius: 14px;
+    padding: 6px;
+    overflow: hidden;
+    border: 1px solid var(--hairline);
+    box-shadow:
+      0 20px 60px -20px rgba(0, 0, 0, 0.6),
+      0 6px 18px -6px rgba(0, 0, 0, 0.4);
+    aspect-ratio: 16 / 9;
+    will-change: opacity, filter;
+  }
+  .gallery-card.depth-near {
+    box-shadow:
+      0 32px 80px -22px rgba(0, 0, 0, 0.8),
+      0 10px 24px -8px rgba(217, 119, 87, 0.12);
+  }
+  .gallery-card.depth-far {
+    box-shadow:
+      0 14px 40px -16px rgba(0, 0, 0, 0.4),
+      0 4px 12px -4px rgba(0, 0, 0, 0.25);
+  }
+  .gallery-card img {
+    width: 100%; height: 100%;
+    object-fit: cover;
+    display: block;
+    border-radius: 9px;
+  }
+
+  /* Overlay statements (on top of gallery) */
+  .over-statement {
+    position: absolute;
+    inset: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    pointer-events: none;
+    z-index: 50;
+    opacity: 0;
+  }
+  .over-statement .text {
+    font-family: var(--sans);
+    font-size: 84px;
+    font-weight: 200;
+    letter-spacing: -0.035em;
+    color: var(--ink);
+    text-align: center;
+    line-height: 1.08;
+    text-shadow: 0 8px 40px rgba(0,0,0,0.8);
+    padding: 0 40px;
+    max-width: 1400px;
+  }
+  .over-statement .text .accent { color: var(--accent); }
+
+  /* ============ Act 3 ============ */
+  .act3 {
+    flex-direction: column;
+    gap: 0;
+  }
+  .statement-big {
+    font-family: var(--sans);
+    font-size: 160px;
+    font-weight: 100;
+    letter-spacing: -0.05em;
+    color: var(--ink);
+    text-align: center;
+    line-height: 1;
+    will-change: opacity, transform, font-variation-settings;
+  }
+  .statement-big .accent { color: var(--accent); font-weight: inherit; }
+
+  .brand-wordmark {
+    font-family: var(--sans);
+    font-size: 140px;
+    font-weight: 100;
+    font-variation-settings: "wght" 100;
+    letter-spacing: -0.045em;
+    color: var(--ink);
+    text-align: center;
+    line-height: 1;
+    will-change: font-variation-settings, opacity, transform;
+  }
+  .brand-wordmark .accent { color: var(--accent); font-weight: inherit; }
+
+  .farewell-quote {
+    margin-top: 44px;
+    font-family: var(--serif-en);
+    font-style: italic;
+    font-weight: 300;
+    font-size: 36px;
+    color: var(--accent);
+    letter-spacing: 0.005em;
+    text-align: center;
+    will-change: opacity, transform;
+  }
+
+  .farewell-cn {
+    margin-top: 18px;
+    font-family: var(--serif-en);
+    font-weight: 300;
+    font-size: 18px;
+    color: var(--muted);
+    letter-spacing: 0.24em;
+    text-align: center;
+  }
+
+  .brand-url {
+    margin-top: 48px;
+    font-size: 14px;
+    color: var(--muted);
+    font-family: var(--mono);
+    letter-spacing: 0.16em;
+    text-align: center;
+  }
+
+  /* Watermark (subtle, always on during Act 2/3) */
+  .watermark {
+    position: absolute;
+    bottom: 28px;
+    right: 36px;
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.24em;
+    text-transform: uppercase;
+    color: rgba(255,255,255,0.22);
+    z-index: 100;
+    opacity: 0;
+    transition: opacity 0.6s;
+    pointer-events: none;
+  }
+  .watermark.visible { opacity: 1; }
+
+  /* ============ Act 0 — Claude Design 致敬(+讽刺) ============ */
+  .act0 {
+    background: #0a0a0a;
+  }
+  .cd-browser {
+    position: absolute;
+    top: 50%; left: 50%;
+    width: 1640px;
+    height: 920px;
+    transform: translate(-50%, -50%);
+    background: var(--cd-bg);
+    border-radius: 14px;
+    overflow: hidden;
+    box-shadow:
+      0 0 0 1px rgba(255,255,255,0.04),
+      0 60px 160px -40px rgba(0,0,0,0.8),
+      0 24px 60px -20px rgba(0,0,0,0.6);
+    will-change: transform, opacity, filter;
+  }
+  .cd-chrome {
+    display: flex; align-items: center;
+    height: 48px;
+    padding: 0 18px;
+    background: #EDEBE5;
+    border-bottom: 1px solid var(--cd-hair);
+    gap: 14px;
+  }
+  .cd-traffic { display: flex; gap: 8px; }
+  .cd-traffic .d {
+    width: 12px; height: 12px; border-radius: 50%;
+    background: #D9D4CB;
+  }
+  .cd-traffic .d.r { background: #E8A5A0; }
+  .cd-traffic .d.y { background: #E8D0A0; }
+  .cd-traffic .d.g { background: #A5D0B0; }
+  .cd-urlbar {
+    flex: 1;
+    max-width: 520px;
+    margin: 0 auto;
+    height: 28px;
+    background: #F9F7F2;
+    border: 1px solid var(--cd-hair);
+    border-radius: 6px;
+    display: flex; align-items: center; justify-content: center;
+    font-family: var(--sans);
+    font-size: 13px;
+    color: var(--cd-dim);
+    letter-spacing: 0;
+  }
+  .cd-urlbar .lock {
+    width: 10px; height: 10px;
+    margin-right: 8px;
+    border: 1.5px solid var(--cd-dim);
+    border-radius: 2px;
+    position: relative;
+  }
+  .cd-urlbar .lock::before {
+    content: '';
+    position: absolute;
+    top: -5px; left: 50%;
+    transform: translateX(-50%);
+    width: 6px; height: 6px;
+    border: 1.5px solid var(--cd-dim);
+    border-bottom: none;
+    border-radius: 3px 3px 0 0;
+  }
+  .cd-tabs-row {
+    display: flex;
+    height: 42px;
+    padding: 0 24px;
+    background: var(--cd-bg);
+    border-bottom: 1px solid var(--cd-hair);
+    align-items: center;
+    gap: 6px;
+  }
+  .cd-tab {
+    height: 28px;
+    padding: 0 14px;
+    display: flex; align-items: center;
+    font-family: var(--sans);
+    font-size: 12px;
+    color: var(--cd-dim);
+    border-radius: 6px;
+    gap: 8px;
+    white-space: nowrap;
+  }
+  .cd-tab.active {
+    background: #FFFFFF;
+    color: var(--cd-ink);
+    font-weight: 500;
+    box-shadow: 0 1px 2px rgba(0,0,0,0.04);
+  }
+  .cd-tab .dot {
+    width: 6px; height: 6px; border-radius: 50%;
+    background: var(--cd-green);
+  }
+  .cd-topbar-right {
+    margin-left: auto;
+    display: flex; align-items: center; gap: 12px;
+    font-family: var(--sans);
+    font-size: 12px;
+    color: var(--cd-dim);
+  }
+  .cd-topbar-right .btn {
+    padding: 6px 12px;
+    background: var(--cd-ink);
+    color: #FFFFFF;
+    border-radius: 6px;
+    font-weight: 500;
+  }
+  .cd-topbar-right .btn.ghost {
+    background: transparent;
+    color: var(--cd-ink);
+    border: 1px solid var(--cd-hair-strong);
+  }
+
+  .cd-body {
+    display: grid;
+    grid-template-columns: 440px 1fr;
+    height: calc(920px - 48px - 42px);
+  }
+
+  /* Chat panel */
+  .cd-chat {
+    background: var(--cd-bg);
+    border-right: 1px solid var(--cd-hair);
+    padding: 28px 24px;
+    display: flex;
+    flex-direction: column;
+    gap: 18px;
+    overflow: hidden;
+  }
+  .cd-msg { display: flex; gap: 10px; align-items: flex-start; }
+  .cd-avatar {
+    width: 26px; height: 26px;
+    border-radius: 50%;
+    display: flex; align-items: center; justify-content: center;
+    font-family: var(--sans);
+    font-size: 11px;
+    font-weight: 600;
+    flex-shrink: 0;
+  }
+  .cd-avatar.user {
+    background: #E8E4DC;
+    color: var(--cd-ink);
+  }
+  .cd-avatar.claude {
+    background: var(--cd-ink);
+    color: #FFFFFF;
+  }
+  .cd-bubble {
+    font-family: var(--sans);
+    font-size: 13px;
+    line-height: 1.55;
+    color: var(--cd-ink);
+    max-width: 100%;
+  }
+  .cd-bubble .dim { color: var(--cd-dim); }
+
+  .cd-tweaks {
+    margin-top: auto;
+    padding: 16px 18px;
+    background: #FFFFFF;
+    border: 1px solid var(--cd-hair);
+    border-radius: 10px;
+  }
+  .cd-tweaks-title {
+    font-family: var(--sans);
+    font-size: 11px;
+    font-weight: 600;
+    letter-spacing: 0.08em;
+    text-transform: uppercase;
+    color: var(--cd-dim);
+    margin-bottom: 14px;
+  }
+  .cd-tweak-row {
+    display: flex; align-items: center;
+    gap: 12px;
+    margin-bottom: 12px;
+  }
+  .cd-tweak-row:last-child { margin-bottom: 0; }
+  .cd-tweak-label {
+    font-family: var(--sans);
+    font-size: 12px;
+    color: var(--cd-ink);
+    width: 72px;
+    flex-shrink: 0;
+  }
+  .cd-tweak-track {
+    flex: 1;
+    height: 4px;
+    background: #E8E4DC;
+    border-radius: 2px;
+    position: relative;
+  }
+  .cd-tweak-thumb {
+    position: absolute;
+    top: 50%;
+    width: 16px; height: 16px;
+    background: #FFFFFF;
+    border: 1.5px solid var(--cd-ink);
+    border-radius: 50%;
+    transform: translate(-50%, -50%);
+    will-change: left;
+  }
+  .cd-color-dots {
+    display: flex; gap: 6px;
+  }
+  .cd-color-dot {
+    width: 16px; height: 16px;
+    border-radius: 50%;
+    border: 1.5px solid transparent;
+    cursor: default;
+  }
+  .cd-color-dot.active {
+    border-color: var(--cd-ink);
+  }
+
+  .cd-input {
+    margin-top: 14px;
+    height: 40px;
+    padding: 0 14px;
+    background: #FFFFFF;
+    border: 1px solid var(--cd-hair);
+    border-radius: 8px;
+    display: flex; align-items: center;
+    font-family: var(--sans);
+    font-size: 12px;
+    color: var(--cd-dim);
+  }
+
+  /* Canvas panel */
+  .cd-canvas {
+    background: #FAF9F5;
+    padding: 40px;
+    overflow: hidden;
+    display: flex; align-items: center; justify-content: center;
+    position: relative;
+  }
+  .cd-poster {
+    width: 780px;
+    aspect-ratio: 4 / 3;
+    background: var(--cd-green);
+    border-radius: 8px;
+    padding: 48px 56px;
+    color: #F5F2E8;
+    display: grid;
+    grid-template-columns: 1.2fr 1fr;
+    gap: 48px;
+    box-shadow: 0 40px 80px -30px rgba(0,0,0,0.4);
+    position: relative;
+    overflow: hidden;
+  }
+  .cd-poster::before {
+    content: '';
+    position: absolute;
+    top: -60px; right: -60px;
+    width: 220px; height: 220px;
+    background: radial-gradient(circle, rgba(245,242,232,0.10), transparent 70%);
+  }
+  .cd-poster-left { position: relative; z-index: 2; }
+  .cd-poster-eyebrow {
+    font-family: var(--sans);
+    font-size: 11px;
+    font-weight: 500;
+    letter-spacing: 0.22em;
+    text-transform: uppercase;
+    opacity: 0.65;
+    margin-bottom: 28px;
+  }
+  .cd-poster-title {
+    font-family: var(--serif-en);
+    font-size: 76px;
+    font-weight: 500;
+    line-height: 0.95;
+    letter-spacing: -0.02em;
+    margin-bottom: 20px;
+  }
+  .cd-poster-sub {
+    font-family: var(--sans);
+    font-size: 14px;
+    opacity: 0.75;
+    line-height: 1.5;
+    margin-bottom: 40px;
+  }
+  .cd-poster-pines {
+    display: flex; gap: 10px;
+    opacity: 0.35;
+  }
+  .cd-pine {
+    width: 0; height: 0;
+    border-left: 10px solid transparent;
+    border-right: 10px solid transparent;
+    border-bottom: 20px solid #F5F2E8;
+    position: relative;
+  }
+  .cd-pine::after {
+    content: '';
+    position: absolute;
+    bottom: -24px; left: 50%;
+    transform: translateX(-50%);
+    width: 3px; height: 6px;
+    background: #F5F2E8;
+  }
+  .cd-schedule {
+    background: rgba(245,242,232,0.08);
+    border: 1px solid rgba(245,242,232,0.15);
+    border-radius: 6px;
+    padding: 20px 22px;
+    position: relative;
+    z-index: 2;
+  }
+  .cd-schedule-title {
+    font-family: var(--sans);
+    font-size: 10px;
+    font-weight: 600;
+    letter-spacing: 0.18em;
+    text-transform: uppercase;
+    opacity: 0.6;
+    margin-bottom: 14px;
+  }
+  .cd-schedule-row {
+    display: flex; justify-content: space-between;
+    font-family: var(--sans);
+    font-size: 12px;
+    padding: 8px 0;
+    border-bottom: 1px solid rgba(245,242,232,0.10);
+  }
+  .cd-schedule-row:last-child { border-bottom: none; }
+  .cd-schedule-row .time { opacity: 0.65; font-variant-numeric: tabular-nums; }
+
+  /* Caption for Act 0 */
+  .cd-caption {
+    position: absolute;
+    bottom: 100px;
+    left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--sans);
+    font-size: 88px;
+    font-weight: 200;
+    letter-spacing: -0.035em;
+    color: var(--ink);
+    text-align: center;
+    opacity: 0;
+    z-index: 60;
+    text-shadow: 0 10px 50px rgba(0,0,0,0.9);
+    will-change: opacity, transform;
+  }
+  .cd-caption .period { color: var(--accent); }
+
+  /* Act 0.5 — pivot */
+  .act05 {
+    flex-direction: column;
+  }
+  .pivot-line {
+    font-family: var(--sans);
+    font-size: 112px;
+    font-weight: 200;
+    letter-spacing: -0.04em;
+    color: var(--ink);
+    text-align: center;
+    line-height: 1.05;
+    will-change: opacity, transform, font-variation-settings;
+  }
+  .pivot-line .accent { color: var(--accent); font-weight: inherit; }
+  .pivot-line .faint { color: var(--muted); }
+</style>
+</head>
+<body>
+
+<div class="stage" id="stage">
+
+  <div class="watermark" id="watermark">Created by Huashu-Design</div>
+
+  <!-- ========== Act 0: Claude Design 致敬 ========== -->
+  <div class="scene act0" id="act0ClaudeDesign">
+    <div class="cd-browser" id="cdBrowser">
+      <!-- Chrome bar -->
+      <div class="cd-chrome">
+        <div class="cd-traffic">
+          <span class="d r"></span><span class="d y"></span><span class="d g"></span>
+        </div>
+        <div class="cd-urlbar"><span class="lock"></span>claude.ai/design</div>
+        <div style="width: 56px;"></div>
+      </div>
+      <!-- Tabs row -->
+      <div class="cd-tabs-row">
+        <div class="cd-tab active"><span class="dot"></span>Company offsite html</div>
+        <div class="cd-tab">Dashboard exploration</div>
+        <div class="cd-tab">Landing v2</div>
+        <div class="cd-topbar-right">
+          <span>100%</span>
+          <span class="btn ghost">Export</span>
+          <span class="btn">Share</span>
+        </div>
+      </div>
+      <!-- Body: split chat + canvas -->
+      <div class="cd-body">
+        <div class="cd-chat">
+          <div class="cd-msg">
+            <div class="cd-avatar user">Y</div>
+            <div class="cd-bubble">Make a welcome guide for our company retreat.</div>
+          </div>
+          <div class="cd-msg">
+            <div class="cd-avatar claude">C</div>
+            <div class="cd-bubble">I've designed a 1-page landscape welcome guide for your planning day. It includes a branded cover with pine trees, a two-column schedule, and activity cards.<br/><br/><span class="dim">Toggle the Tweaks to adjust accent color, headline size, and density.</span></div>
+          </div>
+          <div class="cd-tweaks">
+            <div class="cd-tweaks-title">Tweaks</div>
+            <div class="cd-tweak-row">
+              <div class="cd-tweak-label">Accent</div>
+              <div class="cd-color-dots">
+                <div class="cd-color-dot" style="background:#2D4A3A;" id="cdDot1"></div>
+                <div class="cd-color-dot active" style="background:#D97757;" id="cdDot2"></div>
+                <div class="cd-color-dot" style="background:#3F5E8A;" id="cdDot3"></div>
+                <div class="cd-color-dot" style="background:#8B6F4A;" id="cdDot4"></div>
+              </div>
+            </div>
+            <div class="cd-tweak-row">
+              <div class="cd-tweak-label">Headline</div>
+              <div class="cd-tweak-track"><div class="cd-tweak-thumb" id="cdThumb1" style="left: 58%;"></div></div>
+            </div>
+            <div class="cd-tweak-row">
+              <div class="cd-tweak-label">Density</div>
+              <div class="cd-tweak-track"><div class="cd-tweak-thumb" id="cdThumb2" style="left: 40%;"></div></div>
+            </div>
+          </div>
+          <div class="cd-input">Describe what you want next…</div>
+        </div>
+        <div class="cd-canvas">
+          <div class="cd-poster" id="cdPoster">
+            <div class="cd-poster-left">
+              <div class="cd-poster-eyebrow">Anthropic Labs · Planning Day</div>
+              <div class="cd-poster-title">HEMLARK<br/>RETREAT '26</div>
+              <div class="cd-poster-sub">June 14 · Full Day<br/>Pine Valley Lodge</div>
+              <div class="cd-poster-pines">
+                <div class="cd-pine"></div>
+                <div class="cd-pine"></div>
+                <div class="cd-pine"></div>
+                <div class="cd-pine"></div>
+              </div>
+            </div>
+            <div class="cd-schedule">
+              <div class="cd-schedule-title">Schedule</div>
+              <div class="cd-schedule-row"><span>Breakfast</span><span class="time">9:00</span></div>
+              <div class="cd-schedule-row"><span>Kickoff</span><span class="time">10:00</span></div>
+              <div class="cd-schedule-row"><span>Workshops</span><span class="time">10:30</span></div>
+              <div class="cd-schedule-row"><span>Lunch</span><span class="time">12:30</span></div>
+              <div class="cd-schedule-row"><span>Hike</span><span class="time">14:00</span></div>
+              <div class="cd-schedule-row"><span>Dinner</span><span class="time">18:00</span></div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="cd-caption" id="cdCaption">It's beautiful<span class="period">.</span></div>
+  </div>
+
+  <!-- ========== Act 0.5: Pivot ========== -->
+  <div class="scene act05" id="act05Pivot">
+    <div class="pivot-line" id="pivotLine">
+      But it isn't the <span class="accent">future</span>.
+    </div>
+  </div>
+
+  <!-- ========== Act 1 ========== -->
+  <div class="scene act1" id="act1a">
+    <div class="hero-line" id="heroLine">
+      Here's to the <span class="accent">Agents</span>.
+    </div>
+  </div>
+
+  <div class="scene act1" id="act1b">
+    <!-- "Not the ones who click." + abstract mouse -->
+    <div class="gui-glyph click" id="glyphClick" style="left: 50%; top: 62%; transform: translate(-50%, -50%);">
+      <svg viewBox="0 0 24 24" fill="none">
+        <path d="M4 2l6 18 3-8 8-3L4 2z" stroke="rgba(255,255,255,0.55)" stroke-width="1.4" fill="rgba(255,255,255,0.12)" stroke-linejoin="round"/>
+      </svg>
+    </div>
+    <div class="not-line" id="notLine1" style="position: absolute; top: 28%; left: 50%; transform: translateX(-50%); white-space: nowrap;">
+      Not the ones who <span class="strike">click</span>.
+    </div>
+  </div>
+
+  <div class="scene act1" id="act1c">
+    <!-- "Not the ones who drag." + slider -->
+    <div class="gui-glyph drag" id="glyphDrag" style="left: 50%; top: 62%; transform: translate(-50%, -50%);">
+      <div class="track">
+        <div class="fill"></div>
+        <div class="thumb" id="sliderThumb"></div>
+      </div>
+    </div>
+    <div class="not-line" id="notLine2" style="position: absolute; top: 28%; left: 50%; transform: translateX(-50%); white-space: nowrap;">
+      Not the ones who <span class="strike">drag</span>.
+    </div>
+  </div>
+
+  <div class="scene act1" id="act1d">
+    <!-- "Not the ones who wait..." + folder window -->
+    <div class="gui-glyph folder" id="glyphFolder" style="left: 50%; top: 62%; transform: translate(-50%, -50%);">
+      <div class="head">
+        <span class="d"></span><span class="d"></span><span class="d"></span>
+      </div>
+      <div class="row"><span>design-v1.fig</span><span class="meta">42 KB</span></div>
+      <div class="row"><span>design-v2-final.fig</span><span class="meta">58 KB</span></div>
+      <div class="row"><span>design-v2-FINAL-final.fig</span><span class="meta">61 KB</span></div>
+      <div class="row"><span>design-v3.fig</span><span class="meta">65 KB</span></div>
+    </div>
+    <div class="not-line" id="notLine3" style="position: absolute; top: 22%; left: 50%; transform: translateX(-50%); white-space: nowrap; font-size: 72px;">
+      Not the ones who <span class="strike">wait for you to open the file</span>.
+    </div>
+  </div>
+
+  <!-- ========== Act 2 ========== -->
+  <div class="scene act2" id="act2Terminal">
+    <div class="terminal" id="terminal">
+      <div class="tty-head">
+        <span class="d red"></span>
+        <span class="d yellow"></span>
+        <span class="d green"></span>
+        <span class="tty-title">huashu — claude code</span>
+      </div>
+      <div class="tty-body">
+        <span class="prompt">$</span><span class="typed" id="typed"></span><span class="cursor" id="cursor"></span>
+      </div>
+    </div>
+  </div>
+
+  <div class="scene" id="act2Gallery">
+    <div class="gallery-viewport">
+      <div class="gallery-canvas" id="galleryCanvas"></div>
+    </div>
+  </div>
+
+  <div class="over-statement" id="overStmt1">
+    <div class="text">The ones who design<br/>while you <span class="accent">sleep</span>.</div>
+  </div>
+
+  <div class="over-statement" id="overStmt2">
+    <div class="text">The ones who ship<br/>while you're in a <span class="accent">meeting</span>.</div>
+  </div>
+
+  <!-- ========== Act 3 ========== -->
+  <div class="scene act3" id="act3Medium">
+    <div class="statement-big" id="stmtMedium">
+      <span class="accent">Agent</span> is the<br/>new medium.
+    </div>
+  </div>
+
+  <div class="scene act3" id="act3Brand">
+    <div class="brand-wordmark" id="wordmark">huashu<span class="accent">-</span>design</div>
+    <div class="farewell-quote" id="farewell">For them, we built this.</div>
+    <div class="farewell-cn" id="farewellCn">· 为 他 们 · 我 们 造 了 这 个 ·</div>
+    <div class="brand-url" id="url">huasheng.ai/huashu-design-hero</div>
+  </div>
+
+</div>
+
+<script>
+(function() {
+  // ---------- Fit stage ----------
+  const stage = document.getElementById('stage');
+  function rescale() {
+    const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  rescale();
+  window.addEventListener('resize', rescale);
+
+  const SLIDE_FILES = [
+    'preview-01-cover.png','preview-02-quote.png','preview-03-intro.png','preview-04-toc.png',
+    'preview-05-divider-1.png','preview-06-seldon.png','preview-07-human-psych-limit.png','preview-08-ai-vs-human.png',
+    'preview-09-divider-2.png','preview-10-personas.png','preview-11-four-puzzles.png','preview-12-phenomena-1-2.png',
+    'preview-13-phenomena-3-4.png','preview-14-five-voices.png','preview-15-divider-3.png','preview-16-persona-selection.png',
+    'preview-17-persona-space.png','preview-18-emergent-misalignment.png','preview-19-inoculation.png','preview-20-emotion.png',
+    'preview-21-dosage.png','preview-22-steering.png','preview-23-expression-vs-impact.png','preview-24-concept-injection.png',
+    'preview-25-consciousness-prob.png','preview-26-divider-4.png','preview-27-cot-faithfulness.png','preview-28-alignment-faking.png',
+    'preview-29-divider-5.png','preview-30-open-questions.png','preview-31-giving-back.png','preview-32-closing.png',
+  ];
+  const BASE = '../../../2026.04-AI心理学/演讲PPT-北大/';
+
+  // ---------- Build gallery ----------
+  const COLS = 8, ROWS = 6, COUNT = COLS * ROWS;
+  const galleryCanvas = document.getElementById('galleryCanvas');
+  const galleryCards = [];
+  for (let i = 0; i < COUNT; i++) {
+    const slideIdx = i % 32;
+    const card = document.createElement('div');
+    card.className = 'gallery-card';
+    const zIdx = Math.sin(i * 1.7) * 22 + Math.cos(i * 0.73) * 14;
+    if (zIdx > 12) card.classList.add('depth-near');
+    else if (zIdx < -12) card.classList.add('depth-far');
+    const img = document.createElement('img');
+    img.src = BASE + SLIDE_FILES[slideIdx];
+    img.onerror = () => { img.src = BASE + 'preview-01-cover.png'; };
+    card.appendChild(img);
+    galleryCanvas.appendChild(card);
+    galleryCards.push(card);
+  }
+  for (let i = 0; i < 32; i++) {
+    const im = new Image();
+    im.src = BASE + SLIDE_FILES[i];
+  }
+
+  // ---------- Easings ----------
+  const easeOut = t => 1 - Math.pow(1 - t, 3);
+  const expoOut = t => (t <= 0) ? 0 : (t >= 1) ? 1 : 1 - Math.pow(2, -10 * t);
+  const easeInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;
+  function lerp(time, start, end, fromV, toV, easing) {
+    if (time <= start) return fromV;
+    if (time >= end) return toV;
+    let p = (time - start) / (end - start);
+    if (easing) p = easing(p);
+    return fromV + (toV - fromV) * p;
+  }
+  function clampLerp(time, start, end) {
+    if (time <= start) return 0;
+    if (time >= end) return 1;
+    return (time - start) / (end - start);
+  }
+
+  // ---------- Timeline (30s) ----------
+  const T = {
+    DURATION: 30.0,
+
+    // ===== Act 0: Claude Design 致敬 (0 - 4s) =====
+    a0_in:   [0.3, 1.2],       // browser fade + scale in
+    a0_hold: [1.2, 3.4],       // tweaks 自动动
+    a0_out:  [3.4, 4.0],       // browser 退场
+
+    cd_tweak_anim: [1.4, 3.3], // tweaks thumb 自动拖动窗口
+    cd_accent_switch: [2.1, 2.5], // accent color dot 切换到深绿
+
+    cd_caption_in:  [1.6, 2.2],
+    cd_caption_hold:[2.2, 3.3],
+    cd_caption_out: [3.3, 3.8],
+
+    // ===== Act 0.5: Pivot (3.9 - 5.2s) =====
+    a05_in:   [3.9, 4.6],
+    a05_hold: [4.6, 4.9],
+    a05_out:  [4.9, 5.3],
+
+    // ===== Act 1 (shifted +5s) =====
+    a1a_in:  [5.3, 6.3],       // "Here's to the Agents."
+    a1a_hold:[6.3, 7.8],
+    a1a_out: [7.8, 8.3],
+
+    a1b_in:  [8.2, 8.9],       // "Not the ones who click."
+    a1b_hold:[8.9, 10.3],
+    a1b_out: [10.3, 10.8],
+
+    a1c_in:  [10.7, 11.3],     // "Not the ones who drag."
+    a1c_hold:[11.3, 12.5],
+    a1c_out: [12.5, 13.0],
+
+    a1d_in:  [12.9, 13.5],     // "Not the ones who wait..."
+    a1d_hold:[13.5, 15.2],
+    a1d_out: [15.2, 15.7],
+
+    // ===== Act 2 (shifted +5s) =====
+    a2tty_in: [15.6, 16.2],    // terminal in
+    a2type:   [16.4, 18.6],
+    a2tty_out:[18.9, 19.4],
+
+    a2gal_in: [19.1, 19.9],    // gallery ripple start
+    ripple:   [19.9, 21.6],
+    panStart: 20.2,
+    a2gal_out:[25.5, 26.2],
+
+    // Overlay statements on gallery
+    stmt1:    [21.7, 23.4],    // "design while you sleep"
+    stmt2:    [23.7, 25.4],    // "ship while you're in a meeting"
+
+    // ===== Act 3 (shifted +5s) =====
+    a3med_in: [26.1, 27.0],    // "Agent is the new medium"
+    a3med_hold:[27.0, 28.0],
+    a3med_out:[28.0, 28.4],
+
+    a3brand_in:  [28.3, 29.0],
+    brand_morph: [28.7, 29.4],
+    a3farewell_in: [29.0, 29.6],
+    a3cn_in: [29.3, 29.8],
+    a3url_in: [29.5, 30.0],
+  };
+
+  // ---------- Elements ----------
+  const scenes = {
+    a0: document.getElementById('act0ClaudeDesign'),
+    a05: document.getElementById('act05Pivot'),
+    a1a: document.getElementById('act1a'),
+    a1b: document.getElementById('act1b'),
+    a1c: document.getElementById('act1c'),
+    a1d: document.getElementById('act1d'),
+    a2tty: document.getElementById('act2Terminal'),
+    a2gal: document.getElementById('act2Gallery'),
+    a3med: document.getElementById('act3Medium'),
+    a3brand: document.getElementById('act3Brand'),
+  };
+  const cdBrowser = document.getElementById('cdBrowser');
+  const cdCaption = document.getElementById('cdCaption');
+  const cdThumb1 = document.getElementById('cdThumb1');
+  const cdThumb2 = document.getElementById('cdThumb2');
+  const cdDot1 = document.getElementById('cdDot1');
+  const cdDot2 = document.getElementById('cdDot2');
+  const cdPoster = document.getElementById('cdPoster');
+  const pivotLine = document.getElementById('pivotLine');
+  const overs = {
+    stmt1: document.getElementById('overStmt1'),
+    stmt2: document.getElementById('overStmt2'),
+  };
+  const heroLine = document.getElementById('heroLine');
+  const notLine1 = document.getElementById('notLine1');
+  const notLine2 = document.getElementById('notLine2');
+  const notLine3 = document.getElementById('notLine3');
+  const glyphClick = document.getElementById('glyphClick');
+  const glyphDrag = document.getElementById('glyphDrag');
+  const sliderThumb = document.getElementById('sliderThumb');
+  const glyphFolder = document.getElementById('glyphFolder');
+  const terminal = document.getElementById('terminal');
+  const typed = document.getElementById('typed');
+  const cursor = document.getElementById('cursor');
+  const stmtMedium = document.getElementById('stmtMedium');
+  const wordmark = document.getElementById('wordmark');
+  const farewell = document.getElementById('farewell');
+  const farewellCn = document.getElementById('farewellCn');
+  const urlEl = document.getElementById('url');
+  const watermark = document.getElementById('watermark');
+
+  const COMMAND = '/huashu-design 做一份发布会PPT';
+
+  // ---------- Gallery transforms ----------
+  const GALLERY_TILT = 'perspective(2400px) rotateX(14deg) rotateY(-10deg) rotateZ(-2deg)';
+  const GALLERY_SCALE = 0.94;
+  function galleryTransform(dx, dy, extraScale = 1) {
+    return `translate(-50%, -50%) translate(${dx}px, ${dy}px) scale(${GALLERY_SCALE * extraScale}) ${GALLERY_TILT}`;
+  }
+
+  // ---------- Helpers to show/hide scenes ----------
+  function showScene(key, opacity) {
+    const el = scenes[key];
+    if (opacity > 0.001) el.classList.add('visible');
+    else el.classList.remove('visible');
+    el.style.opacity = opacity;
+  }
+
+  function showOver(key, opacity) {
+    const el = overs[key];
+    el.style.opacity = opacity;
+  }
+
+  // ---------- Render ----------
+  function render(t) {
+    // ============ Act 0: Claude Design 致敬 ============
+    if (t < T.a0_out[1]) {
+      let op;
+      if (t < T.a0_in[1]) op = lerp(t, T.a0_in[0], T.a0_in[1], 0, 1, expoOut);
+      else if (t < T.a0_out[0]) op = 1;
+      else op = lerp(t, T.a0_out[0], T.a0_out[1], 1, 0, easeOut);
+      showScene('a0', op);
+
+      // Browser: subtle breathing scale + exit shrink
+      const scaleIn = lerp(t, T.a0_in[0], T.a0_in[1], 0.94, 1.0, expoOut);
+      let scaleOut = 1.0;
+      let blurOut = 0;
+      if (t >= T.a0_out[0]) {
+        const p = clampLerp(t, T.a0_out[0], T.a0_out[1]);
+        scaleOut = 1.0 - 0.08 * p;
+        blurOut = 6 * p;
+      }
+      const finalScale = Math.min(scaleIn, scaleOut);
+      cdBrowser.style.transform = `translate(-50%, -50%) scale(${finalScale})`;
+      cdBrowser.style.filter = blurOut > 0.1 ? `blur(${blurOut}px)` : '';
+
+      // Tweaks thumb 自动拖动(模拟用户在调节)
+      const tw = clampLerp(t, T.cd_tweak_anim[0], T.cd_tweak_anim[1]);
+      // Headline slider: 58% → 72% → 62%
+      let headlinePct;
+      if (tw < 0.5) headlinePct = 58 + (72 - 58) * easeInOut(tw * 2);
+      else headlinePct = 72 + (62 - 72) * easeInOut((tw - 0.5) * 2);
+      cdThumb1.style.left = headlinePct + '%';
+      // Density slider: 40% → 55%
+      const densityPct = 40 + 15 * easeInOut(tw);
+      cdThumb2.style.left = densityPct + '%';
+
+      // Accent 从橙切换到深绿(模拟用户在选色)
+      const switched = t >= T.cd_accent_switch[0];
+      if (switched) {
+        cdDot1.classList.add('active');
+        cdDot2.classList.remove('active');
+        // Poster 颜色跟着变
+        cdPoster.style.background = 'var(--cd-green)';
+      } else {
+        cdDot1.classList.remove('active');
+        cdDot2.classList.add('active');
+        cdPoster.style.background = '#B85D3D';
+      }
+
+      // Caption "It's beautiful."
+      let capOp = 0;
+      if (t >= T.cd_caption_in[0] && t < T.cd_caption_out[1]) {
+        if (t < T.cd_caption_in[1]) capOp = clampLerp(t, T.cd_caption_in[0], T.cd_caption_in[1]);
+        else if (t < T.cd_caption_out[0]) capOp = 1;
+        else capOp = 1 - clampLerp(t, T.cd_caption_out[0], T.cd_caption_out[1]);
+      }
+      const capRise = lerp(t, T.cd_caption_in[0], T.cd_caption_in[1], 14, 0, expoOut);
+      cdCaption.style.opacity = capOp;
+      cdCaption.style.transform = `translateX(-50%) translateY(${capRise}px)`;
+    } else {
+      showScene('a0', 0);
+    }
+
+    // ============ Act 0.5: Pivot — "But it isn't the future." ============
+    if (t >= T.a05_in[0] - 0.1 && t < T.a05_out[1]) {
+      let op;
+      if (t < T.a05_in[1]) op = lerp(t, T.a05_in[0], T.a05_in[1], 0, 1, expoOut);
+      else if (t < T.a05_out[0]) op = 1;
+      else op = lerp(t, T.a05_out[0], T.a05_out[1], 1, 0, easeOut);
+      showScene('a05', op);
+
+      const rise = lerp(t, T.a05_in[0], T.a05_in[1], 16, 0, expoOut);
+      pivotLine.style.transform = `translate3d(0, ${rise}px, 0)`;
+
+      // Subtle weight morph on "But it isn't the future."
+      const morph = expoOut(clampLerp(t, T.a05_in[0], T.a05_in[1] + 0.3));
+      const w = 120 + (300 - 120) * morph;
+      pivotLine.style.fontVariationSettings = `"wght" ${w.toFixed(0)}`;
+      pivotLine.style.fontWeight = Math.round(w);
+    } else {
+      showScene('a05', 0);
+    }
+
+    // ============ Act 1a: "Here's to the Agents." ============
+    if (t >= T.a1a_in[0] - 0.1 && t < T.a1a_out[1]) {
+      let op;
+      if (t < T.a1a_in[1]) op = lerp(t, T.a1a_in[0], T.a1a_in[1], 0, 1, expoOut);
+      else if (t < T.a1a_out[0]) op = 1;
+      else op = lerp(t, T.a1a_out[0], T.a1a_out[1], 1, 0, easeOut);
+      showScene('a1a', op);
+
+      // Weight morph 100 → 400 on "Here's to the Agents."
+      const morph = expoOut(clampLerp(t, T.a1a_in[0], T.a1a_in[1] + 0.6));
+      const w = 100 + (400 - 100) * morph;
+      heroLine.style.fontVariationSettings = `"wght" ${w.toFixed(0)}`;
+      heroLine.style.fontWeight = Math.round(w);
+
+      // Subtle rise
+      const rise = lerp(t, T.a1a_in[0], T.a1a_in[1], 18, 0, expoOut);
+      heroLine.style.transform = `translate3d(0, ${rise}px, 0)`;
+    } else {
+      showScene('a1a', 0);
+    }
+
+    // ============ Act 1b: Not the ones who click. ============
+    if (t >= T.a1b_in[0] - 0.1 && t < T.a1b_out[1]) {
+      let op;
+      if (t < T.a1b_in[1]) op = lerp(t, T.a1b_in[0], T.a1b_in[1], 0, 1, expoOut);
+      else if (t < T.a1b_out[0]) op = 1;
+      else op = lerp(t, T.a1b_out[0], T.a1b_out[1], 1, 0, easeOut);
+      showScene('a1b', op);
+
+      // Animate the click glyph: appear, then trigger click ring + shake
+      const glyphIn = clampLerp(t, T.a1b_in[0] + 0.15, T.a1b_in[1]);
+      glyphClick.style.opacity = expoOut(glyphIn);
+
+      // Shake at mid-hold
+      const clickT = t - (T.a1b_in[1] + 0.3);
+      if (clickT > 0 && clickT < 0.4) {
+        glyphClick.style.transform = `translate(-50%, -50%) translate(${Math.sin(clickT * 60) * 3}px, 0)`;
+      } else {
+        glyphClick.style.transform = `translate(-50%, -50%)`;
+      }
+
+      // Strike the word "click" at halfway through hold
+      const strikeOn = t >= T.a1b_in[1] + 0.5;
+      notLine1.classList.toggle('struck', strikeOn);
+    } else {
+      showScene('a1b', 0);
+      glyphClick.style.opacity = 0;
+    }
+
+    // ============ Act 1c: Not the ones who drag. ============
+    if (t >= T.a1c_in[0] - 0.1 && t < T.a1c_out[1]) {
+      let op;
+      if (t < T.a1c_in[1]) op = lerp(t, T.a1c_in[0], T.a1c_in[1], 0, 1, expoOut);
+      else if (t < T.a1c_out[0]) op = 1;
+      else op = lerp(t, T.a1c_out[0], T.a1c_out[1], 1, 0, easeOut);
+      showScene('a1c', op);
+
+      const glyphIn = clampLerp(t, T.a1c_in[0] + 0.15, T.a1c_in[1]);
+      glyphDrag.style.opacity = expoOut(glyphIn);
+
+      // Animate slider thumb 30% → 70% position during hold
+      const dragT = clampLerp(t, T.a1c_hold[0], T.a1c_hold[1] - 0.2);
+      const leftPct = 30 + 40 * easeInOut(dragT);
+      sliderThumb.style.left = leftPct + '%';
+      const fillEl = glyphDrag.querySelector('.fill');
+      if (fillEl) fillEl.style.width = leftPct + '%';
+    } else {
+      showScene('a1c', 0);
+      glyphDrag.style.opacity = 0;
+    }
+
+    // ============ Act 1d: Not the ones who wait for you to open the file. ============
+    if (t >= T.a1d_in[0] - 0.1 && t < T.a1d_out[1]) {
+      let op;
+      if (t < T.a1d_in[1]) op = lerp(t, T.a1d_in[0], T.a1d_in[1], 0, 1, expoOut);
+      else if (t < T.a1d_out[0]) op = 1;
+      else op = lerp(t, T.a1d_out[0], T.a1d_out[1], 1, 0, easeOut);
+      showScene('a1d', op);
+
+      const glyphIn = clampLerp(t, T.a1d_in[0] + 0.15, T.a1d_in[1]);
+      glyphFolder.style.opacity = expoOut(glyphIn);
+    } else {
+      showScene('a1d', 0);
+      glyphFolder.style.opacity = 0;
+    }
+
+    // ============ Act 2 Terminal ============
+    if (t >= T.a2tty_in[0] - 0.1 && t < T.a2tty_out[1]) {
+      let op;
+      if (t < T.a2tty_in[1]) op = lerp(t, T.a2tty_in[0], T.a2tty_in[1], 0, 1, expoOut);
+      else if (t < T.a2tty_out[0]) op = 1;
+      else op = lerp(t, T.a2tty_out[0], T.a2tty_out[1], 1, 0, easeOut);
+      showScene('a2tty', op);
+
+      const rise = lerp(t, T.a2tty_in[0], T.a2tty_in[1], 28, 0, expoOut);
+      terminal.style.transform = `translate3d(0, ${rise}px, 0)`;
+
+      // Typing
+      if (t < T.a2type[0]) typed.textContent = '';
+      else if (t < T.a2type[1]) {
+        const p = (t - T.a2type[0]) / (T.a2type[1] - T.a2type[0]);
+        const n = Math.floor(p * COMMAND.length);
+        typed.textContent = COMMAND.slice(0, n);
+      } else typed.textContent = COMMAND;
+
+      cursor.style.opacity = (Math.floor(t * 2.5) % 2 === 0) ? 1 : 0.25;
+    } else {
+      showScene('a2tty', 0);
+    }
+
+    // ============ Act 2 Gallery + statements ============
+    if (t >= T.a2gal_in[0] - 0.1 && t < T.a2gal_out[1]) {
+      let op;
+      if (t < T.a2gal_in[1]) op = lerp(t, T.a2gal_in[0], T.a2gal_in[1], 0, 1, expoOut);
+      else if (t < T.a2gal_out[0]) op = 1;
+      else op = lerp(t, T.a2gal_out[0], T.a2gal_out[1], 1, 0, easeOut);
+      showScene('a2gal', op);
+
+      // Pan
+      const panT = Math.max(0, t - T.panStart);
+      const panX = Math.sin(panT * 0.12) * 180 - panT * 6;
+      const panY = Math.cos(panT * 0.09) * 100 - panT * 4;
+      const cX = Math.max(-600, Math.min(600, panX));
+      const cY = Math.max(-400, Math.min(400, panY));
+
+      // Ripple
+      const inRipple = t < T.ripple[1];
+      const rippleP = clampLerp(t, T.ripple[0], T.ripple[1]);
+      const galScale = inRipple ? (1.25 - 0.31 * expoOut(rippleP)) : 1.0;
+      galleryCanvas.style.transform = galleryTransform(cX, cY, galScale);
+
+      // Per-card ripple entry
+      galleryCards.forEach((card, i) => {
+        let entryOp = 1;
+        if (inRipple) {
+          const col = i % COLS, row = Math.floor(i / COLS);
+          const dc = col - (COLS - 1) / 2, dr = row - (ROWS - 1) / 2;
+          const dist = Math.sqrt(dc * dc + dr * dr);
+          const maxDist = Math.sqrt(((COLS - 1) / 2) ** 2 + ((ROWS - 1) / 2) ** 2);
+          const delay = (dist / maxDist) * 0.8;
+          const localT = Math.max(0, (t - T.ripple[0] - delay) / 0.7);
+          entryOp = expoOut(Math.min(1, localT));
+        }
+
+        // Dim when statements are active
+        const stmt1Active = t >= T.stmt1[0] && t < T.stmt1[1];
+        const stmt2Active = t >= T.stmt2[0] && t < T.stmt2[1];
+        const dimAmount = stmt1Active || stmt2Active ? 0.55 : 0;
+
+        if (dimAmount > 0) {
+          card.style.opacity = entryOp * (1 - dimAmount);
+          card.style.filter = `brightness(${1 - 0.3 * dimAmount}) saturate(${1 - 0.4 * dimAmount})`;
+        } else {
+          card.style.opacity = entryOp < 1 ? entryOp : '';
+          card.style.filter = '';
+        }
+      });
+    } else {
+      showScene('a2gal', 0);
+    }
+
+    // Overlay statement 1: "design while you sleep"
+    {
+      let op = 0;
+      if (t >= T.stmt1[0] && t < T.stmt1[1]) {
+        const inP = expoOut(clampLerp(t, T.stmt1[0], T.stmt1[0] + 0.4));
+        const outP = easeOut(clampLerp(t, T.stmt1[1] - 0.4, T.stmt1[1]));
+        op = inP * (1 - outP);
+      }
+      showOver('stmt1', op);
+    }
+    // Overlay statement 2: "ship while meeting"
+    {
+      let op = 0;
+      if (t >= T.stmt2[0] && t < T.stmt2[1]) {
+        const inP = expoOut(clampLerp(t, T.stmt2[0], T.stmt2[0] + 0.4));
+        const outP = easeOut(clampLerp(t, T.stmt2[1] - 0.4, T.stmt2[1]));
+        op = inP * (1 - outP);
+      }
+      showOver('stmt2', op);
+    }
+
+    // ============ Act 3 Medium ============
+    if (t >= T.a3med_in[0] - 0.1 && t < T.a3med_out[1]) {
+      let op;
+      if (t < T.a3med_in[1]) op = lerp(t, T.a3med_in[0], T.a3med_in[1], 0, 1, expoOut);
+      else if (t < T.a3med_out[0]) op = 1;
+      else op = lerp(t, T.a3med_out[0], T.a3med_out[1], 1, 0, easeOut);
+      showScene('a3med', op);
+
+      const morph = expoOut(clampLerp(t, T.a3med_in[0], T.a3med_in[1] + 0.4));
+      const w = 100 + (300 - 100) * morph;
+      stmtMedium.style.fontVariationSettings = `"wght" ${w.toFixed(0)}`;
+      stmtMedium.style.fontWeight = Math.round(w);
+
+      const rise = lerp(t, T.a3med_in[0], T.a3med_in[1], 24, 0, expoOut);
+      stmtMedium.style.transform = `translate3d(0, ${rise}px, 0)`;
+    } else {
+      showScene('a3med', 0);
+    }
+
+    // ============ Act 3 Brand ============
+    if (t >= T.a3brand_in[0] - 0.1) {
+      const op = clampLerp(t, T.a3brand_in[0], T.a3brand_in[1]);
+      showScene('a3brand', op);
+
+      // Wordmark weight morph
+      const morphP = expoOut(clampLerp(t, T.brand_morph[0], T.brand_morph[1]));
+      const wght = 100 + (700 - 100) * morphP;
+      wordmark.style.fontVariationSettings = `"wght" ${wght.toFixed(0)}`;
+      wordmark.style.fontWeight = Math.round(wght);
+
+      const wRise = lerp(t, T.a3brand_in[0], T.a3brand_in[1], 20, 0, expoOut);
+      wordmark.style.transform = `translate3d(0, ${wRise}px, 0)`;
+
+      // Farewell quote
+      const fOp = clampLerp(t, T.a3farewell_in[0], T.a3farewell_in[1]);
+      const fRise = lerp(t, T.a3farewell_in[0], T.a3farewell_in[1], 12, 0, expoOut);
+      farewell.style.opacity = fOp;
+      farewell.style.transform = `translate3d(0, ${fRise}px, 0)`;
+
+      // CN subtitle
+      const cnOp = clampLerp(t, T.a3cn_in[0], T.a3cn_in[1]);
+      farewellCn.style.opacity = cnOp;
+
+      // URL
+      const uOp = clampLerp(t, T.a3url_in[0], T.a3url_in[1]);
+      urlEl.style.opacity = uOp;
+    } else {
+      showScene('a3brand', 0);
+    }
+
+    // Watermark: visible during Act 2-3
+    if (t >= T.a2tty_in[0] && t < T.DURATION - 0.2) {
+      watermark.classList.add('visible');
+    } else {
+      watermark.classList.remove('visible');
+    }
+  }
+
+  // ---------- Driver ----------
+  let manualT = null;
+  let startMs = null;
+  let hasFinishedOnce = false;
+  function tick(now) {
+    if (manualT != null) render(manualT);
+    else {
+      if (startMs == null) startMs = now;
+      const elapsed = (now - startMs) / 1000;
+      const recording = window.__recording === true;
+      let t;
+      if (recording) {
+        // Non-looping: clamp at DURATION, hold on final frame
+        t = Math.min(elapsed, T.DURATION - 0.001);
+        if (elapsed >= T.DURATION && !hasFinishedOnce) hasFinishedOnce = true;
+      } else {
+        t = elapsed % T.DURATION;
+      }
+      render(t);
+    }
+    requestAnimationFrame(tick);
+  }
+  requestAnimationFrame(tick);
+
+  // For frame-accurate rendering
+  window.__setTime = function(t) {
+    manualT = t;
+    render(t);
+  };
+  window.__resume = function() { manualT = null; startMs = null; };
+  window.__duration = T.DURATION;
+  window.__render = render;
+  window.__ready = true;
+})();
+</script>
+</body>
+</html>

+ 1498 - 0
demos/hero-animation-v10.html

@@ -0,0 +1,1498 @@
+<!doctype html>
+<html lang="zh-Hans">
+<head>
+<meta charset="utf-8" />
+<title>huashu-design · 敬 Agent 时代(v10 中文版)</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<style>
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --dim: rgba(255,255,255,0.18);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;          /* terracotta — 致敬 Anthropic 血统 */
+    --accent-deep: #B85D3D;
+
+    /* Claude Design palette — Act 0 专用 */
+    --cd-bg: #F5F4F0;
+    --cd-panel: #FFFFFF;
+    --cd-ink: #1A1918;
+    --cd-dim: #8B867E;
+    --cd-hair: rgba(0,0,0,0.08);
+    --cd-hair-strong: rgba(0,0,0,0.16);
+    --cd-green: #2D4A3A;
+    --cd-green-deep: #1E3428;
+    --cd-green-soft: #3F5E4D;
+
+    --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
+    --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
+    -webkit-font-smoothing: antialiased;
+  }
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+  }
+
+  .scene {
+    position: absolute; inset: 0;
+    display: flex; align-items: center; justify-content: center;
+    opacity: 0;
+    visibility: hidden;
+    will-change: opacity, transform;
+  }
+  .scene.visible { visibility: visible; }
+
+  /* ============ Act 1 ============ */
+  .act1 {
+    flex-direction: column;
+    gap: 40px;
+  }
+  .hero-line {
+    font-family: var(--sans);
+    font-size: 132px;
+    font-weight: 200;
+    letter-spacing: -0.045em;
+    color: var(--ink);
+    text-align: center;
+    line-height: 1.02;
+    will-change: transform, opacity, font-variation-settings;
+  }
+  .hero-line .accent { color: var(--accent); font-weight: inherit; }
+
+  .not-line {
+    font-family: var(--sans);
+    font-size: 96px;
+    font-weight: 200;
+    letter-spacing: -0.035em;
+    color: var(--ink);
+    text-align: center;
+    line-height: 1.08;
+  }
+  .not-line .strike {
+    color: var(--muted);
+    text-decoration: line-through;
+    text-decoration-thickness: 3px;
+    text-decoration-color: var(--accent);
+  }
+
+  /* ============ Abstract GUI icons (no real product screenshots) ============ */
+  .gui-glyph {
+    position: absolute;
+    opacity: 0;
+    will-change: opacity, transform, filter;
+  }
+  .gui-glyph.click {
+    /* Mouse cursor arrow */
+    width: 120px; height: 120px;
+    display: flex; align-items: center; justify-content: center;
+  }
+  .gui-glyph.click::before {
+    content: '';
+    width: 40px; height: 40px;
+    border: 2px solid var(--muted);
+    border-radius: 50%;
+    position: absolute;
+    animation: clickring 0.8s ease-out forwards;
+    animation-play-state: paused;
+  }
+  @keyframes clickring {
+    0%   { transform: scale(0.5); opacity: 0.8; }
+    100% { transform: scale(2.2); opacity: 0; }
+  }
+  .gui-glyph.click svg { width: 56px; height: 56px; position: relative; z-index: 2; }
+
+  .gui-glyph.drag {
+    /* Slider */
+    width: 400px; height: 48px;
+    display: flex; align-items: center;
+    gap: 0;
+  }
+  .gui-glyph.drag .track {
+    flex: 1;
+    height: 3px;
+    background: var(--hairline);
+    border-radius: 2px;
+    position: relative;
+  }
+  .gui-glyph.drag .fill {
+    position: absolute;
+    height: 100%;
+    background: var(--muted);
+    width: 30%;
+    border-radius: 2px;
+  }
+  .gui-glyph.drag .thumb {
+    position: absolute;
+    width: 24px; height: 24px;
+    background: var(--ink);
+    border: 1px solid var(--muted);
+    border-radius: 50%;
+    top: 50%;
+    left: 30%;
+    transform: translate(-50%, -50%);
+  }
+
+  .gui-glyph.folder {
+    /* Window frame w/ file list */
+    width: 420px; height: 260px;
+    background: rgba(255,255,255,0.02);
+    border: 1px solid var(--hairline);
+    border-radius: 10px;
+    overflow: hidden;
+  }
+  .gui-glyph.folder .head {
+    padding: 12px 16px;
+    border-bottom: 1px solid var(--hairline);
+    display: flex; gap: 8px;
+  }
+  .gui-glyph.folder .head .dot {
+    width: 9px; height: 9px; border-radius: 50%;
+    background: var(--hairline);
+  }
+  .gui-glyph.folder .row {
+    padding: 10px 16px;
+    font-family: var(--mono);
+    font-size: 13px;
+    color: var(--muted);
+    display: flex;
+    justify-content: space-between;
+    border-bottom: 1px solid var(--hairline);
+  }
+  .gui-glyph.folder .row:last-child { border-bottom: none; }
+  .gui-glyph.folder .row .meta {
+    color: var(--dim);
+  }
+
+  /* ============ Act 2 ============ */
+  .act2 {
+    flex-direction: column;
+  }
+
+  .terminal {
+    width: 1180px;
+    border-radius: 16px;
+    background: rgba(20, 20, 20, 1);
+    border: 1px solid var(--hairline);
+    overflow: hidden;
+    box-shadow:
+      0 0 0 1px rgba(255,255,255,0.02),
+      0 60px 120px -30px rgba(217,119,87,0.15);
+  }
+  .tty-head {
+    display: flex; align-items: center; gap: 9px;
+    padding: 18px 22px;
+    background: rgba(255,255,255,0.02);
+    border-bottom: 1px solid var(--hairline);
+  }
+  .tty-head .d {
+    width: 13px; height: 13px; border-radius: 50%;
+    background: var(--hairline);
+  }
+  .tty-head .d.red { background: #5a2a2a; }
+  .tty-head .d.yellow { background: #5a4a2a; }
+  .tty-head .d.green { background: #2a5a35; }
+  .tty-title {
+    margin-left: 16px;
+    color: var(--muted);
+    font-size: 14px;
+    font-family: var(--mono);
+    letter-spacing: 0.02em;
+  }
+  .tty-body {
+    padding: 44px 36px;
+    font-family: var(--mono);
+    font-size: 26px;
+    line-height: 1.6;
+    color: rgba(255,255,255,0.86);
+    min-height: 160px;
+  }
+  .prompt { color: var(--accent); margin-right: 12px; }
+  .typed { white-space: pre; }
+  .cursor {
+    display: inline-block;
+    width: 12px; height: 28px;
+    background: var(--accent);
+    vertical-align: -5px;
+    margin-left: 3px;
+  }
+
+  /* Gallery (v6 structure, dark theme) */
+  .gallery-viewport {
+    position: absolute;
+    inset: 0;
+    overflow: hidden;
+    perspective: 2400px;
+    perspective-origin: 50% 45%;
+  }
+  .gallery-canvas {
+    position: absolute;
+    top: 50%; left: 50%;
+    width: 4320px;
+    height: 2520px;
+    transform-origin: center center;
+    transform-style: preserve-3d;
+    will-change: transform;
+    display: grid;
+    grid-template-columns: repeat(8, 1fr);
+    gap: 40px;
+    padding: 60px;
+  }
+  .gallery-card {
+    background: #1a1a1a;
+    border-radius: 14px;
+    padding: 6px;
+    overflow: hidden;
+    border: 1px solid var(--hairline);
+    box-shadow:
+      0 20px 60px -20px rgba(0, 0, 0, 0.6),
+      0 6px 18px -6px rgba(0, 0, 0, 0.4);
+    aspect-ratio: 16 / 9;
+    will-change: opacity, filter;
+  }
+  .gallery-card.depth-near {
+    box-shadow:
+      0 32px 80px -22px rgba(0, 0, 0, 0.8),
+      0 10px 24px -8px rgba(217, 119, 87, 0.12);
+  }
+  .gallery-card.depth-far {
+    box-shadow:
+      0 14px 40px -16px rgba(0, 0, 0, 0.4),
+      0 4px 12px -4px rgba(0, 0, 0, 0.25);
+  }
+  .gallery-card img {
+    width: 100%; height: 100%;
+    object-fit: cover;
+    display: block;
+    border-radius: 9px;
+  }
+
+  /* Overlay statements (on top of gallery) */
+  .over-statement {
+    position: absolute;
+    inset: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    pointer-events: none;
+    z-index: 50;
+    opacity: 0;
+  }
+  .over-statement .text {
+    font-family: var(--sans);
+    font-size: 84px;
+    font-weight: 200;
+    letter-spacing: -0.035em;
+    color: var(--ink);
+    text-align: center;
+    line-height: 1.08;
+    text-shadow: 0 8px 40px rgba(0,0,0,0.8);
+    padding: 0 40px;
+    max-width: 1400px;
+  }
+  .over-statement .text .accent { color: var(--accent); }
+
+  /* ============ Act 3 ============ */
+  .act3 {
+    flex-direction: column;
+    gap: 0;
+  }
+  .statement-big {
+    font-family: var(--sans);
+    font-size: 160px;
+    font-weight: 100;
+    letter-spacing: -0.05em;
+    color: var(--ink);
+    text-align: center;
+    line-height: 1;
+    will-change: opacity, transform, font-variation-settings;
+  }
+  .statement-big .accent { color: var(--accent); font-weight: inherit; }
+
+  .brand-wordmark {
+    font-family: var(--sans);
+    font-size: 140px;
+    font-weight: 100;
+    font-variation-settings: "wght" 100;
+    letter-spacing: -0.045em;
+    color: var(--ink);
+    text-align: center;
+    line-height: 1;
+    will-change: font-variation-settings, opacity, transform;
+  }
+  .brand-wordmark .accent { color: var(--accent); font-weight: inherit; }
+
+  .farewell-quote {
+    margin-top: 44px;
+    font-family: var(--serif-en);
+    font-style: italic;
+    font-weight: 300;
+    font-size: 36px;
+    color: var(--accent);
+    letter-spacing: 0.005em;
+    text-align: center;
+    will-change: opacity, transform;
+  }
+
+  .farewell-cn {
+    margin-top: 18px;
+    font-family: var(--serif-en);
+    font-weight: 300;
+    font-size: 18px;
+    color: var(--muted);
+    letter-spacing: 0.24em;
+    text-align: center;
+  }
+
+  .brand-url {
+    margin-top: 48px;
+    font-size: 14px;
+    color: var(--muted);
+    font-family: var(--mono);
+    letter-spacing: 0.16em;
+    text-align: center;
+  }
+
+  /* Watermark (subtle, always on during Act 2/3) */
+  .watermark {
+    position: absolute;
+    bottom: 28px;
+    right: 36px;
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.24em;
+    text-transform: uppercase;
+    color: rgba(255,255,255,0.22);
+    z-index: 100;
+    opacity: 0;
+    transition: opacity 0.6s;
+    pointer-events: none;
+  }
+  .watermark.visible { opacity: 1; }
+
+  /* ============ Act 0 — Claude Design 致敬(+讽刺) ============ */
+  .act0 {
+    background: #0a0a0a;
+  }
+  .cd-browser {
+    position: absolute;
+    top: 50%; left: 50%;
+    width: 1640px;
+    height: 920px;
+    transform: translate(-50%, -50%);
+    background: var(--cd-bg);
+    border-radius: 14px;
+    overflow: hidden;
+    box-shadow:
+      0 0 0 1px rgba(255,255,255,0.04),
+      0 60px 160px -40px rgba(0,0,0,0.8),
+      0 24px 60px -20px rgba(0,0,0,0.6);
+    will-change: transform, opacity, filter;
+  }
+  .cd-chrome {
+    display: flex; align-items: center;
+    height: 48px;
+    padding: 0 18px;
+    background: #EDEBE5;
+    border-bottom: 1px solid var(--cd-hair);
+    gap: 14px;
+  }
+  .cd-traffic { display: flex; gap: 8px; }
+  .cd-traffic .d {
+    width: 12px; height: 12px; border-radius: 50%;
+    background: #D9D4CB;
+  }
+  .cd-traffic .d.r { background: #E8A5A0; }
+  .cd-traffic .d.y { background: #E8D0A0; }
+  .cd-traffic .d.g { background: #A5D0B0; }
+  .cd-urlbar {
+    flex: 1;
+    max-width: 520px;
+    margin: 0 auto;
+    height: 28px;
+    background: #F9F7F2;
+    border: 1px solid var(--cd-hair);
+    border-radius: 6px;
+    display: flex; align-items: center; justify-content: center;
+    font-family: var(--sans);
+    font-size: 13px;
+    color: var(--cd-dim);
+    letter-spacing: 0;
+  }
+  .cd-urlbar .lock {
+    width: 10px; height: 10px;
+    margin-right: 8px;
+    border: 1.5px solid var(--cd-dim);
+    border-radius: 2px;
+    position: relative;
+  }
+  .cd-urlbar .lock::before {
+    content: '';
+    position: absolute;
+    top: -5px; left: 50%;
+    transform: translateX(-50%);
+    width: 6px; height: 6px;
+    border: 1.5px solid var(--cd-dim);
+    border-bottom: none;
+    border-radius: 3px 3px 0 0;
+  }
+  .cd-tabs-row {
+    display: flex;
+    height: 42px;
+    padding: 0 24px;
+    background: var(--cd-bg);
+    border-bottom: 1px solid var(--cd-hair);
+    align-items: center;
+    gap: 6px;
+  }
+  .cd-tab {
+    height: 28px;
+    padding: 0 14px;
+    display: flex; align-items: center;
+    font-family: var(--sans);
+    font-size: 12px;
+    color: var(--cd-dim);
+    border-radius: 6px;
+    gap: 8px;
+    white-space: nowrap;
+  }
+  .cd-tab.active {
+    background: #FFFFFF;
+    color: var(--cd-ink);
+    font-weight: 500;
+    box-shadow: 0 1px 2px rgba(0,0,0,0.04);
+  }
+  .cd-tab .dot {
+    width: 6px; height: 6px; border-radius: 50%;
+    background: var(--cd-green);
+  }
+  .cd-topbar-right {
+    margin-left: auto;
+    display: flex; align-items: center; gap: 12px;
+    font-family: var(--sans);
+    font-size: 12px;
+    color: var(--cd-dim);
+  }
+  .cd-topbar-right .btn {
+    padding: 6px 12px;
+    background: var(--cd-ink);
+    color: #FFFFFF;
+    border-radius: 6px;
+    font-weight: 500;
+  }
+  .cd-topbar-right .btn.ghost {
+    background: transparent;
+    color: var(--cd-ink);
+    border: 1px solid var(--cd-hair-strong);
+  }
+
+  .cd-body {
+    display: grid;
+    grid-template-columns: 440px 1fr;
+    height: calc(920px - 48px - 42px);
+  }
+
+  /* Chat panel */
+  .cd-chat {
+    background: var(--cd-bg);
+    border-right: 1px solid var(--cd-hair);
+    padding: 28px 24px;
+    display: flex;
+    flex-direction: column;
+    gap: 18px;
+    overflow: hidden;
+  }
+  .cd-msg { display: flex; gap: 10px; align-items: flex-start; }
+  .cd-avatar {
+    width: 26px; height: 26px;
+    border-radius: 50%;
+    display: flex; align-items: center; justify-content: center;
+    font-family: var(--sans);
+    font-size: 11px;
+    font-weight: 600;
+    flex-shrink: 0;
+  }
+  .cd-avatar.user {
+    background: #E8E4DC;
+    color: var(--cd-ink);
+  }
+  .cd-avatar.claude {
+    background: var(--cd-ink);
+    color: #FFFFFF;
+  }
+  .cd-bubble {
+    font-family: var(--sans);
+    font-size: 13px;
+    line-height: 1.55;
+    color: var(--cd-ink);
+    max-width: 100%;
+  }
+  .cd-bubble .dim { color: var(--cd-dim); }
+
+  .cd-tweaks {
+    margin-top: auto;
+    padding: 16px 18px;
+    background: #FFFFFF;
+    border: 1px solid var(--cd-hair);
+    border-radius: 10px;
+  }
+  .cd-tweaks-title {
+    font-family: var(--sans);
+    font-size: 11px;
+    font-weight: 600;
+    letter-spacing: 0.08em;
+    text-transform: uppercase;
+    color: var(--cd-dim);
+    margin-bottom: 14px;
+  }
+  .cd-tweak-row {
+    display: flex; align-items: center;
+    gap: 12px;
+    margin-bottom: 12px;
+  }
+  .cd-tweak-row:last-child { margin-bottom: 0; }
+  .cd-tweak-label {
+    font-family: var(--sans);
+    font-size: 12px;
+    color: var(--cd-ink);
+    width: 72px;
+    flex-shrink: 0;
+  }
+  .cd-tweak-track {
+    flex: 1;
+    height: 4px;
+    background: #E8E4DC;
+    border-radius: 2px;
+    position: relative;
+  }
+  .cd-tweak-thumb {
+    position: absolute;
+    top: 50%;
+    width: 16px; height: 16px;
+    background: #FFFFFF;
+    border: 1.5px solid var(--cd-ink);
+    border-radius: 50%;
+    transform: translate(-50%, -50%);
+    will-change: left;
+  }
+  .cd-color-dots {
+    display: flex; gap: 6px;
+  }
+  .cd-color-dot {
+    width: 16px; height: 16px;
+    border-radius: 50%;
+    border: 1.5px solid transparent;
+    cursor: default;
+  }
+  .cd-color-dot.active {
+    border-color: var(--cd-ink);
+  }
+
+  .cd-input {
+    margin-top: 14px;
+    height: 40px;
+    padding: 0 14px;
+    background: #FFFFFF;
+    border: 1px solid var(--cd-hair);
+    border-radius: 8px;
+    display: flex; align-items: center;
+    font-family: var(--sans);
+    font-size: 12px;
+    color: var(--cd-dim);
+  }
+
+  /* Canvas panel */
+  .cd-canvas {
+    background: #FAF9F5;
+    padding: 40px;
+    overflow: hidden;
+    display: flex; align-items: center; justify-content: center;
+    position: relative;
+  }
+  .cd-poster {
+    width: 780px;
+    aspect-ratio: 4 / 3;
+    background: var(--cd-green);
+    border-radius: 8px;
+    padding: 48px 56px;
+    color: #F5F2E8;
+    display: grid;
+    grid-template-columns: 1.2fr 1fr;
+    gap: 48px;
+    box-shadow: 0 40px 80px -30px rgba(0,0,0,0.4);
+    position: relative;
+    overflow: hidden;
+  }
+  .cd-poster::before {
+    content: '';
+    position: absolute;
+    top: -60px; right: -60px;
+    width: 220px; height: 220px;
+    background: radial-gradient(circle, rgba(245,242,232,0.10), transparent 70%);
+  }
+  .cd-poster-left { position: relative; z-index: 2; }
+  .cd-poster-eyebrow {
+    font-family: var(--sans);
+    font-size: 11px;
+    font-weight: 500;
+    letter-spacing: 0.22em;
+    text-transform: uppercase;
+    opacity: 0.65;
+    margin-bottom: 28px;
+  }
+  .cd-poster-title {
+    font-family: var(--serif-en);
+    font-size: 76px;
+    font-weight: 500;
+    line-height: 0.95;
+    letter-spacing: -0.02em;
+    margin-bottom: 20px;
+  }
+  .cd-poster-sub {
+    font-family: var(--sans);
+    font-size: 14px;
+    opacity: 0.75;
+    line-height: 1.5;
+    margin-bottom: 40px;
+  }
+  .cd-poster-pines {
+    display: flex; gap: 10px;
+    opacity: 0.35;
+  }
+  .cd-pine {
+    width: 0; height: 0;
+    border-left: 10px solid transparent;
+    border-right: 10px solid transparent;
+    border-bottom: 20px solid #F5F2E8;
+    position: relative;
+  }
+  .cd-pine::after {
+    content: '';
+    position: absolute;
+    bottom: -24px; left: 50%;
+    transform: translateX(-50%);
+    width: 3px; height: 6px;
+    background: #F5F2E8;
+  }
+  .cd-schedule {
+    background: rgba(245,242,232,0.08);
+    border: 1px solid rgba(245,242,232,0.15);
+    border-radius: 6px;
+    padding: 20px 22px;
+    position: relative;
+    z-index: 2;
+  }
+  .cd-schedule-title {
+    font-family: var(--sans);
+    font-size: 10px;
+    font-weight: 600;
+    letter-spacing: 0.18em;
+    text-transform: uppercase;
+    opacity: 0.6;
+    margin-bottom: 14px;
+  }
+  .cd-schedule-row {
+    display: flex; justify-content: space-between;
+    font-family: var(--sans);
+    font-size: 12px;
+    padding: 8px 0;
+    border-bottom: 1px solid rgba(245,242,232,0.10);
+  }
+  .cd-schedule-row:last-child { border-bottom: none; }
+  .cd-schedule-row .time { opacity: 0.65; font-variant-numeric: tabular-nums; }
+
+  /* Caption for Act 0 */
+  .cd-caption {
+    position: absolute;
+    bottom: 100px;
+    left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--sans);
+    font-size: 88px;
+    font-weight: 200;
+    letter-spacing: -0.035em;
+    color: var(--ink);
+    text-align: center;
+    opacity: 0;
+    z-index: 60;
+    text-shadow: 0 10px 50px rgba(0,0,0,0.9);
+    will-change: opacity, transform;
+  }
+  .cd-caption .period { color: var(--accent); }
+
+  /* Act 0.5 — pivot */
+  .act05 {
+    flex-direction: column;
+  }
+  .pivot-line {
+    font-family: var(--sans);
+    font-size: 112px;
+    font-weight: 200;
+    letter-spacing: -0.04em;
+    color: var(--ink);
+    text-align: center;
+    line-height: 1.05;
+    will-change: opacity, transform, font-variation-settings;
+  }
+  .pivot-line .accent { color: var(--accent); font-weight: inherit; }
+  .pivot-line .faint { color: var(--muted); }
+</style>
+</head>
+<body>
+
+<div class="stage" id="stage">
+
+  <div class="watermark" id="watermark">Created by Huashu-Design</div>
+
+  <!-- ========== Act 0: Claude Design 致敬 ========== -->
+  <div class="scene act0" id="act0ClaudeDesign">
+    <div class="cd-browser" id="cdBrowser">
+      <!-- Chrome bar -->
+      <div class="cd-chrome">
+        <div class="cd-traffic">
+          <span class="d r"></span><span class="d y"></span><span class="d g"></span>
+        </div>
+        <div class="cd-urlbar"><span class="lock"></span>claude.ai/design</div>
+        <div style="width: 56px;"></div>
+      </div>
+      <!-- Tabs row -->
+      <div class="cd-tabs-row">
+        <div class="cd-tab active"><span class="dot"></span>Company offsite html</div>
+        <div class="cd-tab">Dashboard exploration</div>
+        <div class="cd-tab">Landing v2</div>
+        <div class="cd-topbar-right">
+          <span>100%</span>
+          <span class="btn ghost">Export</span>
+          <span class="btn">Share</span>
+        </div>
+      </div>
+      <!-- Body: split chat + canvas -->
+      <div class="cd-body">
+        <div class="cd-chat">
+          <div class="cd-msg">
+            <div class="cd-avatar user">Y</div>
+            <div class="cd-bubble">帮我做一份公司团建的欢迎手册。</div>
+          </div>
+          <div class="cd-msg">
+            <div class="cd-avatar claude">C</div>
+            <div class="cd-bubble">我帮你设计了一份横版单页欢迎手册,包含带松树插画的品牌封面、双列日程表和活动卡片。<br/><br/><span class="dim">拖动 Tweaks 可以调整强调色、标题大小和密度。</span></div>
+          </div>
+          <div class="cd-tweaks">
+            <div class="cd-tweaks-title">Tweaks</div>
+            <div class="cd-tweak-row">
+              <div class="cd-tweak-label">Accent</div>
+              <div class="cd-color-dots">
+                <div class="cd-color-dot" style="background:#2D4A3A;" id="cdDot1"></div>
+                <div class="cd-color-dot active" style="background:#D97757;" id="cdDot2"></div>
+                <div class="cd-color-dot" style="background:#3F5E8A;" id="cdDot3"></div>
+                <div class="cd-color-dot" style="background:#8B6F4A;" id="cdDot4"></div>
+              </div>
+            </div>
+            <div class="cd-tweak-row">
+              <div class="cd-tweak-label">Headline</div>
+              <div class="cd-tweak-track"><div class="cd-tweak-thumb" id="cdThumb1" style="left: 58%;"></div></div>
+            </div>
+            <div class="cd-tweak-row">
+              <div class="cd-tweak-label">Density</div>
+              <div class="cd-tweak-track"><div class="cd-tweak-thumb" id="cdThumb2" style="left: 40%;"></div></div>
+            </div>
+          </div>
+          <div class="cd-input">告诉我下一步想要什么…</div>
+        </div>
+        <div class="cd-canvas">
+          <div class="cd-poster" id="cdPoster">
+            <div class="cd-poster-left">
+              <div class="cd-poster-eyebrow">Anthropic Labs · Planning Day</div>
+              <div class="cd-poster-title">HEMLARK<br/>RETREAT '26</div>
+              <div class="cd-poster-sub">June 14 · Full Day<br/>Pine Valley Lodge</div>
+              <div class="cd-poster-pines">
+                <div class="cd-pine"></div>
+                <div class="cd-pine"></div>
+                <div class="cd-pine"></div>
+                <div class="cd-pine"></div>
+              </div>
+            </div>
+            <div class="cd-schedule">
+              <div class="cd-schedule-title">Schedule</div>
+              <div class="cd-schedule-row"><span>Breakfast</span><span class="time">9:00</span></div>
+              <div class="cd-schedule-row"><span>Kickoff</span><span class="time">10:00</span></div>
+              <div class="cd-schedule-row"><span>Workshops</span><span class="time">10:30</span></div>
+              <div class="cd-schedule-row"><span>Lunch</span><span class="time">12:30</span></div>
+              <div class="cd-schedule-row"><span>Hike</span><span class="time">14:00</span></div>
+              <div class="cd-schedule-row"><span>Dinner</span><span class="time">18:00</span></div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="cd-caption" id="cdCaption">它做得确实好<span class="period">。</span></div>
+  </div>
+
+  <!-- ========== Act 0.5: Pivot ========== -->
+  <div class="scene act05" id="act05Pivot">
+    <div class="pivot-line" id="pivotLine">
+      但那不是<span class="accent">未来</span>。
+    </div>
+  </div>
+
+  <!-- ========== Act 1 ========== -->
+  <div class="scene act1" id="act1a">
+    <div class="hero-line" id="heroLine">
+      敬 <span class="accent">Agent</span> 时代。
+    </div>
+  </div>
+
+  <div class="scene act1" id="act1b">
+    <!-- "Not the ones who click." + abstract mouse -->
+    <div class="gui-glyph click" id="glyphClick" style="left: 50%; top: 62%; transform: translate(-50%, -50%);">
+      <svg viewBox="0 0 24 24" fill="none">
+        <path d="M4 2l6 18 3-8 8-3L4 2z" stroke="rgba(255,255,255,0.55)" stroke-width="1.4" fill="rgba(255,255,255,0.12)" stroke-linejoin="round"/>
+      </svg>
+    </div>
+    <div class="not-line" id="notLine1" style="position: absolute; top: 28%; left: 50%; transform: translateX(-50%); white-space: nowrap;">
+      不是那些<span class="strike">点击</span>的。
+    </div>
+  </div>
+
+  <div class="scene act1" id="act1c">
+    <!-- "Not the ones who drag." + slider -->
+    <div class="gui-glyph drag" id="glyphDrag" style="left: 50%; top: 62%; transform: translate(-50%, -50%);">
+      <div class="track">
+        <div class="fill"></div>
+        <div class="thumb" id="sliderThumb"></div>
+      </div>
+    </div>
+    <div class="not-line" id="notLine2" style="position: absolute; top: 28%; left: 50%; transform: translateX(-50%); white-space: nowrap;">
+      不是那些<span class="strike">拖拽</span>的。
+    </div>
+  </div>
+
+  <div class="scene act1" id="act1d">
+    <!-- "Not the ones who wait..." + folder window -->
+    <div class="gui-glyph folder" id="glyphFolder" style="left: 50%; top: 62%; transform: translate(-50%, -50%);">
+      <div class="head">
+        <span class="d"></span><span class="d"></span><span class="d"></span>
+      </div>
+      <div class="row"><span>design-v1.fig</span><span class="meta">42 KB</span></div>
+      <div class="row"><span>design-v2-final.fig</span><span class="meta">58 KB</span></div>
+      <div class="row"><span>design-v2-FINAL-final.fig</span><span class="meta">61 KB</span></div>
+      <div class="row"><span>design-v3.fig</span><span class="meta">65 KB</span></div>
+    </div>
+    <div class="not-line" id="notLine3" style="position: absolute; top: 22%; left: 50%; transform: translateX(-50%); white-space: nowrap; font-size: 72px;">
+      不是那些<span class="strike">等你打开文件</span>的。
+    </div>
+  </div>
+
+  <!-- ========== Act 2 ========== -->
+  <div class="scene act2" id="act2Terminal">
+    <div class="terminal" id="terminal">
+      <div class="tty-head">
+        <span class="d red"></span>
+        <span class="d yellow"></span>
+        <span class="d green"></span>
+        <span class="tty-title">huashu — claude code</span>
+      </div>
+      <div class="tty-body">
+        <span class="prompt">$</span><span class="typed" id="typed"></span><span class="cursor" id="cursor"></span>
+      </div>
+    </div>
+  </div>
+
+  <div class="scene" id="act2Gallery">
+    <div class="gallery-viewport">
+      <div class="gallery-canvas" id="galleryCanvas"></div>
+    </div>
+  </div>
+
+  <div class="over-statement" id="overStmt1">
+    <div class="text">是那些在你<span class="accent">睡觉</span>时<br/>设计的。</div>
+  </div>
+
+  <div class="over-statement" id="overStmt2">
+    <div class="text">是那些在你<span class="accent">开会</span>时<br/>交付的。</div>
+  </div>
+
+  <!-- ========== Act 3 ========== -->
+  <div class="scene act3" id="act3Medium">
+    <div class="statement-big" id="stmtMedium">
+      <span class="accent">Agent</span> 是<br/>新的媒介。
+    </div>
+  </div>
+
+  <div class="scene act3" id="act3Brand">
+    <div class="brand-wordmark" id="wordmark">huashu<span class="accent">-</span>design</div>
+    <div class="farewell-quote" id="farewell">为他们,我们造了这个。</div>
+    <div class="farewell-cn" id="farewellCn">· FOR THE AGENTS · WE BUILT THIS ·</div>
+    <div class="brand-url" id="url">huasheng.ai/huashu-design-hero</div>
+  </div>
+
+</div>
+
+<script>
+(function() {
+  // ---------- Fit stage ----------
+  const stage = document.getElementById('stage');
+  function rescale() {
+    const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  rescale();
+  window.addEventListener('resize', rescale);
+
+  const SLIDE_FILES = [
+    'preview-01-cover.png','preview-02-quote.png','preview-03-intro.png','preview-04-toc.png',
+    'preview-05-divider-1.png','preview-06-seldon.png','preview-07-human-psych-limit.png','preview-08-ai-vs-human.png',
+    'preview-09-divider-2.png','preview-10-personas.png','preview-11-four-puzzles.png','preview-12-phenomena-1-2.png',
+    'preview-13-phenomena-3-4.png','preview-14-five-voices.png','preview-15-divider-3.png','preview-16-persona-selection.png',
+    'preview-17-persona-space.png','preview-18-emergent-misalignment.png','preview-19-inoculation.png','preview-20-emotion.png',
+    'preview-21-dosage.png','preview-22-steering.png','preview-23-expression-vs-impact.png','preview-24-concept-injection.png',
+    'preview-25-consciousness-prob.png','preview-26-divider-4.png','preview-27-cot-faithfulness.png','preview-28-alignment-faking.png',
+    'preview-29-divider-5.png','preview-30-open-questions.png','preview-31-giving-back.png','preview-32-closing.png',
+  ];
+  const BASE = '../../../2026.04-AI心理学/演讲PPT-北大/';
+
+  // ---------- Build gallery ----------
+  const COLS = 8, ROWS = 6, COUNT = COLS * ROWS;
+  const galleryCanvas = document.getElementById('galleryCanvas');
+  const galleryCards = [];
+  for (let i = 0; i < COUNT; i++) {
+    const slideIdx = i % 32;
+    const card = document.createElement('div');
+    card.className = 'gallery-card';
+    const zIdx = Math.sin(i * 1.7) * 22 + Math.cos(i * 0.73) * 14;
+    if (zIdx > 12) card.classList.add('depth-near');
+    else if (zIdx < -12) card.classList.add('depth-far');
+    const img = document.createElement('img');
+    img.src = BASE + SLIDE_FILES[slideIdx];
+    img.onerror = () => { img.src = BASE + 'preview-01-cover.png'; };
+    card.appendChild(img);
+    galleryCanvas.appendChild(card);
+    galleryCards.push(card);
+  }
+  for (let i = 0; i < 32; i++) {
+    const im = new Image();
+    im.src = BASE + SLIDE_FILES[i];
+  }
+
+  // ---------- Easings ----------
+  const easeOut = t => 1 - Math.pow(1 - t, 3);
+  const expoOut = t => (t <= 0) ? 0 : (t >= 1) ? 1 : 1 - Math.pow(2, -10 * t);
+  const easeInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;
+  function lerp(time, start, end, fromV, toV, easing) {
+    if (time <= start) return fromV;
+    if (time >= end) return toV;
+    let p = (time - start) / (end - start);
+    if (easing) p = easing(p);
+    return fromV + (toV - fromV) * p;
+  }
+  function clampLerp(time, start, end) {
+    if (time <= start) return 0;
+    if (time >= end) return 1;
+    return (time - start) / (end - start);
+  }
+
+  // ---------- Timeline (30s) ----------
+  const T = {
+    DURATION: 30.0,
+
+    // ===== Act 0: Claude Design 致敬 (0 - 4s) =====
+    a0_in:   [0.3, 1.2],       // browser fade + scale in
+    a0_hold: [1.2, 3.4],       // tweaks 自动动
+    a0_out:  [3.4, 4.0],       // browser 退场
+
+    cd_tweak_anim: [1.4, 3.3], // tweaks thumb 自动拖动窗口
+    cd_accent_switch: [2.1, 2.5], // accent color dot 切换到深绿
+
+    cd_caption_in:  [1.6, 2.2],
+    cd_caption_hold:[2.2, 3.3],
+    cd_caption_out: [3.3, 3.8],
+
+    // ===== Act 0.5: Pivot (3.9 - 5.2s) =====
+    a05_in:   [3.9, 4.6],
+    a05_hold: [4.6, 4.9],
+    a05_out:  [4.9, 5.3],
+
+    // ===== Act 1 (shifted +5s) =====
+    a1a_in:  [5.3, 6.3],       // "Here's to the Agents."
+    a1a_hold:[6.3, 7.8],
+    a1a_out: [7.8, 8.3],
+
+    a1b_in:  [8.2, 8.9],       // "Not the ones who click."
+    a1b_hold:[8.9, 10.3],
+    a1b_out: [10.3, 10.8],
+
+    a1c_in:  [10.7, 11.3],     // "Not the ones who drag."
+    a1c_hold:[11.3, 12.5],
+    a1c_out: [12.5, 13.0],
+
+    a1d_in:  [12.9, 13.5],     // "Not the ones who wait..."
+    a1d_hold:[13.5, 15.2],
+    a1d_out: [15.2, 15.7],
+
+    // ===== Act 2 (shifted +5s) =====
+    a2tty_in: [15.6, 16.2],    // terminal in
+    a2type:   [16.4, 18.6],
+    a2tty_out:[18.9, 19.4],
+
+    a2gal_in: [19.1, 19.9],    // gallery ripple start
+    ripple:   [19.9, 21.6],
+    panStart: 20.2,
+    a2gal_out:[25.5, 26.2],
+
+    // Overlay statements on gallery
+    stmt1:    [21.7, 23.4],    // "design while you sleep"
+    stmt2:    [23.7, 25.4],    // "ship while you're in a meeting"
+
+    // ===== Act 3 (shifted +5s) =====
+    a3med_in: [26.1, 27.0],    // "Agent is the new medium"
+    a3med_hold:[27.0, 28.0],
+    a3med_out:[28.0, 28.4],
+
+    a3brand_in:  [28.3, 29.0],
+    brand_morph: [28.7, 29.4],
+    a3farewell_in: [29.0, 29.6],
+    a3cn_in: [29.3, 29.8],
+    a3url_in: [29.5, 30.0],
+  };
+
+  // ---------- Elements ----------
+  const scenes = {
+    a0: document.getElementById('act0ClaudeDesign'),
+    a05: document.getElementById('act05Pivot'),
+    a1a: document.getElementById('act1a'),
+    a1b: document.getElementById('act1b'),
+    a1c: document.getElementById('act1c'),
+    a1d: document.getElementById('act1d'),
+    a2tty: document.getElementById('act2Terminal'),
+    a2gal: document.getElementById('act2Gallery'),
+    a3med: document.getElementById('act3Medium'),
+    a3brand: document.getElementById('act3Brand'),
+  };
+  const cdBrowser = document.getElementById('cdBrowser');
+  const cdCaption = document.getElementById('cdCaption');
+  const cdThumb1 = document.getElementById('cdThumb1');
+  const cdThumb2 = document.getElementById('cdThumb2');
+  const cdDot1 = document.getElementById('cdDot1');
+  const cdDot2 = document.getElementById('cdDot2');
+  const cdPoster = document.getElementById('cdPoster');
+  const pivotLine = document.getElementById('pivotLine');
+  const overs = {
+    stmt1: document.getElementById('overStmt1'),
+    stmt2: document.getElementById('overStmt2'),
+  };
+  const heroLine = document.getElementById('heroLine');
+  const notLine1 = document.getElementById('notLine1');
+  const notLine2 = document.getElementById('notLine2');
+  const notLine3 = document.getElementById('notLine3');
+  const glyphClick = document.getElementById('glyphClick');
+  const glyphDrag = document.getElementById('glyphDrag');
+  const sliderThumb = document.getElementById('sliderThumb');
+  const glyphFolder = document.getElementById('glyphFolder');
+  const terminal = document.getElementById('terminal');
+  const typed = document.getElementById('typed');
+  const cursor = document.getElementById('cursor');
+  const stmtMedium = document.getElementById('stmtMedium');
+  const wordmark = document.getElementById('wordmark');
+  const farewell = document.getElementById('farewell');
+  const farewellCn = document.getElementById('farewellCn');
+  const urlEl = document.getElementById('url');
+  const watermark = document.getElementById('watermark');
+
+  const COMMAND = '/huashu-design 做一份发布会PPT';
+
+  // ---------- Gallery transforms ----------
+  const GALLERY_TILT = 'perspective(2400px) rotateX(14deg) rotateY(-10deg) rotateZ(-2deg)';
+  const GALLERY_SCALE = 0.94;
+  function galleryTransform(dx, dy, extraScale = 1) {
+    return `translate(-50%, -50%) translate(${dx}px, ${dy}px) scale(${GALLERY_SCALE * extraScale}) ${GALLERY_TILT}`;
+  }
+
+  // ---------- Helpers to show/hide scenes ----------
+  function showScene(key, opacity) {
+    const el = scenes[key];
+    if (opacity > 0.001) el.classList.add('visible');
+    else el.classList.remove('visible');
+    el.style.opacity = opacity;
+  }
+
+  function showOver(key, opacity) {
+    const el = overs[key];
+    el.style.opacity = opacity;
+  }
+
+  // ---------- Render ----------
+  function render(t) {
+    // ============ Act 0: Claude Design 致敬 ============
+    if (t < T.a0_out[1]) {
+      let op;
+      if (t < T.a0_in[1]) op = lerp(t, T.a0_in[0], T.a0_in[1], 0, 1, expoOut);
+      else if (t < T.a0_out[0]) op = 1;
+      else op = lerp(t, T.a0_out[0], T.a0_out[1], 1, 0, easeOut);
+      showScene('a0', op);
+
+      // Browser: subtle breathing scale + exit shrink
+      const scaleIn = lerp(t, T.a0_in[0], T.a0_in[1], 0.94, 1.0, expoOut);
+      let scaleOut = 1.0;
+      let blurOut = 0;
+      if (t >= T.a0_out[0]) {
+        const p = clampLerp(t, T.a0_out[0], T.a0_out[1]);
+        scaleOut = 1.0 - 0.08 * p;
+        blurOut = 6 * p;
+      }
+      const finalScale = Math.min(scaleIn, scaleOut);
+      cdBrowser.style.transform = `translate(-50%, -50%) scale(${finalScale})`;
+      cdBrowser.style.filter = blurOut > 0.1 ? `blur(${blurOut}px)` : '';
+
+      // Tweaks thumb 自动拖动(模拟用户在调节)
+      const tw = clampLerp(t, T.cd_tweak_anim[0], T.cd_tweak_anim[1]);
+      // Headline slider: 58% → 72% → 62%
+      let headlinePct;
+      if (tw < 0.5) headlinePct = 58 + (72 - 58) * easeInOut(tw * 2);
+      else headlinePct = 72 + (62 - 72) * easeInOut((tw - 0.5) * 2);
+      cdThumb1.style.left = headlinePct + '%';
+      // Density slider: 40% → 55%
+      const densityPct = 40 + 15 * easeInOut(tw);
+      cdThumb2.style.left = densityPct + '%';
+
+      // Accent 从橙切换到深绿(模拟用户在选色)
+      const switched = t >= T.cd_accent_switch[0];
+      if (switched) {
+        cdDot1.classList.add('active');
+        cdDot2.classList.remove('active');
+        // Poster 颜色跟着变
+        cdPoster.style.background = 'var(--cd-green)';
+      } else {
+        cdDot1.classList.remove('active');
+        cdDot2.classList.add('active');
+        cdPoster.style.background = '#B85D3D';
+      }
+
+      // Caption "It's beautiful."
+      let capOp = 0;
+      if (t >= T.cd_caption_in[0] && t < T.cd_caption_out[1]) {
+        if (t < T.cd_caption_in[1]) capOp = clampLerp(t, T.cd_caption_in[0], T.cd_caption_in[1]);
+        else if (t < T.cd_caption_out[0]) capOp = 1;
+        else capOp = 1 - clampLerp(t, T.cd_caption_out[0], T.cd_caption_out[1]);
+      }
+      const capRise = lerp(t, T.cd_caption_in[0], T.cd_caption_in[1], 14, 0, expoOut);
+      cdCaption.style.opacity = capOp;
+      cdCaption.style.transform = `translateX(-50%) translateY(${capRise}px)`;
+    } else {
+      showScene('a0', 0);
+    }
+
+    // ============ Act 0.5: Pivot — "But it isn't the future." ============
+    if (t >= T.a05_in[0] - 0.1 && t < T.a05_out[1]) {
+      let op;
+      if (t < T.a05_in[1]) op = lerp(t, T.a05_in[0], T.a05_in[1], 0, 1, expoOut);
+      else if (t < T.a05_out[0]) op = 1;
+      else op = lerp(t, T.a05_out[0], T.a05_out[1], 1, 0, easeOut);
+      showScene('a05', op);
+
+      const rise = lerp(t, T.a05_in[0], T.a05_in[1], 16, 0, expoOut);
+      pivotLine.style.transform = `translate3d(0, ${rise}px, 0)`;
+
+      // Subtle weight morph on "But it isn't the future."
+      const morph = expoOut(clampLerp(t, T.a05_in[0], T.a05_in[1] + 0.3));
+      const w = 120 + (300 - 120) * morph;
+      pivotLine.style.fontVariationSettings = `"wght" ${w.toFixed(0)}`;
+      pivotLine.style.fontWeight = Math.round(w);
+    } else {
+      showScene('a05', 0);
+    }
+
+    // ============ Act 1a: "Here's to the Agents." ============
+    if (t >= T.a1a_in[0] - 0.1 && t < T.a1a_out[1]) {
+      let op;
+      if (t < T.a1a_in[1]) op = lerp(t, T.a1a_in[0], T.a1a_in[1], 0, 1, expoOut);
+      else if (t < T.a1a_out[0]) op = 1;
+      else op = lerp(t, T.a1a_out[0], T.a1a_out[1], 1, 0, easeOut);
+      showScene('a1a', op);
+
+      // Weight morph 100 → 400 on "Here's to the Agents."
+      const morph = expoOut(clampLerp(t, T.a1a_in[0], T.a1a_in[1] + 0.6));
+      const w = 100 + (400 - 100) * morph;
+      heroLine.style.fontVariationSettings = `"wght" ${w.toFixed(0)}`;
+      heroLine.style.fontWeight = Math.round(w);
+
+      // Subtle rise
+      const rise = lerp(t, T.a1a_in[0], T.a1a_in[1], 18, 0, expoOut);
+      heroLine.style.transform = `translate3d(0, ${rise}px, 0)`;
+    } else {
+      showScene('a1a', 0);
+    }
+
+    // ============ Act 1b: Not the ones who click. ============
+    if (t >= T.a1b_in[0] - 0.1 && t < T.a1b_out[1]) {
+      let op;
+      if (t < T.a1b_in[1]) op = lerp(t, T.a1b_in[0], T.a1b_in[1], 0, 1, expoOut);
+      else if (t < T.a1b_out[0]) op = 1;
+      else op = lerp(t, T.a1b_out[0], T.a1b_out[1], 1, 0, easeOut);
+      showScene('a1b', op);
+
+      // Animate the click glyph: appear, then trigger click ring + shake
+      const glyphIn = clampLerp(t, T.a1b_in[0] + 0.15, T.a1b_in[1]);
+      glyphClick.style.opacity = expoOut(glyphIn);
+
+      // Shake at mid-hold
+      const clickT = t - (T.a1b_in[1] + 0.3);
+      if (clickT > 0 && clickT < 0.4) {
+        glyphClick.style.transform = `translate(-50%, -50%) translate(${Math.sin(clickT * 60) * 3}px, 0)`;
+      } else {
+        glyphClick.style.transform = `translate(-50%, -50%)`;
+      }
+
+      // Strike the word "click" at halfway through hold
+      const strikeOn = t >= T.a1b_in[1] + 0.5;
+      notLine1.classList.toggle('struck', strikeOn);
+    } else {
+      showScene('a1b', 0);
+      glyphClick.style.opacity = 0;
+    }
+
+    // ============ Act 1c: Not the ones who drag. ============
+    if (t >= T.a1c_in[0] - 0.1 && t < T.a1c_out[1]) {
+      let op;
+      if (t < T.a1c_in[1]) op = lerp(t, T.a1c_in[0], T.a1c_in[1], 0, 1, expoOut);
+      else if (t < T.a1c_out[0]) op = 1;
+      else op = lerp(t, T.a1c_out[0], T.a1c_out[1], 1, 0, easeOut);
+      showScene('a1c', op);
+
+      const glyphIn = clampLerp(t, T.a1c_in[0] + 0.15, T.a1c_in[1]);
+      glyphDrag.style.opacity = expoOut(glyphIn);
+
+      // Animate slider thumb 30% → 70% position during hold
+      const dragT = clampLerp(t, T.a1c_hold[0], T.a1c_hold[1] - 0.2);
+      const leftPct = 30 + 40 * easeInOut(dragT);
+      sliderThumb.style.left = leftPct + '%';
+      const fillEl = glyphDrag.querySelector('.fill');
+      if (fillEl) fillEl.style.width = leftPct + '%';
+    } else {
+      showScene('a1c', 0);
+      glyphDrag.style.opacity = 0;
+    }
+
+    // ============ Act 1d: Not the ones who wait for you to open the file. ============
+    if (t >= T.a1d_in[0] - 0.1 && t < T.a1d_out[1]) {
+      let op;
+      if (t < T.a1d_in[1]) op = lerp(t, T.a1d_in[0], T.a1d_in[1], 0, 1, expoOut);
+      else if (t < T.a1d_out[0]) op = 1;
+      else op = lerp(t, T.a1d_out[0], T.a1d_out[1], 1, 0, easeOut);
+      showScene('a1d', op);
+
+      const glyphIn = clampLerp(t, T.a1d_in[0] + 0.15, T.a1d_in[1]);
+      glyphFolder.style.opacity = expoOut(glyphIn);
+    } else {
+      showScene('a1d', 0);
+      glyphFolder.style.opacity = 0;
+    }
+
+    // ============ Act 2 Terminal ============
+    if (t >= T.a2tty_in[0] - 0.1 && t < T.a2tty_out[1]) {
+      let op;
+      if (t < T.a2tty_in[1]) op = lerp(t, T.a2tty_in[0], T.a2tty_in[1], 0, 1, expoOut);
+      else if (t < T.a2tty_out[0]) op = 1;
+      else op = lerp(t, T.a2tty_out[0], T.a2tty_out[1], 1, 0, easeOut);
+      showScene('a2tty', op);
+
+      const rise = lerp(t, T.a2tty_in[0], T.a2tty_in[1], 28, 0, expoOut);
+      terminal.style.transform = `translate3d(0, ${rise}px, 0)`;
+
+      // Typing
+      if (t < T.a2type[0]) typed.textContent = '';
+      else if (t < T.a2type[1]) {
+        const p = (t - T.a2type[0]) / (T.a2type[1] - T.a2type[0]);
+        const n = Math.floor(p * COMMAND.length);
+        typed.textContent = COMMAND.slice(0, n);
+      } else typed.textContent = COMMAND;
+
+      cursor.style.opacity = (Math.floor(t * 2.5) % 2 === 0) ? 1 : 0.25;
+    } else {
+      showScene('a2tty', 0);
+    }
+
+    // ============ Act 2 Gallery + statements ============
+    if (t >= T.a2gal_in[0] - 0.1 && t < T.a2gal_out[1]) {
+      let op;
+      if (t < T.a2gal_in[1]) op = lerp(t, T.a2gal_in[0], T.a2gal_in[1], 0, 1, expoOut);
+      else if (t < T.a2gal_out[0]) op = 1;
+      else op = lerp(t, T.a2gal_out[0], T.a2gal_out[1], 1, 0, easeOut);
+      showScene('a2gal', op);
+
+      // Pan
+      const panT = Math.max(0, t - T.panStart);
+      const panX = Math.sin(panT * 0.12) * 180 - panT * 6;
+      const panY = Math.cos(panT * 0.09) * 100 - panT * 4;
+      const cX = Math.max(-600, Math.min(600, panX));
+      const cY = Math.max(-400, Math.min(400, panY));
+
+      // Ripple
+      const inRipple = t < T.ripple[1];
+      const rippleP = clampLerp(t, T.ripple[0], T.ripple[1]);
+      const galScale = inRipple ? (1.25 - 0.31 * expoOut(rippleP)) : 1.0;
+      galleryCanvas.style.transform = galleryTransform(cX, cY, galScale);
+
+      // Per-card ripple entry
+      galleryCards.forEach((card, i) => {
+        let entryOp = 1;
+        if (inRipple) {
+          const col = i % COLS, row = Math.floor(i / COLS);
+          const dc = col - (COLS - 1) / 2, dr = row - (ROWS - 1) / 2;
+          const dist = Math.sqrt(dc * dc + dr * dr);
+          const maxDist = Math.sqrt(((COLS - 1) / 2) ** 2 + ((ROWS - 1) / 2) ** 2);
+          const delay = (dist / maxDist) * 0.8;
+          const localT = Math.max(0, (t - T.ripple[0] - delay) / 0.7);
+          entryOp = expoOut(Math.min(1, localT));
+        }
+
+        // Dim when statements are active
+        const stmt1Active = t >= T.stmt1[0] && t < T.stmt1[1];
+        const stmt2Active = t >= T.stmt2[0] && t < T.stmt2[1];
+        const dimAmount = stmt1Active || stmt2Active ? 0.55 : 0;
+
+        if (dimAmount > 0) {
+          card.style.opacity = entryOp * (1 - dimAmount);
+          card.style.filter = `brightness(${1 - 0.3 * dimAmount}) saturate(${1 - 0.4 * dimAmount})`;
+        } else {
+          card.style.opacity = entryOp < 1 ? entryOp : '';
+          card.style.filter = '';
+        }
+      });
+    } else {
+      showScene('a2gal', 0);
+    }
+
+    // Overlay statement 1: "design while you sleep"
+    {
+      let op = 0;
+      if (t >= T.stmt1[0] && t < T.stmt1[1]) {
+        const inP = expoOut(clampLerp(t, T.stmt1[0], T.stmt1[0] + 0.4));
+        const outP = easeOut(clampLerp(t, T.stmt1[1] - 0.4, T.stmt1[1]));
+        op = inP * (1 - outP);
+      }
+      showOver('stmt1', op);
+    }
+    // Overlay statement 2: "ship while meeting"
+    {
+      let op = 0;
+      if (t >= T.stmt2[0] && t < T.stmt2[1]) {
+        const inP = expoOut(clampLerp(t, T.stmt2[0], T.stmt2[0] + 0.4));
+        const outP = easeOut(clampLerp(t, T.stmt2[1] - 0.4, T.stmt2[1]));
+        op = inP * (1 - outP);
+      }
+      showOver('stmt2', op);
+    }
+
+    // ============ Act 3 Medium ============
+    if (t >= T.a3med_in[0] - 0.1 && t < T.a3med_out[1]) {
+      let op;
+      if (t < T.a3med_in[1]) op = lerp(t, T.a3med_in[0], T.a3med_in[1], 0, 1, expoOut);
+      else if (t < T.a3med_out[0]) op = 1;
+      else op = lerp(t, T.a3med_out[0], T.a3med_out[1], 1, 0, easeOut);
+      showScene('a3med', op);
+
+      const morph = expoOut(clampLerp(t, T.a3med_in[0], T.a3med_in[1] + 0.4));
+      const w = 100 + (300 - 100) * morph;
+      stmtMedium.style.fontVariationSettings = `"wght" ${w.toFixed(0)}`;
+      stmtMedium.style.fontWeight = Math.round(w);
+
+      const rise = lerp(t, T.a3med_in[0], T.a3med_in[1], 24, 0, expoOut);
+      stmtMedium.style.transform = `translate3d(0, ${rise}px, 0)`;
+    } else {
+      showScene('a3med', 0);
+    }
+
+    // ============ Act 3 Brand ============
+    if (t >= T.a3brand_in[0] - 0.1) {
+      const op = clampLerp(t, T.a3brand_in[0], T.a3brand_in[1]);
+      showScene('a3brand', op);
+
+      // Wordmark weight morph
+      const morphP = expoOut(clampLerp(t, T.brand_morph[0], T.brand_morph[1]));
+      const wght = 100 + (700 - 100) * morphP;
+      wordmark.style.fontVariationSettings = `"wght" ${wght.toFixed(0)}`;
+      wordmark.style.fontWeight = Math.round(wght);
+
+      const wRise = lerp(t, T.a3brand_in[0], T.a3brand_in[1], 20, 0, expoOut);
+      wordmark.style.transform = `translate3d(0, ${wRise}px, 0)`;
+
+      // Farewell quote
+      const fOp = clampLerp(t, T.a3farewell_in[0], T.a3farewell_in[1]);
+      const fRise = lerp(t, T.a3farewell_in[0], T.a3farewell_in[1], 12, 0, expoOut);
+      farewell.style.opacity = fOp;
+      farewell.style.transform = `translate3d(0, ${fRise}px, 0)`;
+
+      // CN subtitle
+      const cnOp = clampLerp(t, T.a3cn_in[0], T.a3cn_in[1]);
+      farewellCn.style.opacity = cnOp;
+
+      // URL
+      const uOp = clampLerp(t, T.a3url_in[0], T.a3url_in[1]);
+      urlEl.style.opacity = uOp;
+    } else {
+      showScene('a3brand', 0);
+    }
+
+    // Watermark: visible during Act 2-3
+    if (t >= T.a2tty_in[0] && t < T.DURATION - 0.2) {
+      watermark.classList.add('visible');
+    } else {
+      watermark.classList.remove('visible');
+    }
+  }
+
+  // ---------- Driver ----------
+  let manualT = null;
+  let startMs = null;
+  let hasFinishedOnce = false;
+  function tick(now) {
+    if (manualT != null) render(manualT);
+    else {
+      if (startMs == null) startMs = now;
+      const elapsed = (now - startMs) / 1000;
+      const recording = window.__recording === true;
+      let t;
+      if (recording) {
+        // Non-looping: clamp at DURATION, hold on final frame
+        t = Math.min(elapsed, T.DURATION - 0.001);
+        if (elapsed >= T.DURATION && !hasFinishedOnce) hasFinishedOnce = true;
+      } else {
+        t = elapsed % T.DURATION;
+      }
+      render(t);
+    }
+    requestAnimationFrame(tick);
+  }
+  requestAnimationFrame(tick);
+
+  // For frame-accurate rendering
+  window.__setTime = function(t) {
+    manualT = t;
+    render(t);
+  };
+  window.__resume = function() { manualT = null; startMs = null; };
+  window.__duration = T.DURATION;
+  window.__render = render;
+  window.__ready = true;
+})();
+</script>
+</body>
+</html>

+ 684 - 0
demos/w1-brand-protocol-en.html

@@ -0,0 +1,684 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8" />
+<title>w1 · Brand Protocol · Five steps, no skipping</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@200;300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<style>
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --dim: rgba(255,255,255,0.18);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;
+    --accent-deep: #B85D3D;
+    --cd-bg: #F5F4F0;
+    --cd-panel: #FFFFFF;
+    --cd-ink: #1A1918;
+
+    --serif-zh: "Noto Serif SC", "Songti SC", serif;
+    --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
+    --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
+    -webkit-font-smoothing: antialiased;
+  }
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+  }
+
+  /* Film grain texture (very subtle) */
+  .stage::before {
+    content: '';
+    position: absolute;
+    inset: 0;
+    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
+    opacity: 0.02;
+    pointer-events: none;
+    z-index: 100;
+  }
+
+  /* Chrome · watermark */
+  .mark {
+    position: absolute;
+    top: 48px; left: 64px;
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,1);
+    opacity: 0.16;
+    pointer-events: none;
+    z-index: 50;
+  }
+  .mark-right {
+    position: absolute;
+    top: 48px; right: 64px;
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,1);
+    opacity: 0.16;
+    pointer-events: none;
+    z-index: 50;
+  }
+
+  /* ====== Title (centered, small, top) ====== */
+  .title-line {
+    position: absolute;
+    top: 128px;
+    left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--mono);
+    font-size: 14px;
+    letter-spacing: 0.28em;
+    color: var(--muted);
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+
+  /* ====== Chain · 5 cards connected by a line ====== */
+  .chain {
+    position: absolute;
+    top: 50%; left: 50%;
+    transform: translate(-50%, -50%);
+    width: 1680px;
+    height: 360px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0 80px;
+  }
+
+  /* The connecting line behind the cards */
+  .chain-line {
+    position: absolute;
+    top: 50%;
+    left: 140px;
+    right: 140px;
+    height: 1px;
+    background: linear-gradient(90deg,
+      transparent 0%,
+      rgba(217,119,87,0.0) 2%,
+      rgba(217,119,87,0.8) 12%,
+      rgba(217,119,87,0.8) 88%,
+      rgba(217,119,87,0.0) 98%,
+      transparent 100%);
+    transform-origin: left center;
+    transform: scaleX(0);
+    will-change: transform;
+  }
+
+  .card {
+    position: relative;
+    width: 248px;
+    height: 320px;
+    background: rgba(255,255,255,0.02);
+    border: 1px solid var(--hairline);
+    border-radius: 14px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: space-between;
+    padding: 32px 20px 26px;
+    opacity: 0;
+    transform: translateY(20px);
+    will-change: opacity, transform;
+    backdrop-filter: blur(10px);
+  }
+
+  .card.active {
+    border-color: rgba(217,119,87,0.6);
+    box-shadow:
+      0 0 0 1px rgba(217,119,87,0.35),
+      0 30px 60px -30px rgba(217,119,87,0.35),
+      0 10px 24px -10px rgba(0,0,0,0.6);
+  }
+
+  .card-num {
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.25em;
+    color: var(--muted);
+  }
+  .card.active .card-num {
+    color: var(--accent);
+  }
+
+  .card-glyph {
+    width: 88px;
+    height: 88px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+  }
+
+  .card-label {
+    text-align: center;
+  }
+  .card-label .zh {
+    font-family: var(--serif-en);
+    font-size: 36px;
+    font-style: italic;
+    font-weight: 300;
+    color: var(--ink);
+    letter-spacing: -0.01em;
+    line-height: 1;
+  }
+
+  /* Glyph · Step 1 · Ask (question mark inside a circle, drawn minimal) */
+  .g-ask {
+    width: 80px; height: 80px;
+    border: 1px solid var(--ink-60);
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-family: var(--serif-en);
+    font-weight: 300;
+    font-size: 44px;
+    color: var(--ink-80);
+    position: relative;
+    transition: border-color 0.3s, color 0.3s;
+  }
+  .card.active .g-ask { border-color: var(--accent); color: var(--accent); }
+
+  /* Glyph · Step 2 · Search (magnifier with crosshair) */
+  .g-search {
+    width: 80px; height: 80px;
+    position: relative;
+  }
+  .g-search .ring {
+    position: absolute;
+    top: 10px; left: 10px;
+    width: 52px; height: 52px;
+    border: 1px solid var(--ink-60);
+    border-radius: 50%;
+    transition: border-color 0.3s;
+  }
+  .g-search .handle {
+    position: absolute;
+    bottom: 8px; right: 6px;
+    width: 22px; height: 1px;
+    background: var(--ink-60);
+    transform: rotate(45deg);
+    transform-origin: right center;
+    transition: background 0.3s;
+  }
+  .g-search .dot {
+    position: absolute;
+    top: 26px; left: 26px;
+    width: 4px; height: 4px;
+    background: var(--muted);
+    border-radius: 50%;
+    opacity: 0;
+    transition: opacity 0.3s, background 0.3s;
+  }
+  .card.active .g-search .ring { border-color: var(--accent); }
+  .card.active .g-search .handle { background: var(--accent); }
+  .card.active .g-search .dot { opacity: 1; background: var(--accent); }
+
+  /* Glyph · Step 3 · Grab (download arrow into a tray) */
+  .g-grab {
+    width: 80px; height: 80px;
+    position: relative;
+  }
+  .g-grab .arrow {
+    position: absolute;
+    top: 8px; left: 50%;
+    transform: translateX(-50%);
+    width: 1px; height: 36px;
+    background: var(--ink-60);
+    transition: background 0.3s;
+  }
+  .g-grab .arrow::before {
+    content: '';
+    position: absolute;
+    bottom: -1px; left: 50%;
+    transform: translateX(-50%) rotate(45deg);
+    width: 14px; height: 14px;
+    border-right: 1px solid currentColor;
+    border-bottom: 1px solid currentColor;
+    color: var(--ink-60);
+    transition: color 0.3s;
+  }
+  .g-grab .tray {
+    position: absolute;
+    bottom: 10px; left: 12px; right: 12px;
+    height: 20px;
+    border: 1px solid var(--ink-60);
+    border-top: none;
+    border-radius: 0 0 4px 4px;
+    transition: border-color 0.3s;
+  }
+  .card.active .g-grab .arrow { background: var(--accent); }
+  .card.active .g-grab .arrow::before { color: var(--accent); }
+  .card.active .g-grab .tray { border-color: var(--accent); }
+
+  /* Glyph · Step 4 · Grep (terminal-like code with highlighted match) */
+  .g-grep {
+    width: 100px; height: 80px;
+    font-family: var(--mono);
+    font-size: 10px;
+    color: var(--muted);
+    line-height: 1.5;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    padding-left: 8px;
+    position: relative;
+  }
+  .g-grep .line { white-space: nowrap; }
+  .g-grep .hit {
+    color: var(--accent);
+    background: rgba(217,119,87,0.12);
+    padding: 1px 3px;
+    border-radius: 2px;
+  }
+
+  /* Glyph · Step 5 · Lock (a file with lines) */
+  .g-lock {
+    width: 72px; height: 86px;
+    position: relative;
+  }
+  .g-lock .file {
+    position: absolute;
+    inset: 0;
+    border: 1px solid var(--ink-60);
+    border-radius: 4px;
+    transition: border-color 0.3s;
+  }
+  .g-lock .fold {
+    position: absolute;
+    top: -1px; right: -1px;
+    width: 18px; height: 18px;
+    background: var(--bg);
+    border-left: 1px solid var(--ink-60);
+    border-bottom: 1px solid var(--ink-60);
+    transition: border-color 0.3s;
+  }
+  .g-lock .row {
+    position: absolute;
+    left: 10px;
+    height: 1px;
+    background: var(--muted);
+    transition: background 0.3s;
+  }
+  .g-lock .row.r1 { top: 22px; width: 40px; }
+  .g-lock .row.r2 { top: 34px; width: 48px; }
+  .g-lock .row.r3 { top: 46px; width: 32px; }
+  .g-lock .row.r4 { top: 58px; width: 44px; }
+  .g-lock .row.r5 { top: 70px; width: 28px; background: var(--accent); }
+  .card.active .g-lock .file { border-color: var(--accent); }
+  .card.active .g-lock .fold { border-color: var(--accent); }
+
+  /* ====== Final · brand-spec.md file ====== */
+  .final-file {
+    position: absolute;
+    top: 50%; left: 50%;
+    transform: translate(-50%, -50%) scale(0.9);
+    width: 520px;
+    background: var(--cd-bg);
+    color: var(--cd-ink);
+    border-radius: 10px;
+    padding: 38px 44px 42px;
+    opacity: 0;
+    box-shadow:
+      0 40px 90px -30px rgba(217,119,87,0.4),
+      0 20px 50px -20px rgba(0,0,0,0.6),
+      0 0 0 1px rgba(217,119,87,0.3);
+    will-change: opacity, transform;
+  }
+  .final-file .file-name {
+    font-family: var(--mono);
+    font-size: 14px;
+    letter-spacing: 0.08em;
+    color: var(--accent-deep);
+    margin-bottom: 20px;
+    display: flex;
+    align-items: center;
+    gap: 10px;
+  }
+  .final-file .file-name::before {
+    content: '';
+    width: 6px; height: 6px;
+    background: var(--accent);
+    border-radius: 50%;
+  }
+  .final-file .h1 {
+    font-family: var(--serif-en);
+    font-size: 28px;
+    font-weight: 400;
+    margin: 0 0 18px;
+    letter-spacing: -0.015em;
+  }
+  .final-file .kv {
+    font-family: var(--mono);
+    font-size: 12px;
+    line-height: 1.9;
+    color: rgba(26,25,24,0.65);
+  }
+  .final-file .kv .k { color: var(--accent-deep); }
+  .final-file .kv .swatch {
+    display: inline-block;
+    width: 10px; height: 10px;
+    border-radius: 2px;
+    vertical-align: middle;
+    margin-right: 6px;
+  }
+  .final-file .caret {
+    display: inline-block;
+    width: 7px; height: 14px;
+    background: var(--accent);
+    vertical-align: -2px;
+    margin-left: 2px;
+    animation: blink 1.1s steps(2) infinite;
+  }
+  @keyframes blink { 50% { opacity: 0; } }
+
+  /* Brand reveal (final 2 sec, keeps with Motion Spec) */
+  .brand-sheet {
+    position: absolute;
+    inset: 0;
+    background: var(--cd-bg);
+    transform: translateY(100%);
+    will-change: transform;
+    z-index: 80;
+  }
+  .brand-reveal {
+    position: absolute;
+    inset: 0;
+    z-index: 81;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+  .brand-reveal .wordmark {
+    font-family: var(--sans);
+    font-weight: 100;
+    font-size: 128px;
+    letter-spacing: -0.045em;
+    color: var(--cd-ink);
+    line-height: 1;
+  }
+  .brand-reveal .wordmark .accent { color: var(--accent); }
+  .brand-reveal .underline {
+    width: 0;
+    height: 2px;
+    background: var(--accent);
+    margin-top: 36px;
+    will-change: width;
+  }
+</style>
+</head>
+<body>
+  <div class="stage" id="stage">
+    <div class="mark">HUASHU · DESIGN</div>
+    <div class="mark-right">V2 · 2026</div>
+
+    <div class="title-line" id="titleLine">w1 · brand protocol</div>
+
+    <div class="chain">
+      <div class="chain-line" id="chainLine"></div>
+
+      <div class="card" data-step="1">
+        <div class="card-num">STEP 01</div>
+        <div class="card-glyph"><div class="g-ask">?</div></div>
+        <div class="card-label">
+          <div class="zh">Ask</div>
+        </div>
+      </div>
+
+      <div class="card" data-step="2">
+        <div class="card-num">STEP 02</div>
+        <div class="card-glyph">
+          <div class="g-search">
+            <div class="ring"></div>
+            <div class="handle"></div>
+            <div class="dot"></div>
+          </div>
+        </div>
+        <div class="card-label">
+          <div class="zh">Search</div>
+        </div>
+      </div>
+
+      <div class="card" data-step="3">
+        <div class="card-num">STEP 03</div>
+        <div class="card-glyph">
+          <div class="g-grab">
+            <div class="arrow"></div>
+            <div class="tray"></div>
+          </div>
+        </div>
+        <div class="card-label">
+          <div class="zh">Grab</div>
+        </div>
+      </div>
+
+      <div class="card" data-step="4">
+        <div class="card-num">STEP 04</div>
+        <div class="card-glyph">
+          <div class="g-grep">
+            <div class="line">#F5F4F0</div>
+            <div class="line"><span class="hit">#D97757</span></div>
+            <div class="line">#1A1918</div>
+            <div class="line">#FFFFFF</div>
+          </div>
+        </div>
+        <div class="card-label">
+          <div class="zh">Grep</div>
+        </div>
+      </div>
+
+      <div class="card" data-step="5">
+        <div class="card-num">STEP 05</div>
+        <div class="card-glyph">
+          <div class="g-lock">
+            <div class="file"></div>
+            <div class="fold"></div>
+            <div class="row r1"></div>
+            <div class="row r2"></div>
+            <div class="row r3"></div>
+            <div class="row r4"></div>
+            <div class="row r5"></div>
+          </div>
+        </div>
+        <div class="card-label">
+          <div class="zh">Lock</div>
+        </div>
+      </div>
+    </div>
+
+    <div class="final-file" id="finalFile">
+      <div class="file-name">brand-spec.md</div>
+      <div class="h1">Assets locked in<span class="caret"></span></div>
+      <div class="kv">
+        <div><span class="k">logo</span> · assets/logo.svg</div>
+        <div><span class="k">hero</span> · product-hero.png</div>
+        <div><span class="k">accent</span> · <span class="swatch" style="background:#D97757"></span>#D97757</div>
+        <div><span class="k">bg</span> · <span class="swatch" style="background:#000;border:1px solid rgba(0,0,0,0.15)"></span>#000000</div>
+      </div>
+    </div>
+
+    <div class="brand-sheet" id="brandSheet"></div>
+    <div class="brand-reveal" id="brandReveal">
+      <div class="wordmark">huashu<span class="accent"> · </span>design</div>
+      <div class="underline" id="brandUnderline"></div>
+    </div>
+  </div>
+
+<script>
+  // ── Auto-scale stage to viewport ─────────────────
+  function fitStage() {
+    const stage = document.getElementById('stage');
+    const sx = window.innerWidth / 1920;
+    const sy = window.innerHeight / 1080;
+    const s = Math.min(sx, sy);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  fitStage();
+  window.addEventListener('resize', fitStage);
+
+  // ── Easing functions ─────────────────
+  const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
+  const expoIn  = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
+  const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
+  const cubicOut = t => 1 - Math.pow(1 - t, 3);
+
+  function lerp(t, a, b, easing) {
+    if (t <= 0) return a;
+    if (t >= 1) return b;
+    const e = easing ? easing(t) : t;
+    return a + (b - a) * e;
+  }
+  function seg(time, start, end) {
+    if (time <= start) return 0;
+    if (time >= end) return 1;
+    return (time - start) / (end - start);
+  }
+
+  // ── Timeline (total 12s) ─────────────────
+  // Beat 1 (0-2s) · Beat 2 (2-10s) · Beat 3 (10-12s)
+  //
+  // Card schedule:
+  //   Card 1 enter 0.8-1.6s, active 1.6-3.0
+  //   Card 2 enter 2.4-3.2s, active 3.2-4.6
+  //   Card 3 enter 4.0-4.8s, active 4.8-6.2
+  //   Card 4 enter 5.6-6.4s, active 6.4-7.8
+  //   Card 5 enter 7.2-8.0s, active 8.0-9.4
+  //   All cards stay visible (frozen after active ends)
+  //
+  // Line draws 0.6-8.0s (while cards come in)
+  // Title fades in 0.2-1.2, fades out 9.6-10.0
+  // Final file: 8.8-9.8 scale in, hold to 10.0
+  // Brand reveal: 10.0-12.0
+
+  const cards = Array.from(document.querySelectorAll('.card'));
+  const cardTimings = [
+    { enter: [0.8, 1.6], active: [1.6, 3.0] },
+    { enter: [2.4, 3.2], active: [3.2, 4.6] },
+    { enter: [4.0, 4.8], active: [4.8, 6.2] },
+    { enter: [5.6, 6.4], active: [6.4, 7.8] },
+    { enter: [7.2, 8.0], active: [8.0, 9.4] },
+  ];
+
+  const titleLine = document.getElementById('titleLine');
+  const chainLine = document.getElementById('chainLine');
+  const finalFile = document.getElementById('finalFile');
+  const brandSheet = document.getElementById('brandSheet');
+  const brandReveal = document.getElementById('brandReveal');
+  const brandUnderline = document.getElementById('brandUnderline');
+
+  const DURATION = 12.0;
+  let startTime = null;
+  let loop = true;
+
+  // Honor recording flag
+  if (window.__recording === true) loop = false;
+
+  function tick(now) {
+    if (startTime === null) startTime = now;
+    let t = (now - startTime) / 1000;
+
+    if (t >= DURATION) {
+      if (loop) { startTime = now; t = 0; }
+      else { t = DURATION; }
+    }
+
+    // Title
+    const titleIn = seg(t, 0.2, 1.2);
+    const titleOut = seg(t, 9.6, 10.0);
+    const titleOpacity = Math.min(cubicOut(titleIn), 1 - titleOut);
+    titleLine.style.opacity = Math.max(0, titleOpacity);
+    titleLine.style.transform = `translateX(-50%) translateY(${lerp(titleIn, -8, 0, cubicOut)}px)`;
+
+    // Chain line — grows left→right as cards arrive
+    const lineT = seg(t, 0.6, 8.0);
+    chainLine.style.transform = `scaleX(${cubicInOut(lineT)})`;
+
+    // Cards
+    cards.forEach((card, i) => {
+      const { enter, active } = cardTimings[i];
+      const enterT = seg(t, enter[0], enter[1]);
+
+      const baseOp = expoOut(enterT);
+      const ty = lerp(enterT, 20, 0, expoOut);
+
+      // Active state during the card's "spotlight" window
+      const isActive = t >= active[0] && t <= active[1];
+      card.classList.toggle('active', isActive);
+
+      // Cards dim to 25% when final file starts zooming in (8.8-9.6),
+      // then fade fully when brand reveal takes over (10.0-10.4)
+      const dimT = seg(t, 8.8, 9.6);
+      const exitT = seg(t, 10.0, 10.4);
+      const dimFactor = lerp(dimT, 1.0, 0.22, cubicInOut);
+      const finalOp = baseOp * dimFactor * (1 - exitT);
+
+      if (dimT > 0) card.classList.remove('active');
+
+      card.style.opacity = finalOp;
+      card.style.transform = `translateY(${ty - 10 * exitT}px)`;
+    });
+
+    // Chain line also dims when final file zooms, fades with cards at 10.0-10.4
+    const chainDim = seg(t, 8.8, 9.6);
+    const chainExit = seg(t, 10.0, 10.4);
+    chainLine.style.opacity = lerp(chainDim, 1, 0.22, cubicInOut) * (1 - chainExit);
+
+    // Final file: 8.8-9.8 scale+fade in, then 9.8-10.2 scale+settle, hold to ~10.0
+    const finalInT = seg(t, 8.8, 9.8);
+    const finalScale = lerp(finalInT, 0.88, 1.0, expoOut);
+    const finalOp = cubicOut(finalInT);
+    // fade final file out into brand reveal
+    const finalOut = seg(t, 10.0, 10.6);
+    finalFile.style.opacity = finalOp * (1 - finalOut);
+    finalFile.style.transform = `translate(-50%, -50%) scale(${finalScale * (1 - finalOut * 0.04)})`;
+
+    // Brand reveal — sheet slides up from bottom 10.0-10.6, wordmark fades in 10.6-11.4, underline 11.4-11.9
+    const sheetT = seg(t, 10.0, 10.6);
+    brandSheet.style.transform = `translateY(${lerp(sheetT, 100, 0, expoOut)}%)`;
+
+    const wordT = seg(t, 10.6, 11.4);
+    brandReveal.style.opacity = cubicOut(wordT);
+    // NOTE: no scale transform on .brand-reveal — it would compound with the
+    // underline width animation and make the line appear mis-placed. Instead,
+    // scale the wordmark alone via font-variation-settings-safe approach: none here.
+
+    const underT = seg(t, 11.4, 11.9);
+    brandUnderline.style.width = `${lerp(underT, 0, 280, expoOut)}px`;
+
+    // Mark as ready for recorder on first frame
+    if (!window.__ready) window.__ready = true;
+
+    if (loop || t < DURATION) requestAnimationFrame(tick);
+  }
+  // Wait for fonts before first paint so Serif glyphs are correct
+  (document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve())
+    .then(() => requestAnimationFrame(tick));
+</script>
+</body>
+</html>

+ 649 - 740
demos/w1-brand-protocol.html

@@ -1,787 +1,696 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
+<!doctype html>
+<html lang="zh-Hans">
 <head>
-<meta charset="UTF-8">
-<title>Huashu-Design · 品牌资产协议 5 步硬流程</title>
-<script crossorigin src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
-<script crossorigin src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
-<script src="https://unpkg.com/@babel/standalone@7.25.6/babel.min.js"></script>
+<meta charset="utf-8" />
+<title>w1 · 品牌协议 · 五步不能跳</title>
 <link rel="preconnect" href="https://fonts.googleapis.com">
 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,300;1,6..72,400;1,6..72,500&family=Noto+Serif+SC:wght@300;400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@200;300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
 <style>
-  * { box-sizing: border-box; margin: 0; padding: 0; }
-  html, body { width: 100%; height: 100%; overflow: hidden; }
-  body {
-    background: #0c0c0c;
-    font-family: 'Newsreader', 'Noto Serif SC', Georgia, serif;
-    color: #1a1a1a;
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --dim: rgba(255,255,255,0.18);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;
+    --accent-deep: #B85D3D;
+    --cd-bg: #F5F4F0;
+    --cd-panel: #FFFFFF;
+    --cd-ink: #1A1918;
+
+    --serif-zh: "Noto Serif SC", "Songti SC", serif;
+    --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
+    --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
     -webkit-font-smoothing: antialiased;
-    text-rendering: optimizeLegibility;
   }
-</style>
-</head>
-<body>
-<div id="root"></div>
-
-<!-- animations.jsx inlined -->
-<script type="text/babel">
-(function() {
-  const { createContext, useContext, useState, useEffect, useRef, useCallback } = React;
-  const TimeContext = createContext({ time: 0, duration: 10, playing: false });
-  const SpriteContext = createContext(null);
-
-  const Easing = {
-    linear: t => t,
-    easeIn: t => t * t,
-    easeOut: t => 1 - (1 - t) * (1 - t),
-    easeInOut: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
-    spring: t => {
-      const c = (2 * Math.PI) / 3;
-      return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c) + 1;
-    },
-  };
-
-  function interpolate(t, input, output, easing) {
-    const [inStart, inEnd] = input;
-    const [outStart, outEnd] = output;
-    if (t <= inStart) return outStart;
-    if (t >= inEnd) return outEnd;
-    let progress = (t - inStart) / (inEnd - inStart);
-    if (easing) progress = easing(progress);
-    return outStart + (outEnd - outStart) * progress;
-  }
-
-  function useTime() { return useContext(TimeContext).time; }
-  function useSprite() {
-    const sprite = useContext(SpriteContext);
-    return sprite || { t: 0, elapsed: 0, duration: 0 };
-  }
-
-  function Stage({ duration = 10, width = 1920, height = 1080, loop = true, children, bgColor = '#fff' }) {
-    const [time, setTime] = useState(0);
-    const [playing, setPlaying] = useState(true);
-    const [scale, setScale] = useState(1);
-    const rafRef = useRef(null);
-    const effectiveLoop = (typeof window !== 'undefined' && window.__recording) ? false : loop;
-
-    useEffect(() => {
-      function updateScale() {
-        const vw = window.innerWidth;
-        const vh = window.innerHeight - 56;
-        const s = Math.min(vw / width, vh / height);
-        setScale(s);
-      }
-      updateScale();
-      window.addEventListener('resize', updateScale);
-      return () => window.removeEventListener('resize', updateScale);
-    }, [width, height]);
-
-    useEffect(() => {
-      if (!playing) return;
-      let cancelled = false;
-      let last = null;
-      function tick(now) {
-        if (cancelled) return;
-        if (last === null) {
-          last = now;
-          if (typeof window !== 'undefined') window.__ready = true;
-        }
-        const delta = (now - last) / 1000;
-        last = now;
-        setTime(prev => {
-          const next = prev + delta;
-          if (next >= duration) return effectiveLoop ? 0 : duration - 0.001;
-          return next;
-        });
-        rafRef.current = requestAnimationFrame(tick);
-      }
-      const start = () => { if (!cancelled) rafRef.current = requestAnimationFrame(tick); };
-      if (document.fonts && document.fonts.ready) document.fonts.ready.then(start); else start();
-      return () => { cancelled = true; cancelAnimationFrame(rafRef.current); };
-    }, [playing, duration, effectiveLoop]);
-
-    const progress = time / duration;
-    const ctx = { time, duration, playing, setPlaying, setTime };
-
-    const canvasStyle = {
-      position: 'absolute',
-      top: '50%',
-      left: '50%',
-      transformOrigin: 'center center',
-      width,
-      height,
-      background: bgColor,
-      overflow: 'hidden',
-      transform: `translate(-50%, -50%) scale(${scale})`,
-    };
-
-    return (
-      <TimeContext.Provider value={ctx}>
-        <div style={{position:'fixed', inset:0, background:'#0c0c0c', display:'flex', flexDirection:'column'}}>
-          <div style={{flex:1, position:'relative', overflow:'hidden'}}>
-            <div style={canvasStyle}>{children}</div>
-          </div>
-          <div className="no-record" style={{position:'fixed', bottom:0, left:0, right:0, background:'rgba(0,0,0,0.8)', backdropFilter:'blur(10px)', padding:'12px 20px', display:'flex', alignItems:'center', gap:16, color:'#fff', fontSize:12, zIndex:100}}>
-            <button onClick={()=>setPlaying(p=>!p)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>{playing?'⏸ 暂停':'▶ 播放'}</button>
-            <button onClick={()=>setTime(0)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>⏮ 开始</button>
-            <div style={{fontFamily:'ui-monospace, monospace', fontVariantNumeric:'tabular-nums', minWidth:90}}>{time.toFixed(2)}s / {duration.toFixed(2)}s</div>
-            <div style={{flex:1, height:4, background:'rgba(255,255,255,0.2)', borderRadius:2, position:'relative'}}>
-              <div style={{position:'absolute', top:0, left:0, height:'100%', width:`${progress*100}%`, background:'#fff', borderRadius:2}} />
-            </div>
-          </div>
-        </div>
-      </TimeContext.Provider>
-    );
-  }
-
-  function Sprite({ start = 0, end, children, style }) {
-    const { time } = useContext(TimeContext);
-    const actualEnd = end == null ? Infinity : end;
-    if (time < start || time >= actualEnd) return null;
-    const duration = actualEnd - start;
-    const elapsed = time - start;
-    const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
-    const spriteValue = { t, elapsed, duration, start, end: actualEnd };
-    return (
-      <SpriteContext.Provider value={spriteValue}>
-        <div style={{position:'absolute', inset:0, ...style}}>{children}</div>
-      </SpriteContext.Provider>
-    );
-  }
-
-  window.Animations = { Stage, Sprite, useTime, useSprite, Easing, interpolate };
-})();
-</script>
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+  }
 
-<!-- Demo scene -->
-<script type="text/babel">
-const { Stage, Sprite, useTime, useSprite, Easing, interpolate } = window.Animations;
-
-// ── Design tokens ─────────────────────────────────────────
-const CREAM = '#FAF6EF';
-const INK = '#1a1a1a';
-const TERRA = '#C04A1A';
-const ASH = '#6b6b6b';
-const LINE = '#d9d2c5';
-const OLIVE = '#6a6b4e';
-
-const serif = "'Newsreader', 'Noto Serif SC', Georgia, serif";
-const sans = "'Inter', -apple-system, sans-serif";
-const mono = "'JetBrains Mono', ui-monospace, monospace";
-
-// ── Scene 1: Trigger (0 – 3s) ─────────────────────────────
-function Scene1_Trigger() {
-  const { elapsed } = useSprite();
-  const labelOp = interpolate(elapsed, [0, 0.5], [0, 1]);
-  const titleY = interpolate(elapsed, [0, 1], [30, 0], Easing.easeOut);
-  const titleOp = interpolate(elapsed, [0.2, 1], [0, 1]);
-  const brandsOp = interpolate(elapsed, [1, 1.6], [0, 1]);
-  const switchOp = interpolate(elapsed, [1.9, 2.4], [0, 1]);
-  const fadeOut = interpolate(elapsed, [2.6, 3], [1, 0]);
-
-  const brands = ['Kimi', 'Linear', 'Lovart', 'Stripe'];
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
-      display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
-      <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
-        color: TERRA, marginBottom: 28, opacity: labelOp}}>
-        品 牌 资 产 协 议 · BRAND PROTOCOL
-      </div>
-      <div style={{fontFamily: serif, fontSize: 120, fontWeight: 500, color: INK,
-        lineHeight: 1, letterSpacing:'-0.01em',
-        opacity: titleOp, transform: `translateY(${titleY}px)`}}>
-        涉及具体品牌<span style={{color: TERRA, fontStyle:'italic'}}>?</span>
-      </div>
+  /* Film grain texture (very subtle) */
+  .stage::before {
+    content: '';
+    position: absolute;
+    inset: 0;
+    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
+    opacity: 0.02;
+    pointer-events: none;
+    z-index: 100;
+  }
 
-      <div style={{display:'flex', gap: 48, marginTop: 64, opacity: brandsOp}}>
-        {brands.map((b, i) => (
-          <span key={i} style={{fontFamily: serif, fontStyle:'italic',
-            fontSize: 44, color: ASH, letterSpacing:'0.02em'}}>
-            {b}
-          </span>
-        ))}
-      </div>
+  /* Chrome · watermark */
+  .mark {
+    position: absolute;
+    top: 48px; left: 64px;
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,1);
+    opacity: 0.16;
+    pointer-events: none;
+    z-index: 50;
+  }
+  .mark-right {
+    position: absolute;
+    top: 48px; right: 64px;
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,1);
+    opacity: 0.16;
+    pointer-events: none;
+    z-index: 50;
+  }
 
-      <div style={{marginTop: 72, opacity: switchOp, textAlign:'center'}}>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 34,
-          color: TERRA, letterSpacing:'0.02em'}}>
-          先停下——走 5 步硬流程
-        </div>
-        <div style={{height:1, background: TERRA, width: 180, margin:'18px auto 0'}} />
-      </div>
-    </div>
-  );
-}
-
-// ── Scene 2: Step 1 & 2 (3 – 7s) ──────────────────────────
-function Scene2_AskAndSearch() {
-  const { elapsed } = useSprite();
-  const headOp = interpolate(elapsed, [0, 0.4], [0, 1]);
-  const leftOp = interpolate(elapsed, [0.3, 0.9], [0, 1]);
-  const leftY = interpolate(elapsed, [0.3, 0.9], [20, 0], Easing.easeOut);
-  const rightOp = interpolate(elapsed, [0.8, 1.4], [0, 1]);
-  const rightY = interpolate(elapsed, [0.8, 1.4], [20, 0], Easing.easeOut);
-  const fadeOut = interpolate(elapsed, [3.5, 4], [1, 0]);
-
-  // cursor sweeping through search paths 1.6 → 3.2
-  const paths = [
-    '<brand>.com/brand',
-    '<brand>.com/press',
-    'brand.<brand>.com',
-    '<brand>.github.io/brand',
-  ];
-  // current active index based on time
-  const activeIdx = Math.min(3, Math.max(0, Math.floor((elapsed - 1.6) / 0.4)));
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
-      padding:'72px 120px', display:'flex', flexDirection:'column'}}>
-      <div style={{display:'flex', justifyContent:'space-between',
-        alignItems:'baseline', opacity: headOp, marginBottom: 48}}>
-        <div style={{fontFamily: serif, fontSize: 44, fontWeight: 500, color: INK}}>
-          Step 1 & 2 · 问 · 搜
-        </div>
-        <div style={{fontFamily: mono, fontSize: 11, color: ASH, letterSpacing:'0.25em'}}>
-          01 / 05  →  02 / 05
-        </div>
-      </div>
+  /* ====== Title (centered, small, top) ====== */
+  .title-line {
+    position: absolute;
+    top: 128px;
+    left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--mono);
+    font-size: 14px;
+    letter-spacing: 0.28em;
+    color: var(--muted);
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity, transform;
+  }
 
-      <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap: 48, flex:1}}>
-        {/* Left: Step 1 Ask */}
-        <div style={{opacity: leftOp, transform:`translateY(${leftY}px)`,
-          display:'flex', flexDirection:'column', gap: 24}}>
-          <div style={{display:'flex', alignItems:'baseline', gap: 18}}>
-            <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-              letterSpacing:'0.3em'}}>STEP · 01</div>
-            <div style={{fontFamily: serif, fontSize: 40, color: INK,
-              fontWeight: 500}}>问</div>
-          </div>
+  /* ====== Chain · 5 cards connected by a line ====== */
+  .chain {
+    position: absolute;
+    top: 50%; left: 50%;
+    transform: translate(-50%, -50%);
+    width: 1680px;
+    height: 360px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0 80px;
+  }
 
-          <div style={{background:'#fff', border:`1px solid ${LINE}`,
-            padding:'32px 34px', position:'relative'}}>
-            <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 64,
-              color: TERRA, lineHeight:1, position:'absolute',
-              top: 8, left: 14}}>「</div>
-            <div style={{fontFamily: serif, fontSize: 24, color: INK,
-              lineHeight: 1.55, paddingLeft: 36}}>
-              这个品牌有 <span style={{fontStyle:'italic', color: TERRA}}>brand guidelines</span> 吗?
-            </div>
-            <div style={{height:1, background: LINE, margin:'22px 0'}} />
-            <div style={{fontFamily: serif, fontSize: 20, color: ASH,
-              lineHeight: 1.6, paddingLeft: 36}}>
-              有的话直接给我;没有我去搜。
-            </div>
-          </div>
+  /* The connecting line behind the cards */
+  .chain-line {
+    position: absolute;
+    top: 50%;
+    left: 140px;
+    right: 140px;
+    height: 1px;
+    background: linear-gradient(90deg,
+      transparent 0%,
+      rgba(217,119,87,0.0) 2%,
+      rgba(217,119,87,0.8) 12%,
+      rgba(217,119,87,0.8) 88%,
+      rgba(217,119,87,0.0) 98%,
+      transparent 100%);
+    transform-origin: left center;
+    transform: scaleX(0);
+    will-change: transform;
+  }
 
-          <div style={{fontFamily: mono, fontSize: 11, color: ASH,
-            letterSpacing:'0.15em', marginTop: 6}}>
-            ▸ 用户提供 &gt; AI 猜测
-          </div>
-        </div>
+  .card {
+    position: relative;
+    width: 248px;
+    height: 320px;
+    background: rgba(255,255,255,0.02);
+    border: 1px solid var(--hairline);
+    border-radius: 14px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: space-between;
+    padding: 32px 20px 26px;
+    opacity: 0;
+    transform: translateY(20px);
+    will-change: opacity, transform;
+    backdrop-filter: blur(10px);
+  }
 
-        {/* Right: Step 2 Search */}
-        <div style={{opacity: rightOp, transform:`translateY(${rightY}px)`,
-          display:'flex', flexDirection:'column', gap: 24}}>
-          <div style={{display:'flex', alignItems:'baseline', gap: 18}}>
-            <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-              letterSpacing:'0.3em'}}>STEP · 02</div>
-            <div style={{fontFamily: serif, fontSize: 40, color: INK,
-              fontWeight: 500}}>搜 官 方 品 牌 页</div>
-          </div>
+  .card.active {
+    border-color: rgba(217,119,87,0.6);
+    box-shadow:
+      0 0 0 1px rgba(217,119,87,0.35),
+      0 30px 60px -30px rgba(217,119,87,0.35),
+      0 10px 24px -10px rgba(0,0,0,0.6);
+  }
 
-          <div style={{background:'#1a1a1a', color:'#e8e3d6',
-            padding:'24px 28px', fontFamily: mono, fontSize: 16,
-            lineHeight: 1.9, flex: 1}}>
-            <div style={{color:'#6b6b6b', fontSize: 11,
-              letterSpacing:'0.2em', marginBottom: 14}}>
-              $ HTTP GET · typical paths
-            </div>
-            {paths.map((p, i) => {
-              const isActive = i === activeIdx && elapsed > 1.6 && elapsed < 3.4;
-              const visited = i < activeIdx;
-              return (
-                <div key={i} style={{
-                  color: isActive ? TERRA : (visited ? '#8a8878' : '#e8e3d6'),
-                  background: isActive ? 'rgba(192,74,26,0.12)' : 'transparent',
-                  padding:'2px 8px', marginLeft:-8,
-                  display:'flex', alignItems:'baseline', gap: 14}}>
-                  <span style={{color: isActive ? TERRA : '#555', fontSize: 13}}>
-                    {isActive ? '▸' : (visited ? '✓' : ' ')}
-                  </span>
-                  <span style={{letterSpacing:'0.02em'}}>{p}</span>
-                </div>
-              );
-            })}
-          </div>
+  .card-num {
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.25em;
+    color: var(--muted);
+  }
+  .card.active .card-num {
+    color: var(--accent);
+  }
 
-          <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
-            color: ASH, marginTop: 6}}>
-            按顺序试,中断也要记 404 状态
-          </div>
-        </div>
-      </div>
-    </div>
-  );
-}
-
-// ── Scene 3: Step 3 · Three fallbacks (7 – 12s) ───────────
-function Scene3_Fallbacks() {
-  const { elapsed } = useSprite();
-  const headOp = interpolate(elapsed, [0, 0.4], [0, 1]);
-  const cardsOp = interpolate(elapsed, [0.3, 1], [0, 1]);
-  const fadeOut = interpolate(elapsed, [4.5, 5], [1, 0]);
-
-  // Card state: 0 idle, 1 active/lit, 2 failed/gray, 3 success
-  // Card 1 active 1.2→2.0, then fails at 2.0
-  // Card 2 active 2.2→3.0, then fails at 3.0
-  // Card 3 active 3.2→4.2, succeeds at 4.2
-  const card1State = elapsed < 1.2 ? 0 : elapsed < 2.0 ? 1 : 2;
-  const card2State = elapsed < 2.2 ? 0 : elapsed < 3.0 ? 1 : (elapsed < 3.2 ? 2 : 2);
-  const card3State = elapsed < 3.2 ? 0 : elapsed < 4.2 ? 1 : 3;
-
-  const captionOp = interpolate(elapsed, [4.2, 4.6], [0, 1]);
-
-  const cards = [
-    { n: '01', title: 'SVG 文件', cmd: 'curl -o logo.svg <url>', status: '最理想', state: card1State },
-    { n: '02', title: '官网 HTML', cmd: 'curl -A "Mozilla/5.0" \\\n  -L <url>', status: '80% 场景必用', state: card2State },
-    { n: '03', title: '产品截图取色', cmd: 'macOS Preview · 吸管', status: 'App 必走', state: card3State },
-  ];
+  .card-glyph {
+    width: 88px;
+    height: 88px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+  }
 
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
-      padding:'70px 100px 60px', display:'flex', flexDirection:'column'}}>
-      <div style={{opacity: headOp, marginBottom: 40}}>
-        <div style={{display:'flex', alignItems:'baseline', gap: 18, marginBottom: 6}}>
-          <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-            letterSpacing:'0.3em'}}>STEP · 03</div>
-          <div style={{fontFamily: serif, fontSize: 18, color: ASH,
-            fontStyle:'italic'}}>下 · download</div>
-        </div>
-        <div style={{fontFamily: serif, fontSize: 60, fontWeight: 500,
-          color: INK, letterSpacing:'-0.01em'}}>
-          三条兜底路径
-        </div>
-        <div style={{height:1, background: LINE, width:'100%', marginTop: 20}} />
-      </div>
+  .card-label {
+    text-align: center;
+  }
+  .card-label .zh {
+    font-family: var(--serif-zh);
+    font-size: 32px;
+    font-weight: 300;
+    color: var(--ink);
+    letter-spacing: 0.04em;
+    line-height: 1;
+    margin-bottom: 10px;
+  }
+  .card-label .en {
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.22em;
+    color: var(--muted);
+    text-transform: uppercase;
+  }
 
-      <div style={{display:'grid', gridTemplateColumns:'1fr 1fr 1fr',
-        gap: 28, flex:1, opacity: cardsOp}}>
-        {cards.map((c, i) => {
-          const isIdle = c.state === 0;
-          const isActive = c.state === 1;
-          const isFailed = c.state === 2;
-          const isSuccess = c.state === 3;
-
-          const borderColor = isSuccess ? TERRA :
-                              isActive ? INK :
-                              isFailed ? '#c4bda7' : LINE;
-          const borderWidth = (isActive || isSuccess) ? 2 : 1;
-          const op = isFailed ? 0.42 : 1;
-          const bg = isSuccess ? '#fffaf3' : '#fff';
-
-          return (
-            <div key={i} style={{
-              background: bg,
-              border:`${borderWidth}px solid ${borderColor}`,
-              opacity: op,
-              padding:'28px 28px 24px',
-              display:'flex', flexDirection:'column',
-              position:'relative',
-              transition: 'none',
-            }}>
-              <div style={{display:'flex', justifyContent:'space-between',
-                alignItems:'baseline', marginBottom: 20}}>
-                <div style={{fontFamily: serif, fontSize: 56, fontWeight: 300,
-                  color: isSuccess ? TERRA : INK, lineHeight:1}}>
-                  {c.n}
-                </div>
-                <div style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.2em',
-                  color: isSuccess ? TERRA : isFailed ? ASH : INK}}>
-                  {isIdle && 'READY'}
-                  {isActive && '▸ TRYING…'}
-                  {isFailed && '× FAILED'}
-                  {isSuccess && '✓ GOT IT'}
-                </div>
-              </div>
-
-              <div style={{fontFamily: serif, fontSize: 28, fontWeight: 500,
-                color: INK, marginBottom: 14, letterSpacing:'-0.005em'}}>
-                {c.title}
-              </div>
-
-              <div style={{background:'#1a1a1a', color:'#e8e3d6',
-                fontFamily: mono, fontSize: 12, padding:'14px 16px',
-                whiteSpace:'pre-wrap', lineHeight: 1.6,
-                marginBottom: 20, borderLeft: `2px solid ${isSuccess ? TERRA : '#333'}`}}>
-                {c.cmd}
-              </div>
-
-              <div style={{marginTop:'auto', borderTop:`1px solid ${LINE}`,
-                paddingTop: 14, display:'flex', justifyContent:'space-between',
-                alignItems:'baseline'}}>
-                <div style={{fontFamily: serif, fontStyle:'italic',
-                  fontSize: 15, color: ASH}}>
-                  场景
-                </div>
-                <div style={{fontFamily: serif, fontSize: 16,
-                  color: isSuccess ? TERRA : INK, fontWeight: 500}}>
-                  {c.status}
-                </div>
-              </div>
-
-              {isSuccess && (
-                <div style={{position:'absolute', top:-10, right:-10,
-                  width: 28, height: 28, borderRadius:'50%', background: TERRA,
-                  color:'#fff', display:'flex', alignItems:'center',
-                  justifyContent:'center', fontFamily: serif, fontSize: 14,
-                  fontWeight: 600}}>
-                  ✓
-                </div>
-              )}
-            </div>
-          );
-        })}
-      </div>
+  /* Glyph · Step 1 · Ask (question mark inside a circle, drawn minimal) */
+  .g-ask {
+    width: 80px; height: 80px;
+    border: 1px solid var(--ink-60);
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-family: var(--serif-en);
+    font-weight: 300;
+    font-size: 44px;
+    color: var(--ink-80);
+    position: relative;
+    transition: border-color 0.3s, color 0.3s;
+  }
+  .card.active .g-ask { border-color: var(--accent); color: var(--accent); }
 
-      <div style={{opacity: captionOp, marginTop: 22, textAlign:'center',
-        fontFamily: serif, fontStyle:'italic', fontSize: 22, color: INK}}>
-        前一条失败,<span style={{color: TERRA}}>立刻</span>走下一条——不要停
-      </div>
-    </div>
-  );
-}
-
-// ── Scene 4: Step 4 · Grep colors (12 – 17s) ──────────────
-function Scene4_GrepColors() {
-  const { elapsed } = useSprite();
-  const headOp = interpolate(elapsed, [0, 0.4], [0, 1]);
-  const cmdOp = interpolate(elapsed, [0.4, 0.9], [0, 1]);
-  const fadeOut = interpolate(elapsed, [4.5, 5], [1, 0]);
-
-  const results = [
-    { count: 47, hex: '#1783FF', label: 'Kimi primary' },
-    { count: 32, hex: '#FAFAFA', label: 'background' },
-    { count: 18, hex: '#1a1a1a', label: 'ink' },
-    { count: 12, hex: '#FF6B35', label: 'accent' },
-    { count:  8, hex: '#6A6B4E', label: 'muted' },
-  ];
+  /* Glyph · Step 2 · Search (magnifier with crosshair) */
+  .g-search {
+    width: 80px; height: 80px;
+    position: relative;
+  }
+  .g-search .ring {
+    position: absolute;
+    top: 10px; left: 10px;
+    width: 52px; height: 52px;
+    border: 1px solid var(--ink-60);
+    border-radius: 50%;
+    transition: border-color 0.3s;
+  }
+  .g-search .handle {
+    position: absolute;
+    bottom: 8px; right: 6px;
+    width: 22px; height: 1px;
+    background: var(--ink-60);
+    transform: rotate(45deg);
+    transform-origin: right center;
+    transition: background 0.3s;
+  }
+  .g-search .dot {
+    position: absolute;
+    top: 26px; left: 26px;
+    width: 4px; height: 4px;
+    background: var(--muted);
+    border-radius: 50%;
+    opacity: 0;
+    transition: opacity 0.3s, background 0.3s;
+  }
+  .card.active .g-search .ring { border-color: var(--accent); }
+  .card.active .g-search .handle { background: var(--accent); }
+  .card.active .g-search .dot { opacity: 1; background: var(--accent); }
+
+  /* Glyph · Step 3 · Grab (download arrow into a tray) */
+  .g-grab {
+    width: 80px; height: 80px;
+    position: relative;
+  }
+  .g-grab .arrow {
+    position: absolute;
+    top: 8px; left: 50%;
+    transform: translateX(-50%);
+    width: 1px; height: 36px;
+    background: var(--ink-60);
+    transition: background 0.3s;
+  }
+  .g-grab .arrow::before {
+    content: '';
+    position: absolute;
+    bottom: -1px; left: 50%;
+    transform: translateX(-50%) rotate(45deg);
+    width: 14px; height: 14px;
+    border-right: 1px solid currentColor;
+    border-bottom: 1px solid currentColor;
+    color: var(--ink-60);
+    transition: color 0.3s;
+  }
+  .g-grab .tray {
+    position: absolute;
+    bottom: 10px; left: 12px; right: 12px;
+    height: 20px;
+    border: 1px solid var(--ink-60);
+    border-top: none;
+    border-radius: 0 0 4px 4px;
+    transition: border-color 0.3s;
+  }
+  .card.active .g-grab .arrow { background: var(--accent); }
+  .card.active .g-grab .arrow::before { color: var(--accent); }
+  .card.active .g-grab .tray { border-color: var(--accent); }
+
+  /* Glyph · Step 4 · Grep (terminal-like code with highlighted match) */
+  .g-grep {
+    width: 100px; height: 80px;
+    font-family: var(--mono);
+    font-size: 10px;
+    color: var(--muted);
+    line-height: 1.5;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    padding-left: 8px;
+    position: relative;
+  }
+  .g-grep .line { white-space: nowrap; }
+  .g-grep .hit {
+    color: var(--accent);
+    background: rgba(217,119,87,0.12);
+    padding: 1px 3px;
+    border-radius: 2px;
+  }
 
-  // Stagger each result row, starting at t=1.5
-  const rowStart = 1.5;
-  const rowStep = 0.32;
-
-  const captionOp = interpolate(elapsed, [3.6, 4.1], [0, 1]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background:'#1a1a1a', opacity: fadeOut,
-      padding:'64px 100px', display:'flex', flexDirection:'column',
-      color:'#e8e3d6'}}>
-      <div style={{display:'flex', justifyContent:'space-between',
-        alignItems:'baseline', marginBottom: 36, opacity: headOp}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-            letterSpacing:'0.3em', marginBottom: 4}}>STEP · 04</div>
-          <div style={{fontFamily: serif, fontSize: 54, fontWeight: 500,
-            color:'#faf6ef', letterSpacing:'-0.01em'}}>
-            Grep 色 值
-          </div>
-        </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
-          color:'#8a8878', textAlign:'right'}}>
-          频次排序 = 品牌色权重<br/>
-          <span style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.2em'}}>
-            / DON'T GUESS · COUNT
-          </span>
-        </div>
-      </div>
+  /* Glyph · Step 5 · Lock (a file with lines) */
+  .g-lock {
+    width: 72px; height: 86px;
+    position: relative;
+  }
+  .g-lock .file {
+    position: absolute;
+    inset: 0;
+    border: 1px solid var(--ink-60);
+    border-radius: 4px;
+    transition: border-color 0.3s;
+  }
+  .g-lock .fold {
+    position: absolute;
+    top: -1px; right: -1px;
+    width: 18px; height: 18px;
+    background: var(--bg);
+    border-left: 1px solid var(--ink-60);
+    border-bottom: 1px solid var(--ink-60);
+    transition: border-color 0.3s;
+  }
+  .g-lock .row {
+    position: absolute;
+    left: 10px;
+    height: 1px;
+    background: var(--muted);
+    transition: background 0.3s;
+  }
+  .g-lock .row.r1 { top: 22px; width: 40px; }
+  .g-lock .row.r2 { top: 34px; width: 48px; }
+  .g-lock .row.r3 { top: 46px; width: 32px; }
+  .g-lock .row.r4 { top: 58px; width: 44px; }
+  .g-lock .row.r5 { top: 70px; width: 28px; background: var(--accent); }
+  .card.active .g-lock .file { border-color: var(--accent); }
+  .card.active .g-lock .fold { border-color: var(--accent); }
+
+  /* ====== Final · brand-spec.md file ====== */
+  .final-file {
+    position: absolute;
+    top: 50%; left: 50%;
+    transform: translate(-50%, -50%) scale(0.9);
+    width: 520px;
+    background: var(--cd-bg);
+    color: var(--cd-ink);
+    border-radius: 10px;
+    padding: 38px 44px 42px;
+    opacity: 0;
+    box-shadow:
+      0 40px 90px -30px rgba(217,119,87,0.4),
+      0 20px 50px -20px rgba(0,0,0,0.6),
+      0 0 0 1px rgba(217,119,87,0.3);
+    will-change: opacity, transform;
+  }
+  .final-file .file-name {
+    font-family: var(--mono);
+    font-size: 14px;
+    letter-spacing: 0.08em;
+    color: var(--accent-deep);
+    margin-bottom: 20px;
+    display: flex;
+    align-items: center;
+    gap: 10px;
+  }
+  .final-file .file-name::before {
+    content: '';
+    width: 6px; height: 6px;
+    background: var(--accent);
+    border-radius: 50%;
+  }
+  .final-file .h1 {
+    font-family: var(--serif-zh);
+    font-size: 26px;
+    font-weight: 400;
+    margin: 0 0 18px;
+    letter-spacing: 0.02em;
+  }
+  .final-file .kv {
+    font-family: var(--mono);
+    font-size: 12px;
+    line-height: 1.9;
+    color: rgba(26,25,24,0.65);
+  }
+  .final-file .kv .k { color: var(--accent-deep); }
+  .final-file .kv .swatch {
+    display: inline-block;
+    width: 10px; height: 10px;
+    border-radius: 2px;
+    vertical-align: middle;
+    margin-right: 6px;
+  }
+  .final-file .caret {
+    display: inline-block;
+    width: 7px; height: 14px;
+    background: var(--accent);
+    vertical-align: -2px;
+    margin-left: 2px;
+    animation: blink 1.1s steps(2) infinite;
+  }
+  @keyframes blink { 50% { opacity: 0; } }
+
+  /* Brand reveal (final 2 sec, keeps with Motion Spec) */
+  .brand-sheet {
+    position: absolute;
+    inset: 0;
+    background: var(--cd-bg);
+    transform: translateY(100%);
+    will-change: transform;
+    z-index: 80;
+  }
+  .brand-reveal {
+    position: absolute;
+    inset: 0;
+    z-index: 81;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+  .brand-reveal .wordmark {
+    font-family: var(--sans);
+    font-weight: 100;
+    font-size: 128px;
+    letter-spacing: -0.045em;
+    color: var(--cd-ink);
+    line-height: 1;
+  }
+  .brand-reveal .wordmark .accent { color: var(--accent); }
+  .brand-reveal .underline {
+    width: 0;
+    height: 2px;
+    background: var(--accent);
+    margin-top: 36px;
+    will-change: width;
+  }
+</style>
+</head>
+<body>
+  <div class="stage" id="stage">
+    <div class="mark">HUASHU · DESIGN</div>
+    <div class="mark-right">V2 · 2026</div>
 
-      {/* Command */}
-      <div style={{opacity: cmdOp, background:'#0e0e0e',
-        border:'1px solid #333', padding:'20px 26px',
-        fontFamily: mono, fontSize: 15, lineHeight: 1.7,
-        marginBottom: 28}}>
-        <div style={{color: TERRA, fontSize: 10, letterSpacing:'0.2em',
-          marginBottom: 10}}>$ COMMAND</div>
-        <div style={{color:'#e8e3d6'}}>
-          <span style={{color:'#888'}}>$</span>{' '}
-          <span style={{color:'#7dd3fc'}}>grep</span>{' '}
-          <span style={{color:'#fbbf24'}}>-hoE</span>{' '}
-          <span style={{color:'#a5f3c5'}}>'#[0-9A-Fa-f]{'{6}'}'</span>{' '}
-          <span style={{color:'#e8e3d6'}}>assets/*.{'{svg,html}'}</span> \<br/>
-          {'  '}<span style={{color:'#666'}}>|</span>{' '}
-          <span style={{color:'#7dd3fc'}}>sort</span>{' '}
-          <span style={{color:'#666'}}>|</span>{' '}
-          <span style={{color:'#7dd3fc'}}>uniq -c</span>{' '}
-          <span style={{color:'#666'}}>|</span>{' '}
-          <span style={{color:'#7dd3fc'}}>sort -rn</span>{' '}
-          <span style={{color:'#666'}}>|</span>{' '}
-          <span style={{color:'#7dd3fc'}}>head -20</span>
-        </div>
-      </div>
+    <div class="title-line" id="titleLine">w1 · 品牌协议</div>
 
-      {/* Results + swatches */}
-      <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap: 60,
-        flex: 1}}>
-        {/* Left: result list */}
-        <div style={{fontFamily: mono, fontSize: 18, lineHeight: 2}}>
-          <div style={{color:'#555', fontSize: 10, letterSpacing:'0.2em',
-            marginBottom: 12}}>▸ RESULT · top 5</div>
-          {results.map((r, i) => {
-            const visible = elapsed > (rowStart + i * rowStep);
-            if (!visible) return <div key={i} style={{height: 36}}/>;
-            const fadeIn = interpolate(elapsed,
-              [rowStart + i*rowStep, rowStart + i*rowStep + 0.3],
-              [0, 1]);
-            return (
-              <div key={i} style={{opacity: fadeIn, display:'flex',
-                alignItems:'center', gap: 20}}>
-                <span style={{color:'#fbbf24', minWidth: 40, textAlign:'right'}}>
-                  {String(r.count).padStart(3, ' ').replace(/ /g, '\u00A0')}
-                </span>
-                <span style={{color:'#e8e3d6'}}>{r.hex}</span>
-                <span style={{color:'#666', fontSize: 14}}>←</span>
-                <span style={{color:'#8a8878', fontStyle:'italic',
-                  fontFamily: serif, fontSize: 16}}>{r.label}</span>
-              </div>
-            );
-          })}
-        </div>
+    <div class="chain">
+      <div class="chain-line" id="chainLine"></div>
 
-        {/* Right: color swatches */}
-        <div style={{display:'flex', flexDirection:'column', gap: 14}}>
-          <div style={{color:'#555', fontFamily: mono, fontSize: 10,
-            letterSpacing:'0.2em', marginBottom: 2}}>▸ PALETTE</div>
-          {results.map((r, i) => {
-            const visible = elapsed > (rowStart + i * rowStep);
-            if (!visible) return <div key={i} style={{height: 54}}/>;
-            const fadeIn = interpolate(elapsed,
-              [rowStart + i*rowStep, rowStart + i*rowStep + 0.3],
-              [0, 1]);
-            const w = interpolate(elapsed,
-              [rowStart + i*rowStep, rowStart + i*rowStep + 0.5],
-              [0, r.count * 9]);
-            return (
-              <div key={i} style={{opacity: fadeIn, display:'flex',
-                alignItems:'center', gap: 14, height: 50}}>
-                <div style={{width: 50, height: 50, background: r.hex,
-                  border:'1px solid #333', flexShrink: 0}} />
-                <div style={{height: 24, background: r.hex,
-                  width: `${w}px`, opacity: 0.7}} />
-                <div style={{fontFamily: mono, fontSize: 12,
-                  color:'#8a8878'}}>×{r.count}</div>
-              </div>
-            );
-          })}
+      <div class="card" data-step="1">
+        <div class="card-num">STEP 01</div>
+        <div class="card-glyph"><div class="g-ask">?</div></div>
+        <div class="card-label">
+          <div class="zh">问</div>
+          <div class="en">Ask</div>
         </div>
       </div>
 
-      {/* Key caption */}
-      <div style={{opacity: captionOp, textAlign:'center', marginTop: 18,
-        paddingTop: 18, borderTop:'1px solid #333'}}>
-        <span style={{fontFamily: serif, fontStyle:'italic', fontSize: 26,
-          color: TERRA}}>
-          不要凭记忆猜品牌色
-        </span>
-        <span style={{fontFamily: serif, fontSize: 20, color:'#8a8878',
-          marginLeft: 14}}>
-          ——频次说了算
-        </span>
-      </div>
-    </div>
-  );
-}
-
-// ── Scene 5: Step 5 · brand-spec.md (17 – 22s) ────────────
-function Scene5_SpecFile() {
-  const { elapsed } = useSprite();
-  const headOp = interpolate(elapsed, [0, 0.4], [0, 1]);
-  const mdOp = interpolate(elapsed, [0.3, 0.9], [0, 1]);
-  const mdX = interpolate(elapsed, [0.3, 0.9], [-40, 0], Easing.easeOut);
-  const cssOp = interpolate(elapsed, [0.9, 1.5], [0, 1]);
-  const cssX = interpolate(elapsed, [0.9, 1.5], [40, 0], Easing.easeOut);
-  const arrowOp = interpolate(elapsed, [2.2, 2.7], [0, 1]);
-  const captionOp = interpolate(elapsed, [3.4, 3.9], [0, 1]);
-  const fadeOut = interpolate(elapsed, [4.5, 5], [1, 0]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
-      padding:'64px 80px 48px', display:'flex', flexDirection:'column'}}>
-      <div style={{opacity: headOp, marginBottom: 28, display:'flex',
-        justifyContent:'space-between', alignItems:'baseline'}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-            letterSpacing:'0.3em', marginBottom: 4}}>STEP · 05 · FINAL</div>
-          <div style={{fontFamily: serif, fontSize: 54, fontWeight: 500,
-            color: INK, letterSpacing:'-0.01em'}}>
-            固化为 <span style={{fontStyle:'italic'}}>brand-spec.md</span>
+      <div class="card" data-step="2">
+        <div class="card-num">STEP 02</div>
+        <div class="card-glyph">
+          <div class="g-search">
+            <div class="ring"></div>
+            <div class="handle"></div>
+            <div class="dot"></div>
           </div>
         </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
-          color: ASH, textAlign:'right'}}>
-          单一真相源 · single source of truth
+        <div class="card-label">
+          <div class="zh">搜</div>
+          <div class="en">Search</div>
         </div>
       </div>
 
-      <div style={{display:'grid', gridTemplateColumns:'1.1fr 0.9fr',
-        gap: 40, flex:1, position:'relative'}}>
-        {/* Left: .md preview */}
-        <div style={{opacity: mdOp, transform:`translateX(${mdX}px)`,
-          background:'#fff', border:`1px solid ${LINE}`,
-          display:'flex', flexDirection:'column'}}>
-          <div style={{padding:'12px 20px', borderBottom:`1px solid ${LINE}`,
-            display:'flex', justifyContent:'space-between', alignItems:'center',
-            background:'#f7f2e8'}}>
-            <div style={{fontFamily: mono, fontSize: 11, color: ASH,
-              letterSpacing:'0.15em'}}>
-              ▸ brand-spec.md
-            </div>
-            <div style={{fontFamily: mono, fontSize: 10, color: TERRA,
-              letterSpacing:'0.15em'}}>
-              ✓ COMMITTED
-            </div>
-          </div>
-          <div style={{padding:'22px 28px', fontFamily: mono, fontSize: 12,
-            lineHeight: 1.75, color: INK, flex:1, overflow:'hidden'}}>
-            <div style={{color: ASH}}>---</div>
-            <div><span style={{color: OLIVE}}>brand</span>: Kimi</div>
-            <div><span style={{color: OLIVE}}>fetched</span>: 2026-04-20</div>
-            <div style={{color: ASH}}>---</div>
-            <div style={{height: 10}} />
-            <div style={{fontFamily: serif, fontSize: 20, fontWeight: 500,
-              marginBottom: 4}}>## 色板</div>
-            <div>- primary: <span style={{color: TERRA}}>#1783FF</span> (47)</div>
-            <div>- bg:      <span style={{color: TERRA}}>#FAFAFA</span> (32)</div>
-            <div>- ink:     <span style={{color: TERRA}}>#1a1a1a</span> (18)</div>
-            <div>- accent:  <span style={{color: TERRA}}>#FF6B35</span> (12)</div>
-            <div style={{height: 12}} />
-            <div style={{fontFamily: serif, fontSize: 20, fontWeight: 500,
-              marginBottom: 4}}>## 字型</div>
-            <div>- display: <span style={{color: OLIVE}}>Newsreader</span></div>
-            <div>- body:    <span style={{color: OLIVE}}>Inter</span></div>
-            <div>- mono:    <span style={{color: OLIVE}}>JetBrains Mono</span></div>
-            <div style={{height: 12}} />
-            <div style={{fontFamily: serif, fontSize: 20, fontWeight: 500,
-              marginBottom: 4}}>## 签名细节</div>
-            <div>- 1px hairlines</div>
-            <div>- 圆角半径统一 4px</div>
-            <div style={{height: 12}} />
-            <div style={{fontFamily: serif, fontSize: 20, fontWeight: 500,
-              marginBottom: 4, color: TERRA}}>## 禁区</div>
-            <div style={{color: ASH}}>- 不用紫渐变 / emoji / 4px 以上 shadow</div>
+      <div class="card" data-step="3">
+        <div class="card-num">STEP 03</div>
+        <div class="card-glyph">
+          <div class="g-grab">
+            <div class="arrow"></div>
+            <div class="tray"></div>
           </div>
         </div>
+        <div class="card-label">
+          <div class="zh">下</div>
+          <div class="en">Grab</div>
+        </div>
+      </div>
 
-        {/* Right: CSS vars + consumers */}
-        <div style={{opacity: cssOp, transform:`translateX(${cssX}px)`,
-          display:'flex', flexDirection:'column', gap: 20}}>
-          <div style={{background:'#0e0e0e', color:'#e8e3d6',
-            fontFamily: mono, fontSize: 14, padding:'22px 26px',
-            lineHeight: 1.7, flex: 1}}>
-            <div style={{color:'#666', fontSize: 10, letterSpacing:'0.2em',
-              marginBottom: 12}}>/* tokens.css */</div>
-            <div><span style={{color:'#7dd3fc'}}>:root</span> {'{'}</div>
-            <div>  <span style={{color:'#a5f3c5'}}>--brand-primary</span>: <span style={{color:'#fbbf24'}}>#1783FF</span>;</div>
-            <div>  <span style={{color:'#a5f3c5'}}>--brand-bg</span>:      <span style={{color:'#fbbf24'}}>#FAFAFA</span>;</div>
-            <div>  <span style={{color:'#a5f3c5'}}>--brand-ink</span>:     <span style={{color:'#fbbf24'}}>#1a1a1a</span>;</div>
-            <div>  <span style={{color:'#a5f3c5'}}>--brand-accent</span>:  <span style={{color:'#fbbf24'}}>#FF6B35</span>;</div>
-            <div>{'}'}</div>
+      <div class="card" data-step="4">
+        <div class="card-num">STEP 04</div>
+        <div class="card-glyph">
+          <div class="g-grep">
+            <div class="line">#F5F4F0</div>
+            <div class="line"><span class="hit">#D97757</span></div>
+            <div class="line">#1A1918</div>
+            <div class="line">#FFFFFF</div>
           </div>
+        </div>
+        <div class="card-label">
+          <div class="zh">grep</div>
+          <div class="en">Extract</div>
+        </div>
+      </div>
 
-          {/* Arrow cascade */}
-          <div style={{opacity: arrowOp, position:'relative',
-            background:'#fff', border:`1px solid ${LINE}`,
-            padding:'20px 24px'}}>
-            <div style={{fontFamily: mono, fontSize: 10, color: TERRA,
-              letterSpacing:'0.2em', marginBottom: 12}}>▸ CONSUMERS</div>
-            <div style={{display:'flex', alignItems:'center', gap: 16,
-              fontFamily: mono, fontSize: 13, color: INK, flexWrap:'wrap'}}>
-              <span style={{padding:'4px 10px', border:`1px solid ${TERRA}`,
-                color: TERRA}}>brand-spec.md</span>
-              <span style={{color: TERRA}}>→</span>
-              <span style={{padding:'4px 10px', border:`1px solid ${INK}`}}>:root</span>
-              <span style={{color: TERRA}}>→</span>
-              <span style={{padding:'4px 10px', background: INK, color:'#fff'}}>all .html</span>
-            </div>
-            <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 15,
-              color: ASH, marginTop: 14, lineHeight: 1.5}}>
-              一个文件改动,所有 HTML 自动跟随。
-            </div>
+      <div class="card" data-step="5">
+        <div class="card-num">STEP 05</div>
+        <div class="card-glyph">
+          <div class="g-lock">
+            <div class="file"></div>
+            <div class="fold"></div>
+            <div class="row r1"></div>
+            <div class="row r2"></div>
+            <div class="row r3"></div>
+            <div class="row r4"></div>
+            <div class="row r5"></div>
           </div>
         </div>
+        <div class="card-label">
+          <div class="zh">定</div>
+          <div class="en">Lock</div>
+        </div>
       </div>
+    </div>
 
-      <div style={{opacity: captionOp, marginTop: 22, textAlign:'center',
-        fontFamily: serif, fontStyle:'italic', fontSize: 22, color: INK}}>
-        全协议的<span style={{color: TERRA}}>制胜点</span>——让所有 CSS 引用这份文件
+    <div class="final-file" id="finalFile">
+      <div class="file-name">brand-spec.md</div>
+      <div class="h1">资产已固化<span class="caret"></span></div>
+      <div class="kv">
+        <div><span class="k">logo</span> · assets/logo.svg</div>
+        <div><span class="k">hero</span> · product-hero.png</div>
+        <div><span class="k">accent</span> · <span class="swatch" style="background:#D97757"></span>#D97757</div>
+        <div><span class="k">bg</span> · <span class="swatch" style="background:#000;border:1px solid rgba(0,0,0,0.15)"></span>#000000</div>
       </div>
     </div>
-  );
-}
-
-// ── Scene 6: Final (22 – 24s) ─────────────────────────────
-function Scene6_Final() {
-  const { elapsed } = useSprite();
-  const fadeIn = interpolate(elapsed, [0, 0.5], [0, 1], Easing.easeOut);
-  const titleY = interpolate(elapsed, [0, 0.8], [24, 0], Easing.easeOut);
-  const lineW = interpolate(elapsed, [0.5, 1.3], [0, 580]);
-  const subOp = interpolate(elapsed, [0.8, 1.4], [0, 1]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeIn,
-      display:'flex', alignItems:'center', justifyContent:'center',
-      flexDirection:'column'}}>
-      <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
-        color: TERRA, marginBottom: 24}}>
-        01 → 02 → 03 → 04 → <span style={{color: INK}}>05 · DONE</span>
-      </div>
 
-      <div style={{fontFamily: serif, fontSize: 92, fontWeight: 500,
-        color: INK, lineHeight: 1.1, letterSpacing:'-0.01em',
-        textAlign:'center', transform: `translateY(${titleY}px)`}}>
-        <span style={{fontStyle:'italic', color: TERRA}}>15 分钟</span>的投资<br/>
-        省下 <span style={{fontStyle:'italic'}}>1–2 小时</span> 返工
-      </div>
+    <div class="brand-sheet" id="brandSheet"></div>
+    <div class="brand-reveal" id="brandReveal">
+      <div class="wordmark">huashu<span class="accent"> · </span>design</div>
+      <div class="underline" id="brandUnderline"></div>
+    </div>
+  </div>
+
+<script>
+  // ── Auto-scale stage to viewport ─────────────────
+  function fitStage() {
+    const stage = document.getElementById('stage');
+    const sx = window.innerWidth / 1920;
+    const sy = window.innerHeight / 1080;
+    const s = Math.min(sx, sy);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  fitStage();
+  window.addEventListener('resize', fitStage);
+
+  // ── Easing functions ─────────────────
+  const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
+  const expoIn  = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
+  const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
+  const cubicOut = t => 1 - Math.pow(1 - t, 3);
+
+  function lerp(t, a, b, easing) {
+    if (t <= 0) return a;
+    if (t >= 1) return b;
+    const e = easing ? easing(t) : t;
+    return a + (b - a) * e;
+  }
+  function seg(time, start, end) {
+    if (time <= start) return 0;
+    if (time >= end) return 1;
+    return (time - start) / (end - start);
+  }
 
-      <div style={{height:1, background: INK, width: lineW, marginTop: 40}} />
+  // ── Timeline (total 12s) ─────────────────
+  // Beat 1 (0-2s) · Beat 2 (2-10s) · Beat 3 (10-12s)
+  //
+  // Card schedule:
+  //   Card 1 enter 0.8-1.6s, active 1.6-3.0
+  //   Card 2 enter 2.4-3.2s, active 3.2-4.6
+  //   Card 3 enter 4.0-4.8s, active 4.8-6.2
+  //   Card 4 enter 5.6-6.4s, active 6.4-7.8
+  //   Card 5 enter 7.2-8.0s, active 8.0-9.4
+  //   All cards stay visible (frozen after active ends)
+  //
+  // Line draws 0.6-8.0s (while cards come in)
+  // Title fades in 0.2-1.2, fades out 9.6-10.0
+  // Final file: 8.8-9.8 scale in, hold to 10.0
+  // Brand reveal: 10.0-12.0
+
+  const cards = Array.from(document.querySelectorAll('.card'));
+  const cardTimings = [
+    { enter: [0.8, 1.6], active: [1.6, 3.0] },
+    { enter: [2.4, 3.2], active: [3.2, 4.6] },
+    { enter: [4.0, 4.8], active: [4.8, 6.2] },
+    { enter: [5.6, 6.4], active: [6.4, 7.8] },
+    { enter: [7.2, 8.0], active: [8.0, 9.4] },
+  ];
 
-      <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 24,
-        color: ASH, marginTop: 28, opacity: subOp,
-        letterSpacing:'0.02em'}}>
-        这是稳定性最便宜的投资。
-      </div>
-    </div>
-  );
-}
-
-// ── Watermark ─────────────────────────────────────────────
-function Watermark() {
-  return (
-    <div style={{position:'absolute', bottom: 24, right: 32,
-      fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
-      fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
-      Created by Huashu-Design
-    </div>
-  );
-}
-
-// ── Main composition ──────────────────────────────────────
-function App() {
-  return (
-    <Stage duration={24} width={1920} height={1080} bgColor={CREAM}>
-      <Sprite start={0} end={3}><Scene1_Trigger /></Sprite>
-      <Sprite start={3} end={7}><Scene2_AskAndSearch /></Sprite>
-      <Sprite start={7} end={12}><Scene3_Fallbacks /></Sprite>
-      <Sprite start={12} end={17}><Scene4_GrepColors /></Sprite>
-      <Sprite start={17} end={22}><Scene5_SpecFile /></Sprite>
-      <Sprite start={22} end={24}><Scene6_Final /></Sprite>
-      <Watermark />
-    </Stage>
-  );
-}
-
-ReactDOM.createRoot(document.getElementById('root')).render(<App />);
+  const titleLine = document.getElementById('titleLine');
+  const chainLine = document.getElementById('chainLine');
+  const finalFile = document.getElementById('finalFile');
+  const brandSheet = document.getElementById('brandSheet');
+  const brandReveal = document.getElementById('brandReveal');
+  const brandUnderline = document.getElementById('brandUnderline');
+
+  const DURATION = 12.0;
+  let startTime = null;
+  let loop = true;
+
+  // Honor recording flag
+  if (window.__recording === true) loop = false;
+
+  function tick(now) {
+    if (startTime === null) startTime = now;
+    let t = (now - startTime) / 1000;
+
+    if (t >= DURATION) {
+      if (loop) { startTime = now; t = 0; }
+      else { t = DURATION; }
+    }
+
+    // Title
+    const titleIn = seg(t, 0.2, 1.2);
+    const titleOut = seg(t, 9.6, 10.0);
+    const titleOpacity = Math.min(cubicOut(titleIn), 1 - titleOut);
+    titleLine.style.opacity = Math.max(0, titleOpacity);
+    titleLine.style.transform = `translateX(-50%) translateY(${lerp(titleIn, -8, 0, cubicOut)}px)`;
+
+    // Chain line — grows left→right as cards arrive
+    const lineT = seg(t, 0.6, 8.0);
+    chainLine.style.transform = `scaleX(${cubicInOut(lineT)})`;
+
+    // Cards
+    cards.forEach((card, i) => {
+      const { enter, active } = cardTimings[i];
+      const enterT = seg(t, enter[0], enter[1]);
+
+      const baseOp = expoOut(enterT);
+      const ty = lerp(enterT, 20, 0, expoOut);
+
+      // Active state during the card's "spotlight" window
+      const isActive = t >= active[0] && t <= active[1];
+      card.classList.toggle('active', isActive);
+
+      // Cards dim to 25% when final file starts zooming in (8.8-9.6),
+      // then fade fully when brand reveal takes over (10.0-10.4)
+      const dimT = seg(t, 8.8, 9.6);
+      const exitT = seg(t, 10.0, 10.4);
+      const dimFactor = lerp(dimT, 1.0, 0.22, cubicInOut);
+      const finalOp = baseOp * dimFactor * (1 - exitT);
+
+      if (dimT > 0) card.classList.remove('active');
+
+      card.style.opacity = finalOp;
+      card.style.transform = `translateY(${ty - 10 * exitT}px)`;
+    });
+
+    // Chain line also dims when final file zooms, fades with cards at 10.0-10.4
+    const chainDim = seg(t, 8.8, 9.6);
+    const chainExit = seg(t, 10.0, 10.4);
+    chainLine.style.opacity = lerp(chainDim, 1, 0.22, cubicInOut) * (1 - chainExit);
+
+    // Final file: 8.8-9.8 scale+fade in, then 9.8-10.2 scale+settle, hold to ~10.0
+    const finalInT = seg(t, 8.8, 9.8);
+    const finalScale = lerp(finalInT, 0.88, 1.0, expoOut);
+    const finalOp = cubicOut(finalInT);
+    // fade final file out into brand reveal
+    const finalOut = seg(t, 10.0, 10.6);
+    finalFile.style.opacity = finalOp * (1 - finalOut);
+    finalFile.style.transform = `translate(-50%, -50%) scale(${finalScale * (1 - finalOut * 0.04)})`;
+
+    // Brand reveal — sheet slides up from bottom 10.0-10.6, wordmark fades in 10.6-11.4, underline 11.4-11.9
+    const sheetT = seg(t, 10.0, 10.6);
+    brandSheet.style.transform = `translateY(${lerp(sheetT, 100, 0, expoOut)}%)`;
+
+    const wordT = seg(t, 10.6, 11.4);
+    brandReveal.style.opacity = cubicOut(wordT);
+    // NOTE: no scale transform on .brand-reveal — it would compound with the
+    // underline width animation and make the line appear mis-placed. Instead,
+    // scale the wordmark alone via font-variation-settings-safe approach: none here.
+
+    const underT = seg(t, 11.4, 11.9);
+    brandUnderline.style.width = `${lerp(underT, 0, 280, expoOut)}px`;
+
+    // Mark as ready for recorder on first frame
+    if (!window.__ready) window.__ready = true;
+
+    if (loop || t < DURATION) requestAnimationFrame(tick);
+  }
+  // Wait for fonts before first paint so Serif glyphs are correct
+  (document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve())
+    .then(() => requestAnimationFrame(tick));
 </script>
 </body>
 </html>

+ 983 - 0
demos/w2-junior-designer-en.html

@@ -0,0 +1,983 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8" />
+<title>w2 · Rough draft now beats perfect draft later</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@200;300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<style>
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --dim: rgba(255,255,255,0.18);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;
+    --accent-deep: #B85D3D;
+    --bad: #6E3A2E;           /* 失败暗红调,不刺眼 */
+    --bad-strong: #C85A42;    /* 失败叉号强调,对比度提升 */
+    --cool: rgba(255,255,255,0.42); /* 冷色参考线(左路径) */
+    --cd-bg: #F5F4F0;
+    --cd-panel: #FFFFFF;
+    --cd-ink: #1A1918;
+
+    --serif-zh: "Noto Serif SC", "Songti SC", serif;
+    --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
+    --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
+    -webkit-font-smoothing: antialiased;
+  }
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+  }
+
+  /* Film grain */
+  .stage::before {
+    content: '';
+    position: absolute;
+    inset: 0;
+    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
+    opacity: 0.02;
+    pointer-events: none;
+    z-index: 100;
+  }
+
+  /* Chrome · watermark */
+  .mark {
+    position: absolute;
+    top: 48px; left: 64px;
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,1);
+    opacity: 0.16;
+    pointer-events: none;
+    z-index: 50;
+  }
+  .mark-right {
+    position: absolute;
+    top: 48px; right: 64px;
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,1);
+    opacity: 0.16;
+    pointer-events: none;
+    z-index: 50;
+  }
+
+  /* Title */
+  .title-line {
+    position: absolute;
+    top: 112px;
+    left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--mono);
+    font-size: 14px;
+    letter-spacing: 0.28em;
+    color: var(--muted);
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity;
+    white-space: nowrap;
+  }
+
+  /* Splitter — horizontal line dividing the two halves */
+  .splitter {
+    position: absolute;
+    left: 160px;
+    right: 160px;
+    top: 50%;
+    height: 1px;
+    background: var(--hairline);
+    transform: scaleX(0);
+    transform-origin: left center;
+    will-change: transform;
+    z-index: 5;
+  }
+  .splitter-label {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    background: var(--bg);
+    padding: 0 28px;
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.32em;
+    color: var(--muted);
+    z-index: 6;
+    opacity: 0;
+    will-change: opacity;
+    white-space: nowrap;
+  }
+
+  /* ======================================================
+   * TOP HALF · 闷头一把梭(3 hours, all at once)
+   * ====================================================== */
+  .half-top {
+    position: absolute;
+    top: 200px;
+    left: 160px;
+    right: 160px;
+    height: 300px;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .half-label {
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.24em;
+    color: var(--muted);
+    text-transform: uppercase;
+    margin-bottom: 24px;
+    display: flex;
+    align-items: center;
+    gap: 12px;
+  }
+  .half-label .tag {
+    padding: 3px 10px;
+    border: 1px solid var(--hairline);
+    border-radius: 2px;
+    color: var(--ink-60);
+  }
+  .half-top .half-label .tag { border-color: rgba(160,74,56,0.4); color: rgba(200,120,100,0.85); }
+  .half-label .zh {
+    font-family: var(--serif-zh);
+    font-size: 22px;
+    font-weight: 400;
+    letter-spacing: 0.02em;
+    color: var(--ink-80);
+    margin-left: 4px;
+  }
+
+  /* Single huge terminal panel */
+  .terminal-big {
+    width: 100%;
+    height: 200px;
+    background: rgba(20, 20, 20, 1);
+    border: 1px solid var(--hairline);
+    border-radius: 10px;
+    overflow: hidden;
+    box-shadow:
+      0 0 0 1px rgba(255,255,255,0.02),
+      0 40px 80px -30px rgba(0,0,0,0.7);
+    position: relative;
+  }
+  .tty-head {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 14px 18px;
+    border-bottom: 1px solid var(--hairline);
+    background: rgba(255,255,255,0.02);
+  }
+  .tty-head .d {
+    width: 10px; height: 10px; border-radius: 50%;
+    background: var(--hairline);
+  }
+  .tty-title {
+    margin-left: 14px;
+    color: var(--muted);
+    font-size: 12px;
+    font-family: var(--mono);
+    letter-spacing: 0.04em;
+  }
+  .tty-body {
+    padding: 28px 30px;
+    font-family: var(--mono);
+    font-size: 17px;
+    line-height: 1.6;
+    color: rgba(255,255,255,0.86);
+  }
+  .tty-body .line {
+    opacity: 0;
+    will-change: opacity;
+  }
+  .tty-body .prompt { color: var(--accent); margin-right: 10px; }
+  .tty-body .dim { color: var(--muted); }
+
+  /* The long running progress bar (simulated "3-hour render") */
+  .progress-row {
+    margin-top: 14px;
+    display: flex;
+    align-items: center;
+    gap: 14px;
+    font-family: var(--mono);
+    font-size: 14px;
+    color: var(--ink-60);
+    opacity: 0;
+    will-change: opacity;
+  }
+  .progress-bar {
+    flex: 1;
+    height: 4px;
+    background: var(--hairline);
+    border-radius: 2px;
+    position: relative;
+    overflow: hidden;
+  }
+  .progress-bar-fill {
+    position: absolute;
+    top: 0; left: 0;
+    height: 100%;
+    background: var(--accent);
+    width: 0%;
+    will-change: width, background;
+  }
+  .progress-bar.failed .progress-bar-fill {
+    background: var(--bad-strong);
+  }
+  .progress-pct {
+    font-variant-numeric: tabular-nums;
+    letter-spacing: 0.04em;
+    min-width: 54px;
+    text-align: right;
+  }
+  .progress-hours {
+    color: var(--muted);
+    font-size: 12px;
+    letter-spacing: 0.12em;
+  }
+  .progress-row.failed {
+    color: var(--bad-strong);
+  }
+
+  /* Big X overlay for failure stamp */
+  .fail-stamp {
+    position: absolute;
+    right: 32px;
+    top: 50%;
+    transform: translateY(-50%) rotate(-8deg);
+    width: 120px; height: 120px;
+    pointer-events: none;
+    opacity: 0;
+    will-change: opacity, transform;
+    z-index: 10;
+  }
+  .fail-stamp svg { width: 100%; height: 100%; }
+  .fail-stamp .stamp-text {
+    position: absolute;
+    bottom: -22px;
+    left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.32em;
+    color: var(--bad-strong);
+    white-space: nowrap;
+  }
+
+  /* ======================================================
+   * BOTTOM HALF · 尽早 show(small iterations)
+   * ====================================================== */
+  .half-bot {
+    position: absolute;
+    top: 580px;
+    left: 160px;
+    right: 160px;
+    height: 340px;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .half-bot .half-label .tag {
+    border-color: rgba(217,119,87,0.35);
+    color: var(--accent);
+  }
+
+  .iter-row {
+    display: flex;
+    gap: 32px;
+    align-items: flex-end;
+    height: 240px;
+    margin-top: 12px;
+  }
+
+  .iter-panel {
+    flex: 1;
+    background: rgba(20, 20, 20, 1);
+    border: 1px solid var(--hairline);
+    border-radius: 8px;
+    overflow: hidden;
+    height: 100%;
+    position: relative;
+    opacity: 0;
+    transform: translateY(20px);
+    will-change: opacity, transform;
+    display: flex;
+    flex-direction: column;
+  }
+  .iter-panel .ip-head {
+    padding: 10px 14px;
+    border-bottom: 1px solid var(--hairline);
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.16em;
+    color: var(--muted);
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+  .iter-panel .ip-version {
+    color: var(--accent);
+    font-weight: 500;
+  }
+  .iter-panel .ip-body {
+    flex: 1;
+    padding: 16px 18px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    gap: 10px;
+  }
+  /* Rough mockup blocks that grow more detailed each iteration */
+  .iter-panel .m-block {
+    height: 8px;
+    background: var(--dim);
+    border-radius: 2px;
+    opacity: 0.8;
+  }
+  .iter-panel .m-block.accent { background: var(--accent); opacity: 0.8; }
+  .iter-panel .m-block.short { width: 40%; }
+  .iter-panel .m-block.med { width: 70%; }
+  .iter-panel .m-block.full { width: 100%; }
+  .iter-panel .m-block.tall { height: 24px; }
+  .iter-panel .m-block.big { height: 40px; }
+
+  .iter-panel .nod {
+    position: absolute;
+    top: 10px;
+    right: 14px;
+    width: 16px; height: 16px;
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+  .iter-panel .nod svg {
+    width: 100%; height: 100%;
+    stroke: var(--accent);
+    fill: none;
+    stroke-width: 2;
+  }
+  .iter-panel .ip-minutes {
+    position: absolute;
+    bottom: 10px;
+    left: 14px;
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.12em;
+    color: var(--muted);
+  }
+
+  /* Rising curve visualization for bottom half */
+  .curve-wrap {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    width: 340px;
+    height: 180px;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .curve-wrap svg {
+    width: 100%;
+    height: 100%;
+    overflow: visible;
+  }
+  .curve-wrap .axis {
+    stroke: var(--hairline);
+    stroke-width: 1;
+    fill: none;
+  }
+  .curve-wrap .curve-path {
+    stroke: var(--accent);
+    stroke-width: 2;
+    fill: none;
+    stroke-linecap: round;
+    stroke-linejoin: round;
+  }
+  .curve-wrap .curve-dot {
+    fill: var(--accent);
+    r: 3;
+  }
+  .curve-wrap .curve-label {
+    font-family: var(--mono);
+    font-size: 9px;
+    fill: var(--muted);
+    letter-spacing: 0.12em;
+  }
+
+  /* ======================================================
+   * BEAT 3 · Full comparison chart crossfade
+   * ====================================================== */
+  .final-chart {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    width: 1280px;
+    height: 620px;
+    opacity: 0;
+    will-change: opacity;
+    z-index: 60;
+  }
+  .final-chart svg {
+    width: 100%; height: 100%;
+    overflow: visible;
+  }
+  .final-chart .axis {
+    stroke: var(--hairline);
+    stroke-width: 1;
+    fill: none;
+  }
+  .final-chart .axis-label {
+    font-family: var(--mono);
+    font-size: 13px;
+    fill: var(--muted);
+    letter-spacing: 0.16em;
+  }
+  .final-chart .tick-label {
+    font-family: var(--mono);
+    font-size: 11px;
+    fill: var(--dim);
+    letter-spacing: 0.06em;
+  }
+  .final-chart .curve-a {
+    stroke: var(--cool);
+    stroke-width: 2;
+    fill: none;
+    stroke-linecap: round;
+    stroke-linejoin: round;
+  }
+  .final-chart .curve-a-dash {
+    stroke: var(--bad-strong);
+    stroke-width: 2.5;
+    fill: none;
+    stroke-dasharray: 5 7;
+    stroke-linecap: round;
+  }
+  .final-chart .curve-b {
+    stroke: var(--accent);
+    stroke-width: 3;
+    fill: none;
+    stroke-linecap: round;
+    stroke-linejoin: round;
+  }
+  .final-chart .curve-b-glow {
+    stroke: var(--accent);
+    stroke-width: 6;
+    fill: none;
+    opacity: 0.18;
+    stroke-linecap: round;
+    stroke-linejoin: round;
+  }
+  .final-chart .curve-dot {
+    fill: var(--accent);
+  }
+  .final-chart .fail-dot {
+    fill: none;
+    stroke: var(--bad-strong);
+    stroke-width: 2.5;
+  }
+  .final-chart .cool-dot {
+    fill: var(--cool);
+  }
+  .final-chart .anchor-label {
+    font-family: var(--serif-zh);
+    font-size: 20px;
+    font-weight: 400;
+    letter-spacing: 0.02em;
+  }
+  .final-chart .anchor-en {
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.18em;
+    text-transform: uppercase;
+  }
+
+  /* ======================================================
+   * BRAND REVEAL — 统一动作
+   * ====================================================== */
+  .brand-sheet {
+    position: absolute;
+    inset: 0;
+    background: var(--cd-bg);
+    transform: translateY(100%);
+    will-change: transform;
+    z-index: 80;
+  }
+  .brand-reveal {
+    position: absolute;
+    inset: 0;
+    z-index: 81;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .brand-reveal .wordmark {
+    font-family: var(--sans);
+    font-weight: 100;
+    font-size: 128px;
+    letter-spacing: -0.045em;
+    color: var(--cd-ink);
+    line-height: 1;
+  }
+  .brand-reveal .wordmark .accent { color: var(--accent-deep); }
+  .brand-reveal .underline {
+    width: 0;
+    height: 2px;
+    background: var(--accent);
+    margin-top: 36px;
+    will-change: width;
+  }
+</style>
+</head>
+<body>
+  <div class="stage" id="stage">
+    <div class="mark">HUASHU · DESIGN</div>
+    <div class="mark-right">V2 · 2026</div>
+
+    <div class="title-line" id="titleLine">w2 · rough draft now beats perfect draft later</div>
+
+    <!-- Splitter -->
+    <div class="splitter" id="splitter"></div>
+    <div class="splitter-label" id="splitterLabel">VS</div>
+
+    <!-- ============ TOP HALF: All-at-once ============ -->
+    <div class="half-top" id="halfTop">
+      <div class="half-label">
+        <span class="tag">A</span>
+        <span class="zh" style="font-family: var(--serif-en); font-style: italic; letter-spacing: 0.01em;">All-at-once</span>
+        <span style="font-family: var(--mono); color: var(--muted); letter-spacing: 0.18em; font-size: 11px; margin-left: auto;">3&nbsp;HOUR&nbsp;SESSION</span>
+      </div>
+      <div class="terminal-big">
+        <div class="tty-head">
+          <div class="d"></div><div class="d"></div><div class="d"></div>
+          <div class="tty-title">designer@studio · 3h session</div>
+        </div>
+        <div class="tty-body">
+          <div class="line" id="ttyL1"><span class="prompt">$</span>build final_design.html <span class="dim">// v1.0 · ship it all at once</span></div>
+          <div class="progress-row" id="progRow">
+            <div class="progress-bar" id="progBar">
+              <div class="progress-bar-fill" id="progFill"></div>
+            </div>
+            <span class="progress-pct" id="progPct">0%</span>
+            <span class="progress-hours" id="progHours">03:00:00</span>
+          </div>
+        </div>
+        <div class="fail-stamp" id="failStamp">
+          <svg viewBox="0 0 120 120">
+            <circle cx="60" cy="60" r="52" fill="none" stroke="#A04A38" stroke-width="3"/>
+            <path d="M 38 38 L 82 82 M 82 38 L 38 82" stroke="#A04A38" stroke-width="4" stroke-linecap="round"/>
+          </svg>
+          <div class="stamp-text">REJECTED</div>
+        </div>
+      </div>
+    </div>
+
+    <!-- ============ BOTTOM HALF: Show early ============ -->
+    <div class="half-bot" id="halfBot">
+      <div class="half-label">
+        <span class="tag">B</span>
+        <span class="zh" style="font-family: var(--serif-en); font-style: italic; letter-spacing: 0.01em;">Show early</span>
+        <span style="font-family: var(--mono); color: var(--muted); letter-spacing: 0.18em; font-size: 11px; margin-left: auto;">SMALL&nbsp;ITERATIONS</span>
+      </div>
+      <div class="iter-row">
+        <div class="iter-panel" id="iter1">
+          <div class="ip-head">
+            <span>draft · v1</span>
+            <span class="ip-version">15 min</span>
+          </div>
+          <div class="ip-body">
+            <div class="m-block short"></div>
+            <div class="m-block med"></div>
+            <div class="m-block short"></div>
+          </div>
+          <div class="nod" id="nod1">
+            <svg viewBox="0 0 16 16"><path d="M3 8 L7 12 L13 4"/></svg>
+          </div>
+        </div>
+        <div class="iter-panel" id="iter2">
+          <div class="ip-head">
+            <span>draft · v2</span>
+            <span class="ip-version">25 min</span>
+          </div>
+          <div class="ip-body">
+            <div class="m-block full tall"></div>
+            <div class="m-block med"></div>
+            <div class="m-block short"></div>
+            <div class="m-block med accent"></div>
+          </div>
+          <div class="nod" id="nod2">
+            <svg viewBox="0 0 16 16"><path d="M3 8 L7 12 L13 4"/></svg>
+          </div>
+        </div>
+        <div class="iter-panel" id="iter3">
+          <div class="ip-head">
+            <span>draft · v3</span>
+            <span class="ip-version">35 min</span>
+          </div>
+          <div class="ip-body">
+            <div class="m-block full big"></div>
+            <div class="m-block full tall accent"></div>
+            <div class="m-block med"></div>
+            <div class="m-block full"></div>
+            <div class="m-block short"></div>
+          </div>
+          <div class="nod" id="nod3">
+            <svg viewBox="0 0 16 16"><path d="M3 8 L7 12 L13 4"/></svg>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- ============ Beat 3 · Final comparison chart ============ -->
+    <div class="final-chart" id="finalChart">
+      <svg viewBox="0 0 1280 620" preserveAspectRatio="xMidYMid meet">
+        <!-- Axes -->
+        <line class="axis" x1="110" y1="60" x2="110" y2="520"/>
+        <line class="axis" x1="110" y1="520" x2="1200" y2="520"/>
+
+        <!-- Y-axis label -->
+        <text class="axis-label" x="58" y="290" transform="rotate(-90 58 290)" text-anchor="middle">QUALITY</text>
+        <!-- X-axis label -->
+        <text class="axis-label" x="655" y="570" text-anchor="middle">TIME</text>
+
+        <!-- Tick marks -->
+        <text class="tick-label" x="110" y="545" text-anchor="middle">0</text>
+        <text class="tick-label" x="290" y="545" text-anchor="middle">15m</text>
+        <text class="tick-label" x="480" y="545" text-anchor="middle">25m</text>
+        <text class="tick-label" x="680" y="545" text-anchor="middle">35m</text>
+        <text class="tick-label" x="1200" y="545" text-anchor="middle">3h</text>
+
+        <!-- Curve A (All-at-once): flat crawl near zero, late spike, then crash -->
+        <path class="curve-a" id="curveA"
+              d="M 110 500 L 400 495 L 700 490 L 1000 485 L 1140 180" />
+        <path class="curve-a-dash" id="curveACrash"
+              d="M 1140 180 L 1200 510" />
+        <circle class="fail-dot" id="failDot" cx="1140" cy="180" r="9"/>
+        <g id="failX" opacity="0">
+          <line x1="1130" y1="170" x2="1150" y2="190" stroke="#C85A42" stroke-width="2.5" stroke-linecap="round"/>
+          <line x1="1150" y1="170" x2="1130" y2="190" stroke="#C85A42" stroke-width="2.5" stroke-linecap="round"/>
+        </g>
+
+        <text class="anchor-label" x="1200" y="150" fill="#C85A42" text-anchor="end" style="font-family: var(--serif-en); font-style: italic;">All-at-once</text>
+        <text class="anchor-en" x="1200" y="170" fill="#C85A42" text-anchor="end">REJECTED</text>
+
+        <!-- Curve B (Show early): steady step rise across first 35 min -->
+        <path class="curve-b-glow" id="curveBGlow"
+              d="M 110 500 L 290 380 L 480 270 L 680 140" />
+        <path class="curve-b" id="curveB"
+              d="M 110 500 L 290 380 L 480 270 L 680 140" />
+        <circle class="curve-dot" cx="290" cy="380" r="6"/>
+        <circle class="curve-dot" cx="480" cy="270" r="6"/>
+        <circle class="curve-dot" cx="680" cy="140" r="8"/>
+
+        <text class="anchor-label" x="680" y="115" fill="#D97757" text-anchor="middle" style="font-family: var(--serif-en); font-style: italic;">Show early</text>
+        <text class="anchor-en" x="680" y="96" fill="#D97757" text-anchor="middle">SHIPPED</text>
+
+        <text class="tick-label" x="555" y="477" text-anchor="middle" fill="rgba(255,255,255,0.3)" style="letter-spacing: 0.12em;">— 3 hours silence —</text>
+      </svg>
+    </div>
+
+    <!-- Brand reveal -->
+    <div class="brand-sheet" id="brandSheet"></div>
+    <div class="brand-reveal" id="brandReveal">
+      <div class="wordmark">huashu<span class="accent"> · </span>design</div>
+      <div class="underline" id="brandUnderline"></div>
+    </div>
+  </div>
+
+<script>
+  // Auto-scale stage
+  function fitStage() {
+    const stage = document.getElementById('stage');
+    const sx = window.innerWidth / 1920;
+    const sy = window.innerHeight / 1080;
+    const s = Math.min(sx, sy);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  fitStage();
+  window.addEventListener('resize', fitStage);
+
+  // Easings
+  const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
+  const expoIn  = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
+  const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
+  const cubicOut = t => 1 - Math.pow(1 - t, 3);
+  const cubicIn  = t => t * t * t;
+
+  function lerp(t, a, b, easing) {
+    if (t <= 0) return a;
+    if (t >= 1) return b;
+    const e = easing ? easing(t) : t;
+    return a + (b - a) * e;
+  }
+  function seg(time, start, end) {
+    if (time <= start) return 0;
+    if (time >= end) return 1;
+    return (time - start) / (end - start);
+  }
+
+  // ────────────────────────────────────
+  // Timeline — total 12s (Beat 1: 0-2 · Beat 2: 2-10 · Beat 3: 10-12)
+  //
+  // 0.0-0.6    title + splitter grow
+  // 0.6-1.4    two half-labels fade in (top first, then bot)
+  // 1.4-2.0    top terminal line 1 types; bot panel 1 enters
+  //
+  // Top track (闷头):
+  //   2.0-7.8  progress bar crawls from 0 to 99% (slow, painful)
+  //   7.8-8.4  stuck at 99%
+  //   8.4-8.9  fail stamp lands + bar turns red + bar drops to 0
+  //
+  // Bottom track (尽早):
+  //   2.0-2.6  iter1 enters, nod1 appears @ 2.8
+  //   3.6-4.2  iter2 enters, nod2 appears @ 4.4
+  //   5.6-6.2  iter3 enters, nod3 appears @ 6.4 (final tick — biggest)
+  //
+  // 8.8-9.8    both halves dim; final chart crossfades in
+  //             (curves draw via stroke-dasharray)
+  // 9.8-10.4   chart settles, anchor labels bloom
+  // 10.0-12.0  brand reveal (sheet + wordmark + underline)
+  // ────────────────────────────────────
+
+  const el = {
+    title:      document.getElementById('titleLine'),
+    splitter:   document.getElementById('splitter'),
+    splitterLb: document.getElementById('splitterLabel'),
+    halfTop:    document.getElementById('halfTop'),
+    halfBot:    document.getElementById('halfBot'),
+    ttyL1:      document.getElementById('ttyL1'),
+    progRow:    document.getElementById('progRow'),
+    progBar:    document.getElementById('progBar'),
+    progFill:   document.getElementById('progFill'),
+    progPct:    document.getElementById('progPct'),
+    progHours:  document.getElementById('progHours'),
+    failStamp:  document.getElementById('failStamp'),
+    iter1:      document.getElementById('iter1'),
+    iter2:      document.getElementById('iter2'),
+    iter3:      document.getElementById('iter3'),
+    nod1:       document.getElementById('nod1'),
+    nod2:       document.getElementById('nod2'),
+    nod3:       document.getElementById('nod3'),
+    finalChart: document.getElementById('finalChart'),
+    brandSheet: document.getElementById('brandSheet'),
+    brandReveal:document.getElementById('brandReveal'),
+    brandUnder: document.getElementById('brandUnderline'),
+    curveA:     document.getElementById('curveA'),
+    curveACrash:document.getElementById('curveACrash'),
+    curveB:     document.getElementById('curveB'),
+    curveBGlow: document.getElementById('curveBGlow'),
+  };
+
+  // Precompute path lengths for draw-on animation
+  const lenA = el.curveA.getTotalLength();
+  const lenACrash = el.curveACrash.getTotalLength();
+  const lenB = el.curveB.getTotalLength();
+
+  el.curveA.style.strokeDasharray = `${lenA} ${lenA}`;
+  el.curveA.style.strokeDashoffset = lenA;
+  el.curveACrash.style.strokeDasharray = `${lenACrash} ${lenACrash}`;
+  el.curveACrash.style.strokeDashoffset = lenACrash;
+  el.curveB.style.strokeDasharray = `${lenB} ${lenB}`;
+  el.curveB.style.strokeDashoffset = lenB;
+  el.curveBGlow.style.strokeDasharray = `${lenB} ${lenB}`;
+  el.curveBGlow.style.strokeDashoffset = lenB;
+
+  // Also precompute chart dot selections (hide initially)
+  const chartDots = el.finalChart.querySelectorAll('circle');
+  const chartAnchors = el.finalChart.querySelectorAll('.anchor-label, .anchor-en');
+  const chartTicks = el.finalChart.querySelectorAll('.tick-label, .axis-label');
+
+  const DURATION = 12.0;
+  let startTime = null;
+  let loop = true;
+  if (window.__recording === true) loop = false;
+
+  function tick(now) {
+    if (startTime === null) startTime = now;
+    let t = (now - startTime) / 1000;
+
+    if (t >= DURATION) {
+      if (loop) { startTime = now; t = 0; }
+      else { t = DURATION; }
+    }
+
+    // ────── Title
+    const titleIn = seg(t, 0.1, 1.0);
+    const titleOut = seg(t, 9.2, 9.8);
+    el.title.style.opacity = Math.max(0, Math.min(cubicOut(titleIn), 1 - titleOut));
+
+    // ────── Splitter (fade out earlier so Beat 3 is clean)
+    const splitT = seg(t, 0.0, 0.8);
+    const splitOut = seg(t, 8.4, 8.9);
+    el.splitter.style.transform = `scaleX(${expoOut(splitT) * (1 - splitOut)})`;
+    const splitLabelT = seg(t, 0.4, 1.0);
+    const splitLabelOut = seg(t, 8.2, 8.7);
+    el.splitterLb.style.opacity = Math.max(0, Math.min(cubicOut(splitLabelT), 1 - splitLabelOut));
+
+    // ────── Halves fade in / out (fade out earlier to clear for Beat 3 chart)
+    const topIn = seg(t, 0.6, 1.4);
+    const topOut = seg(t, 8.4, 9.0);
+    el.halfTop.style.opacity = Math.max(0, Math.min(cubicOut(topIn), 1 - topOut));
+
+    const botIn = seg(t, 1.0, 1.8);
+    const botOut = seg(t, 8.4, 9.0);
+    el.halfBot.style.opacity = Math.max(0, Math.min(cubicOut(botIn), 1 - botOut));
+
+    // ────── TOP track: terminal line + progress bar
+    const ttyL1In = seg(t, 1.4, 1.8);
+    el.ttyL1.style.opacity = cubicOut(ttyL1In);
+
+    // Progress bar appears @ 1.8, starts crawling 2.0-7.8, stuck 7.8-8.4, fails @ 8.4
+    const progRowIn = seg(t, 1.8, 2.2);
+    el.progRow.style.opacity = cubicOut(progRowIn);
+
+    let pct = 0;
+    let hoursTxt = '03:00:00';
+    if (t >= 2.0 && t < 7.8) {
+      const p = seg(t, 2.0, 7.8);
+      // Easing: starts fast, slows down to 99% (mimics the "last 10% takes forever" trope)
+      pct = 99 * (1 - Math.pow(1 - p, 2.2));
+      const remaining = Math.max(0, (1 - p) * 3 * 60 * 60);
+      const hh = String(Math.floor(remaining / 3600)).padStart(2, '0');
+      const mm = String(Math.floor((remaining % 3600) / 60)).padStart(2, '0');
+      const ss = String(Math.floor(remaining % 60)).padStart(2, '0');
+      hoursTxt = `${hh}:${mm}:${ss}`;
+    } else if (t >= 7.8 && t < 8.4) {
+      pct = 99;
+      // Micro-jitter to show "stuck"
+      const jitter = Math.sin(t * 30) * 0.1;
+      pct = 99 + jitter;
+      hoursTxt = '00:00:12';
+    } else if (t >= 8.4 && t < 8.7) {
+      // Fail animation — pct stays at 99 briefly then snaps to 0
+      pct = 99;
+      hoursTxt = '— REJECTED —';
+    } else if (t >= 8.7) {
+      pct = 0;
+      hoursTxt = '— REJECTED —';
+    }
+
+    el.progFill.style.width = `${pct}%`;
+    el.progPct.textContent = `${Math.floor(Math.max(0, pct))}%`;
+    el.progHours.textContent = hoursTxt;
+
+    // Fail state toggle
+    if (t >= 8.4) {
+      el.progBar.classList.add('failed');
+      el.progRow.classList.add('failed');
+    } else {
+      el.progBar.classList.remove('failed');
+      el.progRow.classList.remove('failed');
+    }
+
+    // Fail stamp lands at 8.4
+    const stampIn = seg(t, 8.4, 8.7);
+    if (stampIn > 0) {
+      el.failStamp.style.opacity = cubicOut(stampIn);
+      const scale = lerp(stampIn, 1.6, 1.0, expoOut);
+      el.failStamp.style.transform = `translateY(-50%) rotate(-8deg) scale(${scale})`;
+    } else {
+      el.failStamp.style.opacity = 0;
+    }
+
+    // ────── BOTTOM track: 3 iter panels
+    const iterTimings = [
+      { enter: [2.0, 2.6], nod: [2.8, 3.2] },
+      { enter: [3.6, 4.2], nod: [4.4, 4.8] },
+      { enter: [5.6, 6.2], nod: [6.4, 6.9] },
+    ];
+
+    [el.iter1, el.iter2, el.iter3].forEach((panel, i) => {
+      const { enter } = iterTimings[i];
+      const p = seg(t, enter[0], enter[1]);
+      const op = expoOut(p);
+      const ty = lerp(p, 20, 0, expoOut);
+      panel.style.opacity = op;
+      panel.style.transform = `translateY(${ty}px)`;
+    });
+
+    [el.nod1, el.nod2, el.nod3].forEach((n, i) => {
+      const { nod } = iterTimings[i];
+      const p = seg(t, nod[0], nod[1]);
+      const op = expoOut(p);
+      const scale = lerp(p, 0.4, 1.0, expoOut);
+      n.style.opacity = op;
+      n.style.transform = `scale(${scale})`;
+    });
+
+    // ────── Beat 3 · final chart crossfade (chart appears as halves fade)
+    const chartIn = seg(t, 8.5, 9.2);
+    el.finalChart.style.opacity = cubicOut(chartIn);
+
+    const curveBT = seg(t, 8.8, 9.8);
+    el.curveB.style.strokeDashoffset = lenB * (1 - expoOut(curveBT));
+    el.curveBGlow.style.strokeDashoffset = lenB * (1 - expoOut(curveBT));
+
+    const curveAT = seg(t, 8.9, 9.7);
+    el.curveA.style.strokeDashoffset = lenA * (1 - cubicOut(curveAT));
+    const curveACrashT = seg(t, 9.7, 9.95);
+    el.curveACrash.style.strokeDashoffset = lenACrash * (1 - expoOut(curveACrashT));
+    const failXT = seg(t, 9.65, 9.85);
+    const failXEl = document.getElementById('failX');
+    if (failXEl) {
+      failXEl.style.opacity = cubicOut(failXT);
+      failXEl.style.transform = `scale(${lerp(failXT, 1.6, 1.0, expoOut)})`;
+      failXEl.style.transformOrigin = '1140px 180px';
+    }
+
+    chartDots.forEach((dot, i) => {
+      const dotT = seg(t, 9.0 + i * 0.12, 9.3 + i * 0.12);
+      dot.style.opacity = cubicOut(dotT);
+    });
+    chartAnchors.forEach((a) => {
+      const aT = seg(t, 9.5, 9.95);
+      a.style.opacity = cubicOut(aT);
+    });
+    chartTicks.forEach((tk) => {
+      const tkT = seg(t, 8.7, 9.3);
+      tk.style.opacity = cubicOut(tkT) * 0.9;
+    });
+
+    // ────── Brand reveal 10.0-12.0
+    const sheetT = seg(t, 10.0, 10.6);
+    el.brandSheet.style.transform = `translateY(${lerp(sheetT, 100, 0, expoOut)}%)`;
+
+    const wordT = seg(t, 10.6, 11.4);
+    el.brandReveal.style.opacity = cubicOut(wordT);
+
+    const underT = seg(t, 11.4, 11.9);
+    el.brandUnder.style.width = `${lerp(underT, 0, 280, expoOut)}px`;
+
+    // Mark ready for recorder
+    if (!window.__ready) window.__ready = true;
+
+    if (loop || t < DURATION) requestAnimationFrame(tick);
+  }
+
+  (document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve())
+    .then(() => requestAnimationFrame(tick));
+</script>
+</body>
+</html>

+ 947 - 809
demos/w2-junior-designer.html

@@ -1,856 +1,994 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
+<!doctype html>
+<html lang="zh-Hans">
 <head>
-<meta charset="UTF-8">
-<title>Huashu-Design · Junior Designer 模式</title>
-<script crossorigin src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
-<script crossorigin src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
-<script src="https://unpkg.com/@babel/standalone@7.25.6/babel.min.js"></script>
+<meta charset="utf-8" />
+<title>w2 · 粗糙的第一版,好过完美的大招</title>
 <link rel="preconnect" href="https://fonts.googleapis.com">
 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,300;1,6..72,400;1,6..72,500&family=Noto+Serif+SC:wght@300;400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Noto+Serif+SC:wght@200;300;400;500;600&family=Inter:wght@100;200;300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
 <style>
-  * { box-sizing: border-box; margin: 0; padding: 0; }
-  html, body { width: 100%; height: 100%; overflow: hidden; }
-  body {
-    background: #0c0c0c;
-    font-family: 'Newsreader', 'Noto Serif SC', Georgia, serif;
-    color: #1a1a1a;
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --dim: rgba(255,255,255,0.18);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;
+    --accent-deep: #B85D3D;
+    --bad: #6E3A2E;           /* 失败暗红调,不刺眼 */
+    --bad-strong: #C85A42;    /* 失败叉号强调,对比度提升 */
+    --cool: rgba(255,255,255,0.42); /* 冷色参考线(左路径) */
+    --cd-bg: #F5F4F0;
+    --cd-panel: #FFFFFF;
+    --cd-ink: #1A1918;
+
+    --serif-zh: "Noto Serif SC", "Songti SC", serif;
+    --serif-en: "Source Serif 4", "Tiempos Headline", Georgia, serif;
+    --sans: "Inter", -apple-system, "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
     -webkit-font-smoothing: antialiased;
-    text-rendering: optimizeLegibility;
   }
-</style>
-</head>
-<body>
-<div id="root"></div>
-
-<!-- animations.jsx inlined -->
-<script type="text/babel">
-(function() {
-  const { createContext, useContext, useState, useEffect, useRef, useCallback } = React;
-  const TimeContext = createContext({ time: 0, duration: 10, playing: false });
-  const SpriteContext = createContext(null);
-
-  const Easing = {
-    linear: t => t,
-    easeIn: t => t * t,
-    easeOut: t => 1 - (1 - t) * (1 - t),
-    easeInOut: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
-    spring: t => {
-      const c = (2 * Math.PI) / 3;
-      return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c) + 1;
-    },
-  };
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+  }
 
-  function interpolate(t, input, output, easing) {
-    const [inStart, inEnd] = input;
-    const [outStart, outEnd] = output;
-    if (t <= inStart) return outStart;
-    if (t >= inEnd) return outEnd;
-    let progress = (t - inStart) / (inEnd - inStart);
-    if (easing) progress = easing(progress);
-    return outStart + (outEnd - outStart) * progress;
-  }
-
-  function useTime() { return useContext(TimeContext).time; }
-  function useSprite() {
-    const sprite = useContext(SpriteContext);
-    return sprite || { t: 0, elapsed: 0, duration: 0 };
-  }
-
-  function Stage({ duration = 10, width = 1920, height = 1080, loop = true, children, bgColor = '#fff' }) {
-    const [time, setTime] = useState(0);
-    const [playing, setPlaying] = useState(true);
-    const [scale, setScale] = useState(1);
-    const rafRef = useRef(null);
-    const effectiveLoop = (typeof window !== 'undefined' && window.__recording) ? false : loop;
-
-    useEffect(() => {
-      function updateScale() {
-        const vw = window.innerWidth;
-        const vh = window.innerHeight - 56;
-        const s = Math.min(vw / width, vh / height);
-        setScale(s);
-      }
-      updateScale();
-      window.addEventListener('resize', updateScale);
-      return () => window.removeEventListener('resize', updateScale);
-    }, [width, height]);
-
-    useEffect(() => {
-      if (!playing) return;
-      let cancelled = false;
-      let last = null;
-      function tick(now) {
-        if (cancelled) return;
-        if (last === null) {
-          last = now;
-          if (typeof window !== 'undefined') window.__ready = true;
-        }
-        const delta = (now - last) / 1000;
-        last = now;
-        setTime(prev => {
-          const next = prev + delta;
-          if (next >= duration) return effectiveLoop ? 0 : duration - 0.001;
-          return next;
-        });
-        rafRef.current = requestAnimationFrame(tick);
-      }
-      const start = () => { if (!cancelled) rafRef.current = requestAnimationFrame(tick); };
-      if (document.fonts && document.fonts.ready) document.fonts.ready.then(start); else start();
-      return () => { cancelled = true; cancelAnimationFrame(rafRef.current); };
-    }, [playing, duration, effectiveLoop]);
-
-    const progress = time / duration;
-    const ctx = { time, duration, playing, setPlaying, setTime };
-
-    const canvasStyle = {
-      position: 'absolute',
-      top: '50%',
-      left: '50%',
-      transformOrigin: 'center center',
-      width,
-      height,
-      background: bgColor,
-      overflow: 'hidden',
-      transform: `translate(-50%, -50%) scale(${scale})`,
-    };
-
-    return (
-      <TimeContext.Provider value={ctx}>
-        <div style={{position:'fixed', inset:0, background:'#0c0c0c', display:'flex', flexDirection:'column'}}>
-          <div style={{flex:1, position:'relative', overflow:'hidden'}}>
-            <div style={canvasStyle}>{children}</div>
-          </div>
-          <div className="no-record" style={{position:'fixed', bottom:0, left:0, right:0, background:'rgba(0,0,0,0.8)', backdropFilter:'blur(10px)', padding:'12px 20px', display:'flex', alignItems:'center', gap:16, color:'#fff', fontSize:12, zIndex:100}}>
-            <button onClick={()=>setPlaying(p=>!p)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>{playing?'⏸ 暂停':'▶ 播放'}</button>
-            <button onClick={()=>setTime(0)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>⏮ 开始</button>
-            <div style={{fontFamily:'ui-monospace, monospace', fontVariantNumeric:'tabular-nums', minWidth:90}}>{time.toFixed(2)}s / {duration.toFixed(2)}s</div>
-            <div style={{flex:1, height:4, background:'rgba(255,255,255,0.2)', borderRadius:2, position:'relative'}}>
-              <div style={{position:'absolute', top:0, left:0, height:'100%', width:`${progress*100}%`, background:'#fff', borderRadius:2}} />
-            </div>
-          </div>
-        </div>
-      </TimeContext.Provider>
-    );
-  }
-
-  function Sprite({ start = 0, end, children, style }) {
-    const { time } = useContext(TimeContext);
-    const actualEnd = end == null ? Infinity : end;
-    if (time < start || time >= actualEnd) return null;
-    const duration = actualEnd - start;
-    const elapsed = time - start;
-    const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
-    const spriteValue = { t, elapsed, duration, start, end: actualEnd };
-    return (
-      <SpriteContext.Provider value={spriteValue}>
-        <div style={{position:'absolute', inset:0, ...style}}>{children}</div>
-      </SpriteContext.Provider>
-    );
-  }
-
-  window.Animations = { Stage, Sprite, useTime, useSprite, Easing, interpolate };
-})();
-</script>
+  /* Film grain */
+  .stage::before {
+    content: '';
+    position: absolute;
+    inset: 0;
+    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
+    opacity: 0.02;
+    pointer-events: none;
+    z-index: 100;
+  }
 
-<!-- Demo scene -->
-<script type="text/babel">
-const { Stage, Sprite, useTime, useSprite, Easing, interpolate } = window.Animations;
-
-// ── Design tokens ─────────────────────────────────────────
-const CREAM = '#FAF6EF';
-const INK = '#1a1a1a';
-const TERRA = '#C04A1A';
-const ASH = '#6b6b6b';
-const LINE = '#d9d2c5';
-const OLIVE = '#6a6b4e';
-
-const serif = "'Newsreader', 'Noto Serif SC', Georgia, serif";
-const sans = "'Inter', -apple-system, sans-serif";
-const mono = "'JetBrains Mono', ui-monospace, monospace";
-
-// ── Scene 1: Two working modes contrast (0 – 3s) ──────────
-function Scene1_Contrast() {
-  const { elapsed } = useSprite();
-  const titleOp = interpolate(elapsed, [0, 0.6], [0, 1]);
-  const leftOp = interpolate(elapsed, [0.3, 1], [0, 1]);
-  const rightOp = interpolate(elapsed, [0.5, 1.2], [0, 1]);
-
-  // Left: long bar filling slowly, then user frown at end
-  const badBar = Math.min(1, Math.max(0, (elapsed - 1.0) / 1.6));
-  const badReveal = elapsed > 2.5 ? 1 : 0;
-
-  // Right: small milestones ticking one by one
-  const ticks = [0.9, 1.3, 1.7, 2.1, 2.5];
-  const fadeOut = interpolate(elapsed, [2.85, 3], [1, 0], Easing.easeIn);
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
-      padding:'80px 120px', display:'flex', flexDirection:'column'}}>
-
-      {/* Title */}
-      <div style={{textAlign:'center', marginBottom: 60, opacity: titleOp}}>
-        <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
-          color: TERRA, marginBottom: 16}}>
-          JUNIOR DESIGNER MODE · 工作方式对比
-        </div>
-        <div style={{fontFamily: serif, fontSize: 68, fontWeight: 500,
-          color: INK, lineHeight: 1.1, letterSpacing:'-0.01em'}}>
-          理解错了 <span style={{fontStyle:'italic', color: TERRA}}>早改</span> 比 <span style={{fontStyle:'italic', color: TERRA}}>晚改</span> 便宜 100 倍
-        </div>
-      </div>
+  /* Chrome · watermark */
+  .mark {
+    position: absolute;
+    top: 48px; left: 64px;
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,1);
+    opacity: 0.16;
+    pointer-events: none;
+    z-index: 50;
+  }
+  .mark-right {
+    position: absolute;
+    top: 48px; right: 64px;
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,1);
+    opacity: 0.16;
+    pointer-events: none;
+    z-index: 50;
+  }
 
-      {/* Two columns */}
-      <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap: 60, flex: 1}}>
-
-        {/* Left: bad mode */}
-        <div style={{opacity: leftOp, background:'#f4eee3', padding:'40px 44px',
-          display:'flex', flexDirection:'column', position:'relative'}}>
-          <div style={{display:'flex', alignItems:'center', gap: 14, marginBottom: 28}}>
-            <div style={{width: 32, height: 32, borderRadius:'50%',
-              background:'#ccc', color:'#fff', fontFamily: serif, fontSize: 20,
-              fontWeight: 600, display:'flex', alignItems:'center',
-              justifyContent:'center', lineHeight: 1}}>✗</div>
-            <div style={{fontFamily: serif, fontSize: 30, fontWeight: 500,
-              color:'#555'}}>闷头做大招</div>
-          </div>
+  /* Title */
+  .title-line {
+    position: absolute;
+    top: 112px;
+    left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--mono);
+    font-size: 14px;
+    letter-spacing: 0.28em;
+    color: var(--muted);
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity;
+    white-space: nowrap;
+  }
 
-          <div style={{fontFamily: mono, fontSize: 11, color: ASH,
-            letterSpacing: '0.2em', marginBottom: 14}}>
-            6 HOURS · 0 FEEDBACK
-          </div>
+  /* Splitter — horizontal line dividing the two halves */
+  .splitter {
+    position: absolute;
+    left: 160px;
+    right: 160px;
+    top: 50%;
+    height: 1px;
+    background: var(--hairline);
+    transform: scaleX(0);
+    transform-origin: left center;
+    will-change: transform;
+    z-index: 5;
+  }
+  .splitter-label {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    background: var(--bg);
+    padding: 0 28px;
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.32em;
+    color: var(--muted);
+    z-index: 6;
+    opacity: 0;
+    will-change: opacity;
+    white-space: nowrap;
+  }
 
-          <div style={{height: 18, background:'#e2dbcd', position:'relative',
-            marginBottom: 36}}>
-            <div style={{position:'absolute', top:0, left:0, height:'100%',
-              width: `${badBar * 100}%`, background:'#999'}} />
-          </div>
+  /* ======================================================
+   * TOP HALF · 闷头一把梭(3 hours, all at once)
+   * ====================================================== */
+  .half-top {
+    position: absolute;
+    top: 200px;
+    left: 160px;
+    right: 160px;
+    height: 300px;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .half-label {
+    font-family: var(--mono);
+    font-size: 13px;
+    letter-spacing: 0.24em;
+    color: var(--muted);
+    text-transform: uppercase;
+    margin-bottom: 24px;
+    display: flex;
+    align-items: center;
+    gap: 12px;
+  }
+  .half-label .tag {
+    padding: 3px 10px;
+    border: 1px solid var(--hairline);
+    border-radius: 2px;
+    color: var(--ink-60);
+  }
+  .half-top .half-label .tag { border-color: rgba(160,74,56,0.4); color: rgba(200,120,100,0.85); }
+  .half-label .zh {
+    font-family: var(--serif-zh);
+    font-size: 22px;
+    font-weight: 400;
+    letter-spacing: 0.02em;
+    color: var(--ink-80);
+    margin-left: 4px;
+  }
 
-          <div style={{fontFamily: serif, fontSize: 16, color: ASH,
-            lineHeight: 1.7, flex: 1}}>
-            埋头 6 小时<br/>
-            一次性端出成品<br/>
-            默祷「方向没理解错」
-          </div>
+  /* Single huge terminal panel */
+  .terminal-big {
+    width: 100%;
+    height: 200px;
+    background: rgba(20, 20, 20, 1);
+    border: 1px solid var(--hairline);
+    border-radius: 10px;
+    overflow: hidden;
+    box-shadow:
+      0 0 0 1px rgba(255,255,255,0.02),
+      0 40px 80px -30px rgba(0,0,0,0.7);
+    position: relative;
+  }
+  .tty-head {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 14px 18px;
+    border-bottom: 1px solid var(--hairline);
+    background: rgba(255,255,255,0.02);
+  }
+  .tty-head .d {
+    width: 10px; height: 10px; border-radius: 50%;
+    background: var(--hairline);
+  }
+  .tty-title {
+    margin-left: 14px;
+    color: var(--muted);
+    font-size: 12px;
+    font-family: var(--mono);
+    letter-spacing: 0.04em;
+  }
+  .tty-body {
+    padding: 28px 30px;
+    font-family: var(--mono);
+    font-size: 17px;
+    line-height: 1.6;
+    color: rgba(255,255,255,0.86);
+  }
+  .tty-body .line {
+    opacity: 0;
+    will-change: opacity;
+  }
+  .tty-body .prompt { color: var(--accent); margin-right: 10px; }
+  .tty-body .dim { color: var(--muted); }
+
+  /* The long running progress bar (simulated "3-hour render") */
+  .progress-row {
+    margin-top: 14px;
+    display: flex;
+    align-items: center;
+    gap: 14px;
+    font-family: var(--mono);
+    font-size: 14px;
+    color: var(--ink-60);
+    opacity: 0;
+    will-change: opacity;
+  }
+  .progress-bar {
+    flex: 1;
+    height: 4px;
+    background: var(--hairline);
+    border-radius: 2px;
+    position: relative;
+    overflow: hidden;
+  }
+  .progress-bar-fill {
+    position: absolute;
+    top: 0; left: 0;
+    height: 100%;
+    background: var(--accent);
+    width: 0%;
+    will-change: width, background;
+  }
+  .progress-bar.failed .progress-bar-fill {
+    background: var(--bad-strong);
+  }
+  .progress-pct {
+    font-variant-numeric: tabular-nums;
+    letter-spacing: 0.04em;
+    min-width: 54px;
+    text-align: right;
+  }
+  .progress-hours {
+    color: var(--muted);
+    font-size: 12px;
+    letter-spacing: 0.12em;
+  }
+  .progress-row.failed {
+    color: var(--bad-strong);
+  }
 
-          {badReveal > 0 && (
-            <div style={{marginTop: 20, padding:'14px 18px', background:'#fff',
-              border: `1px solid #d0c8b8`, opacity: badReveal,
-              display:'flex', alignItems:'center', gap: 12}}>
-              <div style={{fontFamily: serif, fontSize: 22, color:'#888'}}>😐</div>
-              <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 15,
-                color:'#666', lineHeight: 1.4}}>
-                「这方向不对…<br/>得重来」
-              </div>
-            </div>
-          )}
-        </div>
+  /* Big X overlay for failure stamp */
+  .fail-stamp {
+    position: absolute;
+    right: 32px;
+    top: 50%;
+    transform: translateY(-50%) rotate(-8deg);
+    width: 120px; height: 120px;
+    pointer-events: none;
+    opacity: 0;
+    will-change: opacity, transform;
+    z-index: 10;
+  }
+  .fail-stamp svg { width: 100%; height: 100%; }
+  .fail-stamp .stamp-text {
+    position: absolute;
+    bottom: -22px;
+    left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.32em;
+    color: var(--bad-strong);
+    white-space: nowrap;
+  }
 
-        {/* Right: good mode */}
-        <div style={{opacity: rightOp, background:'#fff', padding:'40px 44px',
-          border: `1.5px solid ${TERRA}`, display:'flex', flexDirection:'column',
-          position:'relative'}}>
-          <div style={{display:'flex', alignItems:'center', gap: 14, marginBottom: 28}}>
-            <div style={{width: 32, height: 32, borderRadius:'50%',
-              background: TERRA, color:'#fff', fontFamily: serif, fontSize: 20,
-              fontWeight: 600, display:'flex', alignItems:'center',
-              justifyContent:'center', lineHeight: 1}}>✓</div>
-            <div style={{fontFamily: serif, fontSize: 30, fontWeight: 500,
-              color: INK}}>Junior Designer 模式</div>
-          </div>
+  /* ======================================================
+   * BOTTOM HALF · 尽早 show(small iterations)
+   * ====================================================== */
+  .half-bot {
+    position: absolute;
+    top: 580px;
+    left: 160px;
+    right: 160px;
+    height: 340px;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .half-bot .half-label .tag {
+    border-color: rgba(217,119,87,0.35);
+    color: var(--accent);
+  }
 
-          <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-            letterSpacing: '0.2em', marginBottom: 14}}>
-            5 CHECKPOINTS · CONTINUOUS FEEDBACK
-          </div>
+  .iter-row {
+    display: flex;
+    gap: 32px;
+    align-items: flex-end;
+    height: 240px;
+    margin-top: 12px;
+  }
 
-          {/* Timeline with ticks */}
-          <div style={{position:'relative', height: 18, marginBottom: 36}}>
-            <div style={{position:'absolute', top: 8, left: 0, right: 0,
-              height: 2, background:'#e8e1d3'}} />
-            {ticks.map((tick, i) => {
-              const show = elapsed > tick ? 1 : 0;
-              const pct = (i + 1) / ticks.length * 100;
-              return (
-                <div key={i} style={{position:'absolute',
-                  left: `calc(${pct}% - 10px)`, top: 0, width: 20, height: 20,
-                  borderRadius:'50%', background: TERRA, color:'#fff',
-                  fontSize: 11, display:'flex', alignItems:'center',
-                  justifyContent:'center', opacity: show,
-                  transform: `scale(${show})`, transition:'none',
-                  fontFamily: sans, fontWeight: 600}}>✓</div>
-              );
-            })}
-          </div>
+  .iter-panel {
+    flex: 1;
+    background: rgba(20, 20, 20, 1);
+    border: 1px solid var(--hairline);
+    border-radius: 8px;
+    overflow: hidden;
+    height: 100%;
+    position: relative;
+    opacity: 0;
+    transform: translateY(20px);
+    will-change: opacity, transform;
+    display: flex;
+    flex-direction: column;
+  }
+  .iter-panel .ip-head {
+    padding: 10px 14px;
+    border-bottom: 1px solid var(--hairline);
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.16em;
+    color: var(--muted);
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+  .iter-panel .ip-version {
+    color: var(--accent);
+    font-weight: 500;
+  }
+  .iter-panel .ip-body {
+    flex: 1;
+    padding: 16px 18px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    gap: 10px;
+  }
+  /* Rough mockup blocks that grow more detailed each iteration */
+  .iter-panel .m-block {
+    height: 8px;
+    background: var(--dim);
+    border-radius: 2px;
+    opacity: 0.8;
+  }
+  .iter-panel .m-block.accent { background: var(--accent); opacity: 0.8; }
+  .iter-panel .m-block.short { width: 40%; }
+  .iter-panel .m-block.med { width: 70%; }
+  .iter-panel .m-block.full { width: 100%; }
+  .iter-panel .m-block.tall { height: 24px; }
+  .iter-panel .m-block.big { height: 40px; }
+
+  .iter-panel .nod {
+    position: absolute;
+    top: 10px;
+    right: 14px;
+    width: 16px; height: 16px;
+    opacity: 0;
+    will-change: opacity, transform;
+  }
+  .iter-panel .nod svg {
+    width: 100%; height: 100%;
+    stroke: var(--accent);
+    fill: none;
+    stroke-width: 2;
+  }
+  .iter-panel .ip-minutes {
+    position: absolute;
+    bottom: 10px;
+    left: 14px;
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.12em;
+    color: var(--muted);
+  }
 
-          <div style={{fontFamily: serif, fontSize: 16, color: INK,
-            lineHeight: 1.7, flex: 1}}>
-            假设 · 骨架 · 粗图<br/>
-            每步都 show 一下<br/>
-            错得早,改得小
-          </div>
+  /* Rising curve visualization for bottom half */
+  .curve-wrap {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    width: 340px;
+    height: 180px;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .curve-wrap svg {
+    width: 100%;
+    height: 100%;
+    overflow: visible;
+  }
+  .curve-wrap .axis {
+    stroke: var(--hairline);
+    stroke-width: 1;
+    fill: none;
+  }
+  .curve-wrap .curve-path {
+    stroke: var(--accent);
+    stroke-width: 2;
+    fill: none;
+    stroke-linecap: round;
+    stroke-linejoin: round;
+  }
+  .curve-wrap .curve-dot {
+    fill: var(--accent);
+    r: 3;
+  }
+  .curve-wrap .curve-label {
+    font-family: var(--mono);
+    font-size: 9px;
+    fill: var(--muted);
+    letter-spacing: 0.12em;
+  }
 
-          {elapsed > 2.5 && (
-            <div style={{marginTop: 20, padding:'14px 18px', background:'#faf6ef',
-              border: `1px solid ${LINE}`, display:'flex', alignItems:'center', gap: 12,
-              opacity: interpolate(elapsed, [2.5, 2.85], [0, 1])}}>
-              <div style={{fontFamily: serif, fontSize: 22}}>🙂</div>
-              <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 15,
-                color: INK, lineHeight: 1.4}}>
-                「方向对,<br/>继续推进」
-              </div>
-            </div>
-          )}
-        </div>
+  /* ======================================================
+   * BEAT 3 · Full comparison chart crossfade
+   * ====================================================== */
+  .final-chart {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    width: 1280px;
+    height: 620px;
+    opacity: 0;
+    will-change: opacity;
+    z-index: 60;
+  }
+  .final-chart svg {
+    width: 100%; height: 100%;
+    overflow: visible;
+  }
+  .final-chart .axis {
+    stroke: var(--hairline);
+    stroke-width: 1;
+    fill: none;
+  }
+  .final-chart .axis-label {
+    font-family: var(--mono);
+    font-size: 13px;
+    fill: var(--muted);
+    letter-spacing: 0.16em;
+  }
+  .final-chart .tick-label {
+    font-family: var(--mono);
+    font-size: 11px;
+    fill: var(--dim);
+    letter-spacing: 0.06em;
+  }
+  .final-chart .curve-a {
+    stroke: var(--cool);
+    stroke-width: 2;
+    fill: none;
+    stroke-linecap: round;
+    stroke-linejoin: round;
+  }
+  .final-chart .curve-a-dash {
+    stroke: var(--bad-strong);
+    stroke-width: 2.5;
+    fill: none;
+    stroke-dasharray: 5 7;
+    stroke-linecap: round;
+  }
+  .final-chart .curve-b {
+    stroke: var(--accent);
+    stroke-width: 3;
+    fill: none;
+    stroke-linecap: round;
+    stroke-linejoin: round;
+  }
+  .final-chart .curve-b-glow {
+    stroke: var(--accent);
+    stroke-width: 6;
+    fill: none;
+    opacity: 0.18;
+    stroke-linecap: round;
+    stroke-linejoin: round;
+  }
+  .final-chart .curve-dot {
+    fill: var(--accent);
+  }
+  .final-chart .fail-dot {
+    fill: none;
+    stroke: var(--bad-strong);
+    stroke-width: 2.5;
+  }
+  .final-chart .cool-dot {
+    fill: var(--cool);
+  }
+  .final-chart .anchor-label {
+    font-family: var(--serif-zh);
+    font-size: 20px;
+    font-weight: 400;
+    letter-spacing: 0.02em;
+  }
+  .final-chart .anchor-en {
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.18em;
+    text-transform: uppercase;
+  }
+
+  /* ======================================================
+   * BRAND REVEAL — 统一动作
+   * ====================================================== */
+  .brand-sheet {
+    position: absolute;
+    inset: 0;
+    background: var(--cd-bg);
+    transform: translateY(100%);
+    will-change: transform;
+    z-index: 80;
+  }
+  .brand-reveal {
+    position: absolute;
+    inset: 0;
+    z-index: 81;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    opacity: 0;
+    will-change: opacity;
+  }
+  .brand-reveal .wordmark {
+    font-family: var(--sans);
+    font-weight: 100;
+    font-size: 128px;
+    letter-spacing: -0.045em;
+    color: var(--cd-ink);
+    line-height: 1;
+  }
+  .brand-reveal .wordmark .accent { color: var(--accent-deep); }
+  .brand-reveal .underline {
+    width: 0;
+    height: 2px;
+    background: var(--accent);
+    margin-top: 36px;
+    will-change: width;
+  }
+</style>
+</head>
+<body>
+  <div class="stage" id="stage">
+    <div class="mark">HUASHU · DESIGN</div>
+    <div class="mark-right">V2 · 2026</div>
+
+    <div class="title-line" id="titleLine">w2 · 粗糙的第一版,好过完美的大招</div>
+
+    <!-- Splitter -->
+    <div class="splitter" id="splitter"></div>
+    <div class="splitter-label" id="splitterLabel">VS</div>
+
+    <!-- ============ TOP HALF: All-at-once ============ -->
+    <div class="half-top" id="halfTop">
+      <div class="half-label">
+        <span class="tag">A</span>
+        <span class="zh">闷头一把梭</span>
+        <span style="font-family: var(--mono); color: var(--muted); letter-spacing: 0.18em; font-size: 11px; margin-left: auto;">ALL&nbsp;AT&nbsp;ONCE</span>
       </div>
-    </div>
-  );
-}
-
-// ── Scene 2: HTML skeleton with assumptions + placeholders (3 – 8s) ──
-function Scene2_Skeleton() {
-  const { elapsed } = useSprite();
-  const opacity = interpolate(elapsed, [0, 0.5], [0, 1]);
-  const fadeOut = interpolate(elapsed, [4.6, 5], [1, 0]);
-
-  // Typewriter content
-  const fullText = `<!--
-  ASSUMPTIONS:
-  - 主色用暖调橙(未确认,等品牌 spec)
-  - 3 屏切换,Today / Memory / Chat
-  - 字体:Newsreader + Noto Serif SC
-
-  PLACEHOLDERS:
-  - Hero 图暂用灰块 + 文字标签(有图再替换)
-  - AI 洞察文本用「等用户提供真实数据」
-
-  REASONING:
-  - 选橙调是因为客户说要「温暖感」
-  - Serif 因为是阅读类 App
--->`;
-
-  // Type at ~50 chars/sec
-  const charCount = Math.floor(interpolate(elapsed, [0.4, 4.0], [0, fullText.length]));
-  const shownText = fullText.slice(0, charCount);
-  const cursorBlink = Math.floor(elapsed * 2.4) % 2 === 0 && charCount < fullText.length;
-
-  // Hint callout appears after most text typed
-  const hintOp = interpolate(elapsed, [3.5, 4.2], [0, 1]);
-  const hintBob = Math.sin(elapsed * 3) * 4;
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM, opacity: opacity * fadeOut,
-      padding:'60px 100px', display:'flex', flexDirection:'column'}}>
-
-      {/* Header */}
-      <div style={{display:'flex', justifyContent:'space-between',
-        alignItems:'baseline', marginBottom: 32}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-            letterSpacing: '0.3em', marginBottom: 6}}>STEP 1 / 3</div>
-          <div style={{fontFamily: serif, fontSize: 52, fontWeight: 500, color: INK}}>
-            先写 <span style={{fontStyle:'italic', color: TERRA}}>骨架</span> · 不写渲染
-          </div>
-        </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
-          color: ASH, textAlign: 'right', lineHeight: 1.5}}>
-          Assumptions + Placeholders + Reasoning<br/>
-          <span style={{fontSize: 14}}>— 把「猜」写在代码里</span>
+      <div class="terminal-big">
+        <div class="tty-head">
+          <div class="d"></div><div class="d"></div><div class="d"></div>
+          <div class="tty-title">designer@studio · 3h session</div>
         </div>
-      </div>
-
-      {/* Editor */}
-      <div style={{flex: 1, background:'#1a1a1a', position:'relative',
-        display:'flex', flexDirection:'column', overflow:'hidden'}}>
-
-        {/* Editor top bar */}
-        <div style={{padding:'12px 20px', background:'#222',
-          borderBottom:'1px solid #2e2e2e', display:'flex',
-          alignItems:'center', gap: 16}}>
-          <div style={{display:'flex', gap: 8}}>
-            <div style={{width: 12, height: 12, borderRadius:'50%', background:'#ff5f57'}} />
-            <div style={{width: 12, height: 12, borderRadius:'50%', background:'#febc2e'}} />
-            <div style={{width: 12, height: 12, borderRadius:'50%', background:'#28c840'}} />
-          </div>
-          <div style={{fontFamily: mono, fontSize: 11, color:'#888',
-            letterSpacing: '0.1em'}}>
-            index.html · junior-pass-v0
+        <div class="tty-body">
+          <div class="line" id="ttyL1"><span class="prompt">$</span>build final_design.html <span class="dim">// v1.0 · 一次做完</span></div>
+          <div class="progress-row" id="progRow">
+            <div class="progress-bar" id="progBar">
+              <div class="progress-bar-fill" id="progFill"></div>
+            </div>
+            <span class="progress-pct" id="progPct">0%</span>
+            <span class="progress-hours" id="progHours">03:00:00</span>
           </div>
         </div>
-
-        {/* Code body */}
-        <div style={{flex: 1, padding:'32px 40px', fontFamily: mono,
-          fontSize: 19, lineHeight: 1.7, color:'#8a8a8a',
-          whiteSpace:'pre-wrap', position:'relative'}}>
-          <span style={{color:'#6a8a56'}}>{shownText}</span>
-          {cursorBlink && <span style={{background:'#c04a1a', display:'inline-block',
-            width: 10, height: 22, verticalAlign:'text-bottom'}} />}
-        </div>
-
-        {/* Bottom status bar */}
-        <div style={{padding:'10px 20px', background:'#161616',
-          borderTop:'1px solid #2a2a2a', display:'flex',
-          justifyContent:'space-between', fontFamily: mono, fontSize: 10,
-          color:'#666', letterSpacing:'0.12em'}}>
-          <span>HTML · COMMENTS ONLY</span>
-          <span>LN {Math.min(15, Math.floor(charCount / 30) + 1)} / 15</span>
+        <div class="fail-stamp" id="failStamp">
+          <svg viewBox="0 0 120 120">
+            <circle cx="60" cy="60" r="52" fill="none" stroke="#A04A38" stroke-width="3"/>
+            <path d="M 38 38 L 82 82 M 82 38 L 38 82" stroke="#A04A38" stroke-width="4" stroke-linecap="round"/>
+          </svg>
+          <div class="stamp-text">REJECTED</div>
         </div>
       </div>
+    </div>
 
-      {/* Callout */}
-      <div style={{position:'absolute', right: 120, bottom: 100,
-        opacity: hintOp, transform: `translateY(${hintBob}px)`,
-        display:'flex', alignItems:'center', gap: 14}}>
-        <svg width="48" height="24" viewBox="0 0 48 24">
-          <path d="M 46 12 L 6 12 M 14 6 L 6 12 L 14 18"
-            fill="none" stroke={TERRA} strokeWidth="2" strokeLinecap="round"/>
-        </svg>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 22,
-          color: TERRA, letterSpacing:'0.02em'}}>
-          还没写一行渲染代码
-        </div>
+    <!-- ============ BOTTOM HALF: Show early ============ -->
+    <div class="half-bot" id="halfBot">
+      <div class="half-label">
+        <span class="tag">B</span>
+        <span class="zh">尽早 show</span>
+        <span style="font-family: var(--mono); color: var(--muted); letter-spacing: 0.18em; font-size: 11px; margin-left: auto;">SHOW&nbsp;EARLY</span>
       </div>
-    </div>
-  );
-}
-
-// ── Scene 3: First show to user (8 – 13s) ─────────────────
-function Scene3_FirstShow() {
-  const { elapsed } = useSprite();
-  const opacity = interpolate(elapsed, [0, 0.5], [0, 1]);
-  const fadeOut = interpolate(elapsed, [4.6, 5], [1, 0]);
-
-  // Wireframe fade in
-  const wireOp = interpolate(elapsed, [0.2, 0.9], [0, 1]);
-
-  // Chat bubbles stagger in
-  const b1Op = interpolate(elapsed, [1.0, 1.5], [0, 1]);
-  const b2Op = interpolate(elapsed, [2.0, 2.5], [0, 1]);
-  const b3Op = interpolate(elapsed, [2.9, 3.4], [0, 1]);
-
-  // Bottom subtitle
-  const subOp = interpolate(elapsed, [3.8, 4.3], [0, 1]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM, opacity: opacity * fadeOut,
-      padding:'60px 100px', display:'flex', flexDirection:'column'}}>
-
-      {/* Header */}
-      <div style={{display:'flex', justifyContent:'space-between',
-        alignItems:'baseline', marginBottom: 28}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-            letterSpacing: '0.3em', marginBottom: 6}}>STEP 2 / 3</div>
-          <div style={{fontFamily: serif, fontSize: 52, fontWeight: 500, color: INK}}>
-            第一次 <span style={{fontStyle:'italic', color: TERRA}}>show</span> 给用户
+      <div class="iter-row">
+        <div class="iter-panel" id="iter1">
+          <div class="ip-head">
+            <span>draft · v1</span>
+            <span class="ip-version">15 min</span>
           </div>
-        </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
-          color: ASH, textAlign:'right', lineHeight: 1.5}}>
-          「先给你看方向 —— 对吗?」<br/>
-          <span style={{fontSize: 14}}>— 10 分钟对齐</span>
-        </div>
-      </div>
-
-      {/* Two panels */}
-      <div style={{display:'grid', gridTemplateColumns:'1fr 1px 1.1fr', gap: 40,
-        flex: 1, alignItems:'stretch'}}>
-
-        {/* Left: wireframe */}
-        <div style={{background:'#fff', border: `1px solid ${LINE}`,
-          padding: 28, opacity: wireOp, display:'flex', flexDirection:'column'}}>
-          <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-            letterSpacing:'0.2em', marginBottom: 14}}>
-            WIREFRAME · NO STYLING
+          <div class="ip-body">
+            <div class="m-block short"></div>
+            <div class="m-block med"></div>
+            <div class="m-block short"></div>
           </div>
-
-          {/* Hero placeholder */}
-          <div style={{height: 180, background:'#ececec', border:'1.5px dashed #bbb',
-            display:'flex', alignItems:'center', justifyContent:'center',
-            fontFamily: mono, fontSize: 13, color:'#888',
-            letterSpacing:'0.15em', marginBottom: 20}}>
-            [ HERO IMAGE ]
+          <div class="nod" id="nod1">
+            <svg viewBox="0 0 16 16"><path d="M3 8 L7 12 L13 4"/></svg>
           </div>
-
-          {/* Title placeholder */}
-          <div style={{height: 26, background:'#e5e0d3', width:'75%',
-            marginBottom: 10}} />
-          <div style={{height: 16, background:'#ede8db', width:'55%',
-            marginBottom: 24}} />
-
-          {/* AI insight block */}
-          <div style={{background:'#f5f0e3', padding:'16px 18px', marginBottom: 20,
-            border: `1px dashed ${LINE}`}}>
-            <div style={{fontFamily: mono, fontSize: 9, color: ASH,
-              letterSpacing:'0.2em', marginBottom: 8}}>[ AI INSIGHT · 3 行 ]</div>
-            <div style={{height: 8, background:'#d9d2c5', width:'92%', marginBottom: 6}} />
-            <div style={{height: 8, background:'#d9d2c5', width:'88%', marginBottom: 6}} />
-            <div style={{height: 8, background:'#d9d2c5', width:'70%'}} />
+        </div>
+        <div class="iter-panel" id="iter2">
+          <div class="ip-head">
+            <span>draft · v2</span>
+            <span class="ip-version">25 min</span>
           </div>
-
-          <div style={{fontFamily: serif, fontSize: 14, color:'#888',
-            fontStyle:'italic', marginTop:'auto', textAlign:'center',
-            paddingTop: 14, borderTop: `1px solid ${LINE}`}}>
-            Marcus Aurelius · 第四卷
+          <div class="ip-body">
+            <div class="m-block full tall"></div>
+            <div class="m-block med"></div>
+            <div class="m-block short"></div>
+            <div class="m-block med accent"></div>
           </div>
-        </div>
-
-        {/* Divider */}
-        <div style={{background: LINE, width: 1}} />
-
-        {/* Right: chat */}
-        <div style={{display:'flex', flexDirection:'column', gap: 18,
-          paddingTop: 10}}>
-          {/* Designer bubble */}
-          <div style={{opacity: b1Op, display:'flex', gap: 14,
-            alignItems:'flex-start'}}>
-            <div style={{width: 40, height: 40, background: TERRA, color:'#fff',
-              fontFamily: serif, fontSize: 16, fontWeight: 600,
-              display:'flex', alignItems:'center', justifyContent:'center',
-              flexShrink: 0, borderRadius: 2, letterSpacing:'0.05em'}}>JD</div>
-            <div style={{background:'#fff', border: `1px solid ${LINE}`,
-              padding:'16px 20px', maxWidth: '85%'}}>
-              <div style={{fontFamily: mono, fontSize: 9, color: ASH,
-                letterSpacing:'0.2em', marginBottom: 8}}>
-                JUNIOR DESIGNER
-              </div>
-              <div style={{fontFamily: serif, fontSize: 19, color: INK,
-                lineHeight: 1.5}}>
-                我对方向有几个假设,先给你看线框——想法对吗?
-              </div>
-            </div>
+          <div class="nod" id="nod2">
+            <svg viewBox="0 0 16 16"><path d="M3 8 L7 12 L13 4"/></svg>
           </div>
-
-          {/* User bubble 1 */}
-          <div style={{opacity: b2Op, display:'flex', gap: 14,
-            alignItems:'flex-start', justifyContent:'flex-end'}}>
-            <div style={{background: OLIVE, color:'#fff',
-              padding:'14px 20px', maxWidth: '80%'}}>
-              <div style={{fontFamily: mono, fontSize: 9,
-                color:'rgba(255,255,255,0.6)', letterSpacing:'0.2em',
-                marginBottom: 8}}>USER · FEEDBACK</div>
-              <div style={{fontFamily: serif, fontSize: 18, lineHeight: 1.5}}>
-                橙调 OK。AI 洞察那块我想要 <span style={{fontStyle:'italic',
-                  textDecoration:'underline'}}>两行</span> 不要三行
-              </div>
-            </div>
+        </div>
+        <div class="iter-panel" id="iter3">
+          <div class="ip-head">
+            <span>draft · v3</span>
+            <span class="ip-version">35 min</span>
           </div>
-
-          {/* User bubble 2 */}
-          <div style={{opacity: b3Op, display:'flex', gap: 14,
-            alignItems:'flex-start', justifyContent:'flex-end'}}>
-            <div style={{background: OLIVE, color:'#fff',
-              padding:'14px 20px', maxWidth: '80%'}}>
-              <div style={{fontFamily: serif, fontSize: 18, lineHeight: 1.5}}>
-                Hero 图要 <span style={{fontStyle:'italic',
-                  textDecoration:'underline'}}>摄影</span> 不要插画
-              </div>
-            </div>
+          <div class="ip-body">
+            <div class="m-block full big"></div>
+            <div class="m-block full tall accent"></div>
+            <div class="m-block med"></div>
+            <div class="m-block full"></div>
+            <div class="m-block short"></div>
           </div>
-
-          {/* Subtitle */}
-          <div style={{marginTop:'auto', paddingTop: 24, opacity: subOp,
-            borderTop: `1px solid ${LINE}`}}>
-            <div style={{fontFamily: mono, fontSize: 11,
-              color: TERRA, letterSpacing:'0.3em', marginBottom: 6}}>
-              ALIGNMENT
-            </div>
-            <div style={{fontFamily: serif, fontSize: 30, fontWeight: 500,
-              color: INK, lineHeight: 1.2}}>
-              对齐 = <span style={{color: TERRA}}>10 分钟</span>
-              <span style={{fontStyle:'italic', color: ASH, fontSize: 22}}> · 不是 2 小时</span>
-            </div>
+          <div class="nod" id="nod3">
+            <svg viewBox="0 0 16 16"><path d="M3 8 L7 12 L13 4"/></svg>
           </div>
         </div>
       </div>
     </div>
-  );
-}
-
-// ── ArtBlock: oil painting hero (from c1-ios-prototype) ───
-function ArtBlock({ mood = 'warm' }) {
-  const palettes = {
-    warm: ['#8b4a2b', '#c67b4a', '#e3a876', '#f2d4a7'],
-    quiet: ['#3d4a3a', '#6a8066', '#a8b89c', '#e0d8b8'],
-  };
-  const p = palettes[mood];
-  return (
-    <div style={{
-      width:'100%', height:'100%', position:'relative', overflow:'hidden',
-      background: `linear-gradient(135deg, ${p[0]} 0%, ${p[1]} 35%, ${p[2]} 70%, ${p[3]} 100%)`,
-    }}>
-      <div style={{
-        position:'absolute', inset: 0,
-        background: `
-          radial-gradient(ellipse 80px 30px at 30% 40%, ${p[3]}44, transparent 70%),
-          radial-gradient(ellipse 60px 20px at 70% 60%, ${p[0]}33, transparent 70%),
-          radial-gradient(ellipse 100px 40px at 50% 80%, ${p[2]}44, transparent 70%),
-          radial-gradient(ellipse 50px 25px at 20% 70%, ${p[1]}55, transparent 70%)
-        `,
-        filter:'blur(1px)',
-      }} />
-      <svg width="100%" height="100%" style={{position:'absolute', inset:0, opacity: 0.18}}>
-        <filter id="paint-noise-w2">
-          <feTurbulence baseFrequency="0.9" numOctaves="2" />
-          <feColorMatrix values="0 0 0 0 0.3   0 0 0 0 0.2   0 0 0 0 0.1   0 0 0 1 0" />
-        </filter>
-        <rect width="100%" height="100%" filter="url(#paint-noise-w2)" />
+
+    <!-- ============ Beat 3 · Final comparison chart ============ -->
+    <div class="final-chart" id="finalChart">
+      <svg viewBox="0 0 1280 620" preserveAspectRatio="xMidYMid meet">
+        <!-- Axes -->
+        <line class="axis" x1="110" y1="60" x2="110" y2="520"/>
+        <line class="axis" x1="110" y1="520" x2="1200" y2="520"/>
+
+        <!-- Y-axis label -->
+        <text class="axis-label" x="58" y="290" transform="rotate(-90 58 290)" text-anchor="middle">QUALITY</text>
+        <!-- X-axis label -->
+        <text class="axis-label" x="655" y="570" text-anchor="middle">TIME</text>
+
+        <!-- Tick marks -->
+        <text class="tick-label" x="110" y="545" text-anchor="middle">0</text>
+        <text class="tick-label" x="290" y="545" text-anchor="middle">15m</text>
+        <text class="tick-label" x="480" y="545" text-anchor="middle">25m</text>
+        <text class="tick-label" x="680" y="545" text-anchor="middle">35m</text>
+        <text class="tick-label" x="1200" y="545" text-anchor="middle">3h</text>
+
+        <!-- Curve A (All-at-once): flat crawl near zero, late spike, then crash -->
+        <!-- Narrative: 3 hours of silent work → finally reveal at 99% → rejected → drops -->
+        <path class="curve-a" id="curveA"
+              d="M 110 500 L 400 495 L 700 490 L 1000 485 L 1140 180" />
+        <!-- Fall after rejection, red dashed -->
+        <path class="curve-a-dash" id="curveACrash"
+              d="M 1140 180 L 1200 510" />
+        <circle class="fail-dot" id="failDot" cx="1140" cy="180" r="9"/>
+        <!-- Small X marker on top of the fail dot -->
+        <g id="failX" opacity="0">
+          <line x1="1130" y1="170" x2="1150" y2="190" stroke="#C85A42" stroke-width="2.5" stroke-linecap="round"/>
+          <line x1="1150" y1="170" x2="1130" y2="190" stroke="#C85A42" stroke-width="2.5" stroke-linecap="round"/>
+        </g>
+
+        <!-- Anchor for A (right side, top near the spike) -->
+        <text class="anchor-label" x="1200" y="150" fill="#C85A42" text-anchor="end">闷头一把梭</text>
+        <text class="anchor-en" x="1200" y="170" fill="#C85A42" text-anchor="end">REJECTED</text>
+
+        <!-- Curve B (Show early): steady step rise across first 35 min -->
+        <path class="curve-b-glow" id="curveBGlow"
+              d="M 110 500 L 290 380 L 480 270 L 680 140" />
+        <path class="curve-b" id="curveB"
+              d="M 110 500 L 290 380 L 480 270 L 680 140" />
+        <circle class="curve-dot" cx="290" cy="380" r="6"/>
+        <circle class="curve-dot" cx="480" cy="270" r="6"/>
+        <circle class="curve-dot" cx="680" cy="140" r="8"/>
+
+        <!-- Anchor for B (above the peak dot on left-ish side) -->
+        <text class="anchor-label" x="680" y="115" fill="#D97757" text-anchor="middle">尽早 show</text>
+        <text class="anchor-en" x="680" y="96" fill="#D97757" text-anchor="middle">SHIPPED</text>
+
+        <!-- Legend hint: tiny label on A's plateau -->
+        <text class="tick-label" x="555" y="477" text-anchor="middle" fill="rgba(255,255,255,0.3)" style="letter-spacing: 0.12em;">— 3 hours silence —</text>
       </svg>
     </div>
-  );
-}
-
-// ── Scene 4: Full pass fills in (13 – 18s) ────────────────
-function Scene4_FullPass() {
-  const { elapsed } = useSprite();
-  const opacity = interpolate(elapsed, [0, 0.5], [0, 1]);
-  const fadeOut = interpolate(elapsed, [4.6, 5], [1, 0]);
-
-  // Staggered reveal of real content
-  const heroReveal = interpolate(elapsed, [0.6, 1.8], [0, 1], Easing.easeOut);
-  const titleReveal = interpolate(elapsed, [1.5, 2.3], [0, 1]);
-  const insightReveal = interpolate(elapsed, [2.2, 3.1], [0, 1]);
-  const meta = interpolate(elapsed, [3.0, 3.8], [0, 1]);
-
-  // Bottom subtitle
-  const subOp = interpolate(elapsed, [3.9, 4.4], [0, 1]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM, opacity: opacity * fadeOut,
-      padding:'60px 100px', display:'flex', flexDirection:'column'}}>
-
-      {/* Header */}
-      <div style={{display:'flex', justifyContent:'space-between',
-        alignItems:'baseline', marginBottom: 28}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-            letterSpacing: '0.3em', marginBottom: 6}}>STEP 3 / 3</div>
-          <div style={{fontFamily: serif, fontSize: 52, fontWeight: 500, color: INK}}>
-            Full pass · <span style={{fontStyle:'italic', color: TERRA}}>灰块变真图</span>
-          </div>
-        </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
-          color: ASH, textAlign:'right', lineHeight: 1.5}}>
-          Placeholders → Content<br/>
-          <span style={{fontSize: 14}}>— 方向对了再填色</span>
-        </div>
-      </div>
-
-      {/* Two panels: before wireframe (thumbnail) vs after (full card) */}
-      <div style={{display:'grid', gridTemplateColumns:'0.55fr 60px 1fr', gap: 0,
-        flex: 1, alignItems:'center'}}>
-
-        {/* Left: wireframe thumbnail, still, for contrast */}
-        <div style={{background:'#fff', border: `1px solid ${LINE}`,
-          padding: 20, opacity: 0.55, transform:'scale(0.92)',
-          transformOrigin:'center right'}}>
-          <div style={{fontFamily: mono, fontSize: 9, color: ASH,
-            letterSpacing:'0.2em', marginBottom: 10}}>
-            BEFORE
-          </div>
-          <div style={{height: 100, background:'#ececec',
-            border:'1.5px dashed #bbb', display:'flex',
-            alignItems:'center', justifyContent:'center',
-            fontFamily: mono, fontSize: 10, color:'#888',
-            letterSpacing:'0.15em', marginBottom: 12}}>
-            [ HERO IMAGE ]
-          </div>
-          <div style={{height: 14, background:'#e5e0d3', width:'75%', marginBottom: 6}} />
-          <div style={{height: 10, background:'#ede8db', width:'55%', marginBottom: 14}} />
-          <div style={{background:'#f5f0e3', padding:'10px 12px',
-            border: `1px dashed ${LINE}`}}>
-            <div style={{height: 6, background:'#d9d2c5', width:'92%', marginBottom: 4}} />
-            <div style={{height: 6, background:'#d9d2c5', width:'88%', marginBottom: 4}} />
-            <div style={{height: 6, background:'#d9d2c5', width:'70%'}} />
-          </div>
-        </div>
-
-        {/* Arrow */}
-        <div style={{display:'flex', alignItems:'center', justifyContent:'center'}}>
-          <svg width="40" height="24" viewBox="0 0 40 24">
-            <path d="M 2 12 L 38 12 M 30 4 L 38 12 L 30 20"
-              fill="none" stroke={TERRA} strokeWidth="2" strokeLinecap="round"/>
-          </svg>
-        </div>
-
-        {/* Right: full card */}
-        <div style={{background:'#fff', border: `1px solid ${LINE}`,
-          boxShadow:'0 20px 60px rgba(0,0,0,0.08), 0 4px 16px rgba(0,0,0,0.04)',
-          display:'flex', flexDirection:'column', overflow:'hidden'}}>
-
-          <div style={{padding:'14px 24px', borderBottom:`1px solid ${LINE}`,
-            display:'flex', justifyContent:'space-between', alignItems:'center'}}>
-            <div style={{fontFamily: mono, fontSize: 10, color: TERRA,
-              letterSpacing:'0.25em'}}>AFTER · FULL PASS</div>
-            <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-              letterSpacing:'0.15em'}}>STOIC READER · v0.3</div>
-          </div>
-
-          {/* Hero image: oil painting */}
-          <div style={{height: 220, position:'relative', overflow:'hidden'}}>
-            <div style={{position:'absolute', inset: 0, opacity: heroReveal}}>
-              <ArtBlock mood="warm" />
-            </div>
-            {heroReveal < 0.95 && (
-              <div style={{position:'absolute', inset: 0, background:'#ececec',
-                opacity: 1 - heroReveal, display:'flex', alignItems:'center',
-                justifyContent:'center', fontFamily: mono, fontSize: 12,
-                color:'#888', letterSpacing:'0.15em',
-                border:'1.5px dashed #bbb'}}>
-                [ HERO IMAGE ]
-              </div>
-            )}
-            {/* Photo credit */}
-            <div style={{position:'absolute', bottom: 10, right: 12,
-              fontFamily: mono, fontSize: 9, color:'rgba(255,255,255,0.7)',
-              letterSpacing:'0.2em', opacity: heroReveal}}>
-              PHOTO · J. TURNER
-            </div>
-          </div>
-
-          {/* Title */}
-          <div style={{padding:'26px 32px 10px', opacity: titleReveal}}>
-            <div style={{fontFamily: serif, fontSize: 36, fontWeight: 500,
-              color: INK, lineHeight: 1.15, letterSpacing:'-0.01em',
-              marginBottom: 6}}>
-              The Obstacle<br/>
-              <span style={{fontStyle:'italic', color: TERRA}}>is the Way</span>
-            </div>
-            <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 15,
-              color: ASH}}>
-              Marcus Aurelius · 第四卷 · 今日阅读
-            </div>
-          </div>
 
-          {/* AI insight — NOW 2 lines, not 3 */}
-          <div style={{margin:'10px 32px 20px', padding:'16px 20px',
-            background:'#faf6ef', borderLeft: `3px solid ${TERRA}`,
-            opacity: insightReveal}}>
-            <div style={{fontFamily: mono, fontSize: 9, color: TERRA,
-              letterSpacing:'0.25em', marginBottom: 8}}>AI INSIGHT · 2 LINES</div>
-            <div style={{fontFamily: serif, fontSize: 15, color: INK,
-              lineHeight: 1.55}}>
-              你最近笔记里「困难」出现 7 次。<br/>
-              Aurelius 今天正好在说:障碍本身就是路。
-            </div>
-          </div>
-
-          {/* Meta row */}
-          <div style={{padding:'14px 32px', borderTop:`1px solid ${LINE}`,
-            display:'flex', justifyContent:'space-between',
-            fontFamily: mono, fontSize: 10, color: ASH,
-            letterSpacing:'0.15em', opacity: meta}}>
-            <span>READ · 4 MIN</span>
-            <span>MEMORY · 12 NOTES</span>
-            <span>CHAT · ASK AURELIUS</span>
-          </div>
-        </div>
-      </div>
-
-      {/* Subtitle */}
-      <div style={{textAlign:'center', marginTop: 30, opacity: subOp}}>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 22,
-          color: INK}}>
-          迭代到 80%,再做最后 20% 的抛光
-        </div>
-      </div>
+    <!-- Brand reveal -->
+    <div class="brand-sheet" id="brandSheet"></div>
+    <div class="brand-reveal" id="brandReveal">
+      <div class="wordmark">huashu<span class="accent"> · </span>design</div>
+      <div class="underline" id="brandUnderline"></div>
     </div>
-  );
-}
-
-// ── Scene 5: Closing + 4 checkpoints (18 – 22s) ───────────
-function Scene5_Closing() {
-  const { elapsed } = useSprite();
-  const opacity = interpolate(elapsed, [0, 0.6], [0, 1]);
-
-  const titleY = interpolate(elapsed, [0, 1.0], [30, 0], Easing.easeOut);
-  const lineW = interpolate(elapsed, [0.8, 1.6], [0, 620]);
-
-  const items = [
-    { text: '问 clarifying questions(一次一批)' },
-    { text: '写 assumptions + placeholders' },
-    { text: '尽早 show(哪怕只是灰块)' },
-    { text: '迭代再抛光,不一次做完' },
-  ];
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM, opacity,
-      display:'flex', alignItems:'center', justifyContent:'center',
-      flexDirection:'column', padding:'60px 120px'}}>
-
-      <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
-        color: TERRA, marginBottom: 24}}>
-        JUNIOR DESIGNER · RECAP
-      </div>
+  </div>
+
+<script>
+  // Auto-scale stage
+  function fitStage() {
+    const stage = document.getElementById('stage');
+    const sx = window.innerWidth / 1920;
+    const sy = window.innerHeight / 1080;
+    const s = Math.min(sx, sy);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  fitStage();
+  window.addEventListener('resize', fitStage);
+
+  // Easings
+  const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
+  const expoIn  = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
+  const cubicInOut = t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
+  const cubicOut = t => 1 - Math.pow(1 - t, 3);
+  const cubicIn  = t => t * t * t;
+
+  function lerp(t, a, b, easing) {
+    if (t <= 0) return a;
+    if (t >= 1) return b;
+    const e = easing ? easing(t) : t;
+    return a + (b - a) * e;
+  }
+  function seg(time, start, end) {
+    if (time <= start) return 0;
+    if (time >= end) return 1;
+    return (time - start) / (end - start);
+  }
 
-      <div style={{fontFamily: serif, fontSize: 100, fontWeight: 500,
-        color: INK, lineHeight: 1.05, letterSpacing:'-0.015em',
-        transform: `translateY(${titleY}px)`, textAlign:'center'}}>
-        早 <span style={{fontStyle:'italic', color: TERRA}}>show</span>
-        <span style={{color: ASH, fontWeight: 400, margin:'0 18px'}}>·</span>
-        早 <span style={{fontStyle:'italic', color: TERRA}}>改</span>
-        <span style={{color: ASH, fontWeight: 400, margin:'0 18px'}}>·</span>
-        早 <span style={{fontStyle:'italic', color: TERRA}}>对齐</span>
-      </div>
+  // ────────────────────────────────────
+  // Timeline — total 12s (Beat 1: 0-2 · Beat 2: 2-10 · Beat 3: 10-12)
+  //
+  // 0.0-0.6    title + splitter grow
+  // 0.6-1.4    two half-labels fade in (top first, then bot)
+  // 1.4-2.0    top terminal line 1 types; bot panel 1 enters
+  //
+  // Top track (闷头):
+  //   2.0-7.8  progress bar crawls from 0 to 99% (slow, painful)
+  //   7.8-8.4  stuck at 99%
+  //   8.4-8.9  fail stamp lands + bar turns red + bar drops to 0
+  //
+  // Bottom track (尽早):
+  //   2.0-2.6  iter1 enters, nod1 appears @ 2.8
+  //   3.6-4.2  iter2 enters, nod2 appears @ 4.4
+  //   5.6-6.2  iter3 enters, nod3 appears @ 6.4 (final tick — biggest)
+  //
+  // 8.8-9.8    both halves dim; final chart crossfades in
+  //             (curves draw via stroke-dasharray)
+  // 9.8-10.4   chart settles, anchor labels bloom
+  // 10.0-12.0  brand reveal (sheet + wordmark + underline)
+  // ────────────────────────────────────
+
+  const el = {
+    title:      document.getElementById('titleLine'),
+    splitter:   document.getElementById('splitter'),
+    splitterLb: document.getElementById('splitterLabel'),
+    halfTop:    document.getElementById('halfTop'),
+    halfBot:    document.getElementById('halfBot'),
+    ttyL1:      document.getElementById('ttyL1'),
+    progRow:    document.getElementById('progRow'),
+    progBar:    document.getElementById('progBar'),
+    progFill:   document.getElementById('progFill'),
+    progPct:    document.getElementById('progPct'),
+    progHours:  document.getElementById('progHours'),
+    failStamp:  document.getElementById('failStamp'),
+    iter1:      document.getElementById('iter1'),
+    iter2:      document.getElementById('iter2'),
+    iter3:      document.getElementById('iter3'),
+    nod1:       document.getElementById('nod1'),
+    nod2:       document.getElementById('nod2'),
+    nod3:       document.getElementById('nod3'),
+    finalChart: document.getElementById('finalChart'),
+    brandSheet: document.getElementById('brandSheet'),
+    brandReveal:document.getElementById('brandReveal'),
+    brandUnder: document.getElementById('brandUnderline'),
+    curveA:     document.getElementById('curveA'),
+    curveACrash:document.getElementById('curveACrash'),
+    curveB:     document.getElementById('curveB'),
+    curveBGlow: document.getElementById('curveBGlow'),
+  };
 
-      <div style={{height: 1, background: INK, width: lineW, marginTop: 34,
-        marginBottom: 46}} />
-
-      {/* 4 checkpoints */}
-      <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:'18px 60px',
-        maxWidth: 1100, width:'100%'}}>
-        {items.map((it, i) => {
-          const appear = interpolate(elapsed, [1.6 + i * 0.25, 2.2 + i * 0.25], [0, 1]);
-          const checkAppear = interpolate(elapsed, [2.0 + i * 0.25, 2.5 + i * 0.25], [0, 1], Easing.spring);
-          return (
-            <div key={i} style={{display:'flex', alignItems:'center', gap: 20,
-              opacity: appear, borderBottom:`1px solid ${LINE}`, paddingBottom: 14}}>
-              <div style={{width: 36, height: 36, border:`2px solid ${TERRA}`,
-                display:'flex', alignItems:'center', justifyContent:'center',
-                flexShrink: 0, background: checkAppear > 0.5 ? TERRA : 'transparent',
-                color:'#fff', fontFamily: serif, fontWeight: 600, fontSize: 20,
-                lineHeight: 1, transform: `scale(${0.7 + checkAppear * 0.3})`}}>
-                {checkAppear > 0.5 ? '✓' : ''}
-              </div>
-              <div style={{fontFamily: serif, fontSize: 26, color: INK,
-                lineHeight: 1.35, letterSpacing:'0.005em'}}>
-                {it.text}
-              </div>
-            </div>
-          );
-        })}
-      </div>
-    </div>
-  );
-}
-
-// ── Watermark ─────────────────────────────────────────────
-function Watermark() {
-  return (
-    <div style={{position:'absolute', bottom: 24, right: 32,
-      fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
-      fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
-      Created by Huashu-Design
-    </div>
-  );
-}
-
-// ── Main composition ──────────────────────────────────────
-function App() {
-  return (
-    <Stage duration={22} width={1920} height={1080} bgColor={CREAM}>
-      <Sprite start={0} end={3}><Scene1_Contrast /></Sprite>
-      <Sprite start={3} end={8}><Scene2_Skeleton /></Sprite>
-      <Sprite start={8} end={13}><Scene3_FirstShow /></Sprite>
-      <Sprite start={13} end={18}><Scene4_FullPass /></Sprite>
-      <Sprite start={18} end={22}><Scene5_Closing /></Sprite>
-      <Watermark />
-    </Stage>
-  );
-}
-
-ReactDOM.createRoot(document.getElementById('root')).render(<App />);
+  // Precompute path lengths for draw-on animation
+  const lenA = el.curveA.getTotalLength();
+  const lenACrash = el.curveACrash.getTotalLength();
+  const lenB = el.curveB.getTotalLength();
+
+  el.curveA.style.strokeDasharray = `${lenA} ${lenA}`;
+  el.curveA.style.strokeDashoffset = lenA;
+  el.curveACrash.style.strokeDasharray = `${lenACrash} ${lenACrash}`;
+  el.curveACrash.style.strokeDashoffset = lenACrash;
+  el.curveB.style.strokeDasharray = `${lenB} ${lenB}`;
+  el.curveB.style.strokeDashoffset = lenB;
+  el.curveBGlow.style.strokeDasharray = `${lenB} ${lenB}`;
+  el.curveBGlow.style.strokeDashoffset = lenB;
+
+  // Also precompute chart dot selections (hide initially)
+  const chartDots = el.finalChart.querySelectorAll('circle');
+  const chartAnchors = el.finalChart.querySelectorAll('.anchor-label, .anchor-en');
+  const chartTicks = el.finalChart.querySelectorAll('.tick-label, .axis-label');
+
+  const DURATION = 12.0;
+  let startTime = null;
+  let loop = true;
+  if (window.__recording === true) loop = false;
+
+  function tick(now) {
+    if (startTime === null) startTime = now;
+    let t = (now - startTime) / 1000;
+
+    if (t >= DURATION) {
+      if (loop) { startTime = now; t = 0; }
+      else { t = DURATION; }
+    }
+
+    // ────── Title
+    const titleIn = seg(t, 0.1, 1.0);
+    const titleOut = seg(t, 9.2, 9.8);
+    el.title.style.opacity = Math.max(0, Math.min(cubicOut(titleIn), 1 - titleOut));
+
+    // ────── Splitter (fade out earlier so Beat 3 is clean)
+    const splitT = seg(t, 0.0, 0.8);
+    const splitOut = seg(t, 8.4, 8.9);
+    el.splitter.style.transform = `scaleX(${expoOut(splitT) * (1 - splitOut)})`;
+    const splitLabelT = seg(t, 0.4, 1.0);
+    const splitLabelOut = seg(t, 8.2, 8.7);
+    el.splitterLb.style.opacity = Math.max(0, Math.min(cubicOut(splitLabelT), 1 - splitLabelOut));
+
+    // ────── Halves fade in / out (fade out earlier to clear for Beat 3 chart)
+    const topIn = seg(t, 0.6, 1.4);
+    const topOut = seg(t, 8.4, 9.0);
+    el.halfTop.style.opacity = Math.max(0, Math.min(cubicOut(topIn), 1 - topOut));
+
+    const botIn = seg(t, 1.0, 1.8);
+    const botOut = seg(t, 8.4, 9.0);
+    el.halfBot.style.opacity = Math.max(0, Math.min(cubicOut(botIn), 1 - botOut));
+
+    // ────── TOP track: terminal line + progress bar
+    const ttyL1In = seg(t, 1.4, 1.8);
+    el.ttyL1.style.opacity = cubicOut(ttyL1In);
+
+    // Progress bar appears @ 1.8, starts crawling 2.0-7.8, stuck 7.8-8.4, fails @ 8.4
+    const progRowIn = seg(t, 1.8, 2.2);
+    el.progRow.style.opacity = cubicOut(progRowIn);
+
+    let pct = 0;
+    let hoursTxt = '03:00:00';
+    if (t >= 2.0 && t < 7.8) {
+      const p = seg(t, 2.0, 7.8);
+      // Easing: starts fast, slows down to 99% (mimics the "last 10% takes forever" trope)
+      pct = 99 * (1 - Math.pow(1 - p, 2.2));
+      const remaining = Math.max(0, (1 - p) * 3 * 60 * 60);
+      const hh = String(Math.floor(remaining / 3600)).padStart(2, '0');
+      const mm = String(Math.floor((remaining % 3600) / 60)).padStart(2, '0');
+      const ss = String(Math.floor(remaining % 60)).padStart(2, '0');
+      hoursTxt = `${hh}:${mm}:${ss}`;
+    } else if (t >= 7.8 && t < 8.4) {
+      pct = 99;
+      // Micro-jitter to show "stuck"
+      const jitter = Math.sin(t * 30) * 0.1;
+      pct = 99 + jitter;
+      hoursTxt = '00:00:12';
+    } else if (t >= 8.4 && t < 8.7) {
+      // Fail animation — pct stays at 99 briefly then snaps to 0
+      pct = 99;
+      hoursTxt = '— REJECTED —';
+    } else if (t >= 8.7) {
+      pct = 0;
+      hoursTxt = '— REJECTED —';
+    }
+
+    el.progFill.style.width = `${pct}%`;
+    el.progPct.textContent = `${Math.floor(Math.max(0, pct))}%`;
+    el.progHours.textContent = hoursTxt;
+
+    // Fail state toggle
+    if (t >= 8.4) {
+      el.progBar.classList.add('failed');
+      el.progRow.classList.add('failed');
+    } else {
+      el.progBar.classList.remove('failed');
+      el.progRow.classList.remove('failed');
+    }
+
+    // Fail stamp lands at 8.4
+    const stampIn = seg(t, 8.4, 8.7);
+    if (stampIn > 0) {
+      el.failStamp.style.opacity = cubicOut(stampIn);
+      const scale = lerp(stampIn, 1.6, 1.0, expoOut);
+      el.failStamp.style.transform = `translateY(-50%) rotate(-8deg) scale(${scale})`;
+    } else {
+      el.failStamp.style.opacity = 0;
+    }
+
+    // ────── BOTTOM track: 3 iter panels
+    const iterTimings = [
+      { enter: [2.0, 2.6], nod: [2.8, 3.2] },
+      { enter: [3.6, 4.2], nod: [4.4, 4.8] },
+      { enter: [5.6, 6.2], nod: [6.4, 6.9] },
+    ];
+
+    [el.iter1, el.iter2, el.iter3].forEach((panel, i) => {
+      const { enter } = iterTimings[i];
+      const p = seg(t, enter[0], enter[1]);
+      const op = expoOut(p);
+      const ty = lerp(p, 20, 0, expoOut);
+      panel.style.opacity = op;
+      panel.style.transform = `translateY(${ty}px)`;
+    });
+
+    [el.nod1, el.nod2, el.nod3].forEach((n, i) => {
+      const { nod } = iterTimings[i];
+      const p = seg(t, nod[0], nod[1]);
+      const op = expoOut(p);
+      const scale = lerp(p, 0.4, 1.0, expoOut);
+      n.style.opacity = op;
+      n.style.transform = `scale(${scale})`;
+    });
+
+    // ────── Beat 3 · final chart crossfade (chart appears as halves fade)
+    const chartIn = seg(t, 8.5, 9.2);
+    el.finalChart.style.opacity = cubicOut(chartIn);
+
+    // Curve B draws first (our hero path, 8.8-9.8), curve A follows (9.0-9.6 flat + spike)
+    const curveBT = seg(t, 8.8, 9.8);
+    el.curveB.style.strokeDashoffset = lenB * (1 - expoOut(curveBT));
+    el.curveBGlow.style.strokeDashoffset = lenB * (1 - expoOut(curveBT));
+
+    const curveAT = seg(t, 8.9, 9.7);
+    el.curveA.style.strokeDashoffset = lenA * (1 - cubicOut(curveAT));
+    // Crash dash — only after curveA reaches peak AND the X lands
+    const curveACrashT = seg(t, 9.7, 9.95);
+    el.curveACrash.style.strokeDashoffset = lenACrash * (1 - expoOut(curveACrashT));
+    // Fail X pops in right when curve A hits the spike
+    const failXT = seg(t, 9.65, 9.85);
+    const failXEl = document.getElementById('failX');
+    if (failXEl) {
+      failXEl.style.opacity = cubicOut(failXT);
+      failXEl.style.transform = `scale(${lerp(failXT, 1.6, 1.0, expoOut)})`;
+      failXEl.style.transformOrigin = '1140px 180px';
+    }
+
+    // Dots fade in progressively (skip the fail-dot which is handled via X)
+    chartDots.forEach((dot, i) => {
+      // curve-dot for B (3 dots), fail-dot (1 dot)
+      const dotT = seg(t, 9.0 + i * 0.12, 9.3 + i * 0.12);
+      dot.style.opacity = cubicOut(dotT);
+    });
+    chartAnchors.forEach((a) => {
+      const aT = seg(t, 9.5, 9.95);
+      a.style.opacity = cubicOut(aT);
+    });
+    chartTicks.forEach((tk) => {
+      const tkT = seg(t, 8.7, 9.3);
+      tk.style.opacity = cubicOut(tkT) * 0.9;
+    });
+
+    // ────── Brand reveal 10.0-12.0
+    const sheetT = seg(t, 10.0, 10.6);
+    el.brandSheet.style.transform = `translateY(${lerp(sheetT, 100, 0, expoOut)}%)`;
+
+    const wordT = seg(t, 10.6, 11.4);
+    el.brandReveal.style.opacity = cubicOut(wordT);
+
+    const underT = seg(t, 11.4, 11.9);
+    el.brandUnder.style.width = `${lerp(underT, 0, 280, expoOut)}px`;
+
+    // Mark ready for recorder
+    if (!window.__ready) window.__ready = true;
+
+    if (loop || t < DURATION) requestAnimationFrame(tick);
+  }
+
+  (document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve())
+    .then(() => requestAnimationFrame(tick));
 </script>
 </body>
 </html>

+ 647 - 0
demos/w3-fallback-advisor-en.html

@@ -0,0 +1,647 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8" />
+<title>w3 · Fallback Advisor (English)</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700;1,8..60,300..700&family=Inter:wght@200;300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<style>
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --dim: rgba(255,255,255,0.18);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;
+    --accent-deep: #B85D3D;
+    --cd-bg: #F5F4F0;
+    --cd-ink: #1A1918;
+
+    --serif-en: "Source Serif 4", Georgia, serif;
+    --sans: "Inter", -apple-system, system-ui, sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
+    -webkit-font-smoothing: antialiased;
+  }
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+  }
+
+  /* Watermarks */
+  .watermark-tl {
+    position: absolute;
+    top: 40px; left: 56px;
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,0.16);
+    z-index: 200;
+    pointer-events: none;
+    text-transform: uppercase;
+  }
+  .watermark-br {
+    position: absolute;
+    bottom: 32px; right: 40px;
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.24em;
+    color: rgba(255,255,255,0.14);
+    z-index: 200;
+    pointer-events: none;
+    text-transform: uppercase;
+  }
+
+  /* Top title — English uses Serif Display */
+  .top-title {
+    position: absolute;
+    top: 82px; left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--serif-en);
+    font-weight: 300;
+    font-size: 46px;
+    font-style: italic;
+    letter-spacing: -0.01em;
+    color: var(--ink-80);
+    text-align: center;
+    opacity: 0;
+    will-change: opacity, transform;
+    z-index: 120;
+    line-height: 1.12;
+  }
+  .top-title .accent { color: var(--accent); font-style: italic; }
+
+  .sub-caption {
+    position: absolute;
+    top: 148px; left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--sans);
+    font-weight: 300;
+    font-size: 13px;
+    letter-spacing: 0.34em;
+    color: var(--muted);
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity;
+    z-index: 120;
+  }
+
+  /* Philosophy wall */
+  .wall-viewport {
+    position: absolute;
+    top: 50%; left: 50%;
+    transform: translate(-50%, -50%);
+    width: 1480px;
+    height: 760px;
+    perspective: 2400px;
+    perspective-origin: 50% 50%;
+    will-change: transform, opacity, filter;
+  }
+  .wall-grid {
+    position: absolute;
+    inset: 0;
+    display: grid;
+    grid-template-columns: repeat(5, 1fr);
+    grid-template-rows: repeat(4, 1fr);
+    gap: 18px;
+    transform: rotateX(10deg) rotateY(-6deg);
+    transform-style: preserve-3d;
+    will-change: transform, opacity;
+  }
+  .cell {
+    position: relative;
+    background: #0f0f0f;
+    border: 1px solid var(--hairline);
+    border-radius: 8px;
+    overflow: hidden;
+    opacity: 0;
+    will-change: opacity, transform, filter;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    padding: 14px 16px;
+  }
+  .cell .glyph {
+    position: absolute;
+    inset: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    pointer-events: none;
+  }
+  .cell .name {
+    position: relative;
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.08em;
+    color: var(--muted);
+    z-index: 2;
+    align-self: flex-end;
+  }
+  .cell .num {
+    position: relative;
+    font-family: var(--mono);
+    font-size: 10px;
+    color: var(--dim);
+    letter-spacing: 0.1em;
+    z-index: 2;
+  }
+  .cell.selected {
+    border-color: var(--accent);
+    background: #1a0f0a;
+  }
+  .cell.selected .name { color: var(--accent); }
+
+  /* Scan light */
+  .scan-light {
+    position: absolute;
+    left: -5%;
+    right: -5%;
+    top: -15%;
+    height: 200px;
+    background: linear-gradient(
+      180deg,
+      rgba(217, 119, 87, 0) 0%,
+      rgba(217, 119, 87, 0.18) 40%,
+      rgba(255, 220, 200, 0.45) 50%,
+      rgba(217, 119, 87, 0.18) 60%,
+      rgba(217, 119, 87, 0) 100%
+    );
+    filter: blur(8px);
+    z-index: 80;
+    opacity: 0;
+    will-change: opacity, transform;
+    pointer-events: none;
+  }
+
+  /* Foreground 3 cards */
+  .fg-row {
+    position: absolute;
+    top: 50%; left: 50%;
+    transform: translate(-50%, -50%);
+    display: flex;
+    gap: 56px;
+    opacity: 0;
+    will-change: opacity;
+    z-index: 100;
+  }
+  .fg-card {
+    width: 440px;
+    display: flex;
+    flex-direction: column;
+    opacity: 0;
+    transform: translateZ(-800px) scale(0.4);
+    will-change: opacity, transform;
+  }
+  .fg-card .card-body {
+    background: #0f0f0f;
+    border: 1px solid var(--accent);
+    border-radius: 12px;
+    padding: 32px 30px;
+    box-shadow:
+      0 30px 80px -20px rgba(217,119,87,0.25),
+      0 10px 30px -10px rgba(0,0,0,0.6);
+  }
+  .fg-card .label {
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.18em;
+    color: var(--accent);
+    text-transform: uppercase;
+    margin-bottom: 14px;
+  }
+  .fg-card .title-main {
+    font-family: var(--serif-en);
+    font-style: italic;
+    font-size: 40px;
+    font-weight: 300;
+    letter-spacing: -0.01em;
+    line-height: 1.08;
+    color: var(--ink);
+    margin-bottom: 10px;
+  }
+  .fg-card .title-sub {
+    font-family: var(--sans);
+    font-weight: 300;
+    font-size: 14px;
+    letter-spacing: 0.14em;
+    text-transform: uppercase;
+    color: var(--ink-60);
+    margin-bottom: 22px;
+  }
+  .fg-card .feature {
+    font-family: var(--sans);
+    font-size: 13px;
+    font-weight: 300;
+    letter-spacing: 0.03em;
+    color: var(--muted);
+    line-height: 1.6;
+    padding-top: 18px;
+    border-top: 1px solid var(--hairline);
+    text-transform: uppercase;
+  }
+  .fg-card .thumb-wrap {
+    margin-top: 14px;
+    height: 0;
+    overflow: hidden;
+    border-radius: 10px;
+    background: #0a0a0a;
+    border: 1px solid var(--hairline);
+    opacity: 0;
+    will-change: opacity, height;
+  }
+  .fg-card .thumb-wrap img {
+    width: 100%;
+    display: block;
+  }
+
+  /* Brand reveal */
+  .brand-panel {
+    position: absolute;
+    inset: 0;
+    background: var(--cd-bg);
+    opacity: 0;
+    transform: translateY(100%);
+    will-change: opacity, transform;
+    z-index: 300;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-direction: column;
+  }
+  .brand-mark {
+    font-family: var(--serif-en);
+    font-style: italic;
+    font-weight: 300;
+    font-size: 112px;
+    letter-spacing: -0.02em;
+    color: var(--cd-ink);
+    opacity: 0;
+    transform: scale(0.92);
+    will-change: opacity, transform;
+    line-height: 1;
+  }
+  .brand-mark .dot { color: var(--accent); font-style: normal; padding: 0 6px; }
+  .brand-mark .accent { color: var(--accent); font-style: italic; }
+  .brand-underline {
+    margin-top: 34px;
+    height: 2px;
+    width: 0;
+    background: var(--accent);
+    will-change: width;
+  }
+  .brand-tag {
+    margin-top: 22px;
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.32em;
+    color: rgba(26,25,24,0.54);
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity;
+  }
+</style>
+</head>
+<body>
+
+<div class="stage" id="stage">
+
+  <div class="watermark-tl">HUASHU · DESIGN</div>
+  <div class="watermark-br">V2 · 2026 · w3</div>
+
+  <!-- English version: parallel rewrite, fewer words, more breathing room -->
+  <div class="top-title" id="topTitle">
+    Not sure? <span class="accent">Here are 3 roads.</span>
+  </div>
+  <div class="sub-caption" id="subCaption">20 Philosophies · 3 Directions</div>
+
+  <div class="scan-light" id="scanLight"></div>
+
+  <div class="wall-viewport" id="wallViewport">
+    <div class="wall-grid" id="wallGrid">
+      <!-- 20 cells injected by JS -->
+    </div>
+  </div>
+
+  <div class="fg-row" id="fgRow">
+    <div class="fg-card" id="card1">
+      <div class="card-body">
+        <div class="label">Road 01 · Eastern Space</div>
+        <div class="title-main">Kenya Hara</div>
+        <div class="title-sub">Ma / Emptiness</div>
+        <div class="feature">Terracotta · Vast whitespace · Paper grain</div>
+      </div>
+      <div class="thumb-wrap" id="thumb1">
+        <img src="demo-takram.png" alt="demo takram" />
+      </div>
+    </div>
+    <div class="fg-card" id="card2">
+      <div class="card-body">
+        <div class="label">Road 02 · Information Architecture</div>
+        <div class="title-main">Pentagram</div>
+        <div class="title-sub">Grid / Rigor</div>
+        <div class="feature">Strict grid · High contrast · Editorial</div>
+      </div>
+      <div class="thumb-wrap" id="thumb2">
+        <img src="demo-pentagram.png" alt="demo pentagram" />
+      </div>
+    </div>
+    <div class="fg-card" id="card3">
+      <div class="card-body">
+        <div class="label">Road 03 · Experimental Edge</div>
+        <div class="title-main">David Carson</div>
+        <div class="title-sub">Raw / Punk</div>
+        <div class="feature">Broken type · Brutal geometry · Visual shock</div>
+      </div>
+      <div class="thumb-wrap" id="thumb3">
+        <img src="demo-build.png" alt="demo build" />
+      </div>
+    </div>
+  </div>
+
+  <div class="brand-panel" id="brandPanel">
+    <div class="brand-mark" id="brandMark">huashu<span class="dot">·</span><span class="accent">design</span></div>
+    <div class="brand-underline" id="brandUnderline"></div>
+    <div class="brand-tag" id="brandTag">HTML as Designer's Medium</div>
+  </div>
+
+</div>
+
+<script>
+(function(){
+  function scaleStage(){
+    const stage = document.getElementById('stage');
+    const sx = window.innerWidth / 1920;
+    const sy = window.innerHeight / 1080;
+    const s = Math.min(sx, sy);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  window.addEventListener('resize', scaleStage);
+  scaleStage();
+
+  // 20 philosophies — identical structure to zh.html (designer names are brand identifiers, kept as-is)
+  const PHILOSOPHIES = [
+    { name: 'Pentagram',    glyph: 'grid' },
+    { name: 'M. Vignelli',  glyph: 'bars' },
+    { name: 'Apple HIG',    glyph: 'radius' },
+    { name: 'Spin',         glyph: 'slash' },
+    { name: 'Build',        glyph: 'type' },
+    { name: 'Field.io',     glyph: 'wave' },
+    { name: 'Active Theory',glyph: 'orbit' },
+    { name: 'Hi-Res!',      glyph: 'dots' },
+    { name: 'Locomotive',   glyph: 'arrow' },
+    { name: 'Takram',       glyph: 'circle' },
+    { name: 'Kenya Hara',   glyph: 'ma' },
+    { name: 'D. Rams',      glyph: 'square' },
+    { name: 'J. Ive',       glyph: 'arc' },
+    { name: 'J. Morrison',  glyph: 'minimal' },
+    { name: 'S. Ogata',     glyph: 'line' },
+    { name: 'D. Carson',    glyph: 'collage' },
+    { name: 'S. Sagmeister',glyph: 'stamp' },
+    { name: 'P. Scher',     glyph: 'poster' },
+    { name: 'M. Glaser',    glyph: 'heart' },
+    { name: 'K. Sato',      glyph: 'logo' },
+  ];
+  const SELECTED = [10, 0, 15];
+
+  function makeGlyph(kind){
+    const svgs = {
+      grid: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1" fill="none">
+        <rect x="6" y="8" width="28" height="18"/><rect x="38" y="8" width="28" height="18"/><rect x="70" y="8" width="24" height="44"/>
+        <rect x="6" y="30" width="60" height="22"/></g></svg>`,
+      bars: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g fill="rgba(255,255,255,0.22)">
+        <rect x="10" y="40" width="8" height="16"/><rect x="22" y="28" width="8" height="28"/><rect x="34" y="16" width="8" height="40"/>
+        <rect x="46" y="24" width="8" height="32"/><rect x="58" y="10" width="8" height="46"/><rect x="70" y="34" width="8" height="22"/>
+        <rect x="82" y="22" width="8" height="34"/></g></svg>`,
+      radius: `<svg viewBox="0 0 100 60" width="72%" height="58%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none">
+        <rect x="14" y="10" width="72" height="40" rx="20" ry="20"/></g></svg>`,
+      slash: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.4" fill="none" stroke-linecap="square">
+        <path d="M 14 50 L 52 10"/><path d="M 36 50 L 74 10"/><path d="M 58 50 L 86 22"/></g></svg>`,
+      type: `<svg viewBox="0 0 100 60" width="78%" height="62%"><text x="50" y="42" text-anchor="middle" font-family="Source Serif 4, serif" font-size="40" font-style="italic" fill="rgba(255,255,255,0.22)">Aa</text></svg>`,
+      wave: `<svg viewBox="0 0 100 60" width="82%" height="62%"><path d="M 6 30 Q 20 8, 34 30 T 62 30 T 90 30" stroke="rgba(255,255,255,0.22)" stroke-width="1.3" fill="none"/></svg>`,
+      orbit: `<svg viewBox="0 0 100 60" width="74%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.1" fill="none"><ellipse cx="50" cy="30" rx="36" ry="14"/><ellipse cx="50" cy="30" rx="14" ry="22"/><circle cx="50" cy="30" r="2" fill="rgba(255,255,255,0.32)"/></g></svg>`,
+      dots: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g fill="rgba(255,255,255,0.22)"><circle cx="14" cy="18" r="2"/><circle cx="30" cy="18" r="2"/><circle cx="46" cy="18" r="2"/><circle cx="62" cy="18" r="2"/><circle cx="78" cy="18" r="2"/><circle cx="14" cy="30" r="2"/><circle cx="30" cy="30" r="2"/><circle cx="46" cy="30" r="3"/><circle cx="62" cy="30" r="2"/><circle cx="78" cy="30" r="2"/><circle cx="14" cy="42" r="2"/><circle cx="30" cy="42" r="2"/><circle cx="46" cy="42" r="2"/><circle cx="62" cy="42" r="2"/><circle cx="78" cy="42" r="2"/></g></svg>`,
+      arrow: `<svg viewBox="0 0 100 60" width="78%" height="52%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none" stroke-linecap="square"><path d="M 14 30 L 80 30"/><path d="M 68 18 L 82 30 L 68 42"/></g></svg>`,
+      circle: `<svg viewBox="0 0 100 60" width="62%" height="62%"><circle cx="50" cy="30" r="22" stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none"/></svg>`,
+      ma: `<svg viewBox="0 0 100 60" width="72%" height="62%"><g fill="none" stroke="rgba(255,255,255,0.22)" stroke-width="0.9"><rect x="18" y="14" width="64" height="32"/></g><circle cx="50" cy="30" r="1.4" fill="rgba(255,255,255,0.32)"/></svg>`,
+      square: `<svg viewBox="0 0 100 60" width="62%" height="62%"><rect x="30" y="10" width="40" height="40" stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none"/></svg>`,
+      arc: `<svg viewBox="0 0 100 60" width="78%" height="62%"><path d="M 14 46 Q 50 6, 86 46" stroke="rgba(255,255,255,0.22)" stroke-width="1.3" fill="none"/></svg>`,
+      minimal: `<svg viewBox="0 0 100 60" width="78%" height="32%"><line x1="18" y1="30" x2="82" y2="30" stroke="rgba(255,255,255,0.22)" stroke-width="1.2"/></svg>`,
+      line: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="0.9" fill="none"><line x1="14" y1="16" x2="86" y2="16"/><line x1="14" y1="30" x2="86" y2="30"/><line x1="14" y1="44" x2="60" y2="44"/></g></svg>`,
+      collage: `<svg viewBox="0 0 100 60" width="82%" height="62%"><g fill="none" stroke="rgba(255,255,255,0.22)" stroke-width="1"><rect x="8" y="8" width="24" height="18" transform="rotate(-8 20 17)"/><rect x="36" y="18" width="28" height="20" transform="rotate(5 50 28)"/><rect x="60" y="6" width="32" height="24" transform="rotate(-4 76 18)"/></g><text x="50" y="56" text-anchor="middle" font-family="Source Serif 4, serif" font-size="14" font-style="italic" fill="rgba(255,255,255,0.3)">RAY</text></svg>`,
+      stamp: `<svg viewBox="0 0 100 60" width="70%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none"><circle cx="50" cy="30" r="22"/><text x="50" y="35" text-anchor="middle" font-family="Source Serif 4" font-size="16" font-weight="500" fill="rgba(255,255,255,0.3)">S</text></g></svg>`,
+      poster: `<svg viewBox="0 0 100 60" width="82%" height="62%"><g fill="rgba(255,255,255,0.22)"><rect x="8" y="8" width="22" height="44"/><rect x="34" y="8" width="22" height="44"/><rect x="60" y="8" width="22" height="44"/></g></svg>`,
+      heart: `<svg viewBox="0 0 100 60" width="58%" height="58%"><path d="M 50 48 C 30 32, 18 20, 30 14 C 40 10, 50 22, 50 22 C 50 22, 60 10, 70 14 C 82 20, 70 32, 50 48 Z" fill="rgba(217,119,87,0.28)"/></svg>`,
+      logo: `<svg viewBox="0 0 100 60" width="60%" height="60%"><circle cx="50" cy="30" r="20" stroke="rgba(255,255,255,0.22)" stroke-width="1.3" fill="none"/><circle cx="50" cy="30" r="6" fill="rgba(255,255,255,0.22)"/></svg>`,
+    };
+    return svgs[kind] || svgs.minimal;
+  }
+
+  const wallGrid = document.getElementById('wallGrid');
+  PHILOSOPHIES.forEach((p, idx) => {
+    const cell = document.createElement('div');
+    cell.className = 'cell';
+    cell.dataset.idx = idx;
+    const row = Math.floor(idx / 5);
+    const col = idx % 5;
+    const dr = row - 1.5;
+    const dc = col - 2;
+    const dist = Math.sqrt(dr * dr + dc * dc);
+    cell.dataset.dist = dist.toFixed(3);
+    cell.innerHTML = `
+      <div class="glyph">${makeGlyph(p.glyph)}</div>
+      <div class="num">${String(idx + 1).padStart(2, '0')}</div>
+      <div class="name">${p.name}</div>
+    `;
+    wallGrid.appendChild(cell);
+  });
+
+  const cells = Array.from(wallGrid.querySelectorAll('.cell'));
+  const maxDist = Math.max(...cells.map(c => parseFloat(c.dataset.dist)));
+
+  const T_TOTAL = 12.0;
+  const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
+  const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2;
+  const cubicOut = t => 1 - Math.pow(1 - t, 3);
+  const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
+  const clamp01 = v => clamp(v, 0, 1);
+  const lerp = (a, b, t) => a + (b - a) * t;
+
+  const topTitle = document.getElementById('topTitle');
+  const subCap = document.getElementById('subCaption');
+  const wallViewport = document.getElementById('wallViewport');
+  const scanLight = document.getElementById('scanLight');
+  const fgRow = document.getElementById('fgRow');
+  const card1 = document.getElementById('card1');
+  const card2 = document.getElementById('card2');
+  const card3 = document.getElementById('card3');
+  const thumb1 = document.getElementById('thumb1');
+  const thumb2 = document.getElementById('thumb2');
+  const thumb3 = document.getElementById('thumb3');
+  const brandPanel = document.getElementById('brandPanel');
+  const brandMark = document.getElementById('brandMark');
+  const brandUnderline = document.getElementById('brandUnderline');
+  const brandTag = document.getElementById('brandTag');
+
+  function tick(t){
+    t = Math.max(0, Math.min(T_TOTAL, t));
+
+    // Ripple in 20 cells
+    const rippleStart = 0.15;
+    cells.forEach(cell => {
+      const d = parseFloat(cell.dataset.dist);
+      const delay = (d / maxDist) * 0.85;
+      const cellT = clamp01((t - rippleStart - delay * 0.55) / 0.7);
+      const eased = expoOut(cellT);
+      const idx = parseInt(cell.dataset.idx, 10);
+      const isSel = SELECTED.includes(idx);
+      cell.style.opacity = (eased * (isSel ? 1.0 : 0.85)).toFixed(3);
+      const ty = lerp(30, 0, eased);
+      const scale = lerp(0.88, 1, eased);
+      cell.style.transform = `translateY(${ty}px) scale(${scale})`;
+    });
+
+    // Scan light
+    const scanStart = 2.6;
+    const scanEnd = 4.0;
+    const scanT = clamp01((t - scanStart) / (scanEnd - scanStart));
+    if (scanT > 0 && scanT < 1) {
+      scanLight.style.opacity = Math.min(1, Math.sin(scanT * Math.PI) * 1.3).toFixed(3);
+      const py = lerp(-180, 820, cubicInOut(scanT));
+      scanLight.style.transform = `translateY(${py}px)`;
+    } else {
+      scanLight.style.opacity = 0;
+    }
+
+    // Light up selected, dim others
+    const lightStart = 4.0;
+    const lightEnd = 4.8;
+    const lightT = clamp01((t - lightStart) / (lightEnd - lightStart));
+    const lightE = expoOut(lightT);
+    cells.forEach(cell => {
+      const idx = parseInt(cell.dataset.idx, 10);
+      const isSel = SELECTED.includes(idx);
+      if (isSel) {
+        cell.classList.toggle('selected', lightT > 0.05);
+      } else {
+        if (t >= lightStart) {
+          const dimmedOpacity = lerp(0.85, 0.08, lightE);
+          cell.style.opacity = dimmedOpacity.toFixed(3);
+        }
+      }
+    });
+
+    // Foreground cards break out
+    const breakStart = 4.8;
+    if (t >= breakStart - 0.1) fgRow.style.opacity = 1;
+    else fgRow.style.opacity = 0;
+
+    [card1, card2, card3].forEach((card, i) => {
+      const stagger = i * 0.18;
+      const cT = clamp01((t - breakStart - stagger) / 0.85);
+      const cE = expoOut(cT);
+      card.style.opacity = cE.toFixed(3);
+      const tz = lerp(-800, 0, cE);
+      const sc = lerp(0.45, 1, cE);
+      const ty = lerp(40, 0, cE);
+      card.style.transform = `translateZ(${tz}px) scale(${sc}) translateY(${ty}px)`;
+    });
+
+    // Dim wall background
+    if (t >= breakStart) {
+      const dimT = clamp01((t - breakStart) / 0.9);
+      const dimE = expoOut(dimT);
+      wallViewport.style.opacity = lerp(1, 0.25, dimE).toFixed(3);
+      wallViewport.style.filter = `blur(${lerp(0, 6, dimE).toFixed(1)}px)`;
+    } else {
+      wallViewport.style.opacity = 1;
+      wallViewport.style.filter = 'blur(0px)';
+    }
+
+    // Demo thumbnails grow
+    const thumbStart = 6.6;
+    [thumb1, thumb2, thumb3].forEach((thumb, i) => {
+      const stagger = i * 0.32;
+      const ttT = clamp01((t - thumbStart - stagger) / 1.0);
+      const ttE = cubicOut(ttT);
+      thumb.style.opacity = ttE.toFixed(3);
+      const h = lerp(0, 250, ttE);
+      thumb.style.height = `${h}px`;
+    });
+
+    // Top title fade
+    const titleStart = 7.2;
+    const titleT = clamp01((t - titleStart) / 0.9);
+    const titleE = cubicOut(titleT);
+    topTitle.style.opacity = titleE.toFixed(3);
+    topTitle.style.transform = `translateX(-50%) translateY(${lerp(-14, 0, titleE)}px)`;
+    subCap.style.opacity = (titleE * 0.95).toFixed(3);
+
+    // Brand reveal
+    const brandStart = 9.8;
+    const panelT = clamp01((t - brandStart) / 0.7);
+    const panelE = expoOut(panelT);
+    brandPanel.style.opacity = panelE.toFixed(3);
+    brandPanel.style.transform = `translateY(${lerp(100, 0, panelE)}%)`;
+
+    const markStart = 10.3;
+    const markT = clamp01((t - markStart) / 0.6);
+    const markE = expoOut(markT);
+    brandMark.style.opacity = markE.toFixed(3);
+    brandMark.style.transform = `scale(${lerp(0.92, 1, markE)})`;
+
+    const ulStart = 10.7;
+    const ulT = clamp01((t - ulStart) / 0.55);
+    brandUnderline.style.width = `${lerp(0, 280, expoOut(ulT))}px`;
+
+    const tagStart = 11.1;
+    const tagT = clamp01((t - tagStart) / 0.5);
+    brandTag.style.opacity = cubicOut(tagT).toFixed(3);
+  }
+
+  window.__ready = false;
+  window.__duration = T_TOTAL;
+  let startTime = null;
+  let paused = false;
+  const recording = window.__recording === true;
+
+  function loop(now){
+    if (paused) return;
+    if (startTime === null) startTime = now;
+    const t = (now - startTime) / 1000;
+    tick(t);
+    if (t < T_TOTAL) requestAnimationFrame(loop);
+    else if (!recording) { startTime = now; requestAnimationFrame(loop); }
+  }
+
+  tick(0);
+  window.__ready = true;
+  requestAnimationFrame(loop);
+
+  window.__pause = function(){ paused = true; };
+  window.__resume = function(){
+    if (!paused) return;
+    paused = false; startTime = null;
+    requestAnimationFrame(loop);
+  };
+  window.__setTime = function(t){ paused = true; tick(t); };
+})();
+</script>
+
+</body>
+</html>

+ 669 - 712
demos/w3-fallback-advisor.html

@@ -1,747 +1,704 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
+<!doctype html>
+<html lang="zh-Hans">
 <head>
-<meta charset="UTF-8">
-<title>Huashu-Design · Fallback 设计顾问</title>
-<script crossorigin src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
-<script crossorigin src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
-<script src="https://unpkg.com/@babel/standalone@7.25.6/babel.min.js"></script>
+<meta charset="utf-8" />
+<title>w3 · Fallback Advisor(中文版)</title>
 <link rel="preconnect" href="https://fonts.googleapis.com">
 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,300;1,6..72,400;1,6..72,500&family=Noto+Serif+SC:wght@300;400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..700&family=Noto+Serif+SC:wght@300;400;500;600&family=Inter:wght@200;300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
 <style>
-  * { box-sizing: border-box; margin: 0; padding: 0; }
-  html, body { width: 100%; height: 100%; overflow: hidden; }
-  body {
-    background: #0c0c0c;
-    font-family: 'Newsreader', 'Noto Serif SC', Georgia, serif;
-    color: #1a1a1a;
+  :root {
+    --bg: #000000;
+    --ink: #FFFFFF;
+    --ink-80: rgba(255,255,255,0.82);
+    --ink-60: rgba(255,255,255,0.58);
+    --muted: rgba(255,255,255,0.40);
+    --dim: rgba(255,255,255,0.18);
+    --hairline: rgba(255,255,255,0.12);
+    --accent: #D97757;
+    --accent-deep: #B85D3D;
+    --cd-bg: #F5F4F0;
+    --cd-ink: #1A1918;
+
+    --serif-cn: "Noto Serif SC", "Songti SC", serif;
+    --serif-en: "Source Serif 4", Georgia, serif;
+    --sans: "Inter", -apple-system, "PingFang SC", system-ui, sans-serif;
+    --mono: "JetBrains Mono", "SF Mono", ui-monospace, monospace;
+  }
+  html, body {
+    margin: 0; padding: 0;
+    background: #000;
+    overflow: hidden;
+    font-family: var(--sans);
+    color: var(--ink);
     -webkit-font-smoothing: antialiased;
-    text-rendering: optimizeLegibility;
   }
-</style>
-</head>
-<body>
-<div id="root"></div>
-
-<!-- animations.jsx inlined -->
-<script type="text/babel">
-(function() {
-  const { createContext, useContext, useState, useEffect, useRef, useCallback } = React;
-  const TimeContext = createContext({ time: 0, duration: 10, playing: false });
-  const SpriteContext = createContext(null);
-
-  const Easing = {
-    linear: t => t,
-    easeIn: t => t * t,
-    easeOut: t => 1 - (1 - t) * (1 - t),
-    easeInOut: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
-    spring: t => {
-      const c = (2 * Math.PI) / 3;
-      return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c) + 1;
-    },
-  };
+  * { box-sizing: border-box; }
+
+  .stage {
+    position: fixed;
+    top: 50%; left: 50%;
+    width: 1920px; height: 1080px;
+    transform-origin: center center;
+    background: var(--bg);
+    overflow: hidden;
+  }
 
-  function interpolate(t, input, output, easing) {
-    const [inStart, inEnd] = input;
-    const [outStart, outEnd] = output;
-    if (t <= inStart) return outStart;
-    if (t >= inEnd) return outEnd;
-    let progress = (t - inStart) / (inEnd - inStart);
-    if (easing) progress = easing(progress);
-    return outStart + (outEnd - outStart) * progress;
-  }
-
-  function useTime() { return useContext(TimeContext).time; }
-  function useSprite() {
-    const sprite = useContext(SpriteContext);
-    return sprite || { t: 0, elapsed: 0, duration: 0 };
-  }
-
-  function Stage({ duration = 10, width = 1920, height = 1080, loop = true, children, bgColor = '#fff' }) {
-    const [time, setTime] = useState(0);
-    const [playing, setPlaying] = useState(true);
-    const [scale, setScale] = useState(1);
-    const rafRef = useRef(null);
-    const effectiveLoop = (typeof window !== 'undefined' && window.__recording) ? false : loop;
-
-    useEffect(() => {
-      function updateScale() {
-        const vw = window.innerWidth;
-        const vh = window.innerHeight - 56;
-        const s = Math.min(vw / width, vh / height);
-        setScale(s);
-      }
-      updateScale();
-      window.addEventListener('resize', updateScale);
-      return () => window.removeEventListener('resize', updateScale);
-    }, [width, height]);
-
-    useEffect(() => {
-      if (!playing) return;
-      let cancelled = false;
-      let last = null;
-      function tick(now) {
-        if (cancelled) return;
-        if (last === null) {
-          last = now;
-          if (typeof window !== 'undefined') window.__ready = true;
-        }
-        const delta = (now - last) / 1000;
-        last = now;
-        setTime(prev => {
-          const next = prev + delta;
-          if (next >= duration) return effectiveLoop ? 0 : duration - 0.001;
-          return next;
-        });
-        rafRef.current = requestAnimationFrame(tick);
-      }
-      const start = () => { if (!cancelled) rafRef.current = requestAnimationFrame(tick); };
-      if (document.fonts && document.fonts.ready) document.fonts.ready.then(start); else start();
-      return () => { cancelled = true; cancelAnimationFrame(rafRef.current); };
-    }, [playing, duration, effectiveLoop]);
-
-    const progress = time / duration;
-    const ctx = { time, duration, playing, setPlaying, setTime };
-
-    const canvasStyle = {
-      position: 'absolute',
-      top: '50%',
-      left: '50%',
-      transformOrigin: 'center center',
-      width,
-      height,
-      background: bgColor,
-      overflow: 'hidden',
-      transform: `translate(-50%, -50%) scale(${scale})`,
-    };
+  /* ============ Watermark ============ */
+  .watermark-tl {
+    position: absolute;
+    top: 40px; left: 56px;
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.2em;
+    color: rgba(255,255,255,0.16);
+    z-index: 200;
+    pointer-events: none;
+    text-transform: uppercase;
+  }
+  .watermark-br {
+    position: absolute;
+    bottom: 32px; right: 40px;
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.24em;
+    color: rgba(255,255,255,0.14);
+    z-index: 200;
+    pointer-events: none;
+    text-transform: uppercase;
+  }
 
-    return (
-      <TimeContext.Provider value={ctx}>
-        <div style={{position:'fixed', inset:0, background:'#0c0c0c', display:'flex', flexDirection:'column'}}>
-          <div style={{flex:1, position:'relative', overflow:'hidden'}}>
-            <div style={canvasStyle}>{children}</div>
-          </div>
-          <div className="no-record" style={{position:'fixed', bottom:0, left:0, right:0, background:'rgba(0,0,0,0.8)', backdropFilter:'blur(10px)', padding:'12px 20px', display:'flex', alignItems:'center', gap:16, color:'#fff', fontSize:12, zIndex:100}}>
-            <button onClick={()=>setPlaying(p=>!p)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>{playing?'⏸ 暂停':'▶ 播放'}</button>
-            <button onClick={()=>setTime(0)} style={{background:'none', border:'1px solid rgba(255,255,255,0.3)', color:'#fff', padding:'6px 14px', borderRadius:4, cursor:'pointer', fontSize:12}}>⏮ 开始</button>
-            <div style={{fontFamily:'ui-monospace, monospace', fontVariantNumeric:'tabular-nums', minWidth:90}}>{time.toFixed(2)}s / {duration.toFixed(2)}s</div>
-            <div style={{flex:1, height:4, background:'rgba(255,255,255,0.2)', borderRadius:2, position:'relative'}}>
-              <div style={{position:'absolute', top:0, left:0, height:'100%', width:`${progress*100}%`, background:'#fff', borderRadius:2}} />
-            </div>
-          </div>
-        </div>
-      </TimeContext.Provider>
-    );
+  /* ============ Top Title ============ */
+  .top-title {
+    position: absolute;
+    top: 88px; left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--serif-cn);
+    font-weight: 300;
+    font-size: 42px;
+    letter-spacing: 0.02em;
+    color: var(--ink-80);
+    text-align: center;
+    opacity: 0;
+    will-change: opacity, transform;
+    z-index: 120;
+  }
+  .top-title .accent { color: var(--accent); font-weight: 400; }
+
+  .sub-caption {
+    position: absolute;
+    top: 148px; left: 50%;
+    transform: translateX(-50%);
+    font-family: var(--sans);
+    font-weight: 300;
+    font-size: 15px;
+    letter-spacing: 0.32em;
+    color: var(--muted);
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity;
+    z-index: 120;
+  }
+
+  /* ============ Philosophy Wall (4 rows × 5 cols) ============ */
+  .wall-viewport {
+    position: absolute;
+    top: 50%; left: 50%;
+    transform: translate(-50%, -50%);
+    width: 1480px;
+    height: 760px;
+    perspective: 2400px;
+    perspective-origin: 50% 50%;
+    will-change: transform, opacity, filter;
+  }
+  .wall-grid {
+    position: absolute;
+    inset: 0;
+    display: grid;
+    grid-template-columns: repeat(5, 1fr);
+    grid-template-rows: repeat(4, 1fr);
+    gap: 18px;
+    transform: rotateX(10deg) rotateY(-6deg);
+    transform-style: preserve-3d;
+    will-change: transform, opacity;
+  }
+  .cell {
+    position: relative;
+    background: #0f0f0f;
+    border: 1px solid var(--hairline);
+    border-radius: 8px;
+    overflow: hidden;
+    opacity: 0;
+    will-change: opacity, transform, filter;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    padding: 14px 16px;
   }
 
-  function Sprite({ start = 0, end, children, style }) {
-    const { time } = useContext(TimeContext);
-    const actualEnd = end == null ? Infinity : end;
-    if (time < start || time >= actualEnd) return null;
-    const duration = actualEnd - start;
-    const elapsed = time - start;
-    const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
-    const spriteValue = { t, elapsed, duration, start, end: actualEnd };
-    return (
-      <SpriteContext.Provider value={spriteValue}>
-        <div style={{position:'absolute', inset:0, ...style}}>{children}</div>
-      </SpriteContext.Provider>
+  /* abstract glyph per cell — geometric, no imagery */
+  .cell .glyph {
+    position: absolute;
+    inset: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    pointer-events: none;
+  }
+  .cell .name {
+    position: relative;
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.08em;
+    color: var(--muted);
+    z-index: 2;
+    align-self: flex-end;
+  }
+  .cell .num {
+    position: relative;
+    font-family: var(--mono);
+    font-size: 10px;
+    color: var(--dim);
+    letter-spacing: 0.1em;
+    z-index: 2;
+  }
+
+  /* Selected cells — lit up */
+  .cell.selected {
+    border-color: var(--accent);
+    background: #1a0f0a;
+  }
+  .cell.selected .name { color: var(--accent); }
+
+  /* ============ Scan light ============ */
+  .scan-light {
+    position: absolute;
+    left: -5%;
+    right: -5%;
+    top: -15%;
+    height: 200px;
+    background: linear-gradient(
+      180deg,
+      rgba(217, 119, 87, 0) 0%,
+      rgba(217, 119, 87, 0.18) 40%,
+      rgba(255, 220, 200, 0.45) 50%,
+      rgba(217, 119, 87, 0.18) 60%,
+      rgba(217, 119, 87, 0) 100%
     );
+    filter: blur(8px);
+    z-index: 80;
+    opacity: 0;
+    will-change: opacity, transform;
+    pointer-events: none;
   }
 
-  window.Animations = { Stage, Sprite, useTime, useSprite, Easing, interpolate };
-})();
-</script>
+  /* ============ Foreground 3 cards ============ */
+  .fg-row {
+    position: absolute;
+    top: 50%; left: 50%;
+    transform: translate(-50%, -50%);
+    display: flex;
+    gap: 56px;
+    opacity: 0;
+    will-change: opacity;
+    z-index: 100;
+  }
+  .fg-card {
+    width: 440px;
+    display: flex;
+    flex-direction: column;
+    align-items: stretch;
+    opacity: 0;
+    transform: translateZ(-800px) scale(0.4);
+    will-change: opacity, transform;
+  }
+  .fg-card .card-body {
+    background: #0f0f0f;
+    border: 1px solid var(--accent);
+    border-radius: 12px;
+    padding: 32px 30px;
+    box-shadow:
+      0 30px 80px -20px rgba(217,119,87,0.25),
+      0 10px 30px -10px rgba(0,0,0,0.6);
+  }
+  .fg-card .label {
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.18em;
+    color: var(--accent);
+    text-transform: uppercase;
+    margin-bottom: 14px;
+  }
+  .fg-card .title-cn {
+    font-family: var(--serif-cn);
+    font-size: 36px;
+    font-weight: 400;
+    letter-spacing: 0.01em;
+    line-height: 1.15;
+    color: var(--ink);
+    margin-bottom: 10px;
+  }
+  .fg-card .title-en {
+    font-family: var(--serif-en);
+    font-style: italic;
+    font-weight: 300;
+    font-size: 17px;
+    letter-spacing: 0.01em;
+    color: var(--ink-60);
+    margin-bottom: 22px;
+  }
+  .fg-card .feature {
+    font-family: var(--sans);
+    font-size: 14px;
+    font-weight: 300;
+    letter-spacing: 0.02em;
+    color: var(--muted);
+    line-height: 1.6;
+    padding-top: 18px;
+    border-top: 1px solid var(--hairline);
+  }
+  .fg-card .thumb-wrap {
+    margin-top: 14px;
+    height: 0;
+    overflow: hidden;
+    border-radius: 10px;
+    background: #0a0a0a;
+    border: 1px solid var(--hairline);
+    opacity: 0;
+    will-change: opacity, height;
+  }
+  .fg-card .thumb-wrap img {
+    width: 100%;
+    display: block;
+  }
 
-<!-- Demo scene -->
-<script type="text/babel">
-const { Stage, Sprite, useTime, useSprite, Easing, interpolate } = window.Animations;
-
-// ── Design tokens ─────────────────────────────────────────
-const CREAM = '#FAF6EF';
-const INK = '#1a1a1a';
-const TERRA = '#C04A1A';
-const ASH = '#6b6b6b';
-const LINE = '#d9d2c5';
-
-// ── 20 design philosophies ────────────────────────────────
-const PHILOSOPHIES = [
-  { n: 'Pentagram', school: '信息建筑', en: 'INFO-ARCH' },
-  { n: 'Massimo Vignelli', school: '信息建筑', en: 'INFO-ARCH' },
-  { n: 'Dieter Rams', school: '信息建筑', en: 'INFO-ARCH' },
-  { n: 'Otl Aicher', school: '信息建筑', en: 'INFO-ARCH' },
-  { n: 'Field.io', school: '运动诗学', en: 'KINETIC' },
-  { n: 'Active Theory', school: '运动诗学', en: 'KINETIC' },
-  { n: 'Locomotive', school: '运动诗学', en: 'KINETIC' },
-  { n: 'Joshua Davis', school: '运动诗学', en: 'KINETIC' },
-  { n: 'Kenya Hara', school: '东方哲学', en: 'EASTERN' },
-  { n: 'Naoto Fukasawa', school: '东方哲学', en: 'EASTERN' },
-  { n: 'Kashiwa Sato', school: '东方哲学', en: 'EASTERN' },
-  { n: 'John Maeda', school: '东方哲学', en: 'EASTERN' },
-  { n: 'Sagmeister', school: '实验先锋', en: 'AVANT' },
-  { n: 'David Carson', school: '实验先锋', en: 'AVANT' },
-  { n: 'Paula Scher', school: '实验先锋', en: 'AVANT' },
-  { n: 'Tomato', school: '实验先锋', en: 'AVANT' },
-  { n: 'Dan Flavin', school: '极简主义', en: 'MINIMAL' },
-  { n: 'Ryuichi Sakamoto', school: '极简主义', en: 'MINIMAL' },
-  { n: 'Agnes Martin', school: '极简主义', en: 'MINIMAL' },
-  { n: 'Donald Judd', school: '极简主义', en: 'MINIMAL' },
-];
-
-const SELECTED_INDICES = [0, 4, 8]; // Pentagram, Field.io, Kenya Hara
-
-// ── Shared typography helpers ─────────────────────────────
-const serif = "'Newsreader', 'Noto Serif SC', Georgia, serif";
-const sans = "'Inter', -apple-system, sans-serif";
-const mono = "'JetBrains Mono', ui-monospace, monospace";
-
-// ── Scene 1: Vague brief (0 – 3.5s) ───────────────────────
-function Scene1_VagueBrief() {
-  const { t, elapsed } = useSprite();
-  const charCount = Math.floor(interpolate(elapsed, [0.3, 1.8], [0, 9]));
-  const text = '做个好看的页面'.slice(0, charCount);
-  const cursorBlink = Math.floor(elapsed * 2.4) % 2 === 0;
-  const questionOpacity = interpolate(elapsed, [1.8, 2.4], [0, 1]);
-  const questionBob = Math.sin(elapsed * 4) * 6;
-  const fadeOut = interpolate(elapsed, [2.8, 3.5], [1, 0], Easing.easeIn);
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
-      display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
-      <div style={{fontFamily: sans, fontSize:14, letterSpacing:'0.3em',
-        color: ASH, marginBottom: 40}}>
-        用户需求
-      </div>
-      <div style={{display:'flex', alignItems:'flex-start', gap: 36}}>
-        <div style={{fontFamily: serif, fontStyle: 'italic', fontSize: 120,
-          color: TERRA, lineHeight: 1, marginTop: -20}}>「</div>
-        <div style={{fontFamily: serif, fontSize: 96, fontWeight: 400,
-          color: INK, letterSpacing: '0.02em', position: 'relative'}}>
-          {text}
-          <span style={{opacity: cursorBlink ? 1 : 0, color: TERRA,
-            marginLeft: 4, fontWeight: 300}}>|</span>
-        </div>
-        <div style={{fontFamily: serif, fontStyle: 'italic', fontSize: 120,
-          color: TERRA, lineHeight: 1, marginTop: -20}}>」</div>
-      </div>
-      <div style={{fontFamily: sans, fontSize: 20, color: ASH, marginTop: 60,
-        opacity: questionOpacity, transform: `translateY(${questionBob}px)`,
-        letterSpacing: '0.05em'}}>
-        <span style={{color: TERRA, fontSize: 28, marginRight: 12}}>?</span>
-        风格、受众、情感基调—— 都没说
-      </div>
-    </div>
-  );
-}
-
-// ── Scene 2: Advisor activates (3.5 – 6.5s) ───────────────
-function Scene2_AdvisorIntro() {
-  const { elapsed } = useSprite();
-  const mainY = interpolate(elapsed, [0, 1.2], [40, 0], Easing.easeOut);
-  const mainOpacity = interpolate(elapsed, [0, 0.8], [0, 1]);
-  const lineWidth = interpolate(elapsed, [0.8, 1.8], [0, 320]);
-  const subOpacity = interpolate(elapsed, [1.2, 2], [0, 1]);
-  const fadeOut = interpolate(elapsed, [2.5, 3], [1, 0]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeOut,
-      display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
-      <div style={{fontFamily: sans, fontSize: 12, letterSpacing: '0.4em',
-        color: TERRA, marginBottom: 24, opacity: mainOpacity}}>
-        设计方向顾问 · Fallback
-      </div>
-      <div style={{fontFamily: serif, fontSize: 132, fontWeight: 500,
-        color: INK, lineHeight: 1, letterSpacing: '-0.01em',
-        opacity: mainOpacity, transform: `translateY(${mainY}px)`}}>
-        推荐 <span style={{fontStyle:'italic', color: TERRA}}>3</span> 个方向
-      </div>
-      <div style={{height: 1, background: INK, width: lineWidth, marginTop: 36}} />
-      <div style={{fontFamily: serif, fontStyle: 'italic', fontSize: 26,
-        color: ASH, marginTop: 28, opacity: subOpacity}}>
-        从 20 种设计哲学里,按 5 个不同流派差异化推荐
-      </div>
-    </div>
-  );
-}
-
-// ── Scene 3: 20 philosophies grid scan (6.5 – 10.5s) ──────
-function Scene3_GridScan() {
-  const { elapsed } = useSprite();
-  const titleOp = interpolate(elapsed, [0, 0.4], [0, 1]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM,
-      padding: '80px 120px', display:'flex', flexDirection:'column'}}>
-      <div style={{display:'flex', justifyContent:'space-between',
-        alignItems:'baseline', opacity: titleOp, marginBottom: 50}}>
-        <div style={{fontFamily: serif, fontSize: 52, fontWeight: 500, color: INK}}>
-          设计哲学库
-        </div>
-        <div style={{fontFamily: mono, fontSize: 14, color: ASH, letterSpacing:'0.1em'}}>
-          20 位设计师 · 5 个流派
-        </div>
-      </div>
-      <div style={{display:'grid', gridTemplateColumns:'repeat(5, 1fr)', gap: 20, flex: 1}}>
-        {PHILOSOPHIES.map((p, i) => {
-          const stagger = i * 0.06;
-          const appearT = Math.max(0, Math.min(1, (elapsed - 0.5 - stagger) / 0.4));
-          const op = appearT;
-          const ty = (1 - appearT) * 24;
-
-          // Scanner highlight: sweeps through 20 cards from t=2.2 to t=3.2
-          const scannerStart = 2.2 + i * 0.04;
-          const scannerEnd = scannerStart + 0.25;
-          const scanHighlight = elapsed > scannerStart && elapsed < scannerEnd ? 1 : 0;
-
-          // Selected cards get circled at t=3.3+
-          const isSelected = SELECTED_INDICES.includes(i);
-          const selectT = Math.max(0, Math.min(1, (elapsed - 3.3) / 0.5));
-          const selectOp = isSelected ? selectT : 0;
-          const selectDim = !isSelected && elapsed > 3.4 ? interpolate(elapsed, [3.4, 3.8], [1, 0.28]) : 1;
-
-          return (
-            <div key={i} style={{
-              opacity: op * selectDim,
-              transform: `translateY(${ty}px)`,
-              background: scanHighlight ? '#fff' : 'transparent',
-              border: `1px solid ${isSelected && selectT > 0.3 ? TERRA : LINE}`,
-              borderWidth: isSelected && selectT > 0.3 ? 2 : 1,
-              padding: '20px 18px',
-              position: 'relative',
-              transition: 'none',
-            }}>
-              <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-                letterSpacing: '0.15em', marginBottom: 10}}>
-                {String(i+1).padStart(2,'0')} · {p.en}
-              </div>
-              <div style={{fontFamily: serif, fontSize: 22, fontWeight: 500,
-                color: INK, lineHeight: 1.15, marginBottom: 6}}>
-                {p.n}
-              </div>
-              <div style={{fontFamily: serif, fontStyle: 'italic', fontSize: 14,
-                color: ASH}}>
-                {p.school}
-              </div>
-              {isSelected && selectT > 0.4 && (
-                <div style={{position:'absolute', top: -10, right: -10,
-                  width: 26, height: 26, borderRadius: '50%', background: TERRA,
-                  color: '#fff', display:'flex', alignItems:'center',
-                  justifyContent:'center', fontFamily: serif, fontSize: 14,
-                  fontWeight: 600, opacity: selectOp}}>
-                  {SELECTED_INDICES.indexOf(i) + 1}
-                </div>
-              )}
-            </div>
-          );
-        })}
-      </div>
-    </div>
-  );
-}
-
-// ── Scene 4: Three-panel parallel demo generation (10.5 – 19s) ──
-function Scene4_ParallelDemos() {
-  const { elapsed } = useSprite();
-  const slideIn = interpolate(elapsed, [0, 1], [200, 0], Easing.easeOut);
-  const opacity = interpolate(elapsed, [0, 0.6], [0, 1]);
-
-  const panels = [
-    { name: 'Pentagram', school: '信息建筑派', en: 'Information Architecture',
-      delay: 0, render: 'pentagram' },
-    { name: 'Field.io', school: '运动诗学派', en: 'Kinetic Poetry',
-      delay: 0.3, render: 'field' },
-    { name: 'Kenya Hara', school: '东方哲学派', en: 'Eastern Minimalism',
-      delay: 0.6, render: 'hara' },
-  ];
+  /* ============ Brand Reveal (米色盖层) ============ */
+  .brand-panel {
+    position: absolute;
+    inset: 0;
+    background: var(--cd-bg);
+    opacity: 0;
+    transform: translateY(100%);
+    will-change: opacity, transform;
+    z-index: 300;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-direction: column;
+  }
+  .brand-mark {
+    font-family: var(--serif-en);
+    font-style: italic;
+    font-weight: 300;
+    font-size: 112px;
+    letter-spacing: -0.02em;
+    color: var(--cd-ink);
+    opacity: 0;
+    transform: scale(0.92);
+    will-change: opacity, transform;
+    line-height: 1;
+  }
+  .brand-mark .accent { color: var(--accent); font-style: italic; }
+  .brand-mark .dot { color: var(--accent); font-style: normal; padding: 0 6px; }
+  .brand-underline {
+    margin-top: 34px;
+    height: 2px;
+    width: 0;
+    background: var(--accent);
+    will-change: width;
+  }
+  .brand-tag {
+    margin-top: 22px;
+    font-family: var(--mono);
+    font-size: 12px;
+    letter-spacing: 0.32em;
+    color: rgba(26,25,24,0.54);
+    text-transform: uppercase;
+    opacity: 0;
+    will-change: opacity;
+  }
+</style>
+</head>
+<body>
 
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM,
-      padding: '60px 60px 40px', display:'flex', flexDirection:'column',
-      opacity}}>
-      <div style={{display:'flex', justifyContent:'space-between',
-        alignItems:'baseline', marginBottom: 28}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-            letterSpacing: '0.3em', marginBottom: 4}}>步骤 3 / 4</div>
-          <div style={{fontFamily: serif, fontSize: 44, fontWeight: 500, color: INK}}>
-            并行生成视觉 Demo
-          </div>
-        </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18,
-          color: ASH, textAlign: 'right'}}>
-          "看到比说到更有效"<br/>
-          <span style={{fontSize: 14}}>— 设计顾问模式 · Phase 5</span>
-        </div>
-      </div>
+<div class="stage" id="stage">
 
-      <div style={{display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap: 24,
-        flex: 1, transform: `translateY(${slideIn}px)`}}>
-        {panels.map((p, i) => (
-          <DemoPanel key={i} panel={p} localElapsed={elapsed - p.delay} />
-        ))}
-      </div>
-    </div>
-  );
-}
-
-function DemoPanel({ panel, localElapsed }) {
-  const progressT = Math.max(0, Math.min(1, localElapsed / 3.0));
-  const progressPct = progressT * 100;
-  const done = progressT >= 0.92;
-  // Content fades in during the last 0.7s of generation — overlaps with
-  // skeleton fade-out so there's no empty-canvas gap when "READY" appears.
-  const contentReveal = interpolate(localElapsed, [2.4, 3.2], [0, 1], Easing.easeOut);
-  const skeletonOp = interpolate(localElapsed, [2.4, 3.2], [1, 0], Easing.easeOut);
-
-  return (
-    <div style={{
-      background:'#fff',
-      border: `1px solid ${LINE}`,
-      display:'flex', flexDirection:'column',
-      position:'relative',
-    }}>
-      {/* Header */}
-      <div style={{padding: '18px 22px', borderBottom: `1px solid ${LINE}`,
-        display:'flex', justifyContent:'space-between', alignItems:'baseline'}}>
-        <div>
-          <div style={{fontFamily: serif, fontSize: 28, fontWeight: 500, color: INK}}>
-            {panel.name}
-          </div>
-          <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 13,
-            color: ASH, marginTop: 2}}>
-            {panel.en}
-          </div>
-        </div>
-        <div style={{fontFamily: mono, fontSize: 10, color: done ? TERRA : ASH,
-          letterSpacing: '0.15em'}}>
-          {done ? '✓ READY' : 'GENERATING'}
-        </div>
-      </div>
+  <!-- 水印 -->
+  <div class="watermark-tl">HUASHU · DESIGN</div>
+  <div class="watermark-br">V2 · 2026 · w3</div>
 
-      {/* Canvas */}
-      <div style={{flex: 1, position: 'relative', overflow: 'hidden'}}>
-        {skeletonOp > 0.02 && (
-          <div style={{position:'absolute', inset:0, opacity: skeletonOp}}>
-            <GenerationSkeleton progress={progressT} />
-          </div>
-        )}
-        <div style={{position:'absolute', inset:0, opacity: contentReveal}}>
-          {panel.render === 'pentagram' && <PentagramDemo />}
-          {panel.render === 'field' && <FieldDemo elapsed={localElapsed - 3.2} />}
-          {panel.render === 'hara' && <HaraDemo />}
-        </div>
-      </div>
+  <!-- 顶部标题 -->
+  <div class="top-title" id="topTitle">
+    不知道要什么?<span class="accent">先给你 3 个方向</span>
+  </div>
+  <div class="sub-caption" id="subCaption">20 Philosophies · 3 Directions</div>
 
-      {/* Progress bar */}
-      <div style={{height: 2, background: '#eee', position: 'relative'}}>
-        <div style={{position:'absolute', top:0, left:0, height:'100%',
-          width: `${progressPct}%`, background: TERRA,
-          transition:'none'}} />
-      </div>
-    </div>
-  );
-}
-
-function GenerationSkeleton({ progress }) {
-  const bars = [60, 85, 40, 72, 90, 55, 68];
-  return (
-    <div style={{padding: 24, display:'flex', flexDirection:'column', gap: 14}}>
-      {bars.map((w, i) => (
-        <div key={i} style={{height: 10, width: `${w}%`,
-          background: `linear-gradient(90deg, ${LINE} 0%, ${LINE} ${100-progress*80}%, #fff ${100-progress*80}%)`,
-          opacity: 0.6 + progress * 0.4}} />
-      ))}
-      <div style={{fontFamily: mono, fontSize: 10, color: ASH,
-        marginTop: 20, letterSpacing:'0.1em'}}>
-        {progress < 0.3 && '▸ loading style tokens...'}
-        {progress >= 0.3 && progress < 0.6 && '▸ composing layout...'}
-        {progress >= 0.6 && progress < 0.9 && '▸ applying typography...'}
-        {progress >= 0.9 && '▸ finalizing...'}
-      </div>
+  <!-- 扫描光 -->
+  <div class="scan-light" id="scanLight"></div>
+
+  <!-- 4×5 哲学墙 -->
+  <div class="wall-viewport" id="wallViewport">
+    <div class="wall-grid" id="wallGrid">
+      <!-- 20 cells injected by JS -->
     </div>
-  );
-}
-
-// ── Pentagram demo: serif editorial, strict grid, monochrome ──
-function PentagramDemo() {
-  return (
-    <div style={{padding: '28px 28px 24px', background:'#fafafa', height:'100%',
-      display:'flex', flexDirection:'column', fontFamily: serif, color:'#111'}}>
-      <div style={{display:'flex', justifyContent:'space-between',
-        borderBottom:'1px solid #111', paddingBottom: 10, marginBottom: 16,
-        fontFamily: mono, fontSize: 9, letterSpacing:'0.2em'}}>
-        <span>VOL. 01 · MMXXVI</span>
-        <span>NO. 043</span>
-      </div>
-      <div style={{fontFamily: mono, fontSize: 10, letterSpacing:'0.3em',
-        color:'#888', marginBottom: 10}}>ESSAY</div>
-      <div style={{fontSize: 40, lineHeight: 1.05, fontWeight: 500,
-        letterSpacing: '-0.02em', marginBottom: 18}}>
-        A Pure<br/>
-        <span style={{fontStyle:'italic'}}>Information</span><br/>
-        Architecture
-      </div>
-      <div style={{height: 1, background:'#111', margin:'8px 0 14px'}} />
-      <div style={{fontSize: 13, lineHeight: 1.55, color:'#333', flex: 1}}>
-        Designed not to impress, but to inform. The grid carries meaning; typography does the work.
+  </div>
+
+  <!-- 前景 3 张方向卡 -->
+  <div class="fg-row" id="fgRow">
+    <!-- card 1: Kenya Hara · 东方极简 -->
+    <div class="fg-card" id="card1">
+      <div class="card-body">
+        <div class="label">方向 01 · 东方空间</div>
+        <div class="title-cn">原研哉式留白</div>
+        <div class="title-en">Kenya Hara</div>
+        <div class="feature">赤土橙 · 大量留白 · 宣纸质感</div>
       </div>
-      <div style={{borderTop:'1px solid #111', paddingTop: 10, marginTop: 14,
-        display:'flex', justifyContent:'space-between', fontFamily: mono,
-        fontSize: 9, letterSpacing:'0.2em', color:'#888'}}>
-        <span>NEW YORK</span>
-        <span>PENTAGRAM</span>
+      <div class="thumb-wrap" id="thumb1">
+        <img src="demo-takram.png" alt="demo takram" />
       </div>
     </div>
-  );
-}
-
-// ── Field.io demo: dark, kinetic geometric shapes ──
-function FieldDemo({ elapsed }) {
-  const e = Math.max(0, elapsed || 0);
-  return (
-    <div style={{padding: 0, background:'#0e1016', height:'100%',
-      position:'relative', overflow:'hidden'}}>
-      <svg viewBox="0 0 400 500" width="100%" height="100%"
-        style={{position:'absolute', inset:0}} preserveAspectRatio="xMidYMid slice">
-        <defs>
-          <linearGradient id="fg1" x1="0" y1="0" x2="1" y2="1">
-            <stop offset="0%" stopColor="#ff6a3d" />
-            <stop offset="100%" stopColor="#c04a1a" />
-          </linearGradient>
-          <linearGradient id="fg2" x1="0" y1="0" x2="1" y2="1">
-            <stop offset="0%" stopColor="#4a9eff" />
-            <stop offset="100%" stopColor="#1a4fc0" />
-          </linearGradient>
-        </defs>
-        {/* Concentric circles breathing */}
-        {[0, 1, 2, 3].map(i => (
-          <circle key={i} cx="200" cy="280"
-            r={40 + i * 50 + Math.sin(e * 1.2 + i) * 10}
-            fill="none" stroke="url(#fg1)" strokeWidth={1.5}
-            opacity={0.4 - i * 0.08} />
-        ))}
-        {/* Rotating triangle */}
-        <g transform={`translate(200 280) rotate(${e * 20})`}>
-          <polygon points="0,-70 60,35 -60,35" fill="url(#fg2)" opacity="0.7" />
-        </g>
-        {/* Orbiting dots */}
-        {[0, 1, 2, 3, 4, 5].map(i => {
-          const angle = (e * 0.8 + i * Math.PI / 3);
-          return <circle key={i} cx={200 + Math.cos(angle) * 150}
-            cy={280 + Math.sin(angle) * 150} r={4} fill="#ff6a3d" opacity={0.9}/>;
-        })}
-      </svg>
-      <div style={{position:'absolute', top: 24, left: 24, right: 24,
-        display:'flex', justifyContent:'space-between',
-        fontFamily: mono, fontSize: 10, letterSpacing:'0.3em', color:'#fff', opacity: 0.7}}>
-        <span>FIELD.IO</span>
-        <span>LIVE · RECORDING</span>
+    <!-- card 2: Pentagram · 信息建筑 -->
+    <div class="fg-card" id="card2">
+      <div class="card-body">
+        <div class="label">方向 02 · 信息建筑</div>
+        <div class="title-cn">Pentagram 秩序</div>
+        <div class="title-en">Pentagram</div>
+        <div class="feature">强网格 · 高对比 · 理性版式</div>
       </div>
-      <div style={{position:'absolute', bottom: 24, left: 24, right: 24,
-        fontFamily: serif, fontStyle:'italic', fontSize: 20, color:'#fff',
-        letterSpacing:'0.02em'}}>
-        kinetic identity<br/>
-        <span style={{fontFamily: mono, fontSize: 10, fontStyle:'normal',
-          letterSpacing:'0.2em', color:'#ff6a3d', opacity: 0.8}}>
-          / motion is the brand
-        </span>
+      <div class="thumb-wrap" id="thumb2">
+        <img src="demo-pentagram.png" alt="demo pentagram" />
       </div>
     </div>
-  );
-}
-
-// ── Kenya Hara demo: vast white space, tiny dot, haiku ──
-function HaraDemo() {
-  return (
-    <div style={{padding: 0, background:'#fdfbf6', height:'100%',
-      position:'relative'}}>
-      <div style={{position:'absolute', top: 28, left: 32,
-        fontFamily: mono, fontSize: 10, letterSpacing:'0.3em', color:'#aaa'}}>
-        HARA · MMXXVI
-      </div>
-      <div style={{position:'absolute', top: '42%', left:'50%',
-        transform:'translate(-50%, -50%)', width: 14, height: 14,
-        borderRadius:'50%', background:'#1a1a1a'}} />
-      <div style={{position:'absolute', top:'58%', left:'50%',
-        transform:'translateX(-50%)', fontFamily: serif, fontStyle:'italic',
-        fontSize: 14, color:'#1a1a1a', letterSpacing:'0.1em'}}>
-        white.
-      </div>
-      <div style={{position:'absolute', bottom: 32, right: 32,
-        writingMode:'vertical-rl', fontFamily: "'Noto Serif SC', serif",
-        fontSize: 16, color:'#888', letterSpacing:'0.2em'}}>
-        原 研 哉
+    <!-- card 3: David Carson · 实验先锋 -->
+    <div class="fg-card" id="card3">
+      <div class="card-body">
+        <div class="label">方向 03 · 实验先锋</div>
+        <div class="title-cn">David Carson 式</div>
+        <div class="title-en">Experimental Edge</div>
+        <div class="feature">破格排印 · 粗野几何 · 视觉冲击</div>
       </div>
-      <div style={{position:'absolute', bottom: 28, left: 32,
-        fontFamily: serif, fontStyle:'italic', fontSize: 11, color:'#999',
-        maxWidth: 200, lineHeight: 1.6}}>
-        "Emptiness is not nothing—<br/>it is everything that could be."
+      <div class="thumb-wrap" id="thumb3">
+        <img src="demo-build.png" alt="demo build" />
       </div>
     </div>
-  );
-}
-
-// ── Scene 5: User selects Kenya Hara (19 – 22s) ───────────
-function Scene5_Select() {
-  const { elapsed } = useSprite();
-  // Cursor travels from right edge toward middle panel
-  const cursorX = interpolate(elapsed, [0, 1.2], [1750, 960], Easing.easeInOut);
-  const cursorY = interpolate(elapsed, [0, 1.2], [240, 540], Easing.easeInOut);
-  const cursorOp = interpolate(elapsed, [0, 0.2], [0, 1]);
-
-  // Middle panel selection lock-in
-  const selectLock = Math.max(0, Math.min(1, (elapsed - 1.2) / 0.4));
-
-  // Left + right panels dim + shrink
-  const sideDim = interpolate(elapsed, [1.2, 1.8], [1, 0.2]);
-  const sideScale = interpolate(elapsed, [1.2, 1.8], [1, 0.92], Easing.easeOut);
-
-  // Middle scales up
-  const midScale = interpolate(elapsed, [1.2, 1.8], [1, 1.06], Easing.easeOut);
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM,
-      padding: '60px 60px 40px', display:'flex', flexDirection:'column'}}>
-      <div style={{display:'flex', justifyContent:'space-between',
-        alignItems:'baseline', marginBottom: 28}}>
-        <div>
-          <div style={{fontFamily: mono, fontSize: 11, color: TERRA,
-            letterSpacing: '0.3em', marginBottom: 4}}>步骤 4 / 4</div>
-          <div style={{fontFamily: serif, fontSize: 44, fontWeight: 500, color: INK}}>
-            用户选定方向
-          </div>
-        </div>
-        <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 18, color: ASH}}>
-          ——或混合:"A 的配色 + C 的布局"
-        </div>
-      </div>
+  </div>
+
+  <!-- Brand Reveal -->
+  <div class="brand-panel" id="brandPanel">
+    <div class="brand-mark" id="brandMark">huashu<span class="dot">·</span><span class="accent">design</span></div>
+    <div class="brand-underline" id="brandUnderline"></div>
+    <div class="brand-tag" id="brandTag">HTML as Designer's Medium</div>
+  </div>
+
+</div>
+
+<script>
+(function(){
+  // ============ Stage auto-scale ============
+  function scaleStage(){
+    const stage = document.getElementById('stage');
+    const sx = window.innerWidth / 1920;
+    const sy = window.innerHeight / 1080;
+    const s = Math.min(sx, sy);
+    stage.style.transform = `translate(-50%, -50%) scale(${s})`;
+  }
+  window.addEventListener('resize', scaleStage);
+  scaleStage();
+
+  // ============ 20 Philosophies ============
+  // 4 rows × 5 cols = 20. Selected: idx 0 (Pentagram), idx 9 (Kenya Hara), idx 12 (David Carson)
+  const PHILOSOPHIES = [
+    // row 1 — 信息建筑派
+    { name: 'Pentagram',    glyph: 'grid' },
+    { name: 'M. Vignelli',  glyph: 'bars' },
+    { name: 'Apple HIG',    glyph: 'radius' },
+    { name: 'Spin',         glyph: 'slash' },
+    { name: 'Build',        glyph: 'type' },
+    // row 2 — 运动诗学派
+    { name: 'Field.io',     glyph: 'wave' },
+    { name: 'Active Theory',glyph: 'orbit' },
+    { name: 'Hi-Res!',      glyph: 'dots' },
+    { name: 'Locomotive',   glyph: 'arrow' },
+    { name: 'Takram',       glyph: 'circle' },
+    // row 3 — 极简/东方
+    { name: 'Kenya Hara',   glyph: 'ma' },
+    { name: 'D. Rams',      glyph: 'square' },
+    { name: 'J. Ive',       glyph: 'arc' },
+    { name: 'J. Morrison',  glyph: 'minimal' },
+    { name: 'S. Ogata',     glyph: 'line' },
+    // row 4 — 实验 & 海报
+    { name: 'D. Carson',    glyph: 'collage' },
+    { name: 'S. Sagmeister',glyph: 'stamp' },
+    { name: 'P. Scher',     glyph: 'poster' },
+    { name: 'M. Glaser',    glyph: 'heart' },
+    { name: 'K. Sato',      glyph: 'logo' },
+  ];
 
-      <div style={{display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap: 24,
-        flex: 1}}>
-        <StaticPanel which="pentagram" opacity={sideDim} scale={sideScale} />
-        <StaticPanel which="hara" opacity={1} scale={midScale} selected={selectLock > 0.5}/>
-        <StaticPanel which="field" opacity={sideDim} scale={sideScale} />
-      </div>
+  // selected indices — 3 differentiated directions
+  const SELECTED = [10, 0, 15]; // Kenya Hara, Pentagram, David Carson
+
+  function makeGlyph(kind){
+    // Simple geometric SVG glyphs — one per cell, no real logos
+    const svgs = {
+      grid: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1" fill="none">
+        <rect x="6" y="8" width="28" height="18"/><rect x="38" y="8" width="28" height="18"/><rect x="70" y="8" width="24" height="44"/>
+        <rect x="6" y="30" width="60" height="22"/></g></svg>`,
+      bars: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g fill="rgba(255,255,255,0.22)">
+        <rect x="10" y="40" width="8" height="16"/><rect x="22" y="28" width="8" height="28"/><rect x="34" y="16" width="8" height="40"/>
+        <rect x="46" y="24" width="8" height="32"/><rect x="58" y="10" width="8" height="46"/><rect x="70" y="34" width="8" height="22"/>
+        <rect x="82" y="22" width="8" height="34"/></g></svg>`,
+      radius: `<svg viewBox="0 0 100 60" width="72%" height="58%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none">
+        <rect x="14" y="10" width="72" height="40" rx="20" ry="20"/></g></svg>`,
+      slash: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.4" fill="none" stroke-linecap="square">
+        <path d="M 14 50 L 52 10"/><path d="M 36 50 L 74 10"/><path d="M 58 50 L 86 22"/></g></svg>`,
+      type: `<svg viewBox="0 0 100 60" width="78%" height="62%"><text x="50" y="42" text-anchor="middle" font-family="Source Serif 4, serif" font-size="40" font-style="italic" fill="rgba(255,255,255,0.22)">Aa</text></svg>`,
+      wave: `<svg viewBox="0 0 100 60" width="82%" height="62%"><path d="M 6 30 Q 20 8, 34 30 T 62 30 T 90 30" stroke="rgba(255,255,255,0.22)" stroke-width="1.3" fill="none"/></svg>`,
+      orbit: `<svg viewBox="0 0 100 60" width="74%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.1" fill="none"><ellipse cx="50" cy="30" rx="36" ry="14"/><ellipse cx="50" cy="30" rx="14" ry="22"/><circle cx="50" cy="30" r="2" fill="rgba(255,255,255,0.32)"/></g></svg>`,
+      dots: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g fill="rgba(255,255,255,0.22)"><circle cx="14" cy="18" r="2"/><circle cx="30" cy="18" r="2"/><circle cx="46" cy="18" r="2"/><circle cx="62" cy="18" r="2"/><circle cx="78" cy="18" r="2"/><circle cx="14" cy="30" r="2"/><circle cx="30" cy="30" r="2"/><circle cx="46" cy="30" r="3"/><circle cx="62" cy="30" r="2"/><circle cx="78" cy="30" r="2"/><circle cx="14" cy="42" r="2"/><circle cx="30" cy="42" r="2"/><circle cx="46" cy="42" r="2"/><circle cx="62" cy="42" r="2"/><circle cx="78" cy="42" r="2"/></g></svg>`,
+      arrow: `<svg viewBox="0 0 100 60" width="78%" height="52%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none" stroke-linecap="square"><path d="M 14 30 L 80 30"/><path d="M 68 18 L 82 30 L 68 42"/></g></svg>`,
+      circle: `<svg viewBox="0 0 100 60" width="62%" height="62%"><circle cx="50" cy="30" r="22" stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none"/></svg>`,
+      ma: `<svg viewBox="0 0 100 60" width="72%" height="62%"><g fill="none" stroke="rgba(255,255,255,0.22)" stroke-width="0.9"><rect x="18" y="14" width="64" height="32"/></g><circle cx="50" cy="30" r="1.4" fill="rgba(255,255,255,0.32)"/></svg>`,
+      square: `<svg viewBox="0 0 100 60" width="62%" height="62%"><rect x="30" y="10" width="40" height="40" stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none"/></svg>`,
+      arc: `<svg viewBox="0 0 100 60" width="78%" height="62%"><path d="M 14 46 Q 50 6, 86 46" stroke="rgba(255,255,255,0.22)" stroke-width="1.3" fill="none"/></svg>`,
+      minimal: `<svg viewBox="0 0 100 60" width="78%" height="32%"><line x1="18" y1="30" x2="82" y2="30" stroke="rgba(255,255,255,0.22)" stroke-width="1.2"/></svg>`,
+      line: `<svg viewBox="0 0 100 60" width="78%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="0.9" fill="none"><line x1="14" y1="16" x2="86" y2="16"/><line x1="14" y1="30" x2="86" y2="30"/><line x1="14" y1="44" x2="60" y2="44"/></g></svg>`,
+      collage: `<svg viewBox="0 0 100 60" width="82%" height="62%"><g fill="none" stroke="rgba(255,255,255,0.22)" stroke-width="1"><rect x="8" y="8" width="24" height="18" transform="rotate(-8 20 17)"/><rect x="36" y="18" width="28" height="20" transform="rotate(5 50 28)"/><rect x="60" y="6" width="32" height="24" transform="rotate(-4 76 18)"/></g><text x="50" y="56" text-anchor="middle" font-family="Source Serif 4, serif" font-size="14" font-style="italic" fill="rgba(255,255,255,0.3)">RAY</text></svg>`,
+      stamp: `<svg viewBox="0 0 100 60" width="70%" height="62%"><g stroke="rgba(255,255,255,0.22)" stroke-width="1.2" fill="none"><circle cx="50" cy="30" r="22"/><text x="50" y="35" text-anchor="middle" font-family="Source Serif 4" font-size="16" font-weight="500" fill="rgba(255,255,255,0.3)">S</text></g></svg>`,
+      poster: `<svg viewBox="0 0 100 60" width="82%" height="62%"><g fill="rgba(255,255,255,0.22)"><rect x="8" y="8" width="22" height="44"/><rect x="34" y="8" width="22" height="44"/><rect x="60" y="8" width="22" height="44"/></g></svg>`,
+      heart: `<svg viewBox="0 0 100 60" width="58%" height="58%"><path d="M 50 48 C 30 32, 18 20, 30 14 C 40 10, 50 22, 50 22 C 50 22, 60 10, 70 14 C 82 20, 70 32, 50 48 Z" fill="rgba(217,119,87,0.28)"/></svg>`,
+      logo: `<svg viewBox="0 0 100 60" width="60%" height="60%"><circle cx="50" cy="30" r="20" stroke="rgba(255,255,255,0.22)" stroke-width="1.3" fill="none"/><circle cx="50" cy="30" r="6" fill="rgba(255,255,255,0.22)"/></svg>`,
+    };
+    return svgs[kind] || svgs.minimal;
+  }
 
-      {/* Cursor */}
-      <div style={{position:'absolute', left: cursorX, top: cursorY,
-        opacity: cursorOp, pointerEvents:'none', zIndex: 50,
-        filter:'drop-shadow(0 4px 8px rgba(0,0,0,0.2))'}}>
-        <svg width="36" height="44" viewBox="0 0 36 44">
-          <path d="M 2 2 L 2 38 L 11 30 L 16 42 L 22 40 L 17 28 L 28 28 Z"
-            fill="#fff" stroke="#1a1a1a" strokeWidth="2" strokeLinejoin="round"/>
-        </svg>
-      </div>
+  // Build the wall
+  const wallGrid = document.getElementById('wallGrid');
+  PHILOSOPHIES.forEach((p, idx) => {
+    const cell = document.createElement('div');
+    cell.className = 'cell';
+    cell.dataset.idx = idx;
+    const row = Math.floor(idx / 5);
+    const col = idx % 5;
+    // precompute distance from grid center (2, 1.5)
+    const dr = row - 1.5;
+    const dc = col - 2;
+    const dist = Math.sqrt(dr * dr + dc * dc);
+    cell.dataset.dist = dist.toFixed(3);
+    cell.innerHTML = `
+      <div class="glyph">${makeGlyph(p.glyph)}</div>
+      <div class="num">${String(idx + 1).padStart(2, '0')}</div>
+      <div class="name">${p.name}</div>
+    `;
+    wallGrid.appendChild(cell);
+  });
+
+  const cells = Array.from(wallGrid.querySelectorAll('.cell'));
+  const maxDist = Math.max(...cells.map(c => parseFloat(c.dataset.dist)));
+
+  // ============ Timeline ============
+  const T_TOTAL = 12.0; // seconds (flow type w)
+  const fps = 25;
+  const frameDur = 1 / fps;
+
+  // Easing
+  const expoOut = t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
+  const expoIn  = t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
+  const cubicInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2;
+  const cubicOut = t => 1 - Math.pow(1 - t, 3);
+  const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
+  const clamp01 = v => clamp(v, 0, 1);
+  const lerp = (a, b, t) => a + (b - a) * t;
+
+  // Element refs
+  const topTitle = document.getElementById('topTitle');
+  const subCap = document.getElementById('subCaption');
+  const wallViewport = document.getElementById('wallViewport');
+  const wallGridEl = wallGrid;
+  const scanLight = document.getElementById('scanLight');
+  const fgRow = document.getElementById('fgRow');
+  const card1 = document.getElementById('card1');
+  const card2 = document.getElementById('card2');
+  const card3 = document.getElementById('card3');
+  const thumb1 = document.getElementById('thumb1');
+  const thumb2 = document.getElementById('thumb2');
+  const thumb3 = document.getElementById('thumb3');
+  const brandPanel = document.getElementById('brandPanel');
+  const brandMark = document.getElementById('brandMark');
+  const brandUnderline = document.getElementById('brandUnderline');
+  const brandTag = document.getElementById('brandTag');
+
+  function tick(t){
+    // Clamp
+    t = Math.max(0, Math.min(T_TOTAL, t));
+
+    // ========== Phase 1: 0 - 2.5s — Ripple in 20 cells ==========
+    const rippleStart = 0.15;
+    const rippleSpan  = 1.8;
+    cells.forEach(cell => {
+      const d = parseFloat(cell.dataset.dist);
+      // delay scaled by distance-from-center (hero v10 formula)
+      const delay = (d / maxDist) * 0.85;
+      const cellT = clamp01((t - rippleStart - delay * 0.55) / 0.7);
+      const eased = expoOut(cellT);
+      const idx = parseInt(cell.dataset.idx, 10);
+      const isSel = SELECTED.includes(idx);
+      cell.style.opacity = (eased * (isSel ? 1.0 : 0.85)).toFixed(3);
+      const ty = lerp(30, 0, eased);
+      const scale = lerp(0.88, 1, eased);
+      cell.style.transform = `translateY(${ty}px) scale(${scale})`;
+    });
+
+    // ========== Phase 2: 2.5 - 4.0s — scan light sweeps down ==========
+    const scanStart = 2.6;
+    const scanEnd = 4.0;
+    const scanT = clamp01((t - scanStart) / (scanEnd - scanStart));
+    if (scanT > 0 && scanT < 1) {
+      scanLight.style.opacity = Math.min(1, Math.sin(scanT * Math.PI) * 1.3).toFixed(3);
+      // travel from top to bottom across the wall (-150 to 860px within wallViewport-ish)
+      const py = lerp(-180, 820, cubicInOut(scanT));
+      scanLight.style.transform = `translateY(${py}px)`;
+    } else {
+      scanLight.style.opacity = 0;
+    }
+
+    // ========== Phase 3: 4.0 - 4.8s — 3 cells light up, others dim ==========
+    const lightStart = 4.0;
+    const lightEnd = 4.8;
+    const lightT = clamp01((t - lightStart) / (lightEnd - lightStart));
+    const lightE = expoOut(lightT);
+    cells.forEach(cell => {
+      const idx = parseInt(cell.dataset.idx, 10);
+      const isSel = SELECTED.includes(idx);
+      if (isSel) {
+        cell.classList.toggle('selected', lightT > 0.05);
+      } else {
+        // dim non-selected from 0.85 → 0.08
+        const base = 0.85;
+        const dimmedOpacity = lerp(base, 0.08, lightE);
+        // only override after ripple is done
+        if (t >= lightStart) {
+          cell.style.opacity = dimmedOpacity.toFixed(3);
+        }
+      }
+    });
+
+    // ========== Phase 4: 4.8 - 6.5s — 3 cells break out to foreground ==========
+    // We don't literally move the wall cells; we fade in fg-cards "bursting from the wall"
+    const breakStart = 4.8;
+    const breakEnd = 6.5;
+    const breakT = clamp01((t - breakStart) / (breakEnd - breakStart));
+    const breakE = expoOut(breakT);
+
+    if (t >= breakStart - 0.1) {
+      fgRow.style.opacity = 1;
+    } else {
+      fgRow.style.opacity = 0;
+    }
+
+    [card1, card2, card3].forEach((card, i) => {
+      const stagger = i * 0.18; // pop × 3 staggered
+      const cT = clamp01((t - breakStart - stagger) / 0.85);
+      const cE = expoOut(cT);
+      card.style.opacity = cE.toFixed(3);
+      // Z-rush: from translateZ(-800) to 0, scale 0.4 → 1
+      const tz = lerp(-800, 0, cE);
+      const sc = lerp(0.45, 1, cE);
+      const ty = lerp(40, 0, cE);
+      card.style.transform = `translateZ(${tz}px) scale(${sc}) translateY(${ty}px)`;
+    });
+
+    // Dim the wall (behind) when cards come forward
+    if (t >= breakStart) {
+      const dimT = clamp01((t - breakStart) / 0.9);
+      const dimE = expoOut(dimT);
+      wallViewport.style.opacity = lerp(1, 0.25, dimE).toFixed(3);
+      wallViewport.style.filter = `blur(${lerp(0, 6, dimE).toFixed(1)}px)`;
+    } else {
+      wallViewport.style.opacity = 1;
+      wallViewport.style.filter = 'blur(0px)';
+    }
+
+    // ========== Phase 5: 6.5 - 9.5s — thumbnails grow below each card ==========
+    const thumbStart = 6.6;
+    const thumbs = [thumb1, thumb2, thumb3];
+    thumbs.forEach((thumb, i) => {
+      const stagger = i * 0.32;
+      const ttT = clamp01((t - thumbStart - stagger) / 1.0);
+      const ttE = cubicOut(ttT);
+      thumb.style.opacity = ttE.toFixed(3);
+      // height from 0 to 250px
+      const h = lerp(0, 250, ttE);
+      thumb.style.height = `${h}px`;
+    });
+
+    // ========== Top title fade in 7.2 - 8.0 ==========
+    const titleStart = 7.2;
+    const titleT = clamp01((t - titleStart) / 0.9);
+    const titleE = cubicOut(titleT);
+    topTitle.style.opacity = titleE.toFixed(3);
+    topTitle.style.transform = `translateX(-50%) translateY(${lerp(-14, 0, titleE)}px)`;
+    subCap.style.opacity = (titleE * 0.95).toFixed(3);
+
+    // ========== Phase 6: 9.8 - 12.0s — Brand Reveal ==========
+    const brandStart = 9.8;
+    const panelT = clamp01((t - brandStart) / 0.7);
+    const panelE = expoOut(panelT);
+    brandPanel.style.opacity = panelE.toFixed(3);
+    brandPanel.style.transform = `translateY(${lerp(100, 0, panelE)}%)`;
+
+    const markStart = 10.3;
+    const markT = clamp01((t - markStart) / 0.6);
+    const markE = expoOut(markT);
+    brandMark.style.opacity = markE.toFixed(3);
+    brandMark.style.transform = `scale(${lerp(0.92, 1, markE)})`;
+
+    const ulStart = 10.7;
+    const ulT = clamp01((t - ulStart) / 0.55);
+    brandUnderline.style.width = `${lerp(0, 280, expoOut(ulT))}px`;
+
+    const tagStart = 11.1;
+    const tagT = clamp01((t - tagStart) / 0.5);
+    brandTag.style.opacity = cubicOut(tagT).toFixed(3);
+  }
 
-      {/* "Selected" callout */}
-      {selectLock > 0.5 && (
-        <div style={{position:'absolute', left:'50%', top: 140,
-          transform:'translateX(-50%)', background: TERRA, color:'#fff',
-          padding:'10px 24px', fontFamily: mono, fontSize: 12,
-          letterSpacing:'0.25em', opacity: selectLock, zIndex: 40}}>
-          ✓ SELECTED
-        </div>
-      )}
-    </div>
-  );
-}
-
-function StaticPanel({ which, opacity, scale, selected }) {
-  const titles = {
-    pentagram: { n: 'Pentagram', en: 'Information Architecture' },
-    hara: { n: 'Kenya Hara', en: 'Eastern Minimalism' },
-    field: { n: 'Field.io', en: 'Kinetic Poetry' },
+  // ============ Animation loop ============
+  window.__ready = false;
+  window.__duration = T_TOTAL;
+  let startTime = null;
+  let paused = false;
+  const recording = window.__recording === true;
+
+  function loop(now){
+    if (paused) return;
+    if (startTime === null) startTime = now;
+    const t = (now - startTime) / 1000;
+    tick(t);
+    if (t < T_TOTAL) {
+      requestAnimationFrame(loop);
+    } else if (!recording) {
+      startTime = now;
+      requestAnimationFrame(loop);
+    }
+  }
+
+  // First-frame sync BEFORE requesting next frame
+  tick(0);
+  window.__ready = true;
+  requestAnimationFrame(loop);
+
+  // Pause raf loop — tests & recorder call this before seeking
+  window.__pause = function(){ paused = true; };
+  window.__resume = function(){
+    if (!paused) return;
+    paused = false;
+    startTime = null;
+    requestAnimationFrame(loop);
   };
-  const t = titles[which];
-  return (
-    <div style={{
-      background:'#fff',
-      border: selected ? `3px solid ${TERRA}` : `1px solid ${LINE}`,
-      opacity, transform: `scale(${scale})`, transformOrigin:'center center',
-      display:'flex', flexDirection:'column',
-    }}>
-      <div style={{padding: '18px 22px', borderBottom: `1px solid ${LINE}`,
-        display:'flex', justifyContent:'space-between', alignItems:'baseline'}}>
-        <div>
-          <div style={{fontFamily: serif, fontSize: 28, fontWeight: 500, color: INK}}>
-            {t.n}
-          </div>
-          <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 13,
-            color: ASH, marginTop: 2}}>
-            {t.en}
-          </div>
-        </div>
-        <div style={{fontFamily: mono, fontSize: 10,
-          color: selected ? TERRA : '#999',
-          letterSpacing: '0.15em'}}>
-          {selected ? '✓ SELECTED' : 'READY'}
-        </div>
-      </div>
-      <div style={{flex: 1, position:'relative', overflow:'hidden'}}>
-        {which === 'pentagram' && <PentagramDemo />}
-        {which === 'field' && <FieldDemo elapsed={10} />}
-        {which === 'hara' && <HaraDemo />}
-      </div>
-    </div>
-  );
-}
-
-// ── Scene 6: Ready to execute (22 – 24s) ──────────────────
-function Scene6_Final() {
-  const { elapsed } = useSprite();
-  const fadeIn = interpolate(elapsed, [0, 0.6], [0, 1], Easing.easeOut);
-  const lineW = interpolate(elapsed, [0.6, 1.4], [0, 600]);
-
-  return (
-    <div style={{position:'absolute', inset:0, background:CREAM, opacity: fadeIn,
-      display:'flex', alignItems:'center', justifyContent:'center',
-      flexDirection:'column'}}>
-      <div style={{fontFamily: mono, fontSize: 12, letterSpacing:'0.4em',
-        color: TERRA, marginBottom: 20}}>
-        NEXT · JUNIOR DESIGNER PASS
-      </div>
-      <div style={{fontFamily: serif, fontSize: 104, fontWeight: 500,
-        color: INK, lineHeight: 1, letterSpacing:'-0.01em'}}>
-        开始 <span style={{fontStyle:'italic', color: TERRA}}>Kenya Hara</span> 风格
-      </div>
-      <div style={{height: 1, background: INK, width: lineW, marginTop: 36}} />
-      <div style={{fontFamily: serif, fontStyle:'italic', fontSize: 22,
-        color: ASH, marginTop: 28, maxWidth: 700, textAlign:'center', lineHeight: 1.5}}>
-        "方向确认 → 回到 Junior Designer 主干流程<br/>
-        这时已有明确的 design context,不再是凭空做"
-      </div>
-    </div>
-  );
-}
-
-// ── Watermark (always visible) ────────────────────────────
-function Watermark() {
-  return (
-    <div style={{position:'absolute', bottom: 24, right: 32,
-      fontSize: 11, color: 'rgba(0,0,0,0.38)', letterSpacing:'0.15em',
-      fontFamily: mono, pointerEvents:'none', zIndex: 100}}>
-      Created by Huashu-Design
-    </div>
-  );
-}
-
-// ── Main composition ──────────────────────────────────────
-function App() {
-  return (
-    <Stage duration={24} width={1920} height={1080} bgColor={CREAM}>
-      <Sprite start={0} end={3.5}><Scene1_VagueBrief /></Sprite>
-      <Sprite start={3.5} end={6.5}><Scene2_AdvisorIntro /></Sprite>
-      <Sprite start={6.5} end={10.5}><Scene3_GridScan /></Sprite>
-      <Sprite start={10.5} end={19}><Scene4_ParallelDemos /></Sprite>
-      <Sprite start={19} end={22}><Scene5_Select /></Sprite>
-      <Sprite start={22} end={24}><Scene6_Final /></Sprite>
-      <Watermark />
-    </Stage>
-  );
-}
-
-ReactDOM.createRoot(document.getElementById('root')).render(<App />);
+  // Expose for video recorder (scripts/render-video.js uses __setTime)
+  window.__setTime = function(t){ paused = true; tick(t); };
+
+})();
 </script>
+
 </body>
 </html>