-- ============================================================
-- FILE: QuestLog_Panel.lua
-- PURPOSE: Renders the custom QuestLog sidebar panel that
--   slides in from the left edge of the World Map. Displays
--   quests organized into NOW/NEXT/LATER buckets with
--   campaign cards, zone groupings, search, and context menus.
-- HOW IT WORKS:
--   1. Panel.EnsurePanel(map) lazily creates the sidebar Frame
--      parented to the World Map frame ("map"). The panel
--      slides in/out using Alpha + translation animations.
--   2. Widget creation: Panel.CreateQuestRow, CreateBucketHeader,
--      CreateZoneBanner — all return pooled or newly created
--      Frame/Button objects.
--   3. Panel.Rebuild() is the main render entry point. Calls
--      QuestData.CollectQuestListData(searchFilter), then
--      lays out rows in order: campaign card → bucket headers
--      → zone banners → quest rows → objectives (expanded).
--      Uses QuestData pool functions (AcquireFromPool /
--      ReleaseAllInPool) to recycle widgets between rebuilds.
--   4. Quest rows support expand/collapse (click to show
--      objectives), hover highlight, and right-click context
--      menu (track/untrack, share, abandon).
--   5. Search: an EditBox at the panel top filters via
--      CollectQuestListData(searchText) on text change.
--   6. Slide animation: PlaySlideIn/PlaySlideOut with
--      C_Timer.After and alpha tweening. Panel.Toggle(map)
--      is the public API consumed by Map.lua tab buttons.
--   7. Event handling: QUEST_LOG_UPDATE / QUEST_ACCEPTED /
--      QUEST_REMOVED → schedules a debounced Rebuild.
-- DEPENDS ON:
--   Addon.QuestLogConfig (QLC) — layout constants + colors
--   Addon.QuestData (QD) — data pipeline
--   Addon.MapConfig (MC) — theme colors
-- EXPORTS: Addon.QuestLogPanel (Panel table)
-- OWNED SAVEDVARIABLES: none
-- EVENTS REGISTERED:
--   QUEST_LOG_UPDATE, QUEST_ACCEPTED, QUEST_REMOVED
--   PLAYER_ENTERING_WORLD, QUEST_WATCH_LIST_CHANGED
-- FRAMES CREATED:
--   MidnightUIQuestLogPanel (main sidebar panel)
--   Per-quest: quest row buttons, objective rows, bucket
--   headers, zone banners, campaign card frame
-- GOTCHAS:
--   Panel is parented to the World Map frame (map) which may
--   not exist at file load; EnsurePanel is always called lazily.
--   Widget recycling via pools means child frames must be
--   explicitly hidden (not released) before re-layout.
--   Context menu uses MenuUtil.CreateContextMenu (Dragonflight+
--   API) — does not fall back to UIDropDownMenu.
-- ============================================================
local ADDON_NAME, Addon = ...
if type(ADDON_NAME) ~= "string" or ADDON_NAME == "" then
    ADDON_NAME = "MidnightUI"
end
if type(Addon) ~= "table" then
    Addon = {}
end

do
    -- The user's runtime settings live on MidnightUISettingsPerChar (Bootstrap.lua
    -- aliases MidnightUISettings to it on ADDON_LOADED). At file-load that swap
    -- hasn't happened yet, so check per-char first.
    local function IsBlizzardModeEnabled()
        local perChar = _G.MidnightUISettingsPerChar
        if type(perChar) == "table" and type(perChar.General) == "table"
           and perChar.General.useBlizzardQuestingInterface == true then
            return true
        end
        local s = _G.MidnightUISettings
        if type(s) == "table" and type(s.General) == "table"
           and s.General.useBlizzardQuestingInterface == true then
            return true
        end
        return false
    end
    if IsBlizzardModeEnabled() then
        return
    end
end

local QLC = Addon.QuestLogConfig
if type(QLC) ~= "table" then return end

local QD = Addon.QuestData
if type(QD) ~= "table" then return end

local MC = Addon.MapConfig
if type(MC) ~= "table" then return end

local W8 = MC.WHITE8X8 or "Interface\\Buttons\\WHITE8x8"

local Panel = {}
Panel._state = {
    initialized = false,
    panelOpen = false,
    focusedQuestID = nil,
    pendingLayout = false,
    panel = nil,
    ownerMap = nil,
}

local function TC(key)
    return MC.GetThemeColor(key)
end

local function Clamp(v, lo, hi)
    if v < lo then return lo end
    if v > hi then return hi end
    return v
end

local function ApplyGradient(texture, orientation, r1, g1, b1, a1, r2, g2, b2, a2)
    if not texture then return end
    if type(texture.SetGradientAlpha) == "function" then
        texture:SetGradientAlpha(orientation, r1, g1, b1, a1, r2, g2, b2, a2)
    elseif type(texture.SetGradient) == "function" and type(CreateColor) == "function" then
        texture:SetGradient(orientation, CreateColor(r1, g1, b1, a1), CreateColor(r2, g2, b2, a2))
    end
end

local function FormatTimeShort(seconds)
    if type(seconds) ~= "number" or seconds <= 0 then return "" end
    local h = math.floor(seconds / 3600)
    local m = math.floor((seconds % 3600) / 60)
    if h > 0 then
        return string.format("%dh %dm", h, m)
    else
        return string.format("%dm", m)
    end
end

local _searchTimer = nil


