Jelajahi Sumber

feat: integrate Motion One animation system (default-on)

- Add assets/motion.min.js (local fallback for offline demos)
- template.html: embed loader with local + CDN fallback chain,
  5 recipes (cascade / hero / quote / directional / pipeline)
- layouts.md: add data-anim markers to all 10 layouts,
  data-animate on pipeline / quote / directional layouts,
  Pre-flight section E documenting recipe decision tree
- components.md: add Motion 动效系统 chapter with loader,
  data attributes, recipes table, decision tree, FAQ
- checklist.md: add P1 entries 9b/9c and a 动效 self-check block
- SKILL.md: document animations in features list,
  resource tree, and loading order hints

Pipeline pages use manual advance: → / space / wheel-down light
up steps one by one; final → advances to next slide.
郭浩 1 bulan lalu
induk
melakukan
e09f931308
6 mengubah file dengan 335 tambahan dan 69 penghapusan
  1. 10 6
      SKILL.md
  2. 6 0
      assets/motion.min.js
  3. 137 3
      assets/template.html
  4. 32 0
      references/checklist.md
  5. 70 0
      references/components.md
  6. 80 60
      references/layouts.md

+ 10 - 6
SKILL.md

@@ -15,6 +15,7 @@ description: 生成"电子杂志 × 电子墨水"风格的横向翻页网页 PPT
 - **Lucide 线性图标**(不用 emoji)
 - **横向左右翻页**(键盘 ← →、滚轮、触屏滑动、底部圆点、ESC 索引)
 - **主题平滑插值**:翻到 hero 页时颜色和 shader 柔顺过渡
+- **翻页入场动效**(Motion One 驱动,5 种 recipe 自动匹配布局,本地 + CDN 双保险,离线可用)
 
 这个 skill 的美学不是"商务 PPT",也不是"消费互联网 UI"——它像 *Monocle* 杂志贴上了代码后的样子。
 
@@ -226,10 +227,11 @@ open "项目/XXX/ppt/index.html"
 guizang-ppt-skill/
 ├── SKILL.md              ← 你正在读
 ├── assets/
-│   └── template.html     ← 完整的可运行模板(种子文件)
+│   ├── template.html     ← 完整的可运行模板(种子文件)
+│   └── motion.min.js     ← Motion One 本地副本(离线兜底,约 64KB)
 └── references/
