local addonName, addonTable = ...
---@type MK
local MK = addonTable.MK
local core = addonTable
local TRANSLATE = core.TRANSLATE or function(msg) return msg end

core.MeetingStoneLayout = core.MeetingStoneLayout or {}
local MeetingStoneLayout = core.MeetingStoneLayout
local MeetingStoneConstants = core.MeetingStoneConstants or {}
local MEETINGSTONE_PANEL_INSET_LEFT = MeetingStoneConstants.MEETINGSTONE_PANEL_INSET_LEFT
local MEETINGSTONE_PANEL_INSET_TOP = MeetingStoneConstants.MEETINGSTONE_PANEL_INSET_TOP
local MEETINGSTONE_PANEL_INSET_RIGHT = MeetingStoneConstants.MEETINGSTONE_PANEL_INSET_RIGHT
local MEETINGSTONE_PANEL_INSET_BOTTOM = MeetingStoneConstants.MEETINGSTONE_PANEL_INSET_BOTTOM
local MEETINGSTONE_LEFT_PANEL_WIDTH = MeetingStoneConstants.MEETINGSTONE_LEFT_PANEL_WIDTH
local MEETINGSTONE_RIGHT_PANEL_OVERLAP = MeetingStoneConstants.MEETINGSTONE_RIGHT_PANEL_OVERLAP
local MEETINGSTONE_LEFT_PANEL_BACKGROUND_ALPHA = MeetingStoneConstants.MEETINGSTONE_LEFT_PANEL_BACKGROUND_ALPHA
local MEETINGSTONE_PANEL_DIVIDER_WIDTH = MeetingStoneConstants.MEETINGSTONE_PANEL_DIVIDER_WIDTH
local MEETINGSTONE_PANEL_DIVIDER_OFFSET_X = MeetingStoneConstants.MEETINGSTONE_PANEL_DIVIDER_OFFSET_X
local MEETINGSTONE_PANEL_DIVIDER_TOP_OFFSET = MeetingStoneConstants.MEETINGSTONE_PANEL_DIVIDER_TOP_OFFSET
local MEETINGSTONE_PANEL_DIVIDER_BOTTOM_OFFSET = MeetingStoneConstants.MEETINGSTONE_PANEL_DIVIDER_BOTTOM_OFFSET
local MEETINGSTONE_PANEL_DIVIDER_COLOR = MeetingStoneConstants.MEETINGSTONE_PANEL_DIVIDER_COLOR
local MEETINGSTONE_PANEL_DIVIDER_HIGHLIGHT_COLOR = MeetingStoneConstants.MEETINGSTONE_PANEL_DIVIDER_HIGHLIGHT_COLOR
local MEETINGSTONE_PANEL_DIVIDER_SHADOW_COLOR = MeetingStoneConstants.MEETINGSTONE_PANEL_DIVIDER_SHADOW_COLOR
local MEETINGSTONE_HEADER_TOP_OFFSET = MeetingStoneConstants.MEETINGSTONE_HEADER_TOP_OFFSET
local MEETINGSTONE_HEADER_BOTTOM_OFFSET = MeetingStoneConstants.MEETINGSTONE_HEADER_BOTTOM_OFFSET
local MEETINGSTONE_HEADER_CONTENT_INSET_X = MeetingStoneConstants.MEETINGSTONE_HEADER_CONTENT_INSET_X
local MEETINGSTONE_HEADER_CONTENT_OFFSET_Y = MeetingStoneConstants.MEETINGSTONE_HEADER_CONTENT_OFFSET_Y
local MEETINGSTONE_HEADER_TEXT_SIZE = MeetingStoneConstants.MEETINGSTONE_HEADER_TEXT_SIZE
local MEETINGSTONE_FORM_INSET_X = MeetingStoneConstants.MEETINGSTONE_FORM_INSET_X
local MEETINGSTONE_FORM_TOP_OFFSET = MeetingStoneConstants.MEETINGSTONE_FORM_TOP_OFFSET
local MEETINGSTONE_FORM_LABEL_HEIGHT = MeetingStoneConstants.MEETINGSTONE_FORM_LABEL_HEIGHT
local MEETINGSTONE_FORM_LABEL_TO_CONTROL_GAP = MeetingStoneConstants.MEETINGSTONE_FORM_LABEL_TO_CONTROL_GAP
local MEETINGSTONE_FORM_LABEL_TEXT_SIZE = MeetingStoneConstants.MEETINGSTONE_FORM_LABEL_TEXT_SIZE
local MEETINGSTONE_FORM_CONTROL_WIDTH = MeetingStoneConstants.MEETINGSTONE_FORM_CONTROL_WIDTH
local MEETINGSTONE_FORM_CONTROL_HEIGHT = MeetingStoneConstants.MEETINGSTONE_FORM_CONTROL_HEIGHT
local MEETINGSTONE_FORM_SUMMARY_HEIGHT = MeetingStoneConstants.MEETINGSTONE_FORM_SUMMARY_HEIGHT
local MEETINGSTONE_FORM_ROW_GAP = MeetingStoneConstants.MEETINGSTONE_FORM_ROW_GAP
local MEETINGSTONE_FORM_INLINE_LABEL_WIDTH = MeetingStoneConstants.MEETINGSTONE_FORM_INLINE_LABEL_WIDTH
local MEETINGSTONE_FORM_INLINE_INPUT_WIDTH = MeetingStoneConstants.MEETINGSTONE_FORM_INLINE_INPUT_WIDTH
local MEETINGSTONE_FORM_BUTTON_WIDTH = MeetingStoneConstants.MEETINGSTONE_FORM_BUTTON_WIDTH
local MEETINGSTONE_FORM_BUTTON_HEIGHT = MeetingStoneConstants.MEETINGSTONE_FORM_BUTTON_HEIGHT
local MEETINGSTONE_FORM_BUTTON_GAP = MeetingStoneConstants.MEETINGSTONE_FORM_BUTTON_GAP
local MEETINGSTONE_FORM_BUTTON_BOTTOM_OFFSET = MeetingStoneConstants.MEETINGSTONE_FORM_BUTTON_BOTTOM_OFFSET
local MEETINGSTONE_ACTION_BUTTON_TEXT_OFFSET_Y = MeetingStoneConstants.MEETINGSTONE_ACTION_BUTTON_TEXT_OFFSET_Y
local MEETINGSTONE_FORM_SUMMARY_NATIVE_OFFSET_LEFT = MeetingStoneConstants.MEETINGSTONE_FORM_SUMMARY_NATIVE_OFFSET_LEFT
local MEETINGSTONE_FORM_SUMMARY_NATIVE_OFFSET_RIGHT = MeetingStoneConstants.MEETINGSTONE_FORM_SUMMARY_NATIVE_OFFSET_RIGHT
local MEETINGSTONE_APPLICANT_REFRESH_BUTTON_WIDTH = MeetingStoneConstants.MEETINGSTONE_APPLICANT_REFRESH_BUTTON_WIDTH
local MEETINGSTONE_APPLICANT_REFRESH_BUTTON_HEIGHT = MeetingStoneConstants.MEETINGSTONE_APPLICANT_REFRESH_BUTTON_HEIGHT
local MEETINGSTONE_LEFT_BOTTOM_GRADIENT_TOP_OFFSET = MeetingStoneConstants.MEETINGSTONE_LEFT_BOTTOM_GRADIENT_TOP_OFFSET
local MEETINGSTONE_TOP_RIGHT_FILIGREE_OFFSET_X = MeetingStoneConstants.MEETINGSTONE_TOP_RIGHT_FILIGREE_OFFSET_X
local MEETINGSTONE_TOP_RIGHT_FILIGREE_OFFSET_Y = MeetingStoneConstants.MEETINGSTONE_TOP_RIGHT_FILIGREE_OFFSET_Y
local MEETINGSTONE_BOTTOM_RIGHT_FILIGREE_OFFSET_X = MeetingStoneConstants.MEETINGSTONE_BOTTOM_RIGHT_FILIGREE_OFFSET_X
local MEETINGSTONE_BOTTOM_RIGHT_FILIGREE_OFFSET_Y = MeetingStoneConstants.MEETINGSTONE_BOTTOM_RIGHT_FILIGREE_OFFSET_Y
local MEETINGSTONE_MISSING_ADDON_MESSAGE_KEY = MeetingStoneConstants.MEETINGSTONE_MISSING_ADDON_MESSAGE_KEY
local MEETINGSTONE_PANEL_CENTER_ACCENT_WIDTH = 1
local MEETINGSTONE_PANEL_CENTER_ACCENT_COLOR = { 1, 0.82, 0 }
local MEETINGSTONE_READ_ONLY_SECTION_INSET_X = 10
local MEETINGSTONE_READ_ONLY_SECTION_WIDTH = MEETINGSTONE_LEFT_PANEL_WIDTH - (MEETINGSTONE_READ_ONLY_SECTION_INSET_X * 2)
local MEETINGSTONE_READ_ONLY_SECTION_GAP = 8
local MEETINGSTONE_READ_ONLY_BASIC_HEIGHT = 80
local MEETINGSTONE_READ_ONLY_NAME_HEIGHT = 80
local MEETINGSTONE_READ_ONLY_CONFIG_HEIGHT = 80
local MEETINGSTONE_READ_ONLY_REQUIREMENT_HEIGHT = 80
local MEETINGSTONE_READ_ONLY_TEXT_INSET_X = 6
local MEETINGSTONE_READ_ONLY_TEXT_TOP_OFFSET = -3
local MEETINGSTONE_READ_ONLY_LINE_HEIGHT = 16
local MEETINGSTONE_READ_ONLY_CONTENT_GAP = 7
local MEETINGSTONE_READ_ONLY_DUNGEON_CONTENT_HEIGHT = MEETINGSTONE_READ_ONLY_LINE_HEIGHT * 2
local MEETINGSTONE_READ_ONLY_NAME_CONTENT_HEIGHT = MEETINGSTONE_READ_ONLY_LINE_HEIGHT * 2
local MEETINGSTONE_READ_ONLY_CONFIG_ICON_GAP = 7
local MEETINGSTONE_READ_ONLY_REQUIREMENT_FIRST_ROW_GAP = 8
local MEETINGSTONE_READ_ONLY_REQUIREMENT_ROW_GAP = 3
local MEETINGSTONE_READ_ONLY_TITLE_TEXT_SIZE = 13
local MEETINGSTONE_READ_ONLY_CONTENT_TEXT_SIZE = 12
local MEETINGSTONE_READ_ONLY_TITLE_COLOR = { 1, 0.82, 0, 1 }
local MEETINGSTONE_READ_ONLY_CONTENT_COLOR = { 1, 1, 1, 1 }
local MEETINGSTONE_READ_ONLY_ROLE_ICON_SIZE = 18
local MEETINGSTONE_READ_ONLY_ROLE_ICON_GAP = 6
local MEETINGSTONE_READ_ONLY_ROLE_SLOT_TOP_OFFSET = -31
local MEETINGSTONE_READ_ONLY_EMPTY_ROLE_ATLAS = "groupfinder-icon-emptyslot"
local MEETINGSTONE_READ_ONLY_DIVIDER_WIDTH = MEETINGSTONE_LEFT_PANEL_WIDTH - 28
local MEETINGSTONE_READ_ONLY_DIVIDER_HEIGHT = 1.5
local MEETINGSTONE_READ_ONLY_DIVIDER_COLOR = { 126 / 255, 112 / 255, 82 / 255 }
local MEETINGSTONE_READ_ONLY_DIVIDER_ALPHA = 0.86
local MEETINGSTONE_READ_ONLY_DIVIDER_FADE_WIDTH = 28
local MEETINGSTONE_DISABLED_PLACEHOLDER_TEXT_COLOR = { 0.42, 0.42, 0.42, 0.95 }
local MEETINGSTONE_DISABLED_PLACEHOLDER_TEXT_SIZE = MEETINGSTONE_FORM_LABEL_TEXT_SIZE
local MEETINGSTONE_DISABLED_PLACEHOLDER_MASK_ALPHA = 1
local MEETINGSTONE_DISABLED_PLACEHOLDER_TITLE_INSET_X = 10
local MEETINGSTONE_DISABLED_PLACEHOLDER_TITLE_MASK_INSET_X = 7
local MEETINGSTONE_DISABLED_PLACEHOLDER_TITLE_MASK_INSET_Y = 4
local MEETINGSTONE_DISABLED_PLACEHOLDER_SUMMARY_INSET_X = 10
local MEETINGSTONE_DISABLED_PLACEHOLDER_SUMMARY_INSET_TOP = 5
local MEETINGSTONE_DISABLED_PLACEHOLDER_SUMMARY_MASK_INSET_X = 1
local MEETINGSTONE_DISABLED_PLACEHOLDER_SUMMARY_MASK_INSET_Y = 1
local MEETINGSTONE_APPLICANT_SCROLLBAR_WIDTH = 17
local MEETINGSTONE_APPLICANT_SCROLLBAR_GAP = 2
local MEETINGSTONE_APPLICANT_SCROLLBAR_RIGHT_INSET = 10
local MEETINGSTONE_APPLICANT_SCROLL_BOTTOM_INSET = 2
local MeetingStoneForm = core.MeetingStoneForm or {}
local MeetingStoneListing = core.MeetingStoneListing or {}

