pro-deck-builder

Create polished HTML slide decks and PDF-ready documents for consulting deliverables. Uses the RRBC design system with warm light mode, dark mode cover pages, Lora/Inter/Roboto Mono typography, and data visualization palette. Trigger on 'deck', 'slides', 'presentation', 'pitch deck', 'keynote', 'report', or 'PDF'.

Presentation Designer

VC-backed SaaS design quality for PowerPoint. Think Linear, Attio, Vercel, Raycast — not corporate clip art.

Install

git clone https://github.com/thatrebeccarae/claude-marketing.git && cp -r claude-marketing/skills/pro-deck-builder ~/.claude/skills/

Quick Reference

TaskApproach
Create from scratchRead REFERENCE.md, use PptxGenJS
Edit existing / use templateRead editing.md
Read/extract contentpython -m markitdown presentation.pptx
Visual overviewpython scripts/thumbnail.py presentation.pptx

Before You Start (Required Steps)

Step 1: Ask the User

Before writing any code, ask:

  1. Light or dark mode? Dark = premium/keynote feel. Light = reports/data-heavy decks.
  2. Palette preference? Show the palette table below, or ask if they have brand colors.
  3. Logo? Ask if they have a logo file to include (PNG/SVG path).

Step 2: Discover Fonts

Check what's installed — premium fonts make a massive difference:

# macOS — list all installed font families
fc-list : family | sort -u

# Search for premium fonts first
fc-list : family | grep -iE "inter|dm.sans|poppins|avenir|futura|geist|sf.pro|sf.mono|jetbrains|fira.code|montserrat|playfair|source.sans"

# Check macOS Font Book directories directly
ls ~/Library/Fonts/ /Library/Fonts/ /System/Library/Fonts/Supplemental/ 2>/dev/null

Priority order for font selection:

  1. Inter — the gold standard for SaaS UI (if installed, use it for everything)
  2. DM Sans — geometric, startup-friendly
  3. Poppins — rounded, approachable
  4. Avenir / Avenir Next — ships with macOS, Apple-adjacent elegance
  5. Calibri — safe fallback, always available

For data/mono: JetBrains Mono > Fira Code > SF Mono > Consolas


Design Language: VC-Backed SaaS

Aesthetic Principles

  1. Depth through layering — Surfaces float above backgrounds. Cards and panels have subtle elevation via shadows and fine borders, not flat colored boxes.
  2. Bold typography, minimal decoration — Let type hierarchy do the work. No accent lines, no decorative shapes, no clip art.
  3. Restrained color — One accent color used sparingly (10%). Mostly neutrals. Color = signal, not decoration.
  4. Generous whitespace — Empty space signals confidence. If a slide feels sparse, make the content bigger, don't add more.
  5. Every element earns its place — Before adding anything: what does it communicate? If removing it changes nothing, remove it.

Light vs Dark Mode

The user picks one mode for the entire deck. Never mix modes within a deck.


Color System

Dark Mode (Default — Premium/Keynote)

TokenValueUse
bg-primary0A0A0CSlide backgrounds (deep space black)
bg-surface17181ACards, panels (deep slate)
bg-elevated1E1F23Elevated surfaces, hover states
bg-glassFFFFFF at 4% opacityTranslucent overlays
text-primaryFFFFFFHeadlines, key content
text-secondaryA0A0A0Body text, descriptions
text-muted5A5A5ECaptions, metadata, labels
border-subtleFFFFFF at 8% opacityCard borders

Light Mode (Reports/Data-Heavy)

TokenValueUse
bg-primaryFFFFFFSlide backgrounds
bg-surfaceF8F9FACards, panels
bg-elevatedFFFFFFElevated surfaces (use shadow)
text-primary111111Headlines, key content
text-secondary555555Body text, descriptions
text-muted999999Captions, metadata, labels
border-subtleE8E8E8Card borders

Palette System

Each palette defines one accent color (used at 10%) plus semantic status colors. The accent is the personality — everything else is neutral.

Built-In Palettes

NameAccentHexBest For
Electric BlueIntense azure0055FFSaaS, tech, product
Neon LimeAcid green39FF14Dev tools, performance
Hot CoralWarm energyFF6B6BMarketing, creative
IndigoDeep purple6366F1Startups, AI/ML
Teal TrustCool confidence0D9488Healthcare, finance
Amber SignalWarm authorityF59E0BConsulting, strategy
RoseRefined pinkF43F5EDesign, brand, luxury
EmeraldGrowth green10B981Sustainability, success
Cyan EdgeSharp blue-green06B6D4Data, analytics
VioletCreative depth8B5CF6Education, innovation
SlateNo accent64748BMinimal, understated
Gold StandardFinancial premiumD4AF37Investor, board decks

