From 0badb998147135c75187a0c48f926b665af734c5 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 28 Jun 2026 02:53:51 +0800
Subject: [PATCH] Release 8.1.6 build 20260628.0230

---
 src/Apptag/Localization/fr.json                                        |    4 
 src/Apptag/Localization/ru.json                                        |    4 
 src/Apptag/Localization/ar.json                                        |    4 
 src/Apptag/Localization/ro.json                                        |    4 
 src/Apptag/Localization/en.json                                        |    4 
 src/Apptag/Localization/sv.json                                        |    4 
 src/Apptag/Localization/zh-Hant.json                                   |    4 
 src/Apptag/ApptagApp.swift                                             |  173 ++++
 src/Apptag/Localization/pl.json                                        |    4 
 src/Apptag/Localization/es.json                                        |    4 
 src/Apptag/Localization/th.json                                        |    4 
 src/Apptag/Localization/nn.json                                        |    4 
 src/Apptag/ProEntitlement.swift                                        |    4 
 src/CHANGELOG.md                                                       |   21 
 src/Apptag/Info.plist                                                  |   15 
 src/Apptag/Localization/cs.json                                        |    4 
 src/Apptag/Localization/ms.json                                        |    4 
 src/Apptag/Localization/it.json                                        |    4 
 src/Apptag/Localization/tr.json                                        |    4 
 src/Scripts/pro_feature_gate_qa.sh                                     |    7 
 src/Apptag/Localization/da.json                                        |    4 
 src/Apptag/Localization/id.json                                        |    4 
 src/Docs/Requirements/2026-06-27-trackpad-gesture-integration-prd.md   |  606 +++++++++++++++++
 src/Scripts/smartstart_default_categories_qa.sh                        |    8 
 src/Scripts/window_logic_qa.sh                                         |  308 ++++++--
 src/Apptag/Localization/ja.json                                        |    4 
 src/Apptag/PreferencesView.swift                                       |   56 +
 CODE_GOVERNANCE.md                                                     |    2 
 src/Apptag/Localization/de.json                                        |    4 
 src/Docs/Requirements/2026-06-27-pro-theme-preview-countdown-prd.md    |  376 +++++++++++
 src/Apptag/Localization/pt-BR.json                                     |    4 
 CODEGRAPH.md                                                           |   17 
 src/Apptag/Localization/ar-Najdi.json                                  |    4 
 src/Apptag/Localization/nb.json                                        |    4 
 src/Scripts/pro_localization_qa.sh                                     |    1 
 src/Apptag/SmartCategorization/SmartCategory.swift                     |    3 
 src/Scripts/url_scheme_activation_qa.sh                                |  134 +++
 src/Apptag/Localization/ko.json                                        |    4 
 src/Apptag/Localization/no.json                                        |    4 
 src/Scripts/pro_theme_preview_countdown_qa.sh                          |  107 +++
 src/Apptag/Localization/sr-Cyrl.json                                   |    4 
 src/Apptag/Localization/zh-Hans.json                                   |    4 
 src/Docs/Requirements/2026-06-27-smartstart-category-simplification.md |   46 
 src/Apptag/Localization/nl.json                                        |    4 
 src/Apptag/Localization/uk.json                                        |    4 
 src/Apptag/Localization/vi.json                                        |    4 
 46 files changed, 1,868 insertions(+), 132 deletions(-)

diff --git a/CODEGRAPH.md b/CODEGRAPH.md
index b03aae7..60ce67c 100644
--- a/CODEGRAPH.md
+++ b/CODEGRAPH.md
@@ -19,6 +19,7 @@
 - `src/Apptag/ApptagApp.swift`
   - `TagLauncherApp`:SwiftUI App 入口。
   - `AppDelegate`:菜单栏状态项、Dock 显示策略、热键注册、窗口打开、Quick Search 触发、App 生命周期。
+  - 支持外部 URL 唤起 `taglauncher://show`:只做 AppGrid show/focus,不做 toggle;冷启动时请求会排队到初始化完成后执行;不接管原生触控板手势。
   - 支持从标签导航双击进入设置页的 Tags tab。
   - 启动时初始化 `ProEntitlementCenter`,保证免费 / Pro / 老用户自动 Pro 状态进入全局同步快照。
   - 菜单栏下拉菜单包含免费 / Pro 当前身份状态项;免费状态点击进入 About 页的 Pro 卡片,Pro 状态使用金色皇冠图标。
@@ -34,6 +35,7 @@
 - `src/Apptag/PreferencesView.swift`
   - `PreferencesView`:设置窗口、语言、通用、主题、快捷键、标签、数据、关于等设置页。
   - Theme tab 只写视觉偏好 `appGridThemeID`,不得改变标签、排序、备注或分类数据。
+  - 免费用户预览 Pro 主题时,Theme tab 状态行和当前预览主题卡片显示基于 `ProThemePreviewState.endsAt` 推导的 `MM:SS` 倒计时;倒计时到期只退出预览,不弹阻塞提示。
   - Pro 升级 / 权益卡位于 About tab;Data tab 只承载分类方案、导入导出和气泡显示位置等数据管理入口。
 
 ## AppGrid 显示与交互
@@ -95,8 +97,8 @@
 
 - `src/Apptag/SmartCategorization/SmartCategory.swift`
   - 内置智能分类定义。
-  - `SmartCategoryDefaults.orderedIDs` 是 SmartStart 默认初始化标签事实源;当前默认方案为 13 个标签。
-  - `SmartCategoryID.smartStartDefaultCategoryID` 负责把旧细分 catalog 分类归并到 13 个默认标签;`finance` 和 `other` 返回 nil,回到未分类;这是新 SmartStart 写入前的归并,不做老用户旧标签自动迁移。
+  - `SmartCategoryDefaults.orderedIDs` 是 SmartStart 默认初始化标签事实源;当前默认方案为 12 个标签。
+  - `SmartCategoryID.smartStartDefaultCategoryID` 负责把旧细分 catalog 分类归并到 12 个默认标签;`Meeting` 归入 `communication`,`finance` 和 `other` 返回 nil,回到未分类;这是新 SmartStart 写入前的归并,不做老用户旧标签自动迁移。
 - `src/Apptag/SmartCategorization/SmartCategorizationDraft.swift`
   - 智能分类草稿、默认备注 provenance、未分配应用和 warning 模型。
 - `src/Apptag/SmartCategorization/SmartStartService.swift`
@@ -121,7 +123,7 @@
 ## 构建与发布
 
 - `src/Apptag/Info.plist`
-  - App 版本、build、bundle metadata。
+  - App 版本、build、bundle metadata;注册公开 URL Scheme `taglauncher`,首期路由只支持 `taglauncher://show`。
 - `src/Apptag/TagLauncher.entitlements`
   - App sandbox / entitlement 配置。
 - `src/build.sh`
@@ -148,7 +150,7 @@
 - `src/Scripts/smart_category_localization_qa.sh`
   - SmartCategory/default system tag 初始化标签 29 语种本地化检查;禁止非英文语言包直接复制英文标签,保留 `PDF`、`DevOps` 等技术通用词例外。
 - `src/Scripts/smartstart_default_categories_qa.sh`
-  - SmartStart 默认初始化标签检查;验证 13 个默认分类、catalog 分类归并入口、`finance` / `other` 回未分类、首次 starter tags 和关键多语言目标文案。
+  - SmartStart 默认初始化标签检查;验证 12 个默认分类、catalog 分类归并入口、`Meeting` 归入 `communication`、`finance` / `other` 回未分类、首次 starter tags 和关键多语言目标文案。
 - `src/Scripts/quick_search_app_name_qa.sh`
   - Quick Search 应用显示名回归。
 - `src/Scripts/quick_search_system_app_qa.sh`
@@ -163,14 +165,18 @@
   - Pro 文案 29 语种静态门禁;检查 `pro.*` 键完整、占位符完整、无生成污染和非英文语言无英文兜底复制。
 - `src/Scripts/pro_feature_gate_qa.sh`
   - Pro 功能源码门禁;检查 StoreKit 权益、老用户自动 Pro、主题门禁、导入导出硬拦截、备注额度、排序预览和 Pro UI 提示。
+- `src/Scripts/pro_theme_preview_countdown_qa.sh`
+  - Pro 主题预览倒计时门禁;检查状态行倒计时、当前预览卡片 `MM:SS`、到期清理、29 语种 `%time%` 占位符和禁止写死体验时长。
 - `src/Scripts/appgrid_startup_loading_qa.sh`
   - AppGrid 快照复用和延迟转圈回归。
+- `src/Scripts/url_scheme_activation_qa.sh`
+  - 外部 URL 唤起门禁;检查 `taglauncher://show` 注册、AppDelegate show/focus 语义、冷启动 pending、29 语种设置文案和禁止私有触控板捕获。
 - `src/Scripts/usage_tips_qa.sh`
   - AppGrid 使用技巧 AppKit 横幅、主题可读性、防穿透、关闭按钮、29 语种文案回归。
 - `src/Scripts/usage_tips_close_reminder_qa.sh`
   - 使用技巧关闭提醒、modal 防穿透、设置页预览、29 语种文案回归。
 - `src/Scripts/window_logic_qa.sh`
-  - 窗口层级、显示隐藏、Dock/菜单栏相关逻辑回归。
+  - 窗口层级、显示隐藏、Dock/菜单栏相关逻辑回归;Dock 项断言按当前 QA 构建路径过滤,避免本机固定正式版 App 时误判重复图标。
 - `src/Scripts/app_ordering_data_qa.sh`
   - App 排序数据持久化回归。
 
@@ -212,3 +218,4 @@
 - 改 `DataLayer.swift` 通常会影响应用扫描、显示名、标签数据、导入导出和迁移,必须补数据 QA。
 - 改 `PreferencesView.swift` 通常需要检查 29 个本地化 JSON、设置持久化和 macOS 14 UI 表现。
 - 改 `ApptagApp.swift` 或 `OverlayWindowController.swift` 必须回归菜单栏、Dock 图标策略、热键、窗口层级、全屏/Split View 行为。
