Bläddra i källkod

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 3 veckor sedan
förälder
incheckning
8a252e60bf
1 ändrade filer med 464 tillägg och 80 borttagningar
  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 {
20 20
         static let chromeBackground = NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1)
21 21
         static let sidebarBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
22 22
         static let mainHostBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
23
-        /// Subtitle on the welcome hero: readable blue-gray aligned with brand.
24
-        static let welcomeSubtitleText = NSColor(srgbRed: 52 / 255, green: 92 / 255, blue: 142 / 255, alpha: 1)
23
+        /// Subtitle on the welcome hero: dark neutral gray to match the reference layout.
24
+        static let welcomeSubtitleText = NSColor(srgbRed: 64 / 255, green: 64 / 255, blue: 64 / 255, alpha: 1)
25 25
         static let selectionFill = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.12)
26 26
         static let cardBackground = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
27 27
         static let toggleBackground = NSColor(srgbRed: 232 / 255, green: 232 / 255, blue: 232 / 255, alpha: 1)
@@ -29,10 +29,13 @@ final class DashboardView: NSView, NSTextFieldDelegate {
29 29
         static let secondaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
30 30
         static let tertiaryText = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
31 31
         static let border = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
32
-        /// Job search bar outer stroke (charcoal).
33
-        static let searchBarBorder = NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 58 / 255, alpha: 1)
32
+        /// Home status strip and soft accents (light blue panel in the reference UI).
33
+        static let statusBannerBackground = NSColor(srgbRed: 232 / 255, green: 242 / 255, blue: 252 / 255, alpha: 1)
34
+        static let featureIconWell = NSColor(srgbRed: 220 / 255, green: 235 / 255, blue: 252 / 255, alpha: 1)
35
+        /// Job search bar outer stroke (soft blue-gray, pill field in the reference UI).
36
+        static let searchBarBorder = NSColor(srgbRed: 180 / 255, green: 200 / 255, blue: 228 / 255, alpha: 1)
34 37
         /// Search bar border on hover (brand-tinted, matches focus affordance elsewhere).
35
-        static let searchBarBorderHover = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.45)
38
+        static let searchBarBorderHover = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 0.55)
36 39
         static let proCardFill = NSColor(srgbRed: 239 / 255, green: 244 / 255, blue: 252 / 255, alpha: 1)
37 40
         static let proCardBorder = NSColor(srgbRed: 212 / 255, green: 210 / 255, blue: 208 / 255, alpha: 1)
38 41
         static let proAccent = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
@@ -81,11 +84,18 @@ final class DashboardView: NSView, NSTextFieldDelegate {
81 84
     private let findJobsCTAHost = NSView()
82 85
     private let findJobsCTAChrome = HoverableView()
83 86
     private var findJobsCTAGradientLayer: CAGradientLayer?
84
-    private let chatStatusStack = NSStackView()
87
+    private let statusBannerContainer = NSView()
88
+    private let statusBannerInner = NSView()
89
+    private let statusBannerRow = NSStackView()
85 90
     private let chatStatusSymbolContainer = NSView()
86 91
     private let chatStatusIcon = NSImageView()
87 92
     private let chatStatusLoadingIndicator = NSProgressIndicator()
88
-    private let chatStatusLabel = NSTextField(labelWithString: "Opening the vault...")
93
+    private let chatStatusLabel = NSTextField(wrappingLabelWithString: "Opening the vault...")
94
+    private let statusBrandTrailing = NSStackView()
95
+    private let statusBrandStarIcon = NSImageView()
96
+    private let statusBrandLabel = NSTextField(labelWithString: "AI Job Finder")
97
+    private let welcomeSparkleIcon = NSImageView()
98
+    private let featureCardsRow = NSStackView()
89 99
     private let chatScrollView = NSScrollView()
90 100
     private let chatDocumentView = JobListingsDocumentView()
91 101
     private let chatStack = NSStackView()
@@ -145,6 +155,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
145 155
         updateFindJobsCTAShadowPath()
146 156
         updateJobListingDescriptionWidths()
147 157
         updateChatBubbleWidths()
158
+        updateStatusBannerLabelWidth()
148 159
     }
149 160
 
150 161
     func render(_ data: DashboardData) {
@@ -226,14 +237,21 @@ final class DashboardView: NSView, NSTextFieldDelegate {
226 237
         configureSearchBar()
227 238
         configureChatViews()
228 239
 
229
-        let titleBlock = NSStackView(views: [greetingLabel, subtitleLabel])
240
+        welcomeSparkleIcon.translatesAutoresizingMaskIntoConstraints = false
241
+        welcomeSparkleIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
242
+        welcomeSparkleIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
243
+        welcomeSparkleIcon.contentTintColor = Theme.brandBlue
244
+
245
+        let titleBlock = NSStackView(views: [greetingLabel, subtitleLabel, welcomeSparkleIcon])
230 246
         titleBlock.orientation = .vertical
231 247
         titleBlock.spacing = 10
232 248
         titleBlock.alignment = .centerX
233 249
 
250
+        configureFeatureShortcutCards()
251
+
234 252
         let midSpacer = NSView()
235 253
         midSpacer.translatesAutoresizingMaskIntoConstraints = false
236
-        midSpacer.heightAnchor.constraint(equalToConstant: 18).isActive = true
254
+        midSpacer.heightAnchor.constraint(equalToConstant: 12).isActive = true
237 255
 
238 256
         let chatTopSpacer = NSView()
239 257
         chatTopSpacer.translatesAutoresizingMaskIntoConstraints = false
@@ -245,8 +263,9 @@ final class DashboardView: NSView, NSTextFieldDelegate {
245 263
 
246 264
         mainOverlay.addArrangedSubview(topInset)
247 265
         mainOverlay.addArrangedSubview(titleBlock)
266
+        mainOverlay.addArrangedSubview(featureCardsRow)
248 267
         mainOverlay.addArrangedSubview(midSpacer)
249
-        mainOverlay.addArrangedSubview(chatStatusStack)
268
+        mainOverlay.addArrangedSubview(statusBannerContainer)
250 269
         mainOverlay.addArrangedSubview(chatTopSpacer)
251 270
         mainOverlay.addArrangedSubview(chatScrollView)
252 271
         mainOverlay.addArrangedSubview(chatBottomSpacer)
@@ -280,7 +299,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
280 299
             nonHomeHost.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor, constant: -24),
281 300
 
282 301
             searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
283
-            chatStatusStack.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
302
+            statusBannerContainer.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
303
+            featureCardsRow.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
284 304
             chatScrollView.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
285 305
 
286 306
             greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 16),
@@ -290,19 +310,60 @@ final class DashboardView: NSView, NSTextFieldDelegate {
290 310
         ])
