-- Portal Authority — options/panel_combat.lua
-- Combat Alerts settings panel for death-alert audio and on-screen presentation.
--
-- Depends on: PortalAuthority.OptionsInternal shared helpers; PortalAuthority
-- Exposes:    PortalAuthority.OptionsInternal.createCombatAlertsPanel
-- DB keys:    deathAlert*, combatAlertsEnabled
-- Events:     none
-- Search key: combat

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 measureSettingsVisibleContentExtent = OptionsInternal.measureSettingsVisibleContentExtent
local handleSettingsFirstOpenSizeReady = OptionsInternal.handleSettingsFirstOpenSizeReady
local handleSettingsFirstOpenPanelShow = OptionsInternal.handleSettingsFirstOpenPanelShow
local handleSettingsFirstOpenPanelHide = OptionsInternal.handleSettingsFirstOpenPanelHide
local shouldSuppressSettingsFirstOpenExternalRefresh = OptionsInternal.shouldSuppressSettingsFirstOpenExternalRefresh
local isPanelUsingCustomSettingsWindowHost = OptionsInternal.isPanelUsingCustomSettingsWindowHost
local refreshSettingsWindowHeaderAccessoryIfHosted = OptionsInternal.refreshSettingsWindowHeaderAccessoryIfHosted
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 createCombatAlertsPanel()
    local panel = CreateFrame("Frame")
    panel.name = "Combat Alerts"
    panel._paSearchSectionKey = "combat"
    panel._paSearchSectionName = panel.name

    panel.EnsureBuilt = function(self)
        -- Only treat as initialized if the panel actually finished wiring layout/refresh.
        if self._initialized and self._layout and self._refresh then return end
        resetSettingsFirstOpenPanelState(self)
        ensureDatabase()
        settingsSearchRuntime.beginDescriptorCapture(self)

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

        local widgets = {}
        local panelRef = self

        local function onApply()
            if PortalAuthority and PortalAuthority.ApplyCombatAlertsSettings then
                PortalAuthority:ApplyCombatAlertsSettings()
            end

            -- If test mode is currently active, immediately re-render the test message so changes are visible live.
            if PortalAuthority
                and PortalAuthority.DeathAlerts_IsTestModeActive
                and PortalAuthority.DeathAlerts_RefreshTestMode
                and PortalAuthority:DeathAlerts_IsTestModeActive()
            then
                PortalAuthority:DeathAlerts_RefreshTestMode()
            end

            if panelRef and panelRef._refresh then
                panelRef._refresh()
            end
        end

        local function getDeathAlertSoundChoices(currentValue)
            local media = getPortalMedia()
            if media and media.GetChoices then
                return media.GetChoices("sound", {
                    includeNone = true,
                    includeBuiltinIDs = true,
                    currentValue = currentValue,
                }) or {}
            end
            return buildFallbackSoundChoices(currentValue, {
                includeNone = true,
                includeBuiltinIDs = true,
            })
        end

        local function previewDeathAlertSound(value)
            if value == nil or value == "" then
                return
            end

            local soundFile = value
            if PortalAuthority and PortalAuthority.GetDeathAlertSoundFile then
                soundFile = PortalAuthority:GetDeathAlertSoundFile(value)
            end
            if not soundFile or soundFile == "" then
                return
            end

            local channel = "Master"
            if PortalAuthorityDB and type(PortalAuthorityDB.deathAlertSoundChannel) == "string" and PortalAuthorityDB.deathAlertSoundChannel ~= "" then
                channel = PortalAuthorityDB.deathAlertSoundChannel
            elseif PortalAuthority and PortalAuthority.defaults and type(PortalAuthority.defaults.deathAlertSoundChannel) == "string" and PortalAuthority.defaults.deathAlertSoundChannel ~= "" then
                channel = PortalAuthority.defaults.deathAlertSoundChannel
            end

            if type(soundFile) == "number" and PlaySound then
                PlaySound(soundFile, channel)
            elseif PlaySoundFile then
                PlaySoundFile(soundFile, channel)
            end
        end

        local function stylePremiumEditBox(edit, textSize)
            if not edit then
                return
            end
            textSize = tonumber(textSize) or 12
            edit:SetAutoFocus(false)
            edit:SetMultiLine(false)
            edit:SetTextInsets(8, 8, 0, 0)
            edit:SetTextColor(1, 1, 1, 1)
            edit:SetHighlightColor(1, 1, 1, 0.25)
            applyGlobalFontToObject(edit, textSize)

            if not edit._paPremiumBg then
                edit._paPremiumBg = edit:CreateTexture(nil, "ARTWORK")
                edit._paPremiumBg:SetAllPoints(edit)
                edit._paPremiumBg:SetTexture("Interface\\Buttons\\WHITE8x8")
                edit._paPremiumBg:SetVertexColor(0.10, 0.11, 0.14, 0.95)

                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,
                })
                edit._paPremiumBorder:SetBackdropBorderColor(0.24, 0.27, 0.32, 1.0)
            end

            local function hideTemplateChrome()
                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
            end

            local function applyVisual()
                hideTemplateChrome()
                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", applyVisual)
            edit:HookScript("OnEditFocusGained", applyVisual)
            edit:HookScript("OnEditFocusLost", applyVisual)
            applyVisual()
        end

        local function createPremiumPickerButton(parent, labelText, optionsProvider, getter, setter, onApplyCallback, pickerOpts)
            pickerOpts = type(pickerOpts) == "table" and pickerOpts or {}
            local label = createStyledFontString(parent, "ARTWORK", "GameFontHighlight", 12)
            label:SetText(labelText 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(132, math.floor(tonumber(pickerOpts.width) or 180)))

            local text = createStyledFontString(button, "ARTWORK", "GameFontHighlightSmall", 11)
            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)

            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 currentButtonLabel = ""

            local function setButtonLabel(labelText)
                currentButtonLabel = tostring(labelText or "")
                text:SetWordWrap(false)
                if text.SetMaxLines then
                    text:SetMaxLines(1)
                end
                local maxWidth = math.max(20, (button:GetWidth() or 0) - 26)
                text:SetWidth(maxWidth)
                text:SetText(currentButtonLabel)
                if pickerOpts and pickerOpts.ellipsis == false then
                    return
                end
                if text:GetStringWidth() <= maxWidth then
                    return
                end

                local source = currentButtonLabel
                local cut = string.len(source)
                while cut > 0 do
                    local candidate = string.sub(source, 1, cut) .. "..."
                    text:SetText(candidate)
                    if text:GetStringWidth() <= maxWidth then
                        return
                    end
                    cut = cut - 1
                end
                text:SetText("...")
            end

            local picker = CreatePicker(UIParent, {}, function(value)
                local selectedValue = value
                if selectedValue == nil then
                    selectedValue = ""
                end
                if setter then
                    setter(selectedValue)
                end
                if onApplyCallback then
                    onApplyCallback()
                end
            end, {
                title = tostring(pickerOpts.title or "Select Option"),
                searchHint = tostring(pickerOpts.searchHint or "Search"),
                width = tonumber(pickerOpts.pickerWidth) or 420,
                height = tonumber(pickerOpts.pickerHeight) or 360,
                minHeight = tonumber(pickerOpts.minHeight),
                showSecondary = false,
                useOptionFontPreview = pickerOpts.useOptionFontPreview and true or false,
                useOptionTexturePreview = pickerOpts.useOptionTexturePreview and true or false,
                previewFontSize = tonumber(pickerOpts.previewFontSize) or 12,
                rowHeight = tonumber(pickerOpts.rowHeight) or 26,
                showClearButton = false,
                noSearch = pickerOpts.noSearch and true or false,
                hideTitle = pickerOpts.hideTitle and true or false,
                hideScrollBar = pickerOpts.hideScrollBar and true or false,
                onOptionRightClick = pickerOpts.onOptionRightClick,
                footerHint = tostring(pickerOpts.footerHint or ""),
            })

            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

            button:SetScript("OnEnter", function()
                applyVisual(true)
            end)
            button:SetScript("OnLeave", function()
                applyVisual(false)
            end)

            local function buildPickerOptions()
                local built = {}
                for _, option in ipairs(optionsProvider and 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 ""),
                        previewFontPath = option.previewFontPath,
                        previewTexturePath = option.previewTexturePath or option.path,
                        aliases = option.aliases,
                    }
                end
                if #built == 0 then
                    built[1] = { value = "", label = "None", buttonLabel = "None", secondary = "" }
                end
                return built
            end

            local function resolveSelection(rawValue, options)
                local selected = rawValue
                if selected == nil then
                    selected = ""
                end
                local selectedKey = tostring(selected)
                local selectedLabel = nil
                local emptyFallbackLabel = tostring(pickerOpts.emptyLabel or "None")
                for _, option in ipairs(options or {}) do
                    if tostring(option.value or "") == selectedKey then
                        selected = option.value
                        selectedLabel = tostring(option.buttonLabel or option.label or selectedKey)
                        break
                    end
                end
                if not selectedLabel then
                    if selectedKey == "" then
                        selected = ""
                        selectedLabel = emptyFallbackLabel
                    else
                        local first = options and options[1] or nil
                        if first then
                            selected = first.value
                            selectedLabel = tostring(first.buttonLabel or first.label or tostring(selected or ""))
                            if setter then
                                setter(selected)
                            end
                        else
                            selected = ""
                            selectedLabel = "None"
                        end
                    end
                end
                return selected, selectedLabel
            end

            local spec = { kind = "picker_button", label = label, control = button, text = text, picker = picker }

            spec.SetWidth = function(_, newWidth)
                button:SetWidth(math.max(132, math.floor(tonumber(newWidth) or pickerOpts.width or 180)))
                setButtonLabel(currentButtonLabel)
            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.6)
                text:SetAlpha(allow and 1 or 0.65)
                caret:SetAlpha(allow and 1 or 0.65)
                applyVisual(false)
            end

            spec.Refresh = function()
                local options = buildPickerOptions()
                picker:SetOptions(options)
                local selectedValue, selectedLabel = resolveSelection(getter and getter() or "", options)
                picker:SetSelectedValue(selectedValue)
                setButtonLabel(selectedLabel ~= "" and selectedLabel or "None")
            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()
                setButtonLabel(currentButtonLabel)
            end)
            registerSettingsWindowVisualRefresh(button, function()
                applyVisual(button.IsMouseOver and button:IsMouseOver() or false)
            end)

            spec.Refresh()
            return spec
        end

        local header = createStyledFontString(scrollChild, "ARTWORK", "GameFontNormalLarge", 14)
        header:SetText("|cffffd100Combat Alerts|r")
        widgets.sectionDescriptor = createSettingsSectionDescriptor(scrollChild, "Control death alerts, audio, and on-screen messaging.")
        local deathHeader = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        deathHeader:SetText("Death Alerts")
        local deathDesc = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 11)
        deathDesc:SetText("Audio and/or on-screen alerts when a Tank/Healer/DPS (or you) dies in your party/raid.")
        deathDesc:SetTextColor(0.72, 0.72, 0.72, 1)
        deathDesc:SetJustifyH("LEFT")

        widgets.soundColumnLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 11)
        widgets.soundColumnLabel:SetText("Alert Sound |cff7f7f7f(select NONE for visual only)|r")
        widgets.soundColumnLabel:SetJustifyH("LEFT")
        widgets.screenColumnLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 11)
        widgets.screenColumnLabel:SetText("On-screen")
        widgets.screenColumnLabel:SetJustifyH("LEFT")
        widgets.roleHeaderRow = CreateFrame("Frame", nil, scrollChild)
        local applyOnScreenRuleWrite
        local COMBAT_ALERTS_LAYOUT = {
            GRID_UNIT = 8,
            TOP_PAD = 20,
            CONTENT_LEFT = 20,
            CONTENT_RIGHT = 20,
            HEADER_LEFT = 16,
            HEADER_FLOOR = 16,
            HEADER_BOTTOM_GAP = 14,
            DEATH_HEADER_FLOOR = 12,
            DEATH_HEADER_BOTTOM_GAP = 8,
            DEATH_DESC_FLOOR = 11,
            DEATH_DESC_BOTTOM_GAP = 17,
            LOCK_LABEL_TO_TOGGLE_GAP = 12,
            LOCK_TOGGLE_TO_RESET_GAP = 12,
            LOCK_HELPER_MULTIPLIER = 0.625,
            LOCK_DIVIDER_MULTIPLIER = 1.25,
            LOCK_ROW_FLOOR = 22,
            LOCK_SEGMENT_BUTTON_TEXT_PAD_X = 20,
            RESET_W = 102,
            RESET_H = 22,
            LOCK_BUTTON_TEXT_FALLBACK_W = 36,
            LOCK_TOGGLE_MIN_W = 96,
            LOCK_ROW_SEED_H = 22,
            ROLE_BADGE_TEXT_INSET_X = 8,
            FORCED_HELPER_Y_NUDGE = -3,
            ROLE_HEADER_Y_NUDGE = -2,
            ROLE_LABEL_PREF_W = 72,
            ROLE_LABEL_MID_W = 56,
            ROLE_LABEL_MIN_W = 48,
            ENABLE_PREF_W = 148,
            ENABLE_NARROW_W = 132,
            SCREEN_W = 128,
            ROLE_TO_ENABLE_GAP = 10,
            ENABLE_TO_SOUND_GAP = 14,
            SOUND_TO_SCREEN_GAP = 12,
            SOUND_TARGET_MIN_W = 176,
            SOUND_MIN_W = 120,
            ROLE_ROW_FLOOR = 44,
            ROLE_ROW_SEED_H = 44,
            ROLE_TOGGLE_LAYOUT_W = 124,
            ROLE_HELPER_LEFT_Y_NUDGE = -1,
            ROLE_HELPER_MIN_W = 16,
            SOUND_CONTROL_SEED_W = 196,
            SOUND_TEXT_MIN_W = 20,
            SOUND_TEXT_RIGHT_PAD = 26,
            ROLE_HEADER_ROW_FLOOR = 16,
            ROLE_HEADER_BOTTOM_GAP = 4,
            ROLE_BADGE_MIN_W = 56,
            ROLE_BADGE_H = 24,
            ROLE_HELPER_TOP_GAP = 6,
            ROLE_TO_MESSAGE_GAP = 18,
            MESSAGE_AREA_MIN_W = 260,
            MESSAGE_RIGHT_RAIL_W = 190,
            MESSAGE_INTER_BLOCK_GAP = 16,
            MESSAGE_INPUT_MIN_W = 300,
            MESSAGE_PREVIEW_MIN_W = 250,
            MESSAGE_INSERT_MIN_W = 236,
            MESSAGE_EDIT_FLOOR = 22,
            MESSAGE_EDIT_SEED_W = 170,
            MESSAGE_EDIT_SEED_H = 22,
            PREVIEW_LABEL_MIN_W = 30,
            PREVIEW_HELP_MIN_W = 60,
            INSERT_LABEL_MIN_W = 24,
            TOKEN_CHIP_SEED_W = 58,
            PREVIEW_ROW_GAP_FLOOR = 4,
            PREVIEW_LABEL_TO_HELP_GAP = 6,
            PREVIEW_HELP_WIDTH_PAD = 8,
            CHIPS_ROW_GAP_FLOOR = 4,
            INSERT_LABEL_TO_CHIPS_GAP = 6,
            TOKEN_CHIP_H = 19,
            TOKEN_CHIP_GAP = 5,
            MESSAGE_BRANCH_STACK_GAP = 16,
            MESSAGE_TO_SETTINGS_GAP = 16,
            RIGHT_TOGGLE_W = 124,
            RIGHT_LABEL_GAP = 8,
            RIGHT_CONTROL_GAP = 8,
            RIGHT_STACK_GAP = 8,
            RIGHT_LABEL_FLOOR = 12,
            RIGHT_CONTROL_FLOOR = 22,
            SETTINGS_CONTROL_OFFSET = 172,
            SETTINGS_DROPDOWN_MIN_W = 140,
            SETTINGS_DROPDOWN_MAX_W = 190,
            SETTINGS_ROW_FLOOR = 32,
            SETTINGS_LABEL_MIN_W = 80,
            SETTINGS_ROW_MIN_W = 260,
            FONT_CONTROL_MIN_W = 120,
            CHANNEL_HINT_MIN_W = 20,
            COORDS_W = 112,
            FONT_STEPPER_W = 52,
            COORDS_EDIT_SEED_W = 72,
            COORDS_EDIT_SEED_H = 22,
            FONT_PICKER_SEED_W = 154,
            FONT_SIZE_EDIT_SEED_W = 36,
            FONT_SIZE_EDIT_SEED_H = 22,
            FONT_SIZE_FALLBACK_MIN_W = 44,
            FONT_ROW_SEED_H = 30,
            CHANNEL_PICKER_SEED_W = 190,
            HELPER_FLOOR = 10,
            LABEL_FLOOR = 12,
            CHANNEL_HINT_GAP = 6,
            BOTTOM_PAD = 20,
            HEADER_MIN_W = 220,
            HELPER_MIN_W = 120,
            DIVIDER_H = 1,
            FALLBACK_CONTENT_W = 620,
            OFFSCREEN_X = -5000,
            OFFSCREEN_Y = -5000,
        }
        local ROLE_BADGE_TEXT_INSET_X = COMBAT_ALERTS_LAYOUT.ROLE_BADGE_TEXT_INSET_X
        local FORCED_HELPER_Y_NUDGE = COMBAT_ALERTS_LAYOUT.FORCED_HELPER_Y_NUDGE
        local HEADER_Y_NUDGE = COMBAT_ALERTS_LAYOUT.ROLE_HEADER_Y_NUDGE
        local TOKEN_CHIP_H = COMBAT_ALERTS_LAYOUT.TOKEN_CHIP_H
        local TOKEN_CHIP_GAP = COMBAT_ALERTS_LAYOUT.TOKEN_CHIP_GAP

        local function safeSetDropWidth(dd, w)
            if dd == nil then return end
            local name = dd.GetName and dd:GetName() or nil
            if UIDropDownMenu_SetWidth and name and _G[name .. "Button"] then
                UIDropDownMenu_SetWidth(dd, w)
                return
            end
            if dd.SetWidth then
                dd:SetWidth(w)
            end
        end

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

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

        local function measureControlHeight(widget, floor)
            local measured = 0
            if widget and widget.GetHeight then
                measured = math.ceil(widget:GetHeight() or 0)
            end
            return math.max(tonumber(floor) or 0, measured)
        end

        local function setOffscreenTopLeft(frame)
            if not frame then
                return
            end
            if frame.Hide then
                frame:Hide()
            end
            frame:ClearAllPoints()
            frame:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", COMBAT_ALERTS_LAYOUT.OFFSCREEN_X, COMBAT_ALERTS_LAYOUT.OFFSCREEN_Y)
        end

        local function setCombatWidgetVisible(frame, visible)
            if not frame then
                return
            end
            if visible then
                if frame.Show then
                    frame:Show()
                end
            else
                if frame.Hide then
                    frame:Hide()
                end
            end
        end

        local function computeCombatAlertsRoleGeometry(contentW)
            local T = COMBAT_ALERTS_LAYOUT
            local G = {
                contentW = contentW,
                contentLeft = T.CONTENT_LEFT,
                contentRight = contentW - T.CONTENT_RIGHT,
                roleLabelW = T.ROLE_LABEL_PREF_W,
                enableW = T.ENABLE_PREF_W,
                screenW = T.SCREEN_W,
            }
            G.roleX = G.contentLeft
            G.screenX = G.contentRight - G.screenW
            local function recompute()
                G.enableX = G.roleX + G.roleLabelW + T.ROLE_TO_ENABLE_GAP
                G.soundX = G.enableX + G.enableW + T.ENABLE_TO_SOUND_GAP
                G.soundW = G.screenX - T.SOUND_TO_SCREEN_GAP - G.soundX
            end
            recompute()
            if G.soundW < T.SOUND_TARGET_MIN_W then
                G.roleLabelW = T.ROLE_LABEL_MID_W
                recompute()
            end
            if G.soundW < T.SOUND_TARGET_MIN_W then
                G.roleLabelW = T.ROLE_LABEL_MIN_W
                recompute()
            end
            if G.soundW < T.SOUND_TARGET_MIN_W then
                G.enableW = T.ENABLE_NARROW_W
                recompute()
            end
            if G.soundW < T.SOUND_MIN_W then
                G.soundW = T.SOUND_MIN_W
                G.soundX = math.min(G.soundX, G.screenX - T.SOUND_TO_SCREEN_GAP - G.soundW)
            end
            return G
        end

        local function computeCombatAlertsLockGeometry(contentW, lockLabel)
            local T = COMBAT_ALERTS_LAYOUT
            local lockLabelW = lockLabel and math.ceil(lockLabel:GetStringWidth() or 0) or 0
            local lockLabelH = lockLabel and math.ceil(lockLabel:GetStringHeight() or 0) or T.LABEL_FLOOR
            local unit = math.max(1, lockLabelH)
            local lockBtnTextW = 0
            if widgets.lockPosition and type(widgets.lockPosition.buttons) == "table" then
                for _, btn in ipairs(widgets.lockPosition.buttons) do
                    if btn and btn.Text then
                        lockBtnTextW = math.max(lockBtnTextW, math.ceil(btn.Text:GetStringWidth() or 0))
                    end
                end
            end
            lockBtnTextW = math.max(lockBtnTextW, T.LOCK_BUTTON_TEXT_FALLBACK_W)
            local lockToggleWFromContent = ((lockBtnTextW + (T.LOCK_SEGMENT_BUTTON_TEXT_PAD_X * 2)) * 2) + 4
            local lockToggleWMax = math.max(
                T.LOCK_TOGGLE_MIN_W,
                (contentW - T.CONTENT_RIGHT) - (T.CONTENT_LEFT + lockLabelW + T.LOCK_LABEL_TO_TOGGLE_GAP + T.LOCK_TOGGLE_TO_RESET_GAP + T.RESET_W)
            )
            local lockToggleW = math.max(T.LOCK_TOGGLE_MIN_W, math.min(lockToggleWFromContent, lockToggleWMax))
            local toggleX = (lockLabelW > 0) and (lockLabelW + T.LOCK_LABEL_TO_TOGGLE_GAP) or 0
            local resetX = toggleX + lockToggleW + T.LOCK_TOGGLE_TO_RESET_GAP
            return {
                labelW = lockLabelW,
                labelH = lockLabelH,
                helperGap = unit * T.LOCK_HELPER_MULTIPLIER,
                dividerGap = unit * T.LOCK_DIVIDER_MULTIPLIER,
                toggleW = lockToggleW,
                toggleX = toggleX,
                resetX = resetX,
                rowW = math.max(1, contentW - T.CONTENT_RIGHT - T.CONTENT_LEFT),
            }
        end

        local function computeCombatAlertsMessageGeometry(contentW)
            local T = COMBAT_ALERTS_LAYOUT
            local messageAreaW = math.max(T.MESSAGE_AREA_MIN_W, contentW - T.CONTENT_LEFT - T.CONTENT_RIGHT)
            local leftBlockMinW = math.max(T.MESSAGE_INPUT_MIN_W, T.MESSAGE_PREVIEW_MIN_W, T.MESSAGE_INSERT_MIN_W)
            local canSideBySide = messageAreaW >= (leftBlockMinW + T.MESSAGE_RIGHT_RAIL_W + T.MESSAGE_INTER_BLOCK_GAP)
            return {
                areaW = messageAreaW,
                canSideBySide = canSideBySide,
                leftGroupW = canSideBySide and (messageAreaW - T.MESSAGE_RIGHT_RAIL_W - T.MESSAGE_INTER_BLOCK_GAP) or messageAreaW,
                rightRailW = T.MESSAGE_RIGHT_RAIL_W,
                interBlockGap = T.MESSAGE_INTER_BLOCK_GAP,
            }
        end

        local function computeCombatAlertsSettingsGeometry(contentW)
            local T = COMBAT_ALERTS_LAYOUT
            local controlX = T.CONTENT_LEFT + T.SETTINGS_CONTROL_OFFSET
            local dropdownW = math.max(
                T.SETTINGS_DROPDOWN_MIN_W,
                math.min(T.SETTINGS_DROPDOWN_MAX_W, contentW - controlX - T.CONTENT_RIGHT)
            )
            return {
                labelX = T.CONTENT_LEFT,
                controlX = controlX,
                labelW = math.max(T.SETTINGS_LABEL_MIN_W, T.SETTINGS_CONTROL_OFFSET - T.GRID_UNIT),
                rowW = math.max(T.SETTINGS_ROW_MIN_W, contentW - T.CONTENT_LEFT - T.CONTENT_RIGHT),
                dropdownW = dropdownW,
            }
        end

        local function createRoleRow(prefix, roleKey, roleLabel)
            local row = {}
            row.prefix = prefix
            row.roleKey = roleKey
            row.roleLabel = roleLabel
            row.enabledValue = false
            row._helperFallbackText = nil
            row._helperFallbackColor = nil
            row._roleHelperFullText = ""
            row._roleHelperColor = nil
            row._onScreenHelperFullText = ""
            row._paWasForcedByNone = false
            row.container = CreateFrame("Frame", nil, scrollChild)
            row.container:SetSize(1, COMBAT_ALERTS_LAYOUT.ROLE_ROW_SEED_H)
            row.enabled = createPremiumSegmentedChoice(scrollChild, "", nil, {
                { label = "Disabled", value = false, toggleState = "off" },
                { label = "Enabled", value = true, toggleState = "on" },
            }, function(v)
                if PortalAuthorityDB then
                    PortalAuthorityDB[prefix .. "Enabled"] = PA_SafeBool(v)
                end
                row.enabledValue = PA_SafeBool(v)
                if applyOnScreenRuleWrite and PortalAuthorityDB then
                    applyOnScreenRuleWrite(row, PortalAuthorityDB, row.enabledValue)
                end
                onApply()
            end, {
                semantic = "toggle",
                normalizeValue = function(v)
                    return PA_SafeBool(v)
                end,
                getValue = function()
                    return PA_SafeBool(row.enabledValue)
                end,
                setValue = function(v)
                    row.enabledValue = PA_SafeBool(v)
                end,
            })
            if row.enabled and row.enabled.label then
                row.enabled.label:SetText("")
                row.enabled.label:Hide()
            end
            if row.enabled and row.enabled.Layout then
                row.enabled:Layout(COMBAT_ALERTS_LAYOUT.ROLE_TOGGLE_LAYOUT_W)
            end
            row.icon = scrollChild:CreateTexture(nil, "ARTWORK")
            row.icon:Hide()
            row.roleBadge = CreateFrame("Frame", nil, row.container, "BackdropTemplate")
            row.roleBadge:SetBackdrop({
                bgFile = "Interface\\Buttons\\WHITE8x8",
                edgeFile = "Interface\\Buttons\\WHITE8x8",
                edgeSize = 1,
                insets = { left = 0, right = 0, top = 0, bottom = 0 },
            })
            row.roleBadge:SetBackdropColor(0.07, 0.08, 0.10, 0.78)
            row.roleBadge:SetBackdropBorderColor(0.18, 0.20, 0.24, 0.56)
            row.roleBadge:EnableMouse(false)
            row.roleText = createStyledFontString(row.roleBadge, "ARTWORK", "GameFontHighlight", 12)
            row.roleText:SetJustifyH("LEFT")
            row.roleText:SetWordWrap(false)
            row.roleText:SetPoint("LEFT", row.roleBadge, "LEFT", ROLE_BADGE_TEXT_INSET_X, 0)
            row.roleText:SetPoint("RIGHT", row.roleBadge, "RIGHT", -ROLE_BADGE_TEXT_INSET_X, 0)
            row._roleDisplayText = roleLabel
            row._roleTextColor = { 0.94, 0.95, 0.97, 1 }
            row.sound = createPremiumPickerButton(
                scrollChild,
                "",
                function()
                    return getDeathAlertSoundChoices(PortalAuthorityDB and PortalAuthorityDB[prefix .. "Sound"] or "")
                end,
                function()
                    return normalizeSoundChoiceValue(PortalAuthorityDB and PortalAuthorityDB[prefix .. "Sound"] or "")
                end,
                function(v)
                    if PortalAuthorityDB then
                        PortalAuthorityDB[prefix .. "Sound"] = v
                    end
                end,
                function()
                    if applyOnScreenRuleWrite and PortalAuthorityDB then
                        applyOnScreenRuleWrite(row, PortalAuthorityDB, PA_SafeBool(PortalAuthorityDB[prefix .. "Enabled"]))
                    end
                    onApply()
                end,
                {
                    width = COMBAT_ALERTS_LAYOUT.SOUND_CONTROL_SEED_W,
                    title = "Select Alert Sound",
                    searchHint = "Search sounds",
                    pickerWidth = 460,
                    pickerHeight = 390,
                    onOptionRightClick = function(value)
                        previewDeathAlertSound(value)
                    end,
                    footerHint = "Right-click to preview - Left-click selects",
                }
            )
            if row.sound and row.sound.label then
                row.sound.label:SetText("")
                row.sound.label:Hide()
            end
            row._onScreenValue = false
            row.onScreen = createPremiumSegmentedChoice(scrollChild, "", nil, {
                { label = "Off", value = false, toggleState = "off" },
                { label = "On", value = true, toggleState = "on" },
            }, function(v)
                if PortalAuthorityDB then
                    PortalAuthorityDB[prefix .. "OnScreen"] = PA_SafeBool(v)
                end
                row._paOnScreenForcedByNone = false
                row._onScreenValue = PA_SafeBool(v)
                onApply()
            end, {
                semantic = "toggle",
                normalizeValue = function(v)
                    return PA_SafeBool(v)
                end,
                getValue = function()
                    return PA_SafeBool(row._onScreenValue)
                end,
                setValue = function(v)
                    row._onScreenValue = PA_SafeBool(v)
                end,
            })
            if row.onScreen and row.onScreen.label then
                row.onScreen.label:SetText("")
                row.onScreen.label:Hide()
            end
            if row.onScreen and row.onScreen.Layout then
                row.onScreen:Layout(COMBAT_ALERTS_LAYOUT.ROLE_TOGGLE_LAYOUT_W)
            end
            row.roleHelper = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
            row.roleHelper:SetText("")
            row.roleHelper:SetTextColor(0.58, 0.58, 0.58, 0.92)
            row.roleHelper:SetJustifyH("LEFT")
            row.roleHelper:SetWordWrap(false)
            if row.roleHelper.SetMaxLines then
                row.roleHelper:SetMaxLines(1)
            end
            row.onScreenHelper = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
            row.onScreenHelper:SetText("")
            row.onScreenHelper:SetTextColor(0.58, 0.58, 0.58, 0.92)
            row.onScreenHelper:SetJustifyH("LEFT")
            row.onScreenHelper:SetWordWrap(false)
            if row.onScreenHelper.SetMaxLines then
                row.onScreenHelper:SetMaxLines(1)
            end
            return row
        end

        widgets.rowTank = createRoleRow("deathAlertTank", "TANK", "Tank")
        widgets.rowHealer = createRoleRow("deathAlertHealer", "HEALER", "Healer")
        widgets.rowDps = createRoleRow("deathAlertDps", "DAMAGER", "DPS")
        widgets.rowSelf = createRoleRow("deathAlertSelf", "SELF", "Self")
        local rows = { widgets.rowTank, widgets.rowHealer, widgets.rowDps, widgets.rowSelf }

        widgets.messageLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        widgets.messageLabel:SetText("On-screen Alert Message")
        widgets.messageLabel:SetJustifyH("LEFT")
        widgets.messageEdit = CreateFrame("EditBox", nil, scrollChild)
        widgets.messageEdit:SetAutoFocus(false)
        widgets.messageEdit:SetSize(COMBAT_ALERTS_LAYOUT.MESSAGE_EDIT_SEED_W, COMBAT_ALERTS_LAYOUT.MESSAGE_EDIT_SEED_H)
        widgets.messageEdit:SetTextInsets(8, 8, 0, 0)
        widgets.messageEdit:SetTextColor(1, 1, 1, 1)
        widgets.messageEdit:SetHighlightColor(1, 1, 1, 0.25)
        applyGlobalFontToObject(widgets.messageEdit, 12)
        stylePremiumEditBox(widgets.messageEdit, 12)
        local defaults = (PortalAuthority and PortalAuthority.defaults) or {}
        local tmpl = trim((PortalAuthorityDB and PortalAuthorityDB.deathAlertMessageTemplate) or "")
        if tmpl == "" then
            tmpl = defaults.deathAlertMessageTemplate or "[role] Died: [name]"
            if PortalAuthorityDB then
                PortalAuthorityDB.deathAlertMessageTemplate = tmpl
            end
        end
        widgets.messageEdit:SetText(tmpl)
        if widgets.messageEdit.SetCursorPosition then widgets.messageEdit:SetCursorPosition(0) end
        if widgets.messageEdit.HighlightText then widgets.messageEdit:HighlightText(0, 0) end
        widgets.messagePreviewLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
        widgets.messagePreviewLabel:SetText("Preview")
        widgets.messagePreviewLabel:SetTextColor(0.70, 0.70, 0.72, 0.90)
        widgets.messagePreviewLabel:SetJustifyH("LEFT")
        widgets.messageHelp = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
        local tankIconInline = "|TInterface\\LFGFrame\\UI-LFG-ICON-ROLES:18:18:0:0:64:64:0:19:22:41|t"
        if C_Texture and C_Texture.GetAtlasInfo and C_Texture.GetAtlasInfo("groupfinder-icon-role-large-tank") then
            -- Atlas render avoids artifacting from cropped sprite coordinates.
            tankIconInline = "|A:groupfinder-icon-role-large-tank:16:16:0:-1|a"
        end
        widgets.messageHelp:SetText(string.format(
            "%s Tank Died: |c%sStreetxo|r",
            tankIconInline,
            getClassHex("PALADIN")
        ))
        widgets.messageHelp:SetTextColor(0.82, 0.82, 0.84, 0.90)
        widgets.messageHelp:SetJustifyH("LEFT")
        widgets.messageHelp:SetWordWrap(false)
        if widgets.messageHelp.SetMaxLines then
            widgets.messageHelp:SetMaxLines(1)
        end
        widgets.messageHelp._paFullText = widgets.messageHelp:GetText() or ""
        widgets.insertLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
        widgets.insertLabel:SetText("Insert:")
        widgets.insertLabel:SetTextColor(0.70, 0.70, 0.72, 0.90)
        widgets.insertLabel:SetJustifyH("LEFT")
        widgets.showRoleIcon = createPremiumSegmentedChoice(scrollChild, "Show Role Icon", "deathAlertShowRoleIcon", {
            { label = "Off", value = false, toggleState = "off" },
            { label = "On", value = true, toggleState = "on" },
        }, function()
            onApply()
        end, {
            semantic = "toggle",
            normalizeValue = function(v)
                return PA_SafeBool(v)
            end,
            getValue = function()
                return PA_SafeBool(PortalAuthorityDB and PortalAuthorityDB.deathAlertShowRoleIcon)
            end,
            setValue = function(v)
                if PortalAuthorityDB then
                    PortalAuthorityDB.deathAlertShowRoleIcon = PA_SafeBool(v)
                end
            end,
        })
        widgets.tokenChipsLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
        widgets.tokenChipsLabel:SetText("")
        widgets.tokenChipsLabel:Hide()

        local function upsertMessageTemplateFromText(rawText)
            if not PortalAuthorityDB then
                return
            end
            local defaultsRef = (PortalAuthority and PortalAuthority.defaults) or {}
            local text = trim(rawText or "")
            if text == "" then
                text = defaultsRef.deathAlertMessageTemplate or "[role] Died: [name]"
            end
            PortalAuthorityDB.deathAlertMessageTemplate = text
        end

        local function insertMessageToken(token)
            local edit = widgets.messageEdit
            if not edit then
                return
            end
            local current = tostring(edit:GetText() or "")
            local cursor = edit.GetCursorPosition and edit:GetCursorPosition() or nil
            cursor = tonumber(cursor) or #current
            if cursor < 0 then
                cursor = 0
            elseif cursor > #current then
                cursor = #current
            end
            local updated = string.sub(current, 1, cursor) .. tostring(token or "") .. string.sub(current, cursor + 1)
            edit:SetText(updated)
            if edit.SetCursorPosition then
                edit:SetCursorPosition(cursor + string.len(tostring(token or "")))
            end
            if edit.SetFocus then
                edit:SetFocus()
            end
            upsertMessageTemplateFromText(updated)
            onApply()
        end

        local function createTokenChip(labelText, tokenText, width)
            local chip = createActionButton(scrollChild, tostring(labelText or ""), nil, width or 62)
            styleSecondaryActionButtonShared(chip, {
                baseBg = { 0.06, 0.07, 0.09, 0.95 },
                hoverBg = { 0.08, 0.09, 0.11, 0.95 },
                pressedBg = { 0.05, 0.06, 0.08, 0.96 },
                baseBorder = { 0.17, 0.19, 0.23, 0.54 },
                hoverBorder = { 0.21, 0.24, 0.28, 0.60 },
                pressedBorder = { 0.19, 0.22, 0.26, 0.58 },
                textColor = { 0.78, 0.79, 0.83, 1 },
            })
            chip:SetScript("OnClick", function()
                insertMessageToken(tokenText)
            end)
            chip:SetHeight(TOKEN_CHIP_H)
            local chipText = chip.Text or (chip.GetFontString and chip:GetFontString()) or nil
            if chipText then
                applyGlobalFontToObject(chipText, 10)
            end
            return chip
        end

        widgets.tokenChipRole = createTokenChip("[role]", "[role]", COMBAT_ALERTS_LAYOUT.TOKEN_CHIP_SEED_W)
        widgets.tokenChipName = createTokenChip("[name]", "[name]", COMBAT_ALERTS_LAYOUT.TOKEN_CHIP_SEED_W)
        widgets.tokenChipClass = createTokenChip("[class]", "[class]", COMBAT_ALERTS_LAYOUT.TOKEN_CHIP_SEED_W)

        widgets.settingsHeader = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        widgets.settingsHeader:SetText("")
        widgets.roleMessageDivider = scrollChild:CreateTexture(nil, "ARTWORK")
        widgets.roleMessageDivider:SetColorTexture(0.24, 0.27, 0.32, 0.10)
        widgets.messageLeftGroup = CreateFrame("Frame", nil, scrollChild)
        widgets.messageRightGroup = CreateFrame("Frame", nil, scrollChild)
        widgets.coordsRowContainer = CreateFrame("Frame", nil, scrollChild)
        widgets.channelRowContainer = CreateFrame("Frame", nil, scrollChild)
        widgets.masterRowContainer = CreateFrame("Frame", nil, scrollChild)
        widgets.masterRowContainer:SetSize(1, COMBAT_ALERTS_LAYOUT.LOCK_ROW_SEED_H)
        widgets.lockRowContainer = CreateFrame("Frame", nil, scrollChild)
        widgets.lockRowContainer:SetSize(1, COMBAT_ALERTS_LAYOUT.LOCK_ROW_SEED_H)
        widgets.lockStateHelper = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
        widgets.lockStateHelper:SetText("")
        widgets.lockStateHelper:SetTextColor(0.58, 0.58, 0.58, 0.92)
        widgets.lockStateHelper:SetWordWrap(false)
        widgets.lockStateHelper:SetJustifyH("LEFT")
        widgets.moduleEnabled = createPremiumSegmentedChoice(scrollChild, "Module", nil, {
            { label = "Off", value = false, toggleState = "off" },
            { label = "On", value = true, toggleState = "on" },
        }, function(v)
            if PortalAuthorityDB then
                PortalAuthorityDB.combatAlertsEnabled = PA_SafeBool(v)
            end
            onApply()
        end, {
            semantic = "toggle",
            normalizeValue = function(v)
                return PA_SafeBool(v)
            end,
            getValue = function()
                return PortalAuthorityDB == nil or PortalAuthorityDB.combatAlertsEnabled ~= false
            end,
            setValue = function(v)
                if PortalAuthorityDB then
                    PortalAuthorityDB.combatAlertsEnabled = PA_SafeBool(v)
                end
            end,
        })
        self._settingsWindowHeaderToggleSource = widgets.moduleEnabled
        widgets.lockPosition = createPremiumSegmentedChoice(scrollChild, "Lock Position", nil, {
            { label = "Unlocked", value = false },
            { label = "Locked", value = true },
        }, function(v)
            if PortalAuthorityDB then
                PortalAuthorityDB.deathAlertLocked = PA_SafeBool(v)
            end
            onApply()
        end, {
            semantic = "selection",
            selectionVariant = "lock_meta",
            normalizeValue = function(v)
                return PA_SafeBool(v)
            end,
            getValue = function()
                return PA_SafeBool(PortalAuthorityDB and PortalAuthorityDB.deathAlertLocked)
            end,
            setValue = function(v)
                if PortalAuthorityDB then
                    PortalAuthorityDB.deathAlertLocked = PA_SafeBool(v)
                end
            end,
        })

        widgets.settingsDivider = scrollChild:CreateTexture(nil, "ARTWORK")
        widgets.settingsDivider:SetColorTexture(0.24, 0.27, 0.32, 0.10)
        widgets.antiSpam = createPremiumSegmentedChoice(scrollChild, "Anti-spam Raid Wipe", "deathAlertAntiSpamWipeEnabled", {
            { label = "Off", value = false, toggleState = "off" },
            { label = "On", value = true, toggleState = "on" },
        }, function()
            onApply()
        end, {
            semantic = "toggle",
            normalizeValue = function(v)
                return PA_SafeBool(v)
            end,
            getValue = function()
                return PA_SafeBool(PortalAuthorityDB and PortalAuthorityDB.deathAlertAntiSpamWipeEnabled)
            end,
            setValue = function(v)
                if PortalAuthorityDB then
                    PortalAuthorityDB.deathAlertAntiSpamWipeEnabled = PA_SafeBool(v)
                end
            end,
        })
        do
            local antiSpamTooltip = "Suppresses repeated death alerts during a raid wipe."
            local antiSpamLabel = widgets.antiSpam and widgets.antiSpam.label or nil
            if antiSpamLabel then
                local hit = antiSpamLabel._paAntiSpamTooltipHit
                if not hit then
                    hit = CreateFrame("Frame", nil, scrollChild)
                    hit:SetPoint("TOPLEFT", antiSpamLabel, "TOPLEFT", -2, 2)
                    hit:SetPoint("BOTTOMRIGHT", antiSpamLabel, "BOTTOMRIGHT", 2, -2)
                    hit:EnableMouse(true)
                    hit:SetScript("OnEnter", function(selfHit)
                        GameTooltip:SetOwner(selfHit, "ANCHOR_TOP")
                        GameTooltip:SetText(antiSpamTooltip, 1, 1, 1, 1)
                        GameTooltip:Show()
                    end)
                    hit:SetScript("OnLeave", function()
                        GameTooltip:Hide()
                    end)
                    antiSpamLabel._paAntiSpamTooltipHit = hit
                end
                hit:SetShown(antiSpamLabel:IsShown())
            end
        end

        widgets.coordsLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        widgets.coordsLabel:SetText("Coordinates")
        widgets.coordsLabel:SetJustifyH("LEFT")
        widgets.coordsEdit = CreateFrame("EditBox", nil, scrollChild)
        widgets.coordsEdit:SetAutoFocus(false)
        widgets.coordsEdit:SetSize(COMBAT_ALERTS_LAYOUT.COORDS_EDIT_SEED_W, COMBAT_ALERTS_LAYOUT.COORDS_EDIT_SEED_H)
        widgets.coordsEdit:SetEnabled(true)
        widgets.coordsEdit:EnableMouse(true)
        widgets.coordsEdit:SetTextColor(0.86, 0.86, 0.88, 1)
        widgets.coordsEdit:SetAlpha(0.90)
        widgets.coordsEdit:SetJustifyH("CENTER")
        applyGlobalFontToObject(widgets.coordsEdit, 12)
        stylePremiumEditBox(widgets.coordsEdit, 12)
        widgets.resetAnchor = createActionButton(scrollChild, "Reset Anchor", function()
            if PortalAuthorityDB then
                PortalAuthorityDB.deathAlertLocked = false
                PortalAuthorityDB.deathAlertX = 0
                PortalAuthorityDB.deathAlertY = 0
            end
            onApply()
        end, COMBAT_ALERTS_LAYOUT.RESET_W)
        styleSecondaryActionButtonShared(widgets.resetAnchor, {
            baseBg = { 0.06, 0.07, 0.09, 0.95 },
            hoverBg = { 0.08, 0.09, 0.11, 0.95 },
            pressedBg = { 0.05, 0.06, 0.08, 0.96 },
            baseBorder = { 0.21, 0.24, 0.28, 0.62 },
            hoverBorder = { 0.25, 0.28, 0.33, 0.68 },
            pressedBorder = { 0.23, 0.26, 0.30, 0.66 },
            textColor = { 0.82, 0.83, 0.86, 1 },
        })

        local channelChoices = SUMMON_SOUND_CHANNEL_CHOICES
        if type(channelChoices) ~= "table" or #channelChoices == 0 then
            channelChoices = {
                { label = "Master", value = "Master" },
                { label = "SFX", value = "SFX" },
                { label = "Music", value = "Music" },
                { label = "Ambience", value = "Ambience" },
                { label = "Dialog", value = "Dialog" },
            }
        end

        widgets.font = createPremiumPickerButton(
            scrollChild,
            "Font / Size",
            buildSharedFontPickerChoices,
            function()
                return tostring((PortalAuthorityDB and PortalAuthorityDB.deathAlertFontPath) or "")
            end,
            function(v)
                if PortalAuthorityDB then
                    PortalAuthorityDB.deathAlertFontPath = tostring(v or "")
                end
            end,
            onApply,
            {
                width = COMBAT_ALERTS_LAYOUT.FONT_PICKER_SEED_W,
                title = "Select Font Face",
                searchHint = "Search fonts by name",
                pickerWidth = 420,
                pickerHeight = 380,
                useOptionFontPreview = true,
                previewFontSize = 13,
                emptyLabel = "Global UI Font",
            }
        )
        widgets.fontSizeLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        widgets.fontSizeLabel:SetText("")
        widgets.fontSizeStepper = nil
        if type(createCanonicalDockFontSizeStepperControl) == "function" then
            widgets.fontSizeStepper = createCanonicalDockFontSizeStepperControl(
                scrollChild,
                "",
                8,
                72,
                1,
                "deathAlertFontSize",
                52,
                function()
                    onApply()
                end,
                nil,
                { micro = true, shiftMultiplier = 5 }
            )
            if widgets.fontSizeStepper and widgets.fontSizeStepper.label then
                widgets.fontSizeStepper.label:SetText("")
                widgets.fontSizeStepper.label:Hide()
            end
        else
            widgets.fontSizeEdit = CreateFrame("EditBox", nil, scrollChild)
            widgets.fontSizeEdit:SetAutoFocus(false)
            widgets.fontSizeEdit:SetSize(COMBAT_ALERTS_LAYOUT.FONT_SIZE_EDIT_SEED_W, COMBAT_ALERTS_LAYOUT.FONT_SIZE_EDIT_SEED_H)
            widgets.fontSizeEdit:SetNumeric(true)
            widgets.fontSizeEdit:SetTextInsets(8, 8, 0, 0)
            widgets.fontSizeEdit:SetTextColor(1, 1, 1, 1)
            applyGlobalFontToObject(widgets.fontSizeEdit, 12)
            stylePremiumEditBox(widgets.fontSizeEdit, 12)
        end
        widgets.fontRowContainer = CreateFrame("Frame", nil, scrollChild)
        widgets.fontRowContainer:SetSize(1, COMBAT_ALERTS_LAYOUT.FONT_ROW_SEED_H)

        widgets.channel = createPremiumPickerButton(
            scrollChild,
            "Audio Channel",
            function()
                return channelChoices
            end,
            function()
                return tostring((PortalAuthorityDB and PortalAuthorityDB.deathAlertSoundChannel) or "")
            end,
            function(v)
                if PortalAuthorityDB then
                    PortalAuthorityDB.deathAlertSoundChannel = tostring(v or "")
                end
            end,
            onApply,
            {
                -- 5 visible rows in compact popup mode.
                rowHeight = 26,
                pickerHeight = 192,
                minHeight = 192,
                width = COMBAT_ALERTS_LAYOUT.CHANNEL_PICKER_SEED_W,
                title = "Select Audio Channel",
                searchHint = "Search channels",
                pickerWidth = 380,
                noSearch = true,
                hideTitle = true,
                hideScrollBar = true,
            }
        )
        widgets.channelHint = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 11)
        widgets.channelHint:SetText("")
        widgets.channelHint:SetTextColor(0.72, 0.72, 0.72, 1)
        widgets.channelHint:SetJustifyH("LEFT")
        widgets.channelHint:Hide()

        local function setDropdownEnabled(spec, enabled)
            if not (spec and spec.control) then return end
            if spec.SetEnabled then
                spec:SetEnabled(enabled)
                return
            end
            local dd = spec.control
            if UIDropDownMenu_EnableDropDown and UIDropDownMenu_DisableDropDown then
                if enabled then UIDropDownMenu_EnableDropDown(dd) else UIDropDownMenu_DisableDropDown(dd) end
            end
            if dd.EnableMouse then dd:EnableMouse(enabled) end
            local alpha = enabled and 1 or 0.6
            dd:SetAlpha(alpha)
            local name = dd.GetName and dd:GetName()
            if name then
                local button = _G[name .. "Button"]
                local text = _G[name .. "Text"]
                if button then
                    button:SetAlpha(alpha)
                    if button.EnableMouse then button:EnableMouse(enabled) end
                    if button.SetEnabled then button:SetEnabled(enabled) end
                end
                if text then
                    text:SetAlpha(alpha)
                end
            end
        end

        local function updateCoordsDisplay()
            if not widgets.coordsEdit then return end
            if widgets._coordsEditing and widgets.coordsEdit:HasFocus() then
                return
            end
            local x = math.floor(tonumber(PortalAuthorityDB and PortalAuthorityDB.deathAlertX) or 0)
            local y = math.floor(tonumber(PortalAuthorityDB and PortalAuthorityDB.deathAlertY) or 0)
            widgets.coordsEdit:SetText(string.format("%d, %d", x, y))
            widgets.coordsEdit:SetJustifyH("CENTER")
            if widgets.coordsEdit.SetCursorPosition then widgets.coordsEdit:SetCursorPosition(0) end
            if widgets.coordsEdit.HighlightText then widgets.coordsEdit:HighlightText(0, 0) end
            widgets.coordsEdit:SetTextColor(0.86, 0.86, 0.88, 1)
            widgets.coordsEdit:SetAlpha(0.90)
            if widgets.coordsEdit.EnableKeyboard then
                widgets.coordsEdit:EnableKeyboard(false)
            end
        end

        local function setCoordsEditMode(editing)
            widgets._coordsEditing = editing and true or false
            if not widgets.coordsEdit then
                return
            end
            if widgets.coordsEdit.EnableKeyboard then
                widgets.coordsEdit:EnableKeyboard(widgets._coordsEditing)
            end
            if widgets._coordsEditing then
                widgets.coordsEdit:SetJustifyH("CENTER")
                widgets.coordsEdit:SetTextColor(1, 1, 1, 1)
                widgets.coordsEdit:SetAlpha(1)
            else
                widgets.coordsEdit:SetJustifyH("CENTER")
                widgets.coordsEdit:SetTextColor(0.86, 0.86, 0.88, 1)
                widgets.coordsEdit:SetAlpha(0.90)
            end
        end

        local function flashCoordsInvalid()
            local border = widgets.coordsEdit and widgets.coordsEdit._paPremiumBorder
            if not border or not border.SetBackdropBorderColor then
                return
            end
            border:SetBackdropBorderColor(0.42, 0.20, 0.20, 1)
            if C_Timer and C_Timer.After then
                C_Timer.After(0.12, function()
                    if not widgets.coordsEdit or (widgets._coordsEditing and widgets.coordsEdit:HasFocus()) then
                        return
                    end
                    if widgets.coordsEdit._paPremiumBorder and widgets.coordsEdit._paPremiumBorder.SetBackdropBorderColor then
                        widgets.coordsEdit._paPremiumBorder:SetBackdropBorderColor(0.24, 0.27, 0.32, 1.0)
                    end
                end)
            end
        end

        local function parseCoordsInput(raw)
            local values = {}
            for token in string.gmatch(tostring(raw or ""), "[-+]?%d+%.?%d*") do
                values[#values + 1] = tonumber(token)
            end
            if #values ~= 2 or not values[1] or not values[2] then
                return nil, nil
            end
            return math.floor(values[1]), math.floor(values[2])
        end

        local function beginCoordsEdit()
            if not widgets.coordsEdit or widgets._coordsEditing then
                return
            end
            setCoordsEditMode(true)
            widgets.coordsEdit:SetFocus()
            if widgets.coordsEdit.HighlightText then
                widgets.coordsEdit:HighlightText()
            end
        end

        local function finishCoordsEdit(commit)
            if not widgets.coordsEdit or not widgets._coordsEditing then
                return
            end

            if commit then
                local x, y = parseCoordsInput(widgets.coordsEdit:GetText())
                if x ~= nil and y ~= nil and PortalAuthorityDB then
                    PortalAuthorityDB.deathAlertX = x
                    PortalAuthorityDB.deathAlertY = y
                    setCoordsEditMode(false)
                    widgets.coordsEdit:ClearFocus()
                    onApply()
                    return
                end
                flashCoordsInvalid()
            end

            setCoordsEditMode(false)
            widgets.coordsEdit:ClearFocus()
            updateCoordsDisplay()
        end

        local function SetRoleIcon(row, role)
            if not row or not row.icon then return end
            if role == "HEALER" then
                if row.icon.SetAtlas then row.icon:SetAtlas(nil) end
                row.icon:SetTexture("Interface\\LFGFrame\\UI-LFG-ICON-ROLES")
                if GetTexCoordsForRole then
                    local l, r, t, b = GetTexCoordsForRole("HEALER")
                    if l and r and t and b then
                        row.icon:SetTexCoord(l, r, t, b)
                    else
                        row.icon:SetTexCoord(0, 1, 0, 1)
                    end
                else
                    row.icon:SetTexCoord(0, 1, 0, 1)
                end
                return
            end
            local atlasName = nil
            if role == "TANK" then
                atlasName = "groupfinder-icon-role-large-tank"
            else
                atlasName = "groupfinder-icon-role-large-dps"
            end

            local atlasOk = false
            if C_Texture and C_Texture.GetAtlasInfo and atlasName then
                atlasOk = (C_Texture.GetAtlasInfo(atlasName) ~= nil)
            end

            if atlasOk and row.icon.SetAtlas then
                row.icon:SetAtlas(atlasName)
                row.icon:SetTexCoord(0, 1, 0, 1)
                return
            end

            if row.icon.SetAtlas then
                row.icon:SetAtlas(nil)
            end
            row.icon:SetTexture("Interface\\LFGFrame\\UI-LFG-ICON-ROLES")
            if GetTexCoordsForRole then
                local l, r, t, b = GetTexCoordsForRole(role)
                if l and r and t and b then
                    row.icon:SetTexCoord(l, r, t, b)
                    return
                end
            end
            row.icon:SetTexCoord(0, 1, 0, 1)
        end

        local function updateSelfVisual(row)
            local playerName = UnitName and UnitName("player") or "Player"
            if Ambiguate then
                playerName = Ambiguate(playerName, "short")
            end
            local _, classFile = nil, nil
            if UnitClass then
                _, classFile = UnitClass("player")
            end
            local classColor = (classFile and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classFile]) or NORMAL_FONT_COLOR
            local helperText = tostring(playerName or "Player")
            row._roleDisplayText = "Player"
            row._roleTextColor = { 0.94, 0.95, 0.97, 1 }
            row._helperFallbackText = helperText
            row._helperFallbackColor = {
                math.max(0, math.min(1, ((classColor and classColor.r) or 1) * 0.80)),
                math.max(0, math.min(1, ((classColor and classColor.g) or 1) * 0.80)),
                math.max(0, math.min(1, ((classColor and classColor.b) or 1) * 0.80)),
                0.85,
            }
            row._roleHelperFullText = helperText
            row._roleHelperColor = {
                row._helperFallbackColor[1],
                row._helperFallbackColor[2],
                row._helperFallbackColor[3],
                row._helperFallbackColor[4],
            }
            local specIndex = GetSpecialization and GetSpecialization()
            if specIndex and GetSpecializationInfo then
                local _, _, _, specIcon = GetSpecializationInfo(specIndex)
                if specIcon then
                    if row.icon.SetAtlas then
                        row.icon:SetAtlas(nil)
                    end
                    row.icon:SetTexture(specIcon)
                    row.icon:SetTexCoord(0, 1, 0, 1)
                    return
                end
            end
            if classFile and CLASS_ICON_TCOORDS and CLASS_ICON_TCOORDS[classFile] then
                if row.icon.SetAtlas then
                    row.icon:SetAtlas(nil)
                end
                row.icon:SetTexture("Interface\\GLUES\\CHARACTERCREATE\\UI-CHARACTERCREATE-CLASSES")
                local coords = CLASS_ICON_TCOORDS[classFile]
                row.icon:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
            else
                if row.icon.SetAtlas then
                    row.icon:SetAtlas(nil)
                end
                row.icon:SetTexture("Interface\\ICONS\\INV_Misc_QuestionMark")
                row.icon:SetTexCoord(0, 1, 0, 1)
            end
        end
        local function updateRoleIcon(row)
            if row.roleKey == "SELF" then
                updateSelfVisual(row)
                return
            end
            row._roleDisplayText = tostring(row.roleLabel or "")
            row._roleTextColor = { 0.94, 0.95, 0.97, 1 }
            row._helperFallbackText = nil
            row._helperFallbackColor = nil
            row._roleHelperFullText = ""
            row._roleHelperColor = nil
            SetRoleIcon(row, row.roleKey)
        end

        local function getRowSoundValue(db, prefix)
            if not db then
                return ""
            end
            local value = db[prefix .. "Sound"]
            if value == nil then
                return ""
            end
            if type(value) == "number" then
                return tostring(value)
            end
            return tostring(value)
        end

        applyOnScreenRuleWrite = function(row, db, enabled)
            if not row or not db then
                return
            end
            local onScreenKey = row.prefix .. "OnScreen"
            local isEnabled = PA_SafeBool(enabled)
            local soundIsNone = (getRowSoundValue(db, row.prefix) == "")
            local isForced = isEnabled and soundIsNone
            local wasForced = row._paWasForcedByNone and true or false

            if not isEnabled then
                db[onScreenKey] = false
                row._paOnScreenForcedByNone = false
                row._paWasForcedByNone = false
                row._onScreenValue = false
                return
            end

            if isForced then
                if (not wasForced) and (not PA_SafeBool(db[onScreenKey])) then
                    db[onScreenKey] = true
                elseif db[onScreenKey] == nil then
                    db[onScreenKey] = true
                end
                row._paOnScreenForcedByNone = true
                row._paWasForcedByNone = true
                row._onScreenValue = true
                return
            end

            if wasForced or row._paOnScreenForcedByNone then
                db[onScreenKey] = false
            end
            db[onScreenKey] = PA_SafeBool(db[onScreenKey])
            row._paOnScreenForcedByNone = false
            row._paWasForcedByNone = false
            row._onScreenValue = PA_SafeBool(db[onScreenKey])
        end

        local function applyOnScreenRuleVisual(row, db, enabled)
            if not row or not db then
                return
            end
            local isEnabled = PA_SafeBool(enabled)
            local soundIsNone = (getRowSoundValue(db, row.prefix) == "")
            local isForced = isEnabled and soundIsNone
            local visualValue = false
            local allowInteraction = isEnabled

            if isForced then
                visualValue = true
                row._paOnScreenForcedByNone = true
                row._paWasForcedByNone = true
            elseif not isEnabled then
                visualValue = false
                row._paOnScreenForcedByNone = false
                row._paWasForcedByNone = false
            else
                visualValue = PA_SafeBool(db[row.prefix .. "OnScreen"])
                row._paOnScreenForcedByNone = false
                row._paWasForcedByNone = false
            end

            row._onScreenValue = visualValue
            if row.onScreen and row.onScreen.SetChoiceEnabled then
                row.onScreen:SetChoiceEnabled(false, not isForced)
                row.onScreen:SetChoiceEnabled(true, true)
            end
            if row.onScreen and row.onScreen.SetVisualSelectedValue then
                row.onScreen:SetVisualSelectedValue(visualValue)
            elseif row.onScreen and row.onScreen.SetSelectedValue then
                row.onScreen:SetSelectedValue(visualValue, false)
            end
            if row.onScreen and row.onScreen.SetEnabled then
                row.onScreen:SetEnabled(allowInteraction)
            end
            if row.onScreenHelper then
                if soundIsNone then
                    row.onScreenHelper:SetText("Required when Sound=None")
                    row.onScreenHelper:SetTextColor(0.58, 0.58, 0.58, 0.92)
                    row._onScreenHelperFullText = "Required when Sound=None"
                else
                    row.onScreenHelper:SetText("")
                    row.onScreenHelper:SetTextColor(0.58, 0.58, 0.58, 0.92)
                    row._onScreenHelperFullText = ""
                end
            end
        end

        local function updateMessagePreviewLine(db)
            if not widgets.messageHelp then
                return
            end
            local iconPart = ""
            if PA_SafeBool(db and db.deathAlertShowRoleIcon) then
                iconPart = (tankIconInline or "") .. " "
            end
            local previewText = string.format("%sTank Died: |c%sStreetxo|r", iconPart, getClassHex("PALADIN"))
            widgets.messageHelp._paFullText = previewText
            widgets.messageHelp:SetText(previewText)
        end

        local function syncCombatAlertsSelectionControl(spec, value)
            if not spec then
                return
            end
            local normalized = PA_SafeBool(value)
            if spec.SetVisualSelectedValue then
                spec:SetVisualSelectedValue(normalized)
            elseif spec.SetSelectedValue then
                spec:SetSelectedValue(normalized, false)
            end
        end

        local function applyCombatAlertsModuleEnabledState(moduleEnabled)
            setSettingsControlEnabled(widgets.lockPosition, moduleEnabled)
            setSettingsControlEnabled(widgets.resetAnchor, moduleEnabled)
            setSettingsControlEnabled(widgets.antiSpam, moduleEnabled)
            setSettingsControlEnabled(widgets.showRoleIcon, moduleEnabled)
            setSettingsControlEnabled(widgets.coordsEdit, moduleEnabled)
            setSettingsControlEnabled(widgets.font, moduleEnabled)
            setSettingsControlEnabled(widgets.fontSizeStepper or widgets.fontSizeEdit, moduleEnabled)
            setSettingsControlEnabled(widgets.channel, moduleEnabled)
            setSettingsControlEnabled(widgets.messageEdit, moduleEnabled)
            setSettingsControlEnabled(widgets.tokenChipRole, moduleEnabled)
            setSettingsControlEnabled(widgets.tokenChipName, moduleEnabled)
            setSettingsControlEnabled(widgets.tokenChipClass, moduleEnabled)

            if not moduleEnabled then
                if widgets.messageEdit and widgets.messageEdit.ClearFocus then
                    widgets.messageEdit:ClearFocus()
                end
                if widgets.coordsEdit and widgets.coordsEdit.ClearFocus then
                    widgets.coordsEdit:ClearFocus()
                end
                if widgets.fontSizeEdit and widgets.fontSizeEdit.ClearFocus then
                    widgets.fontSizeEdit:ClearFocus()
                end
            end

            if widgets.lockRowContainer and widgets.lockRowContainer.SetAlpha then
                widgets.lockRowContainer:SetAlpha(moduleEnabled and 1 or 0.55)
            end
            if widgets.messageLeftGroup and widgets.messageLeftGroup.SetAlpha then
                widgets.messageLeftGroup:SetAlpha(moduleEnabled and 1 or 0.55)
            end
            if widgets.messageRightGroup and widgets.messageRightGroup.SetAlpha then
                widgets.messageRightGroup:SetAlpha(moduleEnabled and 1 or 0.55)
            end
            if widgets.coordsRowContainer and widgets.coordsRowContainer.SetAlpha then
                widgets.coordsRowContainer:SetAlpha(moduleEnabled and 1 or 0.55)
            end
            if widgets.fontRowContainer and widgets.fontRowContainer.SetAlpha then
                widgets.fontRowContainer:SetAlpha(moduleEnabled and 1 or 0.55)
            end
            if widgets.channelRowContainer and widgets.channelRowContainer.SetAlpha then
                widgets.channelRowContainer:SetAlpha(moduleEnabled and 1 or 0.55)
            end

            for _, textWidget in ipairs({
                widgets.lockStateHelper,
                widgets.soundColumnLabel,
                widgets.screenColumnLabel,
                widgets.messageLabel,
                widgets.messagePreviewLabel,
                widgets.messageHelp,
                widgets.insertLabel,
                widgets.coordsLabel,
                widgets.fontSizeLabel,
                widgets.channelHint,
            }) do
                setSettingsTextEnabled(textWidget, moduleEnabled)
            end

            for _, row in ipairs(rows) do
                local rowEnabled = row and row.enabledValue and true or false
                local rowInteractive = moduleEnabled
                local subControlInteractive = moduleEnabled and rowEnabled

                setSettingsControlEnabled(row and row.enabled or nil, rowInteractive)
                setSettingsControlEnabled(row and row.sound or nil, subControlInteractive)
                setSettingsControlEnabled(row and row.onScreen or nil, subControlInteractive)

                if row and row.container and row.container.SetAlpha then
                    row.container:SetAlpha(moduleEnabled and 1 or 0.55)
                end
                if row and row.icon then
                    row.icon:SetAlpha((moduleEnabled and rowEnabled) and 1 or 0.55)
                end
                if row and row.roleBadge then
                    row.roleBadge:SetAlpha((moduleEnabled and rowEnabled) and 1 or 0.92)
                end
                if row and row.roleText then
                    row.roleText:SetAlpha((moduleEnabled and rowEnabled) and 1 or 0.88)
                end
                setSettingsTextEnabled(row and row.roleHelper or nil, moduleEnabled)
                setSettingsTextEnabled(row and row.onScreenHelper or nil, moduleEnabled)
            end
        end

        local function refresh()
            if ensureDatabase then ensureDatabase() end
            local db = PortalAuthorityDB or {}
            local moduleEnabled = db.combatAlertsEnabled ~= false

            syncCombatAlertsSelectionControl(widgets.moduleEnabled, moduleEnabled)

            for _, row in ipairs(rows) do
                local prefix = row.prefix
                local enabled = PA_SafeBool(db[prefix .. "Enabled"])
                local rowActive = moduleEnabled and enabled
                row.enabledValue = enabled
                if row.enabled and row.enabled.SetVisualSelectedValue then
                    row.enabled:SetVisualSelectedValue(enabled)
                elseif row.enabled and row.enabled.SetSelectedValue then
                    row.enabled:SetSelectedValue(enabled, false)
                end
                if row.sound and row.sound.Refresh then
                    row.sound:Refresh()
                end
                setDropdownEnabled(row.sound, rowActive)
                updateRoleIcon(row)
                applyOnScreenRuleVisual(row, db, enabled)
                if row.roleText then
                    row.roleText:SetText(row._roleDisplayText or row.roleLabel or "")
                end
                row.icon:SetAlpha(rowActive and 1 or 0.55)
                if row.roleBadge then
                    row.roleBadge:SetAlpha(rowActive and 1 or 0.92)
                end
                row.roleText:SetAlpha(rowActive and 1 or 0.88)
                if row._roleTextColor and row.roleText then
                    row.roleText:SetTextColor(row._roleTextColor[1], row._roleTextColor[2], row._roleTextColor[3], row._roleTextColor[4] or 1)
                end
                if row.roleHelper then
                    local roleHelperText = tostring(row._roleHelperFullText or "")
                    if roleHelperText ~= "" then
                        local c = row._roleHelperColor or { 0.58, 0.58, 0.58, 0.92 }
                        row.roleHelper:SetText(roleHelperText)
                        row.roleHelper:SetTextColor(c[1], c[2], c[3], c[4] or 0.92)
                    else
                        row.roleHelper:SetText("")
                        row.roleHelper:SetTextColor(0.58, 0.58, 0.58, 0.92)
                    end
                end
            end

            syncCombatAlertsSelectionControl(widgets.lockPosition, db.deathAlertLocked)
            if widgets.lockStateHelper then
                if not moduleEnabled then
                    widgets.lockStateHelper:SetText("Combat Alerts are disabled. Re-enable to edit settings.")
                elseif PA_SafeBool(db.deathAlertLocked) then
                    widgets.lockStateHelper:SetText("Combat Alerts are locked.")
                else
                    widgets.lockStateHelper:SetText("Combat Alerts are unlocked - drag to reposition.")
                end
            end
            if widgets.antiSpam and widgets.antiSpam.Refresh then
                widgets.antiSpam:Refresh()
            end
            if widgets.channel and widgets.channel.Refresh then widgets.channel:Refresh() end
            if widgets.font and widgets.font.Refresh then widgets.font:Refresh() end
            if widgets.showRoleIcon and widgets.showRoleIcon.Refresh then
                widgets.showRoleIcon:Refresh()
            end
            updateMessagePreviewLine(db)
            updateCoordsDisplay()

            if widgets.messageEdit and not widgets.messageEdit:HasFocus() then
                widgets.messageEdit:SetText(tostring(db.deathAlertMessageTemplate or (PortalAuthority and PortalAuthority.defaults and PortalAuthority.defaults.deathAlertMessageTemplate) or ""))
                if widgets.messageEdit.HighlightText then
                    widgets.messageEdit:HighlightText(0, 0)
                end
            end

            if widgets.fontSizeStepper and widgets.fontSizeStepper.SetVisualValue then
                local v = math.floor(tonumber(db.deathAlertFontSize) or 36)
                if v < 8 then v = 8 end
                if v > 72 then v = 72 end
                widgets.fontSizeStepper:SetVisualValue(v)
            elseif widgets.fontSizeEdit and not widgets.fontSizeEdit:HasFocus() then
                local v = math.floor(tonumber(db.deathAlertFontSize) or 36)
                widgets.fontSizeEdit:SetText(tostring(v))
                if widgets.fontSizeEdit.SetCursorPosition then widgets.fontSizeEdit:SetCursorPosition(0) end
                if widgets.fontSizeEdit.HighlightText then
                    widgets.fontSizeEdit:HighlightText(0, 0)
                end
                widgets.fontSizeEdit:SetTextColor(1, 1, 1, 1)
            end

            applyCombatAlertsModuleEnabledState(moduleEnabled)
            refreshSettingsWindowHeaderAccessoryIfHosted(panelRef)
        end

        local function layout()
            local T = COMBAT_ALERTS_LAYOUT
            local contentW = scrollFrame:GetWidth()
            if not contentW or contentW <= 1 then
                contentW = self:GetWidth()
            end
            if not contentW or contentW <= 1 then
                contentW = T.FALLBACK_CONTENT_W
            end
            scrollChild:SetWidth(contentW)

            for _, control in ipairs({
                header,
                widgets.masterRowContainer,
                widgets.moduleEnabled and widgets.moduleEnabled.label or nil,
                widgets.moduleEnabled and widgets.moduleEnabled.control or nil,
                widgets.lockRowContainer,
                widgets.lockPosition and widgets.lockPosition.label or nil,
                widgets.lockPosition and widgets.lockPosition.control or nil,
                widgets.resetAnchor,
                widgets.lockStateHelper,
                widgets.settingsDivider,
                deathHeader,
                deathDesc,
                widgets.soundColumnLabel,
                widgets.screenColumnLabel,
                widgets.roleHeaderRow,
                widgets.roleMessageDivider,
                widgets.messageLeftGroup,
                widgets.messageLabel,
                widgets.messageEdit,
                widgets.messagePreviewLabel,
                widgets.messageHelp,
                widgets.insertLabel,
                widgets.tokenChipRole,
                widgets.tokenChipName,
                widgets.tokenChipClass,
                widgets.messageRightGroup,
                widgets.showRoleIcon and widgets.showRoleIcon.label or nil,
                widgets.showRoleIcon and widgets.showRoleIcon.control or nil,
                widgets.antiSpam and widgets.antiSpam.label or nil,
                widgets.antiSpam and widgets.antiSpam.control or nil,
                widgets.coordsRowContainer,
                widgets.coordsLabel,
                widgets.coordsEdit,
                widgets.fontRowContainer,
                widgets.font and widgets.font.label or nil,
                widgets.font and widgets.font.control or nil,
                widgets.fontSizeStepper and widgets.fontSizeStepper.control or widgets.fontSizeEdit,
                widgets.channelRowContainer,
                widgets.channel and widgets.channel.label or nil,
                widgets.channelHint,
                widgets.channel and widgets.channel.control or nil,
            }) do
                setCombatWidgetVisible(control, true)
            end
            for _, row in ipairs(rows) do
                if row then
                    setCombatWidgetVisible(row.container, true)
                    setCombatWidgetVisible(row.roleBadge, true)
                    setCombatWidgetVisible(row.roleText, true)
                    setCombatWidgetVisible(row.enabled and row.enabled.control or nil, true)
                    setCombatWidgetVisible(row.sound and row.sound.control or nil, true)
                    setCombatWidgetVisible(row.onScreen and row.onScreen.control or nil, true)
                    setCombatWidgetVisible(row.roleHelper, true)
                    setCombatWidgetVisible(row.onScreenHelper, true)
                end
            end

            local roleGeo = computeCombatAlertsRoleGeometry(contentW)
            local messageGeo = computeCombatAlertsMessageGeometry(contentW)
            local settingsGeo = computeCombatAlertsSettingsGeometry(contentW)
            local headerWidth = math.max(T.HEADER_MIN_W, contentW - T.HEADER_LEFT - T.CONTENT_RIGHT)
            local useHostedChrome = isPanelUsingCustomSettingsWindowHost(panelRef)
            local y = -T.TOP_PAD
            if useHostedChrome then
                y = y - SETTINGS_WINDOW_HOSTED_TOP_RHYTHM_NUDGE
            end

            if useHostedChrome then
                setCombatWidgetVisible(header, false)
                setCombatWidgetVisible(widgets.sectionDescriptor, false)
                setOffscreenTopLeft(header)
                setOffscreenTopLeft(widgets.sectionDescriptor)
            else
                setCombatWidgetVisible(header, true)
                setCombatWidgetVisible(widgets.sectionDescriptor, true)
                header:ClearAllPoints()
                header:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", T.HEADER_LEFT, y)
                header:SetWidth(headerWidth)
                if header.SetJustifyH then
                    header:SetJustifyH("LEFT")
                end
                local headerH = measureFontStringHeight(header, T.HEADER_FLOOR)
                y = y - headerH - T.HEADER_BOTTOM_GAP
                y = layoutSettingsSectionDescriptor(
                    widgets.sectionDescriptor,
                    scrollChild,
                    T.HEADER_LEFT,
                    y,
                    headerWidth
                )
            end

            local lockLabel = widgets.lockPosition and widgets.lockPosition.label or nil
            local lockGeom = computeCombatAlertsLockGeometry(contentW, lockLabel)
            if useHostedChrome then
                setCombatWidgetVisible(widgets.masterRowContainer, false)
                setCombatWidgetVisible(widgets.moduleEnabled and widgets.moduleEnabled.label or nil, false)
                setCombatWidgetVisible(widgets.moduleEnabled and widgets.moduleEnabled.control or nil, false)
                setOffscreenTopLeft(widgets.masterRowContainer)
                setOffscreenTopLeft(widgets.moduleEnabled and widgets.moduleEnabled.label or nil)
                setOffscreenTopLeft(widgets.moduleEnabled and widgets.moduleEnabled.control or nil)
            else
                setCombatWidgetVisible(widgets.masterRowContainer, true)
                setCombatWidgetVisible(widgets.moduleEnabled and widgets.moduleEnabled.label or nil, true)
                setCombatWidgetVisible(widgets.moduleEnabled and widgets.moduleEnabled.control or nil, true)
                if widgets.moduleEnabled and widgets.moduleEnabled.Layout then
                    widgets.moduleEnabled:Layout(math.max(lockGeom.toggleW, 150))
                end
                local masterLabel = widgets.moduleEnabled and widgets.moduleEnabled.label or nil
                local masterLabelH = measureFontStringHeight(masterLabel, T.LABEL_FLOOR)
                local masterToggleH = measureControlHeight(widgets.moduleEnabled and widgets.moduleEnabled.control, T.RIGHT_CONTROL_FLOOR)
                local masterRowH = math.max(T.LOCK_ROW_FLOOR, masterLabelH, masterToggleH)

                widgets.masterRowContainer:ClearAllPoints()
                widgets.masterRowContainer:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", T.CONTENT_LEFT, y)
                widgets.masterRowContainer:SetSize(lockGeom.rowW, masterRowH)
                if masterLabel then
                    masterLabel:ClearAllPoints()
                    masterLabel:SetPoint("LEFT", widgets.masterRowContainer, "LEFT", 0, 0)
                end
                if widgets.moduleEnabled and widgets.moduleEnabled.control then
                    widgets.moduleEnabled.control:ClearAllPoints()
                    widgets.moduleEnabled.control:SetPoint("LEFT", widgets.masterRowContainer, "LEFT", lockGeom.toggleX, 0)
                end
                y = y - masterRowH - 4
            end

            if widgets.lockPosition and widgets.lockPosition.Layout then
                widgets.lockPosition:Layout(lockGeom.toggleW)
            end
            local lockToggleH = measureControlHeight(widgets.lockPosition and widgets.lockPosition.control, T.RIGHT_CONTROL_FLOOR)
            local lockRowH = math.max(T.LOCK_ROW_FLOOR, lockGeom.labelH, lockToggleH, T.RESET_H)

            widgets.lockRowContainer:ClearAllPoints()
            widgets.lockRowContainer:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", T.CONTENT_LEFT, y)
            widgets.lockRowContainer:SetSize(lockGeom.rowW, lockRowH)
            if lockLabel then
                lockLabel:ClearAllPoints()
                lockLabel:SetPoint("LEFT", widgets.lockRowContainer, "LEFT", 0, 0)
            end
            if widgets.lockPosition and widgets.lockPosition.control then
                widgets.lockPosition.control:ClearAllPoints()
                widgets.lockPosition.control:SetPoint("LEFT", widgets.lockRowContainer, "LEFT", lockGeom.toggleX, 0)
            end
            if widgets.resetAnchor then
                widgets.resetAnchor:ClearAllPoints()
                widgets.resetAnchor:SetSize(T.RESET_W, T.RESET_H)
                widgets.resetAnchor:SetPoint("LEFT", widgets.lockRowContainer, "LEFT", lockGeom.resetX, 0)
            end
            y = y - lockRowH

            widgets.lockStateHelper:ClearAllPoints()
            widgets.lockStateHelper:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", T.CONTENT_LEFT, y - lockGeom.helperGap)
            widgets.lockStateHelper:SetWidth(math.max(T.HELPER_MIN_W, contentW - T.CONTENT_LEFT - T.CONTENT_RIGHT))
            if widgets.lockStateHelper.SetJustifyH then widgets.lockStateHelper:SetJustifyH("LEFT") end
            local lockHelperText = tostring(widgets.lockStateHelper:GetText() or "")
            if lockHelperText ~= "" then
                setSingleLineEllipsis(widgets.lockStateHelper, lockHelperText, math.max(T.HELPER_MIN_W, contentW - T.CONTENT_LEFT - T.CONTENT_RIGHT))
            else
            end
            local lockHelperH = measureFontStringHeight(widgets.lockStateHelper, T.HELPER_FLOOR)
            y = y - lockGeom.helperGap - lockHelperH

            if widgets.settingsDivider then
                widgets.settingsDivider:ClearAllPoints()
                local dividerY = y - (lockGeom.dividerGap * 0.5)
                widgets.settingsDivider:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", T.CONTENT_LEFT, dividerY)
                widgets.settingsDivider:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", -T.CONTENT_RIGHT, dividerY)
                widgets.settingsDivider:SetHeight(T.DIVIDER_H)
            end
            y = y - lockGeom.dividerGap

            deathHeader:ClearAllPoints()
            deathHeader:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", T.CONTENT_LEFT, y)
            deathHeader:SetWidth(headerWidth)
            if deathHeader.SetJustifyH then
                deathHeader:SetJustifyH("LEFT")
            end
            local deathHeaderH = measureFontStringHeight(deathHeader, T.DEATH_HEADER_FLOOR)
            y = y - deathHeaderH - T.DEATH_HEADER_BOTTOM_GAP

            deathDesc:ClearAllPoints()
            deathDesc:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", T.CONTENT_LEFT, y)
            deathDesc:SetWidth(math.max(T.HEADER_MIN_W, contentW - T.CONTENT_LEFT - T.CONTENT_RIGHT))
            local deathDescH = measureFontStringHeight(deathDesc, T.DEATH_DESC_FLOOR)
            y = y - deathDescH - T.DEATH_DESC_BOTTOM_GAP

            widgets.soundColumnLabel:SetWidth(math.max(T.SOUND_MIN_W, roleGeo.soundW))
            widgets.screenColumnLabel:SetWidth(roleGeo.screenW)
            local roleHeaderH = math.max(
                T.ROLE_HEADER_ROW_FLOOR,
                measureFontStringHeight(widgets.soundColumnLabel, T.HELPER_FLOOR),
                measureFontStringHeight(widgets.screenColumnLabel, T.HELPER_FLOOR)
            )
            widgets.roleHeaderRow:ClearAllPoints()
            widgets.roleHeaderRow:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, y)
            widgets.roleHeaderRow:SetSize(contentW, roleHeaderH)
            widgets.soundColumnLabel:ClearAllPoints()
            widgets.soundColumnLabel:SetPoint("LEFT", widgets.roleHeaderRow, "LEFT", roleGeo.soundX, HEADER_Y_NUDGE)
            widgets.screenColumnLabel:ClearAllPoints()
            widgets.screenColumnLabel:SetPoint("LEFT", widgets.roleHeaderRow, "LEFT", roleGeo.screenX, HEADER_Y_NUDGE)
            y = y - roleHeaderH - T.ROLE_HEADER_BOTTOM_GAP

            local function placeRow(row, topY)
                local badgeW = math.max(T.ROLE_BADGE_MIN_W, roleGeo.roleLabelW)
                if row.enabled and row.enabled.Layout then
                    row.enabled:Layout(roleGeo.enableW)
                end
                if row.onScreen and row.onScreen.Layout then
                    row.onScreen:Layout(roleGeo.screenW)
                end
                if row.sound and row.sound.SetWidth then
                    row.sound:SetWidth(roleGeo.soundW)
                elseif row.sound and row.sound.control then
                    safeSetDropWidth(row.sound.control, roleGeo.soundW)
                end
                if row.sound and row.sound.text then
                    row.sound.text:SetWidth(math.max(T.SOUND_TEXT_MIN_W, roleGeo.soundW - T.SOUND_TEXT_RIGHT_PAD))
                    row.sound.text:SetWordWrap(false)
                    if row.sound.text.SetMaxLines then
                        row.sound.text:SetMaxLines(1)
                    end
                end

                local leftHelperWidth = math.max(T.ROLE_HELPER_MIN_W, badgeW - (T.ROLE_BADGE_TEXT_INSET_X * 2))
                local leftHelperText = tostring(row._roleHelperFullText or "")
                local leftHelperH = 0
                if row.roleHelper then
                    row.roleHelper:SetWidth(leftHelperWidth)
                    row.roleHelper:SetJustifyH("LEFT")
                    row.roleHelper:SetWordWrap(false)
                    if row.roleHelper.SetMaxLines then
                        row.roleHelper:SetMaxLines(1)
                    end
                    if leftHelperText ~= "" then
                        setSingleLineEllipsis(row.roleHelper, leftHelperText, leftHelperWidth)
                        leftHelperH = measureFontStringHeight(row.roleHelper, T.HELPER_FLOOR)
                    else
                        row.roleHelper:SetText("")
                    end
                end

                local rightHelperText = tostring(row._onScreenHelperFullText or "")
                local rightHelperH = 0
                if row.onScreenHelper then
                    row.onScreenHelper:SetWidth(roleGeo.screenW)
                    row.onScreenHelper:SetJustifyH("LEFT")
                    row.onScreenHelper:SetWordWrap(false)
                    if row.onScreenHelper.SetMaxLines then
                        row.onScreenHelper:SetMaxLines(1)
                    end
                    if rightHelperText ~= "" then
                        setSingleLineEllipsis(row.onScreenHelper, rightHelperText, roleGeo.screenW)
                        rightHelperH = measureFontStringHeight(row.onScreenHelper, T.HELPER_FLOOR)
                    else
                        row.onScreenHelper:SetText("")
                    end
                end

                local helperH = math.max(leftHelperH, rightHelperH)
                local rowH = math.max(
                    T.ROLE_ROW_FLOOR,
                    math.max(
                        T.ROLE_BADGE_H,
                        measureControlHeight(row.enabled and row.enabled.control, T.RIGHT_CONTROL_FLOOR),
                        measureControlHeight(row.sound and row.sound.control, T.RIGHT_CONTROL_FLOOR),
                        measureControlHeight(row.onScreen and row.onScreen.control, T.RIGHT_CONTROL_FLOOR)
                    ) + ((helperH > 0) and (helperH + T.ROLE_HELPER_TOP_GAP) or 0)
                )

                row.container:ClearAllPoints()
                row.container:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, topY)
                row.container:SetSize(contentW, rowH)

                if row.icon then
                    row.icon:Hide()
                end

                if row.roleBadge then
                    row.roleBadge:ClearAllPoints()
                    row.roleBadge:SetPoint("LEFT", row.container, "LEFT", roleGeo.roleX, 0)
                    row.roleBadge:SetSize(badgeW, T.ROLE_BADGE_H)
                end
                if row.roleText then
                    row.roleText:ClearAllPoints()
                    row.roleText:SetPoint("LEFT", row.roleBadge or row.container, "LEFT", T.ROLE_BADGE_TEXT_INSET_X, 0)
                    row.roleText:SetPoint("RIGHT", row.roleBadge or row.container, "RIGHT", -T.ROLE_BADGE_TEXT_INSET_X, 0)
                end

                if row.enabled and row.enabled.control then
                    row.enabled.control:ClearAllPoints()
                    row.enabled.control:SetPoint("LEFT", row.container, "LEFT", roleGeo.enableX, 0)
                end

                if row.sound and row.sound.control then
                    row.sound.control:ClearAllPoints()
                    row.sound.control:SetPoint("LEFT", row.container, "LEFT", roleGeo.soundX, 0)
                end

                if row.onScreen and row.onScreen.control then
                    row.onScreen.control:ClearAllPoints()
                    row.onScreen.control:SetPoint("LEFT", row.container, "LEFT", roleGeo.screenX, 0)
                end

                if row.roleHelper then
                    row.roleHelper:ClearAllPoints()
                    if leftHelperText ~= "" then
                        setCombatWidgetVisible(row.roleHelper, true)
                        row.roleHelper:SetPoint("BOTTOMLEFT", row.container, "BOTTOMLEFT", roleGeo.roleX + T.ROLE_BADGE_TEXT_INSET_X, T.ROLE_HELPER_LEFT_Y_NUDGE)
                    else
                        setCombatWidgetVisible(row.roleHelper, false)
                        row.roleHelper:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", T.OFFSCREEN_X, T.OFFSCREEN_Y)
                    end
                end

                if row.onScreenHelper then
                    row.onScreenHelper:ClearAllPoints()
                    if rightHelperText ~= "" then
                        setCombatWidgetVisible(row.onScreenHelper, true)
                        row.onScreenHelper:SetPoint("BOTTOMLEFT", row.container, "BOTTOMLEFT", roleGeo.screenX, T.FORCED_HELPER_Y_NUDGE)
                    else
                        setCombatWidgetVisible(row.onScreenHelper, false)
                        row.onScreenHelper:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", T.OFFSCREEN_X, T.OFFSCREEN_Y)
                    end
                end

                return rowH
            end

            y = y - placeRow(widgets.rowTank, y)
            y = y - placeRow(widgets.rowHealer, y)
            y = y - placeRow(widgets.rowDps, y)
            y = y - placeRow(widgets.rowSelf, y)

            local roleDividerY = y - (T.ROLE_TO_MESSAGE_GAP * 0.5)
            widgets.roleMessageDivider:ClearAllPoints()
            widgets.roleMessageDivider:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", T.CONTENT_LEFT, roleDividerY)
            widgets.roleMessageDivider:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", -T.CONTENT_RIGHT, roleDividerY)
            widgets.roleMessageDivider:SetHeight(T.DIVIDER_H)
            y = y - T.ROLE_TO_MESSAGE_GAP

            widgets.messageLeftGroup:ClearAllPoints()
            widgets.messageLeftGroup:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", T.CONTENT_LEFT, y)
            widgets.messageLeftGroup:SetSize(messageGeo.leftGroupW, 1)

            widgets.messageLabel:ClearAllPoints()
            widgets.messageLabel:SetPoint("TOPLEFT", widgets.messageLeftGroup, "TOPLEFT", 0, 0)
            widgets.messageLabel:SetWidth(messageGeo.leftGroupW)
            if widgets.messageLabel.SetJustifyH then widgets.messageLabel:SetJustifyH("LEFT") end
            local messageLabelH = measureFontStringHeight(widgets.messageLabel, T.LABEL_FLOOR)

            widgets.messageEdit:ClearAllPoints()
            widgets.messageEdit:SetPoint("TOPLEFT", widgets.messageLeftGroup, "TOPLEFT", 0, -(messageLabelH + T.GRID_UNIT))
            widgets.messageEdit:SetWidth(messageGeo.leftGroupW)
            local editH = measureControlHeight(widgets.messageEdit, T.MESSAGE_EDIT_FLOOR)

            setOffscreenTopLeft(widgets.tokenChipsLabel)

            local previewRowGap = math.max(T.PREVIEW_ROW_GAP_FLOOR, math.floor(T.GRID_UNIT * 0.5))
            local previewRowY = -(messageLabelH + T.GRID_UNIT + editH + previewRowGap)
            widgets.messagePreviewLabel:ClearAllPoints()
            widgets.messagePreviewLabel:SetPoint("TOPLEFT", widgets.messageLeftGroup, "TOPLEFT", 0, previewRowY)
            local previewLabelW = math.max(T.PREVIEW_LABEL_MIN_W, math.ceil((widgets.messagePreviewLabel.GetStringWidth and widgets.messagePreviewLabel:GetStringWidth()) or 34))
            local previewLabelH = measureFontStringHeight(widgets.messagePreviewLabel, T.HELPER_FLOOR)

            local helpWidth = math.max(T.PREVIEW_HELP_MIN_W, messageGeo.leftGroupW - previewLabelW - T.PREVIEW_HELP_WIDTH_PAD)
            widgets.messageHelp:ClearAllPoints()
            widgets.messageHelp:SetPoint("TOPLEFT", widgets.messageLeftGroup, "TOPLEFT", previewLabelW + T.PREVIEW_LABEL_TO_HELP_GAP, previewRowY)
            setSingleLineEllipsis(widgets.messageHelp, widgets.messageHelp._paFullText, helpWidth)
            local helpH = measureFontStringHeight(widgets.messageHelp, T.HELPER_FLOOR)
            local previewRowH = math.max(previewLabelH, helpH)

            local chipsRowGap = math.max(T.CHIPS_ROW_GAP_FLOOR, math.floor(T.GRID_UNIT * 0.5))
            local insertRowY = previewRowY - previewRowH - chipsRowGap
            widgets.insertLabel:ClearAllPoints()
            widgets.insertLabel:SetPoint("TOPLEFT", widgets.messageLeftGroup, "TOPLEFT", 0, insertRowY)
            local insertLabelW = math.max(T.INSERT_LABEL_MIN_W, math.ceil((widgets.insertLabel.GetStringWidth and widgets.insertLabel:GetStringWidth()) or 0))
            local insertLabelH = measureFontStringHeight(widgets.insertLabel, T.HELPER_FLOOR)

            widgets.tokenChipRole:ClearAllPoints()
            widgets.tokenChipRole:SetPoint("TOPLEFT", widgets.messageLeftGroup, "TOPLEFT", insertLabelW + T.INSERT_LABEL_TO_CHIPS_GAP, insertRowY)
            widgets.tokenChipName:ClearAllPoints()
            widgets.tokenChipName:SetPoint("TOPLEFT", widgets.messageLeftGroup, "TOPLEFT", insertLabelW + T.INSERT_LABEL_TO_CHIPS_GAP + (widgets.tokenChipRole:GetWidth() or 0) + T.TOKEN_CHIP_GAP, insertRowY)
            widgets.tokenChipClass:ClearAllPoints()
            widgets.tokenChipClass:SetPoint(
                "TOPLEFT",
                widgets.messageLeftGroup,
                "TOPLEFT",
                insertLabelW + T.INSERT_LABEL_TO_CHIPS_GAP + (widgets.tokenChipRole:GetWidth() or 0) + T.TOKEN_CHIP_GAP + (widgets.tokenChipName:GetWidth() or 0) + T.TOKEN_CHIP_GAP,
                insertRowY
            )
            local chipH = math.max(
                T.TOKEN_CHIP_H,
                measureControlHeight(widgets.tokenChipRole, T.TOKEN_CHIP_H),
                measureControlHeight(widgets.tokenChipName, T.TOKEN_CHIP_H),
                measureControlHeight(widgets.tokenChipClass, T.TOKEN_CHIP_H)
            )
            local insertRowH = math.max(insertLabelH, chipH)
            local leftBlockH = messageLabelH + T.GRID_UNIT + editH + previewRowGap + previewRowH + chipsRowGap + insertRowH
            widgets.messageLeftGroup:SetSize(messageGeo.leftGroupW, math.max(1, leftBlockH))

            if widgets.showRoleIcon and widgets.showRoleIcon.Layout then
                widgets.showRoleIcon:Layout(T.RIGHT_TOGGLE_W)
            end
            if widgets.antiSpam and widgets.antiSpam.Layout then
                widgets.antiSpam:Layout(T.RIGHT_TOGGLE_W)
            end
            if widgets.showRoleIcon and widgets.showRoleIcon.label then
                widgets.showRoleIcon.label:SetWidth(messageGeo.rightRailW)
                if widgets.showRoleIcon.label.SetJustifyH then widgets.showRoleIcon.label:SetJustifyH("LEFT") end
            end
            if widgets.antiSpam and widgets.antiSpam.label then
                widgets.antiSpam.label:SetWidth(messageGeo.rightRailW)
                if widgets.antiSpam.label.SetJustifyH then widgets.antiSpam.label:SetJustifyH("LEFT") end
            end
            local showLabelH = measureFontStringHeight(widgets.showRoleIcon and widgets.showRoleIcon.label, T.RIGHT_LABEL_FLOOR)
            local showControlH = measureControlHeight(widgets.showRoleIcon and widgets.showRoleIcon.control, T.RIGHT_CONTROL_FLOOR)
            local antiLabelH = measureFontStringHeight(widgets.antiSpam and widgets.antiSpam.label, T.RIGHT_LABEL_FLOOR)
            local antiControlH = measureControlHeight(widgets.antiSpam and widgets.antiSpam.control, T.RIGHT_CONTROL_FLOOR)
            local firstControlTopOffset = -math.max(
                (messageLabelH + T.GRID_UNIT) + (editH * 0.5) - (showControlH * 0.5),
                showLabelH + T.RIGHT_LABEL_GAP
            )
            local antiLabelTopOffset = firstControlTopOffset - showControlH - T.RIGHT_STACK_GAP
            local antiControlTopOffset = antiLabelTopOffset - antiLabelH - T.RIGHT_CONTROL_GAP
            local rightBlockBottomY = antiControlTopOffset - antiControlH
            local rightBlockH = math.max(showLabelH, math.abs(rightBlockBottomY))

            widgets.messageRightGroup:ClearAllPoints()
            if messageGeo.canSideBySide then
                widgets.messageRightGroup:SetPoint(
                    "TOPLEFT",
                    scrollChild,
                    "TOPLEFT",
                    T.CONTENT_LEFT + messageGeo.leftGroupW + messageGeo.interBlockGap,
                    y
                )
            else
                widgets.messageRightGroup:SetPoint(
                    "TOPLEFT",
                    scrollChild,
                    "TOPLEFT",
                    T.CONTENT_LEFT,
                    y - (leftBlockH + T.MESSAGE_BRANCH_STACK_GAP)
                )
            end
            widgets.messageRightGroup:SetSize(messageGeo.rightRailW, rightBlockH)

            if widgets.showRoleIcon and widgets.showRoleIcon.label and widgets.showRoleIcon.control then
                widgets.showRoleIcon.label:ClearAllPoints()
                widgets.showRoleIcon.label:SetPoint("TOPLEFT", widgets.messageRightGroup, "TOPLEFT", 0, 0)
                widgets.showRoleIcon.control:ClearAllPoints()
                widgets.showRoleIcon.control:SetPoint("TOPLEFT", widgets.messageRightGroup, "TOPLEFT", 0, firstControlTopOffset)
            end
            if widgets.antiSpam and widgets.antiSpam.label and widgets.antiSpam.control then
                widgets.antiSpam.label:ClearAllPoints()
                widgets.antiSpam.label:SetPoint("TOPLEFT", widgets.messageRightGroup, "TOPLEFT", 0, antiLabelTopOffset)
                widgets.antiSpam.control:ClearAllPoints()
                widgets.antiSpam.control:SetPoint("TOPLEFT", widgets.messageRightGroup, "TOPLEFT", 0, antiControlTopOffset)
            end

            if messageGeo.canSideBySide then
                y = y - math.max(leftBlockH, rightBlockH)
            else
                y = y - (leftBlockH + T.MESSAGE_BRANCH_STACK_GAP + rightBlockH)
            end

            setOffscreenTopLeft(widgets.settingsHeader)
            y = y - T.MESSAGE_TO_SETTINGS_GAP

            local coordsLabelOffset = settingsGeo.controlX - settingsGeo.labelX

            widgets.coordsLabel:SetWidth(settingsGeo.labelW)
            if widgets.coordsLabel.SetJustifyH then widgets.coordsLabel:SetJustifyH("LEFT") end
            local coordsLabelH = measureFontStringHeight(widgets.coordsLabel, T.LABEL_FLOOR)
            local coordsEditH = measureControlHeight(widgets.coordsEdit, T.RIGHT_CONTROL_FLOOR)
            local coordsRowH = math.max(T.SETTINGS_ROW_FLOOR, coordsLabelH, coordsEditH)
            widgets.coordsRowContainer:ClearAllPoints()
            widgets.coordsRowContainer:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", settingsGeo.labelX, y)
            widgets.coordsRowContainer:SetSize(settingsGeo.rowW, coordsRowH)
            widgets.coordsLabel:ClearAllPoints()
            widgets.coordsLabel:SetPoint("LEFT", widgets.coordsRowContainer, "LEFT", 0, 0)
            widgets.coordsEdit:ClearAllPoints()
            widgets.coordsEdit:SetPoint("LEFT", widgets.coordsRowContainer, "LEFT", coordsLabelOffset, 0)
            widgets.coordsEdit:SetSize(T.COORDS_W, T.RIGHT_CONTROL_FLOOR)
            y = y - coordsRowH

            if widgets.font and widgets.font.label then
                widgets.font.label:SetWidth(settingsGeo.labelW)
                if widgets.font.label.SetJustifyH then widgets.font.label:SetJustifyH("LEFT") end
            end
            local fontControlW = math.max(T.FONT_CONTROL_MIN_W, settingsGeo.dropdownW - (T.FONT_STEPPER_W + T.GRID_UNIT))
            if widgets.font and widgets.font.SetWidth then
                widgets.font:SetWidth(fontControlW)
            elseif widgets.font and widgets.font.control then
                safeSetDropWidth(widgets.font.control, fontControlW)
            end
            local fontLabelH = measureFontStringHeight(widgets.font and widgets.font.label, T.LABEL_FLOOR)
            local fontControlH = measureControlHeight(widgets.font and widgets.font.control, T.RIGHT_CONTROL_FLOOR)
            local fontSizeH = math.max(
                measureControlHeight(widgets.fontSizeStepper and widgets.fontSizeStepper.control, T.RIGHT_CONTROL_FLOOR),
                measureControlHeight(widgets.fontSizeEdit, T.RIGHT_CONTROL_FLOOR)
            )
            local fontRowH = math.max(T.SETTINGS_ROW_FLOOR, fontLabelH, fontControlH, fontSizeH)
            widgets.fontRowContainer:ClearAllPoints()
            widgets.fontRowContainer:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", settingsGeo.labelX, y)
            widgets.fontRowContainer:SetSize(settingsGeo.rowW, fontRowH)
            if widgets.font and widgets.font.label then
                widgets.font.label:ClearAllPoints()
                widgets.font.label:SetPoint("LEFT", widgets.fontRowContainer, "LEFT", 0, 0)
            end
            if widgets.font and widgets.font.control then
                widgets.font.control:ClearAllPoints()
                widgets.font.control:SetPoint("LEFT", widgets.fontRowContainer, "LEFT", coordsLabelOffset, 0)
            end
            setOffscreenTopLeft(widgets.fontSizeLabel)
            if widgets.fontSizeStepper and widgets.fontSizeStepper.control then
                widgets.fontSizeStepper.control:ClearAllPoints()
                widgets.fontSizeStepper.control:SetPoint("LEFT", widgets.fontRowContainer, "LEFT", coordsLabelOffset + fontControlW + T.GRID_UNIT, 0)
                if widgets.fontSizeStepper.SetWidth then
                    widgets.fontSizeStepper:SetWidth(T.FONT_STEPPER_W)
                else
                    widgets.fontSizeStepper.control:SetWidth(T.FONT_STEPPER_W)
                end
            elseif widgets.fontSizeEdit then
                widgets.fontSizeEdit:ClearAllPoints()
                widgets.fontSizeEdit:SetPoint("LEFT", widgets.fontRowContainer, "LEFT", coordsLabelOffset + fontControlW + T.GRID_UNIT, 0)
                widgets.fontSizeEdit:SetSize(math.max(T.FONT_SIZE_FALLBACK_MIN_W, T.FONT_STEPPER_W - T.GRID_UNIT), T.RIGHT_CONTROL_FLOOR)
            end
            y = y - fontRowH

            if widgets.channel and widgets.channel.label then
                widgets.channel.label:SetWidth(settingsGeo.labelW)
                if widgets.channel.label.SetJustifyH then widgets.channel.label:SetJustifyH("LEFT") end
            end
            if widgets.channel and widgets.channel.SetWidth then
                widgets.channel:SetWidth(settingsGeo.dropdownW)
            elseif widgets.channel and widgets.channel.control then
                safeSetDropWidth(widgets.channel.control, settingsGeo.dropdownW)
            end
            local channelLabelH = measureFontStringHeight(widgets.channel and widgets.channel.label, T.LABEL_FLOOR)
            local channelControlH = measureControlHeight(widgets.channel and widgets.channel.control, T.RIGHT_CONTROL_FLOOR)
            local channelHintText = tostring(widgets.channelHint and widgets.channelHint:GetText() or "")
            local channelLabelW = math.max(0, math.ceil((widgets.channel and widgets.channel.label and widgets.channel.label.GetStringWidth and widgets.channel.label:GetStringWidth()) or 0))
            local channelHintMaxW = math.max(T.CHANNEL_HINT_MIN_W, settingsGeo.labelW - channelLabelW - T.CHANNEL_HINT_GAP)
            local channelHintH = 0
            if widgets.channelHint and widgets.channelHint:IsShown() and channelHintText ~= "" then
                setSingleLineEllipsis(widgets.channelHint, channelHintText, channelHintMaxW)
                channelHintH = measureFontStringHeight(widgets.channelHint, T.HELPER_FLOOR)
            end
            local channelRowH = math.max(T.SETTINGS_ROW_FLOOR, channelLabelH, channelControlH, channelHintH)
            widgets.channelRowContainer:ClearAllPoints()
            widgets.channelRowContainer:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", settingsGeo.labelX, y)
            widgets.channelRowContainer:SetSize(settingsGeo.rowW, channelRowH)
            if widgets.channel and widgets.channel.label then
                widgets.channel.label:ClearAllPoints()
                widgets.channel.label:SetPoint("LEFT", widgets.channelRowContainer, "LEFT", 0, 0)
            end
            if widgets.channelHint then
                widgets.channelHint:ClearAllPoints()
                widgets.channelHint:SetPoint("LEFT", widgets.channelRowContainer, "LEFT", channelLabelW + T.CHANNEL_HINT_GAP, 0)
                widgets.channelHint:SetWidth(channelHintMaxW)
            end
            if widgets.channel and widgets.channel.control then
                widgets.channel.control:ClearAllPoints()
                widgets.channel.control:SetPoint("LEFT", widgets.channelRowContainer, "LEFT", coordsLabelOffset, 0)
            end
            y = y - channelRowH

            local extentObjects = {
                header,
                widgets.masterRowContainer,
                widgets.lockRowContainer,
                widgets.lockStateHelper,
                widgets.settingsDivider,
                deathHeader,
                deathDesc,
                widgets.roleHeaderRow,
                widgets.rowTank and widgets.rowTank.container or nil,
                widgets.rowHealer and widgets.rowHealer.container or nil,
                widgets.rowDps and widgets.rowDps.container or nil,
                widgets.rowSelf and widgets.rowSelf.container or nil,
                widgets.rowTank and widgets.rowTank.roleHelper or nil,
                widgets.rowHealer and widgets.rowHealer.roleHelper or nil,
                widgets.rowDps and widgets.rowDps.roleHelper or nil,
                widgets.rowSelf and widgets.rowSelf.roleHelper or nil,
                widgets.rowTank and widgets.rowTank.onScreenHelper or nil,
                widgets.rowHealer and widgets.rowHealer.onScreenHelper or nil,
                widgets.rowDps and widgets.rowDps.onScreenHelper or nil,
                widgets.rowSelf and widgets.rowSelf.onScreenHelper or nil,
                widgets.roleMessageDivider,
                widgets.messageLeftGroup,
                widgets.messageRightGroup,
                widgets.messageHelp,
                widgets.coordsRowContainer,
                widgets.fontRowContainer,
                widgets.channelRowContainer,
            }
            local measuredExtent = measureSettingsVisibleContentExtent(scrollChild, extentObjects)
            local visibleExtent = (measuredExtent and math.ceil(measuredExtent)) or nil
            local fallbackExtent = math.abs(y)
            local used = (visibleExtent or fallbackExtent) + T.BOTTOM_PAD
            local contentHeight = math.max(scrollFrame:GetHeight(), used)
            scrollChild:SetHeight(contentHeight)
            updateScrollBehavior(scrollFrame, scrollChild)
        end
        local function commitMessageTemplate()
            if not PortalAuthorityDB then return end
            local raw = widgets.messageEdit:GetText() or ""
            local text = trim(raw)
            if text == "" then
                text = (PortalAuthority and PortalAuthority.defaults and PortalAuthority.defaults.deathAlertMessageTemplate) or "[role] Died: [name]"
            end
            PortalAuthorityDB.deathAlertMessageTemplate = text
            onApply()
        end

        widgets.messageEdit:SetScript("OnTextChanged", function(edit, userInput)
            if not userInput or not PortalAuthorityDB then
                return
            end

            local text = trim(edit:GetText() or "")
            if text == "" then
                text = (PortalAuthority and PortalAuthority.defaults and PortalAuthority.defaults.deathAlertMessageTemplate) or "[role] Died: [name]"
            end
            PortalAuthorityDB.deathAlertMessageTemplate = text

            -- Real-time preview while unlocked.
            if not PA_SafeBool(PortalAuthorityDB.deathAlertLocked) then
                onApply()
            end
        end)

        widgets.messageEdit:SetScript("OnEditFocusGained", function(edit)
            if edit.HighlightText then
                -- Prevent InputBoxTemplate select-all on focus; keep normal cursor editing.
                edit:HighlightText(0, 0)
            end
        end)

        widgets.messageEdit:SetScript("OnEnterPressed", function(edit)
            commitMessageTemplate()
            edit:ClearFocus()
        end)
        widgets.messageEdit:SetScript("OnEditFocusLost", commitMessageTemplate)

        widgets.coordsEdit:SetScript("OnMouseDown", function()
            beginCoordsEdit()
        end)
        widgets.coordsEdit:SetScript("OnEditFocusGained", function(edit)
            if not widgets._coordsEditing then
                edit:ClearFocus()
                return
            end
            if edit.HighlightText then
                edit:HighlightText()
            end
        end)
        widgets.coordsEdit:SetScript("OnEnterPressed", function(edit)
            finishCoordsEdit(true)
            edit:ClearFocus()
        end)
        widgets.coordsEdit:SetScript("OnEscapePressed", function(edit)
            finishCoordsEdit(false)
            edit:ClearFocus()
        end)
        widgets.coordsEdit:SetScript("OnEditFocusLost", function()
            finishCoordsEdit(true)
        end)

        if widgets.fontSizeEdit then
            local function commitFontSize()
                if not PortalAuthorityDB then return end
                local v = tonumber(widgets.fontSizeEdit:GetText())
                if v == nil then
                    widgets.fontSizeEdit:SetText(tostring(PortalAuthorityDB.deathAlertFontSize or 36))
                    return
                end
                v = math.floor(v)
                if v < 8 then v = 8 end
                if v > 72 then v = 72 end
                PortalAuthorityDB.deathAlertFontSize = v
                onApply()
            end

            widgets.fontSizeEdit:SetScript("OnEnterPressed", function(edit)
                commitFontSize()
                edit:ClearFocus()
            end)
            widgets.fontSizeEdit:SetScript("OnEditFocusLost", commitFontSize)
        end

        self._refresh = refresh
        self._layout = layout
        registerRefreshCallback(function()
            if shouldSuppressSettingsFirstOpenExternalRefresh(self) then
                return
            end
            if self._refresh then
                self._refresh()
            end
        end, self)
        scrollFrame:SetScript("OnSizeChanged", function()
            if handleSettingsFirstOpenSizeReady(self, "size_changed") then
                return
            end
            if shouldSuppressSettingsFirstOpenExternalRefresh(self) then
                return
            end
            if self._layout then self._layout() end
            if self._refresh then self._refresh() end
        end)
        self._settingsFirstOpenKey = "combat"
        self._settingsFirstOpenSizeTarget = scrollFrame
        self._settingsFirstOpenRender = function(panel)
            if panel._layout then
                panel._layout()
            end
            if panel._refresh then
                panel._refresh()
            end
        end
        self._settingsFirstOpenIsReady = function(panel)
            return panelHasSizedViewport(scrollFrame)
                and widgets.rowTank ~= nil and widgets.rowTank.container ~= nil and widgets.rowTank.container:IsShown()
                and widgets.rowHealer ~= nil and widgets.rowHealer.container ~= nil and widgets.rowHealer.container:IsShown()
                and widgets.messageEdit ~= nil and widgets.messageEdit:IsShown()
                and widgets.coordsEdit ~= nil and widgets.coordsEdit:IsShown()
                and (tonumber(scrollChild:GetHeight()) or 0) >= math.max(tonumber(scrollFrame:GetHeight()) or 0, 1)
        end

        if PortalAuthority then
            PortalAuthority.DeathAlerts_OnPositionChanged = function(x, y)
                if not panelRef or not panelRef:IsShown() or not widgets.coordsEdit then
                    return
                end
                if widgets._coordsEditing and widgets.coordsEdit:HasFocus() then
                    return
                end
                widgets.coordsEdit:SetText(string.format("%d, %d", math.floor(tonumber(x) or 0), math.floor(tonumber(y) or 0)))
                widgets.coordsEdit:SetTextColor(0.86, 0.86, 0.88, 1)
            end
        end

        layout()
        refresh()

        self._initialized = true
        settingsSearchRuntime.endDescriptorCapture(self)
    end

    panel:SetScript("OnShow", function(self)
        -- Ensure this canvas panel fills the Settings subcategory canvas.
        if not self._paAnchored then
            local p = self:GetParent()
            if p then
                self:ClearAllPoints()
                self:SetAllPoints(p)
                self._paAnchored = true
            end
        end

        self:EnsureBuilt()
        if not handleSettingsFirstOpenPanelShow(self) then
            withPanelHydration(self, function()
                if self._layout then self._layout() end
                if self._refresh then self._refresh() end
            end)
        end
    end)

    panel:HookScript("OnHide", function(self)
        handleSettingsFirstOpenPanelHide(self)
    end)

    return panel
end
OptionsInternal.createCombatAlertsPanel = createCombatAlertsPanel
