local addonName, privateDB = ...
local HDG = _G.HousingDecorGuide

-- ============================================================================
-- DYE PANEL
-- Floating panel for browsing all housing dyes, checking ownership,
-- and viewing crafting info (pigments + herbs)
-- ============================================================================

HDG_DyePanel = {}

-- If VamooseDyeStudio is enabled, it provides the dye panel via bridge — skip all HDG code
if C_AddOns and C_AddOns.GetAddOnInfo
   and select(4, C_AddOns.GetAddOnInfo("VamooseDyeStudio")) then
    -- Forward to VDS directly (safety net if bridge hasn't replaced this global yet)
    local function fwd(method, ...)
        local vds = _G.VDS_DyePanel
        if vds and vds[method] then return vds[method](vds, ...) end
    end
    function HDG_DyePanel:Init(...) fwd("Init", ...) end
    function HDG_DyePanel.Toggle() if _G.VDS_DyePanel then _G.VDS_DyePanel.Toggle() end end
    function HDG_DyePanel.Show() if _G.VDS_DyePanel then _G.VDS_DyePanel.Show() end end
    function HDG_DyePanel.Hide() if _G.VDS_DyePanel then _G.VDS_DyePanel.Hide() end end
    function HDG_DyePanel.IsShown() return _G.VDS_DyePanel and _G.VDS_DyePanel.IsShown() or false end
    function HDG_DyePanel.Refresh() if _G.VDS_DyePanel then _G.VDS_DyePanel.Refresh() end end
    function HDG_DyePanel.RebuildFilterButtons() if _G.VDS_DyePanel then _G.VDS_DyePanel.RebuildFilterButtons() end end
    function HDG_DyePanel.IsLiveMode() return _G.VDS_DyePanel and _G.VDS_DyePanel.IsLiveMode() or false end
    function HDG_DyePanel.FilterToColorGroup(g) if _G.VDS_DyePanel then _G.VDS_DyePanel.FilterToColorGroup(g) end end
    return
end

local widget          -- FloatingWidget result table
local CONFIG
local scrollContent
local selectedDyeID
local activeColorGroup  -- nil = All, or color group name like "Red"
local activeTheme       -- nil = not filtered, or theme name like "Alliance"
local activeCategory    -- nil = All, or Blizzard category ID
local filterMode = "category"  -- "category", "color", "theme", or "custom"
local dyeRows = {}    -- row pool
local swatchPool = {} -- reusable swatch textures
local created = false
local editingThemeName = nil  -- name of custom theme being edited, nil when not editing
local customSwatchPool = {}   -- reusable swatch textures for palette strip

-- Housing editor live preview state
local liveSlotID = nil         -- active dye slot ID for live preview
local liveMode = false         -- true when customize mode has dyeable decor selected
local liveDecorName = nil      -- name of selected decor
local liveDyeSlots = nil       -- dyeSlots array from decorInstanceInfo
local hooksInstalled = false

local GetScheme = function() return HDG_UIHelpers and HDG_UIHelpers.GetScheme() end
local RegisterWidget = function(w, t) if HDG_UIHelpers then HDG_UIHelpers.RegisterWidget(w, t) end end

-- (Category names come from API at runtime — no hardcoded list needed)

-- ============================================================================
-- HELPERS
-- ============================================================================

local function C_Accent()  return CONFIG and CONFIG.TEXT_COLORS.ACCENT or "|cFF14b8a6" end
local function C_Dim()     return CONFIG and CONFIG.TEXT_COLORS.MUTED or "|cFF888888" end
local function C_Text()    return CONFIG and CONFIG.TEXT_COLORS.WHITE or "|cFFFFFFFF" end
local function C_Success() return CONFIG and CONFIG.TEXT_COLORS.SUCCESS or "|cFF00FF00" end
local function C_Gold()    return CONFIG and CONFIG.TEXT_COLORS.GOLD or "|cFFfcd34d" end

-- ============================================================================
-- ROW POOLING
-- ============================================================================

local function GetPooledRow(parent)
    for _, row in ipairs(dyeRows) do
        if not row.inUse then
            row.inUse = true
            row:SetParent(parent)
            row:Show()
            return row
        end
    end

    -- Create new row
    local row = CreateFrame("Button", nil, parent)
    row:SetHeight(24)
    row.inUse = true

    -- Name (left side)
    local nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    nameText:SetPoint("LEFT", 4, 0)
    nameText:SetWidth(120)
    nameText:SetJustifyH("LEFT")
    nameText:SetWordWrap(false)
    row.nameText = nameText

    -- Count (right side, fixed width so swatch stays consistent)
    local countText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    countText:SetPoint("RIGHT", -4, 0)
    countText:SetWidth(34)
    countText:SetJustifyH("RIGHT")
    row.countText = countText

    -- Color swatch (wide bar between name and count)
    local swatch = row:CreateTexture(nil, "ARTWORK")
    swatch:SetPoint("LEFT", nameText, "RIGHT", 4, 0)
    swatch:SetPoint("RIGHT", row, "RIGHT", -42, 0)
    swatch:SetHeight(14)
    swatch:SetTexture("Interface\\Buttons\\WHITE8x8")
    row.swatch = swatch

    -- Hover highlight
    local highlight = row:CreateTexture(nil, "HIGHLIGHT")
    highlight:SetAllPoints()
    highlight:SetTexture("Interface\\Buttons\\WHITE8x8")
    highlight:SetVertexColor(1, 1, 1, 0.05)

    -- Selection highlight
    local selected = row:CreateTexture(nil, "BACKGROUND")
    selected:SetAllPoints()
    selected:SetTexture("Interface\\Buttons\\WHITE8x8")
    selected:SetVertexColor(0.08, 0.5, 0.45, 0.15)
    selected:Hide()
    row.selectedBg = selected

    table.insert(dyeRows, row)
    return row
end

local function ReleaseAllRows()
    for _, row in ipairs(dyeRows) do
        row.inUse = false
        row:Hide()
        row.selectedBg:Hide()
    end
end

-- ============================================================================
-- CREATE UI
-- ============================================================================

local function CreatePanel()
    if created then return end
    created = true

    local FW = CONFIG.FLOATING_WIDGET
    local panelWidth = FW.DYE_PANEL_WIDTH
    local panelHeight = FW.DYE_PANEL_HEIGHT

    widget = HDG.UI:FloatingWidget({
        name = "HDG_DyePanelFrame",
        width = panelWidth,
        height = panelHeight,
        strata = "DIALOG",
        onPositionSaved = function(point, relativePoint, x, y)
            if HDG.Store then
                HDG.Store.state.ui.dyePanelPosition = { point, relativePoint, x, y }
                HDG.Store:QueueSave()
            end
        end,
    })

    local frame = widget.frame

    -- Restore saved position
    if HDG.Store then
        local pos = HDG.Store:GetState().ui.dyePanelPosition
        if pos and pos[1] then
            frame:ClearAllPoints()
            frame:SetPoint(pos[1], UIParent, pos[2] or pos[1], pos[3] or 0, pos[4] or 0)
        end
    end

    frame:Hide()

    -- ====================================================================
    -- HEADER: Spectrum bar + Title + Close button
    -- ====================================================================
    -- Rainbow spectrum behind title (12 color stops)
    local spectrumColors = {
        { 0.85, 0.15, 0.15 }, -- red
        { 0.90, 0.40, 0.10 }, -- orange
        { 0.90, 0.80, 0.15 }, -- yellow
        { 0.30, 0.70, 0.20 }, -- green
        { 0.15, 0.60, 0.55 }, -- teal
        { 0.15, 0.40, 0.80 }, -- blue
        { 0.45, 0.20, 0.70 }, -- purple
        { 0.70, 0.30, 0.55 }, -- pink
    }
    local specW = math.floor((panelWidth - 20) / #spectrumColors)
    for i, c in ipairs(spectrumColors) do
        local seg = frame:CreateTexture(nil, "ARTWORK", nil, 1)
        seg:SetSize(specW, 3)
        seg:SetPoint("TOPLEFT", 10 + (i - 1) * specW, -28)
        seg:SetTexture("Interface\\Buttons\\WHITE8x8")
        seg:SetVertexColor(c[1], c[2], c[3], 0.7)
    end

    -- Rainbow gradient title — each letter interpolated across the spectrum
    local titleText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    titleText:SetPoint("TOPLEFT", 10, -10)
    local scheme = GetScheme()
    if scheme and HDG.Theme then
        HDG.Theme.ApplyFont(titleText, scheme, "header")
    end
    local titleChars = { "V","a","m","o","o","s","e","'","s"," ","D","y","e"," ","S","t","u","d","i","o" }
    local vis = 0
    for _, ch in ipairs(titleChars) do if ch ~= " " then vis = vis + 1 end end
    local titleStr, ci = "", 0
    for _, ch in ipairs(titleChars) do
        if ch == " " then
            titleStr = titleStr .. " "
        else
            ci = ci + 1
            local t = (ci - 1) / (vis - 1) * (#spectrumColors - 1)
            local i1 = math.floor(t) + 1
            local i2 = math.min(i1 + 1, #spectrumColors)
            local f = t - math.floor(t)
            local r = spectrumColors[i1][1] * (1 - f) + spectrumColors[i2][1] * f
            local g = spectrumColors[i1][2] * (1 - f) + spectrumColors[i2][2] * f
            local b = spectrumColors[i1][3] * (1 - f) + spectrumColors[i2][3] * f
            titleStr = titleStr .. string.format("|cFF%02X%02X%02X%s|r", r * 255, g * 255, b * 255, ch)
        end
    end
    titleText:SetText(titleStr)

    -- Close button (using framework CreateHeaderButton)
    local closeBtnSize = FW.HEADER_BTN_SIZE - 2
    local closeBtn = widget.CreateHeaderButton(closeBtnSize, "Close")
    closeBtn:ClearAllPoints()
    closeBtn:SetPoint("TOPRIGHT", -FW.HEADER_BTN_MARGIN, -(FW.HEADER_BTN_MARGIN - 2))
    local closeTex = closeBtn:CreateTexture(nil, "ARTWORK")
    closeTex:SetSize(10, 10)
    closeTex:SetPoint("CENTER")
    closeTex:SetTexture(CONFIG.ADDON_TEXTURES.CLOSE)
    closeBtn:SetScript("OnClick", function() frame:Hide() end)

    -- Help "?" button in header
    local helpBtn = CreateFrame("Button", nil, frame)
    helpBtn:SetSize(16, 16)
    helpBtn:SetPoint("TOPRIGHT", closeBtn, "TOPLEFT", -4, -2)
    local helpLabel = helpBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    helpLabel:SetPoint("CENTER")
    helpLabel:SetText(C_Dim() .. "?|r")
    helpBtn:SetScript("OnEnter", function(self)
        helpLabel:SetText(C_Accent() .. "?|r")
    end)
    helpBtn:SetScript("OnLeave", function(self)
        helpLabel:SetText(C_Dim() .. "?|r")
    end)
    helpBtn:SetScript("OnClick", function()
        StaticPopup_Show("HDG_DYE_STUDIO_HELP")
    end)

    -- "Replace Blizzard UI" toggle checkbox in header
    local replaceCheck = CreateFrame("CheckButton", nil, frame, "UICheckButtonTemplate")
    replaceCheck:SetSize(20, 20)
    replaceCheck:SetPoint("TOPRIGHT", helpBtn, "TOPLEFT", -2, 1)
    if not _G.HDG_DB then _G.HDG_DB = {} end
    replaceCheck:SetChecked(_G.HDG_DB.dyeReplaceBlizzard or false)
    local replaceLabel = replaceCheck:CreateFontString(nil, "OVERLAY", "GameFontHighlightExtraSmall")
    replaceLabel:SetPoint("RIGHT", replaceCheck, "LEFT", -1, 0)
    replaceLabel:SetText(C_Dim() .. "Replace Blizzard UI|r")
    replaceCheck:SetScript("OnClick", function(self)
        if liveMode then
            self:SetChecked(not self:GetChecked())
            return
        end
        if not _G.HDG_DB then _G.HDG_DB = {} end
        _G.HDG_DB.dyeReplaceBlizzard = self:GetChecked()
    end)
    replaceCheck:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_BOTTOMLEFT")
        GameTooltip:SetText("Replace Blizzard Dye UI", 1, 1, 1)
        GameTooltip:AddLine("Auto-opens Dye Studio and hides\nBlizzard's dye panel when editing decor.\nCannot change while editing.", 0.7, 0.7, 0.7, true)
        GameTooltip:Show()
    end)
    replaceCheck:SetScript("OnLeave", function() GameTooltip:Hide() end)
    widget.replaceCheck = replaceCheck

    -- ====================================================================
    -- PROGRESS BAR
    -- ====================================================================
    local sidebarWidth = FW.DYE_SIDEBAR_WIDTH
    local progressFrame = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    progressFrame:SetPoint("TOPLEFT", 8, -36)
    progressFrame:SetPoint("TOPRIGHT", -(sidebarWidth + 12), -36)
    progressFrame:SetHeight(18)
    progressFrame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        tile = false, edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 },
    })
    scheme = GetScheme()
    if scheme then
        progressFrame:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.8)
        progressFrame:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a)
    end
    RegisterWidget(progressFrame, "Panel")

    local progressBar = progressFrame:CreateTexture(nil, "ARTWORK")
    progressBar:SetPoint("TOPLEFT", 1, -1)
    progressBar:SetHeight(16)
    progressBar:SetTexture("Interface\\Buttons\\WHITE8x8")
    progressBar:SetVertexColor(0.08, 0.72, 0.65, 0.6) -- teal
    widget.progressBar = progressBar

    local progressText = progressFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightExtraSmall")
    progressText:SetPoint("CENTER")
    progressText:SetJustifyH("CENTER")
    widget.progressText = progressText

    -- Palette strip (shows custom theme swatches between progress and scroll)
    local paletteStrip = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    paletteStrip:SetPoint("TOPLEFT", progressFrame, "BOTTOMLEFT", 0, -1)
    paletteStrip:SetPoint("TOPRIGHT", progressFrame, "BOTTOMRIGHT", 0, -1)
    paletteStrip:SetHeight(FW.DYE_PALETTE_STRIP_HEIGHT)
    paletteStrip:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        tile = false, edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 },
    })
    scheme = GetScheme()
    if scheme then
        paletteStrip:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.6)
        paletteStrip:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a * 0.5)
    end
    RegisterWidget(paletteStrip, "Panel")
    paletteStrip:Hide()
    widget.paletteStrip = paletteStrip

    local paletteLabel = paletteStrip:CreateFontString(nil, "OVERLAY", "GameFontHighlightExtraSmall")
    paletteLabel:SetPoint("LEFT", 4, 0)
    paletteLabel:SetWidth(80)
    paletteLabel:SetJustifyH("LEFT")
    paletteLabel:SetWordWrap(false)
    widget.paletteLabel = paletteLabel

    local paletteSwatchContainer = CreateFrame("Frame", nil, paletteStrip)
    paletteSwatchContainer:SetPoint("LEFT", paletteLabel, "RIGHT", 2, 0)
    paletteSwatchContainer:SetPoint("RIGHT", -4, 0)
    paletteSwatchContainer:SetHeight(FW.DYE_PALETTE_STRIP_HEIGHT - 4)
    widget.paletteSwatchContainer = paletteSwatchContainer

    -- Live preview indicator bar (between progress and scroll, hidden by default)
    local liveBar = CreateFrame("Frame", nil, frame)
    liveBar:SetPoint("TOPLEFT", progressFrame, "BOTTOMLEFT", 0, -1)
    liveBar:SetPoint("TOPRIGHT", progressFrame, "BOTTOMRIGHT", 0, -1)
    liveBar:SetHeight(0.1)
    liveBar:Hide()
    widget.liveBar = liveBar

    local liveText = liveBar:CreateFontString(nil, "OVERLAY", "GameFontHighlightExtraSmall")
    liveText:SetPoint("LEFT", 2, 0)
    liveText:SetPoint("RIGHT", -2, 0)
    liveText:SetJustifyH("LEFT")
    liveText:SetWordWrap(false)
    widget.liveText = liveText

    -- ====================================================================
    -- SIDEBAR (vertical filter buttons on right)
    -- ====================================================================
    local sidebar = CreateFrame("Frame", nil, frame)
    sidebar:SetPoint("TOPRIGHT", -8, -36)
    sidebar:SetPoint("BOTTOMRIGHT", -8, 120)
    sidebar:SetWidth(sidebarWidth)
    widget.sidebar = sidebar

    -- Mode toggle at top of sidebar
    local modeBtnH = 15
    local function SwitchMode(mode)
        filterMode = mode
        activeColorGroup = nil
        activeTheme = nil
        activeCategory = nil
        editingThemeName = nil
        if widget and widget.paletteStrip then widget.paletteStrip:Hide() end
        HDG_DyePanel.RebuildFilterButtons()
        HDG_DyePanel.Refresh()
    end

    local modeCatBtn = HDG.UI:Button(sidebar, "Category", sidebarWidth, modeBtnH, function() SwitchMode("category") end)
    modeCatBtn:SetPoint("TOPLEFT", 0, 0)
    modeCatBtn:SetPoint("TOPRIGHT", 0, 0)
    modeCatBtn:GetFontString():SetFontObject(GameFontHighlightExtraSmall)
    widget.modeCatBtn = modeCatBtn

    local modeColorBtn = HDG.UI:Button(sidebar, "Color", sidebarWidth, modeBtnH, function() SwitchMode("color") end)
    modeColorBtn:SetPoint("TOPLEFT", modeCatBtn, "BOTTOMLEFT", 0, -1)
    modeColorBtn:SetPoint("TOPRIGHT", modeCatBtn, "BOTTOMRIGHT", 0, -1)
    modeColorBtn:GetFontString():SetFontObject(GameFontHighlightExtraSmall)
    widget.modeColorBtn = modeColorBtn

    local modeThemeBtn = HDG.UI:Button(sidebar, "Theme", sidebarWidth, modeBtnH, function() SwitchMode("theme") end)
    modeThemeBtn:SetPoint("TOPLEFT", modeColorBtn, "BOTTOMLEFT", 0, -1)
    modeThemeBtn:SetPoint("TOPRIGHT", modeColorBtn, "BOTTOMRIGHT", 0, -1)
    modeThemeBtn:GetFontString():SetFontObject(GameFontHighlightExtraSmall)
    widget.modeThemeBtn = modeThemeBtn

    local modeCustomBtn = HDG.UI:Button(sidebar, "Custom", sidebarWidth, modeBtnH, function() SwitchMode("custom") end)
    modeCustomBtn:SetPoint("TOPLEFT", modeThemeBtn, "BOTTOMLEFT", 0, -1)
    modeCustomBtn:SetPoint("TOPRIGHT", modeThemeBtn, "BOTTOMRIGHT", 0, -1)
    modeCustomBtn:GetFontString():SetFontObject(GameFontHighlightExtraSmall)
    widget.modeCustomBtn = modeCustomBtn

    -- Filter button container (below mode toggle)
    local catBtnFrame = CreateFrame("Frame", nil, sidebar)
    catBtnFrame:SetPoint("TOPLEFT", modeCustomBtn, "BOTTOMLEFT", 0, -4)
    catBtnFrame:SetPoint("TOPRIGHT", modeCustomBtn, "BOTTOMRIGHT", 0, -4)
    catBtnFrame:SetPoint("BOTTOM", 0, 0)
    widget.catBtnFrame = catBtnFrame
    widget.catButtons = {}
    widget.filterBtnPool = {}

    -- ====================================================================
    -- SCROLL AREA (dye list — left of sidebar)
    -- ====================================================================
    local scrollFrame = CreateFrame("ScrollFrame", nil, frame, "UIPanelScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", 8, -58)
    scrollFrame:SetPoint("BOTTOMRIGHT", sidebar, "BOTTOMLEFT", -22, 0)
    HDG_UIHelpers.StyleScrollBar(scrollFrame)
    RegisterWidget(scrollFrame, "ScrollFrame")

    local contentWidth = panelWidth - sidebarWidth - 38 -- 8 pad + 22 scrollbar + 8 gap
    scrollContent = CreateFrame("Frame", nil, scrollFrame)
    scrollContent:SetWidth(contentWidth)
    scrollContent:SetHeight(1)
    scrollFrame:SetScrollChild(scrollContent)
    widget.scrollFrame = scrollFrame
    widget.scrollContent = scrollContent

    -- ====================================================================
    -- DETAIL SECTION (bottom)
    -- ====================================================================
    local detailFrame = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    detailFrame:SetPoint("BOTTOMLEFT", 8, 8)
    detailFrame:SetPoint("BOTTOMRIGHT", -8, 8)
    detailFrame:SetHeight(108)
    detailFrame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        tile = false, edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 },
    })
    scheme = GetScheme()
    if scheme then
        detailFrame:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.9)
        detailFrame:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a)
    end
    RegisterWidget(detailFrame, "Panel")

    -- Detail labels
    local detailLines = {}
    for i = 1, 6 do
        local line = detailFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        line:SetPoint("TOPLEFT", 8, -4 - (i - 1) * 15)
        line:SetPoint("RIGHT", -8, 0)
        line:SetJustifyH("LEFT")
        line:SetWordWrap(false)
        line:SetShadowOffset(0, 0)
        detailLines[i] = line
    end
    widget.detailLines = detailLines
    widget.detailFrame = detailFrame

    -- Live status text (inside detail frame, shown in live mode)
    local liveStatus = detailFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    liveStatus:SetPoint("TOP", 0, -6)
    liveStatus:SetJustifyH("CENTER")
    liveStatus:Hide()
    widget.liveStatus = liveStatus

    -- Slot selector bar (lives inside detailFrame in live mode, centered)
    local slotBar = CreateFrame("Frame", nil, detailFrame)
    slotBar:SetFrameLevel(detailFrame:GetFrameLevel() + 10)
    slotBar:SetPoint("TOP", liveStatus, "BOTTOM", 0, -4)
    slotBar:SetHeight(0.1)
    slotBar:Hide()
    widget.slotBar = slotBar
    widget.slotBtns = {}

    -- Action bar (Apply/Cancel for full replacement mode, below detail)
    local actionBar = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    actionBar:SetPoint("BOTTOMLEFT", 8, 8)
    actionBar:SetPoint("BOTTOMRIGHT", -8, 8)
    actionBar:SetHeight(28)
    actionBar:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        tile = false, edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 },
    })
    scheme = GetScheme()
    if scheme then
        actionBar:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.9)
        actionBar:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a)
    end
    RegisterWidget(actionBar, "Panel")
    actionBar:Hide()
    widget.actionBar = actionBar

    local cancelBtn = HDG.UI:Button(actionBar, "Cancel", 70, 22, function() end)
    cancelBtn:SetPoint("LEFT", 4, 0)
    widget.cancelBtn = cancelBtn

    local applyBtn = HDG.UI:Button(actionBar, "Apply", 70, 22, function() end)
    applyBtn:SetPoint("RIGHT", -4, 0)
    widget.applyBtn = applyBtn

    local costText = actionBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    costText:SetPoint("CENTER")
    costText:SetJustifyH("CENTER")
    widget.costText = costText

    -- Build initial filter buttons
    HDG_DyeData.EnsureInitialized()
    HDG_DyePanel.RebuildFilterButtons()

    -- Register for ownership updates
    if HDG.EventBus then
        HDG.EventBus:Register("HDG_DYE_OWNERSHIP_CHANGED", function()
            if frame:IsShown() then
                HDG_DyePanel.Refresh()
            end
        end)
    end
