-- ============================================================
-- FILE: ActionBars.lua
-- PURPOSE: Action bar layout engine. Reparents Blizzard buttons
--   into styled containers for bars 1-8, the pet bar, and the
--   stance bar. Manages drag-to-move via MoverRegistry, hotkey
--   display, range coloring, cast sweep, proc glow, and right-
--   click overlay settings popups.
--
-- DESIGN NOTES:
--   * Single source of truth for muted Blizzard frames lives in
--     the MutedFrames registry. Mute captures original state;
--     Unmute restores from it. Symmetric, idempotent.
--   * Single rebuild entry point: Rebuild(scope). Every external
--     trigger funnels through it.
--   * Combat-safe via a small local defer queue plus delegation
--     to MoverRegistry for position changes.
--   * Re-mute on Blizzard re-show events (Edit Mode, spec change,
--     vehicle, talent loadout) instead of polling.
--   * Micro-menu buttons are NOT touched here. InterfaceMenu.lua
--     owns those.
--
-- PUBLIC API:
--   _G.MyActionBars_ReloadSettings()          deferred rebuild
--   _G.MyActionBars_ReloadSettingsImmediate() sync rebuild
--   _G.MyActionBars_ResetToDefaults()         clear positions + rebuild
--   _G.MidnightUI_RefreshPetBarHotkeys()      refresh pet hotkey labels
--   _G.MyActionBarContainer1 ... 8            per-bar container frames
--   _G.MyPetBarContainer                      pet bar container
--   _G.MyStanceBarContainer                   stance bar container
--
-- DEPENDS ON:
--   MidnightUI_GetActionBarSettings   (Settings.lua)
--   MidnightUI_MoverRegistry          (MoverRegistry.lua)
--   MidnightUI_Core.GetClassColor     (Core.lua)
--   MidnightUI_CreateOverlayBuilder   (Core.lua)
--   MidnightUI_RegisterOverlaySettings(Core.lua)
--   MidnightUISettings.ActionBars / PetBar / StanceBar (SavedVars)
--   MidnightUISettings.Messenger.locked (Edit Mode flag, read-only)
--
-- IMPORTANT: do not capture MidnightUISettings into a file-scope
--   local. Bootstrap.lua swaps it to per-character storage on
--   ADDON_LOADED; capturing freezes a stale reference.
-- ============================================================

local ADDON_NAME = "MidnightUI"

local _G = _G
local CreateFrame = CreateFrame
local C_Timer = C_Timer
local InCombatLockdown = InCombatLockdown
local UnitClass = UnitClass
local UnitExists = UnitExists
local GetBindingKey = GetBindingKey
local GetBindingText = GetBindingText
local GetNumShapeshiftForms = GetNumShapeshiftForms
local GetShapeshiftForm = GetShapeshiftForm
local GetPetActionInfo = GetPetActionInfo
local PetHasActionBar = PetHasActionBar
local HasAction = HasAction
local IsActionInRange = IsActionInRange
local IsUsableAction = IsUsableAction
local GetActionInfo = GetActionInfo
local GetCursorInfo = GetCursorInfo
local GetTime = GetTime
local UnitCastingInfo = UnitCastingInfo
local UnitChannelInfo = UnitChannelInfo
local CheckInteractDistance = CheckInteractDistance
local CreateColor = CreateColor
local UIParent = UIParent
local hooksecurefunc = hooksecurefunc
local pairs = pairs
local ipairs = ipairs
local type = type
local pcall = pcall
local select = select
local math_floor = math.floor
local math_ceil = math.ceil
local math_min = math.min
local math_max = math.max
local math_abs = math.abs

-- ============================================================
-- SECTION A: Constants
-- ============================================================

-- Blizzard button name prefix for each MUI bar 1..8.
local BAR_BUTTON_MAP = {
    [1] = "ActionButton",
    [2] = "MultiBarBottomLeftButton",
    [3] = "MultiBarBottomRightButton",
    [4] = "MultiBarRightButton",
    [5] = "MultiBarLeftButton",
    [6] = "MultiBar5Button",
    [7] = "MultiBar6Button",
    [8] = "MultiBar7Button",
}

-- Blizzard binding command prefix for each MUI bar 1..8.
local BAR_BINDING_MAP = {
    [1] = "ACTIONBUTTON",
    [2] = "MULTIACTIONBAR1BUTTON",
    [3] = "MULTIACTIONBAR2BUTTON",
    [4] = "MULTIACTIONBAR3BUTTON",
    [5] = "MULTIACTIONBAR4BUTTON",
    [6] = "MULTIACTIONBAR5BUTTON",
    [7] = "MULTIACTIONBAR6BUTTON",
    [8] = "MULTIACTIONBAR7BUTTON",
}

-- Blizzard host frames for each MUI bar. These are what we mute
-- when the matching MUI bar is enabled and restore when disabled.
local BAR_HOST_FRAMES = {
    [1] = { "MainMenuBar", "MainMenuBarArtFrame", "ActionBar1", "MainActionBar",
            "MainActionBarArtFrame", "MainActionBarButtonContainer",
            "MainActionBarButtonContainer0", "MainActionBarButtonContainer1",
            "MainActionBarButtonContainer2", "MainActionBarButtonContainer3",
            "MainActionBarButtonContainer4", "MainActionBarButtonContainer5",
            "MainActionBarButtonContainer6", "MainActionBarButtonContainer7",
            "MainActionBarButtonContainer8" },
    [2] = { "MultiBarBottomLeft", "ActionBar2" },
    [3] = { "MultiBarBottomRight", "ActionBar3" },
    [4] = { "MultiBarRight", "ActionBar4" },
    [5] = { "MultiBarLeft", "ActionBar5" },
    [6] = { "MultiBar5", "ActionBar6" },
    [7] = { "MultiBar6", "ActionBar7" },
    [8] = { "MultiBar7", "ActionBar8" },
}

local DEFAULT_POSITIONS = {
    [1] = { "BOTTOM", "UIParent", "BOTTOM", 0.56, 13.33 },
    [2] = { "BOTTOM", "UIParent", "BOTTOM", 1.11, 60.00 },
    [3] = { "BOTTOM", "UIParent", "BOTTOM", 1.12, 107.22 },
    [4] = { "BOTTOM", "UIParent", "BOTTOM", 0, 170 },
    [5] = { "BOTTOM", "UIParent", "BOTTOM", 0, 220 },
    [6] = { "BOTTOM", "UIParent", "BOTTOM", 0, 270 },
    [7] = { "BOTTOM", "UIParent", "BOTTOM", 0, 320 },
    [8] = { "BOTTOM", "UIParent", "BOTTOM", 0, 370 },
}

local PET_DEFAULT_POSITION = { "BOTTOM", "UIParent", "BOTTOM", 0, 140 }
local STANCE_DEFAULT_POSITION = { "BOTTOM", "UIParent", "BOTTOM", 197.77, 154.45 }

local BASE_BUTTON_SIZE = 44
local DEFAULT_CLASS_COLOR = {0.5, 0.5, 0.5}

local VALID_ANCHORS = {
    TOPLEFT = true, TOP = true, TOPRIGHT = true,
    LEFT = true, CENTER = true, RIGHT = true,
    BOTTOMLEFT = true, BOTTOM = true, BOTTOMRIGHT = true,
}

-- ============================================================
-- SECTION B: Module state
-- ============================================================

local ActionBarManager = CreateFrame("Frame")

local actionBarContainers = {}      -- [barNum] = container frame
local actionBarBoxes = {}           -- [barNum][i] = styled box frame
local actionBarButtons = {}         -- [barNum][i] = Blizzard button
local petBarContainer = nil
local petBarBoxes = {}
local stanceBarContainer = nil
local stanceBarBoxes = {}

local CURRENT_HOVER_BUTTON = nil
local hiddenDragActive = false
local initialized = false

-- One-time hook flags (module-local, not on _G).
local petCheckedHooked = false
local moveToHooked = false
local tutorialHooked = false

-- ============================================================
-- SECTION C: Combat-deferred queue
-- Small local queue for non-position protected actions. Positions
-- are deferred by MoverRegistry; this handles SetParent / SetPoint
-- on action buttons themselves which are also protected.
-- ============================================================

local deferredActions = {}
local deferredCount = 0

local function IsInCombat()
    return InCombatLockdown and InCombatLockdown()
end

local function Defer(key, fn)
    if not key or not fn then return end
    if not deferredActions[key] then
        deferredCount = deferredCount + 1
    end
    deferredActions[key] = fn
    ActionBarManager:RegisterEvent("PLAYER_REGEN_ENABLED")
end

local function FlushDeferred()
    if IsInCombat() then return end
    local pending = deferredActions
    deferredActions = {}
    deferredCount = 0
    for _, fn in pairs(pending) do
        pcall(fn)
    end
    if deferredCount <= 0 then
        ActionBarManager:UnregisterEvent("PLAYER_REGEN_ENABLED")
    end
end

