-- HDG_StylesEditor.lua
-- Style Creator / Editor UI: tag chips, facet tabs, preview grid, basket, save.
-- Extracted from HDG_StylesTab.lua to reduce file size.

local addonName, privateDB = ...
local HDG = _G.HousingDecorGuide
local StylesTab = privateDB.HDG_StylesTab
local function GetScheme() return HDG.Theme.GetCurrentScheme() end
local function RegisterWidget(w, t) HDG.Theme.RegisterWidget(w, t) end
local function ApplyFont(fs, s, ft) HDG.Theme.ApplyFont(fs, s, ft) end
local L = HDG_L or {}

-- Lazy-loaded references (set after Init runs)
local CONFIG, StyleEngine
local function EnsureRefs()
    if not CONFIG then
        CONFIG = StylesTab._config
        StyleEngine = StylesTab._styleEngine
    end
end

-- Editor-owned frame
local editorFrame

-- ============================================================================
-- STYLE EDITOR VIEW
-- ============================================================================

-- Editor state
local workingDef = nil
local editingKey = nil  -- nil = creating new, string = editing existing
local editorFacetTab = "room"
local editorTagStates = {} -- [facetKey][tagValue] = "off"/"query"/"boost"/"anti"
local editorPreviewTimer = nil
local editorTagButtons = {} -- pooled tag buttons
local editorFacetButtons = {} -- facet tab buttons
local editorPreviewPool = { all = {}, active = {}, free = {}, timers = {} }
local editorLockedItems = {} -- [itemID] = true, force-pinned items in preview
local editorBasketCount = 0  -- tracked integer, avoids pairs() iteration for count
local editorRemovedItems = {} -- [itemID] = true, hidden items in preview
local editorLastRemoved = nil -- { itemID = N, name = "..." } for undo
local editorAllItems = {} -- all scored items for virtual scroll
local editorTierFilter = "first" -- default tier filter: "all"/"first"/"second"/"neutral"/"clash"
local editorTierButtons = {} -- tier filter chip buttons
local editorScrollDirty = false -- flag for throttled scroll render
local editorMode = "curated" -- "smart" or "curated"
local editorSearchText = "" -- text search filter (curated mode only)
local basketRowPool = nil -- row pool for basket items
local usedInOtherStyles = {} -- [itemID] = count of other custom styles containing this item

-- Arrow-scroll container factory (vertical adaptation of VDS horizontal pattern)
local function CreateVerticalArrowScroll(parent, width, height)
    local SE = CONFIG.STYLE_EDITOR
    local arrowSize = SE.ARROW_BTN_SIZE
    local scrollStep = SE.ARROW_SCROLL_STEP

    local container = CreateFrame("Frame", nil, parent)
    container:SetSize(width, height)

    -- Top scroll arrow
    local topBtn = CreateFrame("Button", nil, container)
    topBtn:SetSize(width, arrowSize)
    topBtn:SetPoint("TOP", 0, 0)
    local topIcon = topBtn:CreateTexture(nil, "ARTWORK")
    topIcon:SetSize(arrowSize, arrowSize)
    topIcon:SetPoint("CENTER")
    topIcon:SetAtlas("common-icon-uparrow")
    topIcon:SetAlpha(0.7)
    topBtn:SetScript("OnEnter", function() topIcon:SetAlpha(1) end)
    topBtn:SetScript("OnLeave", function() topIcon:SetAlpha(0.7) end)
    topBtn:Hide()

    -- Bottom scroll arrow
    local bottomBtn = CreateFrame("Button", nil, container)
    bottomBtn:SetSize(width, arrowSize)
    bottomBtn:SetPoint("BOTTOM", 0, 0)
    local bottomIcon = bottomBtn:CreateTexture(nil, "ARTWORK")
    bottomIcon:SetSize(arrowSize, arrowSize)
    bottomIcon:SetPoint("CENTER")
    bottomIcon:SetAtlas("common-icon-downarrow")
    bottomIcon:SetAlpha(0.7)
    bottomBtn:SetScript("OnEnter", function() bottomIcon:SetAlpha(1) end)
    bottomBtn:SetScript("OnLeave", function() bottomIcon:SetAlpha(0.7) end)
    bottomBtn:Hide()

    -- Clip frame (between arrows, clips overflow)
    local clipFrame = CreateFrame("Frame", nil, container)
    clipFrame:SetPoint("TOPLEFT", 0, 0)
    clipFrame:SetPoint("BOTTOMRIGHT", 0, 0)
    clipFrame:SetClipsChildren(true)

    -- Content frame (all children go here, may be taller than clip)
    local contentFrame = CreateFrame("Frame", nil, clipFrame)
    contentFrame:SetPoint("TOPLEFT", 0, 0)
    contentFrame:SetWidth(width)
    contentFrame:SetHeight(1)

    -- Scroll state
    local scrollOffset = 0
    local contentHeight = 0

    local function UpdateScroll(newOffset)
        local clipH = clipFrame:GetHeight()
        if clipH < 10 then clipH = height end
        local maxScroll = math.max(0, contentHeight - clipH)
        scrollOffset = math.max(0, math.min(maxScroll, newOffset))
        contentFrame:ClearAllPoints()
        contentFrame:SetPoint("TOPLEFT", 0, scrollOffset)
        contentFrame:SetWidth(width)
        -- Show/hide arrows
        local showTop = scrollOffset > 0
        local showBottom = scrollOffset < maxScroll - 1
        topBtn:SetShown(showTop)
        bottomBtn:SetShown(showBottom)
        -- Adjust clip frame margins for visible arrows
        clipFrame:ClearAllPoints()
        clipFrame:SetPoint("TOPLEFT", 0, showTop and -arrowSize or 0)
        clipFrame:SetPoint("BOTTOMRIGHT", 0, showBottom and arrowSize or 0)
    end

    local function SetContentHeight(h)
        contentHeight = h
        contentFrame:SetHeight(h)
        UpdateScroll(scrollOffset)
    end

    topBtn:SetScript("OnClick", function() UpdateScroll(scrollOffset - scrollStep) end)
    bottomBtn:SetScript("OnClick", function() UpdateScroll(scrollOffset + scrollStep) end)

    container:EnableMouseWheel(true)
    container:SetScript("OnMouseWheel", function(_, delta)
        UpdateScroll(scrollOffset - delta * scrollStep)
    end)

    return {
        frame = container,
        content = contentFrame,
        clip = clipFrame,
        UpdateScroll = UpdateScroll,
        SetContentHeight = SetContentHeight,
        ResetScroll = function() scrollOffset = 0; UpdateScroll(0) end,
    }
end

-- Convert "rough-wood" -> "Rough Wood"
-- Sort groups for freeform Blizzard tags (lower = earlier)
local FREEFORM_SORT = {
    -- 0: Other (always first)
    ["Other"] = 0,
    -- 1: Expansions (ordered chronologically)
    ["Classic"] = 100, ["Burning Crusade"] = 101,
    ["Wrath of the Lich King"] = 102, ["Cataclysm"] = 103,
    ["Mists of Pandaria"] = 104, ["Warlords of Draenor"] = 105,
    ["Legion"] = 106, ["Battle for Azeroth"] = 107,
    ["Shadowlands"] = 108, ["Dragonflight"] = 109,
    ["The War Within"] = 110, ["Midnight"] = 111,
    -- 2: Faction
    ["Alliance"] = 200, ["Horde"] = 201, ["Stormwind"] = 202,
    -- 3: Races (alphabetical within group)
    ["Blood Elf"] = 300, ["Bronzebeard Dwarf (Ironforge)"] = 301,
    ["Dark Iron Dwarf"] = 302, ["Dracthyr"] = 303, ["Draenei"] = 304,
    ["Dwarven"] = 305, ["Earthen (Dornish)"] = 306, ["Elven"] = 307,
    ["Fae"] = 308, ["Gilnean / Worgen"] = 309, ["Gnomish"] = 310,
    ["Goblin"] = 311, ["Haranir"] = 312, ["Human"] = 313,
    ["Kul Tiran"] = 314, ["Night Elf"] = 315, ["Nightborne"] = 316,
    ["Orcish"] = 317, ["Pandaren"] = 318, ["Pirate"] = 319,
    ["Tauren"] = 320, ["Troll"] = 321, ["Undead"] = 322,
    ["Void Elf"] = 323, ["Vrykul"] = 324, ["Vulpera"] = 325,
    ["Wildhammer Dwarf"] = 326, ["Zandalari Troll"] = 327,
    -- 4: Size (small to large)
    ["Tiny"] = 400, ["Small"] = 401, ["Medium"] = 402,
    ["Large"] = 403, ["Huge"] = 404,
    -- 5+: Style/mood/furniture/seasonal -> alphabetical (no entry = 500)
}

-- Display overrides for freeform Blizzard tags
local FREEFORM_PRETTY = {
    -- Expansions -> short codes (from HDG_ExpansionData.short)
    ["Classic"] = "CLS", ["Burning Crusade"] = "TBC",
    ["Wrath of the Lich King"] = "WLK", ["Cataclysm"] = "CAT",
    ["Mists of Pandaria"] = "MOP", ["Warlords of Draenor"] = "WOD",
    ["Legion"] = "LEG", ["Battle for Azeroth"] = "BFA",
    ["Shadowlands"] = "SL", ["Dragonflight"] = "DF",
    ["The War Within"] = "TWW", ["Midnight"] = "MN",
    -- Races where first word alone is ambiguous
    ["Blood Elf"] = "Blood Elf", ["Night Elf"] = "Night Elf",
    ["Void Elf"] = "Void Elf", ["Kul Tiran"] = "Kul Tiran",
    ["Dark Iron Dwarf"] = "Dark Iron", ["Hallow's End"] = "Hallow's End",
}

