-- Portal Authority — options/panel_announcements.lua
-- Announcements, summon request, and utility-alert settings panel builders.
--
-- Depends on: PortalAuthority.OptionsInternal shared helpers; PortalAuthority
-- Exposes:    PortalAuthority.OptionsInternal.createAnnouncementsPanelLegacy; PortalAuthority.OptionsInternal.createAnnouncementsPanel
-- DB keys:    announce*, portals*, teleports*, mplus*, warlockSummoning*, summonRequest*, summonStoneRequest*
-- Events:     none
-- Search key: announcements

PortalAuthority = PortalAuthority or {}

if PortalAuthority.IsSettingsBaselineGateEnabled and PortalAuthority:IsSettingsBaselineGateEnabled() then
    return
end

if not Settings or not Settings.RegisterCanvasLayoutCategory then
    return
end

PortalAuthority.OptionsInternal = PortalAuthority.OptionsInternal or {}
local OptionsInternal = PortalAuthority.OptionsInternal

local settingsSearchRuntime = OptionsInternal.SearchRuntime
local trim = OptionsInternal.trim
local rgba255 = OptionsInternal.rgba255
local unpackColor = OptionsInternal.unpackColor
local applyGlobalFontToObject = OptionsInternal.applyGlobalFontToObject
local getPortalMedia = OptionsInternal.getPortalMedia
local normalizeSoundChoiceValue = OptionsInternal.normalizeSoundChoiceValue
local compareChoiceLabels = OptionsInternal.compareChoiceLabels
local normalizeChoicePathKey = OptionsInternal.normalizeChoicePathKey
local resolveFontChoicePath = OptionsInternal.resolveFontChoicePath
local getSoundChoiceValueKey = OptionsInternal.getSoundChoiceValueKey
local buildFallbackSoundChoices = OptionsInternal.buildFallbackSoundChoices
local getSummonSoundChoices = OptionsInternal.getSummonSoundChoices
local getAvailableFontChoices = OptionsInternal.getAvailableFontChoices
local buildSharedFontPickerChoices = OptionsInternal.buildSharedFontPickerChoices
local SUMMON_SOUND_CHANNEL_CHOICES = OptionsInternal.SUMMON_SOUND_CHANNEL_CHOICES
local debugSettingsLog = OptionsInternal.debugSettingsLog or function() end
local FLAT_SETTINGS_BACKDROP = OptionsInternal.FLAT_SETTINGS_BACKDROP
local SETTINGS_WINDOW_HOSTED_TOP_RHYTHM_NUDGE = OptionsInternal.SETTINGS_WINDOW_HOSTED_TOP_RHYTHM_NUDGE
local SETTINGS_WINDOW_SCROLLBAR_WIDTH = OptionsInternal.SETTINGS_WINDOW_SCROLLBAR_WIDTH
local SETTINGS_WINDOW_SCROLLBAR_GAP = OptionsInternal.SETTINGS_WINDOW_SCROLLBAR_GAP
local SETTINGS_WINDOW_SCROLLBAR_MIN_THUMB_HEIGHT = OptionsInternal.SETTINGS_WINDOW_SCROLLBAR_MIN_THUMB_HEIGHT
local SETTINGS_WINDOW_HOST_COLORS = OptionsInternal.SETTINGS_WINDOW_HOST_COLORS
local SETTINGS_WINDOW_SCROLLBAR_TRACK = OptionsInternal.SETTINGS_WINDOW_SCROLLBAR_TRACK
local SETTINGS_WINDOW_SCROLLBAR_THUMB = OptionsInternal.SETTINGS_WINDOW_SCROLLBAR_THUMB
local SETTINGS_WINDOW_SCROLLBAR_THUMB_HOVER = OptionsInternal.SETTINGS_WINDOW_SCROLLBAR_THUMB_HOVER
local frameIsAlive = OptionsInternal.frameIsAlive
local isDescendedFromFrame = OptionsInternal.isDescendedFromFrame
local markSettingsWindowVisualOptIn = OptionsInternal.markSettingsWindowVisualOptIn
local isUsingCustomSettingsWindowVisualStyle = OptionsInternal.isUsingCustomSettingsWindowVisualStyle
local registerSettingsWindowVisualRefresh = OptionsInternal.registerSettingsWindowVisualRefresh
local setTextureSolidColor = OptionsInternal.setTextureSolidColor
local hideDefaultScrollBarPart = OptionsInternal.hideDefaultScrollBarPart
local getTemplateScrollBar = OptionsInternal.getTemplateScrollBar
local hideDefaultScrollBarChrome = OptionsInternal.hideDefaultScrollBarChrome
local refreshCustomSettingsScrollBarThumb = OptionsInternal.refreshCustomSettingsScrollBarThumb
local settingsScrollRuntime = PortalAuthority._settingsScrollRuntime or OptionsInternal.settingsScrollRuntime
local createStyledFontString = OptionsInternal.createStyledFontString
local createTopRightHeader = OptionsInternal.createTopRightHeader
local PA_SafeBool = OptionsInternal.PA_SafeBool
local getClassHex = OptionsInternal.getClassHex
local createCheckbox = OptionsInternal.createCheckbox
local createActionButton = OptionsInternal.createActionButton
local styleSecondaryActionButtonShared = OptionsInternal.styleSecondaryActionButtonShared
local createNumeric = OptionsInternal.createNumeric
local createDropdown = OptionsInternal.createDropdown
local createDynamicDropdown = OptionsInternal.createDynamicDropdown
local createPremiumSegmentedChoice = OptionsInternal.createPremiumSegmentedChoice
local CreatePicker = OptionsInternal.CreatePicker
local CreateStepperControl = OptionsInternal.CreateStepperControl
local createCanonicalDockFontSizeStepperControl = OptionsInternal.CreateStepperControl or OptionsInternal.createCanonicalDockFontSizeStepperControl
local createTextPlacement = OptionsInternal.createTextPlacement
local setSettingsControlEnabled = OptionsInternal.setSettingsControlEnabled
local setSettingsTextEnabled = OptionsInternal.setSettingsTextEnabled
local applySettingsWindowSelectButtonChrome = OptionsInternal.applySettingsWindowSelectButtonChrome
local createScrollableChild = OptionsInternal.createScrollableChild
local updateScrollBehavior = OptionsInternal.updateScrollBehavior
local withPanelHydration = OptionsInternal.withPanelHydration
local panelHasSizedViewport = OptionsInternal.panelHasSizedViewport
local ensureSettingsCanvasPanelAnchored = OptionsInternal.ensureSettingsCanvasPanelAnchored
local setSettingsWindowChromeVisible = OptionsInternal.setSettingsWindowChromeVisible
local handleSettingsFirstOpenSizeReady = OptionsInternal.handleSettingsFirstOpenSizeReady
local handleSettingsFirstOpenPanelShow = OptionsInternal.handleSettingsFirstOpenPanelShow
local handleSettingsFirstOpenPanelHide = OptionsInternal.handleSettingsFirstOpenPanelHide
local shouldSuppressSettingsFirstOpenExternalRefresh = OptionsInternal.shouldSuppressSettingsFirstOpenExternalRefresh
local isPanelUsingCustomSettingsWindowHost = OptionsInternal.isPanelUsingCustomSettingsWindowHost
local refreshPanel = OptionsInternal.refreshPanel
local createScrollableTextArea = OptionsInternal.createScrollableTextArea
local createSettingsSectionDescriptor = OptionsInternal.createSettingsSectionDescriptor
local layoutSettingsSectionDescriptor = OptionsInternal.layoutSettingsSectionDescriptor
local registerRefreshCallback = OptionsInternal.registerRefreshCallback
local ensureDatabase = OptionsInternal.ensureDatabase
local IsPointerInsideActivePickerSafeZone = OptionsInternal.IsPointerInsideActivePickerSafeZone
local CloseActivePortalPicker = OptionsInternal.CloseActivePortalPicker
local settingsFirstOpenHost = OptionsInternal.settingsFirstOpenHost
local resetSettingsFirstOpenPanelState = OptionsInternal.resetSettingsFirstOpenPanelState

