From 2182f72105033db025bb29e08319586112e53c5f Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 10 Jun 2026 19:37:25 +0800
Subject: [PATCH] Fix empty Space lane fill

---
 C1.source/Sources/Aligner/QuickSwitchRootView.swift | 1079 ++++++++++++++++++++++++++++++++++++++++++++++-------------
 1 files changed, 845 insertions(+), 234 deletions(-)

diff --git a/C1.source/Sources/Aligner/QuickSwitchRootView.swift b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
index cb47fe9..0d3590c 100644
--- a/C1.source/Sources/Aligner/QuickSwitchRootView.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -22,11 +22,14 @@
     private let waterfallLayer = CALayer()
     private let waterfallContentLayer = CALayer()
     private lazy var closeConfirmationLayers = makeCloseConfirmationLayers()
+    private let closeFeedbackLayer = CALayer()
+    private let closeFeedbackTextLayer = CATextLayer()
     private let appIconProvider: any AppIconProviderProtocol = WorkspaceAppIconProvider()
     private let skeletonThumbnailProvider = NativeSkeletonThumbnailProvider(size: NSSize(width: 160, height: 96))
     private var screenshotSourcesByWindowID: [UInt32: ScreenshotResolutionSource] = [:]
     private var screenshotSkippedReasonsByWindowID: [UInt32: String] = [:]
     private var currentViewModel: QuickSwitchViewModel?
+    private var spaceLaneDisplayGroups: [SpaceLaneDisplayGroupLayers] = []
     private var spaceLaneSegments: [SpaceLaneSegmentLayers] = []
     private var spaceLaneContentWidth: CGFloat = 0
     private var spaceLaneScrollOffset: CGFloat = 0
@@ -40,9 +43,14 @@
     private var hoveredSpaceID: UInt64?
     private var hoveredCloseTarget: CloseTarget?
     private var pendingCloseTarget: CloseTarget?
+    private var pendingCloseRequest: QuickSwitchCloseRequest?
     private var pendingCloseOptOut = false
     private var closeConfirmationArrowEdge: CloseConfirmationArrowEdge?
     private var closeConfirmationRequired = true
+    private var closeFeedback: CloseFeedback?
+    private var closeFeedbackHideTask: Task<Void, Never>?
+    private var hoverTarget: HoverTarget = .none
+    private var hoverTargetSource: HoverTargetSource = .event
     private var hoveredSpaceLaneID: UInt64?
     private var focusedSpaceID: UInt64?
     private var debugHoveredAppGroupIndex: Int?
@@ -108,9 +116,92 @@
         let cancelButtonTextLayer: CATextLayer
     }
 
+    private struct CloseFeedback: Equatable, Sendable {
+        enum Kind: String, Sendable {
+            case progress
+            case failure
+        }
+
+        let kind: Kind
+        let message: String
+        let targetKind: String
+        let appGroupIndex: Int
+        let windowID: UInt32?
+    }
+
     private enum CloseConfirmationArrowEdge: String {
         case left
         case right
+    }
+
+    private enum HoverTarget: Equatable {
+        case none
+        case space(UInt64)
+        case app(Int)
+        case window(appGroupIndex: Int, windowID: UInt32, spaceID: UInt64?)
+
+        var kindDescription: String {
+            switch self {
+            case .none:
+                return "none"
+            case .space:
+                return "space"
+            case .app:
+                return "app"
+            case .window:
+                return "window"
+            }
+        }
+
+        var appGroupIndex: Int? {
+            switch self {
+            case .app(let appGroupIndex):
+                return appGroupIndex
+            case .window(let appGroupIndex, _, _):
+                return appGroupIndex
+            case .none, .space:
+                return nil
+            }
+        }
+
+        var windowID: UInt32? {
+            switch self {
+            case .window(_, let windowID, _):
+                return windowID
+            case .none, .space, .app:
+                return nil
+            }
+        }
+
+        var spaceID: UInt64? {
+            switch self {
+            case .space(let spaceID):
+                return spaceID
+            case .window(_, _, let spaceID):
+                return spaceID
+            case .none, .app:
+                return nil
+            }
+        }
+
+        var spaceLaneID: UInt64? {
+            guard case .space(let spaceID) = self else { return nil }
+            return spaceID
+        }
+
+        var shouldAlignWaterfallWithAppShelf: Bool {
+            switch self {
+            case .app:
+                return true
+            case .none, .space, .window:
+                return false
+            }
+        }
+    }
+
+    private enum HoverTargetSource: String {
+        case event
+        case debug
     }
 
     private struct SpaceLaneSegmentLayers {
@@ -121,6 +212,12 @@
         let appLayer: CATextLayer
         let windowBlocks: [CALayer]
         let fullscreenMarkerLayer: CALayer?
+    }
+
+    private struct SpaceLaneDisplayGroupLayers {
+        let display: QuickSwitchDisplayViewModel
+        let containerLayer: CALayer
+        let segments: [SpaceLaneSegmentLayers]
     }
 
     private struct AppShelfItemLayers {
@@ -184,11 +281,15 @@
         static let columnWidth: CGFloat = 252
         static let columnMinWidth: CGFloat = 220
         static let columnMaxWidth: CGFloat = 292
-        static let columnPadding: CGFloat = 12
+        static let contentHorizontalPadding: CGFloat = 0
+        static let columnHorizontalPadding: CGFloat = 0
+        static let columnVerticalPadding: CGFloat = 0
+        static let columnHeaderTextInset: CGFloat = 12
         static let headerHeight: CGFloat = 34
         static let cardHeight: CGFloat = 138
         static let cardGap: CGFloat = 12
         static let revealPadding: CGFloat = 24
+        static let horizontalRevealPadding: CGFloat = 0
     }
 
     private enum WaterfallHorizontalIntent {
@@ -200,6 +301,16 @@
         static let tolerance: CGFloat = 1
         static let animationDuration: CFTimeInterval = 0.18
         static let animationTimingName = CAMediaTimingFunctionName.easeOut
+    }
+
+    private enum SpaceLaneMetrics {
+        static let horizontalPadding: CGFloat = 0
+        static let groupGap: CGFloat = 36
+        static let groupHorizontalPadding: CGFloat = 24
+        static let groupVerticalPadding: CGFloat = 6
+        static let segmentGap: CGFloat = 12
+        static let segmentMinWidth: CGFloat = 96
+        static let segmentMaxWidth: CGFloat = 150
     }
 
     override init(frame frameRect: NSRect) {
@@ -231,7 +342,7 @@
 
         let trackingArea = NSTrackingArea(
             rect: bounds,
-            options: [.activeInKeyWindow, .mouseMoved, .mouseEnteredAndExited, .inVisibleRect],
+            options: [.activeAlways, .mouseMoved, .mouseEnteredAndExited, .inVisibleRect],
             owner: self,
             userInfo: nil
         )
@@ -268,6 +379,8 @@
     }
 
     override func scrollWheel(with event: NSEvent) {
+        refreshHoverTarget(at: event.locationInWindow)
+
         let dominantDelta = abs(event.scrollingDeltaX) >= abs(event.scrollingDeltaY)
             ? event.scrollingDeltaX
             : event.scrollingDeltaY
@@ -318,43 +431,13 @@
             hoveredCloseTarget = closeTarget
             needsLayout = true
         }
-
-        if let segment = spaceLaneSegment(at: event.locationInWindow) {
-            hoverSpaceLaneSegment(segment)
-            return
-        }
-
-        if let card = waterfallCard(at: event.locationInWindow) {
-            hoveredSpaceLaneID = nil
-            updateHoverState(
-                appGroupIndex: card.item.appGroupIndex,
-                windowID: card.item.window.id,
-                spaceID: card.item.primarySpaceID
-            )
-            return
-        }
-
-        if let item = appShelfItem(at: event.locationInWindow) {
-            hoveredSpaceLaneID = nil
-            updateHoverState(
-                appGroupIndex: item.item.appGroupIndex,
-                windowID: nil,
-                spaceID: nil,
-                alignsWaterfallWithAppShelf: true
-            )
-            return
-        }
-
-        hoveredSpaceLaneID = nil
-        hoveredCloseTarget = nil
-        updateHoverState(appGroupIndex: nil, windowID: nil, spaceID: nil)
+        refreshHoverTarget(at: event.locationInWindow)
     }
 
     override func mouseExited(with event: NSEvent) {
         guard debugHoveredAppGroupIndex == nil else { return }
-        hoveredSpaceLaneID = nil
         hoveredCloseTarget = nil
-        updateHoverState(appGroupIndex: nil, windowID: nil, spaceID: nil)
+        setHoverTarget(.none)
     }
 
     override func mouseDown(with event: NSEvent) {
@@ -392,10 +475,8 @@
 
     func setDebugHoveredAppGroupIndex(_ index: Int?) {
         debugHoveredAppGroupIndex = index
+        setHoverTarget(.none, source: .debug)
         hoveredAppGroupIndex = index
-        hoveredWindowID = nil
-        hoveredSpaceID = nil
-        hoveredSpaceLaneID = nil
         needsLayout = true
     }
 
@@ -457,10 +538,47 @@
         closeConfirmationRequired = required
         if !required {
             pendingCloseTarget = nil
+            pendingCloseRequest = nil
             pendingCloseOptOut = false
             closeConfirmationLayers.containerLayer.isHidden = true
             closeConfirmationArrowEdge = nil
         }
+        needsLayout = true
+    }
+
+    func showCloseProgress(for request: QuickSwitchCloseRequest) {
+        setCloseFeedback(
+            CloseFeedback(
+                kind: .progress,
+                message: "正在关闭...",
+                targetKind: request.kindDescription,
+                appGroupIndex: request.appGroupIndex,
+                windowID: request.windowID
+            ),
+            autoHideAfter: nil
+        )
+    }
+
+    func showCloseFailure(message: String, for request: QuickSwitchCloseRequest) {
+        setCloseFeedback(
+            CloseFeedback(
+                kind: .failure,
+                message: message,
+                targetKind: request.kindDescription,
+                appGroupIndex: request.appGroupIndex,
+                windowID: request.windowID
+            ),
+            autoHideAfter: 2.75
+        )
+    }
+
+    func clearCloseFeedback() {
+        closeFeedbackHideTask?.cancel()
+        closeFeedbackHideTask = nil
+        closeFeedback = nil
+        closeFeedbackLayer.isHidden = true
+        closeFeedbackLayer.opacity = 0
+        closeFeedbackTextLayer.string = ""
         needsLayout = true
     }
 
@@ -471,7 +589,9 @@
         guard let card = waterfallCardsByWindowID()[windowID] else { return }
         card.thumbnailLayer.contents = resolution.image
         if case .realScreenshot = resolution.source {
-            card.shineLayer.opacity = card.item.window.id == effectiveSelection?.windowID ? 1 : 0
+            let isSelected = card.item.window.id == effectiveSelection?.windowID
+            let isHovered = card.item.window.id == hoveredWindowID
+            card.shineLayer.opacity = shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) ? 1 : 0
         }
         needsLayout = true
     }
@@ -507,20 +627,16 @@
         lastSpaceFocusedAppGroupIndex = nil
         lastSpaceLaneClickSpaceID = nil
         lastAppShelfScrollSyncedAppGroupIndex = nil
-        hoveredWindowID = nil
-        hoveredSpaceID = nil
+        focusedSpaceID = nil
         hoveredCloseTarget = nil
         pendingCloseTarget = nil
+        pendingCloseRequest = nil
         pendingCloseOptOut = false
         closeConfirmationLayers.containerLayer.isHidden = true
         closeConfirmationArrowEdge = nil
-        hoveredSpaceLaneID = nil
-        focusedSpaceID = nil
-        if debugHoveredAppGroupIndex == nil {
-            hoveredAppGroupIndex = nil
-        } else {
-            hoveredAppGroupIndex = debugHoveredAppGroupIndex
-        }
+        clearCloseFeedback()
+        setHoverTarget(.none)
+        hoveredAppGroupIndex = debugHoveredAppGroupIndex
         if let currentSelection {
             columnSelectionHistory[currentSelection.appGroupIndex] = currentSelection.windowIndex
         }
@@ -553,17 +669,26 @@
             "glassBorderWidth": Double(glassLayer.borderWidth),
             "glassShadowOpacity": Double(glassLayer.shadowOpacity),
             "spaceLaneFrame": dictionary(from: laneLayer.frame),
+            "spaceLaneBackgroundColor": colorDictionary(from: laneLayer.backgroundColor),
+            "spaceLaneCornerRadius": Double(laneLayer.cornerRadius),
             "spaceLaneContentWidth": Double(spaceLaneContentWidth),
             "spaceLaneVisibleWidth": Double(laneLayer.bounds.width),
             "spaceLaneMaxScrollOffset": Double(spaceLaneMaxScrollOffset),
             "spaceLaneScrollable": spaceLaneMaxScrollOffset > 0,
+            "spaceLaneDisplayGroupCount": spaceLaneDisplayGroups.count,
+            "spaceLaneDisplayGroups": spaceLaneDisplayGroupReports(),
             "spaceLaneSegmentCount": spaceLaneSegments.count,
             "spaceLaneLabels": currentViewModel?.displays.flatMap { $0.spaces.map(\.label) } ?? [],
             "spaceLaneSegments": spaceLaneSegmentReports(),
+            "hoverTargetKind": hoverTarget.kindDescription,
+            "hoverTargetSource": hoverTargetSource.rawValue,
             "spaceLaneHoveredSpaceID": hoveredSpaceLaneID ?? NSNull(),
             "spaceLaneFocusedSpaceID": focusedSpaceID ?? NSNull(),
             "activeSpaceFocusID": activeSpaceFocusID ?? NSNull(),
             "spaceFocusAppGroupIndexes": spaceFocusedAppGroupIndexes(),
+            "appHoverAssociatedSpaceIDs": appHoverAssociatedSpaceIDs.sorted(),
+            "appHoverAssociatedSpaceLabels": appHoverAssociatedSpaceLabels,
+            "windowHoverAssociatedSpaceID": windowHoverAssociatedSpaceID ?? NSNull(),
             "lastSpaceFocusedAppGroupIndex": lastSpaceFocusedAppGroupIndex ?? NSNull(),
             "lastSpaceLaneClickSpaceID": lastSpaceLaneClickSpaceID ?? NSNull(),
             "lastAppShelfScrollSyncedAppGroupIndex": lastAppShelfScrollSyncedAppGroupIndex ?? NSNull(),
@@ -597,7 +722,8 @@
             "waterfallMaxScrollOffset": Double(waterfallMaxScrollOffset),
             "waterfallAlignmentMinScrollOffset": Double(waterfallAlignmentScrollRange.lowerBound),
             "waterfallAlignmentMaxScrollOffset": Double(waterfallAlignmentScrollRange.upperBound),
-            "waterfallScrollable": waterfallAlignmentScrollable,
+            "waterfallScrollable": waterfallMaxScrollOffset > 0,
+            "waterfallAlignmentScrollable": waterfallAlignmentScrollable,
             "waterfallColumnCount": waterfallColumns.count,
             "waterfallColumnNames": waterfallColumns.map(\.column.app.name),
             "waterfallClipsToBounds": waterfallLayer.masksToBounds,
@@ -627,6 +753,13 @@
             "closeConfirmationVisible": !closeConfirmationLayers.containerLayer.isHidden,
             "closeConfirmationFrame": dictionary(from: closeConfirmationLayers.containerLayer.frame),
             "closeConfirmationArrowEdge": closeConfirmationArrowEdge?.rawValue ?? NSNull(),
+            "closeFeedbackVisible": closeFeedback != nil && !closeFeedbackLayer.isHidden,
+            "closeFeedbackKind": closeFeedback?.kind.rawValue ?? NSNull(),
+            "closeFeedbackMessage": closeFeedback?.message ?? NSNull(),
+            "closeFeedbackTargetKind": closeFeedback?.targetKind ?? NSNull(),
+            "closeFeedbackAppGroupIndex": closeFeedback?.appGroupIndex ?? NSNull(),
+            "closeFeedbackWindowID": closeFeedback?.windowID ?? NSNull(),
+            "closeFeedbackFrame": dictionary(from: closeFeedbackLayer.frame),
             "mouseCommandsApplied": mouseCommandsApplied,
             "lastMouseCommand": lastMouseCommand ?? NSNull(),
             "lastClickedAppGroupIndex": lastClickedAppGroupIndex ?? NSNull(),
@@ -756,7 +889,7 @@
             }
             recordMouseCommand("hover-space:\(spaceID)")
             if let segment = spaceLaneSegment(spaceID: spaceID) {
-                hoverSpaceLaneSegment(segment)
+                hoverSpaceLaneSegment(segment, source: .debug)
             }
         case "click-space" where parts.count == 2:
             guard let spaceID = UInt64(parts[1]) else {
@@ -774,7 +907,7 @@
             }
             recordMouseCommand("hover-card:\(appGroupIndex):\(windowIndex)")
             if let card = waterfallCard(appGroupIndex: appGroupIndex, windowIndex: windowIndex) {
-                hoverWindowCard(card)
+                hoverWindowCard(card, source: .debug)
             }
         case "click-card" where parts.count == 3:
             guard let appGroupIndex = Int(parts[1]), let windowIndex = Int(parts[2]) else {
@@ -826,13 +959,12 @@
     }
 
     private func hoverAppGroup(_ appGroupIndex: Int) {
-        hoveredSpaceLaneID = nil
-        hoveredCloseTarget = nil
         updateHoverState(
             appGroupIndex: appGroupIndex,
             windowID: nil,
             spaceID: nil,
-            alignsWaterfallWithAppShelf: true
+            alignsWaterfallWithAppShelf: true,
+            source: .debug
         )
     }
 
@@ -847,19 +979,18 @@
         beginCloseConfirmation(for: .app(appGroupIndex: appGroupIndex))
     }
 
-    private func hoverWindowCard(_ card: WaterfallCardLayers) {
-        hoveredSpaceLaneID = nil
-        hoveredCloseTarget = nil
+    private func hoverWindowCard(_ card: WaterfallCardLayers, source: HoverTargetSource = .event) {
         updateHoverState(
             appGroupIndex: card.item.appGroupIndex,
             windowID: card.item.window.id,
-            spaceID: card.item.primarySpaceID
+            spaceID: card.item.primarySpaceID,
+            source: source
         )
     }
 
     private func hoverCardCloseButton(appGroupIndex: Int, windowIndex: Int) {
         guard let card = waterfallCard(appGroupIndex: appGroupIndex, windowIndex: windowIndex) else { return }
-        hoverWindowCard(card)
+        hoverWindowCard(card, source: .debug)
         hoveredCloseTarget = .window(
             appGroupIndex: appGroupIndex,
             windowIndex: windowIndex,
@@ -880,8 +1011,8 @@
         )
     }
 
-    private func hoverSpaceLaneSegment(_ segment: SpaceLaneSegmentLayers) {
-        focusSpace(segment.space.id, persistent: false)
+    private func hoverSpaceLaneSegment(_ segment: SpaceLaneSegmentLayers, source: HoverTargetSource = .event) {
+        focusSpace(segment.space.id, persistent: false, source: source)
     }
 
     private func clickSpaceLaneSegment(_ segment: SpaceLaneSegmentLayers) {
@@ -902,11 +1033,8 @@
         commitWindowCard(firstCard, trigger: "spaceLane")
     }
 
-    private func focusSpace(_ spaceID: UInt64, persistent: Bool) {
-        hoveredAppGroupIndex = nil
-        hoveredWindowID = nil
-        hoveredSpaceID = spaceID
-        hoveredSpaceLaneID = spaceID
+    private func focusSpace(_ spaceID: UInt64, persistent: Bool, source: HoverTargetSource = .event) {
+        setHoverTarget(.space(spaceID), source: source)
         if persistent {
             focusedSpaceID = spaceID
             lastSpaceLaneClickSpaceID = spaceID
@@ -1035,24 +1163,67 @@
         appGroupIndex: Int?,
         windowID: UInt32?,
         spaceID: UInt64?,
-        alignsWaterfallWithAppShelf: Bool = false
+        alignsWaterfallWithAppShelf: Bool = false,
+        source: HoverTargetSource = .event
     ) {
-        guard hoveredAppGroupIndex != appGroupIndex
-            || hoveredWindowID != windowID
-            || hoveredSpaceID != spaceID
-        else {
-            if alignsWaterfallWithAppShelf, let appGroupIndex {
-                alignWaterfallColumnWithAppShelfIcon(appGroupIndex: appGroupIndex, animated: true)
-            }
-            return
+        let target: HoverTarget
+        if let appGroupIndex, let windowID {
+            target = .window(appGroupIndex: appGroupIndex, windowID: windowID, spaceID: spaceID)
+        } else if let appGroupIndex {
+            target = .app(appGroupIndex)
+        } else {
+            target = .none
         }
 
-        hoveredAppGroupIndex = appGroupIndex
-        hoveredWindowID = windowID
-        hoveredSpaceID = spaceID
-        if alignsWaterfallWithAppShelf, let appGroupIndex {
+        setHoverTarget(target, source: source, alignsWaterfallWithAppShelf: alignsWaterfallWithAppShelf)
+    }
+
+    private func refreshHoverTarget(at windowLocation: CGPoint) {
+        guard debugHoveredAppGroupIndex == nil else { return }
+        let target = resolveHoverTarget(at: windowLocation)
+        setHoverTarget(target, alignsWaterfallWithAppShelf: target.shouldAlignWaterfallWithAppShelf)
+    }
+
+    private func resolveHoverTarget(at windowLocation: CGPoint) -> HoverTarget {
+        if let segment = spaceLaneSegment(at: windowLocation) {
+            return .space(segment.space.id)
+        }
+
+        if let card = waterfallCard(at: windowLocation) {
+            return .window(
+                appGroupIndex: card.item.appGroupIndex,
+                windowID: card.item.window.id,
+                spaceID: card.item.primarySpaceID
+            )
+        }
+
+        if let item = appShelfItem(at: windowLocation) {
+            return .app(item.item.appGroupIndex)
+        }
+
+        return .none
+    }
+
+    private func setHoverTarget(
+        _ target: HoverTarget,
+        source: HoverTargetSource = .event,
+        alignsWaterfallWithAppShelf: Bool = false
+    ) {
+        let previousTarget = hoverTarget
+        let previousSource = hoverTargetSource
+        hoverTarget = target
+        hoverTargetSource = source
+
+        hoveredAppGroupIndex = target.appGroupIndex
+        hoveredWindowID = target.windowID
+        hoveredSpaceID = target.spaceID
+        hoveredSpaceLaneID = target.spaceLaneID
+
+        if alignsWaterfallWithAppShelf, let appGroupIndex = target.appGroupIndex {
             alignWaterfallColumnWithAppShelfIcon(appGroupIndex: appGroupIndex, animated: true)
         }
+
+        guard previousTarget != target || previousSource != source else { return }
         needsLayout = true
     }
 
@@ -1165,7 +1336,7 @@
             1,
             waterfallLayer.bounds.height
                 - WaterfallMetrics.headerHeight
-                - WaterfallMetrics.columnPadding * 2
+                - WaterfallMetrics.columnVerticalPadding * 2
         )
         let cardStride = WaterfallMetrics.cardHeight + WaterfallMetrics.cardGap
         let cardStart = CGFloat(selection.windowIndex) * cardStride
@@ -1228,7 +1399,7 @@
                 currentOffset: waterfallScrollOffset,
                 visibleWidth: waterfallLayer.bounds.width,
                 maxOffset: waterfallMaxScrollOffset,
-                inset: WaterfallMetrics.revealPadding
+                inset: WaterfallMetrics.horizontalRevealPadding
             )
         )
     }
@@ -1408,8 +1579,8 @@
         glassLayer.shadowOpacity = 0
         rootLayer.addSublayer(glassLayer)
 
-        laneLayer.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.68).cgColor
-        laneLayer.cornerRadius = 16
+        laneLayer.backgroundColor = NSColor.clear.cgColor
+        laneLayer.cornerRadius = 0
         laneLayer.masksToBounds = true
         glassLayer.addSublayer(laneLayer)
         laneLayer.addSublayer(spaceLaneContentLayer)
@@ -1427,6 +1598,24 @@
 
         closeConfirmationLayers.containerLayer.isHidden = true
         glassLayer.addSublayer(closeConfirmationLayers.containerLayer)
+
+        closeFeedbackLayer.isHidden = true
+        closeFeedbackLayer.opacity = 0
+        closeFeedbackLayer.cornerRadius = 11
+        closeFeedbackLayer.borderWidth = 1
+        closeFeedbackLayer.masksToBounds = false
+        closeFeedbackLayer.shadowColor = NSColor.black.cgColor
+        closeFeedbackLayer.shadowOpacity = 0.14
+        closeFeedbackLayer.shadowRadius = 16
+        closeFeedbackLayer.shadowOffset = CGSize(width: 0, height: -5)
+        closeFeedbackLayer.zPosition = 190
+        closeFeedbackTextLayer.contentsScale = backingScaleFactor
+        closeFeedbackTextLayer.font = NSFont.systemFont(ofSize: 13, weight: .medium)
+        closeFeedbackTextLayer.fontSize = 13
+        closeFeedbackTextLayer.alignmentMode = .center
+        closeFeedbackTextLayer.truncationMode = .end
+        closeFeedbackLayer.addSublayer(closeFeedbackTextLayer)
+        glassLayer.addSublayer(closeFeedbackLayer)
     }
 
     private func rebuildAppShelfItems() {
@@ -1434,22 +1623,15 @@
             item.containerLayer.removeFromSuperlayer()
         }
 
-        let selectedAppGroupIndex = effectiveSelection?.appGroupIndex
         appShelfItems = (currentViewModel?.appShelf ?? []).map { item in
             let container = CALayer()
             container.name = item.app.name
 
             let background = CALayer()
             background.cornerRadius = 10
-            background.backgroundColor = (item.appGroupIndex == selectedAppGroupIndex
-                ? NSColor.systemFill.withAlphaComponent(0.16)
-                : NSColor.clear
-            ).cgColor
-            background.borderColor = (item.appGroupIndex == selectedAppGroupIndex
-                ? NSColor.separatorColor.withAlphaComponent(0.36)
-                : NSColor.clear
-            ).cgColor
-            background.borderWidth = item.appGroupIndex == selectedAppGroupIndex ? 1 : 0
+            background.backgroundColor = NSColor.clear.cgColor
+            background.borderColor = NSColor.clear.cgColor
+            background.borderWidth = 0
 
             let icon = CALayer()
             icon.contentsGravity = .resizeAspect
@@ -1523,7 +1705,8 @@
                 string: column.app.name,
                 fontSize: 13,
                 weight: .semibold,
-                color: .labelColor
+                color: .labelColor,
+                alignment: .center
             )
             let count = makeTextLayer(
                 string: "\(column.windows.count)",
@@ -1632,66 +1815,95 @@
     }
 
     private func rebuildSpaceLaneSegments() {
-        for segment in spaceLaneSegments {
-            segment.containerLayer.removeFromSuperlayer()
+        for group in spaceLaneDisplayGroups {
+            group.containerLayer.removeFromSuperlayer()
         }
 
-        let spaces = currentViewModel?.displays.flatMap(\.spaces) ?? []
-        spaceLaneSegments = spaces.map { space in
-            let container = CALayer()
-            container.name = space.label
-            container.cornerRadius = 10
-            container.borderWidth = 1
-            container.borderColor = NSColor.separatorColor.withAlphaComponent(0.34).cgColor
-            container.backgroundColor = spaceColor(for: space).cgColor
-            container.shadowColor = NSColor.black.cgColor
+        var groups: [SpaceLaneDisplayGroupLayers] = []
+        var allSegments: [SpaceLaneSegmentLayers] = []
 
-            let labelLayer = makeTextLayer(
-                string: space.label,
-                fontSize: 15,
-                weight: .semibold,
-                color: .labelColor
-            )
-            let countLayer = makeTextLayer(
-                string: "\(space.windowCount)",
-                fontSize: 12,
-                weight: .medium,
-                color: .secondaryLabelColor,
-                alignment: .center
-            )
-            let appLayer = makeTextLayer(
-                string: appIdentifierText(for: space),
-                fontSize: 12,
-                weight: space.type == .fullscreen ? .medium : .regular,
-                color: space.type == .fullscreen ? .labelColor : .secondaryLabelColor,
-                alignment: space.type == .fullscreen ? .center : .left
-            )
-            let windowBlocks = space.type == .fullscreen
-                ? []
-                : makeWindowBlocks(count: min(space.windowCount, 5), space: space)
-            let fullscreenMarkerLayer = makeFullscreenMarkerLayer(for: space)
+        for display in currentViewModel?.displays ?? [] {
+            let groupContainer = CALayer()
+            groupContainer.name = display.displayLabel
+            groupContainer.cornerRadius = 14
+            groupContainer.masksToBounds = false
+            groupContainer.borderWidth = 0
+            groupContainer.borderColor = NSColor.clear.cgColor
+            groupContainer.backgroundColor = NSColor.windowBackgroundColor.cgColor
+            groupContainer.shadowColor = NSColor.black.cgColor
+            groupContainer.shadowOpacity = 0
+            groupContainer.shadowRadius = 0
+            groupContainer.shadowOffset = .zero
 
-            container.addSublayer(labelLayer)
-            container.addSublayer(countLayer)
-            container.addSublayer(appLayer)
-            if let fullscreenMarkerLayer {
-                container.addSublayer(fullscreenMarkerLayer)
+            let segments = display.spaces.map { space in
+                let container = CAGradientLayer()
+                container.name = space.label
+                container.cornerRadius = 10
+                container.borderWidth = 1
+                container.borderColor = NSColor.separatorColor.withAlphaComponent(0.34).cgColor
+                container.backgroundColor = NSColor.clear.cgColor
+                container.startPoint = CGPoint(x: 0.5, y: 1.0)
+                container.endPoint = CGPoint(x: 0.5, y: 0.0)
+                container.shadowColor = NSColor.black.cgColor
+
+                let labelLayer = makeTextLayer(
+                    string: space.label,
+                    fontSize: 15,
+                    weight: .semibold,
+                    color: .labelColor
+                )
+                let countLayer = makeTextLayer(
+                    string: "\(space.windowCount)",
+                    fontSize: 12,
+                    weight: .medium,
+                    color: .secondaryLabelColor,
+                    alignment: .center
+                )
+                let appLayer = makeTextLayer(
+                    string: appIdentifierText(for: space),
+                    fontSize: 12,
+                    weight: space.type == .fullscreen ? .medium : .regular,
+                    color: space.type == .fullscreen ? .labelColor : .secondaryLabelColor,
+                    alignment: space.type == .fullscreen ? .center : .left
+                )
+                let windowBlocks = space.type == .fullscreen
+                    ? []
+                    : makeWindowBlocks(count: min(space.windowCount, 5), space: space)
+                let fullscreenMarkerLayer = makeFullscreenMarkerLayer(for: space)
+
+                container.addSublayer(labelLayer)
+                container.addSublayer(countLayer)
+                container.addSublayer(appLayer)
+                if let fullscreenMarkerLayer {
+                    container.addSublayer(fullscreenMarkerLayer)
+                }
+                for block in windowBlocks {
+                    container.addSublayer(block)
+                }
+                groupContainer.addSublayer(container)
+
+                return SpaceLaneSegmentLayers(
+                    space: space,
+                    containerLayer: container,
+                    labelLayer: labelLayer,
+                    countLayer: countLayer,
+                    appLayer: appLayer,
+                    windowBlocks: windowBlocks,
+                    fullscreenMarkerLayer: fullscreenMarkerLayer
+                )
             }
-            for block in windowBlocks {
-                container.addSublayer(block)
-            }
-            spaceLaneContentLayer.addSublayer(container)
 
-            return SpaceLaneSegmentLayers(
-                space: space,
-                containerLayer: container,
-                labelLayer: labelLayer,
-                countLayer: countLayer,
-                appLayer: appLayer,
-                windowBlocks: windowBlocks,
-                fullscreenMarkerLayer: fullscreenMarkerLayer
-            )
+            spaceLaneContentLayer.addSublayer(groupContainer)
+            groups.append(SpaceLaneDisplayGroupLayers(
+                display: display,
+                containerLayer: groupContainer,
+                segments: segments
+            ))
+            allSegments.append(contentsOf: segments)
         }
+
+        spaceLaneDisplayGroups = groups
+        spaceLaneSegments = allSegments
     }
 
     private func layoutLayers() {
@@ -1740,6 +1952,7 @@
         layoutAppShelfItems()
         layoutWaterfallColumns()
         layoutCloseConfirmation()
+        layoutCloseFeedback()
 
         CATransaction.commit()
     }
@@ -1823,15 +2036,34 @@
     }
 
     private func layoutSpaceLaneSegments() {
-        guard !spaceLaneSegments.isEmpty else { return }
+        guard !spaceLaneDisplayGroups.isEmpty, !spaceLaneSegments.isEmpty else { return }
 
-        let gap: CGFloat = 10
-        let segmentHeight = max(1, laneLayer.bounds.height - 20)
-        let availableWidth = laneLayer.bounds.width - 24 - CGFloat(spaceLaneSegments.count - 1) * gap
-        let segmentWidth = max(96, min(150, availableWidth / CGFloat(spaceLaneSegments.count)))
+        let groupOuterVerticalInset: CGFloat = 6
+        let groupHeight = max(1, laneLayer.bounds.height - groupOuterVerticalInset * 2)
+        let segmentHeight = max(1, groupHeight - SpaceLaneMetrics.groupVerticalPadding * 2)
+        let groupGapWidth = CGFloat(max(0, spaceLaneDisplayGroups.count - 1)) * SpaceLaneMetrics.groupGap
+        let segmentGapWidth = spaceLaneDisplayGroups.reduce(CGFloat(0)) { total, group in
+            total + CGFloat(max(0, group.segments.count - 1)) * SpaceLaneMetrics.segmentGap
+        }
+        let groupPaddingWidth = CGFloat(spaceLaneDisplayGroups.count) * SpaceLaneMetrics.groupHorizontalPadding * 2
+        let availableWidth = laneLayer.bounds.width
+            - SpaceLaneMetrics.horizontalPadding * 2
+            - groupGapWidth
+            - segmentGapWidth
+            - groupPaddingWidth
+        let segmentWidth = max(
+            SpaceLaneMetrics.segmentMinWidth,
+            min(SpaceLaneMetrics.segmentMaxWidth, availableWidth / CGFloat(spaceLaneSegments.count))
+        )
+        let groupWidths = spaceLaneDisplayGroups.map { group in
+            SpaceLaneMetrics.groupHorizontalPadding * 2
+                + CGFloat(group.segments.count) * segmentWidth
+                + CGFloat(max(0, group.segments.count - 1)) * SpaceLaneMetrics.segmentGap
+        }
+        let segmentGroupWidth = groupWidths.reduce(0, +) + groupGapWidth
         spaceLaneContentWidth = max(
             laneLayer.bounds.width,
-            24 + CGFloat(spaceLaneSegments.count) * segmentWidth + CGFloat(spaceLaneSegments.count - 1) * gap
+            SpaceLaneMetrics.horizontalPadding * 2 + segmentGroupWidth
         )
         spaceLaneScrollOffset = clampedSpaceLaneOffset(spaceLaneScrollOffset)
         spaceLaneContentLayer.frame = CGRect(
@@ -1841,13 +2073,35 @@
             height: laneLayer.bounds.height
         )
 
-        var x: CGFloat = 12
+        var x = max(
+            SpaceLaneMetrics.horizontalPadding,
+            (spaceLaneContentWidth - segmentGroupWidth) / 2
+        )
 
-        for segment in spaceLaneSegments {
-            segment.containerLayer.frame = CGRect(x: x, y: 10, width: segmentWidth, height: segmentHeight)
-            applySpaceLaneVisualState(segment)
-            layoutSpaceLaneSegmentContents(segment)
-            x += segmentWidth + gap
+        for (index, group) in spaceLaneDisplayGroups.enumerated() {
+            let groupWidth = groupWidths[index]
+            group.containerLayer.frame = CGRect(
+                x: x,
+                y: groupOuterVerticalInset,
+                width: groupWidth,
+                height: groupHeight
+            )
+            applySpaceLaneDisplayGroupVisualState(group)
+
+            var segmentX = SpaceLaneMetrics.groupHorizontalPadding
+            for segment in group.segments {
+                segment.containerLayer.frame = CGRect(
+                    x: segmentX,
+                    y: SpaceLaneMetrics.groupVerticalPadding,
+                    width: segmentWidth,
+                    height: segmentHeight
+                )
+                applySpaceLaneVisualState(segment)
+                layoutSpaceLaneSegmentContents(segment)
+                segmentX += segmentWidth + SpaceLaneMetrics.segmentGap
+            }
+
+            x += groupWidth + SpaceLaneMetrics.groupGap
         }
     }
 
@@ -1859,36 +2113,84 @@
         min(max(0, offset), spaceLaneMaxScrollOffset)
     }
 
+    private func applySpaceLaneDisplayGroupVisualState(_ group: SpaceLaneDisplayGroupLayers) {
+        let hasActiveSegment = group.segments.contains { segment in
+            segment.space.id == activeSpaceFocusID
+                || appHoverAssociatedSpaceIDs.contains(segment.space.id)
+                || segment.space.id == windowHoverAssociatedSpaceID
+        }
+        let hasCurrentSegment = group.display.spaces.contains { $0.isCurrent }
+        group.containerLayer.backgroundColor = NSColor.windowBackgroundColor.cgColor
+        group.containerLayer.borderColor = NSColor.clear.cgColor
+        group.containerLayer.borderWidth = 0
+        group.containerLayer.shadowOpacity = 0
+        group.containerLayer.shadowRadius = 0
+        group.containerLayer.shadowOffset = .zero
+        group.containerLayer.zPosition = hasActiveSegment ? 8 : hasCurrentSegment ? 4 : 0
+        group.containerLayer.shadowPath = nil
+    }
+
     private func applySpaceLaneVisualState(_ segment: SpaceLaneSegmentLayers) {
-        let isHovered = segment.space.id == hoveredSpaceID
+        let isHovered = segment.space.id == activeSpaceFocusID
         let isFocused = segment.space.id == activeSpaceFocusID
         let isCurrent = segment.space.isCurrent
+        let hasWindows = segment.space.windowCount > 0
+        let isAppAssociated = appHoverAssociatedSpaceIDs.contains(segment.space.id)
+        let isWindowAssociated = segment.space.id == windowHoverAssociatedSpaceID
         let isActive = isHovered || isFocused
+        let isAssociated = (isAppAssociated || isWindowAssociated) && !isActive
 
-        segment.containerLayer.borderWidth = (isCurrent || isActive) ? 1.5 : 1
+        segment.containerLayer.borderWidth = (hasWindows || isCurrent || isActive || isAssociated) ? 1.5 : 1
         segment.containerLayer.borderColor = (isActive
-            ? NSColor.controlAccentColor.withAlphaComponent(0.54)
-            : isCurrent
-                ? NSColor.labelColor.withAlphaComponent(0.34)
+            ? spaceLaneOccupiedColor.withAlphaComponent(0.62)
+            : isAssociated
+                ? spaceLaneOccupiedColor.withAlphaComponent(0.50)
+                : hasWindows
+                    ? spaceLaneOccupiedColor.withAlphaComponent(isCurrent ? 0.54 : 0.44)
+                : isCurrent
+                    ? NSColor.labelColor.withAlphaComponent(0.34)
                 : NSColor.separatorColor.withAlphaComponent(0.34)
         ).cgColor
-        segment.containerLayer.backgroundColor = (isActive
-            ? accentTintedWindowBackground(fraction: 0.10, alpha: 0.94)
-            : spaceColor(for: segment.space)
-        ).cgColor
-        segment.containerLayer.shadowOpacity = isActive ? 0.26 : isCurrent ? 0.14 : 0
-        segment.containerLayer.shadowRadius = isActive ? 24 : isCurrent ? 12 : 0
-        segment.containerLayer.shadowOffset = CGSize(width: 0, height: isActive ? -6 : isCurrent ? -2.5 : 0)
-        segment.containerLayer.zPosition = isActive ? 30 : isCurrent ? 10 : 0
-        segment.containerLayer.transform = isActive
-            ? CATransform3DMakeScale(1.03, 1.03, 1)
-            : CATransform3DIdentity
-        segment.containerLayer.shadowPath = CGPath(
-            roundedRect: segment.containerLayer.bounds,
-            cornerWidth: segment.containerLayer.cornerRadius,
-            cornerHeight: segment.containerLayer.cornerRadius,
-            transform: nil
+        if !hasWindows {
+            applySpaceLaneSolidFill(to: segment.containerLayer, color: .clear)
+        } else if isActive {
+            applySpaceLaneSolidFill(
+                to: segment.containerLayer,
+                color: accentTintedWindowBackground(fraction: 0.10, alpha: 0.94)
+            )
+        } else if isAssociated {
+            applySpaceLaneSolidFill(
+                to: segment.containerLayer,
+                color: accentTintedWindowBackground(fraction: 0.07, alpha: 0.90)
+            )
+        } else if hasWindows {
+            applySpaceLaneGradientFill(
+                to: segment.containerLayer,
+                colors: spaceLaneIdleOccupiedGradientColors()
+            )
+        }
+        segment.containerLayer.shadowOpacity = hasWindows
+            ? (isActive ? 0.26 : isAssociated ? 0.18 : isCurrent ? 0.14 : 0)
+            : 0
+        segment.containerLayer.shadowRadius = hasWindows
+            ? (isActive ? 24 : isAssociated ? 18 : isCurrent ? 12 : 0)
+            : 0
+        segment.containerLayer.shadowOffset = CGSize(
+            width: 0,
+            height: hasWindows ? (isActive ? -6 : isAssociated ? -4 : isCurrent ? -2.5 : 0) : 0
         )
+        segment.containerLayer.zPosition = isActive ? 30 : isAssociated ? 18 : isCurrent ? 10 : 0
+        segment.containerLayer.transform = (isActive || isAssociated)
+            ? CATransform3DMakeScale(isActive ? 1.03 : 1.015, isActive ? 1.03 : 1.015, 1)
+            : CATransform3DIdentity
+        segment.containerLayer.shadowPath = hasWindows
+            ? CGPath(
+                roundedRect: segment.containerLayer.bounds,
+                cornerWidth: segment.containerLayer.cornerRadius,
+                cornerHeight: segment.containerLayer.cornerRadius,
+                transform: nil
+            )
+            : nil
     }
 
     private func shouldScrollSpaceLane(for event: NSEvent) -> Bool {
@@ -1929,14 +2231,32 @@
 
     private func spaceLaneSegment(at windowLocation: CGPoint) -> SpaceLaneSegmentLayers? {
         let locationInView = convert(windowLocation, from: nil)
+
+        // 严格 band 检查:只有鼠标在 lane 的垂直 band 内才考虑是 lane hover。
+        // 防止“完全空的地方”(y 不在 lane 高度范围内)误 hit 到 segment frame,
+        // 导致 hoveredSpaceLaneID 粘住、activeSpaceFocusID 非 nil、窗口 spaceFocused 高亮。
+        let laneMinYInView = glassLayer.frame.minY + laneLayer.frame.minY
+        let laneMaxYInView = laneMinYInView + laneLayer.frame.height
+        if locationInView.y < laneMinYInView || locationInView.y > laneMaxYInView {
+            return nil
+        }
+
         let locationInSpaceLaneContent = CGPoint(
             x: locationInView.x - glassLayer.frame.minX - laneLayer.frame.minX - spaceLaneContentLayer.frame.minX,
             y: locationInView.y - glassLayer.frame.minY - laneLayer.frame.minY - spaceLaneContentLayer.frame.minY
         )
 
-        return spaceLaneSegments.first { segment in
-            segment.containerLayer.frame.contains(locationInSpaceLaneContent)
+        for group in spaceLaneDisplayGroups where group.containerLayer.frame.contains(locationInSpaceLaneContent) {
+            let locationInGroup = CGPoint(
+                x: locationInSpaceLaneContent.x - group.containerLayer.frame.minX,
+                y: locationInSpaceLaneContent.y - group.containerLayer.frame.minY
+            )
+            if let segment = group.segments.first(where: { $0.containerLayer.frame.contains(locationInGroup) }) {
+                return segment
+            }
         }
+
+        return nil
     }
 
     private func spaceLaneSegment(spaceID: UInt64) -> SpaceLaneSegmentLayers? {
@@ -2059,8 +2379,8 @@
     private func layoutAppShelfItemContents(_ item: AppShelfItemLayers, iconSize: CGFloat) {
         let bounds = item.containerLayer.bounds
         let isSelected = item.item.appGroupIndex == effectiveSelection?.appGroupIndex
-        let showsSelectedVisual = shouldShowSelectedVisual(isSelected)
         let isHovered = item.item.appGroupIndex == effectiveHoveredAppGroupIndex
+        let showsSelectedVisual = shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered)
         let isSpaceFocused = spaceFocusWindowCount(for: item.item.appGroupIndex) > 0
         let visualIconSize = appShelfVisualIconSize(
             forBaseIconSize: iconSize,
@@ -2164,15 +2484,52 @@
     }
 
     private var activeSpaceFocusID: UInt64? {
-        hoveredSpaceLaneID
+        guard case .space(let spaceID) = hoverTarget else { return nil }
+
+        if hoverTargetSource == .debug {
+            return spaceID
+        }
+
+        guard let window else { return nil }
+        let currentMouseLocation = window.mouseLocationOutsideOfEventStream
+        guard let segment = spaceLaneSegment(at: currentMouseLocation),
+              segment.space.id == spaceID
+        else {
+            return nil
+        }
+        return spaceID
+    }
+
+    private var appHoverAssociatedSpaceIDs: Set<UInt64> {
+        guard case .app(let appGroupIndex) = hoverTarget else { return [] }
+        return Set(
+            waterfallColumns
+                .first { $0.column.appGroupIndex == appGroupIndex }?
+                .cards
+                .compactMap(\.item.primarySpaceID) ?? []
+        )
+    }
+
+    private var appHoverAssociatedSpaceLabels: [String] {
+        guard !appHoverAssociatedSpaceIDs.isEmpty else { return [] }
+        return currentViewModel?.displays
+            .flatMap(\.spaces)
+            .filter { appHoverAssociatedSpaceIDs.contains($0.id) }
+            .map(\.label)
+            .sorted() ?? []
+    }
+
+    private var windowHoverAssociatedSpaceID: UInt64? {
+        guard case .window(_, _, let spaceID) = hoverTarget else { return nil }
+        return spaceID
     }
 
     private var isSpaceAssociationActive: Bool {
         activeSpaceFocusID != nil
     }
 
-    private func shouldShowSelectedVisual(_ isSelected: Bool) -> Bool {
-        isSelected && !isSpaceAssociationActive
+    private func shouldShowSelectedVisual(isSelected: Bool, isHovered: Bool) -> Bool {
+        isSelected && isHovered && !isSpaceAssociationActive
     }
 
     private func spaceFocusedAppGroupIndexes() -> [Int] {
@@ -2309,7 +2666,7 @@
         }
 
         let columnWidth = waterfallColumnWidth()
-        let totalWidth = WaterfallMetrics.columnPadding * 2
+        let totalWidth = WaterfallMetrics.contentHorizontalPadding * 2
             + CGFloat(waterfallColumns.count) * columnWidth
             + CGFloat(max(0, waterfallColumns.count - 1)) * WaterfallMetrics.gap
         waterfallContentWidth = max(waterfallLayer.bounds.width, totalWidth)
@@ -2321,7 +2678,7 @@
             height: waterfallLayer.bounds.height
         )
 
-        var x = WaterfallMetrics.columnPadding
+        var x = WaterfallMetrics.contentHorizontalPadding
         for column in waterfallColumns {
             column.containerLayer.frame = CGRect(
                 x: x,
@@ -2337,6 +2694,7 @@
     private func layoutWaterfallColumn(_ column: WaterfallColumnLayers) {
         let bounds = column.containerLayer.bounds
         let isSpaceFocused = spaceFocusWindowCount(for: column.column.appGroupIndex) > 0
+        let isHovered = column.column.appGroupIndex == effectiveHoveredAppGroupIndex
         column.containerLayer.borderColor = (isSpaceFocused
             ? NSColor.separatorColor.withAlphaComponent(0.38)
             : NSColor.separatorColor.withAlphaComponent(0.24)
@@ -2353,11 +2711,15 @@
             : NSColor.controlBackgroundColor.withAlphaComponent(0.52)
         ).cgColor
         column.appNameLayer.frame = CGRect(
-            x: WaterfallMetrics.columnPadding,
+            x: WaterfallMetrics.columnHeaderTextInset,
             y: bounds.height - 24,
-            width: max(1, bounds.width - 64),
+            width: max(1, bounds.width - WaterfallMetrics.columnHeaderTextInset * 2),
             height: 16
         )
+        column.appNameLayer.foregroundColor = (isHovered
+            ? spaceLaneOccupiedColor
+            : NSColor.labelColor
+        ).cgColor
         column.countLayer.frame = CGRect(
             x: bounds.width - 48,
             y: bounds.height - 24,
@@ -2372,10 +2734,10 @@
         )
         waterfallColumnScrollOffsets[appGroupIndex] = verticalOffset
 
-        let cardWidth = max(1, bounds.width - WaterfallMetrics.columnPadding * 2)
+        let cardWidth = max(1, bounds.width - WaterfallMetrics.columnHorizontalPadding * 2)
         let firstCardTop = bounds.height
             - WaterfallMetrics.headerHeight
-            - WaterfallMetrics.columnPadding
+            - WaterfallMetrics.columnVerticalPadding
             + verticalOffset
 
         for card in column.cards {
@@ -2384,7 +2746,7 @@
                 - WaterfallMetrics.cardHeight
                 - CGFloat(cardIndex) * (WaterfallMetrics.cardHeight + WaterfallMetrics.cardGap)
             card.containerLayer.frame = CGRect(
-                x: WaterfallMetrics.columnPadding,
+                x: WaterfallMetrics.columnHorizontalPadding,
                 y: y,
                 width: cardWidth,
                 height: WaterfallMetrics.cardHeight
@@ -2397,9 +2759,13 @@
         let bounds = card.containerLayer.bounds
         let isSelected = card.item.window.id == effectiveSelection?.windowID
         let isHovered = card.item.window.id == hoveredWindowID
-        applyWaterfallCardVisualState(card, selected: shouldShowSelectedVisual(isSelected), hovered: isHovered)
+        applyWaterfallCardVisualState(
+            card,
+            selected: shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered),
+            hovered: isHovered
+        )
 
-        let outerInset: CGFloat = 8
+        let outerInset: CGFloat = 0
         let titleBarHeight: CGFloat = 26
         let titleGap: CGFloat = 6
         let titleBarFrame = CGRect(
@@ -2460,7 +2826,7 @@
     }
 
     private func applyWaterfallCardVisualState(_ card: WaterfallCardLayers, selected: Bool, hovered: Bool) {
-        let isSpaceFocused = card.item.primarySpaceID == activeSpaceFocusID
+        let isSpaceFocused = isWaterfallCardSpaceFocused(card)
         card.containerLayer.backgroundColor = (selected
             ? NSColor.windowBackgroundColor.withAlphaComponent(0.94)
             : hovered
@@ -2517,13 +2883,14 @@
         let size: CGFloat = isButtonHovered ? 24 : 18
         let buttonFill = isButtonHovered
             ? NSColor.systemRed.withAlphaComponent(0.94)
-            : NSColor.labelColor.withAlphaComponent(0.24)
+            : NSColor.white.withAlphaComponent(0.34)
         let buttonStroke = isButtonHovered
             ? NSColor.white.withAlphaComponent(0.76)
-            : NSColor.white.withAlphaComponent(0.52)
+            : NSColor.white.withAlphaComponent(0.46)
         let glyphStroke = isButtonHovered
             ? NSColor.white
-            : NSColor.secondaryLabelColor.withAlphaComponent(0.92)
+            : NSColor.white.withAlphaComponent(0.88)
+
         button.containerLayer.frame = CGRect(
             x: anchorTopRight.x - size / 2,
             y: anchorTopRight.y - size / 2,
@@ -2535,10 +2902,10 @@
         button.containerLayer.borderColor = buttonStroke.cgColor
         button.containerLayer.opacity = visible ? 1 : 0
         button.containerLayer.zPosition = isButtonHovered ? 80 : 60
-        button.containerLayer.shadowOpacity = visible ? (isButtonHovered ? 0.22 : 0.07) : 0
+        button.containerLayer.shadowOpacity = visible ? (isButtonHovered ? 0.22 : 0.04) : 0
         button.containerLayer.shadowRadius = isButtonHovered ? 9 : 5
         button.glyphLayer.frame = CGRect(x: 0, y: 0, width: size, height: size)
-        button.glyphLayer.lineWidth = isButtonHovered ? 2.1 : 1.7
+        button.glyphLayer.lineWidth = isButtonHovered ? 2.1 : 1.45
         button.glyphLayer.strokeColor = glyphStroke.cgColor
         button.glyphLayer.path = closeGlyphPath(size: size, hovered: isButtonHovered)
     }
@@ -2631,6 +2998,80 @@
         layers.confirmButtonTextLayer.frame = layers.confirmButtonLayer.bounds.insetBy(dx: 0, dy: 7)
     }
 
+    private func setCloseFeedback(_ feedback: CloseFeedback, autoHideAfter delay: TimeInterval?) {
+        closeFeedbackHideTask?.cancel()
+        closeFeedback = feedback
+        closeFeedbackLayer.isHidden = false
+        closeFeedbackLayer.opacity = 1
+        closeFeedbackTextLayer.string = feedback.message
+        switch feedback.kind {
+        case .progress:
+            closeFeedbackLayer.backgroundColor = NSColor.windowBackgroundColor.withAlphaComponent(0.94).cgColor
+            closeFeedbackLayer.borderColor = NSColor.separatorColor.withAlphaComponent(0.28).cgColor
+            closeFeedbackTextLayer.foregroundColor = NSColor.secondaryLabelColor.withAlphaComponent(0.96).cgColor
+        case .failure:
+            closeFeedbackLayer.backgroundColor = NSColor.systemRed.withAlphaComponent(0.10).cgColor
+            closeFeedbackLayer.borderColor = NSColor.systemRed.withAlphaComponent(0.32).cgColor
+            closeFeedbackTextLayer.foregroundColor = NSColor.systemRed.withAlphaComponent(0.95).cgColor
+        }
+        needsLayout = true
+
+        guard let delay else {
+            closeFeedbackHideTask = nil
+            return
+        }
+
+        closeFeedbackHideTask = Task { @MainActor [weak self] in
+            try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
+            guard !Task.isCancelled,
+                  let self,
+                  self.closeFeedback == feedback
+            else {
+                return
+            }
+            self.clearCloseFeedback()
+        }
+    }
+
+    private func layoutCloseFeedback() {
+        guard let feedback = closeFeedback else {
+            closeFeedbackLayer.isHidden = true
+            closeFeedbackLayer.opacity = 0
+            return
+        }
+
+        closeFeedbackLayer.isHidden = false
+        closeFeedbackLayer.opacity = 1
+        let font = NSFont.systemFont(
+            ofSize: 13,
+            weight: feedback.kind == .failure ? .medium : .regular
+        )
+        closeFeedbackTextLayer.font = font
+        closeFeedbackTextLayer.fontSize = 13
+        let textWidth = (feedback.message as NSString).size(
+            withAttributes: [.font: font]
+        ).width
+        let maxWidth = max(180, glassLayer.bounds.width - 48)
+        let width = min(max(180, textWidth + 34), min(460, maxWidth))
+        let height: CGFloat = 34
+        let x = max(24, (glassLayer.bounds.width - width) / 2)
+        let preferredY = waterfallLayer.frame.maxY - height - 14
+        let y = min(
+            max(16, preferredY),
+            max(16, glassLayer.bounds.height - height - 16)
+        )
+
+        closeFeedbackLayer.frame = CGRect(x: x, y: y, width: width, height: height)
+        closeFeedbackLayer.cornerRadius = height / 2
+        closeFeedbackLayer.shadowPath = CGPath(
+            roundedRect: closeFeedbackLayer.bounds,
+            cornerWidth: closeFeedbackLayer.cornerRadius,
+            cornerHeight: closeFeedbackLayer.cornerRadius,
+            transform: nil
+        )
+        closeFeedbackTextLayer.frame = closeFeedbackLayer.bounds.insetBy(dx: 17, dy: 8)
+    }
+
     private func closeConfirmationBubblePath(
         bounds: CGRect,
         contentFrame: CGRect,
@@ -2716,18 +3157,23 @@
         }
 
         pendingCloseTarget = nil
+        pendingCloseRequest = nil
         pendingCloseOptOut = false
         closeConfirmationLayers.containerLayer.isHidden = true
         closeConfirmationArrowEdge = nil
-        if pendingCloseOptOut {
-            onCloseConfirmationDisabled?()
-        }
         onRequestClose?(request)
     }
 
     private func beginCloseConfirmation(for target: CloseTarget) {
         if closeConfirmationRequired {
+            guard let request = closeRequest(for: target) else {
+                DevelopmentDiagnostics.log("quickSwitch.view.closeConfirmation.missingRequest", [
+                    "targetKind": target.kindDescription
+                ])
+                return
+            }
             pendingCloseTarget = target
+            pendingCloseRequest = request
             pendingCloseOptOut = false
             DevelopmentDiagnostics.log("quickSwitch.view.closeConfirmation.show", [
                 "targetKind": target.kindDescription
@@ -2740,23 +3186,30 @@
     }
 
     private func confirmPendingClose() {
-        guard let target = pendingCloseTarget else { return }
+        guard let target = pendingCloseTarget,
+              let request = pendingCloseRequest
+        else { return }
         let shouldDisableConfirmation = pendingCloseOptOut
         pendingCloseTarget = nil
+        pendingCloseRequest = nil
         pendingCloseOptOut = false
         closeConfirmationLayers.containerLayer.isHidden = true
         closeConfirmationArrowEdge = nil
-        if shouldDisableConfirmation {
+        if shouldDisableConfirmation && closeConfirmationRequired {
+            closeConfirmationRequired = false
             onCloseConfirmationDisabled?()
         }
-        if let request = closeRequest(for: target) {
-            onRequestClose?(request)
-        }
+        DevelopmentDiagnostics.log("quickSwitch.view.closeConfirmation.confirm", [
+            "targetKind": target.kindDescription,
+            "disableFutureConfirmation": shouldDisableConfirmation
+        ])
+        onRequestClose?(request)
     }
 
     private func cancelPendingClose() {
         DevelopmentDiagnostics.log("quickSwitch.view.closeConfirmation.cancel")
         pendingCloseTarget = nil
+        pendingCloseRequest = nil
         pendingCloseOptOut = false
         closeConfirmationLayers.containerLayer.isHidden = true
         closeConfirmationArrowEdge = nil
@@ -2764,7 +3217,15 @@
     }
 
     private func toggleCloseConfirmationOptOut() {
+        if pendingCloseOptOut && !closeConfirmationRequired {
+            return
+        }
         pendingCloseOptOut.toggle()
+        if pendingCloseOptOut {
+            closeConfirmationRequired = false
+            onCloseConfirmationDisabled?()
+            DevelopmentDiagnostics.log("quickSwitch.view.closeConfirmation.optOutPersisted")
+        }
         needsLayout = true
     }
 
@@ -2931,13 +3392,18 @@
         return color.withAlphaComponent(alpha)
     }
 
+    private func isWaterfallCardSpaceFocused(_ card: WaterfallCardLayers) -> Bool {
+        guard let activeSpaceFocusID else { return false }
+        return card.item.primarySpaceID == activeSpaceFocusID
+    }
+
     private func waterfallColumnWidth() -> CGFloat {
         guard waterfallColumns.count > 0 else { return WaterfallMetrics.columnWidth }
 
         let visibleColumns = min(4, max(1, waterfallColumns.count))
         let fittingWidth = (
             waterfallLayer.bounds.width
-                - WaterfallMetrics.columnPadding * 2
+                - WaterfallMetrics.contentHorizontalPadding * 2
                 - CGFloat(max(0, visibleColumns - 1)) * WaterfallMetrics.gap
         ) / CGFloat(visibleColumns)
         return max(
@@ -2965,7 +3431,7 @@
             1,
             waterfallLayer.bounds.height
                 - WaterfallMetrics.headerHeight
-                - WaterfallMetrics.columnPadding * 2
+                - WaterfallMetrics.columnVerticalPadding * 2
         )
         let contentHeight = CGFloat(column.cards.count) * WaterfallMetrics.cardHeight
             + CGFloat(max(0, column.cards.count - 1)) * WaterfallMetrics.cardGap
@@ -2995,15 +3461,43 @@
         return states.joined(separator: " / ")
     }
 
-    private func spaceColor(for space: QuickSwitchSpaceViewModel) -> NSColor {
-        if space.isCurrent {
-            return NSColor.windowBackgroundColor.withAlphaComponent(0.76)
-        }
-        if space.windowCount > 0 {
-            return NSColor.controlBackgroundColor.withAlphaComponent(0.56)
+    private var spaceLaneOccupiedColor: NSColor {
+        if effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua {
+            return NSColor(calibratedRed: 10 / 255, green: 132 / 255, blue: 1, alpha: 1)
         }
 
-        return NSColor.controlBackgroundColor.withAlphaComponent(0.26)
+        return NSColor(calibratedRed: 0, green: 122 / 255, blue: 1, alpha: 1)
+    }
+
+    private func spaceLaneIdleOccupiedGradientColors() -> [NSColor] {
+        if effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua {
+            let blue = NSColor(calibratedRed: 36 / 255, green: 54 / 255, blue: 72 / 255, alpha: 1)
+            let surface = NSColor.windowBackgroundColor.withAlphaComponent(1)
+            return [blue, surface]
+        }
+
+        return [
+            NSColor(calibratedRed: 232 / 255, green: 246 / 255, blue: 1, alpha: 1),
+            NSColor.white
+        ]
+    }
+
+    private func applySpaceLaneGradientFill(to layer: CALayer, colors: [NSColor]) {
+        if let gradientLayer = layer as? CAGradientLayer {
+            gradientLayer.colors = colors.map(\.cgColor)
+            gradientLayer.locations = [0, 1]
+            gradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0)
+            gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0)
+        }
+        layer.backgroundColor = NSColor.clear.cgColor
+    }
+
+    private func applySpaceLaneSolidFill(to layer: CALayer, color: NSColor) {
+        if let gradientLayer = layer as? CAGradientLayer {
+            gradientLayer.colors = nil
+            gradientLayer.locations = nil
+        }
+        layer.backgroundColor = color.cgColor
     }
 
     private func makeFullscreenMarkerLayer(for space: QuickSwitchSpaceViewModel) -> CALayer? {
@@ -3169,10 +3663,9 @@
         return (0..<count).map { _ in
             let layer = CALayer()
             layer.cornerRadius = 3
-            layer.backgroundColor = (space.type == .fullscreen
-                ? NSColor.labelColor.withAlphaComponent(0.20)
-                : NSColor.labelColor.withAlphaComponent(0.18)
-            ).cgColor
+            layer.backgroundColor = spaceLaneOccupiedColor
+                .withAlphaComponent(space.isCurrent ? 0.72 : 0.58)
+                .cgColor
             return layer
         }
     }
@@ -3226,11 +3719,18 @@
     }
 
     private func fullscreenMarkerFrame(in segmentBounds: CGRect) -> CGRect {
-        CGRect(
+        let topReserved: CGFloat = 32
+        let bottomReserved: CGFloat = 16
+        let availableHeight = max(20, segmentBounds.height - topReserved - bottomReserved)
+        let markerHeight = min(30, max(20, availableHeight))
+        let markerCenterY = bottomReserved + availableHeight * 0.5
+        let markerY = max(8, min(segmentBounds.height - topReserved - markerHeight, markerCenterY - markerHeight * 0.5))
+
+        return CGRect(
             x: 10,
-            y: 30,
+            y: markerY,
             width: max(1, segmentBounds.width - 20),
-            height: max(1, segmentBounds.height - 58)
+            height: markerHeight
         )
     }
 
@@ -3272,7 +3772,9 @@
                 "closeButtonVisible": item.closeButton.containerLayer.opacity > 0,
                 "closeButtonFrame": dictionary(from: item.closeButton.containerLayer.frame),
                 "closeButtonSize": Double(item.closeButton.containerLayer.bounds.width),
-                "closeButtonFillColor": colorDictionary(from: item.closeButton.containerLayer.backgroundColor)
+                "closeButtonFillColor": colorDictionary(from: item.closeButton.containerLayer.backgroundColor),
+                "closeButtonGlyphColor": colorDictionary(from: item.closeButton.glyphLayer.strokeColor),
+                "closeButtonGlyphLineWidth": Double(item.closeButton.glyphLayer.lineWidth)
             ] as [String: Any]
         }
     }
@@ -3280,12 +3782,13 @@
     private func appShelfVisualStates(for item: AppShelfItemLayers) -> [String] {
         var states: [String] = []
         let isSelected = item.item.appGroupIndex == effectiveSelection?.appGroupIndex
-        if shouldShowSelectedVisual(isSelected) {
+        let isHovered = item.item.appGroupIndex == effectiveHoveredAppGroupIndex
+        if shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) {
             states.append("selected")
         } else if isSelected {
             states.append("selectedVisualSuppressed")
         }
-        if item.item.appGroupIndex == effectiveHoveredAppGroupIndex {
+        if isHovered {
             states.append("hover")
         }
         if spaceFocusWindowCount(for: item.item.appGroupIndex) > 0 {
@@ -3298,21 +3801,49 @@
         return states
     }
 
+    private func waterfallColumnVisualStates(for column: WaterfallColumnLayers) -> [String] {
+        var states: [String] = []
+        if column.column.appGroupIndex == effectiveHoveredAppGroupIndex {
+            states.append("hover")
+        }
+        if spaceFocusWindowCount(for: column.column.appGroupIndex) > 0 {
+            states.append("spaceFocused")
+        }
+        return states
+    }
+
     private func waterfallColumnReports() -> [[String: Any]] {
         waterfallColumns.map { column in
             let visibleFrame = column.containerLayer.frame.offsetBy(dx: waterfallContentLayer.frame.minX, dy: 0)
             let appGroupIndex = column.column.appGroupIndex
+            let isHovered = appGroupIndex == effectiveHoveredAppGroupIndex
+            let firstCardFrame = column.cards.first?.containerLayer.frame
+            let headerToFirstCardGap = firstCardFrame.map {
+                column.headerLayer.frame.minY - $0.maxY
+            }
+            let topToFirstCardGap = firstCardFrame.map {
+                column.containerLayer.bounds.height - $0.maxY
+            }
 
             return [
                 "appGroupIndex": appGroupIndex,
                 "appName": column.column.app.name,
                 "bundleIdentifier": column.column.app.bundleIdentifier,
                 "windowCount": column.column.windows.count,
+                "isHovered": isHovered,
+                "visualStates": waterfallColumnVisualStates(for: column),
                 "spaceFocusWindowCount": spaceFocusWindowCount(for: appGroupIndex),
                 "isSpaceFocused": spaceFocusWindowCount(for: appGroupIndex) > 0,
                 "spaceFocusDirection": spaceFocusDirection(for: appGroupIndex),
                 "frame": dictionary(from: column.containerLayer.frame),
                 "visibleFrame": dictionary(from: visibleFrame),
+                "headerFrame": dictionary(from: column.headerLayer.frame),
+                "headerToFirstCardGap": headerToFirstCardGap.map(Double.init) ?? NSNull(),
+                "topToFirstCardGap": topToFirstCardGap.map(Double.init) ?? NSNull(),
+                "appNameFrame": dictionary(from: column.appNameLayer.frame),
+                "appNameAlignment": column.appNameLayer.alignmentMode.rawValue,
+                "appNameColor": colorDictionary(from: column.appNameLayer.foregroundColor),
+                "countFrame": dictionary(from: column.countLayer.frame),
                 "verticalScrollOffset": Double(waterfallColumnScrollOffsets[appGroupIndex] ?? 0),
                 "maxVerticalScrollOffset": Double(waterfallMaxVerticalScrollOffset(for: appGroupIndex)),
                 "cards": waterfallCardReports(for: column)
@@ -3408,6 +3939,8 @@
                 "closeButtonFrame": dictionary(from: card.closeButton.containerLayer.frame),
                 "closeButtonSize": Double(card.closeButton.containerLayer.bounds.width),
                 "closeButtonFillColor": colorDictionary(from: card.closeButton.containerLayer.backgroundColor),
+                "closeButtonGlyphColor": colorDictionary(from: card.closeButton.glyphLayer.strokeColor),
+                "closeButtonGlyphLineWidth": Double(card.closeButton.glyphLayer.lineWidth),
                 "shineVisible": card.shineLayer.opacity > 0,
                 "zPosition": Double(card.containerLayer.zPosition),
                 "frame": dictionary(from: card.containerLayer.frame),
@@ -3503,15 +4036,16 @@
     private func waterfallCardVisualStates(for card: WaterfallCardLayers) -> [String] {
         var states: [String] = []
         let isSelected = card.item.window.id == effectiveSelection?.windowID
-        if shouldShowSelectedVisual(isSelected) {
+        let isHovered = card.item.window.id == hoveredWindowID
+        if shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) {
             states.append("selected")
         } else if isSelected {
             states.append("selectedVisualSuppressed")
         }
-        if card.item.window.id == hoveredWindowID {
+        if isHovered {
             states.append("hover")
         }
-        if card.item.primarySpaceID == activeSpaceFocusID {
+        if isWaterfallCardSpaceFocused(card) {
             states.append("spaceFocused")
         }
         if card.item.window.isMinimized {
@@ -3527,32 +4061,78 @@
         window?.backingScaleFactor ?? NSScreen.main?.backingScaleFactor ?? 2
     }
 
+    private func spaceLaneDisplayGroupReports() -> [[String: Any]] {
+        spaceLaneDisplayGroups.map { group in
+            let visibleFrame = group.containerLayer.frame.offsetBy(dx: spaceLaneContentLayer.frame.minX, dy: 0)
+            return [
+                "displayUUID": group.display.displayUUID,
+                "displayIndex": group.display.displayIndex,
+                "displayLabel": group.display.displayLabel,
+                "physical": group.display.physical,
+                "spaceCount": group.segments.count,
+                "spaceIDs": group.segments.map(\.space.id),
+                "spaceLabels": group.segments.map(\.space.label),
+                "frame": dictionary(from: group.containerLayer.frame),
+                "visibleFrame": dictionary(from: visibleFrame),
+                "backgroundColor": colorDictionary(from: group.containerLayer.backgroundColor),
+                "borderColor": colorDictionary(from: group.containerLayer.borderColor),
+                "borderWidth": Double(group.containerLayer.borderWidth),
+                "shadowOpacity": Double(group.containerLayer.shadowOpacity),
+                "shadowRadius": Double(group.containerLayer.shadowRadius),
+                "groupGap": Double(SpaceLaneMetrics.groupGap),
+                "groupHorizontalPadding": Double(SpaceLaneMetrics.groupHorizontalPadding),
+                "groupVerticalPadding": Double(SpaceLaneMetrics.groupVerticalPadding),
+                "segmentGap": Double(SpaceLaneMetrics.segmentGap)
+            ] as [String: Any]
+        }
+    }
+
     private func spaceLaneSegmentReports() -> [[String: Any]] {
         currentViewModel?.displays.flatMap { display in
             display.spaces.compactMap { space in
                 guard let segment = spaceLaneSegments.first(where: { $0.space.id == space.id }) else {
                     return nil
                 }
-                let visibleFrame = segment.containerLayer.frame.offsetBy(dx: spaceLaneContentLayer.frame.minX, dy: 0)
+                let isAppAssociated = appHoverAssociatedSpaceIDs.contains(space.id)
+                let isWindowAssociated = space.id == windowHoverAssociatedSpaceID
+                let group = spaceLaneDisplayGroup(containingSpaceID: space.id)
+                let visibleFrame = segment.containerLayer.frame.offsetBy(
+                    dx: (group?.containerLayer.frame.minX ?? 0) + spaceLaneContentLayer.frame.minX,
+                    dy: group?.containerLayer.frame.minY ?? 0
+                )
+                let fullscreenMarkerFrame = segment.fullscreenMarkerLayer?.frame
+                let fullscreenMarkerCenterRatio = fullscreenMarkerFrame.map {
+                    Double($0.midY / max(1, segment.containerLayer.bounds.height))
+                }
                 return [
                     "displayUUID": display.displayUUID,
                     "displayLabel": display.displayLabel,
+                    "spaceID": space.id,
                     "label": space.label,
                     "type": spaceTypeName(space.type),
                     "visualStates": visualStates(for: space),
                     "fullscreenMarkerVisible": segment.fullscreenMarkerLayer != nil,
                     "fullscreenMarkerKind": segment.fullscreenMarkerLayer == nil ? "none" : "cornerBracket",
-                    "fullscreenMarkerFrame": segment.fullscreenMarkerLayer.map { dictionary(from: $0.frame) } ?? NSNull(),
+                    "fullscreenMarkerFrame": fullscreenMarkerFrame.map { dictionary(from: $0) } ?? NSNull(),
+                    "fullscreenMarkerCenterRatio": fullscreenMarkerCenterRatio ?? NSNull(),
                     "windowBlockCount": segment.windowBlocks.count,
+                    "windowBlockColors": segment.windowBlocks.map { colorDictionary(from: $0.backgroundColor) },
+                    "windowBlockFrames": segment.windowBlocks.map { dictionary(from: $0.frame) },
+                    "backgroundKind": spaceLaneFillKind(for: segment.containerLayer),
                     "backgroundColor": colorDictionary(from: segment.containerLayer.backgroundColor),
+                    "gradientColors": gradientColorDictionaries(from: segment.containerLayer),
+                    "borderColor": colorDictionary(from: segment.containerLayer.borderColor),
+                    "borderWidth": Double(segment.containerLayer.borderWidth),
                     "shadowOpacity": Double(segment.containerLayer.shadowOpacity),
                     "shadowRadius": Double(segment.containerLayer.shadowRadius),
                     "appFrame": dictionary(from: segment.appLayer.frame),
                     "frame": dictionary(from: segment.containerLayer.frame),
                     "visibleFrame": dictionary(from: visibleFrame),
                     "isCurrent": space.isCurrent,
-                    "isHovered": space.id == hoveredSpaceID,
+                    "isHovered": space.id == activeSpaceFocusID,
                     "isFocused": space.id == activeSpaceFocusID,
+                    "isAppAssociated": isAppAssociated,
+                    "isWindowAssociated": isWindowAssociated,
                     "windowCount": space.windowCount,
                     "appCount": space.appCount,
                     "appNames": space.appNames
@@ -3561,12 +4141,24 @@
         } ?? []
     }
 
+    private func spaceLaneDisplayGroup(containingSpaceID spaceID: UInt64) -> SpaceLaneDisplayGroupLayers? {
+        spaceLaneDisplayGroups.first { group in
+            group.segments.contains { $0.space.id == spaceID }
+        }
+    }
+
     private func visualStates(for space: QuickSwitchSpaceViewModel) -> [String] {
         var states: [String] = []
         if space.isCurrent {
             states.append("current")
         }
-        if space.id == hoveredSpaceID {
+        if appHoverAssociatedSpaceIDs.contains(space.id) {
+            states.append("appAssociated")
+        }
+        if space.id == windowHoverAssociatedSpaceID {
+            states.append("windowAssociated")
+        }
+        if space.id == activeSpaceFocusID {
             states.append("hover")
         }
         if space.id == activeSpaceFocusID {
@@ -3619,4 +4211,23 @@
             "alpha": Double(nsColor.alphaComponent)
         ]
     }
+
+    private func gradientColorDictionaries(from layer: CALayer) -> [[String: Double]] {
+        guard let gradientLayer = layer as? CAGradientLayer else { return [] }
+        return (gradientLayer.colors ?? []).map { color in
+            let cgColor = color as! CGColor
+            return colorDictionary(from: cgColor)
+        }
+    }
+
+    private func spaceLaneFillKind(for layer: CALayer) -> String {
+        if !gradientColorDictionaries(from: layer).isEmpty {
+            return "gradient"
+        }
+        let background = colorDictionary(from: layer.backgroundColor)
+        if background["alpha", default: 0] <= 0.01 {
+            return "clear"
+        }
+        return "solid"
+    }
 }

--
Gitblit v1.9.3