Explorar o código

Align filled CV output with gallery templates in light and dark mode.

Shared résumé colors and per-family layouts so the preview and PDF match the template card the user selected.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 hai 2 meses
pai
achega
4ef6f513d9

+ 80 - 0
App for Indeed/Services/CVResumeAppearance.swift

@@ -0,0 +1,80 @@
+//
+//  CVResumeAppearance.swift
+//  App for Indeed
+//
+//  Shared résumé colours for gallery thumbnails and filled CV preview/export.
+//  Tracks app light / dark mode so the selected template looks the same everywhere.
+//
+
+import AppKit
+
+@MainActor
+enum CVResumeAppearance {
+
+    struct Colors {
+        let paper: NSColor
+        let ink: NSColor
+        let muted: NSColor
+        let rule: NSColor
+        let cardBackground: NSColor
+        let sidebarTint: NSColor
+        let accentRed: NSColor
+        let accentBlue: NSColor
+    }
+
+    static var isDark: Bool { AppAppearanceManager.shared.isDark }
+
+    static func colors(isDark dark: Bool? = nil) -> Colors {
+        let dark = dark ?? isDark
+        if dark {
+            return Colors(
+                paper: NSColor(srgbRed: 28 / 255, green: 30 / 255, blue: 36 / 255, alpha: 1),
+                ink: NSColor(srgbRed: 0.94, green: 0.95, blue: 0.97, alpha: 1),
+                muted: NSColor(srgbRed: 0.62, green: 0.66, blue: 0.72, alpha: 1),
+                rule: NSColor(srgbRed: 0.38, green: 0.40, blue: 0.46, alpha: 1),
+                cardBackground: NSColor(srgbRed: 32 / 255, green: 34 / 255, blue: 40 / 255, alpha: 1),
+                sidebarTint: NSColor(srgbRed: 40 / 255, green: 42 / 255, blue: 48 / 255, alpha: 1),
+                accentRed: NSColor(srgbRed: 235 / 255, green: 88 / 255, blue: 72 / 255, alpha: 1),
+                accentBlue: AppDashboardTheme.brandBlue
+            )
+        }
+        return Colors(
+            paper: NSColor.white,
+            ink: NSColor(srgbRed: 38 / 255, green: 50 / 255, blue: 71 / 255, alpha: 1),
+            muted: NSColor(srgbRed: 110 / 255, green: 118 / 255, blue: 132 / 255, alpha: 1),
+            rule: NSColor(srgbRed: 228 / 255, green: 232 / 255, blue: 240 / 255, alpha: 1),
+            cardBackground: NSColor.white,
+            sidebarTint: AppDashboardTheme.cvMakerPreviewSidebarTint,
+            accentRed: NSColor(srgbRed: 207 / 255, green: 67 / 255, blue: 50 / 255, alpha: 1),
+            accentBlue: AppDashboardTheme.brandBlue
+        )
+    }
+
+    /// Slight paper tint by layout variant (gallery + filled CV use the same rule).
+    static func paperBackground(variant: Int, base: NSColor) -> NSColor {
+        guard !isDark else { return base }
+        switch variant % 5 {
+        case 0: return base
+        case 1: return NSColor(srgbRed: 0.995, green: 0.992, blue: 0.985, alpha: 1)
+        case 2: return NSColor(srgbRed: 0.96, green: 0.99, blue: 1, alpha: 1)
+        case 3: return NSColor(srgbRed: 0.99, green: 0.99, blue: 0.99, alpha: 1)
+        default: return NSColor(srgbRed: 0.99, green: 0.98, blue: 0.995, alpha: 1)
+        }
+    }
+
+    static func accentColor(for template: CVTemplate) -> NSColor {
+        let palette = colors()
+        switch template.accent {
+        case .redUnderline, .redBar:
+            return palette.accentRed
+        case .blueBar:
+            return template.themeColor
+        case .none:
+            return template.themeColor.blended(withFraction: 0.5, of: palette.ink) ?? template.themeColor
+        }
+    }
+
+    static func sectionHeadingColor(for template: CVTemplate) -> NSColor {
+        accentColor(for: template)
+    }
+}

+ 3 - 0
App for Indeed/Views/CVFilledPreviewPageView.swift