local function PrettyTagName(tag)
    if FREEFORM_PRETTY[tag] then return FREEFORM_PRETTY[tag] end
    -- Multi-word tags -> first word, title-cased
    if tag:find(" ") then
        local first = tag:match("^(%S+)")
        return first:sub(1,1):upper() .. first:sub(2)
    end
    return tag:gsub("-", " "):gsub("(%a)([%w_']*)", function(a, b) return a:upper() .. b end)
end

local descManuallyEdited = false

-- Build auto-description from "query" tags (Must Have) across all facets
local function BuildAutoDescription()
    local parts = {}
    for facetKey, tags in pairs(editorTagStates) do
        for tagVal, state in pairs(tags) do
            if state == "query" then
                parts[#parts + 1] = PrettyTagName(tagVal)
            end
        end
    end
    table.sort(parts)
    return table.concat(parts, ", ")
end

-- Rebuild workingDef.query/boost/anti from editorTagStates
local function RebuildWorkingDefFromTags()
    if not workingDef then return end
    workingDef.query = {}
    workingDef.boost = {}
    workingDef.anti = {}

    for facetKey, tags in pairs(editorTagStates) do
        for tagVal, state in pairs(tags) do
            if state == "query" then
                if not workingDef.query[facetKey] then workingDef.query[facetKey] = {} end
                table.insert(workingDef.query[facetKey], tagVal)
            elseif state == "boost" then
                if not workingDef.boost[facetKey] then workingDef.boost[facetKey] = {} end
                table.insert(workingDef.boost[facetKey], tagVal)
            elseif state == "anti" then
                if not workingDef.anti[facetKey] then workingDef.anti[facetKey] = {} end
                table.insert(workingDef.anti[facetKey], tagVal)
            end
        end
    end

    -- Clean up empty tables
    if not next(workingDef.query) then workingDef.query = nil end
    if not next(workingDef.boost) then workingDef.boost = nil end
    if not next(workingDef.anti) then workingDef.anti = nil end
end

-- Build tag states from a style definition
local function BuildTagStatesFromDef(def)
    wipe(editorTagStates)
    if not def then return end

    local function AddTags(facetMap, state)
        if not facetMap then return end
        for facetKey, vals in pairs(facetMap) do
            if not editorTagStates[facetKey] then editorTagStates[facetKey] = {} end
            for _, v in ipairs(vals) do
                editorTagStates[facetKey][v] = state
            end
        end
    end

    AddTags(def.query, "query")
    AddTags(def.boost, "boost")
    AddTags(def.anti, "anti")
end

function StylesTab:ShowEditor(styleKey, prefillDef)
    EnsureRefs()
    if not StylesTab._panel then StylesTab:Create() end
    if not editorFrame then self:CreateEditor() end

    StylesTab._landingFrame:Hide()
    StylesTab._detailFrame:Hide()
    editorFrame:Show()
    StylesTab._panel:Show()

    editingKey = styleKey

    -- Build "used in other styles" map (excludes the style being edited)
    -- Build "used in other styles" — itemID → { styleName, ... } (curated only)
    wipe(usedInOtherStyles)
    if HDG.Store then
        local customStyles, customOrder = HDG.Store:GetCustomStyles()
        if customOrder then
            for _, sid in ipairs(customOrder) do
                if sid ~= styleKey then
                    local def = customStyles[sid]
                    if def and (def.tier == "curated") and def.curatedItems then
                        local styleName = def.displayName or sid
                        for itemID in pairs(def.curatedItems) do
                            if not usedInOtherStyles[itemID] then usedInOtherStyles[itemID] = {} end
                            usedInOtherStyles[itemID][#usedInOtherStyles[itemID] + 1] = styleName
                        end
                    end
                end
            end
        end
    end

    wipe(editorLockedItems)
    editorBasketCount = 0
    wipe(editorRemovedItems)
    editorTierFilter = "first"
    for _, b in pairs(editorTierButtons) do
        HDG_UIHelpers.UpdateChipButtonState(b, b.tierKey == editorTierFilter)
    end

    if styleKey and HDG.Store then
        -- Editing existing custom style
        local customs = HDG.Store:GetState().customStyles
        local def = customs and customs[styleKey]
        if def then
            workingDef = StylesTab.DeepCopyTable(def)
        else
            workingDef = self:NewBlankDef()
        end
        descManuallyEdited = true -- preserve existing description
    elseif prefillDef then
        -- Duplicating (prefilled)
        workingDef = StylesTab.DeepCopyTable(prefillDef)
        descManuallyEdited = true
    else
        -- New blank
        workingDef = self:NewBlankDef()
        descManuallyEdited = false
    end

    -- Detect mode from def tier (new styles default to curated)
    if workingDef.tier == "curated" or (not styleKey and not prefillDef) then
        editorMode = "curated"
        -- Restore curated items into basket
        if workingDef.curatedItems then
            for id in pairs(workingDef.curatedItems) do editorLockedItems[id] = true; editorBasketCount = editorBasketCount + 1 end
        end
        wipe(editorTagStates)
    elseif workingDef.tier == "custom" then
        editorMode = "smart"
        -- Restore lock/remove state from saved def
        if workingDef.lockedItems then
            for id in pairs(workingDef.lockedItems) do editorLockedItems[id] = true; editorBasketCount = editorBasketCount + 1 end
        end
        if workingDef.removedItems then
            for id in pairs(workingDef.removedItems) do editorRemovedItems[id] = true end
        end
        BuildTagStatesFromDef(workingDef)
    else
        editorMode = "smart"
        BuildTagStatesFromDef(workingDef)
    end

    -- Reset search text
    editorSearchText = ""
    if editorFrame.curatedSearch then editorFrame.curatedSearch:SetText("") end

    -- Update mode toggle buttons
    if editorFrame.modeSmartBtn then
        HDG_UIHelpers.UpdateChipButtonState(editorFrame.modeSmartBtn, editorMode == "smart")
        HDG_UIHelpers.UpdateChipButtonState(editorFrame.modeCuratedBtn, editorMode == "curated")
    end

    -- Show/hide tier strip vs curated bar
    self:OnModeToggle()

    -- Warm collection cache before populating (async batched to avoid UI freeze)
    if HDG_Data and not HDG_Data.IsCollectionCacheWarmed() then
        -- Show progress text
        if editorFrame.progressText then
            editorFrame.progressText:Show()
            editorFrame.progressText:SetText("Querying collection...")
        end
        HDG_Data.WarmCollectionCache(
            function(current, total)  -- onProgress
                if editorFrame.progressText then
                    editorFrame.progressText:SetText(string.format("Querying collection... (%d/%d)", current, total))
                end
            end,
            function()  -- onComplete
                if editorFrame.progressText then editorFrame.progressText:Hide() end
                StylesTab:RefreshEditor()
            end
        )
    else
        self:RefreshEditor()
    end
end

function StylesTab:NewBlankDef()
    -- Default name: CharName Style N (or CharName Style (Curated) N)
    local charName = UnitName("player") or "My"
    local counter = 1
    local customs = HDG.Store:GetState().customStyles
    if customs then
        for _ in pairs(customs) do counter = counter + 1 end
    end
    local suffix = editorMode == "curated" and " Style (Curated) " or " Style "
    return {
        displayName = charName .. suffix .. counter,
        icon = "Interface\\Icons\\INV_Misc_Book_09",
        tier = "custom",
        color = { r = 0.5, g = 0.5, b = 0.5 },
        description = "",
        query = nil,
        boost = nil,
        anti = nil,
    }
end

-- Shallow-copy a set table ({[id]=true,...}) to avoid shared-reference corruption
local function CopySetTable(src)
    if not next(src) then return nil end
    local copy = {}
    for id in pairs(src) do copy[id] = true end
    return copy
end

-- Auto-save editor state after lock/unlock/remove actions
local function EditorAutoSave()
    if not workingDef or not HDG.Store then return end
    -- Persist basket into workingDef (curated vs smart) — COPY to avoid shared refs
    if editorMode == "curated" then
        workingDef.curatedItems = CopySetTable(editorLockedItems)
    else
        workingDef.lockedItems = CopySetTable(editorLockedItems)
        workingDef.removedItems = CopySetTable(editorRemovedItems)
    end
    -- Ensure style has a name
    if not workingDef.displayName or workingDef.displayName == "" then
        workingDef.displayName = "Untitled Style"
        local customs = HDG.Store:GetState().customStyles
        if customs then
            local suffix = 2
            for _, def in pairs(customs) do
                if def.displayName and def.displayName:find("^Untitled Style") then
                    suffix = suffix + 1
                end
            end
            if suffix > 2 then workingDef.displayName = "Untitled Style " .. suffix end
        end
        if editorFrame and editorFrame.nameBox then
            editorFrame.nameBox:SetText(workingDef.displayName)
        end
    end
    local key = HDG.Store:SaveCustomStyle(editingKey, workingDef)
    if not editingKey then editingKey = key end -- remember key for future saves
    if StyleEngine then StyleEngine:InvalidateCustomStyle(key) end
end

function StylesTab:CreateEditor()
    EnsureRefs()
    local scheme = GetScheme()
    local SE = CONFIG.STYLE_EDITOR

    editorFrame = CreateFrame("Frame", nil, StylesTab._panel)
    editorFrame:SetAllPoints()
    editorFrame:Hide()
    StylesTab._editorFrame = editorFrame

    -- Progress text for collection cache warm-up
    local progressText = editorFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    progressText:SetPoint("CENTER")
    progressText:SetTextColor(0.8, 0.8, 0.8, 1)
    progressText:Hide()
    editorFrame.progressText = progressText

    -- ================================================================
    -- TOP BAR: Back | Title | Name | Desc (single horizontal row)
    -- ================================================================
    local topBar = CreateFrame("Frame", nil, editorFrame)
    topBar:SetHeight(SE.TOP_BAR_HEIGHT)
    topBar:SetPoint("TOPLEFT", 6, -4)
    topBar:SetPoint("TOPRIGHT", -6, -4)

    local backBtn = CreateFrame("Button", nil, topBar, "UIPanelButtonTemplate")
    backBtn:SetSize(60, 20)
    backBtn:SetPoint("LEFT", 0, 0)
    backBtn:SetText(L["STYLE_BACK"])
    backBtn:SetScript("OnClick", function() StylesTab:ShowLanding() end)

    local edTitle = topBar:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    edTitle:SetPoint("LEFT", backBtn, "RIGHT", 8, 0)
    if scheme then ApplyFont(edTitle, scheme, "header") end
    edTitle:SetText(L["STYLE_CREATOR_TITLE"])

    local nameLbl = topBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    nameLbl:SetPoint("LEFT", edTitle, "RIGHT", 14, 0)
    nameLbl:SetText(L["STYLE_NAME_LABEL"])
    if scheme then ApplyFont(nameLbl, scheme, "small") end

    local nameBox = CreateFrame("EditBox", nil, topBar, "InputBoxTemplate")
    nameBox:SetSize(140, 20)
    nameBox:SetPoint("LEFT", nameLbl, "RIGHT", 4, 0)
    nameBox:SetAutoFocus(false)
    nameBox:SetMaxLetters(SE.NAME_MAX_LENGTH)
    nameBox:SetScript("OnTextChanged", function(self)
        if workingDef then workingDef.displayName = self:GetText() end
    end)
    nameBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    editorFrame.nameBox = nameBox

    local descLbl = topBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    descLbl:SetPoint("LEFT", nameBox, "RIGHT", 10, 0)
    descLbl:SetText(L["STYLE_DESC_LABEL"])
    if scheme then ApplyFont(descLbl, scheme, "small") end

    local descBox = CreateFrame("EditBox", nil, topBar, "InputBoxTemplate")
    descBox:SetSize(200, 20)
    descBox:SetPoint("LEFT", descLbl, "RIGHT", 4, 0)
    descBox:SetAutoFocus(false)
    descBox:SetMaxLetters(SE.DESC_MAX_LENGTH)
    descBox:SetScript("OnTextChanged", function(self, userInput)
        if workingDef then workingDef.description = self:GetText() end
        if userInput then descManuallyEdited = true end
    end)
    descBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    editorFrame.descBox = descBox

    -- Mode toggle: Smart / Curated (two-button radio group)
    local function SetModeBtn(mode)
        editorMode = mode
        HDG_UIHelpers.UpdateChipButtonState(editorFrame.modeSmartBtn, mode == "smart")
        HDG_UIHelpers.UpdateChipButtonState(editorFrame.modeCuratedBtn, mode == "curated")
        StylesTab:OnModeToggle()
    end
    local modeSmartBtn = HDG_UIHelpers.CreateChipButton(topBar, L["STYLE_MODE_SMART"] or "Smart", 55, true, nil)
    modeSmartBtn:SetPoint("LEFT", descBox, "RIGHT", 12, 0)
    if scheme then ApplyFont(modeSmartBtn.text, scheme, "xsmall") end
    modeSmartBtn:SetScript("OnClick", function() SetModeBtn("smart") end)
    editorFrame.modeSmartBtn = modeSmartBtn

    local modeCuratedBtn = HDG_UIHelpers.CreateChipButton(topBar, L["STYLE_MODE_CURATED"] or "Curated", 65, false, nil)
    modeCuratedBtn:SetPoint("LEFT", modeSmartBtn, "RIGHT", 2, 0)
    if scheme then ApplyFont(modeCuratedBtn.text, scheme, "xsmall") end
    modeCuratedBtn:SetScript("OnClick", function() SetModeBtn("curated") end)
    editorFrame.modeCuratedBtn = modeCuratedBtn

    -- ================================================================
    -- INFO BAR: 2-line hover info + basket summary
    -- ================================================================
    local infoBar = CreateFrame("Frame", nil, editorFrame, "BackdropTemplate")
    infoBar:SetHeight(SE.INFO_BAR_HEIGHT)
    infoBar:SetPoint("TOPLEFT", topBar, "BOTTOMLEFT", 0, -2)
    infoBar:SetPoint("TOPRIGHT", topBar, "BOTTOMRIGHT", 0, -2)
    HDG_UIHelpers.ApplyBackdrop(infoBar, "panel",
        {0.05, 0.05, 0.05, 0.6}, {0.12, 0.12, 0.12, 0.3})
    RegisterWidget(infoBar, "Panel")

    local infoLine1Left = infoBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    infoLine1Left:SetPoint("TOPLEFT", 8, -4)
    infoLine1Left:SetJustifyH("LEFT")
    if scheme then ApplyFont(infoLine1Left, scheme, "small") end

    local infoLine1Right = infoBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    infoLine1Right:SetPoint("TOPRIGHT", -8, -4)
    infoLine1Right:SetJustifyH("RIGHT")
    if scheme then ApplyFont(infoLine1Right, scheme, "small") end

    local infoLine2 = infoBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    infoLine2:SetPoint("BOTTOMLEFT", 8, 4)
    infoLine2:SetPoint("BOTTOMRIGHT", -8, 4)
    infoLine2:SetJustifyH("LEFT")
    infoLine2:SetWordWrap(false)
    if scheme then ApplyFont(infoLine2, scheme, "small") end

    editorFrame.infoBar = infoBar
    editorFrame.infoLine1Left = infoLine1Left
    editorFrame.infoLine1Right = infoLine1Right
    editorFrame.infoLine2 = infoLine2

    -- ================================================================
    -- BOTTOM BAR: Clear | Count | Export | Delete | Cancel | Save
    -- ================================================================
    local bottomBar = CreateFrame("Frame", nil, editorFrame)
    bottomBar:SetHeight(SE.BOTTOM_BAR_HEIGHT)
    bottomBar:SetPoint("BOTTOMLEFT", 6, 4)
    bottomBar:SetPoint("BOTTOMRIGHT", -6, 4)

    local clearBtn = CreateFrame("Button", nil, bottomBar, "UIPanelButtonTemplate")
    clearBtn:SetSize(65, 22)
    clearBtn:SetPoint("LEFT", 0, 0)
    clearBtn:SetText(L["STYLE_CLEAR_ALL"])
    clearBtn:SetScript("OnClick", function()
        wipe(editorTagStates)
        wipe(editorLockedItems)
        editorBasketCount = 0
        wipe(editorRemovedItems)
        editorTierFilter = "first"
        for _, b in pairs(editorTierButtons) do
            HDG_UIHelpers.UpdateChipButtonState(b, b.tierKey == editorTierFilter)
        end
        descManuallyEdited = false
        if workingDef then
            workingDef.query, workingDef.boost, workingDef.anti = nil, nil, nil
            workingDef.lockedItems, workingDef.removedItems = nil, nil
            workingDef.curatedItems = nil
            workingDef.description = ""
            if editorFrame.descBox then editorFrame.descBox:SetText("") end
        end
        EditorAutoSave()
        StylesTab:RefreshEditorTags(true)
        StylesTab:DebouncePreview()
        -- Reset basket + info bar + help text + scroll positions
        StylesTab:RefreshBasket()
        editorFrame._tagSummaryText = ""
        StylesTab:UpdateInfoBarDefault()
        if editorFrame.helpStr then editorFrame.helpStr:Show() end
        if editorFrame.chipsScroll then editorFrame.chipsScroll.ResetScroll() end
        if editorFrame.basketScroll then editorFrame.basketScroll.ResetScroll() end
    end)

    local countStr = bottomBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    countStr:SetPoint("LEFT", clearBtn, "RIGHT", 8, 0)
    if scheme then ApplyFont(countStr, scheme, "small") end
    editorFrame.previewCountStr = countStr

    local exportBtn = CreateFrame("Button", nil, bottomBar, "UIPanelButtonTemplate")
    exportBtn:SetSize(70, 22)
    exportBtn:SetPoint("LEFT", countStr, "RIGHT", 8, 0)
    exportBtn:SetText(L["BTN_EXPORT"])
    exportBtn:SetScript("OnClick", function()
        if workingDef and StyleSerializer then
            RebuildWorkingDefFromTags()
            local encoded = StyleSerializer:Export(workingDef)
            if encoded then
                StylesTab:ShowExportDialog(encoded)
            end
        end
    end)
    editorFrame.exportBtn = exportBtn

    local saveBtn = CreateFrame("Button", nil, bottomBar, "UIPanelButtonTemplate")
    saveBtn:SetSize(70, 22)
    saveBtn:SetPoint("RIGHT", 0, 0)
    saveBtn:SetText(L["STYLE_SAVE"])
    saveBtn:SetScript("OnClick", function() StylesTab:SaveEditor() end)

    local cancelBtn = CreateFrame("Button", nil, bottomBar, "UIPanelButtonTemplate")
    cancelBtn:SetSize(70, 22)
    cancelBtn:SetPoint("RIGHT", saveBtn, "LEFT", -6, 0)
    cancelBtn:SetText(L["STYLE_CANCEL"])
    cancelBtn:SetScript("OnClick", function() StylesTab:ShowLanding() end)

    local deleteBtn = CreateFrame("Button", nil, bottomBar, "UIPanelButtonTemplate")
    deleteBtn:SetSize(70, 22)
    deleteBtn:SetPoint("RIGHT", cancelBtn, "LEFT", -6, 0)
    deleteBtn:SetText(L["STYLE_DELETE"])
    deleteBtn:SetScript("OnClick", function()
        if editingKey then
            local name = workingDef and workingDef.displayName or "this style"
            StaticPopup_Show("HDG_DELETE_CUSTOM_STYLE", name, nil, { key = editingKey })
        end
    end)
    editorFrame.deleteBtn = deleteBtn

    -- ================================================================
    -- COLUMNS FRAME: fills space between infoBar and bottomBar
    -- ================================================================
    local columnsFrame = CreateFrame("Frame", nil, editorFrame)
    columnsFrame:SetPoint("TOPLEFT", infoBar, "BOTTOMLEFT", 0, -4)
    columnsFrame:SetPoint("BOTTOMRIGHT", bottomBar, "TOPRIGHT", 0, 4)

    -- ================================================================
    -- COLUMN 1: Category list (14 facet buttons, vertical stack)
    -- ================================================================
    local categoryCol = CreateFrame("Frame", nil, columnsFrame, "BackdropTemplate")
    categoryCol:SetWidth(SE.CATEGORY_COL_WIDTH)
    categoryCol:SetPoint("TOPLEFT", 0, 0)
    categoryCol:SetPoint("BOTTOMLEFT", 0, 0)
    HDG_UIHelpers.ApplyBackdrop(categoryCol, "panel",
        {0.06, 0.06, 0.06, 0.5}, {0.15, 0.15, 0.15, 0.3})
    RegisterWidget(categoryCol, "Panel")

    local catBtnH = 28
    local catGap = 2
    for i, ft in ipairs(SE.FACET_TABS) do
        local btn = CreateFrame("Button", nil, categoryCol)
        btn:SetHeight(catBtnH)
        btn:SetPoint("TOPLEFT", 4, -4 - (i - 1) * (catBtnH + catGap))
        btn:SetPoint("RIGHT", -4, 0)

        btn.bg = btn:CreateTexture(nil, "BACKGROUND")
        btn.bg:SetAllPoints()
        btn.bg:SetAtlas("auctionhouse-nav-button")

        btn.selected = btn:CreateTexture(nil, "BACKGROUND")
        btn.selected:SetPoint("TOPLEFT", 0, -2)
        btn.selected:SetPoint("BOTTOMRIGHT", 0, 2)
        btn.selected:SetAtlas("auctionhouse-nav-button-select")
        btn.selected:SetShown(ft.key == editorFacetTab)
        btn.bg:SetShown(ft.key ~= editorFacetTab)

        btn.highlight = btn:CreateTexture(nil, "HIGHLIGHT")
        btn.highlight:SetPoint("TOPLEFT", 0, -2)
        btn.highlight:SetPoint("BOTTOMRIGHT", 0, 2)
        btn.highlight:SetAtlas("auctionhouse-nav-button-highlight")

        btn.tint = btn:CreateTexture(nil, "ARTWORK")
        btn.tint:SetPoint("TOPLEFT", 0, -2)
        btn.tint:SetPoint("BOTTOMRIGHT", 0, 2)
        btn.tint:SetAtlas("auctionhouse-nav-button-select")
        btn.tint:SetVertexColor(0.2, 0.8, 0.2, 0.6)
        btn.tint:Hide()

        btn.text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        btn.text:SetPoint("LEFT", 6, 4)
        btn.text:SetText(ft.label)
        btn.text:SetWordWrap(false)
        if scheme then ApplyFont(btn.text, scheme, "small") end

        -- Badge for active chip count
        btn.badge = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        btn.badge:SetPoint("RIGHT", -6, 4)
        btn.badge:SetTextColor(0.2, 0.9, 0.2, 1)
        if scheme then ApplyFont(btn.badge, scheme, "xsmall") end
        btn.badge:SetText("")

        btn.facetKey = ft.key
        btn.baseLabel = ft.label
        btn.isActive = ft.key == editorFacetTab

        btn:SetScript("OnClick", function()
            editorFacetTab = ft.key
            StylesTab:RefreshEditorTags(true)
            for _, fb in ipairs(editorFacetButtons) do
                local isActive = fb.facetKey == editorFacetTab
                fb.isActive = isActive
                fb.selected:SetShown(isActive)
                fb.bg:SetShown(not isActive)
                if isActive then
                    fb.text:SetTextColor(1, 1, 1, 1)
                elseif scheme then
                    fb.text:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 1)
                end
            end
        end)

        if ft.key == editorFacetTab then
            btn.text:SetTextColor(1, 1, 1, 1)
        elseif scheme then
            btn.text:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 1)
        end

        RegisterWidget(btn, "ChipButton")
        editorFacetButtons[i] = btn
    end

    -- ================================================================
    -- COLUMN 2: Chips (vertical arrow-scroll of tag chips)
    -- ================================================================
    local chipsColX = SE.CATEGORY_COL_WIDTH + SE.COLUMN_GAP
    local chipsColFrame = CreateFrame("Frame", nil, columnsFrame, "BackdropTemplate")
    chipsColFrame:SetWidth(SE.CHIPS_COL_WIDTH)
    chipsColFrame:SetPoint("TOPLEFT", chipsColX, 0)
    chipsColFrame:SetPoint("BOTTOMLEFT", chipsColX, 0)
    HDG_UIHelpers.ApplyBackdrop(chipsColFrame, "panel",
        {0.06, 0.06, 0.06, 0.5}, {0.15, 0.15, 0.15, 0.3})
    RegisterWidget(chipsColFrame, "Panel")

    local chipsScroll = CreateVerticalArrowScroll(chipsColFrame, SE.CHIPS_COL_WIDTH - 8, 400)
    chipsScroll.frame:SetPoint("TOPLEFT", 4, -4)
    chipsScroll.frame:SetPoint("BOTTOMRIGHT", -4, 4)
    editorFrame.chipsScroll = chipsScroll
    editorFrame.tagContent = chipsScroll.content

    -- ================================================================
    -- COLUMN 3: Pick list (tier strip + preview gallery grid)
    -- ================================================================
    local pickListX = chipsColX + SE.CHIPS_COL_WIDTH + SE.COLUMN_GAP
    local pickListCol = CreateFrame("Frame", nil, columnsFrame)
    pickListCol:SetWidth(SE.PICK_LIST_WIDTH)
    pickListCol:SetPoint("TOPLEFT", pickListX, 0)
    pickListCol:SetPoint("BOTTOMLEFT", pickListX, 0)

    -- Tier strip (horizontal row of filter chips)
    local tierStrip = CreateFrame("Frame", nil, pickListCol)
    tierStrip:SetHeight(SE.TIER_STRIP_HEIGHT)
    tierStrip:SetPoint("TOPLEFT", 0, 0)
    tierStrip:SetPoint("TOPRIGHT", 0, 0)

    local tierFilterDefs = {
        { key = "all",     label = "All" },
        { key = "first",   label = L["STYLE_TIER_SIGNATURE"] },
        { key = "second",  label = L["STYLE_TIER_ACCENT"] },
        { key = "neutral", label = L["STYLE_TIER_VERSATILE"] },
        { key = "clash",   label = L["STYLE_TIER_CLASHING"] },
    }
    local tierX = 0
    for _, td in ipairs(tierFilterDefs) do
        local btn = HDG_UIHelpers.CreateChipButton(tierStrip, td.label, 70, td.key == editorTierFilter, nil)
        if scheme then ApplyFont(btn.text, scheme, "xsmall") end
        btn:SetPoint("TOPLEFT", tierX, 0)
        btn.tierKey = td.key
        btn:SetScript("OnClick", function()
            editorTierFilter = td.key
            for _, b in pairs(editorTierButtons) do
                HDG_UIHelpers.UpdateChipButtonState(b, b.tierKey == editorTierFilter)
            end
            StylesTab:DebouncePreview()
        end)
        editorTierButtons[td.key] = btn
        tierX = tierX + 72
    end

    -- Curated mode controls: search box + Add All (hidden in smart mode)
    local curatedBar = CreateFrame("Frame", nil, pickListCol)
    curatedBar:SetHeight(SE.TIER_STRIP_HEIGHT)
    curatedBar:SetPoint("TOPLEFT", 0, 0)
    curatedBar:SetPoint("TOPRIGHT", 0, 0)
    curatedBar:Hide()

    local curatedSearch = CreateFrame("EditBox", nil, curatedBar, "InputBoxTemplate")
    curatedSearch:SetSize(180, 20)
    curatedSearch:SetPoint("LEFT", 0, 0)
    curatedSearch:SetAutoFocus(false)
    curatedSearch:SetMaxLetters(40)
    curatedSearch:SetScript("OnEscapePressed", function(eb) eb:ClearFocus() end)
    curatedSearch:SetScript("OnTextChanged", function(eb)
        editorSearchText = eb:GetText() or ""
        StylesTab:DebouncePreview()
    end)
    if scheme then
        curatedSearch:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b)
    end

    -- Placeholder text for search box
    local searchPlaceholder = curatedBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    searchPlaceholder:SetPoint("LEFT", curatedSearch, "LEFT", 6, 0)
    searchPlaceholder:SetText(L["STYLE_SEARCH_PLACEHOLDER"] or "Search items...")
    searchPlaceholder:SetTextColor(0.5, 0.5, 0.5)
    curatedSearch:HookScript("OnTextChanged", function(eb)
        searchPlaceholder:SetShown(eb:GetText() == "")
    end)
    curatedSearch:HookScript("OnEditFocusGained", function() searchPlaceholder:Hide() end)
    curatedSearch:HookScript("OnEditFocusLost", function(eb)
        searchPlaceholder:SetShown(eb:GetText() == "")
    end)

    local addAllBtn = CreateFrame("Button", nil, curatedBar, "UIPanelButtonTemplate")
    addAllBtn:SetSize(90, 20)
    addAllBtn:SetPoint("LEFT", curatedSearch, "RIGHT", 6, 0)
    addAllBtn:SetText(L["STYLE_ADD_ALL"] or "Add All")
    addAllBtn:SetScript("OnClick", function()
        for _, item in ipairs(editorAllItems) do
            if not editorLockedItems[item.itemID] then
                editorLockedItems[item.itemID] = true
                editorBasketCount = editorBasketCount + 1
            end
        end
        StylesTab:UpdateEditorPreview()
    end)
    editorFrame.curatedBar = curatedBar
    editorFrame.curatedSearch = curatedSearch
    editorFrame.addAllBtn = addAllBtn

    -- Preview frame (gallery grid below tier strip / curated bar)
    local previewFrame = CreateFrame("Frame", nil, pickListCol, "BackdropTemplate")
    previewFrame:SetPoint("TOPLEFT", tierStrip, "BOTTOMLEFT", 0, -2)
    previewFrame:SetPoint("BOTTOMRIGHT", 0, 0)
    HDG_UIHelpers.ApplyBackdrop(previewFrame, "panel",
        {0.04, 0.04, 0.04, 0.4}, {0.12, 0.12, 0.12, 0.3})
    RegisterWidget(previewFrame, "Panel")

    local previewScroll = CreateFrame("ScrollFrame", nil, previewFrame, "UIPanelScrollFrameTemplate")
    previewScroll:SetPoint("TOPLEFT", 4, -4)
    previewScroll:SetPoint("BOTTOMRIGHT", -22, 4)
    HDG_UIHelpers.StyleScrollBar(previewScroll)
    RegisterWidget(previewScroll, "ScrollFrame")

    local previewContent = CreateFrame("Frame", nil, previewScroll)
    previewContent:SetWidth(previewScroll:GetWidth() or 400)
    previewContent:SetHeight(1)
    previewScroll:SetScrollChild(previewContent)

    previewScroll:HookScript("OnSizeChanged", function(_, w)
        previewContent:SetWidth(w)
    end)

    editorFrame.previewContent = previewContent
    editorFrame.previewScroll = previewScroll

    -- Virtual scroll: re-render visible cells on scroll
    previewScroll:HookScript("OnVerticalScroll", function()
        editorScrollDirty = true
    end)
    previewScroll:HookScript("OnUpdate", function()
        if editorScrollDirty then
            editorScrollDirty = false
            StylesTab:RenderVisibleEditorCells()
        end
    end)

    -- Undo bar (anchored at bottom of preview frame, hidden by default)
    local undoBar = CreateFrame("Frame", nil, previewFrame, "BackdropTemplate")
    undoBar:SetHeight(24)
    undoBar:SetPoint("BOTTOMLEFT", 2, 2)
    undoBar:SetPoint("BOTTOMRIGHT", -22, 2)
    undoBar:SetFrameLevel(previewFrame:GetFrameLevel() + 10)
    HDG_UIHelpers.ApplyBackdrop(undoBar, "panel",
        {0.08, 0.06, 0.02, 0.95}, {0.4, 0.35, 0.1, 0.6})
    RegisterWidget(undoBar, "Panel")
    undoBar:Hide()

    local undoText = undoBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    undoText:SetPoint("LEFT", 8, 0)
    undoText:SetText("")
    if scheme then ApplyFont(undoText, scheme, "small") end

    local undoBtn = CreateFrame("Button", nil, undoBar, "UIPanelButtonTemplate")
    undoBtn:SetSize(50, 18)
    undoBtn:SetPoint("RIGHT", -4, 0)
    undoBtn:SetText(L["STYLE_UNDO"])
    undoBtn:SetScript("OnClick", function()
        if editorLastRemoved then
            editorRemovedItems[editorLastRemoved.itemID] = nil
            editorLastRemoved = nil
            EditorAutoSave()
            StylesTab:UpdateEditorPreview()
        end
        undoBar:Hide()
    end)
    editorFrame.undoBar = undoBar
    editorFrame.undoText = undoText
    editorFrame.undoBtn = undoBtn

    -- Help text (shown once until first tag selected)
    local helpStr = previewFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    helpStr:SetPoint("CENTER", 0, 0)
    helpStr:SetWidth(SE.PICK_LIST_WIDTH - 40)
    helpStr:SetJustifyH("CENTER")
    helpStr:SetWordWrap(true)
    helpStr:SetText(
        "|cFFDDDDDDBuild Your Style|r\n\n" ..
        "|cFF94a3b8Select a category on the left, then click tags\n" ..
        "to define what your style looks like.\n\n" ..
        "Matching items appear here as you build.\n" ..
        "Click items to add them to your basket.|r"
    )
    if scheme then ApplyFont(helpStr, scheme, "small") end
    editorFrame.helpStr = helpStr

    -- ================================================================
    -- COLUMN 4: Basket (selected/locked items)
    -- ================================================================
    local basketX = pickListX + SE.PICK_LIST_WIDTH + SE.COLUMN_GAP
    local basketCol = CreateFrame("Frame", nil, columnsFrame, "BackdropTemplate")
    basketCol:SetWidth(SE.BASKET_WIDTH)
    basketCol:SetPoint("TOPLEFT", basketX, 0)
    basketCol:SetPoint("BOTTOMLEFT", basketX, 0)
    HDG_UIHelpers.ApplyBackdrop(basketCol, "panel",
        {0.06, 0.06, 0.06, 0.5}, {0.15, 0.15, 0.15, 0.3})
    RegisterWidget(basketCol, "Panel")

    local basketHeader = basketCol:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    basketHeader:SetPoint("TOPLEFT", 8, -6)
    basketHeader:SetText(string.format(L["STYLE_BASKET"], 0))
    if scheme then ApplyFont(basketHeader, scheme, "small") end
    basketHeader:SetTextColor(0.9, 0.9, 0.9, 1)
    editorFrame.basketHeader = basketHeader

    local basketCost = basketCol:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    basketCost:SetPoint("TOPRIGHT", -8, -6)
    basketCost:SetText("")
    if scheme then ApplyFont(basketCost, scheme, "small") end
    editorFrame.basketCost = basketCost

    local basketScroll = CreateVerticalArrowScroll(basketCol, SE.BASKET_WIDTH - 8, 300)
    basketScroll.frame:SetPoint("TOPLEFT", 4, -24)
    basketScroll.frame:SetPoint("BOTTOMRIGHT", -4, 4)
    editorFrame.basketScroll = basketScroll

    -- Initialize basket row pool
    basketRowPool = HDG_UIHelpers.CreateRowPool(function(parent)
        local row = CreateFrame("Frame", nil, parent)
        row:SetHeight(SE.BASKET_ROW_HEIGHT)

        row.icon = row:CreateTexture(nil, "ARTWORK")
        row.icon:SetSize(SE.BASKET_ICON_SIZE, SE.BASKET_ICON_SIZE)
        row.icon:SetPoint("LEFT", 2, 0)

        row.nameStr = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row.nameStr:SetPoint("LEFT", row.icon, "RIGHT", 4, 0)
        row.nameStr:SetPoint("RIGHT", -22, 0)
        row.nameStr:SetJustifyH("LEFT")
        row.nameStr:SetWordWrap(false)
        if scheme then ApplyFont(row.nameStr, scheme, "body") end

        row.removeBtn = CreateFrame("Button", nil, row)
        row.removeBtn:SetSize(16, 16)
        row.removeBtn:SetPoint("RIGHT", -2, 0)
        local xTex = row.removeBtn:CreateTexture(nil, "ARTWORK")
        xTex:SetAllPoints()
        xTex:SetAtlas("common-icon-redx")
        xTex:SetAlpha(0.6)
        row.removeBtn:SetScript("OnEnter", function() xTex:SetAlpha(1) end)
        row.removeBtn:SetScript("OnLeave", function() xTex:SetAlpha(0.6) end)

        row.highlight = row:CreateTexture(nil, "HIGHLIGHT")
        row.highlight:SetAllPoints()
        row.highlight:SetColorTexture(1, 1, 1, 0.05)

        return row
    end)

    -- Tag summary is stored on editorFrame and displayed via info bar
    editorFrame.summaryStr = nil -- no longer a separate font string
    editorFrame._tagSummaryText = "" -- cached summary text for info bar