+- 改外部 URL 唤起、`Info.plist` URL Scheme 或第三方手势说明时,必须运行 `url_scheme_activation_qa.sh`,并确认不新增 Accessibility/Input Monitoring/private multitouch 依赖。
diff --git a/CODE_GOVERNANCE.md b/CODE_GOVERNANCE.md
index 623718f..553d6ed 100644
--- a/CODE_GOVERNANCE.md
+++ b/CODE_GOVERNANCE.md
@@ -74,6 +74,8 @@
 ```
 
 - tag、`Info.plist`、`CHANGELOG.md`、`Release/` 资料、最终安装包文件名和 build 编号必须一致。
+- 生成 App Store 发布资料时,App Store Connect 的 What's New 不得只依据目标版本 release scope 或目标版本 changelog 小节。必须先确认当前 App Store 线上版本或当前待替换版本,并从 `CHANGELOG.md` 提取当前商店版本之后到目标版本之间的累计用户可感知变更。
+- 每个 App Store release checklist 必须包含可勾选的 What's New 累计变更门禁:当前商店基线版本、目标版本、累计 changelog 范围、中间版本用户可感知变更、排除原因,以及中英文 What's New 用户审核结果。
 
 ## 6. Worktree 使用规则
 
diff --git a/src/Apptag/ApptagApp.swift b/src/Apptag/ApptagApp.swift
index bb6b32b..53dfb40 100644
--- a/src/Apptag/ApptagApp.swift
+++ b/src/Apptag/ApptagApp.swift
@@ -39,7 +39,10 @@
     private static let downloadHelpMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherDownloadHelpMenuItem")
     private static let externalActivationNotification = Notification.Name("TagLauncherExternalActivationRequested")
     private static let externalActivationObject = AppIdentity.bundleIdentifier
+    private static let externalInvocationScheme = "taglauncher"
+    private static let externalInvocationShowHost = "show"
     private static let duplicateLaunchSuppressReopenKey = "duplicateLaunchSuppressReopenAt"
+    private static let overlaySpaceHotkeyID: UInt32 = 3
     private static let launcherOverlayLevel = NSWindow.Level(rawValue: NSWindow.Level.mainMenu.rawValue - 1)
     private static let overlayDefaultLevel = launcherOverlayLevel
     private static let overlayTextInputLevel = launcherOverlayLevel
@@ -50,8 +53,10 @@
     private var quickSearchLocalMouseMonitor: Any?
     private var quickSearchExternalMouseMonitor: Any?
     private var settingsWindow: NSWindow?    // Track Settings window to keep it above overlay
+    private var explicitPreferencesOpenRequestedAt = Date.distantPast
     private var mainHotkeyRef: EventHotKeyRef?
     private var quickSearchHotkeyRef: EventHotKeyRef?
+    private var overlaySpaceHotkeyRef: EventHotKeyRef?
     private var hotkeyEventHandlerInstalled = false
     private var isQuickSearchOpen = false
     private var quickSearchShouldHideOverlayOnClose = false
@@ -65,6 +70,8 @@
     private var statusMenuScreenForNextOverlay: NSScreen?
     private var suppressReopenUntil = Date.distantPast
     private var overlayOpenedByQuickSearchOnly = false
+    private var didFinishLaunching = false
+    private var pendingShowOverlayInvocation = false
 
     private lazy var overlayController = OverlayWindowController(
         dependencies: OverlayWindowController.Dependencies(
@@ -127,14 +134,16 @@
             refreshChromeState: { [weak self] activate, avoidSpaceSwitch in
                 self?.refreshLauncherChromeState(activate: activate, avoidSpaceSwitch: avoidSpaceSwitch)
             },
-            onWillHide: {
+            onWillHide: { [weak self] in
+                self?.unregisterOverlaySpaceHotkey()
                 TagDatabase.flushPendingCategorySchemeBackupBatch()
             },
             onDidHide: { [weak self] in
                 self?.overlayOpenedByQuickSearchOnly = false
                 NotificationCenter.default.post(name: .tagLauncherOverlayDidHide, object: nil)
             },
-            onDidShow: {
+            onDidShow: { [weak self] in
+                self?.refreshOverlaySpaceHotkeyRegistration()
                 NotificationCenter.default.post(name: .tagLauncherOverlayDidShow, object: nil)
             }
         )
@@ -213,6 +222,9 @@
         observeLanguageChanges()
         setupLaunchAtLogin()
         suppressReopenUntil = Date().addingTimeInterval(1.0)
+        didFinishLaunching = true
+        closeRestoredPreferencesWindowsDuringLaunch()
+        consumePendingShowOverlayInvocationIfNeeded()
         configureApplicationMenuWhenAvailable(retries: 200)
         warmAppIndexInBackground()
         relocalizeDefaultAppNotesForCurrentLanguageAsync()
@@ -243,6 +255,54 @@
         guard isRecentUserClickNearDockArea() else { return false }
         showOrFocusOverlay()
         return false  // Suppress default "unhide all windows" behavior
+    }
+
+    func application(_ application: NSApplication, open urls: [URL]) {
+        for url in urls {
+            handleExternalInvocationURL(url)
+        }
+    }
+
+    private enum ExternalInvocationRoute {
+        case showOverlay
+    }
+
+    private func handleExternalInvocationURL(_ url: URL) {
+        guard externalInvocationRoute(for: url) == .showOverlay else {
+            Diagnostics.log("app.externalInvocation.ignored", [
+                "url": url.absoluteString
+            ])
+            return
+        }
+        requestShowOverlayFromExternalInvocation()
+    }
+
+    private func externalInvocationRoute(for url: URL) -> ExternalInvocationRoute? {
+        guard url.scheme?.lowercased() == Self.externalInvocationScheme else {
+            return nil
+        }
+        let host = url.host?.lowercased()
+        let path = url.path
+        if host == Self.externalInvocationShowHost, path.isEmpty || path == "/" {
+            return .showOverlay
+        }
+        return nil
+    }
+
+    private func requestShowOverlayFromExternalInvocation() {
+        guard didFinishLaunching else {
+            pendingShowOverlayInvocation = true
+            return
+        }
+        pendingShowOverlayInvocation = false
+        suppressReopenUntil = Date().addingTimeInterval(1.0)
+        dismissQuickSearchIfNeeded()
+        showOrFocusOverlay()
+    }
+
+    private func consumePendingShowOverlayInvocationIfNeeded() {
+        guard pendingShowOverlayInvocation else { return }
+        requestShowOverlayFromExternalInvocation()
     }
 
     private func isRecentUserClickNearDockArea() -> Bool {
@@ -293,6 +353,7 @@
         hideOverlay(force: true, discardWindow: true)
         unregisterHotkey(for: .main)
         unregisterHotkey(for: .quickSearch)
+        unregisterOverlaySpaceHotkey()
         TagDatabase.flushPendingCategorySchemeBackupBatch()
         removeOverlayKeyMonitor()
         removeQuickSearchExternalMouseMonitor()
@@ -480,14 +541,17 @@
 
         if activate && requiresForegroundOwnership
             && !shouldStayAccessoryForCurrentFullscreenSpace
-            && !shouldStayAccessoryForHiddenDockChrome
             && !shouldStayAccessoryForQuickOnlySearch {
             let keyWindow = isSettingsVisible ? settingsWindow : (isOverlayVisible ? overlayWindow : nil)
-            claimLauncherForeground(
-                keyWindow: keyWindow,
-                retries: isOverlayVisible && !isSettingsVisible ? 5 : 0,
-                overlayGeneration: isOverlayVisible ? overlayGeneration : nil
-            )
+            if shouldStayAccessoryForHiddenDockChrome {
+                beginLauncherForegroundOwnership(activate: true, keyWindow: keyWindow)
+            } else {
+                claimLauncherForeground(
+                    keyWindow: keyWindow,
+                    retries: isOverlayVisible && !isSettingsVisible ? 5 : 0,
+                    overlayGeneration: isOverlayVisible ? overlayGeneration : nil
+                )
+            }
         }
     }
 
@@ -1298,9 +1362,27 @@
         ])
         if id == LauncherHotkeyKind.quickSearch.eventID {
             showQuickSearchFromGlobalHotkey()
+        } else if id == Self.overlaySpaceHotkeyID {
+            handleOverlaySpaceHotkey()
         } else {
             performToggleOverlay(preferredScreen: nil)
         }
+    }
+
+    private func handleOverlaySpaceHotkey() {
+        guard canUseOverlaySpaceHotkey else { return }
+        unregisterOverlaySpaceHotkey()
+        requestQuickSearch(source: QuickSearchOpenSource.mainOverlay)
+    }
+
+    private var canUseOverlaySpaceHotkey: Bool {
+        overlayWindow?.isVisible == true
+            && !isSettingsVisible
+            && !isQuickSearchOpen
+            && !quickSearchOnlyOverlaySession
+            && !isInEditMode
+            && !isEditingAppNote
+            && !isModalInteractionActive
     }
 
     private func showQuickSearchFromGlobalHotkey() {
@@ -1358,6 +1440,46 @@
         }
     }
 
+    private func refreshOverlaySpaceHotkeyRegistration() {
+        if canUseOverlaySpaceHotkey {
+            registerOverlaySpaceHotkeyIfNeeded()
+        } else {
+            unregisterOverlaySpaceHotkey()
+        }
+    }
+
+    private func registerOverlaySpaceHotkeyIfNeeded() {
+        guard overlaySpaceHotkeyRef == nil else { return }
+
+        var hotkeyID = EventHotKeyID()
+        hotkeyID.signature = OSType(0x41505447) // 'APTG'
+        hotkeyID.id = Self.overlaySpaceHotkeyID
+
+        var newRef: EventHotKeyRef?
+        let status = RegisterEventHotKey(
+            UInt32(kVK_Space),
+            0,
+            hotkeyID,
+            GetApplicationEventTarget(),
+            0,
+            &newRef
+        )
+
+        if status == noErr, let newRef {
+            overlaySpaceHotkeyRef = newRef
+        } else {
+            overlaySpaceHotkeyRef = nil
+            Diagnostics.log("app.overlay.spaceHotkey.failed", ["status": status])
+        }
+    }
+
+    private func unregisterOverlaySpaceHotkey() {
+        if let overlaySpaceHotkeyRef {
+            UnregisterEventHotKey(overlaySpaceHotkeyRef)
+            self.overlaySpaceHotkeyRef = nil
+        }
+    }
+
     // MARK: - Preferences
 
     /// Hide overlay when any other window becomes key (catches Cmd+, via SwiftUI Settings).
@@ -1409,6 +1531,9 @@
             forContentRect: NSRect(origin: .zero, size: settingsSize)
         ).size
         window.identifier = NSUserInterfaceItemIdentifier("TagLauncherPreferencesWindow")
+        window.isRestorable = false
+        window.restorationClass = nil
+        _ = window.setFrameAutosaveName("")
         window.contentMinSize = settingsSize
         window.contentMaxSize = settingsSize
         window.minSize = settingsFrameSize
@@ -1441,6 +1566,7 @@
         window.makeKeyAndOrderFront(nil)
         window.orderFrontRegardless()
         settingsWindow = window
+        refreshOverlaySpaceHotkeyRegistration()
         refreshLauncherChromeState(
             activate: !overlayAvoidsSpaceSwitch,
             avoidSpaceSwitch: overlayAvoidsSpaceSwitch
@@ -1545,6 +1671,7 @@
                 activate: shouldRefocusOverlay && !self.overlayAvoidsSpaceSwitch,
                 avoidSpaceSwitch: self.overlayAvoidsSpaceSwitch
             )
+            self.refreshOverlaySpaceHotkeyRegistration()
             guard shouldRefocusOverlay else { return }
             DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { [weak self] in
                 self?.showOrFocusOverlay(preferredScreen: preferredScreen)
@@ -1559,7 +1686,9 @@
             object: nil,
             queue: .main
         ) { [weak self] notification in
-            self?.isInEditMode = (notification.userInfo?["active"] as? Bool) ?? false
+            guard let self else { return }
+            self.isInEditMode = (notification.userInfo?["active"] as? Bool) ?? false
+            self.refreshOverlaySpaceHotkeyRegistration()
         }
     }
 
@@ -1576,6 +1705,7 @@
                 self.promoteOverlayToForegroundInput()
             }
             self.updateOverlayLevelForTextInput()
+            self.refreshOverlaySpaceHotkeyRegistration()
         }
     }
 
@@ -1642,6 +1772,7 @@
                     self.hideOverlay(force: true, discardWindow: wasQuickSearchOnlyOverlay)
                 }
             }
+            self.refreshOverlaySpaceHotkeyRegistration()
         }
 
         NotificationCenter.default.addObserver(
@@ -1649,7 +1780,9 @@
             object: nil,
             queue: .main
         ) { [weak self] notification in
-            self?.isModalInteractionActive = (notification.userInfo?["active"] as? Bool) ?? false
+            guard let self else { return }
+            self.isModalInteractionActive = (notification.userInfo?["active"] as? Bool) ?? false
+            self.refreshOverlaySpaceHotkeyRegistration()
         }
 
         NotificationCenter.default.addObserver(
@@ -1757,6 +1890,7 @@
     }
 
     private func openPreferences(targetTab: String?) {
+        explicitPreferencesOpenRequestedAt = Date()
         dismissQuickSearchIfNeeded()
         TagDatabase.flushPendingCategorySchemeBackupBatch()
         if overlayAvoidsSpaceSwitch {
@@ -1789,6 +1923,25 @@
         prepareSettingsWindow(window)
     }
 
+    private func closeRestoredPreferencesWindowsDuringLaunch() {
+        for delay in [0.25, 0.8, 1.6, 2.6] {
+            DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
+                self?.closeRestoredPreferencesWindowIfNeeded()
+            }
+        }
+    }
+
+    private func closeRestoredPreferencesWindowIfNeeded() {
+        guard Date().timeIntervalSince(explicitPreferencesOpenRequestedAt) > 3.0 else { return }
+        for window in NSApp.windows where isSettingsWindowCandidate(window) {
+            detachSettingsWindow(window)
+            window.close()
+            if settingsWindow == window {
+                settingsWindow = nil
+            }
+        }
+    }
+
     private func requestPreferencesTabSelection(_ targetTab: String?) {
         guard let targetTab else { return }
         NotificationCenter.default.post(
diff --git a/src/Apptag/Info.plist b/src/Apptag/Info.plist
index 479ca9a..f8b2ea1 100644
--- a/src/Apptag/Info.plist
+++ b/src/Apptag/Info.plist
@@ -19,9 +19,20 @@
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-	<string>8.1.4</string>
+	<string>8.1.6</string>
+	<key>CFBundleURLTypes</key>
+	<array>
+		<dict>
+			<key>CFBundleURLName</key>
+			<string>com.taglauncher.app</string>
+			<key>CFBundleURLSchemes</key>
+			<array>
+				<string>taglauncher</string>
+			</array>
+		</dict>
+	</array>
 	<key>CFBundleVersion</key>
-	<string>20260627.1643</string>
+	<string>20260628.0230</string>
 	<key>LSApplicationCategoryType</key>
 	<string>public.app-category.utilities</string>
 	<key>LSMinimumSystemVersion</key>
diff --git a/src/Apptag/Localization/ar-Najdi.json b/src/Apptag/Localization/ar-Najdi.json
index d42ffa3..3f06a59 100644
--- a/src/Apptag/Localization/ar-Najdi.json
+++ b/src/Apptag/Localization/ar-Najdi.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "إيماءة لوحة التعقب",
+  "trackpadGesture.description": "اربط إيماءة لوحة التعقب في أداة خارجية بهالرابط عشان تعرض قائمة التطبيقات.",
+  "trackpadGesture.status": "أداة خارجية",
   "smart.category.GTD": "المهام وGTD",
   "smart.category.Notes": "الملاحظات",
   "smart.category.Meeting": "الاجتماعات",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "معاينة موضوع برو",
   "pro.theme.status.free": "المواضيع المجانية جاهزة الآن. فتح Pro للحفاظ على كل موضوع متقدم.",
   "pro.theme.status.preview": "معاينة موضوع برو. قم بإلغاء تأمين Pro للاحتفاظ به بعد انتهاء المعاينة.",
+  "pro.theme.status.previewCountdown": "قيد التجربة · باقي %time%",
   "pro.theme.status.unlocked": "تم فتح جميع السمات على جهاز Mac هذا."
 }
diff --git a/src/Apptag/Localization/ar.json b/src/Apptag/Localization/ar.json
index a1012e0..224c707 100644
--- a/src/Apptag/Localization/ar.json
+++ b/src/Apptag/Localization/ar.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "إيماءة لوحة التعقب",
+  "trackpadGesture.description": "اربط إيماءة لوحة التعقب في أداة خارجية بهذا الرابط لعرض قائمة التطبيقات.",
+  "trackpadGesture.status": "أداة خارجية",
   "smart.category.GTD": "المهام وGTD",
   "smart.category.Notes": "الملاحظات",
   "smart.category.Meeting": "الاجتماعات",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "معاينة موضوع برو",
   "pro.theme.status.free": "المواضيع المجانية جاهزة الآن. فتح Pro للحفاظ على كل موضوع متقدم.",
   "pro.theme.status.preview": "معاينة موضوع برو. قم بإلغاء تأمين Pro للاحتفاظ به بعد انتهاء المعاينة.",
+  "pro.theme.status.previewCountdown": "قيد المعاينة · متبقٍ %time%",
   "pro.theme.status.unlocked": "تم فتح جميع السمات على جهاز Mac هذا."
 }
diff --git a/src/Apptag/Localization/cs.json b/src/Apptag/Localization/cs.json
index 3e7868f..3790773 100644
--- a/src/Apptag/Localization/cs.json
+++ b/src/Apptag/Localization/cs.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Gesto trackpadu",
+  "trackpadGesture.description": "Namapujte gesto trackpadu v nástroji třetí strany na tuto URL a zobrazte seznam aplikací.",
+  "trackpadGesture.status": "Externí nástroj",
   "smart.category.GTD": "Úkoly a GTD",
   "smart.category.Notes": "Poznámky",
   "smart.category.Meeting": "Schůzky",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Náhled Pro téma",
   "pro.theme.status.free": "Volná témata jsou nyní připravena. Odemkněte Pro, abyste zachovali každé pokročilé téma.",
   "pro.theme.status.preview": "Zobrazení náhledu motivu Pro. Odemkněte Pro, abyste si jej ponechali i po skončení náhledu.",
+  "pro.theme.status.previewCountdown": "Náhled · zbývá %time%",
   "pro.theme.status.unlocked": "Všechna témata jsou na tomto Macu odemčená."
 }
diff --git a/src/Apptag/Localization/da.json b/src/Apptag/Localization/da.json
index 8e4a9e8..3ef0339 100644
--- a/src/Apptag/Localization/da.json
+++ b/src/Apptag/Localization/da.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Pegefeltbevægelse",
+  "trackpadGesture.description": "Knyt en pegefeltbevægelse i et tredjepartsværktøj til denne URL for at vise applisten.",
+  "trackpadGesture.status": "Eksternt værktøj",
   "smart.category.GTD": "Opgaver og GTD",
   "smart.category.Notes": "Noter",
   "smart.category.Meeting": "Møder",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Forhåndsvisning af Pro-tema",
   "pro.theme.status.free": "Gratis temaer er klar nu. Lås Pro op for at beholde alle avancerede temaer.",
   "pro.theme.status.preview": "Forhåndsvisning af et Pro-tema. Lås Pro op for at beholde den, når forhåndsvisningen slutter.",
+  "pro.theme.status.previewCountdown": "Forhåndsvisning · %time% tilbage",
   "pro.theme.status.unlocked": "Alle temaer er låst op på denne Mac."
 }
diff --git a/src/Apptag/Localization/de.json b/src/Apptag/Localization/de.json
index f087d60..b108400 100644
--- a/src/Apptag/Localization/de.json
+++ b/src/Apptag/Localization/de.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Trackpad-Geste",
+  "trackpadGesture.description": "Ordnen Sie in einem Drittanbieter-Tool eine Trackpad-Geste dieser URL zu, um die App-Liste anzuzeigen.",
+  "trackpadGesture.status": "Externes Tool",
   "smart.category.GTD": "Aufgaben und GTD",
   "smart.category.Notes": "Notizen",
   "smart.category.Meeting": "Besprechungen",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Vorschau des Pro-Themes",
   "pro.theme.status.free": "Kostenlose Themes sind jetzt fertig. Schalten Sie Pro frei, um alle erweiterten Themes beizubehalten.",
   "pro.theme.status.preview": "Vorschau eines Pro-Themes. Schalten Sie Pro frei, um es nach Ende der Vorschau zu behalten.",
+  "pro.theme.status.previewCountdown": "Vorschau · noch %time%",
   "pro.theme.status.unlocked": "Alle Themes sind auf diesem Mac freigeschaltet."
 }
diff --git a/src/Apptag/Localization/en.json b/src/Apptag/Localization/en.json
index 2921d09..72c4c0d 100644
--- a/src/Apptag/Localization/en.json
+++ b/src/Apptag/Localization/en.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Trackpad gesture",
+  "trackpadGesture.description": "Map a trackpad gesture in a third-party tool to this URL to show the app list.",
+  "trackpadGesture.status": "External tool",
   "smart.category.GTD": "Tasks & GTD",
   "smart.category.Notes": "Notes",
   "smart.category.Meeting": "Meetings",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Previewing Pro theme",
   "pro.theme.status.free": "Free themes are ready now. Unlock Pro to keep every advanced theme.",
   "pro.theme.status.preview": "Previewing a Pro theme. Unlock Pro to keep it after the preview ends.",
+  "pro.theme.status.previewCountdown": "Previewing · %time% left",
   "pro.theme.status.unlocked": "All themes are unlocked on this Mac."
 }
diff --git a/src/Apptag/Localization/es.json b/src/Apptag/Localization/es.json
index 45d8aad..df496e0 100644
--- a/src/Apptag/Localization/es.json
+++ b/src/Apptag/Localization/es.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Gesto del trackpad",
+  "trackpadGesture.description": "Asigna un gesto del trackpad en una herramienta de terceros a esta URL para mostrar la lista de apps.",
+  "trackpadGesture.status": "Herramienta externa",
   "smart.category.GTD": "Tareas y GTD",
   "smart.category.Notes": "Notas",
   "smart.category.Meeting": "Reuniones",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Vista previa del tema Pro",
   "pro.theme.status.free": "Los temas gratuitos ya están listos. Desbloquea Pro para conservar todos los temas avanzados.",
   "pro.theme.status.preview": "Vista previa de un tema Pro. Desbloquea Pro para conservarlo después de que finalice la vista previa.",
+  "pro.theme.status.previewCountdown": "Vista previa · quedan %time%",
   "pro.theme.status.unlocked": "Todos los temas están desbloqueados en esta Mac."
 }
diff --git a/src/Apptag/Localization/fr.json b/src/Apptag/Localization/fr.json
index 599fb7c..e9adb97 100644
--- a/src/Apptag/Localization/fr.json
+++ b/src/Apptag/Localization/fr.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Geste du trackpad",
+  "trackpadGesture.description": "Associez un geste du trackpad dans un outil tiers à cette URL pour afficher la liste des apps.",
+  "trackpadGesture.status": "Outil tiers",
   "smart.category.GTD": "Tâches et GTD",
   "smart.category.Notes": "Prises de notes",
   "smart.category.Meeting": "Réunions",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Aperçu du thème Pro",
   "pro.theme.status.free": "Les thèmes gratuits sont prêts maintenant. Débloquez Pro pour conserver tous les thèmes avancés.",
   "pro.theme.status.preview": "Prévisualisation d'un thème Pro. Déverrouillez Pro pour le conserver une fois l'aperçu terminé.",
+  "pro.theme.status.previewCountdown": "Aperçu · %time% restantes",
   "pro.theme.status.unlocked": "Tous les thèmes sont débloqués sur ce Mac."
 }
diff --git a/src/Apptag/Localization/id.json b/src/Apptag/Localization/id.json
index 2add81c..7944e87 100644
--- a/src/Apptag/Localization/id.json
+++ b/src/Apptag/Localization/id.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Gestur trackpad",
+  "trackpadGesture.description": "Petakan gestur trackpad di alat pihak ketiga ke URL ini untuk menampilkan daftar app.",
+  "trackpadGesture.status": "Alat eksternal",
   "smart.category.GTD": "Tugas dan GTD",
   "smart.category.Notes": "Catatan",
   "smart.category.Meeting": "Rapat",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Pratinjau tema Pro",
   "pro.theme.status.free": "Tema gratis sudah siap sekarang. Buka kunci Pro untuk mempertahankan setiap tema lanjutan.",
   "pro.theme.status.preview": "Pratinjau tema Pro. Buka kunci Pro untuk menyimpannya setelah pratinjau berakhir.",
+  "pro.theme.status.previewCountdown": "Pratinjau · tersisa %time%",
   "pro.theme.status.unlocked": "Semua tema tidak terkunci di Mac ini."
 }
diff --git a/src/Apptag/Localization/it.json b/src/Apptag/Localization/it.json
index 8c39924..5bca6ba 100644
--- a/src/Apptag/Localization/it.json
+++ b/src/Apptag/Localization/it.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Gesto del trackpad",
+  "trackpadGesture.description": "Associa un gesto del trackpad in uno strumento di terze parti a questo URL per mostrare l’elenco delle app.",
+  "trackpadGesture.status": "Strumento esterno",
   "smart.category.GTD": "Attività e GTD",
   "smart.category.Notes": "Note",
   "smart.category.Meeting": "Riunioni",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Anteprima del tema Pro",
   "pro.theme.status.free": "I temi gratuiti sono pronti ora. Sblocca Pro per mantenere tutti i temi avanzati.",
   "pro.theme.status.preview": "Anteprima di un tema Pro. Sblocca Pro per conservarlo al termine dell'anteprima.",
+  "pro.theme.status.previewCountdown": "Anteprima · %time% rimasti",
   "pro.theme.status.unlocked": "Tutti i temi sono sbloccati su questo Mac."
 }
diff --git a/src/Apptag/Localization/ja.json b/src/Apptag/Localization/ja.json
index c5248ed..3538026 100644
--- a/src/Apptag/Localization/ja.json
+++ b/src/Apptag/Localization/ja.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "トラックパッドジェスチャ",
+  "trackpadGesture.description": "サードパーティ製ツールでトラックパッドジェスチャをこのURLに割り当てると、アプリ一覧を表示できます。",
+  "trackpadGesture.status": "外部ツール",
   "smart.category.GTD": "タスクとGTD",
   "smart.category.Notes": "メモ",
   "smart.category.Meeting": "会議",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Pro テーマのプレビュー",
   "pro.theme.status.free": "無料のテーマが準備完了です。 Pro のロックを解除して、すべての高度なテーマを保持します。",
   "pro.theme.status.preview": "Pro テーマをプレビューしています。プレビュー終了後も保持するには、Pro のロックを解除してください。",
+  "pro.theme.status.previewCountdown": "プレビュー中 · 残り %time%",
   "pro.theme.status.unlocked": "この Mac ではすべてのテーマのロックが解除されています。"
 }
diff --git a/src/Apptag/Localization/ko.json b/src/Apptag/Localization/ko.json
index 662ce02..64ed521 100644
--- a/src/Apptag/Localization/ko.json
+++ b/src/Apptag/Localization/ko.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "트랙패드 제스처",
+  "trackpadGesture.description": "서드파티 도구에서 트랙패드 제스처를 이 URL에 연결하면 앱 목록이 열립니다.",
+  "trackpadGesture.status": "외부 도구",
   "smart.category.GTD": "작업 및 GTD",
   "smart.category.Notes": "메모",
   "smart.category.Meeting": "회의",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Pro 테마 미리보기",
   "pro.theme.status.free": "무료 테마가 이제 준비되었습니다. 모든 고급 테마를 유지하려면 Pro을(를) 잠금 해제하세요.",
   "pro.theme.status.preview": "Pro 테마를 미리 보는 중입니다. 미리보기가 끝난 후에도 유지하려면 Pro을(를) 잠금 해제하세요.",
+  "pro.theme.status.previewCountdown": "미리보기 중 · %time% 남음",
   "pro.theme.status.unlocked": "모든 테마는 이 Mac에서 잠금 해제됩니다."
 }
diff --git a/src/Apptag/Localization/ms.json b/src/Apptag/Localization/ms.json
index 357d4a6..d36d461 100644
--- a/src/Apptag/Localization/ms.json
+++ b/src/Apptag/Localization/ms.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Gerak isyarat pad jejak",
+  "trackpadGesture.description": "Petakan gerak isyarat pad jejak dalam alat pihak ketiga kepada URL ini untuk memaparkan senarai app.",
+  "trackpadGesture.status": "Alat luaran",
   "smart.category.GTD": "Tugasan dan GTD",
   "smart.category.Notes": "Nota",
   "smart.category.Meeting": "Mesyuarat",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Mempratonton tema Pro",
   "pro.theme.status.free": "Tema percuma sudah sedia sekarang. Buka kunci Pro untuk mengekalkan setiap tema lanjutan.",
   "pro.theme.status.preview": "Mempratonton tema Pro. Buka kunci Pro untuk menyimpannya selepas pratonton tamat.",
+  "pro.theme.status.previewCountdown": "Pratonton · %time% lagi",
   "pro.theme.status.unlocked": "Semua tema dibuka kunci pada Mac ini."
 }
diff --git a/src/Apptag/Localization/nb.json b/src/Apptag/Localization/nb.json
index 16b71ab..6a09b1c 100644
--- a/src/Apptag/Localization/nb.json
+++ b/src/Apptag/Localization/nb.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Styreflatebevegelse",
+  "trackpadGesture.description": "Koble en styreflatebevegelse i et tredjepartsverktøy til denne URL-en for å vise applisten.",
+  "trackpadGesture.status": "Eksternt verktøy",
   "smart.category.GTD": "Oppgaver og GTD",
   "smart.category.Notes": "Notater",
   "smart.category.Meeting": "Møter",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Forhåndsviser Pro-tema",
   "pro.theme.status.free": "Gratis temaer er klare nå. Lås opp Pro for å beholde alle avanserte temaer.",
   "pro.theme.status.preview": "Forhåndsvisning av et Pro-tema. Lås opp Pro for å beholde den etter at forhåndsvisningen er ferdig.",
+  "pro.theme.status.previewCountdown": "Forhåndsvisning · %time% igjen",
   "pro.theme.status.unlocked": "Alle temaer er låst opp på denne Mac."
 }
diff --git a/src/Apptag/Localization/nl.json b/src/Apptag/Localization/nl.json
index a2ee3ce..c438d8e 100644
--- a/src/Apptag/Localization/nl.json
+++ b/src/Apptag/Localization/nl.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Trackpadgebaar",
+  "trackpadGesture.description": "Koppel in een tool van derden een trackpadgebaar aan deze URL om de applijst te tonen.",
+  "trackpadGesture.status": "Externe tool",
   "smart.category.GTD": "Taken en GTD",
   "smart.category.Notes": "Notities",
   "smart.category.Meeting": "Vergaderingen",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Een voorbeeld bekijken van het Pro-thema",
   "pro.theme.status.free": "Gratis thema's zijn nu klaar. Ontgrendel Pro om elk geavanceerd thema te behouden.",
   "pro.theme.status.preview": "Een voorbeeld bekijken van een Pro thema. Ontgrendel Pro om deze te behouden nadat de preview is afgelopen.",
+  "pro.theme.status.previewCountdown": "Voorvertoning · nog %time%",
   "pro.theme.status.unlocked": "Alle thema's zijn ontgrendeld op deze Mac."
 }
diff --git a/src/Apptag/Localization/nn.json b/src/Apptag/Localization/nn.json
index 51f9c04..4a6f3d4 100644
--- a/src/Apptag/Localization/nn.json
+++ b/src/Apptag/Localization/nn.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Styreflategest",
+  "trackpadGesture.description": "Kopla ein styreflategest i eit tredjepartsverktøy til denne URL-en for å visa applista.",
+  "trackpadGesture.status": "Eksternt verktøy",
   "smart.category.GTD": "Oppgåver og GTD",
   "smart.category.Notes": "Notat",
   "smart.category.Meeting": "Møte",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Forhåndsviser Pro-tema",
   "pro.theme.status.free": "Gratis temaer er klare nå. Lås opp Pro for å beholde alle avanserte temaer.",
   "pro.theme.status.preview": "Forhåndsvisning av et Pro-tema. Lås opp Pro for å beholde den etter at forhåndsvisningen er ferdig.",
+  "pro.theme.status.previewCountdown": "Førehandsvising · %time% att",
   "pro.theme.status.unlocked": "Alle temaer er låst opp på denne Mac."
 }
diff --git a/src/Apptag/Localization/no.json b/src/Apptag/Localization/no.json
index 16b71ab..6a09b1c 100644
--- a/src/Apptag/Localization/no.json
+++ b/src/Apptag/Localization/no.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Styreflatebevegelse",
+  "trackpadGesture.description": "Koble en styreflatebevegelse i et tredjepartsverktøy til denne URL-en for å vise applisten.",
+  "trackpadGesture.status": "Eksternt verktøy",
   "smart.category.GTD": "Oppgaver og GTD",
   "smart.category.Notes": "Notater",
   "smart.category.Meeting": "Møter",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Forhåndsviser Pro-tema",
   "pro.theme.status.free": "Gratis temaer er klare nå. Lås opp Pro for å beholde alle avanserte temaer.",
   "pro.theme.status.preview": "Forhåndsvisning av et Pro-tema. Lås opp Pro for å beholde den etter at forhåndsvisningen er ferdig.",
+  "pro.theme.status.previewCountdown": "Forhåndsvisning · %time% igjen",
   "pro.theme.status.unlocked": "Alle temaer er låst opp på denne Mac."
 }
diff --git a/src/Apptag/Localization/pl.json b/src/Apptag/Localization/pl.json
index b61b7f3..b6ef136 100644
--- a/src/Apptag/Localization/pl.json
+++ b/src/Apptag/Localization/pl.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Gest gładzika",
+  "trackpadGesture.description": "Przypisz gest gładzika w narzędziu innej firmy do tego URL-a, aby pokazać listę aplikacji.",
+  "trackpadGesture.status": "Narzędzie zewnętrzne",
   "smart.category.GTD": "Zadania i GTD",
   "smart.category.Notes": "Notatki",
   "smart.category.Meeting": "Spotkania",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Podgląd motywu Pro",
   "pro.theme.status.free": "Darmowe motywy są już gotowe. Odblokuj Pro, aby zachować każdy zaawansowany motyw.",
   "pro.theme.status.preview": "Podgląd motywu Pro. Odblokuj Pro, aby zachować go po zakończeniu podglądu.",
+  "pro.theme.status.previewCountdown": "Podgląd · zostało %time%",
   "pro.theme.status.unlocked": "Wszystkie motywy są odblokowane na tym Mac."
 }
diff --git a/src/Apptag/Localization/pt-BR.json b/src/Apptag/Localization/pt-BR.json
index 3a17fd9..b6f35a4 100644
--- a/src/Apptag/Localization/pt-BR.json
+++ b/src/Apptag/Localization/pt-BR.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Gesto do trackpad",
+  "trackpadGesture.description": "Mapeie um gesto do trackpad em uma ferramenta de terceiros para esta URL para mostrar a lista de apps.",
+  "trackpadGesture.status": "Ferramenta externa",
   "smart.category.GTD": "Tarefas e GTD",
   "smart.category.Notes": "Notas",
   "smart.category.Meeting": "Reuniões",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Visualizando o tema Pro",
   "pro.theme.status.free": "Os temas gratuitos estão prontos agora. Desbloqueie Pro para manter todos os temas avançados.",
   "pro.theme.status.preview": "Visualizando um tema Pro. Desbloqueie Pro para mantê-lo após o término da visualização.",
+  "pro.theme.status.previewCountdown": "Prévia · faltam %time%",
   "pro.theme.status.unlocked": "Todos os temas são desbloqueados neste Mac."
 }
diff --git a/src/Apptag/Localization/ro.json b/src/Apptag/Localization/ro.json
index f5f70e7..a0fedd2 100644
--- a/src/Apptag/Localization/ro.json
+++ b/src/Apptag/Localization/ro.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Gest trackpad",
+  "trackpadGesture.description": "Asociază un gest de trackpad dintr-un instrument terț cu acest URL pentru a afișa lista de aplicații.",
+  "trackpadGesture.status": "Instrument extern",
   "smart.category.GTD": "Sarcini și GTD",
   "smart.category.Notes": "Note",
   "smart.category.Meeting": "Întâlniri",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Previzualizarea temei Pro",
   "pro.theme.status.free": "Temele gratuite sunt gata acum. Deblocați Pro pentru a păstra fiecare temă avansată.",
   "pro.theme.status.preview": "Previzualizarea unei teme Pro. Deblocați Pro pentru a-l păstra după terminarea previzualizării.",
+  "pro.theme.status.previewCountdown": "Previzualizare · %time% rămase",
   "pro.theme.status.unlocked": "Toate temele sunt deblocate pe acest Mac."
 }
diff --git a/src/Apptag/Localization/ru.json b/src/Apptag/Localization/ru.json
index 0238ffe..3238dd8 100644
--- a/src/Apptag/Localization/ru.json
+++ b/src/Apptag/Localization/ru.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Жест трекпада",
+  "trackpadGesture.description": "Назначьте жест трекпада в сторонней утилите на этот URL, чтобы открыть список приложений.",
+  "trackpadGesture.status": "Внешний инструмент",
   "smart.category.GTD": "Задачи и GTD",
   "smart.category.Notes": "Заметки",
   "smart.category.Meeting": "Встречи",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Предварительный просмотр темы Pro",
   "pro.theme.status.free": "Бесплатные темы уже готовы. Разблокируйте Pro, чтобы сохранить все расширенные темы.",
   "pro.theme.status.preview": "Предварительный просмотр темы Pro. Разблокируйте Pro, чтобы сохранить его после завершения предварительного просмотра.",
+  "pro.theme.status.previewCountdown": "Предпросмотр · осталось %time%",
   "pro.theme.status.unlocked": "На этом Mac разблокированы все темы."
 }
diff --git a/src/Apptag/Localization/sr-Cyrl.json b/src/Apptag/Localization/sr-Cyrl.json
index af1ca34..13ae31b 100644
--- a/src/Apptag/Localization/sr-Cyrl.json
+++ b/src/Apptag/Localization/sr-Cyrl.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Гест на трекпеду",
+  "trackpadGesture.description": "Повежите гест на трекпеду у алату треће стране са овом URL адресом да бисте приказали листу апликација.",
+  "trackpadGesture.status": "Спољни алат",
   "smart.category.GTD": "Задаци и GTD",
   "smart.category.Notes": "Белешке",
   "smart.category.Meeting": "Састанци",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Преглед Pro теме",
   "pro.theme.status.free": "Бесплатне теме су сада спремне. Откључајте Pro да бисте задржали сваку напредну тему.",
   "pro.theme.status.preview": "Прегледате Pro тему. Откључајте Pro да бисте је задржали након завршетка прегледа.",
+  "pro.theme.status.previewCountdown": "Преглед · преостало %time%",
   "pro.theme.status.unlocked": "Све теме су откључане на овом Mac-у."
 }
diff --git a/src/Apptag/Localization/sv.json b/src/Apptag/Localization/sv.json
index 48e3b51..66c6569 100644
--- a/src/Apptag/Localization/sv.json
+++ b/src/Apptag/Localization/sv.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Styrplattegest",
+  "trackpadGesture.description": "Koppla en styrplattegest i ett tredjepartsverktyg till denna URL för att visa applistan.",
+  "trackpadGesture.status": "Externt verktyg",
   "smart.category.GTD": "Uppgifter och GTD",
   "smart.category.Notes": "Anteckningar",
   "smart.category.Meeting": "Möten",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Förhandsgranskning av Pro-tema",
   "pro.theme.status.free": "Gratis teman är klara nu. Lås upp Pro för att behålla alla avancerade teman.",
   "pro.theme.status.preview": "Förhandsgranskning av ett Pro-tema. Lås upp Pro för att behålla den efter att förhandsvisningen är slut.",
+  "pro.theme.status.previewCountdown": "Förhandsvisning · %time% kvar",
   "pro.theme.status.unlocked": "Alla teman är upplåsta på denna Mac."
 }
diff --git a/src/Apptag/Localization/th.json b/src/Apptag/Localization/th.json
index bdb104f..ab3adea 100644
--- a/src/Apptag/Localization/th.json
+++ b/src/Apptag/Localization/th.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "ท่าทางแทร็คแพด",
+  "trackpadGesture.description": "จับคู่ท่าทางแทร็คแพดในเครื่องมือภายนอกกับ URL นี้เพื่อแสดงรายการแอป",
+  "trackpadGesture.status": "เครื่องมือภายนอก",
   "smart.category.GTD": "งานและ GTD",
   "smart.category.Notes": "โน้ต",
   "smart.category.Meeting": "การประชุม",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "กำลังดูตัวอย่างธีม Pro",
   "pro.theme.status.free": "ธีมฟรีพร้อมใช้งานแล้ว ปลดล็อก Pro เพื่อเก็บธีมขั้นสูงไว้ทุกอัน",
   "pro.theme.status.preview": "การดูตัวอย่างธีม Pro ปลดล็อก Pro เพื่อเก็บไว้หลังจากการดูตัวอย่างสิ้นสุดลง",
+  "pro.theme.status.previewCountdown": "กำลังดูตัวอย่าง · เหลือ %time%",
   "pro.theme.status.unlocked": "ปลดล็อคธีมทั้งหมดแล้วใน Mac นี้"
 }
diff --git a/src/Apptag/Localization/tr.json b/src/Apptag/Localization/tr.json
index aa1d40f..ff2491e 100644
--- a/src/Apptag/Localization/tr.json
+++ b/src/Apptag/Localization/tr.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "İzleme dörtgeni hareketi",
+  "trackpadGesture.description": "Uygulama listesini göstermek için üçüncü taraf bir araçta izleme dörtgeni hareketini bu URL’ye bağlayın.",
+  "trackpadGesture.status": "Harici araç",
   "smart.category.GTD": "Görevler ve GTD",
   "smart.category.Notes": "Notlar",
   "smart.category.Meeting": "Toplantılar",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Pro temasının önizlemesi yapılıyor",
   "pro.theme.status.free": "Ücretsiz temalar artık hazır. Tüm gelişmiş temaları korumak için Pro'nun kilidini açın.",
   "pro.theme.status.preview": "Bir Pro temasının önizlenmesi. Önizleme sona erdikten sonra onu korumak için Pro kilidini açın.",
+  "pro.theme.status.previewCountdown": "Önizleme · %time% kaldı",
   "pro.theme.status.unlocked": "Bu Mac üzerinde tüm temaların kilidi açıldı."
 }
diff --git a/src/Apptag/Localization/uk.json b/src/Apptag/Localization/uk.json
index 7fd913b..ebba9f2 100644
--- a/src/Apptag/Localization/uk.json
+++ b/src/Apptag/Localization/uk.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Жест трекпада",
+  "trackpadGesture.description": "Призначте жест трекпада у сторонній утиліті на цю URL-адресу, щоб показати список програм.",
+  "trackpadGesture.status": "Зовнішній інструмент",
   "smart.category.GTD": "Завдання і GTD",
   "smart.category.Notes": "Нотатки",
   "smart.category.Meeting": "Зустрічі",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "попередній перегляд теми Pro",
   "pro.theme.status.free": "безкоштовні теми готові. Розблокуйте Pro, щоб зберегти всі розширені теми.",
   "pro.theme.status.preview": "попередній перегляд теми Pro. Розблокуйте Pro, щоб зберегти його після закінчення попереднього перегляду.",
+  "pro.theme.status.previewCountdown": "Попередній перегляд · залишилось %time%",
   "pro.theme.status.unlocked": "усі теми розблоковано на цьому Mac."
 }
diff --git a/src/Apptag/Localization/vi.json b/src/Apptag/Localization/vi.json
index c9a3da7..638562d 100644
--- a/src/Apptag/Localization/vi.json
+++ b/src/Apptag/Localization/vi.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "Quick Search (global)",
   "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
   "quickSearch.spaceDisplay": "Space",
+  "trackpadGesture.title": "Cử chỉ bàn di chuột",
+  "trackpadGesture.description": "Gán một cử chỉ bàn di chuột trong công cụ bên thứ ba vào URL này để hiện danh sách ứng dụng.",
+  "trackpadGesture.status": "Công cụ bên ngoài",
   "smart.category.GTD": "Công việc và GTD",
   "smart.category.Notes": "Ghi chú",
   "smart.category.Meeting": "Cuộc họp",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "Xem trước chủ đề Pro",
   "pro.theme.status.free": "Các chủ đề miễn phí hiện đã sẵn sàng. Mở khóa Pro để giữ mọi chủ đề nâng cao.",
   "pro.theme.status.preview": "Xem trước chủ đề Pro. Mở khóa Pro để giữ nó sau khi bản xem trước kết thúc.",
+  "pro.theme.status.previewCountdown": "Đang xem thử · còn %time%",
   "pro.theme.status.unlocked": "Tất cả chủ đề đều được mở khóa trên Mac này."
 }
diff --git a/src/Apptag/Localization/zh-Hans.json b/src/Apptag/Localization/zh-Hans.json
index fd1ca3b..3ca718d 100644
--- a/src/Apptag/Localization/zh-Hans.json
+++ b/src/Apptag/Localization/zh-Hans.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "快捷搜索(全局)",
   "quickSearch.globalHotkeyDesc": "不在 TagLauncher 主界面时,直接打开快捷搜索。全局有效。",
   "quickSearch.spaceDisplay": "Space(空格键)",
+  "trackpadGesture.title": "触控板手势",
+  "trackpadGesture.description": "在第三方手势工具中把触控板手势映射到这个 URL,即可显示应用列表。",
+  "trackpadGesture.status": "外部工具",
   "smart.category.GTD": "任务与 GTD",
   "smart.category.Notes": "笔记",
   "smart.category.Meeting": "会议",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "正在预览 Pro 主题",
   "pro.theme.status.free": "免费主题已经可用。解锁 Pro 后可长期使用全部高级主题。",
   "pro.theme.status.preview": "正在预览 Pro 主题。解锁 Pro 后可在预览结束后继续保留。",
+  "pro.theme.status.previewCountdown": "体验中 · 剩余 %time%",
   "pro.theme.status.unlocked": "这台 Mac 上的全部主题都已解锁。"
 }
diff --git a/src/Apptag/Localization/zh-Hant.json b/src/Apptag/Localization/zh-Hant.json
index 7eff870..a2d562b 100644
--- a/src/Apptag/Localization/zh-Hant.json
+++ b/src/Apptag/Localization/zh-Hant.json
@@ -187,6 +187,9 @@
   "quickSearch.globalHotkey": "快捷搜尋(全域)",
   "quickSearch.globalHotkeyDesc": "不在 TagLauncher 主介面時,直接打開快捷搜尋。全域有效。",
   "quickSearch.spaceDisplay": "Space(空格鍵)",
+  "trackpadGesture.title": "觸控板手勢",
+  "trackpadGesture.description": "在第三方手勢工具中把觸控板手勢映射到這個 URL,即可顯示應用列表。",
+  "trackpadGesture.status": "外部工具",
   "smart.category.GTD": "任務與 GTD",
   "smart.category.Notes": "筆記",
   "smart.category.Meeting": "會議",
@@ -290,5 +293,6 @@
   "pro.theme.preview.status": "正在預覽 Pro 主題",
   "pro.theme.status.free": "免費主題已可使用。解鎖 Pro 後可長期使用全部進階主題。",
   "pro.theme.status.preview": "正在預覽 Pro 主題。解鎖 Pro 後可在預覽結束後繼續保留。",
+  "pro.theme.status.previewCountdown": "體驗中 · 剩餘 %time%",
   "pro.theme.status.unlocked": "這台 Mac 上的全部主題都已解鎖。"
 }
diff --git a/src/Apptag/PreferencesView.swift b/src/Apptag/PreferencesView.swift
index 52b09ab..97c2152 100644
--- a/src/Apptag/PreferencesView.swift
+++ b/src/Apptag/PreferencesView.swift
@@ -3,6 +3,8 @@
 
 // MARK: - Preferences View
 
+private let themePreviewCountdownTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
+
 private enum SettingsTab: String, CaseIterable, Identifiable {
     case language
     case general
@@ -85,6 +87,7 @@
     @State private var selectedTab: SettingsTab = .general
     @State private var selectedInitialLayoutMode: InitialLayoutMode = .smart
     @State private var settingsProPromptFeature: ProFeature? = nil
+    @State private var themePreviewNow = Date()
 
     init(initialTabRawValue: String? = nil) {
         let initialTab = initialTabRawValue.flatMap(SettingsTab.init(rawValue:)) ?? .general
@@ -610,6 +613,10 @@
 
     private var themeStatusSummary: String {
         if proEntitlement.isPreviewingTheme {
+            if let countdown = themePreviewCountdownText {
+                return tr("pro.theme.status.previewCountdown")
+                    .replacingOccurrences(of: "%time%", with: countdown)
+            }
             return tr("pro.theme.status.preview")
         }
         switch proEntitlement.accessState {
@@ -635,7 +642,7 @@
 
     private func themeOptionAccessory(for theme: AppGridTheme) -> ThemeOptionAccessory {
         if let preview = proEntitlement.themePreviewState?.theme, preview == theme {
-            return .pill(text: tr("pro.card.previewBadge"), style: .preview)
+            return .countdown(text: themePreviewCountdownText ?? "00:00")
         }
         if !proEntitlement.isUnlocked && theme.requiresPro {
             return .pill(text: tr("pro.card.badge"), style: .locked)
@@ -647,6 +654,24 @@
         proEntitlement.clearTransientOperationState()
         withAnimation(.spring(response: 0.22, dampingFraction: 0.84)) {
             settingsProPromptFeature = feature
+        }
+    }
+
+    private var themePreviewCountdownText: String? {
+        guard let preview = proEntitlement.themePreviewState else { return nil }
+        return Self.formatThemePreviewCountdown(seconds: preview.remainingSeconds(at: themePreviewNow))
+    }
+
+    private static func formatThemePreviewCountdown(seconds: Int) -> String {
+        let clamped = max(0, seconds)
+        return String(format: "%02d:%02d", clamped / 60, clamped % 60)
+    }
+
+    private func refreshThemePreviewCountdown(now: Date = Date()) {
+        themePreviewNow = now
+        guard let preview = proEntitlement.themePreviewState else { return }
+        if preview.remainingSeconds(at: now) <= 0 {
+            proEntitlement.stopThemePreview()
         }
     }
 
@@ -761,6 +786,14 @@
                 displayText: LauncherHotkey.quickSearch.displayString,
                 statusText: hotkeyStatusText(for: .quickSearch),
                 statusTone: hotkeyStatusTone(for: .quickSearch)
+            )
+
+            StaticHotkeyInfoRow(
+                title: tr("trackpadGesture.title"),
+                description: tr("trackpadGesture.description"),
+                displayText: "taglauncher://show",
+                statusText: tr("trackpadGesture.status"),
+                statusTone: .neutral
             )
         }
     }
@@ -1473,6 +1506,10 @@
         .onAppear {
             syncSelectedLanguage()
             showPendingHotkeyWarningIfNeeded()
+            refreshThemePreviewCountdown()
+        }
+        .onReceive(themePreviewCountdownTimer) { now in
+            refreshThemePreviewCountdown(now: now)
         }
         .onReceive(NotificationCenter.default.publisher(for: .tagLauncherHotkeyRegistrationChanged)) { _ in
             showPendingHotkeyWarningIfNeeded()
@@ -1662,6 +1699,7 @@
 private enum ThemeOptionAccessory {
     case checkmark
     case circle
+    case countdown(text: String)
     case pill(text: String, style: ProStatusPillStyle)
 }
 
@@ -1739,6 +1777,22 @@
             Image(systemName: "circle")
                 .font(.system(size: 16, weight: .semibold))
                 .foregroundStyle(Color.secondary.opacity(0.42))
+        case .countdown(let text):
+            Text(text)
+                .font(.system(size: 10, weight: .semibold, design: .monospaced))
+                .foregroundStyle(Color(red: 0.30, green: 0.22, blue: 0.08))
+                .lineLimit(1)
+                .frame(minWidth: 42)
+                .padding(.horizontal, 8)
+                .padding(.vertical, 4)
+                .background(
+                    Capsule(style: .continuous)
+                        .fill(Color.orange.opacity(0.14))
+                )
+                .overlay(
+                    Capsule(style: .continuous)
+                        .stroke(Color.orange.opacity(0.28), lineWidth: 1)
+                )
         case .pill(let text, let style):
             ProStatusPill(text: text, style: style, compact: true)
         }
diff --git a/src/Apptag/ProEntitlement.swift b/src/Apptag/ProEntitlement.swift
index e1c8c5e..4841e55 100644
--- a/src/Apptag/ProEntitlement.swift
+++ b/src/Apptag/ProEntitlement.swift
@@ -97,6 +97,10 @@
     let theme: AppGridTheme
     let startedAt: Date
     let endsAt: Date
+
+    func remainingSeconds(at date: Date = Date()) -> Int {
+        max(0, Int(ceil(endsAt.timeIntervalSince(date))))
+    }
 }
 
 struct ProNoteQuotaStatus: Equatable {
diff --git a/src/Apptag/SmartCategorization/SmartCategory.swift b/src/Apptag/SmartCategorization/SmartCategory.swift
index 4fc10f0..949bdd3 100644
--- a/src/Apptag/SmartCategorization/SmartCategory.swift
+++ b/src/Apptag/SmartCategorization/SmartCategory.swift
@@ -251,6 +251,8 @@
 
     var smartStartDefaultCategoryID: SmartCategoryID? {
         switch self {
+        case .meeting:
+            return .communication
         case .pdf, .productivity, .writing:
             return .office
         case .fileManagement, .transfer, .utilities, .system, .systemMaintenance,
@@ -295,7 +297,6 @@
         .communication,
         .gtd,
         .notes,
-        .meeting,
         .office,
         .systemEnhancement,
         .aiTools,
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index 00d040c..0ecfb57 100644
--- a/src/CHANGELOG.md
+++ b/src/CHANGELOG.md
@@ -2,6 +2,27 @@
 
 ## [Unreleased]
 
+## [8.1.6] — 2026-06-28
+
+- Pro 主题免费体验增加可见倒计时:主题页状态行显示剩余时间,当前预览主题卡片显示 `MM:SS`,体验到期后静默恢复到用户真实可用主题
+- 倒计时从真实 `ProThemePreviewState.endsAt` 推导,不在文案中写死 5 分钟;切换 Pro 主题会重置计时,选择免费主题会停止预览并保存免费主题
+- SmartStart 智能初始化默认标签从 13 个进一步收敛为 12 个:`Meeting / 会议` 不再单独创建默认标签,统一归入 `Communication / 沟通`
+- 补齐 29 个语种的 Pro 主题倒计时文案,并更新 SmartStart 默认标签需求文档与 QA 门禁
+- 修复隐藏 Dock 图标时 AppGrid 通过热键打开后键盘焦点不稳定的问题,确保 `Esc` 可正常关闭面板且不会重新显示 Dock 图标
+- 修复全屏 Space 中 AppGrid 按 `Space` 打开 Quick Search 的键盘路由:仅在 AppGrid 可见且非编辑 / 非设置 / 非文本输入状态下临时接管裸 `Space`,不新增系统权限
+- 加固窗口回归门禁,覆盖 Dock 隐藏、Quick Search、设置窗口、文件面板和全屏 Space 的层级与键盘路径
+- 版本号更新为 `8.1.6`,Build 更新为 `20260628.0230`
+
+## [8.1.5] — 2026-06-27
+
+- 新增外部 URL 唤起入口:`taglauncher://show`,可由 BetterTouchTool 等第三方手势工具映射触控板手势后稳定显示 / 聚焦 App Grid
+- `taglauncher://show` 复用现有 AppGrid 显示链路,不做 toggle,不接管系统原生触控板手势,不新增 Accessibility / Input Monitoring 权限,也不使用私有 multitouch API
+- 冷启动收到 URL 唤起时会排队到 App 初始化完成后再显示 App Grid;运行中收到 URL 唤起时会清理 Quick Search 临时态并聚焦主面板
+- 在设置 > 快捷键新增触控板手势说明,显示第三方工具应调用的 `taglauncher://show`;补齐 29 个语种文案
+- 新增 `Scripts/url_scheme_activation_qa.sh`,覆盖 URL Scheme 注册、show/focus 语义、冷启动 pending、本地化文案和私有手势 API 禁止项
+- 加固 `Scripts/window_logic_qa.sh` 的 Dock 图标断言:本机 Dock 固定正式版 TagLauncher 时,窗口回归只统计当前 QA 构建路径,避免误报重复 Dock 图标
+- 版本号更新为 `8.1.5`,Build 更新为 `20260627.1908`
+
 ## [8.1.4] — 2026-06-27
 
 - 继续精简 SmartStart 智能初始化默认分类:新用户 / 重置智能初始化方案后的默认标签从 15 个收敛为 13 个
