-- HDG_StylesTab.lua
-- Style tab UI: landing page (style card grid) + detail view (item list by tier).

local addonName, privateDB = ...

local HDG = _G.HousingDecorGuide
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 {}

-- Module namespace
local StylesTab = {}
privateDB.HDG_StylesTab = StylesTab

StylesTab.initialized = false

-- Local references (set during Init)
local CONFIG
local mainFrame
local StyleEngine

-- Deep copy utility (must be defined early, used by card buttons + editor)
local function DeepCopyTable(orig)
    if type(orig) ~= "table" then return orig end
    local copy = {}
    for k, v in pairs(orig) do
        copy[k] = DeepCopyTable(v)
    end
    return copy
end

-- UI frames
local panel           -- main tab panel
local landingFrame    -- card grid view
local detailFrame     -- single style detail view
local editorFrame     -- style editor view

-- Serializer reference (set during Init)
local StyleSerializer

-- State
local selectedStyle = nil    -- current style ID in detail view
local selectedTier = "first" -- "first", "second", "neutral", "clash"
local selectedItemID = nil   -- currently selected item in list
local searchText = ""
local searchDebounceTimer = nil
local uncollectedOnly = false
local craftableOnly = false
local hideUnreleased = true
local function IsItemHidden(itemID)
    if not hideUnreleased then return false end
    local itemName = HDG_Data and HDG_Data.GetCachedItemName(itemID) or ""
    if itemName == "Unreleased" then return true end
    local ade = HDG_AllDecorDB and HDG_AllDecorDB[itemID]
    if not ade then return true end
    if ade[5] and ade[5] > time() then return true end
    return false
end
local selectedRoom = nil -- nil = "All", or room key like "kitchen"
local selectedSource = nil -- nil = "All", or sourceType int like 5 (Vendor)
local selectedLandingCategory = nil -- nil = All, or tier key like "race", "mood", etc.
local selectedShoppingListId = nil -- nil = not viewing a shopping list; set = viewing SL detail

-- Collapsible tier state + pools
local collapsedTiers = {}
local tierHeaderPool = {}
local activeTierHeaders = {}

-- Detail view mode: "list" or "gallery"
local viewMode = "gallery"
local listFrame
local itemListContent

local ROW_HEIGHT = 22
local rowPool = HDG_UIHelpers.CreateRowPool(function(parent)
    local row = CreateFrame("Button", nil, parent)
    row:SetHeight(ROW_HEIGHT)
    row:SetPoint("LEFT", 0, 0)
    row:SetPoint("RIGHT", 0, 0)

    row.icon = row:CreateTexture(nil, "ARTWORK")
    row.icon:SetSize(18, 18)
    row.icon:SetPoint("LEFT", 4, 0)

    row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.nameText:SetPoint("LEFT", row.icon, "RIGHT", 4, 0)
    row.nameText:SetPoint("RIGHT", -60, 0)
    row.nameText:SetJustifyH("LEFT")
    row.nameText:SetWordWrap(false)

    row.sourceText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.sourceText:SetPoint("RIGHT", -4, 0)
    row.sourceText:SetJustifyH("RIGHT")

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

    row.selected = row:CreateTexture(nil, "BACKGROUND")
    row.selected:SetAllPoints()
    row.selected:SetColorTexture(0.2, 0.6, 0.8, 0.15)
    row.selected:Hide()

    return row
end)

-- Subcategory button pool (for thematic styles)
local subcatButtons = {}

-- Source-type filter buttons (for thematic styles, replaces room buttons)
local sourceButtons = {}

local SOURCE_FILTER_DEFS = {
    { key = nil,         label = "All" },
    { key = 5,           label = "Vendor" },
    { key = 6,           label = "Craft" },
    { key = 2,           label = "Quest" },
    { key = 3,           label = "WQ" },
    { key = 4,           label = "Drop" },
    { key = 1,           label = "Achieve" },
    { key = 10,          label = "Promo" },
    { key = 9,           label = "Treasure" },
    { key = "rep",       label = "Rep" },
    { key = "endeavor",  label = "Endeavor" },
}

-- Cross-reference filters (check VendorDB, not sourceType)
local function ItemHasRepRequirement(itemID)
    local npcIDs = HDG_VendorByItem and HDG_VendorByItem[itemID]
    if not npcIDs or #npcIDs == 0 then return false end
    for _, npcID in ipairs(npcIDs) do
        local vendor = HDG_VendorDB[npcID]
        if vendor and vendor.items and vendor.items[itemID] then
            local itemData = vendor.items[itemID]
            if itemData[2] and itemData[2] > 0 then return true end
        end
    end
    return false
end

local ENDEAVOR_CURRENCY_ID = 3363
local function ItemCostsEndeavor(itemID)
    local npcIDs = HDG_VendorByItem and HDG_VendorByItem[itemID]
    if not npcIDs or #npcIDs == 0 then return false end
    for _, npcID in ipairs(npcIDs) do
        local ie = HDG_VendorDB[npcID] and HDG_VendorDB[npcID].items and HDG_VendorDB[npcID].items[itemID]
        local costs = ie and ie[5]
        if costs then
            for _, t in ipairs(costs) do
                if t[1] == "c" and t[2] == ENDEAVOR_CURRENCY_ID then return true end
            end
        end
    end
    return false
end

-- Lookup style def from engine's merged table (built-in + custom)
local function GetStyleDef(styleId)
    if StyleEngine and StyleEngine.GetAllStyleDefs then
        local defs = StyleEngine:GetAllStyleDefs()
        return defs and defs[styleId]
    end
    return GetStyleDef(styleId)
end

-- Gallery cell pool
local cardGridFrame, cardGridScroll, cardGridContent
local styleCellPool = { all = {}, active = {}, free = {}, timers = {} }
local ReleaseAllStyleCells -- forward declaration

-- ============================================================================
-- INIT / LIFECYCLE
-- ============================================================================

function StylesTab:Init(cfg, mf, pdb)
    CONFIG = cfg or privateDB.CONFIG
    mainFrame = mf
    StyleEngine = privateDB.HDG_StyleEngine
    StyleSerializer = privateDB.HDG_StyleSerializer
    StylesTab.initialized = true

    -- Load persisted collapse state, defaulting all to collapsed
    if HDG_Settings and type(HDG_Settings.stylesCollapsed) == "table" and HDG_Settings.stylesCollapsed._initialized then
        for k, v in pairs(HDG_Settings.stylesCollapsed) do
            if k ~= "_initialized" then collapsedTiers[k] = v end
        end
    else
        -- First time: default all sections to collapsed
        collapsedTiers["_shoppinglist"] = true
        if HDG_StyleTiers then
            for _, ti in ipairs(HDG_StyleTiers) do
                collapsedTiers[ti.key] = true
            end
        end
        if HDG_Settings then
            local saved = { _initialized = true }
            for k, v in pairs(collapsedTiers) do saved[k] = v end
            HDG_Settings.stylesCollapsed = saved
        end
    end
end

function StylesTab:Show()
    if not panel then self:Create() end
    -- Ensure cache is built on first show
    if StyleEngine and not StyleEngine:IsCacheReady() then
        StyleEngine:BuildCache()
    end
    if selectedShoppingListId then
        self:ShowShoppingListDetail(selectedShoppingListId)
    elseif selectedStyle then
        self:ShowDetail(selectedStyle)
    else
        self:ShowLanding()
    end
    panel:Show()
end

function StylesTab:Hide()
    if panel then panel:Hide() end
end

function StylesTab:Refresh()
    if not panel or not panel:IsShown() then return end
    if selectedShoppingListId then
        self:RefreshShoppingListDetail()
    elseif selectedStyle then
        self:RefreshDetail()
    else
        self:RefreshLanding()
    end
end

-- ============================================================================
-- CREATE (called once)
-- ============================================================================

function StylesTab:Create()
    local scheme = GetScheme()

    -- Main panel using TabPanel factory
    local layout = HDG.UI:TabPanel(mainFrame, {
        hasHeaderBar = false,
        hasScroll = false,
    })
    panel = layout.frame
    panel:Hide()

    -- Landing frame (card grid)
    landingFrame = CreateFrame("Frame", nil, panel)
    landingFrame:SetAllPoints()

    -- Detail frame (style browser)
    detailFrame = CreateFrame("Frame", nil, panel)
    detailFrame:SetAllPoints()
    detailFrame:Hide()

    self:CreateLanding()
    self:CreateDetail()
end

-- ============================================================================
-- LANDING PAGE (style card grid)
-- ============================================================================

local cardFrames = {}
local landingScroll, landingContent
local landingCatButtons = {}
local miniPreviewCache = {}
local miniPreviewDirty = true

local LANDING_CAT_DEFS = {
    { key = nil,            label = "All" },
    { key = "shoppinglist", label = "Shopping" },
    { key = "race",         label = "Races" },
    { key = "mood",         label = "Moods" },
    { key = "class",        label = "Classes" },
    { key = "expansion",    label = "Expansions" },
    { key = "thematic",     label = "Thematic" },
    { key = "custom",       label = "My Styles" },
    { key = "snapshot",     label = "Snapshots" },
}

-- ============================================================================
-- TIER HEADER POOL (collapsible section headers, mirrors TrainersTab pattern)
-- ============================================================================

local function AcquireTierHeader(parent)
    local header = table.remove(tierHeaderPool)
    if not header then
        header = CreateFrame("Button", nil, parent, "BackdropTemplate")
        header:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            edgeSize = 1,
            tile = false,
            insets = { left = 0, right = 0, top = 0, bottom = 0 },
        })

        header.collapseIcon = CreateFrame("Button", nil, header)
        header.collapseIcon:SetSize(16, 16)
        header.collapseIcon:SetPoint("LEFT", 6, 0)
        header.collapseIcon:SetNormalAtlas("common-button-collapseExpand-down")
        header.collapseIcon:SetPushedAtlas("common-button-collapseExpand-down-pressed")
        header.collapseIcon:SetHighlightAtlas("common-button-collapseExpand-hover")

        header.titleText = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        header.titleText:SetPoint("LEFT", 26, 0)
        header.titleText:SetShadowOffset(0, 0)

        header.countText = header:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        header.countText:SetPoint("RIGHT", -10, 0)
        header.countText:SetShadowOffset(0, 0)
    end

    header:SetParent(parent)
    header:Show()
    header:ClearAllPoints()
    table.insert(activeTierHeaders, header)
    return header
end

local function ReleaseTierHeaders()
    for _, h in ipairs(activeTierHeaders) do
        h:Hide()
        h:ClearAllPoints()
        h:SetScript("OnClick", nil)
        h:SetScript("OnEnter", nil)
        h:SetScript("OnLeave", nil)
        h.collapseIcon:SetScript("OnClick", nil)
        table.insert(tierHeaderPool, h)
    end
    activeTierHeaders = {}
end

-- ============================================================================
-- SHOPPING LIST CARD POOL
-- ============================================================================

-- (Shopping list cards reuse CreateCard on landing; detail view uses gallery cells)