local TrySetMeetingStoneAtlas = MeetingStoneForm.TrySetAtlas
local CreateMeetingStoneLabel = MeetingStoneForm.CreateLabel
local AddMeetingStoneTitleHolderBorder = MeetingStoneForm.AddTitleHolderBorder
local CreateMeetingStoneInput = MeetingStoneForm.CreateInput
local EnableMeetingStoneSelectAllAfterMouseLeave = MeetingStoneForm.EnableSelectAllAfterMouseLeave
local SetMeetingStoneInputPlaceholder = MeetingStoneForm.SetInputPlaceholder or function() end
local CreateMeetingStoneSummaryBox = MeetingStoneForm.CreateSummaryBox
local CreateMeetingStoneSystemButton = MeetingStoneForm.CreateSystemButton
local CreateMeetingStoneDungeonDropdown = MeetingStoneForm.CreateDungeonDropdown
local CreateMeetingStonePlayStyleDropdown = MeetingStoneForm.CreatePlayStyleDropdown
local CanManageMeetingStoneListing = MeetingStoneListing.CanManageListing

local MeetingStoneLayoutConfig = {
    MEETINGSTONE_PANEL_INSET_LEFT = MEETINGSTONE_PANEL_INSET_LEFT,
    MEETINGSTONE_PANEL_INSET_TOP = MEETINGSTONE_PANEL_INSET_TOP,
    MEETINGSTONE_PANEL_INSET_RIGHT = MEETINGSTONE_PANEL_INSET_RIGHT,
    MEETINGSTONE_PANEL_INSET_BOTTOM = MEETINGSTONE_PANEL_INSET_BOTTOM,
    MEETINGSTONE_LEFT_PANEL_WIDTH = MEETINGSTONE_LEFT_PANEL_WIDTH,
    MEETINGSTONE_RIGHT_PANEL_OVERLAP = MEETINGSTONE_RIGHT_PANEL_OVERLAP,
    MEETINGSTONE_LEFT_PANEL_BACKGROUND_ALPHA = MEETINGSTONE_LEFT_PANEL_BACKGROUND_ALPHA,
    MEETINGSTONE_PANEL_DIVIDER_WIDTH = MEETINGSTONE_PANEL_DIVIDER_WIDTH,
    MEETINGSTONE_PANEL_DIVIDER_OFFSET_X = MEETINGSTONE_PANEL_DIVIDER_OFFSET_X,
    MEETINGSTONE_PANEL_DIVIDER_TOP_OFFSET = MEETINGSTONE_PANEL_DIVIDER_TOP_OFFSET,
    MEETINGSTONE_PANEL_DIVIDER_BOTTOM_OFFSET = MEETINGSTONE_PANEL_DIVIDER_BOTTOM_OFFSET,
    MEETINGSTONE_PANEL_DIVIDER_COLOR = MEETINGSTONE_PANEL_DIVIDER_COLOR,
    MEETINGSTONE_PANEL_DIVIDER_HIGHLIGHT_COLOR = MEETINGSTONE_PANEL_DIVIDER_HIGHLIGHT_COLOR,
    MEETINGSTONE_PANEL_DIVIDER_SHADOW_COLOR = MEETINGSTONE_PANEL_DIVIDER_SHADOW_COLOR,
    MEETINGSTONE_HEADER_TOP_OFFSET = MEETINGSTONE_HEADER_TOP_OFFSET,
    MEETINGSTONE_HEADER_BOTTOM_OFFSET = MEETINGSTONE_HEADER_BOTTOM_OFFSET,
    MEETINGSTONE_HEADER_CONTENT_INSET_X = MEETINGSTONE_HEADER_CONTENT_INSET_X,
    MEETINGSTONE_HEADER_CONTENT_OFFSET_Y = MEETINGSTONE_HEADER_CONTENT_OFFSET_Y,
    MEETINGSTONE_HEADER_TEXT_SIZE = MEETINGSTONE_HEADER_TEXT_SIZE,
    MEETINGSTONE_FORM_INSET_X = MEETINGSTONE_FORM_INSET_X,
    MEETINGSTONE_FORM_TOP_OFFSET = MEETINGSTONE_FORM_TOP_OFFSET,
    MEETINGSTONE_FORM_LABEL_HEIGHT = MEETINGSTONE_FORM_LABEL_HEIGHT,
    MEETINGSTONE_FORM_LABEL_TO_CONTROL_GAP = MEETINGSTONE_FORM_LABEL_TO_CONTROL_GAP,
    MEETINGSTONE_FORM_CONTROL_WIDTH = MEETINGSTONE_FORM_CONTROL_WIDTH,
    MEETINGSTONE_FORM_CONTROL_HEIGHT = MEETINGSTONE_FORM_CONTROL_HEIGHT,
    MEETINGSTONE_FORM_SUMMARY_HEIGHT = MEETINGSTONE_FORM_SUMMARY_HEIGHT,
    MEETINGSTONE_FORM_ROW_GAP = MEETINGSTONE_FORM_ROW_GAP,
    MEETINGSTONE_FORM_INLINE_LABEL_WIDTH = MEETINGSTONE_FORM_INLINE_LABEL_WIDTH,
    MEETINGSTONE_FORM_INLINE_INPUT_WIDTH = MEETINGSTONE_FORM_INLINE_INPUT_WIDTH,
    MEETINGSTONE_FORM_BUTTON_WIDTH = MEETINGSTONE_FORM_BUTTON_WIDTH,
    MEETINGSTONE_FORM_BUTTON_HEIGHT = MEETINGSTONE_FORM_BUTTON_HEIGHT,
    MEETINGSTONE_FORM_BUTTON_GAP = MEETINGSTONE_FORM_BUTTON_GAP,
    MEETINGSTONE_FORM_BUTTON_BOTTOM_OFFSET = MEETINGSTONE_FORM_BUTTON_BOTTOM_OFFSET,
    MEETINGSTONE_ACTION_BUTTON_TEXT_OFFSET_Y = MEETINGSTONE_ACTION_BUTTON_TEXT_OFFSET_Y,
    MEETINGSTONE_FORM_SUMMARY_NATIVE_OFFSET_LEFT = MEETINGSTONE_FORM_SUMMARY_NATIVE_OFFSET_LEFT,
    MEETINGSTONE_FORM_SUMMARY_NATIVE_OFFSET_RIGHT = MEETINGSTONE_FORM_SUMMARY_NATIVE_OFFSET_RIGHT,
    MEETINGSTONE_APPLICANT_REFRESH_BUTTON_WIDTH = MEETINGSTONE_APPLICANT_REFRESH_BUTTON_WIDTH,
    MEETINGSTONE_APPLICANT_REFRESH_BUTTON_HEIGHT = MEETINGSTONE_APPLICANT_REFRESH_BUTTON_HEIGHT,
    MEETINGSTONE_LEFT_BOTTOM_GRADIENT_TOP_OFFSET = MEETINGSTONE_LEFT_BOTTOM_GRADIENT_TOP_OFFSET,
    MEETINGSTONE_TOP_RIGHT_FILIGREE_OFFSET_X = MEETINGSTONE_TOP_RIGHT_FILIGREE_OFFSET_X,
    MEETINGSTONE_TOP_RIGHT_FILIGREE_OFFSET_Y = MEETINGSTONE_TOP_RIGHT_FILIGREE_OFFSET_Y,
    MEETINGSTONE_BOTTOM_RIGHT_FILIGREE_OFFSET_X = MEETINGSTONE_BOTTOM_RIGHT_FILIGREE_OFFSET_X,
    MEETINGSTONE_BOTTOM_RIGHT_FILIGREE_OFFSET_Y = MEETINGSTONE_BOTTOM_RIGHT_FILIGREE_OFFSET_Y,
    MEETINGSTONE_MISSING_ADDON_MESSAGE_KEY = MEETINGSTONE_MISSING_ADDON_MESSAGE_KEY,
    MEETINGSTONE_READ_ONLY_SECTION_WIDTH = MEETINGSTONE_READ_ONLY_SECTION_WIDTH,
    MEETINGSTONE_READ_ONLY_SECTION_GAP = MEETINGSTONE_READ_ONLY_SECTION_GAP,
    MEETINGSTONE_READ_ONLY_BASIC_HEIGHT = MEETINGSTONE_READ_ONLY_BASIC_HEIGHT,
    MEETINGSTONE_READ_ONLY_NAME_HEIGHT = MEETINGSTONE_READ_ONLY_NAME_HEIGHT,
    MEETINGSTONE_READ_ONLY_CONFIG_HEIGHT = MEETINGSTONE_READ_ONLY_CONFIG_HEIGHT,
    MEETINGSTONE_READ_ONLY_REQUIREMENT_HEIGHT = MEETINGSTONE_READ_ONLY_REQUIREMENT_HEIGHT,
    MEETINGSTONE_READ_ONLY_TEXT_INSET_X = MEETINGSTONE_READ_ONLY_TEXT_INSET_X,
    MEETINGSTONE_READ_ONLY_TEXT_TOP_OFFSET = MEETINGSTONE_READ_ONLY_TEXT_TOP_OFFSET,
    MEETINGSTONE_READ_ONLY_LINE_HEIGHT = MEETINGSTONE_READ_ONLY_LINE_HEIGHT,
    MEETINGSTONE_READ_ONLY_TITLE_TEXT_SIZE = MEETINGSTONE_READ_ONLY_TITLE_TEXT_SIZE,
    MEETINGSTONE_READ_ONLY_CONTENT_TEXT_SIZE = MEETINGSTONE_READ_ONLY_CONTENT_TEXT_SIZE,
    MEETINGSTONE_READ_ONLY_TITLE_COLOR = MEETINGSTONE_READ_ONLY_TITLE_COLOR,
    MEETINGSTONE_READ_ONLY_CONTENT_COLOR = MEETINGSTONE_READ_ONLY_CONTENT_COLOR,
    MEETINGSTONE_APPLICANT_SCROLLBAR_WIDTH = MEETINGSTONE_APPLICANT_SCROLLBAR_WIDTH,
    MEETINGSTONE_APPLICANT_SCROLLBAR_GAP = MEETINGSTONE_APPLICANT_SCROLLBAR_GAP,
    MEETINGSTONE_APPLICANT_SCROLLBAR_RIGHT_INSET = MEETINGSTONE_APPLICANT_SCROLLBAR_RIGHT_INSET,
    MEETINGSTONE_APPLICANT_SCROLL_BOTTOM_INSET = MEETINGSTONE_APPLICANT_SCROLL_BOTTOM_INSET,
}