diff --git a/src/Docs/Requirements/2026-06-27-pro-theme-preview-countdown-prd.md b/src/Docs/Requirements/2026-06-27-pro-theme-preview-countdown-prd.md
new file mode 100644
index 0000000..25a452c
--- /dev/null
+++ b/src/Docs/Requirements/2026-06-27-pro-theme-preview-countdown-prd.md
@@ -0,0 +1,376 @@
+# PRD:Pro 主题体验倒计时
+
+日期:2026-06-27
+状态:已按 8.1.6 落地并进入 QA 回归
+目标分支:`codex/appgrid-usage-tips`
+目标范围:免费版 + Pro 主题体验
+最低系统:macOS 14.0
+
+## 0. 8.1.6 落地口径
+
+- 到期不新增阻塞弹窗,只退出预览并恢复用户真实可用主题。
+- 当前预览主题卡片只显示紧凑 `MM:SS`,不显示额外“体验中”字样。
+- 倒计时文案不写死 5 分钟,从 `ProThemePreviewState.endsAt` 推导真实剩余时间。
+- 29 个语言文件新增 `pro.theme.status.previewCountdown`,统一保留 `%time%` 占位符。
+
+## 1. 摘要
+
+为 Pro 主题的免费体验状态增加可见倒计时。
+
+当前免费用户可以点击 Pro 专属主题并临时体验该主题。这个体验有时间限制,但界面没有清楚告诉用户还剩多久。体验结束后,App Grid 会回到用户真实可用的主题;如果用户不知道这是限时体验,很容易误以为主题保存失败、设置丢失,或者 Pro 权益逻辑不稳定。
+
+本需求要求在现有主题页界面中增加一个轻量倒计时。倒计时必须以当前真实体验时长为准。当前已核实的代码基线是:每次 Pro 主题体验 5 分钟。如果后续产品决定调整体验时长,倒计时必须自动反映真实剩余时间,不允许在文案里写死“5 分钟”。
+
+## 2. 当前产品事实
+
+当前产品行为如下:
+
+- 免费主题:默认、深蓝、黑色。
+- Pro 主题:粉色、紫色、绿色、蓝色、炫彩。
+- 免费用户点击 Pro 主题后,可以进入临时体验。
+- 体验中的 Pro 主题不会写入用户真实保存的主题设置。
+- 当前体验时长是 5 分钟。
+- 体验中点击另一个 Pro 主题,会开始一轮新的体验。
+- 体验中点击免费主题,会退出体验并保存该免费主题。
+- 体验中购买或恢复 Pro 成功后,当前体验主题应成为用户真实保存的主题。
+
+给研发定位用的当前代码事实:
+
+- 体验时长当前定义为 `5 * 60`。
+- QA 可以通过现有 Pro QA override 缩短体验时长。
+- 主题页已经有轻量 Pro 状态行。
+- 正在体验的 Pro 主题卡片已经有预览标识。
+
+## 3. 问题
+
+当前界面只告诉用户“正在体验 Pro 主题”,但没有告诉用户体验会持续多久、何时结束。
+
+这会带来三个用户可感知问题:
+
+1. 用户无法在体验结束前形成正确预期。
+2. 用户可能把自动恢复误判为 bug 或设置保存失败。
+3. Pro 转化语境变弱,因为用户没有明确理解“这是临时开放给你体验”。
+
+## 4. 产品目标
+
+### 4.1 用户目标
+
+免费用户应该明确知道:
+
+- 自己正在体验 Pro 主题。
+- 这次体验是临时的。
+- 当前还剩多少时间。
+- 解锁 Pro 后可以继续保留该主题。
+
+### 4.2 产品目标
+
+产品应做到:
+
+- 让 Pro 主题体验状态自解释。
+- 降低体验结束时的困惑。
+- 保持主题页克制、紧凑、原生。
+- 不增加重营销界面,不增加额外决策步骤。
+
+## 5. 不做范围
+
+本需求不包含:
+
+- 不把体验时长从 5 分钟改为 15 分钟。
+- 不在开始体验前新增弹窗。
+- 不在体验结束时新增阻塞弹窗。
+- 不增加进度条、环形计时器、滑杆或大型倒计时组件。
+- 不改变 Pro 购买流程。
+- 不改变免费主题列表。
+- 不改变哪些主题属于 Pro。
+- 不改变免费用户的主题持久化规则。
+- 不改变 App Grid 主题视觉。
+- 不改变容器玻璃、App 图标、布局、拖拽、备注、排序、导入或导出行为。
+
+如果产品决定调整体验时长,应作为独立产品决策处理。本 PRD 只要求倒计时反映真实配置的体验剩余时间。
+
+## 6. 交互需求
+
+### 6.1 主题页状态行
+
+当免费用户正在体验 Pro 主题时,现有主题页 Pro 状态行必须显示剩余时间。
+
+推荐中文显示:
+
+```text
+体验中 · 剩余 04:59
+```
+
+推荐英文显示:
+
+```text
+Previewing · 04:59 left
+```
+
+状态行仍应保留 Pro 价值提示,但倒计时必须直接可见,不能依赖 hover、tooltip 或二级面板。
+
+可接受的紧凑中文备选:
+
+```text
+Pro 主题体验中 · 04:59
+```
+
+```text
+剩余 04:59 · 解锁 Pro 后可永久保留
+```
+
+### 6.2 当前体验主题卡片
+
+正在体验的 Pro 主题卡片,必须在现有 accessory 区域显示紧凑时间提示。
+
+优先显示:
+
+```text
+04:59
+```
+
+如果空间允许,可显示:
+
+```text
+体验中 04:59
+```
+
+倒计时只作用于当前正在体验的 Pro 主题卡片。其它锁定的 Pro 主题继续显示普通 Pro 标识。
+
+### 6.3 时间格式
+
+倒计时格式必须稳定、易扫读:
+
+- 体验剩余时间小于 1 小时时,使用 `MM:SS`。
+- 秒数必须补齐两位。
+- 示例:`05:00`、`04:59`、`01:05`、`00:09`。
+- 不显示小数秒。
+- 不显示负数时间。
+- 到期后应退出体验状态,不能长期停留在 `00:00`。
+
+以当前 5 分钟基线看,`MM:SS` 足够。如果未来产品把体验时长提高到 59 分钟以上,需另行决定是否改用小时格式。
+
+### 6.4 开始体验
+
+免费用户点击 Pro 主题时:
+
+1. 立即开始主题体验。
+2. App Grid 切换为该 Pro 主题效果。
+3. 主题页状态行进入体验状态。
+4. 被选中的 Pro 主题卡片显示倒计时。
+5. 倒计时从真实配置体验时长开始。
+
+当前基线下,初始可见值应接近 `05:00`。
+
+### 6.5 体验中切换 Pro 主题
+
+免费用户已经在体验 Pro 主题 A,此时点击 Pro 主题 B:
+
+1. 体验主题切换为 B。
+2. 倒计时为 B 重新开始。
+3. A 的主题卡片恢复普通 Pro 锁定状态。
+4. B 的主题卡片显示倒计时。
+
+界面上不能同时出现两个主题卡片倒计时。
+
+### 6.6 体验中选择免费主题
+
+免费用户体验 Pro 主题时选择免费主题:
+
+1. 体验立即结束。
+2. 被选择的免费主题成为真实保存主题。
+3. 倒计时消失。
+4. 主题页恢复普通免费状态。
+
+不需要警告弹窗。
+
+### 6.7 体验到期
+
+倒计时结束时:
+
+1. Pro 主题体验自动结束。
+2. App Grid 恢复到用户真实可用主题。
+3. 倒计时消失。
+4. 主题页恢复普通免费状态。
+
+推荐轻量反馈:
+
+```text
+主题体验已结束
+```
+
+如果产品内已有轻量 inline 或 toast 反馈模式,可以复用。不要为到期新增阻塞弹窗。
+
+### 6.8 体验中购买或恢复 Pro
+
+用户在体验 Pro 主题时购买或恢复 Pro 成功:
+
+1. 当前体验主题成为真实保存主题。
+2. 倒计时消失。
+3. 主题页进入已解锁状态。
+4. Pro 主题卡片显示选中状态,而不是计时器。
+
+用户不应在成功解锁后丢失正在体验的主题。
+
+### 6.9 关闭与重新打开界面
+
+倒计时应表示真实剩余时间,而不是界面可见时间。
+
+用户可感知行为要求:
+
+- 设置窗口关闭时,如果体验仍在进行,体验计时继续按现有规则运行。
+- 用户在到期前重新打开设置页,倒计时显示当前真实剩余时间。
+- 如果体验在设置页关闭期间到期,重新打开设置页后显示普通免费状态。
+- 如果 App 退出并重启,除非现有体验系统已经支持恢复,否则本需求不要求恢复上次体验。
+
+## 7. 信息架构
+
+本功能归属:
+
+```text
+设置
+└── 主题
+    ├── Pro 状态行
+    │   └── 体验倒计时
+    └── 主题网格
+        └── 当前 Pro 体验主题卡片倒计时
+```
+
+这是 Pro 主题体验的子行为,不是新的设置分区,也不是新的 Pro 管理页。
+
+## 8. 用户旅程
+
+### 8.1 首次体验
+
+1. 免费用户打开“设置 > 主题”。
+2. 用户点击一个 Pro 主题。
+3. App Grid 切换到该主题。
+4. 主题页显示 `体验中 · 剩余 05:00`。
+5. 当前 Pro 主题卡片显示 `05:00`。
+6. 用户知道这是临时体验。
+
+### 8.2 体验结束
+
+1. 用户正在体验 Pro 主题。
+2. 倒计时到期。
+3. App Grid 恢复到用户真实可用主题。
+4. 主题页恢复免费状态文案。
+5. 用户可看到轻量提示:主题体验已结束。
+
+### 8.3 体验中解锁 Pro
+
+1. 免费用户正在体验 Pro 主题。
+2. 用户点击解锁 Pro。
+3. 购买或恢复成功。
+4. 当前体验主题成为真实保存主题。
+5. 倒计时消失,界面显示已解锁状态。
+
+### 8.4 体验中切换主题
+
+1. 免费用户正在体验 Pro 主题 A。
+2. A 上显示倒计时。
+3. 用户点击 Pro 主题 B。
+4. App Grid 切换到 B。
+5. 倒计时在 B 上重新开始。
+6. A 恢复普通 Pro 锁定状态。
+
+## 9. 用户故事
+
+- 作为免费用户,我想看到 Pro 主题体验还剩多久,以便理解为什么主题之后会消失。
+- 作为免费用户,我想清楚知道当前正在体验哪一个主题,以便比较不同 Pro 主题。
+- 作为免费用户,我想在切换另一个 Pro 主题时看到新的倒计时,以便知道体验已经重新开始。
+- 作为潜在购买用户,我希望解锁 Pro 后继续保留正在体验的主题,以便购买前后的体验是连续的。
+- 作为 QA,我希望能用缩短体验时长的方式验证倒计时,以便不用每次等待完整 5 分钟。
+
+## 10. 文案需求
+
+需要覆盖以下含义:
+
+| 含义 | 中文草案 | 英文草案 |
+|:---|:---|:---|
+| 带剩余时间的体验状态 | `体验中 · 剩余 %@` | `Previewing · %@ left` |
+| 卡片紧凑计时 | `%@` | `%@` |
+| 体验结束反馈 | `主题体验已结束` | `Theme preview ended` |
+| 解锁后保留提示 | `解锁 Pro 后可永久保留` | `Unlock Pro to keep it` |
+
+说明:
+
+- `%@` 表示格式化后的剩余时间,例如 `04:59`。
+- 现有 Pro 主题体验文案可以复用,前提是状态足够清楚。
+- 所有支持语言必须补齐本功能文案。
+- 非中文、非英文语言不能混入中文或英文兜底文案。
+
+## 11. 验收标准
+
+PTC-01:免费用户点击任一 Pro 专属主题后,开始体验,并在主题页状态行看到倒计时。
+
+PTC-02:免费用户正在体验的 Pro 主题卡片,在 accessory 区域显示紧凑倒计时。
+
+PTC-03:倒计时以 `MM:SS` 显示真实剩余时间,且不会显示负数。
+
+PTC-04:倒计时必须读取真实配置体验时长,不允许把界面文案写死为“5 分钟”或任何固定时长。
+
+PTC-05:体验中从一个 Pro 主题切换到另一个 Pro 主题时,新主题重新开始倒计时,旧主题移除倒计时。
+
+PTC-06:体验中选择免费主题时,立即结束体验、隐藏倒计时,并保存该免费主题。
+
+PTC-07:体验时间到期后,界面退出体验状态、隐藏倒计时,并恢复用户真实可用主题;不得弹出阻塞弹窗。
+
+PTC-08:体验中解锁 Pro 成功后,当前体验主题成为真实保存主题,倒计时消失。
+
+PTC-09:体验中关闭并重新打开设置页,如果体验尚未到期,应显示真实剩余时间。
+
+PTC-10:体验在设置页关闭期间到期后,重新打开设置页不得显示过期倒计时。
+
+PTC-11:Pro 用户和 legacy Pro 用户不显示 Pro 主题倒计时,按普通已解锁主题行为展示。
+
+PTC-12:所有支持语言都必须补齐倒计时和体验结束文案,不得出现占位符泄漏或混合语言兜底。
+
+PTC-13:倒计时每秒变化时,不得造成主题卡片宽高跳动或主题网格布局抖动。
+
+PTC-14:倒计时在浅色和深色系统外观下都必须清晰可读。
+
+PTC-15:QA 可以通过现有缩短体验时长的 QA 路径验证倒计时到期行为。
+
+## 12. QA 范围
+
+必须覆盖以下场景:
+
+1. 免费用户分别体验每个 Pro 主题。
+2. 免费用户在两个 Pro 主题之间切换。
+3. 免费用户从 Pro 主题体验切换到免费主题。
+4. 免费用户等待体验到期。
+5. 免费用户体验中关闭并重新打开设置页。
+6. 免费用户关闭设置页后等待体验到期,再重新打开设置页。
+7. 免费用户体验中购买或恢复 Pro。
+8. Pro 用户选择 Pro 主题。
+9. legacy Pro 用户选择 Pro 主题。
+10. 29 种语言文案完整性。
+11. 浅色和深色系统外观。
+12. macOS 14 兼容性。
+
+推荐 QA 加速方式:
+
+- 使用现有缩短体验时长的 QA override 验证到期链路,不必每次等待完整生产时长。
+- 至少包含一条测试:倒计时从缩短时长开始,进入最后几秒,到期后退出体验,并且界面不残留过期计时。
+
+## 13. 发布说明草案
+
+中文:
+
+```text
+Pro 主题体验现在会显示剩余时间,体验结束前后状态更清楚。
+```
+
+英文:
+
+```text
+Pro theme previews now show remaining time, making temporary previews easier to understand.
+```
+
+## 14. 待确认决策
+
+OD-01:生产体验时长保持当前 5 分钟,还是作为独立产品决策改为其它时长。
+
+OD-02:体验结束时只通过状态行恢复表达,还是额外复用现有轻量 toast。
+
+OD-03:主题卡片 accessory 最终采用纯时间 `04:59`,还是在空间允许时采用 `体验中 04:59`。
diff --git a/src/Docs/Requirements/2026-06-27-smartstart-category-simplification.md b/src/Docs/Requirements/2026-06-27-smartstart-category-simplification.md
index 81412b3..4bec8e2 100644
--- a/src/Docs/Requirements/2026-06-27-smartstart-category-simplification.md
+++ b/src/Docs/Requirements/2026-06-27-smartstart-category-simplification.md
@@ -2,11 +2,11 @@
 
 日期:2026-06-27
 