291 311
     }
292 312
 
313
+    private func configureFeatureShortcutCards() {
314
+        featureCardsRow.orientation = .horizontal
315
+        featureCardsRow.spacing = 12
316
+        featureCardsRow.distribution = .fillEqually
317
+        featureCardsRow.alignment = .top
318
+        featureCardsRow.translatesAutoresizingMaskIntoConstraints = false
319
+
320
+        let specs: [(symbol: String, title: String, subtitle: String, action: Selector)] = [
321
+            ("briefcase.fill", "Role", "Explore similar or better job roles", #selector(didTapFeatureRole)),
322
+            ("building.2.fill", "Company", "Find opportunities at other companies", #selector(didTapFeatureCompany)),
323
+            ("chevron.left.forwardslash.chevron.right", "Skill", "Match jobs that fit your skills", #selector(didTapFeatureSkill))
324
+        ]
325
+        for spec in specs {
326
+            let card = FeatureShortcutCardView(
327
+                symbolName: spec.symbol,
328
+                title: spec.title,
329
+                subtitle: spec.subtitle,
330
+                target: self,
331
+                action: spec.action
332
+            )
333
+            featureCardsRow.addArrangedSubview(card)
334
+        }
335
+    }
336
+
293 337
     private func configureChatViews() {
294
-        chatStatusStack.orientation = .vertical
295
-        chatStatusStack.spacing = 6
296
-        chatStatusStack.alignment = .centerX
297
-        chatStatusStack.translatesAutoresizingMaskIntoConstraints = false
338
+        statusBannerContainer.translatesAutoresizingMaskIntoConstraints = false
339
+        statusBannerContainer.setContentHuggingPriority(.defaultHigh, for: .vertical)
340
+
341
+        statusBannerInner.translatesAutoresizingMaskIntoConstraints = false
342
+        statusBannerInner.wantsLayer = true
343
+        statusBannerInner.layer?.backgroundColor = Theme.statusBannerBackground.cgColor
344
+        statusBannerInner.layer?.cornerRadius = 14
345
+        if #available(macOS 11.0, *) {
346
+            statusBannerInner.layer?.cornerCurve = .continuous
347
+        }
348
+        statusBannerInner.layer?.masksToBounds = true
349
+
350
+        statusBannerContainer.addSubview(statusBannerInner)
351
+
352
+        statusBannerRow.orientation = .horizontal
353
+        statusBannerRow.spacing = 12
354
+        statusBannerRow.alignment = .top
355
+        statusBannerRow.translatesAutoresizingMaskIntoConstraints = false
298 356
 
299 357
         chatStatusSymbolContainer.translatesAutoresizingMaskIntoConstraints = false
358
+        chatStatusSymbolContainer.wantsLayer = true
359
+        chatStatusSymbolContainer.layer?.backgroundColor = Theme.brandBlue.cgColor
360
+        chatStatusSymbolContainer.layer?.cornerRadius = 20
300 361
 
301 362
         chatStatusIcon.translatesAutoresizingMaskIntoConstraints = false
302 363
         chatStatusIcon.wantsLayer = true
303
-        chatStatusIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 36, weight: .regular)
364
+        chatStatusIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 15, weight: .semibold)
304 365
         chatStatusIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: "Assistant status")
305
-        chatStatusIcon.contentTintColor = Theme.brandBlue
366
+        chatStatusIcon.contentTintColor = .white
306 367
 
307 368
         chatStatusLoadingIndicator.translatesAutoresizingMaskIntoConstraints = false
308 369
         chatStatusLoadingIndicator.style = .spinning
@@ -323,19 +384,66 @@ final class DashboardView: NSView, NSTextFieldDelegate {
323 384
             chatStatusIcon.centerYAnchor.constraint(equalTo: chatStatusSymbolContainer.centerYAnchor),
324 385
             chatStatusLoadingIndicator.centerXAnchor.constraint(equalTo: chatStatusSymbolContainer.centerXAnchor),
325 386
             chatStatusLoadingIndicator.centerYAnchor.constraint(equalTo: chatStatusSymbolContainer.centerYAnchor),
326
-            chatStatusLoadingIndicator.widthAnchor.constraint(equalToConstant: 32),
327
-            chatStatusLoadingIndicator.heightAnchor.constraint(equalToConstant: 32)
387
+            chatStatusLoadingIndicator.widthAnchor.constraint(equalToConstant: 26),
388
+            chatStatusLoadingIndicator.heightAnchor.constraint(equalToConstant: 26)
328 389
         ])
329 390
 