local function CreateMeetingStoneFrameWithTemplateOptions(frameType, name, parent, templates)
    for _, template in ipairs(templates or {}) do
        local ok, frame = pcall(CreateFrame, frameType, name, parent, template)
        if ok and frame then
            return frame, template
        end
    end

    return CreateFrame(frameType, name, parent), nil
end

local function CreateMeetingStoneDisabledPlaceholderOverlay(parent, anchor, text, multiline)
    if not (parent and anchor) then
        return nil
    end

    local overlay = CreateFrame("Frame", nil, parent)
    overlay:SetPoint("TOPLEFT", anchor, "TOPLEFT", 0, 0)
    overlay:SetPoint("BOTTOMRIGHT", anchor, "BOTTOMRIGHT", 0, 0)
    overlay:SetFrameLevel((anchor:GetFrameLevel() or parent:GetFrameLevel() or 0) + 20)
    overlay:EnableMouse(false)

    local mask = overlay:CreateTexture(nil, "ARTWORK", nil, 0)
    mask:SetTexture("Interface\\Buttons\\WHITE8X8")
    mask:SetVertexColor(0, 0, 0, MEETINGSTONE_DISABLED_PLACEHOLDER_MASK_ALPHA)
    if multiline then
        mask:SetPoint("TOPLEFT", overlay, "TOPLEFT", MEETINGSTONE_DISABLED_PLACEHOLDER_SUMMARY_MASK_INSET_X, -MEETINGSTONE_DISABLED_PLACEHOLDER_SUMMARY_MASK_INSET_Y)
        mask:SetPoint("BOTTOMRIGHT", overlay, "BOTTOMRIGHT", -MEETINGSTONE_DISABLED_PLACEHOLDER_SUMMARY_MASK_INSET_X, MEETINGSTONE_DISABLED_PLACEHOLDER_SUMMARY_MASK_INSET_Y)
    else
        mask:SetPoint("TOPLEFT", overlay, "TOPLEFT", MEETINGSTONE_DISABLED_PLACEHOLDER_TITLE_MASK_INSET_X, -MEETINGSTONE_DISABLED_PLACEHOLDER_TITLE_MASK_INSET_Y)
        mask:SetPoint("BOTTOMRIGHT", overlay, "BOTTOMRIGHT", -MEETINGSTONE_DISABLED_PLACEHOLDER_TITLE_MASK_INSET_X, MEETINGSTONE_DISABLED_PLACEHOLDER_TITLE_MASK_INSET_Y)
    end

    local label = overlay:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
    label:SetText(text or "")
    label:SetTextColor(
        MEETINGSTONE_DISABLED_PLACEHOLDER_TEXT_COLOR[1],
        MEETINGSTONE_DISABLED_PLACEHOLDER_TEXT_COLOR[2],
        MEETINGSTONE_DISABLED_PLACEHOLDER_TEXT_COLOR[3],
        MEETINGSTONE_DISABLED_PLACEHOLDER_TEXT_COLOR[4]
    )
    label:SetJustifyH("LEFT")
    label:SetWordWrap(multiline == true)
    if core.ApplyFont then
        core.ApplyFont(label, MEETINGSTONE_DISABLED_PLACEHOLDER_TEXT_SIZE, "")
    end
    if multiline then
        label:SetPoint("TOPLEFT", overlay, "TOPLEFT", MEETINGSTONE_DISABLED_PLACEHOLDER_SUMMARY_INSET_X, -MEETINGSTONE_DISABLED_PLACEHOLDER_SUMMARY_INSET_TOP)
        label:SetPoint("BOTTOMRIGHT", overlay, "BOTTOMRIGHT", -MEETINGSTONE_DISABLED_PLACEHOLDER_SUMMARY_INSET_X, 0)
        label:SetJustifyV("TOP")
    else
        label:SetPoint("LEFT", overlay, "LEFT", MEETINGSTONE_DISABLED_PLACEHOLDER_TITLE_INSET_X, 0)
        label:SetPoint("RIGHT", overlay, "RIGHT", -MEETINGSTONE_DISABLED_PLACEHOLDER_TITLE_INSET_X, 0)
        label:SetJustifyV("MIDDLE")
    end

    overlay.Mask = mask
    overlay.Label = label
    overlay:Hide()
    return overlay
end

local function CreateMeetingStoneApplicantScrollContainer(parent)
    local scrollFrame = CreateFrame("ScrollFrame", nil, parent)
    scrollFrame:EnableMouseWheel(true)

    local content = CreateFrame("Frame", nil, scrollFrame)
    content:SetPoint("TOPLEFT", scrollFrame, "TOPLEFT", 0, 0)
    content:SetWidth(1)
    content:SetHeight(1)
    scrollFrame:SetScrollChild(content)

    local scrollBar = CreateMeetingStoneFrameWithTemplateOptions("EventFrame", nil, parent, { "MinimalScrollBar" })
    if not scrollBar or (not scrollBar.SetValue and not scrollBar.SetScrollPercentage) then
        scrollBar = CreateFrame("Slider", nil, parent)
        scrollBar:SetOrientation("VERTICAL")
        scrollBar:SetValueStep(1)
    end
    scrollBar:SetWidth(MEETINGSTONE_APPLICANT_SCROLLBAR_WIDTH)
    if scrollBar.SetMinMaxValues then
        scrollBar:SetMinMaxValues(0, 0)
    end
    scrollBar:Hide()

    local useNativeMinimalScrollBar = scrollBar.Init and ScrollUtil and ScrollUtil.InitScrollFrameWithScrollBar
    if useNativeMinimalScrollBar then
        ScrollUtil.InitScrollFrameWithScrollBar(scrollFrame, scrollBar)
    end

    local isSyncingScroll = false
    local scrollMinValue = 0
    local scrollMaxValue = 0

    local function setScrollBarRange(minValue, maxValue)
        scrollMinValue = minValue or 0
        scrollMaxValue = maxValue or 0
        if scrollBar.SetMinMaxValues then
            scrollBar:SetMinMaxValues(scrollMinValue, scrollMaxValue)
        elseif scrollBar.SetScrollPercentage then
            scrollBar:SetScrollPercentage(0)
        end
    end
    scrollBar.mkSetScrollRange = setScrollBarRange

    local function getScrollBarValue()
        if useNativeMinimalScrollBar and scrollBar.GetScrollPercentage then
            return (scrollBar:GetScrollPercentage() or 0) * scrollMaxValue
        end
        if scrollBar.GetValue then
            return scrollBar:GetValue() or 0
        end
        if scrollBar.GetScrollPercentage then
            return (scrollBar:GetScrollPercentage() or 0) * scrollMaxValue
        end
        return 0
    end
    scrollBar.mkGetScrollValue = getScrollBarValue

    local function setScrollBarValue(value)
        if useNativeMinimalScrollBar and scrollBar.SetScrollPercentage then
            local percentage = scrollMaxValue > 0 and ((value or 0) / scrollMaxValue) or 0
            scrollBar:SetScrollPercentage(math.min(math.max(percentage, 0), 1), ScrollBoxConstants and ScrollBoxConstants.NoScrollInterpolation)
        elseif scrollBar.SetValue then
            scrollBar:SetValue(value or 0)
        elseif scrollBar.SetScrollPercentage then
            local percentage = scrollMaxValue > 0 and ((value or 0) / scrollMaxValue) or 0
            scrollBar:SetScrollPercentage(math.min(math.max(percentage, 0), 1))
        end
    end
    scrollBar.mkSetScrollValue = setScrollBarValue

    local function applyScrollValue(value)
        local nextValue = math.min(math.max(value or 0, scrollMinValue), scrollMaxValue)
        isSyncingScroll = true
        scrollFrame:SetVerticalScroll(nextValue)
        if math.abs(getScrollBarValue() - nextValue) > 0.5 then
            setScrollBarValue(nextValue)
        end
        isSyncingScroll = false
    end

    if useNativeMinimalScrollBar then
        -- ScrollUtil.InitScrollFrameWithScrollBar wires the arrows, thumb, mouse wheel, and ScrollFrame sync.
    elseif scrollBar.SetScrollPercentage and scrollBar.RegisterCallback and scrollBar.Event and scrollBar.Event.OnScroll then
        scrollBar:RegisterCallback(scrollBar.Event.OnScroll, function(_, scrollPercentage)
            if isSyncingScroll then
                return
            end
            applyScrollValue((scrollPercentage or 0) * scrollMaxValue)
        end)
    elseif scrollBar.SetScript and scrollBar.SetValue then
        scrollBar:SetScript("OnValueChanged", function(_, value)
            if not isSyncingScroll then
                applyScrollValue(value or getScrollBarValue())
            end
        end)
    end
    setScrollBarValue(0)

    if not useNativeMinimalScrollBar then
        scrollFrame:SetScript("OnVerticalScroll", function(_, offset)
            applyScrollValue(offset)
        end)
        scrollFrame:SetScript("OnMouseWheel", function(_, delta)
            applyScrollValue(getScrollBarValue() - (delta or 0) * 36)
        end)
    end

    scrollFrame:SetScript("OnSizeChanged", function(frame)
        content:SetWidth(math.max(frame:GetWidth() or 0, 1))
    end)

    return scrollFrame, content, scrollBar
end

local function SetMeetingStonePanelAccentGradient(texture, startAlpha, endAlpha)
    if texture.SetGradient and CreateColor then
        local ok = pcall(
            texture.SetGradient,
            texture,
            "VERTICAL",
            CreateColor(MEETINGSTONE_PANEL_CENTER_ACCENT_COLOR[1], MEETINGSTONE_PANEL_CENTER_ACCENT_COLOR[2], MEETINGSTONE_PANEL_CENTER_ACCENT_COLOR[3], startAlpha),
            CreateColor(MEETINGSTONE_PANEL_CENTER_ACCENT_COLOR[1], MEETINGSTONE_PANEL_CENTER_ACCENT_COLOR[2], MEETINGSTONE_PANEL_CENTER_ACCENT_COLOR[3], endAlpha)
        )
        if ok then
            return
        end
    end

    if texture.SetGradientAlpha then
        texture:SetGradientAlpha(
            "VERTICAL",
            MEETINGSTONE_PANEL_CENTER_ACCENT_COLOR[1], MEETINGSTONE_PANEL_CENTER_ACCENT_COLOR[2], MEETINGSTONE_PANEL_CENTER_ACCENT_COLOR[3], startAlpha,
            MEETINGSTONE_PANEL_CENTER_ACCENT_COLOR[1], MEETINGSTONE_PANEL_CENTER_ACCENT_COLOR[2], MEETINGSTONE_PANEL_CENTER_ACCENT_COLOR[3], endAlpha
        )
    else
        texture:SetVertexColor(
            MEETINGSTONE_PANEL_CENTER_ACCENT_COLOR[1],
            MEETINGSTONE_PANEL_CENTER_ACCENT_COLOR[2],
            MEETINGSTONE_PANEL_CENTER_ACCENT_COLOR[3],
            math.max(startAlpha or 0, endAlpha or 0)
        )
    end
end

