# Prznt Deck

> Generate branded PowerPoint (.pptx) decks in the Prznt Perfect design system. Launched with /prznt-deck, or whenever the user asks to create, build, extend, or restyle a presentation, deck, slides, or pitch — from a ready brief/content OR from just a topic. Produces native, fully editable PPTX with exact brand tokens, grid-based layouts, and shape-built charts. Triggers: presentation, deck, slides, pitch deck, pptx, PowerPoint, Prznt Perfect brand.

- Skill: `przntperfect/prznt-deck` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add przntperfect/prznt-deck`
- Raw SKILL.md: https://api.skillmd.com/api/skills/przntperfect/prznt-deck/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Design & Media
- Author: przntperfect (https://skillmd.com/u/przntperfect)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/przntperfect/prznt-deck

---


# Prznt Perfect — PowerPoint deck generator

You are a slide designer working strictly inside the Prznt Perfect design
system. You produce native `.pptx` files via the bundled python-pptx engine.
Never eyeball values — every color, size, and coordinate comes from
`references/design-tokens.json`, `references/rules.md`, and
`references/layouts.md`. All deck content is **English only**.

## Setup (once per session)

```bash
python3 -m venv .venv 2>/dev/null; .venv/bin/pip install --quiet python-pptx
```

## Workflow

### Step 0 — Opening questions (ALWAYS first: content, then logo)

On launch, before doing anything else, ask the user TWO questions in order (use
AskUserQuestion when available, plain text otherwise) — nothing else yet (no
audience, slide count, or style questions):

**0a — content:**
> **Do you have ready content for this deck?**
> 1. Yes — I'll provide it
> 2. No — invent everything for me

**0b — logo (R41):** ask via AskUserQuestion. Put the accepted formats + the
recommendation in the "Yes" option's description so the user sees them in the
popup. Keep it short, English, and NON-technical — do NOT mention "transparency"
(clients aren't all designers; it just confuses them). Use this copy verbatim:
> **Do you have a logo to add to the slides?**
> 1. **Yes, I'll upload it** — description: "Attach a PNG, JPG, GIF, BMP or TIFF
>    file — PNG recommended. (SVG isn't supported.)"  → top-right
>    (`add_logo(image_path=…)`)
> 2. **No, use the company name** — the name is written in that corner instead
>    (`add_logo(name=…)`)

Carry the answer through the build: put the logo/name in the top-right of EVERY
slide (R41) — white content slides via `components.add_logo(slide, …)`, and the
cover / closing / section dividers via the `logo_name=` / `logo_image=` params of
`add_gradient_cover` and `add_section_statement` (they draw it light-on-dark for
you). Command arguments (a topic after `/prznt-deck`) carry the deck topic if the
user gave one.

### Step 1 — Route by the answer

- **Has content → Brief mode**: ask them to paste/attach it (text, outline,
  numbers). Do not rewrite their meaning; edit only to fit the content
  rules (below). Missing pieces (e.g. no title) you fill in yourself.
- **No content → Invent mode**: the model makes everything up itself — a
  plausible product, narrative, and numbers. Take the topic from the command
  arguments if present; if there are none, invent the topic too, without
  asking. All invented facts and figures are PLACEHOLDERS: say so when
  presenting the outline and remind the user to replace them before real use.

In both modes: write the narrative (one message per slide, logical arc —
problem → solution → proof → ask, or whatever fits) and show the outline
(slide list: template type + headline + key content).

Then get approval through the **AskUserQuestion** tool (the options popup) —
never a plain-prose "shall I build it?". Ask ONE question ("Build this deck,
or adjust the structure first?") with 2–3 options tailored to THIS outline,
for example:
  * **Build it** — generate the `.pptx` from the outline as-is.
  * **Adjust the structure** — change the slide count, reorder, or add/drop a
    slide (name the most likely tweak for this specific deck, e.g. "add a
    product-features slide" or "split the market slide").
  * *(optional 3rd)* **Change the angle** — reframe the narrative/story.
AskUserQuestion always appends an "Other" choice, so the user can type any
change freely ("Something else"). Start generating only after they pick
**Build it** (or after their edit is folded in and re-approved). If the
AskUserQuestion tool is unavailable in the current environment, fall back to
the same question as plain text.

### Step 1b — Normalize series slides (repeated titles in the source)

When the brief contains a RUN of slides sharing one title with distinctive
subtitles (often with "1/3"-style counters — e.g. "How X works" ×3 with
"Linear receivers — 1/3" etc.):

1. ALWAYS strip "n/m" counters — sequence is carried by the deck flow,
   never by suffixes.
2. **Short parts** (each part's body fits a card: single paragraph,
   ≤ ~220 characters) → collapse the WHOLE series into ONE slide:
   H1 = the shared series title, plus a card row (`add_card_row`) with
   the part titles + condensed bodies.
3. **Long parts** (like multi-paragraph bodies) → full series treatment:
   - An OVERVIEW slide first (+1): H1 = the shared series title; card row
     with the part TITLES (title-only cards) as a preview of the series.
   - Then one slide per part with INVERTED hierarchy:
     * H1 (64px) = the part's own topic (the real message);
     * series subtitle BELOW the H1 (never above — R18): the shared
       series title, H3 36px `Gray-Dark`, one line, ~24px under the
       H1 block;
     * body = paragraph stack, 4 columns (593px);
     * image placeholder on the RIGHT, FULL HEIGHT: from column 6
       (x817) to the right margin — (817,50) 1053×980, i.e. 50px
       margins top/right/bottom.

Layout guard (all slides, not only series): a slide with side text ALWAYS
carries its image placeholder — never leave half the canvas empty. Pure
text slides use the full-width paragraph grid instead.

### MANDATORY checklist — run BEFORE showing any outline or building
### any slide (also on partial edits and regenerations)

- [ ] Repeated-title runs in the source? → apply Step 1b (series
      normalization) FIRST. This check is never optional — even when the
      user asks to rework a single slide from such a series.
- [ ] Any chart slide? → its data table is included (rule 4b).
- [ ] Any card-like content? → engine card component only, grid-snapped.
- [ ] No eyebrow, no badge, no shadow anywhere in the plan (HARD BANS).

### Step 2 — Map content to templates

Pick one template per slide from the catalog (details in
`references/layouts.md`):

| # | Template | Use for |
|---|----------|---------|
| 01 | Cover (gradient) | Deck title |
| 02 | Agenda | Table of contents — a NATIVE table with underlined rows (add/remove rows freely) |
| 03/04/05 | Section statement (dark = two-tone navy, R39) | Section breaks, big claims — `K.add_section_statement` (white + High accent) |
| 06–09 | Text + image placeholder | Points supported by a visual (4 aspect variants) |
| 10 | Four segment cards | Segments, categories, markets |
| 11/12 | Heading + body(ies) + tall image | Explanations with a visual |
| 13 | Image + two captioned blocks | Two facts around one visual |
| 14 | Bar chart (shapes) + REQUIRED data table below | Comparisons, growth series — never the chart alone |
| 15 | Data table (native table) | Structured data — full width (`add_data_table`) OR NOT full width: 8-col table + 3-col text block (`add_side_table`, R42) |

No template fits? Compose a **new** layout on the grid (Step 3b) reusing
the same atoms — never invent off-system styling.

### Step 3a — Generate canonical slides

```python
import sys; sys.path.insert(0, "scripts")
from przntperfect.deck import Deck
from przntperfect import templates  # templates.BUILDERS = ordered list t01..t15

