From 0ae3a9359e4cdf6ae5b669c4affd91662463f370 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 01 Jul 2026 23:17:36 +0800
Subject: [PATCH] Prepare TagLauncher 8.3.1 candidate

---
 src/Apptag/Localization/fr.json                                    |   17 
 src/Apptag/Localization/ru.json                                    |   17 
 src/Docs/Requirements/free-vs-pro-feature-memo.md                  |  162 ++++
 .gitignore                                                         |    5 
 src/Apptag/Localization/ar.json                                    |   17 
 src/Apptag/Localization/ro.json                                    |   17 
 src/Apptag/Localization/en.json                                    |   17 
 src/Apptag/Localization/sv.json                                    |   17 
 src/Scripts/pro_custom_container_quota_qa.sh                       |  143 +++
 src/Apptag/Localization/zh-Hant.json                               |   17 
 src/Apptag/TagEditorView.swift                                     |   29 
 src/Apptag/ApptagApp.swift                                         |    7 
 src/Apptag/Localization/pl.json                                    |   17 
 src/Apptag/Localization/es.json                                    |   17 
 src/Apptag/Localization/th.json                                    |   17 
 src/Apptag/Localization/nn.json                                    |   17 
 src/Apptag/ProEntitlement.swift                                    |   54 +
 src/CHANGELOG.md                                                   |   17 
 src/Apptag/Info.plist                                              |    4 
 src/Apptag/Localization/cs.json                                    |   17 
 src/Apptag/Localization/ms.json                                    |   17 
 src/Apptag/DataLayer.swift                                         |  218 +++++
 src/Apptag/Localization/it.json                                    |   17 
 src/Apptag/Localization/tr.json                                    |   17 
 src/Scripts/pro_feature_gate_qa.sh                                 |   35 
 src/Scripts/settings_language_title_qa.sh                          |   87 ++
 src/Apptag/Localization/da.json                                    |   17 
 src/Apptag/Localization/id.json                                    |   17 
 src/Apptag/Localization/ja.json                                    |   17 
 src/Apptag/PreferencesView.swift                                   |  208 +++++
 src/Apptag/Localization/de.json                                    |   17 
 src/Docs/Requirements/2026-07-01-pro-custom-container-quota-prd.md |  403 +++++++++++
 src/Apptag/Localization/pt-BR.json                                 |   17 
 CODEGRAPH.md                                                       |    4 
 src/Apptag/ProAccessViews.swift                                    |    2 
 src/Apptag/Localization/ar-Najdi.json                              |   17 
 src/Apptag/Localization/nb.json                                    |   17 
 src/Scripts/pro_localization_qa.sh                                 |   20 
 src/Scripts/tag_editor_mouse_event_qa.sh                           |   81 ++
 src/Apptag/Localization/ko.json                                    |   17 
 src/Apptag/Localization/no.json                                    |   17 
 src/Apptag/ContentView.swift                                       |  168 ++++
 src/Apptag/Localization/sr-Cyrl.json                               |   17 
 src/Apptag/Localization/zh-Hans.json                               |   17 
 src/Apptag/Localization/nl.json                                    |   17 
 src/Apptag/Localization/uk.json                                    |   17 
 src/Apptag/Localization/vi.json                                    |   17 
 src/Scripts/pro_custom_hotkeys_qa.sh                               |    2 
 48 files changed, 2,049 insertions(+), 93 deletions(-)

diff --git a/.gitignore b/.gitignore
index 50527e8..6d3f3ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,11 @@
 /canvas/
 /文档导读.md
 
+# Local release snapshots can be large and noisy. Existing tracked release metadata
+# remains visible; force-add exceptional new release records only when needed.
+/src/Release/AppStore-*/
+/src/Release/Freeze-*/
+
 # Local scratch notes.
 /项目管理规范.md
 
diff --git a/CODEGRAPH.md b/CODEGRAPH.md
index f9284d5..1c910f1 100644
--- a/CODEGRAPH.md
+++ b/CODEGRAPH.md
@@ -1,6 +1,6 @@
 # TagLauncher CODEGRAPH
 
-最后更新:2026-06-30
+最后更新:2026-07-01
 
 用途:本文件是 TagLauncher 的长期工程地图。修改用户可见行为、状态模型、构建/发布入口、核心源码或 QA 入口前,应先读本文件,再读指向的真实源码。若本文件与源码不一致,以源码为准,并在同一任务内更新本文件。
 
@@ -213,6 +213,8 @@
   - 使用技巧关闭提醒、modal 防穿透、设置页预览、29 语种文案回归。
 - `src/Scripts/window_logic_qa.sh`
   - 窗口层级、显示隐藏、Dock/菜单栏相关逻辑回归;Dock 项断言按当前 QA 构建路径过滤,避免本机固定正式版 App 时误判重复图标。
+- `src/Scripts/settings_language_title_qa.sh`
+  - 设置窗口标题栏语言刷新静态门禁;检查创建标题、语言切换通知和恢复窗口 `onAppear` 都回写本地化 `NSWindow.title`。
 - `src/Scripts/app_ordering_data_qa.sh`
   - App 排序数据持久化回归。
 
diff --git a/src/Apptag/ApptagApp.swift b/src/Apptag/ApptagApp.swift
index 0166772..2ef8916 100644
--- a/src/Apptag/ApptagApp.swift
+++ b/src/Apptag/ApptagApp.swift
@@ -2169,12 +2169,15 @@
         // NSHostingView.hitTest may return a SwiftUI-internal wrapper — the
         // actual AppKit subview may be nested deeper.
         if let container = findTextFieldContainer(in: hit) {
-            container.mouseDown(with: event)
+            container.focusTextField()
             return
         }
         if let tf = findNSTextField(in: hit) {
+            let shouldSelectAll = tf.currentEditor() == nil
             tf.window?.makeFirstResponder(tf)
-            tf.mouseDown(with: event)
+            if shouldSelectAll {
+                tf.selectText(nil)
+            }
             return
         }
         super.mouseDown(with: event)
diff --git a/src/Apptag/ContentView.swift b/src/Apptag/ContentView.swift
index 4a9cd47..3fede8e 100644
--- a/src/Apptag/ContentView.swift
+++ b/src/Apptag/ContentView.swift
@@ -38,8 +38,8 @@
 
 // MARK: - Native NSTextField (avoids SwiftUI TextField event issues)
 
-/// Custom container that wraps NSTextField so that hitTest returns the container
-/// and mouseDown can reliably make the text field first responder.
+/// Custom container that wraps NSTextField so first focus is reliable while
+/// subsequent clicks keep native caret placement and selection behavior.
 final class TextFieldContainer: NSView {
     let textField: NSTextField
 
@@ -58,14 +58,33 @@
         textField.frame = bounds
     }
 
+    private var isEditing: Bool {
+        guard let window else { return false }
+        return window.firstResponder === textField || textField.currentEditor() != nil
+    }
+
     override func hitTest(_ point: NSPoint) -> NSView? {
-        if bounds.contains(point) { return self }
-        return nil
+        guard bounds.contains(point) else { return nil }
+        if isEditing {
+            let fieldPoint = textField.convert(point, from: self)
+            return textField.hitTest(fieldPoint) ?? textField
+        }
+        return self
+    }
+
+    func focusTextField() {
+        guard let w = window else { return }
+        let shouldSelectAll = !isEditing
+        w.makeFirstResponder(textField)
+        if shouldSelectAll {
+            textField.selectText(nil)
+        }
     }
 
     override func mouseDown(with event: NSEvent) {
-        if let w = window { w.makeFirstResponder(textField) }
-        textField.mouseDown(with: event)
+        // Do not forward this same NSEvent to NSTextField.mouseDown. AppKit can
+        // forward it back to this container during control tracking and recurse.
+        focusTextField()
     }
 }
 
@@ -374,6 +393,7 @@
     let id = UUID()
     let feature: ProFeature
     let noteQuotaStatus: ProNoteQuotaStatus?
+    let customContainerQuotaStatus: ProCustomContainerQuotaStatus?
 }
 
 struct ContentView: View {
@@ -387,6 +407,7 @@
     @State private var tagColors: [String: Int] = [:]
     @State private var tagCustomColors: [String: TagCustomColor] = [:]
     @State private var tagDefinitions: [String: TagDatabase.TagDef] = [:]
+    @State private var customContainerQuotaStatus = ProEntitlementPolicy.customContainerQuotaStatus()
     @State private var containerAppOrder: [String: [String]] = [:]
     @State private var groupLayoutVersion = 0
     @State private var appGridScrollTargetID: String? = nil
@@ -697,11 +718,11 @@
                     benefitText: tr(prompt.feature.benefitKey),
                     operationText: proEntitlement.operationState.statusMessageKey.map(tr),
                     unlockTitle: tr("pro.card.unlock"),
-                    restoreTitle: tr("pro.card.restore"),
+                    restoreTitle: proPromptSecondaryTitle(for: prompt),
                     isBusy: proEntitlement.operationState.isBusy,
                     onClose: dismissProPrompt,
                     onUnlock: { proEntitlement.purchasePro() },
-                    onRestore: { proEntitlement.restorePurchases() }
+                    onRestore: { handleProPromptSecondaryAction(prompt) }
                 )
             }
             .transition(.opacity)
@@ -757,6 +778,7 @@
             tagCustomColors = store.tags.compactMapValues { $0.customColor }
             tagDefinitions = store.tags
             draggedTagNames = TagEditor.orderedTagNames()
+            refreshCustomContainerQuotaStatus(in: store)
         }
         editPhase = phase
         if phase == .none {
@@ -1399,9 +1421,47 @@
                 excludedTagNames: ["Mac自带", defaultGroupName],
                 isCustomColorUnlocked: proEntitlement.isUnlocked,
                 onLockedCustomColor: { presentProPrompt(for: .customTagColors) },
-                onRefresh: { refreshApps() }
+                onCustomContainerQuotaExceeded: presentCustomContainerQuotaPrompt,
+                onCustomContainerQuotaStatusChanged: refreshCustomContainerQuotaStatus,
+                topLeadingAccessory: AnyView(customContainerQuotaAccessory),
+                onRefresh: {
+                    refreshCustomContainerQuotaStatus()
+                    refreshApps()
+                }
             )
         }
+    }
+
+    private var effectiveCustomContainerQuotaStatus: ProCustomContainerQuotaStatus {
+        if proEntitlement.isUnlocked {
+            return ProCustomContainerQuotaStatus(isUnlimited: true, used: 0, limit: Int.max)
+        }
+        return customContainerQuotaStatus
+    }
+
+    private var customContainerQuotaAccessory: some View {
+        let status = effectiveCustomContainerQuotaStatus
+        let text = status.isUnlimited
+            ? tr("pro.customContainers.quotaUnlimitedShort")
+            : tr(
+                "pro.customContainers.quotaShort",
+                replacements: [
+                    "%used%": "\(status.used)",
+                    "%limit%": "\(status.limit)"
+                ]
+            )
+        return Text(text)
+            .font(.system(size: 12, weight: .medium))
+            .foregroundStyle(.secondary)
+            .lineLimit(1)
+    }
+
+    private func refreshCustomContainerQuotaStatus(_ status: ProCustomContainerQuotaStatus) {
+        customContainerQuotaStatus = status
+    }
+
+    private func refreshCustomContainerQuotaStatus(in store: TagDatabase.Store? = nil) {
+        customContainerQuotaStatus = ProEntitlementPolicy.customContainerQuotaStatus(in: store)
     }
 
     // MARK: - Edit Apps View
@@ -1553,7 +1613,7 @@
             .onEnded { _ in
                 guard canReorderTag(tagName) else { return }
                 dragItem = nil
-                TagEditor.reorderTags(draggedTagNames)
+                persistTagOrderIfAllowed()
             }
     }
 
@@ -1581,9 +1641,11 @@
 
         switch editTagOperation {
         case .add:
-            TagEditor.appendTags(tags, to: paths)
+            let result = TagEditor.appendTags(tags, to: paths)
+            guard handleTagMutationResult(result) else { return }
         case .remove:
-            TagEditor.removeTags(tags, from: paths)
+            let result = TagEditor.removeTags(tags, from: paths)
+            guard handleTagMutationResult(result) else { return }
         }
         selectedAppPaths = []
         selectedTagNames = []
@@ -2231,16 +2293,68 @@
            status.remaining <= 0 {
             return tr("pro.notes.limitReached")
         }
+        if prompt.feature == .customContainerQuota,
+           let status = prompt.customContainerQuotaStatus,
+           !status.isUnlimited {
+            return tr(
+                "pro.customContainers.quotaPrompt",
+                replacements: [
+                    "%used%": "\(status.used)",
+                    "%limit%": "\(status.limit)",
+                    "%remaining%": "\(status.remaining)"
+                ]
+            )
+        }
         return tr(prompt.feature.promptMessageKey)
     }
 
-    private func presentProPrompt(for feature: ProFeature, noteQuotaStatus: ProNoteQuotaStatus? = nil) {
+    private func proPromptSecondaryTitle(for prompt: PendingProPrompt) -> String {
+        prompt.feature == .customContainerQuota
+            ? tr("pro.customContainers.manageExisting")
+            : tr("pro.card.restore")
+    }
+
+    private func handleProPromptSecondaryAction(_ prompt: PendingProPrompt) {
+        if prompt.feature == .customContainerQuota {
+            dismissProPrompt()
+            setEditPhase(.editingTags)
+            return
+        }
+        proEntitlement.restorePurchases()
+    }
+
+    private func presentProPrompt(
+        for feature: ProFeature,
+        noteQuotaStatus: ProNoteQuotaStatus? = nil,
+        customContainerQuotaStatus: ProCustomContainerQuotaStatus? = nil
+    ) {
         proEntitlement.clearTransientOperationState()
         withAnimation(.spring(response: 0.22, dampingFraction: 0.84)) {
             appGridInteraction.proPrompt = PendingProPrompt(
                 feature: feature,
-                noteQuotaStatus: noteQuotaStatus
+                noteQuotaStatus: noteQuotaStatus,
+                customContainerQuotaStatus: customContainerQuotaStatus
             )
+        }
+    }
+
+    private func presentCustomContainerQuotaPrompt(_ status: ProCustomContainerQuotaStatus) {
+        presentProPrompt(
+            for: .customContainerQuota,
+            customContainerQuotaStatus: status
+        )
+    }
+
+    private func handleTagMutationResult(_ result: TagEditorMutationResult) -> Bool {
+        switch result {
+        case .saved:
+            refreshCustomContainerQuotaStatus()
+            return true
+        case .noChange:
+            return false
+        case .blockedCustomContainerQuota(let status):
+            presentCustomContainerQuotaPrompt(status)
+            return false
         }
     }
 
@@ -2530,11 +2644,22 @@
         let hadDragState = tagNavDragModeActive || tagNavDragItem != nil
         guard hadDragState else { return }
         if tagNavDragModeActive && tagNavReorderDidMove {
-            TagEditor.reorderTags(draggedTagNames)
+            persistTagOrderIfAllowed()
         }
         tagNavDragModeActive = false
         tagNavDragItem = nil
         tagNavReorderDidMove = false
+    }
+
+    private func persistTagOrderIfAllowed() {
+        guard proEntitlement.isUnlocked else {
+            showDropWarning(message: tr("pro.tagSorting.previewToast"))
+            return
+        }
+        guard TagEditor.reorderTags(draggedTagNames) else {
+            showDropWarning(message: tr("pro.tagSorting.previewToast"))
+            return
+        }
     }
 
     private func cancelTagNavReorderVisualState() {
@@ -2582,13 +2707,14 @@
         }
 
         guard tagColors[targetTag] != nil else { return }
-        TagEditor.moveApp(
+        let result = TagEditor.moveApp(
             path: path,
             from: sourceTag,
             to: targetTag,
             color: tagColors[targetTag] ?? 0,
             copy: copy
         )
+        guard handleTagMutationResult(result) else { return }
         showDropRefresh()
         refreshApps(forceLayoutRefresh: true)
     }
@@ -2598,7 +2724,8 @@
         guard canAssignDroppedAppToTag(targetTag) else { return }
         guard let app = allApps.first(where: { $0.path.path == path }) else { return }
         guard !appHasTag(app, tagName: targetTag) else { return }
-        TagEditor.appendTags([targetTag], to: [path])
+        let result = TagEditor.appendTags([targetTag], to: [path])
+        guard handleTagMutationResult(result) else { return }
         showDropRefresh()
         refreshApps(forceLayoutRefresh: true)
     }
@@ -2735,7 +2862,8 @@
 
     private func moveDroppedAppToUncategorized(path: String, tags: [String]) {
         guard !tags.isEmpty else { return }
-        TagEditor.removeTags(tags, from: [path])
+        let result = TagEditor.removeTags(tags, from: [path])
+        guard handleTagMutationResult(result) else { return }
         showDropRefresh()
         refreshApps(forceLayoutRefresh: true)
     }
@@ -2802,7 +2930,8 @@
         guard isRemovableRegularTag(tagName),
               appHasTag(app, tagName: tagName)
         else { return }
-        TagEditor.removeTags([tagName], from: [app.path.path])
+        let result = TagEditor.removeTags([tagName], from: [app.path.path])
+        guard handleTagMutationResult(result) else { return }
         showDropRefresh()
         refreshApps(forceLayoutRefresh: true)
     }
@@ -2981,6 +3110,7 @@
         tagCustomColors = snapshot.tagDefinitions.compactMapValues { $0.customColor }
         containerAppOrder = snapshot.containerAppOrder
         draggedTagNames = snapshot.tagOrder
