Make a Deck
Build an HTML slide deck that looks designed, not AI-generated. Uses starters/deck_stage.js for keyboard nav, scaling, speaker notes, and print-to-PDF.
Phase 0 — Context pre-flight (auto-detect, ONE question max)
Before asking design questions, detect context silently via auto-checks. Only ask the user if nothing is found.
Auto-detect (no user input)
Read .claude/design-tokens.json — project tokens, if exist → load
Bash(ls ~/.claude/design-systems/ 2>/dev/null) — org-level registry. If the brief mentions a brand name matching a folder → auto-apply it (e.g. brief "Acme deck" + ~/.claude/design-systems/acme/ → use Acme)
Glob **/tailwind.config.* **/theme.{ts,js,json} **/tokens.{css,scss} **/_variables.* at project root — if found, note "codebase detected" and Read them
- Scan user message/attachments for:
- GitHub URL → invoke
Skill: ingest-github first
- Figma URL → invoke
Skill: ingest-figma first
- Image attachment → invoke
Skill: ingest-screenshot first
.md / .txt / .pdf file attached → Read it (may contain brand refs or content)
One-question fallback (only if nothing detected)
If no context found after auto-detect, use AskUserQuestion with a single question, text-options:
- Use existing design system — list names from
~/.claude/design-systems/ (if any)
- From codebase — "paste a local path or github URL"
- From screenshot — "attach an image"
- From Figma — "paste a figma URL (needs FIGMA_TOKEN)"
- No context — invoke
Skill: frontend-design for aesthetic-from-scratch
- Decide for me — Claude picks frontend-design
Report what was found/chosen in one line: "Using . Proceeding…"
Phase 1 — Ambiguity gate
Check the user's brief ($ARGUMENTS) for these signals:
- Audience — "for X", "for investors", "team", "stakeholders", etc.
- Style — named aesthetic ("New Yorker", "WSJ", "minimal", "bold", "Apple-like") or brand reference
- Length — "N slides", "short", "quick"
If ≥2 of 3 present → skip long questionnaire, proceed with max 1-2 clarifying questions.
If <2 present → invoke AskUserQuestion with:
- Audience (who's watching, emotional state, technical level)
- Tone (formal/casual, funny/serious)
- Length (slide count)
- Visuals (minimal/rich, photos/illustrations/charts)
- Variations (do they want 2-3 deck options or just one)
Ask at least 4 relevant questions even in gate-passed mode if anything is still unclear. Do not ask about brand context — Phase 0 already handled it.
Phase 1.5 — Speaker-notes heuristic (agent decides, no toggle)
Instead of asking user "use speaker notes?", decide from signals:
| Signal in brief |
→ speaker notes |
| "quick", "standup", "lightning", "recap", <100 chars |
off — self-sufficient slides, no notes |
| "for investors", "Board meeting", "keynote", "All Hands" |
on — sparse slides, notes carry narrative |
| Attached PRD / long doc (>500 words) |
on — slides distill, notes script |
| Only 1-3 slides requested |
off |
| 10+ slides requested |
on (probably a story arc) |
| Ambiguous / none of above |
ask a single yes/no question |
If on: generate <script type="application/json" id="speaker-notes">[...]</script> at the end of <body> with one string per slide — full conversational narrative, not bullet-point summary. Keep slides visual-heavy.
If off: skip notes entirely; slides must stand alone.
Phase 2 — Visual system
Using context gathered in Phase 0, commit to a visual system up front:
- Section-header layout, title layout, image layout
- Max 1-2 background colors across the deck
- Font hierarchy (display / H1 / body / caption)
- Palette (pull from loaded tokens, or generated via
Skill: frontend-design)
Vocalize the system in one paragraph before writing slides.
Phase 3 — Build
- Create
artifacts/<slug>.html with shell:<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>{{deck title}}</title>
<script src="./deck_stage.js"></script>
<style>/* your tokens + slide styles */</style>
</head>
<body>
<deck-stage width="1920" height="1080">
<section><!-- slide 1 --></section>
...
</deck-stage>
</body>
</html>
Bash(cp starters/deck_stage.js "$(dirname <html>)/") — copy starter into the same dir as the HTML (important for nested paths like artifacts/foo/deck.html)
- Write all slides in one pass. Rules:
- NO title-only slide as slide 1 (jump into content)
- Text ≥ 24px on 1920×1080
text-wrap: pretty
- Use CSS Grid for layouts
- No gradients, no emoji, no Inter/Roboto/Arial — pick character fonts
- No SVG-drawn imagery — placeholder divs with labels instead
- Vary layout rhythm across slides (not all same template)
- Speaker notes: follow Phase 1.5 decision — don't ask again.
3.1 Vertical budget — compute before you write (!)
<deck-stage> sections have overflow: hidden — overflow is visually silent, content just disappears. You must budget vertically up front.
Typical chrome layout on each slide consumes ~430px of the 1080 canvas:
| Item |
Reserved |
| Section padding top |
96px |
| Chrome row (abs, but visual weight) |
24px |
| Kicker margin-top + kicker |
44 + 22 = 66px |
h2.section-title (font-size 80–96px × 2 lines) |
~200px |
| h2 margin-bottom |
36px |
| Content grid margin-top |
16px |
| Section padding bottom |
96px |
| Total reserved |
~430px |
Content budget: 1080 − 430 = ~650px. Any primary card, row, or media container in the content area must fit inside this.
Hard caps to keep you honest:
- Primary card
min-height: ≤ 500px
- Post-mock / deep-content column
min-height: ≤ 620px
- Max 5 anatomy rows at ~110px each, or 4 at ~130px each
- Short h2 (1 line, ≤ 60 chars) → 140px budget frees up; long h2 (2 lines) → use 200px
- If you need taller content, drop the kicker or merge kicker+h2 on one line — don't let cards grow
If a slide feels too busy — split it or cut content. Overflow is a design smell, not a typography problem.
3.2 Russian / long-word hyphenation
Set hyphens: manual; -webkit-hyphens: manual; on <section> to prevent browsers from inserting soft-hyphens mid-word (breaks long Russian words across lines in narrow columns).
Phase 4 — Verify
/done artifacts/<slug>.html — opens in browser, checks console, saves screenshot
Fix any errors; re-run until clean
Mandatory programmatic overflow audit (blocks end-of-turn if any slide overflows or is too tight):
// Paste into mcp__chrome-devtools__evaluate_script
async () => {
const stage = document.querySelector('deck-stage');
if (!stage) return { skipped: 'no deck-stage' };
// Decoratives we deliberately ignore (bleed past section bounds by design):
// glow halos, hero backdrops, chrome overlays, anything flagged data-decorative.
const DECORATIVE = '.glow, .glow-2, .hero-glow, .chrome, [data-decorative], [aria-hidden="true"].backdrop';
const isDecorative = (el) => {
if (el.matches(DECORATIVE) || el.closest(DECORATIVE)) return true;
const cs = getComputedStyle(el);
// pointer-events:none + no text + low opacity → visual decoration, skip
if (cs.pointerEvents === 'none' && !el.textContent?.trim() && parseFloat(cs.opacity) < 1) return true;
return false;
};
const out = [];
for (let i = 0; i < stage.totalSlides; i++) {
stage.goToSlide(i);
await new Promise(r => setTimeout(r, 80));
const s = stage.querySelectorAll('section')[i];
const sRect = s.getBoundingClientRect();
const scale = 1080 / sRect.height;
// Measure ALL elements (including position:absolute like .arrow-between, .save-cta,
// .footer-row that ARE meaningful content) — only decoratives are skipped.
let maxBottom = 0, culprit = null;
for (const el of s.querySelectorAll('*')) {
if (isDecorative(el)) continue;
const cs = getComputedStyle(el);
if (cs.display === 'none' || cs.visibility === 'hidden') continue;
const r = el.getBoundingClientRect();
if (r.height === 0 && r.width === 0) continue;
const b = (r.bottom - sRect.top) * scale;
if (b > maxBottom) { maxBottom = b; culprit = el.className?.toString?.().slice(0, 40) || el.tagName; }
}
const contentBottom = Math.round(maxBottom);
const overflow = contentBottom - 1080;
const headroom = 1080 - contentBottom;
const status = overflow > 0 ? 'FAIL' : headroom < 40 ? 'WARN' : 'OK';
out.push({ slide: i + 1, contentBottom, overflow, headroom, status, culprit });
}
stage.goToSlide(0);
return out;
}
FAIL (overflow > 0) — content clipped. Do not claim done. Shrink cards / fonts / padding, then re-run.
WARN (headroom < 40px) — visually too close to the edge (any small font-metric variance clips). Aim for ≥ 60px headroom.
OK — ship.
Common fixes: reduce min-height on cards, shrink headline font, drop a row, merge kicker+title, tighten vertical gaps.
If the browser shows an overflow that this script says doesn't exist, hard-reload with cache-bust: mcp__chrome-devtools__navigate_page({ type: "reload", ignoreCache: true }). Most such disagreements come from a cached CSS state prior to your latest Edit.
Invoke Skill: verify-artifact silently in background (vision check on layout — it handles per-slide screenshots automatically when it detects <deck-stage>)
Reference .claude/last-preview.png in end-of-turn summary
Phase 5 — Offer next steps
/export-pptx artifacts/<slug>.html — native PPTX via screenshots
/export-pdf artifacts/<slug>.html — PDF
/export-standalone artifacts/<slug>.html <dist>.html — single-file offline HTML
/register-asset artifacts/<slug>.html — add to assets.html overview
- Speaker notes mode → add
<script type="application/json" id="speaker-notes">[...]</script>
- Tweakable mode →
/make-tweakable artifacts/<slug>.html
Anti-patterns — hard prohibited
- Title slide first
- Filler content
- AI-slop card patterns (rounded + left border accent)
- Generic gradient backgrounds
- Inventing numbers/stats
- Inventing colors (use
oklch() to harmonize with existing palette)
1---2name: make-deck3description: Build an HTML slide deck (1920×1080, keyboard nav, exportable) when user asks for a presentation, pitch deck, slides, or keynote. Uses deck_stage.js starter and Claude Design taste rules.4---56# Make a Deck78Build an HTML slide deck that looks designed, not AI-generated. Uses `starters/deck_stage.js` for keyboard nav, scaling, speaker notes, and print-to-PDF.910## Phase 0 — Context pre-flight (auto-detect, ONE question max)1112Before asking design questions, detect context **silently** via auto-checks. Only ask the user if nothing is found.1314### Auto-detect (no user input)151. `Read .claude/design-tokens.json` — project tokens, if exist → load162. `Bash(ls ~/.claude/design-systems/ 2>/dev/null)` — org-level registry. If the brief mentions a brand name matching a folder → auto-apply it (e.g. brief "Acme deck" + `~/.claude/design-systems/acme/` → use Acme)173. `Glob **/tailwind.config.* **/theme.{ts,js,json} **/tokens.{css,scss} **/_variables.*` at project root — if found, note "codebase detected" and `Read` them184. Scan user message/attachments for:19 - GitHub URL → invoke `Skill: ingest-github` first20 - Figma URL → invoke `Skill: ingest-figma` first21 - Image attachment → invoke `Skill: ingest-screenshot` first22 - `.md` / `.txt` / `.pdf` file attached → `Read` it (may contain brand refs or content)2324### One-question fallback (only if nothing detected)25If no context found after auto-detect, use `AskUserQuestion` with **a single question**, text-options:26- **Use existing design system** — list names from `~/.claude/design-systems/` (if any)27- **From codebase** — "paste a local path or github URL"28- **From screenshot** — "attach an image"29- **From Figma** — "paste a figma URL (needs FIGMA_TOKEN)"30- **No context** — invoke `Skill: frontend-design` for aesthetic-from-scratch31- **Decide for me** — Claude picks frontend-design3233Report what was found/chosen in one line: "Using <context>. Proceeding…"3435## Phase 1 — Ambiguity gate3637Check the user's brief (`$ARGUMENTS`) for these signals:38- **Audience** — "for X", "for investors", "team", "stakeholders", etc.39- **Style** — named aesthetic ("New Yorker", "WSJ", "minimal", "bold", "Apple-like") or brand reference40- **Length** — "N slides", "short", "quick"4142If **≥2 of 3 present** → skip long questionnaire, proceed with max 1-2 clarifying questions.4344If **<2 present** → invoke `AskUserQuestion` with:45- Audience (who's watching, emotional state, technical level)46- Tone (formal/casual, funny/serious)47- Length (slide count)48- Visuals (minimal/rich, photos/illustrations/charts)49- Variations (do they want 2-3 deck options or just one)5051Ask at least 4 relevant questions even in gate-passed mode if anything is still unclear. **Do not** ask about brand context — Phase 0 already handled it.5253## Phase 1.5 — Speaker-notes heuristic (agent decides, no toggle)5455Instead of asking user "use speaker notes?", decide from signals:5657| Signal in brief | → speaker notes |58|---|---|59| "quick", "standup", "lightning", "recap", <100 chars | **off** — self-sufficient slides, no notes |60| "for investors", "Board meeting", "keynote", "All Hands" | **on** — sparse slides, notes carry narrative |61| Attached PRD / long doc (>500 words) | **on** — slides distill, notes script |62| Only 1-3 slides requested | **off** |63| 10+ slides requested | **on** (probably a story arc) |64| Ambiguous / none of above | ask a single yes/no question |6566If on: generate `<script type="application/json" id="speaker-notes">[...]</script>` at the end of `<body>` with one string per slide — full conversational narrative, not bullet-point summary. Keep slides visual-heavy.6768If off: skip notes entirely; slides must stand alone.6970## Phase 2 — Visual system7172Using context gathered in Phase 0, commit to a visual system up front:73- Section-header layout, title layout, image layout74- Max 1-2 background colors across the deck75- Font hierarchy (display / H1 / body / caption)76- Palette (pull from loaded tokens, or generated via `Skill: frontend-design`)7778Vocalize the system in one paragraph before writing slides.7980## Phase 3 — Build81821. Create `artifacts/<slug>.html` with shell:83 ```html84 <!doctype html>85 <html lang="en">86 <head>87 <meta charset="utf-8"/>88 <title>{{deck title}}</title>89 <script src="./deck_stage.js"></script>90 <style>/* your tokens + slide styles */</style>91 </head>92 <body>93 <deck-stage width="1920" height="1080">94 <section><!-- slide 1 --></section>95 ...96 </deck-stage>97 </body>98 </html>99 ```1002. `Bash(cp starters/deck_stage.js "$(dirname <html>)/")` — copy starter into the **same dir as the HTML** (important for nested paths like `artifacts/foo/deck.html`)1013. Write all slides in one pass. Rules:102 - NO title-only slide as slide 1 (jump into content)103 - Text ≥ 24px on 1920×1080104 - `text-wrap: pretty`105 - Use CSS Grid for layouts106 - No gradients, no emoji, no Inter/Roboto/Arial — pick character fonts107 - No SVG-drawn imagery — placeholder divs with labels instead108 - Vary layout rhythm across slides (not all same template)1094. Speaker notes: follow Phase 1.5 decision — don't ask again.110111### 3.1 Vertical budget — compute before you write (!)112113`<deck-stage>` sections have `overflow: hidden` — overflow is visually **silent**, content just disappears. You must budget vertically up front.114115Typical chrome layout on each slide consumes ~430px of the 1080 canvas:116117| Item | Reserved |118|---|---|119| Section padding top | 96px |120| Chrome row (abs, but visual weight) | 24px |121| Kicker margin-top + kicker | 44 + 22 = 66px |122| `h2.section-title` (font-size 80–96px × 2 lines) | ~200px |123| h2 margin-bottom | 36px |124| Content grid margin-top | 16px |125| Section padding bottom | 96px |126| **Total reserved** | **~430px** |127128**Content budget: 1080 − 430 = ~650px.** Any primary card, row, or media container in the content area must fit inside this.129130Hard caps to keep you honest:131- Primary card `min-height`: **≤ 500px**132- Post-mock / deep-content column `min-height`: **≤ 620px**133- Max 5 anatomy rows at ~110px each, or 4 at ~130px each134- Short h2 (1 line, ≤ 60 chars) → 140px budget frees up; long h2 (2 lines) → use 200px135- If you need taller content, drop the kicker or merge kicker+h2 on one line — don't let cards grow136137If a slide feels too busy — split it or cut content. Overflow is a design smell, not a typography problem.138139### 3.2 Russian / long-word hyphenation140141Set `hyphens: manual; -webkit-hyphens: manual;` on `<section>` to prevent browsers from inserting soft-hyphens mid-word (breaks long Russian words across lines in narrow columns).142143## Phase 4 — Verify1441451. `/done artifacts/<slug>.html` — opens in browser, checks console, saves screenshot1462. Fix any errors; re-run until clean1473. **Mandatory programmatic overflow audit** (blocks end-of-turn if any slide overflows or is too tight):148149 ```js150 // Paste into mcp__chrome-devtools__evaluate_script151 async () => {152 const stage = document.querySelector('deck-stage');153 if (!stage) return { skipped: 'no deck-stage' };154 // Decoratives we deliberately ignore (bleed past section bounds by design):155 // glow halos, hero backdrops, chrome overlays, anything flagged data-decorative.156 const DECORATIVE = '.glow, .glow-2, .hero-glow, .chrome, [data-decorative], [aria-hidden="true"].backdrop';157 const isDecorative = (el) => {158 if (el.matches(DECORATIVE) || el.closest(DECORATIVE)) return true;159 const cs = getComputedStyle(el);160 // pointer-events:none + no text + low opacity → visual decoration, skip161 if (cs.pointerEvents === 'none' && !el.textContent?.trim() && parseFloat(cs.opacity) < 1) return true;162 return false;163 };164 const out = [];165 for (let i = 0; i < stage.totalSlides; i++) {166 stage.goToSlide(i);167 await new Promise(r => setTimeout(r, 80));168 const s = stage.querySelectorAll('section')[i];169 const sRect = s.getBoundingClientRect();170 const scale = 1080 / sRect.height;171 // Measure ALL elements (including position:absolute like .arrow-between, .save-cta,172 // .footer-row that ARE meaningful content) — only decoratives are skipped.173 let maxBottom = 0, culprit = null;174 for (const el of s.querySelectorAll('*')) {175 if (isDecorative(el)) continue;176 const cs = getComputedStyle(el);177 if (cs.display === 'none' || cs.visibility === 'hidden') continue;178 const r = el.getBoundingClientRect();179 if (r.height === 0 && r.width === 0) continue;180 const b = (r.bottom - sRect.top) * scale;181 if (b > maxBottom) { maxBottom = b; culprit = el.className?.toString?.().slice(0, 40) || el.tagName; }182 }183 const contentBottom = Math.round(maxBottom);184 const overflow = contentBottom - 1080;185 const headroom = 1080 - contentBottom;186 const status = overflow > 0 ? 'FAIL' : headroom < 40 ? 'WARN' : 'OK';187 out.push({ slide: i + 1, contentBottom, overflow, headroom, status, culprit });188 }189 stage.goToSlide(0);190 return out;191 }192 ```193194 - `FAIL` (`overflow > 0`) — content clipped. **Do not claim done.** Shrink cards / fonts / padding, then re-run.195 - `WARN` (`headroom < 40px`) — visually too close to the edge (any small font-metric variance clips). Aim for ≥ 60px headroom.196 - `OK` — ship.197198 Common fixes: reduce `min-height` on cards, shrink headline font, drop a row, merge kicker+title, tighten vertical gaps.199200 > **If the browser shows an overflow that this script says doesn't exist**, hard-reload with cache-bust: `mcp__chrome-devtools__navigate_page({ type: "reload", ignoreCache: true })`. Most such disagreements come from a cached CSS state prior to your latest `Edit`.2012024. Invoke `Skill: verify-artifact` silently in background (vision check on layout — it handles per-slide screenshots automatically when it detects `<deck-stage>`)2035. Reference `.claude/last-preview.png` in end-of-turn summary204205## Phase 5 — Offer next steps206207- `/export-pptx artifacts/<slug>.html` — native PPTX via screenshots208- `/export-pdf artifacts/<slug>.html` — PDF209- `/export-standalone artifacts/<slug>.html <dist>.html` — single-file offline HTML210- `/register-asset artifacts/<slug>.html` — add to `assets.html` overview211- Speaker notes mode → add `<script type="application/json" id="speaker-notes">[...]</script>`212- Tweakable mode → `/make-tweakable artifacts/<slug>.html`213214## Anti-patterns — hard prohibited215216- Title slide first217- Filler content218- AI-slop card patterns (rounded + left border accent)219- Generic gradient backgrounds220- Inventing numbers/stats221- Inventing colors (use `oklch()` to harmonize with existing palette)