Ariver
2026-06-12 a4af5d51e8fbb8a0e53f98aa9f9e238536aaba24
C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -1,5 +1,6 @@
import AppKit
import QuartzCore
import Darwin
import AlignerCore
enum QuickSwitchCommitSource: String {
@@ -9,7 +10,10 @@
@MainActor
final class QuickSwitchRootView: NSView {
    private let backdropBlurView = NSVisualEffectView()
    private let layerHostView = NSView()
    private let backgroundLayer = CAGradientLayer()
    private let safeAreaFillLayer = CALayer()
    private let glassLayer = CALayer()
    private let laneLayer = CALayer()
    private let spaceLaneContentLayer = CALayer()
@@ -17,11 +21,18 @@
    private let appShelfContentLayer = CALayer()
    private let waterfallLayer = CALayer()
    private let waterfallContentLayer = CALayer()
    private lazy var closeConfirmationLayers = makeCloseConfirmationLayers()
    private let closeFeedbackLayer = CALayer()
    private let closeFeedbackTextLayer = CATextLayer()
    private let settingsButtonLayer = CALayer()
    private let settingsButtonIconLayer = CALayer()
    private let settingsButtonFallbackTextLayer = 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
@@ -33,13 +44,27 @@
    private var hoveredAppGroupIndex: Int?
    private var hoveredWindowID: UInt32?
    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 isSettingsButtonHovered = false
    private var hoverTarget: HoverTarget = .none
    private var hoverTargetSource: HoverTargetSource = .event
    private var hoveredSpaceLaneID: UInt64?
    private var focusedSpaceID: UInt64?
    private var debugHoveredAppGroupIndex: Int?
    private var waterfallColumns: [WaterfallColumnLayers] = []
    private var waterfallViewMode: QuickSwitchWaterfallViewMode = .verticalColumns
    private var waterfallContentWidth: CGFloat = 0
    private var waterfallScrollOffset: CGFloat = 0
    private var waterfallColumnScrollOffsets: [Int: CGFloat] = [:]
    private var horizontalMasonryContentHeight: CGFloat = 0
    private var horizontalMasonryScrollOffset: CGFloat = 0
    private var currentSelection: QuickSwitchSelection?
    private var columnSelectionHistory: [Int: Int] = [:]
    private var keyboardCommandsApplied: [String] = []
@@ -60,8 +85,140 @@
    private var keyboardResponseLatencyMilliseconds: [Double] = []
    private var mouseResponseLatencyMilliseconds: [Double] = []
    private var hoverResponseLatencyMilliseconds: [Double] = []
    private var lastProjectionTransitionMovedLayerCount = 0
    private var lastProjectionTransitionFadeInLayerCount = 0
    private var lastProjectionTransitionFadeOutLayerCount = 0
    private var lastProjectionTransitionDurationMilliseconds: Double = 0
    private var suppressEventInput = false
    private var suppressEventBackgroundClicks = false
    var onEscape: (() -> Void)?
    var onCommitSelection: ((QuickSwitchSelection, QuickSwitchCommitSource) -> Void)?
    var onSpaceLaneClick: ((UInt64, Int) -> Void)?
    var onBackgroundClick: (() -> Bool)?
    var onOpenSettings: (() -> Void)?
    var onRequestClose: ((QuickSwitchCloseRequest) -> Void)?
    var onCloseConfirmationDisabled: (() -> Void)?
    private enum CloseTarget: Equatable {
        case app(appGroupIndex: Int)
        case window(appGroupIndex: Int, windowIndex: Int, windowID: UInt32)
        var kindDescription: String {
            switch self {
            case .app:
                return "app"
            case .window:
                return "window"
            }
        }
    }
    private struct CloseButtonLayers {
        let containerLayer: CALayer
        let glyphLayer: CAShapeLayer
    }
    private struct CloseConfirmationLayers {
        let containerLayer: CALayer
        let backgroundLayer: CAShapeLayer
        let titleLayer: CATextLayer
        let messageLayer: CATextLayer
        let checkboxLayer: CALayer
        let checkboxMarkLayer: CATextLayer
        let checkboxLabelLayer: CATextLayer
        let confirmButtonLayer: CALayer
        let confirmButtonTextLayer: CATextLayer
        let cancelButtonLayer: CALayer
        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 {
        let space: QuickSwitchSpaceViewModel
@@ -69,8 +226,15 @@
        let labelLayer: CATextLayer
        let countLayer: CATextLayer
        let appLayer: CATextLayer
        let splitAppLayers: [CATextLayer]
        let windowBlocks: [CALayer]
        let fullscreenMarkerLayer: CALayer?
    }
    private struct SpaceLaneDisplayGroupLayers {
        let display: QuickSwitchDisplayViewModel
        let containerLayer: CALayer
        let segments: [SpaceLaneSegmentLayers]
    }
    private struct AppShelfItemLayers {
@@ -81,6 +245,7 @@
        let labelLayer: CATextLayer
        let badgeLayer: CATextLayer
        let selectedIndicatorLayer: CALayer
        let closeButton: CloseButtonLayers
    }
    private struct AppShelfLayout {
@@ -100,12 +265,30 @@
    private struct WaterfallCardLayers {
        let item: QuickSwitchWindowCardViewModel
        let containerLayer: CALayer
        let titleBarLayer: CALayer
        let titleLayer: CATextLayer
        let metaLayer: CATextLayer
        let thumbnailLayer: CALayer
        let appIconLayer: CALayer
        let stateLayer: CATextLayer
        let shineLayer: CAGradientLayer
        let closeButton: CloseButtonLayers
    }
    private struct ProjectionLayerSnapshot {
        let layer: CALayer
        let frame: CGRect
    }
    private struct ProjectionColumnSnapshot {
        let layer: CALayer
        let frame: CGRect
        let cardsByWindowID: [UInt32: ProjectionLayerSnapshot]
    }
    private struct ProjectionTransitionSnapshot {
        let appShelfItemsByAppGroupIndex: [Int: ProjectionLayerSnapshot]
        let waterfallColumnsByAppGroupIndex: [Int: ProjectionColumnSnapshot]
    }
    private enum AppShelfMetrics {
@@ -118,16 +301,38 @@
        static let maxRows = 2
    }
    private enum RootLayoutMetrics {
        static let horizontalMargin: CGFloat = 24
        static let topContentMargin: CGFloat = 24
        static let shelfGap: CGFloat = 16
        static let waterfallTopGap: CGFloat = 14
        static let bottomMargin: CGFloat = 34
    }
    private enum WaterfallMetrics {
        static let gap: CGFloat = 16
        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 = 118
        static let cardHeight: CGFloat = 138
        static let cardGap: CGFloat = 12
        static let revealPadding: CGFloat = 24
        static let horizontalRevealPadding: CGFloat = 0
    }
    private enum HorizontalMasonryMetrics {
        static let horizontalPadding: CGFloat = 12
        static let verticalPadding: CGFloat = 0
        static let horizontalGap: CGFloat = 16
        static let verticalGap: CGFloat = WaterfallMetrics.cardGap
        static let cardMinWidth: CGFloat = 220
        static let cardMaxWidth: CGFloat = 320
        static let cardHeight: CGFloat = WaterfallMetrics.cardHeight
    }
    private enum WaterfallHorizontalIntent {
@@ -141,14 +346,28 @@
        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) {
        super.init(frame: frameRect)
        setupBackdropBlurView()
        setupLayerHostView()
        wantsLayer = true
        setupLayers()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupBackdropBlurView()
        setupLayerHostView()
        wantsLayer = true
        setupLayers()
    }
@@ -166,7 +385,7 @@
        let trackingArea = NSTrackingArea(
            rect: bounds,
            options: [.activeInKeyWindow, .mouseMoved, .mouseEnteredAndExited, .inVisibleRect],
            options: [.activeAlways, .mouseMoved, .mouseEnteredAndExited, .inVisibleRect],
            owner: self,
            userInfo: nil
        )
@@ -176,10 +395,19 @@
    override func layout() {
        super.layout()
        backdropBlurView.frame = bounds
        layerHostView.frame = bounds
        layoutLayers()
    }
    override func keyDown(with event: NSEvent) {
        if suppressEventInput {
            DevelopmentDiagnostics.log("quickSwitch.view.keyDown.ignoredForDebugSequence", [
                "keyCode": event.keyCode
            ])
            return
        }
        switch event.keyCode {
        case TriggerKeyCode.escape where Self.shouldDismissOnEscape(event):
            onEscape?()
@@ -201,6 +429,8 @@
    }
    override func scrollWheel(with event: NSEvent) {
        refreshHoverTarget(at: event.locationInWindow)
        let dominantDelta = abs(event.scrollingDeltaX) >= abs(event.scrollingDeltaY)
            ? event.scrollingDeltaX
            : event.scrollingDeltaY
@@ -220,6 +450,15 @@
        }
        if shouldScrollWaterfall(for: event) {
            if isHorizontalMasonryMode {
                let currentOffset = horizontalMasonryScrollOffset
                horizontalMasonryScrollOffset = clampedHorizontalMasonryOffset(
                    currentOffset - event.scrollingDeltaY
                )
                needsLayout = true
                return
            }
            if abs(event.scrollingDeltaX) >= abs(event.scrollingDeltaY), waterfallAlignmentScrollable {
                waterfallScrollOffset = clampedWaterfallAlignmentOffset(waterfallScrollOffset - event.scrollingDeltaX)
                needsLayout = true
@@ -246,50 +485,63 @@
    override func mouseMoved(with event: NSEvent) {
        guard debugHoveredAppGroupIndex == nil else { return }
        if let segment = spaceLaneSegment(at: event.locationInWindow) {
            hoverSpaceLaneSegment(segment)
        let settingsHovered = settingsButtonContains(windowLocation: event.locationInWindow)
        if isSettingsButtonHovered != settingsHovered {
            isSettingsButtonHovered = settingsHovered
            needsLayout = true
        }
        if settingsHovered {
            hoveredCloseTarget = nil
            setHoverTarget(.none)
            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
        let closeTarget = closeButtonTarget(at: event.locationInWindow)
        if hoveredCloseTarget != closeTarget {
            hoveredCloseTarget = closeTarget
            needsLayout = true
        }
        if let item = appShelfItem(at: event.locationInWindow) {
            hoveredSpaceLaneID = nil
            updateHoverState(
                appGroupIndex: item.item.appGroupIndex,
                windowID: nil,
                spaceID: nil,
                alignsWaterfallWithAppShelf: true
            )
            return
        }
        hoveredSpaceLaneID = 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
        updateHoverState(appGroupIndex: nil, windowID: nil, spaceID: nil)
        isSettingsButtonHovered = false
        hoveredCloseTarget = nil
        setHoverTarget(.none)
    }
    override func mouseDown(with event: NSEvent) {
        if suppressEventInput {
            DevelopmentDiagnostics.log("quickSwitch.view.mouseDown.ignoredForDebugSequence", [
                "x": event.locationInWindow.x,
                "y": event.locationInWindow.y,
                "bounds": NSStringFromRect(bounds)
            ])
            return
        }
        if handleCloseConfirmationMouseDown(at: event.locationInWindow) {
            return
        }
        if settingsButtonContains(windowLocation: event.locationInWindow) {
            clickSettingsButton(source: "mouse")
            return
        }
        if let closeTarget = closeButtonTarget(at: event.locationInWindow) {
            beginCloseConfirmation(for: closeTarget)
            return
        }
        if let card = waterfallCard(at: event.locationInWindow) {
            clickWindowCard(card)
            return
        }
        if let segment = spaceLaneSegment(at: event.locationInWindow) {
            clickSpaceLaneSegment(segment)
            clickSpaceLaneSegment(segment, clickCount: max(1, event.clickCount))
            return
        }
@@ -303,15 +555,16 @@
            "y": event.locationInWindow.y,
            "bounds": NSStringFromRect(bounds)
        ])
        if !suppressEventBackgroundClicks, onBackgroundClick?() == true {
            return
        }
        super.mouseDown(with: event)
    }
    func setDebugHoveredAppGroupIndex(_ index: Int?) {
        debugHoveredAppGroupIndex = index
        setHoverTarget(.none, source: .debug)
        hoveredAppGroupIndex = index
        hoveredWindowID = nil
        hoveredSpaceID = nil
        hoveredSpaceLaneID = nil
        needsLayout = true
    }
@@ -342,7 +595,19 @@
            if isHoverCommand(command) {
                hoverResponseLatencyMilliseconds.append(elapsed)
            }
            if lastCommitSource == .mouse {
                break
            }
        }
    }
    func setSuppressEventBackgroundClicks(_ suppressed: Bool) {
        suppressEventBackgroundClicks = suppressed
    }
    func setSuppressEventInput(_ suppressed: Bool) {
        suppressEventInput = suppressed
        suppressEventBackgroundClicks = suppressed
    }
    func beginPerformanceFirstFrameMeasurement() {
@@ -366,6 +631,65 @@
        hoverResponseLatencyMilliseconds = []
    }
    func setCloseConfirmationRequired(_ required: Bool) {
        closeConfirmationRequired = required
        if !required {
            pendingCloseTarget = nil
            pendingCloseRequest = nil
            pendingCloseOptOut = false
            closeConfirmationLayers.containerLayer.isHidden = true
            closeConfirmationArrowEdge = nil
        }
        needsLayout = true
    }
    func setWaterfallViewMode(_ mode: QuickSwitchWaterfallViewMode) {
        guard waterfallViewMode != mode else { return }
        waterfallViewMode = mode
        waterfallScrollOffset = 0
        waterfallColumnScrollOffsets = [:]
        horizontalMasonryScrollOffset = 0
        horizontalMasonryContentHeight = 0
        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
    }
    func applyScreenshot(_ resolution: ScreenshotResolution, for windowID: UInt32) {
        screenshotSkippedReasonsByWindowID[windowID] = nil
        screenshotSourcesByWindowID[windowID] = resolution.source
@@ -373,7 +697,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
    }
@@ -390,34 +716,77 @@
        }
    }
    func effectiveSelectionForProjection() -> QuickSwitchSelection? {
        effectiveSelection
    }
    func apply(viewModel: QuickSwitchViewModel?) {
        apply(viewModel: viewModel, preservingInteractionState: false, animatedProjection: false)
    }
    func applyProjected(viewModel: QuickSwitchViewModel?) {
        apply(viewModel: viewModel, preservingInteractionState: true, animatedProjection: true)
    }
    private func apply(
        viewModel: QuickSwitchViewModel?,
        preservingInteractionState: Bool,
        animatedProjection: Bool
    ) {
        let transitionSnapshot = animatedProjection ? captureProjectionTransitionSnapshot() : nil
        if !animatedProjection {
            resetProjectionTransitionReport()
        }
        currentViewModel = viewModel
        screenshotSourcesByWindowID = [:]
        screenshotSkippedReasonsByWindowID = [:]
        currentSelection = viewModel?.initialSelection
        columnSelectionHistory = [:]
        keyboardCommandsApplied = []
        lastKeyboardCommand = nil
        tabIgnoredCount = 0
        lastCommittedSelection = nil
        lastCommitSource = nil
        mouseCommandsApplied = []
        lastMouseCommand = nil
        lastClickedAppGroupIndex = nil
        lastAppShelfClickChangedSelection = nil
        lastClickedWindowID = nil
        lastSpaceFocusedAppGroupIndex = nil
        lastSpaceLaneClickSpaceID = nil
        lastAppShelfScrollSyncedAppGroupIndex = nil
        hoveredWindowID = nil
        hoveredSpaceID = nil
        hoveredSpaceLaneID = nil
        focusedSpaceID = nil
        if debugHoveredAppGroupIndex == nil {
            hoveredAppGroupIndex = nil
        if preservingInteractionState {
            currentSelection = validSelection(currentSelection, in: viewModel) ?? viewModel?.initialSelection
            columnSelectionHistory = columnSelectionHistory.filter { appGroupIndex, _ in
                viewModel?.waterfallColumns.contains { $0.appGroupIndex == appGroupIndex } == true
            }
            if let currentSelection {
                columnSelectionHistory[currentSelection.appGroupIndex] = currentSelection.windowIndex
            }
            hoveredCloseTarget = nil
            pendingCloseTarget = nil
            pendingCloseRequest = nil
            pendingCloseOptOut = false
            closeConfirmationLayers.containerLayer.isHidden = true
            closeConfirmationArrowEdge = nil
        } else {
            screenshotSourcesByWindowID = [:]
            screenshotSkippedReasonsByWindowID = [:]
            currentSelection = viewModel?.initialSelection
            columnSelectionHistory = [:]
            keyboardCommandsApplied = []
            lastKeyboardCommand = nil
            tabIgnoredCount = 0
            lastCommittedSelection = nil
            lastCommitSource = nil
            mouseCommandsApplied = []
            lastMouseCommand = nil
            lastClickedAppGroupIndex = nil
            lastAppShelfClickChangedSelection = nil
            lastClickedWindowID = nil
            lastSpaceFocusedAppGroupIndex = nil
            lastSpaceLaneClickSpaceID = nil
            lastAppShelfScrollSyncedAppGroupIndex = nil
            waterfallScrollOffset = 0
            waterfallColumnScrollOffsets = [:]
            horizontalMasonryScrollOffset = 0
            horizontalMasonryContentHeight = 0
            focusedSpaceID = nil
            hoveredCloseTarget = nil
            pendingCloseTarget = nil
            pendingCloseRequest = nil
            pendingCloseOptOut = false
            closeConfirmationLayers.containerLayer.isHidden = true
            closeConfirmationArrowEdge = nil
            clearCloseFeedback()
            setHoverTarget(.none)
            hoveredAppGroupIndex = debugHoveredAppGroupIndex
        }
        if let currentSelection {
            columnSelectionHistory[currentSelection.appGroupIndex] = currentSelection.windowIndex
        }
@@ -427,6 +796,250 @@
        needsLayout = true
        ensureCurrentSelectionVisible(horizontalIntent: .alignWithAppShelf(animated: false))
        needsLayout = true
        if animatedProjection, let transitionSnapshot {
            layoutSubtreeIfNeeded()
            applyProjectionTransitionAnimations(from: transitionSnapshot)
        }
    }
    private func validSelection(
        _ selection: QuickSwitchSelection?,
        in viewModel: QuickSwitchViewModel?
    ) -> QuickSwitchSelection? {
        guard let selection, let viewModel else { return nil }
        for column in viewModel.waterfallColumns {
            if let card = column.windows.first(where: { $0.window.id == selection.windowID }) {
                return QuickSwitchSelection(
                    appGroupIndex: card.appGroupIndex,
                    windowIndex: card.windowIndex,
                    windowID: card.window.id
                )
            }
        }
        return nil
    }
    private func captureProjectionTransitionSnapshot() -> ProjectionTransitionSnapshot {
        ProjectionTransitionSnapshot(
            appShelfItemsByAppGroupIndex: Dictionary(
                uniqueKeysWithValues: appShelfItems.map { item in
                    (
                        item.item.appGroupIndex,
                        ProjectionLayerSnapshot(
                            layer: item.containerLayer,
                            frame: item.containerLayer.frame
                        )
                    )
                }
            ),
            waterfallColumnsByAppGroupIndex: Dictionary(
                uniqueKeysWithValues: waterfallColumns.map { column in
                    (
                        column.column.appGroupIndex,
                        ProjectionColumnSnapshot(
                            layer: column.containerLayer,
                            frame: column.containerLayer.frame,
                            cardsByWindowID: Dictionary(
                                uniqueKeysWithValues: column.cards.map { card in
                                    (
                                        card.item.window.id,
                                        ProjectionLayerSnapshot(
                                            layer: card.containerLayer,
                                            frame: card.containerLayer.frame
                                        )
                                    )
                                }
                            )
                        )
                    )
                }
            )
        )
    }
    private func applyProjectionTransitionAnimations(from snapshot: ProjectionTransitionSnapshot) {
        let reduceMotion = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion
        let duration: CFTimeInterval = reduceMotion ? 0.12 : 0.21
        let timing = CAMediaTimingFunction(name: reduceMotion ? .linear : .easeOut)
        var movedLayerCount = 0
        var fadeInLayerCount = 0
        var fadeOutLayerCount = 0
        for item in appShelfItems {
            guard let oldSnapshot = snapshot.appShelfItemsByAppGroupIndex[item.item.appGroupIndex] else {
                if addFadeInAnimation(to: item.containerLayer, duration: duration, timing: timing) {
                    fadeInLayerCount += 1
                }
                continue
            }
            if addPositionAnimation(to: item.containerLayer, from: oldSnapshot.frame, duration: duration, timing: timing) {
                movedLayerCount += 1
            }
        }
        let visibleAppGroupIndexes = Set(appShelfItems.map(\.item.appGroupIndex))
        for (appGroupIndex, oldSnapshot) in snapshot.appShelfItemsByAppGroupIndex
            where !visibleAppGroupIndexes.contains(appGroupIndex) {
            if addFadeOutSnapshot(
                oldSnapshot,
                to: appShelfContentLayer,
                frame: oldSnapshot.frame,
                duration: duration,
                timing: timing
            ) {
                fadeOutLayerCount += 1
            }
        }
        for column in waterfallColumns {
            guard let oldSnapshot = snapshot.waterfallColumnsByAppGroupIndex[column.column.appGroupIndex] else {
                if addFadeInAnimation(to: column.containerLayer, duration: duration, timing: timing) {
                    fadeInLayerCount += 1
                }
                continue
            }
            if addPositionAnimation(to: column.containerLayer, from: oldSnapshot.frame, duration: duration, timing: timing) {
                movedLayerCount += 1
            }
            for card in column.cards {
                guard let oldCardSnapshot = oldSnapshot.cardsByWindowID[card.item.window.id] else {
                    if addFadeInAnimation(to: card.containerLayer, duration: duration, timing: timing) {
                        fadeInLayerCount += 1
                    }
                    continue
                }
                if addPositionAnimation(to: card.containerLayer, from: oldCardSnapshot.frame, duration: duration, timing: timing) {
                    movedLayerCount += 1
                }
            }
        }
        let visibleColumnsByAppGroupIndex = Dictionary(
            uniqueKeysWithValues: waterfallColumns.map { ($0.column.appGroupIndex, $0) }
        )
        for (appGroupIndex, oldSnapshot) in snapshot.waterfallColumnsByAppGroupIndex {
            guard let visibleColumn = visibleColumnsByAppGroupIndex[appGroupIndex] else {
                if addFadeOutSnapshot(
                    ProjectionLayerSnapshot(layer: oldSnapshot.layer, frame: oldSnapshot.frame),
                    to: waterfallContentLayer,
                    frame: oldSnapshot.frame,
                    duration: duration,
                    timing: timing
                ) {
                    fadeOutLayerCount += 1
                }
                continue
            }
            let visibleWindowIDs = Set(visibleColumn.cards.map(\.item.window.id))
            for (windowID, oldCardSnapshot) in oldSnapshot.cardsByWindowID
                where !visibleWindowIDs.contains(windowID) {
                let absoluteFrame = oldCardSnapshot.frame.offsetBy(
                    dx: oldSnapshot.frame.minX,
                    dy: oldSnapshot.frame.minY
                )
                if addFadeOutSnapshot(
                    oldCardSnapshot,
                    to: waterfallContentLayer,
                    frame: absoluteFrame,
                    duration: duration,
                    timing: timing
                ) {
                    fadeOutLayerCount += 1
                }
            }
        }
        lastProjectionTransitionMovedLayerCount = movedLayerCount
        lastProjectionTransitionFadeInLayerCount = fadeInLayerCount
        lastProjectionTransitionFadeOutLayerCount = fadeOutLayerCount
        lastProjectionTransitionDurationMilliseconds = duration * 1_000
    }
    private func resetProjectionTransitionReport() {
        lastProjectionTransitionMovedLayerCount = 0
        lastProjectionTransitionFadeInLayerCount = 0
        lastProjectionTransitionFadeOutLayerCount = 0
        lastProjectionTransitionDurationMilliseconds = 0
    }
    @discardableResult
    private func addPositionAnimation(
        to layer: CALayer,
        from oldFrame: CGRect,
        duration: CFTimeInterval,
        timing: CAMediaTimingFunction
    ) -> Bool {
        let oldPosition = CGPoint(x: oldFrame.midX, y: oldFrame.midY)
        let newPosition = layer.position
        guard abs(oldPosition.x - newPosition.x) > 0.5 || abs(oldPosition.y - newPosition.y) > 0.5 else {
            return false
        }
        let animation = CABasicAnimation(keyPath: "position")
        animation.fromValue = oldPosition
        animation.toValue = newPosition
        animation.duration = duration
        animation.timingFunction = timing
        animation.isRemovedOnCompletion = true
        layer.add(animation, forKey: "quickSwitchProjectionPosition")
        return true
    }
    @discardableResult
    private func addFadeInAnimation(
        to layer: CALayer,
        duration: CFTimeInterval,
        timing: CAMediaTimingFunction
    ) -> Bool {
        let animation = CABasicAnimation(keyPath: "opacity")
        animation.fromValue = 0
        animation.toValue = layer.opacity
        animation.duration = duration
        animation.timingFunction = timing
        animation.isRemovedOnCompletion = true
        layer.add(animation, forKey: "quickSwitchProjectionFadeIn")
        return true
    }
    @discardableResult
    private func addFadeOutSnapshot(
        _ snapshot: ProjectionLayerSnapshot,
        to parentLayer: CALayer,
        frame: CGRect,
        duration: CFTimeInterval,
        timing: CAMediaTimingFunction
    ) -> Bool {
        let layer = snapshot.layer
        layer.removeAllAnimations()
        layer.removeFromSuperlayer()
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        layer.frame = frame
        layer.opacity = max(layer.opacity, 0.01)
        layer.zPosition = max(layer.zPosition, 240)
        parentLayer.addSublayer(layer)
        CATransaction.commit()
        let animation = CABasicAnimation(keyPath: "opacity")
        animation.fromValue = layer.opacity
        animation.toValue = 0
        animation.duration = duration
        animation.timingFunction = timing
        animation.isRemovedOnCompletion = true
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        layer.opacity = 0
        CATransaction.commit()
        layer.add(animation, forKey: "quickSwitchProjectionFadeOut")
        DispatchQueue.main.asyncAfter(deadline: .now() + duration + 0.05) { [weak layer] in
            layer?.removeFromSuperlayer()
        }
        return true
    }
    func reportDictionary() -> [String: Any] {
@@ -443,19 +1056,38 @@
            "viewClass": String(describing: Self.self),
            "isLayerBacked": wantsLayer,
            "bounds": dictionary(from: bounds),
            "safeAreaTopInset": Double(topSafeAreaInset),
            "safeAreaFillFrame": dictionary(from: safeAreaFillLayer.frame),
            "glassFrame": dictionary(from: glassLayer.frame),
            "glassCornerRadius": Double(glassLayer.cornerRadius),
            "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(),
            "spaceFilterLockedSpaceID": currentViewModel?.lockedSpaceID ?? NSNull(),
            "spaceFilterActive": currentViewModel?.lockedSpaceID != nil,
            "settingsButtonVisible": !settingsButtonLayer.isHidden,
            "settingsButtonHovered": isSettingsButtonHovered,
            "settingsButtonFrame": dictionary(from: settingsButtonLayer.frame),
            "spaceFocusAppGroupIndexes": spaceFocusedAppGroupIndexes(),
            "appHoverAssociatedSpaceIDs": appHoverAssociatedSpaceIDs.sorted(),
            "appHoverAssociatedSpaceLabels": appHoverAssociatedSpaceLabels,
            "windowHoverAssociatedSpaceID": windowHoverAssociatedSpaceID ?? NSNull(),
            "lastSpaceFocusedAppGroupIndex": lastSpaceFocusedAppGroupIndex ?? NSNull(),
            "lastSpaceLaneClickSpaceID": lastSpaceLaneClickSpaceID ?? NSNull(),
            "lastAppShelfScrollSyncedAppGroupIndex": lastAppShelfScrollSyncedAppGroupIndex ?? NSNull(),
@@ -483,13 +1115,19 @@
            "appShelfMinIconSize": Double(AppShelfMetrics.minIconSize),
            "appShelfItems": appShelfItemReports(),
            "waterfallFrame": dictionary(from: waterfallLayer.frame),
            "waterfallViewMode": waterfallViewMode.rawValue,
            "waterfallContentWidth": Double(waterfallContentWidth),
            "waterfallVisibleWidth": Double(waterfallLayer.bounds.width),
            "waterfallScrollOffset": Double(waterfallScrollOffset),
            "waterfallMaxScrollOffset": Double(waterfallMaxScrollOffset),
            "horizontalMasonryContentHeight": Double(horizontalMasonryContentHeight),
            "horizontalMasonryScrollOffset": Double(horizontalMasonryScrollOffset),
            "horizontalMasonryMaxScrollOffset": Double(horizontalMasonryMaxScrollOffset),
            "horizontalMasonryScrollable": horizontalMasonryMaxScrollOffset > 0,
            "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,
@@ -498,6 +1136,10 @@
            "waterfallAlignmentTolerance": Double(WaterfallAlignmentMetrics.tolerance),
            "waterfallAlignmentAnimationDurationMilliseconds": Double(WaterfallAlignmentMetrics.animationDuration * 1_000),
            "waterfallAlignmentAnimationTimingName": "easeOut",
            "projectionTransitionMovedLayerCount": lastProjectionTransitionMovedLayerCount,
            "projectionTransitionFadeInLayerCount": lastProjectionTransitionFadeInLayerCount,
            "projectionTransitionFadeOutLayerCount": lastProjectionTransitionFadeOutLayerCount,
            "projectionTransitionDurationMilliseconds": lastProjectionTransitionDurationMilliseconds,
            "reduceMotionEffective": NSWorkspace.shared.accessibilityDisplayShouldReduceMotion,
            "selectedAppGroupIndex": effectiveSelection?.appGroupIndex ?? NSNull(),
            "selectedWindowIndex": effectiveSelection?.windowIndex ?? NSNull(),
@@ -512,6 +1154,20 @@
            "hoveredAppGroupIndex": effectiveHoveredAppGroupIndex ?? NSNull(),
            "hoveredWindowID": hoveredWindowID ?? NSNull(),
            "hoveredSpaceID": hoveredSpaceID ?? NSNull(),
            "hoveredCloseTargetKind": hoveredCloseTarget?.kindDescription ?? NSNull(),
            "pendingCloseTargetKind": pendingCloseTarget?.kindDescription ?? NSNull(),
            "pendingCloseOptOut": pendingCloseOptOut,
            "closeConfirmationRequired": closeConfirmationRequired,
            "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(),
@@ -579,8 +1235,13 @@
            recordKeyboardCommand("enter")
            commitCurrentSelection()
        case "tab":
            tabIgnoredCount += 1
            recordKeyboardCommand("tabIgnored")
            if isHorizontalMasonryMode {
                recordKeyboardCommand("tab")
                moveSelection(.right)
            } else {
                tabIgnoredCount += 1
                recordKeyboardCommand("tabIgnored")
            }
        default:
            recordKeyboardCommand("unknown:\(command)")
        }
@@ -613,6 +1274,20 @@
            }
            recordMouseCommand("click-app:\(appGroupIndex)")
            clickAppShelfItem(appGroupIndex: appGroupIndex)
        case "hover-app-close" where parts.count == 2:
            guard let appGroupIndex = Int(parts[1]) else {
                recordMouseCommand("invalid:\(command)")
                return
            }
            recordMouseCommand("hover-app-close:\(appGroupIndex)")
            hoverAppCloseButton(appGroupIndex)
        case "click-app-close" where parts.count == 2:
            guard let appGroupIndex = Int(parts[1]) else {
                recordMouseCommand("invalid:\(command)")
                return
            }
            recordMouseCommand("click-app-close:\(appGroupIndex)")
            clickAppCloseButton(appGroupIndex)
        case "scroll-app-shelf" where parts.count == 2:
            guard let targetOffset = Double(parts[1]) else {
                recordMouseCommand("invalid:\(command)")
@@ -627,7 +1302,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 {
@@ -636,8 +1311,24 @@
            }
            recordMouseCommand("click-space:\(spaceID)")
            if let segment = spaceLaneSegment(spaceID: spaceID) {
                clickSpaceLaneSegment(segment)
                clickSpaceLaneSegment(segment, clickCount: 1)
            }
        case "double-click-space" where parts.count == 2:
            guard let spaceID = UInt64(parts[1]) else {
                recordMouseCommand("invalid:\(command)")
                return
            }
            recordMouseCommand("double-click-space:\(spaceID)")
            if let segment = spaceLaneSegment(spaceID: spaceID) {
                clickSpaceLaneSegment(segment, clickCount: 1)
                clickSpaceLaneSegment(segment, clickCount: 2)
            }
        case "click-background":
            recordMouseCommand("click-background")
            _ = onBackgroundClick?()
        case "click-settings":
            recordMouseCommand("click-settings")
            clickSettingsButton(source: "debug")
        case "hover-card" where parts.count == 3:
            guard let appGroupIndex = Int(parts[1]), let windowIndex = Int(parts[2]) else {
                recordMouseCommand("invalid:\(command)")
@@ -645,7 +1336,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 {
@@ -656,6 +1347,29 @@
            if let card = waterfallCard(appGroupIndex: appGroupIndex, windowIndex: windowIndex) {
                clickWindowCard(card)
            }
        case "hover-card-close" where parts.count == 3:
            guard let appGroupIndex = Int(parts[1]), let windowIndex = Int(parts[2]) else {
                recordMouseCommand("invalid:\(command)")
                return
            }
            recordMouseCommand("hover-card-close:\(appGroupIndex):\(windowIndex)")
            hoverCardCloseButton(appGroupIndex: appGroupIndex, windowIndex: windowIndex)
        case "click-card-close" where parts.count == 3:
            guard let appGroupIndex = Int(parts[1]), let windowIndex = Int(parts[2]) else {
                recordMouseCommand("invalid:\(command)")
                return
            }
            recordMouseCommand("click-card-close:\(appGroupIndex):\(windowIndex)")
            clickCardCloseButton(appGroupIndex: appGroupIndex, windowIndex: windowIndex)
        case "toggle-close-optout":
            recordMouseCommand("toggle-close-optout")
            toggleCloseConfirmationOptOut()
        case "confirm-close":
            recordMouseCommand("confirm-close")
            confirmPendingClose()
        case "cancel-close":
            recordMouseCommand("cancel-close")
            cancelPendingClose()
        default:
            recordMouseCommand("unknown:\(command)")
        }
@@ -674,41 +1388,75 @@
    }
    private func hoverAppGroup(_ appGroupIndex: Int) {
        hoveredSpaceLaneID = nil
        updateHoverState(
            appGroupIndex: appGroupIndex,
            windowID: nil,
            spaceID: nil,
            alignsWaterfallWithAppShelf: true
            alignsWaterfallWithAppShelf: true,
            source: .debug
        )
    }
    private func hoverWindowCard(_ card: WaterfallCardLayers) {
        hoveredSpaceLaneID = nil
    private func hoverAppCloseButton(_ appGroupIndex: Int) {
        hoverAppGroup(appGroupIndex)
        hoveredCloseTarget = .app(appGroupIndex: appGroupIndex)
        needsLayout = true
    }
    private func clickAppCloseButton(_ appGroupIndex: Int) {
        hoverAppCloseButton(appGroupIndex)
        beginCloseConfirmation(for: .app(appGroupIndex: appGroupIndex))
    }
    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 hoverSpaceLaneSegment(_ segment: SpaceLaneSegmentLayers) {
        focusSpace(segment.space.id, persistent: false)
    private func hoverCardCloseButton(appGroupIndex: Int, windowIndex: Int) {
        guard let card = waterfallCard(appGroupIndex: appGroupIndex, windowIndex: windowIndex) else { return }
        hoverWindowCard(card, source: .debug)
        hoveredCloseTarget = .window(
            appGroupIndex: appGroupIndex,
            windowIndex: windowIndex,
            windowID: card.item.window.id
        )
        needsLayout = true
    }
    private func clickSpaceLaneSegment(_ segment: SpaceLaneSegmentLayers) {
    private func clickCardCloseButton(appGroupIndex: Int, windowIndex: Int) {
        guard let card = waterfallCard(appGroupIndex: appGroupIndex, windowIndex: windowIndex) else { return }
        hoverCardCloseButton(appGroupIndex: appGroupIndex, windowIndex: windowIndex)
        beginCloseConfirmation(
            for: .window(
                appGroupIndex: appGroupIndex,
                windowIndex: windowIndex,
                windowID: card.item.window.id
            )
        )
    }
    private func hoverSpaceLaneSegment(_ segment: SpaceLaneSegmentLayers, source: HoverTargetSource = .event) {
        focusSpace(segment.space.id, persistent: false, source: source)
    }
    private func clickSpaceLaneSegment(_ segment: SpaceLaneSegmentLayers, clickCount: Int = 1) {
        DevelopmentDiagnostics.log("quickSwitch.view.clickSpaceLane", [
            "spaceID": segment.space.id,
            "label": segment.space.label
            "label": segment.space.label,
            "clickCount": clickCount
        ])
        focusSpace(segment.space.id, persistent: true)
        lastSpaceLaneClickSpaceID = segment.space.id
        onSpaceLaneClick?(segment.space.id, clickCount)
        lastSpaceLaneClickSpaceID = segment.space.id
    }
    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
@@ -752,12 +1500,14 @@
            columnSelectionHistory[nextSelection.appGroupIndex] = nextSelection.windowIndex
            lastAppShelfClickChangedSelection = previousSelection != nextSelection
            ensureWaterfallSelectionVisible(nextSelection, horizontalIntent: .alignWithAppShelf(animated: true))
            updateHoverState(appGroupIndex: appGroupIndex, windowID: nil, spaceID: nil)
            commitWindowCard(firstCard, trigger: "appShelf")
        } else {
            lastAppShelfClickChangedSelection = false
            ensureWaterfallColumnVisible(column)
            updateHoverState(appGroupIndex: appGroupIndex, windowID: nil, spaceID: nil)
            needsLayout = true
        }
        updateHoverState(appGroupIndex: appGroupIndex, windowID: nil, spaceID: nil)
        needsLayout = true
    }
    private func scrollAppShelf(to targetOffset: CGFloat) {
@@ -799,12 +1549,17 @@
    private func clickWindowCard(_ card: WaterfallCardLayers) {
        hoverWindowCard(card)
        commitWindowCard(card, trigger: "windowCard")
    }
    private func commitWindowCard(_ card: WaterfallCardLayers, trigger: String) {
        let selection = QuickSwitchSelection(
            appGroupIndex: card.item.appGroupIndex,
            windowIndex: card.item.windowIndex,
            windowID: card.item.window.id
        )
        DevelopmentDiagnostics.log("quickSwitch.view.clickWindowCard", [
        DevelopmentDiagnostics.log("quickSwitch.view.commitMouseWindow", [
            "trigger": trigger,
            "appGroupIndex": selection.appGroupIndex,
            "windowIndex": selection.windowIndex,
            "windowID": selection.windowID,
@@ -830,28 +1585,76 @@
        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
    }
    private func moveSelection(_ direction: QuickSwitchKeyboardDirection) {
        if isHorizontalMasonryMode {
            moveHorizontalMasonrySelection(direction)
            return
        }
        guard let currentViewModel,
              let snapshot = snapshotForFocus(from: currentViewModel),
              let current = currentSelection ?? currentViewModel.initialSelection,
@@ -881,6 +1684,113 @@
            "windowID": next.windowID
        ])
        needsLayout = true
    }
    private func moveHorizontalMasonrySelection(_ direction: QuickSwitchKeyboardDirection) {
        guard let current = currentSelection ?? currentViewModel?.initialSelection,
              let next = nextHorizontalMasonrySelection(from: current, direction: direction)
        else {
            DevelopmentDiagnostics.log("quickSwitch.view.moveHorizontalMasonrySelection.blocked", [
                "direction": String(describing: direction),
                "hasSelection": (currentSelection ?? currentViewModel?.initialSelection) != nil
            ])
            return
        }
        columnSelectionHistory[current.appGroupIndex] = current.windowIndex
        currentSelection = next
        columnSelectionHistory[next.appGroupIndex] = next.windowIndex
        ensureCurrentSelectionVisible(horizontalIntent: .alignWithAppShelf(animated: true))
        DevelopmentDiagnostics.log("quickSwitch.view.moveHorizontalMasonrySelection", [
            "direction": String(describing: direction),
            "appGroupIndex": next.appGroupIndex,
            "windowIndex": next.windowIndex,
            "windowID": next.windowID
        ])
        needsLayout = true
    }
    private func nextHorizontalMasonrySelection(
        from current: QuickSwitchSelection,
        direction: QuickSwitchKeyboardDirection
    ) -> QuickSwitchSelection? {
        layoutSubtreeIfNeeded()
        let cards = horizontalMasonryCardsInPlacementOrder()
        guard let currentIndex = cards.firstIndex(where: { $0.item.window.id == current.windowID }) else {
            return cards.first.map(selectionForCard)
        }
        switch direction {
        case .right:
            return selectionForCard(cards[(currentIndex + 1) % cards.count])
        case .left:
            return selectionForCard(cards[(currentIndex - 1 + cards.count) % cards.count])
        case .up:
            return nearestHorizontalMasonrySelection(from: cards[currentIndex], direction: .up)
                ?? selectionForCard(cards.first ?? cards[currentIndex])
        case .down:
            return nearestHorizontalMasonrySelection(from: cards[currentIndex], direction: .down)
                ?? selectionForCard(cards.last ?? cards[currentIndex])
        }
    }
    private func nearestHorizontalMasonrySelection(
        from currentCard: WaterfallCardLayers,
        direction: QuickSwitchKeyboardDirection
    ) -> QuickSwitchSelection? {
        let currentFrame = currentCard.containerLayer.frame
        let candidates = horizontalMasonryCardsInPlacementOrder().filter { card in
            guard card.item.window.id != currentCard.item.window.id else { return false }
            switch direction {
            case .up:
                return card.containerLayer.frame.minY >= currentFrame.maxY - 0.5
            case .down:
                return card.containerLayer.frame.maxY <= currentFrame.minY + 0.5
            case .left, .right:
                return false
            }
        }
        let best = candidates.min { lhs, rhs in
            let lhsFrame = lhs.containerLayer.frame
            let rhsFrame = rhs.containerLayer.frame
            let lhsPrimaryDistance = direction == .up
                ? lhsFrame.minY - currentFrame.maxY
                : currentFrame.minY - lhsFrame.maxY
            let rhsPrimaryDistance = direction == .up
                ? rhsFrame.minY - currentFrame.maxY
                : currentFrame.minY - rhsFrame.maxY
            if abs(lhsPrimaryDistance - rhsPrimaryDistance) > 0.5 {
                return lhsPrimaryDistance < rhsPrimaryDistance
            }
            let lhsHorizontalDistance = abs(lhsFrame.midX - currentFrame.midX)
            let rhsHorizontalDistance = abs(rhsFrame.midX - currentFrame.midX)
            if abs(lhsHorizontalDistance - rhsHorizontalDistance) > 0.5 {
                return lhsHorizontalDistance < rhsHorizontalDistance
            }
            return lhs.item.globalIndex < rhs.item.globalIndex
        }
        return best.map(selectionForCard)
    }
    private func horizontalMasonryCardsInPlacementOrder() -> [WaterfallCardLayers] {
        waterfallColumns
            .sorted { $0.column.appGroupIndex < $1.column.appGroupIndex }
            .flatMap { column in
                column.cards.sorted { $0.item.windowIndex < $1.item.windowIndex }
            }
    }
    private func selectionForCard(_ card: WaterfallCardLayers) -> QuickSwitchSelection {
        QuickSwitchSelection(
            appGroupIndex: card.item.appGroupIndex,
            windowIndex: card.item.windowIndex,
            windowID: card.item.window.id
        )
    }
    private func commitCurrentSelection() {
@@ -940,9 +1850,24 @@
        _ selection: QuickSwitchSelection,
        horizontalIntent: WaterfallHorizontalIntent = .reveal
    ) {
        if isHorizontalMasonryMode {
            switch horizontalIntent {
            case .reveal:
                ensureHorizontalMasonrySelectionVisible(selection, animated: false)
            case .alignWithAppShelf(let animated):
                ensureHorizontalMasonrySelectionVisible(selection, animated: animated)
            }
            return
        }
        guard
            let column = waterfallColumns.first(where: { $0.column.appGroupIndex == selection.appGroupIndex })
        else {
            return
        }
        if waterfallShouldKeepFilteredColumnsCentered {
            ensureWaterfallCardVisibleVertically(selection)
            return
        }
@@ -956,11 +1881,16 @@
    }
    private func ensureWaterfallCardVisibleVertically(_ selection: QuickSwitchSelection) {
        guard !isHorizontalMasonryMode else {
            ensureHorizontalMasonrySelectionVisible(selection, animated: false)
            return
        }
        let visibleHeight = max(
            1,
            waterfallLayer.bounds.height
                - WaterfallMetrics.headerHeight
                - WaterfallMetrics.columnPadding * 2
                - WaterfallMetrics.columnVerticalPadding * 2
        )
        let cardStride = WaterfallMetrics.cardHeight + WaterfallMetrics.cardGap
        let cardStart = CGFloat(selection.windowIndex) * cardStride
@@ -983,6 +1913,11 @@
    }
    private func alignWaterfallColumnWithAppShelfIcon(appGroupIndex: Int, animated: Bool) {
        if isHorizontalMasonryMode {
            ensureHorizontalMasonryAppVisible(appGroupIndex, animated: animated)
            return
        }
        guard
            let item = appShelfItems.first(where: { $0.item.appGroupIndex == appGroupIndex }),
            let column = waterfallColumns.first(where: { $0.column.appGroupIndex == appGroupIndex })
@@ -1009,6 +1944,11 @@
    }
    private func ensureWaterfallColumnVisible(_ appGroupIndex: Int) {
        if isHorizontalMasonryMode {
            ensureHorizontalMasonryAppVisible(appGroupIndex, animated: false)
            return
        }
        guard let column = waterfallColumns.first(where: { $0.column.appGroupIndex == appGroupIndex }) else {
            return
        }
@@ -1017,18 +1957,31 @@
    }
    private func ensureWaterfallColumnVisible(_ column: WaterfallColumnLayers) {
        if isHorizontalMasonryMode {
            ensureHorizontalMasonryAppVisible(column.column.appGroupIndex, animated: false)
            return
        }
        waterfallScrollOffset = clampedWaterfallOffset(
            scrollOffset(
                makingVisible: column.containerLayer.frame,
                currentOffset: waterfallScrollOffset,
                visibleWidth: waterfallLayer.bounds.width,
                maxOffset: waterfallMaxScrollOffset,
                inset: WaterfallMetrics.revealPadding
                inset: WaterfallMetrics.horizontalRevealPadding
            )
        )
    }
    private var waterfallAlignmentScrollRange: ClosedRange<CGFloat> {
        if isHorizontalMasonryMode {
            return 0...0
        }
        if waterfallShouldKeepFilteredColumnsCentered {
            return 0...0
        }
        var minimumOffset: CGFloat = 0
        var maximumOffset = waterfallMaxScrollOffset
@@ -1053,6 +2006,10 @@
    private func clampedWaterfallAlignmentOffset(_ offset: CGFloat) -> CGFloat {
        let range = waterfallAlignmentScrollRange
        return min(max(range.lowerBound, offset), range.upperBound)
    }
    private var waterfallShouldKeepFilteredColumnsCentered: Bool {
        currentViewModel?.lockedSpaceID != nil && waterfallMaxScrollOffset <= 0.5
    }
    private func setWaterfallScrollOffset(_ offset: CGFloat, animated: Bool) {
@@ -1087,6 +2044,80 @@
        animation.timingFunction = CAMediaTimingFunction(name: WaterfallAlignmentMetrics.animationTimingName)
        animation.isRemovedOnCompletion = true
        waterfallContentLayer.add(animation, forKey: "waterfallColumnAlignment")
    }
    private func ensureHorizontalMasonrySelectionVisible(
        _ selection: QuickSwitchSelection,
        animated: Bool
    ) {
        guard let card = waterfallCard(
            appGroupIndex: selection.appGroupIndex,
            windowIndex: selection.windowIndex
        ) else {
            return
        }
        let visibleFrame = card.containerLayer.frame.offsetBy(
            dx: 0,
            dy: waterfallContentLayer.frame.minY
        )
        let visibleHeight = waterfallLayer.bounds.height
        let padding = WaterfallMetrics.revealPadding
        var nextOffset = horizontalMasonryScrollOffset
        if visibleFrame.maxY > visibleHeight - padding {
            nextOffset -= visibleFrame.maxY - (visibleHeight - padding)
        } else if visibleFrame.minY < padding {
            nextOffset += padding - visibleFrame.minY
        }
        setHorizontalMasonryScrollOffset(nextOffset, animated: animated)
    }
    private func ensureHorizontalMasonryAppVisible(_ appGroupIndex: Int, animated: Bool) {
        guard let firstCard = waterfallColumns
            .first(where: { $0.column.appGroupIndex == appGroupIndex })?
            .cards
            .sorted(by: { $0.item.windowIndex < $1.item.windowIndex })
            .first
        else {
            return
        }
        let cardTopDistance = horizontalMasonryContentHeight - firstCard.containerLayer.frame.maxY
        setHorizontalMasonryScrollOffset(
            cardTopDistance - WaterfallMetrics.revealPadding,
            animated: animated
        )
    }
    private func setHorizontalMasonryScrollOffset(_ offset: CGFloat, animated: Bool) {
        let nextOffset = clampedHorizontalMasonryOffset(offset)
        let previousPositionY = waterfallContentLayer.presentation()?.position.y
            ?? waterfallContentLayer.position.y
        horizontalMasonryScrollOffset = nextOffset
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        waterfallContentLayer.frame = horizontalMasonryContentFrame()
        CATransaction.commit()
        let nextPositionY = waterfallContentLayer.position.y
        guard animated,
              !NSWorkspace.shared.accessibilityDisplayShouldReduceMotion,
              abs(previousPositionY - nextPositionY) > 0.5
        else {
            waterfallContentLayer.removeAnimation(forKey: "horizontalMasonryAlignment")
            return
        }
        let animation = CABasicAnimation(keyPath: "position.y")
        animation.fromValue = previousPositionY
        animation.toValue = nextPositionY
        animation.duration = WaterfallAlignmentMetrics.animationDuration
        animation.timingFunction = CAMediaTimingFunction(name: WaterfallAlignmentMetrics.animationTimingName)
        animation.isRemovedOnCompletion = true
        waterfallContentLayer.add(animation, forKey: "horizontalMasonryAlignment")
    }
    private func scrollOffset(
@@ -1162,35 +2193,54 @@
        value ?? NSNull()
    }
    private func setupBackdropBlurView() {
        backdropBlurView.material = .fullScreenUI
        backdropBlurView.blendingMode = .behindWindow
        backdropBlurView.state = .active
        backdropBlurView.isEmphasized = false
        backdropBlurView.frame = bounds
        backdropBlurView.autoresizingMask = [.width, .height]
        addSubview(backdropBlurView, positioned: .below, relativeTo: nil)
    }
    private func setupLayerHostView() {
        layerHostView.wantsLayer = true
        layerHostView.frame = bounds
        layerHostView.autoresizingMask = [.width, .height]
        layerHostView.layer?.masksToBounds = true
        layerHostView.layer?.backgroundColor = NSColor.clear.cgColor
        addSubview(layerHostView, positioned: .above, relativeTo: backdropBlurView)
    }
    private func setupLayers() {
        guard let rootLayer = layer else { return }
        guard let rootLayer = layerHostView.layer else { return }
        rootLayer.masksToBounds = true
        backgroundLayer.colors = [
            NSColor.windowBackgroundColor.withAlphaComponent(0.72).cgColor,
            NSColor.controlBackgroundColor.withAlphaComponent(0.62).cgColor
            NSColor.windowBackgroundColor.withAlphaComponent(0.58).cgColor,
            NSColor.controlBackgroundColor.withAlphaComponent(0.48).cgColor
        ]
        backgroundLayer.startPoint = CGPoint(x: 0.5, y: 1.0)
        backgroundLayer.endPoint = CGPoint(x: 0.5, y: 0.0)
        rootLayer.addSublayer(backgroundLayer)
        glassLayer.cornerRadius = 24
        glassLayer.backgroundColor = NSColor.windowBackgroundColor.withAlphaComponent(0.72).cgColor
        glassLayer.borderColor = NSColor.separatorColor.withAlphaComponent(0.35).cgColor
        glassLayer.borderWidth = 1
        glassLayer.shadowColor = NSColor.black.cgColor
        glassLayer.shadowOpacity = 0.16
        glassLayer.shadowRadius = 24
        glassLayer.shadowOffset = CGSize(width: 0, height: -8)
        safeAreaFillLayer.backgroundColor = NSColor.black.cgColor
        rootLayer.addSublayer(safeAreaFillLayer)
        glassLayer.cornerRadius = 0
        glassLayer.backgroundColor = NSColor.clear.cgColor
        glassLayer.borderColor = NSColor.clear.cgColor
        glassLayer.borderWidth = 0
        glassLayer.shadowOpacity = 0
        rootLayer.addSublayer(glassLayer)
        laneLayer.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.50).cgColor
        laneLayer.cornerRadius = 16
        laneLayer.backgroundColor = NSColor.clear.cgColor
        laneLayer.cornerRadius = 0
        laneLayer.masksToBounds = true
        glassLayer.addSublayer(laneLayer)
        laneLayer.addSublayer(spaceLaneContentLayer)
        shelfLayer.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.42).cgColor
        shelfLayer.backgroundColor = NSColor.clear.cgColor
        shelfLayer.cornerRadius = 16
        shelfLayer.masksToBounds = true
        glassLayer.addSublayer(shelfLayer)
@@ -1200,6 +2250,47 @@
        waterfallLayer.masksToBounds = true
        glassLayer.addSublayer(waterfallLayer)
        waterfallLayer.addSublayer(waterfallContentLayer)
        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)
        settingsButtonLayer.cornerRadius = 10
        settingsButtonLayer.masksToBounds = false
        settingsButtonLayer.borderWidth = 1
        settingsButtonLayer.shadowColor = NSColor.black.cgColor
        settingsButtonLayer.shadowOffset = CGSize(width: 0, height: -2)
        settingsButtonLayer.zPosition = 170
        settingsButtonIconLayer.contentsGravity = .resizeAspect
        settingsButtonIconLayer.contentsScale = backingScaleFactor
        settingsButtonIconLayer.contents = settingsIconImage()
        settingsButtonFallbackTextLayer.contentsScale = backingScaleFactor
        settingsButtonFallbackTextLayer.string = "⚙"
        settingsButtonFallbackTextLayer.font = NSFont.systemFont(ofSize: 18, weight: .regular)
        settingsButtonFallbackTextLayer.fontSize = 18
        settingsButtonFallbackTextLayer.alignmentMode = .center
        settingsButtonFallbackTextLayer.foregroundColor = NSColor.secondaryLabelColor.cgColor
        settingsButtonFallbackTextLayer.isHidden = settingsButtonIconLayer.contents != nil
        settingsButtonLayer.addSublayer(settingsButtonIconLayer)
        settingsButtonLayer.addSublayer(settingsButtonFallbackTextLayer)
        glassLayer.addSublayer(settingsButtonLayer)
    }
    private func rebuildAppShelfItems() {
@@ -1207,22 +2298,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
@@ -1251,17 +2335,14 @@
            badge.masksToBounds = true
            let selectedIndicator = CALayer()
            selectedIndicator.cornerRadius = 2
            selectedIndicator.backgroundColor = (item.appGroupIndex == selectedAppGroupIndex
                ? NSColor.controlAccentColor
                : NSColor.clear
            ).cgColor
            selectedIndicator.isHidden = true
            let closeButton = makeCloseButtonLayers()
            container.addSublayer(background)
            container.addSublayer(icon)
            container.addSublayer(label)
            container.addSublayer(badge)
            container.addSublayer(selectedIndicator)
            container.addSublayer(closeButton.containerLayer)
            appShelfContentLayer.addSublayer(container)
            return AppShelfItemLayers(
@@ -1271,7 +2352,8 @@
                iconLayer: icon,
                labelLayer: label,
                badgeLayer: badge,
                selectedIndicatorLayer: selectedIndicator
                selectedIndicatorLayer: selectedIndicator,
                closeButton: closeButton
            )
        }
    }
@@ -1287,18 +2369,19 @@
            container.name = column.app.name
            container.cornerRadius = 14
            container.masksToBounds = true
            container.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.34).cgColor
            container.borderColor = NSColor.separatorColor.withAlphaComponent(0.28).cgColor
            container.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.56).cgColor
            container.borderColor = NSColor.separatorColor.withAlphaComponent(0.34).cgColor
            container.borderWidth = 1
            let header = CALayer()
            header.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.58).cgColor
            header.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.68).cgColor
            let appName = makeTextLayer(
                string: column.app.name,
                fontSize: 13,
                weight: .semibold,
                color: .labelColor
                color: .labelColor,
                alignment: .center
            )
            let count = makeTextLayer(
                string: "\(column.windows.count)",
@@ -1324,6 +2407,9 @@
                thumbnail.masksToBounds = true
                thumbnail.contents = skeletonThumbnailProvider
                    .withOverlayTitle(window.window.title, for: window.window.app)
                let titleBar = CALayer()
                titleBar.borderWidth = 0
                let title = makeTextLayer(
                    string: window.window.title.isEmpty ? "Untitled Window" : window.window.title,
@@ -1361,24 +2447,29 @@
                shine.startPoint = CGPoint(x: 0, y: 1)
                shine.endPoint = CGPoint(x: 1, y: 0)
                shine.opacity = 0
                let closeButton = makeCloseButtonLayers()
                card.addSublayer(thumbnail)
                card.addSublayer(titleBar)
                card.addSublayer(appIcon)
                card.addSublayer(title)
                card.addSublayer(meta)
                card.addSublayer(state)
                card.addSublayer(shine)
                card.addSublayer(closeButton.containerLayer)
                container.addSublayer(card)
                return WaterfallCardLayers(
                    item: window,
                    containerLayer: card,
                    titleBarLayer: titleBar,
                    titleLayer: title,
                    metaLayer: meta,
                    thumbnailLayer: thumbnail,
                    appIconLayer: appIcon,
                    stateLayer: state,
                    shineLayer: shine
                    shineLayer: shine,
                    closeButton: closeButton
                )
            }
@@ -1399,63 +2490,108 @@
    }
    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.appNames),
                fontSize: 12,
                weight: .regular,
                color: .secondaryLabelColor
            )
            let windowBlocks = 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 splitAppLayers = splitViewAppNames(for: space).map { name in
                    makeTextLayer(
                        string: name,
                        fontSize: 12,
                        weight: .medium,
                        color: .labelColor,
                        alignment: .center
                    )
                }
                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 splitAppLayer in splitAppLayers {
                    container.addSublayer(splitAppLayer)
                }
                for block in windowBlocks {
                    container.addSublayer(block)
                }
                groupContainer.addSublayer(container)
                return SpaceLaneSegmentLayers(
                    space: space,
                    containerLayer: container,
                    labelLayer: labelLayer,
                    countLayer: countLayer,
                    appLayer: appLayer,
                    splitAppLayers: splitAppLayers,
                    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() {
@@ -1464,40 +2600,85 @@
        backgroundLayer.frame = bounds
        let horizontalInset = max(36, bounds.width * 0.045)
        let topInset = max(36, bounds.height * 0.055)
        glassLayer.frame = bounds.insetBy(dx: horizontalInset, dy: topInset)
        let safeAreaTopInset = topSafeAreaInset
        safeAreaFillLayer.frame = CGRect(
            x: 0,
            y: max(0, bounds.height - safeAreaTopInset),
            width: bounds.width,
            height: safeAreaTopInset
        )
        safeAreaFillLayer.isHidden = safeAreaTopInset <= 0.5
        glassLayer.frame = bounds
        let contentBounds = glassLayer.bounds
        let horizontalMargin: CGFloat = 22
        let horizontalMargin = RootLayoutMetrics.horizontalMargin
        let topMargin = safeAreaTopInset + RootLayoutMetrics.topContentMargin
        let laneHeight = min(104, max(82, contentBounds.height * 0.12))
        let availableContentWidth = contentBounds.width - horizontalMargin * 2
        appShelfLayout = appShelfLayout(for: availableContentWidth)
        let shelfHeight = appShelfHeight(forRows: appShelfLayout.rows)
        laneLayer.frame = CGRect(
            x: horizontalMargin,
            y: contentBounds.height - horizontalMargin - laneHeight,
            y: contentBounds.height - topMargin - laneHeight,
            width: availableContentWidth,
            height: laneHeight
        )
        shelfLayer.frame = CGRect(
            x: horizontalMargin,
            y: laneLayer.frame.minY - 16 - shelfHeight,
            y: laneLayer.frame.minY - RootLayoutMetrics.shelfGap - shelfHeight,
            width: laneLayer.frame.width,
            height: shelfHeight
        )
        waterfallLayer.frame = CGRect(
            x: shelfLayer.frame.minX,
            y: 34,
            y: RootLayoutMetrics.bottomMargin,
            width: shelfLayer.frame.width,
            height: shelfLayer.frame.minY - 48
            height: max(1, shelfLayer.frame.minY - RootLayoutMetrics.bottomMargin - RootLayoutMetrics.waterfallTopGap)
        )
        layoutSpaceLaneSegments()
        layoutAppShelfItems()
        layoutWaterfallColumns()
        layoutCloseConfirmation()
        layoutCloseFeedback()
        layoutSettingsButton()
        CATransaction.commit()
    }
    private func layoutSettingsButton() {
        let size: CGFloat = 34
        let topMargin = topSafeAreaInset + RootLayoutMetrics.topContentMargin
        let x = max(
            RootLayoutMetrics.horizontalMargin,
            glassLayer.bounds.width - RootLayoutMetrics.horizontalMargin - size
        )
        let y = max(
            RootLayoutMetrics.bottomMargin,
            glassLayer.bounds.height - topMargin - size
        )
        settingsButtonLayer.frame = CGRect(x: x, y: y, width: size, height: size)
        settingsButtonLayer.backgroundColor = NSColor.windowBackgroundColor
            .withAlphaComponent(isSettingsButtonHovered ? 0.78 : 0.54)
            .cgColor
        settingsButtonLayer.borderColor = NSColor.separatorColor
            .withAlphaComponent(isSettingsButtonHovered ? 0.42 : 0.26)
            .cgColor
        settingsButtonLayer.shadowOpacity = isSettingsButtonHovered ? 0.14 : 0.08
        settingsButtonLayer.shadowRadius = isSettingsButtonHovered ? 9 : 6
        settingsButtonIconLayer.contentsScale = backingScaleFactor
        settingsButtonFallbackTextLayer.contentsScale = backingScaleFactor
        let iconInset: CGFloat = 8
        settingsButtonIconLayer.frame = settingsButtonLayer.bounds.insetBy(dx: iconInset, dy: iconInset)
        settingsButtonFallbackTextLayer.frame = settingsButtonLayer.bounds.insetBy(dx: 0, dy: 6)
        settingsButtonFallbackTextLayer.foregroundColor = (isSettingsButtonHovered
            ? NSColor.labelColor
            : NSColor.secondaryLabelColor
        ).cgColor
    }
    private var topSafeAreaInset: CGFloat {
        max(0, window?.screen?.safeAreaInsets.top ?? safeAreaInsets.top)
    }
    private func appShelfLayout(for availableWidth: CGFloat) -> AppShelfLayout {
@@ -1575,15 +2756,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(
@@ -1593,13 +2793,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
        }
    }
@@ -1611,32 +2833,127 @@
        min(max(0, offset), spaceLaneMaxScrollOffset)
    }
    private func applySpaceLaneVisualState(_ segment: SpaceLaneSegmentLayers) {
        let isHovered = segment.space.id == hoveredSpaceID
        let isFocused = segment.space.id == activeSpaceFocusID
        let isCurrent = segment.space.isCurrent
        let isActive = isHovered || isFocused
    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
                || segment.space.id == currentViewModel?.lockedSpaceID
        }
        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
    }
        segment.containerLayer.borderWidth = isCurrent ? 1.5 : 1
        segment.containerLayer.borderColor = (isCurrent
            ? NSColor.labelColor.withAlphaComponent(0.34)
    private func applySpaceLaneVisualState(_ segment: SpaceLaneSegmentLayers) {
        let isHovered = segment.space.id == activeSpaceFocusID
        let isFocused = segment.space.id == activeSpaceFocusID
        let isLocked = segment.space.id == currentViewModel?.lockedSpaceID
        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 || isLocked
        let isAssociated = (isAppAssociated || isWindowAssociated) && !isActive
        segment.containerLayer.borderWidth = (hasWindows || isCurrent || isActive || isAssociated) ? 1.5 : 1
        segment.containerLayer.borderColor = (isLocked
            ? NSColor.white.withAlphaComponent(0.86)
            : isActive
                ? NSColor.labelColor.withAlphaComponent(0.24)
            ? spaceLaneOccupiedColor.withAlphaComponent(isLocked ? 0.92 : 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
            ? NSColor.windowBackgroundColor.withAlphaComponent(0.76)
            : spaceColor(for: segment.space)
        ).cgColor
        segment.containerLayer.shadowOpacity = isCurrent ? 0.12 : isActive ? 0.08 : 0
        segment.containerLayer.shadowRadius = isCurrent ? 10 : isActive ? 7 : 0
        segment.containerLayer.shadowOffset = CGSize(width: 0, height: isCurrent ? -2 : -1)
        segment.containerLayer.shadowPath = CGPath(
            roundedRect: segment.containerLayer.bounds,
            cornerWidth: segment.containerLayer.cornerRadius,
            cornerHeight: segment.containerLayer.cornerRadius,
            transform: nil
        if isLocked {
            applySpaceLaneSolidFill(
                to: segment.containerLayer,
                color: spaceLaneLockedFillColor
            )
        } else 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.shadowColor = isLocked
            ? NSColor.white.withAlphaComponent(0.98).cgColor
            : NSColor.black.cgColor
        segment.containerLayer.shadowOpacity = (hasWindows || isLocked)
            ? (isLocked ? 0.72 : isActive ? 0.26 : isAssociated ? 0.18 : isCurrent ? 0.14 : 0)
            : 0
        segment.containerLayer.shadowRadius = (hasWindows || isLocked)
            ? (isLocked ? 30 : isActive ? 24 : isAssociated ? 18 : isCurrent ? 12 : 0)
            : 0
        segment.containerLayer.shadowOffset = CGSize(
            width: 0,
            height: (hasWindows || isLocked) ? (isLocked ? 0 : isActive ? -6 : isAssociated ? -4 : isCurrent ? -2.5 : 0) : 0
        )
        segment.containerLayer.zPosition = isLocked ? 42 : 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 || isLocked)
            ? CGPath(
                roundedRect: segment.containerLayer.bounds,
                cornerWidth: segment.containerLayer.cornerRadius,
                cornerHeight: segment.containerLayer.cornerRadius,
                transform: nil
            )
            : nil
        applySpaceLaneContentVisualState(segment, isLocked: isLocked)
    }
    private func applySpaceLaneContentVisualState(_ segment: SpaceLaneSegmentLayers, isLocked: Bool) {
        let primaryTextColor: NSColor = isLocked ? .white : .labelColor
        let secondaryTextColor: NSColor = isLocked
            ? .white
            : .secondaryLabelColor
        segment.labelLayer.foregroundColor = primaryTextColor.cgColor
        segment.countLayer.foregroundColor = (isLocked ? NSColor.white : .secondaryLabelColor).cgColor
        segment.appLayer.foregroundColor = (segment.space.type == .fullscreen
            ? primaryTextColor
            : secondaryTextColor
        ).cgColor
        for splitAppLayer in segment.splitAppLayers {
            splitAppLayer.foregroundColor = primaryTextColor.cgColor
        }
        for block in segment.windowBlocks {
            block.backgroundColor = (isLocked
                ? NSColor.white
                : spaceLaneOccupiedColor.withAlphaComponent(segment.space.isCurrent ? 0.72 : 0.58)
            ).cgColor
        }
        (segment.fullscreenMarkerLayer as? CAShapeLayer)?.strokeColor = (isLocked
            ? NSColor.white
            : NSColor.secondaryLabelColor.withAlphaComponent(segment.space.isCurrent ? 0.42 : 0.28)
        ).cgColor
    }
    private func shouldScrollSpaceLane(for event: NSEvent) -> Bool {
@@ -1677,18 +2994,56 @@
    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? {
        spaceLaneSegments.first { $0.space.id == spaceID }
    }
    private func settingsButtonContains(windowLocation: CGPoint) -> Bool {
        let locationInView = convert(windowLocation, from: nil)
        let locationInGlass = CGPoint(
            x: locationInView.x - glassLayer.frame.minX,
            y: locationInView.y - glassLayer.frame.minY
        )
        return settingsButtonLayer.frame.contains(locationInGlass)
    }
    private func clickSettingsButton(source: String) {
        DevelopmentDiagnostics.log("quickSwitch.view.clickSettings", [
            "source": source
        ])
        lastMouseCommand = "click-settings"
        if !mouseCommandsApplied.contains("click-settings") {
            mouseCommandsApplied.append("click-settings")
        }
        onOpenSettings?()
    }
    private func appShelfItem(at windowLocation: CGPoint) -> AppShelfItemLayers? {
@@ -1734,8 +3089,48 @@
        let bounds = segment.containerLayer.bounds
        segment.labelLayer.frame = CGRect(x: 10, y: bounds.height - 29, width: max(1, bounds.width - 48), height: 20)
        segment.countLayer.frame = CGRect(x: bounds.width - 34, y: bounds.height - 29, width: 24, height: 20)
        segment.appLayer.frame = CGRect(x: 10, y: 9, width: max(1, bounds.width - 20), height: 17)
        segment.fullscreenMarkerLayer?.frame = CGRect(x: 10, y: bounds.height - 7, width: max(1, bounds.width - 20), height: 3)
        if segment.space.type == .fullscreen {
            let markerFrame = fullscreenMarkerFrame(in: bounds)
            let splitAppNames = splitViewAppNames(for: segment.space)
            segment.appLayer.isHidden = splitAppNames.count >= 2
            if splitAppNames.count >= 2 {
                let labelY = markerFrame.midY - 8.5
                let halfWidth = markerFrame.width / 2
                for (index, splitAppLayer) in segment.splitAppLayers.enumerated() {
                    let x = markerFrame.minX + CGFloat(index) * halfWidth
                    splitAppLayer.isHidden = false
                    splitAppLayer.frame = CGRect(
                        x: x + 4,
                        y: labelY,
                        width: max(1, halfWidth - 8),
                        height: 17
                    )
                }
            } else {
                segment.appLayer.frame = CGRect(
                    x: 8,
                    y: markerFrame.midY - 8.5,
                    width: max(1, bounds.width - 16),
                    height: 17
                )
                for splitAppLayer in segment.splitAppLayers {
                    splitAppLayer.isHidden = true
                }
            }
        } else {
            segment.appLayer.isHidden = false
            segment.appLayer.frame = CGRect(x: 10, y: 9, width: max(1, bounds.width - 20), height: 17)
            for splitAppLayer in segment.splitAppLayers {
                splitAppLayer.isHidden = true
            }
        }
        if let fullscreenMarkerLayer = segment.fullscreenMarkerLayer {
            layoutFullscreenMarkerLayer(
                fullscreenMarkerLayer,
                in: bounds,
                isSplitView: splitViewAppNames(for: segment.space).count >= 2
            )
        }
        let blockGap: CGFloat = 5
        let blockWidth = max(12, min(22, (bounds.width - 20 - CGFloat(max(0, segment.windowBlocks.count - 1)) * blockGap) / CGFloat(max(1, segment.windowBlocks.count))))
@@ -1796,37 +3191,59 @@
        let bounds = item.containerLayer.bounds
        let isSelected = item.item.appGroupIndex == effectiveSelection?.appGroupIndex
        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, selected: isSelected, hovered: isHovered)
        let backgroundPadding: CGFloat = (isSelected || isHovered) ? 4 : 2
        let visualIconSize = appShelfVisualIconSize(
            forBaseIconSize: iconSize,
            selected: showsSelectedVisual,
            hovered: isHovered,
            spaceFocused: isSpaceFocused
        )
        let backgroundPadding: CGFloat = (showsSelectedVisual || isHovered)
            ? 4
            : isSpaceFocused
                ? 6
                : 2
        let iconX = (bounds.width - visualIconSize) / 2
        let iconY = bounds.height - visualIconSize - 2
        let badgeText = appShelfBadgeText(for: item)
        let badgeWidth = max(18, CGFloat(badgeText.count) * 7 + 8)
        item.containerLayer.zPosition = appShelfZPosition(selected: isSelected, hovered: isHovered)
        item.containerLayer.zPosition = appShelfZPosition(
            selected: showsSelectedVisual,
            hovered: isHovered,
            spaceFocused: isSpaceFocused
        )
        item.backgroundLayer.backgroundColor = appShelfBackgroundColor(
            selected: isSelected,
            selected: showsSelectedVisual,
            hovered: isHovered,
            spaceFocused: isSpaceFocused
        ).cgColor
        item.backgroundLayer.borderColor = (isSelected
            ? NSColor.separatorColor.withAlphaComponent(0.38)
        item.backgroundLayer.borderColor = (showsSelectedVisual
            ? NSColor.controlAccentColor.withAlphaComponent(0.40)
            : isHovered
                ? NSColor.separatorColor.withAlphaComponent(0.28)
                ? NSColor.controlAccentColor.withAlphaComponent(0.36)
                : NSColor.clear
        ).cgColor
        item.backgroundLayer.borderWidth = (isSelected || isHovered) ? 1 : 0
        item.backgroundLayer.borderWidth = (showsSelectedVisual || isHovered) ? 1 : 0
        item.backgroundLayer.shadowColor = NSColor.black.cgColor
        item.backgroundLayer.shadowOpacity = isSelected ? 0.11 : isHovered ? 0.06 : 0
        item.backgroundLayer.shadowRadius = isSelected ? 10 : isHovered ? 7 : 0
        item.backgroundLayer.shadowOffset = CGSize(width: 0, height: -2)
        item.backgroundLayer.shadowOpacity = showsSelectedVisual ? 0.11 : isHovered ? 0.07 : isSpaceFocused ? 0.18 : 0
        item.backgroundLayer.shadowRadius = showsSelectedVisual ? 10 : isHovered ? 8 : isSpaceFocused ? 18 : 0
        item.backgroundLayer.shadowOffset = CGSize(width: 0, height: isSpaceFocused ? -4 : -2)
        item.backgroundLayer.frame = CGRect(
            x: iconX - backgroundPadding,
            y: iconY - backgroundPadding,
            width: visualIconSize + backgroundPadding * 2,
            height: visualIconSize + backgroundPadding * 2
        )
        item.backgroundLayer.shadowPath = (showsSelectedVisual || isHovered || isSpaceFocused)
            ? CGPath(
                roundedRect: item.backgroundLayer.bounds,
                cornerWidth: item.backgroundLayer.cornerRadius,
                cornerHeight: item.backgroundLayer.cornerRadius,
                transform: nil
            )
            : nil
        item.iconLayer.frame = CGRect(x: iconX, y: iconY, width: visualIconSize, height: visualIconSize)
        item.labelLayer.contentsScale = backingScaleFactor
        item.labelLayer.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 14)
@@ -1837,21 +3254,22 @@
            : NSColor.controlAccentColor.withAlphaComponent(0.90)
        ).cgColor
        item.badgeLayer.frame = CGRect(
            x: iconX + visualIconSize - badgeWidth + 3,
            y: iconY + visualIconSize - 14,
            x: iconX - 3,
            y: iconY - 2,
            width: badgeWidth,
            height: 16
        )
        item.selectedIndicatorLayer.frame = CGRect(
            x: bounds.midX - 8,
            y: 0,
            width: 16,
            height: 4
        layoutCloseButton(
            item.closeButton,
            target: .app(appGroupIndex: item.item.appGroupIndex),
            visible: isHovered && canClose(app: item.item.app),
            anchorTopRight: CGPoint(
                x: min(iconX + visualIconSize - 6, bounds.width - 12),
                y: min(iconY + visualIconSize - 6, bounds.height - 12)
            )
        )
        item.selectedIndicatorLayer.backgroundColor = (isSelected
            ? NSColor.controlAccentColor
            : NSColor.clear
        ).cgColor
        item.selectedIndicatorLayer.frame = .zero
        item.selectedIndicatorLayer.backgroundColor = NSColor.clear.cgColor
    }
    private func appShelfRows() -> [[AppShelfItemLayers]] {
@@ -1877,7 +3295,52 @@
    }
    private var activeSpaceFocusID: UInt64? {
        focusedSpaceID ?? 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, isHovered: Bool) -> Bool {
        isSelected && isHovered && !isSpaceAssociationActive
    }
    private func spaceFocusedAppGroupIndexes() -> [Int] {
@@ -1893,6 +3356,18 @@
            }
            .map(\.column.appGroupIndex)
            .sorted()
    }
    private func firstWaterfallCard(inSpaceID spaceID: UInt64) -> WaterfallCardLayers? {
        for column in waterfallColumns.sorted(by: { $0.column.appGroupIndex < $1.column.appGroupIndex }) {
            if let card = column.cards
                .sorted(by: { $0.item.windowIndex < $1.item.windowIndex })
                .first(where: { $0.item.primarySpaceID == spaceID }) {
                return card
            }
        }
        return nil
    }
    private func spaceFocusWindowCount(for appGroupIndex: Int) -> Int {
@@ -1912,6 +3387,10 @@
            return "none"
        }
        guard !isHorizontalMasonryMode else {
            return "visible"
        }
        let visibleMinX = waterfallScrollOffset
        let visibleMaxX = waterfallScrollOffset + waterfallLayer.bounds.width
        if column.containerLayer.frame.maxX < visibleMinX + WaterfallMetrics.gap {
@@ -1923,33 +3402,45 @@
        return "visible"
    }
    private func appShelfVisualIconSize(forBaseIconSize iconSize: CGFloat, selected: Bool, hovered: Bool) -> CGFloat {
        guard selected || hovered else { return iconSize }
        return AppShelfMetrics.selectedIconSize
    private func appShelfVisualIconSize(
        forBaseIconSize iconSize: CGFloat,
        selected: Bool,
        hovered: Bool,
        spaceFocused: Bool
    ) -> CGFloat {
        if selected || hovered {
            return AppShelfMetrics.selectedIconSize
        }
        if spaceFocused {
            return min(AppShelfMetrics.selectedIconSize, iconSize + 8)
        }
        return iconSize
    }
    private func appShelfZPosition(selected: Bool, hovered: Bool) -> CGFloat {
        switch (selected, hovered) {
        case (true, true):
    private func appShelfZPosition(selected: Bool, hovered: Bool, spaceFocused: Bool) -> CGFloat {
        switch (selected, hovered, spaceFocused) {
        case (true, true, _):
            return 30
        case (true, false):
            return 20
        case (false, true):
            return 10
        case (false, false):
        case (true, false, _):
            return 24
        case (false, true, _):
            return 18
        case (false, false, true):
            return 12
        case (false, false, false):
            return 0
        }
    }
    private func appShelfBackgroundColor(selected: Bool, hovered: Bool, spaceFocused: Bool) -> NSColor {
        if selected {
            return NSColor.controlAccentColor.withAlphaComponent(0.12)
            return NSColor.controlAccentColor.withAlphaComponent(0.14)
        }
        if hovered {
            return NSColor.systemFill.withAlphaComponent(0.12)
            return NSColor.controlAccentColor.withAlphaComponent(0.13)
        }
        if spaceFocused {
            return NSColor.keyboardFocusIndicatorColor.withAlphaComponent(0.10)
            return NSColor.controlAccentColor.withAlphaComponent(0.14)
        }
        return .clear
    }
@@ -1982,17 +3473,24 @@
    }
    private func layoutWaterfallColumns() {
        if isHorizontalMasonryMode {
            layoutHorizontalMasonryWaterfall()
            return
        }
        guard !waterfallColumns.isEmpty else {
            waterfallContentWidth = waterfallLayer.bounds.width
            waterfallScrollOffset = 0
            horizontalMasonryContentHeight = waterfallLayer.bounds.height
            horizontalMasonryScrollOffset = 0
            waterfallContentLayer.frame = waterfallLayer.bounds
            return
        }
        let columnWidth = waterfallColumnWidth()
        let totalWidth = WaterfallMetrics.columnPadding * 2
            + CGFloat(waterfallColumns.count) * columnWidth
        let columnsWidth = CGFloat(waterfallColumns.count) * columnWidth
            + CGFloat(max(0, waterfallColumns.count - 1)) * WaterfallMetrics.gap
        let totalWidth = WaterfallMetrics.contentHorizontalPadding * 2 + columnsWidth
        waterfallContentWidth = max(waterfallLayer.bounds.width, totalWidth)
        waterfallScrollOffset = clampedWaterfallAlignmentOffset(waterfallScrollOffset)
        waterfallContentLayer.frame = CGRect(
@@ -2002,7 +3500,7 @@
            height: waterfallLayer.bounds.height
        )
        var x = WaterfallMetrics.columnPadding
        var x = waterfallColumnStartX(columnsWidth: columnsWidth)
        for column in waterfallColumns {
            column.containerLayer.frame = CGRect(
                x: x,
@@ -2015,9 +3513,177 @@
        }
    }
    private func layoutHorizontalMasonryWaterfall() {
        guard !waterfallColumns.isEmpty else {
            waterfallContentWidth = waterfallLayer.bounds.width
            waterfallScrollOffset = 0
            horizontalMasonryContentHeight = waterfallLayer.bounds.height
            horizontalMasonryScrollOffset = 0
            waterfallContentLayer.frame = waterfallLayer.bounds
            return
        }
        let layout = horizontalMasonryLayout()
        waterfallContentWidth = waterfallLayer.bounds.width
        waterfallScrollOffset = 0
        horizontalMasonryContentHeight = layout.contentHeight
        horizontalMasonryScrollOffset = clampedHorizontalMasonryOffset(horizontalMasonryScrollOffset)
        waterfallContentLayer.frame = horizontalMasonryContentFrame()
        for column in waterfallColumns {
            column.containerLayer.frame = CGRect(
                x: 0,
                y: 0,
                width: waterfallContentWidth,
                height: horizontalMasonryContentHeight
            )
            column.containerLayer.masksToBounds = false
            column.containerLayer.backgroundColor = NSColor.clear.cgColor
            column.containerLayer.borderColor = NSColor.clear.cgColor
            column.containerLayer.borderWidth = 0
            column.containerLayer.shadowOpacity = 0
            column.containerLayer.shadowRadius = 0
            column.containerLayer.shadowOffset = .zero
            column.headerLayer.isHidden = true
            column.appNameLayer.isHidden = true
            column.countLayer.isHidden = true
            for card in column.cards {
                if let frame = layout.cardFramesByWindowID[card.item.window.id] {
                    card.containerLayer.frame = frame
                } else {
                    card.containerLayer.frame = .zero
                }
                layoutWaterfallCard(card)
            }
        }
    }
    private struct HorizontalMasonryLayout {
        let contentHeight: CGFloat
        let cardFramesByWindowID: [UInt32: CGRect]
    }
    private func horizontalMasonryLayout() -> HorizontalMasonryLayout {
        let cards = horizontalMasonryCardsInPlacementOrder()
        guard !cards.isEmpty else {
            return HorizontalMasonryLayout(
                contentHeight: waterfallLayer.bounds.height,
                cardFramesByWindowID: [:]
            )
        }
        let viewportWidth = max(1, waterfallLayer.bounds.width)
        let availableWidth = max(
            HorizontalMasonryMetrics.cardMinWidth,
            viewportWidth - HorizontalMasonryMetrics.horizontalPadding * 2
        )
        let maximumLaneCount = max(
            1,
            Int(
                floor(
                    (availableWidth + HorizontalMasonryMetrics.horizontalGap)
                        / (HorizontalMasonryMetrics.cardMinWidth + HorizontalMasonryMetrics.horizontalGap)
                )
            )
        )
        let laneCount = min(maximumLaneCount, max(1, cards.count))
        let fittingWidth = (
            availableWidth - CGFloat(max(0, laneCount - 1)) * HorizontalMasonryMetrics.horizontalGap
        ) / CGFloat(laneCount)
        let cardWidth = floor(
            min(
                HorizontalMasonryMetrics.cardMaxWidth,
                max(HorizontalMasonryMetrics.cardMinWidth, fittingWidth)
            )
        )
        let usedWidth = CGFloat(laneCount) * cardWidth
            + CGFloat(max(0, laneCount - 1)) * HorizontalMasonryMetrics.horizontalGap
        let startX = max(
            HorizontalMasonryMetrics.horizontalPadding,
            (viewportWidth - usedWidth) / 2
        )
        var laneHeights = Array(repeating: CGFloat(0), count: laneCount)
        var plannedFrames: [(windowID: UInt32, x: CGFloat, top: CGFloat, width: CGFloat, height: CGFloat)] = []
        for card in cards {
            let laneIndex = laneHeights.enumerated().min { lhs, rhs in
                if abs(lhs.element - rhs.element) > 0.5 {
                    return lhs.element < rhs.element
                }
                return lhs.offset < rhs.offset
            }?.offset ?? 0
            let cardHeight = HorizontalMasonryMetrics.cardHeight
            let x = startX + CGFloat(laneIndex) * (cardWidth + HorizontalMasonryMetrics.horizontalGap)
            let top = HorizontalMasonryMetrics.verticalPadding + laneHeights[laneIndex]
            plannedFrames.append((
                windowID: card.item.window.id,
                x: x,
                top: top,
                width: cardWidth,
                height: cardHeight
            ))
            laneHeights[laneIndex] += cardHeight + HorizontalMasonryMetrics.verticalGap
        }
        let tallestLane = laneHeights.max().map {
            max(0, $0 - HorizontalMasonryMetrics.verticalGap)
        } ?? 0
        let contentHeight = max(
            waterfallLayer.bounds.height,
            HorizontalMasonryMetrics.verticalPadding * 2 + tallestLane
        )
        let frames = Dictionary(
            uniqueKeysWithValues: plannedFrames.map { planned in
                (
                    planned.windowID,
                    CGRect(
                        x: planned.x,
                        y: contentHeight - planned.top - planned.height,
                        width: planned.width,
                        height: planned.height
                    )
                )
            }
        )
        return HorizontalMasonryLayout(contentHeight: contentHeight, cardFramesByWindowID: frames)
    }
    private func horizontalMasonryContentFrame() -> CGRect {
        let contentHeight = max(waterfallLayer.bounds.height, horizontalMasonryContentHeight)
        return CGRect(
            x: 0,
            y: waterfallLayer.bounds.height - contentHeight + horizontalMasonryScrollOffset,
            width: waterfallLayer.bounds.width,
            height: contentHeight
        )
    }
    private func waterfallColumnStartX(columnsWidth: CGFloat) -> CGFloat {
        guard currentViewModel?.lockedSpaceID != nil else {
            return WaterfallMetrics.contentHorizontalPadding
        }
        let centeredStart = (waterfallContentWidth - columnsWidth) / 2
        return max(WaterfallMetrics.contentHorizontalPadding, centeredStart)
    }
    private func layoutWaterfallColumn(_ column: WaterfallColumnLayers) {
        column.containerLayer.cornerRadius = 14
        column.containerLayer.masksToBounds = true
        column.containerLayer.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.56).cgColor
        column.containerLayer.shadowOpacity = 0
        column.containerLayer.shadowRadius = 0
        column.containerLayer.shadowOffset = .zero
        column.headerLayer.isHidden = false
        column.appNameLayer.isHidden = false
        column.countLayer.isHidden = false
        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)
@@ -2034,11 +3700,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,
@@ -2053,10 +3723,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 {
@@ -2065,7 +3735,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
@@ -2078,33 +3748,63 @@
        let bounds = card.containerLayer.bounds
        let isSelected = card.item.window.id == effectiveSelection?.windowID
        let isHovered = card.item.window.id == hoveredWindowID
        applyWaterfallCardVisualState(card, selected: isSelected, hovered: isHovered)
        let thumbnailWidth = max(90, min(116, bounds.width * 0.44))
        card.thumbnailLayer.frame = CGRect(
            x: 10,
            y: 12,
            width: thumbnailWidth,
            height: bounds.height - 24
        applyWaterfallCardVisualState(
            card,
            selected: shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered),
            hovered: isHovered
        )
        card.appIconLayer.frame = CGRect(x: 18, y: bounds.height - 38, width: 22, height: 22)
        let outerInset: CGFloat = 0
        let titleBarHeight: CGFloat = 26
        let titleGap: CGFloat = 6
        let titleBarFrame = CGRect(
            x: outerInset,
            y: bounds.height - outerInset - titleBarHeight,
            width: max(1, bounds.width - outerInset * 2),
            height: titleBarHeight
        )
        let thumbnailFrame = CGRect(
            x: outerInset,
            y: outerInset,
            width: max(1, bounds.width - outerInset * 2),
            height: max(1, titleBarFrame.minY - outerInset - titleGap)
        )
        card.thumbnailLayer.frame = CGRect(
            x: thumbnailFrame.minX,
            y: thumbnailFrame.minY,
            width: thumbnailFrame.width,
            height: thumbnailFrame.height
        )
        card.titleBarLayer.frame = titleBarFrame
        card.appIconLayer.frame = CGRect(x: titleBarFrame.minX + 7, y: titleBarFrame.minY + 5, width: 16, height: 16)
        card.titleLayer.frame = CGRect(
            x: thumbnailWidth + 22,
            y: bounds.height - 31,
            width: max(1, bounds.width - thumbnailWidth - 34),
            x: titleBarFrame.minX + 29,
            y: titleBarFrame.minY + 6,
            width: max(1, titleBarFrame.width - 68),
            height: 16
        )
        card.metaLayer.frame = CGRect(
            x: thumbnailWidth + 22,
            y: bounds.height - 51,
            width: max(1, bounds.width - thumbnailWidth - 34),
            x: titleBarFrame.maxX - 38,
            y: titleBarFrame.minY + 6,
            width: 30,
            height: 14
        )
        card.stateLayer.frame = CGRect(
            x: thumbnailWidth + 22,
            y: 12,
            width: max(1, bounds.width - thumbnailWidth - 34),
            x: thumbnailFrame.maxX - 86,
            y: thumbnailFrame.minY + 6,
            width: 78,
            height: 14
        )
        layoutCloseButton(
            card.closeButton,
            target: .window(
                appGroupIndex: card.item.appGroupIndex,
                windowIndex: card.item.windowIndex,
                windowID: card.item.window.id
            ),
            visible: isHovered,
            anchorTopRight: CGPoint(x: bounds.maxX - 12, y: bounds.maxY - 12)
        )
        card.shineLayer.frame = CGRect(
            x: -bounds.width * 0.18,
@@ -2115,34 +3815,575 @@
    }
    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
                ? NSColor.systemFill.withAlphaComponent(0.13)
                ? accentTintedWindowBackground(fraction: 0.09, alpha: 0.95)
                : isSpaceFocused
                    ? NSColor.windowBackgroundColor.withAlphaComponent(0.88)
                    ? accentTintedWindowBackground(fraction: 0.10, alpha: 0.94)
                : NSColor.windowBackgroundColor.withAlphaComponent(0.82)
        ).cgColor
        card.containerLayer.borderColor = (selected
            ? NSColor.controlAccentColor.withAlphaComponent(0.52)
            : hovered
                ? NSColor.separatorColor.withAlphaComponent(0.46)
                ? NSColor.controlAccentColor.withAlphaComponent(0.44)
                : isSpaceFocused
                    ? NSColor.separatorColor.withAlphaComponent(0.38)
                    ? NSColor.controlAccentColor.withAlphaComponent(0.42)
                : NSColor.separatorColor.withAlphaComponent(0.28)
        ).cgColor
        card.containerLayer.borderWidth = selected ? 1.5 : 1
        card.containerLayer.borderWidth = (selected || hovered || isSpaceFocused) ? 1.5 : 1
        card.containerLayer.shadowColor = NSColor.black.cgColor
        card.containerLayer.shadowOpacity = selected ? 0.20 : hovered ? 0.08 : 0
        card.containerLayer.shadowRadius = selected ? 18 : hovered ? 10 : 0
        card.containerLayer.shadowOpacity = selected ? 0.24 : hovered ? 0.14 : isSpaceFocused ? 0.20 : 0
        card.containerLayer.shadowRadius = selected ? 22 : hovered ? 17 : isSpaceFocused ? 24 : 0
        card.containerLayer.shadowOffset = selected
            ? CGSize(width: 0, height: -5)
            ? CGSize(width: 0, height: -6)
            : hovered
                ? CGSize(width: 0, height: -2)
                : .zero
        card.containerLayer.zPosition = selected ? 20 : hovered ? 10 : 0
                ? CGSize(width: 0, height: -2.5)
                : isSpaceFocused
                    ? CGSize(width: 0, height: -5)
                    : .zero
        card.containerLayer.zPosition = selected ? 20 : hovered ? 10 : isSpaceFocused ? 8 : 0
        let focusedScale: CGFloat = selected ? 1.015 : isSpaceFocused ? 1.012 : 1.008
        card.containerLayer.transform = (selected || hovered || isSpaceFocused)
            ? CATransform3DMakeScale(focusedScale, focusedScale, 1)
            : CATransform3DIdentity
        card.containerLayer.shadowPath = (selected || hovered || isSpaceFocused)
            ? CGPath(
                roundedRect: card.containerLayer.bounds,
                cornerWidth: card.containerLayer.cornerRadius,
                cornerHeight: card.containerLayer.cornerRadius,
                transform: nil
            )
            : nil
        card.titleBarLayer.backgroundColor = NSColor.clear.cgColor
        card.titleBarLayer.borderColor = NSColor.clear.cgColor
        card.titleBarLayer.borderWidth = 0
        card.shineLayer.opacity = selected ? 1 : 0
    }
    private func layoutCloseButton(
        _ button: CloseButtonLayers,
        target: CloseTarget,
        visible: Bool,
        anchorTopRight: CGPoint
    ) {
        let isButtonHovered = hoveredCloseTarget == target
        let size: CGFloat = isButtonHovered ? 24 : 18
        let buttonFill = isButtonHovered
            ? NSColor.systemRed.withAlphaComponent(0.94)
            : NSColor.white.withAlphaComponent(0.34)
        let buttonStroke = isButtonHovered
            ? NSColor.white.withAlphaComponent(0.76)
            : NSColor.white.withAlphaComponent(0.46)
        let glyphStroke = isButtonHovered
            ? NSColor.white
            : NSColor.white.withAlphaComponent(0.88)
        button.containerLayer.frame = CGRect(
            x: anchorTopRight.x - size / 2,
            y: anchorTopRight.y - size / 2,
            width: size,
            height: size
        )
        button.containerLayer.cornerRadius = size / 2
        button.containerLayer.backgroundColor = buttonFill.cgColor
        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.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.45
        button.glyphLayer.strokeColor = glyphStroke.cgColor
        button.glyphLayer.path = closeGlyphPath(size: size, hovered: isButtonHovered)
    }
    private func closeGlyphPath(size: CGFloat, hovered: Bool) -> CGPath {
        let inset: CGFloat = hovered ? 7.2 : 5.6
        let path = CGMutablePath()
        path.move(to: CGPoint(x: inset, y: inset))
        path.addLine(to: CGPoint(x: size - inset, y: size - inset))
        path.move(to: CGPoint(x: size - inset, y: inset))
        path.addLine(to: CGPoint(x: inset, y: size - inset))
        return path
    }
    private func canClose(app: AlignerApp) -> Bool {
        guard let processIdentifier = app.processIdentifier else { return false }
        return processIdentifier != getpid()
    }
    private func layoutCloseConfirmation() {
        guard let pendingCloseTarget,
              let request = closeRequest(for: pendingCloseTarget),
              let anchor = closeConfirmationAnchor(for: pendingCloseTarget)
        else {
            closeConfirmationLayers.containerLayer.isHidden = true
            closeConfirmationArrowEdge = nil
            return
        }
        let contentWidth: CGFloat = 374
        let height: CGFloat = 184
        let arrowWidth: CGFloat = 14
        let edgeMargin: CGFloat = 16
        let anchorGap: CGFloat = 12
        let width = contentWidth + arrowWidth
        let availableRight = glassLayer.bounds.maxX - anchor.x - edgeMargin
        let availableLeft = anchor.x - glassLayer.bounds.minX - edgeMargin
        let arrowEdge: CloseConfirmationArrowEdge = availableRight >= width || availableRight >= availableLeft
            ? .left
            : .right
        let preferredX = arrowEdge == .left
            ? anchor.x + anchorGap
            : anchor.x - width - anchorGap
        let x = min(
            max(edgeMargin, preferredX),
            max(edgeMargin, glassLayer.bounds.width - width - edgeMargin)
        )
        let y = min(
            max(edgeMargin, anchor.y - height + 64),
            max(edgeMargin, glassLayer.bounds.height - height - edgeMargin)
        )
        let localArrowY = min(max(anchor.y - y, 38), height - 38)
        closeConfirmationArrowEdge = arrowEdge
        let layers = closeConfirmationLayers
        layers.containerLayer.isHidden = false
        layers.containerLayer.frame = CGRect(x: x, y: y, width: width, height: height)
        let contentX = arrowEdge == .left ? arrowWidth : 0
        let contentFrame = CGRect(x: contentX, y: 0, width: contentWidth, height: height)
        let bubblePath = closeConfirmationBubblePath(
            bounds: layers.containerLayer.bounds,
            contentFrame: contentFrame,
            arrowEdge: arrowEdge,
            arrowY: localArrowY
        )
        layers.backgroundLayer.frame = layers.containerLayer.bounds
        layers.backgroundLayer.path = bubblePath
        layers.containerLayer.shadowPath = bubblePath
        layers.titleLayer.string = confirmationTitle(for: request)
        layers.messageLayer.string = confirmationMessage(for: request)
        layers.titleLayer.frame = CGRect(x: contentFrame.minX + 22, y: height - 48, width: contentFrame.width - 44, height: 22)
        layers.messageLayer.frame = CGRect(x: contentFrame.minX + 22, y: height - 78, width: contentFrame.width - 44, height: 21)
        layers.checkboxLayer.frame = CGRect(x: contentFrame.minX + 22, y: 80, width: 18, height: 18)
        layers.checkboxLayer.backgroundColor = (pendingCloseOptOut
            ? NSColor.controlAccentColor.withAlphaComponent(0.92)
            : NSColor.clear
        ).cgColor
        layers.checkboxLayer.borderColor = (pendingCloseOptOut
            ? NSColor.controlAccentColor.withAlphaComponent(0.95)
            : NSColor.separatorColor.withAlphaComponent(0.48)
        ).cgColor
        layers.checkboxMarkLayer.string = pendingCloseOptOut ? "✓" : ""
        layers.checkboxMarkLayer.frame = CGRect(x: 0, y: 1, width: 18, height: 16)
        layers.checkboxLabelLayer.frame = CGRect(x: contentFrame.minX + 48, y: 80, width: contentFrame.width - 70, height: 19)
        layers.cancelButtonLayer.frame = CGRect(x: contentFrame.maxX - 164, y: 22, width: 74, height: 32)
        layers.cancelButtonTextLayer.frame = layers.cancelButtonLayer.bounds.insetBy(dx: 0, dy: 7)
        layers.confirmButtonLayer.frame = CGRect(x: contentFrame.maxX - 78, y: 22, width: 56, height: 32)
        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,
        arrowEdge: CloseConfirmationArrowEdge,
        arrowY: CGFloat
    ) -> CGPath {
        let cornerRadius: CGFloat = 18
        let arrowHalfHeight: CGFloat = 10
        let path = CGMutablePath()
        let minX = contentFrame.minX
        let maxX = contentFrame.maxX
        let minY = contentFrame.minY
        let maxY = contentFrame.maxY
        let arrowY = min(max(arrowY, minY + cornerRadius + arrowHalfHeight + 4), maxY - cornerRadius - arrowHalfHeight - 4)
        path.move(to: CGPoint(x: minX + cornerRadius, y: minY))
        path.addLine(to: CGPoint(x: maxX - cornerRadius, y: minY))
        path.addQuadCurve(to: CGPoint(x: maxX, y: minY + cornerRadius), control: CGPoint(x: maxX, y: minY))
        if arrowEdge == .right {
            path.addLine(to: CGPoint(x: maxX, y: arrowY - arrowHalfHeight))
            path.addLine(to: CGPoint(x: bounds.maxX, y: arrowY))
            path.addLine(to: CGPoint(x: maxX, y: arrowY + arrowHalfHeight))
        }
        path.addLine(to: CGPoint(x: maxX, y: maxY - cornerRadius))
        path.addQuadCurve(to: CGPoint(x: maxX - cornerRadius, y: maxY), control: CGPoint(x: maxX, y: maxY))
        path.addLine(to: CGPoint(x: minX + cornerRadius, y: maxY))
        path.addQuadCurve(to: CGPoint(x: minX, y: maxY - cornerRadius), control: CGPoint(x: minX, y: maxY))
        if arrowEdge == .left {
            path.addLine(to: CGPoint(x: minX, y: arrowY + arrowHalfHeight))
            path.addLine(to: CGPoint(x: bounds.minX, y: arrowY))
            path.addLine(to: CGPoint(x: minX, y: arrowY - arrowHalfHeight))
        }
        path.addLine(to: CGPoint(x: minX, y: minY + cornerRadius))
        path.addQuadCurve(to: CGPoint(x: minX + cornerRadius, y: minY), control: CGPoint(x: minX, y: minY))
        path.closeSubpath()
        return path
    }
    private func confirmationTitle(for request: QuickSwitchCloseRequest) -> String {
        switch request {
        case .app:
            return "关闭 \(request.appName)?"
        case .window:
            return "关闭这个窗口?"
        }
    }
    private func confirmationMessage(for request: QuickSwitchCloseRequest) -> String {
        switch request {
        case .app:
            return "将退出该应用,并一次性关闭它的所有窗口。"
        case .window:
            return "将关闭此窗口,未保存内容可能需要应用确认。"
        }
    }
    private func closeRequest(for target: CloseTarget) -> QuickSwitchCloseRequest? {
        switch target {
        case .app(let appGroupIndex):
            return appShelfItems
                .first { $0.item.appGroupIndex == appGroupIndex }
                .map { .app($0.item) }
        case .window(let appGroupIndex, let windowIndex, let windowID):
            guard let card = waterfallCard(appGroupIndex: appGroupIndex, windowIndex: windowIndex),
                  card.item.window.id == windowID
            else {
                return nil
            }
            return .window(card.item)
        }
    }
    private func requestClose(for target: CloseTarget) {
        guard let request = closeRequest(for: target) else {
            DevelopmentDiagnostics.log("quickSwitch.view.closeRequest.missingTarget", [
                "targetKind": target.kindDescription
            ])
            return
        }
        pendingCloseTarget = nil
        pendingCloseRequest = nil
        pendingCloseOptOut = false
        closeConfirmationLayers.containerLayer.isHidden = true
        closeConfirmationArrowEdge = nil
        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
            ])
            needsLayout = true
            return
        }
        requestClose(for: target)
    }
    private func confirmPendingClose() {
        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 && closeConfirmationRequired {
            closeConfirmationRequired = false
            onCloseConfirmationDisabled?()
        }
        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
        needsLayout = true
    }
    private func toggleCloseConfirmationOptOut() {
        if pendingCloseOptOut && !closeConfirmationRequired {
            return
        }
        pendingCloseOptOut.toggle()
        if pendingCloseOptOut {
            closeConfirmationRequired = false
            onCloseConfirmationDisabled?()
            DevelopmentDiagnostics.log("quickSwitch.view.closeConfirmation.optOutPersisted")
        }
        needsLayout = true
    }
    private enum CloseConfirmationHit {
        case confirm
        case cancel
        case checkbox
        case background
        case outside
    }
    private func closeConfirmationHit(at windowLocation: NSPoint) -> CloseConfirmationHit {
        let location = convert(windowLocation, from: nil)
        let locationInGlass = CGPoint(
            x: location.x - glassLayer.frame.minX,
            y: location.y - glassLayer.frame.minY
        )
        let layers = closeConfirmationLayers
        guard !layers.containerLayer.isHidden else { return .outside }
        guard layers.containerLayer.frame.contains(locationInGlass) else { return .outside }
        let local = CGPoint(
            x: locationInGlass.x - layers.containerLayer.frame.minX,
            y: locationInGlass.y - layers.containerLayer.frame.minY
        )
        if layers.confirmButtonLayer.frame.contains(local) {
            return .confirm
        }
        if layers.cancelButtonLayer.frame.contains(local) {
            return .cancel
        }
        let checkboxHitFrame = layers.checkboxLayer.frame.union(layers.checkboxLabelLayer.frame).insetBy(dx: -6, dy: -4)
        if checkboxHitFrame.contains(local) {
            return .checkbox
        }
        return .background
    }
    private func handleCloseConfirmationMouseDown(at windowLocation: NSPoint) -> Bool {
        switch closeConfirmationHit(at: windowLocation) {
        case .confirm:
            confirmPendingClose()
            return true
        case .cancel:
            cancelPendingClose()
            return true
        case .checkbox:
            toggleCloseConfirmationOptOut()
            return true
        case .background:
            return true
        case .outside:
            if pendingCloseTarget != nil {
                cancelPendingClose()
                return true
            }
            return false
        }
    }
    private func closeButtonTarget(at windowLocation: NSPoint) -> CloseTarget? {
        if let target = appShelfCloseButtonTarget(at: windowLocation) {
            return target
        }
        return waterfallCloseButtonTarget(at: windowLocation)
    }
    private func appShelfCloseButtonTarget(at windowLocation: NSPoint) -> CloseTarget? {
        let locationInView = convert(windowLocation, from: nil)
        let locationInAppShelfContent = CGPoint(
            x: locationInView.x - glassLayer.frame.minX - shelfLayer.frame.minX - appShelfContentLayer.frame.minX,
            y: locationInView.y - glassLayer.frame.minY - shelfLayer.frame.minY - appShelfContentLayer.frame.minY
        )
        for item in appShelfItems {
            let target = CloseTarget.app(appGroupIndex: item.item.appGroupIndex)
            guard item.closeButton.containerLayer.opacity > 0,
                  canClose(app: item.item.app)
            else { continue }
            let locationInItem = CGPoint(
                x: locationInAppShelfContent.x - item.containerLayer.frame.minX,
                y: locationInAppShelfContent.y - item.containerLayer.frame.minY
            )
            if item.closeButton.containerLayer.frame.insetBy(dx: -8, dy: -8).contains(locationInItem) {
                return target
            }
        }
        return nil
    }
    private func waterfallCloseButtonTarget(at windowLocation: NSPoint) -> CloseTarget? {
        let locationInView = convert(windowLocation, from: nil)
        let locationInWaterfallContent = CGPoint(
            x: locationInView.x - glassLayer.frame.minX - waterfallLayer.frame.minX - waterfallContentLayer.frame.minX,
            y: locationInView.y - glassLayer.frame.minY - waterfallLayer.frame.minY - waterfallContentLayer.frame.minY
        )
        for column in waterfallColumns where column.containerLayer.frame.contains(locationInWaterfallContent) {
            let locationInColumn = CGPoint(
                x: locationInWaterfallContent.x - column.containerLayer.frame.minX,
                y: locationInWaterfallContent.y - column.containerLayer.frame.minY
            )
            for card in column.cards where card.containerLayer.frame.contains(locationInColumn) {
                guard card.closeButton.containerLayer.opacity > 0 else { continue }
                let locationInCard = CGPoint(
                    x: locationInColumn.x - card.containerLayer.frame.minX,
                    y: locationInColumn.y - card.containerLayer.frame.minY
                )
                if card.closeButton.containerLayer.frame.insetBy(dx: -8, dy: -8).contains(locationInCard) {
                    return .window(
                        appGroupIndex: card.item.appGroupIndex,
                        windowIndex: card.item.windowIndex,
                        windowID: card.item.window.id
                    )
                }
            }
        }
        return nil
    }
    private func closeConfirmationAnchor(for target: CloseTarget) -> CGPoint? {
        switch target {
        case .app(let appGroupIndex):
            guard let item = appShelfItems.first(where: { $0.item.appGroupIndex == appGroupIndex }) else {
                return nil
            }
            return CGPoint(
                x: shelfLayer.frame.minX
                    + appShelfContentLayer.frame.minX
                    + item.containerLayer.frame.minX
                    + item.closeButton.containerLayer.frame.midX,
                y: shelfLayer.frame.minY
                    + appShelfContentLayer.frame.minY
                    + item.containerLayer.frame.minY
                    + item.closeButton.containerLayer.frame.midY
            )
        case .window(let appGroupIndex, let windowIndex, let windowID):
            guard let column = waterfallColumns.first(where: { $0.column.appGroupIndex == appGroupIndex }),
                  let card = column.cards.first(where: {
                      $0.item.windowIndex == windowIndex && $0.item.window.id == windowID
                  })
            else {
                return nil
            }
            return CGPoint(
                x: waterfallLayer.frame.minX
                    + waterfallContentLayer.frame.minX
                    + column.containerLayer.frame.minX
                    + card.containerLayer.frame.minX
                    + card.closeButton.containerLayer.frame.midX,
                y: waterfallLayer.frame.minY
                    + waterfallContentLayer.frame.minY
                    + column.containerLayer.frame.minY
                    + card.containerLayer.frame.minY
                    + card.closeButton.containerLayer.frame.midY
            )
        }
    }
    private func accentTintedWindowBackground(fraction: CGFloat, alpha: CGFloat) -> NSColor {
        let color = NSColor.windowBackgroundColor.blended(withFraction: fraction, of: NSColor.controlAccentColor)
            ?? NSColor.windowBackgroundColor
        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 {
@@ -2151,7 +4392,7 @@
        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(
@@ -2160,15 +4401,30 @@
        )
    }
    private var isHorizontalMasonryMode: Bool {
        waterfallViewMode == .horizontalMasonry
    }
    private var waterfallMaxScrollOffset: CGFloat {
        max(0, waterfallContentWidth - waterfallLayer.bounds.width)
        guard !isHorizontalMasonryMode else { return 0 }
        return max(0, waterfallContentWidth - waterfallLayer.bounds.width)
    }
    private func clampedWaterfallOffset(_ offset: CGFloat) -> CGFloat {
        min(max(0, offset), waterfallMaxScrollOffset)
    }
    private var horizontalMasonryMaxScrollOffset: CGFloat {
        max(0, horizontalMasonryContentHeight - waterfallLayer.bounds.height)
    }
    private func clampedHorizontalMasonryOffset(_ offset: CGFloat) -> CGFloat {
        min(max(0, offset), horizontalMasonryMaxScrollOffset)
    }
    private func waterfallMaxVerticalScrollOffset(for appGroupIndex: Int) -> CGFloat {
        guard !isHorizontalMasonryMode else { return 0 }
        guard
            let column = waterfallColumns.first(where: { $0.column.appGroupIndex == appGroupIndex })
        else {
@@ -2179,7 +4435,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
@@ -2209,24 +4465,78 @@
        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 var spaceLaneLockedFillColor: NSColor {
        if effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua {
            return NSColor(calibratedRed: 58 / 255, green: 148 / 255, blue: 1, alpha: 0.86)
        }
        return NSColor(calibratedRed: 90 / 255, green: 171 / 255, blue: 1, alpha: 0.82)
    }
    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? {
        guard space.type == .fullscreen else { return nil }
        let layer = CALayer()
        layer.cornerRadius = 1.5
        layer.backgroundColor = NSColor.secondaryLabelColor.withAlphaComponent(space.isCurrent ? 0.46 : 0.32).cgColor
        return layer
        let shapeLayer = CAShapeLayer()
        shapeLayer.fillColor = NSColor.clear.cgColor
        shapeLayer.strokeColor = NSColor.secondaryLabelColor.withAlphaComponent(space.isCurrent ? 0.42 : 0.28).cgColor
        shapeLayer.lineWidth = 1.2
        shapeLayer.lineCap = .round
        shapeLayer.lineJoin = .round
        return shapeLayer
    }
    private func settingsIconImage() -> CGImage? {
        let names = [
            NSImage.Name("NSPreferencesGeneral"),
            NSImage.Name("NSAdvanced")
        ]
        for name in names {
            guard let image = NSImage(named: name) else { continue }
            var rect = CGRect(origin: .zero, size: image.size)
            if let cgImage = image.cgImage(forProposedRect: &rect, context: nil, hints: nil) {
                return cgImage
            }
        }
        return nil
    }
    private func makeTextLayer(
@@ -2247,31 +4557,239 @@
        return textLayer
    }
    private func makeCloseButtonLayers() -> CloseButtonLayers {
        let container = CALayer()
        container.cornerRadius = 8
        container.masksToBounds = false
        container.backgroundColor = NSColor.systemRed.withAlphaComponent(0.92).cgColor
        container.borderColor = NSColor.white.withAlphaComponent(0.72).cgColor
        container.borderWidth = 0.8
        container.shadowColor = NSColor.black.cgColor
        container.shadowOpacity = 0.14
        container.shadowRadius = 5
        container.shadowOffset = CGSize(width: 0, height: -1)
        container.opacity = 0
        let glyph = CAShapeLayer()
        glyph.contentsScale = backingScaleFactor
        glyph.strokeColor = NSColor.white.cgColor
        glyph.fillColor = NSColor.clear.cgColor
        glyph.lineCap = .round
        glyph.lineJoin = .round
        container.addSublayer(glyph)
        return CloseButtonLayers(containerLayer: container, glyphLayer: glyph)
    }
    private func makeCloseConfirmationLayers() -> CloseConfirmationLayers {
        let container = CALayer()
        container.masksToBounds = false
        container.backgroundColor = NSColor.clear.cgColor
        container.shadowColor = NSColor.black.cgColor
        container.shadowOpacity = 0.20
        container.shadowRadius = 20
        container.shadowOffset = CGSize(width: 0, height: -8)
        container.zPosition = 200
        let background = CAShapeLayer()
        background.contentsScale = backingScaleFactor
        background.fillColor = NSColor.windowBackgroundColor.withAlphaComponent(0.985).cgColor
        background.strokeColor = NSColor.separatorColor.withAlphaComponent(0.36).cgColor
        background.lineWidth = 1
        let title = makeTextLayer(
            string: "",
            fontSize: 15,
            weight: .semibold,
            color: .labelColor
        )
        let message = makeTextLayer(
            string: "",
            fontSize: 13,
            weight: .regular,
            color: .secondaryLabelColor
        )
        message.truncationMode = .end
        let checkbox = CALayer()
        checkbox.cornerRadius = 4
        checkbox.borderWidth = 1
        checkbox.borderColor = NSColor.separatorColor.withAlphaComponent(0.48).cgColor
        checkbox.backgroundColor = NSColor.clear.cgColor
        let checkmark = makeTextLayer(
            string: "",
            fontSize: 11,
            weight: .bold,
            color: .white,
            alignment: .center
        )
        let checkboxLabel = makeTextLayer(
            string: "以后不再提醒",
            fontSize: 13,
            weight: .regular,
            color: .secondaryLabelColor
        )
        let confirmButton = CALayer()
        confirmButton.cornerRadius = 7
        confirmButton.backgroundColor = NSColor.systemRed.withAlphaComponent(0.94).cgColor
        let confirmText = makeTextLayer(
            string: "关闭",
            fontSize: 13,
            weight: .semibold,
            color: .white,
            alignment: .center
        )
        let cancelButton = CALayer()
        cancelButton.cornerRadius = 7
        cancelButton.backgroundColor = NSColor.systemFill.withAlphaComponent(0.16).cgColor
        cancelButton.borderColor = NSColor.separatorColor.withAlphaComponent(0.22).cgColor
        cancelButton.borderWidth = 1
        let cancelText = makeTextLayer(
            string: "取消",
            fontSize: 13,
            weight: .medium,
            color: .labelColor,
            alignment: .center
        )
        container.addSublayer(background)
        container.addSublayer(title)
        container.addSublayer(message)
        container.addSublayer(checkbox)
        checkbox.addSublayer(checkmark)
        container.addSublayer(checkboxLabel)
        container.addSublayer(confirmButton)
        confirmButton.addSublayer(confirmText)
        container.addSublayer(cancelButton)
        cancelButton.addSublayer(cancelText)
        return CloseConfirmationLayers(
            containerLayer: container,
            backgroundLayer: background,
            titleLayer: title,
            messageLayer: message,
            checkboxLayer: checkbox,
            checkboxMarkLayer: checkmark,
            checkboxLabelLayer: checkboxLabel,
            confirmButtonLayer: confirmButton,
            confirmButtonTextLayer: confirmText,
            cancelButtonLayer: cancelButton,
            cancelButtonTextLayer: cancelText
        )
    }
    private func makeWindowBlocks(count: Int, space: QuickSwitchSpaceViewModel) -> [CALayer] {
        guard count > 0 else { return [] }
        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
        }
    }
    private func appIdentifierText(for appNames: [String]) -> String {
        var identifiers = appNames.prefix(3).compactMap { name -> String? in
    private func appIdentifierText(for space: QuickSwitchSpaceViewModel) -> String {
        if space.type == .fullscreen,
           let appName = space.appNames.first?.trimmingCharacters(in: .whitespacesAndNewlines),
           !appName.isEmpty {
            return appName
        }
        var identifiers = space.appNames.prefix(3).compactMap { name -> String? in
            let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
            return trimmed.first.map { String($0) }
        }
        if appNames.count > 3 {
            identifiers.append("+\(appNames.count - 3)")
        if space.appNames.count > 3 {
            identifiers.append("+\(space.appNames.count - 3)")
        }
        return identifiers.joined(separator: " ")
    }
    private func splitViewAppNames(for space: QuickSwitchSpaceViewModel) -> [String] {
        guard space.type == .fullscreen else { return [] }
        let names = space.splitViewAppNames
            .prefix(2)
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }
        guard names.count >= 2 else { return [] }
        return names
    }
    private func layoutFullscreenMarkerLayer(
        _ layer: CALayer,
        in segmentBounds: CGRect,
        isSplitView: Bool = false
    ) {
        let markerFrame = fullscreenMarkerFrame(in: segmentBounds)
        layer.frame = markerFrame
        guard let shapeLayer = layer as? CAShapeLayer else { return }
        let bounds = shapeLayer.bounds
        if isSplitView {
            let insetBounds = bounds.insetBy(dx: 0.6, dy: 0.6)
            let radius = min(7, max(5, insetBounds.height * 0.22))
            let path = CGMutablePath()
            path.addRoundedRect(
                in: insetBounds,
                cornerWidth: radius,
                cornerHeight: radius
            )
            path.move(to: CGPoint(x: bounds.midX, y: insetBounds.minY + 1.5))
            path.addLine(to: CGPoint(x: bounds.midX, y: insetBounds.maxY - 1.5))
            shapeLayer.path = path
            return
        }
        let cornerLength = min(12, max(7, min(bounds.width, bounds.height) * 0.28))
        let path = CGMutablePath()
        path.move(to: CGPoint(x: 0, y: cornerLength))
        path.addLine(to: CGPoint(x: 0, y: 0))
        path.addLine(to: CGPoint(x: cornerLength, y: 0))
        path.move(to: CGPoint(x: bounds.maxX - cornerLength, y: 0))
        path.addLine(to: CGPoint(x: bounds.maxX, y: 0))
        path.addLine(to: CGPoint(x: bounds.maxX, y: cornerLength))
        path.move(to: CGPoint(x: bounds.maxX, y: bounds.maxY - cornerLength))
        path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
        path.addLine(to: CGPoint(x: bounds.maxX - cornerLength, y: bounds.maxY))
        path.move(to: CGPoint(x: cornerLength, y: bounds.maxY))
        path.addLine(to: CGPoint(x: 0, y: bounds.maxY))
        path.addLine(to: CGPoint(x: 0, y: bounds.maxY - cornerLength))
        shapeLayer.path = path
    }
    private func fullscreenMarkerFrame(in segmentBounds: 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: markerY,
            width: max(1, segmentBounds.width - 20),
            height: markerHeight
        )
    }
    private func columnSelectionHistoryReports() -> [[String: Int]] {
@@ -2308,17 +4826,27 @@
                "labelFrame": dictionary(from: item.labelLayer.frame),
                "labelTruncationMode": "middle",
                "badgeVisible": !item.badgeLayer.isHidden,
                "badgeText": item.badgeLayer.string ?? ""
                "badgeText": item.badgeLayer.string ?? "",
                "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),
                "closeButtonGlyphColor": colorDictionary(from: item.closeButton.glyphLayer.strokeColor),
                "closeButtonGlyphLineWidth": Double(item.closeButton.glyphLayer.lineWidth)
            ] as [String: Any]
        }
    }
    private func appShelfVisualStates(for item: AppShelfItemLayers) -> [String] {
        var states: [String] = []
        if item.item.appGroupIndex == effectiveSelection?.appGroupIndex {
        let isSelected = item.item.appGroupIndex == effectiveSelection?.appGroupIndex
        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 {
@@ -2331,21 +4859,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)
@@ -2427,8 +4983,22 @@
                "screenshotSource": screenshotSourceString(screenshotSource, for: card.item.window),
                "screenshotFallbackReason": screenshotFallbackReasonString(screenshotSource),
                "screenshotNotRequestedReason": screenshotSkippedReasonsByWindowID[card.item.window.id] ?? NSNull(),
                "backgroundColor": colorDictionary(from: card.containerLayer.backgroundColor),
                "shadowOpacity": Double(card.containerLayer.shadowOpacity),
                "shadowRadius": Double(card.containerLayer.shadowRadius),
                "thumbnailFrame": dictionary(from: card.thumbnailLayer.frame),
                "titleBarFrame": dictionary(from: card.titleBarLayer.frame),
                "titleBarBorderWidth": Double(card.titleBarLayer.borderWidth),
                "titleFrame": dictionary(from: card.titleLayer.frame),
                "metaFrame": dictionary(from: card.metaLayer.frame),
                "appIconFrame": dictionary(from: card.appIconLayer.frame),
                "stateFrame": dictionary(from: card.stateLayer.frame),
                "closeButtonVisible": card.closeButton.containerLayer.opacity > 0,
                "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),
@@ -2523,13 +5093,17 @@
    private func waterfallCardVisualStates(for card: WaterfallCardLayers) -> [String] {
        var states: [String] = []
        if card.item.window.id == effectiveSelection?.windowID {
        let isSelected = card.item.window.id == effectiveSelection?.windowID
        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 {
@@ -2545,25 +5119,99 @@
        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))
                }
                let splitAppNames = splitViewAppNames(for: space)
                let splitAppLabelFrames = segment.splitAppLayers.map { dictionary(from: $0.frame) }
                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": fullscreenMarkerKind(
                        markerLayer: segment.fullscreenMarkerLayer,
                        splitAppNames: splitAppNames
                    ),
                    "fullscreenMarkerFrame": fullscreenMarkerFrame.map { dictionary(from: $0) } ?? NSNull(),
                    "fullscreenMarkerCenterRatio": fullscreenMarkerCenterRatio ?? NSNull(),
                    "splitViewAppNames": splitAppNames,
                    "splitViewLabelFrames": splitAppLabelFrames,
                    "splitViewLabelColors": segment.splitAppLayers.map {
                        colorDictionary(from: $0.foregroundColor)
                    },
                    "windowBlockCount": segment.windowBlocks.count,
                    "windowBlockColors": segment.windowBlocks.map { colorDictionary(from: $0.backgroundColor) },
                    "windowBlockFrames": segment.windowBlocks.map { dictionary(from: $0.frame) },
                    "labelColor": colorDictionary(from: segment.labelLayer.foregroundColor),
                    "countColor": colorDictionary(from: segment.countLayer.foregroundColor),
                    "appColor": colorDictionary(from: segment.appLayer.foregroundColor),
                    "fullscreenMarkerStrokeColor": colorDictionary(
                        from: (segment.fullscreenMarkerLayer as? CAShapeLayer)?.strokeColor
                    ),
                    "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),
                    "shadowColor": colorDictionary(from: segment.containerLayer.shadowColor),
                    "shadowOpacity": Double(segment.containerLayer.shadowOpacity),
                    "shadowRadius": Double(segment.containerLayer.shadowRadius),
                    "shadowOffset": [
                        "width": Double(segment.containerLayer.shadowOffset.width),
                        "height": Double(segment.containerLayer.shadowOffset.height)
                    ],
                    "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
@@ -2572,19 +5220,42 @@
        } ?? []
    }
    private func spaceLaneDisplayGroup(containingSpaceID spaceID: UInt64) -> SpaceLaneDisplayGroupLayers? {
        spaceLaneDisplayGroups.first { group in
            group.segments.contains { $0.space.id == spaceID }
        }
    }
    private func fullscreenMarkerKind(markerLayer: CALayer?, splitAppNames: [String]) -> String {
        guard markerLayer != nil else { return "none" }
        return splitAppNames.count >= 2 ? "splitViewPair" : "cornerBracket"
    }
    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 {
            states.append("focused")
        }
        if space.id == currentViewModel?.lockedSpaceID {
            states.append("locked")
        }
        if space.type == .fullscreen {
            states.append("fullscreen")
        }
        if splitViewAppNames(for: space).count >= 2 {
            states.append("splitView")
        }
        return states
    }
@@ -2610,4 +5281,43 @@
            "height": Double(rect.height)
        ]
    }
    private func colorDictionary(from color: CGColor?) -> [String: Double] {
        guard let color,
              let nsColor = NSColor(cgColor: color)?.usingColorSpace(.deviceRGB)
        else {
            return [
                "red": 0,
                "green": 0,
                "blue": 0,
                "alpha": 0
            ]
        }
        return [
            "red": Double(nsColor.redComponent),
            "green": Double(nsColor.greenComponent),
            "blue": Double(nsColor.blueComponent),
            "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"
    }
}