
-- ============================================================
-- FILE: GuildPanel.lua
-- PURPOSE: Custom guild roster panel. Replaces Blizzard's
--   GuildFrame with a styled roster view showing members by
--   rank, class color-coded names, level, zone, online/offline
--   status, public/officer notes, and last-online time.
--   Includes search, rank filter, and invite button.
-- HOW IT WORKS:
--   1. Panel.EnsurePanel() — lazy-creates MidnightUI_GuildPanel
--      with header (guild name, MOTD), search EditBox, rank
--      filter dropdown, scroll frame + member rows.
--   2. Panel.Refresh() — calls GuildRoster() to request fresh
--      data, then iterates GetGuildRosterInfo(1..N) to build
--      a sorted member list, and populates scroll child rows.
--   3. Member rows: name (class color), level, spec icon,
--      zone text, online indicator, note tooltip on hover.
--      Right-click context menu: Whisper, Invite to Group,
--      View/Edit Note, Copy Name. (Rank changes go through
--      the Roster header button which opens Blizzard's
--      Communities roster — the only non-tainted path for
--      Promote/Demote in retail.)
--   4. evf OnEvent: GUILD_ROSTER_UPDATE → Panel.Refresh();
--      PLAYER_ENTERING_WORLD → boot + initial refresh;
--      PLAYER_GUILD_UPDATE → re-check guild membership.
-- DEPENDS ON:
--   IsInGuild, GetGuildInfo, GetNumGuildMembers,
--   GetGuildRosterInfo, GuildRoster, SendChatMessage
-- EXPORTS: _G.MidnightUI_GuildPanelAPI (public API table)
-- OWNED SAVEDVARIABLES: none
-- EVENTS REGISTERED:
--   GUILD_ROSTER_UPDATE, PLAYER_ENTERING_WORLD
--   PLAYER_GUILD_UPDATE, ADDON_LOADED
-- FRAMES CREATED:
--   MidnightUI_GuildPanel (main panel)
--   Per-member: pooled roster row frames
-- GOTCHAS:
--   GuildRoster() is an async request — data is not
--   immediately available; wait for GUILD_ROSTER_UPDATE.
--   GetGuildRosterInfo returns 17+ values; indices matter
--   (name=1, rank=3, level=4, zone=5, note=7, online=9).
--   hooksecurefunc on GuildFrame Show suppresses Blizzard panel.
-- ============================================================

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

local pcall, type, pairs, ipairs, math, string, table, select, tostring =
      pcall, type, pairs, ipairs, math, string, table, select, tostring
local CreateFrame, UIParent, GameTooltip = CreateFrame, UIParent, GameTooltip
local IsInGuild, GetGuildInfo, GetNumGuildMembers = IsInGuild, GetGuildInfo, GetNumGuildMembers
local GetGuildRosterInfo, GetGuildRosterMOTD, GuildRoster = GetGuildRosterInfo, GetGuildRosterMOTD, GuildRoster
local SendChatMessage = SendChatMessage
local InviteUnit = InviteUnit or (C_PartyInfo and C_PartyInfo.InviteUnit)
local RAID_CLASS_COLORS = RAID_CLASS_COLORS
local hooksecurefunc = hooksecurefunc

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

-- Guild rank action via Blizzard macro + user-bound hotkey.
-- Writing a /gpromote macro and binding a key is safe; the rank change fires
-- from the user's hardware keypress, which is untainted by addon code.
-- Promote and Demote use separate macros and separate hotkeys so they can't be
-- confused.
local GUILD_ACTION_MACROS = {
    motd      = { macro = "MidnightUI_GuildMOTD", icon = "INV_Misc_QuestionMark", slash = "/gmotd", hotkeyKey = "motdHotkey",      label = "Set MOTD" },
    rankQueue = { macro = "MidnightUI_RankQueue", icon = "INV_Misc_QuestionMark", slash = nil,      hotkeyKey = "rankQueueHotkey", label = "Promote / Demote" },
}
local RANK_QUEUE_MAX = 10
local RANK_QUEUE_COOLDOWN = 3 -- seconds between macro fires
local _hotkeyPromptFrame

local function PromptForHotkey(actionLabel, onDone)
    if _hotkeyPromptFrame then _hotkeyPromptFrame:Hide() end
    local f = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    _hotkeyPromptFrame = f
    f:SetSize(380, 130)
    f:SetPoint("CENTER")
    f:SetFrameStrata("TOOLTIP")
    f:SetFrameLevel(500)
    f:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    f:SetBackdropColor(0.07, 0.06, 0.04, 0.97)
    f:SetBackdropBorderColor(0.72, 0.62, 0.42, 0.8)

    local title = f:CreateFontString(nil, "OVERLAY")
    TrySetFont(title, TITLE_FONT, 14, "OUTLINE")
    title:SetPoint("TOP", f, "TOP", 0, -14)
    title:SetText("Bind a hotkey for " .. (actionLabel or "rank action"))
    title:SetTextColor(0.96, 0.87, 0.58)

    local instr = f:CreateFontString(nil, "OVERLAY")
    TrySetFont(instr, BODY_FONT, 11, "")
    instr:SetPoint("TOP", title, "BOTTOM", 0, -12)
    instr:SetWidth(340)
    instr:SetJustifyH("CENTER")
    instr:SetText("Press any key (optionally with Ctrl/Alt/Shift) to bind.\nPress this key later to confirm the " .. (actionLabel or "action") .. ".\nEscape to cancel.")
    instr:SetTextColor(0.94, 0.90, 0.80)

    f:EnableKeyboard(true)
    f:SetPropagateKeyboardInput(false)
    f:SetScript("OnKeyDown", function(self, key)
        if key == "ESCAPE" then
            self:Hide()
            if onDone then onDone(nil) end
            return
        end
        if key == "LSHIFT" or key == "RSHIFT" or key == "LCTRL" or key == "RCTRL"
            or key == "LALT" or key == "RALT" or key == "UNKNOWN" then
            return
        end
        local parts = {}
        if IsAltKeyDown() then table.insert(parts, "ALT") end
        if IsControlKeyDown() then table.insert(parts, "CTRL") end
        if IsShiftKeyDown() then table.insert(parts, "SHIFT") end
        table.insert(parts, key)
        self:Hide()
        if onDone then onDone(table.concat(parts, "-")) end
    end)
    f:Show()
    return f
end


-- Rank-change queue — lets officers stack many promotes/demotes visually and
-- commit the whole batch with a single macro + hotkey. Keyed by fullName.
local RANK_QUEUE = {}
-- Names processed by the most recent macro fire. RMM uses this to drop them
-- from the queue so spam-pressing the hotkey can't re-send the same commands.
local LAST_BATCH = {}
-- Time (GetTime()) when the cooldown lifts; rebuilds are held until then.
local RANK_QUEUE_COOLDOWN_UNTIL = 0
-- true once the macro is built & bound, ready for the hotkey. false after a
-- fire or any queue mutation — user must click Commit to arm again.
local RANK_QUEUE_ARMED = false

local function GuildMaxRankIndex()
    local maxRank = 0
    if GuildControlGetNumRanks then
        local n = SafeCall(GuildControlGetNumRanks) or 0
        if n > 0 then maxRank = n - 1 end
    end
    if maxRank == 0 and GetNumGuildMembers and GetGuildRosterInfo then
        local total = SafeCall(GetNumGuildMembers) or 0
        for i = 1, total do
            local _, _, ri = SafeCall(GetGuildRosterInfo, i)
            if type(ri) == "number" and ri > maxRank then maxRank = ri end
        end
    end
    return maxRank
end

local function RankNameForIndex(idx0)
    if GuildControlGetRankName and type(idx0) == "number" then
        local n = SafeCall(GuildControlGetRankName, idx0 + 1)
        if n and n ~= "" then return n end
    end
    return nil
end

local function RankQueueCount()
    local n = 0
    for _ in pairs(RANK_QUEUE) do n = n + 1 end
    return n
end

-- Suppress Blizzard's autocomplete popup when the user is typing /gpromote or
-- /gdemote — guild-member autocomplete fills just "Name" but these commands
-- need "Name-Realm" on connected realms, and the popup's token-lock prevents
-- hand-editing the realm suffix.
local _autocompleteHookInstalled = false
local function InstallPromoteAutocompleteSuppressor()
    if _autocompleteHookInstalled then return end
    local eb = _G.ChatFrame1EditBox
    if not eb or not eb.HookScript then return end
    _autocompleteHookInstalled = true

    -- Cache original autocomplete state so we can restore it once the user
    -- backs out of the /gpromote or /gdemote command.
    local saved = { src = nil, params = nil, context = nil, hadSaved = false }

    eb:HookScript("OnTextChanged", function(self)
        local text = self:GetText() or ""
        local rankCmd = text:find("^/gpromote") or text:find("^/gdemote")
        if rankCmd then
            if not saved.hadSaved then
                saved.src = self.autoCompleteSource
                saved.params = self.autoCompleteParams
                saved.context = self.autoCompleteContext
                saved.hadSaved = true
            end
            self.autoCompleteSource = nil
            self.autoCompleteParams = nil
            self.autoCompleteContext = nil
            if _G.AutoCompleteBox and _G.AutoCompleteBox:IsShown() then
                _G.AutoCompleteBox:Hide()
            end
        elseif saved.hadSaved then
            self.autoCompleteSource = saved.src
            self.autoCompleteParams = saved.params
            self.autoCompleteContext = saved.context
            saved.src, saved.params, saved.context = nil, nil, nil
            saved.hadSaved = false
        end
    end)
end
InstallPromoteAutocompleteSuppressor()
if C_Timer and C_Timer.After then
    C_Timer.After(2, InstallPromoteAutocompleteSuppressor)
end

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

local function GetClassColor(classFile)
    if not classFile or not RAID_CLASS_COLORS then return 0.8, 0.8, 0.8 end
    local cc = RAID_CLASS_COLORS[classFile]
    if cc then return cc.r, cc.g, cc.b end
    return 0.8, 0.8, 0.8
end

local THEMES = {
    parchment = {
        frameBg     = { 0.07, 0.06, 0.04, 0.97 },
        headerBg    = { 0.09, 0.07, 0.04, 0.95 },
        heroBg      = { 0.10, 0.08, 0.05, 0.92 },
        panelBg     = { 0.08, 0.07, 0.05, 0.95 },
        chatBg      = { 0.06, 0.05, 0.03, 0.95 },
        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 },
        inputBg     = { 0.06, 0.05, 0.03, 0.9 },
        hoverBg     = { 0.18, 0.15, 0.10, 0.4 },
        borderTint  = { 0.72, 0.62, 0.42, 0.6 },
    },
    midnight = {
        frameBg     = { 0.06, 0.07, 0.12, 0.97 },
        headerBg    = { 0.07, 0.08, 0.14, 0.95 },
        heroBg      = { 0.08, 0.09, 0.16, 0.92 },
        panelBg     = { 0.06, 0.07, 0.13, 0.95 },
        chatBg      = { 0.04, 0.05, 0.10, 0.95 },
        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.25, 0.35, 0.55 },
        inputBg     = { 0.05, 0.06, 0.10, 0.9 },
        hoverBg     = { 0.10, 0.14, 0.25, 0.4 },
        borderTint  = { 0.00, 0.78, 1.00, 0.5 },
    },
    class = {
        frameBg     = { 0.07, 0.06, 0.05, 0.97 },
        headerBg    = { 0.09, 0.07, 0.06, 0.95 },
        heroBg      = { 0.10, 0.08, 0.06, 0.92 },
        panelBg     = { 0.08, 0.07, 0.06, 0.95 },
        chatBg      = { 0.06, 0.05, 0.04, 0.95 },
        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 },
        inputBg     = { 0.06, 0.05, 0.04, 0.9 },
        hoverBg     = { 0.16, 0.14, 0.12, 0.4 },
        borderTint  = { 0.72, 0.62, 0.42, 0.6 },
    },
}

local C = {}
local function LoadTheme()
    local s = _G.MidnightUISettings
    local key = (s and s.General and s.General.characterPanelTheme) or "parchment"
    local t = THEMES[key] or THEMES.parchment
    for k, v in pairs(t) do C[k] = v end
    if key == "class" then
        local cc = _G.MidnightUI_Core and _G.MidnightUI_Core.GetClassColorTable
            and _G.MidnightUI_Core.GetClassColorTable("player")
        if cc then
            local r, g, b = cc.r or cc[1] or 0.72, cc.g or cc[2] or 0.62, cc.b or cc[3] or 0.42
            C.accent = { r, g, b }
            C.borderTint = { r, g, b, 0.6 }
        end
    end
    C.online  = { 0.40, 0.85, 0.40 }
    C.offline = { 0.50, 0.50, 0.50 }
end
LoadTheme()

local CFG = {
    WIDTH           = 1200,
    HEIGHT          = 780,
    HEADER_H        = 44,
    HERO_H          = 90,
    ROSTER_PCT      = 0.48,
    CHAT_PCT        = 0.52,
    SEARCH_H        = 28,
    SORT_HDR_H      = 24,
    ROSTER_ROW_H    = 24,
    SECTION_HDR_H   = 26,
    CHAT_INPUT_H    = 32,
    CHAT_MSG_H      = 18,
    PAD             = 12,
    STRATA          = "HIGH",
    COL_LVL         = 30,
    COL_RANK        = 90,
    COL_ZONE        = 110,
    COL_LASTON      = 50,
}

local function CreateDropShadow(frame, intensity)
    intensity = intensity or 5
    for i = 1, intensity do
        local s = CreateFrame("Frame", nil, frame)
        s:SetFrameLevel(math.max(0, frame:GetFrameLevel() - 1))
        local off = i * 0.8
        local a = (0.18 - (i * 0.025)) * (intensity / 6)
        s:SetPoint("TOPLEFT", -off, off)
        s:SetPoint("BOTTOMRIGHT", off, -off)
        local t = s:CreateTexture(nil, "BACKGROUND")
        t:SetAllPoints(); t:SetColorTexture(0, 0, 0, a)
    end
end

local Panel = {}
Panel._state = {
    initialized   = false,
    panelOpen     = false,
    searchText    = "",
    sortColumn    = "rank",
    sortAsc       = true,
    onlineExpanded      = true,
    offlineExpanded     = false,
    challengesExpanded  = false,
    newsExpanded        = false,
    filterMode          = "default",
}
Panel._refs = {}

local DetectMPlusMessage
local ShowMPlusToast
local ShowLFGNotification

local function GetGuildData()
    if not IsInGuild() then return nil end
    local guildName, rankName, rankIndex, realm = SafeCall(GetGuildInfo, "player")
    local total, online, onlineMobile = SafeCall(GetNumGuildMembers)
    local motd = SafeCall(GetGuildRosterMOTD) or ""
    return {
        name = guildName or "Unknown Guild",
        realm = realm or SafeCall(GetRealmName) or "",
        rankName = rankName or "",
        rankIndex = rankIndex or 0,
        totalMembers = total or 0,
        onlineMembers = online or 0,
        onlineMobile = onlineMobile or 0,
        motd = motd,
    }
end