-- ============================================================
-- SECTION D: Settings accessors
-- Always read MidnightUISettings fresh through the global (per
-- Bootstrap.lua's per-character swap requirement).
-- ============================================================

local function GetAllSettings()
    if _G.MidnightUI_GetActionBarSettings then
        return _G.MidnightUI_GetActionBarSettings() or {}
    end
    return (_G.MidnightUISettings and _G.MidnightUISettings.ActionBars) or {}
end

local function GetBarConfig(barNum)
    local all = GetAllSettings()
    return all["bar" .. barNum]
end

local function IsBarEnabled(barNum)
    local bar = GetBarConfig(barNum)
    if not bar then return false end
    return bar.enabled ~= false
end

local function AnyBarEnabled()
    for i = 1, 8 do
        if IsBarEnabled(i) then return true end
    end
    return false
end

local function EnsurePetSettings()
    local s = _G.MidnightUISettings
    if not s then return nil end
    if not s.PetBar then s.PetBar = {} end
    local p = s.PetBar
    if p.enabled == nil then p.enabled = true end
    if not p.scale then p.scale = 100 end
    if not p.alpha then p.alpha = 1.0 end
    if not p.buttonSize then p.buttonSize = 40 end
    if not p.spacing then p.spacing = 6 end
    if not p.buttonsPerRow then p.buttonsPerRow = 10 end
    return p
end

local function EnsureStanceSettings()
    local s = _G.MidnightUISettings
    if not s then return nil end
    if not s.StanceBar then s.StanceBar = {} end
    local p = s.StanceBar
    if p.enabled == nil then p.enabled = true end
    if not p.scale then p.scale = 100 end
    if not p.alpha then p.alpha = 1.0 end
    if not p.buttonSize then p.buttonSize = 32 end
    if not p.spacing then p.spacing = 4 end
    if not p.buttonsPerRow then p.buttonsPerRow = 3 end
    return p
end

local function GlobalStyleOverride()
    local all = GetAllSettings()
    local gs = all.globalStyle
    if gs == nil or gs == "Disabled" or gs == "Per Bar" then return nil end
    return gs
end

local function OverlaysUnlocked()
    local s = _G.MidnightUISettings
    return s and s.Messenger and s.Messenger.locked == false
end

-- ============================================================
-- SECTION E: Class + Role helpers
-- ============================================================

local function PlayerHasPetClass()
    local _, classFilename = UnitClass("player")
    return classFilename == "HUNTER"
        or classFilename == "WARLOCK"
        or classFilename == "DEATHKNIGHT"
end

local function PlayerHasStanceClass()
    if GetNumShapeshiftForms and GetNumShapeshiftForms() > 0 then
        return true
    end
    local _, classFilename = UnitClass("player")
    return classFilename == "DRUID"
        or classFilename == "WARRIOR"
        or classFilename == "ROGUE"
        or classFilename == "SHAMAN"
        or classFilename == "DEATHKNIGHT"
end

local function GetPlayerClassColor()
    if _G.MidnightUI_Core and _G.MidnightUI_Core.GetClassColor then
        return _G.MidnightUI_Core.GetClassColor("player")
    end
    return DEFAULT_CLASS_COLOR[1], DEFAULT_CLASS_COLOR[2], DEFAULT_CLASS_COLOR[3]
end

-- ============================================================
-- SECTION F: Muted Frames Registry
-- Single source of truth for every Blizzard frame state we have
-- changed. Mute captures the original, applies the change. Unmute
-- restores from the capture and removes the entry. Both are
-- idempotent: muting a muted frame is a no-op, unmuting an
-- unmuted frame is a no-op.
-- ============================================================

local MutedFrames = {}  -- [frame] = { strata, alpha, mouse, drawLayers }

local function IsFrameMuted(frame)
    return frame and MutedFrames[frame] ~= nil
end

local function MuteFrame(frame, opts)
    if not frame or MutedFrames[frame] then return end
    opts = opts or {}

    local record = {}
    if frame.GetFrameStrata then record.strata = frame:GetFrameStrata() end
    if frame.GetAlpha then record.alpha = frame:GetAlpha() end
    if frame.IsMouseEnabled then record.mouse = frame:IsMouseEnabled() end

    if frame.SetFrameStrata and not opts.keepStrata then
        frame:SetFrameStrata("BACKGROUND")
    end
    if frame.EnableMouse and not opts.keepMouse then
        frame:EnableMouse(false)
    end
    if frame.SetAlpha and not opts.keepAlpha then
        frame:SetAlpha(0)
    end

    if opts.disableDrawLayers and frame.DisableDrawLayer then
        record.drawLayers = { "BACKGROUND", "BORDER", "OVERLAY" }
        for _, layer in ipairs(record.drawLayers) do
            frame:DisableDrawLayer(layer)
        end
    end

    MutedFrames[frame] = record
end

local function UnmuteFrame(frame)
    if not frame then return end
    local record = MutedFrames[frame]
    if not record then return end

    if frame.SetFrameStrata and record.strata then
        frame:SetFrameStrata(record.strata)
    end
    if frame.EnableMouse and record.mouse ~= nil then
        frame:EnableMouse(record.mouse)
    end
    if frame.SetAlpha and record.alpha ~= nil then
        frame:SetAlpha(record.alpha)
    end
    if record.drawLayers and frame.EnableDrawLayer then
        for _, layer in ipairs(record.drawLayers) do
            frame:EnableDrawLayer(layer)
        end
    end

    MutedFrames[frame] = nil
end

local function MuteByName(name, opts)
    local f = _G[name]
    if f then MuteFrame(f, opts) end
end

local function UnmuteByName(name)
    local f = _G[name]
    if f then UnmuteFrame(f) end
end

-- Per-button slot art alphas. Recorded separately because these
-- are texture regions on the button, not frame mutes.
local SlotArtRecords = {}  -- [button] = { slotBg, kbBg, slotArt, hl, hotkey, normal }

local function HideButtonSlotArt(button)
    if not button or SlotArtRecords[button] then return end
    local r = {}
    if button.SlotBackground then
        r.slotBg = button.SlotBackground:GetAlpha()
        button.SlotBackground:SetAlpha(0)
    end
    if button.KeybindSlotBackground then
        r.kbBg = button.KeybindSlotBackground:GetAlpha()
        button.KeybindSlotBackground:SetAlpha(0)
    end
    if button.SlotArt then
        r.slotArt = button.SlotArt:GetAlpha()
        button.SlotArt:SetAlpha(0)
    end
    if button.GetHighlightTexture then
        local hl = button:GetHighlightTexture()
        if hl and hl ~= button.StyledIconHighlight then
            r.hl = hl
            r.hlAlpha = hl:GetAlpha()
            hl:SetAlpha(0)
        end
    end
    if button.HotKey then
        r.hotkey = button.HotKey:GetAlpha()
        button.HotKey:SetAlpha(0)
    end
    if button.NormalTexture then
        local nt = button:GetNormalTexture()
        if nt then
            r.normal = nt
            r.normalAlpha = nt:GetAlpha()
            nt:SetAlpha(0)
        end
    end
    if button.GetPushedTexture then
        local pushed = button:GetPushedTexture()
        if pushed then
            r.pushed = pushed
            r.pushedAlpha = pushed:GetAlpha()
            pushed:SetAlpha(1)
            pushed:SetColorTexture(0, 0, 0, 0.4)
            pushed:SetAllPoints(button)
        end
    end
    SlotArtRecords[button] = r
end

local function RestoreButtonSlotArt(button)
    if not button then return end
    local r = SlotArtRecords[button]
    if not r then
        -- Defensive restore for buttons we never recorded (e.g.
        -- post-reload state where mute happened in the old session).
        if button.SlotBackground then button.SlotBackground:SetAlpha(1) end
        if button.KeybindSlotBackground then button.KeybindSlotBackground:SetAlpha(1) end
        if button.SlotArt then button.SlotArt:SetAlpha(1) end
        if button.HotKey then button.HotKey:SetAlpha(1) end
        if button.GetNormalTexture then
            local nt = button:GetNormalTexture()
            if nt then nt:SetAlpha(1) end
        end
        if button.GetHighlightTexture then
            local hl = button:GetHighlightTexture()
            if hl and hl ~= button.StyledIconHighlight then hl:SetAlpha(1) end
        end
        return
    end
    if button.SlotBackground then button.SlotBackground:SetAlpha(r.slotBg or 1) end
    if button.KeybindSlotBackground then button.KeybindSlotBackground:SetAlpha(r.kbBg or 1) end
    if button.SlotArt then button.SlotArt:SetAlpha(r.slotArt or 1) end
    if button.HotKey then button.HotKey:SetAlpha(r.hotkey or 1) end
    if r.hl then r.hl:SetAlpha(r.hlAlpha or 1) end
    if r.normal then r.normal:SetAlpha(r.normalAlpha or 1) end
    if r.pushed then r.pushed:SetAlpha(r.pushedAlpha or 1) end
    SlotArtRecords[button] = nil
end

-- ============================================================
-- SECTION G: Theme System
-- Themes are data; one resolver looks up the right table.
-- ============================================================

local function MakeClassColorTheme()
    local cR, cG, cB = GetPlayerClassColor()
    return {
        style = "Class Color",
        bg = {cR * 0.15, cG * 0.15, cB * 0.15, 0.85},
        outerBorder = {cR * 0.3, cG * 0.3, cB * 0.3, 1},
        mainBorder = {cR, cG, cB, 0.9},
        topShineStart = {cR, cG, cB, 0.16},
        topShineEnd = {cR, cG, cB, 0},
        edgeGlow = {cR, cG, cB, 0},
        highlight = {cR, cG, cB, 0.25},
        vignette = {0, 0, 0, 0.4},
        vignetteTop = {0, 0, 0, 0.72},
        vignetteBottom = {0, 0, 0, 0.05},
        innerHighlight = {1, 1, 1, 0.05},
        shadow = {0, 0, 0, 0.9},
    }
end

local STATIC_THEMES = {
    ["Faithful"] = {
        style = "Faithful",
        bg = {0.65, 0.58, 0.45, 0.85},
        outerBorder = {0.48, 0.40, 0.28, 1},
        mainBorder = {0.78, 0.69, 0.52, 0.9},
        topShineStart = {1.00, 0.96, 0.86, 0.18},
        topShineEnd = {1.00, 0.96, 0.86, 0},
        edgeGlow = {0.92, 0.82, 0.62, 0},
        highlight = {0.95, 0.86, 0.70, 0.20},
        vignette = {0, 0, 0, 0.4},
        vignetteTop = {0, 0, 0, 0.72},
        vignetteBottom = {0, 0, 0, 0.05},
        innerHighlight = {1, 1, 1, 0.05},
        shadow = {0, 0, 0, 0.9},
    },
    ["Glass"] = {
        style = "Glass",
        bg = {0, 0, 0, 0},
        outerBorder = {0.05, 0.06, 0.10, 0.12},
        mainBorder = {0.05, 0.06, 0.10, 0.18},
        topShineStart = {1, 1, 1, 0},
        topShineEnd = {1, 1, 1, 0},
        edgeGlow = {0.70, 0.85, 1.00, 0},
        highlight = {0.05, 0.06, 0.10, 0.06},
        vignette = {0, 0, 0, 0},
        vignetteTop = {0, 0, 0, 0},
        vignetteBottom = {0, 0, 0, 0},
        innerHighlight = {1, 1, 1, 0},
        shadow = {0, 0, 0, 0},
    },
    ["Hidden"] = {
        style = "Hidden",
        bg = {0, 0, 0, 0},
        outerBorder = {0, 0, 0, 0},
        mainBorder = {0, 0, 0, 0},
        topShineStart = {0, 0, 0, 0},
        topShineEnd = {0, 0, 0, 0},
        edgeGlow = {0, 0, 0, 0},
        highlight = {0.45, 0.75, 1.00, 0.25},
        vignette = {0, 0, 0, 0},
        vignetteTop = {0, 0, 0, 0},
        vignetteBottom = {0, 0, 0, 0},
        innerHighlight = {0, 0, 0, 0},
        shadow = {0, 0, 0, 0},
    },
}

local function ResolveTheme(barNum)
    local override = GlobalStyleOverride()
    local style
    if override then
        style = override
    else
        local bar = barNum and GetBarConfig(barNum)
        style = (bar and bar.style) or "Class Color"
    end
    if style == "Class Color" then return MakeClassColorTheme() end
    return STATIC_THEMES[style] or MakeClassColorTheme()
end

-- ============================================================
-- SECTION H: Hotkey formatting + binding resolution
-- ============================================================

local KEY_ABBREVIATIONS = {
    MOUSEWHEELUP = "Wu", MOUSEWHEELDOWN = "Wd",
    NUMPAD0 = "n0", NUMPAD1 = "n1", NUMPAD2 = "n2", NUMPAD3 = "n3",
    NUMPAD4 = "n4", NUMPAD5 = "n5", NUMPAD6 = "n6", NUMPAD7 = "n7",
    NUMPAD8 = "n8", NUMPAD9 = "n9",
    NUMPADDECIMAL = "n.", NUMPADPLUS = "n+", NUMPADMINUS = "n-",
    NUMPADMULTIPLY = "n*", NUMPADDIVIDE = "n/",
    SPACEBAR = "Sp", BACKSPACE = "Bs", ESCAPE = "Es",
    DELETE = "De", INSERT = "In", HOME = "Hm", ["END"] = "En",
    PAGEUP = "PU", PAGEDOWN = "PD", CAPSLOCK = "Ca",
    PRINTSCREEN = "Pr", SCROLLLOCK = "SL", PAUSE = "Pa",
    TAB = "Tb", ENTER = "Ent",
}

local function FormatHotkey(keybind)
    if not keybind or keybind == "" then return "" end
    local modifiers = ""
    local mainKey = keybind
    if mainKey:find("SHIFT%-") then modifiers = modifiers .. "s"; mainKey = mainKey:gsub("SHIFT%-", "") end
    if mainKey:find("CTRL%-") then modifiers = modifiers .. "c"; mainKey = mainKey:gsub("CTRL%-", "") end
    if mainKey:find("ALT%-") then modifiers = modifiers .. "a"; mainKey = mainKey:gsub("ALT%-", "") end

    if KEY_ABBREVIATIONS[mainKey] then
        mainKey = KEY_ABBREVIATIONS[mainKey]
    elseif mainKey:match("^BUTTON(%d+)$") then
        mainKey = "M" .. mainKey:match("^BUTTON(%d+)$")
    elseif #mainKey > 3 and not mainKey:match("^F%d+$") then
        mainKey = mainKey:sub(1, 2)
    end

    local display = modifiers .. mainKey
    if #display > 5 then display = display:sub(1, 4) .. "..." end
    return display
end

local function ShortBindingText(key)
    if not key or key == "" then return "" end
    local text = GetBindingText and GetBindingText(key, "KEY_") or key
    if text and text ~= "" then return text end
    return key
end

local function ResolveBindingCommand(button, barNum, index)
    if button and button.GetName then
        local name = button:GetName()
        if name then
            for n = 1, 8 do
                local prefix = BAR_BUTTON_MAP[n]
                local suffix = name:match("^" .. prefix .. "(%d+)$")
                if suffix then
                    return BAR_BINDING_MAP[n] .. suffix
                end
            end
        end
    end
    if barNum and index and BAR_BINDING_MAP[barNum] then
        return BAR_BINDING_MAP[barNum] .. index
    end
    return nil
end

-- ============================================================
-- SECTION I: Position handling
-- Prefers MoverRegistry, falls back to legacy bar.position for
-- backward compatibility with old SavedVars.
-- ============================================================

local function ApplySavedPoint(frame, fallbackKey, moverKey, legacyPosition)
    if not frame then return end
    local MR = _G.MidnightUI_MoverRegistry
    if MR and moverKey and MR.GetEntry and MR.GetEntry(moverKey) then
        MR.ApplyPosition(moverKey)
        return
    end
    frame:ClearAllPoints()
    if legacyPosition and VALID_ANCHORS[legacyPosition[1]] and VALID_ANCHORS[legacyPosition[2]] then
        frame:SetPoint(legacyPosition[1], UIParent, legacyPosition[2], legacyPosition[3] or 0, legacyPosition[4] or 0)
        return
    end
    if fallbackKey then
        frame:SetPoint(fallbackKey[1], UIParent, fallbackKey[3], fallbackKey[4], fallbackKey[5])
    end
end

local function PositionToDefaultString(pos)
    if not pos then return nil end
    return pos[1] .. "," .. pos[2] .. "," .. pos[3] .. ","
        .. string.format("%.2f", pos[4]) .. "," .. string.format("%.2f", pos[5])
end

-- ============================================================
-- SECTION J: Blizzard Suppression
-- Symmetric: Suppress*() captures and mutes; Restore*() un-mutes
-- and removes the capture. ApplySuppression() is the single
-- top-level call invoked after every rebuild and on Blizzard
-- re-show events.
-- ============================================================

local function SuppressBlizzardBar(barNum)
    local frames = BAR_HOST_FRAMES[barNum]
    if not frames then return end
    for _, name in ipairs(frames) do
        MuteByName(name)
    end
    if barNum == 1 then
        -- MainMenuBar has named art children that need alpha 0 but
        -- the parent itself needs to keep alpha 1 so muted children
        -- below can keep rendering at alpha 1 if needed elsewhere.
        local mmb = _G.MainMenuBar
        if mmb then
            if mmb.EndCaps then mmb.EndCaps:SetAlpha(0) end
            if mmb.BorderArt then mmb.BorderArt:SetAlpha(0) end
            if mmb.Background then mmb.Background:SetAlpha(0) end
        end
        local mab = _G.MainActionBar
        if mab then
            if mab.EndCaps then mab.EndCaps:SetAlpha(0) end
            if mab.ActionBarPageNumber then mab.ActionBarPageNumber:SetAlpha(0) end
            if mab.BorderArt then mab.BorderArt:SetAlpha(0) end
        end
    end
end

local function RestoreBlizzardBar(barNum)
    local frames = BAR_HOST_FRAMES[barNum]
    if not frames then return end
    for _, name in ipairs(frames) do
        UnmuteByName(name)
    end
    if barNum == 1 then
        local mmb = _G.MainMenuBar
        if mmb then
            if mmb.EndCaps then mmb.EndCaps:SetAlpha(1) end
            if mmb.BorderArt then mmb.BorderArt:SetAlpha(1) end
            if mmb.Background then mmb.Background:SetAlpha(1) end
        end
        local mab = _G.MainActionBar
        if mab then
            if mab.EndCaps then mab.EndCaps:SetAlpha(1) end
            if mab.ActionBarPageNumber then mab.ActionBarPageNumber:SetAlpha(1) end
            if mab.BorderArt then mab.BorderArt:SetAlpha(1) end
        end
    end
end

local function SuppressGlobalChrome()
    -- Controllers exist independent of per-bar art. Mute them while
    -- ANY MUI bar is active so Blizzard's controller logic doesn't
    -- fight ours. Restored when no MUI bar is active.
    MuteByName("ActionBarController")
    MuteByName("ActionBarButtonEventsFrame")
    local stbm = _G.StatusTrackingBarManager
    if stbm and stbm.MainStatusTrackingBarContainer then
        MuteFrame(stbm.MainStatusTrackingBarContainer, { disableDrawLayers = true, keepAlpha = true, keepStrata = true, keepMouse = true })
    end
end

local function RestoreGlobalChrome()
    UnmuteByName("ActionBarController")
    UnmuteByName("ActionBarButtonEventsFrame")
    local stbm = _G.StatusTrackingBarManager
    if stbm and stbm.MainStatusTrackingBarContainer then
        UnmuteFrame(stbm.MainStatusTrackingBarContainer)
    end
end

local function ApplySuppression()
    if IsInCombat() then
        Defer("ApplySuppression", ApplySuppression)
        return
    end

    -- Per-bar: mute hosts of enabled bars, restore hosts of disabled bars.
    for barNum = 1, 8 do
        if IsBarEnabled(barNum) then
            SuppressBlizzardBar(barNum)
        else
            RestoreBlizzardBar(barNum)
        end
    end

    -- Per-button slot art: hide on enabled bars, restore on disabled.
    for barNum = 1, 8 do
        local prefix = BAR_BUTTON_MAP[barNum]
        if prefix then
            local enabled = IsBarEnabled(barNum)
            for i = 1, 12 do
                local button = _G[prefix .. i]
                if button then
                    if enabled then
                        HideButtonSlotArt(button)
                    else
                        RestoreButtonSlotArt(button)
                    end
                end
            end
        end
    end

    -- Global chrome: only muted when at least one MUI bar is active.
    if AnyBarEnabled() then
        SuppressGlobalChrome()
    else
        RestoreGlobalChrome()
    end
end

-- ============================================================
-- SECTION K: Styled Box Creation
-- ============================================================

local function CreateStyledBox(parent, name, size, theme)
    local box = CreateFrame("Frame", name, parent, "BackdropTemplate")
    box:SetSize(size, size)
    box:SetFrameStrata("MEDIUM")
    box:EnableMouse(false)

    local shadow = box:CreateTexture(nil, "BACKGROUND", nil, -8)
    shadow:SetPoint("CENTER", 0, 0)
    shadow:SetSize(size + 16, size + 16)
    shadow:SetTexture("Interface\\GLUES\\MODELS\\UI_MainMenu\\BlizzShadow")
    shadow:SetVertexColor(unpack(theme.shadow))
    shadow:SetBlendMode("BLEND")
    box.shadow = shadow

    local bg = box:CreateTexture(nil, "BACKGROUND", nil, -7)
    bg:SetAllPoints(box)
    bg:SetColorTexture(unpack(theme.bg))
    box.bg = bg

    local vignette = box:CreateTexture(nil, "BACKGROUND", nil, -6)
    vignette:SetPoint("TOPLEFT", 2, -2)
    vignette:SetPoint("BOTTOMRIGHT", -2, 2)
    vignette:SetColorTexture(unpack(theme.vignette))
    vignette:SetGradient("VERTICAL",
        CreateColor(unpack(theme.vignetteTop)),
        CreateColor(unpack(theme.vignetteBottom))
    )
    box.vignette = vignette

    local outerBorder = box:CreateTexture(nil, "BORDER", nil, -5)
    outerBorder:SetPoint("TOPLEFT", -1, 1)
    outerBorder:SetPoint("BOTTOMRIGHT", 1, -1)
    outerBorder:SetColorTexture(unpack(theme.outerBorder))
    box.outerBorder = outerBorder

    local mainBorder = box:CreateTexture(nil, "BORDER", nil, -4)
    mainBorder:SetAllPoints(box)
    mainBorder:SetColorTexture(unpack(theme.mainBorder))
    box.mainBorder = mainBorder

    local innerHighlight = box:CreateTexture(nil, "BORDER", nil, -3)
    innerHighlight:SetPoint("TOPLEFT", 1, -1)
    innerHighlight:SetPoint("BOTTOMRIGHT", -1, 1)
    innerHighlight:SetColorTexture(unpack(theme.innerHighlight))
    box.innerHighlight = innerHighlight

    local topShine = box:CreateTexture(nil, "OVERLAY", nil, 1)
    topShine:SetPoint("TOPLEFT", 3, -3)
    topShine:SetPoint("TOPRIGHT", -3, -3)
    topShine:SetHeight(size * 0.3)
    topShine:SetColorTexture(1, 1, 1, 0.02)
    topShine:SetGradient("VERTICAL",
        CreateColor(unpack(theme.topShineStart)),
        CreateColor(unpack(theme.topShineEnd))
    )
    box.topShine = topShine

    local glowFrame = CreateFrame("Frame", nil, box)
    glowFrame:SetAllPoints()
    glowFrame:SetFrameLevel(box:GetFrameLevel() + 1)
    box.glowFrame = glowFrame

    local edgeGlow = glowFrame:CreateTexture(nil, "OVERLAY", nil, 2)
    edgeGlow:SetPoint("CENTER")
    edgeGlow:SetSize(size + 16, size + 16)
    edgeGlow:SetTexture("Interface\\GLUES\\MODELS\\UI_MainMenu\\BlizzShadow")
    edgeGlow:SetVertexColor(unpack(theme.edgeGlow))
    edgeGlow:SetBlendMode("ADD")
    box.edgeGlow = edgeGlow

    box._muiStyle = theme.style
    return box
end

local function ApplyBoxTheme(box, theme, size)
    if not box or not theme then return end
    if box.bg then box.bg:SetColorTexture(unpack(theme.bg)) end
    if box.outerBorder then box.outerBorder:SetColorTexture(unpack(theme.outerBorder)) end
    if box.mainBorder then box.mainBorder:SetColorTexture(unpack(theme.mainBorder)) end
    if box.shadow then box.shadow:SetVertexColor(unpack(theme.shadow)) end
    if box.vignette then
        box.vignette:SetColorTexture(unpack(theme.vignette))
        box.vignette:SetGradient("VERTICAL",
            CreateColor(unpack(theme.vignetteTop)),
            CreateColor(unpack(theme.vignetteBottom))
        )
    end
    if box.innerHighlight then box.innerHighlight:SetColorTexture(unpack(theme.innerHighlight)) end
    if box.topShine then
        if size then box.topShine:SetHeight(size * 0.3) end
        box.topShine:SetGradient("VERTICAL",
            CreateColor(unpack(theme.topShineStart)),
            CreateColor(unpack(theme.topShineEnd))
        )
    end
    if box.edgeGlow then
        local gR, gG, gB, gA = unpack(theme.edgeGlow)
        if theme.style ~= "Hidden" and (not gA or gA <= 0) then
            gR, gG, gB = theme.highlight[1], theme.highlight[2], theme.highlight[3]
            gA = math_max(theme.highlight[4] or 0.25, 0.25)
        end
        box.edgeGlow:SetVertexColor(gR, gG, gB, gA or 0)
    end
    box._muiStyle = theme.style
end

local function TuneCompactBox(box, theme, borderKey, inset, borderAlpha)
    if not box then return end
    local function ScaleAlpha(tex, scale)
        if not tex or not tex.GetVertexColor then return end
        local r, g, b, a = tex:GetVertexColor()
        tex:SetVertexColor(r, g, b, (a or 1) * scale)
    end
    ScaleAlpha(box.shadow, 0.25)
    if box.outerBorder then box.outerBorder:SetAlpha(0) end
    if box.mainBorder then box.mainBorder:SetAlpha(0) end
    if box.innerHighlight then box.innerHighlight:SetAlpha(0) end
    if box.topShine then box.topShine:SetAlpha(0) end
    if box.edgeGlow then box.edgeGlow:SetAlpha(0) end
    ScaleAlpha(box.bg, 0.35)
    if not box[borderKey] then
        local b = box:CreateTexture(nil, "OVERLAY", nil, 5)
        b:SetPoint("TOPLEFT", inset, -inset)
        b:SetPoint("BOTTOMRIGHT", -inset, inset)
        box[borderKey] = b
    end
    if theme and box[borderKey] then
        local r, g, b, a = unpack(theme.mainBorder)
        box[borderKey]:SetColorTexture(r, g, b, math_min(a or 1, borderAlpha))
    end
end

-- ============================================================
-- SECTION L: Button Styling Shared Helpers
-- ============================================================

local function SlotHasAction(button)
    return button and button.action and HasAction(button.action)
end

local function IsHiddenEmptySlot(box, button)
    return box and box._muiStyle == "Hidden" and not SlotHasAction(button)
end

local function SetButtonHotkeyText(button, box, barNum, index)
    if not button or not box then return end
    if not button.StyledHotkey then
        button.StyledHotkey = button:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        button.StyledHotkey:SetPoint("TOPRIGHT", box, "TOPRIGHT", -2, -2)
        button.StyledHotkey:SetFont(STANDARD_TEXT_FONT, 10, "OUTLINE")
        button.StyledHotkey:SetTextColor(0.85, 0.90, 1.00)
        button.StyledHotkey:SetShadowOffset(1, -1)
        button.StyledHotkey:SetShadowColor(0, 0, 0, 0.8)
    end

    if IsHiddenEmptySlot(box, button) then
        button.StyledHotkey:SetText("")
        button.StyledHotkey:Hide()
        if button.HotKey then button.HotKey:SetAlpha(0); button.HotKey:SetText("") end
        return
    end

    button.StyledHotkey:Show()
    local cmd = ResolveBindingCommand(button, barNum, index)
    if cmd then
        button.StyledHotkey:SetText(FormatHotkey(GetBindingKey(cmd)))
    else
        button.StyledHotkey:SetText("")
    end
end

local function DisableNonInteractiveChildMouse(button)
    -- Children intercept clicks that should hit the button itself.
    -- Disabling them keeps the button as the click target and avoids
    -- taint from secure-aura child frames.
    if button.cooldown and button.cooldown.EnableMouse then button.cooldown:EnableMouse(false) end
    if button.Icon and button.Icon.EnableMouse then button.Icon:EnableMouse(false) end
    if button.IconMask and button.IconMask.EnableMouse then button.IconMask:EnableMouse(false) end
    if button.Count and button.Count.EnableMouse then button.Count:EnableMouse(false) end
    if button.HotKey and button.HotKey.EnableMouse then button.HotKey:EnableMouse(false) end
    if button.Name and button.Name.EnableMouse then button.Name:EnableMouse(false) end
    if button.Border and button.Border.EnableMouse then button.Border:EnableMouse(false) end
    if button.Flash and button.Flash.EnableMouse then button.Flash:EnableMouse(false) end
    if button.SpellHighlightTexture and button.SpellHighlightTexture.EnableMouse then
        button.SpellHighlightTexture:EnableMouse(false)
    end
    for _, child in ipairs({button:GetChildren()}) do
        if child and child.EnableMouse then child:EnableMouse(false) end
    end
end

local function AnchorRegionToBox(region, box)
    if not region or not region.ClearAllPoints then return end
    region:ClearAllPoints()
    region:SetPoint("TOPLEFT", box, "TOPLEFT", 0, 0)
    region:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", 0, 0)
end

local function FixTutorialDragTargetFrame()
    local t = _G.TutorialDragTargetFrame
    if not t then return end
    local parent = t:GetParent()
    local b = (parent and parent._muiBox) or nil
    if not b and parent and parent.GetName then
        local pn = parent:GetName()
        if pn and _G[pn] and _G[pn]._muiBox then
            b = _G[pn]._muiBox
        end
    end
    if not b then return end
    t:ClearAllPoints()
    t:SetPoint("TOPLEFT", b, "TOPLEFT", -2, 2)
    t:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 2, -2)
    if t.Glow then t.Glow:SetAllPoints(t) end
    if t.Border then t.Border:SetAllPoints(t) end
    if t.Circle then t.Circle:SetAllPoints(t) end
