浏览代码

Implement dynamic light, dark, and system appearance across the app.

Add shared dashboard palettes, live UI refresh on theme changes, and dark-mode styling for the dashboard shell and loading screen so the Settings theme picker fully applies.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 2 月之前
父节点
当前提交
d94b1c2fee

+ 9 - 7
App for Indeed/Services/AppAppearanceManager.swift

@@ -48,15 +48,21 @@ final class AppAppearanceManager {
             queue: .main
         ) { [weak self] _ in
             guard let self, self.mode == .system else { return }
-            self.updateWindowChrome()
+            self.apply()
+            NotificationCenter.default.post(name: Self.didChangeNotification, object: self)
         }
     }
 
+    /// Whether the app is currently rendering with a dark appearance (respects System mode).
+    var isDark: Bool {
+        NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
+    }
+
     var mode: Mode {
         get {
             guard let raw = UserDefaults.standard.string(forKey: UserDefaultsKey.appearanceMode),
                   let stored = Mode(rawValue: raw) else {
-                return .light
+                return .system
             }
             return stored
         }
@@ -70,11 +76,7 @@ final class AppAppearanceManager {
 
     /// Window backing color aligned with dashboard chrome for the active appearance.
     var windowChromeColor: NSColor {
-        let isDark = NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
-        if isDark {
-            return NSColor(srgbRed: 28 / 255, green: 28 / 255, blue: 30 / 255, alpha: 1)
-        }
-        return NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1)
+        AppDashboardTheme.chromeBackground
     }
 
     func apply() {

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

@@ -0,0 +1,236 @@
+//
+//  AppDashboardTheme.swift
+//  App for Indeed
+//
+
+import AppKit
+
+/// Indeed-inspired dashboard colors that follow the active light / dark appearance.
+@MainActor
+enum AppDashboardTheme {
+    static var isDark: Bool { AppAppearanceManager.shared.isDark }
+
+    static var brandBlue: NSColor {
+        NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
+    }
+
+    static var brandBlueHover: NSColor {
+        isDark
+            ? NSColor(srgbRed: 54 / 255, green: 110 / 255, blue: 198 / 255, alpha: 1)
+            : NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
+    }
+
+    static var pageBackground: NSColor {
+        isDark
+            ? NSColor(srgbRed: 28 / 255, green: 28 / 255, blue: 30 / 255, alpha: 1)
+            : NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
+    }
+
+    static var chromeBackground: NSColor {
+        isDark
+            ? NSColor(srgbRed: 28 / 255, green: 28 / 255, blue: 30 / 255, alpha: 1)
+            : NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1)
+    }
+
+    static var sidebarBackground: NSColor {
+        isDark
+            ? NSColor(srgbRed: 36 / 255, green: 36 / 255, blue: 38 / 255, alpha: 1)
+            : NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
+    }
+
+    static var mainHostBackground: NSColor {
+        pageBackground
+    }
+
+    static var welcomeHeroHeadingBlue: NSColor {
+        isDark
+            ? NSColor(srgbRed: 96 / 255, green: 165 / 255, blue: 250 / 255, alpha: 1)
+            : NSColor(srgbRed: 0, green: 82 / 255, blue: 204 / 255, alpha: 1)
+    }
+
+    static var welcomeHeroSubtitleText: NSColor {
+        isDark
+            ? NSColor(srgbRed: 174 / 255, green: 184 / 255, blue: 198 / 255, alpha: 1)
+            : NSColor(srgbRed: 51 / 255, green: 65 / 255, blue: 85 / 255, alpha: 1)
+    }
+
+    static var welcomeHeroIconWell: NSColor {
+        isDark
+            ? NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.22)
+            : NSColor(srgbRed: 239 / 255, green: 246 / 255, blue: 255 / 255, alpha: 1)
+    }
+
+    static var welcomeHeroWaveTint: NSColor {
+        isDark
+            ? NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.35)
+            : NSColor(srgbRed: 186 / 255, green: 210 / 255, blue: 253 / 255, alpha: 1)
+    }
+
+    static var welcomeSubtitleText: NSColor {
+        isDark
+            ? NSColor(srgbRed: 174 / 255, green: 174 / 255, blue: 178 / 255, alpha: 1)
+            : NSColor(srgbRed: 64 / 255, green: 64 / 255, blue: 64 / 255, alpha: 1)
+    }
+
+    static var selectionFill: NSColor {
+        NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: isDark ? 0.28 : 0.12)
+    }
+
+    static var selectionFillHover: NSColor {
+        NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: isDark ? 0.38 : 0.2)
+    }
+
+    static var cardBackground: NSColor {
+        isDark
+            ? NSColor(srgbRed: 44 / 255, green: 44 / 255, blue: 46 / 255, alpha: 1)
+            : NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
+    }
+
+    static var toggleBackground: NSColor {
+        isDark
+            ? NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 60 / 255, alpha: 1)
+            : NSColor(srgbRed: 232 / 255, green: 232 / 255, blue: 232 / 255, alpha: 1)
+    }
+
+    static var primaryText: NSColor {
+        isDark
+            ? NSColor(srgbRed: 245 / 255, green: 245 / 255, blue: 247 / 255, alpha: 1)
+            : NSColor(srgbRed: 45 / 255, green: 45 / 255, blue: 45 / 255, alpha: 1)
+    }
+
+    static var secondaryText: NSColor {
+        isDark
+            ? NSColor(srgbRed: 152 / 255, green: 152 / 255, blue: 157 / 255, alpha: 1)
+            : NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
+    }
+
+    static var tertiaryText: NSColor { secondaryText }
+
+    static var border: NSColor {
+        isDark
+            ? NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 60 / 255, alpha: 1)
+            : NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
+    }
+
+    static var searchBarBorder: NSColor {
+        isDark
+            ? NSColor(srgbRed: 72 / 255, green: 96 / 255, blue: 140 / 255, alpha: 0.55)
+            : NSColor(srgbRed: 180 / 255, green: 200 / 255, blue: 228 / 255, alpha: 1)
+    }
+
+    static var searchBarBorderHover: NSColor {
+        isDark
+            ? NSColor(srgbRed: 96 / 255, green: 140 / 255, blue: 210 / 255, alpha: 0.75)
+            : NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.55)
+    }
+
+    static var proCardFill: NSColor {
+        isDark
+            ? NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.14)
+            : NSColor(srgbRed: 239 / 255, green: 244 / 255, blue: 252 / 255, alpha: 1)
+    }
+
+    static var proCardBorder: NSColor { border }
+
+    static var proAccent: NSColor { brandBlue }
+
+    static var proCTABackground: NSColor { brandBlue }
+
+    static var proCTAText: NSColor {
+        NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
+    }
+
+    static var neutralHoverFill: NSColor {
+        isDark
+            ? NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 60 / 255, alpha: 1)
+            : NSColor(srgbRed: 240 / 255, green: 240 / 255, blue: 240 / 255, alpha: 1)
+    }
+
+    static var sidebarRowHoverFill: NSColor {
+        isDark
+            ? NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 0.06)
+            : NSColor(srgbRed: 0, green: 0, blue: 0, alpha: 0.04)
+    }
+
+    static var settingsPageBackground: NSColor { mainHostBackground }
+
+    static var settingsGroupBackground: NSColor { cardBackground }
+
+    static var settingsIconBackground: NSColor {
+        isDark
+            ? NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.2)
+            : NSColor(srgbRed: 239 / 255, green: 244 / 255, blue: 252 / 255, alpha: 1)
+    }
+
+    static var settingsDivider: NSColor {
+        isDark
+            ? NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 60 / 255, alpha: 1)
+            : NSColor(srgbRed: 228 / 255, green: 228 / 255, blue: 228 / 255, alpha: 1)
+    }
+
+    static var featureCardBackground: NSColor { cardBackground }
+
+    static var featureCardBorder: NSColor {
+        isDark
+            ? NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 60 / 255, alpha: 1)
+            : NSColor(srgbRed: 237 / 255, green: 242 / 255, blue: 247 / 255, alpha: 1)
+    }
+
+    static var featureIconWell: NSColor {
+        isDark
+            ? NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.22)
+            : NSColor(srgbRed: 235 / 255, green: 242 / 255, blue: 255 / 255, alpha: 1)
+    }
+
+    static var featurePrimaryBlue: NSColor { welcomeHeroHeadingBlue }
+
+    static var featureSecondaryText: NSColor {
+        isDark
+            ? NSColor(srgbRed: 174 / 255, green: 184 / 255, blue: 198 / 255, alpha: 1)
+            : NSColor(srgbRed: 93 / 255, green: 109 / 255, blue: 126 / 255, alpha: 1)
+    }
+
+    static var loadingPageBackgroundTop: NSColor {
+        isDark
+            ? NSColor(srgbRed: 36 / 255, green: 36 / 255, blue: 38 / 255, alpha: 1)
+            : NSColor(srgbRed: 252 / 255, green: 253 / 255, blue: 255 / 255, alpha: 1)
+    }
+
+    static var loadingPageBackgroundBottom: NSColor {
+        isDark
+            ? NSColor(srgbRed: 28 / 255, green: 28 / 255, blue: 30 / 255, alpha: 1)
+            : NSColor(srgbRed: 241 / 255, green: 245 / 255, blue: 252 / 255, alpha: 1)
+    }
+
+    static var loadingIconWell: NSColor { featureIconWell }
+
+    static var loadingHeadingBlue: NSColor { welcomeHeroHeadingBlue }
+
+    static var loadingSubtitleText: NSColor { welcomeHeroSubtitleText }
+
+    static var loadingStatusText: NSColor { secondaryText }
+
+    static var loadingWaveTint: NSColor { welcomeHeroWaveTint }
+
+    static var loadingBadgeFill: NSColor {
+        isDark
+            ? NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.28)
+            : NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.1)
+    }
+
+    static var loadingBadgeText: NSColor { brandBlue }
+
+    static var loadingTrackFill: NSColor {
+        isDark
+            ? NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 60 / 255, alpha: 1)
+            : NSColor(srgbRed: 228 / 255, green: 233 / 255, blue: 242 / 255, alpha: 1)
+    }
+
+    static var loadingTrackBorder: NSColor { border }
+
+    static var loadingIconWellBorder: NSColor {
+        isDark
+            ? NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 60 / 255, alpha: 1)
+            : NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
+    }
+}

