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

Refine dashboard home UI: status banner, feature cards, and theme colors

Align welcome hero, search bar, and chat status strip with the reference layout.
Add feature shortcut cards (Role, Company, etc.), status branding row, and
adjust subtitle and border colors for readability.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 2 месяцев назад
Родитель
Сommit
8a252e60bf
1 измененных файлов с 464 добавлено и 80 удалено
  1. 464 80
      App for Indeed/Views/DashboardView.swift

+ 464 - 80
App for Indeed/Views/DashboardView.swift

@@ -20,8 +20,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         static let chromeBackground = NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, 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 sidebarBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
         static let mainHostBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
         static let mainHostBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
-        /// Subtitle on the welcome hero: readable blue-gray aligned with brand.
-        static let welcomeSubtitleText = NSColor(srgbRed: 52 / 255, green: 92 / 255, blue: 142 / 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 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 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 toggleBackground = NSColor(srgbRed: 232 / 255, green: 232 / 255, blue: 232 / 255, alpha: 1)
@@ -29,10 +29,13 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         static let secondaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 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 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 border = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
-        /// Job search bar outer stroke (charcoal).
-        static let searchBarBorder = NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 58 / 255, alpha: 1)
+        /// Home status strip and soft accents (light blue panel in the reference UI).
+        static let statusBannerBackground = NSColor(srgbRed: 232 / 255, green: 242 / 255, blue: 252 / 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).
         /// 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.45)
+        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 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 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 proAccent = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
@@ -81,11 +84,18 @@ final class DashboardView: NSView, NSTextFieldDelegate {
     private let findJobsCTAHost = NSView()
     private let findJobsCTAHost = NSView()
     private let findJobsCTAChrome = HoverableView()
     private let findJobsCTAChrome = HoverableView()
     private var findJobsCTAGradientLayer: CAGradientLayer?
     private var findJobsCTAGradientLayer: CAGradientLayer?
-    private let chatStatusStack = NSStackView()
+    private let statusBannerContainer = NSView()
+    private let statusBannerInner = NSView()
+    private let statusBannerRow = NSStackView()
     private let chatStatusSymbolContainer = NSView()
     private let chatStatusSymbolContainer = NSView()
     private let chatStatusIcon = NSImageView()
     private let chatStatusIcon = NSImageView()
     private let chatStatusLoadingIndicator = NSProgressIndicator()
     private let chatStatusLoadingIndicator = NSProgressIndicator()
-    private let chatStatusLabel = NSTextField(labelWithString: "Opening the vault...")
+    private let chatStatusLabel = NSTextField(wrappingLabelWithString: "Opening the vault...")
+    private let statusBrandTrailing = NSStackView()
+    private let statusBrandStarIcon = NSImageView()
+    private let statusBrandLabel = NSTextField(labelWithString: "AI Job Finder")
+    private let welcomeSparkleIcon = NSImageView()
+    private let featureCardsRow = NSStackView()
     private let chatScrollView = NSScrollView()
     private let chatScrollView = NSScrollView()
     private let chatDocumentView = JobListingsDocumentView()
     private let chatDocumentView = JobListingsDocumentView()
     private let chatStack = NSStackView()
     private let chatStack = NSStackView()
@@ -145,6 +155,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         updateFindJobsCTAShadowPath()
         updateFindJobsCTAShadowPath()
         updateJobListingDescriptionWidths()
         updateJobListingDescriptionWidths()
         updateChatBubbleWidths()
         updateChatBubbleWidths()
+        updateStatusBannerLabelWidth()
     }
     }
 
 
     func render(_ data: DashboardData) {
     func render(_ data: DashboardData) {
@@ -226,14 +237,21 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         configureSearchBar()
         configureSearchBar()
         configureChatViews()
         configureChatViews()
 
 
-        let titleBlock = NSStackView(views: [greetingLabel, subtitleLabel])
+        welcomeSparkleIcon.translatesAutoresizingMaskIntoConstraints = false
+        welcomeSparkleIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
+        welcomeSparkleIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
+        welcomeSparkleIcon.contentTintColor = Theme.brandBlue
+
+        let titleBlock = NSStackView(views: [greetingLabel, subtitleLabel, welcomeSparkleIcon])
         titleBlock.orientation = .vertical
         titleBlock.orientation = .vertical
         titleBlock.spacing = 10
         titleBlock.spacing = 10
         titleBlock.alignment = .centerX
         titleBlock.alignment = .centerX
 
 
+        configureFeatureShortcutCards()
+
         let midSpacer = NSView()
         let midSpacer = NSView()
         midSpacer.translatesAutoresizingMaskIntoConstraints = false
         midSpacer.translatesAutoresizingMaskIntoConstraints = false
-        midSpacer.heightAnchor.constraint(equalToConstant: 18).isActive = true
+        midSpacer.heightAnchor.constraint(equalToConstant: 12).isActive = true
 
 
         let chatTopSpacer = NSView()
         let chatTopSpacer = NSView()
         chatTopSpacer.translatesAutoresizingMaskIntoConstraints = false
         chatTopSpacer.translatesAutoresizingMaskIntoConstraints = false
@@ -245,8 +263,9 @@ final class DashboardView: NSView, NSTextFieldDelegate {
 
 
         mainOverlay.addArrangedSubview(topInset)
         mainOverlay.addArrangedSubview(topInset)
         mainOverlay.addArrangedSubview(titleBlock)
         mainOverlay.addArrangedSubview(titleBlock)
+        mainOverlay.addArrangedSubview(featureCardsRow)
         mainOverlay.addArrangedSubview(midSpacer)
         mainOverlay.addArrangedSubview(midSpacer)
-        mainOverlay.addArrangedSubview(chatStatusStack)
+        mainOverlay.addArrangedSubview(statusBannerContainer)
         mainOverlay.addArrangedSubview(chatTopSpacer)
         mainOverlay.addArrangedSubview(chatTopSpacer)
         mainOverlay.addArrangedSubview(chatScrollView)
         mainOverlay.addArrangedSubview(chatScrollView)
         mainOverlay.addArrangedSubview(chatBottomSpacer)
         mainOverlay.addArrangedSubview(chatBottomSpacer)
@@ -280,7 +299,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
             nonHomeHost.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor, constant: -24),
             nonHomeHost.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor, constant: -24),
 
 
             searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
             searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
-            chatStatusStack.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
+            statusBannerContainer.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
+            featureCardsRow.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
             chatScrollView.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
             chatScrollView.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
 
 
             greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 16),
             greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 16),
