
-- ============================================================
-- FILE: CharacterPanel.lua
-- PURPOSE: Custom character stats panel. Replaces Blizzard's
--   CharacterFrame with a styled panel showing equipped gear
--   (item icons, item level, quality colors), character stats
--   (primary/secondary stats, ratings), average item level,
--   specialization, and title. Tabbed interface with gear
--   and stats views.
-- HOW IT WORKS:
--   1. CFG: frame dimensions, stat slot layout, tab config.
--   2. Panel table + Panel._refs: holds all sub-frame refs.
--   3. Panel.EnsurePanel() — lazy-creates the main Frame
--      (MidnightUI_CharacterPanel) with:
--        - header strip + title/name text
--        - character model viewer (SetUnit)
--        - gear grid: 19 equipment slots as icon buttons
--          (SetTexture from GetInventoryItemTexture, tooltip
--          on hover, link on click)
--        - stat rows: Crit/Haste/Mastery/Versatility/Dodge
--          etc. via GetCombatRatingBonus / GetHaste etc.
--        - average ilvl display (GetAverageItemLevel)
--   4. Panel.Refresh() — re-reads all inventory + stat APIs
--      and updates displayed values.
--   5. evf event handler: PLAYER_EQUIPMENT_CHANGED /
--      PLAYER_ENTERING_WORLD → Panel.Refresh();
--      PLAYER_TALENT_UPDATE → refresh spec display.
-- DEPENDS ON:
--   GetInventoryItemLink/Texture, GetAverageItemLevel
--   GetCritChance, GetHaste, GetMasteryEffect, GetVersatilityBonus
--   GetCombatRating, UnitStat, UnitHealthMax, UnitArmor
-- EXPORTS:
--   _G.MidnightUI_CharacterPanel_Toggle
--   _G.MidnightUI_CharacterPanel_Show
--   _G.MidnightUI_CharacterPanel_Hide
-- OWNED SAVEDVARIABLES: none
-- EVENTS REGISTERED:
--   PLAYER_ENTERING_WORLD, PLAYER_EQUIPMENT_CHANGED
--   PLAYER_TALENT_UPDATE, UNIT_INVENTORY_CHANGED
-- FRAMES CREATED:
--   MidnightUI_CharacterPanel (main panel)
--   Per-slot: equipment slot button frames (19 slots)
-- GOTCHAS:
--   GetAverageItemLevel returns two values — use select(1,...)
--   for the equipped value (not the bag-equipped value).
--   Character model: SetUnit("player") must be called after
--   the model frame is shown; may need a C_Timer.After(0) defer.
--   hooksecurefunc used to intercept CharacterFrame show/hide
--   to suppress Blizzard's default character panel.
-- ============================================================

local ADDON_NAME = "MidnightUI"
local W8 = "Interface\\Buttons\\WHITE8X8"
local TITLE_FONT = (_G.MidnightUI_ResolveFontPath and _G.MidnightUI_ResolveFontPath("Fonts\\FRIZQT__.TTF")) or "Fonts\\FRIZQT__.TTF"
local BODY_FONT  = (_G.MidnightUI_ResolveFontPath and _G.MidnightUI_ResolveFontPath("Fonts\\FRIZQT__.TTF")) or "Fonts\\FRIZQT__.TTF"

local pcall, type, pairs, ipairs, math, string, table, select, tostring, tonumber =
      pcall, type, pairs, ipairs, math, string, table, select, tostring, tonumber
local CreateFrame, UIParent, GameTooltip = CreateFrame, UIParent, GameTooltip
local UnitName, UnitClass, UnitLevel, UnitRace, UnitStat =
      UnitName, UnitClass, UnitLevel, UnitRace, UnitStat
local GetSpecialization, GetSpecializationInfo = GetSpecialization, GetSpecializationInfo
local GetCurrentTitle, GetTitleName = GetCurrentTitle, GetTitleName
local GetInventoryItemTexture, GetInventoryItemLink = GetInventoryItemTexture, GetInventoryItemLink
local GetInventorySlotInfo, GetAverageItemLevel = GetInventorySlotInfo, GetAverageItemLevel
local GetCritChance, GetHaste, GetMasteryEffect = GetCritChance, GetHaste, GetMasteryEffect
local GetCombatRating, GetCombatRatingBonus, GetVersatilityBonus = GetCombatRating, GetCombatRatingBonus, GetVersatilityBonus
local GetSpellBonusDamage, GetSpellCritChance, GetDodgeChance = GetSpellBonusDamage, GetSpellCritChance, GetDodgeChance
local UnitHealthMax, UnitAttackPower, UnitArmor = UnitHealthMax, UnitAttackPower, UnitArmor
local GetItemInfo, GetItemQualityColor = GetItemInfo, GetItemQualityColor
local hooksecurefunc = hooksecurefunc

local function SafeCall(fn, ...)
    if type(fn) ~= "function" then return nil end
    local ok, r1, r2, r3, r4, r5, r6 = pcall(fn, ...)
    if not ok then return nil end
    return r1, r2, r3, r4, r5, r6
end

local function TrySetFont(fs, fontPath, size, flags)
    if not fs or not fs.SetFont then return end
    local resolved = (_G.MidnightUI_ResolveFontPath and _G.MidnightUI_ResolveFontPath(fontPath or TITLE_FONT)) or (fontPath or TITLE_FONT)
    pcall(fs.SetFont, fs, resolved, size or 12, flags or "")
end

local function FormatNumber(n)
    if not n or type(n) ~= "number" then return "0" end
    n = math.floor(n + 0.5)
    local formatted = tostring(n)
    while true do
        local k
        formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", "%1,%2")
        if k == 0 then break end
    end
    return formatted
end

local THEMES = {
    parchment = {
        key         = "parchment",
        frameBg     = { 0.04, 0.035, 0.025, 0.97 },
        modelBg     = { 0.03, 0.03, 0.04, 1.0 },
        headerBg    = { 0.05, 0.04, 0.025, 0.95 },
        glassBg     = { 0.04, 0.04, 0.06, 0.70 },
        glassBorder = { 1, 1, 1, 0.08 },
        accent      = { 0.72, 0.62, 0.42 },
        titleText   = { 0.96, 0.87, 0.58 },
        bodyText    = { 0.94, 0.90, 0.80 },
        mutedText   = { 0.71, 0.62, 0.44 },
        divider     = { 0.60, 0.52, 0.35 },
        tabActive   = { 0.96, 0.87, 0.58 },
        tabInactive = { 0.52, 0.46, 0.34 },
    },
    midnight = {
        key         = "midnight",
        frameBg     = { 0.03, 0.04, 0.07, 0.97 },
        modelBg     = { 0.02, 0.025, 0.04, 1.0 },
        headerBg    = { 0.025, 0.03, 0.06, 0.95 },
        glassBg     = { 0.03, 0.04, 0.08, 0.70 },
        glassBorder = { 1, 1, 1, 0.06 },
        accent      = { 0.00, 0.78, 1.00 },
        titleText   = { 0.92, 0.93, 0.96 },
        bodyText    = { 0.82, 0.84, 0.88 },
        mutedText   = { 0.58, 0.60, 0.65 },
        divider     = { 0.35, 0.40, 0.50 },
        tabActive   = { 0.92, 0.93, 0.96 },
        tabInactive = { 0.45, 0.48, 0.55 },
    },
    class = {
        key         = "class",
        frameBg     = { 0.04, 0.035, 0.025, 0.97 },
        modelBg     = { 0.03, 0.03, 0.04, 1.0 },
        headerBg    = { 0.05, 0.04, 0.025, 0.95 },
        glassBg     = { 0.04, 0.04, 0.06, 0.70 },
        glassBorder = { 1, 1, 1, 0.08 },
        accent      = { 0.72, 0.62, 0.42 },
        titleText   = { 0.94, 0.92, 0.88 },
        bodyText    = { 0.92, 0.90, 0.85 },
        mutedText   = { 0.65, 0.62, 0.55 },
        divider     = { 0.50, 0.48, 0.40 },
        tabActive   = { 0.94, 0.92, 0.88 },
        tabInactive = { 0.50, 0.48, 0.42 },
    },
}

local activeTheme = THEMES.parchment

local function GetActiveTheme()
    local s = _G.MidnightUISettings
    local key = (s and s.General and s.General.characterPanelTheme) or "parchment"
    local t = THEMES[key] or THEMES.parchment
    if key == "class" then
        local cc = _G.MidnightUI_Core and _G.MidnightUI_Core.GetClassColorTable
            and _G.MidnightUI_Core.GetClassColorTable("player")
        if cc then
            t.accent = { cc.r or cc[1] or 0.72, cc.g or cc[2] or 0.62, cc.b or cc[3] or 0.42 }
        end
    end
    activeTheme = t
    return t
end

local function TC(key)
    local c = activeTheme[key]
    if not c then return 1, 1, 1, 1 end
    return c[1], c[2], c[3], c[4] or 1
end

local CFG = {
    WIDTH           = 1060,
    HEIGHT          = 780,
    HEADER_H        = 44,
    TAB_BAR_H       = 32,

    LEFT_GLASS_W    = 220,
    RIGHT_GLASS_W   = 200,
    GLASS_PAD       = 10,
    GLASS_MARGIN    = 8,

    SLOT_SIZE       = 34,
    SLOT_GAP        = 4,

    IDENTITY_H      = 70,

    STRATA          = "HIGH",

    modelLight = { true, false, -0.4, 0.8, -0.5, 0.95, 0.82, 0.72, 0.58, 0.7, 1.0, 0.94, 0.84 },

    REP_ROW_H       = 38,
    CURRENCY_ROW_H  = 32,
    HEADER_ROW_H    = 28,
}

