Skill: tobewin/web-slides (version 2.0.4)

--- name: web-pptx-generator description: 专业单页面HTML PPT生成器。Use when user needs to create beautiful HTML presentations. Supports multiple themes, mobile-friendly, export to PDF. 单页PPT、HTML演示文稿、PPT生成。 version: 2.0.4 license: MIT-0 metadata: openclaw: emoji: "🎯" requires: bins: ["node"] dependencies: "npm install -g @aspect-ratio/preview-renderer" --- # Web Slides 为用户生成高质量、单文件、可直接运行的 HTML 演示文稿。 核心定位： - 不依赖任何 API Key - 与具体模型、厂商、推理服务无关 - 优先使用预制高端主题，而不是临时拼设计 - 以效果好为第一优先级，同时兼顾低 token - 输出天然兼容桌面演示与移动端浏览 ## 何时使用 - 用户要做 PPT、Slides、演示稿、pitch deck、发布会页面、汇报稿 - 用户要 HTML 版本的演示文稿，而不是 `.pptx` - 用户强调科技风、高级感、精美、商务、路演、发布会、数据报告 - 用户要求生成结果开箱即用、无需接外部服务 ## 工作方式 主 `SKILL.md` 只保留路由和生成规则。需要细节时按需读取： - 运行时说明：`references/runtime.md` - 主题总览：`references/theme-catalog.md` - 布局系统：`references/layout-system.md` - 质量标准：`references/quality-bar.md` - 输出流程：`references/assembly-workflow.md` - 内容结构：`references/content-schema.md` - 单主题细节：`references/themes/*.md` 本 skill 必须保持模型无关： - 不假设特定模型品牌、版本或厂商能力 - 不依赖 OpenAI、Claude、GPT 等命名能力 - 不把视觉效果建立在某个模型“更会设计”的前提上 - 重点依赖预制主题、布局系统、内容结构和本地脚本 如果需要快速定位主题、场景、布局或直接拿推荐结果，优先运行： - `node scripts/get-theme-bundle.mjs --list` - `node scripts/get-theme-bundle.mjs --theme cyber-grid` - `node scripts/get-theme-bundle.mjs --scene investor-pitch` - `node scripts/recommend-deck.mjs --scene investor-pitch --density medium` - `node scripts/scaffold-deck.mjs --scene ai-product-launch --title "你的标题"` - `node scripts/markdown-to-content.mjs --input examples/investor-pitch.md --output examples/investor-pitch.json --scene investor-pitch --mobile` - `node scripts/validate-content.mjs examples/investor-pitch.json` - `node scripts/qa-deck.mjs --content examples/investor-pitch.json` - `node scripts/smoke-test.mjs` - `node scripts/generate-slide-html.mjs --scene investor-pitch --title "Investor Pitch" --out dist/investor-pitch.html` - `node scripts/generate-slide-html.mjs --content examples/investor-pitch.json --out dist/investor-pitch.html` - `node scripts/render-preview.mjs --input dist/investor-pitch.html --output dist/investor-pitch.png` - `node scripts/build-preview-gallery.mjs` ## 生成原则 1. 先定场景，再定主题，再定布局，不要先写 HTML。 2. 输出流程必须与具体模型无关，核心质量来自主题系统、布局系统和本地生成脚本。 3. 优先从预制主题中选最合适的一套；不要混搭到破坏统一性。 4. 优先使用布局原型承载内容；不要每页都自由发挥。 5. 任何页面都要同时检查桌面显示和移动端缩放后的可读性。 6. 如果用户没有给风格，默认走高级、正式、科技感或商务感，不走卡通和花哨路线。 7. 如果用户给的是长文或提纲，优先整理为 `references/content-schema.md` 里的 JSON 结构，再生成 HTML。 ## 最小决策流程 ### Step 1：识别任务类型 从用户输入中提取： - 场景：汇报 / 路演 / 产品发布 / 技术分享 / 白皮书 / 数据复盘 / 教学培训 - 调性：科技 / 商务 / 极简 / 高端 / 发布会 / 学术 / 创意 - 内容密度：轻 / 中 / 重 - 输出规模：短 deck（6-10 页）/ 标准 deck（10-16 页）/ 长 deck（16-24 页） - 是否强调移动端浏览 ### Step 2：选择主题 优先先跑脚本拿最小推荐结果，再读 `references/theme-catalog.md`。 如果用户要求很明确，再读对应单主题文件： - `references/themes/cyber-grid.md` - `references/themes/executive-dark.md` - `references/themes/executive-light.md` - `references/themes/glass-future.md` - `references/themes/data-intelligence.md` - `references/themes/startup-pitch.md` - `references/themes/product-launch.md` - `references/themes/dev-summit.md` - `references/themes/luxury-black-gold.md` - `references/themes/editorial-serif.md` - `references/themes/neo-minimal.md` - `references/themes/creative-motion.md` ### Step 3：选择布局集 读 `references/layout-system.md`，从以下布局原型中选 6-10 个组合成整套 deck： - cover - agenda - section-break - insight - two-column - metrics - chart-focus - timeline - comparison - quote - closing ### Step 4：按质量标准约束输出 读 `references/quality-bar.md`，确保： - 首屏有高级感 - 页面层级清楚 - 信息密度可控 - 动效克制 - 手机端不崩 ### Step 5：组装 HTML 读 `references/assembly-workflow.md`，或直接用生成器脚本先产出单文件 HTML 骨架。 输出要求： - 单个 HTML 文件 - CSS 内嵌 - JS 尽量精简 - 对移动端友好 - 不依赖任何 API Key ## 输出偏好 默认生成策略： - 渲染：自定义单文件 HTML 演示运行时 - 主题：从 12 套旗舰主题中选择 - 布局：优先复用固定布局原型 - 动画：轻量、克制、不卡顿 - 色彩：统一，不乱换 - 文案：标题短、重点强、避免大段堆砌 ## 不要这样做 - 不要只换配色而没有版式差异 - 不要把“科技风”理解成荧光蓝堆砌 - 不要为了酷炫牺牲可读性 - 不要在每页塞满 8 个以上 bullet - 不要把网页长文直接切成幻灯片 - 不要为了省 token 放弃视觉完成度 ## 成功标准 这项技能的目标不是“能生成 PPT”，而是生成后让用户觉得： - 一打开就高级 - 可以直接拿去讲 - 不像普通网页排版 - 手机上看也精致 - 比同类技能更稳定、更省 token、更不依赖外部能力

path: LICENSE.txt
content: MIT No Attribution (MIT-0)

Copyright 2026 ToBeWin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED.


path: agents/openai.yaml
content: interface:
  display_name: "Web Slides"
  short_description: "Premium HTML slide deck generator"
  icon_small: "./assets/web-slides-small.svg"
  icon_large: "./assets/web-slides-large.svg"
  brand_color: "#59E1FF"
  default_prompt: "Use $web-slides to turn this outline into a polished single-file HTML presentation."


path: assets/web-slides-large.svg
content: <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
  <rect width="512" height="512" rx="96" fill="url(#bg)"/>
  <rect x="82" y="96" width="348" height="226" rx="34" fill="#0A1525" stroke="#59E1FF" stroke-opacity="0.32" stroke-width="6"/>
  <path d="M120 156H392" stroke="#59E1FF" stroke-opacity="0.42" stroke-width="8"/>
  <path d="M132 210H332" stroke="#F8FBFF" stroke-opacity="0.92" stroke-width="22" stroke-linecap="round"/>
  <path d="M132 258H272" stroke="#7AF7C4" stroke-opacity="0.96" stroke-width="22" stroke-linecap="round"/>
  <path d="M132 304H220" stroke="#59E1FF" stroke-opacity="0.9" stroke-width="22" stroke-linecap="round"/>
  <path d="M210 330L158 410H354L302 330" stroke="#7AF7C4" stroke-width="18" stroke-linecap="round" stroke-linejoin="round"/>
  <circle cx="390" cy="160" r="20" fill="#59E1FF" fill-opacity="0.35"/>
  <circle cx="356" cy="132" r="10" fill="#7AF7C4" fill-opacity="0.7"/>
  <defs>
    <linearGradient id="bg" x1="64" y1="44" x2="448" y2="468" gradientUnits="userSpaceOnUse">
      <stop stop-color="#07111F"/>
      <stop offset="1" stop-color="#14304A"/>
    </linearGradient>
  </defs>
</svg>


path: assets/web-slides-small.svg
content: <svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
  <rect width="128" height="128" rx="28" fill="#08131F"/>
  <rect x="20" y="24" width="88" height="62" rx="10" fill="url(#g1)" stroke="#59E1FF" stroke-opacity="0.35"/>
  <path d="M34 44H94" stroke="#59E1FF" stroke-opacity="0.45" stroke-width="2"/>
  <path d="M34 58H74" stroke="#EAF4FF" stroke-opacity="0.85" stroke-width="6" stroke-linecap="round"/>
  <path d="M34 71H62" stroke="#7AF7C4" stroke-opacity="0.85" stroke-width="6" stroke-linecap="round"/>
  <path d="M52 86L40 104H88L76 86" stroke="#7AF7C4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
  <defs>
    <linearGradient id="g1" x1="20" y1="24" x2="108" y2="86" gradientUnits="userSpaceOnUse">
      <stop stop-color="#10263C"/>
      <stop offset="1" stop-color="#0B1D34"/>
    </linearGradient>
  </defs>
</svg>


path: examples/investor-pitch.json
content: {
  "title": "Investor Pitch",
  "subtitle": "AI-native workflow platform",
  "scene": "investor-pitch",
  "density": "medium",
  "mobile": true,
  "slides": [
    {
      "layout": "cover",
      "eyebrow": "Series A",
      "title": "Building the AI Operating Layer",
      "subtitle": "From fragmented agents to one production system"
    },
    {
      "layout": "agenda",
      "title": "Today",
      "points": [
        "Problem",
        "Product",
        "Traction",
        "Roadmap"
      ]
    },
    {
      "layout": "insight",
      "title": "Teams lose time across disconnected AI workflows",
      "points": [
        "Prompts, files, and approvals live in separate tools",
        "Operations lack visibility and governance",
        "Scaling agent workflows reliably is still too expensive"
      ],
      "stat": "63%",
      "statLabel": "time lost to coordination"
    },
    {
      "layout": "two-column",
      "title": "A production system for real AI work",
      "leftTitle": "Platform layer",
      "leftPoints": [
        "Reusable workflow blocks",
        "Centralized memory and data connections",
        "Governed execution across teams"
      ],
      "rightTitle": "User value",
      "rightPoints": [
        "Ship agent workflows faster",
        "Improve reliability and visibility",
        "Cut duplicated prompt engineering effort"
      ]
    },
    {
      "layout": "metrics",
      "title": "Traction",
      "metrics": [
        {
          "value": "$1.2M",
          "label": "ARR pipeline"
        },
        {
          "value": "41",
          "label": "design partners"
        },
        {
          "value": "78%",
          "label": "weekly active usage"
        }
      ]
    },
    {
      "layout": "comparison",
      "title": "Why we win",
      "leftTitle": "Legacy workflow stack",
      "leftPoints": [
        "Point tools with fragmented ownership",
        "Low visibility across AI operations",
        "Hard to standardize reusable workflows"
      ],
      "rightTitle": "Our platform",
      "rightPoints": [
        "Unified workflow operating layer",
        "Central governance and observability",
        "Reusable blocks teams can scale with"
      ]
    },
    {
      "layout": "chart-focus",
      "title": "Usage deepens after onboarding",
      "chartBars": [
        {
          "label": "Week 1",
          "value": 38
        },
        {
          "label": "Week 4",
          "value": 64
        },
        {
          "label": "Week 8",
          "value": 86
        }
      ],
      "conclusion": "Teams expand from single-agent experiments to cross-functional workflows within two months."
    },
    {
      "layout": "timeline",
      "title": "Roadmap",
      "steps": [
        "Workflow OS",
        "Enterprise controls",
        "Marketplace",
        "Global expansion"
      ]
    },
    {
      "layout": "quote",
      "title": "“This is the first product that made our internal AI operations actually manageable.”",
      "subtitle": "Design Partner · Head of Automation"
    },
    {
      "layout": "closing",
      "title": "Raising to scale the AI operations category",
      "subtitle": "Seeking strategic investors for product, GTM, and ecosystem growth"
    }
  ]
}


path: examples/investor-pitch.md
content: # Investor Pitch
> AI-native workflow platform

## Cover: Building the AI Operating Layer
Eyebrow: Series A
Subtitle: From fragmented agents to one production system

## Agenda: Today
- Problem
- Product
- Traction
- Roadmap

## Insight: Teams lose time across disconnected AI workflows
- Prompts, files, and approvals live in separate tools
- Operations lack visibility and governance
- Scaling agent workflows reliably is still too expensive
Stat: 63%
StatLabel: time lost to coordination

## Two-Column: A production system for real AI work
### Platform layer
- Reusable workflow blocks
- Centralized memory and data connections
- Governed execution across teams
### User value
- Ship agent workflows faster
- Improve reliability and visibility
- Cut duplicated prompt engineering effort

## Metrics: Traction
- ARR pipeline: $1.2M
- design partners: 41
- weekly active usage: 78%

## Comparison: Why we win
### Legacy workflow stack
- Point tools with fragmented ownership
- Low visibility across AI operations
- Hard to standardize reusable workflows
### Our platform
- Unified workflow operating layer
- Central governance and observability
- Reusable blocks teams can scale with

## Chart: Usage deepens after onboarding
- Week 1: 38
- Week 4: 64
- Week 8: 86
Conclusion: Teams expand from single-agent experiments to cross-functional workflows within two months.

## Timeline: Roadmap
- Workflow OS
- Enterprise controls
- Marketplace
- Global expansion

## Quote
> "This is the first product that made our internal AI operations actually manageable."
Author: Design Partner · Head of Automation

## Closing: Raising to scale the AI operations category
Subtitle: Seeking strategic investors for product, GTM, and ecosystem growth


path: references/assembly-workflow.md
content: # Assembly Workflow

## Goal

输出单文件 HTML 演示文稿，优先高质量和稳定性，兼顾低 token 和移动端体验。

## Workflow

### 1. Confirm The Runtime

先读 `references/runtime.md`。

默认使用自定义单文件 HTML 演示运行时，不依赖特定模型或外部演示引擎。

### 2. Pick One Theme

先读 `references/theme-catalog.md`，只加载最匹配的单主题文件。

不要一次读取多个主题文件，避免无效占用 token。

优先使用脚本快速路由：

- `node scripts/get-theme-bundle.mjs --scene investor-pitch`
- `node scripts/recommend-deck.mjs --scene investor-pitch --density medium`

### 3. Pick 6-10 Layouts

从 `references/layout-system.md` 中选择适合的布局原型。

避免：

- 每页都不同
- 同套 deck 同时出现两种相互冲突的页面语言

### 4. Build A Slide Outline

先出大纲，再写 HTML。推荐格式：

1. 封面
2. 目录或开场观点
3. 章节页
4. 内容页
5. 数据页
6. 结尾页

如果需要快速拿结构化脚手架，运行：

- `node scripts/scaffold-deck.mjs --scene ai-product-launch --title "你的标题"`

如果已经有结构化内容，直接按 `references/content-schema.md` 准备 JSON 文件。

如果用户先给的是 markdown 提纲，优先转换：

- `node scripts/markdown-to-content.mjs --input examples/investor-pitch.md --output examples/investor-pitch.json --scene investor-pitch --mobile`

生成前可先校验：

- `node scripts/validate-content.mjs examples/investor-pitch.json`
- `node scripts/smoke-test.mjs`
- `node scripts/qa-deck.mjs --content examples/investor-pitch.json`

