# Presentations Guide — Agent Instructions

Instructions for building HTML slide decks in the Fractal system and exporting them to PowerPoint.

Decks are a **fixed 1920×1080 HTML canvas** — not responsive app surfaces. They are authored as static HTML, previewed in the browser, then exported to `.pptx` by the user's `gen_pptx` tool.

---

## Core rules

Two rules govern every deck. Violate either and the export breaks.

### 1. HTML first, always

**Always build and preview the deck as HTML before exporting.** Never generate PPTX directly. The export tool captures what the browser renders — if the deck is broken in the browser, the `.pptx` will be broken too.

Workflow:

1. Author the deck as a single HTML file starting from `starter.html`.
2. `show_to_user` the file so the user can see it rendered.
3. Iterate on content + layout in HTML.
4. Only when the deck looks correct in the browser, call `gen_pptx`.

### 2. Self-contained, always

**A deck is a single HTML file with every style and script inlined.** No `<link rel="stylesheet" href="…">`. No `<script src="…">`. No `@import`. No dependency on the hosted design system URLs.

Why: agent viewers (Axon, sandboxed preview harnesses, offline capture environments) routinely cannot reach external URLs. When a hosted CSS file fails to load, every token (`--bg-canvas`, `--slide-pad-x`, `--font-sans`, `--slide-width`) resolves to nothing — backgrounds go transparent, padding collapses, typography falls back to the browser default, and the `<deck-stage>` scaling math breaks because the canvas dimensions are undefined. The failure mode is **silent and confusing**: the HTML parses fine, but the deck renders as a broken blob. You won't get a console error, you'll get a bug report.

The fonts in `@font-face` URLs are the one permitted external reference. `--font-sans` includes `system-ui, sans-serif` as a fallback so the deck still renders legibly if the font host is blocked.

How to produce a self-contained deck:

- **Start from `starter.html`.** It is regenerated by `scripts/bundle-starter.js` with every token, style rule, and the full `<deck-stage>` component inlined. Copy it and edit the slide content — do not restructure the `<head>` or the trailing `<script>`.
- **If you must add styles**, put them in the deck's own `<style>` block, below the bundled CSS. Never add a `<link>` tag.
- **If you must add scripts** (e.g., a chart library), inline the script source into a `<script>` tag. Do not use `src=""`.
- **If you need an image**, either reference a stable absolute URL the user has confirmed is reachable, or base64-encode it as a `data:` URI.

When in doubt: open the deck in a fresh browser tab with DevTools' Network panel set to "Offline", reload, and confirm it still renders correctly. If any style is missing, the deck is not self-contained.

Inlining CSS is the right instinct. Removing `<deck-stage>` is not — see rule 3.

### 3. Never remove `<deck-stage>`

`<deck-stage>` is not boilerplate. It is what makes a deck _work_:

- It scales the 1920×1080 canvas to the viewer's available space via `transform: scale()` on a shadow-DOM stage. Text, spacing, images — everything inside scales together because CSS transforms apply to the entire rendered subtree.
- It centers the scaled canvas inside the viewer (chat preview panel, Storybook frame, browser window, wherever the deck ends up).
- It toggles `data-deck-active` on the correct slide so the exporter can find it via `selector: "deck-stage > [data-deck-active]"`.
- It responds to the `noscale` attribute set by `gen_pptx` during editable export, dropping the transform so the PPTX captures the full 1920×1080 canvas at natural size.

If you remove the `<deck-stage>` wrapper and drop the `.slide` sections into the body directly, the `:not(:defined)` CSS fallback can't engage either (it scopes against a `deck-stage` ancestor). Slides render at raw 1920×1080, stacked with no scaling, and overflow any viewer narrower than 1920 px. The exporter also can't find slides via `deck-stage > [data-deck-active]`. **These are the symptoms of a "simplify" rewrite.** Keep `<deck-stage>`.