@@ -225,6 +225,9 @@ final class CVFilledPreviewPageView: NSView {
         titleLabel.textColor = AppDashboardTheme.primaryText
         titleLabel.textColor = AppDashboardTheme.primaryText
         subtitleLabel.textColor = AppDashboardTheme.secondaryText
         subtitleLabel.textColor = AppDashboardTheme.secondaryText
         exportButton.applyCurrentAppearance()
         exportButton.applyCurrentAppearance()
+        if let profile = lastProfile, let template = lastTemplate {
+            configure(profile: profile, template: template)
+        }
     }
     }
 
 
     func configure(profile: SavedProfile, template: CVTemplate) {
     func configure(profile: SavedProfile, template: CVTemplate) {

+ 6 - 12
App for Indeed/Views/CVMakerPageView.swift

@@ -563,18 +563,12 @@ final class CVMakerPageView: NSView {
         static var cardBorderSelected: NSColor { AppDashboardTheme.brandBlue }
         static var cardBorderSelected: NSColor { AppDashboardTheme.brandBlue }
         static var cardFooter: NSColor { AppDashboardTheme.cvMakerCardFooter }
         static var cardFooter: NSColor { AppDashboardTheme.cvMakerCardFooter }
         static var previewSurface: NSColor { AppDashboardTheme.cvMakerPreviewSurface }
         static var previewSurface: NSColor { AppDashboardTheme.cvMakerPreviewSurface }
-        static var previewPaper: NSColor { NSColor.white }
-        static var previewSidebarTint: NSColor { AppDashboardTheme.cvMakerPreviewSidebarTint }
-        static var previewInk: NSColor {
-            NSColor(srgbRed: 38 / 255, green: 50 / 255, blue: 71 / 255, alpha: 1)
-        }
-        static var previewMuted: NSColor {
-            NSColor(srgbRed: 165 / 255, green: 175 / 255, blue: 192 / 255, alpha: 1)
-        }
-        static var previewAccentRed: NSColor {
-            NSColor(srgbRed: 207 / 255, green: 67 / 255, blue: 50 / 255, alpha: 1)
-        }
-        static var previewAccentBlue: NSColor { AppDashboardTheme.brandBlue }
+        static var previewPaper: NSColor { CVResumeAppearance.colors().paper }
+        static var previewSidebarTint: NSColor { CVResumeAppearance.colors().sidebarTint }
+        static var previewInk: NSColor { CVResumeAppearance.colors().ink }
+        static var previewMuted: NSColor { CVResumeAppearance.colors().muted }
+        static var previewAccentRed: NSColor { CVResumeAppearance.colors().accentRed }
+        static var previewAccentBlue: NSColor { CVResumeAppearance.colors().accentBlue }
         static var ctaBackground: NSColor { AppDashboardTheme.brandBlue }
         static var ctaBackground: NSColor { AppDashboardTheme.brandBlue }
         static var ctaHover: NSColor { AppDashboardTheme.brandBlueHover }
         static var ctaHover: NSColor { AppDashboardTheme.brandBlueHover }
         static var ctaText: NSColor { AppDashboardTheme.proCTAText }
         static var ctaText: NSColor { AppDashboardTheme.proCTAText }

+ 395 - 47
App for Indeed/Views/CVProfileDocumentView.swift

@@ -38,6 +38,12 @@ private struct DocumentStyle {
 
 
     static func make(for template: CVTemplate) -> DocumentStyle {
     static func make(for template: CVTemplate) -> DocumentStyle {
         let theme = template.themeColor
         let theme = template.themeColor
+        let colors = CVResumeAppearance.colors()
+        let sectionInk = CVResumeAppearance.sectionHeadingColor(for: template)
+        let cardBG = CVResumeAppearance.paperBackground(
+            variant: template.galleryLayoutVariant,
+            base: colors.cardBackground
+        )
         switch template.family {
         switch template.family {
         case .minimal:
         case .minimal:
             return DocumentStyle(
             return DocumentStyle(
@@ -54,14 +60,14 @@ private struct DocumentStyle {
                 bulletBodyFont: .systemFont(ofSize: 12, weight: .regular),
                 bulletBodyFont: .systemFont(ofSize: 12, weight: .regular),
                 bulletMarkerFont: .systemFont(ofSize: 11, weight: .light),
                 bulletMarkerFont: .systemFont(ofSize: 11, weight: .light),
                 bulletMarkerColor: theme.withAlphaComponent(0.55),
                 bulletMarkerColor: theme.withAlphaComponent(0.55),
-                ink: NSColor(srgbRed: 42 / 255, green: 48 / 255, blue: 56 / 255, alpha: 1),
-                muted: NSColor(srgbRed: 110 / 255, green: 118 / 255, blue: 128 / 255, alpha: 1),
-                rule: NSColor(srgbRed: 228 / 255, green: 230 / 255, blue: 234 / 255, alpha: 1),
-                cardBackground: NSColor(srgbRed: 0.998, green: 0.998, blue: 0.998, alpha: 1),
+                ink: colors.ink,
+                muted: colors.muted,
+                rule: colors.rule,
+                cardBackground: cardBG,
                 columnVerticalSpacing: 15,
                 columnVerticalSpacing: 15,
                 bodyBlockSpacing: 15,
                 bodyBlockSpacing: 15,
                 roleUsesThemeColor: false,
                 roleUsesThemeColor: false,
-                sectionInk: theme.withAlphaComponent(0.92)
+                sectionInk: sectionInk
             )
             )
 
 
         case .professional:
         case .professional:
@@ -78,15 +84,15 @@ private struct DocumentStyle {
                 eduMetaFont: .systemFont(ofSize: 11.5, weight: .medium),
                 eduMetaFont: .systemFont(ofSize: 11.5, weight: .medium),
                 bulletBodyFont: .systemFont(ofSize: 12, weight: .regular),
                 bulletBodyFont: .systemFont(ofSize: 12, weight: .regular),
                 bulletMarkerFont: .systemFont(ofSize: 12, weight: .bold),
                 bulletMarkerFont: .systemFont(ofSize: 12, weight: .bold),
-                bulletMarkerColor: NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.55),
-                ink: NSColor(srgbRed: 28 / 255, green: 36 / 255, blue: 48 / 255, alpha: 1),
-                muted: NSColor(srgbRed: 88 / 255, green: 98 / 255, blue: 118 / 255, alpha: 1),
-                rule: NSColor(srgbRed: 210 / 255, green: 218 / 255, blue: 232 / 255, alpha: 1),
-                cardBackground: NSColor.white,
+                bulletMarkerColor: theme,
+                ink: colors.ink,
+                muted: colors.muted,
+                rule: colors.rule,
+                cardBackground: cardBG,
                 columnVerticalSpacing: 13,
                 columnVerticalSpacing: 13,
                 bodyBlockSpacing: 13,
                 bodyBlockSpacing: 13,
                 roleUsesThemeColor: false,
                 roleUsesThemeColor: false,
-                sectionInk: theme
+                sectionInk: sectionInk
             )
             )
 
 
         case .modern:
         case .modern:
@@ -104,14 +110,14 @@ private struct DocumentStyle {
                 bulletBodyFont: .systemFont(ofSize: 12.5, weight: .regular),
                 bulletBodyFont: .systemFont(ofSize: 12.5, weight: .regular),
                 bulletMarkerFont: .systemFont(ofSize: 13, weight: .bold),
                 bulletMarkerFont: .systemFont(ofSize: 13, weight: .bold),
                 bulletMarkerColor: theme,
                 bulletMarkerColor: theme,
-                ink: NSColor(srgbRed: 24 / 255, green: 34 / 255, blue: 52 / 255, alpha: 1),
-                muted: NSColor(srgbRed: 96 / 255, green: 110 / 255, blue: 132 / 255, alpha: 1),
-                rule: NSColor(srgbRed: 200 / 255, green: 214 / 255, blue: 236 / 255, alpha: 1),
-                cardBackground: NSColor(srgbRed: 0.99, green: 0.995, blue: 1, alpha: 1),
+                ink: colors.ink,
+                muted: colors.muted,
+                rule: colors.rule,
+                cardBackground: cardBG,
                 columnVerticalSpacing: 17,
                 columnVerticalSpacing: 17,
                 bodyBlockSpacing: 16,
                 bodyBlockSpacing: 16,
                 roleUsesThemeColor: true,
                 roleUsesThemeColor: true,
-                sectionInk: theme
+                sectionInk: sectionInk
             )
             )
 
 
         case .executive:
         case .executive:
@@ -125,6 +131,9 @@ private struct DocumentStyle {
                 ?? NSFontManager.shared.convert(georgia12, toHaveTrait: .italicFontMask)
                 ?? NSFontManager.shared.convert(georgia12, toHaveTrait: .italicFontMask)
             let eduMeta = NSFont(name: "Georgia-Italic", size: 11.5)
             let eduMeta = NSFont(name: "Georgia-Italic", size: 11.5)
                 ?? NSFontManager.shared.convert(georgia115, toHaveTrait: .italicFontMask)
                 ?? NSFontManager.shared.convert(georgia115, toHaveTrait: .italicFontMask)
+            let execCard = CVResumeAppearance.isDark
+                ? cardBG
+                : NSColor(srgbRed: 0.992, green: 0.99, blue: 0.985, alpha: 1)
             return DocumentStyle(
             return DocumentStyle(
                 nameFont: serifName,
                 nameFont: serifName,
                 roleFont: serifRole,
                 roleFont: serifRole,
@@ -138,15 +147,15 @@ private struct DocumentStyle {
                 eduMetaFont: eduMeta,
                 eduMetaFont: eduMeta,
                 bulletBodyFont: serifCompact,
                 bulletBodyFont: serifCompact,
                 bulletMarkerFont: .systemFont(ofSize: 11, weight: .bold),
                 bulletMarkerFont: .systemFont(ofSize: 11, weight: .bold),
-                bulletMarkerColor: NSColor(srgbRed: 55 / 255, green: 55 / 255, blue: 62 / 255, alpha: 1),
-                ink: NSColor(srgbRed: 22 / 255, green: 22 / 255, blue: 28 / 255, alpha: 1),
-                muted: NSColor(srgbRed: 82 / 255, green: 82 / 255, blue: 90 / 255, alpha: 1),
-                rule: NSColor(srgbRed: 72 / 255, green: 72 / 255, blue: 78 / 255, alpha: 0.35),
-                cardBackground: NSColor(srgbRed: 0.992, green: 0.99, blue: 0.985, alpha: 1),
+                bulletMarkerColor: colors.ink.withAlphaComponent(0.75),
+                ink: colors.ink,
+                muted: colors.muted,
+                rule: colors.rule,
+                cardBackground: execCard,
                 columnVerticalSpacing: 18,
                 columnVerticalSpacing: 18,
                 bodyBlockSpacing: 17,
                 bodyBlockSpacing: 17,
                 roleUsesThemeColor: false,
                 roleUsesThemeColor: false,
-                sectionInk: NSColor(srgbRed: 32 / 255, green: 32 / 255, blue: 38 / 255, alpha: 1)
+                sectionInk: sectionInk
             )
             )
 
 
         case .creative:
         case .creative:
@@ -163,15 +172,15 @@ private struct DocumentStyle {
                 eduMetaFont: .systemFont(ofSize: 12, weight: .medium),
                 eduMetaFont: .systemFont(ofSize: 12, weight: .medium),
                 bulletBodyFont: .systemFont(ofSize: 12.5, weight: .regular),
                 bulletBodyFont: .systemFont(ofSize: 12.5, weight: .regular),
                 bulletMarkerFont: .systemFont(ofSize: 13, weight: .heavy),
                 bulletMarkerFont: .systemFont(ofSize: 13, weight: .heavy),
-                bulletMarkerColor: NSColor(srgbRed: 207 / 255, green: 67 / 255, blue: 50 / 255, alpha: 1),
-                ink: NSColor(srgbRed: 32 / 255, green: 26 / 255, blue: 52 / 255, alpha: 1),
-                muted: NSColor(srgbRed: 108 / 255, green: 96 / 255, blue: 130 / 255, alpha: 1),
+                bulletMarkerColor: CVResumeAppearance.accentColor(for: template),
+                ink: colors.ink,
+                muted: colors.muted,
                 rule: theme.withAlphaComponent(0.22),
                 rule: theme.withAlphaComponent(0.22),
-                cardBackground: NSColor(srgbRed: 0.995, green: 0.993, blue: 1, alpha: 1),
+                cardBackground: cardBG,
                 columnVerticalSpacing: 18,
                 columnVerticalSpacing: 18,
                 bodyBlockSpacing: 17,
                 bodyBlockSpacing: 17,
                 roleUsesThemeColor: true,
                 roleUsesThemeColor: true,
-                sectionInk: NSColor(srgbRed: 207 / 255, green: 67 / 255, blue: 50 / 255, alpha: 1)
+                sectionInk: sectionInk
             )
             )
         }
         }
     }
     }
@@ -187,9 +196,12 @@ final class CVProfileDocumentView: NSView {
 
 
     private let profile: SavedProfile
     private let profile: SavedProfile
     private let template: CVTemplate
     private let template: CVTemplate
-    private let style: DocumentStyle
+    private var style: DocumentStyle
     /// Matches `CVTemplatePreviewView` so the same template id + layout recipe renders the same silhouette as the gallery card.
     /// Matches `CVTemplatePreviewView` so the same template id + layout recipe renders the same silhouette as the gallery card.
     private let variant: Int
     private let variant: Int
+    private var appearanceObserver: NSObjectProtocol?
+    private weak var cardView: NSView?
+
     init(profile: SavedProfile, template: CVTemplate) {
     init(profile: SavedProfile, template: CVTemplate) {
         self.profile = profile
         self.profile = profile
         self.template = template
         self.template = template
@@ -200,9 +212,35 @@ final class CVProfileDocumentView: NSView {
         wantsLayer = true
         wantsLayer = true
         layer?.backgroundColor = NSColor.clear.cgColor
         layer?.backgroundColor = NSColor.clear.cgColor
         userInterfaceLayoutDirection = .leftToRight
         userInterfaceLayoutDirection = .leftToRight
-        // Let the preview stack stretch us to the scroll view width; don’t shrink to label intrinsic widths.
         setContentHuggingPriority(.defaultLow, for: .horizontal)
         setContentHuggingPriority(.defaultLow, for: .horizontal)
+        installCardContent()
+        appearanceObserver = NotificationCenter.default.addObserver(
+            forName: AppAppearanceManager.didChangeNotification,
+            object: nil,
+            queue: .main
+        ) { [weak self] _ in
+            self?.refreshForAppearanceChange()
+        }
+    }
+
+    deinit {
+        if let appearanceObserver {
+            NotificationCenter.default.removeObserver(appearanceObserver)
+        }
+    }
+
+    override func viewDidChangeEffectiveAppearance() {
+        super.viewDidChangeEffectiveAppearance()
+        refreshForAppearanceChange()
+    }
 
 
+    private func refreshForAppearanceChange() {
+        style = DocumentStyle.make(for: template)
+        installCardContent()
+    }
+
+    private func installCardContent() {
+        subviews.forEach { $0.removeFromSuperview() }
         let card = NSView()
         let card = NSView()
         card.translatesAutoresizingMaskIntoConstraints = false
         card.translatesAutoresizingMaskIntoConstraints = false
         card.wantsLayer = true
         card.wantsLayer = true
@@ -211,6 +249,7 @@ final class CVProfileDocumentView: NSView {
         card.layer?.borderWidth = 1
         card.layer?.borderWidth = 1
         card.layer?.borderColor = style.rule.cgColor
         card.layer?.borderColor = style.rule.cgColor
         card.layer?.masksToBounds = true
         card.layer?.masksToBounds = true
+        cardView = card
 
 
         let root = buildRoot()
         let root = buildRoot()
         root.translatesAutoresizingMaskIntoConstraints = false
         root.translatesAutoresizingMaskIntoConstraints = false
@@ -278,8 +317,12 @@ final class CVProfileDocumentView: NSView {
             return buildModernFamilyDocument()
             return buildModernFamilyDocument()
         case .creative:
         case .creative:
             return buildCreativeFamilyDocument()
             return buildCreativeFamilyDocument()
-        case .professional, .minimal, .executive:
-            return buildTraditionalFamilyDocument()
+        case .executive:
+            return buildExecutiveDocument()
+        case .minimal:
+            return buildMinimalDocument()
+        case .professional:
+            return buildProfessionalDocument()
         }
         }
     }
     }
 
 
@@ -550,7 +593,7 @@ final class CVProfileDocumentView: NSView {
         let iv = NSImageView(image: img)
         let iv = NSImageView(image: img)
         iv.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
         iv.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
         iv.contentTintColor = theme
         iv.contentTintColor = theme
-        let t = label(title.uppercased(), font: style.sectionFont, color: style.sectionInk, maxLines: 1)
+        let t = label(title.uppercased(), font: style.sectionFont, color: style.ink, maxLines: 1)
         let r = NSStackView(views: [iv, t])
         let r = NSStackView(views: [iv, t])
         r.orientation = .horizontal
         r.orientation = .horizontal
         r.spacing = 8
         r.spacing = 8
@@ -762,9 +805,300 @@ final class CVProfileDocumentView: NSView {
         return stack
         return stack
     }
     }
 
 
-    // MARK: - Traditional families (professional / minimal / executive)
+    // MARK: - Executive (matches gallery serif layout)
+
+    private func buildExecutiveDocument() -> NSView {
+        let centeredHead = (variant % 2) == 0
+        let nameText = displayable(profile.personal.fullName, placeholder: "Your name")
+        let roleText = displayable(profile.personal.jobTitle, placeholder: "Professional headline")
+        let contactParts = [profile.personal.email, profile.personal.phone, profile.personal.address].filter {
+            !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+        }
+        let contactText = contactParts.isEmpty ? "Add contact details in your profile" : contactParts.joined(separator: " · ")
+
+        let name = label(nameText, font: style.nameFont, color: style.ink, maxLines: 2)
+        let role = label(roleText, font: style.roleFont, color: style.muted, maxLines: 2)
+        let contact = label(contactText, font: style.contactFont, color: style.muted.withAlphaComponent(0.9), maxLines: 3)
+        if centeredHead {
+            name.alignment = .center
+            role.alignment = .center
+            contact.alignment = .center
+        }
+
+        let rule = executiveHeaderRule(wide: variant % 3 == 0)
+        let head = NSStackView(views: [name, role, contact, rule])
+        head.orientation = .vertical
+        head.spacing = 8
+        head.alignment = centeredHead ? .centerX : .leading
+
+        let body: NSView
+        switch template.layout {
+        case .singleColumn:
+            body = executiveMainColumn(compact: false, tightLeading: variant % 5 == 2)
+        case .twoColumn(let side, let tinted):
+            let main = executiveMainColumn(compact: true, tightLeading: variant % 5 == 2)
+            let sideCol = executiveSidebarColumn(tinted: tinted, showMetrics: variant % 4 == 1)
+            let row = NSStackView()
+            row.orientation = .horizontal
+            row.spacing = 20
+            row.alignment = .top
+            if side == .leading {
+                row.addArrangedSubview(sideCol)
+                row.addArrangedSubview(main)
+            } else {
+                row.addArrangedSubview(main)
+                row.addArrangedSubview(sideCol)
+            }
+            let mult: CGFloat = (variant % 5 == 3) ? 0.38 : 0.33
+            sideCol.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
+            body = row
+        }
+
+        let wrap = NSStackView(views: [head, body])
+        wrap.orientation = .vertical
+        wrap.spacing = style.columnVerticalSpacing
+        wrap.alignment = .leading
+        return wrap
+    }
+
+    private func executiveHeaderRule(wide: Bool) -> NSView {
+        let theme = template.themeColor
+        let v = NSView()
+        v.translatesAutoresizingMaskIntoConstraints = false
+        v.wantsLayer = true
+        v.layer?.backgroundColor = theme.withAlphaComponent(0.45).cgColor
+        v.heightAnchor.constraint(equalToConstant: wide ? 2 : 1.5).isActive = true
+        v.widthAnchor.constraint(equalToConstant: wide ? 160 : 110).isActive = true
+        return v
+    }
+
+    private func executiveMainColumn(compact: Bool, tightLeading: Bool) -> NSView {
+        let stack = NSStackView()
+        stack.orientation = .vertical
+        stack.spacing = (compact ? style.bodyBlockSpacing : style.bodyBlockSpacing + 2) - (tightLeading ? 2 : 0)
+        stack.alignment = .leading
+
+        let summaryTitle = (variant % 6 == 3) ? "Summary" : "Professional Summary"
+        if let summary = nonEmpty(profile.careerSummary) {
+            stack.addArrangedSubview(sectionHeading(summaryTitle))
+            stack.addArrangedSubview(paragraph(summary, compact: compact))
+        }
+
+        let jobs = profile.workExperiences.filter { !$0.isEffectivelyEmpty }
+        if !jobs.isEmpty {
+            stack.addArrangedSubview(sectionHeading("Selected Experience"))
+            for job in jobs {
+                stack.addArrangedSubview(executiveExperienceBlock(job: job, compact: compact))
+            }
+        }
+
+        let schools = profile.educations.filter { !$0.isEffectivelyEmpty }
+        if !schools.isEmpty {
+            stack.addArrangedSubview(sectionHeading("Education"))
+            for edu in schools {
+                stack.addArrangedSubview(educationBlock(edu: edu, compact: compact))
+            }
+        }
+        appendCertificatesInterestsReferrals(to: stack, compact: compact)
+        return stack
+    }
+
+    private func executiveExperienceBlock(job: WorkExperiencePayload, compact: Bool) -> NSView {
+        let v = NSStackView()
+        v.orientation = .vertical
+        v.spacing = 4
+        v.alignment = .leading
+        let titleLine = [job.jobTitle, job.company].filter { !$0.isEmpty }.joined(separator: ", ")
+        if !titleLine.isEmpty {
+            v.addArrangedSubview(label(titleLine, font: style.expTitleFont, color: style.ink, maxLines: 0))
+        }
+        let duration = job.duration.trimmingCharacters(in: .whitespacesAndNewlines)
+        if !duration.isEmpty {
+            v.addArrangedSubview(label(duration, font: style.expMetaFont, color: template.themeColor, maxLines: 0))
+        }
+        for bullet in Self.bulletChunks(from: job.description) {
+            v.addArrangedSubview(label(bullet, font: style.bodyFont, color: style.muted, maxLines: 0))
+        }
+        return v
+    }
+
+    private func executiveSidebarColumn(tinted: Bool, showMetrics: Bool) -> NSView {
+        let stack = NSStackView()
+        stack.orientation = .vertical
+        stack.spacing = 10
+        stack.alignment = .leading
+        if tinted {
+            stack.wantsLayer = true
+            let fill = (variant % 3 == 0)
+                ? CVResumeAppearance.colors().sidebarTint
+                : template.themeColor.withAlphaComponent(0.07)
+            stack.layer?.backgroundColor = fill.cgColor
+            stack.layer?.cornerRadius = variant % 4 == 2 ? 8 : 6
+            stack.edgeInsets = NSEdgeInsets(top: 14, left: 14, bottom: 14, right: 14)
+        }
+
+        let skills = skillTokensFromProfile(max: 12)
+        if !skills.isEmpty {
+            stack.addArrangedSubview(sectionHeading("Core Competencies"))
+            for s in skills {
+                stack.addArrangedSubview(label("· \(s)", font: style.bodyFont, color: style.ink, maxLines: 0))
+            }
+        }
+
+        if let cert = nonEmpty(profile.certificates) {
+            stack.addArrangedSubview(sectionHeading("Tools"))
+            stack.addArrangedSubview(paragraph(cert, compact: true))
+        }
+
+        if showMetrics, let hi = highlightsBodyText() {
+            stack.addArrangedSubview(sectionHeading("Impact"))
+            stack.addArrangedSubview(paragraph(hi, compact: true))
+        }
+
+        if stack.arrangedSubviews.isEmpty {
+            stack.addArrangedSubview(sectionHeading("Contact"))
+            for line in contactLines() {
+                stack.addArrangedSubview(label(line, font: style.contactFont, color: style.ink, maxLines: 0))
+            }
+        }
+        return stack
+    }
+
+    // MARK: - Minimal (matches gallery light typography)
+
+    private func buildMinimalDocument() -> NSView {
+        let nameText = displayable(profile.personal.fullName, placeholder: "Your name")
+        let roleText = displayable(profile.personal.jobTitle, placeholder: "Professional headline")
+        let contactParts = [profile.personal.email, profile.personal.phone].filter {
+            !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+        }
+        let contactText = contactParts.isEmpty ? "Add contact in your profile" : contactParts.joined(separator: "   ")
+
+        let nameWeight: NSFont.Weight = (variant % 3 == 0) ? .ultraLight : .light
+        let nameSize: CGFloat = template.headline == .centered ? 22 + CGFloat(variant % 2) : 20 + CGFloat(variant % 3)
+        let name = label(nameText, font: .systemFont(ofSize: nameSize, weight: nameWeight), color: style.ink, maxLines: 2)
+        let role = label(roleText.uppercased(), font: .systemFont(ofSize: 12.5, weight: .medium), color: style.muted, maxLines: 2)
+        let contact = label(contactText, font: style.contactFont, color: style.muted.withAlphaComponent(0.75), maxLines: 2)
+
+        let head = NSStackView()
+        head.orientation = .vertical
+        head.spacing = template.headline == .avatarStacked ? 10 : 6 + CGFloat(variant % 4)
+        head.alignment = template.headline == .centered ? .centerX : .leading
+        if template.headline == .avatarStacked {
+            head.addArrangedSubview(initialsBadge(for: nameText))
+        }
+        if template.headline == .centered {
+            name.alignment = .center
+            role.alignment = .center
+            contact.alignment = .center
+        }
+        head.addArrangedSubview(name)
+        head.addArrangedSubview(role)
+        head.addArrangedSubview(contact)
+        head.addArrangedSubview(hairline())
+        if variant % 5 == 1 {
+            head.addArrangedSubview(hairline())
+        }
+
+        let swapEdu = (variant % 4) == 2
+        let body: NSView
+        switch template.layout {
+        case .singleColumn:
+            body = minimalMainColumn(spacing: style.bodyBlockSpacing, educationBeforeExperience: swapEdu)
+        case .twoColumn(let side, _):
+            let main = minimalMainColumn(spacing: style.bodyBlockSpacing - 2, educationBeforeExperience: swapEdu)
+            let aside = minimalSkillsAside(numbered: variant % 3 == 1)
+            let row = NSStackView()
+            row.orientation = .horizontal
+            row.spacing = 18 + CGFloat(variant % 3)
+            row.alignment = .top
+            if side == .leading {
+                row.addArrangedSubview(aside)
+                row.addArrangedSubview(main)
+            } else {
+                row.addArrangedSubview(main)
+                row.addArrangedSubview(aside)
+            }
+            let mult: CGFloat = (variant % 5 == 0) ? 0.34 : 0.30
+            aside.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
+            body = row
+        }
+
+        let wrap = NSStackView(views: [head, body])
+        wrap.orientation = .vertical
+        wrap.spacing = style.columnVerticalSpacing
+        wrap.alignment = .leading
+        return wrap
+    }
+
+    private func minimalMainColumn(spacing: CGFloat, educationBeforeExperience: Bool) -> NSView {
+        let stack = NSStackView()
+        stack.orientation = .vertical
+        stack.spacing = spacing
+        stack.alignment = .leading
+
+        let appendSummary: () -> Void = { [self] in
+            if let summary = nonEmpty(profile.careerSummary) {
+                stack.addArrangedSubview(sectionHeading("Profile"))
+                stack.addArrangedSubview(paragraph(summary, compact: false))
+            }
+        }
+        let appendExperience: () -> Void = { [self] in
+            let jobs = profile.workExperiences.filter { !$0.isEffectivelyEmpty }
+            if !jobs.isEmpty {
+                stack.addArrangedSubview(sectionHeading("Experience"))
+                for job in jobs {
+                    stack.addArrangedSubview(experienceBlock(job: job, compact: false))
+                }
+            }
+        }
+        let appendEducation: () -> Void = { [self] in
+            let schools = profile.educations.filter { !$0.isEffectivelyEmpty }
+            if !schools.isEmpty {
+                stack.addArrangedSubview(sectionHeading("Education"))
+                for edu in schools {
+                    stack.addArrangedSubview(educationBlock(edu: edu, compact: false))
+                }
+            }
+        }
+
+        if educationBeforeExperience {
+            appendEducation()
+            appendSummary()
+            appendExperience()
+        } else {
+            appendSummary()
+            appendExperience()
+            appendEducation()
+        }
+        appendCertificatesInterestsReferrals(to: stack, compact: false)
+        return stack
+    }
+
+    private func minimalSkillsAside(numbered: Bool) -> NSView {
+        let stack = NSStackView()
+        stack.orientation = .vertical
+        stack.spacing = 8
+        stack.alignment = .leading
+        let skills = skillTokensFromProfile(max: 12)
+        if skills.isEmpty {
+            stack.addArrangedSubview(sectionHeading("Contact"))
+            for line in contactLines() {
+                stack.addArrangedSubview(label(line, font: style.contactFont, color: style.muted, maxLines: 0))
+            }
+            return stack
+        }
+        stack.addArrangedSubview(sectionHeading("Skills"))
+        for (i, s) in skills.enumerated() {
+            let prefix = numbered ? "\(i + 1). " : "·  "
+            stack.addArrangedSubview(label("\(prefix)\(s)", font: style.bodyCompactFont, color: style.muted, maxLines: 0))
+        }
+        return stack
+    }
+
+    // MARK: - Professional
 
 
-    private func buildTraditionalFamilyDocument() -> NSView {
+    private func buildProfessionalDocument() -> NSView {
         switch template.layout {
         switch template.layout {
         case .singleColumn:
         case .singleColumn:
             return singleColumnLayout()
             return singleColumnLayout()
@@ -840,7 +1174,7 @@ final class CVProfileDocumentView: NSView {
         let row = NSStackView()
         let row = NSStackView()
         row.orientation = .horizontal
         row.orientation = .horizontal
         row.alignment = .top
         row.alignment = .top
-        row.spacing = template.family == .minimal ? 18 : 22
+        row.spacing = 22
 
 
         let sidebarCol = sidebarColumn(tinted: tinted)
         let sidebarCol = sidebarColumn(tinted: tinted)
         let mainCol = bodyColumn(compact: true, experienceFirst: professionalExperienceFirst)
         let mainCol = bodyColumn(compact: true, experienceFirst: professionalExperienceFirst)
@@ -852,12 +1186,7 @@ final class CVProfileDocumentView: NSView {
             row.addArrangedSubview(mainCol)
             row.addArrangedSubview(mainCol)
             row.addArrangedSubview(sidebarCol)
             row.addArrangedSubview(sidebarCol)
         }
         }
-        let sidebarMult: CGFloat
-        if template.family == .professional {
-            sidebarMult = (variant % 5 == 2) ? 0.38 : 0.32
-        } else {
-            sidebarMult = template.family == .executive ? 0.34 : 0.32
-        }
+        let sidebarMult: CGFloat = (variant % 5 == 2) ? 0.38 : 0.32
         sidebarCol.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: sidebarMult).isActive = true
         sidebarCol.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: sidebarMult).isActive = true
 
 
         v.addArrangedSubview(row)
         v.addArrangedSubview(row)
@@ -965,12 +1294,12 @@ final class CVProfileDocumentView: NSView {
             bar.heightAnchor.constraint(equalToConstant: 1).isActive = true
             bar.heightAnchor.constraint(equalToConstant: 1).isActive = true
             return bar
             return bar
         case .redUnderline:
         case .redUnderline:
-            bar.layer?.backgroundColor = NSColor(srgbRed: 207 / 255, green: 67 / 255, blue: 50 / 255, alpha: 1).cgColor
+            bar.layer?.backgroundColor = CVResumeAppearance.accentColor(for: template).cgColor
             bar.heightAnchor.constraint(equalToConstant: 2).isActive = true
             bar.heightAnchor.constraint(equalToConstant: 2).isActive = true
             bar.widthAnchor.constraint(equalToConstant: template.family == .minimal ? 140 : 168).isActive = true
             bar.widthAnchor.constraint(equalToConstant: template.family == .minimal ? 140 : 168).isActive = true
             return bar
             return bar
         case .redBar:
         case .redBar:
-            bar.layer?.backgroundColor = NSColor(srgbRed: 207 / 255, green: 67 / 255, blue: 50 / 255, alpha: 1).cgColor
+            bar.layer?.backgroundColor = CVResumeAppearance.accentColor(for: template).cgColor
             bar.heightAnchor.constraint(equalToConstant: 4).isActive = true
             bar.heightAnchor.constraint(equalToConstant: 4).isActive = true
             bar.widthAnchor.constraint(equalToConstant: 56).isActive = true
             bar.widthAnchor.constraint(equalToConstant: 56).isActive = true
             return bar
             return bar
@@ -1004,8 +1333,11 @@ final class CVProfileDocumentView: NSView {
         box.alignment = .leading
         box.alignment = .leading
         if tinted {
         if tinted {
             box.wantsLayer = true
             box.wantsLayer = true
-            box.layer?.backgroundColor = template.themeColor.withAlphaComponent(template.family == .creative ? 0.12 : 0.08).cgColor
-            box.layer?.cornerRadius = 8
+            let tint = (variant % 4 == 1)
+                ? template.themeColor.withAlphaComponent(0.08)
+                : CVResumeAppearance.colors().sidebarTint
+            box.layer?.backgroundColor = tint.cgColor
+            box.layer?.cornerRadius = variant % 3 == 0 ? 8 : 6
         }
         }
         box.edgeInsets = NSEdgeInsets(top: tinted ? 14 : 0, left: tinted ? 14 : 0, bottom: tinted ? 14 : 0, right: tinted ? 14 : 0)
         box.edgeInsets = NSEdgeInsets(top: tinted ? 14 : 0, left: tinted ? 14 : 0, bottom: tinted ? 14 : 0, right: tinted ? 14 : 0)
 
 
@@ -1014,8 +1346,24 @@ final class CVProfileDocumentView: NSView {
             box.addArrangedSubview(label(line, font: style.contactFont, color: style.ink, maxLines: 0))
             box.addArrangedSubview(label(line, font: style.contactFont, color: style.ink, maxLines: 0))
         }
         }
 
 
-        if let skillsBlock = ancillaryBlock(title: "Languages & more", body: combinedAncillaryText()) {
-            box.addArrangedSubview(skillsBlock)
+        let skills = skillTokensFromProfile(max: 8)
+        if !skills.isEmpty {
+            box.addArrangedSubview(sectionHeading("Skills"))
+            if variant % 5 == 2 {
+                box.addArrangedSubview(skillTagRow(theme: template.themeColor, maxTags: 5))
+            } else {
+                for token in skills {
+                    box.addArrangedSubview(label("·  \(token)", font: style.contactFont, color: style.ink, maxLines: 0))
+                }
+            }
+        }
+
+        if variant % 7 == 3, let tools = nonEmpty(profile.certificates) {
+            box.addArrangedSubview(sectionHeading("Tools"))
+            box.addArrangedSubview(paragraph(tools, compact: true))
+        } else if let ancillary = combinedAncillaryText(), !ancillary.isEmpty {
+            box.addArrangedSubview(sectionHeading("Languages & more"))
+            box.addArrangedSubview(paragraph(ancillary, compact: true))
         }
         }
 
 
         return box
         return box

+ 51 - 74
App for Indeed/Views/CVTemplateMiniPreview.swift

@@ -62,21 +62,8 @@ final class CVTemplatePreviewView: NSView {
     private let palette: CVTemplateCardPalette
     private let palette: CVTemplateCardPalette
     private let paper = NSView()
     private let paper = NSView()
 
 
-    /// Stable 0…11 from template identity so each row looks different (not just by `family`).
-    private var idVariant: Int {
-        var h: UInt64 = 1469598103934665603
-        let layoutDesc: String
-        switch template.layout {
-        case .singleColumn: layoutDesc = "1col"
-        case .twoColumn(let s, let t): layoutDesc = "2col_\(s)_\(t)"
-        }
-        let blob = "\(template.id)|\(template.family.rawValue)|\(template.headline)|\(template.accent)|\(layoutDesc)|\(template.sectionLabelStyle)"
-        for b in blob.utf8 {
-            h ^= UInt64(b)
-            h &*= 1_099_511_628_211
-        }
-        return Int(h % 12)
-    }
+    /// Same variant index as `CVProfileDocumentView` (shared `CVTemplate.galleryLayoutVariant`).
+    private var layoutVariant: Int { template.galleryLayoutVariant }
 
 
     init(template: CVTemplate, palette: CVTemplateCardPalette) {
     init(template: CVTemplate, palette: CVTemplateCardPalette) {
         self.template = template
         self.template = template
@@ -133,29 +120,23 @@ final class CVTemplatePreviewView: NSView {
     }
     }
 
 
     private func paperBackgroundColor() -> NSColor {
     private func paperBackgroundColor() -> NSColor {
-        switch idVariant % 5 {
-        case 0: return palette.previewPaper
-        case 1: return NSColor(srgbRed: 0.995, green: 0.992, blue: 0.985, alpha: 1)
-        case 2: return NSColor(srgbRed: 0.96, green: 0.99, blue: 1, alpha: 1)
-        case 3: return NSColor(srgbRed: 0.99, green: 0.99, blue: 0.99, alpha: 1)
-        default: return NSColor(srgbRed: 0.99, green: 0.98, blue: 0.995, alpha: 1)
-        }
+        CVResumeAppearance.paperBackground(variant: layoutVariant, base: palette.previewPaper)
     }
     }
 
 
     // MARK: - Family: Professional (ATS-friendly)
     // MARK: - Family: Professional (ATS-friendly)
 
 
     private func buildProfessionalResume() -> NSView {
     private func buildProfessionalResume() -> NSView {
-        let swapExpFirst = (idVariant % 3) == 1
-        let sidebarMult: CGFloat = (idVariant % 5 == 2) ? 0.38 : 0.34
+        let swapExpFirst = (layoutVariant % 3) == 1
+        let sidebarMult: CGFloat = (layoutVariant % 5 == 2) ? 0.38 : 0.34
 
 
         switch template.layout {
         switch template.layout {
         case .singleColumn:
         case .singleColumn:
             let v = NSStackView()
             let v = NSStackView()
             v.orientation = .vertical
             v.orientation = .vertical
-            v.spacing = 4 + CGFloat(idVariant % 3)
+            v.spacing = 4 + CGFloat(layoutVariant % 3)
             v.alignment = .leading
             v.alignment = .leading
             v.addArrangedSubview(proHeaderBlock())
             v.addArrangedSubview(proHeaderBlock())
-            if (idVariant % 6) == 4 {
+            if (layoutVariant % 6) == 4 {
                 v.addArrangedSubview(proInlineSkillsRow())
                 v.addArrangedSubview(proInlineSkillsRow())
             }
             }
             v.addArrangedSubview(hairline())
             v.addArrangedSubview(hairline())
@@ -169,9 +150,9 @@ final class CVTemplatePreviewView: NSView {
             let rule = hairline()
             let rule = hairline()
             let row = NSStackView()
             let row = NSStackView()
             row.orientation = .horizontal
             row.orientation = .horizontal
-            row.spacing = 5 + CGFloat(idVariant % 3)
+            row.spacing = 5 + CGFloat(layoutVariant % 3)
             row.alignment = .top
             row.alignment = .top
-            let sidebar = proSidebarColumn(tinted: tinted, variant: idVariant)
+            let sidebar = proSidebarColumn(tinted: tinted, variant: layoutVariant)
             let main = proMainColumn(compact: true, experienceFirst: swapExpFirst)
             let main = proMainColumn(compact: true, experienceFirst: swapExpFirst)
             if side == .leading {
             if side == .leading {
                 row.addArrangedSubview(sidebar)
                 row.addArrangedSubview(sidebar)
@@ -315,7 +296,7 @@ final class CVTemplatePreviewView: NSView {
     private func proMainColumn(compact: Bool, experienceFirst: Bool) -> NSView {
     private func proMainColumn(compact: Bool, experienceFirst: Bool) -> NSView {
         let stack = NSStackView()
         let stack = NSStackView()
         stack.orientation = .vertical
         stack.orientation = .vertical
-        stack.spacing = compact ? 4 : 5 + CGFloat(idVariant % 2)
+        stack.spacing = compact ? 4 : 5 + CGFloat(layoutVariant % 2)
         stack.alignment = .leading
         stack.alignment = .leading
         let sp: CGFloat = compact ? 6.2 : 6.5
         let sp: CGFloat = compact ? 6.2 : 6.5
 
 
@@ -348,7 +329,7 @@ final class CVTemplatePreviewView: NSView {
     // MARK: - Family: Modern (three distinct silhouettes per id)
     // MARK: - Family: Modern (three distinct silhouettes per id)
 
 
     private func buildModernResume() -> NSView {
     private func buildModernResume() -> NSView {
-        switch idVariant % 3 {
+        switch layoutVariant % 3 {
         case 0: return buildModernClassicBandLayout()
         case 0: return buildModernClassicBandLayout()
         case 1: return buildModernRailDocLayout()
         case 1: return buildModernRailDocLayout()
         default: return buildModernSplitHeaderLayout()
         default: return buildModernSplitHeaderLayout()
@@ -364,7 +345,7 @@ final class CVTemplatePreviewView: NSView {
             let sideCol = modernSidebar(theme: theme, tinted: tinted)
             let sideCol = modernSidebar(theme: theme, tinted: tinted)
             let row = NSStackView()
             let row = NSStackView()
             row.orientation = .horizontal
             row.orientation = .horizontal
-            row.spacing = 5 + CGFloat(idVariant % 3)
+            row.spacing = 5 + CGFloat(layoutVariant % 3)
             row.alignment = .top
             row.alignment = .top
             if side == .leading {
             if side == .leading {
                 row.addArrangedSubview(sideCol)
                 row.addArrangedSubview(sideCol)
@@ -373,7 +354,7 @@ final class CVTemplatePreviewView: NSView {
                 row.addArrangedSubview(main)
                 row.addArrangedSubview(main)
                 row.addArrangedSubview(sideCol)
                 row.addArrangedSubview(sideCol)
             }
             }
-            let mult: CGFloat = (idVariant % 4 == 2) ? 0.36 : 0.32
+            let mult: CGFloat = (layoutVariant % 4 == 2) ? 0.36 : 0.32
             sideCol.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
             sideCol.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
             return row
             return row
         }
         }
@@ -385,7 +366,7 @@ final class CVTemplatePreviewView: NSView {
         header.translatesAutoresizingMaskIntoConstraints = false
         header.translatesAutoresizingMaskIntoConstraints = false
         header.wantsLayer = true
         header.wantsLayer = true
         header.layer?.backgroundColor = theme.cgColor
         header.layer?.backgroundColor = theme.cgColor
-        header.layer?.cornerRadius = idVariant % 2 == 0 ? 5 : 3
+        header.layer?.cornerRadius = layoutVariant % 2 == 0 ? 5 : 3
 
 
         let white = NSColor.white
         let white = NSColor.white
         let name = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 9, weight: .bold), color: white, alignment: .left, maxLines: 1)
         let name = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 9, weight: .bold), color: white, alignment: .left, maxLines: 1)
@@ -423,13 +404,13 @@ final class CVTemplatePreviewView: NSView {
             topRow.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -7),
             topRow.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -7),
             topRow.topAnchor.constraint(equalTo: header.topAnchor, constant: 6),
             topRow.topAnchor.constraint(equalTo: header.topAnchor, constant: 6),
             topRow.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -6),
             topRow.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -6),
-            header.heightAnchor.constraint(greaterThanOrEqualToConstant: 32 + CGFloat(idVariant % 3) * 2)
+            header.heightAnchor.constraint(greaterThanOrEqualToConstant: 32 + CGFloat(layoutVariant % 3) * 2)
         ])
         ])
 
 
         let body = modernPrimaryBody(theme: theme)
         let body = modernPrimaryBody(theme: theme)
         let wrap = NSStackView(views: [header, body])
         let wrap = NSStackView(views: [header, body])
         wrap.orientation = .vertical
         wrap.orientation = .vertical
-        wrap.spacing = 6 + CGFloat(idVariant % 2)
+        wrap.spacing = 6 + CGFloat(layoutVariant % 2)
         wrap.alignment = .leading
         wrap.alignment = .leading
         return wrap
         return wrap
     }
     }
@@ -443,7 +424,7 @@ final class CVTemplatePreviewView: NSView {
         rail.wantsLayer = true
         rail.wantsLayer = true
         rail.layer?.backgroundColor = theme.cgColor
         rail.layer?.backgroundColor = theme.cgColor
         rail.layer?.cornerRadius = 2
         rail.layer?.cornerRadius = 2
-        rail.widthAnchor.constraint(equalToConstant: 3 + CGFloat(idVariant % 2)).isActive = true
+        rail.widthAnchor.constraint(equalToConstant: 3 + CGFloat(layoutVariant % 2)).isActive = true
 
 
         let inner = NSStackView()
         let inner = NSStackView()
         inner.orientation = .vertical
         inner.orientation = .vertical
@@ -578,18 +559,18 @@ final class CVTemplatePreviewView: NSView {
     private func buildMinimalResume() -> NSView {
     private func buildMinimalResume() -> NSView {
         let ink = palette.previewInk
         let ink = palette.previewInk
         let muted = palette.previewMuted
         let muted = palette.previewMuted
-        let nameWeight: NSFont.Weight = (idVariant % 3 == 0) ? .ultraLight : .light
-        let nameSize: CGFloat = template.headline == .centered ? 11 + CGFloat(idVariant % 2) : 9.5 + CGFloat(idVariant % 3)
+        let nameWeight: NSFont.Weight = (layoutVariant % 3 == 0) ? .ultraLight : .light
+        let nameSize: CGFloat = template.headline == .centered ? 11 + CGFloat(layoutVariant % 2) : 9.5 + CGFloat(layoutVariant % 3)
         let name = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: nameSize, weight: nameWeight), color: ink, alignment: .left, maxLines: 1)
         let name = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: nameSize, weight: nameWeight), color: ink, alignment: .left, maxLines: 1)
         let role = makeLabel(CVPreviewDemoContent.title.uppercased(), font: .systemFont(ofSize: 6.5, weight: .medium), color: muted, alignment: .left, maxLines: 1)
         let role = makeLabel(CVPreviewDemoContent.title.uppercased(), font: .systemFont(ofSize: 6.5, weight: .medium), color: muted, alignment: .left, maxLines: 1)
         let contact = makeLabel("\(CVPreviewDemoContent.email)   \(CVPreviewDemoContent.phone)", font: .systemFont(ofSize: 5.8, weight: .regular), color: muted.withAlphaComponent(0.75), alignment: .left, maxLines: 1)
         let contact = makeLabel("\(CVPreviewDemoContent.email)   \(CVPreviewDemoContent.phone)", font: .systemFont(ofSize: 5.8, weight: .regular), color: muted.withAlphaComponent(0.75), alignment: .left, maxLines: 1)
 
 
         let head = NSStackView()
         let head = NSStackView()
         head.orientation = .vertical
         head.orientation = .vertical
-        head.spacing = template.headline == .avatarStacked ? 8 : 4 + CGFloat(idVariant % 4)
+        head.spacing = template.headline == .avatarStacked ? 8 : 4 + CGFloat(layoutVariant % 4)
         head.alignment = template.headline == .centered ? .centerX : .leading
         head.alignment = template.headline == .centered ? .centerX : .leading
         if template.headline == .avatarStacked {
         if template.headline == .avatarStacked {
-            head.addArrangedSubview(initialsAvatar(diameter: 24 + CGFloat(idVariant % 2) * 2, ink: ink))
+            head.addArrangedSubview(initialsAvatar(diameter: 24 + CGFloat(layoutVariant % 2) * 2, ink: ink))
         }
         }
         if template.headline == .centered {
         if template.headline == .centered {
             name.alignment = .center
             name.alignment = .center
@@ -600,21 +581,21 @@ final class CVTemplatePreviewView: NSView {
         head.addArrangedSubview(role)
         head.addArrangedSubview(role)
         head.addArrangedSubview(contact)
         head.addArrangedSubview(contact)
         head.addArrangedSubview(hairlineSoft())
         head.addArrangedSubview(hairlineSoft())
-        if idVariant % 5 == 1 {
+        if layoutVariant % 5 == 1 {
             head.addArrangedSubview(hairlineSoft())
             head.addArrangedSubview(hairlineSoft())
         }
         }
 
 
-        let swapEdu = (idVariant % 4) == 2
+        let swapEdu = (layoutVariant % 4) == 2
         let body: NSView
         let body: NSView
         switch template.layout {
         switch template.layout {
         case .singleColumn:
         case .singleColumn:
-            body = minimalBody(spacing: 6 + CGFloat(idVariant % 3), educationBeforeExperience: swapEdu)
+            body = minimalBody(spacing: 6 + CGFloat(layoutVariant % 3), educationBeforeExperience: swapEdu)
         case .twoColumn(let side, _):
         case .twoColumn(let side, _):
             let a = minimalBody(spacing: 5, educationBeforeExperience: swapEdu)
             let a = minimalBody(spacing: 5, educationBeforeExperience: swapEdu)
-            let b = minimalAside(numbered: idVariant % 3 == 1)
+            let b = minimalAside(numbered: layoutVariant % 3 == 1)
             let row = NSStackView()
             let row = NSStackView()
             row.orientation = .horizontal
             row.orientation = .horizontal
-            row.spacing = 8 + CGFloat(idVariant % 3)
+            row.spacing = 8 + CGFloat(layoutVariant % 3)
             row.alignment = .top
             row.alignment = .top
             if side == .leading {
             if side == .leading {
                 row.addArrangedSubview(b)
                 row.addArrangedSubview(b)
@@ -623,14 +604,14 @@ final class CVTemplatePreviewView: NSView {
                 row.addArrangedSubview(a)
                 row.addArrangedSubview(a)
                 row.addArrangedSubview(b)
                 row.addArrangedSubview(b)
             }
             }
-            let mult: CGFloat = (idVariant % 5 == 0) ? 0.34 : 0.3
+            let mult: CGFloat = (layoutVariant % 5 == 0) ? 0.34 : 0.3
             b.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
             b.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
             body = row
             body = row
         }
         }
 
 
         let wrap = NSStackView(views: [head, body])
         let wrap = NSStackView(views: [head, body])
         wrap.orientation = .vertical
         wrap.orientation = .vertical
-        wrap.spacing = 7 + CGFloat(idVariant % 2)
+        wrap.spacing = 7 + CGFloat(layoutVariant % 2)
         wrap.alignment = .leading
         wrap.alignment = .leading
         return wrap
         return wrap
     }
     }
@@ -690,13 +671,13 @@ final class CVTemplatePreviewView: NSView {
         let ink = palette.previewInk
         let ink = palette.previewInk
         let muted = palette.previewMuted
         let muted = palette.previewMuted
         let theme = template.themeColor
         let theme = template.themeColor
-        let centeredHead = (idVariant % 2) == 0
+        let centeredHead = (layoutVariant % 2) == 0
 
 
         let name = makeLabel(CVPreviewDemoContent.fullName, font: serifBold, color: ink, alignment: centeredHead ? .center : .left, maxLines: 1)
         let name = makeLabel(CVPreviewDemoContent.fullName, font: serifBold, color: ink, alignment: centeredHead ? .center : .left, maxLines: 1)
         let role = makeLabel(CVPreviewDemoContent.title, font: serif, color: muted, alignment: centeredHead ? .center : .left, maxLines: 1)
         let role = makeLabel(CVPreviewDemoContent.title, font: serif, color: muted, alignment: centeredHead ? .center : .left, maxLines: 1)
         let contact = makeLabel("\(CVPreviewDemoContent.email) · \(CVPreviewDemoContent.phone) · \(CVPreviewDemoContent.location)", font: NSFont(name: "Georgia", size: 5.8) ?? serif, color: muted.withAlphaComponent(0.9), alignment: centeredHead ? .center : .left, maxLines: 2)
         let contact = makeLabel("\(CVPreviewDemoContent.email) · \(CVPreviewDemoContent.phone) · \(CVPreviewDemoContent.location)", font: NSFont(name: "Georgia", size: 5.8) ?? serif, color: muted.withAlphaComponent(0.9), alignment: centeredHead ? .center : .left, maxLines: 2)
 
 
-        let rule = executiveRule(theme: theme, wide: idVariant % 3 == 0)
+        let rule = executiveRule(theme: theme, wide: layoutVariant % 3 == 0)
         let head = NSStackView(views: [name, role, contact, rule])
         let head = NSStackView(views: [name, role, contact, rule])
         head.orientation = .vertical
         head.orientation = .vertical
         head.spacing = 4
         head.spacing = 4
@@ -705,13 +686,13 @@ final class CVTemplatePreviewView: NSView {
         let body: NSView
         let body: NSView
         switch template.layout {
         switch template.layout {
         case .singleColumn:
         case .singleColumn:
-            body = executiveBody(serif: serif, ink: ink, muted: muted, theme: theme, compact: false, tightLeading: idVariant % 5 == 2)
+            body = executiveBody(serif: serif, ink: ink, muted: muted, theme: theme, compact: false, tightLeading: layoutVariant % 5 == 2)
         case .twoColumn(let side, let tinted):
         case .twoColumn(let side, let tinted):
-            let main = executiveBody(serif: serif, ink: ink, muted: muted, theme: theme, compact: true, tightLeading: idVariant % 5 == 2)
-            let sideC = executiveSidebar(serif: serif, tinted: tinted, showMetrics: idVariant % 4 == 1)
+            let main = executiveBody(serif: serif, ink: ink, muted: muted, theme: theme, compact: true, tightLeading: layoutVariant % 5 == 2)
+            let sideC = executiveSidebar(serif: serif, tinted: tinted, showMetrics: layoutVariant % 4 == 1)
             let row = NSStackView()
             let row = NSStackView()
             row.orientation = .horizontal
             row.orientation = .horizontal
-            row.spacing = 6 + CGFloat(idVariant % 3)
+            row.spacing = 6 + CGFloat(layoutVariant % 3)
             row.alignment = .top
             row.alignment = .top
             if side == .leading {
             if side == .leading {
                 row.addArrangedSubview(sideC)
                 row.addArrangedSubview(sideC)
@@ -720,14 +701,14 @@ final class CVTemplatePreviewView: NSView {
                 row.addArrangedSubview(main)
                 row.addArrangedSubview(main)
                 row.addArrangedSubview(sideC)
                 row.addArrangedSubview(sideC)
             }
             }
-            let mult: CGFloat = (idVariant % 5 == 3) ? 0.38 : 0.33
+            let mult: CGFloat = (layoutVariant % 5 == 3) ? 0.38 : 0.33
             sideC.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
             sideC.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
             body = row
             body = row
         }
         }
 
 
         let wrap = NSStackView(views: [head, body])
         let wrap = NSStackView(views: [head, body])
         wrap.orientation = .vertical
         wrap.orientation = .vertical
-        wrap.spacing = 6 + CGFloat(idVariant % 2)
+        wrap.spacing = 6 + CGFloat(layoutVariant % 2)
         wrap.alignment = .leading
         wrap.alignment = .leading
         return wrap
         return wrap
     }
     }
@@ -747,7 +728,7 @@ final class CVTemplatePreviewView: NSView {
         stack.orientation = .vertical
         stack.orientation = .vertical
         stack.spacing = (compact ? 4 : 5) - (tightLeading ? 1 : 0)
         stack.spacing = (compact ? 4 : 5) - (tightLeading ? 1 : 0)
         stack.alignment = .leading
         stack.alignment = .leading
-        let sumTitle = (idVariant % 6 == 3) ? "SUMMARY" : "PROFESSIONAL SUMMARY"
+        let sumTitle = (layoutVariant % 6 == 3) ? "SUMMARY" : "PROFESSIONAL SUMMARY"
         stack.addArrangedSubview(sectionHeading(sumTitle))
         stack.addArrangedSubview(sectionHeading(sumTitle))
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: serif, color: ink, alignment: .left, maxLines: 3))
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: serif, color: ink, alignment: .left, maxLines: 3))
         stack.addArrangedSubview(sectionHeading("SELECTED EXPERIENCE"))
         stack.addArrangedSubview(sectionHeading("SELECTED EXPERIENCE"))
@@ -768,11 +749,11 @@ final class CVTemplatePreviewView: NSView {
         stack.alignment = .leading
         stack.alignment = .leading
         if tinted {
         if tinted {
             stack.wantsLayer = true
             stack.wantsLayer = true
-            let fill = (idVariant % 3 == 0)
+            let fill = (layoutVariant % 3 == 0)
                 ? NSColor(srgbRed: 0.97, green: 0.97, blue: 0.98, alpha: 1)
                 ? NSColor(srgbRed: 0.97, green: 0.97, blue: 0.98, alpha: 1)
                 : template.themeColor.withAlphaComponent(0.07)
                 : template.themeColor.withAlphaComponent(0.07)
             stack.layer?.backgroundColor = fill.cgColor
             stack.layer?.backgroundColor = fill.cgColor
-            stack.layer?.cornerRadius = idVariant % 4 == 2 ? 5 : 3
+            stack.layer?.cornerRadius = layoutVariant % 4 == 2 ? 5 : 3
             stack.edgeInsets = NSEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
             stack.edgeInsets = NSEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
         }
         }
         stack.addArrangedSubview(sectionHeading("CORE COMPETENCIES"))
         stack.addArrangedSubview(sectionHeading("CORE COMPETENCIES"))
@@ -794,18 +775,18 @@ final class CVTemplatePreviewView: NSView {
         let theme = template.themeColor
         let theme = template.themeColor
         let deep = creativeDeepBackground(theme: theme)
         let deep = creativeDeepBackground(theme: theme)
         let onSidebar = NSColor.white.withAlphaComponent(0.95)
         let onSidebar = NSColor.white.withAlphaComponent(0.95)
-        let skillPrefix = (idVariant % 3 == 0) ? "•  " : "▸  "
+        let skillPrefix = (layoutVariant % 3 == 0) ? "•  " : "▸  "
 
 
         let sidebar = NSStackView()
         let sidebar = NSStackView()
         sidebar.orientation = .vertical
         sidebar.orientation = .vertical
-        sidebar.spacing = 4 + CGFloat(idVariant % 3)
+        sidebar.spacing = 4 + CGFloat(layoutVariant % 3)
         sidebar.alignment = .leading
         sidebar.alignment = .leading
         sidebar.wantsLayer = true
         sidebar.wantsLayer = true
         sidebar.layer?.backgroundColor = deep.cgColor
         sidebar.layer?.backgroundColor = deep.cgColor
-        sidebar.layer?.cornerRadius = idVariant % 2 == 0 ? 6 : 4
+        sidebar.layer?.cornerRadius = layoutVariant % 2 == 0 ? 6 : 4
         sidebar.edgeInsets = NSEdgeInsets(top: 6, left: 6, bottom: 6, right: 6)
         sidebar.edgeInsets = NSEdgeInsets(top: 6, left: 6, bottom: 6, right: 6)
 
 
-        let sbTitle = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 7.5 + CGFloat(idVariant % 2), weight: .bold), color: onSidebar, alignment: .left, maxLines: 2)
+        let sbTitle = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 7.5 + CGFloat(layoutVariant % 2), weight: .bold), color: onSidebar, alignment: .left, maxLines: 2)
         sidebar.addArrangedSubview(sbTitle)
         sidebar.addArrangedSubview(sbTitle)
         sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 6.5, weight: .medium), color: onSidebar.withAlphaComponent(0.85), alignment: .left, maxLines: 2))
         sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 6.5, weight: .medium), color: onSidebar.withAlphaComponent(0.85), alignment: .left, maxLines: 2))
         sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 5.8), color: onSidebar.withAlphaComponent(0.8), alignment: .left, maxLines: 1))
         sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 5.8), color: onSidebar.withAlphaComponent(0.8), alignment: .left, maxLines: 1))
