Просмотр исходного кода

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 недель назад: 2
Родитель
Сommit
4ef6f513d9

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

@@ -0,0 +1,80 @@
1
+//
2
+//  CVResumeAppearance.swift
3
+//  App for Indeed
4
+//
5
+//  Shared résumé colours for gallery thumbnails and filled CV preview/export.
6
+//  Tracks app light / dark mode so the selected template looks the same everywhere.
7
+//
8
+
9
+import AppKit
10
+
11
+@MainActor
12
+enum CVResumeAppearance {
13
+
14
+    struct Colors {
15
+        let paper: NSColor
16
+        let ink: NSColor
17
+        let muted: NSColor
18
+        let rule: NSColor
19
+        let cardBackground: NSColor
20
+        let sidebarTint: NSColor
21
+        let accentRed: NSColor
22
+        let accentBlue: NSColor
23
+    }
24
+
25
+    static var isDark: Bool { AppAppearanceManager.shared.isDark }
26
+
27
+    static func colors(isDark dark: Bool? = nil) -> Colors {
28
+        let dark = dark ?? isDark
29
+        if dark {
30
+            return Colors(
31
+                paper: NSColor(srgbRed: 28 / 255, green: 30 / 255, blue: 36 / 255, alpha: 1),
32
+                ink: NSColor(srgbRed: 0.94, green: 0.95, blue: 0.97, alpha: 1),
33
+                muted: NSColor(srgbRed: 0.62, green: 0.66, blue: 0.72, alpha: 1),
34
+                rule: NSColor(srgbRed: 0.38, green: 0.40, blue: 0.46, alpha: 1),
35
+                cardBackground: NSColor(srgbRed: 32 / 255, green: 34 / 255, blue: 40 / 255, alpha: 1),
36
+                sidebarTint: NSColor(srgbRed: 40 / 255, green: 42 / 255, blue: 48 / 255, alpha: 1),
37
+                accentRed: NSColor(srgbRed: 235 / 255, green: 88 / 255, blue: 72 / 255, alpha: 1),
38
+                accentBlue: AppDashboardTheme.brandBlue
39
+            )
40
+        }
41
+        return Colors(
42
+            paper: NSColor.white,
43
+            ink: NSColor(srgbRed: 38 / 255, green: 50 / 255, blue: 71 / 255, alpha: 1),
44
+            muted: NSColor(srgbRed: 110 / 255, green: 118 / 255, blue: 132 / 255, alpha: 1),
45
+            rule: NSColor(srgbRed: 228 / 255, green: 232 / 255, blue: 240 / 255, alpha: 1),
46
+            cardBackground: NSColor.white,
47
+            sidebarTint: AppDashboardTheme.cvMakerPreviewSidebarTint,
48
+            accentRed: NSColor(srgbRed: 207 / 255, green: 67 / 255, blue: 50 / 255, alpha: 1),
49
+            accentBlue: AppDashboardTheme.brandBlue
50
+        )
51
+    }
52
+
53
+    /// Slight paper tint by layout variant (gallery + filled CV use the same rule).
54
+    static func paperBackground(variant: Int, base: NSColor) -> NSColor {
55
+        guard !isDark else { return base }
56
+        switch variant % 5 {
57
+        case 0: return base
58
+        case 1: return NSColor(srgbRed: 0.995, green: 0.992, blue: 0.985, alpha: 1)
59
+        case 2: return NSColor(srgbRed: 0.96, green: 0.99, blue: 1, alpha: 1)
60
+        case 3: return NSColor(srgbRed: 0.99, green: 0.99, blue: 0.99, alpha: 1)
61
+        default: return NSColor(srgbRed: 0.99, green: 0.98, blue: 0.995, alpha: 1)
62
+        }
63
+    }
64
+
65
+    static func accentColor(for template: CVTemplate) -> NSColor {
66
+        let palette = colors()
67
+        switch template.accent {
68
+        case .redUnderline, .redBar:
69
+            return palette.accentRed
70
+        case .blueBar:
71
+            return template.themeColor
72
+        case .none:
73
+            return template.themeColor.blended(withFraction: 0.5, of: palette.ink) ?? template.themeColor
74
+        }
75
+    }
76
+
77
+    static func sectionHeadingColor(for template: CVTemplate) -> NSColor {
78
+        accentColor(for: template)
79
+    }
80
+}

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

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

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