The scaling happens entirely inside the element's shadow DOM — no CSS you write for slides is affected, so you never need to "remove deck-stage to get more control."

---

## Implementation

Three files, all under `design/presentations/`:

| File                                       | Purpose                                                                                                                                                      |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`starter.html`](./starter.html)           | **The self-contained starter.** Fully bundled — every token, style, and the `<deck-stage>` component inlined. Copy this for every deck.                      |
| [`presentations.css`](./presentations.css) | Source-of-truth for `--slide-*` tokens, slide-scale type classes, and template utilities. **Don't link this from a deck** — the starter already includes it. |
| [`deck-stage.js`](./deck-stage.js)         | Source-of-truth for the `<deck-stage>` custom element. **Don't load this from a deck** — the starter already includes it.                                    |

To start a new deck: copy `starter.html`, rename it, edit the slide content, update the speaker-notes JSON. Nothing else.

---

## Canvas

All slides are **1920 × 1080 px** (16:9). This is a hard constraint — the export tool is locked to this size. Do not build responsive layouts. Do not use viewport units (`vw`, `vh`). Use absolute pixel values on the 8pt grid.

### Safe area

```
┌──────────────────────────── 1920 ────────────────────────────┐
│          96                                                   │
│   ┌─────────────────── 1664 ─────────────────────┐           │
│   │                                               │            │
│  128                       ↑                     128   1080   │
│   │                      888                      │            │
│   │                        ↓                      │            │
│   └───────────────────────────────────────────────┘           │
│          96                                                    │
└────────────────────────────────────────────────────────────────┘
```

The safe area is the `.slide` padding box: 128 px left/right, 96 px top/bottom. Anchor all content inside it unless the template explicitly bleeds (`.slide--bleed`).

### Host sizing

`<deck-stage>` sizes itself to its parent. In single-slide presenter mode it uses `height: 100%` with an `aspect-ratio: 16/9` fallback. For the deck to fill the browser window, the starter's `html, body { margin: 0; height: 100% }` block is load-bearing — it gives the body a real height so `<deck-stage>`'s `100%` resolves to the viewport.

The bundled `starter.html` already does this. If you hand-write a deck outside the starter, include the `html, body` block — without it, the body collapses to its content height and the deck falls back to the aspect-ratio rendering (proportional to width), which is usually fine but may not fill as expected.

### Tokens

| Token                          | Value                | Use                      |
| ------------------------------ | -------------------- | ------------------------ |
| `--slide-width`                | 1920px               | Canvas width             |
| `--slide-height`               | 1080px               | Canvas height            |
| `--slide-pad-x`                | 128px (`--space-13`) | Horizontal safe area     |
| `--slide-pad-y`                | 96px (`--space-12`)  | Vertical safe area       |
| `--slide-pad-bleed`            | 64px (`--space-10`)  | Section-divider padding  |
| `--slide-gutter`               | 48px (`--space-9`)   | Column gap               |
| `--slide-gap-stack`            | 24px (`--space-6`)   | Default vertical rhythm  |
| `--slide-gap-tight`            | 12px (`--space-3`)   | List items, tight groups |
| `--slide-gap-loose`            | 48px (`--space-9`)   | Separated groups         |
| `--slide-split-ratio-even`     | `1fr 1fr`            | Two-column 50/50         |
| `--slide-split-ratio-weighted` | `3fr 2fr`            | Two-column 60/40         |
| `--slide-three-col`            | `1fr 1fr 1fr`        | Equal three-column       |
| `--slide-chart-split`          | `7fr 5fr`            | Chart-focused split      |

---

## Typography inside slides

`typography.css` tops out at 28 px (1.75 rem) — correct for app UI, far too small for 1080p. Presentations define a **slide-scale type set** that reuses the same font families and weight discipline.

Use ONLY these classes inside a slide:

