GuildHelper = GuildHelper or {}

local addon = GuildHelper

addon.NewUIMythicPlus = addon.NewUIMythicPlus or {}

local page = addon.NewUIMythicPlus
page.State = page.State or {}

local refreshPage
local PANEL_ART_KEY = "MyToonsPanelBackground"
local PANEL_HEADER_KEY = "HomePanelHeader"
local CARD_ART_KEY = "MyToonsCardBackground"
local BUTTON_ART_KEY = "MyToonsButtonBackground"
local BUTTON_ACTIVE_ART_KEY = "MyToonsButtonSelected"
local TAB_GROUP_GAP = 68
local PAGE_CONTENT_Y_SHIFT = 24
local PORTAL_ANNOUNCEMENT_DEDUPE_SECONDS = 2

local function trimString(value)
    return tostring(value or ""):match("^%s*(.-)%s*$") or ""
end

local function ApplyRect(frame, parent, rect)
    frame:ClearAllPoints()
    frame:SetPoint("TOPLEFT", parent, "TOPLEFT", tonumber(rect.x) or 0, tonumber(rect.y) or 0)
    frame:SetSize(tonumber(rect.w) or 1, tonumber(rect.h) or 1)
end

local function shiftedRect(rect, deltaY)
    return {
        x = tonumber(rect and rect.x) or 0,
        y = (tonumber(rect and rect.y) or 0) + (tonumber(deltaY) or 0),
        w = tonumber(rect and rect.w) or 1,
        h = tonumber(rect and rect.h) or 1,
    }
end

local function clearFrame(frame)
    if not frame then
        return
    end

    if frame.Hide then
        frame:Hide()
    end

    if frame.SetParent then
        frame:SetParent(nil)
    end
end

local function getClassColor(classFile)
    local colors = RAID_CLASS_COLORS or CUSTOM_CLASS_COLORS
    local token = string.upper(trimString(classFile or ""))
    local color = colors and colors[token] or nil

    if color then
        return color.r, color.g, color.b, 1
    end

    return 0.96, 0.93, 0.84, 1
end

local function colorizeClassText(text, classFile)
    local r, g, b = getClassColor(classFile)
    return string.format("|cff%02x%02x%02x%s|r", math.floor(r * 255), math.floor(g * 255), math.floor(b * 255), tostring(text or "-"))
end

local function resolveTexturePath(textureKey)
    local textures = addon.NewUIAssets and addon.NewUIAssets.Textures or nil
    return textures and textures[textureKey] or nil
end

local function resolveArtMetrics(textureKey)
    local metrics = addon.NewUIAssets and addon.NewUIAssets.Metrics or nil
    return metrics and metrics[textureKey] or nil
end

local function applyArtTexture(texture, textureKey, fallbackColor)
    local texturePath = resolveTexturePath(textureKey)
    local metrics = resolveArtMetrics(textureKey)
    if texturePath then
        texture:SetTexture(texturePath)
        texture:SetVertexColor(1, 1, 1, 0.98)
        if metrics and type(metrics.texCoord) == "table" and #metrics.texCoord == 4 then
            texture:SetTexCoord(metrics.texCoord[1], metrics.texCoord[3], metrics.texCoord[2], metrics.texCoord[4])
        end
        return
    end
    texture:SetColorTexture(unpack(fallbackColor or { 0.10, 0.10, 0.12, 0.95 }))
end

local function getButtonArtInsets()
    local metrics = resolveArtMetrics(BUTTON_ACTIVE_ART_KEY) or resolveArtMetrics(BUTTON_ART_KEY) or nil
    local assetInsets = addon.NewUIAssets and addon.NewUIAssets.Insets and addon.NewUIAssets.Insets.MyToonsButton or nil
    local insets = metrics and metrics.insets or assetInsets or {}
    return tonumber(insets.left) or 0, tonumber(insets.right) or 0
end

local function getCardValueColor(backgroundTextureKey)
    if backgroundTextureKey == "HomeRaidCardBackground" then
        return 0.96, 0.82, 0.34, 1
    end
    if backgroundTextureKey == "HomeMythicCardBackground" then
        return 0.86, 0.70, 1.00, 1
    end
    if backgroundTextureKey == "HomeMembersCardBackground" then
        return 0.62, 0.96, 0.54, 1
    end
    if backgroundTextureKey == "HomeNewsCardBackground" then
        return 0.48, 0.78, 1.00, 1
    end

    return 0.96, 0.82, 0.34, 1
end

local function getConnectedButtonOverlap()
    local leftInset, rightInset = getButtonArtInsets()
    return math.max(8, math.floor(math.min(leftInset, rightInset) / 2))
end

local function createSurface(parent, rect)
    local frame = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    ApplyRect(frame, parent, rect)
    if frame.SetBackdrop then
        frame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        frame:SetBackdropColor(0.09, 0.10, 0.13, 0.94)
        frame:SetBackdropBorderColor(0.42, 0.33, 0.16, 0.92)
    end
    return frame
end

local function createPanel(parent, rect, titleText, metaText)
    local panel = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    ApplyRect(panel, parent, rect)
    if panel.SetBackdrop then
        panel:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        panel:SetBackdropColor(0, 0, 0, 0)
        panel:SetBackdropBorderColor(0, 0, 0, 0)
    end

    local background = panel:CreateTexture(nil, "BACKGROUND")
    background:SetAllPoints(panel)
    applyArtTexture(background, PANEL_ART_KEY, { 0.09, 0.10, 0.13, 0.94 })

    local headerBand = panel:CreateTexture(nil, "ARTWORK")
    headerBand:SetPoint("TOPLEFT", panel, "TOPLEFT", 24, -14)
    headerBand:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -24, -14)
    headerBand:SetHeight(24)
    applyArtTexture(headerBand, PANEL_HEADER_KEY, { 0.08, 0.08, 0.10, 0.82 })
    headerBand:SetVertexColor(1, 1, 1, 0.98)

    local headerRule = panel:CreateTexture(nil, "ARTWORK")
    headerRule:SetPoint("BOTTOMLEFT", headerBand, "BOTTOMLEFT", 0, 0)
    headerRule:SetPoint("BOTTOMRIGHT", headerBand, "BOTTOMRIGHT", 0, 0)
    headerRule:SetHeight(1)
    headerRule:SetColorTexture(0.58, 0.44, 0.16, 0.72)

    local title = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("CENTER", headerBand, "CENTER", 0, -1)
    title:SetText(titleText or "")
    title:SetTextColor(0.98, 0.92, 0.76, 1)
    title:SetScale(1.02)
    title:SetShadowColor(0, 0, 0, 0.85)
    title:SetShadowOffset(1, -1)

    if trimString(metaText) ~= "" then
        local meta = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        meta:SetPoint("TOPLEFT", panel, "TOPLEFT", 14, -46)
        meta:SetWidth(math.max(1, (tonumber(rect.w) or 1) - 24))
        meta:SetJustifyH("LEFT")
        meta:SetText(metaText)
        meta:SetTextColor(0.76, 0.72, 0.64, 1)
        panel.Meta = meta
    end

    panel.Title = title
    return panel
end