@@ -563,18 +563,12 @@ final class CVMakerPageView: NSView {
563
         static var cardBorderSelected: NSColor { AppDashboardTheme.brandBlue }
563
         static var cardBorderSelected: NSColor { AppDashboardTheme.brandBlue }
564
         static var cardFooter: NSColor { AppDashboardTheme.cvMakerCardFooter }
564
         static var cardFooter: NSColor { AppDashboardTheme.cvMakerCardFooter }
565
         static var previewSurface: NSColor { AppDashboardTheme.cvMakerPreviewSurface }
565
         static var previewSurface: NSColor { AppDashboardTheme.cvMakerPreviewSurface }
566
-        static var previewPaper: NSColor { NSColor.white }
567
-        static var previewSidebarTint: NSColor { AppDashboardTheme.cvMakerPreviewSidebarTint }
568
-        static var previewInk: NSColor {
569
-            NSColor(srgbRed: 38 / 255, green: 50 / 255, blue: 71 / 255, alpha: 1)
570
-        }
571
-        static var previewMuted: NSColor {
572
-            NSColor(srgbRed: 165 / 255, green: 175 / 255, blue: 192 / 255, alpha: 1)
573
-        }
574
-        static var previewAccentRed: NSColor {
575
-            NSColor(srgbRed: 207 / 255, green: 67 / 255, blue: 50 / 255, alpha: 1)
576
-        }
577
-        static var previewAccentBlue: NSColor { AppDashboardTheme.brandBlue }
566
+        static var previewPaper: NSColor { CVResumeAppearance.colors().paper }
567
+        static var previewSidebarTint: NSColor { CVResumeAppearance.colors().sidebarTint }
568
+        static var previewInk: NSColor { CVResumeAppearance.colors().ink }
569
+        static var previewMuted: NSColor { CVResumeAppearance.colors().muted }
570
+        static var previewAccentRed: NSColor { CVResumeAppearance.colors().accentRed }
571
+        static var previewAccentBlue: NSColor { CVResumeAppearance.colors().accentBlue }
578
         static var ctaBackground: NSColor { AppDashboardTheme.brandBlue }
572
         static var ctaBackground: NSColor { AppDashboardTheme.brandBlue }
579
         static var ctaHover: NSColor { AppDashboardTheme.brandBlueHover }
573
         static var ctaHover: NSColor { AppDashboardTheme.brandBlueHover }
580
         static var ctaText: NSColor { AppDashboardTheme.proCTAText }
574
         static var ctaText: NSColor { AppDashboardTheme.proCTAText }

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

@@ -38,6 +38,12 @@ private struct DocumentStyle {
38
 
38
 
39
     static func make(for template: CVTemplate) -> DocumentStyle {
39
     static func make(for template: CVTemplate) -> DocumentStyle {
40
         let theme = template.themeColor
40
         let theme = template.themeColor
41
+        let colors = CVResumeAppearance.colors()
42
+        let sectionInk = CVResumeAppearance.sectionHeadingColor(for: template)
43
+        let cardBG = CVResumeAppearance.paperBackground(
44
+            variant: template.galleryLayoutVariant,
45
+            base: colors.cardBackground
46
+        )
41
         switch template.family {
47
         switch template.family {
42
         case .minimal:
48
         case .minimal:
43
             return DocumentStyle(
49
             return DocumentStyle(
@@ -54,14 +60,14 @@ private struct DocumentStyle {
54
                 bulletBodyFont: .systemFont(ofSize: 12, weight: .regular),
60
                 bulletBodyFont: .systemFont(ofSize: 12, weight: .regular),
55
                 bulletMarkerFont: .systemFont(ofSize: 11, weight: .light),
61
                 bulletMarkerFont: .systemFont(ofSize: 11, weight: .light),
56
                 bulletMarkerColor: theme.withAlphaComponent(0.55),
62
                 bulletMarkerColor: theme.withAlphaComponent(0.55),
57
-                ink: NSColor(srgbRed: 42 / 255, green: 48 / 255, blue: 56 / 255, alpha: 1),
58
-                muted: NSColor(srgbRed: 110 / 255, green: 118 / 255, blue: 128 / 255, alpha: 1),
59
-                rule: NSColor(srgbRed: 228 / 255, green: 230 / 255, blue: 234 / 255, alpha: 1),
60
-                cardBackground: NSColor(srgbRed: 0.998, green: 0.998, blue: 0.998, alpha: 1),
63
+                ink: colors.ink,
64
+                muted: colors.muted,
65
+                rule: colors.rule,
66
+                cardBackground: cardBG,
61
                 columnVerticalSpacing: 15,
67
                 columnVerticalSpacing: 15,
62
                 bodyBlockSpacing: 15,
68
                 bodyBlockSpacing: 15,
63
                 roleUsesThemeColor: false,
69
                 roleUsesThemeColor: false,
64
-                sectionInk: theme.withAlphaComponent(0.92)
70
+                sectionInk: sectionInk
65
             )
71
             )
66
 
72
 
67
         case .professional:
73
         case .professional:
@@ -78,15 +84,15 @@ private struct DocumentStyle {
78
                 eduMetaFont: .systemFont(ofSize: 11.5, weight: .medium),
84
                 eduMetaFont: .systemFont(ofSize: 11.5, weight: .medium),
79
                 bulletBodyFont: .systemFont(ofSize: 12, weight: .regular),
85
                 bulletBodyFont: .systemFont(ofSize: 12, weight: .regular),
80
                 bulletMarkerFont: .systemFont(ofSize: 12, weight: .bold),
86
                 bulletMarkerFont: .systemFont(ofSize: 12, weight: .bold),
81
-                bulletMarkerColor: NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.55),
82
-                ink: NSColor(srgbRed: 28 / 255, green: 36 / 255, blue: 48 / 255, alpha: 1),
83
-                muted: NSColor(srgbRed: 88 / 255, green: 98 / 255, blue: 118 / 255, alpha: 1),
84
-                rule: NSColor(srgbRed: 210 / 255, green: 218 / 255, blue: 232 / 255, alpha: 1),
85
-                cardBackground: NSColor.white,
87
+                bulletMarkerColor: theme,
88
+                ink: colors.ink,
89
+                muted: colors.muted,
90
+                rule: colors.rule,
91
+                cardBackground: cardBG,
86
                 columnVerticalSpacing: 13,
92
                 columnVerticalSpacing: 13,
87
                 bodyBlockSpacing: 13,
93
                 bodyBlockSpacing: 13,
88
                 roleUsesThemeColor: false,
94
                 roleUsesThemeColor: false,
89
-                sectionInk: theme
95
+                sectionInk: sectionInk
90
             )
96
             )
91
 
97
 
92
         case .modern:
98
         case .modern:
@@ -104,14 +110,14 @@ private struct DocumentStyle {
104
                 bulletBodyFont: .systemFont(ofSize: 12.5, weight: .regular),
110
                 bulletBodyFont: .systemFont(ofSize: 12.5, weight: .regular),
105
                 bulletMarkerFont: .systemFont(ofSize: 13, weight: .bold),
111
                 bulletMarkerFont: .systemFont(ofSize: 13, weight: .bold),
106
                 bulletMarkerColor: theme,
112
                 bulletMarkerColor: theme,
107
-                ink: NSColor(srgbRed: 24 / 255, green: 34 / 255, blue: 52 / 255, alpha: 1),
108
-                muted: NSColor(srgbRed: 96 / 255, green: 110 / 255, blue: 132 / 255, alpha: 1),
109
-                rule: NSColor(srgbRed: 200 / 255, green: 214 / 255, blue: 236 / 255, alpha: 1),
110
-                cardBackground: NSColor(srgbRed: 0.99, green: 0.995, blue: 1, alpha: 1),
113
+                ink: colors.ink,
114
+                muted: colors.muted,
115
+                rule: colors.rule,
116
+                cardBackground: cardBG,
111
                 columnVerticalSpacing: 17,
117
                 columnVerticalSpacing: 17,
112
                 bodyBlockSpacing: 16,
118
                 bodyBlockSpacing: 16,
113
                 roleUsesThemeColor: true,
119
                 roleUsesThemeColor: true,
114
-                sectionInk: theme
120
+                sectionInk: sectionInk
115
             )
121
             )
116
 
122
 
117
         case .executive:
123
         case .executive:
@@ -125,6 +131,9 @@ private struct DocumentStyle {
125
                 ?? NSFontManager.shared.convert(georgia12, toHaveTrait: .italicFontMask)
131
                 ?? NSFontManager.shared.convert(georgia12, toHaveTrait: .italicFontMask)
126
             let eduMeta = NSFont(name: "Georgia-Italic", size: 11.5)
132
             let eduMeta = NSFont(name: "Georgia-Italic", size: 11.5)
127
                 ?? NSFontManager.shared.convert(georgia115, toHaveTrait: .italicFontMask)
133
                 ?? NSFontManager.shared.convert(georgia115, toHaveTrait: .italicFontMask)
134
+            let execCard = CVResumeAppearance.isDark
135
+                ? cardBG
136
+                : NSColor(srgbRed: 0.992, green: 0.99, blue: 0.985, alpha: 1)
128
             return DocumentStyle(
137
             return DocumentStyle(
129
                 nameFont: serifName,
138
                 nameFont: serifName,
130
                 roleFont: serifRole,
139
                 roleFont: serifRole,
@@ -138,15 +147,15 @@ private struct DocumentStyle {
138
                 eduMetaFont: eduMeta,
147
                 eduMetaFont: eduMeta,
139
                 bulletBodyFont: serifCompact,
148
                 bulletBodyFont: serifCompact,
140
                 bulletMarkerFont: .systemFont(ofSize: 11, weight: .bold),
149
                 bulletMarkerFont: .systemFont(ofSize: 11, weight: .bold),
141
-                bulletMarkerColor: NSColor(srgbRed: 55 / 255, green: 55 / 255, blue: 62 / 255, alpha: 1),
142
-                ink: NSColor(srgbRed: 22 / 255, green: 22 / 255, blue: 28 / 255, alpha: 1),
143
-                muted: NSColor(srgbRed: 82 / 255, green: 82 / 255, blue: 90 / 255, alpha: 1),
144
-                rule: NSColor(srgbRed: 72 / 255, green: 72 / 255, blue: 78 / 255, alpha: 0.35),
145
-                cardBackground: NSColor(srgbRed: 0.992, green: 0.99, blue: 0.985, alpha: 1),
150
+                bulletMarkerColor: colors.ink.withAlphaComponent(0.75),
151
+                ink: colors.ink,
152
+                muted: colors.muted,
153
+                rule: colors.rule,
154
+                cardBackground: execCard,
146
                 columnVerticalSpacing: 18,
155
                 columnVerticalSpacing: 18,
147
                 bodyBlockSpacing: 17,
156
                 bodyBlockSpacing: 17,
148
                 roleUsesThemeColor: false,
157
                 roleUsesThemeColor: false,
149
-                sectionInk: NSColor(srgbRed: 32 / 255, green: 32 / 255, blue: 38 / 255, alpha: 1)
158
+                sectionInk: sectionInk
150
             )
159
             )
151
 
160
 
152
         case .creative:
161
         case .creative:
@@ -163,15 +172,15 @@ private struct DocumentStyle {
163
                 eduMetaFont: .systemFont(ofSize: 12, weight: .medium),
172
                 eduMetaFont: .systemFont(ofSize: 12, weight: .medium),
164
                 bulletBodyFont: .systemFont(ofSize: 12.5, weight: .regular),
173
                 bulletBodyFont: .systemFont(ofSize: 12.5, weight: .regular),
165
                 bulletMarkerFont: .systemFont(ofSize: 13, weight: .heavy),
174
                 bulletMarkerFont: .systemFont(ofSize: 13, weight: .heavy),
166
-                bulletMarkerColor: NSColor(srgbRed: 207 / 255, green: 67 / 255, blue: 50 / 255, alpha: 1),
167
-                ink: NSColor(srgbRed: 32 / 255, green: 26 / 255, blue: 52 / 255, alpha: 1),
168
-                muted: NSColor(srgbRed: 108 / 255, green: 96 / 255, blue: 130 / 255, alpha: 1),
175
+                bulletMarkerColor: CVResumeAppearance.accentColor(for: template),
176
+                ink: colors.ink,
177
+                muted: colors.muted,
169
                 rule: theme.withAlphaComponent(0.22),
178
                 rule: theme.withAlphaComponent(0.22),
170
-                cardBackground: NSColor(srgbRed: 0.995, green: 0.993, blue: 1, alpha: 1),
179
+                cardBackground: cardBG,
171
                 columnVerticalSpacing: 18,
180
                 columnVerticalSpacing: 18,
172
                 bodyBlockSpacing: 17,
181
                 bodyBlockSpacing: 17,
173
                 roleUsesThemeColor: true,
182
                 roleUsesThemeColor: true,
174
-                sectionInk: NSColor(srgbRed: 207 / 255, green: 67 / 255, blue: 50 / 255, alpha: 1)
183
+                sectionInk: sectionInk
175
             )
184
             )
176
         }
185
         }
177
     }
186
     }
@@ -187,9 +196,12 @@ final class CVProfileDocumentView: NSView {
187
 
196
 
188
     private let profile: SavedProfile
197
     private let profile: SavedProfile
189
     private let template: CVTemplate
198
     private let template: CVTemplate
190
-    private let style: DocumentStyle
199
+    private var style: DocumentStyle
191
     /// Matches `CVTemplatePreviewView` so the same template id + layout recipe renders the same silhouette as the gallery card.
200
     /// Matches `CVTemplatePreviewView` so the same template id + layout recipe renders the same silhouette as the gallery card.
192
     private let variant: Int
201
     private let variant: Int
202
+    private var appearanceObserver: NSObjectProtocol?
203
+    private weak var cardView: NSView?
204
+
193
     init(profile: SavedProfile, template: CVTemplate) {
205
     init(profile: SavedProfile, template: CVTemplate) {
194
         self.profile = profile
206
         self.profile = profile
195
         self.template = template
207
         self.template = template
@@ -200,9 +212,35 @@ final class CVProfileDocumentView: NSView {
200
         wantsLayer = true
212
         wantsLayer = true
201
         layer?.backgroundColor = NSColor.clear.cgColor
213
         layer?.backgroundColor = NSColor.clear.cgColor
202
         userInterfaceLayoutDirection = .leftToRight
214
         userInterfaceLayoutDirection = .leftToRight
203
-        // Let the preview stack stretch us to the scroll view width; don’t shrink to label intrinsic widths.
204
         setContentHuggingPriority(.defaultLow, for: .horizontal)
215
         setContentHuggingPriority(.defaultLow, for: .horizontal)
216
+        installCardContent()
217
+        appearanceObserver = NotificationCenter.default.addObserver(
218
+            forName: AppAppearanceManager.didChangeNotification,
219
+            object: nil,
220
+            queue: .main
221
+        ) { [weak self] _ in
222
+            self?.refreshForAppearanceChange()
223
+        }
224
+    }
225
+
226
+    deinit {
227
+        if let appearanceObserver {
228
+            NotificationCenter.default.removeObserver(appearanceObserver)
229
+        }
230
+    }
231
+
232
+    override func viewDidChangeEffectiveAppearance() {
233
+        super.viewDidChangeEffectiveAppearance()
234
+        refreshForAppearanceChange()
235
+    }
205
 
236
 
237
+    private func refreshForAppearanceChange() {
238
+        style = DocumentStyle.make(for: template)
239
+        installCardContent()
240
+    }
241
+
242
+    private func installCardContent() {
243
+        subviews.forEach { $0.removeFromSuperview() }
206
         let card = NSView()
244
         let card = NSView()
207
         card.translatesAutoresizingMaskIntoConstraints = false
245
         card.translatesAutoresizingMaskIntoConstraints = false
208
         card.wantsLayer = true
246
         card.wantsLayer = true
@@ -211,6 +249,7 @@ final class CVProfileDocumentView: NSView {
211
         card.layer?.borderWidth = 1
249
         card.layer?.borderWidth = 1
212
         card.layer?.borderColor = style.rule.cgColor
250
         card.layer?.borderColor = style.rule.cgColor
213
         card.layer?.masksToBounds = true
251
         card.layer?.masksToBounds = true
252
+        cardView = card
214
 
253
 
215
         let root = buildRoot()
254
         let root = buildRoot()
216
         root.translatesAutoresizingMaskIntoConstraints = false
255
         root.translatesAutoresizingMaskIntoConstraints = false
@@ -278,8 +317,12 @@ final class CVProfileDocumentView: NSView {
278
             return buildModernFamilyDocument()
317
             return buildModernFamilyDocument()
279
         case .creative:
318
         case .creative:
280
             return buildCreativeFamilyDocument()
319
             return buildCreativeFamilyDocument()
281
-        case .professional, .minimal, .executive:
282
-            return buildTraditionalFamilyDocument()
320
+        case .executive:
321
+            return buildExecutiveDocument()
322
+        case .minimal:
323
+            return buildMinimalDocument()
324
+        case .professional:
325
+            return buildProfessionalDocument()
283
         }
326
         }
284
     }
327
     }
285
 
328
 
@@ -550,7 +593,7 @@ final class CVProfileDocumentView: NSView {
550
         let iv = NSImageView(image: img)
593
         let iv = NSImageView(image: img)
551
         iv.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
594
         iv.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
552
         iv.contentTintColor = theme
595
         iv.contentTintColor = theme
553
-        let t = label(title.uppercased(), font: style.sectionFont, color: style.sectionInk, maxLines: 1)
596
+        let t = label(title.uppercased(), font: style.sectionFont, color: style.ink, maxLines: 1)
554
         let r = NSStackView(views: [iv, t])
597
         let r = NSStackView(views: [iv, t])
555
         r.orientation = .horizontal
598
         r.orientation = .horizontal
556
         r.spacing = 8
599
         r.spacing = 8
@@ -762,9 +805,300 @@ final class CVProfileDocumentView: NSView {
762
         return stack
805
         return stack
763
     }
806
     }
764
 
807
 
765
-    // MARK: - Traditional families (professional / minimal / executive)
808
+    // MARK: - Executive (matches gallery serif layout)
809
+
810
+    private func buildExecutiveDocument() -> NSView {
811
+        let centeredHead = (variant % 2) == 0
812
+        let nameText = displayable(profile.personal.fullName, placeholder: "Your name")
813
+        let roleText = displayable(profile.personal.jobTitle, placeholder: "Professional headline")
814
+        let contactParts = [profile.personal.email, profile.personal.phone, profile.personal.address].filter {
815
+            !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
816
+        }
817
+        let contactText = contactParts.isEmpty ? "Add contact details in your profile" : contactParts.joined(separator: " · ")
818
+
819
+        let name = label(nameText, font: style.nameFont, color: style.ink, maxLines: 2)
820
+        let role = label(roleText, font: style.roleFont, color: style.muted, maxLines: 2)
821
+        let contact = label(contactText, font: style.contactFont, color: style.muted.withAlphaComponent(0.9), maxLines: 3)
822
+        if centeredHead {
823
+            name.alignment = .center
824
+            role.alignment = .center
825
+            contact.alignment = .center
826
+        }
827
+
828
+        let rule = executiveHeaderRule(wide: variant % 3 == 0)
829
+        let head = NSStackView(views: [name, role, contact, rule])
830
+        head.orientation = .vertical
831
+        head.spacing = 8
832
+        head.alignment = centeredHead ? .centerX : .leading
833
+
834
+        let body: NSView
835
+        switch template.layout {
836
+        case .singleColumn:
837
+            body = executiveMainColumn(compact: false, tightLeading: variant % 5 == 2)
838
+        case .twoColumn(let side, let tinted):
839
+            let main = executiveMainColumn(compact: true, tightLeading: variant % 5 == 2)
840
+            let sideCol = executiveSidebarColumn(tinted: tinted, showMetrics: variant % 4 == 1)
841
+            let row = NSStackView()
842
+            row.orientation = .horizontal
843
+            row.spacing = 20
844
+            row.alignment = .top
845
+            if side == .leading {
846
+                row.addArrangedSubview(sideCol)
847
+                row.addArrangedSubview(main)
848
+            } else {
849
+                row.addArrangedSubview(main)
850
+                row.addArrangedSubview(sideCol)
851
+            }
852
+            let mult: CGFloat = (variant % 5 == 3) ? 0.38 : 0.33
853
+            sideCol.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
854
+            body = row
855
+        }
856
+
857
+        let wrap = NSStackView(views: [head, body])
858
+        wrap.orientation = .vertical
859
+        wrap.spacing = style.columnVerticalSpacing
860
+        wrap.alignment = .leading
861
+        return wrap
862
+    }
863
+
864
+    private func executiveHeaderRule(wide: Bool) -> NSView {
865
+        let theme = template.themeColor
866
+        let v = NSView()
867
+        v.translatesAutoresizingMaskIntoConstraints = false
868
+        v.wantsLayer = true
869
+        v.layer?.backgroundColor = theme.withAlphaComponent(0.45).cgColor
870
+        v.heightAnchor.constraint(equalToConstant: wide ? 2 : 1.5).isActive = true
871
+        v.widthAnchor.constraint(equalToConstant: wide ? 160 : 110).isActive = true
872
+        return v
873
+    }
874
+
875
+    private func executiveMainColumn(compact: Bool, tightLeading: Bool) -> NSView {
876
+        let stack = NSStackView()
877
+        stack.orientation = .vertical
878
+        stack.spacing = (compact ? style.bodyBlockSpacing : style.bodyBlockSpacing + 2) - (tightLeading ? 2 : 0)
879
+        stack.alignment = .leading
880
+
881
+        let summaryTitle = (variant % 6 == 3) ? "Summary" : "Professional Summary"
882
+        if let summary = nonEmpty(profile.careerSummary) {
883
+            stack.addArrangedSubview(sectionHeading(summaryTitle))
884
+            stack.addArrangedSubview(paragraph(summary, compact: compact))
885
+        }
886
+
887
+        let jobs = profile.workExperiences.filter { !$0.isEffectivelyEmpty }
888
+        if !jobs.isEmpty {
889
+            stack.addArrangedSubview(sectionHeading("Selected Experience"))
890
+            for job in jobs {
891
+                stack.addArrangedSubview(executiveExperienceBlock(job: job, compact: compact))
892
+            }
893
+        }
894
+
895
+        let schools = profile.educations.filter { !$0.isEffectivelyEmpty }
896
+        if !schools.isEmpty {
897
+            stack.addArrangedSubview(sectionHeading("Education"))
898
+            for edu in schools {
899
+                stack.addArrangedSubview(educationBlock(edu: edu, compact: compact))
900
+            }
901
+        }
902
+        appendCertificatesInterestsReferrals(to: stack, compact: compact)
903
+        return stack
904
+    }
905
+
906
+    private func executiveExperienceBlock(job: WorkExperiencePayload, compact: Bool) -> NSView {
907
+        let v = NSStackView()
908
+        v.orientation = .vertical
909
+        v.spacing = 4
910
+        v.alignment = .leading
911
+        let titleLine = [job.jobTitle, job.company].filter { !$0.isEmpty }.joined(separator: ", ")
912
+        if !titleLine.isEmpty {
913
+            v.addArrangedSubview(label(titleLine, font: style.expTitleFont, color: style.ink, maxLines: 0))
914
+        }
915
+        let duration = job.duration.trimmingCharacters(in: .whitespacesAndNewlines)
916
+        if !duration.isEmpty {
917
+            v.addArrangedSubview(label(duration, font: style.expMetaFont, color: template.themeColor, maxLines: 0))
918
+        }
919
+        for bullet in Self.bulletChunks(from: job.description) {
920
+            v.addArrangedSubview(label(bullet, font: style.bodyFont, color: style.muted, maxLines: 0))
921
+        }
922
+        return v
923
+    }
924
+
925
+    private func executiveSidebarColumn(tinted: Bool, showMetrics: Bool) -> NSView {
926
+        let stack = NSStackView()
927
+        stack.orientation = .vertical
928
+        stack.spacing = 10
929
+        stack.alignment = .leading
930
+        if tinted {
931
+            stack.wantsLayer = true
932
+            let fill = (variant % 3 == 0)
933
+                ? CVResumeAppearance.colors().sidebarTint
934
+                : template.themeColor.withAlphaComponent(0.07)
935
+            stack.layer?.backgroundColor = fill.cgColor
936
+            stack.layer?.cornerRadius = variant % 4 == 2 ? 8 : 6
937
+            stack.edgeInsets = NSEdgeInsets(top: 14, left: 14, bottom: 14, right: 14)
938
+        }
939
+
940
+        let skills = skillTokensFromProfile(max: 12)
941
+        if !skills.isEmpty {
942
+            stack.addArrangedSubview(sectionHeading("Core Competencies"))
943
+            for s in skills {
944
+                stack.addArrangedSubview(label("· \(s)", font: style.bodyFont, color: style.ink, maxLines: 0))
945
+            }
946
+        }
947
+
948
+        if let cert = nonEmpty(profile.certificates) {
949
+            stack.addArrangedSubview(sectionHeading("Tools"))
950
+            stack.addArrangedSubview(paragraph(cert, compact: true))
951
+        }
952
+
953
+        if showMetrics, let hi = highlightsBodyText() {
954
+            stack.addArrangedSubview(sectionHeading("Impact"))
955
+            stack.addArrangedSubview(paragraph(hi, compact: true))
956
+        }
957
+
958
+        if stack.arrangedSubviews.isEmpty {
959
+            stack.addArrangedSubview(sectionHeading("Contact"))
960
+            for line in contactLines() {
961
+                stack.addArrangedSubview(label(line, font: style.contactFont, color: style.ink, maxLines: 0))
962
+            }
963
+        }
964
+        return stack
965
+    }
966
+
967
+    // MARK: - Minimal (matches gallery light typography)
968
+
969
+    private func buildMinimalDocument() -> NSView {
970
+        let nameText = displayable(profile.personal.fullName, placeholder: "Your name")
971
+        let roleText = displayable(profile.personal.jobTitle, placeholder: "Professional headline")
972
+        let contactParts = [profile.personal.email, profile.personal.phone].filter {
973
+            !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
974
+        }
975
+        let contactText = contactParts.isEmpty ? "Add contact in your profile" : contactParts.joined(separator: "   ")
976
+
977
+        let nameWeight: NSFont.Weight = (variant % 3 == 0) ? .ultraLight : .light
978
+        let nameSize: CGFloat = template.headline == .centered ? 22 + CGFloat(variant % 2) : 20 + CGFloat(variant % 3)
979
+        let name = label(nameText, font: .systemFont(ofSize: nameSize, weight: nameWeight), color: style.ink, maxLines: 2)
980
+        let role = label(roleText.uppercased(), font: .systemFont(ofSize: 12.5, weight: .medium), color: style.muted, maxLines: 2)
981
+        let contact = label(contactText, font: style.contactFont, color: style.muted.withAlphaComponent(0.75), maxLines: 2)
982
+
983
+        let head = NSStackView()
984
+        head.orientation = .vertical
985
+        head.spacing = template.headline == .avatarStacked ? 10 : 6 + CGFloat(variant % 4)
986
+        head.alignment = template.headline == .centered ? .centerX : .leading
987
+        if template.headline == .avatarStacked {
988
+            head.addArrangedSubview(initialsBadge(for: nameText))
989
+        }
990
+        if template.headline == .centered {
991
+            name.alignment = .center
992
+            role.alignment = .center
993
+            contact.alignment = .center
994
+        }
995
+        head.addArrangedSubview(name)
996
+        head.addArrangedSubview(role)
997
+        head.addArrangedSubview(contact)
998
+        head.addArrangedSubview(hairline())
999
+        if variant % 5 == 1 {
1000
+            head.addArrangedSubview(hairline())
1001
+        }
1002
+
1003
+        let swapEdu = (variant % 4) == 2
1004
+        let body: NSView
1005
+        switch template.layout {
1006
+        case .singleColumn:
1007
+            body = minimalMainColumn(spacing: style.bodyBlockSpacing, educationBeforeExperience: swapEdu)
1008
+        case .twoColumn(let side, _):
1009
+            let main = minimalMainColumn(spacing: style.bodyBlockSpacing - 2, educationBeforeExperience: swapEdu)
1010
+            let aside = minimalSkillsAside(numbered: variant % 3 == 1)
1011
+            let row = NSStackView()
1012
+            row.orientation = .horizontal
1013
+            row.spacing = 18 + CGFloat(variant % 3)
1014
+            row.alignment = .top
1015
+            if side == .leading {
1016
+                row.addArrangedSubview(aside)
1017
+                row.addArrangedSubview(main)
1018
+            } else {
1019
+                row.addArrangedSubview(main)
1020
+                row.addArrangedSubview(aside)
1021
+            }
1022
+            let mult: CGFloat = (variant % 5 == 0) ? 0.34 : 0.30
1023
+            aside.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
1024
+            body = row
1025
+        }
1026
+
1027
+        let wrap = NSStackView(views: [head, body])
1028
+        wrap.orientation = .vertical
1029
+        wrap.spacing = style.columnVerticalSpacing
1030
+        wrap.alignment = .leading
1031
+        return wrap
1032
+    }
1033
+
1034
+    private func minimalMainColumn(spacing: CGFloat, educationBeforeExperience: Bool) -> NSView {
1035
+        let stack = NSStackView()
1036
+        stack.orientation = .vertical
1037
+        stack.spacing = spacing
1038
+        stack.alignment = .leading
1039
+
1040
+        let appendSummary: () -> Void = { [self] in
1041
+            if let summary = nonEmpty(profile.careerSummary) {
1042
+                stack.addArrangedSubview(sectionHeading("Profile"))
1043
+                stack.addArrangedSubview(paragraph(summary, compact: false))
1044
+            }
1045
+        }
1046
+        let appendExperience: () -> Void = { [self] in
1047
+            let jobs = profile.workExperiences.filter { !$0.isEffectivelyEmpty }
1048
+            if !jobs.isEmpty {
1049
+                stack.addArrangedSubview(sectionHeading("Experience"))
1050
+                for job in jobs {
1051
+                    stack.addArrangedSubview(experienceBlock(job: job, compact: false))
1052
+                }
1053
+            }
1054
+        }
1055
+        let appendEducation: () -> Void = { [self] in
1056
+            let schools = profile.educations.filter { !$0.isEffectivelyEmpty }
1057
+            if !schools.isEmpty {
1058
+                stack.addArrangedSubview(sectionHeading("Education"))
1059
+                for edu in schools {
1060
+                    stack.addArrangedSubview(educationBlock(edu: edu, compact: false))
1061
+                }
1062
+            }
1063
+        }
1064
+
1065
+        if educationBeforeExperience {
1066
+            appendEducation()
1067
+            appendSummary()
1068
+            appendExperience()
1069
+        } else {
1070
+            appendSummary()
1071
+            appendExperience()
1072
+            appendEducation()
1073
+        }
1074
+        appendCertificatesInterestsReferrals(to: stack, compact: false)
1075
+        return stack
1076
+    }
1077
+
1078
+    private func minimalSkillsAside(numbered: Bool) -> NSView {
1079
+        let stack = NSStackView()
1080
+        stack.orientation = .vertical
1081
+        stack.spacing = 8
1082
+        stack.alignment = .leading
1083
+        let skills = skillTokensFromProfile(max: 12)
1084
+        if skills.isEmpty {
1085
+            stack.addArrangedSubview(sectionHeading("Contact"))
1086
+            for line in contactLines() {
1087
+                stack.addArrangedSubview(label(line, font: style.contactFont, color: style.muted, maxLines: 0))
1088
+            }
1089
+            return stack
1090
+        }
1091
+        stack.addArrangedSubview(sectionHeading("Skills"))
1092
+        for (i, s) in skills.enumerated() {
1093
+            let prefix = numbered ? "\(i + 1). " : "·  "
1094
+            stack.addArrangedSubview(label("\(prefix)\(s)", font: style.bodyCompactFont, color: style.muted, maxLines: 0))
1095
+        }
1096
+        return stack
1097
+    }
1098
+
1099
+    // MARK: - Professional
766
 
1100
 
767
-    private func buildTraditionalFamilyDocument() -> NSView {
1101
+    private func buildProfessionalDocument() -> NSView {
768
         switch template.layout {
1102
         switch template.layout {
769
         case .singleColumn:
1103
         case .singleColumn:
770
             return singleColumnLayout()
1104
             return singleColumnLayout()
@@ -840,7 +1174,7 @@ final class CVProfileDocumentView: NSView {
840
         let row = NSStackView()
1174
         let row = NSStackView()
841
         row.orientation = .horizontal
1175
         row.orientation = .horizontal
842
         row.alignment = .top
1176
         row.alignment = .top
843
-        row.spacing = template.family == .minimal ? 18 : 22
1177
+        row.spacing = 22
844
 
1178
 
845
         let sidebarCol = sidebarColumn(tinted: tinted)
1179
         let sidebarCol = sidebarColumn(tinted: tinted)
846
         let mainCol = bodyColumn(compact: true, experienceFirst: professionalExperienceFirst)
1180
         let mainCol = bodyColumn(compact: true, experienceFirst: professionalExperienceFirst)
@@ -852,12 +1186,7 @@ final class CVProfileDocumentView: NSView {
852
             row.addArrangedSubview(mainCol)
1186
             row.addArrangedSubview(mainCol)
853
             row.addArrangedSubview(sidebarCol)
1187
             row.addArrangedSubview(sidebarCol)
854
         }
1188
         }
855
-        let sidebarMult: CGFloat
856
-        if template.family == .professional {
857
-            sidebarMult = (variant % 5 == 2) ? 0.38 : 0.32
858
-        } else {
859
-            sidebarMult = template.family == .executive ? 0.34 : 0.32
860
-        }
1189
+        let sidebarMult: CGFloat = (variant % 5 == 2) ? 0.38 : 0.32
861
         sidebarCol.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: sidebarMult).isActive = true
1190
         sidebarCol.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: sidebarMult).isActive = true
862
 
1191
 
863
         v.addArrangedSubview(row)
1192
         v.addArrangedSubview(row)
@@ -965,12 +1294,12 @@ final class CVProfileDocumentView: NSView {
965
             bar.heightAnchor.constraint(equalToConstant: 1).isActive = true
1294
             bar.heightAnchor.constraint(equalToConstant: 1).isActive = true
966
             return bar
1295
             return bar
967
         case .redUnderline:
1296
         case .redUnderline:
968
-            bar.layer?.backgroundColor = NSColor(srgbRed: 207 / 255, green: 67 / 255, blue: 50 / 255, alpha: 1).cgColor
1297
+            bar.layer?.backgroundColor = CVResumeAppearance.accentColor(for: template).cgColor
969
             bar.heightAnchor.constraint(equalToConstant: 2).isActive = true
1298
             bar.heightAnchor.constraint(equalToConstant: 2).isActive = true
970
             bar.widthAnchor.constraint(equalToConstant: template.family == .minimal ? 140 : 168).isActive = true
1299
             bar.widthAnchor.constraint(equalToConstant: template.family == .minimal ? 140 : 168).isActive = true
971
             return bar
1300
             return bar
972
         case .redBar:
1301
         case .redBar:
973
-            bar.layer?.backgroundColor = NSColor(srgbRed: 207 / 255, green: 67 / 255, blue: 50 / 255, alpha: 1).cgColor
1302
+            bar.layer?.backgroundColor = CVResumeAppearance.accentColor(for: template).cgColor
974
             bar.heightAnchor.constraint(equalToConstant: 4).isActive = true
1303
             bar.heightAnchor.constraint(equalToConstant: 4).isActive = true
975
             bar.widthAnchor.constraint(equalToConstant: 56).isActive = true
1304
             bar.widthAnchor.constraint(equalToConstant: 56).isActive = true
976
             return bar
1305
             return bar
@@ -1004,8 +1333,11 @@ final class CVProfileDocumentView: NSView {
1004
         box.alignment = .leading
1333
         box.alignment = .leading
1005
         if tinted {
1334
         if tinted {
1006
             box.wantsLayer = true
1335
             box.wantsLayer = true
1007
-            box.layer?.backgroundColor = template.themeColor.withAlphaComponent(template.family == .creative ? 0.12 : 0.08).cgColor
1008
-            box.layer?.cornerRadius = 8
1336
+            let tint = (variant % 4 == 1)
1337
+                ? template.themeColor.withAlphaComponent(0.08)
1338
+                : CVResumeAppearance.colors().sidebarTint
1339
+            box.layer?.backgroundColor = tint.cgColor
1340
+            box.layer?.cornerRadius = variant % 3 == 0 ? 8 : 6
1009
         }
1341
         }
1010
         box.edgeInsets = NSEdgeInsets(top: tinted ? 14 : 0, left: tinted ? 14 : 0, bottom: tinted ? 14 : 0, right: tinted ? 14 : 0)
1342
         box.edgeInsets = NSEdgeInsets(top: tinted ? 14 : 0, left: tinted ? 14 : 0, bottom: tinted ? 14 : 0, right: tinted ? 14 : 0)
1011
 
1343
 
@@ -1014,8 +1346,24 @@ final class CVProfileDocumentView: NSView {
1014
             box.addArrangedSubview(label(line, font: style.contactFont, color: style.ink, maxLines: 0))
1346
             box.addArrangedSubview(label(line, font: style.contactFont, color: style.ink, maxLines: 0))
1015
         }
1347
         }
1016
 
1348
 
1017
-        if let skillsBlock = ancillaryBlock(title: "Languages & more", body: combinedAncillaryText()) {
1018
-            box.addArrangedSubview(skillsBlock)
1349
+        let skills = skillTokensFromProfile(max: 8)
1350
+        if !skills.isEmpty {
1351
+            box.addArrangedSubview(sectionHeading("Skills"))
1352
+            if variant % 5 == 2 {
1353
+                box.addArrangedSubview(skillTagRow(theme: template.themeColor, maxTags: 5))
1354
+            } else {
1355
+                for token in skills {
1356
+                    box.addArrangedSubview(label("·  \(token)", font: style.contactFont, color: style.ink, maxLines: 0))
1357
+                }
1358
+            }
1359
+        }
1360
+
1361
+        if variant % 7 == 3, let tools = nonEmpty(profile.certificates) {
1362
+            box.addArrangedSubview(sectionHeading("Tools"))
1363
+            box.addArrangedSubview(paragraph(tools, compact: true))
1364
+        } else if let ancillary = combinedAncillaryText(), !ancillary.isEmpty {
1365
+            box.addArrangedSubview(sectionHeading("Languages & more"))
1366
+            box.addArrangedSubview(paragraph(ancillary, compact: true))
1019
         }
1367
         }
1020
 
1368
 
1021
         return box
1369
         return box

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

@@ -62,21 +62,8 @@ final class CVTemplatePreviewView: NSView {
62
     private let palette: CVTemplateCardPalette
62
     private let palette: CVTemplateCardPalette
63
     private let paper = NSView()
63
     private let paper = NSView()
64
 
64
 
65
-    /// Stable 0…11 from template identity so each row looks different (not just by `family`).
66
-    private var idVariant: Int {
67
-        var h: UInt64 = 1469598103934665603
68
-        let layoutDesc: String
69
-        switch template.layout {
70
-        case .singleColumn: layoutDesc = "1col"
71
-        case .twoColumn(let s, let t): layoutDesc = "2col_\(s)_\(t)"
72
-        }
73
-        let blob = "\(template.id)|\(template.family.rawValue)|\(template.headline)|\(template.accent)|\(layoutDesc)|\(template.sectionLabelStyle)"
74
-        for b in blob.utf8 {
75
-            h ^= UInt64(b)
76
-            h &*= 1_099_511_628_211
77
-        }
78
-        return Int(h % 12)
79
-    }
65
+    /// Same variant index as `CVProfileDocumentView` (shared `CVTemplate.galleryLayoutVariant`).
66
+    private var layoutVariant: Int { template.galleryLayoutVariant }
80
 
67
 
81
     init(template: CVTemplate, palette: CVTemplateCardPalette) {
68
     init(template: CVTemplate, palette: CVTemplateCardPalette) {
82
         self.template = template
69
         self.template = template
@@ -133,29 +120,23 @@ final class CVTemplatePreviewView: NSView {
133
     }
120
     }
134
 
121
 
135
     private func paperBackgroundColor() -> NSColor {
122
     private func paperBackgroundColor() -> NSColor {
136
-        switch idVariant % 5 {
137
-        case 0: return palette.previewPaper
138
-        case 1: return NSColor(srgbRed: 0.995, green: 0.992, blue: 0.985, alpha: 1)
139
-        case 2: return NSColor(srgbRed: 0.96, green: 0.99, blue: 1, alpha: 1)
140
-        case 3: return NSColor(srgbRed: 0.99, green: 0.99, blue: 0.99, alpha: 1)
141
-        default: return NSColor(srgbRed: 0.99, green: 0.98, blue: 0.995, alpha: 1)
142
-        }
123
+        CVResumeAppearance.paperBackground(variant: layoutVariant, base: palette.previewPaper)
143
     }
124
     }
144
 
125
 
145
     // MARK: - Family: Professional (ATS-friendly)
126
     // MARK: - Family: Professional (ATS-friendly)
146
 
127
 
147
     private func buildProfessionalResume() -> NSView {
128
     private func buildProfessionalResume() -> NSView {
148
-        let swapExpFirst = (idVariant % 3) == 1
149
-        let sidebarMult: CGFloat = (idVariant % 5 == 2) ? 0.38 : 0.34
129
+        let swapExpFirst = (layoutVariant % 3) == 1
130
+        let sidebarMult: CGFloat = (layoutVariant % 5 == 2) ? 0.38 : 0.34
150
 
131
 
151
         switch template.layout {
132
         switch template.layout {
152
         case .singleColumn:
133
         case .singleColumn:
153
             let v = NSStackView()
134
             let v = NSStackView()
154
             v.orientation = .vertical
135
             v.orientation = .vertical
155
-            v.spacing = 4 + CGFloat(idVariant % 3)
136
+            v.spacing = 4 + CGFloat(layoutVariant % 3)
156
             v.alignment = .leading
137
             v.alignment = .leading
157
             v.addArrangedSubview(proHeaderBlock())
138
             v.addArrangedSubview(proHeaderBlock())
158
-            if (idVariant % 6) == 4 {
139
+            if (layoutVariant % 6) == 4 {
159
                 v.addArrangedSubview(proInlineSkillsRow())
140
                 v.addArrangedSubview(proInlineSkillsRow())
160
             }
141
             }
161
             v.addArrangedSubview(hairline())
142
             v.addArrangedSubview(hairline())
@@ -169,9 +150,9 @@ final class CVTemplatePreviewView: NSView {
169
             let rule = hairline()
150
             let rule = hairline()
170
             let row = NSStackView()
151
             let row = NSStackView()
171
             row.orientation = .horizontal
152
             row.orientation = .horizontal
172
-            row.spacing = 5 + CGFloat(idVariant % 3)
153
+            row.spacing = 5 + CGFloat(layoutVariant % 3)
173
             row.alignment = .top
154
             row.alignment = .top
174
-            let sidebar = proSidebarColumn(tinted: tinted, variant: idVariant)
155
+            let sidebar = proSidebarColumn(tinted: tinted, variant: layoutVariant)
175
             let main = proMainColumn(compact: true, experienceFirst: swapExpFirst)
156
             let main = proMainColumn(compact: true, experienceFirst: swapExpFirst)
176
             if side == .leading {
157
             if side == .leading {
177
                 row.addArrangedSubview(sidebar)
158
                 row.addArrangedSubview(sidebar)
@@ -315,7 +296,7 @@ final class CVTemplatePreviewView: NSView {
315
     private func proMainColumn(compact: Bool, experienceFirst: Bool) -> NSView {
296
     private func proMainColumn(compact: Bool, experienceFirst: Bool) -> NSView {
316
         let stack = NSStackView()
297
         let stack = NSStackView()
317
         stack.orientation = .vertical
298
         stack.orientation = .vertical
318
-        stack.spacing = compact ? 4 : 5 + CGFloat(idVariant % 2)
299
+        stack.spacing = compact ? 4 : 5 + CGFloat(layoutVariant % 2)
319
         stack.alignment = .leading
300
         stack.alignment = .leading
320
         let sp: CGFloat = compact ? 6.2 : 6.5
301
         let sp: CGFloat = compact ? 6.2 : 6.5
321
 
302
 
@@ -348,7 +329,7 @@ final class CVTemplatePreviewView: NSView {
348
     // MARK: - Family: Modern (three distinct silhouettes per id)
329
     // MARK: - Family: Modern (three distinct silhouettes per id)
349
 
330
 
350
     private func buildModernResume() -> NSView {
331
     private func buildModernResume() -> NSView {
351
-        switch idVariant % 3 {
332
+        switch layoutVariant % 3 {
352
         case 0: return buildModernClassicBandLayout()
333
         case 0: return buildModernClassicBandLayout()
353
         case 1: return buildModernRailDocLayout()
334
         case 1: return buildModernRailDocLayout()
354
         default: return buildModernSplitHeaderLayout()
335
         default: return buildModernSplitHeaderLayout()
@@ -364,7 +345,7 @@ final class CVTemplatePreviewView: NSView {
364
             let sideCol = modernSidebar(theme: theme, tinted: tinted)
345
             let sideCol = modernSidebar(theme: theme, tinted: tinted)
365
             let row = NSStackView()
346
             let row = NSStackView()
366
             row.orientation = .horizontal
347
             row.orientation = .horizontal
367
-            row.spacing = 5 + CGFloat(idVariant % 3)
348
+            row.spacing = 5 + CGFloat(layoutVariant % 3)
368
             row.alignment = .top
349
             row.alignment = .top
369
             if side == .leading {
350
             if side == .leading {
370
                 row.addArrangedSubview(sideCol)
351
                 row.addArrangedSubview(sideCol)
@@ -373,7 +354,7 @@ final class CVTemplatePreviewView: NSView {
373
                 row.addArrangedSubview(main)
354
                 row.addArrangedSubview(main)
374
                 row.addArrangedSubview(sideCol)
355
                 row.addArrangedSubview(sideCol)
375
             }
356
             }
376
-            let mult: CGFloat = (idVariant % 4 == 2) ? 0.36 : 0.32
357
+            let mult: CGFloat = (layoutVariant % 4 == 2) ? 0.36 : 0.32
377
             sideCol.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
358
             sideCol.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
378
             return row
359
             return row
379
         }
360
         }
@@ -385,7 +366,7 @@ final class CVTemplatePreviewView: NSView {
385
         header.translatesAutoresizingMaskIntoConstraints = false
366
         header.translatesAutoresizingMaskIntoConstraints = false
386
         header.wantsLayer = true
367
         header.wantsLayer = true
387
         header.layer?.backgroundColor = theme.cgColor
368
         header.layer?.backgroundColor = theme.cgColor
388
-        header.layer?.cornerRadius = idVariant % 2 == 0 ? 5 : 3
369
+        header.layer?.cornerRadius = layoutVariant % 2 == 0 ? 5 : 3
389
 
370
 
390
         let white = NSColor.white
371
         let white = NSColor.white
391
         let name = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 9, weight: .bold), color: white, alignment: .left, maxLines: 1)
372
         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 {
423
             topRow.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -7),
404
             topRow.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -7),
424
             topRow.topAnchor.constraint(equalTo: header.topAnchor, constant: 6),
405
             topRow.topAnchor.constraint(equalTo: header.topAnchor, constant: 6),
425
             topRow.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -6),
406
             topRow.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -6),
426
-            header.heightAnchor.constraint(greaterThanOrEqualToConstant: 32 + CGFloat(idVariant % 3) * 2)
407
+            header.heightAnchor.constraint(greaterThanOrEqualToConstant: 32 + CGFloat(layoutVariant % 3) * 2)
427
         ])
408
         ])
428
 
409
 
429
         let body = modernPrimaryBody(theme: theme)
410
         let body = modernPrimaryBody(theme: theme)
430
         let wrap = NSStackView(views: [header, body])
411
         let wrap = NSStackView(views: [header, body])
431
         wrap.orientation = .vertical
412
         wrap.orientation = .vertical
432
-        wrap.spacing = 6 + CGFloat(idVariant % 2)
413
+        wrap.spacing = 6 + CGFloat(layoutVariant % 2)
433
         wrap.alignment = .leading
414
         wrap.alignment = .leading
434
         return wrap
415
         return wrap
435
     }
416
     }
@@ -443,7 +424,7 @@ final class CVTemplatePreviewView: NSView {
443
         rail.wantsLayer = true
424
         rail.wantsLayer = true
444
         rail.layer?.backgroundColor = theme.cgColor
425
         rail.layer?.backgroundColor = theme.cgColor
445
         rail.layer?.cornerRadius = 2
426
         rail.layer?.cornerRadius = 2
446
-        rail.widthAnchor.constraint(equalToConstant: 3 + CGFloat(idVariant % 2)).isActive = true
427
+        rail.widthAnchor.constraint(equalToConstant: 3 + CGFloat(layoutVariant % 2)).isActive = true
447
 
428
 
448
         let inner = NSStackView()
429
         let inner = NSStackView()
449
         inner.orientation = .vertical
430
         inner.orientation = .vertical
@@ -578,18 +559,18 @@ final class CVTemplatePreviewView: NSView {
578
     private func buildMinimalResume() -> NSView {
559
     private func buildMinimalResume() -> NSView {
579
         let ink = palette.previewInk
560
         let ink = palette.previewInk
580
         let muted = palette.previewMuted
561
         let muted = palette.previewMuted
581
-        let nameWeight: NSFont.Weight = (idVariant % 3 == 0) ? .ultraLight : .light
582
-        let nameSize: CGFloat = template.headline == .centered ? 11 + CGFloat(idVariant % 2) : 9.5 + CGFloat(idVariant % 3)
562
+        let nameWeight: NSFont.Weight = (layoutVariant % 3 == 0) ? .ultraLight : .light
563
+        let nameSize: CGFloat = template.headline == .centered ? 11 + CGFloat(layoutVariant % 2) : 9.5 + CGFloat(layoutVariant % 3)
583
         let name = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: nameSize, weight: nameWeight), color: ink, alignment: .left, maxLines: 1)
564
         let name = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: nameSize, weight: nameWeight), color: ink, alignment: .left, maxLines: 1)
584
         let role = makeLabel(CVPreviewDemoContent.title.uppercased(), font: .systemFont(ofSize: 6.5, weight: .medium), color: muted, alignment: .left, maxLines: 1)
565
         let role = makeLabel(CVPreviewDemoContent.title.uppercased(), font: .systemFont(ofSize: 6.5, weight: .medium), color: muted, alignment: .left, maxLines: 1)
585
         let contact = makeLabel("\(CVPreviewDemoContent.email)   \(CVPreviewDemoContent.phone)", font: .systemFont(ofSize: 5.8, weight: .regular), color: muted.withAlphaComponent(0.75), alignment: .left, maxLines: 1)
566
         let contact = makeLabel("\(CVPreviewDemoContent.email)   \(CVPreviewDemoContent.phone)", font: .systemFont(ofSize: 5.8, weight: .regular), color: muted.withAlphaComponent(0.75), alignment: .left, maxLines: 1)
586
 
567
 
587
         let head = NSStackView()
568
         let head = NSStackView()
588
         head.orientation = .vertical
569
         head.orientation = .vertical
589
-        head.spacing = template.headline == .avatarStacked ? 8 : 4 + CGFloat(idVariant % 4)
570
+        head.spacing = template.headline == .avatarStacked ? 8 : 4 + CGFloat(layoutVariant % 4)
590
         head.alignment = template.headline == .centered ? .centerX : .leading
571
         head.alignment = template.headline == .centered ? .centerX : .leading
591
         if template.headline == .avatarStacked {
572
         if template.headline == .avatarStacked {
592
-            head.addArrangedSubview(initialsAvatar(diameter: 24 + CGFloat(idVariant % 2) * 2, ink: ink))
573
+            head.addArrangedSubview(initialsAvatar(diameter: 24 + CGFloat(layoutVariant % 2) * 2, ink: ink))
593
         }
574
         }
594
         if template.headline == .centered {
575
         if template.headline == .centered {
595
             name.alignment = .center
576
             name.alignment = .center
@@ -600,21 +581,21 @@ final class CVTemplatePreviewView: NSView {
600
         head.addArrangedSubview(role)
581
         head.addArrangedSubview(role)
601
         head.addArrangedSubview(contact)
582
         head.addArrangedSubview(contact)
602
         head.addArrangedSubview(hairlineSoft())
583
         head.addArrangedSubview(hairlineSoft())
603
-        if idVariant % 5 == 1 {
584
+        if layoutVariant % 5 == 1 {
604
             head.addArrangedSubview(hairlineSoft())
585
             head.addArrangedSubview(hairlineSoft())
605
         }
586
         }
606
 
587
 
607
-        let swapEdu = (idVariant % 4) == 2
588
+        let swapEdu = (layoutVariant % 4) == 2
608
         let body: NSView
589
         let body: NSView
609
         switch template.layout {
590
         switch template.layout {
610
         case .singleColumn:
591
         case .singleColumn:
611
-            body = minimalBody(spacing: 6 + CGFloat(idVariant % 3), educationBeforeExperience: swapEdu)
592
+            body = minimalBody(spacing: 6 + CGFloat(layoutVariant % 3), educationBeforeExperience: swapEdu)
612
         case .twoColumn(let side, _):
593
         case .twoColumn(let side, _):
613
             let a = minimalBody(spacing: 5, educationBeforeExperience: swapEdu)
594
             let a = minimalBody(spacing: 5, educationBeforeExperience: swapEdu)
614
-            let b = minimalAside(numbered: idVariant % 3 == 1)
595
+            let b = minimalAside(numbered: layoutVariant % 3 == 1)
615
             let row = NSStackView()
596
             let row = NSStackView()
616
             row.orientation = .horizontal
597
             row.orientation = .horizontal
617
-            row.spacing = 8 + CGFloat(idVariant % 3)
598
+            row.spacing = 8 + CGFloat(layoutVariant % 3)
618
             row.alignment = .top
599
             row.alignment = .top
619
             if side == .leading {
600
             if side == .leading {
620
                 row.addArrangedSubview(b)
601
                 row.addArrangedSubview(b)
@@ -623,14 +604,14 @@ final class CVTemplatePreviewView: NSView {
623
                 row.addArrangedSubview(a)
604
                 row.addArrangedSubview(a)
624
                 row.addArrangedSubview(b)
605
                 row.addArrangedSubview(b)
625
             }
606
             }
626
-            let mult: CGFloat = (idVariant % 5 == 0) ? 0.34 : 0.3
607
+            let mult: CGFloat = (layoutVariant % 5 == 0) ? 0.34 : 0.3
627
             b.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
608
             b.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
628
             body = row
609
             body = row
629
         }
610
         }
630
 
611
 
631
         let wrap = NSStackView(views: [head, body])
612
         let wrap = NSStackView(views: [head, body])
632
         wrap.orientation = .vertical
613
         wrap.orientation = .vertical
633
-        wrap.spacing = 7 + CGFloat(idVariant % 2)
614
+        wrap.spacing = 7 + CGFloat(layoutVariant % 2)
634
         wrap.alignment = .leading
615
         wrap.alignment = .leading
635
         return wrap
616
         return wrap
636
     }
617
     }
@@ -690,13 +671,13 @@ final class CVTemplatePreviewView: NSView {
690
         let ink = palette.previewInk
671
         let ink = palette.previewInk
691
         let muted = palette.previewMuted
672
         let muted = palette.previewMuted
692
         let theme = template.themeColor
673
         let theme = template.themeColor
693
-        let centeredHead = (idVariant % 2) == 0
674
+        let centeredHead = (layoutVariant % 2) == 0
694
 
675
 
695
         let name = makeLabel(CVPreviewDemoContent.fullName, font: serifBold, color: ink, alignment: centeredHead ? .center : .left, maxLines: 1)
676
         let name = makeLabel(CVPreviewDemoContent.fullName, font: serifBold, color: ink, alignment: centeredHead ? .center : .left, maxLines: 1)
696
         let role = makeLabel(CVPreviewDemoContent.title, font: serif, color: muted, alignment: centeredHead ? .center : .left, maxLines: 1)
677
         let role = makeLabel(CVPreviewDemoContent.title, font: serif, color: muted, alignment: centeredHead ? .center : .left, maxLines: 1)
697
         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)
678
         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)
698
 
679
 
699
-        let rule = executiveRule(theme: theme, wide: idVariant % 3 == 0)
680
+        let rule = executiveRule(theme: theme, wide: layoutVariant % 3 == 0)
700
         let head = NSStackView(views: [name, role, contact, rule])
681
         let head = NSStackView(views: [name, role, contact, rule])
701
         head.orientation = .vertical
682
         head.orientation = .vertical
702
         head.spacing = 4
683
         head.spacing = 4
@@ -705,13 +686,13 @@ final class CVTemplatePreviewView: NSView {
705
         let body: NSView
686
         let body: NSView
706
         switch template.layout {
687
         switch template.layout {
707
         case .singleColumn:
688
         case .singleColumn:
708
-            body = executiveBody(serif: serif, ink: ink, muted: muted, theme: theme, compact: false, tightLeading: idVariant % 5 == 2)
689
+            body = executiveBody(serif: serif, ink: ink, muted: muted, theme: theme, compact: false, tightLeading: layoutVariant % 5 == 2)
709
         case .twoColumn(let side, let tinted):
690
         case .twoColumn(let side, let tinted):
710
-            let main = executiveBody(serif: serif, ink: ink, muted: muted, theme: theme, compact: true, tightLeading: idVariant % 5 == 2)
711
-            let sideC = executiveSidebar(serif: serif, tinted: tinted, showMetrics: idVariant % 4 == 1)
691
+            let main = executiveBody(serif: serif, ink: ink, muted: muted, theme: theme, compact: true, tightLeading: layoutVariant % 5 == 2)
692
+            let sideC = executiveSidebar(serif: serif, tinted: tinted, showMetrics: layoutVariant % 4 == 1)
712
             let row = NSStackView()
693
             let row = NSStackView()
713
             row.orientation = .horizontal
694
             row.orientation = .horizontal
714
-            row.spacing = 6 + CGFloat(idVariant % 3)
695
+            row.spacing = 6 + CGFloat(layoutVariant % 3)
715
             row.alignment = .top
696
             row.alignment = .top
716
             if side == .leading {
697
             if side == .leading {
717
                 row.addArrangedSubview(sideC)
698
                 row.addArrangedSubview(sideC)
@@ -720,14 +701,14 @@ final class CVTemplatePreviewView: NSView {
720
                 row.addArrangedSubview(main)
701
                 row.addArrangedSubview(main)
721
                 row.addArrangedSubview(sideC)
702
                 row.addArrangedSubview(sideC)
722
             }
703
             }
723
-            let mult: CGFloat = (idVariant % 5 == 3) ? 0.38 : 0.33
704
+            let mult: CGFloat = (layoutVariant % 5 == 3) ? 0.38 : 0.33
724
             sideC.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
705
             sideC.widthAnchor.constraint(equalTo: row.widthAnchor, multiplier: mult).isActive = true
725
             body = row
706
             body = row
726
         }
707
         }
727
 
708
 
728
         let wrap = NSStackView(views: [head, body])
709
         let wrap = NSStackView(views: [head, body])
729
         wrap.orientation = .vertical
710
         wrap.orientation = .vertical
730
-        wrap.spacing = 6 + CGFloat(idVariant % 2)
711
+        wrap.spacing = 6 + CGFloat(layoutVariant % 2)
731
         wrap.alignment = .leading
712
         wrap.alignment = .leading
732
         return wrap
713
         return wrap
733
     }
714
     }
@@ -747,7 +728,7 @@ final class CVTemplatePreviewView: NSView {
747
         stack.orientation = .vertical
728
         stack.orientation = .vertical
748
         stack.spacing = (compact ? 4 : 5) - (tightLeading ? 1 : 0)
729
         stack.spacing = (compact ? 4 : 5) - (tightLeading ? 1 : 0)
749
         stack.alignment = .leading
730
         stack.alignment = .leading
750
-        let sumTitle = (idVariant % 6 == 3) ? "SUMMARY" : "PROFESSIONAL SUMMARY"
731
+        let sumTitle = (layoutVariant % 6 == 3) ? "SUMMARY" : "PROFESSIONAL SUMMARY"
751
         stack.addArrangedSubview(sectionHeading(sumTitle))
732
         stack.addArrangedSubview(sectionHeading(sumTitle))
752
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: serif, color: ink, alignment: .left, maxLines: 3))
733
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: serif, color: ink, alignment: .left, maxLines: 3))
753
         stack.addArrangedSubview(sectionHeading("SELECTED EXPERIENCE"))
734
         stack.addArrangedSubview(sectionHeading("SELECTED EXPERIENCE"))
@@ -768,11 +749,11 @@ final class CVTemplatePreviewView: NSView {
768
         stack.alignment = .leading
749
         stack.alignment = .leading
769
         if tinted {
750
         if tinted {
770
             stack.wantsLayer = true
751
             stack.wantsLayer = true
771
-            let fill = (idVariant % 3 == 0)
752
+            let fill = (layoutVariant % 3 == 0)
772
                 ? NSColor(srgbRed: 0.97, green: 0.97, blue: 0.98, alpha: 1)
753
                 ? NSColor(srgbRed: 0.97, green: 0.97, blue: 0.98, alpha: 1)
773
                 : template.themeColor.withAlphaComponent(0.07)
754
                 : template.themeColor.withAlphaComponent(0.07)
774
             stack.layer?.backgroundColor = fill.cgColor
755
             stack.layer?.backgroundColor = fill.cgColor
775
-            stack.layer?.cornerRadius = idVariant % 4 == 2 ? 5 : 3
756
+            stack.layer?.cornerRadius = layoutVariant % 4 == 2 ? 5 : 3
776
             stack.edgeInsets = NSEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
757
             stack.edgeInsets = NSEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
777
         }
758
         }
778
         stack.addArrangedSubview(sectionHeading("CORE COMPETENCIES"))
759
         stack.addArrangedSubview(sectionHeading("CORE COMPETENCIES"))
@@ -794,18 +775,18 @@ final class CVTemplatePreviewView: NSView {
794
         let theme = template.themeColor
775
         let theme = template.themeColor
795
         let deep = creativeDeepBackground(theme: theme)
776
         let deep = creativeDeepBackground(theme: theme)
796
         let onSidebar = NSColor.white.withAlphaComponent(0.95)
777
         let onSidebar = NSColor.white.withAlphaComponent(0.95)
797
-        let skillPrefix = (idVariant % 3 == 0) ? "•  " : "▸  "
778
+        let skillPrefix = (layoutVariant % 3 == 0) ? "•  " : "▸  "
798
 
779
 
799
         let sidebar = NSStackView()
780
         let sidebar = NSStackView()
800
         sidebar.orientation = .vertical
781
         sidebar.orientation = .vertical
801
-        sidebar.spacing = 4 + CGFloat(idVariant % 3)
782
+        sidebar.spacing = 4 + CGFloat(layoutVariant % 3)
802
         sidebar.alignment = .leading
783
         sidebar.alignment = .leading
803
         sidebar.wantsLayer = true
784
         sidebar.wantsLayer = true
804
         sidebar.layer?.backgroundColor = deep.cgColor
785
         sidebar.layer?.backgroundColor = deep.cgColor
805
-        sidebar.layer?.cornerRadius = idVariant % 2 == 0 ? 6 : 4
786
+        sidebar.layer?.cornerRadius = layoutVariant % 2 == 0 ? 6 : 4
806
         sidebar.edgeInsets = NSEdgeInsets(top: 6, left: 6, bottom: 6, right: 6)
787
         sidebar.edgeInsets = NSEdgeInsets(top: 6, left: 6, bottom: 6, right: 6)
807
 
788
 
808
-        let sbTitle = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 7.5 + CGFloat(idVariant % 2), weight: .bold), color: onSidebar, alignment: .left, maxLines: 2)
789
+        let sbTitle = makeLabel(CVPreviewDemoContent.fullName, font: .systemFont(ofSize: 7.5 + CGFloat(layoutVariant % 2), weight: .bold), color: onSidebar, alignment: .left, maxLines: 2)
809
         sidebar.addArrangedSubview(sbTitle)
790
         sidebar.addArrangedSubview(sbTitle)
810
         sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 6.5, weight: .medium), color: onSidebar.withAlphaComponent(0.85), alignment: .left, maxLines: 2))
791
         sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 6.5, weight: .medium), color: onSidebar.withAlphaComponent(0.85), alignment: .left, maxLines: 2))
811
         sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 5.8), color: onSidebar.withAlphaComponent(0.8), alignment: .left, maxLines: 1))
792
         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 {
817
 
798
 
818
         let main = NSStackView()
799
         let main = NSStackView()
819
         main.orientation = .vertical
800
         main.orientation = .vertical
820
-        main.spacing = 4 + CGFloat(idVariant % 3)
801
+        main.spacing = 4 + CGFloat(layoutVariant % 3)
821
         main.alignment = .leading
802
         main.alignment = .leading
822
         main.addArrangedSubview(creativeMainHeader(theme: theme))
803
         main.addArrangedSubview(creativeMainHeader(theme: theme))
823
         main.addArrangedSubview(sectionHeading("PROFILE"))
804
         main.addArrangedSubview(sectionHeading("PROFILE"))
824
         main.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 3))
805
         main.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 3))