-- ============================================================
-- SECTION: Panel Frame Construction + Header + Search Box
-- READS: QLC dimensions, MC theme colors, map frame reference
-- WRITES: Panel._state.panel, panel sub-frames
-- FUNCTIONS:
--   Panel.EnsurePanel(map) → panel frame
--     Lazily creates the sidebar: backdrop, header strip,
--     title text, search EditBox, close button, scroll child.
--     Only runs once; subsequent calls return cached panel.
--   CreateBucketHeader(parent) — pooled bucket label row.
--   CreateZoneBanner(parent) — pooled zone/subzone label row.
--   CreateObjectiveRow(parent) — pooled objective sub-row.
-- ============================================================
function Panel.EnsurePanel(map)
    if Panel._state.panel then
        return Panel._state.panel
    end

    map = map or _G.WorldMapFrame
    if not map then return nil end

    Panel._state.ownerMap = map

    local panelWidth = QLC.PANEL_WIDTH
    local headerHeight = QLC.PANEL_HEADER_HEIGHT
    local searchHeight = QLC.PANEL_SEARCH_HEIGHT

    local panel = CreateFrame("Frame", "MidnightUIQuestLogPanel", map)
    panel:SetFrameStrata(QLC.PANEL_STRATA)
    local okLevel, mapLevel = pcall(function() return map:GetFrameLevel() end)
    local baseLevel = (okLevel and type(mapLevel) == "number") and mapLevel or 10
    panel:SetFrameLevel(baseLevel + 26)
    panel:SetSize(panelWidth, 1)
    panel:SetPoint("TOPLEFT", map, "TOPRIGHT", 3, 0)
    panel:SetPoint("BOTTOMLEFT", map, "BOTTOMRIGHT", 0, 0)
    panel:EnableMouse(true)

    panel:Hide()

    local bg = panel:CreateTexture(nil, "BACKGROUND", nil, -8)
    bg:SetAllPoints()
    bg:SetTexture(W8)
    local bgR, bgG, bgB = TC("bgPanel")
    bg:SetVertexColor(bgR, bgG, bgB, 0.96)
    panel._muiBg = bg

    local acR, acG, acB = TC("accent")

    local leftShadow = panel:CreateTexture(nil, "BACKGROUND", nil, -7)
    leftShadow:SetWidth(20)
    leftShadow:SetPoint("TOPRIGHT", panel, "TOPLEFT", 0, 0)
    leftShadow:SetPoint("BOTTOMRIGHT", panel, "BOTTOMLEFT", 0, 0)
    leftShadow:SetTexture(W8)
    ApplyGradient(leftShadow, "HORIZONTAL", 0, 0, 0, 0, 0, 0, 0, 0.45)
    panel._muiLeftShadow = leftShadow

    local titleBarHeight = QLC.PANEL_HEADER_HEIGHT
    local header = CreateFrame("Frame", nil, panel)
    header:SetHeight(titleBarHeight)
    header:SetPoint("TOPLEFT", panel, "TOPLEFT", 0, 0)
    header:SetPoint("TOPRIGHT", panel, "TOPRIGHT", 0, 0)
    panel._muiHeader = header

    local dragHandle = CreateFrame("Frame", nil, header)
    dragHandle:SetPoint("TOPLEFT", header, "TOPLEFT", 0, 0)
    dragHandle:SetPoint("BOTTOMRIGHT", header, "BOTTOMRIGHT", -36, 0)
    dragHandle:SetFrameLevel(header:GetFrameLevel() + 20)
    dragHandle:EnableMouse(true)
    dragHandle:RegisterForDrag("LeftButton")
    dragHandle:SetScript("OnDragStart", function()
        local ownerMap = Panel._state.ownerMap or _G.WorldMapFrame
        if not ownerMap or type(ownerMap.StartMoving) ~= "function" then return end
        if type(ownerMap.SetMovable) == "function" then
            ownerMap:SetMovable(true)
        end
        if type(ownerMap.SetClampedToScreen) == "function" then
            ownerMap:SetClampedToScreen(true)
        end
        if type(ownerMap.SetUserPlaced) == "function" then
            pcall(ownerMap.SetUserPlaced, ownerMap, true)
        end
        ownerMap._muiUserMoved = true
        pcall(ownerMap.StartMoving, ownerMap)
    end)
    dragHandle:SetScript("OnDragStop", function()
        local ownerMap = Panel._state.ownerMap or _G.WorldMapFrame
        if ownerMap and type(ownerMap.StopMovingOrSizing) == "function" then
            pcall(ownerMap.StopMovingOrSizing, ownerMap)
        end
        if ownerMap and type(ownerMap.GetPoint) == "function" then
            local point, _, relativePoint, xOfs, yOfs = ownerMap:GetPoint(1)
            if point and relativePoint and type(xOfs) == "number" and type(yOfs) == "number" then
                _G.MidnightUISettings = _G.MidnightUISettings or {}
                _G.MidnightUISettings.StandaloneWindows = _G.MidnightUISettings.StandaloneWindows or {}
                local windows = _G.MidnightUISettings.StandaloneWindows
                windows.WorldMap = windows.WorldMap or {}
                local scale = type(ownerMap.GetScale) == "function" and ownerMap:GetScale() or 1
                if type(scale) ~= "number" or scale == 0 then
                    scale = 1
                end
                windows.WorldMap.position = {
                    point,
                    relativePoint,
                    xOfs / scale,
                    yOfs / scale,
                }
            end
        end
    end)
    panel._muiDragHandle = dragHandle

    local tmR, tmG, tmB = TC("textMuted")
    local tsR, tsG, tsB = TC("textSecondary")
    local tpR, tpG, tpB = TC("textPrimary")

    -- Hero line — the campaign name when there is one, otherwise "Quest Log".
    local title = header:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
    title:SetPoint("TOPLEFT", header, "TOPLEFT", QLC.PANEL_PAD_X, -16)
    title:SetPoint("RIGHT", header, "RIGHT", -54, 0)
    title:SetJustifyH("LEFT")
    title:SetWordWrap(false)
    title:SetTextColor(tpR, tpG, tpB, 1)
    title:SetText("Quest Log")
    panel._muiTitle = title

    -- Sub-line — chapter info beneath the hero, or empty if no campaign.
    local subtitle = header:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    subtitle:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 1, -4)
    subtitle:SetPoint("RIGHT", header, "RIGHT", -QLC.PANEL_PAD_X, 0)
    subtitle:SetJustifyH("LEFT")
    subtitle:SetWordWrap(false)
    subtitle:SetTextColor(tmR, tmG, tmB, 0.65)
    subtitle:SetText("")
    panel._muiSubtitle = subtitle

    -- Stub kept so legacy lookups (panel._muiCount, panel._muiChapterMark) don't error
    -- when something else in the codebase still pokes at them. The visible header has
    -- been intentionally simplified down to title + subtitle + actions only.
    local count = header:CreateFontString(nil, "OVERLAY", "GameFontHighlightExtraSmall")
    count:Hide()
    panel._muiCount = count

    local chapterMarkStub = CreateFrame("Frame", nil, header)
    chapterMarkStub:SetSize(1, 1)
    chapterMarkStub:Hide()
    chapterMarkStub._muiCurrent = chapterMarkStub:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    chapterMarkStub._muiCurrent:Hide()
    chapterMarkStub._muiTotal = chapterMarkStub:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    chapterMarkStub._muiTotal:Hide()
    panel._muiChapterMark = chapterMarkStub

    -- Search + close buttons must sit above the drag handle's frame level,
    -- otherwise the drag handle (which covers most of the header so the user
    -- can drag the World Map by its title) intercepts mouse clicks on them.
    local headerActionLevel = header:GetFrameLevel() + 25

    local searchBtn = CreateFrame("Button", nil, header)
    searchBtn:SetSize(16, 16)
    searchBtn:SetPoint("TOPRIGHT", header, "TOPRIGHT", -QLC.PANEL_PAD_X - 22, -16)
    searchBtn:SetFrameLevel(headerActionLevel)
    searchBtn:EnableMouse(true)
    local searchIcon = searchBtn:CreateTexture(nil, "OVERLAY")
    searchIcon:SetAllPoints(searchBtn)
    local okAtlas = pcall(function() searchIcon:SetAtlas("common-search-magnifyingglass") end)
    if not okAtlas then
        searchIcon:SetTexture("Interface\\Common\\UI-Searchbox-Icon")
    end
    searchIcon:SetVertexColor(tsR, tsG, tsB, 0.55)
    searchBtn._muiIcon = searchIcon
    panel._muiSearchBtn = searchBtn

    local closeBtn = CreateFrame("Button", nil, header)
    closeBtn:SetSize(18, 18)
    closeBtn:SetPoint("TOPRIGHT", header, "TOPRIGHT", -QLC.PANEL_PAD_X, -16)
    closeBtn:SetFrameLevel(headerActionLevel)
    closeBtn:EnableMouse(true)
    local closeText = closeBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    closeText:SetPoint("CENTER", closeBtn, "CENTER", 0, 0)
    closeText:SetTextColor(tsR, tsG, tsB, 0.55)
    closeText:SetText("\195\151")
    closeBtn._muiText = closeText
    closeBtn:SetScript("OnEnter", function(self)
        local ar, ag, ab = TC("accent")
        self._muiText:SetTextColor(ar, ag, ab, 1)
    end)
    closeBtn:SetScript("OnLeave", function(self)
        local sr, sg, sb = TC("textSecondary")
        self._muiText:SetTextColor(sr, sg, sb, 0.55)
    end)
    closeBtn:SetScript("OnClick", function() Panel.Hide() end)
    panel._muiCloseBtn = closeBtn

    local progressTrack = header:CreateTexture(nil, "ARTWORK", nil, 1)
    progressTrack:SetHeight(1)
    progressTrack:SetPoint("BOTTOMLEFT", header, "BOTTOMLEFT", QLC.PANEL_PAD_X, 0)
    progressTrack:SetPoint("BOTTOMRIGHT", header, "BOTTOMRIGHT", -QLC.PANEL_PAD_X, 0)
    progressTrack:SetTexture(W8)
    local bdR, bdG, bdB = TC("border")
    progressTrack:SetVertexColor(bdR, bdG, bdB, 0.20)
    panel._muiCampTrack = progressTrack

    local progressFill = header:CreateTexture(nil, "ARTWORK", nil, 2)
    progressFill:SetHeight(1)
    progressFill:SetPoint("BOTTOMLEFT", header, "BOTTOMLEFT", QLC.PANEL_PAD_X, 0)
    progressFill:SetTexture(W8)
    progressFill:SetVertexColor(acR, acG, acB, 0.70)
    progressFill:SetWidth(1)
    progressFill:Hide()
    panel._muiCampFill = progressFill

    local bdR, bdG, bdB = TC("border")

    -- Campaign card host kept as a no-op stub so legacy lookups (panel._muiCampaignHost,
    -- _muiCampTitle, _muiCampProgress, _muiCampPill, _muiCampChapter) don't error.
    -- The visible campaign treatment now lives in the panel header (subtitle + thin progress line).
    local campaignHost = CreateFrame("Frame", nil, panel)
    campaignHost:SetHeight(0)
    campaignHost:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, 0)
    campaignHost:SetPoint("TOPRIGHT", header, "BOTTOMRIGHT", 0, 0)
    campaignHost:Hide()
    panel._muiCampaignHost = campaignHost

    panel._muiCampTitle = panel._muiSubtitle
    panel._muiCampChapter = panel._muiSubtitle

    local campStubProgress = header:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    campStubProgress:Hide()
    panel._muiCampProgress = campStubProgress

    local campStubPill = CreateFrame("Frame", nil, header)
    campStubPill:SetSize(1, 1)
    campStubPill:Hide()
    panel._muiCampPill = campStubPill

    -- Search EditBox: hidden by default, slides in beneath header when search icon clicked.
    -- Styled as a self-contained pill (inset margins, rounded-feel border, inline
    -- magnifying glass on the left, clear button on the right) rather than a flat
    -- slab edge-to-edge with the panel.
    local SEARCH_INSET = 14            -- horizontal margin from panel edges
    local SEARCH_LEFT_ICON_PAD = 30    -- room for the inline magnifying glass
    local SEARCH_RIGHT_ICON_PAD = 26   -- room for the clear (×) button

    local searchBox = CreateFrame("EditBox", nil, panel)
    searchBox:SetHeight(26)
    searchBox:SetAutoFocus(false)
    searchBox:SetFontObject(GameFontHighlightSmall)
    searchBox:SetTextColor(tpR, tpG, tpB, 1)
    searchBox:SetMaxLetters(64)
    searchBox:SetTextInsets(SEARCH_LEFT_ICON_PAD, SEARCH_RIGHT_ICON_PAD, 4, 4)
    searchBox:Hide()
    panel._muiSearchBox = searchBox

    -- Filled background, slightly darker than the main panel so the box reads as a control.
    -- Inset on all four sides so there's breathing room between the pill and panel edges.
    local searchBg = searchBox:CreateTexture(nil, "BACKGROUND", nil, -2)
    searchBg:SetPoint("TOPLEFT", searchBox, "TOPLEFT", SEARCH_INSET, -3)
    searchBg:SetPoint("BOTTOMRIGHT", searchBox, "BOTTOMRIGHT", -SEARCH_INSET, 3)
    searchBg:SetTexture(W8)
    local bgPaR, bgPaG, bgPaB = TC("bgPanelAlt")
    searchBg:SetVertexColor(bgPaR * 0.55, bgPaG * 0.55, bgPaB * 0.55, 0.85)
    searchBox._muiBg = searchBg

    -- Thin 4-side border. Ride on top of the bg.
    local function MakeEdge()
        local t = searchBox:CreateTexture(nil, "BORDER")
        t:SetTexture(W8)
        return t
    end
    local edgeTop, edgeBottom, edgeLeft, edgeRight = MakeEdge(), MakeEdge(), MakeEdge(), MakeEdge()
    edgeTop:SetHeight(1)
    edgeTop:SetPoint("TOPLEFT", searchBg, "TOPLEFT", 0, 0)
    edgeTop:SetPoint("TOPRIGHT", searchBg, "TOPRIGHT", 0, 0)
    edgeBottom:SetHeight(1)
    edgeBottom:SetPoint("BOTTOMLEFT", searchBg, "BOTTOMLEFT", 0, 0)
    edgeBottom:SetPoint("BOTTOMRIGHT", searchBg, "BOTTOMRIGHT", 0, 0)
    edgeLeft:SetWidth(1)
    edgeLeft:SetPoint("TOPLEFT", searchBg, "TOPLEFT", 0, 0)
    edgeLeft:SetPoint("BOTTOMLEFT", searchBg, "BOTTOMLEFT", 0, 0)
    edgeRight:SetWidth(1)
    edgeRight:SetPoint("TOPRIGHT", searchBg, "TOPRIGHT", 0, 0)
    edgeRight:SetPoint("BOTTOMRIGHT", searchBg, "BOTTOMRIGHT", 0, 0)
    local function SetEdgeColor(r, g, b, a)
        edgeTop:SetVertexColor(r, g, b, a)
        edgeBottom:SetVertexColor(r, g, b, a)
        edgeLeft:SetVertexColor(r, g, b, a)
        edgeRight:SetVertexColor(r, g, b, a)
    end
    SetEdgeColor(bdR, bdG, bdB, 0.55)
    searchBox._muiSetEdgeColor = SetEdgeColor

    -- Inline magnifying-glass icon on the left side of the box.
    local inlineIcon = searchBox:CreateTexture(nil, "ARTWORK")
    inlineIcon:SetSize(12, 12)
    inlineIcon:SetPoint("LEFT", searchBg, "LEFT", 8, 0)
    local okInlineAtlas = pcall(function() inlineIcon:SetAtlas("common-search-magnifyingglass") end)
    if not okInlineAtlas then
        inlineIcon:SetTexture("Interface\\Common\\UI-Searchbox-Icon")
    end
    inlineIcon:SetVertexColor(tmR, tmG, tmB, 0.65)
    searchBox._muiInlineIcon = inlineIcon

    -- Clear (×) button — only shown when there is text.
    local clearBtn = CreateFrame("Button", nil, searchBox)
    clearBtn:SetSize(14, 14)
    clearBtn:SetPoint("RIGHT", searchBg, "RIGHT", -8, 0)
    clearBtn:SetFrameLevel(searchBox:GetFrameLevel() + 5)
    local clearText = clearBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    clearText:SetPoint("CENTER", clearBtn, "CENTER", 0, 0)
    clearText:SetText("\195\151")
    clearText:SetTextColor(tmR, tmG, tmB, 0.65)
    clearBtn._muiText = clearText
    clearBtn:Hide()
    clearBtn:SetScript("OnEnter", function(self)
        local ar, ag, ab = TC("accent")
        self._muiText:SetTextColor(ar, ag, ab, 1)
    end)
    clearBtn:SetScript("OnLeave", function(self)
        self._muiText:SetTextColor(tmR, tmG, tmB, 0.65)
    end)
    clearBtn:SetScript("OnClick", function()
        searchBox:SetText("")
        searchBox._muiPlaceholder:Show()
        clearBtn:Hide()
        Panel.Rebuild()
    end)
    searchBox._muiClearBtn = clearBtn

    -- Placeholder text starts after the inline icon.
    local placeholder = searchBox:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    placeholder:SetPoint("LEFT", searchBg, "LEFT", SEARCH_LEFT_ICON_PAD - SEARCH_INSET, 0)
    placeholder:SetTextColor(tmR, tmG, tmB, 0.50)
    placeholder:SetText("Search quests")
    searchBox._muiPlaceholder = placeholder

    searchBox:SetScript("OnEditFocusGained", function(self)
        self._muiPlaceholder:Hide()
        self._muiSetEdgeColor(acR, acG, acB, 0.85)
        self._muiInlineIcon:SetVertexColor(acR, acG, acB, 0.95)
    end)
    searchBox:SetScript("OnEditFocusLost", function(self)
        if self:GetText() == "" then self._muiPlaceholder:Show() end
        self._muiSetEdgeColor(bdR, bdG, bdB, 0.55)
        self._muiInlineIcon:SetVertexColor(tmR, tmG, tmB, 0.65)
    end)
    searchBox:SetScript("OnTextChanged", function(self, userInput)
        local hasText = self:GetText() ~= ""
        if userInput then
            if hasText then self._muiPlaceholder:Hide() else self._muiPlaceholder:Show() end
            if _searchTimer then pcall(function() _searchTimer:Cancel() end) end
            local ok, timer = pcall(function()
                return C_Timer.NewTimer(0.25, function() Panel.Rebuild() end)
            end)
            if ok and timer then _searchTimer = timer else Panel.Rebuild() end
        end
        if hasText then self._muiClearBtn:Show() else self._muiClearBtn:Hide() end
    end)
    searchBox:SetScript("OnEscapePressed", function(self)
        self:ClearFocus(); self:SetText(""); self._muiPlaceholder:Show()
        self._muiClearBtn:Hide()
        self:Hide(); Panel.Rebuild()
    end)
    searchBox:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)

    searchBtn:SetScript("OnEnter", function(self)
        local ar, ag, ab = TC("accent")
        self._muiIcon:SetVertexColor(ar, ag, ab, 1)
    end)
    searchBtn:SetScript("OnLeave", function(self)
        local sr, sg, sb = TC("textSecondary")
        self._muiIcon:SetVertexColor(sr, sg, sb, 0.55)
    end)
    searchBtn:SetScript("OnClick", function()
        if searchBox:IsShown() then
            searchBox:SetText("")
            searchBox._muiPlaceholder:Show()
            searchBox:ClearFocus()
            searchBox:Hide()
            Panel.Rebuild()
        else
            searchBox:Show()
            searchBox:SetFocus()
            Panel.Rebuild()
        end
    end)

    local scrollFrame = CreateFrame("ScrollFrame", nil, panel, "UIPanelScrollFrameTemplate")
    panel._muiScrollFrame = scrollFrame

    local scrollBar = scrollFrame.ScrollBar or _G[scrollFrame:GetName() and (scrollFrame:GetName() .. "ScrollBar") or ""]
    if scrollBar then
        local okHideArt = pcall(function()
            if scrollBar.ScrollUpButton then scrollBar.ScrollUpButton:SetAlpha(0) end
            if scrollBar.ScrollDownButton then scrollBar.ScrollDownButton:SetAlpha(0) end
            local thumbTex = scrollBar.ThumbTexture or scrollBar:GetThumbTexture()
            if thumbTex then
                thumbTex:SetTexture(W8)
                thumbTex:SetVertexColor(acR, acG, acB, 0.30)
                thumbTex:SetWidth(4)
            end
            scrollBar:SetWidth(4)
        end)
    end

    local scrollChild = CreateFrame("Frame", nil, scrollFrame)
    scrollChild:SetWidth(panelWidth - 16)
    scrollChild:SetHeight(1)
    scrollFrame:SetScrollChild(scrollChild)
    panel._muiScrollChild = scrollChild

    local slideInGroup = panel:CreateAnimationGroup()
    local slideInTrans = slideInGroup:CreateAnimation("Translation")
    slideInTrans:SetOffset(-panelWidth, 0)
    slideInTrans:SetDuration(0)
    slideInTrans:SetOrder(1)
    local slideInTrans2 = slideInGroup:CreateAnimation("Translation")
    slideInTrans2:SetOffset(panelWidth, 0)
    slideInTrans2:SetDuration(QLC.PANEL_SLIDE_DURATION)
    slideInTrans2:SetSmoothing("OUT")
    slideInTrans2:SetOrder(2)
    local slideInAlpha = slideInGroup:CreateAnimation("Alpha")
    slideInAlpha:SetFromAlpha(0)
    slideInAlpha:SetToAlpha(1)
    slideInAlpha:SetDuration(QLC.PANEL_SLIDE_DURATION)
    slideInAlpha:SetOrder(2)
    slideInGroup:SetScript("OnFinished", function()
        panel:SetAlpha(1)
    end)
    panel._muiSlideIn = slideInGroup

    local slideOutGroup = panel:CreateAnimationGroup()
    local slideOutTrans = slideOutGroup:CreateAnimation("Translation")
    slideOutTrans:SetOffset(-panelWidth, 0)
    slideOutTrans:SetDuration(QLC.PANEL_SLIDE_DURATION)
    slideOutTrans:SetSmoothing("IN")
    slideOutTrans:SetOrder(1)
    local slideOutAlpha = slideOutGroup:CreateAnimation("Alpha")
    slideOutAlpha:SetFromAlpha(1)
    slideOutAlpha:SetToAlpha(0)
    slideOutAlpha:SetDuration(QLC.PANEL_SLIDE_DURATION)
    slideOutAlpha:SetOrder(1)
    slideOutGroup:SetScript("OnFinished", function()
        panel:Hide()
        panel:SetAlpha(1)
        Panel._state.panelOpen = false
    end)
    panel._muiSlideOut = slideOutGroup

    Panel._state.panel = panel
    return panel