### 5. Generate HTML

规则：

- CSS 尽量内嵌
- 主题 token 用 CSS 变量统一管理
- 布局用可复用 class，不要每页写大量内联样式
- 动画只保留必要部分

如果只需要快速生成一个单文件 HTML 成品骨架，优先运行：

- `node scripts/generate-slide-html.mjs --scene investor-pitch --title "Investor Pitch" --out dist/investor-pitch.html`

如果已经有内容 JSON，优先运行：

- `node scripts/generate-slide-html.mjs --content examples/investor-pitch.json --out dist/investor-pitch.html`

生成后可做质量检查：

- `node scripts/qa-deck.mjs --content examples/investor-pitch.json --html dist/investor-pitch.html`

如果要导出首屏预览图：

- `node scripts/render-preview.mjs --input dist/investor-pitch.html --output dist/investor-pitch.png`
- `node scripts/build-preview-gallery.mjs`

### 6. Self Review

对照 `references/quality-bar.md` 自检：

- 是否有高级感
- 是否有信息层次
- 是否足够稳
- 是否移动端友好

## Token Discipline

### Keep In Main Context

- 当前用户需求
- 主 `SKILL.md`
- 1 个主题文件
- `layout-system.md`
- `quality-bar.md`

### Load Only If Needed

- 其他主题文件
- 具体单主题设计说明

## Implementation Hint

如果要做进一步工程化，可让脚本先根据场景输出：

- 推荐主题
- 推荐布局序列
- 推荐内容密度

然后再写最终 HTML，减少路由阶段的 token 消耗。


path: references/content-schema.md
content: # Content Schema

## Goal

给 `generate-slide-html.mjs` 提供结构化输入，避免每次都从零拼文案。

## Usage

传入：

- `--content path/to/deck.json`

也可以先用 markdown 提纲转换：

- `node scripts/markdown-to-content.mjs --input examples/investor-pitch.md --output examples/investor-pitch.json --scene investor-pitch --mobile`

内容文件应为 JSON，推荐结构：

```json
{
  "title": "Investor Pitch",
  "subtitle": "AI-native workflow platform",
  "scene": "investor-pitch",
  "density": "medium",
  "mobile": true,
  "slides": [
    {
      "layout": "cover",
      "eyebrow": "Series A",
      "title": "Building the AI Operating Layer",
      "subtitle": "From fragmented agents to one production system"
    },
    {
      "layout": "agenda",
      "title": "Today",
      "points": ["Problem", "Product", "Traction", "Roadmap"]
    },
    {
      "layout": "insight",
      "title": "Teams lose time across disconnected AI workflows",
      "points": [
        "Prompts, files, and approvals live in separate tools",
        "Operations lack visibility and governance",
        "Teams cannot scale agent workflows reliably"
      ],
      "stat": "63%",
      "statLabel": "time wasted in coordination"
    }
  ]
}
```

## Supported Layout Fields

### `cover`

- `eyebrow`
- `title`
- `subtitle`

### `agenda`

- `title`
- `points[]`

### `section-break`

- `title`
- `subtitle`

### `insight`

- `title`
- `points[]`
- `stat`
- `statLabel`

### `two-column`

- `title`
- `leftTitle`
- `leftPoints[]`
- `rightTitle`
- `rightPoints[]`

### `metrics`

- `title`
- `metrics[]`
  - `value`
  - `label`

### `chart-focus`

- `title`
- `chartBars[]`
  - `label`
  - `value`
- `conclusion`

### `timeline`

- `title`
- `steps[]`

### `comparison`

- `title`
- `leftTitle`
- `leftPoints[]`
- `rightTitle`
- `rightPoints[]`

### `quote`

- `title`
- `subtitle`

### `closing`

- `title`
- `subtitle`

## Recommendation

如果用户先给的是长讲稿，先整理成这个 JSON 结构，再交给生成器脚本。这样更稳、更省 token，也更容易保证页面质量。


path: references/layout-system.md
content: # Layout System

## Rule

不要为每一页重新发明版式。优先从固定布局原型中组装整套 deck。

## Core Layouts

### `cover`

用途：

- 封面
- 开场标题
- 发布会开幕页

特征：

- 一句话主标题
- 一句副标题或价值陈述
- 品牌标识或演讲者信息
- 背景氛围最强

### `agenda`

用途：

- 目录
- 章节导览

特征：

- 3-6 个条目
- 当前章节高亮
- 结构明确，避免装饰过重

### `section-break`

用途：

- 章节切换
- 节奏重置

特征：

- 大标题
- 小段引导语
- 视觉上比内容页更有仪式感

### `insight`

用途：

- 单页表达一个核心观点
- 适合高管汇报和路演

特征：

- 1 个结论标题
- 2-4 个支持要点
- 可附 1 个强调数字

### `two-column`

用途：

- 图文并列
- 左右论证
- 要点 + 说明

特征：

- 左右内容权重清晰
- 不要两边都塞满字

### `metrics`

用途：

- KPI
- 核心数字
- 成长指标

特征：

- 1 个主指标 + 2-4 个副指标
- 数字最大、说明最短
- 适合移动端卡片堆叠

### `chart-focus`

用途：

- 图表结论页
- 数据洞察页

特征：

- 图表只服务于结论
- 同页必须有一句结论总结
- 避免满屏网格与复杂图例

### `timeline`

用途：

- 路线图
- 项目阶段
- 事件演进

特征：

- 3-6 个节点
- 当前或关键节点最强
- 移动端改为纵向时间线

### `comparison`

用途：

- 方案对比
- 前后对比
- 竞品对比

特征：

- 维度不超过 4
- 视觉上要让优选项更突出

### `quote`

用途：

- 客户证言
- 金句
- 观点强调

特征：

- 大引语
- 小署名
- 装饰弱于内容

### `closing`

用途：

- 总结
- Q&A
- 联系方式

特征：

- 以一句强收束为主
- 可附二维码/链接/联系方式

## Recommended Deck Recipes

### 标准商务汇报

`cover` → `agenda` → `section-break` → `insight` × 3 → `comparison` → `metrics` → `closing`

### 融资路演

`cover` → `insight` → `problem-solution`（用 `two-column`）→ `market`（用 `metrics`）→ `traction`（用 `chart-focus`）→ `roadmap`（用 `timeline`）→ `closing`

### 技术分享

`cover` → `agenda` → `section-break` → `insight` → `two-column` → `timeline` → `chart-focus` → `closing`

### 发布会

`cover` → `section-break` → `insight` → `two-column` → `metrics` → `quote` → `closing`

## Combination Rule

12 套主题都应支持以上布局原型。这样主题和布局可组合，形成高丰富度，但仍保持统一输出质量。


path: references/quality-bar.md
content: # Quality Bar

## Priority Order

1. 效果好
2. 稳定好
3. 低 token
4. 零 API Key
5. 移动端友好

低 token 不能以牺牲视觉完成度为代价。

## Visual Standard

生成结果至少满足以下标准：

- 首屏有记忆点，不像普通网页首屏
- 主题统一，不出现风格漂移
- 标题、正文、数字、注释层级清晰
- 配色控制在 1 个主色系 + 1 个强调色系
- 动效轻量，不杂乱，不廉价
- 页面留白合理，不拥挤

## Content Density Rules

- 一页 1 个中心观点
- 标题最好不超过 16 个中文字符
- 正文 bullet 最好 3-5 条
- 单条 bullet 最好不超过 2 行
- 数据页优先“结论 + 数字”，不要只有数字没有解释
- 图表页必须用一句话写清结论

## Mobile Rules

- 小屏下标题仍然可读
- 双栏需要可以降级为单栏堆叠
- 指标卡片可纵向排列
- 不依赖 hover 才能理解内容
- 边距和字号要用响应式策略

## Motion Rules

- 动效只服务层次和节奏
- 默认使用淡入、轻位移、轻缩放
- 避免大幅旋转、频繁弹跳、强炫光扫过
- 正式场景动效要更克制

## Failure Modes To Avoid

- 科技风变成霓虹夜店风
- 商务风变成模板市场廉价蓝
- 极简风变成空白且无层次
- 图表很多但没有结论
- 页面内容溢出或字号过小
- 手机端标题断裂、卡片挤压、行距失衡

## Final Check

交付前至少问自己：

- 这份演示稿是否可以直接用于正式场合？
- 封面是否足够“高端”？
- 是否比同类技能默认结果更精致？
- 如果用户在手机上打开，是否仍然像成品而不是残版？


path: references/runtime.md
content: # Runtime

## Positioning

本 skill 使用自定义单文件 HTML 演示运行时。

它不是某个模型专属方案，也不依赖特定推理厂商。
视觉效果来自：

- 预制主题系统
- 固定布局原型
- 结构化内容输入
- 本地脚本生成流程

## Core Principles

- 与模型无关
- 与 API Key 无关
- 与外部设计服务无关
- 优先保证效果和稳定性

## Runtime Behavior

当前输出运行时特征：

- 单文件 HTML
- CSS 内嵌
- 轻量 JS 翻页
- 键盘翻页
- 触摸滑动
- 移动端自动降级为纵向连续浏览

可选的本地视觉 QA：

- 可用 headless Chrome 导出 PNG 预览
- 可批量生成桌面与移动端预览图
- 这部分属于本地辅助验收能力，不是核心生成链的强依赖

## Why This Matters

这样设计的好处是：

- skill 可以被不同模型稳定调用
- 输出质量更多由模板系统保证，而不是靠提示词碰运气
- token 消耗更可控
- 用户不需要关心底层模型差异


path: references/theme-catalog.md
content: # Theme Catalog

## Usage

先根据用户场景和调性选主题，再读取对应单主题文件。

优先选择最贴合的一个主题，不要同时加载多个主题文档。

## 12 Flagship Themes

| Theme | Best For | Tone |
|---|---|---|
| `cyber-grid` | AI、云、科技发布、行业峰会 | 强科技、未来感、深色 |
| `executive-dark` | 董事会、高管汇报、战略复盘 | 沉稳、高端、压场 |
| `executive-light` | 咨询方案、客户提案、项目汇报 | 明亮、专业、整洁 |
| `glass-future` | 创新实验室、AIGC、概念展示 | 轻未来、玻璃质感 |
| `data-intelligence` | BI、经营分析、年度复盘 | 数据导向、理性、精准 |
| `startup-pitch` | 融资路演、创业 BP、商业模式 | 进攻感、增长感、品牌化 |
| `product-launch` | 新品发布、路线图、功能介绍 | 舞台感、产品感、现代 |
| `dev-summit` | 技术分享、架构演讲、开发者大会 | 工程感、清晰、可信 |
| `luxury-black-gold` | 高端品牌、管理层演示、重磅发布 | 奢雅、权威、仪式感 |
| `editorial-serif` | 白皮书、研究报告、教育内容 | 编辑感、知识感、耐读 |
| `neo-minimal` | 极简品牌提案、设计复盘、作品陈述 | 克制、留白、高级极简 |
| `creative-motion` | 品牌活动、营销 campaign、创意提案 | 张力、动态、视觉冲击 |

## Scene To Theme Mapping

| Scene | Preferred Theme | Backup Theme |
|---|---|---|
| AI 产品发布 | `cyber-grid` | `glass-future` |
| 企业战略汇报 | `executive-dark` | `executive-light` |
| 咨询项目提案 | `executive-light` | `neo-minimal` |
| 融资路演 | `startup-pitch` | `luxury-black-gold` |
| 数据分析报告 | `data-intelligence` | `executive-light` |
| 技术大会演讲 | `dev-summit` | `cyber-grid` |
| 新品发布会 | `product-launch` | `glass-future` |
| 高端品牌发布 | `luxury-black-gold` | `neo-minimal` |
| 白皮书 / 研究报告 | `editorial-serif` | `neo-minimal` |
| 极简设计提案 | `neo-minimal` | `executive-light` |
| 创意营销案 | `creative-motion` | `product-launch` |
| 创新概念展示 | `glass-future` | `creative-motion` |

## Density Guidance

| Density | Recommended Themes |
|---|---|
| 轻内容 | `product-launch`, `glass-future`, `creative-motion`, `luxury-black-gold` |
| 中内容 | `cyber-grid`, `startup-pitch`, `dev-summit`, `neo-minimal` |
| 重内容 | `executive-light`, `executive-dark`, `data-intelligence`, `editorial-serif` |

## Mobile Guidance

移动端优先级最高的主题：

1. `executive-light`
2. `neo-minimal`
3. `data-intelligence`
4. `startup-pitch`

这些主题更适合小屏下保持标题清晰、卡片整洁、信息层次稳定。


path: references/themes/creative-motion.md
content: # creative-motion

## Positioning

面向创意提案、营销 campaign、品牌活动的高张力主题。

## Visual DNA

- 鲜明色块
- 有节奏的几何形状
- 动态构图
- 动效比其他主题更明显

## Best Scenes

- 活动方案
- 创意 campaign
- 品牌年轻化提案
- 宣传展示

## Design Tokens

- Background: `linear-gradient(135deg, #0f172a, #1d4ed8)`
- Surface: `rgba(255,255,255,0.08)`
- Primary: `#f97316`
- Accent: `#22d3ee`
- Text: `#f8fafc`

## Layout Notes

- `cover`、`section-break`、`quote` 更有表现力
- `timeline` 可以更动感
- 内容型页面仍要克制装饰

## Mobile Note

动画幅度要缩小，保证移动端不会显得凌乱。


path: references/themes/cyber-grid.md
content: # cyber-grid

## Positioning

面向 AI、云计算、科技峰会、行业论坛的旗舰科技风主题。

## Visual DNA

- 深色底
- 冷蓝青光
- 网格、描边、发光线条
- 标题强、数字亮、信息板块清晰

## Best Scenes

- AI 产品发布
- 技术品牌介绍
- 云平台方案宣讲
- 行业趋势 keynote

## Design Tokens

- Background: `#07111f`
- Surface: `#0d1b2f`
- Primary: `#59e1ff`
- Accent: `#7af7c4`
- Text: `#eaf4ff`

## Layout Notes

- `cover` 强背景氛围，可用网格与径向光
- `metrics` 适合做 HUD 仪表盘感
- `chart-focus` 适合荧光描边图表

## Mobile Note

减少装饰线数量，优先保证标题和数字的对比度。


path: references/themes/data-intelligence.md
content: # data-intelligence

## Positioning

面向经营分析、BI 报告、增长复盘的数据型主题。

## Visual DNA

- 冷静理性
- 规则网格
- 数据卡片清晰
- 图表优先服务结论

## Best Scenes

- 月度经营分析
- 年度复盘
- 用户增长汇报
- 业务看板演示

## Design Tokens

- Background: `#0c1220`
- Surface: `#121a2b`
- Primary: `#6aa9ff`
- Accent: `#4be3c2`
- Text: `#e5edf9`

## Layout Notes

- `metrics` 和 `chart-focus` 是绝对主力
- `timeline` 可用于季度规划
- `insight` 页面要结论先行

## Mobile Note

优先将图表简化为关键指标卡，不在手机端展示复杂细图。


path: references/themes/dev-summit.md
content: # dev-summit

## Positioning

面向架构分享、开发者大会、工程团队技术演示的工程型主题。

## Visual DNA

- 工程感强
- 字体清晰
- 代码与图示兼容
- 科技但不过度炫

## Best Scenes

- 架构演讲
- 技术方案分享
- 开发者大会
- 开源项目介绍

## Design Tokens

- Background: `#0b1020`
- Surface: `#121933`
- Primary: `#7dd3fc`
- Accent: `#a3e635`
- Text: `#dbeafe`