local function createButton(parent, rect, labelText, onClick, isActive)
    local button = CreateFrame("Button", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    ApplyRect(button, parent, rect)
    local background = button:CreateTexture(nil, "BACKGROUND")
    background:SetAllPoints(button)
    applyArtTexture(background, isActive and BUTTON_ACTIVE_ART_KEY or BUTTON_ART_KEY, isActive and { 0.20, 0.23, 0.31, 0.98 } or { 0.10, 0.11, 0.14, 0.95 })
    button.Background = background

    local text = button:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    text:SetPoint("CENTER", button, "CENTER", 0, 0)
    text:SetText(labelText or "")
    text:SetTextColor(isActive and 0.98 or 0.96, isActive and 0.88 or 0.93, isActive and 0.58 or 0.84, 1)
    button.Text = text
    button:SetScript("OnClick", function()
        if onClick then
            onClick(button)
        end
    end)
    return button
end

local function createConnectedButtonGroup(parent, startX, startY, definitions, overlap)
    local nextX = tonumber(startX) or 0
    local resolvedOverlap = tonumber(overlap) or getConnectedButtonOverlap()

    for index, definition in ipairs(definitions or {}) do
        local width = tonumber(definition.w) or 0
        local height = tonumber(definition.h) or 28
        createButton(parent, { x = nextX, y = startY, w = width, h = height }, definition.label, definition.onClick, definition.isActive)
        nextX = nextX + width
        if index < #(definitions or {}) then
            nextX = nextX - resolvedOverlap
        end
    end

    return nextX
end

local function createCard(parent, rect, titleText, valueText, subtitleText, iconTexture, backgroundTextureKey, options)
    local card = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    ApplyRect(card, parent, rect)
    local titleOffsetX = iconTexture and 64 or 18
    local hasTitle = trimString(titleText) ~= ""
    local centered = type(options) == "table" and options.centered == true

    local background = card:CreateTexture(nil, "BACKGROUND")
    background:SetAllPoints(card)
    applyArtTexture(background, backgroundTextureKey or CARD_ART_KEY, { 0.09, 0.10, 0.13, 0.94 })

    local title = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    if centered then
        title:SetPoint("TOP", card, "TOP", 0, -16)
        title:SetWidth(math.max(1, (tonumber(rect.w) or 1) - 24))
        title:SetJustifyH("CENTER")
    else
        title:SetPoint("TOPLEFT", card, "TOPLEFT", titleOffsetX, -18)
    end
    title:SetText(titleText or "")
    title:SetTextColor(0.78, 0.74, 0.66, 1)
    if not hasTitle then
        title:Hide()
    end

    local value = card:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    if centered and hasTitle then
        value:SetPoint("TOP", title, "BOTTOM", 0, -8)
    elseif centered then
        value:SetPoint("TOP", card, "TOP", 0, -26)
    elseif hasTitle then
        value:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8)
    else
        value:SetPoint("TOPLEFT", card, "TOPLEFT", titleOffsetX, -26)
    end
    value:SetWidth(math.max(1, (tonumber(rect.w) or 1) - (iconTexture and 74 or 28)))
    value:SetJustifyH(centered and "CENTER" or "LEFT")
    value:SetWordWrap(false)
    value:SetText(valueText or "-")
    value:SetTextColor(getCardValueColor(backgroundTextureKey or CARD_ART_KEY))

    if trimString(iconTexture) ~= "" then
        local icon = card:CreateTexture(nil, "ARTWORK")
        icon:SetPoint("TOPLEFT", card, "TOPLEFT", 16, -18)
        icon:SetSize(38, 38)
        icon:SetTexture(iconTexture)
        card.Icon = icon
    end

    if trimString(subtitleText) ~= "" then
        local subtitle = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        if centered then
            subtitle:SetPoint("BOTTOM", card, "BOTTOM", 0, 12)
        else
            subtitle:SetPoint("BOTTOMLEFT", card, "BOTTOMLEFT", iconTexture and 64 or 18, 12)
        end
        subtitle:SetWidth(math.max(1, (tonumber(rect.w) or 1) - (iconTexture and 74 or 28)))
        subtitle:SetJustifyH(centered and "CENTER" or "LEFT")
        subtitle:SetText(subtitleText)
        subtitle:SetTextColor(0.72, 0.68, 0.58, 1)
        card.Subtitle = subtitle
    end

    card.Title = title
    card.Value = value

    return card
end

local function createInput(parent, rect, initialText)
    local host = createSurface(parent, rect)
    local input = CreateFrame("EditBox", nil, host, "InputBoxTemplate")
    if input.Left then input.Left:SetAlpha(0) end
    if input.Middle then input.Middle:SetAlpha(0) end
    if input.Right then input.Right:SetAlpha(0) end
    input:SetAutoFocus(false)
    input:SetFontObject("GameFontHighlight")
    input:SetPoint("TOPLEFT", host, "TOPLEFT", 8, -4)
    input:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -8, 4)
    input:SetText(initialText or "")
    host.Input = input
    return host
end

local function enableMouseWheelScroll(scrollFrame, step)
    if not scrollFrame or not scrollFrame.EnableMouseWheel then
        return
    end

    scrollFrame:EnableMouseWheel(true)
    scrollFrame:SetScript("OnMouseWheel", function(self, delta)
        local nextValue = (tonumber(self:GetVerticalScroll()) or 0) - (delta * (tonumber(step) or 24))
        local maxValue = tonumber(self:GetVerticalScrollRange()) or 0
        nextValue = math.max(0, math.min(nextValue, maxValue))
        self:SetVerticalScroll(nextValue)
        if self.ScrollBar and self.ScrollBar.SetValue then
            self.ScrollBar:SetValue(nextValue)
        end
    end)
end

local function hidePopupMenu(frame)
    if frame and frame.Hide then
        frame:Hide()
    end
end