330
-        chatStatusLabel.font = .systemFont(ofSize: 20, weight: .semibold)
391
+        chatStatusLabel.font = .systemFont(ofSize: 13, weight: .regular)
331 392
         chatStatusLabel.textColor = Theme.primaryText
332
-        chatStatusLabel.alignment = .center
333
-        chatStatusLabel.maximumNumberOfLines = 1
393
+        chatStatusLabel.alignment = .left
394
+        chatStatusLabel.maximumNumberOfLines = 0
395
+        chatStatusLabel.lineBreakMode = .byWordWrapping
396
+        chatStatusLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
397
+        chatStatusLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
398
+        chatStatusLabel.translatesAutoresizingMaskIntoConstraints = false
399
+
400
+        statusBrandStarIcon.translatesAutoresizingMaskIntoConstraints = false
401
+        statusBrandStarIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
402
+        statusBrandStarIcon.image = NSImage(systemSymbolName: "sparkle", accessibilityDescription: nil)
403
+        statusBrandStarIcon.contentTintColor = Theme.brandBlue
404
+
405
+        statusBrandLabel.font = .systemFont(ofSize: 12, weight: .semibold)
406
+        statusBrandLabel.textColor = Theme.brandBlue
407
+        statusBrandLabel.alignment = .right
408
+        statusBrandLabel.maximumNumberOfLines = 1
409
+
410
+        statusBrandTrailing.orientation = .horizontal
411
+        statusBrandTrailing.spacing = 5
412
+        statusBrandTrailing.alignment = .centerY
413
+        statusBrandTrailing.translatesAutoresizingMaskIntoConstraints = false
414
+        statusBrandTrailing.addArrangedSubview(statusBrandStarIcon)
415
+        statusBrandTrailing.addArrangedSubview(statusBrandLabel)
416
+        statusBrandTrailing.setContentHuggingPriority(.required, for: .horizontal)
417
+        statusBrandTrailing.setContentCompressionResistancePriority(.required, for: .horizontal)
418
+
419
+        statusBannerRow.addArrangedSubview(chatStatusSymbolContainer)
420
+        statusBannerRow.addArrangedSubview(chatStatusLabel)
421
+
422
+        statusBannerInner.addSubview(statusBannerRow)
423
+        statusBannerContainer.addSubview(statusBannerInner)
424
+        statusBannerContainer.addSubview(statusBrandTrailing)
425
+        statusBannerInner.setContentHuggingPriority(.defaultHigh, for: .horizontal)
426
+        statusBannerInner.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
334 427
 
335
-        chatStatusStack.addArrangedSubview(chatStatusSymbolContainer)
336
-        chatStatusStack.addArrangedSubview(chatStatusLabel)
428
+        NSLayoutConstraint.activate([
429
+            statusBannerInner.leadingAnchor.constraint(equalTo: statusBannerContainer.leadingAnchor),
430
+            statusBannerInner.topAnchor.constraint(equalTo: statusBannerContainer.topAnchor),
431
+            statusBannerInner.bottomAnchor.constraint(equalTo: statusBannerContainer.bottomAnchor),
432
+            statusBannerInner.trailingAnchor.constraint(lessThanOrEqualTo: statusBrandTrailing.leadingAnchor, constant: -16),
433
+
434
+            statusBrandTrailing.trailingAnchor.constraint(equalTo: statusBannerContainer.trailingAnchor),
435
+            statusBrandTrailing.centerYAnchor.constraint(equalTo: statusBannerContainer.centerYAnchor),
436
+
437
+            statusBannerRow.leadingAnchor.constraint(equalTo: statusBannerInner.leadingAnchor, constant: 14),
438
+            statusBannerRow.trailingAnchor.constraint(equalTo: statusBannerInner.trailingAnchor, constant: -16),
439
+            statusBannerRow.topAnchor.constraint(equalTo: statusBannerInner.topAnchor, constant: 10),
440
+            statusBannerRow.bottomAnchor.constraint(equalTo: statusBannerInner.bottomAnchor, constant: -10),
441
+
442
+            statusBannerContainer.heightAnchor.constraint(greaterThanOrEqualToConstant: 52)
443
+        ])
337 444
 
338 445
         syncChatStatusLoadingIndicator(forStatusText: chatStatusLabel.stringValue)
446
+        syncChatStatusBannerVisuals(forStatusText: chatStatusLabel.stringValue)
339 447
 
340 448
         chatDocumentView.translatesAutoresizingMaskIntoConstraints = false
341 449
         chatStack.orientation = .vertical
@@ -374,7 +482,43 @@ final class DashboardView: NSView, NSTextFieldDelegate {
374 482
     private func setChatStatusLabel(_ text: String) {
375 483
         chatStatusLabel.stringValue = text
376 484
         syncChatStatusLoadingIndicator(forStatusText: text)
485
+        syncChatStatusBannerVisuals(forStatusText: text)
377 486
         syncChatStatusSparkleAnimation()
487
+        updateStatusBannerLabelWidth()
488
+    }
489
+
490
+    private func updateStatusBannerLabelWidth() {
491
+        guard statusBannerContainer.bounds.width > 1 else { return }
492
+        let trailingWidth = max(96, statusBrandTrailing.fittingSize.width)
493
+        let chrome: CGFloat = 14 + 40 + 12 + 16 + 16 + trailingWidth
494
+        let target = max(80, statusBannerContainer.bounds.width - chrome)
495
+        if abs(chatStatusLabel.preferredMaxLayoutWidth - target) > 0.5 {
496
+            chatStatusLabel.preferredMaxLayoutWidth = target
497
+            chatStatusLabel.invalidateIntrinsicContentSize()
498
+        }
499
+    }
500
+
501
+    /// Leading status glyph: sparkles for prompts, magnifying glass for search-result summaries and errors.
502
+    private func syncChatStatusBannerVisuals(forStatusText text: String) {
503
+        if text == "Thinking..." { return }
504
+        let lower = text.lowercased()
505
+        let useMagnifyingGlass =
506
+            text.hasPrefix("Found ")
507
+            || text.hasPrefix("Here are")
508
+            || text.hasPrefix("No jobs")
509
+            || text.contains("couldn't find")
510
+            || lower.contains("try again")
511
+            || lower.contains("could not reach")
512
+            || lower.contains("search did not finish")
513
+        let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .semibold)
514
+        chatStatusIcon.symbolConfiguration = config
515
+        if useMagnifyingGlass {
516
+            chatStatusIcon.image = NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: "Search status")
517
+        } else {
518
+            chatStatusIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: "Assistant status")
519
+        }
520
+        chatStatusIcon.contentTintColor = .white
521
+        chatStatusSymbolContainer.layer?.backgroundColor = Theme.brandBlue.cgColor
378 522
     }