end

-- ============================================================================
-- REBUILD FILTER BUTTONS (called on mode switch)
-- ============================================================================

function HDG_DyePanel.RebuildFilterButtons()
    if not widget or not widget.catBtnFrame then return end
    local parent = widget.catBtnFrame

    -- Hide all existing filter buttons
    for _, btn in pairs(widget.filterBtnPool) do btn:Hide() end
    widget.catButtons = {}

    local btnHeight = 15
    local btnGap = 1
    local btnY = 0

    local poolIdx = 0
    local function MakeBtn(label, rgb, onClick)
        poolIdx = poolIdx + 1
        local btn = widget.filterBtnPool[poolIdx]
        if not btn then
            btn = HDG.UI:Button(parent, label, 10, btnHeight, onClick) -- width set by anchors
            widget.filterBtnPool[poolIdx] = btn
        else
            btn:SetText(label)
            btn:SetScript("OnClick", onClick)
            btn:Show()
        end
        btn:ClearAllPoints()
        btn:SetPoint("TOPLEFT", 0, -btnY)
        btn:SetPoint("TOPRIGHT", 0, -btnY)
        btn:SetHeight(btnHeight)
        local fs = btn:GetFontString()
        fs:SetFontObject(GameFontHighlightExtraSmall)
        fs:SetWordWrap(false)
        fs:SetWidth(CONFIG.FLOATING_WIDGET.DYE_SIDEBAR_WIDTH - 6)
        fs:SetJustifyH("CENTER")
        if rgb then
            btn:SetBackdropColor(rgb.r, rgb.g, rgb.b, 0.35)
            btn._customBg = { rgb.r, rgb.g, rgb.b, 0.35 }
        else
            local s = HDG.Theme.GetCurrentScheme()
            if s then btn:SetBackdropColor(s.panel.r, s.panel.g, s.panel.b, s.panel.a) end
            btn._customBg = nil
        end
        -- Override OnLeave to restore custom bg color instead of default scheme
        btn:SetScript("OnLeave", function(self)
            if self._customBg then
                self:SetBackdropColor(unpack(self._customBg))
            else
                local c = GetScheme()
                if c then self:SetBackdropColor(c.button_normal.r, c.button_normal.g, c.button_normal.b, c.button_normal.a) end
            end
        end)
        btnY = btnY + btnHeight + btnGap
        return btn
    end

    -- Highlight mode toggle buttons
    local s = HDG.Theme.GetCurrentScheme()
    if s and widget.modeColorBtn then
        local modeButtons = { category = widget.modeCatBtn, color = widget.modeColorBtn, theme = widget.modeThemeBtn, custom = widget.modeCustomBtn }
        for mode, btn in pairs(modeButtons) do
            if mode == filterMode then
                btn:SetBackdropBorderColor(s.accent.r, s.accent.g, s.accent.b, 1)
            else
                btn:SetBackdropBorderColor(s.border.r, s.border.g, s.border.b, s.border.a)
            end
        end
    end

    -- "All" button (shared by all modes)
    local allBtn = MakeBtn("All", nil, function()
        activeColorGroup = nil
        activeTheme = nil
        activeCategory = nil
        HDG_DyePanel.Refresh()
    end)
    widget.catButtons["All"] = allBtn

    if filterMode == "category" then
        for _, catID in ipairs(HDG_DyeData.GetCategories()) do
            local cid = catID
            local catName = HDG_DyeData.GetCategoryName(cid) or ("Category " .. cid)
            local btn = MakeBtn(catName, nil, function()
                activeCategory = cid
                activeColorGroup = nil
                activeTheme = nil
                HDG_DyePanel.Refresh()
            end)
            widget.catButtons[cid] = btn
        end
    elseif filterMode == "color" then
        for _, groupName in ipairs(HDG_DyeData.GetColorGroups()) do
            local gn = groupName
            local btn = MakeBtn(gn, HDG_DyeData.GetColorGroupRGB(gn), function()
                activeColorGroup = gn
                activeTheme = nil
                activeCategory = nil
                HDG_DyePanel.Refresh()
            end)
            widget.catButtons[gn] = btn
        end
    elseif filterMode == "theme" then
        for _, themeInfo in ipairs(HDG_DyeData.GetThemes()) do
            local tn = themeInfo.name
            local btn = MakeBtn(tn, themeInfo.rgb, function()
                activeTheme = tn
                activeColorGroup = nil
                activeCategory = nil
                HDG_DyePanel.Refresh()
            end)
            widget.catButtons[tn] = btn
        end
    else -- custom
        -- "+ New" button
        MakeBtn("+ New", nil, function()
            local theme = HDG_DyeData.CreateCustomTheme()
            if theme then
                activeTheme = theme.name
                editingThemeName = theme.name
                HDG_DyePanel.RebuildFilterButtons()
                HDG_DyePanel.Refresh()
            end
        end)
        -- "Import" button
        MakeBtn("Import", nil, function()
            StaticPopup_Show("HDG_IMPORT_CUSTOM_THEME")
        end)
        -- Existing custom themes
        for _, themeInfo in ipairs(HDG_DyeData.GetCustomThemes()) do
            local tn = themeInfo.name
            local btn = MakeBtn(tn, themeInfo.rgb, function()
                activeTheme = tn
                editingThemeName = tn
                activeColorGroup = nil
                activeCategory = nil
                HDG_DyePanel.Refresh()
            end)
            widget.catButtons["custom:" .. tn] = btn
        end
    end