local function CollectRoster(searchText, sortCol, sortAsc)
    local online, offline = {}, {}
    local total = SafeCall(GetNumGuildMembers) or 0
    local search = (searchText and searchText ~= "") and searchText:lower() or nil

    for i = 1, total do
        local ok, name, rankName, rankIndex, level, classLocalized,
              zone, publicNote, officerNote, isOnline, status,
              classFile, achievementPoints, achievementRank, isMobile,
              canSoR, repStanding, guid
        if GetGuildRosterInfo then
            ok, name, rankName, rankIndex, level, classLocalized,
                zone, publicNote, officerNote, isOnline, status,
                classFile, achievementPoints, achievementRank, isMobile,
                canSoR, repStanding, guid = pcall(GetGuildRosterInfo, i)
            if not ok then name = nil end
        end

        local lastOnline = ""
        if name and not isOnline and not isMobile and GetGuildRosterLastOnline then
            local okLO, years, months, days, hours = pcall(GetGuildRosterLastOnline, i)
            if okLO then
                if (years or 0) > 0 then lastOnline = years .. "y"
                elseif (months or 0) > 0 then lastOnline = months .. "mo"
                elseif (days or 0) > 0 then lastOnline = days .. "d"
                elseif (hours or 0) > 0 then lastOnline = hours .. "h"
                else lastOnline = "<1h"
                end
            end
        end

        if name then
            local shortName = name:match("^([^%-]+)") or name
            if not search or shortName:lower():find(search, 1, true) then
                local entry = {
                    fullName        = name,
                    name            = shortName,
                    rankName        = rankName or "",
                    rankIndex       = rankIndex or 99,
                    level           = level or 0,
                    classFile       = classFile or classLocalized or "WARRIOR",
                    className       = classLocalized or "",
                    zone            = zone or "",
                    publicNote      = publicNote or "",
                    officerNote     = officerNote or "",
                    isOnline        = isOnline or false,
                    isMobile        = isMobile or false,
                    lastOnline      = lastOnline,
                    achievementPts  = achievementPoints or 0,
                    guid            = guid or "",
                    rosterIndex     = i,
                    mythicPlusRating = 0,
                }
                if (isOnline or isMobile) and guid and guid ~= "" then
                    if C_PlayerInfo and C_PlayerInfo.GetPlayerMythicPlusRatingSummary and PlayerLocation then
                        local okLoc, playerLoc = pcall(PlayerLocation.CreateFromGUID, PlayerLocation, guid)
                        if okLoc and playerLoc then
                            local okSum, summary = pcall(C_PlayerInfo.GetPlayerMythicPlusRatingSummary, playerLoc)
                            if okSum and summary and summary.currentSeasonScore then
                                entry.mythicPlusRating = summary.currentSeasonScore
                            end
                        end
                    end
                end
                if _G.MidnightPI then
                    local PI = _G.MidnightPI
                    local prof = PI.GetProfileByName and PI.GetProfileByName(entry.name)
                    if not prof and PI.CommunityLookup then
                        local realm = entry.fullName and entry.fullName:match("%-(.+)$") or (GetRealmName and GetRealmName() or "")
                        prof = PI.CommunityLookup(entry.name, realm)
                    end
                    entry._mpiScore = prof and prof.avgMPI or 0
                end
                if isOnline or isMobile then
                    online[#online + 1] = entry
                else
                    offline[#offline + 1] = entry
                end
            end
        end
    end

    local function Sorter(a, b)
        if sortCol == "name" then
            if sortAsc then return a.name:lower() < b.name:lower() else return a.name:lower() > b.name:lower() end
        elseif sortCol == "level" then
            if sortAsc then return a.level > b.level else return a.level < b.level end
        elseif sortCol == "rank" then
            if sortAsc then return a.rankIndex < b.rankIndex else return a.rankIndex > b.rankIndex end
        elseif sortCol == "zone" then
            if sortAsc then return a.zone:lower() < b.zone:lower() else return a.zone:lower() > b.zone:lower() end
        elseif sortCol == "class" then
            if sortAsc then return a.classFile < b.classFile else return a.classFile > b.classFile end
        elseif sortCol == "lastOn" then
            local filterMode = Panel._state.filterMode or "default"
            if filterMode == "mythicPlus" then
                local aS, bS = a.mythicPlusRating or 0, b.mythicPlusRating or 0
                if sortAsc then return aS > bS else return aS < bS end
            elseif filterMode == "achievePts" then
                local aP, bP = a.achievementPts or 0, b.achievementPts or 0
                if sortAsc then return aP > bP else return aP < bP end
            elseif filterMode == "mpiScore" then
                local aM, bM = a._mpiScore or 0, b._mpiScore or 0
                if sortAsc then return aM > bM else return aM < bM end
            end
            if sortAsc then return a.lastOnline < b.lastOnline else return a.lastOnline > b.lastOnline end
        end
        return a.rankIndex < b.rankIndex
    end

    table.sort(online, Sorter)
    table.sort(offline, Sorter)
    return online, offline
end

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

    local p = CreateFrame("Frame", "MidnightUI_GuildPanel", UIParent, "BackdropTemplate")
    p:SetSize(CFG.WIDTH, CFG.HEIGHT)
    p:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
    p:SetFrameStrata(CFG.STRATA)
    p:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    p:SetBackdropColor(C.frameBg[1], C.frameBg[2], C.frameBg[3], C.frameBg[4])
    p:SetBackdropBorderColor(C.borderTint[1], C.borderTint[2], C.borderTint[3], C.borderTint[4])
    p:Hide()
    R.panel = p
    CreateDropShadow(p, 5)

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

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

    local hdr = CreateFrame("Frame", nil, p)
    hdr:SetHeight(CFG.HEADER_H); hdr:SetPoint("TOPLEFT", 0, 0); hdr:SetPoint("TOPRIGHT", 0, 0)
    local hdrBg = hdr:CreateTexture(nil, "BACKGROUND"); hdrBg:SetAllPoints()
    hdrBg:SetColorTexture(C.headerBg[1], C.headerBg[2], C.headerBg[3], C.headerBg[4])
    R.header = hdr; R.hdrBg = hdrBg

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

    local titleFS = hdr:CreateFontString(nil, "OVERLAY")
    TrySetFont(titleFS, TITLE_FONT, 16, "OUTLINE")
    titleFS:SetPoint("LEFT", hdr, "LEFT", 16, 0)
    titleFS:SetTextColor(C.titleText[1], C.titleText[2], C.titleText[3])
    R.headerTitle = titleFS

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

    local lfgBtn = CreateFrame("Button", nil, hdr)
    lfgBtn:SetSize(24, 24); lfgBtn:SetPoint("RIGHT", closeBtn, "LEFT", -8, 0)
    local lfgIcon = lfgBtn:CreateTexture(nil, "OVERLAY")
    lfgIcon:SetAllPoints()
    lfgIcon:SetTexture("Interface\\Icons\\INV_Relics_Hourglass")
    lfgIcon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
    lfgIcon:SetVertexColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.70)
    lfgBtn:SetScript("OnEnter", function(self)
        lfgIcon:SetVertexColor(C.titleText[1], C.titleText[2], C.titleText[3])
        GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
        GameTooltip:SetText("Find M+ Group", 1, 1, 1)
        GameTooltip:AddLine("Create a Mythic+ listing for your guild", C.mutedText[1], C.mutedText[2], C.mutedText[3])
        GameTooltip:Show()
    end)
    lfgBtn:SetScript("OnLeave", function()
        lfgIcon:SetVertexColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.70)
        GameTooltip:Hide()
    end)
    lfgBtn:SetScript("OnClick", function()
        Panel.ShowLFGOverlay()
    end)

    local perksBtn = CreateFrame("Button", nil, hdr)
    perksBtn:SetSize(24, 24); perksBtn:SetPoint("RIGHT", lfgBtn, "LEFT", -6, 0)
    local perksIcon = perksBtn:CreateTexture(nil, "OVERLAY")
    perksIcon:SetAllPoints(); perksIcon:SetTexture("Interface\\Icons\\Achievement_GuildPerk_MobileBanking")
    perksIcon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
    perksIcon:SetVertexColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.70)
    perksBtn:SetScript("OnEnter", function(self)
        perksIcon:SetVertexColor(C.titleText[1], C.titleText[2], C.titleText[3])
        GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
        GameTooltip:SetText("Guild Perks & Rewards", 1, 1, 1)
        GameTooltip:Show()
    end)
    perksBtn:SetScript("OnLeave", function()
        perksIcon:SetVertexColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.70)
        GameTooltip:Hide()
    end)
    perksBtn:SetScript("OnClick", function()
        Panel.TogglePerks()
    end)

    local recruitBtn = CreateFrame("Button", nil, hdr)
    recruitBtn:SetSize(24, 24); recruitBtn:SetPoint("RIGHT", perksBtn, "LEFT", -6, 0)
    local recruitIcon = recruitBtn:CreateTexture(nil, "OVERLAY")
    recruitIcon:SetAllPoints(); recruitIcon:SetTexture("Interface\\Icons\\INV_Misc_GroupLooking")
    recruitIcon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
    recruitIcon:SetVertexColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.70)
    recruitBtn:SetScript("OnEnter", function(self)
        recruitIcon:SetVertexColor(C.titleText[1], C.titleText[2], C.titleText[3])
        GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
        GameTooltip:SetText("Guild Recruitment", 1, 1, 1)
        GameTooltip:AddLine("Manage recruitment or review applications", C.mutedText[1], C.mutedText[2], C.mutedText[3])
        GameTooltip:Show()
    end)
    recruitBtn:SetScript("OnLeave", function()
        recruitIcon:SetVertexColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.70)
        GameTooltip:Hide()
    end)
    recruitBtn:SetScript("OnClick", function()
        local api = _G.MidnightUI_GuildRecruitAPI
        if api and api.CanManageRecruitment and api.CanManageRecruitment() then
            api.ShowConfigOverlay()
        end
    end)
    recruitBtn:Hide()

    local badgeDot = recruitBtn:CreateTexture(nil, "OVERLAY", nil, 7)
    badgeDot:SetSize(10, 10); badgeDot:SetPoint("TOPRIGHT", recruitBtn, "TOPRIGHT", 3, 3)
    badgeDot:SetColorTexture(0.90, 0.30, 0.30, 0.9)
    badgeDot:Hide()

    local badgeCount = recruitBtn:CreateFontString(nil, "OVERLAY", nil, 8)
    TrySetFont(badgeCount, BODY_FONT, 8, "OUTLINE")
    badgeCount:SetPoint("CENTER", badgeDot, "CENTER", 0, 0)
    badgeCount:SetTextColor(1, 1, 1)
    badgeCount:Hide()

    R.recruitBtn = recruitBtn
    R.recruitBadgeDot = badgeDot
    R.recruitBadgeCount = badgeCount
    R.perksBtn = perksBtn

    local gearBtn = CreateFrame("Button", nil, hdr)
    gearBtn:SetSize(24, 24); gearBtn:SetPoint("RIGHT", recruitBtn, "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(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.70)
    gearBtn:SetScript("OnEnter", function() gearIcon:SetVertexColor(C.titleText[1], C.titleText[2], C.titleText[3]) end)
    gearBtn:SetScript("OnLeave", function() gearIcon:SetVertexColor(C.mutedText[1], C.mutedText[2], C.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

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

    local popupTitle = settingsPopup:CreateFontString(nil, "OVERLAY")
    TrySetFont(popupTitle, BODY_FONT, 10, "OUTLINE")
    popupTitle:SetPoint("TOP", settingsPopup, "TOP", 0, -72)
    popupTitle:SetText("Theme"); popupTitle:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3])

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

    local scaleHeader = settingsPopup:CreateFontString(nil, "OVERLAY")
    TrySetFont(scaleHeader, BODY_FONT, 10, "OUTLINE")
    scaleHeader:SetPoint("TOP", settingsPopup, "TOP", 0, -8)
    scaleHeader:SetText("UI Scale")
    scaleHeader:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3])

    local scaleRow = CreateFrame("Frame", nil, settingsPopup)
    scaleRow:SetSize(160, 28)
    scaleRow:SetPoint("TOP", scaleHeader, "BOTTOM", 0, -6)

    local function MakeStep(label)
        local b = CreateFrame("Button", nil, scaleRow, "BackdropTemplate")
        b:SetSize(28, 22)
        b:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        b:SetBackdropColor(C.panelBg[1], C.panelBg[2], C.panelBg[3], 0.9)
        b:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.5)
        local fs = b:CreateFontString(nil, "OVERLAY")
        TrySetFont(fs, BODY_FONT, 13, "OUTLINE")
        fs:SetPoint("CENTER"); fs:SetText(label)
        fs:SetTextColor(C.titleText[1], C.titleText[2], C.titleText[3])
        b:SetScript("OnEnter", function() b:SetBackdropColor(C.accent[1], C.accent[2], C.accent[3], 0.3) end)
        b:SetScript("OnLeave", function() b:SetBackdropColor(C.panelBg[1], C.panelBg[2], C.panelBg[3], 0.9) end)
        return b
    end

    local minusBtn = MakeStep("-")
    minusBtn:SetPoint("LEFT", scaleRow, "LEFT", 10, 0)

    local plusBtn = MakeStep("+")
    plusBtn:SetPoint("RIGHT", scaleRow, "RIGHT", -10, 0)

    local scaleValFS = scaleRow:CreateFontString(nil, "OVERLAY")
    TrySetFont(scaleValFS, BODY_FONT, 13, "OUTLINE")
    scaleValFS:SetPoint("CENTER")
    scaleValFS:SetTextColor(C.titleText[1], C.titleText[2], C.titleText[3])

    local MIN_SCALE, MAX_SCALE, STEP = 0.70, 1.50, 0.05
    local function CurrentScale()
        local s = _G.MidnightUISettings
        s.GuildPanel = s.GuildPanel or {}
        return s.GuildPanel.uiScale or 1.0
    end
    local function SetScaleValue(newVal)
        newVal = math.max(MIN_SCALE, math.min(MAX_SCALE, math.floor(newVal * 100 + 0.5) / 100))
        local s = _G.MidnightUISettings
        s.GuildPanel = s.GuildPanel or {}
        s.GuildPanel.uiScale = newVal
        scaleValFS:SetText(string.format("%.2f", newVal))
        if p and p.SetScale then p:SetScale(newVal) end
    end
    scaleValFS:SetText(string.format("%.2f", CurrentScale()))
    minusBtn:SetScript("OnClick", function() SetScaleValue(CurrentScale() - STEP) end)
    plusBtn:SetScript("OnClick",  function() SetScaleValue(CurrentScale() + STEP) end)
    R.guildScaleValFS = scaleValFS

    local settingsOverlay = CreateFrame("Button", nil, UIParent)
    settingsOverlay:SetAllPoints(UIParent)
    settingsOverlay:SetFrameStrata(settingsPopup:GetFrameStrata())
    settingsOverlay:SetFrameLevel(settingsPopup:GetFrameLevel() - 1)
    settingsOverlay:RegisterForClicks("AnyUp")
    settingsOverlay:SetScript("OnClick", function() settingsPopup:Hide() end)
    settingsOverlay:Hide()
    settingsPopup:HookScript("OnShow", function() settingsOverlay:Show() end)
    settingsPopup:HookScript("OnHide", function() settingsOverlay:Hide() end)

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

    local rosterW = math.floor(CFG.WIDTH * CFG.ROSTER_PCT)
    local contentH = CFG.HEIGHT - CFG.HEADER_H
    local halfH = math.floor(contentH / 2)

    local hero = CreateFrame("Frame", nil, p)
    hero:SetHeight(halfH)
    hero:SetPoint("TOPLEFT", hdr, "BOTTOMLEFT", 0, 0)
    hero:SetWidth(rosterW)
    R.hero = hero

    local heroBg = hero:CreateTexture(nil, "BACKGROUND", nil, -8)
    heroBg:SetAllPoints()
    heroBg:SetColorTexture(C.heroBg[1], C.heroBg[2], C.heroBg[3], C.heroBg[4])
    R.heroBg = heroBg

    local heroAtlas = hero:CreateTexture(nil, "BACKGROUND", nil, -7)
    heroAtlas:SetAllPoints()
    local atlasOk = pcall(heroAtlas.SetAtlas, heroAtlas, "UI-Frame-Bar-Fill-Yellow", false)
    if not atlasOk then heroAtlas:SetColorTexture(0, 0, 0, 0) end
    heroAtlas:SetAlpha(0.03)
    heroAtlas:SetVertexColor(C.accent[1], C.accent[2], C.accent[3])

    local heroGlow = hero:CreateTexture(nil, "BACKGROUND", nil, -6)
    heroGlow:SetHeight(60)
    heroGlow:SetPoint("TOPLEFT", hero, "TOPLEFT", 0, 0)
    heroGlow:SetPoint("TOPRIGHT", hero, "TOPRIGHT", 0, 0)
    heroGlow:SetTexture(W8)
    if heroGlow.SetGradient and CreateColor then
        heroGlow:SetGradient("VERTICAL",
            CreateColor(0, 0, 0, 0),
            CreateColor(C.accent[1], C.accent[2], C.accent[3], 0.12))
    end
    R.heroGlow = heroGlow

    local heroVig = hero:CreateTexture(nil, "BACKGROUND", nil, -6)
    heroVig:SetHeight(40)
    heroVig:SetPoint("BOTTOMLEFT", hero, "BOTTOMLEFT", 0, 0)
    heroVig:SetPoint("BOTTOMRIGHT", hero, "BOTTOMRIGHT", 0, 0)
    heroVig:SetTexture(W8)
    if heroVig.SetGradient and CreateColor then
        heroVig:SetGradient("VERTICAL",
            CreateColor(C.heroBg[1], C.heroBg[2], C.heroBg[3], 0.8),
            CreateColor(0, 0, 0, 0))
    end

    local emblemFrame = CreateFrame("Frame", nil, hero)
    emblemFrame:SetSize(56, 56)
    emblemFrame:SetPoint("TOPLEFT", hero, "TOPLEFT", CFG.PAD + 4, -CFG.PAD)

    local emblemGlow = emblemFrame:CreateTexture(nil, "BACKGROUND")
    emblemGlow:SetSize(64, 64)
    emblemGlow:SetPoint("CENTER")
    emblemGlow:SetTexture("Interface\\COMMON\\RingBorder")
    emblemGlow:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 0.25)
    emblemGlow:SetBlendMode("ADD")
    R.emblemGlow = emblemGlow

    local emblemBg = emblemFrame:CreateTexture(nil, "ARTWORK", nil, 0)
    emblemBg:SetSize(48, 48)
    emblemBg:SetPoint("CENTER")
    emblemBg:SetColorTexture(0, 0, 0, 0.5)

    local tabardModel = CreateFrame("PlayerModel", nil, emblemFrame)
    tabardModel:SetSize(52, 52)
    tabardModel:SetPoint("CENTER")
    tabardModel:SetFrameLevel(emblemFrame:GetFrameLevel() + 1)
    R.tabardModel = tabardModel

    local emblemIcon = emblemFrame:CreateTexture(nil, "ARTWORK", nil, 1)
    emblemIcon:SetSize(44, 44)
    emblemIcon:SetPoint("CENTER")
    emblemIcon:Hide()
    R.emblemIcon = emblemIcon

    local eBorders = {}
    eBorders[1] = emblemFrame:CreateTexture(nil, "OVERLAY"); eBorders[1]:SetHeight(1)
    eBorders[1]:SetPoint("TOPLEFT", emblemBg, -1, 1); eBorders[1]:SetPoint("TOPRIGHT", emblemBg, 1, 1)
    eBorders[2] = emblemFrame:CreateTexture(nil, "OVERLAY"); eBorders[2]:SetHeight(1)
    eBorders[2]:SetPoint("BOTTOMLEFT", emblemBg, -1, -1); eBorders[2]:SetPoint("BOTTOMRIGHT", emblemBg, 1, -1)
    eBorders[3] = emblemFrame:CreateTexture(nil, "OVERLAY"); eBorders[3]:SetWidth(1)
    eBorders[3]:SetPoint("TOPLEFT", emblemBg, -1, 1); eBorders[3]:SetPoint("BOTTOMLEFT", emblemBg, -1, -1)
    eBorders[4] = emblemFrame:CreateTexture(nil, "OVERLAY"); eBorders[4]:SetWidth(1)
    eBorders[4]:SetPoint("TOPRIGHT", emblemBg, 1, 1); eBorders[4]:SetPoint("BOTTOMRIGHT", emblemBg, 1, -1)
    for _, b in ipairs(eBorders) do b:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.4) end
    R.emblemBorders = eBorders

    local guildNameFS = hero:CreateFontString(nil, "OVERLAY")
    TrySetFont(guildNameFS, TITLE_FONT, 22, "OUTLINE")
    guildNameFS:SetPoint("TOPLEFT", emblemFrame, "TOPRIGHT", 14, -4)
    guildNameFS:SetTextColor(C.titleText[1], C.titleText[2], C.titleText[3])
    guildNameFS:SetShadowColor(0, 0, 0, 0.8)
    guildNameFS:SetShadowOffset(2, -2)
    R.heroGuildName = guildNameFS

    local infoFS = hero:CreateFontString(nil, "OVERLAY")
    TrySetFont(infoFS, BODY_FONT, 11, "")
    infoFS:SetPoint("TOPLEFT", guildNameFS, "BOTTOMLEFT", 0, -4)
    infoFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3])
    R.infoFS = infoFS

    local identityLine = hero:CreateTexture(nil, "OVERLAY")
    identityLine:SetHeight(2)
    identityLine:SetPoint("TOPLEFT", emblemFrame, "BOTTOMLEFT", -4, -8)
    identityLine:SetPoint("RIGHT", hero, "RIGHT", -CFG.PAD, 0)
    identityLine:SetTexture(W8)
    if identityLine.SetGradient and CreateColor then
        identityLine:SetGradient("HORIZONTAL",
            CreateColor(C.accent[1], C.accent[2], C.accent[3], 0.5),
            CreateColor(C.accent[1], C.accent[2], C.accent[3], 0.0))
    end
    R.identityLine = identityLine

    local heroScroll = CreateFrame("ScrollFrame", nil, hero, "UIPanelScrollFrameTemplate")
    heroScroll:SetPoint("TOPLEFT", identityLine, "BOTTOMLEFT", 0, -8)
    heroScroll:SetPoint("BOTTOMRIGHT", hero, "BOTTOMRIGHT", -20, 4)
    local heroContent = CreateFrame("Frame", nil, heroScroll)
    heroContent:SetWidth(1)
    heroScroll:SetScrollChild(heroContent)
    heroScroll:SetScript("OnSizeChanged", function(self, w) if w > 0 then heroContent:SetWidth(w) end end)
    if heroScroll.ScrollBar then
        local sb = heroScroll.ScrollBar
        if sb.ThumbTexture then sb.ThumbTexture:SetTexture(W8); sb.ThumbTexture:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 0.20); sb.ThumbTexture:SetWidth(3) end
        if sb.ScrollUpButton then sb.ScrollUpButton:SetAlpha(0) end
        if sb.ScrollDownButton then sb.ScrollDownButton:SetAlpha(0) end
    end
    R.heroScroll = heroScroll
    R.heroContent = heroContent

    local motdCard = CreateFrame("Frame", nil, heroContent, "BackdropTemplate")
    motdCard:SetPoint("TOPLEFT", heroContent, "TOPLEFT", 0, 0)
    motdCard:SetPoint("RIGHT", heroContent, "RIGHT", 0, 0)
    motdCard:SetHeight(60)
    motdCard:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    motdCard:SetBackdropColor(C.chatBg[1], C.chatBg[2], C.chatBg[3], 0.5)
    motdCard:SetBackdropBorderColor(C.divider[1], C.divider[2], C.divider[3], 0.1)
    R.motdCard = motdCard

    local motdLbl = motdCard:CreateFontString(nil, "OVERLAY")
    TrySetFont(motdLbl, BODY_FONT, 10, "OUTLINE")
    motdLbl:SetPoint("TOPLEFT", motdCard, "TOPLEFT", 8, -8)
    motdLbl:SetText("MESSAGE OF THE DAY")
    motdLbl:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
    R.motdLbl = motdLbl

    local motdFS = motdCard:CreateFontString(nil, "OVERLAY")
    TrySetFont(motdFS, BODY_FONT, 11, "")
    motdFS:SetPoint("TOPLEFT", motdLbl, "BOTTOMLEFT", 0, -6)
    motdFS:SetPoint("RIGHT", motdCard, "RIGHT", -8, 0)
    motdFS:SetJustifyH("LEFT"); motdFS:SetWordWrap(true); motdFS:SetSpacing(3)
    motdFS:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])
    R.motdFS = motdFS

    local motdPendingFS = motdCard:CreateFontString(nil, "OVERLAY")
    TrySetFont(motdPendingFS, BODY_FONT, 9, "OUTLINE")
    motdPendingFS:SetPoint("TOPRIGHT", motdCard, "TOPRIGHT", -8, -8)
    motdPendingFS:SetJustifyH("RIGHT")
    motdPendingFS:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
    motdPendingFS:Hide()
    R.motdPendingFS = motdPendingFS

    local motdClick = CreateFrame("Button", nil, motdCard)
    motdClick:SetAllPoints(motdCard)
    motdClick:SetFrameLevel(motdCard:GetFrameLevel() + 1)
    motdClick:RegisterForClicks("LeftButtonUp")
    local motdHover = motdClick:CreateTexture(nil, "HIGHLIGHT")
    motdHover:SetAllPoints()
    motdHover:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.08)
    local function MOTDEditable()
        local fn = (C_GuildInfo and C_GuildInfo.CanEditMOTD) or _G.CanEditMOTD
        if type(fn) == "function" then
            local ok, res = pcall(fn)
            if ok and res then return true end
        end
        return false
    end
    motdClick:SetScript("OnEnter", function(self)
        if not MOTDEditable() then return end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:SetText("Edit Message of the Day", 1, 1, 1)
        GameTooltip:Show()
    end)
    motdClick:SetScript("OnLeave", function() GameTooltip:Hide() end)
    motdClick:SetScript("OnClick", function()
        if not MOTDEditable() then return end
        Panel.ShowMOTDEditor()
    end)
    R.motdClick = motdClick

    local recruitBadgeFrame = CreateFrame("Frame", nil, heroContent)
    recruitBadgeFrame:SetHeight(20)
    recruitBadgeFrame:SetPoint("TOPLEFT", motdCard, "BOTTOMLEFT", 8, -6)
    recruitBadgeFrame:SetPoint("RIGHT", heroContent, "RIGHT", -8, 0)
    recruitBadgeFrame:Hide()
    R.recruitBadgeAnchor = recruitBadgeFrame

    C_Timer.After(0.5, function()
        local api = _G.MidnightUI_GuildRecruitAPI
        if api and api.CreateBadge then
            api.CreateBadge(recruitBadgeFrame, C)
            api.RefreshBadge()
        end
        if api and R.recruitBtn then
            for _, region in ipairs({ R.recruitBtn:GetRegions() }) do
                if region:GetObjectType() == "Texture" and region:GetSize() and select(1, region:GetSize()) <= 12 then
                    api._headerBadgeDot = region
                end
            end
        end
    end)

    local gInfoCard = CreateFrame("Frame", nil, heroContent, "BackdropTemplate")
    gInfoCard:SetPoint("TOPLEFT", motdCard, "BOTTOMLEFT", 0, -8)
    gInfoCard:SetPoint("RIGHT", heroContent, "RIGHT", 0, 0)
    gInfoCard:SetHeight(60)
    gInfoCard:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    gInfoCard:SetBackdropColor(C.chatBg[1], C.chatBg[2], C.chatBg[3], 0.5)
    gInfoCard:SetBackdropBorderColor(C.divider[1], C.divider[2], C.divider[3], 0.1)
    R.gInfoCard = gInfoCard

    local gInfoLbl = gInfoCard:CreateFontString(nil, "OVERLAY")
    TrySetFont(gInfoLbl, BODY_FONT, 10, "OUTLINE")
    gInfoLbl:SetPoint("TOPLEFT", gInfoCard, "TOPLEFT", 8, -8)
    gInfoLbl:SetText("GUILD INFORMATION")
    gInfoLbl:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
    R.gInfoLbl = gInfoLbl

    local gInfoFS = gInfoCard:CreateFontString(nil, "OVERLAY")
    TrySetFont(gInfoFS, BODY_FONT, 11, "")
    gInfoFS:SetPoint("TOPLEFT", gInfoLbl, "BOTTOMLEFT", 0, -6)
    gInfoFS:SetPoint("RIGHT", gInfoCard, "RIGHT", -8, 0)
    gInfoFS:SetJustifyH("LEFT"); gInfoFS:SetWordWrap(true); gInfoFS:SetSpacing(3)
    gInfoFS:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3], 0.80)
    R.gInfoFS = gInfoFS

    local challengeCard = CreateFrame("Frame", nil, heroContent, "BackdropTemplate")
    challengeCard:SetPoint("TOPLEFT", gInfoCard, "BOTTOMLEFT", 0, -8)
    challengeCard:SetPoint("RIGHT", heroContent, "RIGHT", 0, 0)
    challengeCard:SetHeight(30)
    challengeCard:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    challengeCard:SetBackdropColor(C.chatBg[1], C.chatBg[2], C.chatBg[3], 0.5)
    challengeCard:SetBackdropBorderColor(C.divider[1], C.divider[2], C.divider[3], 0.1)
    R.challengeCard = challengeCard

    local challengeHdrBtn = CreateFrame("Button", nil, challengeCard)
    challengeHdrBtn:SetHeight(26); challengeHdrBtn:SetPoint("TOPLEFT", 0, 0); challengeHdrBtn:SetPoint("TOPRIGHT", 0, 0)
    challengeHdrBtn:EnableMouse(true); challengeHdrBtn:RegisterForClicks("LeftButtonUp")
    local challengeChevron = challengeHdrBtn:CreateTexture(nil, "OVERLAY")
    challengeChevron:SetSize(10, 10); challengeChevron:SetPoint("LEFT", challengeHdrBtn, "LEFT", 8, 0)
    challengeChevron:SetAtlas("common-dropdown-icon-back"); challengeChevron:SetRotation(math.pi)
    challengeChevron:SetVertexColor(C.accent[1], C.accent[2], C.accent[3])
    R.challengeChevron = challengeChevron
    local challengeLbl = challengeHdrBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(challengeLbl, BODY_FONT, 10, "OUTLINE")
    challengeLbl:SetPoint("LEFT", challengeChevron, "RIGHT", 4, 0)
    challengeLbl:SetText("GUILD CHALLENGES"); challengeLbl:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
    R.challengeLbl = challengeLbl
    local challengeContent = CreateFrame("Frame", nil, challengeCard)
    challengeContent:SetPoint("TOPLEFT", challengeHdrBtn, "BOTTOMLEFT", 8, -4)
    challengeContent:SetPoint("RIGHT", challengeCard, "RIGHT", -8, 0)
    challengeContent:SetHeight(1); challengeContent:Hide()
    R.challengeContent = challengeContent
    challengeHdrBtn:SetScript("OnClick", function()
        Panel._state.challengesExpanded = not Panel._state.challengesExpanded
        if Panel._state.challengesExpanded then
            challengeChevron:SetRotation(-math.pi / 2)
            challengeContent:Show()
            Panel.UpdateGuildChallenges()
        else
            challengeChevron:SetRotation(math.pi)
            challengeContent:Hide()
        end
        Panel.SizeInfoCards()
        C_Timer.After(0.15, function() Panel.SizeInfoCards() end)
    end)

    local newsCard = CreateFrame("Frame", nil, heroContent, "BackdropTemplate")
    newsCard:SetPoint("TOPLEFT", challengeCard, "BOTTOMLEFT", 0, -8)
    newsCard:SetPoint("RIGHT", heroContent, "RIGHT", 0, 0)
    newsCard:SetHeight(30)
    newsCard:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    newsCard:SetBackdropColor(C.chatBg[1], C.chatBg[2], C.chatBg[3], 0.5)
    newsCard:SetBackdropBorderColor(C.divider[1], C.divider[2], C.divider[3], 0.1)
    R.newsCard = newsCard

    local newsHdrBtn = CreateFrame("Button", nil, newsCard)
    newsHdrBtn:SetHeight(26); newsHdrBtn:SetPoint("TOPLEFT", 0, 0); newsHdrBtn:SetPoint("TOPRIGHT", 0, 0)
    newsHdrBtn:EnableMouse(true); newsHdrBtn:RegisterForClicks("LeftButtonUp")
    local newsChevron = newsHdrBtn:CreateTexture(nil, "OVERLAY")
    newsChevron:SetSize(10, 10); newsChevron:SetPoint("LEFT", newsHdrBtn, "LEFT", 8, 0)
    newsChevron:SetAtlas("common-dropdown-icon-back"); newsChevron:SetRotation(math.pi)
    newsChevron:SetVertexColor(C.accent[1], C.accent[2], C.accent[3])
    R.newsChevron = newsChevron
    local newsLbl = newsHdrBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(newsLbl, BODY_FONT, 10, "OUTLINE")
    newsLbl:SetPoint("LEFT", newsChevron, "RIGHT", 4, 0)
    newsLbl:SetText("GUILD NEWS"); newsLbl:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
    R.newsLbl = newsLbl
    local newsContent = CreateFrame("Frame", nil, newsCard)
    newsContent:SetPoint("TOPLEFT", newsHdrBtn, "BOTTOMLEFT", 8, -4)
    newsContent:SetPoint("RIGHT", newsCard, "RIGHT", -8, 0)
    newsContent:SetHeight(1); newsContent:Hide()
    R.newsContent = newsContent
    newsHdrBtn:SetScript("OnClick", function()
        Panel._state.newsExpanded = not Panel._state.newsExpanded
        if Panel._state.newsExpanded then
            newsChevron:SetRotation(-math.pi / 2)
            newsContent:Show()
            Panel.UpdateGuildNews()
        else
            newsChevron:SetRotation(math.pi)
            newsContent:Hide()
        end
        Panel.SizeInfoCards()
        C_Timer.After(0.15, function() Panel.SizeInfoCards() end)
    end)

    local heroSep2 = hero:CreateTexture(nil, "OVERLAY"); heroSep2:SetHeight(1)
    heroSep2:SetPoint("BOTTOMLEFT", hero, "BOTTOMLEFT", 0, 0)
    heroSep2:SetPoint("BOTTOMRIGHT", hero, "BOTTOMRIGHT", 0, 0)
    heroSep2:SetTexture(W8)
    if heroSep2.SetGradient and CreateColor then
        heroSep2:SetGradient("HORIZONTAL",
            CreateColor(C.accent[1], C.accent[2], C.accent[3], 0.4),
            CreateColor(C.accent[1], C.accent[2], C.accent[3], 0.0))
    end


    local rosterPanel = CreateFrame("Frame", nil, p)
    rosterPanel:SetPoint("TOPLEFT", hero, "BOTTOMLEFT", 0, 0)
    rosterPanel:SetHeight(halfH)
    rosterPanel:SetWidth(rosterW)
    local rosterBg = rosterPanel:CreateTexture(nil, "BACKGROUND"); rosterBg:SetAllPoints()
    rosterBg:SetColorTexture(C.panelBg[1], C.panelBg[2], C.panelBg[3], C.panelBg[4])
    R.rosterPanel = rosterPanel; R.rosterBg = rosterBg

    local colDiv = p:CreateTexture(nil, "OVERLAY")
    colDiv:SetWidth(1)
    colDiv:SetPoint("TOP", hdr, "BOTTOM", 0, 0)
    colDiv:SetPoint("BOTTOM", p, "BOTTOM", 0, 0)
    colDiv:SetPoint("LEFT", p, "LEFT", rosterW, 0)
    R.colDiv = colDiv
    colDiv:SetColorTexture(C.divider[1], C.divider[2], C.divider[3], 0.2)

    local searchBox = CreateFrame("EditBox", nil, rosterPanel, "BackdropTemplate")
    searchBox:SetHeight(CFG.SEARCH_H)
    searchBox:SetPoint("TOPLEFT", rosterPanel, "TOPLEFT", CFG.PAD, -CFG.PAD)
    searchBox:SetPoint("TOPRIGHT", rosterPanel, "TOPRIGHT", -CFG.PAD, -CFG.PAD)
    searchBox:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 8, right = 8, top = 4, bottom = 4 } })
    searchBox:SetBackdropColor(C.inputBg[1], C.inputBg[2], C.inputBg[3], C.inputBg[4])
    searchBox:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.15)
    TrySetFont(searchBox, BODY_FONT, 11, "")
    searchBox:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])
    searchBox:SetAutoFocus(false)
    searchBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    searchBox:SetScript("OnTextChanged", function(self)
        Panel._state.searchText = self:GetText() or ""
        Panel.UpdateRoster()
    end)
    local searchPlaceholder = searchBox:CreateFontString(nil, "OVERLAY")
    TrySetFont(searchPlaceholder, BODY_FONT, 11, "")
    searchPlaceholder:SetPoint("LEFT", searchBox, "LEFT", 8, 0)
    searchPlaceholder:SetText("Search members...")
    searchPlaceholder:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.5)
    searchBox:SetScript("OnEditFocusGained", function() searchPlaceholder:Hide() end)
    searchBox:SetScript("OnEditFocusLost", function(self)
        if self:GetText() == "" then searchPlaceholder:Show() end
    end)
    R.searchBox = searchBox

    local filterBtn = CreateFrame("Button", nil, rosterPanel)
    filterBtn:SetSize(54, CFG.SEARCH_H)
    filterBtn:SetPoint("LEFT", searchBox, "RIGHT", 4, 0)

    local fbBorder = filterBtn:CreateTexture(nil, "BACKGROUND")
    fbBorder:SetAllPoints(); fbBorder:SetTexture(W8)
    fbBorder:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 0.4)
    local fbBg = filterBtn:CreateTexture(nil, "BACKGROUND", nil, 1)
    fbBg:SetTexture(W8); fbBg:SetVertexColor(0.07, 0.08, 0.10, 0.95)
    fbBg:SetPoint("TOPLEFT", 1, -1); fbBg:SetPoint("BOTTOMRIGHT", -1, 1)
    local fbSheen = filterBtn:CreateTexture(nil, "ARTWORK")
    fbSheen:SetTexture(W8); fbSheen:SetVertexColor(0.18, 0.22, 0.28, 0.35)
    fbSheen:SetPoint("TOPLEFT", 2, -2); fbSheen:SetPoint("BOTTOMRIGHT", -2, 10)
    local fbHover = filterBtn:CreateTexture(nil, "HIGHLIGHT")
    fbHover:SetTexture(W8); fbHover:SetVertexColor(0.35, 0.40, 0.46, 0.25)
    fbHover:SetPoint("TOPLEFT", 1, -1); fbHover:SetPoint("BOTTOMRIGHT", -1, 1)
    filterBtn:SetHighlightTexture(fbHover)

    local filterBtnFS = filterBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(filterBtnFS, BODY_FONT, 10, "")
    filterBtnFS:SetPoint("CENTER")
    filterBtnFS:SetText("Filters")
    filterBtnFS:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])

    local filterDropdown = CreateFrame("Frame", nil, rosterPanel, "BackdropTemplate")
    filterDropdown:SetSize(130, 112)
    filterDropdown:SetPoint("TOPLEFT", filterBtn, "BOTTOMLEFT", 0, -4)
    filterDropdown:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    filterDropdown:SetBackdropColor(C.headerBg[1], C.headerBg[2], C.headerBg[3], 0.96)
    filterDropdown:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.3)
    filterDropdown:SetFrameLevel(rosterPanel:GetFrameLevel() + 10)
    filterDropdown:Hide()
    R.filterDropdown = filterDropdown

    local FILTER_MODES = {
        { key = "default",    label = "Last Online" },
        { key = "achievePts", label = "Achievement Pts" },
        { key = "mythicPlus", label = "M+ Rating" },
        { key = "mpiScore",   label = "MPI Score" },
    }
    local fdYOfs = 6
    for _, mode in ipairs(FILTER_MODES) do
        local btn = CreateFrame("Button", nil, filterDropdown)
        btn:SetHeight(22)
        btn:SetPoint("TOPLEFT", filterDropdown, "TOPLEFT", 1, -fdYOfs)
        btn:SetPoint("TOPRIGHT", filterDropdown, "TOPRIGHT", -1, -fdYOfs)

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

        local fs = btn:CreateFontString(nil, "OVERLAY")
        TrySetFont(fs, BODY_FONT, 11, "")
        fs:SetPoint("LEFT", btn, "LEFT", 10, 0)
        fs:SetText(mode.label)
        if Panel._state.filterMode == mode.key then
            fs:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
        else
            fs:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])
        end

        btn:SetScript("OnEnter", function() hv:Show() end)
        btn:SetScript("OnLeave", function() hv:Hide() end)
        local capturedKey = mode.key
        btn:SetScript("OnClick", function()
            Panel._state.filterMode = capturedKey
            filterDropdown:Hide()
            Panel.UpdateSortHeaders()
            Panel.UpdateRoster()
        end)

        fdYOfs = fdYOfs + 22
    end
    filterDropdown:SetHeight(fdYOfs + 6)

    filterBtn:SetScript("OnClick", function()
        if filterDropdown:IsShown() then
            filterDropdown:Hide()
        else
            filterDropdown:Show()
        end
    end)

    searchBox:ClearAllPoints()
    searchBox:SetHeight(CFG.SEARCH_H)
    searchBox:SetPoint("TOPLEFT", rosterPanel, "TOPLEFT", CFG.PAD, -CFG.PAD)
    searchBox:SetPoint("TOPRIGHT", rosterPanel, "TOPRIGHT", -(CFG.PAD + 54), -CFG.PAD)

    local sortBar = CreateFrame("Frame", nil, rosterPanel)
    sortBar:SetHeight(CFG.SORT_HDR_H)
    sortBar:SetPoint("TOPLEFT", searchBox, "BOTTOMLEFT", 0, -4)
    sortBar:SetPoint("TOPRIGHT", rosterPanel, "TOPRIGHT", -CFG.PAD, 0)
    sortBar:SetPoint("TOP", searchBox, "BOTTOM", 0, -4)

    local COL_FIXED = CFG.COL_LVL + CFG.COL_RANK + CFG.COL_ZONE + CFG.COL_LASTON

    local SORT_COLS = {
        { key = "name",     label = "Name",     flex = true },
        { key = "level",    label = "Lvl",      width = CFG.COL_LVL,    rightOf = CFG.COL_RANK + CFG.COL_ZONE + CFG.COL_LASTON },
        { key = "rank",     label = "Rank",     width = CFG.COL_RANK,   rightOf = CFG.COL_ZONE + CFG.COL_LASTON },
        { key = "zone",     label = "Zone",     width = CFG.COL_ZONE,   rightOf = CFG.COL_LASTON },
        { key = "lastOn",   label = "Last Seen", width = CFG.COL_LASTON, rightOf = 0 },
    }
    local sortBtns = {}
    for _, col in ipairs(SORT_COLS) do
        local btn = CreateFrame("Button", nil, sortBar)
        btn:SetHeight(CFG.SORT_HDR_H)
        if col.flex then
            btn:SetPoint("TOPLEFT", sortBar, "TOPLEFT", 0, 0)
            btn:SetPoint("RIGHT", sortBar, "RIGHT", -(COL_FIXED + CFG.PAD), 0)
        else
            btn:SetPoint("RIGHT", sortBar, "RIGHT", -(col.rightOf + CFG.PAD), 0)
            btn:SetWidth(col.width)
        end

        local fs = btn:CreateFontString(nil, "OVERLAY")
        TrySetFont(fs, BODY_FONT, 10, "OUTLINE")
        if col.flex then
            fs:SetPoint("LEFT", btn, "LEFT", CFG.PAD, 0)
        else
            fs:SetPoint("LEFT", btn, "LEFT", 2, 0)
        end
        fs:SetText(col.label)
        fs:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3])
        btn._fs = fs
        btn._key = col.key

        btn:SetScript("OnClick", function()
            if Panel._state.sortColumn == col.key then
                Panel._state.sortAsc = not Panel._state.sortAsc
            else
                Panel._state.sortColumn = col.key
                Panel._state.sortAsc = true
            end
            Panel.UpdateSortHeaders()
            Panel.UpdateRoster()
        end)
        btn:SetScript("OnEnter", function() fs:SetTextColor(C.titleText[1], C.titleText[2], C.titleText[3]) end)
        btn:SetScript("OnLeave", function()
            if Panel._state.sortColumn == col.key then
                fs:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
            else
                fs:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3])
            end
        end)

        sortBtns[col.key] = btn
    end
    R.sortBtns = sortBtns

    local sortSep = sortBar:CreateTexture(nil, "OVERLAY"); sortSep:SetHeight(1)
    sortSep:SetPoint("BOTTOMLEFT", sortBar, "BOTTOMLEFT", 0, 0)
    sortSep:SetPoint("BOTTOMRIGHT", sortBar, "BOTTOMRIGHT", 0, 0)
    sortSep:SetColorTexture(C.divider[1], C.divider[2], C.divider[3], 0.2)

    local queueBanner = CreateFrame("Button", nil, rosterPanel, "BackdropTemplate")
    queueBanner:SetPoint("BOTTOMLEFT", rosterPanel, "BOTTOMLEFT", 6, 4)
    queueBanner:SetPoint("BOTTOMRIGHT", rosterPanel, "BOTTOMRIGHT", -6, 4)
    queueBanner:SetHeight(22)
    queueBanner:SetFrameLevel(rosterPanel:GetFrameLevel() + 20)
    queueBanner:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    queueBanner:SetBackdropColor(C.accent[1], C.accent[2], C.accent[3], 0.30)
    queueBanner:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.8)
    queueBanner:Hide()
    local queueBannerFS = queueBanner:CreateFontString(nil, "OVERLAY")
    TrySetFont(queueBannerFS, BODY_FONT, 10, "OUTLINE")
    queueBannerFS:SetPoint("CENTER")
    queueBannerFS:SetTextColor(1, 1, 1)
    R.queueBanner = queueBanner
    R.queueBannerFS = queueBannerFS

    queueBanner:SetScript("OnEnter", function()
        if RANK_QUEUE_ARMED then
            queueBanner:SetBackdropColor(0.35, 0.90, 0.45, 0.75)
        else
            queueBanner:SetBackdropColor(C.accent[1], C.accent[2], C.accent[3], 0.55)
        end
    end)
    queueBanner:SetScript("OnLeave", function()
        Panel.UpdateRankQueueBanner()
    end)
    queueBanner:SetScript("OnClick", function() Panel.CommitRankQueue() end)

    local rosterScroll = CreateFrame("ScrollFrame", nil, rosterPanel, "UIPanelScrollFrameTemplate")
    rosterScroll:SetPoint("TOPLEFT", sortBar, "BOTTOMLEFT", 0, -2)
    rosterScroll:SetPoint("BOTTOMRIGHT", rosterPanel, "BOTTOMRIGHT", -20, 30)
    local rosterContent = CreateFrame("Frame", nil, rosterScroll)
    rosterContent:SetWidth(1)
    rosterScroll:SetScrollChild(rosterContent)
    rosterScroll:SetScript("OnSizeChanged", function(self, w) if w > 0 then rosterContent:SetWidth(w) end end)
    if rosterScroll.ScrollBar then
        local sb = rosterScroll.ScrollBar
        if sb.ThumbTexture then sb.ThumbTexture:SetTexture(W8); sb.ThumbTexture:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 0.25); sb.ThumbTexture:SetWidth(3) end
        if sb.ScrollUpButton then sb.ScrollUpButton:SetAlpha(0) end
        if sb.ScrollDownButton then sb.ScrollDownButton:SetAlpha(0) end
    end
    R.rosterScroll = rosterScroll
    R.rosterContent = rosterContent

    local chatPanel = CreateFrame("Frame", nil, p)
    chatPanel:SetPoint("TOPLEFT", hdr, "BOTTOMLEFT", rosterW + 1, 0)
    chatPanel:SetPoint("BOTTOMRIGHT", p, "BOTTOMRIGHT", 0, 0)
    local chatBg = chatPanel:CreateTexture(nil, "BACKGROUND"); chatBg:SetAllPoints()
    chatBg:SetColorTexture(C.chatBg[1], C.chatBg[2], C.chatBg[3], C.chatBg[4])
    R.chatPanel = chatPanel; R.chatBg = chatBg

    local chatTitle = chatPanel:CreateFontString(nil, "OVERLAY")
    TrySetFont(chatTitle, BODY_FONT, 10, "OUTLINE")
    chatTitle:SetPoint("TOPLEFT", chatPanel, "TOPLEFT", CFG.PAD, -8)
    chatTitle:SetText("GUILD CHAT")
    chatTitle:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3])

    local chatScroll = CreateFrame("ScrollFrame", nil, chatPanel, "UIPanelScrollFrameTemplate")
    chatScroll:SetPoint("TOPLEFT", chatPanel, "TOPLEFT", CFG.PAD, -24)
    chatScroll:SetPoint("BOTTOMRIGHT", chatPanel, "BOTTOMRIGHT", -20, CFG.CHAT_INPUT_H + 8)
    local chatContent = CreateFrame("Frame", nil, chatScroll)
    chatContent:SetWidth(1)
    chatScroll:SetScrollChild(chatContent)
    chatScroll:SetScript("OnSizeChanged", function(self, w) if w > 0 then chatContent:SetWidth(w) end end)
    if chatScroll.ScrollBar then
        local sb = chatScroll.ScrollBar
        if sb.ThumbTexture then sb.ThumbTexture:SetTexture(W8); sb.ThumbTexture:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 0.20); sb.ThumbTexture:SetWidth(3) end
        if sb.ScrollUpButton then sb.ScrollUpButton:SetAlpha(0) end
        if sb.ScrollDownButton then sb.ScrollDownButton:SetAlpha(0) end
    end
    R.chatScroll = chatScroll
    R.chatContent = chatContent

    local chatChannel = "GUILD"

    local chanBtn = CreateFrame("Button", nil, chatPanel, "BackdropTemplate")
    chanBtn:SetSize(58, CFG.CHAT_INPUT_H)
    chanBtn:SetPoint("BOTTOMLEFT", chatPanel, "BOTTOMLEFT", CFG.PAD, 8)
    chanBtn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    chanBtn:SetBackdropColor(C.inputBg[1], C.inputBg[2], C.inputBg[3], C.inputBg[4])
    chanBtn:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.2)

    local chanFS = chanBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(chanFS, BODY_FONT, 9, "OUTLINE"); chanFS:SetPoint("CENTER")
    chanFS:SetText("Guild"); chanFS:SetTextColor(0.25, 0.85, 0.25)

    local function UpdateChanVisual()
        if chatChannel == "OFFICER" then
            chanFS:SetText("Officer")
            chanFS:SetTextColor(0.15, 0.55, 0.15)
            chanBtn:SetBackdropBorderColor(0.15, 0.55, 0.15, 0.3)
        else
            chanFS:SetText("Guild")
            chanFS:SetTextColor(0.25, 0.85, 0.25)
            chanBtn:SetBackdropBorderColor(0.25, 0.85, 0.25, 0.3)
        end
    end

    chanBtn:SetScript("OnClick", function()
        if chatChannel == "GUILD" then chatChannel = "OFFICER" else chatChannel = "GUILD" end
        UpdateChanVisual()
    end)
    chanBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        if chatChannel == "GUILD" then
            GameTooltip:SetText("Guild Chat", 0.25, 0.85, 0.25)
            GameTooltip:AddLine("Click to switch to Officer Chat", C.mutedText[1], C.mutedText[2], C.mutedText[3])
        else
            GameTooltip:SetText("Officer Chat", 0.15, 0.55, 0.15)
            GameTooltip:AddLine("Click to switch to Guild Chat", C.mutedText[1], C.mutedText[2], C.mutedText[3])
        end
        GameTooltip:Show()
    end)
    chanBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
    R.chanBtn = chanBtn

    local chatInput = CreateFrame("EditBox", nil, chatPanel, "BackdropTemplate")
    chatInput:SetHeight(CFG.CHAT_INPUT_H)
    chatInput:SetPoint("BOTTOMLEFT", chanBtn, "BOTTOMRIGHT", 4, 0)
    chatInput:SetPoint("BOTTOMRIGHT", chatPanel, "BOTTOMRIGHT", -CFG.PAD, 8)
    chatInput:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 8, right = 8, top = 4, bottom = 4 } })
    chatInput:SetBackdropColor(C.inputBg[1], C.inputBg[2], C.inputBg[3], C.inputBg[4])
    chatInput:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.2)
    TrySetFont(chatInput, BODY_FONT, 11, "")
    chatInput:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])
    chatInput:SetAutoFocus(false)
    chatInput:SetScript("OnEscapePressed", function(self) self:SetText(""); self:ClearFocus() end)
    chatInput:SetScript("OnEnterPressed", function(self)
        local text = self:GetText()
        if text and text ~= "" then
            pcall(SendChatMessage, text, chatChannel)
            self:SetText("")
        end
        self:ClearFocus()
    end)
    local chatPlaceholder = chatInput:CreateFontString(nil, "OVERLAY")
    TrySetFont(chatPlaceholder, BODY_FONT, 11, "")
    chatPlaceholder:SetPoint("LEFT", chatInput, "LEFT", 8, 0)
    chatPlaceholder:SetText("Type a message...")
    chatPlaceholder:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.4)
    chatInput:SetScript("OnEditFocusGained", function()
        chatPlaceholder:Hide()
        chatInput:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.4)
    end)
    chatInput:SetScript("OnEditFocusLost", function(self)
        if self:GetText() == "" then chatPlaceholder:Show() end
        chatInput:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.2)
    end)
    R.chatInput = chatInput

    local ctxMenu = CreateFrame("Frame", nil, p, "BackdropTemplate")
    ctxMenu:SetSize(220, 120)
    ctxMenu:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    ctxMenu:SetBackdropColor(C.headerBg[1], C.headerBg[2], C.headerBg[3], 0.96)
    ctxMenu:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.35)
    ctxMenu:SetFrameLevel(p:GetFrameLevel() + 30)
    ctxMenu:Hide()
    ctxMenu._targetName = nil
    ctxMenu._targetIndex = nil
    R.ctxMenu = ctxMenu

    local CTX_OPTIONS = {
        { label = "Whisper",                  action = "whisper" },
        { label = "Invite to Group",          action = "invite" },
        { label = "Keybind: Promote/Demote",  action = "keybind_rank" },
        { label = "View/Edit Note",           action = "note" },
        { label = "Copy Name",                action = "copy" },
    }
    local ctxYOfs = 6
    for _, opt in ipairs(CTX_OPTIONS) do
        local btn = CreateFrame("Button", nil, ctxMenu)
        btn:SetHeight(24)
        btn:SetPoint("TOPLEFT", ctxMenu, "TOPLEFT", 1, -ctxYOfs)
        btn:SetPoint("TOPRIGHT", ctxMenu, "TOPRIGHT", -1, -ctxYOfs)
        local hv = btn:CreateTexture(nil, "BACKGROUND"); hv:SetAllPoints()
        hv:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.08); hv:Hide()
        local fs = btn:CreateFontString(nil, "OVERLAY")
        TrySetFont(fs, BODY_FONT, 11, ""); fs:SetPoint("LEFT", btn, "LEFT", 10, 0)
        fs:SetText(opt.label); fs:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])
        btn:SetScript("OnEnter", function() hv:Show() end)
        btn:SetScript("OnLeave", function() hv:Hide() end)
        local capturedAction = opt.action
        btn:SetScript("OnClick", function(self, mb)
            local name = ctxMenu._targetName
            local idx = ctxMenu._targetIndex
            ctxMenu:Hide()
            if not name then return end
            if capturedAction == "whisper" then
                local chatFrame = DEFAULT_CHAT_FRAME or ChatFrame1
                if chatFrame and ChatFrame_SendTell then
                    pcall(ChatFrame_SendTell, name, chatFrame)
                elseif chatFrame and chatFrame.editBox then
                    chatFrame.editBox:SetText("/w " .. name .. " ")
                    pcall(ChatEdit_ActivateChat, chatFrame.editBox)
                end
            elseif capturedAction == "invite" then
                if C_PartyInfo and C_PartyInfo.InviteUnit then
                    pcall(C_PartyInfo.InviteUnit, name)
                elseif InviteUnit then
                    pcall(InviteUnit, name)
                end
            elseif capturedAction == "keybind_rank" then
                local cfg = GUILD_ACTION_MACROS.rankQueue
                local settings = _G.MidnightUISettings
                settings.GuildPanel = settings.GuildPanel or {}
                PromptForHotkey(cfg.label, function(hk)
                    if not hk then return end
                    settings.GuildPanel[cfg.hotkeyKey] = hk
                    print(string.format("|cff40d940MidnightUI:|r Promote/Demote hotkey set to |cffffd100%s|r.", hk))
                end)
            elseif capturedAction == "note" then
                Panel.ShowNoteEditor(name, idx)
            elseif capturedAction == "copy" then
                local eb = R.chatInput
                if eb then
                    eb:SetText(name); eb:HighlightText(); eb:SetFocus()
                end
            end
        end)
        opt.btn = btn
        opt.fs = fs
        ctxYOfs = ctxYOfs + 24
    end
    ctxMenu:SetHeight(ctxYOfs + 6)

    ctxMenu._refresh = function(targetRankIndex)
        local settings = _G.MidnightUISettings or {}
        local gp = settings.GuildPanel or {}
        local canPromote = SafeCall(CanGuildPromote) and true or false
        local canDemote  = SafeCall(CanGuildDemote)  and true or false
        local targetName = ctxMenu._targetName or ""
        local pName = UnitName("player") or ""
        local pRealm = (GetRealmName and GetRealmName()) or ""
        local tShort, tRealm = targetName:match("^([^%-]+)%-?(.*)$")
        local isSelf = tShort == pName and (tRealm == "" or tRealm == pRealm)
        local y = 6
        for _, o in ipairs(CTX_OPTIONS) do
            if o.btn then
                local show = true
                if o.action == "keybind_rank" and not (canPromote or canDemote) then show = false end
                if isSelf and (o.action == "keybind_rank" or o.action == "invite") then
                    show = false
                end
                if o.action == "invite" and not ctxMenu._targetOnline then
                    show = false
                end
                if not show then
                    o.btn:Hide()
                else
                    if o.btn.Enable then o.btn:Enable() end
                    if o.fs then
                        o.fs:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])
                        local label = o.label
                        if o.action == "keybind_rank" then
                            local hk = gp.rankQueueHotkey
                            label = label .. "  |cff808080[" .. (hk and hk ~= "" and hk or "unbound") .. "]|r"
                        end
                        o.fs:SetText(label)
                    end
                    o.btn:ClearAllPoints()
                    o.btn:SetPoint("TOPLEFT", ctxMenu, "TOPLEFT", 1, -y)
                    o.btn:SetPoint("TOPRIGHT", ctxMenu, "TOPRIGHT", -1, -y)
                    o.btn:Show()
                    y = y + 24
                end
            end
        end
        ctxMenu:SetHeight(y + 6)
    end

    local ctxOverlay = CreateFrame("Button", nil, p)
    ctxOverlay:SetAllPoints(UIParent)
    ctxOverlay:SetFrameLevel(ctxMenu:GetFrameLevel() - 1)
    ctxOverlay:RegisterForClicks("AnyUp")
    ctxOverlay:SetScript("OnClick", function()
        ctxMenu:Hide()
        ctxOverlay:Hide()
    end)
    ctxOverlay:Hide()
    R.ctxOverlay = ctxOverlay

    ctxMenu:HookScript("OnShow", function() ctxOverlay:Show() end)
    ctxMenu:HookScript("OnHide", function() ctxOverlay:Hide() end)

    return p