end

local function InstallTutorialHook()
    if tutorialHooked then return end
    local t = _G.TutorialDragTargetFrame
    if not t or not t.HookScript then return end
    tutorialHooked = true
    t:HookScript("OnShow", function()
        local frame = _G.TutorialDragTargetFrame
        if frame and frame:GetParent() and frame:GetParent()._muiBox then
            FixTutorialDragTargetFrame()
        end
    end)
end

-- ============================================================
-- SECTION M: Main Action Button Styling
-- ============================================================

local function SyncButtonToBox(button, box)
    if not button or not box or button._muiSyncing then return end
    if box.IsVisible and not box:IsVisible() then return end
    if IsInCombat() and button:IsProtected() then return end

    button._muiSyncing = true
    button:SetScale(1)
    button:ClearAllPoints()
    button:SetPoint("TOPLEFT", box, "TOPLEFT", 0, 0)
    button:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", 0, 0)
    button:SetHitRectInsets(0, 0, 0, 0)
    button._muiBox = box

    -- Anchor every child to the box. Skip the cooldown (handled
    -- separately) and any Blizzard "RotationHelper" atlas frames
    -- (the assisted-highlight ring needs its own scaling).
    for _, child in ipairs({button:GetChildren()}) do
        if child and child ~= button.cooldown then
            local hasHelper = false
            if child.GetRegions then
                for _, region in ipairs({child:GetRegions()}) do
                    local atlas = region.GetAtlas and region:GetAtlas()
                    if atlas and type(atlas) == "string" and atlas:find("RotationHelper") then
                        hasHelper = true
                        break
                    end
                end
            end
            if not hasHelper then
                child:ClearAllPoints()
                child:SetPoint("TOPLEFT", box, "TOPLEFT", 0, 0)
                child:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", 0, 0)
            end
        end
    end

    button._muiSyncing = false