@@ -817,20 +798,20 @@ final class CVTemplatePreviewView: NSView {
 
 
         let main = NSStackView()
         let main = NSStackView()
         main.orientation = .vertical
         main.orientation = .vertical
-        main.spacing = 4 + CGFloat(idVariant % 3)
+        main.spacing = 4 + CGFloat(layoutVariant % 3)
         main.alignment = .leading
         main.alignment = .leading
         main.addArrangedSubview(creativeMainHeader(theme: theme))
         main.addArrangedSubview(creativeMainHeader(theme: theme))
         main.addArrangedSubview(sectionHeading("PROFILE"))
         main.addArrangedSubview(sectionHeading("PROFILE"))
         main.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 3))
         main.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 3))
         main.addArrangedSubview(sectionHeading("IMPACT"))
         main.addArrangedSubview(sectionHeading("IMPACT"))
         main.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.company) — \(CVPreviewDemoContent.experienceRole)", font: .systemFont(ofSize: 6.6, weight: .heavy), color: palette.previewInk, alignment: .left, maxLines: 2))
         main.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.company) — \(CVPreviewDemoContent.experienceRole)", font: .systemFont(ofSize: 6.6, weight: .heavy), color: palette.previewInk, alignment: .left, maxLines: 2))
-        let bMark = (idVariant % 2 == 0) ? "—  " : "▸  "
+        let bMark = (layoutVariant % 2 == 0) ? "—  " : "▸  "
         main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet1)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
         main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet1)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
         main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet2)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
         main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet2)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
         main.addArrangedSubview(sectionHeading("EDUCATION"))
         main.addArrangedSubview(sectionHeading("EDUCATION"))
         main.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.university) · \(CVPreviewDemoContent.degree) · \(CVPreviewDemoContent.educationYears)", font: .systemFont(ofSize: 6.1), color: palette.previewInk, alignment: .left, maxLines: 2))
         main.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.university) · \(CVPreviewDemoContent.degree) · \(CVPreviewDemoContent.educationYears)", font: .systemFont(ofSize: 6.1), color: palette.previewInk, alignment: .left, maxLines: 2))
 
 