@@ -290,19 +310,60 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         ])
         ])
     }
     }
 
 
+    private func configureFeatureShortcutCards() {
+        featureCardsRow.orientation = .horizontal
+        featureCardsRow.spacing = 12
+        featureCardsRow.distribution = .fillEqually
+        featureCardsRow.alignment = .top
+        featureCardsRow.translatesAutoresizingMaskIntoConstraints = false
+
+        let specs: [(symbol: String, title: String, subtitle: String, action: Selector)] = [
+            ("briefcase.fill", "Role", "Explore similar or better job roles", #selector(didTapFeatureRole)),
+            ("building.2.fill", "Company", "Find opportunities at other companies", #selector(didTapFeatureCompany)),
+            ("chevron.left.forwardslash.chevron.right", "Skill", "Match jobs that fit your skills", #selector(didTapFeatureSkill))
+        ]
+        for spec in specs {
+            let card = FeatureShortcutCardView(
+                symbolName: spec.symbol,
+                title: spec.title,
+                subtitle: spec.subtitle,
+                target: self,
+                action: spec.action
+            )
+            featureCardsRow.addArrangedSubview(card)
+        }
+    }
+
     private func configureChatViews() {
     private func configureChatViews() {
-        chatStatusStack.orientation = .vertical
-        chatStatusStack.spacing = 6
-        chatStatusStack.alignment = .centerX
-        chatStatusStack.translatesAutoresizingMaskIntoConstraints = false
+        statusBannerContainer.translatesAutoresizingMaskIntoConstraints = false
+        statusBannerContainer.setContentHuggingPriority(.defaultHigh, for: .vertical)
+
+        statusBannerInner.translatesAutoresizingMaskIntoConstraints = false
+        statusBannerInner.wantsLayer = true
+        statusBannerInner.layer?.backgroundColor = Theme.statusBannerBackground.cgColor
+        statusBannerInner.layer?.cornerRadius = 14
+        if #available(macOS 11.0, *) {
+            statusBannerInner.layer?.cornerCurve = .continuous
+        }
+        statusBannerInner.layer?.masksToBounds = true
+
+        statusBannerContainer.addSubview(statusBannerInner)
+
+        statusBannerRow.orientation = .horizontal
+        statusBannerRow.spacing = 12
+        statusBannerRow.alignment = .top
+        statusBannerRow.translatesAutoresizingMaskIntoConstraints = false
 
 
         chatStatusSymbolContainer.translatesAutoresizingMaskIntoConstraints = false
         chatStatusSymbolContainer.translatesAutoresizingMaskIntoConstraints = false
+        chatStatusSymbolContainer.wantsLayer = true
+        chatStatusSymbolContainer.layer?.backgroundColor = Theme.brandBlue.cgColor
+        chatStatusSymbolContainer.layer?.cornerRadius = 20
 
 
         chatStatusIcon.translatesAutoresizingMaskIntoConstraints = false
         chatStatusIcon.translatesAutoresizingMaskIntoConstraints = false
         chatStatusIcon.wantsLayer = true
         chatStatusIcon.wantsLayer = true
-        chatStatusIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 36, weight: .regular)
+        chatStatusIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 15, weight: .semibold)
         chatStatusIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: "Assistant status")
         chatStatusIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: "Assistant status")
-        chatStatusIcon.contentTintColor = Theme.brandBlue
+        chatStatusIcon.contentTintColor = .white
 
 
         chatStatusLoadingIndicator.translatesAutoresizingMaskIntoConstraints = false
         chatStatusLoadingIndicator.translatesAutoresizingMaskIntoConstraints = false
         chatStatusLoadingIndicator.style = .spinning
         chatStatusLoadingIndicator.style = .spinning
@@ -323,19 +384,66 @@ final class DashboardView: NSView, NSTextFieldDelegate {
             chatStatusIcon.centerYAnchor.constraint(equalTo: chatStatusSymbolContainer.centerYAnchor),
             chatStatusIcon.centerYAnchor.constraint(equalTo: chatStatusSymbolContainer.centerYAnchor),
             chatStatusLoadingIndicator.centerXAnchor.constraint(equalTo: chatStatusSymbolContainer.centerXAnchor),
             chatStatusLoadingIndicator.centerXAnchor.constraint(equalTo: chatStatusSymbolContainer.centerXAnchor),
             chatStatusLoadingIndicator.centerYAnchor.constraint(equalTo: chatStatusSymbolContainer.centerYAnchor),
             chatStatusLoadingIndicator.centerYAnchor.constraint(equalTo: chatStatusSymbolContainer.centerYAnchor),
-            chatStatusLoadingIndicator.widthAnchor.constraint(equalToConstant: 32),
-            chatStatusLoadingIndicator.heightAnchor.constraint(equalToConstant: 32)
+            chatStatusLoadingIndicator.widthAnchor.constraint(equalToConstant: 26),
+            chatStatusLoadingIndicator.heightAnchor.constraint(equalToConstant: 26)
         ])
         ])
 
 