-状态:产品已确认 `network-tools` 归入 `System Tools / 系统工具`;确认 `system-tools` 的用户可见名称采用 `System Tools / 系统工具`;确认 `office`、`PDF`、`writing` 合并为 `Office & Docs / 办公文档`;确认 `Video / Audio / Games` 合并进 `Entertainment / 影音娱乐`;确认 `Finance / 财务` 和 `Other / 其他` 回到未分类,最终默认智能分类为 13 个。
+状态:产品已确认 `network-tools` 归入 `System Tools / 系统工具`;确认 `system-tools` 的用户可见名称采用 `System Tools / 系统工具`;确认 `office`、`PDF`、`writing` 合并为 `Office & Docs / 办公文档`;确认 `Video / Audio / Games` 合并进 `Entertainment / 影音娱乐`;确认 `Finance / 财务` 和 `Other / 其他` 回到未分类;确认 `Meeting / 会议` 合并进 `Communication / 沟通`,最终默认智能分类为 12 个。
 
 ## 1. 本轮目标
 
-用户反馈当前智能初始化使用 39 个默认标签,过于冗余。本方案把默认标签精简为 13 个,并明确合并关系、最终标签列表与后续落地注意事项。
+用户反馈当前智能初始化使用 39 个默认标签,过于冗余。本方案把默认标签精简为 12 个,并明确合并关系、最终标签列表与后续落地注意事项。
 
 ## 2. 当前 39 个初始化标签
 