end

local function CreateRosterRow(parent, entry, yOfs, rosterW)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetHeight(CFG.ROSTER_ROW_H)
    btn:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, -yOfs)
    btn:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, -yOfs)
    btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")

    local hoverBg = btn:CreateTexture(nil, "BACKGROUND")
    hoverBg:SetAllPoints(); hoverBg:SetColorTexture(C.hoverBg[1], C.hoverBg[2], C.hoverBg[3], C.hoverBg[4])
    hoverBg:Hide()

    local dot = btn:CreateTexture(nil, "OVERLAY")
    dot:SetSize(6, 6)
    dot:SetPoint("LEFT", btn, "LEFT", 4, 0)
    if entry.isOnline then
        dot:SetColorTexture(C.online[1], C.online[2], C.online[3], 1)
    elseif entry.isMobile then
        dot:SetColorTexture(0.4, 0.6, 0.9, 1)
    else
        dot:SetColorTexture(C.offline[1], C.offline[2], C.offline[3], 0.5)
    end

    local COL_LVL = CFG.COL_LVL
    local COL_RANK = CFG.COL_RANK
    local COL_ZONE = CFG.COL_ZONE
    local COL_LASTON = CFG.COL_LASTON
    local alpha = (entry.isOnline or entry.isMobile) and 1 or 0.45

    local r, g, b = GetClassColor(entry.classFile)
    local nameFS = btn:CreateFontString(nil, "OVERLAY")
    TrySetFont(nameFS, BODY_FONT, 11, "")
    nameFS:SetPoint("LEFT", dot, "RIGHT", 4, 0)
    nameFS:SetPoint("RIGHT", btn, "RIGHT", -(COL_LVL + COL_RANK + COL_ZONE + COL_LASTON + CFG.PAD), 0)
    nameFS:SetJustifyH("LEFT"); nameFS:SetWordWrap(false)
    nameFS:SetText(entry.name)
    nameFS:SetTextColor(r, g, b, alpha)

    local lvlFS = btn:CreateFontString(nil, "OVERLAY")
    TrySetFont(lvlFS, BODY_FONT, 11, "")
    lvlFS:SetPoint("RIGHT", btn, "RIGHT", -(COL_RANK + COL_ZONE + COL_LASTON + CFG.PAD), 0)
    lvlFS:SetWidth(COL_LVL); lvlFS:SetJustifyH("CENTER"); lvlFS:SetWordWrap(false)
    lvlFS:SetText(entry.level)
    lvlFS:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3], alpha * 0.8)

    local ARROW_W = 14
    local rankFS = btn:CreateFontString(nil, "OVERLAY")
    TrySetFont(rankFS, BODY_FONT, 11, "")
    rankFS:SetPoint("LEFT", btn, "RIGHT", -(COL_RANK + COL_ZONE + COL_LASTON + CFG.PAD) + ARROW_W, 0)
    rankFS:SetWidth(COL_RANK - ARROW_W); rankFS:SetJustifyH("LEFT"); rankFS:SetWordWrap(false)
    rankFS:SetText(entry.rankName)
    rankFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], alpha * 0.8)

    -- Rank-change arrows stacked just left of the rank text
    local canPromoteUI = SafeCall(CanGuildPromote) and true or false
    local canDemoteUI  = SafeCall(CanGuildDemote)  and true or false
    local _, _, playerRankIdx = SafeCall(GetGuildInfo, "player")
    playerRankIdx = playerRankIdx or 99
    local maxRankIdx = GuildMaxRankIndex()
    local mayPromoteMember = canPromoteUI and entry.rankIndex and entry.rankIndex > (playerRankIdx + 1)
    local mayDemoteMember  = canDemoteUI  and entry.rankIndex and entry.rankIndex > playerRankIdx and entry.rankIndex < maxRankIdx

    local function restoreRankText()
        rankFS:SetText(entry.rankName)
    end
    restoreRankText()

    local upArrow, upFS, dnArrow, dnFS
    if mayPromoteMember or mayDemoteMember then
        upArrow = CreateFrame("Button", nil, btn)
        upArrow:SetSize(12, 9)
        upArrow:SetPoint("RIGHT", rankFS, "LEFT", -1, 5)
        upFS = upArrow:CreateFontString(nil, "OVERLAY")
        TrySetFont(upFS, BODY_FONT, 10, "OUTLINE")
        upFS:SetPoint("CENTER")
        upFS:SetText("\226\150\178") -- ▲

        dnArrow = CreateFrame("Button", nil, btn)
        dnArrow:SetSize(12, 9)
        dnArrow:SetPoint("RIGHT", rankFS, "LEFT", -1, -5)
        dnFS = dnArrow:CreateFontString(nil, "OVERLAY")
        TrySetFont(dnFS, BODY_FONT, 10, "OUTLINE")
        dnFS:SetPoint("CENTER")
        dnFS:SetText("\226\150\188") -- ▼
    end

    local function applyArrowColors()
        if not upArrow then return end
        local q = RANK_QUEUE[entry.fullName]
        local action = q and q.action or nil
        if mayPromoteMember then
            if action == "promote" then
                upFS:SetTextColor(0.35, 0.88, 0.55, 1)
            else
                upFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.7)
            end
        else
            upFS:SetTextColor(0.3, 0.3, 0.3, 0.25)
            upArrow:EnableMouse(false)
        end
        if mayDemoteMember then
            if action == "demote" then
                dnFS:SetTextColor(0.95, 0.55, 0.35, 1)
            else
                dnFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.7)
            end
        else
            dnFS:SetTextColor(0.3, 0.3, 0.3, 0.25)
            dnArrow:EnableMouse(false)
        end
    end
    applyArrowColors()

    local function toggleQueueAction(action)
        local q = RANK_QUEUE[entry.fullName]
        if q and q.action == action then
            RANK_QUEUE[entry.fullName] = nil
        else
            if not q and RankQueueCount() >= RANK_QUEUE_MAX then
                print(string.format("|cffff5555MidnightUI:|r Queue is full (max %d members).", RANK_QUEUE_MAX))
                return
            end
            RANK_QUEUE[entry.fullName] = { name = entry.fullName, action = action }
        end
        RANK_QUEUE_ARMED = false
        applyArrowColors()
        if Panel.UpdateRankQueueBanner then Panel.UpdateRankQueueBanner() end
    end

    if upArrow and mayPromoteMember then
        upArrow:SetScript("OnClick", function() toggleQueueAction("promote") end)
    end
    if dnArrow and mayDemoteMember then
        dnArrow:SetScript("OnClick", function() toggleQueueAction("demote") end)
    end

    local zoneFS = btn:CreateFontString(nil, "OVERLAY")
    TrySetFont(zoneFS, BODY_FONT, 11, "")
    zoneFS:SetPoint("RIGHT", btn, "RIGHT", -(COL_LASTON + CFG.PAD), 0)
    zoneFS:SetWidth(COL_ZONE); zoneFS:SetJustifyH("LEFT"); zoneFS:SetWordWrap(false)
    zoneFS:SetText((entry.isOnline or entry.isMobile) and entry.zone or "")
    zoneFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], alpha * 0.6)

    local lastFS = btn:CreateFontString(nil, "OVERLAY")
    TrySetFont(lastFS, BODY_FONT, 11, "")
    lastFS:SetPoint("RIGHT", btn, "RIGHT", -CFG.PAD, 0)
    lastFS:SetWidth(COL_LASTON); lastFS:SetJustifyH("RIGHT"); lastFS:SetWordWrap(false)

    local filterMode = Panel._state.filterMode or "default"
    if filterMode == "achievePts" then
        local pts = entry.achievementPts or 0
        if pts > 0 then
            lastFS:SetText(FormatNumber(pts))
            lastFS:SetTextColor(1, 0.82, 0, alpha * 0.9)
        else
            lastFS:SetText("—")
            lastFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.3)
        end
    elseif filterMode == "mythicPlus" then
        local score = entry.mythicPlusRating or 0
        if score == 0 and (entry.isOnline or entry.isMobile) and entry.guid and entry.guid ~= "" then
            if C_PlayerInfo and C_PlayerInfo.GetPlayerMythicPlusRatingSummary and PlayerLocation then
                local playerLoc = PlayerLocation:CreateFromGUID(entry.guid)
                if playerLoc then
                    local okS, summary = pcall(C_PlayerInfo.GetPlayerMythicPlusRatingSummary, playerLoc)
                    if okS and summary and summary.currentSeasonScore and summary.currentSeasonScore > 0 then
                        score = summary.currentSeasonScore
                        entry.mythicPlusRating = score
                    end
                end
            end
        end
        if score > 0 then
            local sr, sg, sb = 0.7, 0.7, 0.7
            if C_ChallengeMode and C_ChallengeMode.GetDungeonScoreRarityColor then
                local okC, color = pcall(C_ChallengeMode.GetDungeonScoreRarityColor, score)
                if okC and color and color.r then sr, sg, sb = color.r, color.g, color.b end
            end
            lastFS:SetText(FormatNumber(score))
            lastFS:SetTextColor(sr, sg, sb, alpha)
        else
            lastFS:SetText("—")
            lastFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.3)
        end
    elseif filterMode == "mpiScore" then
        local mpiScore = 0
        if _G.MidnightPI then
            local PI = _G.MidnightPI
            local prof = PI.GetProfileByName and PI.GetProfileByName(entry.name)
            if not prof and PI.CommunityLookup then
                local realm = entry.fullName and entry.fullName:match("%-(.+)$") or (GetRealmName and GetRealmName() or "")
                prof = PI.CommunityLookup(entry.name, realm)
            end
            if prof and prof.avgMPI then mpiScore = prof.avgMPI end
        end
        if mpiScore > 0 then
            local sr, sg, sb = 0.5, 0.5, 0.5
            if _G.MidnightPI and _G.MidnightPI.GetScoreTier then
                local _, r2, g2, b2 = _G.MidnightPI.GetScoreTier(mpiScore)
                sr, sg, sb = r2, g2, b2
            end
            lastFS:SetText(tostring(mpiScore))
            lastFS:SetTextColor(sr, sg, sb, alpha)
        else
            lastFS:SetText("—")
            lastFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.3)
        end
    else
        if entry.isOnline or entry.isMobile then
            lastFS:SetText("|cff" .. string.format("%02x%02x%02x", C.online[1]*255, C.online[2]*255, C.online[3]*255) .. "Now|r")
        else
            lastFS:SetText(entry.lastOnline or "")
        end
        lastFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.6)
    end

    btn:SetScript("OnEnter", function(self)
        hoverBg:Show()
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:SetText(entry.name, r, g, b)
        GameTooltip:AddLine("Level " .. entry.level .. " " .. (entry.className or ""), 1, 1, 1)
        GameTooltip:AddLine(entry.rankName, C.accent[1], C.accent[2], C.accent[3])
        if (entry.isOnline or entry.isMobile) and entry.zone ~= "" then
            GameTooltip:AddLine(entry.zone, C.mutedText[1], C.mutedText[2], C.mutedText[3])
        elseif entry.lastOnline and entry.lastOnline ~= "" then
            GameTooltip:AddLine("Last online: " .. entry.lastOnline .. " ago", C.offline[1], C.offline[2], C.offline[3])
        end
        if entry.achievementPts and entry.achievementPts > 0 then
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine("Achievement Points: " .. FormatNumber(entry.achievementPts), 1, 0.82, 0)
        end
        if entry.publicNote and entry.publicNote ~= "" then
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine("|cffaaaaaaNote:|r " .. entry.publicNote, C.bodyText[1], C.bodyText[2], C.bodyText[3], true)
        end
        if entry.officerNote and entry.officerNote ~= "" then
            GameTooltip:AddLine("|cffaaaaaaOfficer:|r " .. entry.officerNote, 0.65, 0.50, 0.80, true)
        end
        if (entry.isOnline or entry.isMobile) and entry.guid and entry.guid ~= "" then
            if C_PlayerInfo and C_PlayerInfo.GetPlayerMythicPlusRatingSummary then
                local playerLoc = PlayerLocation:CreateFromGUID(entry.guid)
                if playerLoc then
                    local ok2, summary = pcall(C_PlayerInfo.GetPlayerMythicPlusRatingSummary, playerLoc)
                    if ok2 and summary and summary.currentSeasonScore and summary.currentSeasonScore > 0 then
                        local score = summary.currentSeasonScore
                        local sr, sg, sb = 0.7, 0.7, 0.7
                        if C_ChallengeMode and C_ChallengeMode.GetDungeonScoreRarityColor then
                            local okC, color = pcall(C_ChallengeMode.GetDungeonScoreRarityColor, score)
                            if okC and color and color.r then sr, sg, sb = color.r, color.g, color.b end
                        end
                        GameTooltip:AddLine("M+ Rating: " .. FormatNumber(score), sr, sg, sb)
                    end
                end
            end
        end
        GameTooltip:Show()
    end)
    btn:SetScript("OnLeave", function() hoverBg:Hide(); GameTooltip:Hide() end)

    local capturedName = entry.fullName
    local capturedIdx = entry.rosterIndex
    local capturedRankIdx = entry.rankIndex
    local capturedOnline = entry.isOnline or entry.isMobile or false
    btn:SetScript("OnClick", function(_, mouseBtn)
        if mouseBtn ~= "RightButton" then return end
        local R = Panel._refs
        if R.ctxMenu then
            R.ctxMenu._targetName = capturedName
            R.ctxMenu._targetIndex = capturedIdx
            R.ctxMenu._targetOnline = capturedOnline
            if R.ctxMenu._refresh then R.ctxMenu._refresh(capturedRankIdx) end
            R.ctxMenu:ClearAllPoints()
            local cx, cy = GetCursorPosition()
            local scale = UIParent:GetEffectiveScale()
            R.ctxMenu:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", cx / scale, cy / scale)
            R.ctxMenu:Show()
        end
    end)

    btn:Show()
    return btn
