---@diagnostic disable: undefined-field, undefined-global
local _, namespace = ...

-- Private and namespace are the SAME shared table (select(2, ...) everywhere else in the
-- addon). The feast descriptors below call into the Private:*FeastEditMode* surface, so this
-- file needs the alias bound explicitly — without it `Private` is a nil global and the
-- descriptors throw the moment BuildWidgets calls their get()/onClick.
local Private = namespace

local Theme = namespace.Theme

-- Approximate line height of the WildThingsFont13 title label EditBox/Dropdown
-- render above themselves (Widgets/EditBox.lua:65-69, Widgets/Dropdown.lua:23-28).
-- Neither widget exposes that label's own frame height, so this is a fixed
-- reserved-space constant rather than a measured value -- a cosmetic follow-up
-- if the real font metrics ever drift from this estimate.
local TITLE_H = 16

-- Own LibStub handle for the minimap-icon effect (WildThings.lua has its own separate
-- `LDBIcon` local — this file needs its own since there is no shared import mechanism).
local LDBIcon = LibStub("LibDBIcon-1.0")

-- READY-05: help copy shown above the per-spec candidate toggles, explaining the
-- generator's 30–120s window and why >=120s cooldowns are never listed (the game
-- resets them on a raid wipe, so they never need a ready-check reminder).
local COOLDOWN_HELP_TEXT = "Only spells with a 30–120s cooldown are listed. Cooldowns of "
    .. "2 minutes or longer are reset by the game when the raid wipes on a boss, so they "
    .. "never need a ready-check reminder. Every spell below is off until you enable it "
    .. "— pick the ones you want the ready-check to watch for each of your specs."

-- READY-05: appends a Cooldowns header + help paragraph only. The per-spec candidate
-- picker (dropdown adder + wrapping pill flow) is built separately by
-- CreateReadyCheckScreen via namespace:CreateSpecCooldownPicker, one per player spec.
local function AppendCooldownDescriptors(descriptors)
    table.insert(descriptors, {type = "header", label = "Cooldowns"})
    table.insert(descriptors, {type = "text", label = COOLDOWN_HELP_TEXT})
end