-        chatStatusLabel.font = .systemFont(ofSize: 20, weight: .semibold)
+        chatStatusLabel.font = .systemFont(ofSize: 13, weight: .regular)
         chatStatusLabel.textColor = Theme.primaryText
         chatStatusLabel.textColor = Theme.primaryText
-        chatStatusLabel.alignment = .center
-        chatStatusLabel.maximumNumberOfLines = 1
+        chatStatusLabel.alignment = .left
+        chatStatusLabel.maximumNumberOfLines = 0
+        chatStatusLabel.lineBreakMode = .byWordWrapping
+        chatStatusLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        chatStatusLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+        chatStatusLabel.translatesAutoresizingMaskIntoConstraints = false
+
+        statusBrandStarIcon.translatesAutoresizingMaskIntoConstraints = false
+        statusBrandStarIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
+        statusBrandStarIcon.image = NSImage(systemSymbolName: "sparkle", accessibilityDescription: nil)
+        statusBrandStarIcon.contentTintColor = Theme.brandBlue
+
+        statusBrandLabel.font = .systemFont(ofSize: 12, weight: .semibold)
+        statusBrandLabel.textColor = Theme.brandBlue
+        statusBrandLabel.alignment = .right
+        statusBrandLabel.maximumNumberOfLines = 1
+
+        statusBrandTrailing.orientation = .horizontal
+        statusBrandTrailing.spacing = 5
+        statusBrandTrailing.alignment = .centerY
+        statusBrandTrailing.translatesAutoresizingMaskIntoConstraints = false
+        statusBrandTrailing.addArrangedSubview(statusBrandStarIcon)
+        statusBrandTrailing.addArrangedSubview(statusBrandLabel)
+        statusBrandTrailing.setContentHuggingPriority(.required, for: .horizontal)
+        statusBrandTrailing.setContentCompressionResistancePriority(.required, for: .horizontal)
+
+        statusBannerRow.addArrangedSubview(chatStatusSymbolContainer)
+        statusBannerRow.addArrangedSubview(chatStatusLabel)
+
+        statusBannerInner.addSubview(statusBannerRow)
+        statusBannerContainer.addSubview(statusBannerInner)
+        statusBannerContainer.addSubview(statusBrandTrailing)
+        statusBannerInner.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+        statusBannerInner.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
 
 
-        chatStatusStack.addArrangedSubview(chatStatusSymbolContainer)
-        chatStatusStack.addArrangedSubview(chatStatusLabel)
+        NSLayoutConstraint.activate([
+            statusBannerInner.leadingAnchor.constraint(equalTo: statusBannerContainer.leadingAnchor),
+            statusBannerInner.topAnchor.constraint(equalTo: statusBannerContainer.topAnchor),
+            statusBannerInner.bottomAnchor.constraint(equalTo: statusBannerContainer.bottomAnchor),
+            statusBannerInner.trailingAnchor.constraint(lessThanOrEqualTo: statusBrandTrailing.leadingAnchor, constant: -16),
+
+            statusBrandTrailing.trailingAnchor.constraint(equalTo: statusBannerContainer.trailingAnchor),
+            statusBrandTrailing.centerYAnchor.constraint(equalTo: statusBannerContainer.centerYAnchor),
+
+            statusBannerRow.leadingAnchor.constraint(equalTo: statusBannerInner.leadingAnchor, constant: 14),
+            statusBannerRow.trailingAnchor.constraint(equalTo: statusBannerInner.trailingAnchor, constant: -16),
+            statusBannerRow.topAnchor.constraint(equalTo: statusBannerInner.topAnchor, constant: 10),
+            statusBannerRow.bottomAnchor.constraint(equalTo: statusBannerInner.bottomAnchor, constant: -10),
+
+            statusBannerContainer.heightAnchor.constraint(greaterThanOrEqualToConstant: 52)
+        ])
 
 
         syncChatStatusLoadingIndicator(forStatusText: chatStatusLabel.stringValue)
         syncChatStatusLoadingIndicator(forStatusText: chatStatusLabel.stringValue)
+        syncChatStatusBannerVisuals(forStatusText: chatStatusLabel.stringValue)
 
 
         chatDocumentView.translatesAutoresizingMaskIntoConstraints = false
         chatDocumentView.translatesAutoresizingMaskIntoConstraints = false
         chatStack.orientation = .vertical
         chatStack.orientation = .vertical
