Skill: wwbwin/canvasflow-ppt (version 2.0.1)

--- name: canvasflow-专业杂志级PPT一键生成专家 description: | 30秒出杂志级PPT，仅需要输入主题或大纲，AI一键生成杂志级高端PPT！自动排版、配色、图表、动画，秒变专业演示神器，无需设计基础。PPT 的制作依靠审美，可很多初学者需要模板。特别将 10 年设计经验的设计师进行知识、审美蒸馏提炼后，给每个刚入手的兄弟姐妹提供一个快速美观、简便的 PPT Skill。生成单文件 HTML 横向翻页演示文档，WebGL 流体背景，5 套主题色预设。 metadata: version: "2.0.1" author: "WuWenBin-BeiJing-ST" license: "MIT" --- # CanvasFlow PPT Skill > **作者**: WuWenBin-BeiJing-ST > > 30秒出杂志级PPT，仅需要输入主题或大纲，AI一键生成杂志级高端PPT！自动排版、配色、图表、动画，秒变专业演示神器，无需设计基础。 --- ## 这个 Skill 做什么 **CanvasFlow PPT** —— 流畅的画布，流动的演示。 生成一份**单文件 HTML** 的横向翻页演示文档，视觉基调是： - **Canvas（画布）**：WebGL 流体背景，像艺术画布一样呈现内容 - **Flow（流动）**：横向翻页，主题平滑过渡，视觉节奏流动 - **衬线标题 + 非衬线正文 + 等宽元数据** 三级字体分工 - **Lucide 线性图标**（不用 emoji） - **键盘 ← → / 滚轮 / 触屏滑动 / 底部圆点** 全方位导航 - **5 套主题色预设**：墨水经典 / 靛蓝瓷 / 森林墨 / 牛皮纸 / 沙丘 ## 何时使用 **合适的场景**： - 线下分享 / 行业内部讲话 / 私享会 - AI 新产品发布 / demo day - 带有强烈个人风格的演讲 **不合适的场景**： - 大段表格数据、图表叠加（用常规 PPT） - 培训课件（信息密度不够） - 需要多人协作编辑（静态 HTML） --- ## 依赖说明 **无外部依赖**。HTML 单文件包含所有 CSS/JS/字体 CDN，浏览器直接打开即可。 如需生成 PPTX 格式： ```bash pip3 install python-pptx ``` --- ## 标准工作流 ### Step 0 · 环境检测（必做） **在生成任何 slide 之前，必须先读取 `assets/template.html` 的 `<style>` 块**，确认所有需要的类都已定义。 这是所有生成问题的源头——如果 template.html 里没有 `h-hero` / `stat-card` / `pipeline` 等类定义，浏览器会 fallback 到默认样式，导致整页样式崩坏。 ### Step 1 · 需求澄清（动手前必做） **如果用户已经给了完整的大纲 + 图片**，可以跳过直接进 Step 2。 **如果用户只给了主题或一个模糊想法**，用这 6 个问题逐个对齐： | # | 问题 | 为什么要问 | |---|------|-----------| | 1 | **受众是谁？分享场景？**（行业内部 / 商业发布 / demo day / 私享会） | 决定语言风格和深度 | | 2 | **分享时长？** | 15 分钟 ≈ 10 页，30 分钟 ≈ 20 页 | | 3 | **有没有原始素材？**（文档 / 数据 / 旧 PPT / 文章链接） | 有素材就基于素材 | | 4 | **想要哪套主题色？** | 见 `references/themes.md`，5 套预设 | | 5 | **有没有硬约束？**（必须包含 XX 数据 / 不能出现 YY） | 避免返工 | | 6 | **输出格式？**（HTML / PPTX / 双格式） | HTML 适合线上分享，PPTX 适合企业汇报 | #### 大纲协助（如果用户没有大纲） 用"叙事弧"模板搭骨架： ``` 钩子(Hook) → 1 页 : 抛一个反差 / 问题 / 硬数据 定调(Context) → 1-2 页 : 说明背景 / 你是谁 / 为什么讲这个 主体(Core) → 3-5 页 : 核心内容，用 Layout 3/4/5/6 穿插 转折(Shift) → 1 页 : 打破预期 / 提出新观点 收束(Takeaway) → 1-2 页 : 金句 / 悬念问题 / 行动建议 ``` ### Step 2 · 拷贝模板 + 主题选择 从 `assets/template.html` 拷贝到目标位置，同时选定主题色。 **5 套主题色预设**： | # | 主题 | 适合 | |---|------|------| | 1 | 🖋 墨水经典 | 通用 / 商业发布 / 不知道选啥的默认 | | 2 | 🌊 靛蓝瓷 | 科技 / 研究 / 数据 / 技术发布会 | | 3 | 🌿 森林墨 | 自然 / 可持续 / 文化 / 非虚构 | | 4 | 🍂 牛皮纸 | 怀旧 / 人文 / 文学 / 独立杂志 | | 5 | 🌙 沙丘 | 艺术 / 设计 / 创意 / 画廊 | **硬规则**：只允许从 5 套预设里选，不接受自定义 hex 值——颜色搭配错了画面瞬间变丑，保护美学比给自由更重要。 ### Step 3 · 规划主题节奏（和类预检同等重要） **在挑布局之前**，必须先列出每一页的主题 class（`hero dark` / `hero light` / `light` / `dark`）。 **主题节奏硬规则**： - 每页 section 必须带 `light` / `dark` / `hero light` / `hero dark` 之一 - 连续 3 页以上同主题 = 视觉疲劳，不允许 - 8 页以上必须有 ≥1 个 `hero dark` + ≥1 个 `hero light` - 整个 deck 不能只有 `light` 正文页，必须有 `dark` 正文页制造呼吸 - 每 3-4 页插入 1 个 hero 页（封面/幕封/问题/大引用） **按布局的主题默认值**： | Layout | 默认主题 | 原因 | |---|---|---| | 1. 开场封面 | `hero dark` | 开场仪式感 | | 2. 章节幕封 | `hero dark` 与 `hero light` 交替 | 呼吸节奏 | | 3. 数据大字报 | `light` | 数字需纸白底 | | 4. 左文右图 | **`light` / `dark` 交替** | 正文节奏主力 | | 5. 图片网格 | `light` | 截图需亮底 | | 6. Pipeline | `light` | 流程图需清晰 | | 7. 问题页 | `hero dark` | 强视觉冲击 | | 8. 大引用 | **`dark` 优先** | 金句仪式感靠暗底 | | 9. 对比页 | `light` | 双列需清晰 | | 10. 图文混排 | **`light` / `dark` 交替** | 节奏 | ### Step 4 · 挑布局填充内容 打开 `references/layouts.md`，里面有 10 种现成布局骨架，每种都是完整可粘贴的 `<section>` 代码块。 **关键约束**： 1. **图片比例规范**：永远用标准比例（16:10 / 4:3 / 3:2 / 1:1 / 16:9），不要用原图奇葩比例 2. **图片网格用 `height:Nvh`，不用 `aspect-ratio`**（会撑破） 3. **图片只裁底部**：用 `object-position: top center` 4. **不要用 emoji 作图标**：用 Lucide 图标库 5. **衬线/非衬线字体分工**：标题衬线、正文非衬线、元数据等宽 6. **大标题 ≤ 5 字且 `nowrap`**：避免 1 字 1 行 ### Step 5 · 对照检查清单自检 打开 `references/checklist.md`，逐项对照。P0 级问题必须全过。 **特别要注意**： 1. 大标题必须是衬线字体——如果显示成非衬线，99% 是类名在 template.html 里缺失 2. 图片网格里只用 `height:Nvh`，不用 `aspect-ratio` 3. chrome 和 kicker 不要写同一句话（chrome = 栏目标签，kicker = 本页钩子） 4. 主题节奏要有交错，不要全是 light ### Step 6 · 本地预览 ```bash open "项目/XXX/ppt/index.html" ``` 不需要本地服务器，图片走相对路径 `images/xxx.png`。 --- ## 参考文档 - [布局系统详解](references/layouts.md) — 10 种骨架的完整代码 - [主题色系统](references/themes.md) — 5 套预设 - [质量检查清单](references/checklist.md) — P0/P1/P2/P3 分级 - [组件手册](references/components.md) — 字体、网格、图标、callout 等 --- ## 核心设计原则 > 这些原则是"一人公司"分享 PPT 的 5 轮迭代总结出来的。违反其中任何一条，视觉感都会垮。 1. **克制优于炫技** — WebGL 背景只在 hero 页透出，普通页几乎看不见 2. **结构优于装饰** — 不用阴影、不用浮动卡片，一切信息靠**大字号 + 字体对比 + 网格留白** 3. **内容层级由字号和字体共同定义** — 最大衬线 = 主标题，中衬线 = 副标，大非衬线 = lead，小非衬线 = body，等宽 = 元数据 4. **图片是第一公民** — 图片只裁底部，保证顶部和左右完整 5. **节奏靠 hero 页** — hero 和 non-hero 交替，才不累眼睛 6. **术语统一** — Skills 就是 Skills，不要中英混合翻译 --- ## 致谢 本 Skill 基于设计师歸藏（op7418）开源的 [guizang-ppt-skill](https://github.com/op7418/guizang-ppt-skill) 优化而来，融合了其 10 年设计经验与审美智慧。 **设计哲学来源**："克制胜于华丽、结构胜于装饰" — 歸藏 --- **CanvasFlow PPT** —— 让演示流动起来。 > 名字寓意： > - **Canvas（画布）**：WebGL 背景，像艺术画布一样承载内容 > - **Flow（流动）**：翻页流畅，主题过渡自然，视觉节奏呼吸

path: README.md
content: # CanvasFlow PPT Skill

---

## 📋 基本信息

| 属性 | 内容 |
|------|------|
| **Skill 名称** | CanvasFlow PPT |
| **作者** | WuWenBin-BeiJing-ST |
| **版本** | V2.0.0 |
| **类型** | 演示文档生成 |
| **输出格式** | HTML（单文件） / PPTX |

---

## 📖 简介

**CanvasFlow PPT** —— 流畅的画布，流动的演示。

PPT 的制作依靠审美，可很多初学者需要模板。特别将 10 年设计经验的设计师进行知识、审美蒸馏提炼后，给每个刚入手的兄弟姐妹提供一个快速美观、简便的 PPT Skill。

---

## 🎨 名字寓意

| 单词 | 含义 |
|------|------|
| **Canvas（画布）** | WebGL 流体背景，像艺术画布一样承载内容，hero 页明显透出 |
| **Flow（流动）** | 横向翻页流畅，主题平滑过渡，视觉节奏呼吸，light/dark 交替 |

---

## ✨ 核心特性

### 视觉设计
- **WebGL 双背景**：dark 页用钛金色散，light 页用银色流动
- **三级字体分工**：衬线标题 / 非衬线正文 / 等宽元数据
- **5 套主题色预设**：墨水经典 / 靛蓝瓷 / 森林墨 / 牛皮纸 / 沙丘
- **10 种布局骨架**：封面 / 幕封 / 数据大字报 / 左文右图 / Pipeline 等

### 交互体验
- **全方位导航**：键盘 ← → / 滚轮 / 触屏滑动 / 底部圆点 / ESC 索引
- **主题平滑切换**：翻到 light 页，背景自动切换为银色流动
- **响应式设计**：桌面 / 移动端自适应

### 工作流规范
- **6 步标准化流程**：环境检测 → 需求澄清 → 主题选择 → 节奏规划 → 填充内容 → 质量自检
- **严格预检机制**：必须先读取 template.html，确认类名存在
- **质量检查清单**：P0/P1/P2/P3 分级，P0 级必须全过

---

## 🚀 适用场景

### ✅ 适合
- 线下分享 / 行业内部讲话 / 私享会
- AI 新产品发布 / demo day
- 带有强烈个人风格的演讲
- 需要视觉冲击力的演示

### ❌ 不适合
- 大段表格数据、图表叠加（用常规 PPT）
- 培训课件（信息密度不够）
- 需要多人协作编辑（静态 HTML）

---

## 📂 文件结构

```
canvasflow-ppt/
├── SKILL.md              ← Skill 主文件（工作流、规范、原则）
├── README.md             ← 本文件（简介、特性、使用说明）
├── assets/
│   └── template.html     ← 完整可运行的 HTML 模板（CSS + WebGL + JS）
├── references/
│   ├── layouts.md        ← 10 种页面布局骨架
│   ├── themes.md         ← 5 套主题色预设
│   ├── checklist.md      ← 质量检查清单
│   └── components.md     ← 组件手册
└── scripts/
    ├── generate_html.py  ← HTML 生成脚本
    ├── generate_pptx.py  ← PPTX 生成脚本
    └── generate_ppt.py   ← 统一入口
```

---

## 🔧 使用方式

### 在 QClaw 中触发

说出以下关键词即可触发：
- "做 PPT"
- "生成演示"
- "CanvasFlow PPT"
- "画布演示"

### 标准工作流

1. **环境检测**：读取 template.html，确认类名存在
2. **需求澄清**：6 问清单（受众/时长/素材/主题色/约束/格式）
3. **主题选择**：从 5 套预设中选择
4. **节奏规划**：规划 light/dark/hero 交替
5. **填充内容**：从 10 种布局骨架中选择，填充内容
6. **质量自检**：对照检查清单，P0 级必须全过

---

## 🎯 设计哲学

> 继承自 guizang-ppt-skill：

1. **克制优于炫技** — WebGL 背景只在 hero 页透出
2. **结构优于装饰** — 信息靠字号 + 字体对比 + 网格留白
3. **内容层级由字号和字体共同定义**
4. **图片是第一公民** — 只裁底部，顶部和左右完整
5. **节奏靠 hero 页** — hero 和 non-hero 交替
6. **术语统一** — Skills 就是 Skills，不中英混译

---

## 🙏 致谢

感谢所有为设计经验分享做出贡献的设计师，让知识得以传承。

---

## 📄 License

MIT © 2026 WuWenBin-BeiJing-ST

---

**CanvasFlow PPT** —— 让演示流动起来。✨


path: _meta.json
content: ownerId: kn7cq99t9kzkxb4g587an3gyw5850tc0
slug: canvasflow-ppt
version: 2.0.0
publishedAt: 1777022874446

