-- Portal Authority — options/panel_timers.lua
-- Timers settings panel for Battle Rez and related timer module presentation.
--
-- Depends on: PortalAuthority.OptionsInternal shared helpers; PortalAuthority.Modules; Timers
-- Exposes:    PortalAuthority.OptionsInternal.createTimersPanel
-- DB keys:    PortalAuthorityDB.modules.brezTimer*, PortalAuthorityDB.modules.timers*
-- Events:     none
-- Search key: timers

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 createScrollableChild = OptionsInternal.createScrollableChild
local updateScrollBehavior = OptionsInternal.updateScrollBehavior
local withPanelHydration = OptionsInternal.withPanelHydration
local panelHasSizedViewport = OptionsInternal.panelHasSizedViewport
local ensureSettingsCanvasPanelAnchored = OptionsInternal.ensureSettingsCanvasPanelAnchored
local measureSettingsVisibleContentExtent = OptionsInternal.measureSettingsVisibleContentExtent
local isControlHydrating = OptionsInternal.isControlHydrating
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 createTimersPanel()
    local panel = CreateFrame("Frame")
    panel.name = "Timers"
    panel._paSearchSectionKey = "timers"
    panel._paSearchSectionName = panel.name

    panel.EnsureBuilt = function(self)
        if self._initialized then return end
        self._initialized = true
        resetSettingsFirstOpenPanelState(self)
        ensureDatabase()
        settingsSearchRuntime.beginDescriptorCapture(self)

        local function getModulesApi()
            return PortalAuthority and PortalAuthority.Modules or nil
        end
        local stack = nil
        local widgets = {}
        local pickerSpecs = {}
        self._timersPickerSpecs = pickerSpecs
        local function isModuleAvailable(moduleId)
            return true, nil
        end
        local function getStack()
            local api = getModulesApi()
            if api and api.registry then
                stack = api.registry.timers
            end
            return stack
        end
        local function ensureTimersStorage()
            PortalAuthorityDB = PortalAuthorityDB or {}
            PortalAuthorityDB.modules = PortalAuthorityDB.modules or {}
            PortalAuthorityDB.modules.timers = PortalAuthorityDB.modules.timers or {}
            return PortalAuthorityDB.modules.timers
        end
        local fallbackTimersDb = ensureTimersStorage()
        local function getLegacyTimersDb()
            if PortalAuthorityDB and PortalAuthorityDB.modules and type(PortalAuthorityDB.modules.modules2Stack) == "table" then
                return PortalAuthorityDB.modules.modules2Stack
            end
            return nil
        end
        local function getPersistedTimersDb()
            if PortalAuthorityDB and PortalAuthorityDB.modules and PortalAuthorityDB.modules.timers then
                return PortalAuthorityDB.modules.timers
            end
            return fallbackTimersDb
        end
        local function readTimerSetting(db, key, defaultValue)
            if db and db[key] ~= nil then
                return db[key]
            end
            local persisted = getPersistedTimersDb()
            if persisted and persisted[key] ~= nil then
                return persisted[key]
            end
            local legacy = getLegacyTimersDb()
            if legacy and legacy[key] ~= nil then
                return legacy[key]
            end
            return defaultValue
        end
        local function writeTimerSetting(db, key, value)
            if db then
                db[key] = value
            end
            local persisted = getPersistedTimersDb()
            if persisted then
                persisted[key] = value
            end
        end
        local function getTimersLockedState()
            local activeStack = getStack()
            if activeStack and activeStack.GetDB then
                local db = activeStack:GetDB()
                if db and db.locked ~= nil then
                    return db.locked and true or false
                end
            end

            local persisted = getPersistedTimersDb()
            if persisted and persisted.locked ~= nil then
                return persisted.locked and true or false
            end

            if widgets and widgets.unlock and widgets.unlock.GetChecked then
                return widgets.unlock:GetChecked() and true or false
            end

            return true
        end
        local function getTimersDBSnapshot()
            local activeStack = getStack()
            local db = nil
            if activeStack and activeStack.GetDB then
                db = activeStack:GetDB()
            else
                db = fallbackTimersDb
            end
            return activeStack, db
        end
        local function getTimersDisplayPosition(activeStack, db)
            local fallbackX = math.floor(tonumber(readTimerSetting(db, "x", 0)) or 0)
            local fallbackY = math.floor(tonumber(readTimerSetting(db, "y", 0)) or 0)
            if not activeStack or not activeStack.frame or not activeStack.frame.GetCenter or not UIParent or not UIParent.GetCenter then
                return fallbackX, fallbackY
            end
            local cx, cy = activeStack.frame:GetCenter()
            local ux, uy = UIParent:GetCenter()
            if not cx or not cy or not ux or not uy then
                return fallbackX, fallbackY
            end
            return math.floor((cx - ux) + 0.5), math.floor((cy - uy) + 0.5)
        end

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

        local header = createStyledFontString(scrollChild, "ARTWORK", "GameFontNormalLarge", 14)
        header:SetText("|cffffd100Timers|r")
        header:SetJustifyH("LEFT")
        local headerDescriptor = createSettingsSectionDescriptor(scrollChild, "Configure your M+ timers.")

        local TIMERS_LAYOUT = {
            MIN_CONTENT_W = 560,
            TOP_PAD = 20,
            BOTTOM_PAD = 20,
            HEADER_LEFT = 16,
            CONTENT_LEFT = 20,
            CONTENT_RIGHT = 20,
            HIDDEN_X = -5000,
            HIDDEN_Y = -5000,
            TOGGLE_COL_X = 220,
            APPEAR_CONTROL_SHIFT_X = -12,
            LABEL_TO_CONTROL_GAP = 12,
            LOCK_TOGGLE_TO_RESET_GAP = 12,
            LOCK_TOGGLE_PREF_W = 140,
            LOCK_TOGGLE_MIN_W = 96,
            MODULE_TOGGLE_PREF_W = 150,
            MIN_PICKER_W = 140,
            MIN_SEGMENT_W = 140,
            MAIN_CONTROL_TARGET_W = 202,
            MAIN_CONTROL_MIN_W = 196,
            PICKER_DEFAULT_W = 180,
            ASPECT_PICKER_W = 186,
            PICKER_CONTROL_H = 22,
            LOCK_ROW_SEED_H = 22,
            SLIDER_CONTROL_W = 260,
            SLIDER_VALUE_BUTTON_W = 44,
            SLIDER_VALUE_BUTTON_H = 16,
            SLIDER_VALUE_GAP = 8,
            SLIDER_TRACK_H = 2,
            SLIDER_THUMB_HIT_SIZE = 16,
            SLIDER_THUMB_SIZE = 8,
            SLIDER_GLOW_SIZE = 14,
            ICON_EDGE_SEGMENT_MIN_W = 82,
            RESET_BUTTON_SEED_W = 104,
            RESET_BUTTON_PREF_W = 102,
            RESET_BUTTON_MIN_W = 102,
            BATCH_BUTTON_COMPRESS_MIN_W = 80,
            ACTION_BUTTON_TEXT_PAD_X = 22,
            SEGMENT_BUTTON_TEXT_PAD_X = 20,
            MEASURE_SINGLE_LINE_W = 4096,
            HEADER_FLOOR = 16,
            SECTION_HEADER_FLOOR = 12,
            LABEL_FLOOR = 12,
            HELPER_FLOOR = 10,
            MINI_LABEL_FLOOR = 10,
            CONTROL_FLOOR = 22,
            LOCK_ROW_FLOOR = 22,
            ROW_FLOOR = 36,
            OFFSET_ROW_FLOOR = 56,
            HEADER_BOTTOM_GAP = 14,
            LOCK_HELPER_GAP_RATIO = 0.625,
            LOCK_DIVIDER_GAP_RATIO = 1.25,
            LOCK_DIVIDER_OFFSET_RATIO = 0.5,
            SECTION_HEADER_BOTTOM_GAP = 4,
            MODULE_CARD_TO_ROWS_GAP = 12,
            MODULE_CHILD_INDENT = 16,
            MODULE_CHILD_ROW_GAP = 4,
            MODULE_ROW_TO_HELPER_GAP = 4,
            MODULE_ROW_TO_EXPLAIN_GAP = 4,
            MODULE_HELPER_TO_EXPLAIN_GAP = 4,
            MODULE_EXPLAIN_LINE_GAP = 5,
            MODULE_EXPLAIN_BLOCK_GAP = 12,
            SECTION_TO_NEXT_DIVIDER_GAP = 24,
            CARD_PADDING = 8,
            CARD_LABEL_TO_BUTTONS_GAP = 8,
            CARD_BUTTONS_TO_STATUS_GAP = 4,
            BATCH_BUTTON_GAP = 8,
            BATCH_BUTTON_H = 22,
            BATCH_BUTTON_PREF_W = 156,
            BATCH_BUTTON_MIN_W = 120,
            BATCH_SCOPE_GAP = 6,
            FONT_SIZE_W = 52,
            FONT_SIZE_GAP = 8,
            FONT_STACK_GAP = 4,
            AXIS_LABEL_MIN_W = 10,
            AXIS_TO_CONTROL_GAP = 6,
            AXIS_LINE_GAP = 8,
        }

        widgets.unlock = CreateFrame("CheckButton", nil, scrollChild, "UICheckButtonTemplate")
        widgets.unlock.text:SetText("Lock Position")
        applyGlobalFontToObject(widgets.unlock.text, 12)
        local unlockLabelNormalColor = { widgets.unlock.text:GetTextColor() }
        local function updateUnlockLabelColor(isLocked)
            if isLocked then
                widgets.unlock.text:SetTextColor(unlockLabelNormalColor[1], unlockLabelNormalColor[2], unlockLabelNormalColor[3], unlockLabelNormalColor[4])
            else
                widgets.unlock.text:SetTextColor(1, 0.2, 0.2)
            end
        end
        widgets.unlock:SetScript("OnClick", function(btn)
            local isLocked = not not btn:GetChecked()
            local desiredUnlocked = not isLocked
            local activeStack = getStack()
            local actualUnlocked = not getTimersLockedState()
            if InCombatLockdown and InCombatLockdown() and actualUnlocked ~= nil and desiredUnlocked ~= not not actualUnlocked then
                local actualLocked = not not (not actualUnlocked)
                btn:SetChecked(actualLocked)
                updateUnlockLabelColor(actualLocked)
                return
            end

            -- Force immediate visual response for lock/unlock so frame + drag handle
            -- appear without waiting for external events/tickers.
            activeStack = getStack()
            if activeStack then
                if desiredUnlocked and activeStack.Initialize and not (activeStack.frame or activeStack.mainFrame) then
                    activeStack:Initialize()
                end
                if activeStack.SetUnlocked then
                    activeStack:SetUnlocked(desiredUnlocked)
                end
                if activeStack.ApplyPosition then
                    activeStack:ApplyPosition()
                end
                if activeStack.EvaluateVisibility and (activeStack.frame or activeStack.mainFrame) then
                    activeStack:EvaluateVisibility("options-lock-toggle")
                end
                if desiredUnlocked and activeStack.Tick then
                    activeStack:Tick()
                end
            end

            local authoritativeLocked = getTimersLockedState()
            btn:SetChecked(authoritativeLocked)
            updateUnlockLabelColor(authoritativeLocked)
            if self._refresh then self._refresh() end
        end)

        local applyStackSettings

        local function mkCheck(parent, text, key)
            local c = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
            c.text:SetText(text)
            applyGlobalFontToObject(c.text, 12)
            c.text:SetTextColor(1, 0.82, 0, 1)
            c:SetScript("OnClick", function(btn)
                local activeStack = getStack()
                if not activeStack then return end
                activeStack:GetDB()[key] = not not btn:GetChecked()
                activeStack:ApplySettings()
                local api = getModulesApi()
                if api and api.timersTestMode then
                    if activeStack.EvaluateVisibility then
                        activeStack:EvaluateVisibility()
                    end
                    if activeStack.Tick then
                        activeStack:Tick()
                    end
                end
                if self._refresh then self._refresh() end
            end)
            return c
        end

        local function mkPickerButton(parent, opts)
            opts = type(opts) == "table" and opts or {}
            local optionsProvider = type(opts.optionsProvider) == "function" and opts.optionsProvider or function() return {} end
            local getter = type(opts.getter) == "function" and opts.getter or function() return "" end
            local setter = type(opts.setter) == "function" and opts.setter or function() end
            local onApply = type(opts.onApply) == "function" and opts.onApply or nil
            local fallbackLabel = type(opts.fallbackLabel) == "function" and opts.fallbackLabel or nil

            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(TIMERS_LAYOUT.PICKER_CONTROL_H)
            button:SetWidth(math.max(0, tonumber(opts.width) or TIMERS_LAYOUT.PICKER_DEFAULT_W))

            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)

            local spec = { kind = "picker_button", control = button, text = text, caret = caret }
            local picker = CreatePicker(UIParent, {}, function(value)
                local selectedValue = tostring(value or "")
                setter(selectedValue)
                if onApply then
                    onApply()
                end
                if spec.Refresh then
                    spec.Refresh()
                end
            end, {
                title = tostring(opts.title or "Select Option"),
                searchHint = tostring(opts.searchHint or "Search"),
                width = tonumber(opts.pickerWidth) or 420,
                height = tonumber(opts.pickerHeight) or 360,
                showSecondary = opts.showSecondary and true or false,
                useOptionFontPreview = opts.useOptionFontPreview and true or false,
                previewFontSize = tonumber(opts.previewFontSize) or 12,
                rowHeight = tonumber(opts.rowHeight) or 26,
                showClearButton = false,
                noSearch = opts.noSearch and true or false,
                hideTitle = opts.hideTitle and true or false,
                hideScrollBar = opts.hideScrollBar and true or false,
            })
            spec.picker = picker

            local function applyVisual(hovered)
                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

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

            local function buildPickerOptions()
                local built = {}
                for _, option in ipairs(optionsProvider() or {}) do
                    local value = tostring(option.value or "")
                    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,
                        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 = tostring(rawValue or "")
                local selectedLabel = nil
                for _, option in ipairs(options or {}) do
                    if tostring(option.value or "") == selected then
                        selected = tostring(option.value or "")
                        selectedLabel = tostring(option.buttonLabel or option.label or selected)
                        break
                    end
                end
                if not selectedLabel then
                    if fallbackLabel then
                        selectedLabel = tostring(fallbackLabel(selected) or "")
                    end
                    if selectedLabel == "" then
                        local first = options and options[1] or nil
                        if first then
                            selected = tostring(first.value or "")
                            selectedLabel = tostring(first.buttonLabel or first.label or selected)
                            setter(selected)
                        else
                            selectedLabel = "None"
                        end
                    end
                end
                return selected, selectedLabel
            end

            spec.SetWidth = function(_, newWidth)
                button:SetWidth(math.max(0, math.floor(tonumber(newWidth) or opts.width or TIMERS_LAYOUT.PICKER_DEFAULT_W)))
            end
            spec.ClosePicker = function()
                if picker and picker.IsShown and picker:IsShown() then
                    picker:Close()
                end
            end

            spec.Refresh = function()
                local options = buildPickerOptions()
                picker:SetOptions(options)
                local selectedValue, selectedLabel = resolveSelection(getter(), options)
                picker:SetSelectedValue(selectedValue)
                text:SetText(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)

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

        local function mkFontDropdown(parent, getter, setter)
            local function fontFallbackLabel(path)
                local value = trim(tostring(path or ""))
                if value == "" then
                    return "Global UI Font"
                end
                value = value:gsub("/", "\\")
                return value:match("([^\\]+)$") or value
            end
            return mkPickerButton(parent, {
                width = TIMERS_LAYOUT.PICKER_DEFAULT_W,
                title = "Select Font Face",
                searchHint = "Search fonts by name",
                pickerWidth = 420,
                pickerHeight = 380,
                showSecondary = false,
                useOptionFontPreview = true,
                previewFontSize = 13,
                rowHeight = 26,
                optionsProvider = buildSharedFontPickerChoices,
                getter = function()
                    return tostring(getter() or "")
                end,
                setter = function(v)
                    setter(v or "")
                end,
                onApply = function()
                    applyStackSettings()
                end,
                fallbackLabel = fontFallbackLabel,
            })
        end

        local function mkNumeric(parent, onApply)
            local e = CreateFrame("EditBox", nil, parent)
            e:SetAutoFocus(false)
            e:SetMultiLine(false)
            e:SetTextInsets(8, 8, 0, 0)
            e:SetTextColor(1,1,1,1)
            e:SetHighlightColor(1,1,1,0.25)
            applyGlobalFontToObject(e, 11)
            e:SetJustifyH("CENTER")

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

                e._paTimersBorder = CreateFrame("Frame", nil, e, "BackdropTemplate")
                e._paTimersBorder:SetPoint("TOPLEFT", e, "TOPLEFT", -1, 1)
                e._paTimersBorder:SetPoint("BOTTOMRIGHT", e, "BOTTOMRIGHT", 1, -1)
                e._paTimersBorder:SetBackdrop({
                    edgeFile = "Interface\\Buttons\\WHITE8x8",
                    edgeSize = 1,
                })
                e._paTimersBorder:SetBackdropBorderColor(0.24, 0.27, 0.32, 1.0)
            end

            local function hideTemplateChrome()
                local left = e.Left
                local mid = e.Middle
                local right = e.Right
                if left then left:SetAlpha(0) end
                if mid then mid:SetAlpha(0) end
                if right then right:SetAlpha(0) end
            end

            local function applyEditVisual()
                hideTemplateChrome()
                if e:HasFocus() then
                    e._paTimersBg:SetVertexColor(0.12, 0.13, 0.17, 0.95)
                    e._paTimersBorder:SetBackdropBorderColor(0.30, 0.33, 0.38, 1.0)
                else
                    e._paTimersBg:SetVertexColor(0.10, 0.11, 0.14, 0.95)
                    e._paTimersBorder:SetBackdropBorderColor(0.24, 0.27, 0.32, 1.0)
                end
            end

            e:SetScript("OnShow", function()
                applyEditVisual()
            end)
            e:SetScript("OnEditFocusGained", function()
                applyEditVisual()
            end)
            e:SetScript("OnEditFocusLost", function(edit)
                applyEditVisual()
                if onApply then
                    onApply(edit)
                end
            end)
            e:SetScript("OnEnterPressed", function(edit)
                if onApply then
                    onApply(edit)
                end
                edit:ClearFocus()
            end)
            e:SetScript("OnEscapePressed", function(edit)
                edit:ClearFocus()
            end)
            return e
        end

        local function setReadableNumericText(edit)
            if not edit then return end
            edit:SetTextColor(1, 1, 1, 1)
            edit:SetCursorPosition(0)
        end

        applyStackSettings = function()
            local activeStack = getStack()
            if not activeStack then return end
            activeStack:ApplySettings()
            local api = getModulesApi()
            if api and api.timersTestMode then
                if activeStack.EvaluateVisibility then
                    activeStack:EvaluateVisibility()
                end
                if activeStack.Tick then
                    activeStack:Tick()
                end
            end
        end

        local fontLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        fontLabel:SetText("Font / Size")
        fontLabel:SetJustifyH("LEFT")
        local fontDropdown = mkFontDropdown(scrollChild, function()
            local activeStack = getStack()
            return activeStack and activeStack:GetDB().fontPath or ""
        end, function(v)
            local activeStack = getStack()
            if activeStack then activeStack:GetDB().fontPath = v end
        end)

        local sizeLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        sizeLabel:SetText("")
        local sizeEdit = nil
        local sizeStepper = nil
        if type(createCanonicalDockFontSizeStepperControl) == "function" then
            sizeStepper = createCanonicalDockFontSizeStepperControl(
                scrollChild,
                "",
                8,
                48,
                1,
                "fontSize",
                52,
                function()
                    applyStackSettings()
                end,
                nil,
                {
                    micro = true,
                    shiftMultiplier = 5,
                    getValue = function()
                        local activeStack = getStack()
                        local db = activeStack and activeStack:GetDB() or nil
                        local raw = tonumber(db and db.fontSize) or 12
                        return math.floor(math.min(48, math.max(8, raw)))
                    end,
                    setValue = function(v)
                        local activeStack = getStack()
                        if not activeStack then
                            return
                        end
                        local db = activeStack:GetDB()
                        local raw = tonumber(v)
                        if raw == nil then
                            raw = tonumber(db.fontSize) or 12
                        end
                        db.fontSize = math.floor(math.min(48, math.max(8, raw)))
                    end,
                }
            )
            if sizeStepper and sizeStepper.label then
                sizeStepper.label:SetText("")
                sizeStepper.label:Hide()
            end
        else
            sizeEdit = mkNumeric(scrollChild, function(edit)
                local activeStack = getStack()
                if not activeStack then return end
                local db = activeStack:GetDB()
                local text = trim(edit:GetText() or "")
                local v = tonumber(text)
                if v ~= nil then
                    db.fontSize = math.floor(math.min(48, math.max(8, v)))
                end
                applyStackSettings()
                edit:SetText(tostring(db.fontSize))
                edit:SetTextColor(1, 1, 1, 1)
            end)
            setReadableNumericText(sizeEdit)
            sizeEdit:SetNumeric(true)
            sizeEdit:HookScript("OnShow", function(edit) edit:SetTextColor(1, 1, 1, 1) end)
            sizeEdit:HookScript("OnEditFocusLost", function(edit) edit:SetTextColor(1, 1, 1, 1) end)
        end

        local iconSizeLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        iconSizeLabel:SetText("Icon Size")
        iconSizeLabel:SetJustifyH("LEFT")
        local iconSizeEdit = mkNumeric(scrollChild, function(edit)
            local activeStack = getStack()
            if not activeStack then return end
            local db = activeStack:GetDB()
            local v = tonumber(trim(edit:GetText() or ""))
            db.iconSize = math.floor(math.min(64, math.max(12, v or db.iconSize or 18)))
            applyStackSettings()
            edit:SetText(tostring(db.iconSize))
            edit:SetTextColor(1, 1, 1, 1)
        end)
        setReadableNumericText(iconSizeEdit)
        iconSizeEdit:HookScript("OnShow", function(edit) edit:SetTextColor(1, 1, 1, 1) end)
        iconSizeEdit:HookScript("OnEditFocusLost", function(edit) edit:SetTextColor(1, 1, 1, 1) end)

        local iconOffsetXLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        iconOffsetXLabel:SetText("Icon Offset X")
        iconOffsetXLabel:SetJustifyH("LEFT")
        local iconOffsetXEdit = mkNumeric(scrollChild, function(edit)
            local activeStack = getStack()
            if not activeStack then return end
            local db = activeStack:GetDB()
            local v = tonumber(trim(edit:GetText() or ""))
            if v == nil then
                edit:SetText(tostring(math.floor(tonumber(db.iconOffsetX) or 0)))
                edit:SetTextColor(1, 1, 1, 1)
                return
            end
            local nextValue = math.floor(math.min(50, math.max(-50, v)))
            local currentValue = math.floor(tonumber(db.iconOffsetX) or 0)
            if nextValue ~= currentValue then
                db.iconOffsetX = nextValue
                applyStackSettings()
            end
            local displayValue = math.floor(tonumber(db.iconOffsetX) or currentValue)
            edit:SetText(tostring(displayValue))
            edit:SetTextColor(1, 1, 1, 1)
        end)
        setReadableNumericText(iconOffsetXEdit)
        iconOffsetXEdit:HookScript("OnShow", function(edit) edit:SetTextColor(1, 1, 1, 1) end)
        iconOffsetXEdit:HookScript("OnEditFocusLost", function(edit) edit:SetTextColor(1, 1, 1, 1) end)

        local iconOffsetYLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        iconOffsetYLabel:SetText("Icon Offset Y")
        iconOffsetYLabel:SetJustifyH("LEFT")
        local iconOffsetYEdit = mkNumeric(scrollChild, function(edit)
            local activeStack = getStack()
            if not activeStack then return end
            local db = activeStack:GetDB()
            local v = tonumber(trim(edit:GetText() or ""))
            if v == nil then
                edit:SetText(tostring(math.floor(tonumber(db.iconOffsetY) or 0)))
                edit:SetTextColor(1, 1, 1, 1)
                return
            end
            local nextValue = math.floor(math.min(50, math.max(-50, v)))
            local currentValue = math.floor(tonumber(db.iconOffsetY) or 0)
            if nextValue ~= currentValue then
                db.iconOffsetY = nextValue
                applyStackSettings()
            end
            local displayValue = math.floor(tonumber(db.iconOffsetY) or currentValue)
            edit:SetText(tostring(displayValue))
            edit:SetTextColor(1, 1, 1, 1)
        end)
        setReadableNumericText(iconOffsetYEdit)
        iconOffsetYEdit:HookScript("OnShow", function(edit) edit:SetTextColor(1, 1, 1, 1) end)
        iconOffsetYEdit:HookScript("OnEditFocusLost", function(edit) edit:SetTextColor(1, 1, 1, 1) end)

        local iconOffsetPairLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        iconOffsetPairLabel:SetText("Icon Offset")
        iconOffsetPairLabel:SetJustifyH("LEFT")
        local iconOffsetAxisXMini = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
        iconOffsetAxisXMini:SetText("X")
        iconOffsetAxisXMini:SetTextColor(0.76, 0.76, 0.78, 0.92)
        iconOffsetAxisXMini:SetJustifyH("LEFT")
        local iconOffsetAxisYMini = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
        iconOffsetAxisYMini:SetText("Y")
        iconOffsetAxisYMini:SetTextColor(0.76, 0.76, 0.78, 0.92)
        iconOffsetAxisYMini:SetJustifyH("LEFT")
        local fontRowContainer = CreateFrame("Frame", nil, scrollChild)
        local iconOffsetRowContainer = CreateFrame("Frame", nil, scrollChild)

        local iconEdgeLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        iconEdgeLabel:SetText("Icon Edge")
        iconEdgeLabel:SetJustifyH("LEFT")
        local iconEdgeControl = createPremiumSegmentedChoice(
            scrollChild,
            "",
            nil,
            {
                { label = "Sharp", value = "Sharp (Zoomed)" },
                { label = "Square", value = "Square (Full)" },
            },
            function()
                applyStackSettings()
            end,
            {
                semantic = "selection",
                controlHeight = 22,
                textSize = 11,
                segmentGap = 0,
                segmentMinWidth = TIMERS_LAYOUT.ICON_EDGE_SEGMENT_MIN_W,
                textEllipsis = true,
                getValue = function()
                    local activeStack = getStack()
                    local db = activeStack and activeStack:GetDB() or nil
                    local value = trim(tostring(db and db.iconEdgeStyle or "Sharp (Zoomed)"))
                    if value ~= "Sharp (Zoomed)" and value ~= "Square (Full)" then
                        return "Sharp (Zoomed)"
                    end
                    return value
                end,
                setValue = function(v)
                    local activeStack = getStack()
                    if not activeStack then
                        return
                    end
                    local db = activeStack:GetDB()
                    local value = trim(tostring(v or "Sharp (Zoomed)"))
                    if value ~= "Square (Full)" then
                        value = "Sharp (Zoomed)"
                    end
                    db.iconEdgeStyle = value
                end,
            }
        )
        if iconEdgeControl and iconEdgeControl.label then
            iconEdgeControl.label:SetText("")
            iconEdgeControl.label:Hide()
        end

        local aspectChoices = {
            { label = "1:1 (Default)", value = "1:1" },
            { label = "16:9 (Wide)", value = "16:9" },
            { label = "4:3", value = "4:3" },
            { label = "3:4", value = "3:4" },
            { label = "9:16 (Tall)", value = "9:16" },
            { label = "2:1", value = "2:1" },
            { label = "1:2", value = "1:2" },
        }
        local aspectLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        aspectLabel:SetText("Aspect Ratio")
        aspectLabel:SetJustifyH("LEFT")
        local aspectDropdown = mkPickerButton(scrollChild, {
            width = TIMERS_LAYOUT.ASPECT_PICKER_W,
            title = "Select Icon Aspect Ratio",
            searchHint = "Search aspect ratios",
            pickerWidth = 360,
            pickerHeight = 300,
            noSearch = true,
            hideTitle = true,
            hideScrollBar = true,
            optionsProvider = function()
                return aspectChoices
            end,
            getter = function()
                local activeStack = getStack()
                return activeStack and activeStack:GetDB().iconAspect or "1:1"
            end,
            setter = function(v)
                local activeStack = getStack()
                if activeStack then
                    activeStack:GetDB().iconAspect = trim(v or "1:1")
                end
            end,
            onApply = function()
                applyStackSettings()
            end,
        })

        local brezEnable = mkCheck(scrollChild, "Battle Rez Timer", "brezEnabled")
        local lustEnable = mkCheck(scrollChild, "Bloodlust Timer", "lustEnabled")
        local healerEnable = mkCheck(scrollChild, "Healer Status", "healerEnabled")
        local cooldownSweepEnable = mkCheck(scrollChild, "Cooldown Sweep", "cooldownSweepEnabled")
        local enableLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        enableLabel:SetText("Enable")
        local divider = scrollChild:CreateTexture(nil, "ARTWORK")
        divider:SetColorTexture(1, 1, 1, 0.15)
        local dividerFont = scrollChild:CreateTexture(nil, "ARTWORK")
        dividerFont:SetColorTexture(1, 1, 1, 0.15)
        local TIMER_HELPER_R, TIMER_HELPER_G, TIMER_HELPER_B, TIMER_HELPER_A = 0.58, 0.58, 0.58, 0.92
        local function applyHelperTone(fs)
            if fs and fs.SetTextColor then
                fs:SetTextColor(TIMER_HELPER_R, TIMER_HELPER_G, TIMER_HELPER_B, TIMER_HELPER_A)
            end
        end
        local function setTimerSingleLineEllipsis(fs, fullText, maxWidth)
            if not fs then
                return
            end
            local text = tostring(fullText or "")
            fs:SetWordWrap(false)
            if fs.SetMaxLines then
                fs:SetMaxLines(1)
            end
            if maxWidth and maxWidth > 0 then
                fs:SetWidth(maxWidth)
            end
            fs:SetText(text)
            if not maxWidth or maxWidth <= 0 or not fs.GetStringWidth then
                return
            end
            if fs:GetStringWidth() <= maxWidth then
                return
            end
            local suffix = "..."
            local low, high = 0, #text
            local best = suffix
            while low <= high do
                local mid = math.floor((low + high) / 2)
                local candidate = text:sub(1, mid) .. suffix
                fs:SetText(candidate)
                if fs:GetStringWidth() <= maxWidth then
                    best = candidate
                    low = mid + 1
                else
                    high = mid - 1
                end
            end
            fs:SetText(best)
        end


        local function parkTimersControl(control)
            if not control then
                return
            end
            if control.Hide then
                control:Hide()
            end
            control:ClearAllPoints()
            control:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", TIMERS_LAYOUT.HIDDEN_X, TIMERS_LAYOUT.HIDDEN_Y)
        end

        local function showTimersControl(control)
            if control and control.Show then
                control:Show()
            end
        end

        local function rememberTimersText(fs, text)
            if not fs then
                return
            end
            if text == nil then
                text = (fs.GetText and fs:GetText()) or ""
            end
            fs._paTimersRawText = tostring(text or "")
        end

        local function setTimersText(fs, text)
            if not fs then
                return
            end
            local value = tostring(text or "")
            fs._paTimersRawText = value
            fs:SetText(value)
        end

        local function getTimersText(fs)
            if not fs then
                return ""
            end
            local raw = fs._paTimersRawText
            if raw == nil then
                raw = (fs.GetText and fs:GetText()) or ""
                fs._paTimersRawText = tostring(raw or "")
            end
            return tostring(raw or "")
        end

        local function isTimersTextActive(fs)
            return trim(getTimersText(fs)) ~= ""
        end

        local function applyTimersSingleLine(fs, width)
            if not fs then
                return
            end
            local clampedWidth = math.max(1, math.floor(tonumber(width) or 0))
            setTimerSingleLineEllipsis(fs, getTimersText(fs), clampedWidth)
        end

        local function measureTimersSingleLineHeight(fs, width, floorValue, isActive)
            if not fs or isActive == false then
                return 0
            end
            applyTimersSingleLine(fs, width)
            return math.max(
                math.floor(tonumber(floorValue) or 0),
                math.ceil(tonumber((fs.GetStringHeight and fs:GetStringHeight()) or 0) or 0)
            )
        end

        local function measureTimersSingleLineWidth(fs, floorValue, isActive)
            if not fs or isActive == false then
                return 0
            end
            local raw = getTimersText(fs)
            fs:SetWordWrap(false)
            if fs.SetMaxLines then
                fs:SetMaxLines(1)
            end
            fs:SetWidth(TIMERS_LAYOUT.MEASURE_SINGLE_LINE_W)
            fs:SetText(raw)
            return math.max(
                math.floor(tonumber(floorValue) or 0),
                math.ceil(tonumber((fs.GetStringWidth and fs:GetStringWidth()) or 0) or 0)
            )
        end

        local function computeTimersLockRhythm(labelHeight)
            local T = TIMERS_LAYOUT
            local measuredLabelH = math.max(1, tonumber(labelHeight) or 0)
            local helperGap = math.max(1, math.floor((measuredLabelH * T.LOCK_HELPER_GAP_RATIO) + 0.5))
            local dividerGap = math.max(1, math.floor((measuredLabelH * T.LOCK_DIVIDER_GAP_RATIO) + 0.5))
            local dividerOffset = math.max(1, math.floor((dividerGap * T.LOCK_DIVIDER_OFFSET_RATIO) + 0.5))
            return helperGap, dividerGap, dividerOffset
        end
        local function measureTimersWrappedTextHeight(fs, width, floorValue, isActive)
            if not fs or isActive == false then
                return 0
            end
            local clampedWidth = math.max(1, math.floor(tonumber(width) or 0))
            fs:SetWidth(clampedWidth)
            fs:SetText(getTimersText(fs))
            return math.max(
                math.floor(tonumber(floorValue) or 0),
                math.ceil(tonumber((fs.GetStringHeight and fs:GetStringHeight()) or 0) or 0)
            )
        end

        local function measureTimersControlHeight(control, floorValue, isActive)
            if not control or isActive == false then
                return 0
            end
            return math.max(
                math.floor(tonumber(floorValue) or 0),
                math.ceil(tonumber((control.GetHeight and control:GetHeight()) or 0) or 0)
            )
        end

        local function measureTimersButtonLabelWidth(button)
            if not button or not button.Text or not button.Text.GetStringWidth then
                return 0
            end
            return math.ceil(tonumber(button.Text:GetStringWidth() or 0) or 0)
        end

        local function computeTimersSegmentWidth(spec, preferredWidth, availableWidth, perButtonPad, minWidth)
            local widestText = 0
            if spec and spec.offBtn then
                widestText = math.max(widestText, measureTimersButtonLabelWidth(spec.offBtn))
            end
            if spec and spec.onBtn then
                widestText = math.max(widestText, measureTimersButtonLabelWidth(spec.onBtn))
            end
            widestText = math.max(widestText, 36)
            local pad = math.max(0, tonumber(perButtonPad) or 0)
            local desiredWidth = ((widestText + (pad * 2)) * 2) + 4
            local preferred = math.max(math.floor(tonumber(minWidth) or 0), math.floor(tonumber(preferredWidth) or desiredWidth))
            local maxWidth = math.max(math.floor(tonumber(minWidth) or 0), math.floor(tonumber(availableWidth) or 0))
            return math.max(math.floor(tonumber(minWidth) or 0), math.min(maxWidth, math.max(desiredWidth, preferred)))
        end

        local function computeTimersActionButtonWidth(button, preferredWidth, availableWidth, minWidth)
            local desiredWidth = measureTimersButtonLabelWidth(button) + (TIMERS_LAYOUT.ACTION_BUTTON_TEXT_PAD_X * 2)
            local preferred = math.max(math.floor(tonumber(minWidth) or 0), math.floor(tonumber(preferredWidth) or desiredWidth))
            local maxWidth = math.max(math.floor(tonumber(minWidth) or 0), math.floor(tonumber(availableWidth) or 0))
            return math.max(math.floor(tonumber(minWidth) or 0), math.min(maxWidth, math.max(desiredWidth, preferred)))
        end

        local function computeTimersGeometry(contentW)
            local T = TIMERS_LAYOUT
            local G = {}
            G.contentW = math.max(math.floor(tonumber(contentW) or 0), T.MIN_CONTENT_W)
            G.contentLeft = T.CONTENT_LEFT
            G.contentRight = G.contentW - T.CONTENT_RIGHT
            G.contentSpan = math.max(1, G.contentRight - G.contentLeft)
            G.labelColX = G.contentLeft
            G.toggleColX = T.TOGGLE_COL_X
            G.helperColX = G.labelColX
            G.settingsLabelX = G.labelColX
            G.settingsControlX = G.toggleColX
            G.appearanceControlX = G.settingsControlX + T.APPEAR_CONTROL_SHIFT_X
            return G
        end

        local function computeTimersLockRowGeometry(G, lockRowRef, resetButton)
            local T = TIMERS_LAYOUT
            local L = {}
            local labelWidth = measureTimersSingleLineWidth(lockRowRef and lockRowRef.label or nil, 0, lockRowRef and lockRowRef.label ~= nil)
            L.labelX = G.labelColX
            L.labelW = math.max(1, labelWidth)
            L.controlX = L.labelX + L.labelW + T.LABEL_TO_CONTROL_GAP
            L.resetH = T.CONTROL_FLOOR
            L.resetW = T.RESET_BUTTON_PREF_W
            local toggleAvailW = math.max(T.LOCK_TOGGLE_MIN_W, G.contentRight - L.controlX - T.LOCK_TOGGLE_TO_RESET_GAP - L.resetW)
            L.controlW = computeTimersSegmentWidth(lockRowRef, T.LOCK_TOGGLE_PREF_W, toggleAvailW, T.SEGMENT_BUTTON_TEXT_PAD_X, T.LOCK_TOGGLE_MIN_W)
            L.resetX = L.controlX + L.controlW + T.LOCK_TOGGLE_TO_RESET_GAP
            L.rowW = math.max(1, G.contentRight - G.labelColX)
            return L
        end

        local function computeTimersModuleGeometry(G, sampleRow)
            local T = TIMERS_LAYOUT
            local M = {}
            M.labelX = G.labelColX
            M.labelW = math.max(1, G.toggleColX - G.labelColX - T.LABEL_TO_CONTROL_GAP)
            M.controlX = G.toggleColX
            M.helperX = G.helperColX
            M.helperW = math.max(1, G.contentRight - G.helperColX)
            local toggleAvailW = math.max(0, G.contentRight - M.controlX)
            M.controlW = computeTimersSegmentWidth(sampleRow, T.MODULE_TOGGLE_PREF_W, toggleAvailW, T.SEGMENT_BUTTON_TEXT_PAD_X, T.MIN_SEGMENT_W)
            return M
        end

        local function computeTimersBatchCardGeometry(G, batchScopeRef, batchEnableRef, batchDisableRef)
            local T = TIMERS_LAYOUT
            local B = {}
            B.cardX = G.labelColX
            B.cardW = G.contentSpan
            B.innerW = math.max(1, B.cardW - (T.CARD_PADDING * 2))
            local scopeText = getTimersText(batchScopeRef)
            if trim(scopeText) ~= "" then
                batchScopeRef:SetText(scopeText)
                B.scopeW = math.min(B.innerW, math.ceil(tonumber((batchScopeRef.GetStringWidth and batchScopeRef:GetStringWidth()) or 0) or 0))
            else
                B.scopeW = 0
            end
            B.scopeX = T.CARD_PADDING + math.max(0, B.innerW - B.scopeW)
            B.titleW = math.max(1, B.innerW - B.scopeW - ((B.scopeW > 0) and T.BATCH_SCOPE_GAP or 0))
            local perButtonAvailW = math.max(T.BATCH_BUTTON_MIN_W, math.floor((B.innerW - T.BATCH_BUTTON_GAP) / 2))
            local desiredButtonW = math.max(
                computeTimersActionButtonWidth(batchEnableRef, T.BATCH_BUTTON_PREF_W, perButtonAvailW, T.BATCH_BUTTON_MIN_W),
                computeTimersActionButtonWidth(batchDisableRef, T.BATCH_BUTTON_PREF_W, perButtonAvailW, T.BATCH_BUTTON_MIN_W)
            )
            B.buttonW = math.max(T.BATCH_BUTTON_MIN_W, math.min(perButtonAvailW, desiredButtonW))
            if ((B.buttonW * 2) + T.BATCH_BUTTON_GAP) > B.innerW then
                B.buttonW = math.max(T.BATCH_BUTTON_COMPRESS_MIN_W, math.floor((B.innerW - T.BATCH_BUTTON_GAP) / 2))
            end
            B.buttonsLeft = T.CARD_PADDING + math.max(0, B.innerW - ((B.buttonW * 2) + T.BATCH_BUTTON_GAP))
            B.enableX = B.buttonsLeft
            B.disableX = B.enableX + B.buttonW + T.BATCH_BUTTON_GAP
            B.statusW = B.innerW
            return B
        end

        local function computeTimersAppearanceGeometry(G, axisXMini, axisYMini)
            local T = TIMERS_LAYOUT
            local A = {}
            A.labelX = G.settingsLabelX
            A.labelW = math.max(1, G.appearanceControlX - G.settingsLabelX - T.LABEL_TO_CONTROL_GAP)
            A.controlX = G.appearanceControlX
            A.controlAvailW = math.max(0, G.contentRight - A.controlX)
            if A.controlAvailW >= T.MAIN_CONTROL_MIN_W then
                A.mainControlW = math.min(T.MAIN_CONTROL_TARGET_W, A.controlAvailW)
            else
                A.mainControlW = A.controlAvailW
            end
            A.valueColX = A.controlX + A.mainControlW
            A.rowContainerW = math.max(1, G.contentRight - G.settingsLabelX)
            A.fontPickerW = math.max(0, A.mainControlW - T.FONT_SIZE_W - T.FONT_SIZE_GAP)
            A.fontDropSizeBelow = (A.fontPickerW < T.MIN_PICKER_W)
            if A.fontDropSizeBelow then
                A.fontPickerW = A.mainControlW
            end
            A.fontSizeW = T.FONT_SIZE_W
            A.fontSizeX = A.controlX + (A.fontDropSizeBelow and 0 or (A.fontPickerW + T.FONT_SIZE_GAP))
            local axisTextW = math.max(
                math.ceil(tonumber((axisXMini and axisXMini.GetStringWidth and axisXMini:GetStringWidth()) or 0) or 0),
                math.ceil(tonumber((axisYMini and axisYMini.GetStringWidth and axisYMini:GetStringWidth()) or 0) or 0)
            )
            A.axisLabelW = math.max(T.AXIS_LABEL_MIN_W, axisTextW)
            A.offsetSliderX = A.controlX + A.axisLabelW + T.AXIS_TO_CONTROL_GAP
            A.offsetSliderW = math.max(0, A.valueColX - A.offsetSliderX)
            return A
        end

        local brezExplain = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
        brezExplain:SetText("|cffffb347[Mythic+ Only]|r Battle Rez Timer shows charges and MM:SS recharge while charges recover.")
        brezExplain:SetJustifyH("LEFT")
        applyHelperTone(brezExplain)
        local lustExplain = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
        lustExplain:SetText("Bloodlust Timer shows local Active/Sated plus party Ready or Next Lust.")
        lustExplain:SetJustifyH("LEFT")
        applyHelperTone(lustExplain)
        local healerExplain = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
        healerExplain:SetText("Healer Status shows party healer state: |cff33ff33OK|r, |cffff3333OFFLINE|r, |cffff3333DEAD|r, |cffffd100OUTSIDE|r.")
        healerExplain:SetJustifyH("LEFT")
        applyHelperTone(healerExplain)
        local cooldownSweepExplain = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
        cooldownSweepExplain:SetText("Shows a radial cooldown animation over the timer icons while a countdown is running.")
        cooldownSweepExplain:SetJustifyH("LEFT")
        applyHelperTone(cooldownSweepExplain)
        local brezStatus = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
        brezStatus:SetText("")
        brezStatus:SetJustifyH("LEFT")
        brezStatus:SetWordWrap(false)
        applyHelperTone(brezStatus)
        local lustStatus = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
        lustStatus:SetText("")
        lustStatus:SetJustifyH("LEFT")
        lustStatus:SetWordWrap(false)
        applyHelperTone(lustStatus)
        local healerStatus = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
        healerStatus:SetText("")
        healerStatus:SetJustifyH("LEFT")
        healerStatus:SetWordWrap(false)
        applyHelperTone(healerStatus)
        local cooldownSweepStatus = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
        cooldownSweepStatus:SetText("")
        cooldownSweepStatus:SetJustifyH("LEFT")
        cooldownSweepStatus:SetWordWrap(false)
        applyHelperTone(cooldownSweepStatus)

        local lockRowContainer = CreateFrame("Frame", nil, scrollChild)
        lockRowContainer:SetSize(1, TIMERS_LAYOUT.LOCK_ROW_SEED_H)

        local coordsLabel = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        coordsLabel:SetText("Coordinates")
        coordsLabel:SetJustifyH("LEFT")
        local function normalizeTimersCoordsEdit(edit)
            if not edit then
                return
            end
            edit:SetJustifyH("CENTER")
            if edit.HasFocus and edit:HasFocus() then
                edit:SetTextColor(1, 1, 1, 1)
                edit:SetAlpha(1)
            else
                edit:SetTextColor(0.86, 0.86, 0.88, 1)
                edit:SetAlpha(0.90)
            end
            if edit.SetCursorPosition and not (edit.HasFocus and edit:HasFocus()) then
                edit:SetCursorPosition(0)
            end
            if edit.HighlightText and not (edit.HasFocus and edit:HasFocus()) then
                edit:HighlightText(0, 0)
            end
        end
        local function setTimersCoordsDisplay(edit, x, y)
            if not edit then
                return
            end
            edit:SetText(string.format("%d,%d", math.floor(tonumber(x) or 0), math.floor(tonumber(y) or 0)))
            normalizeTimersCoordsEdit(edit)
        end
        local function placeTimersCoordsCursorAtEnd(edit)
            if not edit or not edit.SetCursorPosition then
                return
            end
            edit:SetCursorPosition(string.len(tostring(edit:GetText() or "")))
        end
        local function queueTimersCoordsCursorAtEnd(edit)
            local function applyIfFocused()
                if edit and edit.HasFocus and edit:HasFocus() then
                    placeTimersCoordsCursorAtEnd(edit)
                end
            end
            applyIfFocused()
            if C_Timer and C_Timer.After then
                C_Timer.After(0, applyIfFocused)
                C_Timer.After(0.02, applyIfFocused)
            end
        end
        local coords = mkNumeric(scrollChild, function(edit)
            local activeStack = getStack()
            if not activeStack then return end
            local db = activeStack:GetDB()
            local text = trim(edit:GetText() or "")
            local xText, yText = text:match("^([%+%-]?%d+)%s*,%s*([%+%-]?%d+)$")
            if xText and yText then
                db.x = math.floor(tonumber(xText) or db.x or 0)
                db.y = math.floor(tonumber(yText) or db.y or 0)
                if not InCombatLockdown or not InCombatLockdown() then activeStack:ApplySettings() end
            end
            setTimersCoordsDisplay(edit, db.x or 0, db.y or 0)
        end)
        coords:SetTextColor(0.86, 0.86, 0.88, 1)
        coords:SetAlpha(0.90)
        coords:SetNumeric(false)
        setReadableNumericText(coords)
        coords:HookScript("OnEditFocusGained", function(edit)
            edit:SetJustifyH("CENTER")
            normalizeTimersCoordsEdit(edit)
            queueTimersCoordsCursorAtEnd(edit)
        end)
        coords:HookScript("OnMouseUp", function(edit)
            queueTimersCoordsCursorAtEnd(edit)
        end)
        coords:HookScript("OnShow", function(edit) normalizeTimersCoordsEdit(edit) end)
        coords:HookScript("OnEditFocusLost", function(edit) normalizeTimersCoordsEdit(edit) end)

        local resetAnchorButton = createActionButton(scrollChild, "Reset Anchor", function()
            local activeStack = getStack()
            if not activeStack then return end
            if activeStack.Initialize and not (activeStack.frame or activeStack.mainFrame) then
                activeStack:Initialize()
            end
            if activeStack.SetUnlocked then
                activeStack:SetUnlocked(true)
            end

            if activeStack.ResetPositionToCenter then
                activeStack:ResetPositionToCenter()
            else
                local db = activeStack:GetDB()
                db.x = 0
                db.y = 0
                if activeStack.ApplyPosition then
                    activeStack:ApplyPosition()
                end
                if activeStack.EvaluateVisibility then
                    activeStack:EvaluateVisibility("reset-anchor-fallback")
                elseif activeStack.Tick then
                    activeStack:Tick()
                end
            end

            if activeStack.frame then
                activeStack.frame:SetFrameStrata("HIGH")
                activeStack.frame:Raise()
            end

            if self._refresh then
                self._refresh()
            end
        end, TIMERS_LAYOUT.RESET_BUTTON_SEED_W)

        styleSecondaryActionButtonShared(resetAnchorButton, {
            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 function createTimersToggleRow(parent, labelText, opts)
            opts = type(opts) == "table" and opts or {}
            local semantic = tostring(opts.semantic or "toggle")
            local onChanged = type(opts.onChanged) == "function" and opts.onChanged or nil
            local rowValue = false

            local row = createPremiumSegmentedChoice(parent, labelText, nil, {
                { label = tostring(opts.offLabel or "Off"), value = false, toggleState = "off" },
                { label = tostring(opts.onLabel or "On"), value = true, toggleState = "on" },
            }, function(selectedValue)
                local nextValue = PA_SafeBool(selectedValue)
                rowValue = nextValue
                if onChanged then
                    onChanged(nextValue)
                end
            end, {
                semantic = (semantic == "meta") and "selection" or "toggle",
                selectionVariant = (semantic == "meta") and "lock_meta" or "",
                textEllipsis = true,
                normalizeValue = function(v)
                    return PA_SafeBool(v)
                end,
                getValue = function()
                    return rowValue
                end,
                setValue = function(v)
                    rowValue = PA_SafeBool(v)
                end,
            })

            row.value = false
            row.enabled = true
            if row.label then
                row.label:SetJustifyH("LEFT")
            end
            row.offBtn = row.buttons and row.buttons[1] or nil
            row.onBtn = row.buttons and row.buttons[2] or nil

            local baseSetEnabled = row.SetEnabled
            row.SetEnabled = function(self, enabled)
                self.enabled = enabled and true or false
                if baseSetEnabled then
                    baseSetEnabled(self, self.enabled)
                end
            end

            local baseLayout = row.Layout
            row.Layout = function(self, width)
                if baseLayout then
                    baseLayout(self, math.max(TIMERS_LAYOUT.MIN_SEGMENT_W, tonumber(width) or TIMERS_LAYOUT.LOCK_TOGGLE_PREF_W))
                end
            end

            if row.offBtn then
                row.offBtn:SetScript("OnClick", function()
                    if not row.enabled or not rowValue then
                        return
                    end
                    row:SetSelectedValue(false, true)
                end)
            end
            if row.onBtn then
                row.onBtn:SetScript("OnClick", function()
                    if not row.enabled or rowValue then
                        return
                    end
                    row:SetSelectedValue(true, true)
                end)
            end

            row.SetValue = function(self, v, trigger)
                local nextValue = PA_SafeBool(v)
                rowValue = nextValue
                self.value = nextValue
                self:SetVisualSelectedValue(nextValue)
                if trigger and onChanged then
                    onChanged(nextValue)
                end
            end
            row.GetValue = function()
                return rowValue
            end

            row:SetValue(false, false)
            return row
        end

        local function createTimersPremiumSlider(parent, labelText, minV, maxV, stepV, onApplyValue)
            local spec = {}
            local sliderMin = tonumber(minV) or 0
            local sliderMax = tonumber(maxV) or 100
            if sliderMax < sliderMin then
                sliderMin, sliderMax = sliderMax, sliderMin
            end
            local stepValue = tonumber(stepV) or 1
            if stepValue <= 0 then
                stepValue = 1
            end

            spec.label = createStyledFontString(parent, "ARTWORK", "GameFontHighlight", 12)
            spec.label:SetText(labelText or "")
            spec.label:SetJustifyH("LEFT")
            spec.control = CreateFrame("Frame", nil, parent)
            spec.control:SetHeight(TIMERS_LAYOUT.CONTROL_FLOOR)
            spec.control:SetWidth(TIMERS_LAYOUT.SLIDER_CONTROL_W)

            local valueButton = CreateFrame("Button", nil, spec.control)
            valueButton:SetSize(TIMERS_LAYOUT.SLIDER_VALUE_BUTTON_W, TIMERS_LAYOUT.SLIDER_VALUE_BUTTON_H)
            valueButton:SetPoint("RIGHT", spec.control, "RIGHT", 0, 0)

            local valueText = createStyledFontString(valueButton, "ARTWORK", "GameFontHighlightSmall", 11)
            valueText:SetPoint("RIGHT", valueButton, "RIGHT", 0, 0)
            valueText:SetJustifyH("RIGHT")
            valueText:SetTextColor(0.76, 0.76, 0.78, 1)

            local valueEdit = CreateFrame("EditBox", nil, valueButton)
            valueEdit:SetAllPoints(valueButton)
            valueEdit:SetAutoFocus(false)
            valueEdit:SetJustifyH("RIGHT")
            valueEdit:SetTextInsets(0, 0, 0, 0)
            valueEdit:SetTextColor(1, 1, 1, 1)
            valueEdit:SetHighlightColor(1, 1, 1, 0.25)
            valueEdit:SetNumeric(false)
            applyGlobalFontToObject(valueEdit, 11)
            valueEdit:Hide()

            spec.slider = CreateFrame("Slider", nil, spec.control)
            spec.slider:SetPoint("TOPLEFT", spec.control, "TOPLEFT", 0, 0)
            spec.slider:SetPoint("BOTTOMRIGHT", valueButton, "BOTTOMLEFT", -TIMERS_LAYOUT.SLIDER_VALUE_GAP, 0)
            spec.slider:SetMinMaxValues(sliderMin, sliderMax)
            spec.slider:SetValueStep(stepValue)
            if spec.slider.SetObeyStepOnDrag then
                spec.slider:SetObeyStepOnDrag(true)
            end
            spec.slider:SetThumbTexture("Interface\\Buttons\\WHITE8x8")
            local inputThumb = spec.slider:GetThumbTexture()
            if inputThumb then
                inputThumb:SetSize(16, 16)
                inputThumb:SetVertexColor(1, 1, 1, 0.01)
            end
            spec.slider:EnableMouse(true)
            spec.slider:SetOrientation("HORIZONTAL")

            local trackBg = spec.slider:CreateTexture(nil, "BACKGROUND")
            trackBg:SetTexture("Interface\\Buttons\\WHITE8x8")
            trackBg:SetPoint("LEFT", spec.slider, "LEFT", 0, 0)
            trackBg:SetPoint("RIGHT", spec.slider, "RIGHT", 0, 0)
            trackBg:SetHeight(2)
            trackBg:SetVertexColor(0.18, 0.18, 0.19, 0.95)

            local trackActive = spec.slider:CreateTexture(nil, "ARTWORK")
            trackActive:SetTexture("Interface\\Buttons\\WHITE8x8")
            trackActive:SetPoint("LEFT", spec.slider, "LEFT", 0, 0)
            trackActive:SetHeight(2)
            trackActive:SetWidth(0)
            trackActive:SetVertexColor(1.0, 0.82, 0.0, 0.75)

            local thumb = spec.slider:CreateTexture(nil, "OVERLAY")
            thumb:SetTexture("Interface\\Buttons\\WHITE8x8")
            thumb:SetSize(8, 8)
            thumb:SetVertexColor(1.0, 0.84, 0.08, 0.98)
            if thumb.SetMask then
                thumb:SetMask("Interface\\CharacterFrame\\TempPortraitAlphaMask")
            end

            local thumbGlow = spec.slider:CreateTexture(nil, "ARTWORK")
            thumbGlow:SetTexture("Interface\\Buttons\\WHITE8x8")
            thumbGlow:SetSize(14, 14)
            thumbGlow:SetVertexColor(1.0, 0.82, 0.0, 0)
            if thumbGlow.SetMask then
                thumbGlow:SetMask("Interface\\CharacterFrame\\TempPortraitAlphaMask")
            end
            thumbGlow:SetBlendMode("ADD")

            local suppress = false
            local controlEnabled = true
            local currentValue = nil
            local dragging = false
            local hovering = false
            local editingValue = false
            local dragLeft = nil
            local dragRight = nil
            local dragScale = nil
            local lastDragCursorX = nil

            local function resetDragTracking()
                dragLeft = nil
                dragRight = nil
                dragScale = nil
                lastDragCursorX = nil
            end

            local function captureDragBounds()
                dragLeft = spec.slider:GetLeft()
                dragRight = spec.slider:GetRight()
                dragScale = spec.slider:GetEffectiveScale() or 1
                lastDragCursorX = nil
                if not dragLeft or not dragRight or dragRight <= dragLeft or dragScale <= 0 then
                    resetDragTracking()
                end
            end

            local function normalize(n)
                local v = tonumber(n) or sliderMin
                if v < sliderMin then v = sliderMin end
                if v > sliderMax then v = sliderMax end
                v = sliderMin + (math.floor(((v - sliderMin) / stepValue) + 0.5) * stepValue)
                if v < sliderMin then v = sliderMin end
                if v > sliderMax then v = sliderMax end
                return math.floor(v + 0.5)
            end

            local function applyVisualState()
                local minVNow, maxVNow = spec.slider:GetMinMaxValues()
                local value = normalize(spec.slider:GetValue())
                local span = math.max(0.0001, (maxVNow - minVNow))
                local pct = (value - minVNow) / span
                if pct < 0 then pct = 0 end
                if pct > 1 then pct = 1 end
                local widthNow = spec.slider:GetWidth() or 0
                local x = pct * widthNow

                thumb:ClearAllPoints()
                thumb:SetPoint("CENTER", spec.slider, "LEFT", x, 0)
                thumbGlow:ClearAllPoints()
                thumbGlow:SetPoint("CENTER", thumb, "CENTER", 0, 0)

                trackActive:ClearAllPoints()
                trackActive:SetPoint("LEFT", spec.slider, "LEFT", 0, 0)
                trackActive:SetPoint("RIGHT", thumb, "CENTER", 0, 0)

                local glowAlpha = 0
                if not controlEnabled then
                    trackActive:SetVertexColor(0.48, 0.48, 0.50, 0.35)
                    thumb:SetVertexColor(0.62, 0.62, 0.64, 0.65)
                elseif dragging then
                    glowAlpha = 0.34
                    trackActive:SetVertexColor(1.0, 0.86, 0.16, 0.9)
                    thumb:SetVertexColor(1.0, 0.9, 0.22, 1)
                elseif hovering then
                    glowAlpha = 0.22
                    trackActive:SetVertexColor(1.0, 0.85, 0.12, 0.85)
                    thumb:SetVertexColor(1.0, 0.88, 0.18, 1)
                else
                    trackActive:SetVertexColor(1.0, 0.82, 0.0, 0.75)
                    thumb:SetVertexColor(1.0, 0.84, 0.08, 0.98)
                end
                thumbGlow:SetVertexColor(1.0, 0.82, 0.0, glowAlpha)
                spec.label:SetAlpha(controlEnabled and 1 or 0.55)
                spec.control:SetAlpha(controlEnabled and 1 or 0.55)
            end

            local function setValueVisualOnly(v, keepCurrentValue)
                local n = normalize(v)
                if not keepCurrentValue then
                    currentValue = n
                end
                suppress = true
                spec.slider:SetValue(n)
                suppress = false
                valueText:SetText(tostring(n))
                if valueEdit:IsShown() then
                    valueEdit:SetText(tostring(n))
                end
                applyVisualState()
            end

            local function applyValue(v)
                local n = normalize(v)
                if currentValue == n then
                    valueText:SetText(tostring(n))
                    applyVisualState()
                    return
                end
                currentValue = n
                valueText:SetText(tostring(n))
                if valueEdit:IsShown() then
                    valueEdit:SetText(tostring(n))
                end
                if onApplyValue then
                    onApplyValue(n)
                end
                applyVisualState()
            end

            local function beginEdit()
                editingValue = true
                valueText:Hide()
                valueEdit:SetText(tostring(normalize(currentValue or spec.slider:GetValue())))
                valueEdit:Show()
                valueEdit:SetFocus()
                valueEdit:HighlightText()
            end

            local function endEdit(commit)
                if not editingValue then
                    return
                end
                editingValue = false
                if commit then
                    local parsed = tonumber(trim(valueEdit:GetText() or ""))
                    if parsed ~= nil then
                        local n = normalize(parsed)
                        setValueVisualOnly(n, true)
                        applyValue(n)
                    else
                        setValueVisualOnly(currentValue or spec.slider:GetValue())
                    end
                end
                valueEdit:Hide()
                valueText:Show()
                valueEdit:ClearFocus()
                valueText:SetText(tostring(normalize(currentValue or spec.slider:GetValue())))
            end

            local function setValueFromCursor()
                local left = (dragging and dragLeft) or spec.slider:GetLeft()
                local right = (dragging and dragRight) or spec.slider:GetRight()
                if not left or not right or right <= left then
                    return
                end
                local cx = GetCursorPosition()
                local scale = (dragging and dragScale) or (spec.slider:GetEffectiveScale() or 1)
                if not cx or scale <= 0 then
                    return
                end
                cx = cx / scale
                if dragging and lastDragCursorX ~= nil and math.abs(cx - lastDragCursorX) < 0.25 then
                    return
                end
                if dragging then
                    lastDragCursorX = cx
                end
                local pct = (cx - left) / (right - left)
                if pct < 0 then pct = 0 end
                if pct > 1 then pct = 1 end
                local raw = sliderMin + ((sliderMax - sliderMin) * pct)
                local n = normalize(raw)
                if currentValue == n then
                    applyVisualState()
                    return
                end
                setValueVisualOnly(n, true)
                applyValue(n)
            end

            spec.slider:SetScript("OnValueChanged", function(selfSlider, value)
                local n = normalize(value)
                if selfSlider:GetValue() ~= n then
                    suppress = true
                    selfSlider:SetValue(n)
                    suppress = false
                end
                valueText:SetText(tostring(n))
                applyVisualState()
                if suppress or isControlHydrating(selfSlider) then
                    return
                end
                applyValue(n)
            end)
            spec.slider:SetScript("OnMouseDown", function()
                if not controlEnabled then
                    return
                end
                dragging = true
                captureDragBounds()
                setValueFromCursor()
                spec.slider:SetScript("OnUpdate", function()
                    if dragging then
                        setValueFromCursor()
                    end
                end)
                applyVisualState()
            end)
            spec.slider:SetScript("OnMouseUp", function()
                dragging = false
                resetDragTracking()
                spec.slider:SetScript("OnUpdate", nil)
                applyVisualState()
            end)
            spec.slider:SetScript("OnEnter", function()
                if not controlEnabled then
                    return
                end
                hovering = true
                applyVisualState()
            end)
            spec.slider:SetScript("OnLeave", function()
                hovering = false
                dragging = false
                resetDragTracking()
                spec.slider:SetScript("OnUpdate", nil)
                applyVisualState()
            end)

            valueButton:SetScript("OnClick", function()
                if not controlEnabled then
                    return
                end
                beginEdit()
            end)
            valueEdit:SetScript("OnEnterPressed", function(edit)
                endEdit(true)
                edit:ClearFocus()
            end)
            valueEdit:SetScript("OnEscapePressed", function(edit)
                endEdit(false)
                edit:ClearFocus()
            end)
            valueEdit:SetScript("OnEditFocusLost", function()
                endEdit(true)
            end)

            function spec:SetVisualValue(v)
                setValueVisualOnly(v)
            end
            function spec:Refresh(v)
                setValueVisualOnly(v)
            end
            function spec:IsInteracting()
                return dragging or editingValue
            end
            function spec:SetWidth(newWidth)
                local fullW = math.max(0, math.floor(tonumber(newWidth) or TIMERS_LAYOUT.SLIDER_CONTROL_W))
                self.control:SetWidth(fullW)
                applyVisualState()
            end
            function spec:SetEnabled(enabled)
                controlEnabled = enabled and true or false
                if not controlEnabled then
                    dragging = false
                    hovering = false
                    endEdit(false)
                    resetDragTracking()
                    spec.slider:SetScript("OnUpdate", nil)
                end
                spec.slider:EnableMouse(controlEnabled)
                valueButton:EnableMouse(controlEnabled)
                valueEdit:EnableMouse(controlEnabled)
                applyVisualState()
            end
            spec.control:SetScript("OnSizeChanged", function()
                applyVisualState()
            end)

            spec.valueButton = valueButton
            spec.valueText = valueText
            spec.valueEdit = valueEdit
            setValueVisualOnly(sliderMin)
            return spec
        end

        local lockRow = createTimersToggleRow(scrollChild, "Lock Position", {
            semantic = "meta",
            offLabel = "Unlocked",
            onLabel = "Locked",
            onChanged = function(locked)
                widgets.unlock:SetChecked(locked and true or false)
                local clickHandler = widgets.unlock:GetScript("OnClick")
                if clickHandler then
                    clickHandler(widgets.unlock)
                end
            end,
        })
        local masterRowContainer = CreateFrame("Frame", nil, scrollChild)
        masterRowContainer:SetSize(1, TIMERS_LAYOUT.LOCK_ROW_SEED_H)
        local masterEnabledRow = createTimersToggleRow(scrollChild, "Module", {
            semantic = "toggle",
            offLabel = "Off",
            onLabel = "On",
            onChanged = function(enabled)
                local activeStack, db = getTimersDBSnapshot()
                writeTimerSetting(db, "enabled", PA_SafeBool(enabled))
                if activeStack and activeStack.ApplySettings then
                    applyStackSettings()
                end
                if self._refresh then
                    self._refresh()
                end
            end,
        })
        self._settingsWindowHeaderToggleSource = masterEnabledRow
        local modulesHeader = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        modulesHeader:SetText("Modules")
        modulesHeader:SetJustifyH("LEFT")
        local appearanceLayoutHeader = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlight", 12)
        appearanceLayoutHeader:SetText("Appearance & Layout")
        appearanceLayoutHeader:SetJustifyH("LEFT")
        local SECTION_HEADER_R, SECTION_HEADER_G, SECTION_HEADER_B, SECTION_HEADER_A = 1, 0.82, 0, 1
        modulesHeader:SetTextColor(SECTION_HEADER_R, SECTION_HEADER_G, SECTION_HEADER_B, SECTION_HEADER_A)
        appearanceLayoutHeader:SetTextColor(SECTION_HEADER_R, SECTION_HEADER_G, SECTION_HEADER_B, SECTION_HEADER_A)
        local lockStateHelper = createStyledFontString(scrollChild, "ARTWORK", "GameFontHighlightSmall", 10)
        lockStateHelper:SetText("")
        applyHelperTone(lockStateHelper)
        lockStateHelper:SetWordWrap(false)
        lockStateHelper:SetJustifyH("LEFT")
        local clearBatchStatusFeedback = function() end

        local function commitModuleToggle(moduleId, dbKey, value)
            clearBatchStatusFeedback()
            local available = isModuleAvailable(moduleId)
            if not available then
                return
            end
            local activeStack, db = getTimersDBSnapshot()
            writeTimerSetting(db, dbKey, PA_SafeBool(value))
            if activeStack and activeStack.ApplySettings then
                applyStackSettings()
            end
            if self._refresh then
                self._refresh()
            end
        end

        local brezRow = createTimersToggleRow(scrollChild, "Battle Rez Timer", {
            semantic = "toggle",
            onChanged = function(v)
                commitModuleToggle("brez", "brezEnabled", v)
            end,
        })
        local lustRow = createTimersToggleRow(scrollChild, "Bloodlust Timer", {
            semantic = "toggle",
            onChanged = function(v)
                commitModuleToggle("lust", "lustEnabled", v)
            end,
        })
        local lustSoundRow = createTimersToggleRow(scrollChild, "Play sound when Sated ends", {
            semantic = "toggle",
            onChanged = function(v)
                clearBatchStatusFeedback()
                local activeStack, db = getTimersDBSnapshot()
                writeTimerSetting(db, "lustSatedEndSound", PA_SafeBool(v))
                if activeStack and activeStack.ApplySettings then
                    applyStackSettings()
                end
                if self._refresh then
                    self._refresh()
                end
            end,
        })
        local healerRow = createTimersToggleRow(scrollChild, "Healer Status", {
            semantic = "toggle",
            onChanged = function(v)
                commitModuleToggle("healer", "healerEnabled", v)
            end,
        })
        local cooldownSweepRow = createTimersToggleRow(scrollChild, "Cooldown Sweep", {
            semantic = "toggle",
            onChanged = function(v)
                commitModuleToggle("cooldownSweep", "cooldownSweepEnabled", v)
            end,
        })
        local BATCH_STATUS_BASELINE_TEXT = "Applies to available modules only."
        local BATCH_STATUS_NONE_AVAILABLE_TEXT = "No available modules."
        local batchStatusMode = "baseline"
        local batchStatusFeedbackText = ""
        local batchAvailableCount = 0
        local batchStatusMaxWidth = 0
        local applyBatchModules = nil

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

        local batchActionsTitle = createStyledFontString(batchActionsCard, "ARTWORK", "GameFontHighlight", 12)
        batchActionsTitle:SetText("Batch Actions")
        batchActionsTitle:SetTextColor(1, 0.82, 0, 1)
        batchActionsTitle:SetJustifyH("LEFT")

        local batchActionsScope = createStyledFontString(batchActionsCard, "ARTWORK", "GameFontHighlightSmall", 10)
        batchActionsScope:SetText("")
        batchActionsScope:SetJustifyH("LEFT")
        applyHelperTone(batchActionsScope)
        batchActionsScope:SetWordWrap(false)
        if batchActionsScope.SetMaxLines then
            batchActionsScope:SetMaxLines(1)
        end

        local batchEnableButton = createActionButton(batchActionsCard, "Enable all modules", function()
            clearBatchStatusFeedback()
            local changedCount = applyBatchModules and applyBatchModules(true) or 0
            batchStatusMode = "feedback"
            batchStatusFeedbackText = string.format("Enabled %d modules.", math.max(0, changedCount))
            if self._refresh then
                self._refresh()
            end
        end, 156)
        local batchDisableButton = createActionButton(batchActionsCard, "Disable all modules", function()
            clearBatchStatusFeedback()
            local changedCount = applyBatchModules and applyBatchModules(false) or 0
            batchStatusMode = "feedback"
            batchStatusFeedbackText = string.format("Disabled %d modules.", math.max(0, changedCount))
            if self._refresh then
                self._refresh()
            end
        end, 156)
        local batchButtonStyle = {
            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 },
        }
        styleSecondaryActionButtonShared(batchEnableButton, batchButtonStyle)
        styleSecondaryActionButtonShared(batchDisableButton, batchButtonStyle)

        local batchStatus = createStyledFontString(batchActionsCard, "ARTWORK", "GameFontHighlightSmall", 10)
        batchStatus:SetText(BATCH_STATUS_BASELINE_TEXT)
        batchStatus:SetJustifyH("LEFT")
        batchStatus:SetWordWrap(false)
        if batchStatus.SetMaxLines then
            batchStatus:SetMaxLines(1)
        end
        applyHelperTone(batchStatus)

        local function resolveBatchStatusText()
            if batchAvailableCount <= 0 then
                return BATCH_STATUS_NONE_AVAILABLE_TEXT
            end
            if batchStatusMode == "feedback" and batchStatusFeedbackText ~= "" then
                return batchStatusFeedbackText
            end
            return BATCH_STATUS_BASELINE_TEXT
        end

        local function refreshBatchStatusLine()
            local text = resolveBatchStatusText()
            setTimersText(batchStatus, text)
            if batchStatusMaxWidth and batchStatusMaxWidth > 0 then
                setTimerSingleLineEllipsis(batchStatus, text, batchStatusMaxWidth)
            else
                batchStatus:SetText(text)
            end
        end

        clearBatchStatusFeedback = function()
            if batchStatusMode ~= "baseline" or batchStatusFeedbackText ~= "" then
                batchStatusMode = "baseline"
                batchStatusFeedbackText = ""
                refreshBatchStatusLine()
            end
        end

        local moduleRows = {
            {
                id = "brez",
                key = "brezEnabled",
                check = brezEnable,
                row = brezRow,
                unavailableHelper = brezStatus,
                explainers = { brezExplain },
            },
            {
                id = "lust",
                key = "lustEnabled",
                check = lustEnable,
                row = lustRow,
                unavailableHelper = lustStatus,
                explainBeforeChildren = true,
                explainBlockGap = 6,
                explainers = { lustExplain },
            },
            {
                id = "lustSound",
                key = "lustSatedEndSound",
                row = lustSoundRow,
                excludeFromBatch = true,
                visibilityOverride = function(db)
                    return PA_SafeBool(readTimerSetting(db, "lustEnabled", true))
                end,
            },
            {
                id = "healer",
                key = "healerEnabled",
                check = healerEnable,
                row = healerRow,
                unavailableHelper = healerStatus,
                explainers = { healerExplain },
            },
            {
                id = "cooldownSweep",
                key = "cooldownSweepEnabled",
                check = cooldownSweepEnable,
                row = cooldownSweepRow,
                unavailableHelper = cooldownSweepStatus,
                explainers = { cooldownSweepExplain },
            },
        }
        for i = 1, #moduleRows do
            local row = moduleRows[i]
            if row and row.row then
                local extraRefs = {}
                if frameIsAlive(row.unavailableHelper) then
                    extraRefs[#extraRefs + 1] = row.unavailableHelper
                end
                if type(row.explainers) == "table" then
                    for j = 1, #row.explainers do
                        if frameIsAlive(row.explainers[j]) then
                            extraRefs[#extraRefs + 1] = row.explainers[j]
                        end
                    end
                end
                if #extraRefs > 0 then
                    settingsSearchRuntime.attachExtraSearchRefs(row.row, extraRefs)
                end
            end
        end

        local staticTimersTextFields = {
            header,
            modulesHeader,
            appearanceLayoutHeader,
            batchActionsTitle,
            batchActionsScope,
            batchStatus,
            lockRow and lockRow.label or nil,
            brezRow and brezRow.label or nil,
            lustRow and lustRow.label or nil,
            lustSoundRow and lustSoundRow.label or nil,
            healerRow and healerRow.label or nil,
            cooldownSweepRow and cooldownSweepRow.label or nil,
            brezExplain,
            lustExplain,
            healerExplain,
            cooldownSweepExplain,
            fontLabel,
            coordsLabel,
            iconSizeSlider and iconSizeSlider.label or nil,
            iconOffsetPairLabel,
            iconOffsetAxisXMini,
            iconOffsetAxisYMini,
            iconEdgeLabel,
            aspectLabel,
        }
        for _, fs in ipairs(staticTimersTextFields) do
            rememberTimersText(fs)
        end

        for _, module in ipairs(moduleRows) do
            if module and module.row and module.row.control and module.row.control.HookScript then
                module.row.control:HookScript("OnMouseDown", function()
                    clearBatchStatusFeedback()
                end)
            end
        end

        applyBatchModules = function(desiredEnabled)
            local activeStack, db = getTimersDBSnapshot()
            if not db then
                return 0
            end
            local targetValue = PA_SafeBool(desiredEnabled)
            local changedCount = 0
            for _, module in ipairs(moduleRows) do
                if not module.excludeFromBatch then
                local available = isModuleAvailable(module.id)
                if available then
                    local currentValue = PA_SafeBool(readTimerSetting(db, module.key, true))
                    if currentValue ~= targetValue then
                        writeTimerSetting(db, module.key, targetValue)
                        changedCount = changedCount + 1
                    end
                end
                end
            end
            if changedCount > 0 and activeStack and activeStack.ApplySettings then
                applyStackSettings()
            end
            return changedCount
        end

        local iconSizeSlider = createTimersPremiumSlider(scrollChild, "Icon Size", 12, 64, 1, function(v)
            local activeStack = getStack()
            if not activeStack then return end
            local db = activeStack:GetDB()
            db.iconSize = math.floor(math.min(64, math.max(12, tonumber(v) or db.iconSize or 18)))
            applyStackSettings()
        end)
        local iconOffsetXSlider = createTimersPremiumSlider(scrollChild, "Icon Offset X", -50, 50, 1, function(v)
            local activeStack = getStack()
            if not activeStack then return end
            local db = activeStack:GetDB()
            db.iconOffsetX = math.floor(math.min(50, math.max(-50, tonumber(v) or db.iconOffsetX or 0)))
            applyStackSettings()
        end)
        local iconOffsetYSlider = createTimersPremiumSlider(scrollChild, "Icon Offset Y", -50, 50, 1, function(v)
            local activeStack = getStack()
            if not activeStack then return end
            local db = activeStack:GetDB()
            db.iconOffsetY = math.floor(math.min(50, math.max(-50, tonumber(v) or db.iconOffsetY or 0)))
            applyStackSettings()
        end)

        local function syncModuleVisuals(db)
            local availableCount = 0
            for _, module in ipairs(moduleRows) do
                local visibilityOverride = module.visibilityOverride
                local isVisible = (type(visibilityOverride) ~= "function") or visibilityOverride(db)
                local available, unavailableReason = isModuleAvailable(module.id)
                if visibilityOverride then
                    available = available and isVisible
                    if not isVisible then
                        unavailableReason = nil
                    end
                end
                local defaultValue = (module.key == "lustSatedEndSound") and false or true
                local moduleValue = PA_SafeBool(readTimerSetting(db, module.key, defaultValue))
                if module.check then
                    module.check:SetChecked(moduleValue)
                    if available then
                        module.check:Enable()
                        if module.check.text then
                            module.check.text:SetTextColor(1, 0.82, 0, 1)
                        end
                    else
                        module.check:Disable()
                        if module.check.text then
                            module.check.text:SetTextColor(0.6, 0.6, 0.6, 1)
                        end
                    end
                end
                if module.row then
                    module.row:SetEnabled(available)
                    module.row:SetValue(moduleValue, false)
                end
                if module.unavailableHelper then
                    setTimersText(module.unavailableHelper, available and "" or tostring(unavailableReason or "Unavailable"))
                end
                if module.row and visibilityOverride and not isVisible then
                    parkTimersControl(module.row.label)
                    parkTimersControl(module.row.control)
                end
                for _, child in ipairs(module.childRows or {}) do
                    local childValue = PA_SafeBool(readTimerSetting(db, child.key, false))
                    local childEnabled = available and (not child.isVisible or child.isVisible(db))
                    if child.row then
                        child.row:SetEnabled(childEnabled)
                        child.row:SetValue(childValue, false)
                    end
                end
                if available and not module.excludeFromBatch then
                    availableCount = availableCount + 1
                end
            end
            batchAvailableCount = availableCount
            local hasAvailable = availableCount > 0
            if batchEnableButton then
                batchEnableButton:SetEnabled(hasAvailable)
                batchEnableButton:SetAlpha(hasAvailable and 1 or 0.55)
            end
            if batchDisableButton then
                batchDisableButton:SetEnabled(hasAvailable)
                batchDisableButton:SetAlpha(hasAvailable and 1 or 0.55)
            end
            refreshBatchStatusLine()
        end

        local function syncLockStateHelperText(isLocked, moduleEnabled)
            if not lockStateHelper then
                return
            end
            if not moduleEnabled then
                setTimersText(lockStateHelper, "Timers are disabled. Re-enable to edit settings.")
            elseif isLocked then
                setTimersText(lockStateHelper, "Timers are locked.")
            else
                setTimersText(lockStateHelper, "Timers are unlocked - drag to reposition.")
            end
        end

        local function applyTimersModuleEnabledState(moduleEnabled)
            setSettingsControlEnabled(widgets.unlock, moduleEnabled)
            if lockRow then
                lockRow:SetEnabled(moduleEnabled)
            end
            setSettingsControlEnabled(resetAnchorButton, moduleEnabled)

            for _, module in ipairs(moduleRows) do
                if module and module.row and not moduleEnabled then
                    module.row:SetEnabled(false)
                end
                for _, child in ipairs(module and module.childRows or {}) do
                    if child and child.row and not moduleEnabled then
                        child.row:SetEnabled(false)
                    end
                end
            end

            if not moduleEnabled then
                if batchEnableButton then
                    batchEnableButton:SetEnabled(false)
                    batchEnableButton:SetAlpha(0.55)
                end
                if batchDisableButton then
                    batchDisableButton:SetEnabled(false)
                    batchDisableButton:SetAlpha(0.55)
                end
            end

            setSettingsControlEnabled(fontDropdown, moduleEnabled)
            setSettingsControlEnabled(aspectDropdown, moduleEnabled)
            setSettingsControlEnabled(iconEdgeControl, moduleEnabled)
            setSettingsControlEnabled(sizeStepper or sizeEdit, moduleEnabled)
            setSettingsControlEnabled(coords, moduleEnabled)
            setSettingsControlEnabled(iconSizeSlider, moduleEnabled)
            setSettingsControlEnabled(iconOffsetXSlider, moduleEnabled)
            setSettingsControlEnabled(iconOffsetYSlider, moduleEnabled)

            if lockRowContainer and lockRowContainer.SetAlpha then
                lockRowContainer:SetAlpha(moduleEnabled and 1 or 0.55)
            end
            if batchActionsCard and batchActionsCard.SetAlpha then
                batchActionsCard:SetAlpha(moduleEnabled and 1 or 0.55)
            end

            for _, textWidget in ipairs({
                modulesHeader,
                appearanceLayoutHeader,
                batchActionsTitle,
                batchActionsScope,
                batchStatus,
                fontLabel,
                coordsLabel,
                iconEdgeLabel,
                aspectLabel,
                iconOffsetPairLabel,
                iconOffsetAxisXMini,
                iconOffsetAxisYMini,
                brezExplain,
                lustExplain,
                healerExplain,
                cooldownSweepExplain,
            }) do
                setSettingsTextEnabled(textWidget, moduleEnabled)
            end
        end

        local function refresh()
            local activeStack, db = getTimersDBSnapshot()
            local isLocked = getTimersLockedState()
            local moduleEnabled = PA_SafeBool(readTimerSetting(db, "enabled", true))
            widgets.unlock:SetChecked(isLocked)
            updateUnlockLabelColor(isLocked)
            if masterEnabledRow then
                masterEnabledRow:SetValue(moduleEnabled, false)
            end
            if lockRow then
                lockRow:SetValue(isLocked, false)
            end
            syncLockStateHelperText(isLocked, moduleEnabled)

            syncModuleVisuals(db)
            local fontSizeValue = math.floor(tonumber(readTimerSetting(db, "fontSize", 12)) or 12)
            local iconSizeValue = math.floor(tonumber(readTimerSetting(db, "iconSize", 18)) or 18)
            local iconOffsetXValue = math.floor(tonumber(readTimerSetting(db, "iconOffsetX", 0)) or 0)
            local iconOffsetYValue = math.floor(tonumber(readTimerSetting(db, "iconOffsetY", 0)) or 0)
            if fontDropdown.Refresh then fontDropdown.Refresh() end
            if iconEdgeControl and iconEdgeControl.Refresh then iconEdgeControl:Refresh() end
            if aspectDropdown.Refresh then aspectDropdown.Refresh() end
            if sizeStepper and sizeStepper.SetVisualValue then
                local fontSizeEditing = sizeStepper.valueEdit and sizeStepper.valueEdit.IsShown and sizeStepper.valueEdit:IsShown()
                if not fontSizeEditing then
                    sizeStepper:SetVisualValue(fontSizeValue)
                end
            elseif sizeEdit and not sizeEdit:HasFocus() then
                sizeEdit:SetText(tostring(fontSizeValue))
            end
            if not iconSizeEdit:HasFocus() then iconSizeEdit:SetText(tostring(iconSizeValue)) end
            if not iconOffsetXEdit:HasFocus() then iconOffsetXEdit:SetText(tostring(iconOffsetXValue)) end
            if not iconOffsetYEdit:HasFocus() then iconOffsetYEdit:SetText(tostring(iconOffsetYValue)) end
            local liveX, liveY = getTimersDisplayPosition(activeStack, db)
            if not coords:HasFocus() then setTimersCoordsDisplay(coords, liveX, liveY) else normalizeTimersCoordsEdit(coords) end
            if iconSizeSlider and (not iconSizeSlider.IsInteracting or not iconSizeSlider:IsInteracting()) then
                iconSizeSlider:Refresh(iconSizeValue)
            end
            if iconOffsetXSlider and (not iconOffsetXSlider.IsInteracting or not iconOffsetXSlider:IsInteracting()) then
                iconOffsetXSlider:Refresh(iconOffsetXValue)
            end
            if iconOffsetYSlider and (not iconOffsetYSlider.IsInteracting or not iconOffsetYSlider:IsInteracting()) then
                iconOffsetYSlider:Refresh(iconOffsetYValue)
            end
            applyTimersModuleEnabledState(moduleEnabled)
            if self._layout then self._layout() end
            refreshSettingsWindowHeaderAccessoryIfHosted(self)
        end
        local function syncTimersLiveFields()
            local activeStack, db = getTimersDBSnapshot()
            local isLocked = getTimersLockedState()
            local moduleEnabled = PA_SafeBool(readTimerSetting(db, "enabled", true))
            widgets.unlock:SetChecked(isLocked)
            updateUnlockLabelColor(isLocked)
            syncModuleVisuals(db)
            if masterEnabledRow then
                masterEnabledRow:SetValue(moduleEnabled, false)
            end
            if lockRow then
                lockRow:SetValue(isLocked, false)
            end
            syncLockStateHelperText(isLocked, moduleEnabled)

            local fontSizeValue = math.floor(tonumber(readTimerSetting(db, "fontSize", 12)) or 12)
            local iconSizeValue = math.floor(tonumber(readTimerSetting(db, "iconSize", 18)) or 18)
            local iconOffsetXValue = math.floor(tonumber(readTimerSetting(db, "iconOffsetX", 0)) or 0)
            local iconOffsetYValue = math.floor(tonumber(readTimerSetting(db, "iconOffsetY", 0)) or 0)
            if sizeStepper and sizeStepper.SetVisualValue then
                local fontSizeEditing = sizeStepper.valueEdit and sizeStepper.valueEdit.IsShown and sizeStepper.valueEdit:IsShown()
                if not fontSizeEditing then
                    sizeStepper:SetVisualValue(fontSizeValue)
                end
            elseif sizeEdit and not sizeEdit:HasFocus() then
                sizeEdit:SetText(tostring(fontSizeValue))
            end
            if not iconSizeEdit:HasFocus() then iconSizeEdit:SetText(tostring(iconSizeValue)) end
            if not iconOffsetXEdit:HasFocus() then iconOffsetXEdit:SetText(tostring(iconOffsetXValue)) end
            if not iconOffsetYEdit:HasFocus() then iconOffsetYEdit:SetText(tostring(iconOffsetYValue)) end
            local liveX, liveY = getTimersDisplayPosition(activeStack, db)
            if not coords:HasFocus() then setTimersCoordsDisplay(coords, liveX, liveY) else normalizeTimersCoordsEdit(coords) end
            if iconSizeSlider and (not iconSizeSlider.IsInteracting or not iconSizeSlider:IsInteracting()) then
                iconSizeSlider:SetVisualValue(iconSizeValue)
            end
            if iconOffsetXSlider and (not iconOffsetXSlider.IsInteracting or not iconOffsetXSlider:IsInteracting()) then
                iconOffsetXSlider:SetVisualValue(iconOffsetXValue)
            end
            if iconOffsetYSlider and (not iconOffsetYSlider.IsInteracting or not iconOffsetYSlider:IsInteracting()) then
                iconOffsetYSlider:SetVisualValue(iconOffsetYValue)
            end
            applyTimersModuleEnabledState(moduleEnabled)
        end

        self._refresh = refresh
        self._syncTimersLiveFields = syncTimersLiveFields

        local timersLayoutRefs = {
            widgetsUnlock = widgets.unlock,
            brezEnable = brezEnable,
            lustEnable = lustEnable,
            healerEnable = healerEnable,
            cooldownSweepEnable = cooldownSweepEnable,
            enableLabel = enableLabel,
            sizeLabel = sizeLabel,
            iconSizeLabel = iconSizeLabel,
            iconOffsetXLabel = iconOffsetXLabel,
            iconOffsetYLabel = iconOffsetYLabel,
            iconSizeEdit = iconSizeEdit,
            iconOffsetXEdit = iconOffsetXEdit,
            iconOffsetYEdit = iconOffsetYEdit,
            header = header,
            masterRow = masterEnabledRow,
            masterRowContainer = masterRowContainer,
            lockRow = lockRow,
            lockRowContainer = lockRowContainer,
            resetAnchorButton = resetAnchorButton,
            lockStateHelper = lockStateHelper,
            divider = divider,
            modulesHeader = modulesHeader,
            batchActionsCard = batchActionsCard,
            batchActionsTitle = batchActionsTitle,
            batchActionsScope = batchActionsScope,
            batchEnableButton = batchEnableButton,
            batchDisableButton = batchDisableButton,
            batchStatus = batchStatus,
            dividerFont = dividerFont,
            appearanceLayoutHeader = appearanceLayoutHeader,
            fontDropdown = fontDropdown,
            aspectDropdown = aspectDropdown,
            iconEdgeControl = iconEdgeControl,
            sizeStepper = sizeStepper,
            sizeEdit = sizeEdit,
            coords = coords,
            coordsLabel = coordsLabel,
            fontRowContainer = fontRowContainer,
            fontLabel = fontLabel,
            iconSizeSlider = iconSizeSlider,
            iconOffsetXSlider = iconOffsetXSlider,
            iconOffsetYSlider = iconOffsetYSlider,
            lustSoundRow = lustSoundRow,
            cooldownSweepRow = cooldownSweepRow,
            iconOffsetPairLabel = iconOffsetPairLabel,
            iconOffsetAxisXMini = iconOffsetAxisXMini,
            iconOffsetAxisYMini = iconOffsetAxisYMini,
            iconOffsetRowContainer = iconOffsetRowContainer,
            iconEdgeLabel = iconEdgeLabel,
            aspectLabel = aspectLabel,
        }

        local function layout()
            local R = timersLayoutRefs
            local T = TIMERS_LAYOUT
            local _, layoutDb = getTimersDBSnapshot()
            local contentW = math.max((scrollFrame:GetWidth() or 0) - 8, T.MIN_CONTENT_W)
            local useHostedChrome = isPanelUsingCustomSettingsWindowHost(self)
            local G = computeTimersGeometry(contentW)
            local lockG = computeTimersLockRowGeometry(G, R.lockRow, R.resetAnchorButton)
            local moduleG = computeTimersModuleGeometry(G, R.cooldownSweepRow or R.lockRow)
            local batchG = computeTimersBatchCardGeometry(G, R.batchActionsScope, R.batchEnableButton, R.batchDisableButton)
            local appearanceG = computeTimersAppearanceGeometry(G, R.iconOffsetAxisXMini, R.iconOffsetAxisYMini)
            scrollChild:SetWidth(G.contentW)

            for _, control in ipairs({
                R.header,
                R.masterRowContainer,
                R.masterRow and R.masterRow.label or nil,
                R.masterRow and R.masterRow.control or nil,
                R.lockRowContainer,
                R.lockRow and R.lockRow.label or nil,
                R.lockRow and R.lockRow.control or nil,
                R.resetAnchorButton,
                R.lockStateHelper,
                R.divider,
                R.modulesHeader,
                R.batchActionsCard,
                R.batchActionsTitle,
                R.batchActionsScope,
                R.batchEnableButton,
                R.batchDisableButton,
                R.batchStatus,
                R.dividerFont,
                R.appearanceLayoutHeader,
                R.fontRowContainer,
                R.fontLabel,
                R.fontDropdown and R.fontDropdown.control or nil,
                (R.sizeStepper and R.sizeStepper.control) or R.sizeEdit,
                R.coordsLabel,
                R.coords,
                R.iconSizeSlider and R.iconSizeSlider.label or nil,
                R.iconSizeSlider and R.iconSizeSlider.control or nil,
                R.iconOffsetRowContainer,
                R.iconOffsetPairLabel,
                R.iconOffsetAxisXMini,
                R.iconOffsetXSlider and R.iconOffsetXSlider.control or nil,
                R.iconOffsetAxisYMini,
                R.iconOffsetYSlider and R.iconOffsetYSlider.control or nil,
                R.iconEdgeLabel,
                R.iconEdgeControl and R.iconEdgeControl.control or nil,
                R.aspectLabel,
                R.aspectDropdown and R.aspectDropdown.control or nil,
            }) do
                showTimersControl(control)
            end
            for _, module in ipairs(moduleRows) do
                if module and module.row then
                    showTimersControl(module.row.label)
                    showTimersControl(module.row.control)
                end
                for _, child in ipairs(module and module.childRows or {}) do
                    if child and child.row then
                        showTimersControl(child.row.label)
                        showTimersControl(child.row.control)
                    end
                end
                showTimersControl(module and module.unavailableHelper or nil)
                for _, fs in ipairs(module and module.explainers or {}) do
                    showTimersControl(fs)
                end
            end

            local function setPickerWidth(picker, width)
                local w = math.max(0, math.floor(tonumber(width) or 0))
                if picker and picker.SetWidth then
                    picker:SetWidth(w)
                elseif picker and picker.control then
                    picker.control:SetWidth(w)
                end
            end

            parkTimersControl(R.widgetsUnlock)
            parkTimersControl(R.brezEnable)
            parkTimersControl(R.lustEnable)
            parkTimersControl(R.healerEnable)
            parkTimersControl(R.cooldownSweepEnable)
            parkTimersControl(R.enableLabel)
            parkTimersControl(R.sizeLabel)
            parkTimersControl(R.iconSizeLabel)
            parkTimersControl(R.iconOffsetXLabel)
            parkTimersControl(R.iconOffsetYLabel)
            parkTimersControl(R.iconSizeEdit)
            parkTimersControl(R.iconOffsetXEdit)
            parkTimersControl(R.iconOffsetYEdit)

            local y = -T.TOP_PAD
            if useHostedChrome then
                y = y - SETTINGS_WINDOW_HOSTED_TOP_RHYTHM_NUDGE
            end

            if useHostedChrome then
                parkTimersControl(R.header)
                parkTimersControl(headerDescriptor)
                parkTimersControl(R.masterRowContainer)
                parkTimersControl(R.masterRow and R.masterRow.label or nil)
                parkTimersControl(R.masterRow and R.masterRow.control or nil)
            else
                showTimersControl(R.header)
                showTimersControl(headerDescriptor)
                showTimersControl(R.masterRowContainer)
                showTimersControl(R.masterRow and R.masterRow.label or nil)
                showTimersControl(R.masterRow and R.masterRow.control or nil)
                R.header:ClearAllPoints()
                R.header:SetWidth(math.max(1, G.contentRight - T.HEADER_LEFT))
                R.header:SetWordWrap(false)
                if R.header.SetMaxLines then
                    R.header:SetMaxLines(1)
                end
                R.header:SetText(getTimersText(R.header))
                local headerH = math.max(T.HEADER_FLOOR, math.ceil(tonumber((R.header.GetStringHeight and R.header:GetStringHeight()) or 0) or 0))
                R.header:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", T.HEADER_LEFT, y)
                y = y - headerH - T.HEADER_BOTTOM_GAP
                y = layoutSettingsSectionDescriptor(headerDescriptor, scrollChild, T.HEADER_LEFT, y, math.max(1, G.contentRight - T.HEADER_LEFT))

                R.masterRow:Layout(math.max(T.MODULE_TOGGLE_PREF_W, lockG.controlW))
                local masterLabelH = measureTimersSingleLineHeight(R.masterRow.label, lockG.labelW, T.LABEL_FLOOR, true)
                local masterToggleH = measureTimersControlHeight(R.masterRow.control, T.CONTROL_FLOOR, true)
                local masterRowH = math.max(T.LOCK_ROW_FLOOR or T.CONTROL_FLOOR, masterLabelH, masterToggleH)
                R.masterRowContainer:ClearAllPoints()
                R.masterRowContainer:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", lockG.labelX, y)
                R.masterRowContainer:SetSize(lockG.rowW, masterRowH)
                R.masterRow.label:ClearAllPoints()
                R.masterRow.label:SetPoint("LEFT", R.masterRowContainer, "LEFT", 0, 0)
                R.masterRow.control:ClearAllPoints()
                R.masterRow.control:SetPoint("LEFT", R.masterRow.label, "RIGHT", T.LABEL_TO_CONTROL_GAP, 0)
                y = y - masterRowH - T.SECTION_HEADER_BOTTOM_GAP
            end

            R.lockRow:Layout(lockG.controlW)
            R.resetAnchorButton:SetSize(lockG.resetW, lockG.resetH)
            local lockLabelH = measureTimersSingleLineHeight(R.lockRow.label, lockG.labelW, T.LABEL_FLOOR, true)
            local lockToggleH = measureTimersControlHeight(R.lockRow.control, T.CONTROL_FLOOR, true)
            local lockRowH = math.max(T.LOCK_ROW_FLOOR or T.CONTROL_FLOOR, lockLabelH, lockToggleH, lockG.resetH)
            local lockHelperGap, lockDividerGap, lockDividerOffset = computeTimersLockRhythm(lockLabelH)

            R.lockRowContainer:ClearAllPoints()
            R.lockRowContainer:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", lockG.labelX, y)
            R.lockRowContainer:SetSize(lockG.rowW, lockRowH)
            R.lockRow.label:ClearAllPoints()
            R.lockRow.label:SetPoint("LEFT", R.lockRowContainer, "LEFT", 0, 0)
            R.lockRow.control:ClearAllPoints()
            R.lockRow.control:SetPoint("LEFT", R.lockRow.label, "RIGHT", T.LABEL_TO_CONTROL_GAP, 0)
            R.resetAnchorButton:ClearAllPoints()
            R.resetAnchorButton:SetPoint("LEFT", R.lockRow.control, "RIGHT", T.LOCK_TOGGLE_TO_RESET_GAP, 0)
            y = y - lockRowH

            if isTimersTextActive(R.lockStateHelper) then
                local lockHelperH = measureTimersSingleLineHeight(R.lockStateHelper, G.contentSpan, T.HELPER_FLOOR, true)
                y = y - lockHelperGap
                R.lockStateHelper:ClearAllPoints()
                R.lockStateHelper:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", G.labelColX, y)
                y = y - lockHelperH
            else
                parkTimersControl(R.lockStateHelper)
            end

            local dividerY = y - lockDividerOffset
            R.divider:ClearAllPoints()
            R.divider:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", G.labelColX, dividerY)
            R.divider:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", -T.CONTENT_RIGHT, dividerY)
            R.divider:SetHeight(1)
            R.divider:SetColorTexture(0.24, 0.27, 0.32, 0.10)
            y = y - lockDividerGap

            local modulesHeaderH = measureTimersSingleLineHeight(R.modulesHeader, G.contentSpan, T.SECTION_HEADER_FLOOR, true)
            R.modulesHeader:ClearAllPoints()
            R.modulesHeader:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", G.labelColX, y)
            y = y - modulesHeaderH - T.SECTION_HEADER_BOTTOM_GAP

            R.batchEnableButton:SetSize(batchG.buttonW, T.BATCH_BUTTON_H)
            R.batchDisableButton:SetSize(batchG.buttonW, T.BATCH_BUTTON_H)
            batchStatusMaxWidth = batchG.statusW
            local batchTitleH = measureTimersSingleLineHeight(R.batchActionsTitle, batchG.titleW, T.SECTION_HEADER_FLOOR, true)
            local batchScopeActive = isTimersTextActive(R.batchActionsScope)
            local batchScopeH = measureTimersSingleLineHeight(R.batchActionsScope, math.max(1, batchG.scopeW), T.HELPER_FLOOR, batchScopeActive)
            local batchTitleRowH = math.max(batchTitleH, batchScopeH)
            local batchStatusActive = isTimersTextActive(R.batchStatus)
            local batchStatusH = measureTimersSingleLineHeight(R.batchStatus, batchG.statusW, T.HELPER_FLOOR, batchStatusActive)
            local batchStatusGap = batchStatusActive and T.CARD_BUTTONS_TO_STATUS_GAP or 0
            local batchCardH = T.CARD_PADDING + batchTitleRowH + T.CARD_LABEL_TO_BUTTONS_GAP + T.BATCH_BUTTON_H + batchStatusGap + batchStatusH + T.CARD_PADDING
            local batchButtonsTop = T.CARD_PADDING + batchTitleRowH + T.CARD_LABEL_TO_BUTTONS_GAP

            R.batchActionsCard:ClearAllPoints()
            R.batchActionsCard:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", batchG.cardX, y)
            R.batchActionsCard:SetSize(batchG.cardW, batchCardH)
            R.batchActionsTitle:ClearAllPoints()
            R.batchActionsTitle:SetPoint("TOPLEFT", R.batchActionsCard, "TOPLEFT", T.CARD_PADDING, -T.CARD_PADDING)
            if batchScopeActive then
                R.batchActionsScope:ClearAllPoints()
                R.batchActionsScope:SetPoint("TOPLEFT", R.batchActionsCard, "TOPLEFT", batchG.scopeX, -T.CARD_PADDING)
            else
                parkTimersControl(R.batchActionsScope)
            end
            R.batchEnableButton:ClearAllPoints()
            R.batchEnableButton:SetPoint("TOPLEFT", R.batchActionsCard, "TOPLEFT", batchG.enableX, -batchButtonsTop)
            R.batchDisableButton:ClearAllPoints()
            R.batchDisableButton:SetPoint("TOPLEFT", R.batchActionsCard, "TOPLEFT", batchG.disableX, -batchButtonsTop)
            if batchStatusActive then
                R.batchStatus:ClearAllPoints()
                R.batchStatus:SetPoint("TOPLEFT", R.batchActionsCard, "TOPLEFT", T.CARD_PADDING, -(batchButtonsTop + T.BATCH_BUTTON_H + batchStatusGap))
            else
                parkTimersControl(R.batchStatus)
            end
            y = y - batchCardH - T.MODULE_CARD_TO_ROWS_GAP

            local function layoutModuleRow(spec, topY)
                if not spec or not spec.row then
                    return topY
                end
                if type(spec.visibilityOverride) == "function" and not spec.visibilityOverride(layoutDb) then
                    parkTimersControl(spec.row.label)
                    parkTimersControl(spec.row.control)
                    if spec.unavailableHelper then
                        parkTimersControl(spec.unavailableHelper)
                    end
                    for _, fs in ipairs(spec.explainers or {}) do
                        parkTimersControl(fs)
                    end
                    return topY
                end

                spec.row:Layout(moduleG.controlW)
                local labelH = measureTimersSingleLineHeight(spec.row.label, moduleG.labelW, T.LABEL_FLOOR, true)
                local controlH = measureTimersControlHeight(spec.row.control, T.CONTROL_FLOOR, true)
                local rowH = math.max(T.ROW_FLOOR, labelH, controlH)
                local labelTop = math.floor((rowH - labelH) / 2)
                local controlTop = math.floor((rowH - controlH) / 2)

                spec.row.label:ClearAllPoints()
                spec.row.label:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", moduleG.labelX, topY - labelTop)
                spec.row.control:ClearAllPoints()
                spec.row.control:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", moduleG.controlX, topY - controlTop)

                local nextY = topY - rowH

                local function layoutExplainersBlock(startY, gapBefore)
                    local activeExplainers = {}
                    for _, fs in ipairs(spec.explainers or {}) do
                        if isTimersTextActive(fs) then
                            activeExplainers[#activeExplainers + 1] = fs
                        else
                            parkTimersControl(fs)
                        end
                    end
                    if #activeExplainers == 0 then
                        return startY
                    end

                    local nextExplainerY = startY - math.max(0, tonumber(gapBefore) or 0)
                    for idx, fs in ipairs(activeExplainers) do
                        local explainH = measureTimersWrappedTextHeight(fs, moduleG.helperW, T.HELPER_FLOOR, true)
                        fs:ClearAllPoints()
                        fs:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", moduleG.helperX, nextExplainerY)
                        nextExplainerY = nextExplainerY - explainH
                        if idx < #activeExplainers then
                            nextExplainerY = nextExplainerY - T.MODULE_EXPLAIN_LINE_GAP
                        end
                    end

                    return nextExplainerY - math.max(0, tonumber(spec.explainBlockGap) or T.MODULE_EXPLAIN_BLOCK_GAP)
                end

                if spec.explainBeforeChildren then
                    nextY = layoutExplainersBlock(nextY, T.MODULE_ROW_TO_EXPLAIN_GAP)
                end

                local childRowsLaidOut = 0
                for _, child in ipairs(spec.childRows or {}) do
                    local childVisible = child.row and (not child.isVisible or child.isVisible(layoutDb))
                    if childVisible then
                        child.row:Layout(moduleG.controlW)
                        local childLabelW = math.max(1, moduleG.labelW - T.MODULE_CHILD_INDENT)
                        local childLabelH = measureTimersSingleLineHeight(child.row.label, childLabelW, T.LABEL_FLOOR, true)
                        local childControlH = measureTimersControlHeight(child.row.control, T.CONTROL_FLOOR, true)
                        local childRowH = math.max(T.ROW_FLOOR, childLabelH, childControlH)
                        local childLabelTop = math.floor((childRowH - childLabelH) / 2)
                        local childControlTop = math.floor((childRowH - childControlH) / 2)

                        nextY = nextY - T.MODULE_CHILD_ROW_GAP
                        child.row.label:ClearAllPoints()
                        child.row.label:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", moduleG.labelX + T.MODULE_CHILD_INDENT, nextY - childLabelTop)
                        child.row.control:ClearAllPoints()
                        child.row.control:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", moduleG.controlX, nextY - childControlTop)
                        nextY = nextY - childRowH
                        childRowsLaidOut = childRowsLaidOut + 1
                    elseif child.row then
                        parkTimersControl(child.row.label)
                        parkTimersControl(child.row.control)
                    end
                end
                if childRowsLaidOut > 0 then
                    nextY = nextY - T.MODULE_CHILD_ROW_GAP
                end
                local helperActive = isTimersTextActive(spec.unavailableHelper)
                if helperActive then
                    local helperH = measureTimersSingleLineHeight(spec.unavailableHelper, moduleG.helperW, T.HELPER_FLOOR, true)
                    nextY = nextY - T.MODULE_ROW_TO_HELPER_GAP
                    spec.unavailableHelper:ClearAllPoints()
                    spec.unavailableHelper:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", moduleG.helperX, nextY)
                    nextY = nextY - helperH
                elseif spec.unavailableHelper then
                    parkTimersControl(spec.unavailableHelper)
                end

                if not spec.explainBeforeChildren then
                    nextY = layoutExplainersBlock(
                        nextY,
                        helperActive and T.MODULE_HELPER_TO_EXPLAIN_GAP or T.MODULE_ROW_TO_EXPLAIN_GAP
                    )
                end
                return nextY
            end

            for _, module in ipairs(moduleRows) do
                y = layoutModuleRow(module, y)
            end

            R.dividerFont:ClearAllPoints()
            R.dividerFont:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", G.labelColX, y)
            R.dividerFont:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", -T.CONTENT_RIGHT, y)
            R.dividerFont:SetHeight(1)
            R.dividerFont:SetColorTexture(0.24, 0.27, 0.32, 0.58)
            y = y - T.SECTION_TO_NEXT_DIVIDER_GAP

            local appearanceHeaderH = measureTimersSingleLineHeight(R.appearanceLayoutHeader, G.contentSpan, T.SECTION_HEADER_FLOOR, true)
            R.appearanceLayoutHeader:ClearAllPoints()
            R.appearanceLayoutHeader:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", G.labelColX, y)
            y = y - appearanceHeaderH - T.SECTION_HEADER_BOTTOM_GAP

            setPickerWidth(R.fontDropdown, appearanceG.fontPickerW)
            setPickerWidth(R.aspectDropdown, appearanceG.mainControlW)
            if R.iconEdgeControl and R.iconEdgeControl.Layout then
                R.iconEdgeControl:Layout(appearanceG.mainControlW)
            end
            if R.sizeStepper and R.sizeStepper.SetWidth then
                R.sizeStepper:SetWidth(appearanceG.fontSizeW)
            elseif R.sizeEdit then
                R.sizeEdit:SetSize(appearanceG.fontSizeW, T.CONTROL_FLOOR)
            end
            R.iconSizeSlider:SetWidth(appearanceG.mainControlW)
            R.iconOffsetXSlider:SetWidth(appearanceG.offsetSliderW)
            R.iconOffsetYSlider:SetWidth(appearanceG.offsetSliderW)
            R.iconOffsetAxisXMini:SetWidth(appearanceG.axisLabelW)
            R.iconOffsetAxisYMini:SetWidth(appearanceG.axisLabelW)
            R.coords:SetSize(appearanceG.mainControlW, T.CONTROL_FLOOR)

            local fontLabelH = measureTimersSingleLineHeight(R.fontLabel, appearanceG.labelW, T.LABEL_FLOOR, true)
            local fontPickerH = measureTimersControlHeight(R.fontDropdown.control, T.CONTROL_FLOOR, true)
            local fontSizeControl = (R.sizeStepper and R.sizeStepper.control) or R.sizeEdit
            local fontSizeH = measureTimersControlHeight(fontSizeControl, T.CONTROL_FLOOR, fontSizeControl ~= nil)
            local fontStackH = appearanceG.fontDropSizeBelow and (fontPickerH + T.FONT_STACK_GAP + fontSizeH) or math.max(fontPickerH, fontSizeH)
            local fontRowH = math.max(T.ROW_FLOOR, fontLabelH, fontStackH)
            local fontLabelTop = math.floor((fontRowH - fontLabelH) / 2)
            local fontStackTop = appearanceG.fontDropSizeBelow and math.floor((fontRowH - fontStackH) / 2) or 0
            local fontControlTop = appearanceG.fontDropSizeBelow and fontStackTop or math.floor((fontRowH - fontPickerH) / 2)
            local fontSizeTop = appearanceG.fontDropSizeBelow and (fontStackTop + fontPickerH + T.FONT_STACK_GAP) or math.floor((fontRowH - fontSizeH) / 2)

            R.fontRowContainer:ClearAllPoints()
            R.fontRowContainer:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", appearanceG.labelX, y)
            R.fontRowContainer:SetSize(appearanceG.rowContainerW, fontRowH)
            R.fontLabel:ClearAllPoints()
            R.fontLabel:SetPoint("TOPLEFT", R.fontRowContainer, "TOPLEFT", 0, -fontLabelTop)
            R.fontDropdown.control:ClearAllPoints()
            R.fontDropdown.control:SetPoint("TOPLEFT", R.fontRowContainer, "TOPLEFT", appearanceG.controlX - appearanceG.labelX, -fontControlTop)
            if fontSizeControl then
                fontSizeControl:ClearAllPoints()
                fontSizeControl:SetPoint("TOPLEFT", R.fontRowContainer, "TOPLEFT", appearanceG.fontSizeX - appearanceG.labelX, -fontSizeTop)
            end
            y = y - fontRowH

            local coordsLabelH = measureTimersSingleLineHeight(R.coordsLabel, appearanceG.labelW, T.LABEL_FLOOR, true)
            local coordsH = measureTimersControlHeight(R.coords, T.CONTROL_FLOOR, true)
            local coordsRowH = math.max(T.ROW_FLOOR, coordsLabelH, coordsH)
            local coordsLabelTop = math.floor((coordsRowH - coordsLabelH) / 2)
            local coordsTop = math.floor((coordsRowH - coordsH) / 2)
            R.coordsLabel:ClearAllPoints()
            R.coordsLabel:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", appearanceG.labelX, y - coordsLabelTop)
            R.coords:ClearAllPoints()
            R.coords:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", appearanceG.controlX, y - coordsTop)
            y = y - coordsRowH

            local iconSizeLabelH = measureTimersSingleLineHeight(R.iconSizeSlider.label, appearanceG.labelW, T.LABEL_FLOOR, true)
            local iconSizeControlH = measureTimersControlHeight(R.iconSizeSlider.control, T.CONTROL_FLOOR, true)
            local iconSizeRowH = math.max(T.ROW_FLOOR, iconSizeLabelH, iconSizeControlH)
            local iconSizeLabelTop = math.floor((iconSizeRowH - iconSizeLabelH) / 2)
            local iconSizeControlTop = math.floor((iconSizeRowH - iconSizeControlH) / 2)
            R.iconSizeSlider.label:ClearAllPoints()
            R.iconSizeSlider.label:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", appearanceG.labelX, y - iconSizeLabelTop)
            R.iconSizeSlider.control:ClearAllPoints()
            R.iconSizeSlider.control:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", appearanceG.controlX, y - iconSizeControlTop)
            y = y - iconSizeRowH

            parkTimersControl(R.iconOffsetXSlider.label)
            parkTimersControl(R.iconOffsetYSlider.label)
            local offsetPairLabelH = measureTimersSingleLineHeight(R.iconOffsetPairLabel, appearanceG.labelW, T.LABEL_FLOOR, true)
            local axisXH = measureTimersSingleLineHeight(R.iconOffsetAxisXMini, appearanceG.axisLabelW, T.MINI_LABEL_FLOOR, true)
            local axisYH = measureTimersSingleLineHeight(R.iconOffsetAxisYMini, appearanceG.axisLabelW, T.MINI_LABEL_FLOOR, true)
            local offsetXControlH = measureTimersControlHeight(R.iconOffsetXSlider.control, T.CONTROL_FLOOR, true)
            local offsetYControlH = measureTimersControlHeight(R.iconOffsetYSlider.control, T.CONTROL_FLOOR, true)
            local offsetLine1H = math.max(axisXH, offsetXControlH)
            local offsetLine2H = math.max(axisYH, offsetYControlH)
            local offsetStackH = offsetLine1H + T.AXIS_LINE_GAP + offsetLine2H
            local offsetRowH = math.max(T.OFFSET_ROW_FLOOR, offsetPairLabelH, offsetStackH)
            local offsetLabelTop = math.floor((offsetRowH - offsetPairLabelH) / 2)
            local offsetStackTop = math.floor((offsetRowH - offsetStackH) / 2)
            local offsetLine2Top = offsetStackTop + offsetLine1H + T.AXIS_LINE_GAP

            R.iconOffsetRowContainer:ClearAllPoints()
            R.iconOffsetRowContainer:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", appearanceG.labelX, y)
            R.iconOffsetRowContainer:SetSize(appearanceG.rowContainerW, offsetRowH)
            R.iconOffsetPairLabel:ClearAllPoints()
            R.iconOffsetPairLabel:SetPoint("TOPLEFT", R.iconOffsetRowContainer, "TOPLEFT", 0, -offsetLabelTop)
            R.iconOffsetAxisXMini:ClearAllPoints()
            R.iconOffsetAxisXMini:SetPoint("TOPLEFT", R.iconOffsetRowContainer, "TOPLEFT", appearanceG.controlX - appearanceG.labelX, -(offsetStackTop + math.floor((offsetLine1H - axisXH) / 2)))
            R.iconOffsetXSlider.control:ClearAllPoints()
            R.iconOffsetXSlider.control:SetPoint("TOPLEFT", R.iconOffsetRowContainer, "TOPLEFT", appearanceG.offsetSliderX - appearanceG.labelX, -(offsetStackTop + math.floor((offsetLine1H - offsetXControlH) / 2)))
            R.iconOffsetAxisYMini:ClearAllPoints()
            R.iconOffsetAxisYMini:SetPoint("TOPLEFT", R.iconOffsetRowContainer, "TOPLEFT", appearanceG.controlX - appearanceG.labelX, -(offsetLine2Top + math.floor((offsetLine2H - axisYH) / 2)))
            R.iconOffsetYSlider.control:ClearAllPoints()
            R.iconOffsetYSlider.control:SetPoint("TOPLEFT", R.iconOffsetRowContainer, "TOPLEFT", appearanceG.offsetSliderX - appearanceG.labelX, -(offsetLine2Top + math.floor((offsetLine2H - offsetYControlH) / 2)))
            y = y - offsetRowH

            local iconEdgeLabelH = measureTimersSingleLineHeight(R.iconEdgeLabel, appearanceG.labelW, T.LABEL_FLOOR, true)
            local iconEdgeControlH = measureTimersControlHeight(R.iconEdgeControl and R.iconEdgeControl.control or nil, T.CONTROL_FLOOR, R.iconEdgeControl and R.iconEdgeControl.control ~= nil)
            local iconEdgeRowH = math.max(T.ROW_FLOOR, iconEdgeLabelH, iconEdgeControlH)
            local iconEdgeLabelTop = math.floor((iconEdgeRowH - iconEdgeLabelH) / 2)
            local iconEdgeControlTop = math.floor((iconEdgeRowH - iconEdgeControlH) / 2)
            R.iconEdgeLabel:ClearAllPoints()
            R.iconEdgeLabel:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", appearanceG.labelX, y - iconEdgeLabelTop)
            if R.iconEdgeControl and R.iconEdgeControl.control then
                R.iconEdgeControl.control:ClearAllPoints()
                R.iconEdgeControl.control:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", appearanceG.controlX, y - iconEdgeControlTop)
            end
            y = y - iconEdgeRowH

            local aspectLabelH = measureTimersSingleLineHeight(R.aspectLabel, appearanceG.labelW, T.LABEL_FLOOR, true)
            local aspectControlH = measureTimersControlHeight(R.aspectDropdown.control, T.CONTROL_FLOOR, true)
            local aspectRowH = math.max(T.ROW_FLOOR, aspectLabelH, aspectControlH)
            local aspectLabelTop = math.floor((aspectRowH - aspectLabelH) / 2)
            local aspectControlTop = math.floor((aspectRowH - aspectControlH) / 2)
            R.aspectLabel:ClearAllPoints()
            R.aspectLabel:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", appearanceG.labelX, y - aspectLabelTop)
            R.aspectDropdown.control:ClearAllPoints()
            R.aspectDropdown.control:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", appearanceG.controlX, y - aspectControlTop)
            y = y - aspectRowH

            local extentObjects = {
                R.header,
                R.masterRowContainer,
                R.lockRowContainer,
                R.lockStateHelper,
                R.divider,
                R.modulesHeader,
                R.batchActionsCard,
                R.dividerFont,
                R.appearanceLayoutHeader,
                R.fontRowContainer,
                R.coordsLabel,
                R.coords,
                R.iconSizeSlider and R.iconSizeSlider.label or nil,
                R.iconSizeSlider and R.iconSizeSlider.control or nil,
                R.iconOffsetRowContainer,
                R.iconEdgeLabel,
                R.iconEdgeControl and R.iconEdgeControl.control or nil,
                R.aspectLabel,
                R.aspectDropdown and R.aspectDropdown.control or nil,
            }
            for _, module in ipairs(moduleRows) do
                if module and module.row then
                    extentObjects[#extentObjects + 1] = module.row.label
                    extentObjects[#extentObjects + 1] = module.row.control
                end
                for _, child in ipairs(module and module.childRows or {}) do
                    if child and child.row then
                        extentObjects[#extentObjects + 1] = child.row.label
                        extentObjects[#extentObjects + 1] = child.row.control
                    end
                end
                if module and module.unavailableHelper then
                    extentObjects[#extentObjects + 1] = module.unavailableHelper
                end
                for _, fs in ipairs(module and module.explainers or {}) do
                    extentObjects[#extentObjects + 1] = fs
                end
            end
            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

        self._layout = layout

        local registeredHooksApi = nil
        local function ensureOptionsHooksRegistered()
            local hooksApi = getModulesApi()
            if not hooksApi or not hooksApi.RegisterOptionsHooks then
                return
            end
            if registeredHooksApi == hooksApi then
                return
            end

            hooksApi:RegisterOptionsHooks({
                onTimersPositionChanged = function(x, y)
                    if coords and not coords:HasFocus() then
                        setTimersCoordsDisplay(coords, x, y)
                    end
                end,
            })
            registeredHooksApi = hooksApi
        end

        ensureOptionsHooksRegistered()
        self._ensureOptionsHooksRegistered = ensureOptionsHooksRegistered

        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
        end)
        self._settingsFirstOpenKey = "timers"
        self._settingsFirstOpenSizeTarget = scrollFrame
        self._settingsFirstOpenRender = function(panel)
            if panel._ensureOptionsHooksRegistered then
                panel._ensureOptionsHooksRegistered()
            end
            if panel._layout then
                panel._layout()
            end
            if panel._refresh then
                panel._refresh()
            end
        end
        self._settingsFirstOpenIsReady = function(panel)
            local sizeControl = (sizeStepper and sizeStepper.control) or sizeEdit
            local useHostedChrome = isPanelUsingCustomSettingsWindowHost(panel)
            return panelHasSizedViewport(scrollFrame)
                and header ~= nil
                and (useHostedChrome or header:IsShown())
                and lockRowContainer ~= nil and lockRowContainer:IsShown()
                and batchActionsCard ~= nil and batchActionsCard:IsShown()
                and sizeControl ~= nil and sizeControl:IsShown()
                and coords ~= nil and coords:IsShown()
                and (tonumber(scrollChild:GetHeight()) or 0) >= math.max(tonumber(scrollFrame:GetHeight()) or 0, 1)
        end
        settingsSearchRuntime.endDescriptorCapture(self)
        refresh()
    end

    panel:SetScript("OnShow", function(self)
        ensureSettingsCanvasPanelAnchored(self)
        self:EnsureBuilt()
        if not handleSettingsFirstOpenPanelShow(self) then
            withPanelHydration(self, function()
                if self._ensureOptionsHooksRegistered then
                    self._ensureOptionsHooksRegistered()
                end
                if self._layout then self._layout() end
                if self._refresh then self._refresh() end
            end)
        end
        self._timersLiveSyncElapsed = 0
    end)
    panel:HookScript("OnHide", function(self)
        handleSettingsFirstOpenPanelHide(self)
    end)
    panel:HookScript("OnUpdate", function(self, elapsed)
        if not self:IsShown() then return end
        self._timersLiveSyncElapsed = (self._timersLiveSyncElapsed or 0) + (elapsed or 0)
        if self._timersLiveSyncElapsed < 0.15 then
            return
        end
        self._timersLiveSyncElapsed = 0
        if self._syncTimersLiveFields then
            self._syncTimersLiveFields()
        end
    end)

    return panel
end
OptionsInternal.createTimersPanel = createTimersPanel