+ 226 - 62
App for Indeed/Views/DashboardView.swift

@@ -20,47 +20,40 @@ private enum PremiumSheetLayout {
 }
 
 final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDelegate, NSSharingServiceDelegate {
-    /// Indeed.com-inspired neutrals and brand blue (white surfaces, `#2557a7` accent, `#2d2d2d` / `#767676` text, `#d4d2d0` borders).
+    /// Indeed.com-inspired neutrals and brand blue; values follow light / dark / system appearance.
     private enum Theme {
-        static let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
-        static let pageBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
-        static let chromeBackground = NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1)
-        static let sidebarBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
-        static let mainHostBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
-        /// Welcome hero (matches reference: `#0052CC` heading, `#334155` subline, `#EFF6FF` icon well).
-        static let welcomeHeroHeadingBlue = NSColor(srgbRed: 0, green: 82 / 255, blue: 204 / 255, alpha: 1)
-        static let welcomeHeroSubtitleText = NSColor(srgbRed: 51 / 255, green: 65 / 255, blue: 85 / 255, alpha: 1)
-        static let welcomeHeroIconWell = NSColor(srgbRed: 239 / 255, green: 246 / 255, blue: 255 / 255, alpha: 1)
-        /// Light decorative strokes / sparkles behind the welcome hero.
-        static let welcomeHeroWaveTint = NSColor(srgbRed: 186 / 255, green: 210 / 255, blue: 253 / 255, alpha: 1)
-        /// Subtitle on the welcome hero: dark neutral gray to match the reference layout.
-        static let welcomeSubtitleText = NSColor(srgbRed: 64 / 255, green: 64 / 255, blue: 64 / 255, alpha: 1)
-        static let selectionFill = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.12)
-        static let cardBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
-        static let toggleBackground = NSColor(srgbRed: 232 / 255, green: 232 / 255, blue: 232 / 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 tertiaryText = 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 featureIconWell = NSColor(srgbRed: 220 / 255, green: 235 / 255, blue: 252 / 255, alpha: 1)
-        /// Job search bar outer stroke (soft blue-gray, pill field in the reference UI).
-        static let searchBarBorder = NSColor(srgbRed: 180 / 255, green: 200 / 255, blue: 228 / 255, alpha: 1)
-        /// Search bar border on hover (brand-tinted, matches focus affordance elsewhere).
-        static let searchBarBorderHover = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.55)
-        static let proCardFill = NSColor(srgbRed: 239 / 255, green: 244 / 255, blue: 252 / 255, alpha: 1)
-        static let proCardBorder = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
-        static let proAccent = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
-        static let proCTABackground = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
-        static let proCTAText = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
-        /// Hover states: darker brand blue, stronger tints, and subtle neutral fills used across CTAs, toggles, and the sidebar.
-        static let brandBlueHover = NSColor(srgbRed: 28 / 255, green: 70 / 255, blue: 140 / 255, alpha: 1)
-        static let selectionFillHover = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.2)
-        static let neutralHoverFill = NSColor(srgbRed: 240 / 255, green: 240 / 255, blue: 240 / 255, alpha: 1)
-        static let sidebarRowHoverFill = NSColor(srgbRed: 0, green: 0, blue: 0, alpha: 0.04)
-        static let settingsPageBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
-        static let settingsGroupBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
-        static let settingsIconBackground = NSColor(srgbRed: 239 / 255, green: 244 / 255, blue: 252 / 255, alpha: 1)
-        static let settingsDivider = NSColor(srgbRed: 228 / 255, green: 228 / 255, blue: 228 / 255, alpha: 1)
+        static var brandBlue: NSColor { AppDashboardTheme.brandBlue }
+        static var pageBackground: NSColor { AppDashboardTheme.pageBackground }
+        static var chromeBackground: NSColor { AppDashboardTheme.chromeBackground }
+        static var sidebarBackground: NSColor { AppDashboardTheme.sidebarBackground }
+        static var mainHostBackground: NSColor { AppDashboardTheme.mainHostBackground }
+        static var welcomeHeroHeadingBlue: NSColor { AppDashboardTheme.welcomeHeroHeadingBlue }
+        static var welcomeHeroSubtitleText: NSColor { AppDashboardTheme.welcomeHeroSubtitleText }
+        static var welcomeHeroIconWell: NSColor { AppDashboardTheme.welcomeHeroIconWell }
+        static var welcomeHeroWaveTint: NSColor { AppDashboardTheme.welcomeHeroWaveTint }
+        static var welcomeSubtitleText: NSColor { AppDashboardTheme.welcomeSubtitleText }
+        static var selectionFill: NSColor { AppDashboardTheme.selectionFill }
+        static var cardBackground: NSColor { AppDashboardTheme.cardBackground }
+        static var toggleBackground: NSColor { AppDashboardTheme.toggleBackground }
+        static var primaryText: NSColor { AppDashboardTheme.primaryText }
+        static var secondaryText: NSColor { AppDashboardTheme.secondaryText }
+        static var tertiaryText: NSColor { AppDashboardTheme.tertiaryText }
+        static var border: NSColor { AppDashboardTheme.border }
+        static var searchBarBorder: NSColor { AppDashboardTheme.searchBarBorder }
+        static var searchBarBorderHover: NSColor { AppDashboardTheme.searchBarBorderHover }
+        static var proCardFill: NSColor { AppDashboardTheme.proCardFill }
+        static var proCardBorder: NSColor { AppDashboardTheme.proCardBorder }
+        static var proAccent: NSColor { AppDashboardTheme.proAccent }
+        static var proCTABackground: NSColor { AppDashboardTheme.proCTABackground }
+        static var proCTAText: NSColor { AppDashboardTheme.proCTAText }
+        static var brandBlueHover: NSColor { AppDashboardTheme.brandBlueHover }
+        static var selectionFillHover: NSColor { AppDashboardTheme.selectionFillHover }
+        static var neutralHoverFill: NSColor { AppDashboardTheme.neutralHoverFill }
+        static var sidebarRowHoverFill: NSColor { AppDashboardTheme.sidebarRowHoverFill }
+        static var settingsPageBackground: NSColor { AppDashboardTheme.settingsPageBackground }
+        static var settingsGroupBackground: NSColor { AppDashboardTheme.settingsGroupBackground }
+        static var settingsIconBackground: NSColor { AppDashboardTheme.settingsIconBackground }
+        static var settingsDivider: NSColor { AppDashboardTheme.settingsDivider }
     }
 
     /// Multiline `NSTextField` often ignores `alignment` for wrapped runs; explicit paragraph alignment matches the title.