379 523
 
380 524
     private func syncChatStatusLoadingIndicator(forStatusText text: String) {
@@ -383,9 +527,12 @@ final class DashboardView: NSView, NSTextFieldDelegate {
383 527
             chatStatusIcon.isHidden = true
384 528
             chatStatusLoadingIndicator.isHidden = false
385 529
             chatStatusLoadingIndicator.startAnimation(nil)
530
+            chatStatusSymbolContainer.layer?.backgroundColor = Theme.featureIconWell.cgColor
386 531
         } else {
387 532
             chatStatusLoadingIndicator.stopAnimation(nil)
388 533
             chatStatusIcon.isHidden = false
534
+            chatStatusLoadingIndicator.isHidden = true
535
+            chatStatusSymbolContainer.layer?.backgroundColor = Theme.brandBlue.cgColor
389 536
         }
390 537
     }
391 538
 
@@ -399,7 +546,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
399 546
 
400 547
     private func shouldAnimateChatStatusSparkles(for statusText: String) -> Bool {
401 548
         statusText == "Ask me to find jobs"
402
-            || statusText == "Ask for another role, company, or skill match"
549
+            || statusText == "Opening the vault..."
403 550
     }
404 551
 
405 552
     private func syncChatStatusSparkleAnimation() {
@@ -542,30 +689,99 @@ final class DashboardView: NSView, NSTextFieldDelegate {
542 689
         persistSavedJobs()
543 690
     }
544 691
 
692
+    private func jobListingHostSubtitle(_ job: JobListing) -> String {
693
+        guard let raw = job.url, let url = URL(string: raw), let host = url.host?.lowercased() else {
694
+            return "Indeed"
695
+        }
696
+        if host.hasPrefix("www.") {
697
+            return String(host.dropFirst(4))
698
+        }
699
+        return host
700
+    }
701
+
702
+    private func jobListingCategorySymbol(for job: JobListing) -> String {
703
+        let blob = (job.title + " " + job.description).lowercased()
704
+        if blob.contains("machine learning") || blob.contains("deep learning") || blob.contains(" ml ") {
705
+            return "brain.head.profile"
706
+        }
707
+        if blob.contains("audio") || blob.contains(" sound ") || blob.contains("dsp") {
708
+            return "waveform"
709
+        }
710
+        if blob.contains("ios") || blob.contains("swift") || blob.contains("mobile") {
711
+            return "iphone"
712
+        }
713
+        if blob.contains("design") || blob.contains(" ux") || blob.contains("figma") {
714
+            return "paintpalette.fill"
715
+        }
716
+        if blob.contains("data ") || blob.contains("analytics") {
717
+            return "chart.bar.fill"
718
+        }
719
+        if blob.contains("ai") || blob.contains("llm") || blob.contains("nlp") {
720
+            return "cpu"
721
+        }
722
+        return "briefcase.fill"
723
+    }
724
+
545 725
     private func makeJobListingCard(_ job: JobListing, context: JobListingCardContext) -> NSView {
546 726
         let card = NSView()
547 727
         card.translatesAutoresizingMaskIntoConstraints = false
548 728
         card.wantsLayer = true
549 729
         card.layer?.backgroundColor = Theme.cardBackground.cgColor
550
-        card.layer?.cornerRadius = 12
730
+        card.layer?.cornerRadius = 14
551 731
         card.layer?.borderWidth = 1
552 732
         card.layer?.borderColor = Theme.border.cgColor
553 733
         card.layer?.masksToBounds = true
554 734
 
735
+        let iconBox = NSView()
736
+        iconBox.translatesAutoresizingMaskIntoConstraints = false
737
+        iconBox.wantsLayer = true
738
+        iconBox.layer?.backgroundColor = Theme.brandBlue.cgColor
739
+        iconBox.layer?.cornerRadius = 12
740
+        if #available(macOS 11.0, *) {
741
+            iconBox.layer?.cornerCurve = .continuous
742
+        }
743
+
744
+        let categoryIcon = NSImageView()
745
+        categoryIcon.translatesAutoresizingMaskIntoConstraints = false
746
+        categoryIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 22, weight: .medium)
747
+        categoryIcon.image = NSImage(systemSymbolName: jobListingCategorySymbol(for: job), accessibilityDescription: nil)
748
+        categoryIcon.contentTintColor = .white
749
+        iconBox.addSubview(categoryIcon)
750
+
555 751
         let titleField = NSTextField(labelWithString: job.title)
556 752
         titleField.font = .systemFont(ofSize: 16, weight: .semibold)
557
-        titleField.textColor = Theme.primaryText
753
+        titleField.textColor = Theme.brandBlue
558 754
         titleField.maximumNumberOfLines = 2