| Class            | Size / Line-height / Weight | Family | Use                                |
| ---------------- | --------------------------- | ------ | ---------------------------------- |
| `.slide-title`   | 96 / 1.1 / 600              | sans   | Cover and section titles           |
| `.slide-heading` | 56 / 1.2 / 600              | sans   | Per-slide headings                 |
| `.slide-subhead` | 36 / 1.3 / 500              | sans   | Cover subhead, secondary headings  |
| `.slide-body`    | 28 / 1.4 / 400              | sans   | Body prose, bullets                |
| `.slide-caption` | 20 / 1.4 / 400              | sans   | Figure captions, secondary notes   |
| `.slide-label`   | 18 / 1.4 / 600              | sans   | Uppercase eyebrow labels (tracked) |
| `.slide-meta`    | 18 / 1.4 / 400              | mono   | Date/byline, source notes, URLs    |

**Weight discipline:** 400 / 500 / 600 / 700 only. Do not use 300, 800, or 900. Match the rules in `typography-guide.md`.

Do not introduce new font sizes. If a slide needs a size that isn't in this set, rethink the layout.

---

## Colors inside slides

Use the semantic tokens from `color/colors.css` — no new tokens. Everything from `philosophy.md` still applies:

- Background: `--bg-canvas` (default), `--bg-surface-1` (section dividers).
- Text: `--fg-primary`, `--fg-secondary`, `--fg-tertiary`.
- Borders: `--border-subtle` on media/containers.
- Accents: `--accent-default` (blue-500) for eyebrows and emphasis; `--pink-default` for secondary brand moments.
- **No gradients on functional surfaces.** The brand gradient (`linear-gradient(135deg, var(--blue-500), var(--pink-500))`) is allowed on cover slides as decorative, not under body text.
- Status colors follow the philosophy's status accent pattern (background 50 / border 200 / text 500).

---

## Templates

The six templates below are **common starting patterns**, not an exhaustive list. They cover roughly 80% of the slides you'll build — use them when they fit. When they don't, build a custom layout that serves the content. You have full freedom over the internal layout of a slide as long as you:

- Use the slide-scale type classes (`.slide-title`, `.slide-heading`, `.slide-body`, etc.) for text.
- Use the color, border, radius, and shadow tokens from the other design domains.
- Respect the safe-area padding (via `.slide` or `.slide--bleed`) and the 1920×1080 canvas.
- Use pixel values on the 8pt grid (the `--slide-*` / `--space-*` tokens already are).

The utilities in `presentations.css` — `.slide-stack`, `.slide-grid-12`, `.slide-media`, and so on — are composable. Reach for them first before writing bespoke CSS. If you do need a one-off rule for a specific slide, scope it tightly in the deck's `<style>` block; don't add it to `presentations.css`.

Each slide is a `<section>` with `class="slide slide--{variant}"` plus any helpers.

### 1. Cover (`.slide--cover`)

Deck opener: one per deck.

```html
<section class="slide slide--cover slide-stack">
  <h1 class="slide-title">Deck title.</h1>
  <p class="slide-subhead">One-line framing of the deck's thesis.</p>
  <p class="slide-meta">Author · Team · 2026-04-24</p>
</section>
```

- Content left-aligned, vertically centered in the safe area.
- Byline pinned to the bottom-left corner of the safe area (the `.slide--cover .slide-meta` rule handles this).
- Don't: add body text, bullets, or images. The cover is title + subhead + byline only.

### 2. Section divider (`.slide--section.slide--bleed`)

A chapter break between deck sections.

```html
<section class="slide slide--section slide--bleed slide-stack">
  <span class="slide-label">Part 02</span>
  <h2 class="slide-title">Section title.</h2>
</section>
```

- Content bottom-left (`justify-content: flex-end`).
- Background `--bg-surface-1` so it reads as a palate cleanser.
- Don't: add body prose. Title + eyebrow label only.

### 3. Content (`.slide--content`)

Default slide. Use for ~70% of content slides.

