Quellcode durchsuchen

Apply dark theme to Profile hub, editor, and CV preview.

Wire profiles list, profile editor, and filled CV preview chrome to AppDashboardTheme so they match the dashboard when appearance changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 vor 2 Monaten
Ursprung
Commit
fdb581a4ba

+ 12 - 0
App for Indeed/Services/AppDashboardTheme.swift

@@ -287,4 +287,16 @@ enum AppDashboardTheme {
             ? NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 60 / 255, alpha: 1)
             : NSColor.white.withAlphaComponent(0.65)
     }
+
+    // MARK: Profile hub & editor
+
+    static var profileFieldFill: NSColor {
+        isDark
+            ? NSColor(srgbRed: 52 / 255, green: 52 / 255, blue: 54 / 255, alpha: 1)
+            : NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1)
+    }
+
+    static var profileDestructive: NSColor {
+        NSColor(srgbRed: 220 / 255, green: 38 / 255, blue: 38 / 255, alpha: 1)
+    }
 }

+ 51 - 17
App for Indeed/Views/CVFilledPreviewPageView.swift

@@ -14,8 +14,9 @@ private final class CVPreviewFlippedDocumentView: NSView {
 
 /// Same metrics as job cards’ **Apply** (`DashboardView` `JobPayloadButton`): 13pt semibold, 32pt tall, 8pt corners.
 private final class CVPreviewPrimaryCTAButton: NSButton {
-    private static let fill = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
-    private static let fillHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
+    private static var fill: NSColor { AppDashboardTheme.brandBlue }
+    private static var fillHover: NSColor { AppDashboardTheme.brandBlueHover }
+    private static var labelColor: NSColor { AppDashboardTheme.proCTAText }
     /// Slightly wider than default title metrics so the label is not flush to the pill edges.
     private static let horizontalOutset: CGFloat = 20
 
@@ -34,7 +35,7 @@ private final class CVPreviewPrimaryCTAButton: NSButton {
         wantsLayer = true
         layer?.cornerRadius = 8
         layer?.backgroundColor = Self.fill.cgColor
-        contentTintColor = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
+        contentTintColor = Self.labelColor
         focusRingType = .none
         setContentHuggingPriority(.required, for: .horizontal)
         setContentCompressionResistancePriority(.required, for: .horizontal)
@@ -64,9 +65,17 @@ private final class CVPreviewPrimaryCTAButton: NSButton {
         trackingAreaRef = ta
     }
 
+    func applyCurrentAppearance() {
+        contentTintColor = Self.labelColor
+        layer?.backgroundColor = (isHovering ? Self.fillHover : Self.fill).cgColor
+    }
+
+    private var isHovering = false
+
     override func mouseEntered(with event: NSEvent) {
         super.mouseEntered(with: event)
-        layer?.backgroundColor = Self.fillHover.cgColor
+        isHovering = true
+        applyCurrentAppearance()
         if !didPushCursor {
             NSCursor.pointingHand.push()
             didPushCursor = true
@@ -75,7 +84,8 @@ private final class CVPreviewPrimaryCTAButton: NSButton {
 
     override func mouseExited(with event: NSEvent) {
         super.mouseExited(with: event)
-        layer?.backgroundColor = Self.fill.cgColor
+        isHovering = false
+        applyCurrentAppearance()
         if didPushCursor {
             NSCursor.pop()
             didPushCursor = false
@@ -107,41 +117,37 @@ final class CVFilledPreviewPageView: NSView {
     private var lastProfile: SavedProfile?
     private var lastTemplate: CVTemplate?
 
-    private static let pageBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
-    private static let secondaryText = NSColor(srgbRed: 100 / 255, green: 116 / 255, blue: 139 / 255, alpha: 1)
-    private static let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
+    private let subtitleLabel = NSTextField(wrappingLabelWithString: "")
+    private var appearanceObserver: NSObjectProtocol?
 
     override init(frame frameRect: NSRect) {
         super.init(frame: frameRect)
         wantsLayer = true
-        layer?.backgroundColor = Self.pageBackground.cgColor
         userInterfaceLayoutDirection = .leftToRight
 
         backButton.translatesAutoresizingMaskIntoConstraints = false
         backButton.bezelStyle = .rounded
         backButton.isBordered = false
         backButton.font = .systemFont(ofSize: 13, weight: .semibold)
-        backButton.contentTintColor = Self.brandBlue
+        backButton.contentTintColor = AppDashboardTheme.brandBlue
         backButton.target = self
         backButton.action = #selector(didTapBack)
 
         titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
-        titleLabel.textColor = NSColor(srgbRed: 31 / 255, green: 41 / 255, blue: 55 / 255, alpha: 1)
 
         exportButton.target = self
         exportButton.action = #selector(didTapExportPDF)
 
-        let subtitle = NSTextField(wrappingLabelWithString: "Layout matches the CV Maker thumbnail for this template. Export a PDF that matches what you see here (fonts, columns, colours, and rules).")
-        subtitle.font = .systemFont(ofSize: 12, weight: .regular)
-        subtitle.textColor = Self.secondaryText
-        subtitle.maximumNumberOfLines = 0
+        subtitleLabel.stringValue = "Layout matches the CV Maker thumbnail for this template. Export a PDF that matches what you see here (fonts, columns, colours, and rules)."
+        subtitleLabel.font = .systemFont(ofSize: 12, weight: .regular)
+        subtitleLabel.maximumNumberOfLines = 0
 
-        let headerCol = NSStackView(views: [backButton, titleLabel, subtitle, exportButton])
+        let headerCol = NSStackView(views: [backButton, titleLabel, subtitleLabel, exportButton])
         headerCol.orientation = .vertical
         headerCol.alignment = .leading
         headerCol.spacing = 6
         headerCol.setCustomSpacing(14, after: backButton)
-        headerCol.setCustomSpacing(10, after: subtitle)
+        headerCol.setCustomSpacing(10, after: subtitleLabel)
         headerCol.translatesAutoresizingMaskIntoConstraints = false
 
         contentStack.orientation = .vertical
@@ -186,6 +192,21 @@ final class CVFilledPreviewPageView: NSView {
             contentStack.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 8),
             contentStack.widthAnchor.constraint(lessThanOrEqualTo: documentView.widthAnchor, constant: -64)
         ])
+
+        appearanceObserver = NotificationCenter.default.addObserver(
+            forName: AppAppearanceManager.didChangeNotification,
+            object: nil,
+            queue: .main
+        ) { [weak self] _ in
+            self?.applyCurrentAppearance()
+        }
+        applyCurrentAppearance()
+    }
+
+    deinit {
+        if let appearanceObserver {
+            NotificationCenter.default.removeObserver(appearanceObserver)
+        }
     }
 
     @available(*, unavailable)
@@ -193,6 +214,19 @@ final class CVFilledPreviewPageView: NSView {
         fatalError("init(coder:) has not been implemented")
     }
 
+    override func viewDidChangeEffectiveAppearance() {
+        super.viewDidChangeEffectiveAppearance()
+        applyCurrentAppearance()
+    }
+
+    func applyCurrentAppearance() {
+        layer?.backgroundColor = AppDashboardTheme.pageBackground.cgColor
+        backButton.contentTintColor = AppDashboardTheme.brandBlue
+        titleLabel.textColor = AppDashboardTheme.primaryText
+        subtitleLabel.textColor = AppDashboardTheme.secondaryText
+        exportButton.applyCurrentAppearance()
+    }
+
     func configure(profile: SavedProfile, template: CVTemplate) {
         lastProfile = profile
         lastTemplate = template

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

@@ -279,6 +279,9 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
 
         appearanceModeSegment?.selectedSegment = AppAppearanceManager.shared.mode.segmentIndex
         cvMakerPageView.applyCurrentAppearance()
+        profilesListPageView.applyCurrentAppearance()
+        myProfilePageView.applyCurrentAppearance()
+        cvFilledPreviewPageView.applyCurrentAppearance()
         refreshSettingsPageAppearance(in: settingsPageContainer)
         rebuildFeatureShortcutCards()
         configureSidebar()

+ 126 - 18
App for Indeed/Views/MyProfilePageView.swift

@@ -2,22 +2,71 @@
 //  MyProfilePageView.swift
 //  App for Indeed
 //
-//  Light-theme profile editor: card layout, adaptive two-column rows, and
-//  vertical scrolling when the window is short.
+//  Profile editor: card layout, adaptive two-column rows, and vertical scrolling
+//  when the window is short. Follows the active dashboard light / dark appearance.
 //
 
 import Cocoa
 
 private enum ProfilePagePalette {
-    static let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
-    static let brandBlueHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
-    static let pageBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
-    static let cardBackground = NSColor(srgbRed: 252 / 255, green: 252 / 255, blue: 252 / 255, alpha: 1)
-    static let fieldFill = NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1)
-    static let primaryText = NSColor(srgbRed: 45 / 255, green: 45 / 255, blue: 45 / 255, alpha: 1)
-    static let secondaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
-    static let border = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
-    static let destructive = NSColor(srgbRed: 220 / 255, green: 38 / 255, blue: 38 / 255, alpha: 1)
+    static var brandBlue: NSColor { AppDashboardTheme.brandBlue }
+    static var brandBlueHover: NSColor { AppDashboardTheme.brandBlueHover }
+    static var pageBackground: NSColor { AppDashboardTheme.pageBackground }
+    static var cardBackground: NSColor { AppDashboardTheme.cardBackground }
+    static var fieldFill: NSColor { AppDashboardTheme.profileFieldFill }
+    static var primaryText: NSColor { AppDashboardTheme.primaryText }
+    static var secondaryText: NSColor { AppDashboardTheme.secondaryText }
+    static var border: NSColor { AppDashboardTheme.border }
+    static var destructive: NSColor { AppDashboardTheme.profileDestructive }
+    static var ctaText: NSColor { AppDashboardTheme.proCTAText }
+}
+
+private enum ProfileThemeAppearance {
+    static func refreshFormSubtree(_ root: NSView) {
+        for view in root.profileSubviewsRecursive() {
+            if let field = view as? NSTextField {
+                if field.isEditable {
+                    field.textColor = ProfilePagePalette.primaryText
+                    if let placeholder = field.placeholderAttributedString?.string, !placeholder.isEmpty {
+                        field.placeholderAttributedString = NSAttributedString(
+                            string: placeholder,
+                            attributes: [
+                                .foregroundColor: ProfilePagePalette.secondaryText,
+                                .font: field.font ?? NSFont.systemFont(ofSize: 14, weight: .regular),
+                                .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle()
+                            ]
+                        )
+                    }
+                } else if let font = field.font {
+                    if font.pointSize >= 15 {
+                        field.textColor = ProfilePagePalette.primaryText
+                    } else {
+                        field.textColor = ProfilePagePalette.secondaryText
+                    }
+                }
+            }
+            guard view.wantsLayer, let layer = view.layer else { continue }
+            if layer.cornerRadius == 10, layer.borderWidth == 1 {
+                layer.backgroundColor = ProfilePagePalette.fieldFill.cgColor
+                layer.borderColor = ProfilePagePalette.border.cgColor
+            } else if layer.cornerRadius == 14, layer.borderWidth == 1 {
+                layer.backgroundColor = ProfilePagePalette.cardBackground.cgColor
+                layer.borderColor = ProfilePagePalette.border.cgColor
+            }
+        }
+    }
+}
+
+private extension NSView {
+    func profileSubviewsRecursive() -> [NSView] {
+        var result: [NSView] = []
+        var stack: [NSView] = [self]
+        while let view = stack.popLast() {
+            result.append(view)
+            stack.append(contentsOf: view.subviews)
+        }
+        return result
+    }
 }
 
 /// Keeps profile text left-aligned and LTR so fields do not collapse to a narrow trailing strip under RTL / natural alignment.
@@ -168,6 +217,7 @@ final class MyProfilePageView: NSView {
     private var lastCompactLayout: Bool?
 
     private var referralHelperLabel: NSTextField?
+    private var appearanceObserver: NSObjectProtocol?
 
     /// Force left-to-right geometry so profile fields span the full width even when the window uses RTL layout.
     override var userInterfaceLayoutDirection: NSUserInterfaceLayoutDirection {
@@ -178,6 +228,20 @@ final class MyProfilePageView: NSView {
     override init(frame frameRect: NSRect) {
         super.init(frame: frameRect)
         setup()
+        appearanceObserver = NotificationCenter.default.addObserver(
+            forName: AppAppearanceManager.didChangeNotification,
+            object: nil,
+            queue: .main
+        ) { [weak self] _ in
+            self?.applyCurrentAppearance()
+        }
+        applyCurrentAppearance()
+    }
+
+    deinit {
+        if let appearanceObserver {
+            NotificationCenter.default.removeObserver(appearanceObserver)
+        }
     }
 
     required init?(coder: NSCoder) {
@@ -185,6 +249,23 @@ final class MyProfilePageView: NSView {
         setup()
     }
 
+    override func viewDidChangeEffectiveAppearance() {
+        super.viewDidChangeEffectiveAppearance()
+        applyCurrentAppearance()
+    }
+
+    func applyCurrentAppearance() {
+        layer?.backgroundColor = ProfilePagePalette.pageBackground.cgColor
+        cardView.layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor
+        cardView.layer?.borderColor = ProfilePagePalette.border.cgColor
+        backButton.contentTintColor = ProfilePagePalette.brandBlue
+        contextLabel.textColor = ProfilePagePalette.primaryText
+        ProfileThemeAppearance.refreshFormSubtree(formStack)
+        for entry in workExperienceEntries { entry.applyCurrentAppearance() }
+        for entry in educationEntries { entry.applyCurrentAppearance() }
+        saveButton.applyCurrentAppearance()
+    }
+
     override func viewDidMoveToWindow() {
         super.viewDidMoveToWindow()
         guard window != nil else { return }
@@ -1224,6 +1305,14 @@ private final class WorkExperienceEntryView: NSView {
         onDelete?()
     }
 
+    func applyCurrentAppearance() {
+        layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor
+        layer?.borderColor = ProfilePagePalette.border.cgColor
+        subtitleLabel.textColor = ProfilePagePalette.secondaryText
+        deleteButton.contentTintColor = ProfilePagePalette.destructive
+        ProfileThemeAppearance.refreshFormSubtree(self)
+    }
+
     fileprivate static func labeledFieldStack(title: String, field: NSTextField, placeholder: String) -> NSView {
         let label = NSTextField(labelWithString: title)
         label.font = .systemFont(ofSize: 12, weight: .medium)
@@ -1511,6 +1600,14 @@ private final class EducationEntryView: NSView {
     @objc private func didTapDelete() {
         onDelete?()
     }
+
+    func applyCurrentAppearance() {
+        layer?.backgroundColor = ProfilePagePalette.cardBackground.cgColor
+        layer?.borderColor = ProfilePagePalette.border.cgColor
+        subtitleLabel.textColor = ProfilePagePalette.secondaryText
+        deleteButton.contentTintColor = ProfilePagePalette.destructive
+        ProfileThemeAppearance.refreshFormSubtree(self)
+    }
 }
 
 // MARK: - Primary CTA
@@ -1518,6 +1615,7 @@ private final class EducationEntryView: NSView {
 private final class ProfilePrimaryButton: NSButton {
     private var trackingArea: NSTrackingArea?
     private var didPushCursor = false
+    private var isHovering = false
 
     override init(frame frameRect: NSRect) {
         super.init(frame: frameRect)
@@ -1540,13 +1638,18 @@ private final class ProfilePrimaryButton: NSButton {
         bezelStyle = .rounded
         isBordered = false
         font = .systemFont(ofSize: 16, weight: .semibold)
-        contentTintColor = .white
+        contentTintColor = ProfilePagePalette.ctaText
         wantsLayer = true
         layer?.cornerRadius = 14
         if #available(macOS 11.0, *) {
             layer?.cornerCurve = .continuous
         }
-        layer?.backgroundColor = ProfilePagePalette.brandBlue.cgColor
+        applyCurrentAppearance()
+    }
+
+    func applyCurrentAppearance() {
+        contentTintColor = ProfilePagePalette.ctaText
+        layer?.backgroundColor = (isHovering ? ProfilePagePalette.brandBlueHover : ProfilePagePalette.brandBlue).cgColor
     }
 
     override func updateTrackingAreas() {
@@ -1564,7 +1667,8 @@ private final class ProfilePrimaryButton: NSButton {
 
     override func mouseEntered(with event: NSEvent) {
         super.mouseEntered(with: event)
-        layer?.backgroundColor = ProfilePagePalette.brandBlueHover.cgColor
+        isHovering = true
+        applyCurrentAppearance()
         if !didPushCursor {
             NSCursor.pointingHand.push()
             didPushCursor = true
@@ -1573,7 +1677,8 @@ private final class ProfilePrimaryButton: NSButton {
 
     override func mouseExited(with event: NSEvent) {
         super.mouseExited(with: event)
-        layer?.backgroundColor = ProfilePagePalette.brandBlue.cgColor
+        isHovering = false
+        applyCurrentAppearance()
         if didPushCursor {
             NSCursor.pop()
             didPushCursor = false
@@ -1582,9 +1687,12 @@ private final class ProfilePrimaryButton: NSButton {
 
     override func viewWillMove(toWindow newWindow: NSWindow?) {
         super.viewWillMove(toWindow: newWindow)
-        if newWindow == nil, didPushCursor {
-            NSCursor.pop()
-            didPushCursor = false
+        if newWindow == nil {
+            isHovering = false
+            if didPushCursor {
+                NSCursor.pop()
+                didPushCursor = false
+            }
         }
     }
 }

+ 78 - 28
App for Indeed/Views/ProfilesListPageView.swift

@@ -6,14 +6,15 @@
 import Cocoa
 
 private enum ProfilesListPalette {
-    static let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
-    static let brandBlueHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
-    static let pageBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
-    static let cardBackground = NSColor(srgbRed: 252 / 255, green: 252 / 255, blue: 252 / 255, alpha: 1)
-    static let primaryText = NSColor(srgbRed: 45 / 255, green: 45 / 255, blue: 45 / 255, alpha: 1)
-    static let secondaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
-    static let border = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
-    static let destructive = NSColor(srgbRed: 220 / 255, green: 38 / 255, blue: 38 / 255, alpha: 1)
+    static var brandBlue: NSColor { AppDashboardTheme.brandBlue }
+    static var brandBlueHover: NSColor { AppDashboardTheme.brandBlueHover }
+    static var pageBackground: NSColor { AppDashboardTheme.pageBackground }
+    static var cardBackground: NSColor { AppDashboardTheme.cardBackground }
+    static var primaryText: NSColor { AppDashboardTheme.primaryText }
+    static var secondaryText: NSColor { AppDashboardTheme.secondaryText }
+    static var border: NSColor { AppDashboardTheme.border }
+    static var destructive: NSColor { AppDashboardTheme.profileDestructive }
+    static var ctaText: NSColor { AppDashboardTheme.proCTAText }
 }
 
 /// Document view for the profiles `NSScrollView`; flipped coordinates keep short content aligned to the top of the clip (avoids a large empty band above the content on macOS).
@@ -32,9 +33,12 @@ final class ProfilesListPageView: NSView {
     private let scrollView = NSScrollView()
     private let documentView = ProfilesListDocumentView()
     private let contentStack = NSStackView()
+    private let titleLabel = NSTextField(labelWithString: "Profiles")
+    private let subtitleLabel = NSTextField(wrappingLabelWithString: "")
     private let emptyStateLabel = NSTextField(wrappingLabelWithString: "")
     private let addButton = ProfilesPrimaryButton(title: "Add new profile", target: nil, action: nil)
     private let pendingFlowLabel = NSTextField(wrappingLabelWithString: "")
+    private var appearanceObserver: NSObjectProtocol?
     /// Non-`nil` after **Use Template & Select Profile** until the dashboard clears the flow (e.g. leaving Profile).
     private var pendingCVTemplateDisplayName: String?
 
@@ -46,6 +50,20 @@ final class ProfilesListPageView: NSView {
     override init(frame frameRect: NSRect) {
         super.init(frame: frameRect)
         setup()
+        appearanceObserver = NotificationCenter.default.addObserver(
+            forName: AppAppearanceManager.didChangeNotification,
+            object: nil,
+            queue: .main
+        ) { [weak self] _ in
+            self?.applyCurrentAppearance()
+        }
+        applyCurrentAppearance()
+    }
+
+    deinit {
+        if let appearanceObserver {
+            NotificationCenter.default.removeObserver(appearanceObserver)
+        }
     }
 
     required init?(coder: NSCoder) {
@@ -53,6 +71,23 @@ final class ProfilesListPageView: NSView {
         setup()
     }
 
+    override func viewDidChangeEffectiveAppearance() {
+        super.viewDidChangeEffectiveAppearance()
+        applyCurrentAppearance()
+    }
+
+    func applyCurrentAppearance() {
+        layer?.backgroundColor = ProfilesListPalette.pageBackground.cgColor
+        titleLabel.textColor = ProfilesListPalette.primaryText
+        subtitleLabel.textColor = ProfilesListPalette.secondaryText
+        emptyStateLabel.textColor = ProfilesListPalette.secondaryText
+        pendingFlowLabel.textColor = ProfilesListPalette.brandBlue
+        addButton.applyCurrentAppearance()
+        for case let row as ProfileListRowView in contentStack.arrangedSubviews {
+            row.applyCurrentAppearance()
+        }
+    }
+
     func reloadFromStore() {
         for row in contentStack.arrangedSubviews {
             contentStack.removeArrangedSubview(row)
@@ -91,14 +126,11 @@ final class ProfilesListPageView: NSView {
         layer?.backgroundColor = ProfilesListPalette.pageBackground.cgColor
         userInterfaceLayoutDirection = .leftToRight
 
-        let title = NSTextField(labelWithString: "Profiles")
-        title.font = .systemFont(ofSize: 22, weight: .semibold)
-        title.textColor = ProfilesListPalette.primaryText
+        titleLabel.font = .systemFont(ofSize: 22, weight: .semibold)
 
-        let subtitle = NSTextField(wrappingLabelWithString: "Create and manage CV profiles. Each profile stores your details on this Mac.")
-        subtitle.font = .systemFont(ofSize: 13, weight: .regular)
-        subtitle.textColor = ProfilesListPalette.secondaryText
-        subtitle.maximumNumberOfLines = 0
+        subtitleLabel.stringValue = "Create and manage CV profiles. Each profile stores your details on this Mac."
+        subtitleLabel.font = .systemFont(ofSize: 13, weight: .regular)
+        subtitleLabel.maximumNumberOfLines = 0
 
         emptyStateLabel.stringValue = "No profiles yet. Tap “Add new profile” to create your first one."
         emptyStateLabel.font = .systemFont(ofSize: 13, weight: .regular)
@@ -115,7 +147,7 @@ final class ProfilesListPageView: NSView {
             addButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 76)
         ])
 
-        let titleSubtitleStack = NSStackView(views: [title, subtitle])
+        let titleSubtitleStack = NSStackView(views: [titleLabel, subtitleLabel])
         titleSubtitleStack.orientation = .vertical
         titleSubtitleStack.alignment = .leading
         titleSubtitleStack.spacing = 10
@@ -203,6 +235,8 @@ private final class ProfileListRowView: NSView {
     var onBuildCV: ((UUID) -> Void)?
 
     private let profileID: UUID
+    private let nameLabel = NSTextField(labelWithString: "")
+    private let detailLabel = NSTextField(wrappingLabelWithString: "")
     private let buildCVButton = NSButton(title: "Build CV", target: nil, action: nil)
     private let editButton = NSButton(title: "Edit", target: nil, action: nil)
     private let deleteButton = NSButton(title: "Delete", target: nil, action: nil)
@@ -220,17 +254,15 @@ private final class ProfileListRowView: NSView {
             layer?.cornerCurve = .continuous
         }
 
-        let name = NSTextField(labelWithString: profile.profileDisplayName.isEmpty ? "Untitled profile" : profile.profileDisplayName)
-        name.font = .systemFont(ofSize: 15, weight: .semibold)
-        name.textColor = ProfilesListPalette.primaryText
+        nameLabel.stringValue = profile.profileDisplayName.isEmpty ? "Untitled profile" : profile.profileDisplayName
+        nameLabel.font = .systemFont(ofSize: 15, weight: .semibold)
 
         let detailParts = [profile.personal.fullName, profile.personal.email].filter { !$0.isEmpty }
-        let detail = NSTextField(wrappingLabelWithString: detailParts.isEmpty ? "No contact details yet" : detailParts.joined(separator: " · "))
-        detail.font = .systemFont(ofSize: 12, weight: .regular)
-        detail.textColor = ProfilesListPalette.secondaryText
-        detail.maximumNumberOfLines = 2
+        detailLabel.stringValue = detailParts.isEmpty ? "No contact details yet" : detailParts.joined(separator: " · ")
+        detailLabel.font = .systemFont(ofSize: 12, weight: .regular)
+        detailLabel.maximumNumberOfLines = 2
 
-        let textStack = NSStackView(views: [name, detail])
+        let textStack = NSStackView(views: [nameLabel, detailLabel])
         textStack.orientation = .vertical
         textStack.alignment = .leading
         textStack.spacing = 4
@@ -300,6 +332,15 @@ private final class ProfileListRowView: NSView {
     @objc private func didTapDelete() {
         onDelete?(profileID)
     }
+
+    func applyCurrentAppearance() {
+        layer?.borderColor = ProfilesListPalette.border.cgColor
+        layer?.backgroundColor = ProfilesListPalette.cardBackground.cgColor
+        nameLabel.textColor = ProfilesListPalette.primaryText
+        detailLabel.textColor = ProfilesListPalette.secondaryText
+        buildCVButton.contentTintColor = ProfilesListPalette.brandBlue
+        deleteButton.contentTintColor = ProfilesListPalette.destructive
+    }
 }
 
 // MARK: - Primary CTA (matches job cards’ Apply: 13pt semibold, 32pt tall, 8pt corners)
@@ -332,13 +373,20 @@ private final class ProfilesPrimaryButton: NSButton {
         bezelStyle = .rounded
         isBordered = false
         font = .systemFont(ofSize: 13, weight: .semibold)
-        contentTintColor = .white
+        contentTintColor = ProfilesListPalette.ctaText
         focusRingType = .none
         wantsLayer = true
         layer?.cornerRadius = 8
-        layer?.backgroundColor = ProfilesListPalette.brandBlue.cgColor
+        applyCurrentAppearance()
     }
 
+    func applyCurrentAppearance() {
+        contentTintColor = ProfilesListPalette.ctaText
+        layer?.backgroundColor = (isHovering ? ProfilesListPalette.brandBlueHover : ProfilesListPalette.brandBlue).cgColor
+    }
+
+    private var isHovering = false
+
     override var intrinsicContentSize: NSSize {
         let base = super.intrinsicContentSize
         guard base.width != NSView.noIntrinsicMetric, base.width >= 1 else { return base }
@@ -360,7 +408,8 @@ private final class ProfilesPrimaryButton: NSButton {
 
     override func mouseEntered(with event: NSEvent) {
         super.mouseEntered(with: event)
-        layer?.backgroundColor = ProfilesListPalette.brandBlueHover.cgColor
+        isHovering = true
+        applyCurrentAppearance()
         if !didPushCursor {
             NSCursor.pointingHand.push()
             didPushCursor = true
@@ -369,7 +418,8 @@ private final class ProfilesPrimaryButton: NSButton {
 
     override func mouseExited(with event: NSEvent) {
         super.mouseExited(with: event)
-        layer?.backgroundColor = ProfilesListPalette.brandBlue.cgColor
+        isHovering = false
+        applyCurrentAppearance()
         if didPushCursor {
             NSCursor.pop()
             didPushCursor = false