path: assets/template.html
content: <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>[必填] 替换为 PPT 标题 · Deck Title</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=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&family=Source+Serif+4:ital,opsz,wght@0,8..60,300;0,8..60,400;0,8..60,500;0,8..60,600;1,8..60,400&family=IBM+Plex+Mono:wght@300;400;500;600&family=Noto+Serif+SC:wght@300;400;500;600;700;900&family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap" rel="stylesheet">
<style>
  :root{
    /* ============ 主题色(默认:🖋 墨水经典) ============
       切换主题:从 references/themes.md 复制对应的 :root 块
       整体替换这几行(--ink / --ink-rgb / --paper / --paper-rgb)
       其他地方散落的 rgba() 都走 var(--ink-rgb) / var(--paper-rgb),无需逐处改 */
    --ink:#0a0a0b;
    --ink-rgb:10,10,11;
    --paper:#f1efea;
    --paper-rgb:241,239,234;
    --paper-tint:#e8e5de;
    --ink-tint:#18181a;

    /* ============ 字体(跨主题固定) ============ */
    --mono:"IBM Plex Mono",ui-monospace,monospace;
    --serif-en:"Playfair Display","Source Serif 4",Georgia,serif;
    --serif-body-en:"Source Serif 4",Georgia,serif;
    --serif-zh:"Noto Serif SC",source-han-serif-sc,serif;
    --sans-zh:"Noto Sans SC",source-han-sans-sc,sans-serif;
  }
  *{box-sizing:border-box;margin:0;padding:0}
  html,body{width:100%;height:100%;overflow:hidden;background:var(--ink);color:var(--paper);font-family:var(--sans-zh);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}

  /* ============ WebGL 双背景 ============ */
  canvas.bg{position:fixed;inset:0;width:100vw;height:100vh;z-index:0;display:block;transition:opacity 1.2s ease}
  canvas#bg-light{opacity:0}
  canvas#bg-dark{opacity:1}
  body.light-bg canvas#bg-light{opacity:1}
  body.light-bg canvas#bg-dark{opacity:0}

  /* ============ Deck 容器 + 翻页 ============ */
  /* width: NSLIDES * 100vw，会在 JS 里动态矫正 */
  #deck{position:fixed;inset:0;width:10000vw;height:100vh;display:flex;flex-wrap:nowrap;transition:transform .9s cubic-bezier(.77,0,.175,1);z-index:10;will-change:transform}
  .slide{width:100vw;height:100vh;flex:0 0 100vw;position:relative;padding:6vh 6vw 10vh 6vw;display:flex;flex-direction:column;overflow:hidden}
  .slide.light{color:var(--ink)}
  .slide.dark{color:var(--paper)}

  /* 默认页：遮罩较厚，保证文字可读 */
  .slide::before{content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;transition:background .7s ease}
  .slide.light::before{background:rgba(var(--paper-rgb),.78);backdrop-filter:blur(3px)}
  .slide.dark::before{background:rgba(var(--ink-rgb),.78);backdrop-filter:blur(3px)}
  /* Hero 页：遮罩大幅降低，让 WebGL 背景明显透出 */
  .slide.hero.light::before{background:rgba(var(--paper-rgb),.16);backdrop-filter:none}
  .slide.hero.dark::before{background:rgba(var(--ink-rgb),.12);backdrop-filter:none}
  /* Hero 页顶底微弱渐隐，保证 chrome/foot 区域可读 */
  .slide.hero::after{content:"";position:absolute;inset:0;z-index:-1;pointer-events:none}
  .slide.hero.light::after{background:linear-gradient(180deg,rgba(var(--paper-rgb),.28) 0%,rgba(var(--paper-rgb),0) 14%,rgba(var(--paper-rgb),0) 86%,rgba(var(--paper-rgb),.28) 100%)}
  .slide.hero.dark::after{background:linear-gradient(180deg,rgba(var(--ink-rgb),.32) 0%,rgba(var(--ink-rgb),0) 14%,rgba(var(--ink-rgb),0) 86%,rgba(var(--ink-rgb),.32) 100%)}

  /* ============ Magazine chrome：顶部 meta + 底部 foot ============ */
  .chrome{display:flex;justify-content:space-between;align-items:flex-start;font-family:var(--mono);font-size:12px;letter-spacing:.18em;text-transform:uppercase;opacity:.7}
  .chrome .left,.chrome .right{display:flex;gap:2.4em;align-items:center}
  .chrome .sep{width:40px;height:1px;background:currentColor;opacity:.4}
  .foot{margin-top:auto;display:flex;justify-content:space-between;align-items:flex-end;font-family:var(--mono);font-size:12px;letter-spacing:.14em;text-transform:uppercase;opacity:.55}
  .foot .title{font-family:var(--serif-zh);font-weight:400;letter-spacing:.05em;text-transform:none;opacity:.75;font-size:13px}

  .tag{display:inline-block;font-family:var(--mono);font-size:11px;letter-spacing:.24em;text-transform:uppercase;padding:6px 14px;border:1px solid currentColor;opacity:.85}
  .rule{width:100%;height:1px;background:currentColor;opacity:.25;margin:3vh 0}
  .rule.v{width:1px;height:100%;margin:0}

  /* ============ 字体规则 ============
     · 衬线（Noto Serif SC / Playfair）：大标题、重点金句、数字
     · 非衬线（Noto Sans SC）：正文描述、body、补充说明
     · 等宽（IBM Plex Mono）：kicker、meta 小标签、foot 右侧
  */
  .kicker{font-family:var(--mono);font-size:12px;letter-spacing:.3em;text-transform:uppercase;opacity:.6;margin-bottom:2.6vh}
  .display{font-family:var(--serif-en);font-weight:700;font-size:11vw;line-height:.92;letter-spacing:-.025em}
  .display-zh{font-family:var(--serif-zh);font-weight:700;font-size:7.8vw;line-height:1.04;letter-spacing:-.005em}
  .h1-zh{font-family:var(--serif-zh);font-weight:700;font-size:4.6vw;line-height:1.12;letter-spacing:-.005em}
  .h2-zh{font-family:var(--serif-zh);font-weight:600;font-size:3.2vw;line-height:1.2;letter-spacing:0}
  .h3-zh{font-family:var(--serif-zh);font-weight:500;font-size:1.9vw;line-height:1.35}
  .body-zh{font-family:var(--sans-zh);font-weight:400;font-size:max(15px,1.22vw);line-height:1.75;opacity:.82;letter-spacing:.01em}
  .body-serif{font-family:var(--serif-zh);font-weight:400;font-size:max(15px,1.3vw);line-height:1.65;opacity:.88}
  .lead{font-family:var(--serif-zh);font-weight:400;font-size:1.9vw;line-height:1.4;opacity:.85}
  .meta{font-family:var(--mono);font-size:max(11px,.88vw);letter-spacing:.16em;text-transform:uppercase;opacity:.6}
  .big-num{font-family:var(--serif-en);font-weight:800;font-size:10vw;line-height:.85;letter-spacing:-.03em;font-feature-settings:"tnum"}
  .mid-num{font-family:var(--serif-en);font-weight:700;font-size:5.5vw;line-height:.88;letter-spacing:-.02em;font-feature-settings:"tnum"}
  .ghost{font-family:var(--serif-en);font-weight:900;font-size:34vw;line-height:.8;opacity:.06;letter-spacing:-.04em;position:absolute;font-feature-settings:"tnum"}
  em{font-style:italic;font-family:var(--serif-en)}
  .en{font-family:var(--serif-en);font-style:italic;font-weight:500}

  /* ============ 布局工具 ============ */
  .col{display:flex;flex-direction:column;gap:2.4vh}
  .row{display:flex;align-items:center;gap:3vw}
  .grid-6{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(2,1fr);gap:4vw 6vw;flex:1;align-content:center;padding:2vh 0}
  .grid-9{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(3,1fr);gap:3vh 4vw;flex:1;align-content:center}
  .grid-4{display:grid;grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(2,1fr);gap:4vh 6vw;flex:1;align-content:center}
  .grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:4vw;flex:1;align-content:center}
  .split{display:grid;grid-template-columns:1fr 1fr;gap:4vw;flex:1;align-items:center}
  .split-55{display:grid;grid-template-columns:55fr 45fr;gap:5vw;flex:1;align-items:stretch}
  .fill{flex:1}
  .center{align-items:center;justify-content:center;text-align:center}
  .bottom-left{position:absolute;left:6vw;bottom:9vh;max-width:50vw}
  .bottom-right{position:absolute;right:6vw;bottom:9vh;max-width:50vw;text-align:right}
  .top-right{position:absolute;right:6vw;top:6vh;text-align:right}

  /* ============ Stat（数字矩阵） ============ */
  .stat{display:flex;flex-direction:column;gap:1vh;align-items:flex-start}
  .stat .n{font-family:var(--serif-en);font-weight:800;font-size:8vw;line-height:.88;letter-spacing:-.03em;font-feature-settings:"tnum"}
  .stat .l{font-family:var(--sans-zh);font-size:max(13px,1.05vw);opacity:.7;margin-top:1vh;font-weight:400;line-height:1.5}
  .stat .m{font-family:var(--mono);font-size:10px;letter-spacing:.22em;text-transform:uppercase;opacity:.5;margin-bottom:.2vh}

  /* ============ Callout（引用框） ============ */
  .callout{padding:3vh 2.4vw;border-left:3px solid currentColor;position:relative;font-family:var(--serif-zh);font-size:max(15px,1.2vw);line-height:1.55;opacity:.92}
  .slide.light .callout{background:rgba(var(--ink-rgb),.05)}
  .slide.dark .callout{background:rgba(var(--paper-rgb),.06)}
  .callout .cite{display:block;margin-top:1.6vh;font-family:var(--mono);font-size:11px;letter-spacing:.2em;text-transform:uppercase;opacity:.6}
  .callout .q-big{font-family:var(--serif-zh);font-weight:600;font-size:max(17px,1.6vw);line-height:1.42}

  /* ============ Platform（平台卡） ============ */
  .plat{display:flex;flex-direction:column;justify-content:flex-end;padding:2vh 0;border-top:1px solid currentColor;border-color:rgba(127,127,127,.35)}
  .plat .name{font-family:var(--serif-zh);font-weight:700;font-size:1.8vw;margin-bottom:.6vh}
  .plat .nb{font-family:var(--serif-en);font-weight:700;font-size:3.2vw;letter-spacing:-.02em;line-height:1;font-feature-settings:"tnum"}
  .plat .sub{font-family:var(--mono);font-size:10px;letter-spacing:.18em;text-transform:uppercase;opacity:.55;margin-top:.6vh}
  .plat .fill{font-family:var(--sans-zh);font-weight:300;font-size:2.4vw;opacity:.28;letter-spacing:-.01em;line-height:1}

  /* ============ Rowline（表格行） ============ */
  .rowline{display:grid;grid-template-columns:1fr 2fr 1fr;gap:2vw;padding:2.2vh 0;border-top:1px solid currentColor;align-items:center;border-color:rgba(127,127,127,.25)}
  .rowline:last-child{border-bottom:1px solid currentColor;border-color:rgba(127,127,127,.25)}
  .rowline .k{font-family:var(--serif-zh);font-weight:700;font-size:1.7vw}
  .rowline .v{font-family:var(--sans-zh);font-weight:400;font-size:max(14px,1.2vw);opacity:.85;line-height:1.55}
  .rowline .m{font-family:var(--mono);font-size:11px;letter-spacing:.2em;text-transform:uppercase;opacity:.6;justify-self:end}

  /* ============ Pillar（支柱卡片） ============ */
  .pillar{display:flex;flex-direction:column;gap:1.8vh}
  .pillar .ic{font-family:var(--serif-en);font-style:italic;font-size:2.6vw;opacity:.45;font-weight:400}
  .pillar .ic svg{width:2.8vw;height:2.8vw;stroke-width:1.2;opacity:.7}
  .pillar .t{font-family:var(--serif-zh);font-weight:700;font-size:2.4vw;line-height:1.1}
  .pillar .d{font-family:var(--sans-zh);font-weight:400;font-size:max(14px,1.1vw);opacity:.76;line-height:1.6}

  /* ============ Signature / Highlight ============ */
  .sign{font-family:var(--serif-en);font-style:italic;font-weight:500;font-size:2vw;opacity:.7}
  .hi{position:relative;display:inline}
  .slide.dark .hi::after{content:"";position:absolute;left:-.1em;right:-.1em;bottom:-.05em;height:.28em;background:rgba(var(--paper-rgb),.15);z-index:-1}
  .slide.light .hi::after{content:"";position:absolute;left:-.1em;right:-.1em;bottom:-.05em;height:.28em;background:rgba(var(--ink-rgb),.08);z-index:-1}

  /* ============ Icons（Lucide via CDN） ============ */
  .ico{width:1em;height:1em;display:inline-block;vertical-align:-.12em;stroke:currentColor;fill:none;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}
  .ico-lg,.ico-md,.ico-sm{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round}
  .ico-lg{width:2.6vw;height:2.6vw;stroke-width:1.2;display:inline-block}
  .ico-md{width:1.8vw;height:1.8vw;stroke-width:1.3;display:inline-block;vertical-align:-.4em}
  .ico-sm{width:1.1vw;height:1.1vw;stroke-width:1.4;display:inline-block;vertical-align:-.15em;opacity:.7}

  /* ============ 图片占位（虚线框，提示设计师位置） ============ */
  .img-slot{border:1.5px dashed rgba(127,127,127,.4);display:flex;align-items:center;justify-content:center;flex-direction:column;gap:1vh;padding:2vh 2vw;font-family:var(--mono);font-size:10px;letter-spacing:.28em;text-transform:uppercase;opacity:.55;position:relative;aspect-ratio:16/9;width:100%;max-height:56vh;margin-inline:auto;box-sizing:border-box}
  .img-slot::before{content:"";position:absolute;inset:8px;border:1px solid currentColor;opacity:.2}
  .img-slot .plus{font-size:2vw;font-weight:300;opacity:.5;letter-spacing:0}
  .img-slot .label{position:relative;z-index:2;text-align:center}
  .img-slot.r-4x3{aspect-ratio:4/3}
  .img-slot.r-3x2{aspect-ratio:3/2}
  .img-slot.r-1x1{aspect-ratio:1/1}

  /* ============ 图片实填框（关键：固定高度 + 只裁底部） ============
     重要约束：高度用内联 height:Nvh 精确控制，不要用 aspect-ratio（会撑破布局）
     object-position:top center 保证严禁裁剪顶部和左右，只裁剪底部
  */
  .frame-img{overflow:hidden;position:relative;background:rgba(0,0,0,.04);box-sizing:border-box;width:100%;border-radius:4px}
  .slide.dark .frame-img{background:rgba(255,255,255,.04);border-color:rgba(255,255,255,.12)}
  .frame-img > img{width:100%;height:100%;object-fit:cover;object-position:top center;display:block}
  .frame-cap{display:flex;justify-content:space-between;align-items:baseline;gap:1vw;margin-top:.8vh;font-family:var(--mono);font-size:10px;letter-spacing:.22em;text-transform:uppercase;opacity:.72}
  .frame-cap .pf{font-family:var(--serif-zh);font-weight:600;font-size:max(13px,1vw);letter-spacing:.04em;text-transform:none;opacity:.94}
  .frame-cap .nb{font-family:var(--serif-en);font-style:italic;font-size:max(15px,1.2vw);letter-spacing:.02em;text-transform:none;opacity:.88}
  .frame-cap .idx{font-family:var(--mono);opacity:.5}
  figure.tile{display:flex;flex-direction:column;margin:0;min-width:0}
  figure.tile > .frame-img{flex:0 0 auto}

  /* ============ 导航 ============ */
  #nav{position:fixed;left:50%;bottom:2.6vh;transform:translateX(-50%);z-index:30;display:flex;gap:10px;padding:8px 14px;border-radius:999px;background:rgba(0,0,0,.18);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
  #nav .dot{width:8px;height:8px;border-radius:50%;background:rgba(255,255,255,.3);cursor:pointer;transition:all .3s ease;border:0;padding:0}
  #nav .dot:hover{background:rgba(255,255,255,.5);transform:scale(1.15)}
  #nav .dot.active{background:rgba(255,255,255,.95);width:22px;border-radius:999px}
  body.light-bg #nav{background:rgba(255,255,255,.25)}
  body.light-bg #nav .dot{background:rgba(var(--ink-rgb),.25)}
  body.light-bg #nav .dot.active{background:rgba(var(--ink-rgb),.9)}
  #hint{position:fixed;bottom:3vh;right:3vw;z-index:30;font-family:var(--mono);font-size:10px;letter-spacing:.2em;text-transform:uppercase;opacity:.4;mix-blend-mode:difference;color:#aaa}

  /* ============================================================
     ============ LAYOUTS API · 面向 agent 的类（v2）============
     所有 layouts.md 中的骨架都基于下面这套命名。
     如果你在 layouts.md 里看到某个类，它必须在下面有定义。
     ============================================================ */

  /* ---------- .frame：每页主内容容器 ---------- */
  .frame{flex:1;display:flex;flex-direction:column;min-height:0}
  /* 当 .frame 同时加了 grid 类时，grid 的 display:grid 覆盖 flex */
  .frame.grid-2-7-5,
  .frame.grid-2-6-6,
  .frame.grid-2-8-4,
  .frame.grid-3-3,
  .frame.grid-6{display:grid}

  /* ---------- 标题层级（API 名称，衬线为主） ---------- */
  .h-hero{
    font-family:var(--serif-zh);
    font-weight:900;
    font-size:10vw;
    line-height:.96;
    letter-spacing:-.02em;
  }
  .h-xl{
    font-family:var(--serif-zh);
    font-weight:700;
    font-size:6.2vw;
    line-height:1.08;
    letter-spacing:-.01em;
  }
  .h-sub{
    font-family:var(--serif-zh);
    font-weight:500;
    font-size:3.1vw;
    line-height:1.25;
    letter-spacing:0;
    opacity:.7;
  }
  .h-md{
    font-family:var(--serif-zh);
    font-weight:600;
    font-size:2.3vw;
    line-height:1.3;
  }
  /* 英文标题专用（Playfair 衬线） */
  .h-hero-en,.h-xl-en{font-family:var(--serif-en);letter-spacing:-.025em}

  /* ---------- lead 引语 ---------- */
  .lead{
    font-family:var(--serif-zh);
    font-weight:400;
    font-size:1.75vw;
    line-height:1.5;
    opacity:.86;
  }

  /* ---------- meta-row 底部元数据 ---------- */
  .meta-row{
    display:flex;
    gap:1.2em;
    align-items:baseline;
    flex-wrap:wrap;
    font-family:var(--mono);
    font-size:max(12px,.92vw);
    letter-spacing:.16em;
    text-transform:uppercase;
    opacity:.6;
  }

  /* ---------- stat-card（数据大字报用） ---------- */
  .stat-card{
    display:flex;
    flex-direction:column;
    gap:.8vh;
    align-items:flex-start;
    padding-top:1.6vh;
    border-top:1px solid currentColor;
    border-color:rgba(127,127,127,.3);
  }
  .stat-card .stat-label{
    font-family:var(--mono);
    font-size:max(10px,.78vw);
    letter-spacing:.24em;
    text-transform:uppercase;
    opacity:.55;
  }
  .stat-card .stat-nb{
    font-family:var(--serif-en);
    font-weight:800;
    font-size:5.8vw;
    line-height:.9;
    letter-spacing:-.03em;
    font-feature-settings:"tnum";
    margin-top:.4vh;
  }
  .stat-card .stat-nb .stat-unit{
    font-family:var(--serif-zh);
    font-weight:500;
    font-size:.38em;
    letter-spacing:0;
    opacity:.72;
    margin-left:.14em;
  }
  .stat-card .stat-note{
    font-family:var(--sans-zh);
    font-weight:400;
    font-size:max(13px,1.05vw);
    line-height:1.5;
    opacity:.72;
    margin-top:.6vh;
  }
  /* 当 stat-card 用于 grid-4（2x2），数字可以更大 */
  .grid-4 .stat-card .stat-nb{font-size:7.5vw}
  /* 当只有 3 个，字也可以稍大 */
  .grid-3 .stat-card .stat-nb{font-size:6.8vw}

  /* ---------- pipeline（流水线） ---------- */
  .pipeline-section{
    margin-top:4.4vh;
    padding-top:2.8vh;
    border-top:1px dashed rgba(127,127,127,.32);
  }
  .pipeline-section:first-of-type{
    border-top:0;
    padding-top:0;
    margin-top:3vh;
  }
  .pipeline-label{
    font-family:var(--mono);
    font-size:max(11px,.85vw);
    letter-spacing:.24em;
    text-transform:uppercase;
    opacity:.62;
    margin-bottom:2.2vh;
  }
  .pipeline{
    display:grid;
    grid-template-columns:repeat(5,1fr);
    gap:1.2vw;
  }
  .pipeline[data-cols="3"]{grid-template-columns:repeat(3,1fr)}
  .pipeline[data-cols="4"]{grid-template-columns:repeat(4,1fr)}
  .pipeline[data-cols="6"]{grid-template-columns:repeat(6,1fr)}
  .step{
    display:flex;
    flex-direction:column;
    gap:.8vh;
    padding-top:1.4vh;
    border-top:1px solid currentColor;
    border-color:rgba(127,127,127,.35);
  }
  .step-nb{
    font-family:var(--serif-en);
    font-style:italic;
    font-weight:500;
    font-size:1.15vw;
    opacity:.45;
  }
  .step-title{
    font-family:var(--sans-zh);
    font-weight:700;
    font-size:1.55vw;
    letter-spacing:.01em;
    line-height:1.2;
  }
  .step-desc{
    font-family:var(--sans-zh);
    font-weight:400;
    font-size:max(12px,.95vw);
    line-height:1.45;
    opacity:.72;
  }

  /* ---------- 网格（layouts.md 所用） ---------- */
  /* 这些类独立挂到任何容器上都能生效，不依赖 .frame 复合选择器 */
  .grid-2-7-5{display:grid;grid-template-columns:7fr 5fr;gap:3vw 4vh;align-items:start}
  .grid-2-6-6{display:grid;grid-template-columns:1fr 1fr;gap:3vw 4vh;align-items:start}
  .grid-2-8-4{display:grid;grid-template-columns:8fr 4fr;gap:3vw 4vh;align-items:start}
  .grid-3-3{
    display:grid;
    grid-template-columns:repeat(3,1fr);
    grid-auto-rows:minmax(0,1fr);
    gap:2.4vh 2vw;
  }
  /* grid-6 已在旧样式里定义为 3x2，这里仅补 align */

  /* ---------- 图片 frame-img（layouts.md 主命名） ---------- */
  /* 在旧样式里已定义，这里补 img-cap 命名别名与增强 */
  figure.frame-img{margin:0;display:flex;flex-direction:column;min-width:0}
  .img-cap{
    display:block;
    margin-top:.8vh;
    font-family:var(--mono);
    font-size:max(10px,.8vw);
    letter-spacing:.22em;
    text-transform:uppercase;
    opacity:.6;
  }
  /* callout src 命名别名 */
  .callout-src{
    display:block;
    margin-top:1.6vh;
    font-family:var(--mono);
    font-size:11px;
    letter-spacing:.2em;
    text-transform:uppercase;
    opacity:.6;
  }

  /* ---------- chrome & foot 补位（layouts.md 简单写法） ---------- */
  .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}

  /* ---------- 响应式降级 ---------- */
  @media (max-width:900px){
    .display{font-size:16vw}
    .display-zh{font-size:12vw}
    .h1-zh{font-size:7vw}
    .h-hero{font-size:14vw}
    .h-xl{font-size:9vw}
    .pipeline{grid-template-columns:repeat(2,1fr)}
    .grid-2-7-5,.grid-2-6-6,.grid-2-8-4{grid-template-columns:1fr}
  }