+        refreshCustomContainerQuotaStatus()
         rebuildDisplayGroups(apps: snapshot.apps, tagOrder: snapshot.tagOrder)
         if quickSearchVisible {
             refreshQuickSearchResults()
diff --git a/src/Apptag/DataLayer.swift b/src/Apptag/DataLayer.swift
index 2a23bbd..ac1650c 100644
--- a/src/Apptag/DataLayer.swift
+++ b/src/Apptag/DataLayer.swift
@@ -1024,6 +1024,7 @@
         var color: Int
         var systemCategoryID: SmartCategoryID? = nil
         var customColor: TagCustomColor? = nil
+        var isCustomMaintained: Bool? = nil
     }
 
     struct Store: Codable {
@@ -1174,6 +1175,40 @@
         encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
         guard let data = try? encoder.encode(normalizedStore) else { return }
         try? data.write(to: storeURL, options: .atomic)
+    }
+
+    static func customMaintainedContainerCount(in store: Store? = nil) -> Int {
+        customMaintainedContainerNames(in: store).count
+    }
+
+    static func customMaintainedContainerNames(in store: Store? = nil) -> [String] {
+        let effectiveStore = store ?? load()
+        return effectiveStore.tags
+            .filter { isCustomMaintainedTag(name: $0.key, definition: $0.value) }
+            .map(\.key)
+            .sorted()
+    }
+
+    static func isCustomMaintainedTag(name: String, definition: TagDef) -> Bool {
+        guard let systemCategoryID = definition.systemCategoryID else {
+            return true
+        }
+        if definition.isCustomMaintained == true {
+            return true
+        }
+        if definition.customColor != nil {
+            return true
+        }
+        return definition.color != systemCategoryID.defaultColorIndex
+    }
+
+    static func markCustomMaintainedIfNeeded(_ tagName: String, in store: inout Store) {
+        guard var definition = store.tags[tagName],
+              definition.systemCategoryID != nil,
+              !isCustomMaintainedTag(name: tagName, definition: definition)
+        else { return }
+        definition.isCustomMaintained = true
+        store.tags[tagName] = definition
     }
 
     static func normalizedAppOrderPaths(_ paths: [String]) -> [String] {
@@ -1528,6 +1563,7 @@
 
         for (tagName, tagDef) in store.tags {
             guard let categoryID = tagDef.systemCategoryID else { continue }
+            guard !isCustomMaintainedTag(name: tagName, definition: tagDef) else { continue }
             let targetName = categoryID.localizedDisplayName
             guard targetName != tagName else { continue }
             if let existing = store.tags[targetName],
@@ -1555,7 +1591,9 @@
     ) -> (name: String, created: Bool)? {
         if let existingName = systemTagName(for: categoryID, in: store) {
             let targetName = categoryID.localizedDisplayName
-            if existingName != targetName {
+            let existingDefinition = store.tags[existingName]
+            if existingName != targetName,
+               existingDefinition.map({ !isCustomMaintainedTag(name: existingName, definition: $0) }) ?? true {
                 if let existing = store.tags[targetName],
                    existing.systemCategoryID != categoryID {
                     return (existingName, false)
@@ -1832,9 +1870,55 @@
     }
 }
 
+enum TagEditorMutationResult {
+    case saved
+    case noChange
+    case blockedCustomContainerQuota(ProCustomContainerQuotaStatus)
+
+    var didSave: Bool {
+        if case .saved = self {
+            return true
+        }
+        return false
+    }
+}
+
 // MARK: - Tag Editor (CRUD against local TagDatabase)
 
 enum TagEditor {
+    private static func customContainerCandidates(
+        _ tagNames: Set<String>,
+        in store: TagDatabase.Store
+    ) -> Set<String> {
+        Set(tagNames.filter { tagName in
+            guard let definition = store.tags[tagName] else {
+                return true
+            }
+            return !TagDatabase.isCustomMaintainedTag(name: tagName, definition: definition)
+        })
+    }
+
+    private static func customContainerMaintenanceResult(
+        for tagNames: Set<String>,
+        in store: TagDatabase.Store
+    ) -> TagEditorMutationResult? {
+        let addedCount = customContainerCandidates(tagNames, in: store).count
+        switch ProEntitlementPolicy.customContainerMaintenanceDecision(
+            addingCustomContainerCount: addedCount,
+            in: store
+        ) {
+        case .allow:
+            return nil
+        case .blocked(let status):
+            return .blockedCustomContainerQuota(status)
+        }
+    }
+
+    private static func markCustomMaintained(_ tagNames: Set<String>, in store: inout TagDatabase.Store) {
+        for tagName in tagNames {
+            TagDatabase.markCustomMaintainedIfNeeded(tagName, in: &store)
+        }
+    }
 
     // MARK: Read
 
@@ -1857,7 +1941,9 @@
     }
 
     /// Persist a new tag display order.
-    static func reorderTags(_ names: [String]) {
+    @discardableResult
+    static func reorderTags(_ names: [String]) -> Bool {
+        guard ProEntitlementPolicy.isUnlocked(.persistentAppSorting) else { return false }
         var store = TagDatabase.load()
         let previousStore = store
         store.tagOrder = names
@@ -1866,6 +1952,7 @@
             previous: previousStore,
             reason: "reorder-tags"
         )
+        return true
     }
 
     /// Persist the visible app order for one stable container.
@@ -1896,9 +1983,13 @@
     }
 
     /// Create a new tag definition (no app assignments yet).
-    static func createTag(_ name: String, color: Int) {
+    @discardableResult
+    static func createTag(_ name: String, color: Int) -> TagEditorMutationResult {
         var store = TagDatabase.load()
-        guard store.tags[name] == nil else { return }
+        guard store.tags[name] == nil else { return .noChange }
+        if let blocked = customContainerMaintenanceResult(for: [name], in: store) {
+            return blocked
+        }
         let previousStore = store
         store.tags[name] = TagDatabase.TagDef(color: color, systemCategoryID: nil)
         if !store.tagOrder.contains(name) { store.tagOrder.insert(name, at: 0) }
@@ -1907,6 +1998,7 @@
             previous: previousStore,
             reason: "create-tag"
         )
+        return .saved
     }
 
     /// Annotate scanned apps with tags from the database.
@@ -1935,8 +2027,12 @@
     // MARK: Write
 
     /// Assign a tag to multiple apps. Preserves existing tags.
-    static func assignTag(_ tag: String, color: Int, to paths: [String]) {
+    @discardableResult
+    static func assignTag(_ tag: String, color: Int, to paths: [String]) -> TagEditorMutationResult {
         var store = TagDatabase.load()
+        if let blocked = customContainerMaintenanceResult(for: [tag], in: store) {
+            return blocked
+        }
         let previousStore = store
         // Ensure tag definition exists
         if store.tags[tag] == nil {
@@ -1944,6 +2040,7 @@
             // New tag: append to display order
             if !store.tagOrder.contains(tag) { store.tagOrder.insert(tag, at: 0) }
         }
+        markCustomMaintained([tag], in: &store)
         for path in paths {
             var current = store.appTags[path] ?? []
             if !current.contains(tag) {
@@ -1956,14 +2053,27 @@
             previous: previousStore,
             reason: "assign-tag"
         )
+        return .saved
     }
 
     /// Replace the full editable tag set for multiple apps.
-    static func setTags(_ tags: [String], to paths: [String]) {
+    @discardableResult
+    static func setTags(_ tags: [String], to paths: [String]) -> TagEditorMutationResult {
         var store = TagDatabase.load()
-        let previousStore = store
         let selectedUncommon = tags.contains(TagDatabase.uncommonTagKey)
         let validTags = tags.filter { store.tags[$0] != nil }
+        let nextValidTags = Set(validTags)
+        var touchedTags = Set<String>()
+        for path in paths {
+            let currentTags = Set(store.appTags[path] ?? [])
+            touchedTags.formUnion(currentTags.symmetricDifference(nextValidTags))
+        }
+        touchedTags = touchedTags.filter { store.tags[$0] != nil }
+        if let blocked = customContainerMaintenanceResult(for: touchedTags, in: store) {
+            return blocked
+        }
+        let previousStore = store
+        markCustomMaintained(touchedTags, in: &store)
         var uncommonPaths = Set(store.uncommonAppPaths)
         for path in paths {
             if validTags.isEmpty {
@@ -1985,16 +2095,29 @@
             previous: previousStore,
             reason: "set-tags"
         )
+        return .saved
     }
 
     /// Append tags to multiple apps without disturbing their existing tag sets.
-    static func appendTags(_ tags: [String], to paths: [String]) {
-        guard !tags.isEmpty, !paths.isEmpty else { return }
+    @discardableResult
+    static func appendTags(_ tags: [String], to paths: [String]) -> TagEditorMutationResult {
+        guard !tags.isEmpty, !paths.isEmpty else { return .noChange }
 
         var store = TagDatabase.load()
-        let previousStore = store
         let selectedUncommon = tags.contains(TagDatabase.uncommonTagKey)
         let validTags = tags.filter { store.tags[$0] != nil }
+        var touchedTags = Set<String>()
+        for path in paths {
+            let current = Set(store.appTags[path] ?? [])
+            for tag in validTags where !current.contains(tag) {
+                touchedTags.insert(tag)
+            }
+        }
+        if let blocked = customContainerMaintenanceResult(for: touchedTags, in: store) {
+            return blocked
+        }
+        let previousStore = store
+        markCustomMaintained(touchedTags, in: &store)
         var uncommonPaths = Set(store.uncommonAppPaths)
 
         for path in paths {
@@ -2018,16 +2141,27 @@
             previous: previousStore,
             reason: "append-tags"
         )
+        return .saved
     }
 
     /// Remove tags from multiple apps while preserving every unrelated tag.
-    static func removeTags(_ tags: [String], from paths: [String]) {
-        guard !tags.isEmpty, !paths.isEmpty else { return }
+    @discardableResult
+    static func removeTags(_ tags: [String], from paths: [String]) -> TagEditorMutationResult {
+        guard !tags.isEmpty, !paths.isEmpty else { return .noChange }
 
         var store = TagDatabase.load()
-        let previousStore = store
         let selectedUncommon = tags.contains(TagDatabase.uncommonTagKey)
         let validTags = Set(tags.filter { store.tags[$0] != nil })
+        var touchedTags = Set<String>()
+        for path in paths {
+            let current = Set(store.appTags[path] ?? [])
+            touchedTags.formUnion(validTags.intersection(current))
+        }
+        if let blocked = customContainerMaintenanceResult(for: touchedTags, in: store) {
+            return blocked
+        }
+        let previousStore = store
+        markCustomMaintained(touchedTags, in: &store)
         var uncommonPaths = Set(store.uncommonAppPaths)
 
         for path in paths {
@@ -2052,6 +2186,7 @@
             previous: previousStore,
             reason: "remove-tags"
         )
+        return .saved
     }
 
     static func reconcileScannedApps(_ apps: [AppInfo]) -> TagDatabase.Store {
@@ -2245,13 +2380,30 @@
         return decision
     }
 
-    static func moveApp(path: String, from sourceTag: String, to targetTag: String, color: Int, copy: Bool) {
+    @discardableResult
+    static func moveApp(path: String, from sourceTag: String, to targetTag: String, color: Int, copy: Bool) -> TagEditorMutationResult {
         var store = TagDatabase.load()
+        var touchedTags = Set<String>()
+        let currentTags = Set(store.appTags[path] ?? [])
+        if store.tags[targetTag] == nil || !currentTags.contains(targetTag) {
+            touchedTags.insert(targetTag)
+        }
+        if !copy,
+           !sourceTag.isEmpty,
+           sourceTag != "Mac自带",
+           sourceTag != tr("group.appleBuiltIn"),
+           currentTags.contains(sourceTag) {
+            touchedTags.insert(sourceTag)
+        }
+        if let blocked = customContainerMaintenanceResult(for: touchedTags, in: store) {
+            return blocked
+        }
         let previousStore = store
         if store.tags[targetTag] == nil {
             store.tags[targetTag] = TagDatabase.TagDef(color: color, systemCategoryID: nil)
             if !store.tagOrder.contains(targetTag) { store.tagOrder.insert(targetTag, at: 0) }
         }
+        markCustomMaintained(touchedTags, in: &store)
 
         var current = store.appTags[path] ?? []
         if !copy, !sourceTag.isEmpty, sourceTag != "Mac自带", sourceTag != tr("group.appleBuiltIn") {
@@ -2271,17 +2423,31 @@
             previous: previousStore,
             reason: "move-app"
         )
+        return .saved
     }
 
     /// Rename a tag on all apps that have it.
-    static func renameTag(from oldName: String, to newName: String) {
+    @discardableResult
+    static func renameTag(from oldName: String, to newName: String) -> TagEditorMutationResult {
         var store = TagDatabase.load()
-        guard oldName != newName, store.tags[newName] == nil else { return }
+        guard oldName != newName, store.tags[newName] == nil else { return .noChange }
+        guard let oldDefinition = store.tags[oldName] else { return .noChange }
+        let needsCustomContainerSlot = !TagDatabase.isCustomMaintainedTag(
+            name: oldName,
+            definition: oldDefinition
+        )
+        if needsCustomContainerSlot,
+           let blocked = customContainerMaintenanceResult(for: [oldName], in: store) {
+            return blocked
+        }
         let previousStore = store
         // Update tag definition
-        if let def = store.tags.removeValue(forKey: oldName) {
+        if var def = store.tags.removeValue(forKey: oldName) {
             let oldContainerID = AppContainerID.forTag(oldName, definition: def)
             let newContainerID = AppContainerID.forTag(newName, definition: def)
+            if def.systemCategoryID != nil {
+                def.isCustomMaintained = true
+            }
             store.tags[newName] = def
             TagDatabase.migrateContainerAppOrderKey(
                 in: &store,
@@ -2303,6 +2469,7 @@
             previous: previousStore,
             reason: "rename-tag"
         )
+        return .saved
     }
 
     /// Delete a tag completely (from all apps and tag definitions).
@@ -2343,18 +2510,29 @@
     }
 
     /// Set the color for a tag.
-    static func setColor(_ color: Int, for tag: String) {
+    @discardableResult
+    static func setColor(_ color: Int, for tag: String) -> TagEditorMutationResult {
         var store = TagDatabase.load()
-        let previousStore = store
-        var tagDef = store.tags[tag] ?? TagDatabase.TagDef(color: color, systemCategoryID: nil)
+        let existingDefinition = store.tags[tag]
+        var tagDef = existingDefinition ?? TagDatabase.TagDef(color: color, systemCategoryID: nil)
+        let wasCustomMaintained = existingDefinition.map {
+            TagDatabase.isCustomMaintainedTag(name: tag, definition: $0)
+        } ?? false
         tagDef.color = color
         tagDef.customColor = nil
+        let willBeCustomMaintained = TagDatabase.isCustomMaintainedTag(name: tag, definition: tagDef)
+        if !wasCustomMaintained, willBeCustomMaintained,
+           let blocked = customContainerMaintenanceResult(for: [tag], in: store) {
+            return blocked
+        }
+        let previousStore = store
         store.tags[tag] = tagDef
         TagDatabase.saveUserCategorySchemeMutation(
             store,
             previous: previousStore,
             reason: "set-tag-color"
         )
+        return .saved
     }
 
     /// Set a Pro custom color for a tag without changing its fallback base color.
diff --git a/src/Apptag/Info.plist b/src/Apptag/Info.plist
index 99e9171..acf4f6c 100644
--- a/src/Apptag/Info.plist
+++ b/src/Apptag/Info.plist
@@ -19,7 +19,7 @@
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-	<string>8.2.9</string>
+	<string>8.3.1</string>
 	<key>CFBundleURLTypes</key>
 	<array>
 		<dict>
@@ -32,7 +32,7 @@
 		</dict>
 	</array>
 	<key>CFBundleVersion</key>
-	<string>20260630.1617</string>
+	<string>20260701.2302</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 eefed37..567f86f 100644
--- a/src/Apptag/Localization/ar-Najdi.json
+++ b/src/Apptag/Localization/ar-Najdi.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "غير متاح",
   "settings.proStatus.previewFiveMinutes": "معاينة لمدة 5 دقائق فقط",
-  "settings.proGuide.general": "يمكن لمستخدمي Pro فقط استخدام الحاويات/الشبكات الملونة"
+  "settings.proGuide.general": "يمكن لمستخدمي Pro فقط استخدام الحاويات/الشبكات الملونة",
+  "pro.feature.customContainerQuota": "حد الحاويات المخصصة",
+  "pro.feature.customContainerQuota.benefit": "الإصدار المجاني يسمح بإدارة 6 حاويات مخصصة. Pro يفتح إدارة وسوم بدون حدود.",
+  "pro.prompt.customContainerQuota": "حد الحاويات المخصصة من مزايا Pro. افتح Pro لإنشاء وإدارة وسوم بلا حدود.",
+  "pro.customContainers.quotaShort": "يقدر المستخدم المجاني يستخدم 6 وسوم مخصصة (حاويات): %used% / %limit%",
+  "pro.customContainers.quotaUnlimitedShort": "تم فتح Pro: حاويات مخصصة بلا حدود",
+  "pro.customContainers.quotaPrompt": "الإصدار المجاني يسمح بإدارة %limit% حاويات مخصصة. استخدمت %used% / %limit%، وباقي %remaining%. رقِّ إلى Pro لإدارة وسوم بلا حدود، أو عدّل/احذف الوسوم الحالية للاستمرار ضمن الحصة المجانية.",
+  "pro.customContainers.manageExisting": "إدارة الوسوم الحالية",
+  "pro.tagSorting.previewToast": "ترتيب الوسوم الحالي للمعاينة فقط. افتح Pro لحفظ الترتيب.",
+  "pro.feature.maintainableTagCount": "عدد الوسوم القابلة للإدارة",
+  "pro.feature.tagOrderPersistence": "ترتيب مخصص: موضع الوسم في القائمة (= موضع الحاوية)",
+  "pro.feature.containerAppOrderPersistence": "ترتيب مخصص: مواضع أيقونات التطبيقات داخل الحاويات",
+  "settings.proStatus.maxSixTags": "حتى 6",
+  "settings.proStatus.unlimitedTags": "✅ بلا حد",
+  "settings.proStatus.previewOnly": "معاينة فقط",
+  "settings.proStatus.supported": "✅ مدعوم"
 }
diff --git a/src/Apptag/Localization/ar.json b/src/Apptag/Localization/ar.json
index be74fc2..e1f14ef 100644
--- a/src/Apptag/Localization/ar.json
+++ b/src/Apptag/Localization/ar.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "غير متاح",
   "settings.proStatus.previewFiveMinutes": "معاينة لمدة 5 دقائق فقط",
-  "settings.proGuide.general": "يمكن لمستخدمي Pro فقط استخدام الحاويات/الشبكات الملونة"
+  "settings.proGuide.general": "يمكن لمستخدمي Pro فقط استخدام الحاويات/الشبكات الملونة",
+  "pro.feature.customContainerQuota": "حد الحاويات المخصصة",
+  "pro.feature.customContainerQuota.benefit": "يتيح الإصدار المجاني إدارة 6 حاويات مخصصة. يفتح Pro إدارة غير محدودة للوسوم.",
+  "pro.prompt.customContainerQuota": "حد الحاويات المخصصة من ميزات Pro. افتح Pro لإنشاء وإدارة وسوم بلا حدود.",
+  "pro.customContainers.quotaShort": "يمكن للمستخدمين المجانيين استخدام 6 وسوم مخصصة (حاويات): %used% / %limit%",
+  "pro.customContainers.quotaUnlimitedShort": "تم فتح Pro: حاويات مخصصة بلا حدود",
+  "pro.customContainers.quotaPrompt": "يتيح الإصدار المجاني إدارة %limit% حاويات مخصصة. استخدمت %used% / %limit%، والمتبقي %remaining%. قم بالترقية إلى Pro لإدارة وسوم بلا حدود، أو عدّل/احذف الوسوم الحالية للاستمرار ضمن الحصة المجانية.",
+  "pro.customContainers.manageExisting": "إدارة الوسوم الحالية",
+  "pro.tagSorting.previewToast": "ترتيب الوسوم الحالي للمعاينة فقط. افتح Pro لحفظ الترتيب.",
+  "pro.feature.maintainableTagCount": "عدد الوسوم القابلة للإدارة",
+  "pro.feature.tagOrderPersistence": "ترتيب مخصص: موضع الوسم في القائمة (= موضع الحاوية)",
+  "pro.feature.containerAppOrderPersistence": "ترتيب مخصص: مواضع أيقونات التطبيقات داخل الحاويات",
+  "settings.proStatus.maxSixTags": "حتى 6",
+  "settings.proStatus.unlimitedTags": "✅ بلا حد",
+  "settings.proStatus.previewOnly": "معاينة فقط",
+  "settings.proStatus.supported": "✅ مدعوم"
 }
diff --git a/src/Apptag/Localization/cs.json b/src/Apptag/Localization/cs.json
index 13c8264..8f09d1e 100644
--- a/src/Apptag/Localization/cs.json
+++ b/src/Apptag/Localization/cs.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Nedostupné",
   "settings.proStatus.previewFiveMinutes": "Pouze 5minutový náhled",
-  "settings.proGuide.general": "Barevné kontejnery / mřížky mohou používat jen uživatelé Pro"
+  "settings.proGuide.general": "Barevné kontejnery / mřížky mohou používat jen uživatelé Pro",
+  "pro.feature.customContainerQuota": "Limit vlastních kontejnerů",
+  "pro.feature.customContainerQuota.benefit": "Bezplatná verze umožňuje spravovat 6 vlastních kontejnerů. Pro odemkne neomezenou správu štítků.",
+  "pro.prompt.customContainerQuota": "Limit vlastních kontejnerů je funkce Pro. Odemkněte Pro pro neomezené vytváření a správu štítků.",
+  "pro.customContainers.quotaShort": "Bezplatní uživatelé mohou používat 6 vlastních štítků (kontejnerů): využito %used% / %limit%",
+  "pro.customContainers.quotaUnlimitedShort": "Pro odemčeno: vlastní kontejnery bez omezení",
+  "pro.customContainers.quotaPrompt": "Bezplatná verze umožňuje spravovat %limit% vlastních kontejnerů. Využito %used% / %limit%, zbývá %remaining%. Upgradujte na Pro pro neomezené štítky, nebo upravte/smažte stávající štítky a pokračujte v bezplatném limitu.",
+  "pro.customContainers.manageExisting": "Spravovat stávající štítky",
+  "pro.tagSorting.previewToast": "Aktuální pořadí štítků je jen náhled. Odemkněte Pro pro uložení pořadí.",
+  "pro.feature.maintainableTagCount": "Počet spravovatelných štítků",
+  "pro.feature.tagOrderPersistence": "Vlastní řazení: pozice štítku v seznamu (= pozice kontejneru)",
+  "pro.feature.containerAppOrderPersistence": "Vlastní řazení: pozice ikon aplikací v kontejnerech",
+  "settings.proStatus.maxSixTags": "Až 6",
+  "settings.proStatus.unlimitedTags": "✅ Bez omezení",
+  "settings.proStatus.previewOnly": "Jen náhled",
+  "settings.proStatus.supported": "✅ Podporováno"
 }
diff --git a/src/Apptag/Localization/da.json b/src/Apptag/Localization/da.json
index f445cc6..2c1cc90 100644
--- a/src/Apptag/Localization/da.json
+++ b/src/Apptag/Localization/da.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Ikke tilgængelig",
   "settings.proStatus.previewFiveMinutes": "Kun 5 minutters forhåndsvisning",
-  "settings.proGuide.general": "Kun Pro-brugere kan bruge farvede beholdere / gitre"
+  "settings.proGuide.general": "Kun Pro-brugere kan bruge farvede beholdere / gitre",
+  "pro.feature.customContainerQuota": "Kvota for egne beholdere",
+  "pro.feature.customContainerQuota.benefit": "Gratisversionen kan vedligeholde 6 egne beholdere. Pro låser op for ubegrænset tagstyring.",
+  "pro.prompt.customContainerQuota": "Kvoten for egne beholdere er en Pro-funktion. Lås Pro op for at oprette og administrere ubegrænsede tags.",
+  "pro.customContainers.quotaShort": "Gratisbrugere kan bruge 6 egne tags (beholdere): %used% / %limit% brugt",
+  "pro.customContainers.quotaUnlimitedShort": "Pro låst op: ubegrænsede egne beholdere",
+  "pro.customContainers.quotaPrompt": "Gratisversionen kan vedligeholde %limit% egne beholdere. Du har brugt %used% / %limit%, der er %remaining% tilbage. Opgrader til Pro for ubegrænsede tags, eller rediger/slet eksisterende tags for at fortsætte med gratiskvoten.",
+  "pro.customContainers.manageExisting": "Administrer eksisterende tags",
+  "pro.tagSorting.previewToast": "Den aktuelle tagrækkefølge er kun en forhåndsvisning. Lås Pro op for at gemme den.",
+  "pro.feature.maintainableTagCount": "Antal vedligeholdelige tags",
+  "pro.feature.tagOrderPersistence": "Egen sortering: tagposition på listen (= beholderposition)",
+  "pro.feature.containerAppOrderPersistence": "Egen sortering: appikoners placering i beholdere",
+  "settings.proStatus.maxSixTags": "Op til 6",
+  "settings.proStatus.unlimitedTags": "✅ Ingen grænse",
+  "settings.proStatus.previewOnly": "Kun forhåndsvisning",
+  "settings.proStatus.supported": "✅ Understøttet"
 }
diff --git a/src/Apptag/Localization/de.json b/src/Apptag/Localization/de.json
index 9bd3430..a1ee613 100644
--- a/src/Apptag/Localization/de.json
+++ b/src/Apptag/Localization/de.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Nicht verfügbar",
   "settings.proStatus.previewFiveMinutes": "Nur 5 Minuten Vorschau",
-  "settings.proGuide.general": "Nur Pro-Nutzer können farbige Container / Raster verwenden"
+  "settings.proGuide.general": "Nur Pro-Nutzer können farbige Container / Raster verwenden",
+  "pro.feature.customContainerQuota": "Kontingent für eigene Container",
+  "pro.feature.customContainerQuota.benefit": "Die kostenlose Version kann 6 eigene Container verwalten. Pro schaltet unbegrenzte Tag-Verwaltung frei.",
+  "pro.prompt.customContainerQuota": "Das Kontingent für eigene Container ist Teil von Pro. Schalte Pro frei, um unbegrenzt Tags zu erstellen und zu verwalten.",
+  "pro.customContainers.quotaShort": "Kostenlose Nutzer können 6 eigene Tags (Container) verwenden: %used% / %limit% genutzt",
+  "pro.customContainers.quotaUnlimitedShort": "Pro freigeschaltet: unbegrenzte eigene Container",
+  "pro.customContainers.quotaPrompt": "Die kostenlose Version kann %limit% eigene Container verwalten. Verwendet: %used% / %limit%, verbleibend: %remaining%. Upgrade auf Pro für unbegrenzte Tags, oder bearbeite/lösche vorhandene Tags, um das kostenlose Kontingent weiter zu nutzen.",
+  "pro.customContainers.manageExisting": "Vorhandene Tags verwalten",
+  "pro.tagSorting.previewToast": "Die aktuelle Tag-Reihenfolge ist nur eine Vorschau. Schalte Pro frei, um sie zu speichern.",
+  "pro.feature.maintainableTagCount": "Anzahl verwaltbarer Tags",
+  "pro.feature.tagOrderPersistence": "Eigene Sortierung: Tag-Position in der Liste (= Container-Position)",
+  "pro.feature.containerAppOrderPersistence": "Eigene Sortierung: App-Symbolpositionen in Containern",
+  "settings.proStatus.maxSixTags": "Bis zu 6",
+  "settings.proStatus.unlimitedTags": "✅ Keine Begrenzung",
+  "settings.proStatus.previewOnly": "Nur Vorschau",
+  "settings.proStatus.supported": "✅ Unterstützt"
 }
diff --git a/src/Apptag/Localization/en.json b/src/Apptag/Localization/en.json
index f741320..83e8723 100644
--- a/src/Apptag/Localization/en.json
+++ b/src/Apptag/Localization/en.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Unavailable",
   "settings.proStatus.previewFiveMinutes": "5-minute preview only",
-  "settings.proGuide.general": "Only Pro users can use colored containers / colored grids"
+  "settings.proGuide.general": "Only Pro users can use colored containers / colored grids",
+  "pro.feature.customContainerQuota": "Custom container quota",
+  "pro.feature.customContainerQuota.benefit": "Free version can maintain 6 custom containers. Pro unlocks unlimited tag management.",
+  "pro.prompt.customContainerQuota": "Custom container quota is part of Pro. Unlock Pro to create and manage unlimited tags.",
+  "pro.customContainers.quotaShort": "Free users can use 6 custom tags (containers): %used% / %limit% used",
+  "pro.customContainers.quotaUnlimitedShort": "Pro unlocked: unlimited custom containers",
+  "pro.customContainers.quotaPrompt": "Free version can maintain %limit% custom containers. You have used %used% / %limit%, with %remaining% remaining. Upgrade to Pro to create and manage unlimited tags, or edit/delete existing tags to keep using the free quota.",
+  "pro.customContainers.manageExisting": "Manage existing tags",
+  "pro.tagSorting.previewToast": "Current tag order is preview only. Unlock Pro to save tag order.",
+  "pro.feature.maintainableTagCount": "Maintainable tag count",
+  "pro.feature.tagOrderPersistence": "Custom sorting: tag position in the list (= container position)",
+  "pro.feature.containerAppOrderPersistence": "Custom sorting: app icon positions inside containers",
+  "settings.proStatus.maxSixTags": "Up to 6",
+  "settings.proStatus.unlimitedTags": "✅ No limit",
+  "settings.proStatus.previewOnly": "Preview only",
+  "settings.proStatus.supported": "✅ Supported"
 }
diff --git a/src/Apptag/Localization/es.json b/src/Apptag/Localization/es.json
index 2ede230..013bd9c 100644
--- a/src/Apptag/Localization/es.json
+++ b/src/Apptag/Localization/es.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "No disponible",
   "settings.proStatus.previewFiveMinutes": "Solo vista previa de 5 minutos",
-  "settings.proGuide.general": "Solo los usuarios Pro pueden usar contenedores / cuadrículas de color"
+  "settings.proGuide.general": "Solo los usuarios Pro pueden usar contenedores / cuadrículas de color",
+  "pro.feature.customContainerQuota": "Cuota de contenedores personalizados",
+  "pro.feature.customContainerQuota.benefit": "La versión gratuita permite mantener 6 contenedores personalizados. Pro desbloquea gestión ilimitada de etiquetas.",
+  "pro.prompt.customContainerQuota": "La cuota de contenedores personalizados es parte de Pro. Desbloquea Pro para crear y gestionar etiquetas ilimitadas.",
+  "pro.customContainers.quotaShort": "Los usuarios gratis pueden usar 6 etiquetas personalizadas (contenedores): %used% / %limit% usadas",
+  "pro.customContainers.quotaUnlimitedShort": "Pro desbloqueado: contenedores personalizados ilimitados",
+  "pro.customContainers.quotaPrompt": "La versión gratuita permite mantener %limit% contenedores personalizados. Has usado %used% / %limit%, quedan %remaining%. Actualiza a Pro para etiquetas ilimitadas, o edita/elimina etiquetas existentes para seguir usando la cuota gratuita.",
+  "pro.customContainers.manageExisting": "Gestionar etiquetas existentes",
+  "pro.tagSorting.previewToast": "El orden actual de etiquetas es solo una vista previa. Desbloquea Pro para guardarlo.",
+  "pro.feature.maintainableTagCount": "Cantidad de etiquetas mantenibles",
+  "pro.feature.tagOrderPersistence": "Orden personalizado: posición de etiqueta en la lista (= posición del contenedor)",
+  "pro.feature.containerAppOrderPersistence": "Orden personalizado: posiciones de iconos dentro de contenedores",
+  "settings.proStatus.maxSixTags": "Hasta 6",
+  "settings.proStatus.unlimitedTags": "✅ Sin límite",
+  "settings.proStatus.previewOnly": "Solo vista previa",
+  "settings.proStatus.supported": "✅ Compatible"
 }
diff --git a/src/Apptag/Localization/fr.json b/src/Apptag/Localization/fr.json
index 65a6ebc..757cd64 100644
--- a/src/Apptag/Localization/fr.json
+++ b/src/Apptag/Localization/fr.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Indisponible",
   "settings.proStatus.previewFiveMinutes": "Aperçu de 5 minutes seulement",
-  "settings.proGuide.general": "Seuls les utilisateurs Pro peuvent utiliser les conteneurs / grilles colorés"
+  "settings.proGuide.general": "Seuls les utilisateurs Pro peuvent utiliser les conteneurs / grilles colorés",
+  "pro.feature.customContainerQuota": "Quota de conteneurs personnalisés",
+  "pro.feature.customContainerQuota.benefit": "La version gratuite permet de gérer 6 conteneurs personnalisés. Pro déverrouille une gestion illimitée des tags.",
+  "pro.prompt.customContainerQuota": "La limite de conteneurs personnalisés fait partie de Pro. Déverrouillez Pro pour créer et gérer un nombre illimité de tags.",
+  "pro.customContainers.quotaShort": "Les utilisateurs gratuits peuvent utiliser 6 tags personnalisés (conteneurs) : %used% / %limit% utilisés",
+  "pro.customContainers.quotaUnlimitedShort": "Pro déverrouillé : conteneurs personnalisés illimités",
+  "pro.customContainers.quotaPrompt": "La version gratuite permet de gérer %limit% conteneurs personnalisés. Vous avez utilisé %used% / %limit%, il en reste %remaining%. Passez à Pro pour créer et gérer un nombre illimité de tags, ou modifiez/supprimez des tags existants pour continuer avec le quota gratuit.",
+  "pro.customContainers.manageExisting": "Gérer les tags existants",
+  "pro.tagSorting.previewToast": "L’ordre actuel des tags est seulement un aperçu. Déverrouillez Pro pour l’enregistrer.",
+  "pro.feature.maintainableTagCount": "Nombre de tags modifiables",
+  "pro.feature.tagOrderPersistence": "Tri personnalisé : position du tag dans la liste (= position du conteneur)",
+  "pro.feature.containerAppOrderPersistence": "Tri personnalisé : position des icônes dans les conteneurs",
+  "settings.proStatus.maxSixTags": "Jusqu’à 6",
+  "settings.proStatus.unlimitedTags": "✅ Sans limite",
+  "settings.proStatus.previewOnly": "Aperçu seulement",
+  "settings.proStatus.supported": "✅ Pris en charge"
 }
diff --git a/src/Apptag/Localization/id.json b/src/Apptag/Localization/id.json
index 0e94429..a510814 100644
--- a/src/Apptag/Localization/id.json
+++ b/src/Apptag/Localization/id.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Tidak tersedia",
   "settings.proStatus.previewFiveMinutes": "Hanya pratinjau 5 menit",
-  "settings.proGuide.general": "Hanya pengguna Pro yang dapat memakai kontainer / kisi berwarna"
+  "settings.proGuide.general": "Hanya pengguna Pro yang dapat memakai kontainer / kisi berwarna",
+  "pro.feature.customContainerQuota": "Kuota kontainer khusus",
+  "pro.feature.customContainerQuota.benefit": "Versi gratis dapat mengelola 6 kontainer khusus. Pro membuka pengelolaan tag tanpa batas.",
+  "pro.prompt.customContainerQuota": "Kuota kontainer khusus adalah fitur Pro. Buka Pro untuk membuat dan mengelola tag tanpa batas.",
+  "pro.customContainers.quotaShort": "Pengguna gratis dapat memakai 6 tag khusus (kontainer): %used% / %limit% terpakai",
+  "pro.customContainers.quotaUnlimitedShort": "Pro terbuka: kontainer khusus tanpa batas",
+  "pro.customContainers.quotaPrompt": "Versi gratis dapat mengelola %limit% kontainer khusus. Anda memakai %used% / %limit%, tersisa %remaining%. Tingkatkan ke Pro untuk tag tanpa batas, atau edit/hapus tag yang ada untuk tetap memakai kuota gratis.",
+  "pro.customContainers.manageExisting": "Kelola tag yang ada",
+  "pro.tagSorting.previewToast": "Urutan tag saat ini hanya pratinjau. Buka Pro untuk menyimpan urutan tag.",
+  "pro.feature.maintainableTagCount": "Jumlah tag yang dapat dikelola",
+  "pro.feature.tagOrderPersistence": "Pengurutan khusus: posisi tag di daftar (= posisi kontainer)",
+  "pro.feature.containerAppOrderPersistence": "Pengurutan khusus: posisi ikon aplikasi di dalam kontainer",
+  "settings.proStatus.maxSixTags": "Hingga 6",
+  "settings.proStatus.unlimitedTags": "✅ Tanpa batas",
+  "settings.proStatus.previewOnly": "Pratinjau saja",
+  "settings.proStatus.supported": "✅ Didukung"
 }
diff --git a/src/Apptag/Localization/it.json b/src/Apptag/Localization/it.json
index dff1432..6399d25 100644
--- a/src/Apptag/Localization/it.json
+++ b/src/Apptag/Localization/it.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Non disponibile",
   "settings.proStatus.previewFiveMinutes": "Solo anteprima di 5 minuti",
-  "settings.proGuide.general": "Solo gli utenti Pro possono usare contenitori / griglie colorati"
+  "settings.proGuide.general": "Solo gli utenti Pro possono usare contenitori / griglie colorati",
+  "pro.feature.customContainerQuota": "Quota contenitori personalizzati",
+  "pro.feature.customContainerQuota.benefit": "La versione gratuita permette di gestire 6 contenitori personalizzati. Pro sblocca la gestione illimitata dei tag.",
+  "pro.prompt.customContainerQuota": "La quota dei contenitori personalizzati è una funzione Pro. Sblocca Pro per creare e gestire tag illimitati.",
+  "pro.customContainers.quotaShort": "Gli utenti gratuiti possono usare 6 tag personalizzati (contenitori): %used% / %limit% usati",
+  "pro.customContainers.quotaUnlimitedShort": "Pro sbloccato: contenitori personalizzati illimitati",
+  "pro.customContainers.quotaPrompt": "La versione gratuita permette di gestire %limit% contenitori personalizzati. Hai usato %used% / %limit%, ne restano %remaining%. Passa a Pro per tag illimitati, oppure modifica/elimina tag esistenti per continuare con la quota gratuita.",
+  "pro.customContainers.manageExisting": "Gestisci tag esistenti",
+  "pro.tagSorting.previewToast": "L’ordine attuale dei tag è solo un’anteprima. Sblocca Pro per salvarlo.",
+  "pro.feature.maintainableTagCount": "Numero di tag gestibili",
+  "pro.feature.tagOrderPersistence": "Ordinamento personalizzato: posizione del tag nell’elenco (= posizione contenitore)",
+  "pro.feature.containerAppOrderPersistence": "Ordinamento personalizzato: posizioni icone app nei contenitori",
+  "settings.proStatus.maxSixTags": "Fino a 6",
+  "settings.proStatus.unlimitedTags": "✅ Nessun limite",
+  "settings.proStatus.previewOnly": "Solo anteprima",
+  "settings.proStatus.supported": "✅ Supportato"
 }
diff --git a/src/Apptag/Localization/ja.json b/src/Apptag/Localization/ja.json
index cf6d231..ee5e996 100644
--- a/src/Apptag/Localization/ja.json
+++ b/src/Apptag/Localization/ja.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro 版",
   "settings.proStatus.unavailableFeature": "利用不可",
   "settings.proStatus.previewFiveMinutes": "5分間だけプレビュー",
-  "settings.proGuide.general": "カラーコンテナ / カラーグリッドは Pro ユーザーのみ利用できます"
+  "settings.proGuide.general": "カラーコンテナ / カラーグリッドは Pro ユーザーのみ利用できます",
+  "pro.feature.customContainerQuota": "カスタムコンテナ枠",
+  "pro.feature.customContainerQuota.benefit": "無料版では 6 個のカスタムコンテナを管理できます。Pro ではタグ管理が無制限になります。",
+  "pro.prompt.customContainerQuota": "カスタムコンテナ枠は Pro 機能です。Pro を解除すると、タグを無制限に作成・管理できます。",
+  "pro.customContainers.quotaShort": "無料ユーザーは6個のカスタムタグ(コンテナ)を使用できます: %used% / %limit% 使用済み",
+  "pro.customContainers.quotaUnlimitedShort": "Pro 解除済み: カスタムコンテナ無制限",
+  "pro.customContainers.quotaPrompt": "無料版では %limit% 個のカスタムコンテナを管理できます。現在 %used% / %limit% 使用、残り %remaining% 個です。Pro にアップグレードするとタグを無制限に作成・管理できます。既存タグを編集または削除して無料枠を空けることもできます。",
+  "pro.customContainers.manageExisting": "既存タグを管理",
+  "pro.tagSorting.previewToast": "現在のタグ順はプレビューのみです。Pro を解除するとタグ順を保存できます。",
+  "pro.feature.maintainableTagCount": "管理可能なタグ数",
+  "pro.feature.tagOrderPersistence": "カスタム並べ替え: リスト内のタグ位置(= コンテナ表示位置)",
+  "pro.feature.containerAppOrderPersistence": "カスタム並べ替え: コンテナ内のアプリアイコン位置",
+  "settings.proStatus.maxSixTags": "最大 6 個",
+  "settings.proStatus.unlimitedTags": "✅制限なし",
+  "settings.proStatus.previewOnly": "プレビューのみ",
+  "settings.proStatus.supported": "✅ 対応"
 }
diff --git a/src/Apptag/Localization/ko.json b/src/Apptag/Localization/ko.json
index 6a2afe0..7b67c10 100644
--- a/src/Apptag/Localization/ko.json
+++ b/src/Apptag/Localization/ko.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "사용 불가",
   "settings.proStatus.previewFiveMinutes": "5분 미리보기만 가능",
-  "settings.proGuide.general": "컬러 컨테이너 / 컬러 그리드는 Pro 사용자만 사용할 수 있습니다"
+  "settings.proGuide.general": "컬러 컨테이너 / 컬러 그리드는 Pro 사용자만 사용할 수 있습니다",
+  "pro.feature.customContainerQuota": "사용자 컨테이너 한도",
+  "pro.feature.customContainerQuota.benefit": "무료 버전은 사용자 컨테이너 6개를 관리할 수 있습니다. Pro는 태그 관리를 무제한으로 제공합니다.",
+  "pro.prompt.customContainerQuota": "사용자 컨테이너 한도는 Pro 기능입니다. Pro를 잠금 해제하면 태그를 무제한으로 만들고 관리할 수 있습니다.",
+  "pro.customContainers.quotaShort": "무료 사용자는 사용자 지정 태그(컨테이너) 6개를 사용할 수 있습니다: %used% / %limit% 사용됨",
+  "pro.customContainers.quotaUnlimitedShort": "Pro 잠금 해제됨: 사용자 컨테이너 무제한",
+  "pro.customContainers.quotaPrompt": "무료 버전은 사용자 컨테이너 %limit%개를 관리할 수 있습니다. 현재 %used% / %limit%개 사용, 남은 수는 %remaining%개입니다. Pro로 업그레이드하면 태그를 무제한으로 만들고 관리할 수 있습니다. 기존 태그를 편집하거나 삭제해 무료 한도를 계속 사용할 수도 있습니다.",
+  "pro.customContainers.manageExisting": "기존 태그 관리",
+  "pro.tagSorting.previewToast": "현재 태그 순서는 미리보기만 가능합니다. Pro를 잠금 해제하면 태그 순서를 저장할 수 있습니다.",
+  "pro.feature.maintainableTagCount": "관리 가능한 태그 수",
+  "pro.feature.tagOrderPersistence": "사용자 정렬: 목록의 태그 위치(= 컨테이너 위치)",
+  "pro.feature.containerAppOrderPersistence": "사용자 정렬: 컨테이너 안 앱 아이콘 위치",
+  "settings.proStatus.maxSixTags": "최대 6개",
+  "settings.proStatus.unlimitedTags": "✅ 제한 없음",
+  "settings.proStatus.previewOnly": "미리보기만",
+  "settings.proStatus.supported": "✅ 지원"
 }
diff --git a/src/Apptag/Localization/ms.json b/src/Apptag/Localization/ms.json
index 97c9efe..b16ac1b 100644
--- a/src/Apptag/Localization/ms.json
+++ b/src/Apptag/Localization/ms.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Tidak tersedia",
   "settings.proStatus.previewFiveMinutes": "Pratonton 5 minit sahaja",
-  "settings.proGuide.general": "Hanya pengguna Pro boleh menggunakan bekas / grid berwarna"
+  "settings.proGuide.general": "Hanya pengguna Pro boleh menggunakan bekas / grid berwarna",
+  "pro.feature.customContainerQuota": "Kuota bekas tersuai",
+  "pro.feature.customContainerQuota.benefit": "Versi percuma boleh mengurus 6 bekas tersuai. Pro membuka pengurusan tag tanpa had.",
+  "pro.prompt.customContainerQuota": "Kuota bekas tersuai ialah ciri Pro. Buka Pro untuk mencipta dan mengurus tag tanpa had.",
+  "pro.customContainers.quotaShort": "Pengguna percuma boleh menggunakan 6 tag tersuai (bekas): %used% / %limit% digunakan",
+  "pro.customContainers.quotaUnlimitedShort": "Pro dibuka: bekas tersuai tanpa had",
+  "pro.customContainers.quotaPrompt": "Versi percuma boleh mengurus %limit% bekas tersuai. Anda telah menggunakan %used% / %limit%, baki %remaining%. Naik taraf ke Pro untuk tag tanpa had, atau edit/padam tag sedia ada untuk terus menggunakan kuota percuma.",
+  "pro.customContainers.manageExisting": "Urus tag sedia ada",
+  "pro.tagSorting.previewToast": "Susunan tag semasa hanya pratonton. Buka Pro untuk menyimpan susunan tag.",
+  "pro.feature.maintainableTagCount": "Bilangan tag boleh diurus",
+  "pro.feature.tagOrderPersistence": "Susunan tersuai: kedudukan tag dalam senarai (= kedudukan bekas)",
+  "pro.feature.containerAppOrderPersistence": "Susunan tersuai: kedudukan ikon aplikasi dalam bekas",
+  "settings.proStatus.maxSixTags": "Sehingga 6",
+  "settings.proStatus.unlimitedTags": "✅ Tiada had",
+  "settings.proStatus.previewOnly": "Pratonton sahaja",
+  "settings.proStatus.supported": "✅ Disokong"
 }