end

local function InstallButtonHoverAnimation(button, box, theme)
    if button.hoverAnimation then return end

    local gR, gG, gB, gA = unpack(theme.edgeGlow)
    if not gA or gA <= 0 then
        gR, gG, gB = theme.highlight[1], theme.highlight[2], theme.highlight[3]
        gA = math_max(theme.highlight[4] or 0.25, 0.55)
    end
    if box.edgeGlow then box.edgeGlow:SetVertexColor(gR, gG, gB, gA) end

    button.hoverAnimation = box.edgeGlow:CreateAnimationGroup()
    local fadeIn = button.hoverAnimation:CreateAnimation("Alpha")
    fadeIn:SetFromAlpha(0)
    fadeIn:SetToAlpha(1.0)
    fadeIn:SetDuration(0.12)
    fadeIn:SetSmoothing("IN")

    local function SetHoverState(isOver)
        if isOver then
            if button.StyledIconHighlight then button.StyledIconHighlight:SetAlpha(1.0) end
            if box.edgeGlow then box.edgeGlow:SetAlpha(1.0) end
        else
            if button.StyledIconHighlight then button.StyledIconHighlight:SetAlpha(0) end
            if box.edgeGlow then box.edgeGlow:SetAlpha(0) end
        end
    end

    button:HookScript("OnEnter", function()
        if IsHiddenEmptySlot(box, button) and not hiddenDragActive then return end
        if CURRENT_HOVER_BUTTON and CURRENT_HOVER_BUTTON ~= button and CURRENT_HOVER_BUTTON._muiSetHoverState then
            CURRENT_HOVER_BUTTON._muiSetHoverState(false)
        end
        CURRENT_HOVER_BUTTON = button
        SetHoverState(true)
        if button.hoverAnimation and not button.hoverAnimation:IsPlaying() then
            button.hoverAnimation:Play()
        end
    end)
    button:HookScript("OnLeave", function()
        SetHoverState(false)
        if button.hoverAnimation then button.hoverAnimation:Stop() end
        if CURRENT_HOVER_BUTTON == button then CURRENT_HOVER_BUTTON = nil end
    end)
    button._muiSetHoverState = SetHoverState
end

local function StyleMainButton(button, box, barNum, index, theme)
    if not button or not box then return end

    -- One-time mouse and hook setup.
    if not button._muiMouseFix then
        button._muiMouseFix = true
        button:EnableMouse(true)
        button:SetHitRectInsets(0, 0, 0, 0)
        button:SetScale(1)
        DisableNonInteractiveChildMouse(button)
        button._muiSyncToBox = function() SyncButtonToBox(button, box) end
        button:HookScript("OnShow", function() C_Timer.After(0, button._muiSyncToBox) end)
        button:HookScript("OnSizeChanged", function() C_Timer.After(0, button._muiSyncToBox) end)
        if button.SetAttribute and not IsInCombat() then
            button:SetAttribute("flyoutDirection", "UP")
        end
    end

    -- Pushed texture: dark overlay anchored to box.
    if button.GetPushedTexture then
        local pushed = button:GetPushedTexture()
        if pushed then
            pushed:SetAlpha(1)
            pushed:SetColorTexture(0, 0, 0, 0.4)
            pushed:ClearAllPoints()
            pushed:SetAllPoints(box)
        end
    end

    -- Normal texture hidden so it doesn't draw the Blizzard slot.
    if button.GetNormalTexture then
        local nt = button:GetNormalTexture()
        if nt then nt:SetAlpha(0) end
    end

    -- Icon: anchor to box, drop the IconMask Blizzard adds.
    if not button.icon then
        button.icon = button.Icon
            or (button.GetName and _G[button:GetName() .. "Icon"])
            or nil
    end
    if button.icon then
        if button.icon:IsShown() then button.icon:SetAlpha(1) end
        button.icon:SetTexCoord(0, 1, 0, 1)
        AnchorRegionToBox(button.icon, box)
        if button.IconMask then
            button.icon:RemoveMaskTexture(button.IconMask)
            button.IconMask:Hide()
        end
    end

    -- StyledHotkey label.
    SetButtonHotkeyText(button, box, barNum, index)

    -- Anchor every non-icon texture and every child to the box.
    for _, region in ipairs({button:GetRegions()}) do
        if region and region ~= button.icon and region ~= button.Icon
           and region ~= button.IconMask
           and region ~= button.HotKey and region ~= button.Count
           and region ~= button.Name and region ~= button.StyledHotkey
           and region.IsObjectType and region:IsObjectType("Texture") then
            AnchorRegionToBox(region, box)
        end
    end
    for _, child in ipairs({button:GetChildren()}) do
        if child then
            child:ClearAllPoints()
            child:SetPoint("TOPLEFT", box, "TOPLEFT", 0, 0)
            child:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", 0, 0)
        end
    end

    -- StyledIconHighlight on the box overlay layer.
    if not button.StyledIconHighlight and button.icon then
        button.StyledIconHighlight = box:CreateTexture(nil, "OVERLAY")
        button.StyledIconHighlight:SetPoint("TOPLEFT", box, "TOPLEFT", 0, 0)
        button.StyledIconHighlight:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", 0, 0)
        button.StyledIconHighlight:SetBlendMode("ADD")
        button.StyledIconHighlight:SetDrawLayer("OVERLAY", 7)
    end
    if button.StyledIconHighlight then
        local hR, hG, hB, hA = unpack(theme.highlight)
        if IsHiddenEmptySlot(box, button) then
            button.StyledIconHighlight:SetColorTexture(0, 0, 0, 0)
            button.StyledIconHighlight:SetAlpha(0)
            if button.SetHighlightTexture then
                if not button._muiEmptyHighlight then
                    button._muiEmptyHighlight = button:CreateTexture(nil, "HIGHLIGHT")
                    button._muiEmptyHighlight:SetColorTexture(0, 0, 0, 0)
                end
                button:SetHighlightTexture(button._muiEmptyHighlight)
            end
        else
            button.StyledIconHighlight:SetColorTexture(hR, hG, hB, math_max(hA or 0.35, 0.65))
            button.StyledIconHighlight:SetAlpha(0)
            if button.SetHighlightTexture then
                button:SetHighlightTexture(button.StyledIconHighlight)
            end
        end
    end

    InstallButtonHoverAnimation(button, box, theme)

    -- Cooldown anchored to box.
    if button.cooldown then
        button.cooldown:ClearAllPoints()
        button.cooldown:SetAllPoints(box)
        button.cooldown:SetDrawEdge(false)
        button.cooldown:SetDrawSwipe(true)
        button.cooldown:SetHideCountdownNumbers(false)
    end

    -- Count font + position.
    if button.Count then
        button.Count:SetFont(STANDARD_TEXT_FONT, 14, "OUTLINE")
        button.Count:SetTextColor(1, 1, 1)
        button.Count:SetShadowOffset(1, -1)
        button.Count:SetShadowColor(0, 0, 0, 1)
        button.Count:ClearAllPoints()
        button.Count:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -2, 2)
    end

    -- Name font + position.
    if button.Name then
        button.Name:SetFont(STANDARD_TEXT_FONT, 9, "OUTLINE")
        button.Name:SetTextColor(0.85, 0.90, 1.00)
        button.Name:SetShadowOffset(1, -1)
        button.Name:SetShadowColor(0, 0, 0, 0.8)
        button.Name:ClearAllPoints()
        button.Name:SetPoint("BOTTOM", box, "BOTTOM", 0, 2)
    end
end

-- ============================================================
-- SECTION N: Pet Button Styling
-- ============================================================

local function UpdatePetCheckedTexture(button)
    if not button then return end
    local overlay = button._muiPetOverlay or (button._muiBox and button._muiBox.petActiveOverlay)
    local idx = button._muiPetIndex or button.action
    if not idx or not GetPetActionInfo then
        if overlay then overlay:SetAlpha(0) end
        return
    end
    local name, _, _, isActive, _, autoCastEnabled = GetPetActionInfo(idx)
    local isMomentary = (name == "PET_ACTION_MOVE_TO")
    if (isActive and not autoCastEnabled and not isMomentary)
       or (isMomentary and button._muiMoveToHighlight) then
        if overlay then overlay:SetAlpha(1) end
    else
        if overlay then overlay:SetAlpha(0) end
    end
end

local function InstallPetGlobalHooks()
    if not petCheckedHooked and type(_G.PetActionButton_Update) == "function" then
        petCheckedHooked = true
        hooksecurefunc("PetActionButton_Update", function(btn)
            UpdatePetCheckedTexture(btn)
            if _G.MidnightUI_RefreshPetBarHotkeys then
                _G.MidnightUI_RefreshPetBarHotkeys()
            end
        end)
    end
    if not moveToHooked and type(_G.PetActionButton_OnClick) == "function" then
        moveToHooked = true
        hooksecurefunc("PetActionButton_OnClick", function(btn)
            local idx = btn and (btn._muiPetIndex or btn.action)
            if not idx or not GetPetActionInfo then return end
            local name = select(1, GetPetActionInfo(idx))
            if name == "PET_ACTION_MOVE_TO" then
                btn._muiMoveToHighlight = true
                UpdatePetCheckedTexture(btn)
            else
                for i = 1, 10 do
                    local b = _G["PetActionButton" .. i]
                    if b then
                        b._muiMoveToHighlight = false
                        UpdatePetCheckedTexture(b)
                    end
                end
            end
        end)
    end
end

