# CV Theme Engine > Phase 4 (2026-07-18). How CV themes work and how to add one. Companion to > `docs/architecture/cv-builder.md`. Verified against code. ## The rule **A theme is data, not code.** There is exactly one renderer — `ThemedCvRenderer` (`JobTrackerApi/Services/ThemedCvRenderer.cs`) — with a single render path. Every visual difference between themes is expressed by the fields of a `CvTheme` record (`JobTrackerApi/Models/CvTheme.cs`). Adding a theme never touches the renderer. This replaces the previous approach (`CvTemplateRenderer`, one hand-written HTML method per template), which is retained only for the legacy tailored-draft flow and is not used by the builder. ## `CvTheme` fields (the knobs) | Group | Fields | |---|---| | Identity | `Id`, `Name`, `Category`, `Description` | | Layout | `Layout` (`single` \| `sidebar-left` \| `sidebar-right` \| `header-band`), `SidebarWidthMm`, `SidebarSections` | | Palette | `Accent`, `Ink`, `Muted`, `Line`, `Paper`, `SidebarBg`, `SidebarInk`, `HeadingColor` | | Typography | `HeadingFont`, `BodyFont`, `NameSizePt`, `HeadingSizePt`, `BodySizePt`, `LineHeight` | | Spacing | `PageMarginMm`, `SectionGapMm`, `EntryGapMm` | | Styling | `HeaderStyle` (`plain`\|`band`\|`centered`\|`kicker`), `HeadingStyle` (`caps-rule`\|`underline`\|`plain`\|`bar`), `PhotoShape` (`none`\|`square`\|`rounded`\|`circle`), `DefaultIcons` | The renderer computes CSS variables from these plus the variant's runtime overrides (accent, fonts, density, page size, photo, icons) and picks one of the layout wrappers. `SidebarSections` decides which section keys move to the sidebar for the two-column layouts. ## Adding a theme 1. Append one `CvTheme { … }` to `CvThemeCatalog.Themes` (`JobTrackerApi/Models/CvTheme.cs`). Only override the fields that differ from the defaults. 2. Nothing else. It appears in `GET /api/cv/themes`, the Customize tab picker, and renders. Add a `CvBuilderTests.Every_catalog_theme_renders_valid_html` already loops the whole catalog, so a new theme is smoke-tested automatically. ## Shipped themes (8) `modern` (header-band), `minimal` (single), `executive` (single, serif), `technical` (sidebar-left, dense), `ats-classic` (single, no graphics), `nordic` (sidebar-right), `elegant` (single, editorial), `creative` (sidebar-left, bold). ## Rich text, ATS, and print (2026-07-18) - **Rich-text bullets** are shipped: bullets/summary support `**bold**`, `*italic*`, `__underline__`, `[text](url)` via `ThemedCvRenderer.Inline` — everything is HTML-escaped first, then a whitelist is re-applied, so no user tag survives. Storage stays plain text (no HTML, no sanitiser dependency). - **ATS-friendliness** is a data flag: `CvTheme.AtsFriendly` (set on the single-column themes), surfaced in `GET /api/cv/themes` and badged in the Customize tab. Two-column (sidebar) themes are not flagged, as sidebar layouts can trip naive resume parsers. - **Print quality**: the renderer emits `break-inside: avoid` on entries, `break-after: avoid` on section headings, and widow/orphan control, so entries don't split across a page in the Playwright PDF pass. ## Deliberately not here (yet) - **PDF page numbers.** `ShowPageNumbers` is carried through settings but the actual footer is a Playwright `footerTemplate` concern (the PDF exporter), not CSS paged-media (Chromium's margin boxes are unreliable). Wire it in `PlaywrightCvPdfExporter` when needed.