local function CreateDropShadow(frame, intensity)
    intensity = intensity or 6
    local shadows = {}
    for i = 1, intensity do
        local s = CreateFrame("Frame", nil, frame)
        s:SetFrameLevel(math.max(0, frame:GetFrameLevel() - 1))
        local offset = i * 0.8
        local alpha = (0.18 - (i * 0.025)) * (intensity / 6)
        s:SetPoint("TOPLEFT", frame, "TOPLEFT", -offset, offset)
        s:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", offset, -offset)
        local t = s:CreateTexture(nil, "BACKGROUND")
        t:SetAllPoints()
        t:SetColorTexture(0, 0, 0, alpha)
        shadows[#shadows + 1] = s
    end
    return shadows
end

local function CreateGlassPanel(parent, width)
    local glass = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    glass:SetWidth(width)
    glass:SetBackdrop({
        bgFile = W8,
        edgeFile = W8,
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    glass:SetBackdropColor(TC("glassBg"))
    glass:SetBackdropBorderColor(TC("glassBorder"))

    local frost = glass:CreateTexture(nil, "OVERLAY", nil, 3)
    frost:SetHeight(1)
    frost:SetPoint("TOPLEFT", glass, "TOPLEFT", 1, -1)
    frost:SetPoint("TOPRIGHT", glass, "TOPRIGHT", -1, -1)
    frost:SetColorTexture(1, 1, 1, 0.10)
    glass._frost = frost

    local grad = glass:CreateTexture(nil, "BACKGROUND", nil, 1)
    grad:SetPoint("TOPLEFT", 1, -1)
    grad:SetPoint("BOTTOMRIGHT", -1, 1)
    grad:SetTexture(W8)
    if grad.SetGradient and CreateColor then
        grad:SetGradient("VERTICAL",
            CreateColor(0, 0, 0, 0.15),
            CreateColor(0, 0, 0, 0))
    end
    glass._grad = grad

    function glass:ApplyTheme()
        self:SetBackdropColor(TC("glassBg"))
        self:SetBackdropBorderColor(TC("glassBorder"))
    end

    return glass
end

local function GetQualityColorRGB(quality)
    if not quality or quality < 0 then return 0.65, 0.65, 0.65 end
    local r, g, b = GetItemQualityColor(quality)
    if r then return r, g, b end
    return 0.65, 0.65, 0.65
end

local Panel = {}
Panel._state = {
    initialized  = false,
    panelOpen    = false,
    activeTab    = "character",
    modelFacing  = 0.4,
    modelZoom    = 1.0,
    isDragging   = false,
    dragStartX   = 0,
    dragStartFacing = 0,
}
Panel._refs = {}

local CLASS_DEFAULT_STAT = {
    WARRIOR = 1, PALADIN = 1, DEATHKNIGHT = 1,
    HUNTER = 2, ROGUE = 2, MONK = 2, DEMONHUNTER = 2, EVOKER = 4,
    DRUID = 2, SHAMAN = 4, MAGE = 4, WARLOCK = 4, PRIEST = 4,
}

local function GetPrimaryStatIndex()
    local specIndex = SafeCall(GetSpecialization)
    if not specIndex then
        local _, classFile = UnitClass("player")
        return CLASS_DEFAULT_STAT[classFile] or 1
    end
    local _, _, _, _, _, primaryStat = SafeCall(GetSpecializationInfo, specIndex)
    if primaryStat == 1 then return 1
    elseif primaryStat == 2 then return 2
    elseif primaryStat == 4 then return 4
    end
    local _, classFile = UnitClass("player")
    return CLASS_DEFAULT_STAT[classFile] or 1
end

local STAT_NAMES = { [1] = "Strength", [2] = "Agility", [3] = "Stamina", [4] = "Intellect" }

local function GetPrimaryStats()
    local primary = GetPrimaryStatIndex()
    local stats = {}
    local order = { primary, 3 }
    if primary == 3 then order = { 3 } end
    for _, idx in ipairs(order) do
        local base, stat = SafeCall(UnitStat, "player", idx)
        local value = stat or base or 0
        local subs = {}
        if idx == 1 then
            local aBase, aPos, aNeg = SafeCall(UnitAttackPower, "player")
            subs[#subs + 1] = { label = "Attack Power", value = FormatNumber((aBase or 0) + (aPos or 0) + (aNeg or 0)) }
            subs[#subs + 1] = { label = "Critical Strike", value = string.format("%.1f%%", SafeCall(GetCritChance) or 0) }
        elseif idx == 2 then
            local aBase, aPos, aNeg = SafeCall(UnitAttackPower, "player")
            subs[#subs + 1] = { label = "Attack Power", value = FormatNumber((aBase or 0) + (aPos or 0) + (aNeg or 0)) }
            subs[#subs + 1] = { label = "Critical Strike", value = string.format("%.1f%%", SafeCall(GetCritChance) or 0) }
            local dodge = SafeCall(GetDodgeChance) or 0
            if dodge > 0 then subs[#subs + 1] = { label = "Dodge", value = string.format("%.1f%%", dodge) } end
        elseif idx == 4 then
            local sp = SafeCall(GetSpellBonusDamage, 2) or 0
            subs[#subs + 1] = { label = "Spell Power", value = FormatNumber(sp) }
            subs[#subs + 1] = { label = "Spell Critical", value = string.format("%.1f%%", SafeCall(GetSpellCritChance, 2) or 0) }
        elseif idx == 3 then
            subs[#subs + 1] = { label = "Max Health", value = FormatNumber(SafeCall(UnitHealthMax, "player") or 0) }
        end
        stats[#stats + 1] = { name = STAT_NAMES[idx] or "?", value = value, subs = subs }
    end
    local armorBase, armorEffective = SafeCall(UnitArmor, "player")
    local armor = armorEffective or armorBase or 0
    local armorSubs = {}
    if C_PaperDollInfo and C_PaperDollInfo.GetArmorReduction then
        local reduction = SafeCall(C_PaperDollInfo.GetArmorReduction, armor, UnitLevel("player") or 80) or 0
        armorSubs[#armorSubs + 1] = { label = "Damage Reduction", value = string.format("%.1f%%", reduction) }
    end
    stats[#stats + 1] = { name = "Armor", value = armor, subs = armorSubs }
    return stats
end

local ENHANCEMENT_DESCS = {
    ["Critical Strike"] = "Increases damage and healing from critical hits",
    ["Haste"]           = "Increases attack speed, casting speed, and regeneration",
    ["Mastery"]         = "Increases the effectiveness of your Mastery",
    ["Versatility"]     = "Increases damage done and reduces damage taken",
}

local function GetEnhancementStats()
    local stats = {}
    local crit = SafeCall(GetCritChance) or 0
    local critRating = CR_CRIT_MELEE and (SafeCall(GetCombatRating, CR_CRIT_MELEE) or 0) or 0
    local haste = SafeCall(GetHaste) or 0
    local hasteRating = CR_HASTE_MELEE and (SafeCall(GetCombatRating, CR_HASTE_MELEE) or 0) or 0
    local mastery = SafeCall(GetMasteryEffect) or 0
    local masteryRating = CR_MASTERY and (SafeCall(GetCombatRating, CR_MASTERY) or 0) or 0
    local vers = 0
    local versRating = 0
    if CR_VERSATILITY_DAMAGE_DONE then
        local bonus = SafeCall(GetCombatRatingBonus, CR_VERSATILITY_DAMAGE_DONE) or 0
        local vBonus = SafeCall(GetVersatilityBonus, CR_VERSATILITY_DAMAGE_DONE) or 0
        vers = bonus + vBonus
        versRating = SafeCall(GetCombatRating, CR_VERSATILITY_DAMAGE_DONE) or 0
    end
    local avoid = CR_AVOIDANCE and (SafeCall(GetCombatRatingBonus, CR_AVOIDANCE) or 0) or 0
    local avoidRating = CR_AVOIDANCE and (SafeCall(GetCombatRating, CR_AVOIDANCE) or 0) or 0
    local leech = CR_LIFESTEAL and (SafeCall(GetCombatRatingBonus, CR_LIFESTEAL) or 0) or 0
    local leechRating = CR_LIFESTEAL and (SafeCall(GetCombatRating, CR_LIFESTEAL) or 0) or 0
    local speed = CR_SPEED and (SafeCall(GetCombatRatingBonus, CR_SPEED) or 0) or 0
    local speedRating = CR_SPEED and (SafeCall(GetCombatRating, CR_SPEED) or 0) or 0

    stats[#stats + 1] = { label = "Critical Strike", value = crit, rating = critRating, desc = ENHANCEMENT_DESCS["Critical Strike"], detailed = true }
    stats[#stats + 1] = { label = "Haste",           value = haste, rating = hasteRating, desc = ENHANCEMENT_DESCS["Haste"], detailed = true }
    stats[#stats + 1] = { label = "Mastery",         value = mastery, rating = masteryRating, desc = ENHANCEMENT_DESCS["Mastery"], detailed = true }
    stats[#stats + 1] = { label = "Versatility",     value = vers, rating = versRating, desc = ENHANCEMENT_DESCS["Versatility"], detailed = true }
    stats[#stats + 1] = { label = "Avoidance",       value = avoid, rating = avoidRating, detailed = false, desc = "Reduces area of effect damage taken" }
    stats[#stats + 1] = { label = "Leech",           value = leech, rating = leechRating, detailed = false, desc = "Heals you for a percentage of damage and healing done" }
    stats[#stats + 1] = { label = "Speed",           value = speed, rating = speedRating, detailed = false, desc = "Increases movement speed" }
    return stats
end

local STANDING_COLORS = {
    [1] = { 0.80, 0.20, 0.20 },
    [2] = { 0.80, 0.30, 0.22 },
    [3] = { 0.75, 0.55, 0.20 },
    [4] = { 0.85, 0.77, 0.36 },
    [5] = { 0.40, 0.75, 0.35 },
    [6] = { 0.35, 0.70, 0.45 },
    [7] = { 0.30, 0.60, 0.80 },
    [8] = { 0.60, 0.50, 0.90 },
}

local STANDING_NAMES = {
    [1] = "Hated", [2] = "Hostile", [3] = "Unfriendly", [4] = "Neutral",
    [5] = "Friendly", [6] = "Honored", [7] = "Revered", [8] = "Exalted",
}

local RENOWN_COLOR = { 0.55, 0.40, 0.92 }
local LOCKED_COLOR = { 0.45, 0.45, 0.50 }

local function ReadReputationProgress(data, factionIndex)
    local standing = data.reaction or 4
    local minRep = tonumber(data.currentReactionThreshold) or 0
    local maxRep = tonumber(data.nextReactionThreshold) or 1
    local curRep = tonumber(data.currentStanding) or 0

    if factionIndex and GetFactionInfo then
        local ok, _, _, legacyStanding, legacyMin, legacyMax, legacyCur = pcall(GetFactionInfo, factionIndex)
        legacyMin = tonumber(legacyMin)
        legacyMax = tonumber(legacyMax)
        legacyCur = tonumber(legacyCur)
        if ok and legacyMin and legacyMax and legacyCur and legacyMax > legacyMin then
            if data.currentStanding == nil or maxRep <= minRep or (curRep == 0 and legacyCur ~= 0) then
                standing = tonumber(legacyStanding) or standing
                minRep = legacyMin
                maxRep = legacyMax
                curRep = legacyCur
            end
        end
    end

    local displayCurrent = curRep
    local displayMaximum = maxRep
    local progress = 0
    if displayMaximum > 0 then
        progress = displayCurrent / displayMaximum
    elseif maxRep > minRep then
        progress = (curRep - minRep) / (maxRep - minRep)
        displayCurrent = curRep - minRep
        displayMaximum = maxRep - minRep
    end
    return standing, math.max(0, math.min(1, progress)), displayCurrent, displayMaximum
end

local function ApplyRenownDisplay(faction)
    if not faction or not faction.factionID then return faction end
    if not (C_Reputation and C_Reputation.IsMajorFaction) then return faction end
    if not SafeCall(C_Reputation.IsMajorFaction, faction.factionID) then return faction end
    if not (C_MajorFactions and C_MajorFactions.GetMajorFactionData) then return faction end

    local mf = SafeCall(C_MajorFactions.GetMajorFactionData, faction.factionID)
    if not mf then return faction end

    local renownLevel = tonumber(mf.renownLevel) or 0
    local renownEarned = tonumber(mf.renownReputationEarned) or 0
    local renownThreshold = tonumber(mf.renownLevelThreshold) or 0
    local unlocked = mf.isUnlocked and true or false

    if not unlocked then
        faction.standingName = "Locked"
        faction.standingColor = LOCKED_COLOR
        faction.currentRep = 0
        faction.maxRep = math.max(1, renownThreshold)
        faction.progress = 0
        return faction
    end

    local atMax = false
    if C_MajorFactions.HasMaximumRenown then
        atMax = SafeCall(C_MajorFactions.HasMaximumRenown, faction.factionID) or false
    end

    faction.standingName = "Renown " .. tostring(renownLevel)
    faction.standingColor = RENOWN_COLOR
    if atMax then
        faction.currentRep = math.max(1, renownThreshold)
        faction.maxRep = math.max(1, renownThreshold)
        faction.progress = 1
    else
        faction.currentRep = renownEarned
        faction.maxRep = math.max(1, renownThreshold)
        faction.progress = math.min(1, renownEarned / faction.maxRep)
    end
    return faction
end

local function CollectReputations()
    local groups = {}
    local currentGroup = nil
    if not C_Reputation or not C_Reputation.GetNumFactions then return groups end

    local function ExpandAllHeaders()
        local changed = true
        while changed do
            changed = false
            local n = SafeCall(C_Reputation.GetNumFactions) or 0
            for i = 1, n do
                local data = SafeCall(C_Reputation.GetFactionDataByIndex, i)
                if data and (data.isHeader or data.isHeaderWithRep) and data.isCollapsed then
                    if C_Reputation.ExpandFactionHeader then
                        pcall(C_Reputation.ExpandFactionHeader, i)
                        changed = true
                        break
                    elseif ExpandFactionHeader then
                        pcall(ExpandFactionHeader, i)
                        changed = true
                        break
                    end
                end
            end
        end
    end
    ExpandAllHeaders()

    local numFactions = SafeCall(C_Reputation.GetNumFactions) or 0
    local headerStack = {}

    for i = 1, numFactions do
        local data = SafeCall(C_Reputation.GetFactionDataByIndex, i)
        if data then
            if data.isHeader or data.isHeaderWithRep then
                currentGroup = {
                    name = data.name or "Unknown",
                    factions = {},
                }
                if data.isHeaderWithRep and data.name and data.name ~= "" then
                    local standing, progress, currentRep, maxRep = ReadReputationProgress(data, i)
                    currentGroup.factions[#currentGroup.factions + 1] = ApplyRenownDisplay({
                        name = data.name,
                        standingID = standing,
                        standingName = STANDING_NAMES[standing] or "Neutral",
                        standingColor = STANDING_COLORS[standing] or { 0.85, 0.77, 0.36 },
                        progress = progress,
                        currentRep = currentRep,
                        maxRep = maxRep,
                        factionID = data.factionID,
                        factionIndex = i,
                    })
                end
                groups[#groups + 1] = currentGroup
            elseif data.name and data.name ~= "" and currentGroup then
                local standing, progress, currentRep, maxRep = ReadReputationProgress(data, i)
                currentGroup.factions[#currentGroup.factions + 1] = ApplyRenownDisplay({
                    name = data.name,
                    standingID = standing,
                    standingName = STANDING_NAMES[standing] or "Neutral",
                    standingColor = STANDING_COLORS[standing] or { 0.85, 0.77, 0.36 },
                    progress = progress,
                    currentRep = currentRep,
                    maxRep = maxRep,
                    factionID = data.factionID,
                    factionIndex = i,
                })
            end
        end
    end

    local filtered = {}
    for _, g in ipairs(groups) do
        if #g.factions > 0 then
            filtered[#filtered + 1] = g
        end
    end
    return filtered
end

local function CollectCurrencies()
    if not C_CurrencyInfo or not C_CurrencyInfo.GetCurrencyListSize then return {} end

    if C_CurrencyInfo.ExpandCurrencyList then
        local safety = 0
        local expanded = true
        while expanded and safety < 50 do
            expanded = false
            safety = safety + 1
            local n = SafeCall(C_CurrencyInfo.GetCurrencyListSize) or 0
            for i = 1, n do
                local info = SafeCall(C_CurrencyInfo.GetCurrencyListInfo, i)
                if info and info.isHeader then
                    local prevSize = n
                    pcall(C_CurrencyInfo.ExpandCurrencyList, i, true)
                    local newSize = SafeCall(C_CurrencyInfo.GetCurrencyListSize) or 0
                    if newSize > prevSize then
                        expanded = true
                        break
                    end
                end
            end
        end
    end

    local groups = {}
    local currentGroup = nil
    local count = SafeCall(C_CurrencyInfo.GetCurrencyListSize) or 0
    for i = 1, count do
        local info = SafeCall(C_CurrencyInfo.GetCurrencyListInfo, i)
        if info and info.name and info.name ~= "" then
            if info.isHeader then
                currentGroup = {
                    isHeader = true,
                    name = info.name,
                    currencies = {},
                }
                groups[#groups + 1] = currentGroup
            elseif currentGroup then
                local curID = info.currencyTypesID or info.currencyID
                if not curID and C_CurrencyInfo.GetCurrencyListLink then
                    local link = SafeCall(C_CurrencyInfo.GetCurrencyListLink, i)
                    if link then
                        curID = tonumber(link:match("currency:(%d+)"))
                    end
                end
                currentGroup.currencies[#currentGroup.currencies + 1] = {
                    name = info.name,
                    quantity = info.quantity or 0,
                    iconFileID = info.iconFileID,
                    maxQuantity = info.maxQuantity or 0,
                    currencyID = curID,
                    isTransferable = info.isAccountTransferable or false,
                }
            else
                if not currentGroup then
                    currentGroup = { isHeader = true, name = "Other", currencies = {} }
                    groups[#groups + 1] = currentGroup
                end
                local curID = info.currencyTypesID or info.currencyID
                if not curID and C_CurrencyInfo.GetCurrencyListLink then
                    local link = SafeCall(C_CurrencyInfo.GetCurrencyListLink, i)
                    if link then
                        curID = tonumber(link:match("currency:(%d+)"))
                    end
                end
                currentGroup.currencies[#currentGroup.currencies + 1] = {
                    name = info.name,
                    quantity = info.quantity or 0,
                    iconFileID = info.iconFileID,
                    maxQuantity = info.maxQuantity or 0,
                    currencyID = curID,
                    isTransferable = info.isAccountTransferable or false,
                }
            end
        end
    end

    local filtered = {}
    for _, g in ipairs(groups) do
        if #g.currencies > 0 then
            filtered[#filtered + 1] = g
        end
    end
    return filtered
end

local SLOT_LAYOUT = {
    { id = 1,  name = "HeadSlot",          label = "Head",       blizz = "CharacterHeadSlot" },
    { id = 2,  name = "NeckSlot",          label = "Neck",       blizz = "CharacterNeckSlot" },
    { id = 3,  name = "ShoulderSlot",      label = "Shoulders",  blizz = "CharacterShoulderSlot" },
    { id = 15, name = "BackSlot",          label = "Back",       blizz = "CharacterBackSlot" },
    { id = 5,  name = "ChestSlot",         label = "Chest",      blizz = "CharacterChestSlot" },
    { id = 9,  name = "WristSlot",         label = "Wrists",     blizz = "CharacterWristSlot" },
    { id = 10, name = "HandsSlot",         label = "Hands",      blizz = "CharacterHandsSlot" },
    { id = 6,  name = "WaistSlot",         label = "Waist",      blizz = "CharacterWaistSlot" },
    { id = 7,  name = "LegsSlot",          label = "Legs",       blizz = "CharacterLegsSlot" },
    { id = 8,  name = "FeetSlot",          label = "Feet",       blizz = "CharacterFeetSlot" },
    { id = 11, name = "Finger0Slot",       label = "Ring 1",     blizz = "CharacterFinger0Slot" },
    { id = 12, name = "Finger1Slot",       label = "Ring 2",     blizz = "CharacterFinger1Slot" },
    { id = 13, name = "Trinket0Slot",      label = "Trinket 1",  blizz = "CharacterTrinket0Slot" },
    { id = 14, name = "Trinket1Slot",      label = "Trinket 2",  blizz = "CharacterTrinket1Slot" },
    { id = 16, name = "MainHandSlot",      label = "Main Hand",  blizz = "CharacterMainHandSlot" },
    { id = 17, name = "SecondaryHandSlot", label = "Off Hand",   blizz = "CharacterSecondaryHandSlot" },
}

local function AttachBlizzardSlotButtons()
    if not Panel._refs or not Panel._refs.gearSlots then return end
    local gearSlots = Panel._refs.gearSlots
    for _, slotDef in ipairs(SLOT_LAYOUT) do
        local muiBtn = gearSlots[slotDef.id]
        if muiBtn and slotDef.blizz and not muiBtn._blizzAttached then
            local blizzBtn = _G[slotDef.blizz]
            if blizzBtn then
                muiBtn._blizzAttached = true
                blizzBtn:SetParent(muiBtn)
                blizzBtn:ClearAllPoints()
                blizzBtn:SetAllPoints(muiBtn)
                blizzBtn:SetFrameLevel(muiBtn:GetFrameLevel() + 10)
                blizzBtn:Show()
                if blizzBtn.GetRegions then
                    for _, region in ipairs({blizzBtn:GetRegions()}) do
                        region:SetAlpha(0)
                        if region.Hide then region:Hide() end
                    end
                end
                if blizzBtn.GetChildren then
                    for _, child in ipairs({blizzBtn:GetChildren()}) do
                        child:SetAlpha(0)
                        if child.Hide then child:Hide() end
                    end
                end
                blizzBtn:EnableMouse(true)
                blizzBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
            end
        end
    end
end

function Panel.EnsurePanel()
    if Panel._state.initialized then return Panel._refs.panel end
    Panel._state.initialized = true
    GetActiveTheme()

    local R = Panel._refs

    local p = CreateFrame("Frame", "MidnightUI_CharacterPanel", UIParent, "BackdropTemplate")
    p:SetSize(CFG.WIDTH, CFG.HEIGHT)
    p:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
    p:SetFrameStrata(CFG.STRATA)
    p:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    p:SetBackdropColor(TC("frameBg"))
    p:SetBackdropBorderColor(TC("accent"))
    p:Hide()
    R.panel = p

    CreateDropShadow(p, 5)

    local hdr = CreateFrame("Frame", nil, p)
    hdr:SetHeight(CFG.HEADER_H)
    hdr:SetPoint("TOPLEFT", p, "TOPLEFT", 0, 0)
    hdr:SetPoint("TOPRIGHT", p, "TOPRIGHT", 0, 0)
    local hdrBg = hdr:CreateTexture(nil, "BACKGROUND")
    hdrBg:SetAllPoints()
    hdrBg:SetColorTexture(TC("headerBg"))
    R.header = hdr
    R.hdrBg = hdrBg

    local hdrAccent = hdr:CreateTexture(nil, "OVERLAY")
    hdrAccent:SetHeight(2)
    hdrAccent:SetPoint("BOTTOMLEFT", hdr, "BOTTOMLEFT", 0, 0)
    hdrAccent:SetPoint("BOTTOMRIGHT", hdr, "BOTTOMRIGHT", 0, 0)
    hdrAccent:SetTexture(W8)
    if hdrAccent.SetGradient and CreateColor then
        hdrAccent:SetGradient("HORIZONTAL",
            CreateColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.6),
            CreateColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.0))
    end
    R.hdrAccent = hdrAccent

    local titleFS = hdr:CreateFontString(nil, "OVERLAY")
    TrySetFont(titleFS, TITLE_FONT, 16, "OUTLINE")
    titleFS:SetPoint("LEFT", hdr, "LEFT", 16, 0)
    titleFS:SetText("Character")
    titleFS:SetTextColor(TC("titleText"))
    R.titleFS = titleFS

    local closeBtn = CreateFrame("Button", nil, hdr)
    closeBtn:SetSize(28, 28)
    closeBtn:SetPoint("RIGHT", hdr, "RIGHT", -10, 0)
    local closeTx = closeBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(closeTx, TITLE_FONT, 16, "OUTLINE")
    closeTx:SetPoint("CENTER")
    closeTx:SetText("X")
    closeTx:SetTextColor(activeTheme.mutedText[1], activeTheme.mutedText[2], activeTheme.mutedText[3], 0.70)
    closeBtn:SetScript("OnEnter", function() closeTx:SetTextColor(TC("titleText")) end)
    closeBtn:SetScript("OnLeave", function() closeTx:SetTextColor(activeTheme.mutedText[1], activeTheme.mutedText[2], activeTheme.mutedText[3], 0.70) end)
    closeBtn:SetScript("OnClick", function() Panel.Hide() end)

    local equipMgrBtn = CreateFrame("Button", nil, hdr)
    equipMgrBtn:SetSize(24, 24)
    equipMgrBtn:SetPoint("RIGHT", closeBtn, "LEFT", -8, 0)
    local equipMgrIcon = equipMgrBtn:CreateTexture(nil, "OVERLAY")
    equipMgrIcon:SetAllPoints()
    equipMgrIcon:SetTexture("Interface\\Icons\\INV_Chest_Cloth_17")
    equipMgrIcon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
    equipMgrIcon:SetVertexColor(activeTheme.mutedText[1], activeTheme.mutedText[2], activeTheme.mutedText[3], 0.70)
    equipMgrBtn:SetScript("OnEnter", function(self)
        equipMgrIcon:SetVertexColor(TC("titleText"))
        GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
        GameTooltip:SetText("Equipment Manager", 1, 1, 1)
        GameTooltip:Show()
    end)
    equipMgrBtn:SetScript("OnLeave", function()
        equipMgrIcon:SetVertexColor(activeTheme.mutedText[1], activeTheme.mutedText[2], activeTheme.mutedText[3], 0.70)
        GameTooltip:Hide()
    end)
    equipMgrBtn:SetScript("OnClick", function()
        Panel.ToggleEquipmentManager()
    end)
    R.equipMgrIcon = equipMgrIcon

    local gearBtn = CreateFrame("Button", nil, hdr)
    gearBtn:SetSize(24, 24)
    gearBtn:SetPoint("RIGHT", equipMgrBtn, "LEFT", -6, 0)
    local gearIcon = gearBtn:CreateTexture(nil, "OVERLAY")
    gearIcon:SetAllPoints()
    gearIcon:SetTexture("Interface\\Icons\\Trade_Engineering")
    gearIcon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
    gearIcon:SetVertexColor(activeTheme.mutedText[1], activeTheme.mutedText[2], activeTheme.mutedText[3], 0.70)
    gearBtn:SetScript("OnEnter", function() gearIcon:SetVertexColor(TC("titleText")) end)
    gearBtn:SetScript("OnLeave", function()
        gearIcon:SetVertexColor(activeTheme.mutedText[1], activeTheme.mutedText[2], activeTheme.mutedText[3], 0.70)
    end)
    gearBtn:SetScript("OnClick", function()
        if R.settingsPopup and R.settingsPopup:IsShown() then
            R.settingsPopup:Hide()
        else
            Panel.ShowSettingsPopup()
        end
    end)
    R.gearBtn = gearBtn
    R.gearIcon = gearIcon

    p:EnableMouse(true); p:SetMovable(true)
    hdr:EnableMouse(true); hdr:RegisterForDrag("LeftButton")
    hdr:SetScript("OnDragStart", function() p:StartMoving() end)
    hdr:SetScript("OnDragStop", function() p:StopMovingOrSizing() end)

    local modelArea = CreateFrame("Frame", nil, p)
    modelArea:SetPoint("TOPLEFT", hdr, "BOTTOMLEFT", 0, 0)
    modelArea:SetPoint("BOTTOMRIGHT", p, "BOTTOMRIGHT", 0, CFG.TAB_BAR_H)
    R.modelArea = modelArea

    local modelBgBase = modelArea:CreateTexture(nil, "BACKGROUND", nil, -8)
    modelBgBase:SetAllPoints()
    modelBgBase:SetColorTexture(0.02, 0.02, 0.03, 1)

    local modelBg = modelArea:CreateTexture(nil, "BACKGROUND", nil, -7)
    modelBg:SetAllPoints()
    local atlasOk = pcall(modelBg.SetAtlas, modelBg, "completiondialog-midnightcampaign-background", false)
    if not atlasOk then
        modelBg:SetColorTexture(TC("modelBg"))
    end
    modelBg:SetAlpha(0.7)
    R.modelBg = modelBg

    local vigTop = modelArea:CreateTexture(nil, "BACKGROUND", nil, -5)
    vigTop:SetHeight(120)
    vigTop:SetPoint("TOPLEFT", modelArea, "TOPLEFT", 0, 0)
    vigTop:SetPoint("TOPRIGHT", modelArea, "TOPRIGHT", 0, 0)
    vigTop:SetTexture(W8)
    if vigTop.SetGradient and CreateColor then
        vigTop:SetGradient("VERTICAL", CreateColor(0, 0, 0, 0), CreateColor(0, 0, 0, 0.6))
    end
    local vigBot = modelArea:CreateTexture(nil, "BACKGROUND", nil, -5)
    vigBot:SetHeight(100)
    vigBot:SetPoint("BOTTOMLEFT", modelArea, "BOTTOMLEFT", 0, 0)
    vigBot:SetPoint("BOTTOMRIGHT", modelArea, "BOTTOMRIGHT", 0, 0)
    vigBot:SetTexture(W8)
    if vigBot.SetGradient and CreateColor then
        vigBot:SetGradient("VERTICAL", CreateColor(0, 0, 0, 0.7), CreateColor(0, 0, 0, 0))
    end

    local charModel = CreateFrame("PlayerModel", nil, modelArea)
    charModel:SetPoint("TOPLEFT", modelArea, "TOPLEFT", 0, 0)
    charModel:SetPoint("BOTTOMRIGHT", modelArea, "BOTTOMRIGHT", 0, 0)
    charModel:SetFrameLevel(modelArea:GetFrameLevel() + 2)
    R.charModel = charModel

    charModel:EnableMouse(true)
    charModel:SetScript("OnMouseDown", function(self, button)
        if button == "LeftButton" then
            Panel._state.isDragging = true
            Panel._state.dragStartX = GetCursorPosition()
            Panel._state.dragStartFacing = Panel._state.modelFacing
        end
    end)
    charModel:SetScript("OnMouseUp", function(self, button)
        if button == "LeftButton" then
            Panel._state.isDragging = false
        end
    end)
    charModel:SetScript("OnUpdate", function(self)
        if Panel._state.isDragging then
            local cx = GetCursorPosition()
            local delta = (cx - Panel._state.dragStartX) * 0.01
            Panel._state.modelFacing = Panel._state.dragStartFacing + delta
            pcall(self.SetFacing, self, Panel._state.modelFacing)
        end
    end)

    charModel:EnableMouseWheel(true)
    charModel:SetScript("OnMouseWheel", function(self, delta)
        local zoom = Panel._state.modelZoom - (delta * 0.1)
        zoom = math.max(0.5, math.min(2.0, zoom))
        Panel._state.modelZoom = zoom
        pcall(self.SetCamDistanceScale, self, zoom)
    end)

    local identityBar = CreateFrame("Frame", nil, modelArea, "BackdropTemplate")
    identityBar:SetHeight(CFG.IDENTITY_H)
    identityBar:SetPoint("BOTTOMLEFT", modelArea, "BOTTOMLEFT", CFG.LEFT_GLASS_W + CFG.GLASS_MARGIN, 0)
    identityBar:SetPoint("BOTTOMRIGHT", modelArea, "BOTTOMRIGHT", -(CFG.RIGHT_GLASS_W + CFG.GLASS_MARGIN), 0)
    identityBar:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    identityBar:SetBackdropColor(0, 0, 0, 0.55)
    identityBar:SetBackdropBorderColor(1, 1, 1, 0.05)
    identityBar:SetFrameLevel(charModel:GetFrameLevel() + 5)
    R.identityBar = identityBar

    local nameFS = identityBar:CreateFontString(nil, "OVERLAY")
    TrySetFont(nameFS, TITLE_FONT, 18, "OUTLINE")
    nameFS:SetPoint("TOP", identityBar, "TOP", 0, -10)
    nameFS:SetTextColor(TC("titleText"))
    nameFS:SetShadowColor(0, 0, 0, 0.9)
    nameFS:SetShadowOffset(2, -2)
    R.nameFS = nameFS

    local nameBtn = CreateFrame("Button", nil, identityBar)
    nameBtn:SetPoint("TOPLEFT", nameFS, "TOPLEFT", -4, 4)
    nameBtn:SetPoint("BOTTOMRIGHT", nameFS, "BOTTOMRIGHT", 4, -4)
    nameBtn:SetFrameLevel(identityBar:GetFrameLevel() + 2)
    nameBtn:RegisterForClicks("RightButtonUp")
    nameBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        GameTooltip:SetText("|cffaaaaaaRight Click:|r Change Title", 1, 1, 1)
        GameTooltip:Show()
    end)
    nameBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
    nameBtn:SetScript("OnClick", function(_, button)
        if button == "RightButton" then
            Panel.ToggleTitlePicker()
        end
    end)

    local specFS = identityBar:CreateFontString(nil, "OVERLAY")
    TrySetFont(specFS, BODY_FONT, 11, "")
    specFS:SetPoint("TOP", nameFS, "BOTTOM", 0, -4)
    specFS:SetTextColor(TC("bodyText"))
    specFS:SetShadowColor(0, 0, 0, 0.8)
    specFS:SetShadowOffset(1, -1)
    R.specFS = specFS

    local leftGlass = CreateGlassPanel(modelArea, CFG.LEFT_GLASS_W)
    leftGlass:SetPoint("TOPLEFT", modelArea, "TOPLEFT", CFG.GLASS_MARGIN, -CFG.GLASS_MARGIN)
    leftGlass:SetPoint("BOTTOMLEFT", modelArea, "BOTTOMLEFT", CFG.GLASS_MARGIN, CFG.GLASS_MARGIN)
    leftGlass:SetFrameLevel(charModel:GetFrameLevel() + 3)
    R.leftGlass = leftGlass

    local gearLabel = leftGlass:CreateFontString(nil, "OVERLAY")
    TrySetFont(gearLabel, BODY_FONT, 10, "OUTLINE")
    gearLabel:SetPoint("TOPLEFT", leftGlass, "TOPLEFT", CFG.GLASS_PAD, -CFG.GLASS_PAD)
    gearLabel:SetText("EQUIPMENT")
    gearLabel:SetTextColor(TC("mutedText"))
    R.gearLabel = gearLabel

    local gearSlots = {}
    local GEAR_ROW_H = 36
    local GEAR_ROW_GAP = 2
    local GEAR_ICON_SIZE = 32

    for i, slotDef in ipairs(SLOT_LAYOUT) do
        local slotID = slotDef.id
        local slotName = slotDef.name

        local btn = CreateFrame("Button", nil, leftGlass)
        btn:SetHeight(GEAR_ROW_H)
        local yOfs = -(CFG.GLASS_PAD + 16 + (i - 1) * (GEAR_ROW_H + GEAR_ROW_GAP))
        btn:SetPoint("TOPLEFT", leftGlass, "TOPLEFT", 0, yOfs)
        btn:SetPoint("TOPRIGHT", leftGlass, "TOPRIGHT", 0, yOfs)

        local hoverBg = btn:CreateTexture(nil, "BACKGROUND")
        hoverBg:SetAllPoints()
        hoverBg:SetColorTexture(1, 1, 1, 0.05)
        hoverBg:Hide()
        btn._hoverBg = hoverBg

        local icon = btn:CreateTexture(nil, "ARTWORK")
        icon:SetSize(GEAR_ICON_SIZE, GEAR_ICON_SIZE)
        icon:SetPoint("LEFT", btn, "LEFT", CFG.GLASS_PAD, 0)
        icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
        btn._icon = icon

        local bTop = btn:CreateTexture(nil, "OVERLAY"); bTop:SetHeight(1)
        bTop:SetPoint("TOPLEFT", icon, "TOPLEFT", -1, 1); bTop:SetPoint("TOPRIGHT", icon, "TOPRIGHT", 1, 1)
        bTop:SetColorTexture(0.3, 0.3, 0.3, 0.5)
        local bBot = btn:CreateTexture(nil, "OVERLAY"); bBot:SetHeight(1)
        bBot:SetPoint("BOTTOMLEFT", icon, "BOTTOMLEFT", -1, -1); bBot:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 1, -1)
        bBot:SetColorTexture(0.3, 0.3, 0.3, 0.5)
        local bLeft = btn:CreateTexture(nil, "OVERLAY"); bLeft:SetWidth(1)
        bLeft:SetPoint("TOPLEFT", icon, "TOPLEFT", -1, 1); bLeft:SetPoint("BOTTOMLEFT", icon, "BOTTOMLEFT", -1, -1)
        bLeft:SetColorTexture(0.3, 0.3, 0.3, 0.5)
        local bRight = btn:CreateTexture(nil, "OVERLAY"); bRight:SetWidth(1)
        bRight:SetPoint("TOPRIGHT", icon, "TOPRIGHT", 1, 1); bRight:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 1, -1)
        bRight:SetColorTexture(0.3, 0.3, 0.3, 0.5)
        btn._borderParts = { bTop, bBot, bLeft, bRight }

        local slotLabelFS = btn:CreateFontString(nil, "OVERLAY")
        TrySetFont(slotLabelFS, BODY_FONT, 10, "")
        slotLabelFS:SetPoint("TOPLEFT", icon, "TOPRIGHT", 6, -1)
        slotLabelFS:SetText(slotDef.label)
        slotLabelFS:SetTextColor(activeTheme.mutedText[1], activeTheme.mutedText[2], activeTheme.mutedText[3], 0.6)
        btn._slotLabel = slotLabelFS

        local itemNameFS = btn:CreateFontString(nil, "OVERLAY")
        TrySetFont(itemNameFS, BODY_FONT, 10, "")
        itemNameFS:SetPoint("BOTTOMLEFT", icon, "BOTTOMRIGHT", 6, 2)
        itemNameFS:SetPoint("RIGHT", btn, "RIGHT", -CFG.GLASS_PAD, 0)
        itemNameFS:SetJustifyH("LEFT")
        itemNameFS:SetWordWrap(false)
        itemNameFS:SetTextColor(TC("bodyText"))
        btn._itemName = itemNameFS

        local ilvlFS = btn:CreateFontString(nil, "OVERLAY")
        TrySetFont(ilvlFS, BODY_FONT, 9, "OUTLINE")
        ilvlFS:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", -1, 1)
        ilvlFS:SetJustifyH("RIGHT")
        ilvlFS:SetTextColor(1, 1, 1, 0.9)
        btn._ilvl = ilvlFS

        btn._slotID = slotID
        btn._slotName = slotName

        btn:SetScript("OnEnter", function(self)
            self._hoverBg:Show()
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            pcall(GameTooltip.SetInventoryItem, GameTooltip, "player", self._slotID)
            GameTooltip:Show()
        end)
        btn:SetScript("OnLeave", function(self) self._hoverBg:Hide(); GameTooltip:Hide() end)

        btn._muiSlotDef = slotDef

        gearSlots[slotID] = btn
    end
    R.gearSlots = gearSlots

    local rightGlass = CreateGlassPanel(modelArea, CFG.RIGHT_GLASS_W)
    rightGlass:SetPoint("TOPRIGHT", modelArea, "TOPRIGHT", -CFG.GLASS_MARGIN, -CFG.GLASS_MARGIN)
    rightGlass:SetPoint("BOTTOMRIGHT", modelArea, "BOTTOMRIGHT", -CFG.GLASS_MARGIN, CFG.GLASS_MARGIN)
    rightGlass:SetFrameLevel(charModel:GetFrameLevel() + 3)
    R.rightGlass = rightGlass

    local ilvlLabel = rightGlass:CreateFontString(nil, "OVERLAY")
    TrySetFont(ilvlLabel, BODY_FONT, 10, "OUTLINE")
    ilvlLabel:SetPoint("TOP", rightGlass, "TOP", 0, -18)
    ilvlLabel:SetText("ITEM LEVEL")
    ilvlLabel:SetTextColor(TC("mutedText"))
    R.ilvlLabel = ilvlLabel

    local ilvlValue = rightGlass:CreateFontString(nil, "OVERLAY")
    TrySetFont(ilvlValue, TITLE_FONT, 28, "OUTLINE")
    ilvlValue:SetPoint("TOP", ilvlLabel, "BOTTOM", 0, -4)
    ilvlValue:SetTextColor(TC("titleText"))
    ilvlValue:SetShadowColor(0, 0, 0, 0.8)
    ilvlValue:SetShadowOffset(2, -2)
    R.ilvlValue = ilvlValue

    local ilvlSep = rightGlass:CreateTexture(nil, "OVERLAY")
    ilvlSep:SetHeight(1)
    ilvlSep:SetPoint("LEFT", rightGlass, "LEFT", CFG.GLASS_PAD, 0)
    ilvlSep:SetPoint("RIGHT", rightGlass, "RIGHT", -CFG.GLASS_PAD, 0)
    ilvlSep:SetPoint("TOP", ilvlValue, "BOTTOM", 0, -16)
    ilvlSep:SetTexture(W8)
    if ilvlSep.SetGradient and CreateColor then
        ilvlSep:SetGradient("HORIZONTAL",
            CreateColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0),
            CreateColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.4))
    end

    local attrLabel = rightGlass:CreateFontString(nil, "OVERLAY")
    TrySetFont(attrLabel, BODY_FONT, 10, "OUTLINE")
    attrLabel:SetPoint("TOPLEFT", ilvlSep, "BOTTOMLEFT", 0, -10)
    attrLabel:SetText("ATTRIBUTES")
    attrLabel:SetTextColor(TC("mutedText"))
    R.attrLabel = attrLabel

    local MAX_ATTR = 3
    local MAX_SUBS = 3
    local attrRows = {}
    for i = 1, MAX_ATTR do
        local nameFS2 = rightGlass:CreateFontString(nil, "OVERLAY")
        TrySetFont(nameFS2, BODY_FONT, 11, "OUTLINE")
        nameFS2:SetTextColor(TC("accent"))
        nameFS2:SetJustifyH("LEFT")
        local valFS = rightGlass:CreateFontString(nil, "OVERLAY")
        TrySetFont(valFS, BODY_FONT, 11, "")
        valFS:SetTextColor(TC("titleText"))
        valFS:SetJustifyH("RIGHT")

        if i == 1 then
            nameFS2:SetPoint("TOPLEFT", attrLabel, "BOTTOMLEFT", 0, -8)
        end
        nameFS2:SetPoint("LEFT", rightGlass, "LEFT", CFG.GLASS_PAD, 0)
        valFS:SetPoint("RIGHT", rightGlass, "RIGHT", -CFG.GLASS_PAD, 0)
        valFS:SetPoint("TOP", nameFS2, "TOP", 0, 0)

        local subLines = {}
        for j = 1, MAX_SUBS do
            local subName = rightGlass:CreateFontString(nil, "OVERLAY")
            TrySetFont(subName, BODY_FONT, 10, "")
            subName:SetTextColor(TC("bodyText"))
            subName:SetJustifyH("LEFT")
            local subVal = rightGlass:CreateFontString(nil, "OVERLAY")
            TrySetFont(subVal, BODY_FONT, 10, "")
            subVal:SetTextColor(0.55, 0.85, 0.55, 0.9)
            subVal:SetJustifyH("RIGHT")

            subName:SetPoint("LEFT", rightGlass, "LEFT", CFG.GLASS_PAD + 6, 0)
            subVal:SetPoint("RIGHT", rightGlass, "RIGHT", -CFG.GLASS_PAD, 0)
            subName:Hide(); subVal:Hide()

            subLines[j] = { name = subName, value = subVal }
        end

        nameFS2:Hide(); valFS:Hide()
        attrRows[i] = { name = nameFS2, value = valFS, subs = subLines }
    end
    R.attrRows = attrRows

    local attrEnhSep = rightGlass:CreateTexture(nil, "OVERLAY")
    attrEnhSep:SetHeight(1)
    attrEnhSep:SetPoint("LEFT", rightGlass, "LEFT", CFG.GLASS_PAD, 0)
    attrEnhSep:SetPoint("RIGHT", rightGlass, "RIGHT", -CFG.GLASS_PAD, 0)
    attrEnhSep:SetTexture(W8)
    if attrEnhSep.SetGradient and CreateColor then
        attrEnhSep:SetGradient("HORIZONTAL",
            CreateColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.3),
            CreateColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0))
    end
    attrEnhSep:Hide()
    R.attrEnhSep = attrEnhSep

    local enhLabel = rightGlass:CreateFontString(nil, "OVERLAY")
    TrySetFont(enhLabel, BODY_FONT, 10, "OUTLINE")
    enhLabel:SetText("ENHANCEMENTS")
    enhLabel:SetTextColor(TC("mutedText"))
    enhLabel:Hide()
    R.enhLabel = enhLabel

    local enhRows = {}
    for i = 1, 7 do
        local rowBtn = CreateFrame("Button", nil, rightGlass)
        rowBtn:SetHeight(20)
        rowBtn:SetPoint("LEFT", rightGlass, "LEFT", 0, 0)
        rowBtn:SetPoint("RIGHT", rightGlass, "RIGHT", 0, 0)

        local lbl = rowBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(lbl, BODY_FONT, 11, "")
        lbl:SetJustifyH("LEFT")
        lbl:SetPoint("LEFT", rowBtn, "LEFT", CFG.GLASS_PAD, 0)

        local val = rowBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(val, BODY_FONT, 11, "")
        val:SetJustifyH("RIGHT")
        val:SetPoint("RIGHT", rowBtn, "RIGHT", -CFG.GLASS_PAD, 0)

        local ratingFS = rowBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(ratingFS, BODY_FONT, 10, "")
        ratingFS:SetJustifyH("LEFT")
        ratingFS:SetTextColor(0.55, 0.85, 0.55, 0.7)

        rowBtn:Hide()
        enhRows[i] = { btn = rowBtn, label = lbl, value = val, rating = ratingFS }
    end
    R.enhRows = enhRows

    local tabBar = CreateFrame("Frame", nil, p)
    tabBar:SetHeight(CFG.TAB_BAR_H)
    tabBar:SetPoint("BOTTOMLEFT", p, "BOTTOMLEFT", 0, 0)
    tabBar:SetPoint("BOTTOMRIGHT", p, "BOTTOMRIGHT", 0, 0)
    local tabBg = tabBar:CreateTexture(nil, "BACKGROUND")
    tabBg:SetAllPoints()
    tabBg:SetColorTexture(TC("headerBg"))
    R.tabBar = tabBar
    R.tabBg = tabBg

    local tabAccent = tabBar:CreateTexture(nil, "OVERLAY")
    tabAccent:SetHeight(1)
    tabAccent:SetPoint("TOPLEFT", tabBar, "TOPLEFT", 0, 0)
    tabAccent:SetPoint("TOPRIGHT", tabBar, "TOPRIGHT", 0, 0)
    tabAccent:SetColorTexture(activeTheme.divider[1], activeTheme.divider[2], activeTheme.divider[3], 0.3)
    R.tabAccent = tabAccent

    local TAB_DEFS = {
        { key = "character",  label = "CHARACTER" },
        { key = "reputation", label = "REPUTATION" },
        { key = "currency",   label = "CURRENCY" },
    }
    local tabButtons = {}
    local tabWidth = CFG.WIDTH / #TAB_DEFS
    for i, def in ipairs(TAB_DEFS) do
        local btn = CreateFrame("Button", nil, tabBar)
        btn:SetSize(tabWidth, CFG.TAB_BAR_H)
        btn:SetPoint("BOTTOMLEFT", tabBar, "BOTTOMLEFT", (i - 1) * tabWidth, 0)

        local label = btn:CreateFontString(nil, "OVERLAY")
        TrySetFont(label, BODY_FONT, 11, "OUTLINE")
        label:SetPoint("CENTER", btn, "CENTER", 0, 0)
        label:SetText(def.label)
        btn._label = label
        btn._key = def.key

        local underline = btn:CreateTexture(nil, "OVERLAY")
        underline:SetHeight(2)
        underline:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", 20, 0)
        underline:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -20, 0)
        underline:SetColorTexture(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 1)
        underline:Hide()
        btn._underline = underline

        btn:SetScript("OnClick", function()
            Panel.SetActiveTab(def.key)
        end)
        btn:SetScript("OnEnter", function(self)
            if Panel._state.activeTab ~= self._key then
                self._label:SetTextColor(TC("bodyText"))
            end
        end)
        btn:SetScript("OnLeave", function(self)
            if Panel._state.activeTab ~= self._key then
                self._label:SetTextColor(TC("tabInactive"))
            end
        end)

        tabButtons[def.key] = btn
    end
    R.tabButtons = tabButtons

    local repOverlay = CreateFrame("Frame", nil, p, "BackdropTemplate")
    repOverlay:SetPoint("TOPLEFT", hdr, "BOTTOMLEFT", 0, 0)
    repOverlay:SetPoint("BOTTOMRIGHT", tabBar, "TOPRIGHT", 0, 0)
    repOverlay:SetBackdrop({ bgFile = W8 })
    repOverlay:SetBackdropColor(TC("frameBg"))
    repOverlay:SetFrameLevel(charModel:GetFrameLevel() + 10)
    repOverlay:EnableMouse(true)
    repOverlay:Hide()
    R.repOverlay = repOverlay

    local repBack = CreateFrame("Button", nil, repOverlay)
    repBack:SetSize(80, 24)
    repBack:SetPoint("TOPLEFT", repOverlay, "TOPLEFT", 12, -10)
    local repBackFS = repBack:CreateFontString(nil, "OVERLAY")
    TrySetFont(repBackFS, BODY_FONT, 11, "OUTLINE")
    repBackFS:SetPoint("LEFT")
    repBackFS:SetText("< Back")
    repBackFS:SetTextColor(TC("accent"))
    repBack:SetScript("OnClick", function() Panel.SetActiveTab("character") end)
    repBack:SetScript("OnEnter", function() repBackFS:SetTextColor(TC("titleText")) end)
    repBack:SetScript("OnLeave", function() repBackFS:SetTextColor(TC("accent")) end)
    R.repBackFS = repBackFS

    local repTitle = repOverlay:CreateFontString(nil, "OVERLAY")
    TrySetFont(repTitle, TITLE_FONT, 16, "OUTLINE")
    repTitle:SetPoint("TOP", repOverlay, "TOP", 0, -12)
    repTitle:SetText("Reputation")
    repTitle:SetTextColor(TC("titleText"))
    R.repTitle = repTitle

    local repScroll = CreateFrame("ScrollFrame", nil, repOverlay, "UIPanelScrollFrameTemplate")
    repScroll:SetPoint("TOPLEFT", repOverlay, "TOPLEFT", 10, -42)
    repScroll:SetPoint("BOTTOMRIGHT", repOverlay, "BOTTOMRIGHT", -28, 10)
    local repContent = CreateFrame("Frame", nil, repScroll)
    repContent:SetWidth(1)
    repScroll:SetScrollChild(repContent)
    repScroll:SetScript("OnSizeChanged", function(self, w) if w > 0 then repContent:SetWidth(w) end end)
    if repScroll.ScrollBar then
        local sb = repScroll.ScrollBar
        if sb.ThumbTexture then sb.ThumbTexture:SetTexture(W8); sb.ThumbTexture:SetVertexColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.30); sb.ThumbTexture:SetWidth(4) end
        if sb.ScrollUpButton then sb.ScrollUpButton:SetAlpha(0) end
        if sb.ScrollDownButton then sb.ScrollDownButton:SetAlpha(0) end
    end
    R.repScroll = repScroll
    R.repContent = repContent

    local curOverlay = CreateFrame("Frame", nil, p, "BackdropTemplate")
    curOverlay:SetPoint("TOPLEFT", hdr, "BOTTOMLEFT", 0, 0)
    curOverlay:SetPoint("BOTTOMRIGHT", tabBar, "TOPRIGHT", 0, 0)
    curOverlay:SetBackdrop({ bgFile = W8 })
    curOverlay:SetBackdropColor(TC("frameBg"))
    curOverlay:SetFrameLevel(charModel:GetFrameLevel() + 10)
    curOverlay:EnableMouse(true)
    curOverlay:Hide()
    R.curOverlay = curOverlay

    local curBack = CreateFrame("Button", nil, curOverlay)
    curBack:SetSize(80, 24)
    curBack:SetPoint("TOPLEFT", curOverlay, "TOPLEFT", 12, -10)
    local curBackFS = curBack:CreateFontString(nil, "OVERLAY")
    TrySetFont(curBackFS, BODY_FONT, 11, "OUTLINE")
    curBackFS:SetPoint("LEFT")
    curBackFS:SetText("< Back")
    curBackFS:SetTextColor(TC("accent"))
    curBack:SetScript("OnClick", function() Panel.SetActiveTab("character") end)
    curBack:SetScript("OnEnter", function() curBackFS:SetTextColor(TC("titleText")) end)
    curBack:SetScript("OnLeave", function() curBackFS:SetTextColor(TC("accent")) end)
    R.curBackFS = curBackFS

    local curTitle = curOverlay:CreateFontString(nil, "OVERLAY")
    TrySetFont(curTitle, TITLE_FONT, 16, "OUTLINE")
    curTitle:SetPoint("TOP", curOverlay, "TOP", 0, -10)
    curTitle:SetText("Currency")
    curTitle:SetTextColor(TC("titleText"))
    R.curTitle = curTitle

    local curSubtitle = curOverlay:CreateFontString(nil, "OVERLAY")
    TrySetFont(curSubtitle, BODY_FONT, 10, "")
    curSubtitle:SetPoint("TOP", curTitle, "BOTTOM", 0, -2)
    curSubtitle:SetText("Preview  \194\183  Transferable currencies marked with Warband icon")
    curSubtitle:SetTextColor(TC("mutedText"))
    R.curSubtitle = curSubtitle

    local openBlizzBtn = CreateFrame("Button", nil, curOverlay, "BackdropTemplate")
    openBlizzBtn:SetSize(180, 22)
    openBlizzBtn:SetPoint("TOPRIGHT", curOverlay, "TOPRIGHT", -12, -10)
    openBlizzBtn:SetFrameLevel(curOverlay:GetFrameLevel() + 20)
    openBlizzBtn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1 })
    openBlizzBtn:SetBackdropColor(TC("headerBg"))
    openBlizzBtn:SetBackdropBorderColor(TC("accent"))
    local openBlizzFS = openBlizzBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(openBlizzFS, BODY_FONT, 10, "")
    openBlizzFS:SetPoint("CENTER")
    openBlizzFS:SetText("How to Transfer")
    openBlizzFS:SetTextColor(TC("accent"))
    openBlizzBtn:SetScript("OnEnter", function()
        openBlizzBtn:SetBackdropColor(activeTheme.accent[1] * 0.15, activeTheme.accent[2] * 0.15, activeTheme.accent[3] * 0.15, 0.9)
        openBlizzFS:SetTextColor(TC("titleText"))
    end)
    openBlizzBtn:SetScript("OnLeave", function()
        openBlizzBtn:SetBackdropColor(TC("headerBg"))
        openBlizzFS:SetTextColor(TC("accent"))
    end)
    R.openBlizzBtn = openBlizzBtn
    R.openBlizzFS = openBlizzFS
    openBlizzBtn:SetScript("OnClick", function()
        ShowTransferInfoPopup()
    end)

    local curScroll = CreateFrame("ScrollFrame", nil, curOverlay, "UIPanelScrollFrameTemplate")
    curScroll:SetPoint("TOPLEFT", curOverlay, "TOPLEFT", 10, -50)
    curScroll:SetPoint("BOTTOMRIGHT", curOverlay, "BOTTOMRIGHT", -28, 10)
    local curContent = CreateFrame("Frame", nil, curScroll)
    curContent:SetWidth(1)
    curScroll:SetScrollChild(curContent)
    curScroll:SetScript("OnSizeChanged", function(self, w) if w > 0 then curContent:SetWidth(w) end end)
    if curScroll.ScrollBar then
        local sb = curScroll.ScrollBar
        if sb.ThumbTexture then sb.ThumbTexture:SetTexture(W8); sb.ThumbTexture:SetVertexColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.30); sb.ThumbTexture:SetWidth(4) end
        if sb.ScrollUpButton then sb.ScrollUpButton:SetAlpha(0) end
        if sb.ScrollDownButton then sb.ScrollDownButton:SetAlpha(0) end
    end
    R.curScroll = curScroll
    R.curContent = curContent

    local settingsPopup = CreateFrame("Frame", nil, p, "BackdropTemplate")
    settingsPopup:SetSize(180, 120)
    settingsPopup:SetPoint("TOPRIGHT", gearBtn, "BOTTOMRIGHT", 0, -4)
    settingsPopup:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    settingsPopup:SetBackdropColor(0.06, 0.06, 0.08, 0.95)
    settingsPopup:SetBackdropBorderColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.5)
    settingsPopup:SetFrameLevel(p:GetFrameLevel() + 20)
    settingsPopup:Hide()
    R.settingsPopup = settingsPopup

    local popupTitle = settingsPopup:CreateFontString(nil, "OVERLAY")
    TrySetFont(popupTitle, BODY_FONT, 10, "OUTLINE")
    popupTitle:SetPoint("TOP", settingsPopup, "TOP", 0, -8)
    popupTitle:SetText("Theme")
    popupTitle:SetTextColor(TC("mutedText"))

    local themeOptions = {
        { key = "parchment", label = "Warm Parchment" },
        { key = "midnight",  label = "Cool Midnight" },
        { key = "class",     label = "Class Color" },
    }
    for i, opt in ipairs(themeOptions) do
        local btn = CreateFrame("Button", nil, settingsPopup)
        btn:SetSize(160, 24)
        btn:SetPoint("TOP", settingsPopup, "TOP", 0, -24 - (i - 1) * 28)
        local lbl = btn:CreateFontString(nil, "OVERLAY")
        TrySetFont(lbl, BODY_FONT, 11, "")
        lbl:SetPoint("CENTER")
        lbl:SetText(opt.label)
        lbl:SetTextColor(TC("bodyText"))
        btn:SetScript("OnClick", function()
            local s = _G.MidnightUISettings
            if s and s.General then
                s.General.characterPanelTheme = opt.key
            end
            Panel.ApplyTheme()
            settingsPopup:Hide()
        end)
        btn:SetScript("OnEnter", function() lbl:SetTextColor(TC("titleText")) end)
        btn:SetScript("OnLeave", function() lbl:SetTextColor(TC("bodyText")) end)
    end

    p:SetScript("OnShow", function()
        local found = false
        for _, name in ipairs(UISpecialFrames) do
            if name == "MidnightUI_CharacterPanel" then found = true; break end
        end
        if not found then table.insert(UISpecialFrames, "MidnightUI_CharacterPanel") end
    end)
    p:SetScript("OnHide", function()
        Panel._state.panelOpen = false
        if R.settingsPopup then R.settingsPopup:Hide() end
    end)

    local fadeIn = p:CreateAnimationGroup()
    local fadeInAnim = fadeIn:CreateAnimation("Alpha")
    fadeInAnim:SetFromAlpha(0); fadeInAnim:SetToAlpha(1); fadeInAnim:SetDuration(0.20); fadeInAnim:SetSmoothing("OUT")
    p._fadeIn = fadeIn

    local fadeOut = p:CreateAnimationGroup()
    local fadeOutAnim = fadeOut:CreateAnimation("Alpha")
    fadeOutAnim:SetFromAlpha(1); fadeOutAnim:SetToAlpha(0); fadeOutAnim:SetDuration(0.15); fadeOutAnim:SetSmoothing("IN")
    fadeOut:SetScript("OnFinished", function() p:Hide(); p:SetAlpha(1) end)
    p._fadeOut = fadeOut

    return p
end

function Panel.UpdateModel()
    local R = Panel._refs
    if not R.charModel then return end
    R.charModel:ClearModel()
    R.charModel:SetUnit("player")
    pcall(R.charModel.SetAnimation, R.charModel, 0)
    pcall(R.charModel.SetPortraitZoom, R.charModel, 0)
    pcall(R.charModel.SetFacing, R.charModel, Panel._state.modelFacing)
    pcall(R.charModel.SetPosition, R.charModel, 0, 0, -0.15)
    pcall(R.charModel.SetCamDistanceScale, R.charModel, Panel._state.modelZoom)
    local L = CFG.modelLight
    pcall(R.charModel.SetLight, R.charModel,
        L[1], L[2], L[3], L[4], L[5], L[6], L[7], L[8], L[9], L[10], L[11], L[12], L[13])
end

function Panel.UpdateIdentity()
    local R = Panel._refs
    if not R.nameFS then return end

    local name = SafeCall(UnitName, "player") or "Unknown"
    local titleID = SafeCall(GetCurrentTitle) or 0
    if titleID > 0 and GetTitleName then
        local raw = SafeCall(GetTitleName, titleID) or ""
        raw = raw:gsub("^%s+", ""):gsub("%s+$", "")
        if raw ~= "" then
            if raw:sub(1,1) == raw:sub(1,1):lower() then
                R.nameFS:SetText(name .. " " .. raw)
            else
                R.nameFS:SetText(raw .. " " .. name)
            end
        else
            R.nameFS:SetText(name)
        end
    else
        R.nameFS:SetText(name)
    end

    local specName, className = "", ""
    local specIdx = SafeCall(GetSpecialization)
    if specIdx then
        local _, sName = SafeCall(GetSpecializationInfo, specIdx)
        specName = sName or ""
    end
    local _, classToken = SafeCall(UnitClass, "player")
    className = classToken and classToken:sub(1,1) .. classToken:sub(2):lower() or ""
    local localizedClass = SafeCall(UnitClass, "player") or className

    local level = SafeCall(UnitLevel, "player") or 0
    R.specFS:SetText(specName .. "  " .. localizedClass .. "  \194\183  Level " .. level)
end

function Panel.UpdateGearSlots()
    local R = Panel._refs
    if not R.gearSlots then return end
    for slotID, btn in pairs(R.gearSlots) do
        local tex = SafeCall(GetInventoryItemTexture, "player", slotID)
        if tex then
            btn._icon:SetTexture(tex)
            btn._icon:SetVertexColor(1, 1, 1, 1)
            local link = SafeCall(GetInventoryItemLink, "player", slotID)
            if link then
                local itemName, _, quality = SafeCall(GetItemInfo, link)
                local r, g, b = GetQualityColorRGB(quality or 1)
                for _, part in ipairs(btn._borderParts) do
                    part:SetColorTexture(r, g, b, 0.8)
                end
                if btn._itemName then
                    btn._itemName:SetText(itemName or "")
                    btn._itemName:SetTextColor(r, g, b)
                end
                if btn._slotLabel then
                    btn._slotLabel:SetTextColor(activeTheme.mutedText[1], activeTheme.mutedText[2], activeTheme.mutedText[3], 0.7)
                end
                local ilvl = nil
                if C_Item and C_Item.GetDetailedItemLevelInfo then
                    ilvl = SafeCall(C_Item.GetDetailedItemLevelInfo, link)
                end
                btn._ilvl:SetText(ilvl and tostring(ilvl) or "")
                btn._ilvl:SetTextColor(TC("mutedText"))
            end
        else
            btn._icon:SetTexture("Interface\\PaperDoll\\UI-Backpack-EmptySlot")
            btn._icon:SetVertexColor(1, 1, 1, 0.15)
            btn._icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
            for _, part in ipairs(btn._borderParts) do
                part:SetColorTexture(0.3, 0.3, 0.3, 0.1)
            end
            if btn._itemName then
                btn._itemName:SetText("Empty")
                btn._itemName:SetTextColor(activeTheme.mutedText[1], activeTheme.mutedText[2], activeTheme.mutedText[3], 0.3)
            end
            if btn._slotLabel then
                btn._slotLabel:SetTextColor(activeTheme.mutedText[1], activeTheme.mutedText[2], activeTheme.mutedText[3], 0.4)
            end
            btn._ilvl:SetText("")
        end
    end
end

function Panel.UpdateItemLevel()
    local R = Panel._refs
    if not R.ilvlValue then return end
    local equipped, overall = SafeCall(GetAverageItemLevel)
    R.ilvlValue:SetText(tostring(math.floor((equipped or 0) + 0.5)))
end

function Panel.UpdatePrimaryStats()
    local R = Panel._refs
    if not R.attrRows then return end
    local stats = GetPrimaryStats()
    local PAD = CFG.GLASS_PAD
    local SUB_INDENT = 12

    for i = 1, #R.attrRows do
        R.attrRows[i].name:Hide(); R.attrRows[i].value:Hide()
        for j = 1, #R.attrRows[i].subs do
            R.attrRows[i].subs[j].name:Hide(); R.attrRows[i].subs[j].value:Hide()
        end
    end

    local lastAnchor = R.attrLabel
    for i, data in ipairs(stats) do
        local row = R.attrRows[i]
        if not row then break end

        row.name:SetText(data.name)
        row.value:SetText(FormatNumber(data.value))
        row.name:ClearAllPoints()
        row.name:SetPoint("TOPLEFT", lastAnchor, "BOTTOMLEFT", 0, (i == 1) and -8 or -12)
        row.name:SetPoint("LEFT", R.rightGlass, "LEFT", PAD, 0)
        row.value:ClearAllPoints()
        row.value:SetPoint("RIGHT", R.rightGlass, "RIGHT", -PAD, 0)
        row.value:SetPoint("TOP", row.name, "TOP", 0, 0)
        row.name:Show(); row.value:Show()

        local subAnchor = row.name
        for j, sub in ipairs(data.subs or {}) do
            local subRow = row.subs[j]
            if not subRow then break end
            subRow.name:SetText(sub.label)
            subRow.value:SetText(sub.value)
            subRow.name:ClearAllPoints()
            subRow.name:SetPoint("TOPLEFT", subAnchor, "BOTTOMLEFT", 0, -3)
            subRow.name:SetPoint("LEFT", R.rightGlass, "LEFT", PAD + SUB_INDENT, 0)
            subRow.value:ClearAllPoints()
            subRow.value:SetPoint("RIGHT", R.rightGlass, "RIGHT", -PAD, 0)
            subRow.value:SetPoint("TOP", subRow.name, "TOP", 0, 0)
            subRow.name:Show(); subRow.value:Show()
            subAnchor = subRow.name
        end

        lastAnchor = subAnchor
    end

    if R.attrEnhSep then
        R.attrEnhSep:ClearAllPoints()
        R.attrEnhSep:SetPoint("LEFT", R.rightGlass, "LEFT", PAD, 0)
        R.attrEnhSep:SetPoint("RIGHT", R.rightGlass, "RIGHT", -PAD, 0)
        R.attrEnhSep:SetPoint("TOP", lastAnchor, "BOTTOM", 0, -14)
        R.attrEnhSep:Show()
    end
    if R.enhLabel then
        R.enhLabel:ClearAllPoints()
        R.enhLabel:SetPoint("TOPLEFT", R.attrEnhSep, "BOTTOMLEFT", 0, -10)
        R.enhLabel:Show()
    end
end

function Panel.UpdateEnhancements()
    local R = Panel._refs
    if not R.enhRows or not R.enhLabel then return end
    local stats = GetEnhancementStats()
    local PAD = CFG.GLASS_PAD

    for i = 1, #R.enhRows do
        R.enhRows[i].btn:Hide()
        R.enhRows[i].rating:Hide()
    end

    local lastAnchor = R.enhLabel
    local compactSepPlaced = false
    for i, data in ipairs(stats) do
        local row = R.enhRows[i]
        if not row then break end

        row.label:SetText(data.label)
        row.value:SetText(string.format("%.1f%%", data.value or 0))

        local topGap = -8
        if not data.detailed then
            if not compactSepPlaced then
                compactSepPlaced = true
                topGap = -14
            else
                topGap = -6
            end
        end

        if data.detailed then
            row.label:SetTextColor(TC("accent"))
            row.value:SetTextColor(TC("titleText"))
        else
            row.label:SetTextColor(0.60, 0.75, 0.85)
            row.value:SetTextColor(0.60, 0.75, 0.85)
        end

        row.btn:ClearAllPoints()
        row.btn:SetPoint("TOPLEFT", lastAnchor, "BOTTOMLEFT", 0, topGap)
        row.btn:SetPoint("LEFT", R.rightGlass, "LEFT", 0, 0)
        row.btn:SetPoint("RIGHT", R.rightGlass, "RIGHT", 0, 0)
        row.btn:Show()

        lastAnchor = row.btn

        if data.detailed then
            local rating = data.rating or 0
            if rating > 0 then
                row.rating:SetText(FormatNumber(rating) .. " rating")
                row.rating:ClearAllPoints()
                row.rating:SetPoint("TOPLEFT", row.label, "BOTTOMLEFT", 0, -2)
                row.rating:Show()
                row.btn:SetHeight(32)
                lastAnchor = row.btn
            else
                row.btn:SetHeight(20)
            end

            local tipTitle = data.label
            local tipPct = string.format("%.1f%%", data.value or 0)
            local tipRating = rating > 0 and (FormatNumber(rating) .. " rating") or nil
            local tipDesc = data.desc
            row.btn:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_LEFT")
                GameTooltip:SetText(tipTitle, activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3])
                GameTooltip:AddLine(tipPct, 1, 1, 1)
                if tipRating then
                    GameTooltip:AddLine(tipRating, 0.55, 0.85, 0.55)
                end
                if tipDesc then
                    GameTooltip:AddLine(" ")
                    GameTooltip:AddLine(tipDesc, TC("bodyText"))
                end
                GameTooltip:Show()
            end)
            row.btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
        else
            row.btn:SetHeight(20)
            local tipTitle = data.label
            local tipPct = string.format("%.1f%%", data.value or 0)
            local tipRating = (data.rating or 0) > 0 and (FormatNumber(data.rating) .. " rating") or nil
            local tipDesc = data.desc
            row.btn:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_LEFT")
                GameTooltip:SetText(tipTitle, activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3])
                GameTooltip:AddLine(tipPct, 1, 1, 1)
                if tipRating then
                    GameTooltip:AddLine(tipRating, 0.55, 0.85, 0.55)
                end
                if tipDesc then
                    GameTooltip:AddLine(" ")
                    GameTooltip:AddLine(tipDesc, TC("bodyText"))
                end
                GameTooltip:Show()
            end)
            row.btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
        end
    end
end

local repExpandedState = {}

-- Opens Blizzard's Renown panel for a Major Faction. Lazily loads
-- the Blizzard_MajorFactions addon, force-opens CharacterFrame >
-- Reputation, then locates the row in the ScrollBox data provider
-- and simulates a click on the rendered frame so Blizzard's own
-- selection logic renders the Renown detail view. ToggleMajorFactionRenown
-- is bugged on TWW/Midnight (indexes a nil MajorFactionRenownFrame),
-- so we deliberately avoid it. For non-Major-Faction reputations
-- this is a no-op.
function Panel.OpenRenownTrackFor(factionID)
    if not factionID then return end
    if not C_Reputation or not C_Reputation.IsMajorFaction then return end
    local okMF, isMF = pcall(C_Reputation.IsMajorFaction, factionID)
    if not (okMF and isMF) then return end

    if C_AddOns and C_AddOns.LoadAddOn then
        pcall(C_AddOns.LoadAddOn, "Blizzard_MajorFactions")
    elseif _G.UIParentLoadAddOn then
        pcall(_G.UIParentLoadAddOn, "Blizzard_MajorFactions")
    end

    local cf = _G.CharacterFrame
    local rf = _G.ReputationFrame
    if type(_G.ToggleCharacter) == "function" then
        local cfShown = cf and cf.IsShown and cf:IsShown()
        local rfShown = rf and rf.IsShown and rf:IsShown()
        if not (cfShown and rfShown) then
            pcall(_G.ToggleCharacter, "ReputationFrame")
        end
    end

    local sb = rf and rf.ScrollBox
    if not sb then return end

    local foundData
    if type(sb.GetDataProvider) == "function" then
        local okDP, dp = pcall(sb.GetDataProvider, sb)
        if okDP and type(dp) == "table" and type(dp.ForEach) == "function" then
            pcall(function()
                dp:ForEach(function(elementData)
                    if not foundData and type(elementData) == "table"
                        and elementData.factionID == factionID then
                        foundData = elementData
                    end
                end)
            end)
        end
    end

    if not foundData then return end

    -- Scroll to make the row visible (otherwise FindFrame can return
    -- nil if the row isn't currently rendered in the viewport).
    if type(sb.ScrollToElementData) == "function" then
        pcall(sb.ScrollToElementData, sb, foundData)
    end

    -- Two-step click chain. ScrollToElementData may need a frame
    -- to lay out before FindFrame resolves the row, and the row's
    -- click is what populates ReputationDetailFrame with this
    -- faction's data — only after that does ViewRenownButton
    -- have a target and become clickable.
    if not (C_Timer and C_Timer.After) then return end
    C_Timer.After(0, function()
        local view = (type(sb.GetView) == "function" and sb:GetView()) or sb.view
        if view and type(view.FindFrame) == "function" then
            local row = view:FindFrame(foundData)
            if row then
                if type(row.Click) == "function" then
                    pcall(row.Click, row, "LeftButton")
                elseif type(row.GetScript) == "function" then
                    local h = row:GetScript("OnClick")
                    if h then pcall(h, row, "LeftButton") end
                end
            end
        end
        C_Timer.After(0, function()
            local df = rf.ReputationDetailFrame
            local btn = df and df.ViewRenownButton
            if btn and (btn.IsShown and btn:IsShown()) then
                if type(btn.Click) == "function" then
                    pcall(btn.Click, btn, "LeftButton")
                elseif type(btn.GetScript) == "function" then
                    local h = btn:GetScript("OnClick")
                    if h then pcall(h, btn, "LeftButton") end
                end
            end
        end)
    end)
end

local repCtxMenu, repCtxOverlay

local function EnsureRepContextMenu()
    if repCtxMenu then return end
    local R = Panel._refs
    local parent = R.panel

    repCtxMenu = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    repCtxMenu:SetSize(170, 36)
    repCtxMenu:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    repCtxMenu:SetBackdropColor(TC("headerBg"))
    repCtxMenu:SetBackdropBorderColor(TC("accent"))
    repCtxMenu:SetFrameLevel(parent:GetFrameLevel() + 30)
    repCtxMenu:Hide()

    local btn = CreateFrame("Button", nil, repCtxMenu)
    btn:SetHeight(24)
    btn:SetPoint("TOPLEFT", repCtxMenu, "TOPLEFT", 1, -6)
    btn:SetPoint("TOPRIGHT", repCtxMenu, "TOPRIGHT", -1, -6)
    local hv = btn:CreateTexture(nil, "BACKGROUND")
    hv:SetAllPoints()
    hv:SetColorTexture(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.08)
    hv:Hide()
    local fs = btn:CreateFontString(nil, "OVERLAY")
    TrySetFont(fs, BODY_FONT, 11, "")
    fs:SetPoint("LEFT", btn, "LEFT", 10, 0)
    fs:SetTextColor(TC("bodyText"))
    btn:SetScript("OnEnter", function() hv:Show() end)
    btn:SetScript("OnLeave", function() hv:Hide() end)
    repCtxMenu._actionBtn = btn
    repCtxMenu._actionFS = fs

    repCtxOverlay = CreateFrame("Button", nil, parent)
    repCtxOverlay:SetAllPoints(UIParent)
    repCtxOverlay:SetFrameLevel(repCtxMenu:GetFrameLevel() - 1)
    repCtxOverlay:RegisterForClicks("AnyUp")
    repCtxOverlay:SetScript("OnClick", function()
        repCtxMenu:Hide(); repCtxOverlay:Hide()
    end)
    repCtxOverlay:Hide()

    repCtxMenu:HookScript("OnShow", function() repCtxOverlay:Show() end)
    repCtxMenu:HookScript("OnHide", function() repCtxOverlay:Hide() end)
end

function Panel.OpenRepContextMenu(anchorFrame, factionID, factionIndex, isWatched)
    if not factionIndex then return end
    EnsureRepContextMenu()

    local label = isWatched and "Stop Tracking" or "Track Reputation"
    repCtxMenu._actionFS:SetText(label)
    repCtxMenu._actionBtn:SetScript("OnClick", function()
        repCtxMenu:Hide()
        if C_Reputation and C_Reputation.SetWatchedFactionByIndex then
            if isWatched then
                pcall(C_Reputation.SetWatchedFactionByIndex, 0)
            else
                pcall(C_Reputation.SetWatchedFactionByIndex, factionIndex)
            end
            Panel.UpdateReputation()
        end
    end)

    repCtxMenu:ClearAllPoints()
    local cx, cy = GetCursorPosition()
    local scale = UIParent:GetEffectiveScale()
    repCtxMenu:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", cx / scale, cy / scale)
    repCtxMenu:Show()
end

function Panel.UpdateReputation()
    local R = Panel._refs
    if not R.repContent then return end

    local children = { R.repContent:GetChildren() }
    for _, child in ipairs(children) do child:Hide() end
    local regions = { R.repContent:GetRegions() }
    for _, region in ipairs(regions) do region:Hide() end

    local groups = CollectReputations()
    local PAD = 12
    local HDR_H = 32
    local ROW_H = 44
    local yOfs = 4

    local watchedFactionID
    if C_Reputation and C_Reputation.GetWatchedFactionData then
        local wd = C_Reputation.GetWatchedFactionData()
        if wd then watchedFactionID = wd.factionID end
    end

    for gi, group in ipairs(groups) do
        local groupKey = group.name
        if repExpandedState[groupKey] == nil then repExpandedState[groupKey] = false end
        local isExpanded = repExpandedState[groupKey]

        local hdrBtn = CreateFrame("Button", nil, R.repContent)
        hdrBtn:EnableMouse(true)
        hdrBtn:RegisterForClicks("LeftButtonUp")
        hdrBtn:SetHeight(HDR_H)
        hdrBtn:SetPoint("TOPLEFT", R.repContent, "TOPLEFT", 0, -yOfs)
        hdrBtn:SetPoint("TOPRIGHT", R.repContent, "TOPRIGHT", 0, -yOfs)

        local hdrBg = hdrBtn:CreateTexture(nil, "BACKGROUND")
        hdrBg:SetAllPoints()
        hdrBg:SetColorTexture(0.12, 0.10, 0.08, 0.35)

        local chevron = hdrBtn:CreateTexture(nil, "OVERLAY")
        chevron:SetSize(10, 10)
        chevron:SetPoint("LEFT", hdrBtn, "LEFT", PAD, 0)
        chevron:SetAtlas("common-dropdown-icon-back")
        if isExpanded then
            chevron:SetRotation(-math.pi / 2)
        else
            chevron:SetRotation(math.pi)
        end
        chevron:SetVertexColor(0.30, 0.85, 0.40)

        local hdrName = hdrBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(hdrName, BODY_FONT, 11, "OUTLINE")
        hdrName:SetPoint("LEFT", chevron, "RIGHT", 6, 0)
        hdrName:SetText(group.name)
        hdrName:SetTextColor(TC("accent"))

        local countFS = hdrBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(countFS, BODY_FONT, 10, "")
        countFS:SetPoint("RIGHT", hdrBtn, "RIGHT", -PAD, 0)
        countFS:SetText(#group.factions)
        countFS:SetTextColor(TC("mutedText"))

        local hdrHover = hdrBtn:CreateTexture(nil, "BACKGROUND", nil, 1)
        hdrHover:SetAllPoints()
        hdrHover:SetColorTexture(0.18, 0.15, 0.12, 0.3)
        hdrHover:Hide()
        hdrBtn:SetScript("OnEnter", function() hdrHover:Show() end)
        hdrBtn:SetScript("OnLeave", function() hdrHover:Hide() end)

        local hdrSep = hdrBtn:CreateTexture(nil, "OVERLAY")
        hdrSep:SetHeight(1)
        hdrSep:SetPoint("BOTTOMLEFT", hdrBtn, "BOTTOMLEFT", PAD, 0)
        hdrSep:SetPoint("BOTTOMRIGHT", hdrBtn, "BOTTOMRIGHT", -PAD, 0)
        hdrSep:SetColorTexture(activeTheme.divider[1], activeTheme.divider[2], activeTheme.divider[3], 0.15)

        local capturedKey = groupKey
        hdrBtn:SetScript("OnClick", function()
            repExpandedState[capturedKey] = not repExpandedState[capturedKey]
            Panel.UpdateReputation()
        end)
        hdrBtn:Show()
        yOfs = yOfs + HDR_H

        if isExpanded then
            for fi, faction in ipairs(group.factions) do
                local isWatched = (faction.factionID and faction.factionID == watchedFactionID)

                local rowBtn = CreateFrame("Button", nil, R.repContent)
                rowBtn:EnableMouse(true)
                rowBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
                rowBtn:SetHeight(ROW_H)
                rowBtn:SetPoint("TOPLEFT", R.repContent, "TOPLEFT", 0, -yOfs)
                rowBtn:SetPoint("TOPRIGHT", R.repContent, "TOPRIGHT", 0, -yOfs)

                local rowHover = rowBtn:CreateTexture(nil, "BACKGROUND")
                rowHover:SetAllPoints()
                rowHover:SetColorTexture(1, 1, 1, 0.03)
                rowHover:Hide()

                if isWatched then
                    local watchedBg = rowBtn:CreateTexture(nil, "BACKGROUND", nil, -1)
                    watchedBg:SetAllPoints()
                    watchedBg:SetColorTexture(faction.standingColor[1], faction.standingColor[2], faction.standingColor[3], 0.06)
                end

                local accentBar = rowBtn:CreateTexture(nil, "OVERLAY")
                accentBar:SetWidth(3)
                accentBar:SetPoint("TOPLEFT", rowBtn, "TOPLEFT", 0, -4)
                accentBar:SetPoint("BOTTOMLEFT", rowBtn, "BOTTOMLEFT", 0, 4)
                accentBar:SetColorTexture(faction.standingColor[1], faction.standingColor[2], faction.standingColor[3], isWatched and 1 or 0.7)

                local nameFS = rowBtn:CreateFontString(nil, "OVERLAY")
                TrySetFont(nameFS, BODY_FONT, 11, "")
                nameFS:SetPoint("TOPLEFT", rowBtn, "TOPLEFT", PAD + 8, -6)
                nameFS:SetPoint("RIGHT", rowBtn, "RIGHT", -80, 0)
                nameFS:SetJustifyH("LEFT")
                nameFS:SetWordWrap(false)
                nameFS:SetText(faction.name)
                nameFS:SetTextColor(TC("bodyText"))

                local standingFS = rowBtn:CreateFontString(nil, "OVERLAY")
                TrySetFont(standingFS, BODY_FONT, 10, "")
                standingFS:SetPoint("TOPRIGHT", rowBtn, "TOPRIGHT", -PAD, -6)
                standingFS:SetText(faction.standingName)
                standingFS:SetTextColor(faction.standingColor[1], faction.standingColor[2], faction.standingColor[3])

                local repTextFS = rowBtn:CreateFontString(nil, "OVERLAY")
                TrySetFont(repTextFS, BODY_FONT, 10, "")
                repTextFS:SetPoint("BOTTOMLEFT", rowBtn, "BOTTOMLEFT", PAD + 8, 10)
                local repText = FormatNumber(faction.currentRep) .. " / " .. FormatNumber(faction.maxRep)
                repTextFS:SetText(repText)
                repTextFS:SetTextColor(TC("mutedText"))

                local track = rowBtn:CreateTexture(nil, "ARTWORK")
                track:SetHeight(3)
                track:SetPoint("BOTTOMLEFT", rowBtn, "BOTTOMLEFT", PAD + 8, 4)
                track:SetPoint("BOTTOMRIGHT", rowBtn, "BOTTOMRIGHT", -PAD, 4)
                track:SetColorTexture(0.12, 0.12, 0.14, 0.6)

                local fill = rowBtn:CreateTexture(nil, "ARTWORK", nil, 1)
                fill:SetHeight(3)
                fill:SetPoint("TOPLEFT", track, "TOPLEFT", 0, 0)
                fill:SetWidth(math.max(1, 1))
                fill:SetColorTexture(faction.standingColor[1], faction.standingColor[2], faction.standingColor[3], 0.8)

                local capturedProgress = faction.progress
                local function UpdateFillWidth()
                    local w = track:GetWidth()
                    if w > 0 then fill:SetWidth(math.max(1, w * capturedProgress)) end
                end
                rowBtn:SetScript("OnSizeChanged", UpdateFillWidth)

                local capturedFactionIndex = faction.factionIndex
                local capturedFactionID = faction.factionID
                rowBtn:SetScript("OnClick", function(self, btn)
                    if btn == "RightButton" and capturedFactionIndex then
                        Panel.OpenRepContextMenu(self, capturedFactionID, capturedFactionIndex, isWatched)
                    elseif btn == "LeftButton" and capturedFactionID then
                        Panel.OpenRenownTrackFor(capturedFactionID)
                    end
                end)

                rowBtn:SetScript("OnEnter", function() rowHover:Show() end)
                rowBtn:SetScript("OnLeave", function() rowHover:Hide() end)

                rowBtn:Show()
                UpdateFillWidth()
                if C_Timer then C_Timer.After(0, UpdateFillWidth) end
                yOfs = yOfs + ROW_H
            end
        end
    end

    R.repContent:SetHeight(math.max(yOfs + 20, 1))
end

local curExpandedState = {}


local transferInfoPopup = nil

function ShowTransferInfoPopup()
    if transferInfoPopup then
        transferInfoPopup:Show()
        return
    end

    local R = Panel._refs
    local parent = R and R.panel or UIParent
    local PAD = 20

    local f = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    f:SetSize(420, 340)
    f:SetPoint("CENTER", parent, "CENTER", 0, 0)
    f:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    f:SetBackdropColor(TC("frameBg"))
    f:SetBackdropBorderColor(TC("accent"))
    f:SetFrameLevel(parent:GetFrameLevel() + 40)
    f:EnableMouse(true)

    local closeBtn = CreateFrame("Button", nil, f)
    closeBtn:SetSize(20, 20)
    closeBtn:SetPoint("TOPRIGHT", -8, -8)
    closeBtn:SetFrameLevel(f:GetFrameLevel() + 2)
    local closeFS = closeBtn:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    closeFS:SetPoint("CENTER")
    closeFS:SetText("\195\151")
    closeFS:SetTextColor(0.5, 0.5, 0.5)
    closeBtn:SetScript("OnEnter", function() closeFS:SetTextColor(1, 0.2, 0.2) end)
    closeBtn:SetScript("OnLeave", function() closeFS:SetTextColor(0.5, 0.5, 0.5) end)
    closeBtn:SetScript("OnClick", function() f:Hide() end)

    local warbandIcon = f:CreateTexture(nil, "ARTWORK")
    warbandIcon:SetSize(22, 22)
    warbandIcon:SetPoint("TOPLEFT", PAD, -16)
    warbandIcon:SetAtlas("warbands-transferable-icon", false)
    warbandIcon:SetSize(22, 22)

    local title = f:CreateFontString(nil, "OVERLAY")
    TrySetFont(title, TITLE_FONT, 14, "OUTLINE")
    title:SetPoint("LEFT", warbandIcon, "RIGHT", 8, 0)
    title:SetText("Warband Currency Transfer")
    title:SetTextColor(TC("titleText"))

    local div1 = f:CreateTexture(nil, "ARTWORK")
    div1:SetHeight(1)
    div1:SetPoint("TOPLEFT", PAD, -44)
    div1:SetPoint("TOPRIGHT", -PAD, -44)
    div1:SetColorTexture(TC("divider"))

    local whyLabel = f:CreateFontString(nil, "OVERLAY")
    TrySetFont(whyLabel, BODY_FONT, 10, "OUTLINE")
    whyLabel:SetPoint("TOPLEFT", PAD, -54)
    whyLabel:SetText("WHY CAN'T I TRANSFER HERE?")
    whyLabel:SetTextColor(TC("mutedText"))

    local whyBody = f:CreateFontString(nil, "OVERLAY")
    TrySetFont(whyBody, BODY_FONT, 11, "")
    whyBody:SetPoint("TOPLEFT", PAD, -70)
    whyBody:SetPoint("TOPRIGHT", -PAD, -70)
    whyBody:SetJustifyH("LEFT")
    whyBody:SetSpacing(2)
    whyBody:SetText(
        "Blizzard restricts currency transfers to their own " ..
        "protected UI. No addon can perform this action " ..
        "without triggering a security block."
    )
    whyBody:SetTextColor(TC("bodyText"))

    local div2 = f:CreateTexture(nil, "ARTWORK")
    div2:SetHeight(1)
    div2:SetPoint("TOPLEFT", PAD, -120)
    div2:SetPoint("TOPRIGHT", -PAD, -120)
    div2:SetColorTexture(TC("divider"))

    local stepsLabel = f:CreateFontString(nil, "OVERLAY")
    TrySetFont(stepsLabel, BODY_FONT, 10, "OUTLINE")
    stepsLabel:SetPoint("TOPLEFT", PAD, -130)
    stepsLabel:SetText("HOW TO TRANSFER")
    stepsLabel:SetTextColor(TC("mutedText"))

    local steps = {
        { num = "1", text = "Open the addon list and disable |cff88ff88MidnightUI|r" },
        { num = "2", text = "Type |cff88ff88/reload|r in chat" },
        { num = "3", text = "Press |cff88ff88C|r and click the |cff88ff88Currency|r tab" },
        { num = "4", text = "Click the transfer icon on your currency" },
        { num = "5", text = "Re-enable MidnightUI and type |cff88ff88/reload|r" },
    }
    local stepY = -148
    for _, step in ipairs(steps) do
        local numFS = f:CreateFontString(nil, "OVERLAY")
        TrySetFont(numFS, BODY_FONT, 11, "")
        numFS:SetPoint("TOPLEFT", PAD + 4, stepY)
        numFS:SetText("|cff" .. string.format("%02x%02x%02x",
            activeTheme.accent[1] * 255,
            activeTheme.accent[2] * 255,
            activeTheme.accent[3] * 255) .. step.num .. ".|r")

        local textFS = f:CreateFontString(nil, "OVERLAY")
        TrySetFont(textFS, BODY_FONT, 11, "")
        textFS:SetPoint("TOPLEFT", PAD + 24, stepY)
        textFS:SetPoint("TOPRIGHT", -PAD, stepY)
        textFS:SetJustifyH("LEFT")
        textFS:SetText(step.text)
        textFS:SetTextColor(TC("bodyText"))

        stepY = stepY - 20
    end

    local div3 = f:CreateTexture(nil, "ARTWORK")
    div3:SetHeight(1)
    div3:SetPoint("TOPLEFT", PAD, stepY - 8)
    div3:SetPoint("TOPRIGHT", -PAD, stepY - 8)
    div3:SetColorTexture(TC("divider"))

    local footer = f:CreateFontString(nil, "OVERLAY")
    TrySetFont(footer, BODY_FONT, 10, "")
    footer:SetPoint("TOPLEFT", PAD, stepY - 18)
    footer:SetPoint("TOPRIGHT", -PAD, stepY - 18)
    footer:SetJustifyH("LEFT")
    footer:SetSpacing(2)
    footer:SetText("MidnightUI's Currency tab shows your balances and marks " ..
        "which currencies are transferable across your Warband.")
    footer:SetTextColor(TC("mutedText"))

    local btn = CreateFrame("Button", nil, f, "BackdropTemplate")
    btn:SetSize(100, 26)
    btn:SetPoint("BOTTOM", 0, 14)
    btn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1 })
    btn:SetBackdropColor(TC("headerBg"))
    btn:SetBackdropBorderColor(TC("accent"))
    local btnFS = btn:CreateFontString(nil, "OVERLAY")
    TrySetFont(btnFS, BODY_FONT, 11, "")
    btnFS:SetPoint("CENTER")
    btnFS:SetText("Got it")
    btnFS:SetTextColor(TC("accent"))
    btn:SetScript("OnEnter", function()
        btn:SetBackdropColor(activeTheme.accent[1] * 0.2, activeTheme.accent[2] * 0.2, activeTheme.accent[3] * 0.2, 0.9)
        btnFS:SetTextColor(TC("titleText"))
    end)
    btn:SetScript("OnLeave", function()
        btn:SetBackdropColor(TC("headerBg"))
        btnFS:SetTextColor(TC("accent"))
    end)
    btn:SetScript("OnClick", function() f:Hide() end)

    transferInfoPopup = f
    f:Show()
end


local transferDialog = nil
local transferCharButtons = {}

local function EnsureTransferDialog()
    if transferDialog then return transferDialog end
    local R = Panel._refs
    local parent = R.panel

    local dlg = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    dlg:SetSize(320, 340)
    dlg:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    dlg:SetBackdropColor(TC("frameBg"))
    dlg:SetBackdropBorderColor(TC("accent"))
    dlg:SetFrameLevel(parent:GetFrameLevel() + 30)
    dlg:SetClampedToScreen(true)
    dlg:Hide()

    local titleFS = dlg:CreateFontString(nil, "OVERLAY")
    TrySetFont(titleFS, TITLE_FONT, 13, "")
    titleFS:SetPoint("TOPLEFT", 12, -10)
    titleFS:SetTextColor(TC("titleText"))
    dlg._title = titleFS

    local curIcon = dlg:CreateTexture(nil, "ARTWORK")
    curIcon:SetSize(20, 20)
    curIcon:SetPoint("TOPLEFT", 12, -32)
    curIcon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
    dlg._curIcon = curIcon

    local curName = dlg:CreateFontString(nil, "OVERLAY")
    TrySetFont(curName, BODY_FONT, 11, "")
    curName:SetPoint("LEFT", curIcon, "RIGHT", 6, 0)
    curName:SetTextColor(TC("bodyText"))
    dlg._curName = curName

    local div = dlg:CreateTexture(nil, "ARTWORK")
    div:SetHeight(1)
    div:SetPoint("TOPLEFT", 8, -58)
    div:SetPoint("TOPRIGHT", -8, -58)
    div:SetColorTexture(TC("divider"))

    local selectLabel = dlg:CreateFontString(nil, "OVERLAY")
    TrySetFont(selectLabel, BODY_FONT, 10, "")
    selectLabel:SetPoint("TOPLEFT", 12, -66)
    selectLabel:SetText("Select a character:")
    selectLabel:SetTextColor(TC("mutedText"))

    local listArea = CreateFrame("Frame", nil, dlg)
    listArea:SetPoint("TOPLEFT", 8, -82)
    listArea:SetPoint("BOTTOMRIGHT", -8, 48)
    dlg._listArea = listArea

    local statusFS = dlg:CreateFontString(nil, "OVERLAY")
    TrySetFont(statusFS, BODY_FONT, 10, "")
    statusFS:SetPoint("CENTER", listArea, "CENTER")
    statusFS:SetTextColor(TC("mutedText"))
    dlg._status = statusFS

    local closeBtn = CreateFrame("Button", nil, dlg)
    closeBtn:SetSize(20, 20)
    closeBtn:SetPoint("TOPRIGHT", -6, -6)
    closeBtn:SetFrameLevel(dlg:GetFrameLevel() + 2)
    local closeFS = closeBtn:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    closeFS:SetPoint("CENTER")
    closeFS:SetText("\195\151")
    closeFS:SetTextColor(0.5, 0.5, 0.5)
    closeBtn:SetScript("OnEnter", function() closeFS:SetTextColor(1, 0.2, 0.2) end)
    closeBtn:SetScript("OnLeave", function() closeFS:SetTextColor(0.5, 0.5, 0.5) end)
    closeBtn:SetScript("OnClick", function() dlg:Hide() end)

    local xferBtn = CreateFrame("Button", nil, dlg, "BackdropTemplate")
    xferBtn:SetSize(200, 28)
    xferBtn:SetPoint("BOTTOM", dlg, "BOTTOM", 0, 12)
    xferBtn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1 })
    xferBtn:SetBackdropColor(0.12, 0.20, 0.35, 0.9)
    xferBtn:SetBackdropBorderColor(0.25, 0.45, 0.7, 1)
    local xferFS = xferBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(xferFS, BODY_FONT, 11, "")
    xferFS:SetPoint("CENTER")
    xferFS:SetText("How to Transfer")
    xferFS:SetTextColor(0.5, 0.75, 1.0)
    xferBtn:SetScript("OnEnter", function()
        xferBtn:SetBackdropColor(0.18, 0.30, 0.50, 1)
        xferFS:SetTextColor(1, 1, 1)
    end)
    xferBtn:SetScript("OnLeave", function()
        xferBtn:SetBackdropColor(0.12, 0.20, 0.35, 0.9)
        xferFS:SetTextColor(0.5, 0.75, 1.0)
    end)
    xferBtn:SetScript("OnClick", function()
        dlg:Hide()
        ShowTransferInfoPopup()
    end)
    xferBtn:Hide()
    dlg._xferBtn = xferBtn
    dlg._xferFS = xferFS

    transferDialog = dlg
    return dlg
end

local function PopulateTransferDialog(currencyID, currencyName, iconFileID)
    local dlg = EnsureTransferDialog()
    dlg._currencyID = currencyID
    dlg._selectedGUID = nil

    dlg._title:SetText("Warband Currency")
    dlg._curName:SetText(currencyName or "Unknown")
    if iconFileID then
        dlg._curIcon:SetTexture(iconFileID)
        dlg._curIcon:Show()
    else
        dlg._curIcon:Hide()
    end

    for _, btn in ipairs(transferCharButtons) do btn:Hide() end

    if C_CurrencyInfo.RequestCurrencyDataForAccountCharacters then
        C_CurrencyInfo.RequestCurrencyDataForAccountCharacters()
    end

    local charData = {}
    if C_CurrencyInfo.FetchCurrencyDataFromAccountCharacters then
        local ok, data = pcall(C_CurrencyInfo.FetchCurrencyDataFromAccountCharacters, currencyID)
        if ok and data then charData = data end
    end



    if #charData == 0 then
        dlg._status:SetText("No other characters have this currency.\nTry again in a moment.")
        dlg._status:Show()
    else
        dlg._status:Hide()

        local yOff = 0
        for i, entry in ipairs(charData) do
            if entry.quantity and entry.quantity > 0 then
                local btn = transferCharButtons[i]
                if not btn then
                    btn = CreateFrame("Button", nil, dlg._listArea)
                    btn:SetHeight(28)
                    local hv = btn:CreateTexture(nil, "BACKGROUND")
                    hv:SetAllPoints()
                    hv:SetColorTexture(1, 1, 1, 0.03)
                    hv:Hide()
                    btn._hover = hv

                    local sel = btn:CreateTexture(nil, "BACKGROUND")
                    sel:SetAllPoints()
                    sel:SetColorTexture(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.15)
                    sel:Hide()
                    btn._selected = sel

                    btn._nameFS = btn:CreateFontString(nil, "OVERLAY")
                    TrySetFont(btn._nameFS, BODY_FONT, 11, "")
                    btn._nameFS:SetPoint("LEFT", 8, 0)

                    btn._qtyFS = btn:CreateFontString(nil, "OVERLAY")
                    TrySetFont(btn._qtyFS, BODY_FONT, 11, "")
                    btn._qtyFS:SetPoint("RIGHT", -8, 0)
                    btn._qtyFS:SetJustifyH("RIGHT")

                    btn:SetScript("OnEnter", function(self) self._hover:Show() end)
                    btn:SetScript("OnLeave", function(self) self._hover:Hide() end)

                    transferCharButtons[i] = btn
                end

                btn:SetPoint("TOPLEFT", dlg._listArea, "TOPLEFT", 0, -yOff)
                btn:SetPoint("TOPRIGHT", dlg._listArea, "TOPRIGHT", 0, -yOff)

                local charName = entry.characterName or entry.fullCharacterName or "Unknown"
                btn._nameFS:SetText(charName)
                btn._nameFS:SetTextColor(TC("bodyText"))
                btn._qtyFS:SetText(FormatNumber(entry.quantity))
                btn._qtyFS:SetTextColor(TC("titleText"))

                local guid = entry.characterGUID
                local qty = entry.quantity
                btn:SetScript("OnClick", function(self)
                    for _, b in ipairs(transferCharButtons) do
                        if b._selected then b._selected:Hide() end
                    end
                    self._selected:Show()
                    dlg._selectedGUID = guid
                    dlg._selectedQty = qty
                    dlg._selectedName = charName
                    dlg._xferFS:SetText("Close & Press C to Transfer")
                end)

                btn:Show()
                yOff = yOff + 28
            end
        end

        dlg._xferBtn:Show()
        dlg._xferFS:SetText("Select a character above")
    end

    dlg:ClearAllPoints()
    local parent = Panel._refs and Panel._refs.panel
    if parent then
        dlg:SetPoint("CENTER", parent, "CENTER", 0, 0)
    else
        dlg:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
    end
    dlg:Show()
end

local transferDataListener = CreateFrame("Frame")
transferDataListener:RegisterEvent("ACCOUNT_CHARACTER_CURRENCY_DATA_RECEIVED")
transferDataListener:SetScript("OnEvent", function()
    if transferDialog and transferDialog:IsShown() and transferDialog._currencyID then
        local curID = transferDialog._currencyID
        local curName = transferDialog._curName and transferDialog._curName:GetText()
        local iconID = transferDialog._curIcon and transferDialog._curIcon:GetTexture()
        PopulateTransferDialog(curID, curName, iconID)
    end
end)

function Panel.UpdateCurrency()
    local R = Panel._refs
    if not R.curContent then return end

    local children = { R.curContent:GetChildren() }
    for _, child in ipairs(children) do child:Hide() end
    local regions = { R.curContent:GetRegions() }
    for _, region in ipairs(regions) do region:Hide() end

    local groups = CollectCurrencies()



    local PAD = 12
    local HDR_H = 32
    local ROW_H = 32
    local yOfs = 4

    for _, group in ipairs(groups) do
        local groupKey = group.name
        if curExpandedState[groupKey] == nil then curExpandedState[groupKey] = false end
        local isExpanded = curExpandedState[groupKey]

        local hdrBtn = CreateFrame("Button", nil, R.curContent)
        hdrBtn:EnableMouse(true)
        hdrBtn:RegisterForClicks("LeftButtonUp")
        hdrBtn:SetHeight(HDR_H)
        hdrBtn:SetPoint("TOPLEFT", R.curContent, "TOPLEFT", 0, -yOfs)
        hdrBtn:SetPoint("TOPRIGHT", R.curContent, "TOPRIGHT", 0, -yOfs)

        local hdrBg = hdrBtn:CreateTexture(nil, "BACKGROUND")
        hdrBg:SetAllPoints()
        hdrBg:SetColorTexture(0.12, 0.10, 0.08, 0.35)

        local chevron = hdrBtn:CreateTexture(nil, "OVERLAY")
        chevron:SetSize(10, 10)
        chevron:SetPoint("LEFT", hdrBtn, "LEFT", PAD, 0)
        chevron:SetAtlas("common-dropdown-icon-back")
        if isExpanded then
            chevron:SetRotation(-math.pi / 2)
        else
            chevron:SetRotation(math.pi)
        end
        chevron:SetVertexColor(0.30, 0.85, 0.40)

        local hdrName = hdrBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(hdrName, BODY_FONT, 11, "OUTLINE")
        hdrName:SetPoint("LEFT", chevron, "RIGHT", 6, 0)
        hdrName:SetText(group.name)
        hdrName:SetTextColor(TC("accent"))

        local countFS = hdrBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(countFS, BODY_FONT, 10, "")
        countFS:SetPoint("RIGHT", hdrBtn, "RIGHT", -PAD, 0)
        countFS:SetText(#group.currencies)
        countFS:SetTextColor(TC("mutedText"))

        local hdrHover = hdrBtn:CreateTexture(nil, "BACKGROUND", nil, 1)
        hdrHover:SetAllPoints()
        hdrHover:SetColorTexture(0.18, 0.15, 0.12, 0.3)
        hdrHover:Hide()
        hdrBtn:SetScript("OnEnter", function() hdrHover:Show() end)
        hdrBtn:SetScript("OnLeave", function() hdrHover:Hide() end)

        local hdrSep = hdrBtn:CreateTexture(nil, "OVERLAY")
        hdrSep:SetHeight(1)
        hdrSep:SetPoint("BOTTOMLEFT", hdrBtn, "BOTTOMLEFT", PAD, 0)
        hdrSep:SetPoint("BOTTOMRIGHT", hdrBtn, "BOTTOMRIGHT", -PAD, 0)
        hdrSep:SetColorTexture(activeTheme.divider[1], activeTheme.divider[2], activeTheme.divider[3], 0.15)

        local capturedKey = groupKey
        hdrBtn:SetScript("OnClick", function()
            curExpandedState[capturedKey] = not curExpandedState[capturedKey]
            Panel.UpdateCurrency()
        end)
        hdrBtn:Show()
        yOfs = yOfs + HDR_H

        if isExpanded then
            for _, cur in ipairs(group.currencies) do
                local rowBtn = CreateFrame("Button", nil, R.curContent)
                rowBtn:SetHeight(ROW_H)
                rowBtn:SetPoint("TOPLEFT", R.curContent, "TOPLEFT", 0, -yOfs)
                rowBtn:SetPoint("TOPRIGHT", R.curContent, "TOPRIGHT", 0, -yOfs)

                local rowHover = rowBtn:CreateTexture(nil, "BACKGROUND")
                rowHover:SetAllPoints()
                rowHover:SetColorTexture(1, 1, 1, 0.03)
                rowHover:Hide()

                if cur.iconFileID then
                    local icon = rowBtn:CreateTexture(nil, "ARTWORK")
                    icon:SetSize(20, 20)
                    icon:SetPoint("LEFT", rowBtn, "LEFT", PAD + 8, 0)
                    icon:SetTexture(cur.iconFileID)
                    icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
                end

                local nameFS = rowBtn:CreateFontString(nil, "OVERLAY")
                TrySetFont(nameFS, BODY_FONT, 11, "")
                nameFS:SetPoint("LEFT", rowBtn, "LEFT", PAD + 34, 0)
                nameFS:SetPoint("RIGHT", rowBtn, "RIGHT", -80, 0)
                nameFS:SetJustifyH("LEFT")
                nameFS:SetWordWrap(false)
                nameFS:SetText(cur.name)
                nameFS:SetTextColor(TC("bodyText"))

                local transferIcon
                if cur.isTransferable then
                    transferIcon = rowBtn:CreateTexture(nil, "OVERLAY")
                    transferIcon:SetPoint("RIGHT", rowBtn, "RIGHT", -(PAD + 2), 0)
                    transferIcon:SetAtlas("warbands-transferable-icon", false)
                    transferIcon:SetSize(18, 18)
                end

                local qtyFS = rowBtn:CreateFontString(nil, "OVERLAY")
                TrySetFont(qtyFS, BODY_FONT, 11, "")
                if transferIcon then
                    qtyFS:SetPoint("RIGHT", transferIcon, "LEFT", -6, 0)
                else
                    qtyFS:SetPoint("RIGHT", rowBtn, "RIGHT", -PAD, 0)
                end
                qtyFS:SetJustifyH("RIGHT")
                local qtyText = FormatNumber(cur.quantity)
                if cur.maxQuantity and cur.maxQuantity > 0 then
                    qtyText = qtyText .. " / " .. FormatNumber(cur.maxQuantity)
                end
                qtyFS:SetText(qtyText)
                qtyFS:SetTextColor(TC("titleText"))

                local capturedCurID = cur.currencyID
                rowBtn:SetScript("OnEnter", function(self)
                    rowHover:Show()
                    GameTooltip:SetOwner(self, "ANCHOR_LEFT")
                    if capturedCurID and GameTooltip.SetCurrencyByID then
                        pcall(GameTooltip.SetCurrencyByID, GameTooltip, capturedCurID)
                    else
                        GameTooltip:SetText(cur.name, 1, 1, 1)
                        GameTooltip:AddLine(qtyText, TC("titleText"))
                    end
                    GameTooltip:Show()
                end)
                rowBtn:SetScript("OnLeave", function() rowHover:Hide(); GameTooltip:Hide() end)

                rowBtn:Show()
                yOfs = yOfs + ROW_H
            end
        end
    end

    R.curContent:SetHeight(math.max(yOfs + 20, 1))
end

function Panel.SetActiveTab(key)
    Panel._state.activeTab = key
    local R = Panel._refs
    if not R.tabButtons then return end

    for k, btn in pairs(R.tabButtons) do
        if k == key then
            btn._label:SetTextColor(TC("tabActive"))
            btn._underline:Show()
        else
            btn._label:SetTextColor(TC("tabInactive"))
            btn._underline:Hide()
        end
    end

    if R.repOverlay then R.repOverlay:Hide() end
    if R.curOverlay then R.curOverlay:Hide() end

    local showChar = (key == "character")
    if R.modelArea then R.modelArea:SetShown(showChar) end
    if R.leftGlass then R.leftGlass:SetShown(showChar) end
    if R.rightGlass then R.rightGlass:SetShown(showChar) end
    if R.identityBar then R.identityBar:SetShown(showChar) end

    if key == "character" then
    elseif key == "reputation" then
        Panel.UpdateReputation()
        R.repOverlay:Show()
    elseif key == "currency" then
        Panel.UpdateCurrency()
        R.curOverlay:Show()
    end
end

function Panel.Show(tab)
    local p = Panel.EnsurePanel()
    if not p then return end

    local gfHide = _G.MidnightUI_GroupFinder_Hide
    if gfHide then gfHide() end
    local guildFrame = _G["MidnightUI_GuildPanel"]
    if guildFrame and guildFrame:IsShown() then guildFrame:Hide() end

    GetActiveTheme()
    Panel._state.panelOpen = true
    p:Show()
    if p._fadeIn then p._fadeIn:Play() end

    AttachBlizzardSlotButtons()

    Panel.UpdateModel()
    Panel.UpdateIdentity()
    Panel.UpdateGearSlots()
    Panel.UpdateItemLevel()
    Panel.UpdatePrimaryStats()
    Panel.UpdateEnhancements()
    Panel.SetActiveTab(tab or "character")
end

function Panel.Hide()
    local R = Panel._refs
    if not R.panel then return end
    if R.panel._fadeOut then
        R.panel._fadeOut:Play()
    else
        R.panel:Hide()
    end
    Panel._state.panelOpen = false
end

function Panel.Toggle(tab)
    if Panel._state.panelOpen then
        if tab and tab ~= Panel._state.activeTab then
            Panel.SetActiveTab(tab)
        else
            Panel.Hide()
        end
    else
        Panel.Show(tab)
    end
end

function Panel.IsOpen()
    return Panel._state.panelOpen
end

local equipDropdown = nil

function Panel.ToggleEquipmentManager()
    local R = Panel._refs
    if not R.panel then return end

    if equipDropdown and equipDropdown:IsShown() then
        equipDropdown:Hide()
        return
    end

    if not equipDropdown then
        local dd = CreateFrame("Frame", nil, R.panel, "BackdropTemplate")
        dd:SetWidth(280)
        dd:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        dd:SetBackdropColor(0.04, 0.04, 0.06, 0.96)
        dd:SetBackdropBorderColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.35)
        dd:SetFrameLevel(R.panel:GetFrameLevel() + 25)
        dd:Hide()

        for i = 1, 4 do
            local s = dd:CreateTexture(nil, "BACKGROUND", nil, -1)
            local off = i * 1.2
            local a = 0.12 - (i * 0.025)
            s:SetColorTexture(0, 0, 0, a)
            s:SetPoint("TOPLEFT", dd, "TOPLEFT", -off, off)
            s:SetPoint("BOTTOMRIGHT", dd, "BOTTOMRIGHT", off, -off)
        end

        local frost = dd:CreateTexture(nil, "OVERLAY", nil, 3)
        frost:SetHeight(1)
        frost:SetPoint("TOPLEFT", dd, "TOPLEFT", 1, -1)
        frost:SetPoint("TOPRIGHT", dd, "TOPRIGHT", -1, -1)
        frost:SetColorTexture(1, 1, 1, 0.08)

        local topGrad = dd:CreateTexture(nil, "BACKGROUND", nil, 1)
        topGrad:SetHeight(30)
        topGrad:SetPoint("TOPLEFT", dd, "TOPLEFT", 1, -1)
        topGrad:SetPoint("TOPRIGHT", dd, "TOPRIGHT", -1, -1)
        topGrad:SetTexture(W8)
        if topGrad.SetGradient and CreateColor then
            topGrad:SetGradient("VERTICAL",
                CreateColor(0, 0, 0, 0),
                CreateColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.06))
        end

        equipDropdown = dd
    end

    equipDropdown:ClearAllPoints()
    equipDropdown:SetPoint("TOPRIGHT", R.panel, "TOPRIGHT", -10, -(CFG.HEADER_H + 4))

    local oldChildren = { equipDropdown:GetChildren() }
    for _, child in ipairs(oldChildren) do child:Hide() end
    local oldRegions = { equipDropdown:GetRegions() }
    for _, region in ipairs(oldRegions) do
        if region._dynamic then region:Hide() end
    end

    local PAD = 12
    local ROW_H = 34
    local yOfs = PAD

    local ddTitle = equipDropdown:CreateFontString(nil, "OVERLAY")
    ddTitle._dynamic = true
    TrySetFont(ddTitle, BODY_FONT, 10, "OUTLINE")
    ddTitle:SetPoint("TOPLEFT", equipDropdown, "TOPLEFT", PAD, -yOfs)
    ddTitle:SetText("EQUIPMENT SETS")
    ddTitle:SetTextColor(TC("mutedText"))
    ddTitle:Show()
    yOfs = yOfs + 18

    local setIDs = {}
    if C_EquipmentSet and C_EquipmentSet.GetEquipmentSetIDs then
        setIDs = SafeCall(C_EquipmentSet.GetEquipmentSetIDs) or {}
    end

    if #setIDs == 0 then
        local emptyFS = equipDropdown:CreateFontString(nil, "OVERLAY")
        emptyFS._dynamic = true
        TrySetFont(emptyFS, BODY_FONT, 11, "")
        emptyFS:SetPoint("TOP", equipDropdown, "TOP", 0, -(yOfs + 16))
        emptyFS:SetText("No equipment sets saved")
        emptyFS:SetTextColor(TC("mutedText"))
        emptyFS:Show()
        yOfs = yOfs + 44
    else
        for _, setID in ipairs(setIDs) do
            local ok, setName, iconFileID, _, isEquipped, numItems,
                  numEquipped, _, numLost = pcall(C_EquipmentSet.GetEquipmentSetInfo, setID)
            if ok and setName then
                local btn = CreateFrame("Button", nil, equipDropdown)
                btn:SetHeight(ROW_H)
                btn:SetPoint("TOPLEFT", equipDropdown, "TOPLEFT", 1, -yOfs)
                btn:SetPoint("TOPRIGHT", equipDropdown, "TOPRIGHT", -1, -yOfs)

                local hoverBg = btn:CreateTexture(nil, "BACKGROUND")
                hoverBg:SetAllPoints()
                hoverBg:SetColorTexture(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.08)
                hoverBg:Hide()

                if isEquipped then
                    local accentBar = btn:CreateTexture(nil, "OVERLAY")
                    accentBar:SetWidth(2)
                    accentBar:SetPoint("TOPLEFT", btn, "TOPLEFT", 0, -4)
                    accentBar:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", 0, 4)
                    accentBar:SetColorTexture(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.9)
                end

                local iconTex = (iconFileID and iconFileID ~= 0) and iconFileID
                                or "Interface\\Icons\\INV_Misc_QuestionMark"
                local icon = btn:CreateTexture(nil, "ARTWORK")
                icon:SetSize(24, 24)
                icon:SetPoint("LEFT", btn, "LEFT", PAD, 0)
                icon:SetTexture(iconTex)
                icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)

                local ib = {}
                ib[1] = btn:CreateTexture(nil, "OVERLAY"); ib[1]:SetHeight(1)
                ib[1]:SetPoint("TOPLEFT", icon, -1, 1); ib[1]:SetPoint("TOPRIGHT", icon, 1, 1)
                ib[2] = btn:CreateTexture(nil, "OVERLAY"); ib[2]:SetHeight(1)
                ib[2]:SetPoint("BOTTOMLEFT", icon, -1, -1); ib[2]:SetPoint("BOTTOMRIGHT", icon, 1, -1)
                ib[3] = btn:CreateTexture(nil, "OVERLAY"); ib[3]:SetWidth(1)
                ib[3]:SetPoint("TOPLEFT", icon, -1, 1); ib[3]:SetPoint("BOTTOMLEFT", icon, -1, -1)
                ib[4] = btn:CreateTexture(nil, "OVERLAY"); ib[4]:SetWidth(1)
                ib[4]:SetPoint("TOPRIGHT", icon, 1, 1); ib[4]:SetPoint("BOTTOMRIGHT", icon, 1, -1)
                for _, b in ipairs(ib) do
                    if isEquipped then
                        b:SetColorTexture(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.6)
                    else
                        b:SetColorTexture(0.4, 0.4, 0.4, 0.3)
                    end
                end

                local nameFS = btn:CreateFontString(nil, "OVERLAY")
                TrySetFont(nameFS, BODY_FONT, 11, "")
                nameFS:SetPoint("LEFT", icon, "RIGHT", 8, 0)
                nameFS:SetPoint("RIGHT", btn, "RIGHT", -PAD, 0)
                nameFS:SetJustifyH("LEFT")
                nameFS:SetWordWrap(false)
                nameFS:SetText(setName)
                if isEquipped then
                    nameFS:SetTextColor(TC("accent"))
                else
                    nameFS:SetTextColor(TC("bodyText"))
                end

                local delBtn = CreateFrame("Button", nil, btn)
                delBtn:SetSize(16, 16)
                delBtn:SetPoint("RIGHT", btn, "RIGHT", -PAD, 0)
                local delFS = delBtn:CreateFontString(nil, "OVERLAY")
                TrySetFont(delFS, BODY_FONT, 10, "OUTLINE")
                delFS:SetPoint("CENTER")
                delFS:SetText("X")
                delFS:SetTextColor(0.7, 0.25, 0.25, 0.6)
                local capturedDelID = setID
                delBtn:SetScript("OnEnter", function()
                    delFS:SetTextColor(1, 0.3, 0.3, 1)
                    GameTooltip:SetOwner(delBtn, "ANCHOR_TOP")
                    GameTooltip:SetText("Delete Set", 1, 0.3, 0.3)
                    GameTooltip:Show()
                end)
                delBtn:SetScript("OnLeave", function()
                    delFS:SetTextColor(0.7, 0.25, 0.25, 0.6)
                    GameTooltip:Hide()
                end)
                delBtn:SetScript("OnClick", function()
                    if C_EquipmentSet and C_EquipmentSet.DeleteEquipmentSet then
                        pcall(C_EquipmentSet.DeleteEquipmentSet, capturedDelID)
                        C_Timer.After(0.2, function()
                            if equipDropdown and equipDropdown:IsShown() then
                                equipDropdown:Hide()
                                Panel.ToggleEquipmentManager()
                            end
                        end)
                    end
                end)

                local rightAnchor = delBtn
                if isEquipped then
                    local check = btn:CreateTexture(nil, "OVERLAY")
                    check:SetSize(14, 14)
                    check:SetPoint("RIGHT", delBtn, "LEFT", -4, 0)
                    check:SetAtlas("Event-Scheduler-Green-Checkmark")
                    rightAnchor = check
                end
                nameFS:SetPoint("RIGHT", rightAnchor, "LEFT", -4, 0)

                local capturedName = setName
                local capturedNumEquipped = numEquipped
                local capturedNumItems = numItems
                local capturedNumLost = numLost
                btn:SetScript("OnEnter", function()
                    hoverBg:Show()
                    GameTooltip:SetOwner(btn, "ANCHOR_LEFT")
                    GameTooltip:SetText(capturedName or "", 1, 1, 1)
                    local itemLine = (capturedNumEquipped or 0) .. "/" .. (capturedNumItems or 0) .. " items equipped"
                    GameTooltip:AddLine(itemLine, TC("mutedText"))
                    if (capturedNumLost or 0) > 0 then
                        GameTooltip:AddLine(capturedNumLost .. " items missing", 1, 0.3, 0.3)
                    end
                    GameTooltip:AddLine(" ")
                    GameTooltip:AddLine("|cffaaaaaaLeft Click:|r Equip this set", 1, 1, 1)
                    GameTooltip:Show()
                end)
                btn:SetScript("OnLeave", function() hoverBg:Hide(); GameTooltip:Hide() end)

                local capturedSetID = setID
                btn:SetScript("OnClick", function()
                    if C_EquipmentSet and C_EquipmentSet.UseEquipmentSet then
                        pcall(C_EquipmentSet.UseEquipmentSet, capturedSetID)
                        equipDropdown:Hide()
                        C_Timer.After(0.5, function()
                            Panel.UpdateGearSlots()
                            Panel.UpdateItemLevel()
                        end)
                    end
                end)

                btn:Show()
                yOfs = yOfs + ROW_H
            end
        end
    end

    local sep = equipDropdown:CreateTexture(nil, "OVERLAY")
    sep._dynamic = true
    sep:SetHeight(1)
    sep:SetPoint("TOPLEFT", equipDropdown, "TOPLEFT", PAD, -(yOfs + 2))
    sep:SetPoint("TOPRIGHT", equipDropdown, "TOPRIGHT", -PAD, -(yOfs + 2))
    sep:SetTexture(W8)
    if sep.SetGradient and CreateColor then
        sep:SetGradient("HORIZONTAL",
            CreateColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.3),
            CreateColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0))
    end
    sep:Show()
    yOfs = yOfs + 6

    local newBtn = CreateFrame("Button", nil, equipDropdown)
    newBtn:SetHeight(ROW_H)
    newBtn:SetPoint("TOPLEFT", equipDropdown, "TOPLEFT", 1, -yOfs)
    newBtn:SetPoint("TOPRIGHT", equipDropdown, "TOPRIGHT", -1, -yOfs)
    local newHover = newBtn:CreateTexture(nil, "BACKGROUND")
    newHover:SetAllPoints()
    newHover:SetColorTexture(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.06)
    newHover:Hide()
    local newIcon = newBtn:CreateTexture(nil, "ARTWORK")
    newIcon:SetSize(24, 24)
    newIcon:SetPoint("LEFT", newBtn, "LEFT", PAD, 0)
    newIcon:SetTexture("Interface\\Icons\\Spell_ChargePositive")
    newIcon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
    newIcon:SetVertexColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.7)
    local newFS = newBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(newFS, BODY_FONT, 11, "")
    newFS:SetPoint("LEFT", newIcon, "RIGHT", 8, 0)
    newFS:SetText("Save New Set")
    newFS:SetTextColor(TC("accent"))
    local arrowTex = newBtn:CreateTexture(nil, "OVERLAY")
    arrowTex:SetSize(12, 12)
    arrowTex:SetPoint("RIGHT", newBtn, "RIGHT", -PAD, 0)
    arrowTex:SetAtlas("common-dropdown-icon-back")
    arrowTex:SetRotation(-math.pi)
    arrowTex:SetVertexColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.8)
    newBtn:SetScript("OnEnter", function() newHover:Show() end)
    newBtn:SetScript("OnLeave", function() newHover:Hide() end)
    newBtn:SetScript("OnClick", function()
        Panel.ShowNewSetFlyout(equipDropdown)
    end)
    newBtn:Show()
    yOfs = yOfs + ROW_H + PAD

    equipDropdown:SetHeight(yOfs)
    equipDropdown:Show()