local function StylePetButton(button, box, index, theme)
    if not button or not box then return end
    button._muiPetIndex = index
    button._muiBox = box

    -- One-time mouse setup. Pet buttons don't get the same
    -- child-mouse-disable treatment as main buttons because pet
    -- behavior (autocast indicator etc.) needs its own clicks.
    if not button._muiMouseFix then
        button._muiMouseFix = true
        button:EnableMouse(true)
        button:SetHitRectInsets(0, 0, 0, 0)
        button:SetScale(1)
        button._muiSyncToBox = function() SyncButtonToBox(button, box) end
        button:HookScript("OnShow", function() C_Timer.After(0, button._muiSyncToBox) end)
    end

    -- Hide Blizzard pet button slot art and borders.
    if button.SlotBackground then button.SlotBackground:SetAlpha(0) end
    if button.Border then button.Border:SetAlpha(0) end
    if button.iconBorder then button.iconBorder:SetAlpha(0); button.iconBorder:Hide() end
    if button.IconBorder then button.IconBorder:SetAlpha(0); button.IconBorder:Hide() end
    if button.HighlightTexture then
        button.HighlightTexture:SetAlpha(0)
        if button.HighlightTexture.Hide then button.HighlightTexture:Hide() end
    end
    if button.GetHighlightTexture then
        local hl = button:GetHighlightTexture()
        if hl and hl ~= button.StyledIconHighlight then hl:SetAlpha(0) end
    end
    if button.CheckedTexture then button.CheckedTexture:SetAlpha(0) end
    if button.GetNormalTexture then
        local nt = button:GetNormalTexture()
        if nt then nt:SetAlpha(1) end
    end

    -- Icon: anchor to box, keep IconMask for the rounded look.
    if not button.icon then
        button.icon = button.Icon or nil
    end
    if button.icon then
        button.icon:SetTexCoord(0, 1, 0, 1)
        AnchorRegionToBox(button.icon, box)
        if button.IconMask then
            button.IconMask:ClearAllPoints()
            button.IconMask:SetAllPoints(button.icon)
            button.IconMask:Show()
        end
    end

    -- Create the active-state and hover overlays once per box.
    if not box.petActiveOverlay then
        local active = box:CreateTexture(nil, "OVERLAY", nil, 4)
        active:SetPoint("TOPLEFT", 4, -4)
        active:SetPoint("BOTTOMRIGHT", -4, 4)
        active:SetColorTexture(1, 0.9, 0.1, 1.0)
        active:SetAlpha(0)
        box.petActiveOverlay = active
    end
    if not box.petHoverOverlay then
        local h = box:CreateTexture(nil, "OVERLAY", nil, 6)
        h:SetPoint("TOPLEFT", 4, -4)
        h:SetPoint("BOTTOMRIGHT", -4, 4)
        h:SetColorTexture(1, 1, 1, 0.15)
        h:SetAlpha(0)
        box.petHoverOverlay = h
    end
    button._muiPetOverlay = box.petActiveOverlay

    -- AutoCastable / AutoCastShine anchored to box.
    if button.AutoCastable then
        button.AutoCastable:ClearAllPoints()
        button.AutoCastable:SetAllPoints(box)
    end
    if button.AutoCastShine then
        button.AutoCastShine:ClearAllPoints()
        button.AutoCastShine:SetAllPoints(box)
    end

    -- Hover hooks (one-time).
    if not button._muiPetHoverHooked then
        button._muiPetHoverHooked = true
        button:HookScript("OnEnter", function(btn)
            local overlay = btn._muiBox and btn._muiBox.petHoverOverlay
            if overlay then overlay:SetAlpha(1) end
        end)
        button:HookScript("OnLeave", function(btn)
            local overlay = btn._muiBox and btn._muiBox.petHoverOverlay
            if overlay then overlay:SetAlpha(0) end
        end)
    end

    InstallPetGlobalHooks()
    UpdatePetCheckedTexture(button)

    -- Hotkey label for pet button.
    if not button.StyledHotkey then
        button.StyledHotkey = button:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        button.StyledHotkey:SetPoint("TOPRIGHT", box, "TOPRIGHT", -2, -2)
        button.StyledHotkey:SetFont(STANDARD_TEXT_FONT, 10, "OUTLINE")
        button.StyledHotkey:SetTextColor(0.85, 0.90, 1.00)
        button.StyledHotkey:SetShadowOffset(1, -1)
        button.StyledHotkey:SetShadowColor(0, 0, 0, 0.8)
    end
    button.StyledHotkey:SetText(ShortBindingText(GetBindingKey("BONUSACTIONBUTTON" .. index)))
    if button.HotKey then
        button.HotKey:SetText("")
        button.HotKey:SetAlpha(0)
    end
end

-- ============================================================
-- SECTION O: Stance Button Styling
-- ============================================================

local function StyleStanceButton(button, box, index, theme, isActive)
    if not button or not box then return end
    button._muiBox = box

    if not button._muiMouseFix then
        button._muiMouseFix = true
        button:EnableMouse(true)
        button:SetHitRectInsets(0, 0, 0, 0)
        button:SetScale(1)
    end

    if button.SlotBackground then button.SlotBackground:SetAlpha(0) end
    if button.Border then button.Border:SetAlpha(0) end
    if button.iconBorder then button.iconBorder:SetAlpha(0); button.iconBorder:Hide() end
    if button.IconBorder then button.IconBorder:SetAlpha(0); button.IconBorder:Hide() end
    if button.HighlightTexture then
        button.HighlightTexture:SetAlpha(0)
        if button.HighlightTexture.Hide then button.HighlightTexture:Hide() end
    end
    if button.GetHighlightTexture then
        local hl = button:GetHighlightTexture()
        if hl then hl:SetAlpha(0) end
    end
    if button.CheckedTexture then button.CheckedTexture:SetAlpha(0) end
    if button.GetNormalTexture then
        local nt = button:GetNormalTexture()
        if nt then nt:SetAlpha(0) end
    end

    if not button.icon then
        button.icon = button.Icon or nil
    end
    if button.icon then
        button.icon:SetTexCoord(0, 1, 0, 1)
        AnchorRegionToBox(button.icon, box)
        if button.IconMask then
            button.IconMask:ClearAllPoints()
            button.IconMask:SetAllPoints(button.icon)
            button.IconMask:Show()
        end
    end

    if not box.stanceHoverOverlay then
        local h = box:CreateTexture(nil, "OVERLAY", nil, 6)
        h:SetPoint("TOPLEFT", 4, -4)
        h:SetPoint("BOTTOMRIGHT", -4, 4)
        h:SetColorTexture(1, 1, 1, 0.15)
        h:SetAlpha(0)
        box.stanceHoverOverlay = h
    end

    if not button._muiStanceHoverHooked then
        button._muiStanceHoverHooked = true
        button:HookScript("OnEnter", function(btn)
            local overlay = btn._muiBox and btn._muiBox.stanceHoverOverlay
            if overlay then overlay:SetAlpha(1) end
        end)
        button:HookScript("OnLeave", function(btn)
            local overlay = btn._muiBox and btn._muiBox.stanceHoverOverlay
            if overlay then overlay:SetAlpha(0) end
        end)
    end

    -- Active form: gold border. Otherwise: theme border.
    if box.stanceThinBorder and theme and theme.mainBorder then
        if isActive then
            box.stanceThinBorder:SetColorTexture(1, 0.9, 0.1, 1.0)
        else
            local r, g, b, a = unpack(theme.mainBorder)
            box.stanceThinBorder:SetColorTexture(r, g, b, math_min(a or 1, 0.6))
        end
    end
end

-- ============================================================
-- SECTION P: Bar Builders
-- ============================================================

local function ReleaseButtonToBlizzard(button)
    if not button or not button._muiOrigParent then return end
    local origParent = button._muiOrigParent
    button:SetParent(origParent)
    button:ClearAllPoints()
    button:Show()
    if button.EnableMouse then button:EnableMouse(true) end
    if button.SetFrameStrata then
        button:SetFrameStrata(button._muiOrigStrata or "LOW")
    end
    if button.StyledHotkey then button.StyledHotkey:Hide() end
    RestoreButtonSlotArt(button)
    if origParent and IsFrameMuted(origParent) then
        UnmuteFrame(origParent)
    end
end

local function BuildActionBar(barNum)
    if IsInCombat() then
        Defer("BuildActionBar" .. barNum, function() BuildActionBar(barNum) end)
        return
    end

    local config = GetBarConfig(barNum)
    local prefix = BAR_BUTTON_MAP[barNum]
    if not prefix then return end

    -- Disable path: hide our container, return buttons to Blizzard.
    if not config or config.enabled == false then
        local container = actionBarContainers[barNum]
        if container then container:Hide() end
        if actionBarBoxes[barNum] then
            for i = 1, 12 do
                if actionBarBoxes[barNum][i] then actionBarBoxes[barNum][i]:Hide() end
            end
        end
        local releasedParent
        for i = 1, 12 do
            local button = _G[prefix .. i]
            if button and button._muiOrigParent then
                releasedParent = button._muiOrigParent
                ReleaseButtonToBlizzard(button)
            end
        end
        if releasedParent then
            if releasedParent.UpdateGridLayout then pcall(releasedParent.UpdateGridLayout, releasedParent) end
            if releasedParent.LayoutMyButtons then pcall(releasedParent.LayoutMyButtons, releasedParent) end
        end
        return
    end

    -- Enable path.
    local theme = ResolveTheme(barNum)
    local boxSize = BASE_BUTTON_SIZE
    local spacing = config.spacing or 6
    local rows = config.rows or 1
    local iconsPerRow = config.iconsPerRow or 12
    local containerWidth = (boxSize * iconsPerRow) + (spacing * (iconsPerRow - 1))
    local containerHeight = (boxSize * rows) + (spacing * (rows - 1))

    -- Create container once per bar.
    if not actionBarContainers[barNum] then
        local container = CreateFrame("Frame", "MyActionBarContainer" .. barNum, UIParent)
        container:SetMovable(true)
        container:EnableMouse(false)
        container:SetClampedToScreen(true)
        actionBarContainers[barNum] = container
        actionBarBoxes[barNum] = {}
        actionBarButtons[barNum] = {}
        _G["MyActionBarContainer" .. barNum] = container

        local MR = _G.MidnightUI_MoverRegistry
        if MR and MR.RegisterMover then
            MR.RegisterMover("ActionBar" .. barNum, container,
                PositionToDefaultString(DEFAULT_POSITIONS[barNum]),
                {
                    category = "bars",
                    label = "Action Bar " .. barNum,
                    onLock = function(f) f:EnableMouse(false); f:RegisterForDrag() end,
                    onUnlock = function(f) f:EnableMouse(true); f:RegisterForDrag("LeftButton") end,
                    shouldShowOverlay = function(f) return f:IsShown() end,
                })
        end
    end

    local container = actionBarContainers[barNum]
    ApplySavedPoint(container, DEFAULT_POSITIONS[barNum], "ActionBar" .. barNum, config.position)
    container:SetSize(containerWidth, containerHeight)
    container:SetScale((config.scale or 100) / 100)
    container:Show()

    -- Lay out buttons in the grid.
    local buttonIndex = 0
    for row = 1, rows do
        for col = 1, iconsPerRow do
            buttonIndex = buttonIndex + 1
            if buttonIndex > 12 then break end
            local button = _G[prefix .. buttonIndex]
            if button then
                button:Show()
                if button.EnableMouse then button:EnableMouse(true) end

                if not actionBarBoxes[barNum][buttonIndex] then
                    actionBarBoxes[barNum][buttonIndex] = CreateStyledBox(
                        container,
                        "StyledActionBox_Bar" .. barNum .. "_" .. buttonIndex,
                        boxSize, theme)
                end
                local box = actionBarBoxes[barNum][buttonIndex]

                if button:GetParent() ~= container then
                    if not button._muiOrigParent then
                        button._muiOrigParent = button:GetParent()
                    end
                    button:SetParent(container)
                end
                local origParent = button._muiOrigParent
                if origParent and not IsFrameMuted(origParent) then
                    if not button._muiOrigStrata and button.GetFrameStrata then
                        button._muiOrigStrata = button:GetFrameStrata()
                    end
                    MuteFrame(origParent, { keepAlpha = true })
                end

                box:SetSize(boxSize, boxSize)
                box:SetScale(1)
                button:SetSize(boxSize, boxSize)
                button:SetScale(1)
                button:SetHitRectInsets(0, 0, 0, 0)
                box._muiBoundButton = button
                actionBarButtons[barNum][buttonIndex] = button

                box:ClearAllPoints()
                local xOff = (col - 1) * (boxSize + spacing)
                local yOff = -(row - 1) * (boxSize + spacing)
                box:SetPoint("TOPLEFT", container, "TOPLEFT", xOff, yOff)
                button:ClearAllPoints()
                button:SetPoint("TOPLEFT", box, "TOPLEFT", 0, 0)
                button:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", 0, 0)
                box:SetFrameLevel(math_max(0, button:GetFrameLevel() - 1))
                button:SetFrameLevel(box:GetFrameLevel() + 2)

                ApplyBoxTheme(box, theme, boxSize)
                StyleMainButton(button, box, barNum, buttonIndex, theme)
                if button._muiSyncToBox then
                    C_Timer.After(0, button._muiSyncToBox)
                end
                box:Show()
            end
        end
    end

    -- Hide unused button slots.
    for i = buttonIndex + 1, 12 do
        if actionBarBoxes[barNum][i] then actionBarBoxes[barNum][i]:Hide() end
        local button = _G[prefix .. i]
        if button then
            button:Hide()
            if button.EnableMouse then button:EnableMouse(false) end
        end
    end
end

local function EnsureOverlayContainer(existing, frameName, settings, defaultPosition, moverKey)
    if existing then return existing end
    local container = CreateFrame("Frame", frameName, UIParent)
    container:SetMovable(true)
    container:SetClampedToScreen(true)
    container:SetAlpha(settings.alpha or 1.0)
    container:SetScale((settings.scale or 100) / 100)
    ApplySavedPoint(container, defaultPosition, moverKey, settings.position)
    return container
end

local function ApplyProtectedHostLayout(host, container, width, height, queueKey)
    if not host or not container then return false end
    if IsInCombat() then
        Defer(queueKey, function() ApplyProtectedHostLayout(host, container, width, height, queueKey) end)
        return false
    end
    if host:GetParent() ~= container then host:SetParent(container) end
    host:ClearAllPoints()
    host:SetPoint("TOPLEFT", container, "TOPLEFT", 0, 0)
    host:SetSize(width, height)
    return true
end