diff --git a/src/Apptag/Localization/nb.json b/src/Apptag/Localization/nb.json
index 86193a4..4b53453 100644
--- a/src/Apptag/Localization/nb.json
+++ b/src/Apptag/Localization/nb.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Ikke tilgjengelig",
   "settings.proStatus.previewFiveMinutes": "Kun 5 minutters forhåndsvisning",
-  "settings.proGuide.general": "Bare Pro-brukere kan bruke fargede beholdere / fargede rutenett"
+  "settings.proGuide.general": "Bare Pro-brukere kan bruke fargede beholdere / fargede rutenett",
+  "pro.feature.customContainerQuota": "Kvote for egne beholdere",
+  "pro.feature.customContainerQuota.benefit": "Gratisversjonen kan vedlikeholde 6 egne beholdere. Pro låser opp ubegrenset taggstyring.",
+  "pro.prompt.customContainerQuota": "Kvote for egne beholdere er en Pro-funksjon. Lås opp Pro for å opprette og administrere ubegrensede tagger.",
+  "pro.customContainers.quotaShort": "Gratisbrukere kan bruke 6 egne tagger (beholdere): %used% / %limit% brukt",
+  "pro.customContainers.quotaUnlimitedShort": "Pro låst opp: ubegrensede egne beholdere",
+  "pro.customContainers.quotaPrompt": "Gratisversjonen kan vedlikeholde %limit% egne beholdere. Du har brukt %used% / %limit%, %remaining% gjenstår. Oppgrader til Pro for ubegrensede tagger, eller rediger/slett eksisterende tagger for å fortsette med gratiskvoten.",
+  "pro.customContainers.manageExisting": "Administrer eksisterende tagger",
+  "pro.tagSorting.previewToast": "Gjeldende taggrekkefølge er bare forhåndsvisning. Lås opp Pro for å lagre den.",
+  "pro.feature.maintainableTagCount": "Antall tagger som kan vedlikeholdes",
+  "pro.feature.tagOrderPersistence": "Egen sortering: taggposisjon i listen (= beholderposisjon)",
+  "pro.feature.containerAppOrderPersistence": "Egen sortering: appikonposisjoner i beholdere",
+  "settings.proStatus.maxSixTags": "Opptil 6",
+  "settings.proStatus.unlimitedTags": "✅ Ingen grense",
+  "settings.proStatus.previewOnly": "Kun forhåndsvisning",
+  "settings.proStatus.supported": "✅ Støttes"
 }
diff --git a/src/Apptag/Localization/nl.json b/src/Apptag/Localization/nl.json
index 34f4196..9bc018d 100644
--- a/src/Apptag/Localization/nl.json
+++ b/src/Apptag/Localization/nl.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Niet beschikbaar",
   "settings.proStatus.previewFiveMinutes": "Alleen 5 minuten voorbeeld",
-  "settings.proGuide.general": "Alleen Pro-gebruikers kunnen gekleurde containers / rasters gebruiken"
+  "settings.proGuide.general": "Alleen Pro-gebruikers kunnen gekleurde containers / rasters gebruiken",
+  "pro.feature.customContainerQuota": "Limiet voor aangepaste containers",
+  "pro.feature.customContainerQuota.benefit": "De gratis versie kan 6 aangepaste containers beheren. Pro ontgrendelt onbeperkt tagbeheer.",
+  "pro.prompt.customContainerQuota": "Het limiet voor aangepaste containers is een Pro-functie. Ontgrendel Pro om onbeperkt tags te maken en beheren.",
+  "pro.customContainers.quotaShort": "Gratis gebruikers kunnen 6 aangepaste tags (containers) gebruiken: %used% / %limit% gebruikt",
+  "pro.customContainers.quotaUnlimitedShort": "Pro ontgrendeld: onbeperkte aangepaste containers",
+  "pro.customContainers.quotaPrompt": "De gratis versie kan %limit% aangepaste containers beheren. Je hebt %used% / %limit% gebruikt, %remaining% over. Upgrade naar Pro voor onbeperkte tags, of bewerk/verwijder bestaande tags om de gratis limiet te blijven gebruiken.",
+  "pro.customContainers.manageExisting": "Bestaande tags beheren",
+  "pro.tagSorting.previewToast": "De huidige tagvolgorde is alleen een voorbeeld. Ontgrendel Pro om deze op te slaan.",
+  "pro.feature.maintainableTagCount": "Aantal beheerbare tags",
+  "pro.feature.tagOrderPersistence": "Aangepaste sortering: tagpositie in de lijst (= containerpositie)",
+  "pro.feature.containerAppOrderPersistence": "Aangepaste sortering: app-icoonposities in containers",
+  "settings.proStatus.maxSixTags": "Tot 6",
+  "settings.proStatus.unlimitedTags": "✅ Geen limiet",
+  "settings.proStatus.previewOnly": "Alleen voorbeeld",
+  "settings.proStatus.supported": "✅ Ondersteund"
 }
diff --git a/src/Apptag/Localization/nn.json b/src/Apptag/Localization/nn.json
index 1bba986..990634f 100644
--- a/src/Apptag/Localization/nn.json
+++ b/src/Apptag/Localization/nn.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Ikkje tilgjengeleg",
   "settings.proStatus.previewFiveMinutes": "Berre 5 minutt førehandsvising",