@@ -163,6 +156,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
     private weak var sidebarUpgradeDescription: NSTextField?
     private weak var sidebarUpgradeButton: HoverableButton?
     private var subscriptionObserver: NSObjectProtocol?
+    private var appearanceObserver: NSObjectProtocol?
     /// Retains the system share picker until the user picks a destination or dismisses the menu.
     private var appSharePicker: NSSharingServicePicker?
 
@@ -178,17 +172,37 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
     override init(frame frameRect: NSRect) {
         super.init(frame: frameRect)
         setupLayout()
+        registerAppearanceObservers()
     }
 
     required init?(coder: NSCoder) {
         super.init(coder: coder)
         setupLayout()
+        registerAppearanceObservers()
     }
 
     deinit {
         if let subscriptionObserver {
             NotificationCenter.default.removeObserver(subscriptionObserver)
         }
+        if let appearanceObserver {
+            NotificationCenter.default.removeObserver(appearanceObserver)
+        }
+    }
+
+    override func viewDidChangeEffectiveAppearance() {
+        super.viewDidChangeEffectiveAppearance()
+        applyCurrentAppearance()
+    }
+
+    private func registerAppearanceObservers() {
+        appearanceObserver = NotificationCenter.default.addObserver(
+            forName: AppAppearanceManager.didChangeNotification,
+            object: nil,
+            queue: .main
+        ) { [weak self] _ in
+            self?.applyCurrentAppearance()
+        }
     }
 
     override func viewDidMoveToWindow() {
@@ -220,6 +234,95 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
         savedJobOrder = Self.normalizedSavedJobs(SavedJobsStore.load())
         resetChatState()
         updateMainContentVisibility()
+        applyCurrentAppearance()
+    }
+
+    private func applyCurrentAppearance() {
+        window?.backgroundColor = AppAppearanceManager.shared.windowChromeColor
+
+        layer?.backgroundColor = Theme.chromeBackground.cgColor
+        chromeContainer.layer?.backgroundColor = Theme.chromeBackground.cgColor
+        sidebar.layer?.backgroundColor = Theme.sidebarBackground.cgColor
+        mainHost.layer?.backgroundColor = Theme.mainHostBackground.cgColor
+        indeedJobBrowserHost.layer?.backgroundColor = Theme.mainHostBackground.cgColor
+        nonHomeHost.layer?.backgroundColor = Theme.mainHostBackground.cgColor
+        cvMakerPageContainer.layer?.backgroundColor = Theme.mainHostBackground.cgColor
+        profilePageContainer.layer?.backgroundColor = Theme.mainHostBackground.cgColor
+        settingsPageContainer.layer?.backgroundColor = Theme.settingsPageBackground.cgColor
+
+        greetingLabel.textColor = Theme.welcomeHeroHeadingBlue
+        subtitleLabel.textColor = Theme.welcomeHeroSubtitleText
+        welcomeHeroBackgroundView.waveTint = Theme.welcomeHeroWaveTint
+        welcomeLogoWell.layer?.backgroundColor = Theme.welcomeHeroIconWell.cgColor
+
+        nonHomeTitleLabel.textColor = Theme.primaryText
+        nonHomeSubtitleLabel.textColor = Theme.secondaryText
+        savedJobsPageTitleLabel.textColor = Theme.primaryText
+        savedJobsPageSubtitleLabel.textColor = Theme.secondaryText
+
+        let searchHovering = searchCard.isHovering
+        searchCard.layer?.backgroundColor = (searchHovering ? Theme.neutralHoverFill : Theme.cardBackground).cgColor
+        searchCard.layer?.borderColor = (searchHovering ? Theme.searchBarBorderHover : Theme.searchBarBorder).cgColor
+        jobKeywordsField.textColor = Theme.primaryText
+        jobKeywordsField.placeholderAttributedString = NSAttributedString(
+            string: "Ask for roles, skills, salary, or job descriptions...",
+            attributes: [
+                .foregroundColor: Theme.secondaryText,
+                .font: NSFont.systemFont(ofSize: 14, weight: .regular)
+            ]
+        )
+        jobSearchIcon.contentTintColor = Theme.brandBlue
+        let ctaHovering = findJobsButton.isHovering
+        findJobsCTAPill.layer?.backgroundColor = (ctaHovering ? Theme.brandBlueHover : Theme.brandBlue).cgColor
+        sendIconView.contentTintColor = Theme.proCTAText
+        sendLabel.textColor = Theme.proCTAText
+
+        appearanceModeSegment?.selectedSegment = AppAppearanceManager.shared.mode.segmentIndex
+        refreshSettingsPageAppearance(in: settingsPageContainer)
+        rebuildFeatureShortcutCards()
+        configureSidebar()
+        reloadSavedJobsListings()
+        rebuildChatUI()
+        applyProSubscriptionToSidebar()
+        needsLayout = true
+    }
+
+    private func refreshSettingsPageAppearance(in root: NSView) {
+        for case let field as NSTextField in root.subviewsRecursive() where !field.isEditable {
+            switch field.font?.pointSize {
+            case 12, 14:
+                field.textColor = Theme.secondaryText
+            default:
+                break
+            }
+        }
+        for case let stack as NSStackView in root.subviewsRecursive() where stack.orientation == .vertical && stack.layer?.cornerRadius == 14 {
+            stack.layer?.backgroundColor = Theme.settingsGroupBackground.cgColor
+            stack.layer?.borderColor = Theme.border.cgColor
+            for case let divider as NSView in stack.arrangedSubviews where divider.bounds.height <= 1.5 && divider.wantsLayer {
+                divider.layer?.backgroundColor = Theme.settingsDivider.cgColor
+            }
+        }
+        for case let tile as NSView in root.subviewsRecursive() where tile.layer?.cornerRadius == 9 && tile.bounds.width == 38 {
+            tile.layer?.backgroundColor = Theme.settingsIconBackground.cgColor
+            for case let icon as NSImageView in tile.subviews {
+                icon.contentTintColor = Theme.brandBlue
+            }
+        }
+    }
+
+    private func rebuildFeatureShortcutCards() {
+        let selectedIndex = featureCardsRow.arrangedSubviews.firstIndex {
+            ($0 as? FeatureShortcutCardView)?.isSelected == true
+        }
+        featureCardsRow.arrangedSubviews.forEach {
+            featureCardsRow.removeArrangedSubview($0)
+            $0.removeFromSuperview()
+        }
+        configureFeatureShortcutCards()
+        if let selectedIndex, let shortcut = FeatureShortcut(rawValue: selectedIndex) {
+            selectFeatureShortcut(shortcut)
+        }
     }
 
     private func setupLayout() {
@@ -2039,7 +2142,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
                         newJobsCount: freshJobs.count,
                         isContinuation: isContinuation
                     )