</style>
</head>
<body>

<canvas id="bg-dark" class="bg"></canvas>
<canvas id="bg-light" class="bg"></canvas>
<div id="hint">← → 翻页 · ESC 索引</div>

<div id="deck">

<!-- ============================================================
     SLIDES 插入区 · 在此处填充所有 <section class="slide ..."> 页面
     每页模板参考 references/page-patterns.md
     页面组件参考 references/components.md
     ============================================================ -->

<!-- SLIDES_HERE -->

</div>

<div id="nav"></div>

<script>
/* =============== WebGL 双背景 ===============
   深色页：Holographic Dispersion（全息色散 · 钛金暗流）—— 彩虹微扰、鼠标径向涟漪
   浅色页：Spiral Vortex（旋转涡流 · 银色珍珠）—— domain-warp 流动、无中心
   修改风格请参考 references/webgl-backgrounds.md
*/
const VS = `attribute vec2 position;void main(){gl_Position=vec4(position,0.0,1.0);}`;

const FS_DARK = `precision highp float;
uniform vec2 u_resolution;uniform float u_time;uniform vec2 u_mouse;
vec3 palette(float t,vec3 a,vec3 b,vec3 c,vec3 d){return a+b*cos(6.28318*(c*t+d));}
void main(){
  vec2 uv=gl_FragCoord.xy/u_resolution.xy;
  vec2 p=uv*2.0-1.0;p.x*=u_resolution.x/u_resolution.y;
  vec2 m=u_mouse*2.0-1.0;m.x*=u_resolution.x/u_resolution.y;
  float md=length(p-m);
  float mr=sin(md*15.0-u_time*4.0)*exp(-md*3.0);p+=mr*0.08;
  vec2 p0=p;
  for(float i=1.0;i<4.0;i++){
    p.x+=0.1/i*sin(i*3.0*p.y+u_time*0.4)+0.05;
    p.y+=0.1/i*cos(i*2.0*p.x+u_time*0.3)-0.05;
  }
  float r=length(p);float ang=atan(p.y,p.x);
  vec3 a=vec3(0.12,0.12,0.13);
  vec3 b=vec3(0.03,0.04,0.05);
  vec3 c=vec3(1.0,1.0,1.0);
  vec3 d=vec3(0.1,0.2,0.4);
  vec3 col=palette(r*1.5+p0.x*0.5+u_time*0.1,a,b,c,d);
  float disp=sin(r*25.0-u_time*1.5+ang*2.0)*0.5+0.5;
  col+=vec3(disp*0.015,disp*0.01,disp*0.02);
  float hi=pow(sin(p.x*4.0+p.y*3.0+u_time)*0.5+0.5,8.0);
  col+=hi*0.08;
  vec3 base=vec3(0.05,0.05,0.06);
  col=mix(base,col,0.85);
  gl_FragColor=vec4(col,1.0);
}`;

const FS_LIGHT = `precision highp float;
uniform vec2 u_resolution;uniform float u_time;uniform vec2 u_mouse;
float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
float noise(vec2 p){
  vec2 i=floor(p),f=fract(p);
  float a=hash(i),b=hash(i+vec2(1,0));
  float c=hash(i+vec2(0,1)),d=hash(i+vec2(1,1));
  vec2 u=f*f*(3.0-2.0*f);
  return mix(a,b,u.x)+(c-a)*u.y*(1.0-u.x)+(d-b)*u.x*u.y;
}
float fbm(vec2 p){
  float v=0.0,a=0.5;
  mat2 m=mat2(0.80,0.60,-0.60,0.80);
  for(int i=0;i<5;i++){v+=a*noise(p);p=m*p*2.02;a*=0.5;}
  return v;
}
void main(){
  vec2 uv=gl_FragCoord.xy/u_resolution.xy;
  vec2 p=uv;p.x*=u_resolution.x/u_resolution.y;
  vec2 m=u_mouse;m.x*=u_resolution.x/u_resolution.y;
  vec2 md=p-m;float dl=length(md);
  p+=normalize(md+vec2(0.0001))*exp(-dl*5.0)*0.03;
  vec2 q=vec2(fbm(p*1.8+u_time*0.07),fbm(p*1.8+vec2(5.2,1.3)+u_time*0.06));
  vec2 r=vec2(fbm(p*2.0+q*1.3+vec2(1.7,9.2)+u_time*0.05),
              fbm(p*2.0+q*1.3+vec2(8.3,2.8)+u_time*0.04));
  float f=fbm(p*2.2+r*1.5);
  vec3 silverDark=vec3(0.86,0.85,0.84);
  vec3 paper=vec3(0.955,0.945,0.925);
  vec3 col=mix(silverDark,paper,f);
  float ph=r.x*2.2+u_time*0.35;
  col+=vec3(0.78,0.62,0.92)*sin(ph)*0.055;
  col+=vec3(0.55,0.72,0.95)*sin(ph*0.8+2.0)*0.05;
  float hl=smoothstep(0.48,0.92,f);
  col+=hl*0.06;
  gl_FragColor=vec4(col,1.0);
}`;

const mouse={x:0.5,y:0.5};
addEventListener('mousemove',e=>{mouse.x=e.clientX/innerWidth;mouse.y=e.clientY/innerHeight});

function bootGL(canvasId, fsSrc){
  const canvas=document.getElementById(canvasId);
  const gl=canvas.getContext('webgl',{alpha:false,antialias:true});
  if(!gl) return ()=>false;
  const mk=(t,s)=>{const sh=gl.createShader(t);gl.shaderSource(sh,s);gl.compileShader(sh);return sh};
  const prog=gl.createProgram();
  gl.attachShader(prog,mk(gl.VERTEX_SHADER,VS));
  gl.attachShader(prog,mk(gl.FRAGMENT_SHADER,fsSrc));
  gl.linkProgram(prog);gl.useProgram(prog);
  const buf=gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER,buf);
  gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),gl.STATIC_DRAW);
  const pos=gl.getAttribLocation(prog,'position');
  gl.enableVertexAttribArray(pos);gl.vertexAttribPointer(pos,2,gl.FLOAT,false,0,0);
  const lRes=gl.getUniformLocation(prog,'u_resolution');
  const lT=gl.getUniformLocation(prog,'u_time');
  const lM=gl.getUniformLocation(prog,'u_mouse');
  const resize=()=>{
    const d=Math.min(window.devicePixelRatio||1,2);
    canvas.width=innerWidth*d;canvas.height=innerHeight*d;
    gl.viewport(0,0,canvas.width,canvas.height);
  };
  addEventListener('resize',resize);resize();
  return (tSec)=>{
    gl.uniform2f(lRes,canvas.width,canvas.height);
    gl.uniform1f(lT,tSec);
    gl.uniform2f(lM,mouse.x,1-mouse.y);
    gl.drawArrays(gl.TRIANGLES,0,6);
    return true;
  };
}
const drawDark=bootGL('bg-dark',FS_DARK);
const drawLight=bootGL('bg-light',FS_LIGHT);
const t0=Date.now();
(function loop(){
  const t=(Date.now()-t0)/1000;
  drawDark(t);drawLight(t);
  requestAnimationFrame(loop);
})();

// =============== 导航（翻页 / 圆点 / 键盘 / 滚轮 / 触屏） ===============
const deck=document.getElementById('deck');
const slides=deck.querySelectorAll('.slide');
const nav=document.getElementById('nav');
let idx=0,total=slides.length,lock=false;

// 关键：矫正 deck 宽度为 total * 100vw，否则翻页会错位
deck.style.width=(total*100)+'vw';

slides.forEach((s,i)=>{
  const b=document.createElement('button');
  b.className='dot';b.dataset.i=i;b.setAttribute('aria-label','Page '+(i+1));
  b.onclick=()=>go(i);
  nav.appendChild(b);
});

function go(n){
  if(lock)return;
  idx=Math.max(0,Math.min(total-1,n));
  deck.style.transform=`translateX(${-idx*100}vw)`;
  nav.querySelectorAll('.dot').forEach((d,i)=>d.classList.toggle('active',i===idx));
  /* 主题切换：优先读 data-theme，其次从 class（light/dark）推断 */
  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');
  lock=true;setTimeout(()=>lock=false,700);
}

addEventListener('keydown',e=>{
  if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '||e.key==='ArrowDown')go(idx+1);
  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);
});

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;}
  clearTimeout(wheelTO);wheelTO=setTimeout(()=>wheelAcc=0,150);
},{passive:true});

let tx=0,ty=0;
addEventListener('touchstart',e=>{tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:true});
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));
},{passive:true});

go(0);
</script>
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>


path: references/checklist.md
content: # 质量检查清单（Checklist）

这个清单来自"一人公司"分享 PPT 的真实迭代过程。每一条都是踩过坑之后总结的，按重要性排序。

生成 PPT 前，先通读一遍；生成后，逐项自检。

---

## 🔴 P0 · 一定不能犯的错

### 0. 生成前必须通过的类名校验(最重要)

**现象**：直接把 layouts.md 的骨架粘到新 HTML,结果样式全部丢失——大标题变成非衬线、数据大字报字体小得像正文、pipeline 多页糊成一坨、图片堆到浏览器底部。

