# PPTX SDK — LLM Reference

You are generating JSON that will be converted into PowerPoint (.pptx) presentations. Follow this reference exactly. Every property name, nesting level, and type matters.

---

## Deck Structure

```json
{
  "style": "corporate",
  "slideSize": { "width": 13.333, "height": 7.5 },
  "slides": [ ... ]
}
```

| Property | Type | Default | Description |
|---|---|---|---|
| `style` | `string` or `object` | `"corporate"` | Style preset name or inline style object |
| `slideSize` | `{ width, height }` | `{ 10, 7.5 }` | Slide dimensions in inches |
| `slides` | `array` | required | Array of slide definitions |

### Style Presets

| Name | Look | Best For |
|---|---|---|
| `"corporate"` | White bg, dark text, blue accent (#4472C4), Arial/Calibri | Investment banking, finance, formal presentations |
| `"minimal"` | White bg, blue accent (#2980b9), Helvetica Neue | Clean tech pitches, product decks |
| `"dark"` | Dark bg (#1a1a2e), red accent (#E94560), light text | Executive keynotes, dramatic impact |
| `"warm"` | White bg, terracotta accent (#D57F5B), Georgia/Garamond | Creative briefs, editorial presentations |

---

## Slide Type

All slides use the grid type with an inline body layout tree.

```json
{
  "type": "grid",
  "title": "Slide Title",
  "subtitle": "Optional subtitle below the title",
  "sectionLabel": "SECTION NAME",
  "footer": "Source: Company data",
  "margin": 0.5,
  "body": { ... }
}
```

| Property | Type | Required | Description |
|---|---|---|---|
| `title` | `string` | no | Main heading at top |
| `subtitle` | `string` | no | Subtitle text below the title |
| `sectionLabel` | `string` | no | Eyebrow text above title (e.g. section name) |
| `footer` | `string` | no | Footnote at bottom |
| `margin` | `number` | no | Slide-level margin override (inches) |
| `_task` | `string` | no | Task annotation for AI agents. Not rendered or exported. Agent should clear after completing. |
| `body` | `object` | yes | Layout tree (see Grid Layout below) |

---

## Grid Layout System

The layout engine uses a 12-column nested grid (like Bootstrap/MUI). Every node is either a **leaf** (has `content`) or a **container** (has `children`).

### Layout Node

```json
{
  "span": 6,
  "direction": "row",
  "gap": 0.15,
  "padding": 0.1,
  "margin": 0.05,
  "marginTop": 0.1,
  "background": "#F5F0EB",
  "backgroundOpacity": 80,
  "border": "#999999",
  "borderTop": { "color": "#185ABD", "width": 2, "style": "solid" },
  "shadow": { "color": "#000000", "alpha": 0.5, "blur": 4, "dist": 3, "dir": 45 },
  "header": "Panel Header",
  "subheader": "Sub-section",
  "subfooter": "Source note",
  "childShape": "chevron",
  "childShapeColor": "#185ABD",
  "children": [ ... ],
  "content": { ... }
}
```

| Property | Type | Default | Description |
|---|---|---|---|
| `span` | `number` (1-12) | equal split | Column weight in parent's 12-column grid |
| `direction` | `"row"` or `"col"` | `"row"` | Stack children horizontally or vertically |
| `gap` | `number` | 0.15 | Gap between children (inches) |
| `padding` | `number` | 0 | Inner padding on all sides (inches) |
| `paddingTop` | `number` | — | Top padding override (inches). Takes precedence over `padding`. |
| `paddingRight` | `number` | — | Right padding override (inches) |
| `paddingBottom` | `number` | — | Bottom padding override (inches) |
| `paddingLeft` | `number` | — | Left padding override (inches) |
| `margin` | `number` | 0 | Inner margin on all sides (inches) |
| `marginTop` | `number` | — | Top margin override (inches). Takes precedence over `margin`. |
| `marginRight` | `number` | — | Right margin override (inches) |
| `marginBottom` | `number` | — | Bottom margin override (inches) |
| `marginLeft` | `number` | — | Left margin override (inches) |
| `background` | `string` | none | Fill color for this zone (hex) |
| `backgroundOpacity` | `number` | 100 | Background fill opacity (0-100) |
| `border` | `string` or `object` | none | Border color (string) or object (see below). Applies to all sides. |
| `borderTop` | `string` or `object` | — | Top border override. Takes precedence over `border`. |
| `borderRight` | `string` or `object` | — | Right border override |
| `borderBottom` | `string` or `object` | — | Bottom border override |
| `borderLeft` | `string` or `object` | — | Left border override |
| `shadow` | `object` | none | Drop shadow (see below) |
| `header` | `string` | none | Dark bar with white text above content |
| `subheader` | `string` | none | Smaller text label with underline above content |
| `subfooter` | `string` | none | Small italic text below content |
| `childShape` | `string` | none | Decorative shape for child zones: `"chevron"` or `"pyramid"` |
| `childShapeColor` | `string` | none | Color (hex) for the child shape design |
| `children` | `array` | — | Child layout nodes (container mode) |
| `content` | `object` | — | Content object (leaf mode) |
| `_task` | `string` | none | Task annotation for AI agents. Not rendered or exported. Agent should clear after completing. |
| `factCheck` | `object` | none | Fact-check result written by agents after verifying zone content. See **factCheck object** below. |
| `_sources` | `array` | none | Data provenance. Array of source objects citing where the zone's data came from. See **_sources array** below. |

**factCheck object:**
```json
{ "claim": "Shopify 2025E Revenue ~$9B", "verdict": "Confirmed: Shopify FY2025 revenue was $8.9B per 10-K", "status": "confirmed", "checkTimestamp": "2026-02-28T12:00:00Z", "checker": "serpapi" }
```
| Property | Type | Required | Description |
|---|---|---|---|
| `claim` | `string` | yes | The verifiable claim being checked |
| `verdict` | `string` | yes | Explanation of the check result with source |
| `status` | `string` | yes | `"confirmed"`, `"unverified"`, or `"inaccurate"` |
| `checkTimestamp` | `string` | no | ISO 8601 date of when the check was performed |
| `checker` | `string` | no | Who/what performed the check (e.g. `"serpapi"`, `"manual"`) |

**_sources array:**
```json
[{ "url": "https://financialmodelingprep.com/api/v3/income-statement/SHOP", "api": "FMP", "label": "Revenue & margins", "date": "2026-03-01" }]
```
| Property | Type | Required | Description |
|---|---|---|---|
| `url` | `string` | no | URL of the web page, API endpoint, or document the data came from |
| `api` | `string` | no | API service name (e.g. `"FMP"`, `"World Bank"`, `"SEC EDGAR"`) |
| `label` | `string` | no | Short human-readable description of what data came from this source |
| `date` | `string` | no | ISO date when the data was retrieved |

**Border object form:**
```json
{ "color": "#185ABD", "width": 2, "style": "solid" }
```
| Property | Type | Default | Description |
|---|---|---|---|
| `color` | `string` | required | Border color (hex) |
| `width` | `number` | 1 | Border width in points |
| `style` | `"solid"`, `"dash"`, `"dot"`, `"dashDot"`, `"lgDash"`, `"lgDashDot"` | `"solid"` | Border line style |

**Shadow object:**
```json
{ "color": "#000000", "alpha": 0.5, "blur": 4, "dist": 3, "dir": 45, "size": 100 }
```
| Property | Type | Default | Description |
|---|---|---|---|
| `color` | `string` | `"#000000"` | Shadow color (hex) |
| `alpha` | `number` | 0.5 | Opacity (0-1) |
| `blur` | `number` | 4 | Blur radius (points) |
| `dist` | `number` | 3 | Distance from shape (points) |
| `dir` | `number` | 45 | Direction in degrees (0-360) |
| `size` | `number` | 100 | Shadow size (percentage) |

**Rules:**
- A node has EITHER `children` OR `content`, never both.
- `span` values in sibling nodes should sum to 12 for balanced layouts, but the engine handles any sum.
- Nesting depth is unlimited. Use `direction: "col"` for vertical stacking and `direction: "row"` for horizontal.

### Common Grid Patterns

**Two equal columns:**
```json
{ "direction": "row", "children": [{ "span": 6, "content": ... }, { "span": 6, "content": ... }] }
```

**Sidebar layout (wide left, narrow right):**
```json
{ "direction": "row", "children": [{ "span": 8, "content": ... }, { "span": 4, "content": ... }] }
```

**Top bar + main content:**
```json
{ "direction": "col", "children": [{ "span": 3, "content": ... }, { "span": 9, "content": ... }] }
```

**KPI row + two-panel bottom:**
```json
{
  "direction": "col", "children": [
    { "span": 3, "direction": "row", "children": [
      { "content": ... }, { "content": ... }, { "content": ... }
    ]},
    { "span": 9, "direction": "row", "children": [
      { "span": 8, "content": ... }, { "span": 4, "content": ... }
    ]}
  ]
}
```

---

## Content Types

Every leaf node's `content` must have a `type` field. Here are all supported content types:

### `text`

Plain or rich text block. Use for paragraphs, subtitles, large numbers, labels.

```json
{
  "type": "text",
  "text": "Your text here",
  "font": "Arial",
  "fontSize": 1400,
  "color": "#333333",
  "bold": true,
  "italic": false,
  "underline": false,
  "align": "l",
  "anchor": "t",
  "lineSpacing": 1.2
}
```

| Property | Type | Default | Description |
|---|---|---|---|
| `text` | `string` | — | Plain text content (supports `\n` for line breaks). Used when `runs` is absent. |
| `runs` | `TextRun[]` | — | Rich text runs (takes precedence over `text`). See Rich Text below. |
| `font` | `string` | style default | Font family |
| `fontSize` | `number` | style default | Font size in hundredths of a point (1400 = 14pt) |
| `color` | `string` | style default | Hex color |
| `bold` | `boolean` | `false` | Bold text |
| `italic` | `boolean` | `false` | Italic text |
| `underline` | `boolean` | `false` | Underlined text |
| `align` | `"l"`, `"ctr"`, `"r"` | `"l"` | Horizontal alignment |
| `anchor` | `"t"`, `"ctr"`, `"b"` | `"t"` | Vertical alignment within bounds |
| `lineSpacing` | `number` | 1.0 | Line spacing multiplier (1.0 = single, 1.5 = 1.5x, 0.8 = tight) |
| `paddingTop` | `number` | 0.05 | Text inset from top edge (inches) |
| `paddingRight` | `number` | 0.1 | Text inset from right edge (inches) |
| `paddingBottom` | `number` | 0.05 | Text inset from bottom edge (inches) |
| `paddingLeft` | `number` | 0.1 | Text inset from left edge (inches) |
| `bulletStyle` | `object` | — | Per-zone bullet formatting overrides (see below) |

**bulletStyle object** — overrides the global style defaults for bullet formatting within this text zone:

| Property | Type | Default | Description |
|---|---|---|---|
| `char` | `string` | `"•"` | Bullet character (e.g. `"•"`, `"–"`, `"▸"`, `"■"`, `"○"`, `"✓"`) |
| `color` | `string` | style accent | Hex color for the bullet character |
| `indent` | `number` | 0.375 | Hanging indent in inches |
| `marginLeft` | `number` | 0.375 | Left margin in inches |
| `spaceBefore` | `number` | 0 | Space between bullet items in points |

#### Rich Text Runs

For mixed formatting within a single text block, use `runs` instead of `text`. Runs are split into paragraphs by `\n` characters. Each paragraph can be independently bulleted.

```json
{
  "type": "text",
  "runs": [
    { "text": "Revenue grew ", "fontSize": 1200 },
    { "text": "42%", "bold": true, "color": "#16a34a", "fontSize": 1200 },
    { "text": " year-over-year.", "fontSize": 1200 }
  ]
}
```

| Run Property | Type | Default | Description |
|---|---|---|---|
| `text` | `string` | required | The text content of this run. Use `\n` to separate paragraphs. |
| `bold` | `boolean` | `false` | Bold |
| `italic` | `boolean` | `false` | Italic |
| `underline` | `boolean` | `false` | Underlined |
| `color` | `string` | inherit | Hex color |
| `fontSize` | `number` | inherit | Font size (hundredths-pt) |
| `font` | `string` | inherit | Font family |
| `bullet` | `boolean` | `false` | When true on the first run of a paragraph, that paragraph is bulleted (•) |
| `bulletLevel` | `number` | 0 | Nesting level. `0` = top-level bullet (•), `1` = sub-bullet (–). Only meaningful when `bullet: true`. |

#### Bullet Lists with Rich Text

To create bullet lists within a text zone, set `bullet: true` on the first run of each bulleted paragraph. Separate paragraphs with `\n`. For nested (sub-)bullets, set `bulletLevel: 1` — these render with an en-dash (–) and increased indent.

```json
{
  "type": "text",
  "runs": [
    { "text": "First top-level point", "bullet": true },
    { "text": "\n" },
    { "text": "Sub-point under first", "bullet": true, "bulletLevel": 1 },
    { "text": "\n" },
    { "text": "Another sub-point", "bullet": true, "bulletLevel": 1 },
    { "text": "\n" },
    { "text": "Second top-level point", "bullet": true },
    { "text": "\n" },
    { "text": "Third point with ", "bullet": true },
    { "text": "bold emphasis", "bold": true },
    { "text": " in the middle" }
  ]
}
```

This renders as:
- • First top-level point
-   – Sub-point under first
-   – Another sub-point
- • Second top-level point
- • Third point with **bold emphasis** in the middle

**Key rules for rich text bullets:**
- `bullet: true` must be on the **first run** after a `\n` separator (the run that starts the paragraph)
- Only the first run of a paragraph needs `bullet` and `bulletLevel` — subsequent runs in the same paragraph inherit the bullet status
- `bulletLevel` only applies when `bullet: true`. Level 0 (default) = `•`, level 1 = `–`
- To customize the bullet character/color for the whole zone, use `bulletStyle` on the `TextContent`

**When to use:** Section subtitles, large section numbers, descriptive paragraphs, labels, any free-form text. Use `runs` when you need mixed bold/color/size within a paragraph, or bullet lists with rich formatting.


**When to use:** Key takeaways, feature lists, supporting arguments, analysis points. The most common content type for text-heavy slides.

### `cardGrid`

Bordered cards with title + detail lines. Good for entity profiles, product features.

```json
{
  "type": "cardGrid",
  "items": [
    { "title": "Cloud Platform", "lines": ["AWS-based infrastructure", "99.9% uptime SLA"] },
    { "title": "Mobile SDK", "lines": ["iOS & Android", "React Native support"] }
  ]
}
```

| Property | Type | Description |
|---|---|---|
| `items[].title` | `string` | Card heading (bold, accent color) |
| `items[].lines` | `string[]` | Detail lines below title |

**When to use:** Feature comparisons, product capabilities, service offerings. Each card gets a rounded border.

### `table`

Data table with header row and body rows. Auto-styles with alternating row colors.

**IMPORTANT:** Table fields MUST be nested inside a `"data"` key. Placing `rows`/`headers` directly on the content spec is invalid and will be rejected.

```json
{
  "type": "table",
  "data": {
    "headers": ["Company", "Revenue", "Growth", "Margin"],
    "rows": [
      ["Acme Corp", "$520M", "15%", "42%"],
      ["Globex", "$380M", "22%", "38%"],
      ["Initech", "$290M", "8%", "45%"]
    ],
    "colWidths": [4, 3, 2, 3],
    "rowHeight": 0.4,
    "headerHeight": 0.35,
    "align": { "1": "ctr", "2": "ctr", "3": "ctr" }
  }
}
```

| Property | Type | Default | Description |
|---|---|---|---|
| `data.headers` | `string[]` | — | Column headers (styled with accent fill). **Must be plain strings** — image objects are NOT allowed in headers. Optional: omit for headerless tables. |
| `data.rows` | `(string\|object)[][]` | required | 2D array of cell values. Each cell is a plain string or an image object (see below). |
| `data.colWidths` | `number[]` | equal | 12-column grid spans for column proportions. **Must sum to 12.** E.g. `[4, 3, 2, 3]` for 4 columns. |
| `data.rowHeight` | `number` | 0.4 | Body row height in inches. All body rows share this height. |
| `data.headerHeight` | `number` | 0.35 | Header row height in inches. |
| `data.fontSize` | `number` | style default | Body font size override (hundredths-pt, e.g. 1100 = 11pt) |
| `data.headerFontSize` | `number` | style default | Header font size override (hundredths-pt) |
| `data.align` | `object` | — | Per-column alignment overrides. Keys are 0-based column indices: `{ "0": "l", "1": "ctr", "2": "r" }` |
| `data.colAlign` | `array` | — | Array shorthand for align: `["l", "ctr", "ctr", "r"]`. Normalized to `align` internally. |
| `data.overflowWrap` | `"normal"`, `"anywhere"`, `"break-word"` | `"normal"` | CSS overflow-wrap for cell text. Use `"break-word"` for long URLs or strings without spaces. |

**Table cell images:** Use `{ "type": "image", "source": "search", "searchTerm": "CompanyName logo" }` in any row cell that should show a company/investor logo. In the PPTX export, the search term text appears in the cell and the resolved logo image is placed in the pasteboard area (to the left of the slide) for easy drag-and-drop positioning.

**When to use:** Financial comparisons, peer analysis, data summaries. Best for structured data with 3-8 columns. Use image cells for company logos in transaction tables, competitive landscapes, etc.

### `profile`

Single person/entity profile with name + bullet items. Used inside card layouts.

```json
{
  "type": "profile",
  "name": "Jane Smith",
  "items": ["20+ years experience", "Former Partner at McKinsey", "Harvard MBA"]
}
```

**When to use:** Inside profile card templates. Rarely used directly — prefer the `profileCards` template for elaborate layouts.

### `image`

Embedded image with configurable fit behavior. Images can be sourced via search (logos), AI generation (illustrations), file path, or raw buffer.

```json
{ "type": "image", "source": "search", "searchTerm": "Salesforce logo", "objectFit": "contain" }
```
```json
{ "type": "image", "source": "ai", "prompt": "abstract blue geometric pattern", "objectFit": "cover" }
```
```json
{ "type": "image", "path": "images/chart.png", "width": "100%", "height": "100%" }
```

| Property | Type | Default | Description |
|---|---|---|---|
| `source` | `"search"`, `"ai"`, `"upload"` | — | Image source. `"search"` uses web image search, `"ai"` uses AI generation, `"upload"` for user-uploaded images. |
| `searchTerm` | `string` | — | Search query (when `source: "search"`). E.g. `"Acme Corp logo"`. |
| `prompt` | `string` | — | Generation prompt (when `source: "ai"`). E.g. `"modern office building exterior"`. |
| `url` | `string` | — | Direct image URL. Set automatically after search/generation resolves. |
| `path` | `string` | — | File path (resolved relative to JSON file). Use for JSON-to-PPTX converter. |
| `buffer` | `Buffer` | — | Raw image data. Use for programmatic API. |
| `objectFit` | `"contain"`, `"cover"`, `"fill"` | `"cover"` | **`"contain"`**: fits entire image within container, no cropping (best for logos). **`"cover"`**: fills container, may crop edges (best for background/decorative images). **`"fill"`**: stretches to fill container exactly (may distort aspect ratio). |
| `imagePadding` | `number` | 0 | Inset in inches. Shrinks the image from the zone edges before applying fit calculation. Use for adding breathing room around logos. |
| `anchor` | `"t"`, `"ctr"`, `"b"` | `"ctr"` | Vertical alignment. In contain mode: where image sits within container. In cover mode: which part stays visible when cropping. |
| `width` | `number` or `"N%"` | — | Pixel width or percentage of container |
| `height` | `number` or `"N%"` | — | Pixel height or percentage of container |
| `crop` | `object` | — | Crop percentages: `{ "l": 0, "t": 0, "r": 0, "b": 0 }`. Each value is 0-100 representing % to crop from that side. |
| `ext` | `string` | `"png"` | Image extension |

**Image placeholders:** When `source` is set but `url` is not, the image is an unresolved placeholder. These are resolved via the "Process Images" feature, which batch-searches or generates all placeholders in one pass.

**When to use:** Logos (use `source: "search"` with `objectFit: "contain"`), illustrations (use `source: "ai"`), pre-made images, maps, screenshots. For data charts, prefer `type: "chart"` instead (see below).

### `chart`

Inline chart generated at the exact container dimensions. The chart engine renders at the cell's pixel size, so no stretching or squashing occurs.

```json
{ "type": "chart", "chartType": "bar", "title": "Revenue by Year",
  "categories": ["2022", "2023", "2024"], "series": [{ "name": "Rev", "data": [520, 640, 780] }] }
```

Properties vary by `chartType` — see the **Chart Types** section below.

**When to use:** Any data visualization. Always prefer `type: "chart"` over `type: "image"` for charts, because charts are generated at the exact container size and look crisp at any layout position.

### `line`

Horizontal divider line. Used for visual separation.

```json
{ "type": "line", "color": "#999999", "width": 9525 }
```

| Property | Type | Default | Description |
|---|---|---|---|
| `color` | `string` | style primary text color | Line color |
| `width` | `number` | 9525 | Line width in EMU (9525 = 0.75pt) |

**When to use:** Separating sections within a zone. Rarely needed — use `subheader` on layout nodes instead for most cases.

### `timeline`

Timeline visualization showing events along a horizontal axis with connector lines.

```json
{
  "type": "timeline",
  "title": "Company History",
  "events": [
    { "date": "2020-03-15", "name": "Founded", "detail": "Company incorporated in Delaware" },
    { "date": "2021-06-01", "name": "Series A", "detail": "Raised $15M from Sequoia" },
    { "date": "2023-01-10", "name": "Series B", "detail": "Raised $50M at $400M valuation" },
    { "date": "2024-09-01", "name": "IPO Filing", "detail": "Filed S-1 with SEC" }
  ],
  "placement": "split",
  "dateFormat": "month-year"
}
```

| Property | Type | Default | Description |
|---|---|---|---|
| `events` | `array` | required | Array of `{ date, name, detail }` event objects |
| `events[].date` | `string` | required | ISO date string (e.g. `"2024-01-15"`) |
| `events[].name` | `string` | required | Bold heading for this event |
| `events[].detail` | `string` | required | Body text (plain or bullet) |
| `title` | `string` | — | Optional title above the timeline |
| `placement` | `"split"`, `"top"`, `"bottom"` | `"split"` | Where events appear relative to the timeline line |
| `topCount` | `number` | ceil(n/2) | How many events on top (rest go below). Only applies to `"split"`. |
| `dateFormat` | `"month"`, `"year"`, `"month-year"`, `"quarter"`, `"quarter-year"` | `"month-year"` | How dates are formatted on the timeline |
| `lineColor` | `string` | style accent | Timeline line color |
| `lineWidth` | `number` | 25400 | Timeline line thickness (EMU) |
| `nodeColor` | `string` | `"#FFFFFF"` | Node fill color |
| `nodeBorderColor` | `string` | lineColor | Node border color |
| `nodeBorderWidth` | `number` | 19050 | Node border thickness (EMU) |
| `connectorColor` | `string` | lighter lineColor | Elbow connector color |
| `connectorWidth` | `number` | 9525 | Connector thickness (EMU) |
| `textMode` | `"plain"`, `"bullet"` | `"plain"` | How detail text renders |
| `boxWidth` | `number` | — | EMU override for text box width |
| `boxHeight` | `number` | — | EMU override for text box height |

**When to use:** Company milestones, transaction timelines, process phases, fundraising history.

### `flowchart`

Mermaid-based flowchart diagram. Provide Mermaid syntax and the renderer converts it to SVG.

```json
{
  "type": "flowchart",
  "code": "graph TD\n  A[Start] --> B{Decision}\n  B -->|Yes| C[Action 1]\n  B -->|No| D[Action 2]\n  C --> E[End]\n  D --> E",
  "theme": "neutral"
}
```

| Property | Type | Default | Description |
|---|---|---|---|
| `code` | `string` | required | Mermaid diagram syntax |
| `svgData` | `string` | — | Cached rendered SVG for display and PPTX export (auto-generated) |
| `theme` | `string` | `"default"` | Mermaid theme: `"default"`, `"neutral"`, `"forest"`, `"dark"`, `"base"` |

**When to use:** Process flows, decision trees, organizational structures, system architectures.

### `reactflow`

Interactive node/edge diagram using ReactFlow. For complex flow diagrams with precise node positioning.

```json
{
  "type": "reactflow",
  "nodes": [
    { "id": "1", "type": "process", "position": { "x": 100, "y": 100 }, "data": { "label": "Step 1" } },
    { "id": "2", "type": "decision", "position": { "x": 300, "y": 100 }, "data": { "label": "Check?" } },
    { "id": "3", "type": "terminal", "position": { "x": 500, "y": 100 }, "data": { "label": "Done" } }
  ],
  "edges": [
    { "id": "e1-2", "source": "1", "target": "2", "label": "next" },
    { "id": "e2-3", "source": "2", "target": "3", "label": "yes" }
  ]
}
```

| Property | Type | Description |
|---|---|---|
| `nodes` | `array` | Array of node objects with `id`, `type`, `position: { x, y }`, `data: { label }` |
| `nodes[].type` | `string` | Node shape: `"process"` (rectangle), `"decision"` (diamond), `"terminal"` (rounded) |
| `edges` | `array` | Array of edge objects with `id`, `source`, `target`, optional `label`, `type`, `animated` |

**When to use:** Complex process flows with precise layout control, multi-path decision trees, system architecture diagrams.

### `aiSvg`

AI-generated SVG vector graphic. Provide a prompt and the editor generates an SVG, converting it to PNG for PPTX export.

```json
{
  "type": "aiSvg",
  "prompt": "simple icon showing a handshake between two business people"
}
```

| Property | Type | Description |
|---|---|---|
| `prompt` | `string` | Description of the SVG to generate |
| `svgData` | `string` | Cached SVG markup from AI (auto-generated) |

**When to use:** Simple vector icons, illustrations, decorative graphics. For complex structured visuals (tables, dashboards, infographics), prefer `aiHtml` instead.

### `aiHtml`

AI-generated HTML/CSS visual. Renders in a sandboxed iframe and converts to PNG for PPTX export. Use for rich visuals that benefit from HTML/CSS expressiveness.

```json
{
  "type": "aiHtml",
  "prompt": "modern SaaS metrics dashboard with 4 KPIs and a revenue chart",
  "htmlData": "<!DOCTYPE html><html><head><style>...</style></head><body>...</body></html>"
}
```

| Property | Type | Description |
|---|---|---|
| `prompt` | `string` | Description of the HTML visual to generate |
| `htmlData` | `string` | Complete HTML document (auto-generated or provided). Must be self-contained — all CSS inline or in style tags, no external resources. Size content to fill viewport using `width: 100vw; height: 100vh`. |
| `pngUrl` | `string` | URL of rendered PNG snapshot for PPTX export (auto-generated) |
| `pngDataUrl` | `string` | Base64 data URL of rendered PNG (legacy, auto-generated) |
| `crop` | `object` | Crop percentages: `{ "l": 0, "t": 0, "r": 0, "b": 0 }`. Each value is 0-100 representing % to crop from that side. |

**When to use:** Complex layouts with gradients, styled comparison tables, product feature cards, process flow diagrams, organizational charts, infographics, dashboards — any visual where HTML/CSS is more expressive than charts or SVG.

### `aiEcharts`

AI-generated Apache ECharts visualization. Accepts a complete ECharts option config, giving access to the entire ECharts library — any chart type, feature, or configuration.

```json
{
  "type": "aiEcharts",
  "prompt": "radar chart comparing 5 companies across 6 dimensions",
  "echartsOption": {
    "radar": { "indicator": [
      { "name": "Revenue", "max": 100 },
      { "name": "Growth", "max": 100 },
      { "name": "Margin", "max": 100 },
      { "name": "Market Share", "max": 100 },
      { "name": "Innovation", "max": 100 },
      { "name": "Retention", "max": 100 }
    ]},
    "series": [{ "type": "radar", "data": [
      { "name": "Company A", "value": [80, 65, 72, 45, 90, 85] },
      { "name": "Company B", "value": [60, 85, 55, 70, 60, 70] }
    ]}]
  }
}
```

| Property | Type | Description |
|---|---|---|
| `prompt` | `string` | Description of the chart to generate (can be used without `echartsOption` for AI generation) |
| `echartsOption` | `object` | Full Apache ECharts option config. Passed directly to ECharts. |

**Available chart types:** radar, heatmap, treemap, sunburst, funnel, sankey, parallel, candlestick, boxplot, graph/network, tree, calendar, custom series, and all standard types.

**Available features:** gradients, patterns, rich text labels, markLine, markPoint, markArea, polar coordinates, visual mapping, data zoom, graphic layers, dataset transforms.

**Rules:**
- Do NOT set `animation: true` or `backgroundColor` in echartsOption (handled by the renderer).
- You can set just `prompt` without `echartsOption` and the editor will generate the config via AI.

**When to use:** Radar charts, heatmaps, treemaps, sunbursts, funnels, Sankey diagrams, network/graph visualizations, calendar heatmaps — any visualization not covered by the built-in `chart` type. For stock price charts, use type `chart` with `chartType: "line"` instead (supports `pptxExportMode`).

---

## Chart Types

All chart content uses `"type": "chart"` with a `chartType` field. An optional `title` string adds a centered title above the chart.

### Shared Properties (all chart types except pie)

| Property | Type | Default | Description |
|---|---|---|---|
| `yMin` | `number` | auto | Y-axis minimum value |
| `yMax` | `number` | auto | Y-axis maximum value |
| `gridTop` | `number` | varies | Pixels from top edge to chart area |
| `gridRight` | `number` | 20 | Pixels from right edge to chart area |
| `gridBottom` | `number` | varies | Pixels from bottom edge to chart area |
| `gridLeft` | `number` | 60 | Pixels from left edge to chart area |
| `showXAxis` | `boolean` | `true` | Show X-axis line and labels |
| `showYAxis` | `boolean` | `true` | Show Y-axis line and labels |
| `showSplitLine` | `boolean` | `true` | Show horizontal grid lines |
| `showLegend` | `boolean` | `true` | Show legend (multi-series charts) |
| `legendPosition` | `"bottom"`, `"top"`, `"left"`, `"right"` | `"bottom"` | Legend placement |
| `pptxExportMode` | `"image"`, `"nativeExcel"` | `"image"` | Export mode. `"image"` renders as a PNG image in PPTX (pixel-perfect). `"nativeExcel"` creates an editable native PowerPoint chart backed by an embedded Excel workbook. |

**Important:** When a chart has a bottom legend (multi-series bar/line/scatter/combo), set `gridBottom` to at least **45** to prevent the legend from overlapping the chart area.

### `bar`

Vertical (or horizontal) bar chart. Supports grouped and stacked modes.

```json
{
  "type": "chart", "chartType": "bar",
  "title": "Revenue by Segment",
  "categories": ["Software", "Services", "Hardware"],
  "series": [
    { "name": "2023", "data": [320, 180, 95] },
    { "name": "2024", "data": [380, 210, 88] }
  ],
  "horizontal": false,
  "stacked": false
}
```

| Property | Type | Default | Description |
|---|---|---|---|
| `categories` | `string[]` | required | X-axis labels |
| `series` | `array` | required | `[{ name, data: number[] }]` |
| `horizontal` | `boolean` | `false` | Horizontal bars |
| `stacked` | `boolean` | `false` | Stack series |
| `stackTotal` | `number[]` | — | When stacked, array of totals (one per category) to display above each bar stack. Formatted with the same valuePrefix/valueSuffix/valueDecimals |
| `showBarValues` | `boolean` | `true` | Show value labels at end of bars |
| `valuePrefix` | `string` | `""` | Prefix for value labels (e.g. `"$"`) |
| `valueSuffix` | `string` | `""` | Suffix for value labels (e.g. `"%"`) |
| `valueDecimals` | `number` | `0` | Decimal places in value labels |
| `valueThousands` | `boolean` | `false` | Use thousand separator (1,000) in value labels |
| `negativeFormat` | `"minus"` or `"parens"` | `"minus"` | How to display negative values: `-100` or `(100)` |
| `barColor` | `string` | auto | Bar fill color override (hex). Only applies to single-series charts. |
| `barBorderColor` | `string` | none | Bar border color (hex) |
| `barBorderWidth` | `number` | `0` | Bar border width in pixels |
| `categoryColors` | `object` | — | Per-category bar color overrides. Keys are 0-based category indices, values are hex colors: `{ "0": "#4472C4", "2": "#E94560" }` |
| `showSeriesLabels` | `boolean` | `false` | Show series name labels on bars |
| `showTotalValues` | `boolean` | `false` | Show total value labels above stacked bars (only applies when `stacked: true`) |

**Value formatting example:**
```json
{
  "type": "chart", "chartType": "bar",
  "categories": ["Q1", "Q2", "Q3", "Q4"],
  "series": [{ "name": "Revenue", "data": [1250, 1480, -320, 1650] }],
  "showBarValues": true,
  "valuePrefix": "$",
  "valueThousands": true,
  "negativeFormat": "parens",
  "barColor": "#4472C4",
  "barBorderColor": "#2F5496",
  "barBorderWidth": 1
}
```
Value labels would render as: `$1,250`, `$1,480`, `($320)`, `$1,650`

**Grouped vs stacked:** With multiple series, bars are **grouped** (side-by-side) by default. Set `stacked: true` to stack them. The example above with two series and `stacked: false` renders as a grouped bar chart with 2023 and 2024 bars side by side for each category.

**When to use:** Comparing values across categories. The most versatile chart type. Use for revenue breakdowns, market share, year-over-year comparisons.

### `line`

Line chart with optional smoothing and area fill.

```json
{
  "type": "chart", "chartType": "line",
  "categories": ["Q1", "Q2", "Q3", "Q4"],
  "series": [{ "name": "Revenue", "data": [145, 162, 178, 195] }],
  "smooth": true,
  "area": false
}
```

| Property | Type | Default | Description |
|---|---|---|---|
| `categories` | `string[]` | required | X-axis labels |
| `series` | `array` | required | `[{ name, data: number[] }]` |
| `smooth` | `boolean` | `false` | Smooth curves |
| `area` | `boolean` | `false` | Fill area under line |
| `showLineValues` | `boolean` | `false` | Show value labels on each data point |
| `lineColor` | `string` | auto | Line color override (hex) |
| `lineWidth` | `number` | `2` | Line width in pixels |
| `pointShape` | `"circle"`, `"rect"`, `"triangle"`, `"diamond"` | `"circle"` | Marker shape |
| `pointSize` | `number` | `6` | Marker size in pixels |
| `pointColor` | `string` | auto | Marker fill color (hex) |
| `pointBorderColor` | `string` | none | Marker border color (hex) |
| `pointBorderWidth` | `number` | `0` | Marker border width in pixels |
| `xAxisLabel` | `string` | none | X-axis name label |
| `xAxisLabelPosition` | `"end"` or `"center"` | `"end"` | X-axis label placement |
| `yAxisLabel` | `string` | none | Y-axis name label |
| `yAxisLabelPosition` | `"top"` or `"side"` | `"top"` | Y-axis label placement. `"side"` renders rotated -90° along the axis. |
| `valuePrefix` | `string` | `""` | Prefix for value labels (shared with bar) |
| `valueSuffix` | `string` | `""` | Suffix for value labels |
| `valueDecimals` | `number` | `0` | Decimal places in value labels |
| `valueThousands` | `boolean` | `false` | Thousand separator in value labels |

**Line styling example:**
```json
{
  "type": "chart", "chartType": "line",
  "categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
  "series": [{ "name": "Revenue", "data": [145, 162, 178, 195, 210, 225] }],
  "smooth": true,
  "showLineValues": true,
  "valuePrefix": "$",
  "valueSuffix": "M",
  "lineColor": "#4472C4",
  "lineWidth": 3,
  "pointShape": "diamond",
  "pointSize": 8,
  "pointColor": "#4472C4",
  "pointBorderColor": "#FFFFFF",
  "pointBorderWidth": 2,
  "xAxisLabel": "Month",
  "xAxisLabelPosition": "center",
  "yAxisLabel": "Revenue ($M)",
  "yAxisLabelPosition": "side"
}
```

**When to use:** Trends over time, growth trajectories, margin progression.

### `pie`

Pie or doughnut chart.

```json
{
  "type": "chart", "chartType": "pie",
  "items": [
    { "name": "Software", "value": 65 },
    { "name": "Services", "value": 25 },
    { "name": "Other", "value": 10 }
  ],
  "doughnut": false
}
```

| Property | Type | Default | Description |
|---|---|---|---|
| `items` | `array` | required | `[{ name, value }]` |
| `doughnut` | `boolean` | `false` | Doughnut style (hollow center) |
| `showLegend` | `boolean` | `true` | Show legend |
| `legendPosition` | `"bottom"`, `"top"`, `"left"`, `"right"` | `"bottom"` | Legend placement |
| `showLabel` | `boolean` | `true` | Show slice labels |
| `labelPosition` | `string` | `"outside"` | `"outside"`, `"inside"`, `"center"`, or `"none"` (hides labels) |
| `roseType` | `string` | — | `"radius"` or `"area"` for rose/nightingale chart |
| `innerRadius` | `string` | `"0%"` / `"40%"` | Inner radius (% of container). `"40%"` when doughnut |
| `outerRadius` | `string` | `"70%"` | Outer radius (% of container) |
| `centerX` | `string` | `"50%"` | Horizontal center position |
| `centerY` | `string` | `"50%"` | Vertical center position. Use `"45%"` to leave room for bottom legend |
| `startAngle` | `number` | `90` | Start angle in degrees |
| `pieBorderColor` | `string` | none | Slice border color (hex). Creates visual separation between slices. |
| `pieBorderWidth` | `number` | `0` | Slice border width in pixels |
| `padAngle` | `number` | `0` | Spacing between slices in degrees. Creates gaps between pie segments. |
| `sliceColors` | `object` | — | Per-slice color overrides. Keys are 0-based slice indices, values are hex colors: `{ "0": "#4472C4", "1": "#E94560" }` |
| `showPieValues` | `boolean` | `false` | Show numeric values on slices |
| `pieValuePosition` | `"outside"`, `"inside"` | `"outside"` | Position of pie value labels |
| `title` | `string` | — | Chart title text |
| `valuePrefix` | `string` | `""` | Prefix for value labels (e.g. `"$"`) |
| `valueSuffix` | `string` | `""` | Suffix for value labels (e.g. `"%"`) |
| `valueDecimals` | `number` | `0` | Decimal places in value labels |
| `valueThousands` | `boolean` | `false` | Use thousand separator (1,000) in value labels |
| `negativeFormat` | `"minus"` or `"parens"` | `"minus"` | How to display negative values: `-100` or `(100)` |
| `gridTop` | `number` | — | Pixels from top edge. Used to shift pie center position. |
| `gridRight` | `number` | — | Pixels from right edge. Used to shift pie center position. |
| `gridBottom` | `number` | — | Pixels from bottom edge. Used to shift pie center position. |
| `gridLeft` | `number` | — | Pixels from left edge. Used to shift pie center position. |

**Pie border & spacing example:**
```json
{
  "type": "chart", "chartType": "pie",
  "items": [
    { "name": "Software", "value": 65 },
    { "name": "Services", "value": 25 },
    { "name": "Other", "value": 10 }
  ],
  "doughnut": true,
  "pieBorderColor": "#FFFFFF",
  "pieBorderWidth": 2,
  "padAngle": 3
}
```

**Note:** Pie charts translate `gridTop/gridRight/gridBottom/gridLeft` into a shifted center position (they don't use ECharts grid directly). You can also use `centerX`/`centerY` directly to set the center. Use `innerRadius` and `outerRadius` to control size.

**When to use:** Composition breakdowns, revenue mix, market share distribution. Best with 2-6 segments. Note: uses `items` not `series`.

### `combo`

Combined bar + line chart on the same axes. Supports dual Y-axis.

```json
{
  "type": "chart", "chartType": "combo",
  "categories": ["Q1", "Q2", "Q3", "Q4"],
  "bars": [{ "name": "Revenue", "data": [145, 162, 178, 195] }],
  "lines": [{ "name": "Margin %", "data": [62, 64, 63, 65] }],
  "dualAxis": true
}
```

| Property | Type | Default | Description |
|---|---|---|---|
| `categories` | `string[]` | required | X-axis labels |
| `bars` | `array` | `[]` | `[{ name, data }]` for bar series |
| `lines` | `array` | `[]` | `[{ name, data }]` for line series |
| `dualAxis` | `boolean` | `false` | Separate Y-axis for lines |
| `y2Min` | `number` | auto | Secondary Y-axis minimum (when `dualAxis: true`) |
| `y2Max` | `number` | auto | Secondary Y-axis maximum (when `dualAxis: true`) |
| `y2ValuePrefix` | `string` | `""` | Prefix for secondary axis value labels (e.g. `"$"`) |
| `y2ValueSuffix` | `string` | `""` | Suffix for secondary axis value labels (e.g. `"%"`) |
| `y2ValueDecimals` | `number` | `0` | Decimal places for secondary axis values |
| `y2ValueThousands` | `boolean` | `false` | Thousand separator for secondary axis values |

**When to use:** Revenue + margin, volume + price, any metric pair where one is absolute and one is relative. `dualAxis: true` when scales differ.

### `waterfall`

Waterfall (bridge) chart. First and last values are totals; middle values are deltas.

```json
{
  "type": "chart", "chartType": "waterfall",
  "categories": ["FY24 Revenue", "Organic Growth", "Acquisitions", "FX Impact", "FY25E Revenue"],
  "data": [680, 85, 45, -30, 780]
}
```

| Property | Type | Description |
|---|---|---|
| `categories` | `string[]` | Labels for each bar |
| `data` | `number[]` | First = start total, last = end total, middle = increments/decrements |

**When to use:** Revenue bridges, cost walks, year-over-year reconciliations. Classic investment banking chart.

### `scatter`

Scatter plot with multiple series. Each data point is `[x, y]`.

```json
{
  "type": "chart", "chartType": "scatter",
  "series": [
    { "name": "Tech", "data": [[15, 42], [22, 38], [8, 55]] },
    { "name": "Finance", "data": [[12, 35], [18, 28], [25, 45]] }
  ]
}
```

| Property | Type | Description |
|---|---|---|
| `series` | `array` | `[{ name, data: [x, y][] }]` |

**When to use:** Correlation analysis, peer positioning (growth vs. margin), risk-return plots.

### `gauge`

Single-value gauge/speedometer.

```json
{
  "type": "chart", "chartType": "gauge",
  "value": 73,
  "min": 0,
  "max": 100,
  "label": "NPS Score",
  "format": "{value}"
}
```

| Property | Type | Default | Description |
|---|---|---|---|
| `value` | `number` | `0` | Current value |
| `min` | `number` | `0` | Minimum |
| `max` | `number` | `100` | Maximum |
| `label` | `string` | `""` | Label below gauge |
| `format` | `string` | `"{value}%"` | Display format |

**When to use:** Single KPI with context (NPS, utilization rate, goal progress). Best in small zones.

---

## Components

Components are reusable sub-slide building blocks that can be placed in any grid cell. Use `$component` in body node children.

### `tombstone`

Investment banking deal tombstone card.

```json
{
  "$component": "tombstone",
  "clientName": "Acme Corp",
  "clientLogo": "images/acme.png",
  "transactionText": "— Acquired by —",
  "counterpartyLogo": "images/buyer.png",
  "date": "September 2025",
  "logoWidth": 100,
  "logoHeight": 40
}
```

**When to use:** Inside `transactionGrid` or `globalBuyers` templates, or in custom grid layouts for deal credentials.

---

## Decision Guide

### Choosing a Chart Type

| Data Shape | Chart Type |
|---|---|
| Values across categories | `bar` |
| Trend over time | `line` |
| Composition / market share | `pie` |
| Revenue + margin (two scales) | `combo` with `dualAxis: true` |
| Year-over-year bridge | `waterfall` |
| Correlation (x,y pairs) | `scatter` |
| Single KPI with range context | `gauge` |
| Radar, heatmap, treemap, funnel, Sankey, etc. | `aiEcharts` |

### Choosing a Content Type

| Content Need | Content Type |
|---|---|
| Key points / analysis | `text` (with `bullet: true` runs) |
| Headline metrics | grid (stat row — see common-components skill) |
| Structured data comparison | `table` |
| Feature/product cards | `cardGrid` |
| Free-form text | `text` |
| Data visualization (bar/line/pie/etc.) | `chart` |
| Advanced visualization (radar/heatmap/funnel/etc.) | `aiEcharts` |
| Pre-made image / logo | `image` |
| Rich HTML/CSS visual | `aiHtml` |
| Vector icon / illustration | `aiSvg` |
| Timeline / milestones | `timeline` |
| Process flow diagram | `flowchart` or `reactflow` |

---

## Common Presentation Structures

### Corporate / Investment Banking Pitch

```json
{
  "style": "corporate",
  "slides": [
    { "type": "grid", "title": "", "body": { "direction": "row", "children": [{ "span": 1 }, { "span": 7, "direction": "col", "children": [{ "content": { "type": "text", "text": "Presentation Title", "fontSize": 2400, "anchor": "b" } }, { "content": { "type": "text", "text": "February 2026", "fontSize": 1600 } }] }, { "span": 4, "content": { "type": "image", "source": "ai", "prompt": "professional pattern", "objectFit": "cover" } }] } },
    { "type": "grid", "title": "Executive Summary", "body": { "direction": "col", "children": [
      { "span": 3, "direction": "row", "children": [
        { "span": 3, "direction": "col", "children": [{ "span": 7, "content": { "type": "text", "text": "$42M", "fontSize": 2000, "bold": true, "align": "ctr", "anchor": "b", "color": "#4472C4" } }, { "span": 5, "content": { "type": "text", "text": "Revenue", "fontSize": 900, "bold": true, "align": "ctr", "anchor": "t" } }] },
        { "span": 3, "direction": "col", "children": [{ "span": 7, "content": { "type": "text", "text": "68%", "fontSize": 2000, "bold": true, "align": "ctr", "anchor": "b", "color": "#4472C4" } }, { "span": 5, "content": { "type": "text", "text": "Margin", "fontSize": 900, "bold": true, "align": "ctr", "anchor": "t" } }] },
        { "span": 3, "direction": "col", "children": [{ "span": 7, "content": { "type": "text", "text": "4,820", "fontSize": 2000, "bold": true, "align": "ctr", "anchor": "b", "color": "#4472C4" } }, { "span": 5, "content": { "type": "text", "text": "Customers", "fontSize": 900, "bold": true, "align": "ctr", "anchor": "t" } }] },
        { "span": 3, "direction": "col", "children": [{ "span": 7, "content": { "type": "text", "text": "32%", "fontSize": 2000, "bold": true, "align": "ctr", "anchor": "b", "color": "#4472C4" } }, { "span": 5, "content": { "type": "text", "text": "Growth", "fontSize": 900, "bold": true, "align": "ctr", "anchor": "t" } }] }
      ]},
      { "span": 9, "direction": "row", "children": [
        { "span": 8, "header": "Trend", "content": { "type": "chart", "chartType": "bar", ... } },
        { "span": 4, "header": "Key Metrics", "content": { "type": "table", "data": { ... } } }
      ]}
    ]}},
    { "type": "grid", "title": "Market Overview", "body": { "direction": "row", "children": [
      { "span": 6, "header": "Market Size", "content": { "type": "chart", "chartType": "bar", ... } },
      { "span": 6, "header": "Key Trends", "content": { "type": "text", "runs": [{ "text": "...", "bullet": true }, ...] } }
    ]}},
    { "type": "grid", "title": "Competitive Landscape", "body": { "direction": "row", "children": [
      { "span": 7, "header": "Positioning", "content": { "type": "chart", ... } },
      { "span": 5, "direction": "col", "children": [
        { "span": 7, "header": "Peers", "content": { "type": "table", "data": { ... } } },
        { "span": 5, "header": "Valuation", "direction": "row", "children": [{ "span": 6, "direction": "col", "children": [{ "span": 7, "content": { "type": "text", "text": "...", "fontSize": 2000, "bold": true, "align": "ctr", "anchor": "b", "color": "#4472C4" } }, { "span": 5, "content": { "type": "text", "text": "...", "fontSize": 900, "bold": true, "align": "ctr", "anchor": "t" } }] }, { "span": 6, "direction": "col", "children": [{ "span": 7, "content": { "type": "text", "text": "...", "fontSize": 2000, "bold": true, "align": "ctr", "anchor": "b", "color": "#4472C4" } }, { "span": 5, "content": { "type": "text", "text": "...", "fontSize": 900, "bold": true, "align": "ctr", "anchor": "t" } }] }] }
      ]}
    ]}}
  ]
}
```

### Product / Tech Pitch

```json
{
  "style": "minimal",
  "slides": [
    { "type": "grid", "title": "", "body": { "direction": "row", "children": [{ "span": 1 }, { "span": 7, "direction": "col", "children": [{ "content": { "type": "text", "text": "Presentation Title", "fontSize": 2400, "anchor": "b" } }, { "content": { "type": "text", "text": "February 2026", "fontSize": 1600 } }] }, { "span": 4, "content": { "type": "image", "source": "ai", "prompt": "professional pattern", "objectFit": "cover" } }] } },
    { "type": "grid", "title": "The Problem", "body": { "direction": "row", "children": [
      { "span": 7, "content": { "type": "text", "runs": [{ "text": "...", "bullet": true }, ...] } },
      { "span": 5, "direction": "row", "children": [{ "span": 6, "direction": "col", "children": [{ "span": 7, "content": { "type": "text", "text": "...", "fontSize": 2000, "bold": true, "align": "ctr", "anchor": "b", "color": "#4472C4" } }, { "span": 5, "content": { "type": "text", "text": "...", "fontSize": 900, "bold": true, "align": "ctr", "anchor": "t" } }] }, { "span": 6, "direction": "col", "children": [{ "span": 7, "content": { "type": "text", "text": "...", "fontSize": 2000, "bold": true, "align": "ctr", "anchor": "b", "color": "#4472C4" } }, { "span": 5, "content": { "type": "text", "text": "...", "fontSize": 900, "bold": true, "align": "ctr", "anchor": "t" } }] }] }
    ]}},
    { "type": "grid", "title": "Our Solution", "body": { "direction": "row", "children": [
      { "span": 6, "content": { "type": "cardGrid", "items": [...] } },
      { "span": 6, "content": { "type": "chart", "chartType": "bar", ... } }
    ]}},
    { "type": "grid", "title": "Customer Results", "body": { "direction": "row", "children": [
      { "span": 6, "header": "What Clients Say", "content": { "type": "text", "runs": [{ "text": "...", "bullet": true }, ...] } },
      { "span": 6, "direction": "row", "children": [{ "span": 4, "direction": "col", "children": [{ "span": 7, "content": { "type": "text", "text": "...", "fontSize": 2000, "bold": true, "align": "ctr", "anchor": "b", "color": "#4472C4" } }, { "span": 5, "content": { "type": "text", "text": "...", "fontSize": 900, "bold": true, "align": "ctr", "anchor": "t" } }] }, { "span": 4, "direction": "col", "children": [{ "span": 7, "content": { "type": "text", "text": "...", "fontSize": 2000, "bold": true, "align": "ctr", "anchor": "b", "color": "#4472C4" } }, { "span": 5, "content": { "type": "text", "text": "...", "fontSize": 900, "bold": true, "align": "ctr", "anchor": "t" } }] }, { "span": 4, "direction": "col", "children": [{ "span": 7, "content": { "type": "text", "text": "...", "fontSize": 2000, "bold": true, "align": "ctr", "anchor": "b", "color": "#4472C4" } }, { "span": 5, "content": { "type": "text", "text": "...", "fontSize": 900, "bold": true, "align": "ctr", "anchor": "t" } }] }] }
    ]}}
  ]
}
```

---

## Important Rules

1. **Always use `type: "chart"` for data visualizations**, not `type: "image"`. Charts are rendered at the exact container size.
2. **`fontSize` is in hundredths of a point**: 1400 = 14pt, 1200 = 12pt, 2800 = 28pt.
3. **Colors are hex strings** with `#` prefix: `"#4472C4"`, `"#333333"`.
4. **`pie` charts use `items`**, all other charts use `series` (except `waterfall` which uses `data`).
5. **`combo` charts use `bars` and `lines`**, not `series`.
6. **`span` values are relative weights** in a 12-column grid. Two children with span 6 each = two equal halves.
7. **Images use `source: "search"` or `source: "ai"`** for auto-resolved images. Use `path` for file-based images (JSON-to-PPTX converter) or `buffer` for programmatic API. Always set `objectFit: "contain"` for logos.
8. **Keep bullet items concise** — 1-2 lines each, 3-6 items per list.
9. **Use `sectionLabel`** on content slides to show which section the slide belongs to.
10. **Charts with bottom legends need extra spacing** — when a bar/line/scatter/combo chart has multiple series, set `gridBottom: 45` to prevent the legend from overlapping the chart area. For pie charts, use `centerY: "45%"` instead.
11. **Pie charts use `centerX`/`centerY` for positioning**, not `gridTop`/`gridBottom`/etc. Adjust `centerY` to shift the pie up/down and `outerRadius` to resize it.
12. **Table content MUST use a `"data"` wrapper.** Correct: `{ "type": "table", "data": { "headers": [...], "rows": [...] } }`. WRONG: `{ "type": "table", "headers": [...], "rows": [...] }`. The API will reject tables without the `"data"` key.