Custom Palette (User Brand Colors)

If the user provides brand colors, build a custom palette:

const palette = {
  accent: "0055FF",      // primary brand color
  accentMuted: "0055FF" + " at 15% opacity", // for tinted backgrounds
  positive: "10B981",    // good, up, success
  warning: "F59E0B",     // attention, caution
  critical: "EF4444",    // bad, down, error
};

Color Application: 85/10/5 Rule

  • 85% — Backgrounds + neutral text (bg + text tokens from mode)
  • 10% — Accent (key numbers, chart primary, CTAs, active states)
  • 5% — Semantic status (positive/warning/critical in KPIs and data)

Typography

Font Priority

Run font discovery (Step 2 above), then pick:

PriorityHeaderBodyData/MonoVibe
1stInter 700Inter 400JetBrains MonoLinear/Vercel
2ndDM Sans 700DM Sans 400Fira CodeGeometric startup
3rdAvenir Next BoldAvenir 400SF MonoApple-adjacent
4thPoppins 700Poppins 400JetBrains MonoFriendly modern
SafeCalibri BoldCalibriConsolasAlways available

For editorial/financial decks:

HeaderBodyVibe
DM Serif DisplayDM SansSophisticated editorial
Playfair DisplaySource Sans ProLuxury, financial
GeorgiaCalibriClassic corporate

Size Scale

RoleSizeWeightUse
Hero title54-72ptBold (700)Title slide only
Slide title32-40ptBold (700)Every content slide
Section header20-24ptSemibold (600)Within-slide sections
Body text14-16ptRegular (400)Paragraphs, bullets
Captions/meta10-12ptRegular (400)Footnotes, sources
Big KPI number48-72ptBold (700)Dashboard callouts
KPI label9-10ptSemibold, ALL CAPS, +trackingBelow KPI numbers
Data/numbers11-14ptMono regularTables, data labels

Typography Rules

  • Left-align body text and lists — center ONLY titles and KPI values
  • Max 2 font families per deck (+1 mono for data)
  • ALL CAPS + charSpacing only for small labels (KPI labels, category tags)
  • Line height: 1.3x for body, 1.1x for titles
  • Never underline for emphasis — use bold or accent color

Text Fitting (Prevents Overflow)

Big numbers and titles WILL overflow if you use a fixed font size without checking content length. Use these rules:

Big KPI Numbers — scale font to character count:

CharactersExampleMax Font Size (card)Max Font Size (standalone)
1–4$4.2M48pt72pt
5–7$5,307K36pt54pt
8–10$5,307,54028pt44pt
11+$12,345,67822pt36pt

Rule: prefer abbreviated numbers. $5.3M > $5,307,540. Only use full numbers in tables or footnotes where precision matters.

Slide Titles — max characters before wrapping:

Font SizeMax Chars (~9" wide)If Longer
54pt~30Reduce to 44pt or rewrite shorter
44pt~38Reduce to 36pt or rewrite shorter
36pt~46Reduce to 32pt or rewrite shorter
32pt~52Use subtitle for overflow

Rule: count characters before setting fontSize. Never let content slide titles wrap to 2 lines. Section dividers can wrap.

Body Text in Narrow Containers (Insight Panels, Sidebars):

Text overflow in narrow panels (< 4" wide) is a common problem. PptxGenJS does not auto-shrink text. Apply these rules:

Container WidthMax fontSizeMax chars per line (~)Max lines at 1.25x spacing
4.0"+14pt~55Height ÷ 0.22"
3.0-3.9"11pt~45Height ÷ 0.18"
2.0-2.9"10pt~35Height ÷ 0.16"
< 2.0"9pt~25Height ÷ 0.15"

Rule: estimate line count before adding text. Count characters in your text, divide by max chars per line to get line count, then verify it fits in the container height. If it doesn't fit: (1) reduce fontSize, (2) shorten the text, or (3) increase the container. Never let text silently overflow — it gets clipped or overlaps adjacent elements.

// ✅ CORRECT: 2.8" panel → max 10pt, ~35 chars/line
// "Welcome series dominates at 62%..." = 90 chars ≈ 3 lines
// Container h: 1.6" → fits ~10 lines at 10pt. Safe.
s.addText("Short, edited copy.", {
  x: 6.55, y: 1.45, w: 2.7, h: 1.6,
  fontSize: 10, fontFace: theme.font.body,
});

// ❌ WRONG: 2.8" panel with 14pt font and 200+ char paragraph
s.addText("Very long paragraph that will overflow...", {
  x: 6.55, y: 1.45, w: 2.8, h: 1.6,
  fontSize: 14, fontFace: theme.font.body,  // too large for narrow panel
});

Chart Number Formatting

Always format chart numbers for readability — raw numbers like "1000000" are never acceptable on axes OR data labels.

CRITICAL: valAxisLabelFormatCode only formats the AXIS. Data labels from showValue: true are formatted separately via dataLabelFormatCode. If you set valAxisLabelFormatCode but not dataLabelFormatCode, data labels will show raw unformatted numbers like "2888000". numFmt is NOT a valid PptxGenJS property — always use dataLabelFormatCode.

Preferred approach — OOXML conditional format with raw values: Use raw (unscaled) values and let OOXML conditional number formatting handle the display. This is the most reliable approach and handles mixed-magnitude data (e.g., a chart with both $2.9M and $3.6K values).

// ✅ BEST: Raw values + OOXML conditional format
// Under 1K: whole number ($500)
// 1K-999K: X.XXK ($540.18K, $57.72K)
// 1M+: X.XM ($2.9M)
const currFmt = '[>=1000000]"$"#,##0.0,,"M";[>=1000]"$"#,##0.00,"K";"$"#,##0';

values: [2888000, 717734, 425467, 3655],  // raw dollar values
dataLabelFormatCode: currFmt,              // conditional brackets OK here
valAxisLabelFormatCode: '"$"#,##0.0,,"M"', // simple format only (no brackets)
// ✅ ALSO OK: Pre-scale when all values are same magnitude
values: [2888, 718, 425],  // raw values divided by 1000
dataLabelFormatCode: '"$"#,##0"K"',
valAxisLabelFormatCode: '"$"#,##0"K"',
// ❌ WRONG: Raw values with axis-only format
values: [2888000, 718000, 425000],
valAxisLabelFormatCode: "$#,##0,K",  // axis may format but data labels show 2888000
// ❌ WRONG: numFmt (not a valid PptxGenJS property)

OOXML conditional format reference (use with RAW values):

Data RangeFormat CodeRenders As
Currency (auto-scale)'[>=1000000]"$"#,##0.0,,"M";[>=1000]"$"#,##0.00,"K";"$"#,##0'$2.9M, $540.18K, $500
Plain number (auto-scale)'[>=1000000]#,##0.0,,"M";[>=1000]#,##0.00,"K";#,##0'2.9M, 540.18K, 500
Percentage'0%'50%, 85%

Format code syntax: OOXML supports up to 2 conditions in brackets + a default: [condition1]format1;[condition2]format2;defaultFormat. Each trailing , divides by 1,000 (so ,, divides by 1M).

IMPORTANT: valAxisLabelFormatCode does NOT support OOXML conditional brackets [>=1000]. Use a simple format for the axis based on the data range:

Axis RangevalAxisLabelFormatCodeRenders As
Up to ~999K'"$"#,##0,"K"'$0K, $100K, $500K
Up to ~10M+'"$"#,##0.0,,"M"'$0.0M, $0.5M, $1.0M
Up to ~999'"$"#,##0'$0, $100, $500

Use the conditional currFmt for dataLabelFormatCode (which supports brackets), but a simple format for valAxisLabelFormatCode. Always set showValue: true with dataLabelPosition: "outEnd" on bar charts so readers can see exact values without tracing to the axis.


Spacing

TokenValueUse
xs0.125"Within grouped elements
sm0.25"Label to value, icon to text
md0.375"Between sections within a card
lg0.5"Between cards, major sections
xl0.75"Between content zones

Layout Grid (10" × 5.625")

Margins:       0.5" from all edges
Content area:  9.0" wide × ~4.5" tall
Title zone:    y = 0.3"
Content zone:  y = 1.0" to y = 5.0"
Footer zone:   y = 5.2"
Card gap:      0.25" - 0.3"

Logo Placement

Finding Logos

find ~/Desktop ~/Downloads ~/Documents -maxdepth 3 -iname "*logo*" \( -name "*.png" -o -name "*.svg" -o -name "*.jpg" \) 2>/dev/null

Placement Patterns

Slide TypePositionSizeNotes
Title slide (top-left)x:0.5, y:0.4w:1.5, h:0.5Subtle brand mark
Title slide (centered)x:4.0, y:0.8w:2.0, h:0.7Above title text
Content slidesx:8.5, y:0.2w:1.0, h:0.35Top-right corner, every slide
Closing slidex:3.5, y:1.5w:3.0, h:1.0Centered, hero size

Logo Rules

  • Always use sizing: { type: "contain", w, h } to preserve aspect ratio
  • Content slides: max 1" wide, don't compete with content
  • Title/closing: up to 3" wide for brand moment
  • Dark mode: use white/light logo variant if available (ask user)
  • Slide master: if logo goes on every slide, add it to a master definition

Icons

Pipeline: react-icons → sharp → PNG → PptxGenJS

npm install -g react-icons react react-dom sharp

Prefer Lucide (react-icons/lu) or Feather (react-icons/fi) — they match the Linear/Vercel icon language.

PackageImportStyle
react-icons/luLucideLinear-style, clean
react-icons/fiFeatherThin, modern
react-icons/hi2Heroicons v2Apple-ish
react-icons/tbTablerConsistent strokes

Icon Sizes

ContextDisplay SizeColor
KPI card icon0.35" × 0.35"Accent on dark, muted on light
Feature row icon0.4" × 0.4"White inside accent circle
Section divider0.5" × 0.5"Accent color
Inline bullet0.25" × 0.25"text-secondary

Reusable Components

Bottom Callout Box (Standardized)

Many slides end with a highlighted callout box (recommendation, key insight, total impact). These must be visually identical across all slides:

PropertyValueNotes
Box height0.6"Always 0.6" — never 0.5", 0.7", 0.9"
Text height0.5"Fits inside 0.6" box with padding
Font size11ptAlways 11pt bold — never 12pt or 13pt
Fonttheme.font.bodyBold, left-aligned, valign: "middle"
Box x/wx: 0.5, w: 9.0Full content width
Text x/wx: 0.7, w: 8.6Inset 0.2" from box edges
Text y offsetbox y + 0.05"Centers text vertically in box
Corner radius0.06"Consistent across all callouts
// Standard bottom callout — copy this pattern exactly
s.addShape(pres.shapes.RECTANGLE, {
  x: 0.5, y: calloutY, w: 9.0, h: 0.6,
  fill: { color: "DCFCE7" },  // or theme.accentMuted, "FEF3C7", etc.
  line: { color: theme.status.positive, width: 0.75 },
  rectRadius: 0.06,
});

s.addText("Callout text here.", {
  x: 0.7, y: calloutY + 0.05, w: 8.6, h: 0.5,
  fontSize: 11, fontFace: theme.font.body, bold: true,
  color: "166534", align: "left", valign: "middle",
});

Table + Callout Layout (Prevents Overlap)

Always calculate the table bottom position before placing a callout below it. PptxGenJS tables don't auto-report their height, so you must compute it:

tableBottom = tableY + (rowCount × rowH)
calloutY = tableBottom + 0.15"  // minimum 0.15" gap

Verify calloutY + 0.6" (callout height) < 5.2" (footer zone). If it doesn't fit, reduce rowH or table font sizes — never let the callout overlap the footer.

Rows (incl header)rowH: 0.32"rowH: 0.36"rowH: 0.42"rowH: 0.48"rowH: 0.55"
6 rows1.92"2.16"2.52"2.88"3.30"
7 rows2.24"2.52"2.94"3.36"3.85"
8 rows2.56"2.88"3.36"3.84"4.40"
11 rows3.52"3.96"4.62"

With table starting at y: 1.0" and callout needing 0.75" (0.15" gap + 0.6" box), the table height limit is 3.45" to stay above footer. Choose rowH accordingly.

Cross-Slide Consistency Rules

Repeated elements MUST use identical dimensions across all slides:

  1. Bottom callout boxes — same h, fontSize, padding on every slide (see standard above)
  2. Chart font sizes — if multiple slides have bar/line charts, use identical dataLabelFontSize, catAxisLabelFontSize, and valAxisLabelFontSize across all of them
  3. Table styling — header row style (hdr()) and cell style (cell()) should be defined once as helpers and reused
  4. Card dimensions — if using stat cards on multiple slides, keep card width/height/corner radius consistent
  5. Section badge positions — impact badges (e.g., "$400K-$800K") should use the same x, y, w, h, fontSize

Rule: define reusable constants at the top of the script for any dimension that appears on more than one slide.


Slide Compositions

Vary Layouts — The #1 Rule

Never repeat the same layout more than twice consecutively. Rotate through:

TypeWhenElements
Title SlideOpening/closingHero text, subtitle, logo, date
KPI DashboardExecutive summary3-5 cards, big numbers
Chart + InsightData with context60/40 split
Full-Width ChartChart IS the storyFull span, callout overlay
Icon Feature GridFeatures, pillars3-4 icon+text rows
Data TableFinancials, comparisonsStyled rows, mono numbers
Section DividerBetween sectionsBold title on dark/accent bg
QuoteTestimonialsLarge italic, attribution
Big NumberSingle key statHuge centered metric
ComparisonBefore/after, vs.Side-by-side columns
Image + TextProducts, case studiesHalf-bleed image
ClosingEndLogo, CTA, contact

Anti-Patterns (Never Do These)

  1. NEVER add horizontal accent lines/bars under titles, subtitles, or headings — This is the #1 AI tell. No thin rectangles (h: 0.03-0.05) placed under text as decorative separators. Not on title slides, not on section dividers, not on closing slides, not anywhere. If you feel a slide needs visual separation, use whitespace or a tinted panel instead.
  2. No gradient fills on shapes — use solid colors + subtle borders
  3. No heavy shadows — max opacity 0.1, blur 6
  4. Don't repeat layouts on consecutive slides
  5. Don't center body text — left-align everything except titles/KPIs
  6. No more than 2 fonts (+1 mono)
  7. No walls of text — max 6 lines body per slide
  8. No decorative shapes — no random circles, triangles, swooshes
  9. No clip art — use Lucide/Feather icons
  10. No colored card backgrounds — cards = bg-surface + subtle border
  11. No unicode bullets — use bullet: true
  12. No mixing light/dark within a deck
  13. No unformatted chart data labels — if you use showValue: true, you MUST also set dataLabelFormatCode. valAxisLabelFormatCode alone does NOT format data labels. Use OOXML conditional formats with raw values for best results.
  14. No long text in narrow panels without size checks — before adding body text to any container < 4" wide, estimate line count (chars ÷ chars-per-line) and verify it fits the container height. Reduce font size or shorten text if it won't fit.
  15. No inconsistent callout boxes — all full-width bottom callout boxes must use identical height (0.6"), text fontSize (11pt), and padding across every slide. Define the callout pattern once and copy it exactly.
  16. No callout-over-table overlap — always calculate tableBottom = tableY + (rows × rowH) and place the callout at tableBottom + 0.15" minimum. Never hardcode callout y-positions without verifying they clear the table.
  17. No inconsistent chart font sizes — if multiple slides contain charts, use identical dataLabelFontSize, catAxisLabelFontSize, and valAxisLabelFontSize values. Define these as constants and reuse them.

QA (Required)

Assume there are problems. First render is never correct.

Content Check

python -m markitdown output.pptx
python -m markitdown output.pptx | grep -iE "xxxx|lorem|ipsum|placeholder|click to"

Visual QA (Use Subagents)

python scripts/office/soffice.py --headless --convert-to pdf output.pptx
pdftoppm -jpeg -r 150 output.pdf slide

Subagent prompt:

Inspect these slides. Assume issues exist — find them.
Look for:
- TEXT OVERFLOW: Numbers or titles that wrap to a second line inside their container. Also check body text in narrow panels (<4" wide) — long paragraphs at font sizes >11pt will overflow.
- ACCENT LINES: Any thin horizontal bars/lines under titles or headings (must be removed)
- CHART DATA LABELS: Raw unformatted numbers on data labels (e.g., "2888000" instead of "$2,889K"). This is the #1 chart bug — valAxisLabelFormatCode does NOT format data labels.
- CHART AXES: Raw unformatted numbers on axes (should be $500K not 500000)
- Overlapping elements, cramped spacing (<0.3"), insufficient margins (<0.5")
- Low contrast, text-only slides, repeated layouts, placeholder text
- Mode inconsistency (light elements on dark or vice versa)
- Big numbers that should be abbreviated ($5.3M not $5,307,540)
- CALLOUT INCONSISTENCY: Bottom callout boxes with different heights, font sizes, or padding across slides (all must be h:0.6", fontSize:11)
- TABLE-CALLOUT OVERLAP: Callout boxes overlapping tables — calculate tableY + (rows × rowH) and verify callout clears it
- CHART FONT INCONSISTENCY: Different dataLabelFontSize, catAxisLabelFontSize, or valAxisLabelFontSize between charts on different slides

Fix → re-render → inspect → repeat until clean. Complete at least one fix cycle.


Dependencies

npm install -g pptxgenjs                          # Creation
npm install -g react-icons react react-dom sharp  # Icons
pip install "markitdown[pptx]" Pillow             # Reading + QA
# Also: LibreOffice (soffice) + Poppler (pdftoppm) for visual QA

Implementation

Read REFERENCE.md for PptxGenJS API patterns and complete slide composition code for both light and dark modes.

Brand Context (Optional)

If brand-profile.json exists in the working directory, read it before building decks. Override the default RRBC palette with the brand's colors (primary, secondary, background), use brand fonts when available (fall back to system fonts), and include the brand name on title slides and headers. This profile is produced by the brand-dna skill.