end

function Panel.UpdateChat()
    local R = Panel._refs
    if not R.chatContent then return end

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

    local messages = {}
    if _G.MessengerDB and _G.MessengerDB.History and _G.MessengerDB.History["Guild"] then
        local hist = _G.MessengerDB.History["Guild"]
        if hist.messages then
            local start = math.max(1, #hist.messages - 99)
            for i = start, #hist.messages do
                messages[#messages + 1] = hist.messages[i]
            end
        end
    end

    local classCache = {}
    if _G.MessengerDB and _G.MessengerDB.ContactClasses then
        for name, cf in pairs(_G.MessengerDB.ContactClasses) do
            classCache[name] = cf
        end
    end
    local numMembers = SafeCall(GetNumGuildMembers) or 0
    for gi = 1, numMembers do
        local gName, _, _, _, _, _, _, _, _, _, gClassFile = SafeCall(GetGuildRosterInfo, gi)
        if gName and gClassFile then
            local gShort = gName:match("^([^%-]+)") or gName
            if not classCache[gShort] then
                classCache[gShort] = gClassFile
            end
        end
    end

    local resolvedW = R.chatContent:GetWidth()
    if not resolvedW or resolvedW <= 10 then
        resolvedW = R.chatScroll and R.chatScroll:GetWidth() or 0
    end

    local yOfs = 0
    local MSG_H = 18
    local MSG_GAP = 4
    for _, msg in ipairs(messages) do
        local text = msg.msg or msg.text or ""
        local sender = msg.author or msg.sender or ""
        local timestamp = msg.timestamp or ""
        local nameColor = msg.nameColorDefault or "aaddaa"

        local shortName = sender:match("^([^%-]+)") or sender
        local classFile = classCache[shortName]
        if classFile and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classFile] then
            local cc = RAID_CLASS_COLORS[classFile]
            nameColor = string.format("%02x%02x%02x", cc.r * 255, cc.g * 255, cc.b * 255)
        end

        if type(timestamp) == "number" then
            timestamp = date("%H:%M", timestamp)
        end

        local isMPlus, dungeonName, keyLevel = DetectMPlusMessage(text)

        local msgBtn = CreateFrame("Frame", nil, R.chatContent)
        msgBtn:SetPoint("TOPLEFT", R.chatContent, "TOPLEFT", 0, -yOfs)
        msgBtn:SetPoint("RIGHT", R.chatContent, "RIGHT", -4, 0)
        msgBtn:SetHeight(MSG_H)

        if isMPlus then
            local mpBg = msgBtn:CreateTexture(nil, "BACKGROUND")
            mpBg:SetAllPoints()
            mpBg:SetColorTexture(0.00, 0.40, 0.55, 0.15)
            local mpBar = msgBtn:CreateTexture(nil, "BACKGROUND", nil, 1)
            mpBar:SetWidth(2)
            mpBar:SetPoint("TOPLEFT", msgBtn, "TOPLEFT", 0, 0)
            mpBar:SetPoint("BOTTOMLEFT", msgBtn, "BOTTOMLEFT", 0, 0)
            mpBar:SetColorTexture(0.00, 0.78, 1.00, 0.6)
        end
        if msgBtn.SetHyperlinksEnabled then
            msgBtn:SetHyperlinksEnabled(true)
            msgBtn:SetScript("OnHyperlinkClick", function(self, link, text, button)
                local linkType, linkData = link:match("^(%a+):(.+)")
                if linkType == "player" then
                    local whisperTarget = linkData:match("^([^:]+)") or linkData
                    if button == "RightButton" then
                        local R2 = Panel._refs
                        if R2.ctxMenu then
                            R2.ctxMenu._targetName = whisperTarget
                            R2.ctxMenu._targetIndex = nil
                            R2.ctxMenu:ClearAllPoints()
                            local cx, cy = GetCursorPosition()
                            local scale = UIParent:GetEffectiveScale()
                            R2.ctxMenu:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", cx / scale, cy / scale)
                            R2.ctxMenu:Show()
                        end
                    else
                        local R2 = Panel._refs
                        if R2.chatInput then
                            R2.chatInput:SetText("/w " .. whisperTarget .. " ")
                            R2.chatInput:SetFocus()
                            R2.chatInput:SetCursorPosition(R2.chatInput:GetNumLetters())
                        end
                        if ChatFrame_SendTell then
                            pcall(ChatFrame_SendTell, whisperTarget)
                        end
                    end
                    return
                end
                pcall(ChatFrame_OnHyperlinkShow, DEFAULT_CHAT_FRAME or ChatFrame1, link, text, button)
            end)
            msgBtn:SetScript("OnHyperlinkEnter", function(self, link)
                GameTooltip:SetOwner(self, "ANCHOR_CURSOR")
                pcall(GameTooltip.SetHyperlink, GameTooltip, link)
                GameTooltip:Show()
            end)
            msgBtn:SetScript("OnHyperlinkLeave", function() GameTooltip:Hide() end)
        end

        local fs = msgBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(fs, BODY_FONT, 11, "")
        fs:SetPoint("TOPLEFT", msgBtn, "TOPLEFT", 0, 0)
        fs:SetJustifyH("LEFT")
        fs:SetJustifyV("TOP")
        fs:SetWordWrap(true)
        fs:SetSpacing(4)

        local fullSender = sender
        if sender ~= "" and not sender:find("-") then
            local playerRealm = SafeCall(GetRealmName) or ""
            if playerRealm ~= "" then
                fullSender = sender .. "-" .. playerRealm:gsub("%s", "")
            end
        end

        local displayText = ""
        if timestamp ~= "" then
            displayText = "|cff666666" .. timestamp .. "|r  "
        end
        if sender ~= "" then
            local shortSender = sender:match("^([^%-]+)") or sender
            displayText = displayText .. "|Hplayer:" .. fullSender .. "|h|cff" .. nameColor .. shortSender .. "|r|h: "
        end
        displayText = displayText .. text

        if isMPlus and dungeonName then
            displayText = displayText .. "  |cff00c8ff[" .. dungeonName
            if keyLevel then displayText = displayText .. " +" .. keyLevel end
            displayText = displayText .. "]|r"
        elseif isMPlus and keyLevel then
            displayText = displayText .. "  |cff00c8ff[M+ +" .. keyLevel .. "]|r"
        end

        if resolvedW > 10 then
            fs:SetWidth(resolvedW - 4)
        end

        fs:SetText(displayText)
        fs:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])
        fs:Show()
        msgBtn:Show()

        local textH = fs:GetStringHeight() or MSG_H
        msgBtn:SetHeight(math.max(MSG_H, textH))
        yOfs = yOfs + math.max(MSG_H, textH) + MSG_GAP
    end

    R.chatContent:SetHeight(math.max(yOfs + 10, 1))

    C_Timer.After(0.05, function()
        if R.chatScroll then
            local maxScroll = R.chatScroll:GetVerticalScrollRange()
            R.chatScroll:SetVerticalScroll(maxScroll)
        end
    end)
end

function Panel.UpdateSortHeaders()
    local R = Panel._refs
    if not R.sortBtns then return end
    for key, btn in pairs(R.sortBtns) do
        if key == Panel._state.sortColumn then
            btn._fs:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
            local arrow = Panel._state.sortAsc and " v" or " ^"
            btn._fs:SetText(btn._key:sub(1,1):upper() .. btn._key:sub(2) .. arrow)
        else
            btn._fs:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3])
            local filterMode = Panel._state.filterMode or "default"
            local lastLabel = "Last Seen"
            if filterMode == "achievePts" then lastLabel = "Ach"
            elseif filterMode == "mythicPlus" then lastLabel = "M+"
            elseif filterMode == "mpiScore" then lastLabel = "MPI"
            end
            local labels = { name="Name", level="Lvl", rank="Rank", zone="Zone", lastOn=lastLabel }
            if labels[key] then btn._fs:SetText(labels[key]) end
        end
    end
end


local noGuildOverlay = nil
local function EnsureNoGuildOverlay()
    if noGuildOverlay then return noGuildOverlay end
    local R = Panel._refs
    if not R or not R.panel then return nil end

    local classArtifactBGs = {
        DEATHKNIGHT = "Artifacts-DeathKnightFrost-BG",
        DEMONHUNTER = "Artifacts-DemonHunter-BG",
        DRUID       = "Artifacts-Druid-BG",
        HUNTER      = "Artifacts-Hunter-BG",
        MAGE        = "Artifacts-MageArcane-BG",
        MONK        = "Artifacts-Monk-BG",
        PALADIN     = "Artifacts-Paladin-BG",
        PRIEST      = "Artifacts-Priest-BG",
        ROGUE       = "Artifacts-Rogue-BG",
        SHAMAN      = "Artifacts-Shaman-BG",
        WARLOCK     = "Artifacts-Warlock-BG",
        WARRIOR     = "Artifacts-Warrior-BG",
        EVOKER      = "Artifacts-MageArcane-BG",
    }
    local _, playerClass = UnitClass("player")
    if playerClass == "PRIEST" then
        local specIndex = GetSpecialization and GetSpecialization()
        if specIndex then
            local specID = GetSpecializationInfo and GetSpecializationInfo(specIndex)
            if specID == 258 then
                classArtifactBGs["PRIEST"] = "Artifacts-PriestShadow-BG"
            end
        end
    end

    local scrim = CreateFrame("Frame", nil, R.panel)
    scrim:SetPoint("TOPLEFT", R.panel, "TOPLEFT", 0, -CFG.HEADER_H)
    scrim:SetPoint("BOTTOMRIGHT", R.panel, "BOTTOMRIGHT", 0, 0)
    scrim:SetFrameLevel(R.panel:GetFrameLevel() + 20)

    local artBG = classArtifactBGs[playerClass]
    if artBG then
        local classBg = scrim:CreateTexture(nil, "BACKGROUND", nil, 0)
        classBg:SetAllPoints()
        classBg:SetAtlas(artBG, false)
        classBg:SetAlpha(0.3)
    end

    local darkOverlay = scrim:CreateTexture(nil, "BACKGROUND", nil, 1)
    darkOverlay:SetAllPoints()
    darkOverlay:SetColorTexture(0.01, 0.01, 0.02, 0.65)

    local card = CreateFrame("Frame", nil, scrim, "BackdropTemplate")
    card:SetSize(340, 180)
    card:SetPoint("CENTER", scrim, "CENTER", 0, 20)
    card:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    card:SetBackdropColor(0.04, 0.04, 0.06, 0.95)
    card:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.3)

    for i = 1, 5 do
        local s = card:CreateTexture(nil, "BACKGROUND", nil, -1)
        local off = i * 2
        s:SetColorTexture(0, 0, 0, 0.18 - (i * 0.03))
        s:SetPoint("TOPLEFT", -off, off); s:SetPoint("BOTTOMRIGHT", off, -off)
    end

    local topBar = card:CreateTexture(nil, "OVERLAY", nil, 4)
    topBar:SetHeight(2); topBar:SetPoint("TOPLEFT", 1, -1); topBar:SetPoint("TOPRIGHT", -1, -1)
    topBar:SetTexture(W8)
    if topBar.SetGradient and CreateColor then
        topBar:SetGradient("HORIZONTAL",
            CreateColor(C.accent[1], C.accent[2], C.accent[3], 0.8),
            CreateColor(C.accent[1], C.accent[2], C.accent[3], 0.1))
    end

    local icon = card:CreateTexture(nil, "ARTWORK")
    icon:SetSize(40, 40)
    icon:SetPoint("TOP", card, "TOP", 0, -22)
    icon:SetTexture("Interface\\GuildFrame\\GuildLogo-NoLogo")
    icon:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 0.6)

    local title = card:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOP", icon, "BOTTOM", 0, -10)
    title:SetText("You are not in a Guild")
    title:SetTextColor(C.titleText[1], C.titleText[2], C.titleText[3])

    local sub = card:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    sub:SetPoint("TOP", title, "BOTTOM", 0, -6)
    sub:SetText("Find a community to join!")
    sub:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.7)

    local btn = CreateFrame("Button", nil, card, "BackdropTemplate")
    btn:SetSize(180, 32)
    btn:SetPoint("TOP", sub, "BOTTOM", 0, -16)
    btn:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    btn:SetBackdropColor(C.accent[1] * 0.25, C.accent[2] * 0.25, C.accent[3] * 0.25, 0.9)
    btn:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.5)

    local btnText = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    btnText:SetPoint("CENTER")
    btnText:SetText("Open Guild Finder")
    btnText:SetTextColor(C.accent[1], C.accent[2], C.accent[3])

    btn:SetScript("OnEnter", function(self)
        self:SetBackdropColor(C.accent[1] * 0.4, C.accent[2] * 0.4, C.accent[3] * 0.4, 0.9)
        self:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.8)
    end)
    btn:SetScript("OnLeave", function(self)
        self:SetBackdropColor(C.accent[1] * 0.25, C.accent[2] * 0.25, C.accent[3] * 0.25, 0.9)
        self:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.5)
    end)
    btn:SetScript("OnClick", function()
        Panel.Hide()
        if ToggleGuildFinder then
            ToggleGuildFinder()
        elseif Communities_LoadUI and CommunitiesFrame then
            Communities_LoadUI()
            CommunitiesFrame:Show()
            if CommunitiesFrame.GuildFinderFrame then
                CommunitiesFrame:SelectSettingsCategory(CommunitiesFrame.GuildFinderFrame)
            end
        elseif ToggleGuildFrame then
            ToggleGuildFrame()
        end
    end)

    local fadeIn = scrim:CreateAnimationGroup()
    local fi = fadeIn:CreateAnimation("Alpha"); fi:SetFromAlpha(0); fi:SetToAlpha(1); fi:SetDuration(0.25); fi:SetSmoothing("OUT")
    scrim._fadeIn = fadeIn

    scrim:Hide()
    noGuildOverlay = scrim
    return noGuildOverlay
end