end



function Panel.CreateBucketHeader(parent)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetHeight(QLC.BUCKET_HEADER_HEIGHT)

    local hairline = btn:CreateTexture(nil, "ARTWORK", nil, 0)
    hairline:SetHeight(1)
    hairline:SetPoint("TOPLEFT", btn, "TOPLEFT", QLC.PANEL_PAD_X, -2)
    hairline:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -QLC.PANEL_PAD_X, -2)
    hairline:SetTexture(W8)
    local bdR, bdG, bdB = TC("border")
    hairline:SetVertexColor(bdR, bdG, bdB, 0.25)
    btn._muiHairline = hairline

    local accentBar = btn:CreateTexture(nil, "ARTWORK", nil, 1)
    accentBar:SetSize(2, 12)
    accentBar:SetPoint("LEFT", btn, "LEFT", QLC.PANEL_PAD_X, -2)
    accentBar:SetTexture(W8)
    btn._muiAccentBar = accentBar

    local label = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    label:SetPoint("LEFT", accentBar, "RIGHT", 8, 0)
    label:SetJustifyH("LEFT")
    btn._muiLabel = label

    local count = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    count:SetPoint("RIGHT", btn, "RIGHT", -QLC.PANEL_PAD_X, -2)
    count:SetJustifyH("RIGHT")
    btn._muiCount = count

    btn._muiBucketKey = nil

    btn:RegisterForClicks("LeftButtonUp")
    btn:SetScript("OnEnter", function(self)
        local bR, bG, bB = QLC.GetBucketColor(self._muiBucketKey or "later")
        self._muiLabel:SetTextColor(bR, bG, bB, 1.0)
        self._muiAccentBar:SetVertexColor(bR, bG, bB, 1.0)
    end)
    btn:SetScript("OnLeave", function(self)
        local bR, bG, bB = QLC.GetBucketColor(self._muiBucketKey or "later")
        self._muiLabel:SetTextColor(bR, bG, bB, 0.95)
        self._muiAccentBar:SetVertexColor(bR, bG, bB, 0.85)
    end)
    btn:SetScript("OnClick", function(self)
        local key = self._muiBucketKey
        if key then
            QD._bucketCollapseState[key] = not QD._bucketCollapseState[key]
            Panel.Rebuild()
        end
    end)

    function btn:SetBucketData(bucketKey, questCount, isCollapsed)
        self._muiBucketKey = bucketKey

        local labelText = QLC.BUCKET_LABELS[bucketKey] or bucketKey:upper()
        if isCollapsed then labelText = labelText .. "  +" end
        self._muiLabel:SetText(labelText)

        local bR, bG, bB = QLC.GetBucketColor(bucketKey)
        self._muiLabel:SetTextColor(bR, bG, bB, 0.95)
        self._muiAccentBar:SetVertexColor(bR, bG, bB, 0.85)

        self._muiCount:SetText(tostring(questCount))
        local tmR2, tmG2, tmB2 = TC("textMuted")
        self._muiCount:SetTextColor(tmR2, tmG2, tmB2, 0.55)

        self:Show()
    end

    return btn
end



function Panel.CreateZoneHeader(parent)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetHeight(QLC.ZONE_HEADER_HEIGHT)

    local zoneName = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    zoneName:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", QLC.PANEL_PAD_X, 2)
    btn._muiZoneName = zoneName

    btn._muiZoneNameKey = nil

    btn:RegisterForClicks("LeftButtonUp")
    btn:SetScript("OnClick", function(self)
        local key = self._muiZoneNameKey
        if key then
            QD._collapseState[key] = not QD._collapseState[key]
            Panel.Rebuild()
        end
    end)

    function btn:SetZoneHeaderData(zoneNameStr, questCount, isCurrentZone, isCollapsed)
        self._muiZoneNameKey = zoneNameStr

        local displayName = zoneNameStr or ""
        local suffix = isCollapsed and "   +" or ""
        local fullText = displayName .. "   \194\183   " .. tostring(questCount) .. suffix

        if isCurrentZone then
            local tpR, tpG, tpB = TC("textPrimary")
            self._muiZoneName:SetTextColor(tpR, tpG, tpB, 0.80)
        else
            local tmR2, tmG2, tmB2 = TC("textMuted")
            self._muiZoneName:SetTextColor(tmR2, tmG2, tmB2, 0.55)
        end

        self._muiZoneName:SetText(fullText)
        self:Show()
    end

    return btn
end



-- ============================================================
-- SECTION: Quest Row Widget Creation
-- READS: QLC row height/padding constants, MC theme colors
-- WRITES: pooled quest row Button frames
-- FUNCTIONS:
--   Panel.CreateQuestRow(parent) → btn frame
--     Creates a quest row button with: accent strip (left),
--     title FontString, progress bar (2 px), difficulty dot,
--     chevron expand indicator. Wires OnClick (expand/track)
--     and OnEnter/OnLeave (hover highlight).
--   Panel.UpdateQuestRow(btn, questEntry) — applies title
--     text, bucket accent color, progress fill, difficulty
--     color, expanded/collapsed state, and right-click
--     binding for context menu.
-- ============================================================
function Panel.CreateQuestRow(parent)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetHeight(QLC.QUEST_ROW_HEIGHT)
    btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")

    local bg = btn:CreateTexture(nil, "BACKGROUND", nil, -4)
    bg:SetPoint("TOPLEFT", btn, "TOPLEFT", 6, 1)
    bg:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -6, -1)
    bg:SetTexture(W8)
    bg:SetVertexColor(0, 0, 0, 0)
    btn._muiBg = bg

    local dotSize = QLC.STATE_DOT_SIZE
    local stateDot = btn:CreateTexture(nil, "ARTWORK", nil, 1)
    stateDot:SetSize(dotSize, dotSize)
    stateDot:SetPoint("LEFT", btn, "LEFT", QLC.PANEL_PAD_X, 0)
    stateDot:SetTexture(W8)
    btn._muiStateDot = stateDot

    local title = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    title:SetPoint("LEFT", stateDot, "RIGHT", 12, 0)
    title:SetPoint("RIGHT", btn, "RIGHT", -54, 0)
    title:SetJustifyH("LEFT")
    title:SetWordWrap(false)
    btn._muiTitle = title

    local badge = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    badge:SetPoint("RIGHT", btn, "RIGHT", -QLC.PANEL_PAD_X, 0)
    badge:SetJustifyH("RIGHT")
    badge:Hide()
    btn._muiBadge = badge

    -- Legacy stub fields for any caller still poking at the old structure
    btn._muiCompleteAccent = stateDot
    btn._muiCompleteIcon = badge
    btn._muiProgressText = badge
    btn._muiObjSummary = badge
    btn._muiStatusTag = { Hide = function() end, Show = function() end }
    btn._muiSep = bg

    btn._muiAccentColor = { r = 1, g = 1, b = 1 }
    btn._muiQuestID = nil
    btn._muiQuestTitle = nil
    btn._muiBucketKey = nil

    btn:SetScript("OnEnter", function(self)
        local bpaR, bpaG, bpaB = TC("bgPanelAlt")
        self._muiBg:SetVertexColor(bpaR, bpaG, bpaB, 0.35)
    end)
    btn:SetScript("OnLeave", function(self)
        self._muiBg:SetVertexColor(0, 0, 0, 0)
    end)

    btn:SetScript("OnClick", function(self, button)
        local questID = self._muiQuestID
        if not questID then return end
        if button == "RightButton" then
            Panel.OpenContextMenu(self, questID, self._muiQuestTitle or "")
        else
            if QD._expandedQuests[questID] then
                QD._expandedQuests[questID] = nil
            else
                QD._expandedQuests[questID] = true
            end
            Panel.Rebuild()
        end
    end)

    function btn:SetQuestData(questData, bucketKey)
        if type(questData) ~= "table" then
            self:Hide()
            return
        end

        self._muiQuestID = questData.questID
        self._muiQuestTitle = questData.title
        self._muiBucketKey = bucketKey

        local successC = QLC.COLORS.success
        local warnC = QLC.COLORS.warning
        local tpR, tpG, tpB = TC("textPrimary")
        local tsR, tsG, tsB = TC("textSecondary")
        local tmR, tmG, tmB = TC("textMuted")

        local isComplete = questData.isComplete
        local isExpiring = (bucketKey == "expiring") or questData._isExpiringSoon
        local isFocused = (Panel._state.focusedQuestID == questData.questID)
        self._muiIsComplete = isComplete
        self._muiIsExpiring = isExpiring

        -- Quest type detection: dungeon, raid, story
        local tagInfo = questData.questTagInfo
        local tagID = (type(tagInfo) == "table" and tagInfo.tagID) or nil
        local tagName = (type(tagInfo) == "table" and tagInfo.tagName) or ""
        local tagNameLower = type(tagName) == "string" and tagName:lower() or ""

        local isRaid = (tagID == 62 or tagID == 85 or tagID == 88)
            or tagNameLower:find("raid", 1, true) ~= nil
        local isDungeon = (not isRaid) and (
            (tagID == 81 or tagID == 83)
            or tagNameLower:find("dungeon", 1, true) ~= nil
            or tagNameLower:find("heroic", 1, true) ~= nil
        )
        local isStory = (questData._isCampaignQuest == true) and not isRaid and not isDungeon

        -- Type tint mixed into the title color: subtle nudge, not a full recolor
        local tintR, tintG, tintB
        if isRaid then
            tintR, tintG, tintB = 1.00, 0.55, 0.40         -- warm red/orange
        elseif isDungeon then
            tintR, tintG, tintB = 0.74, 0.62, 1.00         -- soft purple
        elseif isStory then
            tintR, tintG, tintB = 1.00, 0.86, 0.50         -- soft gold
        end

        local titleText = questData.title or ""
        self._muiTitle:SetText(titleText)
        self._muiAccentColor = { r = 1, g = 1, b = 1 }

        -- State dot: turn-in (green) > expiring (red) > focused (accent)
        --            > raid/dungeon/story tint > active (dim) > trivial (dimmer)
        if isComplete then
            self._muiStateDot:SetVertexColor(successC.r, successC.g, successC.b, 0.95)
            self._muiTitle:SetTextColor(tpR, tpG, tpB, 1)
        elseif isExpiring then
            self._muiStateDot:SetVertexColor(warnC.r, warnC.g, warnC.b, 0.95)
            self._muiTitle:SetTextColor(tpR, tpG, tpB, 0.95)
        elseif isFocused then
            local ar, ag, ab = TC("accent")
            self._muiStateDot:SetVertexColor(ar, ag, ab, 0.95)
            self._muiTitle:SetTextColor(tpR, tpG, tpB, 1)
        elseif tintR then
            self._muiStateDot:SetVertexColor(tintR, tintG, tintB, 0.95)
            self._muiTitle:SetTextColor(tintR, tintG, tintB, 0.95)
        else
            local diff = questData._difficultyColor
            local isTrivial = diff and diff == QLC.DIFFICULTY_COLORS.trivial
            if isTrivial then
                self._muiStateDot:SetVertexColor(tmR, tmG, tmB, 0.40)
                self._muiTitle:SetTextColor(tmR, tmG, tmB, 0.65)
            else
                self._muiStateDot:SetVertexColor(tsR, tsG, tsB, 0.45)
                self._muiTitle:SetTextColor(tpR, tpG, tpB, 0.92)
            end
        end

        -- Single priority badge cascade:
        --   TURN IN > expiring time > RAID > DUNGEON > STORY > nothing
        local badgeText, badgeR, badgeG, badgeB = "", 1, 1, 1
        if isComplete then
            badgeText = "TURN IN"
            badgeR, badgeG, badgeB = successC.r, successC.g, successC.b
        elseif questData._isExpiringSoon then
            badgeText = FormatTimeShort(questData._expiresInSeconds or 0)
            badgeR, badgeG, badgeB = warnC.r, warnC.g, warnC.b
        elseif isRaid then
            badgeText = "RAID"
            badgeR, badgeG, badgeB = 1.00, 0.55, 0.40
        elseif isDungeon then
            badgeText = "DUNGEON"
            badgeR, badgeG, badgeB = 0.74, 0.62, 1.00
        elseif isStory then
            badgeText = "STORY"
            badgeR, badgeG, badgeB = 1.00, 0.86, 0.50
        end

        if badgeText ~= "" then
            self._muiBadge:SetText(badgeText)
            self._muiBadge:SetTextColor(badgeR, badgeG, badgeB, 0.90)
            self._muiBadge:Show()
        else
            self._muiBadge:Hide()
        end

        self:Show()
    end

    return btn