@@ -374,7 +482,43 @@ final class DashboardView: NSView, NSTextFieldDelegate {
     private func setChatStatusLabel(_ text: String) {
     private func setChatStatusLabel(_ text: String) {
         chatStatusLabel.stringValue = text
         chatStatusLabel.stringValue = text
         syncChatStatusLoadingIndicator(forStatusText: text)
         syncChatStatusLoadingIndicator(forStatusText: text)
+        syncChatStatusBannerVisuals(forStatusText: text)
         syncChatStatusSparkleAnimation()
         syncChatStatusSparkleAnimation()
+        updateStatusBannerLabelWidth()
+    }
+
+    private func updateStatusBannerLabelWidth() {
+        guard statusBannerContainer.bounds.width > 1 else { return }
+        let trailingWidth = max(96, statusBrandTrailing.fittingSize.width)
+        let chrome: CGFloat = 14 + 40 + 12 + 16 + 16 + trailingWidth
+        let target = max(80, statusBannerContainer.bounds.width - chrome)
+        if abs(chatStatusLabel.preferredMaxLayoutWidth - target) > 0.5 {
+            chatStatusLabel.preferredMaxLayoutWidth = target
+            chatStatusLabel.invalidateIntrinsicContentSize()
+        }
+    }
+
+    /// Leading status glyph: sparkles for prompts, magnifying glass for search-result summaries and errors.
+    private func syncChatStatusBannerVisuals(forStatusText text: String) {
+        if text == "Thinking..." { return }
+        let lower = text.lowercased()
+        let useMagnifyingGlass =
+            text.hasPrefix("Found ")
+            || text.hasPrefix("Here are")
+            || text.hasPrefix("No jobs")
+            || text.contains("couldn't find")
+            || lower.contains("try again")
+            || lower.contains("could not reach")
+            || lower.contains("search did not finish")
+        let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .semibold)
+        chatStatusIcon.symbolConfiguration = config
+        if useMagnifyingGlass {
+            chatStatusIcon.image = NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: "Search status")
+        } else {
+            chatStatusIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: "Assistant status")
+        }
+        chatStatusIcon.contentTintColor = .white
+        chatStatusSymbolContainer.layer?.backgroundColor = Theme.brandBlue.cgColor
     }
     }
 
 
     private func syncChatStatusLoadingIndicator(forStatusText text: String) {
     private func syncChatStatusLoadingIndicator(forStatusText text: String) {
@@ -383,9 +527,12 @@ final class DashboardView: NSView, NSTextFieldDelegate {
             chatStatusIcon.isHidden = true
             chatStatusIcon.isHidden = true
             chatStatusLoadingIndicator.isHidden = false
             chatStatusLoadingIndicator.isHidden = false
             chatStatusLoadingIndicator.startAnimation(nil)
             chatStatusLoadingIndicator.startAnimation(nil)
+            chatStatusSymbolContainer.layer?.backgroundColor = Theme.featureIconWell.cgColor
         } else {
         } else {
             chatStatusLoadingIndicator.stopAnimation(nil)
             chatStatusLoadingIndicator.stopAnimation(nil)
             chatStatusIcon.isHidden = false
             chatStatusIcon.isHidden = false
+            chatStatusLoadingIndicator.isHidden = true
+            chatStatusSymbolContainer.layer?.backgroundColor = Theme.brandBlue.cgColor
         }
         }
     }
     }
 
 
@@ -399,7 +546,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
 
 
     private func shouldAnimateChatStatusSparkles(for statusText: String) -> Bool {
     private func shouldAnimateChatStatusSparkles(for statusText: String) -> Bool {
         statusText == "Ask me to find jobs"
         statusText == "Ask me to find jobs"
-            || statusText == "Ask for another role, company, or skill match"
+            || statusText == "Opening the vault..."
     }
     }
 
 
     private func syncChatStatusSparkleAnimation() {
     private func syncChatStatusSparkleAnimation() {
@@ -542,30 +689,99 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         persistSavedJobs()
         persistSavedJobs()
     }
     }
 
 
+    private func jobListingHostSubtitle(_ job: JobListing) -> String {
+        guard let raw = job.url, let url = URL(string: raw), let host = url.host?.lowercased() else {
+            return "Indeed"
+        }
+        if host.hasPrefix("www.") {
+            return String(host.dropFirst(4))
+        }
+        return host
+    }
+
+    private func jobListingCategorySymbol(for job: JobListing) -> String {
+        let blob = (job.title + " " + job.description).lowercased()
+        if blob.contains("machine learning") || blob.contains("deep learning") || blob.contains(" ml ") {
+            return "brain.head.profile"
+        }
+        if blob.contains("audio") || blob.contains(" sound ") || blob.contains("dsp") {
+            return "waveform"
+        }
+        if blob.contains("ios") || blob.contains("swift") || blob.contains("mobile") {
+            return "iphone"
+        }
+        if blob.contains("design") || blob.contains(" ux") || blob.contains("figma") {
+            return "paintpalette.fill"
+        }
+        if blob.contains("data ") || blob.contains("analytics") {
+            return "chart.bar.fill"
+        }
+        if blob.contains("ai") || blob.contains("llm") || blob.contains("nlp") {
+            return "cpu"
+        }
+        return "briefcase.fill"
+    }
+
     private func makeJobListingCard(_ job: JobListing, context: JobListingCardContext) -> NSView {
     private func makeJobListingCard(_ job: JobListing, context: JobListingCardContext) -> NSView {
         let card = NSView()
         let card = NSView()
         card.translatesAutoresizingMaskIntoConstraints = false
         card.translatesAutoresizingMaskIntoConstraints = false
         card.wantsLayer = true
         card.wantsLayer = true
         card.layer?.backgroundColor = Theme.cardBackground.cgColor
         card.layer?.backgroundColor = Theme.cardBackground.cgColor
-        card.layer?.cornerRadius = 12
+        card.layer?.cornerRadius = 14
         card.layer?.borderWidth = 1
         card.layer?.borderWidth = 1
         card.layer?.borderColor = Theme.border.cgColor
         card.layer?.borderColor = Theme.border.cgColor
         card.layer?.masksToBounds = true
         card.layer?.masksToBounds = true
 
 
+        let iconBox = NSView()
+        iconBox.translatesAutoresizingMaskIntoConstraints = false
+        iconBox.wantsLayer = true
+        iconBox.layer?.backgroundColor = Theme.brandBlue.cgColor
+        iconBox.layer?.cornerRadius = 12
+        if #available(macOS 11.0, *) {
+            iconBox.layer?.cornerCurve = .continuous
+        }
+
+        let categoryIcon = NSImageView()
+        categoryIcon.translatesAutoresizingMaskIntoConstraints = false
+        categoryIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 22, weight: .medium)
+        categoryIcon.image = NSImage(systemSymbolName: jobListingCategorySymbol(for: job), accessibilityDescription: nil)
+        categoryIcon.contentTintColor = .white
+        iconBox.addSubview(categoryIcon)
+
         let titleField = NSTextField(labelWithString: job.title)
         let titleField = NSTextField(labelWithString: job.title)
         titleField.font = .systemFont(ofSize: 16, weight: .semibold)
         titleField.font = .systemFont(ofSize: 16, weight: .semibold)