## Layout Notes

- `two-column` 和 `insight` 最常用
- `timeline` 适合讲技术演进
- `chart-focus` 适合性能数据展示

## Mobile Note

代码截图不应成为主角，优先提炼为结构图和结论卡片。


path: references/themes/editorial-serif.md
content: # editorial-serif

## Positioning

面向白皮书、研究报告、教育内容、深度分析的编辑型主题。

## Visual DNA

- 杂志感排版
- 衬线标题
- 温和底色
- 知识型、耐读型

## Best Scenes

- 白皮书
- 研究报告
- 教学课程
- 学术与人文内容

## Design Tokens

- Background: `#f2efe8`
- Surface: `#fbfaf7`
- Primary: `#2f3a4d`
- Accent: `#8b5e3c`
- Text: `#1f2937`

## Layout Notes

- `quote`, `two-column`, `insight` 表现稳定
- `chart-focus` 应使用温和配色
- `closing` 适合知识型总结

## Mobile Note

正文行长要严格控制，避免衬线字体在手机上显得拥挤。


path: references/themes/executive-dark.md
content: # executive-dark

## Positioning

用于董事会、高管汇报、战略复盘的高压场正式主题。

## Visual DNA

- 深墨色背景
- 低饱和中性色
- 强对比标题
- 卡片克制、边框精细

## Best Scenes

- 管理层汇报
- 战略复盘
- 年度经营总结
- 高级客户会议

## Design Tokens

- Background: `#111315`
- Surface: `#1b1f24`
- Primary: `#f3f4f6`
- Accent: `#c7a86a`
- Text: `#f8fafc`

## Layout Notes

- `insight` 与 `comparison` 是主力
- `closing` 要稳，不做夸张动效
- `metrics` 用少量金色强调关键数字

## Mobile Note

保持大标题与留白，避免深色底下文字行距过密。


path: references/themes/executive-light.md
content: # executive-light

## Positioning

用于咨询方案、客户提案、项目汇报的高级明亮商务主题。

## Visual DNA

- 大量留白
- 浅灰与白色表面
- 深色标题
- 局部品牌蓝或青色点缀

## Best Scenes

- 项目提案
- 咨询报告
- 客户汇报
- 周报月报

## Design Tokens

- Background: `#f5f7fb`
- Surface: `#ffffff`
- Primary: `#1f3c88`
- Accent: `#22c1c3`
- Text: `#101828`

## Layout Notes

- `agenda`, `insight`, `two-column` 最稳
- `chart-focus` 要走清爽数据面板路线
- `comparison` 适合做方案优劣矩阵

## Mobile Note

这是最适合移动端阅读的主题之一，优先保留卡片和标题层次。


path: references/themes/glass-future.md
content: # glass-future

## Positioning

用于创新、AIGC、概念展示的轻未来玻璃感主题。

## Visual DNA

- 透明玻璃卡片
- 柔和渐变背景
- 霓虹弱化，不刺眼
- 漂浮感与轻模糊

## Best Scenes

- AIGC 方案
- 创新实验室
- 未来产品概念
- 科技品牌介绍

## Design Tokens

- Background: `linear-gradient(135deg, #08131f, #1f2f56)`
- Surface: `rgba(255,255,255,0.10)`
- Primary: `#8bdcff`
- Accent: `#b69cff`
- Text: `#f3f8ff`

## Layout Notes

- `cover` 与 `quote` 最出彩
- `two-column` 用半透明玻璃卡片承载信息
- `closing` 可以做未来感收束

## Mobile Note

玻璃层数不要过多，避免小屏阅读发灰。


path: references/themes/luxury-black-gold.md
content: # luxury-black-gold

## Positioning

面向高端品牌、重要发布、仪式感场景的奢雅主题。

## Visual DNA

- 黑金配色
- 极低噪音背景
- 高级衬线或精致无衬线标题
- 节奏慢、气场强

## Best Scenes

- 高端品牌发布
- 重要管理层展示
- 高净值客户提案
- 高端活动开场

## Design Tokens

- Background: `#090909`
- Surface: `#141414`
- Primary: `#e8d3a1`
- Accent: `#f6e7c9`
- Text: `#f8f3ea`

## Layout Notes

- `cover` 必须有仪式感
- `quote` 页面非常适合
- `metrics` 只展示少量关键数字

## Mobile Note

控制金色使用面积，避免手机端发灰或显脏。


path: references/themes/neo-minimal.md
content: # neo-minimal

## Positioning

面向极简设计提案、作品陈述、品牌升级说明的高级极简主题。

## Visual DNA

- 大留白
- 少量颜色
- 版式精确
- 质感来自比例而非装饰

## Best Scenes

- 设计提案
- 品牌升级
- 极简商务汇报
- 作品集陈述

## Design Tokens

- Background: `#f8f8f6`
- Surface: `#ffffff`
- Primary: `#111111`
- Accent: `#b6b09f`
- Text: `#18181b`

## Layout Notes

- `cover`、`insight`、`closing` 很强
- `comparison` 适合做克制对照
- 不要堆过多图形元素

## Mobile Note

这是移动端表现最稳的主题之一，但要避免大段正文。


path: references/themes/product-launch.md
content: # product-launch

## Positioning

面向新品发布、功能发布、路线图发布的舞台型主题。

## Visual DNA

- 大幅标题
- 强聚焦产品图或卖点
- 干净黑底或深蓝底
- 大留白配合局部高亮

## Best Scenes

- 新品发布会
- 功能发布
- 产品亮点演讲
- 路线图宣讲

## Design Tokens

- Background: `#05070b`
- Surface: `#10131a`
- Primary: `#ffffff`
- Accent: `#2dd4bf`
- Text: `#f8fafc`

## Layout Notes

- `cover`, `section-break`, `closing` 是情绪锚点
- `insight` 用于卖点表达
- `timeline` 用于 roadmap

## Mobile Note

单页文案要更少，保证发布会大标题在小屏下仍有冲击力。


path: references/themes/startup-pitch.md
content: # startup-pitch

## Positioning

面向融资路演、创业 BP、增长故事的进攻型主题。

## Visual DNA

- 高对比深浅搭配
- 明亮品牌色强调
- 数字和标题有冲劲
- 页面节奏快

## Best Scenes

- Seed / A 轮路演
- 增长故事
- 商业模式说明
- 招商演示

## Design Tokens

- Background: `#0f172a`
- Surface: `#ffffff`
- Primary: `#7c3aed`
- Accent: `#22c55e`
- Text: `#e2e8f0`

## Layout Notes

- `cover` 要强势
- `metrics` 和 `timeline` 很关键
- `comparison` 可用于竞品或方案优势

## Mobile Note

保证关键数字和融资亮点在首屏内即可被看到。


path: scripts/build-preview-gallery.mjs
content: #!/usr/bin/env node

import fs from "node:fs";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { listThemes } from "./theme-bundles.mjs";

const repoRoot = process.cwd();
const outDir = path.resolve(repoRoot, "dist/previews");
fs.mkdirSync(outDir, { recursive: true });

const sceneByTheme = {
  "cyber-grid": "ai-product-launch",
  "executive-dark": "board-review",
  "executive-light": "consulting-proposal",
  "glass-future": "aigc-solution",
  "data-intelligence": "bi-report",
  "startup-pitch": "investor-pitch",
  "product-launch": "new-product-launch",
  "dev-summit": "tech-talk",
  "luxury-black-gold": "premium-brand-launch",
  "editorial-serif": "whitepaper",
  "neo-minimal": "design-proposal",
  "creative-motion": "campaign-proposal",
};

function runNode(script, args) {
  execFileSync(process.execPath, [path.join(repoRoot, script), ...args], {
    stdio: "inherit",
    cwd: repoRoot,
  });
}

const manifest = [];

for (const theme of listThemes()) {
  const html = path.join(outDir, `${theme}.html`);
  const png = path.join(outDir, `${theme}.png`);
  const mobilePng = path.join(outDir, `${theme}.mobile.png`);
  const scene = sceneByTheme[theme] ?? "project-report";

  runNode("scripts/generate-slide-html.mjs", [
    "--theme",
    theme,
    "--scene",
    scene,
    "--title",
    theme,
    "--out",
    html,
  ]);
  runNode("scripts/render-preview.mjs", ["--input", html, "--output", png]);
  runNode("scripts/render-preview.mjs", ["--input", html, "--output", mobilePng, "--mobile"]);

  manifest.push({
    theme,
    scene,
    html: path.relative(repoRoot, html),
    desktopPreview: path.relative(repoRoot, png),
    mobilePreview: path.relative(repoRoot, mobilePng),
  });
}

