Threads Carousel Generator
Converts a text post into a set of visual carousel slides for Threads, Instagram, LinkedIn, TikTok, Stories, YouTube, or a standalone presentation deck. Composable design system with three independent style axes (font × color × purpose) and 12 slide types.
Invocation
/threads-carousel <post text>
/threads-carousel path/to/post.md
Format presets (choose target platform)
| Preset |
Size |
Platforms |
threads-4x5 (default) |
1080×1350 |
Threads, Instagram feed (portrait) |
instagram-square |
1080×1080 |
Instagram, Facebook, LinkedIn feed |
linkedin-square |
1080×1080 |
LinkedIn document post (PDF) |
tiktok-9x16 |
1080×1920 |
TikTok Photo Mode, Reels, Shorts |
story-9x16 |
1080×1920 |
Instagram Stories, Threads Stories |
wide-16x9 |
1920×1080 |
Presentations, YouTube, desktop decks |
Slide types (12)
| Type |
Purpose |
Required fields |
hook |
Opening slide — the catchiest line |
text |
body |
Title + paragraph |
title, text |
body (points) |
Pros/cons list with ✓/✗ SVG icons |
title, points[] (instead of text) |
list |
Numbered items (ordered list) |
title, items[] |
stats |
Big numbers with labels |
title, stats[] |
quote |
Large pulled quote |
text, author |
checklist |
Checkmark bullets |
title, items[] |
process |
Numbered steps with connector line |
title, steps[] |
comparison |
Two-column VS / before-after |
leftLabel, leftItems[], rightLabel, rightItems[] |
cta |
Final call to action |
text, handle |
image |
Title + screenshot/photo + optional caption |
imageSrc, optional title, imageCaption |
emoji |
Giant emoji illustration + title + text |
emoji, optional title, text |
number |
Huge hero number/string + title + text |
bigNumber, optional title, text |
points shape: Array<{ type: "plus" | "minus"; text: string }> — green ✓ for plus, muted ✗ for minus. Adaptive sizing (44–62px) based on item count and longest line.
All types also support optional:
badge — small outlined tag above title (e.g. "01", "TIP")
highlight — a word or phrase within text/title that will be colored in the preset's highlight color (yellow for dark themes)
highlightStyle: "italic-box" — renders the highlighted word in Playfair italic on a colored rectangle (instead of plain color)
Images in slides
image slides expect imageSrc as a path served by Next.js under /public/. Workflow when a user gives you a local file:
- User drops a file path like
/Users/me/Desktop/screenshot.png (or passes a file via the chat).
- Copy it into
template/public/images/ with a safe, lowercase filename — e.g. cp "$USER_PATH" template/public/images/screenshot.png.
- In
slides.ts reference it as imageSrc: "/images/screenshot.png" (absolute from /public/).
Same-origin serving avoids CORS errors in the PNG export pipeline. Do not use external URLs — html-to-image will often blank them out.
Background decorations (8 types)
Switchable via toolbar in preview. Default: glow.
| Type |
What it is |
none |
Solid background |
blobs |
Organic colored shapes |
grid |
Dotted grid pattern |
lines |
Diagonal line pattern |
paper |
Ruled notebook lines + left margin (use with paper/light/white colors for a literary feel) |
noise |
SVG grain overlay (overlay blend) |
bignumber |
Giant slide index as watermark (01, 02…) |
glow (default) |
Soft radial gradient in alternating corners |
Style system (3 independent axes)
The final style is composed at runtime from three axes via composePreset(font, color, purpose):
Font axis (DEFAULT_FONT), 5 typefaces:
| Id |
Font |
Feel |
minimal (default) |
Unbounded (body + hook) |
Geometric display, bold, distinctive |
editorial |
Playfair Display |
Classic serif, literary |
clean |
Inter |
Neutral sans-serif, most standard |
mono |
JetBrains Mono |
Monospace, tech/dev feel |
condensed |
Oswald |
Narrow + tall, editorial poster |
Surface axis (DEFAULT_SURFACE) — bg + text neutrals, 8 options:
| Id |
Bg |
Text |
Feel |
dark (default) |
#0A0A0A |
white |
default bold |
white |
pure #FFFFFF |
near-black |
sharp clinical, Apple-style |
light |
cool zinc #F4F4F5 |
near-black |
neutral cool grey, Vercel-style |
paper |
cream #ECE2C8 |
warm brown |
notebook / literary warm |
gradient |
purple→pink→amber |
white |
bright gradient |
pastel |
lilac #EDE9FE |
indigo |
soft |
neon |
dark gradient |
light cyan |
tech dark |
ember |
black→red radial |
near-white |
dramatic |
Accent axis (DEFAULT_ACCENT) — pop color for highlighted words, 8 options:
| Id |
Hex |
Matches surfaces |
yellow (default) |
#FACC15 |
dark, ember |
red |
#DC2626 |
pure red — white, light, paper |
teal |
#14B8A6 |
versatile — dark, white, light, paper |
coral |
#FB7185 |
pink-family — paper, light, white |
orange |
#F97316 |
pumpkin — paper, light, dark |
violet |
#A78BFA |
dark, neon, ember |
lime |
#D9F056 |
ember, dark |
blue |
#3B82F6 |
white, light, paper, dark |
fuchsia |
#C026D3 |
pastel, white, dark |
pink |
#EC4899 |
pastel, dark, light |
amber |
#F59E0B |
gradient, dark, paper |
Total combinations: 8 surfaces × 8 accents × 5 fonts × 2 purposes = 640 valid styles. Pick surface for neutral base, accent for pop color — independently.
Reference combos (inspired by design refs):
dark + teal — noir / minimalist tech
paper + orange — literary warm
ember + lime — dramatic announcement
white + coral — sharp editorial
light + teal — calm informational
Purpose axis (DEFAULT_PURPOSE):
| Id |
Title |
Body |
Divider |
carousel (default) |
44px, weight 800, UPPERCASE |
weight 600, textColor, line-height 1.2 |
visible (96×4px accent) |
presentation |
72px, weight 700, sentence case |
weight 400, textSecondary, line-height 1.45 |
hidden |
Pick purpose: "presentation" + format: "wide-16x9" for a desktop / YouTube presentation deck. Any 3 × 8 × 2 = 48 combinations are valid.
Workflow
Step 1 — Get the text
- If passed inline — use it.
- If a file path (
.md, .txt) — read it.
- If text is long (>500 chars) — confirm it's complete before planning.
Step 2 — Clarify parameters (optional, can assume defaults)
Ask once, combined:
- Number of slides (3–10, default 6)
- Format (see table above, default
threads-4x5; use wide-16x9 for a presentation deck)
- Purpose (
carousel or presentation, default carousel)
- Font (
minimal / editorial / clean, default minimal)
- Color (8 palettes, default
dark)
- Handle for CTA slide (e.g.
@username)
Shortcut: if user says "presentation" / "презентация" / "slide deck" → default to purpose: presentation, format: wide-16x9, font: clean, color: white.
If user says "your call" — apply defaults, do not block.
Step 3 — Plan the slide breakdown
Show the user a preview list before generation:
Slide 1 (hook): "Headline..." [highlight: "word"]
Slide 2 (body): badge 01 — "Title" / "Text..."
Slide 3 (list): badge 02 — "iOS stack" / 3 items
Slide 4 (stats): badge 03 — "Growth" / 3 stats
Slide 5 (quote): "Quote..." — Author
Slide 6 (cta): "Final message" @username
Rules for splitting text into slides
- Hook = single most intriguing line from the post. 1–3 short lines. Works as a standalone thumbnail.
- Body slides = one idea each. Max 40 words. Max 5 lines. Never join two ideas with "and".
- Mix slide types for visual variety: prefer
list for enumerations, stats for numbers, quote for direct speech, comparison for VS/before-after. Don't make every slide a body.
- CTA = conclusion + follow handle. Centered, short.
Step 4 — Generate
Prepare working copy
WORK_DIR="/tmp/carousel-$(date +%s)"
rsync -a --exclude=node_modules ~/.claude/skills/threads-carousel/template/ "$WORK_DIR/"
ln -s ~/.claude/skills/threads-carousel/template/node_modules "$WORK_DIR/node_modules"
cd "$WORK_DIR"
Symlinking node_modules avoids the 350MB copy per run.
Inject content into src/slides.ts
All content + defaults live in src/slides.ts — never touch the engine (src/app/CarouselApp.tsx, src/lib/*). Edit the SLIDES array and the 5 default constants.
Full injection example:
import type { SlideData, BgType, FormatId, FontId, ColorThemeId, PurposeId } from "./lib/types";
export const SLIDES: SlideData[] = [
{ type: "hook", text: "Line one\nline two", highlight: "two" },
{ type: "body", badge: "01", title: "Title", text: "Body text...", highlight: "key" },
{ type: "body", badge: "02", title: "Pros & Cons", points: [
{ type: "plus", text: "One click to register" },
{ type: "plus", text: "Works on any background" },
{ type: "minus", text: "Requires provider setup" },
]},
{ type: "list", badge: "03", title: "Steps", items: ["First", "Second", "Third"] },
{ type: "stats", title: "Numbers", stats: [
{ value: "10×", label: "Faster" },
{ value: "50%", label: "Smaller" },
]},
{ type: "quote", text: "Big idea\nin few words", author: "Someone", role: "2026" },
{ type: "checklist", title: "Pre-flight", items: ["One", "Two", "Three"] },
{ type: "cta", text: "Last word", handle: "@username" },
];
export const DEFAULT_FONT: FontId = "minimal";
export const DEFAULT_COLOR: ColorThemeId = "dark";
export const DEFAULT_PURPOSE: PurposeId = "carousel";
export const DEFAULT_BG: BgType = "glow";
export const DEFAULT_FORMAT: FormatId = "threads-4x5";
Launch preview and export
bun dev --port 3333
Tell the user to open http://localhost:3333. They can:
- Switch Format / Mode / Font / Color / Background live via toolbar rows
- Toggle UI language RU / EN in the top-right
- Click PDF to download all slides in one file (JPEG-compressed, ~5–8 MB for 10 slides)
- Click PNG (a.k.a. "Export All") to download every slide as
01-hook.png, 02-body.png, …
- Click an individual slide thumbnail to export just that one as PNG
After export, stop the dev server.
Parallel carousels
If making multiple carousels at once: create multiple work dirs and launch on ports 3333, 3334, 3335 — they run side by side without conflict.
Design system
Canonical look (font: minimal, color: dark, purpose: carousel):
- Display typeface: Unbounded (Google Fonts), for hooks — applied only when the font axis provides
hookFontFamily
- Body typeface: Space Grotesk (minimal) / Playfair (editorial) / Inter (clean) — everything else uses
preset.fontFamily
- Palette (dark):
#0A0A0A bg, #FFFFFF text, #FACC15 highlight
- Layout: 80px padding, left-aligned, slide counter bottom-center
- Title discipline: carousel purpose — title → a 96×4px accent divider → body, with ≥64px breathing room above body. Presentation purpose — no divider, sentence case, 72px.
- Hook size: 104–140px, adaptive by character/line count
- Body text size: 48–88px, adaptive
- Text balance:
textWrap: "balance" on hook + title (no orphan words)
Typography table (carousel purpose)
The presentation purpose overrides titles to 72px / 700 / sentence case and body to 400 / textSecondary / line-height 1.45.
| Element |
Size |
Weight |
Font source |
| Hook |
104–140px |
800 |
hookFontFamily ?? fontFamily |
| Title |
44px |
800 uppercase |
fontFamily |
| Body |
48–88px |
600 |
fontFamily |
| Points (pros/cons) |
44–62px |
600 |
fontFamily |
| Badge |
26px |
800 uppercase |
fontFamily |
| Stats value |
140–170px |
900 |
fontFamily |
| Stats label |
32px |
500 uppercase |
fontFamily |
| Quote |
62px |
600 |
fontFamily |
| List item |
46px |
600 |
fontFamily (numbers 48px) |
| Checklist item |
44px |
600 |
fontFamily |
| Process step title |
36px |
700 |
fontFamily |
| Handle |
36px |
500 |
fontFamily |
Common mistakes
| Mistake |
Fix |
| Too much text on a slide |
Max 40 words, max 5 lines |
| Two ideas on one slide |
Split into two slides |
All slides are body type |
Mix in list, stats, quote, checklist for visual variety |
| No hook on first slide |
Slide 1 must be the catchiest line |
| No CTA on last slide |
Slide N must end with a handle or call to action |
| Highlight word too long |
Keep highlight to 1–2 words, not whole phrase |
| Badge has too many characters |
Max 2–4 characters (01, TIP, NEW) |
Future work / TODOs
- Satori server-side export — replace browser-based
html-to-image with Satori + Resvg for CLI export (bun run export → out/*.png). Enables headless runs. See Slashgear/linkedin-carousel-gen for reference.
- Per-slide background override — currently
DEFAULT_BG is global; could accept a per-slide bg field to mix decorations across a deck.
- Cyrillic-optimized adaptive sizing — current thresholds are calibrated for Latin; Russian copy tends to be 20–30% longer at the same font size.
- Pencil MCP mode — previous skill version had a manual design mode via Pencil; not currently implemented in the template.
1---2name: threads-carousel3description: Convert text posts into visual carousel images or presentations for Threads, Instagram, LinkedIn, TikTok, YouTube. 12 slide types (incl. image/emoji/number), 6 format presets (incl. 1920×1080 wide), 8 background styles (incl. ruled paper), 3-axis style system (font × color × purpose), highlighted keywords with optional italic-box style. Generates PNG or single-file PDF via Next.js preview + browser export. RU/EN toolbar. Triggers: threads carousel, instagram carousel, linkedin carousel, tiktok carousel, карусель, slides, carousel images, presentation deck, presentation pdf.4---56# Threads Carousel Generator78Converts a text post into a set of visual carousel slides for Threads, Instagram, LinkedIn, TikTok, Stories, YouTube, or a standalone presentation deck. Composable design system with three independent style axes (font × color × purpose) and 12 slide types.910## Invocation1112```13/threads-carousel <post text>14/threads-carousel path/to/post.md15```1617## Format presets (choose target platform)1819| Preset | Size | Platforms |20|---|---|---|21| `threads-4x5` *(default)* | 1080×1350 | Threads, Instagram feed (portrait) |22| `instagram-square` | 1080×1080 | Instagram, Facebook, LinkedIn feed |23| `linkedin-square` | 1080×1080 | LinkedIn document post (PDF) |24| `tiktok-9x16` | 1080×1920 | TikTok Photo Mode, Reels, Shorts |25| `story-9x16` | 1080×1920 | Instagram Stories, Threads Stories |26| `wide-16x9` | 1920×1080 | Presentations, YouTube, desktop decks |2728## Slide types (12)2930| Type | Purpose | Required fields |31|---|---|---|32| `hook` | Opening slide — the catchiest line | `text` |33| `body` | Title + paragraph | `title`, `text` |34| `body` (points) | Pros/cons list with ✓/✗ SVG icons | `title`, `points[]` (instead of `text`) |35| `list` | Numbered items (ordered list) | `title`, `items[]` |36| `stats` | Big numbers with labels | `title`, `stats[]` |37| `quote` | Large pulled quote | `text`, `author` |38| `checklist` | Checkmark bullets | `title`, `items[]` |39| `process` | Numbered steps with connector line | `title`, `steps[]` |40| `comparison` | Two-column VS / before-after | `leftLabel`, `leftItems[]`, `rightLabel`, `rightItems[]` |41| `cta` | Final call to action | `text`, `handle` |42| `image` | Title + screenshot/photo + optional caption | `imageSrc`, optional `title`, `imageCaption` |43| `emoji` | Giant emoji illustration + title + text | `emoji`, optional `title`, `text` |44| `number` | Huge hero number/string + title + text | `bigNumber`, optional `title`, `text` |4546`points` shape: `Array<{ type: "plus" | "minus"; text: string }>` — green ✓ for plus, muted ✗ for minus. Adaptive sizing (44–62px) based on item count and longest line.4748All types also support optional:49- `badge` — small outlined tag above title (e.g. `"01"`, `"TIP"`)50- `highlight` — a word or phrase within `text`/`title` that will be colored in the preset's highlight color (yellow for dark themes)51- `highlightStyle: "italic-box"` — renders the highlighted word in Playfair italic on a colored rectangle (instead of plain color)5253### Images in slides5455`image` slides expect `imageSrc` as a path served by Next.js under `/public/`. Workflow when a user gives you a local file:56571. User drops a file path like `/Users/me/Desktop/screenshot.png` (or passes a file via the chat).582. Copy it into `template/public/images/` with a safe, lowercase filename — e.g. `cp "$USER_PATH" template/public/images/screenshot.png`.593. In `slides.ts` reference it as `imageSrc: "/images/screenshot.png"` (absolute from `/public/`).6061Same-origin serving avoids CORS errors in the PNG export pipeline. Do not use external URLs — `html-to-image` will often blank them out.6263## Background decorations (8 types)6465Switchable via toolbar in preview. Default: `glow`.6667| Type | What it is |68|---|---|69| `none` | Solid background |70| `blobs` | Organic colored shapes |71| `grid` | Dotted grid pattern |72| `lines` | Diagonal line pattern |73| `paper` | Ruled notebook lines + left margin (use with `paper`/`light`/`white` colors for a literary feel) |74| `noise` | SVG grain overlay (overlay blend) |75| `bignumber` | Giant slide index as watermark (01, 02…) |76| `glow` *(default)* | Soft radial gradient in alternating corners |7778## Style system (3 independent axes)7980The final style is composed at runtime from three axes via `composePreset(font, color, purpose)`:8182**Font axis** (`DEFAULT_FONT`), 5 typefaces:8384| Id | Font | Feel |85|---|---|---|86| `minimal` *(default)* | Unbounded (body + hook) | Geometric display, bold, distinctive |87| `editorial` | Playfair Display | Classic serif, literary |88| `clean` | Inter | Neutral sans-serif, most standard |89| `mono` | JetBrains Mono | Monospace, tech/dev feel |90| `condensed` | Oswald | Narrow + tall, editorial poster |9192**Surface axis** (`DEFAULT_SURFACE`) — bg + text neutrals, 8 options:9394| Id | Bg | Text | Feel |95|---|---|---|---|96| `dark` *(default)* | `#0A0A0A` | white | default bold |97| `white` | pure `#FFFFFF` | near-black | sharp clinical, Apple-style |98| `light` | cool zinc `#F4F4F5` | near-black | neutral cool grey, Vercel-style |99| `paper` | cream `#ECE2C8` | warm brown | notebook / literary warm |100| `gradient` | purple→pink→amber | white | bright gradient |101| `pastel` | lilac `#EDE9FE` | indigo | soft |102| `neon` | dark gradient | light cyan | tech dark |103| `ember` | black→red radial | near-white | dramatic |104105**Accent axis** (`DEFAULT_ACCENT`) — pop color for highlighted words, 8 options:106107| Id | Hex | Matches surfaces |108|---|---|---|109| `yellow` *(default)* | `#FACC15` | dark, ember |110| `red` | `#DC2626` | pure red — white, light, paper |111| `teal` | `#14B8A6` | versatile — dark, white, light, paper |112| `coral` | `#FB7185` | pink-family — paper, light, white |113| `orange` | `#F97316` | pumpkin — paper, light, dark |114| `violet` | `#A78BFA` | dark, neon, ember |115| `lime` | `#D9F056` | ember, dark |116| `blue` | `#3B82F6` | white, light, paper, dark |117| `fuchsia` | `#C026D3` | pastel, white, dark |118| `pink` | `#EC4899` | pastel, dark, light |119| `amber` | `#F59E0B` | gradient, dark, paper |120121Total combinations: 8 surfaces × 8 accents × 5 fonts × 2 purposes = **640 valid styles**. Pick surface for neutral base, accent for pop color — independently.122123**Reference combos** (inspired by design refs):124- `dark + teal` — noir / minimalist tech125- `paper + orange` — literary warm126- `ember + lime` — dramatic announcement127- `white + coral` — sharp editorial128- `light + teal` — calm informational129130**Purpose axis** (`DEFAULT_PURPOSE`):131132| Id | Title | Body | Divider |133|---|---|---|---|134| `carousel` *(default)* | 44px, weight 800, UPPERCASE | weight 600, `textColor`, line-height 1.2 | visible (96×4px accent) |135| `presentation` | 72px, weight 700, sentence case | weight 400, `textSecondary`, line-height 1.45 | hidden |136137Pick `purpose: "presentation"` + `format: "wide-16x9"` for a desktop / YouTube presentation deck. Any 3 × 8 × 2 = 48 combinations are valid.138139---140141## Workflow142143### Step 1 — Get the text144145- If passed inline — use it.146- If a file path (`.md`, `.txt`) — read it.147- If text is long (>500 chars) — confirm it's complete before planning.148149### Step 2 — Clarify parameters (optional, can assume defaults)150151Ask once, combined:1521531. **Number of slides** (3–10, default 6)1542. **Format** (see table above, default `threads-4x5`; use `wide-16x9` for a presentation deck)1553. **Purpose** (`carousel` or `presentation`, default `carousel`)1564. **Font** (`minimal` / `editorial` / `clean`, default `minimal`)1575. **Color** (8 palettes, default `dark`)1586. **Handle** for CTA slide (e.g. `@username`)159160Shortcut: if user says "presentation" / "презентация" / "slide deck" → default to `purpose: presentation`, `format: wide-16x9`, `font: clean`, `color: white`.161162If user says "your call" — apply defaults, do not block.163164### Step 3 — Plan the slide breakdown165166Show the user a preview list before generation:167168```169Slide 1 (hook): "Headline..." [highlight: "word"]170Slide 2 (body): badge 01 — "Title" / "Text..."171Slide 3 (list): badge 02 — "iOS stack" / 3 items172Slide 4 (stats): badge 03 — "Growth" / 3 stats173Slide 5 (quote): "Quote..." — Author174Slide 6 (cta): "Final message" @username175```176177### Rules for splitting text into slides1781791. **Hook** = single most intriguing line from the post. 1–3 short lines. Works as a standalone thumbnail.1802. **Body slides** = one idea each. Max 40 words. Max 5 lines. Never join two ideas with "and".1813. **Mix slide types** for visual variety: prefer `list` for enumerations, `stats` for numbers, `quote` for direct speech, `comparison` for VS/before-after. Don't make every slide a `body`.1824. **CTA** = conclusion + follow handle. Centered, short.183184### Step 4 — Generate185186#### Prepare working copy187188```bash189WORK_DIR="/tmp/carousel-$(date +%s)"190rsync -a --exclude=node_modules ~/.claude/skills/threads-carousel/template/ "$WORK_DIR/"191ln -s ~/.claude/skills/threads-carousel/template/node_modules "$WORK_DIR/node_modules"192cd "$WORK_DIR"193```194195Symlinking `node_modules` avoids the 350MB copy per run.196197#### Inject content into `src/slides.ts`198199All content + defaults live in `src/slides.ts` — never touch the engine (`src/app/CarouselApp.tsx`, `src/lib/*`). Edit the `SLIDES` array and the 5 default constants.200201Full injection example:202203```ts204import type { SlideData, BgType, FormatId, FontId, ColorThemeId, PurposeId } from "./lib/types";205206export const SLIDES: SlideData[] = [207 { type: "hook", text: "Line one\nline two", highlight: "two" },208 { type: "body", badge: "01", title: "Title", text: "Body text...", highlight: "key" },209 { type: "body", badge: "02", title: "Pros & Cons", points: [210 { type: "plus", text: "One click to register" },211 { type: "plus", text: "Works on any background" },212 { type: "minus", text: "Requires provider setup" },213 ]},214 { type: "list", badge: "03", title: "Steps", items: ["First", "Second", "Third"] },215 { type: "stats", title: "Numbers", stats: [216 { value: "10×", label: "Faster" },217 { value: "50%", label: "Smaller" },218 ]},219 { type: "quote", text: "Big idea\nin few words", author: "Someone", role: "2026" },220 { type: "checklist", title: "Pre-flight", items: ["One", "Two", "Three"] },221 { type: "cta", text: "Last word", handle: "@username" },222];223224export const DEFAULT_FONT: FontId = "minimal";225export const DEFAULT_COLOR: ColorThemeId = "dark";226export const DEFAULT_PURPOSE: PurposeId = "carousel";227export const DEFAULT_BG: BgType = "glow";228export const DEFAULT_FORMAT: FormatId = "threads-4x5";229```230231#### Launch preview and export232233```bash234bun dev --port 3333235```236237Tell the user to open `http://localhost:3333`. They can:238- Switch **Format / Mode / Font / Color / Background** live via toolbar rows239- Toggle UI language **RU / EN** in the top-right240- Click **PDF** to download all slides in one file (JPEG-compressed, ~5–8 MB for 10 slides)241- Click **PNG** (a.k.a. "Export All") to download every slide as `01-hook.png`, `02-body.png`, …242- Click an individual slide thumbnail to export just that one as PNG243244After export, stop the dev server.245246#### Parallel carousels247248If making multiple carousels at once: create multiple work dirs and launch on ports 3333, 3334, 3335 — they run side by side without conflict.249250---251252## Design system253254Canonical look (`font: minimal`, `color: dark`, `purpose: carousel`):255256- **Display typeface:** Unbounded (Google Fonts), for hooks — applied only when the font axis provides `hookFontFamily`257- **Body typeface:** Space Grotesk (minimal) / Playfair (editorial) / Inter (clean) — everything else uses `preset.fontFamily`258- **Palette (dark):** `#0A0A0A` bg, `#FFFFFF` text, `#FACC15` highlight259- **Layout:** 80px padding, left-aligned, slide counter bottom-center260- **Title discipline:** carousel purpose — title → a 96×4px accent divider → body, with ≥64px breathing room above body. Presentation purpose — no divider, sentence case, 72px.261- **Hook size:** 104–140px, adaptive by character/line count262- **Body text size:** 48–88px, adaptive263- **Text balance:** `textWrap: "balance"` on hook + title (no orphan words)264265### Typography table (carousel purpose)266267The `presentation` purpose overrides titles to 72px / 700 / sentence case and body to 400 / `textSecondary` / line-height 1.45.268269| Element | Size | Weight | Font source |270|---|---|---|---|271| Hook | 104–140px | 800 | `hookFontFamily` ?? `fontFamily` |272| Title | 44px | 800 uppercase | `fontFamily` |273| Body | 48–88px | 600 | `fontFamily` |274| Points (pros/cons) | 44–62px | 600 | `fontFamily` |275| Badge | 26px | 800 uppercase | `fontFamily` |276| Stats value | 140–170px | 900 | `fontFamily` |277| Stats label | 32px | 500 uppercase | `fontFamily` |278| Quote | 62px | 600 | `fontFamily` |279| List item | 46px | 600 | `fontFamily` (numbers 48px) |280| Checklist item | 44px | 600 | `fontFamily` |281| Process step title | 36px | 700 | `fontFamily` |282| Handle | 36px | 500 | `fontFamily` |283284## Common mistakes285286| Mistake | Fix |287|---|---|288| Too much text on a slide | Max 40 words, max 5 lines |289| Two ideas on one slide | Split into two slides |290| All slides are `body` type | Mix in `list`, `stats`, `quote`, `checklist` for visual variety |291| No hook on first slide | Slide 1 must be the catchiest line |292| No CTA on last slide | Slide N must end with a handle or call to action |293| Highlight word too long | Keep highlight to 1–2 words, not whole phrase |294| Badge has too many characters | Max 2–4 characters (`01`, `TIP`, `NEW`) |295296## Future work / TODOs297298- **Satori server-side export** — replace browser-based `html-to-image` with Satori + Resvg for CLI export (`bun run export → out/*.png`). Enables headless runs. See `Slashgear/linkedin-carousel-gen` for reference.299- **Per-slide background override** — currently `DEFAULT_BG` is global; could accept a per-slide `bg` field to mix decorations across a deck.300- **Cyrillic-optimized adaptive sizing** — current thresholds are calibrated for Latin; Russian copy tends to be 20–30% longer at the same font size.301- **Pencil MCP mode** — previous skill version had a manual design mode via Pencil; not currently implemented in the template.