The aesthetic layer. Read this before touching any script. This file answers "what should it look like and why."
Every design decision must be rooted in the document's content and purpose. Dark teal + cream is not "professional". Serif + beige is not "elegant". A color chosen because it fits the content will always outperform a color chosen because it seems safe.
palette.py takes a short content description and outputs tokens.json.
Here is the reasoning it applies:
| Content signal | Mood | Background | Accent | Text |
|---|---|---|---|---|
| Research, science, analysis | Authoritative | #0F1F2E deep ink |
#00B4A6 teal |
#F0EDE6 warm white |
| Business, strategy, finance | Confident | #1C1C2B near-black |
#E8A020 amber |
#F5F2EC cream |
| Creative, portfolio, design | Expressive | #1A0A2E deep violet |
#FF6B6B coral |
#FAF5FF lavender white |
| Education, academic paper | Scholarly | #FAFAF7 warm white |
#2C4A7C navy |
#1A1A2E dark |
| Healthcare, wellness | Calm | #F5F9F8 pale mint |
#2D8B72 forest |
#1E3830 deep green |
| Resume / personal | Clean | #FFFFFF white |
pick from content | #111111 near-black |
| General / unknown | Neutral | #F8F6F1 warm off-white |
#3D3D3D dark gray |
#1A1A1A black |
| Formal publications, annual reports | Magazine | #F2F0EC warm linen |
#1C3557 deep navy |
#0D1A2B near-black |
| Premium/dark reports, tech reviews | Darkroom | #151C27 deep navy |
#4A6FA5 steel blue |
#F0EDE6 warm white |
| Technical docs, developer reports | Terminal | #0D1117 near-black |
#39D353 neon green |
#E6EDF3 cool white |
| Portfolios, creative, photography | Poster | #FFFFFF white |
#0A0A0A near-black |
#0A0A0A near-black |
| ❌ Avoid | Why |
|---|---|
| Purple gradient on white | The default AI aesthetic — immediately signals "generated" |
| Navy + gold | Overused corporate cliché |
| All-black background | Prints badly, feels aggressive |
| More than 3 colors in the system | Visual noise |
| Accent on body text | Destroys readability |
Two typefaces maximum. Always.
| Role | Criteria | Good choices (system-safe) |
|---|---|---|
| Display (cover title, H1) | Distinctive, strong contrast, high weight | Times New Roman, Georgia (serif) |
| Text (body, captions, UI) | Highly readable at 10–11pt | Helvetica, Arial (sans) |
Cover fonts are loaded live via @import url(...) in the cover HTML — Playwright
fetches them at render time, no local caching. Body pages always use system fonts
(Times-Bold / Helvetica) via ReportLab — consistent and offline-safe.
Pairs by mood (cover HTML only — body always uses system fonts):
Playfair Display / IBM Plex SansSyne / Nunito SansFraunces / InterEB Garamond / Source Sans 3DM Serif Display / DM SansCormorant Garamond / JostBarlow Condensed / BarlowMontserrat / MontserratCormorant / Crimson ProBebas Neue / Libre FranklinTimes-Bold / Helvetica (ReportLab system fonts)All sizes in points. This scale is used by palette.py to populate tokens.json.
| Token | Size | Leading | Usage |
|---|---|---|---|
display |
54pt | 1.0 | Cover title |
h1 |
22pt | 1.3 | Section headings |
h2 |
15pt | 1.4 | Subsection headings |
h3 |
11.5pt | 1.5 | Sub-subsection |
body |
10.5pt | 1.6 | Main prose |
caption |
8.5pt | 1.4 | Figure/table captions |
meta |
8pt | 1.3 | Header/footer text |
Margins and rhythm are what separate "looks designed" from "looks printed".
| Token | Value | Notes |
|---|---|---|
margin_outer |
2.8cm | Left/right page margin |
margin_top |
2.8cm | Top page margin |
margin_bottom |
2.5cm | Bottom page margin |
section_gap |
26pt | Space before H1 |
para_gap |
8pt | Space after paragraph |
line_gap |
17pt | Leading for body text |
Never use ReportLab's default margins (too tight). Always set explicitly.
The cover is the most important page. It determines whether a reader trusts the document.
cover.py selects one based on tokens.json["cover_pattern"].
1. fullbleed — used for: report, general
2. split — used for: proposal
3. typographic — used for: resume, academic
4. atmospheric — used for: portfolio
5. minimal — used for: minimal
6. stripe — used for: stripe
7. diagonal — used for: diagonal
8. frame — used for: frame
9. editorial — used for: editorial
10. magazine — used for: magazine
cover_image URL renders as centered hero thumbnailabstract field: justified text block with bold "Abstract:" label11. darkroom — used for: darkroom
magazine but deep navy background, white textgrayscale(20%) brightness(0.9) filter12. terminal — used for: terminal
SYSTEM_REPORT // <date>> in accent color13. poster — used for: poster
cover_image rendered as 260×340 grayscale thumbnail, right-alignedcover_imagePatterns magazine, darkroom, and poster accept an optional cover_image
token containing an absolute URL or file:// path to an image.
The image renders via <img src="..."> — Playwright fetches it at render time.
If omitted, the image area is simply skipped (layout adjusts gracefully).
These three rules must appear in every cover HTML file or the output will have white borders / incorrect dimensions:
body { margin: 0; padding: 0; }
html, body { width: 794px; height: 1123px; overflow: hidden; }
No @page rules needed — Playwright handles page size via the pdf() call.
Do NOT use CSS background-image for textures — use inline SVG or <canvas>.
Always use position: absolute + z-index for layered elements.
Every design decision should remove something, not add something. The page is done when there is nothing left to remove.
Header: document title (left, 7.5pt, muted) + accent rule (1.5pt, full width below) Footer: author name (left, 7.5pt, muted) + page number (right, 7.5pt, muted) + light rule above
A PDF passes if a designer would not be embarrassed to hand it to a client. Concretely:
All body blocks use the same token system — colors and fonts come from tokens.json, never hardcoded.
| Block | Rendering | Design notes |
|---|---|---|
h1 |
22pt heading + full-width accent rule below | KeepTogether with rule — heading never orphaned |
h2 |
15pt heading, dark text | No rule, no accent — visual hierarchy through size only |
h3 |
11.5pt bold, dark text | No accent color — accent on body headings violates the one-accent-location rule |
body |
10.5pt justified, 17pt leading | Supports <b> <i> <font> markup |
bullet |
Body size with • prefix, 14pt indent |
Use for unordered lists |
numbered |
Body size with N. prefix, hanging indent |
Counter auto-resets on any non-numbered block — no manual numbering needed |
callout |
Accent left-border (4px) + light tint background | Max one callout per section — overuse kills impact |
table |
Accent header row, alternating row tint, outer box only | Supports col_widths (fractions, e.g. [0.3, 0.5, 0.2]) for custom column widths |
image |
Scaled to column width, preserving aspect ratio | Use path or src; always provide a caption |
figure |
Same as image, but caption auto-prefixed "Figure N:" | Figure counter increments across all figure, chart, flowchart blocks |
code |
Courier 8.5pt, accent left-border, light tint background | Supports optional language label (rendered above block) |
math |
Formula centered, optional right-aligned equation label | LaTeX syntax; matplotlib mathtext renderer |
chart |
Bar / line / pie chart rendered via matplotlib | Color palette derived from document accent; figure auto-numbered |
flowchart |
Process diagram with labeled arrows | Supports 4 node shapes; back-edges drawn as curved arcs |
bibliography |
Numbered reference list with hanging indent | Heading rendered as h2 + accent rule; items as [N] text |
divider |
Accent-colored 1.2pt rule with padding | Use sparingly — only for major thematic breaks |
caption |
8.5pt muted text, centered | Appears below images/tables via field or explicit block |
pagebreak |
Force page break | — |
spacer |
Vertical whitespace | pt field (default 12) |
Input syntax: standard LaTeX math notation — \frac{}{}, \int, \sum, \alpha, ^, _, etc.
Rendering engine: matplotlib mathtext — pure Python, no LaTeX compiler, no browser required.
| Syntax example | Rendered as |
|---|---|
E = mc^2 |
Inline expression |
\frac{\sqrt{\pi}}{2} |
Fraction |
\int_0^\infty e^{-x^2} dx |
Integral |
\sum_{i=1}^{n} x_i |
Summation |
\alpha + \beta = \gamma |
Greek letters |
Limitations: matplotlib mathtext covers most common expressions but not advanced LaTeX environments (align, cases, matrix). Split complex multi-line proofs into multiple math blocks.
Fallback: if matplotlib is not installed, renders as expression in code style. Run make.sh fix to install.
Equation labels: "label": "(1)" — rendered right-aligned beside the formula.
Rendered entirely in Python — no external chart services, image files, or internet required.
| chart_type | Use case | Required fields |
|---|---|---|
bar |
Comparing discrete categories | labels, datasets |
line |
Trends over time or ordered categories | labels, datasets |
pie |
Part-to-whole composition | labels, datasets[0].values |
datasets, each with a label and values array."figure": true (default) or "figure": false to suppress.Node shapes:
| shape | Use for |
|---|---|
rect (default) |
Process step |
diamond |
Decision / condition |
oval or terminal |
Start / End |
parallelogram |
Input / Output |
id field is the reference label — use numbers ("1", "2") or alphanumeric ("Smith23").title field defaults to "References". Set "title": "" to suppress the heading.bibliography block always starts with a new section heading + accent rule.figure blocks auto-number; image blocks do not — use figure for numbered figures[Image not found] placeholder is substitutedlanguage field renders a small language label above the block (e.g., "language": "python")