-- namespace:CreateSettingsScreen(contentHost)
-- Settings screen (SET-01): a scrolling body listing General + Guild Bank toggles (via the
-- declarative namespace:BuildWidgets adapter, WTUI-04). The Ready-check toggles + per-spec
-- cooldown pickers moved to their own screen (namespace:CreateReadyCheckScreen below).
-- Each toggle descriptor's get() reads its live SavedVariable once at build time
-- (BuildWidgets' toggle adapter uses SetChecked(..., true) to suppress the initial
-- OnValueChanged) and, on change, set writes it back AND applies its effect.
function namespace:CreateSettingsScreen(contentHost)
    local root = CreateFrame("Frame", nil, contentHost)
    root:SetPoint("TOPLEFT", contentHost, "TOPLEFT", 0, 0)
    root:SetPoint("TOPRIGHT", contentHost, "TOPRIGHT", 0, 0)
    root:SetPoint("BOTTOMRIGHT", contentHost, "BOTTOMRIGHT", 0, 0)

    -- Phase 50 (FEAST-F2): defensive feast-structure accessors. These work
    -- across the old Phase-41 flat format ({enabled=bool, duration=number}),
    -- the post-migration nested format, and a fresh-install DEFAULTS table.
    local function _FeastGetDuration(typeKey)
        local sv = WildThingsSavedVars.feast
        if type(sv) ~= "table" then return 15 end
        if type(sv.duration) == "table" then
            return sv.duration[typeKey] or sv.defaultDuration or 15
        end
        if typeKey == "feast" and type(sv.duration) == "number" then
            return sv.duration
        end
        return sv.defaultDuration or 15
    end

    local function _FeastSetDuration(typeKey, v)
        local sv = WildThingsSavedVars.feast
        if type(sv) ~= "table" then return end
        if type(sv.duration) ~= "table" then
            sv.duration = { feast = nil, cauldron = nil, soulwell = nil, repair = nil }
        end
        local default = sv.defaultDuration or 15
        if v == default then
            sv.duration[typeKey] = nil
        else
            sv.duration[typeKey] = v
        end
    end

    local function _FeastIsEnabled(typeKey)
        local sv = WildThingsSavedVars.feast
        if type(sv) ~= "table" then return true end
        if type(sv.enabled) == "table" then
            return sv.enabled[typeKey]
        end
        if typeKey == "feast" and type(sv.enabled) == "boolean" then
            return sv.enabled
        end
        return true
    end

    local function _FeastSetEnabled(typeKey, checked)
        local sv = WildThingsSavedVars.feast
        if type(sv) ~= "table" then return end
        if type(sv.enabled) ~= "table" then
            sv.enabled = { feast = true, cauldron = true, soulwell = true, repair = true }
        end
        sv.enabled[typeKey] = checked
        -- Live-refresh the edit-mode preview when the user toggles a type
        -- while the anchor is unlocked.
        if Private.RefreshFeastEditPreview then
            Private:RefreshFeastEditPreview()
        end
    end

    local function _FeastLayout()
        local sv = WildThingsSavedVars.feast
        if type(sv) ~= "table" then return "horizontal" end
        return sv.layout or "horizontal"
    end

    local function _FeastSetLayout(v)
        local sv = WildThingsSavedVars.feast
        if type(sv) ~= "table" then return end
        sv.layout = v
        -- Live-refresh the edit-mode preview so the layout change is
        -- immediately visible while the anchor is unlocked.
        if Private.RefreshFeastEditPreview then
            Private:RefreshFeastEditPreview()
        end
    end

    local function _FeastDefaultDuration()
        local sv = WildThingsSavedVars.feast
        if type(sv) ~= "table" then return 15 end
        return sv.defaultDuration or 15
    end

    local function _FeastSetDefaultDuration(v)
        local sv = WildThingsSavedVars.feast
        if type(sv) ~= "table" then return end
        sv.defaultDuration = v
    end

    local descriptors = {
        {type = "header", label = "General"},

        -- 1. Show minimap icon — INVERTED sense: toggle-on means the icon is SHOWN, so it
        -- reads/writes the negation of WildThingsSavedVars.minimap.hide (pattern confirmed
        -- against WildThings.lua's ChatCommand "icon show|hide" branch).
        {
            type = "toggle",
            label = "Show minimap icon",
            get = function() return not WildThingsSavedVars.minimap.hide end,
            set = function(checked)
                WildThingsSavedVars.minimap.hide = not checked
                if checked then
                    LDBIcon:Show("WildThings")
                else
                    LDBIcon:Hide("WildThings")
                end
            end,
        },

        -- 2. Party mode.
        {
            type = "bound-toggle",
            path = "update.party_mode",
            label = "Party mode",
        },

        -- 3. Memes / ads.
        {
            type = "toggle",
            label = "Memes / ads",
            get = function() return WildThingsSavedVars.update.memes end,
            set = function(checked) WildThingsSavedVars.update.memes = checked end,
        },

        {type = "header", label = "Dashboard"},

        -- 4. 3D boss model — opt-OUT (default ON). Off hides the bottom section's left
        -- panel entirely and lets the four role columns take the full width; the repaint
        -- is what makes it visible without reopening the screen.
        {
            type = "toggle",
            label = "Show 3D boss model",
            get = function() return WildThingsSavedVars.timeline.show_boss_model ~= false end,
            set = function(checked)
                WildThingsSavedVars.timeline.show_boss_model = checked
                if namespace.RefreshTimelineScreen then
                    namespace:RefreshTimelineScreen()
                end
            end,
        },

        {type = "header", label = "Guild Bank", bankSection = true},

        -- 5. Spill organize overflow to bags (OVERFLOW-02 opt-in, default OFF --
        -- default is leftmost-column consolidation; ON withdraws surplus into the
        -- presser's character bags instead).
        {
            type = "toggle",
            label = "Spill organize overflow to bags",
            get = function() return WildThingsSavedVars.bankorganize.spill_to_bags end,
            set = function(checked) WildThingsSavedVars.bankorganize.spill_to_bags = checked end,
            bankSection = true,
        },

        {type = "header", label = "Reminders"},

        -- Default auto-hide duration in seconds (5-60). Used as the fallback
        -- for any type whose per-type override is nil. The display path reads
        -- the value live at show time, so a change applies to the next
        -- reminder with no reload.
        {
            type = "slider",
            label = "Default display duration (s)",
            min = 5,
            max = 60,
            get = _FeastDefaultDuration,
            set = _FeastSetDefaultDuration,
        },

        -- Icon layout direction. When ON, icons stack vertically; when OFF,
        -- they arrange side-by-side (horizontal). Default horizontal.
        {
            type = "toggle",
            label = "Stack reminder icons vertically",
            get = function() return _FeastLayout() == "vertical" end,
            set = function(checked)
                _FeastSetLayout(checked and "vertical" or "horizontal")
            end,
        },

        -- Unlock reminder position (FEAST-03 edit mode). This toggle is
        -- deliberately the only way in and out of edit mode: the toggle's
        -- get() runs once at build time and this screen discards the refresh
        -- handle, so any second exit path would leave the checkbox showing a
        -- state that is not true.
        {
            type = "toggle",
            label = "Unlock reminder position",
            get = function()
                if Private.IsFeastEditMode then return Private:IsFeastEditMode() end
                return false
            end,
            set = function(checked)
                if checked then
                    if Private.EnterFeastEditMode then Private:EnterFeastEditMode() end
                else
                    if Private.ExitFeastEditMode then Private:ExitFeastEditMode() end
                end
            end,
        },

        -- Reset reminder position to default (FEAST-03 reset).
        {
            type = "button",
            label = "Reset reminder position",
            onClick = function()
                if Private.ResetFeastAnchorPosition then Private:ResetFeastAnchorPosition() end
            end,
        },

        {type = "header", label = "Reminder Types"},

        -- Per-type enable toggle (each type has its own independent on/off).
        {
            type = "toggle",
            label = "Show feast reminder",
            get = function() return _FeastIsEnabled("feast") end,
            set = function(checked) _FeastSetEnabled("feast", checked) end,
        },
        {
            type = "slider",
            label = "Feast duration (s)",
            min = 5,
            max = 60,
            get = function() return _FeastGetDuration("feast") end,
            set = function(v) _FeastSetDuration("feast", v) end,
        },

        {
            type = "toggle",
            label = "Show cauldron reminder",
            get = function() return _FeastIsEnabled("cauldron") end,
            set = function(checked) _FeastSetEnabled("cauldron", checked) end,
        },
        {
            type = "slider",
            label = "Cauldron duration (s)",
            min = 5,
            max = 60,
            get = function() return _FeastGetDuration("cauldron") end,
            set = function(v) _FeastSetDuration("cauldron", v) end,
        },

        {
            type = "toggle",
            label = "Show soulwell reminder",
            get = function() return _FeastIsEnabled("soulwell") end,
            set = function(checked) _FeastSetEnabled("soulwell", checked) end,
        },
        {
            type = "slider",
            label = "Soulwell duration (s)",
            min = 5,
            max = 60,
            get = function() return _FeastGetDuration("soulwell") end,
            set = function(v) _FeastSetDuration("soulwell", v) end,
        },

        {
            type = "toggle",
            label = "Show repair reminder",
            get = function() return _FeastIsEnabled("repair") end,
            set = function(checked) _FeastSetEnabled("repair", checked) end,
        },
        {
            type = "slider",
            label = "Repair duration (s)",
            min = 5,
            max = 60,
            get = function() return _FeastGetDuration("repair") end,
            set = function(v) _FeastSetDuration("repair", v) end,
        },

        {type = "header", label = "Break Reminder"},

        -- 10. Show break reminder (BREAK-01 master toggle, default ON).
        {
            type = "toggle",
            label = "Show break reminder",
            get = function() return WildThingsSavedVars.breakreminder.enabled end,
            set = function(checked) WildThingsSavedVars.breakreminder.enabled = checked end,
        },

        -- 11. Unlock reminder position (BREAK-03 edit mode). This toggle is
        -- deliberately the only way in and out of edit mode — the toggle's
        -- get() runs once at build time and this screen discards the refresh
        -- handle, so any second exit path would leave the checkbox stale.
        {
            type = "toggle",
            label = "Unlock reminder position",
            get = function()
                if Private.IsBreakEditMode then return Private:IsBreakEditMode() end
                return false
            end,
            set = function(checked)
                if checked then
                    if Private.EnterBreakEditMode then Private:EnterBreakEditMode() end
                else
                    if Private.ExitBreakEditMode then Private:ExitBreakEditMode() end
                end
            end,
        },

        -- 12. Reset reminder position to default (BREAK-03 reset).
        {
            type = "button",
            label = "Reset reminder position",
            onClick = function()
                if Private.ResetBreakAnchorPosition then Private:ResetBreakAnchorPosition() end
            end,
        },

        {type = "header", label = "Resurrection"},

        -- 13. Auto-accept resurrection (AUTORES-04-Master-Toggle, standardmäßig AUS).
        {
            type = "toggle",
            label = "Auto-accept resurrection",
            get = function() return WildThingsSavedVars.autoresaccept.enabled end,
            set = function(checked) WildThingsSavedVars.autoresaccept.enabled = checked end,
        },

        -- Die beiden Ausschlusszeilen bleiben auch bei deaktiviertem Master flach
        -- und bearbeitbar. Der Lebenszyklus des Controls wertet den Zeilenstatus
        -- beim Aufbau des Screens aus, nicht erneut bei Änderung einer anderen Zeile;
        -- ein Live-Greyout wäre daher irreführend.
        -- 14. Exclude combat resurrection (AUTORES-04, standardmäßig AN).
        {
            type = "toggle",
            label = "Exclude combat resurrection",
            get = function() return WildThingsSavedVars.autoresaccept.excludeCombat end,
            set = function(checked) WildThingsSavedVars.autoresaccept.excludeCombat = checked end,
        },
        -- 15. Exclude afterlife resurrection (AUTORES-04, standardmäßig AN).
        {
            type = "toggle",
            label = "Exclude afterlife resurrection",
            get = function() return WildThingsSavedVars.autoresaccept.excludeAfterlife end,
            set = function(checked) WildThingsSavedVars.autoresaccept.excludeAfterlife = checked end,
        },

        {type = "header", label = "Raid inspect"},

        -- 13. Crest threshold (INSPECT-10, default 5, range 0-100).
        -- Players with fewer Myth Dawncrests than this value are flagged
        -- with WARN color and sorted to the top of their column.
        {
            type = "slider",
            label = "Crest threshold",
            min = 0,
            max = 100,
            get = function() return WildThingsSavedVars.inspect.crestThreshold end,
            set = function(v) WildThingsSavedVars.inspect.crestThreshold = v end,
        },

        -- 11. Combat potion threshold (INSPECT-F1, default 2, range 0-20).
        -- Players with fewer combat/throughput potions than this value are flagged.
        {
            type = "slider",
            label = "Combat potion threshold",
            min = 0,
            max = 20,
            get = function() return WildThingsSavedVars.inspect.combatPotionThreshold end,
            set = function(v) WildThingsSavedVars.inspect.combatPotionThreshold = v end,
        },

        -- 12. Flask threshold (INSPECT-10, default 1, range 0-1).
        -- Players with fewer fleeting flasks than this value are flagged.
        {
            type = "slider",
            label = "Flask threshold",
            min = 0,
            max = 1,
            get = function() return WildThingsSavedVars.inspect.flaskThreshold end,
            set = function(v) WildThingsSavedVars.inspect.flaskThreshold = v end,
        },

        {type = "header", label = "Summoning"},

        -- 13. Auto-open the Summoning Window after Invite & Arrange finishes
        -- (SUMMON-01, opt-out default ON). Never affects `/wt summons`, which
        -- always opens the window on demand regardless of this toggle. See
        -- .planning/v2.7-MILESTONE-AUDIT.md §1.
        {
            type = "toggle",
            label = "Auto-open Summoning Window after arrange",
            get = function() return WildThingsSavedVars.summons.auto_open end,
            set = function(checked) WildThingsSavedVars.summons.auto_open = checked end,
        },
    }

    -- GATE-06: fully omit (not shown-disabled) the Guild Bank header + spill_to_bags
    -- toggle for viewers lacking bank.view. BuildWidgets has no requires/capability
    -- concept, so filter the descriptors array once here, before it runs.
    if not Private:ViewerHasCapability("bank.view") then
        local filtered = {}
        for _, d in ipairs(descriptors) do
            if not d.bankSection then filtered[#filtered + 1] = d end
        end
        descriptors = filtered
    end

    -- Scroll the body for future-proofing (short today, but keeps the same idiom as the
    -- Ready Check screen). Content width must be bound BEFORE BuildWidgets runs so the
    -- `text` builder's wrap-height measurement resolves correctly.
    local scrollFrame = namespace:CreateScrollFrame(root)
    scrollFrame:SetPoint("TOPLEFT", root, "TOPLEFT", Theme.PAD_WINDOW, -Theme.PAD_WINDOW)
    scrollFrame:SetPoint("BOTTOMRIGHT", root, "BOTTOMRIGHT", -Theme.PAD_WINDOW, Theme.PAD_WINDOW)

    local viewportWidth = scrollFrame.scrollFrame:GetWidth()
    if not viewportWidth or viewportWidth < 1 then viewportWidth = 1 end
    scrollFrame.contentFrame:SetWidth(viewportWidth)

    local _, totalHeight = namespace:BuildWidgets(scrollFrame.contentFrame, descriptors)
    scrollFrame.contentFrame:SetHeight(Theme.PAD_WINDOW * 2 + totalHeight)
    scrollFrame:FullUpdate()

    return root
end

-- namespace:CreateReadyCheckScreen(contentHost)
-- Ready Check screen: the ready-check trigger-mode dropdown + the two
-- independent auto-decline / announce-chat toggles (READY-05) plus a
-- Cooldowns section with one per-spec spell picker
-- (namespace:CreateSpecCooldownPicker) per player spec. Split out of the
-- Settings screen so ready-check config gets its own nav entry.
--
-- Vision-Angleichung: the Phase-25 `cooldown.enabled` master toggle is
-- replaced by a three-way trigger-mode dropdown (`"always"` /
-- `"wt_only"` / `"disabled"`), and the bundled `auto_not_ready` toggle is
-- split into independent `auto_decline` + `announce_chat` toggles. The
-- dropdown cannot go through the generic `bound-dropdown` BuildWidgets
-- descriptor: BindWidget passes the raw SavedVars leaf value as
-- `initialValue` to CreateDropdown, but CreateDropdown:SetValue expects an
-- array of selection indices, not a string ("always"/"wt_only"/"disabled").
-- Use the proven InviteScreen.lua:124-136 / 266-289 pattern (free-standing
-- row + index-search + WR-02 suppress-initial-fire guard) instead.
--
-- The old `group_assignment` toggle + backend are removed entirely --
-- CheckGroupAssignment is gone from ReadyCheck.lua, no overlay either.
function namespace:CreateReadyCheckScreen(contentHost)
    local root = CreateFrame("Frame", nil, contentHost)
    root:SetPoint("TOPLEFT", contentHost, "TOPLEFT", 0, 0)
    root:SetPoint("TOPRIGHT", contentHost, "TOPRIGHT", 0, 0)
    root:SetPoint("BOTTOMRIGHT", contentHost, "BOTTOMRIGHT", 0, 0)

    local descriptors = {
        {type = "header", label = "Ready check"},

        -- 1. Auto-decline on cooldown (Vision-Angleichung: was bundled into
        -- `auto_not_ready` together with chat announce, now independent).
        {
            type = "toggle",
            label = "Auto-decline on cooldown",
            get = function() return WildThingsSavedVars.readycheck.cooldown.auto_decline end,
            set = function(checked) WildThingsSavedVars.readycheck.cooldown.auto_decline = checked end,
        },

        -- 2. Announce longest on-CD cooldown in raid chat (Vision-Angleichung:
        -- was bundled into `auto_not_ready` together with auto-decline, now
        -- independent). The chat message format ("Cooldown <Spell> ready in
        -- <N> seconds") is fixed by ReadyCheck.lua:PostCooldownChat.
        {
            type = "toggle",
            label = "Announce cooldown in raid chat",
            get = function() return WildThingsSavedVars.readycheck.cooldown.announce_chat end,
            set = function(checked) WildThingsSavedVars.readycheck.cooldown.announce_chat = checked end,
        },
    }

    -- 260726-r3x: rank-0-2 gated "Show cooldown window on ready check" toggle.
    -- Inserted AFTER the two existing toggles but BEFORE AppendCooldownDescriptors
    -- (so it sits visually above the per-spec cooldown pickers). Wrapped in
    -- ViewerHasCapability so non-eligible players never see the row at all
    -- (D-13's read-side gate) -- mirrors the bank.view filter pattern at the
    -- bottom of CreateSettingsScreen. The toggle controls ONLY whether the
    -- LOCAL popup auto-opens; broadcast eligibility is independent (GATE 1 in
    -- ReadyCheckCooldownComm.lua) per CONTEXT.md "Toggle steuert nur ob".
    if Private:ViewerHasCapability("readycheck.cooldownwindow") then
        table.insert(descriptors, {
            type = "toggle",
            label = "Show cooldown window on ready check",
            get = function() return WildThingsSavedVars.readycheck.cooldownwindow.auto_popup end,
            set = function(checked) WildThingsSavedVars.readycheck.cooldownwindow.auto_popup = checked end,
        })
    end

    AppendCooldownDescriptors(descriptors)

    -- Scroll the body: a class with 3-4 specs, each with its own picker, can run taller
    -- than the window. Content width must be bound BEFORE BuildWidgets runs so the `text`
    -- builder's wrap-height measurement (Pitfall 1) resolves correctly -- and before any
    -- picker:Layout() call below, for the same width-before-measure reason.
    local scrollFrame = namespace:CreateScrollFrame(root)
    scrollFrame:SetPoint("TOPLEFT", root, "TOPLEFT", Theme.PAD_WINDOW, -Theme.PAD_WINDOW)
    scrollFrame:SetPoint("BOTTOMRIGHT", root, "BOTTOMRIGHT", -Theme.PAD_WINDOW, Theme.PAD_WINDOW)

    -- Bind the content width to the INNER WowScrollBox viewport (scrollFrame.scrollFrame),
    -- NOT the outer container (scrollFrame): the viewport is inset by the scrollbar gutter
    -- (~30-40px, ScrollFrame.lua), and content is clipped to it -- measuring wrap against the
    -- outer width would push the rightmost pill on a wrapped row behind the scrollbar. `or 1`
    -- alone doesn't guard a not-yet-resolved frame (GetWidth() returns 0, and `0 or 1 == 0` in
    -- Lua, which would drive a negative label width downstream), so clamp to >= 1 explicitly.
    local viewportWidth = scrollFrame.scrollFrame:GetWidth()
    if not viewportWidth or viewportWidth < 1 then viewportWidth = 1 end
    scrollFrame.contentFrame:SetWidth(viewportWidth)

    local rows, totalHeight = namespace:BuildWidgets(scrollFrame.contentFrame, descriptors)

    -- Vision-Angleichung: trigger-mode dropdown. Anchor directly below the last
    -- BuildWidgets row (mirrors the InviteScreen.lua:266-289 dropdown-after-toggles
    -- layout). The totalHeight returned by BuildWidgets already includes the GAPs it
    -- added internally -- we add one more GAP so the dropdown doesn't sit flush
    -- against the last toggle, matching the inter-row visual rhythm above.
    local modeEntries = {
        {text = "Always",                 value = "always"},
        {text = "Wild Things raids only", value = "wt_only"},
        {text = "Disabled",               value = "disabled"},
    }
    local initialModeIndex = 1
    for i, entry in ipairs(modeEntries) do
        if entry.value == WildThingsSavedVars.readycheck.cooldown.trigger_mode then
            initialModeIndex = i
            break
        end
    end

    local modeRow = CreateFrame("Frame", nil, scrollFrame.contentFrame)
    local modeRowHeight = TITLE_H + Theme.H_DROPDOWN
    modeRow:SetPoint("TOPLEFT", scrollFrame.contentFrame, "TOPLEFT", 0, -(totalHeight + Theme.GAP))
    modeRow:SetPoint("TOPRIGHT", scrollFrame.contentFrame, "TOPRIGHT", 0, -(totalHeight + Theme.GAP))
    modeRow:SetHeight(modeRowHeight)

    -- WR-02 (InviteScreen.lua): CreateDropdown's constructor calls SetValue(initialValue)
    -- which fires OnValueChanged unconditionally, unlike Toggle:SetChecked(checked, true).
    -- Guard the live-write so opening the screen doesn't re-stamp the currently-saved
    -- trigger_mode. Flag flips true only after construction, so the initial SetValue
    -- already fired by then.
    local modeDropdownReady = false
    local modeDropdown = namespace:CreateDropdown(modeRow, "Cooldown check trigger",
        modeEntries, function(value)
            if not modeDropdownReady then return end
            WildThingsSavedVars.readycheck.cooldown.trigger_mode = value
        end, {initialModeIndex})
    modeDropdown:SetPoint("TOPLEFT", modeRow, "TOPLEFT", 0, -TITLE_H)
    modeDropdownReady = true

    -- Add the trigger-mode row's height (including its own trailing GAP) to totalHeight
    -- so RecomputeContentHeight below (which builds off totalHeight + pickersHeight)
    -- accounts for the new section. Same arithmetic style as InviteScreen.lua's
    -- `-y` accumulator.
    totalHeight = totalHeight + Theme.GAP + modeRowHeight

    -- READY-05: one CreateSpecCooldownPicker per player spec, replacing the old flat
    -- per-spec toggle wall. Candidates come from Private.CooldownCandidates (aliased here
    -- as namespace.CooldownCandidates -- the SAME shared addon table ReadyCheck.lua's
    -- file-load capture writes into; falls back to the raw generated global, then {}, so
    -- this still degrades gracefully if ReadyCheck.lua's capture line is ever missing).
    local candidates = namespace.CooldownCandidates or KEY_COOLDOWN_CANDIDATES or {}

    -- NOT Private.playerClassID (that field stores a localized class NAME string, a
    -- latent bug elsewhere -- select(3, UnitClass("player")) is the real numeric classID
    -- GetSpecializationInfoForClassID/GetNumSpecializationsForClassID require).
    local classID = select(3, UnitClass("player"))

    local pickers = {}

    local function RecomputeContentHeight()
        local pickersHeight = 0
        for _, picker in ipairs(pickers) do
            pickersHeight = pickersHeight + picker:GetHeight() + Theme.GAP
        end

        scrollFrame.contentFrame:SetHeight(Theme.PAD_WINDOW * 2 + totalHeight + pickersHeight)
        scrollFrame:FullUpdate()
    end

    -- Vision-Angleichung: the trigger-mode dropdown sits between the toggle block
    -- and the per-spec picker chain -- make it the chain's first anchor so the
    -- pickers line up exactly below it. modeRow is already positioned by absolute
    -- y-offset above (relative to the content frame, not the last BuildWidgets
    -- row), so re-using it as the chain anchor is purely a TOPLEFT/BOTTOMLEFT
    -- relative-point for the FIRST picker.
    local prevAnchor = modeRow

    for i = 1, GetNumSpecializationsForClassID(classID) do
        local specID, specName = GetSpecializationInfoForClassID(classID, i)

        local picker = namespace:CreateSpecCooldownPicker(
            scrollFrame.contentFrame, specID, specName, candidates[specID], RecomputeContentHeight)

        picker:SetPoint("TOPLEFT", prevAnchor, "BOTTOMLEFT", 0, -Theme.GAP)
        picker:SetPoint("TOPRIGHT", prevAnchor, "BOTTOMRIGHT", 0, -Theme.GAP)

        picker:Layout() -- width-before-measure: anchoring above bound picker's width

        pickers[#pickers + 1] = picker
        prevAnchor = picker
    end

    RecomputeContentHeight()

    -- Deferred re-layout (fixes: pills seeded from per_spec are invisible until the first
    -- add/remove after a fresh build/reload). Each picker's width is anchor-derived
    -- (TOPLEFT/TOPRIGHT above, no explicit SetWidth), so picker:GetWidth() reads 0 on this
    -- same-frame construction pass and picker:Layout() skips positioning the seeded pills
    -- (SpecCooldownPicker.lua's `availWidth > 0` guard). Re-run Layout once WoW has laid the
    -- frame out and the width has resolved. Same C_Timer.After(0, ...) deferred-sync idiom
    -- Slider.lua:175-187 / EditBox.lua:226 use for this exact anchor-derived-width gotcha.
    C_Timer.After(0, function()
        for _, picker in ipairs(pickers) do
            picker:Layout()
        end
        RecomputeContentHeight()
    end)

    return root
end