const html = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Slides Preview Gallery</title>
  <style>
    * { box-sizing: border-box; }
    body { margin: 0; background: #08111f; color: #f8fafc; font-family: Inter, "PingFang SC", sans-serif; }
    .wrap { max-width: 1480px; margin: 0 auto; padding: 40px 24px 80px; }
    h1 { margin: 0; font-size: clamp(2.4rem, 5vw, 4.8rem); }
    p { color: #94a3b8; line-height: 1.7; }
    .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; margin-top: 28px; }
    .card { border: 1px solid rgba(255,255,255,0.08); border-radius: 28px; background: rgba(255,255,255,0.03); padding: 20px; }
    .meta { display: flex; justify-content: space-between; gap: 20px; align-items: center; margin-bottom: 14px; }
    .theme { font-size: 1.1rem; font-weight: 700; }
    .scene { color: #67e8f9; font-size: 0.92rem; }
    .preview-grid { display: grid; grid-template-columns: 1.3fr 0.7fr; gap: 16px; align-items: start; }
    .preview-grid img { width: 100%; border-radius: 18px; display: block; border: 1px solid rgba(255,255,255,0.08); background: #020617; }
    .links { margin-top: 14px; display: flex; gap: 16px; }
    .links a { color: #7dd3fc; text-decoration: none; font-size: 0.92rem; }
    @media (max-width: 1080px) {
      .grid { grid-template-columns: 1fr; }
      .preview-grid { grid-template-columns: 1fr; }
    }
  </style>
</head>
<body>
  <div class="wrap">
    <h1>Web Slides Preview Gallery</h1>
    <p>桌面与移动端预览一并导出，方便在上架前集中检查 12 套主题的首屏效果与小屏表现。</p>
    <div class="grid">
      ${manifest.map((item) => `
        <div class="card">
          <div class="meta">
            <div class="theme">${item.theme}</div>
            <div class="scene">${item.scene}</div>
          </div>
          <div class="preview-grid">
            <img src="./${path.basename(item.desktopPreview)}" alt="${item.theme} desktop preview">
            <img src="./${path.basename(item.mobilePreview)}" alt="${item.theme} mobile preview">
          </div>
          <div class="links">
            <a href="./${path.basename(item.html)}">Open HTML</a>
            <a href="./${path.basename(item.desktopPreview)}">Desktop PNG</a>
            <a href="./${path.basename(item.mobilePreview)}">Mobile PNG</a>
          </div>
        </div>
      `).join("")}
    </div>
  </div>
</body>
</html>`;

fs.writeFileSync(path.join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
fs.writeFileSync(path.join(outDir, "index.html"), html, "utf8");

console.log(JSON.stringify({
  ok: true,
  outDir,
  themes: manifest.length,
}, null, 2));


path: scripts/chrome-utils.mjs
content: import { execFileSync } from "node:child_process";

export const DEFAULT_CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";

export function renderHtmlToPng({
  chromePath = process.env.CHROME_BIN || DEFAULT_CHROME,
  inputUrl,
  outputPath,
  width = 1600,
  height = 900,
  fullPage = false,
}) {
  const args = [
    "--headless=new",
    "--disable-gpu",
    "--no-first-run",
    "--no-default-browser-check",
    "--allow-file-access-from-files",
    `--window-size=${width},${height}`,
    `--screenshot=${outputPath}`,
  ];

  if (fullPage) args.push("--hide-scrollbars");
  args.push(inputUrl);

  try {
    execFileSync(chromePath, args, {
      stdio: ["ignore", "pipe", "pipe"],
      encoding: "utf8",
    });
  } catch (error) {
    const stderr = String(error?.stderr || "").trim();
    const stdout = String(error?.stdout || "").trim();
    throw new Error(`Chrome preview render failed.${stderr ? `\n${stderr}` : stdout ? `\n${stdout}` : ""}`);
  }
}


path: scripts/content-utils.mjs
content: export const SUPPORTED_LAYOUTS = new Set([
  "cover",
  "agenda",
  "section-break",
  "insight",
  "two-column",
  "metrics",
  "chart-focus",
  "timeline",
  "comparison",
  "quote",
  "closing",
]);

function isNonEmptyString(value) {
  return typeof value === "string" && value.trim().length > 0;
}

function isStringArray(value) {
  return Array.isArray(value) && value.every((item) => typeof item === "string");
}

function isMetricArray(value) {
  return Array.isArray(value) && value.every((item) => item && typeof item === "object" && isNonEmptyString(item.value) && isNonEmptyString(item.label));
}

function isChartArray(value) {
  return Array.isArray(value) && value.every((item) => item && typeof item === "object" && isNonEmptyString(item.label) && Number.isFinite(Number(item.value)));
}

export function validateSlide(slide, index) {
  const errors = [];

  if (!slide || typeof slide !== "object") {
    return [`slides[${index}] must be an object`];
  }

  if (!SUPPORTED_LAYOUTS.has(slide.layout)) {
    errors.push(`slides[${index}].layout must be one of: ${Array.from(SUPPORTED_LAYOUTS).join(", ")}`);
    return errors;
  }

  const requireTitle = !["cover"].includes(slide.layout);
  if (requireTitle && !isNonEmptyString(slide.title)) {
    errors.push(`slides[${index}].title is required for layout "${slide.layout}"`);
  }

  switch (slide.layout) {
    case "agenda":
      if (!isStringArray(slide.points)) errors.push(`slides[${index}].points must be a string array`);
      break;
    case "insight":
      if (!isStringArray(slide.points)) errors.push(`slides[${index}].points must be a string array`);
      break;
    case "two-column":
      if (!isNonEmptyString(slide.leftTitle)) errors.push(`slides[${index}].leftTitle is required`);
      if (!isStringArray(slide.leftPoints)) errors.push(`slides[${index}].leftPoints must be a string array`);
      if (!isNonEmptyString(slide.rightTitle)) errors.push(`slides[${index}].rightTitle is required`);
      if (!isStringArray(slide.rightPoints)) errors.push(`slides[${index}].rightPoints must be a string array`);
      break;
    case "metrics":
      if (!isMetricArray(slide.metrics)) errors.push(`slides[${index}].metrics must be an array of {value,label}`);
      break;
    case "chart-focus":
      if (!isChartArray(slide.chartBars)) errors.push(`slides[${index}].chartBars must be an array of {label,value}`);
      if (!isNonEmptyString(slide.conclusion)) errors.push(`slides[${index}].conclusion is required`);
      break;
    case "timeline":
      if (!isStringArray(slide.steps)) errors.push(`slides[${index}].steps must be a string array`);
      break;
    case "comparison":
      if (!isNonEmptyString(slide.leftTitle)) errors.push(`slides[${index}].leftTitle is required`);
      if (!isStringArray(slide.leftPoints)) errors.push(`slides[${index}].leftPoints must be a string array`);
      if (!isNonEmptyString(slide.rightTitle)) errors.push(`slides[${index}].rightTitle is required`);
      if (!isStringArray(slide.rightPoints)) errors.push(`slides[${index}].rightPoints must be a string array`);
      break;
    default:
      break;
  }

  return errors;
}

export function validateDeckContent(content) {
  const errors = [];

  if (!content || typeof content !== "object") {
    return ["content must be a JSON object"];
  }

  if (content.slides !== undefined && !Array.isArray(content.slides)) {
    errors.push(`content.slides must be an array`);
    return errors;
  }

  for (const [index, slide] of (content.slides ?? []).entries()) {
    errors.push(...validateSlide(slide, index));
  }

  return errors;
}


path: scripts/generate-slide-html.mjs
content: #!/usr/bin/env node

import fs from "node:fs";
import path from "node:path";
import { recommendTheme, DEFAULT_DECK_LENGTH, getTheme } from "./theme-bundles.mjs";
import { getThemeStyle } from "./theme-styles.mjs";
import { validateDeckContent } from "./content-utils.mjs";

function parseArgs(argv) {
  const args = {
    title: "Premium HTML Deck",
    subtitle: "Single-file presentation generated by web-slides",
    scene: "",
    density: "medium",
    mobile: false,
    out: "dist/deck.html",
    content: "",
    theme: "",
  };

  for (let i = 2; i < argv.length; i += 1) {
    const part = argv[i];
    if (part === "--title") args.title = argv[i + 1] ?? args.title;
    if (part === "--subtitle") args.subtitle = argv[i + 1] ?? args.subtitle;
    if (part === "--scene") args.scene = argv[i + 1] ?? args.scene;
    if (part === "--density") args.density = argv[i + 1] ?? args.density;
    if (part === "--mobile") args.mobile = true;
    if (part === "--out") args.out = argv[i + 1] ?? args.out;
    if (part === "--content") args.content = argv[i + 1] ?? args.content;
    if (part === "--theme") args.theme = argv[i + 1] ?? args.theme;
  }

  return args;
}

function escapeHtml(input) {
  return String(input)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

function buildDeckContent(scene, themeName, slideCount) {
  const sceneTitle = scene || "general-presentation";
  return [
    {
      layout: "cover",
      title: sceneTitle.replaceAll("-", " ").toUpperCase(),
      subtitle: "预制高端主题 + 响应式布局 + 单文件 HTML 输出",
      eyebrow: themeName,
    },
    {
      layout: "agenda",
      title: "内容总览",
      points: ["目标与背景", "关键洞察", "核心指标", "行动建议"],
    },
    {
      layout: "section-break",
      title: "第一部分",
      subtitle: "从问题定义切入，快速建立共识。",
    },
    {
      layout: "insight",
      title: "一个页面只讲一个结论",
      points: [
        "先给结论，再给支持证据",
        "信息密度受控，避免大段文字堆砌",
        "标题、数字、说明有清晰层级",
      ],
      stat: "87%",
      statLabel: "演示完成度提升",
    },
    {
      layout: "two-column",
      title: "结构化表达更适合正式场景",
      leftTitle: "产品优势",
      leftPoints: ["零 API Key", "低 token 成本", "单文件可分享"],
      rightTitle: "用户感知",
      rightPoints: ["打开就像成品", "手机端也精致", "适合直接汇报"],
    },
    {
      layout: "metrics",
      title: "核心指标",
      metrics: [
        { value: "12", label: "旗舰主题" },
        { value: "96+", label: "主题 × 布局组合" },
        { value: "1", label: "单文件交付" },
      ],
    },
    {
      layout: "chart-focus",
      title: "效果优先，不靠提示词堆设计",
      chartBars: [
        { label: "视觉稳定性", value: 92 },
        { label: "移动端适配", value: 88 },
        { label: "生成效率", value: 84 },
      ],
      conclusion: "通过预制主题系统把审美前置，生成质量更稳。",
    },
    {
      layout: "timeline",
      title: "落地路径",
      steps: ["主题系统", "布局原型", "脚本路由", "HTML 生成"],
    },
    {
      layout: "quote",
      title: "“不是临时拼设计，而是调用一套已经打磨好的演示系统。”",
      subtitle: "这就是低 token 与高质量可以同时成立的原因。",
    },
    {
      layout: "closing",
      title: "让 HTML PPT 成为 ClawHub 的标杆品类",
      subtitle: `${themeName} · ${slideCount} slides · mobile-ready`,
    },
  ];
}

function loadContent(args) {
  if (!args.content) return null;

  const filePath = path.resolve(process.cwd(), args.content);
  const raw = fs.readFileSync(filePath, "utf8");
  const parsed = JSON.parse(raw);
  const errors = validateDeckContent(parsed);

  if (errors.length) {
    throw new Error(`Invalid content schema:\n- ${errors.join("\n- ")}`);
  }

  return {
    title: parsed.title || args.title,
    subtitle: parsed.subtitle || args.subtitle,
    scene: parsed.scene || args.scene,
    density: parsed.density || args.density,
    mobile: parsed.mobile ?? args.mobile,
    slides: Array.isArray(parsed.slides) ? parsed.slides : [],
  };
}

function normalizeSlides(slides, fallbackScene, fallbackTheme, fallbackCount) {
  if (!slides?.length) {
    return buildDeckContent(fallbackScene, fallbackTheme, fallbackCount);
  }

  return slides.map((slide) => ({
    ...slide,
    layout: slide.layout || "insight",
  }));
}

function renderCover(slide, index, variant) {
  if (variant === "tech-hud") {
    return `
      <section class="slide slide-cover active variant-${variant}" data-index="${index}">
        <div class="panel cover-accent">
          <div class="hud-grid">
            <div>
              <p class="eyebrow">${escapeHtml(slide.eyebrow ?? "web-slides")}</p>
              <h1>${escapeHtml(slide.title ?? "")}</h1>
              <p class="lead">${escapeHtml(slide.subtitle ?? "")}</p>
            </div>
            <div class="cover-side-card">
              <div class="mini-label">SYSTEM STATUS</div>
              <div class="mini-value">ONLINE</div>
              <div class="mini-meta">THEME / ${escapeHtml(slide.eyebrow ?? "cyber-grid")}</div>
            </div>
          </div>
        </div>
      </section>
    `;
  }

  if (variant === "luxury-black-gold") {
    return `
      <section class="slide slide-cover active variant-${variant}" data-index="${index}">
        <div class="panel cover-accent luxury-frame">
          <p class="eyebrow">${escapeHtml(slide.eyebrow ?? "web-slides")}</p>
          <h1>${escapeHtml(slide.title ?? "")}</h1>
          <div class="gold-divider"></div>
          <p class="lead">${escapeHtml(slide.subtitle ?? "")}</p>
        </div>
      </section>
    `;
  }

  if (variant === "editorial-serif") {
    return `
      <section class="slide slide-cover active variant-${variant}" data-index="${index}">
        <div class="panel editorial-cover">
          <p class="eyebrow">${escapeHtml(slide.eyebrow ?? "web-slides")}</p>
          <h1>${escapeHtml(slide.title ?? "")}</h1>
          <p class="lead">${escapeHtml(slide.subtitle ?? "")}</p>
          <p class="editorial-kicker">A refined HTML presentation system for serious storytelling.</p>
        </div>
      </section>
    `;
  }

  return `
    <section class="slide slide-cover active variant-${variant}" data-index="${index}">
      <div class="panel cover-accent">
        <p class="eyebrow">${escapeHtml(slide.eyebrow ?? "web-slides")}</p>
        <h1>${escapeHtml(slide.title ?? "")}</h1>
        <p class="lead">${escapeHtml(slide.subtitle ?? "")}</p>
      </div>
    </section>
  `;
}

function renderAgenda(slide, index, variant) {
  if (variant === "executive-light") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel">
          <p class="eyebrow">AGENDA</p>
          <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
          <div class="agenda-list executive-agenda">
            ${(slide.points ?? []).map((point, pointIndex) => `<div class="agenda-item executive-agenda-item"><span>${pointIndex + 1}</span><strong>${escapeHtml(point)}</strong></div>`).join("")}
          </div>
        </div>
      </section>
    `;
  }

  if (variant === "cyber-grid") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel">
          <div class="metrics-head">
            <div>
              <p class="eyebrow">SYSTEM MAP</p>
              <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
            </div>
            <div class="badge-pill">PHASE PLAN</div>
          </div>
          <div class="agenda-list hud-agenda">
            ${(slide.points ?? []).map((point, pointIndex) => `<div class="agenda-item hud-agenda-item"><span>${pointIndex + 1}</span><strong>${escapeHtml(point)}</strong></div>`).join("")}
          </div>
        </div>
      </section>
    `;
  }

  return `
    <section class="slide variant-${variant}" data-index="${index}">
      <div class="panel">
        <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
        <div class="agenda-list">
          ${(slide.points ?? []).map((point, pointIndex) => `<div class="agenda-item"><span>${pointIndex + 1}</span><strong>${escapeHtml(point)}</strong></div>`).join("")}
        </div>
      </div>
    </section>
  `;
}

function renderSectionBreak(slide, index, variant) {
  if (variant === "product-launch") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel section-panel launch-break">
          <p class="eyebrow">NEXT CHAPTER</p>
          <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
          <p class="subtitle">${escapeHtml(slide.subtitle ?? "")}</p>
          ${slide.points ? `<div class="section-highlights">${slide.points.map(p => `<div class="highlight-item">${escapeHtml(p)}</div>`).join("")}</div>` : ""}
        </div>
      </section>
    `;
  }

  if (variant === "luxury-black-gold") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel section-panel luxury-break">
          <p class="eyebrow">SECTION ${index + 1}</p>
          <div class="gold-divider"></div>
          <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
          <p class="subtitle">${escapeHtml(slide.subtitle ?? "")}</p>
          ${slide.points ? `<div class="section-highlights">${slide.points.map(p => `<div class="highlight-item">${escapeHtml(p)}</div>`).join("")}</div>` : ""}
        </div>
      </section>
    `;
  }

  return `
    <section class="slide variant-${variant}" data-index="${index}">
      <div class="panel section-panel">
        <p class="eyebrow">SECTION ${index + 1}</p>
        <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
        <p class="subtitle">${escapeHtml(slide.subtitle ?? "")}</p>
        ${slide.points ? `<div class="section-highlights">${slide.points.map(p => `<div class="highlight-item">${escapeHtml(p)}</div>`).join("")}</div>` : ""}
      </div>
    </section>
  `;
}

function renderMetrics(slide, index, variant) {
  if (variant === "tech-hud" || variant === "data-intelligence") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel">
          <div class="metrics-head">
            <div>
              <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
              <p class="subtitle">用卡片化指标快速传递最重要的数据结论。</p>
            </div>
            <div class="badge-pill">LIVE DASHBOARD</div>
          </div>
          <div class="metric-grid metric-grid-compact">
            ${(slide.metrics ?? []).map((item) => `<div class="metric-card metric-hud"><div class="metric-label-top">${escapeHtml(item.label)}</div><div class="metric-value">${escapeHtml(item.value)}</div><div class="metric-spark"></div></div>`).join("")}
          </div>
        </div>
      </section>
    `;
  }

  if (variant === "luxury-black-gold") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel">
          <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
          <div class="metric-grid metric-grid-compact">
            ${(slide.metrics ?? []).map((item) => `<div class="metric-card metric-luxury"><div class="metric-value">${escapeHtml(item.value)}</div><div class="metric-label">${escapeHtml(item.label)}</div></div>`).join("")}
          </div>
        </div>
      </section>
    `;
  }

  return `
    <section class="slide variant-${variant}" data-index="${index}">
      <div class="panel">
        <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
        <div class="metric-grid">
          ${(slide.metrics ?? []).map((item) => `<div class="metric-card"><div class="metric-value">${escapeHtml(item.value)}</div><div class="metric-label">${escapeHtml(item.label)}</div></div>`).join("")}
        </div>
      </div>
    </section>
  `;
}

function renderInsight(slide, index, variant) {
  if (variant === "startup-pitch") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel">
          <div class="pitch-insight">
            <div>
              <p class="eyebrow">INVESTMENT HIGHLIGHT</p>
              <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
              <div class="bullet-list">
                ${(slide.points ?? []).map((point) => `<div class="bullet-item">${escapeHtml(point)}</div>`).join("")}
              </div>
            </div>
            <div class="stat-card pitch-stat">
              <div class="stat-label-top">MOMENTUM</div>
              <div class="stat-value">${escapeHtml(slide.stat ?? "42%")}</div>
              <div class="stat-label">${escapeHtml(slide.statLabel ?? "核心提升")}</div>
              <div class="pitch-tag">FAST EXECUTION</div>
            </div>
          </div>
        </div>
      </section>
    `;
  }

  if (variant === "executive-dark") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel">
          <p class="eyebrow">EXECUTIVE SUMMARY</p>
          <div class="split executive-insight">
            <div>
              <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
              <div class="bullet-list">
                ${(slide.points ?? []).map((point) => `<div class="bullet-item executive-bullet">${escapeHtml(point)}</div>`).join("")}
              </div>
            </div>
            <div class="stat-card executive-stat">
              <div class="stat-value">${escapeHtml(slide.stat ?? "42%")}</div>
              <div class="stat-label">${escapeHtml(slide.statLabel ?? "核心提升")}</div>
            </div>
          </div>
        </div>
      </section>
    `;
  }

  return `
    <section class="slide variant-${variant}" data-index="${index}">
      <div class="panel">
        <div class="split">
          <div>
            <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
            <div class="bullet-list">
              ${(slide.points ?? []).map((point) => `<div class="bullet-item">${escapeHtml(point)}</div>`).join("")}
            </div>
          </div>
          <div class="stat-card">
            <div class="stat-value">${escapeHtml(slide.stat ?? "42%")}</div>
            <div class="stat-label">${escapeHtml(slide.statLabel ?? "核心提升")}</div>
          </div>
        </div>
      </div>
    </section>
  `;
}

function renderTwoColumn(slide, index, variant) {
  if (variant === "editorial-serif") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel">
          <p class="eyebrow">ANALYSIS</p>
          <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
          <div class="columns editorial-columns">
            <div class="column editorial-column">
              <h3>${escapeHtml(slide.leftTitle ?? "左侧模块")}</h3>
              ${(slide.leftPoints ?? []).map((point) => `<p class="editorial-paragraph">${escapeHtml(point)}</p>`).join("")}
            </div>
            <div class="column editorial-column">
              <h3>${escapeHtml(slide.rightTitle ?? "右侧模块")}</h3>
              ${(slide.rightPoints ?? []).map((point) => `<p class="editorial-paragraph">${escapeHtml(point)}</p>`).join("")}
            </div>
          </div>
        </div>
      </section>
    `;
  }

  if (variant === "glass-future") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel">
          <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
          <div class="columns glass-columns">
            <div class="column glass-column">
              <h3>${escapeHtml(slide.leftTitle ?? "左侧模块")}</h3>
              ${(slide.leftPoints ?? []).map((point) => `<div class="bullet-item">${escapeHtml(point)}</div>`).join("")}
            </div>
            <div class="column glass-column">
              <h3>${escapeHtml(slide.rightTitle ?? "右侧模块")}</h3>
              ${(slide.rightPoints ?? []).map((point) => `<div class="bullet-item">${escapeHtml(point)}</div>`).join("")}
            </div>
          </div>
        </div>
      </section>
    `;
  }

  return `
    <section class="slide variant-${variant}" data-index="${index}">
      <div class="panel">
        <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
        <div class="columns">
          <div class="column">
            <h3>${escapeHtml(slide.leftTitle ?? "左侧模块")}</h3>
            ${(slide.leftPoints ?? []).map((point) => `<div class="bullet-item">${escapeHtml(point)}</div>`).join("")}
          </div>
          <div class="column">
            <h3>${escapeHtml(slide.rightTitle ?? "右侧模块")}</h3>
            ${(slide.rightPoints ?? []).map((point) => `<div class="bullet-item">${escapeHtml(point)}</div>`).join("")}
          </div>
        </div>
      </div>
    </section>
  `;
}

function renderChartFocus(slide, index, variant) {
  if (variant === "cyber-grid" || variant === "data-intelligence") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel">
          <div class="metrics-head">
            <div>
              <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
              <p class="subtitle">${escapeHtml(slide.conclusion ?? "")}</p>
            </div>
            <div class="badge-pill">SIGNAL BOARD</div>
          </div>
          <div class="chart-hud">
            ${(slide.chartBars ?? []).map((bar) => `
              <div class="chart-hud-row">
                <div class="chart-hud-label">${escapeHtml(bar.label)}</div>
                <div class="chart-hud-track"><div class="chart-hud-fill" style="width:${bar.value}%"></div></div>
                <div class="chart-hud-value">${escapeHtml(String(bar.value))}</div>
              </div>
            `).join("")}
          </div>
        </div>
      </section>
    `;
  }

  if (variant === "product-launch") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel">
          <p class="eyebrow">PRODUCT SIGNAL</p>
          <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
          <div class="chart-bars launch-chart">
            ${(slide.chartBars ?? []).map((bar) => `
              <div class="chart-row">
                <span>${escapeHtml(bar.label)}</span>
                <div class="bar-track"><div class="bar-fill" style="width:${bar.value}%"></div></div>
                <strong>${escapeHtml(String(bar.value))}</strong>
              </div>
            `).join("")}
          </div>
          <p class="conclusion">${escapeHtml(slide.conclusion ?? "")}</p>
        </div>
      </section>
    `;
  }

  return `
    <section class="slide variant-${variant}" data-index="${index}">
      <div class="panel">
        <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
        <div class="chart-bars">
          ${(slide.chartBars ?? []).map((bar) => `
            <div class="chart-row">
              <span>${escapeHtml(bar.label)}</span>
              <div class="bar-track"><div class="bar-fill" style="width:${bar.value}%"></div></div>
              <strong>${escapeHtml(String(bar.value))}</strong>
            </div>
          `).join("")}
        </div>
        <p class="conclusion">${escapeHtml(slide.conclusion ?? "")}</p>
      </div>
    </section>
  `;
}

function renderTimeline(slide, index, variant) {
  if (variant === "creative-motion") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel">
          <p class="eyebrow">CAMPAIGN FLOW</p>
          <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
          <div class="timeline motion-timeline">
            ${(slide.steps ?? []).map((step, stepIndex) => `<div class="timeline-step motion-step"><span>${stepIndex + 1}</span><strong>${escapeHtml(step)}</strong></div>`).join("")}
          </div>
        </div>
      </section>
    `;
  }

  if (variant === "luxury-black-gold") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel">
          <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
          <div class="timeline luxury-timeline">
            ${(slide.steps ?? []).map((step, stepIndex) => `<div class="timeline-step luxury-step"><span>${stepIndex + 1}</span><strong>${escapeHtml(step)}</strong></div>`).join("")}
          </div>
        </div>
      </section>
    `;
  }

  return `
    <section class="slide variant-${variant}" data-index="${index}">
      <div class="panel">
        <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
        <div class="timeline">
          ${(slide.steps ?? []).map((step, stepIndex) => `<div class="timeline-step"><span>${stepIndex + 1}</span><strong>${escapeHtml(step)}</strong></div>`).join("")}
        </div>
      </div>
    </section>
  `;
}

function renderComparison(slide, index, variant) {
  if (variant === "neo-minimal") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel">
          <p class="eyebrow">COMPARISON</p>
          <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
          <div class="comparison-grid minimal-comparison">
            <div class="comparison-card">
              <h3>${escapeHtml(slide.leftTitle ?? "")}</h3>
              ${(slide.leftPoints ?? []).map((point) => `<div class="comparison-item">${escapeHtml(point)}</div>`).join("")}
            </div>
            <div class="comparison-card comparison-card-featured">
              <h3>${escapeHtml(slide.rightTitle ?? "")}</h3>
              ${(slide.rightPoints ?? []).map((point) => `<div class="comparison-item">${escapeHtml(point)}</div>`).join("")}
            </div>
          </div>
        </div>
      </section>
    `;
  }

  if (variant === "executive-light" || variant === "executive-dark") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel">
          <p class="eyebrow">DECISION FRAME</p>
          <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
          <div class="comparison-grid">
            <div class="comparison-card">
              <h3>${escapeHtml(slide.leftTitle ?? "")}</h3>
              ${(slide.leftPoints ?? []).map((point) => `<div class="comparison-item">${escapeHtml(point)}</div>`).join("")}
            </div>
            <div class="comparison-card comparison-card-featured">
              <h3>${escapeHtml(slide.rightTitle ?? "")}</h3>
              ${(slide.rightPoints ?? []).map((point) => `<div class="comparison-item">${escapeHtml(point)}</div>`).join("")}
            </div>
          </div>
        </div>
      </section>
    `;
  }

  return `
    <section class="slide variant-${variant}" data-index="${index}">
      <div class="panel">
        <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
        <div class="comparison-grid">
          <div class="comparison-card">
            <h3>${escapeHtml(slide.leftTitle ?? "")}</h3>
            ${(slide.leftPoints ?? []).map((point) => `<div class="comparison-item">${escapeHtml(point)}</div>`).join("")}
          </div>
          <div class="comparison-card comparison-card-featured">
            <h3>${escapeHtml(slide.rightTitle ?? "")}</h3>
            ${(slide.rightPoints ?? []).map((point) => `<div class="comparison-item">${escapeHtml(point)}</div>`).join("")}
          </div>
        </div>
      </div>
    </section>
  `;
}

function renderQuote(slide, index, variant) {
  if (variant === "editorial-serif") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel quote-panel editorial-quote">
          <p class="eyebrow">EDITORIAL NOTE</p>
          <blockquote>${escapeHtml(slide.title ?? "")}</blockquote>
          <p class="subtitle">${escapeHtml(slide.subtitle ?? "")}</p>
        </div>
      </section>
    `;
  }

  if (variant === "creative-motion") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel quote-panel motion-quote">
          <div class="quote-burst"></div>
          <blockquote>${escapeHtml(slide.title ?? "")}</blockquote>
          <p class="subtitle">${escapeHtml(slide.subtitle ?? "")}</p>
        </div>
      </section>
    `;
  }

  return `
    <section class="slide variant-${variant}" data-index="${index}">
      <div class="panel quote-panel">
        <blockquote>${escapeHtml(slide.title ?? "")}</blockquote>
        ${slide.subtitle ? `<p class="subtitle">${escapeHtml(slide.subtitle)}</p>` : ""}
      </div>
    </section>
  `;
}

function renderClosing(slide, index, variant) {
  if (variant === "product-launch") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel section-panel closing-hero">
          <p class="eyebrow">THANK YOU</p>
          <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
          <p class="subtitle">${escapeHtml(slide.subtitle ?? "")}</p>
        </div>
      </section>
    `;
  }

  if (variant === "luxury-black-gold") {
    return `
      <section class="slide variant-${variant}" data-index="${index}">
        <div class="panel section-panel luxury-closing">
          <p class="eyebrow">FINAL NOTE</p>
          <div class="gold-divider"></div>
          <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
          <p class="subtitle">${escapeHtml(slide.subtitle ?? "")}</p>
        </div>
      </section>
    `;
  }

  return `
    <section class="slide variant-${variant}" data-index="${index}">
      <div class="panel section-panel">
        <h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>
        ${slide.subtitle ? `<p class="subtitle">${escapeHtml(slide.subtitle)}</p>` : ""}
      </div>
    </section>
  `;
}

