That decision raised three problems, and each needed its own answer.
Fifty identities, one codebase
Layout, skill style, pattern and photo frame are each their own enum. One skill-visualiser widget reads the style and switches between real implementations: a segmented circular arc, a DNA helix, a thermometer fill. Each is its own drawing, not a reskin.
Customise without breaking intent
Font, icon colour, header colour and photo frame are each overridable, and every override defaults to null, meaning "respect the template". A sentinel object in copyWith tells "untouched" apart from "cleared". Resolvers like resolveFont decide, field by field, who wins.
One job title, two vocabularies
The career picker offers 68 specific titles ("Chemical Engineer"). Auto-fill summaries are written for 15 broad categories ("Engineering"). An explicit lookup map bridges the direct cases and a fuzzy keyword match handles free-typed titles.
Underneath, each résumé is saved in its own local storage slot with a lightweight index, so switching between drafts never re-parses every saved résumé. Résumés saved in the older single-slot format migrate into the new structure automatically on first load, without losing content.
String resolveFont(CVTemplate t) => fontFamilyOverride ?? t.headingFont;
Color resolveHeaderColor(CVTemplate t) => headerColorOverride ?? t.primaryColor;