```html
<section class="slide slide--content">
  <h2 class="slide-heading">Slide heading.</h2>
  <div class="slide-body slide-stack">
    <p>One tight paragraph introducing the point.</p>
    <ul>
      <li>Supporting bullet one.</li>
      <li>Supporting bullet two.</li>
      <li>Supporting bullet three.</li>
    </ul>
  </div>
</section>
```

- Content top-aligned; do NOT vertically center.
- One level of bullets max. No nested lists.
- Keep body under ~60 words. If more, split across two slides.

### 4. Split (`.slide--split`, optional `.slide--split-weighted` for 60/40)

Image + text side by side.

```html
<section class="slide slide--split">
  <figure class="slide-media"><img src="…" alt="…" /></figure>
  <div class="slide-stack">
    <h2 class="slide-heading">Slide heading.</h2>
    <p class="slide-body">Supporting prose.</p>
  </div>
</section>
```

- 50/50 default; add `slide--split-weighted` for 60/40 (the first child is the wider column).
- Media fills its column edge-to-edge inside the safe area.
- **DOM order matters for `gen_pptx` editable export** (it walks DOM). If you want the image on the right, reorder the HTML — do not flip with CSS `order`.

### 5. Three-column (`.slide--three`)

Three parallel items: image on top, body in the middle, caption below.

```html
<section class="slide slide--three">
  <h2 class="slide-heading">Three items compared.</h2>
  <div class="slide-three-col">
    <figure class="slide-stack">
      <div class="slide-media"><img src="a.png" alt="" /></div>
      <div class="slide-body">Body text that describes this item.</div>
      <p class="slide-caption slide-caption-block">Caption / source.</p>
    </figure>
    <figure class="slide-stack">
      <div class="slide-media"><img src="b.png" alt="" /></div>
      <div class="slide-body">Body text.</div>
      <p class="slide-caption slide-caption-block">Caption / source.</p>
    </figure>
    <figure class="slide-stack">
      <div class="slide-media"><img src="c.png" alt="" /></div>
      <div class="slide-body">Body text.</div>
      <p class="slide-caption slide-caption-block">Caption / source.</p>
    </figure>
  </div>
</section>
```

- Heading spans the top row; three equal columns sit below.
- Each column is a `<figure>` stacked: media → body → caption.
- Must be exactly three items. For two, use `.slide--split`. For four or more, split into multiple slides.
- Columns are equal width. Do not vary them.

### 6. Chart-focused (`.slide--chart`)

One hero chart with commentary on the right.

```html
<section class="slide slide--chart">
  <h2 class="slide-heading">Headline claim the chart supports.</h2>
  <div class="slide-chart">
    <figure class="slide-media slide-media--chart">
      <img src="chart.png" alt="Chart described in the commentary" />
    </figure>
    <div class="slide-stack">
      <p class="slide-body">One-paragraph interpretation of the chart.</p>
      <p class="slide-caption">Source: … · Method: …</p>
    </div>
  </div>
</section>
```

- Heading on top row; chart-left (7fr) + text-right (5fr) below.
- `.slide-media--chart` drops the border/radius so the chart reads edge-to-edge.
- Exactly one chart per slide. Style chart colors per `data-visualization-guide.md`.

---

## Speaker notes

Add a single `<script type="application/json" id="speaker-notes">` in the deck body. Contents: a JSON array of strings, one per slide, indexed in DOM order.

```html
<script type="application/json" id="speaker-notes">
  [
    "Cover — welcome, introduce the thesis.",
    "Section — set up Part 02.",
    "Content — explain the point in 30 seconds.",
    "Split — the chart shows X; emphasize Y.",
    "Three-column — walk through A, B, C.",
    "Chart — land the takeaway."
  ]
</script>
```

`gen_pptx` reads this automatically and attaches each string to its matching slide. Keep notes plain text — no HTML, no markdown. One paragraph per slide is plenty.

---

## `<deck-stage>` reference