function Panel.UpdateGuildInfo()
    local R = Panel._refs
    local data = GetGuildData()
    if not data then
        if R.headerTitle then R.headerTitle:SetText("No Guild") end
        if R.heroGuildName then R.heroGuildName:SetText("") end
        if R.motdFS then R.motdFS:SetText("") end
        if R.infoFS then R.infoFS:SetText("") end
        if R.tabardModel then R.tabardModel:Hide() end
        if R.emblemIcon then R.emblemIcon:Hide() end
        if R.hero then R.hero:Hide() end
        if R.rosterPanel then R.rosterPanel:Hide() end
        if R.chatPanel then R.chatPanel:Hide() end
        local overlay = EnsureNoGuildOverlay()
        if overlay then
            overlay:Show()
            if overlay._fadeIn then overlay._fadeIn:Play() end
        end
        return
    end

    if noGuildOverlay and noGuildOverlay:IsShown() then noGuildOverlay:Hide() end
    if R.hero and not R.hero:IsShown() then R.hero:Show() end
    if R.rosterPanel and not R.rosterPanel:IsShown() then R.rosterPanel:Show() end
    if R.chatPanel and not R.chatPanel:IsShown() then R.chatPanel:Show() end

    if R.headerTitle then
        local titleText = data.name
        if data.realm and data.realm ~= "" then titleText = titleText .. "  \194\183  " .. data.realm end
        R.headerTitle:SetText(titleText)
    end

    if R.heroGuildName then R.heroGuildName:SetText(data.name) end

    if R.motdFS then
        if data.motd and data.motd ~= "" then
            R.motdFS:SetText(data.motd)
            R.motdFS:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])
        else
            R.motdFS:SetText("No message of the day")
            R.motdFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.5)
        end
        if R.motdPendingText then
            local current = (data.motd or ""):gsub("^%s+", ""):gsub("%s+$", "")
            if current == R.motdPendingText then
                R.motdPendingText = nil
                if R.motdPendingFS then R.motdPendingFS:Hide() end
            end
        end
    end

    local guildInfoText = SafeCall(GetGuildInfoText) or ""
    if R.gInfoFS and R.gInfoCard then
        if guildInfoText ~= "" then
            R.gInfoFS:SetText(guildInfoText)
            R.gInfoCard:Show()
        else
            R.gInfoFS:SetText("")
            R.gInfoCard:Hide()
        end
    end

    if Panel._state.challengesExpanded then
        Panel.UpdateGuildChallenges()
    end

    Panel.SizeInfoCards()

    if R.infoFS then
        local info = FormatNumber(data.totalMembers) .. " Members"
            .. "  |cff888888\194\183|r  "
            .. "|cff" .. string.format("%02x%02x%02x", C.online[1]*255, C.online[2]*255, C.online[3]*255)
            .. FormatNumber(data.onlineMembers) .. " Online|r"
            .. "  |cff888888\194\183|r  "
            .. "Rank: |cff" .. string.format("%02x%02x%02x", C.accent[1]*255, C.accent[2]*255, C.accent[3]*255)
            .. data.rankName .. "|r"
        R.infoFS:SetText(info)
    end

    local emblemShown = false

    if R.tabardModel then
        if not R._tabardModelReady then
            local modelOk = pcall(function()
                R.tabardModel:SetUnit("player")
                R.tabardModel:SetPortraitZoom(1)
                R.tabardModel:SetCamDistanceScale(0.8)
                R.tabardModel:SetPosition(0, 0, 0)
                R.tabardModel:SetFacing(0)
                R.tabardModel:SetAlpha(1)
            end)
            if modelOk then
                R._tabardModelReady = true
            end
        end
        if R._tabardModelReady then
            R.tabardModel:Show()
            if R.emblemIcon then R.emblemIcon:Hide() end
            emblemShown = true
        end
    end

    if not emblemShown and R.emblemIcon then
        if C_GuildInfo and C_GuildInfo.GetGuildTabardInfo then
            local tabardInfo = SafeCall(C_GuildInfo.GetGuildTabardInfo, "player")
            if tabardInfo and tabardInfo.emblemFileID then
                R.emblemIcon:SetTexture(tabardInfo.emblemFileID)
                R.emblemIcon:SetVertexColor(1, 1, 1, 1)
                R.emblemIcon:SetTexCoord(0, 1, 0, 1)
                R.emblemIcon:Show()
                if R.tabardModel then R.tabardModel:Hide() end
                emblemShown = true
            end
        end
    end

    if not emblemShown and R.emblemIcon then
        if GetGuildTabardFiles then
            local _, _, tabardEmblem = SafeCall(GetGuildTabardFiles)
            if tabardEmblem and type(tabardEmblem) == "number" and tabardEmblem > 0 then
                R.emblemIcon:SetTexture(tabardEmblem)
                R.emblemIcon:SetVertexColor(1, 1, 1, 1)
                R.emblemIcon:SetTexCoord(0, 1, 0, 1)
                R.emblemIcon:Show()
                if R.tabardModel then R.tabardModel:Hide() end
                emblemShown = true
            end
        end
    end

    if not emblemShown and R.emblemIcon then
        R.emblemIcon:SetTexture("Interface\\GuildFrame\\GuildLogo-NoLogo")
        R.emblemIcon:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 0.7)
        R.emblemIcon:SetTexCoord(0, 1, 0, 1)
        R.emblemIcon:Show()
        if R.tabardModel then R.tabardModel:Hide() end
    end
end

function Panel.UpdateRoster()
    local R = Panel._refs
    if not R.rosterContent then return end

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

    local online, offline = CollectRoster(
        Panel._state.searchText,
        Panel._state.sortColumn,
        Panel._state.sortAsc
    )

    local yOfs = 0
    local rosterW = R.rosterPanel and R.rosterPanel:GetWidth() or (CFG.WIDTH * CFG.ROSTER_PCT)

    local onlineHdr = CreateFrame("Button", nil, R.rosterContent)
    onlineHdr:EnableMouse(true); onlineHdr:RegisterForClicks("LeftButtonUp")
    onlineHdr:SetHeight(CFG.SECTION_HDR_H)
    onlineHdr:SetPoint("TOPLEFT", R.rosterContent, "TOPLEFT", 0, -yOfs)
    onlineHdr:SetPoint("TOPRIGHT", R.rosterContent, "TOPRIGHT", 0, -yOfs)
    local onBg = onlineHdr:CreateTexture(nil, "BACKGROUND")
    onBg:SetAllPoints(); onBg:SetColorTexture(C.online[1], C.online[2], C.online[3], 0.06)
    local onChevron = onlineHdr:CreateTexture(nil, "OVERLAY")
    onChevron:SetSize(10, 10); onChevron:SetPoint("LEFT", onlineHdr, "LEFT", 6, 0)
    onChevron:SetAtlas("common-dropdown-icon-back")
    onChevron:SetRotation(Panel._state.onlineExpanded and (-math.pi / 2) or math.pi)
    onChevron:SetVertexColor(C.online[1], C.online[2], C.online[3])
    local onLabel = onlineHdr:CreateFontString(nil, "OVERLAY")
    TrySetFont(onLabel, BODY_FONT, 10, "OUTLINE")
    onLabel:SetPoint("LEFT", onChevron, "RIGHT", 4, 0)
    onLabel:SetText("ONLINE (" .. #online .. ")")
    onLabel:SetTextColor(C.online[1], C.online[2], C.online[3])
    onlineHdr:SetScript("OnClick", function()
        Panel._state.onlineExpanded = not Panel._state.onlineExpanded
        Panel.UpdateRoster()
    end)
    onlineHdr:Show()
    yOfs = yOfs + CFG.SECTION_HDR_H

    if Panel._state.onlineExpanded then
        for _, entry in ipairs(online) do
            CreateRosterRow(R.rosterContent, entry, yOfs, rosterW)
            yOfs = yOfs + CFG.ROSTER_ROW_H
        end
    end

    local offlineHdr = CreateFrame("Button", nil, R.rosterContent)
    offlineHdr:EnableMouse(true); offlineHdr:RegisterForClicks("LeftButtonUp")
    offlineHdr:SetHeight(CFG.SECTION_HDR_H)
    offlineHdr:SetPoint("TOPLEFT", R.rosterContent, "TOPLEFT", 0, -yOfs)
    offlineHdr:SetPoint("TOPRIGHT", R.rosterContent, "TOPRIGHT", 0, -yOfs)
    local offBg = offlineHdr:CreateTexture(nil, "BACKGROUND")
    offBg:SetAllPoints(); offBg:SetColorTexture(C.offline[1], C.offline[2], C.offline[3], 0.04)
    local offChevron = offlineHdr:CreateTexture(nil, "OVERLAY")
    offChevron:SetSize(10, 10); offChevron:SetPoint("LEFT", offlineHdr, "LEFT", 6, 0)
    offChevron:SetAtlas("common-dropdown-icon-back")
    offChevron:SetRotation(Panel._state.offlineExpanded and (-math.pi / 2) or math.pi)
    offChevron:SetVertexColor(C.offline[1], C.offline[2], C.offline[3])
    local offLabel = offlineHdr:CreateFontString(nil, "OVERLAY")
    TrySetFont(offLabel, BODY_FONT, 10, "OUTLINE")
    offLabel:SetPoint("LEFT", offChevron, "RIGHT", 4, 0)
    offLabel:SetText("OFFLINE (" .. #offline .. ")")
    offLabel:SetTextColor(C.offline[1], C.offline[2], C.offline[3])
    offlineHdr:SetScript("OnClick", function()
        Panel._state.offlineExpanded = not Panel._state.offlineExpanded
        Panel.UpdateRoster()
    end)
    offlineHdr:Show()
    yOfs = yOfs + CFG.SECTION_HDR_H

    if Panel._state.offlineExpanded then
        for _, entry in ipairs(offline) do
            CreateRosterRow(R.rosterContent, entry, yOfs, rosterW)
            yOfs = yOfs + CFG.ROSTER_ROW_H
        end
    end

    R.rosterContent:SetHeight(math.max(yOfs + 10, 1))
end

function Panel.ShowNoteEditor(name, rosterIndex)
    local R = Panel._refs
    if not R.panel then return end
    if R.noteEditor then R.noteEditor:Hide() end

    local ne = CreateFrame("Frame", nil, R.panel, "BackdropTemplate")
    ne:SetSize(300, 100)
    ne:SetPoint("CENTER", R.panel, "CENTER", 0, 0)
    ne:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    ne:SetBackdropColor(0.04, 0.04, 0.06, 0.96)
    ne:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.4)
    ne:SetFrameLevel(R.panel:GetFrameLevel() + 35)
    R.noteEditor = ne

    local neTitle = ne:CreateFontString(nil, "OVERLAY")
    TrySetFont(neTitle, BODY_FONT, 10, "OUTLINE")
    neTitle:SetPoint("TOP", ne, "TOP", 0, -8)
    local shortName = name:match("^([^%-]+)") or name
    neTitle:SetText("NOTE: " .. shortName)
    neTitle:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3])

    local neInput = CreateFrame("EditBox", nil, ne, "BackdropTemplate")
    neInput:SetHeight(28)
    neInput:SetPoint("TOPLEFT", ne, "TOPLEFT", 12, -28)
    neInput:SetPoint("TOPRIGHT", ne, "TOPRIGHT", -12, -28)
    neInput:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 6, right = 6, top = 4, bottom = 4 } })
    neInput:SetBackdropColor(C.inputBg[1], C.inputBg[2], C.inputBg[3], C.inputBg[4])
    neInput:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.2)
    TrySetFont(neInput, BODY_FONT, 11, "")
    neInput:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])
    neInput:SetAutoFocus(true)
    if rosterIndex then
        local _, _, _, _, _, _, publicNote = SafeCall(GetGuildRosterInfo, rosterIndex)
        neInput:SetText(publicNote or "")
    end
    neInput:SetScript("OnEscapePressed", function() ne:Hide() end)
    neInput:SetScript("OnEnterPressed", function(self)
        if rosterIndex and GuildRosterSetPublicNote then
            pcall(GuildRosterSetPublicNote, rosterIndex, self:GetText() or "")
        end
        ne:Hide()
    end)

    local saveBtn = CreateFrame("Button", nil, ne)
    saveBtn:SetSize(60, 22); saveBtn:SetPoint("BOTTOMRIGHT", ne, "BOTTOMRIGHT", -12, 8)
    local saveFS = saveBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(saveFS, BODY_FONT, 11, ""); saveFS:SetPoint("CENTER"); saveFS:SetText("Save")
    saveFS:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
    saveBtn:SetScript("OnClick", function()
        if rosterIndex and GuildRosterSetPublicNote then
            pcall(GuildRosterSetPublicNote, rosterIndex, neInput:GetText() or "")
        end
        ne:Hide()
    end)

    local cancelBtn = CreateFrame("Button", nil, ne)
    cancelBtn:SetSize(50, 22); cancelBtn:SetPoint("RIGHT", saveBtn, "LEFT", -8, 0)
    local cancelFS = cancelBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(cancelFS, BODY_FONT, 11, ""); cancelFS:SetPoint("CENTER"); cancelFS:SetText("Cancel")
    cancelFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3])
    cancelBtn:SetScript("OnClick", function() ne:Hide() end)

    ne:Show()
end

function Panel.UpdateRankQueueBanner()
    local R = Panel._refs
    if not R.queueBanner then return end
    local n = RankQueueCount()
    if n == 0 then
        R.queueBanner:Hide()
        return
    end
    local settings = _G.MidnightUISettings or {}
    local gp = settings.GuildPanel or {}
    local hk = gp[GUILD_ACTION_MACROS.rankQueue.hotkeyKey]
    local label
    local secsLeft = RANK_QUEUE_COOLDOWN_UNTIL - GetTime()
    -- Colors: green (ready/armed), accent (awaiting commit or in cooldown).
    local bgR, bgG, bgB = C.accent[1], C.accent[2], C.accent[3]
    local bgAlpha = 0.30
    if secsLeft > 0 then
        label = string.format("COOLING DOWN (%ds)  ||  %d QUEUED", math.ceil(secsLeft), n)
    elseif RANK_QUEUE_ARMED and hk and hk ~= "" then
        label = string.format("READY!  ||  PRESS %s", hk)
        bgR, bgG, bgB, bgAlpha = 0.25, 0.75, 0.35, 0.55
    elseif hk and hk ~= "" then
        label = string.format("%d MEMBER%s QUEUED  ||  CLICK TO ARM", n, n == 1 and "" or "S")
    else
        label = string.format("%d MEMBER%s QUEUED  ||  CLICK TO BIND HOTKEY",
            n, n == 1 and "" or "S")
    end
    R.queueBanner:SetBackdropColor(bgR, bgG, bgB, bgAlpha)
    R.queueBanner:SetBackdropBorderColor(bgR, bgG, bgB, 0.9)
    R.queueBannerFS:SetText(label)
    R.queueBanner:Show()
end

function Panel.ClearRankQueue()
    for k in pairs(RANK_QUEUE) do RANK_QUEUE[k] = nil end
    LAST_BATCH = {}
    RANK_QUEUE_COOLDOWN_UNTIL = 0
    if Panel.Refresh then Panel.Refresh() end
    Panel.UpdateRankQueueBanner()
end

-- Global entrypoint the macro calls before running its batch. Clears the
-- macro body so a stray second keypress can't double-fire, mirroring GRM's
-- "RMM" pattern. The next batch is rebuilt on GUILD_ROSTER_UPDATE.

_G.MidnightUI_RMM = function()
    local cfg = GUILD_ACTION_MACROS.rankQueue
    if not cfg then return end
    local idx = GetMacroIndexByName and GetMacroIndexByName(cfg.macro) or 0
    if idx > 0 and EditMacro then EditMacro(idx, cfg.macro, cfg.icon, "") end
    LAST_BATCH = {}
    RANK_QUEUE_COOLDOWN_UNTIL = GetTime() + RANK_QUEUE_COOLDOWN
    RANK_QUEUE_ARMED = false
    if Panel.UpdateRankQueueBanner then Panel.UpdateRankQueueBanner() end
    -- Force a fresh roster request so we get GUILD_ROSTER_UPDATE back with the
    -- newly-applied ranks. The event handler redraws the panel on arrival.
    if C_GuildInfo and C_GuildInfo.GuildRoster then
        pcall(C_GuildInfo.GuildRoster)
    elseif GuildRoster then
        pcall(GuildRoster)
    end
    if C_Timer and C_Timer.After then
        -- Server may not have propagated the rank change the first time
        -- GUILD_ROSTER_UPDATE fires — force a fresh query and redraw at
        -- staggered intervals so the roster text catches up.
        for _, t in ipairs({ 0.3, 0.8, 1.5, 3.0 }) do
            C_Timer.After(t, function()
                if Panel.IsOpen and Panel.IsOpen() and Panel.UpdateRoster then
                    if C_GuildInfo and C_GuildInfo.GuildRoster then
                        pcall(C_GuildInfo.GuildRoster)
                    end
                    Panel.UpdateRoster()
                end
            end)
        end
        C_Timer.After(RANK_QUEUE_COOLDOWN + 0.05, function()
            if Panel.RebuildRankQueueMacro then Panel.RebuildRankQueueMacro() end
            if RankQueueCount() > 0 then RANK_QUEUE_ARMED = true end
            if Panel.UpdateRankQueueBanner then Panel.UpdateRankQueueBanner() end
        end)
    end
end

local MACRO_POSTAMBLE = "/run MidnightUI_RMM()"
local MAX_MACRO_LEN = 255

-- Build a batch of rank-change slash commands that fit within the macro
-- limit, with the self-clearing postamble LAST. Each queue entry is a
-- single-step promote or demote, so each member contributes exactly one
-- slash line. Tracks which members ended up in the batch so RMM can drop
-- them from the queue on fire.
local function BuildRankQueueBatch()
    local remainingBudget = MAX_MACRO_LEN - #MACRO_POSTAMBLE - 1
    local lines = {}
    LAST_BATCH = {}
    for name, q in pairs(RANK_QUEUE) do
        local slash = (q.action == "promote") and "/gpromote " or "/gdemote "
        local line = slash .. name
        local need = #line + 1
        if need > remainingBudget then break end
        lines[#lines + 1] = line
        remainingBudget = remainingBudget - need
        LAST_BATCH[name] = true
    end
    if #lines == 0 then return nil end
    return table.concat(lines, "\n") .. "\n" .. MACRO_POSTAMBLE
end


local function WriteRankQueueMacro(body, hk, rebindKey)
    if InCombatLockdown() then return false end
    local cfg = GUILD_ACTION_MACROS.rankQueue
    local idx = GetMacroIndexByName and GetMacroIndexByName(cfg.macro) or 0
    if idx == 0 then
        local _, numAccount = GetNumMacros()
        local maxAccount = MAX_ACCOUNT_MACROS or 120
        if (numAccount or 0) >= maxAccount then
            print("|cffff5555MidnightUI:|r Account macro slots are full.")
            return false
        end
        CreateMacro(cfg.macro, cfg.icon, body, nil)
    elseif EditMacro then
        EditMacro(idx, cfg.macro, cfg.icon, body)
    end
    -- Only touch the key binding when explicitly asked (i.e., on first Commit).
    -- Calling SetBindingMacro/SaveBindings repeatedly from event-driven rebuilds
    -- accumulates taint that poisons chat-editbox dispatch.
    if rebindKey and SetBindingMacro and hk then
        SetBindingMacro(hk, cfg.macro)
        if SaveBindings and GetCurrentBindingSet then
            SaveBindings(GetCurrentBindingSet())
        end
    end
    return true
end

function Panel.RebuildRankQueueMacro()
    if InCombatLockdown() then return end
    if GetTime() < RANK_QUEUE_COOLDOWN_UNTIL then return end
    local cfg = GUILD_ACTION_MACROS.rankQueue
    local settings = _G.MidnightUISettings
    settings.GuildPanel = settings.GuildPanel or {}
    local hk = settings.GuildPanel[cfg.hotkeyKey]
    if not hk or hk == "" then return end
    local body = BuildRankQueueBatch()
    if not body then
        local idx = GetMacroIndexByName and GetMacroIndexByName(cfg.macro) or 0
        if idx > 0 and EditMacro then EditMacro(idx, cfg.macro, cfg.icon, "") end
        return
    end
    -- rebindKey=false: don't re-bind the hotkey on auto-rebuilds (that taints chat
    -- editbox dispatch). The binding was set once during Commit.
    WriteRankQueueMacro(body, hk, false)
end

function Panel.CommitRankQueue()
    local n = RankQueueCount()
    if n == 0 then return end
    if InCombatLockdown() then
        print("|cffff5555MidnightUI:|r Can't configure rank-change macro while in combat.")
        return
    end
    local cfg = GUILD_ACTION_MACROS.rankQueue
    local body = BuildRankQueueBatch()
    if not body then return end
    local function apply(hk)
        if not WriteRankQueueMacro(body, hk, true) then return end
        RANK_QUEUE_ARMED = true
        Panel.UpdateRankQueueBanner()
    end
    local settings = _G.MidnightUISettings
    settings.GuildPanel = settings.GuildPanel or {}
    local hotkey = settings.GuildPanel[cfg.hotkeyKey]
    if hotkey and hotkey ~= "" then
        apply(hotkey)
    else
        PromptForHotkey(cfg.label, function(hk)
            if not hk then return end
            settings.GuildPanel[cfg.hotkeyKey] = hk
            apply(hk)
        end)
    end
end

function Panel.ShowMOTDEditor()
    local R = Panel._refs
    if R.motdEditor then R.motdEditor:Hide() end

    local me = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    me:SetSize(760, 244)
    me:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
    me:SetFrameStrata("DIALOG")
    me:SetFrameLevel(240)
    me:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    me:SetBackdropColor(C.frameBg[1], C.frameBg[2], C.frameBg[3], 0.95)
    me:SetBackdropBorderColor(C.borderTint[1], C.borderTint[2], C.borderTint[3], 1)
    me:EnableMouse(true)
    R.motdEditor = me

    local title = me:CreateFontString(nil, "OVERLAY")
    TrySetFont(title, TITLE_FONT, 18, "OUTLINE")
    title:SetPoint("TOPLEFT", me, "TOPLEFT", 16, -12)
    title:SetPoint("TOPRIGHT", me, "TOPRIGHT", -40, -12)
    title:SetJustifyH("LEFT")
    title:SetText("Edit Message of the Day")
    title:SetTextColor(C.titleText[1], C.titleText[2], C.titleText[3])

    local sub = me:CreateFontString(nil, "OVERLAY")
    TrySetFont(sub, BODY_FONT, 12, "")
    sub:SetPoint("TOPLEFT", me, "TOPLEFT", 16, -40)
    sub:SetPoint("TOPRIGHT", me, "TOPRIGHT", -16, -40)
    sub:SetJustifyH("LEFT")
    sub:SetText("Edit the text, click Save, then press your MOTD hotkey to commit.")
    sub:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3])

    local box = CreateFrame("Frame", nil, me, "BackdropTemplate")
    box:SetPoint("TOPLEFT", me, "TOPLEFT", 16, -66)
    box:SetPoint("TOPRIGHT", me, "TOPRIGHT", -16, -66)
    box:SetHeight(108)
    box:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    box:SetBackdropColor(C.inputBg[1], C.inputBg[2], C.inputBg[3], 0.95)
    box:SetBackdropBorderColor(C.borderTint[1], C.borderTint[2], C.borderTint[3], 0.8)

    local scroll = CreateFrame("ScrollFrame", nil, box, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", 8, -10)
    scroll:SetPoint("BOTTOMRIGHT", -28, 10)

    local edit = CreateFrame("EditBox", nil, scroll)
    edit:SetMultiLine(true)
    edit:SetMaxLetters(128)
    edit:SetAutoFocus(true)
    edit:SetWidth(700)
    TrySetFont(edit, TITLE_FONT, 14, "")
    edit:SetTextColor(C.titleText[1], C.titleText[2], C.titleText[3])
    if edit.SetTextInsets then edit:SetTextInsets(8, 8, 4, 4) end
    edit:SetJustifyH("LEFT")
    edit:SetText(SafeCall(GetGuildRosterMOTD) or "")
    edit:SetCursorPosition(0)
    edit:SetScript("OnEscapePressed", function() me:Hide() end)
    scroll:SetScrollChild(edit)

    local function MakeButton(label, width)
        local b = CreateFrame("Button", nil, me, "BackdropTemplate")
        b:SetSize(width or 120, 30)
        b:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        b:SetBackdropColor(C.panelBg[1], C.panelBg[2], C.panelBg[3], 0.95)
        b:SetBackdropBorderColor(C.borderTint[1], C.borderTint[2], C.borderTint[3], 0.8)
        local fs = b:CreateFontString(nil, "OVERLAY")
        TrySetFont(fs, BODY_FONT, 13, "OUTLINE")
        fs:SetPoint("CENTER")
        fs:SetText(label)
        fs:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])
        b:SetScript("OnEnter", function() b:SetBackdropColor(C.accent[1], C.accent[2], C.accent[3], 0.25) end)
        b:SetScript("OnLeave", function() b:SetBackdropColor(C.panelBg[1], C.panelBg[2], C.panelBg[3], 0.95) end)
        return b
    end

    local btnSave = MakeButton("Save", 130)
    btnSave:SetPoint("BOTTOM", me, "BOTTOM", -72, 16)
    btnSave:SetScript("OnClick", function()
        local text = edit:GetText() or ""
        local cfg = GUILD_ACTION_MACROS.motd
        local settings = _G.MidnightUISettings
        settings.GuildPanel = settings.GuildPanel or {}
        local hotkey = settings.GuildPanel[cfg.hotkeyKey]
        local function apply(hk)
            if InCombatLockdown() then
                print("|cffff5555MidnightUI:|r Can't configure macro while in combat.")
                return
            end
            local body = cfg.slash .. " " .. text
            local idx = GetMacroIndexByName and GetMacroIndexByName(cfg.macro) or 0
            if idx == 0 then
                local _, numAccount = GetNumMacros()
                local maxAccount = MAX_ACCOUNT_MACROS or 120
                if (numAccount or 0) >= maxAccount then
                    print("|cffff5555MidnightUI:|r Account macro slots are full.")
                    return
                end
                CreateMacro(cfg.macro, cfg.icon, body, nil)
            elseif EditMacro then
                EditMacro(idx, cfg.macro, cfg.icon, body)
            end
            if SetBindingMacro and hk then
                SetBindingMacro(hk, cfg.macro)
                if SaveBindings and GetCurrentBindingSet then
                    SaveBindings(GetCurrentBindingSet())
                end
            end
            local Rlocal = Panel._refs
            Rlocal.motdPendingText = (text:gsub("^%s+", ""):gsub("%s+$", ""))
            if Rlocal.motdPendingFS then
                Rlocal.motdPendingFS:SetText("|cffffd100PENDING|r || CONFIRM WITH " .. hk)
                Rlocal.motdPendingFS:Show()
            end
            me:Hide()
        end
        if hotkey and hotkey ~= "" then
            apply(hotkey)
        else
            if edit.ClearFocus then edit:ClearFocus() end
            PromptForHotkey(cfg.label, function(hk)
                if not hk then return end
                settings.GuildPanel[cfg.hotkeyKey] = hk
                apply(hk)
            end)
        end
    end)

    local btnClose = MakeButton("Cancel", 130)
    btnClose:SetPoint("LEFT", btnSave, "RIGHT", 10, 0)
    btnClose:SetScript("OnClick", function() me:Hide() end)

    local btnCorner = MakeButton("X", 24)
    btnCorner:SetSize(24, 24)
    btnCorner:SetPoint("TOPRIGHT", -8, -8)
    btnCorner:SetScript("OnClick", function() me:Hide() end)

    me:Show()