-- MoverRegistry registration for pet and stance containers. Idempotent
-- (MR.RegisterMover returns early if the same frame is already
-- registered), so calling on every rebuild is safe. The registration
-- is what creates the drag overlay frame and wires up the right-click
-- settings popup; without it, the overlay popup never appears.
local function RegisterPetBarMover(container)
    local MR = _G.MidnightUI_MoverRegistry
    if not MR or not MR.RegisterMover or not container then return end
    MR.RegisterMover("PetBar", container,
        PositionToDefaultString(PET_DEFAULT_POSITION),
        {
            category = "bars",
            label = "Pet Bar",
            onLock = function(f) f:EnableMouse(false); f:RegisterForDrag() end,
            onUnlock = function(f) f:EnableMouse(true); f:RegisterForDrag("LeftButton") end,
            shouldShowOverlay = function() return PlayerHasPetClass() end,
        })
end

local function RegisterStanceBarMover(container)
    local MR = _G.MidnightUI_MoverRegistry
    if not MR or not MR.RegisterMover or not container then return end
    MR.RegisterMover("StanceBar", container,
        PositionToDefaultString(STANCE_DEFAULT_POSITION),
        {
            category = "bars",
            label = "Stance Bar",
            onLock = function(f) f:EnableMouse(false); f:RegisterForDrag() end,
            onUnlock = function(f) f:EnableMouse(true); f:RegisterForDrag("LeftButton") end,
            shouldShowOverlay = function() return PlayerHasStanceClass() end,
        })
end

local function BuildPetBar()
    local settings = EnsurePetSettings()
    if not settings then return end
    local petHost = _G.PetActionBarFrame or _G.PetActionBar
    if settings.enabled == false then
        -- MUI pet bar off: restore Blizzard pet bar interactivity.
        if petHost and not IsInCombat() then UnmuteFrame(petHost) end
        if petBarContainer and not IsInCombat() then
            petBarContainer:SetAlpha(0)
            if petBarContainer.dragOverlay then petBarContainer.dragOverlay:Hide() end
        end
        return
    end
    if IsInCombat() then
        Defer("PetBarBuild", BuildPetBar)
        return
    end
    -- MUI pet bar on: disable mouse on Blizzard's host frame so its
    -- (often empty) hit area can't intercept clicks meant for action
    -- buttons sitting visually behind it.
    if petHost then
        MuteFrame(petHost, { keepAlpha = true, keepStrata = true })
    end

    local buttonSize = settings.buttonSize or 32
    local boxSize = math_floor(buttonSize + 0.5)
    local spacing = settings.spacing or 6
    local buttonsPerRow = math_max(1, math_min(settings.buttonsPerRow or 10, 10))
    local rows = math_ceil(10 / buttonsPerRow)
    local containerWidth = (buttonSize * buttonsPerRow) + (spacing * (buttonsPerRow - 1))
    local containerHeight = (buttonSize * rows) + (spacing * (rows - 1))

    if PlayerHasPetClass() and OverlaysUnlocked() then
        petBarContainer = EnsureOverlayContainer(petBarContainer, "MyPetBarContainer",
            settings, PET_DEFAULT_POSITION, "PetBar")
        _G.MyPetBarContainer = petBarContainer
        RegisterPetBarMover(petBarContainer)
    end

    if not PetHasActionBar or not PetHasActionBar() then
        if petBarContainer then
            if PlayerHasPetClass() and OverlaysUnlocked() then
                petBarContainer:SetSize(containerWidth, containerHeight)
                petBarContainer:SetAlpha(settings.alpha or 1.0)
                petBarContainer:SetScale((settings.scale or 100) / 100)
            else
                petBarContainer:SetAlpha(0)
            end
        end
        return
    end

    local theme = ResolveTheme()
    local petHost = _G.PetActionBarFrame or _G.PetActionBar
    if not petHost then
        if petBarContainer then petBarContainer:SetAlpha(0) end
        return
    end

    if not petBarContainer then
        petBarContainer = EnsureOverlayContainer(petBarContainer, "MyPetBarContainer",
            settings, PET_DEFAULT_POSITION, "PetBar")
        _G.MyPetBarContainer = petBarContainer
        RegisterPetBarMover(petBarContainer)
    end

    petBarContainer:SetSize(containerWidth, containerHeight)
    petBarContainer:SetAlpha(settings.alpha or 1.0)
    petBarContainer:SetScale((settings.scale or 100) / 100)

    ApplyProtectedHostLayout(petHost, petBarContainer, containerWidth, containerHeight, "PetHostLayout")
    if not petHost._muiLayoutHooked and type(_G.PetActionBar_UpdateLayout) == "function" then
        petHost._muiLayoutHooked = true
        hooksecurefunc("PetActionBar_UpdateLayout", function()
            if petBarContainer and petHost and not IsInCombat() then
                ApplyProtectedHostLayout(petHost, petBarContainer,
                    petBarContainer:GetWidth(), petBarContainer:GetHeight(), "PetHostLayout")
            end
        end)
    end
    if petHost.SetButtonSpacing then petHost:SetButtonSpacing(spacing)
    elseif _G.PetActionBar_SetButtonSpacing then _G.PetActionBar_SetButtonSpacing(spacing) end
    if _G.PetActionBar_UpdateLayout then _G.PetActionBar_UpdateLayout()
    elseif _G.PetActionBar_Update then _G.PetActionBar_Update() end

    petHost:SetAlpha(1)
    for _, region in ipairs({petHost:GetRegions()}) do
        if region and region.IsObjectType and region:IsObjectType("Texture") then
            region:SetAlpha(0)
        end
    end

    for i = 1, 10 do
        local button = _G["PetActionButton" .. i]
        if button then
            if IsInCombat() and button:IsProtected() then
                Defer("PetButton" .. i, BuildPetBar)
                return
            end
            if not petBarBoxes[i] then
                petBarBoxes[i] = CreateStyledBox(petBarContainer,
                    "StyledPetBox_" .. i, boxSize, theme)
            end
            local box = petBarBoxes[i]
            box:SetSize(boxSize, boxSize)
            ApplyBoxTheme(box, theme, boxSize)
            TuneCompactBox(box, theme, "petThinBorder", 4, 0.4)

            local row = math_floor((i - 1) / buttonsPerRow)
            local col = (i - 1) % buttonsPerRow

            if not IsInCombat() then button:SetParent(petHost) end
            button:ClearAllPoints()
            button:SetPoint("TOPLEFT", petBarContainer, "TOPLEFT",
                col * (buttonSize + spacing), -row * (buttonSize + spacing))
            button:SetSize(buttonSize, buttonSize)

            box:ClearAllPoints()
            box:SetPoint("TOPLEFT", petBarContainer, "TOPLEFT",
                col * (buttonSize + spacing) + (buttonSize - boxSize) / 2,
                -row * (buttonSize + spacing) - (buttonSize - boxSize) / 2)
            box:Show()

            StylePetButton(button, box, i, theme)

            if not button._muiDragHooked then
                button._muiDragHooked = true
                local applyLayout = function()
                    if petBarContainer and petHost then
                        ApplyProtectedHostLayout(petHost, petBarContainer,
                            containerWidth, containerHeight, "PetHostLayout")
                    end
                end
                button:HookScript("OnDragStart", applyLayout)
                button:HookScript("OnDragStop", applyLayout)
            end
        end
    end
end

local function BuildStanceBar()
    if IsInCombat() then
        Defer("BuildStanceBar", BuildStanceBar)
        return
    end
    local settings = EnsureStanceSettings()
    if not settings then return end
    local stanceHost = _G.StanceBar or _G.StanceBarFrame
    if settings.enabled == false then
        -- MUI stance bar off: restore Blizzard stance bar interactivity.
        if stanceHost and not IsInCombat() then UnmuteFrame(stanceHost) end
        if stanceBarContainer then stanceBarContainer:Hide() end
        return
    end
    -- MUI stance bar on: disable mouse on Blizzard's host frame so its
    -- (often empty) hit area can't intercept clicks meant for action
    -- buttons sitting visually behind it.
    if stanceHost and not IsInCombat() then
        MuteFrame(stanceHost, { keepAlpha = true, keepStrata = true })
    end

    local numForms = GetNumShapeshiftForms and GetNumShapeshiftForms() or 0
    local buttonSize = settings.buttonSize or 32
    local boxSize = math_floor((buttonSize * 0.8) + 0.5)
    local spacing = settings.spacing or 4
    local buttonsPerRow = math_max(1, math_min(settings.buttonsPerRow or math_max(numForms, 1), 10))
    if numForms > 0 then buttonsPerRow = math_min(buttonsPerRow, numForms) end
    local previewCount = (numForms > 0) and numForms or buttonsPerRow
    local cols = math_min(buttonsPerRow, previewCount)
    local rows = math_ceil(previewCount / buttonsPerRow)
    local containerWidth = (buttonSize * cols) + (spacing * (cols - 1))
    local containerHeight = (buttonSize * rows) + (spacing * (rows - 1))

    if PlayerHasStanceClass() and OverlaysUnlocked() then
        stanceBarContainer = EnsureOverlayContainer(stanceBarContainer, "MyStanceBarContainer",
            settings, STANCE_DEFAULT_POSITION, "StanceBar")
        _G.MyStanceBarContainer = stanceBarContainer
        RegisterStanceBarMover(stanceBarContainer)
    end
    if numForms <= 0 then
        if stanceBarContainer then
            if PlayerHasStanceClass() and OverlaysUnlocked() then
                stanceBarContainer:SetSize(containerWidth, containerHeight)
                stanceBarContainer:Show()
            else
                stanceBarContainer:Hide()
            end
        end
        return
    end

    local theme = ResolveTheme()
    rows = math_ceil(numForms / buttonsPerRow)
    containerWidth = (buttonSize * buttonsPerRow) + (spacing * (buttonsPerRow - 1))
    containerHeight = (buttonSize * rows) + (spacing * (rows - 1))

    local stanceHost = _G.StanceBar or _G.StanceBarFrame
    if not stanceHost then
        if stanceBarContainer then stanceBarContainer:Hide() end
        return
    end

    if not stanceBarContainer then
        stanceBarContainer = EnsureOverlayContainer(stanceBarContainer, "MyStanceBarContainer",
            settings, STANCE_DEFAULT_POSITION, "StanceBar")
        _G.MyStanceBarContainer = stanceBarContainer
        RegisterStanceBarMover(stanceBarContainer)
    end

    stanceBarContainer:SetSize(containerWidth, containerHeight)
    stanceBarContainer:SetAlpha(settings.alpha or 1.0)
    stanceBarContainer:SetScale((settings.scale or 100) / 100)

    ApplyProtectedHostLayout(stanceHost, stanceBarContainer, containerWidth, containerHeight, "StanceHostLayout")

    stanceHost:SetAlpha(1)
    for _, region in ipairs({stanceHost:GetRegions()}) do
        if region and region.IsObjectType and region:IsObjectType("Texture") then
            region:SetAlpha(0)
        end
    end

    local activeIdx = GetShapeshiftForm and GetShapeshiftForm() or 0

    for i = 1, numForms do
        local button = _G["StanceButton" .. i]
        if button then
            if IsInCombat() and button:IsProtected() then
                Defer("StanceButton" .. i, BuildStanceBar)
                return
            end
            if not stanceBarBoxes[i] then
                stanceBarBoxes[i] = CreateStyledBox(stanceBarContainer,
                    "StyledStanceBox_" .. i, boxSize, theme)
            end
            local box = stanceBarBoxes[i]
            box:SetSize(boxSize, boxSize)
            ApplyBoxTheme(box, theme, boxSize)
            TuneCompactBox(box, theme, "stanceThinBorder", 3, 0.6)

            local row = math_floor((i - 1) / buttonsPerRow)
            local col = (i - 1) % buttonsPerRow

            if not IsInCombat() then button:SetParent(stanceHost) end
            button:ClearAllPoints()
            button:SetPoint("TOPLEFT", stanceBarContainer, "TOPLEFT",
                col * (buttonSize + spacing), -row * (buttonSize + spacing))
            button:SetSize(buttonSize, buttonSize)

            box:ClearAllPoints()
            box:SetPoint("TOPLEFT", stanceBarContainer, "TOPLEFT",
                col * (buttonSize + spacing) + (buttonSize - boxSize) / 2,
                -row * (buttonSize + spacing) - (buttonSize - boxSize) / 2)
            box:Show()

            StyleStanceButton(button, box, i, theme, i == activeIdx)
            button:Show()
        end
    end

    for i = numForms + 1, 10 do
        if stanceBarBoxes[i] then stanceBarBoxes[i]:Hide() end
    end

    stanceBarContainer:Show()
end

local function Rebuild(scope)
    scope = scope or "all"
    if scope == "all" then
        for n = 1, 8 do BuildActionBar(n) end
        BuildPetBar()
        BuildStanceBar()
        ApplySuppression()
        return
    end
    if scope == "pet" then BuildPetBar(); ApplySuppression(); return end
    if scope == "stance" then BuildStanceBar(); ApplySuppression(); return end
    local barNum = tonumber(string.match(tostring(scope), "^bar(%d+)$"))
    if barNum then BuildActionBar(barNum); ApplySuppression(); return end
end

-- ============================================================
-- SECTION Q: Hotkey refresh + Pet bar public refresh
-- ============================================================