`<deck-stage>` is a vanilla custom element that wraps the deck, scales the 1920×1080 canvas to fit the viewport, handles navigation, and cooperates with the export tool. Slides must be **direct children** of `<deck-stage>` — it toggles `data-deck-active` on them.

### Attributes

| Attribute   | Default | Effect                                                                                                                                                                                 |
| ----------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `width`     | `1920`  | Canvas width in px.                                                                                                                                                                    |
| `height`    | `1080`  | Canvas height in px.                                                                                                                                                                   |
| `active`    | `0`     | 0-indexed active slide; reflects through `goTo`.                                                                                                                                       |
| `loop`      | (off)   | `next`/`prev` wrap at deck ends.                                                                                                                                                       |
| `presenter` | (off)   | Opt in to single-slide presenter mode (scaled to viewport, centered, chrome visible, keyboard nav). Without it, the default is scrollable stacked.                                     |
| `nochrome`  | (off)   | Force-hide chrome (tap zones, progress, counter). Only meaningful in presenter mode; in the default mode chrome is already hidden. Required for screenshot-mode export.                |
| `noscale`   | (off)   | Single-slide natural-size mode for export. Overrides both `presenter` and the scrollable default. `gen_pptx` editable export sets this automatically via `resetTransformSelector: "deck-stage"`. |

### Methods

| Method              | Description                                                                   |
| ------------------- | ----------------------------------------------------------------------------- |
| `goTo(n)`           | Navigate to slide `n` (0-indexed). Clamps to valid range (wraps with `loop`). |
| `next()` / `prev()` | Convenience around `goTo`.                                                    |
| `count` (getter)    | Number of direct-child slides.                                                |

### Events (bubble + composed)

| Event                 | Detail                       | Fired                                                  |
| --------------------- | ---------------------------- | ------------------------------------------------------ |
| `deck-ready`          | —                            | After `document.fonts.ready` resolves (or at timeout). |
| `deck-change`         | `{ index, total, previous }` | On every `goTo` call.                                  |
| `deck-noscale-change` | `{ on }`                     | When the `noscale` attribute toggles.                  |

### Keyboard

| Keys                          | Action                    |
| ----------------------------- | ------------------------- |
| `→`, `Space`, `PageDown`, `n` | Next                      |
| `←`, `PageUp`, `p`            | Previous                  |
| `Home` / `End`                | First / last              |
| `1`–`9`                       | Jump to slide (1-indexed) |
| `f`                           | Toggle fullscreen         |

The host element is focusable (`tabindex="0"` set automatically). Click the deck once to give it focus before keyboard nav works.

### Rendering modes

The default is a **scrollable stacked view**: all slides visible, each scaled by CSS `zoom` to fit the host width. This is the right choice for preview viewers, chat sidebars, embedded iframes, and any context narrower than a full window. It's also what the no-JS fallback renders — so the deck looks the same whether `<deck-stage>` successfully upgrades as a custom element or not. That matters because many file previewers (macOS Quick Look, IDE hover previews, some chat sidebars) don't execute JavaScript at all.

For a full-screen talk or a dedicated deck viewer, add `presenter`:

```html
<deck-stage presenter>
  <section class="slide slide--cover">…</section>
  <section class="slide slide--content">…</section>
  …
</deck-stage>
```

Presenter mode shows one slide at a time at fit-to-viewport scale, centered, with chrome (counter, progress bar, tap zones) and keyboard navigation (`→` / `←` / `Space` / `1`–`9` / `f` for fullscreen).

Either mode authors the same way — same `.slide` templates, tokens, typography. The attribute only changes layout.

#### Export always wins

`noscale` (set by `gen_pptx` editable export via `resetTransformSelector: "deck-stage"`) overrides both modes and forces single-slide natural-size rendering. Decks are still exportable from either authoring mode with the same arg shape shown below.

#### Browser support

Both default scrollable mode and the no-JS fallback use CSS `zoom` (all current browsers since Firefox 126 / May 2024) + container query units `100cqw` (all current browsers since 2022). If your target includes browsers older than mid-2024, add `presenter` to force single-slide mode, which only relies on `transform: scale` (supported everywhere).