end



function Panel.CreateExpansion(parent)
    local frame = CreateFrame("Frame", nil, parent)
    frame:SetHeight(1)

    frame._muiChildFrames = {}

    local function ReleaseChildren(self)
        for i = 1, #self._muiChildFrames do
            local child = self._muiChildFrames[i]
            if child and type(child.Hide) == "function" then
                child:Hide()
                child:ClearAllPoints()
            end
        end
        self._muiChildFrames = {}
    end

    function frame:SetExpansionData(questID, questTitle, accentColor, focusedQuestID)
        ReleaseChildren(self)
        self._muiAccentColorRef = accentColor

        local pad = QLC.PANEL_PAD_X
        local indent = pad + 18
        local tpR, tpG, tpB = TC("textPrimary")
        local tsR, tsG, tsB = TC("textSecondary")
        local tmR, tmG, tmB = TC("textMuted")
        local successC = QLC.COLORS.success
        local acR, acG, acB = TC("accent")

        if type(C_QuestLog) == "table" and type(C_QuestLog.SetSelectedQuest) == "function" then
            pcall(C_QuestLog.SetSelectedQuest, questID)
        end
        local questLogIndex = Panel.ResolveQuestLogIndex(questID)
        if questLogIndex and type(SelectQuestLogEntry) == "function" then
            pcall(SelectQuestLogEntry, questLogIndex)
        end

        local questData = { questID = questID, objectives = {} }
        if type(C_QuestLog) == "table" and type(C_QuestLog.GetQuestObjectives) == "function" then
            local okObj, objs = pcall(C_QuestLog.GetQuestObjectives, questID)
            if okObj and type(objs) == "table" then questData.objectives = objs end
        end
        local objRows = QD.BuildQuestObjectiveDisplayRows(questData)

        local itemRewards = {}
        local moneyAmount, xpAmount = 0, 0
        if type(questID) == "number" then
            local numRewards = 0
            if type(GetNumQuestLogRewards) == "function" then
                local okN, n = pcall(GetNumQuestLogRewards, questID)
                if okN and type(n) == "number" then numRewards = n end
            end
            -- Augment quest-API item info with a direct GetItemInfo lookup. Quest
            -- reward APIs frequently return nil / Common-quality for items the local
            -- client has not cached yet (weekly resets, fresh quests, never-seen items).
            -- GetItemInfo uses the standard item cache; if the item is cached, we get
            -- the true name/quality/texture. If it isn't cached, the call queues an
            -- async load and a GET_ITEM_INFO_RECEIVED event will fire later — the
            -- panel listens for that and rebuilds.
            local function AugmentRewardFromItemID(reward)
                if type(reward.itemID) ~= "number" or reward.itemID <= 0 then return end
                if type(GetItemInfo) ~= "function" then return end
                local okGI, liveName, _, liveQuality, _, _, _, _, _, _, liveTexture =
                    pcall(GetItemInfo, reward.itemID)
                if not okGI then return end
                if (not reward.name or reward.name == "") and type(liveName) == "string" then
                    reward.name = liveName
                end
                if (not reward.quality or reward.quality < 2) and type(liveQuality) == "number" then
                    reward.quality = liveQuality
                end
                if (not reward.texture or reward.texture == "") and liveTexture then
                    reward.texture = liveTexture
                end
            end

            for i = 1, numRewards do
                if type(GetQuestLogRewardInfo) == "function" then
                    local okR, name, texture, count, quality, _, itemID = pcall(GetQuestLogRewardInfo, i, questID)
                    if okR and type(name) == "string" and name ~= "" then
                        local entry = { name = name, texture = texture, count = count, quality = quality, itemID = itemID }
                        AugmentRewardFromItemID(entry)
                        itemRewards[#itemRewards + 1] = entry
                    end
                end
            end
            local numChoices = 0
            if type(GetNumQuestLogChoices) == "function" then
                local okC, c = pcall(GetNumQuestLogChoices, questID)
                if okC and type(c) == "number" then numChoices = c end
            end
            local choiceStartIndex = #itemRewards + 1
            for i = 1, numChoices do
                if type(GetQuestLogChoiceInfo) == "function" then
                    local okC, name, texture, count, quality, _, itemID = pcall(GetQuestLogChoiceInfo, i, questID)
                    if okC and type(name) == "string" and name ~= "" then
                        local entry = { name = name, texture = texture, count = count, quality = quality, itemID = itemID, isChoice = true }
                        AugmentRewardFromItemID(entry)
                        itemRewards[#itemRewards + 1] = entry
                    end
                end
            end
            local hasChoices = (numChoices > 1)
            if type(GetQuestLogRewardMoney) == "function" then
                local okM, money = pcall(GetQuestLogRewardMoney, questID)
                if okM and type(money) == "number" then moneyAmount = money end
            end
            if type(GetQuestLogRewardXP) == "function" then
                local okXP, xp = pcall(GetQuestLogRewardXP, questID)
                if okXP and type(xp) == "number" then xpAmount = xp end
            end
        end

        -- Reputation + currency rewards via the modern retail API path.
        -- Quest reputation in modern retail is delivered through the *currency* system
        -- (not the legacy faction reward API). C_QuestLog.GetQuestRewardCurrencies
        -- returns a list of every currency reward; for each, we then resolve its
        -- detail via C_QuestLog.GetQuestRewardCurrencyInfo. If the currency maps to
        -- a faction (via C_CurrencyInfo.GetFactionGrantedByCurrency), we treat it as
        -- a reputation reward. Otherwise it's a generic currency.
        local repRewards = {}
        local currencyRewards = {}

        if type(C_QuestLog) == "table" and type(C_QuestLog.GetQuestRewardCurrencies) == "function" then
            local okCList, currList = pcall(C_QuestLog.GetQuestRewardCurrencies, questID)
            if okCList and type(currList) == "table" then
                local numCurr = #currList
                for i = 1, numCurr do
                    local info = nil
                    if type(C_QuestLog.GetQuestRewardCurrencyInfo) == "function" then
                        local okI, full = pcall(C_QuestLog.GetQuestRewardCurrencyInfo, questID, i, false)
                        if okI and type(full) == "table" then info = full end
                    end
                    if not info and type(currList[i]) == "table" then
                        info = currList[i]
                    end

                    if type(info) == "table" then
                        local cid = info.currencyID
                        local amount = info.totalRewardAmount or info.baseRewardAmount or 0
                        local bonus = info.bonusRewardAmount or 0

                        local factionID
                        if type(cid) == "number" and type(C_CurrencyInfo) == "table"
                           and type(C_CurrencyInfo.GetFactionGrantedByCurrency) == "function" then
                            local okF, fid = pcall(C_CurrencyInfo.GetFactionGrantedByCurrency, cid)
                            if okF and type(fid) == "number" and fid > 0 then
                                factionID = fid
                            end
                        end

                        if factionID then
                            -- Reputation currency — keep the currency's own icon
                            repRewards[#repRewards + 1] = {
                                name = info.name or "Reputation",
                                amount = amount,
                                bonus = bonus,
                                factionID = factionID,
                                currencyID = cid,
                                texture = info.texture,
                                quality = info.quality,
                            }
                        else
                            currencyRewards[#currencyRewards + 1] = {
                                name = info.name,
                                texture = info.texture,
                                count = amount,
                                currencyID = cid,
                                quality = info.quality,
                            }
                        end
                    end
                end
            end
        end

        -- Major-faction renown rewards (Council of Dornogal, Hallowfall Arathi, etc).
        -- These come through their own list rather than the currency system.
        if type(C_QuestLog) == "table" and type(C_QuestLog.GetQuestLogMajorFactionReputationRewards) == "function" then
            local okMF, mfList = pcall(C_QuestLog.GetQuestLogMajorFactionReputationRewards, questID)
            if okMF and type(mfList) == "table" then
                for _, mf in ipairs(mfList) do
                    if type(mf) == "table" and type(mf.factionID) == "number" then
                        local factionName, factionTexture, factionAtlas
                        if type(C_MajorFactions) == "table" and type(C_MajorFactions.GetMajorFactionData) == "function" then
                            local okMD, data = pcall(C_MajorFactions.GetMajorFactionData, mf.factionID)
                            if okMD and type(data) == "table" then
                                if type(data.name) == "string" then factionName = data.name end
                                -- Major factions expose their icon via a textureKit string;
                                -- the canonical atlas pattern is MajorFactions_Icons_<textureKit>512
                                if type(data.textureKit) == "string" and data.textureKit ~= "" then
                                    factionAtlas = "MajorFactions_Icons_" .. data.textureKit .. "512"
                                end
                                if type(data.icon) == "string" or type(data.icon) == "number" then
                                    factionTexture = data.icon
                                end
                            end
                        end
                        repRewards[#repRewards + 1] = {
                            name = factionName or "Major Faction",
                            amount = mf.rewardAmount or mf.amount or 0,
                            bonus = 0,
                            factionID = mf.factionID,
                            texture = factionTexture,
                            atlas = factionAtlas,
                        }
                    end
                end
            end
        end

        local hasCurrency = (moneyAmount > 0 or xpAmount > 0)
        local hasRewards = (#itemRewards > 0 or #currencyRewards > 0 or #repRewards > 0 or hasCurrency)

        local rowHeight = QLC.EXPANSION_OBJ_HEIGHT
        local sectionGap = 14
        local labelGap = 6

        local totalHeight = 8

        local function AddLabel(text, alpha)
            local row = CreateFrame("Frame", nil, self)
            row:SetHeight(16)
            row:SetPoint("TOPLEFT", self, "TOPLEFT", pad, -totalHeight)
            row:SetPoint("TOPRIGHT", self, "TOPRIGHT", -pad, -totalHeight)
            local fs = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightExtraSmall")
            fs:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
            fs:SetText(text)
            fs:SetTextColor(tmR, tmG, tmB, alpha or 0.55)
            self._muiChildFrames[#self._muiChildFrames + 1] = row
            totalHeight = totalHeight + 16 + labelGap
        end

        local function AddRow(height)
            local row = CreateFrame("Frame", nil, self)
            row:SetHeight(height or rowHeight)
            row:SetPoint("TOPLEFT", self, "TOPLEFT", pad, -totalHeight)
            row:SetPoint("TOPRIGHT", self, "TOPRIGHT", -pad, -totalHeight)
            self._muiChildFrames[#self._muiChildFrames + 1] = row
            totalHeight = totalHeight + (height or rowHeight)
            return row
        end

        -- Quest description (gossip): auto-show, no toggle, no header — just the text in a quiet block
        local questDescText = nil
        if questLogIndex and type(GetQuestLogQuestText) == "function" then
            local okText, desc = pcall(GetQuestLogQuestText, questLogIndex)
            if okText and type(desc) == "string" and desc ~= "" then
                questDescText = desc
            end
        end

        if questDescText then
            local row = CreateFrame("Frame", nil, self)
            row:SetPoint("TOPLEFT", self, "TOPLEFT", pad, -totalHeight)
            row:SetPoint("TOPRIGHT", self, "TOPRIGHT", -pad, -totalHeight)

            local txtW = QLC.PANEL_WIDTH - (pad * 2)
            local textFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            textFS:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
            textFS:SetWidth(math.max(40, txtW))
            textFS:SetWordWrap(true)
            textFS:SetJustifyH("LEFT")
            textFS:SetJustifyV("TOP")
            textFS:SetSpacing(3)
            textFS:SetText(questDescText)
            textFS:SetTextColor(tsR, tsG, tsB, 0.78)

            local textHeight = math.ceil(textFS:GetStringHeight() or 0)
            row:SetHeight(textHeight)

            self._muiChildFrames[#self._muiChildFrames + 1] = row
            totalHeight = totalHeight + textHeight + sectionGap
        end

        if objRows and #objRows > 0 then
            AddLabel("OBJECTIVES")

            for _, objData in ipairs(objRows) do
                local row = AddRow(rowHeight)
                local done = objData.finished

                local marker = CreateFrame("Frame", nil, row)
                marker:SetSize(12, 12)
                marker:SetPoint("LEFT", row, "LEFT", 2, 0)

                if done then
                    local check = marker:CreateTexture(nil, "ARTWORK")
                    check:SetAllPoints(marker)
                    local okAtlas = pcall(function() check:SetAtlas("common-icon-checkmark") end)
                    if not okAtlas then
                        check:SetTexture("Interface\\RAIDFRAME\\ReadyCheck-Ready")
                    end
                    check:SetVertexColor(successC.r, successC.g, successC.b, 0.95)
                else
                    local ring = marker:CreateTexture(nil, "ARTWORK")
                    ring:SetSize(8, 8)
                    ring:SetPoint("CENTER", marker, "CENTER", 0, 0)
                    ring:SetTexture(W8)
                    ring:SetVertexColor(tsR, tsG, tsB, 0.55)
                    local hole = marker:CreateTexture(nil, "ARTWORK", nil, 1)
                    hole:SetSize(4, 4)
                    hole:SetPoint("CENTER", marker, "CENTER", 0, 0)
                    hole:SetTexture(W8)
                    local bgR, bgG, bgB = TC("bgPanel")
                    hole:SetVertexColor(bgR, bgG, bgB, 1)
                end

                local objText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
                objText:SetPoint("LEFT", marker, "RIGHT", 8, 0)
                objText:SetPoint("RIGHT", row, "RIGHT", -54, 0)
                objText:SetJustifyH("LEFT")
                objText:SetWordWrap(false)
                objText:SetText(objData.text or "")
                if done then
                    objText:SetTextColor(tmR, tmG, tmB, 0.50)
                else
                    objText:SetTextColor(tpR, tpG, tpB, 0.92)
                end

                local fracText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightExtraSmall")
                fracText:SetPoint("RIGHT", row, "RIGHT", 0, 0)
                fracText:SetJustifyH("RIGHT")
                local filled, required
                if objData.progressText then
                    filled, required = objData.progressText:match("(%d+)/(%d+)")
                end
                if done then
                    fracText:SetText("")
                elseif filled and required then
                    fracText:SetText(filled .. "/" .. required)
                    fracText:SetTextColor(tsR, tsG, tsB, 0.70)
                elseif objData.progressText then
                    fracText:SetText(objData.progressText)
                    fracText:SetTextColor(tsR, tsG, tsB, 0.65)
                else
                    fracText:SetText("")
                end
            end

            totalHeight = totalHeight + sectionGap
        end

        if hasRewards then
            AddLabel("REWARDS")

            local iconSize = 22
            local rowH = iconSize + 6

            for _, reward in ipairs(itemRewards) do
                local row = AddRow(rowH)

                local iconBorder = row:CreateTexture(nil, "BACKGROUND", nil, 0)
                iconBorder:SetSize(iconSize + 2, iconSize + 2)
                iconBorder:SetPoint("LEFT", row, "LEFT", 0, 0)
                iconBorder:SetTexture(W8)

                local iconFrame = CreateFrame("Frame", nil, row)
                iconFrame:SetSize(iconSize, iconSize)
                iconFrame:SetPoint("CENTER", iconBorder, "CENTER", 0, 0)

                local icon = iconFrame:CreateTexture(nil, "ARTWORK")
                icon:SetAllPoints(iconFrame)
                if type(reward.texture) == "string" and reward.texture ~= "" then
                    icon:SetTexture(reward.texture)
                elseif type(reward.texture) == "number" then
                    icon:SetTexture(reward.texture)
                else
                    icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
                end
                icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

                local q = reward.quality
                local qR, qG, qB = 0.40, 0.40, 0.45
                if type(q) == "number" and type(ITEM_QUALITY_COLORS) == "table" and ITEM_QUALITY_COLORS[q] then
                    local qc = ITEM_QUALITY_COLORS[q]
                    qR, qG, qB = qc.r or qR, qc.g or qG, qc.b or qB
                end
                iconBorder:SetVertexColor(qR, qG, qB, 0.90)

                if type(reward.count) == "number" and reward.count > 1 then
                    local stackText = iconFrame:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmall")
                    stackText:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", 1, 0)
                    stackText:SetText(tostring(reward.count))
                    stackText:SetTextColor(1, 1, 1, 1)
                    local stackShadow = iconFrame:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmall")
                    stackShadow:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", 2, -1)
                    stackShadow:SetText(tostring(reward.count))
                    stackShadow:SetTextColor(0, 0, 0, 0.85)
                    stackShadow:SetDrawLayer("ARTWORK", 7)
                    stackText:SetDrawLayer("OVERLAY", 1)
                end

                local rewardFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
                rewardFS:SetPoint("LEFT", iconBorder, "RIGHT", 10, 0)
                rewardFS:SetPoint("RIGHT", row, "RIGHT", 0, 0)
                rewardFS:SetJustifyH("LEFT")
                rewardFS:SetWordWrap(false)
                rewardFS:SetText(reward.name or "")
                rewardFS:SetTextColor(qR, qG, qB, 0.95)

                if type(reward.itemID) == "number" and reward.itemID > 0 then
                    local itemID = reward.itemID
                    row:EnableMouse(true)
                    row:SetScript("OnEnter", function(self)
                        rewardFS:SetTextColor(qR, qG, qB, 1)
                        iconBorder:SetVertexColor(qR, qG, qB, 1)
                        pcall(function()
                            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                            GameTooltip:SetItemByID(itemID)
                            GameTooltip:Show()
                        end)
                    end)
                    row:SetScript("OnLeave", function()
                        rewardFS:SetTextColor(qR, qG, qB, 0.95)
                        iconBorder:SetVertexColor(qR, qG, qB, 0.90)
                        pcall(function() GameTooltip:Hide() end)
                    end)
                end
            end

            -- Currency rewards (e.g. valor, anima) — same icon+name+amount pattern as items
            for _, currency in ipairs(currencyRewards) do
                local row = AddRow(math.max(rowHeight, iconSize + 4))

                local cqR, cqG, cqB = 0.55, 0.55, 0.62
                if type(currency.quality) == "number" and type(ITEM_QUALITY_COLORS) == "table" and ITEM_QUALITY_COLORS[currency.quality] then
                    local qc = ITEM_QUALITY_COLORS[currency.quality]
                    cqR, cqG, cqB = qc.r or cqR, qc.g or cqG, qc.b or cqB
                end

                local cBorder = row:CreateTexture(nil, "BACKGROUND", nil, 0)
                cBorder:SetSize(iconSize + 2, iconSize + 2)
                cBorder:SetPoint("LEFT", row, "LEFT", 0, 0)
                cBorder:SetTexture(W8)
                cBorder:SetVertexColor(cqR, cqG, cqB, 0.85)

                local cIconFrame = CreateFrame("Frame", nil, row)
                cIconFrame:SetSize(iconSize, iconSize)
                cIconFrame:SetPoint("CENTER", cBorder, "CENTER", 0, 0)
                local cIcon = cIconFrame:CreateTexture(nil, "ARTWORK")
                cIcon:SetAllPoints(cIconFrame)
                if type(currency.texture) == "string" and currency.texture ~= "" then
                    cIcon:SetTexture(currency.texture)
                elseif type(currency.texture) == "number" then
                    cIcon:SetTexture(currency.texture)
                else
                    cIcon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
                end
                cIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

                local cFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
                cFS:SetPoint("LEFT", cBorder, "RIGHT", 10, 0)
                cFS:SetPoint("RIGHT", row, "RIGHT", 0, 0)
                cFS:SetJustifyH("LEFT")
                cFS:SetWordWrap(false)
                local label = currency.name or ""
                if type(currency.count) == "number" and currency.count > 1 then
                    label = label .. "  \194\183  " .. tostring(currency.count)
                end
                cFS:SetText(label)
                cFS:SetTextColor(cqR, cqG, cqB, 0.95)

                if type(currency.currencyID) == "number" and currency.currencyID > 0 then
                    local currencyID = currency.currencyID
                    row:EnableMouse(true)
                    row:SetScript("OnEnter", function(self)
                        cFS:SetTextColor(cqR, cqG, cqB, 1)
                        cBorder:SetVertexColor(cqR, cqG, cqB, 1)
                        pcall(function()
                            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                            GameTooltip:SetCurrencyByID(currencyID)
                            GameTooltip:Show()
                        end)
                    end)
                    row:SetScript("OnLeave", function()
                        cFS:SetTextColor(cqR, cqG, cqB, 0.95)
                        cBorder:SetVertexColor(cqR, cqG, cqB, 0.85)
                        pcall(function() GameTooltip:Hide() end)
                    end)
                end
            end

            -- Reputation rewards: each shown with its own faction icon, then
            -- "Reputation +N · <Faction>" so the faction itself stays the visible label.
            for _, rep in ipairs(repRewards) do
                local row = AddRow(math.max(rowHeight, iconSize + 4))

                local repBorder = row:CreateTexture(nil, "BACKGROUND", nil, 0)
                repBorder:SetSize(iconSize + 2, iconSize + 2)
                repBorder:SetPoint("LEFT", row, "LEFT", 0, 0)
                repBorder:SetTexture(W8)
                repBorder:SetVertexColor(0.85, 0.70, 0.30, 0.85)  -- subtle gold ring

                local repIconFrame = CreateFrame("Frame", nil, row)
                repIconFrame:SetSize(iconSize, iconSize)
                repIconFrame:SetPoint("CENTER", repBorder, "CENTER", 0, 0)

                local repIcon = repIconFrame:CreateTexture(nil, "ARTWORK")
                repIcon:SetAllPoints(repIconFrame)
                local applied = false
                if type(rep.atlas) == "string" and rep.atlas ~= "" then
                    local okA = pcall(function() repIcon:SetAtlas(rep.atlas) end)
                    if okA then applied = true end
                end
                if not applied and (type(rep.texture) == "string" or type(rep.texture) == "number") then
                    repIcon:SetTexture(rep.texture)
                    repIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
                    applied = true
                end
                if not applied then
                    -- Last-resort generic icon if neither an atlas nor a texture came back
                    repIcon:SetTexture("Interface\\Icons\\Achievement_Reputation_01")
                    repIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
                end

                local repFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
                repFS:SetPoint("LEFT", repBorder, "RIGHT", 10, 0)
                repFS:SetPoint("RIGHT", row, "RIGHT", 0, 0)
                repFS:SetJustifyH("LEFT")
                repFS:SetWordWrap(false)

                local amountStr
                if type(rep.amount) == "number" and rep.amount ~= 0 then
                    if rep.amount > 0 then
                        amountStr = string.format("+%d", rep.amount)
                    else
                        amountStr = string.format("%d", rep.amount)
                    end
                else
                    amountStr = "+0"
                end
                local bonusSuffix = ""
                if type(rep.bonus) == "number" and rep.bonus > 0 then
                    bonusSuffix = string.format("  |cff7AC57F(+%d bonus)|r", rep.bonus)
                end
                repFS:SetText(string.format("|cffFFD27ARep|r  %s  |cffFFD27A\194\183|r  %s%s", amountStr, rep.name, bonusSuffix))
                repFS:SetTextColor(0.95, 0.86, 0.65, 0.95)

                -- Hover tooltip: prefer the game's own currency tooltip when this rep is
                -- delivered via a currency (covers The Amani Tribe, expansion factions, etc).
                -- For major-faction renown rewards we build a minimal tooltip ourselves
                -- using the major-faction data we already pulled.
                local repCurrencyID = rep.currencyID
                local repFactionID = rep.factionID
                local repAmountForTip = rep.amount
                local repBonusForTip = rep.bonus
                local repNameForTip = rep.name

                row:EnableMouse(true)
                row:SetScript("OnEnter", function(self)
                    repFS:SetTextColor(1, 0.95, 0.78, 1)
                    repBorder:SetVertexColor(1.00, 0.84, 0.40, 1)

                    pcall(function()
                        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                        local handled = false
                        if type(repCurrencyID) == "number" and repCurrencyID > 0
                           and type(GameTooltip.SetCurrencyByID) == "function" then
                            local okSet = pcall(GameTooltip.SetCurrencyByID, GameTooltip, repCurrencyID)
                            if okSet then handled = true end
                        end
                        if not handled then
                            GameTooltip:AddLine(repNameForTip or "Reputation", 1, 0.84, 0.30)
                            local amtText
                            if type(repAmountForTip) == "number" and repAmountForTip ~= 0 then
                                amtText = string.format("%s%d Reputation",
                                    (repAmountForTip > 0) and "+" or "",
                                    repAmountForTip)
                            end
                            if amtText then
                                GameTooltip:AddLine(amtText, 0.85, 0.85, 0.80)
                            end
                            if type(repBonusForTip) == "number" and repBonusForTip > 0 then
                                GameTooltip:AddLine(string.format("+%d bonus reputation", repBonusForTip), 0.45, 0.80, 0.50)
                            end
                            if type(repFactionID) == "number" and repFactionID > 0
                               and type(C_MajorFactions) == "table"
                               and type(C_MajorFactions.GetMajorFactionData) == "function" then
                                local okMD, data = pcall(C_MajorFactions.GetMajorFactionData, repFactionID)
                                if okMD and type(data) == "table" then
                                    if type(data.description) == "string" and data.description ~= "" then
                                        GameTooltip:AddLine(" ")
                                        GameTooltip:AddLine(data.description, 0.70, 0.70, 0.65, true)
                                    end
                                    if type(data.renownLevel) == "number" then
                                        GameTooltip:AddLine(string.format("Renown %d", data.renownLevel), 0.95, 0.78, 0.30)
                                    end
                                end
                            end
                        end
                        GameTooltip:Show()
                    end)
                end)
                row:SetScript("OnLeave", function()
                    repFS:SetTextColor(0.95, 0.86, 0.65, 0.95)
                    repBorder:SetVertexColor(0.85, 0.70, 0.30, 0.85)
                    pcall(function() GameTooltip:Hide() end)
                end)
            end

            if hasCurrency then
                -- Hairline above the currency row to separate item rewards from money/xp
                local sepRow = CreateFrame("Frame", nil, self)
                sepRow:SetHeight(1)
                sepRow:SetPoint("TOPLEFT", self, "TOPLEFT", pad, -(totalHeight + 4))
                sepRow:SetPoint("TOPRIGHT", self, "TOPRIGHT", -pad, -(totalHeight + 4))
                local sepTex = sepRow:CreateTexture(nil, "ARTWORK")
                sepTex:SetAllPoints(sepRow)
                sepTex:SetTexture(W8)
                local sepBdR, sepBdG, sepBdB = TC("border")
                sepTex:SetVertexColor(sepBdR, sepBdG, sepBdB, 0.20)
                self._muiChildFrames[#self._muiChildFrames + 1] = sepRow
                totalHeight = totalHeight + 8

                local row = AddRow(rowHeight)

                local parts = {}
                if moneyAmount > 0 then
                    local g = math.floor(moneyAmount / 10000)
                    local s = math.floor((moneyAmount % 10000) / 100)
                    local c = math.floor(moneyAmount % 100)
                    if g > 0 then
                        parts[#parts + 1] = string.format("|cffFFD86C%d|r|TInterface\\MoneyFrame\\UI-GoldIcon:13:13:2:0|t", g)
                    end
                    if s > 0 then
                        parts[#parts + 1] = string.format("|cffE0E0E0%d|r|TInterface\\MoneyFrame\\UI-SilverIcon:13:13:2:0|t", s)
                    end
                    if c > 0 and g == 0 then
                        parts[#parts + 1] = string.format("|cffC58A55%d|r|TInterface\\MoneyFrame\\UI-CopperIcon:13:13:2:0|t", c)
                    end
                end
                if xpAmount > 0 then
                    parts[#parts + 1] = string.format("|cff8AB4E8%s XP|r", tostring(xpAmount))
                end

                local currFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
                currFS:SetPoint("LEFT", row, "LEFT", 0, 0)
                currFS:SetText(table.concat(parts, "    "))
            end

            totalHeight = totalHeight + sectionGap
        end

        totalHeight = totalHeight + 4

        self:SetHeight(totalHeight)
        self:Show()
    end

    return frame
end



function Panel.CreateEmptyState(parent)
    local frame = CreateFrame("Frame", nil, parent)
    frame:SetHeight(QLC.EMPTY_STATE_HEIGHT)

    local tpR, tpG, tpB = TC("textPrimary")
    local tmR, tmG, tmB = TC("textMuted")

    local mainText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    mainText:SetPoint("CENTER", frame, "CENTER", 0, 10)
    mainText:SetTextColor(tpR, tpG, tpB, 1)
    frame._muiMainText = mainText

    local subText = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    subText:SetPoint("TOP", mainText, "BOTTOM", 0, -6)
    subText:SetTextColor(tmR, tmG, tmB, 1)
    frame._muiSubText = subText

    local clearBtn = CreateFrame("Button", nil, frame)
    clearBtn:SetHeight(20)
    clearBtn:SetPoint("TOP", subText, "BOTTOM", 0, -4)
    local clearText = clearBtn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    clearText:SetPoint("CENTER", clearBtn, "CENTER", 0, 0)
    local acR, acG, acB = TC("accent")
    clearText:SetText("Clear")
    clearText:SetTextColor(acR, acG, acB, 1)
    clearBtn:SetWidth(clearText:GetStringWidth() + 8)
    clearBtn:SetScript("OnClick", function()
        local panel = Panel._state.panel
        if panel and panel._muiSearchBox then
            panel._muiSearchBox:SetText("")
            panel._muiSearchBox._muiPlaceholder:Show()
            Panel.Rebuild()
        end
    end)
    clearBtn:SetScript("OnEnter", function()
        clearText:SetTextColor(1, 1, 1, 1)
    end)
    clearBtn:SetScript("OnLeave", function()
        clearText:SetTextColor(acR, acG, acB, 1)
    end)
    clearBtn:Hide()
    frame._muiClearBtn = clearBtn

    function frame:SetEmptyData(searchText)
        if type(searchText) == "string" and searchText ~= "" then
            self._muiMainText:SetText("No quests match '" .. searchText .. "'")
            self._muiSubText:SetText("")
            self._muiClearBtn:Show()
        else
            self._muiMainText:SetText("No active quests")
            self._muiSubText:SetText("Pick up quests from NPCs")
            self._muiClearBtn:Hide()
        end
        self:Show()
    end

    return frame
end



local function SuppressBlizzardQuestFrame()
    local qmf = _G.QuestMapFrame
    if qmf and type(qmf.Hide) == "function" then
        pcall(function() qmf:Hide() end)
    end
    local searchBox = qmf and qmf.SearchBox
    if searchBox and type(searchBox.Hide) == "function" then
        pcall(function() searchBox:Hide() end)
    end
    if qmf and not qmf._muiHideHook then
        qmf._muiHideHook = true
        pcall(function()
            qmf:HookScript("OnShow", function(self)
                if Addon.QuestLogPanel and Addon.QuestLogPanel.IsOpen and Addon.QuestLogPanel.IsOpen() then
                    self:Hide()
                end
            end)
        end)
    end
end

-- ============================================================
-- SECTION: Layout + Rebuild Engine
-- READS: Panel._state, QD.CollectQuestListData, QLC layout,
--   panel._muiSearchBox:GetText()
-- WRITES: panel scroll child height, all pooled widget states
-- FUNCTIONS:
--   Panel.Rebuild() — master layout entry point:
--     1. Releases all pools via QuestListReleaseAllInPool.
--     2. Calls CollectQuestListData(searchFilter) for data.
--     3. Renders campaign card if present.
--     4. For each bucket (NOW/NEXT/LATER + conditionals):
--        renders BucketHeader, zone banners, quest rows,
--        and expanded objectives.
--     5. Updates scroll child height for the scrollframe.
--   Panel.ScheduleRebuild() — debounce wrapper; coalesces
--     rapid event-triggered rebuilds into a single C_Timer.
-- ============================================================
function Panel.Rebuild()
    local panel = Panel._state.panel
    if not panel then return end
    if not Panel._state.panelOpen then return end
    SuppressBlizzardQuestFrame()

    local searchText = ""
    if panel._muiSearchBox then
        local okText, text = pcall(function() return panel._muiSearchBox:GetText() end)
        if okText and type(text) == "string" then
            searchText = text
        end
    end

    local listData = QD.CollectQuestListData(searchText ~= "" and searchText or nil)

    if panel._muiCount then
        panel._muiCount:SetText(string.format("%d / %d", listData.totalQuestCount, listData.maxQuests))
    end

    local searchBox = panel._muiSearchBox
    local scrollFrame = panel._muiScrollFrame
    local headerHeight = QLC.PANEL_HEADER_HEIGHT

    -- Campaign data drives the new header layout:
    --   Hero line (title)    = campaign name when present, else "Quest Log"
    --   Sub line (subtitle)  = "Chapter Twelve · The Awakening" when present, else empty
    --   Right chapter mark   = current chapter number / total
    --   Bottom progress fill = chapter completion fraction
    local campaign = listData.campaign
    local _campaignIsFallback = false
    if not campaign and listData.headers and #listData.headers > 0 then
        campaign = QD.ResolveCampaignCardFallback(listData.headers)
        _campaignIsFallback = true
    end

    local heroText = "Quest Log"
    local subtitleText = ""
    local chapterCurrent, chapterTotal = nil, nil
    local fillFraction = 0

    if campaign and campaign.name then
        heroText = campaign.name
        local prog = campaign.progress or 0
        local tot = campaign.total or 1
        if tot < 1 then tot = 1 end
        if not _campaignIsFallback then
            chapterCurrent = math.min(prog + 1, tot)
            chapterTotal = tot
            local chapterName = campaign.chapterName
            if type(chapterName) == "string" and chapterName ~= "" then
                subtitleText = "Chapter " .. chapterCurrent .. "  \194\183  " .. chapterName
            else
                subtitleText = "Chapter " .. chapterCurrent .. " of " .. tot
            end
        else
            subtitleText = string.format("%d quests in this storyline", tot)
        end
        fillFraction = Clamp(prog / tot, 0, 1)
    end

    if panel._muiTitle then
        panel._muiTitle:SetText(heroText)
    end

    if panel._muiSubtitle then
        panel._muiSubtitle:SetText(subtitleText)
    end

    if panel._muiChapterMark then
        if chapterCurrent and chapterTotal then
            panel._muiChapterMark._muiCurrent:SetText(tostring(chapterCurrent))
            panel._muiChapterMark._muiTotal:SetText("/ " .. tostring(chapterTotal))
            panel._muiChapterMark:Show()
        else
            panel._muiChapterMark:Hide()
        end
    end

    if panel._muiCampFill then
        if fillFraction > 0 then
            local headerW = panel._muiHeader and panel._muiHeader:GetWidth() or QLC.PANEL_WIDTH
            local trackW = math.max(1, headerW - (QLC.PANEL_PAD_X * 2))
            panel._muiCampFill:SetWidth(math.max(1, math.floor(trackW * fillFraction)))
            panel._muiCampFill:Show()
        else
            panel._muiCampFill:Hide()
        end
    end

    local searchHeight = 0
    if searchBox then
        if searchBox:IsShown() then
            searchHeight = 32
            searchBox:ClearAllPoints()
            searchBox:SetPoint("TOPLEFT", panel._muiHeader, "BOTTOMLEFT", 0, 0)
            searchBox:SetPoint("TOPRIGHT", panel._muiHeader, "BOTTOMRIGHT", 0, 0)
            searchBox:SetHeight(searchHeight)
        end
    end

    local fixedTopHeight = headerHeight + searchHeight + 8
    if scrollFrame then
        scrollFrame:ClearAllPoints()
        scrollFrame:SetPoint("TOPLEFT", panel, "TOPLEFT", 0, -fixedTopHeight)
        scrollFrame:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -6, -4)
    end

    QD.QuestListReleaseAllInPool("bucketHeaders")
    QD.QuestListReleaseAllInPool("zoneSubHeaders")
    QD.QuestListReleaseAllInPool("quests")
    QD.QuestListReleaseAllInPool("expansions")
    QD.QuestListReleaseAllInPool("focusHeaders")
    QD.QuestListReleaseAllInPool("empty")

    local scrollChild = panel._muiScrollChild
    if not scrollChild then return end

    local totalHeight = 0
    local gap = QLC.QUEST_ROW_GAP
    local panelContentWidth = QLC.PANEL_WIDTH - 16
    local hasAnyContent = false

    local function PlaceFrame(frame, height, extraGap)
        frame:ClearAllPoints()
        frame:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, -totalHeight)
        frame:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", 0, -totalHeight)
        if height then
            frame:SetHeight(height)
        end
        frame:Show()
        local h = height or frame:GetHeight()
        totalHeight = totalHeight + h + (extraGap or gap)
        hasAnyContent = true
    end

    local function AddSectionGap()
        totalHeight = totalHeight + 8
    end


    local focusedQuestID = Panel._state.focusedQuestID
    local focusedQuest, focusBucketKey = nil, nil
    if focusedQuestID then
        focusedQuest, focusBucketKey = QD.ExtractFocusedQuestFromBuckets(listData, focusedQuestID)
        if not focusedQuest then
            for _, condKey in ipairs({"turnIn", "expiring"}) do
                local condQuests = listData[condKey]
                if type(condQuests) == "table" then
                    for i = 1, #condQuests do
                        if condQuests[i].questID == focusedQuestID then
                            focusedQuest = table.remove(condQuests, i)
                            focusBucketKey = condKey
                            break
                        end
                    end
                end
                if focusedQuest then break end
            end
        end
    end

    if focusedQuest then
        -- Focused quest pins to the top with an accent state-dot (signal lives on the row itself,
        -- not in a separate FOCUS header). Auto-expanded so the player sees its objectives first.
        local focusRow = QD.QuestListAcquireFromPool("quests", function(p) return Panel.CreateQuestRow(p) end, scrollChild)
        if focusRow then
            focusRow:SetQuestData(focusedQuest, focusBucketKey or "now")
            PlaceFrame(focusRow, QLC.QUEST_ROW_HEIGHT)
        end

        QD._expandedQuests[focusedQuestID] = true
        local focusExpansion = QD.QuestListAcquireFromPool("expansions", function(p) return Panel.CreateExpansion(p) end, scrollChild)
        if focusExpansion then
            local focusAccentColor = { r = 1, g = 0.84, b = 0.30, a = 1 }
            focusExpansion:SetExpansionData(focusedQuestID, focusedQuest.title, focusAccentColor, focusedQuestID)
            PlaceFrame(focusExpansion)
        end
        AddSectionGap()
    end

    local hasConditionalContent = false

    local turnInQuests = listData.turnIn
    if type(turnInQuests) == "table" and #turnInQuests > 0 then
        local isCollapsed = QD._bucketCollapseState["turnIn"] or false

        local bucketHeader = QD.QuestListAcquireFromPool("bucketHeaders", function(p) return Panel.CreateBucketHeader(p) end, scrollChild)
        if bucketHeader then
            bucketHeader:SetBucketData("turnIn", #turnInQuests, isCollapsed)
            PlaceFrame(bucketHeader, QLC.BUCKET_HEADER_HEIGHT)
        end

        if not isCollapsed then
            for _, quest in ipairs(turnInQuests) do
                local questRow = QD.QuestListAcquireFromPool("quests", function(p) return Panel.CreateQuestRow(p) end, scrollChild)
                if questRow then
                    questRow:SetQuestData(quest, "turnIn")
                    PlaceFrame(questRow, QLC.QUEST_ROW_HEIGHT)
                end
                if QD._expandedQuests[quest.questID] then
                    local expansion = QD.QuestListAcquireFromPool("expansions", function(p) return Panel.CreateExpansion(p) end, scrollChild)
                    if expansion then
                        local accentColor = questRow and questRow._muiAccentColor or { r = 0.34, g = 0.82, b = 0.46 }
                        expansion:SetExpansionData(quest.questID, quest.title, accentColor, focusedQuestID)
                        PlaceFrame(expansion)
                    end
                end
            end
        end
        hasConditionalContent = true
        AddSectionGap()
    end

    local expiringQuests = listData.expiring
    if type(expiringQuests) == "table" and #expiringQuests > 0 then
        local isCollapsed = QD._bucketCollapseState["expiring"] or false

        local bucketHeader = QD.QuestListAcquireFromPool("bucketHeaders", function(p) return Panel.CreateBucketHeader(p) end, scrollChild)
        if bucketHeader then
            bucketHeader:SetBucketData("expiring", #expiringQuests, isCollapsed)
            PlaceFrame(bucketHeader, QLC.BUCKET_HEADER_HEIGHT)
        end

        if not isCollapsed then
            for _, quest in ipairs(expiringQuests) do
                local questRow = QD.QuestListAcquireFromPool("quests", function(p) return Panel.CreateQuestRow(p) end, scrollChild)
                if questRow then
                    questRow:SetQuestData(quest, "expiring")
                    PlaceFrame(questRow, QLC.QUEST_ROW_HEIGHT)
                end
                if QD._expandedQuests[quest.questID] then
                    local expansion = QD.QuestListAcquireFromPool("expansions", function(p) return Panel.CreateExpansion(p) end, scrollChild)
                    if expansion then
                        local accentColor = questRow and questRow._muiAccentColor or { r = 1.00, g = 0.52, b = 0.20 }
                        expansion:SetExpansionData(quest.questID, quest.title, accentColor, focusedQuestID)
                        PlaceFrame(expansion)
                    end
                end
            end
        end
        hasConditionalContent = true
        AddSectionGap()
    end

    if hasConditionalContent then
        totalHeight = totalHeight + 4
    end

    for _, bucketKey in ipairs(QLC.BUCKET_ORDER) do
        local quests = listData.buckets[bucketKey]
        if type(quests) == "table" and #quests > 0 then
            local isCollapsed = QD._bucketCollapseState[bucketKey] or false

            local bucketHeader = QD.QuestListAcquireFromPool("bucketHeaders", function(p) return Panel.CreateBucketHeader(p) end, scrollChild)
            if bucketHeader then
                bucketHeader:SetBucketData(bucketKey, #quests, isCollapsed)
                PlaceFrame(bucketHeader, QLC.BUCKET_HEADER_HEIGHT)
            end

            if not isCollapsed then
                local zoneMap = {}
                local zoneOrder = {}
                for _, quest in ipairs(quests) do
                    local zone = quest.headerTitle or "Unknown"
                    if not zoneMap[zone] then
                        zoneMap[zone] = {}
                        zoneOrder[#zoneOrder + 1] = zone
                    end
                    zoneMap[zone][#zoneMap[zone] + 1] = quest
                end

                local currentZone = listData.zoneName or ""
                table.sort(zoneOrder, function(a, b)
                    local aIsCurrent = (a == currentZone)
                    local bIsCurrent = (b == currentZone)
                    if aIsCurrent and not bIsCurrent then return true end
                    if not aIsCurrent and bIsCurrent then return false end
                    return a < b
                end)

                local showZoneHeaders = (#zoneOrder >= 2)

                for _, zoneName in ipairs(zoneOrder) do
                    local zoneQuests = zoneMap[zoneName]
                    local isCurrentZone = (zoneName == currentZone)
                    local zoneCollapsed = QD._collapseState[zoneName] or false

                    if showZoneHeaders then
                        local zoneHeader = QD.QuestListAcquireFromPool("zoneSubHeaders", function(p) return Panel.CreateZoneHeader(p) end, scrollChild)
                        if zoneHeader then
                            zoneHeader:SetZoneHeaderData(zoneName, #zoneQuests, isCurrentZone, zoneCollapsed)
                            PlaceFrame(zoneHeader, QLC.ZONE_HEADER_HEIGHT)
                        end
                    end

                    if not zoneCollapsed or not showZoneHeaders then
                        for _, quest in ipairs(zoneQuests) do
                            local questRow = QD.QuestListAcquireFromPool("quests", function(p) return Panel.CreateQuestRow(p) end, scrollChild)
                            if questRow then
                                questRow:SetQuestData(quest, bucketKey)
                                PlaceFrame(questRow, QLC.QUEST_ROW_HEIGHT)
                            end

                            if QD._expandedQuests[quest.questID] then
                                local expansion = QD.QuestListAcquireFromPool("expansions", function(p) return Panel.CreateExpansion(p) end, scrollChild)
                                if expansion then
                                    local accentColor = questRow and questRow._muiAccentColor or { r = 1, g = 1, b = 1 }
                                    expansion:SetExpansionData(quest.questID, quest.title, accentColor, focusedQuestID)
                                    PlaceFrame(expansion)
                                end
                            end
                        end
                    end
                end
            end
            AddSectionGap()
        end
    end

    if not hasAnyContent then
        local emptyState = QD.QuestListAcquireFromPool("empty", function(p) return Panel.CreateEmptyState(p) end, scrollChild)
        if emptyState then
            emptyState:SetEmptyData(searchText)
            PlaceFrame(emptyState, QLC.EMPTY_STATE_HEIGHT)
        end
    end

    scrollChild:SetHeight(math.max(1, totalHeight))
end



-- ============================================================
-- SECTION: Context Menu
-- READS: questID, QuestMapFrame visibility state
-- WRITES: nothing persistent
-- FUNCTIONS:
--   Panel.OpenContextMenu(anchorFrame, questID, questTitle)
--     Builds a right-click context menu via
--     MenuUtil.CreateContextMenu with entries:
--     Track/Untrack, Share, Abandon, Cancel.
--     Uses C_QuestLog.SetSelectedQuest for tracking toggle.
-- ============================================================
function Panel.OpenContextMenu(anchorFrame, questID, questTitle)
    if not questID then return end

    local isFocused = (Panel._state.focusedQuestID == questID)
    local canShare = Panel.EvaluateShareable(questID)
    local canAbandon = Panel.EvaluateAbandonable(questID)

    if type(MenuUtil) == "table" and type(MenuUtil.CreateContextMenu) == "function" then
        local okMenu = pcall(function()
            MenuUtil.CreateContextMenu(anchorFrame, function(ownerRegion, rootDescription)
                if isFocused then
                    rootDescription:CreateButton("Clear Focus", function()
                        Panel.ClearFocusedQuest()
                        Panel.Rebuild()
                    end)
                else
                    rootDescription:CreateButton("Focus", function()
                        Panel.SetFocusedQuest(questID)
                        Panel.Rebuild()
                    end)
                end

                rootDescription:CreateButton("Share Quest", function()
                    Panel.ExecuteShare(questID)
                end)

                if canAbandon then
                    rootDescription:CreateButton("Abandon", function()
                        Panel.ExecuteAbandon(questID, questTitle)
                    end)
                end
            end)
        end)
        if okMenu then return end
    end

    if type(EasyMenu) == "function" then
        local menuList = {}

        if isFocused then
            menuList[#menuList + 1] = {
                text = "Clear Focus",
                func = function()
                    Panel.ClearFocusedQuest()
                    Panel.Rebuild()
                end,
                notCheckable = true,
            }
        else
            menuList[#menuList + 1] = {
                text = "Focus",
                func = function()
                    Panel.SetFocusedQuest(questID)
                    Panel.Rebuild()
                end,
                notCheckable = true,
            }
        end

        menuList[#menuList + 1] = {
            text = "Share Quest",
            func = function()
                Panel.ExecuteShare(questID)
            end,
            notCheckable = true,
        }

        if canAbandon then
            menuList[#menuList + 1] = {
                text = "Abandon",
                func = function()
                    Panel.ExecuteAbandon(questID, questTitle)
                end,
                notCheckable = true,
            }
        end

        local menuFrame = _G["MidnightUIQuestContextMenu"]
        if not menuFrame then
            menuFrame = CreateFrame("Frame", "MidnightUIQuestContextMenu", UIParent, "UIDropDownMenuTemplate")
        end
        pcall(EasyMenu, menuList, menuFrame, "cursor", 0, 0, "MENU")
    end
end



function Panel.SetFocusedQuest(questID)
    if type(questID) ~= "number" then return end
    Panel._state.focusedQuestID = questID
    if type(C_SuperTrack) == "table" and type(C_SuperTrack.SetSuperTrackedQuestID) == "function" then
        pcall(C_SuperTrack.SetSuperTrackedQuestID, questID)
    end
end

function Panel.ClearFocusedQuest()
    Panel._state.focusedQuestID = nil
    if type(C_SuperTrack) == "table" and type(C_SuperTrack.SetSuperTrackedQuestID) == "function" then
        pcall(C_SuperTrack.SetSuperTrackedQuestID, 0)
    end
end

function Panel.ResolveQuestLogIndex(questID)
    if type(questID) ~= "number" then return nil end
    if type(C_QuestLog) == "table" and type(C_QuestLog.GetLogIndexForQuestID) == "function" then
        local ok, index = pcall(C_QuestLog.GetLogIndexForQuestID, questID)
        if ok and type(index) == "number" and index > 0 then
            return index
        end
    end
    return nil
end

function Panel.EvaluateShareable(questID)
    if type(questID) ~= "number" then return false end

    local inGroup = false
    if type(IsInGroup) == "function" then
        local ok, result = pcall(IsInGroup)
        if ok and result then inGroup = true end
    end
    if not inGroup then return false end

    if type(C_QuestLog) == "table" and type(C_QuestLog.IsPushableQuest) == "function" then
        local ok, pushable = pcall(C_QuestLog.IsPushableQuest, questID)
        if ok and pushable then return true end
    end

    local logIndex = Panel.ResolveQuestLogIndex(questID)
    if logIndex then
        if type(SelectQuestLogEntry) == "function" then
            pcall(SelectQuestLogEntry, logIndex)
        end
        if type(GetQuestLogPushable) == "function" then
            local ok, pushable = pcall(GetQuestLogPushable)
            if ok and pushable then return true end
        end
    end

    return false
end

function Panel.EvaluateAbandonable(questID)
    if type(questID) ~= "number" then return false end
    if type(C_QuestLog) == "table" and type(C_QuestLog.CanAbandonQuest) == "function" then
        local ok, canAbandon = pcall(C_QuestLog.CanAbandonQuest, questID)
        if ok then return canAbandon end
    end
    local logIndex = Panel.ResolveQuestLogIndex(questID)
    return (logIndex ~= nil)
end

function Panel.ExecuteShare(questID)
    if type(questID) ~= "number" then return end
    if type(C_QuestLog) == "table" and type(C_QuestLog.SetSelectedQuest) == "function" then
        pcall(C_QuestLog.SetSelectedQuest, questID)
    end
    if type(QuestMapQuestOptions_ShareQuest) == "function" then
        pcall(QuestMapQuestOptions_ShareQuest, questID)
        return
    end
    if type(C_QuestLog) == "table" and type(C_QuestLog.ShareQuest) == "function" then
        pcall(C_QuestLog.ShareQuest, questID)
        return
    end
    local logIndex = Panel.ResolveQuestLogIndex(questID)
    if logIndex then
        if type(SelectQuestLogEntry) == "function" then
            pcall(SelectQuestLogEntry, logIndex)
        end
        if type(QuestLogPushQuest) == "function" then
            pcall(QuestLogPushQuest)
        end
    end
end

function Panel.ExecuteAbandon(questID, questTitle)
    if type(questID) ~= "number" then return end
    if type(C_QuestLog) == "table" then
        if type(C_QuestLog.SetSelectedQuest) == "function" then
            pcall(C_QuestLog.SetSelectedQuest, questID)
        end
        if type(C_QuestLog.SetAbandonQuest) == "function" then
            pcall(C_QuestLog.SetAbandonQuest)
        end
    else
        local logIndex = Panel.ResolveQuestLogIndex(questID)
        if logIndex then
            if type(SelectQuestLogEntry) == "function" then
                pcall(SelectQuestLogEntry, logIndex)
            end
            if type(SetAbandonQuest) == "function" then
                pcall(SetAbandonQuest)
            end
        end
    end
    if type(StaticPopup_Show) == "function" then
        local displayTitle = questTitle or ""
        pcall(StaticPopup_Show, "ABANDON_QUEST", displayTitle)
    end
end

function Panel.ToggleTracking(questID)
    if type(questID) ~= "number" then return end

    local isTracked = false
    if type(C_QuestLog) == "table" and type(C_QuestLog.GetQuestWatchType) == "function" then
        local ok, watchType = pcall(C_QuestLog.GetQuestWatchType, questID)
        if ok and watchType ~= nil then
            isTracked = true
        end
    end

    if isTracked then
        if type(C_QuestLog) == "table" and type(C_QuestLog.RemoveQuestWatch) == "function" then
            pcall(C_QuestLog.RemoveQuestWatch, questID)
        end
        if type(C_SuperTrack) == "table" and type(C_SuperTrack.GetSuperTrackedQuestID) == "function" then
            local okST, stID = pcall(C_SuperTrack.GetSuperTrackedQuestID)
            if okST and stID == questID then
                if type(C_SuperTrack.SetSuperTrackedQuestID) == "function" then
                    pcall(C_SuperTrack.SetSuperTrackedQuestID, 0)
                end
            end
        end
    else
        if type(C_QuestLog) == "table" and type(C_QuestLog.AddQuestWatch) == "function" then
            local watchType = _G.Enum and _G.Enum.QuestWatchType and _G.Enum.QuestWatchType.Manual
            if watchType then
                pcall(C_QuestLog.AddQuestWatch, questID, watchType)
            else
                pcall(C_QuestLog.AddQuestWatch, questID)
            end
        end
        if type(C_SuperTrack) == "table" and type(C_SuperTrack.SetSuperTrackedQuestID) == "function" then
            pcall(C_SuperTrack.SetSuperTrackedQuestID, questID)
        end
    end
end



-- ============================================================
-- SECTION: Slide Animation + Toggle + Event Registration
-- READS: Panel._state.panel, panel _muiSlideIn/_muiSlideOut
-- WRITES: panel position (ClearAllPoints/SetPoint), alpha
-- FUNCTIONS:
--   PlaySlideIn(panel) — animates panel sliding in from the
--     left edge of the map; sets alpha 0→1 over
--     QLC.PANEL_SLIDE_DURATION seconds.
--   PlaySlideOut(panel) — reverse; hides panel on complete.
--   Panel.Toggle(map) — public API called by Map.lua tab:
--     opens if closed, closes if open; calls EnsurePanel.
--   EventFrame OnEvent — routes quest log events to
--     Panel.ScheduleRebuild(); PLAYER_ENTERING_WORLD also
--     reinitializes panel state.
-- ============================================================
local function PlaySlideIn(panel)
    if not panel then return end
    if panel._muiSlideOut then
        local okStop = pcall(function()
            if panel._muiSlideOut:IsPlaying() then
                panel._muiSlideOut:Stop()
            end
        end)
    end
    panel:SetAlpha(0)
    panel:Show()
    if panel._muiSlideIn then
        pcall(function() panel._muiSlideIn:Play() end)
    else
        panel:SetAlpha(1)
    end
end

local function PlaySlideOut(panel)
    if not panel then return end
    if panel._muiSlideIn then
        local okStop = pcall(function()
            if panel._muiSlideIn:IsPlaying() then
                panel._muiSlideIn:Stop()
            end
        end)
    end
    if panel._muiSlideOut then
        pcall(function() panel._muiSlideOut:Play() end)
    else
        panel:Hide()
        Panel._state.panelOpen = false
    end
end



function Panel.Show(map)
    local panel = Panel.EnsurePanel(map or _G.WorldMapFrame)
    if not panel then return end
    Panel._state.panelOpen = true
    SuppressBlizzardQuestFrame()
    PlaySlideIn(panel)
    Panel.Rebuild()
end

function Panel.Hide()
    local panel = Panel._state.panel
    if not panel then
        Panel._state.panelOpen = false
        return
    end
    PlaySlideOut(panel)
end

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

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



local EventFrame = CreateFrame("Frame")
EventFrame:RegisterEvent("QUEST_LOG_UPDATE")
EventFrame:RegisterEvent("QUEST_ACCEPTED")
EventFrame:RegisterEvent("QUEST_REMOVED")
EventFrame:RegisterEvent("QUEST_WATCH_UPDATE")
EventFrame:RegisterEvent("ZONE_CHANGED")
EventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
EventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
-- GET_ITEM_INFO_RECEIVED fires for every item whose info finishes loading from
-- the server. When the panel is open and a quest reward item finally resolves,
-- rebuild so the icon border picks up the now-accurate quality.
EventFrame:RegisterEvent("GET_ITEM_INFO_RECEIVED")
EventFrame:SetScript("OnEvent", function(_, event)
    if not Panel._state.panelOpen then
        if event == "PLAYER_REGEN_ENABLED" and Panel._state.pendingLayout then
            Panel._state.pendingLayout = false
            Panel.Rebuild()
        end
        return
    end
    if type(InCombatLockdown) == "function" then
        local okCombat, inCombat = pcall(InCombatLockdown)
        if okCombat and inCombat then
            Panel._state.pendingLayout = true
            return
        end
    end
    Panel.Rebuild()
end)

-- ============================================================
-- L-key (TOGGLEQUESTLOG) integration
-- The default Quest Log keybinding calls the game's ToggleQuestLog,
-- which opens the World Map and tells QuestMapFrame to show. We
-- suppress QuestMapFrame, so the map opens with no visible quest
-- panel. Hook the toggle (read-only, taint-safe) and mirror the
-- intent into our custom panel:
--   - if the map is shown after the toggle, open our panel
--   - if the map closed, reset our panel state so the next press
--     opens us cleanly. Also hook the World Map's hide so closing
--     it via M / Esc / its own close button keeps panel state in
--     sync.
-- ============================================================
local function HookQuestLogToggleBinding()
    if Panel._questLogToggleHooked then return end

    local function CloseSilently()
        Panel._state.panelOpen = false
        local p = Panel._state.panel
        if p and type(p.Hide) == "function" then
            pcall(function() p:Hide() end)
        end
    end

    local function CloseMap()
        local map = _G.WorldMapFrame
        if not map then return end
        if type(_G.HideUIPanel) == "function" then
            local ok = pcall(_G.HideUIPanel, map)
            if ok then return end
        end
        if type(map.Hide) == "function" then
            pcall(function() map:Hide() end)
        end
    end

    local function IsBlizzardModeActive()
        local s = _G.MidnightUISettings
        return type(s) == "table" and type(s.General) == "table"
               and s.General.useBlizzardQuestingInterface == true
    end

    local hooked = false
    if type(_G.ToggleQuestLog) == "function" then
        local ok = pcall(hooksecurefunc, "ToggleQuestLog", function()
            -- Setting may have been flipped to "use Blizzard" at runtime after this
            -- hook was installed. If so, stay out of the way and make sure our
            -- panel is hidden so the default quest log is the only thing visible.
            if IsBlizzardModeActive() then
                if Panel.IsOpen() then
                    CloseSilently()
                end
                return
            end
            local map = _G.WorldMapFrame
            local mapShown = map and type(map.IsShown) == "function" and map:IsShown()
            if mapShown then
                if Panel.IsOpen() then
                    -- Second press of L while everything's open: close panel + map together.
                    CloseSilently()
                    CloseMap()
                else
                    Panel.Show(map)
                end
            else
                if Panel.IsOpen() then
                    CloseSilently()
                end
            end
        end)
        if ok then hooked = true end
    end

    if _G.WorldMapFrame and type(_G.WorldMapFrame.HookScript) == "function" then
        pcall(_G.WorldMapFrame.HookScript, _G.WorldMapFrame, "OnHide", function()
            if Panel.IsOpen() then
                CloseSilently()
            end
        end)
    end

    Panel._questLogToggleHooked = hooked
end

local LoginFrame = CreateFrame("Frame")
LoginFrame:RegisterEvent("PLAYER_LOGIN")
LoginFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
LoginFrame:SetScript("OnEvent", function(self, event)
    HookQuestLogToggleBinding()
    if Panel._questLogToggleHooked and event == "PLAYER_ENTERING_WORLD" then
        self:UnregisterAllEvents()
    end
end)

Addon.QuestLogPanel = Panel
