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

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

-- 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
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 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 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 LANDING_CAT_DEFS = {
    { key = nil,          label = "All" },
    { 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" },
}

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("Browse by Style |cFF888888(Beta - in development)|r")
    landingFrame.header = header

    -- Style Creator button (top-right)
    local creatorBtn = HDG_UIHelpers.CreateChipButton(landingFrame, "", 60, false, nil)
    creatorBtn.text:SetText("+ 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("Import")
    importBtn:SetWidth(importBtn.text:GetStringWidth() + 16)
    importBtn:SetPoint("RIGHT", creatorBtn, "LEFT", -4, 0)
    importBtn:SetScript("OnClick", function()
        StylesTab:ShowImportDialog()
    end)
    landingFrame.importBtn = importBtn

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

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
    for _, card in ipairs(cardFrames) do
        card:Hide()
    end

    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

    wipe(miniPreviewCache)
    local showHeaders = (selectedLandingCategory == nil)

    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 tier, get cards from Store
            local isCustomTier = (tierInfo.key == "custom")
            local customStyles, customOrder
            if isCustomTier and HDG.Store then
                customStyles, customOrder = HDG.Store:GetCustomStyles()
                -- Skip section entirely if no custom styles and not filtered to custom
                if (not customOrder or #customOrder == 0) and not selectedLandingCategory then
                    -- still show when specifically filtered
                end
            end

            -- Section header (only when showing all categories)
            if showHeaders then
                cardIdx = cardIdx + 1
                local sectionHeader = cardFrames[cardIdx]
                if not sectionHeader or sectionHeader.icon then
                    sectionHeader = landingContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
                    cardFrames[cardIdx] = sectionHeader
                end
                sectionHeader:ClearAllPoints()
                sectionHeader:SetPoint("TOPLEFT", 4, yOffset)
                if scheme then
                    ApplyFont(sectionHeader, scheme, "header")
                    sectionHeader:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b, 1)
                end
                sectionHeader:SetText(tierInfo.name)
                sectionHeader:Show()
                yOffset = yOffset - 20
            end

            -- Cards for this tier
            local col = 0

            if isCustomTier then
                -- Custom styles from Store
                if customOrder then
                    for _, styleId in ipairs(customOrder) do
                        local def = customStyles[styleId]
                        if def 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, nil, scheme)
                            card:Show()
                            col = col + 1
                            if col >= cardsPerRow then
                                col = 0
                                yOffset = yOffset - (CARD_H + CARD_GAP)
                            end
                        end
                    end
                end

                -- "+ New Style" card
                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)
                -- Style it as a "new" card
                newCard.icon:SetTexture("Interface\\PaperDollInfoFrame\\Character-Plus")
                newCard.nameText:SetText("|cFF94a3b8+ New Style|r")
                newCard.countText:SetText("")
                newCard.descText:SetText("|cFF94a3b8Click to create a custom style with your own facet tags|r")
                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()
                    StylesTab:ShowEditor(nil)
                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, nil, 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

    landingContent:SetHeight(math.abs(yOffset) + 20)
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")

    -- 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 = GetScheme()
        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 = GetScheme()
            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)
    -- Icon
    card.icon:SetTexture(def.icon)

    -- 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 collStats = StyleEngine:GetStyleCollectionStats(styleId, hideUnreleased)
    if scheme then ApplyFont(card.countText, scheme, "small") end
    local 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

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

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

    -- Copy+Edit (all 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

    -- 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.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("< 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 = "Signature" },
        { key = "second",  label = "Accent" },
        { key = "neutral", label = "Versatile" },
        { key = "clash",   label = "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()
        StylesTab:RefreshDetail()
    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()
    detailFrame:Hide()
    if editorFrame then editorFrame:Hide() end
    landingFrame:Show()
    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.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()
    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 styles
    if detailFrame.detailExportBtn then
        detailFrame.detailExportBtn:SetShown(def.tier == "custom" and StyleSerializer ~= nil)
    end

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

    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 IsItemHidden(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 IsItemHidden(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 IsItemHidden(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 IsItemHidden(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 IsItemHidden(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"] = "|cff00ff00Signature|r",
                    ["second-order"] = "|cffffff00Accent|r",
                    ["neutral"] = "|cffaaaaaaVersatile|r",
                    ["clash"] = "|cffff4444Clashing|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 = "Delete custom style \"%s\"?",
    button1 = "Delete",
    button2 = "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,
}

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

local exportFrame

function StylesTab:ShowExportDialog(encodedStr)
    if not exportFrame then
        local f = CreateFrame("Frame", "HDG_StyleExportFrame", UIParent, "BackdropTemplate")
        f:SetSize(500, 200)
        f:SetPoint("CENTER")
        f:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false, edgeSize = 2,
            insets = { left = 0, right = 0, top = 0, bottom = 0 }
        })
        local scheme = GetScheme()
        if scheme then
            f:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.95)
            f:SetBackdropBorderColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, 1)
            RegisterWidget(f, "Panel")
        else
            f:SetBackdropColor(0.1, 0.1, 0.12, 0.95)
            f:SetBackdropBorderColor(0.08, 0.72, 0.65, 1)
        end
        f:SetFrameStrata("DIALOG")
        f:SetMovable(true)
        f:EnableMouse(true)
        f:RegisterForDrag("LeftButton")
        f:SetScript("OnDragStart", f.StartMoving)
        f:SetScript("OnDragStop", f.StopMovingOrSizing)

        local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        title:SetPoint("TOP", 0, -10)
        if scheme then ApplyFont(title, scheme, "header") end
        title:SetText("|cFF14b8a6Export Style|r")

        local instr = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        instr:SetPoint("TOP", title, "BOTTOM", 0, -5)
        instr:SetText("|cFF94a3b8Select all (Ctrl+A) then copy (Ctrl+C)|r")
        if scheme then ApplyFont(instr, scheme, "small") end

        local sf = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
        sf:SetPoint("TOPLEFT", 15, -50)
        sf:SetPoint("BOTTOMRIGHT", -35, 45)

        local eb = CreateFrame("EditBox", nil, sf)
        eb:SetMultiLine(true)
        eb:SetFontObject("GameFontHighlightSmall")
        eb:SetWidth(440)
        eb:SetAutoFocus(false)
        eb:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
        sf:SetScrollChild(eb)
        f.editBox = eb

        local closeBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
        closeBtn:SetPoint("BOTTOM", 0, 10)
        closeBtn:SetSize(80, 22)
        closeBtn:SetText("Close")
        closeBtn:SetScript("OnClick", function() f:Hide() end)

        exportFrame = f
    end

    exportFrame.editBox:SetText(encodedStr)
    exportFrame.editBox:HighlightText()
    exportFrame.editBox:SetFocus()
    exportFrame:Show()
end

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

StaticPopupDialogs["HDG_IMPORT_STYLE"] = {
    text = "Paste an HDG style string to import:",
    button1 = "Import",
    button2 = "Cancel",
    hasEditBox = true,
    editBoxWidth = 350,
    OnShow = function(self)
        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 Import failed: " .. (err or "unknown error"))
            return
        end

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

        print("|cFF00FF00[HDG]|r Imported style: " .. 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 editorScrollDirty = false -- flag for throttled scroll render

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

    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()

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

    -- Back button
    local backBtn = CreateFrame("Button", nil, editorFrame, "UIPanelButtonTemplate")
    backBtn:SetSize(60, 20)
    backBtn:SetPoint("TOPLEFT", 6, -6)
    backBtn:SetText("< Back")
    backBtn:SetScript("OnClick", function()
        StylesTab:ShowLanding()
    end)

    -- Title
    local edTitle = editorFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    edTitle:SetPoint("LEFT", backBtn, "RIGHT", 8, 0)
    if scheme then ApplyFont(edTitle, scheme, "header") end
    edTitle:SetText("Style Creator")

    local edNote = editorFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    edNote:SetPoint("LEFT", edTitle, "RIGHT", 10, 0)
    edNote:SetText("|cFF888888(Showing collected items only, unreleased hidden)|r")
    if scheme then ApplyFont(edNote, scheme, "small") end

    -- Name input
    local nameLbl = editorFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    nameLbl:SetPoint("TOPLEFT", 10, -34)
    nameLbl:SetText("Name:")
    if scheme then ApplyFont(nameLbl, scheme, "small") end

    local nameBox = CreateFrame("EditBox", nil, editorFrame, "InputBoxTemplate")
    nameBox:SetSize(200, 20)
    nameBox:SetPoint("LEFT", nameLbl, "RIGHT", 6, 0)
    nameBox:SetAutoFocus(false)
    nameBox:SetMaxLetters(CONFIG.STYLE_EDITOR.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

    -- Description input
    local descLbl = editorFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    descLbl:SetPoint("TOPLEFT", 10, -58)
    descLbl:SetText("Desc:")
    if scheme then ApplyFont(descLbl, scheme, "small") end

    local descBox = CreateFrame("EditBox", nil, editorFrame, "InputBoxTemplate")
    descBox:SetSize(400, 20)
    descBox:SetPoint("LEFT", descLbl, "RIGHT", 6, 0)
    descBox:SetAutoFocus(false)
    descBox:SetMaxLetters(CONFIG.STYLE_EDITOR.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

    -- Selection summary (shows all selected tags across all facets)
    local summaryStr = editorFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    summaryStr:SetPoint("TOPLEFT", 10, -82)
    summaryStr:SetPoint("RIGHT", -10, 0)
    summaryStr:SetJustifyH("LEFT")
    summaryStr:SetWordWrap(true)
    summaryStr:SetMaxLines(3)
    if scheme then ApplyFont(summaryStr, scheme, "xsmall") end
    editorFrame.summaryStr = summaryStr

    -- Facet tab buttons
    local SE = CONFIG.STYLE_EDITOR
    local facetTabX = 10
    for i, ft in ipairs(SE.FACET_TABS) do
        local btn = HDG_UIHelpers.CreateChipButton(editorFrame, "", 60, false, nil)
        btn.tint = btn:CreateTexture(nil, "ARTWORK")
        btn.tint:SetAllPoints()
        btn.tint:SetBlendMode("ADD")
        btn.tint:Hide()
        btn.text:SetText(ft.label)
        btn:SetWidth(btn.text:GetStringWidth() + 14)
        btn:SetPoint("TOPLEFT", facetTabX, -98)
        btn.facetKey = ft.key
        btn.baseLabel = ft.label
        btn:SetScript("OnClick", function()
            editorFacetTab = ft.key
            StylesTab:RefreshEditorTags()
            for _, fb in ipairs(editorFacetButtons) do
                HDG_UIHelpers.UpdateChipButtonState(fb, fb.facetKey == editorFacetTab)
            end
        end)
        HDG_UIHelpers.UpdateChipButtonState(btn, ft.key == editorFacetTab)
        editorFacetButtons[i] = btn
        facetTabX = facetTabX + btn:GetWidth() + 4
    end

    -- Tag grid area (scrollable)
    local tagFrame = CreateFrame("Frame", nil, editorFrame, "BackdropTemplate")
    tagFrame:SetPoint("TOPLEFT", 6, -120)
    tagFrame:SetPoint("RIGHT", -6, 0)
    tagFrame:SetHeight(140)
    HDG_UIHelpers.ApplyBackdrop(tagFrame, "panel",
        {0.06, 0.06, 0.06, 0.5}, {0.15, 0.15, 0.15, 0.3})
    RegisterWidget(tagFrame, "Panel")

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

    local tagContent = CreateFrame("Frame", nil, tagScroll)
    tagContent:SetWidth(tagScroll:GetWidth() or 600)
    tagContent:SetHeight(1)
    tagScroll:SetScrollChild(tagContent)

    tagScroll:HookScript("OnSizeChanged", function(_, w)
        tagContent:SetWidth(w)
        StylesTab:RefreshEditorTags()
    end)

    editorFrame.tagContent = tagContent

    -- Clear All button (left) + Legend row
    local clearBtn = CreateFrame("Button", nil, editorFrame, "UIPanelButtonTemplate")
    clearBtn:SetSize(65, 18)
    clearBtn:SetPoint("TOPLEFT", tagFrame, "BOTTOMLEFT", 4, -1)
    clearBtn:SetText("Clear All")
    clearBtn:SetScript("OnClick", function()
        wipe(editorTagStates)
        wipe(editorLockedItems)
        wipe(editorRemovedItems)
        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()
        StylesTab:DebouncePreview()
    end)

    local legendStr = editorFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    legendStr:SetPoint("LEFT", clearBtn, "RIGHT", 6, 0)
    legendStr:SetText("|cFF94a3b8Click tags to build your style|r")
    if scheme then ApplyFont(legendStr, scheme, "small") end

    -- Preview counts (same line as legend, right-aligned)
    local previewCountStr = editorFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    previewCountStr:SetPoint("RIGHT", tagFrame, "BOTTOMRIGHT", -4, -10)
    if scheme then ApplyFont(previewCountStr, scheme, "small") end
    editorFrame.previewCountStr = previewCountStr

    -- Preview gallery grid (scrollable, fills space between tag grid and bottom buttons)
    local previewFrame = CreateFrame("Frame", nil, editorFrame, "BackdropTemplate")
    previewFrame:SetPoint("TOPLEFT", tagFrame, "BOTTOMLEFT", 0, -18)
    previewFrame:SetPoint("RIGHT", -6, 0)
    previewFrame:SetPoint("BOTTOM", 0, 32) -- leave room for bottom buttons
    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 600)
    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("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("TOP", 0, -20)
    helpStr:SetWidth(previewFrame:GetWidth() - 40)
    helpStr:SetJustifyH("CENTER")
    helpStr:SetWordWrap(true)
    helpStr:SetText(
        "|cFFDDDDDDBuild Your Style|r\n\n" ..
        "|cFF94a3b8Click tags above to define what your style looks like.\n" ..
        "Tags across different categories combine to narrow your selection.\n\n" ..
        "As you select tags, matching items appear here\n" ..
        "and green/red hints show which tags pair well together.|r"
    )
    if scheme then ApplyFont(helpStr, scheme, "small") end
    editorFrame.helpStr = helpStr

    -- Bottom buttons
    local saveBtn = CreateFrame("Button", nil, editorFrame, "UIPanelButtonTemplate")
    saveBtn:SetSize(70, 22)
    saveBtn:SetPoint("BOTTOMRIGHT", -10, 8)
    saveBtn:SetText("Save")
    saveBtn:SetScript("OnClick", function()
        StylesTab:SaveEditor()
    end)

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

    -- Delete button (only visible when editing)
    local deleteBtn = CreateFrame("Button", nil, editorFrame, "UIPanelButtonTemplate")
    deleteBtn:SetSize(70, 22)
    deleteBtn:SetPoint("RIGHT", cancelBtn, "LEFT", -6, 0)
    deleteBtn:SetText("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

    -- Export button
    local exportBtn = CreateFrame("Button", nil, editorFrame, "UIPanelButtonTemplate")
    exportBtn:SetSize(70, 22)
    exportBtn:SetPoint("BOTTOMLEFT", 10, 8)
    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
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()
    self:UpdateEditorPreview()
end

function StylesTab:RefreshEditorTags()
    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
    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)
    end

    -- Ensure tag state table exists for this facet
    if not editorTagStates[editorFacetTab] then
        editorTagStates[editorFacetTab] = {}
    end
    local facetStates = editorTagStates[editorFacetTab]

    -- Lay out tag buttons in a grid
    local SE = CONFIG.STYLE_EDITOR
    local contentWidth = tagContent:GetWidth() - 8
    local btnH = SE.TAG_BTN_HEIGHT
    local gap = SE.TAG_GRID_SPACING
    local x, y = 4, -4
    local maxX = contentWidth

    local scheme = GetScheme()

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

    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:SetBlendMode("ADD")
        end

        local state = facetStates[tag] or "off"
        local textColor
        if state == "query" then
            textColor = {0, 0.8, 0, 1}
            btn.tint:SetColorTexture(0.08, 0.35, 0.08, 1)
            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:SetColorTexture(0.04, 0.18, 0.04, 1)
                btn.tint:Show()
            elseif pct <= 5 then
                textColor = {0.6, 0.35, 0.35, 1}
                btn.tint:SetColorTexture(0.18, 0.04, 0.04, 1)
                btn.tint:Show()
            else
                btn.tint:Hide()
            end
        end

        btn.text:SetText(PrettyTagName(tag))
        local textW = btn.text:GetStringWidth() + 16
        local btnW = math.max(SE.TAG_BTN_MIN_WIDTH, math.min(textW, SE.TAG_BTN_MAX_WIDTH))
        btn:SetWidth(btnW)
        btn.text:SetTextColor(unpack(textColor))

        -- Wrap to next row if needed
        if x + btnW > maxX and x > 4 then
            x = 4
            y = y - (btnH + gap)
        end

        btn:ClearAllPoints()
        btn:SetPoint("TOPLEFT", x, 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()
        x = x + btnW + gap
    end

    -- Calculate total height
    local totalHeight = math.abs(y) + btnH + 8
    tagContent:SetHeight(totalHeight)

    -- Update facet tab highlights: tint tabs that have selections
    for _, fb in ipairs(editorFacetButtons) do
        local isActive = fb.facetKey == editorFacetTab
        HDG_UIHelpers.UpdateChipButtonState(fb, isActive)
        local hasTags = editorTagStates[fb.facetKey] and next(editorTagStates[fb.facetKey])
        if hasTags then
            fb.tint:SetColorTexture(0.08, 0.35, 0.08, 1)
            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 across all facets
    if editorFrame.summaryStr then
        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.summaryStr:SetText(table.concat(parts, "  "))
        else
            editorFrame.summaryStr:SetText("|cFF888888No tags selected yet|r")
        end
    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)
        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

    editorAllItems = result.sampleItems

    local countText = string.format(
        "|cFF00CC00Signature: %d|r  |cFFCCCC00Accent: %d|r  |cFFAAAAAAVersatile: %d|r  |cFFCC4444Clash: %d|r  |cFFDDDDDD(%d shown)|r",
        result.firstCount, result.secondCount, result.neutralCount, result.clashCount, #editorAllItems
    )
    editorFrame.previewCountStr:SetText(countText)

    -- Set total content height for scrolling
    local ST = CONFIG.STYLES_TAB
    local cellSize = ST.CELL_SIZE
    local cellGap = ST.CELL_SPACING
    local previewContent = editorFrame.previewContent
    local contentWidth = (previewContent:GetWidth() or 400) - 8
    local cols = math.max(1, math.floor(contentWidth / (cellSize + cellGap)))
    local totalRows = math.ceil(#editorAllItems / math.max(1, cols))
    previewContent:SetHeight(math.max(1, totalRows * (cellSize + cellGap) + 8))

    -- Render only visible cells
    self:RenderVisibleEditorCells()
end

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

    local ST = CONFIG.STYLES_TAB
    local cellSize = ST.CELL_SIZE
    local cellGap = ST.CELL_SPACING
    local rowHeight = cellSize + cellGap
    local previewContent = editorFrame.previewContent
    local previewScroll = editorFrame.previewScroll
    local contentWidth = (previewContent:GetWidth() or 400) - 8
    local cols = math.max(1, math.floor(contentWidth / (cellSize + cellGap)))

    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)

        -- Lock icon
        if not cell.lockIcon then
            cell.lockIcon = cell:CreateTexture(nil, "OVERLAY")
            cell.lockIcon:SetAtlas("tradeskills-icon-locked")
            cell.lockIcon:SetSize(28, 28)
            cell.lockIcon:SetPoint("TOPRIGHT", -5, -5)
            cell.lockIcon:SetBlendMode("ADD")
        end
        cell.lockIcon:SetShown(editorLockedItems[sample.itemID] or false)

        -- 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(sample.decorID)) or ("Item #" .. sample.itemID)
            GameTooltip:AddLine(tipName, 1, 1, 1)
            if editorLockedItems[sample.itemID] then
                GameTooltip:AddLine("Locked", 1, 0.8, 0.2)
            end
            GameTooltip:AddLine(" ", 1, 1, 1)
            GameTooltip:AddLine("Left-click: Lock/unlock item", 0.7, 0.7, 0.7)
            if not editorLockedItems[sample.itemID] then
                GameTooltip:AddLine("Right-click: Remove from preview", 0.7, 0.7, 0.7)
            end
            GameTooltip:Show()
        end)
        cell:SetScript("OnLeave", function(self)
            self.hoverBg:Hide()
            GameTooltip:Hide()
        end)

        -- Click handlers
        cell:RegisterForClicks("LeftButtonUp", "RightButtonUp")
        cell:SetScript("OnClick", function(self, button)
            if button == "LeftButton" then
                if IsShiftKeyDown() then
                    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
                    if editorLockedItems[sample.itemID] then
                        editorLockedItems[sample.itemID] = nil
                    else
                        editorLockedItems[sample.itemID] = true
                    end
                    self.lockIcon:SetShown(editorLockedItems[sample.itemID] or false)
                    EditorAutoSave()
                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

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

    -- Validate
    local name = workingDef.displayName
    if not name or name == "" then
        print("|cFFFF4444[HDG]|r Style name cannot be 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 Need at least one tag selected.")
        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 Maximum " .. SE.MAX_CUSTOM_STYLES .. " custom styles reached.")
            return
        end
    end

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

    print("|cFF00FF00[HDG]|r Saved custom style: " .. name)
    selectedStyle = nil
    self:ShowLanding()
end