local function CreateMeetingStonePanelCenterAccent(parent)
    local accent = CreateFrame("Frame", nil, parent)
    accent:SetPoint("TOP", parent, "TOP", 0, 0)
    accent:SetPoint("BOTTOM", parent, "BOTTOM", 0, 0)
    accent:SetPoint("CENTER", parent, "CENTER", 0, 0)
    accent:SetWidth(MEETINGSTONE_PANEL_CENTER_ACCENT_WIDTH)

    local top = accent:CreateTexture(nil, "OVERLAY")
    top:SetTexture("Interface\\Buttons\\WHITE8X8")
    top:SetPoint("TOPLEFT", accent, "TOPLEFT", 0, 0)
    top:SetPoint("BOTTOMRIGHT", accent, "RIGHT", 0, 0)
    SetMeetingStonePanelAccentGradient(top, 1, 0)

    local bottom = accent:CreateTexture(nil, "OVERLAY")
    bottom:SetTexture("Interface\\Buttons\\WHITE8X8")
    bottom:SetPoint("TOPLEFT", accent, "LEFT", 0, 0)
    bottom:SetPoint("BOTTOMRIGHT", accent, "BOTTOMRIGHT", 0, 0)
    SetMeetingStonePanelAccentGradient(bottom, 0, 1)

    return accent
end

local function AddMeetingStoneReadOnlySection(parent, topOffset, height)
    local section = CreateFrame("Frame", nil, parent)
    section:SetPoint("TOPLEFT", parent, "TOPLEFT", MEETINGSTONE_READ_ONLY_SECTION_INSET_X, topOffset)
    section:SetSize(MEETINGSTONE_READ_ONLY_SECTION_WIDTH, height)
    return section
end

local function SetMeetingStoneReadOnlyDividerGradient(texture, startAlpha, endAlpha)
    if not texture then
        return
    end

    local color = MEETINGSTONE_READ_ONLY_DIVIDER_COLOR
    if texture.SetGradient and CreateColor then
        local ok = pcall(
            texture.SetGradient,
            texture,
            "HORIZONTAL",
            CreateColor(color[1], color[2], color[3], startAlpha),
            CreateColor(color[1], color[2], color[3], endAlpha)
        )
        if ok then
            return
        end
    end

    if texture.SetGradientAlpha then
        texture:SetGradientAlpha(
            "HORIZONTAL",
            color[1], color[2], color[3], startAlpha,
            color[1], color[2], color[3], endAlpha
        )
    else
        texture:SetVertexColor(color[1], color[2], color[3], math.max(startAlpha or 0, endAlpha or 0))
    end
end

local function GetMeetingStoneReadOnlyDividerPixelHeight(divider)
    local scale = divider and divider.GetEffectiveScale and divider:GetEffectiveScale() or 1
    if type(scale) ~= "number" or scale <= 0 then
        scale = 1
    end
    local pixelHeight = math.max(1, math.floor((MEETINGSTONE_READ_ONLY_DIVIDER_HEIGHT * scale) + 0.5))
    return pixelHeight / scale
end

local function LayoutMeetingStoneReadOnlyDivider(divider)
    if not divider then
        return
    end

    local height = GetMeetingStoneReadOnlyDividerPixelHeight(divider)
    divider:SetSize(MEETINGSTONE_READ_ONLY_DIVIDER_WIDTH, height)

    local fadeWidth = math.min(MEETINGSTONE_READ_ONLY_DIVIDER_FADE_WIDTH, MEETINGSTONE_READ_ONLY_DIVIDER_WIDTH / 2)
    if divider.Left then
        divider.Left:ClearAllPoints()
        divider.Left:SetPoint("TOPLEFT", divider, "TOPLEFT", 0, 0)
        divider.Left:SetPoint("BOTTOMLEFT", divider, "BOTTOMLEFT", 0, 0)
        divider.Left:SetWidth(fadeWidth)
    end
    if divider.Center then
        divider.Center:ClearAllPoints()
        divider.Center:SetPoint("TOPLEFT", divider, "TOPLEFT", fadeWidth, 0)
        divider.Center:SetPoint("BOTTOMRIGHT", divider, "BOTTOMRIGHT", -fadeWidth, 0)
    end
    if divider.Right then
        divider.Right:ClearAllPoints()
        divider.Right:SetPoint("TOPRIGHT", divider, "TOPRIGHT", 0, 0)
        divider.Right:SetPoint("BOTTOMRIGHT", divider, "BOTTOMRIGHT", 0, 0)
        divider.Right:SetWidth(fadeWidth)
    end
end

local function AddMeetingStoneReadOnlyDivider(parent, centerY)
    local divider = CreateFrame("Frame", nil, parent)
    divider:SetPoint("CENTER", parent, "TOP", 0, centerY)
    divider:SetFrameLevel(parent:GetFrameLevel() + 1)

    local left = divider:CreateTexture(nil, "BORDER")
    left:SetTexture("Interface\\Buttons\\WHITE8X8")
    SetMeetingStoneReadOnlyDividerGradient(left, 0, MEETINGSTONE_READ_ONLY_DIVIDER_ALPHA)

    local center = divider:CreateTexture(nil, "BORDER")
    center:SetTexture("Interface\\Buttons\\WHITE8X8")
    center:SetVertexColor(
        MEETINGSTONE_READ_ONLY_DIVIDER_COLOR[1],
        MEETINGSTONE_READ_ONLY_DIVIDER_COLOR[2],
        MEETINGSTONE_READ_ONLY_DIVIDER_COLOR[3],
        MEETINGSTONE_READ_ONLY_DIVIDER_ALPHA
    )

    local right = divider:CreateTexture(nil, "BORDER")
    right:SetTexture("Interface\\Buttons\\WHITE8X8")
    SetMeetingStoneReadOnlyDividerGradient(right, MEETINGSTONE_READ_ONLY_DIVIDER_ALPHA, 0)

    divider.Left = left
    divider.Center = center
    divider.Right = right
    LayoutMeetingStoneReadOnlyDivider(divider)

    return divider
end

local function AddMeetingStoneReadOnlyText(parent, template, textSize, color)
    local text = parent:CreateFontString(nil, "OVERLAY", template or "GameFontHighlight")
    text:SetJustifyH("CENTER")
    text:SetJustifyV("MIDDLE")
    text:SetTextColor(color[1], color[2], color[3], color[4] or 1)
    if core.ApplyFont and textSize then
        core.ApplyFont(text, textSize, "")
    end
    return text
end

local function SetMeetingStoneReadOnlyTextTop(text, parent, topOffset)
    text:ClearAllPoints()
    text:SetPoint("TOPLEFT", parent, "TOPLEFT", MEETINGSTONE_READ_ONLY_TEXT_INSET_X, topOffset)
    text:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -MEETINGSTONE_READ_ONLY_TEXT_INSET_X, topOffset)
    text:SetHeight(MEETINGSTONE_READ_ONLY_LINE_HEIGHT)
end

local function AddMeetingStoneReadOnlySectionTitle(parent, label)
    local text = AddMeetingStoneReadOnlyText(parent, "GameFontNormal", MEETINGSTONE_READ_ONLY_TITLE_TEXT_SIZE, MEETINGSTONE_READ_ONLY_TITLE_COLOR)
    SetMeetingStoneReadOnlyTextTop(text, parent, MEETINGSTONE_READ_ONLY_TEXT_TOP_OFFSET)
    text:SetText(label)
    return text
end

local function SetMeetingStoneReadOnlyTextBelow(text, relativeTo, topGap, height)
    text:ClearAllPoints()
    text:SetPoint("TOPLEFT", relativeTo, "BOTTOMLEFT", 0, topGap or -MEETINGSTONE_READ_ONLY_CONTENT_GAP)
    text:SetPoint("TOPRIGHT", relativeTo, "BOTTOMRIGHT", 0, topGap or -MEETINGSTONE_READ_ONLY_CONTENT_GAP)
    text:SetHeight(height or MEETINGSTONE_READ_ONLY_LINE_HEIGHT)
end

local function SetMeetingStoneReadOnlyContentTop(text, parent, topOffset, height)
    text:ClearAllPoints()
    text:SetPoint("TOPLEFT", parent, "TOPLEFT", MEETINGSTONE_READ_ONLY_TEXT_INSET_X, topOffset)
    text:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -MEETINGSTONE_READ_ONLY_TEXT_INSET_X, topOffset)
    text:SetHeight(height or MEETINGSTONE_READ_ONLY_LINE_HEIGHT)
end

local function AddMeetingStoneReadOnlySectionContent(parent, relativeTo, textSize, color, topGap, height)
    local text = AddMeetingStoneReadOnlyText(parent, "GameFontHighlight", textSize, color)
    SetMeetingStoneReadOnlyTextBelow(text, relativeTo, topGap, height)
    return text
end

local function SetMeetingStoneReadOnlyRowBelow(row, relativeTo, topGap)
    row:ClearAllPoints()
    row:SetPoint("TOPLEFT", relativeTo, "BOTTOMLEFT", 0, topGap or -MEETINGSTONE_READ_ONLY_CONTENT_GAP)
    row:SetPoint("TOPRIGHT", relativeTo, "BOTTOMRIGHT", 0, topGap or -MEETINGSTONE_READ_ONLY_CONTENT_GAP)
    row:SetHeight(MEETINGSTONE_READ_ONLY_LINE_HEIGHT)
end

local function AddMeetingStoneReadOnlyRequirementRow(parent, relativeTo, label, topGap)
    local row = CreateFrame("Frame", nil, parent)
    SetMeetingStoneReadOnlyRowBelow(row, relativeTo, topGap)

    local labelText = AddMeetingStoneReadOnlyText(row, "GameFontHighlight", MEETINGSTONE_READ_ONLY_CONTENT_TEXT_SIZE, MEETINGSTONE_READ_ONLY_CONTENT_COLOR)
    labelText:SetJustifyH("LEFT")
    labelText:SetPoint("LEFT", row, "LEFT", 0, 0)
    labelText:SetPoint("RIGHT", row, "CENTER", 0, 0)
    labelText:SetHeight(MEETINGSTONE_READ_ONLY_LINE_HEIGHT)
    labelText:SetText(label)

    local valueText = AddMeetingStoneReadOnlyText(row, "GameFontHighlight", MEETINGSTONE_READ_ONLY_CONTENT_TEXT_SIZE, MEETINGSTONE_READ_ONLY_CONTENT_COLOR)
    valueText:SetJustifyH("RIGHT")
    valueText:SetPoint("LEFT", row, "CENTER", 0, 0)
    valueText:SetPoint("RIGHT", row, "RIGHT", 0, 0)
    valueText:SetHeight(MEETINGSTONE_READ_ONLY_LINE_HEIGHT)

    return row, valueText
end

local function SetMeetingStoneReadOnlyRoleSlotPosition(slot, parent, index, totalWidth, topOffset)
    local fullWidth = (MEETINGSTONE_READ_ONLY_ROLE_ICON_SIZE * 5) + (MEETINGSTONE_READ_ONLY_ROLE_ICON_GAP * 4)
    local startX = math.max(0, ((totalWidth or MEETINGSTONE_READ_ONLY_SECTION_WIDTH) - fullWidth) / 2)
    local offsetX = startX + ((index - 1) * (MEETINGSTONE_READ_ONLY_ROLE_ICON_SIZE + MEETINGSTONE_READ_ONLY_ROLE_ICON_GAP))

    slot:ClearAllPoints()
    slot:SetPoint("TOPLEFT", parent, "TOPLEFT", offsetX, topOffset)
end

local function AddMeetingStoneReadOnlyRoleSlot(parent, index, totalWidth)
    local slot = CreateFrame("Frame", nil, parent)
    slot:SetSize(MEETINGSTONE_READ_ONLY_ROLE_ICON_SIZE, MEETINGSTONE_READ_ONLY_ROLE_ICON_SIZE)
    SetMeetingStoneReadOnlyRoleSlotPosition(slot, parent, index, totalWidth, MEETINGSTONE_READ_ONLY_ROLE_SLOT_TOP_OFFSET)

    local icon = slot:CreateTexture(nil, "OVERLAY")
    icon:SetAllPoints(slot)
    icon:Hide()

    local empty = slot:CreateTexture(nil, "OVERLAY")
    empty:SetAllPoints(slot)
    if TrySetMeetingStoneAtlas then
        TrySetMeetingStoneAtlas(empty, MEETINGSTONE_READ_ONLY_EMPTY_ROLE_ATLAS, false)
    elseif empty.SetAtlas then
        pcall(empty.SetAtlas, empty, MEETINGSTONE_READ_ONLY_EMPTY_ROLE_ATLAS, false)
    end

    slot.icon = icon
    slot.empty = empty
    return slot