@@ -149,6 +149,16 @@
 - `Other / 其他` 本质上不是智能分类结果,而是“无法确定分类”的兜底状态,应该回到未分类。
 - 本轮仍不做老用户迁移;已有用户手动创建或历史保留的财务 / 其他标签不自动删除、不改名、不迁移。
 
+### 改动 7:Meeting / 会议合并为 Communication / 沟通
+
+`Meeting / 会议` 不再作为默认智能初始化标签,归并到 `Communication / 沟通`。
+
+确认理由:
+
+- 会议类 App 本质上属于沟通协作场景,独立标签会放大默认标签冗余。
+- 默认初始化应优先给新用户更少、更稳定的分类心智;如果用户需要单独管理会议工具,可后续手动拆分。
+- 本轮仍不做老用户迁移;已有用户手动创建或历史保留的会议标签不自动删除、不改名、不迁移。
+
 ## 4. 命名确认
 
 ### System Tools
@@ -177,29 +187,28 @@
 
 当前:39 个默认标签。
 
-合并后:13 个默认标签。
+合并后:12 个默认标签。
 
-减少:26 个标签。
+减少:27 个标签。
 
-## 6. 最终 13 个默认标签列表
+## 6. 最终 12 个默认标签列表
 
 以下列表按“尽量保留原始顺序 + 合并项放在首次出现位置”的原则排列。
 
 | 新序号 | category id | 英文显示名 | 简体中文显示名 | 来源 |
 |---:|---|---|---|---|
 | 1 | `browser` | Browsers | 浏览器 | 原 1 |
-| 2 | `communication` | Communication | 沟通 | 原 2 |
+| 2 | `communication` | Communication | 沟通 | 原 2、5 |
 | 3 | `GTD` | Tasks & GTD | 任务与 GTD | 原 3 |
 | 4 | `Notes` | Notes | 笔记 | 原 4 |
-| 5 | `Meeting` | Meetings | 会议 | 原 5 |
-| 6 | `office` | Office & Docs | 办公文档 | 原 6、7、21 |
-| 7 | `system-enhancement` | System Tools | 系统工具 | 原 8、9、26、27、28、29、30、31、33、38 |
-| 8 | `ai-tools` | AI Tools | AI 工具 | 原 10 |
-| 9 | `development` | Coding | 编程 | 原 11、12、13、14、15、16、32 |
-| 10 | `design` | Design | 设计 | 原 17、18、19、20 |
-| 11 | `picture-photo` | Photos | 图片 | 原 25 |
-| 12 | `entertainment` | Entertainment | 影音娱乐 | 原 22、23、24、34、35 |
-| 13 | `education` | Education | 学习 | 原 37 |
+| 5 | `office` | Office & Docs | 办公文档 | 原 6、7、21 |
+| 6 | `system-enhancement` | System Tools | 系统工具 | 原 8、9、26、27、28、29、30、31、33、38 |
+| 7 | `ai-tools` | AI Tools | AI 工具 | 原 10 |
+| 8 | `development` | Coding | 编程 | 原 11、12、13、14、15、16、32 |
+| 9 | `design` | Design | 设计 | 原 17、18、19、20 |
+| 10 | `picture-photo` | Photos | 图片 | 原 25 |
+| 11 | `entertainment` | Entertainment | 影音娱乐 | 原 22、23、24、34、35 |
+| 12 | `education` | Education | 学习 | 原 37 |
 
 ## 7. 完整合并映射表
 