-        titleField.textColor = Theme.primaryText
+        titleField.textColor = Theme.brandBlue
         titleField.maximumNumberOfLines = 2
         titleField.maximumNumberOfLines = 2
         titleField.lineBreakMode = .byWordWrapping
         titleField.lineBreakMode = .byWordWrapping
         titleField.alignment = .left
         titleField.alignment = .left
+        titleField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         titleField.translatesAutoresizingMaskIntoConstraints = false
         titleField.translatesAutoresizingMaskIntoConstraints = false
 
 
+        let buildingIcon = NSImageView()
+        buildingIcon.translatesAutoresizingMaskIntoConstraints = false
+        buildingIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .medium)
+        buildingIcon.image = NSImage(systemSymbolName: "building.2.fill", accessibilityDescription: nil)
+        buildingIcon.contentTintColor = Theme.welcomeSubtitleText
+
+        let companyLabel = NSTextField(labelWithString: jobListingHostSubtitle(job))
+        companyLabel.font = .systemFont(ofSize: 12, weight: .medium)
+        companyLabel.textColor = Theme.welcomeSubtitleText
+        companyLabel.maximumNumberOfLines = 1
+        companyLabel.lineBreakMode = .byTruncatingTail
+        companyLabel.translatesAutoresizingMaskIntoConstraints = false
+
+        let companyRow = NSStackView(views: [buildingIcon, companyLabel])
+        companyRow.orientation = .horizontal
+        companyRow.spacing = 5
+        companyRow.alignment = .centerY
+        companyRow.translatesAutoresizingMaskIntoConstraints = false
+
         let descriptionField = NSTextField(wrappingLabelWithString: job.description)
         let descriptionField = NSTextField(wrappingLabelWithString: job.description)
         descriptionField.font = .systemFont(ofSize: 13, weight: .regular)
         descriptionField.font = .systemFont(ofSize: 13, weight: .regular)
         descriptionField.textColor = Theme.secondaryText
         descriptionField.textColor = Theme.secondaryText
-        descriptionField.maximumNumberOfLines = 0
-        descriptionField.alignment = .left
+        descriptionField.maximumNumberOfLines = 2
         descriptionField.lineBreakMode = .byWordWrapping
         descriptionField.lineBreakMode = .byWordWrapping