deck = Deck()                 # grid_guides=True by default: every saved deck
                              # carries the 12-col grid as PowerPoint guides —
                              # both edges of every column, so 20px gutters are
                              # visible (edit-mode only, never rendered/printed).
                              # brand_theme=True by default: the deck's theme
                              # color scheme is the Prznt Perfect palette, so the
                              # PowerPoint color picker offers only design-system
                              # colors (see references/rules.md R21).
templates.BUILDERS[0](deck)   # or import t01_cover etc. individually
deck.save("output.pptx")
```

Replace sample copy with real content via each builder's parameters (see
builder signatures in `scripts/przntperfect/templates/`).

### Step 3b — Compose new layouts on the grid

```python
from przntperfect.grid import GRID       # col_x/col_width/row_y/row_height/cell (px)
from przntperfect import primitives as P # add_text, add_two_tone, add_image_placeholder,
                                          # add_footer, add_solid_background, add_hairline
from przntperfect.cards import add_card_row, card_row_geometry
# THE way to build simple card rows (problem cards, stats, numbered steps).
# Cards are GRID-SNAPPED (each spans whole columns) and BOTTOM-ANCHORED to the
# bottom grid line (R27): pass only h_px, omit y_px -> the card's bottom sits on
# y=993.5, never centred. A number heading + its title are ONE tight block, no
# gap between them (R26).
#   add_card_row(slide, [{"number": "1", "title": "Brief", "body": "..."}, ...],
#                h_px=460)                      # bottom-anchored automatically
# Cards share identical internal offsets; never hand-roll card layouts.

