---
name: hco-pptx
description: Builds Howatson+Company branded PowerPoint presentations with python-pptx, either by populating the bundled 28-slide H+Co template from markdown content or by constructing slides from scratch against the template specifications. Use when someone asks for a PowerPoint, PPTX deck, or H+Co branded slides. For Google Slides output, convert the finished PPTX with pptx-to-google-slides.
---

# H+Co PowerPoint Decks

Create Howatson+Company branded PPTX decks. Two paths, pick one:

- **Template path (default):** duplicate and populate slides from the bundled 28-slide template (`assets/ppt-template-v2.pptx`). Fastest and safest for standard content decks.
- **Freeform path:** build slides from scratch with python-pptx against exact measurements. Use when the content does not fit a template layout, or the user asks for the bold knockout-headline style (see `references/template-specs.md` and `assets/freeform-screenshots/`).

The canonical H+Co visual system (colour, type, layout principles) lives in the `hco-design-system` skill. This file keeps only what is PPTX-specific.

## Dependencies

```bash
pip install python-pptx pdf2image
```

LibreOffice (`soffice`) is required for visual validation (PPTX to PDF to images). If it is missing, ask the user to install it or to export slides manually.

## Template path

### Input and mapping

Accept markdown content. Map it to slide types using `docs/TEMPLATE_CATALOG.md`. Common mappings:

- H1 headings → `section-divider`
- Key quotes → `statement-dark` / `statement-light`
- Statistics → `statistics-grid`, hero stat → `single-stat`
- Bullet lists → `bullet-list`, title + body → `heading-paragraph`
- End of deck → `thank-you`

`[IMAGE: description]` markers in the source flag images for manual replacement. Track them per slide and list them in the final summary.

### Approval before generation

Show the user the proposed slide order before generating. Where content could map to more than one layout (statement vs break, grid vs bubbles vs rows, text-right vs image-right vs fullbleed), present the options and let them choose. `docs/LAYOUT_DECISIONS.md` lists the ambiguity triggers; `docs/ASCII_TEMPLATES.md` has compact previews you can show inline. Wait for approval, then generate.

### Generation

Use the functions in `scripts/generate_presentation.py`:

- `duplicate_slide(prs, index)` copies a template slide preserving image and chart relationships
- `replace_placeholder(slide, token, text)` replaces `{{tokens}}` while preserving formatting, including tokens fragmented across XML runs
- `generate_output_filename(client, title)` produces `{client}_{title}.pptx`

Exact placeholder tokens per slide type are in `docs/SLIDE_DETAILS.md`.

### Content rules

- Never rewrite the user's text without approval.
- Auto-transforms: headlines to UPPERCASE; section numbers zero-padded ("1" → "01"); client names get a + prefix ("Acme" → "+Acme").
- Sidebar colour always matches the headline colour on that slide.
- Full typography and colour rules: `docs/BRAND_ELEMENTS.md`.

### Validation (do not skip)

1. **Programmatic:** `python scripts/validate_presentation.py output.pptx -v` checks for unreplaced `{{placeholders}}`, font consistency, image presence, and the thank-you slide being last.
2. **Visual:** convert the PPTX to PDF with `soffice --headless --convert-to pdf`, render pages to images with pdf2image, then Read each generated slide image alongside its template screenshot in `assets/template-screenshots/{type-id}.jpeg`. Compare layout, fonts (VC Nudge Black headlines, Manrope body), colours, sidebar, and that no `{{tokens}}` remain. Programmatic validation passes decks that look broken; the visual pass is what catches them.

Fix and regenerate until both pass, then summarise: output filename, slide count, and the list of `[IMAGE: ...]` placeholders needing manual replacement in PowerPoint.

## Freeform path

Read `references/template-specs.md` for exact measurements and `references/slide-layouts.md` for layout construction. `assets/template-freeform.pptx` and `assets/freeform-screenshots/` show the target. `scripts/create_slides.py` contains working examples of every slide type.

Quick spec:

- Dimensions: 13.33" x 7.5" (16:9)
- Headlines: VC Nudge Black, 60-400pt, uppercase. Never substitute Arial or Helvetica.
- Body: Manrope Light 14-18pt. Labels: IBM Plex Mono 8-10pt.
- Palette: BLACK #000000, WHITE #FFFFFF, BEIGE #EDDAC1, CYAN #76DBEC, BLUE #3D72FE, LIME #E0EA53, RED #FF4F4D, PINK #E972E2, GREEN #00B948
- Vertical "HOWATSON+COMPANY" text on the left edge of every slide.

## python-pptx pitfalls (hard-won, applies to both paths)

### Multi-line text
Apply the font to EACH paragraph, not just the first:

```python
# WRONG - only first line gets font
tf.text = "LINE 1\nLINE 2"
tf.paragraphs[0].font.name = "VC Nudge Black"  # Only applies to LINE 1!

# CORRECT - format each paragraph
for i, line in enumerate(lines):
    p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
    p.text = line
    p.font.name = "VC Nudge Black"
    p.font.size = Pt(size)
    p.font.color.rgb = color
```

### Shadows on shapes
Call `remove_shadow()` on ALL shapes (text boxes, rectangles, lines):

```python
from pptx.oxml.ns import qn

def remove_shadow(shape):
    spPr = shape._element.spPr
    for effect in spPr.findall(qn('a:effectLst')):
        spPr.remove(effect)
    for shadow in spPr.findall('.//' + qn('a:outerShdw')):
        shadow.getparent().remove(shadow)
```

### Underlines
Use `MSO_CONNECTOR.STRAIGHT` connectors, not thin rectangles, to avoid shadow artifacts:

```python
from pptx.enum.shapes import MSO_CONNECTOR

connector = slide.shapes.add_connector(
    MSO_CONNECTOR.STRAIGHT, Inches(x1), Inches(y1), Inches(x2), Inches(y2))
connector.line.width = Pt(1)
remove_shadow(connector)
```

### Sidebar line
Must run y=0 to y=7.5 (full slide height, edge to edge). Colour: black on coloured backgrounds (cyan, blue, lime); matches the heading colour otherwise.

### Section divider titles
Anchor to the bottom to stop titles floating: `txBox.text_frame.anchor = MSO_ANCHOR.BOTTOM`.

### Line spacing
Tight headlines use `p.line_spacing = 0.85`.

### Run fragmentation
PowerPoint splits text like `{{title` + `.name}}` across runs. `replace_placeholder()` handles this by reconstructing paragraph-level text; if you write your own replacement, check full paragraph text, not individual runs.

### Fonts reverting to Arial/Calibri
Font properties are lost if you replace text naively. Use `replace_placeholder()`, which preserves them.

### Images missing after duplication
rId relationships are not copied by default. Use `duplicate_slide()`, which copies all relationships.

## References

| File | Contents |
|------|----------|
| `docs/TEMPLATE_CATALOG.md` | All template slide types, indices, content mapping |
| `docs/SLIDE_DETAILS.md` | Placeholder tokens and positions per slide |
| `docs/BRAND_ELEMENTS.md` | PPTX typography and colour rules |
| `docs/LAYOUT_DECISIONS.md` | Ambiguity triggers, when to offer layout choices |
| `docs/ASCII_TEMPLATES.md` | Compact layout previews for the approval step |
| `docs/VALIDATION_CHECKLIST.md` | QA procedures, common issues |
| `references/template-specs.md` | Freeform path: exact measurements |
| `references/slide-layouts.md` | Freeform path: layout construction |