local function ensurePopupMenuFrame()
    if page._popupMenuFrame then
        return page._popupMenuFrame
    end

    local frame = CreateFrame("Frame", nil, UIParent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    frame:SetFrameStrata("TOOLTIP")
    frame:SetToplevel(true)
    frame:SetClampedToScreen(true)
    frame:Hide()
    frame.itemButtons = {}
    if frame.SetBackdrop then
        frame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        frame:SetBackdropColor(0.05, 0.06, 0.08, 0.96)
        frame:SetBackdropBorderColor(0.44, 0.34, 0.16, 0.95)
    end
    page._popupMenuFrame = frame
    return frame
end

local function createSelectButton(parent, rect, textValue, onClick)
    local button = createButton(parent, rect, textValue, onClick, false)
    local arrow = button:CreateTexture(nil, "OVERLAY")
    arrow:SetPoint("RIGHT", button, "RIGHT", -8, 0)
    arrow:SetSize(16, 16)
    arrow:SetTexture("Interface\\ChatFrame\\ChatFrameExpandArrow")
    arrow:SetVertexColor(0.96, 0.82, 0.46, 1)
    button.Arrow = arrow
    return button
end

local function showDropdownMenu(anchorFrame, items)
    local frame = ensurePopupMenuFrame()
    local menuItems = type(items) == "function" and (items() or {}) or (items or {})
    local width = math.max(160, tonumber(anchorFrame and anchorFrame:GetWidth()) or 160)

    for index, option in ipairs(menuItems) do
        local button = frame.itemButtons[index] or CreateFrame("Button", nil, frame, BackdropTemplateMixin and "BackdropTemplate" or nil)
        frame.itemButtons[index] = button
        if button.SetBackdrop then
            button:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
            button:SetBackdropColor(0.10, 0.11, 0.14, 0.92)
            button:SetBackdropBorderColor(0.30, 0.24, 0.12, 0.75)
        end
        if not button.Text then
            button.Text = button:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
            button.Text:SetPoint("LEFT", button, "LEFT", 8, 0)
            button.Text:SetPoint("RIGHT", button, "RIGHT", -8, 0)
            button.Text:SetJustifyH("LEFT")
        end
        button:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, -8 - ((index - 1) * 24))
        button:SetSize(width, 22)
        button.Text:SetText(tostring(option.text or "Select"))
        button:SetScript("OnClick", function()
            hidePopupMenu(frame)
            if type(option.func) == "function" then option.func() end
            if type(option.onClick) == "function" then option.onClick(option.value) end
        end)
        button:Show()
    end

    for index = #menuItems + 1, #(frame.itemButtons or {}) do
        frame.itemButtons[index]:Hide()
    end

    frame:ClearAllPoints()
    frame:SetPoint("TOPLEFT", anchorFrame, "BOTTOMLEFT", 0, -4)
    frame:SetSize(width + 16, (#menuItems * 24) + 16)
    frame:Show()
end

local function tryGetMapTexture(dungeonId)
    if type(C_ChallengeMode) ~= "table" or type(C_ChallengeMode.GetMapUIInfo) ~= "function" then
        return nil
    end

    local ok, _, _, _, texture, backgroundTexture = pcall(C_ChallengeMode.GetMapUIInfo, tonumber(dungeonId))
    return ok and (texture or backgroundTexture) or nil
end

local function trimPortalAnnouncementText(value)
    return trimString(value):gsub("^Portal:%s*", ""):gsub("^[Pp]ortal:%s*", "")
end

local function stripPortalSpellPrefix(value)
    return trimPortalAnnouncementText(trimPortalAnnouncementText(value))
end

local function getPortalAnnouncementChannel()
    if LE_PARTY_CATEGORY_INSTANCE and IsInGroup and IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then
        return "INSTANCE_CHAT"
    end
    if IsInRaid and IsInRaid() then
        return "RAID"
    end
    if IsInGroup and IsInGroup() then
        return "PARTY"
    end
    return nil
end

local function getPortalAnnouncementDungeonName(dungeonRow, portalSpellName)
    local rowName = trimPortalAnnouncementText(dungeonRow and dungeonRow.name or "")
    local dungeonId = trimPortalAnnouncementText(dungeonRow and dungeonRow.id or "")
    if rowName == "" or rowName == dungeonId or rowName == "Unknown Dungeon" then
        rowName = stripPortalSpellPrefix(portalSpellName)
    end
    if rowName == "" then
        rowName = "Dungeon"
    end
    return rowName
end

local function announcePortalCastStarted(dungeonRow, portalSpellName, castGUID, portalSpellId)
    if not SendChatMessage then
        return false
    end

    local channel = getPortalAnnouncementChannel()
    if not channel then
        return false
    end

    local dungeonName = getPortalAnnouncementDungeonName(dungeonRow, portalSpellName)
    local now = GetTime and GetTime() or 0
    local announcementState = page.State.portalAnnouncement or {}
    local announcementKey = tostring(castGUID or "")
    local stableAnnouncementKey = string.format("%s:%s:%s", tostring(portalSpellId or ""), dungeonName, channel)

    if announcementKey == "" then
        announcementKey = stableAnnouncementKey
    end

    if ((announcementState.key == announcementKey) or (announcementState.stableKey == stableAnnouncementKey))
        and ((tonumber(announcementState.at) or 0) + PORTAL_ANNOUNCEMENT_DEDUPE_SECONDS) > now then
        return false
    end

    page.State.portalAnnouncement = {
        at = now,
        key = announcementKey,
        stableKey = stableAnnouncementKey,
    }

    local message = string.format("[GH3][Porting to Dungeon %s]", dungeonName):gsub("|", "||")
    local ok = pcall(SendChatMessage, message, channel)
    return ok and true or false
end

local function createPortalBadge(parent, point, relativeTo, relativePoint, x, y, row, service)
    local isUnlocked = trimString(row and row.portalLabel) == "Yes"
    local portalSpellId = service and service.GetPortalSpellId and service:GetPortalSpellId(row.id, row.name) or nil
    local portalSpellName = service and service.GetPortalSpellName and service:GetPortalSpellName(row.id, row.name) or nil
    local canCastPortal = isUnlocked and tonumber(portalSpellId) ~= nil
    local button = CreateFrame("Button", nil, parent, "BackdropTemplate,SecureActionButtonTemplate")
    button:SetPoint(point, relativeTo, relativePoint, x, y)
    button:SetSize(104, 66)
    button:EnableMouse(true)
    button:RegisterForClicks("AnyUp", "AnyDown")
    if button.SetBackdrop then
        button:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        button:SetBackdropColor(0.08, 0.09, 0.12, 0.96)
        button:SetBackdropBorderColor(0.42, 0.33, 0.16, 0.92)
    end
    if button.SetText then
        button:SetText("")
    end
    if button.GetFontString and button:GetFontString() then
        button:GetFontString():SetText("")
        button:GetFontString():Hide()
    end
    if not (InCombatLockdown and InCombatLockdown()) then
        button:SetAttribute("useOnKeyDown", false)
        if canCastPortal then
            button:SetAttribute("type", "spell")
            button:SetAttribute("spell", portalSpellId)
            button:SetAttribute("type1", "spell")
            button:SetAttribute("spell1", portalSpellId)
        else
            button:SetAttribute("type", nil)
            button:SetAttribute("spell", nil)
            button:SetAttribute("type1", nil)
            button:SetAttribute("spell1", nil)
        end
    end

    local icon = button:CreateTexture(nil, "ARTWORK")
    icon:SetPoint("TOPLEFT", button, "TOPLEFT", 2, -2)
    icon:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -2, 2)
    icon:SetTexture(tryGetMapTexture(row.id))
    if not icon:GetTexture() then
        icon:SetColorTexture(0.18, 0.22, 0.30, 0.95)
    elseif icon.SetDesaturated then
        icon:SetDesaturated(not isUnlocked)
    end

    local title = button:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    title:SetPoint("CENTER", button, "CENTER", 0, -8)
    title:SetWidth(92)
    title:SetJustifyH("CENTER")
    title:SetText(trimString(row.shortName) ~= "" and row.shortName or trimString(row.name))

    local glow = button:CreateTexture(nil, "OVERLAY")
    glow:SetPoint("TOPLEFT", button, "TOPLEFT", 1, -1)
    glow:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -1, 1)
    glow:SetColorTexture(0.18, 0.56, 0.96, 0)

    local function applyPortalVisual(self)
        local isCasting = self._gh3PortalCasting and true or false
        local isPressed = self._gh3PortalPressed and true or false
        local isHovered = self._gh3PortalHovered and true or false

        if self.SetBackdrop then
            if not canCastPortal then
                self:SetBackdropColor(0.06, 0.07, 0.10, 0.94)
                self:SetBackdropBorderColor(0.28, 0.26, 0.18, 0.72)
            elseif isCasting or isPressed then
                self:SetBackdropColor(0.08, 0.16, 0.28, 0.98)
                self:SetBackdropBorderColor(0.28, 0.72, 1.00, 0.98)
            elseif isHovered then
                self:SetBackdropColor(0.10, 0.12, 0.18, 0.98)
                self:SetBackdropBorderColor(0.70, 0.54, 0.18, 0.96)
            else
                self:SetBackdropColor(0.08, 0.09, 0.12, 0.96)
                self:SetBackdropBorderColor(0.42, 0.33, 0.16, 0.92)
            end
        end

        if isCasting then
            glow:SetColorTexture(0.18, 0.56, 0.96, 0.22)
            title:SetTextColor(0.92, 0.97, 1.00, 1)
        elseif isPressed then
            glow:SetColorTexture(0.18, 0.56, 0.96, 0.16)
            title:SetTextColor(0.98, 0.94, 0.78, 1)
        elseif isHovered then
            glow:SetColorTexture(0.96, 0.78, 0.24, 0.10)
            title:SetTextColor(0.98, 0.94, 0.78, 1)
        elseif canCastPortal then
            glow:SetColorTexture(0.18, 0.56, 0.96, 0)
            title:SetTextColor(0.96, 0.93, 0.84, 1)
        else
            glow:SetColorTexture(0.18, 0.56, 0.96, 0)
            title:SetTextColor(0.70, 0.68, 0.62, 1)
        end

        if icon and icon.SetVertexColor then
            if not canCastPortal then
                icon:SetVertexColor(0.72, 0.72, 0.72, 0.88)
            elseif isCasting then
                icon:SetVertexColor(1, 1, 1, 1)
            elseif isHovered or isPressed then
                icon:SetVertexColor(1, 1, 1, 0.98)
            else
                icon:SetVertexColor(1, 1, 1, 0.94)
            end
        end
    end

    button:SetScript("OnEnter", function(self)
        self._gh3PortalHovered = true
        applyPortalVisual(self)
    end)
    button:SetScript("OnLeave", function(self)
        self._gh3PortalHovered = false
        self._gh3PortalPressed = false
        applyPortalVisual(self)
    end)
    button:SetScript("OnMouseDown", function(self)
        if canCastPortal then
            self._gh3PortalPressed = true
            applyPortalVisual(self)
        end
    end)
    button:SetScript("OnMouseUp", function(self)
        self._gh3PortalPressed = false
        applyPortalVisual(self)
    end)

    if canCastPortal then
        button:RegisterUnitEvent("UNIT_SPELLCAST_START", "player")
        button:RegisterEvent("PLAYER_STARTED_MOVING")
        button:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player")
        button:RegisterUnitEvent("UNIT_SPELLCAST_INTERRUPTED", "player")
        button:RegisterUnitEvent("UNIT_SPELLCAST_STOP", "player")
        button:RegisterUnitEvent("UNIT_SPELLCAST_FAILED", "player")
        button:RegisterUnitEvent("UNIT_SPELLCAST_FAILED_QUIET", "player")
        button:SetScript("OnEvent", function(self, event, unitTarget, castGUID, spellId)
            if event == "UNIT_SPELLCAST_START" then
                if tonumber(spellId) ~= nil and tonumber(portalSpellId) == tonumber(spellId) then
                    self._gh3PortalCasting = true
                    self._gh3PortalPressed = false
                    applyPortalVisual(self)
                    if castGUID ~= nil and self._gh3PortalAnnouncedCastGUID ~= castGUID then
                        self._gh3PortalAnnouncedCastGUID = castGUID
                        announcePortalCastStarted(row, portalSpellName, castGUID, portalSpellId)
                    elseif castGUID == nil and not self._gh3PortalAnnouncedCastWithoutGUID then
                        self._gh3PortalAnnouncedCastWithoutGUID = true
                        announcePortalCastStarted(row, portalSpellName, nil, portalSpellId)
                    end
                end
            elseif event == "PLAYER_STARTED_MOVING"
                or event == "UNIT_SPELLCAST_SUCCEEDED"
                or event == "UNIT_SPELLCAST_INTERRUPTED"
                or event == "UNIT_SPELLCAST_STOP"
                or event == "UNIT_SPELLCAST_FAILED"
                or event == "UNIT_SPELLCAST_FAILED_QUIET" then
                self._gh3PortalCasting = false
                self._gh3PortalAnnouncedCastWithoutGUID = false
                applyPortalVisual(self)
            end
        end)
    end

    applyPortalVisual(button)

    button._gh3Cleanup = function(control)
        if control and control.UnregisterAllEvents then
            control:UnregisterAllEvents()
        end
    end

    return button