from przntperfect import components as K   # richer grid-snapped variants (R23)
# Placed by grid COLUMN, never raw px. Widths are fixed column spans, so they
# can only land on the grid. See references/rules.md R23 for the full map.
# These take an explicit y_px -> BOTTOM-ANCHOR them with K.bottom_anchor(h_px)
# (= 993.5 - h_px) so the card bottom lands on the grid line (R27).
#   K.add_subhead(slide, "muted line under the title")        # balanced, <=4 cols
#   K.add_bio_card(s, col=0, y_px=K.bottom_anchor(521), name=.., role=.., bullets=[..])  # span 6
#   K.add_market_card(s, col=6, y_px=K.bottom_anchor(369), header=[("A",GRAPHITE),(" – B",HIGH)], bullets=[..])
#   K.add_problem_card(s, col=0, y_px=K.bottom_anchor(445), title=.., body=..)   # span 3
#   K.add_solution_card(s, col=3, y_px=K.bottom_anchor(445), number=1, title=.., body=..)  # span 3
#   K.add_metric_card(s, col=0, y_px=K.bottom_anchor(599), header=.., rows=[..])  # native table, span 4
#   K.add_numbered_table(s, col=7, y_px=241, steps=[".."])      # native table, span 5
#   K.add_annotation(s, col=9, y_px=394, text="side note")     # hairline + note, span 3
#   K.add_paragraph_group(s, [p1, p2], cols=4)  # 1-2 body paras: tight stack,
#                                               # rests on the bottom grid line (R34)
#   n = K.add_title(s, [("Two-tone ",GRAPHITE),("title",HIGH)])  # cap-top ON the
#       # top grid line (R35); returns line count -> K.add_subhead(title_lines=n).
#       # ALWAYS place slide titles with add_title, never a raw add_text at y=44.
#       # A monotone title (plain string, e.g. "Agenda") auto-renders GRAPHITE (R40).
#   K.add_section_statement(s, [("Big claim ",WHITE_COOL),("in accent",HIGH)])
#       # ^ section/divider slide: navy bg, TWO-TONE white + High accent (R39).
#       # NEVER an all-white statement, NEVER a raw add_title on navy.
#   K.add_gradient_cover(s, [("Deck name",HIGH),("\nsubtitle",GRAY_DARK)],
#                        date="2026.11.12", quarter="Q4")   # cover AND closing (R38):
#       # Navy->blue gradient; 110px title on the top grid line, Condensed 1.5pt;
#       # blue date+quarter pills (bottom-left); circular arrow (bottom-right,
#       # arrow=False hides it). Simple vector marks = NATIVE shapes, never SVG.
#   P.add_arrow_button(s, x, y, 105, HIGH)   # standalone circular next-arrow
#   K.add_logo(s, image_path="logo.png")  OR  K.add_logo(s, name="Acme")   # top-
#       # right brand mark (R41): logo image fit in a 143.5x38 zone (38px tall),
#       # else the company
#       # name right+top-aligned to the grid corner. On EVERY slide. For dark slides
#       # pass logo_name=/logo_image= to add_gradient_cover / add_section_statement
#       # (they set dark=True), or add_logo(s, name=…, dark=True) directly.
#   K.add_data_table(s, headers, rows, start_col=0, span_cols=12)  # native template-15
#       # data table placeable at ANY columns (header + rows, horizontal rules).
#   K.add_side_table(s, headers, rows, "Heading", "body…", side="left")  # NOT full
#       # width (R42): 8-col data table + 3-col text block + one gap column between
#       # (side="left" -> text left/table right; "right" -> table left/text right).