559 755
         titleField.lineBreakMode = .byWordWrapping
560 756
         titleField.alignment = .left
757
+        titleField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
561 758
         titleField.translatesAutoresizingMaskIntoConstraints = false
562 759
 
760
+        let buildingIcon = NSImageView()
761
+        buildingIcon.translatesAutoresizingMaskIntoConstraints = false
762
+        buildingIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .medium)
763
+        buildingIcon.image = NSImage(systemSymbolName: "building.2.fill", accessibilityDescription: nil)
764
+        buildingIcon.contentTintColor = Theme.welcomeSubtitleText
765
+
766
+        let companyLabel = NSTextField(labelWithString: jobListingHostSubtitle(job))
767
+        companyLabel.font = .systemFont(ofSize: 12, weight: .medium)
768
+        companyLabel.textColor = Theme.welcomeSubtitleText
769
+        companyLabel.maximumNumberOfLines = 1
770
+        companyLabel.lineBreakMode = .byTruncatingTail
771
+        companyLabel.translatesAutoresizingMaskIntoConstraints = false
772
+
773
+        let companyRow = NSStackView(views: [buildingIcon, companyLabel])
774
+        companyRow.orientation = .horizontal
775
+        companyRow.spacing = 5
776
+        companyRow.alignment = .centerY
777
+        companyRow.translatesAutoresizingMaskIntoConstraints = false
778
+
563 779
         let descriptionField = NSTextField(wrappingLabelWithString: job.description)
564 780
         descriptionField.font = .systemFont(ofSize: 13, weight: .regular)
565 781
         descriptionField.textColor = Theme.secondaryText
566
-        descriptionField.maximumNumberOfLines = 0
567
-        descriptionField.alignment = .left
782
+        descriptionField.maximumNumberOfLines = 2
568 783
         descriptionField.lineBreakMode = .byWordWrapping
784
+        descriptionField.alignment = .left
569 785
         descriptionField.baseWritingDirection = .leftToRight
570 786
         descriptionField.attributedStringValue = Self.jobListingDescriptionAttributedString(job.description)