end

function StylesTab:RefreshEditor()
    if not editorFrame or not workingDef then return end

    editorFrame.nameBox:SetText(workingDef.displayName or "")
    editorFrame.descBox:SetText(workingDef.description or "")

    -- Show/hide delete button based on whether editing existing
    if editorFrame.deleteBtn then
        editorFrame.deleteBtn:SetShown(editingKey ~= nil)
    end

    self:RefreshEditorTags(true)
    self:UpdateEditorPreview()
end

function StylesTab:RefreshEditorTags(resetScroll)
    if not editorFrame or not editorFrame.tagContent then return end

    local tagContent = editorFrame.tagContent
    -- Hide all existing tag buttons
    for _, btn in ipairs(editorTagButtons) do
        btn:Hide()
    end

    if not HDG_FacetVocab then return end
    local vocabTable = HDG_FacetVocab[editorFacetTab]
    if not vocabTable then return end

    -- Collect all tag values for this facet
    local tags = {}
    for _, v in pairs(vocabTable) do
        tags[#tags + 1] = v
    end
    -- Ensure tag state table exists for this facet
    if not editorTagStates[editorFacetTab] then
        editorTagStates[editorFacetTab] = {}
    end
    local facetStates = editorTagStates[editorFacetTab]

    -- Compute tag affinity from current query selections
    RebuildWorkingDefFromTags()
    local affinity
    if StyleEngine and workingDef and workingDef.query then
        affinity = StyleEngine:ComputeTagAffinity(workingDef.query)
    end
    local facetAffinity = affinity and affinity[editorFacetTab] or nil

    -- Sort: selected chips first, then by affinity descending, then alphabetical
    if editorFacetTab == "freeform" then
        table.sort(tags, function(a, b)
            local oa, ob = FREEFORM_SORT[a] or 500, FREEFORM_SORT[b] or 500
            if oa ~= ob then return oa < ob end
            return a < b
        end)
    else
        table.sort(tags, function(a, b)
            local sa = facetStates[a] and 1 or 0
            local sb = facetStates[b] and 1 or 0
            if sa ~= sb then return sa > sb end -- selected first
            if sa == 1 then return a < b end -- selected: stable alphabetical
            if facetAffinity then
                local aa = facetAffinity[a] or 0
                local ab = facetAffinity[b] or 0
                if aa ~= ab then return aa > ab end -- unselected: higher affinity first
            end
            return a < b -- alphabetical fallback
        end)
    end

    -- Vertical stack layout for chips column
    local SE = CONFIG.STYLE_EDITOR
    local btnH = SE.TAG_BTN_HEIGHT
    local gap = SE.TAG_GRID_SPACING
    local chipWidth = SE.CHIPS_COL_WIDTH - 16 -- padding on both sides
    local y = -2

    local scheme = GetScheme()

    for i, tag in ipairs(tags) do
        local btn = editorTagButtons[i]
        if not btn then
            btn = HDG_UIHelpers.CreateChipButton(tagContent, "", 60, false, nil)
            editorTagButtons[i] = btn
        end

        -- Create or reuse tint overlay for state/affinity coloring
        if not btn.tint then
            btn.tint = btn:CreateTexture(nil, "ARTWORK")
            btn.tint:SetAllPoints()
            btn.tint:SetAtlas("auctionhouse-nav-button-select")
        end

        local state = facetStates[tag] or "off"
        local textColor
        if state == "query" then
            textColor = {0, 0.8, 0, 1}
            btn.tint:SetVertexColor(0.3, 1.0, 0.3, 0.9)
            btn.tint:Show()
        else
            textColor = {0.5, 0.5, 0.5, 1}
            btn.tint:Hide()
        end

        -- Apply affinity hint for unselected tags
        local pct = facetAffinity and facetAffinity[tag]
        if state == "off" and pct then
            if pct >= 40 then
                textColor = {0.35, 0.65, 0.35, 1}
                btn.tint:SetVertexColor(0.2, 0.7, 0.2, 0.7)
                btn.tint:Show()
            elseif pct <= 5 then
                textColor = {0.6, 0.35, 0.35, 1}
                btn.tint:SetVertexColor(0.8, 0.2, 0.2, 0.7)
                btn.tint:Show()
            else
                btn.tint:Hide()
            end
        end

        btn.text:SetText(PrettyTagName(tag))
        -- Full width vertical stack (not variable-width grid)
        btn:SetWidth(chipWidth)
        btn.text:SetTextColor(unpack(textColor))

        btn:ClearAllPoints()
        btn:SetPoint("TOPLEFT", 2, y)
        btn.tagValue = tag
        btn.facetKey = editorFacetTab

        -- Set selected appearance for active tags
        HDG_UIHelpers.UpdateChipButtonState(btn, state ~= "off")

        btn:SetScript("OnClick", function()
            -- Toggle: off <-> query
            local curState = facetStates[tag] or "off"
            local nextState
            if curState == "off" then nextState = "query"
            else nextState = "off" end

            facetStates[tag] = (nextState ~= "off") and nextState or nil
            StylesTab:RefreshEditorTags()
            StylesTab:DebouncePreview()
            -- Auto-update description from Must Have tags
            if not descManuallyEdited and workingDef and editorFrame and editorFrame.descBox then
                local auto = BuildAutoDescription()
                workingDef.description = auto
                editorFrame.descBox:SetText(auto)
            end
        end)

        btn:Show()
        y = y - (btnH + gap)
    end

    -- Set content height and update arrow scroll
    local totalHeight = math.abs(y) + 4
    if editorFrame.chipsScroll then
        editorFrame.chipsScroll.SetContentHeight(totalHeight)
        if resetScroll then editorFrame.chipsScroll.ResetScroll() end
    else
        tagContent:SetHeight(totalHeight)
    end

    -- Update category column: highlight active tab + badge counts
    for _, fb in ipairs(editorFacetButtons) do
        local isActive = fb.facetKey == editorFacetTab
        fb.isActive = isActive
        fb.selected:SetShown(isActive)
        fb.bg:SetShown(not isActive)
        if isActive then
            fb.text:SetTextColor(1, 1, 1, 1)
        elseif scheme then
            fb.text:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 1)
        end
        -- Badge: count of active tags in this facet
        local count = 0
        local fs = editorTagStates[fb.facetKey]
        if fs then
            for _ in pairs(fs) do count = count + 1 end
        end
        if fb.badge then
            fb.badge:SetText(count > 0 and tostring(count) or "")
        end
        -- Tint for facets with selections (uses same atlas as selected, green-tinted)
        if count > 0 then
            fb.tint:SetVertexColor(0.3, 1.0, 0.3, 0.9)
            fb.tint:Show()
        else
            fb.tint:Hide()
        end
    end

    -- Hide help text once any tag is selected
    if editorFrame.helpStr and editorFrame.helpStr:IsShown() then
        for _, fs in pairs(editorTagStates) do
            if next(fs) then
                editorFrame.helpStr:Hide()
                break
            end
        end
    end

    -- Update selection summary text (stored for info bar display)
    local parts = {}
    for _, ft in ipairs(CONFIG.STYLE_EDITOR.FACET_TABS) do
        local fs = editorTagStates[ft.key]
        if fs then
            local selected = {}
            for tv, _ in pairs(fs) do
                selected[#selected+1] = PrettyTagName(tv)
            end
            table.sort(selected)
            if #selected > 0 then
                parts[#parts+1] = "|cFFDDDDDD" .. ft.label .. ":|r |cFF00CC00" .. table.concat(selected, ", ") .. "|r"
            end
        end
    end
    if #parts > 0 then
        editorFrame._tagSummaryText = table.concat(parts, "  ")
    else
        editorFrame._tagSummaryText = "|cFF94a3b8No tags selected yet|r"
    end
end

function StylesTab:OnModeToggle()
    if not editorFrame then return end
    local isCurated = (editorMode == "curated")
    -- Update default name to match mode (swap "Style N" <-> "Style (Curated) N")
    if workingDef and editorFrame.nameBox then
        local curName = workingDef.displayName or ""
        local base, num = curName:match("^(.+) Style %(Curated%) (%d+)$")
        if not base then base, num = curName:match("^(.+) Style (%d+)$") end
        if base and num then
            local newName = isCurated and (base .. " Style (Curated) " .. num) or (base .. " Style " .. num)
            workingDef.displayName = newName
            editorFrame.nameBox:SetText(newName)
        end
    end
    -- Swap tier strip / curated bar visibility
    for _, b in pairs(editorTierButtons) do b:GetParent():SetShown(not isCurated) end
    if editorFrame.curatedBar then editorFrame.curatedBar:SetShown(isCurated) end
    -- Reset search when leaving curated
    if not isCurated and editorFrame.curatedSearch then
        editorSearchText = ""
        editorFrame.curatedSearch:SetText("")
    end
    -- Hide export button in curated mode (no serializer support for curated items)
    if editorFrame.exportBtn then editorFrame.exportBtn:SetShown(not isCurated) end
    self:UpdateEditorPreview()
end

function StylesTab:DebouncePreview()
    if editorPreviewTimer then
        editorPreviewTimer:Cancel()
    end
    editorPreviewTimer = C_Timer.NewTimer(CONFIG.STYLE_EDITOR.DEBOUNCE_TIMER, function()
        editorPreviewTimer = nil
        StylesTab:UpdateEditorPreview()
    end)
end

function StylesTab:UpdateEditorPreview()
    if not editorFrame or not workingDef or not StyleEngine then return end

    HDG_UIHelpers.ReleaseAllGalleryCells(editorPreviewPool)
    wipe(editorAllItems)

    -- Hide undo bar on full refresh
    if editorFrame.undoBar then editorFrame.undoBar:Hide() end

    -- Shared filter: collected + released items only, exclude removed items
    local now = time()
    local function editorFilter(itemID, entry)
        if editorRemovedItems[itemID] then return false end
        if not (HDG_Data and HDG_Data.IsItemCollected(itemID)) then return false end
        local itemName = HDG_Data and HDG_Data.GetCachedItemName(itemID) or ""
        if itemName == "Unreleased" then return false end
        if entry[5] and entry[5] > now then return false end
        return true
    end

    if editorMode == "curated" then
        -- CURATED MODE: facet chips = browse filters, no scoring
        -- Build filter map from active tag states (query = "on")
        local filters = {}
        for facetKey, tags in pairs(editorTagStates) do
            for tagVal, state in pairs(tags) do
                if state == "query" then
                    if not filters[facetKey] then filters[facetKey] = {} end
                    filters[facetKey][#filters[facetKey] + 1] = tagVal
                end
            end
        end
        local curSearchText = (editorSearchText ~= "") and editorSearchText or nil
        local results = StyleEngine:FilterItemsByFacets(
            next(filters) and filters or nil, curSearchText, editorFilter)
        -- Sort: basket items first, then alphabetical by name
        table.sort(results, function(a, b)
            local la = editorLockedItems[a.itemID] and 1 or 0
            local lb = editorLockedItems[b.itemID] and 1 or 0
            if la ~= lb then return la > lb end
            local na = HDG_Data and HDG_Data.GetCachedItemName(a.itemID) or ""
            local nb = HDG_Data and HDG_Data.GetCachedItemName(b.itemID) or ""
            return na < nb
        end)
        editorAllItems = results
        -- Update Add All button label with count
        if editorFrame.addAllBtn then
            editorFrame.addAllBtn:SetText(string.format("%s (%d)", L["STYLE_ADD_ALL"] or "Add All", #results))
        end
    else
        -- SMART MODE: tag-based scoring (existing behavior)
        RebuildWorkingDefFromTags()

        local hasQuery = workingDef.query and next(workingDef.query)
        local hasBoost = workingDef.boost and next(workingDef.boost)

        if not hasQuery and not hasBoost then
            editorFrame.previewCountStr:SetText("|cFF888888" .. L["TIP_SELECT_TAGS_PREVIEW"] .. "|r")
            editorFrame.previewContent:SetHeight(1)
            if editorTierButtons["all"] then
                editorTierButtons["all"].text:SetText("All")
                editorTierButtons["first"].text:SetText(L["STYLE_TIER_SIGNATURE"])
                editorTierButtons["second"].text:SetText(L["STYLE_TIER_ACCENT"])
                editorTierButtons["neutral"].text:SetText(L["STYLE_TIER_VERSATILE"])
                editorTierButtons["clash"].text:SetText(L["STYLE_TIER_CLASHING"])
            end
            self:RefreshBasket()
            self:UpdateInfoBarDefault()
            return
        end

        local result = StyleEngine:ScoreStyleLive(workingDef, 99999, editorFilter)

        -- Prepend locked items that aren't already in results
        local seenIDs = {}
        for _, s in ipairs(result.sampleItems) do seenIDs[s.itemID] = true end
        local lockedInserts = {}
        for lockedID in pairs(editorLockedItems) do
            if not seenIDs[lockedID] and not editorRemovedItems[lockedID] then
                local allDecor = HDG_AllDecorDB or {}
                local entry = allDecor[lockedID]
                if entry then
                    table.insert(lockedInserts, { itemID = lockedID, decorID = entry[1] or lockedID, score = 999 })
                end
            end
        end
        if #lockedInserts > 0 then
            for i = #lockedInserts, 1, -1 do
                table.insert(result.sampleItems, 1, lockedInserts[i])
            end
        end

        -- Apply tier filter
        if editorTierFilter ~= "all" then
            local filtered = {}
            for _, s in ipairs(result.sampleItems) do
                if s.score == 999 then
                    table.insert(filtered, s)
                elseif s.tier == editorTierFilter then
                    table.insert(filtered, s)
                end
            end
            editorAllItems = filtered
        else
            editorAllItems = result.sampleItems
        end

        -- Update tier button labels with counts
        if editorTierButtons["all"] then
            editorTierButtons["all"].text:SetText("All")
            editorTierButtons["first"].text:SetText(string.format("%s (%d)", L["STYLE_TIER_SIGNATURE"], result.firstCount))
            editorTierButtons["second"].text:SetText(string.format("%s (%d)", L["STYLE_TIER_ACCENT"], result.secondCount))
            editorTierButtons["neutral"].text:SetText(string.format("%s (%d)", L["STYLE_TIER_VERSATILE"], result.neutralCount))
            editorTierButtons["clash"].text:SetText(string.format("%s (%d)", L["STYLE_TIER_CLASHING"], result.clashCount))
        end
    end

    local countText = string.format("|cFFDDDDDD%d shown|r", #editorAllItems)
    editorFrame.previewCountStr:SetText(countText)

    -- Set total content height for scrolling (use editor-specific cell sizes)
    local SE = CONFIG.STYLE_EDITOR
    local cellSize = SE.EDITOR_CELL_SIZE
    local cellGap = SE.EDITOR_CELL_SPACING
    local cols = SE.EDITOR_CELL_COLS
    local previewContent = editorFrame.previewContent
    local totalRows = math.ceil(#editorAllItems / math.max(1, cols))
    previewContent:SetHeight(math.max(1, totalRows * (cellSize + cellGap) + 8))

    -- Render only visible cells
    self:RenderVisibleEditorCells()

    -- Update basket display
    self:RefreshBasket()

    -- Update info bar default state
    self:UpdateInfoBarDefault()
end

function StylesTab:RenderVisibleEditorCells()
    if not editorFrame or #editorAllItems == 0 then return end
    HDG_UIHelpers.ReleaseAllGalleryCells(editorPreviewPool)

    local SE = CONFIG.STYLE_EDITOR
    local ST = CONFIG.STYLES_TAB
    local cellSize = SE.EDITOR_CELL_SIZE
    local cellGap = SE.EDITOR_CELL_SPACING
    local cols = SE.EDITOR_CELL_COLS
    local rowHeight = cellSize + cellGap
    local previewContent = editorFrame.previewContent
    local previewScroll = editorFrame.previewScroll

    local scrollOffset = previewScroll:GetVerticalScroll() or 0
    local viewportHeight = previewScroll:GetHeight() or 400

    local firstRow = math.max(0, math.floor(scrollOffset / rowHeight) - 1)
    local lastRow = math.ceil((scrollOffset + viewportHeight) / rowHeight) + 1
    local firstIdx = firstRow * cols + 1
    local lastIdx = math.min(#editorAllItems, (lastRow + 1) * cols)

    for i = firstIdx, lastIdx do
        local sample = editorAllItems[i]
        if not sample then break end

        local cell = HDG_UIHelpers.CreateGalleryCell(editorPreviewPool, previewContent, cellSize, {
            statusIconSize = ST.STATUS_ICON_SIZE,
            checkmarkTexture = CONFIG.TEXTURES.CHECKMARK,
        })
        local col = (i - 1) % cols
        local row = math.floor((i - 1) / cols)
        cell:SetPoint("TOPLEFT", 4 + col * (cellSize + cellGap), -4 - row * rowHeight)

        local isCollected = HDG_Data and HDG_Data.IsItemCollected(sample.itemID)
        cell.checkmark:SetShown(isCollected or false)
        cell.redX:SetShown(not isCollected)
        HDG_UIHelpers.UpdateGalleryCellBackground(cell, false)

        local decorInfo = HDG_UIHelpers.UpdateGalleryCellQuantity(cell, sample.decorID)
        if not decorInfo and HDG_Data and HDG_Data.GetCatalog then
            decorInfo = HDG_Data.GetCatalog(sample.decorID)
        end
        HDG_UIHelpers.UpdateGalleryCellVisuals(cell, decorInfo, isCollected, ST.DEFAULT_SCENE_ID)

        -- Basket badge (replaces old lock icon)
        if not cell.basketBadge then
            cell.basketBadge = cell:CreateTexture(nil, "OVERLAY")
            cell.basketBadge:SetAtlas("bags-greenarrow")
            cell.basketBadge:SetSize(20, 20)
            cell.basketBadge:SetPoint("TOPRIGHT", -2, -2)
        end
        cell.basketBadge:SetShown(editorLockedItems[sample.itemID] or false)

        -- Lock icon (keep for backward compat display)
        if not cell.lockIcon then
            cell.lockIcon = cell:CreateTexture(nil, "OVERLAY")
            cell.lockIcon:SetAtlas("tradeskills-icon-locked")
            cell.lockIcon:SetSize(20, 20)
            cell.lockIcon:SetPoint("TOPRIGHT", -2, -2)
            cell.lockIcon:SetBlendMode("ADD")
        end
        cell.lockIcon:Hide() -- basket badge replaces this

        -- "Used in other styles" indicator (small dot, bottom-left)
        if not cell.usedDot then
            cell.usedDot = cell:CreateTexture(nil, "OVERLAY")
            cell.usedDot:SetSize(8, 8)
            cell.usedDot:SetPoint("BOTTOMLEFT", 3, 3)
            cell.usedDot:SetAtlas("poi-door")
        end
        local usedList = usedInOtherStyles[sample.itemID]
        cell.usedDot:SetShown(usedList ~= nil)

        -- Tooltip + info bar hover
        cell:SetScript("OnEnter", function(self)
            self.hoverBg:Show()
            local ic = StyleEngine and StyleEngine:GetItemInfo(sample.itemID)
            local tipName = (ic and ic.name) or ("Item #" .. sample.itemID)
            StylesTab:UpdateInfoBarHover(sample.itemID, sample.decorID, tipName)
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:AddLine(tipName, 1, 1, 1)
            if editorLockedItems[sample.itemID] then
                GameTooltip:AddLine("In Basket", 0.2, 0.9, 0.2)
            end
            local usedStyles = usedInOtherStyles[sample.itemID]
            if usedStyles and #usedStyles > 0 then
                local MAX_SHOWN = 5
                local shown = {}
                for si = 1, math.min(#usedStyles, MAX_SHOWN) do
                    shown[#shown + 1] = usedStyles[si]
                end
                local line = "Used in: " .. table.concat(shown, ", ")
                if #usedStyles > MAX_SHOWN then
                    line = line .. " +" .. (#usedStyles - MAX_SHOWN) .. " more"
                end
                GameTooltip:AddLine(line, 0.6, 0.6, 0.8, true)
            end
            GameTooltip:AddLine(" ", 1, 1, 1)
            GameTooltip:AddLine("Left-click: Add/remove from basket", 0.7, 0.7, 0.7)
            GameTooltip:AddLine("Shift-click: Add to shopping list", 0.7, 0.7, 0.7)
            GameTooltip:AddLine("Right-click: Hide from preview", 0.7, 0.7, 0.7)
            GameTooltip:Show()
        end)
        cell:SetScript("OnLeave", function(self)
            self.hoverBg:Hide()
            GameTooltip:Hide()
            StylesTab:UpdateInfoBarDefault()
        end)

        -- Click handlers
        cell:RegisterForClicks("LeftButtonUp", "RightButtonUp")
        cell:SetScript("OnClick", function(self, button)
            if button == "LeftButton" then
                if IsShiftKeyDown() then
                    -- Shift-click: add to shopping list
                    local npcIDs = HDG_VendorByItem and HDG_VendorByItem[sample.itemID]
                    local npcID = npcIDs and npcIDs[1] or nil
                    if HDG.Store:AddToVendorShoppingList(sample.itemID, npcID) then
                        local iName = HDG_Data and HDG_Data.GetCachedItemName(sample.itemID) or ("Item " .. sample.itemID)
                        print("|cff00ccff[HDG]|r Added " .. iName .. " to shopping list")
                        if HDG_VendorShoppingList and HDG_VendorShoppingList.IsShown and HDG_VendorShoppingList.IsShown() then
                            HDG_VendorShoppingList.Refresh()
                        end
                        if HDG_UI and HDG_UI.UpdateShopListBadge then HDG_UI.UpdateShopListBadge() end
                    end
                else
                    -- Toggle basket membership
                    if editorLockedItems[sample.itemID] then
                        editorLockedItems[sample.itemID] = nil
                        editorBasketCount = editorBasketCount - 1
                    else
                        editorLockedItems[sample.itemID] = true
                        editorBasketCount = editorBasketCount + 1
                    end
                    self.basketBadge:SetShown(editorLockedItems[sample.itemID] or false)
                    EditorAutoSave()
                    StylesTab:RefreshBasket()
                    StylesTab:UpdateInfoBarDefault()
                end
            elseif button == "RightButton" then
                if editorLockedItems[sample.itemID] then return end
                editorRemovedItems[sample.itemID] = true
                local iName = HDG_Data and HDG_Data.GetCachedItemName(sample.itemID) or ("Item " .. sample.itemID)
                editorLastRemoved = { itemID = sample.itemID, name = iName }
                self:Hide()
                EditorAutoSave()
                if editorFrame.undoBar then
                    editorFrame.undoText:SetText("|cFFFFCC00Removed:|r " .. iName)
                    editorFrame.undoBar:Show()
                    if editorFrame.undoTimer then editorFrame.undoTimer:Cancel() end
                    editorFrame.undoTimer = C_Timer.NewTimer(8, function()
                        editorFrame.undoTimer = nil
                        if editorFrame.undoBar:IsShown() then
                            editorFrame.undoBar:Hide()
                            editorLastRemoved = nil
                        end
                    end)
                end
            end
        end)

        cell:Show()
    end
end

-- ============================================================================
-- BASKET DISPLAY
-- ============================================================================

function StylesTab:RefreshBasket()
    if not editorFrame or not basketRowPool or not editorFrame.basketScroll then return end

    basketRowPool:ReleaseAll()
    local SE = CONFIG.STYLE_EDITOR
    local basketContent = editorFrame.basketScroll.content
    local scheme = GetScheme()

    -- Collect locked item IDs into sorted list for stable display
    local lockedList = {}
    for itemID in pairs(editorLockedItems) do
        lockedList[#lockedList + 1] = itemID
    end
    table.sort(lockedList)

    local y = -2
    local totalCost = 0

    for _, itemID in ipairs(lockedList) do
        local row = basketRowPool:Acquire(basketContent)
        row:SetPoint("TOPLEFT", 0, y)
        row:SetPoint("RIGHT", 0, 0)
        row.itemID = itemID

        -- Item name
        local iName = HDG_Data and HDG_Data.GetCachedItemName(itemID) or ("Item " .. itemID)
        row.nameStr:SetText(iName)
        if scheme then
            row.nameStr:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, 1)
        else
            row.nameStr:SetTextColor(0.9, 0.9, 0.9, 1)
        end

        -- Icon: try to get from AllDecorDB or catalog
        local allDecor = HDG_AllDecorDB or {}
        local entry = allDecor[itemID]
        local decorID = entry and entry[1] or itemID
        local iconSet = false
        if HDG_Data and HDG_Data.GetCatalog then
            local info = HDG_Data.GetCatalog(decorID)
            if info then
                if info.iconTexture then
                    row.icon:SetTexture(info.iconTexture)
                    iconSet = true
                elseif info.iconAtlas then
                    row.icon:SetAtlas(info.iconAtlas)
                    iconSet = true
                end
                -- Accumulate placement cost
                if info.placementCost then
                    totalCost = totalCost + info.placementCost
                end
            end
        end
        if not iconSet then
            row.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
        end

        -- Remove button
        row.removeBtn:SetScript("OnClick", function()
            editorLockedItems[itemID] = nil
            editorBasketCount = editorBasketCount - 1
            EditorAutoSave()
            StylesTab:RefreshBasket()
            StylesTab:RenderVisibleEditorCells() -- update cell badges
            StylesTab:UpdateInfoBarDefault()
        end)

        y = y - SE.BASKET_ROW_HEIGHT
    end

    -- Update scroll content height
    local totalHeight = math.abs(y) + 4
    editorFrame.basketScroll.SetContentHeight(totalHeight)

    -- Header count
    if editorFrame.basketHeader then
        editorFrame.basketHeader:SetText(string.format(L["STYLE_BASKET"], #lockedList))
    end

    -- Footer cost
    if editorFrame.basketCost then
        if totalCost > 0 then
            local costStr = HOUSING_DECOR_PLACEMENT_COST_FORMAT and HOUSING_DECOR_PLACEMENT_COST_FORMAT:format(totalCost) or tostring(totalCost)
            editorFrame.basketCost:SetText(costStr)
        else
            editorFrame.basketCost:SetText("")
        end
    end
end

-- ============================================================================
-- INFO BAR (hover + default)
-- ============================================================================

function StylesTab:UpdateInfoBarDefault()
    if not editorFrame or not editorFrame.infoLine1Left then return end

    local basketCount = editorBasketCount

    local shownCount = #editorAllItems

    editorFrame.infoLine1Left:SetText(
        string.format("|cFFe2e8f0%d|r |cFF94a3b8in basket|r", basketCount)
    )
    editorFrame.infoLine1Right:SetText(
        string.format("|cFF94a3b8%d matching|r", shownCount)
    )
    -- Line 2: tag summary or help text
    local summary = editorFrame._tagSummaryText or ""
    if summary ~= "" then
        editorFrame.infoLine2:SetText(summary)
    else
        editorFrame.infoLine2:SetText("|cFF94a3b8Select categories and tags to build your style|r")
    end
end

function StylesTab:UpdateInfoBarHover(itemID, decorID, itemName)
    if not editorFrame or not editorFrame.infoLine1Left then return end

    -- Item name in highlight color
    editorFrame.infoLine1Left:SetText("|cFFe2e8f0" .. (itemName or "") .. "|r")

    -- Cost + basket total on right
    local cost = 0
    if HDG_Data and HDG_Data.GetCatalog then
        local info = HDG_Data.GetCatalog(decorID)
        if info and info.placementCost then
            cost = info.placementCost
        end
    end

    local basketCount = editorBasketCount

    local rightParts = {}
    if cost > 0 then
        local costStr = HOUSING_DECOR_PLACEMENT_COST_FORMAT and HOUSING_DECOR_PLACEMENT_COST_FORMAT:format(cost) or tostring(cost)
        rightParts[#rightParts + 1] = costStr
    end
    rightParts[#rightParts + 1] = "Basket: " .. basketCount
    editorFrame.infoLine1Right:SetText("|cFF94a3b8" .. table.concat(rightParts, "  |  ") .. "|r")

    -- Line 2: decor note if available
    local noteText = ""
    if HDG_DecorNotes and HDG_DecorNotes[itemID] then
        noteText = HDG_DecorNotes[itemID].n or ""
    end
    editorFrame.infoLine2:SetText("|cFF94a3b8" .. noteText .. "|r")
end

function StylesTab:SaveEditor()
    if not workingDef then return end

    -- Validate name
    local name = workingDef.displayName
    if not name or name == "" then
        print("|cFFFF4444[HDG]|r " .. L["STYLE_ERR_NAME_EMPTY"])
        return
    end

    if editorMode == "curated" then
        -- CURATED: save only basket items, no tag definition
        if not next(editorLockedItems) then
            print("|cFFFF4444[HDG]|r " .. (L["STYLE_ERR_NO_CURATED_ITEMS"] or "Add at least one item to your curated style."))
            return
        end
        workingDef.tier = "curated"
        workingDef.curatedItems = {}
        for id in pairs(editorLockedItems) do workingDef.curatedItems[id] = true end
        workingDef.query = nil
        workingDef.boost = nil
        workingDef.anti = nil
        workingDef.lockedItems = nil
        workingDef.removedItems = nil
    else
        -- SMART: existing tag-based save
        RebuildWorkingDefFromTags()
        workingDef.lockedItems = CopySetTable(editorLockedItems)
        workingDef.removedItems = CopySetTable(editorRemovedItems)

        local hasQuery = workingDef.query and next(workingDef.query)
        local hasBoost = workingDef.boost and next(workingDef.boost)
        if not hasQuery and not hasBoost then
            print("|cFFFF4444[HDG]|r " .. L["STYLE_ERR_NO_TAGS"])
            return
        end
    end

    -- Check max custom styles
    local SE = CONFIG.STYLE_EDITOR
    if not editingKey then
        local customs = HDG.Store:GetState().customStyles
        local count = 0
        for _ in pairs(customs) do count = count + 1 end
        if count >= SE.MAX_CUSTOM_STYLES then
            print("|cFFFF4444[HDG]|r " .. string.format(L["STYLE_ERR_MAX_REACHED"], SE.MAX_CUSTOM_STYLES))
            return
        end
    end

    -- Save
    local key = HDG.Store:SaveCustomStyle(editingKey, workingDef)
    if StyleEngine then
        StyleEngine:InvalidateCustomStyle(key)
    end

    print("|cFF00FF00[HDG]|r " .. string.format(L["STYLE_MSG_SAVED"], name))
    StylesTab:SetSelectedStyle(nil)
    self:ShowLanding()
end