function StylesTab:CreateLanding()
    local scheme = GetScheme()

    -- Header
    local header = landingFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    header:SetPoint("TOPLEFT", 10, -8)
    if scheme then ApplyFont(header, scheme, "header") end
    header:SetText(L["STYLE_BROWSE_HEADER"] .. " |cFF888888" .. L["STYLE_BETA"] .. "|r")
    landingFrame.header = header

    -- Style Creator button (top-right)
    local creatorBtn = HDG_UIHelpers.CreateChipButton(landingFrame, "", 60, false, nil)
    creatorBtn.text:SetText(L["STYLE_CREATOR"])
    creatorBtn:SetWidth(creatorBtn.text:GetStringWidth() + 20)
    creatorBtn:SetPoint("TOPRIGHT", -28, -8)
    creatorBtn:SetScript("OnClick", function()
        StylesTab:ShowEditor(nil)
    end)
    landingFrame.creatorBtn = creatorBtn

    -- Import button (next to creator)
    local importBtn = HDG_UIHelpers.CreateChipButton(landingFrame, "", 60, false, nil)
    importBtn.text:SetText(L["STYLE_IMPORT"])
    importBtn:SetWidth(importBtn.text:GetStringWidth() + 16)
    importBtn:SetPoint("RIGHT", creatorBtn, "LEFT", -4, 0)
    importBtn:SetScript("OnClick", function()
        StylesTab:ShowImportDialog()
    end)
    landingFrame.importBtn = importBtn

    -- Snapshot button (captures all placed decor account-wide)
    local snapshotBtn = HDG_UIHelpers.CreateChipButton(landingFrame, "", 60, false, nil)
    snapshotBtn.text:SetText("Save Placed Decor")
    snapshotBtn:SetWidth(snapshotBtn.text:GetStringWidth() + 20)
    snapshotBtn:SetPoint("RIGHT", importBtn, "LEFT", -4, 0)
    snapshotBtn:SetScript("OnClick", function()
        StylesTab:TakeSnapshot()
    end)
    landingFrame.snapshotBtn = snapshotBtn

    -- Category switcher tabs
    local catX = 10
    for _, cd in ipairs(LANDING_CAT_DEFS) do
        local btn = HDG_UIHelpers.CreateChipButton(landingFrame, "", 60, false, nil)
        btn.text:SetText(cd.label)
        btn:SetWidth(btn.text:GetStringWidth() + 16)
        btn:SetPoint("TOPLEFT", catX, -30)
        btn.catKey = cd.key
        btn:SetScript("OnClick", function()
            selectedLandingCategory = cd.key
            for _, b in ipairs(landingCatButtons) do
                HDG_UIHelpers.UpdateChipButtonState(b, b.catKey == selectedLandingCategory)
            end
            self:RefreshLanding()
        end)
        HDG_UIHelpers.UpdateChipButtonState(btn, cd.key == selectedLandingCategory)
        landingCatButtons[#landingCatButtons + 1] = btn
        catX = catX + btn:GetWidth() + 2
    end

    -- Scroll frame for cards (shifted down for tab row)
    landingScroll = CreateFrame("ScrollFrame", nil, landingFrame, "UIPanelScrollFrameTemplate")
    landingScroll:SetPoint("TOPLEFT", 4, -56)
    landingScroll:SetPoint("BOTTOMRIGHT", -24, 4)
    HDG_UIHelpers.StyleScrollBar(landingScroll)
    RegisterWidget(landingScroll, "ScrollFrame")

    landingContent = CreateFrame("Frame", nil, landingScroll)
    landingContent:SetWidth(landingScroll:GetWidth() or 600)
    landingContent:SetHeight(1) -- will be set dynamically
    landingScroll:SetScrollChild(landingContent)

    -- Resize content width when scroll resizes
    landingScroll:HookScript("OnSizeChanged", function(_, w)
        landingContent:SetWidth(w)
        self:RefreshLanding()
    end)
end

-- Helper: render a collapsible tier header and return the new yOffset
local TIER_HEADER_H = 28
local function RenderTierHeader(parent, scheme, yOffset, contentWidth, tierKey, title, cardCount)
    local header = AcquireTierHeader(parent)
    header:SetSize(contentWidth, TIER_HEADER_H)
    header:SetPoint("TOPLEFT", 0, yOffset)

    local isCollapsed = collapsedTiers[tierKey]
    if isCollapsed then
        header.collapseIcon:SetNormalAtlas("common-button-collapseExpand-up")
        header.collapseIcon:SetPushedAtlas("common-button-collapseExpand-up-pressed")
    else
        header.collapseIcon:SetNormalAtlas("common-button-collapseExpand-down")
        header.collapseIcon:SetPushedAtlas("common-button-collapseExpand-down-pressed")
    end

    if scheme then
        header:SetBackdropColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.95)
        header:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, 0.7)
        RegisterWidget(header, "Button")
        ApplyFont(header.titleText, scheme, "body")
        header.titleText:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b, 1)
        ApplyFont(header.countText, scheme, "small")
    else
        header:SetBackdropColor(0.12, 0.12, 0.15, 0.95)
        header:SetBackdropBorderColor(unpack(HDG_UIHelpers.BORDER_COLOR_DIM))
    end

    header.titleText:SetText(title)
    header.countText:SetText(HDG.Theme.C_Dim() .. cardCount .. " items|r")

    local function toggleCollapse()
        collapsedTiers[tierKey] = not collapsedTiers[tierKey]
        StylesTab:RefreshLanding()
    end
    header:SetScript("OnClick", toggleCollapse)
    header.collapseIcon:SetScript("OnClick", toggleCollapse)

    header:SetScript("OnEnter", function(self)
        if scheme then
            self:SetBackdropColor(scheme.button_hover.r, scheme.button_hover.g, scheme.button_hover.b, 0.95)
        end
    end)
    header:SetScript("OnLeave", function(self)
        if scheme then
            self:SetBackdropColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.95)
        end
    end)

    return yOffset - TIER_HEADER_H - 4
end

-- Helper: render shopping list cards using the same UpdateCard path as snapshots
local function RenderShoppingListCards(self, parent, scheme, yOffset, CARD_W, CARD_H, CARD_GAP, cardsPerRow, cardIdx)
    if not HDG.Store then return cardIdx, 0, yOffset end
    local lists = HDG.Store:GetShoppingLists()
    if #lists == 0 then return cardIdx, 0, yOffset end

    local activeId = HDG.Store.state and HDG.Store.state.activeShoppingListId
    local allLists = HDG.Store.state and HDG.Store.state.vendorShoppingLists
    local col = 0

    for _, listInfo in ipairs(lists) do
        cardIdx = cardIdx + 1
        local card = cardFrames[cardIdx]
        if not card or not card.icon then
            card = self:CreateCard(parent)
            cardFrames[cardIdx] = card
        end
        local xOff = 4 + col * (CARD_W + CARD_GAP)
        card:ClearAllPoints()
        card:SetPoint("TOPLEFT", xOff, yOffset)
        card:SetSize(CARD_W, CARD_H)

        -- Build synthetic def to pass through UpdateCard (snapshot-style)
        local listData = allLists and allLists[listInfo.id]
        local items = listData and listData.items or {}
        local isActive = (listInfo.id == activeId)
        local syntheticKey = "_sl_" .. listInfo.id
        local listMeta = listData and listData.meta
        local slIcon, slIconAtlas
        if listMeta and listMeta.source == "wowhead" then
            slIcon = "Interface\\AddOns\\HousingDecorGuide\\textures\\wowhead_logo"
        else
            slIconAtlas = "decor-controls-decoratemode-active"
        end
        local syntheticDef = {
            displayName = listInfo.name or "Shopping List",
            description = isActive and "Active list" or "",
            icon = slIcon,
            iconAtlas = slIconAtlas,
            tier = "snapshot",
            color = isActive and (scheme and scheme.accent or { r = 0.2, g = 0.8, b = 0.6 })
                or { r = 1, g = 1, b = 1 },
            snapshot = { items = {}, totalPlacementCost = 0 },
            meta = listMeta,
        }
        local syntheticStats = { collected = listInfo.count, total = listInfo.count }

        -- Pre-populate miniPreviewCache with up to 6 item icons
        local picks = {}
        for _, entry in ipairs(items) do
            local itemID = entry.itemID or entry.id
            local ade = HDG_AllDecorDB and HDG_AllDecorDB[itemID]
            if ade then
                local isCollected = HDG_Data and HDG_Data.IsItemCollected(itemID) or false
                picks[#picks + 1] = { itemID = itemID, decorID = ade[1], isCollected = isCollected }
                if #picks >= 6 then break end
            end
        end
        miniPreviewCache[syntheticKey] = picks

        self:UpdateCard(card, syntheticKey, syntheticDef, syntheticStats, scheme)

        -- Hide action buttons (UpdateCard shows export/delete for snapshots)
        for _, ab in pairs(card.actionBtns) do ab:Hide() end

        -- Override border for active list accent
        if scheme and isActive then
            card:SetBackdropBorderColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.9)
        end

        -- Override count text to show item count instead of snapshot format
        card.countText:SetText(listInfo.count .. " items")

        -- Override click to open shopping list detail
        local listId = listInfo.id
        card:SetScript("OnClick", function()
            StylesTab:ShowShoppingListDetail(listId)
        end)
        card.styleId = nil

        card:Show()

        col = col + 1
        if col >= cardsPerRow then
            col = 0
            yOffset = yOffset - (CARD_H + CARD_GAP)
        end
    end

    return cardIdx, col, yOffset
end

function StylesTab:RefreshLanding()
    if not landingContent then return end
    if not StyleEngine or not StyleEngine:IsCacheReady() then return end

    local scheme = GetScheme()

    -- Hide existing cards + release pools
    for _, card in ipairs(cardFrames) do
        card:Hide()
    end
    ReleaseTierHeaders()

    local CARD_W = 260
    local CARD_H = 160
    local CARD_GAP = 8
    local SECTION_GAP = 20
    local contentWidth = landingContent:GetWidth() - 8
    local cardsPerRow = math.max(1, math.floor((contentWidth + CARD_GAP) / (CARD_W + CARD_GAP)))
    local yOffset = -4
    local cardIdx = 0

    if miniPreviewDirty then
        wipe(miniPreviewCache)
        miniPreviewDirty = false
    end

    local statsCache = {}
    for _, styleId in ipairs(HDG_StyleOrder) do
        statsCache[styleId] = StyleEngine:GetStyleCollectionStats(styleId, hideUnreleased)
    end
    if HDG.Store then
        local customStyles, customOrder = HDG.Store:GetCustomStyles()
        if customOrder then
            for _, styleId in ipairs(customOrder) do
                statsCache[styleId] = StyleEngine:GetStyleCollectionStats(styleId, hideUnreleased)
            end
        end
    end

    local showHeaders = (selectedLandingCategory == nil)
    local isShoppingFilter = (selectedLandingCategory == "shoppinglist")

    -- Shopping Lists section (shown in "All" view or "Shopping" filter)
    if showHeaders or isShoppingFilter then
        local slLists = HDG.Store and HDG.Store:GetShoppingLists() or {}
        if #slLists > 0 then
            if showHeaders then
                yOffset = RenderTierHeader(landingContent, scheme, yOffset, contentWidth, "_shoppinglist", "Shopping Lists", #slLists)
            end
            if not collapsedTiers["_shoppinglist"] or isShoppingFilter then
                local slCol
                cardIdx, slCol, yOffset = RenderShoppingListCards(self, landingContent, scheme, yOffset, CARD_W, CARD_H, CARD_GAP, cardsPerRow, cardIdx)
                if slCol > 0 then
                    yOffset = yOffset - (CARD_H + CARD_GAP)
                end
            end
            yOffset = yOffset - SECTION_GAP
        end
    end

    -- Skip style tiers when in shopping-only filter
    if not isShoppingFilter then
        for _, tierInfo in ipairs(HDG_StyleTiers) do
            -- Skip tiers not matching filter
            if selectedLandingCategory and tierInfo.key ~= selectedLandingCategory then
                -- skip this tier entirely
            else
                -- For custom/snapshot tiers, get cards from Store
                local isCustomTier = (tierInfo.key == "custom")
                local isSnapshotTier = (tierInfo.key == "snapshot")
                local customStyles, customOrder
                if (isCustomTier or isSnapshotTier) and HDG.Store then
                    customStyles, customOrder = HDG.Store:GetCustomStyles()
                end

                -- Count cards for this tier (for header display)
                local tierCardCount = 0
                if isCustomTier or isSnapshotTier then
                    local filterTier = isSnapshotTier and "snapshot" or "custom"
                    if customOrder then
                        for _, styleId in ipairs(customOrder) do
                            local def = customStyles[styleId]
                            if def and def.tier == filterTier then tierCardCount = tierCardCount + 1 end
                        end
                    end
                else
                    for _, styleId in ipairs(HDG_StyleOrder) do
                        local def = HDG_StyleDefinitions[styleId]
                        if def and def.tier == tierInfo.key then tierCardCount = tierCardCount + 1 end
                    end
                end

                -- Collapsible section header (only when showing all categories)
                if showHeaders then
                    yOffset = RenderTierHeader(landingContent, scheme, yOffset, contentWidth, tierInfo.key, tierInfo.name, tierCardCount)
                end

                -- Skip card rendering if collapsed
                if collapsedTiers[tierInfo.key] and showHeaders then
                    yOffset = yOffset - SECTION_GAP
                else
                    -- Cards for this tier
                    local col = 0

                    if isCustomTier or isSnapshotTier then
                        -- Custom/snapshot styles from Store
                        local filterTier = isSnapshotTier and "snapshot" or "custom"
                        if customOrder then
                            for _, styleId in ipairs(customOrder) do
                                local def = customStyles[styleId]
                                if def and def.tier == filterTier then
                                    cardIdx = cardIdx + 1
                                    local card = cardFrames[cardIdx]
                                    if not card or not card.icon then
                                        card = self:CreateCard(landingContent)
                                        cardFrames[cardIdx] = card
                                    end
                                    local xOff = 4 + col * (CARD_W + CARD_GAP)
                                    card:ClearAllPoints()
                                    card:SetPoint("TOPLEFT", xOff, yOffset)
                                    card:SetSize(CARD_W, CARD_H)
                                    self:UpdateCard(card, styleId, def, statsCache[styleId], scheme)
                                    card:Show()
                                    col = col + 1
                                    if col >= cardsPerRow then
                                        col = 0
                                        yOffset = yOffset - (CARD_H + CARD_GAP)
                                    end
                                end
                            end
                        end

                        -- Action card: "+ New Style" for custom, "+ Take Snapshot" for snapshot
                        cardIdx = cardIdx + 1
                        local newCard = cardFrames[cardIdx]
                        if not newCard or not newCard.icon then
                            newCard = self:CreateCard(landingContent)
                            cardFrames[cardIdx] = newCard
                        end
                        local xOff = 4 + col * (CARD_W + CARD_GAP)
                        newCard:ClearAllPoints()
                        newCard:SetPoint("TOPLEFT", xOff, yOffset)
                        newCard:SetSize(CARD_W, CARD_H)
                        newCard.icon:SetTexture("Interface\\PaperDollInfoFrame\\Character-Plus")
                        if isSnapshotTier then
                            newCard.nameText:SetText("|cFF94a3b8+ Save Placed Decor|r")
                            newCard.descText:SetText("|cFF94a3b8Snapshot all placed decor|r")
                        else
                            newCard.nameText:SetText("|cFF94a3b8" .. L["STYLE_NEW"] .. "|r")
                            newCard.descText:SetText("|cFF94a3b8" .. L["STYLE_NEW_DESC"] .. "|r")
                        end
                        newCard.countText:SetText("")
                        for i = 1, 6 do newCard.previewIcons[i]:Hide() end
                        newCard.progressBg:SetAlpha(0)
                        newCard.progressFill:SetWidth(1)
                        newCard.isCompleted = false
                        if scheme then
                            newCard:SetBackdropBorderColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 0.4)
                        end
                        newCard.styleId = nil
                        for _, ab in pairs(newCard.actionBtns) do ab:Hide() end
                        newCard:RegisterForClicks("LeftButtonUp")
                        newCard:SetScript("OnClick", function()
                            if isSnapshotTier then
                                StylesTab:TakeSnapshot()
                            else
                                StylesTab:ShowEditor(nil)
                            end
                        end)
                        newCard:Show()
                        col = col + 1
                        if col >= cardsPerRow then
                            col = 0
                            yOffset = yOffset - (CARD_H + CARD_GAP)
                        end
                    else
                        -- Built-in styles from HDG_StyleOrder
                        for _, styleId in ipairs(HDG_StyleOrder) do
                            local def = HDG_StyleDefinitions[styleId]
                            if def and def.tier == tierInfo.key then
                                cardIdx = cardIdx + 1
                                local card = cardFrames[cardIdx]
                                if not card or not card.icon then
                                    card = self:CreateCard(landingContent)
                                    cardFrames[cardIdx] = card
                                end
                                local xOff = 4 + col * (CARD_W + CARD_GAP)
                                card:ClearAllPoints()
                                card:SetPoint("TOPLEFT", xOff, yOffset)
                                card:SetSize(CARD_W, CARD_H)
                                self:UpdateCard(card, styleId, def, statsCache[styleId], scheme)
                                card:Show()
                                col = col + 1
                                if col >= cardsPerRow then
                                    col = 0
                                    yOffset = yOffset - (CARD_H + CARD_GAP)
                                end
                            end
                        end
                    end

                    if col > 0 then
                        yOffset = yOffset - (CARD_H + CARD_GAP)
                    end
                    yOffset = yOffset - SECTION_GAP
                end
            end
        end
    end

    landingContent:SetHeight(math.abs(yOffset) + 20)

    -- Persist collapse state
    if HDG_Settings then
        local saved = { _initialized = true }
        for k, v in pairs(collapsedTiers) do saved[k] = v end
        HDG_Settings.stylesCollapsed = saved
    end