end

-- ============================================================================
-- UPDATE PROGRESS BAR
-- ============================================================================

local function UpdateProgress()
    if not widget then return end
    local owned = HDG_DyeData.GetTotalDyesOwned()
    local total = HDG_DyeData.GetTotalDyeCount()
    if total == 0 then total = 62 end -- fallback

    widget.progressText:SetText(string.format("Owned: %d/%d", owned, total))

    local pct = total > 0 and (owned / total) or 0
    local barParent = widget.progressBar:GetParent()
    local barWidth = (barParent:GetWidth() - 2) * pct
    if barWidth < 1 then barWidth = 1 end
    widget.progressBar:SetWidth(barWidth)
end

-- ============================================================================
-- UPDATE DETAIL SECTION
-- ============================================================================

local function UpdateDetail(dyeColorID)
    if not widget or not widget.detailLines then return end
    local lines = widget.detailLines

    if not dyeColorID then
        -- Show theme description when filtering by theme and no dye selected
        local themeName = activeTheme
        if themeName then
            local desc = HDG_DyeData.GetThemeDesc(themeName)
            lines[1]:SetText(C_Accent() .. themeName .. " Palette|r")
            lines[2]:SetText(desc and (C_Gold() .. desc .. "|r") or "")
            local dyes = HDG_DyeData.GetDyesInTheme(themeName)
            lines[3]:SetText(C_Dim() .. "Dyes:|r " .. C_Text() .. (dyes and #dyes or 0) .. "|r")
            for i = 4, 6 do lines[i]:SetText("") end
        else
            for i = 1, 6 do lines[i]:SetText("") end
        end
        return
    end

    local dyeInfo = HDG_DyeData.GetDyeInfo(dyeColorID)
    if not dyeInfo then
        for i = 1, 6 do lines[i]:SetText("") end
        return
    end

    -- Line 1: Dye name
    lines[1]:SetText(C_Accent() .. (dyeInfo.name or "Unknown") .. "|r")

    -- Line 2: Color group + Blizzard category
    local colorGroup = dyeInfo._colorGroup or "?"
    local blizzCat = dyeInfo._categoryName or HDG_DyeData.GetCategoryName(dyeInfo.dyeColorCategoryID) or "?"
    lines[2]:SetText(C_Dim() .. "Color:|r " .. C_Text() .. colorGroup .. "|r  " .. C_Dim() .. "Type:|r " .. C_Text() .. blizzCat .. "|r")

    -- Line 3: Owned count
    local count = dyeInfo.numOwned or 0
    local countColor = count > 0 and C_Success() or C_Dim()
    lines[3]:SetText(C_Dim() .. "Owned:|r " .. countColor .. count .. "|r")

    -- Line 4: Item link (if available)
    if dyeInfo.itemID then
        local itemName = C_Item.GetItemNameByID(dyeInfo.itemID)
        if itemName then
            lines[4]:SetText(C_Dim() .. "Item:|r " .. C_Text() .. itemName .. "|r")
        else
            lines[4]:SetText(C_Dim() .. "Item:|r " .. C_Text() .. "Item #" .. dyeInfo.itemID .. "|r")
        end
    else
        lines[4]:SetText("")
    end

    -- Line 5-6: Crafting info from HDG_DyeCraftingDB
    if HDG_DyeCraftingDB and dyeInfo.itemID then
        local pigmentID, pigmentInfo = HDG_DyeCraftingDB.GetPigmentForDye(dyeInfo.itemID)
        if pigmentInfo then
            lines[5]:SetText(C_Dim() .. "Pigment:|r " .. C_Text() .. pigmentInfo.name .. "|r")
            local herbCount = pigmentInfo.herbs and #pigmentInfo.herbs or 0
            lines[6]:SetText(C_Dim() .. "Cost:|r " .. C_Text() .. pigmentInfo.herbQty .. " herbs|r " .. C_Dim() .. "(" .. herbCount .. " options)|r")
        else
            lines[5]:SetText("")
            lines[6]:SetText("")
        end
    else
        lines[5]:SetText("")
        lines[6]:SetText("")
    end
end

-- ============================================================================
-- PALETTE STRIP (shows custom theme swatches)
-- ============================================================================

local function UpdatePaletteStrip()
    if not widget or not widget.paletteStrip then return end

    -- Only show in custom mode with an active theme
    if filterMode ~= "custom" or not editingThemeName then
        widget.paletteStrip:Hide()
        -- Restore scroll top
        widget.scrollFrame:SetPoint("TOPLEFT", 8, -58)
        return
    end

    local dyes = HDG_DyeData.GetCustomThemeDyes(editingThemeName)
    if #dyes == 0 then
        widget.paletteStrip:Hide()
        widget.scrollFrame:SetPoint("TOPLEFT", 8, -58)
        return
    end

    widget.paletteLabel:SetText(C_Accent() .. editingThemeName .. "|r")
    widget.paletteStrip:Show()

    -- Shift scroll down by palette strip height
    local FW = CONFIG.FLOATING_WIDGET
    widget.scrollFrame:SetPoint("TOPLEFT", 8, -58 - FW.DYE_PALETTE_STRIP_HEIGHT - 2)

    -- Render swatch textures from pool
    for _, sw in ipairs(customSwatchPool) do sw:Hide() end
    local container = widget.paletteSwatchContainer
    local swSize = FW.DYE_PALETTE_STRIP_HEIGHT - 6
    local gap = 2
    for i, dyeID in ipairs(dyes) do
        local sw = customSwatchPool[i]
        if not sw then
            sw = container:CreateTexture(nil, "ARTWORK")
            sw:SetTexture("Interface\\Buttons\\WHITE8x8")
            customSwatchPool[i] = sw
        end
        sw:ClearAllPoints()
        sw:SetSize(swSize, swSize)
        sw:SetPoint("LEFT", (i - 1) * (swSize + gap), 0)
        local info = HDG_DyeData.GetDyeInfo(dyeID)
        if info and info.swatchColorStart then
            local c = info.swatchColorStart
            sw:SetVertexColor(c.r, c.g, c.b, 1)
        else
            sw:SetVertexColor(0.4, 0.4, 0.4, 1)
        end
        sw:Show()
    end
end

-- ============================================================================
-- CUSTOM THEME DETAIL (editing mode override for detail section)
-- ============================================================================

local function ShowThemeManagementButtons()
    if not widget or not widget.detailFrame then return end
    local df = widget.detailFrame

    -- Lazy-create management buttons
    if not widget.mgmtRenameBtn then
        local btnW, btnH = 60, 18
        local renameBtn = HDG.UI:Button(df, "Rename", btnW, btnH, function()
            if editingThemeName then
                StaticPopup_Show("HDG_RENAME_CUSTOM_THEME", nil, nil, editingThemeName)
            end
        end)
        renameBtn:GetFontString():SetFontObject(GameFontHighlightExtraSmall)
        widget.mgmtRenameBtn = renameBtn

        local deleteBtn = HDG.UI:Button(df, "Delete", btnW, btnH, function()
            if editingThemeName then
                StaticPopup_Show("HDG_DELETE_CUSTOM_THEME", editingThemeName, nil, editingThemeName)
            end
        end)
        deleteBtn:GetFontString():SetFontObject(GameFontHighlightExtraSmall)
        widget.mgmtDeleteBtn = deleteBtn

        local exportBtn = HDG.UI:Button(df, "Export", btnW, btnH, function()
            if editingThemeName then
                local str = HDG_DyeData.ExportCustomTheme(editingThemeName)
                if str then
                    StaticPopup_Show("HDG_EXPORT_CUSTOM_THEME", nil, nil, str)
                end
            end
        end)
        exportBtn:GetFontString():SetFontObject(GameFontHighlightExtraSmall)
        widget.mgmtExportBtn = exportBtn

        local doneBtn = HDG.UI:Button(df, "Done", btnW, btnH, function()
            editingThemeName = nil
            HDG_DyePanel.Refresh()
        end)
        doneBtn:GetFontString():SetFontObject(GameFontHighlightExtraSmall)
        widget.mgmtDoneBtn = doneBtn
    end

    -- Position: bottom of detail frame
    widget.mgmtRenameBtn:ClearAllPoints()
    widget.mgmtRenameBtn:SetPoint("BOTTOMLEFT", df, "BOTTOMLEFT", 6, 4)
    widget.mgmtRenameBtn:Show()

    widget.mgmtDeleteBtn:ClearAllPoints()
    widget.mgmtDeleteBtn:SetPoint("LEFT", widget.mgmtRenameBtn, "RIGHT", 4, 0)
    widget.mgmtDeleteBtn:Show()

    widget.mgmtExportBtn:ClearAllPoints()
    widget.mgmtExportBtn:SetPoint("LEFT", widget.mgmtDeleteBtn, "RIGHT", 4, 0)
    widget.mgmtExportBtn:Show()

    widget.mgmtDoneBtn:ClearAllPoints()
    widget.mgmtDoneBtn:SetPoint("BOTTOMRIGHT", df, "BOTTOMRIGHT", -6, 4)
    widget.mgmtDoneBtn:Show()
end

local function HideThemeManagementButtons()
    if widget and widget.mgmtRenameBtn then widget.mgmtRenameBtn:Hide() end
    if widget and widget.mgmtDeleteBtn then widget.mgmtDeleteBtn:Hide() end
    if widget and widget.mgmtExportBtn then widget.mgmtExportBtn:Hide() end
    if widget and widget.mgmtDoneBtn then widget.mgmtDoneBtn:Hide() end
end

local function UpdateCustomDetail()
    if not widget or not widget.detailLines then return end
    local lines = widget.detailLines

    if editingThemeName then
        local dyes = HDG_DyeData.GetCustomThemeDyes(editingThemeName)
        local FW = CONFIG.FLOATING_WIDGET
        local maxDyes = FW and FW.DYE_MAX_DYES_PER_THEME or 20
        lines[1]:SetText(C_Accent() .. editingThemeName .. "|r " .. C_Dim() .. "(editing)|r")
        lines[2]:SetText(C_Dim() .. "Click dyes to add/remove|r")
        lines[3]:SetText(C_Dim() .. "Dyes:|r " .. C_Text() .. #dyes .. "/" .. maxDyes .. "|r")
        for i = 4, 6 do lines[i]:SetText("") end
        ShowThemeManagementButtons()
    else
        local customs = HDG_DyeData.GetCustomThemes()
        if #customs == 0 then
            lines[1]:SetText(C_Dim() .. "No custom palettes yet|r")
            lines[2]:SetText(C_Dim() .. 'Click "+ New" to create one|r')
            for i = 3, 6 do lines[i]:SetText("") end
        else
            for i = 1, 6 do lines[i]:SetText("") end
        end
        HideThemeManagementButtons()
    end
end

-- ============================================================================
-- REFRESH DYE LIST
-- ============================================================================

function HDG_DyePanel.Refresh()
    if not widget or not scrollContent then return end

    HDG_DyeData.EnsureInitialized()
    ReleaseAllRows()
    UpdateProgress()

    local scheme = GetScheme()
    local yOffset = 0
    local rowHeight = 24
    local colorGroups = HDG_DyeData.GetColorGroups()

    -- Highlight active filter button
    local activeKey = activeCategory or activeColorGroup or activeTheme  -- nil = "All"
    if widget.catButtons then
        for key, btn in pairs(widget.catButtons) do
            local isActive = false
            if filterMode == "custom" and activeTheme then
                isActive = (key == "custom:" .. activeTheme)
            elseif activeKey == nil then
                isActive = (key == "All")
            else
                isActive = (activeKey == key)
            end
            local cs = GetScheme()
            if cs then
                if isActive then
                    btn:SetBackdropBorderColor(cs.accent.r, cs.accent.g, cs.accent.b, 1)
                else
                    btn:SetBackdropBorderColor(cs.border.r, cs.border.g, cs.border.b, cs.border.a)
                end
            end
        end
    end

    local function RenderDyeRow(dyeID)
        local dyeInfo = HDG_DyeData.GetDyeInfo(dyeID)
        if not dyeInfo then return end

        local row = GetPooledRow(scrollContent)
        row:SetPoint("TOPLEFT", 0, -yOffset)
        row:SetPoint("RIGHT", 0, 0)
        row:SetHeight(24)
        row.swatch:Show()
        row.nameText:ClearAllPoints()
        row.nameText:SetPoint("LEFT", 4, 0)
        row.nameText:SetWidth(120)

        -- Swatch color
        if dyeInfo.swatchColorStart then
            local c = dyeInfo.swatchColorStart
            row.swatch:SetVertexColor(c.r, c.g, c.b, 1)
        end

        -- Name (always full brightness so user can browse what to buy)
        local owned = (dyeInfo.numOwned or 0) > 0
        row.nameText:SetText(dyeInfo.name or "Unknown")
        if scheme then
            row.nameText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, scheme.text.a)
        end
        row.swatch:SetAlpha(1)

        -- Count (owned = green count, unowned = dash)
        if owned then
            row.countText:SetText(C_Success() .. "x" .. dyeInfo.numOwned .. "|r")
        else
            row.countText:SetText(C_Dim() .. "—|r")
        end

        -- Selection highlight: normal selection or custom theme membership
        if filterMode == "custom" and editingThemeName and HDG_DyeData.IsDyeInCustomTheme(editingThemeName, dyeID) then
            row.selectedBg:Show()
        elseif selectedDyeID == dyeID then
            row.selectedBg:Show()
        end

        -- Click handler
        local capturedID = dyeID
        row:SetScript("OnClick", function()
            -- Custom edit mode: toggle dye in/out of theme
            if filterMode == "custom" and editingThemeName then
                if HDG_DyeData.IsDyeInCustomTheme(editingThemeName, capturedID) then
                    HDG_DyeData.RemoveDyeFromCustomTheme(editingThemeName, capturedID)
                else
                    HDG_DyeData.AddDyeToCustomTheme(editingThemeName, capturedID)
                end
                UpdatePaletteStrip()
                UpdateCustomDetail()
                -- Toggle selection highlight on this row
                if HDG_DyeData.IsDyeInCustomTheme(editingThemeName, capturedID) then
                    row.selectedBg:Show()
                else
                    row.selectedBg:Hide()
                end
                -- Also rebuild sidebar buttons to update theme RGB tint
                HDG_DyePanel.RebuildFilterButtons()
                return
            end
            -- Normal click: select and show detail
            for _, r in ipairs(dyeRows) do r.selectedBg:Hide() end
            selectedDyeID = capturedID
            row.selectedBg:Show()
            UpdateDetail(capturedID)
            -- Live preview: apply dye to selected decor in housing editor
            if liveMode and liveSlotID and C_HousingCustomizeMode then
                C_HousingCustomizeMode.ApplyDyeToSelectedDecor(liveSlotID, capturedID)
            end
        end)

        yOffset = yOffset + rowHeight
    end

    local function RenderGroupedList(groups, getDyesFn, getHeaderFn)
        for _, key in ipairs(groups) do
            local dyes = getDyesFn(key)
            if dyes and #dyes > 0 then
                local headerRow = GetPooledRow(scrollContent)
                headerRow:SetPoint("TOPLEFT", 0, -yOffset)
                headerRow:SetPoint("RIGHT", 0, 0)
                headerRow:SetHeight(20)
                headerRow.swatch:Hide()
                headerRow.countText:SetText(C_Dim() .. #dyes .. "|r")
                headerRow.nameText:SetText(C_Accent() .. "— " .. getHeaderFn(key) .. " —|r")
                headerRow.nameText:ClearAllPoints()
                headerRow.nameText:SetPoint("LEFT", 4, 0)
                headerRow.nameText:SetWidth(0) -- auto-width for headers
                headerRow:SetScript("OnClick", nil)
                yOffset = yOffset + 20
                for _, dyeID in ipairs(dyes) do RenderDyeRow(dyeID) end
                yOffset = yOffset + 4
            end
        end
    end

    if filterMode == "custom" then
        -- Custom mode rendering
        if editingThemeName then
            -- Edit mode: show theme dyes first, then divider, then all dyes grouped by color
            local themeDyes = HDG_DyeData.GetCustomThemeDyes(editingThemeName)
            if #themeDyes > 0 then
                local headerRow = GetPooledRow(scrollContent)
                headerRow:SetPoint("TOPLEFT", 0, -yOffset)
                headerRow:SetPoint("RIGHT", 0, 0)
                headerRow:SetHeight(20)
                headerRow.swatch:Hide()
                headerRow.countText:SetText(C_Dim() .. #themeDyes .. "|r")
                headerRow.nameText:SetText(C_Accent() .. "— " .. editingThemeName .. " —|r")
                headerRow.nameText:ClearAllPoints()
                headerRow.nameText:SetPoint("LEFT", 4, 0)
                headerRow.nameText:SetWidth(0)
                headerRow:SetScript("OnClick", nil)
                yOffset = yOffset + 20
                for _, dyeID in ipairs(themeDyes) do RenderDyeRow(dyeID) end
                yOffset = yOffset + 4
            end
            -- Divider + all dyes
            local divRow = GetPooledRow(scrollContent)
            divRow:SetPoint("TOPLEFT", 0, -yOffset)
            divRow:SetPoint("RIGHT", 0, 0)
            divRow:SetHeight(20)
            divRow.swatch:Hide()
            divRow.countText:SetText("")
            divRow.nameText:SetText(C_Dim() .. "— All Dyes (click to add) —|r")
            divRow.nameText:ClearAllPoints()
            divRow.nameText:SetPoint("LEFT", 4, 0)
            divRow.nameText:SetWidth(0)
            divRow:SetScript("OnClick", nil)
            yOffset = yOffset + 20
            RenderGroupedList(colorGroups, HDG_DyeData.GetDyesInColorGroup, function(k) return k end)
        elseif activeTheme then
            -- Viewing a custom theme (not editing)
            for _, dyeID in ipairs(HDG_DyeData.GetCustomThemeDyes(activeTheme)) do
                RenderDyeRow(dyeID)
            end
        else
            -- No theme selected: list all custom themes grouped
            local customs = HDG_DyeData.GetCustomThemes()
            if #customs == 0 then
                local emptyRow = GetPooledRow(scrollContent)
                emptyRow:SetPoint("TOPLEFT", 0, -yOffset)
                emptyRow:SetPoint("RIGHT", 0, 0)
                emptyRow:SetHeight(24)
                emptyRow.swatch:Hide()
                emptyRow.countText:SetText("")
                emptyRow.nameText:SetText(C_Dim() .. "No custom palettes yet|r")
                emptyRow.nameText:ClearAllPoints()
                emptyRow.nameText:SetPoint("LEFT", 4, 0)
                emptyRow.nameText:SetWidth(0)
                emptyRow:SetScript("OnClick", nil)
                yOffset = yOffset + 24
            else
                local customNames = {}
                for _, t in ipairs(customs) do table.insert(customNames, t.name) end
                RenderGroupedList(customNames, HDG_DyeData.GetCustomThemeDyes, function(k) return k end)
            end
        end
    elseif activeCategory then
        -- Blizzard category filter
        for _, dyeID in ipairs(HDG_DyeData.GetDyesInCategory(activeCategory)) do
            RenderDyeRow(dyeID)
        end
    elseif activeTheme then
        -- Theme filter: show dyes in this themed palette (built-in themes)
        for _, dyeID in ipairs(HDG_DyeData.GetDyesInTheme(activeTheme)) do
            RenderDyeRow(dyeID)
        end
    elseif activeColorGroup then
        -- Color filter: show just this color group
        for _, dyeID in ipairs(HDG_DyeData.GetDyesInColorGroup(activeColorGroup)) do
            RenderDyeRow(dyeID)
        end
    elseif filterMode == "category" then
        -- All categories: grouped by Blizzard category
        RenderGroupedList(HDG_DyeData.GetCategories(), HDG_DyeData.GetDyesInCategory, function(k)
            return HDG_DyeData.GetCategoryName(k) or ("Category " .. k)
        end)
    elseif filterMode == "theme" then
        -- All themes: grouped by theme
        local themes = HDG_DyeData.GetThemes()
        local themeNames = {}
        for _, t in ipairs(themes) do table.insert(themeNames, t.name) end
        RenderGroupedList(themeNames, HDG_DyeData.GetDyesInTheme, function(k) return k end)
    else
        -- All colors: grouped by color
        RenderGroupedList(colorGroups, HDG_DyeData.GetDyesInColorGroup, function(k) return k end)
    end

    scrollContent:SetHeight(math.max(1, yOffset))

    -- Custom mode: show theme detail + palette strip, hide normal detail
    if filterMode == "custom" then
        HideThemeManagementButtons()
        UpdateCustomDetail()
        UpdatePaletteStrip()
    else
        HideThemeManagementButtons()
        UpdatePaletteStrip()
        UpdateDetail(selectedDyeID)
    end
end

-- ============================================================================
-- PUBLIC API
-- ============================================================================

function HDG_DyePanel:Init(configRef)
    CONFIG = configRef
end

function HDG_DyePanel.Toggle()
    if not CONFIG then
        CONFIG = privateDB.CONFIG
    end
    if not created then CreatePanel() end

    if widget.frame:IsShown() then
        widget.frame:Hide()
    else
        widget.frame:Show()
        HDG_DyePanel.Refresh()
    end
end

function HDG_DyePanel.IsShown()
    return widget and widget.frame and widget.frame:IsShown()
end

function HDG_DyePanel.FilterToColorGroup(groupName)
    if not CONFIG then CONFIG = privateDB.CONFIG end
    if not created then CreatePanel() end
    activeColorGroup = groupName
    widget.frame:Show()
    HDG_DyePanel.Refresh()
end

function HDG_DyePanel.Show()
    if not CONFIG then CONFIG = privateDB.CONFIG end
    if not created then CreatePanel() end
    widget.frame:Show()
    HDG_DyePanel.Refresh()
end

function HDG_DyePanel.Hide()
    if widget and widget.frame then
        widget.frame:Hide()
    end
end

function HDG_DyePanel.IsLiveMode()
    return liveMode
end

-- ============================================================================
-- STATIC POPUP DIALOGS
-- ============================================================================

StaticPopupDialogs["HDG_DYE_STUDIO_HELP"] = {
    text = "|cFF14b8a6Vamoose's Dye Studio|r\n\n"
        .. "|cFFe2e8f0Browse Modes|r\n"
        .. "|cFFDC143CCategory|r \226\128\148 Blizzard's General / Wood / Metal groupings\n"
        .. "|cFFFFD700Color|r \226\128\148 Dyes sorted by hue (Red, Blue, Teal, etc.)\n"
        .. "|cFF9932CCTheme|r \226\128\148 23 curated palettes (Alliance, Horde, class themes, etc.)\n\n"
        .. "|cFFe2e8f0Custom Palettes|r\n"
        .. "|cFF32CD32Create|r \226\128\148 Switch to Custom mode, click |cFF14b8a6+ New|r\n"
        .. "|cFFFF8C00Add dyes|r \226\128\148 Click any dye row to toggle it in/out of your palette\n"
        .. "|cFF00CED1Rename|r \226\128\148 Click |cFF14b8a6Rename|r in the detail section below\n"
        .. "|cFF4169E1Share|r \226\128\148 Click |cFF14b8a6Export|r to copy, |cFF14b8a6Import|r to paste\n"
        .. "|cFFFF69B4Done|r \226\128\148 Click |cFF14b8a6Done|r to finish editing and view palette\n\n"
        .. "|cFFe2e8f0Live Dyeing|r\n"
        .. "Check |cFFe2e8f0Replace Blizzard UI|r to auto-open Dye Studio when editing decor. Click dyes for instant preview.\n\n"
        .. "|cFF666666Custom palettes persist across sessions and characters.|r",
    button1 = "Close",
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    wide = true,
    preferredIndex = 3,
    OnShow = function(self)
        self.Text:SetJustifyH("LEFT")
        self.Text:SetTextColor(0.8, 0.8, 0.8, 1)
    end,
}

-- ============================================================================
-- Custom Theme Management Dialogs
-- ============================================================================

StaticPopupDialogs["HDG_RENAME_CUSTOM_THEME"] = {
    text = "Rename palette:",
    button1 = "Rename",
    button2 = "Cancel",
    hasEditBox = true,
    editBoxWidth = 200,
    OnShow = function(self)
        self.EditBox:SetText(self.data or "")
        self.EditBox:HighlightText()
    end,
    OnAccept = function(self)
        local newName = strtrim(self.EditBox:GetText())
        if newName == "" then return end
        local oldName = self.data
        if HDG_DyeData.RenameCustomTheme(oldName, newName) then
            editingThemeName = newName
            activeTheme = newName
            HDG_DyePanel.RebuildFilterButtons()
            HDG_DyePanel.Refresh()
        end
    end,
    EditBoxOnEnterPressed = function(self)
        local parent = self:GetParent()
        local newName = strtrim(self:GetText())
        if newName == "" then return end
        local oldName = parent.data
        if HDG_DyeData.RenameCustomTheme(oldName, newName) then
            editingThemeName = newName
            activeTheme = newName
            HDG_DyePanel.RebuildFilterButtons()
            HDG_DyePanel.Refresh()
        end
        parent:Hide()
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    preferredIndex = 3,
}

StaticPopupDialogs["HDG_DELETE_CUSTOM_THEME"] = {
    text = "Delete palette \"%s\"?",
    button1 = "Delete",
    button2 = "Cancel",
    OnAccept = function(self)
        local name = self.data
        HDG_DyeData.DeleteCustomTheme(name)
        if editingThemeName == name then editingThemeName = nil end
        if activeTheme == name then activeTheme = nil end
        HDG_DyePanel.RebuildFilterButtons()
        HDG_DyePanel.Refresh()
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    preferredIndex = 3,
}

StaticPopupDialogs["HDG_EXPORT_CUSTOM_THEME"] = {
    text = "Copy this string to share your palette:",
    button1 = "Close",
    hasEditBox = true,
    editBoxWidth = 280,
    OnShow = function(self)
        self.EditBox:SetText(self.data or "")
        self.EditBox:HighlightText()
        self.EditBox:SetFocus()
    end,
    EditBoxOnEscapePressed = function(self) self:GetParent():Hide() end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    preferredIndex = 3,
}

StaticPopupDialogs["HDG_IMPORT_CUSTOM_THEME"] = {
    text = "Paste a Dye Studio theme string:",
    button1 = "Import",
    button2 = "Cancel",
    hasEditBox = true,
    editBoxWidth = 280,
    OnAccept = function(self)
        local str = strtrim(self.EditBox:GetText())
        local name, err = HDG_DyeData.ImportCustomTheme(str)
        if name then
            filterMode = "custom"
            activeTheme = name
            editingThemeName = name
            HDG_DyePanel.RebuildFilterButtons()
            HDG_DyePanel.Refresh()
            if HDG_UIHelpers then
                HDG_UIHelpers.PrintMessage("Imported palette: " .. name, "success")
            end
        else
            if HDG_UIHelpers then
                HDG_UIHelpers.PrintMessage("Import failed: " .. (err or "unknown error"), "error")
            end
        end
    end,
    EditBoxOnEnterPressed = function(self)
        local parent = self:GetParent()
        local str = strtrim(self:GetText())
        local name, err = HDG_DyeData.ImportCustomTheme(str)
        if name then
            filterMode = "custom"
            activeTheme = name
            editingThemeName = name
            HDG_DyePanel.RebuildFilterButtons()
            HDG_DyePanel.Refresh()
            if HDG_UIHelpers then
                HDG_UIHelpers.PrintMessage("Imported palette: " .. name, "success")
            end
        else
            if HDG_UIHelpers then
                HDG_UIHelpers.PrintMessage("Import failed: " .. (err or "unknown error"), "error")
            end
        end
        parent:Hide()
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    preferredIndex = 3,
}

-- ============================================================================
-- HOUSING EDITOR INTEGRATION (Full Replacement)
-- Replaces Blizzard's dye customization UI with Vamoose's Dye Studio.
-- Click a dyeable decor → our panel opens, Blizzard's pane hides.
-- Click dye rows → instant live 3D preview on the placed item.
-- ============================================================================

local function ShowDyeStudioPanel()
    if not CONFIG then CONFIG = privateDB.CONFIG end
    if not created then CreatePanel() end
    widget.frame:SetParent(HouseEditorFrame)
    widget.frame:SetFrameStrata("DIALOG")
    widget.frame:Show()
    HDG_DyePanel.Refresh()
end

local function SuppressBlizzardPane()
    if not HouseEditorFrame then return end
    local pane = HouseEditorFrame.CustomizeModeFrame
                 and HouseEditorFrame.CustomizeModeFrame.DecorCustomizationsPane
    if pane then pane:SetAlpha(0) end
    if DyeSelectionPopout then DyeSelectionPopout:SetAlpha(0) end
end

local function RestoreBlizzardPane()
    if not HouseEditorFrame then return end
    local pane = HouseEditorFrame.CustomizeModeFrame
                 and HouseEditorFrame.CustomizeModeFrame.DecorCustomizationsPane
    if pane then pane:SetAlpha(1) end
    if DyeSelectionPopout then DyeSelectionPopout:SetAlpha(1) end
end

-- ============================================================================
-- SLOT BAR (dye channel selector buttons)
-- ============================================================================

local function UpdateSlotBar()
    if not widget or not widget.slotBar or not liveDyeSlots then return end
    for _, btn in ipairs(widget.slotBtns) do btn:Hide() end

    local swatchSize = 30
    local btnW = swatchSize + 44
    local gap = 2
    widget.slotBar:SetHeight(swatchSize + 4)
    widget.slotBar:SetWidth(#liveDyeSlots * (btnW + gap) - gap)
    widget.slotBar:Show()

    for i, slot in ipairs(liveDyeSlots) do
        local btn = widget.slotBtns[i]
        if not btn then
            btn = CreateFrame("Button", nil, widget.slotBar)
            btn:SetSize(btnW, swatchSize)
            -- Swatch using Blizzard's housing dye atlases
            local swBase = btn:CreateTexture(nil, "ARTWORK", nil, 1)
            swBase:SetSize(swatchSize, swatchSize)
            swBase:SetPoint("LEFT", 2, 0)
            swBase:SetAtlas("Housing_Customize_Dye_Swatch_Base")
            btn.swBase = swBase
            local swGrad = btn:CreateTexture(nil, "ARTWORK", nil, 2)
            swGrad:SetAllPoints(swBase)
            swGrad:SetAtlas("Housing_Customize_Dye_Swatch_Gradient")
            btn.swGrad = swGrad
            local swEmpty = btn:CreateTexture(nil, "ARTWORK", nil, 0)
            swEmpty:SetAllPoints(swBase)
            swEmpty:SetAtlas("housing_customize_dye_swatch_empty")
            swEmpty:Hide()
            btn.swEmpty = swEmpty
            local border = btn:CreateTexture(nil, "OVERLAY", nil, 1)
            border:SetAllPoints(swBase)
            border:SetAtlas("Housing_Customize_Dye_Frame_Default")
            btn.border = border
            local selBorder = btn:CreateTexture(nil, "OVERLAY", nil, 2)
            selBorder:SetAllPoints(swBase)
            selBorder:SetAtlas("Housing_Customize_Dye_Frame_Selected")
            selBorder:Hide()
            btn.selBorder = selBorder
            local hl = btn:CreateTexture(nil, "HIGHLIGHT")
            hl:SetAllPoints(swBase)
            hl:SetAtlas("Housing_Customize_Dye_Swatch_Highlight")
            hl:SetAlpha(0.15)
            local label = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightExtraSmall")
            label:SetPoint("LEFT", swBase, "RIGHT", 3, 0)
            btn.label = label
            widget.slotBtns[i] = btn
        end

        btn:ClearAllPoints()
        btn:SetPoint("LEFT", widget.slotBar, "LEFT", (i - 1) * (btnW + gap), 0)

        -- Show current/preview dye color using Blizzard's dual-color atlas system
        local dyeInfo = slot.dyeColorID and C_DyeColor and C_DyeColor.GetDyeColorInfo(slot.dyeColorID)
        if dyeInfo and dyeInfo.swatchColorStart and dyeInfo.swatchColorEnd then
            local cs, ce = dyeInfo.swatchColorStart, dyeInfo.swatchColorEnd
            btn.swBase:SetVertexColor(cs.r, cs.g, cs.b, 1)
            btn.swGrad:SetVertexColor(ce.r, ce.g, ce.b, 1)
            btn.swBase:Show()
            btn.swGrad:Show()
            btn.swEmpty:Hide()
        else
            btn.swBase:Hide()
            btn.swGrad:Hide()
            btn.swEmpty:Show()
        end

        btn.label:SetText("Slot " .. i)
        if slot.ID == liveSlotID then
            btn.selBorder:Show()
            btn.label:SetTextColor(0.08, 0.72, 0.65, 1)
        else
            btn.selBorder:Hide()
            btn.label:SetTextColor(0.7, 0.7, 0.7, 1)
        end

        local capturedSlotID = slot.ID
        btn:SetScript("OnClick", function()
            liveSlotID = capturedSlotID
            UpdateSlotBar()
        end)
        btn:Show()
    end
end

-- ============================================================================
-- COST DISPLAY + ACTION BAR
-- ============================================================================

local function UpdateCostDisplay()
    if not widget or not widget.costText or not C_HousingCustomizeMode then return end
    if not liveMode then
        widget.costText:SetText("")
        return
    end
    local numSpend = C_HousingCustomizeMode.GetNumDyesToSpendOnSelectedDecor()
    local numRemove = C_HousingCustomizeMode.GetNumDyesToRemoveOnSelectedDecor()
    local parts = {}
    if numSpend > 0 then table.insert(parts, C_Accent() .. numSpend .. " dye(s)|r") end
    if numRemove > 0 then table.insert(parts, C_Dim() .. numRemove .. " remove|r") end
    widget.costText:SetText(#parts > 0 and table.concat(parts, "  ") or C_Dim() .. "Click dyes to preview|r")

    -- Enable/disable apply
    local hasChanges = numSpend > 0 or numRemove > 0
    if widget.applyBtn then
        widget.applyBtn:SetAlpha(hasChanges and 1 or 0.4)
    end
end

local function WireActionBar()
    if not widget then return end
    widget.cancelBtn:SetScript("OnClick", function()
        if C_HousingCustomizeMode then
            C_HousingCustomizeMode.ClearDyesForSelectedDecor()
            C_HousingCustomizeMode.CancelActiveEditing()
        end
    end)
    widget.applyBtn:SetScript("OnClick", function()
        if C_HousingCustomizeMode then
            local anyChanges = C_HousingCustomizeMode.CommitDyesForSelectedDecor()
            if anyChanges and PlaySound then
                PlaySound(SOUNDKIT.HOUSING_CUSTOMIZE_DYE_APPLY_CHANGED)
            end
            C_HousingCustomizeMode.CancelActiveEditing()
        end
    end)
end

-- ============================================================================
-- LIVE BAR + LAYOUT MANAGEMENT
-- ============================================================================

local LIVE_OFFSET = 52 -- LIVE text + slot buttons height inside detail frame

local function UpdateLiveBar()
    if not widget or not widget.detailFrame then return end
    if liveMode then
        -- Hide standalone top liveBar (info lives in detail frame instead)
        widget.liveBar:SetHeight(0.1)
        widget.liveBar:Hide()
        widget.liveText:Hide()

        -- Show LIVE text inside detail frame
        widget.liveStatus:SetText(C_Accent() .. "LIVE|r" .. C_Dim() .. " - |r" .. C_Text() .. (liveDecorName or "Decor") .. "|r")
        widget.liveStatus:Show()

        -- Action bar at very bottom
        WireActionBar()
        widget.actionBar:Show()
        UpdateCostDisplay()

        -- Detail frame: same 108px height, moved up above action bar
        widget.detailFrame:ClearAllPoints()
        widget.detailFrame:SetPoint("BOTTOMLEFT", widget.actionBar, "TOPLEFT", 0, 2)
        widget.detailFrame:SetPoint("BOTTOMRIGHT", widget.actionBar, "TOPRIGHT", 0, 2)
        widget.detailFrame:SetHeight(108)

        -- Detail lines shift down for LIVE header + slots; lines 5-6 hidden (clipped)
        for i = 1, 6 do
            widget.detailLines[i]:ClearAllPoints()
            widget.detailLines[i]:SetPoint("TOPLEFT", widget.detailFrame, "TOPLEFT", 8, -4 - (i - 1) * 15 - LIVE_OFFSET)
            widget.detailLines[i]:SetPoint("RIGHT", -8, 0)
            widget.detailLines[i]:SetShown(i <= 4)
        end

        -- Slot swatches (after layout is settled so anchoring to liveStatus works)
        UpdateSlotBar()

        -- Push sidebar + scroll bottom up 30px to make room for action bar
        widget.sidebar:SetPoint("BOTTOMRIGHT", -8, 150)
        widget.scrollFrame:SetPoint("TOPLEFT", 8, -58)

        -- Remove outer border as visual indicator of live mode
        if widget.frame.SetBackdropBorderColor then
            widget.frame:SetBackdropBorderColor(0, 0, 0, 0)
        end
    else
        -- Hide live elements
        widget.liveBar:SetHeight(0.1)
        widget.liveBar:Hide()
        widget.liveText:Hide()
        widget.liveStatus:Hide()
        widget.slotBar:Hide()
        for _, btn in ipairs(widget.slotBtns) do btn:Hide() end
        widget.actionBar:Hide()

        -- Restore detail frame to normal position
        widget.detailFrame:ClearAllPoints()
        widget.detailFrame:SetPoint("BOTTOMLEFT", 8, 8)
        widget.detailFrame:SetPoint("BOTTOMRIGHT", -8, 8)
        widget.detailFrame:SetHeight(108)
        for i = 1, 6 do
            widget.detailLines[i]:ClearAllPoints()
            widget.detailLines[i]:SetPoint("TOPLEFT", widget.detailFrame, "TOPLEFT", 8, -4 - (i - 1) * 15)
            widget.detailLines[i]:SetPoint("RIGHT", -8, 0)
            widget.detailLines[i]:Show()
        end

        -- Restore sidebar + scroll bottom
        widget.sidebar:SetPoint("BOTTOMRIGHT", -8, 120)
        widget.scrollFrame:SetPoint("TOPLEFT", 8, -58)

        -- Restore outer border
        local scheme = GetScheme()
        if scheme and widget.frame.SetBackdropBorderColor then
            widget.frame:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a)
        end
        RestoreBlizzardPane()
    end
end

-- ============================================================================
-- ENTER / EXIT LIVE MODE
-- ============================================================================

local function EnterLiveMode(decorInfo)
    local replaceEnabled = _G.HDG_DB and _G.HDG_DB.dyeReplaceBlizzard
    liveMode = true
    liveDecorName = decorInfo.name
    liveDyeSlots = decorInfo.dyeSlots
    if decorInfo.dyeSlots and decorInfo.dyeSlots[1] then
        liveSlotID = decorInfo.dyeSlots[1].ID
    end
    if replaceEnabled then
        -- Auto-show our panel and suppress Blizzard's
        ShowDyeStudioPanel()
        C_Timer.After(0, SuppressBlizzardPane)
    end
    -- Update live bar if panel is already open (manual /hdg dyes usage)
    if widget and widget.frame and widget.frame:IsShown() then
        UpdateLiveBar()
    end
end

local function ExitLiveMode()
    liveMode = false
    liveSlotID = nil
    liveDecorName = nil
    liveDyeSlots = nil
    UpdateLiveBar()
end

-- ============================================================================
-- INSTALL HOOKS
-- ============================================================================

local function InstallHousingHooks()
    if hooksInstalled then return end
    hooksInstalled = true

    -- Hook DyeSelectionPopout to track which slot the user clicked
    if DyeSelectionPopout and DyeSelectionPopout.SetDyeSlotInfo then
        hooksecurefunc(DyeSelectionPopout, "SetDyeSlotInfo", function(_, dyeSlotInfo)
            if dyeSlotInfo and dyeSlotInfo.ID then
                liveSlotID = dyeSlotInfo.ID
                UpdateSlotBar()
            end
        end)
    end

    -- Suppress Blizzard's pane whenever it tries to show during our live mode
    local pane = HouseEditorFrame and HouseEditorFrame.CustomizeModeFrame
                 and HouseEditorFrame.CustomizeModeFrame.DecorCustomizationsPane
    if pane then
        hooksecurefunc(pane, "Show", function()
            local replaceEnabled = _G.HDG_DB and _G.HDG_DB.dyeReplaceBlizzard
            if liveMode and replaceEnabled then C_Timer.After(0, SuppressBlizzardPane) end
        end)
    end


    -- Restore parent when housing editor closes
    if HouseEditorFrame then
        HouseEditorFrame:HookScript("OnHide", function()
            if widget and widget.frame then
                widget.frame:SetParent(UIParent)
                widget.frame:SetFrameStrata("DIALOG")
            end
            RestoreBlizzardPane()
        end)
    end

    -- Listen for dye changes to update cost display
    local custFrame = HouseEditorFrame and HouseEditorFrame.CustomizeModeFrame
    if custFrame then
        hooksecurefunc(custFrame, "UpdateSelectedDecorInfo", function()
            if liveMode then
                local info = C_HousingCustomizeMode.GetSelectedDecorInfo()
                if info and info.dyeSlots then
                    liveDyeSlots = info.dyeSlots
                    UpdateSlotBar()
                    UpdateCostDisplay()
                end
            end
        end)
    end
end

-- ============================================================================
-- EVENT HANDLING
-- ============================================================================

local housingEventFrame = CreateFrame("Frame")
housingEventFrame:RegisterEvent("ADDON_LOADED")
housingEventFrame:SetScript("OnEvent", function(self, event, ...)
    if event == "ADDON_LOADED" then
        local addon = ...
        if addon == "Blizzard_HouseEditor" then
            C_Timer.After(0, InstallHousingHooks)
            self:UnregisterEvent("ADDON_LOADED")
            self:RegisterEvent("HOUSING_CUSTOMIZE_MODE_SELECTED_TARGET_CHANGED")
            self:RegisterEvent("HOUSING_DECOR_CUSTOMIZATION_CHANGED")
        end
    elseif event == "HOUSING_CUSTOMIZE_MODE_SELECTED_TARGET_CHANGED" then
        local hasTarget, targetType = ...
        if hasTarget and Enum.HousingCustomizeModeTargetType
           and targetType == Enum.HousingCustomizeModeTargetType.Decor then
            local info = C_HousingCustomizeMode.GetSelectedDecorInfo()
            if info and info.canBeCustomized and info.dyeSlots and #info.dyeSlots > 0 then
                EnterLiveMode(info)
            else
                ExitLiveMode()
            end
        else
            ExitLiveMode()
        end
    elseif event == "HOUSING_DECOR_CUSTOMIZATION_CHANGED" then
        if liveMode then
            local info = C_HousingCustomizeMode.GetSelectedDecorInfo()
            if info and info.dyeSlots then
                liveDyeSlots = info.dyeSlots
                UpdateSlotBar()
                UpdateCostDisplay()
            end
        end
    end
end)

-- Handle /reload while already in housing editor
if C_AddOns and C_AddOns.IsAddOnLoaded("Blizzard_HouseEditor") then
    C_Timer.After(0, function()
        InstallHousingHooks()
        housingEventFrame:RegisterEvent("HOUSING_CUSTOMIZE_MODE_SELECTED_TARGET_CHANGED")
        housingEventFrame:RegisterEvent("HOUSING_DECOR_CUSTOMIZATION_CHANGED")
        if C_HousingCustomizeMode and C_HousingCustomizeMode.IsDecorSelected() then
            local info = C_HousingCustomizeMode.GetSelectedDecorInfo()
            if info and info.canBeCustomized and info.dyeSlots and #info.dyeSlots > 0 then
                EnterLiveMode(info)
            end
        end
    end)
end