**根因**：如果 `template.html` 的 `<style>` 里没有这些类的定义,浏览器就 fallback 到默认样式。

**做法**：
- **生成 PPT 前,必须先 `Read` `assets/template.html`**,确认 layouts.md 里用到的类都已定义
- 最常见遗漏的类:`h-hero / h-xl / h-sub / h-md / lead / meta-row / stat-card / stat-label / stat-nb / stat-unit / stat-note / pipeline-section / pipeline-label / pipeline / step / step-nb / step-title / step-desc / grid-2-7-5 / grid-2-6-6 / grid-2-8-4 / grid-3-3 / frame / img-cap / callout-src`
- 如果某个类确实缺了,**在 template.html 的 `<style>` 里补上**,不要在每页 inline 重写
- 生成后打开浏览器,如果看到"大标题是非衬线"或"pipeline 步骤挤在一行",几乎 100% 是这个问题

### 1. 不要用 emoji 作图标

**现象**：在中式杂志风格里用 emoji（🎯 💡 ✅）会立刻破坏格调。

**做法**：用 Lucide 图标库，CDN 方式引用：

```html
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
...
<i data-lucide="target" class="ico-md"></i>
...
<script>lucide.createIcons();</script>
```

常用图标名：`target / palette / search-check / compass / share-2 / crown / check-circle / x-circle / plus / arrow-right / grid-2x2 / network`

### 2. 图片只允许裁底部，左右和顶部绝对不能切

**现象**：用 `aspect-ratio` 撑图，网格会在父容器不足时堆叠或切掉图片关键信息（比如截图上部的标题栏）。

**做法**：图片容器用**固定 height + overflow hidden**，图片走 `object-fit:cover + object-position:top`：

```html
<figure class="frame-img" style="height:26vh">
  <img src="screenshot.png">
</figure>
```

CSS 里 `.frame-img img` 已经预设 `object-position:top`，只裁底。

**绝不用这种写法**（会在网格中撑破容器）：

```html
<!-- 坏例 -->
<figure class="frame-img" style="aspect-ratio: 16/9">...</figure>
```

**例外**：单张主视觉（非网格内）可以用 `aspect-ratio + max-height`，因为父容器会兜底。

### 2b. 亮页面配暗 WebGL = 灰蒙蒙(主题切换没生效)

**现象**:所有 light 页面背景都像蒙了一层灰,甚至 hero light 也灰。

**根因**:JS 根据 slide 的主题切换两张 canvas 的 opacity。如果整个 deck 开场是 hero dark,而没有任何机制能把 bg 切到 light,body 永远不加 `light-bg` 类,`canvas#bg-dark` 一直在上面。

**做法**:
- 模板里 `go()` 函数已改为从 `classList` 推断主题(`light` / `dark`),所以 **slide 必须明确带 `light` 或 `dark` 类**。不要漏写,更不要用其他自定义主题名
- hero 页用 `hero light` / `hero dark`,正文页用 `light` / `dark`。只写 `hero` 不带主题色是坏的
- 一个 deck 里必须至少有一个 **非 hero 的 light 页**,确保 body 有机会加 `light-bg`

### 2b-2. 整个 deck 全是 light,没有节奏

**现象**:除封面 `hero dark` 外,其余所有页面默认写 `light`——视觉平淡,没有呼吸感,白花花一片。

**根因**:layouts.md 的骨架默认全写 `light`,如果只是粘贴骨架不调整主题,就会全亮。

**做法**:
- **生成前画"主题节奏表"**:每一页写清 `hero dark` / `hero light` / `light` / `dark` 中的哪一个,对齐后再写代码
- **硬规则**:连续 3 页以上同主题 = 不允许;8 页以上必须有 ≥1 `hero dark` + ≥1 `hero light`;不能全是 `light` 正文页——必须有 `dark` 正文页
- **按布局选主题**(详见 layouts.md 开头"主题节奏规划"):
  - 左文右图(Layout 4)、大引用(Layout 8)、图文混排(Layout 10)→ **`light` / `dark` 交替**
  - 大字报、图片网格、Pipeline、对比页 → `light`(截图/数字/流程需要亮底)
  - 封面、问题页 → `hero dark`
  - 章节幕封 → `hero dark` 与 `hero light` 交替
- **生成后自检**:`grep 'class="slide' index.html`,目视确认节奏有交错

### 2c. chrome 和 kicker 不要写同一句话

**现象**:左上角 `.chrome` 写"Design First · 设计先行",同一页里 `.kicker` 又写"Phase 01 · 设计阶段"——同义翻译,AI 味浓。

**做法**:
- **chrome = 杂志页眉 / 导航标签**:跨多页可相同(如 "Act II · Workflow"、"Data · Result"、"lukew.com · 2026.04")
- **kicker = 本页独一份的引导句**:短、有钩子、是大标题的"小前缀"(如 "BUT"、"一个人,做了什么。"、"The Question")
- 一个描述栏目,一个描述这一页——绝不互相翻译

### 3. 大标题字号不能超过屏宽 / 单字数

**现象**：中文大标题字号设太大（比如 13vw），结果每行只容 1 个字，强制换行非常难看。

**做法**：
- `h-hero`（最大）：10vw，**且标题长度 ≤ 5 字**
- `h-xl`（次大）：6vw-7vw
- 长标题用 `<br>` 手工断行，不要依赖自动换行
- 必要时加 `white-space:nowrap`

**示例**：`我不是程序员。`（6 字）用 `h-xl` 7.2vw + nowrap，一行排完。

### 4. 字体分工：标题衬线、正文非衬线

**做法**：
- 大标题、重点 quote、数字大字 → **衬线字体**（Noto Serif SC + Playfair Display + Source Serif）
- 正文、描述、pipeline 步骤名 → **非衬线字体**（Noto Sans SC + Inter）
- 元数据、代码、标签 → **等宽字体**（IBM Plex Mono + JetBrains Mono）

所有字体用 Google Fonts CDN 引入，模板里已预设。

### 4b. 图片不要用 `align-self:end` 贴底

**现象**：左文右图布局里,为了让右列图片和左列 callout 底部对齐,在 `<figure>` 上加 `align-self:end`。结果:
- 如果父容器不是 grid(比如类名没定义),`align-self` 完全失效,图片掉到文档流最下面被浏览器底栏遮挡
- 即使是 grid,图片会在 cell 里贴底,低分屏上仍然被 `.foot` 和 `#nav` 圆点遮挡

**做法**:
- 图文混排**必须用 `.frame.grid-2-7-5`**(或 `.grid-2-6-6`/`.grid-2-8-4`)
- 右列 `<figure class="frame-img">` 用 **标准比例 16/10 或 4/3 + max-height:56vh**,自然贴顶即可
- 要让左列 callout 看起来"贴底",给**左列**加 flex column + `justify-content:space-between`,不要动右列

### 4c. 图片不要用原图奇葩比例

**现象**:`aspect-ratio: 2592/1798` 这种从原图复制的比例,在不同屏幕下撑出奇怪的空白或溢出。

**做法**:无论原图什么比例,占位器固定用标准比例 **16/10 / 4/3 / 3/2 / 1/1 / 16/9**。图片自动 `object-fit:cover + object-position:top`,顶部不裁,底部裁掉一点无伤大雅。

### 5. 不要给图片加厚边框 / 阴影

**现象**：为了"高级感"加了强阴影或黑框，瞬间变成商务 PPT。

**做法**：最多 1-4px 的微圆角 + **极淡的底噪**（已在模板里）。不要加 `box-shadow`，不要加 `border`（除非 1px 极淡的灰）。

---

## 🟡 P1 · 排版节奏

### 6. Hero 页和非 hero 页要交替

**推荐节奏**（25-30 页）：
```
Hero Cover → Act Divider (hero) → 3-4 pages non-hero → Act Divider (hero)
→ 4-5 pages non-hero → Hero Question → ... → Hero Close
```

连续 2 页以上 hero 会让人疲劳，连续 4 页以上 non-hero 会让节奏死。

### 7. 大字报页和密集页要交替

大字报（big numbers / hero question）和密集页（pipeline / image grid）交替出现，听众眼睛才不累。

### 8. 同一概念的英文/中文用法要统一

**现象**：一会儿写 "Skills"，一会儿写 "技能"，一会儿写 "薄承载厚技能"，全篇不一致。

**做法**：
- 术语优先用**英文单词**（Skills / Harness / Pipeline / Workflow），这些都是圈内熟悉词
- **别硬翻译**，硬翻译反而生硬
- 整个 deck 里同一个词 1 个写法

### 9. 底部 chrome 的页码要一致

用 `XX / 总页数` 的格式（比如 `05 / 27`）。**不要在右上角加动态页码**（会和 `.chrome` 重复）。

---

## 🟢 P2 · 视觉打磨

### 10. WebGL 背景的遮罩透明度

**dark hero**：遮罩 12-15%（WebGL 明显透出）
**light hero**：遮罩 16-20%（WebGL 隐约可见，不抢字）
**普通 light/dark 页**：遮罩 92-95%（几乎不透）

如果页面文字非常少（hero question），遮罩可以再薄些；如果正文密集，必须加厚遮罩确保可读。

### 11. Light hero 的 shader 不能有强中心点

**现象**：Spiral Vortex、径向涟漪在 light 主题下太显眼，像 Windows 98 屏保。

**做法**：light hero 用 FBM 域扭曲驱动的无中心流动，底色保持银/纸色（接近 #F0F0F0 / #FBF8F3），彩虹偏色 subtle（0.05 以下）。

### 12. Dark hero 允许更多视觉冲击

Dark hero 可以用 Holographic Dispersion（钛金色散）等带中心结构的 shader，因为黑底能容纳更多视觉信息。

### 13. 左文右图的对齐

- 左列的文字组 `justify-content:space-between`：标题贴顶，引用框贴底
- 右列图片 `align-self:end`：和左列的底部元素对齐
- 网格整体 `align-items:start`（不是 `center` / `end`）

### 14. 图片的微弱圆角

所有 `.frame-img` 和 `.frame-img img` 都加 `border-radius:4px`，视觉上"柔和"但不软。**不要超过 8px**，否则像消费 app UI。

---

## 🔵 P3 · 操作细节

### 15. 图片路径用相对路径

图片放在 `images/` 文件夹下，HTML 里用相对路径 `images/xxx.png`，不要用绝对路径。

### 16. 页码在 `.chrome` 里写死

JS 会动态算总页数并扩展底部翻页圆点，但 `.chrome` 里的 `XX / N` 是写死的。加页/删页时要手工改 N。

### 17. 翻页导航要保留

模板默认支持：← → / 滚轮 / 触屏滑动 / 底部圆点 / Home·End。不要删 JS 里的导航逻辑。

### 18. 不要用 `height:100vh` 硬设，用 `min-height:80vh`

`100vh` 会让内容刚好卡满屏幕，但浏览器工具栏、标签栏会吃掉一部分高度，导致内容溢出。用 `min-height:80vh + align-content:center` 更稳。

---

## 🧪 最终自检清单

生成完 PPT 后，逐项对照这个清单（勾一下）：

```
预检(生成前)
  □ 已读过 template.html 的 <style>,确认所需类都存在
  □ 已决定每页用哪个 Layout(1-10)
  □ 已画出"主题节奏表":每页明确 hero dark / hero light / light / dark
  □ 节奏表满足硬规则:无连续 3 页同主题 / 有 ≥1 hero dark + ≥1 hero light(8 页以上) / 至少有 1 个 dark 正文页
  □ `<title>` 已改为实际 deck 标题(grep "[必填]" 应无结果)

内容
  □ 每一幕的页数比例合理(不会头重脚轻)
  □ 没有使用 emoji 作图标
  □ Skills / Harness 等术语用法统一
  □ 每页的 kicker + 标题 + 正文 三级信息清晰

排版
  □ 所有大标题没有出现 1 字 1 行的换行
  □ 图片网格用 height:Nvh 而非 aspect-ratio
  □ 图片只裁底部，顶部和左右完整
  □ 衬线/非衬线字体分工符合模板
  □ Pipeline 多组之间有明显分隔

视觉
  □ hero 页和 non-hero 页交替
  □ WebGL 背景在 hero 页可见
  □ 图片有微弱圆角
  □ 没有沉重的阴影和边框

交互
  □ ← → 翻页正常
  □ 底部圆点数量与总页数匹配
  □ chrome 里的页码和实际页号一致
  □ ESC 键触发索引视图（如果保留）
```

全勾完，才是合格的 PPT。


path: references/components.md
content: # 组件参考 · Components

这是 `magazine-web-ppt` skill 的组件手册。template.html 已经定义好了所有样式，这里只写"这个组件长什么样、怎么用"。

## 目录