end

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

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

    local s = _G.MidnightUISettings
    if s and s.GuildPanel and s.GuildPanel.uiScale and p.SetScale then
        p:SetScale(s.GuildPanel.uiScale)
        local R = Panel._refs
        if R.guildScaleValFS then
            R.guildScaleValFS:SetText(string.format("%.2f", s.GuildPanel.uiScale))
        end
    end

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

    SafeCall(GuildRoster)

    local R = Panel._refs
    if R.recruitBtn and R.perksBtn then
        local isOfficer = false
        local api = _G.MidnightUI_GuildRecruitAPI
        if api and api.CanManageRecruitment then
            isOfficer = api.CanManageRecruitment()
        end

        if isOfficer then
            R.recruitBtn:Show()
            if R.gearBtn then
                R.gearBtn:ClearAllPoints()
                R.gearBtn:SetPoint("RIGHT", R.recruitBtn, "LEFT", -6, 0)
            end
            if api then
                api._headerBadgeDot = R.recruitBadgeDot
                api._headerBadgeCount = R.recruitBadgeCount
                api.RefreshBadge()
            end
        else
            R.recruitBtn:Hide()
            if R.gearBtn then
                R.gearBtn:ClearAllPoints()
                R.gearBtn:SetPoint("RIGHT", R.perksBtn, "LEFT", -6, 0)
            end
        end
    end

    Panel.UpdateGuildInfo()
    Panel.UpdateSortHeaders()
    Panel.UpdateRoster()
    Panel.UpdateChat()
end

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

    R.panel:SetBackdropColor(C.frameBg[1], C.frameBg[2], C.frameBg[3], C.frameBg[4])
    R.panel:SetBackdropBorderColor(C.borderTint[1], C.borderTint[2], C.borderTint[3], C.borderTint[4])

    if R.hdrBg then R.hdrBg:SetColorTexture(C.headerBg[1], C.headerBg[2], C.headerBg[3], C.headerBg[4]) end
    if R.headerTitle then R.headerTitle:SetTextColor(C.titleText[1], C.titleText[2], C.titleText[3]) end

    if R.heroBg then R.heroBg:SetColorTexture(C.heroBg[1], C.heroBg[2], C.heroBg[3], C.heroBg[4]) end
    if R.heroGuildName then R.heroGuildName:SetTextColor(C.titleText[1], C.titleText[2], C.titleText[3]) end
    if R.infoFS then R.infoFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3]) end
    if R.emblemGlow then R.emblemGlow:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 0.25) end
    if R.emblemBorders then
        for _, b in ipairs(R.emblemBorders) do b:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.4) end
    end
    if R.heroGlow and R.heroGlow.SetGradient and CreateColor then
        R.heroGlow:SetGradient("VERTICAL",
            CreateColor(0, 0, 0, 0),
            CreateColor(C.accent[1], C.accent[2], C.accent[3], 0.12))
    end
    if R.identityLine and R.identityLine.SetGradient and CreateColor then
        R.identityLine:SetGradient("HORIZONTAL",
            CreateColor(C.accent[1], C.accent[2], C.accent[3], 0.5),
            CreateColor(C.accent[1], C.accent[2], C.accent[3], 0.0))
    end

    if R.rosterBg then R.rosterBg:SetColorTexture(C.panelBg[1], C.panelBg[2], C.panelBg[3], C.panelBg[4]) end

    if R.colDiv then R.colDiv:SetColorTexture(C.divider[1], C.divider[2], C.divider[3], 0.2) end

    if R.chatBg then R.chatBg:SetColorTexture(C.chatBg[1], C.chatBg[2], C.chatBg[3], C.chatBg[4]) end

    if R.searchBox then
        R.searchBox:SetBackdropColor(C.inputBg[1], C.inputBg[2], C.inputBg[3], C.inputBg[4])
        R.searchBox:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.15)
        R.searchBox:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])
    end

    if R.chatInput then
        R.chatInput:SetBackdropColor(C.inputBg[1], C.inputBg[2], C.inputBg[3], C.inputBg[4])
        R.chatInput:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.2)
        R.chatInput:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])
    end
    if R.chanBtn then
        R.chanBtn:SetBackdropColor(C.inputBg[1], C.inputBg[2], C.inputBg[3], C.inputBg[4])
    end

    if R.ctxMenu then
        R.ctxMenu:SetBackdropColor(C.headerBg[1], C.headerBg[2], C.headerBg[3], 0.96)
        R.ctxMenu:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.35)
    end

    if R.settingsPopup then
        R.settingsPopup:SetBackdropColor(C.headerBg[1], C.headerBg[2], C.headerBg[3], 0.96)
        R.settingsPopup:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.5)
    end

    if R.motdCard then
        R.motdCard:SetBackdropColor(C.chatBg[1], C.chatBg[2], C.chatBg[3], 0.5)
        R.motdCard:SetBackdropBorderColor(C.divider[1], C.divider[2], C.divider[3], 0.1)
    end
    if R.motdLbl then R.motdLbl:SetTextColor(C.accent[1], C.accent[2], C.accent[3]) end
    if R.gInfoCard then
        R.gInfoCard:SetBackdropColor(C.chatBg[1], C.chatBg[2], C.chatBg[3], 0.5)
        R.gInfoCard:SetBackdropBorderColor(C.divider[1], C.divider[2], C.divider[3], 0.1)
    end
    if R.gInfoLbl then R.gInfoLbl:SetTextColor(C.accent[1], C.accent[2], C.accent[3]) end

    Panel.UpdateSortHeaders()

    Panel.UpdateGuildInfo()
    Panel.UpdateRoster()
    Panel.UpdateChat()
end

function Panel.SizeInfoCards()
    local R = Panel._refs
    if not R.heroContent then return end
    C_Timer.After(0.08, function()
        if not R.motdCard then return end
        local motdH = R.motdFS and R.motdFS:GetStringHeight() or 14
        R.motdCard:SetHeight(8 + 14 + 6 + motdH + 8)
        local totalH = R.motdCard:GetHeight() + 8

        if R.gInfoCard and R.gInfoCard:IsShown() and R.gInfoFS then
            local gInfoH = R.gInfoFS:GetStringHeight() or 14
            R.gInfoCard:SetHeight(8 + 14 + 6 + gInfoH + 8)
            totalH = totalH + R.gInfoCard:GetHeight() + 8
        end

        if R.challengeCard then
            if Panel._state.challengesExpanded and R.challengeContent and R.challengeContent:IsShown() then
                local ch = R.challengeContent:GetHeight()
                R.challengeCard:SetHeight(26 + 4 + ch + 8)
            else
                R.challengeCard:SetHeight(30)
            end
            totalH = totalH + R.challengeCard:GetHeight() + 8
        end

        if R.newsCard then
            if Panel._state.newsExpanded and R.newsContent and R.newsContent:IsShown() then
                local nh = R.newsContent:GetHeight()
                R.newsCard:SetHeight(26 + 4 + nh + 8)
            else
                R.newsCard:SetHeight(30)
            end
            totalH = totalH + R.newsCard:GetHeight() + 8
        end

        R.heroContent:SetHeight(math.max(totalH, 1))

        if R.heroScroll and R.heroScroll.ScrollBar then
            local scrollH = R.heroScroll:GetHeight()
            if totalH > scrollH then
                R.heroScroll.ScrollBar:Show()
            else
                R.heroScroll.ScrollBar:Hide()
                R.heroScroll:SetVerticalScroll(0)
            end
        end
    end)
end

function Panel.UpdateGuildChallenges()
    local R = Panel._refs
    if not R.challengeContent then return end

    local oldChildren = { R.challengeContent:GetChildren() }
    for _, child in ipairs(oldChildren) do child:Hide() end
    local oldRegions = { R.challengeContent:GetRegions() }
    for _, region in ipairs(oldRegions) do region:Hide() end

    local yOfs = 0
    local ROW_H = 28
    local PAD = 4
    local challengeFound = false

    local CHALLENGE_NAMES = {
        [1] = "Dungeon",
        [2] = "Mythic+ Dungeon",
        [3] = "Raid",
        [5] = "Rated Battleground",
    }

    if GetGuildChallengeInfo then
        for i = 1, 6 do
            local ok, challengeType, current, max, goldReward, goldMaxReward = pcall(GetGuildChallengeInfo, i)
            if ok and challengeType and max and max > 0 then
                challengeFound = true
                local completed = (current or 0) >= max

                local row = CreateFrame("Frame", nil, R.challengeContent)
                row:SetHeight(ROW_H)
                row:SetPoint("TOPLEFT", R.challengeContent, "TOPLEFT", 0, -yOfs)
                row:SetPoint("RIGHT", R.challengeContent, "RIGHT", 0, 0)

                local label = row:CreateFontString(nil, "OVERLAY")
                TrySetFont(label, BODY_FONT, 11, "")
                label:SetPoint("LEFT", row, "LEFT", 0, 2)
                label:SetText(CHALLENGE_NAMES[challengeType] or ("Challenge " .. challengeType))
                label:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])

                local progText = row:CreateFontString(nil, "OVERLAY")
                TrySetFont(progText, BODY_FONT, 11, "")
                progText:SetPoint("RIGHT", row, "RIGHT", 0, 2)
                progText:SetJustifyH("RIGHT")
                local cur = current or 0
                local rewardText = ""
                if goldReward and goldReward > 0 then
                    rewardText = "  |cffffd700(" .. FormatNumber(goldReward) .. "g)|r"
                end

                if completed then
                    progText:SetText("|cff55ff55Complete|r" .. rewardText)

                    local checkIcon = row:CreateTexture(nil, "OVERLAY")
                    checkIcon:SetSize(14, 14)
                    checkIcon:SetPoint("RIGHT", progText, "LEFT", -4, 0)
                    local checkOk = pcall(checkIcon.SetAtlas, checkIcon, "common-icon-checkmark")
                    if checkOk then
                        checkIcon:SetVertexColor(0.3, 1, 0.3, 1)
                    else
                        checkIcon:SetColorTexture(0.3, 1, 0.3, 1)
                    end

                    label:SetTextColor(C.online[1], C.online[2], C.online[3])
                else
                    progText:SetText(cur .. " / " .. max .. rewardText)
                    progText:SetTextColor(C.titleText[1], C.titleText[2], C.titleText[3])
                end

                local trackH = 4
                local track = row:CreateTexture(nil, "ARTWORK")
                track:SetHeight(trackH)
                track:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
                track:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
                track:SetColorTexture(0.12, 0.12, 0.14, 0.5)

                local fill = row:CreateTexture(nil, "ARTWORK", nil, 1)
                fill:SetHeight(trackH)
                fill:SetPoint("TOPLEFT", track, "TOPLEFT", 0, 0)
                local pct = max > 0 and (cur / max) or 0
                fill:SetWidth(1)
                local capturedPct = pct
                local capturedTrack = track
                local capturedFill = fill
                C_Timer.After(0.2, function()
                    local w = capturedTrack:GetWidth()
                    if w > 0 then capturedFill:SetWidth(math.max(1, w * capturedPct)) end
                end)
                if completed then
                    fill:SetColorTexture(C.online[1], C.online[2], C.online[3], 0.8)
                else
                    fill:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.7)
                end

                row:Show()
                yOfs = yOfs + ROW_H + PAD
            end
        end
    end

    if not challengeFound then
        local noFS = R.challengeContent:CreateFontString(nil, "OVERLAY")
        TrySetFont(noFS, BODY_FONT, 10, "")
        noFS:SetPoint("TOPLEFT", R.challengeContent, "TOPLEFT", 0, 0)
        noFS:SetText("Guild Challenges not available")
        noFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.5)
        noFS:Show()
        yOfs = 18
    end

    R.challengeContent:SetHeight(math.max(yOfs, 1))
end

function Panel.UpdateGuildNews()
    local R = Panel._refs
    if not R.newsContent then return end

    local oldRegions = { R.newsContent:GetRegions() }
    for _, region in ipairs(oldRegions) do region:Hide() end

    local yOfs = 0
    local ROW_H = 16
    local newsFound = false

    if _G.MessengerDB and _G.MessengerDB.History and _G.MessengerDB.History["Guild"] then
        local hist = _G.MessengerDB.History["Guild"]
        if hist.messages then
            local events = {}
            local start = math.max(1, #hist.messages - 199)
            for i = start, #hist.messages do
                local msg = hist.messages[i]
                local text = msg.msg or ""
                local author = msg.author or ""
                local ts = msg.timestamp or ""

                local isEvent = false
                if text:find("has earned the achievement") or text:find("has completed") then
                    isEvent = true
                elseif text:find("has been defeated") or text:find("has killed") then
                    isEvent = true
                elseif msg.tag and (msg.tag == "GUILD_ACHIEVEMENT" or msg.tag == "achievement") then
                    isEvent = true
                end

                if isEvent then
                    events[#events + 1] = { text = text, author = author, timestamp = ts }
                end
            end

            local showStart = math.max(1, #events - 19)
            for i = showStart, #events do
                local evt = events[i]
                local fs = R.newsContent:CreateFontString(nil, "OVERLAY")
                TrySetFont(fs, BODY_FONT, 10, "")
                fs:SetPoint("TOPLEFT", R.newsContent, "TOPLEFT", 0, -yOfs)
                fs:SetPoint("RIGHT", R.newsContent, "RIGHT", 0, 0)
                fs:SetJustifyH("LEFT"); fs:SetWordWrap(false)

                local display = ""
                if evt.timestamp ~= "" then
                    display = "|cff666666" .. evt.timestamp .. "|r  "
                end
                display = display .. evt.text
                fs:SetText(display)
                fs:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])
                fs:Show()
                yOfs = yOfs + ROW_H
                newsFound = true
            end
        end
    end

    if not newsFound then
        local fallbackFS = R.newsContent:CreateFontString(nil, "OVERLAY")
        TrySetFont(fallbackFS, BODY_FONT, 10, "")
        fallbackFS:SetPoint("TOPLEFT", R.newsContent, "TOPLEFT", 0, 0)
        fallbackFS:SetText("No recent guild events")
        fallbackFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.5)
        fallbackFS:Show()
        yOfs = 16
    end

    R.newsContent:SetHeight(math.max(yOfs, 1))
end

local perksOverlay = nil

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

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

    if not perksOverlay then
        perksOverlay = CreateFrame("Frame", nil, R.panel, "BackdropTemplate")
        perksOverlay:SetSize(350, 500)
        perksOverlay:SetPoint("TOPRIGHT", R.panel, "TOPRIGHT", -10, -(CFG.HEADER_H + 4))
        perksOverlay:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        perksOverlay:SetBackdropColor(C.frameBg[1], C.frameBg[2], C.frameBg[3], 0.97)
        perksOverlay:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.35)
        perksOverlay:SetFrameLevel(R.panel:GetFrameLevel() + 20)
        perksOverlay:Hide()

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

        local pTitle = perksOverlay:CreateFontString(nil, "OVERLAY")
        TrySetFont(pTitle, BODY_FONT, 10, "OUTLINE")
        pTitle:SetPoint("TOP", perksOverlay, "TOP", 0, -10)
        pTitle:SetText("GUILD PERKS & REWARDS"); pTitle:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3])

        local pScroll = CreateFrame("ScrollFrame", nil, perksOverlay, "UIPanelScrollFrameTemplate")
        pScroll:SetPoint("TOPLEFT", perksOverlay, "TOPLEFT", 10, -28)
        pScroll:SetPoint("BOTTOMRIGHT", perksOverlay, "BOTTOMRIGHT", -24, 8)
        local pContent = CreateFrame("Frame", nil, pScroll)
        pContent:SetWidth(1)
        pScroll:SetScrollChild(pContent)
        pScroll:SetScript("OnSizeChanged", function(self, w) if w > 0 then pContent:SetWidth(w) end end)
        if pScroll.ScrollBar then
            local sb = pScroll.ScrollBar
            if sb.ThumbTexture then sb.ThumbTexture:SetTexture(W8); sb.ThumbTexture:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 0.20); sb.ThumbTexture:SetWidth(3) end
            if sb.ScrollUpButton then sb.ScrollUpButton:SetAlpha(0) end
            if sb.ScrollDownButton then sb.ScrollDownButton:SetAlpha(0) end
        end
        perksOverlay._content = pContent
    end

    local content = perksOverlay._content
    local oldChildren = { content:GetChildren() }
    for _, child in ipairs(oldChildren) do child:Hide() end
    local oldRegions = { content:GetRegions() }
    for _, region in ipairs(oldRegions) do region:Hide() end

    local yOfs = 0
    local ROW_H = 30
    local PAD = 8

    local perksHdr = content:CreateFontString(nil, "OVERLAY")
    TrySetFont(perksHdr, BODY_FONT, 10, "OUTLINE")
    perksHdr:SetPoint("TOPLEFT", content, "TOPLEFT", 0, 0)
    perksHdr:SetText("PERKS"); perksHdr:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
    perksHdr:Show()
    yOfs = yOfs + 14

    if GetNumGuildPerks then
        local numPerks = SafeCall(GetNumGuildPerks) or 0
        for i = 1, numPerks do
            local ok, name, spellID, iconTexture = pcall(GetGuildPerkInfo, i)
            if ok and name and name ~= "" then
                local row = CreateFrame("Button", nil, content)
                row:SetHeight(ROW_H)
                row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -yOfs)
                row:SetPoint("RIGHT", content, "RIGHT", 0, 0)

                local icon = row:CreateTexture(nil, "ARTWORK")
                icon:SetSize(22, 22); icon:SetPoint("LEFT", row, "LEFT", PAD, 0)
                icon:SetTexture(iconTexture or 134400); icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)

                local nameFS = row:CreateFontString(nil, "OVERLAY")
                TrySetFont(nameFS, BODY_FONT, 11, "")
                nameFS:SetPoint("LEFT", icon, "RIGHT", 6, 0)
                nameFS:SetPoint("RIGHT", row, "RIGHT", -PAD, 0)
                nameFS:SetJustifyH("LEFT"); nameFS:SetWordWrap(false)
                nameFS:SetText(name); nameFS:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])

                local capturedSpell = spellID
                row:SetScript("OnEnter", function(self)
                    GameTooltip:SetOwner(self, "ANCHOR_LEFT")
                    if capturedSpell and GameTooltip.SetSpellByID then
                        pcall(GameTooltip.SetSpellByID, GameTooltip, capturedSpell)
                    else
                        GameTooltip:SetText(name, 1, 1, 1)
                    end
                    GameTooltip:Show()
                end)
                row:SetScript("OnLeave", function() GameTooltip:Hide() end)

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

    yOfs = yOfs + 6
    local sep = content:CreateTexture(nil, "OVERLAY")
    sep:SetHeight(1)
    sep:SetPoint("TOPLEFT", content, "TOPLEFT", PAD, -yOfs)
    sep:SetPoint("RIGHT", content, "RIGHT", -PAD, 0)
    sep:SetTexture(W8)
    if sep.SetGradient and CreateColor then
        sep:SetGradient("HORIZONTAL",
            CreateColor(C.accent[1], C.accent[2], C.accent[3], 0.3),
            CreateColor(C.accent[1], C.accent[2], C.accent[3], 0.0))
    end
    sep:Show()
    yOfs = yOfs + 8

    local rewardsHdr = content:CreateFontString(nil, "OVERLAY")
    TrySetFont(rewardsHdr, BODY_FONT, 10, "OUTLINE")
    rewardsHdr:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -yOfs)
    rewardsHdr:SetText("REWARDS"); rewardsHdr:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
    rewardsHdr:Show()
    yOfs = yOfs + 14

    if GetNumGuildRewards then
        local numRewards = SafeCall(GetNumGuildRewards) or 0
        for i = 1, numRewards do
            local ok, achReq, itemID, itemName, iconFileID, quality, cost = pcall(GetGuildRewardInfo, i)
            if ok and itemName and itemName ~= "" then
                local row = CreateFrame("Button", nil, content)
                row:SetHeight(ROW_H)
                row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -yOfs)
                row:SetPoint("RIGHT", content, "RIGHT", 0, 0)

                local icon = row:CreateTexture(nil, "ARTWORK")
                icon:SetSize(22, 22); icon:SetPoint("LEFT", row, "LEFT", PAD, 0)
                icon:SetTexture(iconFileID or 134400); icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)

                local nameFS = row:CreateFontString(nil, "OVERLAY")
                TrySetFont(nameFS, BODY_FONT, 11, "")
                nameFS:SetPoint("LEFT", icon, "RIGHT", 6, 0)
                nameFS:SetJustifyH("LEFT"); nameFS:SetWordWrap(false)
                nameFS:SetText(itemName)
                if quality then
                    local qr, qg, qb = GetItemQualityColor(quality)
                    if qr then nameFS:SetTextColor(qr, qg, qb) end
                else
                    nameFS:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])
                end

                if cost and cost > 0 then
                    local costFS = row:CreateFontString(nil, "OVERLAY")
                    TrySetFont(costFS, BODY_FONT, 10, "")
                    costFS:SetPoint("RIGHT", row, "RIGHT", -PAD, 0)
                    costFS:SetJustifyH("RIGHT")
                    local gold = math.floor(cost / 10000)
                    costFS:SetText("|cffffd700" .. FormatNumber(gold) .. "g|r")
                    nameFS:SetPoint("RIGHT", costFS, "LEFT", -6, 0)
                else
                    nameFS:SetPoint("RIGHT", row, "RIGHT", -PAD, 0)
                end

                local capturedItemID = itemID
                row:SetScript("OnEnter", function(self)
                    GameTooltip:SetOwner(self, "ANCHOR_LEFT")
                    if capturedItemID and GameTooltip.SetItemByID then
                        pcall(GameTooltip.SetItemByID, GameTooltip, capturedItemID)
                    else
                        GameTooltip:SetText(itemName or "Reward", 1, 1, 1)
                    end
                    GameTooltip:Show()
                end)
                row:SetScript("OnLeave", function() GameTooltip:Hide() end)

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

    content:SetHeight(math.max(yOfs + 10, 1))
    perksOverlay:Show()
end

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

function Panel.Hide()
    local R = Panel._refs
    if not R.panel then return end
    if R.ctxMenu then R.ctxMenu:Hide() end
    if R.noteEditor then R.noteEditor:Hide() end
    if R.settingsPopup then R.settingsPopup:Hide() end
    local chatFrame = DEFAULT_CHAT_FRAME or ChatFrame1
    if chatFrame and chatFrame.editBox and chatFrame.editBox:IsShown() then
        if ChatEdit_DeactivateChat then
            pcall(ChatEdit_DeactivateChat, chatFrame.editBox)
        else
            chatFrame.editBox:SetText("")
            chatFrame.editBox:ClearFocus()
            chatFrame.editBox:Hide()
        end
    end
    if R.panel._fadeOut then
        R.panel._fadeOut:Play()
    else
        R.panel:Hide()
    end
    Panel._state.panelOpen = false
end

function Panel.Toggle()
    if Panel._state.panelOpen then
        Panel.Hide()
    else
        Panel.Show()
    end
end

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

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

SafeReg("ADDON_LOADED")
SafeReg("GUILD_ROSTER_UPDATE")
SafeReg("GUILD_MOTD")
SafeReg("CHAT_MSG_GUILD")
SafeReg("CHAT_MSG_OFFICER")
SafeReg("CHAT_MSG_GUILD_ACHIEVEMENT")
SafeReg("PLAYER_GUILD_UPDATE")

evf:SetScript("OnEvent", function(_, event, arg1)
    if event == "GUILD_ROSTER_UPDATE" then
        if Panel.IsOpen() then
            Panel.UpdateGuildInfo()
            Panel.UpdateRoster()
        end
    elseif event == "GUILD_MOTD" then
        if Panel.IsOpen() then Panel.UpdateGuildInfo() end
    elseif event == "CHAT_MSG_GUILD" or event == "CHAT_MSG_OFFICER"
        or event == "CHAT_MSG_GUILD_ACHIEVEMENT" then
        if Panel.IsOpen() then
            C_Timer.After(0.1, function()
                if Panel.IsOpen() then Panel.UpdateChat() end
            end)
        end
    elseif event == "PLAYER_GUILD_UPDATE" then
        if Panel.IsOpen() then Panel.UpdateGuildInfo() end
    end
end)