function renderLayout(slide, index, variant) {
  const title = `<h2 class="section-title">${escapeHtml(slide.title ?? "")}</h2>`;
  const subtitle = slide.subtitle ? `<p class="subtitle">${escapeHtml(slide.subtitle)}</p>` : "";

  switch (slide.layout) {
    case "cover":
      return renderCover(slide, index, variant);
    case "agenda":
      return renderAgenda(slide, index, variant);
    case "section-break":
      return renderSectionBreak(slide, index, variant);
    case "insight":
      return renderInsight(slide, index, variant);
    case "two-column":
      return renderTwoColumn(slide, index, variant);
    case "metrics":
      return renderMetrics(slide, index, variant);
    case "chart-focus":
      return renderChartFocus(slide, index, variant);
    case "timeline":
      return renderTimeline(slide, index, variant);
    case "comparison":
      return renderComparison(slide, index, variant);
    case "quote":
      return renderQuote(slide, index, variant);
    case "closing":
      return renderClosing(slide, index, variant);
    default:
      return renderClosing(slide, index, variant);
  }
}

function renderHtml({ title, subtitle, themeName, style, slides }) {
  const variant = style.variant ?? "default";
  const slidesHtml = slides.map((slide, index) => renderLayout(slide, index, variant)).join("\n");

  return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${escapeHtml(title)}</title>
  <style>
    :root {
      --bg: ${style.body};
      --panel: ${style.panel};
      --panel-border: ${style.panelBorder};
      --primary: ${style.primary};
      --secondary: ${style.secondary};
      --text: ${style.text};
      --muted: ${style.muted};
      --shadow: ${style.shadow};
      --font-display: ${style.fontDisplay};
      --font-body: ${style.fontBody};
      --radius: 28px;
    }
    * { box-sizing: border-box; }
    html, body { margin: 0; min-height: 100%; background: #020617; color: var(--text); font-family: var(--font-body); }
    body { background: var(--bg); overflow: hidden; }
    .deck { min-height: 100vh; padding: clamp(20px, 2vw, 32px); position: relative; }
    .topbar { position: fixed; inset: 18px 18px auto 18px; display: flex; justify-content: space-between; align-items: center; z-index: 4; color: var(--muted); font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase; }
    .progress { position: fixed; left: 18px; right: 18px; bottom: 18px; height: 4px; border-radius: 999px; background: rgba(255,255,255,0.08); overflow: hidden; z-index: 4; }
    .progress > span { display: block; height: 100%; width: 10%; background: linear-gradient(90deg, var(--primary), var(--secondary)); transition: width 180ms ease; }
    .slides { position: relative; min-height: calc(100vh - 48px); }
    .slide { display: none; min-height: calc(100vh - 48px); align-items: center; justify-content: center; padding: clamp(18px, 2vw, 32px); }
    .slide.active { display: flex; animation: fadeUp 260ms cubic-bezier(0.22, 1, 0.36, 1); }
    .panel { width: min(1200px, 100%); min-height: min(720px, calc(100vh - 120px)); border-radius: var(--radius); border: 1px solid var(--panel-border); background: var(--panel); box-shadow: var(--shadow); padding: clamp(28px, 4vw, 56px); position: relative; overflow: hidden; }
    h1, h2, h3, blockquote { margin: 0; font-family: var(--font-display); }
    h1 { font-size: clamp(2.6rem, 6vw, 5.8rem); line-height: 0.95; max-width: 10ch; }
    h2 { font-size: clamp(2rem, 4vw, 3.8rem); line-height: 1.02; max-width: 14ch; }
    h3 { font-size: clamp(1.05rem, 2vw, 1.4rem); margin-bottom: 14px; }
    p { margin: 0; }
    .eyebrow { color: var(--secondary); font-weight: 700; font-size: 0.8rem; letter-spacing: 0.16em; margin-bottom: 18px; }
    .lead, .subtitle { color: var(--muted); font-size: clamp(1rem, 1.6vw, 1.25rem); margin-top: 18px; max-width: 54ch; line-height: 1.6; }
    .section-title { margin-bottom: 24px; }
    .split, .columns { display: grid; grid-template-columns: 1.3fr 0.9fr; gap: 28px; align-items: stretch; margin-top: 20px; }
    .column, .stat-card, .metric-card, .timeline-step, .agenda-item { border: 1px solid var(--panel-border); border-radius: 22px; background: rgba(255,255,255,0.02); padding: 22px; }
    .bullet-list { display: grid; gap: 12px; margin-top: 18px; }
    .bullet-item { color: var(--text); background: rgba(255,255,255,0.03); border-radius: 16px; padding: 14px 16px; line-height: 1.55; }
    .pitch-insight { display:grid; grid-template-columns: 1.18fr 0.82fr; gap: 28px; }
    .pitch-stat { background: linear-gradient(180deg, rgba(124,58,237,0.22), rgba(34,197,94,0.08)); border-color: rgba(167,139,250,0.35); }
    .pitch-tag { margin-top: 18px; display:inline-flex; padding: 10px 14px; border-radius: 999px; background: rgba(255,255,255,0.08); color: var(--secondary); font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase; }
    .executive-insight { gap: 22px; }
    .executive-bullet { border-left: 3px solid var(--secondary); background: rgba(255,255,255,0.015); }
    .executive-stat { background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.01)); }
    .editorial-columns { gap: 0; border-top: 1px solid var(--panel-border); margin-top: 28px; }
    .editorial-column { border: 0; border-right: 1px solid var(--panel-border); border-radius: 0; background: transparent; padding: 26px 28px 26px 0; }
    .editorial-column:last-child { border-right: 0; padding-left: 28px; padding-right: 0; }
    .editorial-paragraph { color: var(--text); line-height: 1.8; margin-top: 14px; }
    .glass-columns { gap: 20px; }
    .glass-column { background: rgba(255,255,255,0.08); backdrop-filter: blur(20px); }
    .stat-card { display: flex; flex-direction: column; justify-content: center; min-height: 280px; }
    .stat-value { font-size: clamp(3rem, 6vw, 5.5rem); font-weight: 800; color: var(--primary); }
    .stat-label { color: var(--muted); font-size: 1rem; margin-top: 10px; }
    .agenda-list, .metric-grid, .timeline { display: grid; gap: 16px; margin-top: 18px; }
    .metrics-head { display:flex; align-items:flex-end; justify-content:space-between; gap:20px; }
    .badge-pill { padding: 10px 14px; border-radius: 999px; border: 1px solid var(--panel-border); color: var(--secondary); font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase; }
    .agenda-item { display: grid; grid-template-columns: auto 1fr; gap: 16px; align-items: center; }
    .agenda-item span, .timeline-step span { width: 34px; height: 34px; border-radius: 50%; display: inline-grid; place-items: center; background: rgba(255,255,255,0.06); color: var(--primary); font-weight: 700; }
    .executive-agenda-item { background: #fff; }
    .hud-agenda-item { background: rgba(255,255,255,0.025); box-shadow: inset 0 0 0 1px rgba(89,225,255,0.08); }
    .metric-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .metric-value { font-size: clamp(2.2rem, 4vw, 4rem); font-weight: 800; color: var(--primary); }
    .metric-label { margin-top: 8px; color: var(--muted); }
    .metric-label-top { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.12em; margin-bottom: 14px; }
    .metric-grid-compact { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .metric-hud { position: relative; overflow: hidden; }
    .metric-hud::after { content:""; position:absolute; inset:auto -20% -40% auto; width:160px; height:160px; border-radius:50%; background: radial-gradient(circle, rgba(89,225,255,0.16), transparent 65%); }
    .metric-luxury { text-align: center; padding-top: 40px; padding-bottom: 40px; }
    .metric-spark { height: 4px; margin-top: 18px; border-radius: 999px; background: linear-gradient(90deg, var(--primary), transparent); opacity: 0.75; }
    .chart-bars { display: grid; gap: 18px; margin-top: 26px; }
    .chart-hud { display:grid; gap: 16px; margin-top: 28px; }
    .chart-hud-row { display:grid; grid-template-columns: 140px 1fr 56px; gap: 16px; align-items:center; padding: 18px 20px; border-radius: 18px; border: 1px solid var(--panel-border); background: rgba(255,255,255,0.02); }
    .chart-hud-label { color: var(--muted); letter-spacing: 0.06em; text-transform: uppercase; font-size: 12px; }
    .chart-hud-track { height: 12px; border-radius: 999px; background: rgba(255,255,255,0.06); overflow: hidden; }
    .chart-hud-fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--primary), var(--secondary)); box-shadow: 0 0 24px color-mix(in srgb, var(--primary) 55%, transparent); }
    .chart-hud-value { color: var(--text); text-align: right; font-weight: 700; }
    .launch-chart { border-top: 1px solid var(--panel-border); padding-top: 24px; }
    .chart-row { display: grid; grid-template-columns: 120px 1fr 48px; gap: 16px; align-items: center; color: var(--muted); }
    .bar-track { height: 12px; border-radius: 999px; background: rgba(255,255,255,0.07); overflow: hidden; }
    .bar-fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--primary), var(--secondary)); }
    .conclusion { margin-top: 24px; color: var(--text); font-size: 1.05rem; line-height: 1.7; max-width: 60ch; }
    .timeline { grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 28px; }
    .timeline-step { display: grid; gap: 14px; }
    .comparison-grid { display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; margin-top: 28px; }
    .comparison-card { border: 1px solid var(--panel-border); border-radius: 22px; padding: 24px; background: rgba(255,255,255,0.02); }
    .comparison-card-featured { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary) 35%, transparent); background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.02)); }
    .comparison-item { padding: 12px 0; border-top: 1px solid rgba(255,255,255,0.08); line-height: 1.55; }
    .comparison-item:first-of-type { border-top: 0; }
    .minimal-comparison .comparison-card { background: rgba(255,255,255,0.55); }
    .motion-timeline { gap: 18px; }
    .motion-step { background: linear-gradient(180deg, rgba(255,255,255,0.10), rgba(255,255,255,0.02)); transform: rotate(-1deg); }
    .motion-step:nth-child(even) { transform: rotate(1.5deg) translateY(10px); }
    .luxury-timeline { gap: 20px; }
    .luxury-step { text-align:center; background: linear-gradient(180deg, rgba(232,211,161,0.06), rgba(255,255,255,0.01)); border-color: rgba(232,211,161,0.18); }
    .quote-panel { display: flex; flex-direction: column; justify-content: center; }
    .editorial-quote { justify-content: flex-start; padding-top: 72px; }
    .motion-quote { overflow: hidden; }
    .quote-burst { position:absolute; right:-80px; top:-80px; width:260px; height:260px; border-radius:50%; background: radial-gradient(circle, rgba(249,115,22,0.35), transparent 70%); filter: blur(10px); }
    .hud-grid { display:grid; grid-template-columns: 1.3fr 0.7fr; gap: 24px; align-items: end; min-height: 100%; }
    .cover-side-card { border:1px solid var(--panel-border); border-radius: 20px; padding: 24px; background: rgba(255,255,255,0.03); justify-self: end; width: min(320px, 100%); }
    .mini-label, .mini-meta { font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); }
    .mini-value { font-size: clamp(2.2rem, 4vw, 3.4rem); color: var(--primary); font-weight: 800; margin: 14px 0; }
    .luxury-frame::before { content:""; position:absolute; inset:20px; border:1px solid rgba(232,211,161,0.18); border-radius: calc(var(--radius) - 8px); pointer-events:none; }
    .gold-divider { width: 96px; height: 2px; margin: 28px 0 18px; background: linear-gradient(90deg, transparent, var(--primary), transparent); }
    .editorial-cover::after { content:""; position:absolute; right:56px; bottom:48px; width:180px; height:180px; border-radius:50%; border:1px solid var(--panel-border); opacity:0.8; }
    .editorial-kicker { position:absolute; right:56px; bottom:52px; width:180px; color: var(--muted); font-size: 14px; line-height:1.6; }
    blockquote { font-size: clamp(2rem, 4.2vw, 4rem); line-height: 1.16; max-width: 14ch; }
    .section-panel { display: flex; flex-direction: column; justify-content: center; }
    .section-highlights { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-top: 24px; }
    .highlight-item { padding: 16px; background: rgba(255,255,255,0.03); border: 1px solid var(--panel-border); border-radius: 8px; font-size: 14px; }
    .launch-break { background: radial-gradient(circle at top, rgba(45,212,191,0.12), transparent 32%), var(--panel); }
    .luxury-break, .luxury-closing { text-align: center; }
    .closing-hero { background: radial-gradient(circle at top, rgba(45,212,191,0.12), transparent 30%), var(--panel); }
    .kbd-hint { opacity: 0.8; }
    @keyframes fadeUp {
      from { opacity: 0; transform: translateY(18px) scale(0.99); }
      to { opacity: 1; transform: translateY(0) scale(1); }
    }
    @media (max-width: 900px) {
      body { overflow: auto; }
      .slides { display: grid; gap: 16px; padding-top: 60px; }
      .slide, .slide.active { display: block; min-height: auto; }
      .panel { min-height: unset; }
      .split, .columns, .metric-grid, .timeline, .comparison-grid { grid-template-columns: 1fr; }
      .pitch-insight, .chart-hud { grid-template-columns: 1fr; }
      .chart-hud-row { grid-template-columns: 1fr; }
      .metric-grid-compact, .hud-grid { grid-template-columns: 1fr; }
      .chart-row { grid-template-columns: 1fr; }
      .metrics-head { display:grid; }
      .editorial-columns { gap: 18px; border-top: 0; }
      .editorial-column, .editorial-column:last-child { border-right: 0; padding: 0; }
      .editorial-kicker { position: static; width: auto; margin-top: 24px; }
      .topbar, .progress { position: static; margin-bottom: 12px; }
      .progress { height: 6px; }
    }
    ${style.extraCss}
  </style>