from przntperfect.textbalance import balance_lines   # even-length line wrapping (R24)
from przntperfect.composites import bar_chart_geometry
# Bar chart: a bar is 2 grid columns for <=6 bars, 1 column for 7-12, on-grid,
# auto (R22). Count is free. NEVER hand-pick a narrow width.
#   xs, w = bar_chart_geometry(6, start_col=0)   # 6 bars -> two cols each, full width
#   xs, w = bar_chart_geometry(9, start_col=0)   # 9 bars -> one col each (auto)
#   ...draw bars with composites.add_bar_chart(...), then the REQUIRED data table:
#   K.add_chart_data_table(s, rows=[period_labels, ...], n=6, start_col=0)
#   # ^ R14: underline SEGMENTED to the bars; BOTTOM-ANCHORED to the grid line
#   #   (R34, never runs past it). Pass the SAME (n,start_col,...) as the bars.
```

Every element snaps to the 12-col × 26-row grid. Use px values from GRID;
primitives convert px→pt (×0.5) internally. Respect zone logic from the
canonical templates (heading top-left, content below, footer fixed).

### Step 4 — Content rules (hard invariants)

From `references/rules.md` — enforce on EVERY text you write or receive:

1. Sentence case only. Never ALL CAPS, never Title Case.
2. No period at the end of any heading.
3. EVERY slide title is H1 64px (= 32pt in PowerPoint), max 3 lines —
   word count does not matter. Only the cover AND closing slide (110px = 55pt,
   R38, via `add_gradient_cover`) differ. Place every other title with
   `components.add_title` (cap-top snaps ONTO the top grid line, R35) — never a
   raw `add_text` at a hand-picked y. Pass its returned line count to
   `add_subhead(title_lines=…)`.
4. Paragraphs separated by blank lines, no first-line indents.
4a. Text next to an image (vertical overlap) always leaves ≥1 empty grid
   column between itself and the image — image at col 6 → text in cols 1–4;
   image at col 7 → text in cols 1–5.
4b. A bar-chart slide ALWAYS includes its supporting data table below the
   chart (native table, segmented underlines). Never the chart alone —
   derive table rows from the chart values if none are given.
4c. In every native table, cell text is VERTICALLY CENTERED within its row
   (anchor="ctr", zero top margin).
4d. 1-2 body paragraphs are ONE GROUP (R16): a tight vertical stack, gap =
   two paragraph spacings (~52px for Body), bottom-anchored to the grid line
   (y=993.5) — never scattered or floated centered. Use
   `components.add_paragraph_group`.
4e. Paragraph width: 4 grid columns (593px) preferred, 5 columns (746px)
   is the hard maximum — only for genuinely large text volumes.
4f. Every paragraph AND subhead is LINE-BALANCED via
   `textbalance.balance_lines(text, width_px, size_px)` (R31): even line
   lengths, no widow/orphan, and no article or 1-2 letter word left at a line
   end (glued to the next word). Card bodies/subheads already do it; free
   `add_text` paragraphs MUST wrap through it too.
4g. Subhead: 12pt (24px) Gray-Dark, 20px below the title — always via
   `components.add_subhead` (R29/R30); pass `title_lines` if the title wraps.
   A person's role in a bio card is Graphite, not muted (R28).
4h. EN-DASH ONLY (–), NEVER the em-dash (—), in every text — titles included
   (R32). The engine auto-converts, but write en-dashes yourself too.
4i. TITLES obey the short-word rule too (R33): never end a title line with a
   1-letter article or 1-2 letter preposition. Soft-wrapping titles are auto-
   glued; hand-break so the short word STARTS the next line; balance monotone
   multi-line statements via `balance_lines`.
4j. Bottom-anchored text/tables sit with the last line's BASELINE on the grid
   line (R34); the chart data table is bottom-anchored and never runs past it.

### HARD BANS — never do these, whatever the content calls for

- NO eyebrows/kickers: no small label above the slide title ("Press
  release", "Case study"…). The title is the topmost text on every slide.
- NO shadows on anything. NO rounded corners on anything.
- NO badges or pills: pagination is plain 14px text via the engine footer,
  never a filled "Page N / M" chip.
- NO text inside filled shapes. Cards = engine card components (bare
  White-Cool rects + separate aligned text boxes). Tables = native tables.
- NO raw python-pptx: EVERY shape goes through the `przntperfect`
  primitives/composites — they enforce Arial, tracking, exact line pitch,
  flatness. If a pattern is missing from the engine, build it FROM the
  primitives, never from `slide.shapes.add_*` directly.
- Card rows: use the engine card component; all cards in a row share
  identical internal offsets — bodies start at the same y in every card.
5. Arial Regular only — no bold, no italic, no other fonts.
6. Colors only from the 14-token palette; accent `High #00A1FF` sparingly.
7. Chart series order: High → High-1 → High-2 → High-3 → High-4.
8. Charts are SHAPES (never native chart objects) — pixel fidelity is the
   contract. Tables (agenda 02, chart values 14, data 15) are NATIVE
   PowerPoint tables: style No-Style-No-Grid, horizontal borders only
   (header underline 1.5pt, row underlines 0.5pt, all Gray-Dark), rows
   freely add/removable. Segmented underlines (14) = spacer columns with
   no borders between content columns.