local hookInstalled = false
local hookGuard = false
local toggleGuildOverridden = false
local pendingGuildOpen = false

local function UseBlizzardGuildPanel()
    local s = MidnightUISettings
    return s and s.General and s.General.useBlizzardGuildPanel == true
end

local function InstallGuildHook()
    if hookInstalled then return end
    if UseBlizzardGuildPanel() then return end
    if type(ToggleGuildFrame) ~= "function" then return end
    hookInstalled = true

    hooksecurefunc("ToggleGuildFrame", function()
        if UseBlizzardGuildPanel() then return end
        if hookGuard then return end
        hookGuard = true
        if CommunitiesFrame and CommunitiesFrame:IsShown() then
            pcall(HideUIPanel, CommunitiesFrame)
        end
        Panel.Toggle()
        C_Timer.After(0, function() hookGuard = false end)
    end)
end

local function InstallToggleGuildOverride()
    if toggleGuildOverridden then return end
    if UseBlizzardGuildPanel() then return end
    if type(_G.ToggleGuildFrame) ~= "function" then return end
    local origToggleGuildFrame = _G.ToggleGuildFrame
    _G.ToggleGuildFrame = function(...)
        if InCombatLockdown() then
            local panelOpen = Panel.IsOpen and Panel.IsOpen()
            local blizzOpen = CommunitiesFrame and CommunitiesFrame.IsShown and CommunitiesFrame:IsShown()
            if not panelOpen and not blizzOpen then
                if pendingGuildOpen then
                    pendingGuildOpen = false
                else
                    pendingGuildOpen = true
                end
                return
            end
        end
        return origToggleGuildFrame(...)
    end
    toggleGuildOverridden = true
end

InstallGuildHook()
InstallToggleGuildOverride()

local hookEvf = CreateFrame("Frame")
hookEvf:RegisterEvent("ADDON_LOADED")
hookEvf:RegisterEvent("PLAYER_REGEN_ENABLED")
hookEvf:SetScript("OnEvent", function(_, event, addon)
    if event == "ADDON_LOADED" then
        if addon == "Blizzard_Communities" or addon == "Blizzard_GuildUI" then
            if UseBlizzardGuildPanel() then return end
            InstallGuildHook()
            InstallToggleGuildOverride()
            C_Timer.After(0.05, function()
                if CommunitiesFrame and CommunitiesFrame:IsShown() then
                    pcall(HideUIPanel, CommunitiesFrame)
                    if not Panel.IsOpen() then Panel.Show() end
                end
            end)
        end
    elseif event == "PLAYER_REGEN_ENABLED" then
        if pendingGuildOpen then
            pendingGuildOpen = false
            if type(_G.ToggleGuildFrame) == "function" then
                pcall(_G.ToggleGuildFrame)
            end
        end
    end
end)


local DUNGEON_PATTERNS = {
    { full = "Magisters' Terrace",       short = "MT",   patterns = { "magister", "mag terrace", "mgt" } },
    { full = "Maisara Caverns",          short = "MC",   patterns = { "maisara", "caverns" } },
    { full = "Nexus%-Point Xenas",       short = "NPX",  patterns = { "nexus", "xenas", "npx" } },
    { full = "Windrunner Spire",         short = "WS",   patterns = { "windrunner", "spire" } },
    { full = "Algeth'ar Academy",        short = "AA",   patterns = { "algeth", "academy", "aa" } },
    { full = "The Seat of the Triumvirate", short = "SotT", patterns = { "seat", "triumvirate", "sott" } },
    { full = "Skyreach",                 short = "SR",   patterns = { "skyreach" } },
    { full = "Pit of Saron",            short = "PoS",  patterns = { "pit of saron", "pit.*saron", "pos" } },
}

local MPLUS_KEYWORDS = {
    "m%+", "mythic%+", "mythic plus", "mythicplus",
    "key", "keys", "keystone",
    "lf[gm].*dungeon", "lfg", "lfm",
    "anyone.*run", "anyone.*key", "anyone.*m%+",
    "need.*tank", "need.*healer", "need.*dps", "need.*heal",
    "looking for.*m", "looking for.*key", "looking for.*dungeon",
    "want to run", "wanna run", "down for", "down to run",
    "push.*key", "pushing.*key",
    "+%d+", "%+%d+",
}

DetectMPlusMessage = function(text)
    if not text or text == "" then return false, nil end
    local lower = text:lower()

    if lower:find("has earned the achievement") then return false, nil end

    local isMPlus = false
    for _, pattern in ipairs(MPLUS_KEYWORDS) do
        if lower:find(pattern) then
            isMPlus = true
            break
        end
    end

    if not isMPlus then return false, nil end

    local dungeonName = nil
    for _, dungeon in ipairs(DUNGEON_PATTERNS) do
        for _, pat in ipairs(dungeon.patterns) do
            if lower:find(pat) then
                dungeonName = dungeon.full
                break
            end
        end
        if dungeonName then break end
    end

    local keyLevel = lower:match("%+(%d+)") or lower:match("(%d+)%s*key") or lower:match("(%d+)%s*m%+")

    return true, dungeonName, keyLevel
end

local mplusToast = nil
local mplusToastState = { active = false, timer = 0, phase = "idle" }

ShowMPlusToast = function(sender, dungeonName, keyLevel)
    if not mplusToast then
        mplusToast = CreateFrame("Frame", nil, UIParent)
        mplusToast:SetHeight(40)
        local messengerFrame = _G.MyMessengerFrame
        if messengerFrame then
            mplusToast:SetPoint("BOTTOMLEFT", messengerFrame, "TOPLEFT", 0, 4)
            mplusToast:SetPoint("BOTTOMRIGHT", messengerFrame, "TOPRIGHT", 0, 4)
        else
            mplusToast:SetSize(420, 50)
            mplusToast:SetPoint("BOTTOM", UIParent, "BOTTOM", 0, 220)
        end
        mplusToast:SetFrameStrata("FULLSCREEN_DIALOG")
        mplusToast:SetFrameLevel(500)
        mplusToast:SetAlpha(0)
        mplusToast:Hide()

        for i = 1, 4 do
            local s = mplusToast:CreateTexture(nil, "BACKGROUND", nil, -1)
            local off = i * 1.5
            s:SetColorTexture(0, 0, 0, 0.15 - (i * 0.03))
            s:SetPoint("TOPLEFT", -off, off); s:SetPoint("BOTTOMRIGHT", off, -off)
        end

        local bg = mplusToast:CreateTexture(nil, "BACKGROUND")
        bg:SetAllPoints(); bg:SetColorTexture(0.04, 0.04, 0.07, 0.95)

        local bTop = mplusToast:CreateTexture(nil, "OVERLAY"); bTop:SetHeight(2)
        bTop:SetPoint("TOPLEFT"); bTop:SetPoint("TOPRIGHT")
        bTop:SetTexture(W8)
        if bTop.SetGradient and CreateColor then
            bTop:SetGradient("HORIZONTAL",
                CreateColor(0.00, 0.78, 1.00, 0.8),
                CreateColor(0.00, 0.78, 1.00, 0.0))
        end
        local bBot = mplusToast:CreateTexture(nil, "OVERLAY"); bBot:SetHeight(1)
        bBot:SetPoint("BOTTOMLEFT"); bBot:SetPoint("BOTTOMRIGHT")
        bBot:SetColorTexture(0.00, 0.78, 1.00, 0.2)
        local bLeft = mplusToast:CreateTexture(nil, "OVERLAY"); bLeft:SetWidth(1)
        bLeft:SetPoint("TOPLEFT"); bLeft:SetPoint("BOTTOMLEFT")
        bLeft:SetColorTexture(0.00, 0.78, 1.00, 0.3)
        local bRight = mplusToast:CreateTexture(nil, "OVERLAY"); bRight:SetWidth(1)
        bRight:SetPoint("TOPRIGHT"); bRight:SetPoint("BOTTOMRIGHT")
        bRight:SetColorTexture(0.00, 0.78, 1.00, 0.1)

        local icon = mplusToast:CreateTexture(nil, "ARTWORK")
        icon:SetSize(22, 22); icon:SetPoint("LEFT", mplusToast, "LEFT", 10, 0)
        icon:SetTexture("Interface\\Icons\\INV_Relics_Hourglass")
        icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)

        local badge = mplusToast:CreateFontString(nil, "OVERLAY")
        TrySetFont(badge, BODY_FONT, 10, "OUTLINE")
        badge:SetPoint("LEFT", icon, "RIGHT", 8, 0)
        badge:SetText("M+")
        badge:SetTextColor(0.00, 0.78, 1.00)

        local titleFS = mplusToast:CreateFontString(nil, "OVERLAY")
        TrySetFont(titleFS, BODY_FONT, 11, "OUTLINE")
        titleFS:SetPoint("LEFT", badge, "RIGHT", 6, 0)
        titleFS:SetTextColor(0.94, 0.90, 0.80)
        titleFS:SetShadowColor(0, 0, 0, 0.8); titleFS:SetShadowOffset(1, -1)
        mplusToast._title = titleFS

        local dot = mplusToast:CreateFontString(nil, "OVERLAY")
        TrySetFont(dot, BODY_FONT, 11, "")
        dot:SetPoint("LEFT", titleFS, "RIGHT", 6, 0)
        dot:SetText("\194\183")
        dot:SetTextColor(0.50, 0.50, 0.55)
        mplusToast._dot = dot

        local subFS = mplusToast:CreateFontString(nil, "OVERLAY")
        TrySetFont(subFS, BODY_FONT, 11, "")
        subFS:SetPoint("LEFT", dot, "RIGHT", 6, 0)
        subFS:SetPoint("RIGHT", mplusToast, "RIGHT", -10, 0)
        subFS:SetJustifyH("LEFT"); subFS:SetWordWrap(false)
        subFS:SetTextColor(0.70, 0.72, 0.76, 0.9)
        mplusToast._sub = subFS

        mplusToast:SetScript("OnUpdate", function(self, elapsed)
            if not mplusToastState.active then return end
            mplusToastState.timer = mplusToastState.timer + elapsed
            if mplusToastState.phase == "fadein" then
                local p = math.min(1, mplusToastState.timer / 0.3)
                self:SetAlpha(p * p * (3 - 2 * p))
                if p >= 1 then mplusToastState.phase = "hold"; mplusToastState.timer = 0 end
            elseif mplusToastState.phase == "hold" then
                self:SetAlpha(1)
                if mplusToastState.timer >= 4.0 then mplusToastState.phase = "fadeout"; mplusToastState.timer = 0 end
            elseif mplusToastState.phase == "fadeout" then
                local p = math.min(1, mplusToastState.timer / 0.5)
                self:SetAlpha(1 - p)
                if p >= 1 then
                    mplusToastState.active = false; mplusToastState.phase = "idle"
                    self:SetAlpha(0); self:Hide()
                end
            end
        end)
    end

    local shortSender = sender:match("^([^%-]+)") or sender
    mplusToast._title:SetText(shortSender)

    local subText = "is looking for a group"
    if dungeonName then
        subText = dungeonName
        if keyLevel then subText = subText .. "  +" .. keyLevel end
    elseif keyLevel then
        subText = "+" .. keyLevel .. " key"
    end
    mplusToast._sub:SetText(subText)

    mplusToastState.active = true
    mplusToastState.timer = 0
    mplusToastState.phase = "fadein"
    mplusToast:SetAlpha(0)
    mplusToast:Show()

    pcall(PlaySound, SOUNDKIT and SOUNDKIT.TELL_MESSAGE or 3081)
end

Panel._detectMPlus = DetectMPlusMessage
Panel._showMPlusToast = ShowMPlusToast

local mplusEvf = CreateFrame("Frame")
mplusEvf:RegisterEvent("CHAT_MSG_GUILD")
mplusEvf:SetScript("OnEvent", function(_, event, msg, sender)
    if event == "CHAT_MSG_GUILD" and msg and sender then
        local isMPlus, dungeonName, keyLevel = DetectMPlusMessage(msg)
        if isMPlus then
            local playerName = SafeCall(UnitName, "player") or ""
            local shortSender = sender:match("^([^%-]+)") or sender
            if shortSender ~= playerName then
                ShowMPlusToast(sender, dungeonName, keyLevel)
            end
        end
    end
end)

local LFG_PREFIX = "MUI_MPLUS"
pcall(C_ChatInfo.RegisterAddonMessagePrefix, LFG_PREFIX)

local S1_DUNGEONS = {
    { name = "Magisters' Terrace",          abbr = "MT",   accent = {0.80, 0.30, 0.30} },
    { name = "Maisara Caverns",             abbr = "MC",   accent = {0.30, 0.70, 0.40} },
    { name = "Nexus-Point Xenas",           abbr = "NPX",  accent = {0.40, 0.50, 0.90} },
    { name = "Windrunner Spire",            abbr = "WS",   accent = {0.65, 0.40, 0.85} },
    { name = "Algeth'ar Academy",           abbr = "AA",   accent = {0.85, 0.65, 0.20} },
    { name = "The Seat of the Triumvirate", abbr = "SotT", accent = {0.20, 0.75, 0.80} },
    { name = "Skyreach",                    abbr = "SR",   accent = {0.90, 0.55, 0.25} },
    { name = "Pit of Saron",               abbr = "PoS",  accent = {0.45, 0.70, 0.95} },
}

local ROLE_DEFS = {
    { key = "TANK",   label = "Tank",   atlas = "roleicon-tank" },
    { key = "HEALER", label = "Healer", atlas = "roleicon-healer" },
    { key = "DPS",    label = "DPS",    atlas = "roleicon-dps" },
}

local lfgState = {
    hosting = false,
    selectedDungeon = nil,
    selectedLevel = "",
    selectedRoles = { TANK = false, HEALER = false, DPS = false },
    applicants = {},
}

local lfgOverlay = nil

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

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

    lfgState.selectedDungeon = nil
    lfgState.selectedLevel = ""
    lfgState.selectedRoles = { TANK = false, HEALER = false, DPS = false }
    lfgState.applicants = {}

    if lfgOverlay then
        if lfgOverlay._lvlInput then lfgOverlay._lvlInput:SetText("") end
        lfgOverlay:Show()
        return
    end

    if not lfgOverlay then
        local PAD = 20

        lfgOverlay = CreateFrame("Frame", nil, R.panel, "BackdropTemplate")
        lfgOverlay:SetSize(440, 495)
        lfgOverlay:SetPoint("CENTER", R.panel, "CENTER", 0, 0)
        lfgOverlay:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        lfgOverlay:SetBackdropColor(0.025, 0.028, 0.045, 0.98)
        lfgOverlay:SetBackdropBorderColor(0.00, 0.55, 0.80, 0.25)
        lfgOverlay:SetFrameLevel(R.panel:GetFrameLevel() + 25)
        lfgOverlay:Hide()

        for i = 1, 6 do
            local s = lfgOverlay:CreateTexture(nil, "BACKGROUND", nil, -1)
            local off = i * 2
            s:SetColorTexture(0, 0, 0, 0.20 - (i * 0.028))
            s:SetPoint("TOPLEFT", -off, off); s:SetPoint("BOTTOMRIGHT", off, -off)
        end

        local topBar = lfgOverlay:CreateTexture(nil, "OVERLAY", nil, 4)
        topBar:SetHeight(3); topBar:SetPoint("TOPLEFT", 1, -1); topBar:SetPoint("TOPRIGHT", -1, -1)
        topBar:SetTexture(W8)
        if topBar.SetGradient and CreateColor then
            topBar:SetGradient("HORIZONTAL",
                CreateColor(0.00, 0.78, 1.00, 0.9),
                CreateColor(0.30, 0.50, 1.00, 0.2))
        end

        local ambGlow = lfgOverlay:CreateTexture(nil, "BACKGROUND", nil, 1)
        ambGlow:SetHeight(60); ambGlow:SetPoint("TOPLEFT", 1, -4); ambGlow:SetPoint("TOPRIGHT", -1, -4)
        ambGlow:SetTexture(W8)
        if ambGlow.SetGradient and CreateColor then
            ambGlow:SetGradient("VERTICAL",
                CreateColor(0, 0, 0, 0),
                CreateColor(0.00, 0.50, 0.80, 0.05))
        end

        local headerBar = CreateFrame("Frame", nil, lfgOverlay)
        headerBar:SetHeight(44); headerBar:SetPoint("TOPLEFT", 0, 0); headerBar:SetPoint("TOPRIGHT", 0, 0)

        local hIcon = headerBar:CreateTexture(nil, "ARTWORK")
        hIcon:SetSize(26, 26); hIcon:SetPoint("LEFT", headerBar, "LEFT", PAD, 0)
        hIcon:SetTexture("Interface\\Icons\\INV_Relics_Hourglass")
        hIcon:SetTexCoord(0.07, 0.93, 0.07, 0.93)

        local hTitle = headerBar:CreateFontString(nil, "OVERLAY")
        TrySetFont(hTitle, TITLE_FONT, 16, "OUTLINE")
        hTitle:SetPoint("LEFT", hIcon, "RIGHT", 10, 2)
        hTitle:SetText("Mythic+ Listing"); hTitle:SetTextColor(1, 1, 1)
        hTitle:SetShadowColor(0, 0, 0, 0.8); hTitle:SetShadowOffset(1, -1)

        local hSub = headerBar:CreateFontString(nil, "OVERLAY")
        TrySetFont(hSub, BODY_FONT, 10, "")
        hSub:SetPoint("TOPLEFT", hTitle, "BOTTOMLEFT", 0, -1)
        hSub:SetText("Create a listing for your guild"); hSub:SetTextColor(0.55, 0.58, 0.65)

        local closeBtn2 = CreateFrame("Button", nil, headerBar)
        closeBtn2:SetSize(22, 22); closeBtn2:SetPoint("RIGHT", headerBar, "RIGHT", -PAD, 0)
        local closeTx2 = closeBtn2:CreateFontString(nil, "OVERLAY")
        TrySetFont(closeTx2, BODY_FONT, 14, "OUTLINE"); closeTx2:SetPoint("CENTER"); closeTx2:SetText("X")
        closeTx2:SetTextColor(0.45, 0.45, 0.50, 0.6)
        closeBtn2:SetScript("OnClick", function() lfgOverlay:Hide() end)
        closeBtn2:SetScript("OnEnter", function() closeTx2:SetTextColor(1, 0.3, 0.3) end)
        closeBtn2:SetScript("OnLeave", function() closeTx2:SetTextColor(0.45, 0.45, 0.50, 0.6) end)

        local hSep = headerBar:CreateTexture(nil, "OVERLAY")
        hSep:SetHeight(1); hSep:SetPoint("BOTTOMLEFT", PAD, 0); hSep:SetPoint("BOTTOMRIGHT", -PAD, 0)
        hSep:SetColorTexture(0.20, 0.22, 0.30, 0.3)

        local yOfs = 52
        local dungLabel = lfgOverlay:CreateFontString(nil, "OVERLAY")
        TrySetFont(dungLabel, BODY_FONT, 10, "OUTLINE")
        dungLabel:SetPoint("TOPLEFT", lfgOverlay, "TOPLEFT", PAD, -yOfs)
        dungLabel:SetText("SELECT DUNGEON"); dungLabel:SetTextColor(0.00, 0.68, 0.95)
        yOfs = yOfs + 18

        local dungeonBtns = {}
        local DUNG_CARD_H = 30
        local DUNG_CARD_GAP = 3
        local gridW = 440 - (PAD * 2)

        for i, dungInfo in ipairs(S1_DUNGEONS) do
            local dungName = dungInfo.name
            local accentColor = dungInfo.accent

            local btn = CreateFrame("Button", nil, lfgOverlay)
            btn:SetSize(gridW, DUNG_CARD_H)
            btn:SetPoint("TOPLEFT", lfgOverlay, "TOPLEFT",
                PAD, -(yOfs + (i - 1) * (DUNG_CARD_H + DUNG_CARD_GAP)))

            local bg = btn:CreateTexture(nil, "BACKGROUND")
            bg:SetAllPoints(); bg:SetColorTexture(0.04, 0.042, 0.065, 0.7)
            btn._bg = bg

            local accent = btn:CreateTexture(nil, "OVERLAY", nil, 2)
            accent:SetWidth(3); accent:SetPoint("TOPLEFT", 0, 0); accent:SetPoint("BOTTOMLEFT", 0, 0)
            accent:SetColorTexture(accentColor[1], accentColor[2], accentColor[3], 0.6)
            btn._accent = accent

            local abbrFS = btn:CreateFontString(nil, "OVERLAY")
            TrySetFont(abbrFS, TITLE_FONT, 10, "OUTLINE")
            abbrFS:SetPoint("LEFT", btn, "LEFT", 12, 0)
            abbrFS:SetText(dungInfo.abbr)
            abbrFS:SetTextColor(accentColor[1], accentColor[2], accentColor[3], 0.8)
            abbrFS:SetWidth(36); abbrFS:SetJustifyH("CENTER")
            btn._abbrFS = abbrFS

            local fs = btn:CreateFontString(nil, "OVERLAY")
            TrySetFont(fs, BODY_FONT, 11, "")
            fs:SetPoint("LEFT", btn, "LEFT", 52, 0)
            fs:SetText(dungName); fs:SetTextColor(0.78, 0.80, 0.85)
            btn._fs = fs

            local bdrB = btn:CreateTexture(nil, "OVERLAY")
            bdrB:SetHeight(1); bdrB:SetPoint("BOTTOMLEFT", 3, 0); bdrB:SetPoint("BOTTOMRIGHT", 0, 0)
            bdrB:SetColorTexture(0.12, 0.14, 0.20, 0.25)
            btn._bdrB = bdrB

            btn:SetScript("OnClick", function()
                lfgState.selectedDungeon = dungName
                for _, db in ipairs(dungeonBtns) do
                    db._bg:SetColorTexture(0.04, 0.042, 0.065, 0.7)
                    db._fs:SetTextColor(0.78, 0.80, 0.85)
                    db._accent:SetAlpha(0.6)
                    db._abbrFS:SetAlpha(0.8)
                    if db._bdrB then db._bdrB:SetColorTexture(0.12, 0.14, 0.20, 0.25) end
                end
                bg:SetColorTexture(accentColor[1] * 0.08, accentColor[2] * 0.08, accentColor[3] * 0.08, 0.9)
                fs:SetTextColor(1, 1, 1)
                accent:SetAlpha(1)
                abbrFS:SetAlpha(1)
                if btn._bdrB then btn._bdrB:SetColorTexture(accentColor[1], accentColor[2], accentColor[3], 0.3) end
            end)
            btn:SetScript("OnEnter", function()
                if lfgState.selectedDungeon ~= dungName then
                    bg:SetColorTexture(0.055, 0.058, 0.085, 0.8)
                end
            end)
            btn:SetScript("OnLeave", function()
                if lfgState.selectedDungeon ~= dungName then
                    bg:SetColorTexture(0.04, 0.042, 0.065, 0.7)
                end
            end)

            dungeonBtns[#dungeonBtns + 1] = btn
        end
        yOfs = yOfs + #S1_DUNGEONS * (DUNG_CARD_H + DUNG_CARD_GAP) + 14

        local lvlLabel = lfgOverlay:CreateFontString(nil, "OVERLAY")
        TrySetFont(lvlLabel, BODY_FONT, 10, "OUTLINE")
        lvlLabel:SetPoint("TOPLEFT", lfgOverlay, "TOPLEFT", PAD, -yOfs)
        lvlLabel:SetText("KEY LEVEL"); lvlLabel:SetTextColor(0.00, 0.68, 0.95)

        local roleLabel = lfgOverlay:CreateFontString(nil, "OVERLAY")
        TrySetFont(roleLabel, BODY_FONT, 10, "OUTLINE")
        roleLabel:SetPoint("LEFT", lfgOverlay, "LEFT", PAD + 110, 0)
        roleLabel:SetPoint("TOP", lvlLabel, "TOP", 0, 0)
        roleLabel:SetText("LOOKING FOR"); roleLabel:SetTextColor(0.00, 0.68, 0.95)
        yOfs = yOfs + 16

        local lvlInput = CreateFrame("EditBox", nil, lfgOverlay, "BackdropTemplate")
        lvlInput:SetSize(90, 36)
        lvlInput:SetPoint("TOPLEFT", lfgOverlay, "TOPLEFT", PAD, -yOfs)
        lvlInput:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 10, right = 10, top = 4, bottom = 4 } })
        lvlInput:SetBackdropColor(0.04, 0.04, 0.06, 0.9)
        lvlInput:SetBackdropBorderColor(0.15, 0.17, 0.25, 0.4)
        TrySetFont(lvlInput, TITLE_FONT, 16, "")
        lvlInput:SetTextColor(0.00, 0.85, 1.00)
        lvlInput:SetAutoFocus(false); lvlInput:SetNumeric(true); lvlInput:SetMaxLetters(3)
        lvlInput:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
        lvlInput:SetScript("OnEditFocusGained", function(self) self:SetBackdropBorderColor(0.00, 0.65, 0.90, 0.5) end)
        lvlInput:SetScript("OnEditFocusLost", function(self) self:SetBackdropBorderColor(0.15, 0.17, 0.25, 0.4) end)
        lvlInput:SetScript("OnTextChanged", function(self) lfgState.selectedLevel = self:GetText() or "" end)
        lfgOverlay._lvlInput = lvlInput

        local plusFS = lfgOverlay:CreateFontString(nil, "OVERLAY")
        TrySetFont(plusFS, TITLE_FONT, 18, "OUTLINE")
        plusFS:SetPoint("RIGHT", lvlInput, "LEFT", -4, 0)
        plusFS:SetText("+"); plusFS:SetTextColor(0.00, 0.78, 1.00, 0.8)

        local ROLE_COLORS = {
            TANK   = { 0.30, 0.60, 1.00 },
            HEALER = { 0.30, 0.90, 0.40 },
            DPS    = { 0.90, 0.30, 0.30 },
        }
        local roleXOfs = PAD + 110
        for _, role in ipairs(ROLE_DEFS) do
            local rc = ROLE_COLORS[role.key] or { 0.7, 0.7, 0.7 }
            local btn = CreateFrame("Button", nil, lfgOverlay)
            btn:SetSize(90, 36)
            btn:SetPoint("TOPLEFT", lfgOverlay, "TOPLEFT", roleXOfs, -yOfs)

            local bg = btn:CreateTexture(nil, "BACKGROUND")
            bg:SetAllPoints(); bg:SetColorTexture(0.04, 0.042, 0.065, 0.7)
            btn._bg = bg

            local bottomAccent = btn:CreateTexture(nil, "OVERLAY", nil, 2)
            bottomAccent:SetHeight(2); bottomAccent:SetPoint("BOTTOMLEFT", 0, 0); bottomAccent:SetPoint("BOTTOMRIGHT", 0, 0)
            bottomAccent:SetColorTexture(rc[1], rc[2], rc[3], 0)
            btn._bottomAccent = bottomAccent

            local icon = btn:CreateTexture(nil, "ARTWORK")
            icon:SetSize(18, 18); icon:SetPoint("LEFT", btn, "LEFT", 8, 0)
            icon:SetDesaturated(true); icon:SetAlpha(0.5)
            if role.atlas then
                icon:SetAtlas(role.atlas)
            end
            btn._icon = icon

            local fs = btn:CreateFontString(nil, "OVERLAY")
            TrySetFont(fs, BODY_FONT, 11, "OUTLINE")
            fs:SetPoint("LEFT", icon, "RIGHT", 5, 0)
            fs:SetText(role.label); fs:SetTextColor(0.60, 0.62, 0.68)
            btn._fs = fs

            local capturedKey = role.key
            btn:SetScript("OnClick", function()
                lfgState.selectedRoles[capturedKey] = not lfgState.selectedRoles[capturedKey]
                if lfgState.selectedRoles[capturedKey] then
                    bg:SetColorTexture(rc[1] * 0.12, rc[2] * 0.12, rc[3] * 0.12, 0.7)
                    fs:SetTextColor(rc[1], rc[2], rc[3])
                    icon:SetDesaturated(false); icon:SetAlpha(1)
                    bottomAccent:SetColorTexture(rc[1], rc[2], rc[3], 0.8)
                else
                    bg:SetColorTexture(0.04, 0.042, 0.065, 0.7)
                    fs:SetTextColor(0.60, 0.62, 0.68)
                    icon:SetDesaturated(true); icon:SetAlpha(0.5)
                    bottomAccent:SetColorTexture(rc[1], rc[2], rc[3], 0)
                end
            end)

            roleXOfs = roleXOfs + 96
        end

        local submitBtn = CreateFrame("Button", nil, lfgOverlay)
        submitBtn:SetHeight(38)
        submitBtn:SetPoint("BOTTOMLEFT", lfgOverlay, "BOTTOMLEFT", PAD, PAD)
        submitBtn:SetPoint("BOTTOMRIGHT", lfgOverlay, "BOTTOMRIGHT", -PAD, PAD)

        local submitBg = submitBtn:CreateTexture(nil, "BACKGROUND")
        submitBg:SetAllPoints(); submitBg:SetTexture(W8)
        if submitBg.SetGradient and CreateColor then
            submitBg:SetGradient("HORIZONTAL",
                CreateColor(0.00, 0.35, 0.55, 0.40),
                CreateColor(0.00, 0.50, 0.70, 0.18))
        end

        local submitIcon = submitBtn:CreateTexture(nil, "ARTWORK")
        submitIcon:SetSize(18, 18); submitIcon:SetPoint("LEFT", submitBtn, "LEFT", 14, 0)
        submitIcon:SetTexture("Interface\\Icons\\INV_Relics_Hourglass")
        submitIcon:SetTexCoord(0.07, 0.93, 0.07, 0.93); submitIcon:SetAlpha(0.7)

        local submitFS = submitBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(submitFS, BODY_FONT, 13, "OUTLINE")
        submitFS:SetPoint("CENTER", submitBtn, "CENTER", 8, 0)
        submitFS:SetText("Send to Guild"); submitFS:SetTextColor(0.00, 0.88, 1.00)

        local sT = submitBtn:CreateTexture(nil, "OVERLAY"); sT:SetHeight(1)
        sT:SetPoint("TOPLEFT"); sT:SetPoint("TOPRIGHT"); sT:SetColorTexture(0.00, 0.65, 0.90, 0.4)
        local sB = submitBtn:CreateTexture(nil, "OVERLAY"); sB:SetHeight(1)
        sB:SetPoint("BOTTOMLEFT"); sB:SetPoint("BOTTOMRIGHT"); sB:SetColorTexture(0.00, 0.65, 0.90, 0.4)
        local sL = submitBtn:CreateTexture(nil, "OVERLAY"); sL:SetWidth(1)
        sL:SetPoint("TOPLEFT"); sL:SetPoint("BOTTOMLEFT"); sL:SetColorTexture(0.00, 0.65, 0.90, 0.4)
        local sR = submitBtn:CreateTexture(nil, "OVERLAY"); sR:SetWidth(1)
        sR:SetPoint("TOPRIGHT"); sR:SetPoint("BOTTOMRIGHT"); sR:SetColorTexture(0.00, 0.65, 0.90, 0.4)

        submitBtn:SetScript("OnEnter", function()
            if submitBg.SetGradient and CreateColor then
                submitBg:SetGradient("HORIZONTAL",
                    CreateColor(0.00, 0.42, 0.65, 0.55),
                    CreateColor(0.00, 0.58, 0.80, 0.35))
            end
            submitFS:SetTextColor(1, 1, 1)
            submitIcon:SetAlpha(1)
        end)
        submitBtn:SetScript("OnLeave", function()
            if submitBg.SetGradient and CreateColor then
                submitBg:SetGradient("HORIZONTAL",
                    CreateColor(0.00, 0.35, 0.55, 0.40),
                    CreateColor(0.00, 0.50, 0.70, 0.18))
            end
            submitFS:SetTextColor(0.00, 0.88, 1.00)
            submitIcon:SetAlpha(0.7)
        end)
        submitBtn:SetScript("OnClick", function() Panel.SubmitLFGListing() end)
    end

    if lfgOverlay._lvlInput then lfgOverlay._lvlInput:SetText("") end
    lfgOverlay:Show()