571 787
         if let cell = descriptionField.cell as? NSTextFieldCell {
@@ -584,7 +800,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
584 800
         applyButton.bezelStyle = .rounded
585 801
         applyButton.font = .systemFont(ofSize: 13, weight: .semibold)
586 802
         applyButton.wantsLayer = true
587
-        applyButton.layer?.cornerRadius = 6
803
+        applyButton.layer?.cornerRadius = 8
588 804
         applyButton.layer?.backgroundColor = Theme.brandBlue.cgColor
589 805
         applyButton.contentTintColor = Theme.proCTAText
590 806
         applyButton.focusRingType = .none
@@ -603,6 +819,9 @@ final class DashboardView: NSView, NSTextFieldDelegate {
603 819
         savedButton.isBordered = false
604 820
         savedButton.bezelStyle = .rounded
605 821
         savedButton.font = .systemFont(ofSize: 13, weight: .semibold)
822
+        savedButton.image = NSImage(systemSymbolName: savedOn ? "heart.fill" : "heart", accessibilityDescription: nil)
823
+        savedButton.imagePosition = .imageLeading
824
+        savedButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
606 825
         savedButton.focusRingType = .none
607 826
         savedButton.state = savedOn ? .on : .off
608 827
         savedButton.pointerCursor = true
@@ -620,7 +839,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
620 839
         dismissButton.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Dismiss")
621 840
         dismissButton.imagePosition = .imageOnly
622 841
         dismissButton.imageScaling = .scaleProportionallyDown
623
-        dismissButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
842
+        dismissButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
624 843
         dismissButton.isBordered = false
625 844
         dismissButton.bezelStyle = .rounded
626 845
         dismissButton.contentTintColor = Theme.secondaryText
@@ -629,7 +848,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
629 848
         dismissButton.toolTip = context == .savedJobsPage ? "Remove from saved" : "Dismiss"
630 849
         dismissButton.focusRingType = .none
631 850
         dismissButton.wantsLayer = true
632
-        dismissButton.layer?.cornerRadius = 14
851
+        dismissButton.layer?.cornerRadius = 8
633 852
         dismissButton.layer?.backgroundColor = NSColor.clear.cgColor
634 853
         dismissButton.pointerCursor = true
635 854
         dismissButton.hoverHandler = { [weak dismissButton] hovering in
@@ -641,50 +860,64 @@ final class DashboardView: NSView, NSTextFieldDelegate {
641 860
         let buttonRow = NSStackView(views: [applyButton, savedButton, dismissButton])
642 861
         buttonRow.orientation = .horizontal
643 862
         buttonRow.spacing = 8
644
-        buttonRow.alignment = .centerY
863
+        buttonRow.alignment = .top
645 864
         buttonRow.translatesAutoresizingMaskIntoConstraints = false
646 865
         buttonRow.setContentHuggingPriority(.required, for: .horizontal)
647 866
         buttonRow.setContentCompressionResistancePriority(.required, for: .horizontal)
867
+        buttonRow.setContentHuggingPriority(.required, for: .vertical)
868
+        buttonRow.setContentCompressionResistancePriority(.required, for: .vertical)
869
+        applyButton.setContentCompressionResistancePriority(.required, for: .horizontal)
870
+        savedButton.setContentCompressionResistancePriority(.required, for: .horizontal)
871
+        dismissButton.setContentCompressionResistancePriority(.required, for: .horizontal)
872
+
873
+        let middleColumn = NSStackView(views: [titleField, companyRow, descriptionField])
874
+        middleColumn.orientation = .vertical
875
+        middleColumn.spacing = 5
876
+        middleColumn.alignment = .leading
877
+        middleColumn.translatesAutoresizingMaskIntoConstraints = false
878
+        middleColumn.setContentHuggingPriority(.defaultLow, for: .horizontal)
879
+        middleColumn.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
880
+
881
+        let contentRow = NSStackView(views: [iconBox, middleColumn])
882
+        contentRow.orientation = .horizontal
883
+        contentRow.spacing = 14
884
+        contentRow.alignment = .top
885
+        contentRow.distribution = .fill
886
+        contentRow.translatesAutoresizingMaskIntoConstraints = false
887
+
888
+        card.addSubview(contentRow)
889
+        card.addSubview(buttonRow)
890
+        let actionCornerInset: CGFloat = 8
891
+        let contentToActionsGap: CGFloat = 12
892
+        let bodyTrailingInset: CGFloat = 16
893
+        NSLayoutConstraint.activate([
894
+            contentRow.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
895
+            contentRow.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -bodyTrailingInset),
896
+            contentRow.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
897
+            contentRow.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14),
648 898
 
649
-        // Title hugs the leading edge; a low–hugging-priority spacer absorbs remaining width so buttons stay trailing.
650
-        titleField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
651
-        titleField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
899
+            buttonRow.topAnchor.constraint(equalTo: card.topAnchor, constant: actionCornerInset),
900
+            buttonRow.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -actionCornerInset),
652 901
 
653
-        let titleRowSpacer = NSView()
654
-        titleRowSpacer.translatesAutoresizingMaskIntoConstraints = false
655
-        titleRowSpacer.setContentHuggingPriority(NSLayoutConstraint.Priority(1), for: .horizontal)
656
-        titleRowSpacer.setContentCompressionResistancePriority(NSLayoutConstraint.Priority(1), for: .horizontal)
657
-
658
-        let titleAndActionsRow = NSStackView(views: [titleField, titleRowSpacer, buttonRow])
659
-        titleAndActionsRow.orientation = .horizontal
660
-        titleAndActionsRow.spacing = 0
661
-        titleAndActionsRow.setCustomSpacing(14, after: titleRowSpacer)
662
-        titleAndActionsRow.alignment = .centerY
663
-        titleAndActionsRow.distribution = .fill
664
-        titleAndActionsRow.userInterfaceLayoutDirection = .leftToRight
665
-        titleAndActionsRow.translatesAutoresizingMaskIntoConstraints = false
666
-
667
-        let contentColumn = NSStackView(views: [titleAndActionsRow, descriptionField])
668
-        contentColumn.orientation = .vertical
669
-        contentColumn.spacing = 6
670
-        contentColumn.alignment = .width
671
-        contentColumn.translatesAutoresizingMaskIntoConstraints = false
672
-
673
-        card.addSubview(contentColumn)
674
-        NSLayoutConstraint.activate([
675
-            contentColumn.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
676
-            contentColumn.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
677
-            contentColumn.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
678
-            contentColumn.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14),
679
-
680
-            applyButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 72),
681
-            applyButton.heightAnchor.constraint(equalToConstant: 28),
682
-            savedButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 72),
683
-            savedButton.heightAnchor.constraint(equalToConstant: 28),
684
-            dismissButton.widthAnchor.constraint(equalToConstant: 28),
685
-            dismissButton.heightAnchor.constraint(equalToConstant: 28),
686
-
687
-            descriptionField.widthAnchor.constraint(equalTo: contentColumn.widthAnchor)
902
+            middleColumn.trailingAnchor.constraint(lessThanOrEqualTo: buttonRow.leadingAnchor, constant: -contentToActionsGap),
903
+
904
+            iconBox.widthAnchor.constraint(equalToConstant: 58),
905
+            iconBox.heightAnchor.constraint(equalToConstant: 58),
906
+
907
+            categoryIcon.centerXAnchor.constraint(equalTo: iconBox.centerXAnchor),
908
+            categoryIcon.centerYAnchor.constraint(equalTo: iconBox.centerYAnchor),
909
+
910
+            buildingIcon.widthAnchor.constraint(equalToConstant: 14),
911
+            buildingIcon.heightAnchor.constraint(equalToConstant: 14),
912
+
913
+            applyButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 76),
914
+            applyButton.heightAnchor.constraint(equalToConstant: 32),
915
+            savedButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 84),
916
+            savedButton.heightAnchor.constraint(equalToConstant: 32),
917
+            dismissButton.widthAnchor.constraint(equalToConstant: 32),
918
+            dismissButton.heightAnchor.constraint(equalToConstant: 32),
919
+
920
+            descriptionField.widthAnchor.constraint(equalTo: middleColumn.widthAnchor)
688 921
         ])
689 922
 
690 923
         return card
@@ -692,7 +925,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
692 925
 