### Verifying the deck

Before reporting a deck complete:

1. Open the HTML in a **full browser** (Chrome / Safari / Firefox) and confirm the scrollable view renders — all slides visible, each scaled to the window width, scroll to navigate. This confirms the `.slide` CSS loaded and the custom element upgraded.
2. If the deck will be viewed in a JS-restricted file previewer (macOS Quick Look, IDE previews, some chat sidebars), the `:not(:defined)` fallback should render the same scrollable view. A truly blank page in such a viewer means the `.slide` CSS didn't load — usually because the deck was hand-written without the bundled styles. Rebuild from `starter.html`.
3. To verify presenter mode, add `presenter` to `<deck-stage>` temporarily (or via DevTools: `$0.setAttribute('presenter','')`) and confirm keyboard nav + centered single-slide rendering. Remove before shipping unless presenter is the deck's intended default.

---

## PPTX export workflow

Two modes. Two ways to invoke them:

- **Agent harness `gen_pptx` tool** — if your agent environment provides it, use that. The definitive reference is the user's own two guides (`Export as PPTX (editable).md` and `Export as PPTX (screenshots).md`).
- **Local fallback CLI** — this repo ships a reference implementation at [`scripts/gen-pptx.js`](../../scripts/gen-pptx.js) that produces matching output using Playwright + pptxgenjs. See "Local fallback" below.

Both paths accept the same arg shape. The shapes below apply to either.

### Editable mode (default — editable text + shapes)

```jsonc
{
  "width": 1920,
  "height": 1080,
  "resetTransformSelector": "deck-stage",
  "slides": [
    {
      "showJs": "document.querySelector('deck-stage').goTo(0)",
      "selector": "deck-stage > [data-deck-active]",
    },
    {
      "showJs": "document.querySelector('deck-stage').goTo(1)",
      "selector": "deck-stage > [data-deck-active]",
    },
    // …one entry per slide in DOM order
  ],
  "filename": "my-deck",
}
```

Notes:

- `resetTransformSelector: "deck-stage"` sets the `noscale` attribute on the host; `<deck-stage>` drops its scale transform in response. Don't fight it.
- Shadow-DOM chrome (tap zones, progress, counter) is not captured in editable mode.
- Omit `googleFontImports` and `fontSwaps` — see "Fonts" below.

### Screenshot mode (pixel-perfect, non-editable)

```jsonc
{
  "mode": "screenshots",
  "width": 1920,
  "height": 1080,
  "slides": [
    {
      "showJs": "document.querySelector('deck-stage').setAttribute('noscale',''); document.querySelector('deck-stage').setAttribute('nochrome',''); document.querySelector('deck-stage').goTo(0)",
      "selector": "body",
    },
    {
      "showJs": "document.querySelector('deck-stage').goTo(1)",
      "selector": "body",
    },
    // …
  ],
  "filename": "my-deck",
}
```

**Critical:** the first slide's `showJs` must set both `noscale` and `nochrome` on `<deck-stage>`. Without `noscale`, the default scrollable rendering would capture the _top of the stacked view_ instead of slide N. Without `nochrome`, the tap zones, progress bar, and counter appear in the exported PNGs.

### Fonts

The default in this design system is **brand fonts as-is** (Avenir Next + Geist Mono). Omit `googleFontImports` and `fontSwaps`. `starter.html` already inlines `@font-face` declarations pointing at the hosted font files on `design.mirrorphysics.com`, with `system-ui` fallback in `--font-sans` so the deck renders legibly even if the font host is unreachable.

If the deck's audience won't have the brand fonts available and you need fallbacks in the exported `.pptx`, pass the standard strategies described in the user's `Export as PPTX (editable).md` font-strategy section.

### Local fallback