end

local newSetFlyout = nil
local selectedSetIcon = 134400

local SET_ICON_OPTIONS = {
    "Interface\\Icons\\INV_Helmet_04",
    "Interface\\Icons\\INV_Helmet_06",
    "Interface\\Icons\\INV_Helmet_15",
    "Interface\\Icons\\INV_Crown_01",
    "Interface\\Icons\\INV_Shoulder_02",
    "Interface\\Icons\\INV_Shoulder_22",
    "Interface\\Icons\\INV_Chest_Chain_04",
    "Interface\\Icons\\INV_Chest_Plate01",
    "Interface\\Icons\\INV_Chest_Cloth_17",
    "Interface\\Icons\\INV_Chest_Leather_09",
    "Interface\\Icons\\INV_Gauntlets_04",
    "Interface\\Icons\\INV_Boots_Chain_01",
    "Interface\\Icons\\INV_Belt_03",
    "Interface\\Icons\\INV_Pants_06",
    "Interface\\Icons\\INV_Bracer_07",
    "Interface\\Icons\\INV_Misc_Cape_02",
    "Interface\\Icons\\INV_Shield_04",
    "Interface\\Icons\\INV_Shield_06",
    "Interface\\Icons\\INV_Sword_04",
    "Interface\\Icons\\INV_Sword_39",
    "Interface\\Icons\\INV_Axe_01",
    "Interface\\Icons\\INV_Mace_01",
    "Interface\\Icons\\INV_Staff_08",
    "Interface\\Icons\\INV_Spear_04",
    "Interface\\Icons\\INV_Weapon_Bow_07",
    "Interface\\Icons\\INV_Weapon_Rifle_01",
    "Interface\\Icons\\INV_Wand_01",
    "Interface\\Icons\\INV_Weapon_ShortBlade_02",
    "Interface\\Icons\\INV_Gauntlets_04",
    "Interface\\Icons\\INV_Jewelry_Ring_03",
    "Interface\\Icons\\INV_Jewelry_Trinket_04",
    "Interface\\Icons\\INV_Jewelry_Necklace_01",
}