local function createAnnouncementsPanelLegacy()
    local panel = CreateFrame("Frame")
    panel.name = "Announcements"
    debugSettingsLog("create frame:", panel.name)

    panel.EnsureBuilt = function(self)
        debugSettingsLog("EnsureBuilt:", self.name, "initialized=", self._initialized and "true" or "false")
        if self._initialized then
            return
        end
        self._initialized = true
        ensureDatabase()

        local scrollFrame, scrollChild = createScrollableChild(self)
        self._sizingFrame = scrollFrame
        local widgets = {}

        local function helper(text, r, g, b)
            local fs = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 11)
            fs:SetText(text)
            fs:SetJustifyH("LEFT")
            fs:SetTextColor(r or 0.8, g or 0.8, b or 0.8)
            return fs
        end

        local function sectionHeader(text)
            local fs = createStyledFontString(scrollChild, "ARTWORK", "GameFontNormalLarge", 14)
            fs:SetText(string.format("|cffffd100%s|r", text))
            return fs
        end

        widgets.playerHeader = createTopRightHeader(self, -PAGE.PLAYER_HEADER_PAD)
        widgets.header = sectionHeader("Announcements")
        widgets.sectionDescriptor = createSettingsSectionDescriptor(scrollChild, "Customize your utility alerts.")
        widgets.routingHint = helper("Routing: RAID in raids, PARTY in parties.")

        local function createMessageBlock(titleText, enabledKey, textKey, defaultText, showDestinationHelper)
            local block = {}
            block.title = createStyledFontString(scrollChild, "ARTWORK", "GameFontNormal", 12)
            block.title:SetText(titleText)
            block.enabled = createCheckbox(scrollChild, "Enable", enabledKey)
            block.messageLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
            block.messageLabel:SetText("Message")
            block.edit = CreateFrame("EditBox", nil, scrollChild, "InputBoxTemplate")
            block.edit:SetAutoFocus(false)
            block.edit:SetTextInsets(8, 8, 0, 0)
            block.edit:SetText(PortalAuthorityDB[textKey])
            applyGlobalFontToObject(block.edit, 12)

            local function save(box)
                local text = trim(box:GetText())
                if text == "" then text = defaultText end
                PortalAuthorityDB[textKey] = text
                box:SetText(text)
            end
            block.edit:SetScript("OnEnterPressed", function(box) save(box); box:ClearFocus() end)
            block.edit:SetScript("OnEditFocusLost", save)

            if showDestinationHelper then
                block.destinationHint = helper("Use [destination] in your message to insert the destination name.", 1, 0.35, 0.35)
            end

            return block
        end

        local function registerAnnouncementToggleBlockSearch(block)
            if type(block) ~= "table" or not frameIsAlive(block.title) or not frameIsAlive(block.enabled) then
                return nil
            end

            if frameIsAlive(block.enabled.text) then
                settingsSearchRuntime.markSearchTextOrigin(block.enabled.text, "choice_caption", {
                    searchChoiceLabelMode = "suppress_generic",
                })
            end

            local extraRefs = {}
            local function addExtraRef(ref)
                if frameIsAlive(ref) then
                    extraRefs[#extraRefs + 1] = ref
                end
            end

            addExtraRef(block.description)
            addExtraRef(block.destinationHint)
            return settingsSearchRuntime.registerControlText(block.title, block.enabled, "checkbox", extraRefs)
        end

        local function createSummonRequestsBlock()
            local block = {}
            block.title = createStyledFontString(scrollChild, "ARTWORK", "GameFontNormal", 12)
            local warlockOnlyLabel = string.format("|c%sWarlock only|r", getClassHex("WARLOCK"))
            block.title:SetText(string.format("Summon Requests (%s)", warlockOnlyLabel))
            block.enabled = createCheckbox(scrollChild, "Enable", "summonRequestsEnabled")
            block.description = helper("Popup alert for Party, Raid, Whisper, and Battle.net summon asks (e.g., '1', 'sum', 'summon', '123 plz'). It is flexible in understanding intent.", 0.72, 0.72, 0.72)
            block.soundChannel = createDropdown(scrollChild, "Channel |cff7f7f7f(recommended: Master)|r", SUMMON_SOUND_CHANNEL_CHOICES, "summonRequestSoundChannel")
            block.sound = createDynamicDropdown(
                scrollChild,
                "Alert sound |cff7f7f7f(select NONE for visual only)|r",
                function()
                    return getSummonSoundChoices(PortalAuthorityDB and PortalAuthorityDB.summonRequestSound or "")
                end,
                function()
                    return normalizeSoundChoiceValue(PortalAuthorityDB and PortalAuthorityDB.summonRequestSound or "")
                end,
                function(value)
                    PortalAuthorityDB.summonRequestSound = value
                end
            )
            return block
        end

        local function createSummonStoneRequestsBlock()
            local block = {}
            block.title = createStyledFontString(scrollChild, "ARTWORK", "GameFontNormal", 12)
            local warlockOnlyLabel = string.format("|c%sWarlock only|r", getClassHex("WARLOCK"))
            block.title:SetText(string.format("Summon Stone Requests (%s)", warlockOnlyLabel))
            block.enabled = createCheckbox(scrollChild, "Enable", "summonStoneRequestsEnabled")
            block.description = helper("Popup alert for Party, Raid, Whisper, and Battle.net closet requests stone (e.g., 'closet','stone plz').", 0.72, 0.72, 0.72)
            block.soundChannel = createDropdown(scrollChild, "Channel |cff7f7f7f(recommended: Master)|r", SUMMON_SOUND_CHANNEL_CHOICES, "summonStoneRequestSoundChannel")
            block.sound = createDynamicDropdown(
                scrollChild,
                "Alert sound |cff7f7f7f(select NONE for visual only)|r",
                function()
                    return getSummonSoundChoices(PortalAuthorityDB and PortalAuthorityDB.summonStoneRequestSound or "")
                end,
                function()
                    return normalizeSoundChoiceValue(PortalAuthorityDB and PortalAuthorityDB.summonStoneRequestSound or "")
                end,
                function(value)
                    PortalAuthorityDB.summonStoneRequestSound = value
                end
            )
            return block
        end

        widgets.portals = createMessageBlock("Portals", "portalsEnabled", "portalsText", PortalAuthority.defaults.portalsText, true)
        widgets.teleports = createMessageBlock("Teleports", "teleportsEnabled", "teleportsText", PortalAuthority.defaults.teleportsText, true)
        widgets.mplus = createMessageBlock("Mythic+ Teleports", "mplusEnabled", "mplusText", PortalAuthority.defaults.mplusText, true)
        widgets.warlockSummoning = createMessageBlock("Summoning Stone", "warlockSummoningEnabled", "warlockSummoningText", PortalAuthority.defaults.warlockSummoningText)
        if widgets.warlockSummoning and widgets.warlockSummoning.messageLabel then
            widgets.warlockSummoning.messageLabel:SetText("Message")
        end
        widgets.summonRequests = createSummonRequestsBlock()
        widgets.summonStoneRequests = createSummonStoneRequestsBlock()
        registerAnnouncementToggleBlockSearch(widgets.portals)
        registerAnnouncementToggleBlockSearch(widgets.teleports)
        registerAnnouncementToggleBlockSearch(widgets.mplus)
        registerAnnouncementToggleBlockSearch(widgets.warlockSummoning)
        registerAnnouncementToggleBlockSearch(widgets.summonRequests)
        registerAnnouncementToggleBlockSearch(widgets.summonStoneRequests)

        local function layoutCheckboxRow(check, y, checkX, labelX)
            check:ClearAllPoints()
            check:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", checkX, y)
            check.text:ClearAllPoints()
            check.text:SetPoint("LEFT", check, "RIGHT", 10, 0)
            check.text:SetJustifyH("LEFT")
        end

        local function layoutMessageBlock(block, y, contentW, left, right, checkX, labelX, controlX)
            block.title:ClearAllPoints()
            block.title:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", left, y)
            y = y - 18

            layoutCheckboxRow(block.enabled, y, checkX, labelX)

            block.messageLabel:ClearAllPoints()
            block.messageLabel:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", controlX, y)

            y = y - 16
            block.edit:ClearAllPoints()
            block.edit:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", controlX, y)
            block.edit:SetSize(contentW - right - controlX, 22)

            y = y - 27
            if block.destinationHint then
                block.destinationHint:ClearAllPoints()
                block.destinationHint:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", controlX, y)
                block.destinationHint:SetWidth(contentW - right - controlX)
                y = y - 18
            end

            return y - 4
        end

        local function layoutInlineDropdown(spec, x, y, width)
            spec.label:ClearAllPoints()
            spec.label:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", x, y)
            spec.control:ClearAllPoints()
            spec.control:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", x - 20, y - 14)
            UIDropDownMenu_SetWidth(spec.control, width)
        end

        local function layoutSummonRequestsBlock(block, y, contentW, left, right, checkX, labelX, controlX)
            block.title:ClearAllPoints()
            block.title:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", left, y)
            y = y - 18

            layoutCheckboxRow(block.enabled, y, checkX, labelX)

            block.description:ClearAllPoints()
            block.description:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", controlX, y - 2)
            block.description:SetWidth(contentW - controlX - right)
            block.description:SetWordWrap(true)

            y = y - 30
            local gap = 20
            local dropdownWidth = math.max(120, math.floor((contentW - controlX - right - gap) / 2))
            layoutInlineDropdown(block.soundChannel, controlX, y, dropdownWidth)
            layoutInlineDropdown(block.sound, controlX + dropdownWidth + gap, y, dropdownWidth)

            return y - 54
        end

        local function relayout()
            local contentW = math.max(320, scrollFrame:GetWidth() - 24)
            local left, right = 16, 16
            local CHECK_X = left
            local CHECKBOX_WIDTH = 24
            local LABEL_X = CHECK_X + CHECKBOX_WIDTH + 10
            local CONTROL_X = LABEL_X + 88

            scrollChild:SetWidth(contentW)

            widgets.playerHeader:ClearAllPoints()
            widgets.playerHeader:SetPoint("TOPRIGHT", self, "TOPRIGHT", -20, -20)

            local y = -8
            widgets.header:ClearAllPoints(); widgets.header:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", left, y)
            y = y - 24
            y = layoutSettingsSectionDescriptor(widgets.sectionDescriptor, scrollChild, left, y, contentW - left - right)

            widgets.routingHint:ClearAllPoints(); widgets.routingHint:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", left, y)
            widgets.routingHint:SetWidth(contentW - left - right)
            y = y - 20
            y = y - 14

            y = layoutMessageBlock(widgets.portals, y, contentW, left, right, CHECK_X, LABEL_X, CONTROL_X)
            y = layoutMessageBlock(widgets.teleports, y, contentW, left, right, CHECK_X, LABEL_X, CONTROL_X)
            y = layoutMessageBlock(widgets.mplus, y, contentW, left, right, CHECK_X, LABEL_X, CONTROL_X)
            y = layoutMessageBlock(widgets.warlockSummoning, y, contentW, left, right, CHECK_X, LABEL_X, CONTROL_X)
            y = layoutSummonRequestsBlock(widgets.summonRequests, y, contentW, left, right, CHECK_X, LABEL_X, CONTROL_X)
            y = layoutSummonRequestsBlock(widgets.summonStoneRequests, y, contentW, left, right, CHECK_X, LABEL_X, CONTROL_X)

            scrollChild:SetHeight(math.max(scrollFrame:GetHeight(), -y + 20))
            updateScrollBehavior(scrollFrame, scrollChild)
        end

        self._refresh = function()
            widgets.portals.enabled:SetChecked(PortalAuthorityDB.portalsEnabled)
            widgets.teleports.enabled:SetChecked(PortalAuthorityDB.teleportsEnabled)
            widgets.mplus.enabled:SetChecked(PortalAuthorityDB.mplusEnabled)
            widgets.warlockSummoning.enabled:SetChecked(PortalAuthorityDB.warlockSummoningEnabled)
            widgets.summonRequests.enabled:SetChecked(PortalAuthorityDB.summonRequestsEnabled)
            widgets.summonRequests.soundChannel.Refresh()
            widgets.summonRequests.sound.Refresh()
            widgets.summonStoneRequests.enabled:SetChecked(PortalAuthorityDB.summonStoneRequestsEnabled)
            widgets.summonStoneRequests.soundChannel.Refresh()
            widgets.summonStoneRequests.sound.Refresh()
            relayout()
        end

        registerRefreshCallback(self._refresh, self)
        scrollFrame:SetScript("OnSizeChanged", relayout)
    end

    panel:SetScript("OnShow", function(self)
        debugSettingsLog("OnShow:", self.name)
        self:EnsureBuilt()
        refreshPanel(self)
    end)

    return panel
end

local function createAnnouncementsPanel()
    local panel = CreateFrame("Frame")
    panel.name = "Announcements"
    panel._paSearchSectionKey = "announcements"
    panel._paSearchSectionName = panel.name
    debugSettingsLog("create frame:", panel.name)
    ensureDatabase()

    panel.EnsureBuilt = function(self)
        debugSettingsLog("EnsureBuilt:", self.name, "initialized=", self._initialized and "true" or "false")
        if self._initialized then
            return
        end
        self._initialized = true
        resetSettingsFirstOpenPanelState(self)
        local panelOwner = self
        settingsSearchRuntime.beginDescriptorCapture(self)

        local scrollFrame, scrollChild = createScrollableChild(self)
        if not scrollFrame or not scrollChild then
            settingsSearchRuntime.endDescriptorCapture(self)
            return
        end
        self._sizingFrame = scrollFrame

        local st = {
            panel = self,
            scrollFrame = scrollFrame,
            scrollChild = scrollChild,
            rows = {},
            rowsById = {},
            cards = {},
            templateRows = {},
            pickerSpecs = {},
            expandedRowId = nil,
        }
        self._annState = st

        local function buildAnnouncementsLayoutTokens()
            return {
                HEADER_LEFT = 16,
                LEFT = 20,
                RIGHT = 20,
                CARD_TO_CARD_GAP = 12,
                CARD_PAD = 10,
                CARD_HEADER_H = 18,
                CARD_HEADER_TO_ROWS_GAP = 8,
                CARD_TITLE_TO_QUALIFIER_GAP = 8,
                CARD_BOTTOM_PAD = 10,
                ROW_GAP = 6,
                ROW_PAD_TOP = 8,
                ROW_PAD_BOTTOM = 8,
                ROW_HEADER_H = 22,
                ROW_HEADER_TO_PREVIEW_GAP = 4,
                ROW_PREVIEW_LINE_H = 12,
                ROW_BODY_TOP_GAP = 8,
                ENABLE_SEGMENT_W = 136,
                CHEVRON_LANE_W = 16,
                CHEVRON_GAP = 8,
                PREVIEW_TO_CHEVRON_GAP = 8,
                PREVIEW_TEST_W = 20,
                PREVIEW_TEST_GAP = 6,
                ACCENT_GUTTER_W = 3,
                TOKEN_CHIP_H = 19,
                TOKEN_CHIP_PAD_X = 6,
                TOKEN_CHIP_TEXT_SLACK = 8,
                TOKEN_CHIP_GAP = 5,
                MESSAGE_EDIT_H = 22,
                TEST_BUTTON_W = 62,
                SOUND_PICKER_MIN_W = 164,
                SUBGROUP_LABEL_H = 13,
                SUBGROUP_TO_CONTENT_GAP = 5,
                SUBGROUP_DIVIDER_GAP_TOP = 8,
                SUBGROUP_DIVIDER_GAP_BOTTOM = 8,
                ROW_TITLE_TO_RIGHT_CLUSTER_GAP = 8,
                INLINE_BODY_CONTROL_GAP = 8,
                SECTION_HEADER_TO_FIRST_CARD_GAP = 6,
                PREVIEW_PREFIX_TO_VALUE_GAP = 4,
                ROW_CONTENT_AFTER_ACCENT_GAP = 6,
                ROW_TITLE_TOP_NUDGE = 2,
                ROW_PREVIEW_TOP_NUDGE = 1,
                ROW_PREVIEW_TEST_TOP_NUDGE = 2,
                ROW_CHEVRON_TOP_NUDGE = 2,
                ROW_HEADER_BAND_EXTRA = 6,
                ROW_EXPANDED_INFO_TOP_GAP = 4,
                FIELD_LABEL_H = 14,
                LABEL_TO_CONTROL_GAP = 3,
                CONTROL_H = 22,
                CONTROL_TO_HINT_GAP = 5,
                HELPER_LINE_H = 12,
                HELPER_TO_CHIP_GAP = 4,
                CHIP_TO_RESET_GAP = 6,
                UNAVAILABLE_HINT_TOP_GAP = 2,
                UNAVAILABLE_HINT_BLOCK_H = 16,
                SECTION_TOP = 20,
                SECTION_HEADER_H = 28,
                SECTION_BOTTOM_PAD = 20,
                MIN_CONTENT_W = 560,
                SCROLL_WIDTH_FUDGE = 8,
                MIN_ROW_W = 120,
                MIN_TITLE_W = 80,
                MIN_BODY_W = 80,
                MIN_PREVIEW_PREFIX_W = 40,
                MAX_PREVIEW_PREFIX_W = 92,
                MIN_PREVIEW_VALUE_W = 20,
                PREVIEW_PREFIX_MEASURE_FALLBACK_W = 42,
                PREVIEW_PREFIX_MEASURE_PAD = 2,
                TOKEN_CHIP_MIN_W = 56,
                DIVIDER_H = 1,
                MIN_CARD_H = 80,
            }
        end

        local T = buildAnnouncementsLayoutTokens()
        local C = T
        local function getCurrentPlayerClass()
            local classFile = nil
            if UnitClass then
                _, classFile = UnitClass("player")
            end
            return tostring(classFile or "")
        end

        local function isCurrentClass(classFile)
            return getCurrentPlayerClass() == tostring(classFile or "")
        end

        local function helperTone(fs)
            if not fs then
                return
            end
            fs:SetTextColor(0.58, 0.58, 0.58, 0.92)
        end

        local function contextualHelperTone(fs)
            if not fs then
                return
            end
            fs:SetTextColor(0.52, 0.52, 0.54, 0.88)
        end

        local function elevateInteractiveFrame(frame, parent, extraLevel)
            if not frame or not parent or not frame.SetFrameLevel or not parent.GetFrameLevel then
                return
            end
            frame:SetFrameLevel((parent:GetFrameLevel() or 0) + (extraLevel or 5))
        end

        local function setSingleLineEllipsis(fs, fullText, maxWidth)
            if not fs then
                return
            end
            local text = tostring(fullText or "")
            local width = math.max(1, math.floor(tonumber(maxWidth) or 0))
            fs:SetWordWrap(false)
            if fs.SetMaxLines then
                fs:SetMaxLines(1)
            end
            fs:SetWidth(width)
            fs:SetText(text)
            if fs:GetStringWidth() <= width then
                return
            end
            local cut = string.len(text)
            while cut > 0 do
                local candidate = string.sub(text, 1, cut) .. "..."
                fs:SetText(candidate)
                if fs:GetStringWidth() <= width then
                    return
                end
                cut = cut - 1
            end
            fs:SetText("...")
        end

        local announcementsMeasureStrings = {}
        local function getAnnouncementsMeasureString(template, size)
            local key = string.format("%s:%s", tostring(template or ""), tostring(size or ""))
            local fs = announcementsMeasureStrings[key]
            if fs then
                return fs
            end
            fs = scrollChild:CreateFontString(nil, "ARTWORK", template)
            applyGlobalFontToObject(fs, size)
            fs:SetWordWrap(false)
            if fs.SetMaxLines then
                fs:SetMaxLines(1)
            end
            fs:Hide()
            announcementsMeasureStrings[key] = fs
            return fs
        end

        local function measureAnnouncementsSingleLineWidth(text, template, size, floor)
            local fs = getAnnouncementsMeasureString(template, size)
            fs:SetText(tostring(text or ""))
            local measured = math.ceil(tonumber((fs.GetStringWidth and fs:GetStringWidth()) or 0) or 0)
            return math.max(math.floor(tonumber(floor) or 0), measured)
        end

        local function getAnnouncementsEffectiveTextHeight(fs, floor)
            local minimum = math.max(0, math.floor(tonumber(floor) or 0))
            if not fs or not fs.GetStringHeight then
                return minimum
            end
            local measured = math.ceil(tonumber(fs:GetStringHeight() or 0) or 0)
            return math.max(minimum, measured)
        end

        local function getAnnouncementsEffectiveControlHeight(control, floor)
            local minimum = math.max(0, math.floor(tonumber(floor) or 0))
            local measured = 0
            if control and control.GetHeight then
                measured = math.ceil(tonumber(control:GetHeight() or 0) or 0)
            end
            local label = control and control.Text or nil
            if label and label.GetStringHeight then
                measured = math.max(measured, math.ceil(tonumber(label:GetStringHeight() or 0) or 0) + 8)
            end
            return math.max(minimum, measured)
        end

        local function layoutAnnouncementsContainedText(fs, fullText, width, floor)
            if not fs then
                return math.max(0, math.floor(tonumber(floor) or 0))
            end
            setSingleLineEllipsis(fs, fullText, width)
            return getAnnouncementsEffectiveTextHeight(fs, floor)
        end

        local function layoutAnnouncementsContainedButtonText(button, fullText, insetX, floor)
            if not button or not button.Text then
                return getAnnouncementsEffectiveControlHeight(button, floor)
            end
            local padding = math.max(0, math.floor(tonumber(insetX) or 8))
            local textW = math.max(1, math.floor((button:GetWidth() or 0) - (padding * 2)))
            setSingleLineEllipsis(button.Text, fullText, textW)
            return getAnnouncementsEffectiveControlHeight(button, floor)
        end

        local function setAnnouncementsSingleLineWidth(fs, width)
            if not fs then
                return
            end
            fs:SetWordWrap(false)
            if fs.SetMaxLines then
                fs:SetMaxLines(1)
            end
            fs:SetWidth(math.max(1, math.floor(tonumber(width) or 0)))
        end

        local function getAnnouncementsSegmentedHeight(spec, floor)
            local measured = getAnnouncementsEffectiveControlHeight(spec and spec.control or nil, floor)
            for _, btn in ipairs((spec and spec.buttons) or {}) do
                local btnHeight = getAnnouncementsEffectiveControlHeight(btn, floor)
                measured = math.max(measured, btnHeight)
            end
            return math.max(math.floor(tonumber(floor) or 0), measured)
        end

        local function setAnnouncementsSegmentedHeight(spec, height)
            local targetH = math.max(1, math.floor(tonumber(height) or 0))
            if not spec or not spec.control then
                return
            end
            spec.control:SetHeight(targetH)
            for _, btn in ipairs(spec.buttons or {}) do
                btn:SetHeight(targetH)
            end
        end

        local function createSubtleCard(parent)
            local card = CreateFrame("Frame", nil, parent, "BackdropTemplate")
            card:SetBackdrop({
                bgFile = "Interface\\Buttons\\WHITE8x8",
                edgeFile = "Interface\\Buttons\\WHITE8x8",
                edgeSize = 1,
                insets = { left = 0, right = 0, top = 0, bottom = 0 },
            })
            card:SetBackdropColor(0.06, 0.07, 0.09, 0.42)
            card:SetBackdropBorderColor(0.20, 0.23, 0.29, 0.50)
            return card
        end

        local function createInterruptCardBody(card)
            local body = CreateFrame("Frame", nil, card)
            body:SetAllPoints(card)
            body:SetFrameStrata(card:GetFrameStrata())
            body:SetFrameLevel((card:GetFrameLevel() or 0) + 1)
            return body
        end

        local function syncInterruptCardBody(card, body)
            if not card or not body then
                return
            end
            body:ClearAllPoints()
            body:SetAllPoints(card)
            body:SetFrameStrata(card:GetFrameStrata())
            body:SetFrameLevel((card:GetFrameLevel() or 0) + 1)
        end

        local function styleMicroChip(button)
            styleSecondaryActionButtonShared(button, {
                baseBg = { 0.09, 0.10, 0.12, 0.90 },
                hoverBg = { 0.12, 0.13, 0.16, 0.94 },
                pressedBg = { 0.07, 0.08, 0.10, 0.95 },
                baseBorder = { 0.20, 0.23, 0.28, 0.56 },
                hoverBorder = { 0.26, 0.29, 0.34, 0.65 },
                pressedBorder = { 0.23, 0.26, 0.31, 0.62 },
                textColor = { 0.74, 0.76, 0.80, 0.96 },
            })
            button:SetHeight(C.TOKEN_CHIP_H)
            if button.Text then
                applyGlobalFontToObject(button.Text, 10)
            end
        end

        local function getTemplateStoredValue(row)
            local value = trim((PortalAuthorityDB and PortalAuthorityDB[row.textKey]) or "")
            if value == "" then
                value = trim(row.defaultText or "")
            end
            return value
        end

        local function normalizeTemplateInput(text)
            local normalized = trim(tostring(text or ""))
            if normalized == "" then
                return nil
            end
            return normalized
        end

        local function resolveSummonSoundLabel(value)
            local selected = normalizeSoundChoiceValue(value)
            for _, option in ipairs(getSummonSoundChoices(value) or {}) do
                if tostring(option.value or "") == tostring(selected or "") then
                    return tostring(option.label or selected)
                end
            end
            if selected == "" or selected == nil then
                return "None"
            end
            return tostring(selected)
        end

        local function previewSummonSoundValue(value)
            if value == nil or value == "" then
                return
            end
            local soundFile = value
            if PortalAuthority and PortalAuthority.GetDeathAlertSoundFile then
                soundFile = PortalAuthority:GetDeathAlertSoundFile(value) or value
            end
            if not soundFile or soundFile == "" then
                return
            end
            if type(soundFile) == "number" and PlaySound then
                PlaySound(soundFile, "Master")
            elseif PlaySoundFile then
                PlaySoundFile(soundFile, "Master")
            end
        end

        local function playCurrentSummonPreview(row)
            if not row or not row.soundKey then
                return
            end
            previewSummonSoundValue(PortalAuthorityDB and PortalAuthorityDB[row.soundKey] or "")
        end

        local function makeEnableSegment(parent, row)
            row._enabledVisualValue = PA_SafeBool((PortalAuthorityDB and PortalAuthorityDB[row.enabledKey]) or false)
            local segmented = createPremiumSegmentedChoice(parent, "", nil, {
                { label = "Disabled", value = false, toggleState = "off" },
                { label = "Enabled", value = true, toggleState = "on" },
            }, function(v)
                local newValue = PA_SafeBool(v)
                row._enabledVisualValue = newValue
                if PortalAuthorityDB then
                    PortalAuthorityDB[row.enabledKey] = newValue
                end
                if row.kind == "template" then
                    st:ClearTemplateBatchFeedback()
                end
                if st.panel and st.panel._refresh then
                    st.panel._refresh()
                end
            end, {
                semantic = "toggle",
                textEllipsis = true,
                normalizeValue = function(v)
                    return PA_SafeBool(v)
                end,
                getValue = function()
                    return PA_SafeBool(row._enabledVisualValue)
                end,
                setValue = function(v)
                    row._enabledVisualValue = PA_SafeBool(v)
                end,
            })
            if segmented and segmented.label then
                segmented.label:SetText("")
                segmented.label:Hide()
            end
            return segmented
        end

        local function registerAnnouncementRowSearch(row, extraRefs)
            if type(row) ~= "table" or not frameIsAlive(row.title) then
                return nil
            end

            local controlRef = row.enabledControl
                or (row.enabled and row.enabled.control)
                or row.container
            local refs = {}
            if type(extraRefs) == "table" then
                for i = 1, #extraRefs do
                    if frameIsAlive(extraRefs[i]) then
                        refs[#refs + 1] = extraRefs[i]
                    end
                end
            end
            return settingsSearchRuntime.registerControlText(
                row.title,
                controlRef,
                "toggle",
                refs,
                row.container
            )
        end

        local function createPickerButton(parent, cfg)
            cfg = type(cfg) == "table" and cfg or {}
            local button = CreateFrame("Button", nil, parent, "BackdropTemplate")
            button:SetBackdrop({
                bgFile = "Interface\\Buttons\\WHITE8x8",
                edgeFile = "Interface\\Buttons\\WHITE8x8",
                edgeSize = 1,
                insets = { left = 0, right = 0, top = 0, bottom = 0 },
            })
            button:SetBackdropColor(0.10, 0.11, 0.14, 0.95)
            button:SetBackdropBorderColor(0.24, 0.27, 0.32, 1.0)
            button:SetHeight(22)
            button:SetWidth(math.max(C.SOUND_PICKER_MIN_W, math.floor(tonumber(cfg.width) or 200)))

            local text = createStyledFontString(button, "ARTWORK", "GameFontHighlightSmall", 11)
            button.Text = text
            text:SetPoint("LEFT", button, "LEFT", 8, 0)
            text:SetPoint("RIGHT", button, "RIGHT", -18, 0)
            text:SetJustifyH("LEFT")
            text:SetTextColor(0.92, 0.92, 0.92, 1)
            text:SetWordWrap(false)
            if text.SetMaxLines then
                text:SetMaxLines(1)
            end

            local caret = createStyledFontString(button, "ARTWORK", "GameFontHighlightSmall", 10)
            caret:SetPoint("RIGHT", button, "RIGHT", -7, 0)
            caret:SetText("v")
            caret:SetTextColor(0.76, 0.76, 0.78, 1)
            button._paSelectEnabled = true

            local picker = CreatePicker(UIParent, {}, function(value)
                if cfg.setter then
                    cfg.setter(value)
                end
                if cfg.onApply then
                    cfg.onApply()
                end
            end, {
                title = tostring(cfg.title or "Select Option"),
                searchHint = tostring(cfg.searchHint or "Search"),
                width = tonumber(cfg.pickerWidth) or 430,
                height = tonumber(cfg.pickerHeight) or 360,
                minHeight = tonumber(cfg.minHeight),
                showSecondary = false,
                useOptionTexturePreview = cfg.useOptionTexturePreview and true or false,
                showClearButton = false,
                noSearch = cfg.noSearch and true or false,
                hideTitle = cfg.hideTitle and true or false,
                hideScrollBar = cfg.hideScrollBar and true or false,
                onOptionRightClick = type(cfg.onOptionRightClick) == "function" and cfg.onOptionRightClick or nil,
            })

            local currentLabel = ""
            local function setLabel(labelText)
                currentLabel = tostring(labelText or "")
                setSingleLineEllipsis(text, currentLabel, math.max(20, (button:GetWidth() or 0) - 26))
            end

            local function applyVisual(hovered)
                applySettingsWindowSelectButtonChrome(
                    button,
                    text,
                    caret,
                    hovered,
                    false,
                    button._paSelectEnabled ~= false,
                    function()
                        if hovered then
                            button:SetBackdropColor(0.16, 0.17, 0.20, 0.95)
                            button:SetBackdropBorderColor(0.30, 0.33, 0.38, 1.0)
                            caret:SetTextColor(0.88, 0.88, 0.90, 1)
                        else
                            button:SetBackdropColor(0.10, 0.11, 0.14, 0.95)
                            button:SetBackdropBorderColor(0.24, 0.27, 0.32, 1.0)
                            caret:SetTextColor(0.76, 0.76, 0.78, 1)
                        end
                    end
                )
            end

            local function buildOptions()
                local built = {}
                for _, option in ipairs(cfg.optionsProvider and cfg.optionsProvider() or {}) do
                    local value = option.value
                    if value == nil then
                        value = ""
                    end
                    built[#built + 1] = {
                        value = value,
                        label = tostring(option.label or value),
                        buttonLabel = tostring(option.buttonLabel or option.label or value),
                        secondary = tostring(option.secondary or ""),
                        previewTexturePath = option.previewTexturePath or option.path,
                    }
                end
                if #built == 0 then
                    built[1] = { value = "", label = "None", buttonLabel = "None", secondary = "" }
                end
                return built
            end

            local function resolveSelected(options)
                local selected = tostring(cfg.getter and cfg.getter() or "")
                local labelText = nil
                for _, option in ipairs(options or {}) do
                    if tostring(option.value or "") == selected then
                        labelText = tostring(option.buttonLabel or option.label or selected)
                        break
                    end
                end
                if not labelText then
                    for _, option in ipairs(options or {}) do
                        if tostring(option.value or "") == "" then
                            labelText = tostring(option.buttonLabel or option.label or "None")
                            break
                        end
                    end
                end
                if not labelText then
                    local first = options and options[1] or nil
                    labelText = first and tostring(first.buttonLabel or first.label or "None") or "None"
                end
                return selected, labelText
            end

            local spec = { control = button, picker = picker }
            spec.SetWidth = function(_, width)
                button:SetWidth(math.max(C.SOUND_PICKER_MIN_W, math.floor(tonumber(width) or C.SOUND_PICKER_MIN_W)))
                setLabel(currentLabel)
            end
            spec.SetEnabled = function(_, enabled)
                local allow = enabled and true or false
                button._paSelectEnabled = allow
                button:EnableMouse(allow)
                button:SetAlpha(allow and 1 or 0.55)
                text:SetAlpha(allow and 1 or 0.65)
                caret:SetAlpha(allow and 1 or 0.65)
                applyVisual(false)
            end
            spec.ClosePicker = function()
                if picker and picker.IsShown and picker:IsShown() then
                    picker:Close()
                end
            end
            spec.Refresh = function()
                local options = buildOptions()
                picker:SetOptions(options)
                local selected, selectedLabel = resolveSelected(options)
                picker:SetSelectedValue(selected)
                setLabel(selectedLabel)
            end

            button:SetScript("OnEnter", function()
                applyVisual(true)
            end)
            button:SetScript("OnLeave", function()
                applyVisual(false)
            end)
            button:SetScript("OnClick", function()
                if picker and picker.IsShown and picker:IsShown() then
                    picker:Close()
                    return
                end
                spec:Refresh()
                picker:Open(button)
            end)
            button:HookScript("OnSizeChanged", function()
                setLabel(currentLabel)
            end)
            registerSettingsWindowVisualRefresh(button, function()
                applyVisual(button.IsMouseOver and button:IsMouseOver() or false)
            end)

            spec:Refresh()
            st.pickerSpecs[#st.pickerSpecs + 1] = spec
            return spec
        end

        local function createMessageEdit(parent)
            local edit = CreateFrame("EditBox", nil, parent, "InputBoxTemplate")
            edit:SetAutoFocus(false)
            edit:SetTextInsets(8, 8, 0, 0)
            applyGlobalFontToObject(edit, 12)
            if not edit._paPremiumBg then
                edit._paPremiumBg = edit:CreateTexture(nil, "BACKGROUND")
                edit._paPremiumBg:SetAllPoints(edit)
                edit._paPremiumBg:SetTexture("Interface\\Buttons\\WHITE8x8")
                edit._paPremiumBorder = CreateFrame("Frame", nil, edit, "BackdropTemplate")
                edit._paPremiumBorder:SetPoint("TOPLEFT", edit, "TOPLEFT", -1, 1)
                edit._paPremiumBorder:SetPoint("BOTTOMRIGHT", edit, "BOTTOMRIGHT", 1, -1)
                edit._paPremiumBorder:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 })
            end

            local function refreshVisual()
                if edit.Left then edit.Left:SetAlpha(0) end
                if edit.Middle then edit.Middle:SetAlpha(0) end
                if edit.Right then edit.Right:SetAlpha(0) end
                if edit:HasFocus() then
                    edit._paPremiumBg:SetVertexColor(0.12, 0.13, 0.17, 0.95)
                    edit._paPremiumBorder:SetBackdropBorderColor(0.30, 0.33, 0.38, 1.0)
                else
                    edit._paPremiumBg:SetVertexColor(0.10, 0.11, 0.14, 0.95)
                    edit._paPremiumBorder:SetBackdropBorderColor(0.24, 0.27, 0.32, 1.0)
                end
            end
            edit:HookScript("OnShow", refreshVisual)
            edit:HookScript("OnEditFocusGained", refreshVisual)
            edit:HookScript("OnEditFocusLost", refreshVisual)
            refreshVisual()
            return edit
        end

        local header = createStyledFontString(scrollChild, "ARTWORK", "GameFontNormalLarge", 14)
        header:SetText("|cffffd100Announcements|r")
        header:SetJustifyH("LEFT")
        header:SetWordWrap(false)
        if header.SetMaxLines then
            header:SetMaxLines(1)
        end
        st.header = header
        st.sectionDescriptor = createSettingsSectionDescriptor(scrollChild, "Customize your utility alerts.")
        st.playerHeader = createTopRightHeader(self, -20)

        local function createCard(titleText)
            local card = {}
            card.frame = createSubtleCard(scrollChild)
            settingsSearchRuntime.markTextContainer(card.frame)
            card.titleBase = titleText or ""
            card.titleColorCode = nil
            card.title = createStyledFontString(card.frame, "ARTWORK", "GameFontHighlight", 12)
            card.title:SetText(card.titleBase)
            card.title:SetTextColor(1, 0.82, 0, 1)
            card.title:SetJustifyH("LEFT")
            card.title:SetWordWrap(false)
            if card.title.SetMaxLines then
                card.title:SetMaxLines(1)
            end
            card.titleText = card.titleBase
            card.titleQualifierText = ""
            card.titleQualifier = createStyledFontString(card.frame, "ARTWORK", "GameFontHighlight", 12)
            card.titleQualifier:SetText("")
            card.titleQualifier:SetJustifyH("LEFT")
            card.titleQualifier:SetWordWrap(false)
            if card.titleQualifier.SetMaxLines then
                card.titleQualifier:SetMaxLines(1)
            end
            helperTone(card.titleQualifier)
            card.titleQualifier:Hide()
            card.unavailableHint = createStyledFontString(card.frame, "ARTWORK", "GameFontHighlightSmall", 10)
            card.unavailableHintText = "Unavailable on this character."
            card.unavailableHint:SetText(card.unavailableHintText)
            card.unavailableHint:SetJustifyH("CENTER")
            card.unavailableHint:SetWordWrap(false)
            if card.unavailableHint.SetMaxLines then
                card.unavailableHint:SetMaxLines(1)
            end
            helperTone(card.unavailableHint)
            card.unavailableHint:SetAlpha(0.84)
            card.rows = {}
            st.cards[#st.cards + 1] = card
            return card
        end

        local function createRowBase(parent, kind, id, titleText, enabledKey, classLock)
            local row = {
                kind = kind,
                id = id,
                titleText = titleText,
                enabledKey = enabledKey,
                classLock = classLock,
                available = true,
                isExpanded = false,
            }
            row.container = CreateFrame("Frame", nil, parent)
            row.container:SetSize(1, 1)
            row.bg = row.container:CreateTexture(nil, "BACKGROUND")
            row.bg:SetAllPoints(row.container)
            row.bg:SetTexture("Interface\\Buttons\\WHITE8x8")
            row.bg:SetVertexColor(1, 1, 1, 0.02)
            row.border = CreateFrame("Frame", nil, row.container, "BackdropTemplate")
            row.border:SetPoint("TOPLEFT", row.container, "TOPLEFT", 0, 0)
            row.border:SetPoint("BOTTOMRIGHT", row.container, "BOTTOMRIGHT", 0, 0)
            row.border:SetBackdrop({
                edgeFile = "Interface\\Buttons\\WHITE8x8",
                edgeSize = 1,
            })
            row.border:SetBackdropBorderColor(0.20, 0.23, 0.29, 0.65)
            row.accent = row.container:CreateTexture(nil, "BORDER")
            row.accent:SetTexture("Interface\\Buttons\\WHITE8x8")
            row.accent:SetPoint("TOPLEFT", row.container, "TOPLEFT", 0, -1)
            row.accent:SetPoint("BOTTOMLEFT", row.container, "BOTTOMLEFT", 0, 1)
            row.accent:SetWidth(C.ACCENT_GUTTER_W)
            if classLock == "MAGE" then
                row._accentColor = { 0.41, 0.80, 0.94 }
            elseif classLock == "WARLOCK" then
                row._accentColor = { 0.58, 0.51, 0.79 }
            else
                row._accentColor = nil
            end
            row.accent:SetVertexColor(1, 1, 1, 0)
            row.headerBandBg = row.container:CreateTexture(nil, "ARTWORK")
            row.headerBandBg:SetTexture("Interface\\Buttons\\WHITE8x8")
            row.headerBandBg:SetVertexColor(0.12, 0.13, 0.16, 0)
            row.headerBandBg:Hide()
            row.headerDivider = row.container:CreateTexture(nil, "ARTWORK")
            row.headerDivider:SetTexture("Interface\\Buttons\\WHITE8x8")
            row.headerDivider:SetVertexColor(0.22, 0.25, 0.30, 0)
            row.headerDivider:Hide()
            row.title = createStyledFontString(row.container, "ARTWORK", "GameFontHighlight", 12)
            row.title:SetJustifyH("LEFT")
            row.title:SetWordWrap(false)
            if row.title.SetMaxLines then
                row.title:SetMaxLines(1)
            end
            row.previewPrefix = createStyledFontString(row.container, "ARTWORK", "GameFontHighlightSmall", 10)
            row.previewPrefix:SetJustifyH("LEFT")
            row.previewPrefix:SetWordWrap(false)
            if row.previewPrefix.SetMaxLines then
                row.previewPrefix:SetMaxLines(1)
            end
            helperTone(row.previewPrefix)
            row.previewValue = createStyledFontString(row.container, "ARTWORK", "GameFontHighlightSmall", 10)
            row.previewValue:SetJustifyH("LEFT")
            row.previewValue:SetWordWrap(false)
            if row.previewValue.SetMaxLines then
                row.previewValue:SetMaxLines(1)
            end
            row.previewValue:SetTextColor(0.72, 0.72, 0.74, 0.94)
            row.collapsedClickCatcher = CreateFrame("Button", nil, row.container)
            row.collapsedClickCatcher:RegisterForClicks("LeftButtonUp")
            row.collapsedClickCatcher:SetFrameLevel((row.container:GetFrameLevel() or 0) + 1)
            row.expandedHeaderClickCatcher = CreateFrame("Button", nil, row.container)
            row.expandedHeaderClickCatcher:RegisterForClicks("LeftButtonUp")
            row.expandedHeaderClickCatcher:SetFrameLevel((row.container:GetFrameLevel() or 0) + 1)
            row.chevron = createStyledFontString(row.container, "ARTWORK", "GameFontHighlightSmall", 12)
            row.chevron:SetText(">")
            row.chevron:SetJustifyH("CENTER")
            row.chevron:SetWordWrap(false)
            row.enabled = makeEnableSegment(row.container, row)
            if row.enabled and row.enabled.Layout then
                row.enabled:Layout(C.ENABLE_SEGMENT_W)
            end
            row.enabledControl = row.enabled and row.enabled.control or nil
            elevateInteractiveFrame(row.enabledControl, row.container)
            row._hoverCount = 0
            row._hovered = false
            row._lastExpanded = false
            row._lastAvailable = true
            row._syncChrome = nil
            local function setRowHover(entering)
                local delta = entering and 1 or -1
                row._hoverCount = math.max(0, (row._hoverCount or 0) + delta)
                row._hovered = row._hoverCount > 0
                if row._syncChrome then
                    row._syncChrome(row)
                end
            end
            row.collapsedClickCatcher:SetScript("OnEnter", function()
                setRowHover(true)
            end)
            row.collapsedClickCatcher:SetScript("OnLeave", function()
                setRowHover(false)
            end)
            row.expandedHeaderClickCatcher:SetScript("OnEnter", function()
                setRowHover(true)
            end)
            row.expandedHeaderClickCatcher:SetScript("OnLeave", function()
                setRowHover(false)
            end)
            st.rows[#st.rows + 1] = row
            st.rowsById[id] = row
            return row
        end

        local function createTemplateRow(parent, cfg)
            local row = createRowBase(parent, "template", cfg.id, cfg.title, cfg.enabledKey, cfg.classLock)
            row.textKey = cfg.textKey
            row.defaultText = cfg.defaultText
            row.showDestinationToken = cfg.showDestinationToken and true or false
            row.previewPrefixText = "Message:"
            row.routingHintText = tostring(cfg.routingHintText or "")
            row.expandedInfoText = tostring(cfg.expandedInfoText or "")
            row.title:SetText(cfg.title or "")
            row.messageLabelText = tostring(cfg.messageLabel or "Message")
            row.messageLabel = createStyledFontString(row.container, "ARTWORK", "GameFontHighlight", 12)
            row.messageLabel:SetText(row.messageLabelText)
            row.messageLabel:SetJustifyH("LEFT")
            row.messageLabel:SetWordWrap(false)
            if row.messageLabel.SetMaxLines then
                row.messageLabel:SetMaxLines(1)
            end
            row.expandedInfo = createStyledFontString(row.container, "ARTWORK", "GameFontHighlightSmall", 10)
            row.expandedInfo:SetText(row.expandedInfoText)
            row.expandedInfo:SetJustifyH("LEFT")
            row.expandedInfo:SetWordWrap(false)
            if row.expandedInfo.SetMaxLines then
                row.expandedInfo:SetMaxLines(1)
            end
            contextualHelperTone(row.expandedInfo)
            row.messageEdit = createMessageEdit(row.container)
            row.messageEdit:SetHeight(C.MESSAGE_EDIT_H)
            elevateInteractiveFrame(row.messageEdit, row.container)
            row.resetButton = createActionButton(row.container, "Reset", nil, 74)
            styleSecondaryActionButtonShared(row.resetButton)
            elevateInteractiveFrame(row.resetButton, row.container)
            row.routingHint = createStyledFontString(row.container, "ARTWORK", "GameFontHighlightSmall", 10)
            row.routingHint:SetJustifyH("LEFT")
            row.routingHint:SetWordWrap(false)
            if row.routingHint.SetMaxLines then
                row.routingHint:SetMaxLines(1)
            end
            helperTone(row.routingHint)
            row.destinationHelper = createStyledFontString(row.container, "ARTWORK", "GameFontHighlightSmall", 10)
            row.destinationHelperText = tostring(cfg.destinationHelperText or "")
            row.destinationHelper:SetText(row.destinationHelperText)
            row.destinationHelper:SetJustifyH("LEFT")
            row.destinationHelper:SetWordWrap(false)
            if row.destinationHelper.SetMaxLines then
                row.destinationHelper:SetMaxLines(1)
            end
            helperTone(row.destinationHelper)
            row.destinationChipText = "[destination]"
            row.destinationChip = createActionButton(row.container, "[destination]", nil, 92)
            styleMicroChip(row.destinationChip)
            elevateInteractiveFrame(row.destinationChip, row.container)
            registerAnnouncementRowSearch(row, {
                row.expandedInfo,
                row.routingHint,
                row.destinationHelper,
            })
            st.templateRows[#st.templateRows + 1] = row
            return row
        end

        local function createSummonRow(parent, cfg)
            local row = createRowBase(parent, "summon", cfg.id, cfg.title, cfg.enabledKey, cfg.classLock)
            row.soundKey = cfg.soundKey
            row.previewPrefixText = "Sound:"
            row.title:SetText(cfg.title or "")
            row.soundLabelText = "Alert Sound"
            row.soundLabel = createStyledFontString(row.container, "ARTWORK", "GameFontHighlight", 12)
            row.soundLabel:SetText(row.soundLabelText)
            row.soundLabel:SetJustifyH("LEFT")
            row.soundLabel:SetWordWrap(false)
            if row.soundLabel.SetMaxLines then
                row.soundLabel:SetMaxLines(1)
            end
            row.soundPicker = createPickerButton(row.container, {
                title = "Select Alert Sound",
                searchHint = "Search sounds",
                width = 220,
                pickerWidth = 470,
                pickerHeight = 390,
                getter = function()
                    return normalizeSoundChoiceValue(PortalAuthorityDB and PortalAuthorityDB[row.soundKey] or "")
                end,
                setter = function(value)
                    if PortalAuthorityDB then
                        PortalAuthorityDB[row.soundKey] = value
                    end
                end,
                optionsProvider = function()
                    return getSummonSoundChoices(PortalAuthorityDB and PortalAuthorityDB[row.soundKey] or "")
                end,
                onOptionRightClick = function(value)
                    previewSummonSoundValue(value)
                end,
                onApply = function()
                    if st.panel and st.panel._refresh then
                        st.panel._refresh()
                    end
                end,
            })
            row.previewTestButton = createActionButton(row.container, "|>", nil, C.PREVIEW_TEST_W)
            styleSecondaryActionButtonShared(row.previewTestButton)
            row.previewTestButton:SetHeight(18)
            elevateInteractiveFrame(row.previewTestButton, row.container)
            row.previewTestButton:SetScript("OnEnter", function(selfBtn)
                GameTooltip:SetOwner(selfBtn, "ANCHOR_RIGHT")
                GameTooltip:SetText("Play test sound", 1, 1, 1)
                GameTooltip:Show()
            end)
            row.previewTestButton:SetScript("OnLeave", function()
                GameTooltip:Hide()
            end)
            row.testButton = createActionButton(row.container, "Test", nil, C.TEST_BUTTON_W)
            styleSecondaryActionButtonShared(row.testButton)
            elevateInteractiveFrame(row.testButton, row.container)
            elevateInteractiveFrame(row.soundPicker and row.soundPicker.control or nil, row.container)
            row.masterHintText = "Audio Channel: Master (locked)"
            row.masterHint = createStyledFontString(row.container, "ARTWORK", "GameFontHighlightSmall", 10)
            row.masterHint:SetText(row.masterHintText)
            row.masterHint:SetJustifyH("LEFT")
            row.masterHint:SetWordWrap(false)
            if row.masterHint.SetMaxLines then
                row.masterHint:SetMaxLines(1)
            end
            helperTone(row.masterHint)
            row.testPlay = cfg.testPlay
            registerAnnouncementRowSearch(row, {
                row.soundLabel,
                row.masterHint,
            })
            return row
        end

        local cardA = createCard("Mythic+ Teleports")
        cardA.titleColorCode = "D9D7D0C4"
        local cardB = createCard("Mage Travel")
        cardB.titleColorCode = "D969CCF0"
        local cardC = createCard("Warlock Summoning")
        cardC.titleColorCode = "D99482C9"
        st.cardA = cardA
        st.cardB = cardB
        st.cardC = cardC
        cardC.subgroupAnnouncements = createStyledFontString(cardC.frame, "ARTWORK", "GameFontHighlightSmall", 10)
        cardC.subgroupAnnouncementsText = "Announcements"
        cardC.subgroupAnnouncements:SetText(cardC.subgroupAnnouncementsText)
        cardC.subgroupAnnouncements:SetJustifyH("LEFT")
        cardC.subgroupAnnouncements:SetWordWrap(false)
        if cardC.subgroupAnnouncements.SetMaxLines then
            cardC.subgroupAnnouncements:SetMaxLines(1)
        end
        cardC.subgroupIncoming = createStyledFontString(cardC.frame, "ARTWORK", "GameFontHighlightSmall", 10)
        cardC.subgroupIncomingText = "Incoming Alerts"
        cardC.subgroupIncoming:SetText(cardC.subgroupIncomingText)
        cardC.subgroupIncoming:SetJustifyH("LEFT")
        cardC.subgroupIncoming:SetWordWrap(false)
        if cardC.subgroupIncoming.SetMaxLines then
            cardC.subgroupIncoming:SetMaxLines(1)
        end
        cardC.subgroupDivider = cardC.frame:CreateTexture(nil, "ARTWORK")
        cardC.subgroupDivider:SetTexture("Interface\\Buttons\\WHITE8x8")
        cardC.subgroupDivider:SetVertexColor(0.20, 0.23, 0.29, 0.56)

        cardA.rows[#cardA.rows + 1] = createTemplateRow(cardA.frame, {
            id = "mplus",
            title = "Mythic+ Teleports",
            enabledKey = "mplusEnabled",
            textKey = "mplusText",
            defaultText = (PortalAuthority.defaults and PortalAuthority.defaults.mplusText) or "Taking M+ teleport to [destination].",
            showDestinationToken = true,
            expandedInfoText = "Uses the teleports enabled in Mythic+ Dock.",
            routingHintText = "Channel: /raid in raids, /party in parties",
        })

        cardB.rows[#cardB.rows + 1] = createTemplateRow(cardB.frame, {
            id = "portals",
            title = "Portals",
            enabledKey = "portalsEnabled",
            textKey = "portalsText",
            defaultText = (PortalAuthority.defaults and PortalAuthority.defaults.portalsText) or "Porting to [destination] - click my portal!",
            showDestinationToken = true,
            routingHintText = "Channel: /raid in raids, /party in parties",
            classLock = "MAGE",
        })
        cardB.rows[#cardB.rows + 1] = createTemplateRow(cardB.frame, {
            id = "teleports",
            title = "Teleports",
            enabledKey = "teleportsEnabled",
            textKey = "teleportsText",
            defaultText = (PortalAuthority.defaults and PortalAuthority.defaults.teleportsText) or "Teleporting to [destination].",
            showDestinationToken = true,
            routingHintText = "Channel: /raid in raids, /party in parties",
            classLock = "MAGE",
        })

        cardC.rows[#cardC.rows + 1] = createTemplateRow(cardC.frame, {
            id = "warlockSummoning",
            title = "Summoning Stone",
            enabledKey = "warlockSummoningEnabled",
            textKey = "warlockSummoningText",
            defaultText = (PortalAuthority.defaults and PortalAuthority.defaults.warlockSummoningText) or "Summoning stone is up - please click.",
            showDestinationToken = false,
            routingHintText = "Channel: /say",
            classLock = "WARLOCK",
            messageLabel = "Message",
        })
        cardC.rows[#cardC.rows + 1] = createSummonRow(cardC.frame, {
            id = "summonRequests",
            title = "Summon Requests",
            enabledKey = "summonRequestsEnabled",
            soundKey = "summonRequestSound",
            classLock = "WARLOCK",
            testPlay = function()
                if PortalAuthority and PortalAuthority.PlaySummonRequestSound then
                    PortalAuthority:PlaySummonRequestSound()
                end
            end,
        })
        cardC.rows[#cardC.rows + 1] = createSummonRow(cardC.frame, {
            id = "summonStoneRequests",
            title = "Summon Stone Requests",
            enabledKey = "summonStoneRequestsEnabled",
            soundKey = "summonStoneRequestSound",
            classLock = "WARLOCK",
            testPlay = function()
                if PortalAuthority and PortalAuthority.PlaySummonStoneRequestSound then
                    PortalAuthority:PlaySummonStoneRequestSound()
                end
            end,
        })

        local function isRowAvailable(row)
            if not row or not row.classLock then
                return true
            end
            if row.classLock == "MAGE" then
                return isCurrentClass("MAGE")
            end
            if row.classLock == "WARLOCK" then
                return isCurrentClass("WARLOCK")
            end
            return true
        end

        st.ClearTemplateBatchFeedback = function()
            -- Batch template actions removed by design.
        end

        local function commitTemplateEdit(row)
            if not row or row.kind ~= "template" then
                return false
            end
            local stored = getTemplateStoredValue(row)
            local candidate = normalizeTemplateInput(row.messageEdit and row.messageEdit:GetText() or "")
            if not candidate then
                if row.messageEdit then
                    row.messageEdit:SetText(stored)
                end
                return false
            end
            if candidate ~= stored then
                if PortalAuthorityDB then
                    PortalAuthorityDB[row.textKey] = candidate
                end
                if row.messageEdit then
                    row.messageEdit:SetText(candidate)
                end
                return true
            end
            if row.messageEdit then
                row.messageEdit:SetText(stored)
            end
            return false
        end

        local function applyExpandedRowState(targetRowId)
            local resolved = nil
            if targetRowId then
                local targetRow = st.rowsById and st.rowsById[targetRowId] or nil
                if targetRow and targetRow.available then
                    resolved = targetRow.id
                end
            end
            st.expandedRowId = resolved
            for _, entry in ipairs(st.rows or {}) do
                entry.isExpanded = (resolved ~= nil and entry.id == resolved) and true or false
            end
        end

        local function commitFocusedExpandedTemplate()
            local row = st.expandedRowId and st.rowsById[st.expandedRowId] or nil
            if not row or row.kind ~= "template" or not row.messageEdit then
                return
            end
            if row.messageEdit.HasFocus and row.messageEdit:HasFocus() then
                st:ClearTemplateBatchFeedback()
                commitTemplateEdit(row)
                row.messageEdit:ClearFocus()
            end
        end

        st.SetExpandedRow = function(targetRowId)
            local nextRowId = targetRowId
            if st.expandedRowId == targetRowId then
                nextRowId = nil
            end
            commitFocusedExpandedTemplate()
            applyExpandedRowState(nextRowId)
            if st.panel and st.panel._refresh then
                st.panel._refresh()
            end
        end
        local function onRowHeaderClick(row)
            if not row or not row.available then
                return
            end
            local willExpand = st.expandedRowId ~= row.id
            if row.kind == "template" then
                st:ClearTemplateBatchFeedback()
                st.pendingFocusRowId = willExpand and row.id or nil
            else
                st.pendingFocusRowId = nil
            end
            st.SetExpandedRow(row.id)
        end

        local function applyRowChrome(row)
            if not row then
                return
            end
            local available = row.available and true or false
            local expanded = row.isExpanded and available
            local hovered = row._hovered and available
            local disabledByUser = available and (not row._enabledVisualValue)
            local rowBgAlpha = 0.02
            local borderR, borderG, borderB, borderA = 0.20, 0.23, 0.29, 0.60
            local accentA = 0
            local headerBandA = 0
            local headerDividerA = 0

            if not available then
                rowBgAlpha = 0.015
                borderA = 0.34
            elseif disabledByUser then
                rowBgAlpha = hovered and 0.028 or 0.018
                borderA = hovered and 0.68 or 0.52
                accentA = hovered and 0.60 or 0.30
            elseif expanded then
                rowBgAlpha = 0.048
                borderR, borderG, borderB, borderA = 0.95, 0.78, 0.24, 0.78
                accentA = 0.95
            elseif hovered then
                rowBgAlpha = 0.034
                borderR, borderG, borderB, borderA = 0.30, 0.33, 0.39, 0.78
                accentA = 0.46
            end

            if expanded then
                if disabledByUser then
                    headerBandA = hovered and 0.07 or 0.055
                    headerDividerA = hovered and 0.38 or 0.30
                else
                    headerBandA = hovered and 0.10 or 0.08
                    headerDividerA = hovered and 0.46 or 0.36
                end
            end

            row.bg:SetVertexColor(1, 1, 1, rowBgAlpha)
            if row.border and row.border.SetBackdropBorderColor then
                row.border:SetBackdropBorderColor(borderR, borderG, borderB, borderA)
            end
            if row.accent then
                if row._accentColor then
                    row.accent:SetVertexColor(row._accentColor[1], row._accentColor[2], row._accentColor[3], accentA)
                else
                    row.accent:SetVertexColor(1, 1, 1, 0)
                end
            end
            if row.headerBandBg then
                row.headerBandBg:SetShown(expanded)
                row.headerBandBg:SetVertexColor(0.12, 0.13, 0.16, headerBandA)
            end
            if row.headerDivider then
                row.headerDivider:SetShown(expanded)
                row.headerDivider:SetVertexColor(0.22, 0.25, 0.30, headerDividerA)
            end
            if row.title then
                if not available then
                    row.title:SetAlpha(0.52)
                elseif disabledByUser then
                    row.title:SetAlpha(hovered and 0.66 or 0.54)
                else
                    row.title:SetAlpha(0.96)
                end
            end
            if row.previewPrefix then
                if not available then
                    row.previewPrefix:SetAlpha(0.50)
                elseif disabledByUser then
                    row.previewPrefix:SetAlpha(hovered and 0.58 or 0.48)
                else
                    row.previewPrefix:SetAlpha(hovered and 0.94 or 0.88)
                end
            end
            if row.previewValue then
                if not available then
                    row.previewValue:SetAlpha(0.56)
                elseif disabledByUser then
                    row.previewValue:SetAlpha(hovered and 0.66 or 0.56)
                else
                    row.previewValue:SetAlpha(hovered and 0.88 or 0.78)
                end
            end
            if row.chevron then
                if not available then
                    row.chevron:SetAlpha(0)
                elseif disabledByUser then
                    row.chevron:SetAlpha(hovered and 0.60 or 0.35)
                else
                    row.chevron:SetAlpha(hovered and 1.0 or 0.55)
                end
            end
            if row.previewTestButton then
                if not available then
                    row.previewTestButton:SetAlpha(0.45)
                else
                    row.previewTestButton:SetAlpha(hovered and 0.96 or 0.86)
                end
            end
        end

        for _, row in ipairs(st.rows or {}) do
            row._syncChrome = applyRowChrome
            row.collapsedClickCatcher:SetScript("OnClick", function()
                onRowHeaderClick(row)
            end)
            row.expandedHeaderClickCatcher:SetScript("OnClick", function()
                onRowHeaderClick(row)
            end)
            if row.kind == "template" then
                row.messageEdit:SetScript("OnEnterPressed", function(box)
                    st:ClearTemplateBatchFeedback()
                    commitTemplateEdit(row)
                    box:ClearFocus()
                    if st.panel and st.panel._refresh then
                        st.panel._refresh()
                    end
                end)
                row.messageEdit:SetScript("OnEditFocusLost", function()
                    st:ClearTemplateBatchFeedback()
                    if commitTemplateEdit(row) and st.panel and st.panel._refresh then
                        st.panel._refresh()
                    end
                end)
                row.resetButton:SetScript("OnClick", function()
                    st:ClearTemplateBatchFeedback()
                    local fallback = trim(row.defaultText or "")
                    if fallback == "" then
                        fallback = getTemplateStoredValue(row)
                    end
                    row.messageEdit:SetText(fallback)
                    if PortalAuthorityDB and fallback ~= getTemplateStoredValue(row) then
                        PortalAuthorityDB[row.textKey] = fallback
                    end
                    if st.panel and st.panel._refresh then
                        st.panel._refresh()
                    end
                end)
                row.destinationChip:SetScript("OnClick", function()
                    st:ClearTemplateBatchFeedback()
                    local token = "[destination]"
                    local current = tostring(row.messageEdit:GetText() or "")
                    local left = row.messageEdit:GetCursorPosition() or string.len(current)
                    left = math.max(0, math.min(left, string.len(current)))
                    local newValue = string.sub(current, 1, left) .. token .. string.sub(current, left + 1)
                    row.messageEdit:SetText(newValue)
                    row.messageEdit:SetCursorPosition(left + string.len(token))
                    row.messageEdit:SetFocus()
                end)
            else
                row.previewTestButton:SetScript("OnClick", function()
                    if row.available then
                        playCurrentSummonPreview(row)
                    end
                end)
                row.testButton:SetScript("OnClick", function()
                    if row.available then
                        playCurrentSummonPreview(row)
                    end
                end)
            end
        end

        local function refreshCardState(card, available, suffixText)
            card.available = available and true or false
            local baseText = tostring(card.titleBase or "")
            if card.titleColorCode then
                baseText = string.format("|c%s%s|r", tostring(card.titleColorCode), baseText)
            end
            local qualifierText = ""
            if (not card.available) and tostring(suffixText or "") ~= "" then
                qualifierText = tostring(suffixText or "")
            end
            card.titleText = baseText
            card.titleQualifierText = qualifierText
            card.title:SetText(baseText)
            if card.titleQualifier then
                card.titleQualifier:SetText(qualifierText)
                card.titleQualifier:SetShown(qualifierText ~= "")
            end
            for _, row in ipairs(card.rows or {}) do
                row.available = card.available and isRowAvailable(row)
            end
        end

        local function refreshAllCardStates()
            local isMage = isCurrentClass("MAGE")
            local isWarlock = isCurrentClass("WARLOCK")
            refreshCardState(cardA, true, "")
            refreshCardState(cardB, isMage, "Mage only")
            refreshCardState(cardC, isWarlock, "Warlock only")
            applyExpandedRowState(st.expandedRowId)
        end

        local function syncAnnouncementsVisualState()
            for _, row in ipairs(st.rows or {}) do
                row._enabledVisualValue = PA_SafeBool((PortalAuthorityDB and PortalAuthorityDB[row.enabledKey]) or false)
                if row.enabled and row.enabled.Refresh then
                    row.enabled:Refresh()
                end
                if row.enabled and row.enabled.SetEnabled then
                    row.enabled:SetEnabled(row.available)
                end

                local previewValueText
                if row.kind == "template" then
                    previewValueText = getTemplateStoredValue(row)
                    if row.messageEdit and not (row.messageEdit.HasFocus and row.messageEdit:HasFocus()) then
                        row.messageEdit:SetText(previewValueText)
                    end
                else
                    previewValueText = resolveSummonSoundLabel((PortalAuthorityDB and PortalAuthorityDB[row.soundKey]) or "")
                    if row.soundPicker and row.soundPicker.Refresh then
                        row.soundPicker:Refresh()
                    end
                end

                row._previewRawText = previewValueText
                row._previewPrefixRawText = tostring(row.previewPrefixText or "Message:")
                if row.previewPrefix then
                    row.previewPrefix:SetText(row._previewPrefixRawText)
                end
                if row.previewValue then
                    row.previewValue:SetText(previewValueText or "")
                end

                row.container:SetAlpha(row.available and 1 or 0.82)
                if (row._lastExpanded ~= row.isExpanded) or (row._lastAvailable ~= row.available) then
                    row._hoverCount = 0
                    row._hovered = false
                end
                row._lastExpanded = row.isExpanded
                row._lastAvailable = row.available

                if row.collapsedClickCatcher then
                    local collapsedActive = row.available and (not row.isExpanded)
                    row.collapsedClickCatcher:SetShown(collapsedActive)
                    row.collapsedClickCatcher:EnableMouse(collapsedActive)
                end
                if row.expandedHeaderClickCatcher then
                    local expandedActive = row.available and row.isExpanded
                    row.expandedHeaderClickCatcher:SetShown(expandedActive)
                    row.expandedHeaderClickCatcher:EnableMouse(expandedActive)
                end
                if row.chevron then
                    row.chevron:SetText(row.isExpanded and "v" or ">")
                    row.chevron:SetShown(row.available)
                end
                if row.previewTestButton then
                    row.previewTestButton:SetEnabled(row.available and (not row.isExpanded))
                end
                if row.testButton then
                    row.testButton:SetEnabled(row.available and row.isExpanded)
                end
                applyRowChrome(row)
            end
        end

        local function applyPendingAnnouncementsFocus()
            local targetRowId = st.pendingFocusRowId
            if not targetRowId then
                return
            end
            local row = st.rowsById and st.rowsById[targetRowId] or nil
            if not row or row.kind ~= "template" or not row.available or not row.isExpanded then
                st.pendingFocusRowId = nil
                return
            end
            if not row.messageEdit or not row.messageEdit.IsShown or not row.messageEdit:IsShown() then
                return
            end
            st.pendingFocusRowId = nil
        end

        self._refresh = function()
            refreshAllCardStates()
            syncAnnouncementsVisualState()
            if panelOwner and panelOwner._layout then
                panelOwner._layout()
            end
            applyPendingAnnouncementsFocus()
        end

        local function computeAnnouncementsGeometry(contentW)
            local cardW = math.max(0, (contentW - T.RIGHT) - T.LEFT)
            local rowW = math.max(T.MIN_ROW_W, cardW - (T.CARD_PAD * 2))
            local rowContentLeft = T.CARD_PAD + T.ACCENT_GUTTER_W + T.ROW_CONTENT_AFTER_ACCENT_GAP
            local rowContentRight = T.CARD_PAD
            local rowEnableX = rowW - rowContentRight - T.ENABLE_SEGMENT_W
            local rowChevronX = rowEnableX - T.CHEVRON_GAP - T.CHEVRON_LANE_W
            return {
                contentLeft = T.LEFT,
                contentRight = T.RIGHT,
                contentSpan = cardW,
                sectionHeaderX = T.HEADER_LEFT,
                sectionHeaderY = -T.SECTION_TOP,
                sectionHeaderW = math.max(T.MIN_TITLE_W, contentW - T.HEADER_LEFT - T.RIGHT),
                cardX = T.LEFT,
                cardW = cardW,
                cardTitleW = math.max(T.MIN_TITLE_W, cardW - (T.CARD_PAD * 2)),
                cardContentLeft = rowContentLeft,
                cardContentRight = rowContentRight,
                cardContentW = math.max(T.MIN_BODY_W, cardW - rowContentLeft - rowContentRight),
                rowX = T.CARD_PAD,
                rowW = rowW,
                rowContentLeft = rowContentLeft,
                rowContentRight = rowContentRight,
                rowEnableX = rowEnableX,
                rowChevronX = rowChevronX,
                rowBodyLeft = rowContentLeft,
                rowBodyRight = rowContentRight,
                rowBodyW = math.max(T.MIN_BODY_W, rowW - rowContentLeft - rowContentRight),
            }
        end

        local function computeAnnouncementCardHeaderGeometry(card, G)
            local titleRowX = T.CARD_PAD
            local titleRowW = math.max(1, G.cardTitleW)
            local qualifierText = tostring(card.titleQualifierText or "")
            local qualifierVisible = qualifierText ~= ""
            local qualifierMeasuredW = 0
            if qualifierVisible then
                qualifierMeasuredW = measureAnnouncementsSingleLineWidth(qualifierText, "GameFontHighlight", 12, 0)
            end
            local qualifierW = qualifierVisible and math.min(qualifierMeasuredW, math.max(0, titleRowW - 1)) or 0
            local qualifierGap = 0
            if qualifierVisible and qualifierW > 0 and qualifierW < titleRowW then
                qualifierGap = math.min(T.CARD_TITLE_TO_QUALIFIER_GAP, math.max(0, titleRowW - qualifierW - 1))
            end
            local titleW = qualifierVisible and math.max(1, titleRowW - qualifierGap - qualifierW) or titleRowW
            local titleMeasuredW = measureAnnouncementsSingleLineWidth(tostring(card.titleBase or ""), "GameFontHighlight", 12, 0)
            local titleDisplayW = math.min(titleW, math.max(0, titleMeasuredW))
            local qualifierX = titleRowX + titleW
            if qualifierVisible then
                qualifierX = titleRowX + titleDisplayW + qualifierGap
            end
            return {
                titleX = titleRowX,
                titleW = titleW,
                qualifierX = qualifierX,
                qualifierW = qualifierW,
                qualifierVisible = qualifierVisible and qualifierW > 0,
            }
        end

        local function computeAnnouncementRowGeometry(row, G)
            local prefixText = row._previewPrefixRawText or tostring(row.previewPrefixText or "Message:")
            local prefixMeasure = measureAnnouncementsSingleLineWidth(prefixText, "GameFontHighlightSmall", 10, T.PREVIEW_PREFIX_MEASURE_FALLBACK_W)
            local prefixW = math.max(T.MIN_PREVIEW_PREFIX_W, math.min(T.MAX_PREVIEW_PREFIX_W, math.floor(prefixMeasure + T.PREVIEW_PREFIX_MEASURE_PAD)))
            local previewTestX = nil
            if row.kind == "summon" then
                previewTestX = G.rowChevronX - T.PREVIEW_TEST_GAP - T.PREVIEW_TEST_W
            end
            local previewRight = (previewTestX or G.rowChevronX) - T.PREVIEW_TO_CHEVRON_GAP
            local previewValueX = G.rowContentLeft + prefixW + T.PREVIEW_PREFIX_TO_VALUE_GAP
            local soundPickerW = math.max(T.SOUND_PICKER_MIN_W, G.rowBodyW - T.TEST_BUTTON_W - T.INLINE_BODY_CONTROL_GAP)
            return {
                rowTitleX = G.rowContentLeft,
                rowTitleW = math.max(T.MIN_TITLE_W, G.rowChevronX - G.rowContentLeft - T.ROW_TITLE_TO_RIGHT_CLUSTER_GAP),
                rowPreviewPrefixX = G.rowContentLeft,
                rowPreviewPrefixW = prefixW,
                rowPreviewValueX = previewValueX,
                rowPreviewValueW = math.max(T.MIN_PREVIEW_VALUE_W, previewRight - previewValueX),
                rowPreviewTestX = previewTestX,
                soundPickerX = G.rowBodyLeft,
                soundPickerW = soundPickerW,
                testButtonX = G.rowBodyLeft + soundPickerW + T.INLINE_BODY_CONTROL_GAP,
            }
        end

        local function layoutAnnouncementRow(row, y, G)
            local rowG = computeAnnouncementRowGeometry(row, G)

            row.container:ClearAllPoints()
            row.container:SetPoint("TOPLEFT", row.container:GetParent(), "TOPLEFT", G.rowX, y)
            row.container:SetWidth(G.rowW)

            row.title:ClearAllPoints()
            row.title:SetPoint("TOPLEFT", row.container, "TOPLEFT", rowG.rowTitleX, -T.ROW_PAD_TOP - T.ROW_TITLE_TOP_NUDGE)
            local titleH = layoutAnnouncementsContainedText(row.title, row.titleText or "", rowG.rowTitleW, T.ROW_HEADER_H)

            local enableH = T.ROW_HEADER_H
            if row.enabled and row.enabled.Layout then
                row.enabled:Layout(T.ENABLE_SEGMENT_W)
                enableH = getAnnouncementsSegmentedHeight(row.enabled, T.ROW_HEADER_H)
                setAnnouncementsSegmentedHeight(row.enabled, enableH)
            end
            if row.enabledControl then
                row.enabledControl:ClearAllPoints()
                row.enabledControl:SetPoint("TOPLEFT", row.container, "TOPLEFT", G.rowEnableX, -T.ROW_PAD_TOP)
            else
                enableH = T.ROW_HEADER_H
            end

            row.chevron:ClearAllPoints()
            row.chevron:SetPoint("TOPLEFT", row.container, "TOPLEFT", G.rowChevronX, -T.ROW_PAD_TOP + T.ROW_CHEVRON_TOP_NUDGE)
            row.chevron:SetWidth(T.CHEVRON_LANE_W)
            local chevronH = getAnnouncementsEffectiveTextHeight(row.chevron, T.ROW_HEADER_H)
            local headerLineH = math.max(T.ROW_HEADER_H, titleH, enableH, chevronH)
            local previewY = T.ROW_PAD_TOP + headerLineH + T.ROW_HEADER_TO_PREVIEW_GAP + T.ROW_PREVIEW_TOP_NUDGE

            row.previewPrefix:ClearAllPoints()
            row.previewPrefix:SetPoint("TOPLEFT", row.container, "TOPLEFT", rowG.rowPreviewPrefixX, -previewY)
            local previewPrefixH = layoutAnnouncementsContainedText(row.previewPrefix, row._previewPrefixRawText or tostring(row.previewPrefixText or "Message:"), rowG.rowPreviewPrefixW, T.ROW_PREVIEW_LINE_H)

            row.previewValue:ClearAllPoints()
            row.previewValue:SetPoint("TOPLEFT", row.container, "TOPLEFT", rowG.rowPreviewValueX, -previewY)
            local previewValueH = layoutAnnouncementsContainedText(row.previewValue, row._previewRawText or "", rowG.rowPreviewValueW, T.ROW_PREVIEW_LINE_H)
            local previewTextLaneH = math.max(T.ROW_PREVIEW_LINE_H, previewPrefixH, previewValueH)
            local previewLineH = previewTextLaneH

            if row.kind == "summon" and row.previewTestButton then
                local previewTestH = getAnnouncementsEffectiveControlHeight(row.previewTestButton, math.max(18, T.ROW_PREVIEW_LINE_H))
                row.previewTestButton:SetHeight(previewTestH)
                previewTestH = layoutAnnouncementsContainedButtonText(row.previewTestButton, "|>", 4, previewTestH)
                if row.isExpanded then
                    row.previewTestButton:Hide()
                else
                    previewLineH = math.max(previewLineH, previewTestH)
                    local previewTestTopY = -(previewY + math.floor((previewTextLaneH - previewTestH) * 0.5))
                    row.previewTestButton:Show()
                    row.previewTestButton:ClearAllPoints()
                    row.previewTestButton:SetPoint("TOPLEFT", row.container, "TOPLEFT", rowG.rowPreviewTestX, previewTestTopY)
                end
            end

            local collapsedH = T.ROW_PAD_TOP + headerLineH + T.ROW_HEADER_TO_PREVIEW_GAP + previewLineH + T.ROW_PAD_BOTTOM
            local headerBandH = T.ROW_PAD_TOP + headerLineH + T.ROW_HEADER_TO_PREVIEW_GAP + previewLineH + T.ROW_HEADER_BAND_EXTRA
            local bodyStartY = T.ROW_PAD_TOP + headerLineH + T.ROW_HEADER_TO_PREVIEW_GAP + previewLineH + T.ROW_BODY_TOP_GAP
            local rowH = collapsedH

            if row.isExpanded and row.available then
                local cursorY = bodyStartY
                if row.kind == "template" then
                    local expandedInfoText = tostring(row.expandedInfoText or "")
                    if expandedInfoText ~= "" and row.expandedInfo then
                        cursorY = cursorY + T.ROW_EXPANDED_INFO_TOP_GAP
                        row.expandedInfo:Show()
                        row.expandedInfo:ClearAllPoints()
                        row.expandedInfo:SetPoint("TOPLEFT", row.container, "TOPLEFT", G.rowBodyLeft, -cursorY)
                        local expandedInfoH = layoutAnnouncementsContainedText(row.expandedInfo, expandedInfoText, G.rowBodyW, T.HELPER_LINE_H)
                        cursorY = cursorY + expandedInfoH + T.CONTROL_TO_HINT_GAP
                    elseif row.expandedInfo then
                        row.expandedInfo:Hide()
                    end
                    row.messageLabel:Show()
                    row.messageEdit:Show()
                    row.resetButton:Show()
                    row.messageLabel:ClearAllPoints()
                    row.messageLabel:SetPoint("TOPLEFT", row.container, "TOPLEFT", G.rowBodyLeft, -cursorY)
                    local messageLabelH = layoutAnnouncementsContainedText(row.messageLabel, row.messageLabelText or "", G.rowBodyW, T.FIELD_LABEL_H)
                    cursorY = cursorY + messageLabelH + T.LABEL_TO_CONTROL_GAP

                    local messageEditH = getAnnouncementsEffectiveControlHeight(row.messageEdit, T.MESSAGE_EDIT_H)
                    row.messageEdit:ClearAllPoints()
                    row.messageEdit:SetPoint("TOPLEFT", row.container, "TOPLEFT", G.rowBodyLeft, -cursorY)
                    row.messageEdit:SetSize(G.rowBodyW, messageEditH)
                    cursorY = cursorY + messageEditH

                    local routingHintText = tostring(row.routingHintText or "")
                    if routingHintText ~= "" then
                        row.routingHint:Show()
                        cursorY = cursorY + T.CONTROL_TO_HINT_GAP
                        row.routingHint:ClearAllPoints()
                        row.routingHint:SetPoint("TOPLEFT", row.container, "TOPLEFT", G.rowBodyLeft, -cursorY)
                        local routingHintH = layoutAnnouncementsContainedText(row.routingHint, routingHintText, G.rowBodyW, T.HELPER_LINE_H)
                        cursorY = cursorY + routingHintH
                    else
                        row.routingHint:Hide()
                    end

                    if row.showDestinationToken then
                        local destinationHelperText = tostring(row.destinationHelperText or "")
                        local showDestinationHelper = destinationHelperText ~= ""
                        if showDestinationHelper then
                            row.destinationHelper:Show()
                        else
                            row.destinationHelper:Hide()
                        end
                        row.destinationChip:Show()
                        if showDestinationHelper then
                            cursorY = cursorY + T.CONTROL_TO_HINT_GAP
                            row.destinationHelper:ClearAllPoints()
                            row.destinationHelper:SetPoint("TOPLEFT", row.container, "TOPLEFT", G.rowBodyLeft, -cursorY)
                            local destinationHelperH = layoutAnnouncementsContainedText(row.destinationHelper, destinationHelperText, G.rowBodyW, T.HELPER_LINE_H)
                            cursorY = cursorY + destinationHelperH + T.HELPER_TO_CHIP_GAP
                        else
                            cursorY = cursorY + T.CONTROL_TO_HINT_GAP
                        end

                        local chipTextW = measureAnnouncementsSingleLineWidth(row.destinationChipText or "[destination]", "GameFontHighlightSmall", 10, 0)
                        local chipW = math.max(T.TOKEN_CHIP_MIN_W, math.floor(chipTextW + (T.TOKEN_CHIP_PAD_X * 2) + T.TOKEN_CHIP_TEXT_SLACK))
                        local chipH = getAnnouncementsEffectiveControlHeight(row.destinationChip, T.TOKEN_CHIP_H)
                        row.destinationChip:SetWidth(chipW)
                        row.destinationChip:SetHeight(chipH)
                        chipH = layoutAnnouncementsContainedButtonText(row.destinationChip, row.destinationChipText or "[destination]", T.TOKEN_CHIP_PAD_X, chipH)
                        row.destinationChip:ClearAllPoints()
                        row.destinationChip:SetPoint("TOPLEFT", row.container, "TOPLEFT", G.rowBodyLeft, -cursorY)
                        cursorY = cursorY + chipH
                    else
                        row.destinationHelper:Hide()
                        row.destinationChip:Hide()
                    end

                    cursorY = cursorY + T.CHIP_TO_RESET_GAP
                    local resetButtonH = getAnnouncementsEffectiveControlHeight(row.resetButton, T.CONTROL_H)
                    row.resetButton:SetHeight(resetButtonH)
                    resetButtonH = layoutAnnouncementsContainedButtonText(row.resetButton, "Reset", 8, resetButtonH)
                    row.resetButton:ClearAllPoints()
                    row.resetButton:SetPoint("TOPRIGHT", row.container, "TOPRIGHT", -G.rowContentRight, -cursorY)
                    cursorY = cursorY + resetButtonH
                    rowH = math.max(collapsedH, cursorY + T.ROW_PAD_BOTTOM)
                else
                    row.soundLabel:Show()
                    if row.soundPicker and row.soundPicker.control then
                        row.soundPicker.control:Show()
                    end
                    row.testButton:Show()
                    row.masterHint:Show()
                    row.soundLabel:ClearAllPoints()
                    row.soundLabel:SetPoint("TOPLEFT", row.container, "TOPLEFT", G.rowBodyLeft, -cursorY)
                    local soundLabelH = layoutAnnouncementsContainedText(row.soundLabel, row.soundLabelText or "", G.rowBodyW, T.FIELD_LABEL_H)
                    cursorY = cursorY + soundLabelH + T.LABEL_TO_CONTROL_GAP

                    local soundPickerControl = row.soundPicker and row.soundPicker.control or nil
                    local soundPickerH = getAnnouncementsEffectiveControlHeight(soundPickerControl, T.CONTROL_H)
                    local testButtonH = getAnnouncementsEffectiveControlHeight(row.testButton, T.CONTROL_H)
                    row.testButton:SetHeight(testButtonH)
                    testButtonH = layoutAnnouncementsContainedButtonText(row.testButton, "Test", 8, testButtonH)
                    local inlineControlH = math.max(T.CONTROL_H, soundPickerH, testButtonH)
                    if row.soundPicker and row.soundPicker.SetWidth then
                        row.soundPicker:SetWidth(rowG.soundPickerW)
                    end
                    if soundPickerControl then
                        soundPickerControl:SetHeight(inlineControlH)
                        soundPickerControl:ClearAllPoints()
                        soundPickerControl:SetPoint("TOPLEFT", row.container, "TOPLEFT", rowG.soundPickerX, -cursorY)
                    end
                    row.testButton:SetHeight(inlineControlH)
                    layoutAnnouncementsContainedButtonText(row.testButton, "Test", 8, inlineControlH)
                    row.testButton:ClearAllPoints()
                    if soundPickerControl then
                        row.testButton:SetPoint("LEFT", soundPickerControl, "RIGHT", T.INLINE_BODY_CONTROL_GAP, 0)
                    else
                        row.testButton:SetPoint("TOPLEFT", row.container, "TOPLEFT", rowG.testButtonX, -cursorY)
                    end
                    cursorY = cursorY + inlineControlH + T.CONTROL_TO_HINT_GAP

                    row.masterHint:ClearAllPoints()
                    row.masterHint:SetPoint("TOPLEFT", row.container, "TOPLEFT", G.rowBodyLeft, -cursorY)
                    local masterHintH = layoutAnnouncementsContainedText(row.masterHint, row.masterHintText or "", G.rowBodyW, T.HELPER_LINE_H)
                    cursorY = cursorY + masterHintH
                    rowH = math.max(collapsedH, cursorY + T.ROW_PAD_BOTTOM)
                end
            else
                if row.kind == "template" then
                    if row.expandedInfo then
                        row.expandedInfo:Hide()
                    end
                    row.messageLabel:Hide()
                    row.messageEdit:Hide()
                    row.resetButton:Hide()
                    row.routingHint:Hide()
                    row.destinationHelper:Hide()
                    row.destinationChip:Hide()
                else
                    row.soundLabel:Hide()
                    if row.soundPicker and row.soundPicker.control then
                        row.soundPicker.control:Hide()
                    end
                    row.testButton:Hide()
                    row.masterHint:Hide()
                end
            end

            row.container:SetHeight(rowH)
            row.headerBandBg:ClearAllPoints()
            row.headerBandBg:SetPoint("TOPLEFT", row.container, "TOPLEFT", 0, 0)
            row.headerBandBg:SetPoint("TOPRIGHT", row.container, "TOPRIGHT", 0, 0)
            row.headerBandBg:SetHeight(headerBandH)
            row.headerDivider:ClearAllPoints()
            row.headerDivider:SetPoint("TOPLEFT", row.container, "TOPLEFT", 0, -headerBandH)
            row.headerDivider:SetPoint("TOPRIGHT", row.container, "TOPRIGHT", 0, -headerBandH)
            row.headerDivider:SetHeight(T.DIVIDER_H)
            row.collapsedClickCatcher:ClearAllPoints()
            row.collapsedClickCatcher:SetPoint("TOPLEFT", row.container, "TOPLEFT", 0, 0)
            row.collapsedClickCatcher:SetPoint("BOTTOMRIGHT", row.container, "BOTTOMRIGHT", 0, 0)
            row.expandedHeaderClickCatcher:ClearAllPoints()
            row.expandedHeaderClickCatcher:SetPoint("TOPLEFT", row.container, "TOPLEFT", 0, 0)
            row.expandedHeaderClickCatcher:SetPoint("TOPRIGHT", row.container, "TOPRIGHT", 0, 0)
            row.expandedHeaderClickCatcher:SetHeight(headerBandH)
            return y - rowH - T.ROW_GAP
        end

        local function layoutAnnouncementCard(card, y, G)
            card.frame:ClearAllPoints()
            card.frame:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", G.cardX, y)
            card.frame:SetWidth(G.cardW)

            local headerG = computeAnnouncementCardHeaderGeometry(card, G)
            card.title:ClearAllPoints()
            card.title:SetPoint("TOPLEFT", card.frame, "TOPLEFT", headerG.titleX, -T.CARD_PAD)
            setAnnouncementsSingleLineWidth(card.title, headerG.titleW)
            card.title:SetText(card.titleText or tostring(card.titleBase or ""))
            local cardTitleH = getAnnouncementsEffectiveTextHeight(card.title, T.CARD_HEADER_H)

            local qualifierH = 0
            if card.titleQualifier then
                card.titleQualifier:ClearAllPoints()
                card.titleQualifier:SetPoint("TOPLEFT", card.frame, "TOPLEFT", headerG.qualifierX, -T.CARD_PAD)
                if headerG.qualifierVisible then
                    qualifierH = layoutAnnouncementsContainedText(card.titleQualifier, card.titleQualifierText or "", headerG.qualifierW, T.CARD_HEADER_H)
                else
                    setAnnouncementsSingleLineWidth(card.titleQualifier, 1)
                end
            end
            local titleRowH = math.max(T.CARD_HEADER_H, cardTitleH, qualifierH)

            local rowY = -(T.CARD_PAD + titleRowH + T.CARD_HEADER_TO_ROWS_GAP)
            if card.available then
                card.unavailableHint:Hide()
            else
                card.unavailableHint:Show()
                card.unavailableHint:ClearAllPoints()
                card.unavailableHint:SetPoint("TOPLEFT", card.frame, "TOPLEFT", T.CARD_PAD, -(T.CARD_PAD + titleRowH + T.UNAVAILABLE_HINT_TOP_GAP))
                local unavailableHintH = layoutAnnouncementsContainedText(card.unavailableHint, card.unavailableHintText or "", G.cardTitleW, T.HELPER_LINE_H)
                local unavailableBlockH = math.max(T.UNAVAILABLE_HINT_BLOCK_H, T.UNAVAILABLE_HINT_TOP_GAP + unavailableHintH)
                rowY = rowY - unavailableBlockH
            end
            local subgroupW = math.max(T.MIN_TITLE_W, G.cardW - G.cardContentLeft - T.CARD_PAD)
            if card == cardC then
                if card.subgroupAnnouncements then
                    card.subgroupAnnouncements:Show()
                    card.subgroupAnnouncements:SetTextColor(0.58, 0.51, 0.79, 0.70)
                    card.subgroupAnnouncements:ClearAllPoints()
                    card.subgroupAnnouncements:SetPoint("TOPLEFT", card.frame, "TOPLEFT", G.cardContentLeft, rowY)
                    local subgroupAnnouncementsH = layoutAnnouncementsContainedText(card.subgroupAnnouncements, card.subgroupAnnouncementsText or "", subgroupW, T.SUBGROUP_LABEL_H)
                    rowY = rowY - subgroupAnnouncementsH - T.SUBGROUP_TO_CONTENT_GAP
                end

                local templateRow = card.rows and card.rows[1] or nil
                local summonRow1 = card.rows and card.rows[2] or nil
                local summonRow2 = card.rows and card.rows[3] or nil
                if templateRow then
                    rowY = layoutAnnouncementRow(templateRow, rowY, G)
                end

                rowY = rowY - T.SUBGROUP_DIVIDER_GAP_TOP
                if card.subgroupDivider then
                    card.subgroupDivider:Show()
                    card.subgroupDivider:ClearAllPoints()
                    card.subgroupDivider:SetPoint("TOPLEFT", card.frame, "TOPLEFT", G.cardContentLeft, rowY)
                    card.subgroupDivider:SetPoint("TOPRIGHT", card.frame, "TOPRIGHT", -T.CARD_PAD, rowY)
                    card.subgroupDivider:SetHeight(T.DIVIDER_H)
                end
                rowY = rowY - T.DIVIDER_H - T.SUBGROUP_DIVIDER_GAP_BOTTOM

                if card.subgroupIncoming then
                    card.subgroupIncoming:Show()
                    card.subgroupIncoming:SetTextColor(0.58, 0.51, 0.79, 0.70)
                    card.subgroupIncoming:ClearAllPoints()
                    card.subgroupIncoming:SetPoint("TOPLEFT", card.frame, "TOPLEFT", G.cardContentLeft, rowY)
                    local subgroupIncomingH = layoutAnnouncementsContainedText(card.subgroupIncoming, card.subgroupIncomingText or "", subgroupW, T.SUBGROUP_LABEL_H)
                    rowY = rowY - subgroupIncomingH - T.SUBGROUP_TO_CONTENT_GAP
                end

                if summonRow1 then
                    rowY = layoutAnnouncementRow(summonRow1, rowY, G)
                end
                if summonRow2 then
                    rowY = layoutAnnouncementRow(summonRow2, rowY, G)
                end
            else
                if card.subgroupAnnouncements then card.subgroupAnnouncements:Hide() end
                if card.subgroupIncoming then card.subgroupIncoming:Hide() end
                if card.subgroupDivider then card.subgroupDivider:Hide() end
                for _, row in ipairs(card.rows or {}) do
                    rowY = layoutAnnouncementRow(row, rowY, G)
                end
            end

            local cardH = math.max(T.MIN_CARD_H, -rowY + T.CARD_BOTTOM_PAD)
            card.frame:SetHeight(cardH)
            return y - cardH - T.CARD_TO_CARD_GAP
        end

        local function layoutAnnouncements()
            if not scrollFrame or not scrollChild then
                return
            end
            local contentW = math.max((scrollFrame:GetWidth() or 0) - T.SCROLL_WIDTH_FUDGE, T.MIN_CONTENT_W)
            scrollChild:SetWidth(contentW)
            local G = computeAnnouncementsGeometry(contentW)
            local useHostedChrome = isPanelUsingCustomSettingsWindowHost(panelOwner)

            st.playerHeader:ClearAllPoints()
            st.playerHeader:SetPoint("TOPRIGHT", panelOwner, "TOPRIGHT", -20, -20)

            local y = -T.SECTION_TOP
            if useHostedChrome then
                y = y - SETTINGS_WINDOW_HOSTED_TOP_RHYTHM_NUDGE
            end
            if useHostedChrome then
                setSettingsWindowChromeVisible(header, false, scrollChild)
                setSettingsWindowChromeVisible(st.sectionDescriptor, false, scrollChild)
            else
                setSettingsWindowChromeVisible(header, true)
                setSettingsWindowChromeVisible(st.sectionDescriptor, true, scrollChild)
                header:ClearAllPoints()
                header:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", G.sectionHeaderX, G.sectionHeaderY)
                setAnnouncementsSingleLineWidth(header, G.sectionHeaderW)
                local sectionHeaderH = getAnnouncementsEffectiveTextHeight(header, T.SECTION_HEADER_H)
                y = y - sectionHeaderH - T.SECTION_HEADER_TO_FIRST_CARD_GAP
                y = layoutSettingsSectionDescriptor(st.sectionDescriptor, scrollChild, G.sectionHeaderX, y, G.sectionHeaderW)
            end
            y = layoutAnnouncementCard(cardA, y, G)
            y = layoutAnnouncementCard(cardB, y, G)
            y = layoutAnnouncementCard(cardC, y, G)

            scrollChild:SetHeight(math.max(scrollFrame:GetHeight(), -y + T.SECTION_BOTTOM_PAD))
            updateScrollBehavior(scrollFrame, scrollChild)
        end

        self._layout = layoutAnnouncements

        registerRefreshCallback(function()
            if shouldSuppressSettingsFirstOpenExternalRefresh(panelOwner) then
                return
            end
            if panelOwner and panelOwner._refresh then
                panelOwner._refresh()
            end
        end, panelOwner)
        scrollFrame:SetScript("OnSizeChanged", function()
            if handleSettingsFirstOpenSizeReady(panelOwner, "size_changed") then
                return
            end
            if shouldSuppressSettingsFirstOpenExternalRefresh(panelOwner) then
                return
            end
            if panelOwner and panelOwner._layout then
                panelOwner._layout()
            end
        end)

        self._settingsFirstOpenKey = "announcements"
        self._settingsFirstOpenSizeTarget = scrollFrame
        self._settingsFirstOpenRender = function(panel)
            if panel and panel._layout then
                panel._layout()
            end
            if panel and panel._refresh then
                panel._refresh()
            end
        end
        self._settingsFirstOpenIsReady = function(panel)
            local state = panel and panel._annState or nil
            local scroll = panel and panel._sizingFrame or nil
            local child = state and state.scrollChild or nil
            local firstRow = state and state.cardA and state.cardA.rows and state.cardA.rows[1] or nil
            local useHostedChrome = isPanelUsingCustomSettingsWindowHost(panel)
            return panelHasSizedViewport(scroll)
                and state ~= nil
                and state.header ~= nil
                and (useHostedChrome or state.header:IsShown())
                and state.cardA ~= nil and state.cardA.frame ~= nil and state.cardA.frame:IsShown()
                and state.cardB ~= nil and state.cardB.frame ~= nil and state.cardB.frame:IsShown()
                and state.cardC ~= nil and state.cardC.frame ~= nil and state.cardC.frame:IsShown()
                and firstRow ~= nil and firstRow.container ~= nil and firstRow.container:IsShown()
                and child ~= nil
                and (tonumber(child:GetHeight()) or 0) >= math.max(tonumber(scroll and scroll:GetHeight()) or 0, 1)
        end
        settingsSearchRuntime.endDescriptorCapture(self)
    end
    panel:SetScript("OnShow", function(selfPanel)
        debugSettingsLog("OnShow:", selfPanel.name)
        ensureSettingsCanvasPanelAnchored(selfPanel)
        selfPanel:EnsureBuilt()
        if not handleSettingsFirstOpenPanelShow(selfPanel) then
            withPanelHydration(selfPanel, function()
                if selfPanel._layout then
                    selfPanel._layout()
                end
                if selfPanel._refresh then
                    selfPanel._refresh()
                end
            end)
        end
    end)

    panel:SetScript("OnHide", function(selfPanel)
        handleSettingsFirstOpenPanelHide(selfPanel)
        local st = selfPanel and selfPanel._annState
        if not st then
            return
        end
        for _, spec in ipairs(st.pickerSpecs or {}) do
            if spec and spec.ClosePicker then
                spec:ClosePicker()
            end
        end
    end)

    return panel
end
OptionsInternal.createAnnouncementsPanelLegacy = createAnnouncementsPanelLegacy
OptionsInternal.createAnnouncementsPanel = createAnnouncementsPanel