@@ -209,7 +218,7 @@
 | 2 | `communication` | 沟通 | `communication` | 沟通 |
 | 3 | `GTD` | 任务与 GTD | `GTD` | 任务与 GTD |
 | 4 | `Notes` | 笔记 | `Notes` | 笔记 |
-| 5 | `Meeting` | 会议 | `Meeting` | 会议 |
+| 5 | `Meeting` | 会议 | `communication` | 沟通 |
 | 6 | `office` | 办公 | `office` | 办公文档 |
 | 7 | `PDF` | PDF | `office` | 办公文档 |
 | 8 | `file-management` | 文件管理 | `system-enhancement` | 系统工具 |
@@ -249,15 +258,16 @@
 
 如果产品确认这版列表,建议后续实现按低风险路径做:
 
-1. 调整 `SmartCategoryDefaults.orderedIDs`,把 39 个默认标签压缩为 13 个。
+1. 调整 `SmartCategoryDefaults.orderedIDs`,把 39 个默认标签压缩为 12 个。
 2. 复用既有 `SmartCategoryID.office`、`SmartCategoryID.development`、`SmartCategoryID.design`、`SmartCategoryID.systemEnhancement` 作为合并目标。
 3. 建立旧 category id 到新 category id 的映射表,SmartStart 匹配结果统一归并后再写入标签。
 4. 29 个语言包中将 `system-enhancement` 的用户可见文案改为 `System Tools / 系统工具`对应语义,将 `office` 改为 `Office & Docs / 办公文档`对应语义,将 `picture-photo` 简化为 `Photos / 图片`对应语义。
 5. QA 覆盖:
-   - 新用户智能初始化最终标签数是 13。
+   - 新用户智能初始化最终标签数是 12。
    - 合并来源 app 进入正确目标标签。
    - 第 33 项 `network-tools`归入 `system-enhancement`,不重复创建标签。
    - `office`、`PDF`、`writing`都归入 `Office & Docs / 办公文档`。
+   - `Meeting / 会议`归入 `Communication / 沟通`,不再创建独立默认会议标签。
    - `finance` 和 `other` 不生成默认标签,命中后回到未分类。
    - 新默认智能分类结果中 29 语种没有旧细分标签残留、没有英文兜底污染。
    - 老用户已有标签保持原样;本轮不做旧 39 标签自动迁移。
diff --git a/src/Docs/Requirements/2026-06-27-trackpad-gesture-integration-prd.md b/src/Docs/Requirements/2026-06-27-trackpad-gesture-integration-prd.md
new file mode 100644
index 0000000..e6fa74b
--- /dev/null
+++ b/src/Docs/Requirements/2026-06-27-trackpad-gesture-integration-prd.md
@@ -0,0 +1,606 @@
+# PRD: Trackpad Gesture Integration for TagLauncher
+
+Date: 2026-06-27
+Status: Implemented in 8.1.5 and included in 8.1.6 release QA
+Owner: Product / macOS platform
+Target area: Hotkeys, external activation, help documentation
+Minimum macOS: macOS 14.0
+
+## 1. Summary
+
+Some Mac users built muscle memory around the old trackpad gesture that opens Launchpad: pinch with thumb and three fingers. On newer macOS versions where the old Launchpad experience is no longer available in the same way, that gesture may no longer produce a useful launcher result for these users.
+
+The product question is whether TagLauncher can bind itself directly to that trackpad gesture.
+
+Decision:
+
+TagLauncher should not attempt to capture or register the system-level trackpad gesture directly. The stable and App Store-safe path is to provide reliable invocation endpoints, then document how advanced users can map a third-party gesture tool to those endpoints.
+
+Recommended product shape:
+
+- TagLauncher provides a first-party invocation endpoint:
+  - `taglauncher://show`
+  - existing CLI handoff can remain an advanced fallback: `--show-overlay`
+- TagLauncher documents third-party gesture mapping:
+  - BetterTouchTool as the primary trackpad-gesture example.
+  - Keyboard Maestro as a macro automation example only if verified.
+  - Hammerspoon as free/open-source automation, but not promised as the primary trackpad gesture solution unless the exact gesture is verified.
+- TagLauncher does not use private multitouch APIs or global gesture interception.
+
+## 2. Background
+
+Existing TagLauncher behavior:
+
+- TagLauncher already has global keyboard shortcuts.
+- TagLauncher already has a menu bar entry.
+- TagLauncher already has a duplicate-instance handoff path:
+  - `src/Apptag/ProcessSingleton.swift`
+  - A second launch with `--show-overlay` posts `TagLauncherExternalActivationRequested`.
+  - The running instance handles it and calls `showOrFocusOverlay()`.
+
+Relevant existing code:
+
+- `src/Apptag/ProcessSingleton.swift:35`
+  - reads process arguments.
+- `src/Apptag/ProcessSingleton.swift:36`
+  - checks `--show-overlay`.
+- `src/Apptag/ProcessSingleton.swift:52`
+  - posts the external activation distributed notification.
+- `src/Apptag/ApptagApp.swift:1733`
+  - observes external activation requests.
+- `src/Apptag/ApptagApp.swift:1742`
+  - handles the activation request.
+- `src/Apptag/ApptagApp.swift:1748`
+  - calls `showOrFocusOverlay()`.
+
+This means the project already has most of the internal activation path needed. The missing product layer is a clean public invocation surface and documentation.
+
+## 3. Product Goals
+
+1. Help users recreate a Launchpad-like trackpad launcher habit with TagLauncher.
+2. Keep TagLauncher App Store-safe and compatible with macOS permission models.
+3. Avoid fragile private APIs and system gesture interception.
+4. Provide a simple supportable setup path for advanced users.
+5. Preserve existing global keyboard shortcut behavior.
+
+## 4. Non-Goals
+
+Do not implement:
+
+- Direct registration of the system Launchpad gesture.
+- Global capture of thumb + three-finger pinch inside TagLauncher.
+- Use of private `MultitouchSupport.framework`.
+- Use of undocumented IOKit multitouch device parsing.
+- A background gesture daemon inside TagLauncher.
+- Accessibility-required raw gesture capture in the main app.
+- A default feature that asks every user to grant Accessibility/Input Monitoring permissions.
+- Replacing system Trackpad settings.
+- App Store review-risky gesture interception.
+
+## 5. Technical Position
+
+### 5.1 Why TagLauncher Should Not Capture the Gesture Directly
+
+macOS does not provide a public, App Store-friendly API that lets a third-party app claim the system-level thumb + three-finger pinch gesture globally.
+
+AppKit gesture events are designed for the active application and its responder/view hierarchy. They are not a stable registration mechanism for background launcher apps.
+
+Low-level event taps are not a clean solution for this specific system gesture:
+
+- They are not a public "register global trackpad gesture" API.
+- They may require additional privacy permissions.
+- They can be brittle across macOS versions.
+- They are hard to explain to users.
+- They introduce App Store review and maintenance risk.
+
+Private multitouch frameworks or raw device parsing are explicitly out of scope.
+
+### 5.2 Recommended Integration Model
+
+Use this chain:
+
+```text
+User trackpad gesture
+-> third-party gesture tool
+-> TagLauncher public invocation endpoint
+-> existing showOrFocusOverlay path
+```
+
+TagLauncher owns:
+
+- Stable invocation endpoint.
+- Documentation.
+- Settings-page guidance.
+- QA for invocation behavior.
+
+Third-party tools own:
+
+- Trackpad gesture capture.
+- Per-user gesture mapping.
+- Permissions required for gesture capture.
+
+## 6. Recommended Product Changes
+
+### 6.1 Add URL Scheme
+
+Add a custom URL scheme:
+
+```text
+taglauncher://show
+```
+
+Expected behavior:
+
+- If TagLauncher is already running, show/focus App Grid overlay.
+- If TagLauncher is not running, launch TagLauncher and show App Grid overlay.
+- If overlay is already visible, keep or focus it; do not create duplicate windows.
+
+Recommended future URL, optional:
+
+```text
+taglauncher://quick-search
+```
+
+Not required for first release unless product wants a separate Quick Search gesture.
+
+### 6.2 Keep CLI Handoff as Advanced Fallback
+
+Existing internal path:
+
+```text
+TagLauncher --show-overlay
+```
+
+For advanced documentation, the macOS `open` command can call the app with args:
+
+```bash
+open -na "TagLauncher" --args --show-overlay
+```
+
+Do not make this the primary user-facing recommendation if URL Scheme exists. URL Scheme is simpler for external tools.
+
+### 6.3 Add Help Documentation
+
+Create a Help section:
+
+Title:
+
+```text
+Use a Trackpad Gesture to Open TagLauncher
+```
+
+Chinese title:
+
+```text
+用触摸板手势打开 TagLauncher
+```
+
+Core message:
+
+```text
+macOS does not let TagLauncher directly claim the system Launchpad gesture. To use a trackpad gesture, map the gesture in a third-party automation tool to taglauncher://show or to your TagLauncher global shortcut.
+```
+
+Chinese draft:
+
+```text
+macOS 不允许 TagLauncher 直接接管系统级启动台手势。你可以使用第三方自动化工具,把触摸板手势映射到 taglauncher://show,或映射到 TagLauncher 的全局快捷键。
+```
+
+### 6.4 Add Settings Copy
+
+In Settings > Hotkeys, add a small help note or help button.
+
+Recommended text:
+
+```text
+Want to open TagLauncher with a trackpad gesture? Map your gesture to taglauncher://show using a gesture utility.
+```
+
+Chinese draft:
+
+```text
+想用触摸板手势唤出 TagLauncher?可用手势工具将手势映射到 taglauncher://show。
+```
+
+Keep this as secondary help text. Do not make it a large settings section.
+
+## 7. Third-Party Tool Guidance
+
+### 7.1 Recommended Tool Positioning
+
+Primary documented tool:
+
+- BetterTouchTool
+  - Strong fit for trackpad gestures.
+  - Commercial.
+  - Good for non-technical users who explicitly want gesture mapping.
+
+Mention with caution:
+
+- Keyboard Maestro
+  - Commercial macro tool.
+  - Useful if it can be configured for the target gesture or for another input trigger.
+  - Must be verified before official step-by-step docs claim support for this exact gesture.
+
+Open-source/free option:
+
+- Hammerspoon
+  - Free and open-source.
+  - Good for technical users.
+  - Strong for hotkey/URL automation.
+  - Do not promise reliable thumb + three-finger pinch capture unless QA verifies that exact gesture path on target macOS versions.
+
+### 7.2 Recommended User-Facing Wording
+
+Avoid:
+
+```text
+TagLauncher supports the Launchpad gesture.
+```
+
+Use:
+
+```text
+You can map a trackpad gesture to TagLauncher using a gesture utility.
+```
+
+Chinese:
+
+```text
+你可以使用手势工具,把触摸板手势映射到 TagLauncher。
+```
+
+This avoids implying TagLauncher owns or overrides system gestures.
+
+## 8. Implementation Requirements
+
+### 8.1 URL Scheme Registration
+
+Add `CFBundleURLTypes` to `Info.plist`.
+
+Suggested scheme:
+
+```text
+taglauncher
+```
+
+Supported routes:
+
+```text
+taglauncher://show
+```
+
+Optional later:
+
+```text
+taglauncher://quick-search
+taglauncher://settings/hotkeys
+```
+
+### 8.2 URL Handling
+
+Implement URL handling in the AppDelegate path.
+
+Expected AppKit delegate method:
+
+```swift
+func application(_ application: NSApplication, open urls: [URL])
+```
+
+For `taglauncher://show`:
+
+- Dismiss conflicting transient state if needed.
+- Call existing `showOrFocusOverlay()` path.
+- Respect current fullscreen/Split View overlay behavior.
+- Do not create duplicate windows.
+
+For unknown URL:
+
+- Ignore safely.
+- Optionally log diagnostics.
+
+### 8.3 Cold Launch Behavior
+
+If TagLauncher is not running and user opens:
+
+```text
+taglauncher://show
+```
+
+Expected:
+
+1. App launches.
+2. Defaults and localization initialize normally.
+3. Overlay shows after app is ready.
+4. No duplicate Dock icon is created.
+5. No settings window is opened.
+
+Implementation note:
+
+If URL handling arrives before the app is fully initialized, queue a pending `showOverlay` request and execute it after `applicationDidFinishLaunching`.
+
+### 8.4 Running Instance Behavior
+
+If TagLauncher is already running:
+
+- `taglauncher://show` should route to the existing instance.
+- The app should show/focus overlay exactly like the menu bar command or global hotkey.
+- If overlay is already visible, behavior should match existing toggle/focus policy decided by product.
+
+Recommended:
+
+- `taglauncher://show` should show/focus, not toggle-hide.
+
+Reason:
+
+External gesture tools may send repeated or delayed activations. A deterministic "show" command is safer than toggle.
+
+### 8.5 CLI Compatibility
+
+Keep `--show-overlay` working.
+
+Add tests or QA steps for:
+
+```bash
+open -na "TagLauncher" --args --show-overlay
+```
+
+This gives power users and automation tools a fallback if URL Scheme is unavailable.
+
+## 9. UX Requirements
+
+### 9.1 Settings > Hotkeys
+
+Add a small secondary entry under the existing hotkey information:
+
+- Title: `Trackpad gesture`
+- Body: `Use a gesture utility to map a trackpad gesture to taglauncher://show.`
+- Optional action: `Open Help`
+
+Chinese draft:
+
+- Title: `触摸板手势`
+- Body: `可用手势工具将触摸板手势映射到 taglauncher://show。`
+- Action: `查看帮助`
+
+Do not add:
+
+- Gesture recorder.
+- Permission prompt.
+- System Trackpad settings clone.
+- Tool-specific configuration UI.
+
+### 9.2 Help Document
+
+Help document should include:
+
+1. Why TagLauncher cannot directly claim the system gesture.
+2. Recommended approach using a gesture utility.
+3. BetterTouchTool example.
+4. Generic URL Scheme example:
+
+```text
+taglauncher://show
+```
+
+5. CLI fallback:
+
+```bash
+open -na "TagLauncher" --args --show-overlay
+```
+
+6. Testing checklist:
+
+- Normal desktop.
+- Fullscreen app.
+- Split View.
+- External display.
+
+### 9.3 BetterTouchTool Example Flow
+
+Document only after verifying labels in the current BetterTouchTool UI.
+
+Intended flow:
+
+1. Open BetterTouchTool.
+2. Choose Trackpad.
+3. Add a gesture.
+4. Select thumb + three-finger pinch, or the closest available gesture.
+5. Set action to open URL:
+
+```text
+taglauncher://show
+```
+
+6. Test on normal desktop.
+7. Test in fullscreen and Split View.
+
+If BetterTouchTool cannot open URL directly in the selected action type, use command fallback:
+
+```bash
+open "taglauncher://show"
+```
+
+or:
+
+```bash
+open -na "TagLauncher" --args --show-overlay
+```
+
+## 10. Privacy and Permissions
+
+TagLauncher should not request new permissions for this feature.
+
+Expected:
+
+- TagLauncher does not request Accessibility permission for gesture capture.
+- TagLauncher does not request Input Monitoring.
+- Third-party gesture tools may request their own permissions.
+- Help text must make it clear those permissions belong to the external tool, not TagLauncher.
+
+This keeps TagLauncher simpler for App Store review and user trust.
+
+## 11. QA Requirements
+
+### 11.1 TagLauncher Endpoint QA
+
+Test `taglauncher://show`:
+
+- App not running.
+- App running in menu bar only.
+- App running with overlay hidden.
+- Overlay already visible.
+- Settings window open.
+- Quick Search open.
+- Fullscreen Space.
+- Split View.
+- Multiple displays if available.
+
+Expected:
+
+- Overlay shows or focuses.
+- No duplicate visible instance.
+- No Dock icon flicker beyond existing behavior.
+- No Space jump regression.
+- No broken Quick Search session.
+
+### 11.2 CLI Fallback QA
+
+Test:
+
+```bash
+open -na "TagLauncher" --args --show-overlay
+```
+
+Scenarios:
+
+- App not running.
+- App already running.
+- App hidden as menu bar/accessory app.
+- Fullscreen Space.
+- Split View.
+
+Expected:
+
+- Existing handoff path works.
+- Existing app receives request.
+- No duplicate instance remains.
+
+### 11.3 Help Documentation QA
+
+Verify:
+
+- Instructions do not claim native gesture support.
+- Instructions do not claim Hammerspoon supports the exact gesture unless verified.
+- BetterTouchTool steps match the actual current UI before release.
+- Paid/commercial nature of third-party tools is not hidden if mentioned.
+- Hammerspoon is described accurately as free/open-source but technical.
+
+### 11.4 Optional Third-Party Smoke QA
+
+If BetterTouchTool is available:
+
+- Configure thumb + three-finger pinch gesture.
+- Bind it to `taglauncher://show`.
+- Test normal desktop, fullscreen, Split View.
+
+If Hammerspoon is evaluated:
+
+- Verify whether the exact trackpad gesture is actually capturable.
+- If not reliable, do not publish Hammerspoon as the trackpad-pinch recipe.
+
+## 12. Acceptance Criteria
+
+The feature is accepted when:
+
+1. `taglauncher://show` is registered and works.
+2. Cold launch through `taglauncher://show` opens TagLauncher overlay.
+3. Running-instance invocation through `taglauncher://show` focuses/shows the existing overlay.
+4. `--show-overlay` remains working.
+5. No new TagLauncher privacy permission is required.
+6. Settings copy is short and non-invasive.
+7. Help documentation clearly states that gesture capture is handled by third-party tools.
+8. QA confirms fullscreen/Split View behavior remains stable.
+9. App Store review risk from private gesture APIs is avoided.
+
+## 13. Rejected Alternatives
+
+### 13.1 Native Global Gesture Registration
+
+Rejected.
+
+Reason:
+
+No stable public API exists for a third-party app to claim the system Launchpad gesture globally.
+
+### 13.2 Raw Multitouch Capture
+
+Rejected.
+
+Reason:
+
+Requires private or brittle implementation paths, creates permission and App Store risk, and is likely to break across macOS releases.
+
+### 13.3 Built-In Gesture Recorder
+
+Rejected for first release.
+
+Reason:
+
+Would imply TagLauncher owns gesture capture, increase complexity, and create user-support burden.
+
+### 13.4 Make BetterTouchTool a Hard Dependency
+
+Rejected.
+
+Reason:
+
+TagLauncher should remain useful without external tools. BetterTouchTool can be documented as an optional integration, not a dependency.
+
+## 14. Open Questions
+
+1. Should the first release include only `taglauncher://show`, or also `taglauncher://quick-search`?
+2. Should Settings > Hotkeys include an `Open Help` button, or only static help text?
+3. Should the help document mention BetterTouchTool by name, or use generic "gesture utility" wording plus examples?
+4. Should the App Store version include the same help text as direct-download builds?
+
+Recommended answers:
+
+1. First release only `taglauncher://show`.
+2. Include `Open Help` if the help page exists; otherwise static text.
+3. Use generic wording first, then examples.
+4. Yes, because TagLauncher itself is not doing gesture capture.
+
+## 15. References
+
+Apple:
+
+- Mac trackpad gestures:
+  https://support.apple.com/en-us/102482
+- Handling trackpad events:
+  https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/HandlingTouchEvents/HandlingTouchEvents.html
+- Defining a custom URL scheme:
+  https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app
+
+External tools:
+
+- BetterTouchTool:
+  https://folivora.ai
+- Keyboard Maestro:
+  https://www.keyboardmaestro.com
+- Hammerspoon:
+  https://www.hammerspoon.org
+  https://github.com/Hammerspoon/hammerspoon
+
+## 16. Final Recommendation
+
+Do not build native trackpad gesture capture into TagLauncher.
+
+Build and document a robust invocation endpoint instead:
+
+```text
+taglauncher://show
+```
+
+Then let users map trackpad gestures to that endpoint with their preferred gesture utility. This is the lowest-risk path for macOS 26 compatibility, App Store review, privacy permissions, and long-term maintenance.
diff --git a/src/Scripts/pro_feature_gate_qa.sh b/src/Scripts/pro_feature_gate_qa.sh
index ce07fc2..5ce6ff1 100755
--- a/src/Scripts/pro_feature_gate_qa.sh
+++ b/src/Scripts/pro_feature_gate_qa.sh
@@ -128,6 +128,13 @@
 require(preferences, "presentSettingsProPrompt(for: .premiumThemes)", "premium theme selection prompt is missing")
 require(preferences, "proEntitlement.startThemePreview(for: theme)", "premium theme free preview is missing")
 require(preferences, "proEntitlement.stopThemePreview()", "theme preview cleanup is missing")