function Panel.ShowNewSetFlyout(anchorFrame)
    if newSetFlyout and newSetFlyout:IsShown() then
        newSetFlyout:Hide()
        return
    end

    selectedSetIcon = SET_ICON_OPTIONS[1]
    local flyPAD = 14
    local ICON_SIZE = 28
    local ICON_GAP = 3
    local ICONS_PER_ROW = 8
    local gridRows = math.ceil(#SET_ICON_OPTIONS / ICONS_PER_ROW)
    local gridW = ICONS_PER_ROW * (ICON_SIZE + ICON_GAP) - ICON_GAP
    local gridH = gridRows * (ICON_SIZE + ICON_GAP) - ICON_GAP
    local flyW = gridW + (flyPAD * 2)
    local flyH = flyPAD + 18 + 10 + 28 + 14 + 14 + 6 + gridH + 16 + 30 + flyPAD

    if not newSetFlyout then
        local fly = CreateFrame("Frame", nil, anchorFrame:GetParent(), "BackdropTemplate")
        fly:SetSize(flyW, flyH)
        fly:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        fly:SetBackdropColor(0.04, 0.04, 0.06, 0.96)
        fly:SetBackdropBorderColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.25)
        fly:SetFrameLevel(anchorFrame:GetFrameLevel() + 2)
        fly:Hide()

        for i = 1, 4 do
            local s = fly:CreateTexture(nil, "BACKGROUND", nil, -1)
            local off = i * 1.2
            s:SetColorTexture(0, 0, 0, 0.12 - (i * 0.025))
            s:SetPoint("TOPLEFT", -off, off)
            s:SetPoint("BOTTOMRIGHT", off, -off)
        end

        local frost = fly:CreateTexture(nil, "OVERLAY", nil, 3)
        frost:SetHeight(1)
        frost:SetPoint("TOPLEFT", fly, "TOPLEFT", 1, -1)
        frost:SetPoint("TOPRIGHT", fly, "TOPRIGHT", -1, -1)
        frost:SetColorTexture(1, 1, 1, 0.06)

        local topWash = fly:CreateTexture(nil, "BACKGROUND", nil, 1)
        topWash:SetHeight(40)
        topWash:SetPoint("TOPLEFT", fly, "TOPLEFT", 1, -1)
        topWash:SetPoint("TOPRIGHT", fly, "TOPRIGHT", -1, -1)
        topWash:SetTexture(W8)
        if topWash.SetGradient and CreateColor then
            topWash:SetGradient("VERTICAL",
                CreateColor(0, 0, 0, 0),
                CreateColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.05))
        end

        local titleFS = fly:CreateFontString(nil, "OVERLAY")
        TrySetFont(titleFS, BODY_FONT, 10, "OUTLINE")
        titleFS:SetPoint("TOPLEFT", fly, "TOPLEFT", flyPAD, -flyPAD)
        titleFS:SetText("NEW EQUIPMENT SET")
        titleFS:SetTextColor(TC("mutedText"))

        local nameInput = CreateFrame("EditBox", nil, fly, "BackdropTemplate")
        nameInput:SetHeight(28)
        nameInput:SetPoint("TOPLEFT", titleFS, "BOTTOMLEFT", 0, -10)
        nameInput:SetPoint("RIGHT", fly, "RIGHT", -flyPAD, 0)
        nameInput:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 8, right = 8, top = 4, bottom = 4 } })
        nameInput:SetBackdropColor(0.06, 0.06, 0.08, 0.9)
        nameInput:SetBackdropBorderColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.2)
        TrySetFont(nameInput, BODY_FONT, 11, "")
        nameInput:SetTextColor(TC("titleText"))
        nameInput:SetAutoFocus(false)
        nameInput:SetText("New Set")
        nameInput:SetCursorPosition(0)
        nameInput:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
        nameInput:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
        nameInput:SetScript("OnEditFocusGained", function(self)
            self:SetBackdropBorderColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.5)
        end)
        nameInput:SetScript("OnEditFocusLost", function(self)
            self:SetBackdropBorderColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.2)
        end)
        fly._nameInput = nameInput

        local iconLbl = fly:CreateFontString(nil, "OVERLAY")
        TrySetFont(iconLbl, BODY_FONT, 10, "")
        iconLbl:SetPoint("TOPLEFT", nameInput, "BOTTOMLEFT", 0, -14)
        iconLbl:SetText("Choose Icon")
        iconLbl:SetTextColor(TC("mutedText"))

        local iconGrid = CreateFrame("Frame", nil, fly)
        iconGrid:SetPoint("TOPLEFT", iconLbl, "BOTTOMLEFT", 0, -6)
        iconGrid:SetSize(gridW, gridH)

        local iconButtons = {}
        for i, iconPath in ipairs(SET_ICON_OPTIONS) do
            local row = math.floor((i - 1) / ICONS_PER_ROW)
            local col = (i - 1) % ICONS_PER_ROW
            local ib = CreateFrame("Button", nil, iconGrid)
            ib:SetSize(ICON_SIZE, ICON_SIZE)
            ib:SetPoint("TOPLEFT", iconGrid, "TOPLEFT",
                col * (ICON_SIZE + ICON_GAP), -(row * (ICON_SIZE + ICON_GAP)))

            local tex = ib:CreateTexture(nil, "ARTWORK")
            tex:SetPoint("TOPLEFT", 2, -2)
            tex:SetPoint("BOTTOMRIGHT", -2, 2)
            tex:SetTexture(iconPath)
            tex:SetTexCoord(0.07, 0.93, 0.07, 0.93)
            ib._tex = tex

            local ibBg = ib:CreateTexture(nil, "BACKGROUND")
            ibBg:SetAllPoints()
            ibBg:SetColorTexture(0.08, 0.08, 0.10, 0.8)

            local sel = ib:CreateTexture(nil, "OVERLAY")
            sel:SetPoint("TOPLEFT", -1, 1)
            sel:SetPoint("BOTTOMRIGHT", 1, -1)
            sel:SetColorTexture(0, 0, 0, 0)
            ib._sel = sel

            local selParts = {}
            selParts[1] = ib:CreateTexture(nil, "OVERLAY", nil, 2); selParts[1]:SetHeight(2)
            selParts[1]:SetPoint("TOPLEFT", -1, 1); selParts[1]:SetPoint("TOPRIGHT", 1, 1)
            selParts[2] = ib:CreateTexture(nil, "OVERLAY", nil, 2); selParts[2]:SetHeight(2)
            selParts[2]:SetPoint("BOTTOMLEFT", -1, -1); selParts[2]:SetPoint("BOTTOMRIGHT", 1, -1)
            selParts[3] = ib:CreateTexture(nil, "OVERLAY", nil, 2); selParts[3]:SetWidth(2)
            selParts[3]:SetPoint("TOPLEFT", -1, 1); selParts[3]:SetPoint("BOTTOMLEFT", -1, -1)
            selParts[4] = ib:CreateTexture(nil, "OVERLAY", nil, 2); selParts[4]:SetWidth(2)
            selParts[4]:SetPoint("TOPRIGHT", 1, 1); selParts[4]:SetPoint("BOTTOMRIGHT", 1, -1)
            for _, p in ipairs(selParts) do p:SetColorTexture(0, 0, 0, 0) end
            ib._selParts = selParts

            local capturedPath = iconPath
            ib:SetScript("OnEnter", function()
                if selectedSetIcon ~= capturedPath then
                    for _, p in ipairs(selParts) do
                        p:SetColorTexture(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.4)
                    end
                end
            end)
            ib:SetScript("OnLeave", function()
                if selectedSetIcon ~= capturedPath then
                    for _, p in ipairs(selParts) do p:SetColorTexture(0, 0, 0, 0) end
                end
            end)
            ib:SetScript("OnClick", function()
                selectedSetIcon = capturedPath
                for _, ob in ipairs(iconButtons) do
                    for _, p in ipairs(ob._selParts) do p:SetColorTexture(0, 0, 0, 0) end
                    ob._tex:SetVertexColor(1, 1, 1, 0.7)
                end
                for _, p in ipairs(selParts) do
                    p:SetColorTexture(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.9)
                end
                tex:SetVertexColor(1, 1, 1, 1)
            end)

            tex:SetVertexColor(1, 1, 1, 0.7)

            iconButtons[#iconButtons + 1] = ib
        end
        fly._iconButtons = iconButtons

        local btnBar = CreateFrame("Frame", nil, fly)
        btnBar:SetHeight(30)
        btnBar:SetPoint("BOTTOMLEFT", fly, "BOTTOMLEFT", flyPAD, flyPAD)
        btnBar:SetPoint("BOTTOMRIGHT", fly, "BOTTOMRIGHT", -flyPAD, flyPAD)

        local saveBtn = CreateFrame("Button", nil, btnBar)
        saveBtn:SetSize(90, 28)
        saveBtn:SetPoint("RIGHT", btnBar, "RIGHT", 0, 0)
        local saveBg = saveBtn:CreateTexture(nil, "BACKGROUND")
        saveBg:SetAllPoints()
        saveBg:SetColorTexture(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.2)
        local saveFS = saveBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(saveFS, BODY_FONT, 11, "")
        saveFS:SetPoint("CENTER")
        saveFS:SetText("Save Set")
        saveFS:SetTextColor(TC("accent"))
        local function MakeBtnBorder(parent, r, g, b, a)
            local t = parent:CreateTexture(nil, "OVERLAY"); t:SetHeight(1)
            t:SetPoint("TOPLEFT"); t:SetPoint("TOPRIGHT"); t:SetColorTexture(r, g, b, a)
            local b2 = parent:CreateTexture(nil, "OVERLAY"); b2:SetHeight(1)
            b2:SetPoint("BOTTOMLEFT"); b2:SetPoint("BOTTOMRIGHT"); b2:SetColorTexture(r, g, b, a)
            local l = parent:CreateTexture(nil, "OVERLAY"); l:SetWidth(1)
            l:SetPoint("TOPLEFT"); l:SetPoint("BOTTOMLEFT"); l:SetColorTexture(r, g, b, a)
            local rv = parent:CreateTexture(nil, "OVERLAY"); rv:SetWidth(1)
            rv:SetPoint("TOPRIGHT"); rv:SetPoint("BOTTOMRIGHT"); rv:SetColorTexture(r, g, b, a)
        end
        MakeBtnBorder(saveBtn, activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.3)

        saveBtn:SetScript("OnEnter", function()
            saveBg:SetColorTexture(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.35)
        end)
        saveBtn:SetScript("OnLeave", function()
            saveBg:SetColorTexture(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.2)
        end)
        saveBtn:SetScript("OnClick", function()
            local setName = fly._nameInput:GetText() or "New Set"
            if setName == "" then setName = "New Set" end
            local iconArg = selectedSetIcon
            if type(iconArg) == "string" and GetFileIDFromPath then
                local fid = GetFileIDFromPath(iconArg)
                if fid and fid > 0 then iconArg = fid end
            end
            if C_EquipmentSet and C_EquipmentSet.CreateEquipmentSet then
                pcall(C_EquipmentSet.CreateEquipmentSet, setName, iconArg)
                fly:Hide()
                if equipDropdown then equipDropdown:Hide() end
                C_Timer.After(0.3, function() Panel.ToggleEquipmentManager() end)
            end
        end)

        local cancelBtn = CreateFrame("Button", nil, btnBar)
        cancelBtn:SetSize(60, 28)
        cancelBtn:SetPoint("LEFT", btnBar, "LEFT", 0, 0)
        local cancelFS = cancelBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(cancelFS, BODY_FONT, 11, "")
        cancelFS:SetPoint("CENTER")
        cancelFS:SetText("Cancel")
        cancelFS:SetTextColor(TC("mutedText"))
        cancelBtn:SetScript("OnEnter", function() cancelFS:SetTextColor(TC("bodyText")) end)
        cancelBtn:SetScript("OnLeave", function() cancelFS:SetTextColor(TC("mutedText")) end)
        cancelBtn:SetScript("OnClick", function() fly:Hide() end)

        newSetFlyout = fly
    end

    newSetFlyout:SetSize(flyW, flyH)
    newSetFlyout._nameInput:SetText("New Set")
    newSetFlyout._nameInput:SetCursorPosition(0)
    newSetFlyout._nameInput:HighlightText()
    selectedSetIcon = SET_ICON_OPTIONS[1]
    for _, ob in ipairs(newSetFlyout._iconButtons) do
        for _, p in ipairs(ob._selParts) do p:SetColorTexture(0, 0, 0, 0) end
        ob._tex:SetVertexColor(1, 1, 1, 0.7)
    end
    if newSetFlyout._iconButtons[1] then
        for _, p in ipairs(newSetFlyout._iconButtons[1]._selParts) do
            p:SetColorTexture(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.9)
        end
        newSetFlyout._iconButtons[1]._tex:SetVertexColor(1, 1, 1, 1)
    end

    newSetFlyout:ClearAllPoints()
    newSetFlyout:SetPoint("TOPLEFT", anchorFrame, "TOPRIGHT", 4, 0)
    newSetFlyout:Show()
end

function Panel.ApplyTheme()
    GetActiveTheme()
    local R = Panel._refs
    if not R.panel then return end

    R.panel:SetBackdropColor(TC("frameBg"))
    R.panel:SetBackdropBorderColor(TC("accent"))
    R.hdrBg:SetColorTexture(TC("headerBg"))
    R.titleFS:SetTextColor(TC("titleText"))
    R.nameFS:SetTextColor(TC("titleText"))
    R.specFS:SetTextColor(TC("bodyText"))
    R.ilvlLabel:SetTextColor(TC("mutedText"))
    R.ilvlValue:SetTextColor(TC("titleText"))
    if R.enhLabel then R.enhLabel:SetTextColor(TC("mutedText")) end
    if R.attrLabel then R.attrLabel:SetTextColor(TC("mutedText")) end
    R.tabBg:SetColorTexture(TC("headerBg"))

    if R.gearLabel then R.gearLabel:SetTextColor(TC("mutedText")) end
    if R.leftGlass then R.leftGlass:ApplyTheme() end
    if R.rightGlass then R.rightGlass:ApplyTheme() end

    if R.hdrAccent and R.hdrAccent.SetGradient and CreateColor then
        R.hdrAccent:SetGradient("HORIZONTAL",
            CreateColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.6),
            CreateColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.0))
    end

    if R.repOverlay then R.repOverlay:SetBackdropColor(TC("frameBg")) end
    if R.curOverlay then R.curOverlay:SetBackdropColor(TC("frameBg")) end

    if R.repTitle then R.repTitle:SetTextColor(TC("titleText")) end
    if R.repBackFS then R.repBackFS:SetTextColor(TC("accent")) end

    if R.curTitle then R.curTitle:SetTextColor(TC("titleText")) end
    if R.curSubtitle then R.curSubtitle:SetTextColor(TC("mutedText")) end
    if R.curBackFS then R.curBackFS:SetTextColor(TC("accent")) end

    if R.openBlizzBtn then
        R.openBlizzBtn:SetBackdropColor(TC("headerBg"))
        R.openBlizzBtn:SetBackdropBorderColor(TC("accent"))
    end
    if R.openBlizzFS then R.openBlizzFS:SetTextColor(TC("accent")) end

    if R.equipMgrIcon then
        R.equipMgrIcon:SetVertexColor(activeTheme.mutedText[1], activeTheme.mutedText[2], activeTheme.mutedText[3], 0.70)
    end
    if R.gearIcon then
        R.gearIcon:SetVertexColor(activeTheme.mutedText[1], activeTheme.mutedText[2], activeTheme.mutedText[3], 0.70)
    end

    if R.tabButtons then
        for key, btn in pairs(R.tabButtons) do
            if btn._underline then
                btn._underline:SetColorTexture(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 1)
            end
        end
    end
    if R.tabAccent and R.tabAccent.SetColorTexture then
        R.tabAccent:SetColorTexture(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.3)
    end

    Panel.SetActiveTab(Panel._state.activeTab)

    Panel.UpdatePrimaryStats()
    Panel.UpdateEnhancements()

    if Panel._state.activeTab == "reputation" then
        pcall(Panel.UpdateReputation)
    elseif Panel._state.activeTab == "currency" then
        pcall(Panel.UpdateCurrency)
    end