825
         main.addArrangedSubview(sectionHeading("IMPACT"))
806
         main.addArrangedSubview(sectionHeading("IMPACT"))
826
         main.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.company) — \(CVPreviewDemoContent.experienceRole)", font: .systemFont(ofSize: 6.6, weight: .heavy), color: palette.previewInk, alignment: .left, maxLines: 2))
807
         main.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.company) — \(CVPreviewDemoContent.experienceRole)", font: .systemFont(ofSize: 6.6, weight: .heavy), color: palette.previewInk, alignment: .left, maxLines: 2))
827
-        let bMark = (idVariant % 2 == 0) ? "—  " : "▸  "
808
+        let bMark = (layoutVariant % 2 == 0) ? "—  " : "▸  "
828
         main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet1)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
809
         main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet1)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
829
         main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet2)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
810
         main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet2)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
830
         main.addArrangedSubview(sectionHeading("EDUCATION"))
811
         main.addArrangedSubview(sectionHeading("EDUCATION"))
831
         main.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.university) · \(CVPreviewDemoContent.degree) · \(CVPreviewDemoContent.educationYears)", font: .systemFont(ofSize: 6.1), color: palette.previewInk, alignment: .left, maxLines: 2))
812
         main.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.university) · \(CVPreviewDemoContent.degree) · \(CVPreviewDemoContent.educationYears)", font: .systemFont(ofSize: 6.1), color: palette.previewInk, alignment: .left, maxLines: 2))