</head>
<body>
  <div class="deck">
    <div class="topbar">
      <div>${escapeHtml(title)}</div>
      <div class="kbd-hint">Arrow Keys / Swipe</div>
    </div>
    <div class="slides">
      ${slidesHtml}
    </div>
    <div class="progress"><span id="progress-bar"></span></div>
  </div>
  <script>
    const slides = Array.from(document.querySelectorAll(".slide"));
    const progressBar = document.getElementById("progress-bar");
    let current = 0;

    function updateSlides() {
      const mobile = window.matchMedia("(max-width: 900px)").matches;
      if (mobile) {
        slides.forEach((slide) => slide.classList.add("active"));
        progressBar.style.width = "100%";
        return;
      }

      slides.forEach((slide, index) => slide.classList.toggle("active", index === current));
      const ratio = ((current + 1) / slides.length) * 100;
      progressBar.style.width = ratio + "%";
    }

    function next() { current = Math.min(current + 1, slides.length - 1); updateSlides(); }
    function prev() { current = Math.max(current - 1, 0); updateSlides(); }

    document.addEventListener("keydown", (event) => {
      if (event.key === "ArrowRight" || event.key === "PageDown" || event.key === " ") next();
      if (event.key === "ArrowLeft" || event.key === "PageUp") prev();
    });

    let touchStartX = 0;
    document.addEventListener("touchstart", (event) => {
      touchStartX = event.changedTouches[0].clientX;
    }, { passive: true });
    document.addEventListener("touchend", (event) => {
      const delta = event.changedTouches[0].clientX - touchStartX;
      if (Math.abs(delta) < 50) return;
      if (delta < 0) next();
      if (delta > 0) prev();
    }, { passive: true });

    window.addEventListener("resize", updateSlides);
    updateSlides();
  </script>
</body>
</html>`;
}

const args = parseArgs(process.argv);
const providedContent = loadContent(args);
const effectiveArgs = {
  ...args,
  ...(providedContent ?? {}),
};

const recommendation = recommendTheme(effectiveArgs) ?? recommendTheme({ density: effectiveArgs.density, mobile: effectiveArgs.mobile });
if (!recommendation && !effectiveArgs.theme) {
  console.error("could not select a theme");
  process.exit(1);
}

const slideCount = DEFAULT_DECK_LENGTH[effectiveArgs.density] ?? DEFAULT_DECK_LENGTH.medium;
const forcedTheme = effectiveArgs.theme ? getTheme(effectiveArgs.theme) : null;
if (effectiveArgs.theme && !forcedTheme) {
  console.error(`unknown theme: ${effectiveArgs.theme}`);
  process.exit(1);
}

const themeName = effectiveArgs.theme || recommendation.themeName;
const style = getThemeStyle(themeName);
const slides = normalizeSlides(providedContent?.slides, effectiveArgs.scene, themeName, slideCount);
const html = renderHtml({
  title: effectiveArgs.title,
  subtitle: effectiveArgs.subtitle,
  themeName,
  style,
  slides,
});

const outPath = path.resolve(process.cwd(), effectiveArgs.out);
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, html, "utf8");

console.log(JSON.stringify({
  out: outPath,
  theme: themeName,
  scene: effectiveArgs.scene,
  density: effectiveArgs.density,
  slideCount: slides.length,
  content: effectiveArgs.content || null,
}, null, 2));


path: scripts/generate-theme-gallery.mjs
content: #!/usr/bin/env node

import fs from "node:fs";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { listThemes } from "./theme-bundles.mjs";

const scenes = {
  "cyber-grid": "ai-product-launch",
  "executive-dark": "board-review",
  "executive-light": "consulting-proposal",
  "glass-future": "aigc-solution",
  "data-intelligence": "bi-report",
  "startup-pitch": "investor-pitch",
  "product-launch": "new-product-launch",
  "dev-summit": "tech-talk",
  "luxury-black-gold": "premium-brand-launch",
  "editorial-serif": "whitepaper",
  "neo-minimal": "design-proposal",
  "creative-motion": "campaign-proposal",
};

const outDir = path.resolve(process.cwd(), "dist/gallery");
fs.mkdirSync(outDir, { recursive: true });

const commands = listThemes().map((theme) => {
  const title = theme.replaceAll("-", " ");
  const scene = scenes[theme] ?? "project-report";
  const out = path.join(outDir, `${theme}.html`);
  return { theme, scene, title, out };
});

for (const item of commands) {
  execFileSync(
    process.execPath,
    [
      path.resolve(process.cwd(), "scripts/generate-slide-html.mjs"),
      "--scene", item.scene,
      "--title", item.title,
      "--density", "medium",
      "--out", item.out,
    ],
    { stdio: "inherit" },
  );
}

const manifest = commands.map((item) => ({
  theme: item.theme,
  scene: item.scene,
  title: item.title,
  file: path.relative(process.cwd(), item.out),
}));

fs.writeFileSync(
  path.join(outDir, "manifest.json"),
  JSON.stringify(manifest, null, 2),
  "utf8",
);

const indexHtml = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Slides Theme Gallery</title>
  <style>
    * { box-sizing: border-box; }
    body { margin: 0; font-family: Inter, "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #0b1020, #111827); color: #f8fafc; }
    .wrap { max-width: 1240px; margin: 0 auto; padding: 48px 24px 80px; }
    h1 { margin: 0; font-size: clamp(2.4rem, 5vw, 4.8rem); }
    p { color: #94a3b8; line-height: 1.7; }
    .grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; margin-top: 28px; }
    .card { display: block; text-decoration: none; color: inherit; border-radius: 24px; padding: 22px; border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.04); box-shadow: 0 20px 60px rgba(0,0,0,0.20); }
    .theme { font-size: 1.2rem; font-weight: 700; margin-bottom: 10px; }
    .scene { color: #cbd5e1; font-size: 0.95rem; }
    .link { margin-top: 18px; color: #67e8f9; font-size: 0.9rem; letter-spacing: 0.08em; text-transform: uppercase; }
    @media (max-width: 960px) { .grid { grid-template-columns: 1fr; } }
  </style>
</head>
<body>
  <div class="wrap">
    <h1>Web Slides Theme Gallery</h1>
    <p>12 套旗舰主题的 HTML 样张入口。每个页面都是单文件生成结果，可直接打开评估视觉质量与移动端表现。</p>
    <div class="grid">
      ${manifest.map((item) => `
        <a class="card" href="./${path.basename(item.file)}">
          <div class="theme">${item.theme}</div>
          <div class="scene">${item.scene}</div>
          <div class="link">Open Sample</div>
        </a>
      `).join("")}
    </div>
  </div>
</body>
</html>`;

fs.writeFileSync(path.join(outDir, "index.html"), indexHtml, "utf8");

console.log(JSON.stringify({
  outDir,
  themes: commands.length,
  manifest: path.join(outDir, "manifest.json"),
}, null, 2));


path: scripts/get-theme-bundle.mjs
content: #!/usr/bin/env node

import { findThemeByScene, getTheme, listThemes } from "./theme-bundles.mjs";

function parseArgs(argv) {
  const args = { list: false, theme: "", scene: "" };
  for (let i = 2; i < argv.length; i += 1) {
    const part = argv[i];
    if (part === "--list") args.list = true;
    if (part === "--theme") args.theme = argv[i + 1] ?? "";
    if (part === "--scene") args.scene = argv[i + 1] ?? "";
  }
  return args;
}

const args = parseArgs(process.argv);

if (args.list) {
  console.log(JSON.stringify(listThemes(), null, 2));
  process.exit(0);
}

if (args.theme) {
  const theme = getTheme(args.theme);
  if (!theme) {
    console.error(`unknown theme: ${args.theme}`);
    process.exit(1);
  }
  console.log(JSON.stringify({ [args.theme]: theme }, null, 2));
  process.exit(0);
}

if (args.scene) {
  const theme = findThemeByScene(args.scene);
  if (!theme) {
    console.error(`unknown scene: ${args.scene}`);
    process.exit(1);
  }
  const { name, ...rest } = theme;
  console.log(JSON.stringify({ [name]: rest }, null, 2));
  process.exit(0);
}

console.log("Usage: node scripts/get-theme-bundle.mjs --list | --theme <name> | --scene <scene>");


path: scripts/markdown-to-content.mjs
content: #!/usr/bin/env node

import fs from "node:fs";
import path from "node:path";

const LAYOUT_ALIASES = new Map([
  ["cover", "cover"],
  ["封面", "cover"],
  ["agenda", "agenda"],
  ["目录", "agenda"],
  ["section", "section-break"],
  ["section-break", "section-break"],
  ["章节", "section-break"],
  ["insight", "insight"],
  ["观点", "insight"],
  ["two-column", "two-column"],
  ["双栏", "two-column"],
  ["metrics", "metrics"],
  ["指标", "metrics"],
  ["chart", "chart-focus"],
  ["chart-focus", "chart-focus"],
  ["图表", "chart-focus"],
  ["timeline", "timeline"],
  ["时间线", "timeline"],
  ["comparison", "comparison"],
  ["对比", "comparison"],
  ["quote", "quote"],
  ["引用", "quote"],
  ["closing", "closing"],
  ["结尾", "closing"],
]);

function parseArgs(argv) {
  const args = {
    input: "",
    output: "",
    scene: "",
    density: "medium",
    mobile: false,
    title: "",
    subtitle: "",
  };

  for (let i = 2; i < argv.length; i += 1) {
    const part = argv[i];
    if (part === "--input") args.input = argv[i + 1] ?? args.input;
    if (part === "--output") args.output = argv[i + 1] ?? args.output;
    if (part === "--scene") args.scene = argv[i + 1] ?? args.scene;
    if (part === "--density") args.density = argv[i + 1] ?? args.density;
    if (part === "--mobile") args.mobile = true;
    if (part === "--title") args.title = argv[i + 1] ?? args.title;
    if (part === "--subtitle") args.subtitle = argv[i + 1] ?? args.subtitle;
  }

  if (!args.input) {
    console.error("Usage: node scripts/markdown-to-content.mjs --input outline.md [--output deck.json]");
    process.exit(1);
  }

  return args;
}