end

function Panel.ShowSettingsPopup()
    local R = Panel._refs
    if R.settingsPopup then R.settingsPopup:Show() end
end

local titlePickerFrame = nil

function Panel.ToggleTitlePicker()
    if titlePickerFrame and titlePickerFrame:IsShown() then
        titlePickerFrame:Hide()
        return
    end

    local R = Panel._refs
    if not R.panel or not R.identityBar then return end

    if not titlePickerFrame then
        titlePickerFrame = CreateFrame("Frame", nil, R.panel, "BackdropTemplate")
        titlePickerFrame:SetSize(480, 320)
        titlePickerFrame:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        titlePickerFrame:SetBackdropColor(0.05, 0.05, 0.07, 0.95)
        titlePickerFrame:SetBackdropBorderColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.5)
        titlePickerFrame:SetFrameLevel(R.panel:GetFrameLevel() + 25)

        local tpTitle = titlePickerFrame:CreateFontString(nil, "OVERLAY")
        TrySetFont(tpTitle, BODY_FONT, 10, "OUTLINE")
        tpTitle:SetPoint("TOP", titlePickerFrame, "TOP", 0, -8)
        tpTitle:SetText("SELECT TITLE")
        tpTitle:SetTextColor(TC("mutedText"))

        local scrollFrame = CreateFrame("ScrollFrame", nil, titlePickerFrame, "UIPanelScrollFrameTemplate")
        scrollFrame:SetPoint("TOPLEFT", titlePickerFrame, "TOPLEFT", 6, -24)
        scrollFrame:SetPoint("BOTTOMRIGHT", titlePickerFrame, "BOTTOMRIGHT", -24, 6)
        local scrollContent = CreateFrame("Frame", nil, scrollFrame)
        scrollContent:SetWidth(1)
        scrollFrame:SetScrollChild(scrollContent)
        scrollFrame:SetScript("OnSizeChanged", function(self, w) if w > 0 then scrollContent:SetWidth(w) end end)
        if scrollFrame.ScrollBar then
            local sb = scrollFrame.ScrollBar
            if sb.ThumbTexture then sb.ThumbTexture:SetTexture(W8); sb.ThumbTexture:SetVertexColor(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.30); sb.ThumbTexture:SetWidth(4) end
            if sb.ScrollUpButton then sb.ScrollUpButton:SetAlpha(0) end
            if sb.ScrollDownButton then sb.ScrollDownButton:SetAlpha(0) end
        end

        titlePickerFrame._scrollContent = scrollContent
        titlePickerFrame._scrollFrame = scrollFrame
    end

    titlePickerFrame:ClearAllPoints()
    titlePickerFrame:SetPoint("BOTTOM", R.identityBar, "TOP", 0, 4)

    local content = titlePickerFrame._scrollContent
    local oldChildren = { content:GetChildren() }
    for _, child in ipairs(oldChildren) do child:Hide() end

    local ROW_H = 22
    local COL_COUNT = 2
    local currentTitle = SafeCall(GetCurrentTitle) or 0
    local playerName = SafeCall(UnitName, "player") or ""

    local titleList = {}
    titleList[#titleList + 1] = { id = 0, display = playerName .. "  |cff888888(No Title)|r" }
    local numTitles = SafeCall(GetNumTitles) or 0
    for titleID = 1, numTitles do
        local known = SafeCall(IsTitleKnown, titleID)
        if known then
            local rawName = SafeCall(GetTitleName, titleID)
            if rawName and rawName ~= "" then
                local displayName = rawName:gsub("%%s", playerName):gsub("^%s+", ""):gsub("%s+$", "")
                titleList[#titleList + 1] = { id = titleID, display = displayName }
            end
        end
    end

    local contentW = content:GetWidth()
    if contentW < 10 then contentW = 440 end
    local colW = math.floor(contentW / COL_COUNT)
    local col = 0
    local yOfs = 0

    for _, entry in ipairs(titleList) do
        local btn = CreateFrame("Button", nil, content)
        btn:SetSize(colW, ROW_H)
        btn:SetPoint("TOPLEFT", content, "TOPLEFT", col * colW, -yOfs)

        local fs = btn:CreateFontString(nil, "OVERLAY")
        TrySetFont(fs, BODY_FONT, 11, "")
        fs:SetPoint("LEFT", btn, "LEFT", 8, 0)
        fs:SetPoint("RIGHT", btn, "RIGHT", -4, 0)
        fs:SetJustifyH("LEFT")
        fs:SetWordWrap(false)
        fs:SetText(entry.display)
        if entry.id == currentTitle then
            fs:SetTextColor(TC("accent"))
        else
            fs:SetTextColor(TC("bodyText"))
        end

        local hoverBg = btn:CreateTexture(nil, "BACKGROUND")
        hoverBg:SetAllPoints(); hoverBg:SetColorTexture(1, 1, 1, 0.05); hoverBg:Hide()
        btn:SetScript("OnEnter", function() hoverBg:Show() end)
        btn:SetScript("OnLeave", function() hoverBg:Hide() end)
        local capturedID = entry.id
        btn:SetScript("OnClick", function()
            pcall(SetCurrentTitle, capturedID)
            titlePickerFrame:Hide()
            local R = Panel._refs
            if R.nameFS then
                local pName = SafeCall(UnitName, "player") or "Unknown"
                if capturedID == 0 then
                    R.nameFS:SetText(pName)
                else
                    local raw = SafeCall(GetTitleName, capturedID) or ""
                    raw = raw:gsub("^%s+", ""):gsub("%s+$", "")
                    if raw:sub(1,1) == raw:sub(1,1):lower() then
                        R.nameFS:SetText(pName .. " " .. raw)
                    else
                        R.nameFS:SetText(raw .. " " .. pName)
                    end
                end
            end
        end)
        btn:Show()

        col = col + 1
        if col >= COL_COUNT then
            col = 0
            yOfs = yOfs + ROW_H
        end
    end
    if col > 0 then yOfs = yOfs + ROW_H end

    content:SetHeight(math.max(yOfs + 4, 1))
    titlePickerFrame:Show()
end

local evf = CreateFrame("Frame")
local function SafeReg(event)
    pcall(evf.RegisterEvent, evf, event)
end

SafeReg("ADDON_LOADED")
SafeReg("PLAYER_LOGIN")
SafeReg("UNIT_INVENTORY_CHANGED")
SafeReg("PLAYER_EQUIPMENT_CHANGED")
SafeReg("PLAYER_AVG_ITEM_LEVEL_UPDATE")
SafeReg("UNIT_STATS")
SafeReg("COMBAT_RATING_UPDATE")
SafeReg("MASTERY_UPDATE")
SafeReg("PLAYER_SPECIALIZATION_CHANGED")
SafeReg("ACTIVE_TALENT_GROUP_CHANGED")
SafeReg("PLAYER_LEVEL_UP")
SafeReg("PLAYER_TITLE_CHANGED")
SafeReg("UNIT_MODEL_CHANGED")
SafeReg("UPDATE_FACTION")
SafeReg("CURRENCY_DISPLAY_UPDATE")
SafeReg("PLAYER_ENTERING_WORLD")

local pendingUpdate = false
local function DeferUpdate()
    if pendingUpdate then return end
    pendingUpdate = true
    C_Timer.After(0.1, function()
        pendingUpdate = false
        if Panel.IsOpen() and Panel._state.activeTab == "character" then
            Panel.UpdateGearSlots()
            Panel.UpdateItemLevel()
            Panel.UpdatePrimaryStats()
            Panel.UpdateEnhancements()
        end
    end)
end

evf:SetScript("OnEvent", function(_, event, arg1)
    if event == "ADDON_LOADED" and arg1 == ADDON_NAME then
        local s = _G.MidnightUISettings
        if s and s.General and s.General.characterPanelTheme == nil then
            s.General.characterPanelTheme = "parchment"
        end
    elseif event == "UNIT_INVENTORY_CHANGED" or event == "PLAYER_EQUIPMENT_CHANGED"
        or event == "PLAYER_AVG_ITEM_LEVEL_UPDATE" then
        DeferUpdate()
    elseif event == "UNIT_STATS" or event == "COMBAT_RATING_UPDATE" or event == "MASTERY_UPDATE" then
        DeferUpdate()
    elseif event == "PLAYER_SPECIALIZATION_CHANGED" or event == "ACTIVE_TALENT_GROUP_CHANGED" then
        if Panel.IsOpen() then
            Panel.UpdateModel()
            Panel.UpdateIdentity()
            DeferUpdate()
        end
    elseif event == "PLAYER_LEVEL_UP" or event == "PLAYER_TITLE_CHANGED" then
        if Panel.IsOpen() then Panel.UpdateIdentity() end
    elseif event == "UNIT_MODEL_CHANGED" then
        if Panel.IsOpen() then Panel.UpdateModel() end
    elseif event == "UPDATE_FACTION" then
        if Panel.IsOpen() and Panel._state.activeTab == "reputation" then
            Panel.UpdateReputation()
        end
    elseif event == "CURRENCY_DISPLAY_UPDATE" then
        if Panel.IsOpen() and Panel._state.activeTab == "currency" then
            Panel.UpdateCurrency()
        end
    end
end)

local hookGuard = false
local hookInstalled = false

local function UseBlizzardCharacterPanel()
    local s = MidnightUISettings
    return s and s.General and s.General.useBlizzardCharacterPanel == true
end

local function InstallToggleHook()
    if hookInstalled then return end
    if UseBlizzardCharacterPanel() then return end
    if type(ToggleCharacter) ~= "function" then return end
    hookInstalled = true

    hooksecurefunc("ToggleCharacter", function(tab)
        if UseBlizzardCharacterPanel() then return end
        if hookGuard then return end
        hookGuard = true

        if CharacterFrame and CharacterFrame:IsShown() then
            HideUIPanel(CharacterFrame)
        end

        local targetTab = "character"
        if tab == "TokenFrame" then
            targetTab = "currency"
        elseif tab == "ReputationFrame" then
            targetTab = "reputation"
        end
        local isTargeting = SpellIsTargeting and SpellIsTargeting()
        if Panel._state.panelOpen and isTargeting then
            Panel.SetActiveTab(targetTab)
        else
            Panel.Toggle(targetTab)
        end

        C_Timer.After(0, function() hookGuard = false end)
    end)

    if CharacterFrame and not CharacterFrame._muiSuppressHooked then
        CharacterFrame._muiSuppressHooked = true
        local _charHideDepth = 0
        CharacterFrame:HookScript("OnShow", function(self)
            if UseBlizzardCharacterPanel() then return end
            if not hookGuard then
                if _charHideDepth > 0 then return end
                _charHideDepth = _charHideDepth + 1
                HideUIPanel(self)
                _charHideDepth = _charHideDepth - 1
            end
        end)
    end
end

local TAB_TO_BLIZZ = {
    reputation = "ReputationFrame",
    currency   = "TokenFrame",
    character  = "PaperDollFrame",
}

_G.MidnightUI_CharacterPanel_Toggle = function(tab)
    if UseBlizzardCharacterPanel() then
        if type(ToggleCharacter) == "function" then ToggleCharacter(TAB_TO_BLIZZ[tab] or "PaperDollFrame") end
        return
    end
    Panel.Toggle(tab)
end
_G.MidnightUI_CharacterPanel_Show = function(tab)
    if UseBlizzardCharacterPanel() then
        if type(ShowUIPanel) == "function" and CharacterFrame then ShowUIPanel(CharacterFrame) end
        return
    end
    Panel.Show(tab)
end
_G.MidnightUI_CharacterPanel_Hide = function()
    if UseBlizzardCharacterPanel() then
        if type(HideUIPanel) == "function" and CharacterFrame then HideUIPanel(CharacterFrame) end
        return
    end
    Panel.Hide()
end

InstallToggleHook()

local hookEvf = CreateFrame("Frame")
hookEvf:RegisterEvent("ADDON_LOADED")
hookEvf:SetScript("OnEvent", function(_, event, addon)
    if addon == "Blizzard_CharacterUI" or addon == "Blizzard_CharacterCustomize" then
        if UseBlizzardCharacterPanel() then return end
        InstallToggleHook()
        if CharacterFrame and CharacterFrame:IsShown() then
            CharacterFrame:Hide()
            Panel.Show("character")
        end
        C_Timer.After(0.5, AttachBlizzardSlotButtons)
    end
end)