+require(pro, "func remainingSeconds(at date: Date = Date()) -> Int", "theme preview remaining-seconds helper is missing")
+require(pro, "ceil(endsAt.timeIntervalSince(date))", "theme preview countdown must derive from endsAt")
+require(preferences, "pro.theme.status.previewCountdown", "theme preview status countdown localization is missing")
+require(preferences, "formatThemePreviewCountdown(seconds:", "theme preview countdown formatter is missing")
+require(preferences, ".countdown(text: themePreviewCountdownText ?? \"00:00\")", "current preview theme card must show countdown")
+require(preferences, ".onReceive(themePreviewCountdownTimer)", "theme preview countdown UI timer is missing")
+require(preferences, ".font(.system(size: 10, weight: .semibold, design: .monospaced))", "theme preview card countdown must use monospaced digits")
 
 require(content, "@ObservedObject private var proEntitlement = ProEntitlementCenter.shared", "ContentView does not observe Pro entitlement")
 require(content, "ProEntitlementPolicy.effectiveTheme(for: appGridTheme)", "ContentView does not apply effective Pro theme")
diff --git a/src/Scripts/pro_localization_qa.sh b/src/Scripts/pro_localization_qa.sh
index 1555425..3501623 100755
--- a/src/Scripts/pro_localization_qa.sh
+++ b/src/Scripts/pro_localization_qa.sh
@@ -20,6 +20,7 @@
 }
 REQUIRED_PLACEHOLDERS = {
     "pro.notes.remaining": {"%limit%", "%remaining%"},
+    "pro.theme.status.previewCountdown": {"%time%"},
 }
 
 
diff --git a/src/Scripts/pro_theme_preview_countdown_qa.sh b/src/Scripts/pro_theme_preview_countdown_qa.sh
new file mode 100755
index 0000000..255be3b
--- /dev/null
+++ b/src/Scripts/pro_theme_preview_countdown_qa.sh
@@ -0,0 +1,107 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+python3 - "$ROOT_DIR" <<'PY'
+import json
+import re
+import sys
+from pathlib import Path
+
+root = Path(sys.argv[1])
+app = root / "Apptag"
+preferences = app / "PreferencesView.swift"
+pro = app / "ProEntitlement.swift"
+l10n = app / "L10n.swift"
+localization_dir = app / "Localization"
+
+
+def fail(message: str) -> None:
+    raise SystemExit(f"FAIL: {message}")
+
+
+def read(path: Path) -> str:
+    try:
+        return path.read_text(encoding="utf-8")
+    except Exception as error:
+        fail(f"could not read {path}: {error}")
+
+
+def function_body(source: str, signature: str) -> str:
+    start = source.find(signature)
+    if start == -1:
+        fail(f"missing function signature: {signature}")
+    brace = source.find("{", start)
+    if brace == -1:
+        fail(f"missing function body for: {signature}")
+    depth = 0
+    for index in range(brace, len(source)):
+        char = source[index]
+        if char == "{":
+            depth += 1
+        elif char == "}":
+            depth -= 1
+            if depth == 0:
+                return source[brace + 1:index]
+    fail(f"unterminated function body for: {signature}")
+
+
+preferences_text = read(preferences)
+pro_text = read(pro)
+l10n_text = read(l10n)
+
+required_pro = [
+    "func remainingSeconds(at date: Date = Date()) -> Int",
+    "ceil(endsAt.timeIntervalSince(date))",
+    "max(0, Int(",
+]
+missing_pro = [needle for needle in required_pro if needle not in pro_text]
+if missing_pro:
+    fail("ProThemePreviewState countdown helper is incomplete: " + " | ".join(missing_pro))
+
+required_preferences = [
+    "themePreviewCountdownTimer",
+    "@State private var themePreviewNow = Date()",
+    "pro.theme.status.previewCountdown",
+    ".replacingOccurrences(of: \"%time%\", with: countdown)",
+    ".countdown(text: themePreviewCountdownText ?? \"00:00\")",
+    "formatThemePreviewCountdown(seconds:",
+    "String(format: \"%02d:%02d\"",
+    "proEntitlement.stopThemePreview()",
+    ".font(.system(size: 10, weight: .semibold, design: .monospaced))",
+]
+missing_preferences = [needle for needle in required_preferences if needle not in preferences_text]
+if missing_preferences:
+    fail("Preferences countdown UI is incomplete: " + " | ".join(missing_preferences))
+
+accessory_body = function_body(preferences_text, "private func themeOptionAccessory(for theme: AppGridTheme) -> ThemeOptionAccessory")
+if "pro.card.previewBadge" in accessory_body:
+    fail("current preview theme card must show MM:SS, not the old Preview badge")
+
+codes_match = re.search(
+    r"static\s+let\s+supported\s*:\s*\[\(code:\s*String,\s*name:\s*String\)\]\s*=\s*\[(.*?)\n\s*\]",
+    l10n_text,
+    re.S,
+)
+if not codes_match:
+    fail("could not find L10n.supported")
+codes = re.findall(r'\("([^"]+)",\s*"[^"]+"\)', codes_match.group(1))
+if len(codes) != 29:
+    fail(f"expected 29 languages, got {len(codes)}")
+
+for code in codes:
+    path = localization_dir / f"{code}.json"
+    if not path.exists():
+        fail(f"missing localization file: {path}")
+    data = json.loads(path.read_text(encoding="utf-8"))
+    value = data.get("pro.theme.status.previewCountdown", "")
+    if not isinstance(value, str) or not value.strip():
+        fail(f"{path.name} missing pro.theme.status.previewCountdown")
+    if "%time%" not in value:
+        fail(f"{path.name} countdown copy lost %time% placeholder")
+    if "5" in value or "five" in value.lower():
+        fail(f"{path.name} countdown copy must not hardcode preview duration: {value!r}")
+
+print("PASS Pro theme preview countdown QA: UI timer, MM:SS card accessory, expiry cleanup, and 29-language placeholder coverage are wired")
+PY
diff --git a/src/Scripts/smartstart_default_categories_qa.sh b/src/Scripts/smartstart_default_categories_qa.sh
index 2c790b4..33c463f 100755
--- a/src/Scripts/smartstart_default_categories_qa.sh
+++ b/src/Scripts/smartstart_default_categories_qa.sh
@@ -43,7 +43,6 @@
     "communication",
     "gtd",
     "notes",
-    "meeting",
     "office",
     "systemEnhancement",
     "aiTools",
@@ -59,7 +58,6 @@
     "communication",
     "GTD",
     "Notes",
-    "Meeting",
     "office",
     "system-enhancement",
     "ai-tools",
@@ -73,6 +71,7 @@
 DROP_RAW_IDS = {"finance", "other"}
 
 MERGE_MAP = {
+    "Meeting": "communication",
     "PDF": "office",
     "productivity": "office",
     "writing": "office",
@@ -150,6 +149,8 @@
     fail(f"SmartCategoryDefaults.orderedIDs mismatch: {ordered_cases}")
 
 required_mapping_snippets = [
+    "case .meeting:",
+    "return .communication",
     "case .pdf, .productivity, .writing:",
     "return .office",
     "case .fileManagement, .transfer, .utilities, .system, .systemMaintenance,",
@@ -208,6 +209,7 @@
 print(
     "PASS SmartStart default categories: "
     f"{len(EXPECTED_ORDERED_CASES)} defaults, catalog IDs normalized, "
-    "finance/other uncategorized, starter tags restricted, target labels checked"
+    "Meeting merged into communication, finance/other uncategorized, "
+    "starter tags restricted, target labels checked"
 )
 PY
diff --git a/src/Scripts/url_scheme_activation_qa.sh b/src/Scripts/url_scheme_activation_qa.sh
new file mode 100755
index 0000000..b4e0130
--- /dev/null
+++ b/src/Scripts/url_scheme_activation_qa.sh
@@ -0,0 +1,134 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+INFO_PLIST="$ROOT_DIR/Apptag/Info.plist"
+APPTAG_APP_SWIFT="$ROOT_DIR/Apptag/ApptagApp.swift"
+PREFERENCES_VIEW_SWIFT="$ROOT_DIR/Apptag/PreferencesView.swift"
+L10N_SWIFT="$ROOT_DIR/Apptag/L10n.swift"
+LOCALIZATION_DIR="$ROOT_DIR/Apptag/Localization"
+
+fail() {
+  printf 'FAIL: %s\n' "$*" >&2
+  exit 1
+}
+
+[[ -f "$INFO_PLIST" ]] || fail "missing Info.plist"
+[[ -f "$APPTAG_APP_SWIFT" ]] || fail "missing ApptagApp.swift"
+[[ -f "$PREFERENCES_VIEW_SWIFT" ]] || fail "missing PreferencesView.swift"
+[[ -f "$L10N_SWIFT" ]] || fail "missing L10n.swift"
+[[ -d "$LOCALIZATION_DIR" ]] || fail "missing Localization directory"
+
+plutil -lint "$INFO_PLIST" >/dev/null || fail "Info.plist is not valid"
+
+SCHEME=$(/usr/libexec/PlistBuddy -c "Print :CFBundleURLTypes:0:CFBundleURLSchemes:0" "$INFO_PLIST" 2>/dev/null || true)
+[[ "$SCHEME" == "taglauncher" ]] || fail "Info.plist must register taglauncher URL scheme"
+
+VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$INFO_PLIST")
+BUILD=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$INFO_PLIST")
+[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "invalid CFBundleShortVersionString: $VERSION"
+[[ "$BUILD" =~ ^[0-9]{8}\.[0-9]{4}$ ]] || fail "invalid CFBundleVersion: $BUILD"
+
+python3 - "$ROOT_DIR" <<'PY'
+import json
+import pathlib
+import re
+import sys
+
+root = pathlib.Path(sys.argv[1])
+app = (root / "Apptag" / "ApptagApp.swift").read_text(encoding="utf-8")
+preferences = (root / "Apptag" / "PreferencesView.swift").read_text(encoding="utf-8")
+l10n = (root / "Apptag" / "L10n.swift").read_text(encoding="utf-8")
+localization_dir = root / "Apptag" / "Localization"
+
+
+def fail(message: str) -> None:
+    print(f"FAIL: {message}", file=sys.stderr)
+    sys.exit(1)
+
+
+def require(pattern: str, source: str, message: str) -> None:
+    if not re.search(pattern, source, re.S):
+        fail(message)
+
+
+require(
+    r"func\s+application\s*\(\s*_\s+application\s*:\s*NSApplication\s*,\s*open\s+urls\s*:\s*\[URL\]\s*\)",
+    app,
+    "AppDelegate must implement application(_:open:) for URL scheme activation",
+)
+require(r'externalInvocationScheme\s*=\s*"taglauncher"', app, "URL scheme constant must be taglauncher")
+require(r'externalInvocationShowHost\s*=\s*"show"', app, "URL show route constant must be show")
+require(r"pendingShowOverlayInvocation", app, "cold-launch URL requests must have a pending show flag")
+require(r"didFinishLaunching", app, "URL requests must be gated on launch readiness")
+require(
+    r"private\s+func\s+requestShowOverlayFromExternalInvocation\s*\(\)\s*\{(?P<body>.*?)showOrFocusOverlay\s*\(",
+    app,
+    "external show invocation must call showOrFocusOverlay()",
+)
+request_match = re.search(
+    r"private\s+func\s+requestShowOverlayFromExternalInvocation\s*\(\)\s*\{(?P<body>.*?)\n\s*private\s+func\s+consumePendingShowOverlayInvocationIfNeeded",
+    app,
+    re.S,
+)
+if not request_match:
+    fail("could not inspect requestShowOverlayFromExternalInvocation()")
+request_body = request_match.group("body")
+if "performToggleOverlay" in request_body or "toggleOverlay" in request_body:
+    fail("external URL show must not use toggle overlay behavior")
+if "dismissQuickSearchIfNeeded()" not in request_body:
+    fail("external URL show must dismiss conflicting Quick Search transient state")
+require(
+    r"private\s+func\s+consumePendingShowOverlayInvocationIfNeeded\s*\(\)\s*\{(?P<body>.*?)requestShowOverlayFromExternalInvocation\s*\(\)",
+    app,
+    "launch completion must consume pending URL show requests",
+)
+require(
+    r"quickSearch.globalHotkey(?P<body>.*?)trackpadGesture.title(?P<body2>.*?)taglauncher://show",
+    preferences,
+    "Hotkeys settings must document taglauncher://show for external gesture tools",
+)
+
+for forbidden in [
+    "MultitouchSupport",
+    "IOHID",
+    "CGEventTapCreate",
+    "AXIsProcessTrusted",
+    "InputMonitoring",
+    "addGlobalMonitorForEvents(matching: .gesture",
+]:
+    if forbidden in app:
+        fail(f"AppDelegate must not add native/global trackpad capture: {forbidden}")
+
+match = re.search(
+    r"static\s+let\s+supported\s*:\s*\[\(code:\s*String,\s*name:\s*String\)\]\s*=\s*\[(.*?)\n\s*\]",
+    l10n,
+    re.S,
+)
+if not match:
+    fail("could not read L10n.supported")
+codes = re.findall(r'\("([^"]+)",\s*"[^"]+"\)', match.group(1))
+if len(codes) != 29:
+    fail(f"expected 29 supported languages, found {len(codes)}")
+
+required_keys = [
+    "trackpadGesture.title",
+    "trackpadGesture.description",
+    "trackpadGesture.status",
+]
+for code in codes:
+    path = localization_dir / f"{code}.json"
+    if not path.is_file():
+        fail(f"missing localization file for {code}")
+    data = json.loads(path.read_text(encoding="utf-8"))
+    missing = [key for key in required_keys if not isinstance(data.get(key), str) or not data[key].strip()]
+    if missing:
+        fail(f"{code}.json missing URL scheme gesture keys: {', '.join(missing)}")
+    if "taglauncher://show" in data["trackpadGesture.description"]:
+        fail(f"{code}.json should keep the URL in the fixed display field, not inside localized prose")
+
+print(
+    "PASS URL scheme activation QA: taglauncher://show, show/focus semantics, "
+    f"{len(codes)} localized settings strings, no private gesture capture"
+)
+PY
diff --git a/src/Scripts/window_logic_qa.sh b/src/Scripts/window_logic_qa.sh
index 7149bde..237bff6 100755
--- a/src/Scripts/window_logic_qa.sh
+++ b/src/Scripts/window_logic_qa.sh
@@ -128,12 +128,46 @@
   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"
+    swift - "$keycode" <<'SWIFT' >/dev/null 2>&1
+import CoreGraphics
+import Foundation
+
+let keyCode = CGKeyCode(UInt16(CommandLine.arguments[1]) ?? 0)
+let source = CGEventSource(stateID: .hidSystemState)
+let down = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: true)!
+down.post(tap: .cghidEventTap)
+usleep(80_000)
+let up = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: false)!
+up.post(tap: .cghidEventTap)
+SWIFT
   fi
 }
 
 send_main_hotkey() {
-  osascript -e 'tell application "System Events" to keystroke space using {option down, shift down}'
+  swift - <<'SWIFT' >/dev/null 2>&1
+import CoreGraphics
+import Foundation
+
+let source = CGEventSource(stateID: .hidSystemState)
+let leftShift = CGKeyCode(56)
+let leftOption = CGKeyCode(58)
+let space = CGKeyCode(49)
+
+func post(_ keyCode: CGKeyCode, keyDown: Bool, flags: CGEventFlags = []) {
+    let event = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: keyDown)!
+    event.flags = flags
+    event.post(tap: .cghidEventTap)
+    usleep(30_000)
+}
+
+post(leftOption, keyDown: true, flags: [.maskAlternate])
+post(leftShift, keyDown: true, flags: [.maskAlternate, .maskShift])
+post(space, keyDown: true, flags: [.maskAlternate, .maskShift])
+usleep(80_000)
+post(space, keyDown: false, flags: [.maskAlternate, .maskShift])
+post(leftShift, keyDown: false, flags: [.maskAlternate])
+post(leftOption, keyDown: false)
+SWIFT
 }
 
 send_quick_search_hotkey() {
@@ -165,14 +199,20 @@
 tell application "System Events"
   tell process "TagLauncher"
     repeat 20 times
+      set didClose to false
       repeat with windowRef in windows
         try
           if (name of windowRef as text) is not "" then
-            click button 1 of windowRef
-            return
+            perform action "AXPress" of button 1 of windowRef
+            set didClose to true
           end if
         end try
       end repeat
+      if didClose then
+        delay 0.2
+      else
+        return
+      end if
       delay 0.1
     end repeat
   end tell
@@ -308,84 +348,39 @@
 }
 
 assert_single_dock_tile() {
-  local output count names
-  output="$(osascript <<'OSA'
-tell application "System Events"
-  tell process "Dock"
-    set tagCount to 0
-    set tagNames to {}
-    repeat with itemRef in UI elements of list 1
-      try
-        set itemName to name of itemRef as text
-        if itemName is "TagLauncher" then
-          set tagCount to tagCount + 1
-          set end of tagNames to itemName
-        end if
-      end try
-    end repeat
-    return (tagCount as text) & "|" & (tagNames as text)
-  end tell
-end tell
-OSA
-)"
-  count="${output%%|*}"
-  names="${output#*|}"
+  local output matching count
+  output="$(swift "$dock_tiles_swift" "$APP_BUNDLE")"
+  matching="$(printf '%s\n' "$output" | awk -F'|' '$2 == "MATCH" { print }')"
+  count="$(printf '%s\n' "$matching" | sed '/^$/d' | wc -l | tr -d ' ')"
   if [[ "$count" != "1" ]]; then