-  "settings.proGuide.general": "Berre Pro-brukarar kan bruke farga behaldarar / farga rutenett"
+  "settings.proGuide.general": "Berre Pro-brukarar kan bruke farga behaldarar / farga rutenett",
+  "pro.feature.customContainerQuota": "Kvote for eigne behaldarar",
+  "pro.feature.customContainerQuota.benefit": "Gratisversjonen kan vedlikehalde 6 eigne behaldarar. Pro låser opp uavgrensa merkelappstyring.",
+  "pro.prompt.customContainerQuota": "Kvote for eigne behaldarar er ein Pro-funksjon. Lås opp Pro for å opprette og administrere uavgrensa merkelappar.",
+  "pro.customContainers.quotaShort": "Gratisbrukarar kan bruke 6 eigne taggar (behaldarar): %used% / %limit% brukt",
+  "pro.customContainers.quotaUnlimitedShort": "Pro låst opp: uavgrensa eigne behaldarar",
+  "pro.customContainers.quotaPrompt": "Gratisversjonen kan vedlikehalde %limit% eigne behaldarar. Du har brukt %used% / %limit%, %remaining% står att. Oppgrader til Pro for uavgrensa merkelappar, eller rediger/slett eksisterande merkelappar for å halde fram med gratiskvoten.",
+  "pro.customContainers.manageExisting": "Administrer eksisterande merkelappar",
+  "pro.tagSorting.previewToast": "Gjeldande merkelapprekkjefølgje er berre førehandsvising. Lås opp Pro for å lagre henne.",
+  "pro.feature.maintainableTagCount": "Tal på merkelappar som kan vedlikehaldast",
+  "pro.feature.tagOrderPersistence": "Eiga sortering: merkelapposisjon i lista (= behaldarposisjon)",
+  "pro.feature.containerAppOrderPersistence": "Eiga sortering: appikonposisjonar i behaldarar",
+  "settings.proStatus.maxSixTags": "Opptil 6",
+  "settings.proStatus.unlimitedTags": "✅ Inga grense",
+  "settings.proStatus.previewOnly": "Berre førehandsvising",
+  "settings.proStatus.supported": "✅ Støtta"
 }
diff --git a/src/Apptag/Localization/no.json b/src/Apptag/Localization/no.json
index 86193a4..4b53453 100644
--- a/src/Apptag/Localization/no.json
+++ b/src/Apptag/Localization/no.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Ikke tilgjengelig",
   "settings.proStatus.previewFiveMinutes": "Kun 5 minutters forhåndsvisning",
-  "settings.proGuide.general": "Bare Pro-brukere kan bruke fargede beholdere / fargede rutenett"
+  "settings.proGuide.general": "Bare Pro-brukere kan bruke fargede beholdere / fargede rutenett",
+  "pro.feature.customContainerQuota": "Kvote for egne beholdere",
+  "pro.feature.customContainerQuota.benefit": "Gratisversjonen kan vedlikeholde 6 egne beholdere. Pro låser opp ubegrenset taggstyring.",
+  "pro.prompt.customContainerQuota": "Kvote for egne beholdere er en Pro-funksjon. Lås opp Pro for å opprette og administrere ubegrensede tagger.",
+  "pro.customContainers.quotaShort": "Gratisbrukere kan bruke 6 egne tagger (beholdere): %used% / %limit% brukt",
+  "pro.customContainers.quotaUnlimitedShort": "Pro låst opp: ubegrensede egne beholdere",
+  "pro.customContainers.quotaPrompt": "Gratisversjonen kan vedlikeholde %limit% egne beholdere. Du har brukt %used% / %limit%, %remaining% gjenstår. Oppgrader til Pro for ubegrensede tagger, eller rediger/slett eksisterende tagger for å fortsette med gratiskvoten.",
+  "pro.customContainers.manageExisting": "Administrer eksisterende tagger",
+  "pro.tagSorting.previewToast": "Gjeldende taggrekkefølge er bare forhåndsvisning. Lås opp Pro for å lagre den.",
+  "pro.feature.maintainableTagCount": "Antall tagger som kan vedlikeholdes",
+  "pro.feature.tagOrderPersistence": "Egen sortering: taggposisjon i listen (= beholderposisjon)",
+  "pro.feature.containerAppOrderPersistence": "Egen sortering: appikonposisjoner i beholdere",
+  "settings.proStatus.maxSixTags": "Opptil 6",
+  "settings.proStatus.unlimitedTags": "✅ Ingen grense",
+  "settings.proStatus.previewOnly": "Kun forhåndsvisning",
+  "settings.proStatus.supported": "✅ Støttes"
 }
diff --git a/src/Apptag/Localization/pl.json b/src/Apptag/Localization/pl.json
index 5627ba8..6b44813 100644
--- a/src/Apptag/Localization/pl.json
+++ b/src/Apptag/Localization/pl.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Niedostępne",
   "settings.proStatus.previewFiveMinutes": "Tylko 5 minut podglądu",
-  "settings.proGuide.general": "Tylko użytkownicy Pro mogą używać kolorowych kontenerów / siatek"
+  "settings.proGuide.general": "Tylko użytkownicy Pro mogą używać kolorowych kontenerów / siatek",
+  "pro.feature.customContainerQuota": "Limit własnych kontenerów",
+  "pro.feature.customContainerQuota.benefit": "Wersja bezpłatna pozwala utrzymywać 6 własnych kontenerów. Pro odblokowuje nieograniczone zarządzanie tagami.",
+  "pro.prompt.customContainerQuota": "Limit własnych kontenerów to funkcja Pro. Odblokuj Pro, aby tworzyć i zarządzać tagami bez limitu.",
+  "pro.customContainers.quotaShort": "Użytkownicy wersji darmowej mogą używać 6 własnych tagów (kontenerów): użyto %used% / %limit%",
+  "pro.customContainers.quotaUnlimitedShort": "Pro odblokowane: własne kontenery bez limitu",
+  "pro.customContainers.quotaPrompt": "Wersja bezpłatna pozwala utrzymywać %limit% własnych kontenerów. Użyto %used% / %limit%, pozostało %remaining%. Przejdź na Pro, aby mieć tagi bez limitu, albo edytuj/usuń istniejące tagi, aby dalej korzystać z darmowego limitu.",
+  "pro.customContainers.manageExisting": "Zarządzaj istniejącymi tagami",
+  "pro.tagSorting.previewToast": "Bieżąca kolejność tagów jest tylko podglądem. Odblokuj Pro, aby ją zapisać.",
+  "pro.feature.maintainableTagCount": "Liczba tagów do utrzymania",
+  "pro.feature.tagOrderPersistence": "Sortowanie własne: pozycja tagu na liście (= pozycja kontenera)",
+  "pro.feature.containerAppOrderPersistence": "Sortowanie własne: pozycje ikon aplikacji w kontenerach",
+  "settings.proStatus.maxSixTags": "Do 6",
+  "settings.proStatus.unlimitedTags": "✅ Bez limitu",
+  "settings.proStatus.previewOnly": "Tylko podgląd",
+  "settings.proStatus.supported": "✅ Obsługiwane"
 }
diff --git a/src/Apptag/Localization/pt-BR.json b/src/Apptag/Localization/pt-BR.json
index 7f08756..7fff595 100644
--- a/src/Apptag/Localization/pt-BR.json
+++ b/src/Apptag/Localization/pt-BR.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Indisponível",
   "settings.proStatus.previewFiveMinutes": "Apenas prévia de 5 minutos",
-  "settings.proGuide.general": "Só usuários Pro podem usar contêineres / grades coloridos"
+  "settings.proGuide.general": "Só usuários Pro podem usar contêineres / grades coloridos",
+  "pro.feature.customContainerQuota": "Cota de contêineres personalizados",
+  "pro.feature.customContainerQuota.benefit": "A versão gratuita permite manter 6 contêineres personalizados. O Pro libera gerenciamento ilimitado de tags.",
+  "pro.prompt.customContainerQuota": "A cota de contêineres personalizados faz parte do Pro. Desbloqueie o Pro para criar e gerenciar tags ilimitadas.",
+  "pro.customContainers.quotaShort": "Usuários grátis podem usar 6 tags personalizadas (contêineres): %used% / %limit% usadas",
+  "pro.customContainers.quotaUnlimitedShort": "Pro desbloqueado: contêineres personalizados ilimitados",
+  "pro.customContainers.quotaPrompt": "A versão gratuita permite manter %limit% contêineres personalizados. Você usou %used% / %limit%, restam %remaining%. Atualize para o Pro para tags ilimitadas, ou edite/exclua tags existentes para continuar usando a cota gratuita.",
+  "pro.customContainers.manageExisting": "Gerenciar tags existentes",
+  "pro.tagSorting.previewToast": "A ordem atual das tags é apenas uma prévia. Desbloqueie o Pro para salvar.",
+  "pro.feature.maintainableTagCount": "Quantidade de tags editáveis",
+  "pro.feature.tagOrderPersistence": "Ordenação personalizada: posição da tag na lista (= posição do contêiner)",
+  "pro.feature.containerAppOrderPersistence": "Ordenação personalizada: posições dos ícones dentro dos contêineres",
+  "settings.proStatus.maxSixTags": "Até 6",
+  "settings.proStatus.unlimitedTags": "✅ Sem limite",
+  "settings.proStatus.previewOnly": "Só prévia",
+  "settings.proStatus.supported": "✅ Compatível"
 }
diff --git a/src/Apptag/Localization/ro.json b/src/Apptag/Localization/ro.json
index cf195bb..c324790 100644
--- a/src/Apptag/Localization/ro.json
+++ b/src/Apptag/Localization/ro.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Indisponibil",
   "settings.proStatus.previewFiveMinutes": "Doar previzualizare de 5 minute",
-  "settings.proGuide.general": "Doar utilizatorii Pro pot folosi containere / grile colorate"
+  "settings.proGuide.general": "Doar utilizatorii Pro pot folosi containere / grile colorate",
+  "pro.feature.customContainerQuota": "Cotă pentru containere personalizate",
+  "pro.feature.customContainerQuota.benefit": "Versiunea gratuită poate gestiona 6 containere personalizate. Pro deblochează gestionarea nelimitată a etichetelor.",
+  "pro.prompt.customContainerQuota": "Cota pentru containere personalizate este o funcție Pro. Deblochează Pro pentru a crea și gestiona etichete nelimitate.",
+  "pro.customContainers.quotaShort": "Utilizatorii versiunii gratuite pot folosi 6 etichete personalizate (containere): %used% / %limit% folosite",
+  "pro.customContainers.quotaUnlimitedShort": "Pro deblocat: containere personalizate nelimitate",
+  "pro.customContainers.quotaPrompt": "Versiunea gratuită poate gestiona %limit% containere personalizate. Ai folosit %used% / %limit%, mai rămân %remaining%. Treci la Pro pentru etichete nelimitate sau editează/șterge etichete existente pentru a continua cu cota gratuită.",
+  "pro.customContainers.manageExisting": "Gestionează etichetele existente",
+  "pro.tagSorting.previewToast": "Ordinea actuală a etichetelor este doar previzualizare. Deblochează Pro pentru a o salva.",
+  "pro.feature.maintainableTagCount": "Număr de etichete gestionabile",
+  "pro.feature.tagOrderPersistence": "Sortare personalizată: poziția etichetei în listă (= poziția containerului)",
+  "pro.feature.containerAppOrderPersistence": "Sortare personalizată: pozițiile pictogramelor aplicațiilor în containere",
+  "settings.proStatus.maxSixTags": "Până la 6",
+  "settings.proStatus.unlimitedTags": "✅ Fără limită",
+  "settings.proStatus.previewOnly": "Doar previzualizare",
+  "settings.proStatus.supported": "✅ Acceptat"
 }
diff --git a/src/Apptag/Localization/ru.json b/src/Apptag/Localization/ru.json
index de52125..941c049 100644
--- a/src/Apptag/Localization/ru.json
+++ b/src/Apptag/Localization/ru.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Недоступно",
   "settings.proStatus.previewFiveMinutes": "Только 5 минут предпросмотра",
-  "settings.proGuide.general": "Цветные контейнеры / сетки доступны только пользователям Pro"
+  "settings.proGuide.general": "Цветные контейнеры / сетки доступны только пользователям Pro",
+  "pro.feature.customContainerQuota": "Лимит пользовательских контейнеров",
+  "pro.feature.customContainerQuota.benefit": "В бесплатной версии можно вести 6 пользовательских контейнеров. Pro снимает ограничение на управление тегами.",
+  "pro.prompt.customContainerQuota": "Лимит пользовательских контейнеров — функция Pro. Разблокируйте Pro, чтобы создавать и управлять тегами без ограничений.",
+  "pro.customContainers.quotaShort": "Бесплатные пользователи могут использовать 6 пользовательских тегов (контейнеров): использовано %used% / %limit%",
+  "pro.customContainers.quotaUnlimitedShort": "Pro разблокирован: пользовательские контейнеры без лимита",
+  "pro.customContainers.quotaPrompt": "В бесплатной версии можно вести %limit% пользовательских контейнеров. Использовано %used% / %limit%, осталось %remaining%. Перейдите на Pro для неограниченных тегов или измените/удалите существующие теги, чтобы освободить лимит.",
+  "pro.customContainers.manageExisting": "Управлять тегами",
+  "pro.tagSorting.previewToast": "Текущий порядок тегов — только предпросмотр. Разблокируйте Pro, чтобы сохранить его.",
+  "pro.feature.maintainableTagCount": "Количество редактируемых тегов",
+  "pro.feature.tagOrderPersistence": "Своя сортировка: позиция тега в списке (= позиция контейнера)",
+  "pro.feature.containerAppOrderPersistence": "Своя сортировка: позиции значков приложений в контейнерах",
+  "settings.proStatus.maxSixTags": "До 6",
+  "settings.proStatus.unlimitedTags": "✅ Без лимита",
+  "settings.proStatus.previewOnly": "Только предпросмотр",
+  "settings.proStatus.supported": "✅ Поддерживается"
 }
diff --git a/src/Apptag/Localization/sr-Cyrl.json b/src/Apptag/Localization/sr-Cyrl.json
index 342612f..820e8ae 100644
--- a/src/Apptag/Localization/sr-Cyrl.json
+++ b/src/Apptag/Localization/sr-Cyrl.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Недоступно",
   "settings.proStatus.previewFiveMinutes": "Само 5 минута прегледа",
-  "settings.proGuide.general": "Само Pro корисници могу да користе обојене контејнере / мреже"
+  "settings.proGuide.general": "Само Pro корисници могу да користе обојене контејнере / мреже",
+  "pro.feature.customContainerQuota": "Ограничење прилагођених контејнера",
+  "pro.feature.customContainerQuota.benefit": "Бесплатна верзија може да одржава 6 прилагођених контејнера. Pro откључава неограничено управљање ознакама.",
+  "pro.prompt.customContainerQuota": "Ограничење прилагођених контејнера је Pro функција. Откључајте Pro за неограничено креирање и управљање ознакама.",
+  "pro.customContainers.quotaShort": "Бесплатни корисници могу да користе 6 прилагођених ознака (контејнера): искоришћено %used% / %limit%",
+  "pro.customContainers.quotaUnlimitedShort": "Pro откључан: неограничени прилагођени контејнери",
+  "pro.customContainers.quotaPrompt": "Бесплатна верзија може да одржава %limit% прилагођених контејнера. Искористили сте %used% / %limit%, преостало је %remaining%. Надоградите на Pro за неограничене ознаке или уредите/обришите постојеће ознаке да наставите са бесплатним ограничењем.",
+  "pro.customContainers.manageExisting": "Управљај постојећим ознакама",
+  "pro.tagSorting.previewToast": "Тренутни редослед ознака је само преглед. Откључајте Pro да га сачувате.",
+  "pro.feature.maintainableTagCount": "Број ознака за одржавање",
+  "pro.feature.tagOrderPersistence": "Прилагођено сортирање: позиција ознаке на листи (= позиција контејнера)",
+  "pro.feature.containerAppOrderPersistence": "Прилагођено сортирање: позиције икона апликација у контејнерима",
+  "settings.proStatus.maxSixTags": "До 6",
+  "settings.proStatus.unlimitedTags": "✅ Без ограничења",
+  "settings.proStatus.previewOnly": "Само преглед",
+  "settings.proStatus.supported": "✅ Подржано"
 }
diff --git a/src/Apptag/Localization/sv.json b/src/Apptag/Localization/sv.json
index f6ac8b6..5d46075 100644
--- a/src/Apptag/Localization/sv.json
+++ b/src/Apptag/Localization/sv.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Inte tillgängligt",
   "settings.proStatus.previewFiveMinutes": "Endast 5 minuters förhandsvisning",
-  "settings.proGuide.general": "Endast Pro-användare kan använda färgade behållare / rutnät"
+  "settings.proGuide.general": "Endast Pro-användare kan använda färgade behållare / rutnät",
+  "pro.feature.customContainerQuota": "Kvot för egna behållare",
+  "pro.feature.customContainerQuota.benefit": "Gratisversionen kan hantera 6 egna behållare. Pro låser upp obegränsad tagghantering.",
+  "pro.prompt.customContainerQuota": "Kvoten för egna behållare är en Pro-funktion. Lås upp Pro för att skapa och hantera obegränsat antal taggar.",
+  "pro.customContainers.quotaShort": "Gratisanvändare kan använda 6 egna taggar (behållare): %used% / %limit% använda",
+  "pro.customContainers.quotaUnlimitedShort": "Pro upplåst: obegränsade egna behållare",
+  "pro.customContainers.quotaPrompt": "Gratisversionen kan hantera %limit% egna behållare. Du har använt %used% / %limit%, %remaining% återstår. Uppgradera till Pro för obegränsade taggar, eller redigera/ta bort befintliga taggar för att fortsätta med gratiskvoten.",
+  "pro.customContainers.manageExisting": "Hantera befintliga taggar",
+  "pro.tagSorting.previewToast": "Den aktuella taggordningen är bara en förhandsvisning. Lås upp Pro för att spara den.",
+  "pro.feature.maintainableTagCount": "Antal taggar som kan hanteras",
+  "pro.feature.tagOrderPersistence": "Egen sortering: taggposition i listan (= behållarposition)",
+  "pro.feature.containerAppOrderPersistence": "Egen sortering: appikonernas positioner i behållare",
+  "settings.proStatus.maxSixTags": "Upp till 6",
+  "settings.proStatus.unlimitedTags": "✅ Ingen gräns",
+  "settings.proStatus.previewOnly": "Endast förhandsvisning",
+  "settings.proStatus.supported": "✅ Stöds"
 }
diff --git a/src/Apptag/Localization/th.json b/src/Apptag/Localization/th.json
index e8160b2..8265015 100644
--- a/src/Apptag/Localization/th.json
+++ b/src/Apptag/Localization/th.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "ใช้งานไม่ได้",
   "settings.proStatus.previewFiveMinutes": "ทดลองใช้ฟรีได้เพียง 5 นาที",
-  "settings.proGuide.general": "เฉพาะผู้ใช้ Pro เท่านั้นที่ใช้คอนเทนเนอร์ / กริดสีได้"
+  "settings.proGuide.general": "เฉพาะผู้ใช้ Pro เท่านั้นที่ใช้คอนเทนเนอร์ / กริดสีได้",
+  "pro.feature.customContainerQuota": "โควตาคอนเทนเนอร์กำหนดเอง",
+  "pro.feature.customContainerQuota.benefit": "เวอร์ชันฟรีจัดการคอนเทนเนอร์กำหนดเองได้ 6 รายการ Pro ปลดล็อกการจัดการแท็กแบบไม่จำกัด",
+  "pro.prompt.customContainerQuota": "โควตาคอนเทนเนอร์กำหนดเองเป็นฟีเจอร์ Pro ปลดล็อก Pro เพื่อสร้างและจัดการแท็กได้ไม่จำกัด",
+  "pro.customContainers.quotaShort": "ผู้ใช้ฟรีสามารถใช้แท็กกำหนดเอง 6 แท็ก (คอนเทนเนอร์): ใช้แล้ว %used% / %limit%",
+  "pro.customContainers.quotaUnlimitedShort": "ปลดล็อก Pro แล้ว: คอนเทนเนอร์กำหนดเองไม่จำกัด",
+  "pro.customContainers.quotaPrompt": "เวอร์ชันฟรีจัดการคอนเทนเนอร์กำหนดเองได้ %limit% รายการ คุณใช้ไปแล้ว %used% / %limit% เหลือ %remaining% อัปเกรดเป็น Pro เพื่อจัดการแท็กไม่จำกัด หรือแก้ไข/ลบแท็กเดิมเพื่อใช้โควตาฟรีต่อ",
+  "pro.customContainers.manageExisting": "จัดการแท็กที่มีอยู่",
+  "pro.tagSorting.previewToast": "ลำดับแท็กปัจจุบันเป็นเพียงตัวอย่าง ปลดล็อก Pro เพื่อบันทึกลำดับแท็ก",
+  "pro.feature.maintainableTagCount": "จำนวนแท็กที่จัดการได้",
+  "pro.feature.tagOrderPersistence": "การเรียงเอง: ตำแหน่งแท็กในรายการ (= ตำแหน่งคอนเทนเนอร์)",
+  "pro.feature.containerAppOrderPersistence": "การเรียงเอง: ตำแหน่งไอคอนแอปในคอนเทนเนอร์",
+  "settings.proStatus.maxSixTags": "สูงสุด 6",
+  "settings.proStatus.unlimitedTags": "✅ ไม่จำกัด",
+  "settings.proStatus.previewOnly": "ดูตัวอย่างเท่านั้น",
+  "settings.proStatus.supported": "✅ รองรับ"
 }
diff --git a/src/Apptag/Localization/tr.json b/src/Apptag/Localization/tr.json
index d15fa2c..8364404 100644
--- a/src/Apptag/Localization/tr.json
+++ b/src/Apptag/Localization/tr.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Kullanılamaz",
   "settings.proStatus.previewFiveMinutes": "Yalnızca 5 dakikalık önizleme",
-  "settings.proGuide.general": "Renkli kapsayıcılar / ızgaralar yalnızca Pro kullanıcıları içindir"
+  "settings.proGuide.general": "Renkli kapsayıcılar / ızgaralar yalnızca Pro kullanıcıları içindir",
+  "pro.feature.customContainerQuota": "Özel konteyner kotası",
+  "pro.feature.customContainerQuota.benefit": "Ücretsiz sürüm 6 özel konteyneri yönetebilir. Pro sınırsız etiket yönetimini açar.",
+  "pro.prompt.customContainerQuota": "Özel konteyner kotası Pro özelliğidir. Sınırsız etiket oluşturmak ve yönetmek için Pro kilidini açın.",
+  "pro.customContainers.quotaShort": "Ücretsiz kullanıcılar 6 özel etiketi (konteyneri) kullanabilir: %used% / %limit% kullanıldı",
+  "pro.customContainers.quotaUnlimitedShort": "Pro açık: sınırsız özel konteyner",
+  "pro.customContainers.quotaPrompt": "Ücretsiz sürüm %limit% özel konteyneri yönetebilir. %used% / %limit% kullandınız, %remaining% kaldı. Sınırsız etiket için Pro’ya yükseltin veya ücretsiz kotayı kullanmaya devam etmek için mevcut etiketleri düzenleyin/silin.",
+  "pro.customContainers.manageExisting": "Mevcut etiketleri yönet",
+  "pro.tagSorting.previewToast": "Geçerli etiket sırası yalnızca önizlemedir. Kaydetmek için Pro kilidini açın.",
+  "pro.feature.maintainableTagCount": "Yönetilebilir etiket sayısı",
+  "pro.feature.tagOrderPersistence": "Özel sıralama: listedeki etiket konumu (= konteyner konumu)",
+  "pro.feature.containerAppOrderPersistence": "Özel sıralama: konteyner içindeki uygulama simgesi konumları",
+  "settings.proStatus.maxSixTags": "En fazla 6",
+  "settings.proStatus.unlimitedTags": "✅ Sınır yok",
+  "settings.proStatus.previewOnly": "Yalnızca önizleme",
+  "settings.proStatus.supported": "✅ Desteklenir"
 }
