---
name: print-collateral-pipeline
description: Build print-ready one-page PDF collateral (flyers, sell sheets, claim stubs) from HTML/CSS via weasyprint for Rob's local-business walk-ins — per-shop brand scraping (logo, colors), the single-page fit loop, vision verification, and Drive delivery. Trigger when creating or revising printed marketing collateral that must render deterministically server-side and survive office inkjets. For the sales-strategy layer of the bail vertical specifically, load bail-bonds-vertical-playbook.
---

# Print Collateral Pipeline — HTML/CSS → Print-Ready PDF

The working pipeline proven on the bail-bonds flyer series (2026-08-08): brand-scrape the prospect's site → pour copy into an HTML/CSS shell → weasyprint render → fit-to-one-page loop → vision verify → Drive version-push. All files for the bail series live in `/root/.hermes/work/ghl/flyers/` (html sources, `pdf/` renders) with brand assets in `flyers/brand/`.

## When to load this skill

- Building or revising any printed flyer / one-pager / leave-behind / signable claim stub
- Rob says "make it look like their brand" (per-shop personalization)
- A render needs to fit one page and keep getting pushed to Drive for visual review

## The pipeline (in order)

### 1. Brand scrape (per prospect)
Pull the prospect's site and extract:
- **Logo:** grep the homepage HTML for `logo` img tags. Watch for: white-on-transparent versions (made for dark site headers — useless on white paper; find the dark variant, often the favicon/apple-touch-icon) and **transparent padding** (a 512×512 favicon whose actual art is 468×118 — crop with PIL `getbbox()` or the header blows up vertically).
- **Colors:** `grep -oP '#[0-9a-fA-F]{6}' page.html | sort | uniq -c | sort -rn` — the dominant hex is their brand color (ADAN: #2A94D4 blue).
- **Fonts:** Google Fonts families in the CSS — note them, but DON'T depend on them for print (see pitfalls).

### 2. The "Option C" color rule (Rob's decision, 2026-08-08)
Our energy color + THEIR brand color for shop-personal touches. The page says "our system, your colors" in the ink itself. Their color goes on: the "Prepared Specifically For" kicker, the field-report sidebar, the scoreboard checkbox, key emphasis words (e.g. every "Free"). One accent story per page, disciplined — two accent colors max.

### 3. HTML/CSS shell rules for print
- `@page { size: letter; margin: ~0.35-0.42in 0.55in; }`, inline CSS only, NO JS
- **System font stack only** (Liberation Sans/Serif, DejaVu on this VPS) — Google Fonts won't resolve in weasyprint without local font files, and downloads fail on RO-/root. Don't fight it.
- **No flexbox/grid for anything that must align precisely** — weasyprint's support is partial. Use `<table>` for multi-column header/signature layouts and CSS `column-count: 2` for two-column bullet lists.
- `page-break-inside: avoid` on any block that must stay atomic (offer box, payoff box).

### 4. The single-page fit loop (measured, not guessed)
Render → check page count with pymupdf → if 2 pages, measure precisely:
```python
import pymupdf
d = pymupdf.open(path)
p1_blocks = d[0].get_text('blocks')
free = 792 - (margin_bottom_pt) - max(b[3] for b in p1_blocks)  # free space at bottom of p1
p2_blocks = d[1].get_text('blocks')
need = max(b[3] for b in p2_blocks) - min(b[1] for b in p2_blocks)  # height of spilled content
```
Then compress in order of least visual damage: (a) margins, (b) block spacing/padding, (c) font-size steps of 0.2pt, (d) copy edits (flatten bullet lists to one line, shorten the bio) — NEVER guess-and-hope; measure `need` vs `free` each round. Repeat until `pages: 1`.

### 5. Vision-verify before shipping
Render page to PNG (`page.get_pixmap(dpi=120)`) and run vision_analyze with a checklist of the specific claims (section order, colors, exact strings). **Caution:** vision OCR is unreliable for fine spatial judgments (it reported a signature gap as "not implemented" when pixel coordinates showed it was) and for exact spellings — for both, trust `get_text('blocks')` coordinates and source grep over the vision read. Vision is for gross layout breakage, not typography forensics.

### 6. Drive version-push
nginx /stage/ block — **use THIS skill's `scripts/nginx-stage-block.py`** (`add`/`remove` — the durable copy, since the original lived in volatile /tmp and the earlier inline-sed approach corrupted the vhost once) → `mkdir -p /var/www/html/stage`, cp file in, `systemctl reload nginx` → curl-verify 200 → GOOGLEDRIVE_UPLOAD_FROM_URL (see `composio-mcp-ops` for the MCP call pattern) → verify Drive byte-size matches source → **always clean up** (delete staged file, remove block, reload, confirm 404). Version filenames monotonically (`flyer-X-v10-....pdf`) so Rob's Drive history tells the story.

## Pitfalls (all hit live 2026-08-08)

1. **weasyprint ignores `padding-bottom` on table cells** for spacing tricks — use an explicit empty spacer `<tr><td style="height:Npx"></td></tr>` instead.
2. **A "2x bigger" logo request breaks the page if the source is square-padded.** Crop transparent margins first (PIL getbbox), THEN size up.
3. **Write-file and agent-side tools add restrictive permissions (0600)** — content staged for nginx/Syncthing needs an explicit `chmod 644` or consumers can't read it.
4. **Inkjet rule:** heavy dark fills are fine in SMALL doses (one band, one box border) but prefer dark-text-on-light-accent over white-text-on-dark-fill; no gradients, no full-page backgrounds.
5. **Rob's design taste beats contrast-math** — he rejected a monochrome "safe" draft as "boring" and was right (see memory). Present the native-to-vertical palette (bail = navy+amber storefront energy), don't pre-decide on compliance criteria.
6. **Keep the render loop tight and cheap:** one render + one page-count check per iteration; only vision-check when the structure changes, not every nudge.

## Related

- `bail-bonds-vertical-playbook` — the sales/copy layer for the bail vertical (Solo offer, vocabulary rules, walk-in beat sheet)
- `composio-mcp-ops` — the Composio MCP call pattern for GOOGLEDRIVE_UPLOAD_FROM_URL
- `/root/.hermes/work/ghl/FLYER-REBUILD-PUNCHLIST.md` — the 18-item decision log this pipeline executed against