end

function Panel.SubmitLFGListing()
    if not lfgState.selectedDungeon then
        print("|cffff5555[M+ LFG]|r Please select a dungeon")
        return
    end

    local dungeon = lfgState.selectedDungeon
    local level = lfgState.selectedLevel ~= "" and lfgState.selectedLevel or "?"
    local roles = {}
    for _, role in ipairs(ROLE_DEFS) do
        if lfgState.selectedRoles[role.key] then
            roles[#roles + 1] = role.label
        end
    end
    local rolesText = #roles > 0 and table.concat(roles, ", ") or "Any"

    local playerName = SafeCall(UnitName, "player") or "Unknown"
    local payload = "LISTING|" .. dungeon .. "|" .. level .. "|" .. rolesText .. "|" .. playerName

    if C_ChatInfo and C_ChatInfo.SendAddonMessage then
        pcall(C_ChatInfo.SendAddonMessage, LFG_PREFIX, payload, "GUILD")
    end

    local chatMsg = "[M+ LFG] " .. dungeon .. " +" .. level .. " — LF: " .. rolesText .. " | Type !join " .. playerName .. " to sign up"
    pcall(SendChatMessage, chatMsg, "GUILD")

    lfgState.hosting = true
    lfgState.applicants = {}

    if lfgOverlay then lfgOverlay:Hide() end

    Panel.ShowHostPanel(dungeon, level, rolesText)
end

local hostPanel = nil

function Panel.ShowHostPanel(dungeon, level, rolesText)
    local R = Panel._refs
    if not R.panel then return end

    if not hostPanel then
        hostPanel = CreateFrame("Frame", nil, R.panel, "BackdropTemplate")
        hostPanel:SetSize(300, 200)
        hostPanel:SetPoint("CENTER", R.panel, "CENTER", 0, 0)
        hostPanel:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        hostPanel:SetBackdropColor(0.04, 0.04, 0.06, 0.97)
        hostPanel:SetBackdropBorderColor(0.00, 0.78, 1.00, 0.3)
        hostPanel:SetFrameLevel(R.panel:GetFrameLevel() + 25)
        hostPanel:Hide()
    end

    local old = { hostPanel:GetChildren() }
    for _, child in ipairs(old) do child:Hide() end
    local oldR = { hostPanel:GetRegions() }
    for _, region in ipairs(oldR) do region:Hide() end

    hostPanel:SetBackdropColor(0.04, 0.04, 0.06, 0.97)
    hostPanel:SetBackdropBorderColor(0.00, 0.78, 1.00, 0.3)

    local title = hostPanel:CreateFontString(nil, "OVERLAY")
    TrySetFont(title, BODY_FONT, 10, "OUTLINE")
    title:SetPoint("TOP", hostPanel, "TOP", 0, -10)
    title:SetText("YOUR LISTING"); title:SetTextColor(0.00, 0.78, 1.00)
    title:Show()

    local infoFS = hostPanel:CreateFontString(nil, "OVERLAY")
    TrySetFont(infoFS, BODY_FONT, 11, "")
    infoFS:SetPoint("TOP", title, "BOTTOM", 0, -6)
    infoFS:SetText(dungeon .. "  |cff00c8ff+" .. level .. "|r  \194\183  " .. rolesText)
    infoFS:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])
    infoFS:Show()

    local waitFS = hostPanel:CreateFontString(nil, "OVERLAY")
    TrySetFont(waitFS, BODY_FONT, 10, "")
    waitFS:SetPoint("CENTER", hostPanel, "CENTER", 0, -10)
    waitFS:SetText("Waiting for applicants...")
    waitFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3])
    waitFS:Show()
    hostPanel._waitFS = waitFS
    hostPanel._applicantYOfs = 60

    local cancelBtn = CreateFrame("Button", nil, hostPanel)
    cancelBtn:SetSize(80, 24); cancelBtn:SetPoint("BOTTOM", hostPanel, "BOTTOM", 0, 10)
    local cancelFS = cancelBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(cancelFS, BODY_FONT, 11, ""); cancelFS:SetPoint("CENTER")
    cancelFS:SetText("Cancel"); cancelFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3])
    cancelBtn:SetScript("OnClick", function()
        lfgState.hosting = false
        hostPanel:Hide()
        if C_ChatInfo and C_ChatInfo.SendAddonMessage then
            pcall(C_ChatInfo.SendAddonMessage, LFG_PREFIX, "CANCEL", "GUILD")
        end
    end)
    cancelBtn:SetScript("OnEnter", function() cancelFS:SetTextColor(1, 0.3, 0.3) end)
    cancelBtn:SetScript("OnLeave", function() cancelFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3]) end)

    hostPanel:Show()
end

local lfgNotification = nil

ShowLFGNotification = function(sender, dungeon, level, roles)
    local playerName = SafeCall(UnitName, "player") or ""
    if sender == playerName then return end

    if not lfgNotification then
        lfgNotification = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
        lfgNotification:SetHeight(45)
        local messengerFrame = _G.MyMessengerFrame
        if messengerFrame then
            lfgNotification:SetPoint("BOTTOMLEFT", messengerFrame, "TOPLEFT", 0, 4)
            lfgNotification:SetPoint("BOTTOMRIGHT", messengerFrame, "TOPRIGHT", 0, 4)
        else
            lfgNotification:SetSize(420, 45)
            lfgNotification:SetPoint("BOTTOM", UIParent, "BOTTOM", 0, 220)
        end
        lfgNotification:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        lfgNotification:SetBackdropColor(0.04, 0.04, 0.07, 0.95)
        lfgNotification:SetBackdropBorderColor(0.00, 0.78, 1.00, 0.3)
        lfgNotification:SetFrameStrata("FULLSCREEN_DIALOG")
        lfgNotification:SetFrameLevel(500)
        lfgNotification:Hide()
    end

    local oldC = { lfgNotification:GetChildren() }
    for _, child in ipairs(oldC) do child:Hide() end
    lfgNotification:SetBackdrop({ bgFile = W8, edgeFile = W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    lfgNotification:SetBackdropColor(0.03, 0.03, 0.06, 0.92)
    lfgNotification:SetBackdropBorderColor(0.00, 0.60, 0.85, 0.3)

    local accent = lfgNotification:CreateTexture(nil, "OVERLAY")
    accent:SetHeight(2); accent:SetPoint("TOPLEFT"); accent:SetPoint("TOPRIGHT")
    accent:SetColorTexture(0.00, 0.78, 1.00, 0.5)
    accent:Show()

    local icon = lfgNotification:CreateTexture(nil, "ARTWORK")
    icon:SetSize(22, 22); icon:SetPoint("LEFT", lfgNotification, "LEFT", 10, 0)
    icon:SetTexture("Interface\\Icons\\INV_Relics_Hourglass")
    icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
    icon:Show()

    local infoFS = lfgNotification:CreateFontString(nil, "OVERLAY")
    TrySetFont(infoFS, BODY_FONT, 11, "")
    infoFS:SetPoint("LEFT", icon, "RIGHT", 8, 0)
    local displayText = "|cff00c8ffM+|r  |cffffff00" .. sender .. "|r  " .. dungeon .. "  |cff00c8ff+" .. level .. "|r  \194\183  " .. roles
    infoFS:SetText(displayText)
    infoFS:SetTextColor(C.bodyText[1], C.bodyText[2], C.bodyText[3])
    infoFS:Show()

    local joinBtn = CreateFrame("Button", nil, lfgNotification)
    joinBtn:SetSize(60, 26); joinBtn:SetPoint("RIGHT", lfgNotification, "RIGHT", -10, 0)
    local joinBg = joinBtn:CreateTexture(nil, "BACKGROUND")
    joinBg:SetAllPoints(); joinBg:SetColorTexture(0.00, 0.50, 0.70, 0.3)
    local joinFS = joinBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(joinFS, BODY_FONT, 11, "OUTLINE"); joinFS:SetPoint("CENTER")
    joinFS:SetText("Join"); joinFS:SetTextColor(0.00, 0.78, 1.00)
    local jT = joinBtn:CreateTexture(nil, "OVERLAY"); jT:SetHeight(1)
    jT:SetPoint("TOPLEFT"); jT:SetPoint("TOPRIGHT"); jT:SetColorTexture(0.00, 0.78, 1.00, 0.3)
    local jB = joinBtn:CreateTexture(nil, "OVERLAY"); jB:SetHeight(1)
    jB:SetPoint("BOTTOMLEFT"); jB:SetPoint("BOTTOMRIGHT"); jB:SetColorTexture(0.00, 0.78, 1.00, 0.3)
    local jL = joinBtn:CreateTexture(nil, "OVERLAY"); jL:SetWidth(1)
    jL:SetPoint("TOPLEFT"); jL:SetPoint("BOTTOMLEFT"); jL:SetColorTexture(0.00, 0.78, 1.00, 0.3)
    local jR = joinBtn:CreateTexture(nil, "OVERLAY"); jR:SetWidth(1)
    jR:SetPoint("TOPRIGHT"); jR:SetPoint("BOTTOMRIGHT"); jR:SetColorTexture(0.00, 0.78, 1.00, 0.3)

    joinBtn:SetScript("OnEnter", function() joinBg:SetColorTexture(0.00, 0.50, 0.70, 0.5) end)
    joinBtn:SetScript("OnLeave", function() joinBg:SetColorTexture(0.00, 0.50, 0.70, 0.3) end)

    local capturedSender = sender
    joinBtn:SetScript("OnClick", function()
        local myName = SafeCall(UnitName, "player") or "Unknown"
        local _, myClass = SafeCall(UnitClass, "player")
        local specIdx = SafeCall(GetSpecialization)
        local mySpec = ""
        if specIdx then
            local _, sName = SafeCall(GetSpecializationInfo, specIdx)
            mySpec = sName or ""
        end
        local myLevel = SafeCall(UnitLevel, "player") or 0

        local joinPayload = "JOIN|" .. capturedSender .. "|" .. myName .. "|" .. (myClass or "") .. "|" .. mySpec .. "|" .. myLevel
        if C_ChatInfo and C_ChatInfo.SendAddonMessage then
            pcall(C_ChatInfo.SendAddonMessage, LFG_PREFIX, joinPayload, "GUILD")
        end

        joinFS:SetText("Sent!")
        joinBg:SetColorTexture(0.15, 0.40, 0.15, 0.3)
        joinBtn:SetScript("OnClick", nil)
    end)

    local dismissBtn = CreateFrame("Button", nil, lfgNotification)
    dismissBtn:SetSize(16, 16); dismissBtn:SetPoint("RIGHT", joinBtn, "LEFT", -6, 0)
    local dismissFS = dismissBtn:CreateFontString(nil, "OVERLAY")
    TrySetFont(dismissFS, BODY_FONT, 11, "OUTLINE"); dismissFS:SetPoint("CENTER")
    dismissFS:SetText("X"); dismissFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.4)
    dismissBtn:SetScript("OnClick", function() lfgNotification:Hide() end)
    dismissBtn:SetScript("OnEnter", function() dismissFS:SetTextColor(1, 0.3, 0.3) end)
    dismissBtn:SetScript("OnLeave", function() dismissFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3], 0.4) end)

    lfgNotification:Show()
    pcall(PlaySound, SOUNDKIT and SOUNDKIT.TELL_MESSAGE or 3081)
end

local lfgMsgEvf = CreateFrame("Frame")
lfgMsgEvf:RegisterEvent("CHAT_MSG_ADDON")
lfgMsgEvf:RegisterEvent("CHAT_MSG_GUILD")
lfgMsgEvf:SetScript("OnEvent", function(_, event, arg1, arg2, arg3, arg4)
    if event == "CHAT_MSG_ADDON" then
        local prefix, message, channel, sender = arg1, arg2, arg3, arg4
        if prefix ~= LFG_PREFIX or channel ~= "GUILD" then return end
        local shortSender = sender:match("^([^%-]+)") or sender

        local cmd = message:match("^(%a+)|")
        if cmd == "LISTING" then
            local _, dungeon, level, roles, hostName = strsplit("|", message)
            if dungeon and level then
                ShowLFGNotification(hostName or shortSender, dungeon, level, roles or "Any")
            end
        elseif cmd == "JOIN" then
            local _, targetHost, applicantName, applicantClass, applicantSpec, applicantLevel = strsplit("|", message)
            local playerName = SafeCall(UnitName, "player") or ""
            if targetHost == playerName and lfgState.hosting then
                lfgState.applicants[#lfgState.applicants + 1] = {
                    name = applicantName or shortSender,
                    class = applicantClass or "",
                    spec = applicantSpec or "",
                    level = tonumber(applicantLevel) or 0,
                    timestamp = date("%H:%M"),
                }
                Panel.RefreshHostApplicants()
                pcall(PlaySound, SOUNDKIT and SOUNDKIT.TELL_MESSAGE or 3081)
            end
        elseif cmd == "CANCEL" then
            if lfgNotification and lfgNotification:IsShown() then
                lfgNotification:Hide()
            end
        end

    elseif event == "CHAT_MSG_GUILD" then
        local msg, sender = arg1, arg2
        if not msg or not sender then return end
        local trimmed = msg:match("^%s*(.-)%s*$") or msg
        local lower = trimmed:lower()

        local joinTarget = lower:match("^!join%s+(%S+)")
        local isJoinCmd = lower:match("^!join") ~= nil

        if isJoinCmd and lfgState.hosting then
            local playerName = SafeCall(UnitName, "player") or ""
            local shortSender = sender:match("^([^%-]+)") or sender

            if joinTarget then
                local targetShort = joinTarget:match("^([^%-]+)") or joinTarget
                if targetShort ~= playerName:lower() then return end
            end

            if shortSender ~= playerName then
                local alreadyApplied = false
                for _, app in ipairs(lfgState.applicants) do
                    if app.name == shortSender then alreadyApplied = true; break end
                end
                if not alreadyApplied then
                    lfgState.applicants[#lfgState.applicants + 1] = {
                        name = shortSender,
                        class = "",
                        spec = "",
                        level = 0,
                        timestamp = date("%H:%M"),
                        fromChat = true,
                    }
                    Panel.RefreshHostApplicants()
                    pcall(PlaySound, SOUNDKIT and SOUNDKIT.TELL_MESSAGE or 3081)
                end
            end
        end
    end
end)

function Panel.RefreshHostApplicants()
    if not hostPanel or not hostPanel:IsShown() then return end
    if hostPanel._waitFS then hostPanel._waitFS:Hide() end

    if hostPanel._appRows then
        for _, row in ipairs(hostPanel._appRows) do row:Hide() end
    end
    hostPanel._appRows = {}

    local yOfs = hostPanel._applicantYOfs or 60
    for _, app in ipairs(lfgState.applicants) do
        local r, g, b = GetClassColor(app.class)
        if app.class == "" then r, g, b = 0.75, 0.75, 0.75 end

        local row = CreateFrame("Frame", nil, hostPanel)
        row:SetHeight(28)
        row:SetPoint("TOPLEFT", hostPanel, "TOPLEFT", 10, -yOfs)
        row:SetPoint("RIGHT", hostPanel, "RIGHT", -10, 0)

        local tsFS = row:CreateFontString(nil, "OVERLAY")
        TrySetFont(tsFS, BODY_FONT, 9, "")
        tsFS:SetPoint("LEFT", row, "LEFT", 0, 0)
        tsFS:SetText(app.timestamp or "")
        tsFS:SetTextColor(0.50, 0.50, 0.55)

        local infoText = app.name
        if app.spec and app.spec ~= "" then
            infoText = infoText .. "  |cffaaaaaa" .. app.spec .. "|r"
        end
        if app.level and app.level > 0 then
            infoText = infoText .. "  |cffaaaaaa" .. app.level .. "|r"
        end
        if app.fromChat then
            infoText = infoText .. "  |cff666666(via chat)|r"
        end

        local nameFS = row:CreateFontString(nil, "OVERLAY")
        TrySetFont(nameFS, BODY_FONT, 11, "")
        nameFS:SetPoint("LEFT", tsFS, "RIGHT", 8, 0)
        nameFS:SetText(infoText)
        nameFS:SetTextColor(r, g, b)

        local invBtn = CreateFrame("Button", nil, row)
        invBtn:SetSize(50, 20); invBtn:SetPoint("RIGHT", row, "RIGHT", 0, 0)
        local invBg = invBtn:CreateTexture(nil, "BACKGROUND")
        invBg:SetAllPoints(); invBg:SetColorTexture(0.15, 0.40, 0.15, 0.3)
        local invFS = invBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(invFS, BODY_FONT, 10, ""); invFS:SetPoint("CENTER")
        invFS:SetText("Invite"); invFS:SetTextColor(0.3, 0.9, 0.3)
        local capturedName = app.name
        invBtn:SetScript("OnClick", function()
            if C_PartyInfo and C_PartyInfo.InviteUnit then
                pcall(C_PartyInfo.InviteUnit, capturedName)
            elseif InviteUnit then
                pcall(InviteUnit, capturedName)
            end
            invFS:SetText("Invited"); invFS:SetTextColor(C.mutedText[1], C.mutedText[2], C.mutedText[3])
            invBg:SetColorTexture(0.15, 0.15, 0.15, 0.2)
            invBtn:SetScript("OnClick", nil)
        end)
        invBtn:SetScript("OnEnter", function() invBg:SetColorTexture(0.20, 0.50, 0.20, 0.4) end)
        invBtn:SetScript("OnLeave", function() invBg:SetColorTexture(0.15, 0.40, 0.15, 0.3) end)

        local decBtn = CreateFrame("Button", nil, row)
        decBtn:SetSize(16, 16); decBtn:SetPoint("RIGHT", invBtn, "LEFT", -4, 0)
        local decFS = decBtn:CreateFontString(nil, "OVERLAY")
        TrySetFont(decFS, BODY_FONT, 10, "OUTLINE"); decFS:SetPoint("CENTER")
        decFS:SetText("X"); decFS:SetTextColor(0.6, 0.2, 0.2, 0.5)
        decBtn:SetScript("OnEnter", function() decFS:SetTextColor(1, 0.3, 0.3) end)
        decBtn:SetScript("OnLeave", function() decFS:SetTextColor(0.6, 0.2, 0.2, 0.5) end)
        local capturedIdx = #lfgState.applicants
        decBtn:SetScript("OnClick", function()
            table.remove(lfgState.applicants, capturedIdx)
            Panel.RefreshHostApplicants()
        end)

        row:Show()
        hostPanel._appRows[#hostPanel._appRows + 1] = row
        yOfs = yOfs + 30
    end

    if #lfgState.applicants == 0 and hostPanel._waitFS then
        hostPanel._waitFS:Show()
    end

    hostPanel:SetHeight(math.max(yOfs + 40, 200))
end

Panel._theme = C

do
    local origToggle = Panel.Toggle
    Panel.Toggle = function(...)
        if UseBlizzardGuildPanel() and type(_G.ToggleGuildFrame) == "function" then
            _G.ToggleGuildFrame()
            return
        end
        return origToggle(...)
    end
end

_G.MidnightUI_GuildPanelAPI = Panel