diff --git a/src/Apptag/Localization/uk.json b/src/Apptag/Localization/uk.json
index 0a58f51..f16feb0 100644
--- a/src/Apptag/Localization/uk.json
+++ b/src/Apptag/Localization/uk.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Недоступно",
   "settings.proStatus.previewFiveMinutes": "Лише 5 хвилин попереднього перегляду",
-  "settings.proGuide.general": "Кольорові контейнери / сітки доступні лише користувачам Pro"
+  "settings.proGuide.general": "Кольорові контейнери / сітки доступні лише користувачам Pro",
+  "pro.feature.customContainerQuota": "Ліміт користувацьких контейнерів",
+  "pro.feature.customContainerQuota.benefit": "Безкоштовна версія може підтримувати 6 користувацьких контейнерів. Pro відкриває необмежене керування тегами.",
+  "pro.prompt.customContainerQuota": "Ліміт користувацьких контейнерів — функція Pro. Розблокуйте Pro, щоб створювати й керувати тегами без обмежень.",
+  "pro.customContainers.quotaShort": "Безкоштовні користувачі можуть використовувати 6 власних тегів (контейнерів): використано %used% / %limit%",
+  "pro.customContainers.quotaUnlimitedShort": "Pro розблоковано: користувацькі контейнери без ліміту",
+  "pro.customContainers.quotaPrompt": "Безкоштовна версія може підтримувати %limit% користувацьких контейнерів. Використано %used% / %limit%, залишилося %remaining%. Перейдіть на Pro для необмежених тегів або відредагуйте/видаліть наявні теги, щоб і далі користуватися безкоштовним лімітом.",
+  "pro.customContainers.manageExisting": "Керувати наявними тегами",
+  "pro.tagSorting.previewToast": "Поточний порядок тегів — лише попередній перегляд. Розблокуйте Pro, щоб зберегти його.",
+  "pro.feature.maintainableTagCount": "Кількість тегів для підтримки",
+  "pro.feature.tagOrderPersistence": "Власне сортування: позиція тега у списку (= позиція контейнера)",
+  "pro.feature.containerAppOrderPersistence": "Власне сортування: позиції іконок програм у контейнерах",
+  "settings.proStatus.maxSixTags": "До 6",
+  "settings.proStatus.unlimitedTags": "✅ Без ліміту",
+  "settings.proStatus.previewOnly": "Лише перегляд",
+  "settings.proStatus.supported": "✅ Підтримується"
 }
diff --git a/src/Apptag/Localization/vi.json b/src/Apptag/Localization/vi.json
index 64817f7..8ae8be4 100644
--- a/src/Apptag/Localization/vi.json
+++ b/src/Apptag/Localization/vi.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro",
   "settings.proStatus.unavailableFeature": "Không khả dụng",
   "settings.proStatus.previewFiveMinutes": "Chỉ xem trước 5 phút",
-  "settings.proGuide.general": "Chỉ người dùng Pro mới dùng được vùng chứa / lưới màu"
+  "settings.proGuide.general": "Chỉ người dùng Pro mới dùng được vùng chứa / lưới màu",
+  "pro.feature.customContainerQuota": "Hạn mức vùng chứa tùy chỉnh",
+  "pro.feature.customContainerQuota.benefit": "Bản miễn phí có thể quản lý 6 vùng chứa tùy chỉnh. Pro mở khóa quản lý thẻ không giới hạn.",
+  "pro.prompt.customContainerQuota": "Hạn mức vùng chứa tùy chỉnh là tính năng Pro. Mở khóa Pro để tạo và quản lý thẻ không giới hạn.",
+  "pro.customContainers.quotaShort": "Người dùng miễn phí có thể dùng 6 thẻ tùy chỉnh (vùng chứa): đã dùng %used% / %limit%",
+  "pro.customContainers.quotaUnlimitedShort": "Đã mở khóa Pro: vùng chứa tùy chỉnh không giới hạn",
+  "pro.customContainers.quotaPrompt": "Bản miễn phí có thể quản lý %limit% vùng chứa tùy chỉnh. Bạn đã dùng %used% / %limit%, còn %remaining%. Nâng cấp Pro để quản lý thẻ không giới hạn, hoặc sửa/xóa thẻ hiện có để tiếp tục dùng hạn mức miễn phí.",
+  "pro.customContainers.manageExisting": "Quản lý thẻ hiện có",
+  "pro.tagSorting.previewToast": "Thứ tự thẻ hiện tại chỉ là bản xem trước. Mở khóa Pro để lưu thứ tự thẻ.",
+  "pro.feature.maintainableTagCount": "Số thẻ có thể quản lý",
+  "pro.feature.tagOrderPersistence": "Sắp xếp tùy chỉnh: vị trí thẻ trong danh sách (= vị trí vùng chứa)",
+  "pro.feature.containerAppOrderPersistence": "Sắp xếp tùy chỉnh: vị trí biểu tượng ứng dụng trong vùng chứa",
+  "settings.proStatus.maxSixTags": "Tối đa 6",
+  "settings.proStatus.unlimitedTags": "✅ Không giới hạn",
+  "settings.proStatus.previewOnly": "Chỉ xem trước",
+  "settings.proStatus.supported": "✅ Hỗ trợ"
 }
diff --git a/src/Apptag/Localization/zh-Hans.json b/src/Apptag/Localization/zh-Hans.json
index 4e5f246..e0b1b3f 100644
--- a/src/Apptag/Localization/zh-Hans.json
+++ b/src/Apptag/Localization/zh-Hans.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro 版",
   "settings.proStatus.unavailableFeature": "不可用",
   "settings.proStatus.previewFiveMinutes": "只可预览 5 分钟",
-  "settings.proGuide.general": "仅Pro用户可以使用彩色容器/彩色网格"
+  "settings.proGuide.general": "仅Pro用户可以使用彩色容器/彩色网格",
+  "pro.feature.customContainerQuota": "自定义容器额度",
+  "pro.feature.customContainerQuota.benefit": "免费版可维护 6 个自定义容器。Pro 可创建和管理不限数量的标签。",
+  "pro.prompt.customContainerQuota": "自定义容器额度属于 Pro 功能。解锁 Pro 后,可创建和管理不限数量的标签。",
+  "pro.customContainers.quotaShort": "免费用户可以使用6个自定义标签(容器):已使用 %used% / %limit%",
+  "pro.customContainers.quotaUnlimitedShort": "Pro 已解锁:自定义容器不限数量",
+  "pro.customContainers.quotaPrompt": "免费版可维护 %limit% 个自定义容器,当前已使用 %used% / %limit%,还可维护 %remaining% 个。升级 Pro 后可创建和管理不限数量的标签。你也可以编辑或删除已有标签后继续使用免费额度。",
+  "pro.customContainers.manageExisting": "管理已有标签",
+  "pro.tagSorting.previewToast": "当前标签顺序仅为预览。解锁 Pro 后才可保存标签顺序。",
+  "pro.feature.maintainableTagCount": "可维护标签数量",
+  "pro.feature.tagOrderPersistence": "自定义排序:标签在列表中的位置(=容器显示位置)",
+  "pro.feature.containerAppOrderPersistence": "自定义排序:容器内应用图标位置",
+  "settings.proStatus.maxSixTags": "最多 6 个",
+  "settings.proStatus.unlimitedTags": "✅没有限制",
+  "settings.proStatus.previewOnly": "只可预览",
+  "settings.proStatus.supported": "✅支持"
 }
diff --git a/src/Apptag/Localization/zh-Hant.json b/src/Apptag/Localization/zh-Hant.json
index 19714aa..ef7bad4 100644
--- a/src/Apptag/Localization/zh-Hant.json
+++ b/src/Apptag/Localization/zh-Hant.json
@@ -343,5 +343,20 @@
   "settings.proCompare.pro": "Pro 版",
   "settings.proStatus.unavailableFeature": "不可用",
   "settings.proStatus.previewFiveMinutes": "只可預覽 5 分鐘",
-  "settings.proGuide.general": "僅 Pro 使用者可以使用彩色容器/彩色網格"
+  "settings.proGuide.general": "僅 Pro 使用者可以使用彩色容器/彩色網格",
+  "pro.feature.customContainerQuota": "自訂容器額度",
+  "pro.feature.customContainerQuota.benefit": "免費版可維護 6 個自訂容器。Pro 可建立和管理不限數量的標籤。",
+  "pro.prompt.customContainerQuota": "自訂容器額度屬於 Pro 功能。解鎖 Pro 後,可建立和管理不限數量的標籤。",
+  "pro.customContainers.quotaShort": "免費使用者可以使用 6 個自訂標籤(容器):已使用 %used% / %limit%",
+  "pro.customContainers.quotaUnlimitedShort": "Pro 已解鎖:自訂容器不限數量",
+  "pro.customContainers.quotaPrompt": "免費版可維護 %limit% 個自訂容器,目前已使用 %used% / %limit%,還可維護 %remaining% 個。升級 Pro 後可建立和管理不限數量的標籤。你也可以編輯或刪除既有標籤後繼續使用免費額度。",
+  "pro.customContainers.manageExisting": "管理既有標籤",
+  "pro.tagSorting.previewToast": "目前標籤順序僅為預覽。解鎖 Pro 後才可儲存標籤順序。",
+  "pro.feature.maintainableTagCount": "可維護標籤數量",
+  "pro.feature.tagOrderPersistence": "自訂排序:標籤在列表中的位置(=容器顯示位置)",
+  "pro.feature.containerAppOrderPersistence": "自訂排序:容器內 App 圖示位置",
+  "settings.proStatus.maxSixTags": "最多 6 個",
+  "settings.proStatus.unlimitedTags": "✅沒有限制",
+  "settings.proStatus.previewOnly": "只可預覽",
+  "settings.proStatus.supported": "✅支援"
 }
diff --git a/src/Apptag/PreferencesView.swift b/src/Apptag/PreferencesView.swift
index 67f2899..a147190 100644
--- a/src/Apptag/PreferencesView.swift
+++ b/src/Apptag/PreferencesView.swift
@@ -54,6 +54,7 @@
     private let settingsWindowWidth: CGFloat = 1000
     private let settingsWindowHeight: CGFloat = 640
     private let settingsContentWidth: CGFloat = 940
+    private let languageContentWidth: CGFloat = 820
     private let generalContentWidth: CGFloat = 720
     private let generalLabelWidth: CGFloat = 190
     private let generalControlWidth: CGFloat = 500
@@ -93,10 +94,13 @@
     @State private var hotkeyStatusToastToken: UUID? = nil
     @State private var recordingHotkeyKind: LauncherHotkeyKind? = nil
     @State private var hotkeyRecorderMonitor: Any? = nil
+    @State private var settingsEscapeMonitor: Any? = nil
     @State private var hotkeySettingsRefreshToken = UUID()
     @State private var selectedTab: SettingsTab = .general
     @State private var selectedInitialLayoutMode: InitialLayoutMode = .smart
     @State private var settingsProPromptFeature: ProFeature? = nil
+    @State private var settingsCustomContainerQuotaStatus: ProCustomContainerQuotaStatus? = nil
+    @State private var settingsCustomContainerQuotaOverviewStatus = ProEntitlementPolicy.customContainerQuotaStatus()
     @State private var themePreviewNow = Date()
 
     init(initialTabRawValue: String? = nil) {
@@ -116,11 +120,13 @@
             apps = TagEditor.annotate(apps: apps)
             let colors = store.tags.mapValues { $0.color }
             let customColors = store.tags.compactMapValues { $0.customColor }
+            let quotaStatus = ProEntitlementPolicy.customContainerQuotaStatus(in: store)
             DispatchQueue.main.async {
                 allApps = apps
                 tagColors = colors
                 tagCustomColors = customColors
                 categoryScheme = store.categoryScheme
+                settingsCustomContainerQuotaOverviewStatus = quotaStatus
             }
         }
     }
@@ -230,6 +236,68 @@
         }
     }
 