end

function StylesTab:CreateCard(parent)
    local card = CreateFrame("Button", nil, parent, "BackdropTemplate")
    card:SetSize(260, 160)

    local scheme = GetScheme()
    HDG_UIHelpers.ApplyBackdrop(card, "panel",
        {scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.8},
        {scheme.border.r, scheme.border.g, scheme.border.b, 0.5})
    RegisterWidget(card, "Panel")

    -- Atlas background for visual texture
    card.bgTex = card:CreateTexture(nil, "BACKGROUND", nil, 1)
    card.bgTex:SetPoint("TOPLEFT", 3, -3)
    card.bgTex:SetPoint("BOTTOMRIGHT", -3, 3)
    card.bgTex:SetAtlas("ui-frame-midnight-cardparchmentwider")
    card.bgTex:SetAlpha(0.3)

    -- Icon (larger)
    card.icon = card:CreateTexture(nil, "ARTWORK")
    card.icon:SetSize(48, 48)
    card.icon:SetPoint("TOPLEFT", 10, -10)

    -- Action buttons (top-right inside card, stacked vertically, atlas tab style)
    local btnH = 16
    local btnGap = 2
    card.actionBtns = {}
    local actionDefs = {
        { key = "copyEdit", label = "C", tip = "Copy + Edit" },
        { key = "edit",     label = "E", tip = "Edit" },
        { key = "export",   label = "X", tip = "Export" },
        { key = "delete",   label = "D", tip = "Delete" },
    }
    for i, ad in ipairs(actionDefs) do
        local ab = HDG_UIHelpers.CreateChipButton(card, "", 60, false, nil)
        ab.text:SetText(ad.label)
        ab:SetWidth(ab.text:GetStringWidth() + 12)
        ab:SetPoint("TOPRIGHT", -10, -10 - (i - 1) * (btnH + btnGap))
        ab:SetFrameLevel(card:GetFrameLevel() + 5)
        ab.tipText = ad.tip
        ab:SetScript("OnEnter", function(self)
            self.highlight:Show()
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:AddLine(self.tipText, 1, 1, 1)
            GameTooltip:Show()
        end)
        ab:SetScript("OnLeave", function(self)
            self.highlight:Hide()
            GameTooltip:Hide()
        end)
        ab:Hide()
        card.actionBtns[ad.key] = ab
    end

    -- Name
    card.nameText = card:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    card.nameText:SetPoint("TOPLEFT", card.icon, "TOPRIGHT", 8, -2)
    card.nameText:SetPoint("RIGHT", -28, 0)
    card.nameText:SetJustifyH("LEFT")
    card.nameText:SetWordWrap(false)

    -- Count
    card.countText = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    card.countText:SetPoint("TOPLEFT", card.icon, "TOPRIGHT", 8, -22)
    card.countText:SetJustifyH("LEFT")

    -- Placement cost (snapshots only, right-aligned on count line)
    card.costText = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    card.costText:SetPoint("RIGHT", card, "RIGHT", -10, 0)
    card.costText:SetPoint("TOP", card.countText, "TOP", 0, 0)
    card.costText:SetPoint("BOTTOM", card.countText, "BOTTOM", 0, 0)
    card.costText:SetJustifyH("RIGHT")
    card.costText:Hide()

    -- Description (new — room for it now)
    card.descText = card:CreateFontString(nil, "OVERLAY", "GameFontHighlightExtraSmall")
    card.descText:SetPoint("TOPLEFT", 10, -68)
    card.descText:SetPoint("RIGHT", -10, 0)
    card.descText:SetJustifyH("LEFT")
    card.descText:SetWordWrap(true)
    card.descText:SetMaxLines(3)

    -- Mini icon preview strip (6 icons between description and progress bar)
    card.previewStrip = CreateFrame("Frame", nil, card)
    card.previewStrip:SetPoint("TOPLEFT", 10, -108)
    card.previewStrip:SetSize(250, 34)
    card.previewIcons = {}
    for i = 1, 6 do
        local tex = card.previewStrip:CreateTexture(nil, "ARTWORK")
        tex:SetSize(34, 34)
        if i == 1 then
            tex:SetPoint("LEFT", 0, 0)
        else
            tex:SetPoint("LEFT", card.previewIcons[i - 1], "RIGHT", 3, 0)
        end
        tex:Hide()
        card.previewIcons[i] = tex
    end

    -- Progress bar background
    card.progressBg = card:CreateTexture(nil, "ARTWORK")
    card.progressBg:SetPoint("BOTTOMLEFT", 10, 12)
    card.progressBg:SetPoint("BOTTOMRIGHT", -10, 12)
    card.progressBg:SetHeight(8)
    card.progressBg:SetColorTexture(0, 0, 0, 0.4)

    -- Progress bar fill
    card.progressFill = card:CreateTexture(nil, "OVERLAY")
    card.progressFill:SetPoint("BOTTOMLEFT", 10, 12)
    card.progressFill:SetHeight(8)
    card.progressFill:SetColorTexture(0.2, 0.8, 0.6, 0.8)

    -- Hover effect
    card:SetScript("OnEnter", function(self)
        local s = self._scheme
        if s then
            card:SetBackdropBorderColor(s.accent.r, s.accent.g, s.accent.b, 1)
            card.bgTex:SetAlpha(0.45)
        end
    end)
    card:SetScript("OnLeave", function(self)
        if card.isCompleted then
            card:SetBackdropBorderColor(1, 0.84, 0, 1)
        else
            local s = self._scheme
            if s then
                card:SetBackdropBorderColor(s.border.r, s.border.g, s.border.b, 0.5)
            end
        end
        card.bgTex:SetAlpha(0.3)
    end)

    return card
end

function StylesTab:UpdateCard(card, styleId, def, stats, scheme)
    card._scheme = scheme
    -- Icon (atlas or texture path)
    if def.iconAtlas then
        card.icon:SetAtlas(def.iconAtlas)
    else
        card.icon:SetTexture(def.icon)
    end

    -- Name (use normal size for larger card)
    if scheme then ApplyFont(card.nameText, scheme, "header") end
    card.nameText:SetText(def.displayName)
    if def.color then
        card.nameText:SetTextColor(def.color.r, def.color.g, def.color.b, 1)
    end

    -- Collection stats with percentage
    local isSnapshot = (def.tier == "snapshot")
    local collStats = stats or StyleEngine:GetStyleCollectionStats(styleId, hideUnreleased)
    if scheme then ApplyFont(card.countText, scheme, "small") end
    local pct
    if isSnapshot then
        -- Snapshot: collected = unique items, total = total placed
        card.countText:SetText(collStats.collected .. " items (" .. collStats.total .. " placed)")
        pct = 0
        card.isCompleted = false
        card.countText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 1)
        card:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, 0.5)
        -- Placement cost
        local tpc = def.snapshot and def.snapshot.totalPlacementCost
        if tpc and tpc > 0 then
            if scheme then ApplyFont(card.costText, scheme, "small") end
            local costStr = HOUSING_DECOR_PLACEMENT_COST_FORMAT and HOUSING_DECOR_PLACEMENT_COST_FORMAT:format(tpc) or tostring(tpc)
            card.costText:SetText(costStr)
            card.costText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 1)
            card.costText:Show()
        else
            card.costText:Hide()
        end
    else
        card.costText:Hide()
        pct = (collStats.total > 0) and (collStats.collected / collStats.total) or 0
        local pctStr = math.floor(pct * 100)
        card.countText:SetText(collStats.collected .. " / " .. collStats.total .. " (" .. pctStr .. "%)")
        -- Gold border + flavor text for 100% complete styles
        card.isCompleted = (collStats.total > 0 and collStats.collected >= collStats.total)
        if card.isCompleted then
            local fl = scheme.text_flavor or {r=0.85, g=0.70, b=0.30, a=1}
            card.countText:SetTextColor(fl.r, fl.g, fl.b, 1)
            card:SetBackdropBorderColor(1, 0.84, 0, 1)
        else
            card.countText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 1)
            card:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, 0.5)
        end
    end

    -- Description
    if scheme then ApplyFont(card.descText, scheme, "xsmall") end
    card.descText:SetText(def.description or "")
    local fl = scheme.text_flavor or {r=0.85, g=0.70, b=0.30, a=0.90}
    card.descText:SetTextColor(fl.r, fl.g, fl.b, fl.a)

    -- Attribution row (shopping list imports with meta)
    if def.meta and def.meta.source then
        if not card.attrRow then
            card.attrRow = CreateFrame("Frame", nil, card)
            card.attrRow:SetPoint("TOPLEFT", 10, -86)
            card.attrRow:SetPoint("RIGHT", -10, 0)
            card.attrRow:SetHeight(18)
            card.attrIcon = card.attrRow:CreateTexture(nil, "ARTWORK")
            card.attrIcon:SetSize(16, 16)
            card.attrIcon:SetPoint("LEFT", 0, 0)
            card.attrLabel = card.attrRow:CreateFontString(nil, "OVERLAY", "GameFontHighlightExtraSmall")
            card.attrLabel:SetPoint("LEFT", card.attrIcon, "RIGHT", 4, 0)
            card.attrLabel:SetPoint("RIGHT", -4, 0)
            card.attrLabel:SetJustifyH("LEFT")
            card.attrCopyBtn = CreateFrame("Button", nil, card.attrRow)
            card.attrCopyBtn:SetAllPoints(card.attrRow)
            card.attrCopyBtn:RegisterForClicks("LeftButtonUp")
        end
        local m = def.meta
        if m.source == "wowhead" then
            card.attrIcon:SetTexture("Interface\\AddOns\\HousingDecorGuide\\textures\\wowhead_logo")
        elseif m.source == "player" then
            card.attrIcon:SetAtlas("socialqueuing-icon-eye")
        else
            card.attrIcon:SetTexture("Interface\\Icons\\INV_Misc_Note_01")
        end
        local label = m.source == "wowhead" and "Wowhead" or m.author or m.source or "Imported"
        if m.date and m.date ~= "" then
            local ts = tonumber(m.date)
            label = label .. "  " .. (ts and date("%Y-%m-%d", ts) or m.date)
        end
        if scheme then ApplyFont(card.attrLabel, scheme, "xsmall") end
        card.attrLabel:SetText(HDG.Theme.C_Dim() .. label .. "|r")
        if m.url then
            card.attrCopyBtn:SetScript("OnClick", function()
                CopyToClipboard(m.url)
                print("|cFF00FF00[HDG]|r URL copied to clipboard")
            end)
            card.attrCopyBtn:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_TOP")
                GameTooltip:AddLine("Click to copy URL", 1, 1, 1)
                GameTooltip:Show()
            end)
            card.attrCopyBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
        else
            card.attrCopyBtn:SetScript("OnClick", nil)
            card.attrCopyBtn:SetScript("OnEnter", nil)
            card.attrCopyBtn:SetScript("OnLeave", nil)
        end
        card.attrRow:Show()
    elseif card.attrRow then
        card.attrRow:Hide()
    end

    -- Mini icon preview strip
    self:UpdateCardIcons(card, styleId, def)

    -- Progress bar
    local barWidth = card:GetWidth() - 20
    card.progressFill:SetWidth(math.max(1, barWidth * pct))
    card.progressFill:SetColorTexture(def.color.r or 0.2, def.color.g or 0.8, def.color.b or 0.6, 0.8)

    -- Wire up action buttons
    local isCustom = def.tier == "custom"

    -- Hide all first
    for _, ab in pairs(card.actionBtns) do ab:Hide() end

    if isSnapshot then
        -- Snapshot cards: Export + Delete only
        if StyleSerializer then
            local exportBtn = card.actionBtns.export
            exportBtn:SetScript("OnClick", function()
                local encoded = StyleSerializer:ExportSnapshot(def)
                if encoded then StylesTab:ShowExportDialog(encoded, def) end
            end)
            exportBtn:Show()
        end

        local deleteBtn = card.actionBtns.delete
        deleteBtn.text:SetTextColor(0.8, 0.3, 0.3, 1)
        deleteBtn:SetScript("OnClick", function()
            StaticPopup_Show("HDG_DELETE_CUSTOM_STYLE", def.displayName or "this snapshot", nil, { key = styleId })
        end)
        deleteBtn:Show()
    else
        -- Copy+Edit (all non-snapshot styles)
        local copyBtn = card.actionBtns.copyEdit
        copyBtn:SetScript("OnClick", function()
            local dupDef = DeepCopyTable(def)
            dupDef.displayName = "Copy of " .. (def.displayName or "Style")
            dupDef.tier = "custom"
            StylesTab:ShowEditor(nil, dupDef)
        end)
        copyBtn:Show()

        if isCustom then
            -- Edit
            local editBtn = card.actionBtns.edit
            editBtn:SetScript("OnClick", function()
                StylesTab:ShowEditor(styleId)
            end)
            editBtn:Show()

            -- Export
            if StyleSerializer then
                local exportBtn = card.actionBtns.export
                exportBtn:SetScript("OnClick", function()
                    local encoded = StyleSerializer:Export(def)
                    if encoded then StylesTab:ShowExportDialog(encoded) end
                end)
                exportBtn:Show()
            end

            -- Delete
            local deleteBtn = card.actionBtns.delete
            deleteBtn.text:SetTextColor(0.8, 0.3, 0.3, 1)
            deleteBtn:SetScript("OnClick", function()
                StaticPopup_Show("HDG_DELETE_CUSTOM_STYLE", def.displayName or "this style", nil, { key = styleId })
            end)
            deleteBtn:Show()
        end
    end

    -- Click: all styles open detail view
    card:RegisterForClicks("LeftButtonUp")
    card:SetScript("OnClick", function()
        StylesTab:ShowDetail(styleId)
    end)

    card.styleId = styleId