end

local function renderPageHeader(root, layout, service, parent, snapshot)
    local activePane = service and service.GetActivePane and service:GetActivePane() or "toons"
    local tabs = layout.Tabs or {}
    local mainTabsRight = createConnectedButtonGroup(root, tonumber((tabs.MyToons or {}).x) or 20, (tonumber((tabs.MyToons or {}).y) or -52) + PAGE_CONTENT_Y_SHIFT, {
        { label = "My Toons", w = tonumber((tabs.MyToons or {}).w) or 110, h = tonumber((tabs.MyToons or {}).h) or 28, onClick = function() service:SetActivePane("toons"); refreshPage(parent, snapshot) end, isActive = activePane == "toons" },
        { label = "Party Planning", w = tonumber((tabs.Party or {}).w) or 132, h = tonumber((tabs.Party or {}).h) or 28, onClick = function() service:SetActivePane("party"); refreshPage(parent, snapshot) end, isActive = activePane == "party" },
        { label = "Season", w = tonumber((tabs.Season or {}).w) or 92, h = tonumber((tabs.Season or {}).h) or 28, onClick = function() service:SetActivePane("season"); refreshPage(parent, snapshot) end, isActive = activePane == "season" },
        { label = "Leaderboard", w = tonumber((tabs.Leaderboard or {}).w) or 118, h = tonumber((tabs.Leaderboard or {}).h) or 28, onClick = function() service:SetActivePane("leaderboard"); refreshPage(parent, snapshot) end, isActive = activePane == "leaderboard" },
    }, getConnectedButtonOverlap())

    return mainTabsRight
end