If your agent environment doesn't provide `gen_pptx`, use the bundled CLI. It's a Node reference implementation (Playwright + pptxgenjs) that matches the arg shape above.

**With repo access** (you cloned or downloaded the repo):

```bash
npm install            # if you haven't already
npx playwright install chromium
npm run gen-pptx -- path/to/deck.html --mode editable --output deck.pptx
# or
npm run gen-pptx -- path/to/deck.html --mode screenshots --output deck.pptx
```

**Standalone** (no repo access — grab the single file and run it anywhere):

```bash
curl -O https://design.mirrorphysics.com/scripts/gen-pptx.js
npm init -y
npm install pptxgenjs playwright
npx playwright install chromium
node gen-pptx.js path/to/deck.html --mode editable
```

CLI flags (see the top of `scripts/gen-pptx.js` for full docs):

| Flag               | Default               | Use                             |
| ------------------ | --------------------- | ------------------------------- |
| `--mode`           | `editable`            | `editable` or `screenshots`     |
| `--output`         | `<deck>.pptx`         | Output file path                |
| `--width`          | `1920`                | Canvas width in px              |
| `--height`         | `1080`                | Canvas height in px             |
| `--slide-selector` | `deck-stage > .slide` | CSS selector for slide elements |
| `--verbose`        | off                   | Log per-slide progress          |

Behavior parity with harness `gen_pptx`:

- Sets `noscale` on `<deck-stage>` before editable export so the canvas renders at natural 1920×1080.
- Calls `goTo(N)` between slides; waits for `document.fonts.ready` before each capture.
- Reads speaker notes from `<script type="application/json" id="speaker-notes">` and attaches them via `slide.addNotes(...)`.
- Screenshot mode captures the full 1920×1080 viewport and drops each PNG full-bleed.
- Editable mode walks the DOM for text (emitted as text boxes), `<img>` (emitted as images), and elements with solid background colors (emitted as rectangles). Gradients, filters, and transforms are **not** translated — screenshot mode captures those faithfully; editable mode silently skips them. If a slide relies heavily on decorative gradients, use screenshot mode.

---

## Accessibility

- **Keyboard navigation** is handled by `<deck-stage>` (see keyboard table above).
- **Semantic HTML** inside slides: `<h1>` for the cover title, `<h2>` for per-slide headings, `<figure>` + `<figcaption>` for captioned media, `<ul>`/`<ol>` for lists.
- **Alt text** on every `<img>`. Charts get descriptive alt text that summarizes the trend, not "chart".
- **Chrome is `aria-hidden="true"`** in `<deck-stage>` — the counter and progress bar are decorative.
- **Contrast** follows `color-guide.md`. Body text on `--bg-canvas` meets WCAG AA at 28 px easily.
- **Reduced motion**: decks do not animate slide transitions. The only animation is the progress bar width, which is disabled under `prefers-reduced-motion: reduce`.

---

## Choosing a template

| Need                                              | Template        |
| ------------------------------------------------- | --------------- |
| Deck opener                                       | Cover           |
| Chapter break between narrative sections          | Section divider |
| Single point with supporting prose or bullets     | Content         |
| One image supporting one point                    | Split           |
| Three parallel items (features, options, results) | Three-column    |
| One hero chart + commentary                       | Chart-focused   |

If none of the templates fit, build a custom layout using the tokens and utilities. Compose `.slide-stack`, `.slide-grid-12`, `.slide-media`, and the slide-scale type classes, or write scoped one-off CSS in the deck's `<style>` block. Overloaded slides (too much content for one page) are still usually better as two slides — but that's a content judgment, not a template-matching rule.

---

## When NOT to build a deck

- If the user wants an interactive web page or app UI, use the normal component system — not `<deck-stage>`.
- If the user wants a document (long-form prose, mixed media, scrollable), produce HTML or Markdown, not slides.
- If the user asks for "a powerpoint" and doesn't need HTML preview, still build the HTML first and export — the `gen_pptx` flow is the only supported path.