end

local function GetMeetingStoneReadOnlySectionHeights()
    return
        MEETINGSTONE_READ_ONLY_BASIC_HEIGHT,
        MEETINGSTONE_READ_ONLY_NAME_HEIGHT,
        MEETINGSTONE_READ_ONLY_CONFIG_HEIGHT,
        MEETINGSTONE_READ_ONLY_REQUIREMENT_HEIGHT
end

local function LayoutMeetingStoneReadOnlyTitleContentSection(section, sectionHeight)
    local title = section and section.mkReadOnlyTitle
    local content = section and section.mkReadOnlyContent
    if not title or not content then
        return
    end

    local contentHeight = section.mkReadOnlyContentHeight or MEETINGSTONE_READ_ONLY_LINE_HEIGHT
    if section.mkReadOnlyHideTitle then
        title:Hide()
        local contentTopOffset = -math.max(0, (sectionHeight - contentHeight) / 2)
        SetMeetingStoneReadOnlyContentTop(content, section, contentTopOffset, contentHeight)
        return
    end

    title:Show()
    local stackHeight = MEETINGSTONE_READ_ONLY_LINE_HEIGHT + MEETINGSTONE_READ_ONLY_CONTENT_GAP + contentHeight
    local titleTopOffset = -math.max(0, (sectionHeight - stackHeight) / 2)

    SetMeetingStoneReadOnlyTextTop(title, section, titleTopOffset)
    SetMeetingStoneReadOnlyTextBelow(content, title, -MEETINGSTONE_READ_ONLY_CONTENT_GAP, contentHeight)
end

local function LayoutMeetingStoneReadOnlyConfigSection(section, sectionHeight)
    local title = section and section.mkReadOnlyTitle
    if not title then
        return
    end

    local stackHeight = MEETINGSTONE_READ_ONLY_LINE_HEIGHT + MEETINGSTONE_READ_ONLY_CONFIG_ICON_GAP + MEETINGSTONE_READ_ONLY_ROLE_ICON_SIZE
    local titleTopOffset = -math.max(0, (sectionHeight - stackHeight) / 2)
    local roleTopOffset = titleTopOffset - MEETINGSTONE_READ_ONLY_LINE_HEIGHT - MEETINGSTONE_READ_ONLY_CONFIG_ICON_GAP

    SetMeetingStoneReadOnlyTextTop(title, section, titleTopOffset)
    for index, slot in ipairs(section.mkReadOnlyRoleSlots or {}) do
        SetMeetingStoneReadOnlyRoleSlotPosition(slot, section, index, MEETINGSTONE_READ_ONLY_SECTION_WIDTH, roleTopOffset)
    end
end

local function LayoutMeetingStoneReadOnlyRequirementSection(section, sectionHeight)
    local title = section and section.mkReadOnlyTitle
    local itemLevelRow = section and section.mkReadOnlyItemLevelRow
    local scoreRow = section and section.mkReadOnlyScoreRow
    if not title or not itemLevelRow or not scoreRow then
        return
    end

    local stackHeight =
        MEETINGSTONE_READ_ONLY_LINE_HEIGHT +
        MEETINGSTONE_READ_ONLY_REQUIREMENT_FIRST_ROW_GAP +
        MEETINGSTONE_READ_ONLY_LINE_HEIGHT +
        MEETINGSTONE_READ_ONLY_REQUIREMENT_ROW_GAP +
        MEETINGSTONE_READ_ONLY_LINE_HEIGHT
    local titleTopOffset = -math.max(0, (sectionHeight - stackHeight) / 2)

    SetMeetingStoneReadOnlyTextTop(title, section, titleTopOffset)
    SetMeetingStoneReadOnlyRowBelow(itemLevelRow, title, -MEETINGSTONE_READ_ONLY_REQUIREMENT_FIRST_ROW_GAP)
    SetMeetingStoneReadOnlyRowBelow(scoreRow, itemLevelRow, -MEETINGSTONE_READ_ONLY_REQUIREMENT_ROW_GAP)
end

local function LayoutMeetingStoneReadOnlyInfoFrame(frame)
    local data = frame and frame.mkReadOnlyLayout
    if not data then
        return
    end

    local dungeonHeight, nameHeight, configHeight, requirementHeight =
        GetMeetingStoneReadOnlySectionHeights()
    local sectionHeights = { dungeonHeight, nameHeight, configHeight, requirementHeight }
    local readOnlyY = MEETINGSTONE_FORM_TOP_OFFSET

    for index, section in ipairs(data.sections) do
        local sectionHeight = sectionHeights[index]
        section:ClearAllPoints()
        section:SetPoint("TOPLEFT", frame, "TOPLEFT", MEETINGSTONE_READ_ONLY_SECTION_INSET_X, readOnlyY)
        section:SetSize(MEETINGSTONE_READ_ONLY_SECTION_WIDTH, sectionHeight)

        if section.mkReadOnlyKind == "titleContent" then
            LayoutMeetingStoneReadOnlyTitleContentSection(section, sectionHeight)
        elseif section.mkReadOnlyKind == "config" then
            LayoutMeetingStoneReadOnlyConfigSection(section, sectionHeight)
        elseif section.mkReadOnlyKind == "requirement" then
            LayoutMeetingStoneReadOnlyRequirementSection(section, sectionHeight)
        end

        readOnlyY = readOnlyY - sectionHeight

        local divider = data.dividers[index]
        if divider then
            divider:ClearAllPoints()
            LayoutMeetingStoneReadOnlyDivider(divider)
            divider:SetPoint("CENTER", frame, "TOP", 0, readOnlyY - (MEETINGSTONE_READ_ONLY_SECTION_GAP / 2))
        end

        readOnlyY = readOnlyY - MEETINGSTONE_READ_ONLY_SECTION_GAP
    end
end

local function RefreshMeetingStonePublishedData(owner)
    if not owner then
        return
    end

    owner.meetingStoneApplicantSort = nil

    if C_LFGList and C_LFGList.RefreshApplicants then
        pcall(C_LFGList.RefreshApplicants)
    end

    local canManage = not CanManageMeetingStoneListing or CanManageMeetingStoneListing()
    if canManage then
        return false
    end

    local restored = false
    if owner.RestoreMeetingStoneReadOnlyDraftFromActiveEntry then
        restored = owner:RestoreMeetingStoneReadOnlyDraftFromActiveEntry(true) == true
    elseif owner.RestoreMeetingStoneDraftFromActiveEntry then
        restored = owner:RestoreMeetingStoneDraftFromActiveEntry(true) == true
    end
    if not restored and not canManage and owner.RequestGroupSync then
        owner:RequestGroupSync()
    end
    if not restored and owner.RestoreMeetingStoneDraftFromSharedLeaderEntry then
        restored = owner:RestoreMeetingStoneDraftFromSharedLeaderEntry() == true
    end

    return restored == true
end

local function QueueMeetingStonePublishedDataRefresh(owner)
    if not owner then
        return
    end

    local refreshedPublishedData = RefreshMeetingStonePublishedData(owner)
    if refreshedPublishedData and owner.RefreshMainWindow then
        owner:RefreshMainWindow("MeetingStonePublishedDataRefresh")
    elseif owner.RefreshMeetingStoneApplicantsOnly then
        owner:RefreshMeetingStoneApplicantsOnly("MeetingStoneApplicantRefresh")
    elseif owner.RefreshMainWindow then
        owner:RefreshMainWindow("MeetingStoneApplicantRefresh")
    end

    if C_Timer and C_Timer.After then
        C_Timer.After(0.2, function()
            local delayedPublishedData = RefreshMeetingStonePublishedData(owner)
            if delayedPublishedData and owner.RefreshMainWindow then
                owner:RefreshMainWindow("MeetingStonePublishedDataRefresh")
            elseif owner.RefreshMeetingStoneApplicantsOnly then
                owner:RefreshMeetingStoneApplicantsOnly("MeetingStoneApplicantRefresh")
            elseif owner.RefreshMainWindow then
                owner:RefreshMainWindow("MeetingStoneApplicantRefresh")
            end
        end)
    end
end
MeetingStoneLayout.QueuePublishedDataRefresh = QueueMeetingStonePublishedDataRefresh