local function renderLeaderboardTable(parent, rows)
    local columns = {
        { key = "#", x = 0, w = 36, align = "CENTER" },
        { key = "Toon", x = 42, w = 188, align = "LEFT" },
        { key = "Spec", x = 238, w = 114, align = "CENTER" },
        { key = "iLvl", x = 360, w = 56, align = "CENTER" },
        { key = "Score", x = 424, w = 74, align = "CENTER" },
        { key = "Current Key", x = 506, w = 226, align = "LEFT" },
        { key = "Guild", x = 740, w = 88, align = "LEFT" },
    }

    local header = CreateFrame("Frame", nil, parent)
    header:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, 0)
    header:SetSize(828, 24)
    local band = header:CreateTexture(nil, "BACKGROUND")
    band:SetAllPoints(header)
    applyArtTexture(band, "HomePanelHeader", { 0.08, 0.08, 0.10, 0.72 })
    band:SetVertexColor(1, 1, 1, 0.98)
    for _, column in ipairs(columns) do
        local text = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        text:SetPoint(column.align == "CENTER" and "CENTER" or "LEFT", header, "LEFT", column.x + (column.align == "CENTER" and math.floor(column.w / 2) or 2), -1)
        text:SetWidth(column.w)
        text:SetJustifyH(column.align)
        text:SetWordWrap(false)
        text:SetText(column.key)
        text:SetTextColor(0.96, 0.82, 0.46, 1)
    end

    local scroll = CreateFrame("ScrollFrame", nil, parent, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, -28)
    scroll:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -24, 0)
    local host = CreateFrame("Frame", nil, scroll)
    host:SetSize(828, math.max(1, #rows * 32))
    scroll:SetScrollChild(host)
    enableMouseWheelScroll(scroll, 26)

    for index, rowData in ipairs(rows) do
        local row = CreateFrame("Button", nil, host, BackdropTemplateMixin and "BackdropTemplate" or nil)
        row:SetPoint("TOPLEFT", host, "TOPLEFT", 0, -((index - 1) * 32))
        row:SetSize(828, 30)
        if row.SetBackdrop then
            row:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
            row:SetBackdropColor(0.14, 0.17, 0.22, rowData.isPlayer and 0.98 or 0.82)
            row:SetBackdropBorderColor(0.34, 0.46, 0.68, rowData.isPlayer and 0.85 or 0.48)
        end
        local toonName, toonRealm = tostring(rowData.fullName or rowData.shortName or "Unknown"):match("^(.-)%-(.+)$")
        toonName = trimString(toonName) ~= "" and toonName or (rowData.shortName or rowData.fullName or "Unknown")
        toonRealm = toonRealm or ""
        local textValues = {
            ["#"] = tostring(index), Spec = trimString(rowData.spec) ~= "" and rowData.spec or "-", iLvl = tostring(math.floor(tonumber(rowData.itemLevel) or 0)),
            Score = tostring(math.floor(tonumber(rowData.overallRating) or 0)), ["Current Key"] = rowData.currentKeyText or "No Key", Guild = trimString(rowData.guildName) ~= "" and rowData.guildName or "-",
        }
        for _, column in ipairs(columns) do
            local text = row:CreateFontString(nil, "OVERLAY", column.key == "Toon" and "GameFontNormal" or "GameFontNormalSmall")
            if column.key == "Toon" then
                text:SetPoint("TOPLEFT", row, "TOPLEFT", column.x + 2, -4)
                text:SetWidth(column.w)
                text:SetJustifyH("LEFT")
                text:SetText(toonName)
                local r, g, b = getClassColor(rowData.classFile)
                text:SetTextColor(r, g, b, 1)
                local realm = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
                realm:SetPoint("TOPLEFT", text, "BOTTOMLEFT", 0, 0)
                realm:SetWidth(column.w)
                realm:SetJustifyH("LEFT")
                realm:SetWordWrap(false)
                realm:SetText(toonRealm)
                realm:SetTextColor(0.72, 0.68, 0.58, 1)
            else
                text:SetPoint(column.align == "CENTER" and "CENTER" or "LEFT", row, "LEFT", column.x + (column.align == "CENTER" and math.floor(column.w / 2) or 2), 0)
                text:SetWidth(column.w)
                text:SetJustifyH(column.align)
                text:SetWordWrap(false)
                text:SetText(textValues[column.key])
                text:SetTextColor(column.key == "Score" and 0.98 or 0.96, column.key == "Score" and 0.82 or 0.93, column.key == "Score" and 0.46 or 0.84, 1)
            end
        end
    end
end

local function renderLeaderboardPane(root, layout, service)
    local visibleGuilds, rows, keyHolders, partyDataCount = {}, {}, 0, 0
    local guildKeys = addon.FederationService and addon.FederationService.GetAllGuildKeys and addon.FederationService:GetAllGuildKeys() or { addon.Store and addon.Store.EnsureHomeGuild and addon.Store:EnsureHomeGuild() or "" }
    for _, guildKey in ipairs(guildKeys) do if trimString(guildKey) ~= "" then visibleGuilds[guildKey] = true end end
    for _, row in ipairs(service:ListLeaderboardRows()) do if visibleGuilds[trimString(row.guildName)] then rows[#rows + 1] = row; if (tonumber(row.currentKeyLevel) or 0) > 0 then keyHolders = keyHolders + 1 end end end
    for _, row in ipairs(service:ListPartyPlannerRows(nil, 10) or {}) do local guildKey = trimString(row.guildName or row.guild or row.homeGuild or ""); if visibleGuilds[guildKey] then partyDataCount = partyDataCount + 1 end end
    local panel = createPanel(root, shiftedRect((layout.Leaderboard and layout.Leaderboard.Panel) or { x = 20, y = -92, w = 918, h = 560 }, PAGE_CONTENT_Y_SHIFT), "Mythic+ Leaderboard", "Home guild and approved linked guild members only.")

    createCard(panel, { x = 55, y = -62, w = 156, h = 88 }, "Tracked Toons", tostring(#rows), "", nil, "HomeRaidCardBackground", { centered = true })
    createCard(panel, { x = 257, y = -62, w = 156, h = 88 }, "Top Score", tostring(rows[1] and math.floor(tonumber(rows[1].overallRating) or 0) or 0), "", nil, "HomeMythicCardBackground", { centered = true })
    createCard(panel, { x = 459, y = -62, w = 156, h = 88 }, "Key Holders", tostring(keyHolders), "", nil, "HomeMembersCardBackground", { centered = true })
    createCard(panel, { x = 661, y = -62, w = 156, h = 88 }, "Party Data", tostring(partyDataCount), "", nil, "HomeNewsCardBackground", { centered = true })

    local tableHost = CreateFrame("Frame", nil, panel)
    tableHost:SetPoint("TOPLEFT", panel, "TOPLEFT", 14, -166)
    tableHost:SetSize(846, 360)
    renderLeaderboardTable(tableHost, rows)
end

local function renderSeasonPane(root, layout, service, parent, snapshot)
    local selectedToonId = service:GetSelectedToonId()
    local toons = service:ListToons()
    local summary = selectedToonId and service:GetSeasonOverviewSummary(selectedToonId) or nil
    local selected = selectedToonId and service:GetToonSummary(selectedToonId) or nil
    local rows = selectedToonId and service:ListSeasonOverviewRows(selectedToonId) or {}
    local headerPanel = createPanel(root, shiftedRect((layout.Season and layout.Season.Summary) or { x = 20, y = -92, w = 918, h = 150 }, PAGE_CONTENT_Y_SHIFT), (summary and summary.seasonName or "Current Season") .. " Overview", selected and (selected.fullName .. " season progress and dungeon coverage.") or "Select a tracked character to review season progress.")

    local selectButton = createSelectButton(headerPanel, { x = 680, y = -14, w = 220, h = 24 }, selected and selected.fullName or "Select Toon", function(anchor)
        showDropdownMenu(anchor, function()
            local items = {}
            for _, toon in ipairs(toons) do
                items[#items + 1] = { text = toon.fullName, func = function() service:SetSelectedToonId(toon.id); refreshPage(parent, snapshot) end }
            end
            return items
        end)
    end)
    selectButton.Text:SetText(selected and selected.fullName or "Select Toon")

    createCard(headerPanel, { x = 59, y = -58, w = 142, h = 88 }, "Rating", summary and tostring(summary.overallRating or 0) or "0", "", nil, "HomeRaidCardBackground", { centered = true })
    createCard(headerPanel, { x = 215, y = -58, w = 142, h = 88 }, "Timed", summary and tostring(summary.timedCount or 0) or "0", "", nil, "HomeMembersCardBackground", { centered = true })
    createCard(headerPanel, { x = 371, y = -58, w = 142, h = 88 }, "Highest Key", summary and ((tonumber(summary.highestKey) or 0) > 0 and ("+" .. tostring(summary.highestKey)) or "-") or "-", "", nil, "HomeMythicCardBackground", { centered = true })
    createCard(headerPanel, { x = 527, y = -58, w = 142, h = 88 }, "Missing", summary and tostring(summary.missingCount or 0) or "0", "", nil, "HomeNewsCardBackground", { centered = true })
    createCard(headerPanel, { x = 683, y = -58, w = 142, h = 88 }, "Current Key", selected and selected.currentKeyShortText or "No Key", "", nil, "HomeCardBackground", { centered = true })

    local tilesPanel = createPanel(root, shiftedRect((layout.Season and layout.Season.Tiles) or { x = 20, y = -254, w = 918, h = 398 }, PAGE_CONTENT_Y_SHIFT), "Season Dungeon Pool", "")
    for index, row in ipairs(rows) do
        local column = (index - 1) % 5
        local gridRow = math.floor((index - 1) / 5)
        local tile = createSurface(tilesPanel, { x = 14 + (column * 174), y = -46 - (gridRow * 118), w = 166, h = 112 })
        local icon = tile:CreateTexture(nil, "ARTWORK")
        icon:SetPoint("TOPLEFT", tile, "TOPLEFT", 10, -12)
        icon:SetSize(40, 40)
        icon:SetTexture(tryGetMapTexture(row.id))
        if not icon:GetTexture() then icon:SetColorTexture(0.16, 0.20, 0.28, 0.95) end

        local name = tile:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        name:SetPoint("TOPLEFT", tile, "TOPLEFT", 56, -12)
        name:SetWidth(100)
        name:SetJustifyH("LEFT")
        name:SetText(trimString(row.shortName) ~= "" and row.shortName or trimString(row.name))

        local best = tile:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        best:SetPoint("TOPLEFT", tile, "TOPLEFT", 10, -58)
        best:SetWidth(146)
        best:SetJustifyH("LEFT")
        best:SetText(string.format("Best %s | %s", row.bestLabel or "-", row.timedLabel or "Missing"))
        best:SetTextColor(0.76, 0.72, 0.64, 1)

        local score = tile:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        score:SetPoint("TOPLEFT", best, "BOTTOMLEFT", 0, -4)
        score:SetWidth(146)
        score:SetJustifyH("LEFT")
        score:SetText(string.format("Score %s | Portal %s", row.scoreLabel or "0", row.portalLabel or "No"))
        score:SetTextColor(0.76, 0.72, 0.64, 1)
    end
end

local function renderToonsPane(root, layout, service, parent, snapshot)
    local selectedToonId = service:GetSelectedToonId()
    local toons = service:ListToons()
    local selected = selectedToonId and service:GetToonSummary(selectedToonId) or nil
    local planner = service:GetPlannerState(selectedToonId)
    local dungeonRows = selectedToonId and service:ListDungeonRows(selectedToonId, planner.targetKeyLevel) or {}
    local dungeonSummary = selectedToonId and service:GetDungeonTableSummary(selectedToonId, planner.targetKeyLevel) or nil
    local goalSummary = selectedToonId and service:GetDungeonTableScoreGoalSummary(selectedToonId, service:GetCarryMilestone()) or nil
    local rowStep = 24
    local listRowWidth = 214
    local buttonInsetLeft, buttonInsetRight = getButtonArtInsets()
    local contentLeftInset = math.max(18, buttonInsetLeft)
    local contentRightInset = math.max(22, buttonInsetRight + 4)

    local listPanel = createPanel(root, shiftedRect((layout.Toons and layout.Toons.List) or { x = 20, y = -92, w = 244, h = 560 }, PAGE_CONTENT_Y_SHIFT), "My Mythic+ Toons", service:IsShowingUnratedAlts() and "Showing all tracked characters." or "Showing rated or tracked characters.")
    local listViewport = CreateFrame("ScrollFrame", nil, listPanel, "UIPanelScrollFrameTemplate")
    listViewport:SetPoint("TOPLEFT", listPanel, "TOPLEFT", 12, -58)
    listViewport:SetPoint("BOTTOMRIGHT", listPanel, "BOTTOMRIGHT", -18, 12)
    local listHost = CreateFrame("Frame", nil, listViewport)
    listHost:SetPoint("TOPLEFT", listViewport, "TOPLEFT", 0, 0)
    listHost:SetSize(listRowWidth, math.max(1, #toons * rowStep))
    listViewport:SetScrollChild(listHost)
    enableMouseWheelScroll(listViewport, rowStep)

    for index, toon in ipairs(toons) do
        local listButton = createButton(listHost, { x = 0, y = -((index - 1) * rowStep), w = listRowWidth, h = 24 }, "", function()
            service:SetSelectedToonId(toon.id)
            refreshPage(parent, snapshot)
        end, toon.id == selectedToonId)
        listButton.Text:SetText("")

        local scoreText = listButton:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        scoreText:SetPoint("RIGHT", listButton, "RIGHT", -contentRightInset, 0)
        scoreText:SetJustifyH("RIGHT")
        scoreText:SetWordWrap(false)
        scoreText:SetText(string.format("|cffd6ab49%d|r", math.floor(tonumber(toon.overallRating) or 0)))

        local pipeText = listButton:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        pipeText:SetPoint("RIGHT", scoreText, "LEFT", -10, 0)
        pipeText:SetJustifyH("CENTER")
        pipeText:SetWordWrap(false)
        pipeText:SetText("||")
        pipeText:SetTextColor(0.90, 0.88, 0.82, 1)

        local nameText = listButton:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        nameText:SetPoint("LEFT", listButton, "LEFT", contentLeftInset, 0)
        nameText:SetPoint("RIGHT", pipeText, "LEFT", -12, 0)
        nameText:SetJustifyH("LEFT")
        nameText:SetWordWrap(false)
        nameText:SetText(colorizeClassText(toon.fullName or "Unknown", toon.classFile))

        listButton.NameText = nameText
        listButton.PipeText = pipeText
        listButton.ScoreText = scoreText
    end

    local summaryPanel = createPanel(root, shiftedRect((layout.Toons and layout.Toons.Summary) or { x = 280, y = -92, w = 656, h = 150 }, PAGE_CONTENT_Y_SHIFT), selected and selected.fullName or "Mythic+ Summary", selected and selected.metaLine or "Select a tracked character to review Mythic+ data.")
    createCard(summaryPanel, { x = 14, y = -58, w = 142, h = 88 }, "Rating", selected and selected.ratingText or "0", "", resolveTexturePath("HomeMembersCardIcon"), "HomeRaidCardBackground")
    createCard(summaryPanel, { x = 170, y = -58, w = 142, h = 88 }, "Current Key", selected and selected.currentKeyShortText or "No Key", "", resolveTexturePath("HomeMythicCardIcon"), "HomeMythicCardBackground")
    createCard(summaryPanel, { x = 326, y = -58, w = 142, h = 88 }, "Portals", selected and tostring(selected.portalCount or 0) or "0", "", resolveTexturePath("HomeMythicCardIcon"), "HomeMembersCardBackground")
    createCard(summaryPanel, { x = 482, y = -58, w = 142, h = 88 }, "Tracked Runs", selected and tostring(selected.trackedDungeons or 0) or "0", "", resolveTexturePath("HomeNewsCardIcon"), "HomeNewsCardBackground")

    local dungeonPanel = createPanel(root, shiftedRect((layout.Toons and layout.Toons.Dungeon) or { x = 280, y = -254, w = 656, h = 398 }, PAGE_CONTENT_Y_SHIFT), "Season Dungeons", selectedToonId and service:GetDungeonCatalogText(selectedToonId) or "")

    local totalsLine = dungeonPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    totalsLine:SetPoint("TOPLEFT", dungeonPanel, "TOPLEFT", 14, -54)
    totalsLine:SetWidth(620)
    totalsLine:SetJustifyH("LEFT")
    totalsLine:SetText(dungeonSummary and string.format("Current total %d | Timing every shown dungeon at %d adds +%d for %d total.", tonumber(dungeonSummary.currentTotal) or 0, tonumber(dungeonSummary.targetLevel) or 10, tonumber(dungeonSummary.gainTotal) or 0, tonumber(dungeonSummary.projectedTotal) or 0) or "")
    totalsLine:SetTextColor(0.76, 0.72, 0.64, 1)

    local goalLine = dungeonPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    goalLine:SetPoint("TOPLEFT", totalsLine, "BOTTOMLEFT", 0, -4)
    goalLine:SetWidth(620)
    goalLine:SetJustifyH("LEFT")
    goalLine:SetText(goalSummary and goalSummary.summaryLine or "")
    goalLine:SetTextColor(0.76, 0.72, 0.64, 1)

    local tableHost = CreateFrame("Frame", nil, dungeonPanel)
    tableHost:SetPoint("TOPLEFT", dungeonPanel, "TOPLEFT", 14, -104)
    tableHost:SetSize(610, 260)

    local tableRows = {}
    for _, row in ipairs(dungeonRows) do
        tableRows[#tableRows + 1] = {
            values = { row.name, row.bestLabel, row.scoreLabel, row.gainLabel, row.projectedLabel or row.scoreLabel, row.portalLabel },
        }
    end
    if dungeonSummary then
        tableRows[#tableRows + 1] = {
            values = { "Total", goalSummary and goalSummary.recommendedKeyLevel and string.format("Need %ds", tonumber(goalSummary.recommendedKeyLevel) or 0) or "-", tostring(tonumber(dungeonSummary.currentTotal) or 0), string.format("+%d", tonumber(dungeonSummary.gainTotal) or 0), tostring(tonumber(dungeonSummary.projectedTotal) or 0), "-" },
            selected = true,
        }
    end

    addon.TableView:Render(tableHost, {
        headers = { "Dungeon", "Best", "Score", string.format("Gain %d", tonumber(planner.targetKeyLevel) or 10), "After", "Portal" },
        rows = tableRows,
        headerHeight = 26,
        rowHeight = 24,
        columnWidths = { 206, 82, 72, 72, 72, 96 },
    })
end

local function renderPartyPane(root, layout, service, parent, snapshot)
    local plannerToonId = service:GetSelectedToonId() or (addon.Store and addon.Store.GetPlayerName and addon.Store:GetPlayerName()) or nil
    local planner = service:GetPlannerState(plannerToonId)
    local seasonRows = plannerToonId and service:ListSeasonOverviewRows(plannerToonId) or {}
    local partySummary = service:GetPartyPlannerSummary(planner.selectedDungeonId, planner.targetKeyLevel)
    local partyKeyRows = service:ListPartyKeystoneRows(planner.selectedDungeonId, planner.targetKeyLevel)
    local partyRows = service:ListPartyPlannerRows(planner.selectedDungeonId, planner.targetKeyLevel)
    local analysisMode = service:GetAnalysisMode()
    local partyGuide = service:GetPartyKeyLevelGuide()
    local carryMilestone = service:GetCarryMilestone()
    local carryKeyGoal = service:GetCarryKeyGoal()
    local dungeonOptions = planner.options or {}
    local partyRect = shiftedRect((layout.Party and layout.Party.Main) or { x = 20, y = -92, w = 788, h = 578 }, PAGE_CONTENT_Y_SHIFT)
    local railRect = shiftedRect((layout.Party and layout.Party.Rail) or { x = 824, y = -92, w = 132, h = 578 }, PAGE_CONTENT_Y_SHIFT)
    local partyTableHeight = 182
    local toolbarY = -45
    local goalY = -75
    local cardY = -96

    partyRect.y = (tonumber(partyRect.y) or -68) + 15
    railRect.y = (tonumber(railRect.y) or -68) + 15
    partyRect.h = (tonumber(partyRect.h) or 578) + 84
    railRect.h = (tonumber(railRect.h) or 578) + 44

    local partyPanel = createPanel(root, partyRect, "Party Planning", "")
    local railPanel = createPanel(root, railRect, "Portals", "")

    local dungeonSelect = createSelectButton(partyPanel, { x = 14, y = toolbarY, w = 232, h = 24 }, planner.selectedDungeonName or "Select Dungeon", function(anchor)
        showDropdownMenu(anchor, function()
            local items = {}
            for _, option in ipairs(dungeonOptions) do
                items[#items + 1] = { text = option.name, func = function() service:SetSelectedPlannerDungeonId(option.id); refreshPage(parent, snapshot) end }
            end
            return items
        end)
    end)
    dungeonSelect.Text:SetText(planner.selectedDungeonName or "Select Dungeon")

    local keyInput = createInput(partyPanel, { x = 260, y = toolbarY, w = 78, h = 24 }, tostring(planner.targetKeyLevel or 10))
    createButton(partyPanel, { x = 346, y = toolbarY, w = 76, h = 24 }, "Apply", function()
        service:SetPlannerTargetLevel(keyInput.Input:GetText())
        refreshPage(parent, snapshot)
    end, false)

    createConnectedButtonGroup(partyPanel, 430, toolbarY, {
        { label = "Party Gains", w = 98, h = 22, onClick = function() service:SetAnalysisMode("party-gains"); refreshPage(parent, snapshot) end, isActive = analysisMode == "party-gains" },
        { label = "Score Goal", w = 98, h = 22, onClick = function() service:SetAnalysisMode("score-goal"); refreshPage(parent, snapshot) end, isActive = analysisMode == "score-goal" },
        { label = "Portal Goal", w = 98, h = 22, onClick = function() service:SetAnalysisMode("key-goal"); refreshPage(parent, snapshot) end, isActive = analysisMode == "key-goal" },
    }, getConnectedButtonOverlap())

    local goalInput = createInput(partyPanel, { x = 626, y = goalY, w = 72, h = 22 }, tostring(analysisMode == "key-goal" and carryKeyGoal or carryMilestone))
    createButton(partyPanel, { x = 706, y = goalY, w = 54, h = 22 }, "Set", function()
        if analysisMode == "key-goal" then service:SetCarryKeyGoal(goalInput.Input:GetText()) else service:SetCarryMilestone(goalInput.Input:GetText()) end
        refreshPage(parent, snapshot)
    end, false)

    createCard(partyPanel, { x = 14, y = cardY, w = 252, h = 76 }, "Party Size", tostring(#partyRows), "", nil, "HomeRaidCardBackground")
    createCard(partyPanel, { x = 266, y = cardY, w = 252, h = 76 }, "Target", planner.selectedDungeonName and ((planner.selectedDungeonShortName or planner.selectedDungeonName) .. " +" .. tostring(planner.targetKeyLevel or 10)) or "Pick a dungeon", "", nil, "HomeMythicCardBackground")
    local partyCapCard = createCard(partyPanel, { x = 518, y = cardY, w = 252, h = 76 }, "", partyGuide and string.format("Party cap about +%d", tonumber(partyGuide.recommendedLevel) or 10) or "Unknown", partyGuide and partyGuide.summaryLine or "", nil, "HomeNewsCardBackground")
    if partyCapCard.Value then
        partyCapCard.Value:ClearAllPoints()
        partyCapCard.Value:SetPoint("TOPLEFT", partyCapCard, "TOPLEFT", 18, -11)
    end

    local summaryPanel = createPanel(partyPanel, { x = 14, y = -182, w = 760, h = 236 }, "Party Summary", partySummary)
    local summaryTableHost = CreateFrame("Frame", nil, summaryPanel)
    summaryTableHost:SetPoint("TOPLEFT", summaryPanel, "TOPLEFT", 12, -60)
    summaryTableHost:SetSize(736, partyTableHeight)

    local summaryRows = {}
    for _, row in ipairs(partyKeyRows) do
        summaryRows[#summaryRows + 1] = {
            values = {
                colorizeClassText(row.name, row.classFile),
                row.roleLabel or "-",
                (tonumber(row.itemLevel) or 0) > 0 and tostring(math.floor(tonumber(row.itemLevel) or 0)) or "-",
                string.format("%s\nGain %s", row.key or "No Key", row.selectedGainLabel or "-"),
                row.state or "Tracked",
            },
            selected = row.selected and true or false,
            tooltipTitle = row.name,
            tooltipLines = row.guildInviteEligible and {
                "Shift-click to send a guild invite.",
                "Normal click loads this party member's key and focuses them for gain planning.",
            } or {
                "Click to focus this player for gain planning.",
                "Shift-click attempts a guild invite and reports why it cannot be sent.",
            },
            onClick = function()
                if IsShiftKeyDown and IsShiftKeyDown() then
                    local ok, message = service.SendGuildInviteToPartyMember and service:SendGuildInviteToPartyMember(row.fullName)
                    addon:Print(message or (ok and "Guild invite sent." or "Unable to send guild invite."))
                    return
                end

                service:SetSelectedCarryTargetId(row.fullName)
                if row.dungeonId and (tonumber(row.keyLevel) or 0) > 0 then
                    service:SetSelectedPlannerDungeonId(row.dungeonId)
                    service:SetPlannerTargetLevel(row.keyLevel)
                end
                refreshPage(parent, snapshot)
            end,
        }
    end
    addon.TableView:Render(summaryTableHost, { headers = { "Player", "Role", "iLvl", "Keystone", "State" }, rows = summaryRows, headerHeight = 22, rowHeight = 32, columnWidths = { 170, 76, 64, 248, 132 }, wordWrap = true })

    local membersPanel = createPanel(partyPanel, { x = 14, y = -422, w = 760, h = 236 }, "Member Breakdown", "")
    local membersTableHost = CreateFrame("Frame", nil, membersPanel)
    membersTableHost:SetPoint("TOPLEFT", membersPanel, "TOPLEFT", 12, -42)
    membersTableHost:SetSize(736, partyTableHeight)
    local matrixHeaders = { "Player", "Score" }
    local matrixWidths = { 132, 72 }
    for _, dungeonRow in ipairs(seasonRows) do matrixHeaders[#matrixHeaders + 1] = trimString(dungeonRow.shortName) ~= "" and dungeonRow.shortName or trimString(dungeonRow.name) end
    for _ = 1, #seasonRows do matrixWidths[#matrixWidths + 1] = math.max(58, math.floor((736 - 204) / math.max(1, #seasonRows))) end

    local memberRows = {}
    for _, row in ipairs(partyRows) do
        local values = { colorizeClassText(row.name, row.classFile), row.rating or "0" }
        for _, dungeonRow in ipairs(seasonRows) do
            values[#values + 1] = service:GetPartyBreakdownCellLabel(row.fullName, dungeonRow.id, analysisMode, planner.targetKeyLevel, carryMilestone, carryKeyGoal)
        end
        memberRows[#memberRows + 1] = { values = values, onClick = function() service:SetSelectedCarryTargetId(row.fullName); refreshPage(parent, snapshot) end }
    end
    addon.TableView:Render(membersTableHost, { headers = matrixHeaders, rows = memberRows, headerHeight = 22, rowHeight = 32, columnWidths = matrixWidths, wordWrap = true })

    local anchor = railPanel
    for index, row in ipairs(seasonRows) do
        local offsetY = -54 - ((index - 1) * 70)
        anchor = createPortalBadge(railPanel, "TOPLEFT", railPanel, "TOPLEFT", 12, offsetY, row, service)
        page._cleanupControls[#page._cleanupControls + 1] = anchor
    end
end

refreshPage = function(parent, snapshot)
    for _, control in ipairs(page._cleanupControls or {}) do
        if control and control._gh3Cleanup then
            control:_gh3Cleanup()
        end
    end
    page._cleanupControls = {}
    if page._rootFrame then
        clearFrame(page._rootFrame)
        page._rootFrame = nil
    end

    return page:Render(parent, snapshot)
end

function page:Render(parent, snapshot)
    if not parent then
        return nil
    end

    local service = addon.MythicPlusService
    local layout = addon.NewUILayout and addon.NewUILayout.MythicPlus or { Page = { x = 0, y = 0, w = 1380, h = 742 } }
    local root = CreateFrame("Frame", nil, parent)
    ApplyRect(root, parent, layout.Page or { x = 0, y = 0, w = tonumber(parent:GetWidth()) or 1380, h = tonumber(parent:GetHeight()) or 742 })
    page._rootFrame = root
    page._cleanupControls = {}

    if not service then
        local unavailable = createPanel(root, { x = 20, y = -92, w = 918, h = 180 }, "Mythic+ Disabled", "The Mythic+ service is not loaded right now, so this view is temporarily unavailable.")
        return unavailable
    end

    service:AutoRefreshLiveData()
    local mainTabsRight = renderPageHeader(root, layout, service, parent, snapshot)

    local activePane = service:GetActivePane()
    local pageControlX = (tonumber(mainTabsRight) or 400) + TAB_GROUP_GAP
    if activePane == "party" then
        createConnectedButtonGroup(root, pageControlX, -52 + PAGE_CONTENT_Y_SHIFT, {
            { label = service:IsOverlayEnabled() and "HUD On" or "HUD Off", w = 82, h = 24, onClick = function() service:ToggleOverlayEnabled(); refreshPage(parent, snapshot) end, isActive = false },
        }, getConnectedButtonOverlap())
    elseif activePane == "toons" or activePane == "season" then
        createConnectedButtonGroup(root, pageControlX, -52 + PAGE_CONTENT_Y_SHIFT, {
            { label = service:IsShowingUnratedAlts() and "Hide Unrated" or "Show Unrated", w = 112, h = 24, onClick = function() service:ToggleShowUnratedAlts(); refreshPage(parent, snapshot) end, isActive = false },
            { label = "Use Current Character", w = 144, h = 24, onClick = function() service:SetSelectedToonId(addon.Store:GetPlayerName()); refreshPage(parent, snapshot) end, isActive = false },
        }, getConnectedButtonOverlap())
    end

    if activePane == "leaderboard" then
        renderLeaderboardPane(root, layout, service)
    elseif activePane == "season" then
        renderSeasonPane(root, layout, service, parent, snapshot)
    elseif activePane == "party" then
        renderPartyPane(root, layout, service, parent, snapshot)
    else
        renderToonsPane(root, layout, service, parent, snapshot)
    end

    return root
end

return page