
Deck Design Pdf
- 8 installs
- 53 repo stars
- Updated July 9, 2026
- appautomaton/presentation
deck-design-pdf is a Claude skill that creates high-fidelity presentation decks as PDF files rendered from HTML, CSS, and Tailwind via Playwright.
About
This skill creates presentation decks as high-fidelity PDF files rendered from HTML, CSS, and Tailwind via Playwright. A developer or consultant uses it when they want pixel-perfect, print-ready slides with rich typography, icons, gradients, and charts, rather than editable PowerPoint. It first builds a ghost-deck narrative skeleton (governing thought, action titles, exhibit types), then produces the slides, supporting consulting and general presentation styles.
- Creates presentation decks as high-fidelity PDF via HTML/CSS and Playwright
- Uses Tailwind, Font Awesome, Google Fonts, and ECharts (21 chart types)
- Builds a ghost deck narrative skeleton before any HTML
Deck Design Pdf by the numbers
- 8 all-time installs (skills.sh)
- Ranked #1,483 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
deck-design-pdf capabilities & compatibility
- Capabilities
- deck generation · data visualization · ui design
- Works with
- playwright
- Use cases
- presentations · ui design
What deck-design-pdf says it does
Create presentation decks as high-fidelity PDF files using web technology (HTML, CSS, Tailwind, Font Awesome).
npx skills add https://github.com/appautomaton/presentation --skill deck-design-pdfAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 53 |
| Last updated | July 9, 2026 |
| Repository | appautomaton/presentation ↗ |
What it does
Build a pixel-perfect, print-ready pitch or strategy deck as a PDF from HTML/CSS with charts and icons.
Who is it for?
Pixel-perfect, print-ready decks with web typography, icons, gradients, and charts.
Skip if: Editable slides in PowerPoint or Keynote, which deck-design-ppt handles instead.
When should I use this skill?
You want a polished, print-ready deck as a PDF rather than an editable PPTX.
What you get
A PDF deck is rendered from HTML/CSS via Playwright after a ghost-deck narrative is approved.
- High-fidelity PDF deck
- Ghost-deck and outline markdown artifacts
By the numbers
- ECharts 5 with 21 chart types
- Three density levels: L1 Narrative, L2 Structured, L3 Dense
- Five style palettes
Files
Deck Design PDF — Operational Playbook
What This Skill Does
- Input: a deck brief + any structured data the user provides.
- Output: a PDF file — pixel-perfect slides rendered from HTML/CSS via Playwright. Not editable in PowerPoint. This is the high-fidelity, print-ready format.
When to use this vs deck-design-ppt
| Need | Skill |
|---|---|
| Editable slides (PowerPoint/Keynote/Slides) | deck-design-ppt |
| Pixel-perfect PDF — web typography, icons, gradients, CSS layouts | deck-design-pdf (this) |
What the web stack unlocks (beyond pptx)
- CSS Grid and Flexbox — responsive layouts instead of coordinate math
- Google Fonts with optical sizing and variable weights
- Font Awesome 6 icons inline with text
- Gradient backgrounds, box shadows, rounded corners, opacity
- ECharts 5 (SVG renderer) — 21 chart types: bar, line, pie, scatter, waterfall, gauge, sankey, treemap, radar, funnel, heatmap, and more
- Tailwind utility classes — rapid, consistent styling
Phase 1 — Understand and Route
1. Read the room
Before asking the user anything, scan the working directory for artifacts that establish context — briefs, consultant storyboards (*-ghost.md, *-outline.md), data files, brand specs, existing build scripts. Read what's there and form a working hypothesis about what this deck needs: objective, audience, tone, density, and data.
Then ask only for what's genuinely ambiguous and would change the deck's argument or audience calibration. Frame questions as choices, not open fields: "The data suggests a Bain decision-first structure — does that fit, or is this more of an internal working session?" One sharp question beats five vague ones. Maximum 1–3 questions; zero is fine if the artifacts are clear.
2. Classify the deck
This determines how deep the planning phase goes. Two paths:
| Signal | Path | What happens next |
|---|---|---|
| MBB engagement, strategy consulting, due diligence, cost transformation, M&A, pricing, org restructuring | Consulting | Load the full consulting reference library (ghost-deck.md, engagement-archetypes.md, skeletons/, firm-dna.md, evidence-recipes.md, density-adaptation.md). Build a rigorous ghost deck with archetype classification, pillar architecture, transitions, and firm overlay. |
| Startup pitch, board update, product narrative, conference talk, investor roadshow, internal update | General | Stay in this playbook. Build a ghost deck using the universal quality gates below — governing thought, action titles, helicopter test, exhibit assignment. No archetype classification or skeleton required. |
Consultant handoff — if a storyboard from the consultant skill already exists (governing thought, pillars, action titles, content descriptions), the argument layer is done. Skip ghost deck construction entirely. Validate the argument (run the helicopter test on the titles), then proceed to the production outline.
3. Pick style and density
Style:
| If the deck is… | Palette | Surface | Font |
|---|---|---|---|
| Consulting / board / engagement deliverable | consulting-mckinsey | Light | Inter |
| Framework evaluation, process-transparent | consulting-bcg | Light | DM Sans |
| Diagnostic, decision support, facts-vs-perspectives | consulting-bain | Light | Source Sans 3 |
| Startup pitch / product narrative / growth story | founder | Light | Plus Jakarta Sans |
| Crisis / urgency / downturn memo / high-stakes | sequoia | Dark | Georgia + Inter |
The fonts above are vendored locally for the default palettes. The agent is not limited to these — any Google Font can be loaded via a <link> tag in headExtra when the brief calls for a different typeface.
Density:
| Context | Level | What changes |
|---|---|---|
| Startup pitch, roadshow, board update | L1 Narrative | 1 message per slide. Fewer elements, each scaled up to fill the body zone. |
| Corporate strategy, business review | L2 Structured | Multi-panel layouts. Evidence density moderate. |
| MBB engagement, due diligence, deep analysis | L3 Dense | Multi-evidence composites. Chained arguments. |
Show deck vs. working deck — infer from audience and objective, don't ask. "Board presentation" → tier: 'presentation' (body ≥15pt, room-safe). "Diligence data room" → tier: 'document' (body ≥12pt, dense but legible). The tier shifts ALL text tokens automatically.
For L2+ consulting decks, apply firm-specific conventions from firm-dna.md. For density adaptation rules, see density-adaptation.md.
Phase 1.5 — Ghost Deck
Before writing any HTML, build the ghost deck — the narrative skeleton that precedes all visualization. Charts and code are execution. The ghost deck is the thinking.
The ghost deck process produces two artifacts:
- `{slug}-ghost.md` — the argument layer: governing thought, action titles, exhibit types. A senior reviewer can approve this without knowing CSS.
- `{slug}-outline.md` — the execution layer: ghost deck + density tiers, layout geometry, chart dimensions, column alignment. This is what the build phase reads.
Process management. Building a ghost deck requires populating multiple interdependent layers and passing quality gates that may trigger backtracking. Use your task management tools to track which layers are complete, which gates have passed, and what needs revisiting. Externalize this state — don't try to hold it all in one pass.
Universal quality gates
These apply to every deck — consulting, startup, board update, conference talk. They are not consulting methodology; they are good deck design.
Start from the governing thought — one sentence that states the deck's answer. Build pillars that support it. Write action titles for every slide before you touch any charts. Read the titles in sequence — if they don't tell a complete story, the problem is in the pillars, not the titles. Only assign exhibits once the argument holds. Cut any slide that isn't load-bearing.
What must exist in every ghost deck:
| Layer | What it is | Depends on |
|---|---|---|
| Governing thought | One sentence — the deck's answer. If you can't write it, the analysis isn't done. | The brief and data |
| Action titles | Full-sentence conclusion for every slide. "Revenue grew 12% driven by pricing power" — not "Revenue Overview." | Governing thought |
| Helicopter test | Read all titles in sequence. Complete, persuasive story in 2 minutes? If not, fix the titles. | Action titles |
| Exhibit assignment | What visual proves each slide's claim. Use chart-taxonomy.md: "What does this slide need to prove?" → exhibit type. | Helicopter test passed |
| Dead slide test | Can any slide be removed without breaking the argument? If yes, remove it. | All of the above |
Common deck patterns. For non-consulting decks, chart-taxonomy.md § Deck Archetypes provides narrative arcs and exhibit mixes for common formats — investor/roadshow (Problem → Solution → Traction → Market → Model → Team → Ask), board updates (Headlines → KPIs → Progress → Risks → Decisions), due diligence memos, transformation roadmaps, and more. Use these as starting points for the slide sequence, then write action titles and apply the quality gates above. Don't force a pattern that doesn't fit the brief — adapt or combine as the argument requires.
Backtracking, not patching. If the helicopter test fails, the problem is upstream — the pillars don't support the governing thought, or the titles are topic labels instead of conclusions. Go back to where the argument breaks. Don't patch titles to paper over a structural gap.
When the brief supports multiple valid governing thoughts — surface the options and let the user choose before proceeding. Otherwise, commit to the strongest hypothesis and work forward.
Consulting path
For MBB engagements and strategy consulting, the ghost deck goes deeper. Load the full reference library:
| Doc | Purpose |
|---|---|
| ghost-deck.md | 5-layer model, quality gates, anti-patterns |
| engagement-archetypes.md | 8 archetypes, 5 transition types, rising-stakes progression |
skeletons/*.md | Pillar architecture, slide-by-slide index, kill conditions per archetype |
| firm-dna.md | McKinsey/BCG/Bain epistemology, communication signatures |
| evidence-recipes.md | 10 data-shape recipes, layering rules |
| density-adaptation.md | L1/L2/L3 compression/expansion rules |
On top of the universal quality gates, the consulting path adds:
- Archetype classification — classify the engagement using engagement-archetypes.md, then load the matching skeleton from
skeletons/. The skeleton provides a pillar architecture and slide-by-slide index as a starting point — customize it to the specific brief. - Pillar architecture — 3–5 MECE pillars supporting the governing thought. Work backward: "What must be true for the governing thought to hold?"
- Transition architecture — at each pillar boundary, select a transition type (Pivot, Narrowing, Escalation, Synthesis, Decision Gate). Verify rising-stakes progression: Recognition → Clarity → Conviction → Urgency.
- Firm overlay — apply firm-dna.md conventions for the target firm style. This affects header structure, communication signature, and composition patterns — not the argument itself.
- Evidence recipes — for each exhibit, check evidence-recipes.md for the standard composition: primary chart + supporting elements + annotation layers, governed by density level.
These layers have dependencies — you can't design transitions until pillars exist, and you can't assign evidence recipes until exhibits are selected — but the agent decides the working order based on what the brief gives it. A brief with strong data might start from what the data proves and work backward to the governing thought. A brief with a clear strategic question might start top-down.
Consultant handoff
When a storyboard from the consultant skill already exists (governing thought, pillars, action titles, content descriptions, structured data), the argument layer is done. This skill inherits it and focuses on production:
1. Run the helicopter test on the provided action titles. If titles read as topic labels rather than conclusions, strengthen them. 2. For each slide, use the content description + chart-taxonomy.md decision tree to select exhibit types. The consultant describes WHAT each slide proves; this skill decides HOW to visualize it. 3. This skill may tighten action titles, add transition slides, or adjust slide count for density. The consultant's governing thought and pillar architecture are authoritative; exhibit selection and layout composition belong to this skill.
Presenting the ghost deck
Once the ghost deck is complete, present it to the user as the storyline — just the action titles in sequence. This is the helicopter test output:
Here is the argument this deck makes, read top to bottom:
1. [Action title]
2. [Action title]
...
Does this story hold? Anything missing, out of order, or unconvincing?
Don't print the full outline with density tiers and exhibit types — that's implementation detail. The user's job is to validate the argument, not review the geometry. If they approve, proceed to the production outline. If they redirect, revise and present again.
Production outline
The production outline ({slug}-outline.md) extends the ghost deck by adding geometry to every slide. This is the contract between thinking and building — the build phase copies these numbers, no sizing reasoning required at code time.
For every slide in the ghost deck, add:
- Density tier — which CSS class governs typography
- Layout — composition type + percentage split (e.g., "asymmetric split — left 60% / right 40%")
- Column alignment —
items-startwhen columns differ in content height;items-centeronly for visually balanced columns - Chart spec — explicit dimensions derived from the space budget (see § Space audit below)
- Evidence layers — 1 for L1, 1–2 for L2, 2–4 for L3
N. **Action title — full sentence conclusion**
- Density: sd-medium
- Layout: asymmetric split — left 60% / right 40%
- Column alignment: items-start
- Exhibit: clustered bars (market share by segment)
- Chart spec: 700px × 480px — body 600px − 120px (top labels + gap) = 480px
- Data: SWE-bench scores, coding market share
- Evidence layers: 2Save as {slug}-outline.md. The ghost deck is not modified — it remains the clean argument document.
Outlines that omit exhibit types produce decks dominated by KPI cards and text bullets — the most common quality failure. The outline must have Layout + Exhibit before the build starts.
Space audit
Before writing any code, run a space audit on every slide that contains a chart or diagram. The slide canvas is 1280×720px. After header (~76px) and footer (~44px), the body zone is ≈ 600px tall, ≈ 1168px wide (56px margins each side). In a two-column layout, each column is ≈ 576px.
For each chart slide: subtract any non-chart content (card rows, KPI strips, table rows, gaps) from the 600px body height. The remainder is the chart height. Write this math in the outline — the build agent copies the number directly.
body height 600px − top content Apx − bottom content Bpx = chart heightKey rules:
- If chart height > chart width for that column, the chart is portrait. That's correct — portrait orientation eliminates blank space structurally.
- If two columns have different content heights, set
align-items:start. Don't default to center. - If the computed chart height exceeds the density class default, override it. The space budget wins.
Estimation reference (1280×720):
| Content element | Estimated height |
|---|---|
| 3-card row (compact) | ~120px |
| KPI metric strip (3–4 numbers) | ~80px |
| Table (5 rows) | ~180px |
| Gap between elements | 16–24px |
Phase 2 — Build
How to build slides
Read {slug}-outline.md. Each slide entry has an action title, exhibit type, layout, and chart dimensions. The build phase executes these specs — it does not re-derive them. Sizing decisions were made in the outline; the build agent copies the numbers.
You are composing custom HTML for each slide based on the user's actual data and the outline's argument. You are NOT filling in a template. The examples under examples/*.js are visual pattern references — study their approach, then write original HTML that serves the specific argument.
Space budget reference. At 1280×720, the available zones are:
| Zone | Pixels | Notes |
|---|---|---|
| Slide canvas | 720px tall, 1280px wide | Fixed |
| Header zone | ~76px | Section label + title + accent bar + padding |
| Footer zone | ~44px | Source line, fixed by grid shell |
| Body height | ≈ 600px | What remains — this is your working space |
| Horizontal margin | 56px each side | Tailwind px-14 |
| Content width | ≈ 1168px | 1280 − 112px margins |
| Each column (2-col) | ≈ 576px | (1168 − gap) ÷ 2 |
For each slide in the outline:
1. Set the tier — 'presentation' for any deck projected in a room or read by executives. 'document' (default) for working sessions and analysis. The tier shifts all text baselines automatically. Do not adjust individual font sizes. Infer from audience and objective — don't ask. 2. The action title tells you what to prove. The exhibit type tells you how to prove it. The chart spec tells you the exact dimensions. 3. Read the relevant example file to learn the visual pattern — the ECharts config structure, the CSS layout approach, the annotation placement. Do not copy its data or dimensions. 4. Compose the slide HTML with the user's real data. Use the slide shell (header → body → footer). Apply palette CSS custom properties — never hardcode brand colors. 5. Size text and spacing through the token system (tokens.adapt(), getFigureTypography(), getChartChrome()), not hardcoded pixels.
What to learn from each example:
| Look at | To understand |
|---|---|
ECharts setOption({...}) structure | Which config keys produce this chart type |
tokens.adapt(compact, preferred, wide) calls | How geometry responds to canvas size |
getFigureTypography(tokens) roles | Axis labels, data labels, annotations — what size/weight/color |
getChartChrome(tokens) fragments | Axis lines, gridlines, legends — the shared chrome |
| Semantic color usage | Accent for focus, gray for context, green/red for deltas |
The proves field | What analytical question this pattern answers |
What NOT to copy from examples:
- Sample data ("$4.2B", "12.4M units") — use the user's actual numbers
- Action titles and section labels — write new ones from the ghost deck
- Hardcoded hex colors for palette roles — use
var(--accent),var(--text), etc. - Fixed dimensions — adapt to the content density and slide count. Examples follow a roughly landscape aspect ratio; your slide's body space may demand a portrait or square chart. Fill the available body height first, then constrain only if content overflows.
Fill-first orientation. When sizing a chart or visual exhibit, start from the body height (≈ 600px), subtract any labels, gaps, or card rows above/below the chart, and set the chart to that remaining height. Do not start from an example's hardcoded height and stretch slightly. A chart that fills the body zone vertically eliminates blank space structurally — no bottom padding hack required. Portrait charts (height > width) are valid and preferred over leaving empty space at the slide bottom.
Choosing between visual patterns:
When two exhibit types could work, choose based on the argument, not the visual appeal:
- Waterfall answers "what drove the change?" — use when the audience needs to see sequential drivers
- Driver tree answers "what multiplies into what?" — use when the audience needs to see the structural decomposition
- Horizontal bars answer "who is ahead?" — use when ranking is the point
- Distribution answers "how spread out?" — use when the variance is the insight, not the average
If the taxonomy's decision tree doesn't clearly route to one pattern, default to the simplest exhibit that proves the title's claim. A single bold number is often more powerful than a chart. Simplicity is not laziness — it's restraint.
Load build-reference.md for the full slide construction reference — build script template, slide shell, layout primitives, content primitives, typography scale, design tokens, spatial constants, icons, ECharts configuration, composition routing, and layout anti-patterns.
Key build principles
- Use CSS Grid for the slide shell (
grid-template-rows: auto minmax(0,1fr) 48px). This structurally prevents footer overflow. Do not useflex flex-colwithflex-1. - Fill-first orientation. Start from body height (≈ 600px), subtract surrounding elements, set the chart to the remainder. Portrait charts (height > width) are valid and preferred over leaving empty space.
- Use palette tokens (
var(--accent),var(--text), etc.), not hardcoded hex. - Use `headExtra` CSS classes for the shell on decks >5 slides. Use Tailwind for body content layout.
- For decks >10 slides, consider modular authoring — one file per slide, assembled by
build.js. - ECharts: SVG renderer only (
{ renderer: 'svg' }). Canvas blanks in Playwright PDF. Always setanimation: false.
Firm Style
For L2+ consulting decks only. All three firms use the same charts. What differs is communication structure.
| Firm | Header | Composition signature | Accent |
|---|---|---|---|
| McKinsey | Section label + action title. No rule. | Verdict-first — conclusion in title, evidence below. Exhibit numbering. | Navy |
| BCG | Section label + action title + green rule below. | Framework-first — analytical structure visible, panel mini-headers. | Green |
| Bain | Action title + red rule below. No section label. | Decision-first — facts-vs-perspectives split (left 55% data, right 45% interpretation). | Red |
For full firm conventions, load firm-dna.md. For chart-level visual treatment, see chart-taxonomy.md § Firm Visual Identity.
Composition Rules
- Start with a cover slide. Use section dividers for chapter breaks in decks >8 slides.
- One slide = one argument. Every action title advances a distinct claim.
- Dead slide test. If you can remove a slide and the argument still works, remove it. Every slide must be load-bearing.
- Do not repeat the same layout back-to-back. Alternate between layout types.
- Helicopter test. Read all action titles in sequence — they must tell a complete, persuasive story without any exhibit bodies.
- Density level governs composition. Do not use L3 multi-evidence composites on an L1 pitch deck.
- Chart variety. Do not use the same chart type on consecutive slides.
- Icon restraint. One icon per card or list item. No icon walls. Use accent color, not rainbow.
- Section rhythm. For decks >8 slides, group into 3–4 sections. Open each with agenda (active item) or divider. Close with synthesis before the next section.
- Content fit. Content must fill the body zone without overflow or sparseness. If a slide looks empty, add evidence layers. If it's crowded, split into two slides.
- Visual balance. Content is vertically centered in the body zone by Flexbox — let
flex-1handle distribution. Do not force absolute positioning. - Color discipline. Use palette tokens, not hardcoded hex. Semantic colors (emerald/amber/red) only for status indicators, never for decoration.
- Annotation is value, not decoration. See chart-taxonomy.md's Annotation Grammar. The callout that says "This 15% gap = $340M unrealized" is why the engagement costs what it costs.
Output Organization
Save deliverables to the current working directory (or user-specified folder). Kebab-case naming:
{slug}.pdf— the deliverablebuild-{slug}.js— the build script (reproducible){slug}-ghost.md— the argument layer (governing thought, action titles, exhibits){slug}-outline.md— the execution layer (ghost deck + geometry)
Architecture
Self-contained: package.json + node_modules/ are inside the skill folder. All require() calls resolve locally.
| File | When to load | Purpose |
|---|---|---|
build-reference.md | Phase 2 (Build) | Slide shell, typography, ECharts, layout primitives, anti-patterns |
chart-taxonomy.md | Exhibit selection | Decision tree, firm visual DNA, deck archetypes, annotation grammar |
ghost-deck.md | Consulting path | 5-layer model, dependencies, quality gates, golden trace |
engagement-archetypes.md | Consulting path | 8 archetypes, transitions, rising-stakes progression |
skeletons/*.md | Consulting path | Pillar architecture, slide index, kill conditions per archetype |
firm-dna.md | Consulting path | McKinsey/BCG/Bain epistemology, communication signatures |
evidence-recipes.md | Consulting path | 10 data-shape recipes, layering rules |
density-adaptation.md | Consulting path | L1/L2/L3 compression/expansion rules |
engine/index.js | Always | createDeck(options) — assembles HTML, calls renderer |
engine/render.js | Always | Playwright Chromium → page.pdf() |
palettes/*.css | Always | CSS custom properties per style (colors, fonts) |
examples/_shared.js | Build reference | Typography tokens, chart chrome, responsive sizing |
examples/*.js | Build reference | 46 visual pattern references — read-only, not imported at runtime |
How examples relate to deck building: The agent does NOT import or call example files at runtime. Examples exist as read-only references — the agent reads them to learn visual patterns, then writes original HTML in the build script. The build script uses only engine/index.js (createDeck). The examples use _shared.js to demonstrate best practices for token usage, but the agent composes slide HTML directly.
The engine handles: HTML document assembly, vendored font loading, Font Awesome injection, ECharts (SVG renderer), Tailwind injection, palette CSS injection, Playwright launch, page.pdf() rendering. All assets are local — zero network dependency at build time.
What NOT To Do
Rendering constraints:
- Do not use
<canvas>for charts — canvas elements blank in Playwright's PDF pipeline. Use ECharts SVG renderer or CSS-only visuals. - Do not add animation, transitions, or hover states. The output is static PDF.
- Do not use
@media printoverrides — the engine usesemulateMedia('screen'). - Use CSS Grid (
grid-template-rows: auto minmax(0,1fr) 48px) for the slide shell, notflex flex-colwithflex-1. See § Wrapped slide shell and § Slide layout anti-patterns.
Architecture constraints:
- Do not create separate HTML files per slide. All slides go in one build script, one HTML document, one PDF.
- Do not generate interactive presentations (Reveal.js, Slidev). Output is always static PDF.
- Do not hardcode palette colors — use CSS custom properties (
var(--accent),var(--text), etc.) so palette switching works.
Design discipline:
- Do not copy example data into real decks. Example numbers ($4.2B, 12.4M units) are illustrative — use the user's actual data.
- Do not choose exhibit types by visual appeal. Choose by what the slide's argument needs to prove. A bold number can be more powerful than a chart.
- Do not over-decorate. No icon walls, no rainbow colors, no gradients on data elements. Consulting aesthetics come from restraint, not embellishment.
- Do not use the same exhibit type on consecutive slides. Alternate to maintain visual rhythm.
Agent behavior:
- Do not ask the user to pick a chart type. The taxonomy's decision tree and the ghost deck's action titles determine the exhibit. You make the visual decision.
- Do not ask the user for show vs. working deck preference. Infer from audience and objective — "board presentation" is show, "diligence data room" is working.
- Do not present multiple layout options for approval. Commit to the best composition based on the content density and argument structure. The user reviews the output PDF, not a menu of choices.
Build Reference — Slide Construction
Load this reference during Phase 2 (Build) when writing slide HTML, configuring ECharts, or structuring the build script. Not needed during planning or ghost deck construction.
Build script
For decks with more than 5 slides, define a CSS class system in headExtra rather than repeating inline Tailwind on every slide.
const { createDeck } = require('./skills/deck-design-pdf/engine');
createDeck({
palette: 'consulting-mckinsey',
title: 'Strategy Update Q1 2026',
output: 'strategy-update.pdf',
tier: 'document', // 'document' (default) or 'presentation' for show decks
width: 1280, // optional
height: 720, // optional
headExtra: `<style>
/* Slide shell — CSS Grid prevents footer overflow */
.deck-shell { height: 100%; display: grid; grid-template-rows: auto minmax(0,1fr) 48px; }
.deck-header { padding: 28px 56px 8px 56px; border-top: 4px solid var(--accent); }
.deck-body { min-height: 0; overflow: hidden; padding: 12px 56px 8px 56px; }
.deck-footer { display: flex; align-items: center; justify-content: space-between;
padding: 0 56px; font-size: 12px; color: var(--text-fine);
border-top: 1px solid rgba(0,0,0,0.06); font-family: var(--font-body); }
.deck-section { margin: 0 0 4px; font-size: 12px; letter-spacing: 0.2em;
text-transform: uppercase; color: var(--accent); font-weight: 700; }
.deck-title { margin: 0 0 8px; font-size: 30px; line-height: 1.15; letter-spacing: -0.03em;
color: var(--text); font-family: var(--font-heading); font-weight: 700; }
.deck-rule { height: 3px; border-radius: 2px; background: var(--accent); margin-top: 4px; }
</style>`,
slides: [
// Each string is the inner HTML of one slide.
// The engine wraps each in <section class="slide">.
`<div class="deck-shell">
<div class="deck-header">
<p class="deck-section">Section Label</p>
<h2 class="deck-title">Action title — full sentence conclusion</h2>
<div class="deck-rule"></div>
</div>
<div class="deck-body">
<!-- content -->
</div>
<div class="deck-footer">
<span>Source: Analysis, 2026</span>
<span>Confidential</span>
</div>
</div>`,
],
});Run: node build-{slug}.js → produces the PDF directly.
Why `headExtra` CSS classes over inline Tailwind for the shell:
- One change to
.deck-titlefixes all 20+ slides — vs editing each slide'stext-3xl font-semibold leading-snug tracking-tight - The grid shell guarantees footer placement structurally
- Token-efficient: a class name is ~15 chars vs ~80 chars of Tailwind utilities per slide
- Tailwind is still used for body content layout (grids, gaps, cards) — only the shell is CSS classes
ECharts formatter warning: Inside JavaScript template literals (backtick strings), '${value}' is interpreted as JS interpolation. Use function(v) { return '$' + v + 'B'; } instead. This applies to all formatter, axisLabel.formatter, and label.formatter properties.
Modular authoring for large decks
For decks with more than ~10 slides, consider authoring each slide as a separate module file. The createDeck() API is unchanged — the convention only affects how the slide strings are assembled.
Recommended structure:
build-deck-name/
slides/
s01-cover.js
s02-context.js
s04-investment.js
...
s17-flywheel.js
shared/
head-extra.js ← palette tokens, CSS classes
build.js ← imports slides, calls createDeck()Each slide module exports exactly two things:
// s04-investment.js
module.exports = {
html: `<div class="deck-shell">...</div>`,
script: `(function() { /* echarts init for this slide */ })();`
};build.js assembles and calls createDeck():
const slides = require('./slides/s04-investment');
// ...
createDeck({
slides: [s01, s02, s04, ...].map(s => s.html + (s.script ? '<script>' + s.script + '</script>' : '')),
headExtra,
// ...
});Why this matters for quality:
- Each slide file is 80–120 lines — readable in one pass; the agent can fix S11 without scrolling through 1,500 lines
- Slide-level isolation: changing S11's chart height cannot accidentally touch S14
- The space budget comment at the top of each module makes sizing decisions auditable
- One file = one argument = one layout decision — matches the ghost deck's one-slide-one-claim rule
This is a convention, not a requirement. For decks under 10 slides, a single build script is fine.
Slide shell
The shell uses CSS Grid with fixed row heights — this structurally prevents footer overflow, which is the most common layout failure in deck production.
<div style="height: 100%; display: grid; grid-template-rows: auto minmax(0, 1fr) 48px;">
<!-- HEADER ZONE — action title with accent bar -->
<div class="px-14 pt-8 pb-2 border-t-4 border-[var(--accent)]">
<p class="text-xs font-semibold tracking-widest uppercase text-[var(--accent)] mb-1">
Section Label
</p>
<h2 class="text-3xl font-semibold leading-snug tracking-tight text-[var(--text)]"
style="font-family: var(--font-heading)">
Action title — full sentence conclusion
</h2>
</div>
<!-- BODY ZONE — all content lives here; overflow is clipped, never pushes footer -->
<div class="px-14 py-3" style="min-height: 0; overflow: hidden;">
<!-- Layout primitives go here (grids, cards, charts, etc.) -->
</div>
<!-- FOOTER ZONE — fixed 48px row, cannot be displaced -->
<div class="px-14 flex items-center justify-between
text-xs text-[var(--text-fine)]"
style="font-family: var(--font-body); border-top: 1px solid var(--accent-light, #ccc)33;">
<span>Source: Analysis team, 2026</span>
<span>Confidential</span>
</div>
</div>Why grid, not flex: The grid-template-rows: auto minmax(0, 1fr) 48px layout guarantees three things: 1. Header takes its natural height (auto) 2. Body fills remaining space but cannot push the footer off-slide (minmax(0, 1fr)) 3. Footer is always exactly 48px from the bottom, regardless of body content
The older flex flex-col h-full + flex-1 approach allowed body overflow to push the footer off the slide — a failure that required manual QA to catch.
Cover slides and dividers replace this shell with a full-bleed layout (see Layout primitives below).
Atomic exhibits vs wrapped slides
The reusable production unit is the atomic exhibit: graph/table/visual only, with no header or footer baked into the template. Examples under examples/*.js export:
{
id,
title,
tier,
proves,
data,
sectionLabel,
actionTitle,
source,
exhibitId,
responsiveSpec, // optional: chart ranges or layout QA samples
renderExhibit({ checkpoint, width, height, tokens }) { ... }
}Use the shared helpers in examples/_shared.js to render:
- an exhibit-only QA artifact
- a wrapped slide reference artifact
Treat examples/_shared.js as the internal standards layer. Share only reusable typography, text roles, chart chrome, and repeated semantic colors. Keep exhibit geometry, data mapping, label placement, and one-off visual emphasis inside each template.
Layout anti-patterns
Common failures in deck production. Avoid these:
| Anti-pattern | What happens | Fix |
|---|---|---|
| `flex-1` on cards inside a grid | Cards stretch to fill the grid cell height, leaving huge empty zones | Don't use flex-1 on cards. Let cards be auto-height. Use items-start on the grid. |
| `flex-1` on body zone | Short content expands with whitespace; long content pushes footer off-slide. | Use the CSS Grid shell. The minmax(0, 1fr) body row handles both cases. |
| `absolute` positioning for diagrams | Elements float at hardcoded pixel offsets, break at different content sizes. | Use CSS Grid or Flexbox for all diagrams. |
| Template literal `${value}` in ECharts formatters | Inside a JS backtick string, evaluates as JavaScript, not ECharts template. | Use function(v) { return v + '%'; } for formatters. |
| `mt-auto` on card metadata | Pushes metadata to card bottom, leaving visual gap after content. | Use mt-4 (fixed margin). Metadata should follow the content. |
| Rounded corners everywhere | Excessive rounded-xl creates a product/SaaS aesthetic, not consulting. | Use rounded-lg (8px) max. MBB decks prefer rounded (4px) or sharp corners. |
| `align-items:center` on mismatched columns | Unequal content heights float to vertical midpoint, leaving blank space. | Use align-items:start when columns differ in height. |
| Landscape-only chart sizing | Copying example heights (200–300px) on a 600px body zone leaves 300+px blank. | Start from body height, subtract surrounding elements, set chart to remainder. Portrait is valid. |
| Newline characters inside ECharts script blocks | Literal \n in single-quoted JS strings causes SyntaxError — chart renders blank, no visible error. | Use \n escape sequences. Wrap ECharts init in an IIFE (function(){ ... })();. |
Design tokens
Every palette provides these CSS custom properties. Use via Tailwind arbitrary values.
| Token | Purpose | Tailwind |
|---|---|---|
--surface | Default slide background | bg-[var(--surface)] |
--surface-dark | Cover / divider background | bg-[var(--surface-dark)] |
--surface-muted | Cards, evidence panels | bg-[var(--surface-muted)] |
--accent | Brand color, header bars, highlights | text-[var(--accent)] / border-[var(--accent)] |
--accent-light | Borders, light tint | border-[var(--accent-light)] |
--text | Primary text | text-[var(--text)] |
--text-muted | Secondary text, annotations | text-[var(--text-muted)] |
--text-fine | Footnotes, sources | text-[var(--text-fine)] |
--text-on-dark | Text on dark surfaces | text-[var(--text-on-dark)] |
--font-heading | Heading font family | style="font-family: var(--font-heading)" |
--font-body | Body font family | style="font-family: var(--font-body)" |
--chart-primary | Primary chart/data color | For inline SVG fills |
--chart-secondary | Secondary chart color | For inline SVG fills |
Typography scale
Two systems exist for typography sizing. They serve different contexts — don't mix them.
1. Slide HTML — use the Tailwind class table below. Pick a role, use its class. No programmatic tokens. 2. ECharts exhibits — use the responsive tokens from examples/_shared.js. ECharts takes fontSize as a JS number, so Tailwind classes don't apply inside chart config.
Tailwind type roles — slide HTML
Every text element in slide HTML must use one of these roles. All roles use native Tailwind classes — no arbitrary pixel values except tag/badge.
| Role | Tailwind class | Rendered | Weight | Extras | Use for |
|---|---|---|---|---|---|
| Cover title | text-5xl | 48px / 36pt | font-bold | tracking-tight | Title slide headline |
| Action title | text-3xl | 30px / 22.5pt | font-semibold | tracking-tight leading-snug | Slide conclusion sentence |
| Card metric | text-2xl | 24px / 18pt | font-bold | tracking-tight leading-none | "$240B", "200%+", large KPI numbers |
| Subtitle | text-lg | 18px / 13.5pt | font-normal | — | Cover subtitle, context line |
| Body | text-base | 16px / 12pt | font-normal | leading-relaxed | Paragraphs, bullet descriptions, card body text |
| Body small | text-sm | 14px / 10.5pt | font-normal | leading-relaxed | Secondary descriptions, table cells, annotations |
| Card label | text-sm | 14px / 10.5pt | font-semibold | — | Metric name below the number |
| Callout body | text-sm | 14px / 10.5pt | font-normal | leading-relaxed | Text inside insight/callout boxes |
| Card detail | text-xs | 12px / 9pt | font-normal | — | Trend indicators, supplementary data |
| Section label | text-xs | 12px / 9pt | font-semibold | tracking-widest uppercase | "MOMENTUM DIAGNOSTIC" above title |
| Panel header | text-xs | 12px / 9pt | font-bold | tracking-widest uppercase | "KEY METRICS" — mini-headers |
| Data label | text-xs | 12px / 9pt | font-medium | — | Chart annotations, axis labels in HTML |
| Footer / source | text-xs | 12px / 9pt | font-normal | — | Source line, confidentiality notice |
| Tag / badge | text-[10px] | 10px / 7.5pt | font-medium | px-2 py-0.5 rounded-full | Pill labels inside cards |
Legibility floor: No text below text-xs (12px) except tags/badges. Body-readable text must be ≥text-sm (14px).
L1 density adaptation: Bump each role up one Tailwind step. See density-adaptation.md § L1 Sizing for the full mapping.
Tailwind ↔ px reference (default theme):
| Class | px | pt |
|---|---|---|
text-xs | 12px | 9pt |
text-sm | 14px | 10.5pt |
text-base | 16px | 12pt |
text-lg | 18px | 13.5pt |
text-xl | 20px | 15pt |
text-2xl | 24px | 18pt |
text-3xl | 30px | 22.5pt |
text-4xl | 36px | 27pt |
text-5xl | 48px | 36pt |
text-6xl | 60px | 45pt |
ECharts exhibit tokens
For chart/exhibit JavaScript, use the responsive token system from examples/_shared.js:
tokens.bodyText/tokens.smallText/tokens.microText— text sizes that scale with canvastokens.adapt(compact, preferred, wide)— continuous interpolation for geometrytokens.textAdapt(compact, preferred, wide)— slower compression for text legibilitygetFigureTypography(tokens)— axis, legend, label, annotation, metric role presetsgetChartChrome(tokens)— shared axis/gridline/legend config fragments
Use token roles wherever the styling is standard. Do not abstract geometry, chart-specific layout, or exhibit-specific emphasis into shared helpers.
Spatial constants
| Zone | Value | Note |
|---|---|---|
| Horizontal padding | px-14 (56px) | Content never touches slide edges |
| Header top padding | pt-10 (40px) | Space above section label / title |
| Header accent bar | border-t-4 (4px) | Top border in accent color |
| Body vertical padding | py-4 (16px) | Breathing room in body zone |
| Column gap | gap-6 – gap-8 | Between grid columns (24–32px) |
| Card gap | gap-3 – gap-4 | Between card items (12–16px) |
| Card border radius | rounded-lg | Consistent card rounding |
| Footer height | py-2 (~40px) | Compact, fine text |
Icons — Font Awesome 6
Font Awesome 6 Free is vendored locally. Use <i> tags inline with text or as standalone decorators only when the glyph is render-stable in PDF output.
Common icons for consulting slides:
| Category | Icons |
|---|---|
| Trends | fa-arrow-trend-up, fa-arrow-trend-down, fa-arrow-right |
| Status | fa-circle-check, fa-circle-xmark, fa-triangle-exclamation |
| Metrics | fa-chart-line, fa-chart-bar, fa-chart-pie, fa-percent |
| Financial | fa-dollar-sign, fa-coins, fa-money-bill-trend-up |
| Org | fa-building, fa-users, fa-user-tie, fa-sitemap |
| Strategy | fa-bullseye, fa-lightbulb, fa-rocket, fa-chess |
| Process | fa-gears, fa-arrows-spin, fa-diagram-project |
| Risk | fa-shield-halved, fa-lock, fa-bug |
| Time | fa-clock, fa-calendar, fa-hourglass-half |
| Data | fa-database, fa-server, fa-cloud |
Usage pattern:
<!-- Inline with text -->
<span class="text-[var(--accent)]"><i class="fa-solid fa-arrow-trend-up mr-2"></i>12% YoY</span>
<!-- Card decorator (large) -->
<i class="fa-solid fa-chart-line text-3xl text-[var(--accent)] mb-3"></i>
<!-- Status indicator -->
<i class="fa-solid fa-circle-check text-emerald-500 mr-1"></i>
<i class="fa-solid fa-triangle-exclamation text-amber-500 mr-1"></i>
<i class="fa-solid fa-circle-xmark text-red-500 mr-1"></i>Icons work best as visual anchors on KPI cards, process steps, and list items. Do not over-decorate.
Data Visualization — ECharts 5
ECharts 5 is vendored locally with SVG renderer (mandatory — <canvas> blanks in Playwright PDF). For exhibit selection, use chart-taxonomy.md.
Chart container pattern:
<!-- In the slide HTML, place a div with explicit dimensions -->
<div id="chart-1" style="width: 100%; height: 340px;"></div>
<!-- After ALL slides, add a <script> block that initializes charts -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const chart1 = echarts.init(document.getElementById('chart-1'), null, { renderer: 'svg' });
chart1.setOption({
animation: false, // mandatory for static PDF
// ... option config
});
});
</script>Important: The <script> block goes inside the slide HTML string (after the layout HTML but before the closing backtick). For multi-chart slides, use unique IDs and initialize all charts in a single <script> block.
MBB chart aesthetics — the configuration that makes charts look consulting-quality:
{
animation: false,
grid: { left: 48, right: 24, top: 32, bottom: 40, containLabel: true },
textStyle: { fontFamily: 'Inter, system-ui, sans-serif', fontSize: 12, color: '#4E6176' },
title: { show: false },
tooltip: { show: false },
legend: { bottom: 0, textStyle: { fontSize: 11, color: '#4E6176' }, itemWidth: 12, itemHeight: 12, itemGap: 16 },
xAxis: { axisLine: { lineStyle: { color: '#C7D5E5' } }, axisTick: { show: false },
axisLabel: { fontSize: 11, color: '#4E6176' }, splitLine: { show: false } },
yAxis: { axisLine: { show: false }, axisTick: { show: false },
axisLabel: { fontSize: 11, color: '#4E6176' },
splitLine: { lineStyle: { color: '#E4EDF7', type: 'dashed' } } },
}Chart type routing:
| Use case | ECharts type | Key config |
|---|---|---|
| Revenue/metric comparison | type: 'bar' | Vertical bars, accent primary, gray secondary |
| Trend over time | type: 'line' | smooth: false for MBB, areaStyle for area |
| Market share / composition | type: 'pie' | radius: ['40%', '70%'] for donut |
| Scatter / positioning | type: 'scatter' | symbolSize by value for bubble |
| Revenue bridge / delta | type: 'bar' (stacked) | Invisible base + colored delta — waterfall |
| Performance meter | type: 'gauge' | detail.formatter, clean arc |
| Flow / allocation | type: 'sankey' | orient: 'horizontal', nodeWidth: 20 |
| Hierarchy / budget | type: 'treemap' | visibleMin: 300, label.fontSize: 12 |
| Multi-criteria radar | type: 'radar' | indicator array, filled area |
| Pipeline / conversion | type: 'funnel' | sort: 'descending', label inside |
| Correlation matrix | type: 'heatmap' | With visualMap for color mapping |
Waterfall chart pattern (MBB signature):
series: [
{ name: 'base', type: 'bar', stack: 'waterfall',
data: [0, 3800, 3800, 4100, 3900],
itemStyle: { color: 'transparent' },
emphasis: { itemStyle: { color: 'transparent' } },
},
{ name: 'delta', type: 'bar', stack: 'waterfall',
data: [3800, 500, 300, -200, 300],
itemStyle: { color: (params) => params.data >= 0 ? '#123A63' : '#CC4444' },
label: { show: true, position: 'top',
formatter: (p) => (p.data >= 0 ? '+' : '') + p.data,
fontSize: 11, fontWeight: 'bold' },
},
]Color discipline for charts:
- Primary series:
var(--chart-primary)or the hex value from the palette - Secondary series:
var(--chart-secondary)— always gray - Positive delta: accent color or
#2E9E5A(green) - Negative delta:
#CC4444(red) - Never more than 4 colors in one chart. MBB charts are restrained.
CSS-only bar charts (no ECharts needed for simple bars):
<div class="flex items-center gap-3 mb-2">
<span class="w-24 text-sm text-[var(--text)] text-right shrink-0">Revenue</span>
<div class="flex-1 bg-[var(--surface-muted)] rounded-full h-6 overflow-hidden">
<div class="h-full bg-[var(--accent)] rounded-full" style="width: 78%"></div>
</div>
<span class="text-sm font-semibold text-[var(--text)] w-12">78%</span>
</div>Use CSS-only bars for simple progress/comparison visuals. Use ECharts for anything with axes, legends, or complex data.
Responsive support model
Do not author examples against hardwired sm/md/lg buckets. The reference canvas remains 1280×720, but responsiveness follows the actual target width and height.
Use one of two contracts:
1. Chart / figure templates
responsiveSpec: {
templateClass: 'chart',
exhibitRange: {
min: { width: 960, height: 540 },
preferred: { width: 1280, height: 720 },
max: { width: 1600, height: 900 },
},
slideRange: {
min: { width: 1024, height: 576 },
preferred: { width: 1280, height: 720 },
max: { width: 1600, height: 900 },
},
rationale: 'end labels become unreadable below the declared minimum',
}2. Layout / content templates
responsiveSpec: {
templateClass: 'layout',
previewSamples: [
{ label: 'compact', width: 1024, height: 576 },
{ label: 'preferred', width: 1280, height: 720 },
{ label: 'wide', width: 1440, height: 810 },
],
agentSizingNotes: 'agent should split or simplify if width falls below the compact QA sample',
}Layout primitives
Six layout structures for the body zone. Combine with the slide shell above.
| Layout | Grid | Use when |
|---|---|---|
| Single column | flex flex-col gap-4 | Narrative text, ordered list, simple argument |
| Two-column | grid grid-cols-2 gap-8 | Chart + bullets, comparison, evidence + sidebar |
| Asymmetric split | grid grid-cols-[3fr_2fr] gap-8 | Main exhibit + sidebar (55/45) |
| Three-column | grid grid-cols-3 gap-6 | KPI cards, feature comparison, triple evidence |
| Card grid | grid grid-cols-3 gap-4 (or 2/4) | Scorecards, team, feature tiles |
| Full bleed | No shell — direct child of <section> | Cover, divider, hero image |
Content primitives
Small HTML components to compose inside layout primitives.
KPI Metric Card:
<div class="bg-[var(--surface-muted)] rounded-lg p-5 flex flex-col">
<i class="fa-solid fa-chart-line text-2xl text-[var(--accent)] mb-3"></i>
<span class="text-2xl font-bold tracking-tight text-[var(--text)]">$4.2B</span>
<span class="text-sm font-semibold text-[var(--text)] mt-1">Revenue</span>
<span class="text-xs text-[var(--text-muted)] mt-1">
<i class="fa-solid fa-arrow-trend-up text-emerald-500 mr-1"></i>↑ 12% YoY
</span>
</div>Evidence Panel (bordered):
<div class="border border-[var(--accent-light)]/40 rounded-lg p-5">
<h3 class="text-xs font-bold tracking-widest uppercase text-[var(--accent)] mb-3">
Key Drivers
</h3>
<ul class="space-y-2 text-sm text-[var(--text)]" style="font-family: var(--font-body)">
<li class="flex items-start gap-2">
<i class="fa-solid fa-chevron-right text-[10px] text-[var(--accent)] mt-1.5 shrink-0"></i>
<span>Market share grew 3pp driven by pricing advantage in mid-tier segment</span>
</li>
</ul>
</div>Data Table:
<table class="w-full text-sm" style="font-family: var(--font-body)">
<thead>
<tr class="border-b-2 border-[var(--accent)]">
<th class="text-left py-2 font-semibold text-[var(--text)]">Metric</th>
<th class="text-right py-2 font-semibold text-[var(--text)]">2024</th>
<th class="text-right py-2 font-semibold text-[var(--text)]">2025</th>
<th class="text-right py-2 font-semibold text-[var(--text)]">Delta</th>
</tr>
</thead>
<tbody class="text-[var(--text)]">
<tr class="border-b border-[var(--accent-light)]/30">
<td class="py-2">Revenue</td>
<td class="text-right py-2 tabular-nums">$3.8B</td>
<td class="text-right py-2 tabular-nums">$4.2B</td>
<td class="text-right py-2 font-semibold text-emerald-600">+11%</td>
</tr>
</tbody>
</table>Callout Box:
<div class="bg-[var(--accent)]/5 border-l-4 border-[var(--accent)] px-5 py-4 rounded-r-lg">
<p class="text-sm font-medium text-[var(--text)]">
<i class="fa-solid fa-lightbulb text-[var(--accent)] mr-2"></i>
Key insight or recommendation text here
</p>
</div>Composition routing
Map each slide's communication move to a layout composition.
| User intent / data shape | Layout | Content primitives | Notes |
|---|---|---|---|
| Cover / title page | Full bleed | Centered title + subtitle on dark surface | No shell — full h-full |
| Section divider | Full bleed | Section number (large) + title on dark | Page break rhythm |
| Agenda / TOC | Single column | Numbered list items, active item highlighted | Accent color on active |
| 4–6 KPIs / metrics | Card grid (3-col or 2×3) | KPI metric cards | Icons + trend indicators |
| Claim + chart evidence | Asymmetric split | Chart (left) + evidence panel (right) | SVG chart or CSS bars |
| Binary choice (A vs B) | Two-column | Comparison cards with criteria rows | Highlight recommended |
| Process / mechanism | Single column | Numbered step cards with icons | Horizontal or vertical |
| Data comparison table | Single column | Data table + optional callout box | Color-coded deltas |
| Timeline / milestones | Single column | Horizontal timeline with markers | CSS flex with dots |
| Waterfall / bridge | Single column | CSS bar chart + interpretation panels | Start → deltas → end |
| Evaluation matrix | Single column | Grid table with RAG cell fills | Color-coded ratings |
| Facts vs perspectives | Two-column | Left: fact bullets + chart. Right: perspective bullets | Bain signature |
| Multi-evidence composite | Asymmetric split | Main chart + metric cards below + sidebar | L3 density |
| Team / founders | Card grid (4-col) | Name + role + bio cards | Optional photo/icon |
| Closing / recommendation | Full bleed or single | Centered recommendation + next steps list | Clean, authoritative |
| Workplan / Gantt | Single column | CSS grid calendar with colored spans | Time periods as columns |
Exhibit Design — Practitioner Playbook
The chart is never the point. The argument is the point. The chart is proof.
>
Load this reference when designing exhibits. Don't start with "what chart?"
Start with "what does this slide need to prove?"
The Exhibit Principle
An exhibit is NOT a chart. An exhibit is a visual argument:
| Element | Role | Example |
|---|---|---|
| Action title | The conclusion you want believed | "Revenue grew 12% driven entirely by pricing power" |
| Exhibit body | The evidence (chart, table, visual) | Waterfall: price +14%, volume −2%, mix 0% |
| Annotation | The interpretation — what the data means | Callout: "Volume was flat — all growth was price" |
| Source line | The credibility | "Source: Company financials, team analysis" |
A chart without an action title is data. A title without evidence is assertion. Together they form proof.
---
Exhibit Selection — Decision Tree
Given the slide's claim, follow the path to the right exhibit.
"How big is it?" — Sizing, Comparison, Benchmarking
| You need to... | Use | Config |
|---|---|---|
| Rank items by one metric | Horizontal sorted bars | ECharts bar horizontal, sorted desc |
| Compare across 2–3 metrics | Clustered bars | ECharts bar, max 3 series |
| Show value vs. benchmark | Bars + reference line | ECharts bar + markLine |
| Land a single headline metric | KPI card (CSS) | Large number + trend + context |
| Build up a market size | Additive waterfall | Segment → segment → total |
| Show "who plays where" across segments | Marimekko | ECharts custom, variable-width rects |
Practitioner note: A single large number ($4.2B, 44px bold, centered) often hits harder than any chart. If the number is surprising, let it land alone.
"Why did it happen?" — Decomposition, Drivers
| You need to... | Use | Config |
|---|---|---|
| Show how a KPI moved A→B | Waterfall / bridge | ECharts stacked bar (invisible base + colored delta) |
| Price/volume/mix decomposition | Labeled waterfall | Same pattern, driver labels on category axis |
| Actual vs. plan gap | Variance bridge | Green (favorable) / red (unfavorable) |
| Cumulative contribution (80/20) | Pareto | ECharts bar + line combo |
| Multiplicative drivers | Driver tree (CSS/HTML) | Revenue = Volume × Price × Mix |
| Which input moves the needle most | Tornado | ECharts horizontal diverging bars |
| Before/after impact | Split bars | Paired bars or before→after with arrow |
Practitioner note: The waterfall is consulting's default exhibit for any delta question. Sort deltas by magnitude (largest positive first → largest negative last) unless chronological order matters more.
"What should we do?" — Positioning, Prioritization
| You need to... | Use | Config |
|---|---|---|
| Position items on 2 dimensions | 2×2 matrix | CSS Grid quadrants with labeled axes |
| Add size as 3rd variable | Bubble matrix | ECharts scatter, symbolSize by value |
| Rank by impact vs. feasibility | Priority matrix | Quadrant labels: Quick wins / Strategic bets / etc. |
| Multi-criteria evaluation | Eval grid or heatmap | RAG fills at 15–20% opacity. Define R/A/G. |
| Compare end states across scenarios | Scenario panel | Side-by-side bars or multiple waterfalls |
| Show what happens when | Roadmap / timeline | CSS Grid: time columns × workstream rows |
Practitioner note: The axis choice on a 2×2 IS the insight. The value is selecting dimensions that reveal something non-obvious.
---
Chart Repertoire by Deck Need
Grouped by when you need them, not by abstract categories. Build templates in this order.
Tier 1 — Foundation
Every consulting deck draws from these. Master these first.
| Chart | What it proves | Implementation |
|---|---|---|
| Horizontal sorted bars | Ranking / comparison | ECharts bar, horizontal, sorted desc |
| Waterfall / bridge | What drove the delta | ECharts stacked bar (invisible base + colored delta) |
| Line chart | Trend over time | ECharts line, smooth: false, label endpoints directly |
| Stacked bar | Composition | ECharts bar, stack: 'total', max 5 segments |
| KPI cards | Headline metrics at a glance | CSS — large number + trend indicator + context label |
| Data table | Structured evidence | HTML <table> with styled headers, aligned columns |
Tier 2 — Strategic
Strategy, recommendation, positioning, and evaluation decks.
| Chart | What it proves | Implementation |
|---|---|---|
| 2×2 matrix | Strategic positioning on two dimensions | CSS Grid 4-quadrant with axis labels |
| Bubble scatter | Position + magnitude (3 variables) | ECharts scatter, symbolSize by value, markLine quadrants |
| Donut | Dominant share question (2–5 segments only) | ECharts pie, radius: ['40%','70%'] |
| Evaluation grid | Multi-criteria assessment | HTML table with RAG cell background fills |
| Clustered bars | Multi-metric comparison | ECharts bar, multiple series, max 3 |
| 100% stacked bar | Share comparison across categories | ECharts bar, stacked, values as % |
| Scenario lines | Multiple forecast paths | ECharts line, dashed for scenario variants |
Tier 3 — Operational
Transformation, implementation, and monitoring decks.
| Chart | What it proves | Implementation |
|---|---|---|
| Gantt / roadmap | What happens when, with dependencies | CSS Grid (time cols × workstream rows), colored spans |
| RAG status matrix | What's on/off track across workstreams | HTML table with status cell fills |
| Process flow | How work moves through stages | CSS boxes + arrows (FA icons or SVG) |
| Timeline / milestones | Key dates and markers | CSS Flex with positioned dots + labels |
| Burndown line | Progress toward target over time | ECharts line + target markLine |
| Before/after bars | Impact of intervention | Paired bars or split comparison |
Tier 4 — Specialist
Specific analytical needs. Used when the analysis demands it, not routine.
| Chart | What it proves | Implementation |
|---|---|---|
| Tornado / sensitivity | Which variables have most impact | ECharts horizontal diverging bars from center |
| Pareto | Cumulative contribution (80/20 rule) | ECharts bar + line combo, dual axis |
| Marimekko | Market landscape — size AND composition | ECharts custom series, variable-width rects |
| Sankey | How value/material/flow distributes | ECharts sankey, max 8 nodes |
| Radar | Multi-dimensional profile (5–8 dims) | ECharts radar with indicator array |
| Funnel | Pipeline conversion by stage | ECharts funnel, label conversion rates |
| Treemap | Hierarchical breakdown (>12 items) | ECharts treemap, visibleMin: 300 |
| Heatmap | Intensity across two dimensions | ECharts heatmap + visualMap |
| Driver tree | Multiplicative causal decomposition | CSS/HTML nested layout with connector lines |
| Fan chart | Forecast with widening confidence range | ECharts line + stacked areaStyle bands |
| Indexed lines | Growth rates from different bases | ECharts line, all series rebased to 100 |
| Swimlane | Process across organizational lanes | CSS Grid with lane rows |
CSS-Only vs. ECharts
CSS-only (simpler, more reliable in PDF pipeline):
- Horizontal progress bars, simple comparisons (<5 items)
- 2×2 matrices, priority grids, evaluation grids
- Process flows, swimlanes, driver trees
- Timelines, roadmaps, Gantt
- KPI cards, data tables, RACI
ECharts (when you need axes, computed layouts, or complex data):
- Any chart with axes (bar, line, scatter)
- Waterfalls (stacked bar with invisible base)
- Sankey, treemap, radar, funnel, heatmap
- Pareto (dual axis bar + line)
- Any data series with >5 data points
---
Firm Visual Identity
Documented — What Each Firm Actually Invented
Historical facts that shape each firm's visual DNA.
McKinsey: Barbara Minto developed the Pyramid Principle at McKinsey (1960s–70s) — structured communication where the conclusion leads and evidence follows. This is why McKinsey-style slides open with the verdict in the action title. Every slide proves one conclusion stated upfront.
BCG: Bruce Henderson created the Growth-Share Matrix in 1970 — the original 2×2 matrix (Stars, Cash Cows, Dogs, Question Marks). This established two-dimensional strategic positioning as a fundamental consulting tool. BCG's analytical framework tradition descends from this: process-transparent, framework-anchored arguments.
Bain: Fred Reichheld (Bain Fellow) created the Net Promoter Score in 2003. Bain positions around "Results Delivery" — every analysis connects to a specific decision and measurable outcome. This action-orientation shapes their visual language: always tie the exhibit to "what should the CEO do?"
Observable — What's Visible in Public Materials
From published reports, brand guidelines, and public presentations:
| Firm | Primary color | Design character | Observable in published materials |
|---|---|---|---|
| McKinsey | Navy / blue | Institutional, minimal | Exhibit numbering, restrained blues + grays, data-forward, little decoration |
| BCG | Green / teal | Framework-rich | More framework-heavy layouts, green accent system, analytical transparency |
| Bain | Red accent | Concise, direct | Red as focal accent, shorter documents, decision-oriented framing |
Practitioner Conventions — Honest Assessment
These are real patterns but they vary by partner, office, and engagement. Treat as useful defaults, not hard rules.
Communication style is the real differentiator — more than chart selection:
| Firm | Slide structure | Action title style |
|---|---|---|
| McKinsey | Verdict-first. Conclusion in title, evidence below. | Full-sentence conclusions: "Revenue grew 12% driven by pricing power in mid-tier" |
| BCG | Framework-first. Show the analytical structure, then the conclusion it produces. | Framework-anchored claims: "Three of five capability dimensions exceed peer median" |
| Bain | Decision-first. Facts separated from interpretation. Data vs. judgment is always explicit. | Decision-oriented: "Cost structure supports 15% price reduction without margin erosion" |
What's universal (NOT firm-specific):
- Waterfalls, bars, lines, stacked bars — foundational exhibits used identically across all three firms
- Marimekko charts for market landscape — used across firms, not proprietary to any one
- 2×2 matrices — universal despite BCG's historical association
- Data tables, KPI cards — universal
What's genuinely firm-specific:
- Bain's Facts-vs-Perspectives layout — slides split into facts (left, ~55%, data/evidence) and perspectives (right, ~45%, interpretation/recommendation). This separates what the data shows from what the firm recommends. I've not seen this used systematically at McKinsey or BCG. This is the ONE clearly differentiating visual convention.
What varies by partner more than by firm:
- Annotation density (minimal vs. liberal) — some partners at any firm want callout boxes, some kill them
- Chart type tolerance (radar love/hate, pie chart acceptance) — partner-specific
- Color accent system details — each firm has brand colors, but individual engagements vary
Practical guidance for the skill:
- McKinsey palette → navy primary, minimal annotation, two-tier header (section label + action title), clean exhibits that prove the title without additional interpretation
- BCG palette → green primary, annotation welcomed, panel mini-headers on multi-panel slides, green rule below action title as visual signature
- Bain palette → grayscale + red accent on focal element, simpler header (title + red rule), facts-vs-perspectives on analytic slides
---
Deck Archetypes
By what the deck needs to DO. Exhibit mixes below are approximate guidelines — actual decks vary.
Diagnostic / Current State
Purpose: "Here's what we found." Arc: Performance context → Gap analysis → Root causes → Value at stake → Implications
| Exhibit type | Approx. share | Proves |
|---|---|---|
| Benchmark bars (client vs. peers) | ~30% | "We rank 7th of 12 on cost efficiency" |
| Variance bridges | ~25% | "Margin declined 400bps: 200 cost, 200 mix" |
| Heatmap / assessment grid | ~20% | "Capabilities lag in 3 of 7 dimensions" |
| Process flow (pain points marked) | ~15% | "Bottleneck at Stage 3 adds 14 days" |
| KPI cards | ~10% | Headline context |
Strategic Recommendation
Purpose: "Here's what you should do." Arc: Strategic context → Options assessed → Recommended path → Value at stake → Roadmap → Next steps
| Exhibit type | Approx. share | Proves |
|---|---|---|
| 2×2 / positioning matrices | ~25% | "Target the underserved high-growth quadrant" |
| Scenario comparisons | ~20% | "Path B delivers 2.3× more value" |
| Initiative waterfalls | ~20% | "Five initiatives generate $340M total" |
| Priority matrices | ~15% | "Three quick wins, two strategic bets" |
| Roadmap / timeline | ~10% | "Phase 1: 90 days, Phase 2: 6 months" |
| KPI targets | ~10% | Target-state metrics |
Transformation Roadmap
Purpose: "Here's how we'll execute." Arc: Initiative portfolio → Sequencing → Governance → Quick wins → Milestones
| Exhibit type | Approx. share | Proves |
|---|---|---|
| Gantt / workplan | ~25% | Workstreams × time with dependencies |
| Initiative waterfall | ~20% | Value contribution by initiative |
| RAG status matrix | ~20% | Status across workstreams |
| Milestone timeline | ~15% | 30-60-90 day markers |
| RACI / org | ~10% | Ownership and accountability |
| KPI targets | ~10% | How we'll measure success |
Board / Steering Committee
Purpose: "Here's where we are." Arc: Headlines → KPIs → Progress vs. plan → Risks → Decisions needed
| Exhibit type | Approx. share | Proves |
|---|---|---|
| KPI dashboard cards | ~30% | 4–6 metrics with RAG indicators |
| RAG status matrix | ~25% | All initiatives at a glance |
| Variance bridges | ~20% | Plan vs. actual deltas |
| Burndown / progress lines | ~15% | On track or not |
| Decision tables | ~10% | Options requiring board input |
Due Diligence / Investment Memo
Purpose: "Here's what this asset is worth." Arc: Market → Competitive position → Financial performance → Growth → Risks → Valuation → Recommendation
| Exhibit type | Approx. share | Proves |
|---|---|---|
| P&L bridges / waterfalls | ~20% | Margin decomposition, cost structure |
| Competitive positioning | ~20% | Where target sits vs. peers |
| Market sizing waterfall | ~15% | TAM build-up by segment |
| Growth projection lines | ~15% | Scenario-based revenue paths |
| Synergy waterfall | ~15% | Synergy by type (revenue, cost, capex) |
| Risk heatmap | ~10% | Probability × impact matrix |
| Target KPIs | ~5% | Key operating metrics |
Investor / Roadshow
Purpose: "Here's why you should invest." Arc: Problem → Solution → Traction → Market → Model → Competition → Team → Ask
| Exhibit type | Approx. share | Proves |
|---|---|---|
| Growth metrics (line, bar) | ~25% | MRR, users, retention, cohort curves |
| Market sizing | ~20% | TAM/SAM/SOM, waterfall build-up |
| KPI cards | ~20% | Headline traction (ARR, NRR, DAU, etc.) |
| Competitive positioning | ~15% | Where we sit vs. alternatives |
| Team cards | ~10% | Founder/exec bios |
| Financial projections | ~10% | Revenue/burn/runway scenario lines |
---
Annotation Grammar
Annotations turn data into insight. The chart shows what happened; annotations show what it means.
| Type | Implementation | When |
|---|---|---|
| Value label | ECharts label: { show: true } | Always. Every bar/line endpoint. Non-negotiable. |
| Reference line | ECharts markLine with inline label | Benchmark, target, average. Label ON the line. |
| Callout box | CSS positioned div, border, near data point | Key interpretation. One per exhibit max. |
| Delta label | "+12%" formatted, color-coded green/red | Show change. Always include sign. |
| Highlight region | CSS absolute div, dashed border or circle | Circle a cluster on scatter/2×2. |
| Movement arrow | SVG path or CSS pseudo-element | Show direction over time on positioning charts. |
Annotation Rules
1. Every data point gets a direct label. Never force the reader to estimate from an axis. Most common correction in deck reviews. 2. Reference lines get text ON the line. "Target: $4.2B" sitting on the line — not in a legend. 3. One callout per exhibit. Two means the exhibit is trying to say two things — split it. 4. Callout = interpretation, not description. Wrong: "Revenue grew 12%." Right: "Growth was entirely price-driven — volume was flat." 5. Number formatting: "$4.2B" not "$4,234M". "23%" not "0.23". "3.2×" not "320%". Use the unit that makes comparison intuitive. 6. Delta formatting: Always include sign. "+12%" green, "−8%" red. Exception: when positive is bad (cost growth → red "+12%").
Callout Box Implementation
<!-- Position relative to chart container -->
<div class="relative">
<div id="chart-1" style="width: 100%; height: 340px;"></div>
<!-- Callout — positioned over chart area -->
<div class="absolute top-4 right-4 max-w-[200px] bg-white border border-[var(--accent)]
rounded px-3 py-2 text-xs text-[var(--text)] shadow-sm">
<span class="font-semibold text-[var(--accent)]">Key insight:</span>
Growth was entirely price-driven — volume was flat
</div>
</div>---
Consulting Compositions
These are NOT charts — they are structural layouts that no charting library produces. They answer organizational, governance, and synthesis questions where the visual is a system, not a data series.
When to use a composition instead of a chart
| The slide needs to show… | Use | Example file |
|---|---|---|
| Facts separated from interpretation | Facts vs. Perspectives | facts-perspectives.js |
| Decision-rights mapping (RAPID) | RAPID Decision Map | rapid-map.js |
| Value-driver decomposition with algebra (×, +) | Driver Tree | driver-tree.js |
| Target operating model / org structure | Org Model | org-model.js |
| Client vs. peer gap across multiple metrics | Dumbbell Benchmark | dumbbell-benchmark.js |
| Influence × alignment stakeholder positioning | Stakeholder Map | stakeholder-map.js |
| Activity → pipeline → outcome causal chain | KPI Cascade | kpi-cascade.js |
| Cross-team process with bottleneck identification | Swimlane | swimlane.js |
| Systematic option elimination logic | Screening Funnel | screening-funnel.js |
| Current vs. recommended resource allocation | Coverage Matrix | coverage-matrix.js |
| Sequenced stakeholder messaging plan | Communication Cascade | communication-cascade.js |
| Capability risk assessment with mitigations | Capability Matrix | capability-matrix.js |
| Approval-path bottleneck with cycle times | Decision Flow | decision-flow.js |
| Formal vs. informal collaboration structure | Network Map | network-map.js |
| Retention decay patterns across cohorts | Cohort Retention | cohort-retention.js |
| Total economic value decomposition for pricing | Economic Value Stack | economic-value-stack.js |
| Growth initiatives across time horizons | Three Horizons | three-horizons.js |
Composition characteristics
- CSS-only: facts-perspectives, rapid-map, driver-tree, org-model, kpi-cascade, swimlane, screening-funnel, coverage-matrix, communication-cascade, capability-matrix, decision-flow, three-horizons.
- ECharts: dumbbell-benchmark (scatter), stakeholder-map (scatter), network-map (graph), cohort-retention (heatmap), economic-value-stack (stacked bar).
- Firm affinity: Facts-vs-Perspectives is Bain's signature. RAPID maps are Bain-originated. Driver trees are McKinsey's signature. Economic value stacks are BCG-originated (EVE). Three horizons is McKinsey-originated. Others are universal.
- Density: All compositions work at L2 and L3. At L1, Facts-vs-Perspectives, Driver Tree, and Three Horizons adapt well; RAPID maps, Swimlanes, Capability matrices, and Decision flows are inherently L2+.
- Token-driven: All use
_shared.jstokens for responsive text and spacing. No hardcoded font sizes.
---
Aesthetic Rules
Apply regardless of chart type, firm, or archetype.
1. No chartjunk. No 3D, gradients, shadows, decorative elements. 2. Direct label > legend. Label data directly. Legend only when labeling would clutter (4+ overlapping series). 3. Max 4 colors per exhibit. Primary + gray + 1–2 semantic. Never rainbow. 4. Title in the slide header. title: { show: false } in ECharts. The action title IS the chart title. 5. No tooltips. tooltip: { show: false }. Static PDF. 6. No animation. animation: false. 7. Y-axis gridlines only. Dashed, light. No X gridlines. 8. Axis labels: Muted color, 11–12px. Reference, not content. 9. Color discipline: Use palette tokens. Primary accent + gray + 1–2 semantic colors max. 10. Spend visual budget on interpretation. If you can afford one extra element, make it a callout — not a gradient.
Density Adaptation — L1 / L2 / L3
How the same argument structure compresses or expands across density levels.
What Changes Between Levels
| Dimension | L1 Show Deck | L2 Working Deck | L3 Dense / Appendix |
|---|---|---|---|
| Audience | Board / SteerCo / CEO | Project team / working session | Technical reviewers / analysts |
| Purpose | Assert conclusions, get decisions | Expose machinery, invite interrogation | Full audit trail |
| Slide count | 60–70% of L2 | Baseline | 120–150% of L2 |
| Action titles | Conversational claim | Quantified claim | Chained claim with exhibit refs |
| Evidence layers | 1 | 1–2 | 2–4 |
| Transitions | Full pivot slides (dedicated, no data) | Embedded in section openers | In title chains only |
| Methodology | Hidden — appendix only | Architecture + key assumptions | Full model specification |
| Annotations per chart | 0 | 1–2 | 2–4 (required) |
L2 → L1 Compression (Show Deck)
1. Collapse methodology slides → move to appendix. Show deck shows WHAT, not how. 2. Merge diagnostic detail — 3 analytical slides building one insight → 1 slide stating the insight with supporting evidence. 3. Promote action titles — title alone must carry the argument. If you need to read the chart, the title is too weak. 4. Add explicit pivot slides — between pillars, insert a transition slide (no data). Net effect: remove analytical slides, add transitions. Count drops 30–40%. 5. Curate evidence — each slide gets ONE layer. Choose strongest proof point.
L1 compression is NOT "delete slides." It is "restructure the argument for an audience that spends 3 seconds per slide."
L2 → L3 Expansion (Dense / Appendix)
1. Expose model architecture — value driver tree structure, assumption tables, data source maps. 2. Add sensitivity analysis — for every quantified claim, show how conclusions change under different assumptions. 3. Chain exhibit references — number every exhibit. Titles reference prior exhibits. 4. Expand data tables — complete data, not curated subsets. Segment-level, time-series, statistical confidence. 5. Remove transition slides — transitions happen in title chains only.
Selection Heuristic
Board / SteerCo / CEO-only → L1 show deck + L2 appendix
Project team / working session → L2 working deck
Technical review / due diligence → L3 dense
Unsure → Default to L2Most engagements produce BOTH L1 and L2. Build L2 first, compress to L1.
Typical Slide Counts
| Engagement | L1 | L2 |
|---|---|---|
| Market Entry | 15–18 | 20–25 |
| Profitability | 18–20 | 25–32 |
| Growth Strategy | 14–17 | 20–24 |
| M&A | 12–15 | 18–22 |
| Pricing | 15–18 | 22–26 |
| Digital Transformation | 13–16 | 18–22 |
| Org Restructuring | 15–18 | 22–28 |
| Commercial Excellence | 14–17 | 20–24 |
Firm × Density Interaction
See firm-dna.md for full firm-specific density behavior. Key differences:
- McKinsey: Methodology goes to appendix even at L2. L1 appendix can be 40–60% of main deck length.
- BCG: L2 is native density. Panel mini-headers and annotation arrows persist even at L1. BCG at L1 still looks denser than McKinsey at L1.
- Bain: Decision brief stays at every density. Facts vs Perspectives only appears at L2+. Sprint plans at L1 show milestones only; at L2 they show week-by-week detail.
Typography Tier Mapping
Density and typography tier are related but distinct:
| Density | Typical tier | Rationale |
|---|---|---|
| L1 | 'presentation' | Room-safe, projection-safe. Body ≥15pt, axis ≥14pt. |
| L2 | 'document' or 'presentation' | Depends on venue — working session vs. conference room. |
| L3 | 'document' | Dense content needs smaller baselines. Body ≥12pt, axis ≥11pt. |
L1 Sizing — Fill, Don't Float
L1 means fewer elements per slide, not smaller content floating in empty space. Each element should scale up to fill the ~600px body zone. The fill-first rule applies at every density level — L1 achieves it with larger fonts and more breathing room between fewer items, not by leaving blank space at the bottom.
L1 typography rule: Bump each role up one native Tailwind step from the L2 baseline. Footer stays at the floor (text-xs).
| Role | L2 class | L1 class | L1 rendered |
|---|---|---|---|
| Cover title | text-5xl (48px) | text-6xl | 60px / 45pt |
| Action title | text-3xl (30px) | text-4xl | 36px / 27pt |
| Card metric | text-2xl (24px) | text-3xl | 30px / 22.5pt |
| Subtitle | text-lg (18px) | text-xl | 20px / 15pt |
| Body | text-base (16px) | text-lg | 18px / 13.5pt |
| Body small | text-sm (14px) | text-base | 16px / 12pt |
| Card label | text-sm (14px) | text-base | 16px / 12pt |
| Callout body | text-sm (14px) | text-base | 16px / 12pt |
| Card detail | text-xs (12px) | text-sm | 14px / 10.5pt |
| Section label | text-xs (12px) | text-sm | 14px / 10.5pt |
| Panel header | text-xs (12px) | text-sm | 14px / 10.5pt |
| Data label / axis | text-xs (12px) | text-sm | 14px / 10.5pt |
| Footer | text-xs (12px) | text-xs | 12px / 9pt (floor) |
The hierarchy holds at both levels: cover > action > metric > subtitle > body > labels > fine print. No ranges to decide — one class per role per density.
L1 chart sizing: Charts at L1 still follow the fill-first rule. A chart that is the sole exhibit on a slide should fill the body zone (≈ 540–580px after title and gap), not sit at 300px with whitespace below. L1 charts tend to be simpler (fewer series, fewer data points) but physically larger.
Engagement Archetypes & Transitions
Archetype classification, transition patterns, and exhibit routing for deck construction.
The Eight Archetypes
| # | Archetype | Core Question | Skeleton | Typical Slides (L2) |
|---|---|---|---|---|
| 1 | Market Entry | Should we enter? How? Where? | skeletons/market-entry.md | 20–25 |
| 2 | Profitability | Where is margin leaking? How to fix? | skeletons/profitability.md | 25–32 |
| 3 | Growth Strategy | Where should we grow? What levers? | skeletons/growth-strategy.md | 20–24 |
| 4 | M&A / Due Diligence | Should we buy? At what price? | skeletons/m-and-a.md | 18–22 |
| 5 | Pricing Strategy | Are prices right? What architecture? | skeletons/pricing-strategy.md | 22–26 |
| 6 | Digital Transformation | What tech to invest in? How to sequence? | skeletons/digital-transformation.md | 18–22 |
| 7 | Org Restructuring | How should we reorganize? | skeletons/org-restructuring.md | 22–28 |
| 8 | Commercial Excellence | How to reach customers better? | skeletons/commercial-excellence.md | 20–24 |
Hybrid engagements: Use one archetype as primary skeleton, pull pillar modules from secondary. Never stitch two full skeletons together.
Five Transition Types
Every pillar boundary needs a transition. These are the five canonical patterns:
| Type | Converts | Template |
|---|---|---|
| Pivot | Diagnostic → opportunity | "Given [finding], the [opportunity/risk] is [quantified] — [implication]" |
| Narrowing | Open exploration → focused evaluation | "Of [N] options, [M] pass [criteria] — question shifts to [specific]" |
| Escalation | Analytical confidence → implementation realism | "The [opportunity] is confirmed at [value]. Binding constraint is [risk]" |
| Synthesis | Multi-lens analysis → unified recommendation | "[Analysis 1] shows X, [Analysis 2] confirms Y — together they point to [Z]" |
| Decision Gate | Evaluation → decision mode | "Analysis narrows to [N] paths — decision turns on [differentiator]" |
Rising-Stakes Progression
| State | Pillar Role | If Skipped |
|---|---|---|
| Recognition | Context / Diagnostic | Audience doesn't understand why they're here |
| Clarity | Deep Analysis | Audience doesn't trust the analysis |
| Conviction | Financial / Economic Case | Agrees intellectually, won't commit resources |
| Urgency | Implementation / Timeline | Agrees but delays indefinitely |
Never skip a state. Each must be earned through evidence. Flat stakes = dead deck.
Common Transition Sequences
| Engagement | Transition Sequence |
|---|---|
| Market Entry | Pivot → Narrowing → Decision Gate |
| Profitability | Pivot → Escalation → (optional Synthesis) |
| M&A | Narrowing → Synthesis → Decision Gate |
| Growth | Pivot → Narrowing → Escalation |
| Pricing | Pivot → Synthesis → Decision Gate |
| Digital Transformation | Pivot → Narrowing → Escalation |
| Org Restructuring | Synthesis → Decision Gate → Escalation |
| Commercial Excellence | Pivot → Narrowing → Escalation |
Archetype → Exhibit Mix
Which example modules each archetype typically uses. Use this to auto-route from archetype to templates.
| Archetype | Signature exhibits | Supporting exhibits | Consulting compositions |
|---|---|---|---|
| Market Entry | stacked-bar, eval-grid, bubble-scatter | bar-horizontal, scenario-lines, fan-chart | facts-perspectives, stakeholder-map, screening-funnel |
| Profitability | waterfall (2–4×), bar-horizontal, driver-tree | stacked-bar-100, tornado, pareto | facts-perspectives, kpi-cascade |
| Growth | stacked-bar, bubble-scatter, line-chart | marimekko, indexed-lines, gantt | driver-tree, three-horizons, stakeholder-map |
| M&A | scenario-lines, eval-grid, waterfall | fan-chart, radar, data-table | facts-perspectives, rapid-map, screening-funnel |
| Pricing | waterfall (2×), tornado, bar-horizontal | stacked-bar-100, scenario-lines, heatmap | driver-tree, dumbbell-benchmark, economic-value-stack |
| Digital | bubble-scatter, kpi-cards, gantt | radar, heatmap, process-flow | org-model, swimlane, three-horizons |
| Org Restructuring | org-model, rapid-map, process-flow | rag-status, data-table, timeline | facts-perspectives, swimlane, stakeholder-map, capability-matrix, communication-cascade, decision-flow, network-map |
| Commercial Excellence | dumbbell-benchmark, kpi-cards, funnel | bar-horizontal, clustered-bars, line-chart | facts-perspectives, kpi-cascade, coverage-matrix, cohort-retention |
How to use this table: After classifying the archetype, scan the signature and supporting columns to select exhibits. The action titles determine what's needed — not every deck uses every exhibit listed.
/* Deck Design PDF — Base Canvas
*
* The engine injects slide dimensions at render time. Defaults resolve to
* 1280×720 when no explicit size is provided.
*/
*, *::before, *::after {
box-sizing: border-box;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
html {
margin: 0;
padding: 0;
width: var(--slide-width, 1280px);
}
body {
margin: 0;
padding: 0;
width: var(--slide-width, 1280px);
min-height: var(--slide-height, 720px);
overflow: visible;
}
body {
font-family: var(--font-body, 'Inter', system-ui, sans-serif);
color: var(--text, #1a1a1a);
background: var(--surface, #ffffff);
}
.slide {
width: var(--slide-width, 1280px);
height: var(--slide-height, 720px);
overflow: hidden;
position: relative;
background: var(--surface, #ffffff);
page-break-after: always;
break-after: page;
}
.slide:last-child {
page-break-after: auto;
break-after: auto;
}
@media print {
html, body {
width: var(--slide-width, 1280px);
}
body {
min-height: auto;
overflow: visible;
}
}
/**
* Deck Design PDF — Master Builder
*
* All assets are vendored locally — no CDN, no network dependency.
*
* Usage:
* const { createDeck } = require('./skills/deck-design-pdf/engine');
*
* createDeck({
* palette: 'consulting-mckinsey',
* slides: [`<div>...</div>`, `<div>...</div>`],
* output: 'output/deck.pdf',
* title: 'Strategy Presentation',
* });
*/
const fs = require('fs');
const path = require('path');
const { renderPDF } = require('./render');
// ── Paths ────────────────────────────────────────────────────────────
const SKILL_ROOT = path.join(__dirname, '..');
const VENDOR_DIR = path.join(SKILL_ROOT, 'vendor');
const PALETTES_DIR = path.join(SKILL_ROOT, 'palettes');
const FONTS_DIR = path.join(VENDOR_DIR, 'fonts');
// ── Static assets (read once at require time) ────────────────────────
const CANVAS_CSS = fs.readFileSync(path.join(__dirname, 'canvas.css'), 'utf8');
// JS — referenced via file:// in <script src>
const ECHARTS_FILE = path.join(VENDOR_DIR, 'echarts', 'echarts.min.js');
const TAILWIND_FILE = path.join(VENDOR_DIR, 'tailwindcss', 'tailwind.min.js');
// Font Awesome — CSS inlined with rewritten font paths
const FA_DIR = path.join(VENDOR_DIR, 'fontawesome');
// ── Font loading ─────────────────────────────────────────────────────
/**
* Load @fontsource CSS for a font family at specified weights.
* Rewrites relative url() paths to absolute file:// so Playwright can resolve them.
*/
function loadFontCSS(packageName, weights = [300, 400, 500, 600, 700, 800]) {
const fontDir = path.join(FONTS_DIR, packageName);
if (!fs.existsSync(fontDir)) return '';
let css = '';
for (const w of weights) {
const file = path.join(fontDir, `${w}.css`);
if (fs.existsSync(file)) {
let content = fs.readFileSync(file, 'utf8');
// Rewrite url(./files/...) → url(file:///absolute/path/files/...)
content = content.replace(/url\(\.\//g, `url(file://${fontDir}/`);
css += content + '\n';
}
}
return css;
}
/**
* Load Font Awesome CSS with rewritten webfont paths.
*/
function loadFontAwesomeCSS() {
const cssFile = path.join(FA_DIR, 'css', 'all.min.css');
if (!fs.existsSync(cssFile)) return '';
let css = fs.readFileSync(cssFile, 'utf8');
// Rewrite ../webfonts/ → file:///absolute/path/webfonts/
css = css.replace(/\.\.\/webfonts\//g, `file://${FA_DIR}/webfonts/`);
return css;
}
// ── Preload all font CSS at require time ─────────────────────────────
const FONTS_CSS = [
loadFontCSS('inter'),
loadFontCSS('plus-jakarta-sans'),
loadFontCSS('dm-sans'),
loadFontCSS('source-sans-3', [300, 400, 500, 600, 700, 800, 900]),
].join('\n');
const FA_CSS = loadFontAwesomeCSS();
// ── Palette loading ──────────────────────────────────────────────────
function loadPalette(name) {
const file = path.join(PALETTES_DIR, `${name}.css`);
if (!fs.existsSync(file)) {
const available = fs.readdirSync(PALETTES_DIR)
.filter(f => f.endsWith('.css'))
.map(f => f.replace('.css', ''));
throw new Error(
`Unknown palette "${name}". Available: ${available.join(', ')}`
);
}
return fs.readFileSync(file, 'utf8');
}
// ── HTML assembly ────────────────────────────────────────────────────
function buildHTML(options) {
const {
palette,
slides,
title = 'Presentation',
headExtra = '',
width = 1280,
height = 720,
tier = 'document',
} = options;
const paletteCss = loadPalette(palette);
const sizeCss = `:root { --slide-width: ${width}px; --slide-height: ${height}px; } @page { size: ${width}px ${height}px; margin: 0; }`;
const slideHTML = slides
.map((html, i) =>
`<section class="slide" id="slide-${i + 1}">\n${html}\n</section>`
)
.join('\n');
return `<!DOCTYPE html>
<html lang="en" data-palette="${palette}" data-tier="${tier}">
<head>
<meta charset="UTF-8">
<title>${title}</title>
<style>${FONTS_CSS}</style>
<style>${FA_CSS}</style>
<script src="file://${ECHARTS_FILE}"></script>
<script src="file://${TAILWIND_FILE}"></script>
<style>${sizeCss}</style>
<style>${CANVAS_CSS}</style>
<style>${paletteCss}</style>
${headExtra}
</head>
<body>
${slideHTML}
</body>
</html>`;
}
// ── Public API ───────────────────────────────────────────────────────
async function createDeck(options) {
const { output = 'deck.pdf', slides, width = 1280, height = 720 } = options;
const html = buildHTML(options);
await renderPDF(html, output, { width, height });
console.log(`Wrote: ${output} (${slides.length} slides, ${options.palette}, ${width}x${height})`);
return output;
}
module.exports = { createDeck, buildHTML, loadPalette };
/**
* Deck Design PDF — Playwright Renderer
*
* Takes an HTML string, writes to temp file, renders to PDF via Chromium.
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const { chromium } = require('playwright');
/**
* Render an HTML string to a PDF file.
*
* @param {string} html - Full HTML document string
* @param {string} outputPath - Where to write the PDF
* @param {object} [options]
* @param {number} [options.width=1280] - Slide width in px
* @param {number} [options.height=720] - Slide height in px
* @returns {Promise<string>} - Resolved output path
*/
async function renderPDF(html, outputPath, options = {}) {
const { width = 1280, height = 720 } = options;
// Write HTML to a temp file so Playwright can load it as file://
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deck-pdf-'));
const tmpFile = path.join(tmpDir, 'deck.html');
fs.writeFileSync(tmpFile, html, 'utf8');
// Ensure output directory exists
const outDir = path.dirname(path.resolve(outputPath));
fs.mkdirSync(outDir, { recursive: true });
const browser = await chromium.launch();
try {
const page = await browser.newPage();
await page.setViewportSize({ width, height });
await page.emulateMedia({ media: 'screen' });
await page.goto(`file://${tmpFile}`, { waitUntil: 'networkidle' });
// Wait for web fonts to finish loading
await page.evaluate(() => document.fonts.ready);
await page.pdf({
path: path.resolve(outputPath),
width: `${width}px`,
height: `${height}px`,
printBackground: true,
margin: { top: '0', right: '0', bottom: '0', left: '0' },
scale: 1,
});
} finally {
await browser.close();
}
// Cleanup temp files
try { fs.unlinkSync(tmpFile); } catch {}
try { fs.rmdirSync(tmpDir); } catch {}
return outputPath;
}
module.exports = { renderPDF };
Evidence Recipes — Data-Shape Compositions
Standard exhibit compositions for consulting slides. Each recipe answers: "When I need to prove [X], what chart combination should I use?"
Quick Lookup
| You need to prove… | Recipe | Pattern reference |
|---|---|---|
| Value changed through sequential steps | Waterfall Bridge | waterfall example |
| One option is best among alternatives | Evaluation Grid | eval-grid example |
| Opportunity concentrated in segments | Market Sizing Stack | stacked-bar example |
| Items prioritized on two dimensions | Priority Matrix | bubble-scatter / matrix-2x2 |
| Activities sequenced in time | Timeline / Phasing | gantt / timeline example |
| Performance across multiple metrics | KPI Scorecard | kpi-cards example |
| Facts vs. interpretation separated | Facts vs. Perspectives | facts-perspectives example |
| Multiple futures / scenarios | Scenario Fan | fan-chart / scenario-lines |
| Mechanism or process visible | Process Diagram | process-flow example |
| Client vs. peers on a metric | Benchmark Comparison | bar-horizontal / dumbbell |
Layering Rules
One evidence layer = one distinct visual element (chart, table, metric cards, callout stack).
| Density | Layers | Composition |
|---|---|---|
| L1 Narrative | 1 | One exhibit, scaled up to fill the body zone. Title carries 80% of argument. |
| L2 Structured | 1–2 | Primary + supporting (sidebar, metric cards, or callout). |
| L3 Dense | 2–4 | Primary + supporting + annotation stack. Body convinces a skeptic alone. |
Hard ceiling: 4 layers per slide. More = you haven't made the editorial choice.
Recipe Details
1. Waterfall Bridge
Proves: "The gap/change is driven by these specific factors, in this order of magnitude."
- Primary: waterfall chart (5–8 bars: start anchor, 3–6 deltas, end anchor)
- Supporting: sidebar with 2–3 interpretation bullets explaining WHY largest deltas exist
- Annotation: delta labels on each bar, variance share row beneath
2. Evaluation Grid
Proves: "Among all options, [this one] is strongest across [these criteria]."
- Primary: matrix with criteria rows (4–8) × option columns (3–5), RAG-coded cells
- Supporting: summary row or recommendation highlight (bold border on winner)
- Annotation: "Key finding" bullets summarizing the pattern
3. Market Sizing Stack
Proves: "The addressable opportunity is $X, concentrated in [segments]."
- Primary: stacked bar or nested waterfall (total → segments → addressable)
- Supporting: segment callout sidebar with growth rates and fit scores
- Annotation: CAGR labels, segment boundary labels
4. Priority Matrix
Proves: "Given [dim 1] and [dim 2], these items are in the priority zone."
- Primary: scatter/bubble plot with named items (size = 3rd dimension)
- Supporting: quadrant labels ("Quick wins", "Strategic bets", etc.)
- Annotation: highlight zone or callout for recommended cluster
5. Timeline / Phasing
Proves: "Implementation follows N phases, with these milestones as gates."
- Primary: Gantt bars or phase blocks on time axis
- Supporting: milestone markers at decision points
- Annotation: dependency arrows, value realization markers
6. KPI Scorecard
Proves: "Across N dimensions, performance is [strong/mixed/weak]."
- Primary: 4–6 metric cards with KPI number + trend + context
- Supporting: RAG status indicators
- Annotation: assessment summary connecting scorecard to next action
7. Facts vs. Perspectives
Proves: "Here's what we know (facts) and what we recommend (perspectives)."
- Left column (~57%): "KEY FACTS & DATA" — 3–6 quantified bullets + optional mini-exhibit
- Right column (~43%): "PERSPECTIVES" — action-oriented recommendation bullets
- Annotation: implication callout band at bottom
8. Scenario Fan
Proves: "Under N scenarios, outcome ranges from X to Y, base case at Z."
- Primary: line chart with 3 paths (base, upside, downside) diverging from common point
- Supporting: scenario labels with key assumptions
- Annotation: decision-relevant threshold line (breakeven, hurdle rate)
9. Process Diagram
Proves: "This is how the system works, and [this step] is the leverage point."
- Primary: process steps (boxes/nodes) connected by arrows, one step highlighted
- Supporting: feedback loops or reinforcing connections
- Annotation: callout at leverage point explaining WHY it matters
10. Benchmark Comparison
Proves: "[Client] is at X on [metric], [above/below] peer median by Y."
- Primary: horizontal bars with peers in gray, client highlighted in accent
- Supporting: peer median line (dashed)
- Annotation: gap annotation converted to dollar/percentage impact
Recipe × Engagement Type
| Engagement | Primary Recipes | Count |
|---|---|---|
| Market Entry | Market Sizing (2×), Eval Grid (2×), Priority Matrix, Scenario Fan | 6–8 |
| Profitability | Waterfall (3–4×), Benchmark (2×), KPI Scorecard | 6–7 |
| Growth | Market Sizing (2×), Priority Matrix (2×), Waterfall, Timeline | 6–7 |
| M&A | Scenario Fan (2×), Eval Grid (2×), Waterfall (1–2×), Timeline | 6–8 |
| Pricing | Waterfall (2×), Priority Matrix, Scenario Fan, Benchmark | 5–6 |
| Digital | Priority Matrix (2×), KPI Scorecard (2×), Timeline, Process | 6–7 |
| Org Restructure | Process (2×), Facts vs Perspectives (2×), KPI, Timeline | 6–7 |
| Commercial Excellence | Benchmark (2×), KPI (2×), Priority Matrix, Facts vs Perspectives | 6–7 |
// ════════════════════════════════════════════════════════════════════════
// Horizontal Bar Chart — sorted descending, hero item highlighted
// ════════════════════════════════════════════════════════════════════════
// Flexible layout: works in portrait, landscape, or narrow columns.
// Adapts to any aspect ratio — can be tall and narrow or short and wide.
// Recommended minimum width: 300px.
//
// Responsive template for agentic AI. Three things to change:
// 1. Brand variables → swap font + colors from the brand config
// 2. Data → swap categories + values with real data
// 3. Sizing limits → tune the knobs if defaults don't fit
// Everything else adapts automatically to container size and item count.
//
// ECharts gotchas captured here so the agent doesn't rediscover them:
// • label.color does NOT accept a function — set it per data item
// • Y-axis multiline labels can't right-align — use a formatter that
// splits on spaces (not overflow:'break', which leaves a trailing space)
// • containLabel:true makes the grid shrink to fit axis labels
module.exports = {
id: 'bar-horizontal',
title: 'Horizontal Sorted Bars',
tier: 1,
proves: 'ranking / comparison ("Which is biggest?")',
data: 'Operating margin by business unit, sorted descending',
sectionLabel: 'Performance Review',
actionTitle: 'Industrial Solutions leads operating margin across all business units',
source: 'Source: Company financials FY2025',
exhibitId: 'Exhibit 1.1',
renderExhibit({ tokens }) {
const chartId = 'bar-horizontal-chart';
// ── 1. Brand variables ──────────────────────────────────────────────
// Replace these from the brand config. Default is system sans-serif
// with a navy/slate consulting palette.
const fontFamily = 'sans-serif';
const textColor = '#101A27'; // axis labels, value labels
const accentBar = '#123A63'; // hero item (darkest bar)
const mutedBar = '#C7D5E5'; // all other bars
// ── 2. Data ─────────────────────────────────────────────────────────
// Replace with real data. Keep categories and values in matching order.
// The last item is treated as the "hero" (accent color + white label).
const categories = [
'Corporate Services', 'Retail', 'Logistics',
'Digital', 'Healthcare', 'Energy', 'Industrial Solutions',
];
const values = [8.2, 11.5, 14.3, 17.8, 19.4, 22.1, 26.7];
// ── 3. Sizing limits ────────────────────────────────────────────────
// Tune these if the defaults don't suit the deck. The responsive
// formulas below interpolate between min/max based on container size.
const fontSizeRange = [14, 20]; // [min, max] px for all labels
const barWidthRange = [12, 48]; // [min, max] px bar thickness
const barFillRatio = 0.55; // portion of vertical slot per bar (rest is gap)
const labelWrapBelow = 400; // container width below which long labels wrap
// ── Responsive sizing (computed — don't edit) ───────────────────────
// fontSize: linear interpolation across 300–1120px width, clamped to range
// barWidth: height ÷ item count × fill ratio, clamped to range
// wrapChars: below labelWrapBelow, names longer than 10 chars split at spaces
const [fontMin, fontMax] = fontSizeRange;
const fontSize = Math.max(fontMin, Math.min(fontMax,
Math.round(fontMin + (tokens.width - 300) / (1120 - 300) * (fontMax - fontMin))));
const [barMin, barMax] = barWidthRange;
const barWidth = Math.max(barMin, Math.min(barMax,
Math.round((tokens.height - 16) / categories.length * barFillRatio)));
const wrapChars = tokens.width < labelWrapBelow ? 10 : Infinity;
// ── Per-item styling ────────────────────────────────────────────────
// Hero bar = accent color + white text. Others = muted color + dark text.
// Must be per-item objects because ECharts label.color doesn't take a function.
const dataItems = values.map((v, i) => {
const isHero = i === values.length - 1;
return {
value: v,
itemStyle: { color: isHero ? accentBar : mutedBar, borderRadius: [0, 6, 6, 0] },
label: { color: isHero ? '#FFFFFF' : textColor },
};
});
// ── Template ────────────────────────────────────────────────────────
return `<div class="h-full w-full">
<div id="${chartId}" style="width:100%;height:100%;"></div>
</div>
<script>
(() => {
const mount = document.getElementById('${chartId}');
if (!mount) return;
const chart = echarts.init(mount, null, { renderer: 'svg' });
chart.setOption({
animation: false,
tooltip: { show: false },
grid: { left: 2, right: 2, top: 2, bottom: 2 },
xAxis: {
type: 'value',
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false },
splitLine: { show: false },
},
yAxis: {
type: 'category',
data: ${JSON.stringify(categories)},
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
fontSize: ${fontSize},
fontFamily: '${fontFamily}',
color: '${textColor}',
formatter: (name) => name.length > ${wrapChars} ? name.split(' ').join('\\n') : name,
},
},
series: [{
type: 'bar',
data: ${JSON.stringify(dataItems)},
barWidth: ${barWidth},
label: {
show: true,
position: 'insideRight',
formatter: '{c}%',
fontSize: ${fontSize},
fontFamily: '${fontFamily}',
fontWeight: 'bold',
},
}],
});
window.addEventListener('resize', () => chart.resize());
})();
</script>`;
},
};
const fs = require('fs');
const path = require('path');
const SS3_DIR = path.join(__dirname, '../vendor/fonts/source-sans-3');
const SS3_CSS = [400, 600, 700].map(w => {
const file = path.join(SS3_DIR, `${w}.css`);
return fs.readFileSync(file, 'utf8').replace(/url\(\.\//g, `url(file://${SS3_DIR}/`);
}).join('\n');
module.exports = {
id: 'before-after',
title: 'Before / After',
tier: 3,
proves: 'impact of intervention across mixed units',
data: 'Operational metrics before and after process automation',
sectionLabel: 'Impact Assessment',
actionTitle: 'Process automation reduced cycle time by 42% and error rate by 68%',
source: 'Source: Operations dashboard, 90-day post-implementation review',
exhibitId: 'Exhibit 19.1',
renderExhibit({ tokens }) {
const minDim = Math.min(tokens.width, tokens.height);
const lerp = (range) => {
const [lo, hi] = range;
return Math.max(lo, Math.min(hi, Math.round(lo + (minDim - 300) / (720 - 300) * (hi - lo))));
};
const fontFamily = "'Source Sans 3', sans-serif";
const red = '#CC0000';
const charcoal = '#2B2B2B';
const textMuted = '#666666';
const textFine = '#888888';
const rule = '#E0E0E0';
const bodyFont = lerp([9, 13]);
const microFont = lerp([7, 10]);
const deltaFont = lerp([16, 24]);
const pad = lerp([8, 14]);
const gap = lerp([6, 12]);
const cols = minDim < 450 ? 2 : 3;
const metrics = [
{ name: 'Error rate', before: '12.4%', after: '4.0%', delta: '−68%' },
{ name: 'Manual steps', before: '23', after: '8', delta: '−65%' },
{ name: 'Cycle time', before: '47 days', after: '27 days', delta: '−42%' },
{ name: 'Throughput', before: '340/day', after: '580/day', delta: '+71%' },
{ name: 'Cost per txn', before: '$8.50', after: '$3.20', delta: '−62%' },
];
const cards = metrics.map(metric => `
<div style="
display:flex;
flex-direction:column;
gap:${Math.max(gap - 2, 4)}px;
padding:${pad}px;
border:1px solid ${rule};
background:transparent;
font-family:${fontFamily};
overflow:hidden;
">
<div style="
font-size:${bodyFont}px;
font-weight:700;
color:${charcoal};
font-family:${fontFamily};
line-height:1.2;
">${metric.name}</div>
<div style="
font-size:${deltaFont}px;
font-weight:700;
color:${red};
font-family:${fontFamily};
line-height:1;
">${metric.delta}</div>
<div style="
display:grid;
grid-template-columns:1fr 1fr;
gap:${Math.max(gap - 2, 4)}px;
">
<div>
<div style="
font-size:${microFont}px;
font-weight:400;
color:${textFine};
font-family:${fontFamily};
text-transform:uppercase;
letter-spacing:0.05em;
line-height:1.2;
">BEFORE</div>
<div style="
font-size:${bodyFont + 2}px;
font-weight:400;
color:${textMuted};
font-family:${fontFamily};
line-height:1.3;
margin-top:2px;
">${metric.before}</div>
</div>
<div>
<div style="
font-size:${microFont}px;
font-weight:400;
color:${textFine};
font-family:${fontFamily};
text-transform:uppercase;
letter-spacing:0.05em;
line-height:1.2;
">AFTER</div>
<div style="
font-size:${bodyFont + 2}px;
font-weight:700;
color:${charcoal};
font-family:${fontFamily};
line-height:1.3;
margin-top:2px;
">${metric.after}</div>
</div>
</div>
</div>
`).join('');
return `
<style>${SS3_CSS}</style>
<div class="h-full w-full" style="display:grid;grid-template-rows:minmax(0,1fr) auto;gap:${gap}px;padding:2px;overflow:hidden;">
<div style="
display:grid;
grid-template-columns:repeat(${cols}, minmax(0,1fr));
grid-auto-rows:minmax(0,1fr);
gap:${gap}px;
overflow:hidden;
">
${cards}
</div>
<div style="
border-left:3px solid ${red};
padding:${Math.round(pad * 0.6)}px ${pad}px;
font-size:${bodyFont}px;
font-family:${fontFamily};
color:${charcoal};
line-height:1.4;
">
Automation improved all five tracked metrics simultaneously, increasing throughput while reducing cost to serve.
</div>
</div>
`;
},
};
// ════════════════════════════════════════════════════════════════════════
// Bubble Scatter — quadrant chart with three variables (x, y, size)
// ════════════════════════════════════════════════════════════════════════
// Flexible layout: works in portrait, landscape, or square containers.
// Bubbles, labels, and annotations all scale with container dimensions.
// Recommended minimum: 300px on the shorter side.
//
// Responsive template for agentic AI. Three things to change:
// 1. Brand variables → swap font + colors from the brand config
// 2. Data → swap points array with real data
// 3. Sizing limits → tune the knobs if defaults don't fit
// Everything else adapts automatically to container size.
//
// ECharts gotchas captured here so the agent doesn't rediscover them:
// • label.color does NOT accept a function — set it per data point
// • labelLayout.hideOverlap hides labels that collide — always enable it
// • Use markLine for quadrant dividers (data-coordinate, auto-responsive)
// • Use markArea for quadrant labels (data-coordinate, auto-responsive)
// • Do NOT use graphic elements for anything tied to data positions —
// they position relative to container, not the grid/data area
// • symbolSize is in px, not data units — scale it from container size
module.exports = {
id: 'bubble-scatter',
title: 'Bubble Scatter',
tier: 2,
proves: 'positioning with three variables (x, y, size)',
data: 'Market segments by attractiveness versus competitive position; bubble size = TAM ($B)',
sectionLabel: 'Market Positioning',
actionTitle: 'Cloud infrastructure and data analytics represent the largest addressable opportunities',
source: 'Source: Market research, team analysis. Bubble size = TAM ($B)',
exhibitId: 'Exhibit 8.1',
renderExhibit({ tokens }) {
const chartId = 'bubble-scatter-core';
// ── 1. Brand variables ──────────────────────────────────────────────
const fontFamily = 'sans-serif';
const textColor = '#101A27';
const textMuted = '#4E6176';
const accent = '#123A63';
const axisLine = '#C7D5E5';
// Bubble palette — darkest = highest priority, lightest = lowest.
// Lightest colors should still be visible against the background.
const bubblePalette = ['#123A63', '#2E7D9B', '#6B9AB8', '#8DB8D0', '#A7C9DE', '#B8D4E8', '#C7DDF0'];
// ── 2. Data ─────────────────────────────────────────────────────────
// Each point: name, x, y, size value, palette index, label position.
// labelPos: 'inside' for large dark bubbles (white text),
// 'top'/'right'/'bottom' for smaller or light bubbles (dark text).
const points = [
{ name: 'Cloud Infra', x: 75, y: 82, tam: 48, palette: 0, labelPos: 'right' },
{ name: 'Data Analytics', x: 60, y: 78, tam: 35, palette: 1, labelPos: 'left' },
{ name: 'Cybersecurity', x: 85, y: 55, tam: 22, palette: 2, labelPos: 'top' },
{ name: 'AI / ML Platform', x: 40, y: 70, tam: 28, palette: 3, labelPos: 'right' },
{ name: 'Dev Tools', x: 70, y: 40, tam: 15, palette: 4, labelPos: 'top' },
{ name: 'IoT', x: 30, y: 45, tam: 12, palette: 5, labelPos: 'right' },
{ name: 'Edge Compute', x: 55, y: 30, tam: 8, palette: 6, labelPos: 'bottom' },
];
// Axis config
const xAxisName = 'Competitive position';
const yAxisName = 'Market attractiveness';
const axisMin = 15;
const axisMax = 100;
const quadrantMid = 50; // data value where quadrants divide
// Quadrant names — counterclockwise from top-right
const quadrantNames = {
topRight: 'Priority bets',
topLeft: 'Build position',
bottomLeft: 'Deprioritize',
bottomRight: 'Harvest',
};
// ── 3. Sizing limits ────────────────────────────────────────────────
const fontSizeRange = [11, 16]; // [min, max] px for bubble labels
const annotationSizeRange = [12, 16]; // [min, max] px for quadrant/axis labels
const bubbleSizeBase = 16; // minimum bubble diameter before TAM scaling
const bubbleSizeFactor = 0.85; // px per TAM unit
const bubbleScaleRange = [0.55, 1.15]; // [min, max] bubble scale factor
// ── Responsive sizing (computed — don't edit) ───────────────────────
const minDim = Math.min(tokens.width, tokens.height);
const [fontMin, fontMax] = fontSizeRange;
const fontSize = Math.max(fontMin, Math.min(fontMax,
Math.round(fontMin + (minDim - 300) / (720 - 300) * (fontMax - fontMin))));
const [annoMin, annoMax] = annotationSizeRange;
const annotationSize = Math.max(annoMin, Math.min(annoMax,
Math.round(annoMin + (minDim - 300) / (720 - 300) * (annoMax - annoMin))));
const [scaleMin, scaleMax] = bubbleScaleRange;
const bubbleScale = Math.max(scaleMin, Math.min(scaleMax, minDim / 540));
// ── Contrast helper (auto white/dark text based on bubble color) ────
function contrastColor(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.5 ? textColor : '#FFFFFF';
}
// ── Per-point ECharts data ──────────────────────────────────────────
const seriesData = points.map((p) => ({
value: [p.x, p.y],
symbolSize: Math.round((bubbleSizeBase + p.tam * bubbleSizeFactor) * bubbleScale),
label: {
show: true,
formatter: p.name + '\n$' + p.tam + 'B',
fontSize: fontSize,
fontFamily: fontFamily,
fontWeight: 'bold',
lineHeight: fontSize + 2,
color: p.labelPos === 'inside' ? contrastColor(bubblePalette[p.palette]) : textColor,
position: p.labelPos,
},
itemStyle: {
color: bubblePalette[p.palette],
opacity: 0.92,
},
}));
// ── markArea: quadrant labels (data-coordinate, fully responsive) ────
// No divider lines by default — the framework is abstract, not a hard boundary.
// Agent can add markLine with { xAxis: threshold } if the data has a real cutoff.
const quadrantAreas = {
silent: true,
label: {
show: true,
fontSize: annotationSize,
fontFamily: fontFamily,
fontWeight: 'bold',
color: textMuted,
},
itemStyle: { color: 'transparent' },
data: [
[{ name: quadrantNames.topRight, xAxis: quadrantMid, yAxis: quadrantMid,
label: { position: 'insideTopRight', color: accent } },
{ xAxis: axisMax, yAxis: axisMax }],
[{ name: quadrantNames.topLeft, xAxis: axisMin, yAxis: quadrantMid,
label: { position: 'insideTopLeft' } },
{ xAxis: quadrantMid, yAxis: axisMax }],
[{ name: quadrantNames.bottomLeft, xAxis: axisMin, yAxis: axisMin,
label: { position: 'insideBottomLeft' } },
{ xAxis: quadrantMid, yAxis: quadrantMid }],
[{ name: quadrantNames.bottomRight, xAxis: quadrantMid, yAxis: axisMin,
label: { position: 'insideBottomRight' } },
{ xAxis: axisMax, yAxis: quadrantMid }],
],
};
// ── Template ────────────────────────────────────────────────────────
return `<div class="h-full w-full">
<div id="${chartId}" style="width:100%;height:100%;"></div>
</div>
<script>
(() => {
const mount = document.getElementById('${chartId}');
if (!mount) return;
const chart = echarts.init(mount, null, { renderer: 'svg' });
chart.setOption({
animation: false,
tooltip: { show: false },
grid: { left: 2, right: 2, top: 2, bottom: 2 },
xAxis: {
type: 'value',
min: ${axisMin}, max: ${axisMax},
name: '${xAxisName}',
nameLocation: 'center',
nameGap: 2,
nameTextStyle: { fontSize: ${annotationSize}, fontFamily: '${fontFamily}', fontWeight: 'bold', color: '${textMuted}' },
axisLine: { lineStyle: { color: '${axisLine}' } },
axisTick: { show: false },
axisLabel: { show: false },
splitLine: { show: false },
},
yAxis: {
type: 'value',
min: ${axisMin}, max: ${axisMax},
name: '${yAxisName}',
nameLocation: 'center',
nameGap: 2,
nameRotate: 90,
nameTextStyle: { fontSize: ${annotationSize}, fontFamily: '${fontFamily}', fontWeight: 'bold', color: '${textMuted}' },
axisLine: { lineStyle: { color: '${axisLine}' } },
axisTick: { show: false },
axisLabel: { show: false },
splitLine: { show: false },
},
series: [{
type: 'scatter',
labelLayout: { hideOverlap: true },
data: ${JSON.stringify(seriesData)},
markArea: ${JSON.stringify(quadrantAreas)},
}],
});
window.addEventListener('resize', () => chart.resize());
})();
</script>`;
},
};
// ════════════════════════════════════════════════════════════════════════
// Burndown — progress toward target over time (actual vs plan)
// ════════════════════════════════════════════════════════════════════════
// Flexible layout: works in portrait, landscape, or square containers.
// Recommended minimum width: 300px.
//
// Responsive template for agentic AI. Three things to change:
// 1. Brand variables → swap font + colors from the brand config
// 2. Data → swap months, plan, actual, target with real data
// 3. Sizing limits → tune the knobs if defaults don't fit
// Everything else adapts automatically to container size.
//
// ECharts layout gotchas (so the agent doesn't rediscover them):
//
// Grid padding uses percentages, not pixels, so it scales with container.
// • containLabel:true only covers axis labels — NOT markLine or endLabel
// • Top Y-axis label clips without top padding (known ECharts bug #15562)
// • endLabel overflows the grid (ignoreClip:true in source, #17828)
// • Fix: grid.top/right as '%' gives both room to breathe, responsively
//
// Line labels use endLabel (built-in, auto-positions at last data point):
// • align:'right' makes text grow leftward so it stays inside the grid
// • offset:[-4,0] nudges the anchor inward for safety
// • grid.right:'10%' reserves overflow space that scales with container
// • No legend needed — each line is labeled directly
//
// Threshold lines use markLine (data-coordinate, auto-responsive):
// • position:'insideMiddleBottom' keeps label below the line, inside grid
// • Do NOT use 'insideMiddleTop' near the chart ceiling — it clips
//
// Axis labels:
// • alignMinLabel:'left' + alignMaxLabel:'right' (ECharts 5.5+)
// pins first/last x-axis labels so they don't overflow
// • yAxis formatter is a function — must be inline, not JSON.stringify'd
module.exports = {
id: 'burndown',
title: 'Burndown',
tier: 3,
proves: 'progress toward target over time',
data: 'Cost reduction initiative tracking versus plan',
sectionLabel: 'Cost Reduction Tracker',
actionTitle: 'Cost take-out is tracking 8% ahead of plan with $127M captured through March',
source: 'Source: Transformation Office, cumulative savings tracker',
exhibitId: 'Exhibit 18.1',
renderExhibit({ tokens }) {
const chartId = 'burndown-core';
// ── 1. Brand variables ──────────────────────────────────────────────
const fontFamily = 'sans-serif';
const textColor = '#101A27';
const textMuted = '#4E6176';
const accent = '#123A63'; // actual line color
const planColor = '#8BA5BD'; // plan line color (muted)
const successColor = '#2E9E5A'; // target threshold color
const axisLine = '#C7D5E5';
const gridLine = '#E4EDF7';
// ── 2. Data ─────────────────────────────────────────────────────────
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const planData = [12, 28, 48, 72, 98, 128, 156, 182, 210, 238, 264, 290];
const actualData = [15, 38, 127]; // partial year — only months with actuals
const targetValue = 290; // horizontal threshold line value
const targetLabel = 'Target: $' + targetValue + 'M';
const valueUnit = 'M';
// ── 3. Sizing limits ────────────────────────────────────────────────
const fontSizeRange = [11, 16]; // [min, max] px for all labels
const lineWidthRange = [1.5, 3]; // [min, max] px line thickness
const pointSizeRange = [4, 8]; // [min, max] px data point diameter
// ── Responsive sizing (computed — don't edit) ───────────────────────
const minDim = Math.min(tokens.width, tokens.height);
const [fontMin, fontMax] = fontSizeRange;
const fontSize = Math.max(fontMin, Math.min(fontMax,
Math.round(fontMin + (minDim - 300) / (720 - 300) * (fontMax - fontMin))));
const [lwMin, lwMax] = lineWidthRange;
const lineWidth = Math.max(lwMin, Math.min(lwMax,
+(lwMin + (minDim - 300) / (720 - 300) * (lwMax - lwMin)).toFixed(1)));
const [ptMin, ptMax] = pointSizeRange;
const pointSize = Math.max(ptMin, Math.min(ptMax,
Math.round(ptMin + (minDim - 300) / (720 - 300) * (ptMax - ptMin))));
// ── Template ────────────────────────────────────────────────────────
return `<div class="h-full w-full">
<div id="${chartId}" style="width:100%;height:100%;"></div>
</div>
<script>
(() => {
const mount = document.getElementById('${chartId}');
if (!mount) return;
const chart = echarts.init(mount, null, { renderer: 'svg' });
chart.setOption({
animation: false,
tooltip: { show: false },
legend: { show: false },
grid: { left: 2, right: 2, top: 2, bottom: 2 },
xAxis: {
type: 'category',
data: ${JSON.stringify(months)},
boundaryGap: false,
axisLine: { lineStyle: { color: '${axisLine}' } },
axisTick: { show: false },
// alignMinLabel/alignMaxLabel pins first/last labels to their ticks
axisLabel: { fontSize: ${fontSize}, fontFamily: '${fontFamily}', color: '${textMuted}',
alignMinLabel: 'left', alignMaxLabel: 'right' },
splitLine: { show: false },
},
yAxis: {
type: 'value',
axisLine: { show: false },
axisTick: { show: false },
// formatter must be inline function (not JSON.stringify'd)
axisLabel: { fontSize: ${fontSize}, fontFamily: '${fontFamily}', color: '${textMuted}',
formatter: (v) => '$' + v + '${valueUnit}' },
splitLine: { lineStyle: { color: '${gridLine}', type: 'dashed' } },
},
series: [
{
name: 'Plan',
type: 'line',
data: ${JSON.stringify(planData)},
symbol: 'none',
lineStyle: { width: ${Math.max(lineWidth - 0.5, 1.5)}, color: '${planColor}', type: [6, 4] },
// endLabel labels the line directly — no legend needed.
// align:'right' grows text leftward into the grid.
// offset:[-4,0] nudges anchor inside; grid.right:'10%' catches overflow.
endLabel: {
show: true, formatter: 'Plan', align: 'right', offset: [-4, 0],
fontSize: ${fontSize}, fontFamily: '${fontFamily}', fontWeight: 'bold', color: '${planColor}',
valueAnimation: false,
},
// markLine = horizontal target threshold (data-coordinate, auto-responsive)
// insideMiddleBottom keeps label below the line, away from grid ceiling
markLine: {
silent: true,
symbol: 'none',
lineStyle: { color: '${successColor}', type: 'dashed', width: 1.5 },
label: {
show: true,
position: 'insideMiddleBottom',
fontSize: ${fontSize}, fontFamily: '${fontFamily}', fontWeight: 'bold', color: '${successColor}',
formatter: '${targetLabel}',
},
data: [{ yAxis: ${targetValue} }],
},
},
{
name: 'Actual',
type: 'line',
data: ${JSON.stringify(actualData)},
symbol: 'circle',
symbolSize: ${pointSize},
lineStyle: { width: ${lineWidth}, color: '${accent}' },
itemStyle: { color: '${accent}' },
// endLabel at the last actual data point (mid-chart, no overflow risk)
endLabel: {
show: true, align: 'right', distance: 8,
fontSize: ${fontSize}, fontFamily: '${fontFamily}', fontWeight: 'bold', color: '${accent}',
formatter: 'Actual\\n$${actualData[actualData.length - 1]}${valueUnit}',
valueAnimation: false,
},
},
],
});
window.addEventListener('resize', () => chart.resize());
})();
</script>`;
},
};
{
"name": "deck-design-pdf",
"private": true,
"dependencies": {
"playwright": "^1.52.0"
}
}
Related skills
FAQ
PDF or editable PPTX?
This skill outputs a pixel-perfect PDF; for editable PowerPoint use deck-design-ppt instead.
What is a ghost deck?
The narrative skeleton (governing thought, action titles, exhibit types) built before any HTML.