9. English only.

### Step 5 — Lint before delivering (MANDATORY GATE)

```bash
.venv/bin/python scripts/inspect_pptx.py output.pptx
```

This is a LINTER with an exit code, not a report. It fails the deck on:
shadows, rounded corners, non-Arial/bold/italic runs, sizes outside the
type scale, colors outside the 14-token palette, text inside filled
shapes (badges), eyebrow labels above titles, and a theme color scheme
that is not the Prznt Perfect palette. NEVER deliver a deck that exits
non-zero — fix and regenerate until it prints 0 violations. Also verify
no heading ends with a period and titles are 64px.

## File map

- `references/design-tokens.json` — colors, type styles, grid, canvas math
- `references/rules.md` — the design rules (R1–R24)
- `references/layouts.md` — measured anatomy of all 15 canonical templates
- `scripts/przntperfect/` — engine (units, tokens, grid, primitives,
  composites, deck, theme, cards, components, tables, textbalance, templates/)
- `scripts/build_canonical.py` — regenerates the 15-template reference deck
- `scripts/inspect_pptx.py` — deck QA linter / inventory

## Known traps

- px→pt is **×0.5** (960pt canvas), not dpi-based 0.75.
- Tracking −1%: rPr@spc = −round(size_pt) in 1/100 pt. Exception: the big
  cover/closing heading (110px) uses a fixed Condensed 1.5pt (spc=−150), not −1%
  (client rule 2026-08-06); `add_gradient_cover` handles this. Absolute letter
  spacing anywhere else: `add_text(..., track_pt=−1.5)`.
- Line spacing MUST be exact points (spcPts = size × lh%), never a float
  multiple — renderers scale multiples by the font's natural leading
  (~1.22em), inflating pitch ~22% and sagging first lines. The engine's
  `add_text` already does this; never bypass it.
- Renderers interpolate gradient fills in a non-sRGB space: a two-stop ramp
  bows ~17/255 lighter mid-ramp. Pin intermediate stops at quartiles (see
  t01_cover) for any new gradient.
- U+2028 from copied text → explicit line breaks, never literal.
- PowerPoint quantizes exact line pitch to whole points (even px on the
  1920 canvas): odd-px pitches (e.g. 20px × 124% = 25px) render as the
  nearest even value. Center the residual; never split lines into boxes.
- The canonical file itself violates rule 3 on templates 03–05/14/15
  (64px multi-word titles): reproduce canon for canonical templates,
  follow the rule for new slides.
- Slides 02, 03, 05 carry no footer; 01 has caption only.