693 926
     private func styleJobSavedButton(_ button: NSButton) {
694 927
         button.wantsLayer = true
695
-        button.layer?.cornerRadius = 6
928
+        button.layer?.cornerRadius = 8
696 929
         let on = button.state == .on
697 930
         let hovering = (button as? HoverableButton)?.isHovering ?? false
698 931
         if on {
@@ -701,10 +934,10 @@ final class DashboardView: NSView, NSTextFieldDelegate {
701 934
             button.layer?.borderColor = Theme.brandBlue.cgColor
702 935
             button.contentTintColor = Theme.brandBlue
703 936
         } else {
704
-            button.layer?.backgroundColor = (hovering ? Theme.neutralHoverFill : Theme.cardBackground).cgColor
937
+            button.layer?.backgroundColor = (hovering ? Theme.proCardFill : Theme.cardBackground).cgColor
705 938
             button.layer?.borderWidth = 1
706
-            button.layer?.borderColor = Theme.border.cgColor
707
-            button.contentTintColor = Theme.primaryText
939
+            button.layer?.borderColor = Theme.brandBlue.cgColor
940
+            button.contentTintColor = Theme.brandBlue
708 941
         }
709 942
     }
710 943
 
@@ -726,6 +959,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
726 959
         applySavedState(willSave, for: job)
727 960
         sender.state = willSave ? .on : .off
728 961
         sender.title = willSave ? "Saved" : "Save"
962
+        sender.image = NSImage(systemSymbolName: willSave ? "heart.fill" : "heart", accessibilityDescription: nil)
729 963
         styleJobSavedButton(sender)
730 964
         if isSavedJobsSidebarIndex(selectedSidebarIndex) {
731 965
             reloadSavedJobsListings()
@@ -819,9 +1053,9 @@ final class DashboardView: NSView, NSTextFieldDelegate {
819 1053
         }
820 1054
 
821 1055
         jobSearchIcon.translatesAutoresizingMaskIntoConstraints = false
822
-        jobSearchIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 15, weight: .medium)
823
-        jobSearchIcon.image = NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: "Job search")
824
-        jobSearchIcon.contentTintColor = Theme.primaryText
1056
+        jobSearchIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
1057
+        jobSearchIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: "Ask AI")
1058
+        jobSearchIcon.contentTintColor = Theme.brandBlue
825 1059
 
826 1060
         configureField(jobKeywordsField, placeholder: "Ask for roles, skills, salary, or job descriptions...")
827 1061
 
@@ -865,14 +1099,18 @@ final class DashboardView: NSView, NSTextFieldDelegate {
865 1099
 
866 1100
         findJobsButton.translatesAutoresizingMaskIntoConstraints = false
867 1101
         findJobsButton.title = ""
1102
+        findJobsButton.image = NSImage(systemSymbolName: "paperplane.fill", accessibilityDescription: nil)
1103
+        findJobsButton.imagePosition = .imageLeading
1104
+        findJobsButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
868 1105
         findJobsButton.attributedTitle = NSAttributedString(
869
-            string: "Send",
1106
+            string: " Send",
870 1107
             attributes: [
871 1108
                 .font: NSFont.systemFont(ofSize: 14, weight: .semibold),
872 1109
                 .foregroundColor: Theme.proCTAText,
873 1110
                 .kern: 0.35
874 1111
             ]
875 1112
         )
1113
+        findJobsButton.contentTintColor = Theme.proCTAText
876 1114
         findJobsButton.isBordered = false
877 1115
         findJobsButton.bezelStyle = .rounded
878 1116
         findJobsButton.wantsLayer = true
@@ -1324,6 +1562,26 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1324 1562
         window?.makeFirstResponder(nil)
1325 1563
     }
1326 1564
 
1565
+    @objc private func didTapFeatureRole() {
1566
+        focusSearchField(seed: "Find roles similar to: ")
1567
+    }
1568
+
1569
+    @objc private func didTapFeatureCompany() {
1570
+        focusSearchField(seed: "Find jobs at company: ")
1571
+    }
1572
+
1573
+    @objc private func didTapFeatureSkill() {
1574
+        focusSearchField(seed: "Find jobs that require skill: ")
1575
+    }
1576
+
1577
+    private func focusSearchField(seed: String) {
1578
+        jobKeywordsField.stringValue = seed
1579
+        window?.makeFirstResponder(jobKeywordsField)
1580
+        if let editor = jobKeywordsField.window?.fieldEditor(true, for: jobKeywordsField) as? NSTextView {
1581
+            editor.moveToEndOfDocument(nil)
1582
+        }
1583
+    }
1584
+
1327 1585
     @objc private func didTapLoadMoreJobs() {
1328 1586
         let prompt = "Show more jobs"
1329 1587
         guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
@@ -1364,7 +1622,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1364 1622
                     )
1365 1623
                     self.chatMessages.append(ChatMessage(role: "assistant", content: reply))
1366 1624
                     self.appendChatBubble(text: reply, isUser: false, jobs: freshJobs)
1367
-                    self.setChatStatusLabel("Ask for another role, company, or skill match")
1625
+                    self.setChatStatusLabel(reply)
1368 1626
                 case .failure(let error):
1369 1627
                     self.appendChatBubble(text: error.localizedDescription, isUser: false)
1370 1628
                     if error is URLError {
@@ -2289,6 +2547,132 @@ private struct OpenAIAPIErrorResponse: Codable {
2289 2547
     }
2290 2548
 }
2291 2549
 