-        let sidebarMult = 0.32 + CGFloat(idVariant % 3) * 0.02
+        let sidebarMult = 0.32 + CGFloat(layoutVariant % 3) * 0.02
 
 
         switch template.layout {
         switch template.layout {
         case .singleColumn:
         case .singleColumn:
@@ -838,14 +819,14 @@ final class CVTemplatePreviewView: NSView {
             banner.translatesAutoresizingMaskIntoConstraints = false
             banner.translatesAutoresizingMaskIntoConstraints = false
             banner.wantsLayer = true
             banner.wantsLayer = true
             banner.layer?.backgroundColor = theme.cgColor
             banner.layer?.backgroundColor = theme.cgColor
-            banner.layer?.cornerRadius = idVariant % 4 == 1 ? 6 : 3
+            banner.layer?.cornerRadius = layoutVariant % 4 == 1 ? 6 : 3
             let inner = makeLabel("  \(CVPreviewDemoContent.fullName)  ·  \(CVPreviewDemoContent.title)", font: .systemFont(ofSize: 6.5, weight: .bold), color: .white, alignment: .left, maxLines: 1)
             let inner = makeLabel("  \(CVPreviewDemoContent.fullName)  ·  \(CVPreviewDemoContent.title)", font: .systemFont(ofSize: 6.5, weight: .bold), color: .white, alignment: .left, maxLines: 1)
             inner.translatesAutoresizingMaskIntoConstraints = false
             inner.translatesAutoresizingMaskIntoConstraints = false
             banner.addSubview(inner)
             banner.addSubview(inner)
             NSLayoutConstraint.activate([
             NSLayoutConstraint.activate([
                 inner.leadingAnchor.constraint(equalTo: banner.leadingAnchor, constant: 5),
                 inner.leadingAnchor.constraint(equalTo: banner.leadingAnchor, constant: 5),
                 inner.trailingAnchor.constraint(lessThanOrEqualTo: banner.trailingAnchor, constant: -5),
                 inner.trailingAnchor.constraint(lessThanOrEqualTo: banner.trailingAnchor, constant: -5),
-                inner.topAnchor.constraint(equalTo: banner.topAnchor, constant: 4 + CGFloat(idVariant % 2)),
+                inner.topAnchor.constraint(equalTo: banner.topAnchor, constant: 4 + CGFloat(layoutVariant % 2)),
                 inner.bottomAnchor.constraint(equalTo: banner.bottomAnchor, constant: -4)
                 inner.bottomAnchor.constraint(equalTo: banner.bottomAnchor, constant: -4)
             ])
             ])
             let col = NSStackView(views: [banner, main])
             let col = NSStackView(views: [banner, main])
@@ -856,7 +837,7 @@ final class CVTemplatePreviewView: NSView {
         case .twoColumn(let side, _):
         case .twoColumn(let side, _):
             let row = NSStackView()
             let row = NSStackView()
             row.orientation = .horizontal
             row.orientation = .horizontal
-            row.spacing = 5 + CGFloat(idVariant % 3)
+            row.spacing = 5 + CGFloat(layoutVariant % 3)
             row.alignment = .top
             row.alignment = .top
             if side == .leading {
             if side == .leading {
                 row.addArrangedSubview(sidebar)
                 row.addArrangedSubview(sidebar)
@@ -873,7 +854,7 @@ final class CVTemplatePreviewView: NSView {
     private func creativeDeepBackground(theme: NSColor) -> NSColor {
     private func creativeDeepBackground(theme: NSColor) -> NSColor {
         let navy = NSColor(srgbRed: 0.08, green: 0.1, blue: 0.18, alpha: 1)
         let navy = NSColor(srgbRed: 0.08, green: 0.1, blue: 0.18, alpha: 1)
         let plum = NSColor(srgbRed: 0.14, green: 0.07, blue: 0.24, alpha: 1)
         let plum = NSColor(srgbRed: 0.14, green: 0.07, blue: 0.24, alpha: 1)
-        switch idVariant % 4 {
+        switch layoutVariant % 4 {
         case 0: return theme.blended(withFraction: 0.52, of: navy) ?? theme
         case 0: return theme.blended(withFraction: 0.52, of: navy) ?? theme
         case 1: return theme.blended(withFraction: 0.7, of: NSColor.black) ?? theme
         case 1: return theme.blended(withFraction: 0.7, of: NSColor.black) ?? theme
         case 2: return palette.previewInk.blended(withFraction: 0.38, of: theme) ?? theme
         case 2: return palette.previewInk.blended(withFraction: 0.38, of: theme) ?? theme
@@ -944,11 +925,7 @@ final class CVTemplatePreviewView: NSView {
     }
     }
 
 
     private func accentDecorationColor() -> NSColor {
     private func accentDecorationColor() -> NSColor {
-        switch template.accent {
-        case .redUnderline, .redBar: return palette.previewAccentRed
-        case .blueBar: return palette.previewAccentBlue
-        case .none: return template.themeColor.blended(withFraction: 0.5, of: palette.previewInk) ?? palette.previewInk
-        }
+        CVResumeAppearance.accentColor(for: template)
     }
     }
 
 
     private func headlineAccent(theme: NSColor, width: CGFloat) -> NSView {
     private func headlineAccent(theme: NSColor, width: CGFloat) -> NSView {