+        descriptionField.alignment = .left
         descriptionField.baseWritingDirection = .leftToRight
         descriptionField.baseWritingDirection = .leftToRight
         descriptionField.attributedStringValue = Self.jobListingDescriptionAttributedString(job.description)
         descriptionField.attributedStringValue = Self.jobListingDescriptionAttributedString(job.description)
         if let cell = descriptionField.cell as? NSTextFieldCell {
         if let cell = descriptionField.cell as? NSTextFieldCell {
@@ -584,7 +800,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         applyButton.bezelStyle = .rounded
         applyButton.bezelStyle = .rounded
         applyButton.font = .systemFont(ofSize: 13, weight: .semibold)
         applyButton.font = .systemFont(ofSize: 13, weight: .semibold)
         applyButton.wantsLayer = true
         applyButton.wantsLayer = true
-        applyButton.layer?.cornerRadius = 6
+        applyButton.layer?.cornerRadius = 8
         applyButton.layer?.backgroundColor = Theme.brandBlue.cgColor
         applyButton.layer?.backgroundColor = Theme.brandBlue.cgColor
         applyButton.contentTintColor = Theme.proCTAText
         applyButton.contentTintColor = Theme.proCTAText
         applyButton.focusRingType = .none
         applyButton.focusRingType = .none
@@ -603,6 +819,9 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         savedButton.isBordered = false
         savedButton.isBordered = false
         savedButton.bezelStyle = .rounded
         savedButton.bezelStyle = .rounded
         savedButton.font = .systemFont(ofSize: 13, weight: .semibold)
         savedButton.font = .systemFont(ofSize: 13, weight: .semibold)
+        savedButton.image = NSImage(systemSymbolName: savedOn ? "heart.fill" : "heart", accessibilityDescription: nil)
+        savedButton.imagePosition = .imageLeading
+        savedButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
         savedButton.focusRingType = .none
         savedButton.focusRingType = .none
         savedButton.state = savedOn ? .on : .off
         savedButton.state = savedOn ? .on : .off
         savedButton.pointerCursor = true
         savedButton.pointerCursor = true
@@ -620,7 +839,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         dismissButton.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Dismiss")
         dismissButton.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Dismiss")
         dismissButton.imagePosition = .imageOnly
         dismissButton.imagePosition = .imageOnly
         dismissButton.imageScaling = .scaleProportionallyDown
         dismissButton.imageScaling = .scaleProportionallyDown
-        dismissButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
+        dismissButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
         dismissButton.isBordered = false
         dismissButton.isBordered = false
         dismissButton.bezelStyle = .rounded
         dismissButton.bezelStyle = .rounded
         dismissButton.contentTintColor = Theme.secondaryText
         dismissButton.contentTintColor = Theme.secondaryText
@@ -629,7 +848,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         dismissButton.toolTip = context == .savedJobsPage ? "Remove from saved" : "Dismiss"
         dismissButton.toolTip = context == .savedJobsPage ? "Remove from saved" : "Dismiss"
         dismissButton.focusRingType = .none
         dismissButton.focusRingType = .none
         dismissButton.wantsLayer = true
         dismissButton.wantsLayer = true
-        dismissButton.layer?.cornerRadius = 14
+        dismissButton.layer?.cornerRadius = 8
         dismissButton.layer?.backgroundColor = NSColor.clear.cgColor
         dismissButton.layer?.backgroundColor = NSColor.clear.cgColor
         dismissButton.pointerCursor = true
         dismissButton.pointerCursor = true
         dismissButton.hoverHandler = { [weak dismissButton] hovering in
         dismissButton.hoverHandler = { [weak dismissButton] hovering in
@@ -641,50 +860,64 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         let buttonRow = NSStackView(views: [applyButton, savedButton, dismissButton])
         let buttonRow = NSStackView(views: [applyButton, savedButton, dismissButton])
         buttonRow.orientation = .horizontal
         buttonRow.orientation = .horizontal
         buttonRow.spacing = 8
         buttonRow.spacing = 8
-        buttonRow.alignment = .centerY
+        buttonRow.alignment = .top
         buttonRow.translatesAutoresizingMaskIntoConstraints = false
         buttonRow.translatesAutoresizingMaskIntoConstraints = false
         buttonRow.setContentHuggingPriority(.required, for: .horizontal)
         buttonRow.setContentHuggingPriority(.required, for: .horizontal)
         buttonRow.setContentCompressionResistancePriority(.required, for: .horizontal)
         buttonRow.setContentCompressionResistancePriority(.required, for: .horizontal)
+        buttonRow.setContentHuggingPriority(.required, for: .vertical)
+        buttonRow.setContentCompressionResistancePriority(.required, for: .vertical)
+        applyButton.setContentCompressionResistancePriority(.required, for: .horizontal)
+        savedButton.setContentCompressionResistancePriority(.required, for: .horizontal)
+        dismissButton.setContentCompressionResistancePriority(.required, for: .horizontal)
+
+        let middleColumn = NSStackView(views: [titleField, companyRow, descriptionField])
+        middleColumn.orientation = .vertical
+        middleColumn.spacing = 5
+        middleColumn.alignment = .leading
+        middleColumn.translatesAutoresizingMaskIntoConstraints = false
+        middleColumn.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        middleColumn.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+
+        let contentRow = NSStackView(views: [iconBox, middleColumn])
+        contentRow.orientation = .horizontal
+        contentRow.spacing = 14
+        contentRow.alignment = .top
+        contentRow.distribution = .fill
+        contentRow.translatesAutoresizingMaskIntoConstraints = false
+
+        card.addSubview(contentRow)
+        card.addSubview(buttonRow)
+        let actionCornerInset: CGFloat = 8
+        let contentToActionsGap: CGFloat = 12
+        let bodyTrailingInset: CGFloat = 16
+        NSLayoutConstraint.activate([
+            contentRow.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
+            contentRow.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -bodyTrailingInset),
+            contentRow.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
+            contentRow.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14),
 
 
-        // Title hugs the leading edge; a low–hugging-priority spacer absorbs remaining width so buttons stay trailing.
-        titleField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
-        titleField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+            buttonRow.topAnchor.constraint(equalTo: card.topAnchor, constant: actionCornerInset),
+            buttonRow.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -actionCornerInset),
 
 
-        let titleRowSpacer = NSView()
-        titleRowSpacer.translatesAutoresizingMaskIntoConstraints = false
-        titleRowSpacer.setContentHuggingPriority(NSLayoutConstraint.Priority(1), for: .horizontal)
-        titleRowSpacer.setContentCompressionResistancePriority(NSLayoutConstraint.Priority(1), for: .horizontal)
-
-        let titleAndActionsRow = NSStackView(views: [titleField, titleRowSpacer, buttonRow])
-        titleAndActionsRow.orientation = .horizontal
-        titleAndActionsRow.spacing = 0
-        titleAndActionsRow.setCustomSpacing(14, after: titleRowSpacer)
-        titleAndActionsRow.alignment = .centerY
-        titleAndActionsRow.distribution = .fill
-        titleAndActionsRow.userInterfaceLayoutDirection = .leftToRight
-        titleAndActionsRow.translatesAutoresizingMaskIntoConstraints = false
-
-        let contentColumn = NSStackView(views: [titleAndActionsRow, descriptionField])
-        contentColumn.orientation = .vertical
-        contentColumn.spacing = 6
-        contentColumn.alignment = .width
-        contentColumn.translatesAutoresizingMaskIntoConstraints = false
-
-        card.addSubview(contentColumn)
-        NSLayoutConstraint.activate([
-            contentColumn.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
-            contentColumn.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
-            contentColumn.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
-            contentColumn.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14),
-
-            applyButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 72),
-            applyButton.heightAnchor.constraint(equalToConstant: 28),
-            savedButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 72),
-            savedButton.heightAnchor.constraint(equalToConstant: 28),
-            dismissButton.widthAnchor.constraint(equalToConstant: 28),
-            dismissButton.heightAnchor.constraint(equalToConstant: 28),
-
-            descriptionField.widthAnchor.constraint(equalTo: contentColumn.widthAnchor)
+            middleColumn.trailingAnchor.constraint(lessThanOrEqualTo: buttonRow.leadingAnchor, constant: -contentToActionsGap),
+
+            iconBox.widthAnchor.constraint(equalToConstant: 58),
+            iconBox.heightAnchor.constraint(equalToConstant: 58),
+
+            categoryIcon.centerXAnchor.constraint(equalTo: iconBox.centerXAnchor),
+            categoryIcon.centerYAnchor.constraint(equalTo: iconBox.centerYAnchor),
+
+            buildingIcon.widthAnchor.constraint(equalToConstant: 14),
+            buildingIcon.heightAnchor.constraint(equalToConstant: 14),
+
+            applyButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 76),
+            applyButton.heightAnchor.constraint(equalToConstant: 32),
+            savedButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 84),
+            savedButton.heightAnchor.constraint(equalToConstant: 32),
+            dismissButton.widthAnchor.constraint(equalToConstant: 32),
+            dismissButton.heightAnchor.constraint(equalToConstant: 32),
+
+            descriptionField.widthAnchor.constraint(equalTo: middleColumn.widthAnchor)
         ])
         ])
 
 
         return card
         return card