-                    self.chatMessages.append(ChatMessage(role: "assistant", content: reply))
+                    self.chatMessages.append(ChatMessage(role: "assistant", content: reply, attachedJobs: freshJobs.isEmpty ? nil : freshJobs))
                     self.appendChatBubble(text: reply, isUser: false, jobs: freshJobs)
                 case .failure(let error):
                     self.appendChatBubble(text: error.localizedDescription, isUser: false)
@@ -2175,13 +2278,33 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
         trailingLoadMoreJobsButton = nil
         chatMessages.removeAll()
         lastSearchResults.removeAll()
+        clearChatStack()
+        let welcome = "Tell me what role you want and I will return job descriptions, key skills, and a quick fit summary."
+        chatMessages.append(ChatMessage(role: "assistant", content: welcome))
+        appendChatBubble(text: welcome, isUser: false)
+    }
+
+    private func clearChatStack() {
         chatStack.arrangedSubviews.forEach {
             chatStack.removeArrangedSubview($0)
             $0.removeFromSuperview()
         }
-        let welcome = "Tell me what role you want and I will return job descriptions, key skills, and a quick fit summary."
-        chatMessages.append(ChatMessage(role: "assistant", content: welcome))
-        appendChatBubble(text: welcome, isUser: false)
+    }
+
+    private func rebuildChatUI() {
+        guard !chatMessages.isEmpty else { return }
+        removeInlineChatThinkingRow()
+        trailingLoadMoreJobsRow = nil
+        trailingLoadMoreJobsButton = nil
+        clearChatStack()
+        for message in chatMessages {
+            let isUser = message.role == "user"
+            appendChatBubble(text: message.content, isUser: isUser, jobs: message.attachedJobs)
+        }
+        for host in chatStack.arrangedSubviews {
+            host.alphaValue = 1
+        }
+        updateChatBubbleWidths()
     }
 
     private func appendChatBubble(text: String, isUser: Bool, jobs: [JobListing]? = nil) {
@@ -2741,6 +2864,31 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
 private struct ChatMessage: Codable {
     let role: String
     let content: String
+    /// Job cards shown under this assistant message (not sent to the API).
+    var attachedJobs: [JobListing]?
+
+    enum CodingKeys: String, CodingKey {
+        case role
+        case content
+    }
+
+    init(role: String, content: String, attachedJobs: [JobListing]? = nil) {
+        self.role = role
+        self.content = content
+        self.attachedJobs = attachedJobs
+    }
+}
+
+private extension NSView {
+    func subviewsRecursive() -> [NSView] {
+        var result: [NSView] = []
+        var stack: [NSView] = [self]
+        while let view = stack.popLast() {
+            result.append(view)
+            stack.append(contentsOf: view.subviews)
+        }
+        return result
+    }
 }
 
 private final class OpenAIJobSearchService {
@@ -3303,9 +3451,12 @@ private final class WelcomeHeroBackgroundView: NSView {
 /// Home welcome row: three tappable shortcuts that seed the main search field (reference: white cards, pastel icon well, arrow at bottom trailing).
 private final class FeatureShortcutCardView: NSView {
     private static let cardCornerRadius: CGFloat = 14
-    private static let primaryBlue = NSColor(srgbRed: 0, green: 82 / 255, blue: 204 / 255, alpha: 1)
-    private static let defaultBorderColor = NSColor(srgbRed: 237 / 255, green: 242 / 255, blue: 247 / 255, alpha: 1)
     var isSelected = false { didSet { updateSelectionAppearance() } }
+    private weak var titleField: NSTextField?
+    private weak var subtitleField: NSTextField?
+    private weak var iconHost: NSView?
+    private weak var iconView: NSImageView?
+    private weak var chevronView: NSImageView?
     private weak var actionTarget: AnyObject?
     private var actionSelector: Selector
 
@@ -3319,37 +3470,31 @@ private final class FeatureShortcutCardView: NSView {
         if #available(macOS 11.0, *) {
             layer?.cornerCurve = .continuous
         }
-        layer?.backgroundColor = NSColor.white.cgColor
+        layer?.backgroundColor = AppDashboardTheme.featureCardBackground.cgColor
         layer?.masksToBounds = false
-        layer?.shadowColor = NSColor.black.withAlphaComponent(0.06).cgColor
+        layer?.shadowColor = NSColor.black.withAlphaComponent(AppDashboardTheme.isDark ? 0.35 : 0.06).cgColor
         layer?.shadowOffset = CGSize(width: 0, height: 2)
         layer?.shadowRadius = 12
         layer?.shadowOpacity = 1
         updateSelectionAppearance()
 
-        let primaryBlue = Self.primaryBlue
-        // `#EBF2FF` — circular icon well.
-        let iconWellColor = NSColor(srgbRed: 235 / 255, green: 242 / 255, blue: 255 / 255, alpha: 1)
-        // `#5D6D7E` — muted description.
-        let secondary = NSColor(srgbRed: 93 / 255, green: 109 / 255, blue: 126 / 255, alpha: 1)
-
         let iconSize: CGFloat = 40
         let iconHost = NSView()
+        self.iconHost = iconHost
         iconHost.translatesAutoresizingMaskIntoConstraints = false
         iconHost.wantsLayer = true
-        iconHost.layer?.backgroundColor = iconWellColor.cgColor
         iconHost.layer?.cornerRadius = iconSize / 2
 
         let icon = NSImageView()
+        self.iconView = icon
         icon.translatesAutoresizingMaskIntoConstraints = false
         icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .regular)
         icon.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)
-        icon.contentTintColor = primaryBlue
         iconHost.addSubview(icon)
 
         let titleField = NSTextField(wrappingLabelWithString: title)
+        self.titleField = titleField
         titleField.font = .systemFont(ofSize: 15, weight: .bold)
-        titleField.textColor = primaryBlue
         titleField.maximumNumberOfLines = 1
         titleField.isEditable = false
         titleField.isBordered = false
@@ -3357,8 +3502,8 @@ private final class FeatureShortcutCardView: NSView {
         titleField.alignment = .left
 
         let subtitleField = NSTextField(wrappingLabelWithString: subtitle)
+        self.subtitleField = subtitleField
         subtitleField.font = .systemFont(ofSize: 12, weight: .regular)
-        subtitleField.textColor = secondary
         subtitleField.maximumNumberOfLines = 2
         subtitleField.isEditable = false
         subtitleField.isBordered = false
@@ -3369,10 +3514,10 @@ private final class FeatureShortcutCardView: NSView {
         subtitleField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
 
         let chevron = NSImageView()
+        self.chevronView = chevron
         chevron.translatesAutoresizingMaskIntoConstraints = false
         chevron.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
         chevron.image = NSImage(systemSymbolName: "arrow.right", accessibilityDescription: nil)
-        chevron.contentTintColor = primaryBlue
         chevron.setContentHuggingPriority(.required, for: .horizontal)
         chevron.setContentCompressionResistancePriority(.required, for: .horizontal)
 
@@ -3418,17 +3563,36 @@ private final class FeatureShortcutCardView: NSView {
         setAccessibilityElement(true)
         setAccessibilityRole(.button)
         setAccessibilityLabel("\(title). \(subtitle)")
+        applyCardAppearance()
+    }
+
+    func applyCardAppearance() {
+        layer?.backgroundColor = AppDashboardTheme.featureCardBackground.cgColor
+        layer?.shadowColor = NSColor.black.withAlphaComponent(AppDashboardTheme.isDark ? 0.35 : 0.06).cgColor
+        iconHost?.layer?.backgroundColor = AppDashboardTheme.featureIconWell.cgColor
+        let accent = AppDashboardTheme.featurePrimaryBlue
+        iconView?.contentTintColor = accent
+        titleField?.textColor = accent
+        subtitleField?.textColor = AppDashboardTheme.featureSecondaryText
+        chevronView?.contentTintColor = accent
+        updateSelectionAppearance()
+    }
+
+    override func viewDidChangeEffectiveAppearance() {
+        super.viewDidChangeEffectiveAppearance()
+        applyCardAppearance()
     }
 
     private func updateSelectionAppearance() {
         guard let layer else { return }
+        let accent = AppDashboardTheme.featurePrimaryBlue
         if isSelected {
             layer.borderWidth = 2
-            layer.borderColor = Self.primaryBlue.cgColor
-            layer.shadowOpacity = 0.1
+            layer.borderColor = accent.cgColor
+            layer.shadowOpacity = AppDashboardTheme.isDark ? 0.2 : 0.1
         } else {
             layer.borderWidth = 1
-            layer.borderColor = Self.defaultBorderColor.cgColor
+            layer.borderColor = AppDashboardTheme.featureCardBorder.cgColor
             layer.shadowOpacity = 1
         }
         setAccessibilitySelected(isSelected)

+ 65 - 17
App for Indeed/Views/LoadingView.swift

@@ -8,19 +8,22 @@ import QuartzCore
 
 final class LoadingView: NSView {
     private enum Theme {
-        static let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
-        static let brandBlueLight = NSColor(srgbRed: 54 / 255, green: 110 / 255, blue: 198 / 255, alpha: 1)
-        static let pageBackgroundTop = NSColor(srgbRed: 252 / 255, green: 253 / 255, blue: 255 / 255, alpha: 1)
-        static let pageBackgroundBottom = NSColor(srgbRed: 241 / 255, green: 245 / 255, blue: 252 / 255, alpha: 1)
-        static let iconWell = NSColor(srgbRed: 239 / 255, green: 246 / 255, blue: 255 / 255, alpha: 1)
-        static let headingBlue = NSColor(srgbRed: 0, green: 82 / 255, blue: 204 / 255, alpha: 1)
-        static let subtitleText = NSColor(srgbRed: 51 / 255, green: 65 / 255, blue: 85 / 255, alpha: 1)
-        static let statusText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
-        static let waveTint = NSColor(srgbRed: 186 / 255, green: 210 / 255, blue: 253 / 255, alpha: 1)
-        static let badgeFill = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.1)
-        static let badgeText = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
+        static var brandBlue: NSColor { AppDashboardTheme.brandBlue }
+        static var brandBlueLight: NSColor { AppDashboardTheme.brandBlueHover }
+        static var pageBackgroundTop: NSColor { AppDashboardTheme.loadingPageBackgroundTop }
+        static var pageBackgroundBottom: NSColor { AppDashboardTheme.loadingPageBackgroundBottom }
+        static var iconWell: NSColor { AppDashboardTheme.loadingIconWell }
+        static var headingBlue: NSColor { AppDashboardTheme.loadingHeadingBlue }
+        static var subtitleText: NSColor { AppDashboardTheme.loadingSubtitleText }
+        static var statusText: NSColor { AppDashboardTheme.loadingStatusText }
+        static var waveTint: NSColor { AppDashboardTheme.loadingWaveTint }
+        static var badgeFill: NSColor { AppDashboardTheme.loadingBadgeFill }
+        static var badgeText: NSColor { AppDashboardTheme.loadingBadgeText }
+        static var iconWellBorder: NSColor { AppDashboardTheme.loadingIconWellBorder }
     }
 
+    private var appearanceObserver: NSObjectProtocol?
+
     private let backgroundGradientHost = NSView()
     private let heroBackground = LoadingSplashBackgroundView()
     private let iconWell = NSView()
@@ -39,6 +42,19 @@ final class LoadingView: 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()
+        }
+    }
+
+    deinit {
+        if let appearanceObserver {
+            NotificationCenter.default.removeObserver(appearanceObserver)
+        }
     }
 
     @available(*, unavailable)
@@ -46,6 +62,11 @@ final class LoadingView: NSView {
         fatalError("init(coder:) has not been implemented")
     }
 
+    override func viewDidChangeEffectiveAppearance() {
+        super.viewDidChangeEffectiveAppearance()
+        applyCurrentAppearance()
+    }
+
     override func layout() {
         super.layout()
         backgroundGradientHost.layer?.sublayers?
@@ -98,7 +119,7 @@ final class LoadingView: NSView {
             iconWell.layer?.cornerCurve = .continuous
         }
         iconWell.layer?.masksToBounds = true
-        iconWell.layer?.borderColor = NSColor.white.cgColor
+        iconWell.layer?.borderColor = Theme.iconWellBorder.cgColor
         iconWell.layer?.borderWidth = 3
         iconWell.layer?.shadowColor = NSColor.black.cgColor
         iconWell.layer?.shadowOpacity = 0.08
@@ -219,6 +240,9 @@ final class LoadingView: NSView {
     }
 
     private func installPageGradient() {
+        backgroundGradientHost.layer?.sublayers?
+            .filter { $0.name == "pageGradient" }
+            .forEach { $0.removeFromSuperlayer() }
         let gradient = CAGradientLayer()
         gradient.name = "pageGradient"
         gradient.colors = [
@@ -231,17 +255,31 @@ final class LoadingView: NSView {
         backgroundGradientHost.layer?.addSublayer(gradient)
     }
 
+    private func applyCurrentAppearance() {
+        window?.backgroundColor = AppAppearanceManager.shared.windowChromeColor
+        heroBackground.waveTint = Theme.waveTint
+        iconWell.layer?.backgroundColor = Theme.iconWell.cgColor
+        iconWell.layer?.borderColor = Theme.iconWellBorder.cgColor
+        aiBadgeHost.layer?.backgroundColor = Theme.badgeFill.cgColor
+        aiBadgeLabel.textColor = Theme.badgeText
+        titleLabel.textColor = Theme.headingBlue
+        subtitleLabel.textColor = Theme.subtitleText
+        statusLabel.textColor = Theme.statusText
+        installPageGradient()
+        progressBar.applyCurrentAppearance()
+    }
+
 }
 
 // MARK: - Loading progress bar
 
 private final class LoadingProgressBarView: NSView {
     private enum Theme {
-        static let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
-        static let brandBlueLight = NSColor(srgbRed: 54 / 255, green: 110 / 255, blue: 198 / 255, alpha: 1)
-        static let trackFill = NSColor(srgbRed: 228 / 255, green: 233 / 255, blue: 242 / 255, alpha: 1)
-        static let trackBorder = NSColor(srgbRed: 212 / 255, green: 218 / 255, blue: 230 / 255, alpha: 1)
-        static let percentText = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
+        static var brandBlue: NSColor { AppDashboardTheme.brandBlue }
+        static var brandBlueLight: NSColor { AppDashboardTheme.brandBlueHover }
+        static var trackFill: NSColor { AppDashboardTheme.loadingTrackFill }
+        static var trackBorder: NSColor { AppDashboardTheme.loadingTrackBorder }
+        static var percentText: NSColor { AppDashboardTheme.brandBlue }
     }
 
     private let track = NSView()
@@ -319,6 +357,16 @@ private final class LoadingProgressBarView: NSView {
         shimmerLayer?.removeAnimation(forKey: "shimmer")
     }
 
+    func applyCurrentAppearance() {
+        track.layer?.backgroundColor = Theme.trackFill.cgColor
+        track.layer?.borderColor = Theme.trackBorder.cgColor
+        percentLabel.textColor = Theme.percentText
+        fillGradientLayer?.colors = [
+            Theme.brandBlueLight.cgColor,
+            Theme.brandBlue.cgColor
+        ]
+    }
+
     private func setUp() {
         translatesAutoresizingMaskIntoConstraints = false