- [基础 Slide 外壳](#基础-slide-外壳)
- [字体 Typography](#字体-typography)
- [Chrome & Foot](#chrome--foot)
- [Callout 引用框](#callout-引用框)
- [Stat 数字矩阵](#stat-数字矩阵)
- [Platform 平台卡](#platform-平台卡)
- [Rowline 表格行](#rowline-表格行)
- [Pillar 支柱卡](#pillar-支柱卡)
- [Tag & Kicker](#tag--kicker)
- [Figure 图片框](#figure-图片框)
- [Icons 图标](#icons-图标)
- [Ghost 巨型背景字](#ghost-巨型背景字)
- [Highlight 荧光标记](#highlight-荧光标记)

---

## 基础 Slide 外壳

每一页都是一个 `<section class="slide ...">`。必须包含 `data-theme` 属性（`light` 或 `dark`），JS 翻页时会根据这个属性切换背景。

```html
<section class="slide light" data-theme="light">   <!-- 浅色页 -->
<section class="slide dark" data-theme="dark">     <!-- 深色页 -->
<section class="slide light hero" data-theme="light">  <!-- Hero 页：浅色 + 薄遮罩透出 WebGL -->
<section class="slide dark hero" data-theme="dark">    <!-- Hero 页：深色 + 薄遮罩 -->
```

**light vs dark 的使用：交替使用**，每 2-3 页切换一次主题，避免连续超过 3 页同色。翻页时 WebGL 背景会自动在两个 shader 之间渐变过渡。

**hero 类的使用**：只给视觉主导的页面加（封面、金句页、章节过渡、结尾）。加 `hero` 后遮罩降到 12-16%，WebGL 背景会大幅透出，所以不要在 hero 页上放太多文字。

---

## 字体 Typography

字体分工是本模板最重要的规则，严禁混用。

| Class | 用途 | 字体 |
|---|---|---|
| `.display` | 超大号英文（Hero 页） | Playfair Display 700, 11vw |
| `.display-zh` | 超大号中文标题 | Noto Serif SC 700, 7.8vw |
| `.h1-zh` | 页面主标题 | Noto Serif SC 700, 4.6vw |
| `.h2-zh` | 副标题 | Noto Serif SC 600, 3.2vw |
| `.h3-zh` | 流水线步骤标题 | Noto Serif SC 500, 1.9vw |
| `.lead` | 引导段（比 body 大） | Noto Serif SC 400, 1.9vw |
| `.body-zh` | **正文/描述（非衬线）** | Noto Sans SC 400, 1.22vw |
| `.body-serif` | 正文（衬线） | Noto Serif SC 400, 1.3vw |
| `.kicker` | 小节提示（标题上方） | IBM Plex Mono, 12px uppercase |
| `.meta` | 元信息标签 | IBM Plex Mono, 0.88vw uppercase |
| `.big-num` | 巨型数字 | Playfair Display 800, 10vw |
| `.mid-num` | 中号数字 | Playfair Display 700, 5.5vw |

**核心规则**：
- **衬线**（`serif-zh` / `serif-en`）：标题、重点金句、数字 —— 用于"视觉重音"
- **非衬线**（`sans-zh`）：正文描述、大段阅读内容 —— 用于"信息密度"
- **等宽**（`mono`）：kicker、meta、foot 的英文标签 —— 用于"装饰节奏"

**强调技巧**：
- `<em class="en">英文词</em>` —— 把英文词渲染成 Playfair Display 斜体（很好看）
- `<em style="opacity:.65">短语</em>` —— 让标题后半段淡出，制造节奏

---

## Chrome & Foot

每一页的顶部和底部的元信息条。几乎所有页都应该有。

```html
<div class="chrome">
  <div class="left">
    <span>第一幕 · 硬数据</span>
    <span class="sep"></span>
    <span>Act I</span>
  </div>
  <div class="right"><span>02 / 27</span></div>
</div>

<!-- ... 页面主体 ... -->

<div class="foot">
  <div class="title">项目名 · CodePilot　|　github.com/codepilot</div>
  <div>Act I · Dev Numbers</div>
</div>
```

**规则**：
- `chrome.right` 总是放页码 `NN / TOTAL` （TOTAL 为总页数）
- `foot.title` 是中文说明，`foot.right` 是英文 act 标记
- chrome 和 foot 共同构成杂志感的"页眉页脚"

---

## Callout 引用框

展示金句 / 关键观点 / 他人引言。

```html
<div class="callout" style="max-width:80vw">
  <div class="q-big">"这东西在三年前，<br>需要一个十人团队做一年。"</div>
  <span class="cite">— 一个观察者的判断</span>
</div>
```

变体：
- 不带 cite：去掉 `<span class="cite">` 即可
- 带英文金句：`<em class="en">"Thin Harness, Fat Skills."</em>`
- 在 hero 页使用：外层加 `style="position:relative;z-index:2"`（避免被背景遮罩盖住）

---

## Stat 数字矩阵

展示数据指标，常与 `.grid-6` / `.grid-4` 配合。

```html
<div class="grid-6">
  <div class="stat">
    <span class="m">Duration</span>
    <span class="n">64<em style="font-size:.4em;opacity:.5;font-style:normal"> 天</em></span>
    <span class="l">从 0 到现在</span>
  </div>
  <!-- ... 更多 stat ... -->
</div>
```

三段式结构：`.m` 等宽小标签 → `.n` 巨型数字 → `.l` 描述说明。数字后的单位用 `<em>` 缩小到 0.4em，opacity 0.5。

**常用布局容器**：
- `.grid-6` — 3×2 网格（最常用，6 个 stat）
- `.grid-4` — 2×2 网格（4 个 stat）
- `.grid-3` — 3 等分单行（3 个 stat / pillar）

---

## Platform 平台卡

展示社交平台 / 渠道 + 粉丝数。

```html
<div class="plat">
  <div class="sub">Weibo</div>
  <div class="name">微博</div>
  <div class="nb">289K</div>
</div>
```

可选第四行（补充说明）：
```html
<div class="body-zh" style="font-size:max(11px,.8vw);opacity:.5;margin-top:.6vh">
  含小绿书同步
</div>
```

**"Also On" 变体**（补充平台）：
```html
<div class="plat" style="border-top-style:dashed;opacity:.72">
  <div class="sub">Also On</div>
  <div class="body-zh" style="font-weight:600;margin-top:.8vh">
    B 站　·　知乎
  </div>
</div>
```

---

## Rowline 表格行

列表式内容，每行一个条目。

```html
<div class="rowline">
  <div class="k">CLAUDE.md</div>
  <div class="v">你该怎么做事 —— 行为规则 + 工作偏好 + 禁止事项</div>
  <div class="m">EMPLOYEE · HANDBOOK</div>
</div>
```

三列结构：`.k` 衬线关键词 · `.v` 正文描述 · `.m` 等宽标签（右对齐）。第一个和最后一个 rowline 自动加上下边框。

**变体：2 列**：`style="grid-template-columns:1fr 3fr"` 去掉 `.m` 列。

---

## Pillar 支柱卡

三支柱结构，常用于"概念并列"类型页面。

```html
<div class="grid-3">
  <div class="pillar">
    <div class="ic">01</div>
    <div class="t">三层<br>文档体系</div>
    <div class="d">CLAUDE.md<br>+ 项目知识库<br>+ 护栏文件</div>
  </div>
  <!-- ... 更多 pillar ... -->
</div>
```

**带图标的 pillar（用于强调性页面）**：
```html
<div class="pillar" style="padding:4vh 2vw;border:1px solid currentColor;border-color:rgba(10,10,11,.2)">
  <div class="ic"><i data-lucide="compass" class="ico-lg"></i></div>
  <div class="t">判断力</div>
  <div class="d">决策和方向的权威。<br>取舍、品味、方向感。</div>
</div>
```

`.ic` 可以是序号（`01 / 02 / 03` 或 `A. / B. / C.`），也可以是 Lucide 图标。

---

## Tag & Kicker

**Kicker** 是标题上方的小提示文字（等宽、全大写、小字号）：
```html
<div class="kicker">过去 64 天 · 开发篇</div>
<div class="h1-zh">一个人，做了什么。</div>
```

**Tag** 是独立的标签胶囊（带边框）：
```html
<div style="display:flex;gap:1.6vw;flex-wrap:wrap">
  <div class="tag">早上 10 点起床</div>
  <div class="tag">周二 / 四下午健身</div>
  <div class="tag">晚上照样看剧 · 玩游戏</div>
</div>
```

---

## Figure 图片框

**这是本模板最容易踩坑的组件，务必遵守以下规则**。

### 基础结构

```html
<figure class="tile">
  <div class="frame-img" style="height:26vh">
    <img src="图片素材/xxx.png" alt="说明">
  </div>
  <figcaption class="frame-cap">
    <span class="pf">推特 · Twitter</span>
    <span class="nb">137K</span>
  </figcaption>
</figure>
```

### 关键约束（血泪经验，不要违反）

1. **必须用 `height:Nvh` 固定高度**，不要用 `aspect-ratio`。
   - 原因：用 aspect-ratio 在网格里会撑破父容器，导致图片堆叠。
   - 推荐尺寸：`height:18vh` (紧凑条形) / `22vh` (标准网格) / `26vh` (突出展示) / `28vh` (大图)。

2. **`object-position:top center`（已在 CSS 里设好）**，只允许裁掉底部。
   - 严禁裁剪左右和顶部 —— 这是图片的核心身份信息区。

3. **网格里多张图时，用内联 grid 而不是 `grid-3`**：
   ```html
   <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:1vh 1.2vw">
     <figure class="tile">...</figure>
     <figure class="tile">...</figure>
     <figure class="tile">...</figure>
   </div>
   ```

4. **图片与布局其他部分对齐**：figure 单独加 `align-self:end` 让图片贴底。

### Frame Caption 变体

```html
<!-- 标准：左 figure 名，右数字 -->
<figcaption class="frame-cap">
  <span class="pf">推特 · Twitter</span>
  <span class="nb">137K</span>
</figcaption>

<!-- 带编号 -->
<figcaption class="frame-cap">
  <span class="idx">01</span>
  <span class="pf">AI 润色</span>
  <span>Polish</span>
</figcaption>
```

### 图片占位（设计阶段占位符）

图片还没有就位时，用虚线框占位：
```html
<div class="img-slot r-4x3">  <!-- r-4x3 / r-16x9(default) / r-3x2 / r-1x1 -->
  <span class="plus">+</span>
  <span class="label">GitHub 截图位置</span>
</div>
```

---

## Icons 图标

**严禁使用 emoji**。用 Lucide via CDN（template.html 已引入）。

```html
<i data-lucide="compass" class="ico-lg"></i>     <!-- 大图标（pillar 用） -->
<i data-lucide="target" class="ico-md"></i>      <!-- 中图标（列表项用） -->
<i data-lucide="check-circle" class="ico-sm"></i>  <!-- 小图标（inline 用） -->
```

**常用 Lucide 图标名**（按含义分组）：

- 判断类：`compass`, `target`, `crosshair`, `search-check`
- 关系类：`share-2`, `users`, `network`, `link`, `handshake`
- 品牌类：`crown`, `gem`, `award`, `star`, `badge-check`
- 流程类：`workflow`, `route`, `arrow-right-left`, `repeat`
- 数据类：`grid-2x2`, `bar-chart-3`, `trending-up`, `activity`
- 审美类：`palette`, `brush`, `eye`, `sparkles`
- 对错类：`check-circle`, `x-circle`, `check`, `x`
- 方向类：`arrow-right`, `arrow-up-right`, `corner-down-right`

**图标与文字 inline 组合**：
```html
<div class="h3-zh" style="display:flex;align-items:center;gap:.8em">
  <i data-lucide="target" class="ico-md"></i>
  判断 — 什么值得写
</div>
```

---

## Ghost 巨型背景字

用作"装饰性背景字"，极低透明度，营造杂志感。

```html
<div class="ghost" style="right:-6vw;top:-8vh">BUT</div>
<div class="ghost" style="left:-8vw;bottom:-18vh;font-style:italic">Harness</div>
```

- 字号 34vw，opacity 0.06
- 常用定位：`right:-6vw;top:-8vh`（右上超出）/ `left:-8vw;bottom:-18vh`（左下超出）
- 内容：英文单词或数字（章节序号 01/02/03、关键词 BUT/NOW/HERE）

**注意**：使用 ghost 的页面里，其他内容要加 `position:relative;z-index:2` 避免被压到下面。

---

## Highlight 荧光标记

行内短语的"荧光笔"效果：

```html
<span class="hi">不是</span>
<span class="hi">一次性爆发</span>
```

在文字底部生成一条半透明高亮条。深色主题用亮条，浅色主题用暗条（CSS 已处理）。

**适合场景**：只对关键 1-3 个词使用，不要大面积用。


path: references/layouts.md
content: # 页面布局库（Layouts）

本文档收录 10 种最常用的页面布局骨架。每种都是一个完整可粘贴的 `<section class="slide ...">...</section>` 代码块，直接替换文案/图片即可使用。

---

## ⚠️ 生成前必读（Pre-flight）

### A. 类名必须来自 template.html

layouts.md 使用的所有类（`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` / `meta-row` / `stat-card` / `stat-label` / `stat-nb` / `stat-unit` / `stat-note` / `pipeline-section` / `pipeline-label` / `pipeline` / `step` / `step-nb` / `step-title` / `step-desc` / `grid-2-7-5` / `grid-2-6-6` / `grid-2-8-4` / `grid-3-3` / `grid-6` / `grid-3` / `grid-4` / `frame` / `frame-img` / `img-cap` / `callout` / `callout-src` / `kicker`）都在 `assets/template.html` 的 `<style>` 块里预定义。

**不要发明新类名**。如果必须自定义，用 `style="..."` inline 写。生成前若不确定某个类是否存在，grep template.html 确认。

### B. 图片比例规范（非常重要）

**永远用标准比例**，不要用原图 `aspect-ratio: 2592/1798` 这种奇葩比例：

| 场景 | 推荐比例 | 写法 |
|------|---------|------|
| 左文右图 主图 | 16:10 或 4:3 | `aspect-ratio:16/10; max-height:54vh` |
| 图片网格（多图对比） | 统一 | **固定 `height:26vh`，不用 aspect-ratio** |
| 左小图 + 右文字 | 1:1 或 3:2 | `aspect-ratio:1/1; max-width:40vw` |
| 全屏主视觉 | 16:9 | `aspect-ratio:16/9; max-height:64vh` |
| 图文混排小插图 | 3:2 | `aspect-ratio:3/2; max-width:30vw` |

图片必须包在 `<figure class="frame-img">` 里，里面的 `<img>` 会自动 `object-fit:cover + object-position:top center`，只裁底部，不裁顶/左/右。

### C. 图片定位准则（避免图片堆到页面最底部、被浏览器工具栏遮挡）

**错误做法**（已踩坑，不要再犯）：
- 在非 grid 容器里用 `align-self:end`：`align-self` 在 flex/grid 之外完全无效，图片会掉到文档流末尾堆底
- 用 `position:absolute + bottom:0` 把图"固定"到底：会被底部 `.foot` 和 `#nav` 圆点遮挡
- 单张图片只写 `height:N vh` 不限 `max-height`：在低分屏会撑出视口

**正确做法**：
- 图文混排**必须用 `.frame.grid-2-7-5`**（或 `.grid-2-6-6` / `.grid-2-8-4`）的 grid 结构
- grid 容器默认 `align-items:start`（已在 template 中设置），图片自然贴到 cell 顶端
- 如果需要"图片底对齐左列 callout"：**左列用 flex column + `justify-content:space-between`**（让 callout 自己贴左列底），**右列 figure 直接保持 align-items:start 即可**，不要加 `align-self:end`
- 所有 grid 父容器建议加 inline `style="padding-top:6vh"`，给标题区留呼吸空间

### D. 主题色与主题节奏

- 主题色从 `references/themes.md` 的 5 套预设里选一套,不允许自定义 hex 值
- 主题节奏(每页用 light / dark / hero light / hero dark 哪一个)在下文"主题节奏规划"一节有硬规则,生成前必读
- 两件事都要在挑布局之前决定,避免返工

---

## 0. 基础结构（所有 slide 都一样）

```html
<section class="slide [light|dark|hero light|hero dark]">
  <div class="chrome">
    <div>上下文标签 · 子标签</div>
    <div>ACT · 页号 / 总页数</div>
  </div>
  <!-- 主内容 -->
  <div class="foot">
    <div>页码说明 · Page Description</div>
    <div>— · —</div>
  </div>
</section>
```

- 非 hero 页建议加 `light` 或 `dark` 主题；hero 页加 `hero light` 或 `hero dark`（参与 WebGL 主题插值）
- `chrome` 和 `foot` 是可选但推荐保留的上下左右四角元数据
- **hero 页用于章节封面/开场/收束/转场**，非 hero 页用于正文

### ⚠️ chrome 和 kicker 不要写同一句话

这是最常见的内容重复问题。两者在语义上完全不同的维度：

| 位置 | 角色 | 内容性质 | 例子 |
|------|------|---------|------|
| `.chrome` 左上 | **杂志页眉 / 导航元数据** | 稳定的"栏目名"或"章节分类"，跨多页可以相同 | "Act II · Workflow" / "Data · Result" / "lukew.com · 2026.04" |
| `.chrome` 右上 | **页号 + 幕号** | 固定格式 | "Act II · 15 / 25" |
| `.kicker` | **这一页独一份的引导句** | 是大标题的"小前缀"，像杂志大标题上方的一行话，每页都应不同 | "BUT" / "一个人,做了什么。" / "Phase 01 · 设计阶段" |

**反例**（已踩坑）：chrome 写"设计先行 · Design First"，kicker 又写"Phase 01 · 设计阶段"——意思重复，读者一眼就觉得 AI 生成的。

**正确做法**：chrome 是**栏目标签**（稳定、跨页可复用），kicker 是**本页钩子**（短句、有戏剧性），两者互为补充，不互相翻译。

### ⚠️ 主题节奏规划（必读 · 生成前必做)

**核心机制**:每页 `<section>` 必须带 `light` / `dark` / `hero light` / `hero dark` 之一。JS 根据 class 推断主题,决定 body 加不加 `light-bg`,从而切换暗/亮两张 WebGL canvas 哪张在前。不带主题或写自定义名 = fallback 出错。

#### 按布局的主题默认值

| Layout | 默认主题 | 原因 |
|---|---|---|
| 1. 开场封面 | `hero dark` | 开场仪式感,暗底强冲击 |
| 2. 章节幕封 | `hero dark` 与 `hero light` **必须交替** | 呼吸节奏 |
| 3. 大字报(数据) | `light` | 数字需纸白底;多幕连发时可偶插 `dark` |
| 4. 左文右图 | **`light` / `dark` 交替** | 正文节奏主力 |
| 5. 图片网格 | `light` | 截图需亮底 |
| 6. Pipeline | `light` | 流程图需清晰 |
| 7. 问题页 | `hero dark` | 强视觉冲击默认 |
| 8. 大引用 | **`dark` 优先**,偶用 `light` | 金句仪式感靠暗底 |
| 9. 对比页 | `light` | 双列需清晰 |
| 10. 图文混排 | **`light` / `dark` 交替** | 节奏 |

#### 节奏硬规则(生成后 grep 自检)

- ❌ **禁止**连续 3 页以上相同主题(包括 light 堆叠和 dark 堆叠)
- ❌ **禁止**8 页以上的 deck 没有至少 1 个 `hero dark` + 1 个 `hero light`
- ❌ **禁止**整个 deck 只有 `light` 正文页没有任何 `dark` 正文页——会显得平淡、没呼吸
- ✅ **推荐**每 3-4 页插入 1 个 hero(封面/幕封/问题/大引用)

#### 8 页节奏模板(可直接套用)

| 页 | 主题 | 布局 | 备注 |
|---|---|---|---|
| 1 | `hero dark` | 封面 | 开场 |
| 2 | `light` | 大字报 | 数据抛出 |
| 3 | `dark` | 左文右图 | 对比/故事 |
| 4 | `light` | Pipeline | 流程 |
| 5 | `hero light` | 章节幕封 | 呼吸 |
| 6 | `dark` | 左文右图 or 大引用 | |
| 7 | `hero dark` | 问题页 | 悬念收束 |
| 8 | `light` | 大引用/结尾 | 收尾 |

**先画这张表对齐,再动手写 slide**。跳过规划直接粘骨架 = 全是 light。

---

## Layout 1: 开场封面（Hero Cover）

```html
<section class="slide hero dark">
  <div class="chrome">
    <div>A Talk · 2026.04.22</div>
    <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">
      一个 AI 创作者 —— 在 64 天里做了 11 万行代码、在 9 个平台上持续输出，生活节奏几乎没有被改变。
    </p>
    <div class="meta-row">
      <span>歸藏 Guizang</span><span>·</span><span>独立创作者 / CodePilot 作者</span>
    </div>
  </div>
  <div class="foot">
    <div>一场关于 AI · 组织 · 个体的分享</div>
    <div>— 2026 —</div>
  </div>
</section>
```

**要点**：
- 用 `hero dark` 让 WebGL 背景在大部分区域透出
- `h-hero` 是最大字号（10vw），这里作标题主视觉
- 用 `min-height:80vh + align-content:center` 让内容整体垂直居中
- 不需要 `.chrome` 里写页码，封面页自成一体

---

## Layout 2: 章节幕封（Act Divider）

```html
<section class="slide hero light">
  <div class="chrome">
    <div>第一幕 · 硬数据</div>
    <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">
      先看数字，再谈方法。
    </p>
  </div>
  <div class="foot">
    <div>第一幕引子</div>
    <div>— · —</div>
  </div>
</section>
```

**要点**：
- 极简，只需要 kicker + 大标题 + 一行引语
- 两个幕的封面可以交替 `hero light` / `hero dark`，制造节奏
- `h-hero` 字号可以从 10vw 调到 8.5vw 适配长短

---

## Layout 3: 数据大字报（Big Numbers Grid）

```html
<section class="slide light">
  <div class="chrome">
    <div>过去 64 天 · 开发篇</div>
    <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="grid-6" style="margin-top:6vh">
      <div class="stat-card">
        <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-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-label">GitHub Stars</div>
        <div class="stat-nb">5,166</div>
        <div class="stat-note">一个开源仓库</div>
      </div>
      <div class="stat-card">
        <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-label">AI Providers</div>
        <div class="stat-nb">19</div>
        <div class="stat-note">跨平台接入</div>
      </div>
      <div class="stat-card">
        <div class="stat-label">Commits</div>
        <div class="stat-nb">608+</div>
        <div class="stat-note">没有协作者</div>
      </div>
    </div>
  </div>
  <div class="foot">
    <div>项目 · CodePilot　|　github.com/codepilot</div>
    <div>Act I · Dev Numbers</div>
  </div>
</section>
```

**要点**：
- 3×2 或 4×2 网格最稳（见 `.grid-6`）
- 每个 `stat-card` 结构固定：label（英文小字）→ nb（大字数字）→ note（注释）
- 数字建议 2-3 位字符（太长会溢出），用 K / M 简写
- 留 5vh 以上的上方缓冲，让标题区先抢眼球

---

## Layout 4: 左文右图（Quote + Image）

```html
<section class="slide light">
  <div class="chrome">
    <div>身份反差 · The Twist</div>
    <div>03 / 25</div>
  </div>
  <div class="frame grid-2-7-5" style="padding-top:6vh">
    <!-- 左列：标题 + 正文 + 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">
          我不是程序员。
        </h2>
        <p class="lead" style="margin-top:3vh">
          大学毕业之后再也没写过一行代码。过去十年做的是 UI 设计和 AI 特效。
        </p>
      </div>
      <div class="callout">
        "这东西在三年前，<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">
      <img src="images/codepilot.png" alt="CodePilot 产品截图">
      <figcaption class="img-cap">CodePilot · 产品截图</figcaption>
    </figure>
  </div>
  <div class="foot">
    <div>Page 03 · 我不是程序员</div>
    <div>— · —</div>
  </div>
</section>
```

**要点**：
- 用 `grid-2-7-5`（左 7 份、右 5 份），`align-items:start` 已在 template 预设
- **左列**用 flex column + `justify-content:space-between`：标题贴顶，callout 自然贴底
- **右列图片** **不要加 `align-self:end`**。会让图片滑到 cell 底部，低分屏下被浏览器工具栏遮挡
- 图片必须用 **标准比例 16/10 或 4/3 + `max-height:56vh`**，不要用原图奇葩比例（`2592/1798` 这种）

---

## Layout 5: 图片网格（多图对比）

```html
<section class="slide light">
  <div class="chrome">
    <div>平台粉丝实证</div>
    <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="grid-3-3" style="margin-top:4vh">
      <figure class="frame-img" style="height:26vh">
        <img src="images/weibo.png" alt="微博 289K">
        <figcaption class="img-cap">微博 · 289K</figcaption>
      </figure>
      <figure class="frame-img" style="height:26vh">
        <img src="images/twitter.png" alt="推特 137K">
        <figcaption class="img-cap">推特 · 137K</figcaption>
      </figure>
      <figure class="frame-img" style="height:26vh">
        <img src="images/wechat.png" alt="公众号 96K">
        <figcaption class="img-cap">公众号 · 96K</figcaption>
      </figure>
      <figure class="frame-img" style="height:26vh">
        <img src="images/jike.png" alt="即刻 26K">
        <figcaption class="img-cap">即刻 · 26K</figcaption>
      </figure>
      <figure class="frame-img" style="height:26vh">
        <img src="images/xhs.png" alt="小红书 19K">
        <figcaption class="img-cap">小红书 · 19K</figcaption>
      </figure>
      <figure class="frame-img" style="height:26vh">
        <img src="images/douyin.png" alt="抖音 10K">
        <figcaption class="img-cap">抖音 · 10K</figcaption>
      </figure>
    </div>
  </div>
  <div class="foot">
    <div>截图时间 · 2026.04</div>
    <div>Page 05 · 粉丝实证</div>
  </div>
</section>
```

**要点**：
- 关键：每个 `frame-img` 必须写死 `height:NNvh`（不要用 `aspect-ratio`），否则网格会撑破
- 图片会自动 `object-fit:cover + object-position:top`，只裁底部
- 用 `.grid-3-3`（3×2）或 `.grid-3`（3×1）承载

---

## Layout 6: 两列流水线（Pipeline）

```html
<section class="slide light">
  <div class="chrome">
    <div>我的工作流 · Workflow</div>
    <div>Act II · 15 / 27</div>
  </div>
  <div class="frame">
    <div class="kicker">Pipeline · 流水线</div>
    <h2 class="h-xl">两条流水线</h2>

    <!-- 第一组：文本侧 -->
    <div class="pipeline-section">
      <div class="pipeline-label">文本侧 · Text Pipeline</div>
      <div class="pipeline">
        <div class="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-nb">02</div>
          <div class="step-title">Polish</div>
          <div class="step-desc">AI 润色去 AI 味</div>
        </div>
        <div class="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-nb">04</div>
          <div class="step-title">Illustrate</div>
          <div class="step-desc">AI 生成信息图</div>
        </div>
        <div class="step">
          <div class="step-nb">05</div>
          <div class="step-title">Distribute</div>
          <div class="step-desc">一键分发 9 平台</div>
        </div>
      </div>
    </div>

    <!-- 第二组：视频侧 -->
    <div class="pipeline-section">
      <div class="pipeline-label">视觉 · 视频侧 · Video Pipeline</div>
      <div class="pipeline">
        <div class="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-nb">07</div>
          <div class="step-title">Wrap</div>
          <div class="step-desc">AI 帮我包装</div>
        </div>
        <div class="step">
          <div class="step-nb">08</div>
          <div class="step-title">Cover</div>
          <div class="step-desc">AI 生成封面</div>
        </div>
      </div>
    </div>
  </div>
  <div class="foot">
    <div>Page 15 · 我的内容工厂</div>
    <div>Workflow</div>
  </div>
</section>
```

**要点**：
- 用 `.pipeline-section` 分组 + `.pipeline-label` 作组标题
- 两组之间用 3.6vh 的间距 + 顶部细分隔线（已在 CSS 中预设）
- 每个 step 是固定的 nb → title → desc 结构
- 步骤数不限但单行最好 ≤5 个，否则换到第二 pipeline

---

## Layout 7: 悬念收束 / 问题页（Hero Question）

```html
<section class="slide hero dark">
  <div class="chrome">
    <div>留给你的问题</div>
    <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>
    <h1 class="h-hero" style="font-size:7vw; line-height:1.15">
      你的公司里，<br>
      哪些岗位本来就<br>
      不该由人来做？
    </h1>
    <p class="lead" style="max-width:50vw">
      这个问题，不是技术问题，是架构问题。
    </p>
  </div>
  <div class="foot">
    <div>Page 24 · The Question</div>
    <div>— · —</div>
  </div>
</section>
```

**要点**：
- Hero 页留白越多越好，只放一个问题
- `h-hero` 字号视长度调整（7vw 适合 3 行，10vw 适合 1 行）
- 用 `<br>` 手工断行，确保断点在语义处
- 尾巴可以再给一行 `lead` 作为点破

---

## Layout 8: 大引用页（Big Quote · 衬线金句）

```html
<section class="slide light">
  <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>
    <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>所有人都在构建。"
    </blockquote>
    <p class="lead" style="max-width:55vw; opacity:.65">
      Without the handoff, everyone builds.<br>
      And that makes all the difference.
    </p>
    <div class="meta-row">
      <span>— Luke Wroblewski</span><span>·</span><span>2026.04.16</span>
    </div>
  </div>
  <div class="foot">
    <div>Page 18 · 金句</div>
    <div>— · —</div>
  </div>
</section>
```

**要点**：
- 整页留白,只放一个大引用 + 出处
- `<blockquote>` 用 inline style 单独放大（5-6vw）,不要用 `h-hero`（那是页面主标题的命名）
- 下面跟随英文原文（lead · opacity:.65）制造层级
- 配 `meta-row` 写出处 · 日期

---

## Layout 9: 并列对比（A vs B · 旧 vs 新）

```html
<section class="slide light">
  <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="grid-2-6-6" style="gap:5vw 4vh">
      <!-- 左列：旧 -->
      <div 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">
          <li>设计师在 Figma 做稿</li>
          <li>开发者盯着文件翻译像素</li>
          <li>反复 PR 沟通对齐</li>
          <li>非技术人员无法触碰代码</li>
        </ul>
      </div>
      <!-- 右列:新 -->
      <div 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">
          <li>三个角色同时在 Intent 工作</li>
          <li>agents.md 作为共享上下文</li>
          <li>代理处理对齐 / 冲突 / 动画</li>
          <li>任何人都能安全贡献代码</li>
        </ul>
      </div>
    </div>
  </div>
  <div class="foot">
    <div>Page 12 · 范式转变</div>
    <div>Before / After</div>
  </div>
</section>
```

**要点**：
- 用 `.grid-2-6-6`（1:1）左右分半
- 左列 `opacity:.55` 做"旧"的视觉弱化,右列满亮度做"新"的突出
- 两列都用 `border-left:3px solid` + `padding-left` 做引用块感
- 每列结构统一:`kicker` → `h-md` → `<ul>` 要点,节奏一致

---

## Layout 10: 图文混排（Lead Image + Side Text）

```html
<section class="slide light">
  <div class="chrome">
    <div>Design First · 设计先行</div>
    <div>08 / 16</div>
  </div>
  <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>

      <p class="lead" style="margin-bottom:3vh">
        在 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>

      <div class="callout" style="margin-top:3vh">
        "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">
      <img src="images/figma.png" alt="Figma design system">
      <figcaption class="img-cap">Figma · Design System</figcaption>
    </figure>
  </div>
  <div class="foot">
    <div>Page 08 · Design First</div>
    <div>约 2 周</div>
  </div>
</section>
```

**要点**：
- `.grid-2-8-4`(8:4) 让正文占主导,图片作辅助
- 左列包含多种信息层级:kicker → 大标题 → lead → 正文段落 → callout(引用)
- 右列图片用 **竖版 3:4** 或方形 1:1,避免和左列文本竞争注意力
- 这种布局适合**页面信息量偏大**的场景(不像 Layout 4 只有一句金句)

---

## 附录：常用网格模板

| 类名 | 配比 | 用途 |
|---|---|---|
| `.grid-2-6-6` | 6:6（1:1） | 对半分 |
| `.grid-2-7-5` | 7:5 | 文字为主 + 辅助图 |
| `.grid-2-8-4` | 8:4（2:1） | 大段文字 + 小图/数据 |
| `.grid-3` | 1:1:1 | 3 项并列（案例/截图） |
| `.grid-3-3` | 3×2 | 6 图矩阵 |
| `.grid-6` | 3×2 | 6 个数据卡片 |

所有网格都预留 `gap: 3vw 4vh`（水平 3vw、竖直 4vh），可以单独覆写。

---

## 页面节奏建议

一场 25-30 页的分享，推荐以下节奏：

1. **Hero Cover**（第 1 页）
2. **Act Divider**（第一幕开场，hero light 或 hero dark）
3. **Big Numbers**（抛硬数据制造冲击）
4. **Quote + Image**（讲身份反差/挂钩）
5. **Image Grid**（证据支撑）
6. **Hero Question**（幕收束，留悬念）
7. ... 第二幕、第三幕同样节奏 ...
8. **Hero Close**（最后一页，问题或致谢）

hero 页与 non-hero 页应该 **2-3 : 1 比例交错**，不要连续超过 3 页 non-hero，也不要连续超过 2 页 hero。


path: references/themes.md
content: # 主题色预设（Themes）

5 套精心调配的主题色板,保证"电子杂志 × 电子墨水"的美学不垮。**不允许用户自定义颜色——色彩搭配错了画面瞬间变丑**,只从以下预设中挑选。

---

## 使用方法

1. 问用户选哪套(或基于内容推荐一套)
2. 打开 `assets/template.html` 的 `<style>` 块
3. 找到开头的 `:root{` 块
4. **整体替换**标有"主题色"注释的那几行 `--ink` / `--ink-rgb` / `--paper` / `--paper-rgb` / `--paper-tint` / `--ink-tint`
5. 其他 CSS 都走 `var(--...)`,无需任何其他改动

---

## 🖋 墨水经典 (Monocle 默认)

**适合**:通用分享、商业发布、科技产品、任何场景都安全的默认选择。
**调性**:纯墨黑 + 暖米白,杂志感最强,Monocle / Apricot / A Book Apart 风。

```css
--ink:#0a0a0b;
--ink-rgb:10,10,11;
--paper:#f1efea;
--paper-rgb:241,239,234;
--paper-tint:#e8e5de;
--ink-tint:#18181a;
```

---

## 🌊 靛蓝瓷 (Indigo Porcelain)

**适合**:科技/研究/数据分享、工程师文化、深度内容、技术发布会。
**调性**:深靛蓝 + 瓷白,冷静、理性、有深度,像学术期刊或蓝印花瓷器。

```css
--ink:#0a1f3d;
--ink-rgb:10,31,61;
--paper:#f1f3f5;
--paper-rgb:241,243,245;
--paper-tint:#e4e8ec;
--ink-tint:#152a4a;
```

---

## 🌿 森林墨 (Forest Ink)

**适合**:自然/可持续/文化/非虚构内容、户外品牌、环保主题。
**调性**:深森林绿 + 象牙,沉稳、有呼吸感,像旧版《国家地理》。

```css
--ink:#1a2e1f;
--ink-rgb:26,46,31;
--paper:#f5f1e8;
--paper-rgb:245,241,232;
--paper-tint:#ece7da;
--ink-tint:#253d2c;
```

---

## 🍂 牛皮纸 (Kraft Paper)

**适合**:怀旧/人文/阅读/历史/文学分享、独立杂志、手作品牌。
**调性**:深棕 + 暖米,像牛皮信封或老笔记本,温暖、有年代感。

```css
--ink:#2a1e13;
--ink-rgb:42,30,19;
--paper:#eedfc7;
--paper-rgb:238,223,199;
--paper-tint:#e0d0b6;
--ink-tint:#3a2a1d;
```

---

## 🌙 沙丘 (Dune)

**适合**:艺术/设计/创意/时尚分享、画廊手册、审美优先的私享会。
**调性**:炭灰 + 沙色,克制、高级、中性,像沙漠黄昏或建筑设计图册。

```css
--ink:#1f1a14;
--ink-rgb:31,26,20;
--paper:#f0e6d2;
--paper-rgb:240,230,210;
--paper-tint:#e3d7bf;
--ink-tint:#2d2620;
```

---

## 推荐选择参考

| 如果是... | 推荐主题 |
|---|---|
| 不知道选啥 / 第一次用 | 🖋 墨水经典 |
| AI / 技术 / 产品发布 | 🌊 靛蓝瓷 |
| 内容 / 行业观察 / 文化 | 🌿 森林墨 |
| 书评 / 生活方式 / 人文 | 🍂 牛皮纸 |
| 设计 / 艺术 / 品牌 | 🌙 沙丘 |

---

## 切换原则

- **一份 deck 只用一套主题**,不要中途换色
- WebGL shader 的默认主色(钛金色散 / 银色流动)适配所有 5 套(经测试可接受)
- `currentColor` 驱动的 border / icon 会跟随 section 的 text color 自动适配,无需额外调整
- 选定主题后,`<title>` 文字和 `chrome` 文案可以强化该主题的语义(例如牛皮纸配"Vol.03 · 秋"这种)

## ❌ 不要做的事

- ❌ **不允许混搭**(例如 ink 取墨水经典的,paper 取沙丘的)——会彻底违和
- ❌ **不允许用户随便给一个 hex 值**——需委婉拒绝并展示 5 套预设让选
- ❌ **不要直接修改 template.html 其他地方的颜色**——所有散落 rgba 都走 var,改 :root 一处即可

选定主题后在 skill 对话中告诉用户:"用 🖋 墨水经典 / 🌊 靛蓝瓷 ..."并在 deck 项目记录里备注,方便后续迭代时保持一致。


path: scripts/generate_html.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Magazine PPT Generator - Core Script
Author: WuWenBin-BeiJing-ST
Version: 1.0.0

生成单文件 HTML 横向翻页 PPT
"""

import argparse
import json
import os
import sys
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional, Tuple

# 主题色配置
THEMES = {
    "ink-classic": {
        "primary": "#1a1a2e",
        "secondary": "#4a4e69",
        "accent": "#9a8c98",
        "background": "#fafafa",
        "text": "#2d2d2d",
        "text_light": "#666666",
    },
    "indigo-porcelain": {
        "primary": "#2c3e50",
        "secondary": "#3498db",
        "accent": "#1abc9c",
        "background": "#ffffff",
        "text": "#2c3e50",
        "text_light": "#7f8c8d",
    },
    "forest-ink": {
        "primary": "#2d3436",
        "secondary": "#00b894",
        "accent": "#55a3ff",
        "background": "#f8f9fa",
        "text": "#2d3436",
        "text_light": "#636e72",
    },
    "kraft-paper": {
        "primary": "#d4a373",
        "secondary": "#e9c46a",
        "accent": "#f4a261",
        "background": "#fefae0",
        "text": "#264653",
        "text_light": "#6c757d",
    },
    "dune": {
        "primary": "#264653",
        "secondary": "#e76f51",
        "accent": "#2a9d8f",
        "background": "#ffffff",
        "text": "#264653",
        "text_light": "#8d99ae",
    },
}

# 字体配置
FONTS = {
    "title": {
        "family": "Georgia, 'Songti SC', 'Noto Serif SC', serif",
        "sizes": {"hero": 96, "page": 64, "card": 24},
    },
    "body": {
        "family": "Inter, 'PingFang SC', 'Noto Sans SC', sans-serif",
        "sizes": {"large": 24, "normal": 18, "small": 16},
    },
    "mono": {
        "family": "'JetBrains Mono', 'SF Mono', Consolas, monospace",
        "sizes": {"normal": 14, "small": 12},
    },
}


def parse_outline(outline_path: str) -> Dict:
    """解析大纲 JSON 文件"""
    with open(outline_path, "r", encoding="utf-8") as f:
        return json.load(f)


def generate_css(theme: Dict) -> str:
    """生成 CSS 样式"""
    return f"""
/* Reset & Base */
*, *::before, *::after {{
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}}

:root {{
  --primary: {theme['primary']};
  --secondary: {theme['secondary']};
  --accent: {theme['accent']};
  --background: {theme['background']};
  --text: {theme['text']};
  --text-light: {theme['text_light']};
}}

html {{
  scroll-behavior: smooth;
  scroll-snap-type: x mandatory;
}}

body {{
  font-family: {FONTS['body']['family']};
  background: var(--background);
  color: var(--text);
  overflow-x: auto;
  overflow-y: hidden;
  display: flex;
  scroll-snap-type: x mandatory;
}}

/* Slide Container */
.slide {{
  min-width: 100vw;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 80px;
  scroll-snap-align: start;
  position: relative;
}}

/* Typography */
.title-hero {{
  font-family: {FONTS['title']['family']};
  font-size: {FONTS['title']['sizes']['hero']}px;
  font-weight: 700;
  line-height: 1.2;
  color: var(--primary);
}}

.title-page {{
  font-family: {FONTS['title']['family']};
  font-size: {FONTS['title']['sizes']['page']}px;
  font-weight: 700;
  line-height: 1.2;
  color: var(--primary);
}}

.body-large {{
  font-size: {FONTS['body']['sizes']['large']}px;
  line-height: 1.6;
  color: var(--text);
}}

.body-normal {{
  font-size: {FONTS['body']['sizes']['normal']}px;
  line-height: 1.6;
  color: var(--text);
}}

.meta-text {{
  font-family: {FONTS['mono']['family']};
  font-size: {FONTS['mono']['sizes']['normal']}px;
  color: var(--text-light);
}}

/* Navigation Dots */
.nav-dots {{
  position: fixed;
  bottom: 40px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 12px;
  z-index: 1000;
}}

.nav-dot {{
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: rgba(0,0,0,0.2);
  cursor: pointer;
  transition: all 0.3s ease;
}}

.nav-dot.active {{
  background: var(--primary);
  transform: scale(1.3);
}}

.nav-dot:hover {{
  background: rgba(0,0,0,0.4);
}}

/* Page Number */
.page-number {{
  position: fixed;
  bottom: 40px;
  right: 60px;
  font-family: {FONTS['mono']['family']};
  font-size: {FONTS['mono']['sizes']['normal']}px;
  color: var(--text-light);
  z-index: 1000;
}}

/* Animations */
@keyframes fadeIn {{
  from {{ opacity: 0; transform: translateY(20px); }}
  to {{ opacity: 1; transform: translateY(0); }}
}}

.slide {{
  animation: fadeIn 0.6s ease-out;
}}
"""


def generate_html_slide(slide_data: Dict, slide_index: int) -> str:
    """生成单个幻灯片 HTML"""
    layout = slide_data.get("layout", "L01")
    title = slide_data.get("title", "")
    subtitle = slide_data.get("subtitle", "")
    content = slide_data.get("content", [])
    image = slide_data.get("image", "")

    # 根据布局生成不同结构
    if layout == "L01":  # Hero 封面
        return f"""
<div class="slide" data-slide="{slide_index}">
  <div style="text-align: center; max-width: 800px;">
    <h1 class="title-hero">{title}</h1>
    {f'<p class="body-large" style="margin-top: 24px;">{subtitle}</p>' if subtitle else ''}
  </div>
</div>
"""
    elif layout == "L02":  # 左图右文
        return f"""
<div class="slide" data-slide="{slide_index}">
  <div style="display: flex; width: 100%; gap: 80px; align-items: center;">
    <div style="flex: 1;">
      {f'<img src="{image}" alt="{title}" style="width: 100%; border-radius: 16px;">' if image else ''}
    </div>
    <div style="flex: 1;">
      <h2 class="title-page">{title}</h2>
      {f'<p class="body-normal" style="margin-top: 24px;">{subtitle}</p>' if subtitle else ''}
    </div>
  </div>
</div>
"""
    elif layout == "L05":  # 三栏并列
        items = content if isinstance(content, list) else []
        items_html = "".join(
            [
                f"""
      <div style="flex: 1; text-align: center; padding: 40px;">
        <h3 style="font-size: 24px; margin-bottom: 16px;">{item.get('title', '')}</h3>
        <p class="body-normal" style="color: var(--text-light);">{item.get('description', '')}</p>
      </div>
    """
                for item in items[:3]
            ]
        )
        return f"""
<div class="slide" data-slide="{slide_index}">
  <div style="width: 100%;">
    <h2 class="title-page" style="text-align: center; margin-bottom: 60px;">{title}</h2>
    <div style="display: flex; gap: 40px;">
      {items_html}
    </div>
  </div>
</div>
"""
    else:  # 默认布局
        return f"""
<div class="slide" data-slide="{slide_index}">
  <div style="max-width: 800px;">
    <h2 class="title-page">{title}</h2>
    {f'<p class="body-normal" style="margin-top: 24px;">{subtitle}</p>' if subtitle else ''}
  </div>
</div>
"""


def generate_full_html(
    outline: Dict, theme_name: str, output_path: str
) -> str:
    """生成完整 HTML 文件"""
    theme = THEMES.get(theme_name, THEMES["ink-classic"])
    slides = outline.get("slides", [])

    # 生成 CSS
    css = generate_css(theme)

    # 生成所有幻灯片
    slides_html = "\n".join(
        [generate_html_slide(slide, i) for i, slide in enumerate(slides, 1)]
    )

    # 生成导航点
    nav_dots = "".join(
        [
            f'<span class="nav-dot{" active" if i == 0 else ""}" data-slide="{i}"></span>'
            for i in range(1, len(slides) + 1)
        ]
    )

    # 完整 HTML
    html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{outline.get('title', '演示文稿')}</title>
  <style>
{css}
  </style>
</head>
<body>
{slides_html}

  <!-- Navigation -->
  <div class="nav-dots">
    {nav_dots}
  </div>

  <!-- Page Number -->
  <div class="page-number">
    <span class="current">01</span>
    <span class="separator">/</span>
    <span class="total">{len(slides):02d}</span>
  </div>

  <script>
    // Slide navigation
    const slides = document.querySelectorAll('.slide');
    const dots = document.querySelectorAll('.nav-dot');
    const currentNum = document.querySelector('.page-number .current');
    let currentIndex = 0;

    // Keyboard navigation
    document.addEventListener('keydown', (e) => {{
      if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {{
        goToSlide(currentIndex + 1);
      }} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {{
        goToSlide(currentIndex - 1);
      }}
    }});

    // Dot navigation
    dots.forEach((dot, index) => {{
      dot.addEventListener('click', () => goToSlide(index));
    }});

    function goToSlide(index) {{
      if (index < 0 || index >= slides.length) return;
      currentIndex = index;
      slides[index].scrollIntoView({{ behavior: 'smooth' }});

      // Update dots
      dots.forEach((dot, i) => {{
        dot.classList.toggle('active', i === index);
      }});

      // Update page number
      currentNum.textContent = String(index + 1).padStart(2, '0');
    }}
  </script>
</body>
</html>
"""

    # 写入文件
    output_file = Path(output_path)
    output_file.parent.mkdir(parents=True, exist_ok=True)
    output_file.write_text(html, encoding="utf-8")

    return str(output_file)


def main():
    parser = argparse.ArgumentParser(description="生成 HTML 横向翻页 PPT")
    parser.add_argument("--content", required=True, help="PPT 主题/内容描述")
    parser.add_argument("--outline", help="大纲 JSON 文件路径")
    parser.add_argument(
        "--theme", default="ink-classic", choices=THEMES.keys(), help="主题色"
    )
    parser.add_argument("--output", default="./output/presentation.html", help="输出路径")

    args = parser.parse_args()

    # 如果提供了大纲文件，使用大纲生成
    if args.outline:
        outline = parse_outline(args.outline)
    else:
        # 否则使用内容描述生成简单大纲
        outline = {
            "title": args.content,
            "slides": [
                {"layout": "L01", "title": args.content, "subtitle": "演示文稿"},
                {"layout": "L05", "title": "核心要点", "content": []},
                {"layout": "L01", "title": "感谢观看", "subtitle": ""},
            ],
        }

    # 生成 HTML
    output_file = generate_full_html(outline, args.theme, args.output)
    print(f"✅ PPT 已生成: {output_file}")

    # 自动打开（macOS）
    if sys.platform == "darwin":
        os.system(f'open "{output_file}"')


if __name__ == "__main__":
    main()


path: scripts/generate_ppt.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Magazine PPT Generator - Unified Entry
Author: WuWenBin-BeiJing-ST
Version: 1.0.0

统一入口：支持 HTML 和 PPTX 双格式输出
"""

import argparse
import json
import os
import sys
from pathlib import Path
from datetime import datetime

# 添加脚本目录到路径
script_dir = Path(__file__).parent
sys.path.insert(0, str(script_dir))

from generate_html import generate_full_html, THEMES as HTML_THEMES
from generate_pptx import generate_pptx, THEMES as PPTX_THEMES


def generate_ppt(
    content: str,
    output_format: str = "html",
    theme: str = "ink-classic",
    output_dir: str = "./output",
    outline_data: dict = None,
) -> dict:
    """
    生成 PPT 的统一接口

    Args:
        content: PPT 主题/内容描述
        output_format: 输出格式 (html/pptx/both)
        theme: 主题色名称
        output_dir: 输出目录
        outline_data: 大纲数据（可选）

    Returns:
        包含生成文件路径的字典
    """
    # 创建输出目录
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)

    # 时间戳
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

    # 如果没有提供大纲，生成默认大纲
    if not outline_data:
        outline_data = {
            "title": content,
            "slides": [
                {"layout": "L01", "title": content, "subtitle": "演示文稿"},
                {
                    "layout": "L05",
                    "title": "核心要点",
                    "content": [
                        {"title": "要点一", "description": "第一项核心内容"},
                        {"title": "要点二", "description": "第二项核心内容"},
                        {"title": "要点三", "description": "第三项核心内容"},
                    ],
                },
                {"layout": "L01", "title": "感谢观看"},
            ],
        }

    # 保存大纲
    outline_file = output_path / f"outline_{timestamp}.json"
    with open(outline_file, "w", encoding="utf-8") as f:
        json.dump(outline_data, f, ensure_ascii=False, indent=2)

    results = {"outline": str(outline_file), "html": None, "pptx": None}

    # 生成 HTML
    if output_format in ["html", "both"]:
        html_file = output_path / f"presentation_{timestamp}.html"
        html_path = generate_full_html(outline_data, theme, str(html_file))
        results["html"] = html_path
        print(f"✅ HTML 已生成: {html_path}")

    # 生成 PPTX
    if output_format in ["pptx", "both"]:
        pptx_file = output_path / f"presentation_{timestamp}.pptx"
        pptx_path = generate_pptx(outline_data, theme, str(pptx_file))
        results["pptx"] = pptx_path
        print(f"✅ PPTX 已生成: {pptx_path}")

    return results


def main():
    parser = argparse.ArgumentParser(
        description="Magazine PPT 生成器 - 支持 HTML 和 PPTX 双格式"
    )
    parser.add_argument("--content", required=True, help="PPT 主题/内容描述")
    parser.add_argument("--outline", help="大纲 JSON 文件路径")
    parser.add_argument(
        "--output",
        "-o",
        default="html",
        choices=["html", "pptx", "both"],
        help="输出格式 (default: html)",
    )
    parser.add_argument(
        "--theme",
        "-t",
        default="ink-classic",
        choices=list(HTML_THEMES.keys()),
        help="主题色 (default: ink-classic)",
    )
    parser.add_argument(
        "--output-dir", "-d", default="./output", help="输出目录 (default: ./output)"
    )

    args = parser.parse_args()

    # 加载大纲（如果提供）
    outline_data = None
    if args.outline:
        with open(args.outline, "r", encoding="utf-8") as f:
            outline_data = json.load(f)

    # 生成 PPT
    results = generate_ppt(
        content=args.content,
        output_format=args.output,
        theme=args.theme,
        output_dir=args.output_dir,
        outline_data=outline_data,
    )

    # 自动打开（macOS）
    if sys.platform == "darwin":
        if results["html"]:
            os.system(f'open "{results["html"]}"')
        elif results["pptx"]:
            os.system(f'open "{results["pptx"]}"')


if __name__ == "__main__":
    main()


path: scripts/generate_pptx.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Magazine PPT Generator - PPTX Export
Author: WuWenBin-BeiJing-ST
Version: 1.0.0

生成可编辑的 PPTX 格式演示文稿
"""

import argparse
import json
import os
import sys
from pathlib import Path
from typing import Dict, List, Optional

try:
    from pptx import Presentation
    from pptx.util import Inches, Pt
    from pptx.dml.color import RgbColor
    from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
except ImportError:
    print("❌ 缺少依赖: python-pptx")
    print("请运行: pip3 install python-pptx")
    sys.exit(1)

# 主题色配置
THEMES = {
    "ink-classic": {
        "primary": (26, 26, 46),  # RGB
        "secondary": (74, 78, 105),
        "background": (250, 250, 250),
        "text": (45, 45, 45),
    },
    "indigo-porcelain": {
        "primary": (44, 62, 80),
        "secondary": (52, 152, 219),
        "background": (255, 255, 255),
        "text": (44, 62, 80),
    },
    "forest-ink": {
        "primary": (45, 52, 54),
        "secondary": (0, 184, 148),
        "background": (248, 249, 250),
        "text": (45, 52, 54),
    },
    "kraft-paper": {
        "primary": (212, 163, 115),
        "secondary": (233, 196, 106),
        "background": (254, 250, 224),
        "text": (38, 70, 83),
    },
    "dune": {
        "primary": (38, 70, 83),
        "secondary": (231, 111, 81),
        "background": (255, 255, 255),
        "text": (38, 70, 83),
    },
}


def rgb_to_rgbcolor(rgb_tuple: tuple) -> RgbColor:
    """将 RGB 元组转换为 RgbColor"""
    return RgbColor(rgb_tuple[0], rgb_tuple[1], rgb_tuple[2])


def create_presentation(theme_name: str = "ink-classic") -> Presentation:
    """创建演示文稿对象"""
    prs = Presentation()
    prs.slide_width = Inches(13.333)  # 16:9 比例
    prs.slide_height = Inches(7.5)
    return prs


def add_title_slide(prs: Presentation, title: str, subtitle: str = "", theme: dict = None):
    """添加封面幻灯片"""
    slide_layout = prs.slide_layouts[6]  # 空白布局
    slide = prs.slides.add_slide(slide_layout)

    # 背景
    background = slide.background
    fill = background.fill
    fill.solid()
    fill.fore_color.rgb = rgb_to_rgbcolor(theme.get("background", (255, 255, 255)))

    # 标题
    left = Inches(0.5)
    top = Inches(2.5)
    width = Inches(12.333)
    height = Inches(1.5)

    title_box = slide.shapes.add_textbox(left, top, width, height)
    title_frame = title_box.text_frame
    title_frame.text = title

    # 格式化标题
    title_para = title_frame.paragraphs[0]
    title_para.alignment = PP_ALIGN.CENTER
    title_para.font.size = Pt(60)
    title_para.font.bold = True
    title_para.font.color.rgb = rgb_to_rgbcolor(theme.get("primary", (0, 0, 0)))

    # 副标题
    if subtitle:
        sub_top = Inches(4.2)
        sub_height = Inches(1)

        sub_box = slide.shapes.add_textbox(left, sub_top, width, sub_height)
        sub_frame = sub_box.text_frame
        sub_frame.text = subtitle

        sub_para = sub_frame.paragraphs[0]
        sub_para.alignment = PP_ALIGN.CENTER
        sub_para.font.size = Pt(24)
        sub_para.font.color.rgb = rgb_to_rgbcolor(theme.get("text", (100, 100, 100)))


def add_content_slide(
    prs: Presentation, title: str, content: List[str], theme: dict = None
):
    """添加内容幻灯片"""
    slide_layout = prs.slide_layouts[6]  # 空白布局
    slide = prs.slides.add_slide(slide_layout)

    # 背景
    background = slide.background
    fill = background.fill
    fill.solid()
    fill.fore_color.rgb = rgb_to_rgbcolor(theme.get("background", (255, 255, 255)))

    # 标题
    left = Inches(0.5)
    top = Inches(0.5)
    width = Inches(12.333)
    height = Inches(1)

    title_box = slide.shapes.add_textbox(left, top, width, height)
    title_frame = title_box.text_frame
    title_frame.text = title

    title_para = title_frame.paragraphs[0]
    title_para.font.size = Pt(44)
    title_para.font.bold = True
    title_para.font.color.rgb = rgb_to_rgbcolor(theme.get("primary", (0, 0, 0)))

    # 内容
    content_top = Inches(2)
    content_height = Inches(5)

    content_box = slide.shapes.add_textbox(left, content_top, width, content_height)
    content_frame = content_box.text_frame
    content_frame.word_wrap = True

    for i, item in enumerate(content):
        if i == 0:
            p = content_frame.paragraphs[0]
        else:
            p = content_frame.add_paragraph()

        p.text = f"• {item}"
        p.font.size = Pt(22)
        p.font.color.rgb = rgb_to_rgbcolor(theme.get("text", (50, 50, 50)))
        p.space_after = Pt(12)


def add_three_column_slide(
    prs: Presentation,
    title: str,
    columns: List[Dict],
    theme: dict = None,
):
    """添加三栏幻灯片"""
    slide_layout = prs.slide_layouts[6]
    slide = prs.slides.add_slide(slide_layout)

    # 背景
    background = slide.background
    fill = background.fill
    fill.solid()
    fill.fore_color.rgb = rgb_to_rgbcolor(theme.get("background", (255, 255, 255)))

    # 标题
    left = Inches(0.5)
    top = Inches(0.5)
    width = Inches(12.333)
    height = Inches(1)

    title_box = slide.shapes.add_textbox(left, top, width, height)
    title_frame = title_box.text_frame
    title_frame.text = title
    title_para = title_frame.paragraphs[0]
    title_para.alignment = PP_ALIGN.CENTER
    title_para.font.size = Pt(44)
    title_para.font.bold = True
    title_para.font.color.rgb = rgb_to_rgbcolor(theme.get("primary", (0, 0, 0)))

    # 三栏内容
    col_width = Inches(3.5)
    col_height = Inches(4)
    col_top = Inches(2.5)
    gap = Inches(0.9)

    for i, col in enumerate(columns[:3]):
        col_left = Inches(1) + (col_width + gap) * i

        # 栏标题
        col_title_box = slide.shapes.add_textbox(col_left, col_top, col_width, Inches(0.8))
        col_title_frame = col_title_box.text_frame
        col_title_frame.text = col.get("title", "")
        col_title_para = col_title_frame.paragraphs[0]
        col_title_para.alignment = PP_ALIGN.CENTER
        col_title_para.font.size = Pt(24)
        col_title_para.font.bold = True
        col_title_para.font.color.rgb = rgb_to_rgbcolor(theme.get("secondary", (100, 100, 100)))

        # 栏描述
        col_desc_box = slide.shapes.add_textbox(
            col_left, col_top + Inches(1), col_width, col_height
        )
        col_desc_frame = col_desc_box.text_frame
        col_desc_frame.word_wrap = True
        col_desc_frame.text = col.get("description", "")
        col_desc_para = col_desc_frame.paragraphs[0]
        col_desc_para.alignment = PP_ALIGN.CENTER
        col_desc_para.font.size = Pt(16)
        col_desc_para.font.color.rgb = rgb_to_rgbcolor(theme.get("text", (80, 80, 80)))


def add_quote_slide(prs: Presentation, quote: str, author: str, theme: dict = None):
    """添加引用幻灯片"""
    slide_layout = prs.slide_layouts[6]
    slide = prs.slides.add_slide(slide_layout)

    # 背景
    background = slide.background
    fill = background.fill
    fill.solid()
    fill.fore_color.rgb = rgb_to_rgbcolor(theme.get("background", (255, 255, 255)))

    # 引号装饰
    quote_mark_left = Inches(5.5)
    quote_mark_top = Inches(1.5)
    quote_mark_box = slide.shapes.add_textbox(
        quote_mark_left, quote_mark_top, Inches(2), Inches(1.5)
    )
    quote_mark_frame = quote_mark_box.text_frame
    quote_mark_frame.text = "❝"
    quote_mark_para = quote_mark_frame.paragraphs[0]
    quote_mark_para.alignment = PP_ALIGN.CENTER
    quote_mark_para.font.size = Pt(120)
    quote_mark_para.font.color.rgb = RgbColor(200, 200, 200)

    # 引用文字
    quote_left = Inches(1)
    quote_top = Inches(2.8)
    quote_width = Inches(11.333)
    quote_height = Inches(2)

    quote_box = slide.shapes.add_textbox(quote_left, quote_top, quote_width, quote_height)
    quote_frame = quote_box.text_frame
    quote_frame.word_wrap = True
    quote_frame.text = f'"{quote}"'

    quote_para = quote_frame.paragraphs[0]
    quote_para.alignment = PP_ALIGN.CENTER
    quote_para.font.size = Pt(32)
    quote_para.font.italic = True
    quote_para.font.color.rgb = rgb_to_rgbcolor(theme.get("text", (50, 50, 50)))

    # 作者
    if author:
        author_top = Inches(5.5)
        author_box = slide.shapes.add_textbox(quote_left, author_top, quote_width, Inches(0.6))
        author_frame = author_box.text_frame
        author_frame.text = f"—— {author}"

        author_para = author_frame.paragraphs[0]
        author_para.alignment = PP_ALIGN.CENTER
        author_para.font.size = Pt(18)
        author_para.font.color.rgb = rgb_to_rgbcolor(theme.get("text", (120, 120, 120)))


def add_end_slide(prs: Presentation, theme: dict = None):
    """添加结尾幻灯片"""
    slide_layout = prs.slide_layouts[6]
    slide = prs.slides.add_slide(slide_layout)

    # 背景
    background = slide.background
    fill = background.fill
    fill.solid()
    fill.fore_color.rgb = rgb_to_rgbcolor(theme.get("background", (255, 255, 255)))

    # 感谢文字
    left = Inches(0.5)
    top = Inches(3)
    width = Inches(12.333)
    height = Inches(1.5)

    title_box = slide.shapes.add_textbox(left, top, width, height)
    title_frame = title_box.text_frame
    title_frame.text = "感谢观看"

    title_para = title_frame.paragraphs[0]
    title_para.alignment = PP_ALIGN.CENTER
    title_para.font.size = Pt(60)
    title_para.font.bold = True
    title_para.font.color.rgb = rgb_to_rgbcolor(theme.get("primary", (0, 0, 0)))


def parse_outline(outline_path: str) -> Dict:
    """解析大纲 JSON 文件"""
    with open(outline_path, "r", encoding="utf-8") as f:
        return json.load(f)


def generate_pptx(outline: Dict, theme_name: str, output_path: str) -> str:
    """生成 PPTX 文件"""
    theme = THEMES.get(theme_name, THEMES["ink-classic"])
    prs = create_presentation(theme_name)

    slides = outline.get("slides", [])

    for slide_data in slides:
        layout = slide_data.get("layout", "L01")
        title = slide_data.get("title", "")
        subtitle = slide_data.get("subtitle", "")
        content = slide_data.get("content", [])
        quote = slide_data.get("quote", "")
        author = slide_data.get("author", "")

        if layout == "L01":  # 封面
            add_title_slide(prs, title, subtitle, theme)
        elif layout == "L05":  # 三栏
            columns = content if isinstance(content, list) else []
            add_three_column_slide(prs, title, columns, theme)
        elif layout == "L08":  # 引用
            add_quote_slide(prs, quote or title, author, theme)
        elif layout == "L10":  # 结尾
            add_end_slide(prs, theme)
        else:  # 默认内容页
            items = content if isinstance(content, list) else []
            add_content_slide(prs, title, items, theme)

    # 写入文件
    output_file = Path(output_path)
    output_file.parent.mkdir(parents=True, exist_ok=True)
    prs.save(str(output_file))

    return str(output_file)


def main():
    parser = argparse.ArgumentParser(description="生成 PPTX 格式演示文稿")
    parser.add_argument("--content", required=True, help="PPT 主题/内容描述")
    parser.add_argument("--outline", help="大纲 JSON 文件路径")
    parser.add_argument(
        "--theme", default="ink-classic", choices=THEMES.keys(), help="主题色"
    )
    parser.add_argument(
        "--output", default="./output/presentation.pptx", help="输出路径"
    )

    args = parser.parse_args()

    # 如果提供了大纲文件，使用大纲生成
    if args.outline:
        outline = parse_outline(args.outline)
    else:
        # 否则使用内容描述生成简单大纲
        outline = {
            "title": args.content,
            "slides": [
                {"layout": "L01", "title": args.content, "subtitle": "演示文稿"},
                {
                    "layout": "L05",
                    "title": "核心要点",
                    "content": [
                        {"title": "要点一", "description": "详细说明"},
                        {"title": "要点二", "description": "详细说明"},
                        {"title": "要点三", "description": "详细说明"},
                    ],
                },
                {"layout": "L01", "title": "感谢观看"},
            ],
        }

    # 生成 PPTX
    output_file = generate_pptx(outline, args.theme, args.output)
    print(f"✅ PPTX 已生成: {output_file}")

    # 自动打开（macOS）
    if sys.platform == "darwin":
        os.system(f'open "{output_file}"')


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)