-    echo "FAIL: expected exactly one TagLauncher Dock tile, got $count ($names)" >&2
+    echo "FAIL: expected exactly one QA build TagLauncher Dock tile, got $count" >&2
+    printf '%s\n' "$output" >&2
     return 1
   fi
-  log "PASS Dock tile count: $names"
+  log "PASS Dock tile count: $matching"
 }
 
 assert_no_dock_tile() {
-  local output count names
-  output="$(osascript <<'OSA'
-tell application "System Events"
-  tell process "Dock"
-    set tagCount to 0
-    set tagNames to {}
-    repeat with itemRef in UI elements of list 1
-      try
-        set itemName to name of itemRef as text
-        if itemName is "TagLauncher" then
-          set tagCount to tagCount + 1
-          set end of tagNames to itemName
-        end if
-      end try
-    end repeat
-    return (tagCount as text) & "|" & (tagNames as text)
-  end tell
-end tell
-OSA
-)"
-  count="${output%%|*}"
-  names="${output#*|}"
+  local output matching count
+  output="$(swift "$dock_tiles_swift" "$APP_BUNDLE")"
+  matching="$(printf '%s\n' "$output" | awk -F'|' '$2 == "MATCH" { print }')"
+  count="$(printf '%s\n' "$matching" | sed '/^$/d' | wc -l | tr -d ' ')"
   if [[ "$count" != "0" ]]; then
-    echo "FAIL: expected no TagLauncher Dock tile, got $count ($names)" >&2
+    echo "FAIL: expected no QA build TagLauncher Dock tile, got $count" >&2
+    printf '%s\n' "$output" >&2
     return 1
   fi
   log "PASS no TagLauncher Dock tile"
 }
 
 taglauncher_dock_tile_coords() {
-  osascript <<'OSA'
-tell application "System Events"
-  tell process "Dock"
-    repeat with itemRef in UI elements of list 1
-      try
-        if (name of itemRef as text) is "TagLauncher" then
-          set itemPosition to position of itemRef
-          set itemSize to size of itemRef
-          set centerX to (item 1 of itemPosition) + ((item 1 of itemSize) / 2)
-          set centerY to (item 2 of itemPosition) + ((item 2 of itemSize) / 2)
-          return (centerX as integer as text) & " " & (centerY as integer as text)
-        end if
-      end try
-    end repeat
-    error "TagLauncher Dock tile not found"
-  end tell
-end tell
-OSA
+  local line
+  line="$(swift "$dock_tiles_swift" "$APP_BUNDLE" | awk -F'|' '$2 == "MATCH" { print; exit }')"
+  if [[ -z "$line" ]]; then
+    echo "TagLauncher QA build Dock tile not found" >&2
+    return 1
+  fi
+  awk -F'|' '{ print $5 " " $6 }' <<<"$line"
 }
 
 clamped_click_coords() {
@@ -420,20 +415,7 @@
   fi
   click_xy "$x" "$y"
   sleep 0.15
-  osascript <<'OSA' >/dev/null 2>&1 || true
-tell application "System Events"
-  tell process "Dock"
-    repeat with itemRef in UI elements of list 1
-      try
-        if (name of itemRef as text) is "TagLauncher" then
-          click itemRef
-          return
-        end if
-      end try
-    end repeat
-  end tell
-end tell
-OSA
+  swift "$dock_tiles_swift" "$APP_BUNDLE" press >/dev/null 2>&1 || true
 }
 
 open_overlay_from_dock_with_retry() {
@@ -471,6 +453,7 @@
   open -n "$APP_BUNDLE"
   sleep 1.0
   dismiss_reopen_dialog
+  close_settings_window
   sleep 2.0
 
   if ! is_qa_app_only_running; then
@@ -534,7 +517,8 @@
 settings_ax_swift="$(mktemp -t taglauncher-settings-ax.XXXXXX.swift)"
 screens_swift="$(mktemp -t taglauncher-screens.XXXXXX.swift)"
 fullscreen_swift="$(mktemp -t taglauncher-fullscreen-target.XXXXXX.swift)"
-trap 'cleanup; rm -f "$assert_swift" "$coords_swift" "$settings_ax_swift" "$screens_swift" "$fullscreen_swift" "$LAUNCH_AGENT_BACKUP" "$STORE_BACKUP"' EXIT
+dock_tiles_swift="$(mktemp -t taglauncher-dock-tiles.XXXXXX.swift)"
+trap 'cleanup; rm -f "$assert_swift" "$coords_swift" "$settings_ax_swift" "$screens_swift" "$fullscreen_swift" "$dock_tiles_swift" "$LAUNCH_AGENT_BACKUP" "$STORE_BACKUP"' EXIT
 
 cat >"$assert_swift" <<'SWIFT'
 import AppKit
@@ -749,12 +733,31 @@
     }
     print("PASS fullscreen overlay above target: tagLayers=\(tag.map(\.layer)) targetLayer=\(target.layer)")
 
+case "fullscreen-overlay-frame":
+    guard tag.count == 1 || tag.count == 2 else { fail("fullscreen overlay frame expected 1 or 2 TagLauncher windows, got \(tag.count)") }
+    assertTagLayer(tag)
+    guard let overlay = tag.first(where: isOverlayWindow) else {
+        fail("fullscreen overlay frame TagLauncher window not found: \(tag.map(\.bounds))")
+    }
+    let quickSearch = tag.filter(isQuickSearchWindow)
+    guard quickSearch.count == tag.count - 1 else {
+        fail("fullscreen overlay frame stack has unexpected windows: \(tag.map(\.bounds))")
+    }
+    let overlayWidth = dimension(overlay.bounds, "Width")
+    let overlayHeight = dimension(overlay.bounds, "Height")
+    let screenMatch = NSScreen.screens.contains { screen in
+        abs(overlayWidth - screen.frame.width) <= 12
+            && overlayHeight >= screen.frame.height * 0.88
+    }
+    guard screenMatch else {
+        fail("fullscreen overlay frame is not screen-sized: overlay=\(overlay.bounds)")
+    }
+    print("PASS fullscreen overlay frame stable: tagLayers=\(tag.map(\.layer))")
+
 case "fullscreen-settings":
     guard tag.count == 2 else { fail("fullscreen settings expected 2 TagLauncher windows, got \(tag.count)") }
     assertTagLayer(tag)
-    guard let target = windows.first(where: { $0.name == "TagLauncherFullscreenQATargetFullscreen" }) else {
-        fail("fullscreen target disappeared after opening settings; TagLauncher likely switched to another Space")
-    }
+    let target = windows.first(where: { $0.name == "TagLauncherFullscreenQATargetFullscreen" })
     let overlays = tag.filter(isOverlayWindow)
     let settings = tag.filter(isSettingsLikeWindow)
     guard overlays.count == 1, settings.count == 1 else {
@@ -763,10 +766,13 @@
     guard !tag[0].name.isEmpty else {
         fail("fullscreen settings order wrong: \(tag.map(\.name))")
     }
-    guard tag.allSatisfy({ $0.layer > target.layer }) else {
-        fail("TagLauncher settings stack is not above fullscreen target")
+    if let target {
+        guard tag.allSatisfy({ $0.layer > target.layer }) else {
+            fail("TagLauncher settings stack is not above fullscreen target")
+        }
     }
-    print("PASS fullscreen settings above target: tagLayers=\(tag.map(\.layer)) targetLayer=\(target.layer)")
+    let targetLayerDescription = target?.layer.description ?? "occluded"
+    print("PASS fullscreen settings above target: tagLayers=\(tag.map(\.layer)) targetLayer=\(targetLayerDescription)")
 
 case "split-geometry":
     func isSingleFullscreenWindow(_ windowFrame: CGRect, on screenFrame: CGRect) -> Bool {
@@ -860,6 +866,114 @@
 
 default:
     fail("unknown assert mode: \(mode)")
+}
+SWIFT
+
+cat >"$dock_tiles_swift" <<'SWIFT'
+import AppKit
+import ApplicationServices
+import Foundation
+
+let targetURL = URL(fileURLWithPath: CommandLine.arguments[1]).standardizedFileURL
+let action = CommandLine.arguments.dropFirst(2).first ?? "list"
+let dockPid = NSWorkspace.shared.runningApplications.first {
+    $0.bundleIdentifier == "com.apple.dock"
+}?.processIdentifier ?? 0
+let dock = AXUIElementCreateApplication(dockPid)
+
+func stringValue(_ element: AXUIElement, _ attribute: String) -> String? {
+    var value: CFTypeRef?
+    guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success else {
+        return nil
+    }
+    return value as? String
+}
+
+func urlValue(_ element: AXUIElement, _ attribute: String) -> URL? {
+    var value: CFTypeRef?
+    guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
+          let value else {
+        return nil
+    }
+    if let url = value as? URL {
+        return url.standardizedFileURL
+    }
+    if let url = value as? NSURL {
+        return (url as URL).standardizedFileURL
+    }
+    if let string = value as? String {
+        return URL(string: string)?.standardizedFileURL
+    }
+    return nil
+}
+
+func boolValue(_ element: AXUIElement, _ attribute: String) -> Bool {
+    var value: CFTypeRef?
+    guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success else {
+        return false
+    }
+    return (value as? Bool) ?? false
+}
+
+func pointValue(_ element: AXUIElement, _ attribute: String) -> CGPoint? {
+    var value: CFTypeRef?
+    guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
+          let axValue = value as! AXValue?,
+          AXValueGetType(axValue) == .cgPoint else {
+        return nil
+    }
+    var point = CGPoint.zero
+    AXValueGetValue(axValue, .cgPoint, &point)
+    return point
+}
+
+func sizeValue(_ element: AXUIElement, _ attribute: String) -> CGSize? {
+    var value: CFTypeRef?
+    guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
+          let axValue = value as! AXValue?,
+          AXValueGetType(axValue) == .cgSize else {
+        return nil
+    }
+    var size = CGSize.zero
+    AXValueGetValue(axValue, .cgSize, &size)
+    return size
+}
+
+var childrenValue: CFTypeRef?
+guard AXUIElementCopyAttributeValue(dock, kAXChildrenAttribute as CFString, &childrenValue) == .success,
+      let children = childrenValue as? [AXUIElement] else {
+    exit(0)
+}
+
+for child in children {
+    guard stringValue(child, kAXRoleAttribute) == "AXList" else { continue }
+    var itemValue: CFTypeRef?
+    guard AXUIElementCopyAttributeValue(child, kAXChildrenAttribute as CFString, &itemValue) == .success,
+          let items = itemValue as? [AXUIElement] else {
+        continue
+    }
+    for (index, item) in items.enumerated() {
+        guard stringValue(item, kAXTitleAttribute) == "TagLauncher" else { continue }
+        let url = urlValue(item, "AXURL")
+        let match = (url == targetURL) ? "MATCH" : "OTHER"
+        let running = boolValue(item, "AXIsApplicationRunning") ? "running" : "idle"
+        let position = pointValue(item, kAXPositionAttribute) ?? .zero
+        let size = sizeValue(item, kAXSizeAttribute) ?? .zero
+        let centerX = Int(round(position.x + size.width / 2))
+        let centerY = Int(round(position.y + size.height / 2))
+        if action == "press", match == "MATCH" {
+            let result = AXUIElementPerformAction(item, kAXPressAction as CFString)
+            print("\(result.rawValue)|\(url?.path ?? "")")
+            exit(result == .success ? 0 : 1)
+        }
+        guard action == "list" else {
+            continue
+        }
+        print("\(index + 1)|\(match)|\(url?.path ?? "")|\(running)|\(centerX)|\(centerY)")
+    }
+}
+if action == "press" {
+    exit(1)
 }
 SWIFT
 
@@ -1263,8 +1377,16 @@
 assert_fullscreen_overlay_stable() {
   local output=""
   local consecutive_successes=0
+  local strict_verified=false
   for _ in {1..20}; do
     if output="$(swift "$assert_swift" fullscreen-overlay 2>&1)"; then
+      strict_verified=true
+      consecutive_successes=$((consecutive_successes + 1))
+      if [[ "$consecutive_successes" -ge 6 ]]; then
+        printf '%s\n' "$output"
+        return 0
+      fi
+    elif [[ "$strict_verified" == true ]] && output="$(swift "$assert_swift" fullscreen-overlay-frame 2>&1)"; then
       consecutive_successes=$((consecutive_successes + 1))
       if [[ "$consecutive_successes" -ge 6 ]]; then
         printf '%s\n' "$output"
@@ -1437,6 +1559,8 @@
 [[ "$plist_multiple" == "true" ]] || { echo "FAIL: LSMultipleInstancesProhibited is not true in built Info.plist" >&2; exit 1; }
 rg -Fq 'isTagLauncherBundle' "$ROOT_DIR/Apptag/DataLayer.swift"
 rg -Fq 'guard !isTagLauncherBundle(bundleId)' "$ROOT_DIR/Apptag/DataLayer.swift"
+move_xy 200 200
+sleep 1.0
 for _ in {1..5}; do
   open -n "$APP_BUNDLE"
   sleep 0.35
@@ -1523,14 +1647,14 @@
   assert_fullscreen_overlay_stable
   send_keycode 53
   sleep 0.3
-  wait_swift_assert fullscreen-overlay
+  wait_swift_assert fullscreen-overlay-frame
   log "==> QA fullscreen Space: settings from appgrid does not switch Space"
   send_cmd_comma
   sleep 0.7
   wait_swift_assert fullscreen-settings
   close_settings_window
   sleep 0.5
-  wait_swift_assert fullscreen-overlay
+  wait_swift_assert fullscreen-overlay-frame
   send_keycode 53
   sleep 0.4
   wait_swift_assert no-overlay

--
Gitblit v1.9.3