2550
+/// Home welcome row: three tappable shortcuts that seed the main search field (matches the reference dashboard tiles).
2551
+private final class FeatureShortcutCardView: NSView {
2552
+    private weak var actionTarget: AnyObject?
2553
+    private var actionSelector: Selector
2554
+
2555
+    init(symbolName: String, title: String, subtitle: String, target: AnyObject?, action: Selector) {
2556
+        self.actionTarget = target
2557
+        self.actionSelector = action
2558
+        super.init(frame: .zero)
2559
+        translatesAutoresizingMaskIntoConstraints = false
2560
+        wantsLayer = true
2561
+        layer?.cornerRadius = 14
2562
+        if #available(macOS 11.0, *) {
2563
+            layer?.cornerCurve = .continuous
2564
+        }
2565
+        layer?.backgroundColor = NSColor.white.cgColor
2566
+        layer?.masksToBounds = false
2567
+        layer?.shadowColor = NSColor.black.withAlphaComponent(0.12).cgColor
2568
+        layer?.shadowOffset = CGSize(width: 0, height: 2)
2569
+        layer?.shadowRadius = 10
2570
+        layer?.shadowOpacity = 1
2571
+
2572
+        let brandBlue = NSColor(srgbRed: 37 / 255, green: 87 / 255, blue: 167 / 255, alpha: 1)
2573
+        let iconWellColor = NSColor(srgbRed: 220 / 255, green: 235 / 255, blue: 252 / 255, alpha: 1)
2574
+        let secondary = NSColor(srgbRed: 118 / 255, green: 118 / 255, blue: 118 / 255, alpha: 1)
2575
+
2576
+        let iconHost = NSView()
2577
+        iconHost.translatesAutoresizingMaskIntoConstraints = false
2578
+        iconHost.wantsLayer = true
2579
+        iconHost.layer?.backgroundColor = iconWellColor.cgColor
2580
+        iconHost.layer?.cornerRadius = 22
2581
+
2582
+        let icon = NSImageView()
2583
+        icon.translatesAutoresizingMaskIntoConstraints = false
2584
+        icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
2585
+        icon.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)
2586
+        icon.contentTintColor = brandBlue
2587
+        iconHost.addSubview(icon)
2588
+
2589
+        let titleField = NSTextField(wrappingLabelWithString: title)
2590
+        titleField.font = .systemFont(ofSize: 15, weight: .bold)
2591
+        titleField.textColor = brandBlue
2592
+        titleField.maximumNumberOfLines = 1
2593
+        titleField.isEditable = false
2594
+        titleField.isBordered = false
2595
+        titleField.drawsBackground = false
2596
+        titleField.alignment = .left
2597
+
2598
+        let subtitleField = NSTextField(wrappingLabelWithString: subtitle)
2599
+        subtitleField.font = .systemFont(ofSize: 11, weight: .regular)
2600
+        subtitleField.textColor = secondary
2601
+        subtitleField.maximumNumberOfLines = 2
2602
+        subtitleField.isEditable = false
2603
+        subtitleField.isBordered = false
2604
+        subtitleField.drawsBackground = false
2605
+        subtitleField.alignment = .left
2606
+        subtitleField.preferredMaxLayoutWidth = 160
2607
+
2608
+        let textColumn = NSStackView(views: [titleField, subtitleField])
2609
+        textColumn.orientation = .vertical
2610
+        textColumn.spacing = 4
2611
+        textColumn.alignment = .leading
2612
+        textColumn.translatesAutoresizingMaskIntoConstraints = false
2613
+        textColumn.setContentHuggingPriority(.defaultLow, for: .horizontal)
2614
+        textColumn.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
2615
+
2616
+        let chevron = NSImageView()
2617
+        chevron.translatesAutoresizingMaskIntoConstraints = false
2618
+        chevron.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 13, weight: .semibold)
2619
+        chevron.image = NSImage(systemSymbolName: "arrow.right", accessibilityDescription: nil)
2620
+        chevron.contentTintColor = brandBlue
2621
+        chevron.setContentHuggingPriority(.required, for: .horizontal)
2622
+        chevron.setContentCompressionResistancePriority(.required, for: .horizontal)
2623
+
2624
+        iconHost.setContentHuggingPriority(.required, for: .horizontal)
2625
+        iconHost.setContentCompressionResistancePriority(.required, for: .horizontal)
2626
+
2627
+        let row = NSStackView(views: [iconHost, textColumn, chevron])
2628
+        row.orientation = .horizontal
2629
+        row.spacing = 12
2630
+        row.alignment = .centerY
2631
+        row.distribution = .fill
2632
+        row.translatesAutoresizingMaskIntoConstraints = false
2633
+        addSubview(row)
2634
+
2635
+        NSLayoutConstraint.activate([
2636
+            row.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14),
2637
+            row.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14),
2638
+            row.topAnchor.constraint(equalTo: topAnchor, constant: 16),
2639
+            row.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16),
2640
+
2641
+            iconHost.widthAnchor.constraint(equalToConstant: 44),
2642
+            iconHost.heightAnchor.constraint(equalToConstant: 44),
2643
+            icon.centerXAnchor.constraint(equalTo: iconHost.centerXAnchor),
2644
+            icon.centerYAnchor.constraint(equalTo: iconHost.centerYAnchor)
2645
+        ])
2646
+
2647
+        setAccessibilityElement(true)
2648
+        setAccessibilityRole(.button)
2649
+        setAccessibilityLabel("\(title). \(subtitle)")
2650
+    }
2651
+
2652
+    @available(*, unavailable)
2653
+    required init?(coder: NSCoder) {
2654
+        fatalError("init(coder:) has not been implemented")
2655
+    }
2656
+
2657
+    override func layout() {
2658
+        super.layout()
2659
+        guard let layer = layer, bounds.width > 0, bounds.height > 0 else { return }
2660
+        let r = bounds
2661
+        layer.shadowPath = CGPath(roundedRect: r, cornerWidth: 14, cornerHeight: 14, transform: nil)
2662
+    }
2663
+
2664
+    override func mouseDown(with event: NSEvent) {
2665
+        if let target = actionTarget {
2666
+            _ = target.perform(actionSelector, with: nil)
2667
+        }
2668
+    }
2669
+
2670
+    override func resetCursorRects() {
2671
+        super.resetCursorRects()
2672
+        addCursorRect(bounds, cursor: .pointingHand)
2673
+    }
2674
+}
2675
+
2292 2676
 /// `NSButton` that carries a `JobListing` for card actions (`representedObject` is unavailable on `NSButton` in this target).
2293 2677
 private final class JobPayloadButton: HoverableButton {
2294 2678
     var jobPayload: JobListing?