@@ -692,7 +925,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
 
 
     private func styleJobSavedButton(_ button: NSButton) {
     private func styleJobSavedButton(_ button: NSButton) {
         button.wantsLayer = true
         button.wantsLayer = true
-        button.layer?.cornerRadius = 6
+        button.layer?.cornerRadius = 8
         let on = button.state == .on
         let on = button.state == .on
         let hovering = (button as? HoverableButton)?.isHovering ?? false
         let hovering = (button as? HoverableButton)?.isHovering ?? false
         if on {
         if on {
@@ -701,10 +934,10 @@ final class DashboardView: NSView, NSTextFieldDelegate {
             button.layer?.borderColor = Theme.brandBlue.cgColor
             button.layer?.borderColor = Theme.brandBlue.cgColor
             button.contentTintColor = Theme.brandBlue
             button.contentTintColor = Theme.brandBlue
         } else {
         } else {
-            button.layer?.backgroundColor = (hovering ? Theme.neutralHoverFill : Theme.cardBackground).cgColor
+            button.layer?.backgroundColor = (hovering ? Theme.proCardFill : Theme.cardBackground).cgColor
             button.layer?.borderWidth = 1
             button.layer?.borderWidth = 1
-            button.layer?.borderColor = Theme.border.cgColor
-            button.contentTintColor = Theme.primaryText
+            button.layer?.borderColor = Theme.brandBlue.cgColor
+            button.contentTintColor = Theme.brandBlue
         }
         }
     }
     }
 
 
@@ -726,6 +959,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         applySavedState(willSave, for: job)
         applySavedState(willSave, for: job)
         sender.state = willSave ? .on : .off
         sender.state = willSave ? .on : .off
         sender.title = willSave ? "Saved" : "Save"
         sender.title = willSave ? "Saved" : "Save"
+        sender.image = NSImage(systemSymbolName: willSave ? "heart.fill" : "heart", accessibilityDescription: nil)
         styleJobSavedButton(sender)
         styleJobSavedButton(sender)
         if isSavedJobsSidebarIndex(selectedSidebarIndex) {
         if isSavedJobsSidebarIndex(selectedSidebarIndex) {
             reloadSavedJobsListings()
             reloadSavedJobsListings()
@@ -819,9 +1053,9 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         }
         }
 
 
         jobSearchIcon.translatesAutoresizingMaskIntoConstraints = false
         jobSearchIcon.translatesAutoresizingMaskIntoConstraints = false
-        jobSearchIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 15, weight: .medium)
-        jobSearchIcon.image = NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: "Job search")
-        jobSearchIcon.contentTintColor = Theme.primaryText
+        jobSearchIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
+        jobSearchIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: "Ask AI")
+        jobSearchIcon.contentTintColor = Theme.brandBlue
 
 
         configureField(jobKeywordsField, placeholder: "Ask for roles, skills, salary, or job descriptions...")
         configureField(jobKeywordsField, placeholder: "Ask for roles, skills, salary, or job descriptions...")
 
 
@@ -865,14 +1099,18 @@ final class DashboardView: NSView, NSTextFieldDelegate {
 
 
         findJobsButton.translatesAutoresizingMaskIntoConstraints = false
         findJobsButton.translatesAutoresizingMaskIntoConstraints = false
         findJobsButton.title = ""
         findJobsButton.title = ""
+        findJobsButton.image = NSImage(systemSymbolName: "paperplane.fill", accessibilityDescription: nil)
+        findJobsButton.imagePosition = .imageLeading
+        findJobsButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
         findJobsButton.attributedTitle = NSAttributedString(
         findJobsButton.attributedTitle = NSAttributedString(
-            string: "Send",
+            string: " Send",
             attributes: [
             attributes: [
                 .font: NSFont.systemFont(ofSize: 14, weight: .semibold),
                 .font: NSFont.systemFont(ofSize: 14, weight: .semibold),
                 .foregroundColor: Theme.proCTAText,
                 .foregroundColor: Theme.proCTAText,
                 .kern: 0.35
                 .kern: 0.35
             ]
             ]
         )
         )
+        findJobsButton.contentTintColor = Theme.proCTAText
         findJobsButton.isBordered = false
         findJobsButton.isBordered = false
         findJobsButton.bezelStyle = .rounded
         findJobsButton.bezelStyle = .rounded
         findJobsButton.wantsLayer = true
         findJobsButton.wantsLayer = true
@@ -1324,6 +1562,26 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         window?.makeFirstResponder(nil)
         window?.makeFirstResponder(nil)
     }
     }
 
 
+    @objc private func didTapFeatureRole() {
+        focusSearchField(seed: "Find roles similar to: ")
+    }
+
+    @objc private func didTapFeatureCompany() {
+        focusSearchField(seed: "Find jobs at company: ")
+    }
+
+    @objc private func didTapFeatureSkill() {
+        focusSearchField(seed: "Find jobs that require skill: ")
+    }
+
+    private func focusSearchField(seed: String) {
+        jobKeywordsField.stringValue = seed
+        window?.makeFirstResponder(jobKeywordsField)
+        if let editor = jobKeywordsField.window?.fieldEditor(true, for: jobKeywordsField) as? NSTextView {
+            editor.moveToEndOfDocument(nil)
+        }
+    }
+
     @objc private func didTapLoadMoreJobs() {
     @objc private func didTapLoadMoreJobs() {
         let prompt = "Show more jobs"
         let prompt = "Show more jobs"
         guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
         guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
@@ -1364,7 +1622,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
                     )
                     )
                     self.chatMessages.append(ChatMessage(role: "assistant", content: reply))
                     self.chatMessages.append(ChatMessage(role: "assistant", content: reply))
                     self.appendChatBubble(text: reply, isUser: false, jobs: freshJobs)
                     self.appendChatBubble(text: reply, isUser: false, jobs: freshJobs)
