
-- ============================================================
-- FILE: GroupFinder.lua
-- PURPOSE: Custom premade group finder panel. Wraps the
--   Blizzard LFG (Looking for Group) listing system with a
--   styled search UI, team builder, application management,
--   and group statistics (Mythic+ key level, iLvl, role
--   composition). Integrates C_LFGList, C_MythicPlus, and
--   C_ChallengeMode APIs.
-- HOW IT WORKS:
--   1. Panel.EnsurePanel() — lazy-creates the main Frame
--      with: search bar, activity type selector (Mythic+/
--      Raid/PvP etc.), result list scroll frame, stat pills
--      per result (key level, iLvl, role slots, rating).
--   2. Search flow: user sets filter criteria → C_LFGList.
--      Search(params) → LFG_LIST_SEARCH_RESULTS_RECEIVED
--      event → Panel.PopulateResults().
--   3. Result rows: each row shows leader name (class color),
--      activity name, stat pills, Apply button. Click opens
--      application detail overlay.
--   4. Application management: tracks sent applications,
--      shows status (pending/accepted/declined) and allows
--      withdrawal via C_LFGList.RemoveApplicationFromGroup.
--   5. Auto-refresh: StartAutoRefresh/StopAutoRefresh ticker
--      re-issues C_LFGList.Search at a configurable interval
--      to keep results fresh.
--   6. searchEventFrame: routes LFG_LIST_* events to
--      PopulateResults / UpdateApplicationStatus.
-- DEPENDS ON:
--   C_LFGList, C_MythicPlus, C_ChallengeMode, C_PlayerInfo
--   GetAverageItemLevel, RAID_CLASS_COLORS
-- EXPORTS:
--   _G.MidnightUI_GroupFinder_Toggle
--   _G.MidnightUI_GroupFinder_Show
--   _G.MidnightUI_GroupFinder_Hide
-- OWNED SAVEDVARIABLES: none
-- EVENTS REGISTERED:
--   LFG_LIST_SEARCH_RESULTS_RECEIVED
--   LFG_LIST_APPLICATION_STATUS_UPDATED
--   LFG_LIST_ENTRY_EXPIRED_TOO_MANY_PLAYERS
--   PLAYER_ENTERING_WORLD, ADDON_LOADED
-- FRAMES CREATED:
--   Main GroupFinder panel (lazy, unnamed)
--   Per-result: stat pill frames, result row buttons
-- GOTCHAS:
--   C_LFGList.Search is throttled by the server; rapid
--   re-searches can silently fail. Auto-refresh respects
--   the throttle via C_Timer.
--   GetAverageItemLevel for LFG applicants uses the
--   C_LFGList.GetSearchResultInfo iLvl field, not the
--   local GetAverageItemLevel (which is player-only).
--   hooksecurefunc on PVEFrame/LFGListFrame suppresses
--   Blizzard's default group finder UI.
-- ============================================================

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 hooksecurefunc = hooksecurefunc
local PlaySound = PlaySound
local C_Timer = C_Timer
local C_LFGList = C_LFGList
local C_MythicPlus = C_MythicPlus
local C_ChallengeMode = C_ChallengeMode
local C_PlayerInfo = C_PlayerInfo
local GetAverageItemLevel = GetAverageItemLevel

local TC
local activeTheme
local GetActiveTheme
local StartAutoRefresh
local StopAutoRefresh

local function SafeCall(fn, ...)
    if type(fn) ~= "function" then return nil end
    local ok, r1, r2, r3, r4, r5 = pcall(fn, ...)
    if not ok then return nil end
    return r1, r2, r3, r4, r5
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 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"))

    for i = 1, 3 do
        local sh = glass:CreateTexture(nil, "BACKGROUND", nil, -1)
        local off = i * 1.2
        local alpha = 0.06 - (i * 0.015)
        sh:SetPoint("TOPLEFT", glass, "TOPLEFT", -off, off)
        sh:SetPoint("BOTTOMRIGHT", glass, "BOTTOMRIGHT", off, -off)
        sh:SetColorTexture(0, 0, 0, math.max(0.01, alpha))
    end

    local frost = glass:CreateTexture(nil, "OVERLAY", nil, 3)
    frost:SetHeight(2)
    frost:SetPoint("TOPLEFT", glass, "TOPLEFT", 1, -1)
    frost:SetPoint("TOPRIGHT", glass, "TOPRIGHT", -1, -1)
    frost:SetTexture(W8)
    if frost.SetGradient and CreateColor then
        frost:SetGradient("HORIZONTAL",
            CreateColor(1, 1, 1, 0.02),
            CreateColor(1, 1, 1, 0.06))
    else
        frost:SetColorTexture(1, 1, 1, 0.06)
    end
    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.12),
            CreateColor(0, 0, 0, 0))
    end
    glass._grad = grad

    local botShadow = glass:CreateTexture(nil, "BACKGROUND", nil, 2)
    botShadow:SetHeight(6)
    botShadow:SetPoint("BOTTOMLEFT", glass, "BOTTOMLEFT", 1, 1)
    botShadow:SetPoint("BOTTOMRIGHT", glass, "BOTTOMRIGHT", -1, 1)
    botShadow:SetTexture(W8)
    if botShadow.SetGradient and CreateColor then
        botShadow:SetGradient("VERTICAL",
            CreateColor(0, 0, 0, 0.10),
            CreateColor(0, 0, 0, 0))
    end
    glass._botShadow = botShadow

    local acWash = glass:CreateTexture(nil, "BACKGROUND", nil, 0)
    acWash:SetPoint("TOPLEFT", 1, -1)
    acWash:SetPoint("BOTTOMRIGHT", -1, 1)
    acWash:SetTexture(W8)
    local aR, aG, aB = TC("accent")
    if acWash.SetGradient and CreateColor then
        acWash:SetGradient("VERTICAL",
            CreateColor(aR, aG, aB, 0),
            CreateColor(aR, aG, aB, 0.02))
    end
    glass._acWash = acWash

    function glass:ApplyTheme()
        self:SetBackdropColor(TC("glassBg"))
        self:SetBackdropBorderColor(TC("glassBorder"))
        local ar, ag, ab = TC("accent")
        if self._acWash and self._acWash.SetGradient and CreateColor then
            self._acWash:SetGradient("VERTICAL",
                CreateColor(ar, ag, ab, 0),
                CreateColor(ar, ag, ab, 0.02))
        end
    end

    return glass
end

local function ApplyGradient(tex, dir, r1, g1, b1, a1, r2, g2, b2, a2)
    if tex and tex.SetGradient and CreateColor then
        tex:SetTexture(W8)
        tex:SetGradient(dir,
            CreateColor(r1, g1, b1, a1),
            CreateColor(r2, g2, b2, a2))
        return true
    end
    return false
end

local function GenerateAbbreviation(name)
    if not name or name == "" then return "??" end
    local parts = {}
    for token in name:gmatch("[%w']+") do
        parts[#parts + 1] = token
    end
    local skip = { ["the"] = true, ["of"] = true, ["a"] = true, ["an"] = true }
    local abbr = ""
    for i, w in ipairs(parts) do
        local lower = w:lower()
        if skip[lower] then
            if i > 1 then abbr = abbr .. w:sub(1, 1):lower() end
        else
            abbr = abbr .. w:sub(1, 1):upper()
        end
    end
    if #abbr > 5 then abbr = abbr:sub(1, 5) end
    if #abbr <= 1 then
        for _, w in ipairs(parts) do
            if not skip[w:lower()] then
                abbr = w:sub(1, 3):upper()
                break
            end
        end
    end
    if #abbr == 0 then abbr = name:sub(1, 2):upper() end
    return abbr
end

local ACCENT_PALETTE = {
    { 0.80, 0.30, 0.30 },
    { 0.30, 0.70, 0.40 },
    { 0.40, 0.50, 0.90 },
    { 0.65, 0.40, 0.85 },
    { 0.85, 0.65, 0.20 },
    { 0.20, 0.75, 0.80 },
    { 0.90, 0.55, 0.25 },
    { 0.45, 0.70, 0.95 },
    { 0.85, 0.35, 0.60 },
    { 0.55, 0.80, 0.35 },
    { 0.70, 0.55, 0.35 },
    { 0.50, 0.40, 0.70 },
}

local _dungeonAccentByName = {}
local function GenerateDungeonAccent(mapID, name)
    if name and _dungeonAccentByName[name] then
        return _dungeonAccentByName[name]
    end
    local idx = (mapID % #ACCENT_PALETTE) + 1
    local accent = ACCENT_PALETTE[idx]
    if name then
        _dungeonAccentByName[name] = accent
    end
    return accent
end

local KNOWN_ABBREVS = {
    ["Magisters' Terrace"]          = "MT",
    ["Maisara Caverns"]             = "MC",
    ["Nexus-Point Xenas"]           = "NPX",
    ["Windrunner Spire"]            = "WS",
    ["Den of Nalorakk"]             = "DoN",
    ["The Blinding Vale"]           = "BV",
    ["Murder Row"]                  = "MR",
    ["Voidscar Arena"]              = "VA",
    ["Algeth'ar Academy"]           = "AA",
    ["The Seat of the Triumvirate"] = "SotT",
    ["Skyreach"]                    = "SR",
    ["Pit of Saron"]                = "PoS",
}

local function ExtractKeyLevel(name)
    if not name then return nil end
    local level = tonumber(name:match("%+(%d+)"))
    if level and level >= 2 and level <= 40 then return level end
    level = tonumber(name:match("[Mm]ythic%+?%s*(%d+)"))
    if level and level >= 2 and level <= 40 then return level end
    level = tonumber(name:match("^(%d+)"))
    if level and level >= 2 and level <= 40 then return level end
    return nil
end

local function ResolveActivityName(info)
    if info._activityName then return info._activityName end
    local actIDs = info.activityIDs
    local resolvedID = type(actIDs) == "number" and actIDs or (type(actIDs) == "table" and actIDs[1])
    if resolvedID and C_LFGList and C_LFGList.GetActivityInfoTable then
        local aOk, aInfo = pcall(C_LFGList.GetActivityInfoTable, resolvedID)
        if aOk and aInfo then
            if aInfo.fullName and aInfo.fullName ~= "" then
                info._activityName = aInfo.fullName
                return aInfo.fullName
            end
            if aInfo.shortName and aInfo.shortName ~= "" then
                info._activityName = aInfo.shortName
                return aInfo.shortName
            end
        end
    end
    info._activityName = info.name or ""
    return info._activityName
end

local function ResolveDungeonBrand(info)
    if info._dungeonBrand then return info._dungeonBrand end
    local shortName, fullName
    local actIDs = info.activityIDs
    local resolvedID = type(actIDs) == "number" and actIDs or (type(actIDs) == "table" and actIDs[1])
    if resolvedID and C_LFGList and C_LFGList.GetActivityInfoTable then
        local aOk, aInfo = pcall(C_LFGList.GetActivityInfoTable, resolvedID)
        if aOk and aInfo then
            shortName = aInfo.shortName
            fullName  = aInfo.fullName
        end
    end
    local dungName
    if fullName and fullName ~= "" then
        dungName = fullName:gsub("%s*%([^)]*%)%s*$", "")
    end
    if not dungName or dungName == "" then
        if shortName and shortName ~= ""
           and shortName ~= "Mythic+" and shortName ~= "Mythic Keystone"
           and shortName ~= "Mythic+ Keystone" then
            dungName = shortName
        end
    end
    if not dungName or dungName == "" then dungName = info.name or "Unknown" end
    local abbr = KNOWN_ABBREVS[dungName]
    if not abbr then abbr = GenerateAbbreviation(dungName) end
    info._dungeonBrand = { name = dungName, abbr = abbr }
    return info._dungeonBrand
end

-- Per-category cache of selectable LFG activities, used to power the
-- search-box autocomplete. Activity metadata (full names like
-- "Windrunner Spire (Mythic Keystone)") is static for the session and
-- freely readable by addons, so we build each category's list lazily
-- once and reuse it. Returns a sorted array of { id, full, lower } or nil.
local _activityCacheByCat = {}
local function GetCategoryActivities(categoryID)
    if not categoryID then return nil end
    local cached = _activityCacheByCat[categoryID]
    if cached then return cached end
    if not (C_LFGList and C_LFGList.GetAvailableActivities and C_LFGList.GetActivityInfoTable) then
        return nil
    end
    local okIDs, ids = pcall(C_LFGList.GetAvailableActivities, categoryID)
    if not okIDs or type(ids) ~= "table" then return nil end
    local list = {}
    for _, id in ipairs(ids) do
        local okInfo, info = pcall(C_LFGList.GetActivityInfoTable, id)
        if okInfo and info then
            local full = info.fullName
            if type(full) == "string" and full ~= "" then
                list[#list + 1] = { id = id, full = full, lower = full:lower() }
            end
        end
    end
    table.sort(list, function(a, b) return a.full < b.full end)
    _activityCacheByCat[categoryID] = list
    return list
end

-- Raid difficulty keyed off the listing's activity name (e.g. "Liberation of
-- Undermine (Heroic)"). Difficulty is the dimension Blizzard's own raid finder
-- exposes through activity selection. Returns "LFR"/"Normal"/"Heroic"/"Mythic"
-- or nil when it can't be determined.
local function ResolveRaidDifficulty(info)
    local name = (ResolveActivityName(info) or "")
    -- Prefer the trailing "(...)" qualifier, fall back to the whole string.
    local tail = name:match("%(([^)]+)%)%s*$") or name
    local low = tail:lower()
    if low:find("raid finder", 1, true) or low:find("looking for raid", 1, true)
       or low:find("lfr", 1, true) then
        return "LFR"
    elseif low:find("heroic", 1, true) then
        return "Heroic"
    elseif low:find("mythic", 1, true) then
        return "Mythic"
    elseif low:find("normal", 1, true) then
        return "Normal"
    elseif low:find("player", 1, true) or low:find("flex", 1, true) then
        -- Legacy raids: "10/25 Player" and "Flexible" map to Normal.
        return "Normal"
    end
    return nil
end

local function FormatTimeAgo(seconds)
    if not seconds or seconds < 0 then return "" end
    if seconds < 60 then return "<1m" end
    local mins = math.floor(seconds / 60)
    if mins < 60 then return mins .. "m" end
    return math.floor(mins / 60) .. "h"
end

local function TrySetAtlasFromList(tex, atlasList)
    if not tex or not atlasList then return false end
    for _, atlasName in ipairs(atlasList) do
        local ok = pcall(tex.SetAtlas, tex, atlasName, false)
        if ok then return true end
    end
    return false
end

local HERO_ATLASES = {
    dungeons = {
        "groupfinder-background-dungeons",
        "completiondialog-midnightcampaign-background",
    },
    raids = {
        "UI-EJ-BOSS-Default",
        "groupfinder-background-raids",
        "completiondialog-midnightcampaign-background",
    },
    premade = {
        "groupfinder-background-quests",
        "groupfinder-background-arena",
        "completiondialog-midnightcampaign-background",
    },
}

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 },
        heroBg      = { 0.06, 0.05, 0.03, 0.85 },
        glassBg     = { 0.04, 0.04, 0.06, 0.70 },
        glassBorder = { 1, 1, 1, 0.04 },
        surfaceBg   = { 0.06, 0.055, 0.045, 0.55 },
        shadowColor = { 0, 0, 0, 0.12 },
        hoverBg     = { 0.72, 0.62, 0.42, 0.06 },
        activeBg    = { 0.72, 0.62, 0.42, 0.10 },
        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 },
        heroBg      = { 0.035, 0.045, 0.08, 0.85 },
        glassBg     = { 0.03, 0.04, 0.08, 0.70 },
        glassBorder = { 1, 1, 1, 0.03 },
        surfaceBg   = { 0.05, 0.06, 0.10, 0.55 },
        shadowColor = { 0, 0, 0, 0.15 },
        hoverBg     = { 0.00, 0.78, 1.00, 0.06 },
        activeBg    = { 0.00, 0.78, 1.00, 0.10 },
        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 },
        heroBg      = { 0.06, 0.05, 0.03, 0.85 },
        glassBg     = { 0.04, 0.04, 0.06, 0.70 },
        glassBorder = { 1, 1, 1, 0.04 },
        surfaceBg   = { 0.06, 0.055, 0.045, 0.55 },
        shadowColor = { 0, 0, 0, 0.12 },
        hoverBg     = { 0.72, 0.62, 0.42, 0.06 },
        activeBg    = { 0.72, 0.62, 0.42, 0.10 },
        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 },
    },
}

activeTheme = THEMES.parchment

GetActiveTheme = function()
    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
            local ar = cc.r or cc[1] or 0.72
            local ag = cc.g or cc[2] or 0.62
            local ab = cc.b or cc[3] or 0.42
            t.accent  = { ar, ag, ab }
            t.hoverBg = { ar, ag, ab, 0.06 }
            t.activeBg = { ar, ag, ab, 0.10 }
        end
    end
    activeTheme = t
    return t
end

TC = function(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           = 1200,
    HEIGHT          = 780,
    HEADER_H        = 44,
    TAB_BAR_H       = 32,
    STRATA          = "HIGH",
    PAD             = 16,
    SCORE_CAP       = 3000,
    CAT_SIDEBAR_W   = 160,
    PLAYER_SIDEBAR_W = 260,
    FILTER_BAR_H    = 36,
    ROW_H           = 68,
    ROW_POOL_SIZE   = 20,
    DETAIL_PANEL_H  = 300,
}

local Panel = {}
Panel._state = {
    panelOpen       = false,
    activeTab       = "premade",
    activeCategory  = "mythicplus",
    filters         = { dungeon = nil, role = nil, minMPI = nil, searchText = nil },
    -- Raids get their own filter set (difficulty), kept separate from the
    -- Mythic+ filters so the two never bleed into each other.
    raidFilters     = { difficulty = {}, searchText = "" },
    delveFilters    = { searchText = "" },
    searchResults   = {},
    detailOpen      = false,
    prevAppStatuses = {},
    pvpCategory     = "quickmatch",
    pvpRoles        = { tank = false, healer = false, dps = false },
    pvpSelectedActivity = nil,
}
Panel._refs = {}

StartAutoRefresh = function() end
StopAutoRefresh  = function() end

local mpiCache = {}
local MPI_CACHE_TTL = 60

local function GetCachedMPIProfile(name)
    if not name or name == "" then return nil end
    local cached = mpiCache[name]
    if cached and (GetTime() - cached.time) < MPI_CACHE_TTL then
        return cached.profile
    end
    local profile = nil
    local PI = _G.MidnightPI
    if PI then
        profile = PI.GetProfileByName and PI.GetProfileByName(name)
        if not profile and PI.CommunityLookup then
            local pName, pRealm = name:match("^([^%-]+)%-(.+)$")
            if not pName then
                pName = name
                pRealm = GetRealmName and GetRealmName() or ""
            end
            profile = PI.CommunityLookup(pName, pRealm)
        end
    end
    mpiCache[name] = { profile = profile, time = GetTime() }
    return profile
end

local function GetMPIScoreColor(score)
    if not score then return 0.5, 0.5, 0.5 end
    local PI = _G.MidnightPI
    if PI and PI.GetScoreTier then
        local _, r, g, b = PI.GetScoreTier(score)
        return r, g, b
    end
    return 0.5, 0.5, 0.5
end


local function BuildNameToLFGID()
    local map = {}
    if not GetLFGDungeonInfo then return map end

    local CURRENT_EXPANSION = (GetExpansionLevel and GetExpansionLevel()) or 11

    local function PriorityFor(subtypeID, difficulty)
        if subtypeID == 2 and difficulty == 2 then return 2 end
        if subtypeID == 1 and difficulty == 1 then return 4 end
        return nil
    end

    local candidates = {}
    for id = 1, 5000 do
        local ok, name, _, subtypeID, _, _, _, _, _,
              expansionLevel, _, _, difficulty = pcall(GetLFGDungeonInfo, id)
        if ok and name and name ~= "" and expansionLevel == CURRENT_EXPANSION then
            local prio = PriorityFor(subtypeID, difficulty)
            if prio then
                candidates[name] = candidates[name] or {}
                table.insert(candidates[name], { id = id, prio = prio })
            end
        end
    end

    for name, list in pairs(candidates) do
        local bestPrio, bestID = math.huge, nil
        for _, c in ipairs(list) do
            local jok, joinable = false, false
            if IsLFGDungeonJoinable then
                jok, joinable = pcall(IsLFGDungeonJoinable, c.id)
            end
            if jok and joinable == true then
                if c.prio < bestPrio or (c.prio == bestPrio and (not bestID or c.id > bestID)) then
                    bestPrio = c.prio
                    bestID = c.id
                end
            end
        end
        if bestID then map[name] = bestID end
    end
    return map
end

-- Returns the current season's Mythic Keystone dungeons (8 of them in
-- a typical season). Source: C_ChallengeMode.GetMapTable, which Blizzard
-- updates each season cycle. Falls back to an empty list on early load
-- before the M+ data is available.
local function LoadSeasonDungeons()
    local dungeons = {}
    if not (C_ChallengeMode and C_ChallengeMode.GetMapTable) then return dungeons end

    local ok, maps = pcall(C_ChallengeMode.GetMapTable)
    if not ok or type(maps) ~= "table" then return dungeons end

    for _, mapID in ipairs(maps) do
        local infoOk, name = pcall(C_ChallengeMode.GetMapUIInfo, mapID)
        if infoOk and type(name) == "string" and name ~= "" then
            local abbr   = KNOWN_ABBREVS[name] or GenerateAbbreviation(name)
            local accent = GenerateDungeonAccent(mapID, name)
            dungeons[#dungeons + 1] = {
                mapID          = mapID,
                name           = name,
                abbr           = abbr,
                accent         = accent,
                lfgDungeonID   = nil,  -- not applicable: M+ doesn't queue via LFG
                expansionLevel = 0,    -- unused for M+ rendering
            }
        end
    end

    table.sort(dungeons, function(a, b) return a.name < b.name end)

    if #dungeons == 0 then
        dungeons[1] = { mapID = 0, name = "No Dungeons Available", abbr = "--", accent = {0.5, 0.5, 0.5} }
    end
    return dungeons
end

local function BuildDungeonDropdownList(dungeons)
    local list = { "All Dungeons" }
    for _, d in ipairs(dungeons) do
        list[#list + 1] = d.name
    end
    return list
end

-- ============================================================
-- Mythic+ dungeon teleports.
-- Teleport spells keyed by challenge-mode map ID. Sourced from the
-- community "DungeonTeleport" addon's maintained table (covers Dragonflight
-- through Midnight). We only ever surface the ones whose map ID appears in
-- the live season list (C_ChallengeMode.GetMapTable), so the dropdown always
-- matches the current rotation without per-season edits here.
-- ============================================================
local MPLUS_TELEPORT_SPELLS = {
    -- Dragonflight
    [2]={131204},[165]={159899},[200]={393764},[210]={393766},[399]={393256},
    [400]={393262},[401]={393279},[402]={393273},[206]={410078},[245]={410071},
    [251]={410074},[403]={393222},[404]={393276},[405]={393267},[406]={393283},
    [438]={410080},[168]={159901},[198]={424163},[199]={424153},[244]={424187},
    [248]={424167},[456]={424142},[463]={424197},[464]={424197},
    -- The War Within
    [501]={445269},[376]={354462},[505]={445414},[353]={464256},[375]={354464},
    [507]={445424},[502]={445416},[503]={445417},[525]={1216786},[500]={445443},
    [247]={467555},[370]={373274},[382]={354467},[499]={445444},[504]={445441},
    [506]={445440},[378]={354465},[391]={367416},[392]={367416},[542]={1237215},
    -- Midnight Season 1
    [557]={1254400},[161]={159898},[239]={1254551},[556]={1254555},[559]={1254563},
    [560]={1254559},[558]={1254572},
}
-- Dungeons whose teleport differs by faction: { allianceSpellID, hordeSpellID }.
local MPLUS_TELEPORT_FACTION = {
    [353]={445418, 464256}, -- Siege of Boralus
    [247]={467553, 467555}, -- The MOTHERLODE!!
}

local function TeleportSpellExists(spellID)
    if not (spellID and C_Spell and C_Spell.GetSpellInfo) then return false end
    local ok, info = pcall(C_Spell.GetSpellInfo, spellID)
    return ok and info ~= nil
end

local function IsTeleportKnown(spellID)
    if not spellID then return false end
    if type(IsSpellKnown) == "function" then
        local ok, known = pcall(IsSpellKnown, spellID)
        if ok and known == true then return true end
    end
    if type(IsPlayerSpell) == "function" then
        local ok, known = pcall(IsPlayerSpell, spellID)
        if ok and known == true then return true end
    end
    return false
end

-- Resolve the teleport spell for a dungeon map, honouring faction overrides
-- and preferring a spell the player actually knows.
local function ResolveTeleportSpell(mapID)
    if not mapID then return nil end
    local list = MPLUS_TELEPORT_SPELLS[mapID]
    local faction = MPLUS_TELEPORT_FACTION[mapID]
    if faction then
        local isAlliance = (UnitFactionGroup and UnitFactionGroup("player")) == "Alliance"
        local fSpell = isAlliance and faction[1] or faction[2]
        if fSpell then list = { fSpell } end
    end
    if not list then return nil end
    local firstValid
    for _, spellID in ipairs(list) do
        if TeleportSpellExists(spellID) then
            if IsTeleportKnown(spellID) then return spellID end
            firstValid = firstValid or spellID
        end
    end
    return firstValid
end

-- Remaining teleport cooldown in seconds (0 = ready). Ignores the ~1.5s GCD.
local function GetTeleportCooldown(spellID)
    if not spellID then return 0 end
    local start, duration
    if C_Spell and C_Spell.GetSpellCooldown then
        local ok, cd = pcall(C_Spell.GetSpellCooldown, spellID)
        if ok and type(cd) == "table" then start, duration = cd.startTime, cd.duration end
    end
    if start == nil and type(GetSpellCooldown) == "function" then
        local ok, s, d = pcall(GetSpellCooldown, spellID)
        if ok then start, duration = s, d end
    end
    if not start or not duration or duration <= 1.5 then return 0 end
    local remaining = (start + duration) - GetTime()
    return remaining > 0 and remaining or 0
end

local function FormatTeleportTime(seconds)
    seconds = math.floor((seconds or 0) + 0.5)
    if seconds <= 0 then return "" end
    local h = math.floor(seconds / 3600)
    local m = math.floor((seconds % 3600) / 60)
    if h > 0 then
        if m > 0 then return string.format("%d Hour%s %d Minute%s", h, h == 1 and "" or "s", m, m == 1 and "" or "s") end
        return string.format("%d Hour%s", h, h == 1 and "" or "s")
    end
    if m > 0 then return string.format("%d Minute%s", m, m == 1 and "" or "s") end
    return string.format("%d Second%s", seconds, seconds == 1 and "" or "s")
end

local function GetTeleportTexture(spellID)
    if spellID and C_Spell and C_Spell.GetSpellTexture then
        local ok, tex = pcall(C_Spell.GetSpellTexture, spellID)
        if ok and tex then return tex end
    end
    return "Interface\\Icons\\Spell_Arcane_PortalDalaran"
end

local function GetTeleportName(spellID)
    if spellID and C_Spell and C_Spell.GetSpellInfo then
        local ok, info = pcall(C_Spell.GetSpellInfo, spellID)
        if ok and info and type(info.name) == "string" and info.name ~= "" then
            return info.name
        end
    end
    return nil
end

local function LoadHeroicDungeons()
    local dungeons = {}
    local requiredGearLevel = 0
    if not GetLFGDungeonInfo then return dungeons, requiredGearLevel end

    local cmNames = {}
    if C_ChallengeMode and C_ChallengeMode.GetMapTable then
        local ok, maps = pcall(C_ChallengeMode.GetMapTable)
        if ok and maps then
            for _, mapID in ipairs(maps) do
                local ok2, name = pcall(C_ChallengeMode.GetMapUIInfo, mapID)
                if ok2 and name and name ~= "" then
                    cmNames[name] = true
                end
            end
        end
    end

    local seen = {}
    local playerLevel = UnitLevel and UnitLevel("player") or 0

    -- Derive the expected heroic expansion from Blizzard's own joinable
    -- heroic-random pool (self-updating when new expansions ship).
    local expectedExp
    if GetNumRandomDungeons and GetLFGRandomDungeonInfo and GetLFGDungeonInfo then
        local n = GetNumRandomDungeons()
        for i = 1, n do
            local rid = GetLFGRandomDungeonInfo(i)
            if rid then
                local okI, rname, _, rsub, _, _, _, _, _, rexp, _, _, rdiff = pcall(GetLFGDungeonInfo, rid)
                local isTW = rname and string.find(rname, "Timewalking") ~= nil or false
                local isTR = rname and string.find(rname, "Timerunning") ~= nil or false
                if okI and rname and rsub == 2 and rdiff == 2 and not isTW and not isTR
                   and type(rexp) == "number" and rexp > 0 then
                    local joinable = false
                    if IsLFGDungeonJoinable then
                        local okJ, j = pcall(IsLFGDungeonJoinable, rid)
                        joinable = okJ and j or false
                    end
                    if joinable then
                        expectedExp = rexp
                        break
                    end
                end
            end
        end
    end
    if not expectedExp then
        if playerLevel >= 90 then expectedExp = 11
        elseif playerLevel >= 80 then expectedExp = 10
        elseif playerLevel >= 70 then expectedExp = 9
        end
    end
    if not expectedExp then
        return dungeons, requiredGearLevel
    end

    for id = 1, 5000 do
        local t = { pcall(GetLFGDungeonInfo, id) }
        local ok = t[1]
        local name, typeID, subtypeID, expansionLevel, difficulty, maxPlayers, instanceMapID, minGearLevel =
              t[2], t[3], t[4], t[10], t[13], t[14], t[22], t[21]
        if ok and name and name ~= ""
           and typeID == 1
           and subtypeID == 2 and difficulty == 2
           and maxPlayers == 5
           and type(expansionLevel) == "number" and expansionLevel > 0
           and expansionLevel == expectedExp
           and not string.find(name, "^Random ") then
            if type(minGearLevel) == "number" and minGearLevel > 0 then
                if requiredGearLevel == 0 or minGearLevel < requiredGearLevel then
                    requiredGearLevel = minGearLevel
                end
            end
            local joinable = true
            if IsLFGDungeonJoinable then
                local okJ, j = pcall(IsLFGDungeonJoinable, id)
                joinable = okJ and j or false
            end
            if joinable and not seen[name] then
                seen[name] = true
                local safeMapID = (type(instanceMapID) == "number") and instanceMapID or 0
                local abbr = KNOWN_ABBREVS[name] or GenerateAbbreviation(name)
                local accent = GenerateDungeonAccent(safeMapID, name)
                dungeons[#dungeons + 1] = {
                    mapID          = safeMapID,
                    name           = name,
                    abbr           = abbr,
                    accent         = accent,
                    lfgDungeonID   = id,
                    expansionLevel = (type(expansionLevel) == "number") and expansionLevel or 0,
                }
            end
        end
    end
    return dungeons, requiredGearLevel
end

local function LoadFollowerDungeons()
    local dungeons = {}
    if not C_LFGInfo or not C_LFGInfo.IsLFGFollowerDungeon then return dungeons end
    if not GetLFGDungeonInfo then return dungeons end

    local seen = {}
    for id = 1, 5000 do
        local ok, isFollower = pcall(C_LFGInfo.IsLFGFollowerDungeon, id)
        if ok and isFollower then
            local ok2, name, _, _,
                  minLevel, maxLevel, _, _, _,
                  expansionLevel, _, _, difficulty, _, _, _, _, _, _, _,
                  instanceMapID = pcall(GetLFGDungeonInfo, id)
            local joinable = true
            if IsLFGDungeonJoinable then
                local okJ, j = pcall(IsLFGDungeonJoinable, id)
                joinable = okJ and j or false
            end
            if ok2 and name and name ~= "" and difficulty == 205 and joinable then
                if not seen[name] then
                    seen[name] = #dungeons + 1
                    dungeons[#dungeons + 1] = {
                        dungeonID  = id,
                        mapID      = instanceMapID or id,
                        name       = name,
                        abbr       = KNOWN_ABBREVS[name] or GenerateAbbreviation(name),
                        accent     = GenerateDungeonAccent(instanceMapID or id, name),
                        isFollower = true,
                        expansion  = expansionLevel,
                        minLevel   = minLevel or 0,
                        maxLevel   = maxLevel or 0,
                    }
                else
                    local idx = seen[name]
                    dungeons[idx].dungeonID = id
                    dungeons[idx].mapID     = instanceMapID or id
                    dungeons[idx].accent    = GenerateDungeonAccent(instanceMapID or id, name)
                    dungeons[idx].expansion = expansionLevel
                    dungeons[idx].minLevel  = minLevel or 0
                    dungeons[idx].maxLevel  = maxLevel or 0
                end
            end
        end
    end

    table.sort(dungeons, function(a, b) return a.name < b.name end)

    return dungeons
end

local function CollectRandomExpansionEntries()
    local outHeroic, outNormal = {}, {}
    if not GetNumRandomDungeons or not GetLFGRandomDungeonInfo or not GetLFGDungeonInfo then
        return outHeroic, outNormal
    end
    local n = GetNumRandomDungeons()
    for i = 1, n do
        local id = GetLFGRandomDungeonInfo(i)
        if id then
            local okInfo, name, typeID, sub, _, _, _, _, _,
                  _, _, _, diff, maxPlayers = pcall(GetLFGDungeonInfo, id)
            local isTW = name and string.find(name, "Timewalking") ~= nil or false
            local isFollower = false
            if C_LFGInfo and C_LFGInfo.IsLFGFollowerDungeon then
                local okF, f = pcall(C_LFGInfo.IsLFGFollowerDungeon, id)
                if okF and f then isFollower = true end
            end
            local joinable = false
            if IsLFGDungeonJoinable then
                local okJ, j = pcall(IsLFGDungeonJoinable, id)
                joinable = okJ and j or false
            end
            local isTimerunning = name and string.find(name, "Timerunning") ~= nil or false
            if okInfo and name and name ~= "" and maxPlayers == 5
               and (typeID == 1 or typeID == 6)
               and joinable and not isTW and not isFollower and not isTimerunning then
                local _, _, _, _, _, _, _, _, _, expLvl = pcall(GetLFGDungeonInfo, id)
                local bucket = (diff == 2 or sub == 2) and outHeroic or outNormal
                bucket[#bucket + 1] = { lfgDungeonID = id, name = name, expansionLevel = (type(expLvl) == "number") and expLvl or 0 }
            end
        end
    end
    return outHeroic, outNormal
end

local function FindTimewalkingRandomDungeon()
    if not GetLFGDungeonInfo then return nil end
    if not GetNumRandomDungeons or not GetLFGRandomDungeonInfo then return nil end
    local numRandom = GetNumRandomDungeons()
    for i = 1, numRandom do
        local id = GetLFGRandomDungeonInfo(i)
        if id then
            local ok, name = pcall(GetLFGDungeonInfo, id)
            if ok and name and name:find("Timewalking Dungeon") then
                if IsLFGDungeonJoinable then
                    local okJ, joinable = pcall(IsLFGDungeonJoinable, id)
                    if okJ and joinable then
                        return { dungeonID = id, name = name }
                    end
                end
            end
        end
    end
    return nil
end


-- Pull the player's MPI rating for their CURRENT spec from the
-- web-overlay table (Companion writes it). Returns the role's
-- progression+coverage adjusted score so it matches mpi.atyzi.com.
-- Falls back to the most-played role if the player hasn't run keys
-- in their current spec yet, with a flag so the UI can hint at that.
-- Returns score=0 when no overlay / no roles at all.
local function GetPlayerMPIScoreData()
    local data = {
        score        = 0,
        scoreColor   = { 0.5, 0.5, 0.5 },
        role         = nil,           -- TANK / HEALER / DPS / AUG, nil if no data
        roleLabel    = nil,           -- "Tank" / "Healer" / etc., for UI
        isActiveRole = false,         -- true when matched to current spec
        bestMPI      = 0,
        verifiedPeak = 0,
        peakIsVerified = false,
    }
    local PI = _G.MidnightPI
    if not PI or not PI.GetActiveRoleScore then return data end
    local active, fallback = PI.GetActiveRoleScore()
    local pick = active or fallback
    if not pick then return data end

    local function roleLabel(r)
        if r == "TANK"   then return "Tank" end
        if r == "HEALER" then return "Healer" end
        if r == "AUG"    then return "Aug" end
        return "DPS"
    end

    data.score          = pick.overallMPI or 0
    data.role           = pick.role
    data.roleLabel      = roleLabel(pick.role)
    data.isActiveRole   = (active ~= nil)
    data.bestMPI        = pick.bestMPI or 0
    data.verifiedPeak   = pick.verifiedPeak or 0
    data.peakIsVerified = pick.peakIsVerified == true

    local r, g, b = GetMPIScoreColor(data.score)
    data.scoreColor = { r, g, b }
    return data
end

local function GetPlayerMPlusData()
    local data = {
        overallScore = 0,
        scoreColor   = { 0.6, 0.6, 0.6 },
        dungeonBest  = {},
        keystoneMapID = nil,
        keystoneLevel = nil,
        keystoneName  = nil,
    }
    if C_ChallengeMode and C_ChallengeMode.GetOverallDungeonScore then
        local ok, score = pcall(C_ChallengeMode.GetOverallDungeonScore)
        if ok and score and score > 0 then
            data.overallScore = score
            if C_ChallengeMode.GetDungeonScoreRarityColor then
                local ok2, color = pcall(C_ChallengeMode.GetDungeonScoreRarityColor, score)
                if ok2 and color and color.r then
                    data.scoreColor = { color.r, color.g, color.b }
                end
            end
        end
    end
    if C_MythicPlus and C_MythicPlus.GetSeasonBestAffixScoreInfoForMap then
        local maps = C_ChallengeMode and C_ChallengeMode.GetMapTable and SafeCall(C_ChallengeMode.GetMapTable) or {}
        for _, mapID in ipairs(maps or {}) do
            local ok, affixes = pcall(C_MythicPlus.GetSeasonBestAffixScoreInfoForMap, mapID)
            if ok and affixes then
                local best = 0
                for _, info in ipairs(affixes) do
                    if info.level and info.level > best then best = info.level end
                end
                if best > 0 then data.dungeonBest[mapID] = best end
            end
        end
    end
    if next(data.dungeonBest) == nil and C_MythicPlus and C_MythicPlus.GetRunHistory then
        local ok, runs = pcall(C_MythicPlus.GetRunHistory, true, true)
        if ok and runs then
            for _, run in ipairs(runs) do
                local mid = run.mapChallengeModeID
                local lvl = run.level
                if mid and lvl then
                    if not data.dungeonBest[mid] or lvl > data.dungeonBest[mid] then
                        data.dungeonBest[mid] = lvl
                    end
                end
            end
        end
    end
    -- Owned keystone lookup. The keystone item in the player's bag is the
    -- source of truth — its |Hkeystone:itemID:mapID:level:...| link carries
    -- the correct Challenge Mode mapID and current level. The C_MythicPlus
    -- getters return stale/wrong mapIDs in some patches, so we only use
    -- them as a fallback when no keystone item is present.
    local mapID, level

    local container = C_Container
    local getSlots  = container and container.GetContainerNumSlots
    local getLink   = container and container.GetContainerItemLink
    if getSlots and getLink then
        for bag = 0, 5 do
            local okN, slots = pcall(getSlots, bag)
            if okN and (slots or 0) > 0 then
                for slot = 1, slots do
                    local okL, link = pcall(getLink, bag, slot)
                    if okL and type(link) == "string" then
                        local mID, lvl = link:match("|Hkeystone:%d+:(%d+):(%d+):")
                        if not mID then
                            mID, lvl = link:match("|Hitem:180653:[^|]-:(%d+):(%d+):")
                        end
                        if mID and lvl then
                            mapID = tonumber(mID)
                            level = tonumber(lvl)
                            break
                        end
                    end
                end
            end
            if mapID and level then break end
        end
    end

    -- API fallback only if nothing in bags
    if (not mapID or not level) and C_MythicPlus then
        if C_MythicPlus.GetOwnedKeystoneInfo then
            local ok, m, l = pcall(C_MythicPlus.GetOwnedKeystoneInfo)
            if ok then mapID, level = mapID or m, level or l end
        end
        if (not mapID) and C_MythicPlus.GetOwnedKeystoneMapID then
            local okM, m = pcall(C_MythicPlus.GetOwnedKeystoneMapID)
            if okM then mapID = m end
        end
        if (not mapID) and C_MythicPlus.GetOwnedKeystoneChallengeMapID then
            local okC, m = pcall(C_MythicPlus.GetOwnedKeystoneChallengeMapID)
            if okC then mapID = m end
        end
        if (not level) and C_MythicPlus.GetOwnedKeystoneLevel then
            local okL, l = pcall(C_MythicPlus.GetOwnedKeystoneLevel)
            if okL then level = l end
        end
    end

    if mapID and level and mapID > 0 and level > 0 then
        data.keystoneMapID = mapID
        data.keystoneLevel = level
        if C_ChallengeMode and C_ChallengeMode.GetMapUIInfo then
            local ok2, name = pcall(C_ChallengeMode.GetMapUIInfo, mapID)
            if ok2 and type(name) == "string" and name ~= "" then
                data.keystoneName = name
            end
        end
        if not data.keystoneName then
            for _, d in ipairs(Panel._dungeonData or {}) do
                if d.mapID == mapID and d.name then
                    data.keystoneName = d.name
                    break
                end
            end
        end
        if not data.keystoneName then
            data.keystoneName = "Keystone"
        end
    end
    return data
end

local function DeriveRaidTierName()
    local ok, numRF = pcall(GetNumRFDungeons)
    if ok and numRF and numRF > 0 then
        local ok2, _, name = pcall(GetRFDungeonInfo, 1)
        if ok2 and name then
            local tierName = name:match("^(.+):%s") or name:match("^(.+) -") or name
            return tierName
        end
    end
    return "Current Raid Tier"
end

local function FindWeakestDungeon(dungeons, dungeonBest)
    local weakest, weakestLevel = nil, 999
    for _, d in ipairs(dungeons) do
        local lvl = dungeonBest[d.mapID] or 0
        if lvl < weakestLevel then
            weakestLevel = lvl
            weakest = d
        end
    end
    return weakest, weakestLevel
end

local function GetRaidBossProgress()
    local R = Panel._refs
    local wings = {}
    local totalBosses, totalKilled = 0, 0

    if R and R.raidWingCards then
        for _, wcard in ipairs(R.raidWingCards) do
            if wcard:IsShown() and wcard._wingID then
                local id = wcard._wingID
                local name = wcard._nameText and wcard._nameText:GetText() or "Unknown"
                local wing = { id = id, name = name, bosses = {}, killed = 0, total = 0 }
                local ok3, numEnc = pcall(GetLFGDungeonNumEncounters, id)
                if ok3 and numEnc and numEnc > 0 then
                    wing.total = numEnc
                    for e = 1, numEnc do
                        local ok4, eName, eID, isKilled = pcall(GetLFGDungeonEncounterInfo, id, e)
                        wing.bosses[#wing.bosses + 1] = { name = eName, killed = isKilled }
                        if ok4 and isKilled then wing.killed = wing.killed + 1 end
                    end
                end
                totalBosses = totalBosses + wing.total
                totalKilled = totalKilled + wing.killed
                wings[#wings + 1] = wing
            end
        end
    end
    return wings, totalBosses, totalKilled
end

local DELVES_CATEGORY_ID = 121  -- well-known Delves group-finder category id

-- The LFG category list is normally empty at addon-load, so resolve the
-- Delves category lazily at search time instead. Prefer the known id 121;
-- only if the server genuinely doesn't offer it, identify Delves by its
-- activities (whose full names carry a "(Tier N)" suffix). Never falls back
-- to a blind "first high id" guess, which could pick an unrelated category
-- and return zero results.
local _delvesCatResolved = false
local function ResolveDelvesCategoryID()
    if _delvesCatResolved then return DELVES_CATEGORY_ID end
    if not (C_LFGList and C_LFGList.GetAvailableCategories) then
        return DELVES_CATEGORY_ID
    end
    local ok, cats = pcall(C_LFGList.GetAvailableCategories)
    if not ok or type(cats) ~= "table" or #cats == 0 then
        return DELVES_CATEGORY_ID  -- not loaded yet; retry on the next search
    end
    for _, cid in ipairs(cats) do
        if cid == 121 then
            _delvesCatResolved = true
            return DELVES_CATEGORY_ID
        end
    end
    if C_LFGList.GetAvailableActivities and C_LFGList.GetActivityInfoTable then
        for _, cid in ipairs(cats) do
            local aok, acts = pcall(C_LFGList.GetAvailableActivities, cid)
            if aok and type(acts) == "table" then
                for _, aid in ipairs(acts) do
                    local iok, info = pcall(C_LFGList.GetActivityInfoTable, aid)
                    if iok and info and type(info.fullName) == "string"
                       and info.fullName:find("[Tt]ier%s*%d") then
                        DELVES_CATEGORY_ID = cid
                        _delvesCatResolved = true
                        return cid
                    end
                end
            end
        end
    end
    return DELVES_CATEGORY_ID
end

local CATEGORY_DEFS = {
    { key = "mythicplus", label = "Mythic+",        categoryID = 2,  isMPlus = true },
    { key = "delves",     label = "Delves",          categoryID = DELVES_CATEGORY_ID, isDelves = true },
    { key = "raids",      label = "Raids",           categoryID = 3 },
    { key = "legacy",     label = "Raids (Legacy)",  categoryID = 3,  isLegacy = true },
    { key = "questing",   label = "Questing",        categoryID = 1 },
    { key = "custom",     label = "Custom",          categoryID = 6 },
}

local CATEGORY_ACCENTS = {
    mythicplus = { 0.00, 0.78, 1.00 },
    delves     = { 0.85, 0.65, 0.20 },
    raids      = { 0.80, 0.30, 0.30 },
    legacy     = { 0.55, 0.40, 0.70 },
    questing   = { 0.35, 0.75, 0.40 },
    custom     = { 0.55, 0.55, 0.55 },
}

local TAB_DEFS = {
    { key = "dungeons", label = "DUNGEON OVERVIEW" },
    { key = "raids",    label = "RAID FINDER" },
    { key = "premade",  label = "PREMADE GROUPS" },
    { key = "pvp",      label = "PVP" },
}


local function FitPillToContent(pill, minW, maxW)
    if not pill or not pill._value then return end
    local labelW = (pill._label and pill._label.GetStringWidth and pill._label:GetStringWidth()) or 0
    local valueW = pill._value.GetStringWidth and pill._value:GetStringWidth() or 0
    local needed = 4 + 4 + 6 + labelW + 10 + valueW + 8
    if minW and needed < minW then needed = minW end
    if maxW and needed > maxW then needed = maxW end
    pill:SetWidth(math.floor(needed + 0.5))
end

local function CreateStatPill(parent, width, height, xOfs, yOfs, acColor)
    local pill = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    pill:SetSize(width, height)
    pill:SetPoint("TOPLEFT", parent, "TOPLEFT", xOfs, yOfs)
    pill:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    pill:SetBackdropColor(TC("surfaceBg"))
    pill:SetBackdropBorderColor(0, 0, 0, 0)
    local ac = acColor or { TC("accent") }

    local dot = pill:CreateTexture(nil, "OVERLAY", nil, 2)
    dot:SetSize(4, height - 8)
    dot:SetPoint("LEFT", pill, "LEFT", 4, 0)
    dot:SetTexture(W8)
    if dot.SetGradient and CreateColor then
        dot:SetGradient("VERTICAL",
            CreateColor(ac[1], ac[2], ac[3], 0.3),
            CreateColor(ac[1], ac[2], ac[3], 1.0))
    else
        dot:SetColorTexture(ac[1], ac[2], ac[3])
    end
    pill._dot = dot

    local lbl = pill:CreateFontString(nil, "OVERLAY")
    TrySetFont(lbl, BODY_FONT, 9, "OUTLINE")
    lbl:SetPoint("LEFT", dot, "RIGHT", 6, 0)
    lbl:SetTextColor(TC("mutedText"))
    pill._label = lbl

    local val = pill:CreateFontString(nil, "OVERLAY")
    TrySetFont(val, TITLE_FONT, 11, "OUTLINE")
    val:SetPoint("RIGHT", pill, "RIGHT", -8, 0)
    val:SetJustifyH("RIGHT")
    val:SetTextColor(TC("bodyText"))
    pill._value = val

    local frost = pill:CreateTexture(nil, "OVERLAY", nil, 3)
    frost:SetHeight(1)
    frost:SetPoint("TOPLEFT", pill, "TOPLEFT", 1, -1)
    frost:SetPoint("TOPRIGHT", pill, "TOPRIGHT", -1, -1)
    frost:SetTexture(W8)
    if frost.SetGradient and CreateColor then
        frost:SetGradient("HORIZONTAL",
            CreateColor(1, 1, 1, 0.01),
            CreateColor(1, 1, 1, 0.04))
    else
        frost:SetColorTexture(1, 1, 1, 0.04)
    end

    local botLine = pill:CreateTexture(nil, "BACKGROUND", nil, 2)
    botLine:SetHeight(1)
    botLine:SetPoint("BOTTOMLEFT", pill, "BOTTOMLEFT", 1, 1)
    botLine:SetPoint("BOTTOMRIGHT", pill, "BOTTOMRIGHT", -1, 1)
    botLine:SetColorTexture(0, 0, 0, 0.08)

    return pill
end

local function CreateRolePill(parent, width, height, xOfs, yOfs, roleKey, atlas, label)
    local rpill = CreateFrame("Button", nil, parent, "BackdropTemplate")
    rpill:SetSize(width, height)
    rpill:SetPoint("TOPLEFT", parent, "TOPLEFT", xOfs, yOfs)
    rpill:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    rpill:SetBackdropColor(TC("surfaceBg"))
    rpill:SetBackdropBorderColor(0, 0, 0, 0)

    local selGlow = rpill:CreateTexture(nil, "BACKGROUND", nil, 1)
    selGlow:SetPoint("TOPLEFT", rpill, "TOPLEFT", 1, -1)
    selGlow:SetPoint("BOTTOMRIGHT", rpill, "BOTTOMRIGHT", -1, 1)
    selGlow:SetColorTexture(0, 0, 0, 0)
    rpill._selGlow = selGlow

    local ricon = rpill:CreateTexture(nil, "ARTWORK")
    ricon:SetSize(16, 16)
    ricon:SetPoint("LEFT", rpill, "LEFT", 8, 0)
    pcall(ricon.SetAtlas, ricon, atlas)
    ricon:SetDesaturated(true)
    ricon:SetAlpha(0.4)
    rpill._icon = ricon

    local rlbl = rpill:CreateFontString(nil, "OVERLAY")
    TrySetFont(rlbl, BODY_FONT, 10, "OUTLINE")
    rlbl:SetPoint("LEFT", ricon, "RIGHT", 5, 0)
    rlbl:SetTextColor(TC("mutedText"))
    rlbl:SetText(label)
    rpill._label = rlbl
    rpill._roleKey = roleKey

    local frost = rpill:CreateTexture(nil, "OVERLAY", nil, 3)
    frost:SetHeight(1)
    frost:SetPoint("TOPLEFT", rpill, "TOPLEFT", 1, -1)
    frost:SetPoint("TOPRIGHT", rpill, "TOPRIGHT", -1, -1)
    frost:SetTexture(W8)
    if frost.SetGradient and CreateColor then
        frost:SetGradient("HORIZONTAL",
            CreateColor(1, 1, 1, 0.01),
            CreateColor(1, 1, 1, 0.04))
    else
        frost:SetColorTexture(1, 1, 1, 0.04)
    end

    return rpill
end

function Panel.UpdateRoleCards(cards, stateTable, acColor)
    if not cards then return end
    local ac = acColor or activeTheme.accent
    for _, rcard in ipairs(cards) do
        if stateTable[rcard._roleKey] then
            rcard._icon:SetDesaturated(false)
            rcard._icon:SetAlpha(1.0)
            if rcard.SetBackdropBorderColor then rcard:SetBackdropBorderColor(ac[1], ac[2], ac[3], 0.20) end
            if rcard._selGlow then rcard._selGlow:SetColorTexture(ac[1], ac[2], ac[3], 0.08) end
            if rcard._label then rcard._label:SetTextColor(ac[1], ac[2], ac[3]) end
        else
            rcard._icon:SetDesaturated(true)
            rcard._icon:SetAlpha(0.4)
            if rcard.SetBackdropBorderColor then rcard:SetBackdropBorderColor(0, 0, 0, 0) end
            if rcard._selGlow then rcard._selGlow:SetColorTexture(0, 0, 0, 0) end
            if rcard._label then rcard._label:SetTextColor(TC("mutedText")) end
        end
    end
end

local function PopulatePlayerIdentity(nameRef, classIconRef, specLineRef)
    local pName = UnitName("player") or "Unknown"
    local _, pClass = UnitClass("player")
    if nameRef then
        local cc = pClass and RAID_CLASS_COLORS and RAID_CLASS_COLORS[pClass]
        if cc then
            nameRef:SetText(("|cff%02x%02x%02x"):format(cc.r * 255, cc.g * 255, cc.b * 255) .. pName .. "|r")
        else
            nameRef:SetText(pName)
        end
    end
    if classIconRef and pClass then
        local coords = CLASS_ICON_TCOORDS and CLASS_ICON_TCOORDS[pClass]
        if coords then classIconRef:SetTexCoord(unpack(coords)) end
    end
    if specLineRef then
        local specStr, ilvlStr = "", ""
        local specIdx = GetSpecialization and GetSpecialization()
        if specIdx then
            local okS, _, specNameStr = pcall(GetSpecializationInfo, specIdx)
            if okS and specNameStr then specStr = specNameStr end
        end
        if GetAverageItemLevel then
            local okI, _, equipped = pcall(GetAverageItemLevel)
            if okI and equipped and equipped > 0 then ilvlStr = math.floor(equipped) .. " iLvl" end
        end
        local line = specStr
        if ilvlStr ~= "" then line = line .. (specStr ~= "" and "  \194\183  " or "") .. ilvlStr end
        specLineRef:SetText(line)
    end
end

-- ============================================================
-- Reusable activity search box + autocomplete dropdown.
-- Builds a styled search field at the left of `bar` whose dropdown lists
-- the activities for whatever premade category is active (so it shows
-- raids on the Raids tab, delves on the Delves tab). Shared by the Raids
-- and Delves filter bars. Raid/delve listing names aren't protected the
-- way Mythic+ key names are, so this uses a plain addon EditBox rather
-- than adopting Blizzard's search box.
--
-- opts = { width, placeholder, onText(text) }
-- Returns { frame = <slot>, box = <editbox>, ac = <dropdown> }.
-- ============================================================
function Panel.BuildSearchBar(bar, opts)
    opts = opts or {}
    local width = opts.width or 230
    local placeholder = opts.placeholder or "Search\226\128\166"
    local onText = opts.onText
    local accR, accG, accB = TC("accent")

    local slot = CreateFrame("Frame", nil, bar, "BackdropTemplate")
    slot:SetSize(width, 28)
    slot:SetPoint("LEFT", bar, "LEFT", CFG.PAD, 0)
    slot:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    slot:SetBackdropColor(TC("surfaceBg"))
    slot:SetBackdropBorderColor(0.28, 0.28, 0.34, 0.45)

    local underline = slot:CreateTexture(nil, "OVERLAY", nil, 2)
    underline:SetHeight(1)
    underline:SetPoint("BOTTOMLEFT", slot, "BOTTOMLEFT", 1, 0)
    underline:SetPoint("BOTTOMRIGHT", slot, "BOTTOMRIGHT", -1, 0)
    underline:SetColorTexture(accR, accG, accB, 0.25)

    local icon = slot:CreateTexture(nil, "OVERLAY")
    icon:SetPoint("LEFT", slot, "LEFT", 9, 0)
    -- NB: SetAtlas with useAtlasSize=true would reset the size to the atlas's
    -- native size, so pass false and apply SetSize afterwards.
    if not pcall(icon.SetAtlas, icon, "common-search-magnifyingglass", false) then
        icon:SetTexture("Interface\\Common\\UI-Searchbox-Icon")
    end
    icon:SetSize(10, 10)
    icon:SetVertexColor(TC("mutedText"))

    local box = CreateFrame("EditBox", nil, slot)
    box:SetPoint("LEFT", icon, "RIGHT", 7, 0)
    box:SetPoint("RIGHT", slot, "RIGHT", -22, 0)
    box:SetHeight(24)
    box:SetAutoFocus(false)
    TrySetFont(box, BODY_FONT, 11, "")
    box:SetTextColor(TC("bodyText"))

    local ph = box:CreateFontString(nil, "ARTWORK")
    TrySetFont(ph, BODY_FONT, 11, "")
    ph:SetPoint("LEFT", box, "LEFT", 1, 0)
    ph:SetTextColor(TC("mutedText"))
    ph:SetText(placeholder)

    local clear = CreateFrame("Button", nil, slot)
    clear:SetSize(14, 14)
    clear:SetPoint("RIGHT", slot, "RIGHT", -6, 0)
    local clearTex = clear:CreateTexture(nil, "OVERLAY")
    clearTex:SetAllPoints()
    if not pcall(clearTex.SetAtlas, clearTex, "common-search-clearbutton", true) then
        clearTex:Hide()
        local cx = clear:CreateFontString(nil, "OVERLAY")
        TrySetFont(cx, BODY_FONT, 13, "OUTLINE")
        cx:SetPoint("CENTER")
        cx:SetText("\195\151")
        cx:SetTextColor(TC("mutedText"))
    end
    clear:Hide()

    local ac = CreateFrame("Frame", nil, slot, "BackdropTemplate")
    local AC_MAX, AC_ROW_H, AC_W = 8, 22, math.max(width + 38, 268)
    ac:SetSize(AC_W, AC_ROW_H + 2)
    ac:SetPoint("TOPLEFT", slot, "BOTTOMLEFT", 0, -3)
    ac:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    ac:SetBackdropColor(0.04, 0.04, 0.07, 0.98)
    ac:SetBackdropBorderColor(TC("glassBorder"))
    ac:SetFrameStrata("TOOLTIP")
    ac:Hide()
    ac._rows, ac._matches, ac._selected = {}, {}, 0

    local function ACStyle(row, on)
        local aR, aG, aB = TC("accent")
        row._bg:SetColorTexture(aR, aG, aB, on and 0.16 or 0)
    end
    for i = 1, AC_MAX do
        local row = CreateFrame("Button", nil, ac)
        row:SetSize(AC_W - 2, AC_ROW_H)
        row:SetPoint("TOPLEFT", ac, "TOPLEFT", 1, -((i - 1) * AC_ROW_H) - 1)
        row:SetFrameStrata("TOOLTIP")
        local rbg = row:CreateTexture(nil, "BACKGROUND")
        rbg:SetAllPoints(); rbg:SetColorTexture(0, 0, 0, 0)
        row._bg = rbg
        local lbl = row:CreateFontString(nil, "OVERLAY")
        TrySetFont(lbl, BODY_FONT, 11, "")
        lbl:SetPoint("LEFT", row, "LEFT", 10, 0)
        lbl:SetPoint("RIGHT", row, "RIGHT", -8, 0)
        lbl:SetJustifyH("LEFT")
        if lbl.SetWordWrap then lbl:SetWordWrap(false) end
        if lbl.SetMaxLines then lbl:SetMaxLines(1) end
        lbl:SetTextColor(TC("bodyText"))
        row._label = lbl
        row:SetScript("OnEnter", function()
            ac._selected = i
            for ri, r in ipairs(ac._rows) do ACStyle(r, ri == i) end
        end)
        row:SetScript("OnLeave", function() ACStyle(row, false) end)
        row:SetScript("OnClick", function() if ac.Accept then ac.Accept(i) end end)
        ac._rows[i] = row
    end

    local closer = CreateFrame("Frame", nil, UIParent)
    closer:Hide()
    closer:SetScript("OnUpdate", function()
        if ac:IsShown() and not ac:IsMouseOver() and not slot:IsMouseOver() then
            if IsMouseButtonDown("LeftButton") or IsMouseButtonDown("RightButton") then ac:Hide() end
        end
    end)
    ac:SetScript("OnShow", function() closer:Show() end)
    ac:SetScript("OnHide", function() closer:Hide() end)

    function ac.Render()
        local n = math.min(#ac._matches, AC_MAX)
        if n == 0 then ac:Hide(); return end
        for i = 1, AC_MAX do
            local row = ac._rows[i]
            if i <= n then
                row._label:SetText(ac._matches[i].full)
                ACStyle(row, i == ac._selected)
                row:Show()
            else
                row:Hide()
            end
        end
        ac:SetHeight(n * AC_ROW_H + 2)
        ac:Show(); ac:Raise()
    end
    function ac.DoUpdate(query)
        query = query and query:gsub("^%s+", ""):gsub("%s+$", "") or ""
        ac._selected = 0
        if #query < 2 then ac._matches = {}; ac:Hide(); return end
        local cat = Panel._state.activeCategory
        local catID
        for _, c in ipairs(CATEGORY_DEFS) do
            if c.key == cat then catID = c.categoryID; break end
        end
        if cat == "delves" then catID = ResolveDelvesCategoryID() end
        local activities = GetCategoryActivities(catID)
        if not activities then ac._matches = {}; ac:Hide(); return end
        local q, m = query:lower(), {}
        for _, a in ipairs(activities) do
            if a.lower:find(q, 1, true) then
                m[#m + 1] = a
                if #m >= AC_MAX then break end
            end
        end
        ac._matches = m
        ac.Render()
    end
    function ac.MoveSelection(delta)
        local n = math.min(#ac._matches, AC_MAX)
        if n == 0 then return end
        local sel = ac._selected + delta
        if sel < 1 then sel = n elseif sel > n then sel = 1 end
        ac._selected = sel
        for ri, r in ipairs(ac._rows) do ACStyle(r, ri == sel) end
    end
    function ac.Accept(index)
        local idx = index or ac._selected
        local match = idx and ac._matches[idx]
        ac:Hide()
        if not match then return end
        box:SetText(match.full)
        pcall(box.SetCursorPosition, box, #match.full)
        box:ClearFocus()
        Panel.OnSearchResults()
    end

    box:SetScript("OnTextChanged", function(self, byUser)
        local txt = self:GetText() or ""
        ph:SetShown(txt == "")
        clear:SetShown(txt ~= "")
        if onText then onText(txt) end
        if byUser then
            ac.DoUpdate(txt)
            Panel.OnSearchResults()
        end
    end)
    box:SetScript("OnEditFocusGained", function()
        slot:SetBackdropBorderColor(accR, accG, accB, 0.6)
        underline:SetColorTexture(accR, accG, accB, 0.7)
    end)
    box:SetScript("OnEditFocusLost", function(self)
        slot:SetBackdropBorderColor(0.28, 0.28, 0.34, 0.45)
        underline:SetColorTexture(accR, accG, accB, 0.25)
        if ac:IsShown() and not ac:IsMouseOver() then
            C_Timer.After(0.12, function() if ac:IsShown() and not ac:IsMouseOver() then ac:Hide() end end)
        end
    end)
    box:SetScript("OnEnterPressed", function(self)
        if ac:IsShown() and ac._selected and ac._selected > 0 then
            ac.Accept(ac._selected)
        else
            ac:Hide(); self:ClearFocus()
        end
    end)
    box:SetScript("OnEscapePressed", function(self)
        if ac:IsShown() then ac:Hide(); return end
        self:SetText(""); self:ClearFocus(); Panel.OnSearchResults()
    end)
    box:SetScript("OnArrowPressed", function(self, key)
        if ac:IsShown() then
            if key == "DOWN" then ac.MoveSelection(1); return end
            if key == "UP" then ac.MoveSelection(-1); return end
        end
    end)
    box:SetScript("OnTabPressed", function(self)
        if ac:IsShown() and #ac._matches > 0 then
            ac.Accept(ac._selected > 0 and ac._selected or 1)
        else
            self:ClearFocus()
        end
    end)
    clear:SetScript("OnClick", function()
        box:SetText("")
        ac:Hide()
        Panel.OnSearchResults()
    end)

    return { frame = slot, box = box, ac = ac }
end

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

    GetActiveTheme()

    local frameName = "MidnightUI_GroupFinderPanel"
    local f = CreateFrame("Frame", frameName, UIParent, "BackdropTemplate")
    f:SetSize(CFG.WIDTH, CFG.HEIGHT)
    f:SetPoint("CENTER")
    f:SetFrameStrata(CFG.STRATA)
    f:SetBackdrop({
        bgFile = W8,
        edgeFile = W8,
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    f:SetBackdropColor(TC("frameBg"))
    local ac = activeTheme.accent
    f:SetBackdropBorderColor(ac[1], ac[2], ac[3], 0.15)
    f:Hide()
    R.panel = f

    tinsert(UISpecialFrames, frameName)

    CreateDropShadow(f, 5)

    f:SetScript("OnHide", function()
        Panel._state.panelOpen = false
        StopAutoRefresh()
        Panel.HideGroupDetail()
        if R.settingsPopup then R.settingsPopup:Hide() end
    end)

    local frameHide = f.Hide
    f.Hide = function(self)
        if Panel._state.detailOpen then
            Panel.HideGroupDetail()
            return
        end
        frameHide(self)
    end

    local header = CreateFrame("Frame", nil, f)
    header:SetHeight(CFG.HEADER_H)
    header:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
    header:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
    R.header = header

    local headerBg = header:CreateTexture(nil, "BACKGROUND")
    headerBg:SetAllPoints()
    headerBg:SetColorTexture(TC("headerBg"))
    R.headerBg = headerBg

    local accentLine = header:CreateTexture(nil, "OVERLAY", nil, 2)
    accentLine:SetHeight(2)
    accentLine:SetPoint("BOTTOMLEFT", header, "BOTTOMLEFT", 0, 0)
    accentLine:SetPoint("BOTTOMRIGHT", header, "BOTTOMRIGHT", 0, 0)
    accentLine:SetTexture(W8)
    if accentLine.SetGradient and CreateColor then
        accentLine: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))
    else
        accentLine:SetColorTexture(activeTheme.accent[1], activeTheme.accent[2], activeTheme.accent[3], 0.6)
    end
    R.headerAccentLine = accentLine

    local title = header:CreateFontString(nil, "OVERLAY")
    TrySetFont(title, TITLE_FONT, 16, "OUTLINE")
    title:SetPoint("LEFT", header, "LEFT", CFG.PAD, 0)
    title:SetTextColor(TC("titleText"))
    title:SetText("Group Finder (Beta)")
    R.title = title

    header:EnableMouse(true)
    header:RegisterForDrag("LeftButton")
    header:SetScript("OnDragStart", function() f:StartMoving() end)
    header:SetScript("OnDragStop", function() f:StopMovingOrSizing() end)
    f:SetMovable(true)
    f:SetClampedToScreen(true)

    local closeBtn = CreateFrame("Button", nil, header)
    closeBtn:SetSize(28, 28)
    closeBtn:SetPoint("RIGHT", header, "RIGHT", -8, 0)
    local closeTex = closeBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(closeTex, TITLE_FONT, 16, "OUTLINE")
    closeTex:SetPoint("CENTER")
    closeTex:SetTextColor(TC("bodyText"))
    closeTex:SetText("X")
    closeBtn:SetScript("OnClick", function() Panel.Hide() end)
    closeBtn:SetScript("OnEnter", function() closeTex:SetTextColor(1, 0.3, 0.3) end)
    closeBtn:SetScript("OnLeave", function() closeTex:SetTextColor(TC("bodyText")) end)
    R.closeBtn = closeBtn

    local gearBtn = CreateFrame("Button", nil, header)
    gearBtn:SetSize(24, 24)
    gearBtn:SetPoint("RIGHT", closeBtn, "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

    if _G.MidnightPI and _G.MidnightPI.BuildCompanionSettingsButton then _G.MidnightPI.BuildCompanionSettingsButton(gearBtn) end

    local seasonBtn = CreateFrame("Button", nil, f, "BackdropTemplate")
    seasonBtn:SetSize(CFG.CAT_SIDEBAR_W - 24, 30)
    seasonBtn:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 12, 12)
    seasonBtn:SetFrameLevel(f:GetFrameLevel() + 10)
    seasonBtn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    local sbAccR, sbAccG, sbAccB = TC("accent")
    seasonBtn:SetBackdropColor(sbAccR, sbAccG, sbAccB, 0.18)
    seasonBtn:SetBackdropBorderColor(sbAccR, sbAccG, sbAccB, 0.55)
    local sbDot = seasonBtn:CreateTexture(nil, "OVERLAY", nil, 2)
    sbDot:SetSize(5, 5)
    sbDot:SetPoint("LEFT", seasonBtn, "LEFT", 10, 0)
    sbDot:SetColorTexture(sbAccR, sbAccG, sbAccB, 0.9)
    local sbLbl = seasonBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(sbLbl, BODY_FONT, 11, "OUTLINE")
    sbLbl:SetPoint("LEFT", sbDot, "RIGHT", 8, 0)
    sbLbl:SetTextColor(TC("accent"))
    sbLbl:SetText("Season Progress")
    seasonBtn:SetScript("OnEnter", function(self)
        self:SetBackdropColor(sbAccR, sbAccG, sbAccB, 0.30)
        self:SetBackdropBorderColor(sbAccR, sbAccG, sbAccB, 0.85)
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        GameTooltip:AddLine("Season Progress", 1, 1, 1)
        GameTooltip:AddLine("Your current Mythic+ season at a glance — overall MPI, dimension averages, weekly tally, per-dungeon bests, and recent runs.", 0.7, 0.7, 0.7, true)
        GameTooltip:Show()
    end)
    seasonBtn:SetScript("OnLeave", function(self)
        self:SetBackdropColor(sbAccR, sbAccG, sbAccB, 0.18)
        self:SetBackdropBorderColor(sbAccR, sbAccG, sbAccB, 0.55)
        GameTooltip:Hide()
    end)
    seasonBtn:SetScript("OnClick", function()
        if _G.MidnightUI_SeasonProgress_Toggle then
            _G.MidnightUI_SeasonProgress_Toggle()
        end
    end)
    R.seasonBtn = seasonBtn

    local tabBar = CreateFrame("Frame", nil, f)
    tabBar:SetHeight(CFG.TAB_BAR_H)
    tabBar:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, 0)
    tabBar:SetPoint("TOPRIGHT", header, "BOTTOMRIGHT", 0, 0)
    R.tabBar = tabBar

    local tabDivider = tabBar:CreateTexture(nil, "OVERLAY", nil, 2)
    tabDivider:SetHeight(1)
    tabDivider:SetPoint("TOPLEFT", tabBar, "TOPLEFT", 0, 0)
    tabDivider:SetPoint("TOPRIGHT", tabBar, "TOPRIGHT", 0, 0)
    tabDivider:SetColorTexture(TC("accent"))
    R.tabDivider = tabDivider

    local tabWidth = CFG.WIDTH / #TAB_DEFS
    R.tabs = {}
    for i, def in ipairs(TAB_DEFS) do
        local tab = CreateFrame("Button", nil, tabBar)
        tab:SetSize(tabWidth, CFG.TAB_BAR_H)
        if i == 1 then
            tab:SetPoint("TOPLEFT", tabBar, "TOPLEFT", 0, 0)
        else
            tab:SetPoint("LEFT", R.tabs[i - 1], "RIGHT", 0, 0)
        end

        local label = tab:CreateFontString(nil, "OVERLAY")
        TrySetFont(label, BODY_FONT, 11, "OUTLINE")
        label:SetPoint("CENTER")
        label:SetTextColor(TC("tabInactive"))
        label:SetText(def.label)
        tab._label = label

        local underline = tab:CreateTexture(nil, "OVERLAY", nil, 3)
        underline:SetHeight(2)
        underline:SetPoint("BOTTOMLEFT", tab, "BOTTOMLEFT", 20, 0)
        underline:SetPoint("BOTTOMRIGHT", tab, "BOTTOMRIGHT", -20, 0)
        underline:SetColorTexture(TC("accent"))
        underline:Hide()
        tab._underline = underline

        tab._key = def.key

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

        R.tabs[i] = tab
    end

    local content = CreateFrame("Frame", nil, f)
    content:SetPoint("TOPLEFT", tabBar, "BOTTOMLEFT", 0, 0)
    content:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
    content:SetClipsChildren(true)
    R.content = content

    do
    local dungeonContent = CreateFrame("Frame", nil, content)
    dungeonContent:SetAllPoints()
    dungeonContent:SetClipsChildren(true)
    dungeonContent:Hide()
    R.dungeonContent = dungeonContent

    Panel._state.dungeonRoles = { tank = false, healer = false, dps = false }

    Panel._dungeonData = LoadSeasonDungeons()
    do
        local hList, hReqIlvl = LoadHeroicDungeons()
        Panel._heroicDungeonData = hList
        Panel._heroicRequiredGearLevel = hReqIlvl or 0
    end
    Panel._followerDungeonData = LoadFollowerDungeons()
    Panel._twRandomDungeon = FindTimewalkingRandomDungeon()

    local dungeonHero = CreateFrame("Frame", nil, dungeonContent)
    dungeonHero:SetHeight(190)
    dungeonHero:SetPoint("TOPLEFT", dungeonContent, "TOPLEFT", 0, 0)
    dungeonHero:SetPoint("TOPRIGHT", dungeonContent, "TOPRIGHT", 0, 0)
    R.dungeonHero = dungeonHero

    local dAc = activeTheme.accent

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

    local dhAtlas = dungeonHero:CreateTexture(nil, "BACKGROUND", nil, -7)
    dhAtlas:SetAllPoints()
    if not TrySetAtlasFromList(dhAtlas, HERO_ATLASES.dungeons) then
        dhAtlas:SetColorTexture(0, 0, 0, 0)
    end
    dhAtlas:SetAlpha(0.40)

    local dhVigTop = dungeonHero:CreateTexture(nil, "BACKGROUND", nil, -5)
    dhVigTop:SetHeight(80)
    dhVigTop:SetPoint("TOPLEFT", dungeonHero, "TOPLEFT", 0, 0)
    dhVigTop:SetPoint("TOPRIGHT", dungeonHero, "TOPRIGHT", 0, 0)
    dhVigTop:SetTexture(W8)
    ApplyGradient(dhVigTop, "VERTICAL", 0, 0, 0, 0, 0, 0, 0, 0.5)

    local dhVigBot = dungeonHero:CreateTexture(nil, "BACKGROUND", nil, -5)
    dhVigBot:SetHeight(60)
    dhVigBot:SetPoint("BOTTOMLEFT", dungeonHero, "BOTTOMLEFT", 0, 0)
    dhVigBot:SetPoint("BOTTOMRIGHT", dungeonHero, "BOTTOMRIGHT", 0, 0)
    dhVigBot:SetTexture(W8)
    ApplyGradient(dhVigBot, "VERTICAL", 0, 0, 0, 0.6, 0, 0, 0, 0)

    local dhVigLeft = dungeonHero:CreateTexture(nil, "BACKGROUND", nil, -5)
    dhVigLeft:SetWidth(math.floor(CFG.WIDTH * 0.40))
    dhVigLeft:SetPoint("TOPLEFT", dungeonHero, "TOPLEFT", 0, 0)
    dhVigLeft:SetPoint("BOTTOMLEFT", dungeonHero, "BOTTOMLEFT", 0, 0)
    dhVigLeft:SetTexture(W8)
    ApplyGradient(dhVigLeft, "HORIZONTAL", 0, 0, 0, 0.25, 0, 0, 0, 0)

    local dhAccentWash = dungeonHero:CreateTexture(nil, "BACKGROUND", nil, -4)
    dhAccentWash:SetHeight(60)
    dhAccentWash:SetPoint("BOTTOMLEFT", dungeonHero, "BOTTOMLEFT", 0, 0)
    dhAccentWash:SetPoint("BOTTOMRIGHT", dungeonHero, "BOTTOMRIGHT", 0, 0)
    dhAccentWash:SetTexture(W8)
    ApplyGradient(dhAccentWash, "VERTICAL", dAc[1], dAc[2], dAc[3], 0.03, dAc[1], dAc[2], dAc[3], 0)

    local dhCtxLeft = dungeonHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(dhCtxLeft, BODY_FONT, 9, "OUTLINE")
    dhCtxLeft:SetPoint("TOPLEFT", dungeonHero, "TOPLEFT", CFG.PAD + 2, -8)
    dhCtxLeft:SetTextColor(TC("mutedText"))
    dhCtxLeft:SetText("")

    local dhCtxRight = dungeonHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(dhCtxRight, BODY_FONT, 9, "OUTLINE")
    dhCtxRight:SetPoint("TOPRIGHT", dungeonHero, "TOPRIGHT", -CFG.PAD, -8)
    dhCtxRight:SetTextColor(TC("mutedText"))
    dhCtxRight:SetText("")
    R.dungeonHeroScore = dhCtxRight

    local dhClassIcon = dungeonHero:CreateTexture(nil, "OVERLAY")
    dhClassIcon:SetSize(22, 22)
    dhClassIcon:SetPoint("TOP", dungeonHero, "TOP", -46, -34)
    dhClassIcon:SetTexture("Interface\\GLUES\\CHARACTERCREATE\\UI-CharacterCreate-Classes")
    R.dungeonHeroClassIcon = dhClassIcon

    local dhPlayerName = dungeonHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(dhPlayerName, TITLE_FONT, 14, "OUTLINE")
    dhPlayerName:SetPoint("LEFT", dhClassIcon, "RIGHT", 6, 0)
    dhPlayerName:SetTextColor(TC("bodyText"))
    dhPlayerName:SetText("")
    R.dungeonHeroPlayerLine = dhPlayerName

    local dhSpecLine = dungeonHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(dhSpecLine, BODY_FONT, 10, "")
    dhSpecLine:SetPoint("TOP", dhPlayerName, "BOTTOM", 0, -4)
    dhSpecLine:SetJustifyH("CENTER")
    dhSpecLine:SetTextColor(TC("mutedText"))
    dhSpecLine:SetText("")
    R.dungeonHeroSpecLine = dhSpecLine

    local PILL_H = 28
    local PILL_GAP = 8
    local ROLE_PILL_W = 80
    local STAT_PILL_W = 140
    local KEY_PILL_W_MIN = 110
    local KEY_PILL_W_MAX = 240
    local MPI_PILL_W = 180
    local totalPillW = 3 * ROLE_PILL_W + MPI_PILL_W + STAT_PILL_W + KEY_PILL_W_MIN + 5 * PILL_GAP
    local pillStartX = math.floor((CFG.WIDTH - totalPillW) / 2)
    local pillY = -82


    local roleCardDefs = {
        { key = "tank",   atlas = "UI-LFG-RoleIcon-Tank",   label = "Tank" },
        { key = "healer", atlas = "UI-LFG-RoleIcon-Healer", label = "Healer" },
        { key = "dps",    atlas = "UI-LFG-RoleIcon-DPS",    label = "DPS" },
    }
    R.dungeonRoleCards = {}

    for ri, rd in ipairs(roleCardDefs) do
        local rpill = CreateFrame("Button", nil, dungeonHero, "BackdropTemplate")
        rpill:SetSize(ROLE_PILL_W, PILL_H)
        local xOfs = pillStartX + (ri - 1) * (ROLE_PILL_W + PILL_GAP)
        rpill:SetPoint("TOPLEFT", dungeonHero, "TOPLEFT", xOfs, pillY)
        rpill:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        rpill:SetBackdropColor(TC("surfaceBg"))
        rpill:SetBackdropBorderColor(0, 0, 0, 0)

        local selGlow = rpill:CreateTexture(nil, "BACKGROUND", nil, 1)
        selGlow:SetPoint("TOPLEFT", rpill, "TOPLEFT", 1, -1)
        selGlow:SetPoint("BOTTOMRIGHT", rpill, "BOTTOMRIGHT", -1, 1)
        selGlow:SetColorTexture(0, 0, 0, 0)
        rpill._selGlow = selGlow

        local ricon = rpill:CreateTexture(nil, "ARTWORK")
        ricon:SetSize(16, 16)
        ricon:SetPoint("LEFT", rpill, "LEFT", 8, 0)
        pcall(ricon.SetAtlas, ricon, rd.atlas)
        ricon:SetDesaturated(true)
        ricon:SetAlpha(0.4)
        rpill._icon = ricon

        local rlbl = rpill:CreateFontString(nil, "OVERLAY")
        TrySetFont(rlbl, BODY_FONT, 10, "OUTLINE")
        rlbl:SetPoint("LEFT", ricon, "RIGHT", 5, 0)
        rlbl:SetTextColor(TC("mutedText"))
        rlbl:SetText(rd.label)
        rpill._label = rlbl
        rpill._roleKey = rd.key

        local frost = rpill:CreateTexture(nil, "OVERLAY", nil, 3)
        frost:SetHeight(1)
        frost:SetPoint("TOPLEFT", rpill, "TOPLEFT", 1, -1)
        frost:SetPoint("TOPRIGHT", rpill, "TOPRIGHT", -1, -1)
        frost:SetTexture(W8)
        if frost.SetGradient and CreateColor then
            frost:SetGradient("HORIZONTAL",
                CreateColor(1, 1, 1, 0.01),
                CreateColor(1, 1, 1, 0.04))
        else
            frost:SetColorTexture(1, 1, 1, 0.04)
        end

        rpill:SetScript("OnClick", function(self)
            Panel._state.dungeonRoles[self._roleKey] = not Panel._state.dungeonRoles[self._roleKey]
            Panel.UpdateDungeonRoleCards()
        end)
        rpill:SetScript("OnEnter", function(self)
            if not Panel._state.dungeonRoles[self._roleKey] then
                self._icon:SetAlpha(0.7)
            end
        end)
        rpill:SetScript("OnLeave", function(self)
            if not Panel._state.dungeonRoles[self._roleKey] then
                self._icon:SetAlpha(0.4)
            end
        end)

        R.dungeonRoleCards[ri] = rpill
    end
    R.dungeonRoleBtns = R.dungeonRoleCards

    local mpiPillX = pillStartX + 3 * (ROLE_PILL_W + PILL_GAP)
    local mpiPill = CreateStatPill(dungeonHero, MPI_PILL_W, PILL_H, mpiPillX, pillY)
    mpiPill._label:SetText("MPI RATING")
    mpiPill._value:SetText("--")
    R.dungeonHeroMPIPill = mpiPill

    local ratingPillX = mpiPillX + MPI_PILL_W + PILL_GAP
    local ratingPill = CreateStatPill(dungeonHero, STAT_PILL_W, PILL_H, ratingPillX, pillY)
    ratingPill._label:SetText("M+ RATING")
    ratingPill._value:SetText("--")
    R.dungeonRatingPill = ratingPill

    local keyPillX = ratingPillX + STAT_PILL_W + PILL_GAP
    local keyPill = CreateStatPill(dungeonHero, KEY_PILL_W_MIN, PILL_H, keyPillX, pillY)
    keyPill._muiMinW = KEY_PILL_W_MIN
    keyPill._muiMaxW = KEY_PILL_W_MAX
    keyPill._label:SetText("KEY")
    keyPill._value:SetText("None")
    R.dungeonKeystonePill = keyPill
    R.dungeonHeroKeystoneText = keyPill._value

    local BADGE_W = 120
    local BADGE_H = 24
    local BADGE_GAP = 5
    local challengerDungeons = {}
    for _, d in ipairs(Panel._dungeonData) do
        if d.isChallenger then
            challengerDungeons[#challengerDungeons + 1] = d
        end
    end
    local numDungeons = #challengerDungeons
    local totalBadgeW = numDungeons * BADGE_W + (numDungeons - 1) * BADGE_GAP
    local badgeStartX = math.floor((CFG.WIDTH - totalBadgeW) / 2)
    R.heroDungeonBadges = {}

    for i, dung in ipairs(challengerDungeons) do
        local badge = CreateFrame("Frame", nil, dungeonHero, "BackdropTemplate")
        badge:SetSize(BADGE_W, BADGE_H)
        local xOfs = badgeStartX + (i - 1) * (BADGE_W + BADGE_GAP)
        badge:SetPoint("TOPLEFT", dungeonHero, "TOPLEFT", xOfs, -120)
        badge:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        badge:SetBackdropColor(TC("surfaceBg"))
        badge:SetBackdropBorderColor(0, 0, 0, 0)

        local bDot = badge:CreateTexture(nil, "OVERLAY", nil, 2)
        bDot:SetSize(3, BADGE_H)
        bDot:SetPoint("LEFT", badge, "LEFT", 0, 0)
        bDot:SetTexture(W8)
        if bDot.SetGradient and CreateColor then
            bDot:SetGradient("VERTICAL",
                CreateColor(dung.accent[1], dung.accent[2], dung.accent[3], 0.25),
                CreateColor(dung.accent[1], dung.accent[2], dung.accent[3], 0.70))
        else
            bDot:SetColorTexture(dung.accent[1], dung.accent[2], dung.accent[3], 0.70)
        end
        badge._acBar = bDot

        local bAbbr = badge:CreateFontString(nil, "OVERLAY")
        TrySetFont(bAbbr, BODY_FONT, 10, "OUTLINE")
        bAbbr:SetPoint("LEFT", badge, "LEFT", 8, 0)
        bAbbr:SetTextColor(dung.accent[1], dung.accent[2], dung.accent[3])
        bAbbr:SetText(dung.abbr)
        badge._abbrText = bAbbr

        local bLevel = badge:CreateFontString(nil, "OVERLAY")
        TrySetFont(bLevel, TITLE_FONT, 11, "OUTLINE")
        bLevel:SetPoint("RIGHT", badge, "RIGHT", -6, 0)
        bLevel:SetJustifyH("RIGHT")
        bLevel:SetTextColor(TC("mutedText"))
        bLevel:SetText("--")
        badge._levelText = bLevel

        badge._mapID = dung.mapID
        badge._accent = dung.accent
        R.heroDungeonBadges[i] = badge
    end

    local dsWeakText = dungeonHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(dsWeakText, BODY_FONT, 9, "")
    dsWeakText:SetPoint("BOTTOMLEFT", dungeonHero, "BOTTOMLEFT", CFG.PAD + 2, 14)
    dsWeakText:SetPoint("RIGHT", dungeonHero, "CENTER", -8, 0)
    dsWeakText:SetJustifyH("LEFT")
    dsWeakText:SetTextColor(TC("mutedText"))
    dsWeakText:SetText("")
    R.suggestWeakText = dsWeakText

    local dsKeyText = dungeonHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(dsKeyText, BODY_FONT, 9, "")
    dsKeyText:SetPoint("LEFT", dungeonHero, "CENTER", 8, 0)
    dsKeyText:SetPoint("BOTTOMRIGHT", dungeonHero, "BOTTOMRIGHT", -CFG.PAD, 14)
    dsKeyText:SetJustifyH("RIGHT")
    dsKeyText:SetTextColor(TC("mutedText"))
    dsKeyText:SetText("")
    R.suggestKeyText = dsKeyText

    local dhTrack = dungeonHero:CreateTexture(nil, "ARTWORK", nil, 1)
    dhTrack:SetHeight(3)
    dhTrack:SetPoint("BOTTOMLEFT", dungeonHero, "BOTTOMLEFT", CFG.PAD, 5)
    dhTrack:SetPoint("BOTTOMRIGHT", dungeonHero, "BOTTOMRIGHT", -CFG.PAD, 5)
    dhTrack:SetColorTexture(1, 1, 1, 0.04)
    R.dungeonHeroTrack = dhTrack

    local dhTrackHighlight = dungeonHero:CreateTexture(nil, "ARTWORK", nil, 1)
    dhTrackHighlight:SetHeight(1)
    dhTrackHighlight:SetPoint("TOPLEFT", dhTrack, "TOPLEFT", 0, 0)
    dhTrackHighlight:SetPoint("TOPRIGHT", dhTrack, "TOPRIGHT", 0, 0)
    dhTrackHighlight:SetColorTexture(1, 1, 1, 0.06)

    local dhFill = dungeonHero:CreateTexture(nil, "ARTWORK", nil, 2)
    dhFill:SetHeight(3)
    dhFill:SetPoint("LEFT", dhTrack, "LEFT", 0, 0)
    dhFill:SetWidth(1)
    dhFill:SetColorTexture(dAc[1], dAc[2], dAc[3], 0.55)
    R.dungeonHeroFill = dhFill

    local dhGlassDivL = dungeonHero:CreateTexture(nil, "OVERLAY", nil, 3)
    dhGlassDivL:SetHeight(1)
    dhGlassDivL:SetPoint("BOTTOMLEFT", dungeonHero, "BOTTOMLEFT", 0, 0)
    dhGlassDivL:SetPoint("BOTTOM", dungeonHero, "BOTTOM", 0, 0)
    dhGlassDivL:SetTexture(W8)
    ApplyGradient(dhGlassDivL, "HORIZONTAL", 1, 1, 1, 0, 1, 1, 1, 0.06)

    local dhGlassDivR = dungeonHero:CreateTexture(nil, "OVERLAY", nil, 3)
    dhGlassDivR:SetHeight(1)
    dhGlassDivR:SetPoint("BOTTOM", dungeonHero, "BOTTOM", 0, 0)
    dhGlassDivR:SetPoint("BOTTOMRIGHT", dungeonHero, "BOTTOMRIGHT", 0, 0)
    dhGlassDivR:SetTexture(W8)
    ApplyGradient(dhGlassDivR, "HORIZONTAL", 1, 1, 1, 0.06, 1, 1, 1, 0)

    local dungeonSuggest = CreateFrame("Frame", nil, dungeonContent)
    dungeonSuggest:SetHeight(1)
    dungeonSuggest:SetPoint("TOPLEFT", dungeonHero, "BOTTOMLEFT", 0, 0)
    dungeonSuggest:SetPoint("TOPRIGHT", dungeonHero, "BOTTOMRIGHT", 0, 0)
    R.dungeonSuggest = dungeonSuggest

    local queueBar = CreateFrame("Frame", nil, dungeonContent, "BackdropTemplate")
    queueBar:SetHeight(30)
    queueBar:SetPoint("TOPLEFT", dungeonSuggest, "BOTTOMLEFT", 0, 0)
    queueBar:SetPoint("TOPRIGHT", dungeonSuggest, "BOTTOMRIGHT", 0, 0)
    queueBar:SetBackdrop({
        bgFile = W8,
        edgeFile = W8,
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    local qbAc = activeTheme.accent
    queueBar:SetBackdropColor(qbAc[1], qbAc[2], qbAc[3], 0.10)
    queueBar:SetBackdropBorderColor(qbAc[1], qbAc[2], qbAc[3], 0.20)
    queueBar:Hide()
    R.dungeonQueueBar = queueBar

    local queueText = queueBar:CreateFontString(nil, "OVERLAY")
    TrySetFont(queueText, BODY_FONT, 11, "OUTLINE")
    queueText:SetPoint("LEFT", queueBar, "LEFT", 12, 0)
    queueText:SetTextColor(TC("bodyText"))
    queueText:SetText("Queued...")
    R.dungeonQueueText = queueText

    -- Top cancel button removed; Find Group button doubles as cancel when queued.

    local CARD_W = math.floor((CFG.WIDTH - 48) / 2)
    local CARD_H = 48
    local CARD_GAP_X = 8
    local CARD_GAP_Y = 6
    local BOTTOM_BAR_H = 50
    local dungeonData = Panel._dungeonData
    Panel._cardLayout = { W = CARD_W, H = CARD_H, GX = CARD_GAP_X, GY = CARD_GAP_Y, SECTION_H = 28 }

    local cardScroll = CreateFrame("ScrollFrame", nil, dungeonContent, "UIPanelScrollFrameTemplate")
    cardScroll:SetPoint("TOPLEFT", queueBar, "BOTTOMLEFT", 8, -8)
    cardScroll:SetPoint("BOTTOMRIGHT", dungeonContent, "BOTTOMRIGHT", -28, BOTTOM_BAR_H + 4)

    if cardScroll.ScrollBar then
        if cardScroll.ScrollBar.ScrollUpButton then
            cardScroll.ScrollBar.ScrollUpButton:SetAlpha(0)
        end
        if cardScroll.ScrollBar.ScrollDownButton then
            cardScroll.ScrollBar.ScrollDownButton:SetAlpha(0)
        end
    end

    local SECTION_HEADER_H = 28
    local heroicQueueData = Panel._heroicDungeonData
    local followerData = Panel._followerDungeonData
    local twRandom = Panel._twRandomDungeon

    local seasonRows = math.ceil(#dungeonData / 2)
    local heroicRows = math.ceil(#heroicQueueData / 2)
    local followerRows = math.ceil(#followerData / 2)
    local totalH = 0
    if #heroicQueueData > 0 then
        totalH = totalH + SECTION_HEADER_H + heroicRows * (CARD_H + CARD_GAP_Y) + 8
    end
    if #dungeonData > 0 then
        totalH = totalH + SECTION_HEADER_H + seasonRows * (CARD_H + CARD_GAP_Y) + 8
    end
    if #followerData > 0 then
        totalH = totalH + SECTION_HEADER_H + followerRows * (CARD_H + CARD_GAP_Y) + 8
    end
    if twRandom then
        totalH = totalH + SECTION_HEADER_H + (CARD_H + CARD_GAP_Y) + 8
    end
    if totalH == 0 then
        totalH = SECTION_HEADER_H
    end

    local cardContainer = CreateFrame("Frame", nil, cardScroll)
    cardContainer:SetWidth(CFG.WIDTH - 48)
    cardContainer:SetHeight(totalH)
    cardScroll:SetScrollChild(cardContainer)
    R.cardContainer = cardContainer

    local currentY = 0

    local function MakeSectionHeader(label, yOfs, color)
        local hdr = cardContainer:CreateFontString(nil, "OVERLAY")
        TrySetFont(hdr, TITLE_FONT, 11, "OUTLINE")
        hdr:SetPoint("TOPLEFT", cardContainer, "TOPLEFT", 4, -yOfs)
        if color then
            hdr:SetTextColor(color[1], color[2], color[3])
        else
            hdr:SetTextColor(TC("mutedText"))
        end
        hdr:SetText(label)
        local div = cardContainer:CreateTexture(nil, "ARTWORK")
        div:SetHeight(1)
        div:SetPoint("TOPLEFT", hdr, "BOTTOMLEFT", -4, -4)
        div:SetPoint("RIGHT", cardContainer, "RIGHT", 0, 0)
        if color then
            div:SetColorTexture(color[1], color[2], color[3], 0.25)
        else
            div:SetColorTexture(TC("divider"))
        end
        return hdr, div
    end

    R.heroicCards = {}
    if #heroicQueueData > 0 then
        local hdr, div = MakeSectionHeader("HEROIC DUNGEONS", currentY)
        R.heroicHeader = hdr
        R.heroicDiv = div
        local startY = -(currentY + SECTION_HEADER_H)
        for i, dungInfo in ipairs(heroicQueueData) do
            heroicQueueData[i]._sectionStartY = startY
            heroicQueueData[i]._localIdx = i
        end
        currentY = currentY + SECTION_HEADER_H + heroicRows * (CARD_H + CARD_GAP_Y) + 8
    end

    local mplusHeader, mplusDiv = MakeSectionHeader("SEASON DUNGEONS", currentY)
    R.mplusHeader = mplusHeader
    R.mplusDiv = mplusDiv
    local mplusStartY = -(currentY + SECTION_HEADER_H)
    currentY = currentY + SECTION_HEADER_H + seasonRows * (CARD_H + CARD_GAP_Y) + 8

    for i, dungInfo in ipairs(heroicQueueData) do
        local col = (i - 1) % 2
        local row = math.floor((i - 1) / 2)
        local xOfs = col * (CARD_W + CARD_GAP_X)
        local yOfs = dungInfo._sectionStartY - (row * (CARD_H + CARD_GAP_Y))
        heroicQueueData[i]._xOfs = xOfs
        heroicQueueData[i]._yOfs = yOfs
    end

    R.dungeonCards = {}
    for i, dungInfo in ipairs(dungeonData) do
        local col = (i - 1) % 2
        local row = math.floor((i - 1) / 2)
        local xOfs = col * (CARD_W + CARD_GAP_X)
        local yOfs = mplusStartY - (row * (CARD_H + CARD_GAP_Y))
        local dAccent
        if (row % 2) == 0 then
            dAccent = { 0.35, 0.50, 0.62 }
        else
            dAccent = { 0.55, 0.48, 0.32 }
        end

        local card = CreateFrame("Button", nil, cardContainer, "BackdropTemplate")
        card:SetSize(CARD_W, CARD_H)
        card:SetPoint("TOPLEFT", cardContainer, "TOPLEFT", xOfs, yOfs)
        card:SetBackdrop({
            bgFile = W8,
            edgeFile = W8,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        card:SetBackdropColor(TC("surfaceBg"))
        card:SetBackdropBorderColor(0, 0, 0, 0)

        local banner = card:CreateTexture(nil, "BACKGROUND", nil, 2)
        banner:SetPoint("TOPLEFT", card, "TOPLEFT", 1, -1)
        banner:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", -1, 1)
        banner:SetTexture(W8)
        ApplyGradient(banner, "HORIZONTAL",
            dAccent[1], dAccent[2], dAccent[3], 0.12,
            dAccent[1], dAccent[2], dAccent[3], 0.02)
        card._banner = banner

        local watermark = card:CreateFontString(nil, "ARTWORK", nil, 1)
        TrySetFont(watermark, TITLE_FONT, 22, "OUTLINE")
        watermark:SetPoint("RIGHT", card, "RIGHT", -8, 0)
        watermark:SetTextColor(dAccent[1], dAccent[2], dAccent[3], 0.10)
        watermark:SetText(dungInfo.abbr)
        card._watermark = watermark

        local acBar = card:CreateTexture(nil, "OVERLAY", nil, 2)
        acBar:SetSize(3, CARD_H)
        acBar:SetPoint("LEFT", card, "LEFT", 0, 0)
        acBar:SetColorTexture(dAccent[1], dAccent[2], dAccent[3], 1)
        card._accentBar = acBar

        local abbrText = card:CreateFontString(nil, "OVERLAY")
        TrySetFont(abbrText, TITLE_FONT, 11, "OUTLINE")
        abbrText:SetPoint("LEFT", card, "LEFT", 12, 0)
        abbrText:SetTextColor(dAccent[1], dAccent[2], dAccent[3])
        abbrText:SetText(dungInfo.abbr)
        card._abbrText = abbrText

        local nameText = card:CreateFontString(nil, "OVERLAY")
        TrySetFont(nameText, TITLE_FONT, 11, "OUTLINE")
        nameText:SetPoint("LEFT", abbrText, "RIGHT", 8, 0)
        nameText:SetPoint("RIGHT", card, "RIGHT", -12, 0)
        nameText:SetJustifyH("LEFT")
        nameText:SetTextColor(TC("bodyText"))
        nameText:SetText(dungInfo.name)
        card._nameText = nameText

        local bestText = card:CreateFontString(nil, "OVERLAY")
        TrySetFont(bestText, BODY_FONT, 11, "")
        bestText:Hide()
        card._bestText = bestText
        local scoreText = card:CreateFontString(nil, "OVERLAY")
        TrySetFont(scoreText, BODY_FONT, 10, "")
        scoreText:Hide()
        card._scoreText = scoreText

        local trackBg = card:CreateTexture(nil, "BACKGROUND", nil, 2)
        trackBg:SetHeight(3)
        trackBg:SetPoint("BOTTOMLEFT", card, "BOTTOMLEFT", 1, 1)
        trackBg:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", -1, 1)
        trackBg:SetColorTexture(1, 1, 1, 0.06)
        card._trackBg = trackBg

        local trackFill = card:CreateTexture(nil, "ARTWORK", nil, 3)
        trackFill:SetHeight(3)
        trackFill:SetPoint("LEFT", trackBg, "LEFT", 0, 0)
        trackFill:SetWidth(1)
        trackFill:SetColorTexture(dAccent[1], dAccent[2], dAccent[3], 0.6)
        card._trackFill = trackFill

        card._selected = false
        card._mapID = dungInfo.mapID
        card._lfgDungeonID = dungInfo.lfgDungeonID
        card._dungeonName = dungInfo.name
        card._expansionLevel = dungInfo.expansionLevel or 0
        card._dungeonAccent = dAccent

        card:SetScript("OnEnter", function(self)
            if not self._selected then
                local cAc = self._dungeonAccent
                self:SetBackdropColor(cAc[1], cAc[2], cAc[3], 0.05)
                self:SetBackdropBorderColor(cAc[1], cAc[2], cAc[3], 0.12)
            end
        end)
        card:SetScript("OnLeave", function(self)
            if not self._selected then
                self:SetBackdropColor(TC("surfaceBg"))
                self:SetBackdropBorderColor(0, 0, 0, 0)
            end
        end)

        card:SetScript("OnClick", function(self)
            self._selected = not self._selected
            if self._selected then
                local cAc = self._dungeonAccent
                self:SetBackdropColor(cAc[1], cAc[2], cAc[3], 0.10)
                self:SetBackdropBorderColor(cAc[1], cAc[2], cAc[3], 0.25)
                if Panel._state.dungeonQueueMode ~= "specific" and R.dungeonTypeBtn then
                    Panel._state.dungeonQueueMode = "specific"
                    R.dungeonTypeBtn._label:SetText("Selected Dungeon")
                    R.dungeonTypeBtn:SetBackdropBorderColor(TC("accent"))
                    R.dungeonTypeBtn._label:SetTextColor(TC("accent"))
                end
            else
                self:SetBackdropColor(TC("surfaceBg"))
                self:SetBackdropBorderColor(0, 0, 0, 0)
                local anySelected = false
                for _, c in ipairs(R.dungeonCards or {}) do
                    if c._selected then anySelected = true; break end
                end
                if not anySelected then
                    for _, c in ipairs(R.followerCards or {}) do
                        if c._selected then anySelected = true; break end
                    end
                end
                if not anySelected and R.twCard and R.twCard._selected then
                    anySelected = true
                end
                if not anySelected and R.dungeonTypeBtn then
                    Panel._state.dungeonQueueMode = "random"
                    R.dungeonTypeBtn._label:SetText("Random Dungeon")
                    R.dungeonTypeBtn:SetBackdropBorderColor(0.2, 0.2, 0.25, 0.4)
                    R.dungeonTypeBtn._label:SetTextColor(TC("bodyText"))
                end
            end
        end)

        R.dungeonCards[i] = card
    end

    for i, dungInfo in ipairs(heroicQueueData) do
        local zRow = math.floor((i - 1) / 2)
        local dAccent
        if (zRow % 2) == 0 then
            dAccent = { 0.35, 0.50, 0.62 }
        else
            dAccent = { 0.55, 0.48, 0.32 }
        end
        local xOfs = dungInfo._xOfs or 0
        local yOfs = dungInfo._yOfs or 0

        local card = CreateFrame("Button", nil, cardContainer, "BackdropTemplate")
        card:SetSize(CARD_W, CARD_H)
        card:SetPoint("TOPLEFT", cardContainer, "TOPLEFT", xOfs, yOfs)
        card:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        card:SetBackdropColor(TC("surfaceBg"))
        card:SetBackdropBorderColor(0, 0, 0, 0)

        local banner = card:CreateTexture(nil, "BACKGROUND", nil, 2)
        banner:SetPoint("TOPLEFT", card, "TOPLEFT", 1, -1)
        banner:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", -1, 1)
        banner:SetTexture(W8)
        ApplyGradient(banner, "HORIZONTAL", dAccent[1], dAccent[2], dAccent[3], 0.12, dAccent[1], dAccent[2], dAccent[3], 0.02)

        local acBar = card:CreateTexture(nil, "OVERLAY", nil, 2)
        acBar:SetSize(3, CARD_H)
        acBar:SetPoint("LEFT", card, "LEFT", 0, 0)
        acBar:SetColorTexture(dAccent[1], dAccent[2], dAccent[3], 1)

        local abbrText = card:CreateFontString(nil, "OVERLAY")
        TrySetFont(abbrText, TITLE_FONT, 11, "OUTLINE")
        abbrText:SetPoint("LEFT", card, "LEFT", 12, 0)
        abbrText:SetTextColor(dAccent[1], dAccent[2], dAccent[3])
        abbrText:SetText(dungInfo.abbr)
        card._abbrText = abbrText

        local nameText = card:CreateFontString(nil, "OVERLAY")
        TrySetFont(nameText, TITLE_FONT, 11, "OUTLINE")
        nameText:SetPoint("LEFT", abbrText, "RIGHT", 8, 0)
        nameText:SetPoint("RIGHT", card, "RIGHT", -12, 0)
        nameText:SetJustifyH("LEFT")
        nameText:SetTextColor(TC("bodyText"))
        nameText:SetText(dungInfo.name)
        card._nameText = nameText

        card._selected = false
        card._mapID = dungInfo.mapID
        card._lfgDungeonID = dungInfo.lfgDungeonID
        card._dungeonName = dungInfo.name
        card._expansionLevel = dungInfo.expansionLevel or 0
        card._dungeonAccent = dAccent

        card:SetScript("OnEnter", function(self)
            if not self._selected then
                local cAc = self._dungeonAccent
                self:SetBackdropColor(cAc[1], cAc[2], cAc[3], 0.05)
                self:SetBackdropBorderColor(cAc[1], cAc[2], cAc[3], 0.12)
            end
        end)
        card:SetScript("OnLeave", function(self)
            if not self._selected then
                self:SetBackdropColor(TC("surfaceBg"))
                self:SetBackdropBorderColor(0, 0, 0, 0)
            end
        end)
        card:SetScript("OnClick", function(self)
            self._selected = not self._selected
            if self._selected then
                local cAc = self._dungeonAccent
                self:SetBackdropColor(cAc[1], cAc[2], cAc[3], 0.10)
                self:SetBackdropBorderColor(cAc[1], cAc[2], cAc[3], 0.25)
                if Panel._state.dungeonQueueMode ~= "specific" and R.dungeonTypeBtn then
                    Panel._state.dungeonQueueMode = "specific"
                    R.dungeonTypeBtn._label:SetText("Selected Dungeon")
                    R.dungeonTypeBtn:SetBackdropBorderColor(TC("accent"))
                    R.dungeonTypeBtn._label:SetTextColor(TC("accent"))
                end
            else
                self:SetBackdropColor(TC("surfaceBg"))
                self:SetBackdropBorderColor(0, 0, 0, 0)
                local anySelected = false
                for _, c in ipairs(R.heroicCards or {}) do
                    if c._selected then anySelected = true; break end
                end
                if not anySelected then
                    for _, c in ipairs(R.dungeonCards or {}) do
                        if c._selected then anySelected = true; break end
                    end
                end
                if not anySelected then
                    for _, c in ipairs(R.followerCards or {}) do
                        if c._selected then anySelected = true; break end
                    end
                end
                if not anySelected and R.twCard and R.twCard._selected then
                    anySelected = true
                end
                if not anySelected and R.dungeonTypeBtn then
                    Panel._state.dungeonQueueMode = "random"
                    R.dungeonTypeBtn._label:SetText("Random Dungeon")
                    R.dungeonTypeBtn:SetBackdropBorderColor(0.2, 0.2, 0.25, 0.4)
                    R.dungeonTypeBtn._label:SetTextColor(TC("bodyText"))
                end
            end
        end)

        R.heroicCards[i] = card
    end

    R.followerCards = {}
    if #followerData > 0 then
        local followerHeader, followerDiv = MakeSectionHeader(
            "FOLLOWER DUNGEONS", currentY, {0.35, 0.75, 0.55})
        R.followerHeader = followerHeader
        R.followerDiv = followerDiv

        local fCardStartY = -(currentY + SECTION_HEADER_H)
        currentY = currentY + SECTION_HEADER_H + followerRows * (CARD_H + CARD_GAP_Y) + 8

        for i, fInfo in ipairs(followerData) do
            local col = (i - 1) % 2
            local row = math.floor((i - 1) / 2)
            local xOfs = col * (CARD_W + CARD_GAP_X)
            local yOfs = fCardStartY - (row * (CARD_H + CARD_GAP_Y))
            local dAccent
            if (row % 2) == 0 then
                dAccent = { 0.35, 0.50, 0.62 }
            else
                dAccent = { 0.55, 0.48, 0.32 }
            end

            local card = CreateFrame("Button", nil, cardContainer, "BackdropTemplate")
            card:SetSize(CARD_W, CARD_H)
            card:SetPoint("TOPLEFT", cardContainer, "TOPLEFT", xOfs, yOfs)
            card:SetBackdrop({
                bgFile = W8,
                edgeFile = W8,
                edgeSize = 1,
                insets = { left = 1, right = 1, top = 1, bottom = 1 },
            })
            card:SetBackdropColor(TC("surfaceBg"))
            card:SetBackdropBorderColor(0, 0, 0, 0)

            local banner = card:CreateTexture(nil, "BACKGROUND", nil, 2)
            banner:SetPoint("TOPLEFT", card, "TOPLEFT", 1, -1)
            banner:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", -1, 1)
            banner:SetTexture(W8)
            ApplyGradient(banner, "HORIZONTAL",
                dAccent[1], dAccent[2], dAccent[3], 0.10,
                dAccent[1], dAccent[2], dAccent[3], 0.02)
            card._banner = banner

            local watermark = card:CreateFontString(nil, "ARTWORK", nil, 1)
            TrySetFont(watermark, TITLE_FONT, 22, "OUTLINE")
            watermark:SetPoint("RIGHT", card, "RIGHT", -8, 0)
            watermark:SetTextColor(dAccent[1], dAccent[2], dAccent[3], 0.10)
            watermark:SetText(fInfo.abbr)
            card._watermark = watermark

            local acBar = card:CreateTexture(nil, "OVERLAY", nil, 2)
            acBar:SetSize(3, CARD_H)
            acBar:SetPoint("LEFT", card, "LEFT", 0, 0)
            acBar:SetColorTexture(dAccent[1], dAccent[2], dAccent[3], 1)
            card._accentBar = acBar

            local abbrText = card:CreateFontString(nil, "OVERLAY")
            TrySetFont(abbrText, TITLE_FONT, 11, "OUTLINE")
            abbrText:SetPoint("LEFT", card, "LEFT", 12, 0)
            abbrText:SetTextColor(dAccent[1], dAccent[2], dAccent[3])
            abbrText:SetText(fInfo.abbr)
            card._abbrText = abbrText

            local nameText = card:CreateFontString(nil, "OVERLAY")
            TrySetFont(nameText, TITLE_FONT, 11, "OUTLINE")
            nameText:SetPoint("LEFT", abbrText, "RIGHT", 8, 0)
            nameText:SetPoint("RIGHT", card, "RIGHT", -12, 0)
            nameText:SetJustifyH("LEFT")
            nameText:SetTextColor(TC("bodyText"))
            nameText:SetText(fInfo.name)
            card._nameText = nameText

            local infoText = card:CreateFontString(nil, "OVERLAY")
            TrySetFont(infoText, BODY_FONT, 11, "")
            infoText:Hide()
            card._bestText = infoText
            local badge = card:CreateFontString(nil, "OVERLAY")
            TrySetFont(badge, BODY_FONT, 9, "")
            badge:Hide()
            card._scoreText = badge

            card._selected = false
            card._dungeonID = fInfo.dungeonID
            card._mapID = fInfo.mapID
            card._dungeonName = fInfo.name
            card._expansionLevel = fInfo.expansion or 0
            card._dungeonAccent = dAccent
            card._isFollower = true
            card._minLevel = fInfo.minLevel or 0
            card._maxLevel = fInfo.maxLevel or 0

            card:SetScript("OnEnter", function(self)
                if not self._selected then
                    local cAc = self._dungeonAccent
                    self:SetBackdropColor(cAc[1], cAc[2], cAc[3], 0.05)
                    self:SetBackdropBorderColor(cAc[1], cAc[2], cAc[3], 0.12)
                end
            end)
            card:SetScript("OnLeave", function(self)
                if not self._selected then
                    self:SetBackdropColor(TC("surfaceBg"))
                    self:SetBackdropBorderColor(0, 0, 0, 0)
                end
            end)

            card:SetScript("OnClick", function(self)
                self._selected = not self._selected
                if self._selected then
                    local cAc = self._dungeonAccent
                    self:SetBackdropColor(cAc[1], cAc[2], cAc[3], 0.10)
                    self:SetBackdropBorderColor(cAc[1], cAc[2], cAc[3], 0.25)
                    if Panel._state.dungeonQueueMode ~= "specific" and R.dungeonTypeBtn then
                        Panel._state.dungeonQueueMode = "specific"
                        R.dungeonTypeBtn._label:SetText("Selected Dungeon")
                        R.dungeonTypeBtn:SetBackdropBorderColor(TC("accent"))
                        R.dungeonTypeBtn._label:SetTextColor(TC("accent"))
                    end
                else
                    self:SetBackdropColor(TC("surfaceBg"))
                    self:SetBackdropBorderColor(0, 0, 0, 0)
                    local anySelected = false
                    for _, c in ipairs(R.dungeonCards or {}) do
                        if c._selected then anySelected = true; break end
                    end
                    if not anySelected then
                        for _, c in ipairs(R.followerCards or {}) do
                            if c._selected then anySelected = true; break end
                        end
                    end
                    if not anySelected and R.twCard and R.twCard._selected then
                        anySelected = true
                    end
                    if not anySelected and R.dungeonTypeBtn then
                        Panel._state.dungeonQueueMode = "random"
                        R.dungeonTypeBtn._label:SetText("Random Dungeon")
                        R.dungeonTypeBtn:SetBackdropBorderColor(0.2, 0.2, 0.25, 0.4)
                        R.dungeonTypeBtn._label:SetTextColor(TC("bodyText"))
                    end
                end
            end)

            R.followerCards[i] = card
        end
    end

    do
        local TW_ACCENT = { 0.75, 0.60, 0.25 }

        local twHeader = cardContainer:CreateFontString(nil, "OVERLAY")
        TrySetFont(twHeader, TITLE_FONT, 11, "OUTLINE")
        twHeader:SetTextColor(TW_ACCENT[1], TW_ACCENT[2], TW_ACCENT[3])
        twHeader:SetText("TIMEWALKING DUNGEONS")
        twHeader:Hide()
        R.twHeader = twHeader

        local twDiv = cardContainer:CreateTexture(nil, "ARTWORK")
        twDiv:SetHeight(1)
        twDiv:SetPoint("TOPLEFT", twHeader, "BOTTOMLEFT", -4, -4)
        twDiv:SetPoint("RIGHT", cardContainer, "RIGHT", 0, 0)
        twDiv:SetColorTexture(TW_ACCENT[1], TW_ACCENT[2], TW_ACCENT[3], 0.25)
        twDiv:Hide()
        R.twDiv = twDiv

        local card = CreateFrame("Button", nil, cardContainer, "BackdropTemplate")
        card:SetSize(CARD_W, CARD_H)
        card:SetBackdrop({
            bgFile = W8,
            edgeFile = W8,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        card:SetBackdropColor(TC("surfaceBg"))
        card:SetBackdropBorderColor(0, 0, 0, 0)

        local banner = card:CreateTexture(nil, "BACKGROUND", nil, 2)
        banner:SetPoint("TOPLEFT", card, "TOPLEFT", 1, -1)
        banner:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", -1, 1)
        banner:SetTexture(W8)
        ApplyGradient(banner, "HORIZONTAL",
            TW_ACCENT[1], TW_ACCENT[2], TW_ACCENT[3], 0.12,
            TW_ACCENT[1], TW_ACCENT[2], TW_ACCENT[3], 0.02)
        card._banner = banner

        local watermark = card:CreateFontString(nil, "ARTWORK", nil, 1)
        TrySetFont(watermark, TITLE_FONT, 22, "OUTLINE")
        watermark:SetPoint("RIGHT", card, "RIGHT", -8, 0)
        watermark:SetTextColor(TW_ACCENT[1], TW_ACCENT[2], TW_ACCENT[3], 0.10)
        watermark:SetText("TW")
        card._watermark = watermark

        local acBar = card:CreateTexture(nil, "OVERLAY", nil, 2)
        acBar:SetSize(3, CARD_H)
        acBar:SetPoint("LEFT", card, "LEFT", 0, 0)
        acBar:SetColorTexture(TW_ACCENT[1], TW_ACCENT[2], TW_ACCENT[3], 1)
        card._accentBar = acBar

        local abbrText = card:CreateFontString(nil, "OVERLAY")
        TrySetFont(abbrText, TITLE_FONT, 11, "OUTLINE")
        abbrText:SetPoint("LEFT", card, "LEFT", 12, 0)
        abbrText:SetTextColor(TW_ACCENT[1], TW_ACCENT[2], TW_ACCENT[3])
        abbrText:SetText("TW")
        card._abbrText = abbrText

        local nameText = card:CreateFontString(nil, "OVERLAY")
        TrySetFont(nameText, TITLE_FONT, 11, "OUTLINE")
        nameText:SetPoint("LEFT", abbrText, "RIGHT", 8, 0)
        nameText:SetPoint("RIGHT", card, "RIGHT", -12, 0)
        nameText:SetJustifyH("LEFT")
        nameText:SetTextColor(TC("bodyText"))
        nameText:SetText(twRandom and twRandom.name or "Random Timewalking Dungeon")
        card._nameText = nameText

        local infoText = card:CreateFontString(nil, "OVERLAY")
        TrySetFont(infoText, BODY_FONT, 11, "")
        infoText:Hide()
        card._bestText = infoText
        local badge = card:CreateFontString(nil, "OVERLAY")
        TrySetFont(badge, BODY_FONT, 9, "")
        badge:Hide()
        card._scoreText = badge

        card._selected = false
        card._dungeonID = twRandom and twRandom.dungeonID or nil
        card._dungeonName = twRandom and twRandom.name or "Random Timewalking Dungeon"
        card._dungeonAccent = TW_ACCENT
        card._isTimewalking = true
        card:Hide()

        card:SetScript("OnEnter", function(self)
            if not self._selected then
                self:SetBackdropColor(TW_ACCENT[1], TW_ACCENT[2], TW_ACCENT[3], 0.05)
                self:SetBackdropBorderColor(TW_ACCENT[1], TW_ACCENT[2], TW_ACCENT[3], 0.12)
            end
        end)
        card:SetScript("OnLeave", function(self)
            if not self._selected then
                self:SetBackdropColor(TC("surfaceBg"))
                self:SetBackdropBorderColor(0, 0, 0, 0)
            end
        end)
        card:SetScript("OnClick", function(self)
            self._selected = not self._selected
            if self._selected then
                self:SetBackdropColor(TW_ACCENT[1], TW_ACCENT[2], TW_ACCENT[3], 0.10)
                self:SetBackdropBorderColor(TW_ACCENT[1], TW_ACCENT[2], TW_ACCENT[3], 0.25)
                if Panel._state.dungeonQueueMode ~= "specific" and R.dungeonTypeBtn then
                    Panel._state.dungeonQueueMode = "specific"
                    R.dungeonTypeBtn._label:SetText("Selected Dungeon")
                    R.dungeonTypeBtn:SetBackdropBorderColor(TC("accent"))
                    R.dungeonTypeBtn._label:SetTextColor(TC("accent"))
                end
            else
                self:SetBackdropColor(TC("surfaceBg"))
                self:SetBackdropBorderColor(0, 0, 0, 0)
                local anySelected = false
                for _, c in ipairs(R.dungeonCards or {}) do
                    if c._selected then anySelected = true; break end
                end
                if not anySelected then
                    for _, c in ipairs(R.followerCards or {}) do
                        if c._selected then anySelected = true; break end
                    end
                end
                if not anySelected and R.dungeonTypeBtn then
                    Panel._state.dungeonQueueMode = "random"
                    R.dungeonTypeBtn._label:SetText("Random Dungeon")
                    R.dungeonTypeBtn:SetBackdropBorderColor(0.2, 0.2, 0.25, 0.4)
                    R.dungeonTypeBtn._label:SetTextColor(TC("bodyText"))
                end
            end
        end)

        R.twCard = card
    end

    Panel._state.dungeonQueueMode = Panel._state.dungeonQueueMode or "random"

    local function FindRandomDungeonIDForCategory(catKey)
        if catKey == "follower" then
            if not (GetNumRandomDungeons and GetLFGRandomDungeonInfo and C_LFGInfo and C_LFGInfo.IsLFGFollowerDungeon) then return nil end
            local n = GetNumRandomDungeons()
            for i = 1, n do
                local id = GetLFGRandomDungeonInfo(i)
                if id then
                    local okF, isF = pcall(C_LFGInfo.IsLFGFollowerDungeon, id)
                    if okF and isF then return id end
                end
            end
            return nil
        end
        local ok, id = pcall(GetRandomDungeonBestChoice)
        if ok then return id end
        return nil
    end

    local function ExecuteDungeonQueue()
        local roles = Panel._state.dungeonRoles
        pcall(SetLFGRoles, false, roles.tank, roles.healer, roles.dps)
        local lfgCat = LE_LFG_CATEGORY_LFD or 1

        Panel._state.lastQueuedDungeons = {}
        Panel._state.dungeonQueueAttempted = true
        Panel._state._queueErrorMsgs = {}
        if Panel._state.dungeonQueueMode == "specific" then
            local queued = false
            local names = {}
            if ClearAllLFGDungeons then pcall(ClearAllLFGDungeons, lfgCat) end
            local list = R.dungeonList or {}
            for _, d in ipairs(list) do
                if d.selected and d.categoryKey ~= "timewalking" then
                    local qID = d.lfgDungeonID or d.dungeonID or d.mapID
                    local dName = d.name or tostring(qID)
                    table.insert(Panel._state.lastQueuedDungeons, { id = qID, name = dName })
                    if SetLFGDungeon then pcall(SetLFGDungeon, lfgCat, qID) end
                    table.insert(names, dName)
                    queued = true
                end
            end
            if not queued then return end
            pcall(JoinLFG, lfgCat)
            Panel._state.dungeonQueuedNames = table.concat(names, ", ")
        else
            local catKey = Panel._state.dungeonCategory or "heroic"
            local randomID = FindRandomDungeonIDForCategory(catKey)
            if randomID then
                table.insert(Panel._state.lastQueuedDungeons, { id = randomID, name = tostring(randomID) })
                if ClearAllLFGDungeons then pcall(ClearAllLFGDungeons, lfgCat) end
                if SetLFGDungeon then pcall(SetLFGDungeon, lfgCat, randomID) end
                pcall(JoinLFG, lfgCat)
                local dname = tostring(randomID)
                if GetLFGDungeonInfo then
                    local okN, n = pcall(GetLFGDungeonInfo, randomID)
                    if okN and n then dname = n end
                end
                Panel._state.dungeonQueuedNames = dname
                if Panel._state.lastQueuedDungeons[1] then
                    Panel._state.lastQueuedDungeons[1].name = dname
                end
            end
        end
        local function CheckQueueResult()
            if not Panel._state.dungeonQueueAttempted then return end
            local lfgCheck = LE_LFG_CATEGORY_LFD or 1
            local okQ, hasData = pcall(GetLFGQueueStats, lfgCheck)
            if okQ and hasData then
                Panel._state.dungeonQueueAttempted = false
                Panel.UpdateDungeonQueueStatus()
                return true
            end
            return false
        end
        C_Timer.After(0.3, function()
            if not Panel._state.dungeonQueueAttempted then return end
            if CheckQueueResult() then return end
            C_Timer.After(0.3, function()
                if not Panel._state.dungeonQueueAttempted then return end
                if CheckQueueResult() then return end
                C_Timer.After(0.4, function()
                    if not Panel._state.dungeonQueueAttempted then return end
                    if not CheckQueueResult() then
                        Panel._state.dungeonQueueAttempted = false
                        local tried = Panel._state.lastQueuedDungeons or {}
                        local names = {}
                        for _, entry in ipairs(tried) do
                            table.insert(names, entry.name or tostring(entry.id))
                        end
                        local dungeonList = #names > 0 and table.concat(names, "\n") or "Unknown Dungeon"
                        local errorMsgs = Panel._state._queueErrorMsgs or {}
                        local reason = #errorMsgs > 0 and table.concat(errorMsgs, "\n") or "You do not meet the requirements.\nCheck your item level, role, and dungeon eligibility."
                        Panel.ShowToast("Queue Rejected", dungeonList .. "\n\n" .. reason, 8)
                    end
                end)
            end)
        end)
    end
    R.ExecuteDungeonQueue = ExecuteDungeonQueue

    do
        local dtState = { queued = false, findDisabled = false, catInactive = {} }
        local DUNGEONS = {}
        local CAT_KEYS   = { "heroic", "season", "follower", "timewalking" }
        local CAT_LABELS = { heroic = "Heroic", season = "Dungeon", follower = "Follower", timewalking = "Timewalking" }
        local CAT_TITLES = { heroic = "HEROIC", season = "DUNGEONS", follower = "FOLLOWER", timewalking = "TIMEWALKING" }
        local CAT_ACCENT = {
            heroic      = { 0.85, 0.55, 0.15 },
            season      = { 0.70, 0.35, 0.90 },
            follower    = { 0.35, 0.75, 0.55 },
            timewalking = { 0.40, 0.65, 0.95 },
        }
        local CAT_COUNTS = { heroic = 0, season = 0, follower = 0, timewalking = 0 }

        local function addSources(list, key)
            if not list then return end
            for _, c in ipairs(list) do
                c._categoryKey = key
                table.insert(DUNGEONS, {
                    sourceCard     = c,
                    name           = c._dungeonName or (c._nameText and c._nameText:GetText()) or "?",
                    abbr           = (c._abbrText and c._abbrText:GetText()) or "?",
                    accent         = c._dungeonAccent or CAT_ACCENT[key] or {1,1,1},
                    categoryKey    = key,
                    minLevel       = c._minLevel or 0,
                    maxLevel       = c._maxLevel or 0,
                    lfgDungeonID   = c._lfgDungeonID,
                    mapID          = c._mapID,
                    dungeonID      = c._dungeonID,
                    isFollower     = c._isFollower,
                    isTimewalking  = c._isTimewalking,
                    expansionLevel = c._expansionLevel or 0,
                    selected       = false,
                })
                CAT_COUNTS[key] = (CAT_COUNTS[key] or 0) + 1
            end
        end
        local rndHeroic, rndNormal = CollectRandomExpansionEntries()
        local heroicTargetExp = (Panel._heroicDungeonData and Panel._heroicDungeonData[1] and Panel._heroicDungeonData[1].expansionLevel) or nil
        if heroicTargetExp then
            local filtered = {}
            for _, e in ipairs(rndHeroic) do
                if e.expansionLevel == heroicTargetExp then filtered[#filtered + 1] = e end
            end
            rndHeroic = filtered
        else
            rndHeroic = {}
        end
        for _, e in ipairs(rndHeroic) do
            table.insert(DUNGEONS, {
                sourceCard        = nil,
                name              = e.name,
                abbr              = "RND",
                accent            = CAT_ACCENT.heroic,
                categoryKey       = "heroic",
                lfgDungeonID      = e.lfgDungeonID,
                expansionLevel    = e.expansionLevel or 0,
                isRandomExpansion = true,
                selected          = false,
            })
            CAT_COUNTS.heroic = (CAT_COUNTS.heroic or 0) + 1
        end
        addSources(R.heroicCards, "heroic")
        for _, e in ipairs(rndNormal) do
            table.insert(DUNGEONS, {
                sourceCard        = nil,
                name              = e.name,
                abbr              = "RND",
                accent            = CAT_ACCENT.season,
                categoryKey       = "season",
                lfgDungeonID      = e.lfgDungeonID,
                expansionLevel    = e.expansionLevel or 0,
                isRandomExpansion = true,
                selected          = false,
            })
            CAT_COUNTS.season = (CAT_COUNTS.season or 0) + 1
        end
        addSources(R.dungeonCards,  "season")
        addSources(R.followerCards, "follower")
        local twActive = false
        if R.twCard then
            R.twCard._categoryKey = "timewalking"
            if not R.twCard._dungeonID and FindTimewalkingRandomDungeon then
                local fresh = FindTimewalkingRandomDungeon()
                if fresh and fresh.dungeonID then
                    R.twCard._dungeonID = fresh.dungeonID
                    R.twCard._dungeonName = fresh.name or R.twCard._dungeonName
                end
            end
            twActive = R.twCard._dungeonID and true or false
            table.insert(DUNGEONS, {
                sourceCard    = R.twCard,
                name          = R.twCard._dungeonName or "Random Timewalking Dungeon",
                abbr          = "TW",
                accent        = CAT_ACCENT.timewalking,
                categoryKey   = "timewalking",
                dungeonID     = R.twCard._dungeonID,
                isTimewalking = true,
                selected      = false,
            })
            if twActive then CAT_COUNTS.timewalking = CAT_COUNTS.timewalking + 1 end
        end
        R.dungeonList = DUNGEONS

        local legacy = {
            R.heroicHeader, R.heroicDiv, R.mplusHeader, R.mplusDiv,
            R.followerHeader, R.followerDiv, R.twHeader, R.twDiv,
            R.cardContainer,
        }
        for _, f in ipairs(legacy) do if f and f.Hide then f:Hide() end end
        for _, d in ipairs(DUNGEONS) do
            if d.sourceCard then
                d.sourceCard:Hide()
                d.sourceCard:ClearAllPoints()
                d.sourceCard:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -5000, 5000)
            end
        end
        if cardScroll then cardScroll:Hide() end

        -- REGION 1: Category Hero Strip
        local strip = CreateFrame("Frame", nil, dungeonContent)
        strip:SetHeight(72)
        strip:SetPoint("TOPLEFT", queueBar, "BOTTOMLEFT", 8, -8)
        strip:SetPoint("TOPRIGHT", queueBar, "BOTTOMRIGHT", -8, -8)

        local tiles = {}
        for _, key in ipairs(CAT_KEYS) do
            local t = CreateFrame("Button", nil, strip, "BackdropTemplate")
            t:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
            t._key = key
            local title = t:CreateFontString(nil, "OVERLAY")
            TrySetFont(title, TITLE_FONT, 12, "OUTLINE")
            title:SetPoint("TOP", t, "TOP", 0, -10)
            title:SetText(CAT_TITLES[key] or key)
            local count = t:CreateFontString(nil, "OVERLAY")
            local inactiveText = nil
            if key == "timewalking" and not twActive then
                dtState.catInactive[key] = true
                inactiveText = "Not Active"
            elseif key == "heroic" and (CAT_COUNTS.heroic or 0) == 0 and (Panel._heroicRequiredGearLevel or 0) > 0 then
                dtState.catInactive[key] = true
                inactiveText = "iLvl Required: " .. tostring(Panel._heroicRequiredGearLevel)
            end
            if dtState.catInactive[key] then
                TrySetFont(count, BODY_FONT, 12, "OUTLINE")
                count:SetText(inactiveText)
            else
                TrySetFont(count, TITLE_FONT, 22, "OUTLINE")
                count:SetText(tostring(CAT_COUNTS[key] or 0))
            end
            count:SetPoint("CENTER", t, "CENTER", 0, -4)
            local under = t:CreateTexture(nil, "OVERLAY")
            under:SetHeight(2)
            under:SetPoint("BOTTOMLEFT", t, "BOTTOMLEFT", 6, 6)
            under:SetPoint("BOTTOMRIGHT", t, "BOTTOMRIGHT", -6, 6)
            local a = CAT_ACCENT[key]
            under:SetColorTexture(a[1], a[2], a[3], 1)
            t._title = title
            t._count = count
            t._under = under
            tiles[key] = t
        end
        local function LayoutStrip(w)
            if not w or w <= 0 then w = strip:GetWidth() end
            if not w or w <= 0 then w = (CFG and CFG.WIDTH or 700) - 32 end
            local n = #CAT_KEYS
            local gap = 10
            local tw = math.floor((w - gap * (n - 1)) / n)
            for i, k in ipairs(CAT_KEYS) do
                local t = tiles[k]
                t:SetSize(tw, 64)
                t:ClearAllPoints()
                t:SetPoint("TOPLEFT", strip, "TOPLEFT", (i - 1) * (tw + gap), 0)
            end
        end
        strip:SetScript("OnSizeChanged", function(self, w) LayoutStrip(w) end)

        local function ApplyTileActive(key)
            for k, t in pairs(tiles) do
                local a = CAT_ACCENT[k]
                if dtState.catInactive[k] then
                    t:SetBackdropColor(0.05, 0.05, 0.07, 0.3)
                    t:SetBackdropBorderColor(0.15, 0.15, 0.17, 0.35)
                    t._title:SetTextColor(0.35, 0.35, 0.37, 1)
                    t._count:SetTextColor(0.4, 0.4, 0.42, 1)
                    t._under:SetAlpha(0.15)
                elseif k == key then
                    t:SetBackdropColor(a[1], a[2], a[3], 0.18)
                    t:SetBackdropBorderColor(a[1], a[2], a[3], 0.9)
                    t._title:SetTextColor(1, 1, 1, 1)
                    t._count:SetTextColor(1, 1, 1, 1)
                    t._under:SetAlpha(1)
                else
                    t:SetBackdropColor(TC("glassBg"))
                    t:SetBackdropBorderColor(TC("glassBorder"))
                    t._title:SetTextColor(TC("mutedText"))
                    t._count:SetTextColor(TC("bodyText"))
                    t._under:SetAlpha(0.4)
                end
            end
        end

        -- REGION 3: Action Dock (built before Region 2 so Region 2 can anchor to it)
        local dockH = 72
        local dock = CreateFrame("Frame", nil, dungeonContent, "BackdropTemplate")
        dock:SetHeight(dockH)
        dock:SetPoint("BOTTOMLEFT", dungeonContent, "BOTTOMLEFT", 0, 0)
        dock:SetPoint("BOTTOMRIGHT", dungeonContent, "BOTTOMRIGHT", 0, 0)
        dock:SetBackdrop({ bgFile = W8 })
        dock:SetBackdropColor(0.03, 0.03, 0.05, 0.8)
        local dockDiv = dock:CreateTexture(nil, "OVERLAY", nil, 2)
        dockDiv:SetHeight(1)
        dockDiv:SetPoint("TOPLEFT", dock, "TOPLEFT", 0, 0)
        dockDiv:SetPoint("TOPRIGHT", dock, "TOPRIGHT", 0, 0)
        dockDiv:SetColorTexture(TC("divider"))

        local dockLabel = dock:CreateFontString(nil, "OVERLAY")
        TrySetFont(dockLabel, BODY_FONT, 9, "OUTLINE")
        dockLabel:SetPoint("TOPLEFT", dock, "TOPLEFT", 16, -10)
        dockLabel:SetTextColor(TC("mutedText"))
        dockLabel:SetText("QUEUEING FOR")

        local dockSummary = dock:CreateFontString(nil, "OVERLAY")
        TrySetFont(dockSummary, TITLE_FONT, 14, "OUTLINE")
        dockSummary:SetPoint("TOPLEFT", dockLabel, "BOTTOMLEFT", 0, -6)
        dockSummary:SetPoint("RIGHT", dock, "RIGHT", -330, 0)
        dockSummary:SetJustifyH("LEFT")
        dockSummary:SetTextColor(TC("bodyText"))

        local pill = CreateFrame("Frame", nil, dock)
        pill:SetSize(130, 24)
        pill:SetPoint("RIGHT", dock, "RIGHT", -170, 0)
        local function MakePillSeg(label, side)
            local b = CreateFrame("Button", nil, pill, "BackdropTemplate")
            b:SetSize(63, 24)
            b:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
            local fs = b:CreateFontString(nil, "OVERLAY")
            TrySetFont(fs, BODY_FONT, 10, "OUTLINE")
            fs:SetPoint("CENTER")
            fs:SetText(label)
            b._label = fs
            if side == "left" then b:SetPoint("LEFT", pill, "LEFT", 0, 0)
            else b:SetPoint("LEFT", pill, "LEFT", 67, 0) end
            return b
        end
        local segRandom   = MakePillSeg("Random", "left")
        local segSpecific = MakePillSeg("Specific", "right")

        local findBtn = CreateFrame("Button", nil, dock, "BackdropTemplate")
        findBtn:SetSize(150, 40)
        findBtn:SetPoint("RIGHT", dock, "RIGHT", -14, 0)
        findBtn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        findBtn:SetBackdropColor(TC("glassBg"))
        findBtn:SetBackdropBorderColor(TC("glassBorder"))
        local findLabel = findBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(findLabel, TITLE_FONT, 13, "OUTLINE")
        findLabel:SetPoint("CENTER")
        findLabel:SetText("Find Group")
        findLabel:SetTextColor(TC("accent"))
        findBtn:SetScript("OnEnter", function(self)
            local r,g,b = TC("glassBg")
            self:SetBackdropColor(r + 0.05, g + 0.05, b + 0.05, 0.85)
        end)
        findBtn:SetScript("OnLeave", function(self) self:SetBackdropColor(TC("glassBg")) end)
        local function IsDungeonQueued()
            if not GetLFGMode then return false end
            local ok, mode = pcall(GetLFGMode, LE_LFG_CATEGORY_LFD or 1)
            if not ok or not mode then return false end
            return mode == "queued" or mode == "proposal" or mode == "rolecheck" or mode == "suspended" or mode == "listed"
        end

        local function ApplyFindBtnVisual()
            local q = IsDungeonQueued()
            local wasQueued = dtState.queued
            dtState.queued = q
            if q then
                findLabel:SetText("Cancel Queue")
                findLabel:SetTextColor(1, 0.35, 0.35, 1)
                findBtn:SetBackdropColor(0.30, 0.05, 0.05, 0.85)
                findBtn:SetBackdropBorderColor(1, 0.30, 0.30, 0.8)
                findBtn:SetAlpha(1.0)
                findBtn:EnableMouse(true)
            else
                if wasQueued then
                    findLabel:SetText("Find Group")
                end
                findBtn:SetBackdropColor(TC("glassBg"))
                findBtn:SetBackdropBorderColor(TC("glassBorder"))
                if dtState.findDisabled then
                    findBtn:SetAlpha(0.45)
                    findBtn:EnableMouse(false)
                    findLabel:SetTextColor(0.5, 0.5, 0.5, 1)
                else
                    findBtn:SetAlpha(1.0)
                    findBtn:EnableMouse(true)
                    findLabel:SetTextColor(TC("accent"))
                end
            end
        end
        R.RefreshFindBtn = ApplyFindBtnVisual

        if C_Timer and C_Timer.NewTicker then
            C_Timer.NewTicker(0.5, function() ApplyFindBtnVisual() end)
        end

        if not R._dtLfgEventsBound then
            R._dtLfgEventsBound = true
            local lfgEvents = CreateFrame("Frame")
            for _, ev in ipairs({
                "LFG_UPDATE",
                "LFG_PROPOSAL_UPDATE", "LFG_PROPOSAL_SHOW",
                "LFG_PROPOSAL_FAILED", "LFG_PROPOSAL_SUCCEEDED",
                "LFG_ROLE_CHECK_UPDATE", "LFG_ROLE_CHECK_SHOW", "LFG_ROLE_CHECK_HIDE",
                "LFG_QUEUE_STATUS_UPDATE",
            }) do
                pcall(lfgEvents.RegisterEvent, lfgEvents, ev)
            end
            lfgEvents:SetScript("OnEvent", function()
                if R.RefreshFindBtn then pcall(R.RefreshFindBtn) end
            end)
        end

        findBtn:SetScript("OnClick", function()
            if dtState.queued then
                pcall(LeaveLFG, LE_LFG_CATEGORY_LFD or 1)
                C_Timer.After(0.3, function()
                    if Panel.UpdateDungeonQueueStatus then pcall(Panel.UpdateDungeonQueueStatus) end
                    ApplyFindBtnVisual()
                end)
                return
            end
            if Panel._state.dungeonCategory == "timewalking" then
                local tw = R.twCard
                if tw and not tw._dungeonID and FindTimewalkingRandomDungeon then
                    local fresh = FindTimewalkingRandomDungeon()
                    if fresh and fresh.dungeonID then
                        tw._dungeonID = fresh.dungeonID
                        tw._dungeonName = fresh.name or tw._dungeonName
                    end
                end
                if not tw or not tw._dungeonID then
                    if Panel.ShowToast then Panel.ShowToast("Timewalking", "No Timewalking event is active this week.", 5) end
                    return
                end
                local roles = Panel._state.dungeonRoles or {}
                pcall(SetLFGRoles, false, roles.tank, roles.healer, roles.dps)
                local lfgCat = LE_LFG_CATEGORY_LFD or 1
                Panel._state.lastQueuedDungeons = { { id = tw._dungeonID, name = tw._dungeonName or "Random Timewalking Dungeon" } }
                Panel._state.dungeonQueuedNames = tw._dungeonName or "Random Timewalking Dungeon"
                Panel._state.dungeonQueueAttempted = true
                if ClearAllLFGDungeons then pcall(ClearAllLFGDungeons, lfgCat) end
                pcall(JoinSingleLFG, lfgCat, tw._dungeonID)
                if Panel.UpdateDungeonQueueStatus then pcall(Panel.UpdateDungeonQueueStatus) end
                return
            end
            if R.ExecuteDungeonQueue then pcall(R.ExecuteDungeonQueue) end
        end)

        local function ApplyPillVisual()
            local mode = Panel._state.dungeonQueueMode or "random"
            local ar, ag, ab = TC("accent")
            local function mute(b)
                b:SetBackdropColor(0.05, 0.05, 0.08, 0.5)
                b:SetBackdropBorderColor(0.2, 0.2, 0.22, 0.5)
                b._label:SetTextColor(TC("mutedText"))
            end
            local function active(b)
                b:SetBackdropColor(ar, ag, ab, 0.30)
                b:SetBackdropBorderColor(ar, ag, ab, 0.85)
                b._label:SetTextColor(1, 1, 1, 1)
            end
            if mode == "random" then active(segRandom); mute(segSpecific)
            else active(segSpecific); mute(segRandom) end
        end

        -- REGION 2: Roster list (between strip and dock)
        local roster = CreateFrame("ScrollFrame", nil, dungeonContent, "UIPanelScrollFrameTemplate")
        roster:SetPoint("TOPLEFT", strip, "BOTTOMLEFT", 0, -10)
        roster:SetPoint("BOTTOMRIGHT", dock, "TOPRIGHT", -28, 10)
        local rosterBody = CreateFrame("Frame", nil, roster)
        rosterBody:SetWidth((CFG and CFG.WIDTH or 700) - 56)
        rosterBody:SetHeight(1)
        roster:SetScrollChild(rosterBody)

        local ROW_H = 44
        local rowPool = {}
        local function AcquireRow(idx)
            if rowPool[idx] then return rowPool[idx] end
            local row = CreateFrame("Button", nil, rosterBody, "BackdropTemplate")
            row:SetHeight(ROW_H)
            row:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
            row:SetBackdropColor(0.04, 0.04, 0.06, 0.45)
            row:SetBackdropBorderColor(0.15, 0.15, 0.18, 0.5)

            local accent = row:CreateTexture(nil, "OVERLAY")
            accent:SetSize(4, ROW_H)
            accent:SetPoint("LEFT", row, "LEFT", 0, 0)
            row._accent = accent

            local iconFrame = CreateFrame("Frame", nil, row)
            iconFrame:SetSize(32, 32)
            iconFrame:SetPoint("LEFT", row, "LEFT", 14, 0)
            local iconBg = iconFrame:CreateTexture(nil, "BACKGROUND")
            iconBg:SetAllPoints()
            iconBg:SetColorTexture(0.14, 0.14, 0.17, 1)
            row._iconFrame = iconFrame

            local abbr = iconFrame:CreateFontString(nil, "OVERLAY")
            TrySetFont(abbr, TITLE_FONT, 11, "OUTLINE")
            abbr:SetPoint("CENTER")
            row._abbr = abbr

            local name = row:CreateFontString(nil, "OVERLAY")
            TrySetFont(name, TITLE_FONT, 13, "OUTLINE")
            name:SetPoint("LEFT", iconFrame, "RIGHT", 12, 8)
            name:SetPoint("RIGHT", row, "RIGHT", -46, 8)
            name:SetJustifyH("LEFT")
            name:SetTextColor(TC("bodyText"))
            row._name = name

            local meta = row:CreateFontString(nil, "OVERLAY")
            TrySetFont(meta, BODY_FONT, 10, "")
            meta:SetPoint("LEFT", iconFrame, "RIGHT", 12, -8)
            meta:SetPoint("RIGHT", row, "RIGHT", -46, -8)
            meta:SetJustifyH("LEFT")
            meta:SetTextColor(TC("mutedText"))
            row._meta = meta

            local check = CreateFrame("Frame", nil, row)
            check:SetSize(16, 16)
            check:SetPoint("RIGHT", row, "RIGHT", -14, 0)
            local checkBg = check:CreateTexture(nil, "BACKGROUND")
            checkBg:SetAllPoints()
            checkBg:SetColorTexture(0.14, 0.14, 0.17, 1)
            check._bg = checkBg
            row._check = check
            local checkDot = check:CreateTexture(nil, "OVERLAY")
            checkDot:SetSize(8, 8)
            checkDot:SetPoint("CENTER")
            checkDot:SetTexture(W8)
            checkDot:Hide()
            row._checkDot = checkDot

            row:SetScript("OnEnter", function(self)
                local sel = self._data and self._data.selected
                if not sel then self:SetBackdropColor(1, 1, 1, 0.04) end
            end)
            row:SetScript("OnLeave", function(self)
                local sel = self._data and self._data.selected
                if not sel then self:SetBackdropColor(0.04, 0.04, 0.06, 0.45) end
            end)
            row:SetScript("OnClick", function(self)
                local d = self._data
                if not d then return end
                d.selected = not d.selected
                if d.sourceCard then d.sourceCard._selected = d.selected end
                if d.selected then
                    Panel._state.dungeonQueueMode = "specific"
                else
                    local any = false
                    for _, dd in ipairs(DUNGEONS) do
                        if dd.selected then any = true; break end
                    end
                    if not any then Panel._state.dungeonQueueMode = "random" end
                end
                if R.UpdateDungeonTab then R.UpdateDungeonTab() end
            end)

            rowPool[idx] = row
            return row
        end

        local twNote = rosterBody:CreateFontString(nil, "OVERLAY")
        TrySetFont(twNote, TITLE_FONT, 13, "OUTLINE")
        twNote:SetPoint("TOP", rosterBody, "TOP", 0, -40)
        twNote:SetJustifyH("CENTER")
        twNote:SetText("Timewalking can only be queued randomly.\nClick Find Group to queue.")
        twNote:SetTextColor(TC("mutedText"))
        twNote:Hide()

        local headerPool = {}
        local function AcquireHeader(idx)
            if headerPool[idx] then return headerPool[idx] end
            local h = CreateFrame("Frame", nil, rosterBody)
            h:SetHeight(20)
            local label = h:CreateFontString(nil, "OVERLAY")
            TrySetFont(label, TITLE_FONT, 10, "OUTLINE")
            label:SetPoint("LEFT", h, "LEFT", 4, 0)
            label:SetTextColor(TC("mutedText"))
            h._label = label
            local div = h:CreateTexture(nil, "ARTWORK")
            div:SetHeight(1)
            div:SetPoint("LEFT", label, "RIGHT", 8, 0)
            div:SetPoint("RIGHT", h, "RIGHT", 0, 0)
            div:SetColorTexture(TC("divider"))
            headerPool[idx] = h
            return h
        end

        local function ExpansionLabel(expLvl)
            if not expLvl or expLvl == 0 then return "Classic" end
            local g = _G["EXPANSION_NAME" .. tostring(expLvl)]
            if g and g ~= "" then return g end
            return "Expansion " .. tostring(expLvl)
        end

        local function RebuildRoster(key)
            if key == "timewalking" then
                for j = 1, #rowPool do rowPool[j]:Hide() end
                for j = 1, #headerPool do headerPool[j]:Hide() end
                twNote:Show()
                rosterBody:SetHeight(80)
                return
            else
                twNote:Hide()
            end

            local entries = {}
            for _, d in ipairs(DUNGEONS) do
                if d.categoryKey == key then entries[#entries + 1] = d end
            end
            table.sort(entries, function(a, b)
                local ae = a.expansionLevel or 0
                local be = b.expansionLevel or 0
                if ae ~= be then return ae > be end
                local ar = a.isRandomExpansion and 0 or 1
                local br = b.isRandomExpansion and 0 or 1
                if ar ~= br then return ar < br end
                return (a.name or "") < (b.name or "")
            end)

            local rowIdx, hdrIdx, y = 0, 0, 0
            local lastExp = nil
            for _, d in ipairs(entries) do
                local eLvl = d.expansionLevel or 0
                if eLvl ~= lastExp then
                    lastExp = eLvl
                    hdrIdx = hdrIdx + 1
                    local h = AcquireHeader(hdrIdx)
                    h:ClearAllPoints()
                    h:SetPoint("LEFT", rosterBody, "LEFT", 0, 0)
                    h:SetPoint("RIGHT", rosterBody, "RIGHT", 0, 0)
                    h:SetPoint("TOP", rosterBody, "TOP", 0, -y)
                    h._label:SetText(string.upper(ExpansionLabel(eLvl)))
                    h:Show()
                    y = y + 22
                end
                rowIdx = rowIdx + 1
                local row = AcquireRow(rowIdx)
                row._data = d
                row:ClearAllPoints()
                row:SetPoint("LEFT", rosterBody, "LEFT", 0, 0)
                row:SetPoint("RIGHT", rosterBody, "RIGHT", 0, 0)
                row:SetPoint("TOP", rosterBody, "TOP", 0, -y)
                row:Show()
                y = y + ROW_H + 4
                local acc = d.accent or CAT_ACCENT[key] or {1,1,1}
                row._accent:SetColorTexture(acc[1], acc[2], acc[3], 1)
                row._abbr:SetText(d.abbr or "?")
                row._abbr:SetTextColor(acc[1], acc[2], acc[3], 1)
                row._name:SetText(d.name or "?")
                local metaTxt = CAT_LABELS[key] or ""
                if d.minLevel and d.minLevel > 0 and d.maxLevel and d.maxLevel > 0 then
                    metaTxt = metaTxt .. "   Lv " .. d.minLevel .. "-" .. d.maxLevel
                end
                row._meta:SetText(metaTxt)
                if d.selected then
                    row:SetBackdropColor(acc[1], acc[2], acc[3], 0.12)
                    if row._check._bg then row._check._bg:SetColorTexture(acc[1], acc[2], acc[3], 0.85) end
                    row._checkDot:SetVertexColor(1, 1, 1, 1)
                    row._checkDot:Show()
                else
                    row:SetBackdropColor(0.04, 0.04, 0.06, 0.45)
                    if row._check._bg then row._check._bg:SetColorTexture(0.14, 0.14, 0.17, 1) end
                    row._checkDot:Hide()
                end
            end
            for j = rowIdx + 1, #rowPool do rowPool[j]:Hide() end
            for j = hdrIdx + 1, #headerPool do headerPool[j]:Hide() end
            rosterBody:SetHeight(math.max(1, y))
        end

        function R.UpdateDungeonTab()
            local key  = Panel._state.dungeonCategory or "heroic"
            if R.twCard and not R.twCard._dungeonID and FindTimewalkingRandomDungeon then
                local fresh = FindTimewalkingRandomDungeon()
                if fresh and fresh.dungeonID then
                    R.twCard._dungeonID = fresh.dungeonID
                    R.twCard._dungeonName = fresh.name or R.twCard._dungeonName
                    dtState.catInactive.timewalking = nil
                    CAT_COUNTS.timewalking = 1
                    for _, d in ipairs(DUNGEONS) do
                        if d.categoryKey == "timewalking" then
                            d.dungeonID = fresh.dungeonID
                            d.name      = fresh.name or d.name
                        end
                    end
                    local twTile = tiles.timewalking
                    if twTile and twTile._count then
                        TrySetFont(twTile._count, TITLE_FONT, 22, "OUTLINE")
                        twTile._count:SetText("1")
                    end
                end
            end
            if key == "timewalking" then
                Panel._state.dungeonQueueMode = "random"
            end
            local mode = Panel._state.dungeonQueueMode or "random"
            ApplyTileActive(key)
            RebuildRoster(key)
            local selected = {}
            for _, d in ipairs(DUNGEONS) do
                if d.selected and d.categoryKey ~= "timewalking" then
                    table.insert(selected, d)
                end
            end
            local catLbl = CAT_LABELS[key] or "Dungeon"

            local queuedNow = R.RefreshFindBtn and IsDungeonQueued() or false
            local disableFind = false
            if key == "timewalking" then
                pill:Hide()
                dockSummary:SetText("Random Timewalking Dungeon")
                if not queuedNow then findLabel:SetText("Find Group") end
            else
                pill:Show()
                if mode == "specific" and #selected > 0 then
                    local names = {}
                    for i, d in ipairs(selected) do
                        if i > 2 then break end
                        table.insert(names, d.name or "?")
                    end
                    local extra = #selected > 2 and (", +" .. (#selected - 2) .. " more") or ""
                    local plural = (#selected == 1) and "" or "s"
                    local phrase
                    if key == "season" then
                        phrase = string.format("%d Dungeon%s (%s%s)", #selected, plural, table.concat(names, ", "), extra)
                    else
                        phrase = string.format("%d %s Dungeon%s (%s%s)", #selected, catLbl, plural, table.concat(names, ", "), extra)
                    end
                    dockSummary:SetText(phrase)
                    if not queuedNow then findLabel:SetText(string.format("Find Group  (%d)", #selected)) end
                elseif mode == "specific" and #selected == 0 then
                    dockSummary:SetText("Select a dungeon to continue")
                    if not queuedNow then findLabel:SetText("Find Group") end
                    disableFind = true
                else
                    if key == "season" then
                        dockSummary:SetText("Random Dungeon")
                    else
                        dockSummary:SetText("Random " .. catLbl .. " Dungeon")
                    end
                    if not queuedNow then findLabel:SetText("Find Group") end
                end
                ApplyPillVisual()
            end
            dtState.findDisabled = disableFind
            if R.RefreshFindBtn then R.RefreshFindBtn() end
        end

        for _, k in ipairs(CAT_KEYS) do
            tiles[k]:SetScript("OnClick", function(self)
                if dtState.catInactive[self._key] then return end
                Panel._state.dungeonCategory = self._key
                if R.UpdateDungeonTab then R.UpdateDungeonTab() end
            end)
            tiles[k]:SetScript("OnEnter", function(self)
                if dtState.catInactive[self._key] then return end
                if Panel._state.dungeonCategory ~= self._key then
                    self:SetBackdropColor(1, 1, 1, 0.04)
                end
            end)
            tiles[k]:SetScript("OnLeave", function(self)
                if dtState.catInactive[self._key] then return end
                if Panel._state.dungeonCategory ~= self._key then
                    self:SetBackdropColor(TC("glassBg"))
                end
            end)
        end

        segRandom:SetScript("OnClick", function()
            Panel._state.dungeonQueueMode = "random"
            if R.UpdateDungeonTab then R.UpdateDungeonTab() end
        end)
        segSpecific:SetScript("OnClick", function()
            Panel._state.dungeonQueueMode = "specific"
            if R.UpdateDungeonTab then R.UpdateDungeonTab() end
        end)

        Panel._state.dungeonCategory  = Panel._state.dungeonCategory  or "heroic"
        Panel._state.dungeonQueueMode = Panel._state.dungeonQueueMode or "random"
        if dtState.catInactive[Panel._state.dungeonCategory] then
            for _, k in ipairs(CAT_KEYS) do
                if not dtState.catInactive[k] and (CAT_COUNTS[k] or 0) > 0 then
                    Panel._state.dungeonCategory = k
                    break
                end
            end
        end
        LayoutStrip(strip:GetWidth())
        R.UpdateDungeonTab()
    end

    end

    do
    local raidContent = CreateFrame("Frame", nil, content)
    raidContent:SetAllPoints()
    raidContent:Hide()
    R.raidContent = raidContent

    Panel._state.raidRoles = { tank = false, healer = false, dps = false }

    local raidHero = CreateFrame("Frame", nil, raidContent)
    raidHero:SetHeight(190)
    raidHero:SetPoint("TOPLEFT", raidContent, "TOPLEFT", 0, 0)
    raidHero:SetPoint("TOPRIGHT", raidContent, "TOPRIGHT", 0, 0)
    R.raidHero = raidHero

    local raidAc = { 0.80, 0.65, 0.20 }

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

    local rhAtlas = raidHero:CreateTexture(nil, "BACKGROUND", nil, -7)
    rhAtlas:SetAllPoints()
    if not TrySetAtlasFromList(rhAtlas, HERO_ATLASES.raids) then
        rhAtlas:SetColorTexture(0, 0, 0, 0)
    end
    rhAtlas:SetAlpha(0.40)

    local rhVigTop = raidHero:CreateTexture(nil, "BACKGROUND", nil, -5)
    rhVigTop:SetHeight(80)
    rhVigTop:SetPoint("TOPLEFT", raidHero, "TOPLEFT", 0, 0)
    rhVigTop:SetPoint("TOPRIGHT", raidHero, "TOPRIGHT", 0, 0)
    rhVigTop:SetTexture(W8)
    ApplyGradient(rhVigTop, "VERTICAL", 0, 0, 0, 0, 0, 0, 0, 0.5)

    local rhVigBot = raidHero:CreateTexture(nil, "BACKGROUND", nil, -5)
    rhVigBot:SetHeight(60)
    rhVigBot:SetPoint("BOTTOMLEFT", raidHero, "BOTTOMLEFT", 0, 0)
    rhVigBot:SetPoint("BOTTOMRIGHT", raidHero, "BOTTOMRIGHT", 0, 0)
    rhVigBot:SetTexture(W8)
    ApplyGradient(rhVigBot, "VERTICAL", 0, 0, 0, 0.6, 0, 0, 0, 0)

    local rhVigLeft = raidHero:CreateTexture(nil, "BACKGROUND", nil, -5)
    rhVigLeft:SetWidth(math.floor(CFG.WIDTH * 0.40))
    rhVigLeft:SetPoint("TOPLEFT", raidHero, "TOPLEFT", 0, 0)
    rhVigLeft:SetPoint("BOTTOMLEFT", raidHero, "BOTTOMLEFT", 0, 0)
    rhVigLeft:SetTexture(W8)
    ApplyGradient(rhVigLeft, "HORIZONTAL", 0, 0, 0, 0.25, 0, 0, 0, 0)

    local rhAccentWash = raidHero:CreateTexture(nil, "BACKGROUND", nil, -4)
    rhAccentWash:SetHeight(60)
    rhAccentWash:SetPoint("BOTTOMLEFT", raidHero, "BOTTOMLEFT", 0, 0)
    rhAccentWash:SetPoint("BOTTOMRIGHT", raidHero, "BOTTOMRIGHT", 0, 0)
    rhAccentWash:SetTexture(W8)
    ApplyGradient(rhAccentWash, "VERTICAL", raidAc[1], raidAc[2], raidAc[3], 0.03, raidAc[1], raidAc[2], raidAc[3], 0)

    local rhCtxLeft = raidHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(rhCtxLeft, BODY_FONT, 9, "OUTLINE")
    rhCtxLeft:SetPoint("TOPLEFT", raidHero, "TOPLEFT", CFG.PAD + 2, -8)
    rhCtxLeft:SetTextColor(TC("mutedText"))
    rhCtxLeft:SetText("")

    local rhCtxRight = raidHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(rhCtxRight, BODY_FONT, 9, "OUTLINE")
    rhCtxRight:SetPoint("TOPRIGHT", raidHero, "TOPRIGHT", -CFG.PAD, -8)
    rhCtxRight:SetTextColor(TC("mutedText"))
    rhCtxRight:SetText("")
    R.raidHeroWingProgress = rhCtxRight

    local rhClassIcon = raidHero:CreateTexture(nil, "OVERLAY")
    rhClassIcon:SetSize(22, 22)
    rhClassIcon:SetPoint("TOP", raidHero, "TOP", -46, -34)
    rhClassIcon:SetTexture("Interface\\GLUES\\CHARACTERCREATE\\UI-CharacterCreate-Classes")
    R.raidHeroClassIcon = rhClassIcon

    local rhPlayerName = raidHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(rhPlayerName, TITLE_FONT, 14, "OUTLINE")
    rhPlayerName:SetPoint("LEFT", rhClassIcon, "RIGHT", 6, 0)
    rhPlayerName:SetTextColor(TC("bodyText"))
    R.raidHeroPlayerName = rhPlayerName

    local rhSpecLine = raidHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(rhSpecLine, BODY_FONT, 10, "")
    rhSpecLine:SetPoint("TOP", rhPlayerName, "BOTTOM", 0, -4)
    rhSpecLine:SetJustifyH("CENTER")
    rhSpecLine:SetTextColor(TC("mutedText"))
    R.raidHeroSpecLine = rhSpecLine

    local PILL_H = 28
    local PILL_GAP = 8
    local ROLE_PILL_W = 80
    local RAID_STAT_W = 180
    local totalPillW = 3 * ROLE_PILL_W + 2 * RAID_STAT_W + 4 * PILL_GAP
    local pillStartX = math.floor((CFG.WIDTH - totalPillW) / 2)
    local pillY = -82

    local function CreateRaidStatPill(xOfs, acColor)
        local pill = CreateFrame("Frame", nil, raidHero, "BackdropTemplate")
        pill:SetSize(RAID_STAT_W, PILL_H)
        pill:SetPoint("TOPLEFT", raidHero, "TOPLEFT", xOfs, pillY)
        pill:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        pill:SetBackdropColor(TC("surfaceBg"))
        pill:SetBackdropBorderColor(0, 0, 0, 0)
        local dot = pill:CreateTexture(nil, "OVERLAY", nil, 2)
        dot:SetSize(4, PILL_H - 8)
        dot:SetPoint("LEFT", pill, "LEFT", 4, 0)
        dot:SetTexture(W8)
        if dot.SetGradient and CreateColor then
            dot:SetGradient("VERTICAL",
                CreateColor(acColor[1], acColor[2], acColor[3], 0.3),
                CreateColor(acColor[1], acColor[2], acColor[3], 1.0))
        else
            dot:SetColorTexture(acColor[1], acColor[2], acColor[3])
        end
        pill._dot = dot
        local lbl = pill:CreateFontString(nil, "OVERLAY")
        TrySetFont(lbl, BODY_FONT, 9, "OUTLINE")
        lbl:SetPoint("LEFT", dot, "RIGHT", 6, 0)
        lbl:SetTextColor(TC("mutedText"))
        pill._label = lbl
        local val = pill:CreateFontString(nil, "OVERLAY")
        TrySetFont(val, TITLE_FONT, 11, "OUTLINE")
        val:SetPoint("RIGHT", pill, "RIGHT", -8, 0)
        val:SetJustifyH("RIGHT")
        val:SetTextColor(TC("bodyText"))
        pill._value = val
        local frost = pill:CreateTexture(nil, "OVERLAY", nil, 3)
        frost:SetHeight(1)
        frost:SetPoint("TOPLEFT", pill, "TOPLEFT", 1, -1)
        frost:SetPoint("TOPRIGHT", pill, "TOPRIGHT", -1, -1)
        frost:SetTexture(W8)
        if frost.SetGradient and CreateColor then
            frost:SetGradient("HORIZONTAL",
                CreateColor(1, 1, 1, 0.01),
                CreateColor(1, 1, 1, 0.04))
        else
            frost:SetColorTexture(1, 1, 1, 0.04)
        end
        local botLine = pill:CreateTexture(nil, "BACKGROUND", nil, 2)
        botLine:SetHeight(1)
        botLine:SetPoint("BOTTOMLEFT", pill, "BOTTOMLEFT", 1, 1)
        botLine:SetPoint("BOTTOMRIGHT", pill, "BOTTOMRIGHT", -1, 1)
        botLine:SetColorTexture(0, 0, 0, 0.08)
        return pill
    end

    local raidRoleDefs = {
        { key = "tank",   atlas = "UI-LFG-RoleIcon-Tank",   label = "Tank" },
        { key = "healer", atlas = "UI-LFG-RoleIcon-Healer", label = "Healer" },
        { key = "dps",    atlas = "UI-LFG-RoleIcon-DPS",    label = "DPS" },
    }
    R.raidRoleCards = {}

    for ri, rd in ipairs(raidRoleDefs) do
        local rpill = CreateFrame("Button", nil, raidHero, "BackdropTemplate")
        rpill:SetSize(ROLE_PILL_W, PILL_H)
        rpill:SetPoint("TOPLEFT", raidHero, "TOPLEFT", pillStartX + (ri - 1) * (ROLE_PILL_W + PILL_GAP), pillY)
        rpill:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        rpill:SetBackdropColor(TC("surfaceBg"))
        rpill:SetBackdropBorderColor(0, 0, 0, 0)
        local selGlow = rpill:CreateTexture(nil, "BACKGROUND", nil, 1)
        selGlow:SetPoint("TOPLEFT", rpill, "TOPLEFT", 1, -1)
        selGlow:SetPoint("BOTTOMRIGHT", rpill, "BOTTOMRIGHT", -1, 1)
        selGlow:SetColorTexture(0, 0, 0, 0)
        rpill._selGlow = selGlow
        local ricon = rpill:CreateTexture(nil, "ARTWORK")
        ricon:SetSize(16, 16)
        ricon:SetPoint("LEFT", rpill, "LEFT", 8, 0)
        pcall(ricon.SetAtlas, ricon, rd.atlas)
        ricon:SetDesaturated(true)
        ricon:SetAlpha(0.4)
        rpill._icon = ricon
        local rlbl = rpill:CreateFontString(nil, "OVERLAY")
        TrySetFont(rlbl, BODY_FONT, 10, "OUTLINE")
        rlbl:SetPoint("LEFT", ricon, "RIGHT", 5, 0)
        rlbl:SetTextColor(TC("mutedText"))
        rlbl:SetText(rd.label)
        rpill._label = rlbl
        rpill._roleKey = rd.key
        local frost = rpill:CreateTexture(nil, "OVERLAY", nil, 3)
        frost:SetHeight(1)
        frost:SetPoint("TOPLEFT", rpill, "TOPLEFT", 1, -1)
        frost:SetPoint("TOPRIGHT", rpill, "TOPRIGHT", -1, -1)
        frost:SetTexture(W8)
        if frost.SetGradient and CreateColor then
            frost:SetGradient("HORIZONTAL",
                CreateColor(1, 1, 1, 0.01),
                CreateColor(1, 1, 1, 0.04))
        else
            frost:SetColorTexture(1, 1, 1, 0.04)
        end
        rpill:SetScript("OnClick", function(self)
            Panel._state.raidRoles[self._roleKey] = not Panel._state.raidRoles[self._roleKey]
            Panel.UpdateRaidRoleCards()
        end)
        rpill:SetScript("OnEnter", function(self)
            if not Panel._state.raidRoles[self._roleKey] then self._icon:SetAlpha(0.7) end
        end)
        rpill:SetScript("OnLeave", function(self)
            if not Panel._state.raidRoles[self._roleKey] then self._icon:SetAlpha(0.4) end
        end)
        R.raidRoleCards[ri] = rpill
    end
    R.raidRoleBtns = R.raidRoleCards

    local raidTierPill = CreateRaidStatPill(pillStartX + 3 * (ROLE_PILL_W + PILL_GAP), raidAc)
    raidTierPill._label:SetText("TIER")
    raidTierPill._value:SetText(DeriveRaidTierName())
    R.raidHeroTierName = raidTierPill._value
    R.raidTierName = raidTierPill._value

    local raidBossPill = CreateRaidStatPill(pillStartX + 3 * (ROLE_PILL_W + PILL_GAP) + RAID_STAT_W + PILL_GAP, raidAc)
    raidBossPill._label:SetText("BOSSES")
    raidBossPill._value:SetText("--")
    R.raidHeroBossCount = raidBossPill._value

    local WING_BAR_W = 200
    local WING_BAR_H = 24
    local WING_BAR_GAP = 5
    local WING_BAR_COLS = 4
    R.raidWingBars = {}

    local wingAccents = {
        { 0.80, 0.65, 0.20 }, { 0.60, 0.30, 0.30 },
        { 0.30, 0.50, 0.80 }, { 0.50, 0.70, 0.30 },
    }

    for wi = 1, 4 do
        local col = (wi - 1) % WING_BAR_COLS
        local row = math.floor((wi - 1) / WING_BAR_COLS)
        local totalW = WING_BAR_COLS * WING_BAR_W + (WING_BAR_COLS - 1) * WING_BAR_GAP
        local startX = math.floor((CFG.WIDTH - totalW) / 2)
        local xOfs = startX + col * (WING_BAR_W + WING_BAR_GAP)
        local yOfs = -120 - row * (WING_BAR_H + WING_BAR_GAP)

        local bar = CreateFrame("Frame", nil, raidHero, "BackdropTemplate")
        bar:SetSize(WING_BAR_W, WING_BAR_H)
        bar:SetPoint("TOPLEFT", raidHero, "TOPLEFT", xOfs, yOfs)
        bar:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        bar:SetBackdropColor(TC("surfaceBg"))
        bar:SetBackdropBorderColor(0, 0, 0, 0)

        local wAc = wingAccents[wi] or wingAccents[1]

        local acBar = bar:CreateTexture(nil, "OVERLAY", nil, 2)
        acBar:SetSize(3, WING_BAR_H)
        acBar:SetPoint("LEFT", bar, "LEFT", 0, 0)
        acBar:SetTexture(W8)
        if acBar.SetGradient and CreateColor then
            acBar:SetGradient("VERTICAL",
                CreateColor(wAc[1], wAc[2], wAc[3], 0.25),
                CreateColor(wAc[1], wAc[2], wAc[3], 0.7))
        else
            acBar:SetColorTexture(wAc[1], wAc[2], wAc[3], 0.7)
        end

        local fill = bar:CreateTexture(nil, "ARTWORK", nil, 1)
        fill:SetHeight(WING_BAR_H - 2)
        fill:SetPoint("LEFT", bar, "LEFT", 1, 0)
        fill:SetWidth(1)
        fill:SetColorTexture(wAc[1], wAc[2], wAc[3], 0.15)
        bar._fill = fill

        local nameText = bar:CreateFontString(nil, "OVERLAY")
        TrySetFont(nameText, BODY_FONT, 9, "OUTLINE")
        nameText:SetPoint("LEFT", bar, "LEFT", 8, 0)
        nameText:SetTextColor(wAc[1], wAc[2], wAc[3])
        nameText:SetText("")
        bar._nameText = nameText

        local countText = bar:CreateFontString(nil, "OVERLAY")
        TrySetFont(countText, TITLE_FONT, 10, "OUTLINE")
        countText:SetPoint("RIGHT", bar, "RIGHT", -6, 0)
        countText:SetJustifyH("RIGHT")
        countText:SetTextColor(TC("mutedText"))
        countText:SetText("")
        bar._countText = countText

        bar._accent = wAc
        bar:Hide()
        R.raidWingBars[wi] = bar
    end

    local rhTrack = raidHero:CreateTexture(nil, "ARTWORK", nil, 1)
    rhTrack:SetHeight(3)
    rhTrack:SetPoint("BOTTOMLEFT", raidHero, "BOTTOMLEFT", CFG.PAD, 5)
    rhTrack:SetPoint("BOTTOMRIGHT", raidHero, "BOTTOMRIGHT", -CFG.PAD, 5)
    rhTrack:SetColorTexture(1, 1, 1, 0.04)
    R.raidHeroTrack = rhTrack

    local rhTrackHL = raidHero:CreateTexture(nil, "ARTWORK", nil, 1)
    rhTrackHL:SetHeight(1)
    rhTrackHL:SetPoint("TOPLEFT", rhTrack, "TOPLEFT", 0, 0)
    rhTrackHL:SetPoint("TOPRIGHT", rhTrack, "TOPRIGHT", 0, 0)
    rhTrackHL:SetColorTexture(1, 1, 1, 0.06)

    local rhFill = raidHero:CreateTexture(nil, "ARTWORK", nil, 2)
    rhFill:SetHeight(3)
    rhFill:SetPoint("LEFT", rhTrack, "LEFT", 0, 0)
    rhFill:SetWidth(1)
    rhFill:SetColorTexture(raidAc[1], raidAc[2], raidAc[3], 0.55)
    R.raidHeroFill = rhFill

    local rhGlassDivL = raidHero:CreateTexture(nil, "OVERLAY", nil, 3)
    rhGlassDivL:SetHeight(1)
    rhGlassDivL:SetPoint("BOTTOMLEFT", raidHero, "BOTTOMLEFT", 0, 0)
    rhGlassDivL:SetPoint("BOTTOM", raidHero, "BOTTOM", 0, 0)
    rhGlassDivL:SetTexture(W8)
    ApplyGradient(rhGlassDivL, "HORIZONTAL", 1, 1, 1, 0, 1, 1, 1, 0.06)

    local rhGlassDivR = raidHero:CreateTexture(nil, "OVERLAY", nil, 3)
    rhGlassDivR:SetHeight(1)
    rhGlassDivR:SetPoint("BOTTOM", raidHero, "BOTTOM", 0, 0)
    rhGlassDivR:SetPoint("BOTTOMRIGHT", raidHero, "BOTTOMRIGHT", 0, 0)
    rhGlassDivR:SetTexture(W8)
    ApplyGradient(rhGlassDivR, "HORIZONTAL", 1, 1, 1, 0.06, 1, 1, 1, 0)

    local raidQueueBar = CreateFrame("Frame", nil, raidContent, "BackdropTemplate")
    raidQueueBar:SetHeight(30)
    raidQueueBar:SetPoint("TOPLEFT", raidHero, "BOTTOMLEFT", 0, 0)
    raidQueueBar:SetPoint("TOPRIGHT", raidHero, "BOTTOMRIGHT", 0, 0)
    raidQueueBar:SetBackdrop({
        bgFile = W8,
        edgeFile = W8,
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    local rqbAc = activeTheme.accent
    raidQueueBar:SetBackdropColor(rqbAc[1], rqbAc[2], rqbAc[3], 0.10)
    raidQueueBar:SetBackdropBorderColor(rqbAc[1], rqbAc[2], rqbAc[3], 0.20)
    raidQueueBar:Hide()
    R.raidQueueBar = raidQueueBar

    local raidQueueText = raidQueueBar:CreateFontString(nil, "OVERLAY")
    TrySetFont(raidQueueText, BODY_FONT, 11, "OUTLINE")
    raidQueueText:SetPoint("LEFT", raidQueueBar, "LEFT", 12, 0)
    raidQueueText:SetTextColor(TC("bodyText"))
    raidQueueText:SetText("Queued...")
    R.raidQueueText = raidQueueText

    local raidCancelBtn = CreateFrame("Button", nil, raidQueueBar, "BackdropTemplate")
    raidCancelBtn:SetSize(60, 24)
    raidCancelBtn:SetPoint("RIGHT", raidQueueBar, "RIGHT", -8, 0)
    raidCancelBtn:SetBackdrop({
        bgFile = W8,
        edgeFile = W8,
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    raidCancelBtn:SetBackdropColor(0.3, 0.05, 0.05, 0.6)
    raidCancelBtn:SetBackdropBorderColor(1, 0.3, 0.3, 0.3)
    local raidCancelLabel = raidCancelBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(raidCancelLabel, BODY_FONT, 10, "OUTLINE")
    raidCancelLabel:SetPoint("CENTER")
    raidCancelLabel:SetTextColor(1, 0.4, 0.4)
    raidCancelLabel:SetText("Cancel")
    raidCancelBtn:SetScript("OnClick", function()
        pcall(LeaveLFG, LE_LFG_CATEGORY_RF or 2)
        C_Timer.After(0.3, function() Panel.UpdateRaidQueueStatus() end)
    end)
    raidCancelBtn:SetScript("OnEnter", function(self)
        self:SetBackdropColor(0.4, 0.08, 0.08, 0.8)
    end)
    raidCancelBtn:SetScript("OnLeave", function(self)
        self:SetBackdropColor(0.3, 0.05, 0.05, 0.6)
    end)

    local raidNoWingsMsg = raidContent:CreateFontString(nil, "OVERLAY")
    TrySetFont(raidNoWingsMsg, TITLE_FONT, 14, "OUTLINE")
    raidNoWingsMsg:SetPoint("CENTER", raidContent, "CENTER", 0, 0)
    raidNoWingsMsg:SetTextColor(TC("mutedText"))
    raidNoWingsMsg:SetText("No LFR wings available at your level")
    raidNoWingsMsg:Hide()
    R.raidNoWingsMsg = raidNoWingsMsg

    local WING_CARD_H = 60
    local WING_GAP = 6
    local RAID_BOTTOM_BAR_H = 50
    local wingAccents = {
        { 0.80, 0.65, 0.20 },
        { 0.60, 0.30, 0.30 },
        { 0.30, 0.50, 0.80 },
        { 0.50, 0.70, 0.30 },
    }

    R.raidWingCards = {}
    for i = 1, 4 do
        local wcard = CreateFrame("Button", nil, raidContent, "BackdropTemplate")
        wcard:SetHeight(WING_CARD_H)
        wcard:SetPoint("LEFT", raidContent, "LEFT", CFG.PAD, 0)
        wcard:SetPoint("RIGHT", raidContent, "RIGHT", -CFG.PAD, 0)
        local yOff = -(8 + (i - 1) * (WING_CARD_H + WING_GAP))
        wcard:SetPoint("TOP", raidQueueBar, "BOTTOM", 0, yOff)

        wcard:SetBackdrop({
            bgFile = W8,
            edgeFile = W8,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        wcard:SetBackdropColor(TC("surfaceBg"))
        wcard:SetBackdropBorderColor(0, 0, 0, 0)

        local wFrost = wcard:CreateTexture(nil, "OVERLAY", nil, 3)
        wFrost:SetHeight(1)
        wFrost:SetPoint("TOPLEFT", wcard, "TOPLEFT", 1, -1)
        wFrost:SetPoint("TOPRIGHT", wcard, "TOPRIGHT", -1, -1)
        wFrost:SetTexture(W8)
        if wFrost.SetGradient and CreateColor then
            wFrost:SetGradient("HORIZONTAL",
                CreateColor(1, 1, 1, 0.01),
                CreateColor(1, 1, 1, 0.06))
        else
            wFrost:SetColorTexture(1, 1, 1, 0.06)
        end

        local wAccent = wingAccents[i] or wingAccents[1]
        local wAccBar = wcard:CreateTexture(nil, "OVERLAY", nil, 2)
        wAccBar:SetSize(4, WING_CARD_H)
        wAccBar:SetPoint("LEFT", wcard, "LEFT", 0, 0)
        wAccBar:SetTexture(W8)
        if wAccBar.SetGradient and CreateColor then
            wAccBar:SetGradient("VERTICAL",
                CreateColor(wAccent[1], wAccent[2], wAccent[3], 0.3),
                CreateColor(wAccent[1], wAccent[2], wAccent[3], 1.0))
        else
            wAccBar:SetColorTexture(wAccent[1], wAccent[2], wAccent[3], 1)
        end
        wcard._accentBar = wAccBar
        wcard._wingAccent = wAccent

        local wNameText = wcard:CreateFontString(nil, "OVERLAY")
        TrySetFont(wNameText, TITLE_FONT, 13, "OUTLINE")
        wNameText:SetPoint("TOPLEFT", wcard, "TOPLEFT", 14, -12)
        wNameText:SetPoint("RIGHT", wcard, "RIGHT", -12, 0)
        wNameText:SetJustifyH("LEFT")
        wNameText:SetTextColor(TC("bodyText"))
        wNameText:SetText("Wing " .. i)
        wcard._nameText = wNameText

        local wInfoText = wcard:CreateFontString(nil, "OVERLAY")
        TrySetFont(wInfoText, BODY_FONT, 10, "")
        wInfoText:SetPoint("BOTTOMLEFT", wcard, "BOTTOMLEFT", 14, 10)
        wInfoText:SetPoint("RIGHT", wcard, "RIGHT", -12, 0)
        wInfoText:SetJustifyH("LEFT")
        wInfoText:SetTextColor(TC("mutedText"))
        wInfoText:SetText("Select to queue")
        wcard._infoText = wInfoText

        wcard._selected = false
        wcard._wingID = nil

        wcard:SetScript("OnEnter", function(self)
            if not self._selected then
                local wa = self._wingAccent
                self:SetBackdropColor(wa[1], wa[2], wa[3], 0.05)
            end
        end)
        wcard:SetScript("OnLeave", function(self)
            if not self._selected then
                self:SetBackdropColor(TC("surfaceBg"))
                self:SetBackdropBorderColor(0, 0, 0, 0)
            end
        end)

        wcard:SetScript("OnClick", function(self)
            self._selected = not self._selected
            if self._selected then
                local ac = self._wingAccent
                self:SetBackdropColor(ac[1], ac[2], ac[3], 0.10)
                self:SetBackdropBorderColor(ac[1], ac[2], ac[3], 0.20)
            else
                self:SetBackdropColor(TC("surfaceBg"))
                self:SetBackdropBorderColor(0, 0, 0, 0)
            end
        end)

        wcard:Hide()
        R.raidWingCards[i] = wcard
    end

    local raidBottomBar = CreateFrame("Frame", nil, raidContent, "BackdropTemplate")
    raidBottomBar:SetHeight(RAID_BOTTOM_BAR_H)
    raidBottomBar:SetPoint("BOTTOMLEFT", raidContent, "BOTTOMLEFT", 0, 0)
    raidBottomBar:SetPoint("BOTTOMRIGHT", raidContent, "BOTTOMRIGHT", 0, 0)
    raidBottomBar:SetBackdrop({
        bgFile = W8,
        edgeFile = W8,
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    raidBottomBar:SetBackdropColor(0, 0, 0, 0.30)
    raidBottomBar:SetBackdropBorderColor(1, 1, 1, 0.05)
    R.raidBottomBar = raidBottomBar

    local rbbDiv = raidBottomBar:CreateTexture(nil, "OVERLAY", nil, 2)
    rbbDiv:SetHeight(1)
    rbbDiv:SetPoint("TOPLEFT", raidBottomBar, "TOPLEFT", 0, 0)
    rbbDiv:SetPoint("TOPRIGHT", raidBottomBar, "TOPRIGHT", 0, 0)
    rbbDiv:SetColorTexture(TC("divider"))

    local RAID_ROLE_SIZE = 32
    local raidRoleData = {
        { key = "tank",   atlas = "roleicon-tank" },
        { key = "healer", atlas = "roleicon-healer" },
        { key = "dps",    atlas = "roleicon-dps" },
    }
    R.raidRoleBtns = {}

    for ri, rd in ipairs(raidRoleData) do
        local rbtn = CreateFrame("Button", nil, raidBottomBar)
        rbtn:SetSize(RAID_ROLE_SIZE, RAID_ROLE_SIZE)
        rbtn:SetPoint("LEFT", raidBottomBar, "LEFT", 12 + (ri - 1) * (RAID_ROLE_SIZE + 6), 0)

        local ricon = rbtn:CreateTexture(nil, "ARTWORK")
        ricon:SetSize(RAID_ROLE_SIZE - 4, RAID_ROLE_SIZE - 4)
        ricon:SetPoint("CENTER")
        ricon:SetAtlas(rd.atlas)
        ricon:SetDesaturated(true)
        ricon:SetAlpha(0.4)
        rbtn._icon = ricon
        rbtn._roleKey = rd.key

        rbtn:SetScript("OnClick", function(self)
            local st = Panel._state.raidRoles
            st[self._roleKey] = not st[self._roleKey]
            if st[self._roleKey] then
                self._icon:SetDesaturated(false)
                self._icon:SetAlpha(1.0)
            else
                self._icon:SetDesaturated(true)
                self._icon:SetAlpha(0.4)
            end
        end)

        rbtn:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_TOP")
            GameTooltip:SetText(rd.key:sub(1,1):upper() .. rd.key:sub(2))
            GameTooltip:Show()
        end)
        rbtn:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)

        R.raidRoleBtns[ri] = rbtn
    end

    local raidQueueBtn = CreateFrame("Button", nil, raidBottomBar, "BackdropTemplate")
    raidQueueBtn:SetSize(140, 32)
    raidQueueBtn:SetPoint("RIGHT", raidBottomBar, "RIGHT", -12, 0)
    raidQueueBtn:SetBackdrop({
        bgFile = W8,
        edgeFile = W8,
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    raidQueueBtn:SetBackdropColor(TC("glassBg"))
    raidQueueBtn:SetBackdropBorderColor(TC("glassBorder"))

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

    local raidQueueBtnLabel = raidQueueBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(raidQueueBtnLabel, TITLE_FONT, 12, "OUTLINE")
    raidQueueBtnLabel:SetPoint("CENTER")
    raidQueueBtnLabel:SetTextColor(TC("accent"))
    raidQueueBtnLabel:SetText("Queue for Selected")

    raidQueueBtn:SetScript("OnEnter", function(self)
        local r, g, b = TC("glassBg")
        self:SetBackdropColor(r + 0.05, g + 0.05, b + 0.05, 0.85)
    end)
    raidQueueBtn:SetScript("OnLeave", function(self)
        self:SetBackdropColor(TC("glassBg"))
    end)

    raidQueueBtn:SetScript("OnClick", function()
        local roles = Panel._state.raidRoles
        pcall(SetLFGRoles, false, roles.tank, roles.healer, roles.dps)
        local lfgCat = LE_LFG_CATEGORY_RF or 2

        local foundWing = false
        for _, wcard in ipairs(R.raidWingCards or {}) do
            if wcard._selected and wcard._wingID then
                Panel._state._raidQueuedWingName = wcard._nameText and wcard._nameText:GetText() or ("Wing " .. tostring(wcard._wingID))
                pcall(JoinSingleLFG, lfgCat, wcard._wingID)
                foundWing = true
                break
            end
        end

        if not foundWing then
            Panel.ShowToast("No Wing Selected", "Select a raid wing to queue for.", 4)
        else
            Panel._state.raidQueueAttempted = true
            local queuedWingName = Panel._state._raidQueuedWingName or "selected raid wing"
            C_Timer.After(1, function()
                if Panel._state.raidQueueAttempted then
                    Panel._state.raidQueueAttempted = false
                    local okQ, hasData = pcall(GetLFGQueueStats, lfgCat)
                    if not (okQ and hasData) then
                        Panel.ShowToast("Queue Rejected", queuedWingName .. "\n\nYou do not meet the requirements.\nCheck your item level, role, and raid eligibility.", 8)
                    else
                        Panel.UpdateRaidQueueStatus()
                    end
                end
            end)
        end
        C_Timer.After(0.5, function() Panel.UpdateRaidQueueStatus() end)
    end)
    R.raidQueueBtn = raidQueueBtn

    end

    do
    local pvpContent = CreateFrame("Frame", nil, content)
    pvpContent:SetAllPoints()
    pvpContent:Hide()
    R.pvpContent = pvpContent

    local PVP_CATEGORY_DEFS = {
        { key = "quickmatch", label = "Quick Match" },
        { key = "rated",      label = "Rated" },
        { key = "premade",    label = "Premade Groups" },
        { key = "training",   label = "Training Grounds" },
    }
    local PVP_CAT_ACCENTS = {
        quickmatch = { 0.00, 0.78, 1.00 },
        rated      = { 1.00, 0.60, 0.00 },
        premade    = { 0.55, 0.55, 0.55 },
        training   = { 0.35, 0.75, 0.40 },
    }

    local QUICKMATCH_ACTIVITIES = {
        { name = "Random Battlegrounds",      isRandom = true, queueType = "random_bg" },
        { name = "Random Epic Battlegrounds", isRandom = true, queueType = "random_epic" },
        { name = "Arena Skirmish",            isSkirmish = true, queueType = "skirmish" },
        { name = "Brawl",                     isBrawl = true, queueType = "brawl", minLevel = 90 },
    }
    local SPECIFIC_BGS = {
        { name = "Warsong Gulch",           teamSize = "10v10" },
        { name = "Arathi Basin",            teamSize = "15v15" },
        { name = "Deephaul Ravine",         teamSize = "10v10" },
        { name = "Alterac Valley",          teamSize = "40v40" },
        { name = "Eye of the Storm",        teamSize = "15v15" },
        { name = "Isle of Conquest",        teamSize = "40v40" },
        { name = "The Battle for Gilneas",  teamSize = "10v10" },
        { name = "Battle for Wintergrasp",  teamSize = "40v40" },
        { name = "Ashran",                  teamSize = "40v40" },
        { name = "Twin Peaks",              teamSize = "10v10" },
        { name = "Silvershard Mines",       teamSize = "10v10" },
        { name = "Temple of Kotmogu",       teamSize = "10v10" },
        { name = "Seething Shore",          teamSize = "10v10" },
        { name = "Deepwind Gorge",          teamSize = "15v15" },
    }
    local RATED_ACTIVITIES = {
        { name = "Solo Shuffle (Arena)",        bracket = 7, queueType = "solo_arena" },
        { name = "Solo Shuffle (Battlegrounds)", bracket = 4, queueType = "solo_bg" },
        { name = "2v2 Arena",                   bracket = 1, queueType = "arena2v2" },
        { name = "3v3 Arena",                   bracket = 2, queueType = "arena3v3" },
        { name = "10v10 Rated Battlegrounds",   bracket = 3, queueType = "rated_bg" },
    }
    local TRAINING_ACTIVITIES = {
        { name = "Random Training Battlegrounds", isRandom = true, queueType = "training_random" },
        { name = "The Battle for Gilneas Training Grounds", queueType = "training_gilneas" },
        { name = "Silvershard Mines Training Grounds",      queueType = "training_silvershard" },
        { name = "Arathi Basin Training Grounds",           queueType = "training_arathi" },
    }

    local pvpHero = CreateFrame("Frame", nil, pvpContent)
    pvpHero:SetHeight(190)
    pvpHero:SetPoint("TOPLEFT", pvpContent, "TOPLEFT", 0, 0)
    pvpHero:SetPoint("TOPRIGHT", pvpContent, "TOPRIGHT", 0, 0)
    R.pvpHero = pvpHero

    local pvpAc = { 0.75, 0.30, 0.30 }

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

    local pvpHeroAtlas = pvpHero:CreateTexture(nil, "BACKGROUND", nil, -7)
    pvpHeroAtlas:SetAllPoints()
    if not TrySetAtlasFromList(pvpHeroAtlas, HERO_ATLASES.premade) then
        pvpHeroAtlas:SetColorTexture(0, 0, 0, 0)
    end
    pvpHeroAtlas:SetAlpha(0.40)

    local pvpHeroVigTop = pvpHero:CreateTexture(nil, "BACKGROUND", nil, -5)
    pvpHeroVigTop:SetHeight(80)
    pvpHeroVigTop:SetPoint("TOPLEFT", pvpHero, "TOPLEFT", 0, 0)
    pvpHeroVigTop:SetPoint("TOPRIGHT", pvpHero, "TOPRIGHT", 0, 0)
    pvpHeroVigTop:SetTexture(W8)
    ApplyGradient(pvpHeroVigTop, "VERTICAL", 0, 0, 0, 0, 0, 0, 0, 0.5)

    local pvpHeroVigBot = pvpHero:CreateTexture(nil, "BACKGROUND", nil, -5)
    pvpHeroVigBot:SetHeight(60)
    pvpHeroVigBot:SetPoint("BOTTOMLEFT", pvpHero, "BOTTOMLEFT", 0, 0)
    pvpHeroVigBot:SetPoint("BOTTOMRIGHT", pvpHero, "BOTTOMRIGHT", 0, 0)
    pvpHeroVigBot:SetTexture(W8)
    ApplyGradient(pvpHeroVigBot, "VERTICAL", 0, 0, 0, 0.6, 0, 0, 0, 0)

    local pvpCtxLeft = pvpHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(pvpCtxLeft, BODY_FONT, 9, "OUTLINE")
    pvpCtxLeft:SetPoint("TOPLEFT", pvpHero, "TOPLEFT", CFG.PAD + 2, -8)
    pvpCtxLeft:SetTextColor(TC("mutedText"))
    pvpCtxLeft:SetText("PVP")

    local pvpCtxRight = pvpHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(pvpCtxRight, BODY_FONT, 9, "OUTLINE")
    pvpCtxRight:SetPoint("TOPRIGHT", pvpHero, "TOPRIGHT", -CFG.PAD, -8)
    pvpCtxRight:SetTextColor(TC("mutedText"))
    pvpCtxRight:SetText("")
    R.pvpSeasonValue = pvpCtxRight

    local pvpClassIcon = pvpHero:CreateTexture(nil, "OVERLAY")
    pvpClassIcon:SetSize(22, 22)
    pvpClassIcon:SetPoint("TOP", pvpHero, "TOP", -46, -34)
    pvpClassIcon:SetTexture("Interface\\GLUES\\CHARACTERCREATE\\UI-CharacterCreate-Classes")
    R.pvpHeroClassIcon = pvpClassIcon

    local pvpPlayerName = pvpHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(pvpPlayerName, TITLE_FONT, 14, "OUTLINE")
    pvpPlayerName:SetPoint("LEFT", pvpClassIcon, "RIGHT", 6, 0)
    pvpPlayerName:SetTextColor(TC("bodyText"))
    R.pvpHeroPlayerName = pvpPlayerName

    local pvpSpecLine = pvpHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(pvpSpecLine, BODY_FONT, 10, "")
    pvpSpecLine:SetPoint("TOP", pvpPlayerName, "BOTTOM", 0, -4)
    pvpSpecLine:SetJustifyH("CENTER")
    pvpSpecLine:SetTextColor(TC("mutedText"))
    R.pvpHeroSpecLine = pvpSpecLine

    local PILL_H = 28
    local PILL_GAP = 8
    local ROLE_PILL_W = 80
    local STAT_PILL_W = 140
    local totalPillW = 3 * ROLE_PILL_W + 2 * STAT_PILL_W + 4 * PILL_GAP
    local pillStartX = math.floor((CFG.WIDTH - totalPillW) / 2)
    local pillY = -82

    local function CreatePVPStatPill(xOfs, acColor)
        local pill = CreateFrame("Frame", nil, pvpHero, "BackdropTemplate")
        pill:SetSize(STAT_PILL_W, PILL_H)
        pill:SetPoint("TOPLEFT", pvpHero, "TOPLEFT", xOfs, pillY)
        pill:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        pill:SetBackdropColor(TC("surfaceBg"))
        pill:SetBackdropBorderColor(0, 0, 0, 0)
        local dot = pill:CreateTexture(nil, "OVERLAY", nil, 2)
        dot:SetSize(4, PILL_H - 8)
        dot:SetPoint("LEFT", pill, "LEFT", 4, 0)
        dot:SetTexture(W8)
        if dot.SetGradient and CreateColor then
            dot:SetGradient("VERTICAL",
                CreateColor(acColor[1], acColor[2], acColor[3], 0.3),
                CreateColor(acColor[1], acColor[2], acColor[3], 1.0))
        else
            dot:SetColorTexture(acColor[1], acColor[2], acColor[3])
        end
        local lbl = pill:CreateFontString(nil, "OVERLAY")
        TrySetFont(lbl, BODY_FONT, 9, "OUTLINE")
        lbl:SetPoint("LEFT", dot, "RIGHT", 6, 0)
        lbl:SetTextColor(TC("mutedText"))
        pill._label = lbl
        local val = pill:CreateFontString(nil, "OVERLAY")
        TrySetFont(val, TITLE_FONT, 11, "OUTLINE")
        val:SetPoint("RIGHT", pill, "RIGHT", -8, 0)
        val:SetJustifyH("RIGHT")
        val:SetTextColor(TC("bodyText"))
        pill._value = val
        local frost = pill:CreateTexture(nil, "OVERLAY", nil, 3)
        frost:SetHeight(1)
        frost:SetPoint("TOPLEFT", pill, "TOPLEFT", 1, -1)
        frost:SetPoint("TOPRIGHT", pill, "TOPRIGHT", -1, -1)
        frost:SetTexture(W8)
        if frost.SetGradient and CreateColor then
            frost:SetGradient("HORIZONTAL",
                CreateColor(1, 1, 1, 0.01),
                CreateColor(1, 1, 1, 0.04))
        else
            frost:SetColorTexture(1, 1, 1, 0.04)
        end
        return pill
    end

    local pvpRoleDefs = {
        { key = "tank",   atlas = "UI-LFG-RoleIcon-Tank",   label = "Tank" },
        { key = "healer", atlas = "UI-LFG-RoleIcon-Healer", label = "Healer" },
        { key = "dps",    atlas = "UI-LFG-RoleIcon-DPS",    label = "DPS" },
    }
    R.pvpRoleCards = {}

    for ri, rd in ipairs(pvpRoleDefs) do
        local rpill = CreateFrame("Button", nil, pvpHero, "BackdropTemplate")
        rpill:SetSize(ROLE_PILL_W, PILL_H)
        rpill:SetPoint("TOPLEFT", pvpHero, "TOPLEFT", pillStartX + (ri - 1) * (ROLE_PILL_W + PILL_GAP), pillY)
        rpill:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        rpill:SetBackdropColor(TC("surfaceBg"))
        rpill:SetBackdropBorderColor(0, 0, 0, 0)
        local selGlow = rpill:CreateTexture(nil, "BACKGROUND", nil, 1)
        selGlow:SetPoint("TOPLEFT", rpill, "TOPLEFT", 1, -1)
        selGlow:SetPoint("BOTTOMRIGHT", rpill, "BOTTOMRIGHT", -1, 1)
        selGlow:SetColorTexture(0, 0, 0, 0)
        rpill._selGlow = selGlow
        local ricon = rpill:CreateTexture(nil, "ARTWORK")
        ricon:SetSize(16, 16)
        ricon:SetPoint("LEFT", rpill, "LEFT", 8, 0)
        pcall(ricon.SetAtlas, ricon, rd.atlas)
        ricon:SetDesaturated(true)
        ricon:SetAlpha(0.4)
        rpill._icon = ricon
        local rlbl = rpill:CreateFontString(nil, "OVERLAY")
        TrySetFont(rlbl, BODY_FONT, 10, "OUTLINE")
        rlbl:SetPoint("LEFT", ricon, "RIGHT", 5, 0)
        rlbl:SetTextColor(TC("mutedText"))
        rlbl:SetText(rd.label)
        rpill._label = rlbl
        rpill._roleKey = rd.key
        local frost = rpill:CreateTexture(nil, "OVERLAY", nil, 3)
        frost:SetHeight(1)
        frost:SetPoint("TOPLEFT", rpill, "TOPLEFT", 1, -1)
        frost:SetPoint("TOPRIGHT", rpill, "TOPRIGHT", -1, -1)
        frost:SetColorTexture(1, 1, 1, 0.08)
        rpill:SetScript("OnClick", function(self)
            Panel._state.pvpRoles[self._roleKey] = not Panel._state.pvpRoles[self._roleKey]
            Panel.UpdatePVPRoleCards()
        end)
        rpill:SetScript("OnEnter", function(self)
            if not Panel._state.pvpRoles[self._roleKey] then self._icon:SetAlpha(0.7) end
        end)
        rpill:SetScript("OnLeave", function(self)
            if not Panel._state.pvpRoles[self._roleKey] then self._icon:SetAlpha(0.4) end
        end)
        R.pvpRoleCards[ri] = rpill
    end
    R.pvpRoleBtns = R.pvpRoleCards

    local honorPill = CreatePVPStatPill(pillStartX + 3 * (ROLE_PILL_W + PILL_GAP), { 1.0, 0.85, 0.0 })
    honorPill._label:SetText("HONOR LVL")
    honorPill._value:SetText("0")
    R.pvpHonorValue = honorPill._value

    local ratingPill = CreatePVPStatPill(pillStartX + 3 * (ROLE_PILL_W + PILL_GAP) + STAT_PILL_W + PILL_GAP, { 1.0, 0.60, 0.0 })
    ratingPill._label:SetText("HIGHEST")
    ratingPill._value:SetText("--")
    R.pvpRatingValue = ratingPill._value

    local pvpConqTrack = pvpHero:CreateTexture(nil, "ARTWORK", nil, 1)
    pvpConqTrack:SetHeight(3)
    pvpConqTrack:SetPoint("BOTTOMLEFT", pvpHero, "BOTTOMLEFT", CFG.PAD, 5)
    pvpConqTrack:SetPoint("BOTTOMRIGHT", pvpHero, "BOTTOMRIGHT", -CFG.PAD, 5)
    pvpConqTrack:SetColorTexture(1, 1, 1, 0.06)
    R.pvpConqTrack = pvpConqTrack

    local pvpConqFill = pvpHero:CreateTexture(nil, "ARTWORK", nil, 2)
    pvpConqFill:SetHeight(3)
    pvpConqFill:SetPoint("LEFT", pvpConqTrack, "LEFT", 0, 0)
    pvpConqFill:SetWidth(1)
    pvpConqFill:SetColorTexture(1.0, 0.60, 0.00, 0.9)
    R.pvpConqFill = pvpConqFill

    local pvpGlassDiv = pvpHero:CreateTexture(nil, "OVERLAY", nil, 3)
    pvpGlassDiv:SetHeight(1)
    pvpGlassDiv:SetPoint("BOTTOMLEFT", pvpHero, "BOTTOMLEFT", 0, 0)
    pvpGlassDiv:SetPoint("BOTTOMRIGHT", pvpHero, "BOTTOMRIGHT", 0, 0)
    pvpGlassDiv:SetColorTexture(1, 1, 1, 0.08)

    local pvpBody = CreateFrame("Frame", nil, pvpContent)
    pvpBody:SetPoint("TOPLEFT", pvpHero, "BOTTOMLEFT", 0, 0)
    pvpBody:SetPoint("BOTTOMRIGHT", pvpContent, "BOTTOMRIGHT", 0, 0)

    local pvpCatSidebar = CreateGlassPanel(pvpBody, CFG.CAT_SIDEBAR_W or 160)
    pvpCatSidebar:SetPoint("TOPLEFT", pvpBody, "TOPLEFT", 0, 0)
    pvpCatSidebar:SetPoint("BOTTOMLEFT", pvpBody, "BOTTOMLEFT", 0, 0)
    R.pvpCatSidebar = pvpCatSidebar

    R.pvpCatButtons = {}
    for i, def in ipairs(PVP_CATEGORY_DEFS) do
        local btn = CreateFrame("Button", nil, pvpCatSidebar)
        btn:SetHeight(36)
        btn:SetPoint("TOPLEFT", pvpCatSidebar, "TOPLEFT", 0, -((i - 1) * 36))
        btn:SetPoint("TOPRIGHT", pvpCatSidebar, "TOPRIGHT", 0, -((i - 1) * 36))

        local btnBg = btn:CreateTexture(nil, "BACKGROUND")
        btnBg:SetAllPoints()
        btnBg:SetColorTexture(TC("glassBg"))
        btn._bg = btnBg

        local accent = btn:CreateTexture(nil, "OVERLAY", nil, 2)
        accent:SetWidth(3)
        accent:SetPoint("TOPLEFT", btn, "TOPLEFT", 0, 0)
        accent:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", 0, 0)
        local ac = PVP_CAT_ACCENTS[def.key] or {0.5, 0.5, 0.5}
        accent:SetColorTexture(ac[1], ac[2], ac[3])
        accent:Hide()
        btn._accent = accent

        local lbl = btn:CreateFontString(nil, "OVERLAY")
        TrySetFont(lbl, BODY_FONT, 10, "")
        lbl:SetPoint("LEFT", btn, "LEFT", 12, 0)
        lbl:SetTextColor(TC("bodyText"))
        lbl:SetText(def.label)
        btn._label = lbl
        btn._key = def.key

        btn:SetScript("OnClick", function()
            Panel._state.pvpCategory = def.key
            Panel._state.pvpSelectedActivity = nil
            Panel.UpdatePVPCatButtons()
            Panel.UpdatePVPActivities()
        end)
        btn:SetScript("OnEnter", function()
            if Panel._state.pvpCategory ~= def.key then
                btnBg:SetColorTexture(0.1, 0.1, 0.15, 0.6)
            end
        end)
        btn:SetScript("OnLeave", function()
            if Panel._state.pvpCategory ~= def.key then
                btnBg:SetColorTexture(TC("glassBg"))
            end
        end)

        R.pvpCatButtons[i] = btn
    end

    local pvpBottomBar = CreateFrame("Frame", nil, pvpBody)
    pvpBottomBar:SetHeight(50)
    pvpBottomBar:SetPoint("BOTTOMLEFT", pvpBody, "BOTTOMLEFT", (CFG.CAT_SIDEBAR_W or 160), 0)
    pvpBottomBar:SetPoint("BOTTOMRIGHT", pvpBody, "BOTTOMRIGHT", 0, 0)
    local pvpBbBg = pvpBottomBar:CreateTexture(nil, "BACKGROUND")
    pvpBbBg:SetAllPoints()
    pvpBbBg:SetColorTexture(0, 0, 0, 0.30)
    local pvpBbDiv = pvpBottomBar:CreateTexture(nil, "OVERLAY")
    pvpBbDiv:SetHeight(1)
    pvpBbDiv:SetPoint("TOPLEFT", pvpBottomBar, "TOPLEFT", 0, 0)
    pvpBbDiv:SetPoint("TOPRIGHT", pvpBottomBar, "TOPRIGHT", 0, 0)
    pvpBbDiv:SetColorTexture(TC("divider"))
    R.pvpBottomBar = pvpBottomBar

    local pvpRoleDefs = {
        { key = "tank",   atlas = "UI-LFG-RoleIcon-Tank" },
        { key = "healer", atlas = "UI-LFG-RoleIcon-Healer" },
        { key = "dps",    atlas = "UI-LFG-RoleIcon-DPS" },
    }
    R.pvpRoleBtns = {}
    for ri, rd in ipairs(pvpRoleDefs) do
        local rbtn = CreateFrame("Button", nil, pvpBottomBar)
        rbtn:SetSize(32, 32)
        rbtn:SetPoint("LEFT", pvpBottomBar, "LEFT", 12 + (ri - 1) * 38, 0)
        local ricon = rbtn:CreateTexture(nil, "ARTWORK")
        ricon:SetAllPoints()
        pcall(ricon.SetAtlas, ricon, rd.atlas)
        ricon:SetDesaturated(true)
        ricon:SetAlpha(0.4)
        rbtn._icon = ricon
        rbtn._key = rd.key
        rbtn:SetScript("OnClick", function(self)
            local roles = Panel._state.pvpRoles
            roles[self._key] = not roles[self._key]
            self._icon:SetDesaturated(not roles[self._key])
            self._icon:SetAlpha(roles[self._key] and 1.0 or 0.4)
        end)
        R.pvpRoleBtns[ri] = rbtn
    end

    local pvpQueueBtn = CreateFrame("Button", "MidnightUI_PVPQueueBtn", pvpBottomBar, "SecureActionButtonTemplate, BackdropTemplate")
    pvpQueueBtn:SetSize(140, 32)
    pvpQueueBtn:SetPoint("RIGHT", pvpBottomBar, "RIGHT", -12, 0)
    pvpQueueBtn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    local qR, qG, qB = TC("accent")
    pvpQueueBtn:SetBackdropColor(qR, qG, qB, 0.15)
    pvpQueueBtn:SetBackdropBorderColor(qR, qG, qB, 0.8)
    pvpQueueBtn:RegisterForClicks("AnyUp")
    pvpQueueBtn:SetAttribute("type", "macro")
    pvpQueueBtn:SetAttribute("macrotext", "")
    local pvpQueueLabel = pvpQueueBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(pvpQueueLabel, TITLE_FONT, 11, "OUTLINE")
    pvpQueueLabel:SetPoint("CENTER")
    pvpQueueLabel:SetTextColor(TC("accent"))
    pvpQueueLabel:SetText("Join Battle")
    pvpQueueBtn._label = pvpQueueLabel

    pvpQueueBtn:SetScript("PreClick", function(self)
        local activity = Panel._state.pvpSelectedActivity
        if not activity then
            self:SetAttribute("macrotext", "")
            return
        end

        local macro = ""
        if activity.queueType == "random_bg" then
            local okR, info = pcall(C_PvP.GetRandomBGInfo)
            if okR and type(info) == "table" and info.bgIndex then
                macro = "/run C_PvP.JoinBattlefield(" .. info.bgIndex .. ", " .. tostring(IsInGroup()) .. ")"
            end
        elseif activity.queueType == "random_epic" then
            local okR, info = pcall(C_PvP.GetRandomEpicBGInfo)
            if okR and type(info) == "table" and info.bgIndex then
                macro = "/run C_PvP.JoinBattlefield(" .. info.bgIndex .. ", " .. tostring(IsInGroup()) .. ")"
            end
        elseif activity.queueType == "specific" then
            if C_PvP.GetBattlegroundInfo and GetNumBattlegroundTypes then
                local okN, numBGs = pcall(GetNumBattlegroundTypes)
                if okN and numBGs then
                    for i = 1, numBGs do
                        local okB, bgInfo = pcall(C_PvP.GetBattlegroundInfo, i)
                        if okB and type(bgInfo) == "table" and bgInfo.name then
                            if bgInfo.name:lower():find(activity.name:lower(), 1, true) or activity.name:lower():find(bgInfo.name:lower(), 1, true) then
                                macro = "/run C_PvP.JoinBattlefield(" .. (bgInfo.bgIndex or i) .. ", " .. tostring(IsInGroup()) .. ")"
                                break
                            end
                        end
                    end
                end
            end
        elseif activity.queueType == "skirmish" then
            macro = "/run JoinSingleLFG(" .. (LE_LFG_CATEGORY_WORLDPVP or 4) .. ", 0)"
        elseif activity.queueType == "brawl" then
            macro = "/run C_PvP.JoinBrawl()"
        end


        self:SetAttribute("macrotext", macro)
    end)

    pvpQueueBtn:SetScript("PostClick", function(self)
        local activity = Panel._state.pvpSelectedActivity
        if not activity then
            Panel.ShowToast("No Activity Selected", "Select a PVP activity to queue for.", 4)
            return
        end
        pcall(SetLFGRoles, false, Panel._state.pvpRoles.tank, Panel._state.pvpRoles.healer, Panel._state.pvpRoles.dps)
        if activity.bracket then
            Panel.ShowToast("Rated PVP", "Rated queueing is protected by Blizzard\nand cannot be done from addon UI.\n\nUse /pvp to open the Blizzard PVP panel.", 6)
        end
        C_Timer.After(0.5, function() Panel.UpdatePVPQueueStatus() end)
    end)
    pvpQueueBtn:SetScript("OnEnter", function(self) self:SetBackdropBorderColor(qR, qG, qB, 1.0) end)
    pvpQueueBtn:SetScript("OnLeave", function(self) self:SetBackdropBorderColor(qR, qG, qB, 0.8) end)
    R.pvpQueueBtn = pvpQueueBtn

    local pvpQueueBar = CreateFrame("Frame", nil, pvpBody)
    pvpQueueBar:SetHeight(30)
    pvpQueueBar:SetPoint("BOTTOMLEFT", pvpBottomBar, "TOPLEFT", 0, 0)
    pvpQueueBar:SetPoint("BOTTOMRIGHT", pvpBottomBar, "TOPRIGHT", 0, 0)
    local pvpQbBg = pvpQueueBar:CreateTexture(nil, "BACKGROUND")
    pvpQbBg:SetAllPoints()
    pvpQbBg:SetColorTexture(0.05, 0.15, 0.05, 0.8)
    local pvpQueueText = pvpQueueBar:CreateFontString(nil, "OVERLAY")
    TrySetFont(pvpQueueText, BODY_FONT, 10, "OUTLINE")
    pvpQueueText:SetPoint("LEFT", pvpQueueBar, "LEFT", 12, 0)
    pvpQueueText:SetTextColor(0.4, 1.0, 0.4)
    R.pvpQueueText = pvpQueueText

    local pvpQueueCancel = CreateFrame("Button", nil, pvpQueueBar, "BackdropTemplate")
    pvpQueueCancel:SetSize(60, 22)
    pvpQueueCancel:SetPoint("RIGHT", pvpQueueBar, "RIGHT", -8, 0)
    pvpQueueCancel:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    pvpQueueCancel:SetBackdropColor(0.4, 0.1, 0.1, 0.5)
    pvpQueueCancel:SetBackdropBorderColor(1, 0.3, 0.3, 0.6)
    local pvpCancelLabel = pvpQueueCancel:CreateFontString(nil, "OVERLAY")
    TrySetFont(pvpCancelLabel, BODY_FONT, 9, "OUTLINE")
    pvpCancelLabel:SetPoint("CENTER")
    pvpCancelLabel:SetTextColor(1, 0.4, 0.4)
    pvpCancelLabel:SetText("Cancel")
    pvpQueueCancel:SetScript("OnClick", function()
        for i = 1, 3 do
            local status = GetBattlefieldStatus and GetBattlefieldStatus(i)
            if status == "queued" or status == "confirm" then
                pcall(AcceptBattlefieldPort, i, false)
            end
        end
        C_Timer.After(0.3, function() Panel.UpdatePVPQueueStatus() end)
    end)

    pvpQueueBar:Hide()
    R.pvpQueueBar = pvpQueueBar

    local pvpListArea = CreateFrame("Frame", nil, pvpBody)
    pvpListArea:SetPoint("TOPLEFT", pvpCatSidebar, "TOPRIGHT", 0, 0)
    pvpListArea:SetPoint("BOTTOMRIGHT", pvpBottomBar, "TOPRIGHT", 0, 0)
    R.pvpListArea = pvpListArea

    local pvpScroll = CreateFrame("ScrollFrame", nil, pvpListArea, "UIPanelScrollFrameTemplate")
    pvpScroll:SetPoint("TOPLEFT", pvpListArea, "TOPLEFT", 8, -8)
    pvpScroll:SetPoint("BOTTOMRIGHT", pvpListArea, "BOTTOMRIGHT", -24, 8)
    local pvpScrollChild = CreateFrame("Frame", nil, pvpScroll)
    pvpScrollChild:SetWidth(pvpScroll:GetWidth() or 600)
    pvpScrollChild:SetHeight(1)
    pvpScroll:SetScrollChild(pvpScrollChild)
    R.pvpScrollChild = pvpScrollChild
    R.pvpActivityCards = {}

    end

    do
    local premadeContent = CreateFrame("Frame", nil, content)
    premadeContent:SetAllPoints()
    premadeContent:Hide()
    R.premadeContent = premadeContent

    local premadeHero = CreateFrame("Frame", nil, premadeContent)
    premadeHero:SetHeight(190)
    premadeHero:SetPoint("TOPLEFT", premadeContent, "TOPLEFT", 0, 0)
    premadeHero:SetPoint("TOPRIGHT", premadeContent, "TOPRIGHT", 0, 0)
    R.premadeHero = premadeHero

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

    local phAtlas = premadeHero:CreateTexture(nil, "BACKGROUND", nil, -7)
    phAtlas:SetAllPoints()
    if not TrySetAtlasFromList(phAtlas, HERO_ATLASES.premade) then
        phAtlas:SetColorTexture(0, 0, 0, 0)
    end
    phAtlas:SetAlpha(0.40)

    local phVigTop = premadeHero:CreateTexture(nil, "BACKGROUND", nil, -5)
    phVigTop:SetHeight(80)
    phVigTop:SetPoint("TOPLEFT", premadeHero, "TOPLEFT", 0, 0)
    phVigTop:SetPoint("TOPRIGHT", premadeHero, "TOPRIGHT", 0, 0)
    phVigTop:SetTexture(W8)
    ApplyGradient(phVigTop, "VERTICAL", 0, 0, 0, 0, 0, 0, 0, 0.5)

    local phVigBot = premadeHero:CreateTexture(nil, "BACKGROUND", nil, -5)
    phVigBot:SetHeight(60)
    phVigBot:SetPoint("BOTTOMLEFT", premadeHero, "BOTTOMLEFT", 0, 0)
    phVigBot:SetPoint("BOTTOMRIGHT", premadeHero, "BOTTOMRIGHT", 0, 0)
    phVigBot:SetTexture(W8)
    ApplyGradient(phVigBot, "VERTICAL", 0, 0, 0, 0.6, 0, 0, 0, 0)

    local phVigLeft = premadeHero:CreateTexture(nil, "BACKGROUND", nil, -5)
    phVigLeft:SetWidth(math.floor(CFG.WIDTH * 0.40))
    phVigLeft:SetPoint("TOPLEFT", premadeHero, "TOPLEFT", 0, 0)
    phVigLeft:SetPoint("BOTTOMLEFT", premadeHero, "BOTTOMLEFT", 0, 0)
    phVigLeft:SetTexture(W8)
    ApplyGradient(phVigLeft, "HORIZONTAL", 0, 0, 0, 0.25, 0, 0, 0, 0)

    local phAcWash = premadeHero:CreateTexture(nil, "BACKGROUND", nil, -4)
    phAcWash:SetHeight(60)
    phAcWash:SetPoint("BOTTOMLEFT", premadeHero, "BOTTOMLEFT", 0, 0)
    phAcWash:SetPoint("BOTTOMRIGHT", premadeHero, "BOTTOMRIGHT", 0, 0)
    phAcWash:SetTexture(W8)
    local phAcR, phAcG, phAcB = TC("accent")
    ApplyGradient(phAcWash, "VERTICAL", phAcR, phAcG, phAcB, 0.03, phAcR, phAcG, phAcB, 0)

    local phCategoryLabel = premadeHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(phCategoryLabel, BODY_FONT, 9, "OUTLINE")
    phCategoryLabel:SetPoint("TOPLEFT", premadeHero, "TOPLEFT", CFG.PAD + 2, -8)
    phCategoryLabel:SetTextColor(TC("mutedText"))
    phCategoryLabel:SetText("")
    R.premadeHeroTitle = phCategoryLabel

    local phGroupCount = premadeHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(phGroupCount, BODY_FONT, 9, "OUTLINE")
    phGroupCount:SetPoint("TOPRIGHT", premadeHero, "TOPRIGHT", -CFG.PAD, -8)
    phGroupCount:SetTextColor(TC("mutedText"))
    phGroupCount:SetText("")
    R.premadeHeroGroupCount = phGroupCount

    local phClassIcon = premadeHero:CreateTexture(nil, "ARTWORK")
    phClassIcon:SetSize(22, 22)
    phClassIcon:SetPoint("TOP", premadeHero, "TOP", -46, -34)
    R.premadeHeroClassIcon = phClassIcon

    local phPlayerName = premadeHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(phPlayerName, TITLE_FONT, 14, "OUTLINE")
    phPlayerName:SetPoint("LEFT", phClassIcon, "RIGHT", 6, 0)
    phPlayerName:SetTextColor(TC("bodyText"))
    phPlayerName:SetText("")
    R.premadeHeroPlayerName = phPlayerName

    local phSpecLine = premadeHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(phSpecLine, BODY_FONT, 10, "")
    phSpecLine:SetPoint("TOP", phPlayerName, "BOTTOM", 0, -4)
    phSpecLine:SetJustifyH("CENTER")
    phSpecLine:SetTextColor(TC("mutedText"))
    phSpecLine:SetText("")
    R.premadeHeroSpecLine = phSpecLine

    local PILL_H = 28
    local PILL_GAP = 8
    local ROLE_PILL_W = 80
    local STAT_PILL_W = 120
    local KEY_PILL_W_MIN = 100
    local KEY_PILL_W_MAX = 220
    local totalPillW = 3 * ROLE_PILL_W + 2 * STAT_PILL_W + KEY_PILL_W_MIN + 5 * PILL_GAP
    local pillStartX = math.floor((CFG.WIDTH - totalPillW) / 2)
    local pillY = -82

    local function CreatePremadeStatPill(xOfs, width)
        local pill = CreateFrame("Frame", nil, premadeHero, "BackdropTemplate")
        pill:SetSize(width or STAT_PILL_W, PILL_H)
        pill:SetPoint("TOPLEFT", premadeHero, "TOPLEFT", xOfs, pillY)
        pill:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        pill:SetBackdropColor(TC("surfaceBg"))
        pill:SetBackdropBorderColor(0, 0, 0, 0)
        local acR, acG, acB = TC("accent")
        local dot = pill:CreateTexture(nil, "OVERLAY", nil, 2)
        dot:SetSize(4, PILL_H - 8)
        dot:SetPoint("LEFT", pill, "LEFT", 4, 0)
        dot:SetTexture(W8)
        if dot.SetGradient and CreateColor then
            dot:SetGradient("VERTICAL",
                CreateColor(acR, acG, acB, 0.3),
                CreateColor(acR, acG, acB, 1.0))
        else
            dot:SetColorTexture(acR, acG, acB)
        end
        pill._dot = dot
        local lbl = pill:CreateFontString(nil, "OVERLAY")
        TrySetFont(lbl, BODY_FONT, 9, "OUTLINE")
        lbl:SetPoint("LEFT", dot, "RIGHT", 6, 0)
        lbl:SetTextColor(TC("mutedText"))
        pill._label = lbl
        local val = pill:CreateFontString(nil, "OVERLAY")
        TrySetFont(val, TITLE_FONT, 11, "OUTLINE")
        val:SetPoint("RIGHT", pill, "RIGHT", -8, 0)
        val:SetJustifyH("RIGHT")
        val:SetTextColor(TC("bodyText"))
        pill._value = val
        local frost = pill:CreateTexture(nil, "OVERLAY", nil, 3)
        frost:SetHeight(1)
        frost:SetPoint("TOPLEFT", pill, "TOPLEFT", 1, -1)
        frost:SetPoint("TOPRIGHT", pill, "TOPRIGHT", -1, -1)
        frost:SetTexture(W8)
        if frost.SetGradient and CreateColor then
            frost:SetGradient("HORIZONTAL",
                CreateColor(1, 1, 1, 0.01),
                CreateColor(1, 1, 1, 0.04))
        else
            frost:SetColorTexture(1, 1, 1, 0.04)
        end
        local botLine = pill:CreateTexture(nil, "BACKGROUND", nil, 2)
        botLine:SetHeight(1)
        botLine:SetPoint("BOTTOMLEFT", pill, "BOTTOMLEFT", 1, 1)
        botLine:SetPoint("BOTTOMRIGHT", pill, "BOTTOMRIGHT", -1, 1)
        botLine:SetColorTexture(0, 0, 0, 0.08)
        return pill
    end

    local premadeRoleDefs = {
        { key = "tank",   atlas = "UI-LFG-RoleIcon-Tank",   label = "Tank" },
        { key = "healer", atlas = "UI-LFG-RoleIcon-Healer", label = "Healer" },
        { key = "dps",    atlas = "UI-LFG-RoleIcon-DPS",    label = "DPS" },
    }
    R.premadeRoleCards = {}

    for ri, rd in ipairs(premadeRoleDefs) do
        local rpill = CreateFrame("Button", nil, premadeHero, "BackdropTemplate")
        rpill:SetSize(ROLE_PILL_W, PILL_H)
        rpill:SetPoint("TOPLEFT", premadeHero, "TOPLEFT", pillStartX + (ri - 1) * (ROLE_PILL_W + PILL_GAP), pillY)
        rpill:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        rpill:SetBackdropColor(TC("surfaceBg"))
        rpill:SetBackdropBorderColor(0, 0, 0, 0)
        local selGlow = rpill:CreateTexture(nil, "BACKGROUND", nil, 1)
        selGlow:SetPoint("TOPLEFT", rpill, "TOPLEFT", 1, -1)
        selGlow:SetPoint("BOTTOMRIGHT", rpill, "BOTTOMRIGHT", -1, 1)
        selGlow:SetColorTexture(0, 0, 0, 0)
        rpill._selGlow = selGlow
        local ricon = rpill:CreateTexture(nil, "ARTWORK")
        ricon:SetSize(16, 16)
        ricon:SetPoint("LEFT", rpill, "LEFT", 8, 0)
        pcall(ricon.SetAtlas, ricon, rd.atlas)
        ricon:SetDesaturated(true)
        ricon:SetAlpha(0.4)
        rpill._icon = ricon
        local rlbl = rpill:CreateFontString(nil, "OVERLAY")
        TrySetFont(rlbl, BODY_FONT, 10, "OUTLINE")
        rlbl:SetPoint("LEFT", ricon, "RIGHT", 5, 0)
        rlbl:SetTextColor(TC("mutedText"))
        rlbl:SetText(rd.label)
        rpill._label = rlbl
        rpill._roleKey = rd.key
        local frost = rpill:CreateTexture(nil, "OVERLAY", nil, 3)
        frost:SetHeight(1)
        frost:SetPoint("TOPLEFT", rpill, "TOPLEFT", 1, -1)
        frost:SetPoint("TOPRIGHT", rpill, "TOPRIGHT", -1, -1)
        frost:SetTexture(W8)
        if frost.SetGradient and CreateColor then
            frost:SetGradient("HORIZONTAL",
                CreateColor(1, 1, 1, 0.01),
                CreateColor(1, 1, 1, 0.04))
        else
            frost:SetColorTexture(1, 1, 1, 0.04)
        end
        rpill:SetScript("OnClick", function(self)
            local st = Panel._state.applyRole
            st[self._roleKey] = not st[self._roleKey]
            Panel.UpdatePremadeRoleCards()
            Panel.UpdateListingRows()
            local R = Panel._refs
            if R.detailPanel and R.detailPanel:IsShown() and R.detailPanel._resultID then
                pcall(Panel.ShowGroupDetail, R.detailPanel._resultID)
            end
        end)
        rpill:SetScript("OnEnter", function(self)
            if not Panel._state.applyRole[self._roleKey] then self._icon:SetAlpha(0.7) end
        end)
        rpill:SetScript("OnLeave", function(self)
            if not Panel._state.applyRole[self._roleKey] then self._icon:SetAlpha(0.4) end
        end)
        R.premadeRoleCards[ri] = rpill
    end
    R.applyRoleBtns = R.premadeRoleCards

    local statStartX = pillStartX + 3 * (ROLE_PILL_W + PILL_GAP)
    local mpiPill = CreatePremadeStatPill(statStartX)
    mpiPill._label:SetText("MPI RATING")
    mpiPill._value:SetText("--")
    R.premadeHeroMPIPill = mpiPill
    R.premadeHeroRolePill = nil

    local ratingPill = CreatePremadeStatPill(statStartX + STAT_PILL_W + PILL_GAP)
    ratingPill._label:SetText("M+ RATING")
    ratingPill._value:SetText("--")
    R.premadeHeroScorePill = ratingPill
    R.premadeHeroScoreBadge = ratingPill
    R.premadeHeroScoreText = ratingPill._value

    local keystonePill = CreatePremadeStatPill(statStartX + 2 * (STAT_PILL_W + PILL_GAP), KEY_PILL_W_MIN)
    keystonePill._muiMinW = KEY_PILL_W_MIN
    keystonePill._muiMaxW = KEY_PILL_W_MAX
    keystonePill._label:SetText("KEY")
    keystonePill._value:SetText("None")
    R.premadeHeroKeystonePill = keystonePill
    R.premadeHeroKeystoneText = keystonePill._value

    local phSuggestion = premadeHero:CreateFontString(nil, "OVERLAY")
    TrySetFont(phSuggestion, BODY_FONT, 9, "")
    phSuggestion:SetPoint("BOTTOMLEFT", premadeHero, "BOTTOMLEFT", CFG.PAD + 2, 6)
    phSuggestion:SetPoint("BOTTOMRIGHT", premadeHero, "BOTTOMRIGHT", -CFG.PAD, 6)
    phSuggestion:SetJustifyH("CENTER")
    phSuggestion:SetTextColor(TC("mutedText"))
    phSuggestion:SetText("")
    R.premadeHeroSuggestion = phSuggestion

    local listKeyBtn = CreateFrame("Button", nil, premadeHero, "BackdropTemplate")
    listKeyBtn:SetSize(120, 26)
    listKeyBtn:SetPoint("BOTTOMRIGHT", premadeHero, "BOTTOMRIGHT", -CFG.PAD, 6)
    listKeyBtn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    local lkR, lkG, lkB = TC("accent")
    listKeyBtn:SetBackdropColor(lkR, lkG, lkB, 0.12)
    listKeyBtn:SetBackdropBorderColor(lkR, lkG, lkB, 0.25)
    local listKeyLabel = listKeyBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(listKeyLabel, BODY_FONT, 10, "OUTLINE")
    listKeyLabel:SetPoint("CENTER")
    listKeyLabel:SetTextColor(TC("accent"))
    listKeyLabel:SetText("List Your Key")
    listKeyBtn._label = listKeyLabel
    listKeyBtn:SetScript("OnEnter", function(self)
        self:SetBackdropColor(lkR, lkG, lkB, 0.22)
        self:SetBackdropBorderColor(lkR, lkG, lkB, 0.40)
    end)
    listKeyBtn:SetScript("OnLeave", function(self)
        self:SetBackdropColor(lkR, lkG, lkB, 0.12)
        self:SetBackdropBorderColor(lkR, lkG, lkB, 0.25)
    end)
    listKeyBtn:SetScript("OnClick", function() Panel.ListMyKey() end)
    R.listKeyBtn = listKeyBtn

    local phGlassDivL = premadeHero:CreateTexture(nil, "OVERLAY", nil, 3)
    phGlassDivL:SetHeight(1)
    phGlassDivL:SetPoint("BOTTOMLEFT", premadeHero, "BOTTOMLEFT", 0, 0)
    phGlassDivL:SetPoint("BOTTOM", premadeHero, "BOTTOM", 0, 0)
    phGlassDivL:SetTexture(W8)
    ApplyGradient(phGlassDivL, "HORIZONTAL", 1, 1, 1, 0, 1, 1, 1, 0.06)

    local phGlassDivR = premadeHero:CreateTexture(nil, "OVERLAY", nil, 3)
    phGlassDivR:SetHeight(1)
    phGlassDivR:SetPoint("BOTTOM", premadeHero, "BOTTOM", 0, 0)
    phGlassDivR:SetPoint("BOTTOMRIGHT", premadeHero, "BOTTOMRIGHT", 0, 0)
    phGlassDivR:SetTexture(W8)
    ApplyGradient(phGlassDivR, "HORIZONTAL", 1, 1, 1, 0.06, 1, 1, 1, 0)

    local premadeBody = CreateFrame("Frame", nil, premadeContent)
    premadeBody:SetPoint("TOPLEFT", premadeHero, "BOTTOMLEFT", 0, 0)
    premadeBody:SetPoint("BOTTOMRIGHT", premadeContent, "BOTTOMRIGHT", 0, 0)
    R.premadeBody = premadeBody

    local catSidebar = CreateGlassPanel(premadeBody, CFG.CAT_SIDEBAR_W)
    catSidebar:SetPoint("TOPLEFT", premadeBody, "TOPLEFT", 0, 0)
    catSidebar:SetPoint("BOTTOMLEFT", premadeBody, "BOTTOMLEFT", 0, 0)
    R.catSidebar = catSidebar

    R.catButtons = {}
    for i, def in ipairs(CATEGORY_DEFS) do
        local btn = CreateFrame("Button", nil, catSidebar)
        btn:SetSize(CFG.CAT_SIDEBAR_W, 36)
        btn:SetPoint("TOPLEFT", catSidebar, "TOPLEFT", 0, -((i - 1) * 36))

        local btnBg = btn:CreateTexture(nil, "BACKGROUND")
        btnBg:SetAllPoints()
        btnBg:SetTexture(W8)
        local bgR, bgG, bgB, bgA = TC("glassBg")
        if btnBg.SetGradient and CreateColor then
            btnBg:SetGradient("VERTICAL",
                CreateColor(bgR, bgG, bgB, bgA),
                CreateColor(bgR + 0.01, bgG + 0.01, bgB + 0.01, bgA))
        else
            btnBg:SetColorTexture(TC("glassBg"))
        end
        btn._bg = btnBg

        local acBar = btn:CreateTexture(nil, "OVERLAY", nil, 2)
        acBar:SetSize(4, 36)
        acBar:SetPoint("LEFT", btn, "LEFT", 0, 0)
        acBar:SetTexture(W8)
        local catAcColor = CATEGORY_ACCENTS[def.key] or {TC("accent")}
        if acBar.SetGradient and CreateColor then
            acBar:SetGradient("VERTICAL",
                CreateColor(catAcColor[1], catAcColor[2], catAcColor[3], 0.3),
                CreateColor(catAcColor[1], catAcColor[2], catAcColor[3], 1.0))
        else
            acBar:SetColorTexture(catAcColor[1], catAcColor[2], catAcColor[3])
        end
        acBar:Hide()
        btn._accentBar = acBar

        if i > 1 then
            local sep = btn:CreateTexture(nil, "OVERLAY", nil, 1)
            sep:SetHeight(1)
            sep:SetPoint("TOPLEFT", btn, "TOPLEFT", 8, 0)
            sep:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -8, 0)
            local sepR, sepG, sepB = TC("divider")
            sep:SetColorTexture(sepR, sepG, sepB, 0.04)
        end

        local lbl = btn:CreateFontString(nil, "OVERLAY")
        TrySetFont(lbl, BODY_FONT, 11, "OUTLINE")
        lbl:SetPoint("LEFT", btn, "LEFT", 14, 0)
        lbl:SetTextColor(TC("bodyText"))
        lbl:SetText(def.label)
        btn._label = lbl
        btn._key = def.key

        btn:SetScript("OnClick", function()
            Panel._state.activeCategory = def.key
            Panel._state.searchResults = {}
            Panel._state.scrollOffset = 0
            -- Drop any activity locked in from the search autocomplete and
            -- clear the box so its text doesn't filter the new category.
            Panel._state._acActive = false
            Panel._state.filters.activityFull = nil
            if Panel._refs.searchAC then Panel._refs.searchAC:Hide() end
            if Panel._refs.raidSearchAC then Panel._refs.raidSearchAC:Hide() end
            if Panel._refs.delveSearchAC then Panel._refs.delveSearchAC:Hide() end
            if Panel._refs.teleportDropdown then Panel._refs.teleportDropdown:Hide() end
            if Panel._refs.searchBox and Panel._refs.searchBox.SetText then
                pcall(Panel._refs.searchBox.SetText, Panel._refs.searchBox, "")
            end
            Panel.UpdateCatButtons()
            Panel.UpdateFilterBarVisibility()
            Panel.UpdateListingRows()
            Panel.DoSearch()
        end)
        btn:SetScript("OnEnter", function()
            if Panel._state.activeCategory ~= def.key then
                local hAc = CATEGORY_ACCENTS[def.key] or {TC("accent")}
                btnBg:SetTexture(W8)
                btnBg:SetColorTexture(hAc[1], hAc[2], hAc[3], 0.06)
            end
        end)
        btn:SetScript("OnLeave", function()
            if Panel._state.activeCategory ~= def.key then
                btnBg:SetTexture(W8)
                if btnBg.SetGradient and CreateColor then
                    btnBg:SetGradient("VERTICAL",
                        CreateColor(bgR, bgG, bgB, bgA),
                        CreateColor(bgR + 0.01, bgG + 0.01, bgB + 0.01, bgA))
                else
                    btnBg:SetColorTexture(TC("glassBg"))
                end
            end
        end)

        R.catButtons[i] = btn
    end

    local playerSidebar = CreateGlassPanel(premadeBody, CFG.PLAYER_SIDEBAR_W)
    playerSidebar:SetPoint("TOPRIGHT", premadeBody, "TOPRIGHT", 0, 0)
    playerSidebar:SetPoint("BOTTOMRIGHT", premadeBody, "BOTTOMRIGHT", 0, 0)
    R.playerSidebar = playerSidebar

    local listingArea = CreateFrame("Frame", nil, premadeBody)
    listingArea:SetPoint("TOPLEFT", catSidebar, "TOPRIGHT", 0, 0)
    listingArea:SetPoint("BOTTOMRIGHT", playerSidebar, "BOTTOMLEFT", 0, 0)
    R.listingArea = listingArea

    local filterBar = CreateFrame("Frame", nil, listingArea)
    filterBar:SetHeight(CFG.FILTER_BAR_H)
    filterBar:SetPoint("TOPLEFT", listingArea, "TOPLEFT", 0, 0)
    filterBar:SetPoint("TOPRIGHT", listingArea, "TOPRIGHT", 0, 0)
    R.filterBar = filterBar

    local filterBg = filterBar:CreateTexture(nil, "BACKGROUND")
    filterBg:SetAllPoints()
    filterBg:SetColorTexture(0, 0, 0, 0.20)

    -- ---------- Dungeon teleport menu ----------
    -- Icon button at the far left of the M+ filter bar (grouped with the
    -- dungeon picker). Opens a dropdown of the current rotation's teleports.
    local tpBtn = CreateFrame("Button", nil, filterBar, "BackdropTemplate")
    tpBtn:SetSize(28, 28)
    tpBtn:SetPoint("LEFT", filterBar, "LEFT", CFG.PAD, 0)
    tpBtn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    tpBtn:SetBackdropColor(TC("surfaceBg"))
    tpBtn:SetBackdropBorderColor(0.28, 0.28, 0.34, 0.45)
    local tpIcon = tpBtn:CreateTexture(nil, "ARTWORK")
    tpIcon:SetPoint("CENTER")
    local tpAtlasOk = pcall(tpIcon.SetAtlas, tpIcon, "DungeonStoneCheckpoint", false)
    if not tpAtlasOk or not tpIcon:GetAtlas() then
        tpIcon:SetTexture("Interface\\Icons\\Spell_Arcane_PortalDalaran")
        tpIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    end
    tpIcon:SetSize(20, 20)
    R.teleportBtn = tpBtn

    local TP_W, TP_ROW_H, TP_HEADER_H, TP_MAX = 250, 26, 24, 14
    local tpDrop = CreateFrame("Frame", nil, tpBtn, "BackdropTemplate")
    tpDrop:SetFrameStrata("FULLSCREEN_DIALOG")
    tpDrop:SetWidth(TP_W)
    tpDrop:SetHeight(TP_HEADER_H + TP_ROW_H + 8)
    tpDrop:SetPoint("TOPLEFT", tpBtn, "BOTTOMLEFT", 0, -4)
    tpDrop:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    tpDrop:SetBackdropColor(0.04, 0.04, 0.07, 0.98)
    tpDrop:SetBackdropBorderColor(TC("glassBorder"))
    tpDrop:Hide()
    tpDrop._rows = {}
    tpDrop._rowH = TP_ROW_H
    tpDrop._headerH = TP_HEADER_H
    R.teleportDropdown = tpDrop

    local tpHeader = tpDrop:CreateFontString(nil, "OVERLAY")
    TrySetFont(tpHeader, BODY_FONT, 10, "OUTLINE")
    tpHeader:SetPoint("TOPLEFT", tpDrop, "TOPLEFT", 12, -8)
    tpHeader:SetTextColor(TC("mutedText"))
    tpHeader:SetText("DUNGEON TELEPORTS")

    for i = 1, TP_MAX do
        -- SecureActionButtonTemplate: the click is handled by Blizzard's
        -- secure code, so the cast itself is NOT subject to the addon's taint
        -- (provided we don't taint it ourselves with an insecure cast call).
        local row = CreateFrame("Button", nil, tpDrop, "SecureActionButtonTemplate")
        row:SetSize(TP_W - 8, TP_ROW_H)
        -- Register BOTH down and up: the secure handler fires the cast on
        -- whichever matches the player's "cast on key down" cvar (and only
        -- once). Registering only "AnyUp" silently no-ops for cast-on-down.
        row:RegisterForClicks("AnyDown", "AnyUp")
        row:SetFrameStrata("FULLSCREEN_DIALOG")
        row:Hide()
        local rbg = row:CreateTexture(nil, "BACKGROUND")
        rbg:SetAllPoints(); rbg:SetColorTexture(0, 0, 0, 0)
        row._bg = rbg
        local ricon = row:CreateTexture(nil, "ARTWORK")
        ricon:SetSize(18, 18)
        ricon:SetPoint("LEFT", row, "LEFT", 8, 0)
        ricon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        row._icon = ricon
        local rname = row:CreateFontString(nil, "OVERLAY")
        TrySetFont(rname, BODY_FONT, 11, "")
        rname:SetPoint("LEFT", ricon, "RIGHT", 9, 0)
        rname:SetPoint("RIGHT", row, "RIGHT", -8, 0)
        rname:SetJustifyH("LEFT")
        if rname.SetWordWrap then rname:SetWordWrap(false) end
        row._name = rname
        row:SetScript("OnEnter", function(self)
            local ar, ag, ab = TC("accent")
            self._bg:SetColorTexture(ar, ag, ab, 0.10)
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:AddLine(self._dungeonName or "", 1, 1, 1)
            if not self._known then
                GameTooltip:AddLine("Teleport Not Discovered", 0.85, 0.35, 0.35)
            else
                local cd = GetTeleportCooldown(self._spellID)
                if cd > 0 then
                    GameTooltip:AddLine("Teleport on Cooldown: " .. FormatTeleportTime(cd) .. " Remaining", 0.95, 0.7, 0.3)
                else
                    GameTooltip:AddLine("Teleport Ready", 0.35, 0.85, 0.45)
                end
            end
            GameTooltip:Show()
        end)
        row:SetScript("OnLeave", function(self)
            self._bg:SetColorTexture(0, 0, 0, 0)
            GameTooltip:Hide()
        end)
        -- Close the menu once a ready teleport is cast (the cast itself is the
        -- template's secure OnClick).
        row:SetScript("PostClick", function(self)
            if self._ready then tpDrop:Hide() end
        end)
        tpDrop._rows[i] = row
    end

    local tpCloser = CreateFrame("Frame", nil, UIParent)
    tpCloser:Hide()
    tpCloser:SetScript("OnUpdate", function()
        if tpDrop:IsShown() and not tpDrop:IsMouseOver() and not tpBtn:IsMouseOver() then
            if IsMouseButtonDown("LeftButton") or IsMouseButtonDown("RightButton") then tpDrop:Hide() end
        end
    end)
    tpDrop:SetScript("OnShow", function() tpCloser:Show() end)
    tpDrop:SetScript("OnHide", function() tpCloser:Hide() end)

    tpBtn:SetScript("OnClick", function()
        if tpDrop:IsShown() then
            tpDrop:Hide()
        else
            Panel.RefreshTeleportDropdown()
            tpDrop:Show()
        end
    end)
    tpBtn:SetScript("OnEnter", function()
        local ar, ag, ab = TC("accent")
        tpBtn:SetBackdropBorderColor(ar, ag, ab, 0.7)
        GameTooltip:SetOwner(tpBtn, "ANCHOR_TOP")
        GameTooltip:AddLine("Dungeon Teleports", 1, 1, 1)
        GameTooltip:AddLine("Teleport to a current Mythic+ dungeon.", 0.7, 0.7, 0.7, true)
        GameTooltip:Show()
    end)
    tpBtn:SetScript("OnLeave", function()
        tpBtn:SetBackdropBorderColor(0.28, 0.28, 0.34, 0.45)
        GameTooltip:Hide()
    end)

    local ddBtn = CreateFrame("Button", nil, filterBar, "BackdropTemplate")
    ddBtn:SetSize(160, 28)
    ddBtn:SetPoint("LEFT", tpBtn, "RIGHT", 8, 0)
    ddBtn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    ddBtn:SetBackdropColor(TC("surfaceBg"))
    ddBtn:SetBackdropBorderColor(0, 0, 0, 0)
    local ddAccR, ddAccG, ddAccB = TC("accent")
    local ddUnderline = ddBtn:CreateTexture(nil, "OVERLAY", nil, 2)
    ddUnderline:SetHeight(1)
    ddUnderline:SetPoint("BOTTOMLEFT", ddBtn, "BOTTOMLEFT", 1, 0)
    ddUnderline:SetPoint("BOTTOMRIGHT", ddBtn, "BOTTOMRIGHT", -1, 0)
    ddUnderline:SetColorTexture(ddAccR, ddAccG, ddAccB, 0.12)
    local ddLabel = ddBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(ddLabel, BODY_FONT, 10, "OUTLINE")
    ddLabel:SetPoint("LEFT", ddBtn, "LEFT", 8, 0)
    ddLabel:SetTextColor(TC("bodyText"))
    ddLabel:SetText("All Dungeons")
    local ddArrow = ddBtn:CreateTexture(nil, "OVERLAY")
    ddArrow:SetSize(12, 12)
    ddArrow:SetPoint("RIGHT", ddBtn, "RIGHT", -6, 0)
    ddArrow:SetTexture("Interface\\ChatFrame\\ChatFrameExpandArrow")
    ddArrow:SetTexCoord(0, 1, 1, 1, 0, 0, 1, 0)
    ddArrow:SetVertexColor(TC("mutedText"))
    ddBtn._label = ddLabel
    R.ddBtn = ddBtn

    local ddList = CreateFrame("Frame", nil, ddBtn, "BackdropTemplate")
    ddList:SetWidth(160)
    ddList:SetPoint("TOPLEFT", ddBtn, "BOTTOMLEFT", 0, -2)
    ddList:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    ddList:SetBackdropColor(0.04, 0.04, 0.07, 0.97)
    ddList:SetBackdropBorderColor(TC("glassBorder"))
    local ddShadow = ddList:CreateTexture(nil, "BACKGROUND", nil, -1)
    ddShadow:SetPoint("TOPLEFT", ddList, "BOTTOMLEFT", 2, 0)
    ddShadow:SetPoint("TOPRIGHT", ddList, "BOTTOMRIGHT", -2, 0)
    ddShadow:SetHeight(4)
    ddShadow:SetTexture(W8)
    ApplyGradient(ddShadow, "VERTICAL", 0, 0, 0, 0, 0, 0, 0, 0.15)
    ddList:SetFrameStrata("TOOLTIP")
    ddList:Hide()
    R.ddList = ddList

    local dropdownNames = BuildDungeonDropdownList(Panel._dungeonData)
    local ddItems = {}
    for idx, dname in ipairs(dropdownNames) do
        local item = CreateFrame("Button", nil, ddList)
        item:SetSize(158, 22)
        item:SetPoint("TOPLEFT", ddList, "TOPLEFT", 1, -((idx - 1) * 22) - 1)
        local itemBg = item:CreateTexture(nil, "BACKGROUND")
        itemBg:SetAllPoints()
        itemBg:SetColorTexture(0, 0, 0, 0)
        local itemLbl = item:CreateFontString(nil, "OVERLAY")
        TrySetFont(itemLbl, BODY_FONT, 10, "")
        itemLbl:SetPoint("LEFT", item, "LEFT", 8, 0)
        itemLbl:SetTextColor(TC("bodyText"))
        itemLbl:SetText(dname)
        item:SetScript("OnEnter", function()
            local hiR, hiG, hiB = TC("accent")
            itemBg:SetColorTexture(hiR, hiG, hiB, 0.06)
        end)
        item:SetScript("OnLeave", function() itemBg:SetColorTexture(0, 0, 0, 0) end)
        item:SetScript("OnClick", function()
            Panel._state.filters.dungeon = (idx == 1) and nil or dname
            ddLabel:SetText(dname)
            ddList:Hide()
        end)
        ddItems[idx] = item
    end
    ddList:SetHeight(#dropdownNames * 22 + 2)

    for _, it in ipairs(ddItems) do
        it:SetFrameStrata("TOOLTIP")
    end

    local ddClickCloser = CreateFrame("Frame", nil, UIParent)
    ddClickCloser:Hide()
    ddClickCloser:SetScript("OnUpdate", function()
        if ddList:IsShown() and not ddList:IsMouseOver() and not ddBtn:IsMouseOver() then
            if IsMouseButtonDown("LeftButton") or IsMouseButtonDown("RightButton") then
                ddList:Hide()
            end
        end
    end)
    ddList:SetScript("OnShow", function() ddClickCloser:Show() end)
    ddList:SetScript("OnHide", function() ddClickCloser:Hide() end)

    ddBtn:SetScript("OnClick", function()
        if ddList:IsShown() then ddList:Hide() else ddList:Show() end
    end)

    local ROLE_DEFS = {
        { role = "TANK",    atlas = "UI-LFG-RoleIcon-Tank",   maxSlots = 1, tooltip = "Needs Tank" },
        { role = "HEALER",  atlas = "UI-LFG-RoleIcon-Healer", maxSlots = 1, tooltip = "Needs Healer" },
        { role = "DAMAGER", atlas = "UI-LFG-RoleIcon-DPS",    maxSlots = 3, tooltip = "Needs DPS (click to cycle 1+, 2+, 3)" },
    }
    R.roleButtons = {}
    Panel._state.filters.role = {}
    local roleAnchor = ddBtn
    for ri, rd in ipairs(ROLE_DEFS) do
        local rb = CreateFrame("Button", nil, filterBar, "BackdropTemplate")
        rb:SetSize(30, 30)
        rb:SetPoint("LEFT", roleAnchor, "RIGHT", (ri == 1) and 12 or 6, 0)
        rb:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        rb:SetBackdropColor(0.08, 0.08, 0.10, 0.60)
        rb:SetBackdropBorderColor(0.25, 0.25, 0.30, 0.50)
        local rIcon = rb:CreateTexture(nil, "OVERLAY")
        rIcon:SetSize(22, 22)
        rIcon:SetPoint("CENTER")
        rIcon:SetAtlas(rd.atlas)
        rIcon:SetDesaturated(true)
        rIcon:SetAlpha(0.55)
        rb._icon = rIcon
        rb._role = rd.role
        rb._needSlots = 0

        local badge = CreateFrame("Frame", nil, rb)
        badge:SetSize(16, 14)
        badge:SetPoint("BOTTOMRIGHT", rb, "BOTTOMRIGHT", 3, -3)
        badge:SetFrameLevel(rb:GetFrameLevel() + 2)
        local badgeBg = badge:CreateTexture(nil, "BACKGROUND")
        badgeBg:SetAllPoints()
        badgeBg:SetColorTexture(0.06, 0.06, 0.08, 0.95)
        local badgeText = badge:CreateFontString(nil, "OVERLAY")
        TrySetFont(badgeText, BODY_FONT, 9, "OUTLINE")
        badgeText:SetPoint("CENTER")
        badgeText:SetTextColor(1, 1, 1)
        badge._text = badgeText
        badge:Hide()
        rb._badge = badge

        local function UpdateVisual()
            if rb._needSlots > 0 then
                rIcon:SetDesaturated(false)
                rIcon:SetAlpha(1)
                local aR, aG, aB = TC("accent")
                rb:SetBackdropColor(aR, aG, aB, 0.18)
                rb:SetBackdropBorderColor(aR, aG, aB, 0.65)
                if rd.maxSlots > 1 then
                    local label = (rb._needSlots == rd.maxSlots) and tostring(rb._needSlots) or (rb._needSlots .. "+")
                    badgeText:SetText(label)
                    badge:Show()
                else
                    badge:Hide()
                end
            else
                rIcon:SetDesaturated(true)
                rIcon:SetAlpha(0.55)
                rb:SetBackdropColor(0.08, 0.08, 0.10, 0.60)
                rb:SetBackdropBorderColor(0.25, 0.25, 0.30, 0.50)
                badge:Hide()
            end
        end

        rb:SetScript("OnClick", function()
            rb._needSlots = rb._needSlots + 1
            if rb._needSlots > rd.maxSlots then rb._needSlots = 0 end
            UpdateVisual()
            if rb._needSlots > 0 then
                Panel._state.filters.role[rd.role] = rb._needSlots
            else
                Panel._state.filters.role[rd.role] = nil
            end
            -- Also drive the "I'll apply as this role" state so the Apply
            -- buttons enable/disable in lockstep with the filter buttons —
            -- the player only has to click in one place.
            local applyKeyMap = { TANK = "tank", HEALER = "healer", DAMAGER = "dps" }
            local applyKey = applyKeyMap[rd.role]
            if applyKey and Panel._state.applyRole then
                Panel._state.applyRole[applyKey] = (rb._needSlots > 0)
                Panel.UpdatePremadeRoleCards()
            end
            Panel.OnSearchResults()
            local R = Panel._refs
            if R.detailPanel and R.detailPanel:IsShown() and R.detailPanel._resultID then
                pcall(Panel.ShowGroupDetail, R.detailPanel._resultID)
            end
        end)
        rb:SetScript("OnEnter", function()
            GameTooltip:SetOwner(rb, "ANCHOR_TOP")
            GameTooltip:AddLine(rd.tooltip, 1, 1, 1)
            if rb._needSlots > 0 then
                local word = (rb._needSlots == rd.maxSlots) and
                    ("exactly " .. rb._needSlots) or
                    ("at least " .. rb._needSlots)
                GameTooltip:AddLine("Current: " .. word .. " open " .. rd.role:lower() .. " slot(s)", 0.7, 0.7, 0.7)
            end
            GameTooltip:Show()
        end)
        rb:SetScript("OnLeave", function() GameTooltip:Hide() end)
        R.roleButtons[ri] = rb
        roleAnchor = rb
    end

    do
        local _, pClass = UnitClass("player")
        Panel._playerClass = pClass
        local pClassLocal = pClass and LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE[pClass] or pClass or "your class"

        local FILTER_DEFS = {
            { key = "hasTank",     label = "Has Tank",      desc = "Only show groups that already have a tank." },
            { key = "hasHealer",   label = "Has Healer",    desc = "Only show groups that already have a healer." },
            { key = "noSameClass", label = "No " .. pClassLocal, desc = "Hide groups that already have a " .. pClassLocal .. "." },
        }

        local fBtn = CreateFrame("Button", nil, filterBar, "BackdropTemplate")
        fBtn:SetSize(80, 28)
        fBtn:SetPoint("LEFT", roleAnchor, "RIGHT", 12, 0)
        fBtn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        fBtn:SetBackdropColor(TC("surfaceBg"))
        fBtn:SetBackdropBorderColor(0, 0, 0, 0)
        local fAccR, fAccG, fAccB = TC("accent")
        local fUnderline = fBtn:CreateTexture(nil, "OVERLAY", nil, 2)
        fUnderline:SetHeight(1)
        fUnderline:SetPoint("BOTTOMLEFT", fBtn, "BOTTOMLEFT", 1, 0)
        fUnderline:SetPoint("BOTTOMRIGHT", fBtn, "BOTTOMRIGHT", -1, 0)
        fUnderline:SetColorTexture(fAccR, fAccG, fAccB, 0.12)
        local fLabel = fBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(fLabel, BODY_FONT, 10, "OUTLINE")
        fLabel:SetPoint("LEFT", fBtn, "LEFT", 8, 0)
        fLabel:SetTextColor(TC("bodyText"))
        fLabel:SetText("Filters")
        local fArrow = fBtn:CreateTexture(nil, "OVERLAY")
        fArrow:SetSize(12, 12)
        fArrow:SetPoint("RIGHT", fBtn, "RIGHT", -6, 0)
        fArrow:SetTexture("Interface\\ChatFrame\\ChatFrameExpandArrow")
        fArrow:SetTexCoord(0, 1, 1, 1, 0, 0, 1, 0)
        fArrow:SetVertexColor(TC("mutedText"))

        local fBadge = CreateFrame("Frame", nil, fBtn)
        fBadge:SetSize(16, 14)
        fBadge:SetPoint("TOPRIGHT", fBtn, "TOPRIGHT", 3, 3)
        fBadge:SetFrameLevel(fBtn:GetFrameLevel() + 2)
        local fBadgeBg = fBadge:CreateTexture(nil, "BACKGROUND")
        fBadgeBg:SetAllPoints()
        fBadgeBg:SetColorTexture(fAccR, fAccG, fAccB, 0.95)
        local fBadgeText = fBadge:CreateFontString(nil, "OVERLAY")
        TrySetFont(fBadgeText, BODY_FONT, 9, "OUTLINE")
        fBadgeText:SetPoint("CENTER")
        fBadgeText:SetTextColor(0, 0, 0)
        fBadge._text = fBadgeText
        fBadge:Hide()
        R.filtersBtn = fBtn
        R.filtersBadge = fBadge

        local function UpdateBadge()
            local n = 0
            for _, def in ipairs(FILTER_DEFS) do
                if Panel._state.filters[def.key] then n = n + 1 end
            end
            if n > 0 then
                fBadgeText:SetText(tostring(n))
                fBadge:Show()
                fLabel:SetTextColor(TC("accent"))
            else
                fBadge:Hide()
                fLabel:SetTextColor(TC("bodyText"))
            end
        end

        local fList = CreateFrame("Frame", nil, fBtn, "BackdropTemplate")
        local ROW_H = 22
        local LIST_W = 180
        fList:SetSize(LIST_W, #FILTER_DEFS * ROW_H + 2)
        fList:SetPoint("TOPLEFT", fBtn, "BOTTOMLEFT", 0, -2)
        fList:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        fList:SetBackdropColor(0.04, 0.04, 0.07, 0.97)
        fList:SetBackdropBorderColor(TC("glassBorder"))
        fList:SetFrameStrata("TOOLTIP")
        fList:Hide()

        local fItems = {}
        for idx, def in ipairs(FILTER_DEFS) do
            local row = CreateFrame("Button", nil, fList)
            row:SetSize(LIST_W - 2, ROW_H)
            row:SetPoint("TOPLEFT", fList, "TOPLEFT", 1, -((idx - 1) * ROW_H) - 1)
            row:SetFrameStrata("TOOLTIP")
            local rowBg = row:CreateTexture(nil, "BACKGROUND")
            rowBg:SetAllPoints()
            rowBg:SetColorTexture(0, 0, 0, 0)
            local box = row:CreateTexture(nil, "OVERLAY")
            box:SetSize(12, 12)
            box:SetPoint("LEFT", row, "LEFT", 8, 0)
            box:SetTexture(W8)
            box:SetVertexColor(0.25, 0.25, 0.30, 1)
            local check = row:CreateTexture(nil, "OVERLAY")
            check:SetSize(14, 14)
            check:SetPoint("CENTER", box, "CENTER", 0, 0)
            check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
            local lbl = row:CreateFontString(nil, "OVERLAY")
            TrySetFont(lbl, BODY_FONT, 10, "")
            lbl:SetPoint("LEFT", box, "RIGHT", 8, 0)
            lbl:SetTextColor(TC("bodyText"))
            lbl:SetText(def.label)
            local function SyncRow()
                if Panel._state.filters[def.key] then
                    local aR, aG, aB = TC("accent")
                    box:SetVertexColor(aR, aG, aB, 1)
                    check:Show()
                else
                    box:SetVertexColor(0.25, 0.25, 0.30, 1)
                    check:Hide()
                end
            end
            SyncRow()
            row:SetScript("OnEnter", function()
                local hiR, hiG, hiB = TC("accent")
                rowBg:SetColorTexture(hiR, hiG, hiB, 0.06)
                GameTooltip:SetOwner(row, "ANCHOR_RIGHT")
                GameTooltip:AddLine(def.label, 1, 1, 1)
                GameTooltip:AddLine(def.desc, 0.7, 0.7, 0.7, true)
                GameTooltip:Show()
            end)
            row:SetScript("OnLeave", function()
                rowBg:SetColorTexture(0, 0, 0, 0)
                GameTooltip:Hide()
            end)
            row:SetScript("OnClick", function()
                Panel._state.filters[def.key] = (not Panel._state.filters[def.key]) or nil
                SyncRow()
                UpdateBadge()
                Panel.OnSearchResults()
                if R.detailPanel and R.detailPanel:IsShown() and R.detailPanel._resultID then
                    pcall(Panel.ShowGroupDetail, R.detailPanel._resultID)
                end
            end)
            fItems[idx] = row
        end

        local fCloser = CreateFrame("Frame", nil, UIParent)
        fCloser:Hide()
        fCloser:SetScript("OnUpdate", function()
            if fList:IsShown() and not fList:IsMouseOver() and not fBtn:IsMouseOver() then
                if IsMouseButtonDown("LeftButton") or IsMouseButtonDown("RightButton") then
                    fList:Hide()
                end
            end
        end)
        fList:SetScript("OnShow", function() fCloser:Show() end)
        fList:SetScript("OnHide", function() fCloser:Hide() end)

        fBtn:SetScript("OnClick", function()
            if fList:IsShown() then fList:Hide() else fList:Show() end
        end)
        fBtn:SetScript("OnEnter", function()
            GameTooltip:SetOwner(fBtn, "ANCHOR_TOP")
            GameTooltip:AddLine("Filters", 1, 1, 1)
            GameTooltip:AddLine("Extra group requirements.", 0.7, 0.7, 0.7, true)
            GameTooltip:Show()
        end)
        fBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

        roleAnchor = fBtn
        UpdateBadge()
    end

    -- Search slot: a styled host frame in the filter bar. We adopt
    -- Blizzard's real LFG SearchBox into this slot at panel-show time
    -- (see Panel.AdoptBlizzSearchBox). Hardware input into Blizzard's
    -- secure box is allowed; SetText from addons is not, which is why
    -- we adopt rather than mirror.
    local searchSlot = CreateFrame("Frame", nil, filterBar, "BackdropTemplate")
    searchSlot:SetSize(140, 28)
    searchSlot:SetPoint("LEFT", roleAnchor, "RIGHT", 12, 0)
    searchSlot:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    searchSlot:SetBackdropColor(TC("surfaceBg"))
    searchSlot:SetBackdropBorderColor(0, 0, 0, 0)
    local searchUnderline = searchSlot:CreateTexture(nil, "OVERLAY", nil, 2)
    searchUnderline:SetHeight(1)
    searchUnderline:SetPoint("BOTTOMLEFT", searchSlot, "BOTTOMLEFT", 1, 0)
    searchUnderline:SetPoint("BOTTOMRIGHT", searchSlot, "BOTTOMRIGHT", -1, 0)
    searchUnderline:SetColorTexture(ddAccR, ddAccG, ddAccB, 0.12)
    R.searchSlot = searchSlot

    -- ============================================================
    -- Search autocomplete dropdown.
    -- As the player types into the adopted Blizzard SearchBox, suggest
    -- matching LFG activities for the active category (e.g. "Windrunner
    -- Spire (Normal/Heroic/Mythic/Mythic Keystone)") in a styled list,
    -- mirroring Blizzard's default group-finder behaviour. The dropdown
    -- is driven by AdoptBlizzSearchBox's OnTextChanged / arrow / enter
    -- scripts via the acFrame.* methods stored below.
    -- ============================================================
    do
        local AC_MAX_ROWS = 8
        local AC_ROW_H = 22
        local AC_W = 248
        local acAccR, acAccG, acAccB = TC("accent")

        local acFrame = CreateFrame("Frame", nil, searchSlot, "BackdropTemplate")
        acFrame:SetSize(AC_W, AC_ROW_H + 2)
        acFrame:SetPoint("TOPLEFT", searchSlot, "BOTTOMLEFT", 0, -3)
        acFrame:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        acFrame:SetBackdropColor(0.04, 0.04, 0.07, 0.97)
        acFrame:SetBackdropBorderColor(TC("glassBorder"))
        acFrame:SetFrameStrata("TOOLTIP")
        acFrame:Hide()
        acFrame._rows = {}
        acFrame._matches = {}
        acFrame._selected = 0
        R.searchAC = acFrame

        local function StyleRowSelected(row, on)
            if on then
                row._bg:SetColorTexture(acAccR, acAccG, acAccB, 0.16)
            else
                row._bg:SetColorTexture(0, 0, 0, 0)
            end
        end

        for i = 1, AC_MAX_ROWS do
            local row = CreateFrame("Button", nil, acFrame)
            row:SetSize(AC_W - 2, AC_ROW_H)
            row:SetPoint("TOPLEFT", acFrame, "TOPLEFT", 1, -((i - 1) * AC_ROW_H) - 1)
            row:SetFrameStrata("TOOLTIP")
            local rbg = row:CreateTexture(nil, "BACKGROUND")
            rbg:SetAllPoints()
            rbg:SetColorTexture(0, 0, 0, 0)
            row._bg = rbg
            local lbl = row:CreateFontString(nil, "OVERLAY")
            TrySetFont(lbl, BODY_FONT, 11, "")
            lbl:SetPoint("LEFT", row, "LEFT", 10, 0)
            lbl:SetPoint("RIGHT", row, "RIGHT", -8, 0)
            lbl:SetJustifyH("LEFT")
            if lbl.SetWordWrap then lbl:SetWordWrap(false) end
            if lbl.SetMaxLines then lbl:SetMaxLines(1) end
            lbl:SetTextColor(TC("bodyText"))
            row._label = lbl
            row:SetScript("OnEnter", function()
                acFrame._selected = i
                for ri, r in ipairs(acFrame._rows) do StyleRowSelected(r, ri == i) end
            end)
            row:SetScript("OnLeave", function()
                StyleRowSelected(row, false)
            end)
            row:SetScript("OnClick", function()
                if acFrame.Accept then acFrame.Accept(i) end
            end)
            acFrame._rows[i] = row
        end

        -- Outside-click closer (mirrors the Filters dropdown pattern).
        local acCloser = CreateFrame("Frame", nil, UIParent)
        acCloser:Hide()
        acCloser:SetScript("OnUpdate", function()
            if acFrame:IsShown() and not acFrame:IsMouseOver() and not searchSlot:IsMouseOver() then
                if IsMouseButtonDown("LeftButton") or IsMouseButtonDown("RightButton") then
                    acFrame:Hide()
                end
            end
        end)
        acFrame:SetScript("OnShow", function() acCloser:Show() end)
        acFrame:SetScript("OnHide", function() acCloser:Hide() end)

        -- Render the current match set into the row pool and size to fit.
        function acFrame.Render()
            local matches = acFrame._matches
            local n = math.min(#matches, AC_MAX_ROWS)
            if n == 0 then
                acFrame:Hide()
                return
            end
            for i = 1, AC_MAX_ROWS do
                local row = acFrame._rows[i]
                if i <= n then
                    row._label:SetText(matches[i].full)
                    StyleRowSelected(row, i == acFrame._selected)
                    row:Show()
                else
                    row:Hide()
                end
            end
            acFrame:SetHeight(n * AC_ROW_H + 2)
            acFrame:Show()
            acFrame:Raise()
        end

        -- Recompute matches for the typed query within the active category.
        function acFrame.DoUpdate(query)
            query = query and query:gsub("^%s+", ""):gsub("%s+$", "") or ""
            acFrame._selected = 0
            if #query < 2 then
                acFrame._matches = {}
                acFrame:Hide()
                return
            end
            local catID
            for _, c in ipairs(CATEGORY_DEFS) do
                if c.key == Panel._state.activeCategory then catID = c.categoryID; break end
            end
            local activities = GetCategoryActivities(catID)
            if not activities then
                acFrame._matches = {}
                acFrame:Hide()
                return
            end
            local q = query:lower()
            local m = {}
            for _, a in ipairs(activities) do
                if a.lower:find(q, 1, true) then
                    m[#m + 1] = a
                    if #m >= AC_MAX_ROWS then break end
                end
            end
            acFrame._matches = m
            acFrame.Render()
        end

        function acFrame.MoveSelection(delta)
            local n = math.min(#acFrame._matches, AC_MAX_ROWS)
            if n == 0 then return end
            local sel = acFrame._selected + delta
            if sel < 1 then sel = n elseif sel > n then sel = 1 end
            acFrame._selected = sel
            for ri, r in ipairs(acFrame._rows) do StyleRowSelected(r, ri == sel) end
        end

        function acFrame.Accept(index)
            local idx = index or acFrame._selected
            local match = idx and acFrame._matches[idx]
            acFrame:Hide()
            if not match then return end
            -- Filter addon-side by the exact activity name. OnSearchResults
            -- skips Blizzard's text filter while this is active.
            Panel._state._acActive = true
            Panel._state.filters.activityFull = match.full
            local box = R.searchBox
            if box then
                -- Best-effort: show the choice in the adopted box. SetText
                -- taints the box, but we no longer route through Blizzard's
                -- filter while a selection is active, so that's harmless here.
                pcall(box.SetText, box, match.full)
                pcall(box.SetCursorPosition, box, #match.full)
                if box.Instructions and box.Instructions.SetShown then box.Instructions:SetShown(false) end
                if box.ClearFocus then box:ClearFocus() end
            end
            Panel.OnSearchResults()
        end
    end

    local mpiBox = CreateFrame("EditBox", nil, filterBar, "BackdropTemplate")
    mpiBox:SetSize(70, 28)
    mpiBox:SetPoint("LEFT", searchSlot, "RIGHT", 12, 0)
    mpiBox:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    mpiBox:SetBackdropColor(TC("surfaceBg"))
    mpiBox:SetBackdropBorderColor(0, 0, 0, 0)
    local mpiUnderline = mpiBox:CreateTexture(nil, "OVERLAY", nil, 2)
    mpiUnderline:SetHeight(1)
    mpiUnderline:SetPoint("BOTTOMLEFT", mpiBox, "BOTTOMLEFT", 1, 0)
    mpiUnderline:SetPoint("BOTTOMRIGHT", mpiBox, "BOTTOMRIGHT", -1, 0)
    mpiUnderline:SetColorTexture(ddAccR, ddAccG, ddAccB, 0.12)
    TrySetFont(mpiBox, BODY_FONT, 10, "")
    mpiBox:SetTextColor(TC("bodyText"))
    mpiBox:SetJustifyH("CENTER")
    mpiBox:SetNumeric(true)
    mpiBox:SetMaxLetters(3)
    mpiBox:SetAutoFocus(false)
    mpiBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    mpiBox:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
    R.mpiBox = mpiBox

    local mpiPlaceholder = mpiBox:CreateFontString(nil, "OVERLAY")
    TrySetFont(mpiPlaceholder, BODY_FONT, 10, "")
    mpiPlaceholder:SetPoint("CENTER")
    mpiPlaceholder:SetTextColor(TC("mutedText"))
    mpiPlaceholder:SetText("MPI")
    mpiBox._placeholder = mpiPlaceholder
    mpiBox:SetScript("OnTextChanged", function(self)
        mpiPlaceholder:SetShown(self:GetText() == "")
        Panel._state.filters.minMPI = tonumber(self:GetText())
    end)

    local goBtn = CreateFrame("Button", nil, filterBar, "BackdropTemplate")
    goBtn:SetSize(60, 28)
    goBtn:SetPoint("LEFT", mpiBox, "RIGHT", 8, 0)
    goBtn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    local ar2, ag2, ab2 = TC("accent")
    goBtn:SetBackdropColor(ar2, ag2, ab2, 0.15)
    goBtn:SetBackdropBorderColor(ar2, ag2, ab2, 0.25)
    local goLabel = goBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(goLabel, BODY_FONT, 11, "OUTLINE")
    goLabel:SetPoint("CENTER")
    goLabel:SetTextColor(TC("accent"))
    goLabel:SetText("Refresh")
    goBtn:SetScript("OnClick", function() Panel.DoSearch(true) end)
    goBtn:SetScript("OnEnter", function()
        goBtn:SetBackdropColor(ar2, ag2, ab2, 0.25)
        goBtn:SetBackdropBorderColor(ar2, ag2, ab2, 0.45)
    end)
    goBtn:SetScript("OnLeave", function()
        goBtn:SetBackdropColor(ar2, ag2, ab2, 0.15)
        goBtn:SetBackdropBorderColor(ar2, ag2, ab2, 0.25)
    end)

    -- ============================================================
    -- Raid filter bar (shown for the Raids / Legacy Raids categories).
    -- An activity search box with a raid dropdown (Blizzard's primary
    -- raid filter) + difficulty chips. Deliberately NOT a copy of the
    -- Mythic+ bar — no key level, MPI, or dungeon dropdown.
    --
    -- Raid listing names aren't protected the way M+ key names are, so
    -- this uses a plain addon EditBox rather than adopting Blizzard's box.
    -- ============================================================
    local raidFilterBar = CreateFrame("Frame", nil, listingArea)
    raidFilterBar:SetHeight(CFG.FILTER_BAR_H)
    raidFilterBar:SetPoint("TOPLEFT", listingArea, "TOPLEFT", 0, 0)
    raidFilterBar:SetPoint("TOPRIGHT", listingArea, "TOPRIGHT", 0, 0)
    raidFilterBar:Hide()
    R.raidFilterBar = raidFilterBar

    local raidFilterBg = raidFilterBar:CreateTexture(nil, "BACKGROUND")
    raidFilterBg:SetAllPoints()
    raidFilterBg:SetColorTexture(0, 0, 0, 0.20)
    local raidFilterEdge = raidFilterBar:CreateTexture(nil, "BORDER")
    raidFilterEdge:SetHeight(1)
    raidFilterEdge:SetPoint("BOTTOMLEFT", raidFilterBar, "BOTTOMLEFT", 0, 0)
    raidFilterEdge:SetPoint("BOTTOMRIGHT", raidFilterBar, "BOTTOMRIGHT", 0, 0)
    do local er, eg, eb = TC("accent"); raidFilterEdge:SetColorTexture(er, eg, eb, 0.10) end

    -- ---------- Search box + raid dropdown (shared builder) ----------
    local rs = Panel.BuildSearchBar(raidFilterBar, {
        width = 230,
        placeholder = "Search Raids\226\128\166",
        onText = function(t) Panel._state.raidFilters.searchText = t end,
    })
    R.raidSearchBox = rs.box
    R.raidSearchAC = rs.ac

    -- Divider between search and difficulty chips.
    local rDivider = raidFilterBar:CreateTexture(nil, "OVERLAY")
    rDivider:SetSize(1, 18)
    rDivider:SetPoint("LEFT", rs.frame, "RIGHT", 12, 0)
    rDivider:SetColorTexture(1, 1, 1, 0.07)

    -- ---------- Difficulty chips (coloured by WoW difficulty) ----------
    R.raidDiffChips = {}
    local RAID_DIFFICULTIES = {
        { key = "LFR",    label = "LFR",    width = 46, color = { 0.55, 0.70, 0.55 } },
        { key = "Normal", label = "Normal", width = 62, color = { 0.35, 0.78, 0.40 } },
        { key = "Heroic", label = "Heroic", width = 62, color = { 0.25, 0.58, 0.95 } },
        { key = "Mythic", label = "Mythic", width = 62, color = { 0.70, 0.42, 0.95 } },
    }
    local chipAnchor = rDivider
    for ci, d in ipairs(RAID_DIFFICULTIES) do
        local cR, cG, cB = d.color[1], d.color[2], d.color[3]
        local chip = CreateFrame("Button", nil, raidFilterBar, "BackdropTemplate")
        chip:SetSize(d.width, 28)
        chip:SetPoint("LEFT", chipAnchor, "RIGHT", (ci == 1) and 12 or 6, 0)
        chip:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        local chipLbl = chip:CreateFontString(nil, "OVERLAY")
        TrySetFont(chipLbl, BODY_FONT, 11, "OUTLINE")
        chipLbl:SetPoint("CENTER")
        chipLbl:SetText(d.label)
        chip._label = chipLbl
        chip._key = d.key

        local function SyncChip(hover)
            local on = Panel._state.raidFilters.difficulty[d.key]
            if on then
                chip:SetBackdropColor(cR, cG, cB, 0.22)
                chip:SetBackdropBorderColor(cR, cG, cB, 0.80)
                chipLbl:SetTextColor(0.97, 0.97, 0.99)
            elseif hover then
                chip:SetBackdropColor(cR, cG, cB, 0.10)
                chip:SetBackdropBorderColor(cR, cG, cB, 0.45)
                chipLbl:SetTextColor(0.88, 0.90, 0.93)
            else
                chip:SetBackdropColor(0.10, 0.10, 0.13, 0.55)
                chip:SetBackdropBorderColor(0.28, 0.28, 0.33, 0.45)
                chipLbl:SetTextColor(TC("mutedText"))
            end
        end
        chip._sync = SyncChip
        SyncChip(false)

        chip:SetScript("OnClick", function()
            local set = Panel._state.raidFilters.difficulty
            set[d.key] = (not set[d.key]) or nil
            SyncChip(chip:IsMouseOver())
            Panel.OnSearchResults()
        end)
        chip:SetScript("OnEnter", function()
            SyncChip(true)
            GameTooltip:SetOwner(chip, "ANCHOR_TOP")
            GameTooltip:AddLine(d.label .. " Raids", cR, cG, cB)
            GameTooltip:AddLine("Click difficulties to show. None selected shows all.", 0.7, 0.7, 0.7, true)
            GameTooltip:Show()
        end)
        chip:SetScript("OnLeave", function()
            SyncChip(false)
            GameTooltip:Hide()
        end)

        R.raidDiffChips[ci] = chip
        chipAnchor = chip
    end

    local raidGoBtn = CreateFrame("Button", nil, raidFilterBar, "BackdropTemplate")
    raidGoBtn:SetSize(72, 28)
    raidGoBtn:SetPoint("RIGHT", raidFilterBar, "RIGHT", -CFG.PAD, 0)
    raidGoBtn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    local rgr, rgg, rgb = TC("accent")
    raidGoBtn:SetBackdropColor(rgr, rgg, rgb, 0.15)
    raidGoBtn:SetBackdropBorderColor(rgr, rgg, rgb, 0.30)
    local raidGoLabel = raidGoBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(raidGoLabel, BODY_FONT, 11, "OUTLINE")
    raidGoLabel:SetPoint("CENTER")
    raidGoLabel:SetTextColor(TC("accent"))
    raidGoLabel:SetText("Refresh")
    raidGoBtn:SetScript("OnClick", function() Panel.DoSearch(true) end)
    raidGoBtn:SetScript("OnEnter", function()
        raidGoBtn:SetBackdropColor(rgr, rgg, rgb, 0.25)
        raidGoBtn:SetBackdropBorderColor(rgr, rgg, rgb, 0.50)
    end)
    raidGoBtn:SetScript("OnLeave", function()
        raidGoBtn:SetBackdropColor(rgr, rgg, rgb, 0.15)
        raidGoBtn:SetBackdropBorderColor(rgr, rgg, rgb, 0.30)
    end)

    -- ============================================================
    -- Delves filter bar (shown for the Delves category).
    -- Delves don't have raid difficulties, so this is just the activity
    -- search box + dropdown — type a delve (or tier) to filter the list.
    -- ============================================================
    local delveFilterBar = CreateFrame("Frame", nil, listingArea)
    delveFilterBar:SetHeight(CFG.FILTER_BAR_H)
    delveFilterBar:SetPoint("TOPLEFT", listingArea, "TOPLEFT", 0, 0)
    delveFilterBar:SetPoint("TOPRIGHT", listingArea, "TOPRIGHT", 0, 0)
    delveFilterBar:Hide()
    R.delveFilterBar = delveFilterBar

    local delveBg = delveFilterBar:CreateTexture(nil, "BACKGROUND")
    delveBg:SetAllPoints()
    delveBg:SetColorTexture(0, 0, 0, 0.20)
    local delveEdge = delveFilterBar:CreateTexture(nil, "BORDER")
    delveEdge:SetHeight(1)
    delveEdge:SetPoint("BOTTOMLEFT", delveFilterBar, "BOTTOMLEFT", 0, 0)
    delveEdge:SetPoint("BOTTOMRIGHT", delveFilterBar, "BOTTOMRIGHT", 0, 0)
    do local er, eg, eb = TC("accent"); delveEdge:SetColorTexture(er, eg, eb, 0.10) end

    local dlv = Panel.BuildSearchBar(delveFilterBar, {
        width = 240,
        placeholder = "Search Delves\226\128\166",
        onText = function(t) Panel._state.delveFilters.searchText = t end,
    })
    R.delveSearchBox = dlv.box
    R.delveSearchAC = dlv.ac

    local delveGoBtn = CreateFrame("Button", nil, delveFilterBar, "BackdropTemplate")
    delveGoBtn:SetSize(72, 28)
    delveGoBtn:SetPoint("RIGHT", delveFilterBar, "RIGHT", -CFG.PAD, 0)
    delveGoBtn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    local dgr, dgg, dgb = TC("accent")
    delveGoBtn:SetBackdropColor(dgr, dgg, dgb, 0.15)
    delveGoBtn:SetBackdropBorderColor(dgr, dgg, dgb, 0.30)
    local delveGoLabel = delveGoBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(delveGoLabel, BODY_FONT, 11, "OUTLINE")
    delveGoLabel:SetPoint("CENTER")
    delveGoLabel:SetTextColor(TC("accent"))
    delveGoLabel:SetText("Refresh")
    delveGoBtn:SetScript("OnClick", function() Panel.DoSearch(true) end)
    delveGoBtn:SetScript("OnEnter", function()
        delveGoBtn:SetBackdropColor(dgr, dgg, dgb, 0.25)
        delveGoBtn:SetBackdropBorderColor(dgr, dgg, dgb, 0.50)
    end)
    delveGoBtn:SetScript("OnLeave", function()
        delveGoBtn:SetBackdropColor(dgr, dgg, dgb, 0.15)
        delveGoBtn:SetBackdropBorderColor(dgr, dgg, dgb, 0.30)
    end)

    local scrollArea = CreateFrame("Frame", nil, listingArea)
    scrollArea:SetPoint("TOPLEFT", filterBar, "BOTTOMLEFT", 0, 0)
    scrollArea:SetPoint("BOTTOMRIGHT", listingArea, "BOTTOMRIGHT", 0, 0)
    scrollArea:SetClipsChildren(true)
    R.scrollArea = scrollArea

    local emptyLabel = scrollArea:CreateFontString(nil, "OVERLAY")
    TrySetFont(emptyLabel, BODY_FONT, 14, "OUTLINE")
    emptyLabel:SetPoint("CENTER", scrollArea, "CENTER", 0, 12)
    emptyLabel:SetTextColor(TC("mutedText"))
    emptyLabel:SetText("No groups found.")
    R.emptyLabel = emptyLabel

    local retryBtn = CreateFrame("Button", nil, scrollArea, "BackdropTemplate")
    retryBtn:SetSize(80, 26)
    retryBtn:SetPoint("TOP", emptyLabel, "BOTTOM", 0, -8)
    retryBtn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    local rtR, rtG, rtB = TC("accent")
    retryBtn:SetBackdropColor(rtR, rtG, rtB, 0.10)
    retryBtn:SetBackdropBorderColor(rtR, rtG, rtB, 0.20)
    local retryLabel = retryBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(retryLabel, BODY_FONT, 11, "OUTLINE")
    retryLabel:SetPoint("CENTER")
    retryLabel:SetTextColor(TC("accent"))
    retryLabel:SetText("Retry")
    retryBtn:SetScript("OnClick", function() Panel.DoSearch() end)
    retryBtn:SetScript("OnEnter", function(self)
        self:SetBackdropColor(rtR, rtG, rtB, 0.20)
        self:SetBackdropBorderColor(rtR, rtG, rtB, 0.35)
    end)
    retryBtn:SetScript("OnLeave", function(self)
        self:SetBackdropColor(rtR, rtG, rtB, 0.10)
        self:SetBackdropBorderColor(rtR, rtG, rtB, 0.20)
    end)
    retryBtn:Hide()
    R.retryBtn = retryBtn

    local searchingLabel = scrollArea:CreateFontString(nil, "OVERLAY")
    TrySetFont(searchingLabel, BODY_FONT, 14, "OUTLINE")
    searchingLabel:SetPoint("CENTER")
    searchingLabel:SetTextColor(TC("mutedText"))
    searchingLabel:SetText("Searching...")
    searchingLabel:Hide()
    R.searchingLabel = searchingLabel

    Panel._state.scrollOffset = 0
    R.rowPool = {}

    for ri = 1, CFG.ROW_POOL_SIZE do
        local row = CreateFrame("Button", nil, scrollArea)
        row:SetHeight(CFG.ROW_H)
        row:SetPoint("TOPLEFT", scrollArea, "TOPLEFT", 0, -((ri - 1) * CFG.ROW_H + (ri - 1) * 4))
        row:SetPoint("RIGHT", scrollArea, "RIGHT", -8, 0)
        row:Hide()

        local rowBg = row:CreateTexture(nil, "BACKGROUND")
        rowBg:SetAllPoints()
        rowBg:SetColorTexture(0.06, 0.06, 0.08, 0.55)
        row._bg = rowBg
        row._bgDefault = 0.55

        local banner = row:CreateTexture(nil, "BACKGROUND", nil, 2)
        banner:SetPoint("TOPLEFT", row, "TOPLEFT", 3, 0)
        banner:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
        banner:SetTexture(W8)
        row._banner = banner

        local acBar = row:CreateTexture(nil, "OVERLAY", nil, 2)
        acBar:SetWidth(3)
        acBar:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
        acBar:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
        acBar:SetTexture(W8)
        row._accentBar = acBar

        local watermark = row:CreateFontString(nil, "ARTWORK", nil, 1)
        TrySetFont(watermark, TITLE_FONT, 28, "OUTLINE")
        row._watermark = watermark

        local APPLY_W = 72
        local APPLY_H = 30
        local APPLY_PAD = 12
        local ROLE_SIZE = 22
        local ROLE_GAP = 6
        local ROLES_TOTAL = ROLE_SIZE * 3 + ROLE_GAP * 2
        local ROLE_APPLY_GAP = 12
        local RIGHT_ZONE = APPLY_PAD + APPLY_W + ROLE_APPLY_GAP + ROLES_TOTAL + 12
        local COL_TEXT_LEFT = 68
        local MPI_W = 48
        local MPI_H = 46

        watermark:SetPoint("RIGHT", row, "TOPRIGHT", -(RIGHT_ZONE + 8), -34)
        watermark:SetJustifyH("RIGHT")

        local mpiBadge = CreateFrame("Frame", nil, row, "BackdropTemplate")
        mpiBadge:SetSize(MPI_W, MPI_H)
        mpiBadge:SetPoint("TOPLEFT", row, "TOPLEFT", 10, -11)
        mpiBadge:SetBackdrop({
            bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        mpiBadge:SetBackdropColor(0.08, 0.08, 0.10, 0.85)
        mpiBadge:SetBackdropBorderColor(0.25, 0.25, 0.30, 0.55)
        local mpiBadgeBg = mpiBadge:CreateTexture(nil, "BACKGROUND")
        mpiBadgeBg:SetPoint("TOPLEFT", 1, -1)
        mpiBadgeBg:SetPoint("BOTTOMRIGHT", -1, 1)
        mpiBadgeBg:SetColorTexture(0.5, 0.5, 0.55, 0.18)
        mpiBadge._bg = mpiBadgeBg
        local mpiScoreText = mpiBadge:CreateFontString(nil, "OVERLAY")
        TrySetFont(mpiScoreText, TITLE_FONT, 14, "OUTLINE")
        mpiScoreText:SetPoint("CENTER", 0, 5)
        mpiScoreText:SetText("--")
        mpiBadge._text = mpiScoreText
        local mpiLabel = mpiBadge:CreateFontString(nil, "OVERLAY")
        TrySetFont(mpiLabel, BODY_FONT, 8, "OUTLINE")
        mpiLabel:SetPoint("CENTER", 0, -11)
        mpiLabel:SetTextColor(0.85, 0.85, 0.90, 0.75)
        mpiLabel:SetText("MPI")
        row._mpiBadge = mpiBadge

        local LEFT_COL_W = 250
        local LEFT_COL_RIGHT_X = COL_TEXT_LEFT + LEFT_COL_W

        local activityName = row:CreateFontString(nil, "OVERLAY")
        TrySetFont(activityName, TITLE_FONT, 13, "OUTLINE")
        activityName:SetPoint("TOPLEFT", row, "TOPLEFT", COL_TEXT_LEFT, -16)
        activityName:SetJustifyH("LEFT")
        activityName:SetWordWrap(false)
        activityName:SetTextColor(TC("bodyText"))
        row._activityName = activityName

        local titleSep = row:CreateFontString(nil, "OVERLAY")
        TrySetFont(titleSep, TITLE_FONT, 13, "OUTLINE")
        titleSep:SetPoint("CENTER", activityName, "RIGHT", 10, 1)
        titleSep:SetTextColor(0.50, 0.50, 0.55, 0.85)
        titleSep:SetText("·")
        row._titleSep = titleSep

        local dungNameText = row:CreateFontString(nil, "OVERLAY")
        TrySetFont(dungNameText, TITLE_FONT, 13, "OUTLINE")
        dungNameText:SetPoint("BOTTOMLEFT", titleSep, "BOTTOMRIGHT", 8, 0)
        dungNameText:SetPoint("RIGHT", row, "TOPLEFT", LEFT_COL_RIGHT_X, 0)
        dungNameText:SetJustifyH("LEFT")
        dungNameText:SetWordWrap(false)
        dungNameText:SetTextColor(0.75, 0.75, 0.80, 1)
        row._dungName = dungNameText

        local listedText = row:CreateFontString(nil, "OVERLAY")
        TrySetFont(listedText, BODY_FONT, 10, "")
        listedText:SetPoint("TOPLEFT", row, "TOPLEFT", COL_TEXT_LEFT, -46)
        listedText:SetJustifyH("LEFT")
        listedText:SetTextColor(TC("mutedText"))
        row._listedText = listedText

        local sepA = row:CreateTexture(nil, "OVERLAY")
        sepA:SetSize(1, 10)
        sepA:SetPoint("LEFT", listedText, "RIGHT", 10, 0)
        sepA:SetColorTexture(1, 1, 1, 0.16)
        row._sepA = sepA

        local countText = row:CreateFontString(nil, "OVERLAY")
        TrySetFont(countText, BODY_FONT, 10, "")
        countText:SetPoint("LEFT", sepA, "RIGHT", 10, 0)
        countText:SetJustifyH("LEFT")
        countText:SetTextColor(TC("bodyText"))
        row._countText = countText

        local sepB = row:CreateTexture(nil, "OVERLAY")
        sepB:SetSize(1, 10)
        sepB:SetPoint("LEFT", countText, "RIGHT", 10, 0)
        sepB:SetColorTexture(1, 1, 1, 0.16)
        row._sepB = sepB

        local leaderName = row:CreateFontString(nil, "OVERLAY")
        TrySetFont(leaderName, BODY_FONT, 10, "")
        leaderName:SetPoint("LEFT", sepB, "RIGHT", 10, 0)
        leaderName:SetJustifyH("LEFT")
        leaderName:SetWordWrap(false)
        leaderName:SetTextColor(TC("mutedText"))
        row._leaderName = leaderName

        local leaderClassIcon = row:CreateTexture(nil, "ARTWORK")
        leaderClassIcon:SetSize(12, 12)
        leaderClassIcon:SetPoint("LEFT", leaderName, "RIGHT", 5, 0)
        row._leaderClassIcon = leaderClassIcon

        local descCard = CreateFrame("Frame", nil, row, "BackdropTemplate")
        descCard:SetPoint("TOPLEFT", row, "TOPLEFT", LEFT_COL_RIGHT_X + 12, -11)
        descCard:SetPoint("BOTTOMRIGHT", row, "TOPRIGHT", -(RIGHT_ZONE + 8), -57)
        descCard:SetBackdrop({
            bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        descCard:SetBackdropColor(0.04, 0.04, 0.06, 0.55)
        descCard:SetBackdropBorderColor(0.28, 0.28, 0.34, 0.55)
        descCard:Hide()
        row._descCard = descCard

        local descAccent = descCard:CreateTexture(nil, "ARTWORK", nil, 2)
        descAccent:SetPoint("TOPLEFT", descCard, "TOPLEFT", 1, -1)
        descAccent:SetPoint("BOTTOMLEFT", descCard, "BOTTOMLEFT", 1, 1)
        descAccent:SetWidth(3)
        local daR, daG, daB = TC("accent")
        if descAccent.SetGradient and CreateColor then
            descAccent:SetGradient("VERTICAL",
                CreateColor(daR, daG, daB, 0.30),
                CreateColor(daR, daG, daB, 0.75))
        else
            descAccent:SetColorTexture(daR, daG, daB, 0.55)
        end

        local descText = descCard:CreateFontString(nil, "OVERLAY")
        TrySetFont(descText, BODY_FONT, 11, "")
        descText:SetPoint("TOPLEFT", descCard, "TOPLEFT", 14, -8)
        descText:SetPoint("BOTTOMRIGHT", descCard, "BOTTOMRIGHT", -10, 8)
        descText:SetJustifyH("LEFT")
        descText:SetJustifyV("MIDDLE")
        descText:SetWordWrap(true)
        descText:SetMaxLines(2)
        descText:SetSpacing(2)
        descText:SetTextColor(0.85, 0.85, 0.90, 1)
        row._descText = descText


        local applyBtn = CreateFrame("Button", nil, row, "BackdropTemplate")
        applyBtn:SetSize(APPLY_W, APPLY_H)
        applyBtn:SetPoint("TOPRIGHT", row, "TOPRIGHT", -APPLY_PAD, -19)
        applyBtn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        local aR, aG, aB = TC("accent")
        applyBtn:SetBackdropColor(aR, aG, aB, 0.16)
        applyBtn:SetBackdropBorderColor(aR, aG, aB, 0.35)
        local applyLabel = applyBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(applyLabel, BODY_FONT, 11, "OUTLINE")
        applyLabel:SetPoint("CENTER")
        applyLabel:SetTextColor(TC("accent"))
        applyLabel:SetText("Apply")
        applyBtn._resultID = nil
        applyBtn._label = applyLabel
        applyBtn:SetScript("OnClick", function(self)
            if not self._resultID then return end
            if self._isCancelMode then
                pcall(C_LFGList.CancelApplication, self._resultID)
                C_Timer.After(0.3, function()
                    Panel.UpdateGroupIntelSidebar()
                    Panel.UpdateListingRows()
                end)
            else
                Panel.ApplyToGroup(self._resultID)
            end
        end)
        applyBtn:SetScript("OnEnter", function(self)
            self:SetBackdropColor(aR, aG, aB, 0.30)
            self:SetBackdropBorderColor(aR, aG, aB, 0.60)
        end)
        applyBtn:SetScript("OnLeave", function(self)
            self:SetBackdropColor(aR, aG, aB, 0.16)
            self:SetBackdropBorderColor(aR, aG, aB, 0.35)
        end)
        row._applyBtn = applyBtn

        local roleIcons = {}
        local roleBubbles = {}
        for rri, rrd in ipairs(ROLE_DEFS) do
            local btn = CreateFrame("Button", nil, row)
            btn:SetSize(ROLE_SIZE, ROLE_SIZE)
            btn:SetFrameLevel(row:GetFrameLevel() + 2)
            local icon = btn:CreateTexture(nil, "ARTWORK")
            icon:SetAllPoints()
            pcall(icon.SetAtlas, icon, rrd.atlas)
            icon:SetDesaturated(true)
            icon:SetAlpha(0.3)
            icon:Show()
            btn._tex = icon
            btn:EnableMouse(false)
            btn:Show()
            roleIcons[rri] = btn
        end
        roleIcons[3]:SetPoint("RIGHT", applyBtn, "LEFT", -ROLE_APPLY_GAP, 0)
        roleIcons[2]:SetPoint("RIGHT", roleIcons[3], "LEFT", -ROLE_GAP, 0)
        roleIcons[1]:SetPoint("RIGHT", roleIcons[2], "LEFT", -ROLE_GAP, 0)

        for rri = 1, 3 do
            local bubble = CreateFrame("Frame", nil, row)
            bubble:SetSize(13, 13)
            bubble:SetPoint("BOTTOMRIGHT", roleIcons[rri], "BOTTOMRIGHT", 4, -3)
            bubble:SetFrameLevel(row:GetFrameLevel() + 4)
            local bubbleBg = bubble:CreateTexture(nil, "BACKGROUND")
            bubbleBg:SetAllPoints()
            bubbleBg:SetColorTexture(0.06, 0.06, 0.08, 0.9)
            local bubbleBorder = bubble:CreateTexture(nil, "BORDER")
            bubbleBorder:SetPoint("TOPLEFT", -1, 1)
            bubbleBorder:SetPoint("BOTTOMRIGHT", 1, -1)
            bubbleBorder:SetColorTexture(0.25, 0.25, 0.30, 0.5)
            local countFs = bubble:CreateFontString(nil, "OVERLAY")
            pcall(countFs.SetFont, countFs, BODY_FONT, 8, "OUTLINE")
            countFs:SetPoint("CENTER", 0, 0)
            countFs:SetTextColor(1, 1, 1)
            bubble._text = countFs
            bubble:Hide()
            roleBubbles[rri] = bubble
        end
        row._roleIcons = roleIcons
        row._roleBubbles = roleBubbles

        row:SetScript("OnEnter", function(self)
            rowBg:SetColorTexture(0.10, 0.10, 0.13, 0.85)
            if self._tooltipTitle and self._tooltipTitle ~= "" then
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                GameTooltip:AddLine(self._tooltipTitle, 0.9, 0.9, 0.9)
                if self._tooltipComment and self._tooltipComment ~= "" then
                    GameTooltip:AddLine(self._tooltipComment, 0.6, 0.6, 0.6, true)
                end
                GameTooltip:Show()
            end
        end)
        row:SetScript("OnLeave", function(self)
            rowBg:SetColorTexture(0.06, 0.06, 0.08, 0.55)
            GameTooltip:Hide()
        end)
        row:SetScript("OnClick", function()
            if row._resultID then Panel.ShowGroupDetail(row._resultID) end
        end)

        row._resultID = nil
        row._tooltipTitle = nil
        row._tooltipComment = nil
        R.rowPool[ri] = row
    end

    local scrollTrack = listingArea:CreateTexture(nil, "OVERLAY", nil, 1)
    scrollTrack:SetWidth(3)
    scrollTrack:SetPoint("TOPRIGHT", scrollArea, "TOPRIGHT", 6, -2)
    scrollTrack:SetPoint("BOTTOMRIGHT", scrollArea, "BOTTOMRIGHT", 6, 2)
    scrollTrack:SetColorTexture(1, 1, 1, 0.04)
    scrollTrack:Hide()
    R.scrollTrack = scrollTrack

    local scrollThumb = CreateFrame("Frame", nil, listingArea)
    scrollThumb:SetWidth(3)
    scrollThumb:SetHeight(30)
    scrollThumb:SetPoint("TOPRIGHT", scrollTrack, "TOPRIGHT", 0, 0)
    scrollThumb:SetFrameLevel(listingArea:GetFrameLevel() + 5)
    local thumbTex = scrollThumb:CreateTexture(nil, "OVERLAY")
    thumbTex:SetAllPoints()
    thumbTex:SetColorTexture(TC("accent"))
    thumbTex:SetAlpha(0.35)
    scrollThumb:Hide()
    R.scrollThumb = scrollThumb

    scrollArea:EnableMouseWheel(true)
    scrollArea:SetScript("OnMouseWheel", function(_, delta)
        local rowPitch = CFG.ROW_H + 4
        local h = (R.scrollArea and R.scrollArea:GetHeight()) or 0
        local visibleCap = (h > 0) and math.max(1, math.floor(h / rowPitch)) or CFG.ROW_POOL_SIZE
        if visibleCap > CFG.ROW_POOL_SIZE then visibleCap = CFG.ROW_POOL_SIZE end
        local maxOffset = math.max(0, #Panel._state.searchResults - visibleCap)
        Panel._state.scrollOffset = math.max(0, math.min(Panel._state.scrollOffset - delta, maxOffset))
        Panel.UpdateListingRows()
        Panel.UpdateScrollBar()
    end)

    do
        local ps = R.playerSidebar
        local yOff = -10
        local INNER_PAD = 10
        local PANEL_GAP = 8
        local PANEL_W = CFG.PLAYER_SIDEBAR_W - INNER_PAD * 2
        local SECTION_PAD = 12
        local ihlR, ihlG, ihlB = TC("accent")

        Panel._state.applyRole = { tank = false, healer = false, dps = false }

        local mpiCardH = 270
        local mpiPanel = CreateGlassPanel(ps, PANEL_W)
        mpiPanel:SetHeight(mpiCardH)
        mpiPanel:SetPoint("TOPLEFT", ps, "TOPLEFT", INNER_PAD, yOff)
        R.intel_mpiPanel = mpiPanel

        local mpiDot = mpiPanel:CreateTexture(nil, "OVERLAY", nil, 2)
        mpiDot:SetSize(4, 4)
        mpiDot:SetPoint("TOPLEFT", mpiPanel, "TOPLEFT", SECTION_PAD, -SECTION_PAD)
        mpiDot:SetColorTexture(ihlR, ihlG, ihlB, 0.8)

        local mpiHeader = mpiPanel:CreateFontString(nil, "OVERLAY")
        TrySetFont(mpiHeader, BODY_FONT, 9, "OUTLINE")
        mpiHeader:SetPoint("LEFT", mpiDot, "RIGHT", 5, 0)
        mpiHeader:SetTextColor(TC("mutedText"))
        mpiHeader:SetText("LEADER MPI")

        local mpiEmpty = mpiPanel:CreateFontString(nil, "OVERLAY")
        TrySetFont(mpiEmpty, BODY_FONT, 10, "")
        mpiEmpty:SetPoint("CENTER", mpiPanel, "CENTER", 0, 4)
        mpiEmpty:SetTextColor(TC("mutedText"))
        mpiEmpty:SetText("Select a group to see\nleader MPI score")
        mpiEmpty:SetJustifyH("CENTER")
        mpiEmpty:SetSpacing(3)
        R.intel_mpiEmpty = mpiEmpty

        local cardTopY = -28

        local classIcon = mpiPanel:CreateTexture(nil, "ARTWORK")
        classIcon:SetSize(28, 28)
        classIcon:SetPoint("TOPLEFT", mpiPanel, "TOPLEFT", SECTION_PAD, cardTopY)
        classIcon:SetColorTexture(0.4, 0.2, 0.6, 1)
        R.mpi_classIcon = classIcon

        local classInitial = mpiPanel:CreateFontString(nil, "OVERLAY")
        TrySetFont(classInitial, TITLE_FONT, 12, "OUTLINE")
        classInitial:SetPoint("CENTER", classIcon, "CENTER", 0, 0)
        classInitial:SetTextColor(1, 1, 1)
        R.mpi_classInitial = classInitial

        local cardName = mpiPanel:CreateFontString(nil, "OVERLAY")
        TrySetFont(cardName, BODY_FONT, 11, "")
        cardName:SetPoint("TOPLEFT", classIcon, "TOPRIGHT", 6, -1)
        cardName:SetTextColor(1, 1, 1)
        R.mpi_cardName = cardName

        local cardRealm = mpiPanel:CreateFontString(nil, "OVERLAY")
        TrySetFont(cardRealm, BODY_FONT, 8, "")
        cardRealm:SetPoint("LEFT", cardName, "RIGHT", 4, 0)
        cardRealm:SetTextColor(TC("mutedText"))
        R.mpi_cardRealm = cardRealm

        local cardClass = mpiPanel:CreateFontString(nil, "OVERLAY")
        TrySetFont(cardClass, BODY_FONT, 9, "")
        cardClass:SetPoint("TOPLEFT", cardName, "BOTTOMLEFT", 0, -2)
        R.mpi_cardClass = cardClass

        local cardRole = mpiPanel:CreateFontString(nil, "OVERLAY")
        TrySetFont(cardRole, BODY_FONT, 8, "OUTLINE")
        cardRole:SetPoint("LEFT", cardClass, "RIGHT", 6, 0)
        cardRole:SetTextColor(TC("mutedText"))
        R.mpi_cardRole = cardRole

        local cardScore = mpiPanel:CreateFontString(nil, "OVERLAY")
        TrySetFont(cardScore, TITLE_FONT, 20, "OUTLINE")
        cardScore:SetPoint("TOPRIGHT", mpiPanel, "TOPRIGHT", -40, cardTopY - 2)
        R.mpi_cardScore = cardScore

        local tierBadge = CreateFrame("Frame", nil, mpiPanel, "BackdropTemplate")
        tierBadge:SetSize(22, 22)
        tierBadge:SetPoint("LEFT", cardScore, "RIGHT", 4, 0)
        tierBadge:SetBackdrop({
            bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        tierBadge:SetBackdropColor(0, 0, 0, 0.3)
        R.mpi_tierBadge = tierBadge

        local tierText = tierBadge:CreateFontString(nil, "OVERLAY")
        TrySetFont(tierText, TITLE_FONT, 11, "OUTLINE")
        tierText:SetPoint("CENTER", tierBadge, "CENTER", 0, 0)
        R.mpi_tierText = tierText

        local trendIcon = mpiPanel:CreateFontString(nil, "OVERLAY")
        TrySetFont(trendIcon, TITLE_FONT, 9, "")
        trendIcon:SetPoint("TOPRIGHT", mpiPanel, "TOPRIGHT", -SECTION_PAD, cardTopY - 26)
        R.mpi_trendIcon = trendIcon

        local cardDiv = mpiPanel:CreateTexture(nil, "ARTWORK")
        cardDiv:SetTexture(W8)
        cardDiv:SetHeight(1)
        cardDiv:SetPoint("TOPLEFT", mpiPanel, "TOPLEFT", SECTION_PAD, cardTopY - 36)
        cardDiv:SetPoint("RIGHT", mpiPanel, "RIGHT", -SECTION_PAD, 0)
        cardDiv:SetVertexColor(1, 1, 1, 0.06)
        R.mpi_cardDiv = cardDiv

        local DIM_NAMES = { "Awareness", "Survival", "Output", "Utility", "Consistency" }
        R.mpi_dimRows = {}
        local barStartY = cardTopY - 46
        local BAR_ROW_H = 28

        for i, dimName in ipairs(DIM_NAMES) do
            local rowY = barStartY - (i - 1) * BAR_ROW_H
            local dimRow = {}

            local label = mpiPanel:CreateFontString(nil, "OVERLAY")
            TrySetFont(label, BODY_FONT, 9, "")
            label:SetPoint("TOPLEFT", mpiPanel, "TOPLEFT", SECTION_PAD, rowY)
            label:SetTextColor(TC("mutedText"))
            label:SetText(dimName)
            dimRow.label = label

            local value = mpiPanel:CreateFontString(nil, "OVERLAY")
            TrySetFont(value, BODY_FONT, 9, "OUTLINE")
            value:SetPoint("TOPRIGHT", mpiPanel, "TOPRIGHT", -SECTION_PAD, rowY)
            dimRow.value = value

            local track = mpiPanel:CreateTexture(nil, "BACKGROUND")
            track:SetHeight(4)
            track:SetPoint("TOPLEFT", label, "BOTTOMLEFT", 0, -3)
            track:SetPoint("RIGHT", mpiPanel, "RIGHT", -(SECTION_PAD + 30), 0)
            track:SetColorTexture(0.15, 0.15, 0.20, 0.8)
            dimRow.track = track

            local fill = mpiPanel:CreateTexture(nil, "ARTWORK")
            fill:SetHeight(4)
            fill:SetPoint("TOPLEFT", track, "TOPLEFT", 0, 0)
            fill:SetWidth(1)
            dimRow.fill = fill

            R.mpi_dimRows[i] = dimRow
        end

        local badgeDiv = mpiPanel:CreateTexture(nil, "ARTWORK")
        badgeDiv:SetTexture(W8)
        badgeDiv:SetHeight(1)
        local badgeDivY = barStartY - 5 * BAR_ROW_H - 4
        badgeDiv:SetPoint("TOPLEFT", mpiPanel, "TOPLEFT", SECTION_PAD, badgeDivY)
        badgeDiv:SetPoint("RIGHT", mpiPanel, "RIGHT", -SECTION_PAD, 0)
        badgeDiv:SetVertexColor(1, 1, 1, 0.06)
        R.mpi_badgeDiv = badgeDiv

        local badgeLabel = mpiPanel:CreateFontString(nil, "OVERLAY")
        TrySetFont(badgeLabel, BODY_FONT, 8, "OUTLINE")
        badgeLabel:SetPoint("TOPLEFT", badgeDiv, "BOTTOMLEFT", 0, -6)
        badgeLabel:SetTextColor(TC("mutedText"))
        badgeLabel:SetText("BADGES")
        R.mpi_badgeLabel = badgeLabel

        R.mpi_badgeSlots = {}
        for bi = 1, 6 do
            local slot = mpiPanel:CreateFontString(nil, "OVERLAY")
            TrySetFont(slot, BODY_FONT, 8, "OUTLINE")
            if bi == 1 then
                slot:SetPoint("LEFT", badgeLabel, "RIGHT", 8, 0)
            else
                slot:SetPoint("LEFT", R.mpi_badgeSlots[bi - 1], "RIGHT", 4, 0)
            end
            slot:SetTextColor(ihlR, ihlG, ihlB, 0.8)
            slot:Hide()
            R.mpi_badgeSlots[bi] = slot
        end

        local noDataMsg = mpiPanel:CreateFontString(nil, "OVERLAY")
        TrySetFont(noDataMsg, BODY_FONT, 9, "")
        noDataMsg:SetPoint("TOPLEFT", cardDiv, "BOTTOMLEFT", 0, -16)
        noDataMsg:SetPoint("RIGHT", mpiPanel, "RIGHT", -SECTION_PAD, 0)
        noDataMsg:SetTextColor(TC("mutedText"))
        noDataMsg:SetJustifyH("CENTER")
        noDataMsg:SetWordWrap(true)
        noDataMsg:SetSpacing(3)
        noDataMsg:SetText("No community data for this leader.\nRun keys with MPI to build scores.")
        noDataMsg:Hide()
        R.mpi_noDataMsg = noDataMsg

        classIcon:Hide(); classInitial:Hide()
        cardName:Hide(); cardRealm:Hide(); cardClass:Hide(); cardRole:Hide()
        cardScore:Hide(); tierBadge:Hide(); trendIcon:Hide()
        cardDiv:Hide(); badgeDiv:Hide(); badgeLabel:Hide()
        for _, dr in ipairs(R.mpi_dimRows) do
            dr.label:Hide(); dr.value:Hide(); dr.track:Hide(); dr.fill:Hide()
        end
        mpiEmpty:Show()

        yOff = yOff - mpiCardH - PANEL_GAP

        local notePanel = CreateGlassPanel(ps, PANEL_W)
        notePanel:SetHeight(80)
        notePanel:SetPoint("TOPLEFT", ps, "TOPLEFT", INNER_PAD, yOff)
        R.intel_notePanel = notePanel

        local noteDot = notePanel:CreateTexture(nil, "OVERLAY", nil, 2)
        noteDot:SetSize(4, 4)
        noteDot:SetPoint("TOPLEFT", notePanel, "TOPLEFT", SECTION_PAD, -SECTION_PAD)
        noteDot:SetColorTexture(ihlR, ihlG, ihlB, 0.8)

        local descLabel = notePanel:CreateFontString(nil, "OVERLAY")
        TrySetFont(descLabel, BODY_FONT, 9, "OUTLINE")
        descLabel:SetPoint("LEFT", noteDot, "RIGHT", 5, 0)
        descLabel:SetTextColor(TC("mutedText"))
        descLabel:SetText("NOTE TO LEADER")

        local descBox = CreateFrame("EditBox", nil, notePanel, "BackdropTemplate")
        descBox:SetHeight(42)
        descBox:SetPoint("TOPLEFT", notePanel, "TOPLEFT", 8, -28)
        descBox:SetPoint("TOPRIGHT", notePanel, "TOPRIGHT", -8, -28)
        descBox:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        descBox:SetBackdropColor(0, 0, 0, 0.35)
        descBox:SetBackdropBorderColor(1, 1, 1, 0.12)
        TrySetFont(descBox, BODY_FONT, 10, "")
        descBox:SetTextColor(TC("bodyText"))
        descBox:SetAutoFocus(false)
        descBox:SetMaxLetters(60)
        descBox:SetMultiLine(true)
        descBox:SetTextInsets(10, 10, 8, 8)
        descBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
        descBox:SetScript("OnEditFocusGained", function(self)
            self:SetBackdropBorderColor(ihlR, ihlG, ihlB, 0.5)
        end)
        descBox:SetScript("OnEditFocusLost", function(self)
            self:SetBackdropBorderColor(1, 1, 1, 0.12)
        end)
        R.applyDescBox = descBox

        local descPlaceholder = descBox:CreateFontString(nil, "OVERLAY")
        TrySetFont(descPlaceholder, BODY_FONT, 10, "")
        descPlaceholder:SetPoint("TOPLEFT", descBox, "TOPLEFT", 10, -8)
        descPlaceholder:SetTextColor(TC("mutedText"))
        descPlaceholder:SetText("Write a note...")
        descBox:SetScript("OnTextChanged", function(self)
            descPlaceholder:SetShown(self:GetText() == "")
        end)

        yOff = yOff - 80 - PANEL_GAP


        local appTrigger = CreateFrame("Frame", nil, ps, "BackdropTemplate")
        appTrigger:SetHeight(28)
        appTrigger:SetPoint("BOTTOMLEFT", ps, "BOTTOMLEFT", INNER_PAD, 10)
        appTrigger:SetPoint("BOTTOMRIGHT", ps, "BOTTOMRIGHT", -INNER_PAD, 10)
        appTrigger:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        appTrigger:SetBackdropColor(0.06, 0.06, 0.08, 0.75)
        appTrigger:SetBackdropBorderColor(0.20, 0.20, 0.25, 0.5)
        appTrigger:EnableMouse(true)

        local appTrigDot = appTrigger:CreateTexture(nil, "OVERLAY", nil, 2)
        appTrigDot:SetSize(4, 4)
        appTrigDot:SetPoint("LEFT", appTrigger, "LEFT", 10, 0)
        appTrigDot:SetColorTexture(ihlR, ihlG, ihlB, 0.8)

        local appTrigLabel = appTrigger:CreateFontString(nil, "OVERLAY")
        TrySetFont(appTrigLabel, BODY_FONT, 9, "OUTLINE")
        appTrigLabel:SetPoint("LEFT", appTrigDot, "RIGHT", 5, 0)
        appTrigLabel:SetTextColor(TC("mutedText"))
        appTrigLabel:SetText("MY APPLICATIONS (0/5)")
        R.intel_appHeader = appTrigLabel

        local appTrigArrow = appTrigger:CreateFontString(nil, "OVERLAY")
        TrySetFont(appTrigArrow, BODY_FONT, 10, "")
        appTrigArrow:SetPoint("RIGHT", appTrigger, "RIGHT", -8, 0)
        appTrigArrow:SetTextColor(TC("mutedText"))
        appTrigArrow:SetText("<")

        local FLYOUT_W = 240
        local APP_CARD_H = 40
        local APP_CARD_GAP = 4
        local FLYOUT_MAX_H = 28 + 5 * (APP_CARD_H + APP_CARD_GAP) + 8

        local appFlyout = CreateFrame("Frame", nil, ps, "BackdropTemplate")
        appFlyout:SetSize(FLYOUT_W, FLYOUT_MAX_H)
        appFlyout:SetPoint("BOTTOMRIGHT", ps, "BOTTOMLEFT", -4, 0)
        appFlyout:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        appFlyout:SetBackdropColor(0.04, 0.04, 0.06, 0.96)
        appFlyout:SetBackdropBorderColor(ihlR, ihlG, ihlB, 0.25)
        appFlyout:SetFrameLevel(ps:GetFrameLevel() + 15)
        appFlyout:EnableMouse(true)
        appFlyout:Hide()
        R.intel_appFlyout = appFlyout

        local flyHeader = appFlyout:CreateFontString(nil, "OVERLAY")
        TrySetFont(flyHeader, BODY_FONT, 9, "OUTLINE")
        flyHeader:SetPoint("TOPLEFT", appFlyout, "TOPLEFT", 10, -10)
        flyHeader:SetTextColor(TC("mutedText"))
        flyHeader:SetText("MY APPLICATIONS")

        local appEmpty = appFlyout:CreateFontString(nil, "OVERLAY")
        TrySetFont(appEmpty, BODY_FONT, 10, "")
        appEmpty:SetPoint("CENTER", appFlyout, "CENTER", 0, 0)
        appEmpty:SetTextColor(TC("mutedText"))
        appEmpty:SetText("No applications yet")
        R.intel_appEmpty = appEmpty

        R.intel_appRows = {}
        for ai = 1, 5 do
            local cardY = -28 - (ai - 1) * (APP_CARD_H + APP_CARD_GAP)

            local card = CreateFrame("Frame", nil, appFlyout, "BackdropTemplate")
            card:SetHeight(APP_CARD_H)
            card:SetPoint("TOPLEFT", appFlyout, "TOPLEFT", 6, cardY)
            card:SetPoint("TOPRIGHT", appFlyout, "TOPRIGHT", -6, cardY)
            card:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
                insets = { left = 1, right = 1, top = 1, bottom = 1 } })
            card:SetBackdropColor(0, 0, 0, 0.25)
            card:SetBackdropBorderColor(1, 1, 1, 0.06)

            local statusBar = card:CreateTexture(nil, "OVERLAY", nil, 2)
            statusBar:SetWidth(2)
            statusBar:SetPoint("TOPLEFT", card, "TOPLEFT", 0, 0)
            statusBar:SetPoint("BOTTOMLEFT", card, "BOTTOMLEFT", 0, 0)
            statusBar:SetTexture(W8)
            local appAcR, appAcG, appAcB = TC("accent")
            if statusBar.SetGradient and CreateColor then
                statusBar:SetGradient("VERTICAL",
                    CreateColor(appAcR, appAcG, appAcB, 0.2),
                    CreateColor(appAcR, appAcG, appAcB, 0.8))
            else
                statusBar:SetColorTexture(appAcR, appAcG, appAcB)
            end
            card._statusBar = statusBar

            local appName = card:CreateFontString(nil, "OVERLAY")
            TrySetFont(appName, BODY_FONT, 10, "")
            appName:SetPoint("TOPLEFT", card, "TOPLEFT", 10, -6)
            appName:SetPoint("RIGHT", card, "RIGHT", -24, 0)
            appName:SetJustifyH("LEFT")
            appName:SetWordWrap(false)
            appName:SetTextColor(TC("bodyText"))
            card._name = appName

            local appStatus = card:CreateFontString(nil, "OVERLAY")
            TrySetFont(appStatus, BODY_FONT, 9, "")
            appStatus:SetPoint("BOTTOMLEFT", card, "BOTTOMLEFT", 10, 6)
            appStatus:SetTextColor(TC("mutedText"))
            card._status = appStatus

            local cancelBtn = CreateFrame("Button", nil, card)
            cancelBtn:SetSize(20, 20)
            cancelBtn:SetPoint("RIGHT", card, "RIGHT", -6, 0)
            local cancelX = cancelBtn:CreateFontString(nil, "OVERLAY")
            TrySetFont(cancelX, BODY_FONT, 10, "OUTLINE")
            cancelX:SetPoint("CENTER")
            cancelX:SetTextColor(TC("mutedText"))
            cancelX:SetText("x")
            cancelBtn:SetScript("OnEnter", function() cancelX:SetTextColor(1, 0.3, 0.3) end)
            cancelBtn:SetScript("OnLeave", function() cancelX:SetTextColor(TC("mutedText")) end)
            cancelBtn._resultID = nil
            cancelBtn:SetScript("OnClick", function(self)
                if self._resultID then
                    pcall(C_LFGList.CancelApplication, self._resultID)
                    C_Timer.After(0.3, function()
                        Panel.UpdateGroupIntelSidebar()
                        if R.intel_appFlyout then R.intel_appFlyout:Show() end
                    end)
                end
            end)
            card._cancelBtn = cancelBtn

            card:Hide()
            R.intel_appRows[ai] = card
        end

        local hideTimer = nil
        local function ShowFlyout()
            if hideTimer then hideTimer:Cancel(); hideTimer = nil end
            Panel.UpdateGroupIntelSidebar()
            appFlyout:Show()
            appTrigger:SetBackdropBorderColor(ihlR, ihlG, ihlB, 0.4)
            appTrigLabel:SetTextColor(1, 1, 1)
            appTrigArrow:SetTextColor(1, 1, 1)
        end
        local function ScheduleHide()
            if hideTimer then hideTimer:Cancel() end
            hideTimer = C_Timer.NewTimer(0.25, function()
                appFlyout:Hide()
                appTrigger:SetBackdropBorderColor(0.20, 0.20, 0.25, 0.5)
                appTrigLabel:SetTextColor(TC("mutedText"))
                appTrigArrow:SetTextColor(TC("mutedText"))
                hideTimer = nil
            end)
        end

        appTrigger:SetScript("OnEnter", ShowFlyout)
        appTrigger:SetScript("OnLeave", ScheduleHide)
        appFlyout:SetScript("OnEnter", ShowFlyout)
        appFlyout:SetScript("OnLeave", ScheduleHide)

        R.intel_categoryLine = nil
        R.intel_searchTimeLine = nil
    end

    do
        local detailPanel = CreateFrame("Frame", nil, R.listingArea, "BackdropTemplate")
        detailPanel:SetHeight(CFG.DETAIL_PANEL_H)
        detailPanel:SetPoint("BOTTOMLEFT", R.listingArea, "BOTTOMLEFT", 0, 0)
        detailPanel:SetPoint("BOTTOMRIGHT", R.listingArea, "BOTTOMRIGHT", 0, 0)
        detailPanel:SetFrameLevel(R.listingArea:GetFrameLevel() + 10)
        detailPanel:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        detailPanel:SetBackdropColor(0.025, 0.028, 0.045, 0.98)
        local ac = activeTheme.accent
        detailPanel:SetBackdropBorderColor(ac[1], ac[2], ac[3], 0.12)
        detailPanel:Hide()
        R.detailPanel = detailPanel

        detailPanel:EnableMouse(true)


        local DETAIL_HEADER_H = 36
        local backBtn = CreateFrame("Button", nil, detailPanel)
        backBtn:SetSize(24, DETAIL_HEADER_H)
        backBtn:SetPoint("TOPLEFT", detailPanel, "TOPLEFT", 8, -2)
        local backIcon = backBtn:CreateTexture(nil, "ARTWORK")
        backIcon:SetSize(14, 14)
        backIcon:SetPoint("CENTER")
        local arrowOk = pcall(backIcon.SetAtlas, backIcon, "shop-header-arrow")
        if arrowOk then
            backIcon:SetTexCoord(0, 1, 1, 0)
        else
            backIcon:Hide()
            local backFallback = backBtn:CreateFontString(nil, "OVERLAY")
            TrySetFont(backFallback, TITLE_FONT, 16, "")
            backFallback:SetPoint("CENTER")
            backFallback:SetTextColor(TC("mutedText"))
            backFallback:SetText("\226\134\144")
        end
        backIcon:SetVertexColor(TC("mutedText"))
        backBtn:SetScript("OnEnter", function() backIcon:SetVertexColor(TC("accent")) end)
        backBtn:SetScript("OnLeave", function() backIcon:SetVertexColor(TC("mutedText")) end)
        backBtn:SetScript("OnClick", function() Panel.HideGroupDetail() end)

        local titleBlock = CreateFrame("Frame", nil, detailPanel)
        titleBlock:SetHeight(DETAIL_HEADER_H)
        titleBlock:SetPoint("TOPLEFT", backBtn, "TOPRIGHT", 4, 0)
        titleBlock:SetPoint("RIGHT", detailPanel, "RIGHT", -100, 0)

        local groupName = titleBlock:CreateFontString(nil, "OVERLAY")
        TrySetFont(groupName, TITLE_FONT, 14, "OUTLINE")
        groupName:SetPoint("TOPLEFT", titleBlock, "TOPLEFT", 0, -4)
        groupName:SetPoint("RIGHT", titleBlock, "RIGHT", 0, 0)
        groupName:SetJustifyH("LEFT")
        groupName:SetTextColor(TC("bodyText"))
        groupName:SetWordWrap(false)
        R.detail_groupName = groupName

        local comment = titleBlock:CreateFontString(nil, "OVERLAY")
        TrySetFont(comment, BODY_FONT, 10, "")
        comment:SetPoint("TOPLEFT", groupName, "BOTTOMLEFT", 0, -4)
        comment:SetPoint("RIGHT", titleBlock, "RIGHT", 0, 0)
        comment:SetJustifyH("LEFT")
        comment:SetTextColor(TC("mutedText"))
        comment:SetWordWrap(false)
        R.detail_comment = comment

        local applyBtn = CreateFrame("Button", nil, detailPanel, "BackdropTemplate")
        applyBtn:SetSize(80, 26)
        applyBtn:SetPoint("RIGHT", detailPanel, "RIGHT", -10, 0)
        applyBtn:SetPoint("TOP", detailPanel, "TOP", 0, -(math.floor((DETAIL_HEADER_H - 26) / 2) + 2))
        applyBtn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        local dAr, dAg, dAb = TC("accent")
        applyBtn:SetBackdropColor(dAr, dAg, dAb, 0.15)
        applyBtn:SetBackdropBorderColor(dAr, dAg, dAb, 0.30)
        local applyLbl = applyBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(applyLbl, BODY_FONT, 11, "OUTLINE")
        applyLbl:SetPoint("CENTER")
        applyLbl:SetTextColor(TC("accent"))
        applyLbl:SetText("Apply")
        applyBtn._label = applyLbl
        applyBtn:SetScript("OnEnter", function(self)
            self:SetBackdropColor(dAr, dAg, dAb, 0.25)
            self:SetBackdropBorderColor(dAr, dAg, dAb, 0.50)
        end)
        applyBtn:SetScript("OnLeave", function(self)
            self:SetBackdropColor(dAr, dAg, dAb, 0.15)
            self:SetBackdropBorderColor(dAr, dAg, dAb, 0.30)
        end)
        applyBtn:SetScript("OnClick", function(self)
            if not detailPanel._resultID then return end
            if self._isCancelMode then
                pcall(C_LFGList.CancelApplication, detailPanel._resultID)
                C_Timer.After(0.3, function()
                    Panel.UpdateGroupIntelSidebar()
                    Panel.UpdateListingRows()
                    Panel.ShowGroupDetail(detailPanel._resultID)
                end)
            else
                Panel.ApplyToGroup(detailPanel._resultID)
            end
        end)
        R.detail_applyBtn = applyBtn

        local PILL_Y = -38

        local leaderPill = CreateFrame("Frame", nil, detailPanel, "BackdropTemplate")
        leaderPill:SetHeight(22)
        leaderPill:SetPoint("TOPLEFT", detailPanel, "TOPLEFT", 10, PILL_Y)
        leaderPill:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        leaderPill:SetBackdropColor(TC("surfaceBg"))
        leaderPill:SetBackdropBorderColor(0, 0, 0, 0)
        local lpDot = leaderPill:CreateTexture(nil, "OVERLAY", nil, 2)
        lpDot:SetSize(3, 14)
        lpDot:SetPoint("LEFT", leaderPill, "LEFT", 3, 0)
        lpDot:SetColorTexture(TC("accent"))
        local lpLabel = leaderPill:CreateFontString(nil, "OVERLAY")
        TrySetFont(lpLabel, BODY_FONT, 9, "OUTLINE")
        lpLabel:SetPoint("LEFT", lpDot, "RIGHT", 5, 0)
        lpLabel:SetTextColor(TC("mutedText"))
        lpLabel:SetText("LEADER")
        local lpValue = leaderPill:CreateFontString(nil, "OVERLAY")
        TrySetFont(lpValue, BODY_FONT, 10, "")
        lpValue:SetPoint("LEFT", lpLabel, "RIGHT", 6, 0)
        lpValue:SetTextColor(TC("bodyText"))
        leaderPill._value = lpValue
        R.detail_leaderPill = leaderPill

        local listedPill = CreateFrame("Frame", nil, detailPanel, "BackdropTemplate")
        listedPill:SetHeight(22)
        listedPill:SetPoint("LEFT", leaderPill, "RIGHT", 6, 0)
        listedPill:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        listedPill:SetBackdropColor(TC("surfaceBg"))
        listedPill:SetBackdropBorderColor(0, 0, 0, 0)
        local ltDot = listedPill:CreateTexture(nil, "OVERLAY", nil, 2)
        ltDot:SetSize(3, 14)
        ltDot:SetPoint("LEFT", listedPill, "LEFT", 3, 0)
        ltDot:SetColorTexture(TC("accent"))
        local ltLabel = listedPill:CreateFontString(nil, "OVERLAY")
        TrySetFont(ltLabel, BODY_FONT, 9, "OUTLINE")
        ltLabel:SetPoint("LEFT", ltDot, "RIGHT", 5, 0)
        ltLabel:SetTextColor(TC("mutedText"))
        ltLabel:SetText("LISTED")
        local ltValue = listedPill:CreateFontString(nil, "OVERLAY")
        TrySetFont(ltValue, BODY_FONT, 10, "")
        ltValue:SetPoint("LEFT", ltLabel, "RIGHT", 6, 0)
        ltValue:SetTextColor(TC("bodyText"))
        listedPill._value = ltValue
        R.detail_listedPill = listedPill

        local voicePill = CreateFrame("Frame", nil, detailPanel, "BackdropTemplate")
        voicePill:SetHeight(22)
        voicePill:SetPoint("LEFT", listedPill, "RIGHT", 6, 0)
        voicePill:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        voicePill:SetBackdropColor(TC("surfaceBg"))
        voicePill:SetBackdropBorderColor(0, 0, 0, 0)
        local vpDot = voicePill:CreateTexture(nil, "OVERLAY")
        vpDot:SetSize(3, 14)
        vpDot:SetPoint("LEFT", voicePill, "LEFT", 3, 0)
        if vpDot.SetColorTexture then
            pcall(vpDot.SetColorTexture, vpDot, 0.35, 0.88, 0.62, 0.8)
        end
        local vpLabel = voicePill:CreateFontString(nil, "OVERLAY")
        TrySetFont(vpLabel, BODY_FONT, 9, "OUTLINE")
        vpLabel:SetPoint("LEFT", vpDot, "RIGHT", 5, 0)
        vpLabel:SetTextColor(TC("mutedText"))
        vpLabel:SetText("VOICE")
        local vpValue = voicePill:CreateFontString(nil, "OVERLAY")
        TrySetFont(vpValue, BODY_FONT, 10, "")
        vpValue:SetPoint("LEFT", vpLabel, "RIGHT", 6, 0)
        vpValue:SetTextColor(TC("bodyText"))
        voicePill._value = vpValue
        voicePill:Hide()
        R.detail_voicePill = voicePill

        R.detail_infoLine = nil

        local sep = detailPanel:CreateTexture(nil, "ARTWORK")
        sep:SetHeight(1)
        sep:SetPoint("TOPLEFT", detailPanel, "TOPLEFT", 8, -72)
        sep:SetPoint("TOPRIGHT", detailPanel, "TOPRIGHT", -8, -72)
        sep:SetColorTexture(1, 1, 1, 0.08)

        local membersHeader = detailPanel:CreateFontString(nil, "OVERLAY")
        TrySetFont(membersHeader, BODY_FONT, 10, "OUTLINE")
        membersHeader:SetPoint("TOPLEFT", detailPanel, "TOPLEFT", 10, -80)
        membersHeader:SetTextColor(TC("mutedText"))
        membersHeader:SetText("MEMBERS")

        local memberScroll = CreateFrame("Frame", nil, detailPanel)
        memberScroll:SetPoint("TOPLEFT", detailPanel, "TOPLEFT", 10, -96)
        memberScroll:SetPoint("RIGHT", detailPanel, "RIGHT", -10, 0)
        memberScroll:SetHeight(140)
        memberScroll:SetClipsChildren(true)
        memberScroll:EnableMouseWheel(true)
        R.detailMemberScroll = memberScroll

        local MEMBER_ROW_H = 28
        local MAX_DETAIL_MEMBERS = 40
        Panel._state.detailMemberScrollOffset = 0
        Panel._state.detailMemberCount = 0

        memberScroll:SetScript("OnMouseWheel", function(_, delta)
            local visibleRows = math.floor(memberScroll:GetHeight() / MEMBER_ROW_H)
            local maxOffset = math.max(0, Panel._state.detailMemberCount - visibleRows)
            Panel._state.detailMemberScrollOffset = math.max(0, math.min(
                Panel._state.detailMemberScrollOffset - delta, maxOffset))
            local offset = Panel._state.detailMemberScrollOffset
            local resultID = Panel._state._detailMemberResultID
            local info = Panel._state._detailMemberInfo
            if not resultID or not info then return end
            for mi, mRow in ipairs(R.detailMembers) do
                local dataIdx = mi + offset
                if dataIdx <= Panel._state.detailMemberCount then
                    local ok2, role, class, classLocal, specLocal = pcall(C_LFGList.GetSearchResultMemberInfo, resultID, dataIdx)
                    if ok2 then
                        local memberName = ""
                        if specLocal and specLocal ~= "" and classLocal then
                            memberName = specLocal .. " " .. classLocal
                        elseif classLocal then
                            memberName = classLocal
                        end
                        mRow._nameText:SetText(memberName)
                        local cc = class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[class]
                        if cc then mRow._nameText:SetTextColor(cc.r, cc.g, cc.b)
                        else mRow._nameText:SetTextColor(TC("bodyText")) end
                        local roleLabel = role == "TANK" and "Tank" or role == "HEALER" and "Healer" or "DPS"
                        mRow._roleText:SetText("(" .. roleLabel .. ")")
                        mRow._mpiBg:SetColorTexture(0.5, 0.5, 0.5, 0.3)
                        mRow._mpiText:SetText("--")
                        mRow._bestText:SetText("")
                        mRow:Show()
                    else
                        mRow:Hide()
                    end
                else
                    mRow:Hide()
                end
            end
        end)

        R.detailMembers = {}
        local memberPoolSize = math.min(MAX_DETAIL_MEMBERS, math.floor(200 / MEMBER_ROW_H) + 2)

        for mi = 1, memberPoolSize do
            local mRow = CreateFrame("Frame", nil, memberScroll)
            mRow:SetHeight(MEMBER_ROW_H)
            mRow:SetPoint("TOPLEFT", memberScroll, "TOPLEFT", 0, -((mi - 1) * MEMBER_ROW_H))
            mRow:SetPoint("RIGHT", memberScroll, "RIGHT", 0, 0)

            local mpiBg = mRow:CreateTexture(nil, "BACKGROUND")
            mpiBg:SetSize(32, 18)
            mpiBg:SetPoint("LEFT", mRow, "LEFT", 0, 0)
            mpiBg:SetColorTexture(0.5, 0.5, 0.5, 0.3)
            mRow._mpiBg = mpiBg

            local mpiText = mRow:CreateFontString(nil, "OVERLAY")
            TrySetFont(mpiText, BODY_FONT, 9, "OUTLINE")
            mpiText:SetPoint("CENTER", mpiBg, "CENTER", 0, 0)
            mpiText:SetTextColor(1, 1, 1, 0.9)
            mpiText:SetText("--")
            mRow._mpiText = mpiText

            local nameText = mRow:CreateFontString(nil, "OVERLAY")
            TrySetFont(nameText, BODY_FONT, 11, "")
            nameText:SetPoint("LEFT", mpiBg, "RIGHT", 6, 0)
            nameText:SetTextColor(TC("bodyText"))
            mRow._nameText = nameText

            local roleText = mRow:CreateFontString(nil, "OVERLAY")
            TrySetFont(roleText, BODY_FONT, 10, "")
            roleText:SetPoint("LEFT", nameText, "RIGHT", 4, 0)
            roleText:SetTextColor(TC("mutedText"))
            mRow._roleText = roleText

            local bestText = mRow:CreateFontString(nil, "OVERLAY")
            TrySetFont(bestText, BODY_FONT, 10, "")
            bestText:SetPoint("RIGHT", mRow, "RIGHT", -4, 0)
            bestText:SetJustifyH("RIGHT")
            bestText:SetTextColor(TC("bodyText"))
            mRow._bestText = bestText

            mRow:Hide()
            R.detailMembers[mi] = mRow
        end
    end

    end

    local settingsPopup = CreateFrame("Frame", nil, f, "BackdropTemplate")
    settingsPopup:SetSize(180, 120)
    settingsPopup:SetPoint("TOPRIGHT", R.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(f: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 tBtn = CreateFrame("Button", nil, settingsPopup)
        tBtn:SetSize(160, 24)
        tBtn:SetPoint("TOP", settingsPopup, "TOP", 0, -24 - (i - 1) * 28)
        local tLbl = tBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(tLbl, BODY_FONT, 11, "")
        tLbl:SetPoint("CENTER")
        tLbl:SetText(opt.label)
        tLbl:SetTextColor(TC("bodyText"))
        tBtn:SetScript("OnClick", function()
            local s = _G.MidnightUISettings
            if s and s.General then s.General.characterPanelTheme = opt.key end
            Panel.ApplyTheme()
            settingsPopup:Hide()
        end)
        tBtn:SetScript("OnEnter", function() tLbl:SetTextColor(TC("titleText")) end)
        tBtn:SetScript("OnLeave", function() tLbl:SetTextColor(TC("bodyText")) end)
    end

    return f
end

function Panel.SetActiveTab(key)
    Panel.HideGroupDetail()
    Panel._state.activeTab = key
    if key ~= "pvp" then
        _G.MidnightUISettings = _G.MidnightUISettings or {}
        _G.MidnightUISettings.GroupFinder = _G.MidnightUISettings.GroupFinder or {}
        _G.MidnightUISettings.GroupFinder.lastTab = key
    end
    local R = Panel._refs
    if R.seasonBtn then
        R.seasonBtn:SetShown(key == "premade")
        if key ~= "premade" then GameTooltip:Hide() end
    end

    local contentMap = {
        dungeons = R.dungeonContent,
        raids    = R.raidContent,
        premade  = R.premadeContent,
        pvp      = R.pvpContent,
    }

    if R.tabs then
        for _, tab in ipairs(R.tabs) do
            if tab._key == key then
                tab._label:SetTextColor(TC("tabActive"))
                tab._underline:Show()
            else
                tab._label:SetTextColor(TC("tabInactive"))
                tab._underline:Hide()
            end
        end
    end

    for _, frame in pairs(contentMap) do
        if frame then frame:Hide() end
    end
    if contentMap[key] then
        contentMap[key]:Show()
    end

    if key == "premade" then
        StartAutoRefresh()
    else
        StopAutoRefresh()
    end

    if key == "premade" then
        Panel.UpdateCatButtons()
        Panel.UpdateFilterBarVisibility()
        Panel.UpdatePlayerSidebar()
        Panel.UpdatePremadeHero()
        if Panel._state._searchCategory ~= nil
           and Panel._state._searchCategory ~= Panel._state.activeCategory then
            Panel._state.searchResults = {}
            Panel._state.scrollOffset = 0
            Panel.UpdateListingRows()
        end
        if Panel._state._searchCategory == nil and #(Panel._state.searchResults or {}) > 0 then
            Panel._state._searchCategory = Panel._state.activeCategory
        end
        local R = Panel._refs
        if #Panel._state.searchResults == 0 then
            if R.emptyLabel then
                R.emptyLabel:SetText("Search For Groups")
                R.emptyLabel:Show()
            end
        end
    elseif key == "dungeons" then
        Panel.AutoDetectDungeonRoles()
        Panel.UpdateDungeonCards()
        Panel.UpdateDungeonQueueStatus()
        Panel.UpdateDungeonHero()
        Panel.UpdateDungeonSuggestions()
    elseif key == "raids" then
        Panel.AutoDetectRaidRoles()
        Panel.UpdateRaidFinder()
        Panel.UpdateRaidQueueStatus()
        Panel.UpdateRaidHero()
    elseif key == "pvp" then
        Panel._state.activeTab = "premade"
        Panel._state._pvpHandoff = true
        Panel.Hide()
        C_Timer.After(0.2, function()
            for _, addon in ipairs({"Blizzard_PVEFrame", "Blizzard_PVPUI"}) do
                local isLoaded = (C_AddOns and C_AddOns.IsAddOnLoaded) and C_AddOns.IsAddOnLoaded(addon) or (IsAddOnLoaded and IsAddOnLoaded(addon))
                if not isLoaded then
                    pcall((C_AddOns and C_AddOns.LoadAddOn) or LoadAddOn, addon)
                end
            end
            if PVEFrame then
                pcall(ShowUIPanel, PVEFrame)
                if PVEFrame_ShowFrame then
                    pcall(PVEFrame_ShowFrame, "PVPUIFrame")
                end
            end
            C_Timer.After(0.3, function()
                Panel._state._pvpHandoff = false
            end)
        end)
        return
    end
end


local PVP_QUICKMATCH = {
    { name = "Random Battlegrounds",      isRandom = true, queueType = "random_bg" },
    { name = "Random Epic Battlegrounds", isRandom = true, queueType = "random_epic" },
    { name = "Arena Skirmish",            isSkirmish = true, queueType = "skirmish" },
    { name = "Brawl",                     isBrawl = true, queueType = "brawl", minLevel = 90 },
    { name = "Warsong Gulch",             teamSize = "10v10", queueType = "specific" },
    { name = "Arathi Basin",              teamSize = "15v15", queueType = "specific" },
    { name = "Deephaul Ravine",           teamSize = "10v10", queueType = "specific" },
    { name = "Alterac Valley",            teamSize = "40v40", queueType = "specific" },
    { name = "Eye of the Storm",          teamSize = "15v15", queueType = "specific" },
    { name = "Isle of Conquest",          teamSize = "40v40", queueType = "specific" },
    { name = "The Battle for Gilneas",    teamSize = "10v10", queueType = "specific" },
    { name = "Battle for Wintergrasp",    teamSize = "40v40", queueType = "specific" },
    { name = "Ashran",                    teamSize = "40v40", queueType = "specific" },
    { name = "Twin Peaks",               teamSize = "10v10", queueType = "specific" },
    { name = "Silvershard Mines",         teamSize = "10v10", queueType = "specific" },
    { name = "Temple of Kotmogu",         teamSize = "10v10", queueType = "specific" },
    { name = "Seething Shore",            teamSize = "10v10", queueType = "specific" },
    { name = "Deepwind Gorge",           teamSize = "15v15", queueType = "specific" },
}
local PVP_RATED = {
    { name = "Solo Shuffle (Arena)",         bracket = 7, queueType = "solo_arena" },
    { name = "Solo Shuffle (Battlegrounds)", bracket = 4, queueType = "solo_bg" },
    { name = "2v2 Arena",                    bracket = 1, queueType = "arena2v2" },
    { name = "3v3 Arena",                    bracket = 2, queueType = "arena3v3" },
    { name = "10v10 Rated Battlegrounds",    bracket = 3, queueType = "rated_bg" },
}
local PVP_TRAINING = {
    { name = "Random Training Battlegrounds",              isRandom = true, queueType = "training_random" },
    { name = "The Battle for Gilneas Training Grounds",    queueType = "training_gilneas" },
    { name = "Silvershard Mines Training Grounds",         queueType = "training_silvershard" },
    { name = "Arathi Basin Training Grounds",              queueType = "training_arathi" },
}

function Panel.AutoDetectPVPRoles()
    local ok, tank, healer, dps = pcall(GetLFGRoles)
    if ok then
        Panel._state.pvpRoles.tank = tank or false
        Panel._state.pvpRoles.healer = healer or false
        Panel._state.pvpRoles.dps = dps or false
    end
    local R = Panel._refs
    if R.pvpRoleBtns then
        local roleKeys = { "tank", "healer", "dps" }
        for i, btn in ipairs(R.pvpRoleBtns) do
            local active = Panel._state.pvpRoles[roleKeys[i]]
            btn._icon:SetDesaturated(not active)
            btn._icon:SetAlpha(active and 1.0 or 0.4)
        end
    end
end

function Panel.UpdatePVPCatButtons()
    local R = Panel._refs
    if not R.pvpCatButtons then return end
    local PVP_CAT_ACCENTS = {
        quickmatch = { 0.00, 0.78, 1.00 },
        rated      = { 1.00, 0.60, 0.00 },
        premade    = { 0.55, 0.55, 0.55 },
        training   = { 0.35, 0.75, 0.40 },
    }
    local PVP_CAT_KEYS = { "quickmatch", "rated", "premade", "training" }
    for i, btn in ipairs(R.pvpCatButtons) do
        local key = PVP_CAT_KEYS[i]
        local active = Panel._state.pvpCategory == key
        if active then
            local ac = PVP_CAT_ACCENTS[key] or {0.5, 0.5, 0.5}
            btn._bg:SetColorTexture(ac[1] * 0.15, ac[2] * 0.15, ac[3] * 0.15, 0.6)
            btn._accent:SetColorTexture(ac[1], ac[2], ac[3])
            btn._accent:Show()
            btn._label:SetTextColor(1, 1, 1)
        else
            btn._bg:SetColorTexture(TC("glassBg"))
            btn._accent:Hide()
            btn._label:SetTextColor(TC("bodyText"))
        end
    end

    if R.pvpQueueBtn and R.pvpQueueBtn._label then
        local cat = Panel._state.pvpCategory
        if cat == "rated" then
            R.pvpQueueBtn._label:SetText("Queue Rated")
        elseif cat == "training" then
            R.pvpQueueBtn._label:SetText("Join Training")
        else
            R.pvpQueueBtn._label:SetText("Join Battle")
        end
    end
end

function Panel.UpdatePVPActivities()
    local R = Panel._refs
    if not R.pvpScrollChild then return end
    local cat = Panel._state.pvpCategory

    for _, card in ipairs(R.pvpActivityCards or {}) do
        card:Hide()
        card:SetParent(nil)
    end
    R.pvpActivityCards = {}

    local activities
    if cat == "quickmatch" then
        activities = PVP_QUICKMATCH
    elseif cat == "rated" then
        activities = PVP_RATED
    elseif cat == "training" then
        activities = PVP_TRAINING
    else
        local prompt = R.pvpScrollChild:CreateFontString(nil, "OVERLAY")
        TrySetFont(prompt, BODY_FONT, 11, "")
        prompt:SetPoint("TOP", R.pvpScrollChild, "TOP", 0, -40)
        prompt:SetTextColor(TC("mutedText"))
        prompt:SetText("Use the Premade Groups tab for PVP group listings.")
        R.pvpActivityCards[1] = prompt
        R.pvpScrollChild:SetHeight(100)
        return
    end

    local CARD_W = 280
    local CARD_H = 50
    local CARD_GAP = 6
    local COLS = 2
    local scrollW = R.pvpScrollChild:GetWidth()
    if scrollW and scrollW > 0 then
        CARD_W = math.floor((scrollW - CARD_GAP) / COLS)
    end

    for i, act in ipairs(activities) do
        local col = ((i - 1) % COLS)
        local row = math.floor((i - 1) / COLS)
        local xOff = col * (CARD_W + CARD_GAP)
        local yOff = -(row * (CARD_H + CARD_GAP))

        local card = CreateFrame("Button", nil, R.pvpScrollChild, "BackdropTemplate")
        card:SetSize(CARD_W, CARD_H)
        card:SetPoint("TOPLEFT", R.pvpScrollChild, "TOPLEFT", xOff, yOff)
        card:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        card:SetBackdropColor(0.06, 0.06, 0.08, 0.7)
        card:SetBackdropBorderColor(0.2, 0.2, 0.25, 0.4)

        local nameFs = card:CreateFontString(nil, "OVERLAY")
        TrySetFont(nameFs, TITLE_FONT, 10, "OUTLINE")
        nameFs:SetPoint("TOPLEFT", card, "TOPLEFT", 10, -8)
        nameFs:SetPoint("RIGHT", card, "RIGHT", -10, 0)
        nameFs:SetJustifyH("LEFT")
        nameFs:SetWordWrap(false)
        nameFs:SetTextColor(TC("bodyText"))

        local displayName = act.name
        if act.isBrawl then
            local brawlInfo = C_PvP and C_PvP.GetActiveBrawlInfo and SafeCall(C_PvP.GetActiveBrawlInfo)
            if brawlInfo and brawlInfo.name then
                displayName = "Brawl: " .. brawlInfo.name
            else
                displayName = "Brawl: Cooking: Impossible"
            end
        end
        nameFs:SetText(displayName)
        card._nameFs = nameFs

        local subFs = card:CreateFontString(nil, "OVERLAY")
        TrySetFont(subFs, BODY_FONT, 9, "")
        subFs:SetPoint("BOTTOMLEFT", card, "BOTTOMLEFT", 10, 6)
        subFs:SetTextColor(TC("mutedText"))

        if act.teamSize then
            subFs:SetText(act.teamSize)
        elseif act.isRandom then
            subFs:SetText("Random")
        elseif act.isSkirmish then
            subFs:SetText("Arena")
        elseif act.bracket then
            local rating = 0
            if GetPersonalRatedInfo then
                local ok2, r = pcall(GetPersonalRatedInfo, act.bracket)
                if ok2 and r then rating = r end
            end
            if rating > 0 then
                subFs:SetText("Rating: " .. rating)
                subFs:SetTextColor(1.0, 0.80, 0.20)
            else
                subFs:SetText("--")
            end
        elseif act.isBrawl then
            local lvl = UnitLevel("player") or 0
            if lvl < (act.minLevel or 90) then
                subFs:SetText("Unlocks at Level " .. (act.minLevel or 90))
                subFs:SetTextColor(1.0, 0.3, 0.3)
                card:SetAlpha(0.5)
            else
                subFs:SetText("Weekly Brawl")
            end
        else
            subFs:SetText("")
        end
        card._subFs = subFs
        card._activity = act

        card:SetScript("OnClick", function(self)
            for _, c in ipairs(R.pvpActivityCards) do
                if c.SetBackdropBorderColor then
                    c:SetBackdropBorderColor(0.2, 0.2, 0.25, 0.4)
                end
            end
            local ar, ag, ab = TC("accent")
            self:SetBackdropBorderColor(ar, ag, ab, 0.8)
            Panel._state.pvpSelectedActivity = self._activity
        end)
        card:SetScript("OnEnter", function(self)
            if Panel._state.pvpSelectedActivity ~= self._activity then
                self:SetBackdropBorderColor(0.3, 0.3, 0.4, 0.6)
            end
        end)
        card:SetScript("OnLeave", function(self)
            if Panel._state.pvpSelectedActivity ~= self._activity then
                self:SetBackdropBorderColor(0.2, 0.2, 0.25, 0.4)
            end
        end)

        R.pvpActivityCards[i] = card
    end

    local totalRows = math.ceil(#activities / COLS)
    R.pvpScrollChild:SetHeight(math.max(1, totalRows * (CARD_H + CARD_GAP)))
end

function Panel.UpdatePVPQueueStatus()
    local R = Panel._refs
    if not R.pvpQueueBar then return end

    if not GetBattlefieldStatus then
        R.pvpQueueBar:Hide()
        return
    end

    for i = 1, 3 do
        local ok, status, mapName = pcall(GetBattlefieldStatus, i)
        if ok and status == "queued" then
            local waitTime = 0
            if GetBattlefieldTimeWaited then
                local ok2, t = pcall(GetBattlefieldTimeWaited, i)
                if ok2 and t then waitTime = t / 1000 end
            end
            R.pvpQueueText:SetText("Queued: " .. (mapName or "PVP") .. " — " .. math.floor(waitTime / 60) .. "m")
            R.pvpQueueBar:Show()
            return
        elseif ok and status == "confirm" then
            R.pvpQueueText:SetText((mapName or "PVP") .. " — READY!")
            R.pvpQueueText:SetTextColor(1.0, 0.85, 0.0)
            R.pvpQueueBar:Show()
            return
        end
    end
    R.pvpQueueBar:Hide()
end

function Panel.UpdatePVPRoleCards()
    Panel.UpdateRoleCards(Panel._refs.pvpRoleCards, Panel._state.pvpRoles, { 0.75, 0.30, 0.30 })
end

function Panel.UpdatePremadeRoleCards()
    Panel.UpdateRoleCards(Panel._refs.premadeRoleCards, Panel._state.applyRole)
end

function Panel.UpdatePVPHero()
    local R = Panel._refs

    PopulatePlayerIdentity(R.pvpHeroPlayerName, R.pvpHeroClassIcon, R.pvpHeroSpecLine)

    if R.pvpSeasonValue then
        local totalWins, totalLosses = 0, 0
        if GetPersonalRatedInfo then
            for _, bracket in ipairs({1, 2, 3, 4, 7}) do
                local ok, rating, seasonPlayed, seasonWon = pcall(GetPersonalRatedInfo, bracket)
                if ok and seasonPlayed then
                    totalWins = totalWins + (seasonWon or 0)
                    totalLosses = totalLosses + ((seasonPlayed or 0) - (seasonWon or 0))
                end
            end
        end
        R.pvpSeasonValue:SetText("SEASON: " .. totalWins .. "W - " .. totalLosses .. "L")
    end

    if R.pvpHonorValue then
        local honorLevel = UnitHonorLevel and UnitHonorLevel("player") or 0
        R.pvpHonorValue:SetText(tostring(honorLevel))
    end

    if R.pvpRatingValue then
        local bestRating, bestName = 0, "--"
        local bracketNames = { [1] = "2v2", [2] = "3v3", [3] = "RBG", [4] = "Solo BG", [7] = "Solo Shuffle" }
        if GetPersonalRatedInfo then
            for bracket, bName in pairs(bracketNames) do
                local ok, rating = pcall(GetPersonalRatedInfo, bracket)
                if ok and rating and rating > bestRating then
                    bestRating = rating
                    bestName = bName .. " " .. rating
                end
            end
        end
        if bestRating > 0 then
            R.pvpRatingValue:SetText(bestName)
            R.pvpRatingValue:SetTextColor(1.0, 0.80, 0.20)
        else
            R.pvpRatingValue:SetText("--")
            R.pvpRatingValue:SetTextColor(TC("mutedText"))
        end
    end

    if R.pvpSeasonValue then
        local totalWins, totalLosses = 0, 0
        if GetPersonalRatedInfo then
            for _, bracket in ipairs({1, 2, 3, 4, 7}) do
                local ok, rating, seasonPlayed, seasonWon = pcall(GetPersonalRatedInfo, bracket)
                if ok and seasonPlayed then
                    totalWins = totalWins + (seasonWon or 0)
                    totalLosses = totalLosses + ((seasonPlayed or 0) - (seasonWon or 0))
                end
            end
        end
        R.pvpSeasonValue:SetText(totalWins .. "W - " .. totalLosses .. "L")
    end

    if R.pvpConqTrack and R.pvpConqFill then
        local earned, cap = 0, 1
        if C_CurrencyInfo and C_CurrencyInfo.GetCurrencyInfo then
            local ok, info = pcall(C_CurrencyInfo.GetCurrencyInfo, 1602)
            if ok and info then
                earned = info.quantity or 0
                cap = info.maxQuantity or 1
                if cap <= 0 then cap = 1 end
            end
        end
        local trackW = R.pvpConqTrack:GetWidth()
        if trackW and trackW > 0 then
            R.pvpConqFill:SetWidth(math.max(1, trackW * math.min(1, earned / cap)))
        end
    end

    Panel.UpdatePVPRoleCards()
end


function Panel.UpdateCatButtons()
    Panel.HideGroupDetail()
    local R = Panel._refs
    if not R.catButtons then return end
    local active = Panel._state.activeCategory
    for _, btn in ipairs(R.catButtons) do
        if btn._key == active then
            local catAc = CATEGORY_ACCENTS[btn._key] or {TC("accent")}
            btn._bg:SetTexture(W8)
            btn._bg:SetColorTexture(catAc[1], catAc[2], catAc[3], 0.08)
            btn._accentBar:Show()
            btn._label:SetTextColor(catAc[1], catAc[2], catAc[3])
        else
            local bgR, bgG, bgB, bgA = TC("glassBg")
            btn._bg:SetTexture(W8)
            if btn._bg.SetGradient and CreateColor then
                btn._bg:SetGradient("VERTICAL",
                    CreateColor(bgR, bgG, bgB, bgA),
                    CreateColor(bgR + 0.01, bgG + 0.01, bgB + 0.01, bgA))
            else
                btn._bg:SetColorTexture(TC("glassBg"))
            end
            btn._accentBar:Hide()
            btn._label:SetTextColor(TC("bodyText"))
        end
    end
    Panel.UpdatePremadeHero()
    Panel.UpdateGroupIntelSidebar()
end

-- Populate the dungeon-teleport dropdown from the live Mythic+ rotation and
-- each teleport's current state (ready / on cooldown / not discovered).
function Panel.RefreshTeleportDropdown()
    local R = Panel._refs
    local tpDrop = R.teleportDropdown
    if not tpDrop then return end
    -- Secure attributes can't change in combat; the panel is hidden in combat
    -- anyway, so just bail out defensively.
    if InCombatLockdown and InCombatLockdown() then return end

    local dungeons = {}
    if C_ChallengeMode and C_ChallengeMode.GetMapTable then
        local ok, maps = pcall(C_ChallengeMode.GetMapTable)
        if ok and type(maps) == "table" then
            for _, mapID in ipairs(maps) do
                local iok, name = pcall(C_ChallengeMode.GetMapUIInfo, mapID)
                if iok and type(name) == "string" and name ~= "" then
                    dungeons[#dungeons + 1] = { mapID = mapID, name = name }
                end
            end
        end
    end
    table.sort(dungeons, function(a, b) return a.name < b.name end)

    local rows = tpDrop._rows
    local rowH, headerH = tpDrop._rowH, tpDrop._headerH
    local shown = 0
    for _, d in ipairs(dungeons) do
        local row = rows[shown + 1]
        if not row then break end
        local spellID = ResolveTeleportSpell(d.mapID)
        local known = IsTeleportKnown(spellID)
        local ready = known and GetTeleportCooldown(spellID) <= 0

        row._mapID = d.mapID
        row._dungeonName = d.name
        row._spellID = spellID
        row._known = known
        row._ready = ready

        row._name:SetText(d.name)
        row._icon:SetTexture(GetTeleportTexture(spellID))
        if ready then
            row._name:SetTextColor(0.98, 0.98, 1.0)
            row._icon:SetDesaturated(false)
            row._icon:SetAlpha(1)
        else
            -- On cooldown OR not discovered → greyed out.
            row._name:SetTextColor(0.48, 0.48, 0.54)
            row._icon:SetDesaturated(true)
            row._icon:SetAlpha(0.5)
        end

        -- Only ready teleports are castable. The secure "spell" attribute is
        -- cast via CastSpellByName, so it needs the spell NAME (a numeric ID
        -- silently no-ops). Clear it otherwise so a greyed row does nothing.
        local castName = ready and GetTeleportName(spellID) or nil
        row._ready = (castName ~= nil)
        if castName then
            row:SetAttribute("type", "spell")
            row:SetAttribute("spell", castName)
        else
            row:SetAttribute("type", nil)
            row:SetAttribute("spell", nil)
            row:SetAttribute("macrotext", nil)
        end

        shown = shown + 1
        row:ClearAllPoints()
        row:SetPoint("TOPLEFT", tpDrop, "TOPLEFT", 4, -(headerH + (shown - 1) * rowH))
        row:SetWidth(tpDrop:GetWidth() - 8)
        row:Show()
    end
    for i = shown + 1, #rows do rows[i]:Hide() end

    tpDrop:SetHeight(headerH + math.max(shown, 1) * rowH + 8)
end

function Panel.UpdateFilterBarVisibility()
    local R = Panel._refs
    if not R.filterBar or not R.scrollArea then return end
    local cat = Panel._state.activeCategory
    local showMplus = (cat == "mythicplus")
    local showRaid  = (cat == "raids" or cat == "legacy")
    local showDelve = (cat == "delves")
    R.filterBar:SetShown(showMplus)
    if R.raidFilterBar then R.raidFilterBar:SetShown(showRaid) end
    if R.delveFilterBar then R.delveFilterBar:SetShown(showDelve) end

    local anchorBar = (showMplus and R.filterBar) or (showRaid and R.raidFilterBar)
        or (showDelve and R.delveFilterBar) or nil
    R.scrollArea:ClearAllPoints()
    if anchorBar then
        R.scrollArea:SetPoint("TOPLEFT", anchorBar, "BOTTOMLEFT", 0, 0)
        R.scrollArea:SetPoint("BOTTOMRIGHT", R.listingArea, "BOTTOMRIGHT", 0, 0)
    else
        R.scrollArea:SetPoint("TOPLEFT", R.listingArea, "TOPLEFT", 0, 0)
        R.scrollArea:SetPoint("BOTTOMRIGHT", R.listingArea, "BOTTOMRIGHT", 0, 0)
    end
end

function Panel.DoSearch(forceRefresh)
    local cat = nil
    for _, c in ipairs(CATEGORY_DEFS) do
        if c.key == Panel._state.activeCategory then cat = c; break end
    end
    if not cat then return end

    -- Delves' category id isn't reliably known until the LFG list loads, so
    -- resolve it at search time rather than trusting the load-time value.
    local categoryID = cat.categoryID
    if cat.isDelves then categoryID = ResolveDelvesCategoryID() end

    local R = Panel._refs
    if R.searchingLabel then R.searchingLabel:Show() end
    if R.emptyLabel then R.emptyLabel:Hide() end
    if R.retryBtn then R.retryBtn:Hide() end
    Panel._state.searchResults = {}
    Panel._state.scrollOffset = 0
    Panel._state._searchStartTime = GetTime()
    Panel._state._searchCategory = cat.key
    Panel.UpdateListingRows()

    if MidnightUI_Debug then
        MidnightUI_Debug("GroupFinder DoSearch cat=" .. tostring(cat.key) ..
            " categoryID=" .. tostring(categoryID) .. " force=" .. tostring(forceRefresh and true or false))
    end

    local lastCatID = Panel._state._lastSearchCategoryID
    local lastTime = Panel._state._lastSearchTime or 0
    local lastGotResults = Panel._state._lastSearchGotResults
    if not forceRefresh and lastCatID == categoryID and (GetTime() - lastTime) < 15 and lastGotResults then
        C_Timer.After(0, function() Panel.OnSearchResults() end)
        return
    end
    Panel._state._lastSearchCategoryID = categoryID
    Panel._state._lastSearchTime = GetTime()
    Panel._state._lastSearchGotResults = false

    if C_LFGList.Search then
        C_LFGList.Search(categoryID)
    end

    Panel._state._searchGeneration = (Panel._state._searchGeneration or 0) + 1
    local thisGeneration = Panel._state._searchGeneration
    C_Timer.After(8, function()
        if Panel._state._searchGeneration ~= thisGeneration then return end
        if R.searchingLabel and R.searchingLabel:IsShown() then
            R.searchingLabel:Hide()
            if #Panel._state.searchResults == 0 and R.emptyLabel then
                R.emptyLabel:SetText("No groups found.")
                R.emptyLabel:Show()
                if R.retryBtn then R.retryBtn:Show() end
            end
        end
    end)
end

-- Returns false when an explicit dungeon/activity name filter is active and
-- this listing doesn't match it. Used in two places: the normal filter pass
-- and the delisted-listing branch (which otherwise bypasses filtering and
-- would leak unrelated greyed-out dungeons into a filtered list).
local function PassesNameFilter(info, filters)
    -- The dungeon dropdown and search autocomplete are Mythic+-only inputs;
    -- never let them filter other categories (Delves, Raids, etc.).
    if Panel._state.activeCategory ~= "mythicplus" then return true end
    if filters.activityFull and filters.activityFull ~= "" then
        local actName = (ResolveActivityName(info) or ""):lower()
        if not actName:find(filters.activityFull:lower(), 1, true) then return false end
    end
    if filters.dungeon and filters.dungeon ~= "" then
        local brand = ResolveDungeonBrand(info)
        local dungName = (brand and brand.name or ""):lower()
        if not dungName:find(filters.dungeon:lower(), 1, true) then return false end
    end
    return true
end

-- Raid difficulty filter (Raids / Legacy categories only). Returns false when
-- a difficulty is selected and this listing isn't one of them. With no
-- difficulty selected, everything passes.
local function PassesRaidFilter(info)
    local cat = Panel._state.activeCategory
    if cat ~= "raids" and cat ~= "legacy" then return true end
    local rf = Panel._state.raidFilters
    if not rf then return true end

    -- Difficulty chips.
    local diffSet = rf.difficulty
    if diffSet and next(diffSet) then
        local diff = ResolveRaidDifficulty(info)
        if not (diff and diffSet[diff]) then return false end
    end

    -- Search text (typed keyword, or a raid picked from the dropdown).
    local q = rf.searchText
    if q and q ~= "" then
        local ql = q:lower()
        local actName = (ResolveActivityName(info) or ""):lower()
        local grpOk, grpName = pcall(function() return (info.name or ""):lower() end)
        if not (actName:find(ql, 1, true) or (grpOk and grpName:find(ql, 1, true))) then
            return false
        end
    end
    return true
end

-- Delves search filter (Delves category only). Matches the typed keyword (or
-- a delve picked from the dropdown) against the activity name and group title.
local function PassesDelveFilter(info)
    if Panel._state.activeCategory ~= "delves" then return true end
    local q = Panel._state.delveFilters and Panel._state.delveFilters.searchText
    if not q or q == "" then return true end
    local ql = q:lower()
    local actName = (ResolveActivityName(info) or ""):lower()
    if actName:find(ql, 1, true) then return true end
    local grpOk, grpName = pcall(function() return (info.name or ""):lower() end)
    if grpOk and grpName:find(ql, 1, true) then return true end
    return false
end

function Panel.OnSearchResults()
    local R = Panel._refs
    if R.searchingLabel then R.searchingLabel:Hide() end

    local results = {}
    local ok, numResults, resultList = pcall(function()
        return C_LFGList.GetSearchResults()
    end)

    if ok and resultList and #resultList > 0 then
        Panel._state._rawResultIDs = {}
        for _, rid in ipairs(resultList) do
            Panel._state._rawResultIDs[#Panel._state._rawResultIDs + 1] = rid
        end
    end
    if (not ok or not resultList or #resultList == 0) and Panel._state._rawResultIDs then
        resultList = Panel._state._rawResultIDs
        ok = true
    end

    if MidnightUI_Debug then
        MidnightUI_Debug("GroupFinder OnSearchResults cat=" .. tostring(Panel._state.activeCategory) ..
            " rawResults=" .. tostring((ok and resultList) and #resultList or 0))
    end

    -- If Blizzard's adopted search box has text, ask Blizzard's panel to
    -- run its privileged filter (which CAN read the protected listing
    -- names addons can't), then use the resulting filtered list. Falls
    -- back to the full server result list when the box is empty.
    -- When the player picked an activity from our autocomplete dropdown we
    -- filter addon-side by exact activity name (filters.activityFull) and
    -- skip Blizzard's privileged text filter — feeding it our programmatic
    -- (tainted) box text can make it return an empty list during M+ keys.
    -- The search box only exists on the Mythic+ tab, so only honour its text
    -- there. Without this gate, leftover box text would run Blizzard's filter
    -- against other categories (e.g. Delves) and wipe out every result.
    local blizzPanel = LFGListFrame and LFGListFrame.SearchPanel
    local blizzBox = blizzPanel and blizzPanel.SearchBox
    if blizzBox and blizzBox.GetText and not Panel._state._acActive
       and Panel._state.activeCategory == "mythicplus" then
        local searchText = blizzBox:GetText() or ""
        if searchText ~= "" then
            -- Make Blizzard's panel aware of the current category so its
            -- filter applies to the right activity set, then ask it to
            -- recompute panel.results from the latest server response.
            local catID
            for _, c in ipairs(CATEGORY_DEFS) do
                if c.key == Panel._state.activeCategory then catID = c.categoryID; break end
            end
            Panel._state._inBlizzFilter = true
            if catID and LFGListSearchPanel_SetCategory then
                pcall(LFGListSearchPanel_SetCategory, blizzPanel, catID)
            end
            if LFGListSearchPanel_UpdateResultList then
                pcall(LFGListSearchPanel_UpdateResultList, blizzPanel)
            end
            Panel._state._inBlizzFilter = false
            if type(blizzPanel.results) == "table" and #blizzPanel.results > 0 then
                resultList = blizzPanel.results
                ok = true
            elseif type(blizzPanel.results) == "table" and #blizzPanel.results == 0 then
                resultList = {}
                ok = true
            end
        end
    end


    if ok and resultList then
        local filters = Panel._state.filters
        for _, resultID in ipairs(resultList) do
            local infoOk, info = pcall(C_LFGList.GetSearchResultInfo, resultID)
            -- Blizzard's secret-value system can tag `isDelisted` as a
            -- secret boolean during Mythic+ keys — a direct `if
            -- info.isDelisted` throws. Read via pcall and treat an
            -- unreadable value as "not delisted" so the listing is
            -- still considered.
            local isDelistedOk, isDelisted = false, false
            if infoOk and info ~= nil then
                isDelistedOk, isDelisted = pcall(function() return info.isDelisted and true or false end)
            end
            if not infoOk or not info then
            elseif isDelistedOk and isDelisted then
                -- Delisted groups are normally kept (rendered greyed) so the
                -- list doesn't jump as groups expire — but still honour an
                -- active dungeon/activity filter so unrelated delisted groups
                -- don't leak in greyed-out.
                if PassesNameFilter(info, filters) and PassesRaidFilter(info) and PassesDelveFilter(info) then
                    info._resultID = resultID
                    results[#results + 1] = info
                end
            else
                local dominated = false

                -- Dungeon (dropdown) / exact-activity (search autocomplete,
                -- e.g. "Windrunner Spire (Mythic Keystone)") name filters.
                if not dominated and not PassesNameFilter(info, filters) then
                    dominated = true
                end

                -- Raid difficulty chips (Raids / Legacy categories only).
                if not dominated and not PassesRaidFilter(info) then
                    dominated = true
                end

                -- Delves search (Delves category only).
                if not dominated and not PassesDelveFilter(info) then
                    dominated = true
                end

                -- Role / composition / MPI filters are Mythic+-only inputs;
                -- gate them so stale values can't filter Raids or Delves.
                local isMplus = (Panel._state.activeCategory == "mythicplus")
                local needsMemberScan = isMplus and ((filters.role and next(filters.role))
                    or filters.hasTank or filters.hasHealer or filters.noSameClass)
                local filled, classCounts
                if not dominated and needsMemberScan then
                    filled = { TANK = 0, HEALER = 0, DAMAGER = 0 }
                    classCounts = {}
                    local nM = info.numMembers or 0
                    for mi = 1, nM do
                        local rOk, mRole, mClass = pcall(C_LFGList.GetSearchResultMemberInfo, resultID, mi)
                        if rOk then
                            if mRole then filled[mRole] = (filled[mRole] or 0) + 1 end
                            if mClass then classCounts[mClass] = (classCounts[mClass] or 0) + 1 end
                        end
                    end
                end

                if not dominated and isMplus and filters.role and next(filters.role) then
                    local wantTank   = filters.role.TANK    or 0
                    local wantHeal   = filters.role.HEALER  or 0
                    local wantDps    = filters.role.DAMAGER or 0
                    local anyOpen = false
                    if wantTank > 0 and (1 - (filled.TANK   or 0)) >= wantTank then anyOpen = true end
                    if wantHeal > 0 and (1 - (filled.HEALER or 0)) >= wantHeal then anyOpen = true end
                    if wantDps  > 0 and (3 - (filled.DAMAGER or 0)) >= wantDps  then anyOpen = true end
                    if not anyOpen then dominated = true end
                end

                if not dominated and isMplus and filters.hasTank and (filled.TANK or 0) < 1 then
                    dominated = true
                end
                if not dominated and isMplus and filters.hasHealer and (filled.HEALER or 0) < 1 then
                    dominated = true
                end
                if not dominated and isMplus and filters.noSameClass and Panel._playerClass then
                    if (classCounts[Panel._playerClass] or 0) > 0 then
                        dominated = true
                    end
                end

                if not dominated and isMplus and filters.minMPI and filters.minMPI > 0 then
                    if info.leaderName then
                        local profile = GetCachedMPIProfile(info.leaderName)
                        if profile and (profile.avgMPI or 0) < filters.minMPI then
                            dominated = true
                        end
                    end
                end



                if not dominated then
                    local activeCat = Panel._state.activeCategory
                    if activeCat == "raids" or activeCat == "legacy" then
                        local isCurrent = nil
                        local actIDs = info.activityIDs

                        local resolvedID = nil
                        if type(actIDs) == "number" and actIDs > 0 then
                            resolvedID = actIDs
                        elseif type(actIDs) == "table" and #actIDs > 0 then
                            resolvedID = actIDs[1]
                        end

                        if resolvedID and C_LFGList.GetActivityInfoTable then
                            local aOk, actInfo = pcall(C_LFGList.GetActivityInfoTable, resolvedID)
                            if aOk and actInfo then
                                if actInfo.isCurrentRaidActivity == true then
                                    isCurrent = true
                                elseif actInfo.isCurrentRaidActivity == false then
                                    isCurrent = false
                                end
                            end
                        end

                        if isCurrent ~= nil then
                            if activeCat == "raids" and not isCurrent then dominated = true end
                            if activeCat == "legacy" and isCurrent then dominated = true end
                        end
                    end
                end

                if not dominated then
                    info._resultID = resultID
                    results[#results + 1] = info
                else
                end
            end
        end
    end

    Panel._state.searchResults = results
    Panel._state.scrollOffset = 0
    Panel._state._lastSearchGotResults = true

    if MidnightUI_Debug then
        MidnightUI_Debug("GroupFinder OnSearchResults cat=" .. tostring(Panel._state.activeCategory) ..
            " finalResults=" .. tostring(#results))
    end

    if #results == 0 then
        if R.emptyLabel then
            R.emptyLabel:SetText("No groups found.")
            R.emptyLabel:Show()
        end
        if R.retryBtn then R.retryBtn:Show() end
    else
        if R.emptyLabel then R.emptyLabel:Hide() end
        if R.retryBtn then R.retryBtn:Hide() end
    end

    Panel.UpdateListingRows()
    Panel.UpdateScrollBar()
    if Panel._state.activeTab == "premade" then
        Panel.UpdatePremadeHero()
        Panel.UpdateGroupIntelSidebar()
    end

end

function Panel.GetPendingApplications()
    local apps = {}
    if not C_LFGList or not C_LFGList.GetApplications then return apps end
    local ok, appList = pcall(C_LFGList.GetApplications)
    if not ok or not appList then return apps end
    for _, resultID in ipairs(appList) do
        local aOk, _, appStatus = pcall(C_LFGList.GetApplicationInfo, resultID)
        local hideStatuses = { none=true, cancelled=true, timedout=true, declined=true,
            declined_full=true, declined_delisted=true, failed=true }
        if aOk and appStatus and not hideStatuses[appStatus] then
            local iOk, info = pcall(C_LFGList.GetSearchResultInfo, resultID)
            local groupName = (iOk and info and info.name) or "Unknown Group"
            apps[#apps + 1] = {
                name = groupName,
                status = appStatus,
                resultID = resultID,
            }
        end
    end
    return apps
end

function Panel.UpdateMPIScoreCard(resultID)
    local R = Panel._refs
    if not R.intel_mpiPanel then return end

    local function ShowEmpty()
        if R.intel_mpiEmpty then R.intel_mpiEmpty:Show() end
        if R.mpi_classIcon then R.mpi_classIcon:Hide() end
        if R.mpi_classInitial then R.mpi_classInitial:Hide() end
        if R.mpi_cardName then R.mpi_cardName:Hide() end
        if R.mpi_cardRealm then R.mpi_cardRealm:Hide() end
        if R.mpi_cardClass then R.mpi_cardClass:Hide() end
        if R.mpi_cardRole then R.mpi_cardRole:Hide() end
        if R.mpi_cardScore then R.mpi_cardScore:Hide() end
        if R.mpi_tierBadge then R.mpi_tierBadge:Hide() end
        if R.mpi_trendIcon then R.mpi_trendIcon:Hide() end
        if R.mpi_cardDiv then R.mpi_cardDiv:Hide() end
        if R.mpi_badgeDiv then R.mpi_badgeDiv:Hide() end
        if R.mpi_badgeLabel then R.mpi_badgeLabel:Hide() end
        if R.mpi_dimRows then
            for _, dr in ipairs(R.mpi_dimRows) do
                dr.label:Hide(); dr.value:Hide(); dr.track:Hide(); dr.fill:Hide()
            end
        end
        if R.mpi_badgeSlots then
            for _, s in ipairs(R.mpi_badgeSlots) do s:Hide() end
        end
    end

    local function ShowContent()
        if R.intel_mpiEmpty then R.intel_mpiEmpty:Hide() end
        if R.mpi_classIcon then R.mpi_classIcon:Show() end
        if R.mpi_classInitial then R.mpi_classInitial:Show() end
        if R.mpi_cardName then R.mpi_cardName:Show() end
        if R.mpi_cardRealm then R.mpi_cardRealm:Show() end
        if R.mpi_cardClass then R.mpi_cardClass:Show() end
        if R.mpi_cardRole then R.mpi_cardRole:Show() end
        if R.mpi_cardScore then R.mpi_cardScore:Show() end
        if R.mpi_tierBadge then R.mpi_tierBadge:Show() end
        if R.mpi_trendIcon then R.mpi_trendIcon:Show() end
        if R.mpi_cardDiv then R.mpi_cardDiv:Show() end
        if R.mpi_badgeDiv then R.mpi_badgeDiv:Show() end
        if R.mpi_badgeLabel then R.mpi_badgeLabel:Show() end
        if R.mpi_dimRows then
            for _, dr in ipairs(R.mpi_dimRows) do
                dr.label:Show(); dr.value:Show(); dr.track:Show(); dr.fill:Show()
            end
        end
    end

    if not resultID then ShowEmpty(); return end

    local ok, info = pcall(C_LFGList.GetSearchResultInfo, resultID)
    if not ok or not info then ShowEmpty(); return end

    local mOk, mRole, mClass, mClassLocal, mSpecLocal
    if C_LFGList.GetSearchResultMemberInfo then
        mOk, mRole, mClass, mClassLocal, mSpecLocal = pcall(C_LFGList.GetSearchResultMemberInfo, resultID, 1)
    end
    if not mOk then ShowEmpty(); return end
    local mName = info.leaderName or ""

    local shortName = mName or ""
    local realm = ""
    if mName and mName:find("-") then
        shortName = mName:match("^([^%-]+)")
        realm = mName:match("%-(.+)$") or ""
    end

    if shortName == "" then
        shortName = mSpecLocal or mClassLocal or mClass or ""
    end

    local cc = mClass and RAID_CLASS_COLORS and RAID_CLASS_COLORS[mClass]
    local cr, cg, cb = 0.6, 0.6, 0.6
    if cc then cr, cg, cb = cc.r, cc.g, cc.b end

    local profile = nil
    if _G.MidnightPI and _G.MidnightPI.GetProfileByName and mName and mName ~= "" then
        profile = _G.MidnightPI.GetProfileByName(mName)
    end

    ShowContent()
    local className = mClassLocal or mClass or ""
    local specLabel = mSpecLocal or ""
    local classLine = (specLabel ~= "" and specLabel ~= className) and (specLabel .. " " .. className) or className
    R.mpi_classIcon:SetColorTexture(cr, cg, cb, 0.8)
    R.mpi_classInitial:SetText(className:sub(1, 1) or "?")
    R.mpi_cardName:SetText(shortName)
    R.mpi_cardName:SetTextColor(cr, cg, cb)
    R.mpi_cardRealm:SetText(realm)
    R.mpi_cardClass:SetText(classLine)
    R.mpi_cardClass:SetTextColor(cr, cg, cb)
    local roleLabel = mRole == "TANK" and "TANK" or mRole == "HEALER" and "HEALER" or "DPS"
    R.mpi_cardRole:SetText(roleLabel)

    if not profile or not profile.avgMPI or profile.avgMPI == 0 then
        R.mpi_cardScore:SetText("--")
        R.mpi_cardScore:SetTextColor(0.35, 0.35, 0.40)
        R.mpi_tierText:SetText("?")
        R.mpi_tierText:SetTextColor(0.35, 0.35, 0.40)
        R.mpi_tierBadge:SetBackdropBorderColor(0.25, 0.25, 0.30, 0.6)
        R.mpi_trendIcon:SetText("")

        if R.mpi_dimRows then
            for _, dr in ipairs(R.mpi_dimRows) do
                dr.label:Hide(); dr.value:Hide(); dr.track:Hide(); dr.fill:Hide()
            end
        end
        if R.mpi_badgeDiv then R.mpi_badgeDiv:Hide() end
        if R.mpi_badgeLabel then R.mpi_badgeLabel:Hide() end
        if R.mpi_badgeSlots then
            for _, s in ipairs(R.mpi_badgeSlots) do s:Hide() end
        end
        if R.mpi_noDataMsg then R.mpi_noDataMsg:Show() end
        return
    end

    if R.mpi_noDataMsg then R.mpi_noDataMsg:Hide() end

    local score = profile.avgMPI
    local tierLabel, tr, tg, tb = "D", 1.0, 0.35, 0.35
    if _G.MidnightPI and _G.MidnightPI.GetScoreTier then
        tierLabel, tr, tg, tb = _G.MidnightPI.GetScoreTier(score)
    end

    R.mpi_cardScore:SetText(tostring(score))
    R.mpi_cardScore:SetTextColor(tr, tg, tb)
    R.mpi_tierText:SetText(tierLabel)
    R.mpi_tierText:SetTextColor(tr, tg, tb)
    R.mpi_tierBadge:SetBackdropBorderColor(tr, tg, tb, 0.6)

    if profile.trend == "up" then
        R.mpi_trendIcon:SetText("|cff59e073\226\150\178 Trending Up|r")
    elseif profile.trend == "down" then
        R.mpi_trendIcon:SetText("|cffff5959\226\150\188 Trending Down|r")
    else
        R.mpi_trendIcon:SetText("")
    end

    local DIM_KEYS = { "awarenessScore", "survivalScore", "outputScore", "utilityScore", "consistencyScore" }
    for i, key in ipairs(DIM_KEYS) do
        local dr = R.mpi_dimRows[i]
        if dr then
            local val = profile[key] or 0
            val = math.floor(val + 0.5)
            local _, dr_r, dg, db2 = "D", 1.0, 0.35, 0.35
            if _G.MidnightPI and _G.MidnightPI.GetScoreTier then
                _, dr_r, dg, db2 = _G.MidnightPI.GetScoreTier(val)
            end
            dr.label:Show(); dr.value:Show(); dr.track:Show(); dr.fill:Show()
            dr.value:SetText(tostring(val))
            dr.value:SetTextColor(dr_r, dg, db2)
            dr.fill:SetColorTexture(dr_r, dg, db2, 0.8)
            local trackW = dr.track:GetWidth()
            if trackW and trackW > 0 then
                dr.fill:SetWidth(math.max(1, trackW * val / 100))
            else
                dr.fill:SetWidth(math.max(1, val))
            end
        end
    end

    if R.mpi_badgeDiv then R.mpi_badgeDiv:Show() end
    if R.mpi_badgeLabel then R.mpi_badgeLabel:Show() end
    local badges = profile.badges or {}
    for bi = 1, 6 do
        local slot = R.mpi_badgeSlots[bi]
        if slot then
            if badges[bi] then
                slot:SetText(badges[bi])
                slot:Show()
            else
                slot:Hide()
            end
        end
    end
end

function Panel.UpdateGroupIntelSidebar()
    local R = Panel._refs
    if not R.playerSidebar then return end

    if R.intel_appRows then
        local apps = Panel.GetPendingApplications()
        local hasApps = #apps > 0

        if R.intel_appEmpty then
            R.intel_appEmpty:SetShown(not hasApps)
        end

        if R.intel_appHeader then
            R.intel_appHeader:SetText("MY APPLICATIONS (" .. #apps .. "/5)")
        end

        local statusInfo = {
            applied           = { color = { TC("accent") },            label = "Pending" },
            invited           = { color = { 0.35, 0.88, 0.62 },       label = "Invited!" },
            declined          = { color = { 1.0, 0.4, 0.4 },          label = "Declined" },
            declined_full     = { color = { 1.0, 0.4, 0.4 },          label = "Group Full" },
            declined_delisted = { color = { 0.6, 0.6, 0.6 },          label = "Delisted" },
            timedout          = { color = { 0.6, 0.6, 0.6 },          label = "Expired" },
            cancelled         = { color = { 0.6, 0.6, 0.6 },          label = "Cancelled" },
            failed            = { color = { 1.0, 0.4, 0.4 },          label = "Failed" },
        }

        local visibleCount = 0
        for i, card in ipairs(R.intel_appRows) do
            local app = apps[i]
            if app then
                card._name:SetText(app.name)
                local si = statusInfo[app.status]
                local statusLabel = si and si.label or (app.status:sub(1,1):upper() .. app.status:sub(2)):gsub("_", " ")
                local statusColor = si and si.color or { TC("mutedText") }
                card._status:SetText(statusLabel)
                card._status:SetTextColor(statusColor[1], statusColor[2], statusColor[3])
                card._statusBar:SetColorTexture(statusColor[1], statusColor[2], statusColor[3], 0.8)
                card._cancelBtn._resultID = app.resultID
                local canCancel = (app.status == "applied" or app.status == "invited")
                card._cancelBtn:SetShown(canCancel)
                card:Show()
                visibleCount = visibleCount + 1
            else
                card:Hide()
            end
        end

        if R.intel_appFlyout then
            local APP_CARD_H = 40
            local APP_CARD_GAP = 4
            if hasApps then
                local flyH = 28 + visibleCount * (APP_CARD_H + APP_CARD_GAP) + 8
                R.intel_appFlyout:SetHeight(math.max(60, flyH))
            else
                R.intel_appFlyout:SetHeight(60)
            end
        end
    end

    if _G.MidnightPI and _G.MidnightPI.UpdateSidebarTeamMPI then _G.MidnightPI.UpdateSidebarTeamMPI(R.playerSidebar, nil) end
end

function Panel.UpdatePlayerSidebar()
    Panel.UpdateGroupIntelSidebar()
end

local function ComputeVisibleRowCap(R)
    local rowPitch = CFG.ROW_H + 4
    local h = (R.scrollArea and R.scrollArea:GetHeight()) or 0
    if h <= 0 then return CFG.ROW_POOL_SIZE end
    local cap = math.max(1, math.floor(h / rowPitch))
    return math.min(cap, CFG.ROW_POOL_SIZE)
end

function Panel.UpdateScrollBar()
    local R = Panel._refs
    if not R.scrollTrack or not R.scrollThumb then return end
    local totalResults = #(Panel._state.searchResults or {})
    local visibleCap = ComputeVisibleRowCap(R)
    if totalResults <= visibleCap then
        R.scrollTrack:Hide()
        R.scrollThumb:Hide()
        return
    end
    R.scrollTrack:Show()
    R.scrollThumb:Show()
    local trackH = R.scrollTrack:GetHeight()
    if trackH <= 0 then return end
    local maxOffset = totalResults - visibleCap
    local thumbH = math.max(20, trackH * (visibleCap / totalResults))
    local thumbOffset = (Panel._state.scrollOffset / maxOffset) * (trackH - thumbH)
    R.scrollThumb:SetHeight(thumbH)
    R.scrollThumb:ClearAllPoints()
    R.scrollThumb:SetPoint("TOPRIGHT", R.scrollTrack, "TOPRIGHT", 0, -thumbOffset)
end

function Panel.UpdateListingRows()
    local R = Panel._refs
    if not R.rowPool then return end
    local results = Panel._state.searchResults
    local offset = Panel._state.scrollOffset

    for i = 1, CFG.ROW_POOL_SIZE do
        local row = R.rowPool[i]
        local dataIdx = offset + i
        if dataIdx <= #results then
            local info = results[dataIdx]
            local cachedRID = info._resultID
            local liveOk, liveInfo = pcall(C_LFGList.GetSearchResultInfo, cachedRID)
            if liveOk and liveInfo then
                liveInfo._resultID = cachedRID
                info = liveInfo
                results[dataIdx] = liveInfo
            end
            local rawDelisted = info.isDelisted
            local isDelisted = false
            if _G.issecretvalue then
                local okSecret, isSecret = pcall(_G.issecretvalue, rawDelisted)
                if not (okSecret and isSecret == true) then
                    isDelisted = rawDelisted and true or false
                end
            else
                isDelisted = rawDelisted and true or false
            end
            row._resultID = info._resultID
            row._isDelisted = isDelisted

            local rawComment = info.comment or ""
            local hasDesc = rawComment ~= ""
            if row._descText and row._descCard then
                if hasDesc then
                    row._descText:SetText(rawComment)
                    row._descCard:Show()
                    if row._watermark then row._watermark:Hide() end
                else
                    row._descCard:Hide()
                    if row._watermark then row._watermark:Show() end
                end
            end

            local brand = ResolveDungeonBrand(info)

            local isOdd = (dataIdx % 2) == 1
            local zR, zG, zB
            if isOdd then
                zR, zG, zB = 0.35, 0.50, 0.62
            else
                zR, zG, zB = 0.55, 0.48, 0.32
            end

            if row._banner then
                if row._banner.SetGradient and CreateColor then
                    pcall(row._banner.SetGradient, row._banner, "HORIZONTAL",
                        CreateColor(zR, zG, zB, 0.14),
                        CreateColor(zR, zG, zB, 0.02))
                else
                    row._banner:SetColorTexture(zR, zG, zB, 0.06)
                end
            end
            if row._accentBar then
                row._accentBar:SetColorTexture(zR, zG, zB, 0.85)
                row._accentBar:SetAlpha(1)
            end
            if row._watermark then
                row._watermark:SetText(brand.abbr or "")
                row._watermark:SetTextColor(0.55, 0.55, 0.60, 0.10)
            end
            if row._dungName then
                row._dungName:SetText(brand.name or "")
            end

            local mpiProfile = info.leaderName and GetCachedMPIProfile(info.leaderName)
            if mpiProfile and mpiProfile.avgMPI then
                local sr, sg, sb = GetMPIScoreColor(mpiProfile.avgMPI)
                row._mpiBadge._bg:SetColorTexture(sr, sg, sb, 0.35)
                row._mpiBadge._text:SetText(tostring(mpiProfile.avgMPI))
            else
                row._mpiBadge._bg:SetColorTexture(0.5, 0.5, 0.55, 0.18)
                row._mpiBadge._text:SetText("--")
            end

            local actName = ResolveActivityName(info)
            local displayTitle = (info.name and info.name ~= "" and info.name) or actName
            row._activityName:SetText(displayTitle)


            row._tooltipTitle = info.name or ""
            row._tooltipComment = info.comment or ""

            local ok2, mRole, mClass = pcall(C_LFGList.GetSearchResultMemberInfo, info._resultID, 1)
            if row._leaderClassIcon then
                if ok2 and mClass and CLASS_ICON_TCOORDS then
                    local coords = CLASS_ICON_TCOORDS[mClass]
                    row._leaderClassIcon:SetTexture("Interface\\GLUES\\CHARACTERCREATE\\UI-CharacterCreate-Classes")
                    if coords then
                        row._leaderClassIcon:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
                    end
                    row._leaderClassIcon:Show()
                else
                    row._leaderClassIcon:Hide()
                end
            end

            if row._leaderName then
                if info.leaderName then
                    local shortName = info.leaderName:match("^([^-]+)") or info.leaderName
                    row._leaderName:SetText(shortName)
                    local leaderCC = (ok2 and mClass) and RAID_CLASS_COLORS and RAID_CLASS_COLORS[mClass] or nil
                    if leaderCC then
                        row._leaderName:SetTextColor(leaderCC.r, leaderCC.g, leaderCC.b)
                    else
                        row._leaderName:SetTextColor(TC("mutedText"))
                    end
                else
                    row._leaderName:SetText("")
                end
            end

            local maxMem = info.maxMembers
            if not maxMem or maxMem == 0 then
                local actIDs = info.activityIDs
                local resolvedID = type(actIDs) == "number" and actIDs or (type(actIDs) == "table" and actIDs[1])
                if resolvedID and C_LFGList.GetActivityInfoTable then
                    local aOk, aInfo = pcall(C_LFGList.GetActivityInfoTable, resolvedID)
                    if aOk and aInfo and aInfo.maxNumPlayers and aInfo.maxNumPlayers > 0 then
                        maxMem = aInfo.maxNumPlayers
                    end
                end
            end
            if not maxMem or maxMem <= 0 then
                if Panel._state.activeCategory == "custom" then maxMem = nil else maxMem = 5 end
            end
            if maxMem then
                row._countText:SetText((info.numMembers or 0) .. "/" .. maxMem)
            else
                row._countText:SetText(tostring(info.numMembers or 0))
            end

            if row._listedText then
                row._listedText:SetText(FormatTimeAgo(info.age))
            end

            if row._roleIcons then
                local roleCountData = { TANK = 0, HEALER = 0, DAMAGER = 0 }
                local numMem = info.numMembers or 0
                for mi = 1, numMem do
                    local mOk, mR = pcall(C_LFGList.GetSearchResultMemberInfo, info._resultID, mi)
                    if mOk and mR then roleCountData[mR] = (roleCountData[mR] or 0) + 1 end
                end
                local ROLE_KEYS = { "TANK", "HEALER", "DAMAGER" }
                for ri, iconFrame in ipairs(row._roleIcons) do
                    local rKey = ROLE_KEYS[ri]
                    local count = roleCountData[rKey] or 0
                    local tex = iconFrame._tex
                    if count > 0 then
                        if tex then tex:SetDesaturated(false); tex:SetAlpha(1.0) end
                    else
                        if tex then tex:SetDesaturated(true); tex:SetAlpha(0.3) end
                    end
                    if row._roleBubbles and row._roleBubbles[ri] then
                        local bubble = row._roleBubbles[ri]
                        if count > 0 then
                            bubble._text:SetText(tostring(count))
                            bubble:Show()
                        else
                            bubble:Hide()
                        end
                    end
                end

            end

            row._applyBtn._resultID = info._resultID
            local appOk, _, appStatus = pcall(C_LFGList.GetApplicationInfo, info._resultID)
            local disableApply = false
            local isCancellable = false
            local applyLabelText = "Apply"
            if appOk and appStatus and appStatus ~= "none" then
                if appStatus == "applied" then
                    applyLabelText = "Cancel"
                    isCancellable = true
                elseif appStatus == "invited" then applyLabelText = "Invited"; disableApply = true
                elseif appStatus == "declined" or appStatus == "declined_full" or appStatus == "declined_delisted" then applyLabelText = "Declined"; disableApply = true
                elseif appStatus == "cancelled" then applyLabelText = "Cancelled"; disableApply = true
                elseif appStatus == "timedout" then applyLabelText = "Expired"; disableApply = true
                elseif appStatus == "failed" then applyLabelText = "Failed"; disableApply = true
                else disableApply = true
                end
            end
            if isDelisted then
                applyLabelText = "Delisted"
                disableApply = true
                isCancellable = false
            end
            -- Block Apply (but never Cancel) when the player has no role
            -- selected. Re-label the button so it's clear what's missing.
            local roles = Panel._state.applyRole or {}
            local anyRole = roles.tank or roles.healer or roles.dps
            if not isCancellable and not anyRole and applyLabelText == "Apply" then
                disableApply = true
                applyLabelText = "Select Role"
            end
            if row._applyBtn._label then row._applyBtn._label:SetText(applyLabelText) end
            row._applyBtn._isCancelMode = isCancellable
            row._applyBtn:SetAlpha(disableApply and 0.35 or 1.0)
            row._applyBtn:EnableMouse(not disableApply)
            row:SetAlpha(isDelisted and 0.45 or 1.0)
            row:EnableMouse(not isDelisted)

            if _G.MidnightPI and _G.MidnightPI.UpdateListingRowMPI then _G.MidnightPI.UpdateListingRowMPI(row, info) end

            row:Show()
        else
            row:Hide()
        end
    end
end

function Panel.ShowGroupDetail(resultID)
    local R = Panel._refs
    if not R.detailPanel then return end

    Panel._state.detailOpen = true

    Panel.UpdateMPIScoreCard(resultID)

    local ok, info = pcall(C_LFGList.GetSearchResultInfo, resultID)
    if not ok or not info then return end

    R.detail_groupName:SetText(info.name or "")
    local hasComment = info.comment and info.comment ~= ""
    if hasComment then
        R.detail_comment:SetText(info.comment)
        R.detail_comment:Show()
        R.detail_groupName:ClearAllPoints()
        R.detail_groupName:SetPoint("TOPLEFT", R.detail_groupName:GetParent(), "TOPLEFT", 0, -4)
        R.detail_groupName:SetPoint("RIGHT", R.detail_groupName:GetParent(), "RIGHT", 0, 0)
    else
        R.detail_comment:SetText("")
        R.detail_comment:Hide()
        R.detail_groupName:ClearAllPoints()
        R.detail_groupName:SetPoint("LEFT", R.detail_groupName:GetParent(), "LEFT", 0, 0)
        R.detail_groupName:SetPoint("RIGHT", R.detail_groupName:GetParent(), "RIGHT", 0, 0)
    end

    if R.detail_leaderPill then
        local leaderStr = info.leaderName or ""
        if leaderStr == "" then
            local mOk, _, mClass, mClassLocal = pcall(C_LFGList.GetSearchResultMemberInfo, resultID, 1)
            if mOk and mClassLocal then leaderStr = mClassLocal end
        end
        R.detail_leaderPill._value:SetText(leaderStr)
        local pillW = 3 + 5 + (R.detail_leaderPill._value:GetStringWidth() or 40) + 50
        R.detail_leaderPill:SetWidth(math.max(80, math.min(200, pillW)))
    end

    if R.detail_listedPill then
        local listedStr = ""
        if info.age then
            local mins = math.floor(info.age / 60)
            listedStr = (mins < 1) and "just now" or (mins .. " min ago")
        end
        R.detail_listedPill._value:SetText(listedStr)
        local pillW = 3 + 5 + (R.detail_listedPill._value:GetStringWidth() or 40) + 50
        R.detail_listedPill:SetWidth(math.max(80, math.min(160, pillW)))
    end

    if R.detail_voicePill then
        if info.voiceChat and info.voiceChat ~= "" then
            R.detail_voicePill._value:SetText(info.voiceChat)
            local pillW = 3 + 5 + (R.detail_voicePill._value:GetStringWidth() or 30) + 50
            R.detail_voicePill:SetWidth(math.max(70, math.min(160, pillW)))
            R.detail_voicePill:Show()
        else
            R.detail_voicePill:Hide()
        end
    end

    local numMembers = info.numMembers or 0
    Panel._state.detailMemberCount = numMembers
    Panel._state.detailMemberScrollOffset = 0
    Panel._state._detailMemberResultID = resultID
    Panel._state._detailMemberInfo = info

    if R.detailMembers then
        local poolSize = #R.detailMembers
        for i = 1, poolSize do
            local memberRow = R.detailMembers[i]
            local dataIdx = i
            if dataIdx <= numMembers then
                local ok2, role, class, classLocal, specLocal = pcall(C_LFGList.GetSearchResultMemberInfo, resultID, dataIdx)
                if ok2 then
                    local roleLabel = role == "TANK" and "Tank" or role == "HEALER" and "Healer" or "DPS"
                    local memberName = ""
                    if specLocal and specLocal ~= "" and classLocal then
                        memberName = specLocal .. " " .. classLocal
                    elseif classLocal then
                        memberName = classLocal
                    elseif specLocal then
                        memberName = specLocal
                    end
                    memberRow._nameText:SetText(memberName)

                    local cc = class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[class]
                    if cc then
                        memberRow._nameText:SetTextColor(cc.r, cc.g, cc.b)
                    else
                        memberRow._nameText:SetTextColor(TC("bodyText"))
                    end

                    memberRow._roleText:SetText("(" .. roleLabel .. ")")

                    local memberMPI = nil
                    if dataIdx == 1 and info.leaderName then
                        local prof = GetCachedMPIProfile(info.leaderName)
                        if prof then memberMPI = prof.avgMPI end
                    end
                    if memberMPI then
                        local sr, sg, sb = GetMPIScoreColor(memberMPI)
                        memberRow._mpiBg:SetColorTexture(sr, sg, sb, 0.3)
                        memberRow._mpiText:SetText(tostring(memberMPI))
                    else
                        memberRow._mpiBg:SetColorTexture(0.5, 0.5, 0.5, 0.3)
                        memberRow._mpiText:SetText("--")
                    end

                    memberRow._bestText:SetText("")

                    memberRow:Show()
                else
                    memberRow:Hide()
                end
            else
                memberRow:Hide()
            end
        end
    end

    local MEMBER_ROW_H = 28
    local memberListH = math.min(numMembers, 5) * MEMBER_ROW_H
    if R.detailMemberScroll then
        R.detailMemberScroll:SetHeight(math.max(MEMBER_ROW_H, memberListH))
    end
    local panelH = 96 + math.max(MEMBER_ROW_H, memberListH) + 44
    R.detailPanel:SetHeight(math.min(CFG.DETAIL_PANEL_H, panelH))

    R.detailPanel._resultID = resultID
    if R.detail_applyBtn then
        local dAppOk, _, dAppStatus = pcall(C_LFGList.GetApplicationInfo, resultID)
        local dDisable = false
        local dIsCancelMode = false
        local dLabel = "Apply"
        if dAppOk and dAppStatus and dAppStatus ~= "none" then
            if dAppStatus == "applied" then
                dLabel = "Cancel"
                dIsCancelMode = true
            elseif dAppStatus == "invited" then dLabel = "Invited"; dDisable = true
            elseif dAppStatus == "declined" or dAppStatus == "declined_full" or dAppStatus == "declined_delisted" then dLabel = "Declined"; dDisable = true
            elseif dAppStatus == "cancelled" then dLabel = "Cancelled"; dDisable = true
            elseif dAppStatus == "timedout" then dLabel = "Expired"; dDisable = true
            else dDisable = true
            end
        end
        if info.isDelisted then
            dLabel = "Delisted"
            dDisable = true
            dIsCancelMode = false
        end
        local dRoles = Panel._state.applyRole or {}
        local dAnyRole = dRoles.tank or dRoles.healer or dRoles.dps
        if not dIsCancelMode and not dAnyRole and dLabel == "Apply" then
            dDisable = true
            dLabel = "Select Role"
        end
        if R.detail_applyBtn._label then R.detail_applyBtn._label:SetText(dLabel) end
        R.detail_applyBtn._isCancelMode = dIsCancelMode
        if dDisable then
            R.detail_applyBtn:SetAlpha(0.35)
            R.detail_applyBtn:EnableMouse(false)
        else
            R.detail_applyBtn:SetAlpha(1.0)
            R.detail_applyBtn:EnableMouse(true)
        end
    end
    R.detailPanel:Show()
end

function Panel.HideGroupDetail()
    local R = Panel._refs
    if R.detailPanel then R.detailPanel:Hide() end
    Panel._state.detailOpen = false
end

-- Hand off to Blizzard's own Premade Group Finder UI in "Start a Group"
-- mode for the given category (e.g. categoryID 2 for Mythic+). Used by
-- both the "List Your Key" and "Create Group" buttons.
--
-- Mechanics:
--   1. Set _blizzHandoff so our PVEFrame_ToggleFrame hook leaves PVEFrame
--      alone for the duration.
--   2. Hide our panel.
--   3. Synchronously load the relevant Blizzard addons (LoadAddOn blocks
--      until the addon's tables are ready, so no timer is needed here).
--   4. Show PVEFrame on the Group Finder tab.
--   5. Defer the category-select + Start-Group click by one frame so
--      Blizzard's CategorySelection OnShow handlers have run and its
--      StartGroupButton is wired up.
--   6. Reset _blizzHandoff when PVEFrame closes (one-shot OnHide hook),
--      not on a timer that has to guess how long the player will take.
local _blizzHandoffOnHideHooked = false
function Panel.HandoffToBlizzardCreate(categoryID)
    Panel._state._blizzHandoff = true
    Panel.Hide()

    local loader = (C_AddOns and C_AddOns.LoadAddOn) or _G.LoadAddOn
    if loader then
        for _, addon in ipairs({
            "Blizzard_PVEFrame", "Blizzard_GroupFinder", "Blizzard_LookingForGroupUI",
        }) do
            local isLoaded = (C_AddOns and C_AddOns.IsAddOnLoaded
                              and C_AddOns.IsAddOnLoaded(addon))
                          or (IsAddOnLoaded and IsAddOnLoaded(addon))
            if not isLoaded then pcall(loader, addon) end
        end
    end

    if PVEFrame and PVEFrame_ShowFrame then
        pcall(PVEFrame_ShowFrame, "GroupFinderFrame")
    end

    -- One-frame defer so CategorySelection has finished its OnShow setup.
    C_Timer.After(0, function()
        local sel = LFGListFrame and LFGListFrame.CategorySelection
        -- Pre-select a category only if the caller specified one.
        if categoryID and sel and LFGListCategorySelection_SelectCategory then
            pcall(LFGListCategorySelection_SelectCategory, sel, categoryID, 0)
        end
        if sel and sel.StartGroupButton then
            pcall(function() sel.StartGroupButton:Click() end)
        end
    end)

    -- Reset the handoff flag the moment Blizzard's UI is dismissed,
    -- regardless of how long the player took to finish the listing.
    if not _blizzHandoffOnHideHooked and PVEFrame and PVEFrame.HookScript then
        _blizzHandoffOnHideHooked = true
        PVEFrame:HookScript("OnHide", function()
            Panel._state._blizzHandoff = false
        end)
    end
end

function Panel.ListMyKey()
    Panel.HandoffToBlizzardCreate(2)
end

function Panel.ApplyToGroup(resultID)
    if not resultID then return end

    local validOk, validInfo = pcall(C_LFGList.GetSearchResultInfo, resultID)
    if not validOk or not validInfo then
        if DEFAULT_CHAT_FRAME then
            DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[MidnightUI]|r Group is no longer available.", 1, 0.4, 0.4)
        end
        Panel.UpdateListingRows()
        return
    end
    if validInfo.isDelisted then
        if DEFAULT_CHAT_FRAME then
            DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[MidnightUI]|r Group is no longer available.", 1, 0.4, 0.4)
        end
        Panel.UpdateListingRows()
        return
    end

    local roles = Panel._state.applyRole or {}
    local isTank = roles.tank or false
    local isHealer = roles.healer or false
    local isDPS = roles.dps or false
    if not isTank and not isHealer and not isDPS then
        if DEFAULT_CHAT_FRAME then
            DEFAULT_CHAT_FRAME:AddMessage(
                "|cff00ccff[MidnightUI]|r Pick a role (Tank, Healer, or DPS) before applying.",
                1, 0.6, 0.2)
        end
        return
    end

    local pendingCount = 0
    if C_LFGList.GetApplications then
        local aOk, apps = pcall(C_LFGList.GetApplications)
        if aOk and apps then pendingCount = #apps end
    end
    if pendingCount >= 5 then
        if DEFAULT_CHAT_FRAME then
            DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[MidnightUI]|r Cannot apply — you already have 5 pending applications.", 1, 0.6, 0.2)
        end
        return
    end

    if SetLFGRoles then
        pcall(SetLFGRoles, false, isTank, isHealer, isDPS)
    end
    -- Retail expects the three role choices immediately after the group ID.
    local ok = pcall(C_LFGList.ApplyToGroup, resultID, isTank, isHealer, isDPS)
    if ok then
        if DEFAULT_CHAT_FRAME then
            DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[MidnightUI]|r Applied to group. (" .. (pendingCount + 1) .. "/5)", 0.5, 1.0, 0.8)
        end
        C_Timer.After(0.3, function()
            Panel.UpdateGroupIntelSidebar()
            Panel.UpdateListingRows()
            local R2 = Panel._refs
            if Panel._state.detailOpen and R2.detailPanel and R2.detailPanel._resultID == resultID then
                Panel.ShowGroupDetail(resultID)
            end
        end)
    end
end


function Panel.UpdateDungeonQueueStatus()
    local R = Panel._refs
    if not R.dungeonQueueBar then return end

    local inQueue = false
    local lfgCat = LE_LFG_CATEGORY_LFD or 1
    local ok, hasData, leaderNeeds, tankNeeds, healerNeeds, dpsNeeds,
          totalTanks, totalHealers, totalDPS, instanceType, instanceSubType,
          instanceName, averageWait, tankWait, healerWait, damageWait,
          myWait, queuedTime = pcall(GetLFGQueueStats, lfgCat)
    if ok and hasData then
        inQueue = true
        local estSec = myWait
        if not estSec or estSec < 0 then estSec = averageWait end
        local estStr
        if not estSec or estSec < 0 then
            estStr = "--"
        else
            estStr = math.ceil(estSec / 60) .. "m"
        end
        local elapsedSec = 0
        if queuedTime and queuedTime > 0 then
            elapsedSec = GetTime() - queuedTime
            if elapsedSec < 0 then elapsedSec = 0 end
        end
        local elapsedStr = math.floor(elapsedSec / 60) .. "m"
        local queuedFor = Panel._state.dungeonQueuedNames or instanceName or "Dungeon"
        R.dungeonQueueText:SetText(tostring(queuedFor) .. " \226\128\148 Est: " .. estStr .. " \226\128\148 In queue: " .. elapsedStr)
    end

    if inQueue then
        R.dungeonQueueBar:Show()
    else
        R.dungeonQueueBar:Hide()
    end
end

function Panel.UpdateDungeonCards()
    -- Legacy layout pass for the retired 2-column card grid. The new
    -- three-region dock handles eligibility and layout itself, so this
    -- function is a no-op now. Left in place so external callers and
    -- legacy ticker paths don't error.
    return
end

--[==[  REMOVED: legacy two-column card grid layout path.
local function _unused_legacy_UpdateDungeonCards()
    local R = Panel._refs
    if not R.dungeonCards then return end

    local L = Panel._cardLayout
    if not L then return end

    local mplusData = GetPlayerMPlusData()
    local maxLevel = 0

    for _, lvl in pairs(mplusData.dungeonBest) do
        if lvl > maxLevel then maxLevel = lvl end
    end

    local playerLevel = UnitLevel and UnitLevel("player") or 0

    local visibleHeroic = {}
    for _, card in ipairs(R.heroicCards or {}) do
        local eligible = false
        local qID = card._lfgDungeonID
        if qID and IsLFGDungeonJoinable then
            local ok, joinable = pcall(IsLFGDungeonJoinable, qID)
            if ok then eligible = (joinable == true) end
        elseif qID and playerLevel > 0 and GetLFGDungeonInfo then
            eligible = true
            local ok, _, _, _, mnLvl, mxLvl = pcall(GetLFGDungeonInfo, qID)
            if ok then
                local mn = mnLvl or 0
                local mx = mxLvl or 0
                if mn > 0 and playerLevel < mn then eligible = false end
                if mx > 0 and playerLevel > mx then eligible = false end
            end
        end
        if eligible then
            card:Show()
            visibleHeroic[#visibleHeroic + 1] = card
        else
            card:Hide()
            card._selected = false
        end
    end
    if R.heroicHeader then
        if #visibleHeroic > 0 then
            R.heroicHeader:Show()
            R.heroicHeader:ClearAllPoints()
            R.heroicHeader:SetPoint("TOPLEFT", R.cardContainer, "TOPLEFT", 4, 0)
        else
            R.heroicHeader:Hide()
        end
    end
    if R.heroicDiv then
        if #visibleHeroic > 0 then R.heroicDiv:Show() else R.heroicDiv:Hide() end
    end
    local heroicRows = math.ceil(#visibleHeroic / 2)
    local heroicStartY = -(L.SECTION_H)
    for i, card in ipairs(visibleHeroic) do
        local col = (i - 1) % 2
        local row = math.floor((i - 1) / 2)
        local xOfs = col * (L.W + L.GX)
        local yOfs = heroicStartY - (row * (L.H + L.GY))
        card:ClearAllPoints()
        card:SetPoint("TOPLEFT", R.cardContainer, "TOPLEFT", xOfs, yOfs)
    end
    local heroicSectionH = 0
    if #visibleHeroic > 0 then
        heroicSectionH = L.SECTION_H + heroicRows * (L.H + L.GY) + 8
    end
    Panel._visibleHeroicCount = #visibleHeroic

    local visibleSeason = {}
    for _, card in ipairs(R.dungeonCards) do
        local eligible = false
        local qID = card._lfgDungeonID

        if qID and IsLFGDungeonJoinable then
            local ok, joinable = pcall(IsLFGDungeonJoinable, qID)
            if ok then
                eligible = (joinable == true)
            end
        elseif qID and playerLevel > 0 and GetLFGDungeonInfo then
            eligible = true
            local ok, _, _, _, mnLvl, mxLvl = pcall(GetLFGDungeonInfo, qID)
            if ok then
                local mn = mnLvl or 0
                local mx = mxLvl or 0
                if mn > 0 and playerLevel < mn then eligible = false end
                if mx > 0 and playerLevel > mx then eligible = false end
            end
        end

        if eligible then
            card:Show()
            visibleSeason[#visibleSeason + 1] = card
        else
            card:Hide()
            card._selected = false
        end
    end

    if R.mplusHeader then
        if #visibleSeason > 0 then R.mplusHeader:Show() else R.mplusHeader:Hide() end
    end
    if R.mplusDiv then
        if #visibleSeason > 0 then R.mplusDiv:Show() else R.mplusDiv:Hide() end
    end

    local mplusStartY = -(heroicSectionH + L.SECTION_H)
    if R.mplusHeader and #visibleSeason > 0 then
        R.mplusHeader:ClearAllPoints()
        R.mplusHeader:SetPoint("TOPLEFT", R.cardContainer, "TOPLEFT", 4, -heroicSectionH)
    end
    for i, card in ipairs(visibleSeason) do
        local col = (i - 1) % 2
        local row = math.floor((i - 1) / 2)
        local xOfs = col * (L.W + L.GX)
        local yOfs = mplusStartY - (row * (L.H + L.GY))
        card:ClearAllPoints()
        card:SetPoint("TOPLEFT", R.cardContainer, "TOPLEFT", xOfs, yOfs)
    end

    Panel._visibleSeasonCount = #visibleSeason

    for _, card in ipairs(visibleSeason) do
        local mapID = card._mapID
        local bestLevel = mplusData.dungeonBest[mapID] or 0

        if bestLevel > 0 then
            card._bestText:SetText("Best: +" .. bestLevel)
            card._bestText:SetTextColor(TC("bodyText"))
        else
            card._bestText:SetText("No runs yet")
            card._bestText:SetTextColor(TC("mutedText"))
        end

        if card._scoreText then
            if C_ChallengeMode and C_ChallengeMode.GetSpecificDungeonOverallScoreRankForMap then
                local ok, dungScore = pcall(C_ChallengeMode.GetSpecificDungeonOverallScoreRankForMap, mapID)
                if ok and dungScore and dungScore > 0 then
                    card._scoreText:SetText("Score: " .. dungScore)
                else
                    card._scoreText:SetText("")
                end
            else
                card._scoreText:SetText("")
            end
        end

        if maxLevel > 0 and bestLevel > 0 then
            local pct = bestLevel / maxLevel
            local trackW = card._trackBg:GetWidth()
            if trackW and trackW > 0 then
                card._trackFill:SetWidth(math.max(1, trackW * pct))
            end
        else
            card._trackFill:SetWidth(1)
        end
    end

    Panel.UpdateFollowerCardVisibility()
end

function Panel.UpdateFollowerCardVisibility()
    local R = Panel._refs
    local cards = R.followerCards
    if not cards or #cards == 0 then
        Panel._visibleFollowerCount = 0
        Panel.UpdateTimewalkingCardVisibility()
        return
    end

    local L = Panel._cardLayout
    if not L then return end

    local playerLevel = UnitLevel and UnitLevel("player") or 0
    local visibleHeroicCount = Panel._visibleHeroicCount or #(R.heroicCards or {})
    local visibleSeasonCount = Panel._visibleSeasonCount or #(R.dungeonCards or {})
    local heroicRows = math.ceil(visibleHeroicCount / 2)
    local mplusRows = math.ceil(visibleSeasonCount / 2)
    local heroicSectionH = 0
    if visibleHeroicCount > 0 then
        heroicSectionH = L.SECTION_H + heroicRows * (L.H + L.GY) + 8
    end
    local seasonSectionH = 0
    if visibleSeasonCount > 0 then
        seasonSectionH = L.SECTION_H + mplusRows * (L.H + L.GY) + 8
    end
    local followerStartY = -(heroicSectionH + seasonSectionH)

    local visible = {}
    for _, card in ipairs(cards) do
        local eligible = false
        local qID = card._dungeonID
        if qID and IsLFGDungeonJoinable then
            local ok, joinable = pcall(IsLFGDungeonJoinable, qID)
            if ok then
                eligible = (joinable == true)
            end
        elseif playerLevel > 0 then
            eligible = true
            local mn = card._minLevel or 0
            local mx = card._maxLevel or 0
            if mn > 0 and playerLevel < mn then eligible = false end
            if mx > 0 and playerLevel > mx then eligible = false end
        end
        if eligible then
            card:Show()
            visible[#visible + 1] = card
        else
            card:Hide()
            card._selected = false
        end
    end

    if R.followerHeader then
        if #visible > 0 then R.followerHeader:Show() else R.followerHeader:Hide() end
    end
    if R.followerDiv then
        if #visible > 0 then R.followerDiv:Show() else R.followerDiv:Hide() end
    end

    local fCardStartY = followerStartY - L.SECTION_H
    for i, card in ipairs(visible) do
        local col = (i - 1) % 2
        local row = math.floor((i - 1) / 2)
        local xOfs = col * (L.W + L.GX)
        local yOfs = fCardStartY - (row * (L.H + L.GY))
        card:ClearAllPoints()
        card:SetPoint("TOPLEFT", R.cardContainer, "TOPLEFT", xOfs, yOfs)
    end

    Panel._visibleFollowerCount = #visible

    if R.followerHeader and #visible > 0 then
        R.followerHeader:ClearAllPoints()
        R.followerHeader:SetPoint("TOPLEFT", R.cardContainer, "TOPLEFT", 4, followerStartY)
    end

    Panel.UpdateTimewalkingCardVisibility()
end

function Panel.UpdateTimewalkingCardVisibility()
    local R = Panel._refs
    local card = R.twCard
    local L = Panel._cardLayout
    if not L then return end

    local visibleHeroicCount = Panel._visibleHeroicCount or #(R.heroicCards or {})
    local visibleSeasonCount = Panel._visibleSeasonCount or #(R.dungeonCards or {})
    local visibleFollowerCount = Panel._visibleFollowerCount or #(R.followerCards or {})
    local heroicRows = math.ceil(visibleHeroicCount / 2)
    local mplusRows = math.ceil(visibleSeasonCount / 2)
    local followerRows = math.ceil(visibleFollowerCount / 2)
    local heroicSectionH = 0
    if visibleHeroicCount > 0 then
        heroicSectionH = L.SECTION_H + heroicRows * (L.H + L.GY) + 8
    end

    local twInfo = FindTimewalkingRandomDungeon()
    local twVisible = false
    if twInfo and card then
        card._dungeonID = twInfo.dungeonID
        card._dungeonName = twInfo.name
        if card._nameText then card._nameText:SetText(twInfo.name) end
        twVisible = true
    end

    if card then
        if twVisible then card:Show() else card:Hide(); card._selected = false end
    end
    if R.twHeader then
        if twVisible then R.twHeader:Show() else R.twHeader:Hide() end
    end
    if R.twDiv then
        if twVisible then R.twDiv:Show() else R.twDiv:Hide() end
    end

    local twStartY = -heroicSectionH
    if visibleSeasonCount > 0 then
        twStartY = twStartY - L.SECTION_H - (mplusRows * (L.H + L.GY)) - 8
    end
    if visibleFollowerCount > 0 then
        twStartY = twStartY - L.SECTION_H - (followerRows * (L.H + L.GY)) - 8
    end

    if twVisible then
        if R.twHeader then
            R.twHeader:ClearAllPoints()
            R.twHeader:SetPoint("TOPLEFT", R.cardContainer, "TOPLEFT", 4, twStartY)
        end
        if card then
            card:ClearAllPoints()
            card:SetPoint("TOPLEFT", R.cardContainer, "TOPLEFT", 0, twStartY - L.SECTION_H)
        end
    end

    local totalH = heroicSectionH
    if visibleSeasonCount > 0 then
        totalH = totalH + L.SECTION_H + mplusRows * (L.H + L.GY) + 8
    end
    if visibleFollowerCount > 0 then
        totalH = totalH + L.SECTION_H + followerRows * (L.H + L.GY) + 8
    end
    if twVisible then
        totalH = totalH + L.SECTION_H + (L.H + L.GY) + 8
    end
    if R.cardContainer then
        R.cardContainer:SetHeight(totalH)
    end
end
]==]

function Panel.UpdateDungeonRoleCards()
    Panel.UpdateRoleCards(Panel._refs.dungeonRoleCards or Panel._refs.dungeonRoleBtns, Panel._state.dungeonRoles)
end

function Panel.AutoDetectDungeonRoles()
    local R = Panel._refs
    local cards = R.dungeonRoleCards or R.dungeonRoleBtns
    if not cards then return end

    local spec = GetSpecialization and GetSpecialization()
    local role = nil
    if spec then
        local ok, _, _, _, specRole = pcall(GetSpecializationInfo, spec)
        if ok and specRole then role = specRole end
    end

    local st = Panel._state.dungeonRoles
    st.tank = (role == "TANK")
    st.healer = (role == "HEALER")
    st.dps = (role == "DAMAGER")
    if not st.tank and not st.healer and not st.dps then st.dps = true end

    Panel.UpdateDungeonRoleCards()
end


function Panel.AutoDetectRaidRoles()
    local R = Panel._refs
    if not R.raidRoleBtns then return end

    local spec = GetSpecialization and GetSpecialization()
    local role = nil
    if spec then
        local ok, _, _, _, specRole = pcall(GetSpecializationInfo, spec)
        if ok and specRole then role = specRole end
    end

    local st = Panel._state.raidRoles
    if not st then
        Panel._state.raidRoles = { tank = false, healer = false, dps = false }
        st = Panel._state.raidRoles
    end
    st.tank = (role == "TANK")
    st.healer = (role == "HEALER")
    st.dps = (role == "DAMAGER")
    if not st.tank and not st.healer and not st.dps then st.dps = true end

    Panel.UpdateRaidRoleCards()
end

function Panel.UpdateRaidFinder()
    local R = Panel._refs
    if not R.raidWingCards then return end

    local wings = {}
    local playerLevel = UnitLevel("player") or 80
    local _, playerIlvl = pcall(GetAverageItemLevel)
    playerIlvl = tonumber(playerIlvl) or 0
    local ok, numRF = pcall(GetNumRFDungeons)
    if ok and numRF and numRF > 0 then
        for i = 1, numRF do
            local ok2, id, name = pcall(GetRFDungeonInfo, i)
            if ok2 and id and name then
                local eligible = true
                if GetLFGDungeonInfo then
                    local okI, dName, typeID, subtypeID, minLevel, maxLevel,
                          recLevel, minRecLevel, maxRecLevel, expansionLevel,
                          groupID, textureFilename, difficulty, maxPlayers,
                          dungeonDescription, isHoliday, bonusRepAmount,
                          minPlayers, isTimewalker, name2, minGearLevel = pcall(GetLFGDungeonInfo, id)
                    if okI then
                        if minLevel and maxLevel and (playerLevel < minLevel or playerLevel > maxLevel) then
                            eligible = false
                        end
                        if eligible and minGearLevel and minGearLevel > 0 and playerIlvl > 0 and playerIlvl < minGearLevel then
                            eligible = false
                        end
                    end
                end
                if eligible and _G.LFGLockList and _G.LFGLockList[id] then
                    eligible = false
                end
                if eligible then
                    wings[#wings + 1] = { id = id, name = name }
                end
            end
        end
    end

    if #wings == 0 then
        for i = 1, 4 do
            if R.raidWingCards[i] then R.raidWingCards[i]:Hide() end
        end
        if R.raidNoWingsMsg then R.raidNoWingsMsg:Show() end
        if R.raidTierName then R.raidTierName:SetText("") end
        return
    end

    if R.raidNoWingsMsg then R.raidNoWingsMsg:Hide() end

    for i = 1, 4 do
        local card = R.raidWingCards[i]
        if not card then break end
        local wing = wings[i]
        if wing then
            card._nameText:SetText(wing.name)
            local defeatedText = "Select to queue"
            local ok3, numEnc = pcall(GetLFGDungeonNumEncounters, wing.id)
            if ok3 and numEnc and numEnc > 0 then
                local killed = 0
                for e = 1, numEnc do
                    local ok4, eName, eID, isKilled = pcall(GetLFGDungeonEncounterInfo, wing.id, e)
                    if ok4 and isKilled then killed = killed + 1 end
                end
                if killed > 0 then
                    defeatedText = "Bosses: " .. numEnc .. "  \194\183  Defeated: " .. killed .. "/" .. numEnc
                else
                    defeatedText = "Bosses: " .. numEnc .. "  \194\183  Not started"
                end
            end
            card._infoText:SetText(defeatedText)
            card._wingID = wing.id
            card:Show()
        else
            card:Hide()
        end
    end
end

function Panel.UpdateRaidQueueStatus()
    local R = Panel._refs
    if not R.raidQueueBar then return end

    local inQueue = false
    local lfgCat = LE_LFG_CATEGORY_RF or 2
    local ok, hasData, leaderNeeds, tankNeeds, healerNeeds, dpsNeeds,
          totalTanks, totalHealers, totalDPS, instanceType, instanceSubType,
          instanceName, averageWait, tankWait, healerWait, damageWait,
          myWait, queuedTime = pcall(GetLFGQueueStats, lfgCat)
    if ok and hasData then
        inQueue = true
        local estSec = myWait
        if not estSec or estSec < 0 then estSec = averageWait end
        local estStr
        if not estSec or estSec < 0 then
            estStr = "--"
        else
            estStr = math.ceil(estSec / 60) .. "m"
        end
        local elapsedSec = 0
        if queuedTime and queuedTime > 0 then
            elapsedSec = GetTime() - queuedTime
            if elapsedSec < 0 then elapsedSec = 0 end
        end
        local elapsedStr = math.floor(elapsedSec / 60) .. "m"
        R.raidQueueText:SetText("Queued \226\128\148 Est: " .. estStr .. " \226\128\148 In queue: " .. elapsedStr)
    end

    if inQueue then
        R.raidQueueBar:Show()
    else
        R.raidQueueBar:Hide()
    end
end


function Panel.UpdateDungeonHero()
    local R = Panel._refs
    if not R.dungeonHero then return end

    local mplusData = GetPlayerMPlusData()
    -- MPI pill is populated by MidnightPI._applyMpiToPill (called below
    -- from Panel.UpdateDungeonSuggestions). Single source of truth for
    -- the pill avoids edits-overwriting-edits when both run on the same
    -- panel-show cycle.

    PopulatePlayerIdentity(R.dungeonHeroPlayerLine, R.dungeonHeroClassIcon, R.dungeonHeroSpecLine)

    if R.dungeonRatingPill then
        if mplusData.overallScore > 0 then
            R.dungeonRatingPill._value:SetText(FormatNumber(mplusData.overallScore))
            local sc = mplusData.scoreColor
            R.dungeonRatingPill._value:SetTextColor(sc[1], sc[2], sc[3])
        else
            R.dungeonRatingPill._value:SetText("--")
            R.dungeonRatingPill._value:SetTextColor(TC("mutedText"))
        end
    end

    if R.dungeonKeystonePill then
        if mplusData.keystoneLevel and mplusData.keystoneName then
            R.dungeonKeystonePill._value:SetText("+" .. mplusData.keystoneLevel .. " " .. mplusData.keystoneName)
        elseif mplusData.keystoneLevel then
            R.dungeonKeystonePill._value:SetText("+" .. mplusData.keystoneLevel)
        else
            R.dungeonKeystonePill._value:SetText("None")
        end
        FitPillToContent(R.dungeonKeystonePill,
            R.dungeonKeystonePill._muiMinW, R.dungeonKeystonePill._muiMaxW)
    end

    if R.heroDungeonBadges then
        for _, badge in ipairs(R.heroDungeonBadges) do
            local best = mplusData.dungeonBest[badge._mapID]
            if best and best > 0 then
                badge._levelText:SetText("+" .. best)
                badge._levelText:SetTextColor(badge._accent[1], badge._accent[2], badge._accent[3])
                badge:SetAlpha(1.0)
            else
                badge._levelText:SetText("--")
                badge._levelText:SetTextColor(TC("mutedText"))
                badge:SetAlpha(0.5)
            end
        end
    end

    if R.dungeonHeroTrack and R.dungeonHeroFill then
        local pct = math.min(1, mplusData.overallScore / CFG.SCORE_CAP)
        local trackW = R.dungeonHeroTrack:GetWidth()
        if trackW and trackW > 0 then
            R.dungeonHeroFill:SetWidth(math.max(1, trackW * pct))
        end
    end

    Panel.UpdateDungeonRoleCards()
end

function Panel.UpdateRaidRoleCards()
    Panel.UpdateRoleCards(Panel._refs.raidRoleCards, Panel._state.raidRoles, { 0.80, 0.65, 0.20 })
end

function Panel.UpdateRaidHero()
    local R = Panel._refs
    if not R.raidHero then return end

    PopulatePlayerIdentity(R.raidHeroPlayerName, R.raidHeroClassIcon, R.raidHeroSpecLine)

    if R.raidHeroTierName then
        R.raidHeroTierName:SetText(DeriveRaidTierName())
    end

    local wings, totalBosses, totalKilled = GetRaidBossProgress()


    if R.raidHeroBossCount then
        R.raidHeroBossCount:SetText(totalKilled .. "/" .. totalBosses)
    end

    if R.raidWingBars then
        for wi = 1, 4 do
            local bar = R.raidWingBars[wi]
            if not bar then break end
            local wing = wings[wi]
            if wing then
                bar._nameText:SetText(wing.name)
                bar._countText:SetText(wing.killed .. "/" .. wing.total)
                if wing.killed > 0 then
                    bar._countText:SetTextColor(bar._accent[1], bar._accent[2], bar._accent[3])
                else
                    bar._countText:SetTextColor(TC("mutedText"))
                end
                local barInnerW = bar:GetWidth() - 2
                if barInnerW > 0 and wing.total > 0 then
                    bar._fill:SetWidth(math.max(1, barInnerW * (wing.killed / wing.total)))
                else
                    bar._fill:SetWidth(1)
                end
                bar:Show()
            else
                bar:Hide()
            end
        end
    end

    if R.raidHeroTrack and R.raidHeroFill then
        if totalBosses > 0 then
            local pct = totalKilled / totalBosses
            local trackW = R.raidHeroTrack:GetWidth()
            if trackW and trackW > 0 then
                R.raidHeroFill:SetWidth(math.max(1, trackW * pct))
            end
        else
            R.raidHeroFill:SetWidth(1)
        end
    end

    Panel.UpdateRaidRoleCards()
end

function Panel.UpdatePremadeHero()
    local R = Panel._refs
    if not R.premadeHero then return end

    local cat = Panel._state.activeCategory
    local titles = {
        mythicplus = "MYTHIC+ GROUPS", delves = "DELVE GROUPS",
        raids = "RAID GROUPS", legacy = "LEGACY RAID GROUPS",
        questing = "QUEST GROUPS", custom = "CUSTOM GROUPS",
    }

    PopulatePlayerIdentity(R.premadeHeroPlayerName, R.premadeHeroClassIcon, R.premadeHeroSpecLine)

    local mplusData = GetPlayerMPlusData()
    -- MPI pill is populated by MidnightPI._applyMpiToPill (called at the
    -- bottom of this function via UpdatePremadeHeroPill).

    if R.premadeHeroScorePill then
        local pill = R.premadeHeroScorePill
        if mplusData.overallScore > 0 then
            pill._value:SetText(FormatNumber(mplusData.overallScore))
            local sc = mplusData.scoreColor
            pill._value:SetTextColor(sc[1], sc[2], sc[3])
            pill._dot:SetColorTexture(sc[1], sc[2], sc[3], 0.80)
        else
            pill._value:SetText("--")
            pill._value:SetTextColor(TC("mutedText"))
            pill._dot:SetColorTexture(TC("accent"))
        end
    end

    if R.premadeHeroKeystonePill then
        local pill = R.premadeHeroKeystonePill
        if mplusData.keystoneLevel and mplusData.keystoneName then
            pill._value:SetText("+" .. mplusData.keystoneLevel .. " " .. mplusData.keystoneName)
            pill._value:SetTextColor(TC("bodyText"))
            local ksAccent = nil
            for _, d in ipairs(Panel._dungeonData or {}) do
                if d.mapID == mplusData.keystoneMapID then ksAccent = d.accent; break end
            end
            if ksAccent then
                pill._dot:SetColorTexture(ksAccent[1], ksAccent[2], ksAccent[3], 0.80)
            end
        else
            pill._value:SetText("None")
            pill._value:SetTextColor(TC("mutedText"))
        end
        FitPillToContent(pill, pill._muiMinW, pill._muiMaxW)
    end

    Panel.UpdatePremadeRoleCards()

    if R.premadeHeroSuggestion then
        local weakest, weakLevel = FindWeakestDungeon(Panel._dungeonData or {}, mplusData.dungeonBest)
        local hasAnyRuns = false
        for _ in pairs(mplusData.dungeonBest) do hasAnyRuns = true; break end
        if hasAnyRuns and weakest and weakLevel > 0 then
            R.premadeHeroSuggestion:SetText("Weakest: " .. weakest.name .. " (+" .. weakLevel .. ")")
        else
            R.premadeHeroSuggestion:SetText("")
        end
    end

    if R.listKeyBtn then
        local hasKey = mplusData.keystoneLevel and mplusData.keystoneMapID
        local hasListing = C_LFGList.HasActiveEntryInfo and SafeCall(C_LFGList.HasActiveEntryInfo)
        if hasListing then
            R.listKeyBtn._label:SetText("Delist")
            R.listKeyBtn:SetAlpha(1.0)
            R.listKeyBtn:EnableMouse(true)
            R.listKeyBtn:SetScript("OnClick", function()
                pcall(C_LFGList.RemoveListing)
                C_Timer.After(0.5, function() Panel.UpdatePremadeHero() end)
            end)
        elseif hasKey then
            R.listKeyBtn._label:SetText("List Your Key")
            R.listKeyBtn:SetAlpha(1.0)
            R.listKeyBtn:EnableMouse(true)
            R.listKeyBtn:SetScript("OnClick", function() Panel.ListMyKey() end)
        else
            R.listKeyBtn._label:SetText("Create Group")
            R.listKeyBtn:SetAlpha(1.0)
            R.listKeyBtn:EnableMouse(true)
            R.listKeyBtn:SetScript("OnClick", function()
                Panel.HandoffToBlizzardCreate(nil)  -- nil = no preselected category
            end)
        end
    end

    if _G.MidnightPI and _G.MidnightPI.UpdatePremadeHeroPill then _G.MidnightPI.UpdatePremadeHeroPill(R) end

    if _G.MidnightPI and _G.MidnightPI.UpdateCompanionBanner then _G.MidnightPI.UpdateCompanionBanner(R.premadeHero) end
end


function Panel.UpdateDungeonSuggestions()
    local R = Panel._refs
    if not R.suggestWeakText then return end

    local mplusData = GetPlayerMPlusData()
    local dungeons = Panel._dungeonData or {}

    local weakest, weakLevel = FindWeakestDungeon(dungeons, mplusData.dungeonBest)
    local hasAnyRuns = false
    for _ in pairs(mplusData.dungeonBest) do hasAnyRuns = true; break end

    if hasAnyRuns and weakest then
        if weakLevel == 0 then
            R.suggestWeakText:SetText("Try: " .. weakest.name)
            R.suggestWeakText:SetTextColor(TC("accent"))
        else
            R.suggestWeakText:SetText("Weakest: " .. weakest.name .. " (+" .. weakLevel .. ")")
            R.suggestWeakText:SetTextColor(TC("mutedText"))
        end
    else
        R.suggestWeakText:SetText("")
    end

    if R.suggestKeyText then
        if mplusData.keystoneLevel and mplusData.keystoneName then
            R.suggestKeyText:SetText("Key: +" .. mplusData.keystoneLevel .. " " .. mplusData.keystoneName)
            R.suggestKeyText:SetTextColor(TC("bodyText"))
        else
            R.suggestKeyText:SetText("")
        end
    end

    if _G.MidnightPI and _G.MidnightPI.UpdateDungeonHeroDashboard then _G.MidnightPI.UpdateDungeonHeroDashboard(R) end
end


function Panel.ShowInviteBanner(groupName)
    local R = Panel._refs
    if not R.inviteBanner then
        local parent = R.premadeHero or R.listingArea
        if not parent then return end
        local banner = CreateFrame("Frame", nil, parent, "BackdropTemplate")
        banner:SetHeight(28)
        banner:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", CFG.PAD, 4)
        banner:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -CFG.PAD, 4)
        banner:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        banner:SetBackdropColor(0.12, 0.45, 0.2, 0.95)
        banner:SetBackdropBorderColor(0.3, 0.9, 0.4, 0.8)
        banner:SetFrameLevel(parent:GetFrameLevel() + 10)
        local bannerText = banner:CreateFontString(nil, "OVERLAY")
        TrySetFont(bannerText, TITLE_FONT, 12, "OUTLINE")
        bannerText:SetPoint("CENTER")
        bannerText:SetTextColor(1, 1, 1)
        banner._text = bannerText
        R.inviteBanner = banner
    end
    R.inviteBanner._text:SetText("Invited to: " .. (groupName or "a group") .. "!")
    R.inviteBanner:Show()
    C_Timer.After(8, function()
        if R.inviteBanner then R.inviteBanner:Hide() end
    end)
end


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

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

    R.panel:SetBackdropColor(TC("frameBg"))
    local ac = activeTheme.accent
    R.panel:SetBackdropBorderColor(ac[1], ac[2], ac[3], 0.15)

    if R.headerBg then R.headerBg:SetColorTexture(TC("headerBg")) end
    if R.title then R.title:SetTextColor(TC("titleText")) end
    if R.headerAccentLine and R.headerAccentLine.SetGradient and CreateColor then
        R.headerAccentLine:SetGradient("HORIZONTAL",
            CreateColor(ac[1], ac[2], ac[3], 0.6),
            CreateColor(ac[1], ac[2], ac[3], 0.0))
    end

    if R.settingsPopup then
        R.settingsPopup:SetBackdropBorderColor(ac[1], ac[2], ac[3], 0.5)
    end

    if R.tabDivider then R.tabDivider:SetColorTexture(TC("accent")) end

    if R.catSidebar and R.catSidebar.ApplyTheme then R.catSidebar:ApplyTheme() end
    if R.playerSidebar and R.playerSidebar.ApplyTheme then R.playerSidebar:ApplyTheme() end

    Panel.SetActiveTab(Panel._state.activeTab)
end


do
    local toastFrame, toastTitle, toastSep, toastBody, toastDismiss, toastTimer

    function Panel.ShowToast(title, body, duration)
        local R = Panel._refs
        local parent = R.panel or UIParent

        if not toastFrame then
            toastFrame = CreateFrame("Frame", nil, parent, "BackdropTemplate")
            toastFrame:SetSize(460, 90)
            toastFrame:SetPoint("TOP", parent, "TOP", 0, -80)
            toastFrame:SetFrameStrata("DIALOG")
            toastFrame:SetFrameLevel(200)
            toastFrame:SetBackdrop({
                bgFile = W8, edgeFile = W8, edgeSize = 1,
                insets = { left = 1, right = 1, top = 1, bottom = 1 },
            })
            toastFrame:SetBackdropColor(0.06, 0.02, 0.02, 0.96)
            toastFrame:SetBackdropBorderColor(0.5, 0.12, 0.12, 0.7)

            local frost = toastFrame:CreateTexture(nil, "OVERLAY", nil, 3)
            frost:SetHeight(1)
            frost:SetPoint("TOPLEFT", toastFrame, "TOPLEFT", 1, -1)
            frost:SetPoint("TOPRIGHT", toastFrame, "TOPRIGHT", -1, -1)
            frost:SetColorTexture(1, 0.3, 0.3, 0.15)

            toastTitle = toastFrame:CreateFontString(nil, "OVERLAY")
            toastTitle:SetPoint("TOP", toastFrame, "TOP", 0, -12)
            TrySetFont(toastTitle, TITLE_FONT, 12, "OUTLINE")
            toastTitle:SetTextColor(1, 0.35, 0.35)
            toastTitle:SetJustifyH("CENTER")

            toastSep = toastFrame:CreateTexture(nil, "OVERLAY", nil, 2)
            toastSep:SetHeight(1)
            toastSep:SetPoint("TOPLEFT", toastTitle, "BOTTOMLEFT", -60, -6)
            toastSep:SetPoint("TOPRIGHT", toastTitle, "BOTTOMRIGHT", 60, -6)
            toastSep:SetColorTexture(1, 0.3, 0.3, 0.2)

            toastBody = toastFrame:CreateFontString(nil, "OVERLAY")
            toastBody:SetPoint("TOP", toastSep, "BOTTOM", 0, -8)
            toastBody:SetPoint("LEFT", toastFrame, "LEFT", 20, 0)
            toastBody:SetPoint("RIGHT", toastFrame, "RIGHT", -20, 0)
            TrySetFont(toastBody, BODY_FONT, 10)
            toastBody:SetTextColor(0.8, 0.7, 0.7)
            toastBody:SetJustifyH("CENTER")
            toastBody:SetWordWrap(true)

            toastDismiss = toastFrame:CreateFontString(nil, "OVERLAY")
            toastDismiss:SetPoint("BOTTOM", toastFrame, "BOTTOM", 0, 5)
            TrySetFont(toastDismiss, BODY_FONT, 8)
            toastDismiss:SetTextColor(0.4, 0.3, 0.3)
            toastDismiss:SetText("click to dismiss")

            toastFrame:EnableMouse(true)
            toastFrame:SetScript("OnMouseDown", function(self)
                self:Hide()
                if toastTimer then toastTimer:Cancel(); toastTimer = nil end
            end)

            toastFrame:Hide()
        end

        if toastFrame:GetParent() ~= parent then
            toastFrame:SetParent(parent)
            toastFrame:ClearAllPoints()
            toastFrame:SetPoint("TOP", parent, "TOP", 0, -80)
        end

        toastTitle:SetText(title or "Notice")
        toastBody:SetText(body or "")

        local bodyH = toastBody:GetStringHeight() or 14
        toastFrame:SetHeight(math.max(80, 12 + 16 + 6 + 1 + 8 + bodyH + 8 + 14 + 5))

        toastFrame:Show()

        if toastTimer then toastTimer:Cancel() end
        toastTimer = C_Timer.NewTimer(duration or 5, function()
            if toastFrame then toastFrame:Hide() end
            toastTimer = nil
        end)
    end
end


local function EnsureMythicLockoutOverlay(p)
    if not p then return nil end
    if p._muiMythicLockoutOverlay then return p._muiMythicLockoutOverlay end

    -- Dim backdrop: sits over the whole panel, captures clicks, tints
    -- the listings behind at ~85% so they read as "disabled" instead of
    -- hidden (fully opaque looked broken).
    local overlay = CreateFrame("Frame", nil, p, "BackdropTemplate")
    overlay:SetAllPoints(p)
    overlay:SetFrameLevel((p:GetFrameLevel() or 0) + 50)
    overlay:EnableMouse(true)
    if overlay.SetBackdrop then
        overlay:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            edgeSize = 1,
            insets = { left = 0, right = 0, top = 0, bottom = 0 },
        })
        overlay:SetBackdropColor(0.02, 0.02, 0.03, 0.85)
        overlay:SetBackdropBorderColor(0, 0, 0, 0)
    end

    -- Centered message card so the text doesn't float over the dim alone.
    local card = CreateFrame("Frame", nil, overlay, "BackdropTemplate")
    card:SetSize(540, 120)
    card:SetPoint("CENTER", overlay, "CENTER", 0, 0)
    if card.SetBackdrop then
        card:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            edgeSize = 1,
            insets = { left = 0, right = 0, top = 0, bottom = 0 },
        })
        card:SetBackdropColor(0.06, 0.06, 0.09, 0.98)
        card:SetBackdropBorderColor(0.95, 0.82, 0.35, 0.80)
    end
    local title = card:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOP", card, "TOP", 0, -22)
    title:SetText("Group Finder Unavailable")
    title:SetTextColor(0.95, 0.85, 0.45)

    local body = card:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    body:SetPoint("TOP", title, "BOTTOM", 0, -14)
    body:SetText("You cannot use the Group Finder in Mythic+.")
    body:SetTextColor(0.85, 0.85, 0.90)

    overlay:Hide()
    p._muiMythicLockoutOverlay = overlay
    return overlay
end

local function ApplyMythicLockout(p)
    if not p then return end
    local overlay = EnsureMythicLockoutOverlay(p)
    local keyActive = C_ChallengeMode and C_ChallengeMode.IsChallengeModeActive and C_ChallengeMode.IsChallengeModeActive() or false
    if keyActive then
        overlay:Show()
        overlay:Raise()
    elseif overlay then
        overlay:Hide()
    end
end

-- Reparents Blizzard's actual LFG SearchBox into our M+ filter bar slot
-- so the player can type into it (hardware input is unrestricted) and
-- Blizzard's privileged filter — which can read protected listing names —
-- runs on its own. We then read LFGListFrame.SearchPanel.results, which
-- Blizzard populates with the filtered subset of the most recent search.
local function AdoptBlizzSearchBox()
    local R = Panel._refs
    local slot = R and R.searchSlot
    if not slot then return false end

    -- Lazy-load Blizzard's group-finder addon if it isn't loaded yet.
    if not (LFGListFrame and LFGListFrame.SearchPanel and LFGListFrame.SearchPanel.SearchBox) then
        local loader = (C_AddOns and C_AddOns.LoadAddOn) or _G.LoadAddOn
        if loader then
            pcall(loader, "Blizzard_GroupFinder")
            if not (LFGListFrame and LFGListFrame.SearchPanel) then
                pcall(loader, "Blizzard_LookingForGroupUI")
            end
        end
    end

    local box = LFGListFrame and LFGListFrame.SearchPanel and LFGListFrame.SearchPanel.SearchBox
    if not box then return false end

    -- Reparent and re-anchor to fill our slot
    box:SetParent(slot)
    box:ClearAllPoints()
    box:SetPoint("TOPLEFT",     slot, "TOPLEFT",     2, -2)
    box:SetPoint("BOTTOMRIGHT", slot, "BOTTOMRIGHT", -2, 2)
    box:SetFrameStrata(slot:GetFrameStrata())
    box:SetFrameLevel((slot:GetFrameLevel() or 0) + 1)
    box:Show()

    -- Hide Blizzard's stock decorations on the SearchBoxTemplate so only
    -- the input text shows through; our slot provides the visual frame.
    pcall(function()
        local hideTargets = { "Left", "Right", "Middle", "searchIcon", "clearButton", "Icon" }
        for _, key in ipairs(hideTargets) do
            local sub = box[key]
            if sub and sub.Hide then sub:Hide() end
            if sub and sub.SetAlpha then sub:SetAlpha(0) end
        end
        -- Wipe any remaining background textures rendered as regions
        for i = 1, box:GetNumRegions() do
            local region = select(i, box:GetRegions())
            if region and region:GetObjectType() == "Texture" then
                region:SetTexture(nil)
                region:Hide()
            end
        end
        -- Replace Blizzard's stock placeholder with a clean "Search..." prompt.
        if box.Instructions then
            if box.Instructions.SetText then box.Instructions:SetText("Search...") end
            if box.Instructions.SetTextColor then
                local mr, mg, mb = TC("mutedText")
                box.Instructions:SetTextColor(mr, mg, mb)
            end
            local fontPath = (BODY_FONT or "Fonts\\FRIZQT__.TTF")
            if box.Instructions.SetFont then
                pcall(box.Instructions.SetFont, box.Instructions, fontPath, 11, "")
            end
            if box.Instructions.SetJustifyH then box.Instructions:SetJustifyH("CENTER") end
        end
    end)

    -- Apply our own font + colors to the input text
    pcall(function()
        local fontPath = (BODY_FONT or "Fonts\\FRIZQT__.TTF")
        box:SetFont(fontPath, 11, "")
        local r, g, b = TC("bodyText")
        box:SetTextColor(r, g, b)
        box:SetJustifyH("CENTER")
        box:SetTextInsets(6, 6, 0, 0)
    end)

    -- Expose the adopted box so the autocomplete dropdown (built in
    -- EnsurePanel) can fill it when a suggestion is chosen.
    Panel._refs.searchBox = box

    -- Replace Blizzard's scripts that walk up via self:GetParent() to
    -- reach SearchPanel internals — those crash now that the box's
    -- parent is our slot. We provide minimal scripts that drive our
    -- re-filter plus our own activity autocomplete dropdown (Blizzard's
    -- native autocomplete can't be reused once the box is reparented).
    if not box._muiAdopted then
        box._muiAdopted = true

        box:SetScript("OnTextChanged", function(self, byUser)
            if self.Instructions then
                if self.Instructions.SetText then self.Instructions:SetText("Search...") end
                self.Instructions:SetShown(self:GetText() == "")
            end
            if byUser then
                -- Manual typing reverts to free-text filtering; drop any
                -- activity locked in from a previous dropdown selection.
                Panel._state._acActive = false
                Panel._state.filters.activityFull = nil
                local ac = Panel._refs.searchAC
                if ac and ac.DoUpdate then ac.DoUpdate(self:GetText()) end
                Panel.OnSearchResults()
            end
        end)
        box:SetScript("OnEditFocusGained", function() end)
        box:SetScript("OnEditFocusLost", function(self)
            if self.HighlightText then self:HighlightText(0, 0) end
            local ac = Panel._refs.searchAC
            if not (ac and ac:IsShown()) then return end
            -- Defer the close so a click on a dropdown row (which drops
            -- editbox focus on mouse-down) still lands its OnClick first.
            C_Timer.After(0.12, function()
                if ac:IsShown() and not ac:IsMouseOver() then ac:Hide() end
            end)
        end)
        box:SetScript("OnEnterPressed", function(self)
            local ac = Panel._refs.searchAC
            if ac and ac:IsShown() and ac._selected and ac._selected > 0 then
                ac.Accept(ac._selected)
            else
                if ac then ac:Hide() end
                self:ClearFocus()
            end
        end)
        box:SetScript("OnEscapePressed", function(self)
            local ac = Panel._refs.searchAC
            if ac and ac:IsShown() then
                ac:Hide()
                return
            end
            self:SetText("")
            Panel._state._acActive = false
            Panel._state.filters.activityFull = nil
            self:ClearFocus()
            Panel.OnSearchResults()
        end)
        box:SetScript("OnArrowPressed", function(self, key)
            local ac = Panel._refs.searchAC
            if ac and ac:IsShown() then
                if key == "DOWN" then ac.MoveSelection(1); return end
                if key == "UP" then ac.MoveSelection(-1); return end
            end
        end)
        box:SetScript("OnTabPressed", function(self)
            local ac = Panel._refs.searchAC
            if ac and ac:IsShown() and #ac._matches > 0 then
                ac.Accept(ac._selected > 0 and ac._selected or 1)
            else
                self:ClearFocus()
            end
        end)
    end
    return true
end

function Panel.Show(tab)
    if Panel._state._pvpHandoff then
        return
    end
    if InCombatLockdown and InCombatLockdown() then
        if DEFAULT_CHAT_FRAME then
            DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[MidnightUI]|r Cannot open Group Finder during combat.", 1, 0.4, 0.4)
        end
        return
    end
    local p = Panel.EnsurePanel()
    if not p then return end

    local charFrame = _G["MidnightUI_CharacterPanel"]
    if charFrame and charFrame:IsShown() then charFrame:Hide() end
    local guildFrame = _G["MidnightUI_GuildPanel"]
    if guildFrame and guildFrame:IsShown() then guildFrame:Hide() end

    GetActiveTheme()
    Panel._state.panelOpen = true
    p:Show()
    local savedTab = _G.MidnightUISettings and _G.MidnightUISettings.GroupFinder and _G.MidnightUISettings.GroupFinder.lastTab
    Panel.SetActiveTab(tab or savedTab or Panel._state.activeTab or "premade")
    ApplyMythicLockout(p)
    AdoptBlizzSearchBox()
end

function Panel.Hide()
    StopAutoRefresh()
    Panel.HideGroupDetail()
    local R = Panel._refs
    if R.dungeonDropdown and R.dungeonDropdown:IsShown() then
        R.dungeonDropdown:Hide()
    end
    if R.searchAC and R.searchAC:IsShown() then
        R.searchAC:Hide()
    end
    if R.raidSearchAC and R.raidSearchAC:IsShown() then
        R.raidSearchAC:Hide()
    end
    if R.delveSearchAC and R.delveSearchAC:IsShown() then
        R.delveSearchAC:Hide()
    end
    if R.teleportDropdown and R.teleportDropdown:IsShown() then
        R.teleportDropdown:Hide()
    end
    if R.panel then R.panel:Hide() end
    Panel._state.panelOpen = false
end

function Panel.Toggle()
    Panel._state._pvpHandoff = false
    if Panel._state.panelOpen and Panel._refs.panel and Panel._refs.panel:IsShown() then
        Panel.Hide()
    else
        Panel.Show()
    end
end

function Panel.IsOpen()
    return Panel._state.panelOpen and Panel._refs.panel and Panel._refs.panel:IsShown()
end

local searchEventFrame = CreateFrame("Frame")
searchEventFrame:RegisterEvent("LFG_LIST_SEARCH_RESULTS_RECEIVED")
searchEventFrame:RegisterEvent("LFG_LIST_SEARCH_RESULT_UPDATED")
searchEventFrame:RegisterEvent("LFG_UPDATE")
searchEventFrame:RegisterEvent("LFG_QUEUE_STATUS_UPDATE")
searchEventFrame:RegisterEvent("LFG_LIST_APPLICATION_STATUS_UPDATED")
searchEventFrame:RegisterEvent("LFG_LIST_ACTIVE_ENTRY_UPDATE")
searchEventFrame:RegisterEvent("LFG_LIST_APPLICANT_LIST_UPDATED")
searchEventFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
searchEventFrame:RegisterEvent("UPDATE_BATTLEFIELD_STATUS")
searchEventFrame:RegisterEvent("PVP_RATED_STATS_UPDATE")
searchEventFrame:RegisterEvent("HONOR_LEVEL_UPDATE")
searchEventFrame:RegisterEvent("UI_ERROR_MESSAGE")
searchEventFrame:RegisterEvent("LFG_LOCK_INFO_RECEIVED")
searchEventFrame:RegisterEvent("LFG_PROPOSAL_SHOW")
searchEventFrame:RegisterEvent("LFG_PROPOSAL_FAILED")
searchEventFrame:RegisterEvent("LFG_PROPOSAL_SUCCEEDED")
searchEventFrame:SetScript("OnEvent", function(self, event, ...)
    if event == "UI_ERROR_MESSAGE" then
        local errType, errMsg = ...
        if Panel._state.dungeonQueueAttempted or Panel._state.raidQueueAttempted then
            Panel._state._queueErrorMsgs = Panel._state._queueErrorMsgs or {}
            if errMsg then
                table.insert(Panel._state._queueErrorMsgs, errMsg)
            end
        end
        return
    elseif event == "LFG_LOCK_INFO_RECEIVED" then
        if Panel._state.activeTab == "raids" and Panel.UpdateRaidFinder then
            Panel.UpdateRaidFinder()
        end
        return
    elseif event == "LFG_PROPOSAL_SHOW" then
        return
    elseif event == "LFG_PROPOSAL_FAILED" then
        if DEFAULT_CHAT_FRAME then
            DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[MidnightUI]|r Queue proposal failed — someone declined or didn't accept.", 1, 0.7, 0.3)
        end
        return
    elseif event == "LFG_PROPOSAL_SUCCEEDED" then
        return
    elseif event == "PLAYER_REGEN_DISABLED" then
        if Panel.IsOpen() then Panel.Hide() end
        return
    elseif event == "LFG_LIST_APPLICATION_STATUS_UPDATED"
        or event == "LFG_LIST_ACTIVE_ENTRY_UPDATE"
        or event == "LFG_LIST_APPLICANT_LIST_UPDATED" then
        if Panel._state.activeTab == "premade" then
            local apps = Panel.GetPendingApplications()
            for _, app in ipairs(apps) do
                local prev = Panel._state.prevAppStatuses[app.resultID]
                if app.status == "invited" and prev ~= "invited" then
                    Panel.ShowInviteBanner(app.name)
                    pcall(PlaySound, SOUNDKIT and SOUNDKIT.READY_CHECK or 8960)
                end
                Panel._state.prevAppStatuses[app.resultID] = app.status
            end
            Panel.UpdateListingRows()
            Panel.UpdateGroupIntelSidebar()
            if _G.MidnightPI and _G.MidnightPI.OnApplicantListUpdated then _G.MidnightPI.OnApplicantListUpdated(Panel._refs.playerSidebar) end
            local R3 = Panel._refs
            if Panel._state.detailOpen and R3.detailPanel and R3.detailPanel._resultID then
                Panel.ShowGroupDetail(R3.detailPanel._resultID)
            end
        end
        return
    elseif event == "LFG_LIST_SEARCH_RESULTS_RECEIVED" then
        Panel.OnSearchResults()
    elseif event == "LFG_LIST_SEARCH_RESULT_UPDATED" then
        local updatedRID = arg1
        if Panel._state.activeTab == "premade" then
            Panel.UpdateListingRows()
            local R4 = Panel._refs
            if Panel._state.detailOpen and R4.detailPanel and R4.detailPanel._resultID then
                local detailRID = R4.detailPanel._resultID
                if updatedRID == nil or updatedRID == detailRID then
                    Panel.ShowGroupDetail(detailRID)
                end
            end
        end
        return
    elseif event == "LFG_UPDATE" or event == "LFG_QUEUE_STATUS_UPDATE" then
        if Panel._state.activeTab == "dungeons" then
            Panel.UpdateDungeonQueueStatus()
        elseif Panel._state.activeTab == "raids" then
            Panel.UpdateRaidQueueStatus()
        elseif Panel._state.activeTab == "pvp" then
            Panel.UpdatePVPQueueStatus()
        end
    elseif event == "UPDATE_BATTLEFIELD_STATUS" then
        if Panel._state.activeTab == "pvp" then
            Panel.UpdatePVPQueueStatus()
        end
    elseif event == "PVP_RATED_STATS_UPDATE" or event == "HONOR_LEVEL_UPDATE" then
        if Panel._state.activeTab == "pvp" then
            Panel.UpdatePVPHero()
            Panel.UpdatePVPActivities()
        end
    end
end)

local hookInstalled = false
local hookGuard = false
local pendingPostCombatSwap = false
local postCombatSwapFrame = nil
local pendingSwapNotice = nil

local function UseBlizzardGroupFinder()
    local s = _G.MidnightUISettings
    return s and s.General and s.General.useBlizzardGroupFinder == true
end

local function PerformPVESwap()
    hookGuard = true
    if PVEFrame and PVEFrame:IsShown() then
        pcall(HideUIPanel, PVEFrame)
    end
    Panel.Toggle()
    C_Timer.After(0, function() hookGuard = false end)
end

local function PerformDeferredPVESwap()
    hookGuard = true
    if PVEFrame and PVEFrame:IsShown() then
        pcall(HideUIPanel, PVEFrame)
    end
    if Panel.IsOpen and not Panel.IsOpen() then
        Panel.Show()
    elseif not Panel.IsOpen then
        Panel.Toggle()
    end
    C_Timer.After(0, function() hookGuard = false end)
end

local HidePendingSwapNotice
local ShowPendingSwapNotice

local function CancelPostCombatSwap()
    if not pendingPostCombatSwap then return end
    pendingPostCombatSwap = false
    if postCombatSwapFrame then
        postCombatSwapFrame:UnregisterEvent("PLAYER_REGEN_ENABLED")
    end
    if HidePendingSwapNotice then HidePendingSwapNotice() end
end

HidePendingSwapNotice = function()
    if pendingSwapNotice and pendingSwapNotice:IsShown() then
        pendingSwapNotice:Hide()
    end
end

local function AnchorPendingSwapNotice(f)
    f:ClearAllPoints()
    local messenger = _G.MyMessengerFrame
    if messenger then
        -- Lock width to Messenger and float just above it.
        f:SetPoint("BOTTOMLEFT", messenger, "TOPLEFT", 0, 6)
        f:SetPoint("BOTTOMRIGHT", messenger, "TOPRIGHT", 0, 6)
    else
        -- Fallback if Messenger hasn't been created yet (shouldn't normally happen).
        f:SetWidth(380)
        f:SetPoint("TOP", UIParent, "TOP", 0, -140)
    end
end

ShowPendingSwapNotice = function()
    if not pendingSwapNotice then
        local f = CreateFrame("Frame", "MidnightUIPendingGroupFinderNotice", UIParent)
        f:SetHeight(38)
        f:SetFrameStrata("HIGH")

        local bg = f:CreateTexture(nil, "BACKGROUND")
        bg:SetAllPoints(f)
        bg:SetColorTexture(0.04, 0.07, 0.11, 0.94)

        local borderTop = f:CreateTexture(nil, "BORDER")
        borderTop:SetColorTexture(0, 0.8, 1.0, 0.85)
        borderTop:SetHeight(1)
        borderTop:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
        borderTop:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)

        local borderBot = f:CreateTexture(nil, "BORDER")
        borderBot:SetColorTexture(0, 0.8, 1.0, 0.85)
        borderBot:SetHeight(1)
        borderBot:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 0)
        borderBot:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)

        local borderLeft = f:CreateTexture(nil, "BORDER")
        borderLeft:SetColorTexture(0, 0.8, 1.0, 0.85)
        borderLeft:SetWidth(1)
        borderLeft:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
        borderLeft:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 0)

        local borderRight = f:CreateTexture(nil, "BORDER")
        borderRight:SetColorTexture(0, 0.8, 1.0, 0.85)
        borderRight:SetWidth(1)
        borderRight:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
        borderRight:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)

        local text = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        text:SetPoint("LEFT", f, "LEFT", 14, 0)
        text:SetPoint("RIGHT", f, "RIGHT", -34, 0)
        text:SetJustifyH("LEFT")
        text:SetText("|cff00ccffMidnightUI Group Finder|r triggered and deferred to end of combat.")

        local close = CreateFrame("Button", nil, f)
        close:SetSize(22, 22)
        close:SetPoint("RIGHT", f, "RIGHT", -8, 0)
        local closeText = close:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        closeText:SetPoint("CENTER", close, "CENTER", 0, 0)
        closeText:SetText("x")
        closeText:SetTextColor(0.7, 0.7, 0.7, 1)
        close:SetScript("OnEnter", function() closeText:SetTextColor(1, 0.5, 0.5, 1) end)
        close:SetScript("OnLeave", function() closeText:SetTextColor(0.7, 0.7, 0.7, 1) end)
        close:SetScript("OnClick", function() CancelPostCombatSwap() end)

        f:Hide()
        pendingSwapNotice = f
    end
    AnchorPendingSwapNotice(pendingSwapNotice)
    pendingSwapNotice:Show()
end

-- Protected frames (PVEFrame, panels) can't be hidden/shown during combat
-- lockdown, so if the player presses I mid-fight we let Blizzard's UI open
-- and queue our swap for when combat ends. The deferred swap fires even if
-- the player manually closed Blizzard's window in combat — pressing I once
-- means they wanted the Group Finder.
local function SchedulePostCombatSwap()
    if pendingPostCombatSwap then return end
    pendingPostCombatSwap = true
    if not postCombatSwapFrame then
        postCombatSwapFrame = CreateFrame("Frame")
        postCombatSwapFrame:SetScript("OnEvent", function(self, event)
            if event ~= "PLAYER_REGEN_ENABLED" then return end
            self:UnregisterEvent("PLAYER_REGEN_ENABLED")
            pendingPostCombatSwap = false
            HidePendingSwapNotice()
            if UseBlizzardGroupFinder() then return end
            if Panel._state._pvpHandoff then return end
            if Panel._state._blizzHandoff then return end
            PerformDeferredPVESwap()
        end)
    end
    postCombatSwapFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
    ShowPendingSwapNotice()
end

local function InstallPVEHook()
    if hookInstalled then return end
    if type(PVEFrame_ToggleFrame) ~= "function" then return end
    hookInstalled = true
    hooksecurefunc("PVEFrame_ToggleFrame", function()
        if UseBlizzardGroupFinder() then return end
        if hookGuard then return end
        if Panel._state._pvpHandoff then return end
        if Panel._state._blizzHandoff then return end
        if InCombatLockdown and InCombatLockdown() then
            if PVEFrame and PVEFrame:IsShown() then
                SchedulePostCombatSwap()
            else
                CancelPostCombatSwap()
            end
            return
        end
        PerformPVESwap()
    end)
end

InstallPVEHook()

local hookEvf = CreateFrame("Frame")
hookEvf:RegisterEvent("ADDON_LOADED")
hookEvf:RegisterEvent("PLAYER_LOGIN")
hookEvf:SetScript("OnEvent", function(_, event, addon)
    if event == "ADDON_LOADED" and (addon == "Blizzard_LookingForGroupUI" or addon == "Blizzard_PVPUI") then
        InstallPVEHook()
        if not UseBlizzardGroupFinder() then
            C_Timer.After(0.05, function()
                if Panel._state._pvpHandoff then return end
                if Panel._state._blizzHandoff then return end
                if PVEFrame and PVEFrame:IsShown() then
                    pcall(HideUIPanel, PVEFrame)
                    if not Panel.IsOpen() then Panel.Show() end
                end
            end)
        end
    elseif event == "PLAYER_LOGIN" then
        InstallPVEHook()
    end
end)

_G.MidnightUI_GroupFinder_Toggle = function() Panel.Toggle() end
_G.MidnightUI_GroupFinder_Show   = function(tab) Panel.Show(tab) end

-- ── Auto-refresh hero pills on spec change ─────────────────────
-- When the player swaps spec (Bear -> Resto, Frost -> Demo, etc.) the active
-- role flips, which means the MPI pill should display a different role's
-- score. PLAYER_SPECIALIZATION_CHANGED fires for any group member; we only
-- react when it's the player. ACTIVE_TALENT_GROUP_CHANGED catches the older
-- dual-spec swap path. The pills are no-ops if the panel isn't shown, so
-- this is safe to fire whenever — we just thrash slightly less by gating
-- on visibility.
local mpiSpecEvFrame = CreateFrame("Frame")
mpiSpecEvFrame:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
mpiSpecEvFrame:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED")
mpiSpecEvFrame:SetScript("OnEvent", function(_, event, unit)
    if event == "PLAYER_SPECIALIZATION_CHANGED" and unit and unit ~= "player" then
        return
    end
    local r = Panel._refs
    if not r or not r.panel or not r.panel:IsShown() then return end
    local function refresh()
        if Panel.UpdateDungeonHero        then pcall(Panel.UpdateDungeonHero) end
        if Panel.UpdateDungeonSuggestions then pcall(Panel.UpdateDungeonSuggestions) end  -- routes to _applyMpiToPill
        if Panel.UpdateRaidHero           then pcall(Panel.UpdateRaidHero) end
        if Panel.UpdatePremadeHero        then pcall(Panel.UpdatePremadeHero) end          -- routes to _applyMpiToPill
        if Panel.AutoDetectDungeonRoles   then pcall(Panel.AutoDetectDungeonRoles) end
        if Panel.AutoDetectRaidRoles      then pcall(Panel.AutoDetectRaidRoles) end
    end
    if C_Timer and C_Timer.After then
        -- Defer one frame so GetSpecialization() returns the post-swap value.
        C_Timer.After(0, refresh)
    else
        refresh()
    end
end)
_G.MidnightUI_GroupFinder_Hide   = function() Panel.Hide() end