+    private func installSettingsEscapeMonitor() {
+        guard settingsEscapeMonitor == nil else { return }
+        settingsEscapeMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
+            guard Int(event.keyCode) == kVK_Escape,
+                  isSettingsOverlayEscapeEvent(event)
+            else { return event }
+            return handleSettingsEscapeKey() ? nil : event
+        }
+    }
+
+    private func removeSettingsEscapeMonitor() {
+        if let settingsEscapeMonitor {
+            NSEvent.removeMonitor(settingsEscapeMonitor)
+        }
+        settingsEscapeMonitor = nil
+    }
+
+    private func isSettingsOverlayEscapeEvent(_ event: NSEvent) -> Bool {
+        guard let window = preferencesWindow,
+              window.isVisible,
+              let parent = window.parent as? OverlayPanel,
+              parent.isVisible
+        else { return false }
+
+        if let eventWindow = event.window {
+            return eventWindow == window || eventWindow == parent || eventWindow.parent == window
+        }
+        return NSApp.keyWindow == window || NSApp.keyWindow == parent || NSApp.mainWindow == window
+    }
+
+    private func handleSettingsEscapeKey() -> Bool {
+        guard let window = preferencesWindow else { return false }
+        guard !isDataFilePanelPresented, window.attachedSheet == nil else { return false }
+
+        if settingsProPromptFeature != nil {
+            dismissSettingsProPrompt()
+            return true
+        }
+
+        if showApplySystemSchemeConfirmation {
+            withAnimation(.easeOut(duration: 0.14)) {
+                showApplySystemSchemeConfirmation = false
+            }
+            return true
+        }
+
+        if showResetToUncategorizedConfirmation {
+            withAnimation(.easeOut(duration: 0.14)) {
+                showResetToUncategorizedConfirmation = false
+            }
+            return true
+        }
+
+        if recordingHotkeyKind != nil {
+            stopHotkeyRecording(showCancelledToast: true)
+            return true
+        }
+
+        window.performClose(nil)
+        return true
+    }
+
     private var preferencesWindow: NSWindow? {
         if let taggedWindow = NSApp.windows.first(where: {
             $0.identifier?.rawValue == "TagLauncherPreferencesWindow" && $0.isVisible
@@ -242,14 +310,21 @@
             .first(where: isPreferencesWindowFallback(_:))
     }
 
+    private var localizedPreferencesWindowTitle: String {
+        tr("menu.preferences").replacingOccurrences(of: "…", with: "")
+    }
+
+    private func refreshPreferencesWindowTitle() {
+        preferencesWindow?.title = localizedPreferencesWindowTitle
+    }
+
     private func isPreferencesWindowFallback(_ window: NSWindow) -> Bool {
         guard window.isVisible,
               !(window is NSPanel),
               !(window is OverlayPanel)
         else { return false }
 
-        let preferencesTitle = tr("menu.preferences").replacingOccurrences(of: "…", with: "")
-        return window.title == preferencesTitle
+        return window.title == localizedPreferencesWindowTitle
     }
 
     private func showDataAlert(title: String, message: String) {
@@ -842,6 +917,38 @@
         }
     }
 
+    private var effectiveCustomContainerQuotaStatus: ProCustomContainerQuotaStatus {
+        if proEntitlement.isUnlocked {
+            return ProCustomContainerQuotaStatus(isUnlimited: true, used: 0, limit: Int.max)
+        }
+        return settingsCustomContainerQuotaOverviewStatus
+    }
+
+    private var customContainerQuotaAccessory: some View {
+        let status = effectiveCustomContainerQuotaStatus
+        let text = status.isUnlimited
+            ? tr("pro.customContainers.quotaUnlimitedShort")
+            : tr(
+                "pro.customContainers.quotaShort",
+                replacements: [
+                    "%used%": "\(status.used)",
+                    "%limit%": "\(status.limit)"
+                ]
+            )
+        return Text(text)
+            .font(.system(size: 12, weight: .medium))
+            .foregroundStyle(.secondary)
+            .lineLimit(1)
+    }
+
+    private func refreshSettingsCustomContainerQuotaStatus(_ status: ProCustomContainerQuotaStatus) {
+        settingsCustomContainerQuotaOverviewStatus = status
+    }
+
+    private func refreshSettingsCustomContainerQuotaStatus(in store: TagDatabase.Store? = nil) {
+        settingsCustomContainerQuotaOverviewStatus = ProEntitlementPolicy.customContainerQuotaStatus(in: store)
+    }
+
     private var shouldShowSettingsFixedProHeader: Bool {
         true
     }
@@ -932,6 +1039,33 @@
 
     private var proTabComparisonFeatures: [ProTabComparisonFeature] {
         [
+            ProTabComparisonFeature(
+                title: tr("pro.feature.maintainableTagCount"),
+                freeStatusKey: "settings.proStatus.maxSixTags",
+                proLockedStatusKey: "settings.proStatus.unlimitedTags",
+                proUnlockedStatusKey: "settings.proStatus.unlimitedTags",
+                freeStyle: .neutral,
+                proLockedStyle: .neutral,
+                proUnlockedStyle: .neutral
+            ),
+            ProTabComparisonFeature(
+                title: tr("pro.feature.tagOrderPersistence"),
+                freeStatusKey: "settings.proStatus.previewOnly",
+                proLockedStatusKey: "settings.proStatus.supported",
+                proUnlockedStatusKey: "settings.proStatus.supported",
+                freeStyle: .neutral,
+                proLockedStyle: .neutral,
+                proUnlockedStyle: .neutral
+            ),
+            ProTabComparisonFeature(
+                title: tr("pro.feature.containerAppOrderPersistence"),
+                freeStatusKey: "settings.proStatus.previewOnly",
+                proLockedStatusKey: "settings.proStatus.supported",
+                proUnlockedStatusKey: "settings.proStatus.supported",
+                freeStyle: .neutral,
+                proLockedStyle: .neutral,
+                proUnlockedStyle: .neutral
+            ),
             ProTabComparisonFeature(
                 title: tr("pro.feature.customTagColors"),
                 freeStatusKey: "settings.proStatus.unavailableFeature",
@@ -1169,11 +1303,22 @@
         return effectiveDisplayMode == mode ? .checkmark : .circle
     }
 
-    private func presentSettingsProPrompt(for feature: ProFeature) {
+    private func presentSettingsProPrompt(
+        for feature: ProFeature,
+        customContainerQuotaStatus: ProCustomContainerQuotaStatus? = nil
+    ) {
         proEntitlement.clearTransientOperationState()
+        settingsCustomContainerQuotaStatus = customContainerQuotaStatus
         withAnimation(.spring(response: 0.22, dampingFraction: 0.84)) {
             settingsProPromptFeature = feature
         }
+    }
+
+    private func presentSettingsCustomContainerQuotaPrompt(_ status: ProCustomContainerQuotaStatus) {
+        presentSettingsProPrompt(
+            for: .customContainerQuota,
+            customContainerQuotaStatus: status
+        )
     }
 
     private var themePreviewCountdownText: String? {
@@ -1208,6 +1353,38 @@
         withAnimation(.easeOut(duration: 0.18)) {
             settingsProPromptFeature = nil
         }
+        settingsCustomContainerQuotaStatus = nil
+    }
+
+    private func settingsProPromptMessage(for feature: ProFeature) -> String {
+        if feature == .customContainerQuota,
+           let status = settingsCustomContainerQuotaStatus,
+           !status.isUnlimited {
+            return tr(
+                "pro.customContainers.quotaPrompt",
+                replacements: [
+                    "%used%": "\(status.used)",
+                    "%limit%": "\(status.limit)",
+                    "%remaining%": "\(status.remaining)"
+                ]
+            )
+        }
+        return tr(feature.promptMessageKey)
+    }
+
+    private func settingsProPromptSecondaryTitle(for feature: ProFeature) -> String {
+        feature == .customContainerQuota
+            ? tr("pro.customContainers.manageExisting")
+            : tr("pro.card.restore")
+    }
+
+    private func handleSettingsProPromptSecondaryAction(for feature: ProFeature) {
+        if feature == .customContainerQuota {
+            dismissSettingsProPrompt()
+            selectedTab = .tags
+            return
+        }
+        proEntitlement.restorePurchases()
     }
 
     private func handleProAccessStateChange(_ newState: ProAccessState) {
@@ -1487,7 +1664,7 @@
                     switch selectedTab {
                     case .language:
                 // Tab 1: Language
-                centeredSettingsScrollContent(width: settingsContentWidth) {
+                centeredSettingsScrollContent(width: languageContentWidth) {
                         LazyVGrid(columns: languageColumns, alignment: .leading, spacing: 6) {
                             Button {
                                 selectedLanguage = L10n.automaticCode
@@ -1680,10 +1857,19 @@
                     excludedTagNames: ["Mac自带", defaultGroupName],
                     isCustomColorUnlocked: proEntitlement.isUnlocked,
                     onLockedCustomColor: { presentSettingsProPrompt(for: .customTagColors) },
-                    onRefresh: { scanApps() }
+                    onCustomContainerQuotaExceeded: presentSettingsCustomContainerQuotaPrompt,
+                    onCustomContainerQuotaStatusChanged: refreshSettingsCustomContainerQuotaStatus,
+                    topLeadingAccessory: AnyView(customContainerQuotaAccessory),
+                    onRefresh: {
+                        refreshSettingsCustomContainerQuotaStatus()
+                        scanApps()
+                    }
                 )
             }
-            .onAppear { scanApps() }
+            .onAppear {
+                refreshSettingsCustomContainerQuotaStatus()
+                scanApps()
+            }
 
                     case .data:
             // Tab 5: Data
@@ -1908,6 +2094,7 @@
                 } else {
                     syncSelectedLanguage()
                 }
+                refreshPreferencesWindowTitle()
                 scanApps()
                 refreshDataState()
                 showLanguageRefresh()
@@ -1977,15 +2164,15 @@
 
                     ProUpgradePromptView(
                         title: tr(feature.titleKey),
-                        message: tr(feature.promptMessageKey),
+                        message: settingsProPromptMessage(for: feature),
                         benefitText: tr(feature.benefitKey),
                         operationText: proEntitlement.operationState.statusMessageKey.map(tr),
                         unlockTitle: unlockButtonTitle(),
-                        restoreTitle: tr("pro.card.restore"),
+                        restoreTitle: settingsProPromptSecondaryTitle(for: feature),
                         isBusy: proEntitlement.operationState.isBusy,
                         onClose: dismissSettingsProPrompt,
                         onUnlock: { proEntitlement.purchasePro() },
-                        onRestore: { proEntitlement.restorePurchases() }
+                        onRestore: { handleSettingsProPromptSecondaryAction(for: feature) }
                     )
                 }
                 .zIndex(4)
@@ -2017,10 +2204,13 @@
         .frame(width: settingsWindowWidth, height: settingsWindowHeight)
         .onAppear {
             syncSelectedLanguage()
+            refreshPreferencesWindowTitle()
+            installSettingsEscapeMonitor()
             showPendingHotkeyWarningIfNeeded()
             refreshThemePreviewCountdown()
         }
         .onDisappear {
+            removeSettingsEscapeMonitor()
             stopHotkeyRecording()
         }
         .onReceive(themePreviewCountdownTimer) { now in
diff --git a/src/Apptag/ProAccessViews.swift b/src/Apptag/ProAccessViews.swift
index 49b65d1..acdfe9f 100644
--- a/src/Apptag/ProAccessViews.swift
+++ b/src/Apptag/ProAccessViews.swift
@@ -97,6 +97,8 @@
             return "pro.prompt.export"
         case .customTagColors:
             return "pro.prompt.customTagColors"
+        case .customContainerQuota:
+            return "pro.prompt.customContainerQuota"
         case .unlimitedNotes:
             return "pro.prompt.notes"
         case .persistentAppSorting:
diff --git a/src/Apptag/ProEntitlement.swift b/src/Apptag/ProEntitlement.swift
index 1674af4..b0b6f46 100644
--- a/src/Apptag/ProEntitlement.swift
+++ b/src/Apptag/ProEntitlement.swift
@@ -7,6 +7,7 @@
     case layoutImport
     case layoutExport
     case customTagColors
+    case customContainerQuota
     case unlimitedNotes
     case persistentAppSorting
     case customHotkeys
@@ -21,6 +22,8 @@
             return "pro.feature.export"
         case .customTagColors:
             return "pro.feature.customTagColors"
+        case .customContainerQuota:
+            return "pro.feature.customContainerQuota"
         case .unlimitedNotes:
             return "pro.feature.notes"
         case .persistentAppSorting:
@@ -40,6 +43,8 @@
             return "pro.feature.export.benefit"
         case .customTagColors:
             return "pro.feature.customTagColors.benefit"
+        case .customContainerQuota:
+            return "pro.feature.customContainerQuota.benefit"
         case .unlimitedNotes:
             return "pro.feature.notes.benefit"
         case .persistentAppSorting:
@@ -139,6 +144,21 @@
     case blocked(ProNoteQuotaStatus)
 }
 
+struct ProCustomContainerQuotaStatus: Equatable {
+    let isUnlimited: Bool
+    let used: Int
+    let limit: Int
+
+    var remaining: Int {
+        max(0, limit - used)
+    }
+}
+
+enum ProCustomContainerMaintenanceDecision: Equatable {
+    case allow(ProCustomContainerQuotaStatus)
+    case blocked(ProCustomContainerQuotaStatus)
+}
+
 enum ProEntitlementConfig {
     // 发布前只改这里,不允许把商品信息和边界版本散落到业务模块。
     static let lifetimeProductID = "com.taglauncher.pro.lifetime"
@@ -147,6 +167,7 @@
 
     static let freeThemes: Set<AppGridTheme> = [.defaultLight, .black]
     static let freeNoteLimit = 3
+    static let freeCustomContainerLimit = 6
     static let defaultThemePreviewDuration: TimeInterval = 5 * 60
 
     static let qaStateEnvKey = "TAGLAUNCHER_QA_PRO_STATE"
@@ -225,7 +246,7 @@
 
     static func isUnlocked(_ feature: ProFeature) -> Bool {
         switch feature {
-        case .premiumThemes, .layoutImport, .layoutExport, .customTagColors, .unlimitedNotes, .persistentAppSorting, .customHotkeys:
+        case .premiumThemes, .layoutImport, .layoutExport, .customTagColors, .customContainerQuota, .unlimitedNotes, .persistentAppSorting, .customHotkeys:
             return accessState().isUnlocked
         }
     }
@@ -344,6 +365,37 @@
             throw ProEntitlementError.noteQuotaExceeded(status)
         }
     }
+
+    static func customContainerQuotaStatus(in store: TagDatabase.Store? = nil) -> ProCustomContainerQuotaStatus {
+        guard !isUnlocked(.customContainerQuota) else {
+            return ProCustomContainerQuotaStatus(isUnlimited: true, used: 0, limit: Int.max)
+        }
+        let used = TagDatabase.customMaintainedContainerCount(in: store)
+        return ProCustomContainerQuotaStatus(
+            isUnlimited: false,
+            used: used,
+            limit: ProEntitlementConfig.freeCustomContainerLimit
+        )
+    }
+
+    static func customContainerMaintenanceDecision(
+        addingCustomContainerCount addedCount: Int,
+        in store: TagDatabase.Store? = nil
+    ) -> ProCustomContainerMaintenanceDecision {
+        let status = customContainerQuotaStatus(in: store)
+        guard !status.isUnlimited else { return .allow(status) }
+        guard addedCount > 0 else { return .allow(status) }
+        guard status.used + addedCount <= status.limit else {
+            return .blocked(status)
+        }
+        return .allow(
+            ProCustomContainerQuotaStatus(
+                isUnlimited: false,
+                used: status.used + addedCount,
+                limit: status.limit
+            )
+        )
+    }
 }
 
 @MainActor
diff --git a/src/Apptag/TagEditorView.swift b/src/Apptag/TagEditorView.swift
index 1d2963b..701e532 100644
--- a/src/Apptag/TagEditorView.swift
+++ b/src/Apptag/TagEditorView.swift
@@ -11,6 +11,8 @@
     let excludedTagNames: Set<String>
     let isCustomColorUnlocked: Bool
     let onLockedCustomColor: () -> Void
+    let onCustomContainerQuotaExceeded: (ProCustomContainerQuotaStatus) -> Void
+    let onCustomContainerQuotaStatusChanged: (ProCustomContainerQuotaStatus) -> Void
     var topLeadingAccessory: AnyView? = nil
     var onRefresh: (() -> Void)?
 
@@ -101,7 +103,8 @@
             HStack(spacing: 4) {
                 ForEach(TagColor.allIndices, id: \.self) { idx in
                     ColorSwatch(index: idx, isSelected: !hasCustomColor && idx == colorIndex) {
-                        TagEditor.setColor(idx, for: tagName)
+                        let result = TagEditor.setColor(idx, for: tagName)
+                        guard handleMutationResult(result) else { return }
                         tagColors[tagName] = idx
                         tagCustomColors.removeValue(forKey: tagName)
                         onRefresh?()
@@ -179,7 +182,8 @@
     private func commitTagRename(_ oldName: String) {
         let newName = editingTagText.trimmingCharacters(in: .whitespaces)
         guard !newName.isEmpty, newName != oldName else { editingTagName = nil; return }
-        TagEditor.renameTag(from: oldName, to: newName)
+        let result = TagEditor.renameTag(from: oldName, to: newName)
+        guard handleMutationResult(result) else { return }
         tagColors[newName] = tagColors[oldName]
         tagColors.removeValue(forKey: oldName)
         tagCustomColors[newName] = tagCustomColors[oldName]
@@ -192,16 +196,19 @@
     private func addNewTag() {
         let name = newTagNameText.trimmingCharacters(in: .whitespaces)
         guard !name.isEmpty else { return }
+        let result = TagEditor.createTag(name, color: newTagColorIndex)
+        guard handleMutationResult(result) else { return }
         tagColors[name] = newTagColorIndex
-        TagEditor.createTag(name, color: newTagColorIndex)
         addingNewTag = false
         newTagNameText = ""
+        onRefresh?()
     }
 
     private func deleteTag(_ name: String) {
         TagEditor.deleteTagCompletely(name)
         tagColors.removeValue(forKey: name)
         tagCustomColors.removeValue(forKey: name)
+        onCustomContainerQuotaStatusChanged(ProEntitlementPolicy.customContainerQuotaStatus())
         onRefresh?()
     }
 
@@ -275,8 +282,24 @@
                     return
                 }
                 tagCustomColors[tagName] = customColor
+                onCustomContainerQuotaStatusChanged(ProEntitlementPolicy.customContainerQuotaStatus())
                 onRefresh?()
             }
         )
     }
+
+    @discardableResult
+    private func handleMutationResult(_ result: TagEditorMutationResult) -> Bool {
+        switch result {
+        case .saved:
+            onCustomContainerQuotaStatusChanged(ProEntitlementPolicy.customContainerQuotaStatus())
+            return true
+        case .noChange:
+            return false
+        case .blockedCustomContainerQuota(let status):
+            onCustomContainerQuotaStatusChanged(status)
+            onCustomContainerQuotaExceeded(status)
+            return false
+        }
+    }
 }
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index 1cf5f75..b0b9c02 100644
--- a/src/CHANGELOG.md
+++ b/src/CHANGELOG.md
@@ -2,6 +2,23 @@
 
 ## [Unreleased]
 
+## [8.3.1] — 2026-07-01
+
+- 修复设置窗口在切换语言后,标题栏仍停留在旧语言的问题;标题会在设置窗口出现和语言切换通知时同步刷新
+- 修复免费版标签额度提示“已使用 X / 6”在新建、改名、改色或删除标签后不实时更新的问题
+- 修复标签编辑文本框点击路径,避免编辑标签时因重复转发同一次鼠标事件导致卡死或崩溃
+- 新增 `Scripts/settings_language_title_qa.sh` 和 `Scripts/tag_editor_mouse_event_qa.sh`,并扩展自定义容器额度 QA 覆盖实时额度刷新
+- 版本号更新为 `8.3.1`,Build 更新为 `20260701.2302`
+
+## [8.3.0] — 2026-07-01
+
+- 新增免费版自定义维护容器额度:免费用户最多维护 6 个自定义标签 / 容器;系统默认分类在被改名、改色或手动调整成员后计入额度,Pro 用户不限制
+- 第 7 个自定义维护动作不再写入临时数据,会弹出 Pro 提示,并提供“管理已有标签”入口
+- 标签拖动排序改为免费仅预览、Pro 才持久保存;容器内应用排序仍沿用同一 Pro 排序保存逻辑
+- Pro 页对比表新增前三项:可维护标签数量、标签列表位置排序、容器内应用图标位置排序;同步补齐 29 个语种
+- 新增 `Scripts/pro_custom_container_quota_qa.sh`,并扩展 Pro / 本地化 QA 门禁覆盖新额度、排序预览和对比表文案
+- 版本号更新为 `8.3.0`,Build 更新为 `20260701.1530`
+
 ## [8.2.9] — 2026-06-30
 
 - 统一已解锁 Pro 用户在设置页“语言 / Pro / 关于”顶部状态行:居中显示 `Pro 已解锁` 胶囊和“您当前是尊贵的 Pro 用户”
diff --git a/src/Docs/Requirements/2026-07-01-pro-custom-container-quota-prd.md b/src/Docs/Requirements/2026-07-01-pro-custom-container-quota-prd.md
new file mode 100644
index 0000000..8a07223
--- /dev/null
+++ b/src/Docs/Requirements/2026-07-01-pro-custom-container-quota-prd.md
@@ -0,0 +1,403 @@
+# PRD:自定义标签额度、排序保存与 Pro 转化
+
+日期:2026-07-01
+状态:产品方案讨论稿,待研发评审与实现
+适用产品:TagLauncher macOS App
+
+## 1. 功能名
+
+免费版自定义标签额度、标签排序预览与 Pro 无限容器管理
+
+## 2. 需求描述
+
+TagLauncher 的核心价值是用标签 / 容器管理 Mac 应用。为了提高 Pro 付费转化,需要把 Pro 权益更直接地绑定到核心功能上:免费用户可以体验默认智能分类和基础整理能力,但当用户开始搭建自己的长期容器体系时,应自然触发 Pro 升级理由。
+
+本需求不采用“维护次数”限制。免费版限制的是可个性化维护的自定义标签 / 容器数量,以及标签拖动排序的保存能力。
+
+## 3. 背景与目标
+
+### 3.1 背景
+
+当前 Pro 权益已覆盖高级主题、自定义标签颜色、自定义快捷键、无限备注、数据导入导出、手动排序持久化等能力。但这些能力中,部分并不直接指向 TagLauncher 最核心的容器管理价值。
+
+用户最可能为 TagLauncher 付费的理由,是需要长期维护更多自定义容器、构建更完整的应用分组体系。因此,本需求希望围绕“标签维护规模”建立更清晰的免费 / Pro 边界。
+
+### 3.2 目标
+
+| 目标 | 说明 |
+|:---|:---|
+| 提升 Pro 转化 | 把升级触点放在用户开始扩展自定义容器体系、希望沉淀长期排序习惯时。 |
+| 保留新用户激活价值 | 默认智能分类仍可免费使用,避免下载后立刻被付费墙挡住。 |
+| 规则可解释 | 用户应能理解“免费可维护 6 个自定义容器,Pro 不限”。 |
+| 避免数据被扣押 | 老用户和导入数据不能被删除或隐藏;超额时应保留查看和启动能力。 |
+| 避免惩罚试错 | 不限制改名、改色等操作次数,避免用户不敢整理。 |
+| 保持体验可感知 | 免费用户可以预览标签拖动排序效果,但保存长期排序结果需要 Pro。 |
+
+## 4. 术语定义
+
+| 术语 | 定义 | 是否计入免费额度 |
+|:---|:---|:---|
+| 系统分类 | App 固定存在或系统生成的分类,例如“未分类”“Mac 自带”。 | 不计入 |
+| 默认智能分类 | SmartStart / 智能初始化生成的默认分类,当前约 12 个。 | 默认不计入 |
+| 自定义标签 / 自建容器 | 用户手动创建的标签或容器。 | 计入 |
+| 自定义维护容器 | 用户对默认智能分类进行个性化维护后形成的容器,或用户自建容器。 | 计入 |
+| 个性化维护 | 会让默认分类变成用户长期自定义容器的操作,例如改名、改基础颜色、手动调整应用归属。 | 会触发计入 |
+| 减法操作 | 删除、隐藏、清理、合并,目的是减少或恢复到免费额度内。 | 不新增额度 |
+| 标签拖动排序预览 | 用户拖动标签并看到临时顺序变化,但该顺序不作为长期结果保存。 | 不计入 |
+| 标签排序保存 | 用户拖动标签后的顺序被保留,并在后续打开、刷新或重启后继续生效。 | Pro 权益 |
+
+## 5. 产品结论
+
+### 5.1 推荐方案
+
+免费用户最多可维护 6 个自定义维护容器。
+
+这 6 个可以来自:
+
+- 用户手动新建的标签。
+- 用户从导入 / 恢复数据中带来的自定义标签。
+- 用户对默认智能分类进行个性化维护后转化出的标签。
+
+Pro 用户可维护不限数量的自定义标签 / 容器。
+
+标签拖动排序的保存能力属于 Pro 权益。免费用户可以拖动标签预览排序效果,但不能保存新的标签顺序。
+
+### 5.2 不采用方案
+
+不采用“维护次数”限制,例如“免费用户只能改名 / 改颜色 100 次”。
+
+原因:
+
+- 次数型限制会惩罚用户试错和整理行为。
+- 用户不容易理解什么算一次维护。
+- 撤销、批量操作、导入恢复、失败重试会带来大量解释和 QA 成本。
+- 付费理由会从“升级组织能力”变成“避免被扣次数”,口碑风险更高。
+
+## 6. 免费版规则
+
+### 6.1 免费用户始终可用
+
+| 能力 | 免费规则 |
+|:---|:---|
+| 使用默认智能分类 | 可浏览、启动、搜索默认分类中的应用。 |
+| 使用系统分类 | “未分类”“Mac 自带”等系统分类可正常使用。 |
+| 创建自定义标签 | 可创建,直到自定义维护容器数量达到 6 个。 |
+| 删除 / 隐藏 / 清理标签 | 允许,且用于释放额度或回到免费限制内。 |
+| 拖动标签排序 | 允许预览排序效果,但不保存新的标签顺序。 |
+| 启动 App | 不受标签额度影响。 |
+| Quick Search | 基础搜索不受标签额度影响。 |
+
+### 6.2 免费用户受限
+
+| 操作 | 规则 |
+|:---|:---|
+| 创建第 7 个自定义标签 | 阻止创建,并提示升级 Pro。 |
+| 把第 7 个默认智能分类改名 | 阻止改名,并提示升级 Pro。 |
+| 把第 7 个默认智能分类改基础颜色 | 阻止改色,并提示升级 Pro。 |
+| 手动重组第 7 个默认智能分类的应用归属 | 阻止维护动作,并提示升级 Pro。 |
+| 保存标签拖动排序 | 免费用户只能预览,保存排序需要 Pro。 |
+| 对超额自定义容器继续维护 | 允许查看和启动;继续改名、改色、增加维护需要 Pro。 |
+
+### 6.3 默认智能分类的处理
+
+默认智能分类免费可用,但不是免费无限维护额度。
+
+| 场景 | 行为 |
+|:---|:---|
+| 用户只是浏览 / 启动默认分类中的应用 | 不计入 6 个额度。 |
+| 用户删除或隐藏默认分类 | 不计入 6 个额度。 |
+| 用户改默认分类名称 | 该默认分类转为自定义维护容器,占用 1 个额度。 |
+| 用户改默认分类基础颜色 | 该默认分类转为自定义维护容器,占用 1 个额度。 |
+| 用户手动调整默认分类中的应用归属 | 该默认分类转为自定义维护容器,占用 1 个额度。 |
+| 用户已经有 6 个自定义维护容器,再维护第 7 个默认分类 | 提示升级 Pro,不执行该维护动作。 |
+
+## 7. Pro 规则
+
+Pro 用户解锁不限数量的自定义标签 / 容器维护。
+
+| 能力 | Pro 规则 |
+|:---|:---|
+| 创建标签 | 不限制数量。 |
+| 个性化维护默认分类 | 不限制数量。 |
+| 改名 / 改基础颜色 | 不限制已拥有的标签数量。 |
+| 维护应用归属 | 不限制自定义容器数量。 |
+| 保存标签拖动排序 | 不限制,拖动完成后保留排序结果。 |
+| 老用户超额数据 | 可完整继续维护。 |
+
+## 8. 额度计算规则
+
+### 8.1 计入额度
+
+| 动作 | 计入方式 |
+|:---|:---|
+| 新建用户标签 | 新增 1 个自定义维护容器。 |
+| 导入 / 恢复自定义标签 | 进入自定义维护容器集合。 |
+| 默认智能分类改名 | 转为自定义维护容器,新增 1 个。 |
+| 默认智能分类改基础颜色 | 转为自定义维护容器,新增 1 个。 |
+| 默认智能分类被手动调整应用归属 | 转为自定义维护容器,新增 1 个。 |
+
+### 8.2 不新增额度
+
+| 动作 | 规则 |
+|:---|:---|
+| 浏览分类 | 不新增额度。 |
+| 启动应用 | 不新增额度。 |
+| 搜索应用 | 不新增额度。 |
+| 删除标签 | 不新增额度;如果删除的是自定义维护容器,则释放额度。 |
+| 隐藏默认分类 | 不新增额度。 |
+| 编辑已计入额度的标签 | 不新增额度,只要该标签已经属于 6 个可维护容器之一。 |
+| 清理或合并标签 | 不新增额度,目的是帮助用户回到免费限制内。 |
+
+## 9. 老用户与历史数据
+
+### 9.1 基本原则
+
+不得因为免费额度限制删除、隐藏或破坏用户已有数据。
+
+### 9.2 老用户已有超过 6 个自定义标签
+
+| 场景 | 规则 |
+|:---|:---|
+| 查看已有标签 | 允许。 |
+| 从已有标签启动 App | 允许。 |
+| 删除 / 合并 / 清理已有标签 | 允许。 |
+| 新增更多自定义标签 | 需要 Pro。 |
+| 继续维护超额标签 | 需要 Pro,或先清理到 6 个可维护容器以内。 |
+
+### 9.3 从 Pro 降级为免费
+
+如果用户失去 Pro,已有自定义容器不删除、不隐藏。用户仍可查看和启动,但超出免费额度的维护能力需要重新解锁 Pro,或通过删除 / 合并回到免费额度内。
+
+## 10. 页面与交互要求
+
+### 10.1 自定义容器额度计数
+
+标签设置页应展示免费用户当前还能维护多少个自定义容器,让用户在触发限制前就能理解额度状态。
+
+免费用户建议展示:
+
+> 免费版可维护 6 个自定义容器,当前还可维护 X 个。升级 Pro 后可创建和管理不限数量的标签。
+
+也可以在空间受限时使用短文案:
+
+> 自定义容器:已使用 X / 6
+
+Pro 用户建议展示:
+
+> Pro 已解锁:自定义容器不限数量
+
+额度计数规则:
+
+| 状态 | 展示要求 |
+|:---|:---|
+| 免费用户,已使用 0-5 个额度 | 显示已使用数量和剩余额度。 |
+| 免费用户,已使用 6 个额度 | 显示已用满,并在后续超额维护动作中触发 Pro 提示。 |
+| 免费用户,历史数据超过 6 个额度 | 显示已超出免费额度,但不得删除或隐藏已有标签。 |
+| Pro 用户 | 显示不限数量,不展示 6 个上限压力。 |
+
+### 10.2 标签设置页
+
+标签设置页应让用户知道当前额度状态,并让用户能管理已有标签,以便删除、合并或清理标签后释放免费额度。
+
+建议展示:
+
+> 自定义容器:已使用 X / 6
+
+当用户已是 Pro:
+
+> Pro 已解锁:自定义容器不限数量
+
+### 10.3 新建标签
+
+用户点击“新建标签”时:
+
+| 状态 | 行为 |
+|:---|:---|
+| 免费用户,已使用 0-5 个额度 | 正常创建。 |
+| 免费用户,已使用 6 个额度 | 不创建,显示 Pro 提示。 |
+| Pro 用户 | 正常创建。 |
+
+### 10.4 个性化维护默认分类
+
+当免费用户首次对默认智能分类做个性化维护时,如果仍有额度,允许操作,并可轻提示该分类已计入自定义容器额度。
+
+当免费用户额度已满时,阻止该维护动作,提示升级 Pro。
+
+### 10.5 Pro 提示文案
+
+当用户触发第 7 个自定义维护动作时,直接拦截,不创建临时标签、不保存临时改动、不提供预览逻辑。
+
+本节文案仅适用于“自定义容器额度已满”的拦截场景;标签拖动排序采用预览逻辑,见 10.7。
+
+标题建议:
+
+> 自定义容器已用满
+
+正文建议:
+
+> 免费版可维护 6 个自定义标签。升级 Pro 后可创建和管理不限数量的标签,让你的应用分组保持完整。你也可以编辑或删除已有标签后继续使用免费额度。
+
+按钮建议:
+
+| 按钮 | 用途 |
+|:---|:---|
+| 升级 Pro | 进入购买流程。 |
+| 管理已有标签 | 回到标签管理页,帮助用户删除、合并或清理标签。 |
+
+### 10.6 Pro 页 / 功能对比
+
+Pro 页应新增或更新一行:
+
+| 功能 | 免费版 | Pro 版 |
+|:---|:---|:---|
+| 自定义标签 / 容器维护 | 6 个 | 不限数量 |
+| 标签排序保存 | 仅预览 | 可保存 |
+
+### 10.7 标签拖动排序保存
+
+标签拖动排序应作为独立的 Pro 权益处理,交互参考已实现的“在容器内部拖动改变 App 图标顺序,免费版用户只能预览”。
+
+| 用户状态 | 行为 |
+|:---|:---|
+| 免费用户 | 可以拖动标签,并看到临时排序效果。 |
+| 免费用户拖动结束 | 不保存新的标签顺序,展示 Pro 提示。 |
+| 免费用户重新打开 / 刷新 / 触发标签列表重建 | 标签顺序恢复为当前已保存顺序。 |
+| Pro 用户 | 可以拖动标签,拖动结束后保存新的标签顺序。 |
+| Pro 用户重新打开 / 刷新 / 触发标签列表重建 | 标签顺序保持为上次保存结果。 |
+
+免费用户的提示方式应与现有 App 图标排序预览提示保持一致,不新增独立视觉样式。
+
+建议文案:
+
+> 当前仅为预览。升级 Pro 后可保存标签排序。
+
+交互边界:
+
+- 不在拖动开始前阻断用户。
+- 免费用户拖动过程应保持顺畅,可以看到排序预览。
+- 拖动结束后不保存新的顺序。
+- 历史已保存的标签顺序不被清空。
+- 本需求不改变容器内部 App 图标排序的现有规则。
+
+## 11. 用户旅程
+
+### 11.1 新用户正常激活
+
+1. 用户安装并打开 TagLauncher。
+2. 用户完成智能初始化,看到约 12 个默认分类。
+3. 用户可直接浏览默认分类并启动应用。
+4. 用户没有被“默认分类超过免费额度”提示打断。
+
+### 11.2 免费用户创建第 7 个自定义标签
+
+1. 用户已经有 6 个自定义维护容器。
+2. 用户点击“新建标签”。
+3. 系统不创建第 7 个自定义标签。
+4. 系统展示 Pro 提示。
+5. 用户可选择升级 Pro,或返回管理已有标签。
+
+### 11.3 免费用户改造默认分类
+
+1. 用户已有 5 个自定义维护容器。
+2. 用户把一个默认智能分类改名。
+3. 系统允许改名,并将该分类计入自定义维护容器。
+4. 用户额度变为 6 / 6。
+5. 用户再尝试改造另一个默认分类时,系统提示升级 Pro。
+
+### 11.4 老用户超额
+
+1. 老用户已有 10 个自定义标签。
+2. 用户升级到带有本限制的版本。
+3. 已有 10 个标签仍可查看和启动。
+4. 用户尝试新增第 11 个标签时,提示升级 Pro。
+5. 用户可删除或合并标签,回到免费额度内。
+
+### 11.5 免费用户预览标签排序
+
+1. 免费用户希望调整标签展示顺序。
+2. 用户拖动标签,界面展示拖动后的临时排序效果。
+3. 用户松手完成拖动。
+4. 系统提示当前仅为预览,保存标签排序需要 Pro。
+5. 用户重新打开、刷新或触发标签列表重建后,标签顺序恢复为当前已保存顺序。
+
+### 11.6 Pro 用户保存标签排序
+
+1. Pro 用户拖动标签调整顺序。
+2. 用户松手完成拖动。
+3. 系统保存新的标签顺序。
+4. 用户重新打开、刷新或触发标签列表重建后,标签顺序保持为上次保存结果。
+
+## 12. 用户故事
+
+| 编号 | 用户故事 | 验收条件 |
+|:---|:---|:---|
+| TAG-QUOTA-01 | 作为新用户,我希望默认智能分类可以直接使用,以便先体验应用分组价值。 | 默认智能分类不因数量超过 6 而触发付费墙。 |
+| TAG-QUOTA-02 | 作为免费用户,我希望可以创建少量自定义标签,以便整理自己的常用应用。 | 免费用户可维护最多 6 个自定义维护容器。 |
+| TAG-QUOTA-03 | 作为免费用户,我希望知道为什么第 7 个标签需要 Pro,以便理解升级价值。 | 第 7 个自定义维护动作被阻止,并展示清晰的 Pro 提示。 |
+| TAG-QUOTA-04 | 作为免费用户,我希望删除或合并标签后恢复额度,以便继续整理。 | 删除自定义维护容器后释放额度。 |
+| TAG-QUOTA-05 | 作为 Pro 用户,我希望不受标签数量限制,以便建立完整应用容器体系。 | Pro 用户可创建和维护不限数量的自定义标签。 |
+| TAG-QUOTA-06 | 作为老用户,我希望已有标签不被删除,以便继续访问我的整理结果。 | 超额历史标签仍可查看和启动。 |
+| TAG-SORT-01 | 作为免费用户,我希望能拖动标签看看排序效果,以便判断升级 Pro 后是否有价值。 | 免费用户可拖动标签并看到临时排序效果。 |
+| TAG-SORT-02 | 作为免费用户,我希望知道为什么排序没有保存,以便理解 Pro 权益边界。 | 免费用户拖动结束后看到“仅预览,保存需 Pro”的提示。 |
+| TAG-SORT-03 | 作为 Pro 用户,我希望保存自定义标签顺序,以便长期保持自己的容器浏览习惯。 | Pro 用户拖动标签后,新的标签顺序被保存并持续生效。 |
+
+## 13. 验收标准
+
+| 编号 | 验收项 |
+|:---|:---|
+| QA-TQ-01 | 新用户默认智能分类约 12 个不计入免费额度,不触发 Pro 提示。 |
+| QA-TQ-02 | 免费用户可创建第 1-6 个自定义标签。 |
+| QA-TQ-03 | 免费用户创建第 7 个自定义标签时被阻止,并看到 Pro 提示。 |
+| QA-TQ-04 | 免费用户删除 1 个自定义标签后,可再次创建 1 个。 |
+| QA-TQ-05 | 免费用户改名默认智能分类时,该分类计入自定义维护容器额度。 |
+| QA-TQ-06 | 免费用户已满 6 个额度后,不能改造第 7 个默认智能分类。 |
+| QA-TQ-07 | Pro 用户可创建和维护超过 6 个自定义标签。 |
+| QA-TQ-08 | 老用户已有超过 6 个标签时,数据不被删除、不被隐藏。 |
+| QA-TQ-09 | 老用户免费状态下新增更多自定义标签需要 Pro。 |
+| QA-TQ-10 | Pro 页功能对比展示“自定义标签 / 容器维护:免费 6 个,Pro 不限”。 |
+| QA-TQ-11 | 免费用户可看到自定义容器额度计数,例如“已使用 X / 6”或“当前还可维护 X 个”。 |
+| QA-TQ-12 | 触发第 7 个自定义维护动作时不创建临时标签、不保存临时改动、不进入预览,只展示“升级 Pro”和“管理已有标签”。 |
+| QA-TS-01 | 免费用户可以拖动标签,并看到排序预览效果。 |
+| QA-TS-02 | 免费用户拖动标签结束后,不保存新的标签顺序。 |
+| QA-TS-03 | 免费用户重新打开、刷新或触发标签列表重建后,标签顺序恢复为当前已保存顺序。 |
+| QA-TS-04 | 免费用户拖动标签结束后,看到与 App 图标排序预览一致风格的 Pro 提示。 |
+| QA-TS-05 | Pro 用户拖动标签结束后,新的标签顺序被保存。 |
+| QA-TS-06 | Pro 用户重新打开、刷新或触发标签列表重建后,标签顺序保持为上次保存结果。 |
+| QA-TS-07 | 历史已保存的标签顺序不因本需求上线被清空。 |
+| QA-TS-08 | 本需求不影响容器内部 App 图标排序的既有免费预览规则。 |
+
+## 14. 不做范围
+
+| 不做项 | 说明 |
+|:---|:---|
+| 维护次数限制 | 不做改名 / 改色 / 调整次数的消耗型额度。 |
+| 默认分类总数限制 | 默认智能分类不因为数量超过 6 而触发付费墙。 |
+| 自动删除超额标签 | 不删除、不隐藏老用户或导入数据中的超额标签。 |
+| 标签图标能力 | 本需求不新增标签图标或上传图标。 |
+| App 图标排序规则 | 不调整容器内部 App 图标排序的现有 Pro 预览 / 保存规则。 |
+| 新增排序视觉样式 | 不为标签排序新增一套独立提示视觉,沿用现有 Pro 提示体系。 |
+| 订阅模式 | 本需求不改变 Pro 一次性买断策略。 |
+
+## 15. 指标建议
+
+| 指标 | 观察目的 |
+|:---|:---|
+| 智能初始化完成率 | 确认默认分类不被付费墙干扰。 |
+| 第 7 个自定义标签触达率 | 衡量升级触点覆盖面。 |
+| Pro 提示点击率 | 衡量文案和触发时机是否有效。 |
+| 触达后 7 天购买率 | 衡量该权益对转化的真实贡献。 |
+| 触达后流失率 / 卸载率 | 监控限制是否过重。 |
+| 删除 / 合并标签行为 | 判断用户是否能理解并管理额度。 |
+| 标签排序预览触达率 | 衡量免费用户是否会尝试调整标签顺序。 |
+| 标签排序预览后的 Pro 点击率 | 衡量“保存标签排序”权益是否具备转化价值。 |
+
+## 16. 决策记录
+
+| 日期 | 决策 |
+|:---|:---|
+| 2026-07-01 | 不采用维护次数限制。 |
+| 2026-07-01 | 免费额度限制对象定义为“自定义维护容器”,不是所有标签总数。 |
+| 2026-07-01 | 默认智能分类可免费使用;一旦被个性化维护,则计入 6 个免费额度。 |
+| 2026-07-01 | V1 方案按免费 6 个自定义维护容器编写。 |
+| 2026-07-01 | 增加自定义容器额度计数;第 7 个自定义维护动作直接拦截,不做预览。 |
+| 2026-07-01 | 标签拖动排序保存定义为 Pro 权益;免费用户可以拖动预览,但不保存新的标签顺序。 |
diff --git a/src/Docs/Requirements/free-vs-pro-feature-memo.md b/src/Docs/Requirements/free-vs-pro-feature-memo.md
new file mode 100644
index 0000000..3938903
--- /dev/null
+++ b/src/Docs/Requirements/free-vs-pro-feature-memo.md
@@ -0,0 +1,162 @@
+# 免费版与 Pro 版功能对比备忘
+
+更新时间:2026-07-01
+维护目的:记录 TagLauncher 免费版限制与 Pro 权益的当前产品口径。后续围绕免费 / Pro 主题的变化,应优先更新本文件,避免不同 PRD、实现计划和历史讨论之间互相冲突。
+当前状态:包含已实现能力与 2026-07-01 新增讨论中的“自定义标签额度”待实现能力。
+
+## 1. 使用规则
+
+- 本文件是免费 / Pro 权益的业务备忘,不替代具体 PRD、研发计划或 QA 报告。
+- 当历史 PRD 与本文件冲突时,应以本文件中标注为“当前口径”的内容为准,再回查对应 PRD 或代码确认实现状态。
+- 每次新增、删除或调整 Pro 权益时,应更新:
+  - 功能对比表。
+  - 限制边界。
+  - 老用户 / 降级 / 导入恢复规则。
+  - 状态标记:已实现、待实现、已废弃或待确认。
+
+## 2. 总体原则
+
+| 原则 | 说明 |
+|:---|:---|
+| 免费版要能体验核心价值 | 用户应能免费体验应用启动、默认智能分类和基础标签整理。 |
+| Pro 应围绕长期组织能力 | Pro 重点解锁更大规模、更稳定、更个性化的应用容器管理。 |
+| 不惩罚试错 | 不采用“维护次数”这类会让用户不敢改名、改色、整理的限制。 |
+| 不扣押数据 | 免费限制不得删除、隐藏或破坏用户已有标签、备注和布局数据。 |
+| 限制要可解释 | 文案应优先说“免费额度 / Pro 不限”,避免让用户觉得被突然惩罚。 |
+
+## 3. 功能对比总表
+
+| 功能 | 免费版 | Pro 版 | 状态 | 备注 |
+|:---|:---|:---|:---|:---|
+| App 启动 | 支持 | 支持 | 当前已实现 | 核心基础能力,不作为 Pro 限制。 |
+| Quick Search 基础搜索 | 支持 | 支持 | 当前已实现 | 自定义快捷键另算 Pro 能力。 |
+| 默认智能分类 | 可使用 | 可使用并可全面维护 | 当前口径 | 默认约 12 个分类不因数量触发付费墙。 |
+| 系统分类 | 可使用 | 可使用 | 当前口径 | 包括“未分类”“Mac 自带”等,不计入自定义额度。 |
+| 自定义标签 / 容器维护 | 最多 6 个自定义维护容器 | 不限数量 | 待实现 | 详见《2026-07-01-pro-custom-container-quota-prd.md》。 |
+| 默认分类个性化维护 | 计入 6 个自定义维护容器额度 | 不限数量 | 待实现 | 改名、改基础颜色、手动调整应用归属都算个性化维护。 |
+| 删除 / 隐藏 / 合并标签 | 支持 | 支持 | 当前口径 | 减法操作不新增额度;删除自定义容器释放额度。 |
+| 基础标签颜色 | 对已可维护标签可用 | 不限数量使用 | 当前口径 / 待整合 | 免费用户改默认分类基础颜色时会让该分类计入额度。 |
+| 自定义标签颜色 | 不可新增或修改;已有颜色继续显示 | 支持 | 当前已实现 | 自定义任意颜色属于 Pro。 |
+| 应用备注 | 最多 3 个非空自定义备注 | 无限备注 | 当前已实现 | 编辑已有备注不增加额度;删除备注释放额度。 |
+| 自定义快捷键 | 不支持 | 支持 | 当前已实现 | 免费用户使用默认快捷键。 |
+| 高级主题 | 可 5 分钟预览 | 永久使用 | 当前已实现 | 免费主题当前为默认和黑色。 |
+| 彩色容器 / 彩色网格 | 可 5 分钟预览 | 永久使用 | 当前已实现 | 与 Pro 主题相同预览逻辑。 |
+| 手动应用排序持久化 | 可临时预览,不保存 | 保存排序 | 当前已实现 | 免费用户可拖动预览;只有 Pro 持久保存。 |
+| 数据导入导出 | 不支持 | 支持 | 当前已实现 | 包括分类、布局、备份恢复相关能力。 |
+| 老付费用户权益 | 符合条件自动 Pro | Pro | 当前已实现 | 首个免费 + Pro 版本前的付费用户保护。 |
+| 恢复购买 | 可用于恢复 Pro | 可用于多设备恢复 | 当前已实现 | 已是 Pro 时不应继续展示无意义的恢复购买入口。 |
+
+## 4. 自定义标签额度备忘
+
+### 4.1 当前决策
+
+免费用户最多可维护 6 个自定义维护容器。
+
+计入额度的对象:
+
+- 用户手动新建的标签。
+- 导入 / 恢复数据中的用户自定义标签。
+- 被用户个性化维护过的默认智能分类。
+
+不计入额度的对象:
+
+- 未分类。
+- Mac 自带。
+- 默认智能分类在未被个性化维护前的原始状态。
+- 用户只是浏览、搜索、启动应用的行为。
+
+### 4.2 默认智能分类
+
+默认智能分类免费可用,但不是 12 个免费自定义名额。
+
+| 用户行为 | 免费版结果 |
+|:---|:---|
+| 浏览默认分类 | 不计入额度。 |
+| 从默认分类启动 App | 不计入额度。 |
+| 删除 / 隐藏默认分类 | 不计入额度。 |
+| 改默认分类名称 | 该分类转为自定义维护容器,占用 1 个额度。 |
+| 改默认分类基础颜色 | 该分类转为自定义维护容器,占用 1 个额度。 |
+| 手动调整默认分类应用归属 | 该分类转为自定义维护容器,占用 1 个额度。 |
+| 已满 6 个额度后继续改造默认分类 | 阻止该维护动作,提示升级 Pro。 |
+
+### 4.3 老用户与降级
+
+- 已有超额标签不删除、不隐藏。
+- 免费用户仍可查看超额标签并从中启动 App。
+- 免费用户可删除、合并、清理标签以回到额度内。
+- 超额状态下新增更多标签、继续维护更多超额容器,需要 Pro。
+- Pro 用户失去 Pro 后,已有超额数据保留,但超出免费额度的继续维护需要重新解锁 Pro 或清理回额度内。
+
+## 5. 备注额度备忘
+
+| 场景 | 免费版 | Pro 版 |
+|:---|:---|:---|
+| 新增第 1-3 个非空应用备注 | 允许 | 允许 |
+| 新增第 4 个非空应用备注 | 阻止并提示 Pro | 允许 |
+| 编辑已有备注 | 允许 | 允许 |
+| 删除 / 清空备注 | 释放额度 | 不受限制 |
+
+## 6. 主题与显示模式备忘
+
+| 能力 | 免费版 | Pro 版 |
+|:---|:---|:---|
+| 免费主题 | 默认、黑色 | 默认、黑色也可用 |
+| Pro 主题 | 5 分钟预览 | 永久使用 |
+| 彩色容器 | 5 分钟预览 | 永久使用 |
+| 彩色网格 | 5 分钟预览 | 永久使用 |
+
+预览原则:
+
+- 免费用户可以预览,目的是理解 Pro 效果。
+- 预览结束后恢复到可用状态。
+- 预览不应破坏用户标签、备注、排序等真实数据。
+
+## 7. 数据与排序备忘
+
+| 能力 | 免费版 | Pro 版 |
+|:---|:---|:---|
+| 手动拖动应用排序 | 可临时预览 | 可预览并保存 |
+| 排序持久化 | 不保存 | 保存 |
+| 分类 / 布局导出 | 不支持 | 支持 |
+| 分类 / 布局导入 | 不支持 | 支持 |
+| 备份恢复 | 受限或不可执行 | 支持 |
+
+已确认口径:容器内应用拖动排序当前是“免费可拖动预览、Pro 才能持久保存”,不是“免费用户完全不能拖动”。
+
+## 8. Pro 提示文案方向
+
+### 8.1 自定义标签额度
+
+标题:
+
+> 自定义容器已用满
+
+正文:
+
+> 免费版可维护 6 个自定义标签。升级 Pro 后可创建和管理不限数量的标签,让你的应用分组保持完整。
+
+按钮:
+
+- 升级 Pro
+- 管理已有标签
+- 稍后
+
+### 8.2 备注额度
+
+> 免费版最多可保存 3 个应用备注。解锁 Pro 后,可为所有应用保存备注。
+
+### 8.3 自定义标签颜色
+
+> 自定义标签颜色属于 Pro 功能。解锁 Pro 后,可为每个标签设置任意颜色,让分类更容易识别。
+
+### 8.4 手动排序持久化
+
+> 当前仅为预览。解锁 Pro 后才可保存手动排序。
+
+## 9. 更新记录
+
+| 日期 | 变更 |
+|:---|:---|
+| 2026-07-01 | 新增免费 / Pro 功能对比备忘。 |
+| 2026-07-01 | 记录自定义标签额度方案:免费 6 个自定义维护容器,Pro 不限。 |
+| 2026-07-01 | 记录默认智能分类边界:默认可用,个性化维护后计入额度。 |
diff --git a/src/Scripts/pro_custom_container_quota_qa.sh b/src/Scripts/pro_custom_container_quota_qa.sh
new file mode 100755
index 0000000..7c7225f
--- /dev/null
+++ b/src/Scripts/pro_custom_container_quota_qa.sh
@@ -0,0 +1,143 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+python3 - "$ROOT_DIR" <<'PY'
+import json
+import sys
+from pathlib import Path
+
+root = Path(sys.argv[1])
+app_dir = root / "Apptag"
+localization_dir = app_dir / "Localization"
+
+
+def fail(message: str) -> None:
+    raise SystemExit(f"FAIL: {message}")
+
+
+def read(name: str) -> str:
+    return (app_dir / name).read_text(encoding="utf-8")
+
+
+def require(source: str, needle: str, message: str) -> None:
+    if needle not in source:
+        fail(message)
+
+
+pro = read("ProEntitlement.swift")
+data_layer = read("DataLayer.swift")
+tag_editor = read("TagEditorView.swift")
+content = read("ContentView.swift")
+preferences = read("PreferencesView.swift")
+access = read("ProAccessViews.swift")
+
+require(pro, "case customContainerQuota", "ProFeature.customContainerQuota is missing")
+require(pro, "static let freeCustomContainerLimit = 6", "free custom container limit must be 6")
+require(pro, "struct ProCustomContainerQuotaStatus", "custom container quota status model is missing")
+require(pro, "customContainerMaintenanceDecision", "custom container maintenance decision helper is missing")
+require(pro, "TagDatabase.customMaintainedContainerCount", "quota must count through TagDatabase")
+require(access, "case .customContainerQuota:", "custom container quota prompt key is missing")
+
+require(data_layer, "var isCustomMaintained: Bool? = nil", "TagDef must store an optional custom-maintenance marker for old-data compatibility")
+require(data_layer, "static func customMaintainedContainerCount", "TagDatabase custom container count helper is missing")
+require(data_layer, "guard let systemCategoryID = definition.systemCategoryID else", "user-created tags must count as custom maintained containers")
+require(data_layer, "definition.color != systemCategoryID.defaultColorIndex", "system tags with changed base color must count")
+require(data_layer, "definition.customColor != nil", "system tags with custom color must count")
+require(data_layer, "markCustomMaintainedIfNeeded", "system tag manual maintenance marker is missing")
+require(data_layer, "static func createTag(_ name: String, color: Int) -> TagEditorMutationResult", "createTag must return a mutation result")
+require(data_layer, "customContainerMaintenanceResult(for: [name], in: store)", "createTag must check free quota before saving")
+require(data_layer, "static func renameTag(from oldName: String, to newName: String) -> TagEditorMutationResult", "renameTag must return a mutation result")
+require(data_layer, "def.isCustomMaintained = true", "renamed system tags must become custom maintained")
+require(data_layer, "static func setColor(_ color: Int, for tag: String) -> TagEditorMutationResult", "setColor must return a mutation result")
+require(data_layer, "willBeCustomMaintained", "setColor must check whether a system tag becomes custom maintained")
+require(data_layer, "static func appendTags(_ tags: [String], to paths: [String]) -> TagEditorMutationResult", "appendTags must return a mutation result")
+require(data_layer, "static func removeTags(_ tags: [String], from paths: [String]) -> TagEditorMutationResult", "removeTags must return a mutation result")
+require(data_layer, "static func moveApp(path: String, from sourceTag: String, to targetTag: String, color: Int, copy: Bool) -> TagEditorMutationResult", "moveApp must return a mutation result")
+require(data_layer, "TagDatabase.markCustomMaintainedIfNeeded", "membership changes must mark maintained system tags")
+require(data_layer, "static func reorderTags(_ names: [String]) -> Bool", "tag order persistence must return whether it saved")
+require(data_layer, "guard ProEntitlementPolicy.isUnlocked(.persistentAppSorting) else { return false }", "tag order persistence must be Pro-gated")
+
+require(tag_editor, "onCustomContainerQuotaExceeded", "TagEditorView must expose quota-blocked callback")
+require(tag_editor, "onCustomContainerQuotaStatusChanged", "TagEditorView must expose quota status refresh callback")
+require(tag_editor, "onCustomContainerQuotaStatusChanged(ProEntitlementPolicy.customContainerQuotaStatus())", "TagEditorView must refresh quota status after successful mutations")
+require(tag_editor, "onCustomContainerQuotaStatusChanged(status)", "TagEditorView must surface blocked quota status to the header")
+require(tag_editor, "guard handleMutationResult(result) else { return }", "TagEditorView must not update local state before a save succeeds")
+require(tag_editor, "blockedCustomContainerQuota", "TagEditorView must surface quota blocks")
+
+require(content, "persistTagOrderIfAllowed()", "ContentView must route tag order save through a Pro gate")
+require(content, "pro.tagSorting.previewToast", "free tag sorting preview toast is missing")
+require(content, "presentCustomContainerQuotaPrompt", "ContentView must show custom container quota prompt")
+require(content, "customContainerQuotaAccessory", "ContentView tag editor must show quota count")
+require(content, "@State private var customContainerQuotaStatus = ProEntitlementPolicy.customContainerQuotaStatus()", "ContentView quota count must be backed by SwiftUI state")
+require(content, "let status = effectiveCustomContainerQuotaStatus", "ContentView quota accessory must read state-backed effective status")
+require(content, "onCustomContainerQuotaStatusChanged: refreshCustomContainerQuotaStatus", "ContentView must refresh quota count after TagEditorView saves")
+require(content, "refreshCustomContainerQuotaStatus(in: store)", "ContentView must initialize quota count from the edit-mode store")
+require(content, "pro.customContainers.manageExisting", "quota prompt secondary action must manage existing tags")
+require(content, "handleTagMutationResult(result)", "ContentView membership mutations must respect quota blocks")
+
+require(preferences, "presentSettingsCustomContainerQuotaPrompt", "Settings tag page must show quota prompt")
+require(preferences, "customContainerQuotaAccessory", "Settings tag page must show quota count")
+require(preferences, "@State private var settingsCustomContainerQuotaOverviewStatus = ProEntitlementPolicy.customContainerQuotaStatus()", "Settings quota count must be backed by SwiftUI state")
+require(preferences, "let status = effectiveCustomContainerQuotaStatus", "Settings quota accessory must read state-backed effective status")
+require(preferences, "onCustomContainerQuotaStatusChanged: refreshSettingsCustomContainerQuotaStatus", "Settings must refresh quota count after TagEditorView saves")
+require(preferences, "settingsCustomContainerQuotaOverviewStatus = quotaStatus", "Settings scanApps must initialize quota count from the same store")
+require(preferences, 'title: tr("pro.feature.maintainableTagCount")', "Pro comparison must include maintainable tag count row")
+require(preferences, 'title: tr("pro.feature.tagOrderPersistence")', "Pro comparison must include tag order row")
+require(preferences, 'title: tr("pro.feature.containerAppOrderPersistence")', "Pro comparison must include container app order row")
+
+first_three = [
+    preferences.find('title: tr("pro.feature.maintainableTagCount")'),
+    preferences.find('title: tr("pro.feature.tagOrderPersistence")'),
+    preferences.find('title: tr("pro.feature.containerAppOrderPersistence")'),
+]
+custom_color_index = preferences.find('title: tr("pro.feature.customTagColors")')
+if any(index == -1 for index in first_three) or custom_color_index == -1:
+    fail("could not locate Pro comparison feature rows")
+if not (first_three[0] < first_three[1] < first_three[2] < custom_color_index):
+    fail("new Pro comparison rows must be the first three rows")
+
+required_l10n = [
+    "pro.feature.customContainerQuota",
+    "pro.feature.customContainerQuota.benefit",
+    "pro.prompt.customContainerQuota",
+    "pro.customContainers.quotaShort",
+    "pro.customContainers.quotaUnlimitedShort",
+    "pro.customContainers.quotaPrompt",
+    "pro.customContainers.manageExisting",
+    "pro.tagSorting.previewToast",
+    "pro.feature.maintainableTagCount",
+    "pro.feature.tagOrderPersistence",
+    "pro.feature.containerAppOrderPersistence",
+    "settings.proStatus.maxSixTags",
+    "settings.proStatus.unlimitedTags",
+    "settings.proStatus.previewOnly",
+    "settings.proStatus.supported",
+]
+
+files = sorted(localization_dir.glob("*.json"))
+if len(files) != 29:
+    fail(f"expected 29 localization files, found {len(files)}")
+
+english = json.loads((localization_dir / "en.json").read_text(encoding="utf-8"))
+for path in files:
+    data = json.loads(path.read_text(encoding="utf-8"))
+    missing = [key for key in required_l10n if not data.get(key)]
+    if missing:
+        fail(f"{path.name} missing keys: {', '.join(missing)}")
+    if not data.get("settings.proStatus.unlimitedTags", "").startswith("✅"):
+        fail(f"{path.name} unlimited tag status must include the leading checkmark")
+    if path.name != "en.json":
+        copied = [key for key in required_l10n if data.get(key) == english.get(key)]
+        if copied:
+            fail(f"{path.name} copies English quota text: {', '.join(copied)}")
+
+zh_hans = json.loads((localization_dir / "zh-Hans.json").read_text(encoding="utf-8"))
+if zh_hans.get("pro.customContainers.quotaShort") != "免费用户可以使用6个自定义标签(容器):已使用 %used% / %limit%":
+    fail("zh-Hans custom container quota line must use the approved free-user wording")
+if zh_hans.get("settings.proStatus.unlimitedTags") != "✅没有限制":
+    fail("zh-Hans unlimited tag status must include the approved leading checkmark")
+
+print("PASS Pro custom container quota QA: quota gate, tag-order preview, Pro table rows, and 29-language keys are wired")
+PY
diff --git a/src/Scripts/pro_custom_hotkeys_qa.sh b/src/Scripts/pro_custom_hotkeys_qa.sh
index a70de65..23baf5f 100755
--- a/src/Scripts/pro_custom_hotkeys_qa.sh
+++ b/src/Scripts/pro_custom_hotkeys_qa.sh
@@ -222,7 +222,7 @@
 require(hotkey_button, ".buttonStyle(.bordered)", "locked custom hotkey action must visually match Data tab Pro-gated buttons")
 
 require(pro, "case customHotkeys", "ProFeature.customHotkeys is missing")
-require(pro, "case .premiumThemes, .layoutImport, .layoutExport, .customTagColors, .unlimitedNotes, .persistentAppSorting, .customHotkeys:", "customHotkeys must be unlocked only by the Pro access state")
+require(pro, "case .premiumThemes, .layoutImport, .layoutExport, .customTagColors, .customContainerQuota, .unlimitedNotes, .persistentAppSorting, .customHotkeys:", "customHotkeys must be unlocked only by the Pro access state")
 require(pro, "NotificationCenter.default.post(name: .tagLauncherProEntitlementChanged", "entitlement changes must notify hotkey registration")
 require(access_views, "case .customHotkeys:", "Pro prompt routing must include custom hotkeys")
 
diff --git a/src/Scripts/pro_feature_gate_qa.sh b/src/Scripts/pro_feature_gate_qa.sh
index 14186dd..b9d419f 100755
--- a/src/Scripts/pro_feature_gate_qa.sh
+++ b/src/Scripts/pro_feature_gate_qa.sh
@@ -61,6 +61,7 @@
     "layoutImport",
     "layoutExport",
     "customTagColors",
+    "customContainerQuota",
     "unlimitedNotes",
     "persistentAppSorting",
     "customHotkeys",
@@ -85,7 +86,7 @@
 require(pro, "ProEntitlementSnapshotStore", "sync entitlement snapshot for data-layer policy is missing")
 require(pro, "manualNonEmptyNoteCount", "note quota must count existing manual notes")
 require(pro, "manualNonEmptyNotePaths", "note quota must avoid double-counting existing notes")
-require(pro, "case .premiumThemes, .layoutImport, .layoutExport, .customTagColors, .unlimitedNotes, .persistentAppSorting, .customHotkeys:", "custom tag colors and custom hotkeys must be unlocked only by the Pro access state")
+require(pro, "case .premiumThemes, .layoutImport, .layoutExport, .customTagColors, .customContainerQuota, .unlimitedNotes, .persistentAppSorting, .customHotkeys:", "custom tag colors, custom container quota, and custom hotkeys must be unlocked only by the Pro access state")
 require(pro, "NotificationCenter.default.post(name: .tagLauncherProEntitlementChanged", "Pro entitlement changes must notify hotkey registration")
 
 effective_theme = function_body(pro, "static func effectiveTheme(for storedTheme: AppGridTheme) -> AppGridTheme")
@@ -248,15 +249,36 @@
 if 'Text(tr("settings.language"))' in preferences:
     fail("Language tab must not render the removed header/title block")
 require(preferences, "centeredSettingsScrollContent<Content: View>", "settings pages with sparse content must use the shared global centering container")
-require(preferences, "centeredSettingsScrollContent(width: settingsContentWidth)", "language picker matrix must use the shared centered settings container")
+require(preferences, "private let languageContentWidth: CGFloat = 820", "language picker matrix must have its own centered width aligned with fixed settings panels")
+require(preferences, "centeredSettingsScrollContent(width: languageContentWidth)", "language picker matrix must use the centered settings container with the language-specific width")
 if preferences.count("centeredSettingsScrollContent(width: dataPanelWidth)") < 2:
     fail("Pro and About pages must both use the shared centered settings container")
 require(preferences, ".frame(minHeight: proxy.size.height, alignment: .center)", "centered settings content must be vertically centered in the visible area")
-if "topLeadingAccessory: AnyView(" in preferences:
-    fail("Tags page must not render a second Pro guidance row inside the tag editor")
+if "topLeadingAccessory: AnyView(settingsProStatusOverview" in preferences:
+    fail("Tags page must not render a second bulky Pro guidance row inside the tag editor")
 if preferences.count("ScrollView(.vertical, showsIndicators: true)") < 3:
     fail("hotkeys/data/about pages must use vertical scroll containers to avoid clipping")
 require(preferences, "NSEvent.addLocalMonitorForEvents(matching: .keyDown)", "custom hotkey recording must be local to Settings")
+require(preferences, "settingsEscapeMonitor", "Settings must own a local Escape monitor for overlay presentation")
+require(preferences, "installSettingsEscapeMonitor()", "Settings Escape monitor must be installed when Preferences appears")
+require(preferences, "removeSettingsEscapeMonitor()", "Settings Escape monitor must be removed when Preferences disappears")
+settings_escape_body = function_body(preferences, "private func handleSettingsEscapeKey() -> Bool")
+for needle, message in [
+    ("guard !isDataFilePanelPresented, window.attachedSheet == nil else { return false }", "Settings Escape must let file panels/sheets handle Escape first"),
+    ("dismissSettingsProPrompt()", "Settings Escape must close the in-window Pro prompt before closing Settings"),
+    ("showApplySystemSchemeConfirmation = false", "Settings Escape must close apply-confirmation overlays before closing Settings"),
+    ("showResetToUncategorizedConfirmation = false", "Settings Escape must close reset-confirmation overlays before closing Settings"),
+    ("stopHotkeyRecording(showCancelledToast: true)", "Settings Escape must cancel hotkey recording before closing Settings"),
+    ("window.performClose(nil)", "Settings Escape must close the Settings window in the normal overlay case"),
+]:
+    require(settings_escape_body, needle, message)
+settings_escape_filter_body = function_body(preferences, "private func isSettingsOverlayEscapeEvent(_ event: NSEvent) -> Bool")
+for needle, message in [
+    ("let parent = window.parent as? OverlayPanel", "Settings Escape must be limited to Settings shown over the AppGrid overlay"),
+    ("parent.isVisible", "Settings Escape must require the overlay parent to be visible"),
+    ("eventWindow == window || eventWindow == parent", "Settings Escape must handle both Settings-key and overlay-key event routing"),
+]:
+    require(settings_escape_filter_body, needle, message)
 require(preferences, "proEntitlement.startThemePreview(for: theme, tuning:", "premium theme free preview is missing")
 require(preferences, "proEntitlement.stopThemePreview()", "theme preview cleanup is missing")
 require(preferences, "proEntitlement.startDisplayModePreview(for: mode)", "colored display mode free preview is missing")
@@ -287,7 +309,8 @@
 if "localizedCaseInsensitiveContains" in access_views:
     fail("ProStatusPill must not hide the crown based on localized text content")
 require(content, "onUnlock: { proEntitlement.purchasePro() }", "AppGrid Pro prompt is not wired to purchase action")
-require(content, "onRestore: { proEntitlement.restorePurchases() }", "AppGrid Pro prompt is not wired to restore action")
+require(content, "onRestore: { handleProPromptSecondaryAction(prompt) }", "AppGrid Pro prompt is not wired to secondary action handler")
+require(content, "proEntitlement.restorePurchases()", "AppGrid Pro prompt secondary handler must still support restore purchase")
 
 tag_editor = read("TagEditorView.swift")
 require(tag_editor, "ColorPicker(", "Pro custom tag colors must use a native macOS color picker path")
@@ -303,5 +326,5 @@
 require(tag_nav, "let customColor: TagCustomColor?", "Tag navigation items must carry custom colors")
 require(tag_nav, "TagColor.nsColor(for: item.colorIndex, customColor: item.customColor)", "Tag navigation must render custom colors")
 
-print("PASS Pro feature gate QA: StoreKit entitlement, legacy unlock, theme gating, import/export locks, 3-note quota, custom tag colors, custom hotkeys, sorting preview, and Pro UI prompts are wired")
+print("PASS Pro feature gate QA: StoreKit entitlement, legacy unlock, theme gating, import/export locks, 3-note quota, custom tag colors, custom container quota, custom hotkeys, sorting preview, and Pro UI prompts are wired")
 PY
diff --git a/src/Scripts/pro_localization_qa.sh b/src/Scripts/pro_localization_qa.sh
index b33b450..b1a953c 100755
--- a/src/Scripts/pro_localization_qa.sh
+++ b/src/Scripts/pro_localization_qa.sh
@@ -22,6 +22,8 @@
 REQUIRED_PLACEHOLDERS = {
     "pro.notes.remaining": {"%limit%", "%remaining%"},
     "pro.theme.status.previewCountdown": {"%time%"},
+    "pro.customContainers.quotaShort": {"%used%", "%limit%"},
+    "pro.customContainers.quotaPrompt": {"%used%", "%limit%", "%remaining%"},
 }
 REQUIRED_NON_PRO_KEYS = [
     "settings.proStatus.freeUser",
@@ -36,6 +38,10 @@
     "settings.proStatus.unsupportedFeature",
     "settings.proStatus.unavailableFeature",
     "settings.proStatus.previewFiveMinutes",
+    "settings.proStatus.maxSixTags",
+    "settings.proStatus.unlimitedTags",
+    "settings.proStatus.previewOnly",
+    "settings.proStatus.supported",
     "settings.proFeature.appNotes",
     "settings.proFeature.dataBackup",
     "settings.proBenefit.general",
@@ -122,6 +128,12 @@
     fail("en.json Pro comparison unlimited notes status must include the leading checkmark")
 if english.get("settings.proStatus.unlockedFeature") != "✅ Unlocked":
     fail("en.json Pro comparison unlocked status must be '✅ Unlocked'")
+if english.get("settings.proStatus.supported") != "✅ Supported":
+    fail("en.json Pro comparison supported status must be '✅ Supported'")
+if english.get("settings.proStatus.unlimitedTags") != "✅ No limit":
+    fail("en.json Pro comparison unlimited tag status must be '✅ No limit'")
+if english.get("pro.customContainers.quotaShort") != "Free users can use 6 custom tags (containers): %used% / %limit% used":
+    fail("en.json custom container quota line must use the approved free-user wording")
 if english.get("settings.proStatus.proUser") != "You are currently a valued Pro user":
     fail("en.json settings.proStatus.proUser must not duplicate the crown icon in text")
 
@@ -132,6 +144,12 @@
     fail("zh-Hans.json Pro comparison unlimited notes status must be '✅无限备注'")
 if zh_hans.get("settings.proStatus.unlockedFeature") != "✅已解锁":
     fail("zh-Hans.json Pro comparison unlocked status must be '✅已解锁'")
+if zh_hans.get("settings.proStatus.supported") != "✅支持":
+    fail("zh-Hans.json Pro comparison supported status must be '✅支持'")
+if zh_hans.get("settings.proStatus.unlimitedTags") != "✅没有限制":
+    fail("zh-Hans.json Pro comparison unlimited tag status must be '✅没有限制'")
+if zh_hans.get("pro.customContainers.quotaShort") != "免费用户可以使用6个自定义标签(容器):已使用 %used% / %limit%":
+    fail("zh-Hans.json custom container quota line must use the approved free-user wording")
 if zh_hans.get("settings.proStatus.proUser") != "您当前是尊贵的 Pro 用户":
     fail("zh-Hans.json settings.proStatus.proUser must be the approved centered Pro identity sentence")
 
@@ -203,6 +221,8 @@
 
     if "👑" in data.get("settings.proStatus.proUser", ""):
         fail(f"{code}.json settings.proStatus.proUser must not duplicate the crown icon")
+    if not data.get("settings.proStatus.unlimitedTags", "").startswith("✅"):
+        fail(f"{code}.json settings.proStatus.unlimitedTags must include the leading checkmark")
 
 print(
     "PASS Pro localization QA: "
diff --git a/src/Scripts/settings_language_title_qa.sh b/src/Scripts/settings_language_title_qa.sh
new file mode 100755
index 0000000..e3946e5
--- /dev/null
+++ b/src/Scripts/settings_language_title_qa.sh
@@ -0,0 +1,87 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+python3 - "$ROOT_DIR" <<'PY'
+import re
+import sys
+from pathlib import Path
+
+root = Path(sys.argv[1])
+app_dir = root / "Apptag"
+preferences = (app_dir / "PreferencesView.swift").read_text(encoding="utf-8")
+app = (app_dir / "ApptagApp.swift").read_text(encoding="utf-8")
+
+
+def fail(message: str) -> None:
+    raise SystemExit(f"FAIL: {message}")
+
+
+def require(source: str, needle: str, message: str) -> None:
+    if needle not in source:
+        fail(message)
+
+
+require(
+    app,
+    'window.title = tr("menu.preferences").replacingOccurrences(of: "…", with: "")',
+    "Settings window must use the localized preferences title at creation",
+)
+require(
+    preferences,
+    "private var localizedPreferencesWindowTitle: String",
+    "PreferencesView must have a single localized title source",
+)
+require(
+    preferences,
+    'tr("menu.preferences").replacingOccurrences(of: "…", with: "")',
+    "PreferencesView localized title must use menu.preferences and strip ellipsis",
+)
+require(
+    preferences,
+    "private func refreshPreferencesWindowTitle()",
+    "PreferencesView must expose a window-title refresh helper",
+)
+require(
+    preferences,
+    "preferencesWindow?.title = localizedPreferencesWindowTitle",
+    "PreferencesView must write the localized title back to NSWindow.title",
+)
+require(
+    preferences,
+    "return window.title == localizedPreferencesWindowTitle",
+    "Preferences window fallback detection must reuse the localized title source",
+)
+
+language_marker = ".onReceive(NotificationCenter.default.publisher(for: .appLanguageDidChange))"
+language_start = preferences.find(language_marker)
+if language_start == -1:
+    fail("PreferencesView must observe appLanguageDidChange")
+language_end = preferences.find(".onReceive(NotificationCenter.default.publisher(for: .tagLauncherDataDidChange))", language_start)
+if language_end == -1:
+    fail("PreferencesView appLanguageDidChange handler must be followed by tagLauncherDataDidChange handler")
+language_block = preferences[language_start:language_end]
+if "refreshPreferencesWindowTitle()" not in language_block:
+    fail("appLanguageDidChange handler must refresh NSWindow.title")
+
+root_frame_marker = ".frame(width: settingsWindowWidth, height: settingsWindowHeight)"
+root_frame_start = preferences.find(root_frame_marker)
+if root_frame_start == -1:
+    fail("PreferencesView root frame marker is missing")
+appear_marker = ".onAppear {"
+appear_start = preferences.find(appear_marker, root_frame_start)
+if appear_start == -1:
+    fail("PreferencesView must have an onAppear handler")
+appear_end = preferences.find(".onDisappear", appear_start)
+if appear_end == -1:
+    fail("PreferencesView root onAppear must be followed by onDisappear")
+appear_block = preferences[appear_start:appear_end]
+if "refreshPreferencesWindowTitle()" not in appear_block:
+    fail("PreferencesView.onAppear must refresh NSWindow.title for restored windows")
+
+if "window.title == tr(\"menu.preferences\")" in preferences:
+    fail("PreferencesView must not duplicate raw localized title comparisons")
+
+print("PASS Settings language title QA: preferences window title refreshes on appear and language changes")
+PY
diff --git a/src/Scripts/tag_editor_mouse_event_qa.sh b/src/Scripts/tag_editor_mouse_event_qa.sh
new file mode 100755
index 0000000..6f9a4bf
--- /dev/null
+++ b/src/Scripts/tag_editor_mouse_event_qa.sh
@@ -0,0 +1,81 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+python3 - "$ROOT_DIR" <<'PY'
+import re
+import sys
+from pathlib import Path
+
+root = Path(sys.argv[1])
+app_dir = root / "Apptag"
+
+
+def fail(message: str) -> None:
+    raise SystemExit(f"FAIL: {message}")
+
+
+content = (app_dir / "ContentView.swift").read_text(encoding="utf-8")
+app = (app_dir / "ApptagApp.swift").read_text(encoding="utf-8")
+
+if "func focusTextField()" not in content:
+    fail("TextFieldContainer must expose a focus-only path")
+if "private var isEditing: Bool" not in content:
+    fail("TextFieldContainer must track whether the text field is already editing")
+if "return textField.hitTest(fieldPoint) ?? textField" not in content:
+    fail("TextFieldContainer must let active editors handle native caret placement")
+
+container_match = re.search(
+    r"final\s+class\s+TextFieldContainer:\s+NSView\s*\{(?P<body>.*?)\n\}",
+    content,
+    re.S,
+)
+if not container_match:
+    fail("TextFieldContainer class is missing")
+
+container_body = container_match.group("body")
+mouse_down_match = re.search(
+    r"override\s+func\s+mouseDown\s*\([^)]*\)\s*\{(?P<body>.*?)\n\s*\}",
+    container_body,
+    re.S,
+)
+if not mouse_down_match:
+    fail("TextFieldContainer.mouseDown implementation is missing")
+
+mouse_down_body = mouse_down_match.group("body")
+if "textField.mouseDown(with: event)" in mouse_down_body:
+    fail("TextFieldContainer.mouseDown must not forward the same event to NSTextField.mouseDown")
+if "focusTextField()" not in mouse_down_body:
+    fail("TextFieldContainer.mouseDown must focus the text field")
+
+if "container.mouseDown(with: event)" in app:
+    fail("Overlay hosting view must not manually forward mouseDown to TextFieldContainer")
+if "tf.mouseDown(with: event)" in app:
+    fail("Overlay hosting view must not manually forward mouseDown to NSTextField")
+if "container.focusTextField()" not in app:
+    fail("Overlay hosting view must use the focus-only text field path")
+if "let shouldSelectAll = tf.currentEditor() == nil" not in app:
+    fail("Overlay hosting view must not reselect all text while an editor is active")
+
+for path in app_dir.glob("*.swift"):
+    source = path.read_text(encoding="utf-8")
+    forbidden = [
+        ".mouseDown(with: event)",
+        ".mouseDown(with:event)",
+    ]
+    for needle in forbidden:
+        for match in re.finditer(re.escape(needle), source):
+            line_start = source.rfind("\n", 0, match.start()) + 1
+            line_end = source.find("\n", match.start())
+            if line_end == -1:
+                line_end = len(source)
+            line = source[line_start:line_end].strip()
+            if line.startswith("super."):
+                continue
+            if "floatingButton.mouseDown(with: event)" in line:
+                continue
+            fail(f"{path.name} must not manually forward the same mouseDown event: {line}")
+
+print("PASS Tag editor mouse event QA: text-field clicks focus without recursive mouseDown forwarding")
+PY

--
Gitblit v1.9.3