-    ├── components.md     ← 组件手册(字体、色、网格、图标、callout、stat、pipeline...)
-    ├── layouts.md        ← 10 种页面布局骨架(可直接粘贴)
+    ├── components.md     ← 组件手册(字体、色、网格、图标、callout、stat、pipeline、动效...)
+    ├── layouts.md        ← 10 种页面布局骨架(可直接粘贴,含动效标记
     ├── themes.md         ← 5 套主题色预设(只能选不能自定义)
     └── checklist.md      ← 质量检查清单(P0/P1/P2/P3 分级)
 ```
@@ -238,9 +240,11 @@ guizang-ppt-skill/
 1. 先读完 `SKILL.md`(这个文件)了解整体
 2. Step 1 需求澄清完成后,读 `themes.md` 帮用户选定一套主题色
 3. **动手前 Read `assets/template.html` 的 `<style>` 块**——这是类名的唯一来源,缺类会导致整页样式崩
-4. 读 `layouts.md` 挑布局(顶部有 Pre-flight 类名清单和主题节奏规划)
-5. 细节调整时读 `components.md` 查组件
-6. 生成后读 `checklist.md` 自检(顶部 P0-0 规则强制预检)
+4. 读 `layouts.md` 挑布局(顶部有 Pre-flight 类名清单、主题节奏规划、动效 recipe 决策树)
+5. 细节调整时读 `components.md` 查组件(含 Motion 动效系统章节)
+6. 生成后读 `checklist.md` 自检(顶部 P0-0 规则强制预检 + 动效自检块)
+
+**动效相关**:模板已把 Motion One 的加载和 5 种 recipe 逻辑全部内嵌到 `template.html` 底部的 module script。你不需要改 JS,只需要按 `layouts.md` 的骨架在 HTML 里加 `data-anim` / `data-animate` 即可。离线演示靠 `assets/motion.min.js`,断网时自动降级为"无动画但内容可读"。
 
 ## 核心设计原则(哲学)
 

File diff ditekan karena terlalu besar
+ 6 - 0
assets/motion.min.js


+ 137 - 3
assets/template.html

@@ -396,6 +396,21 @@
   .chrome{font-family:var(--mono);font-size:max(11px,.78vw);letter-spacing:.2em;text-transform:uppercase;opacity:.62}
   .foot{font-family:var(--mono);font-size:max(11px,.78vw);letter-spacing:.18em;text-transform:uppercase;opacity:.5}
 
+  /* ============ 动效系统(Motion One 驱动) ============
+     所有 [data-anim] 元素默认隐藏,进入页面时由 JS 逐个揭示。
+     - cascade(默认):按 DOM 顺序 stagger 120ms 淡入 + y:16→0
+     - hero(hero 页自动):慢一点的 stagger 180ms
+     - quote(含 [data-anim="line"]):逐行 600ms 淡入
+     - directional(含 [data-anim="left|right"]):左→分隔线→右
+     - pipeline(data-animate="pipeline"):Space/→ 手动推进
+     Motion One 没加载成功时(CDN 断 + 本地丢),所有 [data-anim] 会兜底显示。
+  */
+  [data-anim]{opacity:0}
+  [data-anim="left"]{transform:translateX(-24px)}
+  [data-anim="right"]{transform:translateX(24px)}
+  [data-anim="line"]{opacity:0;transform:translateY(10px)}
+  [data-animate="pipeline"] [data-anim]{opacity:.15}
+
   /* ---------- 响应式降级 ---------- */
   @media (max-width:900px){
     .display{font-size:16vw}
@@ -570,6 +585,8 @@ function go(n){
   const el=slides[idx];
   const th=el.dataset.theme || (el.classList.contains('light')?'light':(el.classList.contains('dark')?'dark':'dark'));
   document.body.classList.toggle('light-bg',th==='light');
+  /* 动效:翻页过渡中段触发当前页的入场动画(由 motion-boot 注册) */
+  if(window.__playSlide) setTimeout(()=>window.__playSlide(idx), 450);
   lock=true;setTimeout(()=>lock=false,700);
 }
 
@@ -614,7 +631,11 @@ function toggleOverview(){
 addEventListener('keydown',e=>{
   if(e.key==='Escape'){e.preventDefault();toggleOverview();return;}
   if(overviewOn)return;
-  if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '||e.key==='ArrowDown')go(idx+1);
+  if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '||e.key==='ArrowDown'){
+    if(window.__pipeAdvance && window.__pipeAdvance()) return;
+    go(idx+1);
+    return;
+  }
   if(e.key==='ArrowLeft'||e.key==='PageUp'||e.key==='ArrowUp')go(idx-1);
   if(e.key==='Home')go(0);
   if(e.key==='End')go(total-1);
@@ -623,7 +644,13 @@ addEventListener('keydown',e=>{
 let wheelTO=null,wheelAcc=0;
 addEventListener('wheel',e=>{
   wheelAcc+=e.deltaY+e.deltaX;
-  if(Math.abs(wheelAcc)>50){go(idx+(wheelAcc>0?1:-1));wheelAcc=0;}
+  if(Math.abs(wheelAcc)>50){
+    if(wheelAcc>0 && window.__pipeAdvance && window.__pipeAdvance()){
+      wheelAcc=0;
+    }else{
+      go(idx+(wheelAcc>0?1:-1));wheelAcc=0;
+    }
+  }
   clearTimeout(wheelTO);wheelTO=setTimeout(()=>wheelAcc=0,150);
 },{passive:true});
 
@@ -632,12 +659,119 @@ addEventListener('touchstart',e=>{tx=e.touches[0].clientX;ty=e.touches[0].client
 addEventListener('touchend',e=>{
   const dx=(e.changedTouches[0].clientX-tx);
   const dy=(e.changedTouches[0].clientY-ty);
-  if(Math.abs(dx)>50&&Math.abs(dx)>Math.abs(dy))go(idx+(dx<0?1:-1));
+  if(Math.abs(dx)>50&&Math.abs(dx)>Math.abs(dy)){
+    if(dx<0 && window.__pipeAdvance && window.__pipeAdvance()) return;
+    go(idx+(dx<0?1:-1));
+  }
 },{passive:true});
 
 go(0);
 </script>
 <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
 <script>lucide.createIcons();</script>
+
+<!-- ============ Motion One 动效引擎(本地优先,CDN 兜底) ============
+     加载策略:先 ./assets/motion.min.js(本地,离线可用),失败 fallback 到 jsDelivr CDN
+     加载完成后挂 window.__playSlide / window.__pipeAdvance,
+     导航脚本会在翻页中段调用 __playSlide(idx),键盘/滚轮/触屏会在翻页前调用 __pipeAdvance()
+     双双失败时会兜底把所有 [data-anim] 设为可见,不让动效破坏阅读
+-->
+<script type="module">
+let motion;
+try {
+  motion = await import('./assets/motion.min.js');
+} catch(e1) {
+  try {
+    motion = await import('https://cdn.jsdelivr.net/npm/motion@11.11.17/+esm');
+  } catch(e2) {
+    console.warn('[motion] local + CDN both failed, disabling animations', e1, e2);
+    document.querySelectorAll('[data-anim]').forEach(el=>{el.style.opacity='1';el.style.transform='none'});
+    document.querySelectorAll('[data-animate="pipeline"] [data-anim]').forEach(el=>el.style.opacity='1');
+  }
+}
+
+if(motion){
+  const { animate, stagger } = motion;
+  const EASE = [.22, 1, .36, 1];
+  const slides = [...document.querySelectorAll('.slide')];
+  let pipeStep = -1;
+  let lastIdx = -1;
+
+  function resetAnims(slide){
+    slide.querySelectorAll('[data-anim]').forEach(el=>{
+      el.style.opacity='';
+      el.style.transform='';
+    });
+  }
+
+  function playSlide(i){
+    const slide = slides[i];
+    if(!slide) return;
+    lastIdx = i;
+    const recipe = slide.dataset.animate || (slide.classList.contains('hero') ? 'hero' : 'cascade');
+
+    if(recipe === 'pipeline'){
+      pipeStep = -1;
+      slide.querySelectorAll('[data-anim]').forEach(el=>{
+        el.style.opacity='0.15';
+        el.style.transform='none';
+      });
+      return;
+    }
+
+    resetAnims(slide);
+    const all = [...slide.querySelectorAll('[data-anim]')];
+    if(!all.length) return;
+
+    if(recipe === 'directional'){
+      const lefts  = all.filter(el=>el.dataset.anim==='left');
+      const divs   = all.filter(el=>el.dataset.anim==='divider');
+      const rights = all.filter(el=>el.dataset.anim==='right');
+      const others = all.filter(el=>!['left','right','divider'].includes(el.dataset.anim));
+      if(others.length) animate(others, {opacity:[0,1], y:[12,0]}, {duration:.6, delay:stagger(.1, {start:.15}), easing:EASE});
+      if(lefts.length)  animate(lefts,  {opacity:[0,1], x:[-24,0]}, {duration:.8, delay:.35, easing:EASE});
+      if(divs.length)   animate(divs,   {opacity:[0,.25]},          {duration:.5, delay:.9});
+      if(rights.length) animate(rights, {opacity:[0,1], x:[24,0]},  {duration:.8, delay:1.0, easing:EASE});
+      return;
+    }
+
+    if(recipe === 'quote'){
+      const lines = all.filter(el=>el.dataset.anim==='line');
+      const others = all.filter(el=>el.dataset.anim!=='line');
+      if(others.length) animate(others, {opacity:[0,1], y:[8,0]},    {duration:.6, delay:stagger(.12, {start:.2}), easing:EASE});
+      if(lines.length)  animate(lines,  {opacity:[.35,1], y:[10,0]}, {duration:.8, delay:stagger(.55, {start:.5}), easing:EASE});
+      return;
+    }
+
+    if(recipe === 'hero'){
+      animate(all, {opacity:[0,1], y:[14,0]}, {duration:.9, delay:stagger(.16, {start:.2}), easing:EASE});
+      return;
+    }
+
+    // default: cascade
+    animate(all, {opacity:[0,1], y:[16,0]}, {duration:.75, delay:stagger(.1, {start:.15}), easing:EASE});
+  }
+
+  function pipeAdvance(){
+    const slide = slides[lastIdx];
+    if(!slide || slide.dataset.animate !== 'pipeline') return false;
+    const steps  = [...slide.querySelectorAll('[data-anim="step"]')];
+    const arrows = [...slide.querySelectorAll('[data-anim="arrow"]')];
+    if(pipeStep >= steps.length - 1) return false;
+    pipeStep++;
+    animate(steps[pipeStep], {opacity:[0.15,1], y:[8,0]}, {duration:.5, easing:EASE});
+    if(pipeStep > 0 && arrows[pipeStep-1]){
+      animate(arrows[pipeStep-1], {opacity:[0.15,.7]}, {duration:.3, delay:.15});
+    }
+    return true;
+  }
+
+  window.__playSlide = playSlide;
+  window.__pipeAdvance = pipeAdvance;
+
+  // 首屏:go(0) 已跑过(同步),没来得及触发动效 → 这里补一下
+  playSlide(0);
+}
+</script>
 </body>
 </html>

+ 32 - 0
references/checklist.md

@@ -170,6 +170,30 @@ Hero Cover → Act Divider (hero) → 3-4 pages non-hero → Act Divider (hero)
 
 用 `XX / 总页数` 的格式(比如 `05 / 27`)。**不要在右上角加动态页码**(会和 `.chrome` 重复)。
 
+### 9b. 动效系统:每一页都要有 data-anim 标记
+
+**现象**:生成后打开浏览器,翻页时内容直接"啪"地出来,没有任何节奏感——杂志风完全靠排版硬撑,少了层级展开的仪式感。
+
+**根因**:完全没给任何元素加 `data-anim`,Motion One 脚本找不到可播的元素,整页静态出现。
+
+**做法**:
+- 所有正文页,**至少给 kicker / 主标题 / lead / callout / stat-card / figure 这些叶子元素加 `data-anim`**
+- **Hero 页**(开场/幕封/问题/结尾):所有核心块(kicker + 大标题 + lead + meta-row)都要加
+- **不需要特殊 recipe 的页**:什么也不用写,默认 cascade 就好看
+- **需要特殊 recipe 的 4 类页**:必须在 `<section>` 上加对应 `data-animate`
+  - 大引用 → `data-animate="quote"` + 每行 `<span data-anim="line" style="display:block">`
+  - Before/After 对比 → `data-animate="directional"` + 左列 `data-anim="left"` + 右列 `data-anim="right"`
+  - Pipeline 流水线 → `data-animate="pipeline"` + 每 step 加 `data-anim="step"`
+  - Hero 页(自动用 hero recipe,但仍需给元素加 `data-anim`)
+
+**自检**:生成后 `grep -c 'data-anim' index.html`,应该数十条以上。如果只有个位数,一定漏标了。
+
+### 9c. Pipeline 页必须加 data-animate="pipeline"
+
+**现象**:流水线页直接全部淡入,失去"一步步讲"的节奏,但切到下一页时又只能往前翻,没法回到上一个 step。
+
+**做法**:Layout 6 的 `<section>` 必须加 `data-animate="pipeline"`。演示时按 →/空格/滚轮下滑可以**逐个点亮 step**,全部点亮之后再按 → 才会翻到下一页。这个节奏是刻意的,不是 bug。
+
 ---
 
 ## 🟢 P2 · 视觉打磨
@@ -260,6 +284,14 @@ JS 会动态算总页数并扩展底部翻页圆点,但 `.chrome` 里的 `XX /
   □ 底部圆点数量与总页数匹配
   □ chrome 里的页码和实际页号一致
   □ ESC 键触发索引视图(如果保留)
+
+动效
+  □ `assets/motion.min.js` 存在(本地兜底)
+  □ 翻页时内容逐个淡入,不是"啪"一下全出
+  □ 大引用页 `<section>` 带 `data-animate="quote"`,每行 `<span data-anim="line">`
+  □ Before/After 对比页 `<section>` 带 `data-animate="directional"`,左右列标 left/right
+  □ Pipeline 页 `<section>` 带 `data-animate="pipeline"`,每 step 标 data-anim="step"
+  □ `grep -c 'data-anim' index.html` 数量 ≥ 页数 × 3(平均每页 3 个以上标记)
 ```
 
 全勾完,才是合格的 PPT。

+ 70 - 0
references/components.md

@@ -17,6 +17,7 @@
 - [Icons 图标](#icons-图标)
 - [Ghost 巨型背景字](#ghost-巨型背景字)
 - [Highlight 荧光标记](#highlight-荧光标记)
+- [Motion 动效系统](#motion-动效系统)
 
 ---
 
@@ -361,3 +362,72 @@
 在文字底部生成一条半透明高亮条。深色主题用亮条,浅色主题用暗条(CSS 已处理)。
 
 **适合场景**:只对关键 1-3 个词使用,不要大面积用。
+
+---
+
+## Motion 动效系统
+
+整套 deck 默认开启翻页入场动画,由 Motion One(vanilla 版 Framer Motion,约 4KB)驱动。
+
+### 加载方式
+
+`assets/template.html` 底部的 module script 会先尝试**本地** `assets/motion.min.js`,失败则回落到 **jsdelivr CDN**,两者都失败则强制把所有带 `data-anim` 的元素设为 `opacity:1`—— 内容永远可读,演示不依赖网络。
+
+```js
+// template 里的核心加载器(不用改)
+let motion;
+try { motion = await import('./assets/motion.min.js'); }
+catch(e1) {
+  try { motion = await import('https://cdn.jsdelivr.net/npm/motion@11.11.17/+esm'); }
+  catch(e2) {
+    document.querySelectorAll('[data-anim]').forEach(el=>{el.style.opacity='1';el.style.transform='none'});
+  }
+}
+```
+
+### 数据属性驱动
+
+你只需要在 HTML 里加两种属性:
+
+```html
+<!-- 1. 在 section 上选 recipe(可选,默认 cascade / hero 自动) -->
+<section class="slide light" data-animate="quote">
+
+<!-- 2. 在需要入场的元素上加 data-anim(可选值:left/right/line/step/divider) -->
+<h1 class="h-xl" data-anim>大标题</h1>
+<div class="stat-card" data-anim>...</div>
+<div data-anim="left">左列内容</div>
+<span data-anim="line" style="display:block">引用第一行</span>
+```
+
+### 5 种 recipe 一览
+
+| recipe | 触发方式 | 行为 | 代表布局 |
+|---|---|---|---|
+| `cascade`(默认) | 不加 `data-animate` 即为此值 | 所有 `data-anim` 逐个 stagger 淡入,75ms/step | Layout 3 / 4 / 5 / 10 |
+| `hero` | `.hero` slide 自动用此值 | 慢节奏 stagger,仪式感更强,160ms/step | Layout 1 / 2 / 7 |
+| `quote` | `data-animate="quote"` | 其他元素先出,`data-anim="line"` 的行 550ms 间隔逐句揭示 | Layout 8 |
+| `directional` | `data-animate="directional"` | `data-anim="left"` 从左滑入 → divider → `data-anim="right"` 从右滑入 | Layout 9 |
+| `pipeline` | `data-animate="pipeline"` | 翻到此页 step 保持 15% 透明;按 →/空格/滚轮逐个点亮,最后一步才放行翻页 | Layout 6 |
+
+### 给 slide 选 recipe 的决策树
+
+1. **它是 `.hero` slide 吗?** → 不用加 `data-animate`,自动用 `hero`
+2. **它是大引用金句页?** → `data-animate="quote"`,每句用 `<span data-anim="line" style="display:block">`
+3. **它是左右对比 Before/After?** → `data-animate="directional"`,左列 `data-anim="left"`、右列 `data-anim="right"`
+4. **它是流水线分步讲解?** → `data-animate="pipeline"`,每步 `data-anim="step"`
+5. **其他所有正文页** → 什么也不加,自动用 `cascade`
+
+### 什么元素该加 `data-anim`?
+
+- ✅ 每一层有独立语义的块:kicker / h1 / h-xl / lead / callout / stat-card / figure / tag / rowline
+- ✅ 多列结构里每一列,让它们逐列淡入而不是一起
+- ❌ 不要在容器(`.grid-6` / `.frame`)上加,只加给叶子元素
+- ❌ 不要在每个 `<li>` 上加,一般在 `<ul>` 层加就够
+- ❌ 如果某页不想要任何动画(比如过渡页),整页不加 `data-anim` 即可 — Motion One 只对带标记的元素生效
+
+### 常见问题
+
+- **图片闪一下再出现?** 这是预期行为,翻页中段(450ms 时)触发动画
+- **Pipeline 页卡住翻不下页?** 正确的,按 → 一步一步点亮 step,全部点亮后再按 → 才翻页
+- **内容静态时也不显示?** 检查 motion.min.js 是否在 `assets/` 下;或者浏览器控制台看错误信息

+ 80 - 60
references/layouts.md

@@ -45,6 +45,24 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
 - 主题节奏(每页用 light / dark / hero light / hero dark 哪一个)在下文"主题节奏规划"一节有硬规则,生成前必读
 - 两件事都要在挑布局之前决定,避免返工
 
+### E. 动效系统(默认开启 · Motion One 驱动)
+
+**核心机制**:template.html 底部的 module script 会在翻页时触发入场动画。所有带 `data-anim` 的元素初始不可见,翻到当前页时由 Motion One 逐个淡入。
+
+**动效策略**:在 `<section>` 上加 `data-animate="<recipe>"` 选择动画风格;每个需要入场动画的元素加 `data-anim`(可选附值,如 `left` / `right` / `line` / `step`)。
+
+| recipe | 用法 | 适合布局 |
+|---|---|---|
+| 默认(cascade) | 什么也不加,自动级联淡入 | 大部分正文页(Layout 3 / 4 / 5 / 10) |
+| `hero` | `.hero` 页自动启用,节奏更慢更仪式感 | Layout 1 / 2 / 7(所有 hero 页) |
+| `quote` | 一句一句揭示,慢节奏(550ms stagger) | Layout 8 大引用 |
+| `directional` | 左进 → 分割 → 右进,用于对比 | Layout 9 Before/After |
+| `pipeline` | 手动推进,按 →/空格 一步步点亮 | Layout 6 流水线 |
+
+**降级保底**:如果 motion.min.js 本地 + CDN 都加载失败,脚本会强制把所有 `data-anim` 元素设为 `opacity:1`,内容永远可读。
+
+**不需要动效的页面**:如果某页想完全跳过动效,不加任何 `data-anim` 即可 —— Motion One 只对带标记的元素生效。
+
 ---
 
 ## 0. 基础结构(所有 slide 都一样)
@@ -133,13 +151,13 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
     <div>Vol.01</div>
   </div>
   <div class="frame" style="display:grid; gap:4vh; align-content:center; min-height:80vh">
-    <div class="kicker">私享会 · 李继刚</div>
-    <h1 class="h-hero">一人公司</h1>
-    <h2 class="h-sub">被 AI 折叠的组织</h2>
-    <p class="lead" style="max-width:60vw">
+    <div class="kicker" data-anim>私享会 · 李继刚</div>
+    <h1 class="h-hero" data-anim>一人公司</h1>
+    <h2 class="h-sub" data-anim>被 AI 折叠的组织</h2>
+    <p class="lead" style="max-width:60vw" data-anim>
       一个 AI 创作者 —— 在 64 天里做了 11 万行代码、在 9 个平台上持续输出,生活节奏几乎没有被改变。
     </p>
-    <div class="meta-row">
+    <div class="meta-row" data-anim>
       <span>歸藏 Guizang</span><span>·</span><span>独立创作者 / CodePilot 作者</span>
     </div>
   </div>
@@ -167,9 +185,9 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
     <div>Act I · 01 / 25</div>
   </div>
   <div class="frame" style="display:grid; gap:6vh; align-content:center; min-height:80vh">
-    <div class="kicker">Act I</div>
-    <h1 class="h-hero" style="font-size:8.5vw">硬数据</h1>
-    <p class="lead" style="max-width:55vw">
+    <div class="kicker" data-anim>Act I</div>
+    <h1 class="h-hero" style="font-size:8.5vw" data-anim>硬数据</h1>
+    <p class="lead" style="max-width:55vw" data-anim>
       先看数字,再谈方法。
     </p>
   </div>
@@ -196,37 +214,37 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
     <div>Act I / Dev · 02 / 25</div>
   </div>
   <div class="frame" style="padding-top:6vh">
-    <div class="kicker">一个人,做了什么。</div>
-    <h2 class="h-xl">过去 64 天</h2>
-    <p class="lead" style="margin-bottom:5vh">从 0 到开源 CodePilot。</p>
+    <div class="kicker" data-anim>一个人,做了什么。</div>
+    <h2 class="h-xl" data-anim>过去 64 天</h2>
+    <p class="lead" style="margin-bottom:5vh" data-anim>从 0 到开源 CodePilot。</p>
 
     <div class="grid-6" style="margin-top:6vh">
-      <div class="stat-card">
+      <div class="stat-card" data-anim>
         <div class="stat-label">Duration</div>
         <div class="stat-nb">64 <span class="stat-unit">天</span></div>
         <div class="stat-note">从 0 到现在</div>
       </div>
-      <div class="stat-card">
+      <div class="stat-card" data-anim>
         <div class="stat-label">Lines of Code</div>
         <div class="stat-nb">110K+</div>
         <div class="stat-note">一行行写到 11 万+</div>
       </div>
-      <div class="stat-card">
+      <div class="stat-card" data-anim>
         <div class="stat-label">GitHub Stars</div>
         <div class="stat-nb">5,166</div>
         <div class="stat-note">一个开源仓库</div>
       </div>
-      <div class="stat-card">
+      <div class="stat-card" data-anim>
         <div class="stat-label">Downloads</div>
         <div class="stat-nb">41K+</div>
         <div class="stat-note">装到了几万台电脑里</div>
       </div>
-      <div class="stat-card">
+      <div class="stat-card" data-anim>
         <div class="stat-label">AI Providers</div>
         <div class="stat-nb">19</div>
         <div class="stat-note">跨平台接入</div>
       </div>
-      <div class="stat-card">
+      <div class="stat-card" data-anim>
         <div class="stat-label">Commits</div>
         <div class="stat-nb">608+</div>
         <div class="stat-note">没有协作者</div>
@@ -260,22 +278,22 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
     <!-- 左列:标题 + 正文 + callout,flex column 让 callout 贴列底 -->
     <div style="display:flex; flex-direction:column; justify-content:space-between; gap:3vh">
       <div>
-        <div class="kicker">BUT</div>
-        <h2 class="h-xl" style="white-space:nowrap; font-size:7.2vw">
+        <div class="kicker" data-anim>BUT</div>
+        <h2 class="h-xl" style="white-space:nowrap; font-size:7.2vw" data-anim>
           我不是程序员。
         </h2>
-        <p class="lead" style="margin-top:3vh">
+        <p class="lead" style="margin-top:3vh" data-anim>
           大学毕业之后再也没写过一行代码。过去十年做的是 UI 设计和 AI 特效。
         </p>
       </div>
-      <div class="callout">
+      <div class="callout" data-anim>
         "这东西在三年前,<br>
         需要一个十人团队做一年。"
         <div class="callout-src">— 一个观察者的判断</div>
       </div>
     </div>
     <!-- 右列:图片用标准 16/10 比例 + max-height,不要 align-self:end -->
-    <figure class="frame-img" style="aspect-ratio:16/10; max-height:56vh">
+    <figure class="frame-img" style="aspect-ratio:16/10; max-height:56vh" data-anim>
       <img src="images/codepilot.png" alt="CodePilot 产品截图">
       <figcaption class="img-cap">CodePilot · 产品截图</figcaption>
     </figure>
@@ -304,31 +322,31 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
     <div>Act I / Ops · 05 / 27</div>
   </div>
   <div class="frame" style="padding-top:5vh">
-    <div class="kicker">Proof · 粉丝实证</div>
-    <h2 class="h-xl">10 个平台 · 6 张截图</h2>
+    <div class="kicker" data-anim>Proof · 粉丝实证</div>
+    <h2 class="h-xl" data-anim>10 个平台 · 6 张截图</h2>
 
     <div class="grid-3-3" style="margin-top:4vh">
-      <figure class="frame-img" style="height:26vh">
+      <figure class="frame-img" style="height:26vh" data-anim>
         <img src="images/weibo.png" alt="微博 289K">
         <figcaption class="img-cap">微博 · 289K</figcaption>
       </figure>
-      <figure class="frame-img" style="height:26vh">
+      <figure class="frame-img" style="height:26vh" data-anim>
         <img src="images/twitter.png" alt="推特 137K">
         <figcaption class="img-cap">推特 · 137K</figcaption>
       </figure>
-      <figure class="frame-img" style="height:26vh">
+      <figure class="frame-img" style="height:26vh" data-anim>
         <img src="images/wechat.png" alt="公众号 96K">
         <figcaption class="img-cap">公众号 · 96K</figcaption>
       </figure>
-      <figure class="frame-img" style="height:26vh">
+      <figure class="frame-img" style="height:26vh" data-anim>
         <img src="images/jike.png" alt="即刻 26K">
         <figcaption class="img-cap">即刻 · 26K</figcaption>
       </figure>
-      <figure class="frame-img" style="height:26vh">
+      <figure class="frame-img" style="height:26vh" data-anim>
         <img src="images/xhs.png" alt="小红书 19K">
         <figcaption class="img-cap">小红书 · 19K</figcaption>
       </figure>
-      <figure class="frame-img" style="height:26vh">
+      <figure class="frame-img" style="height:26vh" data-anim>
         <img src="images/douyin.png" alt="抖音 10K">
         <figcaption class="img-cap">抖音 · 10K</figcaption>
       </figure>
@@ -351,7 +369,7 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
 ## Layout 6: 两列流水线(Pipeline)
 
 ```html
-<section class="slide light">
+<section class="slide light" data-animate="pipeline">
   <div class="chrome">
     <div>我的工作流 · Workflow</div>
     <div>Act II · 15 / 27</div>
@@ -364,27 +382,27 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
     <div class="pipeline-section">
       <div class="pipeline-label">文本侧 · Text Pipeline</div>
       <div class="pipeline">
-        <div class="step">
+        <div class="step" data-anim="step">
           <div class="step-nb">01</div>
           <div class="step-title">Draft</div>
           <div class="step-desc">AI 帮我起草初稿</div>
         </div>
-        <div class="step">
+        <div class="step" data-anim="step">
           <div class="step-nb">02</div>
           <div class="step-title">Polish</div>
           <div class="step-desc">AI 润色去 AI 味</div>
         </div>
-        <div class="step">
+        <div class="step" data-anim="step">
           <div class="step-nb">03</div>
           <div class="step-title">Morph</div>
           <div class="step-desc">AI 变形成推特 / 小红书</div>
         </div>
-        <div class="step">
+        <div class="step" data-anim="step">
           <div class="step-nb">04</div>
           <div class="step-title">Illustrate</div>
           <div class="step-desc">AI 生成信息图</div>
         </div>
-        <div class="step">
+        <div class="step" data-anim="step">
           <div class="step-nb">05</div>
           <div class="step-title">Distribute</div>
           <div class="step-desc">一键分发 9 平台</div>
@@ -396,17 +414,17 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
     <div class="pipeline-section">
       <div class="pipeline-label">视觉 · 视频侧 · Video Pipeline</div>
       <div class="pipeline">
-        <div class="step">
+        <div class="step" data-anim="step">
           <div class="step-nb">06</div>
           <div class="step-title">Cut</div>
           <div class="step-desc">AI 帮我剪辑</div>
         </div>
-        <div class="step">
+        <div class="step" data-anim="step">
           <div class="step-nb">07</div>
           <div class="step-title">Wrap</div>
           <div class="step-desc">AI 帮我包装</div>
         </div>
-        <div class="step">
+        <div class="step" data-anim="step">
           <div class="step-nb">08</div>
           <div class="step-title">Cover</div>
           <div class="step-desc">AI 生成封面</div>
@@ -426,6 +444,7 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
 - 两组之间用 3.6vh 的间距 + 顶部细分隔线(已在 CSS 中预设)
 - 每个 step 是固定的 nb → title → desc 结构
 - 步骤数不限但单行最好 ≤5 个,否则换到第二 pipeline
+- **动效**:`<section>` 加 `data-animate="pipeline"`,每个 `.step` 加 `data-anim="step"`。翻到此页时步骤默认 `opacity:.15`,按 →/空格/滚轮下滑时一次点亮一个 step;**所有 step 点亮完才会翻到下一页**,可制造演讲互动感
 
 ---
 
@@ -438,13 +457,13 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
     <div>24 / 27</div>
   </div>
   <div class="frame" style="display:grid; gap:8vh; align-content:center; min-height:80vh">
-    <div class="kicker">The Question</div>
+    <div class="kicker" data-anim>The Question</div>
     <h1 class="h-hero" style="font-size:7vw; line-height:1.15">
-      你的公司里,<br>
-      哪些岗位本来就<br>
-      不该由人来做?
+      <span data-anim style="display:block">你的公司里,</span>
+      <span data-anim style="display:block">哪些岗位本来就</span>
+      <span data-anim style="display:block">不该由人来做?</span>
     </h1>
-    <p class="lead" style="max-width:50vw">
+    <p class="lead" style="max-width:50vw" data-anim>
       这个问题,不是技术问题,是架构问题。
     </p>
   </div>
@@ -466,21 +485,22 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
 ## Layout 8: 大引用页(Big Quote · 衬线金句)
 
 ```html
-<section class="slide light">
+<section class="slide light" data-animate="quote">
   <div class="chrome">
     <div>The Takeaway · 核心金句</div>
     <div>18 / 25</div>
   </div>
   <div class="frame" style="display:grid; gap:5vh; align-content:center; min-height:80vh">
-    <div class="kicker">Quote · 金句</div>
+    <div class="kicker" data-anim>Quote · 金句</div>
     <blockquote style="font-family:var(--serif-zh); font-weight:700; font-size:5.8vw; line-height:1.2; letter-spacing:-.01em; max-width:72vw">
-      "没有交接,<br>所有人都在构建。"
+      <span data-anim="line" style="display:block">"没有交接,</span>
+      <span data-anim="line" style="display:block">所有人都在构建。"</span>
     </blockquote>
-    <p class="lead" style="max-width:55vw; opacity:.65">
+    <p class="lead" style="max-width:55vw; opacity:.65" data-anim>
       Without the handoff, everyone builds.<br>
       And that makes all the difference.
     </p>
-    <div class="meta-row">
+    <div class="meta-row" data-anim>
       <span>— Luke Wroblewski</span><span>·</span><span>2026.04.16</span>
     </div>
   </div>
@@ -502,18 +522,18 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
 ## Layout 9: 并列对比(A vs B · 旧 vs 新)
 
 ```html
-<section class="slide light">
+<section class="slide light" data-animate="directional">
   <div class="chrome">
     <div>旧 vs 新 · The Shift</div>
     <div>12 / 25</div>
   </div>
   <div class="frame" style="padding-top:5vh">
-    <div class="kicker">Before / After · 范式转变</div>
-    <h2 class="h-xl" style="margin-bottom:4vh">从交接到共建</h2>
+    <div class="kicker" data-anim>Before / After · 范式转变</div>
+    <h2 class="h-xl" style="margin-bottom:4vh" data-anim>从交接到共建</h2>
 
     <div class="grid-2-6-6" style="gap:5vw 4vh">
       <!-- 左列:旧 -->
-      <div style="padding:3vh 2vw; border-left:3px solid currentColor; opacity:.55">
+      <div data-anim="left" style="padding:3vh 2vw; border-left:3px solid currentColor; opacity:.55">
         <div class="kicker" style="opacity:.9">Before · 旧模式</div>
         <h3 class="h-md" style="margin-top:2vh">设计 → 开发 → 交接</h3>
         <ul style="margin-top:3vh; padding-left:1.2em; display:flex; flex-direction:column; gap:1.4vh; font-family:var(--sans-zh); font-size:max(14px,1.1vw); line-height:1.55">
@@ -524,7 +544,7 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
         </ul>
       </div>
       <!-- 右列:新 -->
-      <div style="padding:3vh 2vw; border-left:3px solid currentColor">
+      <div data-anim="right" style="padding:3vh 2vw; border-left:3px solid currentColor">
         <div class="kicker" style="opacity:.9">After · 新模式</div>
         <h3 class="h-md" style="margin-top:2vh">同工具 · 并行 · 共建</h3>
         <ul style="margin-top:3vh; padding-left:1.2em; display:flex; flex-direction:column; gap:1.4vh; font-family:var(--sans-zh); font-size:max(14px,1.1vw); line-height:1.55">
@@ -562,24 +582,24 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
   <div class="frame grid-2-8-4" style="padding-top:6vh">
     <!-- 左列:大段正文 + 引用 -->
     <div>
-      <div class="kicker">Phase 01 · 设计阶段</div>
-      <h2 class="h-xl" style="margin-top:1vh; margin-bottom:3vh">设计先行 · 2 周</h2>
+      <div class="kicker" data-anim>Phase 01 · 设计阶段</div>
+      <h2 class="h-xl" style="margin-top:1vh; margin-bottom:3vh" data-anim>设计先行 · 2 周</h2>
 
-      <p class="lead" style="margin-bottom:3vh">
+      <p class="lead" style="margin-bottom:3vh" data-anim>
         在 Figma 中完成视觉探索与设计系统,网格 / 排版 / 颜色变量 / 可复用组件,桌面和移动端稿件几轮反馈迭代。
       </p>
 
-      <p style="font-family:var(--sans-zh); font-size:max(14px,1.15vw); line-height:1.75; opacity:.78; margin-bottom:2.4vh">
+      <p data-anim style="font-family:var(--sans-zh); font-size:max(14px,1.15vw); line-height:1.75; opacity:.78; margin-bottom:2.4vh">
         两周之内,视觉风格、粗略结构、方向性内容全部稳定。这是扎实的传统设计流程——在这里还没什么新鲜事。
       </p>
 
-      <div class="callout" style="margin-top:3vh">
+      <div class="callout" style="margin-top:3vh" data-anim>
         "This phase was pretty standard.<br>Just a solid Web design process."
         <div class="callout-src">— Luke Wroblewski</div>
       </div>
     </div>
     <!-- 右列:辅助图 · 竖版或方形 -->
-    <figure class="frame-img" style="aspect-ratio:3/4; max-height:60vh">
+    <figure class="frame-img" style="aspect-ratio:3/4; max-height:60vh" data-anim>
       <img src="images/figma.png" alt="Figma design system">
       <figcaption class="img-cap">Figma · Design System</figcaption>
     </figure>

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini