From a416923b1298c818ed9c991d6b439c5eea30a653 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sat, 23 May 2026 03:10:18 +0800
Subject: [PATCH] Archive QA-passed 7.6.0 window logic build

---
 Apptag/ApptagApp.swift                                                                     |  362 +++++-
 Apptag/AppGridItem.swift                                                                   |  335 -------
 Apptag/EditModeViews.swift                                                                 |    2 
 .hermes/plans/2026-05-05_124500-apptag-design.md                                           |    2 
 Apptag/PreferencesView.swift                                                               |  114 +
 Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.csv           |    2 
 Apptag/QuickSearch.swift                                                                   |    4 
 /dev/null                                                                                  |  112 --
 generate_icon.py                                                                           |    5 
 Apptag/AppGridCollectionView.swift                                                         |  393 +++++++-
 Scripts/window_logic_qa.sh                                                                 |  451 +++++++++
 Apptag/ContentView.swift                                                                   |  795 +---------------
 Apptag/DataLayer.swift                                                                     |  190 +++
 Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.base.json     |    3 
 Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.manifest.json |    4 
 15 files changed, 1,403 insertions(+), 1,371 deletions(-)

diff --git a/.hermes/plans/2026-05-05_124500-apptag-design.md b/.hermes/plans/2026-05-05_124500-apptag-design.md
index f10e8e4..1179aab 100644
--- a/.hermes/plans/2026-05-05_124500-apptag-design.md
+++ b/.hermes/plans/2026-05-05_124500-apptag-design.md
@@ -89,7 +89,7 @@
 
 ## Files to Create
 
-All under `~/Projects/Apptag/Apptag/`:
+All under `~/Projects/Taglauncher/Apptag/`:
 - `ApptagApp.swift`
 - `TagStore.swift`
 - `AppIndexer.swift`
diff --git a/Apptag/AppGridCollectionView.swift b/Apptag/AppGridCollectionView.swift
index 2f46f71..9fdd8af 100644
--- a/Apptag/AppGridCollectionView.swift
+++ b/Apptag/AppGridCollectionView.swift
@@ -1,6 +1,34 @@
 import SwiftUI
 import AppKit
 
+fileprivate enum AppGridCollectionDisplayMode: Equatable {
+    case flat
+    case masonryContainer
+    case gridContainer
+
+    init(_ rawValue: String) {
+        switch rawValue {
+        case "container", "coloredContainer":
+            self = .masonryContainer
+        case "gridContainer", "coloredGridContainer":
+            self = .gridContainer
+        default:
+            self = .flat
+        }
+    }
+
+    var usesCardSurface: Bool {
+        self != .flat
+    }
+}
+
+fileprivate struct AppGridBubbleSuppressionReasons: OptionSet {
+    let rawValue: Int
+
+    static let externalInteraction = AppGridBubbleSuppressionReasons(rawValue: 1 << 0)
+    static let scroll = AppGridBubbleSuppressionReasons(rawValue: 1 << 1)
+}
+
 struct AppGridCollectionView: NSViewRepresentable {
     let groups: [TagGroup]
     let tagColors: [String: Int]
@@ -61,7 +89,8 @@
         var displayMode = AppDefaults.displayMode
         var iconSize: CGFloat = AppDefaults.iconSize
         var showNames = true
-        var bubbleDisabled = false
+        private var externalBubbleDisabled = false
+        private var scrollBubbleDisabled = false
         var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles
         var highlightedGroupName: String?
         var contentRevision = 0
@@ -104,7 +133,7 @@
             self.displayMode = displayMode
             self.iconSize = iconSize
             self.showNames = showNames
-            self.bubbleDisabled = bubbleDisabled
+            self.externalBubbleDisabled = bubbleDisabled
             self.showUncommonAppBubbles = showUncommonAppBubbles
             self.highlightedGroupName = highlightedGroupName
             self.contentRevision = contentRevision
@@ -161,6 +190,40 @@
             groups.firstIndex { $0.id == tagID || $0.name == tagID }
         }
 
+        fileprivate var displayStyle: AppGridCollectionDisplayMode {
+            AppGridCollectionDisplayMode(displayMode)
+        }
+
+        var isColoredContainerMode: Bool {
+            displayMode == "coloredContainer" || displayMode == "coloredGridContainer"
+        }
+
+        var isColorlessContainerMode: Bool {
+            displayMode == "container" || displayMode == "gridContainer"
+        }
+
+        fileprivate var bubbleSuppressionReasons: AppGridBubbleSuppressionReasons {
+            var reasons: AppGridBubbleSuppressionReasons = []
+            if externalBubbleDisabled {
+                reasons.insert(.externalInteraction)
+            }
+            if scrollBubbleDisabled {
+                reasons.insert(.scroll)
+            }
+            return reasons
+        }
+
+        var bubbleDisabled: Bool {
+            !bubbleSuppressionReasons.isEmpty
+        }
+
+        @discardableResult
+        func setScrollBubbleDisabled(_ disabled: Bool) -> Bool {
+            guard scrollBubbleDisabled != disabled else { return false }
+            scrollBubbleDisabled = disabled
+            return true
+        }
+
         private static func signature(
             tagColors: [String: Int],
             displayMode: String,
@@ -195,6 +258,7 @@
     private var scrollObserver: NSObjectProtocol?
     private var lastLayoutSize: NSSize = .zero
     private var lastReportedBoundsOrigin: NSPoint?
+    private var scrollUnfreezeWorkItem: DispatchWorkItem?
 
     override var isFlipped: Bool { true }
 
@@ -212,6 +276,7 @@
         if let scrollObserver {
             NotificationCenter.default.removeObserver(scrollObserver)
         }
+        scrollUnfreezeWorkItem?.cancel()
     }
 
     func configure(coordinator: AppGridCollectionView.Coordinator) {
@@ -228,9 +293,7 @@
             collectionView.reloadData()
             gridLayout.invalidateLayout()
         } else {
-            collectionView.visibleItems().forEach { item in
-                (item as? AppGridGroupCollectionItem)?.refreshRuntimeState()
-            }
+            refreshVisibleRuntimeState()
         }
 
         if coordinator.scrollRequestToken != coordinator.lastHandledScrollRequestToken {
@@ -285,7 +348,39 @@
             guard let self,
                   self.recordScrollIfNeeded()
             else { return }
-            self.coordinator?.onScrollActivity()
+            self.handleScrollActivity()
+        }
+    }
+
+    private func handleScrollActivity() {
+        if coordinator?.setScrollBubbleDisabled(true) == true {
+            refreshVisibleRuntimeState()
+        }
+        coordinator?.onScrollActivity()
+
+        scrollUnfreezeWorkItem?.cancel()
+        let workItem = DispatchWorkItem { [weak self] in
+            guard let self else { return }
+            self.replayPointerHover()
+            if self.coordinator?.setScrollBubbleDisabled(false) == true {
+                self.refreshVisibleRuntimeState()
+            }
+        }
+        scrollUnfreezeWorkItem = workItem
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.18, execute: workItem)
+    }
+
+    private func refreshVisibleRuntimeState() {
+        collectionView.visibleItems().forEach { item in
+            (item as? AppGridGroupCollectionItem)?.refreshRuntimeState()
+        }
+    }
+
+    private func replayPointerHover() {
+        guard let window else { return }
+        let windowPoint = window.convertPoint(fromScreen: NSEvent.mouseLocation)
+        collectionView.visibleItems().forEach { item in
+            (item as? AppGridGroupCollectionItem)?.replayPointerHover(windowPoint: windowPoint)
         }
     }
 
@@ -328,7 +423,8 @@
         let plan = Self.makePlan(
             groups: visibleGroups,
             contentWidth: collectionView.bounds.width,
-            iconSize: iconSize
+            iconSize: iconSize,
+            displayMode: coordinator?.displayMode ?? AppDefaults.displayMode
         )
 
         var nextAttributes: [IndexPath: NSCollectionViewLayoutAttributes] = [:]
@@ -373,6 +469,22 @@
     private static func makePlan(
         groups: [TagGroup],
         contentWidth: CGFloat,
+        iconSize: CGFloat,
+        displayMode: String
+    ) -> LayoutPlan {
+        switch AppGridCollectionDisplayMode(displayMode) {
+        case .flat:
+            return makeFlatPlan(groups: groups, contentWidth: contentWidth, iconSize: iconSize)
+        case .masonryContainer:
+            return makeMasonryPlan(groups: groups, contentWidth: contentWidth, iconSize: iconSize)
+        case .gridContainer:
+            return makeGridPlan(groups: groups, contentWidth: contentWidth, iconSize: iconSize)
+        }
+    }
+
+    private static func makeGridPlan(
+        groups: [TagGroup],
+        contentWidth: CGFloat,
         iconSize: CGFloat
     ) -> LayoutPlan {
         let boundedContentWidth = max(1, contentWidth)
@@ -404,6 +516,79 @@
         }
 
         let contentHeight = rows.isEmpty ? outerPadding * 2 : y - gap + outerPadding
+        return LayoutPlan(
+            items: items,
+            contentSize: NSSize(width: boundedContentWidth, height: max(1, contentHeight))
+        )
+    }
+
+    private static func makeFlatPlan(
+        groups: [TagGroup],
+        contentWidth: CGFloat,
+        iconSize: CGFloat
+    ) -> LayoutPlan {
+        let boundedContentWidth = max(1, contentWidth)
+        let outerPadding = AppGridCollectionMetrics.outerPadding
+        let availableWidth = max(1, boundedContentWidth - outerPadding * 2)
+        var y = outerPadding
+        var items: [LayoutItem] = []
+
+        for (index, group) in groups.enumerated() {
+            let height = AppGridCollectionMetrics.flatGroupHeight(
+                appCount: group.apps.count,
+                width: availableWidth,
+                iconSize: iconSize
+            )
+            items.append(
+                LayoutItem(
+                    index: index,
+                    frame: NSRect(x: outerPadding, y: y, width: availableWidth, height: height)
+                )
+            )
+            y += height + AppGridCollectionMetrics.flatGroupGap
+        }
+
+        let contentHeight = groups.isEmpty ? outerPadding * 2 : y - AppGridCollectionMetrics.flatGroupGap + outerPadding
+        return LayoutPlan(
+            items: items,
+            contentSize: NSSize(width: boundedContentWidth, height: max(1, contentHeight))
+        )
+    }
+
+    private static func makeMasonryPlan(
+        groups: [TagGroup],
+        contentWidth: CGFloat,
+        iconSize: CGFloat
+    ) -> LayoutPlan {
+        let boundedContentWidth = max(1, contentWidth)
+        let outerPadding = AppGridCollectionMetrics.outerPadding
+        let gap = AppGridCollectionMetrics.cardGap
+        let availableWidth = max(1, boundedContentWidth - outerPadding * 2)
+        let columnCount = preferredMasonryColumnCount(availableWidth: availableWidth)
+        let columnWidth = floor((availableWidth - gap * CGFloat(columnCount - 1)) / CGFloat(columnCount))
+        var columnHeights = Array(repeating: outerPadding, count: columnCount)
+        var items: [LayoutItem] = []
+
+        for (index, group) in groups.enumerated() {
+            let columnIndex = columnHeights.indices.min { columnHeights[$0] < columnHeights[$1] } ?? 0
+            let x = outerPadding + CGFloat(columnIndex) * (columnWidth + gap)
+            let y = columnHeights[columnIndex]
+            let height = AppGridCollectionMetrics.cardHeight(
+                appCount: group.apps.count,
+                width: columnWidth,
+                iconSize: iconSize
+            )
+            items.append(
+                LayoutItem(
+                    index: index,
+                    frame: NSRect(x: x, y: y, width: columnWidth, height: height)
+                )
+            )
+            columnHeights[columnIndex] = y + height + gap
+        }
+
+        let tallest = columnHeights.max() ?? outerPadding
+        let contentHeight = groups.isEmpty ? outerPadding * 2 : tallest - gap + outerPadding
         return LayoutPlan(
             items: items,
             contentSize: NSSize(width: boundedContentWidth, height: max(1, contentHeight))
@@ -505,6 +690,14 @@
         return 1
     }
 
+    private static func preferredMasonryColumnCount(availableWidth: CGFloat) -> Int {
+        let preferredColumnWidth: CGFloat = 280
+        return max(
+            1,
+            Int((availableWidth + AppGridCollectionMetrics.cardGap) / (preferredColumnWidth + AppGridCollectionMetrics.cardGap))
+        )
+    }
+
     private static func spanPatterns(trackCount: Int) -> [[Int]] {
         switch trackCount {
         case 3:
@@ -525,6 +718,7 @@
 private enum AppGridCollectionMetrics {
     static let outerPadding: CGFloat = 20
     static let cardGap: CGFloat = 16
+    static let flatGroupGap: CGFloat = 24
     static let cardPadding: CGFloat = 16
     static let headerHeight: CGFloat = 28
     static let headerBottomGap: CGFloat = 6
@@ -541,10 +735,14 @@
         iconSize * hoverScale + labelHeight + 22
     }
 
-    static func columns(width: CGFloat, iconSize: CGFloat) -> Int {
-        let inner = max(1, width - cardPadding * 2)
+    static func columnsForIconArea(width: CGFloat, iconSize: CGFloat) -> Int {
+        let inner = max(1, width)
         let itemW = iconCellWidth(iconSize: iconSize)
         return max(1, Int((inner + iconColumnGap) / (itemW + iconColumnGap)))
+    }
+
+    static func columns(width: CGFloat, iconSize: CGFloat) -> Int {
+        columnsForIconArea(width: width - cardPadding * 2, iconSize: iconSize)
     }
 
     static func cardHeight(appCount: Int, width: CGFloat, iconSize: CGFloat) -> CGFloat {
@@ -563,6 +761,15 @@
     static func iconRows(appCount: Int, width: CGFloat, iconSize: CGFloat) -> Int {
         let cols = columns(width: width, iconSize: iconSize)
         return max(1, (appCount + cols - 1) / cols)
+    }
+
+    static func flatGroupHeight(appCount: Int, width: CGFloat, iconSize: CGFloat) -> CGFloat {
+        let cols = columnsForIconArea(width: width, iconSize: iconSize)
+        let rows = max(1, (appCount + cols - 1) / cols)
+        return headerHeight
+            + headerBottomGap
+            + CGFloat(rows) * iconCellHeight(iconSize: iconSize)
+            + CGFloat(max(0, rows - 1)) * iconRowGap
     }
 }
 
@@ -585,6 +792,10 @@
         cardView.refreshRuntimeState()
     }
 
+    func replayPointerHover(windowPoint: NSPoint) {
+        cardView.replayPointerHover(windowPoint: windowPoint)
+    }
+
     override func prepareForReuse() {
         super.prepareForReuse()
         cardView.prepareForReuse()
@@ -596,6 +807,7 @@
     private weak var coordinator: AppGridCollectionView.Coordinator?
     private var group: TagGroup?
     private var iconViews: [AppGridIconNSView] = []
+    private var isMouseInside = false
     private var isHovered = false
     private var trackingAreaRef: NSTrackingArea?
 
@@ -629,6 +841,9 @@
         AppDragCoordinator.shared.unregister(id: dropTargetID)
         group = nil
         coordinator = nil
+        isMouseInside = false
+        isHovered = false
+        layer?.shadowOpacity = 0
         iconViews.forEach {
             $0.prepareForReuse()
             $0.removeFromSuperview()
@@ -638,10 +853,11 @@
 
     func refreshRuntimeState() {
         let runtime = AppGridIconRuntimeState(
-            bubbleDisabled: coordinator?.bubbleDisabled ?? false,
+            bubbleSuppressionReasons: coordinator?.bubbleSuppressionReasons ?? [],
             showUncommonAppBubbles: coordinator?.showUncommonAppBubbles ?? AppDefaults.showUncommonAppBubbles
         )
         iconViews.forEach { $0.runtimeState = runtime }
+        updateHoverPresentation()
         needsDisplay = true
     }
 
@@ -670,13 +886,11 @@
     }
 
     override func mouseEntered(with event: NSEvent) {
-        isHovered = true
-        needsDisplay = true
+        setPointerInside(true)
     }
 
     override func mouseExited(with event: NSEvent) {
-        isHovered = false
-        needsDisplay = true
+        setPointerInside(false)
     }
 
     override func mouseUp(with event: NSEvent) {
@@ -689,48 +903,47 @@
     override func layout() {
         super.layout()
         guard let coordinator, let group else { return }
-        let cols = AppGridCollectionMetrics.columns(width: bounds.width, iconSize: coordinator.iconSize)
-        let innerWidth = max(1, bounds.width - AppGridCollectionMetrics.cardPadding * 2)
+        let contentRect = iconContentRect(displayStyle: coordinator.displayStyle)
+        let cols = AppGridCollectionMetrics.columnsForIconArea(width: contentRect.width, iconSize: coordinator.iconSize)
         let cellWidth = max(
             1,
-            (innerWidth - AppGridCollectionMetrics.iconColumnGap * CGFloat(cols - 1)) / CGFloat(cols)
+            (contentRect.width - AppGridCollectionMetrics.iconColumnGap * CGFloat(cols - 1)) / CGFloat(cols)
         )
         let cellHeight = AppGridCollectionMetrics.iconCellHeight(iconSize: coordinator.iconSize)
-        let startY = AppGridCollectionMetrics.cardPadding
-            + AppGridCollectionMetrics.headerHeight
-            + AppGridCollectionMetrics.headerBottomGap
 
         for index in group.apps.indices {
             guard index < iconViews.count else { continue }
             let row = index / cols
             let col = index % cols
-            let x = AppGridCollectionMetrics.cardPadding
+            let x = contentRect.minX
                 + CGFloat(col) * (cellWidth + AppGridCollectionMetrics.iconColumnGap)
-            let y = startY + CGFloat(row) * (cellHeight + AppGridCollectionMetrics.iconRowGap)
+            let y = contentRect.minY + CGFloat(row) * (cellHeight + AppGridCollectionMetrics.iconRowGap)
             iconViews[index].frame = NSRect(x: x, y: y, width: cellWidth, height: cellHeight)
         }
     }
 
     override func draw(_ dirtyRect: NSRect) {
         guard let coordinator, let group else { return }
-        let rect = bounds.insetBy(dx: 0.5, dy: 0.5)
-        let path = NSBezierPath(roundedRect: rect, xRadius: 14, yRadius: 14)
+        let displayStyle = coordinator.displayStyle
         let tagColor = TagColor.nsColor(for: coordinator.tagColors[group.name] ?? 0)
-        let isColored = coordinator.displayMode == "coloredGridContainer"
-        let isColorlessActive = coordinator.displayMode == "gridContainer"
+        let isColorlessActive = coordinator.isColorlessContainerMode
             && (isHovered || coordinator.highlightedGroupName == group.name)
 
-        cardSurfaceColor().setFill()
-        path.fill()
-        if isColored || isColorlessActive {
-            tagColor.withAlphaComponent(0.30).setFill()
+        if displayStyle.usesCardSurface {
+            let rect = bounds.insetBy(dx: 0.5, dy: 0.5)
+            let path = NSBezierPath(roundedRect: rect, xRadius: 14, yRadius: 14)
+            cardSurfaceColor().setFill()
             path.fill()
+            if coordinator.isColoredContainerMode || isColorlessActive {
+                tagColor.withAlphaComponent(0.30).setFill()
+                path.fill()
+            }
+            NSColor.labelColor.withAlphaComponent(0.08).setStroke()
+            path.lineWidth = 1
+            path.stroke()
         }
-        NSColor.labelColor.withAlphaComponent(0.08).setStroke()
-        path.lineWidth = 1
-        path.stroke()
 
-        drawHeader(title: group.name)
+        drawHeader(title: group.name, displayStyle: displayStyle)
     }
 
     func performDrop(path: String, source: String, copy: Bool) {
@@ -739,6 +952,11 @@
         if source != group.name || copy {
             coordinator?.onDragModeChange(false)
         }
+    }
+
+    func replayPointerHover(windowPoint: NSPoint) {
+        setPointerInside(bounds.contains(convert(windowPoint, from: nil)))
+        iconViews.forEach { $0.replayPointerHover(windowPoint: windowPoint) }
     }
 
     private func rebuildIconViews() {
@@ -769,6 +987,59 @@
         AppDragCoordinator.shared.register(id: dropTargetID, view: self, tag: group.name)
     }
 
+    private func setPointerInside(_ inside: Bool) {
+        isMouseInside = inside
+        updateHoverPresentation()
+    }
+
+    private func updateHoverPresentation() {
+        guard let coordinator else { return }
+        let shouldHover = coordinator.displayStyle.usesCardSurface
+            && isMouseInside
+            && !coordinator.bubbleDisabled
+        guard isHovered != shouldHover else {
+            updateCardShadow()
+            return
+        }
+        isHovered = shouldHover
+        updateCardShadow()
+        needsDisplay = true
+    }
+
+    private func updateCardShadow() {
+        guard let coordinator else { return }
+        let isColorlessActive = coordinator.isColorlessContainerMode
+            && (isHovered || coordinator.highlightedGroupName == group?.name)
+        let shouldShadow = coordinator.displayStyle.usesCardSurface
+            && ((coordinator.isColoredContainerMode && isHovered) || isColorlessActive)
+        layer?.shadowColor = NSColor.black.cgColor
+        layer?.shadowOpacity = shouldShadow ? 0.22 : 0
+        layer?.shadowRadius = shouldShadow ? 8 : 0
+        layer?.shadowOffset = NSSize(width: 0, height: -3)
+    }
+
+    private func iconContentRect(displayStyle: AppGridCollectionDisplayMode) -> NSRect {
+        switch displayStyle {
+        case .flat:
+            return NSRect(
+                x: 0,
+                y: AppGridCollectionMetrics.headerHeight + AppGridCollectionMetrics.headerBottomGap,
+                width: bounds.width,
+                height: max(1, bounds.height - AppGridCollectionMetrics.headerHeight - AppGridCollectionMetrics.headerBottomGap)
+            )
+        case .masonryContainer, .gridContainer:
+            let startY = AppGridCollectionMetrics.cardPadding
+                + AppGridCollectionMetrics.headerHeight
+                + AppGridCollectionMetrics.headerBottomGap
+            return NSRect(
+                x: AppGridCollectionMetrics.cardPadding,
+                y: startY,
+                width: max(1, bounds.width - AppGridCollectionMetrics.cardPadding * 2),
+                height: max(1, bounds.height - startY - AppGridCollectionMetrics.cardPadding)
+            )
+        }
+    }
+
     private func cardSurfaceColor() -> NSColor {
         if effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua {
             return NSColor.white.withAlphaComponent(0.055)
@@ -776,11 +1047,13 @@
         return NSColor.white.withAlphaComponent(0.62)
     }
 
-    private func drawHeader(title: String) {
+    private func drawHeader(title: String, displayStyle: AppGridCollectionDisplayMode) {
+        let horizontalInset = displayStyle.usesCardSurface ? AppGridCollectionMetrics.cardPadding : 0
+        let verticalInset = displayStyle.usesCardSurface ? AppGridCollectionMetrics.cardPadding : 0
         let headerRect = NSRect(
-            x: AppGridCollectionMetrics.cardPadding,
-            y: AppGridCollectionMetrics.cardPadding,
-            width: max(1, bounds.width - AppGridCollectionMetrics.cardPadding * 2),
+            x: horizontalInset,
+            y: verticalInset,
+            width: max(1, bounds.width - horizontalInset * 2),
             height: AppGridCollectionMetrics.headerHeight
         )
         let attributes: [NSAttributedString.Key: Any] = [
@@ -818,8 +1091,12 @@
 }
 
 private struct AppGridIconRuntimeState {
-    var bubbleDisabled: Bool
+    var bubbleSuppressionReasons: AppGridBubbleSuppressionReasons
     var showUncommonAppBubbles: Bool
+
+    var bubbleDisabled: Bool {
+        !bubbleSuppressionReasons.isEmpty
+    }
 }
 
 private final class AppGridIconNSView: NSView {
@@ -828,6 +1105,7 @@
     private var sourceTag = ""
     private var iconSize: CGFloat = AppDefaults.iconSize
     private var showName = true
+    private var isMouseInside = false
     private var isHovered = false
     private var trackingAreaRef: NSTrackingArea?
     private var mouseDownEvent: NSEvent?
@@ -836,13 +1114,11 @@
     private var longPressWorkItem: DispatchWorkItem?
 
     var runtimeState = AppGridIconRuntimeState(
-        bubbleDisabled: false,
+        bubbleSuppressionReasons: [],
         showUncommonAppBubbles: AppDefaults.showUncommonAppBubbles
     ) {
         didSet {
-            if runtimeState.bubbleDisabled {
-                setHover(false, notify: true)
-            }
+            updateHoverPresentation(notify: true)
         }
     }
 
@@ -861,7 +1137,7 @@
         self.showName = showName
         self.coordinator = coordinator
         runtimeState = AppGridIconRuntimeState(
-            bubbleDisabled: coordinator.bubbleDisabled,
+            bubbleSuppressionReasons: coordinator.bubbleSuppressionReasons,
             showUncommonAppBubbles: coordinator.showUncommonAppBubbles
         )
         needsDisplay = true
@@ -872,6 +1148,7 @@
         if isHovered {
             setHover(false, notify: true)
         }
+        isMouseInside = false
         mouseDownEvent = nil
         didStartDrag = false
         isLongPressActive = false
@@ -935,17 +1212,16 @@
                 .foregroundColor: NSColor.labelColor.withAlphaComponent(alpha),
                 .paragraphStyle: paragraph
             ]
-            app.name.draw(with: labelRect, options: [.usesLineFragmentOrigin], attributes: attributes)
+            app.displayName.draw(with: labelRect, options: [.usesLineFragmentOrigin], attributes: attributes)
         }
     }
 
     override func mouseEntered(with event: NSEvent) {
-        guard !runtimeState.bubbleDisabled else { return }
-        setHover(true, notify: true)
+        setPointerInside(true, notify: true)
     }
 
     override func mouseExited(with event: NSEvent) {
-        setHover(false, notify: true)
+        setPointerInside(false, notify: true)
     }
 
     override func mouseDown(with event: NSEvent) {
@@ -956,7 +1232,7 @@
             guard let self, self.mouseDownEvent != nil else { return }
             self.isLongPressActive = true
             self.coordinator?.onDragModeChange(true)
-            self.setHover(false, notify: true)
+            self.updateHoverPresentation(notify: true, forceHidden: true)
         }
         longPressWorkItem = workItem
         DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: workItem)
@@ -1007,11 +1283,12 @@
     }
 
     override func rightMouseDown(with event: NSEvent) {
-        guard let app else { return }
+        guard !runtimeState.bubbleDisabled, let app else { return }
         coordinator?.onEditNote(app, rootLocalFrame())
     }
 
     override func menu(for event: NSEvent) -> NSMenu? {
+        guard !runtimeState.bubbleDisabled else { return nil }
         let menu = NSMenu()
         let item = NSMenuItem(title: tr("appNote.edit"), action: #selector(editNoteFromMenu), keyEquivalent: "")
         item.target = self
@@ -1020,8 +1297,12 @@
     }
 
     @objc private func editNoteFromMenu() {
-        guard let app else { return }
+        guard !runtimeState.bubbleDisabled, let app else { return }
         coordinator?.onEditNote(app, rootLocalFrame())
+    }
+
+    func replayPointerHover(windowPoint: NSPoint) {
+        setPointerInside(bounds.contains(convert(windowPoint, from: nil)), notify: true)
     }
 
     private var shouldShowAppBubble: Bool {
@@ -1050,6 +1331,16 @@
         }
     }
 
+    private func setPointerInside(_ inside: Bool, notify: Bool) {
+        isMouseInside = inside
+        updateHoverPresentation(notify: notify)
+    }
+
+    private func updateHoverPresentation(notify: Bool, forceHidden: Bool = false) {
+        let shouldHover = isMouseInside && !runtimeState.bubbleDisabled && !forceHidden
+        setHover(shouldHover, notify: notify)
+    }
+
     private func makeDragImage() -> NSImage {
         guard let app else { return NSImage(size: .zero) }
         let scale: CGFloat = AppGridCollectionMetrics.hoverScale * 1.5
diff --git a/Apptag/AppGridItem.swift b/Apptag/AppGridItem.swift
index c187acf..5b39626 100644
--- a/Apptag/AppGridItem.swift
+++ b/Apptag/AppGridItem.swift
@@ -1,30 +1,11 @@
 import SwiftUI
 import AppKit
 
-// MARK: - App Grid Item (icon + name card, with Dock-like hover)
+// MARK: - App Grid Shared Types
 
-struct AppGridItem: View {
-    let app: AppInfo
-    let iconSize: CGFloat
-    var showName: Bool = true
-    var sourceTag: String? = nil
-    var dragModeActive: Bool = false
-    var onDragModeChange: ((Bool) -> Void)? = nil
-    var onBubbleHover: ((AppInfo, CGRect, AppBubbleHoverEvent) -> Void)? = nil
-    var onEditNote: ((AppInfo, CGRect) -> Void)? = nil
-    var bubbleDisabled: Bool = false
-    var showUncommonAppBubbles: Bool = AppDefaults.showUncommonAppBubbles
-    var itemID: String
-    var dragResetToken: Int = 0
-    let onSelect: () -> Void
-
-    @State private var interactionFrame: CGRect = .zero
-    @State private var isHovered = false
-
+enum AppGridItemMetrics {
     static let hoverScale: CGFloat = 1.22
     static let labelHeight: CGFloat = 14
-    private static let hoverInAnimation = Animation.easeOut(duration: 0.07)
-    private static let hoverOutAnimation = Animation.easeOut(duration: 0.045)
 
     static func stableWidth(iconSize: CGFloat) -> CGFloat {
         iconSize * hoverScale + 8
@@ -33,166 +14,11 @@
     static func stableHeight(iconSize: CGFloat) -> CGFloat {
         iconSize * hoverScale + labelHeight + 22
     }
-
-    private var iconSlotSize: CGFloat { iconSize * Self.hoverScale }
-    private var labelWidth: CGFloat { iconSize + 20 }
-    private var labelHeight: CGFloat { Self.labelHeight }
-
-    var body: some View {
-        VStack(spacing: 6) {
-            ZStack {
-                DraggableAppIconView(
-                    icon: app.icon,
-                    iconSize: iconSize,
-                    payload: "\(app.path.path)\n\(sourceTag ?? "")",
-                    resetToken: dragResetToken,
-                    onLongPress: { onDragModeChange?(true) },
-                    onDragEnd: { onDragModeChange?(false) },
-                    onClick: onSelect
-                )
-                .frame(width: iconSize, height: iconSize)
-                .scaleEffect(isHovered ? Self.hoverScale : 1.0)
-                .shadow(
-                    color: .black.opacity(isHovered ? 0.35 : 0),
-                    radius: isHovered ? 14 : 0,
-                    y: isHovered ? 8 : 0
-                )
-            }
-            .frame(width: iconSlotSize, height: iconSlotSize)
-
-            Text(app.name)
-                .font(.system(size: 11, weight: .medium))
-                .lineLimit(1)
-                .truncationMode(.tail)
-                .frame(width: labelWidth, height: labelHeight)
-                .opacity(showName ? 1 : (shouldShowHoverName ? 0.85 : 0))
-        }
-        .padding(.vertical, 8)
-        .padding(.horizontal, 4)
-        .frame(width: Self.stableWidth(iconSize: iconSize), height: Self.stableHeight(iconSize: iconSize))
-        .background(
-            AppHoverTrackingView { hovering, frame in
-                interactionFrame = frame
-                if bubbleDisabled || dragModeActive {
-                    setHoverState(false)
-                    onBubbleHover?(app, frame, .exited)
-                    return
-                }
-                if hovering {
-                    setHoverState(true)
-                    onBubbleHover?(app, frame, .entered(canShowBubble: shouldShowAppBubble))
-                } else if isHovered {
-                    setHoverState(false)
-                    onBubbleHover?(app, frame, .exited)
-                } else {
-                    setHoverState(false)
-                }
-            }
-        )
-        .contentShape(Rectangle())
-        .contextMenu {
-            Button(tr("appNote.edit")) {
-                onEditNote?(app, interactionFrame)
-            }
-        }
-        .opacity(dragModeActive ? 0.92 : 1)
-        .animation(.easeOut(duration: 0.08), value: dragModeActive)
-        .onChange(of: dragModeActive) { _, active in
-            if active {
-                setHoverState(false)
-                onBubbleHover?(app, interactionFrame, .exited)
-            }
-        }
-        .onChange(of: bubbleDisabled) { _, disabled in
-            if disabled {
-                setHoverState(false)
-                onBubbleHover?(app, interactionFrame, .exited)
-            }
-        }
-        .onDisappear {
-            if isHovered {
-                isHovered = false
-                onBubbleHover?(app, interactionFrame, .exited)
-            }
-        }
-    }
-
-    private var shouldShowAppBubble: Bool {
-        !showUncommonAppBubbles || app.isUncommon
-    }
-
-    private var shouldShowHoverName: Bool {
-        !showName && !shouldShowAppBubble && isHovered
-    }
-
-    private func setHoverState(_ hovering: Bool) {
-        guard isHovered != hovering else { return }
-        withAnimation(hovering ? Self.hoverInAnimation : Self.hoverOutAnimation) {
-            isHovered = hovering
-        }
-    }
 }
 
 enum AppBubbleHoverEvent {
     case entered(canShowBubble: Bool)
     case exited
-}
-
-struct AppHoverTrackingView: NSViewRepresentable {
-    let onHover: (Bool, CGRect) -> Void
-
-    func makeNSView(context: Context) -> AppHoverTrackingNSView {
-        let view = AppHoverTrackingNSView()
-        view.onHover = onHover
-        return view
-    }
-
-    func updateNSView(_ view: AppHoverTrackingNSView, context: Context) {
-        view.onHover = onHover
-    }
-}
-
-final class AppHoverTrackingNSView: NSView {
-    var onHover: ((Bool, CGRect) -> Void)?
-
-    override func updateTrackingAreas() {
-        super.updateTrackingAreas()
-        trackingAreas.forEach(removeTrackingArea)
-        addTrackingArea(
-            NSTrackingArea(
-                rect: .zero,
-                options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect],
-                owner: self,
-                userInfo: nil
-            )
-        )
-    }
-
-    override func hitTest(_ point: NSPoint) -> NSView? {
-        nil
-    }
-
-    override func mouseEntered(with event: NSEvent) {
-        onHover?(true, rootLocalFrame())
-    }
-
-    override func mouseExited(with event: NSEvent) {
-        onHover?(false, rootLocalFrame())
-    }
-
-    private func rootLocalFrame() -> CGRect {
-        guard let contentView = window?.contentView else { return .zero }
-        let rectInContent = contentView.convert(bounds, from: self)
-        let y = contentView.isFlipped
-            ? rectInContent.minY
-            : contentView.bounds.height - rectInContent.maxY
-        return CGRect(
-            x: rectInContent.minX,
-            y: y,
-            width: rectInContent.width,
-            height: rectInContent.height
-        )
-    }
 }
 
 enum BubblePlacement {
@@ -299,162 +125,5 @@
         }
         path.closeSubpath()
         return path
-    }
-}
-
-private struct DraggableAppIconView: NSViewRepresentable {
-    let icon: NSImage
-    let iconSize: CGFloat
-    let payload: String
-    let resetToken: Int
-    let onLongPress: () -> Void
-    let onDragEnd: () -> Void
-    let onClick: () -> Void
-
-    func makeNSView(context: Context) -> DragIconNSView {
-        let view = DragIconNSView()
-        view.image = icon
-        view.iconSize = iconSize
-        view.payload = payload
-        view.resetToken = resetToken
-        view.onLongPress = onLongPress
-        view.onDragEnd = onDragEnd
-        view.onClick = onClick
-        return view
-    }
-
-    func updateNSView(_ view: DragIconNSView, context: Context) {
-        let imageChanged = view.image !== icon
-        let sizeChanged = view.iconSize != iconSize
-        view.image = icon
-        view.iconSize = iconSize
-        view.payload = payload
-        if view.resetToken != resetToken {
-            view.resetToken = resetToken
-            view.resetInteractionState()
-        }
-        view.onLongPress = onLongPress
-        view.onDragEnd = onDragEnd
-        view.onClick = onClick
-        if imageChanged || sizeChanged {
-            view.needsDisplay = true
-        }
-    }
-}
-
-private final class DragIconNSView: NSView {
-    var image: NSImage = NSImage()
-    var iconSize: CGFloat = 56
-    var payload: String = ""
-    var resetToken = 0
-    var onLongPress: (() -> Void)?
-    var onDragEnd: (() -> Void)?
-    var onClick: (() -> Void)?
-
-    private var mouseDownEvent: NSEvent?
-    private var didStartDrag = false
-    private var isLongPressActive = false
-    private var longPressWorkItem: DispatchWorkItem?
-    override var isFlipped: Bool { true }
-
-    func resetInteractionState() {
-        longPressWorkItem?.cancel()
-        longPressWorkItem = nil
-        mouseDownEvent = nil
-        didStartDrag = false
-        isLongPressActive = false
-    }
-
-    override func draw(_ dirtyRect: NSRect) {
-        super.draw(dirtyRect)
-        let rect = NSRect(
-            x: (bounds.width - iconSize) / 2,
-            y: (bounds.height - iconSize) / 2,
-            width: iconSize,
-            height: iconSize
-        )
-        image.draw(in: rect)
-    }
-
-    override func mouseDown(with event: NSEvent) {
-        mouseDownEvent = event
-        didStartDrag = false
-        isLongPressActive = false
-        let workItem = DispatchWorkItem { [weak self] in
-            guard let self, self.mouseDownEvent != nil else { return }
-            self.isLongPressActive = true
-            self.onLongPress?()
-        }
-        longPressWorkItem = workItem
-        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: workItem)
-    }
-
-    override func mouseDragged(with event: NSEvent) {
-        if didStartDrag {
-            AppDragCoordinator.shared.updateDragLocation(
-                screenPoint(for: event),
-                copy: event.modifierFlags.contains(.option)
-            )
-            return
-        }
-
-        guard let mouseDownEvent, isLongPressActive else { return }
-        let dx = event.locationInWindow.x - mouseDownEvent.locationInWindow.x
-        let dy = event.locationInWindow.y - mouseDownEvent.locationInWindow.y
-        guard hypot(dx, dy) > 3 else { return }
-
-        didStartDrag = true
-        longPressWorkItem?.cancel()
-        AppDragCoordinator.shared.beginDrag(
-            image: makeDragImage(),
-            payload: payload,
-            at: screenPoint(for: event),
-            copy: event.modifierFlags.contains(.option),
-            in: window
-        )
-    }
-
-    override func mouseUp(with event: NSEvent) {
-        longPressWorkItem?.cancel()
-        if didStartDrag {
-            AppDragCoordinator.shared.finishDrag(
-                at: screenPoint(for: event),
-                copy: event.modifierFlags.contains(.option)
-            )
-            onDragEnd?()
-        } else if !isLongPressActive {
-            onClick?()
-        } else {
-            onDragEnd?()
-        }
-        didStartDrag = false
-        isLongPressActive = false
-        mouseDownEvent = nil
-        longPressWorkItem = nil
-    }
-
-    private func makeDragImage() -> NSImage {
-        let scale: CGFloat = 1.22 * 1.5
-        let imageSize = iconSize * scale
-        let padding = iconSize * 0.45
-        let canvasSize = NSSize(width: imageSize + padding * 2, height: imageSize + padding * 2)
-        let dragImage = NSImage(size: canvasSize)
-
-        dragImage.lockFocus()
-        NSGraphicsContext.current?.imageInterpolation = .high
-
-        let shadow = NSShadow()
-        shadow.shadowColor = NSColor.black.withAlphaComponent(0.48)
-        shadow.shadowBlurRadius = 24
-        shadow.shadowOffset = NSSize(width: 0, height: -14)
-        shadow.set()
-
-        image.draw(in: NSRect(x: padding, y: padding, width: imageSize, height: imageSize))
-        dragImage.unlockFocus()
-        return dragImage
-    }
-
-    private func screenPoint(for event: NSEvent) -> NSPoint {
-        window?.convertPoint(toScreen: event.locationInWindow) ?? NSEvent.mouseLocation
     }
 }
diff --git a/Apptag/ApptagApp.swift b/Apptag/ApptagApp.swift
index c030bc6..b78c05a 100644
--- a/Apptag/ApptagApp.swift
+++ b/Apptag/ApptagApp.swift
@@ -25,16 +25,21 @@
 final class OverlayPanel: NSPanel {
     override var canBecomeKey: Bool { true }
     override var canBecomeMain: Bool { true }
+
+    override func constrainFrameRect(_ frameRect: NSRect, to screen: NSScreen?) -> NSRect {
+        frameRect
+    }
 }
 
-final class AppDelegate: NSObject, NSApplicationDelegate {
+final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
     private static let showDockIconKey = "showDockIcon"
     private static let statusItemAutosaveName = AppIdentity.statusItemAutosaveName
     private static let statusItemButtonIdentifier = NSUserInterfaceItemIdentifier("TagLauncherStatusItemButton")
     private static let statusItemAccessibilityLabel = AppIdentity.displayName
     private static let showAppListMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherShowAppListMenuItem")
-    private static let overlayDefaultLevel = NSWindow.Level.normal
-    private static let overlayTextInputLevel = NSWindow.Level.normal
+    private static let launcherOverlayLevel = NSWindow.Level(rawValue: NSWindow.Level.mainMenu.rawValue - 1)
+    private static let overlayDefaultLevel = launcherOverlayLevel
+    private static let overlayTextInputLevel = launcherOverlayLevel
 
     private var statusItem: NSStatusItem?
     private var overlayWindow: NSWindow?
@@ -50,6 +55,14 @@
     private var isEditingAppNote = false
     private var isConfiguringApplicationMenu = false
     private var lastShowDockIcon: Bool?
+    private var statusMenuScreenForNextOverlay: NSScreen?
+    private var overlayGeneration = 0
+    private var suppressReopenUntil = Date.distantPast
+
+    private struct OverlayPlacementContext {
+        let screen: NSScreen
+        let frame: NSRect
+    }
 
     private var isOverlayVisible: Bool {
         overlayWindow?.isVisible == true
@@ -103,11 +116,17 @@
         observeChromeSettings()
         observeLanguageChanges()
         setupLaunchAtLogin()
-        configureApplicationMenuWhenAvailable()
+        suppressReopenUntil = Date().addingTimeInterval(1.0)
+        configureApplicationMenuWhenAvailable(retries: 200)
+    }
+
+    func applicationDidBecomeActive(_ notification: Notification) {
+        configureApplicationMenuWhenAvailable(retries: 12)
     }
 
     /// Dock icon click → show overlay (same as menubar "Show TagLauncher")
     func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
+        guard Date() >= suppressReopenUntil else { return false }
         showOverlay()
         return false  // Suppress default "unhide all windows" behavior
     }
@@ -163,13 +182,61 @@
         }
     }
 
-    private func beginLauncherForegroundOwnership(activate: Bool = true) {
+    private func beginLauncherForegroundOwnership(activate: Bool = true, keyWindow: NSWindow? = nil) {
         if NSApp.activationPolicy() != .regular {
             NSApp.setActivationPolicy(.regular)
         }
         if activate {
-            NSApp.activate(ignoringOtherApps: true)
-            configureApplicationMenuWhenAvailable(retries: 4)
+            claimLauncherForeground(keyWindow: keyWindow)
+        }
+    }
+
+    private func claimLauncherForeground(
+        keyWindow: NSWindow? = nil,
+        retries: Int = 0,
+        overlayGeneration expectedOverlayGeneration: Int? = nil
+    ) {
+        if NSApp.activationPolicy() != .regular {
+            NSApp.setActivationPolicy(.regular)
+        }
+
+        if isOverlayVisible && !NSApp.presentationOptions.contains(.hideDock) {
+            NSApp.presentationOptions = [.hideDock]
+        }
+
+        NSApp.unhide(nil)
+        NSApp.activate(ignoringOtherApps: true)
+
+        if let keyWindow, keyWindow.isVisible {
+            keyWindow.makeKeyAndOrderFront(nil)
+            keyWindow.makeMain()
+            keyWindow.orderFrontRegardless()
+        }
+        configureApplicationMenuWhenAvailable(retries: 4)
+
+        let overlayShouldYieldToSettings = keyWindow == overlayWindow && isSettingsVisible
+        let keyWindowStillNeedsFocus = !overlayShouldYieldToSettings
+            && keyWindow?.isVisible == true
+            && keyWindow?.isKeyWindow == false
+        let shouldRetry = !NSApp.isActive
+            || !NSApp.presentationOptions.contains(.hideDock)
+            || keyWindowStillNeedsFocus
+        guard retries > 0, isOverlayVisible else { return }
+        guard shouldRetry else { return }
+
+        let retryWindow = keyWindow
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) { [weak self] in
+            guard let self, self.isOverlayVisible else { return }
+            if let expectedOverlayGeneration,
+               expectedOverlayGeneration != self.overlayGeneration {
+                return
+            }
+            self.refreshLauncherChromeState()
+            self.claimLauncherForeground(
+                keyWindow: retryWindow,
+                retries: retries - 1,
+                overlayGeneration: expectedOverlayGeneration
+            )
         }
     }
 
@@ -190,8 +257,12 @@
         }
 
         if activate && requiresForegroundOwnership {
-            NSApp.activate(ignoringOtherApps: true)
-            configureApplicationMenuWhenAvailable(retries: 4)
+            let keyWindow = isSettingsVisible ? settingsWindow : (isOverlayVisible ? overlayWindow : nil)
+            claimLauncherForeground(
+                keyWindow: keyWindow,
+                retries: isOverlayVisible && !isSettingsVisible ? 5 : 0,
+                overlayGeneration: isOverlayVisible ? overlayGeneration : nil
+            )
         }
     }
 
@@ -200,13 +271,12 @@
             NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
         }
 
-        guard isOverlayVisible else {
-            refreshLauncherChromeState()
+        guard !isOverlayVisible else {
             return
         }
-
-        // Once another app takes the menu bar, don't leave the overlay stranded onscreen.
-        hideOverlay(force: true)
+        if !requiresForegroundOwnership {
+            refreshLauncherChromeState()
+        }
     }
 
     /// Keep app chrome in sync when language changes from any entry point.
@@ -295,14 +365,15 @@
             button.imageScaling = .scaleProportionallyDown
             button.imagePosition = .imageOnly
             button.toolTip = "TagLauncher — Tag-based app launcher"
-            button.action = #selector(toggleOverlay)
+            button.action = #selector(toggleOverlay(_:))
             button.target = self
         }
 
         let menu = NSMenu()
+        menu.delegate = self
         let showItem = NSMenuItem(
             title: showAppListMenuTitle,
-            action: #selector(toggleOverlay),
+            action: #selector(toggleOverlayFromStatusMenu(_:)),
             keyEquivalent: ""
         )
         showItem.target = self
@@ -323,7 +394,7 @@
         menu.addItem(.separator())
         let prefsItem = NSMenuItem(
             title: tr("menu.preferences"),
-            action: #selector(openPreferences),
+            action: #selector(openPreferences(_:)),
             keyEquivalent: ","
         )
         prefsItem.target = self
@@ -353,6 +424,19 @@
             )
         )
         statusItem.menu = menu
+    }
+
+    func menuWillOpen(_ menu: NSMenu) {
+        statusMenuScreenForNextOverlay = screenContainingCurrentPointer()
+            ?? statusItem?.button?.window?.screen
+            ?? NSScreen.main
+            ?? NSScreen.screens.first
+    }
+
+    func menuDidClose(_ menu: NSMenu) {
+        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
+            self?.statusMenuScreenForNextOverlay = nil
+        }
     }
 
     private func makeMenuBarIcon() -> NSImage {
@@ -518,7 +602,7 @@
 
     private func configureShowAppListItem(_ item: NSMenuItem) {
         item.title = showAppListMenuTitle
-        item.action = #selector(toggleOverlay)
+        item.action = #selector(toggleOverlayFromStatusMenu(_:))
         item.target = self
         item.keyEquivalent = ""
         item.keyEquivalentModifierMask = []
@@ -539,7 +623,7 @@
         }
 
         item.title = tr("menu.preferences")
-        item.action = #selector(openPreferences)
+        item.action = #selector(openPreferences(_:))
         item.target = self
         item.keyEquivalent = ","
         item.keyEquivalentModifierMask = .command
@@ -548,7 +632,7 @@
 
     private func isSettingsMenuItem(_ item: NSMenuItem) -> Bool {
         item.action == Selector(("showSettingsWindow:"))
-            || item.action == #selector(openPreferences)
+            || item.action == #selector(openPreferences(_:))
             || item.title == tr("menu.preferences")
     }
 
@@ -580,78 +664,106 @@
 
     // MARK: - Overlay Window
 
-    @objc private func toggleOverlay() {
-        if overlayWindow?.isVisible == true {
-            hideOverlay(force: true)
-        } else {
-            showOverlay()
+    @objc func toggleOverlay(_ sender: Any) {
+        performToggleOverlay(preferredScreen: nil)
+    }
+
+    @objc func toggleOverlayFromStatusMenu(_ sender: Any) {
+        let preferredScreen = statusMenuScreenForNextOverlay
+        statusMenuScreenForNextOverlay = nil
+        DispatchQueue.main.async { [weak self] in
+            self?.performToggleOverlay(preferredScreen: preferredScreen)
         }
     }
 
-    private func showOverlay(initialQuickSearchSource: String? = nil) {
-        // Use the screen under the mouse cursor — works in fullscreen spaces
-        let mousePoint = NSEvent.mouseLocation
-        guard let screen = NSScreen.screens.first(where: {
-            NSMouseInRect(mousePoint, $0.frame, false)
-        }) ?? NSScreen.main ?? NSScreen.screens.first else { return }
-
-        let window: NSWindow
-        let createdWindow: Bool
-        if let existingWindow = overlayWindow {
-            window = existingWindow
-            createdWindow = false
+    private func performToggleOverlay(preferredScreen: NSScreen?) {
+        if overlayWindow?.isVisible == true {
+            hideOverlay(force: true)
         } else {
-            window = makeOverlayWindow(on: screen, initialQuickSearchSource: initialQuickSearchSource)
-            overlayWindow = window
-            createdWindow = true
+            showOverlay(preferredScreen: preferredScreen)
         }
+    }
 
-        installOverlayKeyMonitor()
-        beginLauncherForegroundOwnership()
+    private func showOverlay(
+        initialQuickSearchSource: String? = nil,
+        preferredScreen: NSScreen? = nil,
+        stagedForAllSpaces: Bool = false
+    ) {
+        guard let placement = overlayPlacementContextForNextOverlay(preferredScreen: preferredScreen) else { return }
 
-        let targetFrame = screen.frame
-        let targetLevel = overlayLevel(initialQuickSearchSource: initialQuickSearchSource)
-        let canReuseVisibleWindow = !createdWindow
-            && window.isVisible
-            && NSEqualRects(window.frame, targetFrame)
-            && window.level == targetLevel
-
-        if canReuseVisibleWindow {
-            if !window.isKeyWindow {
-                window.makeKeyAndOrderFront(nil)
+        if !stagedForAllSpaces && !isSettingsVisible && NSApp.isActive {
+            // Let the all-spaces panel attach to the pointer's display before the app reclaims focus.
+            if NSApp.activationPolicy() != .accessory {
+                NSApp.setActivationPolicy(.accessory)
             }
-            refreshLauncherChromeState(activate: true)
-            if let initialQuickSearchSource {
-                NotificationCenter.default.post(
-                    name: .tagLauncherQuickSearchRequested,
-                    object: nil,
-                    userInfo: ["source": initialQuickSearchSource]
+            NSApp.deactivate()
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) { [weak self] in
+                self?.showOverlay(
+                    initialQuickSearchSource: initialQuickSearchSource,
+                    preferredScreen: placement.screen,
+                    stagedForAllSpaces: true
                 )
             }
             return
         }
 
-        window.setFrame(targetFrame, display: true)
-        window.level = targetLevel
-
-        if let initialQuickSearchSource, !createdWindow {
-            NotificationCenter.default.post(
-                name: .tagLauncherQuickSearchRequested,
-                object: nil,
-                userInfo: ["source": initialQuickSearchSource]
-            )
+        if overlayWindow?.isVisible == true {
+            hideOverlay(force: true)
+        } else if let existingWindow = overlayWindow {
+            existingWindow.orderOut(nil)
+            overlayWindow = nil
         }
+
+        let window = makeOverlayWindow(
+            on: placement.screen,
+            initialQuickSearchSource: initialQuickSearchSource
+        )
+        overlayGeneration &+= 1
+        overlayWindow = window
+        installOverlayKeyMonitor()
+        if !isSettingsVisible {
+            if NSApp.isActive {
+                NSApp.deactivate()
+            }
+            if NSApp.activationPolicy() != .accessory {
+                NSApp.setActivationPolicy(.accessory)
+            }
+        }
+
+        let targetLevel = overlayLevel(initialQuickSearchSource: initialQuickSearchSource)
+        window.setFrame(placement.frame, display: true)
+        window.level = targetLevel
 
         window.makeKeyAndOrderFront(nil)
         window.orderFrontRegardless()
-        refreshLauncherChromeState(activate: true)
-        NotificationCenter.default.post(name: .tagLauncherOverlayDidShow, object: nil)
+
+        let placementFrame = placement.frame
+        let finishForegroundClaim: () -> Void = { [weak self, weak window] in
+            guard let self, let window, self.overlayWindow === window else { return }
+            self.refreshLauncherChromeState(activate: true)
+            if window.frame != placementFrame {
+                window.setFrame(placementFrame, display: true)
+                window.makeKeyAndOrderFront(nil)
+                window.orderFrontRegardless()
+            }
+            if let settingsWindow = self.settingsWindow, settingsWindow.isVisible {
+                self.prepareSettingsWindow(settingsWindow)
+            }
+            NotificationCenter.default.post(name: .tagLauncherOverlayDidShow, object: nil)
+        }
+
+        if !isSettingsVisible && stagedForAllSpaces {
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.12, execute: finishForegroundClaim)
+        } else {
+            finishForegroundClaim()
+        }
     }
 
     private func installOverlayKeyMonitor() {
         guard overlayKeyMonitor == nil else { return }
         overlayKeyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
             guard let self else { return event }
+            guard self.shouldHandleOverlayKeyEvent(event) else { return event }
             if event.keyCode == 53 { // Escape
                 if self.isQuickSearchOpen {
                     NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
@@ -670,6 +782,12 @@
             }
             return event
         }
+    }
+
+    private func shouldHandleOverlayKeyEvent(_ event: NSEvent) -> Bool {
+        guard overlayWindow?.isVisible == true else { return false }
+        if event.window == overlayWindow { return true }
+        return event.window == nil && NSApp.keyWindow == overlayWindow
     }
 
     private func shouldOpenQuickSearch(for event: NSEvent) -> Bool {
@@ -715,21 +833,22 @@
     private func makeOverlayWindow(on screen: NSScreen, initialQuickSearchSource: String? = nil) -> NSWindow {
         let panel = OverlayPanel(
             contentRect: screen.frame,
-            styleMask: [.borderless, .fullSizeContentView],
+            styleMask: [.borderless, .fullSizeContentView, .nonactivatingPanel],
             backing: .buffered,
             defer: false
         )
         panel.isFloatingPanel = true
         panel.hidesOnDeactivate = false
+        // This combination keeps the overlay on the pointer display, including fullscreen Spaces.
         panel.collectionBehavior = [
-            .moveToActiveSpace,
+            .canJoinAllSpaces,
             .fullScreenAuxiliary,
             .stationary,
             .transient,
             .ignoresCycle
         ]
         panel.isOpaque = false
-        panel.backgroundColor = .clear
+        panel.backgroundColor = NSColor.black.withAlphaComponent(0.001)
         panel.hasShadow = false
         panel.titlebarAppearsTransparent = true
         panel.titleVisibility = .hidden
@@ -750,6 +869,7 @@
 
     private func hideOverlay(force: Bool = false, discardWindow: Bool = false) {
         guard force || !isInEditMode else { return }
+        overlayGeneration &+= 1
         TagDatabase.flushPendingCategorySchemeBackupBatch()
         if let settingsWindow, settingsWindow.parent == overlayWindow {
             detachSettingsWindow(settingsWindow)
@@ -856,12 +976,21 @@
         if id == LauncherHotkeyKind.quickSearch.eventID {
             showQuickSearchFromGlobalHotkey()
         } else {
-            toggleOverlay()
+            performToggleOverlay(preferredScreen: nil)
         }
     }
 
     private func showQuickSearchFromGlobalHotkey() {
-        guard overlayWindow?.isVisible != true else { return }
+        if overlayWindow?.isVisible == true {
+            guard !isSettingsVisible else { return }
+            refreshLauncherChromeState(activate: true)
+            NotificationCenter.default.post(
+                name: .tagLauncherQuickSearchRequested,
+                object: nil,
+                userInfo: ["source": QuickSearchOpenSource.globalVisible]
+            )
+            return
+        }
         showOverlay(initialQuickSearchSource: QuickSearchOpenSource.globalHidden)
     }
 
@@ -901,6 +1030,10 @@
                   keyWindow != self.overlayWindow,
                   !self.isInEditMode
             else { return }
+
+            if self.isSettingsOwnedPanel(keyWindow) {
+                return
+            }
 
             // Settings/Preferences window -> float it above overlay for real-time preview.
             if self.isSettingsWindowCandidate(keyWindow) {
@@ -967,6 +1100,15 @@
         return true
     }
 
+    private func isSettingsOwnedPanel(_ window: NSWindow) -> Bool {
+        guard window is NSPanel else { return false }
+        if window is NSSavePanel { return true }
+        guard let settingsWindow else { return false }
+        return window.sheetParent == settingsWindow
+            || settingsWindow.attachedSheet == window
+            || window.parent == settingsWindow
+    }
+
     private func center(_ window: NSWindow, over rect: NSRect) {
         let frame = window.frame
         let origin = NSPoint(
@@ -977,10 +1119,24 @@
     }
 
     private func screenUnderMouse() -> NSScreen? {
+        overlayPlacementContextForNextOverlay(preferredScreen: nil)?.screen
+    }
+
+    private func overlayPlacementContextForNextOverlay(preferredScreen: NSScreen?) -> OverlayPlacementContext? {
+        if let screen = preferredScreen {
+            return OverlayPlacementContext(screen: screen, frame: screen.frame)
+        }
+        guard let screen = screenContainingCurrentPointer() ?? NSScreen.main ?? NSScreen.screens.first else {
+            return nil
+        }
+        return OverlayPlacementContext(screen: screen, frame: screen.frame)
+    }
+
+    private func screenContainingCurrentPointer() -> NSScreen? {
         let mousePoint = NSEvent.mouseLocation
         return NSScreen.screens.first(where: {
             NSMouseInRect(mousePoint, $0.frame, false)
-        }) ?? NSScreen.main ?? NSScreen.screens.first
+        })
     }
 
     /// Clean up settingsWindow reference when the Settings window closes.
@@ -1011,7 +1167,7 @@
         }
     }
 
-    /// Lower the overlay while text input is active so IME and cursor services are not hidden behind it.
+    /// Keep text-input overlays at the launcher level so Quick Search stays visible in fullscreen Spaces.
     private func observeAppNoteEditing() {
         NotificationCenter.default.addObserver(
             forName: .tagLauncherAppNoteEditingChanged,
@@ -1020,8 +1176,18 @@
         ) { [weak self] notification in
             guard let self else { return }
             self.isEditingAppNote = (notification.userInfo?["active"] as? Bool) ?? false
+            if self.isEditingAppNote {
+                self.promoteOverlayToForegroundInput()
+            }
             self.updateOverlayLevelForTextInput()
         }
+    }
+
+    private func promoteOverlayToForegroundInput() {
+        beginLauncherForegroundOwnership()
+        guard let overlayWindow else { return }
+        overlayWindow.makeKeyAndOrderFront(nil)
+        overlayWindow.orderFrontRegardless()
     }
 
     private func observeQuickSearch() {
@@ -1032,6 +1198,9 @@
         ) { [weak self] notification in
             guard let self else { return }
             self.isQuickSearchOpen = (notification.userInfo?["active"] as? Bool) ?? false
+            if self.isQuickSearchOpen {
+                self.promoteOverlayToForegroundInput()
+            }
             self.updateOverlayLevelForTextInput()
             if self.isQuickSearchOpen {
                 self.installQuickSearchExternalMouseMonitor()
@@ -1095,7 +1264,7 @@
         }
     }
 
-    @objc private func openPreferences() {
+    @objc private func openPreferences(_ sender: Any? = nil) {
         TagDatabase.flushPendingCategorySchemeBackupBatch()
         beginLauncherForegroundOwnership()
         // Don't hide overlay — keep it visible for real-time setting preview.
@@ -1133,29 +1302,46 @@
 
 final class DismissibleHostingView<Content: View>: NSHostingView<Content> {
     private let onBackdropTap: () -> Void
-    private var suppressBackdropDismiss = false
+    private var modalInteractionSuppressesBackdropDismiss = false
+    private var quickSearchSuppressesBackdropDismiss = false
     private var modalInteractionObserver: NSObjectProtocol?
+    private var quickSearchVisibilityObserver: NSObjectProtocol?
+
+    private var suppressBackdropDismiss: Bool {
+        modalInteractionSuppressesBackdropDismiss || quickSearchSuppressesBackdropDismiss
+    }
 
     @MainActor required init(rootView: Content) {
         self.onBackdropTap = {}
         super.init(rootView: rootView)
-        observeModalInteractionChanges()
+        installWindowServerAnchorLayer()
+        observeBackdropDismissSuppressionChanges()
     }
 
     init(rootView: Content, onBackdropTap: @escaping () -> Void) {
         self.onBackdropTap = onBackdropTap
         super.init(rootView: rootView)
-        observeModalInteractionChanges()
+        installWindowServerAnchorLayer()
+        observeBackdropDismissSuppressionChanges()
     }
 
     deinit {
         if let modalInteractionObserver {
             NotificationCenter.default.removeObserver(modalInteractionObserver)
         }
+        if let quickSearchVisibilityObserver {
+            NotificationCenter.default.removeObserver(quickSearchVisibilityObserver)
+        }
     }
 
     @available(*, unavailable)
     required init?(coder: NSCoder) { fatalError() }
+
+    private func installWindowServerAnchorLayer() {
+        wantsLayer = true
+        // A near-transparent backing pixel makes the WindowServer publish the panel immediately.
+        layer?.backgroundColor = NSColor.black.withAlphaComponent(0.001).cgColor
+    }
 
     override func mouseDown(with event: NSEvent) {
         let location = convert(event.locationInWindow, from: nil)
@@ -1164,6 +1350,10 @@
             return
         }
         if hit == self {
+            if quickSearchSuppressesBackdropDismiss {
+                NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
+                return
+            }
             if suppressBackdropDismiss {
                 super.mouseDown(with: event)
                 return
@@ -1190,13 +1380,21 @@
         super.mouseDown(with: event)
     }
 
-    private func observeModalInteractionChanges() {
+    private func observeBackdropDismissSuppressionChanges() {
         modalInteractionObserver = NotificationCenter.default.addObserver(
             forName: .tagLauncherModalInteractionChanged,
             object: nil,
             queue: .main
         ) { [weak self] notification in
-            self?.suppressBackdropDismiss = (notification.userInfo?["active"] as? Bool) ?? false
+            self?.modalInteractionSuppressesBackdropDismiss = (notification.userInfo?["active"] as? Bool) ?? false
+        }
+
+        quickSearchVisibilityObserver = NotificationCenter.default.addObserver(
+            forName: .tagLauncherQuickSearchVisibilityChanged,
+            object: nil,
+            queue: .main
+        ) { [weak self] notification in
+            self?.quickSearchSuppressesBackdropDismiss = (notification.userInfo?["active"] as? Bool) ?? false
         }
     }
 
diff --git a/Apptag/ContentView.swift b/Apptag/ContentView.swift
index 7ba68bf..c163918 100644
--- a/Apptag/ContentView.swift
+++ b/Apptag/ContentView.swift
@@ -1,6 +1,5 @@
 import SwiftUI
 import AppKit
-import UniformTypeIdentifiers
 
 // MARK: - Notification for manual re-index
 
@@ -315,6 +314,7 @@
     let id = UUID()
     let app: AppInfo
     let assignedTags: [String]
+    let removableTags: [String]
 }
 
 private enum SmartStartNoticeMode {
@@ -339,11 +339,7 @@
     @State private var allApps: [AppInfo] = []
     @State private var displayGroups: [TagGroup] = []
     @State private var tagColors: [String: Int] = [:]
-    @State private var scrollProxy: ScrollViewProxy? = nil
-    @StateObject private var scrollInteractionState = AppGridScrollInteractionState()
     @State private var groupLayoutVersion = 0
-    @State private var cachedGridContainerRowsKey: GridContainerRowsKey? = nil
-    @State private var cachedGridContainerRows: [GridContainerLayoutRow] = []
     @State private var appGridScrollTargetID: String? = nil
     @State private var appGridScrollRequestToken = 0
 
@@ -361,7 +357,6 @@
     @State private var tagNavDragItem: String? = nil
     @State private var tagNavReorderFrames: [String: CGRect] = [:]
     @State private var tagNavReorderDidMove = false
-    @State private var hoveredContainer: String? = nil  // colored container lift
     // Fixed interaction for "Colorless Container": hover fills persistently; click clears.
     @State private var filledColorlessContainer: String? = nil
     @State private var appDragModeActive = false
@@ -417,15 +412,9 @@
     private let floatingControlsTrailingInset: CGFloat = 20
     private let floatingControlsReservedWidth: CGFloat = 120
     private var appBubbleDisabled: Bool {
-        appDragModeActive || pendingUncategorizedDrop != nil || scrollInteractionState.isFrozen
+        appDragModeActive || pendingUncategorizedDrop != nil
     }
     private let rightSidebarFloatingClearance: CGFloat = 44
-
-    private var cardSurfaceColor: Color {
-        colorScheme == .dark
-            ? Color.white.opacity(0.055)
-            : Color.white.opacity(0.62)
-    }
 
     private var floatingButtonSurfaceColor: Color {
         colorScheme == .dark
@@ -445,23 +434,19 @@
         displayMode == "container" || displayMode == "gridContainer"
     }
 
-    private var isColoredContainerMode: Bool {
-        displayMode == "coloredContainer" || displayMode == "coloredGridContainer"
-    }
-
-    private var usesAppKitContainerGrid: Bool {
-        displayMode == "gridContainer" || displayMode == "coloredGridContainer"
+    private var shouldRenderAppGridBehindQuickSearch: Bool {
+        !quickSearchVisible || !quickSearchCloseHidesOverlay
     }
 
     var body: some View {
         ZStack {
-            if !quickSearchVisible {
+            if shouldRenderAppGridBehindQuickSearch {
                 VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
                     .ignoresSafeArea()
                     .allowsHitTesting(false)
             }
 
-            if !quickSearchVisible {
+            if shouldRenderAppGridBehindQuickSearch {
                 if notchHeight > 0 {
                     VStack {
                         Rectangle().fill(.black)
@@ -489,7 +474,7 @@
 
             quickSearchOverlay
 
-            if !quickSearchVisible, let message = dropWarningToast {
+            if shouldRenderAppGridBehindQuickSearch, let message = dropWarningToast {
                 Text(message)
                     .font(.system(size: 16, weight: .semibold))
                     .foregroundStyle(.primary)
@@ -504,7 +489,7 @@
                     .allowsHitTesting(false)
             }
 
-            if !quickSearchVisible && dropRefreshVisible {
+            if shouldRenderAppGridBehindQuickSearch && dropRefreshVisible {
                 Color.black.opacity(0.08)
                     .ignoresSafeArea()
                     .transition(.opacity)
@@ -955,7 +940,7 @@
                 Spacer()
                 ProgressView().scaleEffect(0.8)
                 Spacer()
-            } else if usesAppKitContainerGrid {
+            } else {
                 AppGridCollectionView(
                     groups: displayGroups,
                     tagColors: tagColors,
@@ -975,17 +960,13 @@
                         dropApp(path: path, sourceTag: source, targetTag: target, copy: copy)
                     },
                     onGroupActivate: { groupName in
-                        if displayMode == "gridContainer" {
+                        if isColorlessContainerMode {
                             toggleColorlessFill(groupName)
                         }
                     },
                     onScrollActivity: handleAppGridScrollActivity,
                     onDragModeChange: { setAppDragMode($0) }
                 )
-            } else if displayMode == "container" || displayMode == "coloredContainer" {
-                containerGrid
-            } else {
-                flatGrid
             }
         }
     }
@@ -1007,7 +988,7 @@
                 )
 
                 AppNameBubble(
-                    appName: context.app.name,
+                    appName: context.app.displayName,
                     note: currentNote(for: context.app),
                     isEditing: editing,
                     placement: placement,
@@ -1112,538 +1093,6 @@
         .ignoresSafeArea()
         .allowsHitTesting(smartStartNotice != nil)
     }
-    private var flatGrid: some View {
-        ScrollViewReader { proxy in
-            ScrollView {
-                LazyVStack(alignment: .leading, spacing: 24) {
-                    ForEach(displayGroups) { group in
-                        TagGroupView(
-                            group: group,
-                            onSelectApp: { app in openApp(app) },
-                            tagFontSize: tagFontSize,
-                            iconSize: iconSize,
-                            showNames: !hideAppNames,
-                            dragModeActive: appDragModeActive,
-                            onDragModeChange: { setAppDragMode($0) },
-                            onBubbleHover: handleBubbleHover,
-                            onEditNote: beginEditingBubbleNote,
-                            bubbleDisabled: appBubbleDisabled,
-                            showUncommonAppBubbles: showUncommonAppBubbles,
-                            dragResetToken: appDragResetToken,
-                            onDropApp: { path, source, copy in
-                                dropApp(path: path, sourceTag: source, targetTag: group.name, copy: copy)
-                            }
-                        ).id(group.id)
-                    }
-                }
-                .padding(20)
-                .background(AppGridScrollActivityObserver(onScroll: handleAppGridScrollActivity))
-            }
-            .id(displayMode)  // force rebuild on mode switch
-            .onAppear { scrollProxy = proxy }
-        }
-    }
-
-    private var containerGrid: some View {
-        GeometryReader { geo in
-            let outerPad: CGFloat = 20
-            let gap: CGFloat = 16
-            let available = geo.size.width - outerPad * 2
-            let colW: CGFloat = 280
-            let colCount = max(1, Int((available + gap) / (colW + gap)))
-            let actualColW = (available - gap * CGFloat(colCount - 1)) / CGFloat(colCount)
-
-            let columns = distributeToColumns(groups: displayGroups, colCount: colCount, colWidth: actualColW)
-
-            ScrollViewReader { proxy in
-                ScrollView {
-                    HStack(alignment: .top, spacing: gap) {
-                        ForEach(0..<colCount, id: \.self) { ci in
-                            LazyVStack(spacing: gap) {
-                                ForEach(columns[ci]) { group in
-                                    masonryCard(group, width: actualColW)
-                                        .id(group.id)
-                                }
-                            }
-                        }
-                    }
-                    .padding(outerPad)
-                    .background(AppGridScrollActivityObserver(onScroll: handleAppGridScrollActivity))
-                }
-                .id(displayMode)  // force rebuild on mode switch
-                .onAppear { scrollProxy = proxy }
-            }
-        }
-    }
-
-    /// Distribute groups to the shortest column.
-    private func distributeToColumns(groups: [TagGroup], colCount: Int, colWidth: CGFloat) -> [[TagGroup]] {
-        var cols = Array(repeating: [TagGroup](), count: colCount)
-        var h = Array(repeating: CGFloat(0), count: colCount)
-        for g in groups {
-            let est = estimatedCardHeight(g, width: colWidth)
-            let ci = h.firstIndex(of: h.min()!)!
-            cols[ci].append(g)
-            h[ci] += est + 16
-        }
-        return cols
-    }
-
-    private func estimatedCardHeight(_ group: TagGroup, width: CGFloat) -> CGFloat {
-        let inner = width - 32
-        let itemW = AppGridItem.stableWidth(iconSize: iconSize) + 6
-        let perRow = max(1, Int(inner / itemW))
-        let rows = (group.apps.count + perRow - 1) / perRow
-        return 32
-            + CGFloat(rows) * AppGridItem.stableHeight(iconSize: iconSize)
-            + CGFloat(max(0, rows - 1)) * 2
-    }
-
-    private func masonryCard(_ group: TagGroup, width: CGFloat) -> some View {
-        let isColored = displayMode == "coloredContainer"
-        let isColorless = isColorlessContainerMode
-        let isColorlessFilled = isColorless && filledColorlessContainer == group.name
-        let isHovered = hoveredContainer == group.name
-        let isColorlessActive = isColorless && (isColorlessFilled || isHovered)
-        let tagColor = Color(nsColor: TagColor.nsColor(for: tagColors[group.name] ?? 0))
-        return VStack(alignment: .leading, spacing: 6) {
-            HStack(spacing: 0) {
-                Rectangle().fill(.secondary.opacity(0.25)).frame(height: 1)
-                    .layoutPriority(0)
-                Text(group.name)
-                    .font(.system(size: tagFontSize, weight: .semibold))
-                    .foregroundStyle(.secondary)
-                    .lineLimit(1)
-                    .truncationMode(.middle)
-                    .padding(.horizontal, 10)
-                    .layoutPriority(1)
-                Rectangle().fill(.secondary.opacity(0.25)).frame(height: 1)
-                    .layoutPriority(0)
-            }
-            let itemSize = AppGridItem.stableWidth(iconSize: iconSize)
-            LazyVGrid(
-                columns: [GridItem(.adaptive(minimum: itemSize, maximum: itemSize + 36), spacing: 6)],
-                spacing: 2
-            ) {
-                ForEach(group.apps) { app in
-                    AppGridItem(
-                        app: app,
-                        iconSize: iconSize,
-                        showName: !hideAppNames,
-                        sourceTag: group.name,
-                        dragModeActive: appDragModeActive,
-                        onDragModeChange: { setAppDragMode($0) },
-                        onBubbleHover: handleBubbleHover,
-                        onEditNote: beginEditingBubbleNote,
-                        bubbleDisabled: appBubbleDisabled,
-                        showUncommonAppBubbles: showUncommonAppBubbles,
-                        itemID: "\(group.name)|\(app.path.path)",
-                        dragResetToken: appDragResetToken,
-                        onSelect: { openApp(app) }
-                    )
-                }
-            }
-        }
-        .frame(maxWidth: width)
-        .padding(16)
-        .background(
-            RoundedRectangle(cornerRadius: 14)
-                .fill((isColored || isColorlessActive) ? tagColor.opacity(0.30) : Color.clear)
-                .background(
-                    RoundedRectangle(cornerRadius: 14)
-                        .fill(cardSurfaceColor)
-                )
-        )
-        .overlay(
-            RoundedRectangle(cornerRadius: 14)
-                .stroke(Color.primary.opacity(0.08), lineWidth: 1)
-        )
-        .overlay {
-            if appDragModeActive {
-                AppDropTargetView(targetTag: group.name) { path, source, copy in
-                    dropApp(path: path, sourceTag: source, targetTag: group.name, copy: copy)
-                }
-                .allowsHitTesting(false)
-            }
-        }
-        .shadow(color: .black.opacity((isColored && isHovered) || isColorlessActive ? 0.22 : 0),
-                radius: (isColored && isHovered) || isColorlessActive ? 8 : 0,
-                y: (isColored && isHovered) || isColorlessActive ? 3 : 0)
-        .zIndex(isHovered ? 50 : 0)
-        .animation(.easeOut(duration: 0.045), value: isHovered)
-        .animation(.easeOut(duration: 0.08), value: isColorlessFilled)
-        .background(AppHoverTrackingView { hovering, _ in
-            guard !scrollInteractionState.isFrozen else {
-                if !hovering && hoveredContainer == group.name {
-                    hoveredContainer = nil
-                }
-                return
-            }
-            if isColored || isColorless {
-                hoveredContainer = hovering ? group.name : nil
-            }
-        })
-        .contentShape(RoundedRectangle(cornerRadius: 14))
-        .onTapGesture {
-            if isColorless {
-                toggleColorlessFill(group.name)
-            }
-        }
-        .onDrop(of: [UTType.plainText], isTargeted: nil) { providers in
-            handleAppDrop(providers, targetTag: group.name)
-        }
-    }
-
-    private var gridContainerGrid: some View {
-        GeometryReader { geo in
-            let outerPad: CGFloat = 20
-            let gap: CGFloat = 16
-            let available = geo.size.width - outerPad * 2
-            let preferredCount = preferredGridContainersPerRow(availableWidth: available)
-            let rowsKey = GridContainerRowsKey(
-                groupLayoutVersion: groupLayoutVersion,
-                trackCount: preferredCount,
-                availableWidth: Int(available.rounded()),
-                iconSize: Int(iconSize.rounded())
-            )
-            let rows = gridContainerRowsForRender(
-                key: rowsKey,
-                groups: displayGroups,
-                trackCount: preferredCount,
-                availableWidth: available,
-                gap: gap
-            )
-
-            ScrollViewReader { proxy in
-                ScrollView {
-                    LazyVStack(alignment: .leading, spacing: gap) {
-                        ForEach(Array(rows.indices), id: \.self) { rowIndex in
-                            let row = rows[rowIndex]
-
-                            HStack(alignment: .top, spacing: gap) {
-                                ForEach(Array(row.items.indices), id: \.self) { itemIndex in
-                                    let item = row.items[itemIndex]
-                                    gridContainerCard(item.group, width: item.width, fixedRows: row.fixedRows)
-                                        .id(item.group.id)
-                                }
-                            }
-                        }
-                    }
-                    .padding(outerPad)
-                    .frame(maxWidth: .infinity, alignment: .topLeading)
-                    .background(AppGridScrollActivityObserver(onScroll: handleAppGridScrollActivity))
-                }
-                .id(displayMode)
-                .onAppear { scrollProxy = proxy }
-                .onAppear {
-                    updateCachedGridContainerRows(
-                        key: rowsKey,
-                        groups: displayGroups,
-                        trackCount: preferredCount,
-                        availableWidth: available,
-                        gap: gap
-                    )
-                }
-                .onChange(of: rowsKey) { _, newKey in
-                    updateCachedGridContainerRows(
-                        key: newKey,
-                        groups: displayGroups,
-                        trackCount: preferredCount,
-                        availableWidth: available,
-                        gap: gap
-                    )
-                }
-            }
-        }
-    }
-
-    private func preferredGridContainersPerRow(availableWidth: CGFloat) -> Int {
-        let minCardWidth = max(260, AppGridItem.stableWidth(iconSize: iconSize) * 3 + 64)
-        if availableWidth >= minCardWidth * 3 + 32 { return 3 }
-        if availableWidth >= minCardWidth * 2 + 16 { return 2 }
-        return 1
-    }
-
-    private struct GridContainerLayoutItem {
-        let group: TagGroup
-        let width: CGFloat
-    }
-
-    private struct GridContainerLayoutRow {
-        let items: [GridContainerLayoutItem]
-        let fixedRows: Int
-    }
-
-    private struct GridContainerRowsKey: Equatable {
-        let groupLayoutVersion: Int
-        let trackCount: Int
-        let availableWidth: Int
-        let iconSize: Int
-    }
-
-    private struct GridContainerCandidate {
-        let spans: [Int]
-        let rows: Int
-        let cost: CGFloat
-    }
-
-    private func gridContainerRowsForRender(
-        key: GridContainerRowsKey,
-        groups: [TagGroup],
-        trackCount: Int,
-        availableWidth: CGFloat,
-        gap: CGFloat
-    ) -> [GridContainerLayoutRow] {
-        if cachedGridContainerRowsKey == key {
-            return cachedGridContainerRows
-        }
-        return gridContainerRows(
-            groups: groups,
-            trackCount: trackCount,
-            availableWidth: availableWidth,
-            gap: gap
-        )
-    }
-
-    private func updateCachedGridContainerRows(
-        key: GridContainerRowsKey,
-        groups: [TagGroup],
-        trackCount: Int,
-        availableWidth: CGFloat,
-        gap: CGFloat
-    ) {
-        guard cachedGridContainerRowsKey != key else { return }
-        cachedGridContainerRows = gridContainerRows(
-            groups: groups,
-            trackCount: trackCount,
-            availableWidth: availableWidth,
-            gap: gap
-        )
-        cachedGridContainerRowsKey = key
-    }
-
-    private func gridContainerRows(groups: [TagGroup], trackCount: Int, availableWidth: CGFloat, gap: CGFloat) -> [GridContainerLayoutRow] {
-        let trackCount = max(1, trackCount)
-        let trackWidth = (availableWidth - gap * CGFloat(trackCount - 1)) / CGFloat(trackCount)
-        let patterns = gridContainerSpanPatterns(trackCount: trackCount)
-        let n = groups.count
-        guard n > 0 else { return [] }
-
-        var bestCost = Array(repeating: CGFloat.greatestFiniteMagnitude, count: n + 1)
-        var bestPattern = Array(repeating: [Int](), count: n)
-        bestCost[n] = 0
-
-        for index in stride(from: n - 1, through: 0, by: -1) {
-            for pattern in patterns where index + pattern.count <= n {
-                let candidate = gridContainerCandidate(
-                    groups: groups,
-                    startIndex: index,
-                    spans: pattern,
-                    trackWidth: trackWidth,
-                    availableWidth: availableWidth,
-                    gap: gap
-                )
-                let totalCost = candidate.cost + bestCost[index + pattern.count]
-                if totalCost < bestCost[index] {
-                    bestCost[index] = totalCost
-                    bestPattern[index] = candidate.spans
-                }
-            }
-        }
-
-        var rows: [GridContainerLayoutRow] = []
-        var index = 0
-        while index < n {
-            let spans = bestPattern[index].isEmpty ? [trackCount] : bestPattern[index]
-            let widths = spans.map { gridContainerWidth(trackWidth: trackWidth, span: $0, gap: gap) }
-            let fixedRows = widths.indices.map {
-                iconRows(appCount: groups[index + $0].apps.count, width: widths[$0])
-            }.max() ?? 1
-            let items = widths.indices.map {
-                GridContainerLayoutItem(group: groups[index + $0], width: widths[$0])
-            }
-            rows.append(GridContainerLayoutRow(items: items, fixedRows: fixedRows))
-            index += spans.count
-        }
-        return rows
-    }
-
-    private func gridContainerSpanPatterns(trackCount: Int) -> [[Int]] {
-        switch trackCount {
-        case 3:
-            return [[1, 1, 1], [1, 2], [2, 1], [3]]
-        case 2:
-            return [[1, 1], [2]]
-        default:
-            return [[1]]
-        }
-    }
-
-    private func gridContainerCandidate(
-        groups: [TagGroup],
-        startIndex: Int,
-        spans: [Int],
-        trackWidth: CGFloat,
-        availableWidth: CGFloat,
-        gap: CGFloat
-    ) -> GridContainerCandidate {
-        let widths = spans.map { gridContainerWidth(trackWidth: trackWidth, span: $0, gap: gap) }
-        let rowCounts = widths.indices.map {
-            iconRows(appCount: groups[startIndex + $0].apps.count, width: widths[$0])
-        }
-        let fixedRows = rowCounts.max() ?? 1
-        let rowArea = CGFloat(fixedRows) * iconCellHeight() * availableWidth
-        let paddingCost = CGFloat(spans.count) * 0.001
-        return GridContainerCandidate(spans: spans, rows: fixedRows, cost: rowArea + paddingCost)
-    }
-
-    private func gridContainerWidth(trackWidth: CGFloat, span: Int, gap: CGFloat) -> CGFloat {
-        let span = max(1, span)
-        return trackWidth * CGFloat(span) + gap * CGFloat(span - 1)
-    }
-
-    private func iconColumns(width: CGFloat) -> Int {
-        let inner = width - 32
-        let itemW = AppGridItem.stableWidth(iconSize: iconSize) + 6
-        return max(1, Int((inner + 6) / itemW))
-    }
-
-    private func iconRows(appCount: Int, width: CGFloat) -> Int {
-        let cols = iconColumns(width: width)
-        return max(1, (appCount + cols - 1) / cols)
-    }
-
-    private func gridContainerCard(_ group: TagGroup, width: CGFloat, fixedRows: Int) -> some View {
-        let isColored = displayMode == "coloredGridContainer"
-        let isColorlessGrid = displayMode == "gridContainer"
-        let isColorless = isColorlessContainerMode
-        let isColorlessFilled = isColorless && filledColorlessContainer == group.name
-        let isHovered = hoveredContainer == group.name
-        let isColorlessGridActive = isColorlessGrid && (isColorlessFilled || isHovered)
-        let cols = iconColumns(width: width)
-        let contentWidth = max(1, width - 32)
-        let cellWidth = max(1, (contentWidth - 6 * CGFloat(cols - 1)) / CGFloat(cols))
-        let cellHeight = iconCellHeight()
-        let gridHeight = CGFloat(fixedRows) * cellHeight + CGFloat(max(0, fixedRows - 1)) * 2
-        let rows = appRows(group.apps, columns: cols, fixedRows: fixedRows)
-        let tagColor = Color(nsColor: TagColor.nsColor(for: tagColors[group.name] ?? 0))
-
-        return VStack(alignment: .leading, spacing: 6) {
-            HStack(spacing: 0) {
-                Rectangle().fill(.secondary.opacity(0.25)).frame(height: 1)
-                    .layoutPriority(0)
-                Text(group.name)
-                    .font(.system(size: tagFontSize, weight: .semibold))
-                    .foregroundStyle(.secondary)
-                    .lineLimit(1)
-                    .truncationMode(.middle)
-                    .padding(.horizontal, 10)
-                    .layoutPriority(1)
-                Rectangle().fill(.secondary.opacity(0.25)).frame(height: 1)
-                    .layoutPriority(0)
-            }
-
-            VStack(alignment: .leading, spacing: 2) {
-                ForEach(rows.indices, id: \.self) { rowIndex in
-                    HStack(alignment: .top, spacing: 6) {
-                        ForEach(rows[rowIndex]) { app in
-                            AppGridItem(
-                                app: app,
-                                iconSize: iconSize,
-                                showName: !hideAppNames,
-                                sourceTag: group.name,
-                                dragModeActive: appDragModeActive,
-                                onDragModeChange: { setAppDragMode($0) },
-                                onBubbleHover: handleBubbleHover,
-                                onEditNote: beginEditingBubbleNote,
-                                bubbleDisabled: appBubbleDisabled,
-                                showUncommonAppBubbles: showUncommonAppBubbles,
-                                itemID: "\(group.name)|\(app.path.path)",
-                                dragResetToken: appDragResetToken,
-                                onSelect: { openApp(app) }
-                            )
-                                .frame(width: cellWidth)
-                                .frame(height: cellHeight)
-                        }
-
-                        let emptyCells = max(0, cols - rows[rowIndex].count)
-                        ForEach(0..<emptyCells, id: \.self) { _ in
-                            Color.clear
-                                .frame(width: cellWidth, height: cellHeight)
-                        }
-                    }
-                    .frame(height: cellHeight)
-                }
-            }
-            .frame(height: gridHeight, alignment: .topLeading)
-        }
-        .frame(width: contentWidth)
-        .padding(16)
-        .background(
-            RoundedRectangle(cornerRadius: 14)
-                .fill((isColored || isColorlessGridActive) ? tagColor.opacity(0.30) : Color.clear)
-                .background(
-                    RoundedRectangle(cornerRadius: 14)
-                        .fill(cardSurfaceColor)
-                )
-        )
-        .overlay(
-            RoundedRectangle(cornerRadius: 14)
-                .stroke(Color.primary.opacity(0.08), lineWidth: 1)
-        )
-        .overlay {
-            if appDragModeActive {
-                AppDropTargetView(targetTag: group.name) { path, source, copy in
-                    dropApp(path: path, sourceTag: source, targetTag: group.name, copy: copy)
-                }
-                .allowsHitTesting(false)
-            }
-        }
-        .shadow(color: .black.opacity((isColored && isHovered) || isColorlessGridActive ? 0.22 : 0),
-                radius: (isColored && isHovered) || isColorlessGridActive ? 8 : 0,
-                y: (isColored && isHovered) || isColorlessGridActive ? 3 : 0)
-        .zIndex(isHovered ? 50 : 0)
-        .animation(.easeOut(duration: 0.045), value: isHovered)
-        .animation(.easeOut(duration: 0.08), value: isColorlessFilled)
-        .background(AppHoverTrackingView { hovering, _ in
-            guard !scrollInteractionState.isFrozen else {
-                if !hovering && hoveredContainer == group.name {
-                    hoveredContainer = nil
-                }
-                return
-            }
-            if isColored || isColorlessGrid {
-                hoveredContainer = hovering ? group.name : nil
-            } else if hovering {
-                fillColorlessContainer(group.name)
-            }
-        })
-        .contentShape(RoundedRectangle(cornerRadius: 14))
-        .onTapGesture {
-            if isColorlessGrid {
-                toggleColorlessFill(group.name)
-            }
-        }
-        .onDrop(of: [UTType.plainText], isTargeted: nil) { providers in
-            handleAppDrop(providers, targetTag: group.name)
-        }
-    }
-
-    private func iconCellHeight() -> CGFloat {
-        AppGridItem.stableHeight(iconSize: iconSize)
-    }
-
-    private func appRows(_ apps: [AppInfo], columns: Int, fixedRows: Int) -> [[AppInfo]] {
-        let columns = max(1, columns)
-        let rowCount = max(1, fixedRows)
-        return (0..<rowCount).map { rowIndex in
-            let start = rowIndex * columns
-            guard start < apps.count else { return [] }
-            let end = min(start + columns, apps.count)
-            return Array(apps[start..<end])
-        }
-    }
-
     // MARK: - Edit Tags View
 
     private var editTagsView: some View {
@@ -1768,8 +1217,8 @@
                 columns: [
                     GridItem(
                         .adaptive(
-                            minimum: AppGridItem.stableWidth(iconSize: iconSize),
-                            maximum: AppGridItem.stableWidth(iconSize: iconSize) + 36
+                            minimum: AppGridItemMetrics.stableWidth(iconSize: iconSize),
+                            maximum: AppGridItemMetrics.stableWidth(iconSize: iconSize) + 36
                         ),
                         spacing: 6
                     )
@@ -2227,7 +1676,6 @@
     private func rebuildDisplayGroups(apps: [AppInfo], tagOrder: [String]) {
         displayGroups = makeDisplayGroups(apps: apps, tagOrder: tagOrder)
         groupLayoutVersion &+= 1
-        cachedGridContainerRowsKey = nil
     }
 
     private var tagLabels: [TagLabel] {
@@ -2261,11 +1709,7 @@
     }
 
     private func handleAppGridScrollActivity() {
-        if !scrollInteractionState.isFrozen {
-            hoveredBubble = nil
-            hoveredContainer = nil
-        }
-        scrollInteractionState.noteScroll()
+        hoveredBubble = nil
     }
 
     private func handleBubbleHover(app: AppInfo, frame: CGRect, event: AppBubbleHoverEvent) {
@@ -2396,12 +1840,8 @@
     }
 
     func scrollTo(_ id: String) {
-        if usesAppKitContainerGrid {
-            appGridScrollTargetID = id
-            appGridScrollRequestToken &+= 1
-        } else {
-            withAnimation(.easeInOut(duration: 0.25)) { scrollProxy?.scrollTo(id, anchor: .top) }
-        }
+        appGridScrollTargetID = id
+        appGridScrollRequestToken &+= 1
     }
 
     private func activateTagNavigation(_ id: String) {
@@ -2504,33 +1944,6 @@
         }
     }
 
-    private func handleAppDrop(_ providers: [NSItemProvider], targetTag: String) -> Bool {
-        guard let provider = providers.first(where: { $0.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) }) else {
-            return false
-        }
-        provider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { item, _ in
-            let text: String?
-            if let data = item as? Data {
-                text = String(data: data, encoding: .utf8)
-            } else if let string = item as? String {
-                text = string
-            } else if let string = item as? NSString {
-                text = string as String
-            } else {
-                text = nil
-            }
-            guard let text else { return }
-            let parts = text.components(separatedBy: "\n")
-            guard let path = parts.first, !path.isEmpty else { return }
-            let source = parts.dropFirst().first ?? ""
-            let copy = NSEvent.modifierFlags.contains(.option)
-            DispatchQueue.main.async {
-                dropApp(path: path, sourceTag: source, targetTag: targetTag, copy: copy)
-            }
-        }
-        return true
-    }
-
     private func dropApp(path: String, sourceTag: String, targetTag: String, copy: Bool) {
         resetTransientDragState(keepingPendingUncategorizedDrop: true)
 
@@ -2559,18 +1972,34 @@
 
     private func confirmAndMoveAppToUncategorized(path: String) {
         guard let app = allApps.first(where: { $0.path.path == path }) else { return }
-        let assignedTags = assignedRegularDisplayTags(for: app)
-        guard !assignedTags.isEmpty else { return }
+        let removableTags = removableRegularTags(for: app)
+        guard !removableTags.isEmpty else { return }
+        let assignedTags = assignedRegularDisplayTags(for: removableTags)
 
         clearAppBubbleState()
         withAnimation(.spring(response: 0.24, dampingFraction: 0.84)) {
-            pendingUncategorizedDrop = PendingUncategorizedDrop(app: app, assignedTags: assignedTags)
+            pendingUncategorizedDrop = PendingUncategorizedDrop(
+                app: app,
+                assignedTags: assignedTags,
+                removableTags: removableTags
+            )
         }
     }
 
-    private func assignedRegularDisplayTags(for app: AppInfo) -> [String] {
+    private func removableRegularTags(for app: AppInfo) -> [String] {
         var result: [String] = []
         for tag in app.tags {
+            guard isRemovableRegularTag(tag) else { continue }
+            if !result.contains(tag) {
+                result.append(tag)
+            }
+        }
+        return result
+    }
+
+    private func assignedRegularDisplayTags(for tags: [String]) -> [String] {
+        var result: [String] = []
+        for tag in tags {
             let name = displayTagName(tag)
             if !result.contains(name) {
                 result.append(name)
@@ -2583,7 +2012,7 @@
         formattedFeedbackMessage(
             forKey: "drop.uncategorizedConfirmMessage",
             replacements: [
-                "%appName%": pendingDrop.app.name,
+                "%appName%": pendingDrop.app.displayName,
                 "%tagCount%": "\(pendingDrop.assignedTags.count)",
                 "%tagNames%": pendingDrop.assignedTags.joined(separator: localizedListSeparator)
             ]
@@ -2600,11 +2029,12 @@
     private func confirmPendingUncategorizedDrop() {
         guard let pendingDrop = pendingUncategorizedDrop else { return }
         let path = pendingDrop.app.path.path
-        let tags = pendingDrop.app.tags
+        let tags = pendingDrop.removableTags
         resetTransientDragState(keepingPendingUncategorizedDrop: true)
         withAnimation(.easeOut(duration: 0.16)) {
             pendingUncategorizedDrop = nil
         }
+        guard !tags.isEmpty else { return }
         TagEditor.removeTags(tags, from: [path])
         showDropRefresh()
         refreshApps(forceLayoutRefresh: true)
@@ -2624,6 +2054,18 @@
             tr("group.appleBuiltIn")
         ]
         return defaultNames.contains(targetTag)
+    }
+
+    private func isRemovableRegularTag(_ tag: String) -> Bool {
+        let protectedNames = [
+            "Mac自带",
+            tr("group.appleBuiltIn"),
+            defaultGroupName,
+            tr("group.uncategorized"),
+            TagDatabase.uncommonTagKey,
+            tr("group.uncommon")
+        ]
+        return !protectedNames.contains(tag) && tagColors[tag] != nil
     }
 
     private func showDropWarning() {
@@ -2675,7 +2117,6 @@
     private func resetTransientDragState(keepingPendingUncategorizedDrop: Bool = false) {
         let hadAppDragState = appDragModeActive
         AppDragCoordinator.shared.cancelDrag()
-        scrollInteractionState.reset()
         if appDragModeActive {
             appDragModeActive = false
         }
@@ -2693,9 +2134,6 @@
         }
         if dragItem != nil {
             dragItem = nil
-        }
-        if hoveredContainer != nil {
-            hoveredContainer = nil
         }
         clearAppBubbleState()
         if !keepingPendingUncategorizedDrop, pendingUncategorizedDrop != nil {
@@ -2779,135 +2217,8 @@
     }
 
     func openApp(_ app: AppInfo) {
-        launchApp(app)
-    }
-}
-
-private final class AppGridScrollInteractionState: ObservableObject {
-    @Published var isFrozen = false
-    private var unfreezeWorkItem: DispatchWorkItem?
-
-    func noteScroll() {
-        if !isFrozen {
-            isFrozen = true
-        }
-        unfreezeWorkItem?.cancel()
-        let workItem = DispatchWorkItem { [weak self] in
-            guard let self, self.isFrozen else { return }
-            self.isFrozen = false
-        }
-        unfreezeWorkItem = workItem
-        DispatchQueue.main.asyncAfter(deadline: .now() + 0.18, execute: workItem)
-    }
-
-    func reset() {
-        unfreezeWorkItem?.cancel()
-        unfreezeWorkItem = nil
-        if isFrozen {
-            isFrozen = false
-        }
-    }
-}
-
-private struct AppGridScrollActivityObserver: NSViewRepresentable {
-    let onScroll: () -> Void
-
-    func makeCoordinator() -> Coordinator {
-        Coordinator(onScroll: onScroll)
-    }
-
-    func makeNSView(context: Context) -> ScrollActivityNSView {
-        let view = ScrollActivityNSView()
-        view.coordinator = context.coordinator
-        return view
-    }
-
-    func updateNSView(_ view: ScrollActivityNSView, context: Context) {
-        context.coordinator.onScroll = onScroll
-        view.coordinator = context.coordinator
-    }
-
-    final class Coordinator {
-        var onScroll: () -> Void
-        private weak var observedClipView: NSClipView?
-        private var observer: NSObjectProtocol?
-        private var lastReportedBoundsOrigin: NSPoint?
-
-        init(onScroll: @escaping () -> Void) {
-            self.onScroll = onScroll
-        }
-
-        func install(from view: NSView) {
-            guard let clipView = view.enclosingScrollView?.contentView else { return }
-            guard observedClipView !== clipView else { return }
-            removeObserver()
-            observedClipView = clipView
-            lastReportedBoundsOrigin = clipView.bounds.origin
-            clipView.postsBoundsChangedNotifications = true
-            observer = NotificationCenter.default.addObserver(
-                forName: NSView.boundsDidChangeNotification,
-                object: clipView,
-                queue: .main
-            ) { [weak self, weak clipView] _ in
-                guard let self,
-                      let clipView,
-                      self.recordScrollIfNeeded(from: clipView)
-                else { return }
-                self.onScroll()
-            }
-        }
-
-        private func removeObserver() {
-            if let observer {
-                NotificationCenter.default.removeObserver(observer)
-            }
-            observer = nil
-            observedClipView = nil
-            lastReportedBoundsOrigin = nil
-        }
-
-        private func recordScrollIfNeeded(from clipView: NSClipView) -> Bool {
-            let origin = clipView.bounds.origin
-            guard let last = lastReportedBoundsOrigin else {
-                lastReportedBoundsOrigin = origin
-                return false
-            }
-            let didScroll = abs(origin.x - last.x) > 0.5
-                || abs(origin.y - last.y) > 0.5
-            if didScroll {
-                lastReportedBoundsOrigin = origin
-            }
-            return didScroll
-        }
-
-        deinit {
-            removeObserver()
-        }
-    }
-
-    final class ScrollActivityNSView: NSView {
-        weak var coordinator: Coordinator?
-        private var installScheduled = false
-
-        override func viewDidMoveToWindow() {
-            super.viewDidMoveToWindow()
-            installObserverIfPossible()
-        }
-
-        override func viewDidMoveToSuperview() {
-            super.viewDidMoveToSuperview()
-            installObserverIfPossible()
-        }
-
-        func installObserverIfPossible() {
-            guard !installScheduled else { return }
-            installScheduled = true
-            DispatchQueue.main.async { [weak self] in
-                guard let self else { return }
-                installScheduled = false
-                coordinator?.install(from: self)
-            }
-        }
+        hideOverlay()
+        launchApp(app, closeOverlayOnSuccess: false)
     }
 }
 
diff --git a/Apptag/DataLayer.swift b/Apptag/DataLayer.swift
index d1a09a8..a3fc089 100644
--- a/Apptag/DataLayer.swift
+++ b/Apptag/DataLayer.swift
@@ -11,9 +11,50 @@
     let tags: [String]
     let bundleIdentifier: String?
     let localizedNames: [String]
+    let localizedNamesByLanguage: [String: String]
     let icon: NSImage  // Pre-loaded during background scan
     var isUncommon: Bool = false
     var note: String? = nil
+
+    var displayName: String {
+        localizedDisplayName(for: L10n.currentCode)
+    }
+
+    func localizedDisplayName(for languageCode: String) -> String {
+        let candidates = AppInfo.displayLanguageFallbacks(for: languageCode)
+        for code in candidates {
+            if let localizedName = localizedNamesByLanguage[code],
+               !localizedName.isEmpty {
+                return localizedName
+            }
+        }
+        return name
+    }
+
+    private static func displayLanguageFallbacks(for languageCode: String) -> [String] {
+        var candidates = [languageCode]
+        switch languageCode {
+        case "zh-Hans":
+            candidates.append("zh-Hant")
+        case "zh-Hant":
+            candidates.append("zh-Hans")
+        case "nb":
+            candidates.append("no")
+        case "nn":
+            candidates.append("no")
+        case "no":
+            candidates.append(contentsOf: ["nb", "nn"])
+        default:
+            break
+        }
+        candidates.append("en")
+        return uniqueLanguageCodes(candidates)
+    }
+
+    fileprivate static func uniqueLanguageCodes(_ codes: [String]) -> [String] {
+        var seen = Set<String>()
+        return codes.filter { seen.insert($0).inserted }
+    }
 
     /// True if this is an Apple pre-installed app.
     var isAppleApp: Bool {
@@ -180,10 +221,14 @@
 
         let bundle = Bundle(url: displayURL) ?? Bundle(url: resolvedURL)
         let bundleId = bundle?.bundleIdentifier
-        let localizedNames = localizedAppNames(
-            for: displayURL,
+        let localizedNamesByLanguage = localizedAppNameMap(
             bundle: bundle,
             fallbackName: name
+        )
+        let localizedNames = localizedAppNames(
+            for: displayURL,
+            fallbackName: name,
+            localizedNamesByLanguage: localizedNamesByLanguage
         )
 
         apps.append(AppInfo(
@@ -192,24 +237,150 @@
             tags: [],
             bundleIdentifier: bundleId,
             localizedNames: localizedNames,
+            localizedNamesByLanguage: localizedNamesByLanguage,
             icon: icon
         ))
     }
 
-    private static func localizedAppNames(
-        for appURL: URL,
+    private static func localizedAppNameMap(
         bundle: Bundle?,
         fallbackName: String
+    ) -> [String: String] {
+        guard let bundle else { return [:] }
+        let loctable = infoPlistLoctable(in: bundle)
+        var result: [String: String] = [:]
+
+        for language in L10n.supported {
+            guard let localizedName = localizedAppName(
+                for: language.code,
+                bundle: bundle,
+                loctable: loctable,
+                fallbackName: fallbackName
+            ) else { continue }
+            result[language.code] = localizedName
+        }
+
+        return result
+    }
+
+    private static func localizedAppNames(
+        for appURL: URL,
+        fallbackName: String,
+        localizedNamesByLanguage: [String: String]
     ) -> [String] {
-        var values: [String?] = []
-        values.append(bundle?.localizedInfoDictionary?["CFBundleDisplayName"] as? String)
-        values.append(bundle?.localizedInfoDictionary?["CFBundleName"] as? String)
-        values.append(bundle?.infoDictionary?["CFBundleDisplayName"] as? String)
-        values.append(bundle?.infoDictionary?["CFBundleName"] as? String)
+        var values = L10n.supported.map { localizedNamesByLanguage[$0.code] }
         values.append(spotlightDisplayName(for: appURL))
         values.append(FileManager.default.displayName(atPath: appURL.path).replacingOccurrences(of: ".app", with: ""))
 
         return uniqueLocalizedNames(values, excluding: fallbackName)
+    }
+
+    private static func localizedAppName(
+        for languageCode: String,
+        bundle: Bundle,
+        loctable: [String: [String: Any]],
+        fallbackName: String
+    ) -> String? {
+        for localization in localizationCandidates(for: languageCode) {
+            if let table = loctable[localization],
+               let value = firstValidLocalizedName(
+                   [table["CFBundleDisplayName"], table["CFBundleName"]],
+                   excluding: fallbackName
+               ) {
+                return value
+            }
+
+            if let strings = infoPlistStrings(in: bundle, localization: localization),
+               let value = firstValidLocalizedName(
+                   [strings["CFBundleDisplayName"], strings["CFBundleName"]],
+                   excluding: fallbackName
+               ) {
+                return value
+            }
+        }
+
+        if languageCode == "en",
+           let value = firstValidLocalizedName(
+               [
+                   bundle.infoDictionary?["CFBundleDisplayName"],
+                   bundle.infoDictionary?["CFBundleName"]
+               ],
+               excluding: fallbackName
+           ) {
+            return value
+        }
+
+        return nil
+    }
+
+    private static func infoPlistLoctable(in bundle: Bundle) -> [String: [String: Any]] {
+        guard let url = bundle.url(forResource: "InfoPlist", withExtension: "loctable"),
+              let rawTable = NSDictionary(contentsOf: url) as? [String: Any]
+        else { return [:] }
+
+        var result: [String: [String: Any]] = [:]
+        for (key, value) in rawTable {
+            if let localizedTable = value as? [String: Any] {
+                result[key] = localizedTable
+            }
+        }
+        return result
+    }
+
+    private static func infoPlistStrings(
+        in bundle: Bundle,
+        localization: String
+    ) -> [String: Any]? {
+        guard let url = bundle.url(
+            forResource: "InfoPlist",
+            withExtension: "strings",
+            subdirectory: nil,
+            localization: localization
+        ) else { return nil }
+        return NSDictionary(contentsOf: url) as? [String: Any]
+    }
+
+    private static func localizationCandidates(for languageCode: String) -> [String] {
+        var candidates = [languageCode, languageCode.replacingOccurrences(of: "-", with: "_")]
+        switch languageCode {
+        case "zh-Hans":
+            candidates.append(contentsOf: ["zh_CN", "zh"])
+        case "zh-Hant":
+            candidates.append(contentsOf: ["zh_TW", "zh_HK", "zh"])
+        case "pt-BR":
+            candidates.append(contentsOf: ["pt_BR", "pt"])
+        case "sr-Cyrl":
+            candidates.append(contentsOf: ["sr_Cyrl", "sr"])
+        case "ar-Najdi":
+            candidates.append(contentsOf: ["ar_Najdi", "ar"])
+        case "nb":
+            candidates.append(contentsOf: ["nb", "no"])
+        case "nn":
+            candidates.append(contentsOf: ["nn", "no"])
+        case "no":
+            candidates.append(contentsOf: ["no", "nb", "nn"])
+        default:
+            if let base = languageCode.split(separator: "-").first.map(String.init) {
+                candidates.append(base)
+            }
+        }
+        return AppInfo.uniqueLanguageCodes(candidates)
+    }
+
+    private static func firstValidLocalizedName(
+        _ values: [Any?],
+        excluding excludedValue: String
+    ) -> String? {
+        let normalizedExcluded = normalizedLocalizedName(excludedValue)
+        for value in values {
+            guard let value = value as? String else { continue }
+            let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
+            guard !trimmed.isEmpty,
+                  normalizedLocalizedName(trimmed) != normalizedExcluded
+            else { continue }
+            return trimmed
+        }
+        return nil
     }
 
     private static func spotlightDisplayName(for appURL: URL) -> String? {
@@ -1081,6 +1252,7 @@
                 name: app.name, path: app.path, tags: appTags,
                 bundleIdentifier: app.bundleIdentifier,
                 localizedNames: app.localizedNames,
+                localizedNamesByLanguage: app.localizedNamesByLanguage,
                 icon: app.icon,
                 isUncommon: uncommonPaths.contains(app.path.path),
                 note: store.appNotes[app.path.path]
diff --git a/Apptag/EditModeViews.swift b/Apptag/EditModeViews.swift
index b64956d..aca3c77 100644
--- a/Apptag/EditModeViews.swift
+++ b/Apptag/EditModeViews.swift
@@ -249,7 +249,7 @@
                         .offset(x: 6, y: -6)
                 }
 
-                Text(app.name)
+                Text(app.displayName)
                     .font(.system(size: 11, weight: .medium))
                     .lineLimit(1)
                     .truncationMode(.tail)
diff --git a/Apptag/PreferencesView.swift b/Apptag/PreferencesView.swift
index d01e500..e856ff9 100644
--- a/Apptag/PreferencesView.swift
+++ b/Apptag/PreferencesView.swift
@@ -32,6 +32,7 @@
     @State private var categoryScheme = TagDatabase.CategorySchemeState()
     @State private var isApplyingSystemScheme = false
     @State private var showApplySystemSchemeConfirmation = false
+    @State private var isDataFilePanelPresented = false
     @State private var hotkeyStatusToast: String? = nil
     @State private var hotkeyStatusToastToken: UUID? = nil
 
@@ -54,48 +55,81 @@
     }
 
     private func exportTags() {
-        let panel = NSSavePanel()
-        panel.title = tr("settings.export")
-        TagDatabase.flushPendingCategorySchemeBackupBatch()
-        refreshDataState()
-        panel.nameFieldStringValue = TagDatabase.exportFileName(for: categoryScheme)
-        panel.allowedContentTypes = [.json]
-        beginFilePanel(panel) { response in
-            guard response == .OK, let url = panel.url else { return }
-            do {
-                try TagDatabase.exportTo(url)
-            } catch {
-                fputs("[TagLauncher] Export failed: \(error)\n", stderr)
-                showDataAlert(title: tr("settings.exportFailed"), message: error.localizedDescription)
+        guard prepareDataFilePanelPresentation() else { return }
+        DispatchQueue.main.async {
+            TagDatabase.flushPendingCategorySchemeBackupBatch()
+            let currentScheme = TagDatabase.loadWithEnsuredCategoryScheme().categoryScheme
+            categoryScheme = currentScheme
+
+            let panel = NSSavePanel()
+            panel.title = tr("settings.export")
+            panel.nameFieldStringValue = TagDatabase.exportFileName(for: currentScheme)
+            panel.allowedContentTypes = [.json]
+
+            beginFilePanel(panel) { response in
+                finishDataFilePanelPresentation()
+                guard response == .OK, let url = panel.url else { return }
+                do {
+                    try TagDatabase.exportTo(url)
+                } catch {
+                    fputs("[TagLauncher] Export failed: \(error)\n", stderr)
+                    showDataAlert(title: tr("settings.exportFailed"), message: error.localizedDescription)
+                }
             }
         }
     }
 
     private func importTags() {
-        let panel = NSOpenPanel()
-        panel.title = tr("settings.import")
-        panel.allowedContentTypes = [.json]
-        panel.allowsMultipleSelection = false
-        beginFilePanel(panel) { response in
-            guard response == .OK, let url = panel.url else { return }
-            do {
-                TagDatabase.flushPendingCategorySchemeBackupBatch()
-                _ = try TagDatabase.importFrom(url)
-                scanApps()
-                refreshDataState()
-                notifyDataChanged()
-            } catch {
-                fputs("[TagLauncher] Import failed: \(error)\n", stderr)
-                showDataAlert(title: tr("settings.importFailed"), message: error.localizedDescription)
+        guard prepareDataFilePanelPresentation() else { return }
+        DispatchQueue.main.async {
+            let panel = NSOpenPanel()
+            panel.title = tr("settings.import")
+            panel.allowedContentTypes = [.json]
+            panel.allowsMultipleSelection = false
+
+            beginFilePanel(panel) { response in
+                finishDataFilePanelPresentation()
+                guard response == .OK, let url = panel.url else { return }
+                do {
+                    TagDatabase.flushPendingCategorySchemeBackupBatch()
+                    _ = try TagDatabase.importFrom(url)
+                    scanApps()
+                    refreshDataState()
+                    notifyDataChanged()
+                } catch {
+                    fputs("[TagLauncher] Import failed: \(error)\n", stderr)
+                    showDataAlert(title: tr("settings.importFailed"), message: error.localizedDescription)
+                }
             }
         }
+    }
+
+    private func prepareDataFilePanelPresentation() -> Bool {
+        guard !isDataFilePanelPresented else { return false }
+        isDataFilePanelPresented = true
+        NotificationCenter.default.post(
+            name: .tagLauncherModalInteractionChanged,
+            object: nil,
+            userInfo: ["active": true]
+        )
+        NSApp.activate(ignoringOtherApps: true)
+        preferencesWindow?.makeKeyAndOrderFront(nil)
+        return true
+    }
+
+    private func finishDataFilePanelPresentation() {
+        isDataFilePanelPresented = false
+        NotificationCenter.default.post(
+            name: .tagLauncherModalInteractionChanged,
+            object: nil,
+            userInfo: ["active": false]
+        )
     }
 
     private func beginFilePanel(
         _ panel: NSSavePanel,
         completion: @escaping (NSApplication.ModalResponse) -> Void
     ) {
-        NSApp.activate(ignoringOtherApps: true)
         if let window = preferencesWindow {
             panel.beginSheetModal(for: window, completionHandler: completion)
         } else {
@@ -106,9 +140,25 @@
     }
 
     private var preferencesWindow: NSWindow? {
-        NSApp.windows.first {
+        if let taggedWindow = NSApp.windows.first(where: {
             $0.identifier?.rawValue == "TagLauncherPreferencesWindow" && $0.isVisible
-        } ?? NSApp.keyWindow ?? NSApp.mainWindow
+        }) {
+            return taggedWindow
+        }
+
+        return [NSApp.keyWindow, NSApp.mainWindow]
+            .compactMap { $0 }
+            .first(where: isPreferencesWindowFallback(_:))
+    }
+
+    private func isPreferencesWindowFallback(_ window: NSWindow) -> Bool {
+        guard window.isVisible,
+              !(window is NSPanel),
+              !(window is OverlayPanel)
+        else { return false }
+
+        let preferencesTitle = tr("menu.preferences").replacingOccurrences(of: "…", with: "")
+        return window.title == preferencesTitle
     }
 
     private func showDataAlert(title: String, message: String) {
@@ -683,8 +733,10 @@
                             HStack(spacing: 12) {
                                 Button(tr("settings.export")) { exportTags() }
                                     .buttonStyle(.bordered)
+                                    .disabled(isDataFilePanelPresented)
                                 Button(tr("settings.import")) { importTags() }
                                     .buttonStyle(.bordered)
+                                    .disabled(isDataFilePanelPresented)
                             }
                             Text(tr("settings.backupDesc"))
                                 .font(.caption)
diff --git a/Apptag/QuickSearch.swift b/Apptag/QuickSearch.swift
index eaf968b..6056150 100644
--- a/Apptag/QuickSearch.swift
+++ b/Apptag/QuickSearch.swift
@@ -857,7 +857,7 @@
                 .cornerRadius(10)
 
             VStack(alignment: .leading, spacing: 4) {
-                Text(result.app.name)
+                Text(result.app.displayName)
                     .font(.system(size: 20, weight: .semibold))
                     .foregroundStyle(.primary)
                     .lineLimit(1)
@@ -921,7 +921,7 @@
     }
 
     private var accessibilityText: String {
-        [result.app.name, detailText].compactMap { $0 }.joined(separator: ", ")
+        [result.app.displayName, detailText].compactMap { $0 }.joined(separator: ", ")
     }
 }
 
diff --git a/Apptag/TagGroupView.swift b/Apptag/TagGroupView.swift
deleted file mode 100644
index d576054..0000000
--- a/Apptag/TagGroupView.swift
+++ /dev/null
@@ -1,112 +0,0 @@
-import SwiftUI
-import UniformTypeIdentifiers
-
-// MARK: - Tag Group Section (with centered separator-line header)
-
-struct TagGroupView: View {
-    let group: TagGroup
-    let onSelectApp: (AppInfo) -> Void
-    let tagFontSize: CGFloat
-    let iconSize: CGFloat
-    var showNames: Bool = true
-    var dragModeActive: Bool = false
-    var onDragModeChange: ((Bool) -> Void)? = nil
-    var onBubbleHover: ((AppInfo, CGRect, AppBubbleHoverEvent) -> Void)? = nil
-    var onEditNote: ((AppInfo, CGRect) -> Void)? = nil
-    var bubbleDisabled: Bool = false
-    var showUncommonAppBubbles: Bool = AppDefaults.showUncommonAppBubbles
-    var dragResetToken: Int = 0
-    var onDropApp: ((String, String, Bool) -> Void)? = nil
-
-    /// Adaptive columns — auto-fit based on icon size and available width.
-    private var columns: [GridItem] {
-        let itemWidth = AppGridItem.stableWidth(iconSize: iconSize)
-        return [GridItem(.adaptive(minimum: itemWidth, maximum: itemWidth + 36), spacing: 6)]
-    }
-
-    // Subtle separator line color — adapts to light/dark mode
-    private var lineColor: Color {
-        .secondary.opacity(0.25)
-    }
-
-    var body: some View {
-        VStack(alignment: .leading, spacing: 0) {
-            // Centered separator line with tag name
-            HStack(spacing: 0) {
-                Rectangle()
-                    .fill(lineColor)
-                    .frame(height: 1)
-
-                Text(group.name)
-                    .font(.system(size: tagFontSize, weight: .semibold))
-                    .foregroundStyle(.secondary)
-                    .padding(.horizontal, 10)
-
-                Rectangle()
-                    .fill(lineColor)
-                    .frame(height: 1)
-            }
-            .padding(.bottom, 6)
-
-            // App icon grid — columns auto-adjust to icon size
-            LazyVGrid(columns: columns, spacing: 2) {
-                ForEach(group.apps) { app in
-                    AppGridItem(
-                        app: app,
-                        iconSize: iconSize,
-                        showName: showNames,
-                        sourceTag: group.name,
-                        dragModeActive: dragModeActive,
-                        onDragModeChange: onDragModeChange,
-                        onBubbleHover: onBubbleHover,
-                        onEditNote: onEditNote,
-                        bubbleDisabled: bubbleDisabled,
-                        showUncommonAppBubbles: showUncommonAppBubbles,
-                        itemID: "\(group.name)|\(app.path.path)",
-                        dragResetToken: dragResetToken,
-                        onSelect: { onSelectApp(app) }
-                    )
-                }
-            }
-        }
-        .contentShape(Rectangle())
-        .overlay {
-            if dragModeActive {
-                AppDropTargetView(targetTag: group.name) { path, source, copy in
-                    onDropApp?(path, source, copy)
-                }
-                .allowsHitTesting(false)
-            }
-        }
-        .onDrop(of: [UTType.plainText], isTargeted: nil) { providers in
-            handleDrop(providers)
-        }
-    }
-
-    private func handleDrop(_ providers: [NSItemProvider]) -> Bool {
-        guard let provider = providers.first(where: { $0.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) }) else {
-            return false
-        }
-        provider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { item, _ in
-            let text: String?
-            if let data = item as? Data {
-                text = String(data: data, encoding: .utf8)
-            } else if let string = item as? String {
-                text = string
-            } else if let string = item as? NSString {
-                text = string as String
-            } else {
-                text = nil
-            }
-            guard let text else { return }
-            let parts = text.components(separatedBy: "\n")
-            guard let path = parts.first, !path.isEmpty else { return }
-            let source = parts.dropFirst().first ?? ""
-            let copy = NSEvent.modifierFlags.contains(.option)
-            DispatchQueue.main.async {
-                onDropApp?(path, source, copy)
-            }
-        }
-        return true
-    }
-}
diff --git a/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.base.json b/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.base.json
index 0d12668..7abb2df 100644
--- a/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.base.json
+++ b/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.base.json
@@ -1145,8 +1145,7 @@
       "bundleIdentifier": "com.apple.iBooksX",
       "defaultTag": [
         "writing",
-        "education",
-        "entertainment"
+        "education"
       ],
       "sourceEvidence": [
         "curated_tager_catalog"
diff --git a/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.csv b/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.csv
index 74764c6..78b6929 100644
--- a/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.csv
+++ b/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.csv
@@ -82,7 +82,7 @@
 audio-midi-setup,audio|device-management,com.apple.audio.audiomidisetup,管音频设备和 MIDI 路由,音乐人和调试党很常用
 automator,Automation,com.apple.automator,不写代码也能串起一串 Mac 自动化动作
 bluetooth-file-exchange,transfer|device-management,com.apple.bluetoothfileexchange,用蓝牙收发文件,古早但关键时能救急
-books,writing|education|entertainment,com.apple.iBooksX,阅读和管理电子书的入口,适合把 Mac 变成书桌
+books,writing|education,com.apple.iBooksX,阅读和管理电子书的入口,适合把 Mac 变成书桌
 boot-camp-assistant,system,com.apple.bootcampassistant,Intel Mac 安装 Windows 的官方向导
 calculator,utilities,com.apple.calculator,小计算器,单位换算和程序员模式也藏在里面
 chess,game,com.apple.chess,系统自带国际象棋,摸鱼时比刷网页更优雅
diff --git a/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.manifest.json b/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.manifest.json
index 5607d0a..0c1a111 100644
--- a/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.manifest.json
+++ b/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.manifest.json
@@ -42,8 +42,8 @@
   ],
   "baseResource": {
     "file": "SmartStart_UltimateDefaultCatalog.base.json",
-    "sha256": "650dcfba5bbf35816aea987baf6c29cfcab827ce72313a53b1aeb592a02949da",
-    "bytes": 1025647,
+    "sha256": "ac7f3a7c4dfa412a3196181d5aa03b72f25d8776f368b6f15b30d30db69da8d0",
+    "bytes": 1025622,
     "count": 3530
   },
   "notesResources": {
diff --git a/Scripts/window_logic_qa.sh b/Scripts/window_logic_qa.sh
new file mode 100755
index 0000000..c05ae1c
--- /dev/null
+++ b/Scripts/window_logic_qa.sh
@@ -0,0 +1,451 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+APP_BUNDLE="$ROOT_DIR/build/TagLauncher.app"
+APP_PROCESS="TagLauncher"
+LAUNCH_AGENT_LABEL="com.taglauncher.app"
+LAUNCH_AGENT_PLIST="$HOME/Library/LaunchAgents/$LAUNCH_AGENT_LABEL.plist"
+USER_GUI_DOMAIN="gui/$(id -u)"
+RESTORE_LAUNCH_AGENT=false
+CLICK_TOOL="${CLICK_TOOL:-$(command -v cliclick || true)}"
+
+if [[ -z "$CLICK_TOOL" ]]; then
+  echo "FAIL: cliclick is required for window-position click checks." >&2
+  exit 2
+fi
+
+log() {
+  printf '%s\n' "$*"
+}
+
+send_keycode() {
+  local keycode="$1"
+  local modifiers="${2:-}"
+  if [[ -n "$modifiers" ]]; then
+    osascript -e "tell application \"System Events\" to key code $keycode using {$modifiers}"
+  else
+    osascript -e "tell application \"System Events\" to key code $keycode"
+  fi
+}
+
+send_main_hotkey() {
+  osascript -e 'tell application "System Events" to keystroke space using {option down, shift down}'
+}
+
+send_cmd_comma() {
+  osascript -e 'tell application "System Events" to keystroke "," using {command down}'
+}
+
+send_cmd_w() {
+  osascript -e 'tell application "System Events" to keystroke "w" using {command down}'
+}
+
+show_overlay_from_app_menu() {
+  osascript <<'OSA'
+tell application "System Events"
+  tell process "TagLauncher"
+    repeat 50 times
+      set frontmost to true
+      delay 0.2
+      repeat with itemRef in menu items of menu 1 of menu bar item "TagLauncher" of menu bar 1
+        try
+          set itemName to name of itemRef as text
+          if itemName contains "显示应用列表" or itemName contains "Show" then
+            click itemRef
+            return
+          end if
+        end try
+      end repeat
+    end repeat
+    error "TagLauncher Show App List menu item did not become available"
+  end tell
+end tell
+OSA
+}
+
+cliclick_coord() {
+  local value="$1"
+  if [[ "$value" == -* ]]; then
+    printf '=%s' "$value"
+  else
+    printf '%s' "$value"
+  fi
+}
+
+click_xy() {
+  local x="$1"
+  local y="$2"
+  "$CLICK_TOOL" c:"$(cliclick_coord "$x")","$(cliclick_coord "$y")"
+}
+
+move_xy() {
+  local x="$1"
+  local y="$2"
+  "$CLICK_TOOL" m:"$(cliclick_coord "$x")","$(cliclick_coord "$y")"
+}
+
+cleanup() {
+  osascript -e 'tell application "System Events" to key code 53' >/dev/null 2>&1 || true
+  sleep 0.2
+  osascript -e 'tell application "System Events" to key code 53' >/dev/null 2>&1 || true
+  kill_all_taglauncher_instances
+  if [[ "$RESTORE_LAUNCH_AGENT" == true && -f "$LAUNCH_AGENT_PLIST" ]]; then
+    launchctl bootstrap "$USER_GUI_DOMAIN" "$LAUNCH_AGENT_PLIST" >/dev/null 2>&1 || true
+  fi
+}
+trap cleanup EXIT
+
+kill_all_taglauncher_instances() {
+  local pids
+  pids="$(pgrep -f '/TagLauncher.app/Contents/MacOS/TagLauncher' || true)"
+  if [[ -n "$pids" ]]; then
+    kill $pids >/dev/null 2>&1 || true
+    sleep 0.4
+  fi
+  pids="$(pgrep -f '/TagLauncher.app/Contents/MacOS/TagLauncher' || true)"
+  if [[ -n "$pids" ]]; then
+    kill -9 $pids >/dev/null 2>&1 || true
+  fi
+}
+
+is_qa_app_only_running() {
+  local lines
+  lines="$(pgrep -fl '/TagLauncher.app/Contents/MacOS/TagLauncher' || true)"
+  [[ -n "$lines" ]] || return 1
+  while IFS= read -r line; do
+    [[ "$line" == *"$APP_BUNDLE/Contents/MacOS/TagLauncher"* ]] || return 1
+  done <<<"$lines"
+}
+
+prepare_isolated_app_instance() {
+  if launchctl print "$USER_GUI_DOMAIN/$LAUNCH_AGENT_LABEL" >/dev/null 2>&1; then
+    RESTORE_LAUNCH_AGENT=true
+    launchctl bootout "$USER_GUI_DOMAIN/$LAUNCH_AGENT_LABEL" >/dev/null 2>&1 || true
+  fi
+
+  kill_all_taglauncher_instances
+  open -n "$APP_BUNDLE"
+  sleep 2.5
+
+  if ! is_qa_app_only_running; then
+    echo "FAIL: expected only QA build TagLauncher instance to be running" >&2
+    pgrep -fl '/TagLauncher.app/Contents/MacOS/TagLauncher' >&2 || true
+    exit 1
+  fi
+}
+
+assert_swift="$(mktemp -t taglauncher-window-assert.XXXXXX.swift)"
+coords_swift="$(mktemp -t taglauncher-window-coords.XXXXXX.swift)"
+screens_swift="$(mktemp -t taglauncher-screens.XXXXXX.swift)"
+trap 'rm -f "$assert_swift" "$coords_swift" "$screens_swift"; cleanup' EXIT
+
+cat >"$assert_swift" <<'SWIFT'
+import AppKit
+import CoreGraphics
+import Foundation
+
+struct WindowInfo {
+    let owner: String
+    let name: String
+    let layer: Int
+    let bounds: NSDictionary
+}
+
+func allWindows() -> [WindowInfo] {
+    let raw = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
+    return raw.map {
+        WindowInfo(
+            owner: $0[kCGWindowOwnerName as String] as? String ?? "",
+            name: $0[kCGWindowName as String] as? String ?? "",
+            layer: $0[kCGWindowLayer as String] as? Int ?? -999,
+            bounds: $0[kCGWindowBounds as String] as? NSDictionary ?? [:]
+        )
+    }
+}
+
+func dumpRelevantWindows() {
+    fputs("---- relevant on-screen windows ----\n", stderr)
+    for (index, window) in allWindows().enumerated() {
+        let isRelevant = window.owner == "TagLauncher"
+            || window.owner == "Window Server"
+            || window.owner == "Dock"
+            || window.owner == "loginwindow"
+        guard isRelevant else { continue }
+        fputs("#\(index) owner=\(window.owner) name=\(window.name) layer=\(window.layer) bounds=\(window.bounds)\n", stderr)
+    }
+    fputs("------------------------------------\n", stderr)
+}
+
+func fail(_ message: String) -> Never {
+    fputs("FAIL: \(message)\n", stderr)
+    dumpRelevantWindows()
+    exit(1)
+}
+
+func tagWindows(_ windows: [WindowInfo]) -> [WindowInfo] {
+    windows.filter { $0.owner == "TagLauncher" }
+}
+
+func assertTagLayer(_ tag: [WindowInfo]) {
+    for window in tag where window.layer != 23 {
+        fail("TagLauncher window has unexpected layer \(window.layer); expected 23")
+    }
+}
+
+let mode = CommandLine.arguments.dropFirst().first ?? ""
+let windows = allWindows()
+let tag = tagWindows(windows)
+
+switch mode {
+case "overlay":
+    guard tag.count == 1 else { fail("overlay expected 1 TagLauncher window, got \(tag.count)") }
+    assertTagLayer(tag)
+    let menubarLayer = windows.first { $0.owner == "Window Server" && $0.name == "Menubar" }?.layer
+    guard menubarLayer == 24 else { fail("menubar layer expected 24, got \(String(describing: menubarLayer))") }
+    let dockWindows = windows.filter { $0.owner == "Dock" }
+    guard dockWindows.isEmpty else { fail("Dock should be hidden while overlay is visible; found \(dockWindows.count) Dock windows") }
+    print("PASS overlay: tagLayer=\(tag[0].layer) menubarLayer=24 dockWindows=0")
+
+case "settings":
+    guard tag.count == 2 else { fail("settings expected 2 TagLauncher windows, got \(tag.count)") }
+    assertTagLayer(tag)
+    guard !tag[0].name.isEmpty, tag[1].name.isEmpty else {
+        fail("settings order wrong: \(tag.map(\.name))")
+    }
+    print("PASS settings over overlay: order=\(tag.map(\.name))")
+
+case "file-panel":
+    guard tag.count == 3 else { fail("file panel expected 3 TagLauncher windows, got \(tag.count)") }
+    assertTagLayer(tag)
+    guard !tag[0].name.isEmpty, !tag[1].name.isEmpty, tag[2].name.isEmpty else {
+        fail("file panel order wrong: \(tag.map(\.name))")
+    }
+    print("PASS file panel over settings: order=\(tag.map(\.name))")
+
+case "no-overlay":
+    guard tag.isEmpty else { fail("expected no TagLauncher windows, got \(tag.count)") }
+    print("PASS overlay hidden")
+
+case "force-quit":
+    guard let overlay = tag.first else { fail("force quit check expected overlay window") }
+    guard let forceQuit = windows.first(where: {
+        $0.owner == "loginwindow" && ($0.name.localizedCaseInsensitiveContains("force") || $0.name.contains("强制"))
+    }) else {
+        fail("force quit window not found")
+    }
+    guard forceQuit.layer > overlay.layer else {
+        fail("force quit layer \(forceQuit.layer) is not above overlay layer \(overlay.layer)")
+    }
+    print("PASS force quit above overlay: forceQuitLayer=\(forceQuit.layer) overlayLayer=\(overlay.layer)")
+
+case "screen-count":
+    print("INFO screens=\(NSScreen.screens.count) frames=\(NSScreen.screens.map { NSStringFromRect($0.frame) })")
+
+default:
+    fail("unknown assert mode: \(mode)")
+}
+SWIFT
+
+cat >"$coords_swift" <<'SWIFT'
+import CoreGraphics
+import Foundation
+
+let raw = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
+let tag = raw.filter { ($0[kCGWindowOwnerName as String] as? String) == "TagLauncher" }
+let mode = CommandLine.arguments.dropFirst().first ?? ""
+switch mode {
+case "data-tab":
+    guard let settings = tag.first(where: { (($0[kCGWindowName as String] as? String) ?? "").isEmpty == false }),
+          let bounds = settings[kCGWindowBounds as String] as? NSDictionary,
+          let x = bounds["X"] as? CGFloat,
+          let y = bounds["Y"] as? CGFloat else {
+        fputs("FAIL: could not find settings window bounds\n", stderr)
+        exit(1)
+    }
+    print("\(Int(round(x + 523))) \(Int(round(y + 50)))")
+case "export":
+    guard let settings = tag.first(where: { (($0[kCGWindowName as String] as? String) ?? "").isEmpty == false }),
+          let bounds = settings[kCGWindowBounds as String] as? NSDictionary,
+          let x = bounds["X"] as? CGFloat,
+          let y = bounds["Y"] as? CGFloat else {
+        fputs("FAIL: could not find settings window bounds\n", stderr)
+        exit(1)
+    }
+    print("\(Int(round(x + 375))) \(Int(round(y + 331)))")
+case "overlay-outside":
+    guard let overlay = tag.first(where: { (($0[kCGWindowName as String] as? String) ?? "").isEmpty }),
+          let overlayBounds = overlay[kCGWindowBounds as String] as? NSDictionary,
+          let overlayX = overlayBounds["X"] as? CGFloat,
+          let overlayY = overlayBounds["Y"] as? CGFloat else {
+        fputs("FAIL: could not find overlay window bounds\n", stderr)
+        exit(1)
+    }
+    print("\(Int(round(overlayX + 120))) \(Int(round(overlayY + 160)))")
+default:
+    fputs("FAIL: unknown coords mode \(mode)\n", stderr)
+    exit(1)
+}
+SWIFT
+
+cat >"$screens_swift" <<'SWIFT'
+import AppKit
+import Foundation
+
+for (index, screen) in NSScreen.screens.enumerated() {
+    let frame = screen.frame
+    let cgTop = NSScreen.screens.map { $0.frame.maxY }.max() ?? frame.maxY
+    let cgY = cgTop - frame.maxY
+    let clickY = cgY + frame.height / 2
+    print("\(index)|\(Int(round(frame.midX)))|\(Int(round(clickY)))|\(Int(round(frame.origin.x)))|\(Int(round(cgY)))|\(Int(round(frame.width)))|\(Int(round(frame.height)))")
+}
+SWIFT
+
+swift_assert() {
+  swift "$assert_swift" "$1"
+}
+
+wait_swift_assert() {
+  local mode="$1"
+  local output=""
+  for _ in {1..12}; do
+    if output="$(swift "$assert_swift" "$mode" 2>&1)"; then
+      printf '%s\n' "$output"
+      return 0
+    fi
+    sleep 0.2
+  done
+  printf '%s\n' "$output" >&2
+  return 1
+}
+
+show_overlay() {
+  send_main_hotkey
+  if wait_swift_assert overlay >/dev/null 2>&1; then
+    swift_assert overlay
+    return 0
+  fi
+
+  show_overlay_from_app_menu
+  wait_swift_assert overlay
+}
+
+click_relative_to_settings() {
+  local mode="$1"
+  local coords
+  coords="$(swift "$coords_swift" "$mode")"
+  read -r x y <<<"$coords"
+  click_xy "$x" "$y"
+}
+
+click_overlay_outside_quick_search() {
+  local coords
+  coords="$(swift "$coords_swift" overlay-outside)"
+  read -r x y <<<"$coords"
+  click_xy "$x" "$y"
+}
+
+log "==> Building app"
+bash "$ROOT_DIR/build.sh" >/dev/null
+
+log "==> Starting clean app instance"
+prepare_isolated_app_instance
+
+log "==> QA 1/7: overlay claims foreground, hides Dock, keeps menu bar visible"
+show_overlay
+frontmost="$(osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true')"
+[[ "$frontmost" == "TagLauncher" ]] || { echo "FAIL: frontmost app is $frontmost, expected TagLauncher" >&2; exit 1; }
+swift_assert overlay
+
+log "==> QA 1/7 and 4/7: settings floats above appgrid and quick search"
+send_keycode 49
+sleep 0.4
+send_cmd_comma
+sleep 0.7
+swift_assert settings
+
+log "==> QA 2/7: import/export file panel floats above settings"
+click_relative_to_settings data-tab
+sleep 0.3
+click_relative_to_settings export
+sleep 0.7
+swift_assert file-panel
+send_keycode 53
+sleep 0.3
+send_cmd_w
+sleep 0.5
+swift_assert overlay
+
+log "==> QA 4/7 and 5/7: appgrid-space quick search and double-Esc behavior"
+send_keycode 49
+sleep 0.4
+send_keycode 53
+sleep 0.4
+swift_assert overlay
+send_keycode 53
+sleep 0.4
+swift_assert no-overlay
+
+log "==> QA 5/7: clicking outside quick search closes search, not appgrid"
+show_overlay
+send_keycode 49
+sleep 0.4
+click_overlay_outside_quick_search
+sleep 0.4
+swift_assert overlay
+
+log "==> QA 3/7: system force-quit window stays above TagLauncher"
+send_keycode 53 "option down, command down"
+sleep 0.7
+swift_assert force-quit
+send_keycode 53
+sleep 0.3
+
+log "==> QA 6/7: screen-following logic"
+swift_assert screen-count
+screen_count="$(swift "$screens_swift" | wc -l | tr -d ' ')"
+if [[ "$screen_count" -gt 1 ]]; then
+  while IFS='|' read -r index cx cy sx sy sw sh; do
+    kill_all_taglauncher_instances
+    open -n "$APP_BUNDLE"
+    sleep 2.5
+    if ! is_qa_app_only_running; then
+      echo "FAIL: expected only QA build TagLauncher instance during screen-following check" >&2
+      pgrep -fl '/TagLauncher.app/Contents/MacOS/TagLauncher' >&2 || true
+      exit 1
+    fi
+    move_xy "$cx" "$cy"
+    sleep 0.2
+    show_overlay
+    wait_swift_assert overlay
+    swift - "$sx" "$sy" "$sw" "$sh" <<'SWIFT'
+import CoreGraphics
+import Foundation
+let expected = CommandLine.arguments.dropFirst().map { Int($0)! }
+let raw = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
+let tag = raw.filter { ($0[kCGWindowOwnerName as String] as? String) == "TagLauncher" }
+guard tag.count == 1, let bounds = tag[0][kCGWindowBounds as String] as? NSDictionary else {
+    fputs("FAIL: expected one overlay for screen-following check\n", stderr)
+    exit(1)
+}
+let actual = ["X", "Y", "Width", "Height"].map { Int(round((bounds[$0] as? Double) ?? 0)) }
+guard actual == expected else {
+    fputs("FAIL: overlay bounds \(actual) did not match screen frame \(expected)\n", stderr)
+    exit(1)
+}
+print("PASS screen frame: \(actual)")
+SWIFT
+  done < <(swift "$screens_swift")
+else
+  rg -Fq 'screenContainingCurrentPointer() ??' "$ROOT_DIR/Apptag/ApptagApp.swift"
+  rg -Fq 'NSMouseInRect(mousePoint, $0.frame, false)' "$ROOT_DIR/Apptag/ApptagApp.swift"
+  log "PASS screen-following static path: single physical display here; code selects NSScreen under current pointer"
+fi
+
+log "==> QA 7/7: final chrome state still valid"
+swift_assert overlay
+send_keycode 53
+sleep 0.4
+swift_assert no-overlay
+
+log "ALL WINDOW LOGIC QA PASSED"
diff --git a/generate_icon.py b/generate_icon.py
index 7ae30dd..8331fc9 100644
--- a/generate_icon.py
+++ b/generate_icon.py
@@ -4,6 +4,7 @@
 import math, os, subprocess, tempfile
 
 SIZE = 1024
+PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
 
 def rounded_rectangle_mask(size, radius):
     mask = Image.new("L", (size, size), 0)
@@ -97,12 +98,12 @@
         print(f"  {name} ({size}x{size}): {os.path.getsize(path):,}B")
 
     # Generate .icns to project root
-    out_icns = "/Users/ar/Projects/Apptag/icon-icns.icns"
+    out_icns = os.path.join(PROJECT_DIR, "icon-icns.icns")
     subprocess.run(["iconutil", "-c", "icns", iconset, "-o", out_icns], check=True)
     icns_size = os.path.getsize(out_icns)
     print(f"\nGenerated: {out_icns} ({icns_size:,}B, {icns_size/1024:.0f}KB)")
 
 # Also save 1024x1024 preview
-preview_path = "/Users/ar/Projects/Apptag/icon_preview.png"
+preview_path = os.path.join(PROJECT_DIR, "icon_preview.png")
 img.save(preview_path, "PNG", optimize=True)
 print(f"Preview: {preview_path}")

--
Gitblit v1.9.3