-                    self.setChatStatusLabel("Ask for another role, company, or skill match")
+                    self.setChatStatusLabel(reply)
                 case .failure(let error):
                 case .failure(let error):
                     self.appendChatBubble(text: error.localizedDescription, isUser: false)
                     self.appendChatBubble(text: error.localizedDescription, isUser: false)
                     if error is URLError {
                     if error is URLError {
@@ -2289,6 +2547,132 @@ private struct OpenAIAPIErrorResponse: Codable {
     }
     }
 }
 }
 
 
+/// Home welcome row: three tappable shortcuts that seed the main search field (matches the reference dashboard tiles).
+private final class FeatureShortcutCardView: NSView {
+    private weak var actionTarget: AnyObject?
+    private var actionSelector: Selector
+
+    init(symbolName: String, title: String, subtitle: String, target: AnyObject?, action: Selector) {
+        self.actionTarget = target
+        self.actionSelector = action
+        super.init(frame: .zero)
+        translatesAutoresizingMaskIntoConstraints = false
+        wantsLayer = true
+        layer?.cornerRadius = 14
+        if #available(macOS 11.0, *) {
+            layer?.cornerCurve = .continuous
+        }
+        layer?.backgroundColor = NSColor.white.cgColor
+        layer?.masksToBounds = false
+        layer?.shadowColor = NSColor.black.withAlphaComponent(0.12).cgColor
+        layer?.shadowOffset = CGSize(width: 0, height: 2)
+        layer?.shadowRadius = 10
+        layer?.shadowOpacity = 1
+
+        let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
+        let iconWellColor = NSColor(srgbRed: 220 / 255, green: 235 / 255, blue: 252 / 255, alpha: 1)
+        let secondary = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
+
+        let iconHost = NSView()
+        iconHost.translatesAutoresizingMaskIntoConstraints = false
+        iconHost.wantsLayer = true
+        iconHost.layer?.backgroundColor = iconWellColor.cgColor
+        iconHost.layer?.cornerRadius = 22
+
+        let icon = NSImageView()
+        icon.translatesAutoresizingMaskIntoConstraints = false
+        icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
+        icon.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)
+        icon.contentTintColor = brandBlue
+        iconHost.addSubview(icon)
+
+        let titleField = NSTextField(wrappingLabelWithString: title)
+        titleField.font = .systemFont(ofSize: 15, weight: .bold)
+        titleField.textColor = brandBlue
+        titleField.maximumNumberOfLines = 1
+        titleField.isEditable = false
+        titleField.isBordered = false
+        titleField.drawsBackground = false
+        titleField.alignment = .left
+
+        let subtitleField = NSTextField(wrappingLabelWithString: subtitle)
+        subtitleField.font = .systemFont(ofSize: 11, weight: .regular)
+        subtitleField.textColor = secondary
+        subtitleField.maximumNumberOfLines = 2
+        subtitleField.isEditable = false
+        subtitleField.isBordered = false
+        subtitleField.drawsBackground = false
+        subtitleField.alignment = .left
+        subtitleField.preferredMaxLayoutWidth = 160
+
+        let textColumn = NSStackView(views: [titleField, subtitleField])
+        textColumn.orientation = .vertical
+        textColumn.spacing = 4
+        textColumn.alignment = .leading
+        textColumn.translatesAutoresizingMaskIntoConstraints = false
+        textColumn.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        textColumn.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+
+        let chevron = NSImageView()
+        chevron.translatesAutoresizingMaskIntoConstraints = false
+        chevron.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 13, weight: .semibold)
+        chevron.image = NSImage(systemSymbolName: "arrow.right", accessibilityDescription: nil)
+        chevron.contentTintColor = brandBlue
+        chevron.setContentHuggingPriority(.required, for: .horizontal)
+        chevron.setContentCompressionResistancePriority(.required, for: .horizontal)
+
+        iconHost.setContentHuggingPriority(.required, for: .horizontal)
+        iconHost.setContentCompressionResistancePriority(.required, for: .horizontal)
+
+        let row = NSStackView(views: [iconHost, textColumn, chevron])
+        row.orientation = .horizontal
+        row.spacing = 12
+        row.alignment = .centerY
+        row.distribution = .fill
+        row.translatesAutoresizingMaskIntoConstraints = false
+        addSubview(row)
+
+        NSLayoutConstraint.activate([
+            row.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14),
+            row.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14),
+            row.topAnchor.constraint(equalTo: topAnchor, constant: 16),
+            row.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16),
+
+            iconHost.widthAnchor.constraint(equalToConstant: 44),
+            iconHost.heightAnchor.constraint(equalToConstant: 44),
+            icon.centerXAnchor.constraint(equalTo: iconHost.centerXAnchor),
+            icon.centerYAnchor.constraint(equalTo: iconHost.centerYAnchor)
+        ])
+
+        setAccessibilityElement(true)
+        setAccessibilityRole(.button)
+        setAccessibilityLabel("\(title). \(subtitle)")
+    }
+
+    @available(*, unavailable)
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func layout() {
+        super.layout()
+        guard let layer = layer, bounds.width > 0, bounds.height > 0 else { return }
+        let r = bounds
+        layer.shadowPath = CGPath(roundedRect: r, cornerWidth: 14, cornerHeight: 14, transform: nil)
+    }
+
+    override func mouseDown(with event: NSEvent) {
+        if let target = actionTarget {
+            _ = target.perform(actionSelector, with: nil)
+        }
+    }
+
+    override func resetCursorRects() {
+        super.resetCursorRects()
+        addCursorRect(bounds, cursor: .pointingHand)
+    }
+}
+
 /// `NSButton` that carries a `JobListing` for card actions (`representedObject` is unavailable on `NSButton` in this target).
 /// `NSButton` that carries a `JobListing` for card actions (`representedObject` is unavailable on `NSButton` in this target).
 private final class JobPayloadButton: HoverableButton {
 private final class JobPayloadButton: HoverableButton {
     var jobPayload: JobListing?
     var jobPayload: JobListing?