end

function StylesTab:UpdateCardIcons(card, styleId, def)
    -- Hide all icons first
    for i = 1, 6 do card.previewIcons[i]:Hide() end

    -- Check cache
    if not miniPreviewCache[styleId] then
        local items = StyleEngine:GetStyleItems(styleId)
        local bucket
        if def.tier == "snapshot" then
            bucket = items and items.items or {}
        elseif def.subcategories then
            -- Use first subcategory
            bucket = items[def.subcategories[1].key] or {}
        else
            bucket = items.first or {}
        end

        -- Prefer collected items, pick up to 6 (skip unreleased)
        local collected, uncoll = {}, {}
        for _, item in ipairs(bucket) do
            if not IsItemHidden(item.itemID) then
                if HDG_Data and HDG_Data.IsItemCollected(item.itemID) then
                    collected[#collected + 1] = item
                else
                    uncoll[#uncoll + 1] = item
                end
                if #collected >= 6 then break end
            end
        end
        local picks = {}
        for _, item in ipairs(collected) do
            picks[#picks + 1] = { itemID = item.itemID, decorID = item.decorID, isCollected = true }
            if #picks >= 6 then break end
        end
        if #picks < 6 then
            for _, item in ipairs(uncoll) do
                picks[#picks + 1] = { itemID = item.itemID, decorID = item.decorID, isCollected = false }
                if #picks >= 6 then break end
            end
        end
        miniPreviewCache[styleId] = picks
    end

    local picks = miniPreviewCache[styleId]
    for i, pick in ipairs(picks) do
        local tex = card.previewIcons[i]
        if not tex then break end

        local iconID
        if C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID then
            local ok, catInfo = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID,
                Enum.HousingCatalogEntryType and Enum.HousingCatalogEntryType.Decor or 1,
                pick.decorID, true)
            if ok and catInfo then
                if catInfo.iconAtlas then
                    tex:SetAtlas(catInfo.iconAtlas)
                elseif catInfo.iconTexture then
                    tex:SetTexture(catInfo.iconTexture)
                else
                    tex:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
                end
            else
                tex:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
            end
        elseif C_HousingDecor and C_HousingDecor.GetDecorIcon then
            local ok, id = pcall(C_HousingDecor.GetDecorIcon, pick.decorID)
            tex:SetTexture((ok and id) or "Interface\\Icons\\INV_Misc_QuestionMark")
        else
            tex:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
        end

        tex:SetDesaturated(false)
        tex:SetAlpha(1)
        tex:Show()
    end
end

-- ============================================================================
-- DETAIL VIEW (single style browser)
-- ============================================================================

local detailHeader, detailDesc, detailBackBtn
local tierButtons = {}
local filterBar, searchBox
local roomButtons = {}
local itemListScroll, itemListContent
local detailPanel -- right-side item detail

function StylesTab:CreateDetail()
    local scheme = GetScheme()

    -- Back button
    detailBackBtn = CreateFrame("Button", nil, detailFrame, "UIPanelButtonTemplate")
    detailBackBtn:SetSize(60, 20)
    detailBackBtn:SetPoint("TOPLEFT", 6, -6)
    detailBackBtn:SetText(L["STYLE_BACK"])
    detailBackBtn:SetScript("OnClick", function()
        selectedStyle = nil
        StylesTab:ShowLanding()
    end)

    -- Style name header
    detailHeader = detailFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    detailHeader:SetPoint("LEFT", detailBackBtn, "RIGHT", 8, 0)
    if scheme then ApplyFont(detailHeader, scheme, "header") end

    -- Description
    detailDesc = detailFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightExtraSmall")
    detailDesc:SetPoint("TOPLEFT", 6, -30)
    detailDesc:SetPoint("RIGHT", -6, 0)
    detailDesc:SetJustifyH("LEFT")
    if scheme then
        ApplyFont(detailDesc, scheme, "xsmall")
        detailDesc:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 1)
    end

    -- Tier sub-tabs
    local tierDefs = {
        { 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 = 6
    for _, td in ipairs(tierDefs) do
        local btn = HDG_UIHelpers.CreateChipButton(detailFrame, "", 60, false, nil)
        btn:SetSize(95, 22)
        btn:SetPoint("TOPLEFT", tierX, -48)

        btn.text:SetText(td.label)

        btn.tierKey = td.key
        btn:SetScript("OnClick", function()
            selectedTier = td.key
            selectedItemID = nil
            self:RefreshDetail()
        end)

        tierButtons[td.key] = btn
        tierX = tierX + 99
    end

    -- Filter bar
    filterBar = CreateFrame("Frame", nil, detailFrame)
    filterBar:SetPoint("TOPLEFT", 6, -74)
    filterBar:SetPoint("RIGHT", -6, 0)
    filterBar:SetHeight(22)

    -- Uncollected checkbox
    local uncollCheck = CreateFrame("CheckButton", nil, filterBar, "UICheckButtonTemplate")
    uncollCheck:SetSize(20, 20)
    uncollCheck:SetPoint("LEFT", 0, 0)
    uncollCheck:SetChecked(uncollectedOnly)
    uncollCheck:SetScript("OnClick", function(self)
        uncollectedOnly = self:GetChecked()
        StylesTab:RefreshDetail()
    end)
    local uncollLabel = filterBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    uncollLabel:SetPoint("LEFT", uncollCheck, "RIGHT", 2, 0)
    uncollLabel:SetText("Uncollected")
    if scheme then ApplyFont(uncollLabel, scheme, "small") end

    -- Craftable checkbox
    local craftCheck = CreateFrame("CheckButton", nil, filterBar, "UICheckButtonTemplate")
    craftCheck:SetSize(20, 20)
    craftCheck:SetPoint("LEFT", uncollLabel, "RIGHT", 10, 0)
    craftCheck:SetChecked(craftableOnly)
    craftCheck:SetScript("OnClick", function(self)
        craftableOnly = self:GetChecked()
        StylesTab:RefreshDetail()
    end)
    local craftLabel = filterBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    craftLabel:SetPoint("LEFT", craftCheck, "RIGHT", 2, 0)
    craftLabel:SetText("Craftable")
    if scheme then ApplyFont(craftLabel, scheme, "small") end

    -- Hide Unreleased checkbox
    local releasedCheck = CreateFrame("CheckButton", nil, filterBar, "UICheckButtonTemplate")
    releasedCheck:SetSize(20, 20)
    releasedCheck:SetPoint("LEFT", craftLabel, "RIGHT", 10, 0)
    releasedCheck:SetChecked(hideUnreleased)
    releasedCheck:SetScript("OnClick", function(self)
        hideUnreleased = self:GetChecked()
        StylesTab:RefreshDetail()
    end)
    local releasedLabel = filterBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    releasedLabel:SetPoint("LEFT", releasedCheck, "RIGHT", 2, 0)
    releasedLabel:SetText("Hide Unreleased")
    if scheme then ApplyFont(releasedLabel, scheme, "small") end

    -- Search box
    searchBox = CreateFrame("EditBox", nil, filterBar, "SearchBoxTemplate")
    searchBox:SetSize(140, 20)
    searchBox:SetPoint("RIGHT", -4, 0)
    searchBox:SetAutoFocus(false)
    searchBox:SetScript("OnTextChanged", function(self)
        searchText = self:GetText():lower()
        if searchDebounceTimer then searchDebounceTimer:Cancel() end
        searchDebounceTimer = C_Timer.NewTimer(0.2, function()
            searchDebounceTimer = nil
            StylesTab:RefreshDetail()
        end)
    end)

    -- Room filter buttons
    local roomDefs = {
        { key = nil,          label = "All" },
        { key = "great-hall", label = "Great Hall" },
        { key = "kitchen",    label = "Kitchen" },
        { key = "study",      label = "Study" },
        { key = "bedroom",    label = "Bedroom" },
        { key = "garden",     label = "Garden" },
        { key = "workshop",   label = "Workshop" },
        { key = "trophy",     label = "Trophy" },
    }
    local roomX = 6
    for _, rd in ipairs(roomDefs) do
        local btn = HDG_UIHelpers.CreateChipButton(detailFrame, "", 60, false, nil)
        btn:SetPoint("TOPLEFT", roomX, -96)
        btn.text:SetText(rd.label)

        btn:SetWidth(btn.text:GetStringWidth() + 16)
        btn.roomKey = rd.key
        btn.baseLabel = rd.label
        btn:SetScript("OnClick", function()
            selectedRoom = rd.key
            self:RefreshDetail()
        end)

        roomButtons[#roomButtons + 1] = btn
    end

    -- Item list (left side)
    listFrame = CreateFrame("Frame", nil, detailFrame, "BackdropTemplate")
    listFrame:SetPoint("TOPLEFT", 4, -118)
    listFrame:SetPoint("BOTTOMLEFT", 4, 4)
    listFrame:SetWidth(280)
    HDG_UIHelpers.ApplyBackdrop(listFrame, "panel",
        {0.08, 0.08, 0.08, 0.6}, {0.2, 0.2, 0.2, 0.4})
    RegisterWidget(listFrame, "Panel")

    itemListScroll = CreateFrame("ScrollFrame", nil, listFrame, "UIPanelScrollFrameTemplate")
    itemListScroll:SetPoint("TOPLEFT", 2, -2)
    itemListScroll:SetPoint("BOTTOMRIGHT", -20, 2)
    HDG_UIHelpers.StyleScrollBar(itemListScroll)
    RegisterWidget(itemListScroll, "ScrollFrame")

    itemListContent = CreateFrame("Frame", nil, itemListScroll)
    itemListContent:SetWidth(256)
    itemListContent:SetHeight(1)
    itemListScroll:SetScrollChild(itemListContent)

    -- Detail panel (right side)
    detailPanel = CreateFrame("Frame", nil, detailFrame, "BackdropTemplate")
    detailPanel:SetPoint("TOPLEFT", listFrame, "TOPRIGHT", 4, 0)
    detailPanel:SetPoint("BOTTOMRIGHT", -4, 4)
    HDG_UIHelpers.ApplyBackdrop(detailPanel, "panel",
        {0.08, 0.08, 0.08, 0.6}, {0.2, 0.2, 0.2, 0.4})
    RegisterWidget(detailPanel, "Panel")

    -- Detail panel contents
    detailPanel.nameText = detailPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    detailPanel.nameText:SetPoint("TOPLEFT", 10, -10)
    if scheme then ApplyFont(detailPanel.nameText, scheme, "header") end

    detailPanel.tierText = detailPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    detailPanel.tierText:SetPoint("TOPLEFT", detailPanel.nameText, "BOTTOMLEFT", 0, -4)
    if scheme then ApplyFont(detailPanel.tierText, scheme, "small") end

    detailPanel.sourceText = detailPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    detailPanel.sourceText:SetPoint("TOPLEFT", detailPanel.tierText, "BOTTOMLEFT", 0, -4)
    if scheme then ApplyFont(detailPanel.sourceText, scheme, "small") end

    detailPanel.reasonText = detailPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    detailPanel.reasonText:SetPoint("TOPLEFT", detailPanel.sourceText, "BOTTOMLEFT", 0, -8)
    if scheme then ApplyFont(detailPanel.reasonText, scheme, "small") end
    detailPanel.reasonText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 1)

    -- 3D Model Preview
    local previewContainer = CreateFrame("Frame", nil, detailPanel, "BackdropTemplate")
    previewContainer:SetPoint("TOPLEFT", detailPanel.reasonText, "BOTTOMLEFT", 0, -10)
    previewContainer:SetPoint("RIGHT", detailPanel, "RIGHT", -10, 0)
    previewContainer:SetPoint("BOTTOM", detailPanel, "BOTTOM", 0, 70)
    HDG_UIHelpers.ApplyBackdrop(previewContainer, "panel",
        {0.05, 0.05, 0.05, 0.8}, {0.2, 0.2, 0.2, 0.4})
    RegisterWidget(previewContainer, "Panel")
    previewContainer:Hide()
    detailPanel.previewContainer = previewContainer

    local decorPreview = HDG_UIHelpers.CreateDecorPreview(previewContainer, {
        squareAspect = false,
        showOverlays = false,
        showControls = true,
        showCorbels = true,
        sceneInsets = { top = 4, right = 4, bottom = 4, left = 4 },
    })
    decorPreview:SetAllPoints()
    decorPreview.placeholder:Hide() -- container visibility controls this
    detailPanel.decorPreview = decorPreview

    -- Add to Crafting Queue button (for craftable items)
    detailPanel.craftBtn = CreateFrame("Button", nil, detailPanel, "UIPanelButtonTemplate")
    detailPanel.craftBtn:SetSize(180, 24)
    detailPanel.craftBtn:SetPoint("BOTTOMLEFT", 10, 40)
    detailPanel.craftBtn:SetText("Add to Crafting Queue")
    detailPanel.craftBtn:SetScript("OnClick", function()
        if selectedItemID and StyleEngine and HDG_UI and HDG_UI.AddToCraftingQueue then
            local recipe = StyleEngine:GetRecipe(selectedItemID)
            if recipe then
                local queueItem = {
                    id = recipe.itemID,
                    name = recipe.name,
                    spell = recipe.spellID,
                    reagents = recipe.reagents,
                    expansion = recipe.expansion or "Unknown",
                    profession = recipe.profession,
                }
                HDG_UI.AddToCraftingQueue(queueItem)
                print("|cff00ccff[HDG]|r Added " .. (recipe.name or "recipe") .. " to crafting queue")
            end
        end
    end)
    detailPanel.craftBtn:Hide()

    -- Add to Vendor Shopping List button (for vendor items)
    detailPanel.vendorBtn = CreateFrame("Button", nil, detailPanel, "UIPanelButtonTemplate")
    detailPanel.vendorBtn:SetSize(180, 24)
    detailPanel.vendorBtn:SetPoint("BOTTOMLEFT", 10, 10)
    detailPanel.vendorBtn:SetText("Add to Shopping List")
    detailPanel.vendorBtn:SetScript("OnClick", function()
        if selectedItemID and HDG.Store then
            local npcIDs = HDG_VendorByItem and HDG_VendorByItem[selectedItemID]
            if npcIDs and #npcIDs > 0 then
                if HDG.Store:AddToVendorShoppingList(selectedItemID, npcIDs[1]) then
                    local iName = HDG_Data and HDG_Data.GetCachedItemName(selectedItemID) or ("Item " .. selectedItemID)
                    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
            end
        end
    end)
    detailPanel.vendorBtn:Hide()

    -- Export button (custom styles only)
    local detailExportBtn = HDG_UIHelpers.CreateChipButton(detailFrame, "", 60, false, nil)
    detailExportBtn.text:SetText("Export")
    detailExportBtn:SetWidth(detailExportBtn.text:GetStringWidth() + 20)
    detailExportBtn:SetPoint("TOPRIGHT", -28, -6)
    detailExportBtn:SetScript("OnClick", function()
        if not selectedStyle or not StyleSerializer then return end
        local def = GetStyleDef(selectedStyle)
        if def then
            local encoded = StyleSerializer:Export(def)
            if encoded then StylesTab:ShowExportDialog(encoded) end
        end
    end)
    detailExportBtn:Hide()
    detailFrame.detailExportBtn = detailExportBtn

    -- View toggle button (right of tier buttons)
    local viewToggleBtn = HDG_UIHelpers.CreateChipButton(detailFrame, "", 60, false, nil)
    viewToggleBtn:SetSize(60, 22)
    viewToggleBtn:SetPoint("TOPLEFT", tierX + 6, -48)
    viewToggleBtn.text:SetText("List")

    viewToggleBtn:SetScript("OnClick", function()
        if viewMode == "gallery" then
            viewMode = "list"
            viewToggleBtn.text:SetText("Grid")
        else
            viewMode = "gallery"
            viewToggleBtn.text:SetText("List")
        end
        StylesTab:RefreshDetail()
    end)
    detailFrame.viewToggleBtn = viewToggleBtn

    -- Card grid container (hidden by default)
    cardGridFrame = CreateFrame("Frame", nil, detailFrame, "BackdropTemplate")
    cardGridFrame:SetPoint("TOPLEFT", 4, -118)
    cardGridFrame:SetPoint("BOTTOMRIGHT", -4, 4)
    HDG_UIHelpers.ApplyBackdrop(cardGridFrame, "panel",
        {0.08, 0.08, 0.08, 0.6}, {0.2, 0.2, 0.2, 0.4})
    RegisterWidget(cardGridFrame, "Panel")
    cardGridFrame:Hide()

    cardGridScroll = CreateFrame("ScrollFrame", nil, cardGridFrame, "UIPanelScrollFrameTemplate")
    cardGridScroll:SetPoint("TOPLEFT", 2, -2)
    cardGridScroll:SetPoint("BOTTOMRIGHT", -20, 2)
    HDG_UIHelpers.StyleScrollBar(cardGridScroll)
    RegisterWidget(cardGridScroll, "ScrollFrame")

    cardGridContent = CreateFrame("Frame", nil, cardGridScroll)
    cardGridContent:SetWidth(cardGridScroll:GetWidth() or 600)
    cardGridContent:SetHeight(1)
    cardGridScroll:SetScrollChild(cardGridContent)

    cardGridScroll:HookScript("OnSizeChanged", function(_, w)
        cardGridContent:SetWidth(w)
    end)

end

-- ============================================================================
-- SHOW / REFRESH
-- ============================================================================

function StylesTab:ShowLanding()
    selectedShoppingListId = nil
    detailFrame:Hide()
    if editorFrame then editorFrame:Hide() end
    landingFrame:Show()
    miniPreviewDirty = true
    self:RefreshLanding()
end

function StylesTab:ShowDetail(styleId)
    selectedStyle = styleId
    -- Default selected tier/subcategory based on style type
    local def = GetStyleDef(styleId)
    if def and def.tier == "snapshot" then
        selectedTier = nil
        selectedRoom = nil
        selectedSource = nil
    elseif def and def.subcategories then
        selectedTier = def.subcategories[1].key
        selectedSource = nil
    else
        selectedTier = "first"
        selectedRoom = nil
    end
    selectedItemID = nil
    landingFrame:Hide()
    if editorFrame then editorFrame:Hide() end
    detailFrame:Show()
    self:RefreshDetail()
end

function StylesTab:RefreshDetail()
    -- Redirect to shopping list detail if viewing one
    if selectedShoppingListId then
        self:RefreshShoppingListDetail()
        return
    end
    if not selectedStyle or not StyleEngine then return end

    local def = GetStyleDef(selectedStyle)
    if not def then return end

    local scheme = GetScheme()

    -- Update header
    detailHeader:SetText(def.displayName)
    if def.color then
        detailHeader:SetTextColor(def.color.r, def.color.g, def.color.b, 1)
    end
    detailDesc:SetText(def.description or "")

    -- Show export button for custom/snapshot styles
    if detailFrame.detailExportBtn then
        local showExport = (def.tier == "custom" or def.tier == "snapshot") and StyleSerializer ~= nil
        detailFrame.detailExportBtn:SetShown(showExport)
        if def.tier == "snapshot" then
            detailFrame.detailExportBtn:SetScript("OnClick", function()
                local encoded = StyleSerializer:ExportSnapshot(def)
                if encoded then StylesTab:ShowExportDialog(encoded, def) end
            end)
        else
            detailFrame.detailExportBtn:SetScript("OnClick", function()
                local encoded = StyleSerializer:Export(def)
                if encoded then StylesTab:ShowExportDialog(encoded) end
            end)
        end
    end

    -- Snapshot branch: flat item gallery, no tier/subcategory buttons
    if def.tier == "snapshot" then
        self:RefreshSnapshotDetail(def, scheme)
        return
    end

    -- Restore view toggle (snapshot branch hides it)
    if detailFrame.viewToggleBtn then detailFrame.viewToggleBtn:Show() end

    -- Branch: subcategory vs tier buttons
    local items = StyleEngine:GetStyleItems(selectedStyle)
    local isSubcat = def.subcategories ~= nil

    local hiddenSet = {}
    for _, bucket in pairs(items) do
        if type(bucket) == "table" then
            for _, it in ipairs(bucket) do
                if IsItemHidden(it.itemID) then hiddenSet[it.itemID] = true end
            end
        end
    end
    local function IsHidden(itemID) return hiddenSet[itemID] end

    if isSubcat then
        -- Hide tier buttons
        for _, btn in pairs(tierButtons) do btn:Hide() end
        -- Show/create subcategory buttons
        local scX = 6
        for i, sc in ipairs(def.subcategories) do
            local btn = subcatButtons[i]
            if not btn then
                btn = HDG_UIHelpers.CreateChipButton(detailFrame, "", 60, false, nil)
                subcatButtons[i] = btn
            end
            local count = 0
            if items[sc.key] then
                for _, it in ipairs(items[sc.key]) do
                    if not IsHidden(it.itemID) then count = count + 1 end
                end
            end
            btn.text:SetText(sc.label .. " (" .. count .. ")")
            btn:SetWidth(btn.text:GetStringWidth() + 24)
            btn:ClearAllPoints()
            btn:SetPoint("TOPLEFT", scX, -48)
            btn.subcatKey = sc.key
            btn:SetScript("OnClick", function()
                selectedTier = sc.key
                selectedItemID = nil
                self:RefreshDetail()
            end)
            HDG_UIHelpers.UpdateChipButtonState(btn, sc.key == selectedTier)
            btn:Show()
            scX = scX + btn:GetWidth() + 4
        end
        -- Hide excess subcatButtons
        for j = #def.subcategories + 1, #subcatButtons do
            subcatButtons[j]:Hide()
        end
        -- Reposition view toggle button after subcat buttons
        if detailFrame.viewToggleBtn then
            detailFrame.viewToggleBtn:ClearAllPoints()
            detailFrame.viewToggleBtn:SetPoint("TOPLEFT", scX + 6, -48)
        end
    else
        -- Hide subcategory buttons
        for _, btn in ipairs(subcatButtons) do btn:Hide() end
        -- Show tier buttons (existing behavior)
        local tierCounts = { first = 0, second = 0, neutral = 0, clash = 0 }
        for tk, _ in pairs(tierCounts) do
            if items[tk] then
                for _, it in ipairs(items[tk]) do
                    if not IsHidden(it.itemID) then tierCounts[tk] = tierCounts[tk] + 1 end
                end
            end
        end
        for key, btn in pairs(tierButtons) do
            btn:Show()
            local label = btn.text:GetText():gsub(" %(%d+%)", "")
            btn.text:SetText(label .. " (" .. (tierCounts[key] or 0) .. ")")
            HDG_UIHelpers.UpdateChipButtonState(btn, key == selectedTier)
        end
        -- Restore view toggle position
        if detailFrame.viewToggleBtn then
            detailFrame.viewToggleBtn:ClearAllPoints()
            detailFrame.viewToggleBtn:SetPoint("TOPLEFT", 6 + 4 * 99 + 6, -48)
        end
    end

    -- Get items for current tier/subcategory
    local tierItems
    if isSubcat then
        tierItems = items[selectedTier] or (def.subcategories[1] and items[def.subcategories[1].key]) or {}
    else
        tierItems = items[selectedTier] or items.first
    end

    if isSubcat then
        -- Source-type buttons (replace rooms for thematic styles)
        for _, btn in ipairs(roomButtons) do btn:Hide() end
        local sourceCounts = {}
        local repCount, endeavorCount, visibleTotal = 0, 0, 0
        for _, item in ipairs(tierItems) do
            if not IsHidden(item.itemID) then
                visibleTotal = visibleTotal + 1
                local st = item.entry[2]
                sourceCounts[st] = (sourceCounts[st] or 0) + 1
                if ItemHasRepRequirement(item.itemID) then repCount = repCount + 1 end
                if ItemCostsEndeavor(item.itemID) then endeavorCount = endeavorCount + 1 end
            end
        end
        sourceCounts["rep"] = repCount
        sourceCounts["endeavor"] = endeavorCount
        local srcX = 6
        for i, sd in ipairs(SOURCE_FILTER_DEFS) do
            local btn = sourceButtons[i]
            if not btn then
                btn = HDG_UIHelpers.CreateChipButton(detailFrame, "", 60, false, nil)
                sourceButtons[i] = btn
            end
            local cnt = sd.key and (sourceCounts[sd.key] or 0) or visibleTotal
            btn.text:SetText(sd.label .. " (" .. cnt .. ")")
            btn:SetWidth(btn.text:GetStringWidth() + 16)
            btn:ClearAllPoints()
            btn:SetPoint("TOPLEFT", srcX, -96)
            btn.sourceKey = sd.key
            btn:SetScript("OnClick", function()
                selectedSource = sd.key
                self:RefreshDetail()
            end)
            HDG_UIHelpers.UpdateChipButtonState(btn, sd.key == selectedSource)
            btn:Show()
            srcX = srcX + btn:GetWidth() + 2
        end
        for j = #SOURCE_FILTER_DEFS + 1, #sourceButtons do
            sourceButtons[j]:Hide()
        end
    else
        -- Room buttons (existing behavior for tier styles)
        for _, btn in ipairs(sourceButtons) do btn:Hide() end
        local roomCounts = {}
        local visibleTotal = 0
        for _, item in ipairs(tierItems) do
            if not IsHidden(item.itemID) then
                visibleTotal = visibleTotal + 1
                local rooms = StyleEngine:GetItemRooms(item.itemID)
                if rooms then
                    for _, r in ipairs(rooms) do
                        roomCounts[r] = (roomCounts[r] or 0) + 1
                    end
                end
            end
        end
        local roomX = 6
        for _, btn in ipairs(roomButtons) do
            local cnt = btn.roomKey and (roomCounts[btn.roomKey] or 0) or visibleTotal
            btn.text:SetText(btn.baseLabel .. " (" .. cnt .. ")")
            btn:SetWidth(btn.text:GetStringWidth() + 16)
            btn:ClearAllPoints()
            btn:SetPoint("TOPLEFT", roomX, -96)
            roomX = roomX + btn:GetWidth() + 2
            HDG_UIHelpers.UpdateChipButtonState(btn, btn.roomKey == selectedRoom)
            btn:Show()
        end
    end

    -- Build filtered item list for current tier
    local filtered = {}
    for _, item in ipairs(tierItems) do
        local pass = true

        -- Hide unreleased filter
        if pass and IsHidden(item.itemID) then pass = false end

        -- Search filter
        if pass and searchText ~= "" then
            local name = C_HousingDecor and C_HousingDecor.GetDecorName(item.decorID) or ""
            if not (name and name:lower():find(searchText, 1, true)) then
                pass = false
            end
        end

        -- Craftable filter
        if pass and craftableOnly then
            if not StyleEngine:IsCraftable(item.itemID) then
                pass = false
            end
        end

        -- Uncollected filter (check firstAcquisitionBonus > 0 means not yet collected)
        if pass and uncollectedOnly then
            local ok, catInfo = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID,
                Enum.HousingCatalogEntryType and Enum.HousingCatalogEntryType.Decor or 1,
                item.decorID, true
            )
            if ok and catInfo and catInfo.firstAcquisitionBonus == 0 then
                pass = false -- already collected
            end
        end

        -- Room filter (tier styles) / Source filter (subcategory styles)
        if pass and isSubcat and selectedSource then
            if selectedSource == "rep" then
                if not ItemHasRepRequirement(item.itemID) then pass = false end
            elseif selectedSource == "endeavor" then
                if not ItemCostsEndeavor(item.itemID) then pass = false end
            elseif item.entry[2] ~= selectedSource then pass = false end
        elseif pass and not isSubcat and selectedRoom then
            local rooms = StyleEngine:GetItemRooms(item.itemID)
            if not rooms then
                pass = false
            else
                local found = false
                for _, r in ipairs(rooms) do
                    if r == selectedRoom then found = true; break end
                end
                if not found then pass = false end
            end
        end

        if pass then
            filtered[#filtered + 1] = item
        end
    end

    -- Sort by name
    table.sort(filtered, function(a, b)
        local nameA = C_HousingDecor and C_HousingDecor.GetDecorName(a.decorID) or ""
        local nameB = C_HousingDecor and C_HousingDecor.GetDecorName(b.decorID) or ""
        return (nameA or "") < (nameB or "")
    end)

    -- Render based on view mode
    if viewMode == "gallery" then
        listFrame:Hide()
        detailPanel:Hide()
        cardGridFrame:Show()
        self:RenderCardGrid(filtered, scheme)
    else
        cardGridFrame:Hide()
        ReleaseAllStyleCells()
        listFrame:Show()
        detailPanel:Show()
        self:RenderItemList(filtered, scheme)
        self:RefreshItemDetail(scheme)
    end
end

-- ============================================================================
-- GALLERY CELL POOL + RENDERING (delegates to HDG_UIHelpers shared factory)
-- ============================================================================

ReleaseAllStyleCells = function()
    HDG_UIHelpers.ReleaseAllGalleryCells(styleCellPool)
end

local function UpdateStyleCellHighlights()
    for _, cell in ipairs(styleCellPool.active) do
        local wasSelected = cell.isSelected
        cell.isSelected = (selectedItemID == cell.itemID)
        if cell.isSelected ~= wasSelected then
            HDG_UIHelpers.UpdateGalleryCellBackground(cell, cell.isSelected)
        end
    end
end

local function CreateStyleCell(parent, item, x, y, cellSize, cfg)
    local ST = cfg.STYLES_TAB

    local cell = HDG_UIHelpers.CreateGalleryCell(styleCellPool, parent, cellSize, {
        statusIconSize = ST.STATUS_ICON_SIZE,
        checkmarkTexture = cfg.TEXTURES.CHECKMARK,
    })
    cell:SetPoint("TOPLEFT", x, y)

    local isCollected = HDG_Data.IsItemCollected(item.itemID)
    cell.isSelected = (selectedItemID == item.itemID)
    HDG_UIHelpers.UpdateGalleryCellBackground(cell, cell.isSelected)
    cell.checkmark:SetShown(isCollected)
    cell.redX:SetShown(not isCollected)

    -- Quantity badge + reuse catInfo as decorInfo
    local decorInfo = HDG_UIHelpers.UpdateGalleryCellQuantity(cell, item.decorID)

    cell.itemID = item.itemID
    cell.decorID = item.decorID
    cell.entry = item.entry

    -- Fetch decorInfo if not already obtained from quantity badge
    if not decorInfo and C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID then
        local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID,
            Enum.HousingCatalogEntryType and Enum.HousingCatalogEntryType.Decor or 1,
            item.decorID, true)
        if ok then decorInfo = info end
    end

    HDG_UIHelpers.UpdateGalleryCellVisuals(cell, decorInfo, isCollected, ST.DEFAULT_SCENE_ID)

    -- Click: left = add to shopping list, right = remove, shift-left = select
    cell:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    cell:SetScript("OnClick", function(self, button)
        if button == "LeftButton" and IsShiftKeyDown() then
            selectedItemID = self.itemID
            UpdateStyleCellHighlights()
        end
    end)

    -- Tooltip (Blizzard pattern: show hover bg on enter)
    cell:SetScript("OnEnter", function(self)
        self.hoverBg:Show()
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        local tipName = (C_HousingDecor and C_HousingDecor.GetDecorName and C_HousingDecor.GetDecorName(self.decorID)) or ("Item #" .. self.itemID)
        local tipCollected = HDG_Data.IsItemCollected(self.itemID)
        GameTooltip:AddLine(tipName, 1, 1, 1)
        if tipCollected then
            GameTooltip:AddLine("|cff33cc33Collected|r", 0.2, 0.9, 0.2)
        else
            GameTooltip:AddLine("|cff888888Not collected|r", 0.78, 0.78, 0.78)
        end
        if selectedStyle and StyleEngine then
            local sDef = GetStyleDef(selectedStyle)
            if sDef and sDef.subcategories then
                local reason = StyleEngine:GetTierReason(selectedStyle, self.decorID)
                if reason and reason ~= "" then
                    GameTooltip:AddLine(reason, 0.6, 0.8, 0.6, true)
                end
            else
                local tier = StyleEngine:GetCompatibility(selectedStyle, self.decorID)
                local tierLabels = {
                    ["first-order"] = "|cff00ff00" .. L["STYLE_TIER_SIGNATURE"] .. "|r",
                    ["second-order"] = "|cffffff00" .. L["STYLE_TIER_ACCENT"] .. "|r",
                    ["neutral"] = "|cffaaaaaa" .. L["STYLE_TIER_VERSATILE"] .. "|r",
                    ["clash"] = "|cffff4444" .. L["STYLE_TIER_CLASHING"] .. "|r",
                }
                if tier and tierLabels[tier] then
                    GameTooltip:AddLine("Tier: " .. tierLabels[tier], 0.8, 0.8, 0.8)
                end
                local reason = StyleEngine:GetTierReason(selectedStyle, self.decorID)
                if reason and reason ~= "" then
                    GameTooltip:AddLine(reason, 0.6, 0.6, 0.6, true)
                end
            end
        end
        if self.entry then
            local sourceLabel = StyleEngine:GetSourceLabel(self.entry[2])
            GameTooltip:AddLine("Source: " .. sourceLabel, 0.7, 0.7, 0.7)
        end
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("Shift-Click to select/inspect", 0.78, 0.78, 0.78)
        GameTooltip:Show()
    end)
    cell:SetScript("OnLeave", function(self)
        self.hoverBg:Hide()
        GameTooltip:Hide()
    end)

    cell:Show()
    return cell
end

function StylesTab:RenderCardGrid(items, scheme)
    ReleaseAllStyleCells()
    local ST = CONFIG.STYLES_TAB
    local cellH = ST.CELL_SIZE
    local contentWidth = (cardGridFrame:GetWidth() or 400) - 8
    local cols = math.max(1, math.floor(contentWidth / (ST.CELL_SIZE + ST.CELL_SPACING)))
    for i, item in ipairs(items) do
        local col = (i - 1) % cols
        local row = math.floor((i - 1) / cols)
        local x = 4 + col * (ST.CELL_SIZE + ST.CELL_SPACING)
        local y = -4 - row * (cellH + ST.CELL_SPACING)
        CreateStyleCell(cardGridContent, item, x, y, ST.CELL_SIZE, CONFIG)
    end
    local totalRows = math.ceil(#items / math.max(1, cols))
    cardGridContent:SetHeight(math.max(1, totalRows * (cellH + ST.CELL_SPACING)))
end

-- ============================================================================
-- ITEM LIST RENDERING
-- ============================================================================

function StylesTab:RenderItemList(items, scheme)
    rowPool:ReleaseAll()

    for i, item in ipairs(items) do
        local row = rowPool:Acquire(itemListContent)
        row:ClearAllPoints()
        row:SetPoint("TOPLEFT", 0, -(i - 1) * ROW_HEIGHT)
        row:SetPoint("RIGHT", 0, 0)

        -- Icon
        local iconID
        if C_HousingDecor and C_HousingDecor.GetDecorIcon then
            local ok, id = pcall(C_HousingDecor.GetDecorIcon, item.decorID)
            if ok then iconID = id end
        end
        row.icon:SetTexture(iconID or "Interface\\Icons\\INV_Misc_QuestionMark")

        -- Name
        local name
        if C_HousingDecor and C_HousingDecor.GetDecorName then
            local ok, n = pcall(C_HousingDecor.GetDecorName, item.decorID)
            if ok then name = n end
        end
        row.nameText:SetText(name or ("Item #" .. item.itemID))
        if scheme then ApplyFont(row.nameText, scheme, "small") end

        -- Source type label
        local sourceType = item.entry[2]
        row.sourceText:SetText(StyleEngine:GetSourceLabel(sourceType))
        if scheme then
            ApplyFont(row.sourceText, scheme, "small")
            row.sourceText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 1)
        end

        -- Selection state
        row.selected:SetShown(selectedItemID == item.itemID)

        -- Click (shift-click adds to style list)
        row.itemID = item.itemID
        row.decorID = item.decorID
        row.entry = item.entry
        row:SetScript("OnClick", function()
            selectedItemID = item.itemID
            StylesTab:RefreshDetail()
        end)

        row:Show()
    end

    itemListContent:SetHeight(math.max(1, #items * ROW_HEIGHT))
end

-- ============================================================================
-- ITEM DETAIL PANEL
-- ============================================================================

function StylesTab:LoadPreviewModel(decorID)
    if not detailPanel or not detailPanel.decorPreview then return end
    detailPanel._currentDecorID = decorID

    local ok = detailPanel.decorPreview:LoadItemData({ decorID = decorID })
    detailPanel.previewContainer:SetShown(ok)
end

function StylesTab:RefreshItemDetail(scheme)
    if not selectedItemID or not detailPanel then
        detailPanel.nameText:SetText("")
        detailPanel.tierText:SetText("")
        detailPanel.sourceText:SetText("")
        detailPanel.reasonText:SetText("")
        detailPanel.craftBtn:Hide()
        detailPanel.vendorBtn:Hide()
        if detailPanel.previewContainer then detailPanel.previewContainer:Hide() end
        detailPanel._currentDecorID = nil
        return
    end

    -- Find the item data
    local entry = HDG_AllDecorDB[selectedItemID]
    if not entry then return end
    local decorID = entry[1]

    -- Name
    local name
    if C_HousingDecor and C_HousingDecor.GetDecorName then
        local ok, n = pcall(C_HousingDecor.GetDecorName, decorID)
        if ok then name = n end
    end
    detailPanel.nameText:SetText(name or ("Item #" .. selectedItemID))

    -- Tier / Subcategory label
    local sDef = GetStyleDef(selectedStyle)
    if sDef and sDef.subcategories then
        local subcatLabel = StyleEngine:GetSubcategoryLabel(selectedStyle, selectedTier)
        detailPanel.tierText:SetText("|cff88cc88" .. subcatLabel .. "|r")
    else
        local tier = StyleEngine:GetCompatibility(selectedStyle, decorID)
        local tierLabels = {
            ["first-order"] = "|cff00ff001ST ORDER|r",
            ["second-order"] = "|cffffff002ND ORDER|r",
            ["neutral"] = "|cffaaaaaaNEUTRAL|r",
            ["clash"] = "|cffff4444CLASH|r",
            ["unclassified"] = "|cff888888UNCLASSIFIED|r",
        }
        detailPanel.tierText:SetText("Compatibility: " .. (tierLabels[tier] or tier))
    end

    -- Source
    local sourceType = entry[2]
    local sourceName = entry[3] or ""
    local sourceDetail = entry[4] or ""
    local sourceLabel = StyleEngine:GetSourceLabel(sourceType)
    if sourceType ~= 5 and HDG_VendorResolver and HDG_VendorResolver.GetVendorsForItem then
        local vendors = HDG_VendorResolver.GetVendorsForItem(selectedItemID)
        if vendors and #vendors > 0 then
            sourceLabel = sourceLabel .. " + Vendor"
        end
    end
    local sourceStr = "Source: " .. sourceLabel
    if #sourceName > 0 then sourceStr = sourceStr .. " — " .. sourceName end
    if #sourceDetail > 0 then sourceStr = sourceStr .. " (" .. sourceDetail .. ")" end
    detailPanel.sourceText:SetText(sourceStr)
    if scheme then
        ApplyFont(detailPanel.sourceText, scheme, "small")
        local s = GetScheme()
        if s then detailPanel.sourceText:SetTextColor(s.text.r, s.text.g, s.text.b, 1) end
    end

    -- Why this tier
    local reason = StyleEngine:GetTierReason(selectedStyle, decorID)
    detailPanel.reasonText:SetText(reason)

    -- Show action buttons based on item source
    local recipe = StyleEngine:GetRecipe(selectedItemID)
    local npcIDs = HDG_VendorByItem and HDG_VendorByItem[selectedItemID]
    if recipe then
        detailPanel.craftBtn:Show()
    else
        detailPanel.craftBtn:Hide()
    end
    if npcIDs and #npcIDs > 0 then
        detailPanel.vendorBtn:Show()
    else
        detailPanel.vendorBtn:Hide()
    end

    -- 3D preview
    self:LoadPreviewModel(decorID)
end


-- Delete confirmation popup
StaticPopupDialogs["HDG_DELETE_CUSTOM_STYLE"] = {
    text = L["STYLE_CONFIRM_DELETE"],
    button1 = L["STYLE_DELETE"],
    button2 = L["STYLE_CANCEL"],
    OnAccept = function(self, data)
        if data and data.key and HDG.Store then
            HDG.Store:DeleteCustomStyle(data.key)
            if StyleEngine then
                StyleEngine:RemoveCustomStyle(data.key)
            end
            if selectedStyle == data.key then
                selectedStyle = nil
            end
            StylesTab:ShowLanding()
        end
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    preferredIndex = 3,
}

-- ============================================================================
-- SNAPSHOT DETAIL VIEW
-- ============================================================================

function StylesTab:RefreshSnapshotDetail(def, scheme)
    -- Hide tier/subcategory/room/source/filter buttons
    for _, btn in pairs(tierButtons) do btn:Hide() end
    for _, btn in ipairs(subcatButtons) do btn:Hide() end
    for _, btn in ipairs(roomButtons) do btn:Hide() end
    for _, btn in ipairs(sourceButtons) do btn:Hide() end
    if detailFrame.viewToggleBtn then detailFrame.viewToggleBtn:Hide() end

    -- Update description with snapshot stats
    local totalPlaced, totalStored, uniqueItems = 0, 0, 0
    if def.snapshot and def.snapshot.items then
        for _, counts in pairs(def.snapshot.items) do
            uniqueItems = uniqueItems + 1
            totalPlaced = totalPlaced + (counts.placed or 0)
            totalStored = totalStored + (counts.stored or 0)
        end
    end
    detailDesc:SetText(totalPlaced .. " placed across all houses, " .. totalStored .. " in storage (" .. uniqueItems .. " unique)")

    -- Get cached items
    local items = StyleEngine:GetStyleItems(selectedStyle)
    local snapshotItems = items and items.items or {}

    -- Filter by search
    local filtered = {}
    for _, item in ipairs(snapshotItems) do
        local pass = true
        if searchText ~= "" then
            local name = C_HousingDecor and C_HousingDecor.GetDecorName(item.decorID) or ""
            if not (name and name:lower():find(searchText, 1, true)) then pass = false end
        end
        if pass then filtered[#filtered + 1] = item end
    end

    -- Sort by placed count descending, then name
    table.sort(filtered, function(a, b)
        if (a.placed or 0) ~= (b.placed or 0) then
            return (a.placed or 0) > (b.placed or 0)
        end
        local nameA = C_HousingDecor and C_HousingDecor.GetDecorName(a.decorID) or ""
        local nameB = C_HousingDecor and C_HousingDecor.GetDecorName(b.decorID) or ""
        return (nameA or "") < (nameB or "")
    end)

    -- Always show as gallery for snapshots
    listFrame:Hide()
    detailPanel:Hide()
    cardGridFrame:Show()
    ReleaseAllStyleCells()

    local ST = CONFIG.STYLES_TAB
    local cellH = ST.CELL_SIZE
    local contentWidth = (cardGridFrame:GetWidth() or 400) - 8
    local cols = math.max(1, math.floor(contentWidth / (ST.CELL_SIZE + ST.CELL_SPACING)))

    for i, item in ipairs(filtered) do
        local col = (i - 1) % cols
        local row = math.floor((i - 1) / cols)
        local x = 4 + col * (ST.CELL_SIZE + ST.CELL_SPACING)
        local y = -4 - row * (cellH + ST.CELL_SPACING)

        local cell = HDG_UIHelpers.CreateGalleryCell(styleCellPool, cardGridContent, ST.CELL_SIZE, {
            statusIconSize = ST.STATUS_ICON_SIZE,
            checkmarkTexture = CONFIG.TEXTURES.CHECKMARK,
        })
        cell:SetPoint("TOPLEFT", x, y)
        cell.itemID = item.itemID
        cell.decorID = item.decorID
        cell.entry = item.entry

        local isCollected = HDG_Data.IsItemCollected(item.itemID)
        cell.isSelected = (selectedItemID == item.itemID)
        HDG_UIHelpers.UpdateGalleryCellBackground(cell, cell.isSelected)
        cell.checkmark:SetShown(isCollected)
        cell.redX:SetShown(not isCollected)

        local decorInfo = HDG_UIHelpers.UpdateGalleryCellQuantity(cell, item.decorID)
        if not decorInfo and C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID then
            local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID,
                Enum.HousingCatalogEntryType and Enum.HousingCatalogEntryType.Decor or 1,
                item.decorID, true)
            if ok then decorInfo = info end
        end
        HDG_UIHelpers.UpdateGalleryCellVisuals(cell, decorInfo, isCollected, ST.DEFAULT_SCENE_ID)

        -- Placed count badge (override the quantity text)
        if item.placed and item.placed > 0 then
            cell.infoText:SetText("x" .. item.placed)
            cell.infoText:SetTextColor(0.4, 0.7, 1.0)
            cell.infoText:Show()
        end

        -- Tooltip
        cell:SetScript("OnEnter", function(self)
            self.hoverBg:Show()
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            local tipName = (C_HousingDecor and C_HousingDecor.GetDecorName and C_HousingDecor.GetDecorName(self.decorID)) or ("Item #" .. self.itemID)
            GameTooltip:AddLine(tipName, 1, 1, 1)
            if item.placed and item.placed > 0 then
                GameTooltip:AddLine("Placed: " .. item.placed, 0.4, 0.7, 1.0)
            end
            if item.stored and item.stored > 0 then
                GameTooltip:AddLine("In storage: " .. item.stored, 0.6, 0.6, 0.6)
            end
            GameTooltip:Show()
        end)
        cell:SetScript("OnLeave", function(self)
            self.hoverBg:Hide()
            GameTooltip:Hide()
        end)

        cell:Show()
    end

    local totalRows = math.ceil(#filtered / math.max(1, cols))
    cardGridContent:SetHeight(math.max(1, totalRows * (cellH + ST.CELL_SPACING)))
end

-- ============================================================================
-- SHOPPING LIST DETAIL VIEW
-- ============================================================================

function StylesTab:ShowShoppingListDetail(listId)
    selectedShoppingListId = listId
    selectedStyle = nil
    selectedItemID = nil
    landingFrame:Hide()
    if editorFrame then editorFrame:Hide() end
    detailFrame:Show()
    self:RefreshShoppingListDetail()
end

function StylesTab:RefreshShoppingListDetail()
    if not selectedShoppingListId or not HDG.Store then return end

    local allLists = HDG.Store.state and HDG.Store.state.vendorShoppingLists
    local listData = allLists and allLists[selectedShoppingListId]
    if not listData then return end

    local scheme = GetScheme()

    -- Update header
    detailHeader:SetText(listData.name or "Shopping List")
    if scheme then
        detailHeader:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b, 1)
    end
    local itemCount = listData.items and #listData.items or 0
    detailDesc:SetText(itemCount .. " items in list")

    -- Hide export button
    if detailFrame.detailExportBtn then detailFrame.detailExportBtn:Hide() end

    -- Hide tier/subcategory/room/source buttons and view toggle
    for _, btn in pairs(tierButtons) do btn:Hide() end
    for _, btn in ipairs(subcatButtons) do btn:Hide() end
    for _, btn in ipairs(roomButtons) do btn:Hide() end
    for _, btn in ipairs(sourceButtons) do btn:Hide() end
    if detailFrame.viewToggleBtn then detailFrame.viewToggleBtn:Hide() end

    -- Build items from shopping list entries
    local items = {}
    for _, entry in ipairs(listData.items or {}) do
        local itemID = entry.itemID or entry.id
        local qty = entry.qty or entry.quantity or 1
        local ade = HDG_AllDecorDB and HDG_AllDecorDB[itemID]
        local decorID = ade and ade[1]
        if itemID and decorID then
            items[#items + 1] = {
                itemID = itemID,
                decorID = decorID,
                entry = ade,
                qty = qty,
            }
        end
    end

    -- Filter by search
    local filtered = {}
    for _, item in ipairs(items) do
        local pass = true
        if searchText ~= "" then
            local name = C_HousingDecor and C_HousingDecor.GetDecorName(item.decorID) or ""
            if not (name and name:lower():find(searchText, 1, true)) then pass = false end
        end
        if pass then filtered[#filtered + 1] = item end
    end

    -- Sort by name
    table.sort(filtered, function(a, b)
        local nameA = C_HousingDecor and C_HousingDecor.GetDecorName(a.decorID) or ""
        local nameB = C_HousingDecor and C_HousingDecor.GetDecorName(b.decorID) or ""
        return (nameA or "") < (nameB or "")
    end)

    -- Show as gallery (same as snapshot)
    listFrame:Hide()
    detailPanel:Hide()
    cardGridFrame:Show()
    ReleaseAllStyleCells()

    local ST = CONFIG.STYLES_TAB
    local cellH = ST.CELL_SIZE
    local contentWidth = (cardGridFrame:GetWidth() or 400) - 8
    local cols = math.max(1, math.floor(contentWidth / (ST.CELL_SIZE + ST.CELL_SPACING)))

    for i, item in ipairs(filtered) do
        local col = (i - 1) % cols
        local row = math.floor((i - 1) / cols)
        local x = 4 + col * (ST.CELL_SIZE + ST.CELL_SPACING)
        local y = -4 - row * (cellH + ST.CELL_SPACING)

        local cell = HDG_UIHelpers.CreateGalleryCell(styleCellPool, cardGridContent, ST.CELL_SIZE, {
            statusIconSize = ST.STATUS_ICON_SIZE,
            checkmarkTexture = CONFIG.TEXTURES.CHECKMARK,
        })
        cell:SetPoint("TOPLEFT", x, y)
        cell.itemID = item.itemID
        cell.decorID = item.decorID
        cell.entry = item.entry

        local isCollected = HDG_Data.IsItemCollected(item.itemID)
        cell.isSelected = (selectedItemID == item.itemID)
        HDG_UIHelpers.UpdateGalleryCellBackground(cell, cell.isSelected)
        cell.checkmark:SetShown(isCollected)
        cell.redX:SetShown(not isCollected)

        local decorInfo = HDG_UIHelpers.UpdateGalleryCellQuantity(cell, item.decorID)
        if not decorInfo and C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID then
            local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID,
                Enum.HousingCatalogEntryType and Enum.HousingCatalogEntryType.Decor or 1,
                item.decorID, true)
            if ok then decorInfo = info end
        end
        HDG_UIHelpers.UpdateGalleryCellVisuals(cell, decorInfo, isCollected, ST.DEFAULT_SCENE_ID)

        -- Qty badge from shopping list
        if item.qty and item.qty > 0 then
            cell.infoText:SetText(item.qty)
            cell.infoText:SetTextColor(1, 1, 1)
            cell.infoText:Show()
        end

        -- Tooltip
        cell:SetScript("OnEnter", function(self)
            self.hoverBg:Show()
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            local tipName = (C_HousingDecor and C_HousingDecor.GetDecorName and C_HousingDecor.GetDecorName(self.decorID)) or ("Item #" .. self.itemID)
            GameTooltip:AddLine(tipName, 1, 1, 1)
            if item.qty and item.qty > 1 then
                GameTooltip:AddLine("Qty: " .. item.qty, 0.7, 0.7, 0.7)
            end
            if isCollected then
                GameTooltip:AddLine("Collected", 0.4, 0.8, 0.4)
            else
                GameTooltip:AddLine("Not collected", 0.8, 0.4, 0.4)
            end
            GameTooltip:Show()
        end)
        cell:SetScript("OnLeave", function(self)
            self.hoverBg:Hide()
            GameTooltip:Hide()
        end)

        cell:Show()
    end

    local totalRows = math.ceil(#filtered / math.max(1, cols))
    cardGridContent:SetHeight(math.max(1, totalRows * (cellH + ST.CELL_SPACING)))
end

-- ============================================================================
-- SNAPSHOT PLACEMENT COST
-- ============================================================================

function StylesTab:ComputeSnapshotCost(def)
    if not def or not def.snapshot or not def.snapshot.items then return end
    if not C_HousingCatalog or not C_HousingCatalog.GetCatalogEntryInfoByRecordID then return end
    if not HDG_AllDecorDB then return end
    local cost = 0
    for itemID, counts in pairs(def.snapshot.items) do
        local entry = HDG_AllDecorDB[itemID]
        if entry then
            local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID,
                Enum.HousingCatalogEntryType and Enum.HousingCatalogEntryType.Decor or 1,
                entry[1], true)
            if ok and info and info.placementCost then
                cost = cost + info.placementCost * (counts.placed or 0)
            end
        end
    end
    def.snapshot.totalPlacementCost = cost
end

-- ============================================================================
-- SNAPSHOT CAPTURE
-- ============================================================================

function StylesTab:TakeSnapshot()
    -- Collect all itemIDs to scan
    local allItemIDs = {}
    for itemID in pairs(HDG_AllDecorDB) do
        allItemIDs[#allItemIDs + 1] = itemID
    end
    table.sort(allItemIDs)

    local snapshotItems = {}
    local totalCost = 0
    local idx = 0
    local total = #allItemIDs
    local BATCH_SIZE = 200
    local dbgOk, dbgNil, dbgHit = 0, 0, 0

    -- Update button text to show scanning
    if landingFrame and landingFrame.snapshotBtn then
        landingFrame.snapshotBtn.text:SetText("Scanning...")
        landingFrame.snapshotBtn:Disable()
    end

    local function ProcessBatch()
        local batchEnd = math.min(idx + BATCH_SIZE, total)
        for i = idx + 1, batchEnd do
            local itemID = allItemIDs[i]
            local entry = HDG_AllDecorDB[itemID]
            if entry then
                local decorID = entry[1]
                local ok, catInfo = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID,
                    Enum.HousingCatalogEntryType and Enum.HousingCatalogEntryType.Decor or 1,
                    decorID, true)
                if ok and catInfo then
                    dbgOk = dbgOk + 1
                    local placed = catInfo.numPlaced or 0
                    if placed > 0 then
                        dbgHit = dbgHit + 1
                        local stored = catInfo.quantity or 0
                        snapshotItems[itemID] = { placed = placed, stored = stored }
                        if catInfo.placementCost then totalCost = totalCost + catInfo.placementCost * placed end
                    end
                else
                    dbgNil = dbgNil + 1
                end
            end
        end
        idx = batchEnd

        if idx < total then
            C_Timer.After(0, ProcessBatch)
        else
            print("|cFF14b8a6[HDG Snapshot]|r Catalog scan: " .. total .. " items, " .. dbgOk .. " ok, " .. dbgNil .. " nil, " .. dbgHit .. " placed (account-wide)")
            StylesTab:FinishSnapshot(snapshotItems, totalCost)
        end
    end

    ProcessBatch()
end

function StylesTab:FinishSnapshot(snapshotItems, totalCost)
    -- Restore button
    if landingFrame and landingFrame.snapshotBtn then
        landingFrame.snapshotBtn.text:SetText("Save Placed Decor")
        landingFrame.snapshotBtn:Enable()
    end

    -- Check if we found anything
    local itemCount = 0
    local totalPlaced = 0
    for _, counts in pairs(snapshotItems) do
        itemCount = itemCount + 1
        totalPlaced = totalPlaced + (counts.placed or 0)
    end

    if itemCount == 0 then
        print("|cFFFF4444[HDG]|r No placed or stored decor found")
        return
    end

    -- Show name dialog
    StaticPopupDialogs["HDG_SNAPSHOT_NAME"] = StaticPopupDialogs["HDG_SNAPSHOT_NAME"] or {
        text = "Name your snapshot (all placed decor, account-wide):",
        button1 = "Save",
        button2 = "Cancel",
        hasEditBox = true,
        editBoxWidth = 250,
        OnShow = function(self)
            self.EditBox:SetText("All Placed Decor")
            self.EditBox:HighlightText()
            self.EditBox:SetFocus()
        end,
        OnAccept = function(self)
            local name = self.EditBox:GetText()
            if not name or name == "" then name = "All Placed Decor" end
            StylesTab:SaveSnapshot(name, self.data.items, self.data.totalCost)
        end,
        timeout = 0,
        whileDead = true,
        hideOnEscape = true,
        preferredIndex = 3,
    }

    StaticPopup_Show("HDG_SNAPSHOT_NAME", nil, nil, { items = snapshotItems, totalCost = totalCost or 0 })
end

function StylesTab:SaveSnapshot(name, snapshotItems, totalCost)
    local totalPlaced = 0
    for _, counts in pairs(snapshotItems) do
        totalPlaced = totalPlaced + (counts.placed or 0)
    end

    local def = {
        displayName = name,
        iconAtlas = "decor-controls-decoratemode-active",
        tier = "snapshot",
        color = { r = 0.4, g = 0.7, b = 1.0 },
        description = totalPlaced .. " placed -- " .. date("%b %d, %Y"),
        snapshot = {
            capturedAt = time(),
            items = snapshotItems,
            totalPlacementCost = totalCost or 0,
        },
    }

    local key = "snapshot-" .. time() .. "-" .. math.random(1000, 9999)
    if HDG.Store then
        HDG.Store:SaveCustomStyle(key, def)
    end
    if StyleEngine then
        StyleEngine:InvalidateCustomStyle(key)
    end

    print("|cFF00FF00[HDG]|r Snapshot saved: " .. name .. " (" .. totalPlaced .. " items placed)")

    if panel and panel:IsShown() then
        self:ShowLanding()
    end
end

-- ============================================================================
-- EXPORT DIALOG
-- ============================================================================

local exportCtrl

function StylesTab:ShowExportDialog(encodedStr, snapshotDef)
    if not exportCtrl then
        exportCtrl = HDG_UIHelpers.CreateExportDialog({ width = 500, height = 220, bottomPad = 65 })
        -- Decor ID checkbox (shown only for snapshot exports)
        local decorCB = CreateFrame("CheckButton", nil, exportCtrl.frame, "UICheckButtonTemplate")
        decorCB:SetSize(22, 22)
        decorCB:SetPoint("BOTTOMLEFT", 12, 32)
        local decorLabel = exportCtrl.frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        decorLabel:SetPoint("LEFT", decorCB, "RIGHT", 2, 0)
        decorLabel:SetText("Use Decor IDs")
        local scheme = HDG_UIHelpers.GetScheme()
        if scheme then HDG.Theme.ApplyFont(decorLabel, scheme, "small") end
        decorCB:Hide()
        exportCtrl.decorCB = decorCB
    end

    exportCtrl:Show(encodedStr, "|cFF14b8a6" .. L["STYLE_EXPORT_TITLE"] .. "|r")

    -- Show decorID checkbox only for snapshot exports
    if snapshotDef and snapshotDef.snapshot and StyleSerializer then
        exportCtrl.decorCB:SetChecked(false)
        exportCtrl.decorCB:Show()
        exportCtrl.decorCB:SetScript("OnClick", function(self)
            local useDecor = self:GetChecked()
            local reEncoded = StyleSerializer:ExportSnapshot(snapshotDef, useDecor)
            if reEncoded then
                exportCtrl.editBox:SetText(reEncoded)
                exportCtrl.editBox:HighlightText()
                exportCtrl.editBox:SetFocus()
            end
        end)
    else
        exportCtrl.decorCB:Hide()
    end
end

-- ============================================================================
-- IMPORT DIALOG
-- ============================================================================

StaticPopupDialogs["HDG_IMPORT_STYLE"] = {
    text = L["STYLE_IMPORT_PROMPT"],
    button1 = L["STYLE_IMPORT"],
    button2 = L["STYLE_CANCEL"],
    hasEditBox = true,
    editBoxWidth = 350,
    OnShow = function(self)
        self.EditBox:SetMaxLetters(0) -- unlimited (default 255 truncates long HDGVL strings)
        self.EditBox:SetText("")
        self.EditBox:SetFocus()
    end,
    OnAccept = function(self)
        local text = self.EditBox:GetText()
        if not text or text == "" then return end
        if not StyleSerializer then return end

        local styleDef, err = StyleSerializer:Import(text)
        if not styleDef then
            print("|cFFFF4444[HDG]|r " .. string.format(L["STYLE_MSG_IMPORT_FAILED"], err or "unknown error"))
            return
        end

        -- Compute placement cost for snapshot imports
        if styleDef.tier == "snapshot" and styleDef.snapshot and not styleDef.snapshot.totalPlacementCost then
            StylesTab:ComputeSnapshotCost(styleDef)
        end

        -- Save (use snapshot key prefix for snapshot imports)
        local key
        if styleDef.tier == "snapshot" then
            key = "snapshot-" .. time() .. "-" .. math.random(1000, 9999)
        end
        key = HDG.Store:SaveCustomStyle(key, styleDef)
        if StyleEngine then
            StyleEngine:InvalidateCustomStyle(key)
        end

        print("|cFF00FF00[HDG]|r " .. string.format(L["STYLE_MSG_IMPORTED"], styleDef.displayName))
        if StylesTab and panel and panel:IsShown() then
            StylesTab:ShowLanding()
        end
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    preferredIndex = 3,
}

function StylesTab:ShowImportDialog()
    StaticPopup_Show("HDG_IMPORT_STYLE")
end

-- ============================================================================
-- 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 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 basketRowPool = nil -- row pool for basket items

-- 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)
    if not panel then self:Create() end
    if not editorFrame then self:CreateEditor() end

    landingFrame:Hide()
    detailFrame:Hide()
    editorFrame:Show()
    panel:Show()

    editingKey = styleKey

    wipe(editorLockedItems)
    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 = DeepCopyTable(def)
        else
            workingDef = self:NewBlankDef()
        end
        descManuallyEdited = true -- preserve existing description
    elseif prefillDef then
        -- Duplicating (prefilled)
        workingDef = DeepCopyTable(prefillDef)
        descManuallyEdited = true
    else
        -- New blank
        workingDef = self:NewBlankDef()
        descManuallyEdited = false
    end

    -- Restore lock/remove state from saved def
    if workingDef.lockedItems then
        for id in pairs(workingDef.lockedItems) do editorLockedItems[id] = true end
    end
    if workingDef.removedItems then
        for id in pairs(workingDef.removedItems) do editorRemovedItems[id] = true end
    end

    BuildTagStatesFromDef(workingDef)
    self:RefreshEditor()
end

function StylesTab:NewBlankDef()
    -- Default name: CharName Style N
    local charName = UnitName("player") or "My"
    local counter = 1
    if HDG.Store then
        local customs = HDG.Store:GetState().customStyles
        if customs then
            for _ in pairs(customs) do counter = counter + 1 end
        end
    end
    return {
        displayName = charName .. " Style " .. 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

-- Auto-save editor state after lock/unlock/remove actions
local function EditorAutoSave()
    if not workingDef or not HDG.Store then return end
    -- Persist lock/remove state into workingDef
    workingDef.lockedItems = next(editorLockedItems) and editorLockedItems or nil
    workingDef.removedItems = next(editorRemovedItems) and editorRemovedItems or nil
    -- 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()
    local scheme = GetScheme()
    local SE = CONFIG.STYLE_EDITOR

    editorFrame = CreateFrame("Frame", nil, panel)
    editorFrame:SetAllPoints()
    editorFrame:Hide()

    -- ================================================================
    -- 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

    -- ================================================================
    -- 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)
        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.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("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:UpdateEditorPreview()
        end)
        editorTierButtons[td.key] = btn
        tierX = tierX + 72
    end

    -- Preview frame (gallery grid below tier strip)
    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: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

    RebuildWorkingDefFromTags()
    HDG_UIHelpers.ReleaseAllGalleryCells(editorPreviewPool)
    wipe(editorAllItems)

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

    -- Check if there are any tags at all
    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(Select tags to see preview)|r")
        editorFrame.previewContent:SetHeight(1)
        -- Reset tier button labels
        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
        return
    end

    -- 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
    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 (uses semantic tier from ScoreStyleLive)
    if editorTierFilter ~= "all" then
        local filtered = {}
        for _, s in ipairs(result.sampleItems) do
            if s.score == 999 then -- locked items always show
                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

    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 C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID then
            local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID,
                Enum.HousingCatalogEntryType and Enum.HousingCatalogEntryType.Decor or 1,
                sample.decorID, true)
            if ok then decorInfo = info end
        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

        -- Tooltip + info bar hover
        cell:SetScript("OnEnter", function(self)
            self.hoverBg:Show()
            -- Info bar update
            local tipName = (C_HousingDecor and C_HousingDecor.GetDecorName and C_HousingDecor.GetDecorName(sample.decorID)) or ("Item #" .. sample.itemID)
            StylesTab:UpdateInfoBarHover(sample.itemID, sample.decorID, tipName)
            -- Tooltip
            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
            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
                    if HDG.Store then
                        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
                    end
                else
                    -- Toggle basket membership
                    if editorLockedItems[sample.itemID] then
                        editorLockedItems[sample.itemID] = nil
                    else
                        editorLockedItems[sample.itemID] = true
                    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 C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID then
            local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID,
                Enum.HousingCatalogEntryType and Enum.HousingCatalogEntryType.Decor or 1,
                decorID, true)
            if ok and 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
            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 = 0
    for _ in pairs(editorLockedItems) do basketCount = basketCount + 1 end

    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 C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID then
        local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID,
            Enum.HousingCatalogEntryType and Enum.HousingCatalogEntryType.Decor or 1,
            decorID, true)
        if ok and info and info.placementCost then
            cost = info.placementCost
        end
    end

    local basketCount = 0
    for _ in pairs(editorLockedItems) do basketCount = basketCount + 1 end

    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
    local name = workingDef.displayName
    if not name or name == "" then
        print("|cFFFF4444[HDG]|r " .. L["STYLE_ERR_NAME_EMPTY"])
        return
    end

    RebuildWorkingDefFromTags()
    workingDef.lockedItems = next(editorLockedItems) and editorLockedItems or nil
    workingDef.removedItems = next(editorRemovedItems) and editorRemovedItems or nil

    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

    -- 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))
    selectedStyle = nil
    self:ShowLanding()
end