832
 
813
 
833
-        let sidebarMult = 0.32 + CGFloat(idVariant % 3) * 0.02
814
+        let sidebarMult = 0.32 + CGFloat(layoutVariant % 3) * 0.02
834
 
815
 
835
         switch template.layout {
816
         switch template.layout {
836
         case .singleColumn:
817
         case .singleColumn:
@@ -838,14 +819,14 @@ final class CVTemplatePreviewView: NSView {
838
             banner.translatesAutoresizingMaskIntoConstraints = false
819
             banner.translatesAutoresizingMaskIntoConstraints = false
839
             banner.wantsLayer = true
820
             banner.wantsLayer = true
840
             banner.layer?.backgroundColor = theme.cgColor
821
             banner.layer?.backgroundColor = theme.cgColor
841
-            banner.layer?.cornerRadius = idVariant % 4 == 1 ? 6 : 3
822
+            banner.layer?.cornerRadius = layoutVariant % 4 == 1 ? 6 : 3
842
             let inner = makeLabel("  \(CVPreviewDemoContent.fullName)  ·  \(CVPreviewDemoContent.title)", font: .systemFont(ofSize: 6.5, weight: .bold), color: .white, alignment: .left, maxLines: 1)
823
             let inner = makeLabel("  \(CVPreviewDemoContent.fullName)  ·  \(CVPreviewDemoContent.title)", font: .systemFont(ofSize: 6.5, weight: .bold), color: .white, alignment: .left, maxLines: 1)
843
             inner.translatesAutoresizingMaskIntoConstraints = false
824
             inner.translatesAutoresizingMaskIntoConstraints = false
844
             banner.addSubview(inner)
825
             banner.addSubview(inner)
845
             NSLayoutConstraint.activate([
826
             NSLayoutConstraint.activate([
846
                 inner.leadingAnchor.constraint(equalTo: banner.leadingAnchor, constant: 5),
827
                 inner.leadingAnchor.constraint(equalTo: banner.leadingAnchor, constant: 5),
847
                 inner.trailingAnchor.constraint(lessThanOrEqualTo: banner.trailingAnchor, constant: -5),
828
                 inner.trailingAnchor.constraint(lessThanOrEqualTo: banner.trailingAnchor, constant: -5),
848
-                inner.topAnchor.constraint(equalTo: banner.topAnchor, constant: 4 + CGFloat(idVariant % 2)),
829
+                inner.topAnchor.constraint(equalTo: banner.topAnchor, constant: 4 + CGFloat(layoutVariant % 2)),
849
                 inner.bottomAnchor.constraint(equalTo: banner.bottomAnchor, constant: -4)
830
                 inner.bottomAnchor.constraint(equalTo: banner.bottomAnchor, constant: -4)
850
             ])
831
             ])
851
             let col = NSStackView(views: [banner, main])
832
             let col = NSStackView(views: [banner, main])
@@ -856,7 +837,7 @@ final class CVTemplatePreviewView: NSView {
856
         case .twoColumn(let side, _):
837
         case .twoColumn(let side, _):
857
             let row = NSStackView()
838
             let row = NSStackView()
858
             row.orientation = .horizontal
839
             row.orientation = .horizontal
859
-            row.spacing = 5 + CGFloat(idVariant % 3)
840
+            row.spacing = 5 + CGFloat(layoutVariant % 3)
860
             row.alignment = .top
841
             row.alignment = .top
861
             if side == .leading {
842
             if side == .leading {
862
                 row.addArrangedSubview(sidebar)
843
                 row.addArrangedSubview(sidebar)
@@ -873,7 +854,7 @@ final class CVTemplatePreviewView: NSView {
873
     private func creativeDeepBackground(theme: NSColor) -> NSColor {
854
     private func creativeDeepBackground(theme: NSColor) -> NSColor {
874
         let navy = NSColor(srgbRed: 0.08, green: 0.1, blue: 0.18, alpha: 1)
855
         let navy = NSColor(srgbRed: 0.08, green: 0.1, blue: 0.18, alpha: 1)
875
         let plum = NSColor(srgbRed: 0.14, green: 0.07, blue: 0.24, alpha: 1)
856
         let plum = NSColor(srgbRed: 0.14, green: 0.07, blue: 0.24, alpha: 1)
876
-        switch idVariant % 4 {
857
+        switch layoutVariant % 4 {
877
         case 0: return theme.blended(withFraction: 0.52, of: navy) ?? theme
858
         case 0: return theme.blended(withFraction: 0.52, of: navy) ?? theme
878
         case 1: return theme.blended(withFraction: 0.7, of: NSColor.black) ?? theme
859
         case 1: return theme.blended(withFraction: 0.7, of: NSColor.black) ?? theme
879
         case 2: return palette.previewInk.blended(withFraction: 0.38, of: theme) ?? theme
860
         case 2: return palette.previewInk.blended(withFraction: 0.38, of: theme) ?? theme
@@ -944,11 +925,7 @@ final class CVTemplatePreviewView: NSView {
944
     }
925
     }
945
 
926
 
946
     private func accentDecorationColor() -> NSColor {
927
     private func accentDecorationColor() -> NSColor {
947
-        switch template.accent {
948
-        case .redUnderline, .redBar: return palette.previewAccentRed
949
-        case .blueBar: return palette.previewAccentBlue
950
-        case .none: return template.themeColor.blended(withFraction: 0.5, of: palette.previewInk) ?? palette.previewInk
951
-        }
928
+        CVResumeAppearance.accentColor(for: template)
952
     }
929
     }
953
 
930
 
954
     private func headlineAccent(theme: NSColor, width: CGFloat) -> NSView {
931
     private func headlineAccent(theme: NSColor, width: CGFloat) -> NSView {