local function RefreshAllHotkeyText()
    for barNum = 1, 8 do
        local prefix = BAR_BUTTON_MAP[barNum]
        if prefix and actionBarButtons[barNum] then
            for i = 1, 12 do
                local button = actionBarButtons[barNum][i]
                local box = actionBarBoxes[barNum] and actionBarBoxes[barNum][i]
                if button and box and button.StyledHotkey then
                    SetButtonHotkeyText(button, box, barNum, i)
                end
            end
        end
    end
    if _G.MidnightUI_RefreshPetBarHotkeys then
        _G.MidnightUI_RefreshPetBarHotkeys()
    end
end

function MidnightUI_RefreshPetBarHotkeys()
    for i = 1, 10 do
        local btn = _G["PetActionButton" .. i]
        if btn then
            local key = GetBindingKey and GetBindingKey("BONUSACTIONBUTTON" .. i)
            local display = ShortBindingText(key)
            if btn.StyledHotkey then
                btn.StyledHotkey:SetText(display)
                btn.StyledHotkey:Show()
            end
            if btn.HotKey then btn.HotKey:SetText(""); btn.HotKey:SetAlpha(0) end
        end
    end
end

-- ============================================================
-- SECTION R: Hidden Empty Slot Drag Discovery
-- When the player picks up a spell/item, hidden-style empty
-- slots light up faintly so they're discoverable.
-- ============================================================

local function SetHiddenBoxesDragVisible(visible)
    if hiddenDragActive == visible then return end
    hiddenDragActive = visible
    for barNum = 1, 8 do
        local boxes = actionBarBoxes[barNum]
        if boxes then
            for i = 1, 12 do
                local box = boxes[i]
                if box and box._muiStyle == "Hidden" and box:IsShown() then
                    if visible then
                        if box.mainBorder then box.mainBorder:SetColorTexture(0.45, 0.75, 1.00, 0.55) end
                        if box.outerBorder then box.outerBorder:SetColorTexture(0.30, 0.55, 0.85, 0.35) end
                    else
                        if box.mainBorder then box.mainBorder:SetColorTexture(0, 0, 0, 0) end
                        if box.outerBorder then box.outerBorder:SetColorTexture(0, 0, 0, 0) end
                    end
                end
            end
        end
    end
end

local function CheckCursorForDrag()
    local cursorType = GetCursorInfo()
    local interestingCursor = cursorType == "spell" or cursorType == "item"
        or cursorType == "mount" or cursorType == "macro"
        or cursorType == "companion" or cursorType == "petaction"
        or cursorType == "flyout" or cursorType == "equipmentset"
        or cursorType == "toybox"
    SetHiddenBoxesDragVisible(interestingCursor == true)
end

-- ============================================================
-- SECTION S: Range Coloring
-- Throttled 0.05s OnUpdate. Cheap and event-bounded.
-- ============================================================

do
    local rangeFrame = CreateFrame("Frame")
    local OOR_R, OOR_G, OOR_B = 0.8, 0.1, 0.1
    local OOM_R, OOM_G, OOM_B = 0.4, 0.4, 0.4
    local OK_R, OK_G, OK_B = 1.0, 1.0, 1.0
    local rangeCache = {}
    local THROTTLE = 0.05
    local elapsed = 0

    local function GetRangeStrategy(action)
        local cached = rangeCache[action]
        if cached then return cached end
        local entry = { id = 0, distIdx = 0 }
        local actionType, id = GetActionInfo(action)
        if actionType == "spell" and id then
            local resolvedID = id
            if _G.C_Spell and _G.C_Spell.GetOverrideSpell then
                local ov = _G.C_Spell.GetOverrideSpell(id)
                if ov and ov > 0 then resolvedID = ov end
            end
            entry.id = resolvedID
            if _G.C_Spell and _G.C_Spell.GetSpellInfo then
                local ok, info = pcall(_G.C_Spell.GetSpellInfo, resolvedID)
                if ok and info and info.maxRange and info.maxRange > 0 then
                    entry.distIdx = (info.maxRange <= 8) and 3 or 4
                end
            end
        end
        rangeCache[action] = entry
        return entry
    end

    rangeFrame:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
    rangeFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
    rangeFrame:RegisterEvent("UPDATE_BONUS_ACTIONBAR")
    rangeFrame:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
    rangeFrame:SetScript("OnEvent", function() rangeCache = {} end)

    rangeFrame:SetScript("OnUpdate", function(_, dt)
        elapsed = elapsed + dt
        if elapsed < THROTTLE then return end
        elapsed = 0

        local hasTarget = UnitExists("target")
        for barNum = 1, 8 do
            local prefix = BAR_BUTTON_MAP[barNum]
            if prefix then
                for i = 1, 12 do
                    local button = _G[prefix .. i]
                    if button and button:IsVisible() then
                        local icon = button.Icon or button.icon
                        local action = button.action
                        if icon and action and HasAction(action) then
                            local inRange = IsActionInRange(action)
                            if inRange == nil and hasTarget then
                                local strat = GetRangeStrategy(action)
                                if strat.id > 0 then
                                    if _G.C_Spell and _G.C_Spell.IsSpellInRange then
                                        local ok, r = pcall(_G.C_Spell.IsSpellInRange, strat.id, "target")
                                        if ok and r ~= nil then inRange = r end
                                    end
                                    if inRange == nil and strat.distIdx > 0 and not IsInCombat() then
                                        inRange = CheckInteractDistance("target", strat.distIdx) or false
                                    end
                                end
                            end
                            if inRange == false then
                                icon:SetVertexColor(OOR_R, OOR_G, OOR_B)
                                icon:SetDesaturated(false)
                                button._muiOOR = true
                            elseif button._muiOOR then
                                button._muiOOR = nil
                                local isUsable, notEnoughMana = IsUsableAction(action)
                                if isUsable then
                                    icon:SetVertexColor(OK_R, OK_G, OK_B)
                                    icon:SetDesaturated(false)
                                elseif notEnoughMana then
                                    icon:SetVertexColor(OOM_R, OOM_G, OOM_B)
                                    icon:SetDesaturated(false)
                                else
                                    icon:SetVertexColor(OK_R, OK_G, OK_B)
                                    icon:SetDesaturated(true)
                                end
                            end
                        end
                    end
                end
            end
        end
    end)
end

-- ============================================================
-- SECTION T: Cast Sweep
-- Amber progress bar swept across button during cast/channel.
-- ============================================================

do
    local SWEEP_COLOR = { 0.95, 0.72, 0.15, 0.62 }

    local function EnsureSweep(box)
        if box._muiSweep then return box._muiSweep end
        local clip = CreateFrame("Frame", nil, box)
        clip:SetPoint("TOPLEFT")
        clip:SetPoint("BOTTOMLEFT")
        clip:SetWidth(1)
        clip:SetClipsChildren(true)
        clip:SetFrameLevel(box:GetFrameLevel() + 5)
        clip:Hide()

        local tex = clip:CreateTexture(nil, "OVERLAY", nil, 3)
        tex:SetPoint("TOPLEFT", box, "TOPLEFT")
        tex:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT")
        tex:SetColorTexture(unpack(SWEEP_COLOR))
        tex:SetBlendMode("BLEND")

        clip:SetScript("OnUpdate", function(self)
            local now = GetTime() * 1000
            local s = self._castStartMS or 0
            local e = self._castEndMS or 0
            local dur = e - s
            if dur <= 0 then self:Hide(); return end
            local progress = (now - s) / dur
            if progress >= 1 then self:Hide(); return end
            if progress < 0 then progress = 0 end
            local w = box:GetWidth() * progress
            self:SetWidth(math_max(1, w))
        end)
        box._muiSweep = clip
        return clip
    end

    local function PlaySweep(box)
        local name, _, _, startMS, endMS = UnitCastingInfo("player")
        if not name or not startMS or not endMS then
            name, _, _, startMS, endMS = UnitChannelInfo("player")
        end
        if not startMS or not endMS or endMS <= startMS then return end
        local sweep = EnsureSweep(box)
        sweep._castStartMS = startMS
        sweep._castEndMS = endMS
        sweep:SetWidth(1)
        sweep:Show()
    end

    -- Listen to cast events instead of polling Blizzard's overlay.
    local sweepFrame = CreateFrame("Frame")
    sweepFrame:RegisterEvent("UNIT_SPELLCAST_START")
    sweepFrame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")
    sweepFrame:SetScript("OnEvent", function(_, _, unit)
        if unit ~= "player" then return end
        for barNum = 1, 8 do
            local prefix = BAR_BUTTON_MAP[barNum]
            if prefix then
                for i = 1, 12 do
                    local button = _G[prefix .. i]
                    if button and button:IsVisible() and button._muiBox
                        and button.action and IsCurrentAction(button.action) then
                        PlaySweep(button._muiBox)
                    end
                end
            end
        end
    end)
end

-- ============================================================
-- SECTION U: Proc Glow
-- Polled detection of Blizzard's SpellActivationAlert (no reliable
-- hookable event exists in modern retail; polling is intentional).
-- ============================================================

do
    local GLOW_R, GLOW_G, GLOW_B = 1.00, 0.82, 0.30
    local BLOOM_OVERSHOOT  = 12
    local BORDER_THICKNESS = 3
    local BREATHE_SPEED    = 2.2
    local BREATHE_MIN      = 0.35
    local BREATHE_MAX      = 0.85
    local SWEEP_PERIOD     = 2.4
    local SWEEP_HOLD       = 0.55
    local SWEEP_WIDTH_FRAC = 0.38

    local function EnsureProcGlow(box)
        if box._muiProcGlow then return box._muiProcGlow end

        local f = CreateFrame("Frame", nil, box)
        f:SetFrameLevel(box:GetFrameLevel() + 4)
        f:SetAllPoints(box)
        f:Hide()

        local bloom = f:CreateTexture(nil, "BACKGROUND", nil, 7)
        bloom:SetPoint("CENTER")
        local bSize = box:GetWidth() + BLOOM_OVERSHOOT * 2
        bloom:SetSize(bSize, bSize)
        bloom:SetTexture("Interface\\GLUES\\MODELS\\UI_MainMenu\\BlizzShadow")
        bloom:SetBlendMode("ADD")
        bloom:SetVertexColor(GLOW_R, GLOW_G, GLOW_B, BREATHE_MAX)
        f.bloom = bloom

        local function MakeStrip(p1, r1, p2, r2, w, h)
            local s = f:CreateTexture(nil, "OVERLAY", nil, 3)
            s:SetPoint(p1, box, r1, 0, 0)
            s:SetPoint(p2, box, r2, 0, 0)
            s:SetSize(w, h)
            s:SetColorTexture(GLOW_R, GLOW_G, GLOW_B, 1)
            s:SetBlendMode("ADD")
            return s
        end

        local bw = box:GetWidth()
        local bh = box:GetHeight()
        f.stripTop    = MakeStrip("TOPLEFT", "TOPLEFT", "TOPRIGHT", "TOPRIGHT", bw, BORDER_THICKNESS)
        f.stripBottom = MakeStrip("BOTTOMLEFT", "BOTTOMLEFT", "BOTTOMRIGHT", "BOTTOMRIGHT", bw, BORDER_THICKNESS)
        f.stripLeft   = MakeStrip("TOPLEFT", "TOPLEFT", "BOTTOMLEFT", "BOTTOMLEFT", BORDER_THICKNESS, bh)
        f.stripRight  = MakeStrip("TOPRIGHT", "TOPRIGHT", "BOTTOMRIGHT", "BOTTOMRIGHT", BORDER_THICKNESS, bh)

        local sweepClip = CreateFrame("Frame", nil, f)
        sweepClip:SetPoint("TOPLEFT", box, "TOPLEFT", 1, -1)
        sweepClip:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -1, 1)
        sweepClip:SetClipsChildren(true)
        f.sweepClip = sweepClip

        local sweep = sweepClip:CreateTexture(nil, "OVERLAY", nil, 4)
        sweep:SetSize(bw * SWEEP_WIDTH_FRAC, bh + 20)
        sweep:SetTexture("Interface\\Buttons\\WHITE8X8")
        sweep:SetBlendMode("ADD")
        sweep:SetVertexColor(1, 0.96, 0.80, 0.22)
        sweep:SetRotation(math.rad(25))
        f.sweep = sweep

        f._phase = 0
        f:SetScript("OnUpdate", function(self, dt)
            self._phase = self._phase + dt
            local breathe = BREATHE_MIN + (BREATHE_MAX - BREATHE_MIN)
                * (0.5 + 0.5 * math.sin(self._phase * BREATHE_SPEED * 2 * math.pi))
            self.bloom:SetAlpha(breathe)

            local sp1 = 0.5 + 0.5 * math.sin(self._phase * BREATHE_SPEED * 2.6 * math.pi)
            local sp2 = 0.5 + 0.5 * math.sin(self._phase * BREATHE_SPEED * 2.6 * math.pi + 1.2)
            self.stripTop:SetAlpha(0.6 + 0.4 * sp1)
            self.stripBottom:SetAlpha(0.6 + 0.4 * sp1)
            self.stripLeft:SetAlpha(0.6 + 0.4 * sp2)
            self.stripRight:SetAlpha(0.6 + 0.4 * sp2)

            local cyc = self._phase % SWEEP_PERIOD
            if cyc < SWEEP_HOLD then
                local progress = cyc / SWEEP_HOLD
                local boxW = self.sweepClip:GetWidth()
                local sweepW = self.sweep:GetWidth()
                local startX = -sweepW
                local endX = boxW + sweepW
                local x = startX + (endX - startX) * progress
                self.sweep:ClearAllPoints()
                self.sweep:SetPoint("CENTER", self.sweepClip, "LEFT", x, 0)
                self.sweep:SetAlpha(0.22 * math.sin(progress * math.pi))
            else
                self.sweep:SetAlpha(0)
            end
        end)

        box._muiProcGlow = f
        return f
    end

    local function ShowProcGlow(button)
        if not button or not button._muiBox then return end
        local glow = EnsureProcGlow(button._muiBox)
        glow._phase = 0
        glow:Show()
    end

    local function HideProcGlow(button)
        if not button or not button._muiBox then return end
        if button._muiBox._muiProcGlow then
            button._muiBox._muiProcGlow:Hide()
        end
    end

    -- Poll Blizzard's SpellActivationAlert visibility at 0.1s.
    -- The native flipbook glow has no reliable hook in modern retail.
    local pollFrame = CreateFrame("Frame")
    local pollElapsed = 0
    pollFrame:SetScript("OnUpdate", function(_, dt)
        pollElapsed = pollElapsed + dt
        if pollElapsed < 0.1 then return end
        pollElapsed = 0

        for barNum = 1, 8 do
            local prefix = BAR_BUTTON_MAP[barNum]
            if prefix then
                for i = 1, 12 do
                    local button = _G[prefix .. i]
                    if button and button._muiBox then
                        local box = button._muiBox
                        local saa = button.SpellActivationAlert
                        local visible = saa and saa.IsShown and saa:IsShown()
                        local wasActive = box._muiProcGlowActive == true
                        if visible and not wasActive then
                            box._muiProcGlowActive = true
                            saa:SetAlpha(0)
                            ShowProcGlow(button)
                        elseif not visible and wasActive then
                            box._muiProcGlowActive = false
                            HideProcGlow(button)
                            if saa then saa:SetAlpha(1) end
                        elseif visible and wasActive then
                            if saa:GetAlpha() > 0 then saa:SetAlpha(0) end
                        end
                    end
                end
            end
        end
    end)