function MeetingStoneLayout.Build(owner, tabGroup)
    local layout = MeetingStoneLayoutConfig
    owner.mkBuildingMeetingStoneTab = true

    local container = CreateFrame("Frame", nil, UIParent)
    container.mkFillDecorFrame = true
    container.mkDecorFrameInsetLeft = layout.MEETINGSTONE_PANEL_INSET_LEFT
    container.mkDecorFrameInsetTop = layout.MEETINGSTONE_PANEL_INSET_TOP
    container.mkDecorFrameInsetRight = layout.MEETINGSTONE_PANEL_INSET_RIGHT
    container.mkDecorFrameInsetBottom = layout.MEETINGSTONE_PANEL_INSET_BOTTOM
    tabGroup:AddChild(container)

    local MSLeftPanel = CreateFrame("Frame", "MSLeftPanel", container, "BackdropTemplate")
    MSLeftPanel:SetPoint("TOPLEFT", container, "TOPLEFT", 0, 0)
    MSLeftPanel:SetPoint("BOTTOMLEFT", container, "BOTTOMLEFT", 0, 0)
    MSLeftPanel:SetWidth(layout.MEETINGSTONE_LEFT_PANEL_WIDTH)

    local leftBackground = MSLeftPanel:CreateTexture(nil, "BACKGROUND")
    leftBackground:SetAllPoints(MSLeftPanel)
    leftBackground:SetTexture("Interface\\Buttons\\WHITE8X8")
    leftBackground:SetVertexColor(0, 0, 0, layout.MEETINGSTONE_LEFT_PANEL_BACKGROUND_ALPHA)
    leftBackground:Hide()
    MSLeftPanel.Background = leftBackground

    local leftHeaderBackground = MSLeftPanel:CreateTexture(nil, "BACKGROUND", nil, 1)
    leftHeaderBackground:SetPoint("TOPLEFT", MSLeftPanel, "TOPLEFT", 0, layout.MEETINGSTONE_HEADER_TOP_OFFSET)
    leftHeaderBackground:SetPoint("BOTTOMRIGHT", MSLeftPanel, "TOPRIGHT", 0, layout.MEETINGSTONE_HEADER_BOTTOM_OFFSET)
    TrySetMeetingStoneAtlas(leftHeaderBackground, "housefinder_header-bg-gradient", false)
    MSLeftPanel.HeaderBackground = leftHeaderBackground

    local leftHeaderTitle = MSLeftPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    leftHeaderTitle:SetPoint("TOPLEFT", leftHeaderBackground, "TOPLEFT", layout.MEETINGSTONE_HEADER_CONTENT_INSET_X, layout.MEETINGSTONE_HEADER_CONTENT_OFFSET_Y)
    leftHeaderTitle:SetPoint("BOTTOMRIGHT", leftHeaderBackground, "BOTTOMRIGHT", -layout.MEETINGSTONE_HEADER_CONTENT_INSET_X, layout.MEETINGSTONE_HEADER_CONTENT_OFFSET_Y)
    leftHeaderTitle:SetJustifyH("CENTER")
    leftHeaderTitle:SetJustifyV("MIDDLE")
    leftHeaderTitle:SetText(TRANSLATE("组队管理"))
    leftHeaderTitle:SetTextColor(1, 0.82, 0, 1)
    if core.ApplyFont then
        core.ApplyFont(leftHeaderTitle, layout.MEETINGSTONE_HEADER_TEXT_SIZE, "OUTLINE")
    end
    MSLeftPanel.HeaderTitle = leftHeaderTitle

    local formY = layout.MEETINGSTONE_FORM_TOP_OFFSET
    local function addStackedLabel(text)
        local label = CreateMeetingStoneLabel(MSLeftPanel, text)
        label:SetPoint("TOPLEFT", MSLeftPanel, "TOPLEFT", layout.MEETINGSTONE_FORM_INSET_X, formY)
        label:SetWidth(layout.MEETINGSTONE_FORM_CONTROL_WIDTH)
        label:SetHeight(layout.MEETINGSTONE_FORM_LABEL_HEIGHT)
        formY = formY - layout.MEETINGSTONE_FORM_LABEL_HEIGHT - layout.MEETINGSTONE_FORM_LABEL_TO_CONTROL_GAP
        return label
    end
    local function addStackedControl(control, height)
        control:SetPoint("TOPLEFT", MSLeftPanel, "TOPLEFT", layout.MEETINGSTONE_FORM_INSET_X, formY)
        formY = formY - (height or layout.MEETINGSTONE_FORM_CONTROL_HEIGHT) - layout.MEETINGSTONE_FORM_ROW_GAP
        return control
    end
    local function clearPublishFailureFromUserInput(input)
        if input and input.mkMeetingStoneProgrammaticSetText then
            return
        end
        if not (owner.ClearMeetingStonePublishFailureMessage and owner:ClearMeetingStonePublishFailureMessage()) then
            return
        end
        if owner.RefreshMeetingStoneApplicantsOnly then
            owner:RefreshMeetingStoneApplicantsOnly("MeetingStonePublishFailureCleared")
        elseif owner.RefreshMainWindow then
            owner:RefreshMainWindow("MeetingStonePublishFailureCleared")
        end
    end

    local dungeonLabel = addStackedLabel(TRANSLATE("选择赛季副本"))
    local dungeonDropdown = addStackedControl(CreateMeetingStoneDungeonDropdown(MSLeftPanel, owner, layout.MEETINGSTONE_FORM_CONTROL_WIDTH), layout.MEETINGSTONE_FORM_CONTROL_HEIGHT)

    local titleLabel = addStackedLabel(TRANSLATE("队伍名称"))
    local titleHolder = CreateFrame("Frame", nil, MSLeftPanel)
    titleHolder:SetSize(layout.MEETINGSTONE_FORM_CONTROL_WIDTH, layout.MEETINGSTONE_FORM_CONTROL_HEIGHT)
    AddMeetingStoneTitleHolderBorder(titleHolder)
    addStackedControl(titleHolder, layout.MEETINGSTONE_FORM_CONTROL_HEIGHT)
    local fallbackTitleBox = CreateMeetingStoneInput(titleHolder, layout.MEETINGSTONE_FORM_CONTROL_WIDTH, layout.MEETINGSTONE_FORM_CONTROL_HEIGHT, false)
    fallbackTitleBox:SetAllPoints(titleHolder)
    if fallbackTitleBox.mkMeetingStoneInputBorder then
        fallbackTitleBox.mkMeetingStoneInputBorder:Hide()
    end
    SetMeetingStoneInputPlaceholder(fallbackTitleBox, TRANSLATE("请输入队伍名称"), false)
    fallbackTitleBox:SetScript("OnTextChanged", function(input)
        owner:SyncMeetingStoneDraftFromWidgets()
        clearPublishFailureFromUserInput(input)
        owner:RefreshMeetingStoneActionButtons()
    end)
    local titleBox = fallbackTitleBox
    local titleDisabledPlaceholderOverlay = CreateMeetingStoneDisabledPlaceholderOverlay(
        MSLeftPanel,
        titleHolder,
        TRANSLATE("请输入队伍名称"),
        false
    )

    local summaryLabel = addStackedLabel(TRANSLATE("组队说明"))
    local summaryHolder = CreateFrame("Frame", nil, MSLeftPanel)
    summaryHolder:SetSize(layout.MEETINGSTONE_FORM_CONTROL_WIDTH, layout.MEETINGSTONE_FORM_SUMMARY_HEIGHT)
    addStackedControl(summaryHolder, layout.MEETINGSTONE_FORM_SUMMARY_HEIGHT)
    local fallbackSummaryFrame, fallbackSummaryBox = CreateMeetingStoneSummaryBox(summaryHolder, layout.MEETINGSTONE_FORM_CONTROL_WIDTH, layout.MEETINGSTONE_FORM_SUMMARY_HEIGHT)
    fallbackSummaryFrame:SetPoint("TOPLEFT", summaryHolder, "TOPLEFT", layout.MEETINGSTONE_FORM_SUMMARY_NATIVE_OFFSET_LEFT, 0)
    fallbackSummaryFrame:SetPoint("BOTTOMRIGHT", summaryHolder, "BOTTOMRIGHT", layout.MEETINGSTONE_FORM_SUMMARY_NATIVE_OFFSET_RIGHT, 0)
    SetMeetingStoneInputPlaceholder(fallbackSummaryBox, TRANSLATE("请输入更多组队要求（可选）"), true)
    fallbackSummaryBox:SetScript("OnTextChanged", function(input)
        owner:SyncMeetingStoneDraftFromWidgets()
        clearPublishFailureFromUserInput(input)
    end)
    local summaryFrame, summaryBox = fallbackSummaryFrame, fallbackSummaryBox
    local summaryDisabledPlaceholderOverlay = CreateMeetingStoneDisabledPlaceholderOverlay(
        MSLeftPanel,
        summaryHolder,
        TRANSLATE("请输入更多组队要求（可选）"),
        true
    )

    local playStyleLabel = addStackedLabel(TRANSLATE("队伍风格"))
    local playStyleDropdown = addStackedControl(CreateMeetingStonePlayStyleDropdown(MSLeftPanel, owner, layout.MEETINGSTONE_FORM_CONTROL_WIDTH), layout.MEETINGSTONE_FORM_CONTROL_HEIGHT)

    local itemLevelLabel = CreateMeetingStoneLabel(MSLeftPanel, TRANSLATE("最低装等"))
    itemLevelLabel:SetPoint("LEFT", MSLeftPanel, "TOPLEFT", layout.MEETINGSTONE_FORM_INSET_X, formY - (layout.MEETINGSTONE_FORM_CONTROL_HEIGHT / 2))
    itemLevelLabel:SetWidth(layout.MEETINGSTONE_FORM_INLINE_LABEL_WIDTH)
    itemLevelLabel:SetHeight(layout.MEETINGSTONE_FORM_LABEL_HEIGHT)
    local itemLevelBox = CreateMeetingStoneInput(MSLeftPanel, layout.MEETINGSTONE_FORM_INLINE_INPUT_WIDTH, layout.MEETINGSTONE_FORM_CONTROL_HEIGHT, true)
    itemLevelBox:SetPoint("TOPRIGHT", MSLeftPanel, "TOPLEFT", layout.MEETINGSTONE_FORM_INSET_X + layout.MEETINGSTONE_FORM_CONTROL_WIDTH, formY)
    itemLevelBox:SetText("0")
    EnableMeetingStoneSelectAllAfterMouseLeave(itemLevelBox)
    itemLevelBox:SetScript("OnTextChanged", function(input)
        owner:SyncMeetingStoneDraftFromWidgets()
        clearPublishFailureFromUserInput(input)
        owner:RefreshMeetingStoneActionButtons()
    end)
    formY = formY - layout.MEETINGSTONE_FORM_CONTROL_HEIGHT - layout.MEETINGSTONE_FORM_ROW_GAP

    local scoreLabel = CreateMeetingStoneLabel(MSLeftPanel, TRANSLATE("最低分数"))
    scoreLabel:SetPoint("LEFT", MSLeftPanel, "TOPLEFT", layout.MEETINGSTONE_FORM_INSET_X, formY - (layout.MEETINGSTONE_FORM_CONTROL_HEIGHT / 2))
    scoreLabel:SetWidth(layout.MEETINGSTONE_FORM_INLINE_LABEL_WIDTH)
    scoreLabel:SetHeight(layout.MEETINGSTONE_FORM_LABEL_HEIGHT)
    local scoreBox = CreateMeetingStoneInput(MSLeftPanel, layout.MEETINGSTONE_FORM_INLINE_INPUT_WIDTH, layout.MEETINGSTONE_FORM_CONTROL_HEIGHT, true)
    scoreBox:SetPoint("TOPRIGHT", MSLeftPanel, "TOPLEFT", layout.MEETINGSTONE_FORM_INSET_X + layout.MEETINGSTONE_FORM_CONTROL_WIDTH, formY)
    scoreBox:SetText("0")
    EnableMeetingStoneSelectAllAfterMouseLeave(scoreBox)
    scoreBox:SetScript("OnTextChanged", function(input)
        owner:SyncMeetingStoneDraftFromWidgets()
        clearPublishFailureFromUserInput(input)
        owner:RefreshMeetingStoneActionButtons()
    end)

    local readOnlyInfoFrame = CreateFrame("Frame", nil, MSLeftPanel)
    readOnlyInfoFrame:SetPoint("TOPLEFT", MSLeftPanel, "TOPLEFT", 0, 0)
    readOnlyInfoFrame:SetPoint("BOTTOMRIGHT", MSLeftPanel, "BOTTOMRIGHT", 0, 0)
    readOnlyInfoFrame:EnableMouse(false)
    readOnlyInfoFrame:Hide()

    local readOnlyY = layout.MEETINGSTONE_FORM_TOP_OFFSET
    local readOnlyDividers = {}
    local dungeonSection = AddMeetingStoneReadOnlySection(readOnlyInfoFrame, readOnlyY, layout.MEETINGSTONE_READ_ONLY_BASIC_HEIGHT)
    local dungeonTitle = AddMeetingStoneReadOnlySectionTitle(dungeonSection, TRANSLATE("活动类型"))
    local dungeonText = AddMeetingStoneReadOnlySectionContent(
        dungeonSection,
        dungeonTitle,
        layout.MEETINGSTONE_READ_ONLY_TITLE_TEXT_SIZE,
        layout.MEETINGSTONE_READ_ONLY_TITLE_COLOR,
        -MEETINGSTONE_READ_ONLY_CONTENT_GAP,
        MEETINGSTONE_READ_ONLY_DUNGEON_CONTENT_HEIGHT
    )
    dungeonText:SetWordWrap(true)
    dungeonSection.mkReadOnlyKind = "titleContent"
    dungeonSection.mkReadOnlyTitle = dungeonTitle
    dungeonSection.mkReadOnlyContent = dungeonText
    dungeonSection.mkReadOnlyContentHeight = MEETINGSTONE_READ_ONLY_DUNGEON_CONTENT_HEIGHT
    readOnlyDividers[1] = AddMeetingStoneReadOnlyDivider(
        readOnlyInfoFrame,
        readOnlyY - layout.MEETINGSTONE_READ_ONLY_BASIC_HEIGHT - (layout.MEETINGSTONE_READ_ONLY_SECTION_GAP / 2)
    )
    readOnlyY = readOnlyY - layout.MEETINGSTONE_READ_ONLY_BASIC_HEIGHT - layout.MEETINGSTONE_READ_ONLY_SECTION_GAP

    local nameSection = AddMeetingStoneReadOnlySection(readOnlyInfoFrame, readOnlyY, layout.MEETINGSTONE_READ_ONLY_NAME_HEIGHT)
    local nameTitle = AddMeetingStoneReadOnlySectionTitle(nameSection, TRANSLATE("队伍名称"))
    local titleText = AddMeetingStoneReadOnlySectionContent(
        nameSection,
        nameTitle,
        layout.MEETINGSTONE_READ_ONLY_CONTENT_TEXT_SIZE,
        layout.MEETINGSTONE_READ_ONLY_CONTENT_COLOR,
        -MEETINGSTONE_READ_ONLY_CONTENT_GAP,
        MEETINGSTONE_READ_ONLY_NAME_CONTENT_HEIGHT
    )
    titleText:SetWordWrap(true)
    nameSection.mkReadOnlyKind = "titleContent"
    nameSection.mkReadOnlyTitle = nameTitle
    nameSection.mkReadOnlyContent = titleText
    nameSection.mkReadOnlyContentHeight = MEETINGSTONE_READ_ONLY_NAME_CONTENT_HEIGHT
    readOnlyDividers[2] = AddMeetingStoneReadOnlyDivider(
        readOnlyInfoFrame,
        readOnlyY - layout.MEETINGSTONE_READ_ONLY_NAME_HEIGHT - (layout.MEETINGSTONE_READ_ONLY_SECTION_GAP / 2)
    )
    readOnlyY = readOnlyY - layout.MEETINGSTONE_READ_ONLY_NAME_HEIGHT - layout.MEETINGSTONE_READ_ONLY_SECTION_GAP

    local configSection = AddMeetingStoneReadOnlySection(readOnlyInfoFrame, readOnlyY, layout.MEETINGSTONE_READ_ONLY_CONFIG_HEIGHT)
    local configTitle = AddMeetingStoneReadOnlySectionTitle(configSection, TRANSLATE("队伍配置"))

    local roleSlots = {}
    for index = 1, 5 do
        roleSlots[index] = AddMeetingStoneReadOnlyRoleSlot(configSection, index, layout.MEETINGSTONE_READ_ONLY_SECTION_WIDTH)
    end

    configSection.mkReadOnlyKind = "config"
    configSection.mkReadOnlyTitle = configTitle
    configSection.mkReadOnlyRoleSlots = roleSlots
    readOnlyDividers[3] = AddMeetingStoneReadOnlyDivider(
        readOnlyInfoFrame,
        readOnlyY - layout.MEETINGSTONE_READ_ONLY_CONFIG_HEIGHT - (layout.MEETINGSTONE_READ_ONLY_SECTION_GAP / 2)
    )
    readOnlyY = readOnlyY - layout.MEETINGSTONE_READ_ONLY_CONFIG_HEIGHT - layout.MEETINGSTONE_READ_ONLY_SECTION_GAP

    local requirementSection = AddMeetingStoneReadOnlySection(readOnlyInfoFrame, readOnlyY, layout.MEETINGSTONE_READ_ONLY_REQUIREMENT_HEIGHT)
    local requirementTitle = AddMeetingStoneReadOnlySectionTitle(requirementSection, TRANSLATE("队伍需求"))
    local itemLevelRow, itemLevelText = AddMeetingStoneReadOnlyRequirementRow(
        requirementSection,
        requirementTitle,
        TRANSLATE("最低装等："),
        -MEETINGSTONE_READ_ONLY_REQUIREMENT_FIRST_ROW_GAP
    )
    local scoreRow, scoreText = AddMeetingStoneReadOnlyRequirementRow(
        requirementSection,
        itemLevelRow,
        TRANSLATE("最低分数："),
        -MEETINGSTONE_READ_ONLY_REQUIREMENT_ROW_GAP
    )
    requirementSection.mkReadOnlyKind = "requirement"
    requirementSection.mkReadOnlyTitle = requirementTitle
    requirementSection.mkReadOnlyItemLevelRow = itemLevelRow
    requirementSection.mkReadOnlyScoreRow = scoreRow

    readOnlyInfoFrame.mkReadOnlyLayout = {
        sections = {
            dungeonSection,
            nameSection,
            configSection,
            requirementSection,
        },
        dividers = readOnlyDividers,
    }
    readOnlyInfoFrame:SetScript("OnSizeChanged", LayoutMeetingStoneReadOnlyInfoFrame)
    readOnlyInfoFrame:HookScript("OnShow", LayoutMeetingStoneReadOnlyInfoFrame)
    LayoutMeetingStoneReadOnlyInfoFrame(readOnlyInfoFrame)

    local publishButton = CreateMeetingStoneSystemButton(
        MSLeftPanel,
        "MKMeetingStonePublishButton",
        layout.MEETINGSTONE_FORM_BUTTON_WIDTH,
        layout.MEETINGSTONE_FORM_BUTTON_HEIGHT,
        TRANSLATE("创建招募"),
        layout.MEETINGSTONE_ACTION_BUTTON_TEXT_OFFSET_Y
    )
    publishButton:SetPoint("BOTTOMLEFT", MSLeftPanel, "BOTTOMLEFT", layout.MEETINGSTONE_FORM_INSET_X, layout.MEETINGSTONE_FORM_BUTTON_BOTTOM_OFFSET)
    publishButton:SetScript("OnClick", function()
        local ok, status = owner:PublishMeetingStoneListing()
        if not ok and owner.Print then
            if status == TRANSLATE(layout.MEETINGSTONE_MISSING_ADDON_MESSAGE_KEY) then
                owner:Print(status)
            else
                local failureMessage
                if owner.SetMeetingStonePublishFailureMessage then
                    failureMessage = owner:SetMeetingStonePublishFailureMessage(status)
                elseif owner.FormatMeetingStonePublishFailureMessage then
                    failureMessage = owner:FormatMeetingStonePublishFailureMessage(status)
                else
                    failureMessage = string.format(TRANSLATE("集合石发布失败：%s"), tostring(status or TRANSLATE("未知错误")))
                end
                owner:Print(failureMessage)
            end
        elseif status == "updated" and owner.Print then
            owner:Print(TRANSLATE("集合石招募已更新。"))
        elseif status == "pending" and owner.Print then
            owner:Print(TRANSLATE("集合石发布请求已发送，等待系统确认。"))
        elseif ok and owner.Print then
            owner:Print(TRANSLATE("集合石发布请求已发送。"))
        end
        owner:RefreshMeetingStoneActionButtons()
        if owner.RefreshMainWindow then
            owner:RefreshMainWindow("MeetingStonePublish")
        end
    end)

    local disbandButton = CreateMeetingStoneSystemButton(
        MSLeftPanel,
        "MKMeetingStoneDisbandButton",
        layout.MEETINGSTONE_FORM_BUTTON_WIDTH,
        layout.MEETINGSTONE_FORM_BUTTON_HEIGHT,
        TRANSLATE("解散招募"),
        layout.MEETINGSTONE_ACTION_BUTTON_TEXT_OFFSET_Y
    )
    disbandButton:SetPoint("LEFT", publishButton, "RIGHT", layout.MEETINGSTONE_FORM_BUTTON_GAP, 0)
    disbandButton:SetScript("OnClick", function()
        local hasActiveEntry = false
        if C_LFGList and C_LFGList.HasActiveEntryInfo then
            local ok, activeEntry = pcall(C_LFGList.HasActiveEntryInfo)
            hasActiveEntry = ok and activeEntry == true
        end
        if C_LFGList and C_LFGList.RemoveListing and CanManageMeetingStoneListing() and hasActiveEntry then
            local ok, reason = pcall(C_LFGList.RemoveListing)
            if not ok then
                if owner.Print then
                    owner:Print(string.format(TRANSLATE("集合石解散失败：%s"), tostring(reason or TRANSLATE("未知错误"))))
                end
                return
            end
            if owner.MarkMeetingStoneListingRemovedLocally then
                owner:MarkMeetingStoneListingRemovedLocally()
            end
            owner:RefreshMeetingStoneActionButtons()
            if owner.RefreshMainWindow then
                owner:RefreshMainWindow("MeetingStoneDisband")
            end
        end
    end)

    local leftBottomGradient = MSLeftPanel:CreateTexture(nil, "OVERLAY")
    leftBottomGradient:SetPoint("TOPLEFT", MSLeftPanel, "BOTTOMLEFT", 0, layout.MEETINGSTONE_LEFT_BOTTOM_GRADIENT_TOP_OFFSET)
    leftBottomGradient:SetPoint("BOTTOMRIGHT", MSLeftPanel, "BOTTOMRIGHT", 0, 0)
    if TrySetMeetingStoneAtlas(leftBottomGradient, "QuestLog-Frame-Gradient-bottom", false) then
        leftBottomGradient:SetAlpha(0.75)
    else
        leftBottomGradient:Hide()
    end
    MSLeftPanel.BottomGradient = leftBottomGradient

    local MSRightPanel = CreateFrame("Frame", "MSRightPanel", container, "BackdropTemplate")
    MSRightPanel:SetPoint("TOPLEFT", MSLeftPanel, "TOPRIGHT", layout.MEETINGSTONE_RIGHT_PANEL_OVERLAP, 0)
    MSRightPanel:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", 0, 0)

    local panelDivider = CreateFrame("Frame", nil, container)
    panelDivider:SetPoint("TOP", MSLeftPanel, "TOPRIGHT", layout.MEETINGSTONE_PANEL_DIVIDER_OFFSET_X, -layout.MEETINGSTONE_PANEL_DIVIDER_TOP_OFFSET)
    panelDivider:SetPoint("BOTTOM", MSLeftPanel, "BOTTOMRIGHT", layout.MEETINGSTONE_PANEL_DIVIDER_OFFSET_X, layout.MEETINGSTONE_PANEL_DIVIDER_BOTTOM_OFFSET)
    panelDivider:SetWidth(layout.MEETINGSTONE_PANEL_DIVIDER_WIDTH)
    panelDivider:SetFrameLevel(math.max(MSLeftPanel:GetFrameLevel(), MSRightPanel:GetFrameLevel()) + 2)

    local dividerLeftShadow = panelDivider:CreateTexture(nil, "ARTWORK", nil, 1)
    dividerLeftShadow:SetPoint("TOPLEFT", panelDivider, "TOPLEFT", 0, 0)
    dividerLeftShadow:SetPoint("BOTTOMLEFT", panelDivider, "BOTTOMLEFT", 0, 0)
    dividerLeftShadow:SetWidth(2)
    dividerLeftShadow:SetTexture("Interface\\Buttons\\WHITE8X8")
    dividerLeftShadow:SetVertexColor(
        layout.MEETINGSTONE_PANEL_DIVIDER_SHADOW_COLOR[1],
        layout.MEETINGSTONE_PANEL_DIVIDER_SHADOW_COLOR[2],
        layout.MEETINGSTONE_PANEL_DIVIDER_SHADOW_COLOR[3],
        layout.MEETINGSTONE_PANEL_DIVIDER_SHADOW_COLOR[4]
    )

    local dividerCenter = panelDivider:CreateTexture(nil, "ARTWORK", nil, 2)
    dividerCenter:SetPoint("TOPLEFT", dividerLeftShadow, "TOPRIGHT", 0, 0)
    dividerCenter:SetPoint("BOTTOMRIGHT", panelDivider, "BOTTOMRIGHT", -2, 0)
    dividerCenter:SetTexture("Interface\\Buttons\\WHITE8X8")
    dividerCenter:SetVertexColor(
        layout.MEETINGSTONE_PANEL_DIVIDER_COLOR[1],
        layout.MEETINGSTONE_PANEL_DIVIDER_COLOR[2],
        layout.MEETINGSTONE_PANEL_DIVIDER_COLOR[3],
        layout.MEETINGSTONE_PANEL_DIVIDER_COLOR[4]
    )

    local dividerHighlight = panelDivider:CreateTexture(nil, "ARTWORK", nil, 3)
    dividerHighlight:SetPoint("TOPLEFT", dividerCenter, "TOPLEFT", 0, 0)
    dividerHighlight:SetPoint("BOTTOMLEFT", dividerCenter, "BOTTOMLEFT", 0, 0)
    dividerHighlight:SetWidth(1)
    dividerHighlight:SetTexture("Interface\\Buttons\\WHITE8X8")
    dividerHighlight:SetVertexColor(
        layout.MEETINGSTONE_PANEL_DIVIDER_HIGHLIGHT_COLOR[1],
        layout.MEETINGSTONE_PANEL_DIVIDER_HIGHLIGHT_COLOR[2],
        layout.MEETINGSTONE_PANEL_DIVIDER_HIGHLIGHT_COLOR[3],
        layout.MEETINGSTONE_PANEL_DIVIDER_HIGHLIGHT_COLOR[4]
    )

    local dividerRightShadow = panelDivider:CreateTexture(nil, "ARTWORK", nil, 1)
    dividerRightShadow:SetPoint("TOPRIGHT", panelDivider, "TOPRIGHT", 0, 0)
    dividerRightShadow:SetPoint("BOTTOMRIGHT", panelDivider, "BOTTOMRIGHT", 0, 0)
    dividerRightShadow:SetWidth(2)
    dividerRightShadow:SetTexture("Interface\\Buttons\\WHITE8X8")
    dividerRightShadow:SetVertexColor(
        layout.MEETINGSTONE_PANEL_DIVIDER_SHADOW_COLOR[1],
        layout.MEETINGSTONE_PANEL_DIVIDER_SHADOW_COLOR[2],
        layout.MEETINGSTONE_PANEL_DIVIDER_SHADOW_COLOR[3],
        layout.MEETINGSTONE_PANEL_DIVIDER_SHADOW_COLOR[4]
    )

    panelDivider.LeftShadow = dividerLeftShadow
    panelDivider.Center = dividerCenter
    panelDivider.Highlight = dividerHighlight
    panelDivider.RightShadow = dividerRightShadow
    panelDivider.CenterAccent = CreateMeetingStonePanelCenterAccent(panelDivider)
    container.MSPanelDivider = panelDivider

    local rightBackground = MSRightPanel:CreateTexture(nil, "BACKGROUND")
    rightBackground:SetAllPoints(MSRightPanel)
    rightBackground:SetTexture("Interface\\Buttons\\WHITE8X8")
    rightBackground:SetVertexColor(0, 0, 0, 1)
    rightBackground:Hide()
    MSRightPanel.Background = rightBackground

    local rightHeaderBackground = MSRightPanel:CreateTexture(nil, "BACKGROUND", nil, 1)
    rightHeaderBackground:SetPoint("TOPLEFT", MSRightPanel, "TOPLEFT", 0, layout.MEETINGSTONE_HEADER_TOP_OFFSET)
    rightHeaderBackground:SetPoint("BOTTOMRIGHT", MSRightPanel, "TOPRIGHT", 0, layout.MEETINGSTONE_HEADER_BOTTOM_OFFSET)
    TrySetMeetingStoneAtlas(rightHeaderBackground, "housefinder_header-bg-gradient", false)
    MSRightPanel.HeaderBackground = rightHeaderBackground

    local rightHeaderFrame = core.MeetingStoneApplicantList.CreateHeader(MSRightPanel, owner)
    rightHeaderFrame:SetPoint("TOPLEFT", rightHeaderBackground, "TOPLEFT", layout.MEETINGSTONE_HEADER_CONTENT_INSET_X, layout.MEETINGSTONE_HEADER_CONTENT_OFFSET_Y)
    rightHeaderFrame:SetPoint("BOTTOMRIGHT", rightHeaderBackground, "BOTTOMRIGHT", -layout.MEETINGSTONE_HEADER_CONTENT_INSET_X, layout.MEETINGSTONE_HEADER_CONTENT_OFFSET_Y)
    rightHeaderFrame:SetHeight(math.abs(layout.MEETINGSTONE_HEADER_BOTTOM_OFFSET - layout.MEETINGSTONE_HEADER_TOP_OFFSET))
    rightHeaderFrame:SetFrameLevel(MSRightPanel:GetFrameLevel() + 3)
    rightHeaderFrame:Show()
    core.MeetingStoneApplicantList.ApplyHeaderLayout(rightHeaderFrame, math.max((MSRightPanel:GetWidth() or 0) - (layout.MEETINGSTONE_HEADER_CONTENT_INSET_X * 2), 1))
    MSRightPanel.HeaderFrame = rightHeaderFrame

    local applicantScrollFrame
    MSRightPanel:SetScript("OnSizeChanged", function(panel)
        if panel.HeaderFrame then
            local headerWidth = applicantScrollFrame and applicantScrollFrame.GetWidth and applicantScrollFrame:GetWidth()
            if not headerWidth or headerWidth <= 0 then
                headerWidth = (panel:GetWidth() or 0) - (layout.MEETINGSTONE_HEADER_CONTENT_INSET_X * 2)
            end
            core.MeetingStoneApplicantList.ApplyHeaderLayout(panel.HeaderFrame, math.max(headerWidth - 12, 1))
        end
    end)

    local applicantRowsFrame, applicantScrollBar
    applicantScrollFrame, applicantRowsFrame, applicantScrollBar = CreateMeetingStoneApplicantScrollContainer(MSRightPanel)
    applicantScrollFrame:SetPoint("TOPLEFT", rightHeaderBackground, "BOTTOMLEFT", 0, -4)
    applicantScrollFrame:SetPoint(
        "BOTTOMRIGHT",
        MSRightPanel,
        "BOTTOMRIGHT",
        -(layout.MEETINGSTONE_APPLICANT_SCROLLBAR_WIDTH + layout.MEETINGSTONE_APPLICANT_SCROLLBAR_GAP + layout.MEETINGSTONE_APPLICANT_SCROLLBAR_RIGHT_INSET),
        layout.MEETINGSTONE_APPLICANT_SCROLL_BOTTOM_INSET
    )
    applicantScrollFrame:SetFrameLevel(MSRightPanel:GetFrameLevel() + 2)
    applicantRowsFrame:SetFrameLevel(applicantScrollFrame:GetFrameLevel() + 1)
    applicantScrollBar:SetFrameLevel(applicantScrollFrame:GetFrameLevel() + 2)
    applicantScrollBar:SetPoint("TOPLEFT", applicantScrollFrame, "TOPRIGHT", layout.MEETINGSTONE_APPLICANT_SCROLLBAR_GAP, -2)
    applicantScrollBar:SetPoint("BOTTOMLEFT", applicantScrollFrame, "BOTTOMRIGHT", layout.MEETINGSTONE_APPLICANT_SCROLLBAR_GAP, 2)
    MSRightPanel.ApplicantRowsFrame = applicantRowsFrame

    local applicantEmpty = applicantRowsFrame:CreateFontString(nil, "OVERLAY", "GameFontDisable")
    applicantEmpty:SetPoint("CENTER", applicantRowsFrame, "CENTER", 0, 0)
    applicantEmpty:SetJustifyH("CENTER")
    applicantEmpty:SetJustifyV("MIDDLE")
    applicantEmpty:SetText(TRANSLATE("请创建集合石招募"))
    applicantEmpty:Hide()
    if core.ApplyPanelEmptyPromptStyle then
        core.ApplyPanelEmptyPromptStyle(applicantEmpty)
    end
    MSRightPanel.ApplicantEmpty = applicantEmpty

    local topRightFiligree = MSRightPanel:CreateTexture(nil, "ARTWORK", nil, 5)
    topRightFiligree:SetPoint("TOPRIGHT", MSRightPanel, "TOPRIGHT", layout.MEETINGSTONE_TOP_RIGHT_FILIGREE_OFFSET_X, layout.MEETINGSTONE_TOP_RIGHT_FILIGREE_OFFSET_Y)
    TrySetMeetingStoneAtlas(topRightFiligree, "housing-dashboard-filigree-wood-small-TR", true)
    MSRightPanel.TopRightFiligree = topRightFiligree

    local bottomRightFiligree = MSRightPanel:CreateTexture(nil, "ARTWORK", nil, 5)
    bottomRightFiligree:SetPoint("BOTTOMRIGHT", MSRightPanel, "BOTTOMRIGHT", layout.MEETINGSTONE_BOTTOM_RIGHT_FILIGREE_OFFSET_X, layout.MEETINGSTONE_BOTTOM_RIGHT_FILIGREE_OFFSET_Y)
    TrySetMeetingStoneAtlas(bottomRightFiligree, "housing-dashboard-filigree-wood-small-BR", true)
    MSRightPanel.BottomRightFiligree = bottomRightFiligree

    owner.widgets = {
        tabGroup = owner.widgets and owner.widgets.tabGroup or tabGroup,
        root = container,
        MSLeftPanel = MSLeftPanel,
        MSRightPanel = MSRightPanel,
        meetingStoneLeftHeaderTitle = leftHeaderTitle,
        meetingStoneDungeonLabel = dungeonLabel,
        meetingStoneDungeonDropdown = dungeonDropdown,
        meetingStoneTitleLabel = titleLabel,
        meetingStoneTitleHolder = titleHolder,
        meetingStoneTitleBox = titleBox,
        meetingStoneFallbackTitleBox = fallbackTitleBox,
        meetingStoneTitleDisabledPlaceholderOverlay = titleDisabledPlaceholderOverlay,
        meetingStoneSummaryLabel = summaryLabel,
        meetingStoneSummaryHolder = summaryHolder,
        meetingStoneSummaryFrame = summaryFrame,
        meetingStoneSummaryBox = summaryBox,
        meetingStoneFallbackSummaryFrame = fallbackSummaryFrame,
        meetingStoneFallbackSummaryBox = fallbackSummaryBox,
        meetingStoneSummaryDisabledPlaceholderOverlay = summaryDisabledPlaceholderOverlay,
        meetingStonePlayStyleLabel = playStyleLabel,
        meetingStonePlayStyleDropdown = playStyleDropdown,
        meetingStoneItemLevelLabel = itemLevelLabel,
        meetingStoneItemLevelBox = itemLevelBox,
        meetingStoneScoreLabel = scoreLabel,
        meetingStoneScoreBox = scoreBox,
        meetingStoneFormWidgets = {
            dungeonLabel,
            dungeonDropdown,
            titleLabel,
            titleHolder,
            titleDisabledPlaceholderOverlay,
            summaryLabel,
            summaryHolder,
            summaryDisabledPlaceholderOverlay,
            playStyleLabel,
            playStyleDropdown,
            itemLevelLabel,
            itemLevelBox,
            scoreLabel,
            scoreBox,
            publishButton,
            disbandButton,
        },
        meetingStoneReadOnlyInfoFrame = readOnlyInfoFrame,
        meetingStoneReadOnlyDungeonText = dungeonText,
        meetingStoneReadOnlyTitleText = titleText,
        meetingStoneReadOnlyRoleSlots = roleSlots,
        meetingStoneReadOnlyItemLevelText = itemLevelText,
        meetingStoneReadOnlyScoreText = scoreText,
        meetingStonePublishButton = publishButton,
        meetingStoneDisbandButton = disbandButton,
        meetingStoneApplicantHeader = rightHeaderFrame,
        meetingStoneApplicantScrollFrame = applicantScrollFrame,
        meetingStoneApplicantRowsFrame = applicantRowsFrame,
        meetingStoneApplicantScrollBar = applicantScrollBar,
        meetingStoneApplicantEmpty = applicantEmpty,
        meetingStoneApplicantRows = {},
        skipApplyFontToFrame = true,
    }
    owner.tabViews.meetingstone = owner.widgets
    container:HookScript("OnShow", function()
        owner:AttachMeetingStoneNativeInputControls(owner.tabViews and owner.tabViews.meetingstone)
    end)
    container:HookScript("OnHide", function()
        owner:DetachMeetingStoneNativeInputControls()
    end)
    owner:AttachMeetingStoneNativeInputControls(owner.widgets)
    owner.mkBuildingMeetingStoneTab = nil
    owner:RefreshMeetingStoneTab(owner.widgets)

    if core.ApplyThemeToTabGroup then
        core.ApplyThemeToTabGroup(tabGroup)
    end
    owner:RefreshMainWindow("BuildSelectedTab", "meetingstone")
end