function normalizeLayout(raw) {
  if (!raw) return "insight";
  return LAYOUT_ALIASES.get(raw.trim().toLowerCase()) ?? "insight";
}

function cleanText(line) {
  return line.replace(/^\s*[-*]\s*/, "").trim();
}

function parseKeyValue(line) {
  const match = line.match(/^([A-Za-z\u4e00-\u9fa5][^:：]{0,20})[:：]\s*(.+)$/);
  if (!match) return null;
  return { key: match[1].trim().toLowerCase(), value: match[2].trim() };
}

function parseMetricItems(lines) {
  return lines
    .map(cleanText)
    .map((line) => line.match(/^(.+?)[:：]\s*(.+)$/))
    .filter(Boolean)
    .map(([, label, value]) => ({ value: value.trim(), label: label.trim() }));
}

function parseChartItems(lines) {
  return lines
    .map(cleanText)
    .map((line) => line.match(/^(.+?)[:：]\s*(\d+(?:\.\d+)?)$/))
    .filter(Boolean)
    .map(([, label, value]) => ({ label: label.trim(), value: Number(value) }));
}

function parseSection(heading, bodyLines) {
  const rawTitle = heading.replace(/^##\s*/, "").trim();
  const [, maybeLayout, maybeTitle] = rawTitle.match(/^([^:：]+)[:：]?\s*(.*)$/) ?? [];
  const layout = normalizeLayout(maybeLayout);
  const title = maybeTitle || rawTitle;
  const textLines = bodyLines.filter((line) => line.trim().length > 0);
  const bulletLines = textLines.filter((line) => /^\s*[-*]\s+/.test(line));
  const blockQuotes = textLines.filter((line) => /^\s*>\s*/.test(line)).map((line) => line.replace(/^\s*>\s*/, "").trim());
  const subheads = [];
  let currentSubhead = null;

  for (const line of textLines) {
    if (/^###\s+/.test(line)) {
      currentSubhead = { title: line.replace(/^###\s+/, "").trim(), bullets: [] };
      subheads.push(currentSubhead);
      continue;
    }
    if (currentSubhead && /^\s*[-*]\s+/.test(line)) {
      currentSubhead.bullets.push(cleanText(line));
    }
  }

  const kv = new Map();
  for (const line of textLines) {
    const pair = parseKeyValue(line);
    if (pair) kv.set(pair.key, pair.value);
  }

  if (layout === "cover") {
    return {
      layout,
      eyebrow: kv.get("eyebrow") ?? kv.get("眉标") ?? "",
      title: kv.get("title") ?? kv.get("标题") ?? title,
      subtitle: kv.get("subtitle") ?? kv.get("副标题") ?? textLines.find((line) => !line.startsWith("Eyebrow")) ?? "",
    };
  }

  if (layout === "agenda") {
    return { layout, title, points: bulletLines.map(cleanText) };
  }

  if (layout === "section-break") {
    return { layout, title, subtitle: kv.get("subtitle") ?? kv.get("副标题") ?? textLines.find((line) => !/^###|^\s*[-*]\s+/.test(line)) ?? "" };
  }

  if (layout === "insight") {
    return {
      layout,
      title,
      points: bulletLines.map(cleanText),
      stat: kv.get("stat") ?? kv.get("数字") ?? "",
      statLabel: kv.get("statlabel") ?? kv.get("数字说明") ?? "",
    };
  }

  if (layout === "two-column") {
    return {
      layout,
      title,
      leftTitle: subheads[0]?.title ?? "Left",
      leftPoints: subheads[0]?.bullets ?? [],
      rightTitle: subheads[1]?.title ?? "Right",
      rightPoints: subheads[1]?.bullets ?? [],
    };
  }

  if (layout === "metrics") {
    return { layout, title, metrics: parseMetricItems(bulletLines) };
  }

  if (layout === "chart-focus") {
    return {
      layout,
      title,
      chartBars: parseChartItems(bulletLines),
      conclusion: kv.get("conclusion") ?? kv.get("结论") ?? textLines.find((line) => !/^\s*[-*]\s+/.test(line)) ?? "",
    };
  }

  if (layout === "timeline") {
    return { layout, title, steps: bulletLines.map(cleanText) };
  }

  if (layout === "comparison") {
    return {
      layout,
      title,
      leftTitle: subheads[0]?.title ?? "Option A",
      leftPoints: subheads[0]?.bullets ?? [],
      rightTitle: subheads[1]?.title ?? "Option B",
      rightPoints: subheads[1]?.bullets ?? [],
    };
  }

  if (layout === "quote") {
    return {
      layout,
      title: blockQuotes[0] ?? title,
      subtitle: kv.get("author") ?? kv.get("署名") ?? kv.get("subtitle") ?? "",
    };
  }

  if (layout === "closing") {
    return { layout, title: kv.get("title") ?? kv.get("标题") ?? title, subtitle: kv.get("subtitle") ?? kv.get("副标题") ?? "" };
  }

  return {
    layout: "insight",
    title,
    points: bulletLines.map(cleanText),
  };
}

function parseMarkdown(markdown, args) {
  const lines = markdown.split(/\r?\n/);
  const sections = [];
  let title = args.title;
  let subtitle = args.subtitle;
  let currentHeading = null;
  let currentBody = [];

  for (const line of lines) {
    if (/^#\s+/.test(line) && !title) {
      title = line.replace(/^#\s+/, "").trim();
      continue;
    }
    if (!subtitle && /^>\s+/.test(line) && !currentHeading) {
      subtitle = line.replace(/^>\s+/, "").trim();
      continue;
    }
    if (/^##\s+/.test(line)) {
      if (currentHeading) {
        sections.push(parseSection(currentHeading, currentBody));
      }
      currentHeading = line;
      currentBody = [];
      continue;
    }
    if (currentHeading) currentBody.push(line);
  }

  if (currentHeading) {
    sections.push(parseSection(currentHeading, currentBody));
  }

  if (!sections.length || sections[0].layout !== "cover") {
    sections.unshift({
      layout: "cover",
      eyebrow: args.scene || "web-slides",
      title: title || "Untitled Deck",
      subtitle: subtitle || "",
    });
  }

  return {
    title: title || sections[0].title || "Untitled Deck",
    subtitle: subtitle || "",
    scene: args.scene,
    density: args.density,
    mobile: args.mobile,
    slides: sections,
  };
}

const args = parseArgs(process.argv);
const inputPath = path.resolve(process.cwd(), args.input);
const outputPath = path.resolve(process.cwd(), args.output || inputPath.replace(/\.md$/i, ".json"));
const markdown = fs.readFileSync(inputPath, "utf8");
const content = parseMarkdown(markdown, args);

fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(content, null, 2), "utf8");

console.log(JSON.stringify({
  input: inputPath,
  output: outputPath,
  slides: content.slides.length,
  title: content.title,
}, null, 2));


path: scripts/qa-deck.mjs
content: #!/usr/bin/env node

import fs from "node:fs";
import path from "node:path";
import { validateDeckContent } from "./content-utils.mjs";

function parseArgs(argv) {
  const args = { content: "", html: "" };
  for (let i = 2; i < argv.length; i += 1) {
    const part = argv[i];
    if (part === "--content") args.content = argv[i + 1] ?? "";
    if (part === "--html") args.html = argv[i + 1] ?? "";
  }
  if (!args.content && !args.html) {
    console.error("Usage: node scripts/qa-deck.mjs [--content deck.json] [--html deck.html]");
    process.exit(1);
  }
  return args;
}

function measureTitleLength(title = "") {
  return Array.from(title).length;
}

function qaContent(filePath) {
  const raw = fs.readFileSync(filePath, "utf8");
  const content = JSON.parse(raw);
  const errors = validateDeckContent(content);
  const warnings = [];
  const slides = content.slides ?? [];

  if (slides.length < 6) warnings.push("deck has fewer than 6 slides");
  if (slides.length > 24) warnings.push("deck has more than 24 slides");
  if (!slides.some((slide) => slide.layout === "cover")) warnings.push("deck has no cover slide");
  if (!slides.some((slide) => slide.layout === "closing")) warnings.push("deck has no closing slide");

  slides.forEach((slide, index) => {
    if (slide.title && measureTitleLength(slide.title) > 36) {
      warnings.push(`slides[${index}].title is long (${measureTitleLength(slide.title)} chars)`);
    }
    if (Array.isArray(slide.points) && slide.points.length > 5) {
      warnings.push(`slides[${index}] has more than 5 bullet points`);
    }
    if (Array.isArray(slide.leftPoints) && slide.leftPoints.length > 4) {
      warnings.push(`slides[${index}].leftPoints has more than 4 items`);
    }
    if (Array.isArray(slide.rightPoints) && slide.rightPoints.length > 4) {
      warnings.push(`slides[${index}].rightPoints has more than 4 items`);
    }
  });

  return { errors, warnings, slides: slides.length };
}

function qaHtml(filePath) {
  const html = fs.readFileSync(filePath, "utf8");
  const warnings = [];
  const errors = [];

  if (!html.includes('meta name="viewport"')) errors.push("missing viewport meta tag");
  if (!html.includes("@media (max-width: 900px)")) warnings.push("missing mobile media query");
  if (!html.includes("touchstart") || !html.includes("touchend")) warnings.push("missing swipe navigation handlers");
  if (!html.includes("ArrowRight") || !html.includes("ArrowLeft")) warnings.push("missing keyboard navigation handlers");
  if (!html.includes("--primary") || !html.includes("--panel")) warnings.push("missing theme CSS variables");
  if (!html.includes('class="progress"')) warnings.push("missing progress bar");

  return { errors, warnings };
}

const args = parseArgs(process.argv);
const result = { ok: true, content: null, html: null };

if (args.content) {
  result.content = qaContent(path.resolve(process.cwd(), args.content));
  if (result.content.errors.length) result.ok = false;
}

if (args.html) {
  result.html = qaHtml(path.resolve(process.cwd(), args.html));
  if (result.html.errors.length) result.ok = false;
}

console.log(JSON.stringify(result, null, 2));
process.exit(result.ok ? 0 : 1);


path: scripts/recommend-deck.mjs
content: #!/usr/bin/env node

import { DEFAULT_DECK_LENGTH, recommendTheme } from "./theme-bundles.mjs";

function parseArgs(argv) {
  const args = { scene: "", density: "medium", mobile: false };
  for (let i = 2; i < argv.length; i += 1) {
    const part = argv[i];
    if (part === "--scene") args.scene = argv[i + 1] ?? "";
    if (part === "--density") args.density = argv[i + 1] ?? "medium";
    if (part === "--mobile") args.mobile = true;
  }
  return args;
}

const args = parseArgs(process.argv);
const recommendation = recommendTheme(args);

if (!recommendation) {
  console.error("could not recommend a theme");
  process.exit(1);
}

const result = {
  scene: args.scene,
  density: args.density,
  mobile: args.mobile,
  recommendedTheme: recommendation.themeName,
  engine: recommendation.engine,
  tone: recommendation.tone,
  suggestedLayouts: recommendation.layouts,
  suggestedSlideCount: DEFAULT_DECK_LENGTH[args.density] ?? DEFAULT_DECK_LENGTH.medium,
};

console.log(JSON.stringify(result, null, 2));


path: scripts/render-preview.mjs
content: #!/usr/bin/env node

import fs from "node:fs";
import path from "node:path";
import { renderHtmlToPng } from "./chrome-utils.mjs";

function parseArgs(argv) {
  const args = {
    input: "",
    output: "",
    mobile: false,
    fullPage: false,
  };

  for (let i = 2; i < argv.length; i += 1) {
    const part = argv[i];
    if (part === "--input") args.input = argv[i + 1] ?? args.input;
    if (part === "--output") args.output = argv[i + 1] ?? args.output;
    if (part === "--mobile") args.mobile = true;
    if (part === "--full-page") args.fullPage = true;
  }

  if (!args.input) {
    console.error("Usage: node scripts/render-preview.mjs --input deck.html [--output deck.png] [--mobile]");
    process.exit(1);
  }

  return args;
}

const args = parseArgs(process.argv);
const inputPath = path.resolve(process.cwd(), args.input);
const outputPath = path.resolve(process.cwd(), args.output || inputPath.replace(/\.html?$/i, args.mobile ? ".mobile.png" : ".png"));

fs.mkdirSync(path.dirname(outputPath), { recursive: true });

renderHtmlToPng({
  inputUrl: `file://${inputPath}`,
  outputPath,
  width: args.mobile ? 430 : 1600,
  height: args.mobile ? 932 : 900,
  fullPage: args.mobile || args.fullPage,
});

console.log(JSON.stringify({
  input: inputPath,
  output: outputPath,
  mode: args.mobile ? "mobile" : "desktop",
}, null, 2));


path: scripts/scaffold-deck.mjs
content: #!/usr/bin/env node

import { DEFAULT_DECK_LENGTH, recommendTheme } from "./theme-bundles.mjs";

function parseArgs(argv) {
  const args = {
    title: "Untitled Deck",
    scene: "",
    density: "medium",
    mobile: false,
  };

  for (let i = 2; i < argv.length; i += 1) {
    const part = argv[i];
    if (part === "--title") args.title = argv[i + 1] ?? args.title;
    if (part === "--scene") args.scene = argv[i + 1] ?? args.scene;
    if (part === "--density") args.density = argv[i + 1] ?? args.density;
    if (part === "--mobile") args.mobile = true;
  }

  return args;
}

function buildOutline(layouts, slideCount) {
  const seed = layouts.length ? layouts : ["cover", "insight", "closing"];
  const opening = seed[0] ?? "cover";
  const ending = seed[seed.length - 1] ?? "closing";
  const middlePool = seed.slice(1, -1).length ? seed.slice(1, -1) : ["insight", "two-column", "metrics"];

  return Array.from({ length: slideCount }, (_, index) => {
    if (index === 0) {
      return { index: 1, layout: opening, purpose: "opening" };
    }

    if (index === slideCount - 1) {
      return { index: slideCount, layout: ending, purpose: "closing" };
    }

    return {
      index: index + 1,
      layout: middlePool[(index - 1) % middlePool.length],
      purpose: "content",
    };
  });
}

const args = parseArgs(process.argv);
const recommendation = recommendTheme(args);

if (!recommendation) {
  console.error("could not scaffold deck");
  process.exit(1);
}

const slideCount = DEFAULT_DECK_LENGTH[args.density] ?? DEFAULT_DECK_LENGTH.medium;

const scaffold = {
  title: args.title,
  scene: args.scene,
  density: args.density,
  mobile: args.mobile,
  engine: recommendation.engine,
  theme: recommendation.themeName,
  tone: recommendation.tone,
  slideCount,
  layouts: recommendation.layouts,
  outline: buildOutline(recommendation.layouts, slideCount),
};

console.log(JSON.stringify(scaffold, null, 2));


path: scripts/smoke-test.mjs
content: #!/usr/bin/env node

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { listThemes } from "./theme-bundles.mjs";

const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "web-slides-smoke-"));
const repoRoot = process.cwd();

function runNode(script, args) {
  execFileSync(process.execPath, [path.join(repoRoot, script), ...args], {
    stdio: "inherit",
    cwd: repoRoot,
  });
}

runNode("scripts/validate-content.mjs", ["examples/investor-pitch.json"]);
runNode("scripts/markdown-to-content.mjs", [
  "--input",
  "examples/investor-pitch.md",
  "--output",
  path.join(tmpDir, "investor-pitch.json"),
  "--scene",
  "investor-pitch",
  "--mobile",
]);
runNode("scripts/validate-content.mjs", [path.join(tmpDir, "investor-pitch.json")]);
runNode("scripts/generate-slide-html.mjs", [
  "--content",
  path.join(tmpDir, "investor-pitch.json"),
  "--out",
  path.join(tmpDir, "investor-pitch.html"),
]);
runNode("scripts/qa-deck.mjs", [
  "--content",
  path.join(tmpDir, "investor-pitch.json"),
  "--html",
  path.join(tmpDir, "investor-pitch.html"),
]);

for (const theme of listThemes()) {
  runNode("scripts/generate-slide-html.mjs", [
    "--theme",
    theme,
    "--scene",
    theme === "startup-pitch" ? "investor-pitch" : "project-report",
    "--title",
    theme,
    "--out",
    path.join(tmpDir, `${theme}.html`),
  ]);
}

console.log(JSON.stringify({
  ok: true,
  tmpDir,
  themes: listThemes().length,
}, null, 2));


path: scripts/theme-bundles.mjs
content: export const THEMES = {
  "cyber-grid": {
    scenes: ["ai-product-launch", "cloud-platform", "industry-keynote"],
    layouts: ["cover", "section-break", "insight", "metrics", "chart-focus", "closing"],
    tone: "future-tech",
    engine: "reveal",
    density: ["medium"],
    mobileScore: 3,
  },
  "executive-dark": {
    scenes: ["board-review", "strategy-review", "annual-summary"],
    layouts: ["cover", "agenda", "insight", "comparison", "metrics", "closing"],
    tone: "premium-formal",
    engine: "reveal",
    density: ["medium", "heavy"],
    mobileScore: 3,
  },
  "executive-light": {
    scenes: ["consulting-proposal", "client-update", "project-report"],
    layouts: ["cover", "agenda", "two-column", "insight", "chart-focus", "closing"],
    tone: "clean-business",
    engine: "reveal",
    density: ["medium", "heavy"],
    mobileScore: 5,
  },
  "glass-future": {
    scenes: ["aigc-solution", "innovation-lab", "concept-showcase"],
    layouts: ["cover", "section-break", "two-column", "quote", "insight", "closing"],
    tone: "soft-futuristic",
    engine: "reveal",
    density: ["light", "medium"],
    mobileScore: 3,
  },
  "data-intelligence": {
    scenes: ["bi-report", "growth-review", "operations-analysis"],
    layouts: ["cover", "agenda", "metrics", "chart-focus", "timeline", "closing"],
    tone: "data-driven",
    engine: "reveal",
    density: ["medium", "heavy"],
    mobileScore: 5,
  },
  "startup-pitch": {
    scenes: ["investor-pitch", "fundraising", "startup-story"],
    layouts: ["cover", "insight", "two-column", "metrics", "timeline", "closing"],
    tone: "aggressive-growth",
    engine: "reveal",
    density: ["light", "medium"],
    mobileScore: 4,
  },
  "product-launch": {
    scenes: ["new-product-launch", "feature-release", "roadmap-launch"],
    layouts: ["cover", "section-break", "insight", "timeline", "quote", "closing"],
    tone: "stage-product",
    engine: "reveal",
    density: ["light", "medium"],
    mobileScore: 3,
  },
  "dev-summit": {
    scenes: ["tech-talk", "architecture-sharing", "developer-conference"],
    layouts: ["cover", "agenda", "insight", "two-column", "chart-focus", "closing"],
    tone: "engineering-trust",
    engine: "reveal",
    density: ["medium", "heavy"],
    mobileScore: 4,
  },
  "luxury-black-gold": {
    scenes: ["premium-brand-launch", "vip-presentation", "executive-event"],
    layouts: ["cover", "quote", "insight", "metrics", "closing"],
    tone: "luxury-ceremony",
    engine: "reveal",
    density: ["light"],
    mobileScore: 2,
  },
  "editorial-serif": {
    scenes: ["whitepaper", "research-report", "education-deck"],
    layouts: ["cover", "agenda", "insight", "two-column", "quote", "closing"],
    tone: "editorial-knowledge",
    engine: "reveal",
    density: ["medium", "heavy"],
    mobileScore: 3,
  },
  "neo-minimal": {
    scenes: ["design-proposal", "brand-refresh", "minimal-briefing"],
    layouts: ["cover", "insight", "comparison", "two-column", "closing"],
    tone: "minimal-premium",
    engine: "reveal",
    density: ["light", "medium"],
    mobileScore: 5,
  },
  "creative-motion": {
    scenes: ["campaign-proposal", "creative-pitch", "event-showcase"],
    layouts: ["cover", "section-break", "quote", "timeline", "insight", "closing"],
    tone: "energetic-creative",
    engine: "reveal",
    density: ["light", "medium"],
    mobileScore: 2,
  },
};

export const SCENE_ALIASES = {
  "ai-launch": "ai-product-launch",
  "product-launch": "new-product-launch",
  "pitch": "investor-pitch",
  "board": "board-review",
  "report": "project-report",
  "tech-share": "tech-talk",
};

export const DEFAULT_DECK_LENGTH = {
  light: 8,
  medium: 10,
  heavy: 14,
};

export function normalizeScene(scene = "") {
  return SCENE_ALIASES[scene] ?? scene;
}

export function listThemes() {
  return Object.keys(THEMES).sort();
}

export function getTheme(name) {
  return THEMES[name] ?? null;
}

export function findThemeByScene(scene) {
  const normalized = normalizeScene(scene);
  for (const [name, theme] of Object.entries(THEMES)) {
    if (theme.scenes.includes(normalized)) {
      return { name, ...theme };
    }
  }
  return null;
}

export function scoreTheme({ scene, density = "medium", mobile = false, themeName }) {
  const theme = getTheme(themeName);
  if (!theme) return -1;

  let score = 0;
  const normalized = normalizeScene(scene);

  if (normalized && theme.scenes.includes(normalized)) score += 5;
  if (theme.density.includes(density)) score += 2;
  if (mobile) score += theme.mobileScore;

  return score;
}

export function recommendTheme({ scene, density = "medium", mobile = false }) {
  const candidates = listThemes()
    .map((themeName) => ({
      themeName,
      score: scoreTheme({ scene, density, mobile, themeName }),
      ...getTheme(themeName),
    }))
    .sort((a, b) => b.score - a.score);

  return candidates[0] ?? null;
}


path: scripts/theme-styles.mjs
content: export const THEME_STYLES = {
  "cyber-grid": {
    variant: "tech-hud",
    body: "radial-gradient(circle at top, rgba(89,225,255,0.18), transparent 32%), linear-gradient(135deg, #050b14 0%, #07111f 48%, #0b1d34 100%)",
    panel: "rgba(10, 21, 37, 0.82)",
    panelBorder: "rgba(89, 225, 255, 0.22)",
    primary: "#59e1ff",
    secondary: "#7af7c4",
    text: "#eaf4ff",
    muted: "#8aa3bd",
    shadow: "0 30px 80px rgba(0, 0, 0, 0.45)",
    fontDisplay: "'Inter', 'Segoe UI', sans-serif",
    fontBody: "'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif",
    extraCss: `
      .deck::before { content: ""; position: fixed; inset: 0; background-image:
        linear-gradient(rgba(89,225,255,0.08) 1px, transparent 1px),
        linear-gradient(90deg, rgba(89,225,255,0.08) 1px, transparent 1px);
        background-size: 60px 60px; pointer-events: none; mask-image: radial-gradient(circle at center, black 45%, transparent 88%); }
      .cover-accent { box-shadow: 0 0 0 1px rgba(89,225,255,0.22), 0 0 40px rgba(89,225,255,0.16) inset; }
    `,
  },
  "executive-dark": {
    variant: "executive-dark",
    body: "linear-gradient(135deg, #0f1114 0%, #181b20 100%)",
    panel: "rgba(25, 29, 34, 0.88)",
    panelBorder: "rgba(255,255,255,0.08)",
    primary: "#f4f4f5",
    secondary: "#c7a86a",
    text: "#fafafa",
    muted: "#a1a1aa",
    shadow: "0 25px 70px rgba(0, 0, 0, 0.35)",
    fontDisplay: "'Inter', 'Segoe UI', sans-serif",
    fontBody: "'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif",
    extraCss: "",
  },
  "executive-light": {
    variant: "executive-light",
    body: "linear-gradient(180deg, #eef2f8 0%, #f8fafc 100%)",
    panel: "rgba(255,255,255,0.92)",
    panelBorder: "rgba(31,60,136,0.12)",
    primary: "#1f3c88",
    secondary: "#22c1c3",
    text: "#0f172a",
    muted: "#475569",
    shadow: "0 20px 60px rgba(15, 23, 42, 0.10)",
    fontDisplay: "'Inter', 'Segoe UI', sans-serif",
    fontBody: "'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif",
    extraCss: "",
  },
  "glass-future": {
    variant: "glass-future",
    body: "linear-gradient(135deg, #08131f 0%, #1f2f56 55%, #3b2e68 100%)",
    panel: "rgba(255,255,255,0.10)",
    panelBorder: "rgba(255,255,255,0.18)",
    primary: "#8bdcff",
    secondary: "#b69cff",
    text: "#f3f8ff",
    muted: "#c4d3e6",
    shadow: "0 30px 80px rgba(0, 0, 0, 0.35)",
    fontDisplay: "'Inter', 'Segoe UI', sans-serif",
    fontBody: "'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif",
    extraCss: ".panel{backdrop-filter: blur(18px);}",
  },
  "data-intelligence": {
    variant: "data-intelligence",
    body: "linear-gradient(135deg, #0b1220 0%, #111b32 100%)",
    panel: "rgba(18, 26, 43, 0.92)",
    panelBorder: "rgba(106,169,255,0.16)",
    primary: "#6aa9ff",
    secondary: "#4be3c2",
    text: "#e5edf9",
    muted: "#93a7c4",
    shadow: "0 24px 64px rgba(0, 0, 0, 0.28)",
    fontDisplay: "'Inter', 'Segoe UI', sans-serif",
    fontBody: "'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif",
    extraCss: ".metric-card{background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.00));}",
  },
  "startup-pitch": {
    variant: "startup-pitch",
    body: "linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%)",
    panel: "rgba(15, 23, 42, 0.86)",
    panelBorder: "rgba(124,58,237,0.24)",
    primary: "#a78bfa",
    secondary: "#22c55e",
    text: "#eef2ff",
    muted: "#cbd5e1",
    shadow: "0 30px 90px rgba(15, 23, 42, 0.32)",
    fontDisplay: "'Inter', 'Segoe UI', sans-serif",
    fontBody: "'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif",
    extraCss: "",
  },
  "product-launch": {
    variant: "product-launch",
    body: "radial-gradient(circle at top, rgba(45,212,191,0.14), transparent 28%), linear-gradient(180deg, #05070b 0%, #0b1119 100%)",
    panel: "rgba(12, 18, 28, 0.86)",
    panelBorder: "rgba(255,255,255,0.10)",
    primary: "#ffffff",
    secondary: "#2dd4bf",
    text: "#f8fafc",
    muted: "#94a3b8",
    shadow: "0 28px 70px rgba(0, 0, 0, 0.35)",
    fontDisplay: "'Inter', 'Segoe UI', sans-serif",
    fontBody: "'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif",
    extraCss: "",
  },
  "dev-summit": {
    variant: "dev-summit",
    body: "linear-gradient(135deg, #0b1020 0%, #111827 100%)",
    panel: "rgba(17, 24, 39, 0.90)",
    panelBorder: "rgba(125,211,252,0.16)",
    primary: "#7dd3fc",
    secondary: "#a3e635",
    text: "#e5efff",
    muted: "#9fb0c8",
    shadow: "0 24px 70px rgba(0, 0, 0, 0.32)",
    fontDisplay: "'Inter', 'Segoe UI', sans-serif",
    fontBody: "'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif",
    extraCss: "",
  },
  "luxury-black-gold": {
    variant: "luxury-black-gold",
    body: "linear-gradient(180deg, #070707 0%, #14110d 100%)",
    panel: "rgba(20, 20, 20, 0.88)",
    panelBorder: "rgba(232,211,161,0.18)",
    primary: "#f3deb0",
    secondary: "#f7ead0",
    text: "#fcf6ec",
    muted: "#cfb98a",
    shadow: "0 25px 80px rgba(0, 0, 0, 0.45)",
    fontDisplay: "'Cormorant Garamond', 'Times New Roman', serif",
    fontBody: "'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif",
    extraCss: ".eyebrow{letter-spacing:0.28em;text-transform:uppercase;}",
  },
  "editorial-serif": {
    variant: "editorial-serif",
    body: "linear-gradient(180deg, #f2efe8 0%, #ede7dd 100%)",
    panel: "rgba(251,250,247,0.92)",
    panelBorder: "rgba(47,58,77,0.10)",
    primary: "#2f3a4d",
    secondary: "#8b5e3c",
    text: "#202733",
    muted: "#6b7280",
    shadow: "0 18px 50px rgba(47, 58, 77, 0.12)",
    fontDisplay: "'Cormorant Garamond', 'Times New Roman', serif",
    fontBody: "'Source Serif 4', 'PingFang SC', 'Microsoft YaHei', serif",
    extraCss: "",
  },
  "neo-minimal": {
    variant: "neo-minimal",
    body: "linear-gradient(180deg, #f8f8f6 0%, #f1f1ee 100%)",
    panel: "rgba(255,255,255,0.86)",
    panelBorder: "rgba(17,17,17,0.08)",
    primary: "#111111",
    secondary: "#b6b09f",
    text: "#111827",
    muted: "#6b7280",
    shadow: "0 18px 48px rgba(17, 17, 17, 0.08)",
    fontDisplay: "'Inter', 'Segoe UI', sans-serif",
    fontBody: "'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif",
    extraCss: "",
  },
  "creative-motion": {
    variant: "creative-motion",
    body: "linear-gradient(135deg, #0f172a 0%, #1d4ed8 65%, #0ea5e9 100%)",
    panel: "rgba(255,255,255,0.10)",
    panelBorder: "rgba(255,255,255,0.18)",
    primary: "#f97316",
    secondary: "#22d3ee",
    text: "#f8fafc",
    muted: "#dbeafe",
    shadow: "0 28px 80px rgba(15, 23, 42, 0.35)",
    fontDisplay: "'Inter', 'Segoe UI', sans-serif",
    fontBody: "'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif",
    extraCss: ".section-title{transform:rotate(-2deg);display:inline-block;}",
  },
};

export function getThemeStyle(themeName) {
  return THEME_STYLES[themeName] ?? THEME_STYLES["executive-light"];
}


path: scripts/validate-content.mjs
content: #!/usr/bin/env node

import fs from "node:fs";
import path from "node:path";
import { validateDeckContent } from "./content-utils.mjs";

const input = process.argv[2];

if (!input) {
  console.error("Usage: node scripts/validate-content.mjs <path/to/deck.json>");
  process.exit(1);
}

const filePath = path.resolve(process.cwd(), input);
const raw = fs.readFileSync(filePath, "utf8");
const content = JSON.parse(raw);
const errors = validateDeckContent(content);

if (errors.length) {
  console.error(JSON.stringify({ ok: false, errors }, null, 2));
  process.exit(1);
}

console.log(JSON.stringify({ ok: true, file: filePath, slides: content.slides?.length ?? 0 }, null, 2));


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