end

-- ============================================================
-- SECTION V: Overlay Settings Builders (right-click popups)
-- ============================================================

local function BuildActionBarOverlaySettings(content, key)
    if not _G.MidnightUI_CreateOverlayBuilder then return end
    local barNum = tonumber(string.match(tostring(key), "^ActionBar(%d+)$"))
    if not barNum then return end
    local settings = _G.MidnightUISettings
    if not settings then return end
    if not settings.ActionBars then settings.ActionBars = {} end
    settings.ActionBars["bar" .. barNum] = settings.ActionBars["bar" .. barNum] or {}
    local s = settings.ActionBars["bar" .. barNum]

    local BAR_BUTTONS = 12
    local curRows = math_max(1, math_floor(tonumber(s.rows) or 1))
    local curIcons = math_max(1, math_floor(tonumber(s.iconsPerRow) or BAR_BUTTONS))
    if curRows * curIcons > BAR_BUTTONS then
        curRows = math_max(1, math_floor(BAR_BUTTONS / curIcons))
    end
    s.rows = curRows
    s.iconsPerRow = curIcons

    local b = _G.MidnightUI_CreateOverlayBuilder(content, { startY = -6 })
    b:Header("Settings")
    b:Checkbox("Enable This Bar", s.enabled ~= false, function(v)
        s.enabled = v
        _G.MyActionBars_ReloadSettingsImmediate()
    end)

    local rowsSlider, iconsSlider
    rowsSlider = b:Slider("Rows", 1, BAR_BUTTONS, 1, curRows, function(v)
        local r = math_max(1, math_min(BAR_BUTTONS, math_floor(v)))
        s.rows = r
        local fit = math_max(1, math_floor(BAR_BUTTONS / r))
        s.iconsPerRow = fit
        if iconsSlider then iconsSlider:SetValue(fit) end
        _G.MyActionBars_ReloadSettingsImmediate()
    end)
    iconsSlider = b:Slider("Icons Per Row", 1, BAR_BUTTONS, 1, curIcons, function(v)
        local ic = math_max(1, math_min(BAR_BUTTONS, math_floor(v)))
        s.iconsPerRow = ic
        local fit = math_max(1, math_floor(BAR_BUTTONS / ic))
        s.rows = fit
        if rowsSlider then rowsSlider:SetValue(fit) end
        _G.MyActionBars_ReloadSettingsImmediate()
    end)
    b:Dropdown("Per-Bar Style", {"Class Color", "Faithful", "Glass", "Hidden"},
        s.style or "Class Color", function(v)
        s.style = v
        if settings.ActionBars.globalStyle and settings.ActionBars.globalStyle ~= "Disabled" then
            settings.ActionBars.globalStyle = "Disabled"
        end
        _G.MyActionBars_ReloadSettingsImmediate()
    end)
    b:Slider("Scale %", 50, 200, 5, s.scale or 100, function(v)
        s.scale = math_floor(v)
        _G.MyActionBars_ReloadSettingsImmediate()
    end)
    b:Slider("Spacing", 0, 30, 1, s.spacing or 6, function(v)
        s.spacing = math_floor(v)
        _G.MyActionBars_ReloadSettingsImmediate()
    end)
    return b:Height()
end

local function BuildPetBarOverlaySettings(content)
    if not _G.MidnightUI_CreateOverlayBuilder then return end
    local settings = _G.MidnightUISettings
    if not settings then return end
    if not settings.PetBar then settings.PetBar = {} end
    local s = settings.PetBar

    local b = _G.MidnightUI_CreateOverlayBuilder(content, { startY = -6 })
    b:Header("Settings")
    b:Checkbox("Enable Pet Bar", s.enabled ~= false, function(v)
        s.enabled = v; _G.MyActionBars_ReloadSettingsImmediate()
    end)
    b:Slider("Scale %", 50, 200, 5, s.scale or 100, function(v)
        s.scale = math_floor(v); _G.MyActionBars_ReloadSettingsImmediate()
    end)
    b:Slider("Opacity", 0.1, 1.0, 0.05, s.alpha or 1.0, function(v)
        s.alpha = v; _G.MyActionBars_ReloadSettingsImmediate()
    end)
    b:Slider("Button Size", 20, 56, 1, s.buttonSize or 32, function(v)
        s.buttonSize = math_floor(v); _G.MyActionBars_ReloadSettingsImmediate()
    end)
    b:Slider("Spacing", 0, 30, 1, s.spacing or 15, function(v)
        s.spacing = math_floor(v); _G.MyActionBars_ReloadSettingsImmediate()
    end)
    b:Slider("Per Row", 1, 10, 1, s.buttonsPerRow or 10, function(v)
        s.buttonsPerRow = math_floor(v); _G.MyActionBars_ReloadSettingsImmediate()
    end)
    return b:Height()
end

local function BuildStanceBarOverlaySettings(content)
    if not _G.MidnightUI_CreateOverlayBuilder then return end
    local settings = _G.MidnightUISettings
    if not settings then return end
    if not settings.StanceBar then settings.StanceBar = {} end
    local s = settings.StanceBar

    local b = _G.MidnightUI_CreateOverlayBuilder(content, { startY = -6 })
    b:Header("Settings")
    b:Checkbox("Enable Stance Bar", s.enabled ~= false, function(v)
        s.enabled = v; _G.MyActionBars_ReloadSettingsImmediate()
    end)
    b:Slider("Scale %", 50, 200, 5, s.scale or 100, function(v)
        s.scale = math_floor(v); _G.MyActionBars_ReloadSettingsImmediate()
    end)
    b:Slider("Opacity", 0.1, 1.0, 0.05, s.alpha or 1.0, function(v)
        s.alpha = v; _G.MyActionBars_ReloadSettingsImmediate()
    end)
    b:Slider("Button Size", 20, 56, 1, s.buttonSize or 32, function(v)
        s.buttonSize = math_floor(v); _G.MyActionBars_ReloadSettingsImmediate()
    end)
    b:Slider("Spacing", -6, 30, 1, s.spacing or 4, function(v)
        s.spacing = math_floor(v); _G.MyActionBars_ReloadSettingsImmediate()
    end)
    b:Slider("Per Row", 1, 4, 1, s.buttonsPerRow or 3, function(v)
        s.buttonsPerRow = math_floor(v); _G.MyActionBars_ReloadSettingsImmediate()
    end)
    return b:Height()
end

if _G.MidnightUI_RegisterOverlaySettings then
    for i = 1, 8 do
        _G.MidnightUI_RegisterOverlaySettings("ActionBar" .. i,
            { title = "Action Bar " .. i, build = BuildActionBarOverlaySettings })
    end
    _G.MidnightUI_RegisterOverlaySettings("PetBar",
        { title = "Pet Bar", build = BuildPetBarOverlaySettings })
    _G.MidnightUI_RegisterOverlaySettings("StanceBar",
        { title = "Stance Bar", build = BuildStanceBarOverlaySettings })
end

-- ============================================================
-- SECTION W: Public API
-- ============================================================

function MyActionBars_ReloadSettings()
    if IsInCombat() then
        Defer("ReloadSettings", function() Rebuild("all") end)
        return
    end
    C_Timer.After(0.1, function() Rebuild("all") end)
end

function MyActionBars_ReloadSettingsImmediate()
    if IsInCombat() then
        Defer("ReloadSettingsImm", function() Rebuild("all") end)
        return
    end
    Rebuild("all")
end

function MyActionBars_ResetToDefaults()
    local settings = _G.MidnightUISettings
    if not settings or not settings.ActionBars then return end
    for n = 1, 8 do
        local key = "bar" .. n
        if settings.ActionBars[key] then
            settings.ActionBars[key].position = nil
        end
        local MR = _G.MidnightUI_MoverRegistry
        if MR and MR.ResetPosition then
            MR.ResetPosition("ActionBar" .. n)
        end
    end
    if _G.MidnightUI_MoverRegistry and _G.MidnightUI_MoverRegistry.ResetPosition then
        _G.MidnightUI_MoverRegistry.ResetPosition("PetBar")
        _G.MidnightUI_MoverRegistry.ResetPosition("StanceBar")
    end
    Rebuild("all")
end

-- ============================================================
-- SECTION X: Event Handler
-- Single registered frame dispatches every event to the right
-- builder. No tickers, no repeating polls for Blizzard art --
-- events that can cause re-shows are listened to explicitly.
-- ============================================================

ActionBarManager:RegisterEvent("ADDON_LOADED")
ActionBarManager:RegisterEvent("PLAYER_ENTERING_WORLD")
ActionBarManager:RegisterEvent("PET_BAR_UPDATE")
ActionBarManager:RegisterEvent("UNIT_PET")
ActionBarManager:RegisterEvent("UPDATE_SHAPESHIFT_FORMS")
ActionBarManager:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
ActionBarManager:RegisterEvent("UPDATE_BINDINGS")
ActionBarManager:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
ActionBarManager:RegisterEvent("EDIT_MODE_LAYOUTS_UPDATED")
ActionBarManager:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
ActionBarManager:RegisterEvent("UPDATE_BONUS_ACTIONBAR")
ActionBarManager:RegisterEvent("UNIT_ENTERED_VEHICLE")
ActionBarManager:RegisterEvent("UNIT_EXITED_VEHICLE")
ActionBarManager:RegisterEvent("PLAYER_TALENT_UPDATE")
ActionBarManager:RegisterEvent("CURSOR_CHANGED")

ActionBarManager:SetScript("OnEvent", function(self, event, arg1)
    if event == "ADDON_LOADED" and arg1 == ADDON_NAME then
        C_Timer.After(0.1, ApplySuppression)
        return
    end

    if event == "PLAYER_REGEN_ENABLED" then
        FlushDeferred()
        return
    end

    if event == "PLAYER_ENTERING_WORLD" then
        if not initialized then
            initialized = true
            C_Timer.After(0.2, function()
                Rebuild("all")
                InstallTutorialHook()
            end)
        else
            -- Subsequent zone loads: re-apply mute (Blizzard re-shows
            -- some art on zone transitions) plus a one-shot at +1s
            -- to catch late frame creation.
            ApplySuppression()
            C_Timer.After(1, ApplySuppression)
        end
        return
    end

    if event == "PET_BAR_UPDATE" or event == "UNIT_PET" then
        Rebuild("pet")
        return
    end

    if event == "UPDATE_SHAPESHIFT_FORMS" or event == "UPDATE_SHAPESHIFT_FORM" then
        Rebuild("stance")
        return
    end

    if event == "UPDATE_BINDINGS" or event == "ACTIONBAR_SLOT_CHANGED" then
        if initialized then RefreshAllHotkeyText() end
        return
    end

    if event == "EDIT_MODE_LAYOUTS_UPDATED"
       or event == "PLAYER_SPECIALIZATION_CHANGED"
       or event == "UPDATE_BONUS_ACTIONBAR"
       or event == "UNIT_ENTERED_VEHICLE"
       or event == "UNIT_EXITED_VEHICLE"
       or event == "PLAYER_TALENT_UPDATE" then
        ApplySuppression()
        return
    end

    if event == "CURSOR_CHANGED" then
        CheckCursorForDrag()
        return
    end
end)
